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 .claude/skills/levantar-issue/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Talk to the owner in Portuguese. The issue body is written in Portuguese, from t
## Steps

1. **Read the issue** and its comments: `gh issue view <n> --comments`.
2. **Read the documentation it touches:** the feature document in `docs/product/features/`, `docs/product/conventions.md`, `docs/api/conventions.md`, the architecture decisions it cites and `docs/product/open-questions.md`. The scope decisions in `AGENTS.md` override older ADRs and issues.
2. **Read the documentation it touches:** the feature document in `docs/product/features/`, `docs/product/conventions.md`, `docs/api/conventions.md`, the decisions that apply in `docs/decisoes-de-arquitetura.md` and `docs/product/open-questions.md`. The scope decisions in `AGENTS.md` override older issues.
3. **Read the related code:** the files the issue will touch, and the nearest similar feature already built, which is the pattern to follow.
4. **Survey:**
- the requirement or the problem, in one sentence;
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
- name: Restore
run: dotnet restore ${{ env.SOLUTION }}

# Warnings are errors in every project (Directory.Build.props, ADR-0026).
# Warnings are errors in every project (Directory.Build.props).
- name: Build
run: dotnet build ${{ env.SOLUTION }} --configuration Release --no-restore

Expand Down Expand Up @@ -74,6 +74,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v7

# Validates the Dockerfile only; publishing the image is part of the delivery pipeline (ADR-0029).
# Validates the Dockerfile only: the image is built here but never published.
- name: Build image
run: docker build --file src/ControlService.API/Dockerfile --tag controlservice-api:ci .
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Control Service is an ERP for service companies and the owner's **public portfol
- **Product goal, in this order:** sign-in and security → the records the planned screens need → clients and services → the commercial and financial workflow → reports → the whole cycle closed and demonstrable.
- **Stack:** .NET 10 / C# 14, ASP.NET Core Minimal APIs, EF Core 10, PostgreSQL 18, .NET Aspire 13.5, xUnit v3 on Microsoft.Testing.Platform. Four projects by layer (Domain, Application, Infrastructure, API) with feature folders.
- **Business rules:** `docs/product/` is the source of truth: one document per feature in `docs/product/features/`, plus the rules shared by all in `docs/product/conventions.md`. Rules have stable IDs (`USR-06`, `PERM-05`); cite them in tests and pull requests. User-facing messages are in Portuguese, verbatim. Terms: [domain glossary](docs/product/glossary.md). Start at the [docs index](docs/README.md).
- **Technical decisions:** `docs/adr/`, until one decisions document replaces it. Where an ADR or an older issue disagrees with this file, this file wins.
- **Technical decisions:** [docs/decisoes-de-arquitetura.md](docs/decisoes-de-arquitetura.md), one document in Portuguese that explains each decision in six points. Where an older issue disagrees with this file, this file wins.
- **Work items:** GitHub issues, grouped in milestones and numbered in dependency order. Unless the owner names one, take the lowest open issue of the current milestone (`gh issue list --milestone "M1: Sign-in, users and permissions"`).

| Path | Content |
Expand All @@ -18,14 +18,14 @@ Control Service is an ERP for service companies and the owner's **public portfol
| `backend/ControlService/tests/` | Domain, Application, API integration and architecture tests |
| `docs/product/` | Business rules: overview, glossary, conventions, features, open questions, screen catalog |
| `docs/api/` | What every endpoint shares: routes, paging, versions, errors |
| `docs/adr/` | Architecture Decision Records |
| `docs/decisoes-de-arquitetura.md` | Architecture decisions, in Portuguese: what, why, cost and alternatives |
| `docs/agents/` | Guides for agents, and the owner's guide |
| `.claude/skills/` | The three workflow commands |
| `docs/frontend/` | The front-end prototype (not in this repository yet) and its simulated server |

### Scope decisions of 2026-10-05

Taken by the owner; they override the ADRs and issues written before that date.
Taken by the owner; they override the issues written before that date.

- **No permission cache.** The account status and the effective levels are read from the database on each request.
- **Access by e-mail is postponed.** Whoever registers a user sets a temporary password, and the person must replace it on first sign-in, through the mechanism the Admin already uses. No SMTP, activation links or reset links until the product cycle is closed.
Expand Down Expand Up @@ -172,6 +172,6 @@ If a request conflicts with `docs/`, point out the conflict and ask before actin
| [Documentation](docs/agents/guides/documentation.md) | Behavior, contract or decisions change |
| [Local environment](docs/agents/guides/local-environment.md) | Running the system, Docker, HTTPS, PowerShell |
| [Communication](docs/agents/guides/communication.md) | Asking the owner a question or reporting a result |
| [Record a decision](docs/agents/workflows/record-a-decision.md) | Changing an ADR, while the ADRs exist |
| [Record a decision](docs/agents/workflows/record-a-decision.md) | Adding or changing an architecture decision |
| [Git and pull requests](docs/agents/workflows/git-and-pull-requests.md) | Delivering any change |
| [Review dependency updates](docs/agents/workflows/review-dependency-updates.md) | Handling Dependabot pull requests |
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
```
feat(users): validate CPF check digits
fix(auth): reset lockout after password reset
docs(adr): accept ADR-0018
docs: record the decision on signature storage
```

