Skip to content

build(arch): report module edges and namespace ownership at compile time - #1085

Merged
mforce merged 6 commits into
mainfrom
feat/module-edge-analyzer
Oct 5, 2026
Merged

mforce merged 6 commits into
mainfrom
feat/module-edge-analyzer

Conversation

@mforce

@mforce mforce commented Oct 5, 2026

Copy link
Copy Markdown
Owner

Refs #859

Adds a Roslyn analyzer that reports undeclared module edges, stale edge rows and unowned namespaces while you edit and build. It is developer feedback only. Every architecture test is unchanged and stays the CI authority; no test calls the analyzer.

Where the rows live now (one copy)

The analyzer cannot read the test project, so the owner and edge rows moved from RealModuleLedger.Owners.cs / RealModuleLedger.Edges.cs to assembly attributes in src/Cluckwork.Domain/Common/Architecture/:

  • ModuleMapAttributes.cs defines [ModuleOwner] and [ModuleEdge].
  • ModuleOwners.cs holds the nine whole owner rows (namespaces, contract, implementations, seam, types).
  • ModuleEdges.cs holds the 22 edge cells.

Domain is the one assembly every module compilation references. The analyzer reads the attributes from source while compiling Domain and from metadata elsewhere, so an edited row takes effect in the editor without rebuilding the analyzer. RealModuleLedger.Owners and Edges now read the same attributes by reflection, so every test keeps its logic and assertions; only the data source moved. The adapter, table, tier and compatibility rows stay in RealModuleLedger.*.cs because nothing outside the tests reads them.

