<!--
    ==========================================================================================
    CLAUDE.md / AGENTS.md STARTER

    How to use this in a new Laravel project:

      1. Copy this file in as CLAUDE.md, then `cp CLAUDE.md AGENTS.md`.
      2. Run `php artisan boost:install` (or `boost:update`) FIRST if Boost is not set up yet —
         it rewrites the whole file and would discard everything below. Then paste the two
         fenced blocks back in, after whatever Boost generated.
      3. Publish the Boost config and set `purpose`:
             php artisan vendor:publish --tag=boost-config
         The shipped config has no `purpose` key; add one. Boost's template emits it into
         Foundational Context, which makes it the only project text that survives a regeneration.
      4. Fill in the PROJECT block. Delete any prompt that does not apply — an empty heading is
         worse than no heading.
      5. Leave the HOUSE RULES block alone. That is the point of it.

    Three zones live in the finished file, in this order:
      · whatever Boost generates          — never hand-edited
      · <!-- house-rules --> block        — identical in every project
      · <!-- project --> block            — the only thing you write
    ==========================================================================================
-->

<!-- house-rules:start -->
<!--
    HOUSE RULES — portable. Copy this block into any Laravel project unchanged.

    Everything below is true of any project built this way. Nothing in it names a table, a screen,
    a database or a domain: those live in the PROJECT block that follows. If you find yourself
    wanting to add a project fact here, it belongs there instead.
-->

# House rules

Hand-written, and not generated by anything. They are here because each one was learned by getting
it wrong, and the reason is kept with the rule — a rule with no reason behind it is the first thing
anyone talks themselves out of.

## How these two files work

**`CLAUDE.md` and `AGENTS.md` are the same file, byte for byte.** Different agents read different
names, and an agent reading one must not be working from a shorter set of rules than an agent
reading the other. They have drifted apart before: one stayed at the generated template while the
other grew hundreds of lines of hand-written rules that the first agent never saw.

**Edit `CLAUDE.md`, then `cp CLAUDE.md AGENTS.md`.**

**⚠ `php artisan boost:update` rewrites this file and discards everything hand-written in it.** Not
some of it — all of it. It has happened more than once: it runs as a Composer script, so it fires on
`composer require` and `composer remove` as well as when invoked directly.

After any of those:

```bash
git diff --numstat CLAUDE.md      # a large deletion count means it happened
git checkout HEAD -- CLAUDE.md    # restore, then re-apply anything you actually wanted
```

The same applies to `boost.json`. **This is why the hand-written content sits in two fenced blocks:**
recovering from a rewrite is pasting two blocks back, not reconstructing rules out of a diff.

**Put the application's description in `config/boost.php`, not in prose here.** The generator reads
`config('boost.purpose')` and emits it into Foundational Context itself, so a description held there
is the one piece of project context that survives a regeneration. Publish the config
(`php artisan vendor:publish --tag=boost-config`) and add a `purpose` key; it ships without one.

## Reach for the shared component before writing one

The project block lists what exists. The rule is the same everywhere:

- **A list of records goes through the shared data-table component.** Those wrappers own searching,
  sorting, paging, selection, export and the phone card view, and a hand-rolled table gets none of
  it. If one genuinely does not fit — a two-column summary inside a dialog, a layout the wrapper
  cannot express — a plain `<table>` is fine, **but give it its own horizontal scroll container**,
  and wrap it in an element rather than setting `display: block` on the table, which strips its
  semantics from a screen reader.
- **A checkbox is a native `<input type="checkbox">`** carrying the project's own checkbox class
  from `app.css`, not a component wrapper. It stays a real form control, and the styling stays in
  one place.
- **Searchable dropdowns, telephone fields, charts and download controls each have one component.**
  Use it. Two implementations of the same control will not stay in step, and the second one is
  always the one that misses the accessibility work.

## One design system, one file-handling library

**Every interface aligns to the project's design system reference**, so the product reads as one
thing rather than as the sequence of sprints that built it. The design tokens live in the
stylesheet; take colours, spacing and type from there rather than writing a hex value into a
component. The project block names the reference.

**Documents and images go through one media library** — upload, storage, conversion and download —
rather than each feature inventing its own path handling. Its schema can be extended where a
feature needs extra columns. Two file-handling paths in one application means two sets of rules
about what is public, and one of them will be wrong.

## Controllers stay lean

