# Boilerplate execution progress

Plan: 2026-08-23-nrems-boilerplate.md · Branch: feat/boilerplate
Loop per task: Opus implementer → spec review → quality review (Fable-level) → next.

| Task | Status | Commit | Notes |
|---|---|---|---|
| 1 Laravel 13 scaffold | ✅ done+reviewed | ee4296f | spec compliant; no secrets committed |
| 2 Packages + QA toolchain | ✅ done+reviewed | 770c266 | fortify 1.38 (passkeys dep), yajra 13.0, pest 4.7; Task 7 must trim fortify features |
| 3 Theme A layout shell | ✅ done+reviewed | 69bd813+3fbb239 | shell verbatim; gating fix after review |
| 4 Base conventions | ✅ done+reviewed | 445515f | format_inr verified on 14 cases; phpstan path widening to drop in Task 14 |
| 5 UI components | ✅ done+reviewed | b893366 | XSS probe passed; held/blocked share amber per wireframe (design note for client) |
| 6 Dashboard | ✅ done+reviewed | f75c8b9 | content region mechanically diffed vs wireframe; chunk 1 complete |
| 7 Fortify backend | ✅ done+reviewed | 80e7d05+7f991f6 | closed 2 enumeration oracles (stock Fortify leaks); route takeover; throttles on all reset endpoints |
| 8 Auth pages | ✅ done+reviewed | 7b530dd | 81 tests; readonly-UX nit → Task 13 |
| 9 Roles/permissions seed | ✅ done+reviewed | e3d1485 | matrix exact; Gate::before guest-safe; throw-path test → Task 10 |
| 10 Auth activity log | ✅ done+reviewed | a17b824 | mutation-tested; causedByAnonymous trap verified in vendor; chunk 2 complete |
| 11 Form components | ✅ done+reviewed | 7c9447c+5138802 | 63 comp tests; resurrection bug fixed on checkbox+multiselect; API swap-ready for Task 23 |
| 12 Datatable/modal components | ✅ done+reviewed | 8bf0496 | modal matches inquiry.html class-for-class; module pages will follow wireframe filters-card-above layout |
| 13 jsvalidation + auth retrofit | ✅ done+reviewed | f738a72+083ee18 | Password-rule-object AJAX trap found+avoided; regex mirror proven equivalent; headless-Chrome verified; chunk 3 complete |
| 14 Inventory schema | ✅ done+reviewed | bcff95e | 5 tables live-probed; enums verbatim from wireframe; pincode required@form (Task 17); relation name is `wings` |
| 15 Policies | ✅ done+reviewed | e8a88dd | all 12 permission strings verified vs seeder; wing/floor grid must gate on projects.view (Task 18 note) |
| 16 UnitPriceCalculator | ✅ done+reviewed(Fable direct) | 18524a2 | 200k-input bcmath sweep, 0 drift; JS mirror path in docblock → fix in Task 20 |
| 17 Projects CRUD | ✅ done+reviewed | fbafdb7+93c026b+e3aa1c5+519e08c | reference module; search-tamper 500 fixed+documented; DataTables 2.3.8 vendored (browser-verified); retry helper shared |
| 18 Wings/floor plans | ✅ done+reviewed | 205fcb9+3492ec1 | stored JS injection in confirm() found+fixed (Js::from); validation matches DB uniques; per-form error bags; floors_count is a free label (product sign-off later) |
| 19 Unit types CRUD | ✅ done+reviewed | c665f0a | template copied cleanly; 15 security probes passed; 404→405 doc fix + carpet↔super gap ride into Task 20 |
| 20 Units CRUD + cascade | ✅ done+reviewed | 6fa810a | price authority structural; 14/14 adversarial probes; JS parity 1.2M-input sweep; 4 wireframe controls schema-less (client note) |
| 21 Unit inventory page | ✅ done+reviewed | 36cfafe+15c0468 | 5 filters + cascade wing; strict id parsing backported to unit types; 767 tests |
| 22 Seeds/smoke/docs | ✅ done+reviewed | 0b3fa57+7906241+7e9275c+6a3db3d | smoke reproducible; docs audit 45/45 paths valid; select2 placeholder fix needed jQuery data-cache handling |
| 23 Variant repo (spatie/html) | in progress | | |

