Step 1 found the reason the standard names never held, and it is sharper than
"prose doesn't fire": the list WAS real and it WAS seeded — but only on the
inception path, for a project with zero Systems. Ad-hoc create_system never
consulted it, which is how Forge minted "CI and Release" and Portal minted
"CI & release" after the constant already existed. This wires the vocabulary to
the moment that mints a name.
- services/systems.assess_system_name: the local duplicate gate AND the
catalog lookup, in ONE service function both doors call. The gate lived only
in the MCP tool, which is exactly how the web UI shipped without a check the
agent surface enforced (#2482). REST now answers 409 with the System that
already covers the area.
- An `exact` catalog hit is APPLIED (mechanical — the names differ only in
spelling). An `overlap` is only OFFERED, on both doors: applying a judgment
call silently is how a cross-project rule surfaces in the wrong project.
- canonical_systems.best_overlap is the ONE scorer behind the create-time
offer and the review sweep, so the two surfaces can never name different
areas for one System. It also takes the catalog the caller already holds,
so the review is not an N+1.
UI (folded in from step 1 — rule 27, that step shipped with no human surface):
- SystemsSection: a Shared area picker on create and edit, the area on each
card, and a collapsed review of proposals that appears only when there is
something to decide. `exact` and `overlap` never share a style — one is
mechanical, the other is the reviewer's judgment, and presenting them alike
is how a wrong mapping gets waved through.
- Settings → Admin → Areas: the catalog itself, showing each entry's slug,
because the slug is what decides whether two names are the same area and a
rename moves it.
- A picker rather than a live matcher: reproducing the slug rule in TypeScript
would give this feature two matchers to keep in step — the exact drift the
catalog exists to end. The server stays authoritative.
tests/helpers.fake_system gains canonical_id=None: an unnamed attribute is an
auto-MagicMock and therefore truthy, which is the trap that helper exists for
(note 2109) and a nullable FK walks straight into it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Third slice of Issues + Systems (spec #825).
routes/systems.py (nested /api/projects/<id>/...): GET/POST systems (list adds
per-system open_issue_count via one grouped query), GET/PATCH/DELETE a system
(GET returns records split into issues/tasks/notes), GET .../systems/<id>/records
(kind/open_only filters), GET .../issues (project's open issues for the project
view + dashboard roll-up). login_required; project access via get_project_for_user;
writes gated by can_write_project (clean 403); system.project_id verified to match
the path. Blueprint registered in app.py.
services/systems.py: + open_issue_counts_by_system (one grouped query) and
list_issues (project issues, open by default).
Tests: structural (blueprint registered + in app, handlers callable, service
contracts take user_id) — matches the house route-test pattern.
Refs plan 825 (S3).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>