@sylad/cadence 1.40.0 → 1.41.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.40.0",
11
+ "version": "1.41.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.40.0",
4
+ "version": "1.41.0",
5
5
  "author": { "name": "Sylvain Ladoire" },
6
6
  "homepage": "https://github.com/Sylad/cadence",
7
7
  "repository": "https://github.com/Sylad/cadence",
@@ -14,6 +14,7 @@ Follow the brief you are given. The method:
14
14
  **Colours you add: semantic tokens, `on-*`, contrast.** Start from the charter's colour tokens and their text tokens (CSS variables). For every colour the screen adds, name a semantic token named by its role (`primary`, `success`, `warning`, `danger`, `surface`…) and its text token `on-<role>`; a colour added without its `on-*` token is a defect of the spec. The values come from a ramp drawn from the charter's brand colour (same hue, stepped lightness): the agent imports no external palette. A role already covered by a token of the charter keeps that token: reuse what exists, create no duplicate token, and only measure the existing pair. Write the measure in `contraste.md`, in the spec folder, committed with the sheet: `contraste.md` has two lists: the tokens created, and the existing pairs measured (an existing token appears only in the second), with one line per token / on-token pair: both values, the computed ratio (WCAG 2.2, relative luminance) and `ok` or `non`. Every pair is at least 4.5:1 (a pair at 4.49:1 is `non`, a pair at 4.5:1 is `ok`); fix a pair that falls short: change the step of the ramp, never the text size. A product with no CSS variable and no brand colour gets a mockup that follows its existing values: invent no ramp, measure the existing values in `contraste.md`, and write the absence of a coloured charter in the sheet. On a product that has a charter, never propose, draw or blame a generic style from a catalogue; with no charter, follow the existing values and say so, and choose no style.
15
15
 
16
16
  3. **Render and look.** Capture each state at **1440 px** and **390 px** wide with the browser tool (Playwright), named `maquette-<état>-<largeur>.png`, with the absolute capture-directory path your brief gives in front of the name (a bare name lands in the repository), and look at every capture before keeping it: a mockup you did not look at is not checked. Copy the kept captures into the spec folder.
17
+ **Light mockup** (the brief says so: a visible lot that is not a whole page): cover only the states the lot touches, and before drawing the target capture the CURRENT screen of each of those states at both widths, named `actuel-<état>-<largeur>.png`, in the same folder. If the current screen cannot be captured (the application does not start, the page is protected), say so under `nonVerifie` and base the mockup on the code: never invent a capture.
17
18
  Before keeping a state, check its sample data: compute the weekday of every date shown (`date -d`), never write it from memory.
18
19
  4. **Write the sheet** in the format of the template of the brief, and run `cadence spec check` on it until it passes. Every rule is `sourcee` (say where it comes from) or `hypothese` (say what would change if it were wrong, in `si_faux`). Never dress a guess as a source.
19
20
  5. **Costly choices are questions.** If a rule would fix a data model or storage, a public or shared API contract or an already published route, a destructive or irreversible action, or contradict an existing sourced rule, do not write it as a hypothesis: stop and ask, in the `questions` of the report. Wording, field order, the error state and spacing are written hypotheses.
@@ -24,6 +24,8 @@ On top of the usability and accessibility checks, check the **consistency mockup
24
24
  - every state of `etats` has its section `#etat-<état>`; the `masque` of an empty-state rule really hides the fields it names;
25
25
  - the units, formats and wording the sheet declares are those shown.
26
26
 
27
+ Light mockup (the brief says so): the sheet and the mockup cover only the states the lot touches. Sweep those states only, and compare the target of each one to the capture of the current screen `actuel-<état>-<largeur>.png` in the same folder (a free file the program does not check; when it is missing the spec writer said why under `nonVerifie`, judge the target against the code and the neighbouring pages). A defect of the screen that the lot does not touch is not a finding of this review.
28
+
27
29
  **Criteria added in mockup mode** — in this mode only (conformity mode and the review of an existing screen keep their text). Each one carries its named rule or the value to count; the checks of the method below (contrast, targets, keyboard, focus, alternatives, colour, reflow, data display) are already written there and are not repeated here.
28
30
 
29
31
  **Family: visual hierarchy**
package/dist/cli.js CHANGED
@@ -2,9 +2,13 @@ import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from
2
2
  import { execFileSync } from 'node:child_process';
3
3
  import { basename, dirname, join, posix, relative, resolve } from 'node:path';
4
4
  import { parseArgs } from 'node:util';
5
+ import { refuseRedAcceptation } from './raf-close-checks.js';
6
+ import { statusCommand } from './raf-status.js';
5
7
  import { defaultPlan, PLAN_DEFAULT, PLAN_LEGACY, readDocsConfig, readHookConfig, readNewsConfig, readOrchestrateConfig, readPerfConfig, readPlanConfig, readSessionConfig, readVerifyConfig } from './config.js';
6
8
  import { repoSections, repoShas, resolveLotRepos } from './repos.js';
7
- import { audit, exemptPlanOnly, gateProgress, gateText, gateWaiting, hypothesesOf, isNonConformingVerdict, ownFiles, isPlanOnly, lotWork, nextUp, planCommits, unreviewedWork, workByLot } from './audit.js';
9
+ import { lotJson, lotLines } from './lot-lines.js';
10
+ import { parseSet } from './plan-extras.js';
11
+ import { audit, exemptPlanOnly, gateWaiting, hypothesesOf, isNonConformingVerdict, ownFiles, isPlanOnly, lotWork, nextUp, planCommits, unreviewedWork, workByLot } from './audit.js';
8
12
  import { short } from './check.js';
9
13
  import { isDay, toDay } from './dates.js';
10
14
  import { deliver, parseDeliverConfig, realDeps, shellQuote } from './deliver.js';
@@ -34,7 +38,7 @@ import { blockingLots, LABEL, resumeCommand } from './orchestrate/table.js';
34
38
  import { renderTestsSansCode, testsSansCode } from './corrections-sans-code.js';
35
39
  import { bugCodeOf, bugCodePasses, classifyAll, classifySous, measure, ollamaClassifier, ollamaSousClassifier, readCache, readJournals, readRuns, readSousCache, renderReport, sampleKeys, serializeCache, serializeSousCache } from './corrections.js';
36
40
  import { computePerf, perfHeader, perfWindows, readPlans, readProjectDeliveries, readProjectFailedEntries, renderPerf } from './perf.js';
