@sylad/cadence 1.41.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/dist/archive.js +103 -6
- package/dist/audit.js +2 -2
- package/dist/clean.js +0 -4
- package/dist/cli/context.js +129 -0
- package/dist/cli/deliver-commands.js +114 -0
- package/dist/cli/gates.js +234 -0
- package/dist/cli/lead-commands.js +161 -0
- package/dist/cli/lot-commands.js +257 -0
- package/dist/cli/read-commands.js +227 -0
- package/dist/cli/session-commands.js +152 -0
- package/dist/cli/setup-commands.js +72 -0
- package/dist/cli/spec-commands.js +298 -0
- package/dist/cli/stats-commands.js +138 -0
- package/dist/cli-add.js +37 -0
- package/dist/cli.js +82 -1651
- package/dist/config.js +16 -103
- package/dist/corrections.js +28 -42
- package/dist/docsync.js +15 -3
- package/dist/lead.js +6 -24
- package/dist/orchestrate/attente.js +82 -0
- package/dist/orchestrate/command.js +18 -17
- package/dist/orchestrate/continue.js +2 -12
- package/dist/orchestrate/cycle.js +5 -5
- package/dist/orchestrate/integration-vivante.js +138 -0
- package/dist/orchestrate/launch.js +6 -18
- package/dist/orchestrate/lock.js +3 -20
- package/dist/orchestrate/registry.js +8 -1
- package/dist/orchestrate/schemas.js +5 -1
- package/dist/orchestrate/snapshot.js +13 -0
- package/dist/orchestrate/state.js +17 -0
- package/dist/plan-extras.js +3 -2
- package/dist/plan-ids.js +95 -0
- package/dist/plan-read.js +2 -0
- package/dist/plan.js +29 -34
- package/dist/ref/command.js +2 -3
- package/dist/resources.js +3 -9
- package/dist/state.js +8 -8
- package/dist/verify-gate.js +33 -2
- package/dist/yaml-keys.js +19 -0
- package/docs/cadence-hud.md +1 -1
- package/docs/deliver.md +1 -1
- package/docs/lead.md +2 -2
- package/docs/news.md +2 -0
- package/docs/orchestrate.md +6 -2
- package/docs/qualite.md +1 -1
- package/docs/raf.md +22 -4
- package/docs/whats-new.md +6 -0
- package/package.json +1 -1
- package/skills/lead/SKILL.md +1 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Les commandes du lead : lead tour|priority|hold|release, config set (L488, sorti de cli.ts).
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { gitRoot } from '../git.js';
|
|
5
|
+
import { RafError } from '../plan.js';
|
|
6
|
+
import { configSet } from '../config-set.js';
|
|
7
|
+
import { movePriority } from '../priority.js';
|
|
8
|
+
import { findProjects, isProject, leadTour, readDepth, readLeadUx, tourFreeLine, tourLine, tourNoTestLines, tourWorktreeLines } from '../lead.js';
|
|
9
|
+
import { tourMemoryLines } from '../memory-index.js';
|
|
10
|
+
import { localOverdueLines } from '../local-task.js';
|
|
11
|
+
import { configuredCap, expiredHoldLines, slotsLine } from '../orchestrate/command.js';
|
|
12
|
+
import { addHold, cadenceHome, releaseHold } from '../orchestrate/registry.js';
|
|
13
|
+
import { perfHeader } from '../perf.js';
|
|
14
|
+
import { readPerf } from './stats-commands.js';
|
|
15
|
+
/** Les racines git des projets du tour (la clé `depth:` du dossier, un seul niveau si elle est illisible : le tour a déjà sa ligne d'erreur). */
|
|
16
|
+
function tourRoots(parent, depth) {
|
|
17
|
+
let levels = depth;
|
|
18
|
+
if (levels === undefined) {
|
|
19
|
+
try {
|
|
20
|
+
levels = readDepth(parent);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
levels = 1;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const dirs = findProjects(parent, levels);
|
|
27
|
+
return (dirs.length === 0 && isProject(parent) ? [parent] : dirs).map((d) => gitRoot(d) ?? d);
|
|
28
|
+
}
|
|
29
|
+
/** Durée par défaut (minutes) d'une session du lead inscrite par `lead hold` : de quoi finir un correctif ou une revue, pas de quoi oublier la réservation toute la journée. */
|
|
30
|
+
const HOLD_TTL_MINUTES = 60;
|
|
31
|
+
/** Longueur maximale de l'étiquette encodée : un nom de fichier tient en 255 octets, `.json.tmp` en prend 9. */
|
|
32
|
+
const HOLD_LABEL_MAX = 200;
|
|
33
|
+
/** `cadence lead priority <projet> --before|--after <projet> [dossier]` (L449) : range un projet dans la liste `priority:` du cadence.yaml du dossier (celui du tour), sans modèle ni commit. */
|
|
34
|
+
function leadPriority(rest, before, after, depth, io) {
|
|
35
|
+
const usage = 'usage : cadence lead priority <projet> --before|--after <projet> [dossier] [--depth n]';
|
|
36
|
+
if (rest.length < 2 || rest.length > 3 || (before === undefined) === (after === undefined))
|
|
37
|
+
throw new RafError(usage);
|
|
38
|
+
const place = before !== undefined ? 'before' : 'after';
|
|
39
|
+
const levels = depth === undefined ? undefined : Number(depth);
|
|
40
|
+
if (levels !== undefined && !(Number.isInteger(levels) && levels >= 1))
|
|
41
|
+
throw new RafError(`--depth invalide : ${depth} (un entier ≥ 1)`);
|
|
42
|
+
const parent = resolve(io.cwd, rest[2] ?? '.');
|
|
43
|
+
if (!existsSync(parent))
|
|
44
|
+
throw new RafError(`lead priority : dossier introuvable : ${parent}`);
|
|
45
|
+
const target = (before ?? after);
|
|
46
|
+
const moved = movePriority(parent, rest[1], place, target, levels);
|
|
47
|
+
io.out(moved.changed ? `${rest[1]} placé ${place === 'before' ? 'avant' : 'après'} ${target} dans priority: de ${join(parent, 'cadence.yaml')} (ordre : ${moved.order.join(' > ')}) — le fichier est modifié, à commiter` : `${rest[1]} est déjà ${place === 'before' ? 'avant' : 'après'} ${target} : rien à changer`);
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
/** `cadence lead hold <étiquette> [--ttl minutes]` / `lead release <étiquette>` (L255) : inscrit, ou rend, une session lancée à la main par le lead, qui compte dans les créneaux libres. */
|
|
51
|
+
function leadHold(rest, ttl, io, today) {
|
|
52
|
+
const [verb, label] = rest;
|
|
53
|
+
const usage = verb === 'hold' ? 'usage : cadence lead hold <étiquette> [--ttl minutes]' : 'usage : cadence lead release <étiquette>';
|
|
54
|
+
if (rest.length !== 2 || !label.trim())
|
|
55
|
+
throw new RafError(usage);
|
|
56
|
+
if (verb === 'release') {
|
|
57
|
+
if (ttl !== undefined)
|
|
58
|
+
throw new RafError(`--ttl ne va qu'avec lead hold (${usage})`);
|
|
59
|
+
io.out(releaseHold(cadenceHome(), label) ? `session « ${label} » rendue` : `aucune session « ${label} » inscrite`);
|
|
60
|
+
// ce que la libération laisse (L416) : les créneaux, les lots prêts qui peuvent les prendre, et un ⚠ tant qu'un créneau reste vide
|
|
61
|
+
const { line, free } = slotsLine(configuredCap(io.env), io.now());
|
|
62
|
+
io.out(line);
|
|
63
|
+
const ready = tourFreeLine(leadTour(resolve(io.cwd), today));
|
|
64
|
+
if (ready)
|
|
65
|
+
io.out(ready);
|
|
66
|
+
if (free > 0)
|
|
67
|
+
io.out(`⚠ ${free} ${free === 1 ? 'créneau libre' : 'créneaux libres'} sur ${configuredCap(io.env)} : lancer un lot prêt ou une session, un créneau vide ne sert à rien`);
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
// le nom de fichier encode l'étiquette (6 octets par lettre accentuée) : au-delà, le système de fichiers refuse (ENAMETOOLONG)
|
|
71
|
+
if (encodeURIComponent(label).length > HOLD_LABEL_MAX)
|
|
72
|
+
throw new RafError(`étiquette trop longue (${usage})`);
|
|
73
|
+
const minutes = ttl === undefined ? HOLD_TTL_MINUTES : Number(ttl);
|
|
74
|
+
if (!(Number.isInteger(minutes) && minutes >= 1))
|
|
75
|
+
throw new RafError(`--ttl invalide : ${ttl} (un nombre entier de minutes ≥ 1)`);
|
|
76
|
+
addHold(cadenceHome(), label, minutes, io.now());
|
|
77
|
+
io.out(`session « ${label} » inscrite pour ${minutes} min : elle compte dans les créneaux libres (cadence lead release « ${label} » à sa fin)`);
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
/** Poser une clé dans le cadence.yaml de chaque projet du dossier (L342) : ne lit ni le plan ni le cadence.yaml du dossier courant. */
|
|
81
|
+
export function cmdConfig(c) {
|
|
82
|
+
const { rest, values, io, today } = c;
|
|
83
|
+
if (rest[0] !== 'set' || rest.length !== 3)
|
|
84
|
+
throw new RafError('usage : cadence config set <clé> <valeur> --all [--dry-run] [--depth n]');
|
|
85
|
+
if (!values.all)
|
|
86
|
+
throw new RafError('config set demande --all : la commande s\'applique aux projets du dossier courant (lancer la commande dans le projet visé pour un seul)');
|
|
87
|
+
if (values.depth !== undefined && !(Number.isInteger(Number(values.depth)) && Number(values.depth) >= 1))
|
|
88
|
+
throw new RafError(`--depth invalide : ${values.depth} (un entier ≥ 1)`);
|
|
89
|
+
const parent = resolve(io.cwd);
|
|
90
|
+
const result = configSet(parent, rest[1], rest[2], { dryRun: values['dry-run'], depth: values.depth === undefined ? readDepth(parent) : Number(values.depth) }, today);
|
|
91
|
+
for (const l of result.lines)
|
|
92
|
+
io.out(l);
|
|
93
|
+
return result.code;
|
|
94
|
+
}
|
|
95
|
+
/** cadence lead tour|priority|hold|release. Le tour parcourt les sous-dossiers (ou, sans sous-projet, prend le dossier lui-même s'il est un projet) : il ne lit pas le plan du dossier courant. */
|
|
96
|
+
export function cmdLead(c) {
|
|
97
|
+
const { rest, values, io, today } = c;
|
|
98
|
+
if (rest[0] === 'hold' || rest[0] === 'release')
|
|
99
|
+
return leadHold(rest, values.ttl, io, today);
|
|
100
|
+
if (rest[0] === 'priority')
|
|
101
|
+
return leadPriority(rest, values.before, values.after, values.depth, io);
|
|
102
|
+
if (values.ttl !== undefined)
|
|
103
|
+
throw new RafError('--ttl ne va qu\'avec lead hold');
|
|
104
|
+
if (rest[0] !== 'tour' || rest.length > 2)
|
|
105
|
+
throw new RafError('usage : cadence lead tour [dossier] [--idle 3] [--depth n] [--json] | lead priority <projet> --before|--after <projet> [dossier] | lead hold <étiquette> [--ttl minutes] | lead release <étiquette>');
|
|
106
|
+
const idle = values.idle === undefined ? undefined : Number(values.idle);
|
|
107
|
+
if (idle !== undefined && !(Number.isInteger(idle) && idle >= 0))
|
|
108
|
+
throw new RafError(`--idle invalide : ${values.idle}`);
|
|
109
|
+
const depth = values.depth === undefined ? undefined : Number(values.depth);
|
|
110
|
+
if (depth !== undefined && !(Number.isInteger(depth) && depth >= 1))
|
|
111
|
+
throw new RafError(`--depth invalide : ${values.depth} (un entier ≥ 1)`);
|
|
112
|
+
const parent = resolve(io.cwd, rest[1] ?? '.');
|
|
113
|
+
if (!existsSync(parent))
|
|
114
|
+
throw new RafError(`lead tour : dossier introuvable : ${parent}`);
|
|
115
|
+
const rows = leadTour(parent, today, idle, depth);
|
|
116
|
+
if (values.json)
|
|
117
|
+
io.out(JSON.stringify(rows, null, 2));
|
|
118
|
+
else if (rows.length === 0)
|
|
119
|
+
io.out(`aucun projet sous ${parent} (.cadence/raf.yaml, docs/plan/raf.yaml ou cadence.yaml avec plan:)`);
|
|
120
|
+
else {
|
|
121
|
+
// L'en-tête perf (L227) : un ⚠ par ligne avant les projets ; un dossier que perf ne lit pas n'en donne pas, et ses avertissements ne sont pas repris.
|
|
122
|
+
try {
|
|
123
|
+
for (const l of perfHeader(readPerf(parent, today, undefined, () => { })))
|
|
124
|
+
io.out(l);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// pas de .cadence/runs, grille de prix mal formée, journaux ou plans illisibles : le tour ne change pas
|
|
128
|
+
}
|
|
129
|
+
for (const r of rows)
|
|
130
|
+
io.out(tourLine(r));
|
|
131
|
+
// les worktrees de lot que des vagues ont conservés : aucun n'est oublié en silence (L279, R19)
|
|
132
|
+
// … puis l'index MEMORY.md du dossier, s'il pèse trop (L442) : le lead propose le regroupement, rien n'est écrit
|
|
133
|
+
for (const l of [...tourWorktreeLines(rows, io.now().getTime()), ...tourMemoryLines(io.env, parent)])
|
|
134
|
+
io.out(l);
|
|
135
|
+
// un projet sans orchestrate.test : la vague ne lancera pas la suite (L327)
|
|
136
|
+
for (const l of tourNoTestLines(rows))
|
|
137
|
+
io.out(l);
|
|
138
|
+
// les créneaux libres viennent du registre, le lead les lit au lieu de les calculer (L141)
|
|
139
|
+
// toujours imprimés, même sans vague ni session du lead (L416) : un créneau vide se lit, il ne se déduit pas d'une ligne absente
|
|
140
|
+
io.out(slotsLine(configuredCap(io.env), io.now()).line);
|
|
141
|
+
// en face des créneaux libres, les lots prêts dont aucune ressource n'est prise (L252)
|
|
142
|
+
const free = tourFreeLine(rows);
|
|
143
|
+
if (free)
|
|
144
|
+
io.out(free);
|
|
145
|
+
// une session du lead dont le TTL est échu (L278) : rien ne l'arrête, le tour la signale (hors --json, comme les créneaux)
|
|
146
|
+
// … et une tâche locale (L519) au-delà de son délai : le dépôt reste verrouillé, le tour le dit au lieu de le libérer en silence
|
|
147
|
+
for (const l of [...expiredHoldLines(io.now()), ...localOverdueLines(tourRoots(parent, depth), io.now())])
|
|
148
|
+
io.out(l);
|
|
149
|
+
// le réglage lead.ux du cadence.yaml du dossier (L197) : le lead lit ici qui pose le verdict UX, au lieu d'ouvrir le fichier
|
|
150
|
+
try {
|
|
151
|
+
if (readLeadUx(parent) === 'lead')
|
|
152
|
+
io.out('verdict UX : lead (le lead pose raf ux d\'après le rapport du ux-reviewer et les captures ; l\'humain n\'est consulté qu\'en cas de doute)');
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
// un cadence.yaml illisible a déjà sa ligne d'erreur en tête de tour
|
|
156
|
+
if (/lead\.ux/.test(e.message))
|
|
157
|
+
io.out(`cadence.yaml · ✗ erreur : ${e.message.replace(/^.*cadence\.yaml : /, '')}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// Les commandes qui écrivent le plan, un lot à la fois (L488, sorti de cli.ts).
|
|
2
|
+
import { relative, resolve } from 'node:path';
|
|
3
|
+
import { refuseRedAcceptation } from '../raf-close-checks.js';
|
|
4
|
+
import { statusCommand } from '../raf-status.js';
|
|
5
|
+
import { PLAN_LEGACY } from '../config.js';
|
|
6
|
+
import { audit, isNonConformingVerdict, lotWork, planCommits, unreviewedWork } from '../audit.js';
|
|
7
|
+
import { short } from '../check.js';
|
|
8
|
+
import { isDay } from '../dates.js';
|
|
9
|
+
import { PLAN_ACTIONS } from '../plan-commit.js';
|
|
10
|
+
import { printUncommittedPlan } from '../plan-uncommitted.js';
|
|
11
|
+
import { resolveCommit } from '../git.js';
|
|
12
|
+
import { loadEntries } from '../news.js';
|
|
13
|
+
import { addLot } from '../cli-add.js';
|
|
14
|
+
import { RafError, SCREENS } from '../plan.js';
|
|
15
|
+
import { warnIntegrating, warnWaveWorking, waveHolding } from '../repo-busy.js';
|
|
16
|
+
import { knownLot, touchedLot } from './context.js';
|
|
17
|
+
/** raf add "titre" : ajoute un lot ou une sous-tâche. */
|
|
18
|
+
export function cmdAdd(c) {
|
|
19
|
+
const { rest, values, io, today, root, loadPlan, refuseLongTitle, need } = c;
|
|
20
|
+
need(1, 'add "titre"');
|
|
21
|
+
const plan = loadPlan();
|
|
22
|
+
const id = addLot(plan, values, rest.join(' '), today, refuseLongTitle);
|
|
23
|
+
plan.save();
|
|
24
|
+
io.out(id);
|
|
25
|
+
warnIntegrating(root, io.err);
|
|
26
|
+
warnWaveWorking(root, io.env, io.err);
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
/** raf start|done|drop <id> : pose le statut du lot, sous les refus de la porte de revue, de l'acceptation et du titre public. */
|
|
30
|
+
export function cmdSetStatus(c) {
|
|
31
|
+
const { rest, values, io, today, root, configPath, planConfig, loadPlan, newsDir, refuseLongTitle, need, command } = c;
|
|
32
|
+
need(1, `${command} <id>`);
|
|
33
|
+
const plan = loadPlan();
|
|
34
|
+
const status = command === 'start' ? 'doing' : command === 'done' ? 'done' : 'dropped';
|
|
35
|
+
// Le titre public part au site : un titre au-delà de la limite déclarée y ferait échouer le build.
|
|
36
|
+
if (status === 'done' && !rest[0].includes('/')) {
|
|
37
|
+
const lot = plan.lots().find((l) => l.id === rest[0]);
|
|
38
|
+
// Sans public: d'un lot visible, le site reprend le titre de sa Nouveauté la plus récente.
|
|
39
|
+
const pub = lot?.public ?? (lot?.visible ? loadEntries(newsDir).find((e) => e.lots.includes(lot.id))?.title : undefined);
|
|
40
|
+
if (pub)
|
|
41
|
+
refuseLongTitle(pub, lot?.public ? '' : ' — titre de la Nouveauté reprise par le site : corriger l\'entrée ou poser raf public');
|
|
42
|
+
}
|
|
43
|
+
// Le plan ne lit pas git : lui dire combien de commits du lot sa revue ne couvre pas quand elle est exigée.
|
|
44
|
+
if (status === 'done' && !rest[0].includes('/'))
|
|
45
|
+
refuseRedAcceptation(root, configPath, rest[0], !!values.force, io.err);
|
|
46
|
+
const unreviewed = status === 'done' && plan.reviewSince && !plan.readonly && !values.force ? unreviewedWork(plan, root, rest[0]) : 0;
|
|
47
|
+
const verdict = status === 'done' && !rest[0].includes('/') ? plan.lots().find((l) => l.id === rest[0])?.review?.verdict : undefined;
|
|
48
|
+
const nonConforming = verdict !== undefined && isNonConformingVerdict(verdict);
|
|
49
|
+
plan.setStatus(rest[0], status, today, { force: values.force, unreviewed, nonConforming });
|
|
50
|
+
if (command === 'drop' && values.reason)
|
|
51
|
+
plan.note(rest[0], `abandonné : ${values.reason}`, today);
|
|
52
|
+
plan.save();
|
|
53
|
+
io.out(`${rest[0]} → ${status}`);
|
|
54
|
+
if (status === 'done') {
|
|
55
|
+
warnIntegrating(root, io.err);
|
|
56
|
+
warnWaveWorking(root, io.env, io.err);
|
|
57
|
+
}
|
|
58
|
+
touchedLot(plan, rest[0], values.verbose, io);
|
|
59
|
+
if (status === 'done' && !rest[0].includes('/')) {
|
|
60
|
+
const lot = plan.lot(rest[0]);
|
|
61
|
+
if (lot.visible && !loadEntries(newsDir).some((e) => e.lots.includes(lot.id))) {
|
|
62
|
+
io.out(`${lot.id} est visible : écrire l'entrée : cadence news new ${lot.id}`);
|
|
63
|
+
}
|
|
64
|
+
printUncommittedPlan(io.out, plan, root, lot.id, planConfig?.commit);
|
|
65
|
+
}
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
/** raf archive : déplace les lots clos vers .cadence/archive/. */
|
|
69
|
+
export function cmdArchive(c) {
|
|
70
|
+
const { values, io, today, root, planConfig, planPath, loadPlan, newsDir } = c;
|
|
71
|
+
if (values.before !== undefined && !isDay(values.before))
|
|
72
|
+
throw new RafError(`--before invalide : ${values.before} (attendu AAAA-MM-JJ)`);
|
|
73
|
+
// L'archive vit à côté du plan : à l'ancien emplacement, elle irait sous docs/ (publié) et raf migrate ne la déplacerait pas — migrer d'abord.
|
|
74
|
+
if (planPath === resolve(root, PLAN_LEGACY))
|
|
75
|
+
throw new RafError(`raf archive refusé : le plan est à l'ancien emplacement ${PLAN_LEGACY}, l'archive irait sous docs/ — le déplacer d'abord : raf migrate`);
|
|
76
|
+
const dry = !!values['dry-run'];
|
|
77
|
+
const plan = loadPlan();
|
|
78
|
+
// Un lot clos qui a encore un écart (revue, UX, docs.sync, Nouveauté… ce que `raf check` signale) reste dans le plan, avec l'écart pour raison.
|
|
79
|
+
const issues = audit(plan, root, newsDir, today);
|
|
80
|
+
const blocked = (lot) => issues.find((i) => plan.refs(i.message).some((r) => r.lot === lot.id))?.message ?? null;
|
|
81
|
+
const { moved, kept } = plan.archive(today, { before: values.before, dryRun: dry, blocked: (lot) => (blocked(lot) ? `écart ouvert — ${blocked(lot)}` : null) });
|
|
82
|
+
const files = [...new Set(moved.map((m) => m.file))];
|
|
83
|
+
for (const f of files) {
|
|
84
|
+
const ids = moved.filter((m) => m.file === f).map((m) => m.lot.id);
|
|
85
|
+
io.out(`${dry ? 'à archiver' : 'archivé'} dans ${relative(io.cwd, f)} : ${ids.join(', ')}`);
|
|
86
|
+
}
|
|
87
|
+
for (const k of kept)
|
|
88
|
+
io.out(`${k.id} reste dans le plan : ${k.why}`);
|
|
89
|
+
if (moved.length === 0)
|
|
90
|
+
io.out('rien à archiver');
|
|
91
|
+
else if (!dry) {
|
|
92
|
+
if (plan.republishes)
|
|
93
|
+
io.err('raf: plan.onWrite republie le fichier du plan sans les lots archivés : le générateur de la copie publiée doit aussi lire .cadence/archive/*.yaml (clé `lots`), sinon l\'historique livré disparaît de la feuille de route publique');
|
|
94
|
+
printUncommittedPlan(io.out, plan, root, moved[0].lot.id, planConfig?.commit, PLAN_ACTIONS.archived(moved.length));
|
|
95
|
+
}
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
/** raf todo <id> : remet en attente un lot démarré sans commit de travail, ou rouvre un lot clos. */
|
|
99
|
+
export function cmdTodo(c) {
|
|
100
|
+
const { rest, values, io, today, root, loadPlan, need } = c;
|
|
101
|
+
need(1, 'todo <id> [--reason texte]');
|
|
102
|
+
const plan = loadPlan();
|
|
103
|
+
plan.putBack(rest[0], today, values.reason ?? '', () => lotWork(plan, root, rest[0]).length);
|
|
104
|
+
plan.save();
|
|
105
|
+
io.out(`${rest[0]} → todo`);
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
/** raf note <id> "texte". */
|
|
109
|
+
export function cmdNote(c) {
|
|
110
|
+
const { rest, values, io, today, root, loadPlan, need } = c;
|
|
111
|
+
need(2, 'note <id> "texte"');
|
|
112
|
+
const plan = loadPlan();
|
|
113
|
+
plan.note(rest[0], rest.slice(1).join(' '), today);
|
|
114
|
+
plan.save();
|
|
115
|
+
warnIntegrating(root, io.err);
|
|
116
|
+
warnWaveWorking(root, io.env, io.err);
|
|
117
|
+
touchedLot(plan, rest[0], values.verbose, io);
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
/** raf did <id> : un lot récurrent a été refait. */
|
|
121
|
+
export function cmdDid(c) {
|
|
122
|
+
const { rest, io, today, loadPlan, need } = c;
|
|
123
|
+
need(1, 'did <id> ["texte"]');
|
|
124
|
+
const plan = loadPlan();
|
|
125
|
+
plan.did(rest[0], today, rest.slice(1).join(' '));
|
|
126
|
+
plan.save();
|
|
127
|
+
io.out(`${rest[0]} refait le ${today}`);
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
/** raf title <id> "titre" : remplace le titre d'un lot. */
|
|
131
|
+
export function cmdTitle(c) {
|
|
132
|
+
const { rest, today, loadPlan, need } = c;
|
|
133
|
+
need(2, 'title <id> "nouveau titre"');
|
|
134
|
+
const plan = loadPlan();
|
|
135
|
+
plan.setTitle(rest[0], rest.slice(1).join(' '), today);
|
|
136
|
+
plan.save();
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
/** raf uses <lot> [<ressource>…] : remplace les ressources partagées du lot. */
|
|
140
|
+
export function cmdUses(c) {
|
|
141
|
+
const { rest, loadPlan, need } = c;
|
|
142
|
+
need(1, 'uses <lot> [<ressource>…]');
|
|
143
|
+
const plan = loadPlan();
|
|
144
|
+
plan.setUses(rest[0], rest.slice(1));
|
|
145
|
+
plan.save();
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
/** raf move <id> --before|--after <id> : réordonne un lot dans le plan. */
|
|
149
|
+
export function cmdMove(c) {
|
|
150
|
+
const { rest, values, io, root, loadPlan } = c;
|
|
151
|
+
// Une seule position : --before ou --after, avec une cible (« after » est aussi le drapeau de « add », « before » celui de « archive »).
|
|
152
|
+
const place = values.before !== undefined && values.after === undefined ? 'before' : values.after !== undefined && values.before === undefined ? 'after' : null;
|
|
153
|
+
if (rest.length !== 1 || !place)
|
|
154
|
+
throw new RafError('usage : raf move <id> --before <id> | raf move <id> --after <id>');
|
|
155
|
+
const plan = loadPlan();
|
|
156
|
+
plan.move(rest[0], place, values[place]); // en mémoire : les refus de la commande (plan en lecture seule, parent, archive) passent avant celui de la vague
|
|
157
|
+
// Un lot de la vague s'appuie sur l'ordre du plan qu'il a lu : le réordonner sous ses pieds est refusé tant que la vague tient le dépôt.
|
|
158
|
+
const holding = waveHolding(root, 'move');
|
|
159
|
+
if (holding)
|
|
160
|
+
throw new RafError(`move : orchestration en cours (vague ${holding.wave}, pid ${holding.pid}) — attendre que les lots de ce dépôt soient finis (le verrou tombe dès que tous sont prêts ou rendus), puis relancer`);
|
|
161
|
+
plan.save();
|
|
162
|
+
touchedLot(plan, rest[0], values.verbose, io);
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
165
|
+
/** raf public <id> "titre public" | --clear. */
|
|
166
|
+
export function cmdPublic(c) {
|
|
167
|
+
const { rest, values, io, loadPlan, refuseLongTitle, need } = c;
|
|
168
|
+
need(values.clear ? 1 : 2, 'public <id> "titre public" | public <id> --clear');
|
|
169
|
+
if (values.clear && rest.length > 1)
|
|
170
|
+
throw new RafError('--clear n\'attend pas de titre');
|
|
171
|
+
const plan = loadPlan();
|
|
172
|
+
if (!values.clear)
|
|
173
|
+
refuseLongTitle(rest.slice(1).join(' '));
|
|
174
|
+
plan.setPublic(rest[0], values.clear ? null : rest.slice(1).join(' '));
|
|
175
|
+
plan.save();
|
|
176
|
+
touchedLot(plan, rest[0], values.verbose, io);
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
/** raf screen <id> page|component|journey|none. */
|
|
180
|
+
export function cmdScreen(c) {
|
|
181
|
+
const { rest, loadPlan, need } = c;
|
|
182
|
+
need(2, 'screen <id> page|component|journey|none');
|
|
183
|
+
if (rest.length > 2)
|
|
184
|
+
throw new RafError('usage : raf screen <id> page|component|journey|none');
|
|
185
|
+
const plan = loadPlan();
|
|
186
|
+
if (rest[1] !== 'none' && !SCREENS.includes(rest[1]))
|
|
187
|
+
throw new RafError(`screen inconnu : ${rest[1]} (${SCREENS.join(', ')}, none)`);
|
|
188
|
+
plan.setScreen(rest[0], rest[1] === 'none' ? null : rest[1]);
|
|
189
|
+
plan.save();
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
192
|
+
/** raf estimate <id> <jours>. */
|
|
193
|
+
export function cmdEstimate(c) {
|
|
194
|
+
const { rest, loadPlan } = c;
|
|
195
|
+
if (rest.length !== 2)
|
|
196
|
+
throw new RafError('usage : raf estimate <id> <jours>');
|
|
197
|
+
const plan = loadPlan();
|
|
198
|
+
if (!/^\d+(\.\d+)?$/.test(rest[1].trim()))
|
|
199
|
+
throw new RafError(`estimation invalide : ${rest[1]} (un nombre de jours > 0)`);
|
|
200
|
+
plan.setEstimate(rest[0], Number(rest[1]));
|
|
201
|
+
plan.save();
|
|
202
|
+
return 0;
|
|
203
|
+
}
|
|
204
|
+
/** raf visible <id> on|off. */
|
|
205
|
+
export function cmdVisible(c) {
|
|
206
|
+
const { rest, loadPlan } = c;
|
|
207
|
+
if (rest.length !== 2 || (rest[1] !== 'on' && rest[1] !== 'off'))
|
|
208
|
+
throw new RafError('usage : raf visible <id> on|off');
|
|
209
|
+
const plan = loadPlan();
|
|
210
|
+
plan.setVisible(rest[0], rest[1] === 'on');
|
|
211
|
+
plan.save();
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
/** raf ignore <sha> | "<sujet exact>" : acquitte un commit sans lot. */
|
|
215
|
+
export function cmdIgnore(c) {
|
|
216
|
+
const { rest, values, io, today, root, loadPlan, need } = c;
|
|
217
|
+
need(1, 'ignore <sha> | "<sujet exact>" [--reason texte]');
|
|
218
|
+
if (rest.length > 1)
|
|
219
|
+
throw new RafError('un seul commit attendu ; la raison se passe par --reason "texte"');
|
|
220
|
+
const plan = loadPlan();
|
|
221
|
+
const arg = rest[0];
|
|
222
|
+
const commits = planCommits(plan, root);
|
|
223
|
+
// Un sujet exact l'emporte sur un nom de ref homonyme (`other` à la fois sujet et branche).
|
|
224
|
+
const sha = commits.some((c) => c.subject === arg) ? null : resolveCommit(root, arg);
|
|
225
|
+
const found = sha ? commits.find((c) => c.sha === sha) : commits.find((c) => c.subject === arg);
|
|
226
|
+
if (!found)
|
|
227
|
+
throw new RafError(`aucun commit ne correspond à « ${arg} » (sha ou sujet exact)`);
|
|
228
|
+
// Un sha est exact ; un sujet vaut pour tous les commits qui le portent.
|
|
229
|
+
const bySha = sha !== null;
|
|
230
|
+
plan.acknowledge({ ...(bySha ? { sha: found.sha, subject: found.subject } : { subject: found.subject }), date: today, ...(values.reason ? { reason: values.reason } : {}) });
|
|
231
|
+
plan.save();
|
|
232
|
+
io.out(`${bySha ? short(found) : found.subject} acquitté`);
|
|
233
|
+
return 0;
|
|
234
|
+
}
|
|
235
|
+
/** raf docs-ok <lot> --reason texte : acquitte l'écart docs.sync d'un lot. */
|
|
236
|
+
export function cmdDocsOk(c) {
|
|
237
|
+
const { rest, values, io, today, loadPlan, need } = c;
|
|
238
|
+
need(1, 'docs-ok <lot> --reason texte');
|
|
239
|
+
if (rest.length > 1)
|
|
240
|
+
throw new RafError('un seul lot attendu ; la raison se passe par --reason "texte"');
|
|
241
|
+
const plan = loadPlan();
|
|
242
|
+
knownLot(plan, rest[0], 'docs-ok se pose sur un lot, pas sur une sous-tâche');
|
|
243
|
+
plan.setDocsOk(rest[0], values.reason ?? '', today);
|
|
244
|
+
// Un plan en lecture seule n'est pas écrit : l'acquittement est dans .cadence/docs-ok.yaml, à commiter à la place du plan (L386).
|
|
245
|
+
if (plan.readonly)
|
|
246
|
+
io.out(`${rest[0]} : écart docs.sync acquitté dans .cadence/docs-ok.yaml (le plan n'est pas touché) — commiter ce fichier`);
|
|
247
|
+
else {
|
|
248
|
+
plan.save();
|
|
249
|
+
io.out(`${rest[0]} : écart docs.sync acquitté`);
|
|
250
|
+
}
|
|
251
|
+
return 0;
|
|
252
|
+
}
|
|
253
|
+
/** raf status : les états détaillés du projet, ou la pose de l'état d'un lot. */
|
|
254
|
+
export function cmdStatus(c) {
|
|
255
|
+
const { rest, values, io, today, root, loadPlan } = c;
|
|
256
|
+
return statusCommand(loadPlan(), rest, { back: !!values.back, today, out: io.out, commits: (p, id) => lotWork(p, root, id).length });
|
|
257
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// Les commandes qui lisent le plan et l'historique : commits, show, list, now, check, gantt, news (L488, sorti de cli.ts).
|
|
2
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { readOrchestrateConfig } from '../config.js';
|
|
5
|
+
import { repoSections } from '../repos.js';
|
|
6
|
+
import { lotJson, lotLines } from '../lot-lines.js';
|
|
7
|
+
import { audit, gateWaiting, hypothesesOf, lotWork, nextUp, planCommits, workByLot } from '../audit.js';
|
|
8
|
+
import { short } from '../check.js';
|
|
9
|
+
import { toDay } from '../dates.js';
|
|
10
|
+
import { ganttData, renderGantt } from '../gantt.js';
|
|
11
|
+
import { linkCommits } from '../link.js';
|
|
12
|
+
import { buildNews, loadEntries, newEntry, newsData, newsIssues, stampEntries } from '../news.js';
|
|
13
|
+
import { RafError, STATUSES } from '../plan.js';
|
|
14
|
+
import { dueLine, recurringByDue } from '../recurring.js';
|
|
15
|
+
import { schedule } from '../schedule.js';
|
|
16
|
+
import { coverageCommand } from '../ref/coverage.js';
|
|
17
|
+
import { knownLot } from './context.js';
|
|
18
|
+
function describe(l, commits) {
|
|
19
|
+
const bits = [`${l.estimate} j`];
|
|
20
|
+
if (commits)
|
|
21
|
+
bits.push(`${commits} commit(s)`);
|
|
22
|
+
if (l.tasks.length)
|
|
23
|
+
bits.push(`sous-tâches ${l.tasks.filter((t) => t.status === 'done').length}/${l.tasks.length}`);
|
|
24
|
+
return `${l.quickwin ? '⚡ ' : ''}${l.id} ${l.title} (${bits.join(', ')})`;
|
|
25
|
+
}
|
|
26
|
+
function now(plan, root, newsDir, today, io) {
|
|
27
|
+
const lots = plan.lots();
|
|
28
|
+
const linked = linkCommits(lots, planCommits(plan, root), plan.refs);
|
|
29
|
+
const { doing, ready, blocked } = nextUp(lots);
|
|
30
|
+
const work = workByLot(plan, root, linked, [...doing, ...ready.slice(0, 5)].map((l) => l.id));
|
|
31
|
+
const count = (id) => work.get(id)?.length ?? 0;
|
|
32
|
+
io.out(`${plan.project} — ${today}`);
|
|
33
|
+
io.out('\nEn cours');
|
|
34
|
+
if (doing.length === 0)
|
|
35
|
+
io.out(' (rien)');
|
|
36
|
+
for (const l of doing)
|
|
37
|
+
io.out(` ${describe(l, count(l.id))}`);
|
|
38
|
+
io.out('\nÀ suivre');
|
|
39
|
+
if (ready.length === 0)
|
|
40
|
+
io.out(' (rien de prêt)');
|
|
41
|
+
for (const l of ready.slice(0, 5))
|
|
42
|
+
io.out(` ${describe(l, count(l.id))}`);
|
|
43
|
+
if (ready.length > 5)
|
|
44
|
+
io.out(` … et ${ready.length - 5} autre(s)`);
|
|
45
|
+
const gated = gateWaiting(lots, blocked);
|
|
46
|
+
const byDeps = blocked.filter((l) => !gated.some((g) => g.lot === l));
|
|
47
|
+
if (byDeps.length)
|
|
48
|
+
io.out(` en attente de dépendances : ${byDeps.map((l) => l.id).join(', ')}`);
|
|
49
|
+
if (gated.length)
|
|
50
|
+
io.out(` en attente de condition de tirage : ${gated.map((g) => `${g.lot.id} (${g.text})`).join(', ')}`);
|
|
51
|
+
const recurring = recurringByDue(lots, today);
|
|
52
|
+
if (recurring.length) {
|
|
53
|
+
io.out('\nRécurrent');
|
|
54
|
+
for (const l of recurring)
|
|
55
|
+
io.out(` ${l.id} ${l.title} (tous les ${l.every} j, ${dueLine(l, today)})`);
|
|
56
|
+
}
|
|
57
|
+
const done = lots
|
|
58
|
+
.filter((l) => l.status === 'done' && l.finished)
|
|
59
|
+
.sort((a, b) => (b.finished > a.finished ? 1 : -1))
|
|
60
|
+
.slice(0, 3);
|
|
61
|
+
if (done.length) {
|
|
62
|
+
io.out('\nTerminés récemment');
|
|
63
|
+
for (const l of done)
|
|
64
|
+
io.out(` ${l.id} ${l.title} (${l.finished})`);
|
|
65
|
+
}
|
|
66
|
+
const issues = audit(plan, root, newsDir, today).filter((i) => !i.warning);
|
|
67
|
+
if (issues.length)
|
|
68
|
+
io.out(`\n${issues.length} écart(s) entre le plan et l'historique — raf check`);
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
function news([sub, ...args], plan, dir, today, values, io, refuseLongTitle, hypotheses) {
|
|
72
|
+
const lots = plan.lots();
|
|
73
|
+
switch (sub) {
|
|
74
|
+
case 'new': {
|
|
75
|
+
if (args.length === 0)
|
|
76
|
+
throw new RafError('usage : cadence news new <lot…> [--title t]');
|
|
77
|
+
for (const id of args)
|
|
78
|
+
plan.lot(id);
|
|
79
|
+
// Mesuré comme newEntry l'écrit : espaces fusionnés.
|
|
80
|
+
const title = (values.title ?? plan.lot(args[0]).public ?? plan.lot(args[0]).title).replace(/\s+/g, ' ').trim();
|
|
81
|
+
refuseLongTitle(title, values.title === undefined ? ' — donner un titre court : cadence news new <lot> --title "…"' : '');
|
|
82
|
+
io.out(newEntry(dir, args, title, today, io.now()));
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
case 'stamp': {
|
|
86
|
+
const stamped = stampEntries(dir, io.now());
|
|
87
|
+
for (const s of stamped)
|
|
88
|
+
io.out(`${s.file} created: ${s.created}`);
|
|
89
|
+
if (stamped.length === 0)
|
|
90
|
+
io.out('✓ toutes les entrées ont une heure de création');
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
case 'list':
|
|
94
|
+
for (const e of loadEntries(dir))
|
|
95
|
+
io.out(`${e.date} ${e.title} (${e.lots.join(', ')})`);
|
|
96
|
+
return 0;
|
|
97
|
+
case 'check':
|
|
98
|
+
case 'build': {
|
|
99
|
+
const entries = loadEntries(dir);
|
|
100
|
+
const issues = newsIssues(lots, entries, dir, sub === 'check' ? hypotheses() : undefined);
|
|
101
|
+
for (const i of issues)
|
|
102
|
+
io.out(`✗ ${i.message}`);
|
|
103
|
+
// Le build refuse seulement ce qu'il ne saurait pas publier : entrée illisible, capture absente.
|
|
104
|
+
const blocking = sub === 'check' ? issues : issues.filter((i) => i.kind === 'bad-entry' || i.kind === 'missing-capture');
|
|
105
|
+
if (sub === 'check')
|
|
106
|
+
io.out(issues.length === 0 ? '✓ Nouveautés cohérentes avec le plan' : `${issues.length} écart(s)`);
|
|
107
|
+
if (blocking.length > 0)
|
|
108
|
+
return 1;
|
|
109
|
+
if (sub === 'build') {
|
|
110
|
+
const stamp = io.now();
|
|
111
|
+
const data = newsData(plan.project, entries, `${toDay(stamp)} ${stamp.toTimeString().slice(0, 5)}`);
|
|
112
|
+
for (const f of buildNews(data, dir, resolve(io.cwd, values.output ?? join(dir, 'site'))))
|
|
113
|
+
io.out(f);
|
|
114
|
+
}
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
default:
|
|
118
|
+
throw new RafError('usage : cadence news new|list|check|stamp|build');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** raf commits <lot> : les commits que compte la porte de revue. */
|
|
122
|
+
export function cmdCommits(c) {
|
|
123
|
+
const { rest, io, root, loadPlan, need } = c;
|
|
124
|
+
need(1, 'commits <lot>');
|
|
125
|
+
const plan = loadPlan();
|
|
126
|
+
const id = rest[0];
|
|
127
|
+
knownLot(plan, id, 'les commits se listent par lot, pas par sous-tâche');
|
|
128
|
+
// Une lecture : le même ensemble que « raf done » et « raf check », plan en lecture seule compris.
|
|
129
|
+
for (const c of lotWork(plan, root, id).reverse())
|
|
130
|
+
io.out(short(c));
|
|
131
|
+
for (const line of repoSections(plan, root, plan.lot(id)))
|
|
132
|
+
io.out(line);
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
/** raf show <lot> [--notes]. */
|
|
136
|
+
export function cmdShow(c) {
|
|
137
|
+
const { rest, values, io, root, loadPlan, need } = c;
|
|
138
|
+
need(1, 'show <lot> [--notes]');
|
|
139
|
+
const plan = loadPlan();
|
|
140
|
+
const lot = knownLot(plan, rest[0], 'show se lit par lot, pas par sous-tâche');
|
|
141
|
+
const notes = lot.notes.map((n) => `${n.date} ${n.text}`);
|
|
142
|
+
if (values.notes) {
|
|
143
|
+
for (const n of notes)
|
|
144
|
+
io.out(n);
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
for (const line of lotLines(plan, lot))
|
|
148
|
+
io.out(line);
|
|
149
|
+
if (notes.length) {
|
|
150
|
+
io.out('notes :');
|
|
151
|
+
// Les lignes suivantes d'une note multiligne s'alignent sous son texte, pas en colonne 0.
|
|
152
|
+
for (const n of lot.notes)
|
|
153
|
+
io.out(` ${n.date} ${n.text.replace(/\n/g, `\n${' '.repeat(n.date.length + 4)}`)}`);
|
|
154
|
+
}
|
|
155
|
+
const commits = lotWork(plan, root, lot.id).reverse();
|
|
156
|
+
io.out(`commits (${commits.length}) :`);
|
|
157
|
+
for (const c of commits)
|
|
158
|
+
io.out(` ${short(c)}`);
|
|
159
|
+
for (const line of repoSections(plan, root, lot, ' '))
|
|
160
|
+
io.out(line);
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
/** raf list [--status s] [--json]. */
|
|
164
|
+
export function cmdList(c) {
|
|
165
|
+
const { values, io, loadPlan } = c;
|
|
166
|
+
const plan = loadPlan();
|
|
167
|
+
if (values.status && !STATUSES.includes(values.status))
|
|
168
|
+
throw new RafError(`statut inconnu : ${values.status}`);
|
|
169
|
+
const shown = plan.lots().filter((l) => !values.status || l.status === values.status);
|
|
170
|
+
if (values.json)
|
|
171
|
+
io.out(JSON.stringify(shown.map(lotJson), null, 2));
|
|
172
|
+
else
|
|
173
|
+
for (const l of shown)
|
|
174
|
+
io.out(`${l.id.padEnd(6)} ${l.status.padEnd(8)} ${l.quickwin ? '⚡ ' : ''}${l.title}`);
|
|
175
|
+
return 0;
|
|
176
|
+
}
|
|
177
|
+
/** raf check : les écarts entre le plan et l'historique, code 1 s'il y en a. */
|
|
178
|
+
export function cmdCheck(c) {
|
|
179
|
+
const { values, io, today, root, loadPlan, newsDir } = c;
|
|
180
|
+
if (values.ignored) {
|
|
181
|
+
const list = loadPlan().acknowledged;
|
|
182
|
+
if (list.length === 0)
|
|
183
|
+
io.out('aucun commit acquitté');
|
|
184
|
+
for (const a of list)
|
|
185
|
+
io.out(`${a.sha ? a.sha.slice(0, 7) : 'sujet '} ${a.subject ?? ''} (${a.date}${a.reason ? ` — ${a.reason}` : ''})`);
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
188
|
+
const idle = values.idle === undefined ? 7 : Number(values.idle);
|
|
189
|
+
if (!Number.isInteger(idle) || idle < 0)
|
|
190
|
+
throw new RafError(`--idle invalide : ${values.idle}`);
|
|
191
|
+
const issues = audit(loadPlan(), root, newsDir, today, { since: values.since, idle });
|
|
192
|
+
for (const i of issues)
|
|
193
|
+
io.out(`${i.warning ? '⚠' : '✗'} ${i.message}`);
|
|
194
|
+
// Un avertissement s'affiche mais ne fait pas échouer : seuls les écarts comptent pour le code de sortie.
|
|
195
|
+
const errors = issues.filter((i) => !i.warning).length;
|
|
196
|
+
const warnings = issues.length - errors;
|
|
197
|
+
io.out(errors === 0 ? `✓ plan et historique cohérents${warnings ? ` (${warnings} avertissement(s))` : ''}` : `${errors} écart(s)${warnings ? `, ${warnings} avertissement(s)` : ''}`);
|
|
198
|
+
return errors === 0 ? 0 : 1;
|
|
199
|
+
}
|
|
200
|
+
/** raf gantt [-o fichier] : la page Gantt du plan. */
|
|
201
|
+
export function cmdGantt(c) {
|
|
202
|
+
const { values, io, today, root, planPath, loadPlan } = c;
|
|
203
|
+
const plan = loadPlan();
|
|
204
|
+
const lots = plan.lots();
|
|
205
|
+
const linked = linkCommits(lots, planCommits(plan, root), plan.refs);
|
|
206
|
+
const stamp = io.now();
|
|
207
|
+
const html = renderGantt(ganttData(plan.project, schedule(lots, workByLot(plan, root, linked), today), today, `${toDay(stamp)} ${stamp.toTimeString().slice(0, 5)}`));
|
|
208
|
+
const out = resolve(io.cwd, values.output ?? join(dirname(planPath), 'gantt.html'));
|
|
209
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
210
|
+
writeFileSync(out, html);
|
|
211
|
+
io.out(out);
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
/** raf now : ce qui est en cours, la suite, les derniers terminés. */
|
|
215
|
+
export function cmdNow(c) {
|
|
216
|
+
return now(c.loadPlan(), c.root, c.newsDir, c.today, c.io);
|
|
217
|
+
}
|
|
218
|
+
/** raf news … : les entrées Nouveautés. */
|
|
219
|
+
export function cmdNews(c) {
|
|
220
|
+
const { rest, values, io, today, root, configPath, loadPlan, newsDir, refuseLongTitle } = c;
|
|
221
|
+
return news(rest, loadPlan(), newsDir, today, values, io, refuseLongTitle, () => hypothesesOf(root, configPath));
|
|
222
|
+
}
|
|
223
|
+
/** cadence ref coverage lit le plan (les lots faits qui citent les éléments) : il passe après le choix du plan, les autres ref n'y touchent pas (L367). */
|
|
224
|
+
export function cmdRef(c) {
|
|
225
|
+
const { rest, io, root, configPath, loadPlan } = c;
|
|
226
|
+
return coverageCommand(rest.slice(1), loadPlan, io, { repo: root, specDir: readOrchestrateConfig(configPath).spec.dir });
|
|
227
|
+
}
|