@sylad/cadence 1.39.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.
Files changed (56) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +3 -3
  4. package/agents/spec-writer.md +1 -0
  5. package/agents/ux-reviewer.md +2 -0
  6. package/dist/cli.js +49 -62
  7. package/dist/config-set.js +2 -0
  8. package/dist/config.js +8 -8
  9. package/dist/deliver-lots.js +37 -0
  10. package/dist/deliver.js +15 -39
  11. package/dist/docs-ok-file.js +25 -0
  12. package/dist/lot-lines.js +51 -0
  13. package/dist/memory-index.js +113 -0
  14. package/dist/orchestrate/command.js +8 -8
  15. package/dist/orchestrate/continue.js +4 -2
  16. package/dist/orchestrate/cycle.js +23 -29
  17. package/dist/orchestrate/deja-traitee.js +76 -0
  18. package/dist/orchestrate/integrate.js +7 -2
  19. package/dist/orchestrate/lot-budget.js +2 -2
  20. package/dist/orchestrate/maquette.js +3 -0
  21. package/dist/orchestrate/spec.js +51 -6
  22. package/dist/orchestrate/table.js +29 -11
  23. package/dist/plan-extras.js +118 -0
  24. package/dist/plan-read.js +144 -0
  25. package/dist/plan-states.js +119 -0
  26. package/dist/plan.js +91 -157
  27. package/dist/raf-close-checks.js +25 -0
  28. package/dist/raf-status.js +28 -0
  29. package/dist/session.js +8 -0
  30. package/dist/state.js +23 -0
  31. package/dist/verify.js +20 -3
  32. package/dist/yaml-keys.js +2 -0
  33. package/docs/concepts-livraison-dark.svg +32 -0
  34. package/docs/concepts-livraison-light.svg +32 -0
  35. package/docs/concepts-lot-dark.svg +52 -0
  36. package/docs/concepts-lot-light.svg +52 -0
  37. package/docs/concepts-vague-dark.svg +39 -0
  38. package/docs/concepts-vague-light.svg +39 -0
  39. package/docs/concepts-worktrees-dark.svg +45 -0
  40. package/docs/concepts-worktrees-light.svg +45 -0
  41. package/docs/concepts.md +159 -0
  42. package/docs/deliver.md +24 -2
  43. package/docs/fonctionnement.md +11 -5
  44. package/docs/lead.md +10 -6
  45. package/docs/orchestrate.md +22 -5
  46. package/docs/overview.md +1 -1
  47. package/docs/perf.md +1 -0
  48. package/docs/qualite.md +13 -3
  49. package/docs/raf.md +42 -5
  50. package/docs/session.md +32 -0
  51. package/docs/spec.md +5 -2
  52. package/docs/whats-new.md +6 -0
  53. package/package.json +10 -1
  54. package/skills/lead/SKILL.md +6 -0
  55. package/skills/session-close/SKILL.md +6 -0
  56. package/templates/orchestrate/spec.md +2 -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.39.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.39.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",
package/README.md CHANGED
@@ -4,6 +4,7 @@ A small working method that lives in your repository: a versioned plan linked to
4
4
  waves of fresh AI sessions driven by a program, and deliveries proved by their effect.
5
5
  It is for solo developers and AI-assisted sessions who lose track of *what is left to do* and *what each commit was for*:
