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
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ This file explains repo-wide conventions and where to find scoped rules.
- Lint/format: `composer cs-check` / `composer cs-fix`
- Tests: `composer test`

The CI "Lint & Static Analysis" gate runs FOUR static tools, not two: phpstan, phpat (`bin/phpstan analyze -c config/quality/phpat.neon`), php-cs-fixer, and rector (`bin/rector process src --config=config/quality/rector.php --dry-run`). Run all four on changed PHP before pushing — rector is the one that gets forgotten. Tools live in `bin/` (composer bin-dir), not `vendor/bin`. When a change touches a base/parent method signature, run FULL-tree phpstan (`bin/phpstan analyze --no-progress`), not file-scoped — anonymous-class overrides in `tests/` break invisibly otherwise.

The captainhook pre-commit hook runs the unit suite on the HOST php. A host without `pdo_mysql` fails with "could not find driver" — never bypass with `--no-verify`; either run the suite in the container (`docker compose --profile dev exec -T -e APP_ENV=test -e DATABASE_URL=mysql://unittest:unittest@db_unittest:3306/unittest app-dev bin/phpunit …`) or point `DATABASE_URL` at the unittest DB via a real env var. If phpat aborts with a Nette `ContainerLoader … .lock` error after a container run, it is a uid-split on `var/phpstan-phpat/` — remove that cache dir, it is not an architecture violation. Container and host runs regenerate `config/reference.php`; restore it (`git checkout -- config/reference.php`) before committing, never stage it.

## Releases

No workflow creates releases. `docker-publish.yml` builds images on tag push; `slsa-provenance.yml` runs on `release: published`; treat the release as immutable once its provenance asset is uploaded (the tag/assets are locked and the provenance attests the published state — do not rely on editing anything after publish), so get the notes right before publishing. Order: verify main CI green → `git tag -s vX.Y.Z -m "vX.Y.Z"` → `git push origin vX.Y.Z` (Docker Publish runs) → `gh release create vX.Y.Z --title "vX.Y.Z" --notes-file <notes>` (SLSA runs). The metadata-action strips the leading `v`: images are `:6.0.0`/`:6.0`/`:6`. Credit contributors inline per change with `@mentions` (GitHub builds the Contributors row from body mentions); auto-generated notes miss direct pushes — cross-check via the compare API. Credit the human driving an agent-authored commit (committer / `Co-authored-by`), never a bot author.

## Index of scoped AGENTS.md

| Path | Purpose |
Expand Down
8 changes: 8 additions & 0 deletions frontend/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,11 @@ See [`README.md`](README.md) for the full stack description.
disk icon forces a save and row-leave shows the full error). Relation cells
use `ChipSelect` (`src/lib/chipSelect.tsx`, an Ark Combobox) body-portalled
(whitelist `data-chipselect-popup`) to escape the table scroll container

## Popups inside a modal dialog

A body-portalled combobox/popover popup is a SIBLING of an Ark Dialog's portal, so the dialog's modal focus trap makes it `inert` — it looks dead. Render the Positioner INLINE (no `<Portal>`) with `positioning: {strategy: 'fixed'}`: it stays inside the dialog's DOM subtree (the trap only inerts siblings), and `fixed` still escapes `overflow` clipping. Portalling *into* the dialog breaks `openOnClick` (the combobox machine initialises without a present Positioner). The body-portal guidance elsewhere in this file applies OUTSIDE modals.

## MCP-playwright clicks do not focus inputs here

In an `mcp__playwright__*` session, clicking any input leaves `document.activeElement` on `#main-content` (the SPA autofocus wins the race), so `openOnClick` and option-click commits silently fail. This is a session artifact, not a component bug — the real e2e runner clicks fine. Verify comboboxes in MCP by TYPING (`fill`/`pressSequentially`), and trust the e2e suite for click flows; never "fix" the component off a failing MCP click without checking a known-green reference.
11 changes: 11 additions & 0 deletions src/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,14 @@ public function list(EntityManagerInterface $em): JsonResponse
Migrating deploys: back up first (`mariadb-dump` via the URL user — root has
no pw) and pre-flight each pending migration's columns. For local testing use
the dev compose stack (`COMPOSE_PROFILES=dev`, port 8765, `APP_ENV=dev`)

## Index direction vs. MIN/MAX loose scan

MariaDB 10.11 cannot use a loose scan (`Using index for group-by`) for `MIN()`/`MAX()` over a `DESC` key part — a `(col, day DESC)` index turns `SELECT col, MAX(day) … GROUP BY col` into a full index scan (~240k rows, ~150 ms), while plain ASC `(col, day)` loose-scans in ~1 ms AND still serves `WHERE col=X ORDER BY day DESC` without filesort (backward scan). That is why `Version20260704_LastActivityIndexesAscLooseScan` recreated the three `idx_entries_*_day` indexes ASC. Before adding a `DESC` key part, check whether an aggregate reads the index; verify with a real `EXPLAIN`, not from docs or tickets.

## Settings/2FA endpoint conventions

- Write actions (SaveSettings, ChangePassword): `{"success": true}` / `{"success": false, "message": <translated>}`; HTTP 422 on validation failure, 403 on auth-source refusal.
- 2FA state-changing actions (ConfirmTotpEnrollment, DisableTwoFactor): `{"enabled": true|false, …}` — the toggle state, not generic success. StartTotpEnrollment is the exception: it returns the enrollment material (`provisioningUri`, `secret`), no `enabled` field.
- Errors translate via `$this->translate()` (`messages` domain); DTO violations via the `validators` domain. Both need de/es/fr/ru entries — `debug:translation` does NOT detect `translate()`-wrapped strings.
- Admin `/user/save` `authSource` is TRI-STATE (`?string`, ADR-018 D1): `null` (legacy client) = no source change and never clears an existing local hash; `'ldap'` clears the hash; `'local'` sets/keeps a password (new local account without password → 422). Probe "has a local password" with `User::isLocalAccount()`, never `getPassword() === null` — `getPassword()` synthesises an LDAP signature and never returns null.
12 changes: 12 additions & 0 deletions tests/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,15 @@ public function testSyncWithJiraHandlesApiError(): void
so a validator's `find($loggedInUserId)` returns the CACHED entity — a raw
`UPDATE users …` is invisible to it. Unit-test such validators (mock `find`)
or operate on a non-logged-in entity

## `.env.test.local` hijacks PHPUnit's database

`make e2e-up` writes `.env.test.local` whose `DATABASE_URL` points at the dev `db` — Symfony dotenv loads it LAST for any `APP_ENV=test` process, so `bin/phpunit` silently runs against the (often migration-behind) dev DB and fails with `SQLSTATE 1054 Unknown column` on recently added columns. Do NOT delete the file (the e2e stack needs it); override with a REAL env var instead, which dotenv never overrides:

```bash
docker compose --profile dev exec -T -e APP_ENV=test \
-e 'DATABASE_URL=mysql://unittest:unittest@db_unittest:3306/unittest?serverVersion=mariadb-12.1.2&charset=utf8mb4' \
app-dev bin/phpunit tests/...
```

`db_unittest` seeds `sql/unittest/00{1,2}_*.sql` only on FIRST volume init; `001_testtables.sql` is a gitignored artifact regenerated from `sql/full.sql`. A stale schema means: recreate the volume.