Skip to content

build: enforce file-scoped namespaces and using placement at build time - #985

Merged
mforce merged 2 commits into
mainfrom
chore/editorconfig-csharp-style
Oct 1, 2026
Merged

mforce merged 2 commits into
mainfrom
chore/editorconfig-csharp-style

Conversation

@mforce

@mforce mforce commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

Encodes two C# conventions in .editorconfig and makes them build errors, so they hold by construction during Track C rather than by reminder in every brief.

Why now

.editorconfig covered whitespace only, and its own header said it was a convenience rather than a gate. Nothing ran dotnet format, and none of EnforceCodeStyleInBuild, AnalysisLevel or EnableNETAnalyzers was set, so C# style rules reached editors and never the build. #514's Track C adds nine module contracts across a dozen slices and several authors, which is exactly when an unenforced convention drifts.

What changed

  • Two rules at warning severity in .editorconfig: csharp_using_directive_placement = outside_namespace and csharp_style_namespace_declarations = file_scoped.
  • EnforceCodeStyleInBuild in Directory.Build.props. TreatWarningsAsErrors was already true there, so a violation is a build error, which is how this repo treats every other rule.
  • 590 files swept to put usings before the namespace declaration, by the committed script tools/style/move-usings-outside.py.
  • tests/Cluckwork.Application.Tests/FlockScope/ renamed to FlockScoping/, with its namespace, in its own commit. See the root cause below.
  • Two deliberate exclusions, each with its reason in the file.
  • The .editorconfig header now names the two gated rules instead of claiming nothing is enforced.

Placement, measured on main

Measured on main at f28bb470, before the rebase. Of 722 tracked .cs files outside Migrations/ that declare a namespace: 589 put usings after the namespace declaration, 3 put them before, 126 have no using directives, and 4 are files whose apparent usings sit inside raw string literals that build synthetic source for the architecture scanners. Inside placement was the repo's style in 589 of 592 files that had a choice, while every C# template defaults to outside, so each new file landed in the opposite style from the rest of the repo. #1002 has since added one more file, which is why the sweep is now 590.

The one non-mechanical change

FlockScope is both a production class, Cluckwork.Infrastructure.Persistence.FlockScope, and was a test namespace, Cluckwork.Application.Tests.FlockScope. Moving usings out of that namespace's scope let the sibling namespace shadow the type. Reverting only the rename against this head produces 13 errors across six files, 11 CS0118 and two CS0234, reaching CouplingMatrixRealTreeTests.cs, TableOwnerRealModelTests.cs and the tenant-bypass tests as well as the two files in the renamed namespace. Renaming the test namespace fixes all of them, and a test namespace that shadows a production type is worth removing regardless of style.

Mutations, observed

Run at this head, not recalled:

Mutation Observed
new file under src/ with usings after a file-scoped namespace error IDE0065: Using directives must be placed outside of a namespace declaration, Build FAILED
new file under src/ with a block-scoped namespace error IDE0161: Convert to file-scoped namespace, Build FAILED
compliant probe Build succeeded, zero warnings

Verification

dotnet build Cluckwork.sln clean. Domain 495, Application 544, AppHost 10, Integration 1,873, all passing. A green build is necessary but not sufficient here: the compiler catches a name that became ambiguous, not one that silently rebinds to a different valid type. Review covered that gap by comparing Roslyn bindings across all nine projects' real compiler inputs, including implicit and generated sources: 2,770 using targets and 351,085 expression bindings match, with no difference outside the intentional rename. The architecture scanners report the same 473 files, 67 module edges, 405 adapters and 288 reaches on both trees.

Reviewers can re-derive the sweep instead of reading 590 headers. Run the script on main and diff; it reproduces every file byte for byte.

Deliberately out of scope

No dotnet format in CI or the pre-commit hook. That is a separate decision, and bundling it would hide a real choice inside a small change. No broader style ruleset imported.

Rebased onto ad258d8f

#1000, #1002 and #1003 reworked 31 integration test files that this sweep also touches, and 10 of them conflicted in the using block. Rather than hand-resolve them, the branch was rebuilt on current main: the FlockScope rename redone, the non-code changes applied on top (AGENTS.md merged cleanly with main's edits), and the script rerun. All 590 swept files are pure line moves with no content change. Build clean with zero warnings; Domain 495, Application 544, AppHost 10, Integration 1,873, all passing locally at the new head.

