2026-03-13 12:05:34 +01:00
# CLAUDE.md — CoreCore
2026-02-24 08:49:40 +01:00
Multi-tenant admin application built on MintyPHP. Manages tenants, users, departments, roles, permissions, address books, mail, CSV imports, scheduled jobs, and Microsoft Entra ID SSO.
2026-03-22 22:56:24 +01:00
## Mandatory Workflow
All guard IDs (GR-\*) and gate IDs (QG-\*) from `.agents/checks/` are **binding — not advisory ** .
2026-04-01 19:41:56 +02:00
- **Full workflow trigger is risk-based** and defined in `.agents/checks/enforcement-policy.json` (`full_workflow_required_when` ). Matching changes MUST follow `.agents/workflow.md` (Analyst → Planner → Executor → Reviewers → Finalizer). Read `.agents/README.md` and the relevant role prompt in `.agents/prompts/` before starting.
- **Quick fixes** may skip full workflow only if they match `.agents/checks/enforcement-policy.json` (`quick_fix_allowlist` ) and still pass required gates.
2026-04-13 20:50:24 +02:00
- **New modules** MUST conform to `.agents/contracts/module-manifest.schema.json` and pass all 10 security guards (GR-SEC-001 through GR-SEC-010).
2026-03-22 22:56:24 +01:00
2026-04-01 19:41:56 +02:00
Full workflow definition: `.agents/workflow.md` | Guard catalog: `.agents/checks/guard-catalog.json` | Enforcement policy: `.agents/checks/enforcement-policy.json` | Guard enforcement map: `.agents/checks/guard-enforcement-map.json` | Skills: `.agents/skills/`
2026-03-19 19:04:28 +01:00
2026-02-24 08:49:40 +01:00
## Tech Stack
- **Runtime:** PHP 8.5 (FPM) on MintyPHP framework (`mintyphp/core` )
- **Web server:** Nginx 1.25 (Alpine)
- **Database:** MariaDB 11.4
- **Cache:** Memcached 1.6
- **Frontend:** Vanilla JS (ES2022 modules), vanilla CSS with `@layer` system — no build step
- **Key PHP libs:** PHPMailer 7, Dompdf 3, endroid/qr-code 6, league/commonmark 2
- **Containerized:** Docker Compose (dev + prod variants)
## Quick Commands
```bash
# Start dev environment
2026-04-02 10:49:35 +02:00
cp config/config.php.example config/config.php
2026-02-24 08:49:40 +01:00
docker compose up --build -d
# App: http://localhost:8080 | phpMyAdmin: http://localhost:8081
2026-03-04 15:56:58 +01:00
# Run PHPUnit tests (phpunit.xml configures bootstrap + testsuite)
docker compose exec php vendor/bin/phpunit
2026-02-24 08:49:40 +01:00
# Run PHPStan (level 5)
docker compose exec php vendor/bin/phpstan analyse -c phpstan.neon --no-progress
2026-03-19 19:04:28 +01:00
# CLI commands (single entry point)
docker compose exec php php bin/console list
docker compose exec php php bin/console module:sync # full module runtime sync
docker compose exec php php bin/console module:migrate # apply module SQL migrations
docker compose exec php php bin/console module:permissions-sync # sync + deactivate orphaned
docker compose exec php php bin/console module:build # build runtime page root
docker compose exec php php bin/console module:assets-sync # publish module web assets
docker compose exec php php bin/console module:deactivate <id> --confirm
docker compose exec php php bin/console scheduler:run
docker compose exec php php bin/console doctor
2026-03-18 22:58:15 +01:00
2026-02-24 08:49:40 +01:00
# Check unused Composer packages
docker compose exec php vendor/bin/composer-unused
```
## Project Structure
```
2026-04-13 23:20:05 +02:00
core/ # All backend PHP (namespace MintyPHP\)
2026-03-04 15:56:58 +01:00
App/ # DI container + bootstrap (AppContainer, Container/Registrars/)
2026-03-18 22:58:15 +01:00
Module/ # Module platform (Registry, Manifest, Autoloader, Builder)
2026-02-24 08:49:40 +01:00
Repository/ # SQL queries, prepared statements, filters/paging
Service/ # Business logic, validation, orchestration
Http/ # Auth guards, API auth, request helpers
2026-03-04 15:56:58 +01:00
Input/ # Request input handling (RequestInput, FormErrors)
2026-03-19 19:04:28 +01:00
Console/ # CLI command framework (ConsoleApplication, Command base class)
Commands/ # Command classes (Module/, Scheduler/, Tools/)
2026-02-24 08:49:40 +01:00
Support/ # Crypto, flash, guard, search, helpers
2026-03-18 22:58:15 +01:00
modules/ # Self-contained feature modules (namespace MintyPHP\Module\<Name>\)
<id>/module.php # Module manifest (routes, slots, providers, permissions)
2026-04-13 23:20:05 +02:00
<id>/lib/ # Module PHP classes (Service, Repository, Providers)
2026-03-18 22:58:15 +01:00
<id>/pages/ # Module actions + views (symlinked into runtime)
<id>/templates/ # Module-specific templates
<id>/web/ # Module CSS/JS (published to web/modules/<id>/)
<id>/tests/ # Module PHPUnit tests
pages/ # Core actions (controllers) — file-based routing
2026-02-24 08:49:40 +01:00
pages/**/*.php # Action files (read input, check permissions, call services)
pages/**/*.phtml # View files (rendering only, no DB calls)
2026-03-18 22:58:15 +01:00
templates/ # Core layouts and partials
2026-02-24 08:49:40 +01:00
partials/ # Reusable UI components
emails/ # Email HTML templates
pdfs/ # PDF templates (Dompdf)
2026-04-01 20:27:42 +02:00
config/ # App config (routes, assets, themes, modules)
2026-02-24 08:49:40 +01:00
web/ # Document root (entry point, CSS, JS, static assets)
2026-03-18 22:58:15 +01:00
js/core/ # DOM utilities, Grid.js factory, component runtime
2026-02-24 08:49:40 +01:00
js/components/ # Reusable UI components
css/ # Layered CSS (base → components → layout → pages)
2026-03-18 22:58:15 +01:00
modules/ # Published module assets (symlinks to modules/<id>/web/)
storage/ # Uploaded files + runtime state
runtime/pages/ # Symlinked page tree (core + modules, built by module-build)
2026-04-01 20:27:42 +02:00
runtime/settings.php # Hot-path settings cache file (generated by SettingCacheService)
2026-02-24 08:49:40 +01:00
db/init/init.sql # Full schema + seed data (no migration framework)
2026-03-04 15:56:58 +01:00
db/updates/ # Idempotent SQL update scripts for existing installs
2026-02-24 08:49:40 +01:00
tests/ # PHPUnit tests (namespace MintyPHP\Tests\)
docs/ # German-language documentation (Markdown)
i18n/ # Translation files (de, en)
2026-04-01 17:14:20 +02:00
bin/ # CLI entry points (bin/console, bin/dev) + shell tools
2026-02-24 08:49:40 +01:00
docker/ # Dockerfiles, Nginx configs, PHP configs
2026-03-19 19:04:28 +01:00
.agents/ # Agent workflow system (contracts, prompts, checks, skills, runs)
2026-02-24 08:49:40 +01:00
```
## Architecture Rules
### Strict Layering (enforce in all changes)
2026-04-13 23:20:05 +02:00
1. **App ** (`core/App/` ): DI container bootstrap. Registers all service/repository factories. No business logic.
2. **Repository ** (`core/Repository/` ): All SQL lives here. No HTTP, session, or rendering.
3. **Service ** (`core/Service/` ): Business rules and validation. No HTML. Four internal sub-types:
2026-03-05 11:17:42 +01:00
- **Service** (`*Service.php` ): Business logic + orchestration. Coordinates repositories and gateways.
- **Gateway** (`*Gateway.php` ): Adapter for a single external dependency (repository, settings, HTTP API, scope). No orchestration flow.
2026-04-13 23:20:05 +02:00
- **Factory** (`*Factory.php` ): Only place inside `core/Service/` that may call `new` on services/gateways/repositories. Registers via DI container.
- **Policy** (`*Policy.php` in `core/Service/Access/` ): Authorization decisions per resource type. Returns `AuthorizationDecision` . No HTTP, no business logic.
2026-03-04 15:56:58 +01:00
4. **Action ** (`pages/**/*.php` ): Reads input, checks permissions + tenant scope, calls services, sets view vars.
5. **View ** (`pages/**/*.phtml` ): Pure rendering. **No DB calls. ** HTML escaping via `e(...)` .
6. **Template ** (`templates/` ): Layouts and partials.
2026-02-24 08:49:40 +01:00
2026-03-18 22:58:15 +01:00
### Modules
- Modules are self-contained feature packages under `modules/<id>/`
- **Namespace:** `MintyPHP\Module\<Name>\*` — never use core namespaces (`MintyPHP\Service\*` , `MintyPHP\Repository\*` )
- **Manifest:** `module.php` declares routes, UI slots, providers, permissions, scheduler jobs
- **UI integration:** Only via platform slots (`aside.tab_panel` , `topbar.right_item` , `layout.head_style` , `runtime.component` , etc.) — no core template hardcoding
- **Session keys:** Prefixed with `module.<id>.*`
- **Activation:** `config/modules.php` or `APP_ENABLED_MODULES` env override
2026-04-01 17:14:20 +02:00
- **Runtime sync:** `php bin/console module:sync` after any module/manifest change (builds page symlinks, syncs permissions, publishes assets)
2026-03-18 22:58:15 +01:00
- **Fingerprint guard:** `web/index.php` validates runtime state matches active modules; fails fast if stale
- **API isolation:** API requests (`api/` prefix) skip session/auth/cookie flows entirely; modules using API endpoints rely on `ApiBootstrap.php`
2026-03-22 22:56:24 +01:00
- **Cross-module coupling:** Modules may depend on other modules but MUST declare `requires: [<module-id>]` in their manifest. Undeclared cross-module imports are forbidden. Prefer loose coupling via events and providers over direct service injection
- **Module DB changes:** Via `module:migrate` , not `db/updates/` (which is for core schema only)
- **Module permissions:** Registered in manifest, synced via `module:permissions-sync` — never hardcoded
- **Manifest contract:** Must conform to `.agents/contracts/module-manifest.schema.json`
2026-03-18 22:58:15 +01:00
2026-02-24 08:49:40 +01:00
### Routing
- File-based: `pages/admin/users/edit($id).php` → URL parameter `id`
2026-03-18 22:58:15 +01:00
- Module pages: `modules/<id>/pages/` symlinked into `storage/runtime/pages/` at build time
2026-02-24 08:49:40 +01:00
- `...(none).phtml` → no template layout
- `data().php` suffix → data/AJAX endpoints
- Named aliases in `config/routes.php` with `public` flag for auth guard
2026-03-18 22:58:15 +01:00
- Module routes declared in manifest, merged at boot (fail-fast on collision)
2026-02-24 08:49:40 +01:00
### PHP Conventions
2026-04-13 23:20:05 +02:00
- PSR-4 autoload: `MintyPHP\` → `core/` , `MintyPHP\Module\*` → `modules/*/lib/` , `MintyPHP\Tests\` → `tests/`
2026-03-22 22:56:24 +01:00
- All user-facing text uses `t('key')` translation helper — keys must exist in all language files under `i18n/`
- Request input via `requestInput()` — never raw `$_GET` /`$_POST` /`$_SESSION` (GR-CORE-003)
2026-02-24 08:49:40 +01:00
- Permissions + tenant scope enforced in actions/services, never just in UI
2026-04-01 20:27:42 +02:00
- Security/integration settings stay in DB, not in `storage/runtime/settings.php` file cache (GR-CORE-011)
2026-03-22 22:56:24 +01:00
- POST-Redirect-GET (PRG) pattern on form submissions; flash before redirect (GR-CORE-012)
- Constructor injection for dependencies; no hidden global state (GR-TEST-002)
- New/changed Service or Gateway must have PHPUnit tests — happy path + edge case (GR-TEST-001)
2026-02-24 08:49:40 +01:00
### Frontend Conventions
- Vanilla ES2022 modules, no bundler
- CSS classes prefixed with `app-`
- Defensive DOM checks (elements may not exist on every page)
- No business logic in frontend
- Assets served raw with `assetVersion()` cache-busting (filemtime-based)
### UI Patterns
- Edit pages: tabs → `Master data` | `Visibility` | optional `Danger zone`
2026-04-15 18:44:29 +02:00
- **Tab panel width**: `data-tab-panel` defaults to `max-width: 70ch` (optimal for form readability). Add `data-tab-panel-wide` for panels that need full width (e.g. split layouts, side-by-side editors, preview panels, tables). Use 70ch for standard form fields (text inputs, selects, textareas); use wide for multi-column layouts or content that benefits from horizontal space.
2026-02-24 08:49:40 +01:00
- Titlebar partial for primary actions (Save, Save & close)
- Secondary actions in Aside block via `app-details-aside-actions.phtml`
2026-03-22 22:56:24 +01:00
- Lists use Grid.js via `initStandardListPage()` (GR-UI-LIST)
- Detail pages use shared partials and layout contracts (GR-UI-DETAIL)
- New views must handle loading, empty, error, and success states (GR-UI-014)
2026-02-24 08:49:40 +01:00
- All visible text wrapped in `t('...')`
2026-03-22 22:56:24 +01:00
- Check existing JS/CSS components before adding new ones (GR-UI-REUSE)
2026-04-15 18:44:29 +02:00
- **Buttons**: Always use core classes (`secondary outline small` , `danger outline small` ) — custom CSS only for shape/position. Icon-only buttons must have `data-tooltip` + `aria-label` .
- **Components inside forms**: Reset core `margin-bottom` on buttons/inputs/selects/labels at component scope. Checkboxes in flex need explicit `1.25em` sizing + `flex-shrink: 0` .
- **Sticky elements**: Always use `top: calc(var(--app-topbar-height) + ...)` — never `top: 0` . Multi-column grids must have a responsive single-column `@media` fallback.
2026-04-06 11:58:14 +02:00
- **File uploads** use `app-file-upload.phtml` partial (deliberate exception to plain-HTML inputs — the markup is too complex for inline use). See `templates/partials/app-file-upload.phtml` for the `$fileUpload` array contract. Never write file-upload markup inline; always use the partial.
docs(agents): encode list-export convention as GR-UI-EXPORT guard
Make the core-primitive-end-to-end rule for Grid.js list exports binding
instead of advisory — so future consumers (and reviewers) cannot quietly
drift back into inline fputcsv / hand-rolled headers / bespoke click
listeners / lurl() for query-carrying URLs.
- CLAUDE.md: new UI Patterns bullet + two "Never Do This" entries
spelling out the server/view/client contract in one place.
- .agents/checks/guard-catalog.json: new GR-UI-EXPORT entry describing
the end-to-end requirement (exportRequireGetRequest + exportCapLimit +
exportSendCsv + ExportColumn + shared filter-schema + dropdown partial
+ endpointUrl() + initListExport).
- .agents/checks/guard-enforcement-map.json: wire the new guard to
tests/Architecture/ListExportContractTest.php as the automated
evidence source.
- .agents/checks/guard-checklist.md & .agents/prompts/reviewer-code.md:
add GR-UI-EXPORT so the Code Reviewer prompt and the checklist flag
violations alongside GR-UI-LIST.
- tests/Architecture/ListExportContractFiles: registry of every list
export (currently helpdesk-domains, admin/users, audit/system-audit).
Adding a new list = one entry, contract test covers the rest.
- tests/Architecture/ListExportContractTest: six checks per registered
entry — endpoint uses the primitive, no hand-rolled output, data and
export share the same filter-schema, template includes the dropdown
partial and uses endpointUrl(), page module imports and invokes
initListExport, and the shared dropdown partial stays zero-config.
- pages/admin/users/export().php: align with the new contract by
switching from ad-hoc requestInput()->queryAll() reads to
gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'), same
as the data endpoint — eliminates the last place Grid and export
could disagree on what "the current filter" means.
Gates: PHPUnit 1900 OK (+6 contract checks), PHPStan 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:50:43 +02:00
- **List exports**: every Grid.js list that offers a download MUST go through the core primitive — no inline `fputcsv` , no hand-rolled headers, no bespoke dropdown. (GR-UI-EXPORT)
- **Server:** endpoint calls `exportRequireGetRequest()` + `exportCapLimit()` + `exportSendCsv()` from `core/Support/helpers/export.php` , builds columns as `ExportColumn[]` (`core/Service/Export/CsvExportService.php` ), and reuses the SAME `filter-schema.php` via `gridParseFiltersFromSchemaFile()` as the data endpoint. Extract a shared service or presenter if row-formatting is non-trivial (see `DomainListService` / `SystemAuditRowPresenter` ).
- **View:** `require templatePath('partials/app-list-export-dropdown.phtml');` inside `$listTitleActionsHtml` . Pass the URL via `'exportUrl' => endpointUrl('<route-path>')` in the page config — **always `endpointUrl()`, never plain `lurl()` ** . The helper resolves module routes to their target path so query strings survive.
- **Client:** `initListExport({ gridConfig, exportUrl })` from `/js/core/app-list-export.js` . No hand-rolled click listener, no inline URL construction.
2026-04-22 15:22:26 +02:00
- **Detail drawer**: Row-level detail views that slide in from the right use the core primitive — no per-module drawer implementation.
- **Server:** Action `<route>/<name>-fragment($id).php` + view `<route>/<name>-fragment(none).phtml` . The `(none)` template renders without layout/topbar/breadcrumbs. Reuse the same partial that the full-page view uses (extract a `modules/<id>/templates/<name>-profile.phtml` if needed). Must enforce auth + scope exactly like the full-page view.
- **Route:** Declare `['path' => '<route>/<name>-fragment', 'target' => '<route>/<name>-fragment']` in the module manifest.
- **Client:** `initDetailDrawer({ gridConfig, fetchUrl: (uuid) => new URL('<route>/<name>-fragment/' + uuid, appBase).toString(), fullUrl, hashPrefix, rowProvider? })` from `/js/components/app-detail-drawer.js` . The drawer handles focus-trap, body-scroll-lock, session-expiry reload, and auto-initializes tabs/tooltips/fslightbox in the fragment via `initFragmentContent()` . Consumers do NOT need to call `initTabs` , `refreshFsLightbox` , etc. manually.
- **Architecture test** `DetailDrawerFragmentContractTest` enforces that every `initDetailDrawer({ fetchUrl })` has matching `*-fragment($id).php` + `*-fragment(none).phtml` .
- **Filename rule:** URL parameters go only into the `.php` action filename (`*-fragment($id).php` ). The `(none).phtml` view filename must NOT contain `($id)` — MintyPHP's router parses paren groups and breaks silently on nested ones.
2026-03-22 22:56:24 +01:00
### Never Do This
- Direct `$_GET` /`$_POST` /`$_SESSION` access — use `requestInput()` (GR-CORE-003)
- SQL outside Repository classes (GR-SEC-003)
- `new Service()` or `new Gateway()` outside Factory classes (GR-TEST-002)
- Hardcoding module UI into core templates — use platform slots
- Core schema changes without idempotent `db/updates/` script (GR-CORE-010)
- Settings in config files instead of DB `settings` table (GR-CORE-011)
- Skipping PRG pattern on form submissions (GR-CORE-012)
- Loading assets from external CDNs (GR-SEC-004)
- Undeclared cross-module imports (must use `requires` in manifest)
- Inline SQL or string-concatenated queries (GR-SEC-003)
2026-04-06 12:08:25 +02:00
- Raw `echo` in views without `// raw-html-ok` marker — use `e()` (GR-SEC-010)
2026-03-22 22:56:24 +01:00
- OpenAPI changes without updating the spec (GR-CORE-007)
docs(agents): encode list-export convention as GR-UI-EXPORT guard
Make the core-primitive-end-to-end rule for Grid.js list exports binding
instead of advisory — so future consumers (and reviewers) cannot quietly
drift back into inline fputcsv / hand-rolled headers / bespoke click
listeners / lurl() for query-carrying URLs.
- CLAUDE.md: new UI Patterns bullet + two "Never Do This" entries
spelling out the server/view/client contract in one place.
- .agents/checks/guard-catalog.json: new GR-UI-EXPORT entry describing
the end-to-end requirement (exportRequireGetRequest + exportCapLimit +
exportSendCsv + ExportColumn + shared filter-schema + dropdown partial
+ endpointUrl() + initListExport).
- .agents/checks/guard-enforcement-map.json: wire the new guard to
tests/Architecture/ListExportContractTest.php as the automated
evidence source.
- .agents/checks/guard-checklist.md & .agents/prompts/reviewer-code.md:
add GR-UI-EXPORT so the Code Reviewer prompt and the checklist flag
violations alongside GR-UI-LIST.
- tests/Architecture/ListExportContractFiles: registry of every list
export (currently helpdesk-domains, admin/users, audit/system-audit).
Adding a new list = one entry, contract test covers the rest.
- tests/Architecture/ListExportContractTest: six checks per registered
entry — endpoint uses the primitive, no hand-rolled output, data and
export share the same filter-schema, template includes the dropdown
partial and uses endpointUrl(), page module imports and invokes
initListExport, and the shared dropdown partial stays zero-config.
- pages/admin/users/export().php: align with the new contract by
switching from ad-hoc requestInput()->queryAll() reads to
gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'), same
as the data endpoint — eliminates the last place Grid and export
could disagree on what "the current filter" means.
Gates: PHPUnit 1900 OK (+6 contract checks), PHPStan 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:50:43 +02:00
- Hand-rolled list exports (inline `fputcsv` , own header setup, bespoke click-to-download JS) — always use `core/Service/Export/*` + `exportSendCsv()` + `app-list-export-dropdown.phtml` + `initListExport()` (GR-UI-EXPORT)
- Building an export/data endpoint URL with `lurl()` when it will carry query params — use `endpointUrl()` so module routes where `path ≠ target` still resolve (GR-UI-EXPORT)
2026-02-24 08:49:40 +01:00
## Database
- Schema defined in `db/init/init.sql` (applied once on container init)
2026-03-22 22:56:24 +01:00
- No migration framework — core schema changes for existing installs are idempotent SQL scripts in `db/updates/`
- Module schema changes via `module:migrate` (separate from core)
2026-04-01 20:27:42 +02:00
- Settings stored in `settings` DB table (single source of truth); `storage/runtime/settings.php` is a partial file cache for hot-path UI reads only
2026-02-24 08:49:40 +01:00
2026-03-22 22:56:24 +01:00
## Security (non-negotiable)
2026-04-13 20:50:24 +02:00
These rules are enforced by guards GR-SEC-001 through GR-SEC-010 in `.agents/checks/guard-catalog.json` . Any violation is a blocking finding.
2026-03-22 22:56:24 +01:00
- CSRF token verified on every POST (GR-SEC-001)
- No PII or secrets in logs or audit metadata (GR-SEC-002)
- SQL only via Repository prepared statements — no inline queries (GR-SEC-003)
- No external CDN or remote asset loading (GR-SEC-004)
2026-04-13 23:20:05 +02:00
- Encryption only via `core/Support/Crypto.php` AES-256-GCM (GR-SEC-005)
2026-03-22 22:56:24 +01:00
- File uploads: `storage/` only, authz-gated serving, MIME-validated (GR-SEC-006)
- API requests must not start sessions (GR-SEC-007)
- Authorization enforced server-side, not just in UI (GR-SEC-008)
- Tenant scope (`tenant_id` ) enforced on all queries — no unscoped data access (GR-SEC-009)
2026-04-06 12:08:25 +02:00
- Views use `e()` for all output — raw `echo` only with `// raw-html-ok: reason` marker (GR-SEC-010)
2026-03-22 22:56:24 +01:00
2026-02-24 08:49:40 +01:00
## Quality Gates
2026-04-01 19:41:56 +02:00
9 gates defined in `.agents/checks/quality-gates.json` .
Required/optional enforcement is defined in `.agents/checks/enforcement-policy.json` .
2026-03-22 22:56:24 +01:00
| ID | Gate | Command |
|---|---|---|
| QG-001 | PHPUnit | `vendor/bin/phpunit` |
| QG-002 | PHPStan level 5 | `vendor/bin/phpstan analyse -c phpstan.neon` |
| QG-003 | Architecture Contract | `vendor/bin/phpunit tests/Architecture/CoreStarterkitContractTest.php` |
2026-04-01 16:38:58 +02:00
| QG-006 | PHP Style | `vendor/bin/php-cs-fixer fix --config=tools/php-cs-fixer/.php-cs-fixer.dist.php --dry-run --diff --verbose` |
2026-04-02 10:49:35 +02:00
| QG-008 | Docs link + drift integrity | `bin/docs-link-check.sh` (plus `bin/docs-drift-check.sh` via `bin/qa-required.sh` ) |
2026-04-01 19:41:56 +02:00
| QG-009 | Codex skills sync | `bin/codex-skills-sync.sh --check` |
2026-03-22 22:56:24 +01:00
2026-04-01 19:41:56 +02:00
Additional non-blocking gates: QG-004 structural checks, QG-005 JS smoke, QG-007 composer-unused. See gate catalog + enforcement policy for details.
2026-02-24 08:49:40 +01:00
## Environment
- Config via `.env` (gitignored) — copy from `.env.example` for dev, `.env.prod.example` for prod
- `config/config.php` (gitignored) — copy from `config/config.php.example`
- `APP_CRYPTO_KEY` (64-char hex) needed for AES-256-GCM encryption of SSO secrets