@sylad/cadence 1.30.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/dist/audit.js +37 -1
- package/dist/cli.js +27 -3
- package/dist/deliver.js +22 -1
- package/dist/orchestrate/main-tree.js +34 -0
- package/dist/plan.js +5 -1
- package/docs/deliver.md +9 -0
- package/docs/overview.md +3 -2
- package/docs/qualite.md +130 -0
- package/docs/raf.md +2 -1
- package/docs/releasing.md +2 -2
- package/docs/whats-new.md +6 -0
- package/package.json +10 -2
|
@@ -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.
|
|
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.
|
|
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/audit.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import { dirname, join, relative } from 'node:path';
|
|
3
3
|
import { check } from './check.js';
|
|
4
4
|
import { parse } from 'yaml';
|
|
5
|
-
import { changedFiles, fileAt, preloadChangedFiles, readCommits } from './git.js';
|
|
5
|
+
import { changedFiles, fileAt, isAncestor, preloadChangedFiles, readCommits } from './git.js';
|
|
6
6
|
import { citedRefs, linkCommits, scopeOf } from './link.js';
|
|
7
7
|
import { PLAN_DEFAULT, PLAN_LEGACY, readDocsConfig, readNewsConfig, readOrchestrateConfig } from './config.js';
|
|
8
8
|
import { ficheHypotheses } from './spec.js';
|
|
@@ -303,6 +303,42 @@ function unreviewed(plan, root, review, linked) {
|
|
|
303
303
|
// Seuls les commits postérieurs sont examinés : un lot relu à son dernier commit ne coûte aucun appel git.
|
|
304
304
|
return workCommits(plan, root, at < 0 ? linked : linked.slice(0, at));
|
|
305
305
|
}
|
|
306
|
+
/** Un texte de verdict de revue dit « non conforme » / « not compliant » (le mot « conforme » dedans ne compte pas) ; « conforme après 1 correction » reste conforme. */
|
|
307
|
+
export function isNonConformingVerdict(text) {
|
|
308
|
+
return /\bnon[\s-]*conforme|\bnot\s+compliant|\bnon[\s-]*compliant/i.test(text);
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Les lots dont les commits de travail sont dans l'histoire de `sha` après `base` (toute l'histoire sans `base`), avec leur état de livraison (L474, hors vague) :
|
|
312
|
+
* `done` et abandonné ne bloquent pas ; un lot « prêt » (son verdict de revue, conforme, couvre tous ses commits) non plus ; un lot à faire ou en cours, non relu
|
|
313
|
+
* ou relu avant des commits postérieurs, bloque. Un commit qui cite un lot que le plan ne connaît pas (clos et archivé, ou inconnu) n'en fait pas un lot.
|
|
314
|
+
*/
|
|
315
|
+
export function deliveryLots(plan, root, sha, base) {
|
|
316
|
+
const inRange = planCommits(plan, root, { range: base ? `${base}..${sha}` : sha });
|
|
317
|
+
const { byLot } = linkCommits(plan.lots(), inRange, plan.refs);
|
|
318
|
+
const lots = [];
|
|
319
|
+
for (const l of plan.lots()) {
|
|
320
|
+
if (!workCommits(plan, root, byLot.get(l.id) ?? []).length)
|
|
321
|
+
continue;
|
|
322
|
+
const entry = (blocking, state) => lots.push({ id: l.id, title: l.title, blocking, state });
|
|
323
|
+
if (l.status === 'done')
|
|
324
|
+
entry(false, 'terminé');
|
|
325
|
+
else if (l.status === 'dropped')
|
|
326
|
+
entry(false, 'abandonné');
|
|
327
|
+
else {
|
|
328
|
+
// Couverture jugée sur les commits du lot jusqu'à `sha` seulement : un `fix` posé au-dessus du sha livré n'est pas livré.
|
|
329
|
+
const late = l.review ? unreviewed(plan, root, l.review, lotCommits(plan, root, l.id)).filter((c) => isAncestor(root, c.sha, sha)).length : 0;
|
|
330
|
+
const state = l.status === 'todo' ? 'à faire' : 'en cours';
|
|
331
|
+
// Prêt = revue CONFORME couvrant les commits (côté vague, un lot n'est ready qu'après une revue conforme).
|
|
332
|
+
if (l.review && late === 0 && !isNonConformingVerdict(l.review.verdict))
|
|
333
|
+
entry(false, 'prêt');
|
|
334
|
+
else if (l.review && late === 0)
|
|
335
|
+
entry(true, `${state}, relu non conforme`);
|
|
336
|
+
else
|
|
337
|
+
entry(true, `${state}, ${l.review ? `relu avant ${late} commit(s)` : 'non relu'}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return lots;
|
|
341
|
+
}
|
|
306
342
|
/** Nombre de commits d'un lot que sa revue de code ne couvre pas — ce que `raf done` refuse quand la porte est active. */
|
|
307
343
|
export function unreviewedWork(plan, root, lotId) {
|
|
308
344
|
const lot = plan.lots().find((l) => l.id === lotId);
|
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';
|
|
@@ -28,6 +28,7 @@ import { configuredCap, expiredHoldLines, slotsLine, orchestrate, realOrchestrat
|
|
|
28
28
|
import { addHold, releaseHold } from './orchestrate/registry.js';
|
|
29
29
|
import { activeLock, REPO_LOCK } from './orchestrate/lock.js';
|
|
30
30
|
import { INTEGRATION_LOCK } from './orchestrate/integrate.js';
|
|
31
|
+
import { mainTreeWorkers } from './orchestrate/main-tree.js';
|
|
31
32
|
import { cadenceHome, liveWaves, openLotsIn, openWavesIn, waveLotsIn } from './orchestrate/registry.js';
|
|
32
33
|
import { blockingLots, LABEL, resumeCommand } from './orchestrate/table.js';
|
|
33
34
|
import { bugCodeOf, bugCodePasses, classifyAll, classifySous, measure, ollamaClassifier, ollamaSousClassifier, readCache, readJournals, readRuns, readSousCache, renderReport, sampleKeys, serializeCache, serializeSousCache } from './corrections.js';
|
|
@@ -423,6 +424,7 @@ function dispatch(argv, io) {
|
|
|
423
424
|
plan.save();
|
|
424
425
|
io.out(id);
|
|
425
426
|
warnIntegrating(root, io.err);
|
|
427
|
+
warnWaveWorking(root, io.env, io.err);
|
|
426
428
|
return 0;
|
|
427
429
|
}
|
|
428
430
|
case 'start':
|
|
@@ -459,13 +461,17 @@ function dispatch(argv, io) {
|
|
|
459
461
|
}
|
|
460
462
|
}
|
|
461
463
|
const unreviewed = status === 'done' && plan.reviewSince && !plan.readonly && !values.force ? unreviewedWork(plan, root, rest[0]) : 0;
|
|
462
|
-
|
|
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 });
|
|
463
467
|
if (command === 'drop' && values.reason)
|
|
464
468
|
plan.note(rest[0], `abandonné : ${values.reason}`, today);
|
|
465
469
|
plan.save();
|
|
466
470
|
io.out(`${rest[0]} → ${status}`);
|
|
467
|
-
if (status === 'done')
|
|
471
|
+
if (status === 'done') {
|
|
468
472
|
warnIntegrating(root, io.err);
|
|
473
|
+
warnWaveWorking(root, io.env, io.err);
|
|
474
|
+
}
|
|
469
475
|
touchedLot(plan, rest[0], values.verbose, io);
|
|
470
476
|
if (status === 'done' && !rest[0].includes('/')) {
|
|
471
477
|
const lot = plan.lot(rest[0]);
|
|
@@ -522,6 +528,7 @@ function dispatch(argv, io) {
|
|
|
522
528
|
plan.note(rest[0], rest.slice(1).join(' '), today);
|
|
523
529
|
plan.save();
|
|
524
530
|
warnIntegrating(root, io.err);
|
|
531
|
+
warnWaveWorking(root, io.env, io.err);
|
|
525
532
|
touchedLot(plan, rest[0], values.verbose, io);
|
|
526
533
|
return 0;
|
|
527
534
|
}
|
|
@@ -1219,6 +1226,23 @@ function warnIntegrating(root, err) {
|
|
|
1219
1226
|
if (holder)
|
|
1220
1227
|
err(`avertissement : intégration en cours (vague ${holder.wave}, pid ${holder.pid}) — ce plan est un fichier suivi modifié de l'arbre principal, l'intégration en cours peut refuser son lot (arbre du dépôt modifié) : committer le plan sans attendre`);
|
|
1221
1228
|
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Avertit (stderr, sans refuser) quand une session de vague écrit dans l'arbre principal du dépôt (L475) : un lot en implémentation, revue ou correction sans worktree.
|
|
1231
|
+
* Un commit de plan du lead à ce moment-là rend le lot (cas du 30-10 : cadence L458 rendu par le commit du plan). Sans effet dans le worktree d'un lot, ou depuis une session de la vague (c'est elle qui tient l'arbre).
|
|
1232
|
+
*/
|
|
1233
|
+
function warnWaveWorking(root, env, err) {
|
|
1234
|
+
if (env.CADENCE_ORCHESTRATED)
|
|
1235
|
+
return;
|
|
1236
|
+
const top = gitRoot(root);
|
|
1237
|
+
if (!top || !statSync(join(top, '.git')).isDirectory())
|
|
1238
|
+
return;
|
|
1239
|
+
const holder = activeLock(join(sharedStateDir(root), REPO_LOCK));
|
|
1240
|
+
if (!holder)
|
|
1241
|
+
return;
|
|
1242
|
+
const working = mainTreeWorkers(cadenceHome(), holder.pid, top);
|
|
1243
|
+
if (working?.length)
|
|
1244
|
+
err(`avertissement : la vague ${holder.wave} (pid ${holder.pid}) travaille dans l'arbre principal de ce dépôt (${working.join(', ')}) — un commit de plan maintenant rendrait ces lots à la revue ; attendre la fin de leur étape, ou écrire le plan sans le committer`);
|
|
1245
|
+
}
|
|
1222
1246
|
/** La vague qui tient le verrou du dépôt tant qu'un de ses lots y travaille encore (état illisible : on la tient pour active) ; null si le dépôt est libre. Partagé par `deliver` et `spec reseal`. */
|
|
1223
1247
|
function waveHolding(root) {
|
|
1224
1248
|
const orchestrating = activeLock(join(sharedStateDir(root), REPO_LOCK));
|
package/dist/deliver.js
CHANGED
|
@@ -4,7 +4,7 @@ import { constants } from 'node:os';
|
|
|
4
4
|
import { join, relative } from 'node:path';
|
|
5
5
|
import { parse } from 'yaml';
|
|
6
6
|
import { onTermination, readProcs, SIGNAL_GRACE_MS, SIGNAL_PENDING_MS, TreeTracker } from './proc.js';
|
|
7
|
-
import { isPlanOnly } from './audit.js';
|
|
7
|
+
import { deliveryLots, isPlanOnly } from './audit.js';
|
|
8
8
|
import { commitPaths, dirtyPaths, headSha, isAncestor, onRemote, readCommits, repoStatus, resolveCommit } from './git.js';
|
|
9
9
|
import { PLAN_ACTIONS, planCommitMessage } from './plan-commit.js';
|
|
10
10
|
import { citedRefs } from './link.js';
|
|
@@ -235,6 +235,7 @@ export function describeCheck(c, sha) {
|
|
|
235
235
|
return c.command;
|
|
236
236
|
return `GET ${substitute(c.url, sha)} → ${c.status ?? 200}${c.contains === undefined ? '' : `, contient « ${substitute(c.contains, sha)} »`}`;
|
|
237
237
|
}
|
|
238
|
+
const guardMessage = (sha, lots) => `${sha.slice(0, 7)} contient des commits de lots ni terminés ni prêts : ${lots.map((l) => `${l.id} (${l.state})`).join(', ')} ; livrer un sha antérieur, ou relire ces lots (raf review) avant`;
|
|
238
239
|
/** Livre HEAD. 0 livré, 1 étape en échec, 2 refus avant d'agir. */
|
|
239
240
|
export async function deliver(ctx, deps) {
|
|
240
241
|
const { config, out, err } = ctx;
|
|
@@ -259,6 +260,24 @@ export async function deliver(ctx, deps) {
|
|
|
259
260
|
err(`deliver : ${dirty} fichier(s) suivi(s) modifié(s) : non livré(s), seul ${sha.slice(0, 7)} l'est (allowDirty)`);
|
|
260
261
|
if (!onRemote(ctx.root, sha))
|
|
261
262
|
return refuse(`${sha.slice(0, 7)} non poussé : la CI n'a rien construit (git push)`);
|
|
263
|
+
// Hors vague, un sha choisi emporte les commits de TOUS les lots sous lui (« L49 seul » a livré L50 et L51 non relus, 30-10) : on les nomme, avec leur état.
|
|
264
|
+
// Pendant une vague, la garde de la vague (cli, `blockingLots`) en tient lieu.
|
|
265
|
+
let undelivered = [];
|
|
266
|
+
if (ctx.sha !== undefined && !ctx.duringWave && ctx.plan) {
|
|
267
|
+
const last = lastDelivery(ctx.state);
|
|
268
|
+
const from = last && isAncestor(ctx.root, last, sha) ? last : null;
|
|
269
|
+
const lots = deliveryLots(ctx.plan, ctx.root, sha, from);
|
|
270
|
+
if (ctx.dryRun) {
|
|
271
|
+
out(`Lots de l'intervalle ${from ? `${from.slice(0, 7)}..` : 'jusqu\'à '}${sha.slice(0, 7)} :`);
|
|
272
|
+
if (lots.length === 0)
|
|
273
|
+
out(' (aucun)');
|
|
274
|
+
for (const l of lots)
|
|
275
|
+
out(` ${l.id} « ${l.title} » — ${l.state}`);
|
|
276
|
+
}
|
|
277
|
+
undelivered = lots.filter((l) => l.blocking);
|
|
278
|
+
if (undelivered.length && !ctx.dryRun)
|
|
279
|
+
return refuse(guardMessage(sha, undelivered));
|
|
280
|
+
}
|
|
262
281
|
const lock = readLock(ctx.state);
|
|
263
282
|
if (lock && lockAlive(lock)) {
|
|
264
283
|
return refuse(`livraison déjà en cours (${lock.sha.slice(0, 7)}, pid ${lock.pid}, depuis ${lock.started}) — jamais deux à la fois ; ` +
|
|
@@ -292,6 +311,8 @@ export async function deliver(ctx, deps) {
|
|
|
292
311
|
out(` Vérifications (réessayées pendant ${config.verifyTimeout} s) :`);
|
|
293
312
|
config.verify.forEach((c, i) => out(` ${i + 1}. ${describeCheck(c, sha)}`));
|
|
294
313
|
out(` Variables : ${Object.entries(env).map(([k, v]) => `${k}=${v}`).join(' ')}`);
|
|
314
|
+
if (undelivered.length)
|
|
315
|
+
return refuse(guardMessage(sha, undelivered));
|
|
295
316
|
return 0;
|
|
296
317
|
}
|
|
297
318
|
if (lock && removeStaleLock(ctx.state, lock)) {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { liveWaves } from './registry.js';
|
|
3
|
+
import { lotKey, lotRepoPaths, RunStore } from './state.js';
|
|
4
|
+
import { lotTreeDir } from './worktree.js';
|
|
5
|
+
/** Les étapes d'un lot où une session de la vague écrit dans le dépôt (L475) : le lot en file, en question, prêt, rendu, en échec ou suspendu n'y touche pas. */
|
|
6
|
+
const WRITING = new Set(['implementing', 'reviewing', 'fixing']);
|
|
7
|
+
const real = (p) => {
|
|
8
|
+
try {
|
|
9
|
+
return realpathSync(p);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return p;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Lots de la vague vivante `pid` dont une session travaille dans l'ARBRE PRINCIPAL de `repo` (L475) : un lot en implémentation, revue ou correction qui ne joue pas dans un worktree
|
|
17
|
+
* (vague sans worktree, ou lot dont le dossier de worktree n'existe pas). Un commit de plan du lead y est un commit de plus dans l'historique que la revue ou l'intégration compare
|
|
18
|
+
* à celui du lot : le lot est rendu (cas du 30-10). Clés `projet:lot` ; liste vide si aucun ; null si l'état de la vague est illisible.
|
|
19
|
+
*/
|
|
20
|
+
export function mainTreeWorkers(home, pid, repo) {
|
|
21
|
+
try {
|
|
22
|
+
const w = liveWaves(home).find((x) => x.pid === pid);
|
|
23
|
+
const store = w && RunStore.find(w.cwd, w.wave);
|
|
24
|
+
if (!store)
|
|
25
|
+
return null;
|
|
26
|
+
return store
|
|
27
|
+
.lots()
|
|
28
|
+
.filter((l) => WRITING.has(l.status) && lotRepoPaths(l).some((r) => real(r) === real(repo)) && !existsSync(lotTreeDir(store.dir, l.project, l.lot, l.repo)))
|
|
29
|
+
.map((l) => lotKey(l.project, l.lot));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
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/deliver.md
CHANGED
|
@@ -172,6 +172,15 @@ itself never reads the working tree: a tracked file modified outside the deliver
|
|
|
172
172
|
sha (the plan after `raf done`, say) neither refuses nor is reported, with or
|
|
173
173
|
without `allowDirty`.
|
|
174
174
|
|
|
175
|
+
**Outside a wave: the lots under the sha (L474).** `--sha` also refuses, with no wave running, a sha whose history carries work commits of a lot of the
|
|
176
|
+
plan that is neither done nor ready — a commit cited « L49 » that sits above commits of L50 and L51 delivers them too (30-10: `09217b0` « L49 alone »
|
|
177
|
+
shipped two lots nobody had reviewed). The interval checked runs from the last delivery of the journal (when it is in the history of `<sha>`) up to
|
|
178
|
+
`<sha>`; with no such delivery, the whole history, the plan's adoption day and the plan-only, acknowledged and release commits left out as everywhere. A lot passes
|
|
179
|
+
when it is `done` or dropped, or **ready**: its code review verdict is compliant and covers all its commits (a verdict reading `non conforme` / `not compliant` keeps the lot back, state `relu non conforme`). A lot to do or in progress, not reviewed or reviewed before later commits, holds the sha back; the
|
|
180
|
+
refusal (exit 2, before anything runs) names each such lot with its state. `--dry-run` prints every lot of the interval with its state (`terminé`, `prêt`,
|
|
181
|
+
`abandonné`, `en cours, non relu`, `à faire, non relu`, `en cours, relu avant N commit(s)`, `en cours, relu non conforme`) after the same refusal checks, and refuses the same way when one blocks. A plain `cadence deliver` (HEAD) and a delivery during a wave (the
|
|
182
|
+
wave's own guard, below) do not run this check; a commit that cites a lot the plan does not know (archived, unknown) makes no lot.
|
|
183
|
+
|
|
175
184
|
**While a wave runs.** `cadence orchestrate` holds the repository: a plain `cadence deliver` is refused until every lot of the
|
|
176
185
|
repository in the wave is ready or handed back. `--sha` is the exception (L277): it is accepted while the wave runs when no
|
|
177
186
|
commit in the history of `<sha>` belongs to a lot of the wave that is not ready (running, queued, in question, suspended,
|
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>
|
|
@@ -80,7 +81,7 @@ Also: [news](news.md) (changelog, UX, code and QA reviews, documentation that fo
|
|
|
80
81
|
- **raf** (French *reste à faire*, "what is left to do"): the plan, linked to your commits by the lot id in the message, computed from `git log` and never stored.
|
|
81
82
|
- **news**: a user-facing changelog with screenshots, tied to the plan; a lot is not done until its entry is written.
|
|
82
83
|
- **session**: `session start` and `session close` gather the facts of a work session (each lot in progress shows its commit count, `0 commit` included, next to its last activity).
|
|
83
|
-
- **deliver**: wait for the CI of the pushed commit, run the deploy commands, then **verify the effect**; a delivery is done only when its checks pass. A failed delivery is logged as a *red delivery* with its class (refusal, CI, deploy, check) and the last lines of the failing command's output. While a wave runs, `cadence deliver --sha <sha>` is accepted when no commit of the sha belongs to a lot that is not ready, so a ready lot ships while the wave runs.
|
|
84
|
+
- **deliver**: wait for the CI of the pushed commit, run the deploy commands, then **verify the effect**; a delivery is done only when its checks pass. A failed delivery is logged as a *red delivery* with its class (refusal, CI, deploy, check) and the last lines of the failing command's output. While a wave runs, `cadence deliver --sha <sha>` is accepted when no commit of the sha belongs to a lot that is not ready, so a ready lot ships while the wave runs. Outside a wave, `--sha` refuses a range that holds commits of a lot that is neither done nor ready (its code review covering its commits up to the sha, not above), and `--dry-run` lists the lots of the range with their state (L474).
|
|
84
85
|
- **auto-verification** (L236): before it reports, an `implement` or `fix` session re-reads the complete diff of its lot (of its fix commits, in `fix`) against the grid of the `code-reviewer` (code bug, architecture drift, architecture documentation, edge cases, one test per changed behaviour), fixes what it finds and writes an « auto-vérification » paragraph in its report; the measure is `cadence corrections --classify` and the share right the first time of `cadence perf`.
|
|
85
86
|
- **real walkthrough and decisions** (L344): once the tests are green an `implement` session plays what it delivered as a user (the command in a throwaway repository following its own documentation, the screen in each state with the failure in view) and reports it under « parcours réel »; the notes of the lot and of its parent that record a decision of the human reach the `implement` and `fix` briefs under « Decisions to respect », a departure being a question, never a choice (`docs/orchestrate.md`).
|
|
86
87
|
- **perf**: `cadence perf` is the dashboard of the workflow (speed next to quality, `⚠` on a trend), including the red deliveries and the blocking QA findings; `cadence lead tour` opens with its *perf header*, one line per `⚠`, nothing when stable; a rise of the tokens per lot names the step that carries it, and when the share right the first time rose too it reads as quality paid for, not a wrong estimate ([docs/perf.md](perf.md)).
|
package/docs/qualite.md
ADDED
|
@@ -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
|
@@ -60,8 +60,9 @@ raf gantt # .cadence/gantt.html (next to the plan)
|
|
|
60
60
|
| `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) |
|
|
61
61
|
| `raf move <id> --before\|--after <id>` | reorder a lot in the plan file (L400): `<id>` is placed right before or right after the other lot, the others keep their order, and the lot travels with all its keys, its sub-tasks and its comments. Only among siblings: a lot among lots, a sub-task (`L1/t1`) among the sub-tasks of its own lot (`move : L2 et L1/t1 n'ont pas le même parent …`, plan unchanged). Exactly one of `--before` / `--after`, one id (else `usage : raf move <id> --before <id> \| raf move <id> --after <id>`); a lot placed where it already is, or an unknown id (`lot inconnu : <id>`), changes nothing. **Refused** on a read-only plan, plan kept by another tool (`plan en lecture seule : … — le modifier avec l'outil du projet`: reorder it with the project's own tool), on an archived lot (`move : L1 est archivé …`: the archive is presented in its former order and the order of the plan file never moves, see `raf archive`) and while a wave holds the repository (`move : orchestration en cours (vague …, pid …) — attendre …`: the lots of the wave rely on the order they read; the lock falls as soon as all of them are ready or handed back). The order is the order of the file, so it is the order `raf list`, `raf now`, the Gantt page and the draw of `cadence orchestrate` follow; the command only writes the plan, it does not commit it (`plan.commit` prints the command, as for every plan write) |
|
|
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
|
+
| `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 |
|
|
63
64
|
| `raf public <id> "title"` · `raf public <id> --clear` | set, replace or remove the public title of a lot |
|
|
64
|
-
| `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) |
|
|
65
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) |
|
|
66
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 |
|
|
67
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,8 @@
|
|
|
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`.
|
|
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.
|
|
7
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.
|
|
8
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.
|
|
9
11
|
- **1.28.0**: projects are reordered from the lots panel or `cadence lead priority`; the lead's context threshold is in tokens; reviewers answer the lead in a few lines; the gate replays under plan commits when tests read the plan.
|
|
@@ -57,6 +59,10 @@ Each is a paragraph in [docs/whats-new.md](whats-new.md) and a full section in [
|
|
|
57
59
|
|
|
58
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).
|
|
59
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
|
+
|
|
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.
|
|
65
|
+
|
|
60
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).
|
|
61
67
|
|
|
62
68
|
**1.29.0**: two incidents of 30-10 turned into mechanisms. A writing session of a wave no longer commits on the main branch of a repository its lot does not declare (L456, `docs/orchestrate.md`): the brief forbids it and names the way, a branch `wip/<project>-<lot>`, and at the end of each session the program compares the main branches of the sibling repositories of the folder; a commit found there hands the lot back with the repository, the branch and the shas, and never moves or reverts anything — the lead moves the commits. Commits of the lead (subject `plan:`, plan file only) and commits of lots of another live wave of the folder are tolerated; repositories declared by the lot (`repos:`) keep their behaviour. The case was a session that committed a chart on the production branch of a GitOps repository. `cadence verify --gate` no longer links the ignored folders of the main tree that are not dependencies (`data/`, `data-local/`, `.env`) into its throwaway copy (L455, `docs/deliver.md`): a test comparing paths by `realpath` went red in the copy and green in the main tree at the same sha, and stopped a delivery.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sylad/cadence",
|
|
3
|
-
"version": "1.
|
|
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": {
|