@mforce

mforce commented Sep 28, 2026

Copy link
Copy Markdown
Owner Author

Owner decision: usings go OUTSIDE the namespace for new code, and the existing 593 files are left alone. df601c4c implements that. Summary of what changed in this PR's intent.

Rule Before this commit Now
file-scoped namespace gated, build error unchanged, still a build error
using placement gated as inside_namespace recorded preference outside_namespace:silent, not gated

Why placement is not gated. An analyzer has no concept of a new file, so the rule can only apply to every file or none. Gating outside_namespace would fail the build on 593 existing files; converting them is a 593-file diff that would conflict with both open PRs and other in-flight work. silent records the preference so editors place new usings outside and the intent is discoverable in the file, while nothing breaks.

Four files reverted. The earlier commits moved usings into the namespace in the demo seeder, the integration collection orderer and the two AppHost test files. Those four were the only files in the repo already matching the chosen direction, so they are restored to their state on main.

The correctness note, since it argues for outside. A using X; written inside a namespace resolves its own name relative to that namespace, so it can bind to a sibling X rather than the global one. Outside placement resolves from the root and is the C# default. That hazard is not theoretical here: review round 2 on #872 found the ledger scanner missing a cross-module edge introduced by exactly this relative-resolution rule, which is why ModuleLedgerTests.SingleIdentifierRelativeImport_IsAnEdge exists. No file in src/ uses that form today.

Mutations re-run on this head

Mutation Observed
new file with a block-scoped namespace error IDE0161: Convert to file-scoped namespace, Build FAILED
new file with usings outside the namespace Build succeeded
full solution with all 593 legacy inside-namespace files Build succeeded, zero IDE0065 diagnostics

The EF migrations exclusion now covers only the namespace rule; those files already place usings outside, which is the stated preference.

@mforce

mforce commented Sep 29, 2026

Copy link
Copy Markdown
Owner Author

The conversion is now in this PR, per the owner's call to do it here rather than drip it through Phase C. The rule is a build-time gate again, in the direction the owner chose.

Rule Status
csharp_style_namespace_declarations = file_scoped gated, build error
csharp_using_directive_placement = outside_namespace gated, build error

What changed

589 files converted, plus the script that did it. It is committed at tools/style/move-usings-outside.py so a reviewer can rerun it and diff the result instead of reading 589 mechanical changes. Every changed hunk is in a file header; no hunk in the diff begins past line 60.

The part that was not mechanical, and is the reason to read this carefully

FlockScope was both a production class (Cluckwork.Infrastructure.Persistence.FlockScope) and a test namespace (Cluckwork.Application.Tests.FlockScope). With a using inside a namespace, the import wins at that level. Moved outside, the sibling namespace shadows it. Six files stopped compiling with CS0118: the two in that namespace and four more across Architecture and TenantBypass that used the type by its short name.

The namespace is renamed to Cluckwork.Application.Tests.FlockScoping, in its own commit. That is a root-cause fix worth having regardless of style: a test namespace that shadows a production type is a latent trap. No file imported the old namespace, and every documentation reference is to the type or to class names, so nothing else moved.

Two smaller findings. DemoDataSeeder.cs already mixed both placements on main; the script merges the inside group into the outside one rather than guessing. The two multi-namespace guard fixtures now need the using rule silenced as well as the namespace rule, since they cannot comply with either.

Verification

Suite Result
dotnet build Cluckwork.sln succeeded, zero warnings, rule enforced
Domain 495 passed
Application 544 passed
AppHost 10 passed
Integration 1,873 passed

The suites matter more than usual here. The compiler catches a move that makes a name ambiguous; it cannot catch one that resolves to a different valid type. A full green integration run is the only evidence against that, and this is why the conversion was verified rather than assumed.

Script behaviour, stated so it can be checked

It skips block-scoped files, which covers EF migrations and the two fixtures, since those already place usings outside. It moves a comment that touches a using with no blank line between them, because such a comment annotates that using and would otherwise be orphaned; one did get orphaned on the first pass. It collapses blank lines only at the seam it creates, never elsewhere in the file.

