# PRD — task-ledger-api

Evidence date: 2026-10-01.

Source: `git@bitbucket.org:overdrive/task-ledger-api.git`, `main` at `8ab244e` (2026-01-03).

There is no prior PRD in the source repository or in Silicon Ark. Requirements below are recovered from `TasksController`, `TaskService`, `AppDbContext`, the `Task` entity, the EF migration, `Program.cs`, appsettings, `cursor.rules.md`, tests, and git history. Provenance is labelled.

`cursor.rules.md` constrains how the repository is edited. It is not a statement of product behaviour except where the code follows it. Commit messages that name `TaskRepository`, `UpdateTaskAsync`, and `DeleteTaskAsync` are not requirements: those symbols are not in any source file.

## Product purpose

**Inferred.** A local HTTP API, project name TaskLedger, that records tasks in SQL Server and returns the full list.

**Implemented.** Two task operations: create and list. A template weather forecast endpoint is still mapped.

**Documented** only by XML comments on the controller, service, and entity, and by the entity summary "Represents a task entity in the TaskLedger system." There is no product brief and no README.

**Unknown / Requires Confirmation:** who the product is for, whether a user interface exists outside this repository, and whether this API is still in use after 2026-01-03.

## Users and interactions

| Actor | Evidence | Status |
|---|---|---|
| Anonymous HTTP caller | No auth middleware, no `[Authorize]`, `launchSettings.json` sets `anonymousAuthentication: true` and `windowsAuthentication: false` | **Implemented**. The caller application is not in this repo |
| Person running the API in Development | Launch profiles open `/swagger`. Swagger is registered only when `IsDevelopment()` | **Implemented** |
| Person calling the sample `.http` file | `TaskLedger.Api.http` sends `GET http://localhost:5037/weatherforecast/` | **Implemented**. It does not call the task routes |
| SQL Server LocalDB | Connection string in both appsettings files | **Implemented** as configuration. Whether a database has been created is **Unknown** |
| Coding agent following `cursor.rules.md` | File present since `e12e94a` | **Documented** as editor instructions. Not an API user |

No user account, role, or task owner exists in the model.

## Systems the product talks to

1. The HTTP host itself (Kestrel via `dotnet`, or IIS Express from the launch profile).
2. SQL Server through EF Core. Database name `TaskLedgerDb`. Server `(localdb)\mssqllocaldb`. Authentication is Windows trusted connection. `TrustServerCertificate=true`. No password is stored in git.
3. Swagger UI, Development only.

No queue, mailer, cache, file store, identity provider, or monitoring vendor is configured. There is no pipeline file.

Earlier configuration, replaced in `8ab244e`, used `Server=localhost` for the same database name. `appsettings.Development.json` had no connection string until that commit.

## Major workflows

### WF-1 — Create a task

**Implemented** in `TasksController.CreateTask` and `TaskService.CreateTaskAsync`. Route `POST /api/Tasks`.

1. Bind JSON body to `CreateTaskRequest` (`Title` string, default empty; `Description` optional string).
2. The controller calls the service with those two fields.
3. If `title` is null, empty, or whitespace, the service throws `ArgumentException` ("Title is required."). The controller returns HTTP 400 and body `{ "error": "<message>" }`.
4. Otherwise the service trims the title, trims the description when it is not null, sets `IsCompleted` to false, sets `CreatedUtc` to `DateTime.UtcNow`, inserts the row, and saves.
5. On success the controller returns HTTP 201. The body is the saved entity. `CreatedAtAction` targets `GetAllTasks` and supplies `id`. `GetAllTasks` has no `id` parameter.

**Inferred** from ASP.NET Core `CreatedAtAction` behaviour: the `Location` header is the list route with `id` as a query value, and `GET /api/Tasks?id={id}` still returns every task. This was not executed during documentation.

The request type has no validation attributes. Length limits of 200 and 1000 characters are EF column configuration, not a 400 response. A title or description that exceeds those lengths is not caught by the `ArgumentException` handler.

`CreatedUtc` is not accepted from the client. `IsCompleted` is not accepted from the client.

### WF-2 — List tasks

**Implemented** in `TasksController.GetAllTasks` and `TaskService.GetAllTasksAsync`. Route `GET /api/Tasks`.

1. Load every row from `Tasks`.
2. Order by `CreatedUtc` ascending. There is no `ThenBy`.
3. Return HTTP 200 and the list. An empty table returns an empty list, not 404. The empty case is tested on the service, not on the controller.

There is no filter, page, or search parameter. The action ignores any `id` query value.

### WF-3 — Weather forecast sample

**Implemented** in `Program.cs`, `GET /weatherforecast`. Returns five random forecasts. It does not read the database. OpenAPI name `GetWeatherForecast`.

This is the ASP.NET template endpoint. It is still mapped on `main`.

### WF-4 — Database creation

**Documented** in `cursor.rules.md`: generate migrations, do not apply them automatically, emit an idempotent script.

