@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +23 -7
- package/agents/code-reviewer.md +2 -2
- package/agents/doc-keeper.md +19 -0
- package/agents/qa-reviewer.md +2 -2
- package/agents/spec-writer.md +1 -1
- package/agents/ux-reviewer.md +1 -1
- package/bin/cadence.js +9 -1
- package/dist/audit.js +14 -4
- package/dist/check.js +6 -0
- package/dist/cli.js +235 -30
- package/dist/config.js +101 -5
- package/dist/corrections.js +23 -2
- package/dist/deliver.js +78 -21
- package/dist/docsync.js +19 -3
- package/dist/git.js +16 -3
- package/dist/lead.js +37 -12
- package/dist/migrate.js +166 -0
- package/dist/orchestrate/briefs.js +28 -6
- package/dist/orchestrate/command.js +303 -34
- package/dist/orchestrate/cycle.js +720 -273
- package/dist/orchestrate/guard.js +2 -2
- package/dist/orchestrate/integrate.js +85 -0
- package/dist/orchestrate/launch.js +37 -5
- package/dist/orchestrate/mutex.js +30 -0
- package/dist/orchestrate/pool.js +74 -36
- package/dist/orchestrate/registry.js +123 -14
- package/dist/orchestrate/result.js +1 -1
- package/dist/orchestrate/snapshot.js +25 -0
- package/dist/orchestrate/spec.js +89 -5
- package/dist/orchestrate/state.js +42 -6
- package/dist/orchestrate/table.js +61 -2
- package/dist/orchestrate/timeouts.js +40 -0
- package/dist/orchestrate/worktree.js +368 -0
- package/dist/perf.js +80 -19
- package/dist/plan.js +92 -6
- package/dist/ready-free.js +109 -0
- package/dist/ref/add.js +116 -0
- package/dist/ref/check.js +338 -0
- package/dist/ref/command.js +78 -0
- package/dist/ref/corpus-render.js +83 -0
- package/dist/ref/corpus.js +201 -0
- package/dist/ref/evaluate.js +241 -0
- package/dist/ref/figures.js +179 -0
- package/dist/ref/measure.js +330 -0
- package/dist/ref/pdf.js +104 -0
- package/dist/ref/png.js +125 -0
- package/dist/ref/store.js +40 -0
- package/dist/ref/tools.js +51 -0
- package/dist/resources.js +51 -0
- package/dist/session.js +5 -2
- package/dist/state.js +60 -7
- package/dist/testgate.js +15 -0
- package/docs/cadence-hud.md +12 -0
- package/docs/cycle-vague-dark.svg +1 -1
- package/docs/cycle-vague-light.svg +1 -1
- package/docs/deliver.md +58 -3
- package/docs/fonctionnement-wave-dark.svg +1 -1
- package/docs/fonctionnement-wave-light.svg +1 -1
- package/docs/fonctionnement.md +5 -5
- package/docs/lead.md +35 -12
- package/docs/news.md +24 -1
- package/docs/orchestrate.md +230 -19
- package/docs/perf.md +13 -8
- package/docs/raf.md +89 -11
- package/docs/ref.md +316 -0
- package/docs/session.md +2 -2
- package/docs/skills.md +9 -2
- package/docs/spec.md +1 -1
- package/docs/stabilite.md +4 -4
- package/docs/whats-new.md +2 -0
- package/package.json +2 -1
- package/skills/deliver/SKILL.md +6 -1
- package/skills/lead/SKILL.md +24 -6
- package/skills/session-close/SKILL.md +1 -1
- package/skills/session-start/SKILL.md +2 -2
- package/templates/inventaire.json +96 -0
- package/templates/orchestrate/accept-back.md +1 -0
- package/templates/orchestrate/accept.md +1 -0
- package/templates/orchestrate/fiche.md +1 -0
- package/templates/orchestrate/fix-docs.md +18 -0
- package/templates/orchestrate/fix-minors.md +2 -1
- package/templates/orchestrate/fix.md +5 -2
- package/templates/orchestrate/implement.md +6 -3
- package/templates/orchestrate/maquette.md +2 -1
- package/templates/orchestrate/precheck.md +2 -1
- package/templates/orchestrate/review-recheck.md +2 -0
- package/templates/orchestrate/review-small.md +5 -1
- package/templates/orchestrate/review.md +2 -0
- package/templates/orchestrate/spec-back.md +2 -1
- package/templates/orchestrate/spec.md +4 -3
- 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`
|
|
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
|
|
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
|
|
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 #
|
|
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 #
|
|
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
|
|
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
|
|
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
|
|
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
|
+
```
|