**Validation belongs in a Form Request. Business logic belongs in a service.** A controller
resolves what it was given, delegates, and responds.

The reason is not tidiness: validation inline in a controller cannot be reused by a console command,
an import or a queued job, so the second caller re-implements it slightly differently and the two
drift. A service is also the only place a multi-step write can be wrapped in a transaction and
tested without a HTTP request.

**⚠ Check the project block for how far this has actually got.** On an inherited codebase this is
usually a rule for new work rather than a description of what is there, and it is worth saying which
so nobody assumes the pattern is already established.

## Confirming an action

**Anything with a consequence asks first** — deleting, submitting something that cannot be
withdrawn, sending something to somebody, changing who can see what.

- **Destructive actions require the word `DELETE` typed**, with the submit button disabled until it
  matches. A confirm dialog you can dismiss with a stray tap is not a confirmation for something
  that cannot be undone. Use one word across the whole project so it is a habit rather than a puzzle.
- **From a page** — a header button, a table row action, a toolbar — a SweetAlert is right, and is
  the majority of uses.
- **⚠ Never open a SweetAlert while a `Dialog`, `AlertDialog` or `Sheet` is open. It freezes the
  whole screen.** The dialog holds a focus trap and marks everything outside itself inert;
  SweetAlert renders at `document.body`, which is outside it. Focus can never reach the confirm
  button and neither layer will close.
- **When a dialog's own action needs confirming, confirm inside that dialog.** Either a two-step
  footer, where the button becomes `Confirm …` and Cancel becomes Back, or an inline typed
  confirmation. Do not stack a second dialog either — keeping it in the open one reads better
  anyway, because the figures being confirmed stay on screen while the user decides.

## Accessibility: build it in, do not audit it later

**Every new page and component carries its own accessibility, at the time it is written.** It is a
requirement, not a polish pass, and retrofitting it across a finished application is several times
the work of writing it in.

**Assume anything you did not write yourself is missing it.** The state this was written against was
about thirty screen-reader-only labels in an entire front end — thirty in total, not per page — with
no skip link and no focus-trap utility.

**The rules, in the order they are most often broken:**

- **An icon-only control needs a name.** A button whose only content is an icon is an unlabelled
  button to a screen reader. Add `<span class="sr-only">…</span>` or `aria-label`. A tooltip does
  not count — `title` is not reliably announced, and on a touch device it never fires at all.
- **A figure that means something by its colour must also say it in words.** Roughly one man in
  twelve has a colour vision deficiency. If the project has a component that renders such a figure
  with its label, everything showing that figure uses it.
- **State changes that happen without a page load need announcing.** A toast, a filter that empties
  a table, a figure that arrives on a partial reload: wrap the region in `aria-live="polite"`, or
  the change is silent.
- **Anything that opens over the page traps focus and returns it.** Use the library's `Dialog`,
  `AlertDialog` or `Sheet` rather than building a panel by hand.
- **Every form control has a `<Label for=…>`**, not a paragraph that looks like one. Use the
  project's error component rather than colouring the border red.
- **Tables get real `<th>` with `scope`**, and a `<caption>` or `aria-label` saying what the table
  is. A grid of `<div>`s is not a table to anybody using one.
- **Never remove a focus ring.** If it looks wrong, restyle it — `outline: none` with nothing in its
  place makes the keyboard path invisible.
- **Interactive means `<button>` or `<a>`.** A `<div @click>` is not reachable by keyboard and is
  announced as nothing.

**If the project ships a user manual, editing a screen means editing its chapter.** Keep a test that
holds the manual against the application — the routes it prints must answer, the menu entries it
names must exist — because nothing else ties a chapter to the screen it describes, and documentation
drifts silently by nature.

## Every screen is a phone screen too

**Every rule here fails silently on a desktop and breaks the screen on a phone.** That asymmetry is
why they are written down, and why they are worth a test that reads source.

**Target 360px and up.** Check 360, 390 and 768 in device mode — 768 because that is where a
`md:`-style breakpoint and any JavaScript media query meet, and they must not both be true at once.

