Skip to content

[A] #514 slice 1: module ledger and cross-owner edge ratchet #842

Description

@mforce

Slice 1 of #514 (modular monolith). Enforcement only — no production code, no file moves.

Write down who owns what, and make a test fail when code crosses a boundary the ledger does not
name. Track A of the 2026-09-14 re-plan.
It moves nothing and reverts with git rm.

Why this first

The 2026-08 design's own exception list went stale in five weeks. It names four compatibility
exceptions; there are now five — BusinessRecordModel.cs:21-34 arrived with #819 and holds 11
aggregate types from six future modules in a Platform file, which is the "Platform-owned composition
file" codex review round 3 rejected as forcing a cycle. Its file:line cites rotted too: the plan
records the seeder's expense injection at :112-113/:1034/:1049/:1283-1284; today they are
:124-125, :1859, :1875, :2237-2238.

A list maintained by recall goes stale. A walk does not. This is AGENTS.md's "walk everything,
exclude deliberately".

It also wants to land before #806 and #795 — see those issues.

Scope

  • tests/Cluckwork.Application.Tests/Architecture/Data/module-ledger.json — the nine owners, their
    namespaces, and every cross-owner reference in src/ with a stated reason.
  • tests/Cluckwork.Application.Tests/Architecture/ — a Roslyn walk reporting undeclared edges, stale
    rows and unowned namespaces.

The ledger goes beside the test, not in docs/

Two reasons, and the second is load-bearing.

It matches the existing precedent exactly: TenantBypass/Data/tenant-bypass-allowlist.json and
filter-free-set-sites.tsv sit beside the guard that reads them, wired through
<None Include="…" CopyToOutputDirectory="PreserveNewest" />
(Cluckwork.Application.Tests.csproj:7-10). Same shape, same wiring.

And a file a test reads is code, not documentation. .github/scripts/changed-paths.mjs:36
classifies every path under docs/ as documentation, and that file's own comment (:29-35) records
why specs/ was deliberately kept out of that list: web/src/routes/helpGlossary.test.ts reads
specs/product/GLOSSARY.md, so a specs-only PR would otherwise skip the guard written to police
specs-only PRs. A ledger under docs/architecture/ would repeat exactly that mistake. The tests
matrix is not gated by docs_only today, so this is not a live break — it is a hole that opens the
moment anything in web or image reads the ledger, and the cost of deciding it now is zero.

Human-facing prose about the ledger belongs in docs/decisions/514-*.md, pointing at the file. One
copy of the data, never two.

No new test project. Cluckwork.Application.Tests already carries
Microsoft.CodeAnalysis.CSharp (.csproj:4) and TenantBypass/GuardScanner.cs, a 1,424-line walker
that already parses every .cs under src/ — including Cluckwork.Api, which that project does not
reference — with a parse-error gate and a 400-file floor (GuardScanner.cs:74, :118, :136).
Reuse its root/parse/floor helpers. A new project would cost a CI matrix leg (#775), a
SolutionTestProjectSplitTests reconcile, a tools/coverage/collect.sh entry and a lock file, and
buy nothing.

Keyed by owner, never by namespace or position

Two of the five apparent "new coupling edges" are measurement artifacts. The key choice is what makes
them disappear automatically, rather than a human writing why: "not really an edge" — which is
how a registry stops being read:

  • Customers → Sales (5 refs) — Customer.cs lives in src/Cluckwork.Domain/Sales/. Both ends are
    Commerce.
  • Accounts → Media (2) — Domain.Media holds ImageKind/ImageSanitizer/SanitizedImage. No
    entity, no table; FarmLogo is in Domain/Accounts/.

Rows key on owner and enclosing symbol, never file:line (#632).

The starting edge set

Three cells the design's §3.4 marks — are real and go in as R:

Cell Evidence
Farm → Commerce UpdateFarmSettingsHandler.cs:12 injects IEggUnitConversionRepository; :149 and UpdateFarmSettingsValidator.cs:98 call DiscountCeiling.TryParsePercent (#727)
Access → Commerce SetStepperUnitHandler.cs:13 injects IEggUnitConversionRepository
Commerce → Access ConfirmSaleHandler.cs:23 injects IUserRoleAssignmentRepository; :171 calls GetEffectiveRoleAsync in-transaction (#727)

Done when

Four mutations recorded red, per AGENTS.md's "mutation first, claim second" — red output attached to
the PR, mutation reverted before commit:

Mutation Expected
add using Cluckwork.Application.Features.Catalog; to a Features/Flocks/ handler red — undeclared edge FlockManagement → Commerce, naming the enclosing symbol
delete the Farm → Commerce row from the ledger red — the edge is still in source
add a ledger row for an edge no file has red — stale row
point the walk at a tree with fewer than 400 files red — file-count floor

The third is the one that matters. A ratchet that only catches additions rots into a list of
edges that were true once. GuardScanner's AllowListMismatch already solved this; copy it rather
than reinventing it.

Green on a clean tree. dotnet build unchanged. No src/ file, no .csproj, no CI change, no
package.

Explicitly not in this slice

Table ownership, adapter-privilege classification and the seam-shape guard are Track B, after #789
and #788 close. No module contracts, no file moves, no namespace changes, no new assembly — Track C,
which needs an authorisation decision that has not been made.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:apiAPI/endpoint layerepic-514Modular monolith architecture (#514)priority:tier4Deferred or speculativesize:SHours to a day; few files, no migrationsliceThin vertical work itemtrack:A#514 Track A — ledger and ratchet; no production code; ~2-4 days

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions