diff --git a/ai/backend-context.md b/ai/backend-context.md
index 8ab6a97..9214acd 100644
--- a/ai/backend-context.md
+++ b/ai/backend-context.md
@@ -8,57 +8,76 @@ Read `ai/shared.md` first. This file covers backend-specific rules.
**Location:** `backend/`.
-The application is still close to the Laravel starter structure. Do not
-introduce a domain architecture, repository layer, service layer, or other
-abstraction before the codebase and requested behavior justify it. Match
-existing Laravel conventions and inspect similar code before adding a new
-pattern.
+The backend follows a domain-oriented structure. Existing areas use entities,
+DTOs, repository interfaces, Eloquent implementations, use cases, and test
+fakes. Extend those patterns when adding behavior to an established area. Do
+not add speculative layers or interfaces that the requested behavior does not
+need.
-## Laravel patterns
+The Vue application remains a separate project under `frontend/website/`.
+Keep frontend source, dependencies, builds, and delivery out of the backend
+unless the user explicitly asks to integrate them. The backend root route is
+intentionally unclaimed; the built-in health endpoint is `/up`.
-- Keep controllers thin. Put reusable business behavior in an appropriately
- named application or domain class once the behavior warrants extraction.
-- Use dedicated request validation rather than validating substantial payloads
- inline in controllers.
-- Let unexpected exceptions reach Laravel's exception handler. Catch only
- exceptions that can be handled meaningfully at the current boundary.
+## Code patterns
+
+- Inspect similar domain code before adding a new entity, DTO, repository,
+ use case, controller action, or fake.
+- Entities own domain state and behavior and expose descriptive methods.
+- DTOs are explicit data containers for creation or transfer between seams.
+- Repository interfaces define domain-facing persistence operations. Keep
+ Eloquent details in their implementations and register bindings in the
+ application provider.
+- Put reusable business behavior in a use case. Give a use case a request DTO
+ when it accepts a payload; call `execute()` directly when it has no input.
+- Document use-case exceptions with `@throws` when callers are expected to
+ handle them.
+- Keep controllers thin. Translate HTTP input to use-case input and domain
+ output to the response contract.
+- Catch only documented, expected exceptions at the controller boundary. Let
+ unexpected exceptions reach Laravel's exception handler.
+- Fake repositories are in-memory implementations used by unit tests. Return
+ a new entity instance from create and lookup methods rather than exposing a
+ stored reference.
- Use Eloquent relationships and query scopes consistently rather than
duplicating query fragments.
-- Avoid speculative interfaces and abstractions with only one trivial
- implementation.
-- The Vue application is a separate project under `frontend/website/`.
-- Keep frontend source, dependencies, builds, and delivery out of the backend
- unless the user explicitly asks to integrate them.
-- The backend root route is intentionally unclaimed. The built-in health
- endpoint is `/up`.
-## Tests
+## Unit tests
-- Follow the existing PHPUnit organization under `tests/Unit/` and
- `tests/Feature/`.
-- Prefer `PHPUnit\Framework\TestCase` when a test only exercises plain PHP.
-- Extend `Tests\TestCase` only when the test needs Laravel's container,
- facades, database, routing, or HTTP kernel.
-- HTTP feature tests extend `Tests\TestCase`.
-- Use `RefreshDatabase` when a test reads or writes database state.
-- Assert behavior at the appropriate seam:
- - Unit tests cover isolated business behavior and edge cases.
- - Feature tests cover routing, middleware, validation, persistence, and
- response shape.
-- Do not duplicate every business branch through the HTTP layer when unit
- coverage already proves it. Feature tests should focus on wiring and the
- public contract.
+- Follow the existing organization under `tests/Unit//`.
+- Use fake repositories and fake collaborators for the behavior under test.
+ Construct unrelated dependency entities directly instead of routing them
+ through additional repositories.
+- Test use-case branches at the use-case seam. Do not repeat every branch in
+ controller or HTTP tests.
+- Plain entities, value objects, use cases, middleware, and controller units
+ should extend `PHPUnit\Framework\TestCase` when they do not need Laravel.
+- Extend `Tests\TestCase` only when a test needs Laravel's container, facades,
+ database, routing, or HTTP kernel.
+- Reserve direct Eloquent repository feature tests for persistence mapping,
+ query behavior, or database constraints that cannot be proven through a
+ plain unit test.
-## Test database
+## Feature tests
+- Feature tests exercise the HTTP seam: routing, middleware, real Eloquent
+ bindings, cookies, persistence, validation, and response shape.
+- Feature tests are additive to unit tests. Prefer a happy path and the
+ relevant authorization guard instead of duplicating all business branches.
+- Place endpoint tests under `tests/Feature//`, extend
+ `Tests\TestCase`, and use `RefreshDatabase` when database state is involved.
- Development and runtime use PostgreSQL through the local Unix socket.
-- PHPUnit intentionally uses SQLite `:memory:` as configured in
- `phpunit.xml`.
-- Feature tests are self-contained and do not require the process-compose
- PostgreSQL service.
+ PHPUnit uses SQLite `:memory:` as configured in `phpunit.xml`, so feature
+ tests are self-contained and do not need the worktree stack.
- Never point `RefreshDatabase` tests at the development PostgreSQL database.
-- Keep mail set to the PHPUnit `array` transport unless a test explicitly
- exercises a real mail integration.
+- Keep mail on PHPUnit's `array` transport unless a test explicitly exercises
+ a real mail integration.
+- Authentication uses a custom database-session cookie, not a Laravel guard.
+ `actingAs()` does not apply. Credentialed requests use
+ `withCredentials()` and `withUnencryptedCookie()` because API middleware
+ reads the raw cookie.
+- Read an unencrypted response cookie with
+ `$response->getCookie($name, false)`.
## PHP rules
@@ -80,7 +99,7 @@ pattern.
- Do not add production repository or model APIs solely to make seeding
convenient.
- Use existing lookup methods for cross-seeder relationships. When a group of
- records only makes sense together, keep them in one seeder and retain local
+ records only makes sense together, keep it in one seeder and retain local
references.
## Migrations
@@ -99,15 +118,12 @@ pattern.
- Once a production database exists, replace this policy with additive,
forward-only migrations.
-## Before completing backend work
+## Backend workflow
-- Run the focused test during development.
-- Run the full test suite before completion:
-
- ```sh
- direnv exec "$(git rev-parse --show-toplevel)" php artisan test
- ```
-
-- Run the Composer static-analysis scripts.
+- Run the focused PHPUnit test while developing.
+- Use `just backend-types-check` for Larastan and `just backend-test` for the
+ full PHPUnit suite during iteration.
- Fix failures caused by the change. Report unrelated baseline failures
- precisely rather than hiding them or expanding scope without authorization.
+ precisely rather than expanding scope silently.
+- The shared `just test-all` command is the required completion gate. Focused
+ backend recipes never replace it.
diff --git a/ai/frontend-context.md b/ai/frontend-context.md
index 213d164..a2acb1e 100644
--- a/ai/frontend-context.md
+++ b/ai/frontend-context.md
@@ -4,7 +4,8 @@ Read `ai/shared.md` first. This file covers frontend-specific rules.
## Project context
-**Stack:** Vue 3.5, TypeScript 6, Vite 8, Vue Router 5, Pinia 4, npm.
+**Stack:** Vue 3.5, TypeScript 6, Vite 8, Vue Router 5, Pinia 4, Zod 4,
+Cypress 15, npm.
**Location:** `frontend/website/`.
@@ -12,18 +13,16 @@ The frontend is a standalone application. Keep its source, dependencies,
development server, and production build independent from the backend unless
the user explicitly requests integration.
-The scaffold uses:
+The application currently has route-level views, reusable authentication
+components, a Pinia authentication store, Zod schemas, API URL handling, and
+Cypress end-to-end specs. The main entry points are:
-- `src/App.vue` as the root component.
-- `src/main.ts` to create the app and install the router and Pinia.
-- `src/router/index.ts` for routes.
-- `src/stores/` for Pinia stores.
-- `@` as an alias for `src/` in both Vite and TypeScript.
-
-There are no views, shared components, API layer, or configured test suite
-yet. Cypress is installed as a frontend development dependency, but there is
-no Cypress configuration or npm test script. Do not invent an architecture
-before requested behavior establishes one.
+- `src/App.vue` for the root component.
+- `src/main.ts` for app creation, Pinia, the router, and global styles.
+- `src/router/index.ts` for routes and authentication guards.
+- `src/stores/` for Pinia stores and API boundaries.
+- `src/views/` and `src/components/` for route and reusable UI.
+- `@` as the `src/` alias in Vite and TypeScript.
## Package management and commands
@@ -36,32 +35,14 @@ Install dependencies in a fresh checkout or worktree:
direnv exec "$(git rev-parse --show-toplevel)" npm install
```
-To start only the development server on the port assigned by the shell hook:
+`npm run format` and `npm run lint` rewrite files. Their `format:check` and
+`lint:check` counterparts are non-mutating completion checks. `npm run build`
+runs type checking and the production build; generated `dist/` output is
+ignored.
-```sh
-direnv exec "$(git rev-parse --show-toplevel)" \
- npm run dev -- \
- --host 127.0.0.1 \
- --port "$VITE_PORT" \
- --strictPort
-```
-
-`process-compose` starts the frontend as part of the full development stack.
-The frontend remains directly accessible on `VITE_PORT`; Caddy does not proxy
-it.
-
-The available validation commands are:
-
-```sh
-direnv exec "$(git rev-parse --show-toplevel)" npm run format
-direnv exec "$(git rev-parse --show-toplevel)" npm run lint
-direnv exec "$(git rev-parse --show-toplevel)" npm run type-check
-direnv exec "$(git rev-parse --show-toplevel)" npm run build
-```
-
-`npm run format` and `npm run lint` rewrite files. Review the resulting diff.
-`npm run build` runs type checking and the production build. Build output
-under `dist/` is generated and ignored.
+`process-compose` starts the frontend as part of the complete development
+stack. The frontend is directly accessible on `VITE_PORT`; Caddy serves the
+backend and does not proxy the frontend.
## Vue conventions
@@ -72,58 +53,69 @@ under `dist/` is generated and ignored.
`src/router/index.ts`.
- Keep page, component, composable, and store responsibilities distinct.
- Prefer small components with explicit props and emitted events.
-- Keep component styles scoped until the project adopts a deliberate global
- styling system.
- Use setup-style Pinia stores named `useXxxStore`.
-- Inspect similar files before introducing a new component, composable,
- store, or data-access pattern.
+- Keep application-wide resets and base styles in `src/styles/main.css`.
+ Keep component and view styles scoped and match established Attainly visual
+ patterns.
+- Inspect similar files before introducing a component, composable, store,
+ route, or data-access pattern.
-## TypeScript
+## TypeScript and runtime validation
-- Preserve the strict TypeScript configuration and
- `noUncheckedIndexedAccess`.
-- Do not use `any`. Model unknown external values as `unknown`, then narrow or
- validate them.
-- Derive types from runtime schemas if the project adopts a schema library.
- Do not maintain a hand-written type that can drift from its schema.
-- Validate payloads at trust boundaries, especially backend responses and
- user-submitted forms.
-- Keep request and response types close to the API or store boundary that
- owns them.
-- Keep the `@` alias aligned across Vite, TypeScript, and any future test
- configuration.
+- Preserve strict TypeScript and `noUncheckedIndexedAccess`.
+- Do not use `any`. Model unknown external values as `unknown`, then parse or
+ narrow them.
+- Zod schemas are the source of truth for runtime payloads. Define a schema
+ and derive its TypeScript type with `z.infer` instead of maintaining a
+ parallel hand-written interface.
+- Parse every backend response at the store or API boundary before placing it
+ in application state.
+- Keep read and write schemas separate when their shapes differ.
+- Validate user-submitted forms with a Zod object schema when the form has
+ meaningful validation rules. Surface field errors from the schema rather
+ than maintaining parallel regular expressions and error logic.
+- Keep request and response schemas and types near the API or store boundary
+ that owns them.
+- Keep the `@` alias aligned across Vite, TypeScript, and future test tooling.
## State and API access
- Use Pinia for shared client state. Keep component-local state in components.
-- Keep server requests and response transformation at an API or store
- boundary, not scattered through presentation components.
+- Keep requests, response parsing, and response transformation at an API or
+ store boundary, not in presentation components.
- Represent loading, empty, success, validation-error, and unexpected-error
states explicitly.
-- Do not cast unchecked JSON directly to an application interface.
-- Keep read and write payload types separate when their shapes differ.
+- Do not cast unchecked JSON to an application interface.
+- Send cookie-backed API requests with the established credentials behavior.
## Testing
-Cypress is installed, but no frontend test configuration or test script
-exists yet.
+- Cypress is configured under `cypress/` and runs through `npm run test:e2e`
+ or `just frontend-cypress-run`.
+- Prefer the cheapest test seam that proves the behavior. Cypress covers
+ routing, browser forms, authentication flows, request wiring, and responsive
+ behavior.
+- Mock backend calls in frontend-focused Cypress tests. Use the worktree
+ backend only for a deliberately end-to-end integration scenario.
+- Assert both the request contract and the rendered response behavior when a
+ spec intercepts an API call.
+- Keep mock payloads synchronized with exported store types and Zod schemas.
+ Add typed builders when payloads repeat across specs, and parse builder
+ output through the exported schema when practical.
+- Authentication requests are mockable like every other frontend boundary.
+ Backend persistence, cookie creation, middleware, and mail behavior belong
+ in PHPUnit tests.
+- No Vitest unit or component suite is configured. Do not claim that coverage.
+ If new pure logic or component behavior cannot be proved economically with
+ Cypress, establish the smallest appropriate Vitest setup test-first.
-- Do not invent test commands or claim frontend tests passed.
-- New frontend behavior must still follow the shared test-first workflow.
- Establish the smallest appropriate test setup before implementing behavior
- that needs it.
-- Unit tests should cover pure transformations, composables, and store logic.
-- Component tests should cover rendering, events, form behavior, and
- conditional UI.
-- End-to-end tests should cover routing, multi-page flows, and request wiring.
-- Prefer the cheapest layer that proves the behavior.
-- Mock backend requests in frontend tests. Do not retest backend persistence,
- validation, authentication, or mail behavior through the frontend.
+## Frontend workflow
-## Before completing frontend work
-
-- Run the focused test while developing once test tooling exists.
-- Run the formatter, linter, type checker, production build, and every
- configured test script affected by the change.
-- Do not claim a green gate when a command fails. Report a baseline or
- environmental failure precisely.
+- Run the focused Cypress spec while developing when browser behavior changes.
+- Run `npm run format` and `npm run lint` before committing frontend changes,
+ then review every rewrite.
+- Use the focused `just frontend-*` recipes for development feedback.
+- The shared `just test-all` command is the required completion gate. Focused
+ frontend checks never replace it.
+- Do not claim a green gate when a command fails. Report baseline or
+ environmental failures precisely.
diff --git a/ai/shared.md b/ai/shared.md
index 00a1273..bf52fe5 100644
--- a/ai/shared.md
+++ b/ai/shared.md
@@ -39,6 +39,24 @@ Use judgment for changes that cannot meaningfully be test-driven, such as
documentation-only edits or declarative environment configuration. Validate
those changes with the most relevant parser, formatter, dry run, or check.
+## Maintaining these instructions
+
+- Treat user steering as durable when it corrects or establishes a reusable
+ project rule for workflow, architecture, conventions, validation, safety,
+ or scope.
+- When durable steering is received during work, update the appropriate
+ `ai/*.md` file in the active worktree before completing the task. Do not
+ wait for a separate request to maintain the instructions.
+- Put repository-wide rules in `shared.md` and stack-specific rules in the
+ matching backend or frontend guide.
+- Merge new guidance into the existing rule set. Keep it concise, remove
+ duplication, and resolve conflicts between shared and stack-specific text.
+- Do not persist task-specific scope, temporary directions, secrets,
+ environment incidents, or instructions that conflict with higher-priority
+ guidance.
+- Commit a durable instruction update separately from implementation unless
+ the task itself is solely an instruction change.
+
## Approval discipline
- Treat dependency installation, tests, static analysis, formatting, linting,
@@ -64,58 +82,42 @@ those changes with the most relevant parser, formatter, dry run, or check.
- A worktree owns its own isolated stack. The flake shell hook assigns a
deterministic port offset and creates worktree-local PostgreSQL state.
- Start a worktree stack only when runtime or integration validation requires
- it. Start it once from the worktree root, reuse it throughout validation,
- and stop it once when finished.
-- Do not start any service for PHPUnit, frontend formatting, linting, type
- checking, or production builds.
-- When authorized worktree stack control is necessary, operate it directly.
- Do not ask the user to start or stop worktree services.
-- For non-interactive use, start the stack detached and stop it when finished,
- as shown below.
-- Do not use `process-compose -t=false` for a detached stack. It can leave an
+ it. Start it once, reuse it throughout validation, and stop it once when
+ finished.
+- PHPUnit, frontend formatting, linting, type checking, and production builds
+ do not require services. The Cypress completion suite does require the
+ worktree stack.
+- When worktree stack control is necessary, operate it directly. Do not ask
+ the user to start or stop worktree services.
+- Never use `process-compose -t=false` for a detached stack. It can leave an
orphaned PostgreSQL process holding the data directory.
- Non-interactive agent shells do not automatically load direnv. Bare project
commands can use missing tools, default ports, or paths from the main
checkout.
-- Run project tooling that depends on the repository development environment
- through direnv. This includes PHP, Composer, Artisan, npm, tests, builds,
- database clients, and services.
-- Resolve the direnv target from the worktree containing the agent's current
- working directory. Never target the main checkout or a different worktree:
+- Run PHP, Composer, Artisan, npm, tests, builds, database clients, and
+ services through the development environment.
+- Resolve the direnv target from the worktree containing the current working
+ directory. Never target the main checkout or a different worktree:
```sh
direnv exec "$(git rev-parse --show-toplevel)"
```
-- Git and environment-neutral read-only file inspection do not need the
- direnv wrapper.
-- Worktree stack examples:
+- Git and environment-neutral read-only inspection do not need direnv.
+- Start and stop a worktree stack from its root:
```sh
direnv exec "$(git rev-parse --show-toplevel)" process-compose up -D
direnv exec "$(git rev-parse --show-toplevel)" process-compose down
```
-- Run backend commands from `backend/`, or explicitly change into it in the
- command. The direnv target remains the worktree root.
-- Run frontend commands from `frontend/website/`.
-- `process-compose` starts the frontend on the worktree's assigned port. To
- start only the frontend:
-
- ```sh
- direnv exec "$(git rev-parse --show-toplevel)" \
- npm run dev -- \
- --host 127.0.0.1 \
- --port "$VITE_PORT" \
- --strictPort
- ```
-
+- Run backend commands from `backend/` and frontend commands from
+ `frontend/website/`. The direnv target remains the worktree root.
- When a normally valid check fails because a required service is down,
- surface the environmental failure. Do not skip the check or silently switch
- to a different database or service.
+ surface the environmental failure. Do not skip the check, change databases,
+ or claim the work is complete.
- PHPUnit is self-contained and uses in-memory SQLite. It does not require the
- PostgreSQL stack unless a future test is explicitly designed as a real
- PostgreSQL integration test.
+ PostgreSQL stack unless a future test explicitly targets real PostgreSQL.
## Code style
@@ -155,14 +157,13 @@ those changes with the most relevant parser, formatter, dry run, or check.
- Do not include drive-by formatter or linter changes. Restore them or land
them as a separate formatting commit.
- If a check fails on untouched code, do not bundle an unrelated fix. Report
- the pre-existing failure or handle it as its own explicitly scoped change.
+ the baseline failure or handle it as its own explicitly scoped change.
## Branching
- Never implement features directly in the main checkout or on
`master`/`main`.
-- Use a dedicated worktree under
- `/.worktrees/`.
+- Use a dedicated worktree under `/.worktrees/`.
- Create it with:
```sh
@@ -183,10 +184,10 @@ those changes with the most relevant parser, formatter, dry run, or check.
```
- Never symlink `backend/vendor` or `frontend/website/node_modules` from
- another checkout. Dependency paths and generated files must remain
+ another checkout. Dependencies and generated files must remain
worktree-local.
-- The shell hook installs backend dependencies but does not install frontend
- dependencies. From `frontend/website/`, provision them with:
+- The shell hook installs backend dependencies but not frontend dependencies.
+ Install frontend dependencies from `frontend/website/`:
```sh
direnv exec "$(git rev-parse --show-toplevel)" npm install
@@ -196,59 +197,35 @@ Do not push anything. Make commits as the TDD workflow requires.
## Before completing a change
-Run the smallest relevant checks while iterating, then run every repository
-gate affected by the change.
+A change is not complete until the worktree stack is ready and the unified
+gate passes against that worktree:
-### Backend
+1. Start the stack detached from the worktree root:
-- Run tests from `backend/`:
+ ```sh
+ direnv exec "$(git rev-parse --show-toplevel)" \
+ process-compose up -D
+ ```
- ```sh
- direnv exec "$(git rev-parse --show-toplevel)" php artisan test
- ```
+2. Poll `process-compose process list` until every process is running and
+ ready.
+3. Run the complete gate from the worktree root:
-- Run the Composer checks defined by `backend/composer.json`:
+ ```sh
+ direnv exec "$(git rev-parse --show-toplevel)" just test-all
+ ```
- ```sh
- direnv exec "$(git rev-parse --show-toplevel)" composer types:check
- direnv exec "$(git rev-parse --show-toplevel)" composer test
- ```
+4. Do not hand-assemble a substitute from focused commands. `test-all` runs
+ frontend format and lint checks, frontend type checking, Larastan, the
+ production build, PHPUnit, and Cypress in fail-fast order.
+5. Everything must pass before completion. Report exact baseline or
+ environmental failures rather than hiding them.
+6. If the stack was started only for validation, stop it when finished:
-- Do not claim a green gate when a command fails. If the failure predates the
- change, report the precise baseline failure.
+ ```sh
+ direnv exec "$(git rev-parse --show-toplevel)" process-compose down
+ ```
-### Frontend
-
-- Run these commands from `frontend/website/`:
-
- ```sh
- direnv exec "$(git rev-parse --show-toplevel)" npm run format
- direnv exec "$(git rev-parse --show-toplevel)" npm run lint
- direnv exec "$(git rev-parse --show-toplevel)" npm run type-check
- direnv exec "$(git rev-parse --show-toplevel)" npm run build
- ```
-
-- The formatter and linters rewrite files. Review their changes before
- committing.
-- No frontend test runner is configured yet. Do not claim unit, component, or
- end-to-end test coverage until the relevant scripts exist and pass.
-
-### Environment and integration
-
-- For Nix or shell-hook changes, run:
-
- ```sh
- direnv exec "$(git rev-parse --show-toplevel)" nix fmt
- direnv exec "$(git rev-parse --show-toplevel)" nix flake check
- ```
-
-- For service configuration changes, run:
-
- ```sh
- direnv exec "$(git rev-parse --show-toplevel)" \
- process-compose --dry-run
- ```
-
-- When a change affects runtime wiring, start the worktree's stack and verify
- the relevant endpoint or service against that worktree.
-- If you started the stack only for validation, stop it before finishing.
+Focused `just` recipes are for iteration only. For Nix or shell-hook changes,
+also run `nix fmt` and `nix flake check`. For service configuration changes,
+also run `process-compose --dry-run`.