- **A dialog sets its width from `sm:` upward, never unprefixed and never with `!`.** The base
  carries a mobile cap (`w-full max-w-[calc(100%-2rem)] … sm:max-w-lg`), and because the class merge
  is `tailwind-merge`, a call site's own `max-w-*` **replaces** it. Worse, an unprefixed width and
  the base `sm:` width are different merge keys, so the override never removes the `sm:` one and
  loses on source order — the width appears to do nothing on desktop, someone reaches for `!`, and
  now it is broken at both ends. `sm:max-w-4xl` gives the same desktop result and keeps the guard.
  Anything wider than `lg` needs an `md:`/`lg:` step too, or a 640px viewport gets a 1000px layout.
- **Heights use `dvh`, never `vh` and never `max-h-screen`.** `vh` on mobile is the URL-bar-hidden
  height. A full-height box centred with `translate-y-[-50%]` puts its own title above the screen,
  unreachable, because the scrolling happens inside the box.
- **A form control is `text-base md:text-sm`, never `text-sm` alone.** Safari zooms the whole page
  when a focused control is under 16px and does not zoom back. The base components do this already;
  the failure is a call site overriding it.
- **Touch sizing goes through `pointer-coarse:`, not a breakpoint.** A finger needs ~44px and a
  mouse does not, and `pointer-coarse` keys off the input device rather than the width — so touch
  gets a real target and no desktop screen moves a pixel. Set it once in the button variants.
  ⚠ It cannot be checked in device emulation, which emulates width and not pointer type.
- **A `flex-1` item needs `min-w-0`.** A flex item's default `min-width: auto` refuses to shrink
  below its content's intrinsic width, so one wide table widens the row past the viewport and the
  whole document scrolls sideways. ⚠ Before deleting a width that looks like debris on a page
  container, check it is not the only thing clamping that minimum.
- **A horizontally scrolling row is `justify-start`, never centred.** A centred flex container whose
  content overflows puts its first item at a negative offset, and `scrollLeft` cannot go negative —
  so the start becomes unreachable. Applies to tab strips, filter chips, any pill row. Give the row
  `max-w-full overflow-x-auto` and the items `shrink-0`.
- **An overlay needs a way out that works without a keyboard.** A phone has no Escape key, so a
  sheet that hides its close button *and* disables outside dismissal cannot be closed at all — and
  it looks fine on a desktop, where Escape still works. That is exactly how it ships.
- **Grids start at one column.** `sm:grid-cols-2` is right; a bare `grid-cols-3` is three columns on
  a phone. A tab strip given a column count starts at two.
- **`title=""` is invisible on touch.** If it carries information that appears nowhere else, it is
  not documented.

**Judgement matters more than the grep.** Distinguish unusable from merely tight: a two-column grid
of label/value pairs is snug and fine, a two-column grid of input fields is not. An audit that
reports everything is as useless as one that reports nothing.

## Guard tests that read source

When breaking a rule produces no symptom on the machine the developer is using and a broken screen
on the machine the user is using, **assert it by scanning source**. Most of the mobile and dialog
rules above are enforced this way.

Three things make the difference between a guard and a decoration:

- **Prove it fails against the unfixed code before believing it.** Stash the fix, run the test,
  confirm it fails, restore. A guard that passes for the wrong reason is worse than none.
- **Mutation-test the important ones.** Re-break one instance deliberately and confirm the guard
  fires and names the right file.
- **⚠ Parse elements, not lines.** A formatter wraps long attribute lists, so a tag and its `class`
  routinely sit on different lines. A line-based scan silently skips exactly the worst offenders and
  then reports a clean run.

**Say what they are.** They catch regressions; they did not catch the original bug and could not.
Nothing here drives a browser, so layout is verified by looking at it.

## Migrations are consolidated, not accumulated

**One migration per module, holding that module's tables in their settled form.** Every `ALTER`-style
migration is folded back into the `Schema::create` it belongs to, and every table sits in the module
that owns it.

**So a new column goes into the create migration, not into a new file.** The schema is read as a
definition rather than as a history; the history is in git, where it belongs.

**Cross-cutting migrations stay separate and must run last**, because they run against every table:
the CHECK constraints in one registry, any triggers, row-level security policies, and every foreign
key after every table exists. Name them so they sort last.

**⚠ ORDER MATTERS AND IS EASY TO GET WRONG.** A create migration must sort *before* the constraint
registry, or its CHECK constraints are silently skipped — the registry walks its list and passes over
any table that does not exist yet, so the constraint never appears and nothing fails. A consolidation
once dropped seven CHECK constraints exactly that way, and the only thing that caught it was the
schema fingerprint.