@mforce
mforce force-pushed the chore/editorconfig-csharp-style branch from df601c4 to 757e216 Compare September 29, 2026 01:46
@mforce

mforce commented Sep 29, 2026

Copy link
Copy Markdown
Owner Author

Reviewed head 757e2162c1eef07cc32be64539127a5a6e6521e4 against f28bb47098a43dbaa634e3292b5c11bf8c9c893e, focusing on sweep semantics and the FlockScope rename. No code defects found in that scope.

Checks I ran:

  • Replayed the committed script on a separate base worktree. All 589 changed C# files match byte for byte after applying the documented namespace rename. The other 587 have identical nonblank-line multisets. Confirmed 1,181 blank-line changes and identical file bodies from the first type declaration onward.
  • Compared Roslyn bindings using the nine projects' actual compiler inputs, implicit/global usings and generated sources. Both versions compile without errors. Across the 589 changed files, 2,770 using targets and 351,085 expression symbol/type bindings match, normalizing only the intentional test namespace rename. The actual type/namespace-name collisions are FlockScope and an unchanged architecture fixture, DtoWithQueryableProperty.
  • Audited actual syntax, excluding test strings that merely contain C#. The mixed-placement DemoDataSeeder and the TestHarness alias with its attached comment are preserved correctly. The five global-using files, two multi-namespace fixtures, eight namespace-free files and pragma-bearing files are unchanged. There are no actual conditional using blocks, using static, extern alias or region-wrapped usings affected by this sweep.
  • dotnet build Cluckwork.sln: zero warnings/errors. Application tests: 544 passed, including all 240 architecture cases and both renamed tests. Head scanners run on both source trees report the same 473 files, 67 module edges, 405 adapters and 288 reaches. Running within each worktree confirms fixed floors of 400 files and 40 adapters. The head model/assembly scans inspect 37 tables and 35 interfaces against floors of 30, with no evaluation failures.
  • Searched the whole repository for rename references and test filters. CI and coverage run the whole Application project; existing filters still select the intended classes. Old test paths/namespaces remain only in generated graph artifacts.

CONFIRMED, nonblocking correction to the stated collision scope: reverting just the namespace rename in an in-memory compilation produces 13 errors across six files, comprising 11 CS0118 and two CS0234, rather than seven sites confined to two files. Additional affected sites include CouplingMatrixRealTreeTests.cs:52, TableOwnerRealModelTests.cs:14, and the tenant-bypass tests. The committed rename fixes them all.

The PR description also still describes the earlier inside_namespace implementation and four outliers; it should be updated to match this head. Gate design and exclusions belong to the other review. I did not rerun integration tests.

— GPT-6 Astra

@mforce

mforce commented Sep 29, 2026

Copy link
Copy Markdown
Owner Author

Fable review of #985 at 757e2162: the gate, the exclusions, the script, and what the repo's rules ask for

Scope: the analyzer gate and its blast radius, the two .editorconfig exclusions, the codemod, and AGENTS.md obligations. The 589 header moves are the other reviewer's angle. Every experiment ran in a fresh worktree at the head SHA; nothing was committed or pushed and the tree was clean after each step.

Findings

1. CONFIRMED — .editorconfig comments state the opposite of the settings beside them. .editorconfig:5-8 says "ONE C# rule below is gated … Using-directive placement is a recorded preference at silent severity, not a gate." .editorconfig:29-34 says "Preference only, deliberately NOT gated … 593 legacy files still place usings inside … so the rule is silent." Line 35 is csharp_using_directive_placement = outside_namespace:warning, and the build proves it is a gate (finding 3). Both comments date from 2cd96251; 757e2162 flipped the severity and swept the files but left the prose. AGENTS.md forbids citing an unenforced entry as enforced; a comment calling a gate silent is the mirror image. Directory.Build.props:12-15 is accurate at head, so the two files currently disagree with each other.

2. CONFIRMED — the PR body describes the superseded revision. It says the rule is inside_namespace, that "the four using-placement outliers" were fixed, that migrations have "both rules silenced", and its mutation table quotes IDE0065 as "must be placed inside". All four are reversed at head. The second PR comment is the accurate description; the body is what the squash and the changelog reader see, so replace it with that.

