M1-06: Module register checked against the core contract¶
Status: ready | Depends on: M1-02, M1-05 | Milestone: M1
Goal¶
A new gate make register-check keeps the module register
(services/server/architecture/modules.yaml), the table owners of 03, the action catalogue
(docs/spec/actions.yaml), the module tables of 02, the server component diagram and the module
READMEs equal. Any difference fails with file and item. After this ticket the register is a
trustworthy single source: go-arch-lint, table ownership and later checks can rely on it, and a
chapter edit cannot silently drift away from the code frame.
Read first¶
docs/PLAN.md: section "Way of working per ticket", section "M1 Foundation" (row M1-06), P-11.docs/spec/02-architecture.md: "Modules of the monolith" ("Rules" items 6 and 7, "Module list", "Allowed dependencies", "Cross-cutting ports (internal/kernel)", "Actions per module", "Event types and WebSocket ops per module"), "Automatic check of the rules" ("Module register as the source", "Other checks in the gate").docs/spec/03-data-model.md: "Modules", "Tables" (each table heading#### \` with the sentenceModule: ``.` in its purpose paragraph), "Technical tables". docs/spec/05-actions.md: "Binding rules" items 3 and 4, "Actions", "Durable event types and receive action", "WebSocket ops and action".docs/spec/actions.yamlandtools/catalog/with its README (from M1-02), ADR 0049.services/server/architecture/modules.yaml,services/server/tools/archgen, ADR 0050 (from M1-05).docs/architecture/c4-3-server-components.mmd,services/server/internal/*/README.md.tools/README.md(rules for new tools),tools/pyproject.toml, rootMakefile,docs/quality/gates.md,docs/spec/23-quality.md"Principles".
Fixed requirements that apply¶
- No MVP. New from scratch and directly in the final state; stages are only the order.
- Strictly modular: clear module and service boundaries, dependencies only through defined interfaces, no cross access.
- Documentation in code: every public module, class, function and interface with a docstring/doc comment (purpose, parameters, return value, errors, example where useful); comments explain the why, not the what. API documentation is generated from code (OpenAPI from the routes, dartdoc, Python API docs in MkDocs).
- Documentation in the repo: README per module/service, ADR for every architecture decision, CHANGELOG per release, keep the documentation site current. A change without matching documentation is not done.
- Quality gates (SonarQube, linters, type checking, security scanners, test coverage) block the merge.
- Test first: write the failing test, run it and keep its red output for the report, make it green, then clean up.
- No en dash (U+2013) or em dash (U+2014) in any file you write;
uv run --project tools --locked --quiet python tools/hooks/text_rules.py <files>over your changed files exits 0 (plaingrep -Pfails on macOS). - White label: no company, person, host or instance names, no paths of a personal machine, no real data.
- Configuration values only through named keys from
docs/operations/configuration.md. This ticket adds none. - Every table of 03 belongs to exactly one module; every action of the catalogue is executed by
exactly one module (02 "Rules" item 6). The modules that own no tables are the ones 02 "Rules" item 7 and 03
"Modules" name (
nonein 02 "Module list"); the check reads them, it does not count them. - Every durable and every ephemeral event type has exactly one producing module (02 "Event types and WebSocket ops per module").
- The checks "Completeness" and "Docs alignment" of 02 "Other checks in the gate" run in the gate.
- New repository tools: Python with the standard library or PyYAML, one test file per tool, SPDX
line in every file, one make target, a row in
tools/README.md(tools README rules). - The core contract (00, 03, 04, 05 and
actions.yaml) is never changed here. A difference that can only be fixed there ends the ticket withVerdict: BLOCKED(P-11).
Scope¶
Create:
tools/doccheck/register_check.py(the check, CLIpython tools/doccheck/register_check.py [--repo PATH]).tools/doccheck/tests/test_register_check.pyand small fixture builders inside the test file (usetmp_path; no large fixture trees).
Change:
- Root
Makefile: targetregister-checkwith a##help text, added togatesand.PHONY; addtools/doccheckto the bandit directories oflint. tools/pyproject.toml: adddoccheckto pytesttestpathsandpythonpath, coveragesourceand mypyfiles.tools/README.md(row),docs/quality/gates.md(row forregister-check).docs/spec/02-architecture.md"Other checks in the gate": column "Implementation (ticket)" of the rows "Completeness" and "Docs alignment" namesmake register-check.- Data fixes the check finds, only in
services/server/architecture/modules.yaml(then regenerate.go-arch-lint.ymlwithgo generate ./tools/archgen), in 02, in the component diagram (then re-render its SVG withmake diagramstooling) or in a module README. docs/DECISIONS.md, rootCHANGELOG.md(Added).
Out of scope¶
- The register format and the go-arch-lint generator (M1-05). Action catalogue structure, its generator and the tables of 05 (M1-02). API contract checks (M1-03).
- Table ownership inside SQL files (
adapters/postgres/queries/*.sql, 02 "Other checks in the gate" row "Table ownership"): it needs sqlc queries and comes with M1-12. - Route and op handler placement (M1-17, M1-25). ERD completeness (not planned in M1).
Boundaries¶
- Work in a dedicated git worktree on the branch
feat/M1-06-module-register-check. - Conventional Commits with DCO sign-off (
git commit -s), scopetoolsorspec. - Never push, tag, open pull requests or create remote repositories.
- Do not modify files outside Scope, except docs and changelog entries named here.
- Follow the host rules supplied by the launcher. Stop only processes you started, by PID.
- Temporary directories only via
mktemp -dor pytesttmp_path; remove them. - Never read or print secrets.
- No network calls except package downloads for pinned tools and what the repository gates in
make checkdo themselves.
Design notes¶
Sources and parsing (locate sections by title, never by number)¶
- Register: PyYAML
safe_loadofservices/server/architecture/modules.yaml; fields as defined in ADR 0050 and 02 "Module register as the source" (modules.<m>.tables,actions,events,ephemeral_events,ws_ops,depends_on;kernel.core,kernel.ports). - 03: the level-2 section titled "Tables" up to the next level-2 heading. Each
#### \` heading is a table; its owner is the firstModule: `` after the heading and before the first table row. A table heading without owner is a finding. The section titled "Modules" gives the module set (first column) and whether a module owns tables (third columnyesornone`). - Catalogue: use the loader M1-02 ships in
tools/catalog(see its README and ADR 0049) for action ids, durable event types, ephemeral event types and ops with their direction. Do not parseactions.yamla second time. Iftools/catalogexposes no importable loader, add one public function there with a test and no other change, and record it in the report. - 02: subsections titled "Module list", "Allowed dependencies", "Cross-cutting ports",
"Actions per module" and "Event types and WebSocket ops per module". Rows start with
| \``; split cells on|; read backticked names per cell; a cell withnothing,noneor no content is the empty set. In "Cross-cutting ports" the first column iskernel/`. - Diagram: node declarations
^\s*([a-z]+)\["and edges^\s*([a-z]+)\s*-->\s*([a-z]+)\s*$whose both ends are register modules; lines with~~~are layout only. - READMEs
services/server/internal/<m>/README.md: the line starting withMay use the ports of these modules:and the backticked names of the paragraph under## Tables(nonemeans the empty set).
Checks (one finding per line, sorted, then exit 1)¶
| Id | Rule |
|---|---|
| C1 | Every 03 table is listed by exactly one register module, and that module equals its Module: line; a register table unknown to 03 is a finding. Technical tables (section "Technical tables") are not module tables. |
| C2 | Module set of 03 "Modules" equals the register; modules marked none list no tables and modules marked yes list at least one. |
| C3 | Every catalogue action is in exactly one module's actions; a register action unknown to the catalogue is a finding. |
| C4 | Every durable event type is in exactly one module's events, every ephemeral one in exactly one module's ephemeral_events; unknown or misplaced types are findings. |
| C5 | Every op sent by a client, agent or app (05 "WebSocket ops and action", not server-to-client) is in exactly one module's ws_ops; server-to-client frames never appear in the register. |
| C6 | 02 "Module list": same modules, same table set per module. |
| C7 | 02 "Allowed dependencies": same depends_on set per module. |
| C8 | 02 "Actions per module": same action set per module. |
| C9 | 02 "Event types and WebSocket ops per module": same durable, ephemeral and op sets per module. |
| C10 | 02 "Cross-cutting ports": package set equals kernel.core plus kernel.ports. |
| C11 | Diagram: module node set equals the register modules; edge set equals all depends_on pairs, in both directions (missing and extra edges). |
| C12 | Each module README: dependency line and table list equal the register. |
- Finding format:
register-check: <repo-relative path>: <rule id>: <message>, for exampleregister-check: docs/spec/02-architecture.md: C8: module chat: action thread.move missing. - Success prints
register-check: ok (<n> modules, <t> tables, <a> actions, <e> event types, <o> ops)and exits 0. A missing file or section exits 2 with a message naming it. - Compare, do not generate: the tables of 02 mix machine columns with prose columns ("Responsibility", "Reason per edge") that people maintain, so the check compares the machine columns only. Record this decision in DECISIONS.md.
- Resolution rule for real differences on the merged tree: names and table owners come from the
core contract (03, 05,
actions.yaml); the register follows the core contract; 02, the diagram and the READMEs follow the register. A difference that needs a core contract change ends the ticket withVerdict: BLOCKED. An action that the catalogue has but 02 does not assign goes to the module that owns the tables of its resource (03); record each such assignment in DECISIONS.md. - Typed code (mypy strict), no global state, functions small enough for the ruff pylint limits in
tools/pyproject.toml; module docstring states purpose, sources and rule ids.
Tests (write them first)¶
tools/doccheck/tests/test_register_check.py, each test builds a minimal repository in tmp_path
(two or three modules, a few tables, actions, events, ops, a diagram and READMEs) with one
deliberate defect and asserts the rule id and item in the output:
test_clean_fixture_passestest_table_from_03_without_module_fails(C1)test_table_in_two_modules_fails(C1)test_table_owner_differs_from_03_fails(C1)test_table_heading_without_owner_fails(C1)test_module_set_differs_from_03_failsandtest_tableless_module_with_tables_fails(C2)test_action_twice_failsandtest_action_without_module_failsandtest_register_action_unknown_to_catalogue_fails(C3)test_event_type_without_producer_fails,test_event_type_two_producers_fails,test_ephemeral_event_listed_as_durable_fails(C4)test_client_op_without_module_failsandtest_server_frame_in_register_fails(C5)test_module_list_row_differs_fails(C6),test_dependency_row_differs_fails(C7),test_actions_row_differs_fails(C8),test_events_row_differs_fails(C9),test_kernel_packages_differ_fails(C10)test_module_edge_missing_in_diagram_failsandtest_extra_edge_in_diagram_fails(C11)test_readme_tables_differ_failsandtest_readme_dependencies_differ_fails(C12)test_missing_section_exits_2test_real_repository_passes(runs against the repository itself; the check is useless if it does not hold the real files)
Commands that must pass:
uv run --project tools --locked pytest tools/doccheck/tests
make register-check
make lint typecheck test coverage docs linkcheck diagrams
make check
Acceptance criteria¶
-
make register-checkis part ofgatesand green on the merged tree. - Each rule C1 to C12 has at least one failing fixture test; all tests pass.
-
test_real_repository_passesruns against the real register, 02, 03, catalogue, diagram and READMEs. - Tools coverage stays at or above
quality.coverage.tools_min_percent; mypy strict, ruff and bandit are clean. -
tools/README.mdanddocs/quality/gates.mdlist the new gate; 02 "Other checks in the gate" names it. - No file of the core contract changed.
-
make checkis green.
Definition of done¶
- Code, tests and docs as above: docstrings on every public function and class, rows in
tools/README.mdanddocs/quality/gates.md, DECISIONS.md entries, rootCHANGELOG.mdentry under[Unreleased](Added). make checkgreen.- Independent review over the diff (a fresh reviewer,
make review) and the red team gate ofdocs/quality/red-team-gate.mdover the diff, verdict recorded in the report; fix every finding with a test and have the delta checked again. - Merge locally into the base branch only if review and red team are green and
make checkpasses on the merged state; thengit worktree remove.
Report¶
Write to {{REPORT_PATH}}: first line Verdict: DONE or Verdict: BLOCKED <reason>; then commits
(SHA and subject), files with line counts, test results with counts (including the red output of
the first test run), the findings the check produced on the real tree and how each was resolved,
what is not proven, and follow-ups that could not be done in scope with the reason. Then write
{{DONE_PATH}} with exit=0 sha=<merge sha> or exit=1 <reason>.