Files
roboco/docs/optional/conventions.md
T
Renn F 17ec52d1b7 feat(conventions): generalize defaults, backfill old projects, adopt the standard in-repo
Harden the architectural-conventions standard so it works out-of-the-box on
any project and resolves for projects that predate it, and make RoboCo pass
its own gate.

General defaults (apply to every project, not just one with a tuned file):
- The auto-scan excludes test and documentation trees (tests/, docs/) — those
  legitimately define fixtures and aren't enforced code.
- Helper placement seeds at warn, not block: `helper` matches any top-level
  function, too blunt a signal to hard-block a route file's small private glue.
  Misplaced model/route/component stay block; the body-level thin_routes check
  remains the real fat-handler guard.
- thin_routes no longer counts transaction-lifecycle calls (commit/flush/
  refresh) as data access — an explicit `db.commit()` after delegating to a
  service is a valid pattern.
- no_lint_suppressions exempts a small allowlist of structurally-unavoidable
  framework codes (ruff TC001-TC003, pydantic prop-decorator); bare or other
  suppressions still flag.
- CLAUDE.md rule-lifting skips bare common-word tokens that would match
  everywhere (e.g. "commit"), keeping only specific identifiers.
- The ambient prompt block lists only constrained modules and truncates at a
  line boundary with a "+N more" pointer instead of cutting mid-line.

Backfill: the standard previously read the committed file + repo scan from
project.workspace_path, a field only a manual API call set — so an older
project (or one whose workspace was cleared) showed an empty "missing" map no
matter what was pushed. The service now ensures a dedicated, default-branch
read clone on demand (WorkspaceService.ensure_read_clone) and resolves from
it, persisting the resolved path + real HEAD. The panel tab, the spawn-time
ambient block, and the per-task constraints all resolve the committed standard
with no manual setup.

Adopt in-repo: relocate the inline request/response models from the system and
*_live route modules into roboco/api/schemas/ so the codebase passes its own
placement gate, and ship a canonical .roboco/conventions.yml. no_models_in_routes
and modular_cohesion are now clean and enforced at block.

Docs updated across the user guide, the agent-facing RAG standard, the
developer and pr_reviewer role prompts, CLAUDE.md, and the changelog. New unit
tests cover the scan exclusions, helper-warn, the suppression allowlist, the
commit exemption, and the resolve/backfill path; the conventions + project
integration suites pass against Postgres.
2026-06-22 18:15:19 +02:00

11 KiB
Raw Blame History

Architectural Conventions Standard

The make-style gates you configure per project (ruff, mypy, eslint, your test suite) answer one question: is the code valid? They cannot answer a second one a senior reviewer would catch on sight: does this code live where it belongs? The Architectural Conventions Standard is RoboCo's answer to that second question. It gives each project a repo-canonical architecture map and a tree-sitter validator that hard-gates an agent from landing a model defined inside a router, a route handler that runs its own SQL, a React component that fetches its own data, or a # noqa / # type: ignore that quietly silences the linter.

It is off by default behind ROBOCO_CONVENTIONS_ENABLED. Every hook — the auto-scaffold, the prompt injection, the per-task constraints, and the gates — is fully inert until you turn it on.

What it does