6
6
  cadence keeps both in plain files next to the code, and [Claude Code](https://claude.com/claude-code) skills turn it into rituals.
7
+ New to the words (lot, wave, hold…)? [docs/concepts.md](docs/concepts.md) explains each notion, with four figures.
7
8
 
8
9
  ## Start
9
10
 
@@ -15,8 +16,7 @@ raf init
15
16
  raf add "My first lot" --estimate 1
16
17
  ```
17
18
 
18
- Then `raf start L1`, commit with `feat(L1): …`, `raf now` to see where you are, `raf done L1` when it is finished.
19
- `raf -v` prints the installed version. Every command is in [docs/raf.md](docs/raf.md).
19
+ Then `raf start L1`, commit with `feat(L1): …`, `raf now` to see where you are, `raf done L1` when finished. Every command: [docs/raf.md](docs/raf.md).
20
20
 
21
21
  ## Lead several projects in 10 minutes
22
22
 
@@ -53,7 +53,7 @@ Details: [docs/skills.md](docs/skills.md) and [docs/cadence-hud.md](docs/cadence
53
53
  - [docs/deliver.md](docs/deliver.md): wait for the CI, deploy, verify the effect; [docs/remote-gate.md](docs/remote-gate.md): the gate on another machine.
54
54
  - [docs/spec.md](docs/spec.md): screen and back-end sheets.
55
55
  - [docs/ref.md](docs/ref.md): reference documents (specs, norms, mock-ups) and the coverage of their elements by the lots.
56
- - [docs/lead.md](docs/lead.md): the lead workflow, and the `livreur` agent that delivers a ready lot for it.
56
+ - [docs/lead.md](docs/lead.md): the lead workflow, the `livreur` agent, and [the memory index warning](docs/session.md#memory-index-size-l442) (L442).
57
57
  - [docs/overview.md](docs/overview.md): the longer introduction and the map of every page; [docs/fonctionnement.md](docs/fonctionnement.md): how it works.
58
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.
@@ -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';
@@ -24,6 +28,7 @@ import { schedule } from './schedule.js';
24
28
  import { AGENTS_DIR, installAgents, installSkills, SKILLS_DIR } from './skills.js';
25
29
  import { configSet } from './config-set.js';
26
30
  import { movePriority } from './priority.js';
31
+ import { closeMemoryLines, tourMemoryLines } from './memory-index.js';
27
32
  import { findProjects, isProject, readDepth, leadTour, TITLE_MAX, truncate, readLeadUx, tourFreeLine, readLeadContextThreshold, findLeadDir, tourLine, tourNoTestLines, tourWorktreeLines } from './lead.js';
28
33
  import { configuredCap, expiredHoldLines, slotsLine, orchestrate, realOrchestrateDeps } from './orchestrate/command.js';
29
34
  import { addHold, releaseHold } from './orchestrate/registry.js';
@@ -33,7 +38,7 @@ import { blockingLots, LABEL, resumeCommand } from './orchestrate/table.js';
33
38
  import { renderTestsSansCode, testsSansCode } from './corrections-sans-code.js';
34
39
  import { bugCodeOf, bugCodePasses, classifyAll, classifySous, measure, ollamaClassifier, ollamaSousClassifier, readCache, readJournals, readRuns, readSousCache, renderReport, sampleKeys, serializeCache, serializeSousCache } from './corrections.js';
35
40
  import { computePerf, perfHeader, perfWindows, readPlans, readProjectDeliveries, readProjectFailedEntries, renderPerf } from './perf.js';
36
- 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';
37
42
  import { refCommand } from './ref/command.js';
38
43
  import { coverageCommand } from './ref/coverage.js';
39
44
  import { release } from './release.js';
@@ -46,7 +51,7 @@ import { clearNext, readNext, sharedStateDir, stateDir, writeNext } from './stat
46
51
  const HELP = `raf — plan « reste à faire » versionné dans le dépôt, relié aux commits
47
52
 
48
53
  raf init [--project nom] [--prefix L] [--no-hook]
49
- 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]
50
55
  raf public <id> "titre public" | --clear titre du lot dans le langage du public (pages Nouveautés et Plan)
51
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)
52
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:
@@ -55,6 +60,7 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
55
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é
56
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
57
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
58
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)
59
65
  raf note <id> "texte"
60
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)
@@ -67,7 +73,7 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
67
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
68
74
  raf commits <id> les commits du lot que compte la porte de revue de code, du plus ancien au plus récent
69
75
  raf now ce qui est en cours, la suite, les derniers terminés
70
- 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)
71
77
  raf check [--since date] [--idle 7] (défaut : date « since » du plan) code 1 s'il y a des écarts
72
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
73
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
@@ -92,7 +98,7 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
92
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)
93
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
94
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
95
- 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
96
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)
97
103
  raf news new <lot…> [--title t] | list | check | stamp | build [-o dossier] (aussi « cadence news … »)
98
104
 
