Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Do not commit code generated by `GenerateTools` — generated code is regenerate
## Critical rules (read this every session)

1. **At PR-creation time, follow the [PR-creation flow](docs/agents/release-and-versioning.md#pr-creation-flow) in `docs/agents/release-and-versioning.md`.** Every PR gets a `target/YYYY` label (`target/2026`; legacy v10 work uses `target/v10`). Breaking changes additionally get a `breaking-change` label, a `⚠️ Breaking change` callout, and a `CHANGELOG.md` entry — recorded under the **next yearly major** (breaking changes are batched to the year cut, not shipped mid-year). A breaking-change PR may target **`main` only** — never a `release/YYYY` stable train. This is non-negotiable — review will block.
2. **Default to backwards compatibility.** Prefer additive over breaking changes. Before changing a public signature, removing an API, renaming a package, or altering an on-disk format, ask: can this be additive instead? `[Obsolete]` markers, transition shims (see `src/Shims/` + `Fallout.SourceGenerators.TransitionShimGenerator`), the `[Experimental("FALLOUT0xx")]` opt-in escape hatch for not-yet-stable surface, feature flags, and overload-based extension are all preferred to a hard break. When a breaking change is genuinely unavoidable, it lands on `main`, is held for the next yearly major, and follows rule #1's flow — the break must be deliberate, named, and migration-pathed in the CHANGELOG. See [#262](https://github.com/ChrisonSimtian/Fallout/issues/262) for the broader discussion.
2. **Default to backwards compatibility.** Prefer additive over breaking changes. Before changing a public signature, removing an API, renaming a package, or altering an on-disk format, ask: can this be additive instead? `[Obsolete]` markers, transition shims (see `src/Shims/` + `Fallout.SourceGenerators.TransitionShimGenerator`), the `[Experimental("FALLOUT0xx")]` opt-in escape hatch for not-yet-stable surface, feature flags, and overload-based extension are all preferred to a hard break. When a breaking change is genuinely unavoidable, it lands on `main`, is held for the next yearly major, and follows rule #1's flow — the break must be deliberate, named, and migration-pathed in the CHANGELOG. See [#262](https://github.com/ChrisonSimtian/Fallout/issues/262) for the broader discussion. The `[Experimental]` convention (diagnostic-ID scheme + registry) is documented in [docs/agents/conventions.md](docs/agents/conventions.md#experimental-for-opt-in-unstable-apis) and [docs/experimental-apis.md](docs/experimental-apis.md).
3. **Central package versions only** — add to `Directory.Packages.props`, never `Version=` inline.
4. **Tests next to code** — every `src/Foo` has a `tests/Foo.Tests` sibling. Mirror namespaces.
5. **Stay on xUnit + FluentAssertions + Verify.** Don't introduce new test frameworks.
Expand Down
23 changes: 23 additions & 0 deletions docs/agents/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,29 @@ Three groups: conventions to respect, things never to do, and the tool-wrapper r
- **No IDE-specific style files committed.** `.editorconfig` and `*.DotSettings` were removed during the takeover — relying on `dotnet format` defaults 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 [`LICENSE`](../../LICENSE) at the repo root, and NuGet packages declare MIT via `PackageLicenseExpression`. 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 under `src/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](#experimental-for-opt-in-unstable-apis) below and the [diagnostic-ID registry](../experimental-apis.md).

## `[Experimental]` for opt-in unstable APIs

Per [ADR-0004 §5](../adr/0004-calendar-versioning-and-dual-pace-channels.md), public APIs that aren't ready to commit to a stability guarantee are marked with [`System.Diagnostics.CodeAnalysis.ExperimentalAttribute`](https://learn.microsoft.com/dotnet/api/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).

```csharp
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](../experimental-apis.md) in the same PR that introduces it.
- **Consumers must explicitly opt in.** `ExperimentalAttribute` is an *error-by-default* diagnostic: code that touches the API fails to compile until the consumer suppresses the exact ID — `#pragma warning disable FALLOUT001` around 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 trunk (`main`/edge), there is no cross-branch cherry-pick — deleting the `[Experimental]` line is the whole promotion. This is what lets stabilised work feed back into the regular channels without a divergent fork. Adding *or* removing `[Experimental]` is **not** a breaking change.
- **Channel discipline differs.** On `main`/edge, experimental churn is expected and the attribute is a courtesy. On a `release/YYYY` **stable train**, any risky-but-shipped public surface **must** wear `[Experimental]` — that contract is what keeps the stable line trustworthy while still carrying new work.
- **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.

## What not to do

Expand Down
57 changes: 57 additions & 0 deletions docs/experimental-apis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Experimental APIs

Fallout marks not-yet-stable public APIs with [`System.Diagnostics.CodeAnalysis.ExperimentalAttribute`](https://learn.microsoft.com/dotnet/api/system.diagnostics.codeanalysis.experimentalattribute) so consumers opt into instability deliberately. This page is the **canonical registry of allocated `FALLOUT0xx` diagnostic IDs**.

See [ADR-0004 §5](adr/0004-calendar-versioning-and-dual-pace-channels.md#5-experimental-for-opt-in-unstable-apis) for the decision and [the agent conventions](agents/conventions.md#experimental-for-opt-in-unstable-apis) for the contributor rules.

## How it works

An experimental API carries a diagnostic ID:

```csharp
using System.Diagnostics.CodeAnalysis;

[Experimental("FALLOUT001")]
public sealed class NewPluginHost
{
// ...
}
```

`ExperimentalAttribute` is an **error-by-default** diagnostic. Code that touches the API will not compile until you explicitly suppress the exact ID, which is your conscious opt-in to an API that may change without notice:

```csharp
#pragma warning disable FALLOUT001 // opting into an experimental API
var host = new NewPluginHost();
#pragma warning restore FALLOUT001
```

or, project-wide, in your `.csproj`:

```xml
<PropertyGroup>
<NoWarn>$(NoWarn);FALLOUT001</NoWarn>
</PropertyGroup>
```

## Diagnostic-ID scheme

- IDs use the form `FALLOUT0xx` and are allocated **sequentially**.
- An ID is **never reused** — once retired it stays retired, so a suppression can never silently re-bind to a different API.
- Every allocation is recorded in the registry table below, in the same PR that introduces the attribute.
- **Promoting an API to stable means deleting the `[Experimental]` attribute** (the feature already rode the trunk — no cross-branch cherry-pick). The ID's row moves to **Promoted** status and the ID is retired, not recycled. Adding or removing `[Experimental]` is not a breaking change.
- **Channel discipline:** on `main`/edge the attribute is a courtesy; on a `release/YYYY` stable train any risky-but-shipped public surface **must** wear it.

## Registry

Status values: **Experimental** (live, opt-in), **Promoted** (attribute removed, now stable — ID retired), **Withdrawn** (API removed before promotion — ID retired).

| ID | Surface | Introduced | Status | Notes |
|----|---------|------------|--------|-------|
| _none allocated yet_ | — | — | — | First experimental API to land claims `FALLOUT001`. |

<!--
Allocation example (do not uncomment unless a real API is marked):

| `FALLOUT001` | `Fallout.X.NewPluginHost` | 2026.2 | Experimental | Plugin-host entry point; shape may change while the plugin SDK firms up. |
-->