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.
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-34arrived with #819 and holds 11aggregate 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:linecites rotted too: the planrecords 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, theirnamespaces, and every cross-owner reference in
src/with a stated reason.tests/Cluckwork.Application.Tests/Architecture/— a Roslyn walk reporting undeclared edges, stalerows 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.jsonandfilter-free-set-sites.tsvsit 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:36classifies every path under
docs/as documentation, and that file's own comment (:29-35) recordswhy
specs/was deliberately kept out of that list:web/src/routes/helpGlossary.test.tsreadsspecs/product/GLOSSARY.md, so a specs-only PR would otherwise skip the guard written to policespecs-only PRs. A ledger under
docs/architecture/would repeat exactly that mistake. Thetestsmatrix is not gated by
docs_onlytoday, so this is not a live break — it is a hole that opens themoment anything in
weborimagereads 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. Onecopy of the data, never two.
No new test project.
Cluckwork.Application.Testsalready carriesMicrosoft.CodeAnalysis.CSharp(.csproj:4) andTenantBypass/GuardScanner.cs, a 1,424-line walkerthat already parses every
.csundersrc/— includingCluckwork.Api, which that project does notreference — 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
SolutionTestProjectSplitTestsreconcile, atools/coverage/collect.shentry and a lock file, andbuy 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 ishow a registry stops being read:
Customers → Sales(5 refs) —Customer.cslives insrc/Cluckwork.Domain/Sales/. Both ends areCommerce.
Accounts → Media(2) —Domain.MediaholdsImageKind/ImageSanitizer/SanitizedImage. Noentity, no table;
FarmLogois inDomain/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 asR:UpdateFarmSettingsHandler.cs:12injectsIEggUnitConversionRepository;:149andUpdateFarmSettingsValidator.cs:98callDiscountCeiling.TryParsePercent(#727)SetStepperUnitHandler.cs:13injectsIEggUnitConversionRepositoryConfirmSaleHandler.cs:23injectsIUserRoleAssignmentRepository;:171callsGetEffectiveRoleAsyncin-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:
using Cluckwork.Application.Features.Catalog;to aFeatures/Flocks/handlerFlockManagement → Commerce, naming the enclosing symbolFarm → Commercerow from the ledgerThe third is the one that matters. A ratchet that only catches additions rots into a list of
edges that were true once.
GuardScanner'sAllowListMismatchalready solved this; copy it ratherthan reinventing it.
Green on a clean tree.
dotnet buildunchanged. Nosrc/file, no.csproj, no CI change, nopackage.
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.