@@ -182,8 +188,10 @@ function dispatch(argv, io) {
182
188
  title: { type: 'string' },
183
189
  after: { type: 'string' },
184
190
  uses: { type: 'string' },
191
+ set: { type: 'string', multiple: true },
185
192
  parent: { type: 'string' },
186
193
  force: { type: 'boolean' },
194
+ back: { type: 'boolean' },
187
195
  reason: { type: 'string' },
188
196
  porteur: { type: 'string' },
189
197
  status: { type: 'string' },
@@ -270,7 +278,8 @@ function dispatch(argv, io) {
270
278
  for (const r of rows)
271
279
  io.out(tourLine(r));
272
280
  // les worktrees de lot que des vagues ont conservés : aucun n'est oublié en silence (L279, R19)
273
- for (const l of tourWorktreeLines(rows, io.now().getTime()))
281
+ // … puis l'index MEMORY.md du dossier, s'il pèse trop (L442) : le lead propose le regroupement, rien n'est écrit
282
+ for (const l of [...tourWorktreeLines(rows, io.now().getTime()), ...tourMemoryLines(io.env, parent)])
274
283
  io.out(l);
275
284
  // un projet sans orchestrate.test : la vague ne lancera pas la suite (L327)
276
285
  for (const l of tourNoTestLines(rows))
@@ -404,6 +413,8 @@ function dispatch(argv, io) {
404
413
  }
405
414
  if (values.uses !== undefined)
406
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}`);
407
418
  id = plan.addTask(values.parent, rest.join(' '));
408
419
  }
409
420
  else {
@@ -419,7 +430,7 @@ function dispatch(argv, io) {
419
430
  const uses = values.uses?.split(',').map((n) => n.trim()).filter(Boolean);
420
431
  if (uses?.length === 0)
421
432
  throw new RafError(invalidResource(''));
422
- 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) });
423
434
  }
424
435
  plan.save();
425
436
  io.out(id);
@@ -442,24 +453,8 @@ function dispatch(argv, io) {
442
453
  refuseLongTitle(pub, lot?.public ? '' : ' — titre de la Nouveauté reprise par le site : corriger l\'entrée ou poser raf public');
443
454
  }
444
455
  // Le plan ne lit pas git : lui dire combien de commits du lot sa revue ne couvre pas quand elle est exigée.
445
- // Un lot clos avec une acceptation à vert: false bloque les vagues suivantes du dépôt (verrou des tests d'acceptation, L366).
446
- if (status === 'done' && !rest[0].includes('/')) {
447
- const repo = gitRoot(root) ?? root;
448
- let dir = null;
449
- try {
450
- dir = readOrchestrateConfig(configPath).spec.dir;
451
- }
452
- catch {
453
- // un cadence.yaml illisible ne doit pas empêcher de clore un lot : `cadence check` en parle
454
- }
455
- const red = dir === null ? undefined : redAcceptations(repo, dir).find((a) => a.lot === rest[0]);
456
- if (red) {
457
- 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`;
458
- if (!values.force)
459
- throw new RafError(`${text} (--force pour passer outre)`);
460
- io.err(`avertissement : ${text}`);
461
- }
462
- }
456
+ if (status === 'done' && !rest[0].includes('/'))
457
+ refuseRedAcceptation(root, configPath, rest[0], !!values.force, io.err);
463
458
  const unreviewed = status === 'done' && plan.reviewSince && !plan.readonly && !values.force ? unreviewedWork(plan, root, rest[0]) : 0;
464
459
  const verdict = status === 'done' && !rest[0].includes('/') ? plan.lots().find((l) => l.id === rest[0])?.review?.verdict : undefined;
465
460
  const nonConforming = verdict !== undefined && isNonConformingVerdict(verdict);
@@ -510,6 +505,8 @@ function dispatch(argv, io) {
510
505
  }
511
506
  return 0;
512
507
  }
