@sylad/cadence 1.40.0 → 1.42.1

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 (69) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/spec-writer.md +1 -0
  4. package/agents/ux-reviewer.md +2 -0
  5. package/dist/archive.js +103 -6
  6. package/dist/audit.js +2 -2
  7. package/dist/clean.js +0 -4
  8. package/dist/cli/context.js +129 -0
  9. package/dist/cli/deliver-commands.js +114 -0
  10. package/dist/cli/gates.js +234 -0
  11. package/dist/cli/lead-commands.js +161 -0
  12. package/dist/cli/lot-commands.js +257 -0
  13. package/dist/cli/read-commands.js +227 -0
  14. package/dist/cli/session-commands.js +152 -0
  15. package/dist/cli/setup-commands.js +72 -0
  16. package/dist/cli/spec-commands.js +298 -0
  17. package/dist/cli/stats-commands.js +138 -0
  18. package/dist/cli-add.js +37 -0
  19. package/dist/cli.js +85 -1669
  20. package/dist/config.js +21 -108
  21. package/dist/corrections.js +28 -42
  22. package/dist/deliver-lots.js +37 -0
  23. package/dist/deliver.js +15 -39
  24. package/dist/docs-ok-file.js +25 -0
  25. package/dist/docsync.js +15 -3
  26. package/dist/lead.js +6 -24
  27. package/dist/lot-lines.js +51 -0
  28. package/dist/orchestrate/attente.js +82 -0
  29. package/dist/orchestrate/command.js +26 -25
  30. package/dist/orchestrate/continue.js +6 -14
  31. package/dist/orchestrate/cycle.js +18 -27
  32. package/dist/orchestrate/integrate.js +7 -2
  33. package/dist/orchestrate/integration-vivante.js +138 -0
  34. package/dist/orchestrate/launch.js +6 -18
  35. package/dist/orchestrate/lock.js +3 -20
  36. package/dist/orchestrate/lot-budget.js +2 -2
  37. package/dist/orchestrate/maquette.js +3 -0
  38. package/dist/orchestrate/registry.js +8 -1
  39. package/dist/orchestrate/schemas.js +5 -1
  40. package/dist/orchestrate/snapshot.js +13 -0
  41. package/dist/orchestrate/spec.js +51 -6
  42. package/dist/orchestrate/state.js +17 -0
  43. package/dist/plan-extras.js +119 -0
  44. package/dist/plan-ids.js +95 -0
  45. package/dist/plan-read.js +146 -0
  46. package/dist/plan-states.js +119 -0
  47. package/dist/plan.js +119 -190
  48. package/dist/raf-close-checks.js +25 -0
  49. package/dist/raf-status.js +28 -0
  50. package/dist/ref/command.js +2 -3
  51. package/dist/resources.js +3 -9
  52. package/dist/state.js +31 -8
  53. package/dist/verify-gate.js +33 -2
  54. package/dist/verify.js +20 -3
  55. package/dist/yaml-keys.js +21 -0
  56. package/docs/cadence-hud.md +1 -1
  57. package/docs/deliver.md +25 -3
  58. package/docs/fonctionnement.md +9 -5
  59. package/docs/lead.md +9 -7
  60. package/docs/news.md +2 -0
  61. package/docs/orchestrate.md +24 -7
  62. package/docs/perf.md +1 -0
  63. package/docs/qualite.md +14 -4
  64. package/docs/raf.md +62 -7
  65. package/docs/spec.md +5 -2
  66. package/docs/whats-new.md +9 -0
  67. package/package.json +1 -1
  68. package/skills/lead/SKILL.md +1 -1
  69. 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.40.0",
11
+ "version": "1.42.1",
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.42.1",
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/archive.js CHANGED
@@ -29,13 +29,17 @@ export function selectArchivable(lots, before, blocked = () => null) {
29
29
  }
30
30
  /** Le numéro d'un identifiant (`L12` → 12) ; NaN pour un identifiant sans numéro. */