**A schema fingerprint is how you prove a schema change was the one you meant.** Dump every column,
comment, constraint and index in a stable order; run it before a structural refactor, run
`migrate:fresh` on a scratch database afterwards, and diff. An empty diff is the only evidence worth
having — folding migrations by hand is exactly where a column quietly becomes nullable, a default
moves, or an index is not recreated, and none of those break a test. They break a deployment months
later on a table nobody was looking at.

**Comments are part of the schema, not documentation about it.** If generated documents read table
and column comments, a fold that drops one silently empties a row of a deliverable. Note that
`->change()` DROPS a column comment on PostgreSQL.

## Database conventions

Write database-agnostic queries and portable schemas. Treat the primary database as the source of
truth and make the other paths explicit.

- Default to the query builder and Eloquent so the driver handles dialect differences.
- Prefer portable column types (`string`, `text`, `json`, `timestamp`); avoid database-specific
  types unless guarded by a driver check.
- Where a query genuinely differs — JSON operators, full-text search, `ILIKE` vs `LIKE`, date
  functions, `RETURNING` — branch on `DB::connection()->getDriverName()`.
- Performance-critical paths may use database-specific features where the portable builder is too
  slow, but both drivers must be handled explicitly.

**⚠ MariaDB reports itself as `'mysql'`, and it is not MySQL.** So `getDriverName() === 'mysql'` is
necessary and *not sufficient* wherever the two diverge. Tell them apart by the server banner:

```php
str_contains(strtolower(DB::selectOne('SELECT VERSION() AS v')->v), 'mariadb')
```

The known divergences, all found the hard way:

- **Dropping a CHECK:** MySQL wants `DROP CHECK <name>` (8.0.16+) and has **no `IF EXISTS`** for it
  at any version. MariaDB wants `DROP CONSTRAINT IF EXISTS <name>` (10.2+) and does not accept
  `DROP CHECK`. `DROP CHECK IF EXISTS` is valid on neither — and a project emitted exactly that for
  months, because it only ever ran on PostgreSQL so nothing caught it.
- **Dropping a foreign key:** MySQL wants `DROP FOREIGN KEY <name>`, again with no `IF EXISTS`.
  MariaDB accepts `DROP FOREIGN KEY IF EXISTS` (10.1+).
- Where the engine will not accept `IF EXISTS`, ask `information_schema.TABLE_CONSTRAINTS` first
  rather than dropping blind.

**Run a fresh `migrate` against every engine you claim to support before believing a migration is
portable.** A suite that runs on SQLite and the primary database touches neither MySQL nor MariaDB,
so a migration can be green in CI and fail on the first deploy.

## Running the test suite

- **A green run on SQLite has not tested what the database enforces.** SQLite installs no CHECK
  constraints, no triggers and no row-level security, so the tests that prove those guarantees skip
  — and, worse, tests that *do not* skip can pass there for the wrong reason. Two live 500s once sat
  green on SQLite for months, both writing a string a CHECK constraint did not permit. **Any change
  that writes a constrained string column — a status, a channel, a type, a scope — must be exercised
  on the real database.**
- **Point the suite at a separate database.** `RefreshDatabase` runs `migrate:fresh` and will
  otherwise destroy development data. Override the connection on the command line rather than
  editing `phpunit.xml`.
- **Never run two suites against the same test database at once**, and never edit files while a run
  is in progress — the result describes a tree that no longer exists.
- **Prove a new test fails against the old code before believing it.**
- **`php artisan test` can die partway through, print no summary, and still exit 0** — so a CI step
  checking the exit code sees a pass for a run that never finished. It is a memory ceiling, not a
  broken test, and it returns as the suite grows. Use `php -d memory_limit=2G vendor/bin/phpunit`
  for the whole suite and keep `php artisan test --compact --filter=…` for single files.
- **A front end with a build step needs building before the suite runs on a fresh clone**, or every
  test that renders a full page fails with a manifest error that reads like an application bug.

## Docker: running and deploying

The patterns worth keeping whatever the project:

- **Development and production are separate down to the compose project name**, so nothing in one
  can reach a container, volume or database belonging to the other.
- **The production image is the artefact.** Dependencies, built assets and source are layers, not
  mounts, so what runs is what was built and a deployment is a new container rather than a file
  copy. Opcache with `validate_timestamps = 0` depends on that being true.