**Implemented:** `Program.cs` does not call `Database.Migrate` or `EnsureCreated`. The tree contains migration `20260101150955_InitialCreate` and `Migrations/InitialCreate.sql`, which creates `__EFMigrationsHistory` and table `Tasks` if that migration id is absent.

**Unknown / Requires Confirmation:** whether anyone has run that script or `dotnet ef database update` against a LocalDB instance. `cursor.rules.md` lists `dotnet ef database update` as a denied command.

## Functional requirements

### FR-001 — Create a task with a title

The caller sends a title and an optional description. The system stores a new task and returns it.

- Trigger: `POST /api/Tasks`.
- Input: JSON `Title`, optional `Description`.
- Behaviour: reject a missing or whitespace title with 400; otherwise persist and return 201 with the entity.
- Output: `Id`, `Title`, `Description`, `IsCompleted` (false), `CreatedUtc`.
- Validation: title required after trim. See business rules for length and description trimming.
- Errors: `ArgumentException` becomes 400. Other failures (database down, column too long) are not mapped by the controller.
- Evidence: `TasksController.cs`, `TaskService.cs`, `TaskServiceTests`.
- Implementation: **Implemented**.
- Confidence: high.

### FR-002 — List every task, oldest first

The caller receives every stored task ordered by creation time ascending.

- Trigger: `GET /api/Tasks`.
- Input: none.
- Output: JSON array. Empty array when there are no rows.
- Evidence: `TaskService.GetAllTasksAsync`, test `GetAllTasksAsync_WithMultipleTasks_ShouldReturnAllTasksOrderedByCreatedUtc`.
- Implementation: **Implemented**.
- Confidence: high.

### FR-003 — Completion flag is stored and not changed

Each new task is stored incomplete. No operation in this repository sets `IsCompleted` to true.

- Evidence: entity property, migration column `bit` not null, service assignment `IsCompleted = false`. No update method.
- Implementation: **Implemented** as a fixed initial value.
- Confidence: high.
- **Unknown / Requires Confirmation:** whether completion was intended. Commit `8f0658b` describes update and delete methods that were never added to source.

### FR-004 — Server assigns identity and creation time

`Id` is a SQL Server identity. `CreatedUtc` is set in the service to UTC now. The client does not send either value.

- Evidence: `TaskService.CreateTaskAsync`, migration `SqlServer:Identity` starting at 1, tests asserting `Id > 0` and `DateTimeKind.Utc`.
- Implementation: **Implemented**.
- Confidence: high.

### FR-005 — Persist to SQL Server LocalDB

The API uses connection string `DefaultConnection`. On `main` both appsettings files name LocalDB and database `TaskLedgerDb`.

- Evidence: `appsettings.json`, `appsettings.Development.json`, `Program.cs`.
- Implementation: **Implemented** as configuration.
- Confidence: high for the configuration. Confidence low that a database exists.

### FR-006 — Retry transient SQL failures

Save and query use EF Core `EnableRetryOnFailure` with 5 retries and a maximum delay of 30 seconds. No extra SQL error numbers are added.

- Evidence: `Program.cs` at `8ab244e`.
- Implementation: **Implemented**.
- Confidence: high.

### FR-007 — Open API document in Development

When `ASPNETCORE_ENVIRONMENT` is Development, Swagger and Swagger UI are enabled. Launch profiles set that environment and open `swagger`.

- Evidence: `Program.cs`, `Properties/launchSettings.json`.
- Implementation: **Implemented**.
- Confidence: high.

### FR-008 — Anonymous access

Any caller who can reach the host can create and list tasks. There is no login.

- Evidence: `Program.cs` has no authentication or authorization middleware. Launch settings allow anonymous authentication.
- Implementation: **Implemented**.
- Confidence: high.
- **Unknown / Requires Confirmation:** whether this is acceptable, or whether a host in front of the API is expected to restrict access. Nothing in the repository describes that host.

## Constraints

- **Documented** in `cursor.rules.md`: SQL Server is local only; do not point this project at a production database; do not apply migrations automatically; prefer idempotent scripts; do not emit `DROP DATABASE` or `TRUNCATE`.
- **Implemented** in configuration: the committed connection strings are LocalDB, not a remote production server. Startup does not migrate.
- **Implemented** in the model: `Title` max length 200, required; `Description` max length 1000, optional; `CreatedUtc` required.
- The API project targets `net8.0`.

## Removed behaviour (history, not current)

At `e12e94a` the entity had `CreatedAt` and `UpdatedAt`, and the context class was `TaskLedgerDbContext`. Migration id `20260101145453_InitialCreate` created those two timestamp columns. `310c137` replaced that with `AppDbContext`, `CreatedUtc`, and no update timestamp. The old migration is not on `main`.

## Assumptions that must not be treated as requirements

- That tasks belong to a person. The model has no owner.
- That `IsCompleted` will change later. Nothing on `main` says so. Only a commit message describes an update method that was not committed as source.
- That `GET /weatherforecast` is a product feature. It is leftover template code that still runs.
- That SonarQube is part of this product. The discovery snapshot named `sonar-project.properties` and `sonarqube_scan.bat`. Those files are not in git.