Rejected shapes: an AdditionalFiles data file (brings back the file format #859 removed, needs a parser on both sides); one C# rows file compiled into both the analyzer and the tests (bakes rows into the analyzer DLL, so an IDE shows stale diagnostics until it reloads the analyzer).

Review guide

Commits are in dependency order; read them one at a time.

Commit What to check
copy rows into Domain attributes Mechanical. ModuleOwners.cs / ModuleEdges.cs were generated from the C# rows. A throwaway parity test (in this commit, deleted in the next) compared the reflected attributes with the old arrays by JSON serialisation, order included; a one-character reason edit turned it red. Ran locally, not in CI.
read rows from Domain RealModuleLedger.cs reflection; old row files deleted. The undeclared-edge fix now prints [assembly: ModuleEdge(...)] and names ModuleEdges.cs. This is the one test-side text change: ModuleLedgerTests asserts the new row format, because the old new(...) row no longer pastes anywhere.
analyzer src/Cluckwork.Analyzers/ (core). Ports ModuleLedgerScanner's attribution and #1071's semantic walk. Wired into Domain, Application, Infrastructure and Api as OutputItemType="Analyzer" ReferenceOutputAssembly="false" PrivateAssets="all". Platform claims Cluckwork.Analyzers (beside Cluckwork.AppHost). Roslyn central pin 5.9.0 → 5.0.0.
analyzer tests ModuleEdgeAnalyzerTests in Application.Tests (no new test project, no matrix leg). Microsoft.CodeAnalysis.Testing; runtime references, no reference-pack download.
Docker + lockfix Dockerfile restore layer, the three lockfix inventories and lockfix.test.mjs.
docs 859-typed-rule-registries.md new section, src/AGENTS.md paragraph, 514 mechanism note, root layout line.

Mechanical noise: the two moved row files (~460 lines out, ~460 in) and lock files.

Diagnostics

Id Meaning Where
CW1001 undeclared cross-owner edge, with the row to paste at the reference
CW1002 stale symbol in an edge row compilation end; a symbol that exists nowhere is reported only by Cluckwork.Api
CW1003 unowned namespace at the type name
CW1000 compilation read no map compilation end

Not shipped: the spike's suppression barriers (CW1003-suppression, ModuleAnalyzer.targets) and its measurement toggle. #pragma/NoWarn/.editorconfig can silence a CW id; the tests still fail. Registry validation, the global-using rule and the file floor stay test-only.

CI behaviour change: a module-edge violation now fails dotnet build (and so every leg's build) with CW1001 before ModuleLedgerRealTreeTests runs. The test still fails the same change when built with analyzers off (table below).

Proofs

Check Result
Parity on the real tree analyzer's realised edges = ModuleLedgerRealTreeTests' LiveEdges: 73 and 73, diff identical
SDK 10.0.112 (compiler 5.0.0-2.26422.108) clean build green; Finance mutation red, CW1001
SDK 10.0.401 (compiler 5.9.0-1.26423.113) same
Docker --target build, pinned SDK image (10.0.401) clean exit 0; Finance mutation exit 1, CW1001
dotnet format analyzers Cluckwork.sln --verify-no-changes --diagnostics CW1001 CW1002 CW1003 --severity error clean exit 0; mutated exit 2 with CW1001 (pointed at one project it skips referenced projects and reports nothing)
Full rebuild dotnet build src/Cluckwork.Api --no-incremental, 6 alternating pairs after a warm-up pair median 3.87 s with, 3.79 s without (+0.08 s, 2 %); analyzer CPU ≈1.6 s per build across 4 compilations, mostly parallel
Cluckwork.Domain.dll Release 125,952 → 156,160 bytes (the rows' strings)
Roslyn 5.9 → 5.0 dotnet build Cluckwork.sln clean; every Roslyn-using class passes: Architecture + TenantBypass + EggLotLockSqlTests + TransactionDelegateShapeTests 445/445, integration TrackedMutationReadTests 2/2
Application.Tests count base 719 → head 725 (+6 analyzer tests), via --list-tests
Docs guards TenancyDocsFreshnessTests/Documentation 20/20, ImagePin 2/2
lockfix.test.mjs 10/10; without the new allowlist entry 2 fail

Mutations

Mutation dotnet build src/Cluckwork.Api Existing test (-p:RunAnalyzers=false)
Finance CreateExpenseHandler gets typeof(Domain.Sales.DiscountCeiling) CW1001 Finance -> Commerce, row to paste ModuleLedgerRealTreeTests red: undeclared edge
EggGradeFloorPolicy removed from EggOperations -> Farm while in use CW1001, names the row to extend red: undeclared edge
UpdateEggGradeHandler (no Farm reference) added to that row CW1002 (Application) red: stale ledger row
GoneHandler (exists nowhere) added to that row CW1002 (Api) red: stale ledger row
New type in Cluckwork.Application.Features.Nowhere CW1003 red: unowned namespace

Analyzer unit tests, each red under a mutation of the analyzer:

Test Mutation that turns it red
UndeclaredEdge_IsCW1001, MemberAccessOnlyReference_IsAnEdge walk returns at once
MemberAccessOnlyReference_IsAnEdge members no longer charged to their declaring type
DeclaredEdge_IsClean nothing counts as declared
StaleRow_IsCW1002 stale never reported
UnownedNamespace_IsCW1003 unowned never reported
UsingWithNoUse_IsClean (plain + using static) using directives walked

Not verified

  • Live squiggles in a GUI IDE. CW1002 is compilation-end, so IDEs show it only with full-solution analysis.
  • Full test suite (host rule: filtered runs only); CI is the authority.

mforce added 6 commits October 5, 2026 14:50
…ributes

ZzMapParity proves the reflected attributes equal RealModuleLedger.Owners and
Edges, in order (local run; a one-character reason edit turns it red).

Refs #859
RealModuleLedger.Owners and Edges now reflect src/Cluckwork.Domain/Common/
Architecture, so the rows exist once. The undeclared-edge fix prints the
attribute row to paste there.

Refs #859
@mforce

mforce commented Oct 5, 2026

Copy link
Copy Markdown
Owner Author

No actionable P1, P2 or P3 findings at 06c82d3c. Reviewed against the PR's main base, 623111ce, including the requested slice-scoped thermo-nuclear and ponytail passes. The analyzer meets the agreed role as developer feedback; existing architecture tests remain the CI authority.

I reproduced the following independently:

Check Result
One copy of owner/edge rows Compiled main's original arrays and compared their JSON with HEAD's actual reflected rows. All 22 edge cells match exactly, including order, reasons and symbols. All nine owners match except the intentional addition of Cluckwork.Analyzers to Platform. Contract, implementation, seam and claimed-type lists match.
Existing guard independence Existing test logic and assertions are unchanged except the undeclared-edge message format. Added a Finance reference to DiscountCeiling: build failed with CW1001. Wrapped it in #pragma warning disable CW1001: build passed, but the existing ModuleLedgerRealTreeTests failed on that exact undeclared edge. Restoring the source made both tests pass.
Real-tree parity Dumped the analyzer's observed tuples and the existing scanner's LiveEdges: 73 versus 73, identical sorted sets. Scanner covered 549 files with no errors and retained its 400-file floor.
Roslyn compatibility Locked solution restore and build passed with zero warnings. SDK 10.0.112 and 10.0.401 both rejected the mutation with CW1001. The pinned Docker build stage passed clean and rejected the same mutation.
Tests 445/445 architecture and Roslyn-related application tests, 20/20 documentation tests, 2/2 TrackedMutationReadTests with real Postgres, the solution test-project inventory check, and 10/10 lockfix tests passed. Classes ran one at a time with polling enabled.
Build integration Docker's restore layer and all three lockfix inventories include the analyzer. No guard was weakened to accommodate its namespace. The published output contains no analyzer DLL or PDB.

The Domain placement is acceptable for this slice. The published Cluckwork.Domain.dll is 156,160 bytes, which I reproduced; the supplied baseline is 125,952 bytes, a 30,208-byte increase. The attributes are passive metadata, publicly inspectable through reflection, with no production reader or runtime policy behavior. A test-only location cannot supply the compiler's map under the current dependency graph. A separate non-shipping map would add build/loading arrangements without a demonstrated benefit sufficient for a P3 finding.

The new analyzer files are 284 and 107 lines, with direct responsibilities and no suppression machinery. Keeping its semantic walk independent of the authoritative tests is justified by the agreed boundary. Ponytail review: Lean already. Ship.

The decision record and scoped instructions explicitly preserve test authority and permit CW suppression. Added documentation does not copy mutable issue state. GUI editor squiggles and the unfiltered suite remain unverified; no new timing claim is made.

Reproduction scripts, patches, raw row snapshots and logs are retained locally in /home/mforce/.cluckwork-slices/reviews/1085-06c82d3c-astra-evidence/, with separate analyzer-review and verification reports. Temporary probes were removed; the checkout was clean. No commits or pushes.

@mforce
mforce merged commit ebf8200 into main Oct 5, 2026
20 checks passed
@mforce
mforce deleted the feat/module-edge-analyzer branch October 5, 2026 15:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant