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
9 changes: 5 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ Guidance for all coding agents working in this repository (included by CLAUDE.md

## Agent Context Regions — maintenance rule

Non-trivial source files carry embedded context regions (`#region Purpose`, `#region Design`,
optionally `#region Open Questions`). These are part of the code, not decoration:
Every source file carries a `#region Purpose` block (enforced at build time by **TWPA0004**);
files with design decisions also carry `#region Design`, and optionally `#region Open Questions`.
These are part of the code, not decoration:

- **When you edit a file that has regions, reconcile them with your change before finishing.**
A Design region describing the old approach is a bug you just introduced.
- **When you create a non-trivial source file, add `Purpose` and `Design` regions** at the top,
before the namespace.
- **When you create a source file, add `#region Purpose`** (one honest line minimum) at the top,
before the namespace — plus `Design` where there are genuine decisions to record.
- **When you read an unanswered question in `#region Open Questions` that you can answer,**
answer it (or implement the answer and remove the pair).

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Broaden contract nullability analyzer to api / grpc / foundation contracts

> **CLOSED — ABSORBED BY
> [[084-convention-analyzer-require-region-purpose-in-every-source-file-rename-assembly-wire-via-directorybuildprops-absorb-082]]
> (2026-07-02).** The assembly (renamed `timewarp-architecture-convention-analyzers`) is now wired
> once via `source/Directory.Build.props`, reaching **every** project under `source/` — a superset
> of this task's scope. The broadened sweep found one real violation
> (`web-server/configuration/sample-options.cs` — `= string.Empty` + validated `NotEmpty()`),
> fixed same-axis; api/grpc/foundation contracts were clean. Stretch items (SetValidator
> composition-following, code-fix provider) remain unpicked — revive as their own tasks if wanted.

## Description

Task 080 shipped `ContractNullabilityValidatorAnalyzer` (TWPA0002: `string?` + `NotEmpty()`/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Convention analyzer: require `#region Purpose` in every source file (rename assembly, wire via Directory.Build.props, absorb 082)

## Description

Enforcement backstop for the agent-context-regions convention (follow-up to
[[050-010-add-region-purpose-and-region-design-to-all-migrated-source-files]]), built on the
080 lesson: a convention nobody enforces decays (regions sat at 4/304 for a year).

**Key design decision (maintainer, 2026-07-02): the rule is UNIVERSAL, not threshold-based.**
Every compiled `.cs` file requires `#region Purpose` — trivial files get a one-line region
("Assembly marker enabling typed assembly references.") instead of an exemption. Rationale:

- A "non-trivial file" heuristic (member counts, line thresholds) misclassifies in both
directions and pushes complexity into the analyzer + suppressions. A universal rule is
**exact**: no threshold to tune, no judgment to encode, no escape hatches to manage.
- Even trivial files benefit — "why does this AssemblyMarker exist" is a real question a
one-line Purpose answers; a dead-code reference file gets "kept as reference for X".
- Cost is one 3-line insertion per trivial file (~140 files), paid once, mechanical.
- Generated code is excluded structurally (`GeneratedCodeAnalysisFlags.None`), not by judgment.

`#region Design` is **not** analyzer-enforced: "has design decisions worth recording" cannot be
judged mechanically. Design stays governed by the AGENTS.md maintenance rule and review.

## Diagnostics

- **TWPA0004** — compiled, non-generated `.cs` file lacks a `#region Purpose` block.
Severity Warning (build-breaking under the repo's warnings-as-errors).

## Architecture / wiring (this is the other half of the task)

1. **Rename** `timewarp-architecture-contract-analyzers` → `timewarp-architecture-convention-analyzers`
(the assembly now hosts contract rules TWPA0002/0003 *and* repo-wide convention rules; the
"contract" name stops fitting). Update: csproj + folder, `.slnx`, test project reference,
`web-contracts.csproj` reference (superseded by step 2), AnalyzerReleases files.
2. **Wire once in `source/Directory.Build.props`** — `<ProjectReference OutputItemType="Analyzer"
ReferenceOutputAssembly="false">` with a condition excluding the analyzer/attributes projects
themselves (self-reference guard). Remove the now-redundant per-csproj reference in
web-contracts. Scope = `source/` only (tests deliberately excluded — a test's why is its name).
3. **This absorbs [[082-broaden-contract-nullability-analyzer-to-api-grpc-foundation-contracts]]**:
TWPA0002/0003 reach api/grpc/foundation contracts through the same wiring (inert in projects
without `AbstractValidator` usage). Close 082 into this task.
4. **Template effect:** generated `dotnet new timewarp-architecture` apps inherit both region and
nullability enforcement out of the box.

## Recipe (080-proven: wire + reconcile in the same PR, tree never red)

- [x] `PurposeRegionAnalyzer` (TWPA0004): syntax-tree action; region-name match; generated code
excluded via `GeneratedCodeAnalysisFlags.None`; registered in AnalyzerReleases.
- [x] Fixie tests (5): without = flagged; with = clean; **anywhere-in-file counts** (placement is
skill guidance); Design-only still flagged; generated clean. All 21 analyzer tests green.
- [x] Assembly renamed `timewarp-architecture-convention-analyzers`; wired once in
`source/Directory.Build.props` (self-excluded; `IsAspireProjectResource="false"` to keep the
Aspire AppHost from treating the analyzer ref as an app resource — ASPIRE004).
- [x] Backfill: 51 formulaic files scripted (assembly markers, global-usings, identical one-liners);
88 small files via 8-agent fan-out (one-line Purpose, Design only where genuine — includes
the 050-010 skips: dead-code files now say *why they're kept*). **304/304 coverage.**
- [x] TWPA0002/0003 broadening caught **one real violation outside web-contracts**:
`web-server/configuration/sample-options.cs` `SampleOption = string.Empty` (+ `NotEmpty()`
via the options validator) → `= null!`. api/grpc/foundation contracts were clean.
- [x] `dev build` green (0/0); analyzer 21/21, sourcegen 14/14, web-server integration 11 passed.
- [x] 082 closed as absorbed; `agent-context-regions` skill updated (universal Purpose, no
triviality exemption, TWPA0004 noted) — flow commit `25e288b`, synced.

## Result — analyzer defect found & fixed via the test framework's hidden suppression pass

The testing framework prepends `#pragma warning disable TWPA0004` and verifies the diagnostic
disappears. The first implementation reported at `TextSpan(0,0)` — a position *before* any leading
pragma takes effect — making the diagnostic **unsuppressable** (a real defect, not a test
artifact). Fix: anchor on the file's **first token**; trivia-only files (fully commented out /
`#if false`) have no active code and are structurally exempt.

## Notes

- Freshness remains out of scope — no tool can verify a region is *true*; that stays with the
AGENTS.md reconcile rule and PR review. This analyzer guarantees presence only.
- Consumers who want the softer stance can downgrade via
`dotnet_diagnostic.TWPA0004.severity = suggestion` in `.editorconfig` — document, don't build,
the escape hatch.
12 changes: 11 additions & 1 deletion source/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,15 @@
<ItemGroup Label="Shared Package Assets">
<None Include="$(RepositoryRoot)assets/logo.png" Pack="true" PackagePath="" />
</ItemGroup>


<!-- Repo-wide convention analyzers (analyzer-only assembly — no source generators):
TWPA0002/0003 (contract nullability must agree with FluentValidation presence rules) and
TWPA0004 (every source file carries a #region Purpose block). Wired once here so every
project under source/ — and every app generated from the template — is enforced. -->
<ItemGroup Condition="'$(MSBuildProjectName)' != 'timewarp-architecture-convention-analyzers'">
<ProjectReference Include="$(MSBuildThisFileDirectory)analyzers/timewarp-architecture-convention-analyzers/timewarp-architecture-convention-analyzers.csproj"
OutputItemType="Analyzer" ReferenceOutputAssembly="false"
IsAspireProjectResource="false" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#region Purpose
// Marker type providing a typed reference to this assembly (registration, scanning, InternalsVisibleTo).
#endregion

namespace TimeWarp.Architecture.Analyzers;

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#region Purpose
// Project-wide using directives so individual files omit repeated imports.
#endregion

global using Microsoft.CodeAnalysis;
global using Microsoft.CodeAnalysis.CSharp;
global using Microsoft.CodeAnalysis.CSharp.Syntax;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
#region Purpose
// Marks a contract class for FastEndpoint source generation — the generator emits the endpoint class, so none is hand-written.
#endregion

#region Design
// Lives in a small runtime-attributes assembly, separate from the analyzer, so contract projects
// reference plain attributes without taking a Roslyn dependency.
// EndpointType optionally overrides the generated endpoint's base class (default BaseFastEndpoint).
#endregion

namespace TimeWarp.Architecture.Attributes;

[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#region Purpose
// Marker type providing a typed reference to this assembly (registration, scanning, InternalsVisibleTo).
#endregion

namespace TimeWarp.Architecture.Attributes;

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ Rule ID | Category | Severity | Notes
--------|----------|----------|-------
TWPA0002 | Design | Warning | ContractNullabilityValidatorAnalyzer: nullable property has a NotEmpty()/NotNull() presence rule
TWPA0003 | Design | Warning | ContractNullabilityValidatorAnalyzer: required property has a fabricated empty-string default
TWPA0004 | Documentation | Warning | PurposeRegionAnalyzer: source file lacks a #region Purpose block
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
#region Purpose
// Global usings for the convention-analyzers assembly.
#endregion

global using Microsoft.CodeAnalysis;
global using Microsoft.CodeAnalysis.CSharp;
global using Microsoft.CodeAnalysis.CSharp.Syntax;
global using Microsoft.CodeAnalysis.Diagnostics;
global using Microsoft.CodeAnalysis.Text;
global using System.Collections.Immutable;
global using System.IO;
global using System.Linq;
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#region Purpose
// Enforces the agent-context-regions convention: every source file carries a #region Purpose block.
#endregion

#region Design
// The rule is deliberately UNIVERSAL rather than "non-trivial files only": any triviality
// heuristic (member counts, line thresholds) misclassifies in both directions and pushes
// complexity into suppressions. Trivial files satisfy the rule with a one-line Purpose instead
// of an exemption, so the check needs no judgment. Generated code is excluded structurally via
// GeneratedCodeAnalysisFlags.None (auto-generated headers, *.g.cs), not by heuristic.
// A #region Design block is intentionally NOT enforced — "has decisions worth recording" cannot
// be judged mechanically; that half of the convention is governed by AGENTS.md and review.
// Placement anywhere in the file counts: position is skill guidance, not analyzer scope.
#endregion

namespace TimeWarp.Architecture.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class PurposeRegionAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "TWPA0004";

private static readonly DiagnosticDescriptor Rule =
new
(
DiagnosticId,
title: "Source file lacks a #region Purpose block",
messageFormat: "File '{0}' has no '#region Purpose' block; add one (a single // line suffices) stating what the file is for",
category: "Documentation",
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "Every source file embeds agent/human context in a '#region Purpose' block. Trivial files use a one-line Purpose rather than being exempt. See the agent-context-regions skill."
);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxTreeAction(AnalyzeTree);
}

private static void AnalyzeTree(SyntaxTreeAnalysisContext context)
{
SyntaxNode root = context.Tree.GetRoot(context.CancellationToken);

bool hasPurposeRegion = root
.DescendantNodes(descendIntoTrivia: true)
.OfType<RegionDirectiveTriviaSyntax>()
.Any(static region => RegionName(region) == "Purpose");

if (hasPurposeRegion) return;

// Anchor on the first token (not position 0): a diagnostic located before a leading
// `#pragma warning disable` could never be suppressed by it. Trivia-only files (fully
// commented out / #if false) have no active code and are not flagged.
SyntaxToken firstToken = root.GetFirstToken();
if (firstToken.RawKind == 0) return;

string fileName = Path.GetFileName(context.Tree.FilePath);
context.ReportDiagnostic(Diagnostic.Create(Rule, firstToken.GetLocation(), fileName));
}

// "#region Purpose" -> "Purpose" (the name rides in the directive's trailing preprocessing message).
private static string RegionName(RegionDirectiveTriviaSyntax region)
{
string text = region.ToString();
int keywordEnd = text.IndexOf("#region", System.StringComparison.Ordinal);
return keywordEnd < 0 ? string.Empty : text.Substring(keywordEnd + "#region".Length).Trim();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<IsRoslynComponent>true</IsRoslynComponent>
<!-- Convention-enforcement DiagnosticAnalyzers only (no source generators), so this assembly is
safe to reference from contract projects without triggering the FastEndpoint generator. -->
safe to reference from every project — including contracts — without triggering the
FastEndpoint generator. Wired repo-wide via source/Directory.Build.props. -->
<!-- Suppress RS1041: Analyzer intentionally targets net10.0 for CLI usage -->
<!-- Suppress CA1031: Analyzers must be resilient and catch general exceptions -->
<!-- Suppress CA1062: Roslyn framework guarantees non-null analysis-context parameters -->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
#region Purpose
// IModule registration hook for the Api application layer's services.
#endregion

#region Design
// ConfigureServices deliberately throws: the layer has no services to register, and the throw
// makes accidental composition loud instead of silently succeeding. Replace the throw with real
// registrations when the layer gains services.
#endregion

namespace TimeWarp.Architecture.Api.Application;

public class ApiApplicationModule : IModule
Expand Down
4 changes: 4 additions & 0 deletions source/container-apps/api/api-application/assembly-marker.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#region Purpose
// Marker type providing a typed reference to this assembly (registration, scanning, InternalsVisibleTo).
#endregion

namespace TimeWarp.Architecture.Api.Application;

/// <summary>
Expand Down
4 changes: 4 additions & 0 deletions source/container-apps/api/api-application/global-usings.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#region Purpose
// Project-wide using directives so individual files omit repeated imports.
#endregion

global using Microsoft.Extensions.Configuration;
global using Microsoft.Extensions.DependencyInjection;
global using OneOf;
Expand Down
4 changes: 4 additions & 0 deletions source/container-apps/api/api-contracts/assembly-marker.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#region Purpose
// Marker type providing a typed reference to this assembly (registration, scanning, InternalsVisibleTo).
#endregion

namespace TimeWarp.Architecture.Api.Contracts;

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#region Purpose
// Per-feature constants slot: FeatureGroup names the WeatherForecast group used to categorize its endpoints in API metadata.
#endregion

namespace TimeWarp.Architecture.Features.WeatherForecasts;

public static class FeatureAnnotations
Expand Down
4 changes: 4 additions & 0 deletions source/container-apps/api/api-contracts/global-usings.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#region Purpose
// Project-wide using directives so individual files omit repeated imports.
#endregion

global using Ardalis.GuardClauses;
global using FluentValidation;
global using OneOf;
Expand Down
4 changes: 4 additions & 0 deletions source/container-apps/api/api-domain/assembly-marker.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#region Purpose
// Marker type providing a typed reference to this assembly (registration, scanning, InternalsVisibleTo).
#endregion

namespace TimeWarp.Architecture.Api.Domain;

/// <summary>
Expand Down
4 changes: 4 additions & 0 deletions source/container-apps/api/api-domain/global-usings.cs
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
#region Purpose
// Project-wide using directives so individual files omit repeated imports.
#endregion

// global using xyz;
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#region Purpose
// Marker type providing a typed reference to this assembly (registration, scanning, InternalsVisibleTo).
#endregion

namespace TimeWarp.Architecture.Api.Infrastructure;

/// <summary>
Expand Down
4 changes: 4 additions & 0 deletions source/container-apps/api/api-infrastructure/global-usings.cs
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
#region Purpose
// Project-wide using directives so individual files omit repeated imports.
#endregion

// global using xyz;
4 changes: 4 additions & 0 deletions source/container-apps/api/api-server/assembly-marker.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#region Purpose
// Marker type providing a typed reference to this assembly (registration, scanning, InternalsVisibleTo).
#endregion

namespace TimeWarp.Architecture.Api.Server;

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#region Purpose
// Base message-bearing error type for Api server features; a convention anchor with no derived types in the template.
#endregion

namespace TimeWarp.Architecture.Features;

public class BaseError
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#region Purpose
// Base exception type for Api server feature failures; a convention anchor with no derived types in the template.
#endregion

namespace TimeWarp.Architecture.Features;

public class BaseException : Exception
Expand Down
4 changes: 4 additions & 0 deletions source/container-apps/api/api-server/global-usings.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#region Purpose
// Project-wide using directives so individual files omit repeated imports.
#endregion

global using FastEndpoints;
global using FluentValidation;
global using Microsoft.AspNetCore.Mvc;
Expand Down
4 changes: 4 additions & 0 deletions source/container-apps/aspire/aspire-app-host/constants.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#region Purpose
// Aspire AppHost resource names, pinned to the ServiceNames the apps use for service discovery.
#endregion

namespace TimeWarp.Architecture.Aspire;

internal class Constants
Expand Down
4 changes: 4 additions & 0 deletions source/container-apps/aspire/aspire-app-host/global-usings.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#region Purpose
// Project-wide using directives so individual files omit repeated imports.
#endregion

global using Aspire.Customization.AppHost;
global using Aspire.Hosting.Yarp;
global using Aspire.Hosting.Yarp.Transforms;
Expand Down
Loading
Loading