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
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ the template a living reference for "one shape, shared validation, no separate v
- [ ] **Edit/View modes** — deferred: `GetRole`/`UpdateRole` routes have a `RoleId` `int`-vs-`Guid`
wart (`update-role.cs` uses `api/Role/{RoleId:int}`) that belongs to the contracts cleanup.
New mode fully demonstrates the binding+validation pattern; Edit/View is follow-up.
- [ ] **Save round-trip** — deferred to
- [x] **Save round-trip** — RESOLVED by task 079 (2026-07-02): POST api/Roles now has a real
server endpoint + handler with backend validation; integration tests prove 200 + RoleId and
400 problem-details on invalid input. Originally deferred to
[[079-implement-server-side-createrole-endpoint--backend-validation-roles-contract]]. On Save the
valid `POST api/Roles` reaches the **real** web-server, which has no `CreateRole` endpoint yet →
**405** → generic error toast (confirmed via Aspire console logs). Expected: this task owns the
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Implement server-side CreateRole endpoint + backend validation (roles contract)

## Description

The `admin/roles` contract is the **clean, exemplary** contract (`IRoleDetails` +
`RoleDetailsValidator`, `CreateRole`/`UpdateRole`/`GetRole` commands) but has **contracts only —
no server-side implementation**. Task 078 built the client half (`RoleForm` → dispatches a valid
`CreateRole.Command`); on Save the request reaches the **real web-server**, which has no
`POST api/Roles` handler, so it returns **405 Method Not Allowed** and the client surfaces the
generic error toast. This is expected and correct until the server half exists.

Build the **server-side `CreateRole` slice** so the whole contract use case works end-to-end,
including **backend validation** (the same `RoleDetailsValidator` / `AbstractValidator<IRoleDetails>`
shape enforced server-side, not just in the browser — never trust the client).

## Evidence / current state
- Client dispatches `POST api/Roles` (`create-role.cs`: `[RouteMixin("api/Roles", HttpVerb.Post)]`).
- Aspire console log (web-server) confirms: `POST .../api/Roles - 405` — route answers GET only, no POST.
- No server-side role files exist under the web-server / web-application (contracts-only feature).
- `CreateRole.GetMockResponseFactory()` + mock registration already exist (078) — the client works
under `MOCK_WEB_API`; this task makes it work against the **real** server.

## Checklist
- [x] Server `CreateRole` Endpoint + Handler — **note:** web-server uses the hand-written
MVC-style `BaseEndpoint<Command, Response>` pattern (mirrored `TrackEventEndpoint`), not
FastEndpoints (that's api-server's path). Files:
`web-server/features/admin/roles/{feature-annotations,create-role-endpoint}.cs`,
`web-application/features/admin/roles/create-role-handler.cs`. No mapper needed — the
Command *is* the pipeline message.
- [x] Backend validation: **zero new code** — `FluentValidationBehavior` (already in the mediator
pipeline) + `AddValidatorsFromAssemblyContaining<Web.Contracts.IAssemblyMarker>` run the
contract's composed Validator (shared `RoleDetailsValidator` + `AuthApiRequestValidator`)
server-side. Integration tests prove both rejection paths (empty Name, empty UserId → 400
problem details naming the property).
- [x] Persistence: **deliberate in-memory stub, documented** in the handler's Design region
(static `ConcurrentDictionary`) — roles are a template demo feature; swap for a repository
when real, contract/endpoint unchanged.
- [x] End-to-end verification via **web-server integration tests** (real host, real pipeline):
`POST api/Roles` → 200 + non-empty `RoleId`; 18 passed (was 11). Manual RoleForm-in-browser
check pending healthy local Docker (Aspire) — the integration tests cover the same seam.
- [x] 078's Save-round-trip checkbox flipped (kanban/done/078, with resolution note).

## Results — beyond the checklist

- **Unblocked by prerequisite fix:** `BaseEndpoint`/`BaseFastEndpoint` constrained
`TResponse : BaseResponse`, which would have forced `CreateRole.Response` back into the
`BaseResponse` shape that RFC Decision 5 rejected. Loosened to `where TResponse : class`
(`Send` uses no `BaseResponse` member; both bases kept aligned).
- **All roles route warts fixed** (was: blocks 078 Edit/View): `get-role.cs`
`{RoleId:min(1)}`→`{RoleId:guid}`; `update-role.cs` `api/Role/{RoleId:int}`→
`api/Roles/{RoleId:guid}` **+ removed stray `Guid Guid` property**; `delete-role.cs` RPC-style
`api/DeleteRole`→`api/Roles/{RoleId:guid}` **+ removed hand-declared `required int RoleId`**
(generator emits the Guid from the route). Serialization test updated (RoleId now `Guid`) —
the 083 tests caught the contract change, as intended by the tests-first sequencing.
- Verified: `dev build` 0/0; web-server integration 18 passed; contracts round-trips 7/7;
analyzer 21/21; sourcegen 14/14.

