Skip to content

refactor(models): centralize router Pydantic models in models.py (INV-14, #654)#1308

Open
AndriiPasternak31 wants to merge 7 commits into
devfrom
AndriiPasternak31/autoplan-issue-654
Open

refactor(models): centralize router Pydantic models in models.py (INV-14, #654)#1308
AndriiPasternak31 wants to merge 7 commits into
devfrom
AndriiPasternak31/autoplan-issue-654

Conversation

@AndriiPasternak31

@AndriiPasternak31 AndriiPasternak31 commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

What

Clears Architectural Invariant #14 (Pydantic models centralized in models.py) for issue #654: moves 97 request/response BaseModel classes out of 32 routers into src/backend/models.py, plus the fan_out / loops / webhooks module-level constants their validators reference. Each class moved verbatim — validators, default_factory, and Config preserved.

#654 was a stale validate-architecture drift report flagging INV-7 / INV-8 / INV-14. Scope (confirmed in plan review): this PR clears INV-14 only (backend, mechanical) + applies the D1/D2 doc fixes. INV-7 (frontend api.js) and INV-8 (auth sprawl) are split into separate child issues — they're owned by other areas and need behavioral/security work.

Why flat models.py (not a models/ package)

A package dir would break two by-file mechanisms invisible to source-only CI (the #1033 class): tests/conftest.py's by-file model preload, and the Dockerfile's by-file COPY *.py. Flat keeps both working. Scope is router models only — db_models.py (95) and adapters/base.py (4) are intentional separate homes.

Behaviour preserved — two proofs

The move is mechanically simple but not purely build-time: a careless cut-paste can change runtime validation while imports + schema still pass. Both gaps are covered:

  1. Normalized app.openapi() snapshot difforigin/dev vs branch: 330 paths / 198 schemas, byte-for-byte identical. Schema-level proof.
  2. Validator-preservation unit tests (tests/unit/test_router_model_validators.py) — the @field_validator logic, default_factory dicts, and Config.from_attributes that an OpenAPI diff cannot see, for the 5 behaviour-bearing files (fan_out, loops, canary, audit_log, schedules).

One documented exception (allowlisted in the guard)

canary.py::RunCycleRequest stays in its router: it evaluates INVARIANTS (from the canary library) in a Field(description=…) at class-definition time, and the canary library imports TaskExecutionStatus back from models. Relocating it would force models.py to from canary import …, inverting the dependency direction of a module meant to be a low-level leaf everything imports from. The new static guard allowlists exactly this one model (and fails if the allowlist goes stale).

Tests / docs

Verification

  • python -c "import main" — clean.
  • Full tests/unit suite: only the two pre-existing failure clusters remain (test_1115_schedules_summary, test_login_rate_limit_split — both confirmed failing identically on pristine origin/dev). Zero new failures; +23 passing tests (19 new + 4 telegram fixed).
  • ruff: error breakdown byte-identical to origin/dev (zero new lint).
  • verify-local is optional here — flat move, no new module dir, no dep/Docker change (models.py is already in the COPY *.py).

🤖 Generated with Claude Code


Closes #654 (narrowed to INV-14 + docs). INV-7 → #1309, INV-8 → #1310.

@github-actions

Copy link
Copy Markdown

⚠️ Nightly unit-suite check skipped — merge conflict against dev.

Resolve by running git merge dev locally and pushing the result. The next nightly run will re-test once the conflict is gone.

…-14, #654)

Move 97 request/response BaseModel classes from 32 routers into
src/backend/models.py (Architectural Invariant #14), plus the fan_out /
loops / webhooks module-level constants their validators reference. Each
class moved verbatim — validators, default_factory, and Config preserved.

Behaviour preserved, proven two ways the move could silently break it:
- Normalized app.openapi() snapshot diff: 330 paths / 198 schemas,
  byte-identical origin/dev vs branch (schema-level proof).
- Validator-preservation unit tests for the 5 behaviour-bearing files
  (fan_out, loops, canary, audit_log, schedules) — the field_validator
  logic, default_factory dicts, and Config.from_attributes an OpenAPI
  diff can't see.

One documented exception, allowlisted in the new static guard:
canary.py::RunCycleRequest evaluates INVARIANTS (the canary library) in a
Field(description=...) at class-definition time, and the canary library
imports TaskExecutionStatus back from models — relocating it would force
models.py to `from canary import ...`, inverting the dependency direction
of a module meant to be a low-level leaf everything imports from.

Flat models.py (not a package): a models/ dir would break
tests/conftest.py's by-file model preload and the Dockerfile's by-file
COPY *.py (the #1033 crash-loop class). Scope is router models only —
db_models.py and adapters/base.py are intentional separate homes.

New / changed:
- tests/unit/test_models_centralized.py: static guard (no BaseModel under
  routers/ except the documented allowlist) + planted-violation tests
- tests/unit/test_router_model_validators.py: validator / default_factory
  / Config preservation negatives
- tests/unit/test_telegram_webhook_backfill.py: load the real models
  module in the standalone settings.py stub (was already red on dev) and
  stub the #1197 services.agent_service.capabilities import -> green
- docs/memory/architecture.md: Invariant #14 scope note + re-measured
  router/service/agents counts

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@AndriiPasternak31 AndriiPasternak31 force-pushed the AndriiPasternak31/autoplan-issue-654 branch from 38d28e3 to a6da83f Compare June 23, 2026 21:43
- feature-flows.md: Recent Updates row for the router-model centralization
  (Invariant #14), noting behavior-preserved / OpenAPI-byte-identical and the
  documented canary.RunCycleRequest allowlist exception.
- security-reports: CSO --diff report for PR #1308 (37 files); zero findings
  across all categories.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@AndriiPasternak31 AndriiPasternak31 marked this pull request as ready for review June 23, 2026 21:51
@AndriiPasternak31 AndriiPasternak31 requested review from dolho and vybe June 23, 2026 21:51
INV-14 centralized EmailStr-bearing models (SendMessageRequest, etc.) into
models.py, which the db layer imports non-tolerantly (db/schedules.py ->
`from models import TaskExecutionStatus`). Building those schemas at import
time now requires email-validator, so the unit-CI job failed at conftest
collection: "ImportError: email-validator is not installed". The backend
Docker image already installs pydantic[email]>=2.10.4; tests/requirements-test.txt
had bare pydantic. Align the test env with production.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@vybe vybe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Validated via /validate-pr — clean refactor (CSO verdict CLEAR, behavior-preserving, dedicated centralization tests, Closes #654). Blocking only on a rebase: this is now CONFLICTING with dev after #1333 and #1317 merged, both of which added new models:

  • #1333 — cancelled-status models in routers/agents.py / chat.py / paid.py / public.py + models.py
  • #1317 — access models in routers/sharing.py

Please rebase onto current dev and relocate those newly-added models into models.py so INV-14 centralization stays complete. Otherwise good to go.

— posted via /validate-pr

AndriiPasternak31 and others added 2 commits June 26, 2026 00:22
…plan-issue-654

# Conflicts:
#	src/backend/routers/sharing.py
#	src/backend/routers/voice.py
#	src/backend/routers/voip.py
…(INV-14, #654)

The INV-14 centralization moved router-defined models into models.py,
which left three flow docs citing a routers/X.py home this PR emptied:

- scheduling.md: ScheduleUpdateRequest routers/schedules.py:76-86 -> models.py:1574
- audit-trail.md: fix wrong invariant number (#15 -> #14) and the now-inverted
  claim "response models in routers/audit_log.py" -> models.py:947-982
- business-validation.md: Schedule/execution response models routers/schedules.py
  -> models.py

Citation-only; no behavioral or model-shape change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@AndriiPasternak31

Copy link
Copy Markdown
Contributor Author

@vybe — rebase done + ready for re-review.

Conflict resolved: merged current dev (5d6cd26). The models added to dev after your review are relocated into models.py, and all conflict-touched routers now have zero inline BaseModel:

mergeable = MERGEABLE; full CI green (pytest base+head × 3 seeds, schema-parity, prod-image-smoke, regression-diff, verify-non-root, CodeQL).

Plus one follow-up commit (fa7b617): /sync-feature-flows caught three flow docs still citing a routers/X.py model home this PR emptied — repointed to models.py (citation-only, no behavioral change):

Could you re-review to clear the stale CHANGES_REQUESTED?

@AndriiPasternak31 AndriiPasternak31 requested a review from vybe June 25, 2026 23:54
AndriiPasternak31 and others added 2 commits June 26, 2026 10:09
…plan-issue-654

Resolve conflicts from #1084 (effect-scoped idempotency) and #1131:
- messages.py / voip.py: keep INV-14 centralization (inline models removed),
  carry #1084's new execution_id + dedup_label fields into models.py'
  SendMessageRequest and VoipCallRequest so idempotency is preserved.
- feature-flows.md: keep both new Recent Updates rows (#1131, #654).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolves the routers/loops.py conflict from #1182 (loop max_duration_seconds)
landing on dev while this branch centralizes the loop models into models.py
(INV-14). Semantic resolution:

- models.py: add MAX_DURATION_SECONDS + StartLoopRequest.max_duration_seconds
  and LoopStatusResponse.max_duration_seconds/elapsed_seconds (the field #1182
  added to the loop models, which this branch had already moved here without it
  — git could not flag this since dev never had the models in models.py).
- routers/loops.py: keep the models centralized (deleted block), retain #1182's
  endpoint deadline validation + _elapsed_seconds, and define the router-only
  DEFAULT_PER_RUN_TIMEOUT constant the validation needs.

Verified: no markers, py_compile clean, router defines 0 BaseModel, model
field bounds/validators exercised in isolation.

Co-Authored-By: Claude Opus 4.8 (1M context) <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