## Deploy checklist notes

The full, maintained list now lives in the README's "Deploy checklist" section.
Kept here as the running log of what each task discovered:

- Queue the password-reset notification in production (sync SMTP >200ms defeats the Timebox anti-timing guard on /forgot-password)
- If deployed behind a proxy/CDN, configure TrustProxies — the IP-keyed forgot/reset rate limiters otherwise collapse into one shared bucket
- Confirm the unit price formula with the client before production (spec §7 default)
- APP_DEBUG=false, or a DataTables SQL error leaks the query; set DATATABLES_ERROR to a fixed string so yajra stops echoing exception messages into the JSON
- SEED_ADMIN_PASSWORD required outside local/testing; decide whether DemoProjectSeeder runs at all on a live tenant
- `php artisan event:cache` in the deploy — LogAuthenticationActivity is discovered by reflection on every request otherwise

## v12 projects rebuild

Plan: 2026-08-24-v12-projects-spatie.md · Branch: `feat/v12-projects` (this repo only;
`~/nrems-app` is frozen). Same loop: implementer → spec review → quality review.

Rows below are written by the task that finished them; a task not listed here has
not reported in this file.

| Task | Status | Commit | Notes |
|---|---|---|---|
| D3 Browser evidence + headless flows | ✅ done | 1a17f04 + 0701adc + (this commit) | Upload size guard built from `ProjectFileSlot::maxKilobytes()` and verified live on the 2 MB logo and 10 MB brochure slots; CSV import driven end to end in a real browser (parse → re-upload → submit → units in the DB → rejected submit re-renders read-only rows); 390/768/1280 shots of create/edit/show/units-index; boilerplate smoke re-run green on the v12 build. Evidence + the deliberate upload-zone copy divergence: `docs/superpowers/evidence/responsive-v12/README.md`. |

### What D3 found

- **A 500 on the live create path** (`0701adc`): a blank Total Units posted as
  `''`, the nullable rule made it null, and `total_units` is `NOT NULL DEFAULT 0`
  — a default that only applies when the INSERT omits the column, which the form
  never does. Every test omitted the key, the one shape that worked. Two
  regression tests now post what a browser posts.
- The dev database's seeded admin password does not match `.env`'s
  `SEED_ADMIN_PASSWORD`; the browser runs pass `SEED_ADMIN_PASSWORD=password`.
  Worth reconciling before D4 runs `migrate:fresh --seed`.
- `tools/` now has three files: `harness.mjs` (server boot, login, overflow
  measurement, failure registry — shared), `responsive-smoke.mjs` (the
  boilerplate walk, unchanged in what it asserts) and `v12-evidence.mjs`.

### Task D4 — acceptance, 2026-08-25

Swept on branch `feat/v12-projects` at `8abcb0f`, against a database rebuilt
from nothing. The sweep itself changed no application code — it read, ran and
recorded — and found one gap, which the orchestrator closed in `11d12ae` with a
single test. §9.4 is not mine: it is the separate security review, and the merge
waits on it.