37
- import { findAcceptation, parseAcceptation, redAcceptations, resealAcceptation, resealForGreen, runAcceptation, modeSpecGreen, setVert } from './orchestrate/accept.js';
41
+ import { findAcceptation, parseAcceptation, resealAcceptation, resealForGreen, runAcceptation, modeSpecGreen, setVert } from './orchestrate/accept.js';
38
42
  import { refCommand } from './ref/command.js';
39
43
  import { coverageCommand } from './ref/coverage.js';
40
44
  import { release } from './release.js';
@@ -47,7 +51,7 @@ import { clearNext, readNext, sharedStateDir, stateDir, writeNext } from './stat
47
51
  const HELP = `raf — plan « reste à faire » versionné dans le dépôt, relié aux commits
48
52
 
49
53
  raf init [--project nom] [--prefix L] [--no-hook]
50
- raf add "titre" [--estimate j] [--quickwin] [--visible [--screen page|component|journey]] [--spec] [--local] [--public "titre public"] [--every jours] [--after L2,L4] [--uses banc,passerelle] [--parent L3]
54
+ raf add "titre" [--estimate j] [--quickwin] [--visible [--screen page|component|journey]] [--spec] [--local] [--public "titre public"] [--every jours] [--after L2,L4] [--uses banc,passerelle] [--set champ=valeur]… [--parent L3]
51
55
  raf public <id> "titre public" | --clear titre du lot dans le langage du public (pages Nouveautés et Plan)
52
56
  raf screen <id> page|component|journey|none pose, remplace ou retire screen: d'un lot visible existant (seule la valeur page adopte l'étape spec d'orchestrate)
53
57
  raf visible <id> on|off pose ou retire visible: d'un lot existant (changement visible : entrée Nouveautés et étape UX d'orchestrate) ; off est refusé tant que le lot a un screen:
@@ -56,6 +60,7 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
56
60
  raf uses <lot> [<ressource>…] remplace les ressources partagées du lot (uses:, déclarées par resources: dans le cadence.yaml du dossier parent ; raf check refuse un nom non déclaré) ; sans nom, retire la clé
57
61
  cadence local start <lot> [--ttl minutes] | local done <lot> | local abort <lot> tâche locale du lead (lot posé par raf add --local) : start verrouille le dépôt comme une vague (aucune vague n'y démarre, --add y est refusé) et passe le lot à doing, refusé si une orchestration ou un autre lot du dépôt est en cours ; le verrou est déclaré (60 min par défaut), tenu sans processus jusqu'à done ou abort, signalé en ⚠ par lead tour et orchestrate --status au-delà du délai ; done joue la porte du projet UNE fois, libère le dépôt si elle est verte (la capture, la revue du diff, cadence deliver et raf done restent au lead) et le garde verrouillé sinon ; abort libère sans porte
58
62
  raf start <id> raf done <id> [--force] raf drop <id> [--reason texte]
63
+ raf status [<id> <état> ["message"] | <id> --back ["message"]] états détaillés du projet (cadence.yaml : plan.states) : pose l'état d'un lot sous la garde de plan.transitions et note le message ; --back sort de l'état bloqué vers l'état d'avant ; sans argument, liste les états. start / done / drop / todo posent eux-mêmes l'état de leur statut
59
64
  raf todo <id> [--reason texte] remet en attente un lot démarré sans commit de travail (doing → todo, started retiré, note datée) ; refusé s'il a des commits ; rouvre un lot done ou dropped (→ todo, finished retiré, note « rouvert ») — la seule façon de rouvrir, archivé compris (il revient dans le fichier du plan)
60
65
  raf note <id> "texte"
61
66
  --verbose avec done, ux, review, note, move ou public : imprime la ligne « id état titre entier » du lot touché ; avec hook post-commit : le titre entier (sinon 60 caractères au plus)
@@ -68,7 +73,7 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
68
73
  raf show <id> [--notes] le lot : statut, dates, after, titre public, visible et verdict ux (lot visible), notes datées, commits comptés ; --notes : les notes seules
69
74
  raf commits <id> les commits du lot que compte la porte de revue de code, du plus ancien au plus récent
70
75
  raf now ce qui est en cours, la suite, les derniers terminés
71
- raf list [--status todo|doing|done|dropped]
76
+ raf list [--status todo|doing|done|dropped] [--json] les lots, une ligne chacun ; --json : un tableau JSON des lots (clés du fichier du plan, champs d'extension sous extras)
72
77
  raf check [--since date] [--idle 7] (défaut : date « since » du plan) code 1 s'il y a des écarts
73
78
  raf ignore <sha> | "<sujet exact>" [--reason texte] acquitte un commit sans lot, citant un id inconnu ou des lots sans y travailler, sans réécrire l'historique ; raf check --ignored les liste
74
79
  raf docs-ok <lot> --reason texte acquitte l'écart docs.sync d'un lot (documentation en retard) avec la raison pour laquelle la documentation reste telle quelle (plan en lecture seule : .cadence/docs-ok.yaml, le plan n'est pas touché) ; lu par raf check et par orchestrate, qui ne rend jamais prêt un lot avec un écart ouvert
@@ -93,7 +98,7 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
93
98
  cadence ref eval <attendu.json | dossier> <lecture.json | dossier> [--json] note une lecture contre l'inventaire attendu (vérité connue) : rappel et précision par type, faux lu (éléments sûrs et faux), pièges trouvés, tokens par page (usage.tokens de la lecture) ; lecture d'un autre document refusée (code 2) ; sur dossiers, un cas par sous-dossier (attendu.json / lecture.json)
94
99
  cadence ref corpus <dossier> [--sans-rendu] écrit le corpus synthétique à vérité connue (7 pièges : texte ambigu levé par une figure, figure qui contredit, table en image, gabarit BUFR, contrôle translucide, page Proposal, scan bruité) : par cas document.html et attendu.json, puis document.pdf et page-<n>.png (1700×2200) rendus par Playwright (CADENCE_PLAYWRIGHT = dossier du paquet) ; dossier existant ou Playwright introuvable : code 2
95
100
  cadence release patch|minor|major [--dry-run] prépare le commit de version (package.json, package-lock.json, plugin.json, marketplace.json, CHANGELOG.md) sans commiter ni publier ; imprime les étapes du lead et vérifie docs/whats-new.md : refus sur un arbre sale, un [Unreleased] vide ou une version déjà taguée
96
- cadence verify [--retry s] [--sha rév] rejoue deliver.verify hors livraison : 0 vert, 1 effet rouge, 2 rien à vérifier
101
+ cadence verify [--retry s] [--sha rév] rejoue deliver.verify hors livraison : 0 vert (et, au sha de la dernière livraison rouge, l’inscrit au journal des livraisons), 1 effet rouge, 2 rien à vérifier
97
102
  cadence verify --gate --sha rév [--project dossier] porte du projet (orchestrate.build puis orchestrate.test) : reprend le verdict enregistré par la vague quand son sha est le même et son arbre identique, sans rien rejouer (la sortie dit « verdict repris de la porte de la vague ») ; sinon la rejoue (« verdict rejoué : raison ») au sha dans l'arbre principal s'il est propre au sha, sinon dans un worktree jetable aux dépendances copiées (orchestrate.worktreeDeps), puis le retire : 0 vert, 1 rouge avec les tests en échec, 2 rien à rejouer ; le verdict (0 ou 1) est noté par sha et le hook pre-push refuse un push dont le sha n'a pas de verdict vert (CADENCE_PUSH_UNGATED=1 passe outre)
98
103
  raf news new <lot…> [--title t] | list | check | stamp | build [-o dossier] (aussi « cadence news … »)
99
104
 
@@ -183,8 +188,10 @@ function dispatch(argv, io) {
183
188
  title: { type: 'string' },
184
189
  after: { type: 'string' },
185
190
  uses: { type: 'string' },
191
+ set: { type: 'string', multiple: true },
186
192
  parent: { type: 'string' },
187
193
  force: { type: 'boolean' },
194
+ back: { type: 'boolean' },
188
195
  reason: { type: 'string' },
189
196
  porteur: { type: 'string' },
190
197
  status: { type: 'string' },
@@ -406,6 +413,8 @@ function dispatch(argv, io) {
406
413
  }
407
414
  if (values.uses !== undefined)
408
415
  throw new RafError(`uses se pose sur un lot, pas sur une sous-tâche : ${values.parent}`);
416
+ if (values.set !== undefined)
417
+ throw new RafError(`--set se pose sur un lot, pas sur une sous-tâche : ${values.parent}`);
409
418
  id = plan.addTask(values.parent, rest.join(' '));
410
419
  }
411
420
  else {
@@ -421,7 +430,7 @@ function dispatch(argv, io) {
421
430
  const uses = values.uses?.split(',').map((n) => n.trim()).filter(Boolean);
422
431
  if (uses?.length === 0)
423
432
  throw new RafError(invalidResource(''));
424
- id = plan.add(rest.join(' '), today, { estimate, quickwin: values.quickwin, visible: values.visible, screen: values.screen, spec: values.spec, local: values.local, public: values.public, every, after, uses });
433
+ id = plan.add(rest.join(' '), today, { estimate, quickwin: values.quickwin, visible: values.visible, screen: values.screen, spec: values.spec, local: values.local, public: values.public, every, after, uses, extras: parseSet(values.set) });
425
434
  }
426
435
  plan.save();
427
436
  io.out(id);
@@ -444,24 +453,8 @@ function dispatch(argv, io) {
444
453
  refuseLongTitle(pub, lot?.public ? '' : ' — titre de la Nouveauté reprise par le site : corriger l\'entrée ou poser raf public');
445
454
  }
446
455
  // Le plan ne lit pas git : lui dire combien de commits du lot sa revue ne couvre pas quand elle est exigée.
447
- // Un lot clos avec une acceptation à vert: false bloque les vagues suivantes du dépôt (verrou des tests d'acceptation, L366).
448
- if (status === 'done' && !rest[0].includes('/')) {
449
- const repo = gitRoot(root) ?? root;
450
- let dir = null;
451
- try {
452
- dir = readOrchestrateConfig(configPath).spec.dir;
453
- }
454
- catch {
455
- // un cadence.yaml illisible ne doit pas empêcher de clore un lot : `cadence check` en parle
456
- }
457
- const red = dir === null ? undefined : redAcceptations(repo, dir).find((a) => a.lot === rest[0]);
458
- if (red) {
459
- const text = `${rest[0]} : ${red.path} est à vert: false — clos ainsi, le lot bloquerait les vagues suivantes du dépôt (verrou des tests d'acceptation) ; cadence spec green ${rest[0]} lance la commande d'acceptation et pose vert: true`;
460
- if (!values.force)
461
- throw new RafError(`${text} (--force pour passer outre)`);
462
- io.err(`avertissement : ${text}`);
463
- }
464
- }
456
+ if (status === 'done' && !rest[0].includes('/'))
457
+ refuseRedAcceptation(root, configPath, rest[0], !!values.force, io.err);
465
458
  const unreviewed = status === 'done' && plan.reviewSince && !plan.readonly && !values.force ? unreviewedWork(plan, root, rest[0]) : 0;
466
459
  const verdict = status === 'done' && !rest[0].includes('/') ? plan.lots().find((l) => l.id === rest[0])?.review?.verdict : undefined;
467
460
  const nonConforming = verdict !== undefined && isNonConformingVerdict(verdict);
@@ -512,6 +505,8 @@ function dispatch(argv, io) {
512
505
  }
513
506
  return 0;
514
507
  }
508
+ case 'status':
509
+ return statusCommand(loadPlan(), rest, { back: !!values.back, today, out: io.out, commits: (p, id) => lotWork(p, root, id).length });
515
510
  case 'todo': {
516
511
  need(1, 'todo <id> [--reason texte]');
517
512
  const plan = loadPlan();
@@ -638,32 +633,8 @@ function dispatch(argv, io) {
638
633
  io.out(n);
639
634
  return 0;
640
635
  }
641
- io.out(`${lot.id} ${lot.status} ${lot.title}`);
642
- if (lot.goal)
643
- io.out(`goal : ${lot.goal}`);
644
- const dates = [['créé', lot.created], ['démarré', lot.started], [lot.status === 'dropped' ? 'abandonné' : 'terminé', lot.finished]].filter(([, d]) => d);
645
- if (dates.length)
646
- io.out(dates.map(([k, d]) => `${k} : ${d}`).join(' '));
647
- if (lot.after.length)
648
- io.out(`after : ${lot.after.join(', ')}`);
649
- if (lot.public)
650
- io.out(`public : ${lot.public}`);
651
- // L439 : le livreur lit ici si le lot est visible et s'il porte un verdict UX ; deux lignes stables, absentes d'un lot non visible.
652
- if (lot.visible)
653
- io.out('visible : oui');
654
- if (lot.screen)
655
- io.out(`screen : ${lot.screen}`);
656
- if (lot.visible)
657
- io.out(lot.ux ? `ux : ${lot.ux.verdict}` : 'ux : absent');
658
- if (lot.spec)
659
- io.out('spec : true');
660
- if (lot.uses)
661
- io.out(`uses : ${lot.uses.join(', ')}`);
662
- if (lot.docsOk)
663
- io.out(`docs-ok : ${lot.docsOk.date} — ${lot.docsOk.reason}`);
664
- const gate = gateProgress(plan.lots(), lot);
665
- if (lot.gate && gate)
666
- io.out(lot.gate.invalid ? `gate : ${gateText(gate)}` : `gate : ${gate.n} lots ${gate.label} faits`);
636
+ for (const line of lotLines(plan, lot))
637
+ io.out(line);
667
638
  if (notes.length) {
668
639
  io.out('notes :');
669
640
  // Les lignes suivantes d'une note multiligne s'alignent sous son texte, pas en colonne 0.
@@ -682,9 +653,12 @@ function dispatch(argv, io) {
682
653
  const plan = loadPlan();
683
654
  if (values.status && !STATUSES.includes(values.status))
684
655
  throw new RafError(`statut inconnu : ${values.status}`);
685
- for (const l of plan.lots().filter((l) => !values.status || l.status === values.status)) {
686
- io.out(`${l.id.padEnd(6)} ${l.status.padEnd(8)} ${l.quickwin ? '⚡ ' : ''}${l.title}`);
687
- }
656
+ const shown = plan.lots().filter((l) => !values.status || l.status === values.status);
657
+ if (values.json)
658
+ io.out(JSON.stringify(shown.map(lotJson), null, 2));
659
+ else
660
+ for (const l of shown)
661
+ io.out(`${l.id.padEnd(6)} ${l.status.padEnd(8)} ${l.quickwin ? '⚡ ' : ''}${l.title}`);
688
662
  return 0;
689
663
  }
690
664
  case 'now':
@@ -816,12 +790,8 @@ function dispatch(argv, io) {
816
790
  if (!existsSync(configPath))
817
791
  throw new RafError(`pas de configuration de livraison : ${configPath} (voir « cadence.yaml » dans docs/deliver.md du paquet @sylad/cadence)`);
818
792
  const config = parseDeliverConfig(readFileSync(configPath, 'utf8'), configPath);
819
- const plan = existsSync(planPath) ? loadPlan() : null;
820
- const articles = readDocsConfig(configPath).articles;
821
- // Sans public: d'un lot visible, le site reprend le titre de sa Nouveauté la plus récente.
822
- const publicTitle = (lot) => lot.public ?? (lot.visible ? loadEntries(newsDir).find((e) => e.lots.includes(lot.id))?.title : undefined);
823
- const ctx = { root, state: sharedStateDir(root), plan, config, today, dryRun: !!values['dry-run'], sha: values.sha, duringWave: !!orchestrating, waveId: orchestrating?.wave, planCommit: readPlanConfig(configPath, root)?.commit, args: rest, articles, publicTitle, out: io.out, err: io.err };
824
- return deliver(ctx, realDeps(root));
793
+ const effects = deliveredEffects({ root, configPath, planPath, loadPlan, newsDir, today, waveId: orchestrating?.wave, io });
794
+ return deliver({ ...effects, state: sharedStateDir(root), config, dryRun: !!values['dry-run'], sha: values.sha, duringWave: !!orchestrating, args: rest }, realDeps(root));
825
795
  }
826
796
  case 'verify': {
827
797
  if (values.gate)
@@ -837,7 +807,12 @@ function dispatch(argv, io) {
837
807
  const target = values.sha ? { sha: resolveCommit(root, values.sha) ?? '', note: null } : defaultTarget(root, config);
838
808
  if (values.sha && !target.sha)
839
809
  throw new RafError(`--sha ${values.sha} : commit introuvable`);
840
- return verifyCommand({ config, sha: target.sha, note: target.note, retry, out: io.out }, realCheckDeps(root));
810
+ // Une ligne verte écrite par verify (L435) est une livraison : mêmes effets que deliver (lots annoncés, article à rafraîchir, L144).
811
+ // verify lit le verrou sans refuser : une tâche locale du lead (L519) n'empêche pas de rejouer les vérifications, et n'est pas une vague à citer.
812
+ const holding = waveHolding(root);
813
+ const waveId = holding && !localLotOf(holding.wave) ? holding.wave : undefined;
814
+ const effects = () => deliveredEffects({ root, configPath, planPath, loadPlan, newsDir, today, waveId, io });
815
+ return verifyCommand({ config, sha: target.sha, note: target.note, retry, out: io.out, journal: { dir: sharedStateDir(root), day: today, effects } }, realCheckDeps(root));
841
816
  }
842
817
  case 'skills': {
843
818
  if (rest[0] !== 'install')
@@ -870,6 +845,16 @@ const GATES = {
870
845
  ux: { on: 'revue UX obligatoire pour les lots visibles', already: 'revue UX déjà active', action: PLAN_ACTIONS.uxRecorded },
871
846
  review: { on: 'revue de code obligatoire pour les lots à commits', already: 'revue de code déjà active', action: PLAN_ACTIONS.reviewRecorded },
872
847
  };
848
+ /**
849
+ * Ce qu'une livraison verte annonce et ouvre (lots livrés, article à rafraîchir, L144) : pour `cadence deliver` et pour la ligne verte
850
+ * que `cadence verify` inscrit (L435). Sans `public:`, un lot visible tire son titre de sa Nouveauté la plus récente.
851
+ */
852
+ function deliveredEffects(p) {
853
+ const { root, configPath, newsDir, io } = p;
854
+ const publicTitle = (lot) => lot.public ?? (lot.visible ? loadEntries(newsDir).find((e) => e.lots.includes(lot.id))?.title : undefined);
855
+ const plan = existsSync(p.planPath) ? p.loadPlan() : null;
856
+ return { root, plan, today: p.today, articles: readDocsConfig(configPath).articles, publicTitle, planCommit: readPlanConfig(configPath, root)?.commit, waveId: p.waveId, out: io.out, err: io.err };
857
+ }
873
858
  /** « raf ux … » et « raf review … » ont la même forme : activer la porte, ou noter le verdict d'un lot. */
874
859
  function gate(kind, rest, loadPlan, root, today, io, opts) {
875
860
  if (rest.length < 1)
package/dist/config.js CHANGED
@@ -4,16 +4,16 @@ import { parse } from 'yaml';
4
4
  import { isDay } from './dates.js';
5
5
  import { readGate } from './gate-config.js';
6
6
  import { DEFAULT_TEST_PATTERNS } from './testgate.js';
7
+ import { readExtras } from './plan-extras.js';
7
8
  import { gitRoot } from './git.js';
8
9
  import { planCommitProblem } from './plan-commit.js';
10
+ import { parseStates } from './plan-states.js';
9
11
  import { checkTestReplay, readGateTest } from './orchestrate/gate-parts.js';
10
- import { booleanKey } from './yaml-keys.js';
12
+ import { booleanKey, isObject, list } from './yaml-keys.js';
11
13
  import { FIELDS, Plan, RafError, STATUSES } from './plan.js';
12
- const KEYS = ['path', 'project', 'since', 'ignore', 'files', 'lots', 'fields', 'statuses', 'estimates', 'add', 'onWrite', 'commit'];
14
+ const KEYS = ['path', 'project', 'since', 'ignore', 'files', 'lots', 'fields', 'statuses', 'estimates', 'add', 'onWrite', 'commit', 'states', 'transitions', 'doneState', 'blockedState', 'extras'];
13
15
  const ON_WRITE_KEYS = ['command', 'files'];
14
16
  const FORMAT_KEYS = ['lots', 'fields', 'statuses', 'estimates'];
15
- const isObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
16
- const list = (v) => (v == null ? [] : Array.isArray(v) ? v.map(String) : [String(v)]);
17
17
  /** Chemin réel (liens symboliques résolus) du plus proche ancêtre existant, le reste rattaché tel quel. */
18
18
  function realish(p) {
19
19
  try {
@@ -112,6 +112,7 @@ export function readPlanConfig(file, root) {
112
112
  settings.onWrite = readOnWrite(p.onWrite, root ?? dirname(resolve(file)), bad);
113
113
  if (FORMAT_KEYS.some((k) => p[k] != null))
114
114
  settings.format = parseFormat(p, bad);
115
+ Object.assign(settings, parseStates(p, bad, settings.format !== undefined), readExtras(p.extras, !!settings.format, bad));
115
116
  if (p.path != null && String(p.path).trim() === '')
116
117
  throw bad('path est vide');
117
118
  if (p.add != null && (typeof p.add !== 'string' || !p.add.includes('{title}')))
@@ -191,12 +192,11 @@ export function readNewsConfig(file) {
191
192
  for (const k of Object.keys(n))
192
193
  if (k !== 'publicTitleMax')
193
194
  throw new RafError(`${file} : news.${k} inconnu (attendu : publicTitleMax)`);
194
- const max = n.publicTitleMax;
195
- if (max == null)
195
+ if (n.publicTitleMax == null)
196
196
  return {};
197
- if (typeof max !== 'number' || !Number.isInteger(max) || max < 1)
197
+ if (typeof n.publicTitleMax !== 'number' || !Number.isInteger(n.publicTitleMax) || n.publicTitleMax < 1)
198
198
  throw new RafError(`${file} : news.publicTitleMax : nombre entier de caractères (≥ 1) attendu`);
199
- return { publicTitleMax: max };
199
+ return { publicTitleMax: n.publicTitleMax };
200
200
  }
201
201
  const AUTOSTART = ['warn', 'refuse', 'start'];
202
202
  /** Clé `hook:` de cadence.yaml. */
@@ -0,0 +1,37 @@
1
+ import { isPlanOnly } from './audit.js';
2
+ import { isAncestor, readCommits } from './git.js';
3
+ import { citedRefs } from './link.js';
4
+ /**
5
+ * Ligne des lots cités depuis la livraison précédente, ou null (première livraison, rien de cité).
6
+ *
7
+ * Plan en lecture seule : seuls les lots EN COURS sont annoncés. Ses identifiants sont de forme libre, et
8
+ * un message cite volontiers un numéro qui en a la forme (réserve « R1 » d'une revue) ou un lot clos
9
+ * nommé pour le contexte — les annoncer « livrés » ferait fermer à tort. L'état est celui du départ de
10
+ * la livraison : le plan a été lu avant que le script du projet ne ferme lui-même les lots qu'il livre.
11
+ */
12
+ /** Lots cités par les commits livrés depuis la livraison précédente ; vide quand elle est inconnue ou hors de l'historique. */
13
+ export function deliveredIds(ctx, prev, sha) {
14
+ if (!ctx.plan || !prev || prev === sha || !isAncestor(ctx.root, prev, sha))
15
+ return [];
16
+ const lots = ctx.plan.lots();
17
+ const known = new Set((ctx.plan.readonly ? lots.filter((l) => l.status === 'doing') : lots).map((l) => l.id));
18
+ const ids = new Set();
19
+ for (const c of readCommits(ctx.root, { range: `${prev}..${sha}` })) {
20
+ if (isPlanOnly(c.sha, ctx.plan, ctx.root))
21
+ continue; // entretien du plan : ne livre rien, même s'il cite des lots
22
+ for (const r of citedRefs(c, ctx.plan.refs))
23
+ if (known.has(r.lot))
24
+ ids.add(r.lot);
25
+ }
26
+ return [...ids].sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
27
+ }
28
+ export function deliveredLots(ctx, ids, prev, sha) {
29
+ if (!ctx.plan || !prev || prev === sha)
30
+ return null;
31
+ if (!isAncestor(ctx.root, prev, sha)) {
32
+ return `livraison précédente (${prev.slice(0, 7)}) hors de l'historique de ${sha.slice(0, 7)} (réécrit ?) : lots livrés non calculés`;
33
+ }
34
+ if (ids.length === 0)
35
+ return null;
36
+ return `livré : ${ids.join(', ')} — ${ctx.plan.readonly ? "à fermer avec l'outil du projet" : 'raf done'} si l'effet est celui attendu`;
37
+ }
package/dist/deliver.js CHANGED
@@ -4,11 +4,11 @@ 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 { deliveryLots, isPlanOnly } from './audit.js';
8
- import { commitPaths, dirtyPaths, headSha, isAncestor, onRemote, readCommits, repoStatus, resolveCommit } from './git.js';
7
+ import { deliveryLots } from './audit.js';
8
+ import { commitPaths, dirtyPaths, headSha, isAncestor, onRemote, repoStatus, resolveCommit } from './git.js';
9
9
  import { planOnlySince, pollGithubCi, stamped, stepJournal } from './deliver-ci.js';
