MMOframework
Pourquoi l'IA Playground Bibliothèques Documentation Rechercher Agence
FR EN
Index
# Release checklists What to look at before a version is committed or shipped, one box per item -- or one per item and per member when a team tests it together -- saved as it is checked. A checklist may follow a `` board: its items then name the tickets they verify, a ticket all of whose items are checked is labelled verified on the board, and can be moved to done from the checklist, which comments in its discussion what was checked. | | | |---|---| | `MMO\admin\helpers\Checklist` | a checklist: its items, its checks, its progress, its tickets' progress | | `MMO\admin\helpers\ChecklistBuilder` | builds the items from a recipe: `git status` of repositories, the board's tickets, items written by hand | | `MMO\admin\helpers\KanbanBoard` | a board's file: tickets, discussions, labels, moves, and its lock | | `MMO\admin\helpers\BoardFileRepository` | a board's file as mmo.client's kanban entities (mmo.git's `BoardSync` with GitHub) | | `MMO\admin\controllers\ChecklistController` | the checklists' JSON API, and the items' actions: the diffs, the commands (▶), the decisions | | `MMO\admin\controllers\ChecklistDiffController` | mmo.git's `DiffController` on a recipe's repositories, untracked files included (never routed by itself) | | `MMO\admin\controllers\BoardController` | the boards' JSON API: a board, its discussions, its tickets' AI prompts, a new board with its checklist, the board each user works on | | `MMO\admin\helpers\BoardSelection` | the board each user picked (`boards/.current.json`, this machine's: git-ignore it) | | `{MMO\admin}/templates/checklists.phtml` | the page of the checklists: one (`?name=`), the default one, or every one (`?list=1`), a selector between them | | `{MMO\admin}/templates/board.phtml` | the page of a board (`?board=`, `?ticket=12` opens a ticket) | | `{MMO\admin}/pages/checklists`, `pages/boards` | the two templates as pages, in English | | `{MMO\admin}/pages/developer/diff.phtml` | Developer > Diff: an item's changes in mmo.git's `` (`?checklist=&repository=&path=&from=&to=`) | | `MMO\admin\helpers\TestAccounts` | the recipe's test accounts (`accounts`): the profiles, the conditions they recognize, where they may be made and opened | | `MMO\admin\helpers\TestData`, `TestDataStore` | the recipe's test records (`testData`): what each test account must see and must not, made through the application's entities | The `mmo-change-tracking` agent skill puts a change on a board, in « Backlog ». The `mmo-release-bugs` agent skill (`.claude/skills/`, also a prompt of the MCP server) is the whole method around it: a bug report becomes tickets with a fix prompt each, agents fix them and report in the tickets, the checklist proves them. ## Adding them to an application ```jsonc // configs/config.json "MMO\\core\\Application": { "routes": { "MMO\\admin\\controllers\\ChecklistController": {}, "MMO\\admin\\controllers\\BoardController": {}, "MMO\\client\\controllers\\PageController": {} // after them: its `**` route catches everything } }, "MMO\\admin\\controllers\\ChecklistController": { "folder": "{root}/checklists", // one JSON file per checklist "boards": "{root}/boards", // the boards a checklist may follow: {boards}/{board}.json; null for none "scope": "admin", // mmo.oauth: who may read and check; null on a local tool "anonymous": false, // true: the routes answer whoever calls, logged in or not "checklists": { // the checklists the application declares (see below) "general": {"title": "Release checklist", "recipe": {"itemsFile": "{root}/src/checklists/general.json"}} }, "default": "general", // the one the page opens when it is given none "verifiedLabel": {"id": "verified", "label": "verified", "color": "green", "icon": "check"}, "doneComment": "**Release checklist**: verified, moved to **done**.", "autoDone": true, // a ticket of an active status every checklist of the board verifies moves to done by itself; false: by hand "supervisors": null, // ["marc"]: only their ticks move a ticket (their member box, else the tick's author); null: anyone's "autoDoneComment": "**Checklists**: every item checked, moved to **done** automatically.", "diffScope": "dev.checklists", // who reads the recipe's diffs -- from this machine only "openAsScope": "sudo.switchAccount", // with dev.checklists: who opens a check as its test account -- this machine only "testAccountsCommand": "php admin.php developer test-accounts --checklist={name}", // what a missing test account's item says to run "testDataCommand": "php admin.php developer test-data --checklist={name}", // what makes the test records (the expectations' tooltip) "runScope": "dev.checklists#run", // who runs an item's command (▶) -- from this machine only "commands": {"mmo": {"pattern": "#^php tools/mmo\\.php (verify|i18n)( \\S+)*$#"}, ...}, // what ▶ runs (below) "readOnly": ["cpam-orphans"], "readOnlyForbids": ["--fix"], // the commands of admin.php that only read (below) "runTimeout": 600, "php": "php", // how long a command may run (s); the php it runs with "decisionComment": "**Decision of {by} ({date})**: {option}\n\n> {question}", // what a decision posts on its ticket "decisionDate": "d/m/Y" }, "MMO\\admin\\controllers\\BoardController": { "folder": "{root}/boards", // one JSON file per board; null: no boards (every route 404) "scope": "admin", "anonymous": false, "default": null // the board proposed to whoever picked none; null: the developer layer's, else the first }, "MMO\\client\\Theme": {"pages": ["{root}/src/pages", "{MMO\\admin}/pages"]} ``` ## Several boards, each with its checklist A project has several boards, one per feature or deployment objective, and each board its checklist: the board names it (`"checklist"` at the top of its file, beside `title` and `description`, changed by `PUT api/boards/{board}.json`), the checklist follows it (its `board`). Other checklists may follow the same board (a release's). - **A new board** is made with its checklist: `POST api/boards/{board}/create.json {title, description, checklist, from, refPrefix}` -- the columns Backlog, À faire, En cours, Recette, Terminé (or `from`'s), `from`'s labels, categories and people, and a checklist (`checklist`, the board's name by default) following it with the recipe of `from`'s. The board page's « New board » does it, copying the board shown. - **The board each user works on**: `GET|PUT api/current-board.json {board}` (@see `BoardSelection`). Picking a board in the board page's selector makes it the user's: the page then opens on it, the checklists page on its checklist, and the developer layer files its tickets there (`DeveloperController::getBoard()`, a development machine only). The agents' skills (`mmo-change-tracking`, `mmo-release-bugs`) propose it first when they ask which board to use. - `GET api/boards.json` says, for each board, its `title`, `description`, `checklist` and how many tickets each column holds (`columns`). ## A ticket's life Backlog → À faire → En cours → Recette → Terminé: each column plays a role (`KanbanBoard::roles()`: `intake`, `ready`, `doing`, `verify`, `done`), found from the statuses' types or declared by the board (`roles` at the top of its file). - `POST api/boards/{board}/{ticket}/move.json {status, comment}` moves a ticket only when it is ready for the column -- À faire wants its prompt (a ```` ```claude ```` block), Recette a ```` ```check ```` block and items of the board's checklist naming it, Terminé every one of them checked -- else 409 with the `reasons`. The agents move tickets so; the board's page still drops them anywhere. - `PUT|GET api/boards/{board}/plan.json`: the plan of the À faire tickets (groups, their tickets in order, what is left out and why), kept as the board's `plan`, shown folded above it; each ticket planned gets a comment with its rank. - `ChecklistController::$autoDone` is on by default: a ticket whose items are all checked (by `supervisors`, when set) goes to Terminé by itself. Rather than mmo.admin's pages (in English, a menu group of their own), a page of the application imports the templates, in its words (every key optional; `texts` lists each template's words at its top): ```php import(\MMO\filename('{MMO\admin}/templates/checklists.phtml'), ['locale' => 'fr-FR', 'texts' => ['title' => 'Checklists', ...]], false) ?> import(\MMO\filename('{MMO\admin}/templates/board.phtml'), ['board' => 'acme', 'checklists' => \MMO\url('admin/checklists')], false) ?> ``` The web server writes the files: the folders must be writable by it. Without `scope`, whoever reaches the routes writes the checklists and the boards: keep that to localhost, as `anonymous` (the agents' `curl` on a development machine, `configs/dev.json`). my.oviepro has both: its general checklist in production (`{db}/checklists`, `sudo`), the board and the review checklists in dev only. ### Declared checklists A checklist of `checklists` is made the first time it is asked for, and kept up to its declaration (`title`, `description`, `members`, `types`, `groups`, `board`, `boardUrl`, `recipe`). Its items are built from its recipe when it is made, and again whenever what they come from changes -- the recipe, or its `itemsFile` (a new version of the application asking other checks) -- for a recipe of `items` and `itemsFile` alone; a recipe reading repositories or a board is built by `build.json` only. What was checked stays checked while its item is there. Such a checklist is the general one of an application: what is tested before each version, round after round (`reset.json`). Its items file says, with the release that brings it, what to check again on the server: - `"round": "4.2.9"` at its top: the release the checklist is checked for. Once deployed, the first look at the checklist starts that round -- every box unchecked, the last round in the history -- unless it is at it already (it is not started twice). Change it when a release needs the whole checklist checked again. - `"recheck": "4.2.9"` on a check, or on a page or a section for all of theirs (a check's own wins): a release that touched a page asks its checks again. A value the item did not have unchecks it, every member's box, once; the other items stay checked. The page marks it « to check again (4.2.9) » until it is done again. Taking the marker away unchecks nothing: leave it, or remove it, after the round. ## The file `{folder}/{name}.json`: ```jsonc { "version": 4, "updatedAt": "2026-10-05T14:17:00+02:00", "title": "my.oviepro 4.1: review before commit", "description": "...", "board": "my.oviepro-deploy", // optional: the board of the tickets "boardUrl": "boards/oviepro-deploy", // optional: its page, where `?ticket=12` opens a ticket "members": [], // optional: one box per member "recipe": {...}, // optional: how `build.json` makes the items "refPrefix": "R421-", // optional: what the items' refs start with (`C` by default) "lastRef": 224, // the last ref number given: never given again "types": {"pdf": {"label": "PDF", "icon": "file-pdf"}}, // optional: what each `kind` is called, and its icon "groups": {"General": {"icon": "globe"}}, // optional: a group's icon "items": [{"id": "3d332c914651", "ref": "R421-12", "group": "mmo.core", "section": "src/entities", "kind": "code", "title": "src/entities/Entity.php (modified)", "detail": "Review the diff: `git -C ... diff -- ...`", "url": null, "note": "", "tickets": ["80"], "recheck": ""}], // recheck: what it is to be checked again for "checks": {"3d332c914651": {"*": {"at": "2026-10-05T13:34:55+02:00", "by": "marc"}}}, "release": {"name": "4.1", "startedAt": "...", "startedBy": "marc"}, // the round being checked "history": [{"name": "4.0", "startedAt": "...", "startedBy": "marc", "closedAt": "...", "closedBy": "marc", "done": 700, "total": 760}] } ``` - An item's `id` is `sha1(group|section|title)`, 12 characters: rebuilt items keep their checks, a reworded item starts again unchecked. - An item's `ref` (`R421-12`) is what people call it by: `refPrefix` and a number, unique in the checklist, shown as a badge before its title. Give each checklist its own prefix (`G` the general one, `F` the front one, `R421-` release 4.2.1's) and a ref names one item of them all. The ref stays the item's while it is there -- rebuilt items keep theirs, a new one gets the number after `lastRef`, a removed item's is never given again; a reworded item is a new item, with a new ref. Items an items file brings with their `ref` keep it (a release carries its refs to the server). The filter finds an item by its ref, `#R421-12` in the page's url scrolls to it, clicking the badge copies it, the routes take it in place of the id, and the comment of a ticket moved to done lists its items' refs. Changing `refPrefix` (`PUT`, or a declaration's) renames the refs, their numbers kept. - `group` is what the page folds (a repository, the pages, "before committing"), `section` a heading in it (a folder, a page), `kind` what it is (`code`, `page`, `global`, a test's type), `url` where to look, `detail` how (`` `code` `` shows as code, nothing else is markup). - `checks[item][member]`, `*` without members. An item is done once every box is checked. - `note` is a word on the item's section (a page's), shown by its heading; a section whose items share an `url` links it. - The first check starts the round (`release`); `reset.json` closes it -- every box unchecked, a line of `history` saying how far it got -- and names the next. An items file asking for a new `round` does the same when it is built. - A rebuild bringing an item a `recheck` it did not have unchecks that item alone. - `action` is how the item is checked in one click (below); `runs[item]` its last ▶ (`{at, by, exitCode, passed, duration}`, never its output), `decisions[item]` the option its buttons posted (`{at, by, option}`). ## One click per item: `action` Each item may say how it is checked, and the page gives it a button. `Checklist::NormalizeAction` checks it: an action that is not valid is dropped (its item stays), a `PUT` bringing one is refused (400). | `type` | keys | the page | |---|---|---| | `diff` | `repository` (a label of the recipe's `repositories`), `path?`, `from?` (`HEAD`), `to?` (none: the working tree) | « See the diff »: Developer > Diff, in `` | | `page` | `url`, `as?` (a profile of the recipe's `accounts`, else the check's words), `steps: [...]` | « Open » (a new tab), the steps numbered; with a profile, « Open as test.… » first (*The test accounts*) | | `run` | `command`, `expect?` (what a person reads), `match?` / `notMatch?` (what the machine finds, or must not: a text, or a `/regular expression/`), `cwd?` (a repository's label) | ▶: the server runs it; its output in the item, folded; checked when it passes (below) | | `decision` | `ticket`, `question`, `options: [...]` (two or more), `asked?` (the date it was asked) | a button per option; « Decided on … » once decided | | `manual` | `command?`, `steps?` (one of them), `as?` (a request sent as a profile) | « Copy » the command; the steps: what only a person can do (a server, a console); « Open as » the site's home | ```jsonc {"group": "before committing", "section": "checks", "title": "`php tools/mmo.php verify --lib=admin` passes", "action": {"type": "run", "command": "php tools/mmo.php verify --lib=admin", "match": "/^ok\\.$/m", "cwd": "hub (MMO/master)"}} ``` **What the builder gives** (`build.json`, a declared checklist's items file): - a changed file (`git status`) → `diff` from `HEAD` to the working tree: what is about to be committed; - a page's check (```` ```check ```` `page | url | what to check`) → `page`: its url, as whom when the text says it (`connecté en compte limité à un groupe`, `as an account limited to a group`: the recipe's `account`, a regular expression, replaces the default), its steps -- the text, `;` between them; a page of an items file → `page`; - a page's check whose second cell is no address to open as it is → still an action (`ChecklistBuilder::screenAction`): - a command (`php admin.php x --dry-run`, `(ligne de commande) php ...`, `cd folder && ...`, `~/folder (command)`; a quote or a pipeline the block's `|` cut is put back) → a `run` when the server runs it, its group runs commands (below) and nothing is said besides it but `(ligne de commande)` (the builder's `localNote`; `(sur l'instance, après une sauvegarde)`, `dev.oviepro.com`... keep it copied), else a `manual` to copy (`cd folder && command`), its parentheses and `puis ...` as steps; - a delivery (`Livraison stg | php admin.php ...`, the recipe's `delivery`, a regular expression) → `manual`: a server is a person's; - a request (`POST x.pdf`, `(outils réseau) POST ...`, an `.json` / `.pdf` address with a placeholder) → `manual`; - an address with a placeholder (`declaration/AccidentTravail/documents?id=`, `{id}`, `…`) → `page`: the same address with a record the ticket names (`?id=6979c45f...` in its discussion or description), else the recipe's `screens` for its page or its path (the closest parent: a list to pick the record from), its first step « Open un AT from this list (...) » (`texts.openRecord`, `texts.aRecord`); else `manual`; - `{uid}` in an address is the connected user (`{uid de l'utilisateur connecté}` becomes it): the page puts in `MMO.user.uid` when it opens it; - a screen named without its address (`(menu du compte) Profil`) → `page`: the address another check of the ticket gives it, else `screens[name]`; else `manual`, the cells as steps; - the other lines of a ```` ```check ```` block, by their first cell: ````markdown ```check diff | mmo.admin:src/helpers | the guard comes first, before any write run | php tools/mmo.php verify --lib=admin | ok. | hub (MMO/master) decision | Keep the old export? | keep it / drop it manual | `php admin.php decode-html` | on each instance, after the release ``` ```` `diff | repository:path | what to read` reads the changes since the last release: its most recent `v*` tag the branch holds, or its last `version X.Y.Z` commit of `mmo.json` when it is later (mmo.admin's release tags nothing; an old `v16.0.0` of a library its releases no longer tag gives way), to the branch. `run | command | expected | repository` is a `run` when the command is on the list and its group runs commands, else a `manual` to copy; a cell `match=` or `notMatch=...` (4th, 5th...) is what the machine finds, `expected` stays the sentence to read -- in a page's line too (`page | php admin.php x --dry-run | what to see | match=/0 error/`). `decision | question | option 1 / option 2` is asked on the ticket (`asked`: the date of the comment holding it). `manual | command or step | what to do`. They make a group of their own (`texts.actions`), a section per type (`texts.diffSection`, `runSection`, `decisionSection`, `manualSection`), a kind per type (named and iconed); - an item of the recipe keeps its `action`; without one, its first command in `code` (title, then detail; `cd folder && command` runs it in that repository) is a `run` when it is on the list and its group runs commands, else a `manual`; a `diff` without revisions reads the working tree in the `beforeCommit` group (`texts.beforeCommit`, « before committing »), the last release's changes elsewhere. - **where a command gets a ▶**: in every group but the recipe's `noRunGroups` -- a release's groups, whose commands are a server's (`"noRunGroups": ["release dev.oviepro.com"]`): copied there --, or only in its `runGroups` when it gives them (`ChecklistBuilder::runsIn`). Without either, every group runs (before 22/10/2026: only `beforeCommit`). - **the same check twice is one item** (`ChecklistBuilder::merge`, since 23/10/2026; `"merge": false` in the recipe keeps them apart): items with the same action -- a diff of the same repository and path, the same command (where it runs, what it must print), the same page with the same steps and account, the same decision -- become the first of them, with the tickets of all. Its title gets `texts.merged` (`{title} (+ {count} more)`): a new title, so a new id and ref, unticked -- a box ticked for one of them was not ticked for all. Its detail says what each other one was for (`texts.mergedItem`). A merged diff reads commits: `from` the oldest revision of its items (the last release for `HEAD`), `to` the branch -- the working tree only while its path has changes not committed --: `` shows who changed each line, and the detail lists the commits touching its path (`texts.commits`, the newest 12, then `texts.moreCommits`). - a page's check -- or a request sent by hand -- gets the profile of `recipe.accounts` its line names (`as=`, a last cell: `page | url | what to check | as=limited`), else the one whose `match` is found first in its text (the check, then the page's name; an item written by hand: its title and detail); a command to copy is no one's. Without profiles, the words `account` finds stay. `build.json` lists in `build.unmatchedAccounts` (`{ref, id, title, text, as}`) the checks speaking of an account (`TestAccounts::MENTION`: `en compte …`, `avec un compte …`, `en administrateur`, `as an editor`...) or naming one (`as=`) that no profile knows: complete the profiles until it is empty. ### The test accounts `recipe.accounts` says as whom a check is done, and which account of this machine's database fills the condition: ```jsonc "accounts": { "limited": {"username": "test.groupe.nord", "label": "Account limited to a group (North)", "match": "/\\bcompte limité\\b|limited to a group/iu", "roles": ["dev-user"], "groups": ["North"], "description": "sees the establishments of the North group only"}, "nonAdmin": {"username": "test.user", "match": "/compte (?:non administrateur|sans admin)/iu", "roles": ["dev-user"]}, "self": {"self": true, "label": "Your account (god)", "match": "/connecté en god/iu"} // the account logged in: nothing to switch to } ``` - A profile's `username` starts with `test.` (`TestAccounts::USERNAME`): never an operator's (`god`) nor a real person's -- any other is left out. Several profiles may name one account (with the same roles and groups). - **They exist**: `php admin.php developer test-accounts [--checklist=] [--dry-run]` makes the accounts of the profiles the database lacks, with their roles and groups (`groups` by label, joined through the account's `setGroupes()`; `DeveloperShellController::$groupsCollection`), a password nobody knows (never shown): they open through « Switch account » only. Only on a development instance (`MMO_MODE` `dev`), in a database of this machine, the one `configs/config.json` declares -- never a copy of a client's that `local.json` points to, never `--database=`. An account that exists is left as it is (its differences from its profile reported), unless the command made it (`{db}/developer/test-accounts.json`, by database): its roles and groups are then brought back to its profile. - **The button**: a page's check with a profile shows « Open as test.… » (its label beside): `GET developer/open-as?checklist=&item=` (or `&account=&url=`) opens the page in that account's session as mmo.oauth's « Switch account » does -- a token whose actor is the account logged in: journaled, the banner « Logged in as … (by …) », `login/restore` back --, without the password, so only when: 1. the request comes from this machine (`LocalRefusal`, as ▶); 2. the account really logged in (the actor of a switched session) holds `dev.checklists` (the developer layer) and `openAsScope` (`sudo.switchAccount`); 3. the profile is one of the checklist's, a `test.` account this database has, active, not the one logged in; 4. the address is one of the site (`TestAccounts::SiteRelative`: the recipe's `site` or the application's url, made relative; another host, a scheme, `//`, `..`, a backslash are refused): never a redirection elsewhere. `{uid}` is the test account's. A profile whose account is missing shows « No test account … » and the command to copy (`accountsCommand`). ### The test records An account limited to a group has nothing to check when its group has no record: `recipe.testData` gives each profile records in its perimeter and records outside it, and says what each check opened as a test account must show (`TestData`): ```jsonc "testData": { "prefix": "TEST CLAUDE", // every record's label starts with it (the default): they are found, and archived, by it "by": "admin", // the profile whose account writes them: its rights, the application's rules "records": { "employeeNorth": {"collection": "salaries", "label": "TEST CLAUDE Salarié Nord", "fields": {"nom": "TEST CLAUDE", "prenom": "Salarié Nord"}}, "contractNorth": {"collection": "contrats", "label": "TEST CLAUDE Contrat Nord", "fields": {"salarie": "@employeeNorth", "entreprise": {"$find": "groupes", "where": {"label": "North"}, "path": "entreprises/0"}, "debut": {"$date": "2024-01-02"}}}, "atNorth": {"collection": "accidentTravails", "label": "TEST CLAUDE AT Nord", "status": "draft", "fields": {"contrat": "@contractNorth", "date": {"$date": "-10 days"}}}, "mpNorth": {"collection": "maladiePros", "label": "TEST CLAUDE MP Nord", "status": "draft", "fields": {"contrat": "@contractNorth"}, "calls": [{"method": "addCommentaire", "args": ["$by", "TEST CLAUDE comment"]}]}, "certificateNorth": {"collection": "files", "label": "TEST CLAUDE certificat Nord.pdf", "file": {}, "labels": ["\"#certificat\""], "fields": {"attachment": "@atNorth"}} }, "expect": [ {"as": "limited", "match": "/sousTraitants/", "sees": ["astNorth"], "hides": ["astSouthEast"]}, {"as": "otherScope", "sees": ["atSouthEast"], "hides": ["atNorth", "@certificateNorth (its link: 403)"]}, {"items": ["R501-316"], "sees": ["@astDraftNorth (complete: becomes active)"]} ] } ``` - a field's value: `"@key"` a record of the recipe (made before it), `"@key/path"` a value of it, `{"$find": collection, "where": {...}, "sort": {...}, "path": "a/0"}` a record the database has -- read, never written --, `{"$date": "-10 days"}` a date; `"$by"` the account writing; `"@@x"` the text `@x`. A record naming one that is not, or itself, is left out; - `status` the status it is made with (`draft`: a declaration that is not complete is not submitted); `calls` the entity's `add…()` methods called before it is saved (nothing else); `file` (`{name?, content?}`, a one-page PDF saying its label by default) makes a file as mmo.edms' `FileManager::uploadFile()` does, `labels` its tokens, its bytes under `test-data/` in the storage (`TestDataStore::$filesKey`: the month folders are the web server's), readable by it -- the storage's `files` path must be the web's from the command line too (my.oviepro: `{root}/www/files`); - a record's label, a file's name, starts with `prefix`, else it is left out (the command says why); - `expect`: the first rule that fits an item -- its profile (`as`), its ref (`items`), its text (`match`: its address, title, detail, steps) -- gives the line « must see: …; must not see: … » beside its « Open as » button; a record's key, or `@key` in words, shows as its label. A rule with neither `as` nor `items` fits only the items done as a profile. **They exist**: `php admin.php developer test-data [--checklist=] [--dry-run] [--archive]` (after `test-accounts`: `by` is one of them) makes the records the database lacks, in the order they name each other, each committed alone as the account `by` names (\MMO\oauth\user() while it is saved): the application's rules apply as to that account's request -- a contract without its employee is refused (my.oviepro #329), a record outside the perimeter of a limited writer too --, never a raw insert. It stops at the first record refused. The same guards as `test-accounts`: a development instance, this machine's database, the one `configs/config.json` declares (every collection of the records, and the accounts'), never `--database=`. `{db}/developer/test-data.json` (not versioned) keeps what it made, by database and key: - a record it made that is still there is not made again: a second run makes nothing (« exists »); one that is gone (a database restored) is made again; - a record labelled so that it did not make is left as it is, and not made (« skipped »): it never writes in a record it did not make, and `$find` only reads; - `--archive` archives what it made, the last made first (`archived` in the file); a run after it makes them again (an archived record of that label is no longer in the way). Several checklists may give one key: the same record (else the later one is left out). my.oviepro's front checklist copies the release's `testData` with its `accounts` (`boards/my.oviepro-deploy.front-recette.py`). ### The commands ▶ runs `POST api/checklists/{name}/items/{item}/run.json` runs **the command the item declares** -- never one the request sends -- when: 1. the request comes from this machine: `REMOTE_ADDR` `127.0.0.1` or `::1`, no proxy header (`X-Forwarded-For`, `Forwarded`, `X-Real-IP`...), a local host name (`localhost`, `127.0.0.1`, `[::1]`, `*.localhost`, `*.test`). A server never runs one, even behind its own proxy; 2. the asker holds `runScope` (`dev.checklists#run`, mmo.oauth's, or the developer mode's: every scope on an `open` development machine; the role `developer` has it, `tester` not); 3. the command is on `commands`, a word of it holds only letters, digits and `.,/:=@~+%_-` (no quote, `;`, `|`, `$`, `*`, backquote: nothing a shell reads) -- but `--eval 'var DRY_RUN = true'`, exactly, for `mongosh` --, it holds what its rule `requires` and nothing its rule `forbids`: | name | pattern | requires | |---|---|---| | `mmo` | `php tools/mmo.php verify …`, `php tools/mmo.php i18n …` | | | `phpunit` | `phpunit …`, `vendor/bin/phpunit …` | | | `npm` | `npm test …` | | | `node` | `node --test …` | | | `admin` | `php admin.php …` | `--dry-run`: a reprise writes in the database, only its dry run runs | | `readOnly` | `php admin.php …` | never `--fix` (`readOnlyForbids`); the command is one `admin.php` declares | | `plan` | `php scripts/admin.php plan …` | | | `check-release` | `php scripts/check-release.php …` | `timeout` 900: it runs the verifies and asks every page (15 min) | | `mongosh` | `mongosh [--quiet] --eval 'var DRY_RUN = true' scripts/patch/.js` | the database a name (this machine's server: never a host, an uri, an option), the file one of the application's `scripts/patch/`; no other `--eval` | - **`cd && `**: `mmo`, `phpunit`, `npm` and `node` (their `cd` key) also run in a repository of the checklist's recipe (`recipe.repositories`: a library of the hub, the hub, the application): `cd ~/dev/src/MMO/master/libs/core && phpunit --filter x`, or one of its `runFolders`: folders a ▶ may `cd` to without their changes being listed (no diff item) -- `"runFolders": ["~/dev/src/MMO/master", "~/dev/src/MMO/master/libs/core", {"label": "hub", "path": "~/dev/src/MMO/master"}]`; my.oviepro's front checklist gets the release's repositories as its `runFolders` (`boards/my.oviepro-deploy.front-recette.py`). A folder that is neither (`/tmp`, `libs/`), or holds `..`, is refused. - **`timeout`**: a rule's own limit, in seconds (`check-release`: 900); the others `runTimeout` (600). An item never sets its own. - **`readOnly`**: the commands of the application's `admin.php` that only read, which the application declares (`"readOnly": ["cpam-orphans", "unattached-declarations", "incomplete-declarations", "invalid-nir"]` in my.oviepro's `configs/dev.json`): they run without `--dry-run`, never with a word of `readOnlyForbids` (`--fix`: `invalid-nir --fix` is a reprise; `invalid-nir --fix --dry-run` runs as any dry run). A name `adminScript` (`{root}/admin.php`) does not declare (`#[Command([['const' => 'name']], ...)]`, `#[InteractiveCommand('name', ...)]`) is refused. - **`mongosh`**: a patch's dry run, as the patch reads it (`DRY_RUN === true`: lists, writes nothing), on a database of this machine's server named by itself -- `configs/config.json`'s, or a copy of a customer's beside it --; the run passes `var DRY_RUN = true` as one argument, no shell. - **Passed**: exit code 0, `match` found (without a `match`, `expect`, as before) and `notMatch` absent (`ChecklistController::Verdict`, the output's colours left aside): the item is checked. Exit 0 without the `match`: « read the output against what is expected », unchecked; `notMatch` found (`unwanted`) or another code: failed. - **The builder derives `match`** for a `run` that has none (`ChecklistBuilder::DeriveMatch`, the `cd folder &&` left aside): | command | `match` | `notMatch` | |---|---|---| | `php tools/mmo.php verify …`, `… i18n …` | `/^ok\.$/m` (its last line) | `/^failed:/m` | | `phpunit …` | `/^OK \(/m` | `/FAILURES!\|ERRORS!/` | | `npm test` | `/\b0 failed\b/` (mmo.client's runner: « 505 passed, 0 failed ») | | | `node --test …` | `/\bfail 0\b/` | | | `php admin.php …` (a dry run, a listing that reads) | `/END\(200\)/` (the framework's last line) | `/!! \|Uncaught\|Fatal error/` (`shell\error`, `!! END(500) !!`) | | `mongosh … DRY_RUN …` | `/dry run/` (« (dry run: nothing written) ») | `/MongoServerError\|MongoNetworkError\|Uncaught/` | | `php scripts/check-release.php …` | `/point\(s\) prouvé\(s\)/u` (its summary) | as `admin.php` | | `php scripts/admin.php plan …` | `/versions/` | as `admin.php` | Another command keeps its `expect` as what is found. `commands` is a map: a configuration adds a rule by name, or takes one away with `null`. The command runs in its `cwd` repository, else in the first folder -- the application's, then the recipe's repositories -- holding the script it names (`php tools/mmo.php`: the hub), as the web server's user, `runTimeout` seconds at most (10 minutes), its output (`2>&1`, its last 200 kB) answered and shown in the item. What the web server's user may not read stops the command with its cause: `php scripts/admin.php plan` on a `.mmo/deploy.history.json` kept `600` says `the deployment history [...] is not readable by [www-data]` and the command its owner gives it the right with (`setfacl -m u:www-data:r .../.mmo/deploy.history.json`). Exit code 0, and `expect` found: the item is checked by whoever ran it (their box, on a checklist with members; not at all when they are not one). ### The diffs `GET api/checklists/{name}/git/{repository}/diff.json` and `…/diff/file.json` (`?from=&to=&path=`) are mmo.git's DiffController on the checklist's recipe's repositories -- `{repository}` its label made a path segment (`hub (MMO/master)` is `hub-MMO-master`, the checklist's `repositories` lists them) --, nothing else: a request never names a folder. Against the working tree, a new file git does not track yet is shown too, all of it added. They hand out the code: the same rule as ▶ -- this machine only -- with `diffScope` (`dev.checklists`). Developer > Diff (`developer/diff?checklist=&repository=&path=&from=&to=`) shows them in ``; the checklist page's `diffPage` names another page. ### The decisions `POST api/checklists/{name}/items/{item}/decide.json` `{option}` posts `decisionComment` on the item's ticket (`{by}`, `{date}`, `{option}`, `{question}`), records the option (`decisions`) and checks the item. A decision is taken once: 409 when it was, here or on the ticket -- a comment reading as `decisionComment` up to `{option}` (« Decision of … (…)** »), posted since the line was asked, or naming the question or an option. The page then shows « Decided on … » without buttons (`decided`, in the answer). ## The recipe ```jsonc { "repositories": [ {"label": "acme", "path": "~/dev/src/acme", "together": ["modules/old/"], "exclude": ["libs/"], "pages": true} ], "treated": "/\\*\\*Claude — (résultat|result)\\*\\*/u", "site": "http://localhost/acme/", "pages": "acme — pages", "checks": {"12": {"page": "gestion/accidents", "check": "sorted by date, newest first", "url": "gestion/accidents"}}, "items": [{"group": "before committing", "section": "checks", "title": "`php tools/mmo.php verify` passes"}], "itemsFile": "src/pages/admin/release-checklist.items.json", "texts": {"diff": "Relire le diff : `git -C {root} diff -- {path}`"}, "screens": {"Profil": "entity?uid={uid}", "declaration/AccidentTravail": "gestion/accidents"}, "delivery": "/^(livraison|delivery)\\b/iu" } ``` 1. **items**, **itemsFile**: written by hand, first. The file holds flat items, or a page by page test: `{members, types, round, sections: [{label, icon, recheck, pages: [{label, path, note, recheck, checks: [{label, type, recheck}]}]}]}`, whose `members`, `types`, `round` and sections' icons the checklist takes too (the section is the group, the page the section, the check's type the kind; `round` and `recheck`: see *Declared checklists*). 2. **repositories**: one `code` item per file of `git status --porcelain -uall`, under the repository's `label`; its section is its folder (two levels, three under `src/`), a page's folder for a file of `src/pages` unless `"pages": false`. `together` makes one item of a folder's files, `exclude` leaves paths out. `~/` is the home of whoever owns the application's folder (not the web server's). The item names the treated tickets whose discussion cites the file -- by its path, or its last three or two parts, `a/{b,c}.php` written out; a file no ticket cites says it is a transverse change. 3. **pages**: for each treated ticket -- one whose discussion matches `treated`, or that `checks` names -- its functional checks: `checks[id]` (one, or a list), else the last ```` ```check ```` block of its discussion, else of its description, one check per line: ````markdown ```check gestion/accidents | sorted by date, newest first home (mobile 390 px) | home | the drawer closes on Escape ``` ```` `page | what to check`, or `page | url | what to check`; a relative url is relative to `site`. A treated ticket with no check gets one item asking to check its result on screen. 4. **texts**: the words of the generated items, in the application's language: `M`, `D`, `??`, `A`, `R`, `T` (the states), `files`, `diff`, `new`, `deleted`, `orphan`, `pages`, `root`, `page`, `unchecked`, `uncheckedDetail`, `beforeCommit` (the group of the checks of this machine), `actions`, `diffSection`, `runSection`, `decisionSection`, `manualSection` (the other lines of the ```` ```check ```` blocks: see *One click per item*), `openRecord`, `aRecord` (the first step of a check whose address needs a record). 5. **account**: the regular expression finding as whom a page's check is done, in its text (its first group), when the recipe has no **accounts** (*The test accounts*): the profiles that recognize it, by key. 6. **screens**: a screen a ```` ```check ```` line names without its address -- by its page's name (`Profil`), or by the path of an address needing a record (`declaration/AccidentTravail`, its sub-pages too) -- and the url opening it (a list); `{uid}`: the connected user's. **delivery**: the page names of a delivery on a server (`manual`). `POST api/checklists/{name}/build.json` answers the checklist and `build: {orphans, unmatched, unmatchedAccounts}`: the changed files no ticket cites, the treated tickets no file names, the checks speaking of an account no profile knows. ## The API | | | |---|---| | `GET api/checklists.json` | `default`, and every checklist: name, title, description, board, version, release, progress, declared | | `GET api/checklists/{name}.json` | one, with `progress`, `tickets` (each ticket its items name: title, status, total, checked, verified), `decided` (the decision items answered: `{at, option}`), `repositories` (each recipe repository's name in the diffs' url), `accounts` (each profile: username, label, description, self, exists) and `accountsCommand`, `expects` (by item id: `{sees, hides}`, the labels of `testData`'s `expect`) and `testDataCommand` | | `PUT api/checklists/{name}.json` | `{version, title, description, board, boardUrl, members, recipe, refPrefix, items}`: create (version 0) or change; 409 when saved meanwhile | | `POST api/checklists/{name}/build.json` | rebuild the items from the recipe | | `PUT api/checklists/{name}/items/{item}.json` | `{checked, member}`; `{item}` its id or its ref | | `POST api/checklists/{name}/reset.json` | `{name}`: a new round, every box unchecked, a line of `history` | | `POST api/checklists/{name}/tickets/{ticket}/done.json` | move a verified ticket to the board's first done status, with a comment | | `POST api/checklists/{name}/items/{item}/run.json` | run the item's command (this machine, `runScope`, `commands`); `run: {command, cwd, exitCode, passed, output, duration, checked}` | | `POST api/checklists/{name}/items/{item}/decide.json` | `{option}`: post the decision on the item's ticket, check the item; 409 when decided | | `GET api/checklists/{name}/git/{repository}/diff.json`, `…/diff/file.json` | the recipe's repositories' diffs, for `` (this machine, `diffScope`) | | `GET developer/open-as?checklist=&item=` | the item's page in its test account's session (this machine, `dev.checklists` and `sudo.switchAccount`, a `test.` account, a page of the site) | | Boards | | |---|---| | `GET api/boards.json` | every board: name, title, version, updatedAt, tickets | | `GET api/boards/{board}.json` | `{board, title, version, updatedAt, data}`, each ticket counting its `comments`; an unknown board is empty, version 0 | | `PUT api/boards/{board}.json` | `{version, data}`: the whole board; 409 with the board as it is when it was saved meanwhile | | `GET api/boards/{board}/prompts.json` | `?agent=claude&status=todo&done=1`: the tickets' AI prompts, and `markdown`, the brief | | `GET`, `POST api/boards/{board}/{ticket}/comments.json` | a ticket's discussion; `{body, parent}` comments it | | `PUT`, `DELETE api/boards/{board}/{ticket}/comments/{comment}.json` | `{body}` changes a comment | ## From PHP ```php use MMO\admin\helpers\{Checklist, ChecklistBuilder, KanbanBoard}; $board = KanbanBoard::Edit(\MMO\filename('{root}/boards/acme.json'), fn(KanbanBoard $b) => $b); Checklist::Edit(\MMO\filename('{root}/checklists/acme-2.0.json'), function (Checklist $list) use ($board) { $builder = new ChecklistBuilder($list->data['recipe'] ?? [], $board); $list->setItems($builder->build()); print_r($builder->orphans()); }, true); ```