## Notes
- **Contract warts to resolve first (or alongside):** `update-role.cs` /`get-role.cs` route uses
`api/Role/{RoleId:int}` — `RoleId` typed **`int`** in the route but **`Guid`** everywhere else, and
**singular** `api/Role` vs plural `api/Roles`. This blocks 078's Edit/View modes and should be fixed
as part of the contracts cleanup. See [[contract-conventions-rfc]] /
[[077-contracts-compliance-01-nullability-validator-agreement]].
- Ties into the open FastEndpoints source-gen rename work (task 053-002: `[RouteMixin]` →`[Route]`);
keep the new endpoint consistent with whatever attribute naming lands.
- Follow-up to [[078-add-roleform-demonstrating-idetails-binding-and-shared-validation]] — that task
proved the client binding + validation; this one completes the round-trip.
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Create web-contracts-tests project with serialization round-trips

## Description

Build-out of **RFC Decision 3 (maintainer-resolved)**: a dedicated, host-free contracts test
project so serialization is checkable in the contract-first window (contracts are authored before
the server exists in the BFF workflow — maintainer testimony from copic: the contract tests were
the only seam check until backend integration tests arrived later).

Create `tests/container-apps/web/web-contracts-tests/` (Fixie + Shouldly + TimeWarp.Fixie —
mirror `timewarp-architecture-analyzers-tests` wiring) with `SerializeAndDeserialize` round-trips
using camelCase `JsonSerializerOptions`.

**Prioritize by GLM's trigger list** (per the resolved decision — this is what to test, not a gate):
contracts using `required`/`init` members, non-default constructors (all the ctor+`Guard`
Responses!), custom converters, `OneOf`/`SharedProblemDetails` envelopes, `ListResponse<T>`.
Plain auto-property POCOs are low-priority.

## Checklist

- [x] Project scaffold — host-free: references **web-contracts only** (no server, no host); added
to `.slnx` `#if (web)` region. `dev test` discovery confirmed: it globs `tests/**/*.csproj`,
so no wiring needed (and the glob survives template feature-flag exclusion).
- [x] Roles round-trips: `CreateRole.Command`/`Response`, `GetRole.Response`, `GetRoles.Response`
(`ListResponse<RoleDto>` envelope with ctor+Guard DTOs).
- [x] Edge shapes: `SharedProblemDetails` losslessness (validation `errors` + `traceId` survive the
Extensions catch-all in both directions); `GetRole.Query` round-trips its **source-generated**
`RoleId` route property. **Bonus test:** Guard invariants run *during deserialization* — a
`Guid.Empty` `roleId` payload throws rather than materializing an invalid `Response`.
- [x] Trivial-POCO skip documented in `contract-serialization.cs` Design region (single authority
for options + RoundTrip helper).
- [x] No validator tests (skill rule).

## Results

**7 tests, all passing in ~0.07s** — genuinely host-free; runs in the contract-first window.

**Inference-removal candidate surfaced (per standing directive):** the canonical
`JsonSerializerOptions` (CamelCase) now exists in **three places by convention** — web-spa
`program.cs` DI config, the old web-spa-integration-tests serialization test, and
`contract-serialization.cs` here. Candidate: hoist one canonical options declaration into
foundation-contracts and reference it everywhere. Recorded in the test file's Design region.

## Notes

- Skill spec: `skills/web-api-contracts/SKILL.md` §"Contract serialization tests" + Tier 4 in
`references/examples.md` (Shouldly assertions).
- Rationale + testimony recorded in [[contract-conventions-rfc]] Decision 3.
- Copic's `Web.Contracts.Tests` (23 test files, FluentAssertions) is the read-only reference shape.
- Consider whether the existing round-trips in `web-spa-integration-tests/Serialization/` move
here or stay (they run under the Docker-dependent Aspire host; moving them makes them host-free).

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Analyzer and source-gen opportunities to remove inference (collected candidates)

## Description

Running collection per the maintainer directive: **spot places where correctness depends on two
things agreeing by memory/convention, and replace the inference with an analyzer or generator.**
Each candidate below names the agreement, how it breaks, and the proposed mechanism. Pick
individually — each accepted candidate becomes its own task (080-recipe: ship + reconcile in one
PR). Infrastructure exists: `timewarp-architecture-convention-analyzers` (wired repo-wide) for
checks; the generator assemblies for emission.

