@sylad/cadence 1.39.0 → 1.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -3
- package/agents/spec-writer.md +1 -0
- package/agents/ux-reviewer.md +2 -0
- package/dist/cli.js +49 -62
- package/dist/config-set.js +2 -0
- 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/memory-index.js +113 -0
- package/dist/orchestrate/command.js +8 -8
- package/dist/orchestrate/continue.js +4 -2
- package/dist/orchestrate/cycle.js +23 -29
- package/dist/orchestrate/deja-traitee.js +76 -0
- 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/orchestrate/table.js +29 -11
- 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/session.js +8 -0
- package/dist/state.js +23 -0
- package/dist/verify.js +20 -3
- package/dist/yaml-keys.js +2 -0
- package/docs/concepts-livraison-dark.svg +32 -0
- package/docs/concepts-livraison-light.svg +32 -0
- package/docs/concepts-lot-dark.svg +52 -0
- package/docs/concepts-lot-light.svg +52 -0
- package/docs/concepts-vague-dark.svg +39 -0
- package/docs/concepts-vague-light.svg +39 -0
- package/docs/concepts-worktrees-dark.svg +45 -0
- package/docs/concepts-worktrees-light.svg +45 -0
- package/docs/concepts.md +159 -0
- package/docs/deliver.md +24 -2
- package/docs/fonctionnement.md +11 -5
- package/docs/lead.md +10 -6
- package/docs/orchestrate.md +22 -5
- package/docs/overview.md +1 -1
- package/docs/perf.md +1 -0
- package/docs/qualite.md +13 -3
- package/docs/raf.md +42 -5
- package/docs/session.md +32 -0
- package/docs/spec.md +5 -2
- package/docs/whats-new.md +6 -0
- package/package.json +10 -1
- package/skills/lead/SKILL.md +6 -0
- package/skills/session-close/SKILL.md +6 -0
- 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/README.md
CHANGED
|
@@ -4,6 +4,7 @@ A small working method that lives in your repository: a versioned plan linked to
|
|
|
4
4
|
waves of fresh AI sessions driven by a program, and deliveries proved by their effect.
|
|
5
5
|
It is for solo developers and AI-assisted sessions who lose track of *what is left to do* and *what each commit was for*:
|
|
6
6
|
cadence keeps both in plain files next to the code, and [Claude Code](https://claude.com/claude-code) skills turn it into rituals.
|
|
7
|
+
New to the words (lot, wave, hold…)? [docs/concepts.md](docs/concepts.md) explains each notion, with four figures.
|
|
7
8
|
|
|
8
9
|
## Start
|
|
9
10
|
|
|
@@ -15,8 +16,7 @@ raf init
|
|
|
15
16
|
raf add "My first lot" --estimate 1
|
|
16
17
|
```
|
|
17
18
|
|
|
18
|
-
Then `raf start L1`, commit with `feat(L1): …`, `raf now` to see where you are, `raf done L1` when
|
|
19
|
-
`raf -v` prints the installed version. Every command is in [docs/raf.md](docs/raf.md).
|
|
19
|
+
Then `raf start L1`, commit with `feat(L1): …`, `raf now` to see where you are, `raf done L1` when finished. Every command: [docs/raf.md](docs/raf.md).
|
|
20
20
|
|
|
21
21
|
## Lead several projects in 10 minutes
|
|
22
22
|
|
|
@@ -53,7 +53,7 @@ Details: [docs/skills.md](docs/skills.md) and [docs/cadence-hud.md](docs/cadence
|
|
|
53
53
|
- [docs/deliver.md](docs/deliver.md): wait for the CI, deploy, verify the effect; [docs/remote-gate.md](docs/remote-gate.md): the gate on another machine.
|
|
54
54
|
- [docs/spec.md](docs/spec.md): screen and back-end sheets.
|
|
55
55
|
- [docs/ref.md](docs/ref.md): reference documents (specs, norms, mock-ups) and the coverage of their elements by the lots.
|
|
56
|
-
- [docs/lead.md](docs/lead.md): the lead workflow,
|
|
56
|
+
- [docs/lead.md](docs/lead.md): the lead workflow, the `livreur` agent, and [the memory index warning](docs/session.md#memory-index-size-l442) (L442).
|
|
57
57
|
- [docs/overview.md](docs/overview.md): the longer introduction and the map of every page; [docs/fonctionnement.md](docs/fonctionnement.md): how it works.
|
|
58
58
|
- [docs/stabilite.md](docs/stabilite.md): what is guaranteed from 1.0.0; [docs/qualite.md](docs/qualite.md): the code-quality ratchet.
|
|
59
59
|
- [docs/whats-new.md](docs/whats-new.md) and [CHANGELOG.md](CHANGELOG.md): what changed.
|
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';
|
|
@@ -24,6 +28,7 @@ import { schedule } from './schedule.js';
|
|
|
24
28
|
import { AGENTS_DIR, installAgents, installSkills, SKILLS_DIR } from './skills.js';
|
|
25
29
|
import { configSet } from './config-set.js';
|
|
26
30
|
import { movePriority } from './priority.js';
|
|
31
|
+
import { closeMemoryLines, tourMemoryLines } from './memory-index.js';
|
|
27
32
|
import { findProjects, isProject, readDepth, leadTour, TITLE_MAX, truncate, readLeadUx, tourFreeLine, readLeadContextThreshold, findLeadDir, tourLine, tourNoTestLines, tourWorktreeLines } from './lead.js';
|
|
28
33
|
import { configuredCap, expiredHoldLines, slotsLine, orchestrate, realOrchestrateDeps } from './orchestrate/command.js';
|
|
29
34
|
import { addHold, releaseHold } from './orchestrate/registry.js';
|
|
@@ -33,7 +38,7 @@ import { blockingLots, LABEL, resumeCommand } from './orchestrate/table.js';
|
|
|
33
38
|
import { renderTestsSansCode, testsSansCode } from './corrections-sans-code.js';
|
|
34
39
|
import { bugCodeOf, bugCodePasses, classifyAll, classifySous, measure, ollamaClassifier, ollamaSousClassifier, readCache, readJournals, readRuns, readSousCache, renderReport, sampleKeys, serializeCache, serializeSousCache } from './corrections.js';
|
|
35
40
|
import { computePerf, perfHeader, perfWindows, readPlans, readProjectDeliveries, readProjectFailedEntries, renderPerf } from './perf.js';
|
|
36
|
-
import { findAcceptation, parseAcceptation,
|
|
41
|
+
import { findAcceptation, parseAcceptation, resealAcceptation, resealForGreen, runAcceptation, modeSpecGreen, setVert } from './orchestrate/accept.js';
|
|
37
42
|
import { refCommand } from './ref/command.js';
|
|
38
43
|
import { coverageCommand } from './ref/coverage.js';
|
|
39
44
|
import { release } from './release.js';
|
|
@@ -46,7 +51,7 @@ import { clearNext, readNext, sharedStateDir, stateDir, writeNext } from './stat
|
|
|
46
51
|
const HELP = `raf — plan « reste à faire » versionné dans le dépôt, relié aux commits
|
|
47
52
|
|
|
48
53
|
raf init [--project nom] [--prefix L] [--no-hook]
|
|
49
|
-
raf add "titre" [--estimate j] [--quickwin] [--visible [--screen page|component|journey]] [--spec] [--local] [--public "titre public"] [--every jours] [--after L2,L4] [--uses banc,passerelle] [--parent L3]
|
|
54
|
+
raf add "titre" [--estimate j] [--quickwin] [--visible [--screen page|component|journey]] [--spec] [--local] [--public "titre public"] [--every jours] [--after L2,L4] [--uses banc,passerelle] [--set champ=valeur]… [--parent L3]
|
|
50
55
|
raf public <id> "titre public" | --clear titre du lot dans le langage du public (pages Nouveautés et Plan)
|
|
51
56
|
raf screen <id> page|component|journey|none pose, remplace ou retire screen: d'un lot visible existant (seule la valeur page adopte l'étape spec d'orchestrate)
|
|
52
57
|
raf visible <id> on|off pose ou retire visible: d'un lot existant (changement visible : entrée Nouveautés et étape UX d'orchestrate) ; off est refusé tant que le lot a un screen:
|
|
@@ -55,6 +60,7 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
|
|
|
55
60
|
raf uses <lot> [<ressource>…] remplace les ressources partagées du lot (uses:, déclarées par resources: dans le cadence.yaml du dossier parent ; raf check refuse un nom non déclaré) ; sans nom, retire la clé
|
|
56
61
|
cadence local start <lot> [--ttl minutes] | local done <lot> | local abort <lot> tâche locale du lead (lot posé par raf add --local) : start verrouille le dépôt comme une vague (aucune vague n'y démarre, --add y est refusé) et passe le lot à doing, refusé si une orchestration ou un autre lot du dépôt est en cours ; le verrou est déclaré (60 min par défaut), tenu sans processus jusqu'à done ou abort, signalé en ⚠ par lead tour et orchestrate --status au-delà du délai ; done joue la porte du projet UNE fois, libère le dépôt si elle est verte (la capture, la revue du diff, cadence deliver et raf done restent au lead) et le garde verrouillé sinon ; abort libère sans porte
|
|
57
62
|
raf start <id> raf done <id> [--force] raf drop <id> [--reason texte]
|
|
63
|
+
raf status [<id> <état> ["message"] | <id> --back ["message"]] états détaillés du projet (cadence.yaml : plan.states) : pose l'état d'un lot sous la garde de plan.transitions et note le message ; --back sort de l'état bloqué vers l'état d'avant ; sans argument, liste les états. start / done / drop / todo posent eux-mêmes l'état de leur statut
|
|
58
64
|
raf todo <id> [--reason texte] remet en attente un lot démarré sans commit de travail (doing → todo, started retiré, note datée) ; refusé s'il a des commits ; rouvre un lot done ou dropped (→ todo, finished retiré, note « rouvert ») — la seule façon de rouvrir, archivé compris (il revient dans le fichier du plan)
|
|
59
65
|
raf note <id> "texte"
|
|
60
66
|
--verbose avec done, ux, review, note, move ou public : imprime la ligne « id état titre entier » du lot touché ; avec hook post-commit : le titre entier (sinon 60 caractères au plus)
|
|
@@ -67,7 +73,7 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
|
|
|
67
73
|
raf show <id> [--notes] le lot : statut, dates, after, titre public, visible et verdict ux (lot visible), notes datées, commits comptés ; --notes : les notes seules
|
|
68
74
|
raf commits <id> les commits du lot que compte la porte de revue de code, du plus ancien au plus récent
|
|
69
75
|
raf now ce qui est en cours, la suite, les derniers terminés
|
|
70
|
-
raf list [--status todo|doing|done|dropped]
|
|
76
|
+
raf list [--status todo|doing|done|dropped] [--json] les lots, une ligne chacun ; --json : un tableau JSON des lots (clés du fichier du plan, champs d'extension sous extras)
|
|
71
77
|
raf check [--since date] [--idle 7] (défaut : date « since » du plan) code 1 s'il y a des écarts
|
|
72
78
|
raf ignore <sha> | "<sujet exact>" [--reason texte] acquitte un commit sans lot, citant un id inconnu ou des lots sans y travailler, sans réécrire l'historique ; raf check --ignored les liste
|
|
73
79
|
raf docs-ok <lot> --reason texte acquitte l'écart docs.sync d'un lot (documentation en retard) avec la raison pour laquelle la documentation reste telle quelle (plan en lecture seule : .cadence/docs-ok.yaml, le plan n'est pas touché) ; lu par raf check et par orchestrate, qui ne rend jamais prêt un lot avec un écart ouvert
|
|
@@ -92,7 +98,7 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
|
|
|
92
98
|
cadence ref eval <attendu.json | dossier> <lecture.json | dossier> [--json] note une lecture contre l'inventaire attendu (vérité connue) : rappel et précision par type, faux lu (éléments sûrs et faux), pièges trouvés, tokens par page (usage.tokens de la lecture) ; lecture d'un autre document refusée (code 2) ; sur dossiers, un cas par sous-dossier (attendu.json / lecture.json)
|
|
93
99
|
cadence ref corpus <dossier> [--sans-rendu] écrit le corpus synthétique à vérité connue (7 pièges : texte ambigu levé par une figure, figure qui contredit, table en image, gabarit BUFR, contrôle translucide, page Proposal, scan bruité) : par cas document.html et attendu.json, puis document.pdf et page-<n>.png (1700×2200) rendus par Playwright (CADENCE_PLAYWRIGHT = dossier du paquet) ; dossier existant ou Playwright introuvable : code 2
|
|
94
100
|
cadence release patch|minor|major [--dry-run] prépare le commit de version (package.json, package-lock.json, plugin.json, marketplace.json, CHANGELOG.md) sans commiter ni publier ; imprime les étapes du lead et vérifie docs/whats-new.md : refus sur un arbre sale, un [Unreleased] vide ou une version déjà taguée
|
|
95
|
-
cadence verify [--retry s] [--sha rév] rejoue deliver.verify hors livraison : 0 vert, 1 effet rouge, 2 rien à vérifier
|
|
101
|
+
cadence verify [--retry s] [--sha rév] rejoue deliver.verify hors livraison : 0 vert (et, au sha de la dernière livraison rouge, l’inscrit au journal des livraisons), 1 effet rouge, 2 rien à vérifier
|
|
96
102
|
cadence verify --gate --sha rév [--project dossier] porte du projet (orchestrate.build puis orchestrate.test) : reprend le verdict enregistré par la vague quand son sha est le même et son arbre identique, sans rien rejouer (la sortie dit « verdict repris de la porte de la vague ») ; sinon la rejoue (« verdict rejoué : raison ») au sha dans l'arbre principal s'il est propre au sha, sinon dans un worktree jetable aux dépendances copiées (orchestrate.worktreeDeps), puis le retire : 0 vert, 1 rouge avec les tests en échec, 2 rien à rejouer ; le verdict (0 ou 1) est noté par sha et le hook pre-push refuse un push dont le sha n'a pas de verdict vert (CADENCE_PUSH_UNGATED=1 passe outre)
|
|
97
103
|
raf news new <lot…> [--title t] | list | check | stamp | build [-o dossier] (aussi « cadence news … »)
|
|
98
104
|
|
|
@@ -182,8 +188,10 @@ function dispatch(argv, io) {
|
|
|
182
188
|
title: { type: 'string' },
|
|
183
189
|
after: { type: 'string' },
|
|
184
190
|
uses: { type: 'string' },
|
|
191
|
+
set: { type: 'string', multiple: true },
|
|
185
192
|
parent: { type: 'string' },
|
|
186
193
|
force: { type: 'boolean' },
|
|
194
|
+
back: { type: 'boolean' },
|
|
187
195
|
reason: { type: 'string' },
|
|
188
196
|
porteur: { type: 'string' },
|
|
189
197
|
status: { type: 'string' },
|
|
@@ -270,7 +278,8 @@ function dispatch(argv, io) {
|
|
|
270
278
|
for (const r of rows)
|
|
271
279
|
io.out(tourLine(r));
|
|
272
280
|
// les worktrees de lot que des vagues ont conservés : aucun n'est oublié en silence (L279, R19)
|
|
273
|
-
|
|
281
|
+
// … puis l'index MEMORY.md du dossier, s'il pèse trop (L442) : le lead propose le regroupement, rien n'est écrit
|
|
282
|
+
for (const l of [...tourWorktreeLines(rows, io.now().getTime()), ...tourMemoryLines(io.env, parent)])
|
|
274
283
|
io.out(l);
|
|
275
284
|
// un projet sans orchestrate.test : la vague ne lancera pas la suite (L327)
|
|
276
285
|
for (const l of tourNoTestLines(rows))
|
|
@@ -404,6 +413,8 @@ function dispatch(argv, io) {
|
|
|
404
413
|
}
|
|
405
414
|
if (values.uses !== undefined)
|
|
406
415
|
throw new RafError(`uses se pose sur un lot, pas sur une sous-tâche : ${values.parent}`);
|
|
416
|
+
if (values.set !== undefined)
|
|
417
|
+
throw new RafError(`--set se pose sur un lot, pas sur une sous-tâche : ${values.parent}`);
|
|
407
418
|
id = plan.addTask(values.parent, rest.join(' '));
|
|
408
419
|
}
|
|
409
420
|
else {
|
|
@@ -419,7 +430,7 @@ function dispatch(argv, io) {
|
|
|
419
430
|
const uses = values.uses?.split(',').map((n) => n.trim()).filter(Boolean);
|
|
420
431
|
if (uses?.length === 0)
|
|
421
432
|
throw new RafError(invalidResource(''));
|
|
422
|
-
id = plan.add(rest.join(' '), today, { estimate, quickwin: values.quickwin, visible: values.visible, screen: values.screen, spec: values.spec, local: values.local, public: values.public, every, after, uses });
|
|
433
|
+
id = plan.add(rest.join(' '), today, { estimate, quickwin: values.quickwin, visible: values.visible, screen: values.screen, spec: values.spec, local: values.local, public: values.public, every, after, uses, extras: parseSet(values.set) });
|
|
423
434
|
}
|
|
424
435
|
plan.save();
|
|
425
436
|
io.out(id);
|
|
@@ -442,24 +453,8 @@ function dispatch(argv, io) {
|
|
|
442
453
|
refuseLongTitle(pub, lot?.public ? '' : ' — titre de la Nouveauté reprise par le site : corriger l\'entrée ou poser raf public');
|
|
443
454
|
}
|
|
444
455
|
// Le plan ne lit pas git : lui dire combien de commits du lot sa revue ne couvre pas quand elle est exigée.
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const repo = gitRoot(root) ?? root;
|
|
448
|
-
let dir = null;
|
|
449
|
-
try {
|
|
450
|
-
dir = readOrchestrateConfig(configPath).spec.dir;
|
|
451
|
-
}
|
|
452
|
-
catch {
|
|
453
|
-
// un cadence.yaml illisible ne doit pas empêcher de clore un lot : `cadence check` en parle
|
|
454
|
-
}
|
|
455
|
-
const red = dir === null ? undefined : redAcceptations(repo, dir).find((a) => a.lot === rest[0]);
|
|
456
|
-
if (red) {
|
|
457
|
-
const text = `${rest[0]} : ${red.path} est à vert: false — clos ainsi, le lot bloquerait les vagues suivantes du dépôt (verrou des tests d'acceptation) ; cadence spec green ${rest[0]} lance la commande d'acceptation et pose vert: true`;
|
|
458
|
-
if (!values.force)
|
|
459
|
-
throw new RafError(`${text} (--force pour passer outre)`);
|
|
460
|
-
io.err(`avertissement : ${text}`);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
456
|
+
if (status === 'done' && !rest[0].includes('/'))
|
|
457
|
+
refuseRedAcceptation(root, configPath, rest[0], !!values.force, io.err);
|
|
463
458
|
const unreviewed = status === 'done' && plan.reviewSince && !plan.readonly && !values.force ? unreviewedWork(plan, root, rest[0]) : 0;
|
|
464
459
|
const verdict = status === 'done' && !rest[0].includes('/') ? plan.lots().find((l) => l.id === rest[0])?.review?.verdict : undefined;
|
|
465
460
|
const nonConforming = verdict !== undefined && isNonConformingVerdict(verdict);
|
|
@@ -510,6 +505,8 @@ function dispatch(argv, io) {
|
|
|
510
505
|
}
|
|
511
506
|
return 0;
|
|
512
507
|
}
|
|
508
|
+
case 'status':
|
|
509
|
+
return statusCommand(loadPlan(), rest, { back: !!values.back, today, out: io.out, commits: (p, id) => lotWork(p, root, id).length });
|
|
513
510
|
case 'todo': {
|
|
514
511
|
need(1, 'todo <id> [--reason texte]');
|
|
515
512
|
const plan = loadPlan();
|
|
@@ -636,32 +633,8 @@ function dispatch(argv, io) {
|
|
|
636
633
|
io.out(n);
|
|
637
634
|
return 0;
|
|
638
635
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
io.out(`goal : ${lot.goal}`);
|
|
642
|
-
const dates = [['créé', lot.created], ['démarré', lot.started], [lot.status === 'dropped' ? 'abandonné' : 'terminé', lot.finished]].filter(([, d]) => d);
|
|
643
|
-
if (dates.length)
|
|
644
|
-
io.out(dates.map(([k, d]) => `${k} : ${d}`).join(' '));
|
|
645
|
-
if (lot.after.length)
|
|
646
|
-
io.out(`after : ${lot.after.join(', ')}`);
|
|
647
|
-
if (lot.public)
|
|
648
|
-
io.out(`public : ${lot.public}`);
|
|
649
|
-
// L439 : le livreur lit ici si le lot est visible et s'il porte un verdict UX ; deux lignes stables, absentes d'un lot non visible.
|
|
650
|
-
if (lot.visible)
|
|
651
|
-
io.out('visible : oui');
|
|
652
|
-
if (lot.screen)
|
|
653
|
-
io.out(`screen : ${lot.screen}`);
|
|
654
|
-
if (lot.visible)
|
|
655
|
-
io.out(lot.ux ? `ux : ${lot.ux.verdict}` : 'ux : absent');
|
|
656
|
-
if (lot.spec)
|
|
657
|
-
io.out('spec : true');
|
|
658
|
-
if (lot.uses)
|
|
659
|
-
io.out(`uses : ${lot.uses.join(', ')}`);
|
|
660
|
-
if (lot.docsOk)
|
|
661
|
-
io.out(`docs-ok : ${lot.docsOk.date} — ${lot.docsOk.reason}`);
|
|
662
|
-
const gate = gateProgress(plan.lots(), lot);
|
|
663
|
-
if (lot.gate && gate)
|
|
664
|
-
io.out(lot.gate.invalid ? `gate : ${gateText(gate)}` : `gate : ${gate.n} lots ${gate.label} faits`);
|
|
636
|
+
for (const line of lotLines(plan, lot))
|
|
637
|
+
io.out(line);
|
|
665
638
|
if (notes.length) {
|
|
666
639
|
io.out('notes :');
|
|
667
640
|
// Les lignes suivantes d'une note multiligne s'alignent sous son texte, pas en colonne 0.
|
|
@@ -680,9 +653,12 @@ function dispatch(argv, io) {
|
|
|
680
653
|
const plan = loadPlan();
|
|
681
654
|
if (values.status && !STATUSES.includes(values.status))
|
|
682
655
|
throw new RafError(`statut inconnu : ${values.status}`);
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
656
|
+
const shown = plan.lots().filter((l) => !values.status || l.status === values.status);
|
|
657
|
+
if (values.json)
|
|
658
|
+
io.out(JSON.stringify(shown.map(lotJson), null, 2));
|
|
659
|
+
else
|
|
660
|
+
for (const l of shown)
|
|
661
|
+
io.out(`${l.id.padEnd(6)} ${l.status.padEnd(8)} ${l.quickwin ? '⚡ ' : ''}${l.title}`);
|
|
686
662
|
return 0;
|
|
687
663
|
}
|
|
688
664
|
case 'now':
|
|
@@ -779,7 +755,7 @@ function dispatch(argv, io) {
|
|
|
779
755
|
const sconf = rest[0] === 'start' || rest[0] === 'close' ? readSessionConfig(configPath) : {};
|
|
780
756
|
const facts = rest[0] === 'start' || rest[0] === 'close' ? sconf[rest[0]] : undefined;
|
|
781
757
|
const clean = rest[0] === 'close' && sconf.clean ? { patterns: sconf.clean, days: sconf.cleanDays } : undefined;
|
|
782
|
-
const sctx = { plan: loadPlan(), root, newsDir, state: stateDir(root), shared: sharedStateDir(root), today, now: io.now(), out: io.out, facts, clean };
|
|
758
|
+
const sctx = { plan: loadPlan(), root, newsDir, state: stateDir(root), shared: sharedStateDir(root), today, now: io.now(), out: io.out, facts, clean, memory: closeMemoryLines(io.env, io.cwd, configPath) };
|
|
783
759
|
if (rest[0] !== 'start')
|
|
784
760
|
return session(rest, sctx, values);
|
|
785
761
|
// La reprise rejoue les vérifications d'effet (un essai, borné) : un effet rouge d'hier se lit avec les faits du matin.
|
|
@@ -814,12 +790,8 @@ function dispatch(argv, io) {
|
|
|
814
790
|
if (!existsSync(configPath))
|
|
815
791
|
throw new RafError(`pas de configuration de livraison : ${configPath} (voir « cadence.yaml » dans docs/deliver.md du paquet @sylad/cadence)`);
|
|
816
792
|
const config = parseDeliverConfig(readFileSync(configPath, 'utf8'), configPath);
|
|
817
|
-
const
|
|
818
|
-
|
|
819
|
-
// Sans public: d'un lot visible, le site reprend le titre de sa Nouveauté la plus récente.
|
|
820
|
-
const publicTitle = (lot) => lot.public ?? (lot.visible ? loadEntries(newsDir).find((e) => e.lots.includes(lot.id))?.title : undefined);
|
|
821
|
-
const ctx = { root, state: sharedStateDir(root), plan, config, today, dryRun: !!values['dry-run'], sha: values.sha, duringWave: !!orchestrating, waveId: orchestrating?.wave, planCommit: readPlanConfig(configPath, root)?.commit, args: rest, articles, publicTitle, out: io.out, err: io.err };
|
|
822
|
-
return deliver(ctx, realDeps(root));
|
|
793
|
+
const effects = deliveredEffects({ root, configPath, planPath, loadPlan, newsDir, today, waveId: orchestrating?.wave, io });
|
|
794
|
+
return deliver({ ...effects, state: sharedStateDir(root), config, dryRun: !!values['dry-run'], sha: values.sha, duringWave: !!orchestrating, args: rest }, realDeps(root));
|
|
823
795
|
}
|
|
824
796
|
case 'verify': {
|
|
825
797
|
if (values.gate)
|
|
@@ -835,7 +807,12 @@ function dispatch(argv, io) {
|
|
|
835
807
|
const target = values.sha ? { sha: resolveCommit(root, values.sha) ?? '', note: null } : defaultTarget(root, config);
|
|
836
808
|
if (values.sha && !target.sha)
|
|
837
809
|
throw new RafError(`--sha ${values.sha} : commit introuvable`);
|
|
838
|
-
|
|
810
|
+
// Une ligne verte écrite par verify (L435) est une livraison : mêmes effets que deliver (lots annoncés, article à rafraîchir, L144).
|
|
811
|
+
// verify lit le verrou sans refuser : une tâche locale du lead (L519) n'empêche pas de rejouer les vérifications, et n'est pas une vague à citer.
|
|
812
|
+
const holding = waveHolding(root);
|
|
813
|
+
const waveId = holding && !localLotOf(holding.wave) ? holding.wave : undefined;
|
|
814
|
+
const effects = () => deliveredEffects({ root, configPath, planPath, loadPlan, newsDir, today, waveId, io });
|
|
815
|
+
return verifyCommand({ config, sha: target.sha, note: target.note, retry, out: io.out, journal: { dir: sharedStateDir(root), day: today, effects } }, realCheckDeps(root));
|
|
839
816
|
}
|
|
840
817
|
case 'skills': {
|
|
841
818
|
if (rest[0] !== 'install')
|
|
@@ -868,6 +845,16 @@ const GATES = {
|
|
|
868
845
|
ux: { on: 'revue UX obligatoire pour les lots visibles', already: 'revue UX déjà active', action: PLAN_ACTIONS.uxRecorded },
|
|
869
846
|
review: { on: 'revue de code obligatoire pour les lots à commits', already: 'revue de code déjà active', action: PLAN_ACTIONS.reviewRecorded },
|
|
870
847
|
};
|
|
848
|
+
/**
|
|
849
|
+
* Ce qu'une livraison verte annonce et ouvre (lots livrés, article à rafraîchir, L144) : pour `cadence deliver` et pour la ligne verte
|
|
850
|
+
* que `cadence verify` inscrit (L435). Sans `public:`, un lot visible tire son titre de sa Nouveauté la plus récente.
|
|
851
|
+
*/
|
|
852
|
+
function deliveredEffects(p) {
|
|
853
|
+
const { root, configPath, newsDir, io } = p;
|
|
854
|
+
const publicTitle = (lot) => lot.public ?? (lot.visible ? loadEntries(newsDir).find((e) => e.lots.includes(lot.id))?.title : undefined);
|
|
855
|
+
const plan = existsSync(p.planPath) ? p.loadPlan() : null;
|
|
856
|
+
return { root, plan, today: p.today, articles: readDocsConfig(configPath).articles, publicTitle, planCommit: readPlanConfig(configPath, root)?.commit, waveId: p.waveId, out: io.out, err: io.err };
|
|
857
|
+
}
|
|
871
858
|
/** « raf ux … » et « raf review … » ont la même forme : activer la porte, ou noter le verdict d'un lot. */
|
|
872
859
|
function gate(kind, rest, loadPlan, root, today, io, opts) {
|
|
873
860
|
if (rest.length < 1)
|
package/dist/config-set.js
CHANGED
|
@@ -7,6 +7,7 @@ import { defaultPlan, readDocsConfig, readHookConfig, readMigrateConfig, readNew
|
|
|
7
7
|
import { parseDeliverConfig } from './deliver.js';
|
|
8
8
|
import { commitPaths, dirtyPaths, gitRoot, restorePaths } from './git.js';
|
|
9
9
|
import { findProjects, isProject, readDepth, readLeadContextThreshold, readLeadUx } from './lead.js';
|
|
10
|
+
import { readMemoryIndexMax } from './memory-index.js';
|
|
10
11
|
import { readPriority } from './orchestrate/continue.js';
|
|
11
12
|
import { Plan, RafError } from './plan.js';
|
|
12
13
|
const KEY = /^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*$/;
|
|
@@ -25,6 +26,7 @@ export function validateConfig(file, root) {
|
|
|
25
26
|
readDocsConfig(file);
|
|
26
27
|
readReferencesConfig(file);
|
|
27
28
|
readMigrateConfig(file);
|
|
29
|
+
readMemoryIndexMax(file);
|
|
28
30
|
readLeadUx(dirname(file));
|
|
29
31
|
readLeadContextThreshold(dirname(file));
|
|
30
32
|
readDepth(dirname(file));
|
package/dist/config.js
CHANGED
|
@@ -4,16 +4,16 @@ import { parse } from 'yaml';
|
|
|
4
4
|
import { isDay } from './dates.js';
|
|
5
5
|
import { readGate } from './gate-config.js';
|
|
6
6
|
import { DEFAULT_TEST_PATTERNS } from './testgate.js';
|
|
7
|
+
import { readExtras } from './plan-extras.js';
|
|
7
8
|
import { gitRoot } from './git.js';
|
|
8
9
|
import { planCommitProblem } from './plan-commit.js';
|
|
10
|
+
import { parseStates } from './plan-states.js';
|
|
9
11
|
import { checkTestReplay, readGateTest } from './orchestrate/gate-parts.js';
|
|
10
|
-
import { booleanKey } from './yaml-keys.js';
|
|
12
|
+
import { booleanKey, isObject, list } from './yaml-keys.js';
|
|
11
13
|
import { FIELDS, Plan, RafError, STATUSES } from './plan.js';
|
|
12
|
-
const KEYS = ['path', 'project', 'since', 'ignore', 'files', 'lots', 'fields', 'statuses', 'estimates', 'add', 'onWrite', 'commit'];
|
|
14
|
+
const KEYS = ['path', 'project', 'since', 'ignore', 'files', 'lots', 'fields', 'statuses', 'estimates', 'add', 'onWrite', 'commit', 'states', 'transitions', 'doneState', 'blockedState', 'extras'];
|
|
13
15
|
const ON_WRITE_KEYS = ['command', 'files'];
|
|
14
16
|
const FORMAT_KEYS = ['lots', 'fields', 'statuses', 'estimates'];
|
|
15
|
-
const isObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
16
|
-
const list = (v) => (v == null ? [] : Array.isArray(v) ? v.map(String) : [String(v)]);
|
|
17
17
|
/** Chemin réel (liens symboliques résolus) du plus proche ancêtre existant, le reste rattaché tel quel. */
|
|
18
18
|
function realish(p) {
|
|
19
19
|
try {
|
|
@@ -112,6 +112,7 @@ export function readPlanConfig(file, root) {
|
|
|
112
112
|
settings.onWrite = readOnWrite(p.onWrite, root ?? dirname(resolve(file)), bad);
|
|
113
113
|
if (FORMAT_KEYS.some((k) => p[k] != null))
|
|
114
114
|
settings.format = parseFormat(p, bad);
|
|
115
|
+
Object.assign(settings, parseStates(p, bad, settings.format !== undefined), readExtras(p.extras, !!settings.format, bad));
|
|
115
116
|
if (p.path != null && String(p.path).trim() === '')
|
|
116
117
|
throw bad('path est vide');
|
|
117
118
|
if (p.add != null && (typeof p.add !== 'string' || !p.add.includes('{title}')))
|
|
@@ -191,12 +192,11 @@ export function readNewsConfig(file) {
|
|
|
191
192
|
for (const k of Object.keys(n))
|
|
192
193
|
if (k !== 'publicTitleMax')
|
|
193
194
|
throw new RafError(`${file} : news.${k} inconnu (attendu : publicTitleMax)`);
|
|
194
|
-
|
|
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
|
+
}
|