@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/agents/spec-writer.md +1 -0
- package/agents/ux-reviewer.md +2 -0
- 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 +85 -1669
- package/dist/config.js +21 -108
- package/dist/corrections.js +28 -42
- package/dist/deliver-lots.js +37 -0
- package/dist/deliver.js +15 -39
- package/dist/docs-ok-file.js +25 -0
- package/dist/docsync.js +15 -3
- package/dist/lead.js +6 -24
- package/dist/lot-lines.js +51 -0
- package/dist/orchestrate/attente.js +82 -0
- package/dist/orchestrate/command.js +26 -25
- package/dist/orchestrate/continue.js +6 -14
- package/dist/orchestrate/cycle.js +18 -27
- package/dist/orchestrate/integrate.js +7 -2
- 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/lot-budget.js +2 -2
- package/dist/orchestrate/maquette.js +3 -0
- package/dist/orchestrate/registry.js +8 -1
- package/dist/orchestrate/schemas.js +5 -1
- package/dist/orchestrate/snapshot.js +13 -0
- package/dist/orchestrate/spec.js +51 -6
- package/dist/orchestrate/state.js +17 -0
- package/dist/plan-extras.js +119 -0
- package/dist/plan-ids.js +95 -0
- package/dist/plan-read.js +146 -0
- package/dist/plan-states.js +119 -0
- package/dist/plan.js +119 -190
- package/dist/raf-close-checks.js +25 -0
- package/dist/raf-status.js +28 -0
- package/dist/ref/command.js +2 -3
- package/dist/resources.js +3 -9
- package/dist/state.js +31 -8
- package/dist/verify-gate.js +33 -2
- package/dist/verify.js +20 -3
- package/dist/yaml-keys.js +21 -0
- package/docs/cadence-hud.md +1 -1
- package/docs/deliver.md +25 -3
- package/docs/fonctionnement.md +9 -5
- package/docs/lead.md +9 -7
- package/docs/news.md +2 -0
- package/docs/orchestrate.md +24 -7
- package/docs/perf.md +1 -0
- package/docs/qualite.md +14 -4
- package/docs/raf.md +62 -7
- package/docs/spec.md +5 -2
- package/docs/whats-new.md +9 -0
- package/package.json +1 -1
- package/skills/lead/SKILL.md +1 -1
- package/templates/orchestrate/spec.md +2 -2
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// Les portes : raf ux / raf review, et les trois hooks git (pre-commit, post-commit, pre-push) (L488, sorti de cli.ts).
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join, relative } from 'node:path';
|
|
4
|
+
import { readHookConfig, readOrchestrateConfig } from '../config.js';
|
|
5
|
+
import { repoShas, resolveLotRepos } from '../repos.js';
|
|
6
|
+
import { exemptPlanOnly, isPlanOnly, lotWork, ownFiles } from '../audit.js';
|
|
7
|
+
import { PLAN_ACTIONS } from '../plan-commit.js';
|
|
8
|
+
import { printUncommittedPlan } from '../plan-uncommitted.js';
|
|
9
|
+
import { matchesHead, readCommits, stage, stagedFiles, syncIndexWithHead } from '../git.js';
|
|
10
|
+
import { installHook, pendingCommitMessage, REFUSED } from '../hook.js';
|
|
11
|
+
import { gateRefusal, pushRefusal, UNGATED_ENV } from '../push-guard.js';
|
|
12
|
+
import { citedRefs } from '../link.js';
|
|
13
|
+
import { loadEntries } from '../news.js';
|
|
14
|
+
import { RafError } from '../plan.js';
|
|
15
|
+
import { dueLine, isRecurring } from '../recurring.js';
|
|
16
|
+
import { TITLE_MAX, truncate } from '../lead.js';
|
|
17
|
+
import { waveHolding } from '../repo-busy.js';
|
|
18
|
+
import { cadenceHome, waveLotsIn } from '../orchestrate/registry.js';
|
|
19
|
+
import { localLotOf } from '../local-task.js';
|
|
20
|
+
import { planFileIn, touchedLot } from './context.js';
|
|
21
|
+
const GATES = {
|
|
22
|
+
ux: { on: 'revue UX obligatoire pour les lots visibles', already: 'revue UX déjà active', action: PLAN_ACTIONS.uxRecorded },
|
|
23
|
+
review: { on: 'revue de code obligatoire pour les lots à commits', already: 'revue de code déjà active', action: PLAN_ACTIONS.reviewRecorded },
|
|
24
|
+
};
|
|
25
|
+
/** « raf ux … » et « raf review … » ont la même forme : activer la porte, ou noter le verdict d'un lot. */
|
|
26
|
+
function gate(kind, rest, loadPlan, root, today, io, opts) {
|
|
27
|
+
if (rest.length < 1)
|
|
28
|
+
throw new RafError(`usage : raf ${kind} enable | ${kind} <lot> "verdict"`);
|
|
29
|
+
const plan = loadPlan();
|
|
30
|
+
if (rest[0] === 'enable') {
|
|
31
|
+
const changed = kind === 'ux' ? plan.enableUx(today) : plan.enableReview(today);
|
|
32
|
+
plan.save();
|
|
33
|
+
io.out(changed ? `${GATES[kind].on} à partir du ${today}` : `${GATES[kind].already} depuis le ${kind === 'ux' ? plan.uxSince : plan.reviewSince}`);
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
if (rest.length < 2)
|
|
37
|
+
throw new RafError(`usage : raf ${kind} <lot> "verdict"`);
|
|
38
|
+
// Le plan refuse lui-même une sous-tâche, un verdict vide, un lot inconnu.
|
|
39
|
+
const verdict = rest.slice(1).join(' ');
|
|
40
|
+
if (kind === 'ux')
|
|
41
|
+
plan.recordUx(rest[0], verdict, today);
|
|
42
|
+
// Le verdict vaut jusqu'au dernier commit compté du lot ; un plan en lecture seule refuse sans lire git.
|
|
43
|
+
else {
|
|
44
|
+
// Les dépôts voisins du lot : un dépôt qu'on ne peut pas lire ferait enregistrer un verdict sur un sha non lu.
|
|
45
|
+
const neighbours = plan.readonly || rest[0].includes('/') ? undefined : resolveLotRepos(root, plan.lots().find((l) => l.id === rest[0]) ?? {});
|
|
46
|
+
if (neighbours?.problems.length)
|
|
47
|
+
throw new RafError(`${neighbours.problems.join(' ; ')} — la revue de code relit aussi les dépôts du lot`);
|
|
48
|
+
plan.recordReview(rest[0], verdict, today, plan.readonly ? null : (lotWork(plan, root, rest[0])[0]?.sha ?? null), neighbours?.repos.length ? repoShas(plan, neighbours.repos, rest[0]) : undefined);
|
|
49
|
+
}
|
|
50
|
+
plan.save();
|
|
51
|
+
touchedLot(plan, rest[0], opts.verbose, io);
|
|
52
|
+
// Comme `raf done` (L371) : le plan et la copie publiée que `plan.onWrite` vient de régénérer partent dans le même commit (L509).
|
|
53
|
+
printUncommittedPlan(io.out, plan, root, rest[0], opts.commit, GATES[kind].action);
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
/** Un titre de lot n'est jamais rendu en entier par défaut (L453) : ~300 caractères répétés à chaque commande, chaque commit de plan. */
|
|
57
|
+
function shortTitle(title, verbose) {
|
|
58
|
+
return verbose ? title : truncate(title, TITLE_MAX);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Pre-commit : un commit qui cite un lot encore todo. `warn` (défaut) : rien ici, le post-commit avertit ; `refuse` :
|
|
62
|
+
* échoue (REFUSED) avec « raf start <id> » ; `start` : démarre le lot et met le plan dans l'index, donc dans CE commit,
|
|
63
|
+
* mais seulement si le plan est identique à HEAD : sinon rien n'est démarré (message, puis avertissement du post-commit).
|
|
64
|
+
* Ne rend REFUSED que pour un refus : toute autre panne laisse le commit passer. Sans message lisible (éditeur), rien.
|
|
65
|
+
*/
|
|
66
|
+
function preCommit(load, mode, root, today, io) {
|
|
67
|
+
if (!load || !mode || mode === 'warn')
|
|
68
|
+
return 0;
|
|
69
|
+
try {
|
|
70
|
+
const message = pendingCommitMessage(io.env);
|
|
71
|
+
if (message === null)
|
|
72
|
+
return 0;
|
|
73
|
+
const plan = load();
|
|
74
|
+
const [subject = '', ...body] = message.split('\n');
|
|
75
|
+
const refs = citedRefs({ sha: '', day: today, subject, body: body.join('\n').trim() }, plan.refs);
|
|
76
|
+
const staged = stagedFiles(root);
|
|
77
|
+
const own = ownFiles(plan, root);
|
|
78
|
+
// Entretien du plan : tous les fichiers du commit sont ceux du plan (la configuration seule ne compte pas).
|
|
79
|
+
if (staged.length > 0 && staged.every((f) => own.has(f) && f !== relative(root, plan.configFile ?? join(root, 'cadence.yaml'))))
|
|
80
|
+
return 0;
|
|
81
|
+
const todo = [...new Set(refs.map((r) => r.lot))].filter((id) => {
|
|
82
|
+
const lot = plan.lots().find((l) => l.id === id);
|
|
83
|
+
return lot?.status === 'todo' && !isRecurring(lot);
|
|
84
|
+
});
|
|
85
|
+
if (todo.length === 0)
|
|
86
|
+
return 0;
|
|
87
|
+
if (mode === 'refuse') {
|
|
88
|
+
for (const id of todo)
|
|
89
|
+
io.err(`raf: ${id} est encore todo — commit refusé (hook.autostart: refuse)${plan.readonly ? '' : ` : raf start ${id}`} (--no-verify pour passer outre)`);
|
|
90
|
+
return REFUSED;
|
|
91
|
+
}
|
|
92
|
+
if (plan.readonly)
|
|
93
|
+
return 0;
|
|
94
|
+
const file = relative(root, plan.path);
|
|
95
|
+
// Un plan déjà modifié (stagé ou non) : le restager emporterait ces modifications dans ce commit, ou, sur un
|
|
96
|
+
// commit partiel, les perdrait. On ne démarre rien ; le post-commit avertit comme en mode warn.
|
|
97
|
+
if (!matchesHead(root, file)) {
|
|
98
|
+
io.err(`raf: plan modifié non commité : démarrage automatique sauté (hook.autostart: start), raf start ${todo.join(' ')} à la main`);
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
for (const id of todo)
|
|
102
|
+
plan.setStatus(id, 'doing', today, {});
|
|
103
|
+
try {
|
|
104
|
+
plan.save();
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
if (!(e instanceof RafError))
|
|
108
|
+
throw e;
|
|
109
|
+
// plan.onWrite en échec (L346) : le plan est resté tel quel, le commit part sans lui et le dit.
|
|
110
|
+
io.err(`raf: démarrage automatique refusé : ${e.message} ; raf start ${todo.join(' ')} à la main`);
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
for (const id of todo)
|
|
114
|
+
io.err(`raf: ${id} était todo — raf start ${id} fait par le hook, plan inclus dans ce commit`);
|
|
115
|
+
stage(root, file);
|
|
116
|
+
for (const f of plan.produced)
|
|
117
|
+
stage(root, f);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// Un hook ne doit jamais gêner un commit, hors refus.
|
|
121
|
+
}
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
function postCommit(load, newsDir, root, today, io, mode, verbose) {
|
|
125
|
+
if (!load)
|
|
126
|
+
return 0;
|
|
127
|
+
try {
|
|
128
|
+
const plan = load();
|
|
129
|
+
if (mode === 'start' && !plan.readonly)
|
|
130
|
+
syncIndexWithHead(root, relative(root, plan.path));
|
|
131
|
+
const head = readCommits(root, { range: '-1' })[0];
|
|
132
|
+
if (!head)
|
|
133
|
+
return 0;
|
|
134
|
+
const refs = citedRefs(head, plan.refs);
|
|
135
|
+
if (refs.length === 0) {
|
|
136
|
+
const planOnly = exemptPlanOnly({ byLot: new Map(), orphans: [head], unknown: [] }, plan, root).orphans.length === 0;
|
|
137
|
+
if (!planOnly && !/^Merge\b/.test(head.subject)) {
|
|
138
|
+
io.err('raf: commit sans lot — citer un identifiant la prochaine fois (raf now)');
|
|
139
|
+
}
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
const lots = new Map(plan.lots().map((l) => [l.id, l]));
|
|
143
|
+
const planning = isPlanOnly(head.sha, plan, root);
|
|
144
|
+
for (const r of refs) {
|
|
145
|
+
const lot = lots.get(r.lot);
|
|
146
|
+
if (!lot)
|
|
147
|
+
io.err(`raf: ${r.lot} n'existe pas dans le plan`);
|
|
148
|
+
else if (isRecurring(lot))
|
|
149
|
+
io.err(`raf: ${r.lot} est récurrent (${dueLine(lot, today)})${plan.readonly ? '' : ` — raf did ${r.lot}`}`);
|
|
150
|
+
else if (lot.status === 'todo' && !planning)
|
|
151
|
+
io.err(`raf: ${r.lot} est encore todo${plan.readonly ? '' : ` — raf start ${r.lot}`}`);
|
|
152
|
+
else
|
|
153
|
+
io.err(`raf: ${r.lot} (${lot.status}) ${shortTitle(lot.title, verbose)}`);
|
|
154
|
+
}
|
|
155
|
+
const entries = loadEntries(newsDir);
|
|
156
|
+
for (const id of new Set(refs.map((r) => r.lot))) {
|
|
157
|
+
if (lots.get(id)?.visible && !entries.some((e) => e.lots.includes(id)))
|
|
158
|
+
io.err(`raf: ${id} est visible : cadence news new ${id}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// Un hook ne doit jamais gêner un commit.
|
|
163
|
+
}
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* `raf hook pre-push` (L405), appelé par le hook que la vague pose : refuse (code 3) un push qui emporte un commit d'un lot pas prêt de la vague vivante
|
|
168
|
+
* du dépôt, ou dont un lot pas prêt a des commits illisibles (la garde de `deliver --sha`). Puis (L426), quand le projet déclare une porte, refuse un push dont
|
|
169
|
+
* le sha de tête n'a pas de verdict vert de `cadence verify --gate` (`CADENCE_PUSH_UNGATED=1` passe outre, et le dit) ; un commit qui ne change que le plan reprend le verdict de son parent (L432). Sans vague qui tient le dépôt, ou avec un état de vague illisible
|
|
170
|
+
* (avertissement), la première garde ne gêne jamais un push ; une config illisible laisse passer la seconde.
|
|
171
|
+
*/
|
|
172
|
+
function prePush(root, io, remoteName, configPath, planFile) {
|
|
173
|
+
let input = '';
|
|
174
|
+
try {
|
|
175
|
+
input = io.stdin ? io.stdin() : readFileSync(0, 'utf8');
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
const holding = waveHolding(root);
|
|
181
|
+
// Une tâche locale du lead (L519) tient le dépôt comme une vague, mais n'a pas de lots de vague : rien à vérifier pour la première garde, et pas de faux « état illisible ».
|
|
182
|
+
if (holding && !localLotOf(holding.wave)) {
|
|
183
|
+
const lots = waveLotsIn(cadenceHome(), holding.pid, root);
|
|
184
|
+
if (lots === null) {
|
|
185
|
+
io.err(`raf: vague ${holding.wave} : état illisible, push non vérifié`);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
const refusal = pushRefusal(root, holding.wave, lots, input, remoteName || undefined);
|
|
189
|
+
if (refusal) {
|
|
190
|
+
io.err(`raf: ${refusal}`);
|
|
191
|
+
return REFUSED;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// La porte du projet (L426) : une config illisible ne gêne pas un push, comme un état de vague illisible.
|
|
196
|
+
let gate = false;
|
|
197
|
+
try {
|
|
198
|
+
const config = readOrchestrateConfig(configPath);
|
|
199
|
+
gate = Boolean(config.test || config.build);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return 0;
|
|
203
|
+
}
|
|
204
|
+
const refusal = gateRefusal(root, gate, input, planFile);
|
|
205
|
+
if (!refusal)
|
|
206
|
+
return 0;
|
|
207
|
+
if (io.env[UNGATED_ENV] === '1') {
|
|
208
|
+
io.err(`raf: ${refusal.replace(/^push refusé/, 'push passé outre')} — ${UNGATED_ENV}=1 est posée`);
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
|
211
|
+
io.err(`raf: ${refusal}`);
|
|
212
|
+
return REFUSED;
|
|
213
|
+
}
|
|
214
|
+
/** raf hook install|pre-commit|post-commit|pre-push. */
|
|
215
|
+
export function cmdHook(c) {
|
|
216
|
+
const { rest, values, io, today, root, configPath, planPath, loadPlan, newsDir } = c;
|
|
217
|
+
if (rest[0] === 'install') {
|
|
218
|
+
const { path, changed } = installHook(io.cwd);
|
|
219
|
+
io.out(changed ? `hook installé : ${path}` : `hook déjà présent : ${path}`);
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
if (rest[0] === 'post-commit')
|
|
223
|
+
return postCommit(existsSync(planPath) ? loadPlan : null, newsDir, root, today, io, readHookConfig(configPath).autostart, !!values.verbose);
|
|
224
|
+
if (rest[0] === 'pre-commit')
|
|
225
|
+
return preCommit(existsSync(planPath) ? loadPlan : null, readHookConfig(configPath).autostart, root, today, io);
|
|
226
|
+
if (rest[0] === 'pre-push')
|
|
227
|
+
return prePush(root, io, rest[1], configPath, planFileIn(root, planPath));
|
|
228
|
+
throw new RafError('usage : raf hook install|pre-commit|post-commit|pre-push');
|
|
229
|
+
}
|
|
230
|
+
/** raf ux|review : active la porte, ou note le verdict d'un lot. */
|
|
231
|
+
export function cmdGate(c) {
|
|
232
|
+
const { command, rest, values, io, today, root, planConfig, loadPlan } = c;
|
|
233
|
+
return gate(command, rest, loadPlan, root, today, io, { verbose: !!values.verbose, commit: planConfig?.commit });
|
|
234
|
+
}
|
|
@@ -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
|
+
}
|