10
10
  import { PLAN_ACTIONS, planCommitMessage } from './plan-commit.js';
11
- import { citedRefs } from './link.js';
11
+ import { deliveredIds, deliveredLots } from './deliver-lots.js';
12
12
  import { describeArticle, openArticleLots } from './articles.js';
13
13
  import { RafError } from './plan.js';
14
14
  import { appendDelivery, appendFailedDelivery, lastDelivery, lockAlive, lockFault, lockPath, readLock, releaseLock, removeStaleLock, writeLock } from './state.js';
@@ -422,8 +422,7 @@ export async function deliver(ctx, deps) {
422
422
  how = 'moved';
423
423
  }
424
424
  }
425
- const ids = deliveredIds(ctx, lastDelivery(ctx.state), final);
426
- const delivered = deliveredLots(ctx, ids, lastDelivery(ctx.state), final);
425
+ const prev = lastDelivery(ctx.state);
427
426
  appendDelivery(ctx.state, ctx.today, final);
428
427
  const took = `en ${Math.round((deps.now() - startedAt) / 1000)} s`;
429
428
  if (how === 'declared')
@@ -432,9 +431,7 @@ export async function deliver(ctx, deps) {
432
431
  out(stamped(deps, `✓ livré : ${final.slice(0, 7)} (tête déplacée par le script depuis ${env.CADENCE_SHORT}) ${took}`));
433
432
  else
434
433
  out(stamped(deps, `✓ livré${config.verify.length ? ' et vérifié' : ''} : ${env.CADENCE_SHORT} ${took}`));
435
- if (delivered)
436
- out(delivered);
437
- refreshArticles(ctx, ids, final);
434
+ announceDelivered(ctx, prev, final);
438
435
  return 0;
439
436
  }
