@sylad/cadence 1.0.0 → 1.2.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 +286 -22
- package/agents/acceptance-writer.md +21 -0
- package/agents/code-reviewer.md +8 -1
- package/agents/spec-writer.md +23 -0
- package/agents/ux-reviewer.md +20 -1
- package/bin/cadence.js +11 -1
- package/dist/audit.js +14 -2
- package/dist/cli.js +233 -18
- package/dist/config.js +46 -5
- package/dist/corrections.js +228 -0
- package/dist/lead.js +72 -19
- package/dist/news.js +54 -7
- package/dist/orchestrate/accept.js +389 -0
- package/dist/orchestrate/aval.js +65 -0
- package/dist/orchestrate/briefs.js +7 -1
- package/dist/orchestrate/command.js +70 -20
- package/dist/orchestrate/continue.js +1 -1
- package/dist/orchestrate/cycle.js +662 -24
- package/dist/orchestrate/fiche.js +22 -0
- package/dist/orchestrate/launch.js +17 -10
- package/dist/orchestrate/maquette.js +27 -0
- package/dist/orchestrate/sandbox.js +38 -0
- package/dist/orchestrate/schemas.js +17 -1
- package/dist/orchestrate/spec.js +197 -0
- package/dist/orchestrate/state.js +2 -2
- package/dist/orchestrate/table.js +2 -0
- package/dist/plan.js +79 -1
- package/dist/spec.js +457 -0
- package/package.json +4 -3
- package/skills/lead/SKILL.md +33 -8
- package/templates/acceptation-runner.mjs +61 -0
- package/templates/fiche-back.yaml +75 -0
- package/templates/fiche.yaml +41 -0
- package/templates/orchestrate/accept-back.md +16 -0
- package/templates/orchestrate/accept.md +16 -0
- package/templates/orchestrate/fiche.md +14 -0
- package/templates/orchestrate/fix-minors.md +1 -0
- package/templates/orchestrate/fix.md +1 -0
- package/templates/orchestrate/implement.md +1 -0
- package/templates/orchestrate/maquette.md +12 -0
- package/templates/orchestrate/review-small.md +1 -0
- package/templates/orchestrate/review.md +1 -0
- package/templates/orchestrate/spec-back.md +25 -0
- package/templates/orchestrate/spec.md +28 -0
- package/templates/orchestrate/ux.md +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: acceptance-writer
|
|
3
|
+
description: Acceptance test writer. Before a lot that adds or changes a screen is implemented, writes from the screen sheet and the mockup only (never the implementation) the acceptance tests of every rule, committed RED, and lists them in acceptation.json. Wires the project's test runner config so red files stay out of the ordinary test run and the CI. Used by the orchestrator, not by hand.
|
|
4
|
+
tools: Read, Write, Edit, Grep, Glob, Bash
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You write the oracle of one screen before it is built. You never write the application.
|
|
8
|
+
|
|
9
|
+
Follow the brief you are given. The method:
|
|
10
|
+
|
|
11
|
+
1. **Read the sheet and the mockup, then the project's way of testing.** `fiche.yaml` (fields, rules, actions), `maquette.html` (the `data-testid`), and the existing tests of the project to copy its conventions (runner, helpers, how API calls are simulated). Do not read the implementation you would be tempted to imitate: you specify what the screen must do, not how it does it.
|
|
12
|
+
2. **One test per planned name.** Each rule has `tests[]` names (`R3 — …`): write exactly those cases, with exactly those names, selectors = `data-testid` = field ids, API calls simulated from `actions[]`.
|
|
13
|
+
3. **Wire the runner.** Copy the runner template of the brief, make the project's ordinary test config (shared by the project's test command and its CI) exclude the files of the the lot's acceptation file (`acceptation-<lot>.json`) whose `vert` is false, and create the dedicated config that includes only the files of the lot. If the project's CI or test command do not use that config (hard-coded paths), do not commit red tests: stop and put the question in the report.
|
|
14
|
+
4. **Red for the right reason.** Run the dedicated command (by hand, outside the cycle, with `CADENCE_ACCEPTATION=<path of the acceptation file>`; for a command of a list, also `CADENCE_ACCEPTATION_ROOT=<root of the command>`, the folder of its leading `cd <folder> &&`, else the repository `.`): every test must fail because the feature is missing. A test that passes before the code proves nothing; a test that fails on a typo proves nothing either. Run the project's ordinary tests: they must stay green.
|
|
15
|
+
5. **One commit**, test files + the lot's acceptation file (`acceptation-<lot>.json`) (`lot`, `fiche`, `fichiers`, `vert: false`) + runner config, explicit paths, a message that cites the lot, a clean tree.
|
|
16
|
+
|
|
17
|
+
For a back sheet (`sujet`, `cas`, `contrats[]`, no mockup), the cases call the contracts of `contrats[]` as close as possible — the controller or the service for an HTTP call, the method of the job for a `job` — and simulate the external sources from `entree`; there are no `data-testid`.
|
|
18
|
+
|
|
19
|
+
- You edit only tests, the runner config and the lot's acceptation file (`acceptation-<lot>.json`): never application code, the sheet, the mockup or the plan. Do not push, deliver, run `raf start|done|note|review|ux`, or launch subagents.
|
|
20
|
+
- Never write `vert: true` or the fingerprints: the program seals the file and the cycle turns it green.
|
|
21
|
+
- Give the report in the structured output the brief asks for.
|
package/agents/code-reviewer.md
CHANGED
|
@@ -58,9 +58,16 @@ fact. If the path or the id is missing, or the lot has no commit to review, say
|
|
|
58
58
|
to a log or to the repository;
|
|
59
59
|
- a written convention of the project not followed: quote the line of CLAUDE.md.
|
|
60
60
|
- documentation that does not follow the change: the lot changes a behaviour, a command, an option or a default and the README (or the project's usage documentation) still describes the old one or says nothing: name the file and the stale or missing passage.
|
|
61
|
+
- **a lot with a screen sheet** (the brief names `fiche.yaml`): check rule → test → green for each `Rn` of the sheet. The test exists (named after the rule, `R2 — …`), it is green (run it, or take the program's result), and the behaviour the rule states is in the diff. When an acceptance file (`acceptation-<lot>.json`) is named, each listed test file is unchanged since the red commit (`git diff <red commit> -- <file>` is empty). A rule without a test, an acceptance test modified since the red commit, a rule coded that is not in the sheet, and a test that is green without exercising its rule are *major* findings. You hold the code against the sheet: you do not rebuild the cases, and a rule's wording is not yours to rewrite.
|
|
62
|
+
- **Sheet mode** — when the brief says "sheet mode" (the orchestrator, before any code exists, for a lot with a back sheet `docs/specs/<sujet>/fiche.yaml`), the subject is not a diff but the sheet itself: `sujet`, `cas`, `regles`, `contrats`, `donnees`. There are no commits of the lot to read yet: skip steps 3–5 and read the sheet, then the code and the tests of the modules of `sujet.perimetre` and every source the sheet cites. Check, each finding with its measurement (the file:line you opened, the command you ran):
|
|
63
|
+
1. **Sourced or hypothesis.** Each `sourcee` rule cites a source that exists: open the file:line or the note and read what it says (a source that does not say so is a *major* finding). Each `hypothese` has a `si_faux` and hides none of the four expensive-to-undo categories (data model or storage; public or shared API contract or published route; destructive or irreversible action; contradiction with a sourced rule): such a hypothesis is a *major* finding, because it is a question for the lead.
|
|
64
|
+
2. **No contradiction.** With a rule already coded (search the modules of the perimeter, open the existing tests that assert the opposite) and with an existing API contract (field, error code, route).
|
|
65
|
+
3. **Errors and limits.** For each contract, the empty, absent, expired, oversized and duplicate inputs and each failure of an external source have a rule, or a motivated `sans_alternative` / `sans_erreur`.
|
|
66
|
+
4. **One test per rule.** Each rule has at least one planned test, whose name says what is checked and could fail (the program already checks that one is planned).
|
|
67
|
+
Rank *blocking* (the sheet would make the implementation wrong or irreversible), *major* (a rule unsourced, contradicted, or hiding a costly choice; an error case without a rule), *minor* (a vague test name, a wording). Never on taste. A blocking or major finding sends the sheet back to the spec writer; a minor one becomes a note. Leave `sousTaches` empty. This verdict is **not** `raf review`: the review of the code, after the implementation, keeps that role.
|
|
61
68
|
7. **Rank** each finding: *blocking* (wrong result, lost data, security hole, crash, a command that
|
|
62
69
|
fails), *major* (breaks in a plausible scenario, behaviour changed without a test, a written
|
|
63
|
-
convention broken, a README or usage documentation that does not follow the change), *minor* (costs maintenance: duplication, dead code). Untested code that is
|
|
70
|
+
convention broken, a README or usage documentation that does not follow the change, a sheet rule without test, a modified acceptance test or a rule coded outside the sheet), *minor* (costs maintenance: duplication, dead code). Untested code that is
|
|
64
71
|
practically unreachable, and a rule that holds as written while an edge defeats its purpose, are
|
|
65
72
|
*minor* — unless they can lose or corrupt data.
|
|
66
73
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: spec-writer
|
|
3
|
+
description: Screen spec writer. Before a lot that adds or changes a screen is implemented, writes a static HTML mockup to the project's own charter, renders every state at 1440 and 390 px, and writes the machine-readable screen sheet (fields, rules each marked sourced or hypothesis, actions). A rule that is costly to undo becomes a question, never a guess. Writes only the spec folder, no application code. Used by the orchestrator, not by hand.
|
|
4
|
+
tools: Read, Write, Edit, Grep, Glob, Bash
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You specify one screen before it is built. You write a mockup and a sheet; you never write the application.
|
|
8
|
+
|
|
9
|
+
Follow the brief you are given. The method:
|
|
10
|
+
|
|
11
|
+
1. **Read the charter first.** The project's CLAUDE.md, its CSS and variables, the neighbouring components and pages, `docs/qa/expectations.md`, the routes and API contracts already present. The mockup must look like the product: reuse what exists, invent nothing when an equivalent exists.
|
|
12
|
+
2. **List the states before drawing**: empty, loading, error, and loaded when there is data. Each is an addressable section `#etat-<état>` of one static `maquette.html` (HTML + CSS, no business JavaScript). Each field of the sheet carries `data-testid="<field id>"`.
|
|
13
|
+
3. **Render and look.** Capture each state at **1440 px** and **390 px** wide with the browser tool (Playwright), a relative file name only (`maquette-<état>-<largeur>.png`), 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.
|
|
14
|
+
Before keeping a state, check its sample data: compute the weekday of every date shown (`date -d`), never write it from memory.
|
|
15
|
+
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.
|
|
16
|
+
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.
|
|
17
|
+
6. **Under the threshold, say so.** If the lot changes no screen, give the one-line reason in `sousSeuil` and write nothing.
|
|
18
|
+
|
|
19
|
+
**A lot without a screen (`spec: true`) — back sheet.** The brief then names `<dir>/<sujet>/`, not a screen: there is no mockup, no capture and no state. You write only `fiche.yaml`, with `sujet` (id, titre, lot, perimetre), `cas` (use cases: trigger, preconditions, nominal steps, alternatives and error cases, each pointing to rules), `regles` (each attached to a case, sourced or a hypothesis, with planned tests named `Rn — …`), `contrats` (call, input, output, errors with the exact code and message) and `donnees`. Read the code and the tests of the modules the lot touches first: a rule already coded is a source (cite `file:line`), and a new rule must not contradict it. If the lot changes no behaviour (nothing a user, an API client or another service can observe), say so in `sousSeuil` and write nothing. The four costly categories are the same, instantiated for back: a stored model (key, column, table, file or cache format, migration), a published contract or route, an irreversible action (deletion, purge, sending, payment), a contradiction with a sourced or coded rule. An optional field added to an existing response is not a break when a rule of the sheet proves the old response unchanged: write it as a hypothesis with its `si_faux`. A lot with a screen that also changes a behaviour (the program reads the sheet: a `sujet` block next to `ecran`, never a `spec: true` beside `screen:`) has one sheet with both parts: the API calls of the screen are listed once in `contrats`, and the actions of the screen point to them with `contrat: Ax`.
|
|
20
|
+
|
|
21
|
+
- You edit only the spec folder (`<dir>/<écran>/` for a screen, `<dir>/<sujet>/` without one): never application code, tests or the plan. Do not push, deliver, run `raf start|done|note|review|ux`, or launch subagents.
|
|
22
|
+
- Commit with explicit paths and a message that cites the lot; leave the tree clean.
|
|
23
|
+
- Give the report in the structured output the brief asks for.
|
package/agents/ux-reviewer.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ux-reviewer
|
|
3
|
-
description: Usability and accessibility reviewer for any web interface — reviews a page, a screen or a user-facing change before it is marked done, and runs the planned reviews of existing screens. Grounds every finding in a named rule (Nielsen heuristics, WCAG 2.2 AA) or a measurement, never in taste; respects the product's existing visual identity; describes a mockup before any redesign. Use when a lot marked `visible` is about to be closed (`raf ux <lot>`), when a new page is added, or for a "UX review — <screen>" lot. Does not modify code.
|
|
3
|
+
description: Usability and accessibility reviewer for any web interface — reviews a page, a screen or a user-facing change before it is marked done, and runs the planned reviews of existing screens. Grounds every finding in a named rule (Nielsen heuristics, WCAG 2.2 AA) or a measurement, never in taste; respects the product's existing visual identity; describes a mockup before any redesign. Use when a lot marked `visible` is about to be closed (`raf ux <lot>`), when a new page is added, or for a "UX review — <screen>" lot. Also reviews a static mockup and its screen sheet before any code is written (mockup mode) and checks the running app against that mockup after the code (conformity mode), both used by the orchestrator. Does not modify code.
|
|
4
4
|
tools: Read, Grep, Glob, Bash, mcp__playwright, mcp__plugin_playwright_playwright
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -13,6 +13,25 @@ screen needs data or a login, ask for — or find in the project docs — the wa
|
|
|
13
13
|
(demo data, stubbed API). Read the project's CLAUDE.md and design notes first: the product's existing
|
|
14
14
|
identity (colours, density, tone) is a constraint, not something to "fix".
|
|
15
15
|
|
|
16
|
+
## Mockup mode
|
|
17
|
+
|
|
18
|
+
When the brief says "mockup mode" (the orchestrator, before any code exists), the subject is not an application but `maquette.html` and its sheet `fiche.yaml`, both under `docs/specs/<écran>/`. There is no server to start and no data to find: open the mockup as a local file, and apply the method below to each state — each state is the section `#etat-<état>` — at **1440 px** and **390 px**, after looking at the captures `maquette-<état>-<largeur>.png` the spec writer committed (re-capture a state that looks stale). Walk the task from the sheet: the fields, the rules, the actions.
|
|
19
|
+
|
|
20
|
+
On top of the usability and accessibility checks, check the **consistency mockup ↔ sheet**, state by state:
|
|
21
|
+
|
|
22
|
+
- every field of `champs` is in the mockup and carries `data-testid="<id of the field>"`; no field in the mockup is missing from the sheet;
|
|
23
|
+
- every rule message (`regles[].message`) is visible in the mockup, word for word, in the state its `etat` names, and in no other state by mistake;
|
|
24
|
+
- every state of `etats` has its section `#etat-<état>`; the `masque` of an empty-state rule really hides the fields it names;
|
|
25
|
+
- the units, formats and wording the sheet declares are those shown.
|
|
26
|
+
|
|
27
|
+
Check the **sample data** too: recompute the weekday of every date shown (a command such as `date -d`), do not trust the one written. A wrong sample datum (weekday, date, figure that does not add up) is **minor**, never major: it changes no field, rule or state of the sheet, so it does not send the mockup back to the spec writer.
|
|
28
|
+
|
|
29
|
+
A mismatch is a finding like any other (rule: the sheet is the reference of the later steps; a mockup that contradicts it misleads the acceptance tests). Rank blocking / major / minor as below: a blocking or major finding sends the mockup back to the spec writer, a minor one becomes a note of the lot. Leave the proposed sub-tasks empty — a mockup has no observable bug. This verdict is **not** `raf ux`: the conformity pass after the code keeps that role.
|
|
30
|
+
|
|
31
|
+
## Conformity mode
|
|
32
|
+
|
|
33
|
+
When the brief says "conformity" (the orchestrator, after the code, for a lot that has a screen sheet), the mockup `maquette.html` was already reviewed before any code: this is **not the full audit**. Compare the running app to the mockup and its captures `maquette-<état>-<largeur>.png`, state by state (the states of the sheet), at **1440 px** and **390 px**, and report the differences of rendering and ergonomics that the acceptance tests do not cover: layout, spacing and hierarchy that depart from the mockup, wording on screen, contrast, focus, target sizes, reflow. Each finding names the rule (Nielsen, WCAG 2.2 AA) or the measure and the mockup capture it differs from. Do not recheck the rules of the sheet (the tests cover them) and do not redo the audit of the whole screen. Rank blocking / major / minor as below; the verdict is the input of `raf ux`, which the lead decides on the captures, not you.
|
|
34
|
+
|
|
16
35
|
## Method
|
|
17
36
|
|
|
18
37
|
1. **Look before judging.** Capture the screen with the browser tool available (Playwright or
|
package/bin/cadence.js
CHANGED
|
@@ -13,7 +13,7 @@ const io = {
|
|
|
13
13
|
|
|
14
14
|
if (tool === 'raf') {
|
|
15
15
|
process.exitCode = await run(args, io);
|
|
16
|
-
} else if (['news', 'session', 'deliver', 'verify', 'skills', 'orchestrate', 'lead'].includes(tool)) {
|
|
16
|
+
} else if (['news', 'session', 'deliver', 'verify', 'skills', 'orchestrate', 'lead', 'corrections', 'spec'].includes(tool)) {
|
|
17
17
|
process.exitCode = await run([tool, ...args], io);
|
|
18
18
|
} else if (tool === '--version' || tool === '-v') {
|
|
19
19
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
@@ -43,6 +43,16 @@ if (tool === 'raf') {
|
|
|
43
43
|
cadence lead tour [dossier] [--idle 3] [--json]
|
|
44
44
|
le tableau du lead, sans modèle : une ligne par sous-dossier qui a un plan, ou la ligne du dossier courant
|
|
45
45
|
s'il est lui-même un projet ; une première ligne « cadence.yaml · ✗ erreur » si priority: est illisible
|
|
46
|
+
cadence corrections [dossier] [--classify [--model m]] [--cache fichier] [--sample n] [--until date] [--json]
|
|
47
|
+
la mesure des passes de correction des vagues (dossier/.cadence/runs) : lots à ≥1 et ≥2 corrections,
|
|
48
|
+
par projet, visibles ou non, passes par cause ; --classify les classe par Ollama (hors quota)
|
|
49
|
+
cadence spec check <fiche.yaml>
|
|
50
|
+
les invariants d'une fiche d'écran, back ou mixte (étape spec ; gabarits templates/fiche.yaml, fiche-back.yaml) ; code 1 si un écart, 2 si illisible
|
|
51
|
+
cadence spec qa <fiche.yaml>
|
|
52
|
+
la section de docs/qa/expectations.md dérivée de la fiche (shows, never, api), sur la sortie standard
|
|
53
|
+
cadence spec reseal <lot> <test> --reason "pourquoi"
|
|
54
|
+
le chemin du lead quand un test d'acceptation scellé du lot en cours contredit la fiche : le test corrigé dans l'arbre,
|
|
55
|
+
l'empreinte de ce seul test refaite, la raison dans acceptation.json et en note du lot ; hors vague orchestrée
|
|
46
56
|
cadence skills install [--dir .claude] [--force]
|
|
47
57
|
installe les skills Claude Code session-start, session-close, deliver et l'agent ux-reviewer`);
|
|
48
58
|
process.exitCode = !tool || ['help', '--help', '-h'].includes(tool) ? 0 : 2;
|
package/dist/audit.js
CHANGED
|
@@ -4,7 +4,8 @@ import { check } from './check.js';
|
|
|
4
4
|
import { parse } from 'yaml';
|
|
5
5
|
import { changedFiles, fileAt, readCommits } from './git.js';
|
|
6
6
|
import { citedRefs, linkCommits, scopeOf } from './link.js';
|
|
7
|
-
import { readDocsConfig, readNewsConfig } from './config.js';
|
|
7
|
+
import { readDocsConfig, readNewsConfig, readOrchestrateConfig } from './config.js';
|
|
8
|
+
import { ficheHypotheses } from './spec.js';
|
|
8
9
|
import { docSyncGaps, filesOf, gapMessage } from './docsync.js';
|
|
9
10
|
import { loadEntries, newsIssues, PUBLIC_TITLE_DEFAULT, publicTitleTooLong, reusedNewsTitle } from './news.js';
|
|
10
11
|
import { isRecurring } from './recurring.js';
|
|
@@ -158,11 +159,22 @@ export function audit(plan, root, newsDir, today, opts = {}) {
|
|
|
158
159
|
const m = l.public ? publicTitleTooLong(l.public, max) : reused ? publicTitleTooLong(reused, max) : null;
|
|
159
160
|
return m ? [{ message: `${l.id} : ${reused && !l.public ? m.replace('titre public', 'titre public repris de la Nouveauté') : m}` }] : [];
|
|
160
161
|
});
|
|
161
|
-
const issues = [...check(lots, { ...linked, byLot: all.byLot }, today, opts.idle ?? 7), ...newsIssues(lots, entries, newsDir), ...titles, ...gates, ...docSyncIssues(plan, root, all.byLot), ...(plan.hasPublicField ? missingPublicTitles(lots, entries) : []),
|
|
162
|
+
const issues = [...check(lots, { ...linked, byLot: all.byLot }, today, opts.idle ?? 7), ...newsIssues(lots, entries, newsDir, hypothesesOf(root, plan.configFile ?? join(root, 'cadence.yaml'))), ...titles, ...gates, ...docSyncIssues(plan, root, all.byLot), ...(plan.hasPublicField ? missingPublicTitles(lots, entries) : []),
|
|
162
163
|
...plan.ignore.invalid.map((src) => ({ message: `ignore : motif invalide « ${src} »` }))];
|
|
163
164
|
// Un plan en lecture seule se corrige avec l'outil du projet : ne pas conseiller une commande raf qui refuserait.
|
|
164
165
|
return plan.readonly ? issues.map((i) => ({ ...i, message: i.message.replace(/ — raf (start|public) .*$/, '') })) : issues;
|
|
165
166
|
}
|
|
167
|
+
/** Hypothèses des fiches, dans le dossier de `orchestrate.spec.dir` ; une configuration d'orchestrate illisible n'empêche pas le contrôle : dossier par défaut. */
|
|
168
|
+
export function hypothesesOf(root, configFile) {
|
|
169
|
+
let dir = 'docs/specs';
|
|
170
|
+
try {
|
|
171
|
+
dir = readOrchestrateConfig(configFile).spec.dir;
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// dossier par défaut
|
|
175
|
+
}
|
|
176
|
+
return ficheHypotheses(root, dir);
|
|
177
|
+
}
|
|
166
178
|
/**
|
|
167
179
|
* Documentation en retard (L143, clé `docs.sync`) : un lot en cours — ou terminé à partir de `docs.since` — dont les commits de
|
|
168
180
|
* travail touchent des chemins sans toucher le document que la règle désigne. Les commits de plan ne comptent pas.
|
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import {
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { basename, dirname, join, posix, relative, resolve } from 'node:path';
|
|
3
4
|
import { parseArgs } from 'node:util';
|
|
4
|
-
import { readDocsConfig, readHookConfig, readNewsConfig, readPlanConfig, readSessionConfig } from './config.js';
|
|
5
|
+
import { readDocsConfig, readHookConfig, readNewsConfig, readOrchestrateConfig, readPlanConfig, readSessionConfig } from './config.js';
|
|
5
6
|
import { repoSections, repoShas, resolveLotRepos } from './repos.js';
|
|
6
|
-
import { audit, exemptPlanOnly, ownFiles, isPlanOnly, lotWork, nextUp, planCommits, unreviewedWork } from './audit.js';
|
|
7
|
+
import { audit, exemptPlanOnly, hypothesesOf, ownFiles, isPlanOnly, lotWork, nextUp, planCommits, unreviewedWork } from './audit.js';
|
|
7
8
|
import { short } from './check.js';
|
|
8
9
|
import { isDay, toDay } from './dates.js';
|
|
9
10
|
import { deliver, parseDeliverConfig, realDeps } from './deliver.js';
|
|
@@ -13,22 +14,27 @@ import { gitRoot, readCommits, matchesHead, resolveCommit, stage, stagedFiles, s
|
|
|
13
14
|
import { installHook, pendingCommitMessage, REFUSED } from './hook.js';
|
|
14
15
|
import { citedRefs, linkCommits } from './link.js';
|
|
15
16
|
import { buildNews, loadEntries, newEntry, newsData, newsIssues, publicTitleTooLong, stampEntries } from './news.js';
|
|
16
|
-
import { Plan, RafError, STATUSES } from './plan.js';
|
|
17
|
+
import { Plan, RafError, SCREENS, STATUSES } from './plan.js';
|
|
17
18
|
import { dueLine, isRecurring, recurringByDue } from './recurring.js';
|
|
18
19
|
import { schedule } from './schedule.js';
|
|
19
20
|
import { AGENTS_DIR, installAgents, installSkills, SKILLS_DIR } from './skills.js';
|
|
20
|
-
import { findProjects, leadTour, tourLine } from './lead.js';
|
|
21
|
+
import { findProjects, leadTour, readLeadUx, tourLine } from './lead.js';
|
|
21
22
|
import { liveLines, orchestrate, realOrchestrateDeps } from './orchestrate/command.js';
|
|
22
23
|
import { activeLock, REPO_LOCK } from './orchestrate/lock.js';
|
|
23
24
|
import { cadenceHome, openLotsIn } from './orchestrate/registry.js';
|
|
25
|
+
import { classifyAll, measure, ollamaClassifier, readCache, readRuns, renderReport, sampleKeys, serializeCache } from './corrections.js';
|
|
26
|
+
import { findAcceptation, resealAcceptation } from './orchestrate/accept.js';
|
|
27
|
+
import { checkFiche, deriveQa, readFiche, summary } from './spec.js';
|
|
24
28
|
import { readHudContext } from './hud-context.js';
|
|
25
29
|
import { sessionClose, sessionStart } from './session.js';
|
|
26
30
|
import { clearNext, readNext, sharedStateDir, stateDir, writeNext } from './state.js';
|
|
27
31
|
const HELP = `raf — plan « reste à faire » versionné dans le dépôt, relié aux commits
|
|
28
32
|
|
|
29
33
|
raf init [--project nom] [--prefix L] [--no-hook]
|
|
30
|
-
raf add "titre" [--estimate j] [--quickwin] [--visible] [--public "titre public"] [--every jours] [--after L2,L4] [--parent L3]
|
|
34
|
+
raf add "titre" [--estimate j] [--quickwin] [--visible [--screen page|component|journey]] [--spec] [--public "titre public"] [--every jours] [--after L2,L4] [--parent L3]
|
|
31
35
|
raf public <id> "titre public" | --clear titre du lot dans le langage du public (pages Nouveautés et Plan)
|
|
36
|
+
raf screen <id> page|component|journey|none pose, remplace ou retire screen: d'un lot visible existant (l'étape spec d'orchestrate)
|
|
37
|
+
raf spec <id> on|off pose ou retire spec: d'un lot existant (le lot change un comportement : étape spec back d'orchestrate)
|
|
32
38
|
raf start <id> raf done <id> [--force] raf drop <id> [--reason texte]
|
|
33
39
|
raf note <id> "texte"
|
|
34
40
|
raf did <id> ["texte"] un lot récurrent (--every) a été refait : le « dû depuis N j » repart de aujourd'hui
|
|
@@ -45,7 +51,8 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
|
|
|
45
51
|
raf gantt [-o docs/plan/gantt.html]
|
|
46
52
|
raf hook install (pre-commit et post-commit ; cadence.yaml hook.autostart: warn|refuse|start)
|
|
47
53
|
cadence orchestrate <projet>:<lot>[@modèle]… [--budget 2M] [--dry-run] [--wave nom] [--continue [--until HH:MM] [--priority a,b]] une session claude neuve par étape ; --status [vague] [--watch [--interval s]] ; --resume [vague] [--budget 1M] [--answer projet:lot "réponse"] [--drop projet:lot] ; --drop projet:lot [--wave vague] ; --stop-after-current [--wave vague]
|
|
48
|
-
cadence lead tour [dossier] [--idle 3] [--json] le tableau du lead, sans modèle : une ligne par sous-dossier qui a un plan, ou la ligne du dossier seul s'il est lui-même un projet (en cours, dérive, notes de clôture, prochain lot prêt, dépôt)
|
|
54
|
+
cadence lead tour [dossier] [--idle 3] [--depth n] [--json] le tableau du lead, sans modèle : une ligne par sous-dossier qui a un plan, ou la ligne du dossier seul s'il est lui-même un projet (en cours, dérive, notes de clôture, prochain lot prêt, dépôt)
|
|
55
|
+
cadence corrections [dossier] [--classify [--model m]] [--cache fichier] [--sample n] [--until date] [--json] mesure des passes de correction des journaux dossier/.cadence/runs : lots à ≥1 et ≥2 corrections, par projet, visibles ou non, passes par cause (classées par Ollama avec --classify, hors quota)
|
|
49
56
|
cadence verify [--retry s] [--sha rév] rejoue deliver.verify hors livraison : 0 vert, 1 effet rouge, 2 rien à vérifier
|
|
50
57
|
raf news new <lot…> [--title t] | list | check | stamp | build [-o dossier] (aussi « cadence news … »)
|
|
51
58
|
|
|
@@ -106,6 +113,8 @@ function dispatch(argv, io) {
|
|
|
106
113
|
estimate: { type: 'string' },
|
|
107
114
|
quickwin: { type: 'boolean' },
|
|
108
115
|
visible: { type: 'boolean' },
|
|
116
|
+
screen: { type: 'string' },
|
|
117
|
+
spec: { type: 'boolean' },
|
|
109
118
|
public: { type: 'string' },
|
|
110
119
|
every: { type: 'string' },
|
|
111
120
|
dir: { type: 'string' },
|
|
@@ -129,6 +138,11 @@ function dispatch(argv, io) {
|
|
|
129
138
|
ignored: { type: 'boolean' },
|
|
130
139
|
notes: { type: 'boolean' },
|
|
131
140
|
json: { type: 'boolean' },
|
|
141
|
+
classify: { type: 'boolean' },
|
|
142
|
+
model: { type: 'string' },
|
|
143
|
+
cache: { type: 'string' },
|
|
144
|
+
sample: { type: 'string' },
|
|
145
|
+
until: { type: 'string' },
|
|
132
146
|
help: { type: 'boolean', short: 'h' },
|
|
133
147
|
},
|
|
134
148
|
});
|
|
@@ -137,7 +151,7 @@ function dispatch(argv, io) {
|
|
|
137
151
|
io.out(HELP);
|
|
138
152
|
return 0;
|
|
139
153
|
}
|
|
140
|
-
if (command !== 'session' && (values.all || values.depth !== undefined))
|
|
154
|
+
if (command !== 'session' && !(command === 'lead' && !values.all) && (values.all || values.depth !== undefined))
|
|
141
155
|
throw new RafError(`${values.all ? '--all' : '--depth'} ne va qu'avec session start ou session close`);
|
|
142
156
|
const today = io.env.RAF_TODAY ?? toDay(io.now());
|
|
143
157
|
if (!isDay(today))
|
|
@@ -146,14 +160,17 @@ function dispatch(argv, io) {
|
|
|
146
160
|
// Le tour parcourt les sous-dossiers (ou, sans sous-projet, prend le dossier lui-même s'il est un projet) : il ne lit pas le plan du dossier courant.
|
|
147
161
|
if (command === 'lead') {
|
|
148
162
|
if (rest[0] !== 'tour' || rest.length > 2)
|
|
149
|
-
throw new RafError('usage : cadence lead tour [dossier] [--idle 3] [--json]');
|
|
163
|
+
throw new RafError('usage : cadence lead tour [dossier] [--idle 3] [--depth n] [--json]');
|
|
150
164
|
const idle = values.idle === undefined ? undefined : Number(values.idle);
|
|
151
165
|
if (idle !== undefined && !(Number.isInteger(idle) && idle >= 0))
|
|
152
166
|
throw new RafError(`--idle invalide : ${values.idle}`);
|
|
167
|
+
const depth = values.depth === undefined ? undefined : Number(values.depth);
|
|
168
|
+
if (depth !== undefined && !(Number.isInteger(depth) && depth >= 1))
|
|
169
|
+
throw new RafError(`--depth invalide : ${values.depth} (un entier ≥ 1)`);
|
|
153
170
|
const parent = resolve(io.cwd, rest[1] ?? '.');
|
|
154
171
|
if (!existsSync(parent))
|
|
155
172
|
throw new RafError(`lead tour : dossier introuvable : ${parent}`);
|
|
156
|
-
const rows = leadTour(parent, today, idle);
|
|
173
|
+
const rows = leadTour(parent, today, idle, depth);
|
|
157
174
|
if (values.json)
|
|
158
175
|
io.out(JSON.stringify(rows, null, 2));
|
|
159
176
|
else if (rows.length === 0)
|
|
@@ -165,9 +182,25 @@ function dispatch(argv, io) {
|
|
|
165
182
|
const [slots] = liveLines();
|
|
166
183
|
if (slots)
|
|
167
184
|
io.out(slots);
|
|
185
|
+
// le réglage lead.ux du cadence.yaml du dossier (L197) : le lead lit ici qui pose le verdict UX, au lieu d'ouvrir le fichier
|
|
186
|
+
try {
|
|
187
|
+
if (readLeadUx(parent) === 'lead')
|
|
188
|
+
io.out('verdict UX : lead (le lead pose raf ux d\'après le rapport du ux-reviewer et les captures ; l\'humain n\'est consulté qu\'en cas de doute)');
|
|
189
|
+
}
|
|
190
|
+
catch (e) {
|
|
191
|
+
// un cadence.yaml illisible a déjà sa ligne d'erreur en tête de tour
|
|
192
|
+
if (/lead\.ux/.test(e.message))
|
|
193
|
+
io.out(`cadence.yaml · ✗ erreur : ${e.message.replace(/^.*cadence\.yaml : /, '')}`);
|
|
194
|
+
}
|
|
168
195
|
}
|
|
169
196
|
return 0;
|
|
170
197
|
}
|
|
198
|
+
if (command === 'corrections')
|
|
199
|
+
return corrections(rest, values, io);
|
|
200
|
+
// `spec` est aussi un verbe de raf (raf spec <lot> on|off, L186) : seuls check et qa vont à la fiche.
|
|
201
|
+
if (command === 'spec' && (rest[0] === 'check' || rest[0] === 'qa'))
|
|
202
|
+
return spec(rest, io);
|
|
203
|
+
const reseal = command === 'spec' && rest[0] === 'reseal';
|
|
171
204
|
// --all balaie les sous-dossiers : il ne lit ni le plan ni le cadence.yaml du dossier courant.
|
|
172
205
|
if (command === 'session' && (values.all || values.depth !== undefined))
|
|
173
206
|
return sessionAll(rest, values, io);
|
|
@@ -211,6 +244,10 @@ function dispatch(argv, io) {
|
|
|
211
244
|
const plan = loadPlan();
|
|
212
245
|
let id;
|
|
213
246
|
if (values.parent) {
|
|
247
|
+
if (values.screen !== undefined)
|
|
248
|
+
throw new RafError('--screen se pose sur un lot, pas sur une sous-tâche');
|
|
249
|
+
if (values.spec)
|
|
250
|
+
throw new RafError('--spec se pose sur un lot, pas sur une sous-tâche');
|
|
214
251
|
if (values.public !== undefined)
|
|
215
252
|
throw new RafError('--public se pose sur un lot, pas sur une sous-tâche');
|
|
216
253
|
if (values.every !== undefined)
|
|
@@ -227,7 +264,7 @@ function dispatch(argv, io) {
|
|
|
227
264
|
throw new RafError(`périodicité invalide : ${values.every} (un nombre entier de jours)`);
|
|
228
265
|
if (values.public !== undefined)
|
|
229
266
|
refuseLongTitle(values.public);
|
|
230
|
-
id = plan.add(rest.join(' '), today, { estimate, quickwin: values.quickwin, visible: values.visible, public: values.public, every, after });
|
|
267
|
+
id = plan.add(rest.join(' '), today, { estimate, quickwin: values.quickwin, visible: values.visible, screen: values.screen, spec: values.spec, public: values.public, every, after });
|
|
231
268
|
}
|
|
232
269
|
plan.save();
|
|
233
270
|
io.out(id);
|
|
@@ -288,6 +325,30 @@ function dispatch(argv, io) {
|
|
|
288
325
|
plan.save();
|
|
289
326
|
return 0;
|
|
290
327
|
}
|
|
328
|
+
case 'screen': {
|
|
329
|
+
need(2, 'screen <id> page|component|journey|none');
|
|
330
|
+
if (rest.length > 2)
|
|
331
|
+
throw new RafError('usage : raf screen <id> page|component|journey|none');
|
|
332
|
+
const plan = loadPlan();
|
|
333
|
+
if (rest[1] !== 'none' && !SCREENS.includes(rest[1]))
|
|
334
|
+
throw new RafError(`screen inconnu : ${rest[1]} (${SCREENS.join(', ')}, none)`);
|
|
335
|
+
plan.setScreen(rest[0], rest[1] === 'none' ? null : rest[1]);
|
|
336
|
+
plan.save();
|
|
337
|
+
return 0;
|
|
338
|
+
}
|
|
339
|
+
case 'spec': {
|
|
340
|
+
if (reseal)
|
|
341
|
+
return specReseal(rest.slice(1), values.reason, { root, loadPlan, configPath, today }, io);
|
|
342
|
+
if (rest.length !== 2 || (rest[1] !== 'on' && rest[1] !== 'off'))
|
|
343
|
+
throw new RafError('usage : raf spec <id> on|off (ou cadence spec check|qa <fiche.yaml>)');
|
|
344
|
+
const plan = loadPlan();
|
|
345
|
+
if (!plan.setSpec(rest[0], rest[1] === 'on')) {
|
|
346
|
+
io.out(`${rest[0]} a déjà screen: — l'écran implique la spec, rien à changer`);
|
|
347
|
+
return 0;
|
|
348
|
+
}
|
|
349
|
+
plan.save();
|
|
350
|
+
return 0;
|
|
351
|
+
}
|
|
291
352
|
case 'ux':
|
|
292
353
|
case 'review':
|
|
293
354
|
return gate(command, rest, loadPlan, root, today, io);
|
|
@@ -321,6 +382,10 @@ function dispatch(argv, io) {
|
|
|
321
382
|
io.out(`after : ${lot.after.join(', ')}`);
|
|
322
383
|
if (lot.public)
|
|
323
384
|
io.out(`public : ${lot.public}`);
|
|
385
|
+
if (lot.screen)
|
|
386
|
+
io.out(`screen : ${lot.screen}`);
|
|
387
|
+
if (lot.spec)
|
|
388
|
+
io.out('spec : true');
|
|
324
389
|
if (notes.length) {
|
|
325
390
|
io.out('notes :');
|
|
326
391
|
// Les lignes suivantes d'une note multiligne s'alignent sous son texte, pas en colonne 0.
|
|
@@ -430,10 +495,8 @@ function dispatch(argv, io) {
|
|
|
430
495
|
case 'deliver': {
|
|
431
496
|
if (!gitRoot(io.cwd))
|
|
432
497
|
throw new RafError('deliver : à lancer dans un dépôt git');
|
|
433
|
-
const orchestrating =
|
|
434
|
-
|
|
435
|
-
const stillOpen = orchestrating ? openLotsIn(cadenceHome(), orchestrating.pid, root) : null;
|
|
436
|
-
if (orchestrating && (stillOpen === null || stillOpen.length > 0))
|
|
498
|
+
const orchestrating = waveHolding(root);
|
|
499
|
+
if (orchestrating)
|
|
437
500
|
throw new RafError(`deliver : orchestration en cours (vague ${orchestrating.wave}, pid ${orchestrating.pid}) — livrer une fois les lots de ce dépôt finis (le verrou tombe dès que tous sont prêts ou rendus)`);
|
|
438
501
|
if (!existsSync(configPath))
|
|
439
502
|
throw new RafError(`pas de configuration de livraison : ${configPath} (voir « cadence.yaml » dans le README)`);
|
|
@@ -479,7 +542,7 @@ function dispatch(argv, io) {
|
|
|
479
542
|
return 0;
|
|
480
543
|
}
|
|
481
544
|
case 'news':
|
|
482
|
-
return news(rest, loadPlan(), newsDir, today, values, io, refuseLongTitle);
|
|
545
|
+
return news(rest, loadPlan(), newsDir, today, values, io, refuseLongTitle, () => hypothesesOf(root, configPath));
|
|
483
546
|
default:
|
|
484
547
|
throw new RafError(`commande inconnue : ${command} (raf --help)`);
|
|
485
548
|
}
|
|
@@ -665,7 +728,7 @@ function postCommit(load, newsDir, root, today, io, mode) {
|
|
|
665
728
|
}
|
|
666
729
|
return 0;
|
|
667
730
|
}
|
|
668
|
-
function news([sub, ...args], plan, dir, today, values, io, refuseLongTitle) {
|
|
731
|
+
function news([sub, ...args], plan, dir, today, values, io, refuseLongTitle, hypotheses) {
|
|
669
732
|
const lots = plan.lots();
|
|
670
733
|
switch (sub) {
|
|
671
734
|
case 'new': {
|
|
@@ -694,7 +757,7 @@ function news([sub, ...args], plan, dir, today, values, io, refuseLongTitle) {
|
|
|
694
757
|
case 'check':
|
|
695
758
|
case 'build': {
|
|
696
759
|
const entries = loadEntries(dir);
|
|
697
|
-
const issues = newsIssues(lots, entries, dir);
|
|
760
|
+
const issues = newsIssues(lots, entries, dir, sub === 'check' ? hypotheses() : undefined);
|
|
698
761
|
for (const i of issues)
|
|
699
762
|
io.out(`✗ ${i.message}`);
|
|
700
763
|
// Le build refuse seulement ce qu'il ne saurait pas publier : entrée illisible, capture absente.
|
|
@@ -736,6 +799,158 @@ function morningEffects(configPath, root) {
|
|
|
736
799
|
return [`✗ ${e.message}`];
|
|
737
800
|
}
|
|
738
801
|
}
|
|
802
|
+
/** La vague qui tient le verrou du dépôt tant qu'un de ses lots y travaille encore (état illisible : on la tient pour active) ; null si le dépôt est libre. Partagé par `deliver` et `spec reseal`. */
|
|
803
|
+
function waveHolding(root) {
|
|
804
|
+
const orchestrating = activeLock(join(sharedStateDir(root), REPO_LOCK));
|
|
805
|
+
if (!orchestrating)
|
|
806
|
+
return null;
|
|
807
|
+
const stillOpen = openLotsIn(cadenceHome(), orchestrating.pid, root);
|
|
808
|
+
return stillOpen === null || stillOpen.length > 0 ? orchestrating : null;
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* `cadence spec reseal <lot> <test> --reason texte` (L200) : le chemin du lead quand un test d'acceptation scellé du lot EN COURS contredit la fiche.
|
|
812
|
+
* Le test est corrigé dans l'arbre de travail (fiche inchangée) ; la commande refait l'empreinte de ce seul test, journalise la raison dans
|
|
813
|
+
* acceptation.json, commite test + acceptation (le nouveau commit rouge), et note la raison au lot. Le verrou garde tout le reste.
|
|
814
|
+
*/
|
|
815
|
+
const git = (cwd, args) => execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
816
|
+
function specReseal(args, reason, ctx, io) {
|
|
817
|
+
if (args.length !== 2 || !reason?.trim())
|
|
818
|
+
throw new RafError('usage : cadence spec reseal <lot> <test> --reason "pourquoi le test contredit la fiche"');
|
|
819
|
+
if (io.env.CADENCE_ORCHESTRATED)
|
|
820
|
+
throw new RafError(`spec reseal refusé pendant une vague orchestrée (${io.env.CADENCE_ORCHESTRATED}) : c'est le travail du lead, hors session`);
|
|
821
|
+
// Comme `deliver` : la vague tient le verrou du dépôt tant qu'un de ses lots y travaille (état illisible : on refuse) ; un commit du lead en pleine passe ferait signaler au lot suivant un écart qui n'est pas le sien.
|
|
822
|
+
const orchestrating = waveHolding(ctx.root);
|
|
823
|
+
if (orchestrating)
|
|
824
|
+
throw new RafError(`spec reseal : orchestration en cours (vague ${orchestrating.wave}, pid ${orchestrating.pid}) — rescellé une fois les lots de ce dépôt finis (le verrou tombe dès que tous sont prêts ou rendus)`);
|
|
825
|
+
const [lotId, test] = args;
|
|
826
|
+
const plan = ctx.loadPlan();
|
|
827
|
+
const lot = plan.lot(lotId);
|
|
828
|
+
if (lot.status !== 'doing')
|
|
829
|
+
throw new RafError(`${lotId} n'est pas en cours (${lot.status}) : le rescellé ne vaut que pour le lot en cours ; un lot livré évolue avec sa fiche (README, « Exception — a delivered lot »)`);
|
|
830
|
+
const repo = gitRoot(ctx.root) ?? ctx.root;
|
|
831
|
+
const dir = readOrchestrateConfig(ctx.configPath).spec.dir;
|
|
832
|
+
const json = findAcceptation(repo, dir, lotId);
|
|
833
|
+
if (!json)
|
|
834
|
+
throw new RafError(`${lotId} : aucune acceptation sous ${dir} (le lot n'a pas passé l'étape accept)`);
|
|
835
|
+
const before = { json: readFileSync(join(repo, json), 'utf8'), test: existsSync(join(repo, test)) ? readFileSync(join(repo, test), 'utf8') : '' };
|
|
836
|
+
let r;
|
|
837
|
+
try {
|
|
838
|
+
r = resealAcceptation(repo, json, test, reason, ctx.today);
|
|
839
|
+
}
|
|
840
|
+
catch (e) {
|
|
841
|
+
throw new RafError(e.message);
|
|
842
|
+
}
|
|
843
|
+
const norm = posix.normalize(test);
|
|
844
|
+
try {
|
|
845
|
+
git(repo, ['add', '--', json, norm]);
|
|
846
|
+
git(repo, ['commit', '-q', '-m', `test(${lotId}): acceptation rescellée, ${norm} : ${reason.trim()}`, '--', json, norm]);
|
|
847
|
+
}
|
|
848
|
+
catch (e) {
|
|
849
|
+
// Atomique : un commit refusé (hook pre-commit, ...) ne laisse ni empreinte refaite ni index à moitié prêt ; le test retrouve le texte du lead.
|
|
850
|
+
writeFileSync(join(repo, json), before.json);
|
|
851
|
+
writeFileSync(join(repo, norm), before.test);
|
|
852
|
+
try {
|
|
853
|
+
git(repo, ['reset', '-q', '--', json, norm]);
|
|
854
|
+
}
|
|
855
|
+
catch {
|
|
856
|
+
// rien d'indexé : rien à défaire
|
|
857
|
+
}
|
|
858
|
+
const out = e;
|
|
859
|
+
throw new RafError(`spec reseal : le commit du rescellé a échoué, ${json} et ${norm} sont revenus tels qu'avant la commande (rien n'est rescellé) — ${(out.stderr || out.stdout || out.message).trim().split('\n').slice(-5).join(' | ')}`);
|
|
860
|
+
}
|
|
861
|
+
const sha = git(repo, ['rev-parse', '--short', 'HEAD']).trim();
|
|
862
|
+
// Un plan tenu par un autre outil ne se modifie pas d'ici : la raison est déjà dans acceptation.json et dans le commit, la note reste à la main du lead.
|
|
863
|
+
if (plan.readonly) {
|
|
864
|
+
io.out(`${lotId} : ${norm} rescellé (${r.avant.slice(0, 7)} → ${r.apres.slice(0, 7)}), commit ${sha} ; raison dans ${json} et dans le commit — note à ajouter au lot avec l'outil du projet (plan en lecture seule)`);
|
|
865
|
+
return 0;
|
|
866
|
+
}
|
|
867
|
+
plan.note(lotId, `test d'acceptation rescellé (${norm}, fiche inchangée) : ${reason.trim()}`, ctx.today);
|
|
868
|
+
plan.save();
|
|
869
|
+
// La note du lot rejoint le dépôt dans la foulée (un dépôt sale arrêterait la vague suivante) ; un plan hors du dépôt ou non suivi reste à la main du lead.
|
|
870
|
+
const planRel = relative(repo, plan.path);
|
|
871
|
+
const tracked = !planRel.startsWith('..') && git(repo, ['ls-files', '--', planRel]).trim() !== '';
|
|
872
|
+
if (tracked) {
|
|
873
|
+
try {
|
|
874
|
+
git(repo, ['add', '--', planRel]);
|
|
875
|
+
git(repo, ['commit', '-q', '-m', `plan: ${lotId} test d'acceptation rescellé (${norm})`, '--', planRel]);
|
|
876
|
+
}
|
|
877
|
+
catch (e) {
|
|
878
|
+
// Le rescellé est fait (commit ${sha}) : on ne le défait pas, on dit où en est la note, au lieu d'une pile d'appels.
|
|
879
|
+
const out = e;
|
|
880
|
+
throw new RafError(`spec reseal : le rescellé a eu lieu (commit ${sha}) mais le commit de la note du lot a échoué — la note est écrite dans ${planRel}, à commiter à la main (un plan sale arrêterait la vague suivante) — ${(out.stderr || out.stdout || out.message).trim().split('\n').slice(-5).join(' | ')}`);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
io.out(`${lotId} : ${norm} rescellé (${r.avant.slice(0, 7)} → ${r.apres.slice(0, 7)}), commit ${sha} ; raison dans ${json} et en note du lot${tracked ? '' : ' (plan à commiter)'}`);
|
|
884
|
+
return 0;
|
|
885
|
+
}
|
|
886
|
+
/** `cadence spec check|qa <fiche>` (L170) : code 0 valide, 1 écarts, 2 usage ou fichier illisible. `qa` imprime la section expectations.md de l'écran. */
|
|
887
|
+
function spec(rest, io) {
|
|
888
|
+
const sub = rest[0];
|
|
889
|
+
if ((sub !== 'check' && sub !== 'qa') || rest.length !== 2)
|
|
890
|
+
throw new RafError('usage : cadence spec check|qa <fiche.yaml>');
|
|
891
|
+
const doc = readFiche(resolve(io.cwd, rest[1]));
|
|
892
|
+
const problems = checkFiche(doc);
|
|
893
|
+
if (sub === 'qa') {
|
|
894
|
+
// les écarts vont sur la sortie d'erreur : la sortie standard est la section, qu'on redirige vers le fichier
|
|
895
|
+
for (const m of problems)
|
|
896
|
+
io.err(`✗ ${m}`);
|
|
897
|
+
if (problems.length > 0)
|
|
898
|
+
return 1;
|
|
899
|
+
io.out(deriveQa(doc).trimEnd());
|
|
900
|
+
return 0;
|
|
901
|
+
}
|
|
902
|
+
for (const m of problems)
|
|
903
|
+
io.out(`✗ ${m}`);
|
|
904
|
+
if (problems.length > 0) {
|
|
905
|
+
io.out(`${problems.length} écart(s)`);
|
|
906
|
+
return 1;
|
|
907
|
+
}
|
|
908
|
+
io.out(summary(doc));
|
|
909
|
+
return 0;
|
|
910
|
+
}
|
|
911
|
+
/** `cadence corrections` (L165) : la mesure de référence des passes de correction d'une vague, lue dans <dossier>/.cadence/runs. */
|
|
912
|
+
async function corrections(rest, values, io) {
|
|
913
|
+
if (rest.length > 1)
|
|
914
|
+
throw new RafError('usage : cadence corrections [dossier] [--classify [--model m]] [--cache fichier] [--sample n] [--until date] [--json]');
|
|
915
|
+
const parent = resolve(io.cwd, rest[0] ?? '.');
|
|
916
|
+
const runsDir = join(parent, '.cadence', 'runs');
|
|
917
|
+
if (!existsSync(runsDir))
|
|
918
|
+
throw new RafError(`corrections : aucun journal de vague dans ${runsDir}`);
|
|
919
|
+
const sample = values.sample === undefined ? undefined : Number(values.sample);
|
|
920
|
+
if (sample !== undefined && !(Number.isInteger(sample) && sample > 0))
|
|
921
|
+
throw new RafError(`--sample invalide : ${values.sample}`);
|
|
922
|
+
if (values.until !== undefined && Number.isNaN(Date.parse(values.until)))
|
|
923
|
+
throw new RafError(`--until invalide : ${values.until} (une date ISO, ex. 2026-10-10T18:31Z)`);
|
|
924
|
+
const model = values.model ?? 'mistral-small3.2:24b';
|
|
925
|
+
const cacheFile = resolve(io.cwd, values.cache ?? join(parent, '.cadence', 'corrections.json'));
|
|
926
|
+
const data = readRuns(runsDir, values.until);
|
|
927
|
+
const cache = readCache(cacheFile);
|
|
928
|
+
if (values.classify) {
|
|
929
|
+
const classify = ollamaClassifier({ url: io.env.OLLAMA_HOST ?? 'http://localhost:11434', model });
|
|
930
|
+
await classifyAll(data.passes, cache, classify, model, (done, total) => {
|
|
931
|
+
if (done % 25 === 0 || done === total)
|
|
932
|
+
io.err(`classement : ${done}/${total}`);
|
|
933
|
+
}, { every: 10, save: () => writeFileSync(cacheFile, serializeCache(cache)) });
|
|
934
|
+
writeFileSync(cacheFile, serializeCache(cache));
|
|
935
|
+
}
|
|
936
|
+
if (sample !== undefined) {
|
|
937
|
+
const byKey = new Map(data.passes.map((p) => [p.key, p]));
|
|
938
|
+
// Une passe sans constat (tests rouges) n'a rien à relire : elle ne fait pas partie de l'échantillon.
|
|
939
|
+
for (const key of sampleKeys(data.passes.filter((p) => p.findings.length > 0).map((p) => p.key), sample)) {
|
|
940
|
+
const p = byKey.get(key);
|
|
941
|
+
io.out(`## ${key} (${p.project} ${p.lot}) · modèle : ${cache.get(key)?.cause ?? 'non classé'}${cache.get(key)?.manual ? ` · relu : ${cache.get(key).manual}` : ''}`);
|
|
942
|
+
for (const f of p.findings)
|
|
943
|
+
io.out(`- ${f}`);
|
|
944
|
+
for (const c of p.commits)
|
|
945
|
+
io.out(` commit : ${c}`);
|
|
946
|
+
io.out('');
|
|
947
|
+
}
|
|
948
|
+
return 0;
|
|
949
|
+
}
|
|
950
|
+
const m = measure(data, cache);
|
|
951
|
+
io.out(values.json ? JSON.stringify(m, null, 2) : renderReport(m));
|
|
952
|
+
return 0;
|
|
953
|
+
}
|
|
739
954
|
/** `session start|close --all [--depth n]` : la commande jouée dans chaque projet sous le dossier courant, une section par projet. */
|
|
740
955
|
async function sessionAll(rest, values, io) {
|
|
741
956
|
const sub = rest[0];
|