## Candidates (ranked by observed pain)

1. **Endpoint HTTP verb ↔ contract `[ApiRoute]` verb** — `CreateRoleEndpoint` says `[HttpPost]`,
the contract says `HttpVerb.Post`; nothing checks they match. Drift = the exact 405/mismatch
bug class hit in task 078. *Analyzer* (in server projects): for each `BaseEndpoint<TCommand,_>`,
compare the `[Http*]` attribute against `TCommand`'s `ApiRoute` verb (readable from referenced
contract metadata). **Highest value — this bug class already bit us.**
2. **Contract has `[ApiRoute]` but no server endpoint** — the 405 itself: roles contracts existed
for months with no `POST api/Roles`. *Analyzer* (server projects): enumerate referenced contract
types carrying `RouteTemplate` and warn when no endpoint class covers them. Needs an opt-out for
deliberately client-only/mock-only contracts (attribute or config list).
3. **Canonical `JsonSerializerOptions` declared 3× by convention** — web-spa `program.cs` DI
config, web-spa-integration-tests, and web-contracts-tests (`contract-serialization.cs` Design
region documents this). *Refactor, not analyzer*: hoist one canonical options declaration into
foundation-contracts; everyone references it. Silent-drift risk today: client and tests could
diverge without any signal.
4. **Mock factory registration ↔ `GetMockResponseFactory()`** — a contract can define a factory
that is never registered in `MockWebApiService.Factories` (or vice versa); task 078 had to
hand-add the registration. *Source generator* (preferred): generate the `Factories` dictionary
by scanning referenced contract types for `GetMockResponseFactory()` — the registration step
disappears entirely. (*Analyzer* fallback: warn on unregistered factories.)
5. **Aspire resource names ↔ `ServiceNames` constants** — documented trap (memory:
`aspire-resource-names-must-match-servicenames`): mismatch → server-side `BaseAddress` resolves
null, only under Auto render mode. *Analyzer* (app-host project): literals passed to
`AddProject(...)` resource names must appear in `ServiceNames`.
6. **`BaseEndpoint` ↔ `BaseFastEndpoint` "keep semantics aligned"** — the agreement lives in a
Design-region comment only (and 079 just edited both constraints in lockstep by hand). Weakest
candidate: consider extracting the shared `Match`/problem-mapping logic instead of checking
alignment; an analyzer here is probably over-engineering.

## Checklist

- [ ] Maintainer triage: accept/reject each candidate; spawn tasks for accepted ones.
- [ ] Keep appending future candidates here (standing directive) until triaged.

## Notes

- Directive + seed list recorded in memory (`prefer-analyzers-sourcegen-over-inference`).
- Candidates 1+2 could ship together as one "endpoint coverage" analyzer in the convention
assembly (both walk server compilations against referenced contract metadata).
9 changes: 6 additions & 3 deletions skills/web-api-contracts/analysis/contract-conventions-rfc.md
Original file line number Diff line number Diff line change
Expand Up @@ -474,9 +474,12 @@ propose a third option — with reasoning.**
stubs; `Response : BaseResponse`.
- ~~`features/hello/hello.cs` — `string?` + `NotEmpty()`~~ ✅ (`Name` → `string` + `= null!`).
- ~~`features/analytics/track-event.cs` — `string?` + `NotEmpty()`~~ ✅ (`EventName` → `string` + `= null!`).
- `features/admin/roles` — conforms, **except** the `update-role.cs`/`get-role.cs` route wart found
by 078: `api/Role/{RoleId:int}` (singular route + `int` route param vs `Guid` everywhere else).
Tracked with the server-side slice in kanban task 079.
- ~~`features/admin/roles` — conforms, **except** the `update-role.cs`/`get-role.cs` route wart found
by 078: `api/Role/{RoleId:int}` (singular route + `int` route param vs `Guid` everywhere else)~~
✅ **fixed by task 079 (2026-07-02)**: all roles routes are plural resource routes with
`{RoleId:guid}`; also removed `update-role.cs`'s stray `Guid Guid` property and `delete-role.cs`'s
RPC-style `api/DeleteRole` route + hand-declared `int RoleId`. Server-side CreateRole endpoint +
handler + backend validation landed with integration tests.
- Everything else (`profile`, `auth`, `authentication`, `chat`) already conforms.

**Open sub-question:** is `todo-items` a real feature or a template placeholder? Its two empty query
Expand Down
Loading
Loading