@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.
@@ -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 deux sessions
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) >= 2) {
1167
- stop(c, 'handed-back', `spec non conforme après 2 passes : ${problems.join(' ; ')}`);
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 deux sessions, comme celle d'une question de la session
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 (une seule seconde passe, avec
1284
- * les constats ; pour un lot mixte, ceux des deux revues ensemble), puis la spec est relue ; ce qui reste après la seconde revue est rendu au lead.
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
- if (passes >= 2) {
1323
- stop(c, 'handed-back', `${r.label} non conforme après 2 revues : ${summary.verdict}\n${bulletLines(constats).join('\n')}`);
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 deux sessions spec est épuisé (écarts du programme compris) : un renvoi à la spec serait une 3ᵉ session.
1327
- if ((l.specRuns ?? 0) >= 2) {
1328
- stop(c, 'handed-back', `${r.label} non conforme et la spec a déjà joué 2 sessions : ${summary.verdict}\n${bulletLines(constats).join('\n')}`);
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. Le code vient de changer : l'UX d'un lot visible est donc toujours rejouée
1644
- * (une revue antérieure serait périmée), puis une revue de code clôt le cycle.
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
- if (!c.config.ux) {
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, la revue de code suit) ; la passe
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
- // Aucune session : une revue UX antérieure, périmée par le code qui a changé, ne pèse plus.
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, le lot conclut sur la revue conforme et rend les mineurs.
1753
- const wanted = summary.conforme && uxOk && !l.minorPass && !l.light && minors.length > 0; // lot léger : les mineurs restent des notes (L108)
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 && uxOk && l.minorLines?.length) {
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 && uxOk)
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 && uxOk) {
1790
- await conclude(c, summary);
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 = [...blockingConstats(rep, 'code'), ...(!l.small && l.ux && !l.ux.conforme ? uxConstatsKept(c) : [])];
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 seconde passe `spec` : les constats bloquants et majeurs des revues de la fiche (et de la maquette d'un lot mixte), ensemble. */
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' : ''} (one more pass only: it is reviewed again, and what remains 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:`,
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 seconde passe.
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 passe en cours. */
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 second = o.passes >= 1;
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
- second
14
- ? 'This is the second and last review: the spec writer fixed the points of the first one (below). Check them first, then review the whole mockup again.'
15
- : 'This is the first review of this mockup.',
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 (second && o.previous?.length)
18
- lines.push('', 'Points of the first review that the spec writer had to fix:', ...bulletLines(o.previous));
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 seconde passe `spec` : les constats bloquants et majeurs de la revue UX de la maquette. */
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
- 'The UX review of your mockup found the blocking and major points below. Fix exactly these in the mockup, the captures and the sheet (one more pass only: the mockup is reviewed again, and what remains 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
+ `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.4.0",
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",
@@ -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), UX review if the lot is `visible`
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.