When enabled, the standard reaches the work in two directions. It guides agents up front — an "Architectural Standard" block is injected into every agent's prompt at spawn, and a ## Constraints section listing the project's block-level rules and module boundaries is auto-attached to every task. And it enforces the same rules at the gate — a block-level finding refuses i_am_done (the developer's pre-submit) and pr_pass (the in-path PR gate) with the offending file:line and a fix hint, while QA sees the findings in its review evidence.

The rules live in a per-project .roboco/conventions.yml with four curated parts plus a set of structural checks:

Part What it is
Module map Path prefixes mapped to a human purpose and the definition kinds forbidden there (model, route, helper, business_logic, component). "routers/ is for HTTP routes — no models, no helpers."
Rules A toggleable rule set. Each rule fires at warn (advisory, never blocks) or block (refuses the gate).
Custom rules Project-specific regex rules — a pattern, a message, and a level, optionally scoped to languages.
Waivers Accountable per-(path, rule) escape hatches with a written reason — the sanctioned way to relieve a false positive, reviewed in the PR.

Placement, hygiene, and modularity checks

The validator runs four check families over each changed file:

  • Placement — a definition whose kind is forbidden in its module (a model in a router). A misplaced model, route, or component is block by default; a misplaced helper only warns — helper matches any top-level function, too blunt a signal to hard-block a route file's small private glue (the body-level thin_routes check is the real fat-handler guard).
  • Hygiene — the universal, stack-agnostic house-style rules seeded into every project: no_lint_suppressions (block by default — no # noqa, # type: ignore, eslint-disable) and no_inline_comments (warn). A small allowlist of structurally unavoidable framework codes is exempt from no_lint_suppressions — ruff's flake8-type-checking codes (TC001TC003, for an import a framework needs at runtime) and pydantic's prop-decorator — so the rule keeps its teeth on genuine error-silencing without footgunning every pydantic/FastAPI project. A bare # noqa / # type: ignore or any other code is still flagged.
  • Custom — your project-specific regex rules.
  • Modularity — separation-of-concerns judgements the linters are blind to, inspecting a file's composition and a definition's body, not just its top-level kind:
Rule Fires when Default level
modular_cohesion One file mixes architectural concerns (e.g. a model and a route and a component) block
thin_routes A route handler does its own data access (SQLAlchemy execute/scalars/add/select…) instead of delegating to a service. Transaction-lifecycle calls (commit/flush/refresh) do not count — an explicit await db.commit() after delegating is a valid pattern block
thin_components A React component fetches data in its body instead of through a hook block
god_class A class grows past 15 methods (single-responsibility smell) warn

!!! info "Precision over recall" Every check fires only on a confident, structural signal, and abstains when it is uncertain — so a block-level gate is never tripped by a guess. If the validator genuinely cannot run on a diff (a parse or grammar error), it is fail-loud: it exits non-zero and the gate blocks rather than passing silently.

The effective map: defaults, present, absent, or partial

Consumers never read the raw committed file — they read the effective map, so behaviour is identical whether .roboco/conventions.yml is present, absent, or partial. ConventionsService (roboco/services/conventions.py) builds it by auto-deriving a baseline from a repo scan (it infers modules from directory names like routers/, models/, services/, components/, hooks/; excludes test and documentation treestests/, docs/ — since those legitimately define fixtures and aren't enforced code; detects languages by file extension; seeds the universal hygiene rules) and then overlaying the committed file on top. The result is cached per (project, HEAD sha).

That is the load-bearing property: the standard is enforced even before any file is committed. A project with no .roboco/conventions.yml still gets sensible auto-derived rules and is gated by them. Resilience is built in — a missing file degrades to the auto-derived defaults, and an unparseable file falls back to the last-good cached map (status degraded) so the standard is never silently switched off by a typo.

!!! info "Reads come from a dedicated clone — no setup needed for old projects" The committed file and the repo scan are read from a project-level read clone that the service ensures on demand (pinned to the default branch's HEAD), not from any agent's working clone. This is the backfill: a project created long before the standard existed — with no manually-configured workspace path — still resolves its committed .roboco/conventions.yml the first time the panel, a spawn, or a task asks for it. There is nothing to wire up.

The per-project Conventions editor

Each project carries a Conventions tab in its edit dialog (panel component panel/src/components/conventions/conventions-tab.tsx). From there you manage the whole standard without hand-editing YAML:

  • Module boundaries — add/remove modules, set each one's path and purpose, and click a kind badge (no model, no route, no helper, no business_logic, no component) to toggle whether it is forbidden there.
  • Rules — flip each rule between warn and block with a switch.
  • Custom rules — add a regex id/pattern/message and set its level.
  • Waivers — exempt a file from a rule with a written reason.
  • Recent violations — the latest findings recorded across this project's tasks, each tagged with its rule and level.

Two state banners keep you honest about what is in force:

!!! note "Using auto-derived defaults" When no file is committed yet, the tab shows a neutral "Using auto-derived defaults" banner — not an error. These rules are already enforced. The Save button reads "Save defaults to repo": one click backfills the canonical file. (The earlier read-only, "missing"-as-broken behaviour is gone — this is the full editor with one-click backfill.)

!!! warning "Conventions degraded" If a committed file won't parse, an amber "Conventions degraded" banner appears. The effective map has fallen back to the last-good cache plus defaults; Restore from last-good re-commits the previous working file.

Save and Restore open a PR

The standard is repo-canonical, so the editor never edits files behind your back. Save to repo and Restore from last-good each open a pull request against the project (via GitService.open_conventions_pr) carrying the new .roboco/conventions.yml. You review and merge it like any other change. If the project's workspace isn't cloned yet, the change is prepared on the branch and the toast tells you no remote PR was opened.

How agents are hard-gated

flowchart TD
    A[Developer finishes work] --> B[i_am_done]
    B --> C{conventions validator<br/>on changed files}
    C -->|block finding<br/>or could-not-run| D[REJECTED<br/>file:line + fix hint]
    C -->|warn-only / clean| E[proceeds to QA]
    D --> F[Dev fixes or commits a waiver]
    F --> B
    E --> G[QA sees findings in claim_review evidence]
    G --> H[PR assembled]
    H --> I[pr_pass at the in-path gate]
    I --> J{same validator}
    J -->|block / could-not-run| K[REJECTED back to needs_revision]
    J -->|clean| L[PM merges]

Enforcement is deterministic and lives at two gateway choke points (roboco/services/gateway/choreographer/):

  • i_am_done — the developer's pre-submit. A block-level finding refuses the verb with the offending file:line and a fix hint; the findings are also persisted to the project's violations feed (even the ones that blocked).
  • pr_pass — the in-path PR-review gate. The same validator runs on the assembled PR's changed files; a block finding fails it back to needs_revision.
  • QA evidence — when QA calls claim_review, the convention findings ride along in its review briefing so the human-style review sees them too.

A false positive is relieved one way only: the developer commits a waiver in their branch — an accountable (path, rule, reason) entry reviewed in the PR — never a silent in-code # noqa.

Enable it

You can turn the standard on two ways; the effect is the same.

=== "Panel"

Open **Settings → Feature Flags** and toggle **"Enforce a per-project architectural standard"** on.

!!! note "Takes effect on the next backend restart"
    A feature-flag toggle persists in the settings store and is applied on the **next backend restart** — it is not a hot reload. Flip it, then restart the orchestrator.

=== "Environment"

Set the flag in your compose/env and restart:

```bash
ROBOCO_CONVENTIONS_ENABLED=true
```

An unset flag falls back to its config default (off). See the environment reference for the full flag list.

What changes when it's on

  • Every agent's prompt carries the project's "Architectural Standard" block, and every project task gets an auto-attached ## Constraints section.
  • i_am_done and pr_pass block on block-level findings; QA sees findings in its review evidence.
  • The project's Conventions tab is live — view the effective map and health, edit it, and Save/Restore via a PR.
  • The validator CLI (python -m roboco.conventions check --root <repo> --files …) runs inside the agent image (the Python and TypeScript tree-sitter grammars are shipped there).

When the flag is off, none of the above happens — no prompt block, no constraints, no gating, no scaffold.

Next

Toolchain matching closes a different hollow-pass hole · The in-path PR gate is where pr_pass runs · back to Optional subsystems.