- **There is no dev-server service in production**, so the browser can never be told to fetch assets
  from a machine that is not there. Removing the service is the only way to make that impossible.
- **Guard the production build on the settings that fail silently at run time** — an empty app key,
  an empty database password, a non-production environment (which may seed demo data with published
  logins), and debug left on. Refuse to build rather than discover it later.
- **`optimize` runs at container start, not at build**, because config caching freezes what `env()`
  returns and those values arrive at run time. One consequence: `env()` outside `config/` returns
  null once cached, so anything reading `env()` directly breaks under a cached config.
- **A backup takes both halves** — the database and the file storage. The database knows which file
  belongs to which record; the volume holds the file. Either alone restores to rows pointing at
  documents that do not exist.
- **Environment files are read twice**, by the framework and by compose, so the database name, user
  and password provision the container *and* configure the connection to it. They cannot drift, and
  changing them only takes effect on a volume that does not exist yet.

<!-- house-rules:end -->

<!-- project:start -->
<!--
    THIS PROJECT — the only block that changes between projects.

    Everything here is true of this codebase and nowhere else. Keep it factual and keep it
    current: a guideline that names a component which is not there, or a figure that stopped
    being true six months ago, teaches an agent to distrust the whole file.
-->

# This project

## What it is

<!-- TODO: Two or three paragraphs. What the application is for, who uses it, and the one or two
     constraints that shape every decision (tenancy? privacy? an approval chain? a regulator?).
     Put the same text in config/boost.php as `purpose` so it survives boost:update. -->

## Platform and delivery

<!-- TODO: Web only? Installable? An API? A mobile client? Anything the product deliberately does
     NOT do, and the test that holds that line if there is one. -->

## Module folders

<!-- TODO: If the same folder names run through app/Models, app/Http/Controllers, app/Services,
     resources/js/pages and tests/, list them in a table with what each holds. A new file then
     has one obvious home instead of a plausible one. Note any file that sits loose on purpose. -->

## Tenancy and authorisation

<!-- TODO: DELETE THIS SECTION IF THE APPLICATION IS SINGLE-TENANT.

     Otherwise this is the most consequential thing in the file. Name the trait or scope that
     decides which rows a user may see; say whether it fails closed; and list every model that
     deliberately opts out, with the method to use instead. The models that opt out are the ones
     where forgetting leaks one tenant's data to another SILENTLY — everywhere else you get an
     empty screen and notice. -->

## Domain rules that are not obvious from the schema

<!-- TODO: The decisions a reader would otherwise undo. Why two similar columns are not the same
     thing; what is immutable once written and what enforces it; anything generated centrally
     (codes, references) and where the registry is. -->

## Shared components to reach for

<!-- TODO: A table of need -> component. The house rules point at this list, so it is what makes
     "use the shared component" actionable. Include the data table, searchable select, phone
     input, charts, download control, checkbox class, and anything else with exactly one right
     answer. Verify each path exists before writing it down. -->

| Need | Component |
| --- | --- |
|  |  |

<!-- TODO: Name the design system reference and where the design tokens live. Name the media
     library used for documents and images. -->

## How far the conventions have actually got

<!-- TODO: Be honest. On an inherited codebase, say how many controllers use Form Requests versus
     inline validation, and anything else that is a rule for new work rather than a description
     of what is there. A reader who assumes a pattern is established and finds it is not stops
     trusting the rest. -->

## Running it locally

<!-- TODO: The commands, the URLs, the database name and port, and anything that is a trap on a
     fresh clone (a build step the tests need, an env file read twice, a volume that has to be
     dropped before a setting takes). -->

## Deploying

<!-- TODO: Which Dockerfile/compose/env files, the one-time steps, and what the build refuses to
     start on. -->

## Running the suite here

<!-- TODO: The current test and assertion count and against which database. Which tests skip on
     which engine and what guarantee is therefore untested when they do. The exact command for
     the real database, including the separate test database name. Where the guard tests live. -->

## Tools installed but switched off

<!-- TODO: Anything present in package.json or composer.json that is NOT wired up, and what
     happens if somebody assumes it is. This section prevents a whole class of wasted afternoon. -->

## Documentation and where the plan lives

<!-- TODO: The user manual if there is one and what gates it. Generated documents, the command
     that builds them, and which parts are hand-written and preserved. Anything gitignored, so
     nobody assumes a push carried it. -->

<!-- project:end -->
