@sylad/cadence 1.4.0 → 1.6.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 +68 -31
- package/agents/acceptance-writer.md +1 -0
- package/agents/code-reviewer.md +1 -1
- package/agents/ux-reviewer.md +2 -0
- package/bin/cadence.js +1 -1
- package/dist/audit.js +21 -1
- package/dist/cli.js +25 -7
- package/dist/config.js +7 -2
- package/dist/corrections.js +8 -4
- package/dist/lead.js +6 -3
- package/dist/orchestrate/accept.js +21 -12
- package/dist/orchestrate/command.js +90 -13
- package/dist/orchestrate/cycle.js +89 -44
- package/dist/orchestrate/fiche.js +3 -3
- package/dist/orchestrate/maquette.js +12 -11
- package/dist/orchestrate/schemas.js +1 -1
- package/dist/orchestrate/snapshot.js +14 -0
- package/dist/plan.js +46 -1
- package/dist/session.js +3 -2
- package/package.json +1 -1
- package/skills/lead/SKILL.md +2 -3
- package/templates/orchestrate/accept-back.md +1 -0
- package/templates/orchestrate/accept.md +1 -0
- package/templates/orchestrate/fix.md +9 -0
- package/templates/orchestrate/implement.md +9 -0
- package/templates/orchestrate/maquette.md +2 -0
- package/templates/orchestrate/review-small.md +1 -0
- package/templates/orchestrate/review.md +1 -0
|
@@ -450,13 +450,14 @@ function briefFor(c, kind) {
|
|
|
450
450
|
const reviewed = [...(l.ficheConstats ?? []), ...(l.maquetteConstats ?? [])];
|
|
451
451
|
const written = ficheOf(c);
|
|
452
452
|
const back = partsOf(l, written).back;
|
|
453
|
+
const session = { run: (l.specRuns ?? 0) + 1, max: specBudget(c) };
|
|
453
454
|
vars.spec = specText({
|
|
454
455
|
dir: c.config.spec.dir,
|
|
455
456
|
render: c.config.spec.render,
|
|
456
457
|
screen: l.screen,
|
|
457
458
|
back,
|
|
458
459
|
problems: l.specProblems,
|
|
459
|
-
review: !reviewed.length ? undefined : back ? reviewedSheetText(reviewed, !!l.screen) : reviewedText(reviewed),
|
|
460
|
+
review: !reviewed.length ? undefined : back ? reviewedSheetText(reviewed, !!l.screen, session) : reviewedText(reviewed, session),
|
|
460
461
|
template: l.screen ? ficheTemplate() : undefined,
|
|
461
462
|
templateBack: back || l.screen ? ficheBackTemplate() : undefined,
|
|
462
463
|
});
|
|
@@ -471,7 +472,7 @@ function briefFor(c, kind) {
|
|
|
471
472
|
vars.reponse = '';
|
|
472
473
|
}
|
|
473
474
|
if (kind === 'maquette') {
|
|
474
|
-
vars.maquette = maquetteText({ fiche: ficheOf(c) ?? '', passes: l.maquette?.passes ?? 0, previous: l.maquetteConstats });
|
|
475
|
+
vars.maquette = maquetteText({ fiche: ficheOf(c) ?? '', passes: l.maquette?.passes ?? 0, max: c.config.maquetteReviews, previous: l.maquetteConstats });
|
|
475
476
|
vars.reponse = '';
|
|
476
477
|
}
|
|
477
478
|
if (kind === 'accept') {
|
|
@@ -517,6 +518,20 @@ const lotOver = (l) => l.budget !== undefined && lotSpent(l) >= l.budget;
|
|
|
517
518
|
* alors être refusée et le lot revient au lead sans revue, avec ses constats.
|
|
518
519
|
*/
|
|
519
520
|
export const REVIEW_RESERVE = 155_000;
|
|
521
|
+
/** Étapes d'écriture : les seules que le budget du lot borne (`session`), donc les seules qu'il puisse laisser non jouées. */
|
|
522
|
+
const WRITE_STEPS = ['implement', 'fix', 'spec', 'accept'];
|
|
523
|
+
/**
|
|
524
|
+
* Étape d'écriture que le budget a laissée non jouée (L223), pour un lot rendu au lead sur budget ; null sinon. `stop` vide `next` : l'étape se lit
|
|
525
|
+
* dans l'outcome que `overBudget` écrit (« étape « fix » non jouée »), repli sur `next` s'il est resté. Reconnu par ce texte (aucun marqueur dans
|
|
526
|
+
* l'état) : « correction non jouée (budget) » ou « budget du lot atteint ».
|
|
527
|
+
*/
|
|
528
|
+
export function budgetStep(l) {
|
|
529
|
+
if (l.status !== 'handed-back' || !/correction non jouée \(budget\)|budget du lot atteint/.test(l.outcome ?? ''))
|
|
530
|
+
return null;
|
|
531
|
+
const named = /étape « ([\w-]+) » non jouée/.exec(l.outcome ?? '')?.[1];
|
|
532
|
+
const step = named ?? l.next;
|
|
533
|
+
return step && WRITE_STEPS.includes(step) ? step : null;
|
|
534
|
+
}
|
|
520
535
|
/** Le budget du lot paie-t-il la passe de correction et la revue courte qui la suit ? Sans budget de lot : toujours. */
|
|
521
536
|
const fixAffordable = (l) => l.budget === undefined || lotSpent(l) + REVIEW_RESERVE < l.budget;
|
|
522
537
|
/** Cause d'une suspension demandée par `--stop-after-current` (L79). */
|
|
@@ -1112,6 +1127,8 @@ function takeReport(c, done, rep) {
|
|
|
1112
1127
|
l.warnings.push(`question ignorée (non-question) : « ${q.trim()} »`);
|
|
1113
1128
|
return rep.questions.filter((q) => !isNonQuestion(q));
|
|
1114
1129
|
}
|
|
1130
|
+
/** Plafond de sessions `spec` du lot (L230) : les revues de maquette d'un lot à écran, sinon deux. Fiche et maquette partagent `l.specRuns`. */
|
|
1131
|
+
const specBudget = (c) => (c.lot.screen && c.config.maquette ? c.config.maquetteReviews : 2);
|
|
1115
1132
|
/**
|
|
1116
1133
|
* Étape spec (L171, spec etape-spec S3) : la session écrit maquette, captures et fiche dans `<spec.dir>/<écran>/`, le programme
|
|
1117
1134
|
* en relève les écarts (une seconde passe, puis le lot est rendu), une hypothèse coûteuse à défaire devient une question au lead,
|
|
@@ -1134,7 +1151,7 @@ async function specStep(c) {
|
|
|
1134
1151
|
const questions = takeReport(c, done, rep);
|
|
1135
1152
|
if (questions.length) {
|
|
1136
1153
|
l.questions = questions;
|
|
1137
|
-
l.specRuns = undefined; // la réponse ouvre un nouveau budget de
|
|
1154
|
+
l.specRuns = undefined; // la réponse ouvre un nouveau budget de sessions
|
|
1138
1155
|
l.specAsked = undefined; // la prochaine réponse porte sur la question de la session, pas sur d'anciennes hypothèses coûteuses
|
|
1139
1156
|
l.next = 'spec';
|
|
1140
1157
|
transition(c, 'question', questions[0]);
|
|
@@ -1163,8 +1180,8 @@ async function specStep(c) {
|
|
|
1163
1180
|
const inDir = fiche === '' || fiche.startsWith(`${c.config.spec.dir.replace(/\/+$/, '')}/`);
|
|
1164
1181
|
const problems = !outside && fiche && !inDir ? [`${fiche} : fiche hors de ${c.config.spec.dir} (la spec vit dans ${c.config.spec.dir}/${what}/fiche.yaml, où l'étape la retrouve)`] : outside ? [`${reported} : fiche hors du dépôt (le chemin de « fiche » est relatif au dépôt : ${c.config.spec.dir}/${what}/fiche.yaml)`] : fiche ? checkSpec(l.repo, fiche, l.lot, partsOf(l, fiche)) : [`aucune fiche écrite : ${c.config.spec.dir}/${what}/fiche.yaml est attendue (ou une raison dans « sousSeuil » si le lot ne change ${l.screen ? 'aucun écran' : 'aucun comportement'})`];
|
|
1165
1182
|
if (problems.length) {
|
|
1166
|
-
if ((l.specRuns ?? 0) >=
|
|
1167
|
-
stop(c, 'handed-back', `spec non conforme après
|
|
1183
|
+
if ((l.specRuns ?? 0) >= specBudget(c)) {
|
|
1184
|
+
stop(c, 'handed-back', `spec non conforme après ${specBudget(c)} passes : ${problems.join(' ; ')}`);
|
|
1168
1185
|
return;
|
|
1169
1186
|
}
|
|
1170
1187
|
l.specProblems = problems;
|
|
@@ -1179,7 +1196,7 @@ async function specStep(c) {
|
|
|
1179
1196
|
if (costly.length) {
|
|
1180
1197
|
l.questions = costly.map((x) => `${x.id} est posée comme hypothèse mais touche un choix coûteux à défaire (${x.categorie}) : « ${x.siFaux} » — la confirmer, ou la corriger, avant que la fiche serve de référence.`);
|
|
1181
1198
|
l.specAsked = [...l.questions];
|
|
1182
|
-
l.specRuns = undefined; // la réponse du lead ouvre un nouveau budget de
|
|
1199
|
+
l.specRuns = undefined; // la réponse du lead ouvre un nouveau budget de sessions, comme celle d'une question de la session
|
|
1183
1200
|
l.next = 'spec';
|
|
1184
1201
|
transition(c, 'question', l.questions[0]);
|
|
1185
1202
|
return;
|
|
@@ -1280,8 +1297,8 @@ async function noteMinors(c, rep, label) {
|
|
|
1280
1297
|
const FICHE_REVIEW = { kind: 'fiche', label: 'fiche', source: 'code', after: afterFiche, retry: afterFiche };
|
|
1281
1298
|
const MAQUETTE_REVIEW = { kind: 'maquette', label: 'maquette', source: 'ux', after: afterMaquette, retry: () => 'spec' };
|
|
1282
1299
|
/**
|
|
1283
|
-
* Revue de la spec (fiche ou maquette) : une session de lecture seule. Un bloquant ou un majeur renvoie à la spec (
|
|
1284
|
-
*
|
|
1300
|
+
* Revue de la spec (fiche ou maquette) : une session de lecture seule. Un bloquant ou un majeur renvoie à la spec (avec les constats ; pour un lot mixte, ceux des deux revues ensemble), puis la spec est relue : deux revues pour la fiche,
|
|
1301
|
+
* `orchestrate.maquetteReviews` pour la maquette, dans le plafond de sessions spec du lot (`specBudget`) ; ce qui reste à la dernière revue est rendu au lead.
|
|
1285
1302
|
* Les mineurs de la fiche vont en notes du lot (ceux de la maquette restent des propositions). Ce verdict n'est pas `raf review` ni `raf ux` : la revue après code garde ce rôle.
|
|
1286
1303
|
*/
|
|
1287
1304
|
async function reviewSpecStep(c, r) {
|
|
@@ -1312,6 +1329,12 @@ async function reviewSpecStep(c, r) {
|
|
|
1312
1329
|
if (r.kind === 'fiche' && (await noteMinors(c, rep, `la ${r.label}`)))
|
|
1313
1330
|
return;
|
|
1314
1331
|
const constats = blockingConstats(rep, r.source);
|
|
1332
|
+
if (r.kind === 'maquette' && passes >= 2) {
|
|
1333
|
+
// Lu sur rep.constats : blockingConstats ne garde pas `inchange`. Seul le booléen vrai est une marque (checkShape ne descend pas dans constats[]).
|
|
1334
|
+
for (const k of rep.constats)
|
|
1335
|
+
if ((k.gravite === 'bloquant' || k.gravite === 'majeur') && k.inchange === true)
|
|
1336
|
+
c.wave.log(`${lotKey(l.project, l.lot)} · défaut de revue : maquette, revue ${passes}, majeur sur un point inchangé absent de la revue ${passes - 1} : ${k.texte}`);
|
|
1337
|
+
}
|
|
1315
1338
|
const key = r.kind === 'fiche' ? 'ficheConstats' : 'maquetteConstats';
|
|
1316
1339
|
if (summary.conforme) {
|
|
1317
1340
|
l[key] = undefined;
|
|
@@ -1319,13 +1342,14 @@ async function reviewSpecStep(c, r) {
|
|
|
1319
1342
|
save(c);
|
|
1320
1343
|
return;
|
|
1321
1344
|
}
|
|
1322
|
-
|
|
1323
|
-
|
|
1345
|
+
const reviews = r.kind === 'maquette' ? c.config.maquetteReviews : 2;
|
|
1346
|
+
if (passes >= reviews) {
|
|
1347
|
+
stop(c, 'handed-back', `${r.label} non conforme après ${reviews} revue${reviews > 1 ? 's' : ''} : ${summary.verdict}\n${bulletLines(constats).join('\n')}`);
|
|
1324
1348
|
return;
|
|
1325
1349
|
}
|
|
1326
|
-
// Le budget de
|
|
1327
|
-
if ((l.specRuns ?? 0) >=
|
|
1328
|
-
stop(c, 'handed-back', `${r.label} non conforme et la spec a déjà joué
|
|
1350
|
+
// Le budget de sessions spec est épuisé (écarts du programme compris) : un renvoi à la spec serait une session de trop.
|
|
1351
|
+
if ((l.specRuns ?? 0) >= specBudget(c)) {
|
|
1352
|
+
stop(c, 'handed-back', `${r.label} non conforme et la spec a déjà joué ${specBudget(c)} sessions : ${summary.verdict}\n${bulletLines(constats).join('\n')}`);
|
|
1329
1353
|
return;
|
|
1330
1354
|
}
|
|
1331
1355
|
l[key] = constats.length ? constats : [{ source: r.source, gravite: 'majeur', texte: summary.verdict }];
|
|
@@ -1476,6 +1500,12 @@ async function work(c, kind) {
|
|
|
1476
1500
|
badReport(c, done.step, e);
|
|
1477
1501
|
return;
|
|
1478
1502
|
}
|
|
1503
|
+
// L'écriture a changé le code (L221) : le verdict et les constats de la revue UX d'avant ne valent plus, l'UX est rejouée après la prochaine revue de code conforme.
|
|
1504
|
+
if (!l.small) {
|
|
1505
|
+
l.ux = undefined;
|
|
1506
|
+
l.uxVerdict = null;
|
|
1507
|
+
l.constats = l.constats.filter((k) => k.source !== 'ux');
|
|
1508
|
+
}
|
|
1479
1509
|
const plan = c.loadPlan();
|
|
1480
1510
|
const range = done.step.headBefore ? `${done.step.headBefore}..HEAD` : undefined;
|
|
1481
1511
|
const commits = readCommits(l.repo, { range });
|
|
@@ -1552,7 +1582,12 @@ async function work(c, kind) {
|
|
|
1552
1582
|
propose(c, minorLine('code', k));
|
|
1553
1583
|
l.constats = [];
|
|
1554
1584
|
l.warnings.push('passe des mineurs sans commit : le lot conclut sur la revue conforme d\'origine, mineurs rendus en propositions');
|
|
1555
|
-
if (l.code)
|
|
1585
|
+
if (l.code && uxAfterReview(c)) {
|
|
1586
|
+
// L221 : l'UX finale suit la dernière revue de code conforme, y compris quand la passe des mineurs n'a rien commité.
|
|
1587
|
+
l.next = 'ux';
|
|
1588
|
+
save(c);
|
|
1589
|
+
}
|
|
1590
|
+
else if (l.code)
|
|
1556
1591
|
await conclude(c, l.code, ' sans commit');
|
|
1557
1592
|
else
|
|
1558
1593
|
stop(c, 'handed-back', 'passe des mineurs sans commit, mais aucune revue conforme gardée');
|
|
@@ -1637,25 +1672,24 @@ async function work(c, kind) {
|
|
|
1637
1672
|
l.next = firstReview(c);
|
|
1638
1673
|
save(c);
|
|
1639
1674
|
}
|
|
1675
|
+
/** Mention « sans commit » du verdict quand la passe des mineurs n'a rien commité (l'UX finale conclut alors à la place de la passe). */
|
|
1676
|
+
const noCommitNote = (l) => (l.warnings.some((w) => w.startsWith('passe des mineurs sans commit')) ? ' sans commit' : '');
|
|
1640
1677
|
/** Revue de code qui clôt le cycle : complète, ou courte une fois la passe des mineurs jouée. */
|
|
1641
1678
|
const codeReview = (l) => (l.minorPass ? 'review-small' : 'review');
|
|
1642
1679
|
/**
|
|
1643
|
-
* Première étape après un travail d'écriture
|
|
1644
|
-
*
|
|
1680
|
+
* Première étape après un travail d'écriture (L221) : la revue de code, jamais l'UX. La revue UX d'un lot visible est jouée une seule fois,
|
|
1681
|
+
* après la dernière revue de code conforme (`uxAfterReview`), sur le code qui sera livré.
|
|
1645
1682
|
*/
|
|
1646
1683
|
function firstReview(c) {
|
|
1647
1684
|
const l = c.lot;
|
|
1648
1685
|
if (l.small)
|
|
1649
1686
|
return l.visible || l.minorPass ? 'review-small' : 'review'; // passe unique code + ergonomie ; sans écran, une revue de code
|
|
1650
|
-
if (l.visible)
|
|
1651
|
-
|
|
1652
|
-
l.uxNote = 'UX à faire par le lead (aucune application déclarée : cadence.yaml orchestrate.ux)';
|
|
1653
|
-
return codeReview(l);
|
|
1654
|
-
}
|
|
1655
|
-
return 'ux';
|
|
1656
|
-
}
|
|
1687
|
+
if (l.visible && !c.config.ux)
|
|
1688
|
+
l.uxNote = 'UX à faire par le lead (aucune application déclarée : cadence.yaml orchestrate.ux)';
|
|
1657
1689
|
return codeReview(l);
|
|
1658
1690
|
}
|
|
1691
|
+
/** La revue de code conforme d'un lot visible non petit est-elle suivie de la revue UX ? Sans `orchestrate.ux`, le lot conclut (note « UX à faire par le lead »). */
|
|
1692
|
+
const uxAfterReview = (c) => !c.lot.small && c.lot.visible && !!c.config.ux;
|
|
1659
1693
|
/** Une passe de correction de plus, ou la main rendue au lead quand les deux sont faites. */
|
|
1660
1694
|
function toFix(c, constats, what) {
|
|
1661
1695
|
const l = c.lot;
|
|
@@ -1677,12 +1711,20 @@ const seesApp = (l, kind) => kind === 'ux' || (kind === 'review-small' && l.smal
|
|
|
1677
1711
|
const appNote = (state, ux) => state.kind === 'busy' ? `UX non vérifiée : port occupé (${ux.url} répond déjà, l'application n'a pas été lancée)` : state.kind === 'unverified' ? `UX non vérifiée : ${state.cause}` : '';
|
|
1678
1712
|
/**
|
|
1679
1713
|
* Avec `command` ET `url`, le programme lance l'application avant l'étape et la tue après (succès, erreur ou signal) :
|
|
1680
|
-
* port occupé ou application muette, la revue UX n'a pas lieu (note dans le lot
|
|
1681
|
-
* unique d'un petit lot, elle, a lieu sur le code seul.
|
|
1714
|
+
* port occupé ou application muette, la revue UX n'a pas lieu (étape `skipped`, note dans le lot) et le lot conclut
|
|
1715
|
+
* sur la dernière revue de code conforme (R6) ; la passe unique d'un petit lot, elle, a lieu sur le code seul.
|
|
1682
1716
|
*/
|
|
1683
1717
|
async function review(c, kind) {
|
|
1684
1718
|
const l = c.lot;
|
|
1685
1719
|
const ux = c.config.ux;
|
|
1720
|
+
// Un état repris, écrit avant L221, peut porter next = ux : l'UX ne part que sur une revue de code conforme du HEAD courant, sinon la revue de code passe d'abord (R9).
|
|
1721
|
+
if (kind === 'ux' && !(l.code?.conforme && l.code.head === git(l.repo, 'rev-parse', 'HEAD'))) {
|
|
1722
|
+
l.next = codeReview(l);
|
|
1723
|
+
if (l.status === 'suspended')
|
|
1724
|
+
l.status = 'reviewing'; // reprise d'un lot suspendu : la boucle de runLot enchaîne sur la revue de code au lieu de rendre la main
|
|
1725
|
+
save(c);
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1686
1728
|
if (!ux?.command || !ux.url || !seesApp(l, kind))
|
|
1687
1729
|
return reviewStep(c, kind);
|
|
1688
1730
|
const halt = halted(c.wave, l);
|
|
@@ -1690,6 +1732,7 @@ async function review(c, kind) {
|
|
|
1690
1732
|
suspend(c, halt); // la session n'aurait pas lieu : l'application ne se lance pas pour rien
|
|
1691
1733
|
return;
|
|
1692
1734
|
}
|
|
1735
|
+
let skipped = false;
|
|
1693
1736
|
c.wave.log(`${lotKey(l.project, l.lot)} · lancement de l'application (${ux.url})`);
|
|
1694
1737
|
const app = await startApp({ command: ux.command, url: ux.url, cwd: l.repo, nodeBin: l.node?.link, log: join(c.wave.store.lotDir(l.project, l.lot), 'ux-app.log'), timeoutMs: (ux.timeout ?? APP_TIMEOUT_S) * 1000 });
|
|
1695
1738
|
try {
|
|
@@ -1698,12 +1741,7 @@ async function review(c, kind) {
|
|
|
1698
1741
|
l.uxNote = appNote(app.state, ux);
|
|
1699
1742
|
c.wave.log(`${lotKey(l.project, l.lot)} · ${l.uxNote.split('\n')[0]}`);
|
|
1700
1743
|
if (kind === 'ux') {
|
|
1701
|
-
//
|
|
1702
|
-
l.ux = undefined;
|
|
1703
|
-
l.uxVerdict = null;
|
|
1704
|
-
l.constats = l.constats.filter((k) => k.source !== 'ux');
|
|
1705
|
-
l.next = codeReview(l);
|
|
1706
|
-
save(c);
|
|
1744
|
+
skipped = true; // aucune session : la revue de code conforme suffit, le lot conclut avec la note
|
|
1707
1745
|
return;
|
|
1708
1746
|
}
|
|
1709
1747
|
}
|
|
@@ -1714,6 +1752,8 @@ async function review(c, kind) {
|
|
|
1714
1752
|
finally {
|
|
1715
1753
|
c.app = undefined;
|
|
1716
1754
|
await app.stop();
|
|
1755
|
+
if (skipped && l.code)
|
|
1756
|
+
await conclude(c, l.code, noCommitNote(l));
|
|
1717
1757
|
}
|
|
1718
1758
|
}
|
|
1719
1759
|
async function reviewStep(c, kind) {
|
|
@@ -1735,8 +1775,12 @@ async function reviewStep(c, kind) {
|
|
|
1735
1775
|
addProposals(c, rep, 'ux');
|
|
1736
1776
|
l.uxVerdict = summary.verdict;
|
|
1737
1777
|
l.constats = blockingConstats(rep, 'ux');
|
|
1738
|
-
l.next = codeReview(l); // la revue de code clôt toujours le cycle
|
|
1739
1778
|
save(c);
|
|
1779
|
+
// L'UX est la dernière étape (L221) : conforme, le lot conclut avec la revue de code ; sinon une correction (jamais une revue de code d'office), la revue de code et l'UX rejouée suivent.
|
|
1780
|
+
if (summary.conforme)
|
|
1781
|
+
await conclude(c, l.code, noCommitNote(l));
|
|
1782
|
+
else
|
|
1783
|
+
toFix(c, l.constats, 'UX non conforme');
|
|
1740
1784
|
return;
|
|
1741
1785
|
}
|
|
1742
1786
|
l.code = summary;
|
|
@@ -1746,18 +1790,18 @@ async function reviewStep(c, kind) {
|
|
|
1746
1790
|
l.ux = summary;
|
|
1747
1791
|
l.uxVerdict = summary.verdict;
|
|
1748
1792
|
}
|
|
1749
|
-
const uxOk = l.small || !l.ux || l.ux.conforme;
|
|
1750
1793
|
const minors = minorConstats(rep);
|
|
1751
1794
|
// Revue conforme avec mineurs : une seule passe de correction des mineurs, avant de conclure (rien sous le tapis).
|
|
1752
|
-
// Budget épuisé : la passe n'aurait aucune session pour la jouer,
|
|
1753
|
-
|
|
1795
|
+
// Budget épuisé : la passe n'aurait aucune session pour la jouer, les mineurs sont rendus en propositions ; la revue conforme conclut le lot,
|
|
1796
|
+
// sauf un lot visible avec orchestrate.ux, qui passe à next = 'ux' puis reste suspendu tant que le budget de la vague l'est.
|
|
1797
|
+
const wanted = summary.conforme && !l.minorPass && !l.light && minors.length > 0; // lot léger : les mineurs restent des notes (L108)
|
|
1754
1798
|
const noBudget = wanted && (w.budget.exhausted || !fixAffordable(l)) && !w.incident && !w.quota.hit;
|
|
1755
1799
|
const minorPass = wanted && !noBudget;
|
|
1756
1800
|
if (noBudget)
|
|
1757
1801
|
l.warnings.push(`${w.budget.exhausted ? 'budget atteint' : 'budget du lot trop juste pour payer la correction et la revue courte qui la suivrait'} : la passe des mineurs n'a pas eu lieu, mineurs rendus en propositions`);
|
|
1758
1802
|
// Les mineurs confiés à la passe ont pu être proposés par une revue non conforme antérieure : si la revue qui la suit est
|
|
1759
1803
|
// conforme, ils sont traités et ne restent pas en propositions (ceux que cette revue signale encore sont ajoutés juste après).
|
|
1760
|
-
if (l.minorPass && !minorPass && summary.conforme &&
|
|
1804
|
+
if (l.minorPass && !minorPass && summary.conforme && l.minorLines?.length) {
|
|
1761
1805
|
// Chaque mineur confié retire UNE proposition : la même ligne d'abord, sinon une de même fichier et même texte (le correctif a pu décaler la ligne).
|
|
1762
1806
|
// Un autre mineur de même fichier et même texte, à une autre ligne, reste donc proposé. Limite connue : un texte reformulé n'est pas reconnu et reste proposé.
|
|
1763
1807
|
const left = [...l.proposals];
|
|
@@ -1774,7 +1818,7 @@ async function reviewStep(c, kind) {
|
|
|
1774
1818
|
l.minorLines = [];
|
|
1775
1819
|
}
|
|
1776
1820
|
addProposals(c, rep, 'code', !minorPass);
|
|
1777
|
-
if (summary.conforme
|
|
1821
|
+
if (summary.conforme)
|
|
1778
1822
|
(l.reviewCounts ??= []).push(0); // une revue conforme compte pour 0 dans la comparaison de la convergence (L205)
|
|
1779
1823
|
if (minorPass) {
|
|
1780
1824
|
l.minorPass = true;
|
|
@@ -1786,18 +1830,19 @@ async function reviewStep(c, kind) {
|
|
|
1786
1830
|
save(c);
|
|
1787
1831
|
return;
|
|
1788
1832
|
}
|
|
1789
|
-
if (summary.conforme
|
|
1790
|
-
|
|
1833
|
+
if (summary.conforme) {
|
|
1834
|
+
if (uxAfterReview(c)) {
|
|
1835
|
+
l.next = 'ux';
|
|
1836
|
+
save(c);
|
|
1837
|
+
}
|
|
1838
|
+
else
|
|
1839
|
+
await conclude(c, summary);
|
|
1791
1840
|
return;
|
|
1792
1841
|
}
|
|
1793
|
-
const constats =
|
|
1842
|
+
const constats = blockingConstats(rep, 'code');
|
|
1794
1843
|
(l.reviewCounts ??= []).push(constats.length);
|
|
1795
1844
|
toFix(c, constats, 'revue non conforme');
|
|
1796
1845
|
}
|
|
1797
|
-
/** Constats UX de la dernière revue UX, gardés dans l'état par la revue elle-même (l.constats avant la revue de code). */
|
|
1798
|
-
function uxConstatsKept(c) {
|
|
1799
|
-
return c.lot.constats.filter((k) => k.source === 'ux');
|
|
1800
|
-
}
|
|
1801
1846
|
/** Revue conforme : le verdict est enregistré avec le sha relu, puis le commit du plan. `raf done` reste au lead. */
|
|
1802
1847
|
async function conclude(c, code, minorNote = '') {
|
|
1803
1848
|
const l = c.lot;
|
|
@@ -13,10 +13,10 @@ export function ficheReviewText(o) {
|
|
|
13
13
|
lines.push('', 'Points of the first review that the spec writer had to fix:', ...bulletLines(o.previous));
|
|
14
14
|
return lines.join('\n');
|
|
15
15
|
}
|
|
16
|
-
/** Consigne de la
|
|
17
|
-
export function reviewedSheetText(constats, mockup) {
|
|
16
|
+
/** Consigne de la passe `spec` qui suit une revue : les constats bloquants et majeurs des revues de la fiche (et de la maquette d'un lot mixte), ensemble, la session `run` sur `max`. */
|
|
17
|
+
export function reviewedSheetText(constats, mockup, o) {
|
|
18
18
|
return [
|
|
19
|
-
`The review of your ${mockup ? 'sheet and mockup' : 'sheet'} found the blocking and major points below. Fix exactly these in the sheet${mockup ? ', the mockup and the captures' : ''} (
|
|
19
|
+
`The review of your ${mockup ? 'sheet and mockup' : 'sheet'} found the blocking and major points below. Fix exactly these in the sheet${mockup ? ', the mockup and the captures' : ''} (spec session ${o.run} of ${o.max}: it is reviewed again, and what remains once the sessions are used goes back to the lead). Keep the sheet consistent: every rule sourced or a hypothesis with its \`si_faux\`, every rule attached to a case and tested, every contract with its errors:`,
|
|
20
20
|
...bulletLines(constats),
|
|
21
21
|
].join('\n');
|
|
22
22
|
}
|
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
import { posix } from 'node:path';
|
|
2
2
|
import { LARGEURS } from '../spec.js';
|
|
3
|
-
// Revue UX de la maquette (L172, spec etape-spec S4) : ce que le programme dit à l'agent ux-reviewer en mode « maquette », et à l'agent spec à la
|
|
3
|
+
// Revue UX de la maquette (L172, spec etape-spec S4) : ce que le programme dit à l'agent ux-reviewer en mode « maquette », et à l'agent spec à la passe suivante.
|
|
4
4
|
/** Une ligne par constat : gravité, endroit, texte. */
|
|
5
5
|
export const bulletLines = (constats) => constats.map((k) => `- [${k.gravite}] ${k.fichier ? `${k.fichier}${k.ligne ? `:${k.ligne}` : ''} — ` : ''}${k.texte}`);
|
|
6
|
-
/** Consigne de la revue de maquette dans le brief : la fiche, la maquette, les captures attendues, la
|
|
6
|
+
/** Consigne de la revue de maquette dans le brief : la fiche, la maquette, les captures attendues, le rang de la revue sur le plafond `max` (L230). */
|
|
7
7
|
export function maquetteText(o) {
|
|
8
8
|
const folder = posix.dirname(o.fiche);
|
|
9
|
-
const
|
|
9
|
+
const rank = o.passes + 1;
|
|
10
|
+
const later = rank >= 2;
|
|
10
11
|
const lines = [
|
|
11
12
|
`Mockup mode: review the mockup \`${folder}/maquette.html\` against its sheet \`${o.fiche}\`. It is a local file, not an application: open it directly (file URL), do not start any server.`,
|
|
12
13
|
`The spec writer committed the captures \`${folder}/maquette-<état>-<largeur>.png\` (one per state of the sheet and per width: ${LARGEURS.join(' and ')} px): look at every one, and re-capture a state yourself when a capture looks stale. Each state is the section \`#etat-<état>\` of the mockup.`,
|
|
13
|
-
|
|
14
|
-
?
|
|
15
|
-
:
|
|
14
|
+
later
|
|
15
|
+
? `This is review ${rank} of ${o.max}${rank >= o.max ? ', the last review' : ''}: the spec writer fixed the points of the previous one (below). Check them first, then run the whole sweep again and report ALL the blocking and major findings of it in this report. Mark \`inchange: true\` on every blocking or major finding about a point the spec writer did not change and the previous review had not raised.`
|
|
16
|
+
: `This is the first review of this mockup (${o.max} review${o.max > 1 ? 's' : ''} at most): there is no later review to keep a finding for. Report ALL the blocking and major findings in this first report; do not keep one back for a later review.`,
|
|
16
17
|
];
|
|
17
|
-
if (
|
|
18
|
-
lines.push('', 'Points of the
|
|
18
|
+
if (later && o.previous?.length)
|
|
19
|
+
lines.push('', 'Points of the previous review that the spec writer had to fix:', ...bulletLines(o.previous));
|
|
19
20
|
return lines.join('\n');
|
|
20
21
|
}
|
|
21
|
-
/** Consigne de la
|
|
22
|
-
export function reviewedText(constats) {
|
|
22
|
+
/** Consigne de la passe `spec` qui suit une revue : les constats bloquants et majeurs de la revue UX de la maquette, la session `run` sur `max`. */
|
|
23
|
+
export function reviewedText(constats, o) {
|
|
23
24
|
return [
|
|
24
|
-
|
|
25
|
+
`The UX review of your mockup found the blocking and major points below. Fix exactly these in the mockup, the captures and the sheet (spec session ${o.run} of ${o.max}: the mockup is reviewed again, and what remains after the last review goes back to the lead). Keep the sheet consistent with the mockup — every field, every rule message visible in the state its \`etat\` names, every \`data-testid\`:`,
|
|
25
26
|
...bulletLines(constats),
|
|
26
27
|
].join('\n');
|
|
27
28
|
}
|
|
@@ -40,7 +40,7 @@ export const REVIEW_SCHEMA = {
|
|
|
40
40
|
bloquants: num,
|
|
41
41
|
majeurs: num,
|
|
42
42
|
mineurs: num,
|
|
43
|
-
constats: { type: 'array', items: { type: 'object', properties: { gravite: { type: 'string', enum: ['bloquant', 'majeur', 'mineur'] }, fichier: str, ligne: num, texte: str }, required: ['gravite', 'texte'] } },
|
|
43
|
+
constats: { type: 'array', items: { type: 'object', properties: { gravite: { type: 'string', enum: ['bloquant', 'majeur', 'mineur'] }, fichier: str, ligne: num, texte: str, inchange: { type: 'boolean', description: 'optional, review 2 or later of a mockup: true when the finding is on a point the spec writer did not change and the previous review had not raised' } }, required: ['gravite', 'texte'] } },
|
|
44
44
|
sousTaches: strings,
|
|
45
45
|
nonVerifie: strings,
|
|
46
46
|
verdict: str,
|
|
@@ -89,6 +89,20 @@ export function waveReadsControl(waveDir) {
|
|
|
89
89
|
return false;
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* L'instantané de la vague connaît-il `--budget projet:lot=+N` (L223) ? Un instantané plus ancien relit l'argument avec son `parseBudget` et le refuse
|
|
94
|
+
* comme budget invalide. Sans instantané : rien à relire, on suppose que oui.
|
|
95
|
+
*/
|
|
96
|
+
export function waveReadsLotBudget(waveDir) {
|
|
97
|
+
if (!snapshotExists(waveDir))
|
|
98
|
+
return true;
|
|
99
|
+
try {
|
|
100
|
+
return readFileSync(join(toolDirOf(waveDir), 'dist', 'orchestrate', 'command.js'), 'utf8').includes('lotBudgets');
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
92
106
|
/** Relance réelle : un processus node sur la copie, signaux transmis, code de sortie rendu. */
|
|
93
107
|
export function spawnReexec(toolDir, argv, env, sink) {
|
|
94
108
|
return new Promise((resolve, reject) => {
|
package/dist/plan.js
CHANGED
|
@@ -3,9 +3,10 @@ import { dirname } from 'node:path';
|
|
|
3
3
|
import { isMap, isSeq, parseDocument } from 'yaml';
|
|
4
4
|
import { isDay, toDay } from './dates.js';
|
|
5
5
|
export const STATUSES = ['todo', 'doing', 'done', 'dropped'];
|
|
6
|
+
export const GATE_INVALID = 'gate invalide : count: entier ≥ 1 et un critère spec: true ou screen: page attendus';
|
|
6
7
|
export class RafError extends Error {
|
|
7
8
|
}
|
|
8
|
-
export const FIELDS = ['id', 'title', 'status', 'estimate', 'quickwin', 'visible', 'screen', 'spec', 'public', 'every', 'last', 'after', 'created', 'started', 'finished', 'notes', 'repos', 'parent'];
|
|
9
|
+
export const FIELDS = ['id', 'title', 'status', 'estimate', 'quickwin', 'visible', 'screen', 'spec', 'public', 'every', 'last', 'after', 'created', 'started', 'finished', 'notes', 'repos', 'parent', 'gate'];
|
|
9
10
|
/** Nature de l'écran d'un lot visible : nouvelle page, nouveau composant, parcours modifié. */
|
|
10
11
|
export const SCREENS = ['page', 'component', 'journey'];
|
|
11
12
|
export function isOpen(status) {
|
|
@@ -398,6 +399,28 @@ export class Plan {
|
|
|
398
399
|
if (status === 'done' || status === 'dropped')
|
|
399
400
|
node.set('finished', today);
|
|
400
401
|
}
|
|
402
|
+
/**
|
|
403
|
+
* Remet en attente un lot démarré (doing → todo, `started` retiré, note datée) — `raf todo` (L220). Les refus passent dans
|
|
404
|
+
* l'ordre : plan modifiable, lot et non sous-tâche, lot connu, lot doing ; `commits` n'est lu qu'ensuite (il lit git) et un lot
|
|
405
|
+
* qui a du travail est refusé. Les sous-tâches et les autres clés du lot ne bougent pas.
|
|
406
|
+
*/
|
|
407
|
+
putBack(ref, today, reason, commits) {
|
|
408
|
+
this.writable();
|
|
409
|
+
if (ref.includes('/'))
|
|
410
|
+
throw new RafError(`raf todo se pose sur un lot, pas une sous-tâche : ${ref}`);
|
|
411
|
+
const lot = this.lot(ref);
|
|
412
|
+
if (lot.status === 'todo')
|
|
413
|
+
throw new RafError(`${ref} est déjà todo`);
|
|
414
|
+
if (lot.status !== 'doing')
|
|
415
|
+
throw new RafError(`${ref} est ${lot.status} ; le rouvrir à la main dans le YAML si c'est voulu`);
|
|
416
|
+
const n = commits();
|
|
417
|
+
if (n > 0)
|
|
418
|
+
throw new RafError(`${ref} a ${n} commit(s) qui le citent : le remettre en attente perdrait leur lien — raf drop ou le laisser doing`);
|
|
419
|
+
const node = this.lotNode(ref);
|
|
420
|
+
node.set('status', 'todo');
|
|
421
|
+
node.delete('started');
|
|
422
|
+
this.note(ref, reason.trim() ? `remis en attente : ${reason.trim()}` : 'remis en attente', today);
|
|
423
|
+
}
|
|
401
424
|
/** Pose, remplace (texte) ou efface (null) le titre public d'un lot ; la clé suit `title`. */
|
|
402
425
|
setPublic(ref, text) {
|
|
403
426
|
this.writable();
|
|
@@ -541,6 +564,7 @@ function foreignLots(raw, format) {
|
|
|
541
564
|
finished: day('finished'),
|
|
542
565
|
notes: typeof notes === 'string' ? [{ text: notes }] : notes,
|
|
543
566
|
repos: pick('repos'),
|
|
567
|
+
gate: pick('gate'),
|
|
544
568
|
}, problems);
|
|
545
569
|
const parent = pick('parent');
|
|
546
570
|
return { lot, parent: parent == null ? undefined : String(parent) };
|
|
@@ -559,6 +583,25 @@ function foreignLots(raw, format) {
|
|
|
559
583
|
}
|
|
560
584
|
return lots;
|
|
561
585
|
}
|
|
586
|
+
/** Lit `gate: { count: N, spec: true }` ou `{ count: N, screen: page }` ; toute autre forme est un problème du lot et une condition jamais remplie. */
|
|
587
|
+
function readGate(raw, problems) {
|
|
588
|
+
if (raw == null)
|
|
589
|
+
return undefined;
|
|
590
|
+
const invalid = () => {
|
|
591
|
+
problems.push(GATE_INVALID);
|
|
592
|
+
return { invalid: true };
|
|
593
|
+
};
|
|
594
|
+
if (typeof raw !== 'object' || Array.isArray(raw))
|
|
595
|
+
return invalid();
|
|
596
|
+
const g = raw;
|
|
597
|
+
if (!Number.isInteger(g.count) || g.count < 1)
|
|
598
|
+
return invalid();
|
|
599
|
+
const criteria = [...(g.spec === true ? ['spec'] : []), ...(g.screen === 'page' ? ['screen'] : [])];
|
|
600
|
+
const others = Object.keys(g).filter((k) => k !== 'count' && k !== 'spec' && k !== 'screen');
|
|
601
|
+
if (criteria.length !== 1 || others.length || ('spec' in g && g.spec !== true) || ('screen' in g && g.screen !== 'page'))
|
|
602
|
+
return invalid();
|
|
603
|
+
return { count: g.count, criterion: criteria[0] };
|
|
604
|
+
}
|
|
562
605
|
/** Un titre public vide ne dirait rien au public : refusé plutôt qu'écrit. */
|
|
563
606
|
function cleanPublic(text) {
|
|
564
607
|
const t = text.trim();
|
|
@@ -595,6 +638,7 @@ function normalizeLot(raw, problems = []) {
|
|
|
595
638
|
else if (raw.spec !== false)
|
|
596
639
|
problems.push(`spec : true ou false attendu (reçu « ${String(raw.spec)} »)`);
|
|
597
640
|
}
|
|
641
|
+
const gate = readGate(raw.gate, problems);
|
|
598
642
|
return {
|
|
599
643
|
id: String(raw.id),
|
|
600
644
|
title: String(raw.title ?? ''),
|
|
@@ -604,6 +648,7 @@ function normalizeLot(raw, problems = []) {
|
|
|
604
648
|
visible: raw.visible === true,
|
|
605
649
|
...(screen ? { screen } : {}),
|
|
606
650
|
...(spec ? { spec } : {}),
|
|
651
|
+
...(gate ? { gate } : {}),
|
|
607
652
|
...(typeof raw.public === 'string' && raw.public.trim() !== '' ? { public: raw.public.trim() } : {}),
|
|
608
653
|
...(Number.isInteger(raw.every) && raw.every > 0 ? { every: raw.every } : {}),
|
|
609
654
|
...(last ? { last } : {}),
|
package/dist/session.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import { CLEAN_DAYS, scanStale } from './clean.js';
|
|
3
3
|
import { dueLine, isRecurring, recurringByDue } from './recurring.js';
|
|
4
|
-
import { audit, exemptPlanOnly, nextUp, planCommits } from './audit.js';
|
|
4
|
+
import { audit, exemptPlanOnly, gateWaiting, nextUp, planCommits } from './audit.js';
|
|
5
5
|
import { diffDays, maxDay } from './dates.js';
|
|
6
6
|
import { repoStatus } from './git.js';
|
|
7
7
|
import { linkCommits } from './link.js';
|
|
@@ -109,7 +109,7 @@ export function sessionStart(ctx, opts) {
|
|
|
109
109
|
const lock = lockStatus(ctx.shared);
|
|
110
110
|
if (lock)
|
|
111
111
|
out(`\n${lock.line}`);
|
|
112
|
-
const { doing, ready } = nextUp(lots);
|
|
112
|
+
const { doing, ready, blocked } = nextUp(lots);
|
|
113
113
|
out('\nEn cours');
|
|
114
114
|
if (doing.length === 0)
|
|
115
115
|
out(' (rien)');
|
|
@@ -119,6 +119,7 @@ export function sessionStart(ctx, opts) {
|
|
|
119
119
|
const state = last ? `dernière activité ${last}${idle > opts.idle ? `, silencieux depuis ${idle} j` : ''}` : 'aucune activité';
|
|
120
120
|
out(` ${l.id} ${l.title} (${state})`);
|
|
121
121
|
}
|
|
122
|
+
section(out, 'En attente', gateWaiting(lots, blocked).map(({ lot, text }) => `${lot.id} ${lot.title} (${text})`));
|
|
122
123
|
const recent = period(ctx, opts.since);
|
|
123
124
|
const done = byLotLines(recent);
|
|
124
125
|
if (recent.orphans.length)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sylad/cadence",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "A small, repo-native working method: a versioned plan linked to your commits, a changelog with screenshots, session rituals and deliveries proven by their effect.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Sylvain Ladoire",
|
package/skills/lead/SKILL.md
CHANGED
|
@@ -90,12 +90,11 @@ ends `interrupted`, and `--resume` takes it up again. To take a lot out of a wav
|
|
|
90
90
|
`--resume --drop <project>:<lot>` (the lot is handed back instead of being replayed). Neither kills a session.
|
|
91
91
|
|
|
92
92
|
`cadence orchestrate --dry-run …` first when a precondition is in doubt. The program, not the lead, runs
|
|
93
|
-
for each lot a fresh short session per step — implementation (Sonnet),
|
|
94
|
-
and the project declares how to see its app (`orchestrate.ux` in `cadence.yaml`), code review last
|
|
93
|
+
for each lot a fresh short session per step — implementation (Sonnet), code review
|
|
95
94
|
(Opus, the `code-reviewer` agent; Sonnet, with no pass on the minor findings, for a lot whose estimate is
|
|
96
95
|
≤ 0.25 d — `orchestrate.review` in `cadence.yaml`; a blocker or major finding still brings a correction and an
|
|
97
96
|
Opus review), a correction in a new session if the review is not compliant (two
|
|
98
|
-
passes at most, a third when the review converges: blocking + major findings down from one review to the next and only one left) — and records `raf review` itself when the code review is compliant. A lot with `spec: true` (no screen) goes `spec → fiche → accept` before the code: the `code-reviewer` reviews its back-end sheet, and the hypotheses of the final sheet come back in the table as « hypothèse de la fiche » lines, which the lead relays to the human. The state is in
|
|
97
|
+
passes at most, a third when the review converges: blocking + major findings down from one review to the next and only one left), then, once, a UX review if the lot is `visible` and the project declares how to see its app (`orchestrate.ux` in `cadence.yaml`) — and records `raf review` itself when the code review is compliant and the UX review too. A lot with `spec: true` (no screen) goes `spec → fiche → accept` before the code: the `code-reviewer` reviews its back-end sheet, and the hypotheses of the final sheet come back in the table as « hypothèse de la fiche » lines, which the lead relays to the human. The state is in
|
|
99
98
|
`.cadence/runs/<wave>/`, not in this conversation. `@haiku` only when the human writes it, for a
|
|
100
99
|
mechanical lot. Exit code: 0 all ready · 1 some lots handed back · 2 refused before acting · 3 wave
|
|
101
100
|
suspended (budget or usage limit; `--resume --budget …` continues).
|
|
@@ -5,6 +5,7 @@ Acceptance tests of the behaviour of lot `{{lot}}` in `{{chemin}}`, as the accep
|
|
|
5
5
|
Rules:
|
|
6
6
|
- One or more cases per rule of the sheet, named exactly as the sheet plans it (`regles[].tests`, e.g. `R5 — une entrée de 14 jours est refusée : null`); the tests 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`. What cannot be automated is out of scope.
|
|
7
7
|
- Every test must FAIL for now, because the feature is missing (not because of a typo or a broken setup): the program runs the dedicated command and refuses tests that pass.
|
|
8
|
+
- Cite what you assert: above each assertion, a comment cites the rule and the sentence of its statement that it verifies (`// R4 — « <sentence of the rule> »`), and you assert nothing beyond that sentence (a rule that forbids X does not license asserting that Y never happens). Self-check before the commit, assertion by assertion: one with no sentence of the sheet behind it is removed, or listed under "choix" of your report; a test that asserts more than its rule says binds the implementer to a requirement nobody wrote.
|
|
8
9
|
- Do not touch the sheet, the plan or any application code. Do not push, deliver, run `raf done`, `raf ux` or `raf review`.
|
|
9
10
|
- Commit with explicit paths (never `git add -A` or `commit -a`), ONE commit citing the lot (`test({{lot}}): acceptation R1–Rn (rouge)`) with the test files, the acceptation file and the runner configuration, and leave the working tree clean. The program seals the acceptation file afterwards (it adds the fingerprint of each test file): never write `empreintes` or `vert: true` yourself.
|
|
10
11
|
Never run `git checkout` or `git switch` to another commit or branch in this repository: the orchestrator watches it and a moved HEAD breaks the other sessions of the wave. To look at another sha, use a throwaway `git worktree add --detach <tmp> <sha>`, removed afterwards.
|
|
@@ -5,6 +5,7 @@ Acceptance tests of the screen of lot `{{lot}}` in `{{chemin}}`, as the acceptan
|
|
|
5
5
|
Rules:
|
|
6
6
|
- One or more cases per rule of the sheet, named exactly as the sheet plans it (`regles[].tests`, e.g. `R3 — erreur : message et bouton Réessayer`); selectors are the `data-testid` of the fields (= field ids of the sheet); API calls are simulated from `actions[]`. What cannot be automated (rendering, ergonomics, readability) is out of scope.
|
|
7
7
|
- Every test must FAIL for now, because the feature is missing (not because of a typo or a broken setup): the program runs the dedicated command and refuses tests that pass.
|
|
8
|
+
- Cite what you assert: above each assertion, a comment cites the rule and the sentence of its statement that it verifies (`// R4 — « <sentence of the rule> »`), and you assert nothing beyond that sentence (a rule that forbids X does not license asserting that Y never happens). Self-check before the commit, assertion by assertion: one with no sentence of the sheet behind it is removed, or listed under "choix" of your report; a test that asserts more than its rule says binds the implementer to a requirement nobody wrote.
|
|
8
9
|
- Do not touch the sheet, the mockup, the plan or any application code. Do not push, deliver, run `raf done`, `raf ux` or `raf review`.
|
|
9
10
|
- Commit with explicit paths (never `git add -A` or `commit -a`), ONE commit citing the lot (`test({{lot}}): acceptation R1–Rn (rouge)`) with the test files, the acceptation file and the runner configuration, and leave the working tree clean. The program seals the acceptation file afterwards (it adds the fingerprint of each test file): never write `empreintes` or `vert: true` yourself.
|
|
10
11
|
Never run `git checkout` or `git switch` to another commit or branch in this repository: the orchestrator watches it and a moved HEAD breaks the other sessions of the wave. To look at another sha, use a throwaway `git worktree add --detach <tmp> <sha>`, removed afterwards.
|
|
@@ -12,7 +12,16 @@ Rules: test first; commit each fix as soon as its tests pass, with explicit path
|
|
|
12
12
|
`git add -A` or `commit -a`), and a message that cites the lot (`fix({{lot}}): …`); run the project's
|
|
13
13
|
full test suite and build before reporting; do not push, deliver, run `raf done`, `raf ux` or
|
|
14
14
|
`raf review`. Read the project's CLAUDE.md first.
|
|
15
|
+
Sealed test of ANOTHER lot: if what your own sealed tests require contradicts a sealed acceptance test of another lot (an `acceptation-<lot>.json` that is not yours), it is a question to the lead naming both tests (file and `Rn — …` name of each): never modify, rewrite or reseal it and never pick a side. Stop there and put the question in your report. The program compares the fingerprints of the sealed tests of every other lot at the end of each session and hands your lot back, naming the first offending commit, before any review.
|
|
15
16
|
Never run `git checkout` or `git switch` to another commit or branch in this repository: the orchestrator watches it and a moved HEAD breaks the other sessions of the wave. To look at another sha, use a throwaway `git worktree add --detach <tmp> <sha>`, removed afterwards.
|
|
17
|
+
Self-check before handing back (the recurring findings of the reviews), covering only what you changed: go through this list point by point on your own fix commits before you report, and touch nothing else:
|
|
18
|
+
- documentation: the README and the architecture documentation describe the behaviour, updated in the same commits as the behaviour (and the CHANGELOG);
|
|
19
|
+
- tests: one test per rule of the fiche, named « Rn — … »; with no fiche, one test per business rule of the lot, named after it;
|
|
20
|
+
- no dead code, and no test that cannot fail: a test whose assertion is always true, or that does not exercise the rule it names;
|
|
21
|
+
- screens: for a modified screen, the capture due of each state, at 1440 and 390 px, with a relative file name; a lot with no modified screen owes none, and says so;
|
|
22
|
+
- no false comment: a comment, a docstring or a message never describes a behaviour the code no longer has or does not have.
|
|
23
|
+
A point you cannot hold is written under "choix" or "nonVerifie" of your report, not left unsaid.
|
|
24
|
+
|
|
16
25
|
If a finding is wrong, say so under "choix" with the reason. Ask (under "questions") only when a fix needs a decision that passes the rule below.
|
|
17
26
|
A question is legitimate only if it names what it would change: the scope of the lot, the architecture, or a costly rollback (data, production, public API);
|
|
18
27
|
otherwise it is a choice: decide, write the alternative you did not take, go on. Minor choices to settle yourself:
|
|
@@ -7,8 +7,17 @@ Rules: test first; commit each sub-part as soon as its tests pass, with explicit
|
|
|
7
7
|
`git add -A` or `commit -a`), and a message that cites the lot (`feat({{lot}}): …`); run the project's
|
|
8
8
|
full test suite and build before reporting; do not push, deliver, run `raf done`, `raf ux` or
|
|
9
9
|
`raf review`.
|
|
10
|
+
Sealed test of ANOTHER lot: if what your own sealed tests require contradicts a sealed acceptance test of another lot (an `acceptation-<lot>.json` that is not yours), it is a question to the lead naming both tests (file and `Rn — …` name of each): never modify, rewrite or reseal it and never pick a side. Stop there and put the question in your report. The program compares the fingerprints of the sealed tests of every other lot at the end of each session and hands your lot back, naming the first offending commit, before any review.
|
|
10
11
|
Never run `git checkout` or `git switch` to another commit or branch in this repository: the orchestrator watches it and a moved HEAD breaks the other sessions of the wave. To look at another sha, use a throwaway `git worktree add --detach <tmp> <sha>`, removed afterwards.
|
|
11
12
|
Documentation: the project's README (and its usage documentation) describes the behaviour you deliver, updated in the same commits, like the CHANGELOG — a README that does not follow your change is a major finding of the review.
|
|
13
|
+
Self-check before handing back (the recurring findings of the reviews). Go through this list point by point on your own commits before you report, and fix what is missing:
|
|
14
|
+
- documentation: the README and the architecture documentation describe the behaviour, updated in the same commits as the behaviour (and the CHANGELOG);
|
|
15
|
+
- tests: one test per rule of the fiche, named « Rn — … »; with no fiche, one test per business rule of the lot, named after it;
|
|
16
|
+
- no dead code, and no test that cannot fail: a test whose assertion is always true, or that does not exercise the rule it names;
|
|
17
|
+
- screens: for a modified screen, the capture due of each state, at 1440 and 390 px, with a relative file name; a lot with no modified screen owes none, and says so;
|
|
18
|
+
- no false comment: a comment, a docstring or a message never describes a behaviour the code no longer has or does not have.
|
|
19
|
+
A point you cannot hold is written under "choix" or "nonVerifie" of your report, not left unsaid.
|
|
20
|
+
|
|
12
21
|
Decide minor interpretation questions yourself (the wording of a message, a name, a default, the
|
|
13
22
|
reading of an ambiguous line of the lot) and list each one under "choix" in your report, with the
|
|
14
23
|
alternative you did not take — the reviewer re-reads them. Stop and ask (under "questions") only on a real blocker.
|