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