mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 03:43:11 +00:00
chore(governance): enforce backend architecture and quality gates (#31)
* docs(governance): add canonical governance docs, unified quality-gate script, and CI parity - ARCHITECTURE_AND_CODE_QUALITY.md: canonical layering, EF allowlist, transaction/commit convention, cancellation, migrations, error disclosure, Big-O/perf, ADR exceptions (supersedes BACKEND_ARCHITECTURE.md) - QUALITY_GATES.md: gate inventory + pass/fail/skip semantics; local==CI - REVIEW_AND_PR_FRAMEWORK.md: exact-head review, board-backed regression inventory, security/perf evidence, ADR exceptions, no godfile theater - AGENTS.md: repo-specific delta + precedence pointers - scripts/governance-check.sh: unified G1 restore + G2 ArchitectureTests + G3 changed-file format (portable bash) - .github/workflows/architecture-quality.yml: call the same local script - ArchitectureTests.cs: add business-service interface-dependency invariant * fix(governance): make backend gate complete * fix(ci): enforce governance on every pull request
This commit is contained in:
parent
7d245eb717
commit
833fb816ee
7 changed files with 709 additions and 28 deletions
34
.github/workflows/architecture-quality.yml
vendored
34
.github/workflows/architecture-quality.yml
vendored
|
|
@ -2,7 +2,6 @@ name: Architecture and changed-file quality
|
|||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main, dev]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
@ -21,31 +20,10 @@ jobs:
|
|||
with:
|
||||
dotnet-version: "8.0.x"
|
||||
|
||||
- name: Restore
|
||||
run: dotnet restore SeaHavenIndustries.sln
|
||||
|
||||
- name: Verify architecture boundaries
|
||||
run: >-
|
||||
dotnet test
|
||||
Api.SeaHavenIndustries.Tests/Api.SeaHavenIndustries.Tests.csproj
|
||||
--no-restore
|
||||
--filter FullyQualifiedName~ArchitectureTests
|
||||
|
||||
- name: Verify formatting of changed C# files
|
||||
# CI and local run the same complete repository gate.
|
||||
- name: Repository quality gate
|
||||
shell: bash
|
||||
run: |
|
||||
mapfile -t files < <(
|
||||
git diff --name-only --diff-filter=ACMR \
|
||||
"${{ github.event.pull_request.base.sha }}" \
|
||||
"${{ github.event.pull_request.head.sha }}" \
|
||||
-- '*.cs'
|
||||
)
|
||||
|
||||
if (( ${#files[@]} == 0 )); then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
dotnet format SeaHavenIndustries.sln \
|
||||
--no-restore \
|
||||
--verify-no-changes \
|
||||
--include "${files[@]}"
|
||||
env:
|
||||
BASE_REF: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_REF: ${{ github.event.pull_request.head.sha }}
|
||||
run: bash scripts/governance-check.sh
|
||||
|
|
|
|||
70
AGENTS.md
Normal file
70
AGENTS.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# AGENTS.md — SeaHaven backend (repo-specific delta)
|
||||
|
||||
This file is the **repo-specific delta** for the Seahaven backend. It layers on
|
||||
top of the operator/workspace agent baseline and does not repeat it. This file
|
||||
may add stricter backend rules but may never weaken the workspace baseline.
|
||||
|
||||
## Canonical governance documents (precedence)
|
||||
|
||||
1. `ARCHITECTURE_AND_CODE_QUALITY.md` — *what* the rules mean (canonical;
|
||||
supersedes the older `BACKEND_ARCHITECTURE.md`, which is retained only as
|
||||
historical reference).
|
||||
2. `QUALITY_GATES.md` — *how* rules are enforced (commands + CI mapping).
|
||||
3. `REVIEW_AND_PR_FRAMEWORK.md` — *who* reviews, in what order, with what
|
||||
evidence.
|
||||
|
||||
When these conflict with each other, the one that *owns* the topic wins
|
||||
(meaning → architecture doc; execution → quality gates; process → review
|
||||
framework). When unsure, ask; do not silently pick.
|
||||
|
||||
## Architecture in one line
|
||||
|
||||
```text
|
||||
Controller -> I{Feature}Service -> I{Feature}DataService -> ApplicationDbContext
|
||||
```
|
||||
|
||||
- Controllers depend on `I{Feature}Service` only — never `DbContext`/EF/concrete
|
||||
services/data services.
|
||||
- Business services depend on feature-specific data/query/command interfaces —
|
||||
never `DbContext`, never `IConfiguration`, never a generic repository.
|
||||
- Data services own EF; EF/`DbContext` lives only on the documented
|
||||
infrastructure allowlist (data services, `ApplicationDbContext`, migrations,
|
||||
Identity/composition, transactions inside data services).
|
||||
- One commit convention: `SaveChangesAsync(CancellationToken)` inside the owning
|
||||
data service is the atomic boundary; explicit transactions stay inside data
|
||||
services.
|
||||
- Tenant scope is server-derived; authorization is enforced at service entry.
|
||||
- No HTTP response leaks exception/stack/SQL/credential/crypto detail; secrets
|
||||
are never logged.
|
||||
|
||||
Full detail: `ARCHITECTURE_AND_CODE_QUALITY.md`.
|
||||
|
||||
## Running the complete gate (local == CI)
|
||||
|
||||
```bash
|
||||
bash scripts/governance-check.sh
|
||||
```
|
||||
|
||||
The command restores, verifies architecture and changed-file formatting, builds
|
||||
Release, and runs the full test suite. CI (`architecture-quality` workflow)
|
||||
calls the same script. See `QUALITY_GATES.md`.
|
||||
|
||||
## Hard rules (deviation needs an ADR; no wildcard suppressions)
|
||||
|
||||
- EF allowlist, dependency direction, no client exception disclosure,
|
||||
server-derived tenant scope, one commit convention, cancellation forwarding
|
||||
(verified, not just signed).
|
||||
- Suppress a single diagnostic with a cited ADR only — never global sweeps.
|
||||
- See `ARCHITECTURE_AND_CODE_QUALITY.md` §10 for the ADR exception process.
|
||||
|
||||
## Do not touch without explicit instruction
|
||||
|
||||
- Product behavior, database **migrations**, secrets, dependency versions, and
|
||||
files unrelated to the current task.
|
||||
- The `.ai-config-kit-sidecar-write-scope` marker file is a transient
|
||||
authorization artifact — **never commit it**.
|
||||
|
||||
## Reporting
|
||||
|
||||
State every changed file with a one-line summary. Report each gate as
|
||||
pass / fail / skipped / not-run with evidence; never infer a pass from silence.
|
||||
273
ARCHITECTURE_AND_CODE_QUALITY.md
Normal file
273
ARCHITECTURE_AND_CODE_QUALITY.md
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
# Architecture and Code Quality (canonical)
|
||||
|
||||
> **Status: canonical.** This document owns the *meaning* of the backend's
|
||||
> architecture and code-quality rules. On any conflict with the older
|
||||
> `BACKEND_ARCHITECTURE.md`, **this document governs**; that file is retained
|
||||
> only as historical reference pending removal.
|
||||
>
|
||||
> Companion documents:
|
||||
> - `QUALITY_GATES.md` — *how* each rule is enforced (commands + CI mapping).
|
||||
> - `REVIEW_AND_PR_FRAMEWORK.md` — *who* reviews, in what order, with what
|
||||
> evidence, and how exceptions are recorded.
|
||||
> - `AGENTS.md` — precedence and repo-specific agent delta.
|
||||
|
||||
## 1. Layering model
|
||||
|
||||
The backend is a feature-oriented service boundary over Entity Framework Core:
|
||||
|
||||
```text
|
||||
HTTP controller -> I{Feature}Service -> I{Feature}DataService -> ApplicationDbContext
|
||||
```
|
||||
|
||||
The interfaces are **architectural seams**, not a mandate to wrap every class or
|
||||
every EF method. Add an interface when it separates HTTP, business,
|
||||
persistence, or external I/O responsibility *and* enables behavior-focused
|
||||
testing through the public contract.
|
||||
|
||||
| Layer | May depend on | May not depend on |
|
||||
|-------|---------------|-------------------|
|
||||
| API controller | `I{Feature}Service`, `ILogger`, framework types | `DbContext`, concrete services, concrete data services, raw SQL, business workflow code |
|
||||
| Business service (`SeaHaven.Services`) | `I{Feature}DataService`, `IOptions<T>`, `ILogger`, pure ports (`IServicePorts`, `IDispatchEmailPort`, …) | `DbContext`, `IConfiguration`, concrete data services, a generic repository |
|
||||
| Data service (`SeaHaven.DataServices`) | `ApplicationDbContext`, EF Core | Business decisions, HTTP concerns |
|
||||
| Composition / infrastructure | `Program.cs`, `*Module.cs`, migrations, Identity | Being depended on by controllers or business services |
|
||||
|
||||
### 1.1 Controllers
|
||||
|
||||
- Parse HTTP input, authorize the caller, invoke a business-service interface,
|
||||
and map the result to the established HTTP contract.
|
||||
- Depend **only on `I{Feature}Service` interfaces** — never on `DbContext`, EF,
|
||||
concrete service implementations, or concrete data-service implementations.
|
||||
- Contain no persistence queries and no business workflows.
|
||||
- Map known failures explicitly; let the centralized exception boundary handle
|
||||
unexpected failures (see §5).
|
||||
|
||||
### 1.2 Business services
|
||||
|
||||
- Own validation, business decisions, orchestration, and **transaction intent**
|
||||
(the *what* of semantic atomicity — see §4).
|
||||
- Depend on **feature-specific data/query/command interfaces**, never on a
|
||||
generic repository and never on `DbContext`.
|
||||
- Use `IOptions<T>` for typed configuration. Never inject `IConfiguration`.
|
||||
- Keep feature responsibilities cohesive. Split a service when it has
|
||||
independently changing business reasons — **not** because it crossed an
|
||||
arbitrary line count (see §9, "no godfile threshold theater").
|
||||
|
||||
### 1.3 Data services
|
||||
|
||||
- Own EF Core query shape, persistence, batching, and **transaction mechanics**
|
||||
(the *how* of commits — see §4).
|
||||
- Accept `CancellationToken` on new I/O methods and propagate it (see §6).
|
||||
- Use `AsNoTracking()` for read-only queries.
|
||||
- Project only required columns for list/read models; include full graphs only
|
||||
when the caller needs them.
|
||||
- Page unbounded collections at the database.
|
||||
- Avoid query-in-loop / save-in-loop patterns; prefer set-based reads and
|
||||
batched writes **while preserving the workflow's failure and transaction
|
||||
semantics**.
|
||||
- Do **not** introduce a generic repository over EF Core. Feature data services
|
||||
expose intent-revealing operations.
|
||||
|
||||
### 1.4 Infrastructure ownership and the EF allowlist
|
||||
|
||||
EF Core / `DbContext` is owned by approved infrastructure only. The explicit
|
||||
allowlist of places that may touch EF directly:
|
||||
|
||||
- `SeaHaven.DataServices.Implementation.*` (feature data services).
|
||||
- `Data.SeaHavenIndustries` — the `ApplicationDbContext`, entity configuration,
|
||||
and **migrations** (see §7).
|
||||
- `SeaHaven.DataServices.DependencyInjection.DataServicesModule` — context
|
||||
registration and lifetime.
|
||||
- Identity / authentication composition in `Program.cs` and the DI modules
|
||||
(`AddDbContext`, `AddIdentity`, token/options wiring).
|
||||
- Transaction mechanics inside data services when semantic atomicity requires
|
||||
it (see §4).
|
||||
|
||||
Anything outside this allowlist that references EF / `DbContext` is an
|
||||
architecture violation. New allowlist entries require an ADR (see §10).
|
||||
|
||||
## 2. Tenant scope and authorization
|
||||
|
||||
- **Tenant scope is server-derived.** Filtering by tenant/customer/owner is
|
||||
applied in the data access layer from authenticated principal claims, never
|
||||
from client-supplied request bodies or query strings.
|
||||
- **Authorization remains at service entry.** A business service's public method
|
||||
is the trust boundary: it authorizes the caller and resolves the effective
|
||||
tenant scope before doing work. Controllers express HTTP-level authorization
|
||||
(`[Authorize]`, route/policy) and then delegate; they do not re-implement
|
||||
service-level authorization.
|
||||
- Data services receive an already-resolved, server-derived scope; they must not
|
||||
invent authorization rules of their own.
|
||||
|
||||
## 3. Dependency-direction enforcement (mechanical)
|
||||
|
||||
`ArchitectureTests` (in `Api.SeaHavenIndustries.Tests`) enforces mechanical
|
||||
dependency direction by reflection over constructor parameters:
|
||||
|
||||
1. Controllers do not inject `DbContext`, concrete data services, or concrete
|
||||
service implementations.
|
||||
2. Business services do not inject `DbContext` or `IConfiguration`.
|
||||
3. Business services depend on interfaces for their data/persistence
|
||||
dependencies, not on concrete data-service or service implementations.
|
||||
4. **Discovery must not silently pass on an empty set.** Each invariant asserts
|
||||
the discovered type set is non-empty, so the gate **fails** if reflection
|
||||
ever returns zero controllers/services (e.g., after a namespace move). A
|
||||
passing run therefore means "real types were checked," not "nothing was
|
||||
found."
|
||||
|
||||
These tests cover *dependency direction*. Behavior lives in behavior tests
|
||||
through public interfaces. Do **not** add tests merely to prove a linter or
|
||||
analyzer itself works — test the product behavior the rule protects. Do not
|
||||
claim full runtime properties from constructor reflection; it proves wiring
|
||||
shape, not execution behavior.
|
||||
|
||||
## 4. Transaction semantics and the one commit convention
|
||||
|
||||
- **Services own semantic atomicity.** A business service decides which set of
|
||||
operations constitutes a unit of work and expresses that intent.
|
||||
- **Scoped infrastructure owns transaction mechanics.** The actual commit
|
||||
boundary is `DbContext.SaveChangesAsync(...)` and lives **inside a data
|
||||
service**. A service must not sprinkle independent `SaveChanges` calls across
|
||||
data-service boundaries in a way that breaks the semantic unit.
|
||||
- **The repo documents one commit convention:** a single
|
||||
`SaveChangesAsync(CancellationToken)` inside the owning data service is the
|
||||
atomic commit boundary. This is the current convention observed across the
|
||||
data-service layer.
|
||||
- When a unit of work genuinely requires multiple statements to be all-or-
|
||||
nothing, use EF Core's transaction (`IDbContextTransaction`) **inside the data
|
||||
service**. Explicit transactions are infrastructure (EF allowlist), owned by
|
||||
data services, never by controllers or business services.
|
||||
- Do not "optimize" atomicity away: collapsing several semantically independent
|
||||
saves into one final save changes partial-success behavior and is a business
|
||||
decision, not mechanical cleanup.
|
||||
|
||||
## 5. Error disclosure and structured logging (security)
|
||||
|
||||
- **No HTTP response may expose** arbitrary exception messages, stack traces,
|
||||
or SQL/provider/path/credential/crypto detail. Known failures are mapped to
|
||||
explicit public messages; unexpected failures are funneled through the
|
||||
centralized boundary (`SanitizedErrors`), which returns a stable message plus
|
||||
a correlation reference and never echoes internal exception text.
|
||||
- **Correlated structured internal logs** record the original exception at
|
||||
`Error` level with a correlation id, so incidents are traceable without
|
||||
leaking detail to the client.
|
||||
- **Secrets are never logged.** Configuration values, tokens, connection
|
||||
strings, and credentials are treated as redacted; structured log templates
|
||||
must not interpolate secret-bearing fields.
|
||||
- The `SanitizedErrorsTests` guard the public-facing contract (stable message,
|
||||
correlation id present, original exception text absent, original logged at
|
||||
`Error`).
|
||||
|
||||
## 6. Cancellation
|
||||
|
||||
- New async I/O methods accept a `CancellationToken` where the public contract
|
||||
permits and **propagate it** to EF Core / downstream I/O.
|
||||
- **Do not falsely claim propagation.** A `CancellationToken` parameter that is
|
||||
ignored, swallowed, or replaced with `default`/`CancellationToken.None` at the
|
||||
call site is a defect, not a courtesy. The signature is a contract.
|
||||
- Enforcement combines **behavior tests** (assert the token reaches the data
|
||||
service / EF call for changed I/O paths) and **analyzers** where available.
|
||||
Either is acceptable evidence; silence is not. For changed paths, review must
|
||||
confirm forwarding, not merely the presence of the parameter.
|
||||
|
||||
## 7. Database migrations
|
||||
|
||||
Migration gates require **actual-provider** evidence, not reflection or
|
||||
assumptions:
|
||||
|
||||
- **Actual-provider discovery** — the migration provider is the configured
|
||||
database provider (e.g., SQL Server / Npgsql), discovered from the runtime
|
||||
configuration, not inferred from package references.
|
||||
- **Snapshot consistency** — `ModelSnapshot` must be consistent with the model
|
||||
after the migration set (`dotnet ef ... --output` / script diff with no
|
||||
phantom operations).
|
||||
- **Empty-db apply** — applying the full migration history to an empty database
|
||||
must reproduce the current model with no errors.
|
||||
- **Supported-prior upgrade** — upgrading from each still-supported prior state
|
||||
applies cleanly.
|
||||
- **No untested rollback claims.** Do not assert "down/rollback is supported"
|
||||
unless a downgrade is actually exercised against the real provider; otherwise
|
||||
state rollback as **not guaranteed**. EF "EnsureDeleted"/re-create is not a
|
||||
migration rollback.
|
||||
|
||||
## 8. Query performance and complexity
|
||||
|
||||
For each changed hot path, document and test:
|
||||
|
||||
- input-size variables;
|
||||
- CPU and memory complexity (Big-O) for any in-memory reconstruction;
|
||||
- database round trips;
|
||||
- whether filtering, ordering, and paging execute **in SQL** vs. in memory;
|
||||
- whether tracking is necessary;
|
||||
- whether repeated work can be hoisted out of loops;
|
||||
- the partial-failure and transaction behavior.
|
||||
|
||||
Distinguish two classes of finding:
|
||||
|
||||
- **Analyzable anti-patterns** — query-in-loop / save-in-loop, unbounded load
|
||||
followed by in-memory `Where`, repeated `First`/`Single` scans over a result
|
||||
(prefer dictionary/hash-set reconstruction, normally `O(n)` vs `O(n²)`). These
|
||||
can be flagged by inspection.
|
||||
- **SQL-backed evidence** — actual paging/filtering/sort location, query count,
|
||||
and plan shape must be backed by provider query logs, `ToQueryString()`,
|
||||
store-generated SQL, or a plan/count measurement. Do **not** assert "executes
|
||||
in SQL" or "single round trip" from LINQ shape alone.
|
||||
|
||||
Do not optimize by weakening correctness: batching all writes into one final
|
||||
save can change partial-success behavior; adding a transaction can change
|
||||
locking/retry semantics. Both are business decisions.
|
||||
|
||||
## 9. Size, complexity, and "no godfile theater"
|
||||
|
||||
- We do **not** enforce arbitrary line-count or file-size thresholds as quality
|
||||
gates. A number is not an architecture.
|
||||
- A file is too large when it has **independently changing responsibilities**
|
||||
(low cohesion) or **untestable coupling**, not when it crosses a count.
|
||||
Split on seams, not on a counter; describe the seam in the PR.
|
||||
- Gates that operate on size must name the *consequence* (e.g., "two unrelated
|
||||
features in one service") and be backed by review, not by a magic threshold.
|
||||
|
||||
## 10. Exceptions and ADRs
|
||||
|
||||
Some rules here are hard (EF allowlist, no client exception leakage, server-
|
||||
derived tenant scope). A deviation from a hard rule requires an **Architecture
|
||||
Decision Record** under `docs/adr/` (create the path if absent):
|
||||
|
||||
- A short `NNNN-title.md` stating context, decision, consequences, and the
|
||||
specific rule being excepted.
|
||||
- The excepted rule and an expiry/review date.
|
||||
- Reference from the PR that relies on it.
|
||||
|
||||
Without an ADR, the rule stands as written. Explicit, documented exceptions
|
||||
only — **no wildcard suppressions** (no global `[SuppressMessage]`, no
|
||||
`.editorconfig` `dotnet_diagnostic.*.severity = none` sweeps, no
|
||||
`#pragma` swaths). A suppression must name the single diagnostic and cite the
|
||||
ADR/justification.
|
||||
|
||||
## 11. Configuration
|
||||
|
||||
Configuration is bound once during composition in
|
||||
`SeaHaven.Services/DependencyInjection/ServicesModule.cs`. Business services
|
||||
receive typed options (`FrontendOptions`, `JwtOptions`, `ApprovalsOptions`,
|
||||
`VendorPortalOptions`, …). Defaults must preserve current behavior. Startup
|
||||
validation is introduced only when every deployed environment is known to
|
||||
provide the required value.
|
||||
|
||||
## 12. Review checklist (quick reference)
|
||||
|
||||
- Controller depends only on `I{Feature}Service` abstractions.
|
||||
- Business logic is in a cohesive feature service.
|
||||
- EF operations are behind a feature data-service abstraction.
|
||||
- No raw `IConfiguration` in a business service.
|
||||
- No unbounded load + in-memory filtering.
|
||||
- No query/save loop where a set-based operation preserves semantics.
|
||||
- Read-only EF queries use no tracking.
|
||||
- New async I/O accepts and **propagates** cancellation where the contract
|
||||
permits (verified, not just signed).
|
||||
- Errors do not leak internal exception details; secrets are not logged.
|
||||
- Tenant scope is server-derived; authorization enforced at service entry.
|
||||
- Commit convention respected (single `SaveChangesAsync` in the owning data
|
||||
service; transactions only in data services when required).
|
||||
- Tests cover behavior, authorization, data contracts, ordering, duplicates,
|
||||
missing records, and relevant failure boundaries.
|
||||
- Changed behavior checked against the authoritative ticket board (see
|
||||
`REVIEW_AND_PR_FRAMEWORK.md`). Missing board access blocks readiness/merge.
|
||||
|
|
@ -73,4 +73,25 @@ public class ArchitectureTests
|
|||
violations.Should().BeEmpty(
|
||||
"business services must depend on data-service interfaces and typed options, never on DbContext or IConfiguration");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BusinessServices_DependOnInterfaces_NotConcreteServiceOrDataServiceImplementations()
|
||||
{
|
||||
// Guards the feature-specific interface boundary: a business service must
|
||||
// depend on I{Feature}DataService / I{Feature}Service abstractions, never on
|
||||
// concrete service or data-service implementations (no generic-repository
|
||||
// theater and no coupling to persistence implementations).
|
||||
BusinessServiceImplementations.Should().NotBeEmpty(
|
||||
"business services must be discovered for this invariant to be meaningful");
|
||||
|
||||
var violations = ParametersOf(BusinessServiceImplementations)
|
||||
.Where(p => IsConcreteServiceImplementation(p.ParameterType)
|
||||
|| IsConcreteDataService(p.ParameterType))
|
||||
.Select(p => $"{p.Member.DeclaringType!.Name} depends on {p.ParameterType.FullName} ({p.Name})")
|
||||
.ToList();
|
||||
|
||||
violations.Should().BeEmpty(
|
||||
"business services must depend on interfaces (I{Feature}Service / I{Feature}DataService), "
|
||||
+ "never on concrete service or data-service implementations");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
125
QUALITY_GATES.md
Normal file
125
QUALITY_GATES.md
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
# Quality Gates (canonical)
|
||||
|
||||
> **Status: canonical.** This document owns *how* the architecture and
|
||||
> code-quality rules are enforced: the gate list, the local command, the CI
|
||||
> mapping, and the pass/fail/skip reporting semantics. The *meaning* of each
|
||||
> rule lives in `ARCHITECTURE_AND_CODE_QUALITY.md`.
|
||||
>
|
||||
> **CI and local run the same check.** Every gate below is executed by
|
||||
> `scripts/governance-check.sh`. The `architecture-quality` workflow calls that
|
||||
> script so a green run means the same thing locally and in CI. Do not add a
|
||||
> check to CI that is not also runnable locally through the script (and vice
|
||||
> versa).
|
||||
|
||||
## Gate inventory
|
||||
|
||||
| # | Gate | Protects (architecture §) | Local command | CI step |
|
||||
|---|------|---------------------------|---------------|---------|
|
||||
| G1 | Restore | build integrity | `dotnet restore SeaHavenIndustries.sln` | `architecture-quality` → `governance-check.sh` |
|
||||
| G2 | Architecture boundary tests | §1, §3 (dependency direction) | `dotnet test Api.SeaHavenIndustries.Tests ... --filter FullyQualifiedName~ArchitectureTests --no-restore` | same script |
|
||||
| G3 | Changed-file formatting | §1 (conventions) | `dotnet format SeaHavenIndustries.sln --no-restore --verify-no-changes --include <changed .cs>` | same script |
|
||||
| G4 | Release build | compile correctness | `dotnet build SeaHavenIndustries.sln -c Release --no-restore` | `architecture-quality` and `ci` |
|
||||
| G5 | Full test suite | behavior | `dotnet test SeaHavenIndustries.sln -c Release --no-build` | `architecture-quality` and `ci` |
|
||||
| G6 | Migration gates | §7 | see §"Migration gates" below | on-demand / release |
|
||||
| G7 | Cancellation forwarding | §6 | behavior tests on changed I/O paths + analyzer | review-enforced on changed paths |
|
||||
| G8 | Error disclosure | §5 | `SanitizedErrorsTests` (part of G5) | `ci` |
|
||||
| G9 | Board-backed regression | review framework | `REVIEW_AND_PR_FRAMEWORK.md` inventory | review-enforced |
|
||||
|
||||
## How to run locally
|
||||
|
||||
```bash
|
||||
# Complete local repository gate (mirrors the architecture-quality workflow):
|
||||
bash scripts/governance-check.sh
|
||||
|
||||
# By default it compares against the default branch for changed-file formatting.
|
||||
# Override the comparison point:
|
||||
BASE_REF=origin/dev bash scripts/governance-check.sh
|
||||
BASE_REF=main bash scripts/governance-check.sh
|
||||
```
|
||||
|
||||
The script:
|
||||
1. `dotnet restore` (G1)
|
||||
2. runs the `ArchitectureTests` filter with `--no-restore` (G2)
|
||||
3. computes changed `.cs` files vs `BASE_REF` (default `origin/dev`) and runs
|
||||
`dotnet format --verify-no-changes --include ...` (G3). When there are no
|
||||
changed C# files it skips G3 with an explicit "skipped: no changed C#" line.
|
||||
4. builds the complete solution in Release with no restore (G4).
|
||||
5. runs the complete solution test suite in Release with no rebuild (G5).
|
||||
|
||||
## Migration gates (G6)
|
||||
|
||||
Provider-real, not reflection-derived. Run against the configured provider:
|
||||
|
||||
- **Actual-provider discovery** — confirm the runtime provider matches the
|
||||
configured connection (e.g., SQL Server / Npgsql) from configuration, not
|
||||
from package guesses.
|
||||
- **Snapshot consistency** — `dotnet ef migrations script --idempotent` (or a
|
||||
model diff) shows no phantom operations; `ModelSnapshot` matches the model.
|
||||
- **Empty-db apply** — `dotnet ef database update` against a fresh empty
|
||||
database applies the full history cleanly.
|
||||
- **Supported-prior upgrade** — upgrade from each still-supported prior
|
||||
migration applies cleanly.
|
||||
- **Rollback** — report **not guaranteed** unless a real downgrade was
|
||||
exercised against the provider. Never claim "EF can roll it back" untested.
|
||||
|
||||
Migration gates are run on-demand and gated for releases; they are not in the
|
||||
per-PR fast path because they require a database. A PR that adds/changes a
|
||||
migration must document the G6 evidence in the PR description.
|
||||
|
||||
## Cancellation forwarding (G7)
|
||||
|
||||
For changed async I/O paths, the requirement (§6) is **verified forwarding**,
|
||||
not a signature checkbox. Acceptable evidence, any one of:
|
||||
|
||||
- a behavior test asserting the `CancellationToken` reaches the data service /
|
||||
EF call; or
|
||||
- an analyzer diagnostic confirming propagation on the changed method.
|
||||
|
||||
A parameter that exists but is swallowed/`default`-ed is a **fail**. If an
|
||||
analyzer is unavailable for a path, a behavior test is required; silence is not
|
||||
acceptable. Review enforces this on changed paths.
|
||||
|
||||
## Architecture discovery must fail on zero (G2 invariants)
|
||||
|
||||
Each reflection invariant in `ArchitectureTests` asserts the discovered set is
|
||||
non-empty. A future namespace move that makes reflection return zero types will
|
||||
**fail the gate** rather than silently pass. The current invariants:
|
||||
|
||||
1. Controllers inject neither `DbContext`, concrete data services, nor concrete
|
||||
service implementations.
|
||||
2. Business services inject neither `DbContext` nor `IConfiguration`.
|
||||
3. Business services depend on interfaces (not concrete service/data-service
|
||||
implementations) for their data and persistence dependencies.
|
||||
|
||||
## What this does not claim
|
||||
|
||||
- Reflection over constructors proves **wiring shape**, not runtime behavior.
|
||||
Runtime/semantic properties (cancellation actually cancelling, a query
|
||||
executing in SQL, a transaction actually committing atomically) are proven by
|
||||
behavior tests or provider evidence, never by reflection.
|
||||
- A passing script proves G1–G5 ran in sequence. It does not prove the
|
||||
provider-backed or review-owned gates G6–G9.
|
||||
|
||||
## Pass / fail / skip / not-run reporting
|
||||
|
||||
Every gate result is reported as exactly one of:
|
||||
|
||||
- **pass** — ran and succeeded, with evidence.
|
||||
- **fail** — ran and failed; cite the failing test/command and output.
|
||||
- **skipped** — intentionally not run for this change (e.g., no changed C# for
|
||||
G3); name the gate and the reason.
|
||||
- **not-run** — could not run (environment/dependency missing); name the
|
||||
blocker.
|
||||
|
||||
A PR is not "green" if any applicable gate is fail, skipped-without-justification,
|
||||
or not-run. Never infer a pass from silence.
|
||||
|
||||
## Adding or changing a gate
|
||||
|
||||
- Any new executable check goes into `scripts/governance-check.sh` **and** the
|
||||
`architecture-quality` workflow together (or into the full CI for G4/G5-type
|
||||
checks). One source of truth for local and CI.
|
||||
- A new rule documents its meaning in `ARCHITECTURE_AND_CODE_QUALITY.md` and its
|
||||
command/mapping here.
|
||||
- Explicit, documented exceptions only — no wildcard suppressions (see
|
||||
`ARCHITECTURE_AND_CODE_QUALITY.md` §10).
|
||||
132
REVIEW_AND_PR_FRAMEWORK.md
Normal file
132
REVIEW_AND_PR_FRAMEWORK.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# Review and PR Framework (canonical)
|
||||
|
||||
> **Status: canonical.** This document owns *who* reviews, in what order, with
|
||||
> what evidence, and how exceptions are recorded. The rules under review live in
|
||||
> `ARCHITECTURE_AND_CODE_QUALITY.md`; the gates live in `QUALITY_GATES.md`.
|
||||
|
||||
## 1. Exact-head review
|
||||
|
||||
Reviews are performed against the **exact HEAD** the PR will merge, not a stale
|
||||
snapshot:
|
||||
|
||||
- Fetch and review the precise merge commit / head SHA. If new commits land
|
||||
after review, the review covers only the previously seen diff until re-run.
|
||||
- Comments cite `file:line` against current HEAD so they are navigable and
|
||||
reproducible.
|
||||
- A "looks good from what I remember" approval is invalid; re-diff before
|
||||
approving.
|
||||
|
||||
Review order is **business-rule-first, high-signal, low-false-positive**:
|
||||
|
||||
1. Correctness of the changed business behavior vs. the acceptance criteria.
|
||||
2. Regression risk against board-backed protected behavior (§2).
|
||||
3. Security / data / authorization (§3).
|
||||
4. Architecture / dependency direction (the `ArchitectureTests` gate already
|
||||
covers mechanical direction; review covers *semantic* layering).
|
||||
5. Performance / complexity evidence (§4).
|
||||
6. Tests: do they assert behavior through public contracts?
|
||||
7. Nitpicks / style last, and only if not already caught by `dotnet format`.
|
||||
|
||||
## 2. Board-backed regression inventory
|
||||
|
||||
A task is incomplete if it implements the current change while regressing
|
||||
behavior that was already working. Before claiming review/merge readiness:
|
||||
|
||||
- Pull the **Seahaven Jira SH board inventory** — all
|
||||
visible tickets, not only the current one: key, title, type, status,
|
||||
component, acceptance criteria, linked PR/release, and QA evidence.
|
||||
- Check the change against that inventory. A plausible regression against
|
||||
protected (Done / released / QA-approved) behavior is a **Blocker** until
|
||||
disproven with repo evidence and targeted validation.
|
||||
- **Missing board access, incomplete inventory, or missing PR-to-ticket
|
||||
traceability is Blocked / NOT READY** — never a pass. Say so explicitly.
|
||||
|
||||
If Jira cannot be reached, report the gate as **not-run / blocked**, not green.
|
||||
The current workflow has no `Ready for QA` transition; merged-but-unverified
|
||||
work remains in the documented pre-QA status and must not be mislabeled Done.
|
||||
|
||||
## 3. Security, data, and authorization evidence
|
||||
|
||||
For changes touching auth, data, crypto, external input, or configuration,
|
||||
review requires evidence (not assertion) for:
|
||||
|
||||
- **No exception disclosure** — changed endpoints do not leak exception
|
||||
messages, stack traces, or SQL/provider/path/credential/crypto detail to the
|
||||
client. `SanitizedErrorsTests` covers the shared boundary; changed handlers
|
||||
must route through it.
|
||||
- **Secret handling** — no secret-bearing field is logged or serialized into a
|
||||
response. Structured log templates must not interpolate secrets.
|
||||
- **Tenant scope** — scope is server-derived and authorization is enforced at
|
||||
service entry (§2 of the architecture doc).
|
||||
- **Authorization** — the service entry trust boundary is intact; controllers
|
||||
express HTTP policy and delegate.
|
||||
|
||||
Cite the test, log redaction, or code path for each. "We don't think it leaks"
|
||||
without evidence is a fail.
|
||||
|
||||
## 4. Performance and Big-O evidence
|
||||
|
||||
For each changed hot path, the PR provides:
|
||||
|
||||
- **Complexity** — CPU and memory Big-O for any in-memory step (e.g., dictionary
|
||||
reconstruction is `O(n)`; repeated `First`/`Single` scans are `O(n²)`).
|
||||
- **SQL-backed evidence** — claims that filtering/ordering/paging execute in
|
||||
SQL, or that there is a single round trip, are backed by provider query logs,
|
||||
`ToQueryString()`, store SQL, or a measured count/plan — **not** inferred from
|
||||
LINQ shape.
|
||||
- **Round trips / loops** — no query-in-loop / save-in-loop where a set-based
|
||||
operation preserves semantics; document when a loop is intentional and why.
|
||||
- **Partial-failure / transaction behavior** — preserved or explicitly changed
|
||||
by business decision (see architecture §4/§8).
|
||||
|
||||
## 5. Tests
|
||||
|
||||
- Tests assert **behavior through public interfaces**, not linter/analyzer
|
||||
self-proofs.
|
||||
- Cover: the happy path, authorization, data contracts, ordering, duplicates,
|
||||
missing records, and relevant failure boundaries.
|
||||
- New I/O paths include cancellation-forwarding evidence (architecture §6).
|
||||
- A test that only proves "the rule exists" is not a behavior test; rewrite it
|
||||
to exercise the product behavior the rule protects.
|
||||
|
||||
## 6. Size and "no godfile theater"
|
||||
|
||||
- Do not request splits based on line/file counts. A number is not feedback.
|
||||
- If a unit is too large, name the **concrete second responsibility** or the
|
||||
**untestable coupling** and propose the seam. Otherwise size is not a blocker.
|
||||
- Do not introduce file/line thresholds into gates (architecture §9).
|
||||
|
||||
## 7. Exceptions (ADR)
|
||||
|
||||
A deviation from a hard rule requires an **Architecture Decision Record** under
|
||||
`docs/adr/NNNN-title.md` (create the path if absent), referenced from the PR:
|
||||
|
||||
- context, decision, consequences;
|
||||
- the specific rule being excepted;
|
||||
- expiry/review date.
|
||||
|
||||
Suppressions are single-diagnostic and cite the ADR — **no wildcard
|
||||
suppressions** (no global `[SuppressMessage]`, no `.editorconfig` severity
|
||||
sweeps, no `#pragma` swaths). See architecture §10.
|
||||
|
||||
## 8. PR description contract (minimal)
|
||||
|
||||
- **Summary** — what changed and why, in plain language.
|
||||
- **Changes and value** — grouped by area, each with the value it delivers.
|
||||
- **Ticket** — the board key(s) when one applies. A missing ticket link is not
|
||||
itself a blocker; missing board regression access or contradictory acceptance
|
||||
criteria is.
|
||||
- Link G6 migration evidence if the PR adds/changes a migration.
|
||||
- Link any ADR relied upon.
|
||||
|
||||
Avoid boilerplate: no deployment notes, no validation transcripts, no
|
||||
"residual-risk" theatre, no AI signatures. The validation story lives in the
|
||||
check run results and the close-out, not in the PR body.
|
||||
|
||||
## 9. Merge readiness
|
||||
|
||||
Merge requires: exact-head review done; board-backed regression check pass (or
|
||||
documented blocked); security/perf evidence present where applicable; CI green
|
||||
(G1–G5); migration evidence (G6) if touched; no open hard-rule exceptions
|
||||
without an ADR. Report each as pass / fail / skipped / not-run — never infer a
|
||||
pass.
|
||||
82
scripts/governance-check.sh
Executable file
82
scripts/governance-check.sh
Executable file
|
|
@ -0,0 +1,82 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# governance-check.sh — local/CI parity governance gate for the Seahaven backend.
|
||||
#
|
||||
# Runs G1 restore, G2 ArchitectureTests, G3 changed-file formatting, G4 Release
|
||||
# build, and G5 full tests exactly as the `architecture-quality` workflow does.
|
||||
# A green run here means the same thing locally and in that CI workflow.
|
||||
#
|
||||
# Usage:
|
||||
# bash scripts/governance-check.sh
|
||||
# BASE_REF=origin/dev bash scripts/governance-check.sh
|
||||
# BASE_REF=<base-sha> HEAD_REF=<head-sha> bash scripts/governance-check.sh
|
||||
set -euo pipefail
|
||||
|
||||
SOLUTION="SeaHavenIndustries.sln"
|
||||
ARCH_TEST_PROJECT="Api.SeaHavenIndustries.Tests/Api.SeaHavenIndustries.Tests.csproj"
|
||||
|
||||
log() { printf '\n\033[1m== %s ==\033[0m\n' "$1"; }
|
||||
ok() { printf '\033[32mPASS\033[0m %s\n' "$1"; }
|
||||
bad() { printf '\033[31mFAIL\033[0m %s\n' "$1"; }
|
||||
|
||||
if [[ -n "${DOTNET_BIN:-}" ]]; then
|
||||
DOTNET="$DOTNET_BIN"
|
||||
elif command -v dotnet >/dev/null 2>&1; then
|
||||
DOTNET="$(command -v dotnet)"
|
||||
elif [[ -x "$HOME/.dotnet/dotnet" ]]; then
|
||||
DOTNET="$HOME/.dotnet/dotnet"
|
||||
else
|
||||
bad "dotnet is unavailable; set DOTNET_BIN or install the repository SDK."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Comparison point for changed-file formatting. Default to the dev integration
|
||||
# branch locally; CI overrides BASE_REF/HEAD_REF with the PR base/head SHAs.
|
||||
BASE_REF="${BASE_REF:-origin/dev}"
|
||||
HEAD_REF="${HEAD_REF:-HEAD}"
|
||||
|
||||
# Resolve the base ref before using it for a diff.
|
||||
if ! git rev-parse --verify --quiet "${BASE_REF}^{commit}" >/dev/null; then
|
||||
bad "G3: BASE_REF '${BASE_REF}' does not resolve to a commit (run: git fetch origin)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "G1: restore"
|
||||
"$DOTNET" restore "$SOLUTION"
|
||||
ok "G1: restore"
|
||||
|
||||
log "G2: architecture boundary tests (dependency direction)"
|
||||
"$DOTNET" test "$ARCH_TEST_PROJECT" \
|
||||
--no-restore \
|
||||
--filter "FullyQualifiedName~ArchitectureTests" \
|
||||
--nologo
|
||||
ok "G2: ArchitectureTests"
|
||||
|
||||
log "G3: changed-file formatting (${BASE_REF}..${HEAD_REF})"
|
||||
changed_cs=()
|
||||
while IFS= read -r f; do
|
||||
changed_cs+=("$f")
|
||||
done < <(
|
||||
git diff --name-only --diff-filter=ACMR "${BASE_REF}" "${HEAD_REF}" -- '*.cs'
|
||||
)
|
||||
|
||||
if (( ${#changed_cs[@]} == 0 )); then
|
||||
printf '\033[33mSKIP\033[0m G3: no changed C# files between %s..%s\n' "${BASE_REF}" "${HEAD_REF}"
|
||||
else
|
||||
printf ' checking %d changed C# file(s)\n' "${#changed_cs[@]}"
|
||||
"$DOTNET" format "$SOLUTION" \
|
||||
--no-restore \
|
||||
--verify-no-changes \
|
||||
--include "${changed_cs[@]}"
|
||||
ok "G3: changed-file formatting"
|
||||
fi
|
||||
|
||||
log "G4: Release build"
|
||||
"$DOTNET" build "$SOLUTION" -c Release --no-restore --nologo
|
||||
ok "G4: Release build"
|
||||
|
||||
log "G5: full test suite"
|
||||
"$DOTNET" test "$SOLUTION" -c Release --no-build --nologo
|
||||
ok "G5: full test suite"
|
||||
|
||||
log "governance-check: all required repository gates passed"
|
||||
Loading…
Add table
Reference in a new issue