3. CONFIRMED — the fixture exclusion glob matches, but its namespace line is dead. Removing the whole [tests/Cluckwork.Application.Tests/Architecture/{SeamSurfaceTests,TableOwnerTests}.cs] block: 54 × IDE0065 across both files, zero IDE0161. Removing only .editorconfig:44 (csharp_style_namespace_declarations = block_scoped:silent): build succeeds. IDE0161 never reports on a compilation unit with more than one namespace, so the analyzer already knows what the comment at .editorconfig:38-42 explains, and only line 45 does any work. Drop line 44 and reword the comment to say what the block actually exempts. On whether the inside usings are load-bearing: yes, as written. SeamSurfaceTests.cs:267-289 (24 aliases like using QueryableReturnFixtures = SeamFixtures.QueryableReturn;) and TableOwnerTests.cs:5 (using Fixtures = TableOwnerFixtures;) are relative aliases resolving against the enclosing namespace, which is exactly the relative-resolution behaviour the PR's rationale calls a hazard. They could comply by fully qualifying 25 aliases and hoisting the imports, but that is a content change to two guard fixtures; the exemption is the cheaper honest answer. Note these two files are now the only real (non-string-literal) inside-namespace usings in the tree.

4. CONFIRMED — the migrations exclusion is necessary, its glob matches, and its stated reason is the wrong one. Removing [src/Cluckwork.Infrastructure/Persistence/Migrations/**]: IDE0161 on all 19 non-Designer migration files (Designer and snapshot files carry <auto-generated /> and are skipped regardless). Then the workflow check: dotnet ef migrations add ZzGateProbe -p src/Cluckwork.Infrastructure -s src/Cluckwork.Api (dotnet-ef 10.0.12 from the tool manifest) scaffolds a block-scoped namespace with usings outside; Infrastructure builds green with the block and fails IDE0161 without it. So there is no dotnet ef trap, but only because of this block. The comment at .editorconfig:47-49 justifies it by #407's freeze ("never hand-edited or regenerated"); the load-bearing reason is that EF's scaffolder emits block-scoped namespaces, so every future migration needs it too. Say that. "Never hand-edited" is also false in the tree: 20260913212515_AddBusinessRecordChronology.cs (#820) is hand-written and file-scoped.

5. CONFIRMED — the effective warning-severity set under EnforceCodeStyleInBuild is exactly {IDE0065, IDE0161}, by construction rather than luck. Method: reflected over every DiagnosticAnalyzer in SDK 10.0.401's Microsoft.CodeAnalysis.CodeStyle.dll and Microsoft.CodeAnalysis.CSharp.CodeStyle.dll, the two assemblies Microsoft.NET.Sdk.Analyzers.targets:151-159 adds under this property. 121 distinct ids: 116 default Hidden, 4 Info, 1 Warning, and that one is EnableGenerateDocumentationFile, which only reports when IDE0005 is configured at warning or above (it is not). The repo has no .globalconfig, no ruleset, and no AnalysisLevel, AnalysisMode or dotnet_diagnostic.* anywhere; tests/Directory.Build.props only chains to the root. Cross-check: a probe file with ~25 common style deviations built with -p:TreatWarningsAsErrors=false produced only CS0169/CS0414/CS0649/CS8618/CS8625, zero IDE*. Build cost: full --no-incremental solution build 7.3–8.0 s with the property off, 8.1–8.3 s on.

What this means for the next contributor: today nothing else can fail. But the property's contract is that every .editorconfig option written with :warning, and every dotnet_diagnostic.IDExxxx.severity = warning, becomes a repo-wide build error the moment it is committed. That is the feature, and it needs to be written where briefs are drawn from (finding 7).

6. CONFIRMED — dotnet format fixes both rules, so the script is not the only remedy. dotnet format style <proj> --diagnostics IDE0065 IDE0161 --severity warn --include <file> moved the usings out of a file-scoped namespace, and on a second probe converted a block-scoped namespace with inside usings to file-scoped with usings outside. Recommendation: drop tools/style/move-usings-outside.py. Its docstring (move-usings-outside.py:5-6) says it is committed so a reviewer can rerun it during this review; that purpose ends at merge, and after merge the build rejects the only input the script consumes, so it can never act again except on a checkout that predates the gate. Put the dotnet format one-liner in the AGENTS.md paragraph instead. If it stays anyway: the mixed-file branch at move-usings-outside.py:49-50 collapses blank lines across the whole file, which the comment at lines 58-61 says the script never does.

7. CONFIRMED — what the repo's own rules ask for and the PR does not carry.

  • AGENTS.md paragraph: missing, and required. AGENTS.md calls itself the canonical rule set for every coding agent and already lists build-breaking conventions ("Nullable enabled, no unused usings — both are build-breaking"). This PR adds two more plus a mechanism (.editorconfig severity ⇒ build error) and says nothing there. The PR's own goal, "hold by construction during Track C rather than by reminder in every brief", is defeated if the rule is absent from the file briefs are built from. One paragraph: the two rules, the remedy command, the fact that any :warning entry in .editorconfig is a gate, and the two exemptions.
  • Decision record: missing, and warranted. docs/decisions/README.md says records exist for "anyone about to simplify a rule". This rule was chosen against a stated failure mode on each side and forced a production-type-shadowing rename; without a record, the next person who wants to flip to inside_namespace, or who hits CS0118 from the same shadowing, has only this thread. Records are keyed by issue number and indexed in that README's table; no issue tracks this work, so key it by PR (985-…) or open one.
  • Title: build: parses as a conventional commit. Under release-please-config.json, build is hidden: true and not a releasable type, so this PR yields no version bump and no changelog line. That is right for a change with no user-visible effect; just do not expect a patch.
  • Closing link: none, and none needed. closingIssuesReferences on the API is empty and no issue tracks this.
  • Directory.Build.props: consistent with its stated boundary (a build setting, not a version). It is not a restore input, so the Dockerfile restore layer and the CI drift guard need no change. .editorconfig is now a build input; .dockerignore does not exclude it and src/Cluckwork.Api/Dockerfile:43 copies it before publish, so the image build enforces the same gate (the green "Image build + Trivy scan" check is that evidence).

Verdict on the decision

Gating as a hard error, now, is the right call; I would not have taken the warning-only route. (a) TreatWarningsAsErrors is already global, so "warning without error" means WarningsNotAsErrors for two ids and a warning line in a CI log nobody reads. The silent-preference state the branch passed through (2cd96251) enforced nothing, and an analyzer cannot tell a new file from an old one, so the only honest states are gate-all or gate-none. (b) The blast radius is provably two rules (finding 5), each with a one-command fix (finding 6). (c) The cost is rebase friction on in-flight .cs work, and right now that is zero: the other open PRs (#996, #993, #952) touch no .cs files. That window closes when Track C opens a dozen slices, which is also when the most new files get written. (d) The real risk in a 589-header move is a name that silently resolves to a different valid type; FlockScope shows the hazard is real, and that is the other review's job.

What I would hold merge on: findings 1 and 2 (the branch and its description contradict each other) and the AGENTS.md paragraph in finding 7. Findings 3, 4 and 6 are cleanups for the same push.

Checks run

  • dotnet build Cluckwork.sln at head: 0 warnings, 0 errors.
  • Fixture block removed / namespace line only removed → 54 IDE0065 + 0 IDE0161 / build succeeded.
  • Migrations block removed → IDE0161 on 19 files.
  • 25-deviation probe with TreatWarningsAsErrors=false → CS-prefixed warnings only.
  • dotnet format style --diagnostics IDE0065 IDE0161 on two probes → both fixed.
  • dotnet ef migrations add at head → block-scoped scaffold; green with the block, IDE0161 without.
  • Four non-incremental solution builds, property on and off → 8.1–8.3 s vs 7.3–8.0 s.
  • Reflection over the SDK's two CodeStyle analyzer assemblies → 121 ids, one default-Warning (IDE0005's GenerateDocumentationFile helper).
  • closingIssuesReferences via GraphQL → empty.

— Claude Fable 5.1

mforce pushed a commit that referenced this pull request Sep 29, 2026
Review of #985 found the branch's own documentation contradicting what it
enforces, plus two missing artifacts the repo's rules require.

- .editorconfig's header and block comment still called using placement
  "a recorded preference at silent severity, not a gate" and said "nothing
  fails if it does not". Both are false at this head. Rewritten to state
  that any `:warning` entry in this file is a build error.
- Dropped a dead `block_scoped:silent` from the fixture exemption. IDE0161
  never reports on a compilation unit declaring more than one namespace, so
  the line configured nothing. Verified by building without it.
- Corrected the migrations exemption's reason. It is there because
  `dotnet ef migrations add` scaffolds block-scoped namespaces, so it must
  keep working for migrations nobody has written yet, not because #407
  freezes the existing ones. The old comment also claimed migrations are
  "never hand-edited", which AddBusinessRecordChronology contradicts.
- AGENTS.md paragraph and docs/decisions/985-csharp-style-gate.md, both
  required for a build-gating convention. The record keeps the measurement
  that the gate's effective set is exactly {IDE0065, IDE0161}, and the
  correction that outside placement is not inherently safer.
- move-usings-outside.py: the mixed-file branch collapsed blank lines across
  the whole file while a comment below it said a global pass is churn the
  change has no business making. Narrowed to the seam, shared with the other
  branch. The docstring also said 586 files and claimed mixed files are
  skipped; both were wrong. Re-running the fixed script on main reproduces
  all 589 files byte for byte.
@mforce

mforce commented Sep 29, 2026

Copy link
Copy Markdown
Owner Author

Response to both reviews, at 3c1b9050

Two independent reviews ran in parallel on 757e2162: GPT-6 Astra on sweep semantics and the rename, Claude Fable 5.1 on the gate, the exclusions and the repo's own obligations. Between them, nine findings, all CONFIRMED by reproduction. No defect in the swept code. Every finding was in the documentation of the change or in the tooling, which is where this PR was actually weak.

Fixed in 3c1b9050

Fable 1 — .editorconfig said the opposite of what it does. The header called using placement "a recorded preference at silent severity, not a gate", and the block comment said "the rule is silent. New code should follow it; nothing fails if it does not." Line 35 is outside_namespace:warning and the build fails on it. This is the worst class of stale comment, because it tells the next contributor the inverse of the truth. Both rewritten, and they now state the general contract: any :warning entry in this file is a build error, not just these two.

Fable 2 — the PR body described the superseded revision. It documented inside_namespace, "four outliers fixed", and a mutation table quoting IDE0065 as "must be placed inside". Body replaced. The mutation table in it was rerun at this head rather than recalled; IDE0065 now reads "must be placed outside of a namespace declaration".

Fable 3 — dead configuration in the fixture exemption. csharp_style_namespace_declarations = block_scoped:silent configured nothing: IDE0161 never reports on a compilation unit declaring more than one namespace. Removed, verified by building without it. The comment now records why the placement exemption is load-bearing, which is the part I had not written down: 25 of those usings are relative aliases resolving against the enclosing namespace, so hoisting them means fully qualifying every one.

Fable 4 — the migrations exemption had the wrong reason. I cited #407's freeze. The real reason is that dotnet ef migrations add scaffolds block-scoped namespaces, which fable verified by scaffolding one at this head. The freeze explains why existing files are not rewritten; it says nothing about migrations nobody has written yet, which is what the exemption actually protects. The old comment also claimed migrations are "never hand-edited", and 20260913212515_AddBusinessRecordChronology.cs is hand-written and file-scoped.

Fable 7 — the two required artifacts. AGENTS.md paragraph added, and docs/decisions/985-csharp-style-gate.md with a row in the decisions index. A build-gating convention absent from the file every brief is drawn from defeats this PR's own stated purpose.

Fable 6, partly — a latent bug in the script. Its mixed-file branch collapsed blank lines across the whole file, while a comment twenty lines below said a global pass is "churn this change has no business making". I fixed that in the main path earlier and the mixed path I added later reintroduced it; since exactly one file takes that path the diff never showed it. Narrowed to the seam and shared between both branches. The docstring also said 586 files and claimed mixed files are skipped, both wrong. Re-running the fixed script on main still reproduces all 589 files byte for byte, so the fix is behaviour-preserving here and removes a hazard rather than changing output.

Where I did not take the recommendation

Fable 6 recommended deleting tools/style/move-usings-outside.py. The argument is good: after merge the gate rejects the script's only input, so it can never act again, and dotnet format style --diagnostics IDE0065 IDE0161 is the supported remedy. I am keeping it for this PR anyway, because its value is live right now — it is what makes a 589-file diff reviewable in one command instead of 589 reads, and astra's byte-for-byte reproduction is only checkable while it is here. Its docstring now says dotnet format is the route for new code, and the AGENTS.md paragraph names that command rather than the script. Deleting it after merge is a reasonable follow-up and I would not argue against it.

Corrections to my own earlier claims on this PR

  • dotnet format does have a code fix for IDE0065. I said it did not, in the PR discussion and in the review brief. Fable tested it and it works; I reran it and confirmed. The claim was wrong.
  • The FlockScope collision spans six files, not two. Astra reproduced 13 errors across six, 11 CS0118 and two CS0234, reaching CouplingMatrixRealTreeTests, TableOwnerRealModelTests and the tenant-bypass tests. I had earlier corrected a right number (six) into a wrong one (two) by counting only the files inside the renamed namespace; the collision hits any file whose own namespace makes the sibling FlockScope namespace visible.
  • My own file census over-counted by four. A line-regex says 593 files need conversion; the script says 589. The four extra are AdapterReachTests, AdapterTierTests, CouplingMatrixTests and ModuleLedgerTests, whose apparent usings sit inside raw string literals building synthetic Cluckwork.Temp.* source for the scanners to walk. The script was right and my regex was not. The "593 legacy files" figure in the old .editorconfig comment came from the same error.

The finding that changes how this should be read

I told the owner the blast radius of EnforceCodeStyleInBuild was the open question, because the property runs the IDE analyzers rather than enabling two named rules. Fable settled it by reflecting over every DiagnosticAnalyzer in the SDK's two CodeStyle assemblies: 121 distinct ids, 116 default Hidden, four Info, one Warning that only reports when IDE0005 is raised, and no .globalconfig, ruleset, AnalysisLevel, AnalysisMode or dotnet_diagnostic.* anywhere in the repo. So the effective set is exactly {IDE0065, IDE0161} by construction, not by luck. My concern was unfounded, and it is now measured in the decision record so nobody re-derives it. Cost is about one second on an eight-second full build.

Both reviewers independently judged hard-error-now the right call over warning-only, on the ground that TreatWarningsAsErrors is already global so "warning" means a log line nobody reads, and an analyzer cannot distinguish new files from old.

Verification at 3c1b9050

dotnet build Cluckwork.sln: 0 warnings, 0 errors. Domain 495, Application 544, AppHost 10 passing. TenancyDocsFreshnessTests + RenameAccountDocs 20 passing, SchemaDocsTests 4 passing — run because this commit adds tracked documentation, and those guards walk every tracked file. Integration is CI's to run.

Review loop stopped here deliberately, not abandoned: two rounds, both of which found real problems, and every confirmed finding is either fixed above or answered with a reason.

FlockScope is also the production class Cluckwork.Infrastructure.Persistence.FlockScope.
Once usings sit outside the namespace, the sibling test namespace shadows that
type: 13 errors across six files (11 CS0118, two CS0234). A test namespace
should never share a simple name with a production type.
@mforce
mforce force-pushed the chore/editorconfig-csharp-style branch from 3c1b905 to f8bc254 Compare October 1, 2026 01:23
Two .editorconfig rules at :warning, plus EnforceCodeStyleInBuild beside the
existing TreatWarningsAsErrors, so each is a build error. 590 files swept by
tools/style/move-usings-outside.py; rerun it on main to re-derive the diff.
AGENTS.md paragraph and docs/decisions/985-csharp-style-gate.md record the
gate, its measured blast radius (exactly IDE0065 and IDE0161) and the two
exemptions.
@mforce
mforce force-pushed the chore/editorconfig-csharp-style branch from f8bc254 to 08f9ef3 Compare October 1, 2026 01:24
@mforce
mforce merged commit 05d7e7a into main Oct 1, 2026
27 of 31 checks passed
@mforce
mforce deleted the chore/editorconfig-csharp-style branch October 1, 2026 05:00
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