31
31
  const num = (id) => Number(/\d+$/.exec(id)?.[0] ?? NaN);
32
+ /** La clé d'un lot archivé qui garde sa place : l'identifiant du lot qui le précédait dans le plan présenté (vide : il le commençait). */
33
+ export const PLACE_KEY = 'place';
32
34
  /**
33
- * Les lots archivés remis à leur place parmi ceux du fichier actif : chacun juste avant le premier lot actif de numéro plus grand, à la fin
34
- * sinon, les archivés entre eux par numéro. L'ordre du fichier actif ne bouge jamais ; un plan que `raf add` tient dans l'ordre des numéros
35
- * (le cas ordinaire) se lit exactement comme avant l'archivage (L396).
35
+ * Les lots archivés remis à leur place parmi ceux du fichier actif. Un lot qui porte un rang (`places` : id → id du lot qui le précédait
36
+ * dans le plan présenté au moment de l'archivage, `raf archive` le pose sur tout lot archivé dès que `plan.idPattern` est déclaré : le nombre final d'un id ne dit alors rien de
37
+ * l'ordre) revient juste après ce lot, en tête quand le rang est vide, à la fin quand le lot du rang n'existe plus. Les autres vont juste
38
+ * avant le premier lot actif de numéro plus grand, à la fin sinon, entre eux par numéro. L'ordre du fichier actif ne bouge jamais ; un plan que
39
+ * `raf add` tient dans l'ordre des numéros (le cas ordinaire) se lit exactement comme avant l'archivage (L396).
36
40
  */