**Environment reconcile (D3's open item).** `.env` named a
`SEED_ADMIN_PASSWORD` the seeded administrator did not actually have, so the
browser runs had to pass one on the command line. `.env` now reads
`SEED_ADMIN_PASSWORD=password` — the same value `AdminUserSeeder` falls back to
in `local`/`testing`, so the file and the seeder can no longer disagree here.
After the reseed below, `Auth::attempt()` with `env('SEED_ADMIN_PASSWORD')`
returns true for `admin@nrems.local` (Super Admin). `.env` is untracked, so this
is a note, not a diff; `.env.example` still ships the key blank and the README
still tells a deployer to set a real secret.

**Fresh database.** `php artisan migrate:fresh --seed` on `nrems_html`: 13
migrations clean, including `2026_08_24_100000_realign_projects_and_units_to_v12`,
then RolePermissionSeeder / AdminUserSeeder / DemoProjectSeeder. D3's evidence
rows are gone, as expected (0 projects named "Evidence Import …" remain; 2 demo
projects, 32 units).

**`composer setup` — what was actually verified.** Not a clean-clone run, and
the MySQL user in `.env` has no `CREATE DATABASE` grant, so the throwaway second
database was not possible; the only spare databases on this box belong to the
frozen `~/nrems-app` and are not mine to overwrite. What was verified instead,
step by step:

| Setup step | How it was checked |
|---|---|
| `file_exists('.env') \|\| copy('.env.example', '.env')` | By inspection plus a key-set diff: `.env` and `.env.example` carry an identical set of keys, so a fresh copy leaves nothing unset that the app reads. |
| `php artisan key:generate` | **Not run** — it would rotate this install's `APP_KEY` and invalidate every session and encrypted value. Exercised as `key:generate --show`, which runs the same generator and prints a candidate without writing. |
| `php artisan migrate --seed` | Run literally, on an empty schema: `php artisan db:wipe` followed by `php artisan migrate --seed`. Clean, and the admin logs in afterwards. |

That left the `.env` copy proven by key-set equality rather than execution, and
`key:generate`'s write path untouched. **The orchestrator then closed both**: a
throwaway clone against a throwaway database `nrems_setup_proof` ran the script
end to end — `.env` copied from `.env.example`, a real `key:generate` write, a
clean `migrate --seed`, and the seeded administrator logging in with the `.env`
password against 2 projects / 32 units. Both throwaways were deleted afterwards.
`composer setup` is therefore proven from scratch, step for step.

The orchestrator also dropped and recreated `nrems_html_test`, whose schema the
killed concurrent runs had left mid-migration.

**Gates.** Two solo runs stand as the evidence, either side of the one test
added during acceptance:

| Gate | Output | Run |
|---|---|---|
| `composer test` | `{"tool":"pest","result":"passed","tests":1097,"passed":1097,"assertions":3839,"duration_ms":140216}` | `b3c7126` — the §9 sweep, on the fresh database |
| `composer test` | 1098 passed, 3842 assertions | `11d12ae` — after the ₹0-render test closed the §9.1 gap |
| `composer analyse` | `{"tool":"phpstan","result":"passed","errors":0}` | both |
| `composer lint` | `{"tool":"pint","result":"passed"}` | both |

**Two red runs during D4 were concurrency artefacts and are discarded.** The
first collided with the stalled D3 agent replaying gates it had been told to
skip; the second with the orchestrator's own filtered runs while it wrote the
₹0 test. Both showed the same signature — MySQL 1213 deadlocks, 1412 "table
definition has changed", 1054 "Unknown column 'promoter_name'" (the migration
suite's rollback DDL seen mid-flight on another connection), 1050/1146 on the
`migrations` table — and neither says anything about the code. The standing
evidence is the two runs above, each watched to a single `pest` process for its
whole duration. **Never run this suite concurrently with another** — the
migration suite's DDL and `RefreshDatabase`'s transactions cannot share a
database, and this project has now hit that failure mode three times.

**Spec §9, criterion by criterion**

| § | Criterion | Verdict | Evidence |
|---|---|---|---|
| 9.1 | Tests for every behaviour in §§3–8 | ✅ met | Uploads incl. adversarial: `tests/Feature/Projects/ProjectFileTest.php` (31 tests — real-type extension, wrong-MIME rejection, replace-deletes-old, transaction rollback, hostile filename disposition, unicode fallback, guest/permission/slot/soft-deleted 404s). CSV: `ProjectUnitImportTest.php` incl. "rejects more than 500 rows"; old-input re-render: `ProjectUnitImportViewTest.php` incl. "brings file rows back read-only, badged and marked" and "does not trust the from_file marker with anything". Twin parity: `tests/Feature/TwinViewParityTest.php`. Components: 11 `x-form.file` + 12 `x-form.radio` tests in `tests/Feature/Components/FormComponentsTest.php`. Project-scoped uniqueness incl. soft-deleted: `ProjectUnitImportTest.php` "rejects a number a deleted unit still reserves" + `V12SchemaTest.php`. Reversals rewritten as the spec ordered: `InventorySchemaTest.php:260` and `UnitCrudTest.php:625` now assert rejection. `environmental_clearance` round-trip: `ProjectCrudTest.php:1414`. Migration conversions: `tests/Migration/V12MigrationConversionTest.php` (renames, ÷9, null stays null, cross-wing-duplicate abort). Unassigned: `ProjectCrudTest.php:1641/1713`, `UnitCrudTest.php:1014`, `UnitInventoryTest.php:254`. The ₹0 display was the one gap this sweep found — asserted at the model level and visible in `project-show-after-import-1280.png`, but never as rendered text — and it was closed during acceptance by `ProjectUnitImportTest.php:386` "shows an imported unit at ₹0 until it is priced", which loads the show page and asserts `₹0` is on it (commit `11d12ae`, green in the 1098-test run above). |
| 9.2 | Larastan level 6 + Pint clean | ✅ met | Both gates above; `Unit::$project_wing_id`/`$floor_plan_id` are `?int` and level 6 passes with them. |
| 9.3 | Browser evidence at 390/768/1280 + headless CSV flow | ✅ met (D3's, re-verified as artefacts) | 23 PNGs + `README.md` in `docs/superpowers/evidence/responsive-v12/`. Create/edit/show and units-index each at all three widths; inventory rows, row errors, upload zones ×2 at all three widths, the over-cap guard, post-import show. The README records the CSV flow end to end and the guard numbers. Browsers were not re-run for D4. |
| 9.4 | Security review | ⏳ delegated, pending | Separate reviewer, per the plan's D4 bullet. Includes the `filesystems.php` `'serve' => true` question from B1's spec review. The merge to main waits on it. |
| 9.5 | Seeds/factories updated · `composer setup` from scratch · greps empty · per-wing prose purged | ✅ met | `ProjectFactory` supplies `promoter_name`, `total_land_area`, `towers_count`, `floors_per_tower`, `rera_no`; the demo seed runs clean and the migration's cross-wing-duplicate precondition passed on it. Greps: `developer_name`/`total_plot_area`/`expected_possession` survive only in migration files (`create_projects_table`, the v12 realignment), in the two tests whose job is the old names (`V12MigrationConversionTest` inserts them, `V12SchemaTest` asserts their absence), and in prose (README/spec/plan) — no application code. `unit_no` as a whole word: zero hits outside the spec and plan sentences that quote the grep. Per-wing prose: `StoreUnitRequest` now explains the move, `UnitController::destroy:158` reads "reserved across the whole project", `UnitFactory:20` names the `(project_id, unit_number)` index, the units migration note points at the realignment migration, and `StoreProjectRequest`'s city comment is rewritten. Remaining "per wing" hits are about wings and floor plans, which are still wing-scoped, and are correct. |
| 9.6 | Docs — adding-a-module, README deploy notes | ✅ met | `docs/adding-a-module.md` carries the twin-file rule, the v12 wireframes as markup source, `x-form.file`/`x-form.radio`/`x-form.open`, and a "CSV import, when a form takes a file of rows" section. README's deploy checklist covers `storage/app/private/projects/{id}/` and its backup, `upload_max_filesize`/`post_max_size` ≥ 12M with `client_max_body_size`, the `post_max_size` discard behaviour, `APP_DEBUG=false`, `DATATABLES_ERROR` and `SEED_ADMIN_PASSWORD`. |

**Not merged.** `feat/v12-projects` stays unmerged; the orchestrator merges after
the security review.
