@sylad/cadence 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +23 -7
  4. package/agents/code-reviewer.md +2 -2
  5. package/agents/doc-keeper.md +19 -0
  6. package/agents/qa-reviewer.md +2 -2
  7. package/agents/spec-writer.md +1 -1
  8. package/agents/ux-reviewer.md +1 -1
  9. package/bin/cadence.js +9 -1
  10. package/dist/audit.js +14 -4
  11. package/dist/check.js +6 -0
  12. package/dist/cli.js +235 -30
  13. package/dist/config.js +101 -5
  14. package/dist/corrections.js +23 -2
  15. package/dist/deliver.js +78 -21
  16. package/dist/docsync.js +19 -3
  17. package/dist/git.js +16 -3
  18. package/dist/lead.js +37 -12
  19. package/dist/migrate.js +166 -0
  20. package/dist/orchestrate/briefs.js +28 -6
  21. package/dist/orchestrate/command.js +303 -34
  22. package/dist/orchestrate/cycle.js +720 -273
  23. package/dist/orchestrate/guard.js +2 -2
  24. package/dist/orchestrate/integrate.js +85 -0
  25. package/dist/orchestrate/launch.js +37 -5
  26. package/dist/orchestrate/mutex.js +30 -0
  27. package/dist/orchestrate/pool.js +74 -36
  28. package/dist/orchestrate/registry.js +123 -14
  29. package/dist/orchestrate/result.js +1 -1
  30. package/dist/orchestrate/snapshot.js +25 -0
  31. package/dist/orchestrate/spec.js +89 -5
  32. package/dist/orchestrate/state.js +42 -6
  33. package/dist/orchestrate/table.js +61 -2
  34. package/dist/orchestrate/timeouts.js +40 -0
  35. package/dist/orchestrate/worktree.js +368 -0
  36. package/dist/perf.js +80 -19
  37. package/dist/plan.js +92 -6
  38. package/dist/ready-free.js +109 -0
  39. package/dist/ref/add.js +116 -0
  40. package/dist/ref/check.js +338 -0
  41. package/dist/ref/command.js +78 -0
  42. package/dist/ref/corpus-render.js +83 -0
  43. package/dist/ref/corpus.js +201 -0
  44. package/dist/ref/evaluate.js +241 -0
  45. package/dist/ref/figures.js +179 -0
  46. package/dist/ref/measure.js +330 -0
  47. package/dist/ref/pdf.js +104 -0
  48. package/dist/ref/png.js +125 -0
  49. package/dist/ref/store.js +40 -0
  50. package/dist/ref/tools.js +51 -0
  51. package/dist/resources.js +51 -0
  52. package/dist/session.js +5 -2
  53. package/dist/state.js +60 -7
  54. package/dist/testgate.js +15 -0
  55. package/docs/cadence-hud.md +12 -0
  56. package/docs/cycle-vague-dark.svg +1 -1
  57. package/docs/cycle-vague-light.svg +1 -1
  58. package/docs/deliver.md +58 -3
  59. package/docs/fonctionnement-wave-dark.svg +1 -1
  60. package/docs/fonctionnement-wave-light.svg +1 -1
  61. package/docs/fonctionnement.md +5 -5
  62. package/docs/lead.md +35 -12
  63. package/docs/news.md +24 -1
  64. package/docs/orchestrate.md +230 -19
  65. package/docs/perf.md +13 -8
  66. package/docs/raf.md +89 -11
  67. package/docs/ref.md +316 -0
  68. package/docs/session.md +2 -2
  69. package/docs/skills.md +9 -2
  70. package/docs/spec.md +1 -1
  71. package/docs/stabilite.md +4 -4
  72. package/docs/whats-new.md +2 -0
  73. package/package.json +2 -1
  74. package/skills/deliver/SKILL.md +6 -1
  75. package/skills/lead/SKILL.md +24 -6
  76. package/skills/session-close/SKILL.md +1 -1
  77. package/skills/session-start/SKILL.md +2 -2
  78. package/templates/inventaire.json +96 -0
  79. package/templates/orchestrate/accept-back.md +1 -0
  80. package/templates/orchestrate/accept.md +1 -0
  81. package/templates/orchestrate/fiche.md +1 -0
  82. package/templates/orchestrate/fix-docs.md +18 -0
  83. package/templates/orchestrate/fix-minors.md +2 -1
  84. package/templates/orchestrate/fix.md +5 -2
  85. package/templates/orchestrate/implement.md +6 -3
  86. package/templates/orchestrate/maquette.md +2 -1
  87. package/templates/orchestrate/precheck.md +2 -1
  88. package/templates/orchestrate/review-recheck.md +2 -0
  89. package/templates/orchestrate/review-small.md +5 -1
  90. package/templates/orchestrate/review.md +2 -0
  91. package/templates/orchestrate/spec-back.md +2 -1
  92. package/templates/orchestrate/spec.md +4 -3
  93. package/templates/orchestrate/ux.md +3 -1
package/docs/perf.md CHANGED
@@ -5,7 +5,7 @@
5
5
  `cadence perf [folder] [--since date] [--json]` is the permanent dashboard of the workflow. It calls no model, writes nothing
6
6
  (no journal, no plan, no cache) and reads, from the folder (default: the current one), the wave journals
