Conventions
Three groups: conventions to respect, things never to do, and the tool-wrapper recipe.
Conventions worth respecting
- Centralized package versions — add new packages to
Directory.Packages.props, never inline. Adding a meaningful library (not a tiny transitive helper)? Add a row to docs/dependencies.md in the same PR — reviewers will ask. - Test naming follows AV1600 behavior-focused style. Write test names as short, present-tense descriptions of observable behavior, not method calls. Example good:
Missing_changelog_does_not_add_a_full_changelog_link_to_release_notes. Example bad:GetNuGetReleaseNotes_WithMissingChangelog_AndGitHubRepository_DoesNotThrow. Focus on "what happens" not "what method is called" (see AV1600). - Test files and classes use
Specssuffix. Name test projectsFoo.Specs, test filesFooSpecs.cs, and test classesFooSpecs. UseSpecsnotTestorTests— it clarifies that the class describes the expected behavior (specification) of the subject under test. - Tool wrappers: copy/paste from neighbours; cover full commands; use
<c>,<a>,<ul>/<ol>,<em>,<para/>inhelp; don't writesecret: falseordefault: xxx. See Tool wrapper recipe below. - Tests next to code, separate folder: every
Fooproject undersrc/has a siblingFoo.Testsproject undertests/. Mirror the namespace. - No IDE-specific style files committed.
.editorconfigand*.DotSettingswere removed during the takeover — relying ondotnet formatdefaults and review. - Telemetry opt-out is set in test runs (
FALLOUT_TELEMETRY_OPTOUT=true). Keep it that way. - No per-file license headers. The MIT notice lives in
LICENSEat the repo root, and NuGet packages declare MIT viaPackageLicenseExpression. Per-file headers were stripped in v11 (one source of truth + the header URL would have rotted on the repo-org transfer). Vendored third-party code keeps its own copyright headers — don't touch those (e.g. files undersrc/Persistence/Fallout.Persistence.Solution/retain Microsoft's MIT notice). [Experimental]for opt-in unstable public APIs. Not-yet-stable public surface is marked with[Experimental("FALLOUT0xx")]rather than held back or shipped silently. See the[Experimental]convention below and the diagnostic-ID registry.[Obsolete]with aDiagnosticIdfor deprecations. Deprecated public surface carries[Obsolete(..., DiagnosticId = "FALLOUTOBS0xx")]soTreatWarningsAsErrorsconsumers can suppress a single deprecation. See the[Obsolete]convention below and the diagnostic-ID registry.
Writing style for issues, PRs, and commits
Applies to AI tools and humans. Many readers are non-native English speakers — keep it readable.
- Be short and precise. Lead with the point.
- Prefer bullet points over paragraphs.
- Use plain, simple English. Short sentences, common words.
- Cut filler: no preamble, no hedging, no AI-flavored padding.
- Say what changed and why. Drop the rest.
Covers GitHub issues, PR titles/bodies/comments, and commit messages.
[Experimental] for opt-in unstable APIs
Per ADR-0004 §5 (channel ladder amended by ADR-0008), public APIs that aren't ready to commit to a stability guarantee are marked with System.Diagnostics.CodeAnalysis.ExperimentalAttribute instead of being held back or shipped silently. The attribute ships in the .NET 8+ BCL — no package reference needed (the repo targets .NET 10).
using System.Diagnostics.CodeAnalysis;
[Experimental("FALLOUT001")]
public sealed class NewPluginHost
{
// ...
}
Rules:
- Diagnostic-ID scheme:
FALLOUT0xx. Each experimental surface gets its own ID (e.g.FALLOUT001), allocated sequentially and never reused — a retired ID stays retired. Register every allocation in the diagnostic-ID registry in the same PR that introduces it. - Consumers must explicitly opt in.
ExperimentalAttributeis an error-by-default diagnostic: code that touches the API fails to compile until the consumer suppresses the exact ID —#pragma warning disable FALLOUT001around the call site, or<NoWarn>$(NoWarn);FALLOUT001</NoWarn>in their project. Opting into instability is therefore a conscious, per-API choice — which is right for a framework (a product devs build on), not an app. - Promoting to stable = removing the attribute. Because the feature already rode the
maintest lane, deleting the[Experimental]line is the whole promotion — no special cross-branch dance. This is what lets stabilised work feed into the production line without a divergent fork. Adding or removing[Experimental]is not a breaking change. - Channel discipline differs. On the
main(preview) test lane, churn is expected and the attribute is a courtesy. On arelease/YYYYproduction line, any risky-but-shipped public surface must wear[Experimental]— that contract is what keeps the stable line trustworthy while still carrying new work. With theexperimentalbranch retired (ADR-0008),[Experimental]is now the primary mechanism for isolating unstable surface onmain— including breaking changes batched toward the yearly major. - Don't apply it speculatively. Because the diagnostic is error-by-default, marking an API that's already used internally breaks the build everywhere it's referenced. Only add
[Experimental]to a genuinely not-yet-stable API, and suppress every internal usage in the same change so the build stays green.
[Obsolete] for deprecating public APIs
When a public API is on its way out, mark it with System.ObsoleteAttribute and give it a DiagnosticId. This is the sanctioned deprecation path under AGENTS.md rule #2 — keep the old surface working (usually bridging to the replacement) while steering consumers to the new one. DiagnosticId/UrlFormat ship in the .NET 5+ BCL — no package reference needed (the repo targets .NET 10).
using System;
[Obsolete(
"Use [GitHubActionsInputAttribute] instead. Removed in 2027.x.x.",
DiagnosticId = "FALLOUTOBS001",
UrlFormat = "https://github.com/Fallout-build/Fallout/blob/main/docs/obsolete_apis.md")]
public string[] OnWorkflowDispatchOptionalInputs { get; set; } = new string[0];
Rules:
- Adding
[Obsolete]is not a breaking change. It's warning-level by default, so existing code keeps compiling — this is why it's preferred over a hard break. The removal is the break, and it's batched to the next yearly major. State the removal target in the message (e.g.Removed in 2027.x.x.). - Always set a
DiagnosticId. Without one the compiler reports the genericCS0618, so aTreatWarningsAsErrorsconsumer can only fix every usage at once or blanket-NoWarnall deprecations. A per-deprecationFALLOUTOBS0xxID lets them suppress just this one while they migrate. - Diagnostic-ID scheme:
FALLOUTOBS0xx. Allocated sequentially and never reused, from a sequence separate from theFALLOUT0xxused by[Experimental]. Register every allocation in the diagnostic-ID registry in the same PR that introduces the attribute. - Keep the deprecated surface functional. Prefer bridging the old member to the new one (e.g. fold legacy arrays into the typed replacement) over leaving it inert, and suppress the internal bridge usage with
#pragma warning disablescoped to the exact ID.
CI pipeline & triggers
Shaped by milestone #18 and the ADR-0004 ladder (amended by ADR-0008, which collapsed experimental into main). Invariants:
- Feature branches run zero CI until a PR is opened. Push triggers list only long-lived branches; nothing fires on
feature/*,bugfix/*, etc. until they're PR'd againstmain/release/*/support/*. Do not add a working-branch pattern to anyOnPush*/branches:trigger. - The Linux PR gate (
ubuntu-latest) is the only required check — runs on PRs to the long-lived branches. main(push) →-previewto GitHub Packages (preview.yml) — the sole continuous publisher now thatexperimental.ymlis deleted.- Cross-platform
windows/macosare gated to release intent — PR-to-release/*/support/*or av*tag push only. They do not run onmainpushes. ("Onmainwe've got our edge.") concurrency: cancel-in-progresson every build workflow exceptrelease.yml— never cancel a publish mid-flight.- Canonical CI-ignore paths:
docs/**,.assets/**,**/*.md— applied to every PR/push trigger. - The
ubuntu-latest/windows-latest/macos-latestworkflows are generated frombuild/Build.CI.GitHubActions.cs— edit the attributes + constants there and regenerate (./build.sh), never hand-edit the.yml.preview.yml/release.ymlare hand-written. - Every publishing lane runs
Testbefore it publishes (#324).preview.ymlandrelease.ymlboth run a singledotnet fallout Test Packinvocation — NUKE executes it as discrete internal stages (Restore → Compile → Test → Pack) and fails at the breaking stage, so a test failure stops the job before the push step. Don't split a lane into separatedotnet fallout Compile/Test/Packsteps — each invocation re-runs the dependency graph (double-compile); the single invocation is the staged build. - Caching (#328): every workflow caches
~/.nuget/packages+.fallout/temp, keyed onglobal.json+**/*.csproj+Directory.Packages.props(the dependency-affecting set), with arestore-keys:prefix fallback for partial restores. There is nopackages.lock.jsonto add to the key, and build outputs (bin/obj) are deliberately not cached (stale-artifact correctness risk).
What not to do
- Don't reintroduce
source/— production code lives undersrc/, tests undertests/. Same forimages/(now.assets/). - Don't add
main(or any working-branch pattern) to the push triggers of the cross-platform workflows — they're release-intent-gated on purpose (milestone #18 / #318 / #326). - Don't add
submodules: recursiveto checkouts — there are no submodules (no.gitmodules); it's a dead init step. - Don't add
secret/defaultdefaults to tool JSON files (see CONTRIBUTING.md). - Don't introduce a new test framework or assertion library — stay on xUnit + FluentAssertions + Verify.
- Don't commit
output/or anybin//obj/directory. - Don't commit
fallout-global.slnor otherfallout-global.*files — they're generated byGenerateGlobalSolution. - Don't bypass
Directory.Packages.propsorDirectory.Build.targets. - Don't reintroduce
.editorconfigor*.DotSettingswithout a maintainer-level decision — they were intentionally removed.
Tool wrapper recipe
When asked to add or extend a tool wrapper:
- Find the closest existing tool under
src/Fallout.Common/Tools/<Tool>/<Tool>.json. - Copy its shape; cover a full command with all arguments.
- Run
./build.ps1 GenerateToolsto verify it generates cleanly. - Do not commit the generated
.csoutput.