build: standardize repository tooling and documentation - #29
Merged
rent-a-developer merged 17 commits intoSep 8, 2026
Merged
Conversation
.editorconfig is about to require two-space indentation for .csproj, .props, .targets, .slnx, .config, .xml and .DotSettings. Nothing in the tidy pipeline applies that: .csharpierignore keeps CSharpier away from XML, `dotnet format` does not touch XML whitespace, and the ReSharper profile only reorders C# members. So the transformation is done here, on its own, before the content changes land. Leading tabs only, two spaces each. No other byte moved: no reflow, no tab elsewhere on a line, and in DbConnectionPlus.slnx.DotSettings the XML-encoded file-layout string is the value of an element and is left exactly as it was. Every file still parses and every evaluated MSBuild property is unchanged. This commit is mechanical and belongs in .git-blame-ignore-revs; a later commit records it. Refs #28 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every version in the repository now lives in Directory.Packages.props, grouped by what it is for, so a bump is one edit in one file instead of a hunt through eleven projects that could disagree with each other. The package-consumption projects opt out of central management on purpose: they exist to consume the produced packages the way a stranger would, and pinning their versions centrally would make that test lie. Packing is opt-in. Every project is IsPackable=false at the root and the six shipping projects say yes themselves, so a new test, sample or tool project cannot ship by forgetting to opt out. The four packed files move to the repository-root Directory.Build.targets, because IsPackable is set by the project after every .props file has been read and a condition on it in a .props file is always false. Packaging changes that follow from that: - <Version> moves to the repository-root Directory.Build.props, so one file answers what version the repository is at, and scripts read it from there. - The licence is the SPDX expression MIT rather than a file reference, which nuget.org and licence tooling can read without opening the package. LICENSE.md is still packed, unchanged. - PackageRequireLicenseAcceptance is false: acceptance prompts are for restricted licences, and one in front of an MIT package trains people to click through the prompts that matter. - Package validation runs against the published 4.0.0, so removing or changing a public signature fails the pack instead of reaching nuget.org. - NuGetAudit runs at "all" scope, where transitive advisories are. NU1901-NU1904 stay warnings rather than errors, so a newly published advisory in the transitive graph cannot break a build that touched nothing. The shared test settings move to tests/Directory.Build.props, which imports the repository root explicitly because MSBuild stops at the first Directory.Build.props it finds. No version, framework floor, package id, assembly name or dependency was changed. The six packages, their contents and their dependency groups are byte-identical apart from the two licence lines and the release notes URL above. Refs #28 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nothing The `var` decision had two of its three halves written down; the third was left to the analyzer default, so what the repository wanted depended on a tool version. It is now stated - explicitly typed where nothing on the line names the type - and deliberately silent rather than an error, because raising it would rewrite every such declaration and that is a code change, not a configuration one. The rest is the same kind of tidying: - One indentation rule for every structured configuration format, two spaces, matching what Visual Studio and Rider do to XML by themselves. The files were re-indented in the previous commit. - The top-level-statements preference said the opposite of what all three executables here do, each of which has an explicit Program class and one of which names it in <StartupObject>. - The naming rule for non-field members demanded plain PascalCase, which rejects every Method_Scenario_ShouldExpectedOutcome test name AGENTS.md requires. It now uses the same style as the type rule instead of contradicting a documented convention, and pascal_case, referenced by nothing afterwards, is gone. - usingDirectivesPlacement in stylecop.json was read by no enabled rule - SA1200 is off, because CSharpier decides placement - so it stated a policy nothing enforced. - The ReSharper file layout now pins StaticFieldReorderingPolicy explicitly. It was the implicit default; saying it means a ReSharper release cannot change member order without a commit that says so. Measured: byte-identical output on a full cleanup run before and after. No .cs file changes as a result of this - the C# half of the contract already held. The comment about "zero violations across 283 files" is replaced by the fact that makes it true: the rules are errors and the build is green. Refs #28 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
preflight.ps1 was a formatter that also verified: it rewrote the working tree and then reported on what it had written. It now checks by default and writes nothing but build output and caches, and -Fix is what applies. That is the order a pre-commit gate needs - you look at what is wrong before something changes it under you - and it is what lets CI run the same script the contributor runs. tidy-code.ps1 -Check honours that too. Checking the whole solution needs the formatters to run, and they edit in place, so -Check -Scope all now copies the tracked and untracked tree to a temporary directory, initializes a throwaway repository there, runs the pipeline against the copy and reports the diff. The real working tree is never touched, and the temporary directory is deleted only when its path starts with the prefix the script itself created. While measuring that, a defect: `dotnet format style --no-restore` on a tree whose projects are not restored DELETES using directives it believes are unused, reports success, and leaves source that does not compile. On a fresh clone it removed `using Humanizer;` and `using LinkDotNet.StringBuilder;` from the shipping library - twelve compiler errors from a formatting step that said it had done nothing wrong. --no-restore is gone from that step and the reason is written into the function, so it is not put back as a performance optimization. The rest: - clean-build-artifacts.ps1 deleted .xml files next to a .dll by a name heuristic, which reaches documents that are not build output; it now deletes only what it can attribute to a build, skips reparse points rather than following a junction out of the tree, and refuses any path that resolves outside it. - extract-release-notes.ps1 writes to artifacts/release/ with the rest of the generated output instead of the repository root, and strips the link definitions that a changelog section carries and a release page cannot resolve. - public-api-guard.ps1 no longer asks a contributor for a version bump. The version is the maintainer's, at release time. - benchmarks.ps1 and update-public-api.ps1 resolve the repository root and restore the caller's location and PATH in a finally block, so an interrupted run leaves the shell as it found it. - Every script requires PowerShell 7 explicitly rather than failing later with a syntax error. No behaviour a package consumer can see changes here. Refs #28 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The code of conduct was referenced but did not exist: CONTRIBUTING.md said "please note we have a code of conduct" and there was nothing to follow. It is Contributor Covenant 2.1, with a real reporting address. The enforcement ladder is deliberately left out - a single-maintainer project cannot staff a four-stage escalation process, and publishing one it will not run is worse than not publishing it. CONTRIBUTING.md becomes the page a contributor can work from without opening another: what to install, what the integration tests need from Docker, what the Native AOT gate needs from a C++ toolchain, how branches and commits are named, which tool owns formatting, style and ordering, and what to run before pushing. Previously it deferred the working detail to AGENTS.md, a file written for agents, and left the human contributor reading around it. Two policy corrections, applied here and mirrored in the agent guidance and the pull-request template: - **Contributors never bump a version.** The old text asked for a SemVer bump in the pull request. The version, the changelog date, promoting PublicAPI.Unshipped.txt and the tag are the maintainer's, at release time. A contributor who bumps it makes every concurrent pull request conflict. - **An AI agent's branch is named after what it does**, not after who typed it. The `claude/` and `codex/` prefixes are gone; there is one branch convention. Refs #28 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
README.md was 1323 lines - the entire manual on the landing page, where a reader looking for "what is this and how do I install it" had to scroll past every feature to find out, and where anything that changed had to be found among a thousand lines of prose. It is now 181 lines: what the library is, how to install it, one example, and where to go next. The material is not lost, it is filed. Six guides carry the how-to - querying, parameters and temporary tables, entity mapping and CRUD, configuration, custom adapters, Native AOT - and three reference pages carry the lookups: a one-line index of the public API, the comparison table, and the benchmark numbers. Each page is one subject, so a change lands in one file. - PACKAGE_README.md is the NuGet package page and now reads like one: 92 lines, the pitch and the install line, then links. It is not a place to maintain a second copy of the manual. - DESIGN-DECISIONS.md moves under docs/, where the documentation lives. The links that pointed at the root copy - CHANGELOG.md and the AOT consumer's README - move with it. - docfx.json moves to build/docfx/, with the rest of the build configuration, so docs/ holds content and nothing else. Its content mappings are rewritten for the new layout; the generated API URLs are unchanged, so no published link breaks. - CHANGELOG.md gains the empty `## [Unreleased]` heading Keep a Changelog expects, so a contributor has a section to write into without inventing a version number. No entry is added by this branch. `docfx build --warningsAsErrors` is clean, and every relative link and anchor in the repository resolves. Refs #28 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ed cache The Native AOT gate is the only check in the repository that can see silent trimming damage, so what it consumes has to be exactly what would be published, and nothing else. - The six packages are confirmed from their **nuspecs** rather than from the file names on disk: id, version and target frameworks are read out of each package, so a stale artifact from an earlier pack, or a missing adapter, is a failure with a name rather than a run against the wrong input. - The consumer package cache is emptied on every run and NUGET_PACKAGES points at it, so a package restored from the machine's global cache cannot stand in for the one just built. Proved by setting an inherited NUGET_PACKAGES deliberately and watching the consumer's closure still land in the local directory. - RestoreConfigFile names the consumer's own NuGet.config, so the feeds are the consumer's and not whatever the machine has configured, and the publish runs from the consumer directory for the same reason. - The whole body is in try/finally: PATH, NUGET_PACKAGES and the working directory are restored however the run ends, so an interrupted gate does not leave the shell pointed at a throwaway cache. Refs #28 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Codex adapter discarded the payload and ran the tidy script with no arguments, which formats every file git reports as changed. An agent editing one file therefore reformatted the contributor's unrelated work in progress, in the same edit, without saying so. Both adapters now format only the path the triggering edit carried: Claude Code's `tool_input.file_path`, and for Codex the patch text parsed for `*** Add File:`, `*** Update File:` and `*** Move to:`. A delete is skipped - there is nothing left to format - and a rename formats the new name only, because the old one no longer exists. There is **no fallback**. An unparseable payload formats nothing and says so in one line; the old behaviour was the fallback, and it was worse than doing nothing. - Every path is resolved and then required to sit under the resolved repository root, so `../` traversal is refused and so is a junction or symlink pointing out of the tree - the check happens after resolution, which follows it. Paths under bin/ and obj/ are refused too. - The public-API guard now fires only when the edited file is actually a PublicAPI.*.txt, instead of running on every edit. - Concurrent edits serialize on a named mutex derived from the repository path, shared by both agents and distinct per clone, with a timeout so a stuck hold cannot wedge later edits. - Both adapters exit 0 whatever happens. A PostToolUse failure cannot undo an edit that already happened, so failing only costs the agent its next step; they report through their own channel instead. - Each adapter runs the shared script as a child `pwsh -File` process. The shared scripts end with `exit`, which inside the adapter's own process would end the adapter before it could write its response - fatal for Codex, where a hook that writes no JSON is a hook that failed. - A missing formatter is reported with the command that installs it, and nothing is installed on your behalf. The wiring in .claude/settings.json and .codex/hooks.json is unchanged: the triggers are still Edit|Write and apply_patch|Edit|Write, and neither was broadened to every shell command. Two defects were found by feeding hand-written payloads to the adapters in throwaway clones: PowerShell unrolls a one-element list on the way out of a function, so a single-file patch threw and formatted nothing; both adapters now wrap the result as [string[]] @(...). Refs #28 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ewers read-only The agent guidance and CONTRIBUTING.md described the same repository differently, which is how an agent ends up doing what a contributor is told not to. They now say the same things in the same words: the branch convention with no agent-specific prefix, the commit convention, the companion edits a change needs, and that a version bump is the maintainer's at release time and nobody else's. - The two custom reviewers lose `Bash` and keep Read, Grep and Glob. They review; they report findings and cite file and line. A reviewer that can run a shell can also write a file, and "please do not edit" is not a sandbox. Where a checklist wants a diff or a build, it now says to ask the caller for it. - The `model:` and `model_reasoning_effort` pins are removed. They are the operator's choice, not the repository's, and a pinned model in a checked-in agent definition goes stale silently. - .agents/README.md documents the hooks the previous commit scoped: what they format, the three rules that keep them from reaching anything else, why each adapter runs a child process, how to review and trust the declared hooks in Codex, and what to run by hand when no hook covered the edit - a shell redirection, an editor outside the agent, a `git apply`, or an untrusted session. - The links that pointed at the root DESIGN-DECISIONS.md follow it under docs/. Refs #28 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dependabot ran monthly and split what is reviewed together. It now runs quarterly with grouped pull requests: one review of "what does the build reject now" instead of one per analyzer, and one driver review instead of five. Drivers, analyzers and the test stack take minor and patch only, because a major version of a driver is a breaking change for the adapter that wraps it and deserves its own integration run. The coupled families - AutoFixture, Testcontainers, xunit, the CodeQL actions - move together at every update type, including major, because they will not resolve against each other across a major boundary and a split major produces a branch that cannot restore. Security updates are untouched and stay immediate. The quarterly cadence is about noise; a published advisory is not noise, and NuGetAudit reports the same advisories in every build in the meantime. The NSubstitute ignore was re-verified against AutoFixture.AutoNSubstitute's own nuspec rather than assumed: the [2.0.3, 6.0.0) constraint is still there. .gitignore now ignores generated output and nothing else. It ignored `**/AssemblyInfo.cs`, which would silently hide a hand-written one - an ignore rule that matches an authored file is worse than no rule, because the file never gets added and the build works for you and for nobody else. The bare `obj` and `bin` entries are anchored so a source directory that happens to be called `bin` is not swallowed. The pull-request checklist stops asking every contributor to confirm items that do not apply to their change - the AOT gate, the integration run, the public-API snapshot are conditional now - and no longer asks for a version bump. `docfx.json` moved to build/docfx/ in an earlier commit; the workflow's DOCFX_CONFIG follows it. Refs #28 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The commit that re-indented the MSBuild and configuration files is mechanical: leading tabs to two spaces, 534 lines in 17 files, no non-whitespace byte moved. Recording it here keeps `git blame` on those files pointing at whoever wrote the line rather than at the transformation. It is a separate commit because a commit cannot record its own id.⚠️ Merge this branch with a MERGE COMMIT. "Squash and merge" and "Rebase and merge" rewrite every SHA on the branch, the entry above stops resolving, and git does not warn - blame quietly goes back to pointing at the tool. CI's lint job fails on an unresolvable revision, which is what makes that visible. Refs #28 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three unrelated fixes to the same class of problem - text that goes stale without anyone noticing. **Counts are gone.** "The six shipping libraries", "the two test projects", "all five adapters", "the four database servers", "both target frameworks": every one of those is a number that a new project, adapter or target framework silently falsifies, in about thirty places across the build files, the workflows, the agent guidance and the documentation. They now name what they mean - the shipping libraries, the adapters, net8.0 and net10.0 - and a number survives only where the items are listed in the same breath. **History is gone.** A reader wants to know how the repository works, not how it used to: what .gitignore once ignored, what clean-build-artifacts.ps1 once deleted, what readiness gate the CI job once carried by hand, and which sections once lived in the README. Each of those keeps the rule it was justifying and drops the story. **"Why not just use Dapper?" is back in README.md and PACKAGE_README.md**, as the first section after the introduction in both, which is where the question is actually asked. Moving it to a reference page put the one thing a new reader wants first behind a link. It is the same text as before, and docs/reference/ comparison.md is deleted rather than kept as a second copy that would drift. Also in .editorconfig: `csharp_style_var_elsewhere` becomes `true:silent`. It said the repository preferred an explicit type where the right-hand side does not name one, and the repository does not - `var` is used that way 455 times in src/ and the explicit form essentially never. It stays silent, because that case is a readability judgement rather than something a build should reject. The top-level-statements comment is removed; the setting says what it does. Refs #28 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Preflight" says an aviation metaphor; it does not say when to run the script or what it decides. This is the gate you run before a commit - hygiene, style, formatting, member ordering, a Release build and the unit suite - and the name now says so, next to the pre-release gate that runs the heavier jobs. Every reference moves with it: the contribution and agent guidance, the pull-request template, both editor hook adapters, the sibling scripts and the comment in Directory.Build.props. Nothing about the script's behaviour changes; CI never referenced it by name. Refs #28 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6442d2c left them in the scripts' comment-based help and console output, in the feature-request template and in the package-consumption readme and project file: "the six shipping projects", "all six packages", "all five adapters", "obliges all five". Same rule as before - name the thing, not how many there are - and the one count that stays is the one whose items are listed in the same sentence. Two occurrences are deliberately left alone, because changing them would be the first C# edit on this branch: the `all five providers` remark in src/DbConnectionPlus/Readers/EnumerableReader.cs and the `all five adapters` comments in tests/package-consumption/AllAdaptersConsumer/Program.cs. Refs #28 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pushing to a public repository and finding out from a red X is a slow way to learn something this machine could have told you in twenty minutes. scripts/pre-release-gate.ps1 runs everything CI checks that can honestly be checked locally, in CI's order: the ignored-revision check, the tidiness check, a Release build, the unit suite and the full integration matrix, the documentation build with warnings as errors, the pack with package validation, the Native AOT gate on net8.0 and net10.0 against the packages just packed, and the all-adapters package consumer against the same ones. It ends in one line - `PASSED: All checks passed.` or `FAILED: Check X failed. See output.` - and it stops at the first failure, because the later steps consume what the earlier ones produce and a second failure caused by the first only buries the one that matters. What it deliberately does not do is reimplement CI. Each omission is named in the script's own help with the reason: CodeQL and the dependency review need GitHub's services, the Codecov upload needs CI's token, the Pages deployment needs the Pages environment, publication needs Trusted Publishing and a tag, and the Linux legs and the .NET-8-SDK-only leg need machines this one is not. A dirty working tree is reported rather than rejected - CI tests the commit you push, so anything uncommitted is untested by definition, but you may still be iterating. `-Version` adds the two checks CI runs immediately before it publishes: the version declared in Directory.Build.props matches, and CHANGELOG.md holds exactly one dated, non-empty section for it. They are opt-in because a branch push publishes nothing. `-SkipIntegrationTests` drops the Docker requirement and `-SkipNativeAot` the C++ toolchain, and both say so in the output rather than passing quietly. One thing the design has to get right, and it is written into the function so it survives editing: a step reports through a script-scoped flag, not by returning a value. A script block's output IS its return value in PowerShell, so capturing it would swallow every line the build and the test run print and - worse - make a failing step that printed anything at all evaluate as a pass. Verified end to end on this machine: a full run passes (11809 tests, 0 failed; both AOT frameworks; the consumer's every assertion), a deliberately wrong -Version fails at the first step with a named reason and exit 1, and the skip paths run exactly what they say. Refs #28 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A fresh clone whose files are CRLF passes every check both gates make and then fails the Release build with one "Was not formatted." error per file in the solution. Every one of those errors names CSharpier, which is not the problem, and the two steps that could have said so had just reported a clean working tree and a tidy one. Neither was lying. Git normalizes on read, so nothing that asks git about content can see a line ending: `git status` reports no change, and tidy-code.ps1 -Check -Scope all commits its disposable copy before it runs the tools, which stores every file as LF whatever was on disk - CSharpier rewriting CRLF to LF is then a change `git diff` reports as nothing at all. The build is the first step that reads the bytes, and all it can say about them is that the formatter is unhappy. scripts/verify-line-endings.ps1 asks the one question that is not laundered. `git ls-files --eol` reports, per file, what git stored, what is on disk and the attributes that decide both, and it writes nothing. Both columns are checked and only one of them is CI's: the stored column is what CI renormalizes to catch a file committed past .gitattributes, and the on-disk column is the one only a working copy can answer, because a CI checkout is written from the index seconds earlier and cannot disagree with it. The expected ending is read from each file's own eol attribute rather than assumed, and a file that declares none is left alone. The failure names the files and the fix for each column: `git checkout-index --force --all` for a working tree that drifted, `git add --renormalize .` for a repository that stored the wrong thing. Both gates run it, before their tidiness step rather than after, and the pre-release gate now has it where CI has it - the lint job's second check, which was simply missing here. Both gates also ask CSharpier about the tree itself once the copy-based check passes: that costs about a second and turns "275 files are unformatted" into a named file and a reason, whether the cause is the line endings or a .csharpierrc or .editorconfig in a directory ABOVE the repository, which a copy under the temp directory never sees. Verified by reproducing the original failure - a clone with CRLF sources builds into one error per file - and then running both gates over it: each stops with the two offending files named, and `git checkout-index --force --all` returns the tree to a state where both pass and the build succeeds. Refs #28 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t does
`git checkout-index --force --all` is what the check told you to run against a working tree whose endings
had drifted, and on the tree that prompted it, it did nothing at all: exit 0, not a byte written, every
file still CRLF. The advice was tested against a tree whose files had been rewritten after checkout, which
is not the case it exists for.
Git skips a file whose stat information matches the index before it looks at --force. A tree that was
written wrong by whatever produced it - a clone through a tool that ignored the eol attribute, an unpacked
archive, a sync client - matches the index perfectly, which is precisely why `git status` calls it clean
and why nothing noticed. `git checkout -- .` is a no-op for the same reason. The files have to be gone
before git will write them again, which is the recipe gitattributes(5) gives for exactly this:
git ls-files | ForEach-Object { Remove-Item -LiteralPath $_ -Force }
git checkout -- .
The failure message now says that, says what it costs - it deletes tracked files before restoring them, so
uncommitted work has to be committed or stashed first - and says what it means if the endings come back:
something other than git is writing them. The script's help records why checking the tree out again cannot
work, so the next person to reach for it finds the answer before running it.
Verified on a clone of this repository put into the state that produced the report: the checkout wrote CRLF
so the index stat matched and `git status` was clean, `git checkout-index --force --all` left all 387 files
CRLF and exited 0, and the delete-then-checkout pair brought every one of them back to LF with a clean
working tree.
Refs #28
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4 tasks
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
rent-a-developer
deleted the
chore/issue-28-harmonize-repository-tooling
branch
September 8, 2026 14:38
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #28
Standardize the build configuration, formatting, scripts, CI, AI guidance, and documentation, while preserving the public API and the runtime behavior.
docs/, and separate the DocFX configuration and output from the authored documentation.Merge instruction
Merge with a merge commit. Do not squash.
.git-blame-ignore-revsrecords the mechanical commit IDs, and a squash merge replaces those commits.Validation
Implementation draft: every check below is pending. Complete this description from the actual diff before opening the pull request, and tick a box only after that check has actually passed.
.git-blame-ignore-revsand verified withgit blameNo release version change and no publication are included. Record any approved exception here.