@sylad/cadence 1.31.0 → 1.32.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.
@@ -8,7 +8,7 @@
8
8
  {
9
9
  "name": "cadence",
10
10
  "description": "Session start and close rituals driven by a versioned plan (raf), and deliveries proven by their effect. Needs the cadence CLI (npm i -g @sylad/cadence).",
11
- "version": "1.31.0",
11
+ "version": "1.32.0",
12
12
  "source": "./",
13
13
  "author": {
14
14
  "name": "Sylvain Ladoire"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cadence",
3
3
  "description": "A repo-native working method: session start and close rituals driven by a versioned plan (raf), deliveries proven by their effect, and three reviewer agents (UX, code, QA).",
4
- "version": "1.31.0",
4
+ "version": "1.32.0",
5
5
  "author": { "name": "Sylvain Ladoire" },
6
6
  "homepage": "https://github.com/Sylad/cadence",
7
7
  "repository": "https://github.com/Sylad/cadence",
package/README.md CHANGED
@@ -55,7 +55,7 @@ Details: [docs/skills.md](docs/skills.md) and [docs/cadence-hud.md](docs/cadence
55
55
  - [docs/lead.md](docs/lead.md): the lead workflow, and the `livreur` agent that delivers a ready lot for it.
56
56
  - [docs/overview.md](docs/overview.md): the longer introduction and the map of every page.
57
57
  - [docs/fonctionnement.md](docs/fonctionnement.md): how it all works, in four figures.
58
- - [docs/stabilite.md](docs/stabilite.md): what is guaranteed from 1.0.0.
58
+ - [docs/stabilite.md](docs/stabilite.md): what is guaranteed from 1.0.0; [docs/qualite.md](docs/qualite.md): the code-quality ratchet.
59
59
  - [docs/whats-new.md](docs/whats-new.md) and [CHANGELOG.md](CHANGELOG.md): what changed.
60
60
 
61
61
  ## License
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import { basename, dirname, join, posix, relative, resolve } from 'node:path';
4
4
  import { parseArgs } from 'node:util';
5
5
  import { defaultPlan, PLAN_DEFAULT, PLAN_LEGACY, readDocsConfig, readHookConfig, readNewsConfig, readOrchestrateConfig, readPerfConfig, readPlanConfig, readSessionConfig, readVerifyConfig } from './config.js';
6
6
  import { repoSections, repoShas, resolveLotRepos } from './repos.js';
7
- import { audit, exemptPlanOnly, gateProgress, gateText, gateWaiting, hypothesesOf, ownFiles, isPlanOnly, lotWork, nextUp, planCommits, unreviewedWork, workByLot } from './audit.js';
7
+ import { audit, exemptPlanOnly, gateProgress, gateText, gateWaiting, hypothesesOf, isNonConformingVerdict, ownFiles, isPlanOnly, lotWork, nextUp, planCommits, unreviewedWork, workByLot } from './audit.js';
8
8
  import { short } from './check.js';
9
9
  import { isDay, toDay } from './dates.js';
10
10
  import { deliver, parseDeliverConfig, realDeps, shellQuote } from './deliver.js';
@@ -461,7 +461,9 @@ function dispatch(argv, io) {
461
461
  }
462
462
  }
463
463
  const unreviewed = status === 'done' && plan.reviewSince && !plan.readonly && !values.force ? unreviewedWork(plan, root, rest[0]) : 0;
464
- plan.setStatus(rest[0], status, today, { force: values.force, unreviewed });
464
+ const verdict = status === 'done' && !rest[0].includes('/') ? plan.lots().find((l) => l.id === rest[0])?.review?.verdict : undefined;
465
+ const nonConforming = verdict !== undefined && isNonConformingVerdict(verdict);
466
+ plan.setStatus(rest[0], status, today, { force: values.force, unreviewed, nonConforming });
465
467
  if (command === 'drop' && values.reason)
466
468
  plan.note(rest[0], `abandonné : ${values.reason}`, today);
467
469
  plan.save();
package/dist/plan.js CHANGED
@@ -704,7 +704,8 @@ export class Plan {
704
704
  /**
705
705
  * `ref` is `L3` or `L3/t1`. `unreviewed` : nombre de commits du lot que sa revue de code ne couvre
706
706
  * pas (le plan ne lit pas git) — tous sans verdict, ceux postérieurs au commit relu sinon ; sans lui,
707
- * la revue de code n'est pas exigée.
707
+ * la revue de code n'est pas exigée. `nonConforming` : le verdict enregistré dit « non conforme » (le texte du
708
+ * verdict se juge dans `audit.ts`, `isNonConformingVerdict`) — refusé même si la revue couvre tous les commits.
708
709
  */
709
710
  setStatus(ref, status, today, opts = {}) {
710
711
  this.writable();
@@ -736,6 +737,9 @@ export class Plan {
736
737
  ? `${lotId} : la revue de code précède ${opts.unreviewed} commit(s) du lot, elle est à refaire — raf review ${lotId} "verdict" (--force pour passer outre)`
737
738
  : `${lotId} a ${opts.unreviewed} commit(s) : revue de code attendue avant done — raf review ${lotId} "verdict" (--force pour passer outre)`);
738
739
  }
740
+ if (this.reviewSince && opts.nonConforming && lot.review && !opts.force) {
741
+ throw new RafError(`${lotId} : la revue de code enregistrée est non conforme (« ${lot.review.verdict} ») — corriger puis refaire la revue : raf review ${lotId} "verdict" (--force pour passer outre)`);
742
+ }
739
743
  }
740
744
  node.set('status', status);
741
745
  if (!lot.started && status !== 'dropped')
package/docs/overview.md CHANGED
@@ -71,8 +71,9 @@ The two plugins are independent. `cadence@cadence` carries the rituals, which ca
71
71
  | [docs/lead.md](lead.md) | the lead workflow and `cadence lead tour` |
72
72
  | [docs/ref.md](ref.md) | reference documents: `cadence ref add` (rendered pages, positioned text, images, figure index), `cadence ref measure` (colours, WCAG contrast, boxes, OCR, font size), `cadence ref check` (the inventory schema, `templates/inventaire.json`), `cadence ref eval` (recall, precision, false « read », tokens per page against a known inventory), `cadence ref corpus` (the synthetic corpus with a known truth, rendered by Playwright) `cadence ref coverage` (which elements of the inventory no done lot covers, nor a written exclusion), `cadence ref diff` (reconciles two readings of a document by program) and `cadence ref analyse` (one reading by the `reference-analyst` agent; `--double` for two independent readings, `ref diff`, a third look by `reference-verifier` and the reconciled inventory, an option whose measurement showed no gain; 20 pages at most per reading) |
73
73
  | [docs/stabilite.md](stabilite.md) | what is guaranteed from 1.0.0 and what is not |
74
+ | [docs/qualite.md](qualite.md) | code-quality guard rails: ESLint (typescript-eslint, sonarjs), knip and jscpd compared to a versioned baseline by a ratchet (`npm run qualite`, `--init`, `--ecrire`, `--migrer`, `--contre`), in the wave's gate and the CI; the recipe for TypeScript, Python, Java and shell |
74
75
 
75
- Also: [news](news.md) (changelog, UX, code and QA reviews, documentation that follows the code), [session](session.md), [corrections](corrections.md), [perf](perf.md), [skills and agents](skills.md), [cadence-hud](cadence-hud.md), [releasing](releasing.md), [overview](overview.md), [how it all works](fonctionnement.md) (four figures).
76
+ Also: [news](news.md) (changelog, UX, code and QA reviews, documentation that follows the code), [quality](qualite.md), [session](session.md), [corrections](corrections.md), [perf](perf.md), [skills and agents](skills.md), [cadence-hud](cadence-hud.md), [releasing](releasing.md), [overview](overview.md), [how it all works](fonctionnement.md) (four figures).
76
77
 
77
78
  <details>
78
79
  <summary>The four tools in short</summary>
@@ -0,0 +1,130 @@
1
+ # Code-quality guard rails
2
+
3
+ What Checkstyle, PMD and Sonar do for a Java project, cadence does for its own code with four tools and a **ratchet**.
4
+ This page is also a recipe that does not depend on cadence: the same guard rails for TypeScript, Python, Java and shell,
5
+ to apply to any project (each project does it in a lot of its own).
6
+
7
+ ## The four tools and what each one is for
8
+
9
+ | Tool | Question it answers | What it measures | Starting threshold |
10
+ |---|---|---|---|
11
+ | [ESLint](https://eslint.org) + [typescript-eslint](https://typescript-eslint.io) | Is this unit too big or too tangled to read? | `max-lines` (file), `max-lines-per-function`, `max-depth`, `max-params`, `complexity` (cyclomatic) | 400 lines per file, 80 per function, depth 4, 5 parameters, complexity 15 |
12
+ | [eslint-plugin-sonarjs](https://github.com/SonarSource/SonarJS) | Is it hard to *follow*? | `sonarjs/cognitive-complexity` (nesting weighs more than a flat `switch`) | 15 |
13
+ | [knip](https://knip.dev) | Is something there that nobody uses? | unused files, exports, types, dependencies and binaries | none: any finding counts |
14
+ | [jscpd](https://github.com/kucherenko/jscpd) | Was this block copied? | duplicated blocks across files | 8 lines and 70 tokens |
15
+
16
+ Blank lines and comments do not count towards the length rules. The configuration is in `eslint.config.mjs`, `knip.json`
17
+ and `.jscpd.json`; the tests (`test/`) and the Claude Code plugin (`plugins/`) are not linted — the tests are knip entry
18
+ points, so what they import is not reported as dead. `jscpd` fails on a `path` that does not exist: keep the list in
19
+ `.jscpd.json` to the folders the project really has.
20
+
21
+ ## The ratchet
22
+
23
+ Putting a threshold on existing code turns hundreds of findings red on day one, and the usual answers are bad: raise the
24
+ threshold until it means nothing, or sprinkle `eslint-disable`. The ratchet keeps the thresholds honest instead:
25
+
26
+ 1. `node scripts/qualite.mjs --init` measures once and writes `qualite-baseline.json`, a flat map sorted by key,
27
+ versioned with the code. A key is `tool|file|rule` (`eslint|src/cli.ts|max-lines`, `knip|src/audit.ts|exports`,
28
+ `jscpd|src/config.ts`) and its value a count. Two metrics have a `…|total` or `…|lignes` twin that sums the figures:
29
+ for ESLint, every key has a `…|total` (the sum of the figures measured, a file of 1397 lines records 1397, three
30
+ functions of cognitive complexity 463, 20 and 18 record 501), and for jscpd every file has a `…|lignes` (the lines
31
+ of the duplicated blocks that touch it). A function that grows without passing the worst of its file, or a new
32
+ function that replaces a small one, makes the sum go up: the count alone would not see it.
33
+ 2. `npm run qualite` measures again and compares key by key. A key that is **higher** than in the baseline, or absent from
34
+ it (a new file, a rule never broken before), makes the command fail (exit 1) and names it with its old and new value.
35
+ The existing code passes; nothing can get worse; a touched file can only go down.
36
+ 3. A measure that went **down** passes, with a reminder. `npm run qualite:cliquet` (`--ecrire`) writes the gain into the
37
+ baseline — and refuses, leaving the file untouched, if anything got worse. `--init` refuses to run when the baseline
38
+ exists: the baseline never goes up, it is only ever lowered.
39
+ 4. A tool that crashes or whose output cannot be read fails the command (exit 2); it never counts as « zero finding ».
40
+ A missing baseline also exits 2, and says how to create it.
41
+
42
+ Counts, not line numbers, are compared: moving code inside a file does not move anything.
43
+
44
+ **Moving or splitting a file** (a 3000-line file cut into three modules, a function moved to its own file) is the one
45
+ legitimate way to get new paths in the baseline, and it has its own command: `node scripts/qualite.mjs --migrer --contre
46
+ main`. It measures, and rewrites the baseline only if (a) no key of a file that **existed at the start of the lot** went
47
+ up, and (b) the total of every measure over all files (sum of the `…|total`, `…|lignes` and knip counts per tool and
48
+ rule) did not go up. A new path starts free — its own `max-lines` finding is fine when the file it was cut from lost
49
+ more — but a defect cannot be moved into an existing file, nor added. The same rule is what `--contre` applies to the
50
+ baseline of the lot, so a migrated baseline passes the gate and the CI, and a hand-edited one does not. It needs
51
+ `--contre` (the files that « existed » are those of the start of the lot) and exits 2 without it.
52
+ The way out of a finding is to fix it and lock the gain. Two other ways would also lower the measure without fixing
53
+ anything, and the ratchet closes them:
54
+
55
+ - **A directive**: `eslint.config.mjs` sets `linterOptions.noInlineConfig`, so an `eslint-disable` comment disables
56
+ nothing (ESLint reports it, and the ratchet counts that report under `analyse`). The directives that the tools cannot
57
+ refuse (`jscpd:ignore-start`/`-end`, knip's `@public`/`@internal` tags) and any `eslint-disable` comment are counted per
58
+ file under `directive|file|name`, so adding one makes the measure go up like a new finding. Only the files the tool
59
+ concerned analyses are read (the files ESLint linted, the sources jscpd scanned, knip's `project` globs minus `ignore`):
60
+ a directive in a file the tool does not read disables nothing and is not counted.
61
+ - **A threshold or an exclusion**: under `--contre`, every file the tools read must be byte-identical to the start of
62
+ the lot — present, absent or changed the same way (exit 1 otherwise, naming each one as `nouveau`, `supprimé` or
63
+ `modifié`). That is not only the three files this repository uses: ESLint 9 loads `eslint.config.js` **before**
64
+ `eslint.config.mjs` (then `.cjs`, `.ts`, `.mts`, `.cts`), knip reads `knip.json`, `knip.jsonc`, `.knip.json`,
65
+ `.knip.jsonc`, `knip.ts`, `knip.js`, `knip.config.ts`, `knip.config.js` and `package.json#knip`, jscpd reads
66
+ `.jscpd.json` and `package.json#jscpd`, and knip as jscpd respect `.gitignore`. Of `package.json`, only the `knip` and
67
+ `jscpd` keys are compared. Changing a threshold is a decision of the plan, delivered alone; a lot judged by those
68
+ thresholds cannot move them. The lot that adopts the tools (no baseline at its start) introduces its configs freely.
69
+
70
+ **When the start of the lot has no baseline yet** (the lot that introduces the ratchet, while `main` moves under it):
71
+ the baseline measured at the start no longer matches the code once `main` is merged or the lot rebased, since `main`'s
72
+ own commits changed the figures, and the gate would hand back code the lot did not write. There is nothing to not
73
+ exceed in that case, so `node scripts/qualite.mjs --init --contre main` re-measures at the rebased tip and rewrites the
74
+ baseline (`--init` refuses as soon as `main` has a baseline: from then on `--ecrire` and `--migrer` are the only ways).
75
+ The refusal itself says so: when the start of the lot has no baseline, the list of measures that got worse ends with
76
+ that command.
77
+
78
+ The baseline is also the project's list of cleanups: each cleanup lot is measured by what it removes from it.
79
+
80
+ ## Where it runs
81
+
82
+ - **The wave's gate**: `cadence.yaml` declares `orchestrate.build: npx tsc --noEmit && node scripts/qualite.mjs --contre main`. The
83
+ build runs before the tests ([orchestrate.md](orchestrate.md)); a worse measure hands the lot back as a « build » finding,
84
+ and the session is told the exact command.
85
+ - **The CI**: `.github/workflows/tests.yml` runs `npm run qualite -- --contre origin/main` after the typecheck (the checkout is full-depth), and `prepublishOnly` runs it too.
86
+
87
+ ## Recipe for another project
88
+
89
+ Do it in one lot per project: install, configure, `--init`, wire into the gate and the CI, commit the baseline.
90
+ `scripts/qualite.mjs` is generic apart from its three readers (`lireEslint`, `lireKnip`, `lireJscpd`): copy it, and
91
+ change `mesurer` to the tools of the project. Whatever the language, a tool joins the ratchet once its report is turned
92
+ into `tool|file|rule → count`.
93
+
94
+ ### TypeScript / JavaScript
95
+
96
+ `npm i -D eslint typescript-eslint eslint-plugin-sonarjs knip jscpd`, then copy `eslint.config.mjs`, `knip.json`
97
+ and `.jscpd.json` of this repository and adapt `files`, `entry`, `project` and `path`. Add the `qualite` scripts to
98
+ `package.json`. In a monorepo, run the measure per workspace and prefix the keys with it.
99
+
100
+ ### Python
101
+
102
+ | Role | Tool | Command |
103
+ |---|---|---|
104
+ | complexity and size | [ruff](https://docs.astral.sh/ruff/) | `ruff check --select C901,PLR0912,PLR0913,PLR0915 --output-format json` (`C901` is McCabe, `PLR0913` too many arguments, `PLR0915` too many statements); threshold: `[tool.ruff.lint.mccabe] max-complexity = 15` |
105
+ | complexity, maintainability | [radon](https://radon.readthedocs.io) | `radon cc -j -n C src` (blocks of rank C or worse), `radon mi -j -n B src` |
106
+ | dead code | [vulture](https://github.com/jendrikseipp/vulture) | `vulture src --min-confidence 80` |
107
+ | duplicates | jscpd, which also reads Python | `format: ["python"]` in `.jscpd.json` |
108
+
109
+ ### Java (Maven)
110
+
111
+ | Role | Tool | Where |
112
+ |---|---|---|
113
+ | size and complexity | [Checkstyle](https://checkstyle.org) | `maven-checkstyle-plugin` with `FileLength`, `MethodLength`, `NestedIfDepth`, `ParameterNumber`, `CyclomaticComplexity` in the ruleset |
114
+ | smells, cognitive complexity, dead code | [PMD](https://pmd.github.io) | `maven-pmd-plugin`, rules `CognitiveComplexity`, `CyclomaticComplexity`, `UnusedPrivateField`, `UnusedPrivateMethod`, `UnusedLocalVariable` |
115
+ | duplicates | PMD CPD | `pmd:cpd-check` (`minimumTokens` 100 to start) |
116
+ | probable bugs | [SpotBugs](https://spotbugs.github.io) | `spotbugs-maven-plugin`, `check` goal |
117
+
118
+ Both Checkstyle and PMD write XML reports (`target/checkstyle-result.xml`, `target/pmd.xml`): count the violations per
119
+ file and rule from them. Bind `check` goals to the `verify` phase so that `mvn verify` is the gate's build command.
120
+
121
+ ### Shell
122
+
123
+ [shellcheck](https://www.shellcheck.net): `shellcheck -f json1 scripts/*.sh`, one key per file and code (`SC2086`…).
124
+ Shell has no complexity rule: keep a script small, and move logic into a language that has the tools above.
125
+
126
+ ## Choosing the thresholds
127
+
128
+ Start from the table above, measure, and fix the baseline **where the code is**: a threshold chosen to fit the existing
129
+ code is not a guard rail. Lower a threshold only when the baseline for it is empty or near, and never raise one: a
130
+ threshold that goes up is a decision, written in the plan.
package/docs/raf.md CHANGED
@@ -62,7 +62,7 @@ raf gantt # .cadence/gantt.html (next to the plan)
62
62
  | `raf add`, `raf note`, `raf done` during an integration (L425) | while a wave integrates a lot of the repository (the integration lock is held by a live process), these three commands write the plan as usual and print on stderr `avertissement : intégration en cours (vague …, pid …) — …`: the plan is a tracked file of the main tree, and an integration that sees it modified refuses its lot (`arbre du dépôt modifié`, 2026-10-29: a `raf add` of the lead during the integration of L416). Commit the plan right away. Exit code 0; nothing is printed in a lot's worktree (its plan is not the tree the integration checks) nor when the lock belongs to a dead process |
63
63
  | `raf add`, `raf note`, `raf done` while a wave works in the main tree (L475) | when the repository's orchestration lock is held by a live wave and one of its lots is in `implementing`, `reviewing` or `fixing` **without a worktree** (a wave without worktrees, or a lot whose worktree folder does not exist), these commands write the plan as usual and print on stderr `avertissement : la vague … travaille dans l'arbre principal de ce dépôt (projet:lot) — …`: a plan commit of the lead then hands the lot back at review (2026-10-30: cadence L458 handed back by a plan commit during its review). Write the plan without committing it, or commit once the step ends. Exit code 0; nothing is printed for lots that are queued, in question, ready, handed back, failed or suspended, for lots playing in their worktree, from a worktree of the repository, from a session of the wave itself (`CADENCE_ORCHESTRATED` set), when the lock belongs to a dead process, or when the wave's state is unreadable |
64
64
  | `raf public <id> "title"` · `raf public <id> --clear` | set, replace or remove the public title of a lot |
65
- | `raf start <id>` · `raf done <id> [--force]` · `raf drop <id> [--reason text]` | dated transitions (`done` refuses open sub-tasks unless `--force`, and a lot whose `acceptation-<lot>.json` is still `vert: false`, L366: closed like that it would lock every later wave of the repository, see `cadence spec green`; with `--force` it only warns on stderr. Like every `raf` command it only writes the plan file and **never commits** it: when the plan or a file `plan.onWrite` produces is left with changes, `done` prints the `git add` / `git commit` command that takes them, the subject from the `plan.commit` template, L371) |
65
+ | `raf start <id>` · `raf done <id> [--force]` · `raf drop <id> [--reason text]` | dated transitions (`done` refuses open sub-tasks unless `--force`, with the code-review gate active (`raf review enable`) a lot whose commits its review does not cover, **and a lot whose recorded review verdict says `non conforme` / `not compliant` even when it covers every commit** (L476: `non conforme : 1 bloquant` at the last commit no longer closes; `conforme après 1 correction` is compliant; redo the review with `raf review <id> "verdict"` after the fix, or `--force`), and a lot whose `acceptation-<lot>.json` is still `vert: false`, L366: closed like that it would lock every later wave of the repository, see `cadence spec green`; with `--force` it only warns on stderr. Like every `raf` command it only writes the plan file and **never commits** it: when the plan or a file `plan.onWrite` produces is left with changes, `done` prints the `git add` / `git commit` command that takes them, the subject from the `plan.commit` template, L371) |
66
66
  | `cadence spec green <lot>` | L366: run the dedicated acceptance command (`orchestrate.accept`) on a lot driven or closed by hand; green, set `vert: true` in its `acceptation-<lot>.json` and commit that file alone (subject `chore(<lot>): acceptation verte, vert: true posé par cadence spec green` or the `plan.commit` template; the resealed tests are listed in the commit body, never in the subject), resealing the fingerprints of the listed tests refined since the red commit (journal `resceaux`) so the lock goes quiet; a listed test changed but not committed is refused. Red: exit 1, nothing changed. Refused for a `todo` lot, during a wave, and while an unfinished wave keeps a lot of the repository open; see [orchestrate](orchestrate.md) |
67
67
  | `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`), or on a sub-task; sub-tasks and other keys are left as they are. **Reopening** (L396): on a `done` or `dropped` lot, `raf todo` is THE command — never the YAML — that reopens it (`→ todo`, `finished:` removed, `started:` kept so its commits stay its own, dated note « rouvert (était done) : reason »); `raf start`/`raf done`/`raf drop` on a closed lot say so (`L3 est done ; le rouvrir si c'est voulu : raf todo L3`). An archived lot reopened this way comes back into the plan file at that write |
68
68
  | `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) |
package/docs/releasing.md CHANGED
@@ -33,8 +33,8 @@ A version exists in three places and is published in two; a release does all of
33
33
  on that version commit; a case below the threshold stops the release until you have read it in the report.
34
34
  4. `git tag v<version> && git push origin main v<version>` — the tag starts `.github/workflows/publish.yml`,
35
35
  which publishes to npm through Trusted Publishing (OIDC, no token stored anywhere): it checks the tag
36
- matches `package.json`, `.claude-plugin/plugin.json` and `.claude-plugin/marketplace.json` and that `CHANGELOG.md` has a `## [x.y.z]` section for it (no section, no publication), then `npm publish --provenance`, where `prepublishOnly` runs the type-check and the
37
- tests and `prepare` builds `dist/`; a red suite stops the publication; then it creates the GitHub release with that CHANGELOG section as its text. The trusted publisher is declared
36
+ matches `package.json`, `.claude-plugin/plugin.json` and `.claude-plugin/marketplace.json` and that `CHANGELOG.md` has a `## [x.y.z]` section for it (no section, no publication), then `npm publish --provenance`, where `prepublishOnly` runs the type-check, the quality ratchet (`npm run qualite`, [qualite.md](qualite.md): exit 1 when a measure got worse, 2 when a tool crashed or the baseline is missing) and the
37
+ tests and `prepare` builds `dist/`; a red suite or a measure that got worse stops the publication (fix the code, then `npm run qualite:cliquet` locks the gain); then it creates the GitHub release with that CHANGELOG section as its text. The trusted publisher is declared
38
38
  once on npmjs.com (package settings → Trusted Publisher → GitHub Actions, `Sylad/cadence`, `publish.yml`).
39
39
  5. Watch the run: `gh run watch` (or `gh run list --workflow publish.yml`).
40
40
  6. Check the effect: `npm view @sylad/cadence version` answers the new version.
package/docs/whats-new.md CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  ## In two lines
6
6
 
7
+ - **1.32.0**: code-quality guard rails in a ratchet (ESLint, sonarjs, knip, jscpd vs a versioned baseline), a recipe for TS, Python, Java, shell; `raf done` refuses a lot whose recorded review is `non conforme`.
7
8
  - **1.31.0**: `deliver --sha` refuses, outside a wave too, a sha that carries commits of a lot neither done nor ready (a `non conforme` verdict is not ready); `raf add/note/done` warn when a wave session writes in the main tree.
8
9
  - **1.30.0**: a question can be answered while its wave still runs; the sibling-repository check of 1.29.0 only flags commits that cite a lot of the wave; `session context` reads the lead's threshold from the lead's folder.
9
10
  - **1.29.0**: a wave session can no longer commit on the main branch of another repository than its lot's (the lot is handed back with the finding); `verify --gate` no longer links data folders into its throwaway copy.
@@ -58,6 +59,8 @@ Each is a paragraph in [docs/whats-new.md](whats-new.md) and a full section in [
58
59
 
59
60
  The long form of « In two lines » above (the README no longer carries a « What's new » section since L326: it links here). The full history, with the date of each version, is in [CHANGELOG.md](../CHANGELOG.md).
60
61
 
62
+ **1.32.0**: code-quality guard rails for cadence itself and a reference recipe for any project (L462, `docs/qualite.md`): ESLint with typescript-eslint and sonarjs (file and function length, depth, parameters, cyclomatic and cognitive complexity), knip (dead code, unused exports and dependencies) and jscpd (duplicates), compared by `npm run qualite` to a versioned baseline — existing code passes, nothing may get worse, a file a lot touches can only improve; `--migrer` is the admitted path to move or split a file of the baseline, and a changed tool configuration or a new suppression directive is refused. The recipe gives the same ratchet for Python, Java and shell. `raf done` refuses, without `--force`, a lot whose recorded code-review verdict is non-compliant, even when the review covers all its commits (L476, `docs/raf.md`), the predicate `cadence deliver --sha` uses since 1.31.0.
63
+
61
64
  **1.31.0**: `cadence deliver --sha <sha>` refuses, outside a wave too, a sha whose history since the last delivery carries work commits of a lot that is neither done nor ready — ready meaning a compliant code review that covers the lot's commits up to that sha, so a fix stacked above a reviewed sha does not block it and a `non conforme` verdict does — and `--dry-run` lists the lots of the interval with their state (L474, `docs/deliver.md`). Before, the check only ran during a wave of the repository: on 30-10 a sha delivered as « L49 alone » took two unreviewed lots whose commits sat beneath it. `raf add`, `raf note` and `raf done` warn on stderr when a session of a live wave writes in the main tree of the repository (L475, `docs/raf.md`): a plan commit of the lead at that moment hands the lot back, as it did on 30-10.
62
65
 
63
66
  **1.30.0**: `cadence orchestrate --answer <project>:<lot> "…"` (or `--resume --wave <wave> --answer`) is accepted while the wave is still running (L407, `docs/orchestrate.md`): the answer goes into a queue the live wave reads, the lot takes a slot again under the wave's cap, plays in its own worktree when the mode is on, and never runs beside another session of the same repository; the registry counts it at once, so « créneaux libres » stays right. Before, the lead had to wait for the end of the wave. The check of 1.29.0 against commits on the main branch of a sibling repository (L456) now flags only the commits that cite a lot of the wave (L458): a delivery's tag bump in a GitOps repository, the lead's plan commits and the lots of another live wave launched later no longer hand a lot back — three false positives on 30-10. `cadence session context` reads `lead.contextThreshold` from the lead's folder, the first parent holding `lead:` or `priority:`, instead of the current folder (L457).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylad/cadence",
3
- "version": "1.31.0",
3
+ "version": "1.32.0",
4
4
  "description": "A small, repo-native working method: a versioned plan linked to your commits, a changelog with screenshots, session rituals and deliveries proven by their effect.",
5
5
  "license": "MIT",
6
6
  "author": "Sylvain Ladoire",
@@ -31,6 +31,7 @@
31
31
  "docs/skills.md",
32
32
  "docs/cadence-hud.md",
33
33
  "docs/releasing.md",
34
+ "docs/qualite.md",
34
35
  "docs/overview.md",
35
36
  "docs/whats-new.md",
36
37
  "docs/fonctionnement.md",
@@ -62,16 +63,23 @@
62
63
  "service": "node scripts/mise-en-service.mjs",
63
64
  "test": "vitest run",
64
65
  "typecheck": "tsc --noEmit",
66
+ "qualite": "node scripts/qualite.mjs",
67
+ "qualite:cliquet": "node scripts/qualite.mjs --ecrire",
65
68
  "eval:plugin": "claude plugin eval . --max-cost-usd 5 --no-publish",
66
69
  "prepare": "tsc -p tsconfig.build.json",
67
- "prepublishOnly": "npm run typecheck && npm test"
70
+ "prepublishOnly": "npm run typecheck && npm run qualite && npm test"
68
71
  },
69
72
  "dependencies": {
70
73
  "yaml": "^2.5.0"
71
74
  },
72
75
  "devDependencies": {
73
76
  "@types/node": "^22.0.0",
77
+ "eslint": "^9.39.5",
78
+ "eslint-plugin-sonarjs": "^3.0.7",
79
+ "jscpd": "^4.3.0",
80
+ "knip": "^5.88.1",
74
81
  "typescript": "^5.5.0",
82
+ "typescript-eslint": "^8.72.1",
75
83
  "vitest": "^2.0.0"
76
84
  },
77
85
  "repository": {