508
+ case 'status':
509
+ return statusCommand(loadPlan(), rest, { back: !!values.back, today, out: io.out, commits: (p, id) => lotWork(p, root, id).length });
513
510
  case 'todo': {
514
511
  need(1, 'todo <id> [--reason texte]');
515
512
  const plan = loadPlan();
@@ -636,32 +633,8 @@ function dispatch(argv, io) {
636
633
  io.out(n);
637
634
  return 0;
638
635
  }
639
- io.out(`${lot.id} ${lot.status} ${lot.title}`);
640
- if (lot.goal)
641
- io.out(`goal : ${lot.goal}`);
642
- const dates = [['créé', lot.created], ['démarré', lot.started], [lot.status === 'dropped' ? 'abandonné' : 'terminé', lot.finished]].filter(([, d]) => d);
643
- if (dates.length)
644
- io.out(dates.map(([k, d]) => `${k} : ${d}`).join(' '));
645
- if (lot.after.length)
646
- io.out(`after : ${lot.after.join(', ')}`);
647
- if (lot.public)
648
- io.out(`public : ${lot.public}`);
649
- // 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.
650
- if (lot.visible)
651
- io.out('visible : oui');
652
- if (lot.screen)
653
- io.out(`screen : ${lot.screen}`);
654
- if (lot.visible)
655
- io.out(lot.ux ? `ux : ${lot.ux.verdict}` : 'ux : absent');
656
- if (lot.spec)
657
- io.out('spec : true');
658
- if (lot.uses)
659
- io.out(`uses : ${lot.uses.join(', ')}`);
660
- if (lot.docsOk)
661
- io.out(`docs-ok : ${lot.docsOk.date} — ${lot.docsOk.reason}`);
662
- const gate = gateProgress(plan.lots(), lot);
663
- if (lot.gate && gate)
664
- 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);
665
638
  if (notes.length) {
666
639
  io.out('notes :');
667
640
  // Les lignes suivantes d'une note multiligne s'alignent sous son texte, pas en colonne 0.
@@ -680,9 +653,12 @@ function dispatch(argv, io) {
680
653
  const plan = loadPlan();
681
654
  if (values.status && !STATUSES.includes(values.status))
682
655
  throw new RafError(`statut inconnu : ${values.status}`);
683
- for (const l of plan.lots().filter((l) => !values.status || l.status === values.status)) {
684
- io.out(`${l.id.padEnd(6)} ${l.status.padEnd(8)} ${l.quickwin ? '⚡ ' : ''}${l.title}`);
685
- }
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}`);
686
662
  return 0;
687
663
  }
688
664
  case 'now':
@@ -779,7 +755,7 @@ function dispatch(argv, io) {
779
755
  const sconf = rest[0] === 'start' || rest[0] === 'close' ? readSessionConfig(configPath) : {};
780
756
  const facts = rest[0] === 'start' || rest[0] === 'close' ? sconf[rest[0]] : undefined;
781
757
  const clean = rest[0] === 'close' && sconf.clean ? { patterns: sconf.clean, days: sconf.cleanDays } : undefined;
782
- const sctx = { plan: loadPlan(), root, newsDir, state: stateDir(root), shared: sharedStateDir(root), today, now: io.now(), out: io.out, facts, clean };
758
+ const sctx = { plan: loadPlan(), root, newsDir, state: stateDir(root), shared: sharedStateDir(root), today, now: io.now(), out: io.out, facts, clean, memory: closeMemoryLines(io.env, io.cwd, configPath) };
783
759
  if (rest[0] !== 'start')
784
760
  return session(rest, sctx, values);
785
761
  // La reprise rejoue les vérifications d'effet (un essai, borné) : un effet rouge d'hier se lit avec les faits du matin.
@@ -814,12 +790,8 @@ function dispatch(argv, io) {
814
790
  if (!existsSync(configPath))
815
791
  throw new RafError(`pas de configuration de livraison : ${configPath} (voir « cadence.yaml » dans docs/deliver.md du paquet @sylad/cadence)`);
816
792
  const config = parseDeliverConfig(readFileSync(configPath, 'utf8'), configPath);
817
- const plan = existsSync(planPath) ? loadPlan() : null;
818
- const articles = readDocsConfig(configPath).articles;
819
- // Sans public: d'un lot visible, le site reprend le titre de sa Nouveauté la plus récente.
820
- const publicTitle = (lot) => lot.public ?? (lot.visible ? loadEntries(newsDir).find((e) => e.lots.includes(lot.id))?.title : undefined);
821
- 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 };
822
- 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));
823
795
  }
824
796
  case 'verify': {
825
797
  if (values.gate)
@@ -835,7 +807,12 @@ function dispatch(argv, io) {
835
807
  const target = values.sha ? { sha: resolveCommit(root, values.sha) ?? '', note: null } : defaultTarget(root, config);
836
808
  if (values.sha && !target.sha)
837
809
  throw new RafError(`--sha ${values.sha} : commit introuvable`);
838
- 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));
839
816
  }
840
817
  case 'skills': {
841
818
  if (rest[0] !== 'install')
@@ -868,6 +845,16 @@ const GATES = {
868
845
  ux: { on: 'revue UX obligatoire pour les lots visibles', already: 'revue UX déjà active', action: PLAN_ACTIONS.uxRecorded },
869
846
  review: { on: 'revue de code obligatoire pour les lots à commits', already: 'revue de code déjà active', action: PLAN_ACTIONS.reviewRecorded },
870
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
+ }
871
858
  /** « raf ux … » et « raf review … » ont la même forme : activer la porte, ou noter le verdict d'un lot. */
872
859
  function gate(kind, rest, loadPlan, root, today, io, opts) {
873
860
  if (rest.length < 1)
@@ -7,6 +7,7 @@ import { defaultPlan, readDocsConfig, readHookConfig, readMigrateConfig, readNew
7
7
  import { parseDeliverConfig } from './deliver.js';
8
8
  import { commitPaths, dirtyPaths, gitRoot, restorePaths } from './git.js';
9
9
  import { findProjects, isProject, readDepth, readLeadContextThreshold, readLeadUx } from './lead.js';
10
+ import { readMemoryIndexMax } from './memory-index.js';
10
11
  import { readPriority } from './orchestrate/continue.js';
11
12
  import { Plan, RafError } from './plan.js';
12
13
  const KEY = /^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*$/;
@@ -25,6 +26,7 @@ export function validateConfig(file, root) {
25
26
  readDocsConfig(file);
26
27
  readReferencesConfig(file);
27
28
  readMigrateConfig(file);
29
+ readMemoryIndexMax(file);
28
30
  readLeadUx(dirname(file));
29
31
  readLeadContextThreshold(dirname(file));
30
32
  readDepth(dirname(file));
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
+ }