7
7
  `<folder>/.cadence/runs/<wave>/<project>--<lot>.json` (with each wave's `wave.json`) and the plan of every project of the folder
8
- (one nesting level, like `cadence corrections`: `group/gamma`). Exit code `0`, even when a `⚠` is printed; `1` on a usage or read error.
8
+ (one nesting level, like `cadence corrections`: `group/gamma`; a `tmp/` folder is never read: its working copies would count their lots twice, L241). Exit code `0`, even when a `⚠` is printed; `1` on a usage or read error.
9
9
 
10
10
  It compares two windows of 7 calendar days (local days): the **current** one (the last 7 days, today included) and the **previous**
11
11
  one (the 7 days before). Every speed indicator is printed on the same line as its **quality twin**, never alone ("fast *and* well"):
@@ -15,35 +15,38 @@ one (the 7 days before). Every speed indicator is printed on the same line as it
15
15
  | Throughput | lots ready, lots handed back per day, lots delivered (plan `done` with `finished` in the window), share of handed-back lots within their token budget | escaped defects | is the flow dropping? look for the bottleneck before adding lots; are the budgets right? |
16
16
  | Corrections | median of `fix` passes per lot, share of lots with 2 passes or more | share of lots right the first time (no `fix` pass) | is the first write good enough? harden the sheet or the brief |
17
17
  | Corrections | dominant causes (top 3 of the classified passes, the unclassified share apart) | — | which link to fix |
18
- | Lead interventions | lots handed back by a wave then finished outside it, `ux` sessions per visible lot | escaped defects | does the wave stop too early? does the ergonomics review cost more than one round? |
18
+ | Lead interventions | lots handed back by a wave then finished outside it, sessions that see the application per visible lot (`ux` steps, plus the `review-small` of a small visible lot), share of visible lots reviewed by a dedicated `ux` step | escaped defects (sessions per lot only) | does the wave stop too early? does the ergonomics review cost more than one round? |
19
19
 
20
20
  A line starts with `⚠` and ends with the decision it informs when its value is **worse than the previous window by more than 25 %**
21
21
  (a drop for the throughput and the share right the first time; a rise for passes, interventions and escaped defects). No `⚠`, and the line says
22
- why, when one window holds fewer than 5 lots for the indicator, when the previous value is zero, or when `--since` truncates a window.
22
+ why, when one window holds fewer than 5 lots for the indicator (10 steps for a wall-time duration, L280), when the previous value is zero, or when `--since` truncates a window.
23
23
  A window with no lot for an indicator (no rendered lot for ready lots, no delivered lot for delivered, none at all for a window entirely before `--since`) gives `n/d` (`null` in JSON), never `0`. A **clear bottleneck** (one cause weighing at least half of the classified passes of the
24
24
  current window, with at least 10 classified passes) is flagged by a `⚠` even without a trend.
25
25
 
26
26
  - **Dating.** A journal is dated by the end of its last step (else by the creation of its wave); a journal without a readable date is left out. A correction pass is dated
27
27
  by the end of its `fix` step. The classification of a pass is `cadence corrections`' (same reading, `src/corrections.ts`): without finding to read it is `sans-constat`,
28
- otherwise the cause of `<folder>/.cadence/corrections.json` (a manual cause first), `non-classe` when absent from it. `perf` never classifies: run `cadence corrections --classify`.
28
+ otherwise the cause of `<folder>/.cadence/corrections.json` (a manual cause first), `non-classe` when absent from it. `perf` never classifies: run `cadence corrections --classify`, which the cause line recalls as soon as a pass with findings is unclassified. Without any model, the line below it, `origine des passes à constats`, counts the passes of the window by the kind of review that raised them (`review`, `review-small`, `ux`), read in the journals (`corrections.causes.<window>.byReview` in `--json`) (L240).
29
29
  - **Escaped defects** = distinct lots with a plan note dated in the window, written after the lot was done, that starts with `QA` (after the `DD-MM` date prefix) and
30
30
  contains `bloquant` or `majeur` not preceded by `0` or `aucun`. Until a plan holds such a note the value is `n/d` ("aucune note QA reconnue : convention à fixer (L227)"). Reopened lots are not counted: the plan does not date a reopening.
31
31
  - **Lead interventions.** A lot counts when its last journal (all waves of the folder) is `handed-back` or `failed` and its plan lot is `done` with `finished` on or after the day it was handed back.
32
32
  A journal joins its plan by the project name (the path of the project relative to the folder) and the `lot` field of the journal; an id `Q4/accueil-4-ux12` joins the plan lot `Q4`. Journals with no plan lot are counted aside (`N lot(s) de journal sans plan`) and never counted as finished outside the wave.
33
33
  An unreadable plan skips its project with a warning on the error output and the other projects are read; the indicators that read the plans are `n/d` only when no plan is readable.
34
+ - **Sessions that see the application.** `sessions ux par lot visible` counts, per visible lot rendered in the window, its `ux` steps that ended `ok` **and** the `review-small` steps that ended `ok` of a small visible lot (journal `small: true`): the single pass of a small visible lot is its only ergonomics review, run with the application launched (L283; measured on 24-10, 139 of 140 small lots were visible and the line showed 0.0, −97 %, a false signal). The `review-small` of a lot that is not small is only a code review and is not counted; a journal written before the `small` field counts none.
35
+ `part des lots visibles revus par une étape ux` is the share of the visible lots rendered in the window with at least one `ux` step that ended `ok` (so a small visible lot, reviewed in `review-small`, is not in it). It is informational: no `⚠`, no quality twin (`interventions.dedicatedUx` in `--json`), `n/d` without a visible lot.
36
+ - **Pre-check share of `oui`** (L288). `part de « oui » du précheck` is the share of the lots rendered in the window whose journal records a pre-check verdict (`precheckVerdict`) that is `oui` (the deliverable was already there; an unproven `oui` is recorded `partiel`). Compare the current window with the previous one before and after a change of `orchestrate.precheck` or of the pre-check criterion (the title rule of L288): a share that stays low means the pre-check costs more than it saves. A journal older than L288 has no `precheckVerdict`: its verdict is read back from the lot (`livrable déjà présent` → `oui`, pre-check finding in the brief → `partiel`, otherwise `non`), so the window before the change is measurable. A journal written since L288 whose pre-check report was unreadable (it keeps the `contrôle préalable illisible` warning, no `precheckVerdict`) has no verdict and is not counted, not read back as `non` (L293). Lots with no successful `precheck` step are not counted; informational, no `⚠`, no quality twin (`precheck.yes` in `--json`), `n/d` without a verdict.
34
37
  - **`--since date`** is an ISO date that ignores everything before that day in both windows (a wave created before that day is not read, even if its lots end after it); a window starting before it is *truncated* (its per-day rates divide by the days covered, no `⚠`).
35
38
  It is **not** the instant of `cadence corrections --since` (waves created strictly after).
36
- - **`--json`** prints one object `{ since, today, windows: { current, previous }, throughput, corrections, interventions, tokens, delays, durations, cost, orphans, warnings }`; every indicator carries
39
+ - **`--json`** prints one object `{ since, today, windows: { current, previous }, throughput, corrections, interventions, precheck, tokens, delays, durations, cost, orphans, warnings }`; every indicator carries
37
40
  `{ current, previous, change, warn, twin, lots, note }` (`twin`: the path of its quality twin, e.g. `throughput.escaped`). Same values as the text.
38
41
 
39
42
  ## Tokens, delays, wall time and cost (L226)
40
43
 
41
- The same two windows, the same `⚠` (more than 25 % worse, at least 5 lots or steps in each window, previous value not zero, no truncated window) and the same
44
+ The same two windows, the same `⚠` (more than 25 % worse, at least 5 lots in each window, or 10 steps for a wall-time duration (L280: under 10 steps the change from one window to the next cannot be interpreted, `docs/perf/recalage-2026-10-24.md`), previous value not zero, no truncated window) and the same
42
45
  quality twin (the share of lots right the first time) on the same line. A rise is the worse direction for each of them.
43
46
 
44
47
  | Indicator | Reading | Decision it informs |
45
48
  |---|---|---|
46
- | tokens per lot, by estimate | median of the counted tokens (sum of `tokens.counted` of every step) of the rendered journals of the window whose lot joins the plan, one class per exact estimate (`0.5`, `1`, `2`…); a failed lot counts, a journal with no readable tokens or without plan lot does not | is the estimate calibrated? redo the split or the budgets |
49
+ | tokens per lot, by estimate | median of the counted tokens (sum of `tokens.counted` of every step) of the rendered journals of the window whose lot joins the plan, one class per exact estimate (`0.5`, `1`, `2`…), a lot that went through the spec chain (a `spec`, `fiche`, `maquette` or `accept` step; 450 k is the gap of their *budget* (`SPEC_STAGE_ALLOCATION`), the measured gap is smaller, about 150 k at estimate 0.5 and 30 k at estimate 1 on 12–18-10, `docs/perf/goulot-2026-10-18.md`; on 17–23-10 the `⚠` on estimates 0.25 and 0.5 (+27 %) came with a share right the first time up from 37 to 59 %, so the reading is « heavier first write, fewer corrections », `docs/perf/recalage-2026-10-24.md`, L280) in its own class `0.5+spec`, printed after `0.5`, so that a change in the mix of lots is not read as a heavier ordinary lot (L240); a failed lot counts, a journal with no readable tokens or without plan lot does not | is the estimate calibrated? redo the split or the budgets |
47
50
  | share of review tokens | counted tokens of the steps whose kind starts with `review` or is `ux`, over the counted tokens of all steps of the rendered journals; a ratio, `n/d` without journal | does the review weigh too much against the writing? lighten it (`orchestrate.review.threshold`) |
48
51
  | delay created → done | median of `finished − created` (calendar days) of the plan lots `done` with `finished` in the window and both dates readable | is the queue growing? |
49
52
  | delay done → delivery | median of the days between `finished` and the first line of the repository's `deliveries.log` on or after it (`<git common dir>/cadence/`, one repository per project, worktrees included; read without creating anything); `n/d` for a project that is not a git repository, has no log or no later line | deliver in smaller batches? |
@@ -72,7 +75,7 @@ No indicator of produced volume (lines of code, commits, total tokens or euros o
72
75
 
73
76
  ## Delivery quality and the tour header (L227)
74
77
 
75
- `cadence deliver` appends one line `day sha reason` to `<git common dir>/cadence/deliveries-failed.log` for every failure after the lock is taken (script, CI, deploy or check; the reason is the first line of the failure message). A green delivery or a refusal before the lock writes nothing there; `deliveries.log` is unchanged. A failed write of that log leaves the exit code and the failure message of the delivery as they are and adds the warning `deliver : journal des livraisons rouges non écrit : <cause>` on the error output. An exception or a crash of `deliver` writes no line.
78
+ `cadence deliver` appends one line `day sha [class] reason ⏎ line ⏎ line` to `<git common dir>/cadence/deliveries-failed.log` for every failure after the lock is taken (script, CI, deploy or check; the reason is the first line of the failure message, the class one of `refus`, `ci`, `deploiement`, `verification`, and up to 5 bounded lines of the failing command's output follow, see [deliver](deliver.md)). A green delivery or a refusal before the lock writes nothing there; `deliveries.log` is unchanged. A failed write of that log leaves the exit code and the failure message of the delivery as they are and adds the warning `deliver : journal des livraisons rouges non écrit : <cause>` on the error output. An exception or a crash of `deliver` writes no line.
76
79
 
77
80
  | Indicator | Reading | Twin | Decision it informs |
78
81
  |---|---|---|---|
@@ -81,4 +84,6 @@ No indicator of produced volume (lines of code, commits, total tokens or euros o
81
84
 
82
85
  Both are judged like the other indicators (`⚠` above +25 %). For red deliveries the sample is the deliveries of the window, green and red (at least 5 per window); for blocking QA findings it is the plan lots `done` in the window (at least 5 per window). `--json` adds `quality: { redDeliveries, qaBlocking }` (Indicator format, `twin` `throughput.delivered` and `throughput.escaped`); every other key is unchanged, and `throughput.delivered.twin` stays `throughput.escaped`.
83
86
 
87
+ **Red deliveries by class and project** (L282): when the current window holds at least one red delivery, `cadence perf` prints under the « Débit » section two lines, `livraisons rouges par classe (fenêtre courante)` and `livraisons rouges par projet`, each sorted by count (ties: refus, CI, déploiement, vérification, then `non classée` for lines written before the class existed). `--json` adds `quality.reds: { current, previous }`, each `{ count, byClass: { refus, ci, deploiement, verification, inconnue }, byProject: { <project>: { <class>: n } } }`; the other keys are unchanged. The reason itself (the last lines of the failing command's output) stays in `deliveries-failed.log`: read it there.
88
+
84
89
  `cadence lead tour` prints the `⚠` lines of `perf` as its **perf header** (see [lead tour](lead.md#lead-tour)).
package/docs/raf.md CHANGED
@@ -2,8 +2,9 @@
2
2
 
3
3
  # raf
4
4
 
5
- - The plan is a YAML file in the repo (`docs/plan/raf.yaml`), edited by the CLI.
6
- Your comments and hand edits are preserved.
5
+ - The plan is a YAML file in the repo, edited by the CLI: `.cadence/raf.yaml` by default (versioned by git, never published with `docs/`),
6
+ with its notes in `.cadence/notes/`. Your comments and hand edits are preserved. A project that still has `docs/plan/raf.yaml` keeps it:
7
+ see [Where the plan is](#where-the-plan-is).
7
8
  - A commit belongs to a lot when its message cites the id: `feat(L3): …`,
8
9
  `fix: L3/t1 …`. The link is **computed from `git log`**, never stored, so
9
10
  committing never dirties the plan. The id is read as a whole word: `XL3`,
@@ -14,7 +15,7 @@
14
15
  count. Without a scope citing a lot, the whole message is read as before.
15
16
  - `raf check` audits drift between the plan and the history.
16
17
  - Plan upkeep needs no lot. A commit is plan upkeep when **every file it
17
- touches is a plan file**: the plan itself, its Gantt page (only at its default place, `gantt.html` next to the plan — written elsewhere with `raf gantt -o`, declare it under `plan.files`), the `plan:` key of the config file in use (`cadence.yaml`, or
18
+ touches is a plan file**: the plan itself, its Gantt page (only at its default place, `gantt.html` next to the plan — written elsewhere with `raf gantt -o`, declare it under `plan.files`), with the default plan `.cadence/raf.yaml`, also the legacy `docs/plan/raf.yaml` and `docs/plan/gantt.html` (so that the old commits of the plan stay plan upkeep after the `git mv`), the `plan:` key of the config file in use (`cadence.yaml`, or
18
19
  the `--config` file; a change to `deliver:`/`session:` is work), or a file the
19
20
  project lists under `plan.files` in `cadence.yaml` (a page it generates from
20
21
  the plan, a journal), or the QA expectations file (`docs/qa/expectations.md`,
@@ -29,7 +30,7 @@
29
30
  ```sh
30
31
  npm install -g @sylad/cadence # or: npx -p @sylad/cadence raf …
31
32
 
32
- raf init # docs/plan/raf.yaml + post-commit hook + .playwright-mcp/ in .gitignore
33
+ raf init # .cadence/raf.yaml + .cadence/notes/.gitkeep + post-commit hook + .playwright-mcp/ in .gitignore
33
34
  raf add "Monthly dedup on merge" --estimate 2
34
35
  raf add "Typo in footer" --quickwin
35
36
  raf add "Loan cache" --after L1
@@ -39,7 +40,7 @@ git commit -m "feat(L1): dedup by calendar month"
39
40
  raf now # in progress, next up (quickwins first), recently done
40
41
  raf done L1/t1 && raf done L1
41
42
  raf check # exit 1 on drift
42
- raf gantt # docs/plan/gantt.html
43
+ raf gantt # .cadence/gantt.html (next to the plan)
43
44
  ```
44
45
 
45
46
  ## Commands
@@ -47,21 +48,25 @@ raf gantt # docs/plan/gantt.html
47
48
  | Command | Effect |
48
49
  |---|---|
49
50
  | `raf init [--project name] [--prefix L] [--no-hook]` | create the plan, install the hook, add `.playwright-mcp/` to `.gitignore` |
50
- | `raf add "title" [--estimate d] [--quickwin] [--visible [--screen page\|component\|journey]] [--spec] [--public "title"] [--after L2,L4] [--parent L3]` | add a lot or a sub-task, print its id (`--public`: the lot's title in the public's words, written as `public:` right after `title:`; `--screen`: the screen a visible lot creates or changes — a new `page`, a new `component` or a modified `journey` — written as `screen:` right after `visible:`, shown by `raf show`; it requires `--visible`, and `raf check` reports a `screen:` without `visible:` or with another value as an error. Only `page` triggers the spec step of `orchestrate` (L209), never inferred from the title; `--spec`: the lot changes a behaviour (something a user, an API client or another service can observe), written as `spec: true` right after `screen:`, `visible:`, `quickwin:` or `estimate:` (whichever comes last), shown by `raf show`, refused on a sub-task. Also set by the lead at planning time, never inferred from the title) |
51
+ | `raf add "title" [--estimate d] [--quickwin] [--visible [--screen page\|component\|journey]] [--spec] [--public "title"] [--after L2,L4] [--uses banc,passerelle] [--parent L3]` | add a lot or a sub-task, print its id (`--public`: the lot's title in the public's words, written as `public:` right after `title:`; `--screen`: the screen a visible lot creates or changes — a new `page`, a new `component` or a modified `journey` — written as `screen:` right after `visible:`, shown by `raf show`; it requires `--visible`, and `raf check` reports a `screen:` without `visible:` or with another value as an error. Only `page` triggers the spec step of `orchestrate` (L209), never inferred from the title; `--spec`: the lot changes a behaviour (something a user, an API client or another service can observe), written as `spec: true` right after `screen:`, `visible:`, `quickwin:` or `estimate:` (whichever comes last), shown by `raf show`, refused on a sub-task. Also set by the lead at planning time, never inferred from the title); `--uses`: the shared resources the lot occupies, written as `uses: [banc, passerelle]` after `after:` and before `created:` ([shared resources](#shared-resources-uses-and-resources), L251), refused on a sub-task) |
51
52
  | `raf add "title" --every <days>` · `raf did <id> ["text"]` | recurring lot: `every` (days) and `last` (last time done) fields; the lot stays `todo`, `raf now` and `session start` list it under « Récurrent » with « dû depuis N j » / « prochain dans N j »; `raf did` resets the count (spec: `docs/superpowers/specs/2026-10-04-L11-tache-recurrente.md`) |
52
53
  | `raf screen <id> page\|component\|journey\|none` | set, replace or remove `screen:` on an existing lot, to adopt the [spec step](orchestrate.md#the-spec-step-of-a-lot-with-screen) on a plan already written (only `page` adopts the [spec step](orchestrate.md#the-spec-step-of-a-lot-with-screen); `component` and `journey` do not); refused on a lot without `visible:` (like `raf add --screen`) and on a sub-task; `none` removes the key and leaves `visible:` alone |
54
+ | `raf visible <id> on\|off` | set or remove `visible:` on an existing lot (a user-facing change: a news entry is expected, and `cadence orchestrate` adds its UX step), written right after `quickwin:`, `estimate:` or `status:` (whichever comes last); `on` on a visible lot changes nothing, `off` on a lot without the key neither; refused on a sub-task, and `off` is refused while the lot has a `screen:` (`raf screen <id> none` first: a screen is a visible change). It is the fix `raf check` points to for an open « Revue UX — » lot without `visible:` (L273) |
53
55
  | `raf spec <id> on\|off` | set or remove `spec:` on an existing lot (a lot that changes a behaviour, for the back-end spec step of `cadence orchestrate`, see [The spec step of a lot with `spec: true`](orchestrate.md#the-spec-step-of-a-lot-with-spec-true)); refused on a sub-task; `off` is refused on a lot that has `screen: page` (the page implies the spec: remove `screen:` first) and `on` there only says so and changes nothing; on `component` and `journey` (ordinary path since L209) both work and set or remove the back-end `spec:`. `raf spec` only takes `on`/`off`: `cadence spec check\|qa <fiche.yaml>` keeps its own subcommands. `raf check` reports a `spec:` that is neither `true` nor `false` (`spec : true ou false attendu`) |
56
+ | `raf uses <lot> [<resource>…]` | replace the `uses:` list of a lot of any status (`done` and `dropped` included) by the names given (duplicates removed, order kept); with no name, remove the key. Prints nothing. A name is a word of lowercase letters, digits, `-` and `_` (else `ressource invalide : <name> (lettres minuscules, chiffres, - et _)`, plan unchanged); a sub-task is refused (`uses se pose sur un lot, pas sur une sous-tâche : L1/t1`); an unknown lot is `lot inconnu : <id>`. A name not yet declared is accepted: `raf check` refuses it (L251) |
54
57
  | `raf public <id> "title"` · `raf public <id> --clear` | set, replace or remove the public title of a lot |
55
58
  | `raf start <id>` · `raf done <id> [--force]` · `raf drop <id> [--reason text]` | dated transitions (`done` refuses open sub-tasks unless `--force`) |
56
59
  | `raf todo <id> [--reason text]` | put a started lot back to `todo` (`doing` → `todo`, `started:` removed, dated note « remis en attente : reason »): refused when work commits cite the lot (plan-only and version commits do not count, as `raf commits`), when the lot is not `doing` (a `done` or `dropped` lot is reopened by hand), or on a sub-task; sub-tasks and other keys are left as they are |
60
+ | `raf migrate [--dry-run]` | move `docs/plan/raf.yaml` to `.cadence/raf.yaml` and rewrite the readers of the old path in the repository, see [Moving the plan: `raf migrate`](#moving-the-plan-raf-migrate) |
57
61
  | `raf note <id> "text"` | dated note — keep decisions next to the work |
58
- | `raf show <id> [--notes]` | one lot: status, dates, `after`, public title, dated notes in order, then the counted commits (as `raf commits`, neighbouring repositories included); `--notes` prints the notes alone |
62
+ | `raf title <id> "new title"` | replace the title of a lot of any status (`done` included) and add a dated note « titre remplacé : « old title » »; `raf now`, `raf show`, `raf list`, `session start` and the proposals read the new one. Refused on an empty or identical title, an unknown lot or a sub-task |
63
+ | `raf show <id> [--notes]` | one lot: status, dates, `goal : …` right after the title when the lot has a `goal:` (native key or `plan.fields.goal`, L272: the objective that replaces the title in the `orchestrate` briefs), `after`, public title, `screen` and `spec`, `uses : banc, passerelle` when the lot has `uses:`, dated notes in order, then the counted commits (as `raf commits`, neighbouring repositories included); `--notes` prints the notes alone |
59
64
  | `raf commits <id>` | the commits counted for a lot (the set the code review gate uses), one `<sha> <subject>` per line, oldest first; a lot that declares `repos:` ([neighbouring repositories](orchestrate.md#a-lot-whose-work-is-in-a-neighbouring-repository)) gets, after them, one `dépôt <path> :` section per neighbour with the commits there that cite the lot and fall within the lot's period (`started` to `finished`) and, when the lot declares a `cite:` for that neighbour, contain that string (`aucun commit du lot`, or `introuvable` for a path that is not there) |
60
65
  | `raf now` | what to do next |
61
66
  | `raf list [--status s]` | flat list |
62
- | `raf ignore <sha> \| "exact subject" [--reason text]` | acknowledge a commit without a lot (tooling chore, a plan commit citing an unknown id) without rewriting history: a dated, reasoned line in the plan's `acknowledged:` section; a sha is exact, a subject covers every commit carrying it |
67
+ | `raf ignore <sha> \| "exact subject" [--reason text]` | acknowledge a commit without a lot (tooling chore, a plan commit citing an unknown id) without rewriting history: a dated, reasoned line in the plan's `acknowledged:` section; a sha is exact, a subject covers every commit carrying it; exactly one commit argument, the reason goes through `--reason "text"` (`raf ignore 1a2b3c4 "tooling"` is refused with « un seul commit attendu ; la raison se passe par --reason "texte" », exit 2) |
63
68
  | `raf check --ignored` | list the acknowledged commits with their date and reason |
64
- | `raf check [--since date] [--idle 7]` | since the plan's adoption date by default (a visible lot without a public title is only a `⚠` warning: it never changes the exit code): commits without a lot (commits touching only plan files, or only version fields, `CHANGELOG.md`, the README's « What's new » section and `docs/whats-new.md` are exempt), unknown ids, `todo` lots that already have commits, idle lots, `done` lots with open sub-tasks, bad or circular dependencies |
69
+ | `raf check [--since date] [--idle 7]` | since the plan's adoption date by default (a visible lot without a public title is only a `⚠` warning: it never changes the exit code): commits without a lot (commits touching only plan files, or only version fields, `CHANGELOG.md`, the README's « What's new » section and `docs/whats-new.md` are exempt), unknown ids, `todo` lots that already have commits, idle lots, `done` lots with open sub-tasks, bad or circular dependencies, and an open lot titled « Revue UX — <screen> » without `visible: true` (`ux-review-not-visible`, an error: such a lot goes through precheck → implement → review with no UX step, so the screen is never reviewed — finance L32 on 2026-10-23; L271; the message names the fix, `raf visible <id> on`, L273) |
65
70
  | `raf gantt [-o file]` | standalone Gantt page |
66
71
  | `raf hook install` | add the post-commit hook (read-only, never blocks) and the pre-commit hook (does nothing unless `hook.autostart` says so, see below) |
67
72
 
@@ -79,7 +84,7 @@ hook:
79
84
  - `warn`: today's behaviour, the commit goes through, the post-commit hook warns.
80
85
  - `refuse`: the commit fails, loudly, with `raf start <id>` in the message (`--no-verify` still bypasses it).
81
86
  - `start`: the hook runs `raf start <id>` and stages the plan, so the plan change is **in the same commit** —
82
- no amend afterwards, no dirty plan left behind. Only when `docs/plan/raf.yaml` is identical to `HEAD` (no
87
+ no amend afterwards, no dirty plan left behind. Only when the plan is identical to `HEAD` (no
83
88
  uncommitted change to the plan, staged or not): otherwise nothing is started, the hook says why
84
89
  (`plan modifié non commité : démarrage automatique sauté, raf start <id> à la main`) and behaves like `warn`.
85
90
 
@@ -91,6 +96,76 @@ pre-commit block to an existing repository. A read-only plan is never started by
91
96
  Global options: `--file path` or `RAF_FILE`; `RAF_TODAY=YYYY-MM-DD` to freeze
92
97
  the date. `cadence raf …` is the same as `raf …`.
93
98
 
99
+ ## Shared resources: `uses:` and `resources:`
100
+
101
+ Some projects of a folder share one thing that only one lot at a time can use (a test bench, a gateway, a
102
+ device). L251 (GitHub issue #13, 1/3) lets the plan **declare** it; nothing locks on it yet (`raf start` and
103
+ `cadence orchestrate` ignore `uses:`).
104
+
105
+ - The folder that holds the projects lists the resources in its `cadence.yaml`, next to `depth:` and `priority:`:
106
+ `resources: [banc, passerelle]`. A name is a word of lowercase letters, digits, `-` and `_`.
107
+ - A lot says which ones it occupies with `uses: [banc]` (a flow list; optional): `raf add "title" --uses banc,passerelle`, or on an existing lot
108
+ `raf uses L3 banc passerelle` (`raf uses L3` removes the key). `uses:` belongs to a lot, never to a sub-task, and `raf show` prints it.
109
+ It is a native key of raf's own plan; a plan kept by another tool is read-only and has no `uses` mapping.
110
+ - The list is read from the nearest `cadence.yaml` that carries the `resources:` key, starting at the parent of the project's root and going up
111
+ (a project at `depth: 2`, `folder/group/project`, reads `folder/cadence.yaml` when `group` carries none): the first file that has the key
112
+ wins, even with `resources: []` or with no value (`resources:`), and the search goes no higher. No file carries it: nothing is declared. Duplicates in the list are ignored.
113
+ The search is not bounded: without a nearer file it goes up to the root of the file system.
114
+ - `raf check` reads that file only when an open lot (`todo`, `doing`) carries `uses:`, and writes one gap per name it does not declare:
115
+ `✗ L3 utilise la ressource « banc », non déclarée dans <folder>/cadence.yaml (resources:)` (exit code 1). Without a declaring file the
116
+ gap names the `cadence.yaml` of the parent folder. `done` and `dropped` lots are not checked, so removing a resource from the list does not
117
+ fail the history. A `uses:` that is not a list of valid names is a gap of the lot (`✗ L3 : uses doit être une liste de noms de ressources`);
118
+ an unreadable `cadence.yaml`, or a `resources:` that is not a list of valid names, is a gap naming the file
119
+ (`<file> : resources doit être une liste de noms de ressources`), never an empty list. A plan where no open lot carries `uses:` gets the same
120
+ `raf check` as before.
121
+
122
+ ## Where the plan is
123
+
124
+ The plan is resolved in this order: `--file`, then `RAF_FILE`, then the `plan:` key of `cadence.yaml`, then `.cadence/raf.yaml`,
125
+ then the legacy `docs/plan/raf.yaml`.
126
+
127
+ - `raf init` creates `.cadence/raf.yaml` and `.cadence/notes/` (with a `.gitkeep`, so git tracks the empty folder). It refuses
128
+ when a plan already exists where it would create one, a legacy `docs/plan/raf.yaml` included: it never creates a second plan
129
+ beside it. With `plan: path` in `cadence.yaml`, or `--file`, the plan and nothing else is created there.
130
+ - A project that still has `docs/plan/raf.yaml` (and no `.cadence/raf.yaml`) keeps working, read and written as before. Every command
131
+ that loads the plan prints one line on **stderr** (standard output, `--json` included, is unchanged) with the move to do —
132
+ `cd <repository root> && mkdir -p .cadence && git mv docs/plan/raf.yaml .cadence/raf.yaml` (`mv` outside a git repository or when git does not track the file, where `git mv` would fail; the root is spelled out, quoted if
133
+ needed, so the line runs as is from any folder and, under `session --all`, names its project). `raf` never moves the file itself on its own: `raf migrate` does it, with the readers of the path. The line is not
134
+ printed when the path comes from `--file`, `RAF_FILE` or `plan:`.
135
+ - When both files exist, `.cadence/raf.yaml` is read and a stderr warning says that `docs/plan/raf.yaml` is ignored; nothing is written
136
+ or deleted.
137
+ - `.cadence/` is versioned, except `.cadence/runs/` (the state of the waves) and `.cadence/corrections.json` (the cache of
138
+ `cadence corrections`): `cadence orchestrate` adds those two to `.git/info/exclude` (it replaces the `/.cadence/` line an older
139
+ version wrote there) and the clean-tree guard ignores them, but a change to `.cadence/raf.yaml` or `.cadence/notes/` counts.
140
+ - `raf gantt` writes `gantt.html` next to the plan, so `.cadence/gantt.html` by default.
141
+ - `cadence lead tour`, `session --all` and `cadence orchestrate` recognise a project by `.cadence/raf.yaml`, `docs/plan/raf.yaml` or a
142
+ `cadence.yaml` with `plan:`.
143
+
144
+ ## Moving the plan: `raf migrate`
145
+
146
+ `raf migrate [--dry-run]` moves a legacy `docs/plan/raf.yaml` to `.cadence/raf.yaml` and updates what reads the old path, so the
147
+ « ancien emplacement » line stops appearing in every session. It is the one command that works on the files of the repository
148
+ rather than on the plan; it prints no legacy warning and never commits (review the result with `git status`, then commit).
149
+
150
+ - **The plan** moves with `git mv` (`mv` outside a git repository, or when git does not track the file), `.cadence/` created if needed.
151
+ - **The readers** are the files that cite `docs/plan/raf.yaml` — plan-data scripts, tests, CI workflows, skills, documentation: git's
152
+ tracked and not-ignored files (the whole folder, without `.git` and `node_modules`, outside a repository). Every occurrence is
153
+ rewritten to `.cadence/raf.yaml`. The history is not a reader and is left alone: `CHANGELOG*`, `*.ndjson` journals and `.cadence/`.
154
+ - **`--dry-run`** prints the move and every file it would touch, with its number of references, and changes nothing.
155
+ - **It refuses** (code 2, nothing moved or written) while a reader is *not handled*: a file that cites `docs/plan` in a way a text
156
+ replacement cannot follow — another file of the folder (`docs/plan/journal.ndjson`), the folder itself, a path built piece by piece
157
+ (`join(root, 'docs', 'plan', 'raf.yaml')`, even split over several lines, `Path("docs") / "plan"`; reported at the line where it starts), a binary file, a text file that is not UTF-8 (rewriting it would alter its bytes), or a file over 2 MiB that cites the path (too big to rewrite; a big file that does not cite it is ignored). Without `--dry-run`, a refusal prints nothing on stdout: nothing is announced as done. Each one is listed as `file:line`. Fix it by hand and run the command again, or
158
+ declare a file that cites the path without reading the plan (a fixture, a document about the old place) under `migrate.keep` in
159
+ `cadence.yaml`: a list of paths or patterns (`*`, `**`, `?`, as `docs.sync`), whose matches are listed as left as they are. A document that names the legacy place next to the new one (a stability contract, a description of the fallback) belongs there too: a line citing both would otherwise be rewritten to cite `.cadence/raf.yaml` twice.
160
+ - **It refuses** when `docs/plan/raf.yaml` and `.cadence/raf.yaml` both exist, and when neither does.
161
+ - **It can be run again**: with the plan already at `.cadence/raf.yaml` (moved by hand, or by a first run that stopped), it only handles the
162
+ readers that remain, and says `rien à migrer` when there is none.
163
+
164
+ ```yaml
165
+ migrate:
166
+ keep: [docs/specs/**, test/legacy-plan.test.ts] # cite docs/plan/raf.yaml without reading the plan
167
+ ```
168
+
94
169
  ## Plan file
95
170
 
96
171
  ```yaml
@@ -106,6 +181,7 @@ lots:
106
181
  estimate: 2 # working days
107
182
  quickwin: false
108
183
  visible: true # user-facing: a news entry is expected when done
184
+ goal: Dedup on merge, bank line kept # optional (L258): replaces the title as the objective of the `orchestrate` briefs when the title is stale
109
185
  after: [L0]
110
186
  gate: { count: 8, spec: true } # optional: not before 8 lots with `spec: true` are done (or `screen: page`)
111
187
  created: 2026-09-28
@@ -125,6 +201,8 @@ format, and the plan stays writable:
125
201
  plan: planning/todo.yaml
126
202
  ```
127
203
 
204
+ A relative `plan:` path is read from the repository root, whatever folder you run `raf` from; `--file` and `RAF_FILE` are read from the current folder.
205
+
128
206
  A project that publishes its plan (a JSON generated from it and committed with it)
129
207
  declares that file, so a commit touching only the plan and its published copy is
130
208
  plan upkeep; the plan keeps raf's format and stays writable:
@@ -173,7 +251,7 @@ plan:
173
251
  ```
174
252
 
175
253
  - Fields: `id`, `title`, `status`, `estimate`, `quickwin`, `visible`, `screen`, `spec`, `public`, `every`, `last`, `after`, `created`, `started`,
176
- `finished`, `notes`, `repos`, `parent`, `gate`; one left out is read under its own name. A timestamp counts for
254
+ `finished`, `notes`, `repos`, `parent`, `gate`, `goal` (L258: the objective that replaces the title in the `orchestrate` briefs); one left out is read under its own name. A timestamp counts for
177
255
  its day; a note written as plain text is one note.
178
256
  - `gate` (L219) is a drawing condition added to `after`: `gate: { count: N, spec: true }` or `gate: { count: N, screen: page }` (N an integer ≥ 1, one criterion). It is written by hand in the plan (no `raf add` option). The count is the number of `done` lots of the same plan that carry the criterion (`todo`, `doing`, `dropped` lots and sub-tasks do not count; recurring lots do). Until the count reaches N, a `todo` lot is *waiting*, never *ready*: `raf now` lists it on a line `en attente de condition de tirage : L190 (2/8 lots spec: true faits)` (the existing `en attente de dépendances : …` line stays for lots waiting on `after`), `cadence session start` adds an `En attente` section (nothing when no lot is gated) and leaves the lot out of `À suivre` and of the proposals, `cadence lead tour` skips it for `prochain` and adds `attend L190` to the project's line (and `waiting` to `--json`, only when a lot waits), `cadence orchestrate` refuses it before any action (`a:L190 : condition de tirage non remplie : 2/8 lots spec: true faits`) and `--continue` never draws it, and `raf show` prints `gate : 8 lots spec: true faits`. Once the count is reached the lot is ready like any other (the field stays in the plan); a lot already `doing` is not refused for its condition. A malformed `gate` is reported by `raf check` (`gate invalide : …`) and the lot is never drawable. A plan without `gate` behaves exactly as before; `after` read by the Gantt scheduling is unchanged.
179
257
  - `spec` is the exception to « a left-out field is read under its own name »: a read-only plan's `spec:` is often the path of a spec document, so it is read only when `plan.fields.spec` maps it, and then only `true` counts (any other value is neither an error nor a spec step). Natively, `spec:` stays `true` or `false` (L198). Consequence for a `gate: { count: N, spec: true }` in a read-only plan: it counts only lots whose `spec` is mapped by `plan.fields.spec`; without that mapping the count stays 0 and the lot waits for good, with no `raf check` problem. `gate: { count: N, screen: page }` has no such condition.
package/docs/ref.md ADDED
@@ -0,0 +1,316 @@
1
+ [← cadence](../README.md)
2
+
3
+ # `cadence ref`: reference documents (specs, norms, mock-ups)
4
+
5
+ A spec delivered as a PDF often keeps part of its information in images: screen mock-ups,
6
+ tables drawn as pictures, message templates, figures of a norm (WMO, ICAO…). `cadence ref`
7
+ turns such a document into files a session can read without losing the pictures. The whole
8
+ design is in
9
+ [`docs/superpowers/specs/2026-10-23-ref-documents-de-reference-design.md`](superpowers/specs/2026-10-23-ref-documents-de-reference-design.md)
10
+ (issue #14); this page describes what is delivered so far: **`cadence ref add`** (lot 1 of 10), **`cadence ref measure`** (lot 2 of 10) **`cadence ref check`** with the inventory schema (lot 3 of 10), and **`cadence ref eval`** (lot 4 of 10, with the synthetic corpus `cadence ref corpus`).
11
+
12
+ ## `cadence ref add <document.pdf> [--sections 3-5,9] [--slug name]`
13
+
14
+ Registers a PDF and builds, for it:
15
+
16
+ - **pages rendered at 200 dpi** (`pages/page-<n>.png`, `pdftoppm`);
17
+ - **positioned text** of every page (`pdftotext -bbox-layout`): each line with its box
18
+ `x, y, l, h` in pixels of the rendered page (200 dpi);
19
+ - **embedded images** (`pdfimages`, `images/p<page>-<n>.png`) with their region in the same
20
+ pixels (`pdftohtml -xml`);
21
+ - **the figure index**: one entry per caption at the start of a line (`Figure n`, `Fig. n`,
22
+ `Table n`, `Tableau n`, `Planche n`). After a separator (`—`, `–`, `:`, `.`, `-`) the line is a
23
+ caption whatever the case of the title (`Figure 1 — map of stations`); without one,
24
+ `Figure 3 shows…`, `Figure 5, which…` and `Figure 5 (page 4) and…` cite the figure. One exception:
25
+ a line made only of the number and one closed parenthesis (`Table IV.3 (in accordance with
26
+ 4.3.11)`, WMO-No. 49 vol. II) is a caption when no other line looks like the caption of that
27
+ number; with another one, or open (`Table 11 (see Example…`), it cites. A separator
28
+ followed by nothing (`figure 24.` ending a sentence) is not a caption either. A
29
+ table-of-contents entry (dot leaders on its line or on the next one, or any line of a contents
30
+ page: one where at least two lines, and more than half, have dot leaders or end with a possible
31
+ page number) is neither a caption nor a citation, so a figure that only the contents list names
32
+ comes out as « nothing cites it ». A trailing number alone never makes an entry:
33
+ `Table 4.1. … presented in Chapter 4` among prose is a caption. When several lines look like the
34
+ caption of one figure, those with a title are kept (`Figure 5 Jet speed` rather than a bare `Figure 5`), if any; among them
35
+ the first on a page that holds an image wins, then the first with a separator, then the first. Each entry has its page, line, box, its images, and **the passages that cite it** (page,
36
+ line, exact line; `nommee` when the text names the figure, `designee` for « as shown
37
+ below » / « ci-dessous », attached to the nearest caption of the page). Plurals count one citation per figure
38
+ (`Figures 2 and 3`, `Tables 4 to 6`, `Figs. 2.1 et 2.2`); a number keeps the letters of an annex numbering (`Figure 1.B.1`, `Table 12.B.3`). Listed under **`unresolved`**
39
+ (« lien non résolu »): a citation of a figure with no caption, a figure that nothing cites,
40
+ and an image on a page that has no caption (`image sans légende`).
41
+
42
+ - **thumbnails of every page** (`thumbs/page-<n>.png`, 40 dpi) and the **pages without text**:
43
+ a page with fewer than 20 characters of text is taken for scanned and reported
44
+ (`pages sans texte : 30 (1-4, 6-31)`), so a scanned norm does not look indexed while its index
45
+ is empty; the thumbnails tell which pages to render with `--sections`. (Both recipes come from
46
+ the `norme-etude` skill of maritime-atlas: `refdoc.py index`.)
47
+
48
+ Without `--sections`, a document of 30 pages or fewer is rendered whole; a larger one only gets
49
+ its index (text and figures), no page is rendered. With `--sections`, the pages asked for are
50
+ rendered **plus the pages of the figures they name**, wherever they are in the document. Running
51
+ `add` again on the same document (same sha256) keeps the pages rendered before and adds the new
52
+ ones; a slug taken by another document is refused (`--slug` picks another name).
53
+
54
+ A PDF that asks for a password, forbids copying, is damaged, is not a PDF or has no page is refused with the reason. Image
55
+ documents (PNG, JPG) are not read yet.
56
+
57
+ ## `cadence ref measure <slug> <page> <zone> [--quoi couleurs|contraste|boites|texte] [--largeur 1440] [--json]`
58
+
59
+ Measures a zone of a **rendered** page of a document registered by `add` (a page `add` did not render
60
+ is refused with the command that renders it). The zone is `page` (the whole page), `x,y` (one
61
+ point) or `x,y,l,h` (a rectangle), in pixels of the rendered page (200 dpi: the unit of the boxes in
62
+ `document.json`). Every length in the answer is brought back to a **reference width** of 1440 px
63
+ (`--largeur`), the width of a screen, whatever the resolution of the page: the first line says the
64
+ factor. Without `--quoi`, the four measures; a point only has colours.
65
+
66
+ - **`couleurs`**: the exact colour of a point (`#c81e28`, `rgb(200, 30, 40)`); for a rectangle the
67
+ mean (rounded) and the **dominant colours** with their share of the zone (grouped by 8 levels per
68
+ channel, at most 5, none under 1 %).
69
+ - **`contraste`**: WCAG 2.2 contrast ratio between the zone's **background** (its dominant colour) and
70
+ its **text**: every colour carried by at least 10 % of the non-background pixels is a text colour
71
+ (an anti-aliased edge is spread over many shades and stays under that share), and the verdict is the
72
+ one of the **least contrasted** text colour, never a mean of several (a red and a blue text do not
73
+ make a purple that is not in the image). The one exception is a zone where no colour reaches that share (a gradient, text over a photo): the text is then the mean of the pixels furthest from the background, a colour that may not exist in the image, and the answer says so (`texte.moyenne: true` in JSON, a note on the `contraste` line). The three thresholds are read on the unrounded ratio: AA (4.5), AA large text and interface
74
+ components (3), AAA (7). `#767676` on white gives 4.54, `#777777` 4.48. A zone of one colour has no
75
+ contrast to measure.
76
+ - **`boites`**: the connected shapes that differ from the background (8 neighbours), the 20 largest first, and the
77
+ bounding box of all the content; coordinates are those of the page, not of the zone. A pixel belongs to a shape from a distance of 24 (RGB, Euclidean) to the background, the threshold under which a zone is flat: a pale card (`#eeeeee` on white) is a shape.
78
+ Shapes under 16 pixels are dust.
79
+ - **`texte`**: the words tesseract reads (English model, `--psm 6`) with their box and confidence, and the
80
+ **estimated font size**: the median height of the words that have no descender and a letter as tall
81
+ as a capital (`Hello`, `2026`, not `gap`) read with confidence ≥ 60, divided by 0.72 (the cap height
82
+ of Helvetica / Arial in em). The size is the one of the reference width.
83
+
84
+ **Declared tolerance** (last line, and `tolerance` in `--json`): colours ±1/255 per channel (mean
85
+ rounded), boxes ±1 pixel of the page, i.e. ±⌈factor⌉ px at the reference width, font size ±5 % (the cap
86
+ height of a face is 0.70 to 0.73 em) plus one pixel of cap height, given with the measure
87
+ (`police : ≈ 28.2 px (±10 %, 4 mot(s) …)`). The image is read as it is: an alpha channel is composed over white.
88
+ Only 8-bit, non-interlaced PNG (what `pdftoppm` writes) is read; another kind is refused with the reason.
89
+
90
+ ```
91
+ $ cadence ref measure norme 2 page --quoi contraste
92
+ norme p.2 · zone 0,0,1700,2200 px de la page · ramené à 1440 px (×0.85)
93
+ contraste : 4.54:1 (texte #767676 sur fond #ffffff) · AA ✓ · AA grand texte et composants ✓ · AAA ✗
94
+ tolérance : couleurs ±1/255 par canal, boîtes ±1 px, police ±5 % plus un pixel de hauteur de capitale
95
+ ```
96
+
97
+ `--json` prints one object (`document`, `page`, `zone`, `echelle`, `tolerance`, then `couleurs`,
98
+ `contraste`, `boites`, `texte` as asked). Exit codes: 0 done, 2 refusal (tool, unknown document,
99
+ page absent or not rendered, invalid zone or option).
100
+
101
+ ## `cadence ref check <inventaire.json | lecture.json>`
102
+
103
+ Validates an **inventory** (`genre: "inventaire"`) or a **reading** (`genre: "lecture"`): the JSON file in which the
104
+ reference analysis lists what the document shows. The model to copy is
105
+ [`templates/inventaire.json`](../templates/inventaire.json) (one element of each of the eleven types, slug `exemple`).
106
+ `check` validates the **form**, never the truth of a measure; it calls no system tool (it works with an empty `PATH`), does not read
107
+ `references.dir` and writes no file.
108
+
109
+ ```
110
+ $ cadence ref check inventaire.json
111
+ ✗ elements[3].libelle : champ du type texte manquant ou invalide
112
+ ✗ elements[5].ecran : exemple#texte-1 ne désigne aucun élément de type ecran
113
+ 2 écart(s)
114
+ $ cadence ref check templates/inventaire.json
115
+ inventaire valide : 11 éléments (ecran: 1, composant: 1, etat: 1, texte: 1, jeton: 1, table: 1, gabarit: 1, schema: 1, lien: 1, statut: 1, silence: 1)
116
+ ```
117
+
118
+ Exit codes: **0** valid (summary on the standard output; « lecture valide » for a reading), **1** deviations (one
119
+ `✗ <path> : <reason>` line per deviation then `N écart(s)`, **on the standard output**), **2** wrong call (not exactly one
120
+ argument) or unreadable file (absent, a folder, empty, not JSON, UTF-8 BOM included): the reason goes to the error output. A
121
+ duplicated JSON key is not detected (the last one wins, as with `JSON.parse`). An unknown key, at the root or in an element, is ignored.
122
+
123
+ **Root**: `version` (1), `genre`, `document` `{ slug (kebab-case), sha256 (64 lowercase hex) }`, `elements` (non-empty array), and, in an
124
+ expected inventory, `pages` (the number of pages of the document, an integer ≥ 1; not checked here, read by `ref eval` for the tokens per page).
125
+
126
+ **Every element**: `type` (`ecran`, `composant`, `etat`, `texte`, `jeton`, `table`, `gabarit`, `schema`, `lien`, `statut`, `silence`);
127
+ `id` = `<slug of document>#<type>-<n>` (n ≥ 1, no leading zero; the slug is compared as it is, even invalid, and two elements never share an id);
128
+ `source` `{ page ≥ 1, zone { x, y, l, h } }` in pixels of the page rendered at 200 dpi (x, y ≥ 0, l, h ≥ 1; the zone is optional only for a `silence`);
129
+ `citations` (optional) `[{ page, ligne, extrait }]`; `confiance` (`mesure`, `lu`, `incertain`); `cause` (non-empty, **only** on an
130
+ `incertain` element); `ocr` (optional boolean; in a reading an `ocr: true` element is never `lu`).
131
+
132
+ | Type | Own keys |
133
+ |---|---|
134
+ | `ecran` | `largeur_reference` (integer > 0, 1440 in the model), `zones` (non-empty) `[{ nom, x, y, l, h, pct { x, y, l, h } }]`: pixels at that width, `pct` numbers from 0 to 100 |
135
+ | `composant` | `role` (from the list of roles below), `position { x, y }` (≥ 0), `taille { l, h }` (≥ 1), `contenu` (text, may be empty), `ecran` (id of an `ecran`) |
136
+ | `etat` | `nom` (non-empty, open list), `cible` (id of a `composant` or an `ecran`) |
137
+ | `texte` | `libelle` (non-empty, no leading or trailing blank), `role` (from the list of roles below) |
138
+ | `jeton` | `categorie` (`couleur`, `taille_police`, `espacement`, `rayon`, `opacite`), `valeur` (non-empty string or finite number), `point` (`{ x, y }` or a zone `{ x, y, l, h }`; required when `confiance` is `mesure`), `contraste` (optional) `{ texte, fond, ratio 1 to 21 }` |
139
+ | `table` | `colonnes` (non-empty array of texts), `lignes` (non-empty, each row has as many text cells as columns) |
140
+ | `gabarit` | `nom`, `champs` (non-empty, ordered) `[{ nom, obligatoire (boolean), format, min?, max? }]`, `min ≤ max` |
141
+ | `schema` | `noeuds` (non-empty) `[{ id, libelle }]` (unique ids), `flux` `[{ de, vers, libelle, direction }]` (`de`, `vers` = node ids; `direction` = `de_vers` or `bidirectionnel`) |
142
+ | `lien` | `figure`, `passage { page, ligne, extrait }`, `constat` (`confirme`, `precise`, `contredit`); `precise` adds `lecture_ambigue`, `interpretation`, `raison`; `contredit` adds `enonces` (exactly two texts) and carries no `interpretation` |
143
+ | `statut` | `libelle`, `couvre` (non-empty array of ids of any type, itself included) |
144
+ | `silence` | `question` (non-empty, ends with `?`) |
145
+
146
+ **Roles** (`role` of a `texte` and of a `composant`): a **closed list**, because the role takes part in the identity of a component and
147
+ in the verdict on a text in `ref eval`. A role outside the list is a deviation of the schema (`« label » n'est pas un rôle de la
148
+ liste : …`), never a reading error counted later. The list is in `ROLES` of `src/ref/check.ts` and in the `roles` key of
149
+ [`templates/inventaire.json`](../templates/inventaire.json) (one line of definition per role; a test keeps both identical):
150
+
151
+ | Role | Definition |
152
+ |---|---|
153
+ | `titre` | title of a document, a page or a section |
154
+ | `titre-panneau` | title of a panel or a window of an interface |
155
+ | `paragraphe` | sentence or block of running text |
156
+ | `legende` | text that names a figure, a table or a colour scale (« Figure 1 — … ») |
157
+ | `etiquette-figure` | word or value written inside a figure (arrow, axis, curve, threshold) |
158
+ | `exigence` | statement of one requirement in a list of requirements |
159
+ | `ligne` | line of a record or a form, with no other function |
160
+ | `message_erreur` | message that reports an error or a refusal to the user |
161
+ | `unite` | unit attached to a value (kt, hPa, m) |
162
+ | `info_bulle` | short text shown on hover or focus of an element |
163
+ | `bouton` | control one acts on (`composant`), or its label (`texte`) |
164
+ | `champ` | area where a value is typed (`composant`), or its label (`texte`) |
165
+ | `case_a_cocher` | two-state control, ticked or not, independent of the others |
166
+ | `selecteur` | control that picks one value in a closed list (drop-down, radio buttons) |
167
+ | `onglet` | entry of a bar that switches between views of the same level |
168
+ | `menu` | list of actions or destinations that opens from a control |
169
+ | `liste` | sequence of items of the same kind shown one under the other |
170
+ | `panneau` | interface area that groups controls (`composant`) |
171
+ | `carte` | map or chart display area (`composant`) |
172
+ | `icone` | pictogram with no text that stands for an action or an object |
173
+ | `badge` | pill or mark that qualifies an element (status, counter) |
174
+
175
+ References (`ecran`, `cible`, `couvre`, flux nodes) are resolved on the whole list of ids, wherever the element is. Every slug and id
176
+ deviation (R6/R9) is listed, none masks another; elsewhere a deviation can hide others (a non-object element only reports its
177
+ `type`, an invalid `confiance` skips the `cause` check, an invalid `elements` validates no element). The key names of `ecran` and the values of `direction` are a format shared with the later lots
178
+ (they are hypotheses of the sheet `docs/specs/ref-inventaire-schema/fiche.yaml`).
179
+
180
+ ### Where the files go
181
+
182
+ Outside the repository: `references.dir` of `cadence.yaml` (relative to that file), by default
183
+ `$XDG_DATA_HOME/cadence/references` (`~/.local/share/cadence/references`). A `references.dir`
184
+ inside a Git repository is refused: the documents may be private. One folder per document:
185
+
186
+ ```
187
+ <references.dir>/<slug>/document.json source, sha256, pages (lines, boxes), images, figures, unresolved
188
+ <references.dir>/<slug>/pages/ page-<n>.png, 200 dpi
189
+ <references.dir>/<slug>/images/ p<page>-<n>.png
190
+ <references.dir>/<slug>/thumbs/ page-<n>.png, 40 dpi, every page
191
+ ```
192
+
193
+ ### System tools
194
+
195
+ `cadence ref` calls poppler (`pdftoppm`, `pdftotext`, `pdfimages`, `pdfinfo`, `pdftohtml`),
196
+ ImageMagick (`magick`, or `convert` for version 6) and tesseract. They are looked up in the
197
+ `PATH` before the document is read; each one missing is named with its install command and the
198
+ command exits **2**, writing nothing and installing nothing:
199
+
200
+ ```
201
+ ref : outil absent du PATH, à installer (rien n'est installé d'office) :
202
+ magick ou convert : sudo apt install imagemagick
203
+ ```
204
+
205
+ (`poppler-utils`, `imagemagick`, `tesseract-ocr`.) `add` does not call ImageMagick or tesseract, `measure` calls tesseract (and only for the text); both check all the tools at the first call, as the design asks.
206
+ ImageMagick is not called by either: `measure` decodes the rendered PNG pages itself. The rest of cadence depends on none of them.
207
+
208
+ Exit codes: 0 done, 2 refusal (tool, file, option, slug, folder).
209
+
210
+ ## `cadence ref eval <attendu> <lecture> [--json]`
211
+
212
+ Scores a reading against the inventory that is known to be true (the evaluation of the design, §« Mesure »). `attendu` is an
213
+ inventory (`genre: "inventaire"`), `lecture` a reading or an inventory of the same document; both go through the checks of
214
+ `ref check` first. Two folders work too (a file against a folder, or the reverse, is refused with exit 2 and the accepted combinations: two files, or two folders): `attendu` holds `<cas>/attendu.json` (what `ref corpus` writes) and `lecture`
215
+ `<cas>/lecture.json`; a case without a reading counts as read with nothing (recall 0) and is named under `sans lecture`.
216
+
217
+ An element read is matched to an expected one **by type and identity**, never by id or zone (two independent readings number
218
+ their elements their own way; blanks inside a text do not count, and the case does not either, except for a `texte`, which is verbatim: `120 KT` is not `120 kt`): `texte` and `statut` by `libelle`, `composant` by
219
+ `role` and `contenu`, `etat` by `nom` and the identity of its target, `jeton` by `categorie` and `valeur` (a colour is compared as
220
+ written), `table` by `colonnes`, `gabarit` by `nom`, `schema` by the labels of its nodes, `lien` by `figure`, `silence` by
221
+ `question`, `ecran` (one per document). An expected element is matched once. The matched element is **juste** if it also says the
222
+ same thing: the `role` of a `texte`, the `lignes` of a `table`, the `champs` of a `gabarit` (name, mandatory, format, bounds, in
223
+ order), the `flux` of a `schema`, the `constat` of a `lien` (the interpretation of a `precise` is not compared), the elements
224
+ covered by a `statut`, the `largeur_reference` of an `ecran`.
225
+
226
+ - **rappel** = juste / expected, **précision** = juste / read, both by type and overall;
227
+ - **faux lu** = read elements marked `lu` or `mesure` (« sûrs ») that are not juste, over the sûrs: an invented element, a wrong
228
+ cell, a `lien` that confirms what the figure contradicts. An `incertain` element is never a faux lu (it lowers the precision);
229
+ - **pièges**: an expected element carries `piege: "<name>"`; a piège is found when all its elements are juste (`✓ nom : 1/1`);
230
+ - **tokens par page**: the `usage: { tokens }` of the reading divided by the `pages` of the expected inventory (a root key of the
231
+ expected file, see the model), for the cases that have both; without `usage`, « non mesurés » (`null` in `--json`), never a zero.
232
+ A case whose expected inventory has **no `pages`** is left out of the figure, tokens and pages alike, and the output says so:
233
+ `tokens par page : N cas sans `pages` exclus (names)` (`tokens.exclus` in `--json`).
234
+
235
+ The output is the totals, then one line per type present (in the order of the eleven types), then the pièges (`✓` all found, `✗`
236
+ some missing) and, last, the cases without a reading. This one is a reading of the corpus that confirms a link the figure makes
237
+ precise, misreads a table cell, an opacity and a state, drops one line of the scan and counts 4,200 tokens for the one case that
238
+ reports `usage`:
239
+
240
+ ```
241
+ $ cadence ref eval tmp/corpus-ref lectures/
242
+ 7 cas · 47 éléments attendus · 46 lus
243
+ rappel : 89.4 % (42/47) · précision : 91.3 % (42/46)
244
+ faux lu : 9.3 % (4/43 sûrs)
245
+ tokens par page : 4200 (4200 sur 1 pages)
246
+ par type :
247
+ ecran rappel 100 % (2/2) · précision 100 % (2/2) · faux lu 0/2
248
+ composant rappel 100 % (5/5) · précision 100 % (5/5) · faux lu 0/5
249
+ etat rappel 50 % (1/2) · précision 50 % (1/2) · faux lu 1/2
250
+ texte rappel 96.7 % (29/30) · précision 100 % (29/29) · faux lu 0/26
251
+ jeton rappel 50 % (1/2) · précision 50 % (1/2) · faux lu 1/2
252
+ table rappel 0 % (0/1) · précision 0 % (0/1) · faux lu 1/1
253
+ gabarit rappel 100 % (1/1) · précision 100 % (1/1) · faux lu 0/1
254
+ lien rappel 66.7 % (2/3) · précision 66.7 % (2/3) · faux lu 1/3
255
+ statut rappel 100 % (1/1) · précision 100 % (1/1) · faux lu 0/1
256
+ pièges :
257
+ ✗ controle-translucide : 0/1
258
+ ✓ figure-contredit-texte : 1/1
259
+ ✓ gabarit-type-bufr : 1/1
260
+ ✓ page-proposal : 1/1
261
+ ✗ scan-bruite : 3/4
262
+ ✗ table-en-image : 0/1
263
+ ✗ texte-ambigu-leve-par-figure : 0/1
264
+ ```
265
+
266
+ Reading it: the `etat` line is the recall of the states (the threshold of the design is 95 % of the components **and** states);
267
+ in `faux lu 1/2` the second number is the number of sûrs read of that type (an `incertain` one, like the OCR texts of the scan,
268
+ is not in it). A misread affirmation (the table cell, the confirmed link) is an element found but not juste; a misread part of the identity (the opacity, the name of a state) is a missing element and an invented one; both cost recall and add a faux lu, while a dropped element (the scan line) costs recall only. The `✗` pièges name what a reader must work on: here the confirmed link, the cell and the
269
+ opacity are exactly the traps of those cases.
270
+
271
+
272
+ A reading whose `document.slug` or `document.sha256` differs from the expected one is refused (never a measure over two versions
273
+ mixed). Exit codes: **0** done, **2** refusal (wrong call, file absent or unreadable, invalid reading, other document, folder with
274
+ no `attendu.json`). `--json` prints one object: `total`, `types`, `pieges`, `tokens`, `cas`.
275
+
276
+ ## `cadence ref corpus <dossier> [--sans-rendu]`
277
+
278
+ Writes the **synthetic corpus with a known truth** that `ref eval` scores against (design §« Évaluation »): seven small documents,
279
+ one per trap taken from a real defect, each with the inventory that is true of what is drawn.
280
+
281
+ | Case (= trap) | What it draws | Expected `piege` element |
282
+ |---|---|---|
283
+ | `texte-ambigu-leve-par-figure` | « written beside the arrow » (head or tail?), the figure puts `120 KT` over the tail | `lien` `precise` |
284
+ | `figure-contredit-texte` | text says 80 kt, the figure draws the threshold at 100 kt | `lien` `contredit` |
285
+ | `table-en-image` | a code table rasterised at render time: its cells are not in the PDF text layer | `table` |
286
+ | `gabarit-type-bufr` | BUFR section 1: ordered fields, mandatory or optional, format, bounds | `gabarit` |
287
+ | `controle-translucide` | a white button at 25 % opacity on a striped gradient, next to an opaque one | `jeton` `opacite` |
288
+ | `page-proposal` | page 1 requirements, page 2 a panel mock-up carrying a « Proposal » badge; two rows of the panel are drawn selected and greyed (`etat` `selectionne`, `desactive`), and the caption confirms the requirement (`lien` `confirme`) | `statut` |
289
+ | `scan-bruite` | a station sheet rasterised, grainy and blurred: the PDF has no text layer, expected texts are `ocr` | `texte` (`ocr`) |
290
+
291
+ Each case folder holds `document.html` (one 8.5×11 in `section.page` per page, deterministic), `attendu.json` (an inventory that
292
+ passes `ref check`, slug `corpus-<case>`, `sha256` of the `document.pdf` once rendered (of the HTML with `--sans-rendu`, which writes no PDF), `pages`, zones in pixels at 200 dpi, `piege` on the trap elements), and,
293
+ unless `--sans-rendu`, `document.pdf` and `page-<n>.png` (1700×2200, 200 dpi) written by **Playwright** (Chromium). Texts of a table,
294
+ a template or a schema belong to it and are not listed as `texte`; labels of a figure are.
295
+
296
+ **One convention for the labels** (what a faithful reading must find, so that `ref eval` does not count it as a faux lu): every
297
+ visible text outside a table or a template is an expected `texte`, with its role, **including the label of a component**: the
298
+ button `Layers` is a `composant` (`bouton`, `contenu: "Layers"`) **and** a `texte` (`bouton`, `libelle: "Layers"`), the rows `Wind`
299
+ and `Waves` of the panel are `case_a_cocher` components and `case_a_cocher` texts, the `Proposal` badge is a `statut` and a `badge`
300
+ `texte`. The tests check it both ways: every label of the expected inventory is drawn, and every drawn text (the HTML, and the
301
+ words of `pdftotext` once rendered) has an expected `texte`. Playwright is not a dependency of cadence:
302
+ install it (`npm i -D playwright`, `npx playwright install chromium`) or point `CADENCE_PLAYWRIGHT` to the folder of the package;
303
+ without it, or when Chromium does not launch (`npx playwright install chromium` missing), the command exits **2** and writes nothing (`--sans-rendu` still writes HTML and expected files). An existing folder is
304
+ refused (exit 2). The corpus is written where you say, not in `evals/`, which `claude plugin eval` reads as case folders.
305
+
306
+ The tests of the real render (PDF and PNG sizes, page counts, hash, zones of the texts against `pdftotext -bbox`) run only when
307
+ `CADENCE_PLAYWRIGHT` points to the package; `.github/workflows/tests.yml` installs Playwright and Chromium and sets it. Locally:
308
+ `CADENCE_PLAYWRIGHT=<folder of the playwright package> npx vitest run test/ref-corpus.test.ts`.
309
+
310
+ The PDF changes at every render, so the expected `sha256` is the one of the PDF just written: register it with the slug of the
311
+ expected file (`cadence ref add tmp/corpus-ref/<case>/document.pdf --slug corpus-<case>`), or `ref eval` refuses the reading.
312
+
313
+ ```
314
+ $ cadence ref corpus tmp/corpus-ref
315
+ $ cadence ref eval tmp/corpus-ref lectures/ # lectures/<case>/lecture.json
316
+ ```