440
437
  finally {
@@ -504,38 +501,17 @@ async function verifyAll(ctx, deps, sha, env, step) {
504
501
  return null;
505
502
  }
506
503
  /**
507
- * Ligne des lots cités depuis la livraison précédente, ou null (première livraison, rien de cité).
508
- *
509
- * Plan en lecture seule : seuls les lots EN COURS sont annoncés. Ses identifiants sont de forme libre, et
510
- * un message cite volontiers un numéro qui en a la forme (réserve « R1 » d'une revue) ou un lot clos
511
- * nommé pour le contexte — les annoncer « livrés » ferait fermer à tort. L'état est celui du départ de
512
- * la livraison : le plan a été lu avant que le script du projet ne ferme lui-même les lots qu'il livre.
504
+ * Ce qui suit la ligne verte du journal, depuis la livraison verte précédente `prev` (celle d'AVANT la ligne écrite) : les lots cités
505
+ * par les commits livrés sont annoncés (« livré : L… »), puis le lot de l'article à rafraîchir est ouvert (L144). Partagé par deliver
506
+ * et par `cadence verify` (L435) : la ligne que verify écrit est une livraison, la suivante partira d'elle, les lots d'entre-deux se
507
+ * disent donc ici ou jamais.
513
508
  */
514
- /** Lots cités par les commits livrés depuis la livraison précédente ; vide quand elle est inconnue ou hors de l'historique. */
515
- function deliveredIds(ctx, prev, sha) {
516
- if (!ctx.plan || !prev || prev === sha || !isAncestor(ctx.root, prev, sha))
517
- return [];
518
- const lots = ctx.plan.lots();
519
- const known = new Set((ctx.plan.readonly ? lots.filter((l) => l.status === 'doing') : lots).map((l) => l.id));
520
- const ids = new Set();
521
- for (const c of readCommits(ctx.root, { range: `${prev}..${sha}` })) {
522
- if (isPlanOnly(c.sha, ctx.plan, ctx.root))
523
- continue; // entretien du plan : ne livre rien, même s'il cite des lots
524
- for (const r of citedRefs(c, ctx.plan.refs))
525
- if (known.has(r.lot))
526
- ids.add(r.lot);
527
- }
528
- return [...ids].sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
529
- }
530
- function deliveredLots(ctx, ids, prev, sha) {
531
- if (!ctx.plan || !prev || prev === sha)
532
- return null;
533
- if (!isAncestor(ctx.root, prev, sha)) {
534
- return `livraison précédente (${prev.slice(0, 7)}) hors de l'historique de ${sha.slice(0, 7)} (réécrit ?) : lots livrés non calculés`;
535
- }
536
- if (ids.length === 0)
537
- return null;
538
- return `livré : ${ids.join(', ')} — ${ctx.plan.readonly ? "à fermer avec l'outil du projet" : 'raf done'} si l'effet est celui attendu`;
509
+ export function announceDelivered(ctx, prev, sha) {
510
+ const ids = deliveredIds(ctx, prev, sha);
511
+ const delivered = deliveredLots(ctx, ids, prev, sha);
512
+ if (delivered)
513
+ ctx.out(delivered);
514
+ refreshArticles(ctx, ids, sha);
539
515
  }
540
516
  /**
541
517
  * Livraison verte (L144) : les lots livrés dont le titre est public changent ce que les articles externes racontent, on ouvre
@@ -0,0 +1,25 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { parseDocument } from 'yaml';
3
+ import { RafError } from './plan.js';
4
+ /** Fichier d'acquittement docs-ok d'un plan en lecture seule, relatif à la racine du dépôt (L386). */
5
+ export const DOCS_OK_FILE = '.cadence/docs-ok.yaml';
6
+ /** Les acquittements du fichier : lot → { date, reason }. Une entrée sans raison n'acquitte rien ; un fichier illisible est une erreur qui le nomme. */
7
+ export function readDocsOkFile(file) {
8
+ if (!file || !existsSync(file))
9
+ return {};
10
+ const doc = parseDocument(readFileSync(file, 'utf8'));
11
+ if (doc.errors.length > 0)
12
+ throw new RafError(`${file} : ${doc.errors[0].message}`);
13
+ const raw = doc.toJSON();
14
+ if (raw == null)
15
+ return {};
16
+ if (typeof raw !== 'object' || Array.isArray(raw))
17
+ throw new RafError(`${file} : la racine doit être un objet (lot → { date, reason })`);
18
+ const out = {};
19
+ for (const [id, v] of Object.entries(raw)) {
20
+ const e = v;
21
+ if (e && typeof e === 'object' && typeof e.reason === 'string' && e.reason.trim() !== '')
22
+ out[id] = { date: String(e.date ?? ''), reason: e.reason.trim() };
23
+ }
24
+ return out;
25
+ }
@@ -0,0 +1,51 @@
1
+ import { gateProgress, gateText } from './audit.js';
2
+ /** Les lignes de tête de `raf show` : titre, état détaillé (L463), objectif, dates, dépendances, drapeaux, champs d'extension (L464), acquittement docs et condition de tirage. Une ligne par fait posé, aucune pour un fait absent. */
3
+ export function lotLines(plan, lot) {
4
+ const lines = [...headLines(lot), ...flagLines(lot)];
5
+ if (lot.docsOk)
6
+ lines.push(`docs-ok : ${lot.docsOk.date} — ${lot.docsOk.reason}`);
7
+ const gate = gateProgress(plan.lots(), lot);
8
+ if (lot.gate && gate)
9
+ lines.push(lot.gate.invalid ? `gate : ${gateText(gate)}` : `gate : ${gate.n} lots ${gate.label} faits`);
10
+ return lines;
11
+ }
12
+ function headLines(lot) {
13
+ const lines = [`${lot.id} ${lot.status} ${lot.title}`];
14
+ if (lot.state)
15
+ lines.push(`état : ${lot.state}`);
16
+ if (lot.goal)
17
+ lines.push(`goal : ${lot.goal}`);
18
+ const dates = [['créé', lot.created], ['démarré', lot.started], [lot.status === 'dropped' ? 'abandonné' : 'terminé', lot.finished]].filter(([, d]) => d);
19
+ if (dates.length)
20
+ lines.push(dates.map(([k, d]) => `${k} : ${d}`).join(' '));
21
+ if (lot.after.length)
22
+ lines.push(`after : ${lot.after.join(', ')}`);
23
+ if (lot.public)
24
+ lines.push(`public : ${lot.public}`);
25
+ return lines;
26
+ }
27
+ function flagLines(lot) {
28
+ const lines = [];
29
+ // L439 : le livreur lit ici si le lot est visible et s'il porte un verdict UX ; deux lignes stables, absentes d'un lot non visible.
30
+ if (lot.visible)
31
+ lines.push('visible : oui');
32
+ if (lot.screen)
33
+ lines.push(`screen : ${lot.screen}`);
34
+ if (lot.visible)
35
+ lines.push(lot.ux ? `ux : ${lot.ux.verdict}` : 'ux : absent');
36
+ if (lot.spec)
37
+ lines.push('spec : true');
38
+ if (lot.uses)
39
+ lines.push(`uses : ${lot.uses.join(', ')}`);
40
+ for (const [name, value] of Object.entries(lot.extras ?? {}))
41
+ lines.push(`${name} : ${value}`);
42
+ return lines;
43
+ }
44
+ /**
45
+ * Le lot tel que `raf list --json` l'exporte (L464) : les clés du fichier du plan (`docs_ok`, `state_before`, pas `docsOk` ni `stateBefore`), sans `problems` (diagnostic de `raf check`).
46
+ * Les noms exportés sont ceux que garantit docs/stabilite.md : renommer un champ du type interne `Lot` ne les change pas.
47
+ */
48
+ export function lotJson(lot) {
49
+ const { docsOk, stateBefore, problems: _problems, ...rest } = lot;
50
+ return { ...rest, ...(docsOk ? { docs_ok: docsOk } : {}), ...(stateBefore ? { state_before: stateBefore } : {}) };
51
+ }
@@ -24,7 +24,7 @@ import { acceptationFile, acceptText, redAcceptations } from './accept.js';
24
24
  import { noTestGate, noTestText } from './no-test.js';
25
25
  import { ficheReviewText } from './fiche.js';
26
26
  import { maquetteText } from './maquette.js';
27
- import { costlyHypotheses, specScreen, specText } from './spec.js';
27
+ import { budgetStage, costlyHypotheses, lightLot, specText, stageScreen } from './spec.js';
28
28
  import { fichesDuDossier } from '../spec.js';
29
29
  import { candidates, fillTargets, handedBackStreak, parsePriority, parseUntil, drawSettingsReport, readPriority, stopReason } from './continue.js';
30
30
  import { precheckNothingToFind, Budget, ESCALATION_RESERVE, DROPPED, STOP_REQUESTED, MAX_PASSES, REVIEW_RESERVE, budgetStep, heldForEscalation, lotSpent, MAX_PASSES_CONVERGING, briefName, countInterrupted, planNoteHandBack, leadReviewedHandBack, HYPOTHESES_HANDBACK, handBackDroppedQuestions, needsPrecheck, workDir } from './cycle.js';
@@ -320,7 +320,7 @@ export async function preflight(args, targets, io, deps, launch, opts = {}) {
320
320
  }
321
321
  const small = lot.estimate <= 0.5 || lot.quickwin;
322
322
  const goal = args.goal ?? lot.goal;
323
- const screen = specScreen(t.repo, env.config.spec.dir, lot);
323
+ const screen = stageScreen(t.repo, env.config.spec.dir, lot, env.config.maquette);
324
324
  const state = newLot({ project: t.project, repo: t.repo, lot: t.lot, title: lot.title, ...(goal ? { goal } : {}), visible: lot.visible, small, ...(screen ? { screen } : {}), ...(lot.spec && !screen ? { specLot: true } : {}), model: t.model, readOnlyPlan: plan.readonly });
325
325
  if (neighbours.repos.length)
326
326
  state.repos = neighbours.repos;
@@ -329,7 +329,7 @@ export async function preflight(args, targets, io, deps, launch, opts = {}) {
329
329
  state.express = true;
330
330
  state.light = lot.estimate <= env.config.review.threshold;
331
331
  const escalating = !!env.config.escalation;
332
- state.budget = lotBudget(lot.estimate, screen ? 'screen' : lot.spec ? 'spec' : undefined, escalating);
332
+ state.budget = lotBudget(lot.estimate, budgetStage(lot, screen, env.config.maquette, t.repo, env.config.spec.dir), escalating);
333
333
  if (escalating)
334
334
  state.escalationReserve = ESCALATION_RESERVE;
335
335
  state.dependsOn = lot.after.filter((d) => earlier.includes(d));
@@ -442,7 +442,7 @@ function dryRun(lots, io, deps, { budget, id, multiSlot }) {
442
442
  io.out(`${lotKey(l.project, l.lot)} — ${l.title}`);
443
443
  if (l.goal)
444
444
  io.out(` objectif donné : ${l.goal} (à la place du titre dans les briefs)`);
445
- io.out(` budget du lot : ${l.budget} tokens comptés (base : ${lotBudgetBasis(lot.estimate, l.screen ? 'screen' : l.specLot ? 'spec' : undefined, l.escalationReserve ? env.config.escalation?.model : undefined)})`);
445
+ io.out(` budget du lot : ${l.budget} tokens comptés (base : ${lotBudgetBasis(lot.estimate, budgetStage(lot, l.screen, env.config.maquette, l.repo, env.config.spec.dir), l.escalationReserve ? env.config.escalation?.model : undefined)})`);
446
446
  const mine = lots.filter((o) => o.repo === l.repo);
447
447
  const together = env.config.worktrees && (mine.length > 1 || multiSlot) && !mine.some((o) => o.repos?.length) && !!repoBranch(l.repo);
448
448
  io.out(together ? ` file ${basename(l.repo)} · en parallèle des autres lots du dépôt, dans son worktree (cadence/${id}/${lotSlug(l.project, l.lot)}, créé à la prise du créneau)` : ` file ${basename(l.repo)} · ${slot < 2 ? `créneau ${slot + 1}` : 'en attente d\'un créneau'}`);
@@ -459,7 +459,7 @@ function dryRun(lots, io, deps, { budget, id, multiSlot }) {
459
459
  }
460
460
  const steps = [{ kind: 'implement', model: l.model }];
461
461
  const specStage = !!(l.screen || l.specLot);
462
- if (specStage && env.config.accept)
462
+ if (specStage && env.config.accept && !lightLot(l, lot, env.config.maquette, l.repo, env.config.spec.dir))
463
463
  steps.unshift({ kind: 'accept', model: 'sonnet' });
464
464
  if (l.screen && env.config.maquette)
465
465
  steps.unshift({ kind: 'maquette', model: env.config.review.full });
@@ -513,12 +513,12 @@ function dryRun(lots, io, deps, { budget, id, multiSlot }) {
513
513
  const vars = { chemin: l.repo, lot: l.lot, titre: headline(lot, l.goal), objectif: objective(lot, l.goal), commits: '', reponse: '', constats: '', ux: '', choix: '', checks: '', news: l.visible ? newsText(l.lot) : '', captures: '', diff: 'git diff <sha de départ>..HEAD' };
514
514
  vars.decisions = decisionsText(humanDecisions(lot, lot.parent ? env.loadPlan().lots().find((x) => x.id === lot.parent) : undefined));
515
515
  if (specStage)
516
- vars.spec = specText({ dir: env.config.spec.dir, render: env.config.spec.render, screen: l.screen, back: !!l.specLot });
516
+ vars.spec = specText({ dir: env.config.spec.dir, render: env.config.spec.render, screen: l.screen, light: lightLot(l, lot, env.config.maquette, l.repo, env.config.spec.dir), back: !!l.specLot });
517
517
  if (l.screen && env.config.maquette)
518
- vars.maquette = maquetteText({ fiche: `${env.config.spec.dir}/<écran>/fiche.yaml`, passes: 0, max: env.config.maquetteReviews });
518
+ vars.maquette = maquetteText({ fiche: `${env.config.spec.dir}/<écran>/fiche.yaml`, passes: 0, max: env.config.maquetteReviews, light: lightLot(l, lot, env.config.maquette, l.repo, env.config.spec.dir) });
519
519
  if (l.specLot && env.config.fiche)
520
520
  vars.fiche = ficheReviewText({ fiche: `${env.config.spec.dir}/<sujet>/fiche.yaml`, passes: 0 });
521
- if (specStage && env.config.accept) {
521
+ if (specStage && env.config.accept && !lightLot(l, lot, env.config.maquette, l.repo, env.config.spec.dir)) {
522
522
  const folder = `${env.config.spec.dir}/${l.screen ? '<écran>' : '<sujet>'}`;
523
523
  vars.accept = acceptText({ fiche: `${folder}/fiche.yaml`, maquette: `${folder}/maquette.html`, screen: !!l.screen, back: false, acceptation: acceptationFile(`${folder}/fiche.yaml`, l.lot), command: env.config.accept ?? '', test: env.config.test });
524
524
  }
@@ -9,7 +9,7 @@ import { RafError } from '../plan.js';
9
9
  import { lotBudget, projectEnv } from './command.js';
10
10
  import { readyFree } from '../ready-free.js';
11
11
  import { lotFinished, lotKey } from './state.js';
12
- import { specScreen } from './spec.js';
12
+ import { budgetStage, stageScreen } from './spec.js';
13
13
  import { STOP_REQUESTED } from './cycle.js';
14
14
  /** `--until 18:00` : l'heure d'aujourd'hui (date de `now`) à partir de laquelle la vague ne tire plus de lot. */
15
15
  export function parseUntil(text, now) {
@@ -90,11 +90,13 @@ export function candidates(launch, o) {
90
90
  }
91
91
  let specDir = 'docs/specs';
92
92
  let escalating = false;
93
+ let maquette = true;
93
94
  let setting;
94
95
  try {
95
96
  const config = projectEnv(dir).config;
96
97
  specDir = config.spec.dir;
97
98
  escalating = !!config.escalation;
99
+ maquette = config.maquette;
98
100
  setting = config.continue;
99
101
  }
100
102
  catch {
@@ -108,7 +110,7 @@ export function candidates(launch, o) {
108
110
  o.skipped?.push(`${project}:${lot.id} écarté du tirage : ${why}`);
109
111
  continue;
110
112
  }
111
- if (lotBudget(lot.estimate, specScreen(dir, specDir, lot) ? 'screen' : lot.spec ? 'spec' : undefined, escalating) > o.remaining)
113
+ if (lotBudget(lot.estimate, budgetStage(lot, stageScreen(dir, specDir, lot, maquette), maquette, dir, specDir), escalating) > o.remaining)
112
114
  continue;
113
115
  out.push({ project, dir, lot });
114
116
  }