37
- export function mergeArchived(archived, active) {
38
- const queue = [...archived].sort((a, b) => num(a.id) - num(b.id) || 0);
41
+ export function mergeArchived(archived, active, places = new Map()) {
42
+ const queue = archived.filter((l) => !places.has(l.id)).sort((a, b) => num(a.id) - num(b.id) || 0);
39
43
  const out = [];
40
44
  for (const lot of active) {
41
45
  const n = num(lot.id);
@@ -43,7 +47,100 @@ export function mergeArchived(archived, active) {
43
47
  out.push(queue.shift());
44
48
  out.push(lot);
45
49
  }
46
- return [...out, ...queue];
50
+ out.push(...queue);
51
+ return placeRanked(out, archived.filter((l) => places.has(l.id)), places);
52
+ }
53
+ /**
54
+ * Pose les lots à rang derrière leur prédécesseur, en un passage (L536 : sous `plan.idPattern` les archivés forment une seule chaîne de rangs,
55
+ * un plan de 685 lots ne peut pas payer k²·n à chaque lecture). Les lots d'un même rang se suivent dans l'ordre d'archivage (celui du fichier),
56
+ * chacun suivi des lots dont il est lui-même le rang ; le rang vide ouvre le plan. Un lot dont le prédécesseur ne viendra jamais (inconnu,
57
+ * cycle, lui-même) va à la fin du plan, dans l'ordre d'archivage.
58
+ */
59
+ function placeRanked(out, ranked, places) {
60
+ const followers = new Map();
61
+ for (const l of ranked) {
62
+ const from = places.get(l.id);
63
+ followers.set(from, [...(followers.get(from) ?? []), l]);
64
+ }
65
+ const result = [];
66
+ const placed = new Set();
67
+ // Un lot posé entraîne ses suiveurs, puis les leurs : le parcours en profondeur (pile explicite, une chaîne peut être longue) rend
68
+ // exactement l'ordre que donnait la pose tour par tour.
69
+ const emit = (from) => {
70
+ const stack = [[...(followers.get(from) ?? [])].reverse()];
71
+ while (stack.length > 0) {
72
+ const next = stack[stack.length - 1].pop();
73
+ if (next === undefined)
74
+ stack.pop();
75
+ else if (!placed.has(next)) {
76
+ result.push(next);
77
+ placed.add(next);
78
+ stack.push([...(followers.get(next.id) ?? [])].reverse());
79
+ }
80
+ }
81
+ };
82
+ emit('');
83
+ // Un prédécesseur dont l'id est aussi dans le fichier actif (plan déjà en erreur) : les suiveurs vont derrière celui du fichier.
84
+ const seen = new Set();
85
+ for (const lot of out) {
86
+ result.push(lot);
87
+ if (!seen.has(lot.id))
88
+ emit(lot.id);
89
+ seen.add(lot.id);
90
+ }
91
+ return [...result, ...ranked.filter((l) => !placed.has(l))];
92
+ }
93
+ /**
94
+ * Les rangs à réécrire quand des lots archivés rouvrent (`back`, parmi `nodes`, les lots de toutes les archives) : un lot dont le `place:` nommait un lot
95
+ * rouvert est re-pointé sur le prédécesseur de celui-ci, de proche en proche, pour que seul le lot rouvert bouge dans le plan présenté (L536). Un lot rouvert
96
+ * sans rang (archivé avant `plan.idPattern`) prend pour prédécesseur celui du plan présenté (`order`, lu seulement alors). Rend les lots restés archivés dont le rang change.
97
+ */
98
+ function repointPlaces(nodes, back, order) {
99
+ const places = new Map();
100
+ for (const n of nodes) {
101
+ const v = n.get(PLACE_KEY);
102
+ if (typeof v === 'string' || v === null)
103
+ places.set(n, v ?? '');
104
+ }
105
+ let presented;
106
+ for (const n of back.filter((b) => !places.has(b))) {
107
+ presented ??= order();
108
+ places.set(n, presented[presented.indexOf(String(n.get('id'))) - 1] ?? '');
109
+ }
110
+ const moved = new Map();
111
+ for (const n of back) {
112
+ const id = String(n.get('id'));
113
+ for (const s of nodes.filter((o) => places.get(o) === id)) {
114
+ places.set(s, places.get(n));
115
+ if (!back.includes(s))
116
+ moved.set(s, places.get(s));
117
+ }
118
+ }
119
+ return moved;
120
+ }
121
+ /**
122
+ * Les lots archivés qui ne sont plus clos (`raf todo` sur un lot done archivé) reviennent à la fin de la liste `active` du plan : seuls eux bougent,
123
+ * les lots restés archivés dont le `place:` les nommait sont re-pointés (`repointPlaces`, L536). Chaque archive touchée est marquée `dirty`.
124
+ */
125
+ export function reopenArchived(archives, active, order) {
126
+ const nodes = archives.flatMap((a) => archiveLots(a.doc).items.filter(isMap).map((n) => ({ a, n: n })));
127
+ const back = nodes.filter(({ n }) => !['done', 'dropped'].includes(String(n.get('status'))));
128
+ if (back.length === 0)
129
+ return;
130
+ const moved = repointPlaces(nodes.map(({ n }) => n), back.map(({ n }) => n), order);
131
+ for (const { a, n } of nodes) {
132
+ if (back.some((b) => b.n === n)) {
133
+ const seq = archiveLots(a.doc);
134
+ seq.items = seq.items.filter((i) => i !== n);
135
+ n.delete(PLACE_KEY);
136
+ active.add(n);
137
+ }
138
+ else if (moved.has(n))
139
+ n.set(PLACE_KEY, moved.get(n));
140
+ else
141
+ continue;
142
+ a.dirty = true;
143
+ }
47
144
  }
48
145
  /** Année de rangement d'un lot : celle de sa fin, à défaut de sa création, à défaut d'aujourd'hui. */
49
146
  export function archiveYear(lot, today) {
package/dist/audit.js CHANGED
@@ -6,7 +6,7 @@ import { changedFiles, fileAt, isAncestor, preloadChangedFiles, readCommits } fr
6
6
  import { citedRefs, linkCommits, scopeOf } from './link.js';
7
7
  import { PLAN_DEFAULT, PLAN_LEGACY, readDocsConfig, readNewsConfig, readOrchestrateConfig } from './config.js';
8
8
  import { ficheHypotheses } from './spec.js';
9
- import { docSyncGaps, filesOf, gapMessage } from './docsync.js';
9
+ import { gapMessage, lotDocSyncGaps } from './docsync.js';
10
10
  import { loadEntries, newsIssues, PUBLIC_TITLE_DEFAULT, publicTitleTooLong, reusedNewsTitle } from './news.js';
11
11
  import { isRecurring } from './recurring.js';
12
12
  import { resourceIssues } from './resources.js';
@@ -233,7 +233,7 @@ function docSyncIssues(plan, root, byLot) {
233
233
  .lots()
234
234
  .filter((l) => !l.docsOk)
235
235
  .filter((l) => l.status === 'doing' || (l.status === 'done' && !!since && !!l.finished && l.finished >= since))
236
- .flatMap((l) => docSyncGaps(sync, filesOf(root, workCommits(plan, root, byLot.get(l.id) ?? []))).map((g) => ({ message: gapMessage(l.id, g) })));
236
+ .flatMap((l) => lotDocSyncGaps(sync, root, workCommits(plan, root, byLot.get(l.id) ?? [])).map((g) => ({ message: gapMessage(l.id, g) })));
237
237
  }
238
238
  /** Commits qui portent du travail sur un lot : ni antérieurs à l'adoption du plan, ni acquittés (`raf ignore`), ni réduits au plan, ni une simple version. */
239
239
  function workCommits(plan, root, commits) {
package/dist/clean.js CHANGED
@@ -219,7 +219,3 @@ export function scanStale(root, patterns, days, today) {
219
219
  unreadable: [...unreadable].sort(),
220
220
  };
221
221
  }
222
- /** Éléments périmés à proposer (voir `scanStale`), du plus vieux au plus récent. Ne supprime rien. */
223
- export function staleFiles(root, patterns, days, today) {
224
- return scanStale(root, patterns, days, today).stale;
225
- }
@@ -0,0 +1,129 @@
1
+ // Le contexte commun des commandes de raf et de cadence (L488, sorti de cli.ts) : les options, ce que dispatch construit, les assistants partagés.
2
+ import { parseArgs } from 'node:util';
3
+ import { dirname, join, relative, resolve } from 'node:path';
4
+ import { defaultPlan, PLAN_DEFAULT, PLAN_LEGACY, readNewsConfig, readPlanConfig } from '../config.js';
5
+ import { shellQuote } from '../deliver.js';
6
+ import { gitRoot, isTracked } from '../git.js';
7
+ import { publicTitleTooLong } from '../news.js';
8
+ import { Plan, RafError } from '../plan.js';
9
+ /** Les options de la ligne de commande, toutes commandes confondues : une option n'a de sens que pour certaines, chaque commande lit les siennes. */
10
+ const OPTIONS = {
11
+ file: { type: 'string' },
12
+ project: { type: 'string' },
13
+ prefix: { type: 'string' },
14
+ 'no-hook': { type: 'boolean' },
15
+ estimate: { type: 'string' },
16
+ quickwin: { type: 'boolean' },
17
+ visible: { type: 'boolean' },
18
+ screen: { type: 'string' },
19
+ spec: { type: 'boolean' },
20
+ local: { type: 'boolean' },
21
+ public: { type: 'string' },
22
+ every: { type: 'string' },
23
+ dir: { type: 'string' },
24
+ title: { type: 'string' },
25
+ after: { type: 'string' },
26
+ uses: { type: 'string' },
27
+ set: { type: 'string', multiple: true },
28
+ parent: { type: 'string' },
29
+ id: { type: 'string' },
30
+ under: { type: 'string' },
31
+ force: { type: 'boolean' },
32
+ back: { type: 'boolean' },
33
+ reason: { type: 'string' },
34
+ porteur: { type: 'string' },
35
+ status: { type: 'string' },
36
+ since: { type: 'string' },
37
+ before: { type: 'string' },
38
+ idle: { type: 'string' },
39
+ ttl: { type: 'string' },
40
+ session: { type: 'string' },
41
+ all: { type: 'boolean' },
42
+ depth: { type: 'string' },
43
+ output: { type: 'string', short: 'o' },
44
+ config: { type: 'string' },
45
+ 'dry-run': { type: 'boolean' },
46
+ sha: { type: 'string' },
47
+ retry: { type: 'string' },
48
+ gate: { type: 'boolean' },
49
+ clear: { type: 'boolean' },
50
+ ignored: { type: 'boolean' },
51
+ notes: { type: 'boolean' },
52
+ json: { type: 'boolean' },
53
+ verbose: { type: 'boolean' },
54
+ classify: { type: 'boolean' },
55
+ model: { type: 'string' },
56
+ cache: { type: 'string' },
57
+ 'cache-sous-causes': { type: 'string' },
58
+ sample: { type: 'string' },
59
+ sections: { type: 'string' },
60
+ slug: { type: 'string' },
61
+ quoi: { type: 'string' },
62
+ 'sans-rendu': { type: 'boolean' },
63
+ out: { type: 'string' },
64
+ seuil: { type: 'string' },
65
+ 'model-b': { type: 'string' },
66
+ double: { type: 'boolean' },
67
+ largeur: { type: 'string' },
68
+ until: { type: 'string' },
69
+ help: { type: 'boolean', short: 'h' },
70
+ };
71
+ export function parseCli(argv) {
72
+ return parseArgs({ args: argv, allowPositionals: true, options: OPTIONS });
73
+ }
74
+ /** Le contexte d'une commande qui lit le plan : ce que dispatch construit une fois, après les commandes qui n'en ont pas besoin. */
75
+ export function buildCtx(base) {
76
+ const { command, rest, values, io, root } = base;
77
+ // cadence.yaml peut dire où est le plan et, s'il est tenu par un autre outil, comment le lire.
78
+ const configPath = resolve(io.cwd, values.config ?? join(root, 'cadence.yaml'));
79
+ // Installer les skills ou le hook ne lit pas le plan : un cadence.yaml fautif ne doit pas l'empêcher.
80
+ const installing = command === 'skills' || (command === 'hook' && rest[0] === 'install');
81
+ const planConfig = installing ? null : readPlanConfig(configPath, root);
82
+ // Le défaut (ni --file, ni RAF_FILE, ni plan:) : .cadence/raf.yaml, ou le docs/plan/raf.yaml hérité, dont le déplacement est proposé sur stderr (L246).
83
+ // --file et RAF_FILE se lisent depuis le dossier courant ; le plan: de cadence.yaml, depuis la racine du dépôt où est ce fichier.
84
+ const chosen = values.file ?? io.env.RAF_FILE ?? planConfig?.path;
85
+ const fallback = chosen === undefined ? defaultPlan(root) : null;
86
+ const planPath = resolve(io.cwd, values.file ?? io.env.RAF_FILE ?? (planConfig?.path !== undefined ? resolve(root, planConfig.path) : fallback.path));
87
+ if (fallback?.legacy && !installing)
88
+ (io.notice ?? io.err)(legacyPlanLine(root, fallback.legacy));
89
+ const loadPlan = () => Plan.load(planPath, { ...planConfig?.settings, config: configPath });
90
+ const newsDir = resolve(io.cwd, values.dir ?? join(root, 'docs/nouveautes'));
91
+ // Limite DÉCLARÉE (news.publicTitleMax) : sans clé, seul « raf check » avertit ; avec, la pose d'un titre public est refusée.
92
+ const refuseLongTitle = (text, hint = '') => {
93
+ const max = readNewsConfig(configPath).publicTitleMax;
94
+ const m = max === undefined ? null : publicTitleTooLong(text, max);
95
+ if (m)
96
+ throw new RafError(m + hint);
97
+ };
98
+ const need = (n, usage) => {
99
+ if (rest.length < n)
100
+ throw new RafError(`usage : raf ${usage}`);
101
+ };
102
+ return { ...base, configPath, planConfig, planPath, chosen, loadPlan, newsDir, refuseLongTitle, need };
103
+ }
104
+ /** `--verbose` (L453) : la ligne `id état titre entier` du lot touché par `done`, `ux`, `review`, `note`, `move`, `public` ; sans lui, rien de plus (une sous-tâche n'a pas de ligne). */
105
+ export function touchedLot(plan, id, verbose, io) {
106
+ const lot = verbose ? plan.lots().find((l) => l.id === id) : undefined;
107
+ if (lot)
108
+ io.out(`${lot.id} ${lot.status} ${lot.title}`);
109
+ }
110
+ export function knownLot(plan, id, subtask) {
111
+ const lot = plan.lots().find((l) => l.id === id);
112
+ if (!lot)
113
+ throw new RafError(id.includes('/') ? `${subtask} : ${id}` : `lot inconnu : ${id}`);
114
+ return lot;
115
+ }
116
+ /** Le plan, relatif à la racine du dépôt git (comme les fichiers d'un commit), séparateurs `/`. */
117
+ export function planFileIn(root, planPath) {
118
+ return relative(gitRoot(root) ?? root, planPath).split('\\').join('/');
119
+ }
120
+ /**
121
+ * La ligne stderr d'un docs/plan/raf.yaml hérité : le déplacement proposé (jamais fait), ou l'avertissement quand .cadence/raf.yaml existe aussi.
122
+ * La commande proposée commence par `cd <racine>` : elle s'exécute telle quelle depuis n'importe quel dossier (et non depuis la racine
123
+ * seulement) et, sous `session --all`, elle nomme le projet dont il s'agit.
124
+ */
125
+ function legacyPlanLine(root, legacy) {
126
+ if (legacy === 'ignored')
127
+ return `raf: ${PLAN_LEGACY} est ignoré : ${PLAN_DEFAULT} existe aussi et fait foi (supprimer ou fusionner l'ancien)`;
128
+ return `raf: ${PLAN_LEGACY} est l'ancien emplacement du plan ; le déplacer : cd ${shellQuote(root)} && mkdir -p ${dirname(PLAN_DEFAULT)} && ${gitRoot(root) && isTracked(root, PLAN_LEGACY) ? 'git mv' : 'mv'} ${PLAN_LEGACY} ${PLAN_DEFAULT}`;
129
+ }
@@ -0,0 +1,114 @@
1
+ // deliver, verify (et sa porte --gate) et local : la livraison du projet (L488, sorti de cli.ts).
2
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { defaultPlan, readDocsConfig, readOrchestrateConfig, readPlanConfig, readVerifyConfig } from '../config.js';
5
+ import { deliver, parseDeliverConfig, realDeps } from '../deliver.js';
6
+ import { defaultTarget, realCheckDeps, verifyCommand } from '../verify.js';
7
+ import { verifyGate } from '../verify-gate.js';
8
+ import { gitRoot, resolveCommit } from '../git.js';
9
+ import { loadEntries } from '../news.js';
10
+ import { RafError } from '../plan.js';
11
+ import { waveHolding } from '../repo-busy.js';
12
+ import { cadenceHome, waveLotsIn } from '../orchestrate/registry.js';
13
+ import { blockingLots, LABEL } from '../orchestrate/table.js';
14
+ import { localCommand, localLotOf } from '../local-task.js';
15
+ import { sharedStateDir } from '../state.js';
16
+ import { planFileIn } from './context.js';
17
+ /**
18
+ * Ce qu'une livraison verte annonce et ouvre (lots livrés, article à rafraîchir, L144) : pour `cadence deliver` et pour la ligne verte
19
+ * que `cadence verify` inscrit (L435). Sans `public:`, un lot visible tire son titre de sa Nouveauté la plus récente.
20
+ */
21
+ function deliveredEffects(p) {
22
+ const { root, configPath, newsDir, io } = p;
23
+ const publicTitle = (lot) => lot.public ?? (lot.visible ? loadEntries(newsDir).find((e) => e.lots.includes(lot.id))?.title : undefined);
24
+ const plan = existsSync(p.planPath) ? p.loadPlan() : null;
25
+ 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 };
26
+ }
27
+ /** `verify --gate --sha rév [--project dossier]` (L348) : la porte du projet rejouée au sha ; `--project` est un sous-dossier du dossier courant qui a son propre dépôt. */
28
+ async function verifyGateCommand(values, io) {
29
+ if (values.sha === undefined)
30
+ throw new RafError('verify --gate : --sha obligatoire (le sha à re-vérifier, jamais la tête mouvante d\'une vague)');
31
+ if (values.retry !== undefined)
32
+ throw new RafError('verify --gate : --retry ne va qu\'à verify seul (deliver.verify)');
33
+ const dir = values.project === undefined ? io.cwd : resolve(io.cwd, values.project);
34
+ if (values.project !== undefined && !existsSync(dir))
35
+ throw new RafError(`verify --gate : --project ${values.project} : dossier introuvable sous ${io.cwd}`);
36
+ const top = gitRoot(dir);
37
+ if (!top)
38
+ throw new RafError('verify --gate : à lancer dans un dépôt git (ou --project vers le dossier d\'un projet)');
39
+ if (values.project !== undefined && realpathSync(top) !== realpathSync(dir))
40
+ throw new RafError(`verify --gate : ${values.project} n'a pas son propre dépôt git (dépôt parent : ${top})`);
41
+ const configPath = resolve(io.cwd, values.config ?? join(top, 'cadence.yaml'));
42
+ const sha = resolveCommit(top, values.sha);
43
+ if (!sha)
44
+ throw new RafError(`verify --gate : --sha ${values.sha} : commit introuvable dans ${top}`);
45
+ // Le plan du projet (L444) : les commits qui ne changent que lui au-dessus du sha reprennent la porte du sha dessous. Illisible, il n'est pas cherché : la porte se rejoue.
46
+ let planFile;
47
+ try {
48
+ const configured = readPlanConfig(configPath, top)?.path;
49
+ planFile = planFileIn(top, configured !== undefined ? resolve(top, configured) : defaultPlan(top).path);
50
+ }
51
+ catch {
52
+ planFile = undefined;
53
+ }
54
+ return verifyGate({ repo: top, sha, config: readOrchestrateConfig(configPath), verify: readVerifyConfig(configPath), launchDirs: [io.cwd, top, dirname(top)], planFile, now: io.now, out: io.out });
55
+ }
56
+ /** cadence deliver : refusé pendant une vague sauf un sha livrable, puis la livraison du projet. */
57
+ export function cmdDeliver(c) {
58
+ const { rest, values, io, today, root, configPath, planPath, loadPlan, newsDir } = c;
59
+ if (!gitRoot(io.cwd))
60
+ throw new RafError('deliver : à lancer dans un dépôt git');
61
+ const orchestrating = waveHolding(root, 'deliver');
62
+ if (orchestrating) {
63
+ const refusal = `deliver : orchestration en cours (vague ${orchestrating.wave}, pid ${orchestrating.pid})`;
64
+ if (values.sha === undefined)
65
+ throw new RafError(`${refusal} — livrer une fois les lots de ce dépôt finis (le verrou tombe dès que tous sont prêts ou rendus), ou un sha « livrable jusqu'à » de la vague (--sha)`);
66
+ // L277 : un sha dont tous les commits de la vague citent des lots prêts se livre au fil de l'eau ; l'état de la vague illisible ou un sha inconnu : refus.
67
+ const lots = waveLotsIn(cadenceHome(), orchestrating.pid, root);
68
+ const target = resolveCommit(root, values.sha);
69
+ if (lots === null)
70
+ throw new RafError(`${refusal} — état de la vague illisible : --sha ${values.sha} ne peut pas être vérifié`);
71
+ if (!target)
72
+ throw new RafError(`${refusal} — --sha ${values.sha} : commit introuvable`);
73
+ const { blocking, unreadable } = blockingLots(lots, root, target);
74
+ if (blocking.length > 0) {
75
+ throw new RafError(`${refusal} — ${target.slice(0, 7)} contient des commits de lots pas prêts : ${blocking.map((l) => `${l.project}:${l.lot} (${LABEL[l.status]})`).join(', ')} ; livrer un sha antérieur (« livrable jusqu'à » du tableau de la vague) ou attendre leur fin`);
76
+ }
77
+ if (unreadable.length > 0) {
78
+ throw new RafError(`${refusal} — commits illisibles (plan du projet ou dépôt) des lots pas prêts ${unreadable.map((l) => `${l.project}:${l.lot} (${LABEL[l.status]})`).join(', ')} : --sha ${values.sha} ne peut pas être vérifié ; rétablir le plan ou attendre la fin de ces lots`);
79
+ }
80
+ }
81
+ if (!existsSync(configPath))
82
+ throw new RafError(`pas de configuration de livraison : ${configPath} (voir « cadence.yaml » dans docs/deliver.md du paquet @sylad/cadence)`);
83
+ const config = parseDeliverConfig(readFileSync(configPath, 'utf8'), configPath);
84
+ const effects = deliveredEffects({ root, configPath, planPath, loadPlan, newsDir, today, waveId: orchestrating?.wave, io });
85
+ return deliver({ ...effects, state: sharedStateDir(root), config, dryRun: !!values['dry-run'], sha: values.sha, duringWave: !!orchestrating, args: rest }, realDeps(root));
86
+ }
87
+ /** cadence verify [--gate] : rejoue deliver.verify, ou la porte du projet. */
88
+ export function cmdVerify(c) {
89
+ const { values, io, today, root, configPath, planPath, loadPlan, newsDir } = c;
90
+ if (values.gate)
91
+ return verifyGateCommand(values, io);
92
+ if (!gitRoot(io.cwd))
93
+ throw new RafError('verify : à lancer dans un dépôt git');
94
+ if (!existsSync(configPath))
95
+ throw new RafError(`pas de configuration : ${configPath} (voir « cadence.yaml » dans docs/deliver.md du paquet @sylad/cadence)`);
96
+ const retry = values.retry === undefined ? 0 : Number(values.retry);
97
+ if (!Number.isFinite(retry) || retry < 0)
98
+ throw new RafError(`--retry invalide : ${values.retry} (secondes, 0 ou plus)`);
99
+ const config = parseDeliverConfig(readFileSync(configPath, 'utf8'), configPath);
100
+ const target = values.sha ? { sha: resolveCommit(root, values.sha) ?? '', note: null } : defaultTarget(root, config);
101
+ if (values.sha && !target.sha)
102
+ throw new RafError(`--sha ${values.sha} : commit introuvable`);
103
+ // Une ligne verte écrite par verify (L435) est une livraison : mêmes effets que deliver (lots annoncés, article à rafraîchir, L144).
104
+ // 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.
105
+ const holding = waveHolding(root);
106
+ const waveId = holding && !localLotOf(holding.wave) ? holding.wave : undefined;
107
+ const effects = () => deliveredEffects({ root, configPath, planPath, loadPlan, newsDir, today, waveId, io });
108
+ return verifyCommand({ config, sha: target.sha, note: target.note, retry, out: io.out, journal: { dir: sharedStateDir(root), day: today, effects } }, realCheckDeps(root));
109
+ }
110
+ /** cadence local start|done|abort : la tâche locale du lead. */
111
+ export function cmdLocal(c) {
112
+ const { rest, values, io, today, root, loadPlan } = c;
113
+ return localCommand(rest, values.ttl, { repo: gitRoot(root) ?? root, loadPlan, today, now: io.now, gate: (sha) => verifyGateCommand({ sha }, io), out: io.out, err: io.err });
114
+ }