3. Run the tests before pushing:
Expand All @@ -44,6 +44,6 @@

- Code, identifiers, commits and documentation in English; user-facing messages in Portuguese, verbatim from `docs/product/`.
- Business rules have stable IDs (`USR-06`, `PERM-05`): cite them in tests and pull requests ([docs index](docs/README.md#rule-ids)).
- Development is test-driven: every behavior starts as a failing test, and each issue carries a numbered test list ([AGENTS.md](AGENTS.md#tests)). Business rules live in the Domain project (ADR-0005).
- Development is test-driven: every behavior starts as a failing test, and each issue carries a numbered test list ([AGENTS.md](AGENTS.md#tests)). Business rules live in the Domain project.
- Package versions are declared only in `Directory.Packages.props`.
- A significant technical decision gets an ADR in `docs/adr/`.
- A significant technical decision is recorded in the [decisions document](docs/decisoes-de-arquitetura.md), which is in Portuguese ([how](docs/agents/workflows/record-a-decision.md)).
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ An ERP for service companies: users and permissions, service catalog, clients an
- **Invitation-based accounts.** Nobody signs up alone: a new user receives a single-use activation link by e-mail and creates their own password. Nobody ever sees or types someone else's password.
- **Safe concurrent editing.** Optimistic concurrency rejects a save when someone else changed the record in the meantime, and tells who did it.
- **Traceability.** Every record stores who created it, who last changed it and when. Users are deactivated, never deleted.
- **Test-driven development.** Every behavior starts as a failing test, in every layer, and business rules carry stable IDs that the tests cite ([ADR-0033](docs/adr/0033-develop-test-first-in-pair-mode.md)).
- **Test-driven development.** Every behavior starts as a failing test, in every layer, and business rules carry stable IDs that the tests cite ([decision 27](docs/decisoes-de-arquitetura.md)).
- **Architecture enforced by tests.** Clean Architecture layers are separate projects, and architecture tests fail the build if a layer depends on the wrong one.
- **Decisions on record.** Every significant technical choice has an [Architecture Decision Record](docs/adr/README.md) with the alternatives considered.
- **Decisions on record.** Every significant technical choice is explained in the [decisions document](docs/decisoes-de-arquitetura.md), with what it costs and the alternatives considered.
- **AI-assisted, human-reviewed.** AI coding agents implement changes following [AGENTS.md](AGENTS.md): they work on branches and open pull requests, and the owner reviews and merges every one of them.

## Tech stack
Expand Down Expand Up @@ -45,7 +45,7 @@ backend/ControlService/src
└── ControlService.ServiceDefaults Telemetry, health checks and resilience
```

Dependencies point inward only: `API → Application → Domain`, with `Infrastructure` implementing the Application interfaces. Inside each project, code is grouped by feature (`Users`, `PermissionProfiles`, `Auth`). See [ADR-0005](docs/adr/0005-adopt-clean-architecture.md) and the [back-end README](backend/ControlService/README.md).
Dependencies point inward only: `API → Application → Domain`, with `Infrastructure` implementing the Application interfaces. Inside each project, code is grouped by feature (`Users`, `PermissionProfiles`, `Auth`). See the [decisions document](docs/decisoes-de-arquitetura.md) and the [back-end README](backend/ControlService/README.md).

## Getting started

Expand Down Expand Up @@ -77,7 +77,7 @@ dotnet test --solution ControlService.slnx
| Document | Language | Content |
|---|---|---|
| [Product documentation](docs/README.md) | English | Product overview, glossary, business rules by feature (with stable rule IDs), API conventions and open questions |
| [Architecture Decision Records](docs/adr/README.md) | English | What was decided for the back-end, why, and which alternatives were rejected |
| [Architecture decisions](docs/decisoes-de-arquitetura.md) | Portuguese | How the back-end is built, why, what each choice costs and which alternatives were rejected |
| [Back-end guide](backend/ControlService/README.md) | Portuguese | How to run, project structure and what each Docker resource is |
| [AGENTS.md](AGENTS.md) and [agent guides](docs/agents/README.md) | English | Rules, guides and workflows for the AI coding agents that work on this repository |
| [Trabalhando com agentes](docs/agents/trabalhando-com-agentes.md) | Portuguese | How the owner pairs with AI agents: asking for tasks, reviewing pull requests |
Expand Down
2 changes: 1 addition & 1 deletion backend/ControlService/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,6 @@ dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.underscore_camel_case.required_prefix = _
dotnet_naming_style.underscore_camel_case.capitalization = camel_case

# Test names describe behavior with underscores (ADR-0024): Cpf_with_all_equal_digits_is_rejected
# Test names describe behavior with underscores: Cpf_with_all_equal_digits_is_rejected
[tests/**.cs]
dotnet_diagnostic.CA1707.severity = none
2 changes: 1 addition & 1 deletion backend/ControlService/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<Project>
<!-- Applied to every project in the solution (ADR-0026). -->
<!-- Applied to every project in the solution. -->
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
Expand Down
6 changes: 3 additions & 3 deletions backend/ControlService/Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<Project>
<!-- Central Package Management (ADR-0026): versions live here, projects reference packages without a version. -->
<!-- Central Package Management: versions live here, projects reference packages without a version. -->
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
Expand All @@ -11,7 +11,7 @@
<PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.24.2-preview.1" />
</ItemGroup>

<!-- Aspire (ADR-0027) -->
<!-- Aspire -->
<ItemGroup>
<PackageVersion Include="Aspire.Hosting.PostgreSQL" Version="13.5.4" />
<PackageVersion Include="CommunityToolkit.Aspire.Hosting.MailPit" Version="13.5.0" />
Expand Down Expand Up @@ -41,7 +41,7 @@
<PackageVersion Include="Npgsql" Version="10.0.3" />
</ItemGroup>

<!-- Tests (ADR-0024) -->
<!-- Tests -->
<ItemGroup>
<PackageVersion Include="xunit.v3" Version="4.0.1" />
<PackageVersion Include="Microsoft.Testing.Extensions.CodeCoverage" Version="18.11.2" />
Expand Down
4 changes: 2 additions & 2 deletions backend/ControlService/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Control Service: back-end

API em C# com .NET 10, organizada em Clean Architecture ([ADR-0005](../../docs/adr/0005-adopt-clean-architecture.md)) com Minimal APIs ([ADR-0003](../../docs/adr/0003-use-minimal-apis-grouped-by-feature.md)). As regras de negócio estão em [`docs/`](../../docs/README.md).
API em C# com .NET 10, organizada em Clean Architecture com Minimal APIs (as decisões estão em [`docs/decisoes-de-arquitetura.md`](../../docs/decisoes-de-arquitetura.md)). As regras de negócio estão em [`docs/`](../../docs/README.md).

## Como rodar

Expand Down Expand Up @@ -105,7 +105,7 @@ Pastas previstas para a primeira etapa: `Access` (níveis, chaves das telas, ace

## Configurações comuns

- `Directory.Build.props`: .NET 10, nullable, avisos tratados como erros e analisadores ([ADR-0026](../../docs/adr/0026-enforce-build-quality-settings.md)).
- `Directory.Build.props`: .NET 10, nullable, avisos tratados como erros e analisadores (decisão 29 em [`docs/decisoes-de-arquitetura.md`](../../docs/decisoes-de-arquitetura.md)).
- `Directory.Packages.props`: **todas as versões de pacotes ficam aqui**. No `.csproj`, use `<PackageReference Include="Nome" />` sem versão.
- `tests/Directory.Build.props`: xUnit v3 e Shouldly para todos os projetos de teste.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace ControlService.API.Auth;

/// <summary>Rate limits of the authentication routes (ADR-0023, D14, T9): a sliding window of 60 seconds
/// <summary>Rate limits of the authentication routes (D14, T9): a sliding window of 60 seconds
/// in 6 segments per client address, with no queue. A refused request answers like a lockout does:
/// 429 `locked_out` with `Retry-After` (AUTH-26).</summary>
internal static class AuthRateLimiting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public static IServiceCollection AddAuthFeature(this IServiceCollection services
services.AddAuthorization();
services.AddAuthRateLimiting(configuration);

// Handlers are registered by hand (ADR-0008); the ones with input to validate are wrapped in the decorator.
// Handlers are registered by hand; the ones with input to validate are wrapped in the decorator.
services.AddScoped<ICommandHandler<SignInCommand, SessionGrant>, SignInHandler>();
services.AddScoped<ICommandHandler<ChangePasswordCommand, SessionGrant>>(serviceProvider =>
new ValidatingCommandHandler<ChangePasswordCommand, SessionGrant>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace ControlService.API.Auth;

/// <summary>The `Auth` keys that decide how access tokens are signed and checked (ADR-0019, ADR-0032).
/// <summary>The `Auth` keys that decide how access tokens are signed and checked.
/// The keys Infrastructure reads live in the same section.</summary>
public sealed class JwtOptions
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace ControlService.API.Auth;

/// <summary>While the Admin has not replaced the initial password, its access token carries
/// `must_change_password` and every endpoint except the ones that opted out answers 401
/// `password_change_required` (AUTH-14, ADR-0032, T8).</summary>
/// `password_change_required` (AUTH-14, T8).</summary>
public sealed class PasswordChangeRequiredFilter : IEndpointFilter
{
public ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
Expand All @@ -23,7 +23,7 @@ public sealed class PasswordChangeRequiredFilter : IEndpointFilter
}

/// <summary>Marks an endpoint the person may call before replacing the initial password: `me`, sign-out
/// and change-password (ADR-0032).</summary>
/// and change-password.</summary>
public sealed record AllowPendingPasswordChangeMetadata;

public static class PasswordChangeRequiredExtensions
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace ControlService.API.Auth;

/// <summary>Requests per client address in a 60-second window (ADR-0023, AUTH-22). Sign-in and refresh
/// <summary>Requests per client address in a 60-second window (AUTH-22). Sign-in and refresh
/// each have their own limit; the global limit and the e-mail routes come with their own issues.</summary>
public sealed class RateLimitingOptions
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace ControlService.API.Auth;

/// <summary>The refresh token cookie (ADR-0019, T6). Its `Path` is the refresh route only, so the
/// <summary>The refresh token cookie (T6). Its `Path` is the refresh route only, so the
/// browser sends it nowhere else; expiring it needs the same name and `Path`.</summary>
internal static class RefreshCookie
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

namespace ControlService.API.Common;

/// <summary>The `/api/v1` group every feature registers its endpoints under (ADR-0003, API-01).
/// <summary>The `/api/v1` group every feature registers its endpoints under (API-01).
/// `Program.cs` and the test factories both map it through here.</summary>
public static class ApiV1Group
{
/// <summary>Every endpoint requires a signed-in person unless it opts out with `AllowAnonymous()`, and
/// refuses a person who still has to replace the initial password unless it opts out with
/// `AllowPendingPasswordChange()` (AUTH-14, ADR-0032).</summary>
/// `AllowPendingPasswordChange()` (AUTH-14).</summary>
public static RouteGroupBuilder MapApiV1(this IEndpointRouteBuilder app) =>
app.MapGroup("/api/v1").RequireAuthorization().AddEndpointFilter<PasswordChangeRequiredFilter>();
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@
namespace ControlService.API.Common;

/// <summary>Translates a domain <see cref="Error"/> into the Problem Details response documented
/// in ADR-0009 and `docs/api/conventions.md#errors` (API-12, API-13).</summary>
/// in `docs/api/conventions.md#errors` (API-12, API-13).</summary>
public static class ErrorResults
{
/// <param name="error">The error to translate.</param>
/// <param name="statusOverride">For the one code whose status depends on the endpoint: `account_inactive`
/// is a 403 at sign-in and a 401 during a session (ADR-0009).</param>
/// is a 403 at sign-in and a 401 during a session.</param>
public static ProblemHttpResult ToProblem(this Error error, int? statusOverride = null)
{
// An unmapped code is a bug: the exception handler logs it and answers with the generic 500 (API-12).
var status = statusOverride ?? ErrorStatusCodes.For(error.Code)
?? throw new InvalidOperationException(
$"The error code '{error.Code}' has no HTTP status in ErrorStatusCodes (ADR-0009).");
$"The error code '{error.Code}' has no HTTP status in ErrorStatusCodes.");
var (type, title) = ProblemTypes.For(status);

var problemDetails = new ProblemDetails
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
namespace ControlService.API.Common;

/// <summary>The HTTP status of every error code in the ADR-0009 table. A code missing here is a
/// programming mistake, not a business outcome: <see cref="ErrorResults"/> treats it as an
/// unexpected failure (API-12, API-14).</summary>
/// <summary>The HTTP status of every error code in the table of docs/api/conventions.md#error-codes.
/// A code missing here is a programming mistake, not a business outcome: <see cref="ErrorResults"/>
/// treats it as an unexpected failure (API-12, API-14).</summary>
internal static class ErrorStatusCodes
{
private static readonly Dictionary<string, int> StatusesByCode = new()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace ControlService.API.Common;

/// <summary>ICurrentUser backed by the request's authenticated principal (ADR-0015, ADR-0019).
/// <summary>ICurrentUser backed by the request's authenticated principal.
/// Outside a request (startup, seeding), there is no HttpContext at all, and the system acts
/// as the Admin (CNV-20).</summary>
public sealed class HttpCurrentUser(IHttpContextAccessor httpContextAccessor) : ICurrentUser
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace ControlService.API.Common;

/// <summary>The `type` and `title` pair for each status code this API returns (ADR-0009): RFC 9110,
/// <summary>The `type` and `title` pair for each status code this API returns: RFC 9110,
/// and RFC 6585 for 429, which RFC 9110 does not define.</summary>
internal static class ProblemTypes
{
Expand Down
Loading
Loading