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>
This commit is contained in:
2026-04-21 22:50:43 +02:00
parent 0e82e00495
commit 53ccd212d5
8 changed files with 256 additions and 27 deletions

View File

@@ -92,6 +92,13 @@
"title": "Reuse-first (JS + CSS)",
"requirement": "MUST check web/js/core/ and web/js/components/ for existing exports before adding new functions. MUST check existing CSS layers and app-* classes before writing new rules. No duplication of logic from core modules."
},
{
"id": "GR-UI-EXPORT",
"area": "ui",
"reviewer": "code",
"title": "List export via core primitive",
"requirement": "Grid.js list pages that offer CSV/Excel downloads MUST use the core export primitive end-to-end. Server endpoints MUST call exportRequireGetRequest(), cap rows via exportCapLimit(), stream with exportSendCsv() from core/Support/helpers/export.php, and declare columns as MintyPHP\\Service\\Export\\ExportColumn. The SAME filter-schema.php as the list's data endpoint MUST be parsed via gridParseFiltersFromSchemaFile() so exported rows match the grid under identical filters. Views MUST include templates/partials/app-list-export-dropdown.phtml in $listTitleActionsHtml and pass 'exportUrl' => endpointUrl('<route-path>') in the page config (never plain lurl() — query-string survival on module routes where path != target depends on endpointUrl). Page JS MUST call initListExport({ gridConfig, exportUrl }) from /js/core/app-list-export.js. No inline fputcsv, no hand-rolled headers, no bespoke click listener."
},
{
"id": "GR-SEC-001",
"area": "security",

View File

@@ -17,6 +17,7 @@ Guard IDs live in `.agents/checks/guard-catalog.json` (guards where `reviewer` =
## UI (skip if backend-only)
- `GR-UI-014` new views/components handle loading, empty, error, success states
- `GR-UI-LIST` list/grid standards preserved (row actions, page size, initStandardListPage)
- `GR-UI-EXPORT` list exports go through the core primitive: server uses exportRequireGetRequest + exportCapLimit + exportSendCsv + ExportColumn, shares filter-schema.php with the data endpoint; view uses `app-list-export-dropdown.phtml` partial and `endpointUrl()` (never `lurl()`) for the exportUrl; JS calls `initListExport` from `/js/core/app-list-export.js`. Add the new list to `tests/Architecture/ListExportContractFiles.php`.
- `GR-UI-DETAIL` detail page contracts, shared partials, no inline confirm, layout system
- `GR-UI-A11Y` no a11y regression; new components use semantic HTML, keyboard-operable
- `GR-UI-I18N` all visible text uses t(); keys present in all language files

View File

@@ -127,6 +127,13 @@
".agents/runs/<TASK>/review-code.json"
]
},
{
"guard_id": "GR-UI-EXPORT",
"enforcement_mode": "automated",
"evidence_source": [
"tests/Architecture/ListExportContractTest.php"
]
},
{
"guard_id": "GR-SEC-001",
"enforcement_mode": "automated",

View File

@@ -25,6 +25,7 @@ Scope — verify these guards:
- GR-TEST-002: code is testable
- GR-UI-014: UI state completeness (if UI touched)
- GR-UI-LIST: list/grid standards (if lists touched)
- GR-UI-EXPORT: list export via core primitive (if list exports touched)
- GR-UI-DETAIL: detail page standards (if detail pages touched)
- GR-UI-A11Y: accessibility (if UI touched)
- GR-UI-I18N: i18n completeness (if visible text touched)