@sylad/cadence 1.26.0 → 1.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/agents/doc-keeper.md +17 -0
- package/agents/spec-writer.md +1 -0
- package/agents/ux-reviewer.md +48 -1
- package/dist/cli.js +90 -15
- package/dist/config-set.js +2 -1
- package/dist/config.js +16 -1
- package/dist/corrections.js +164 -1
- package/dist/lead.js +24 -7
- package/dist/orchestrate/cycle.js +29 -2
- package/dist/perf.js +18 -4
- package/dist/priority.js +77 -0
- package/dist/verify-gate.js +3 -2
- package/docs/cadence-hud.md +2 -0
- package/docs/corrections.md +11 -1
- package/docs/deliver.md +2 -0
- package/docs/fonctionnement-lead-dark.svg +2 -2
- package/docs/fonctionnement-lead-light.svg +2 -2
- package/docs/lead-workflow-dark.svg +2 -2
- package/docs/lead-workflow-light.svg +2 -2
- package/docs/lead.md +12 -4
- package/docs/orchestrate.md +7 -2
- package/docs/perf.md +1 -1
- package/docs/raf.md +1 -0
- package/docs/session.md +7 -0
- package/docs/skills.md +4 -0
- package/docs/whats-new.md +6 -0
- package/package.json +1 -1
- package/skills/lead/SKILL.md +8 -5
- package/templates/orchestrate/implement.md +1 -1
- package/templates/orchestrate/maquette.md +1 -1
- package/templates/orchestrate/spec.md +2 -0
package/dist/corrections.js
CHANGED
|
@@ -274,11 +274,172 @@ export function readCache(file) {
|
|
|
274
274
|
export function serializeCache(cache) {
|
|
275
275
|
return JSON.stringify(Object.fromEntries([...cache].sort(([a], [b]) => a.localeCompare(b))), null, 2) + '\n';
|
|
276
276
|
}
|
|
277
|
+
/** Les sous-causes d'une passe « bug de code » (L447, R1), dans l'ordre où le rapport les liste. */
|
|
278
|
+
export const SOUS_CAUSES = ['cas-limite', 'test-casse', 'contrat-mal-lu', 'appelant-oublie', 'affirmation-fausse', 'test-vide', 'autre'];
|
|
279
|
+
const SOUS_LABELS = {
|
|
280
|
+
'cas-limite': 'cas limite',
|
|
281
|
+
'test-casse': 'test cassé',
|
|
282
|
+
'contrat-mal-lu': 'contrat mal lu',
|
|
283
|
+
'appelant-oublie': 'appelant oublié',
|
|
284
|
+
'affirmation-fausse': 'affirmation fausse',
|
|
285
|
+
'test-vide': 'test vide',
|
|
286
|
+
autre: 'autre',
|
|
287
|
+
};
|
|
288
|
+
/** Ce que chaque valeur veut dire, une ligne chacune : le prompt du modèle de sous-cause (R10). */
|
|
289
|
+
const SOUS_DEFINITIONS = {
|
|
290
|
+
'cas-limite': 'un cas limite (entrée vide, absente, en double, d\'un autre type, refusée, forme réelle des données) que le code ne tient pas ou qu\'aucun test ne couvre',
|
|
291
|
+
'test-casse': 'un test existant que le changement casse',
|
|
292
|
+
'contrat-mal-lu': 'un contrat, une fiche ou un énoncé de lot mal lu : le code fait autre chose que ce qui est écrit',
|
|
293
|
+
'appelant-oublie': 'un appelant, un consommateur ou un autre site à changer a été oublié',
|
|
294
|
+
'affirmation-fausse': 'une documentation, un commentaire, un message ou un rapport affirme une chose fausse (chiffre, « jamais », « toujours », ancien comportement)',
|
|
295
|
+
'test-vide': 'un test qui ne peut pas échouer, ou un comportement ajouté sans test',
|
|
296
|
+
autre: 'tout bug de code qui n\'entre dans aucune des valeurs précédentes',
|
|
297
|
+
};
|
|
298
|
+
/** Les maillons de la chaîne où un mécanisme peut être posé (R2). */
|
|
299
|
+
export const MAILLONS = ['brief implement', 'auto-vérification avant rendu', 'porte locale', 'fiche'];
|
|
300
|
+
/** Le maillon dont relève chaque sous-cause (R2, hypothèse) ; `autre` n'en désigne aucun. */
|
|
301
|
+
export const MAILLON_DE = {
|
|
302
|
+
'cas-limite': 'brief implement',
|
|
303
|
+
'test-vide': 'brief implement',
|
|
304
|
+
'affirmation-fausse': 'auto-vérification avant rendu',
|
|
305
|
+
'appelant-oublie': 'auto-vérification avant rendu',
|
|
306
|
+
'test-casse': 'porte locale',
|
|
307
|
+
'contrat-mal-lu': 'fiche',
|
|
308
|
+
autre: null,
|
|
309
|
+
};
|
|
310
|
+
/** Seuils de désignation du maillon dominant (R2, hypothèse) : passes sous-classées au moins, et part minimale du maillon en tête, en %. */
|
|
311
|
+
export const MAILLON_MIN_PASSES = 10;
|
|
312
|
+
export const MAILLON_MIN_PART = 40;
|
|
313
|
+
/** Extrait une sous-cause connue d'une réponse (JSON `{sous}`) ; null sinon. */
|
|
314
|
+
export function parseSousCause(text) {
|
|
315
|
+
try {
|
|
316
|
+
const s = JSON.parse(text)?.sous;
|
|
317
|
+
return typeof s === 'string' && SOUS_CAUSES.includes(s) ? s : null;
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const SOUS_SYSTEM = `Tu sous-classes une passe de correction déjà classée « bug de code » : une session a écrit du code, une revue a formulé des constats, une session de correction les a traités. Réponds par UNE sous-cause, celle du constat le plus grave. Les valeurs :
|
|
324
|
+
${SOUS_CAUSES.map((s) => `- ${s} : ${SOUS_DEFINITIONS[s]} ;`).join('\n')}`;
|
|
325
|
+
/** Sous-classeur sur un modèle local servi par Ollama (comme `ollamaClassifier` : /api/chat, température 0, sortie contrainte par un schéma à sept valeurs). */
|
|
326
|
+
export function ollamaSousClassifier(opts) {
|
|
327
|
+
const doFetch = opts.fetch ?? fetch;
|
|
328
|
+
return async (pass) => {
|
|
329
|
+
const findings = pass.findings.map((f, i) => `${i + 1}. ${f.slice(0, 1500)}`).join('\n');
|
|
330
|
+
const user = `Lot ${pass.project} ${pass.lot}.\nConstats de la revue :\n${findings}\n\nCommits de la correction :\n${pass.commits.join('\n') || '(aucun)'}`;
|
|
331
|
+
try {
|
|
332
|
+
const res = await doFetch(`${opts.url.replace(/\/$/, '')}/api/chat`, {
|
|
333
|
+
method: 'POST',
|
|
334
|
+
headers: { 'content-type': 'application/json' },
|
|
335
|
+
body: JSON.stringify({
|
|
336
|
+
model: opts.model,
|
|
337
|
+
stream: false,
|
|
338
|
+
options: { temperature: 0 },
|
|
339
|
+
format: { type: 'object', properties: { sous: { type: 'string', enum: [...SOUS_CAUSES] }, raison: { type: 'string' } }, required: ['sous', 'raison'] },
|
|
340
|
+
messages: [{ role: 'system', content: SOUS_SYSTEM }, { role: 'user', content: user }],
|
|
341
|
+
}),
|
|
342
|
+
});
|
|
343
|
+
if (!res.ok)
|
|
344
|
+
return null;
|
|
345
|
+
const body = (await res.json());
|
|
346
|
+
return parseSousCause(body.message?.content ?? '');
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
/** La cause retenue d'une passe à constats : la relecture à la main, sinon le modèle ; absente sans classement. */
|
|
354
|
+
const retainedCause = (cache, p) => {
|
|
355
|
+
const c = cache.get(p.key);
|
|
356
|
+
return c ? (c.manual ?? c.cause) : undefined;
|
|
357
|
+
};
|
|
358
|
+
/** Les passes à sous-classer : à constats, de cause retenue bug-code (R1). */
|
|
359
|
+
export const bugCodePasses = (passes, cache) => passes.filter((p) => p.findings.length > 0 && retainedCause(cache, p) === 'bug-code');
|
|
360
|
+
/** Sous-classe les passes bug-code absentes du cache des sous-causes ; un échec n'y entre pas (R3). Rend le nombre d'entrées ajoutées. */
|
|
361
|
+
export async function classifySous(passes, cache, sousCache, classify, model, progress, checkpoint) {
|
|
362
|
+
const todo = bugCodePasses(passes, cache).filter((p) => !sousCache.has(p.key));
|
|
363
|
+
let done = 0;
|
|
364
|
+
let added = 0;
|
|
365
|
+
for (const p of todo) {
|
|
366
|
+
const sous = await classify(p);
|
|
367
|
+
if (sous) {
|
|
368
|
+
sousCache.set(p.key, { sous, by: `ollama:${model}` });
|
|
369
|
+
added++;
|
|
370
|
+
}
|
|
371
|
+
done++;
|
|
372
|
+
progress?.(done, todo.length);
|
|
373
|
+
if (checkpoint && added > 0 && done % checkpoint.every === 0)
|
|
374
|
+
checkpoint.save();
|
|
375
|
+
}
|
|
376
|
+
return added;
|
|
377
|
+
}
|
|
378
|
+
export function readSousCache(file) {
|
|
379
|
+
let raw = null;
|
|
380
|
+
if (existsSync(file)) {
|
|
381
|
+
raw = readJson(file);
|
|
382
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
383
|
+
throw new RafError(`${file} : JSON illisible (un objet clé → classement est attendu), corrigez ou supprimez le fichier`);
|
|
384
|
+
}
|
|
385
|
+
const known = (s) => typeof s === 'string' && SOUS_CAUSES.includes(s);
|
|
386
|
+
const entries = Object.entries((raw ?? {}));
|
|
387
|
+
for (const [key, c] of entries) {
|
|
388
|
+
if (!known(c?.sous))
|
|
389
|
+
throw new RafError(`${file} : sous-cause inconnue pour ${key} : ${String(c?.sous)}`);
|
|
390
|
+
if (c.manual !== undefined && !known(c.manual))
|
|
391
|
+
throw new RafError(`${file} : sous-cause inconnue pour ${key} : ${String(c.manual)}`);
|
|
392
|
+
}
|
|
393
|
+
return new Map(entries);
|
|
394
|
+
}
|
|
395
|
+
export function serializeSousCache(cache) {
|
|
396
|
+
return JSON.stringify(Object.fromEntries([...cache].sort(([a], [b]) => a.localeCompare(b))), null, 2) + '\n';
|
|
397
|
+
}
|
|
398
|
+
/** Désigne le maillon dominant : le plus lourd en passes, si au moins MAILLON_MIN_PASSES sont sous-classées et qu'il pèse MAILLON_MIN_PART % d'elles ; ni égalité en tête ni `autre` ne désignent (R2). */
|
|
399
|
+
export function dominantMaillon(parSousCause) {
|
|
400
|
+
const total = SOUS_CAUSES.reduce((n, s) => n + parSousCause[s], 0);
|
|
401
|
+
if (total < MAILLON_MIN_PASSES)
|
|
402
|
+
return { nom: null, raison: `${total} passes sous-classées, il en faut au moins ${MAILLON_MIN_PASSES}` };
|
|
403
|
+
const poids = MAILLONS.map((m) => ({ m, passes: SOUS_CAUSES.filter((s) => MAILLON_DE[s] === m).reduce((n, s) => n + parSousCause[s], 0) })).sort((a, b) => b.passes - a.passes);
|
|
404
|
+
const [first, second] = poids;
|
|
405
|
+
if (first.passes > 0 && first.passes === second.passes)
|
|
406
|
+
return { nom: null, raison: `égalité en tête entre ${first.m} et ${second.m} (${first.passes} passes chacun)` };
|
|
407
|
+
if (first.passes * 100 < MAILLON_MIN_PART * total)
|
|
408
|
+
return { nom: null, raison: `aucun maillon à ${MAILLON_MIN_PART} % des passes sous-classées (le premier : ${first.passes} sur ${total})` };
|
|
409
|
+
return { nom: first.m, raison: `${first.passes} passes sur ${total} sous-classées (${Math.round((100 * first.passes) / total)} %)` };
|
|
410
|
+
}
|
|
411
|
+
/** Compte les sous-causes des passes bug-code données (R4 : manual prime ; R12 : une entrée du cache hors de ces passes n'est pas lue). Rend null sans passe bug-code (R9). */
|
|
412
|
+
export function bugCodeOf(passes, sousCache) {
|
|
413
|
+
if (passes.length === 0)
|
|
414
|
+
return null;
|
|
415
|
+
const parSousCause = Object.fromEntries(SOUS_CAUSES.map((s) => [s, 0]));
|
|
416
|
+
let classees = 0;
|
|
417
|
+
for (const p of passes) {
|
|
418
|
+
const c = sousCache.get(p.key);
|
|
419
|
+
if (!c)
|
|
420
|
+
continue;
|
|
421
|
+
parSousCause[c.manual ?? c.sous]++;
|
|
422
|
+
classees++;
|
|
423
|
+
}
|
|
424
|
+
return { classees, sansSousCause: passes.length - classees, parSousCause, maillon: dominantMaillon(parSousCause) };
|
|
425
|
+
}
|
|
426
|
+
/** Les sous-causes dans l'ordre décroissant des passes (à égalité, l'ordre de SOUS_CAUSES), celles sans passe écartées. */
|
|
427
|
+
export function sousCausesRanked(b) {
|
|
428
|
+
return SOUS_CAUSES.map((sous, i) => ({ sous, label: SOUS_LABELS[sous], passes: b.parSousCause[sous], i })).filter((x) => x.passes > 0).sort((a, b2) => b2.passes - a.passes || a.i - b2.i);
|
|
429
|
+
}
|
|
430
|
+
/** La section « Bug de code par sous-cause » de `cadence corrections` (A1) : sous-cause, passes, part des passes sous-classées, maillon. */
|
|
431
|
+
export function renderSousCauses(b) {
|
|
432
|
+
const out = ['### Bug de code par sous-cause', '', `${b.classees} passes sous-classées sur ${b.classees + b.sansSousCause}${b.sansSousCause > 0 ? ` · sans sous-cause : ${b.sansSousCause} (à classer : cadence corrections --classify)` : ''}.`, ''];
|
|
433
|
+
if (b.classees > 0)
|
|
434
|
+
out.push('| Sous-cause | Passes | Part | Maillon |', '|---|---|---|---|', ...sousCausesRanked(b).map((r) => `| ${r.label} | ${r.passes} | ${pct(r.passes, b.classees)} | ${MAILLON_DE[r.sous] ?? 'aucun'} |`), '');
|
|
435
|
+
out.push(`Maillon dominant : ${b.maillon.nom ?? `aucun (${b.maillon.raison})`}${b.maillon.nom ? ` — ${b.maillon.raison}` : ''}.`, '');
|
|
436
|
+
return out;
|
|
437
|
+
}
|
|
277
438
|
const pct = (a, b) => (b === 0 ? '—' : `${Math.round((100 * a) / b)} %`);
|
|
278
439
|
/** Les causes qu'une meilleure spécification évite (critère de réussite de l'étape spec). */
|
|
279
440
|
const SPEC_CAUSES = ['spec-floue', 'regle-inventee', 'test-manquant'];
|
|
280
441
|
const row = (name, t) => `| ${name} | ${t.lots} | ${t.atLeast1} (${pct(t.atLeast1, t.lots)}) | ${t.atLeast2} (${pct(t.atLeast2, t.lots)}) | ${t.passes} |`;
|
|
281
|
-
export function renderReport(m) {
|
|
442
|
+
export function renderReport(m, bugCode) {
|
|
282
443
|
const all = [...CAUSES, 'sans-constat', 'non-classe'];
|
|
283
444
|
const used = all.filter((c) => (c !== 'non-classe' && c !== 'sans-constat') || m.byCause[c] > 0);
|
|
284
445
|
const projects = Object.keys(m.byProject).sort();
|
|
@@ -299,5 +460,7 @@ export function renderReport(m) {
|
|
|
299
460
|
out.push('');
|
|
300
461
|
if (m.agreement.reviewed > 0)
|
|
301
462
|
out.push(`Échantillon relu à la main : ${m.agreement.reviewed} passes, le modèle local est d'accord avec la relecture sur ${m.agreement.agree} (${pct(m.agreement.agree, m.agreement.reviewed)}) ; la cause relue prime dans les tableaux.`, '');
|
|
463
|
+
if (bugCode)
|
|
464
|
+
out.push(...renderSousCauses(bugCode));
|
|
302
465
|
return out.join('\n');
|
|
303
466
|
}
|
package/dist/lead.js
CHANGED
|
@@ -16,11 +16,11 @@ import { cadenceHome } from './orchestrate/registry.js';
|
|
|
16
16
|
import { staleNotes, staleText } from './stale-notes.js';
|
|
17
17
|
import { noTestGate, noTestText } from './orchestrate/no-test.js';
|
|
18
18
|
import { KEPT_TITLE, keptLine, keptTrees } from './orchestrate/worktree.js';
|
|
19
|
-
/**
|
|
20
|
-
|
|
19
|
+
/** Le bloc `lead:` du cadence.yaml du dossier parent (absent = `undefined`) ; un `lead` qui n'est pas une table est refusé en nommant `key`. */
|
|
20
|
+
function readLeadBlock(parent, key, expected) {
|
|
21
21
|
const file = join(parent, 'cadence.yaml');
|
|
22
22
|
if (!existsSync(file))
|
|
23
|
-
return
|
|
23
|
+
return undefined;
|
|
24
24
|
let raw;
|
|
25
25
|
try {
|
|
26
26
|
raw = parse(readFileSync(file, 'utf8'));
|
|
@@ -30,16 +30,33 @@ export function readLeadUx(parent) {
|
|
|
30
30
|
}
|
|
31
31
|
const l = raw?.lead;
|
|
32
32
|
if (l == null)
|
|
33
|
-
return
|
|
34
|
-
const ux = l.ux;
|
|
33
|
+
return undefined;
|
|
35
34
|
if (typeof l !== 'object' || Array.isArray(l))
|
|
36
|
-
throw new RafError(`${file} :
|
|
35
|
+
throw new RafError(`${file} : ${key} doit être ${expected}`);
|
|
36
|
+
return { file, block: l };
|
|
37
|
+
}
|
|
38
|
+
/** Clé `lead.ux` du cadence.yaml du dossier parent (L197) : qui pose le verdict UX (`raf ux`) d'un lot visible. `human` (défaut) : l'humain ; `lead` : le lead, l'humain n'étant consulté qu'en cas de doute. */
|
|
39
|
+
export function readLeadUx(parent) {
|
|
40
|
+
const found = readLeadBlock(parent, 'lead.ux', '« lead » ou « human »');
|
|
41
|
+
const ux = found?.block.ux;
|
|
37
42
|
if (ux == null)
|
|
38
43
|
return 'human';
|
|
39
44
|
if (ux !== 'lead' && ux !== 'human')
|
|
40
|
-
throw new RafError(`${file} : lead.ux doit être « lead » ou « human »`);
|
|
45
|
+
throw new RafError(`${found.file} : lead.ux doit être « lead » ou « human »`);
|
|
41
46
|
return ux;
|
|
42
47
|
}
|
|
48
|
+
/** Seuil de contexte du lead quand `lead.contextThreshold` n'est pas posé (L437). */
|
|
49
|
+
export const CONTEXT_THRESHOLD_DEFAULT = 150_000;
|
|
50
|
+
/** Clé `lead.contextThreshold` du cadence.yaml du dossier (L437) : le nombre de tokens de contexte à partir duquel le lead ne lance plus de vague. En tokens et non en % : 60 % d'une fenêtre de 1M valait 600k. Un entier ≥ 1, sinon refus. */
|
|
51
|
+
export function readLeadContextThreshold(parent) {
|
|
52
|
+
const found = readLeadBlock(parent, 'lead.contextThreshold', 'un entier ≥ 1 (tokens)');
|
|
53
|
+
const t = found?.block.contextThreshold;
|
|
54
|
+
if (t == null)
|
|
55
|
+
return CONTEXT_THRESHOLD_DEFAULT;
|
|
56
|
+
if (typeof t !== 'number' || !Number.isInteger(t) || t < 1)
|
|
57
|
+
throw new RafError(`${found.file} : lead.contextThreshold doit être un entier ≥ 1 (tokens), pas ${JSON.stringify(t)}`);
|
|
58
|
+
return t;
|
|
59
|
+
}
|
|
43
60
|
/** Clé `depth:` du cadence.yaml du dossier parent (L201, issue #8), à côté de `priority:` : combien de niveaux `lead tour` parcourt (1 : sous-dossiers directs, le défaut). */
|
|
44
61
|
export function readDepth(parent) {
|
|
45
62
|
const file = join(parent, 'cadence.yaml');
|
|
@@ -6,7 +6,7 @@ import { delimiter, dirname, isAbsolute, join, posix, relative, resolve } from '
|
|
|
6
6
|
import { copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { tmpdir } from 'node:os';
|
|
8
8
|
import { coveredTasks, lotCommits, lotWork } from '../audit.js';
|
|
9
|
-
import { readDocsConfig, readPlanConfig } from '../config.js';
|
|
9
|
+
import { planLoader, readDocsConfig, readPlanConfig } from '../config.js';
|
|
10
10
|
import { PLAN_ACTIONS, leadAnswerMessage, planCommitMessage } from '../plan-commit.js';
|
|
11
11
|
import { articleRepoDir, articlesBrief, docSyncBrief, docSyncGaps, filesOf, gapMessage } from '../docsync.js';
|
|
12
12
|
import { testGateMissing } from '../testgate.js';
|
|
@@ -2318,15 +2318,42 @@ async function work(c, kind) {
|
|
|
2318
2318
|
const noCommitNote = (l) => (l.warnings.some((w) => w.startsWith('passe des mineurs sans commit')) ? ' sans commit' : '');
|
|
2319
2319
|
/** Revue de code qui clôt le cycle : complète, ou courte une fois la passe des mineurs jouée. */
|
|
2320
2320
|
const codeReview = (l) => (l.minorPass ? 'review-small' : 'review');
|
|
2321
|
+
/** Note d'un lot que sa session a rendu `visible` après le lancement de la vague (L454) : son verdict UX n'existe pas encore, le lead le sait. */
|
|
2322
|
+
const VISIBLE_ADDED = 'visible ajouté en cours de vague, étape UX à jouer';
|
|
2323
|
+
/**
|
|
2324
|
+
* Relit `visible` dans le plan du worktree après une session d'écriture (L454) : un lot que sa session a rendu visible (warhammer L37 et L43,
|
|
2325
|
+
* 30-10) est lancé non visible, et sortait « prêt » sans étape UX. Il le devient dans l'état du lot, donc l'UX est jouée (`uxAfterReview`, ou la
|
|
2326
|
+
* revue courte d'un petit lot) ; sans application déclarée, il est rendu avec la note. Un plan illisible, ou un lot qui n'est plus visible
|
|
2327
|
+
* dans le plan, laisse l'état tel quel (retirer `visible` en cours de vague n'est pas traité).
|
|
2328
|
+
*/
|
|
2329
|
+
function addedVisible(c) {
|
|
2330
|
+
const l = c.lot;
|
|
2331
|
+
if (l.visible)
|
|
2332
|
+
return;
|
|
2333
|
+
let now;
|
|
2334
|
+
try {
|
|
2335
|
+
now = (wd(c) === l.repo ? c.loadPlan() : planLoader(wd(c))()).lot(l.lot).visible;
|
|
2336
|
+
}
|
|
2337
|
+
catch {
|
|
2338
|
+
return;
|
|
2339
|
+
}
|
|
2340
|
+
if (!now)
|
|
2341
|
+
return;
|
|
2342
|
+
l.visible = true;
|
|
2343
|
+
if (!c.config.ux)
|
|
2344
|
+
l.uxNote = `${VISIBLE_ADDED} (aucune application déclarée : cadence.yaml orchestrate.ux)`;
|
|
2345
|
+
c.wave.log(`${lotKey(l.project, l.lot)} · ${VISIBLE_ADDED}`);
|
|
2346
|
+
}
|
|
2321
2347
|
/**
|
|
2322
2348
|
* 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,
|
|
2323
2349
|
* après la dernière revue de code conforme (`uxAfterReview`), sur le code qui sera livré.
|
|
2324
2350
|
*/
|
|
2325
2351
|
function firstReview(c) {
|
|
2326
2352
|
const l = c.lot;
|
|
2353
|
+
addedVisible(c);
|
|
2327
2354
|
if (l.small)
|
|
2328
2355
|
return l.visible || l.minorPass ? 'review-small' : 'review'; // passe unique code + ergonomie ; sans écran, une revue de code
|
|
2329
|
-
if (l.visible && !c.config.ux)
|
|
2356
|
+
if (l.visible && !c.config.ux && !l.uxNote?.startsWith(VISIBLE_ADDED))
|
|
2330
2357
|
l.uxNote = 'UX à faire par le lead (aucune application déclarée : cadence.yaml orchestrate.ux)';
|
|
2331
2358
|
return codeReview(l);
|
|
2332
2359
|
}
|
package/dist/perf.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { join, relative, sep } from 'node:path';
|
|
2
2
|
import { planLoader } from './config.js';
|
|
3
|
-
import { CAUSES, isReview as isReviewKind } from './corrections.js';
|
|
3
|
+
import { bugCodeOf, CAUSES, isReview as isReviewKind, sousCausesRanked } from './corrections.js';
|
|
4
4
|
import { addDays, diffDays, isDay, toDay } from './dates.js';
|
|
5
5
|
import { gitCommonDir } from './git.js';
|
|
6
6
|
import { findProjects } from './lead.js';
|
|
@@ -129,8 +129,9 @@ function indicator(current, previous, lots, windows, dir, twin, nullNote = null,
|
|
|
129
129
|
const byEstimateKey = (a, b) => parseFloat(a) - parseFloat(b) || a.length - b.length;
|
|
130
130
|
/** Les deux valeurs d'un indicateur, calculées fenêtre par fenêtre. */
|
|
131
131
|
const both = (windows, f) => ({ current: f(windows.current), previous: f(windows.previous) });
|
|
132
|
-
function causeWindow(passes, w, cache) {
|
|
132
|
+
function causeWindow(passes, w, cache, sousCache) {
|
|
133
133
|
const counts = new Map();
|
|
134
|
+
const bugCodePasses = [];
|
|
134
135
|
let unclassified = 0;
|
|
135
136
|
let withoutFindings = 0;
|
|
136
137
|
const byReview = {};
|
|
@@ -150,14 +151,17 @@ function causeWindow(passes, w, cache) {
|
|
|
150
151
|
}
|
|
151
152
|
const cause = c.manual ?? c.cause;
|
|
152
153
|
counts.set(cause, (counts.get(cause) ?? 0) + 1);
|
|
154
|
+
if (cause === 'bug-code')
|
|
155
|
+
bugCodePasses.push(pass);
|
|
153
156
|
}
|
|
157
|
+
const bugCode = sousCache ? bugCodeOf(bugCodePasses, sousCache) : null;
|
|
154
158
|
const classified = [...counts.values()].reduce((a, b) => a + b, 0);
|
|
155
159
|
const top = CAUSES.filter((c) => counts.has(c))
|
|
156
160
|
.map((cause) => ({ cause, passes: counts.get(cause), share: counts.get(cause) / classified }))
|
|
157
161
|
.sort((a, b) => b.passes - a.passes)
|
|
158
162
|
.slice(0, 3);
|
|
159
163
|
const withFindings = classified + unclassified;
|
|
160
|
-
return { top, classified, unclassified, withoutFindings, unclassifiedShare: withFindings === 0 ? null : unclassified / withFindings, byReview };
|
|
164
|
+
return { top, classified, unclassified, withoutFindings, unclassifiedShare: withFindings === 0 ? null : unclassified / withFindings, byReview, ...(bugCode ? { bugCode } : {}) };
|
|
161
165
|
}
|
|
162
166
|
/** La sorte d'étape sous laquelle la hausse se compte : `review` et `review-small` ne font qu'une (la taille du lot choisit l'une ou l'autre, `orchestrate.review.threshold` : le passage de l'une à l'autre n'est pas une hausse). */
|
|
163
167
|
const carrierKind = (kind) => (kind === 'review-small' ? 'review' : kind);
|
|
@@ -222,7 +226,7 @@ export function computePerf(input) {
|
|
|
222
226
|
};
|
|
223
227
|
const lotCounts = both(windows, (w) => lotsIn(w).length);
|
|
224
228
|
const passDays = withPasses.flatMap((d) => d.j.passes.map((pass) => ({ pass, day: pass.fixEnded && !Number.isNaN(Date.parse(pass.fixEnded)) ? toDay(new Date(pass.fixEnded)) : d.day })));
|
|
225
|
-
const causes = { current: causeWindow(passDays, windows.current, cache), previous: causeWindow(passDays, windows.previous, cache) };
|
|
229
|
+
const causes = { current: causeWindow(passDays, windows.current, cache, input.sousCache), previous: causeWindow(passDays, windows.previous, cache) };
|
|
226
230
|
// Interventions du lead (R10, R15)
|
|
227
231
|
const lastOf = new Map();
|
|
228
232
|
for (const d of all) {
|
|
@@ -630,6 +634,14 @@ function redBreakdownLines(b) {
|
|
|
630
634
|
` livraisons rouges par projet : ${byProject.map(([name, n, cl]) => `${name} ${n} (${cl.map(([k, c]) => `${k} ${c}`).join(', ')})`).join(' · ')}`,
|
|
631
635
|
];
|
|
632
636
|
}
|
|
637
|
+
/** La ligne des sous-causes bug-code sous « causes dominantes » (L447, R6/R7) : sans sous-cause classée, elle dit comment les classer. */
|
|
638
|
+
function bugCodeLine(b) {
|
|
639
|
+
if (b.classees === 0)
|
|
640
|
+
return ` bug de code (${b.sansSousCause} passes) : sous-causes n/d · à classer : cadence corrections --classify`;
|
|
641
|
+
const ranked = sousCausesRanked(b).map((r, i) => `${i + 1}. ${r.label} ${r.passes}`).join(' · ');
|
|
642
|
+
const maillon = b.maillon.nom ?? `aucun (${b.maillon.raison})`;
|
|
643
|
+
return ` bug de code (${b.classees} passes sous-classées sur ${b.classees + b.sansSousCause}) : ${ranked}${b.sansSousCause > 0 ? ` · sans sous-cause : ${b.sansSousCause}` : ''} · maillon dominant : ${maillon}`;
|
|
644
|
+
}
|
|
633
645
|
/** Le tableau en texte : chaque indicateur de vitesse sur la même ligne que son jumeau de qualité (R8). */
|
|
634
646
|
export function renderPerf(r) {
|
|
635
647
|
const lines = indicatorLines(r);
|
|
@@ -669,6 +681,8 @@ export function renderPerf(r) {
|
|
|
669
681
|
const c = r.corrections.causes.current;
|
|
670
682
|
const top = c.top.map((t, i) => `${i + 1}. ${LABELS[t.cause]} ${t.passes} (${pct(t.share)})`).join(' · ');
|
|
671
683
|
out.push(` causes dominantes (${c.classified} passes classées) : ${top || 'aucune'} · non classé : ${c.unclassifiedShare === null ? 'n/d' : pct(c.unclassifiedShare)} · sans revue (tests rouges) : ${c.withoutFindings}${c.unclassified > 0 ? ' · à classer : cadence corrections --classify' : ''}`);
|
|
684
|
+
if (c.bugCode)
|
|
685
|
+
out.push(bugCodeLine(c.bugCode));
|
|
672
686
|
const origins = Object.entries(c.byReview).sort(([a, x], [b, y]) => y - x || a.localeCompare(b));
|
|
673
687
|
if (origins.length > 0)
|
|
674
688
|
out.push(` origine des passes à constats : ${origins.map(([k, n]) => `${k} ${n}`).join(' · ')}`);
|
package/dist/priority.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, join, relative, sep } from 'node:path';
|
|
3
|
+
import { Document, isScalar, isSeq, parseDocument } from 'yaml';
|
|
4
|
+
import { findProjects, readDepth } from './lead.js';
|
|
5
|
+
import { priorityRank, readPriority } from './orchestrate/continue.js';
|
|
6
|
+
import { RafError } from './plan.js';
|
|
7
|
+
import { setKey } from './config-set.js';
|
|
8
|
+
/**
|
|
9
|
+
* `cadence lead priority <projet> --before|--after <projet>` (L449) : place `project` juste avant ou après `target` dans l'ordre des projets du dossier,
|
|
10
|
+
* en réécrivant la liste `priority:` de son cadence.yaml (créé s'il manque). Les projets sont nommés comme `lead tour` les nomme.
|
|
11
|
+
* La liste écrite nomme tous les projets du dossier, dans l'ordre affiché : un nom qui en couvrait plusieurs par préfixe (`maritime` pour `maritime-atlas`) devient explicite
|
|
12
|
+
* (le commentaire de sa ligne passe sur le premier projet qu'il couvrait), et un nom qui ne couvre aucun projet reste en fin de liste. Le reste du fichier, commentaires compris, est gardé (setKey). Un fichier illisible, ou une clé qui n'est pas une liste, est refusé sans rien écrire.
|
|
13
|
+
*/
|
|
14
|
+
export function movePriority(parent, project, place, target, depth) {
|
|
15
|
+
const file = join(parent, 'cadence.yaml');
|
|
16
|
+
const declared = readPriority(parent); // refuse un yaml illisible ou une clé qui n'est pas une liste de noms
|
|
17
|
+
const projects = findProjects(parent, depth ?? readDepth(parent)).map((dir) => ({ key: basename(dir), name: relative(parent, dir).split(sep).join('/') }));
|
|
18
|
+
const byName = new Map(projects.map((p) => [p.name, p]));
|
|
19
|
+
for (const n of [project, target])
|
|
20
|
+
if (!byName.has(n))
|
|
21
|
+
throw new RafError(`projet inconnu : ${n} (projets de ${parent} : ${projects.map((p) => p.name).join(', ') || 'aucun'})`);
|
|
22
|
+
if (project === target)
|
|
23
|
+
throw new RafError(`${project} : le même projet des deux côtés`);
|
|
24
|
+
const keys = projects.map((p) => p.key);
|
|
25
|
+
const twice = keys.find((k, i) => keys.indexOf(k) !== i);
|
|
26
|
+
if (twice)
|
|
27
|
+
throw new RafError(`deux projets s'appellent ${twice} : la liste priority: les nomme par leur dossier et ne peut pas les distinguer`);
|
|
28
|
+
const ranked = [...projects].sort((a, b) => priorityRank(a.key, declared) - priorityRank(b.key, declared) || a.key.localeCompare(b.key) || a.name.localeCompare(b.name));
|
|
29
|
+
const without = ranked.filter((p) => p.name !== project);
|
|
30
|
+
const at = without.findIndex((p) => p.name === target) + (place === 'after' ? 1 : 0);
|
|
31
|
+
const moved = [...without.slice(0, at), byName.get(project), ...without.slice(at)];
|
|
32
|
+
const order = moved.map((p) => p.name);
|
|
33
|
+
if (order.every((n, i) => n === ranked[i].name))
|
|
34
|
+
return { order, changed: false };
|
|
35
|
+
const unmatched = declared.filter((n) => !keys.some((k) => k === n || k.startsWith(`${n}-`)));
|
|
36
|
+
const list = [...moved.map((p) => p.key), ...unmatched];
|
|
37
|
+
// la liste écrite doit restituer l'ordre voulu : un nom qui en préfixe un autre (`shop` et `shop-web`) les rangerait ensemble
|
|
38
|
+
const back = [...projects].sort((a, b) => priorityRank(a.key, list) - priorityRank(b.key, list) || a.key.localeCompare(b.key) || a.name.localeCompare(b.name)).map((p) => p.name);
|
|
39
|
+
if (back.some((n, i) => n !== order[i])) {
|
|
40
|
+
const clash = keys.find((k) => keys.some((o) => o !== k && o.startsWith(`${k}-`)));
|
|
41
|
+
throw new RafError(`${project} ne peut pas être placé ${place === 'before' ? 'avant' : 'après'} ${target} : la liste priority: nomme un projet par son dossier, et ${clash ?? 'un nom'} est le début du nom d'un autre projet (« ${clash ?? 'x'}- »), qui le suit toujours — rien n'est écrit`);
|
|
42
|
+
}
|
|
43
|
+
const text = existsSync(file) ? readFileSync(file, 'utf8') : '';
|
|
44
|
+
const old = parseDocument(text).get('priority', true);
|
|
45
|
+
// les noms déjà dans la liste gardent leur nœud, donc le commentaire de leur ligne ; un nom qui couvrait des projets par préfixe (`maritime`)
|
|
46
|
+
// donne le sien au premier projet qu'il couvre, dans l'ordre écrit, sous le nom explicite ; les autres sont des nœuds neufs
|
|
47
|
+
const kept = new Map();
|
|
48
|
+
const scalars = isSeq(old) ? old.items.filter((item) => isScalar(item) && typeof item.value === 'string') : [];
|
|
49
|
+
for (const item of scalars)
|
|
50
|
+
if (!kept.has(item.value.trim()))
|
|
51
|
+
kept.set(item.value.trim(), item); // deux fois le même nom : le premier nœud compte, comme pour le rang
|
|
52
|
+
for (const item of scalars) {
|
|
53
|
+
const n = item.value.trim();
|
|
54
|
+
if (kept.get(n) !== item || keys.includes(n))
|
|
55
|
+
continue; // un nom qui est déjà un projet garde son nœud et n'en donne pas une copie à `nom-…`
|
|
56
|
+
const covered = list.find((k) => !kept.has(k) && k.startsWith(`${n}-`));
|
|
57
|
+
if (covered === undefined)
|
|
58
|
+
continue;
|
|
59
|
+
const explicit = item.clone();
|
|
60
|
+
explicit.value = covered;
|
|
61
|
+
kept.set(covered, explicit);
|
|
62
|
+
}
|
|
63
|
+
const value = new Document(list);
|
|
64
|
+
if (isSeq(value.contents)) {
|
|
65
|
+
value.contents.items = list.map((n) => kept.get(n) ?? value.createNode(n));
|
|
66
|
+
value.contents.flow = isSeq(old) && old.flow === true; // la liste en ligne reste en ligne
|
|
67
|
+
// la séquence porte le commentaire de la ligne de la clé et celui écrit avant son premier nom : la nouvelle les reprend
|
|
68
|
+
if (isSeq(old)) {
|
|
69
|
+
value.contents.commentBefore = old.commentBefore;
|
|
70
|
+
value.contents.comment = old.comment;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const next = setKey(text, 'priority', value);
|
|
74
|
+
if (next !== null)
|
|
75
|
+
writeFileSync(file, next);
|
|
76
|
+
return { order, changed: next !== null };
|
|
77
|
+
}
|
package/dist/verify-gate.js
CHANGED
|
@@ -127,7 +127,8 @@ async function playGate(ctx) {
|
|
|
127
127
|
}
|
|
128
128
|
let recorded = { reason: 'aucune porte de vague enregistrée pour ce sha' };
|
|
129
129
|
try {
|
|
130
|
-
|
|
130
|
+
// Un test qui lit le plan (orchestrate.gate.readsPlan, L450) voit un autre contenu après un `raf done` : les commits de plan seuls ne sont pas traversés.
|
|
131
|
+
recorded = recordedGate(ctx.launchDirs ?? [], repo, sha, steps, config.gate ? undefined : ctx.planFile);
|
|
131
132
|
}
|
|
132
133
|
catch (e) {
|
|
133
134
|
recorded = { reason: `état des vagues illisible (${gitFailure(e)})` };
|
|
@@ -143,7 +144,7 @@ async function playGate(ctx) {
|
|
|
143
144
|
out(`verify --gate ${short} : vert`);
|
|
144
145
|
return 0;
|
|
145
146
|
}
|
|
146
|
-
const replayReason = `verdict rejoué : ${recorded.reason}`;
|
|
147
|
+
const replayReason = `verdict rejoué : ${recorded.reason}${config.gate && ctx.planFile ? ' (orchestrate.gate.readsPlan : un test lit le plan, les commits de plan seuls ne reprennent pas le verdict d\'un sha dessous)' : ''}`;
|
|
147
148
|
const obstacle = mainTreeObstacle(repo, sha);
|
|
148
149
|
const mode = ctx.verify?.inPlace;
|
|
149
150
|
if (mode === true && obstacle) {
|
package/docs/cadence-hud.md
CHANGED
|
@@ -81,6 +81,8 @@ A project shows at most 5 lots (L411), the first ones in the order above (so tho
|
|
|
81
81
|
|
|
82
82
|
**Reordering (L402).** Drag a lot onto another lot of the same project and of the same display group (or a sub-task onto another sub-task of its lot, same group): press on the row, move with the button held, release. While you drag, the row you started from shows `●` and the row under the pointer shows where it will land, `⇡` (before it) or `⇣` (after it), or a red `⊘` when it belongs to another parent or to another display group (L410) and the drop will be refused. The keyboard does the same wherever the panel is drawn by a `Client` region (terminal and desktop app): click a row (or press ↑ / ↓ once the panel has the focus), then **Alt**+↑ / **Alt**+↓ moves the selected lot before its previous sibling / after its next one. Only the rows the panel shows take part (L411): a lot hidden behind `+ N lots`, or the sub-lots of a closed lot, are not rows, so they are never a target — Alt+↓ on the fifth lot, the last row before the `+ N lots` button, moves nothing, like Alt+↓ on the last row of a project; open the rest (or the lot) first, and the drop runs `raf move` as usual. The `+ N lots` / `moins` button sits outside the `Client` region, after the project's rows. The drop runs `raf move <id> --before|--after <id>` ([raf move](raf.md)) in the project folder, then the panel reads the plans again and says, under the title, `✔ L13 placé avant L10 — le plan est modifié, à commiter (raf move ne commite pas)`: the panel never commits, the plan file is yours to commit. Everything is relative to what the panel shows, in the L410 order, not to the order of the plan: the row you drop on, and the "previous" and "next" sibling of Alt+↑ / Alt+↓, are the ones displayed around the lot, and `raf move` is given that lot as its target. **A lot is reordered only inside its display group** (L410: in progress, free, blocked): dropping a lot on a lot of another group is refused, `raf` is not called and nothing moves. While you drag, the row under the pointer shows the red `⊘` (as for another parent), and on release the panel says why, by group: `✖ L411 est en cours : un lot libre se range parmi les libres` (the words: `en cours` / `libre` / `bloqué`; the lot is named by what it is, `un lot` or `une sous-tâche`). A parent with an in-progress sub-lot is shown with the lots in progress, so it counts as one of them. Alt+↑ / Alt+↓ toward a displayed neighbour of another group gets the same refusal; at the edge of the displayed rows (or before the `+ N lots` button) nothing happens. (Case that led to this: plan L402 L403 L407 L411, L403 free dropped on L411 in progress moved L403 below L407 and hid it behind `+ 11 lots`.) Inside a group the screen follows the plan, so a gesture that goes through always shows: the lot moves where you dropped it. A drop that would leave the order of the group as it is — the lot is already right before (or after) the target among the lots of its group — runs nothing and says `• L10 est déjà avant L13 dans le plan : rien à déplacer`; no gesture of the panel leads there (the screen follows the plan inside a group), the check guards a request the drawing did not make. The same holds between the sub-tasks of a lot. The `déjà` check is made on the table the panel last read: from a successful `raf move` until the end of a re-reading started *after* it (`cadence lead tour` takes several seconds; a reading already in flight read the old plan, so another one follows it) the table is held stale: no `déjà` short-circuit, `raf move` runs and decides (the group refusal stays: it depends on the states, not on the order). A refusal is written under the title with its reason and nothing moves: a wave holds the repository (`move : orchestration en cours …`), a plan kept by another tool (`plan en lecture seule : … — le modifier avec l'outil du projet`, the message does not name that tool's command: `raf` does not know it, so look in the project), an archived lot, different parents (`L402 et L401/2 n'ont pas le même parent …`) or different display groups (`L411 est en cours : …`), the last two checked before `raf` runs; `raf` missing from the `PATH` says so. On a surface with no `Client` region (mobile, editor) the panel stays read-only (`lecture seule` in its title).
|
|
83
83
|
|
|
84
|
+
**Reordering the projects (L449).** Above the tree, `ordre des projets (priority:) — glisser, ou Alt+↑ / Alt+↓` and one row per project (`cadence 7 lots non clos`; `illisible` for a project the tour could not read), in the order of `priority:`. It is drawn only where the lots can be reordered (a `Client` region: terminal and desktop app), when the folder has two projects or more, and when the panel is tall enough to keep a row for each project and one lot under it (otherwise the tree has the room). Drag a project onto another (`●` the row you hold, `⇡` / `⇣` where it will land) or click it and press **Alt**+↑ / **Alt**+↓ (↑ / ↓ alone select): the drop runs `cadence lead priority <project> --before|--after <project> <folder>` ([lead priority](lead.md#lead-priority)), which rewrites the `priority:` list of the `cadence.yaml` of the parent folder, comments kept; the panel then reads the plans again and says under the title `✔ ol placé avant cadence — priority: du cadence.yaml est modifiée, à commiter`. This order is the one of the tour, of `cadence orchestrate --continue` and of the panel itself. The panel never commits. A refusal is written under the title with its reason (`✖ /dev/cadence.yaml illisible : …`, a `priority:` that is not a list) and nothing is written; `cadence` missing from the `PATH` says so; a project already at that place says `• … est déjà avant …`. The error row `cadence.yaml` that the tour adds for an unreadable file is not a project and cannot be moved or targeted.
|
|
85
|
+
|
|
84
86
|
States: `▶ en cours` (doing), `○ libre` (todo, no open `after:`), `⊘ bloqué` (todo, an `after:` is still open); a lot in a wave takes the wave's state and words, as the band writes them (`… en attente`, `⟳ implémente`, `⟳ relit`, `⟳ corrige`, `? QUESTION`, `✔ prêt`, `↩ rendu`, `✖ échec`, `⏸ suspendu`). In a native plan the sub-tasks of an open lot (`L401/2`) are its sub-lots (`├`/`└`); they only read `▶ en cours` or `○ à faire`, and are not counted in the project's `N lots non clos`. In a plan kept by another tool the sub-lots come from `parent:`. An `after:` id that is closed or unknown is struck through and blocks nothing. A project the tour could not read shows `illisible : <error>`; the others stay.
|
|
85
87
|
|
|
86
88
|
Before the first reading the panel says `Lecture des plans…`; if `cadence lead tour` does not answer it says `Lots illisibles : cadence lead tour n'a pas répondu. Nouvelle lecture dans une minute.` (a failed re-reading keeps the last table); a `cadence` too old to give the `lots` field says `Lots indisponibles : cadence est trop ancien, mettez-le à jour.`; with no open lot, `Aucun lot non clos dans les projets de ce dossier.`. A tree taller than the panel ends with `… +N lots de plus`. Under 60 columns the `after:` list moves to its own line and the title is cut with `…`, never the id or the state.
|
package/docs/corrections.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# corrections
|
|
4
4
|
|
|
5
|
-
`cadence corrections [folder] [--classify [--model m]] [--cache file] [--sample n] [--until date] [--since date] [--json]` measures how often a wave
|
|
5
|
+
`cadence corrections [folder] [--classify [--model m]] [--cache file] [--cache-sous-causes file] [--sample n] [--until date] [--since date] [--json]` measures how often a wave
|
|
6
6
|
has to correct itself. From the folder a wave was launched from (default: the current one) it reads the journals
|
|
7
7
|
`<folder>/.cadence/runs/<wave>/<project>--<lot>.json` of **every project** and prints:
|
|
8
8
|
|
|
@@ -23,3 +23,13 @@ beside a pass in the cache file (one of the seven causes, else the command refus
|
|
|
23
23
|
on the reviewed sample. `--until date` (ISO, e.g. `2026-10-10T18:31Z`) reads only the waves created up to then (`created` in the wave's `wave.json`) and, in them, only the steps ended by then (`ended`), so that neither later waves nor a wave still running at that time change a published baseline; a wave or a step with no readable date is left out. `--since date` (ISO, same format) keeps only the lots of the waves **created after** that date (`created` in the wave's `wave.json`), **with all their passes**: a wave renders every brief from the copy of the tool frozen when it is created (`<wave>/tool/`), so the creation date of the wave, not the start of a lot, tells which brief the lot was given. A `--continue` wave created before a change of brief therefore does not give it to its lots, even those that start after the change (a difference of two unbounded runs is wrong as soon as a wave straddles the date); the exception is a wave resumed (`--resume`) without a snapshot (older than L61) or, with `--budget project:lot=+N`, from a snapshot older than L223: it runs with the current `dist/` and templates, so it does receive the new brief. A wave with no readable `created` is left out, by `--since` as by `--until`. `--since` is strict and `--until` inclusive, so a wave created exactly at the date is on the `--until` side only and the two sets never overlap; `--since` and `--until` can be combined (a window of creation dates). Together `--until d` and `--since d` split the waves, and they add up to the unbounded total only when `d` is set while no wave is running (between two sessions, as `npm run service` requires; `cadence orchestrate --status` shows none, **and** no wave created before `d` is still resumable: `--status` lists only the live waves, an interrupted or suspended one is not shown and `--resume` would replay passes after `d`; list them with `jq -r 'select(.status != "done") | "\(.id) \(.status) \(.created)"' <folder>/.cadence/runs/*/wave.json`, and finish each before setting `d` or never resume it): a wave created before `d` that ends a pass after `d` leaves that pass in neither (`--until` does not read it, so that a published baseline does not move), and a gap between the sum and the unbounded total means `d` fell inside a wave. `--json` prints the measure as data. The command reads the journals and writes nothing but the cache.
|
|
24
24
|
The first baseline is in [docs/mesures/corrections-2026-10-10.md](mesures/corrections-2026-10-10.md); `test/corrections-mesure.test.ts` replays its command on the journals and checks the document's results word for word (skipped when the journals are not there). The measure of the screen-sheet step against the visible lots of this baseline (L176: 8 lots with a sheet, verdict per row of the success table, token cost beside the passes avoided) is in [docs/mesures/etape-spec-2026-10-12.md](mesures/etape-spec-2026-10-12.md); the target of its third row is ≤ 8 %, half the 16 % reference. The measure of the back-end spec step against the non-visible lots (L190: 10 lots with a back-end sheet, passes by cause, token cost beside the passes avoided) is in [docs/mesures/etape-spec-back-2026-10-16.md](mesures/etape-spec-back-2026-10-16.md).
|
|
25
25
|
The reading of the « bug de code » passes of the 23–29-10 window, the block added to the `implement` brief against them (L428) and the protocol to measure its effect on two `cadence perf` windows are in [docs/mesures/bug-code-2026-10-29.md](mesures/bug-code-2026-10-29.md).
|
|
26
|
+
|
|
27
|
+
## Sub-causes of the « bug de code » passes (L447)
|
|
28
|
+
|
|
29
|
+
The cause `bug-code` alone is 69 % of the classified passes (74 of 107, 24→30-10) and does not say *which* bug. A pass whose retained cause (`manual`, else the model's) is `bug-code` and that has review findings is therefore also given **one sub-cause** from a closed list of seven: `cas-limite` (edge case not tested or not held), `test-casse` (an existing test broken by the change), `contrat-mal-lu` (contract, sheet or lot statement misread), `appelant-oublie` (a caller or another site to change was forgotten), `affirmation-fausse` (a document, comment or report that is false), `test-vide` (a test that cannot fail, or a behaviour with no test) and `autre`. A pass of another cause, or with no finding, is never sent to the model for this.
|
|
30
|
+
|
|
31
|
+
- `--classify` first classifies the causes as before, then asks the same local model for the sub-cause of each `bug-code` pass that has none (temperature 0, output constrained to the seven values, findings cut to 1500 characters). The answers are cached in `<folder>/.cadence/corrections-sous-causes.json` (or `--cache-sous-causes file`): an object `pass key → { sous, by, manual? }`. A failed answer (Ollama down, non-2xx status, value outside the list) leaves the pass out of the cache, counted « sans sous-cause » and taken again by the next `--classify`; `corrections.json` is not touched. A `manual` field (one of the seven values) beats the model in every table. An entry whose key is not a `bug-code` pass of the window (re-classified, cause changed by hand, key absent from the window) is ignored: neither counted, erased nor refused.
|
|
32
|
+
- An unreadable file (not a JSON object) or an unknown sub-cause makes the command refuse with exit code 1, naming the file (and the key), without printing or rewriting anything; an absent file reads as empty. Without `--classify` the file is read, never written.
|
|
33
|
+
- The report gains the section « Bug de code par sous-cause » (sub-cause, passes, share of the sub-classified passes, link) and a line « Maillon dominant ». `--json` gains the key `bugCode` — `{ classees, sansSousCause, parSousCause, maillon: { nom, raison } }`. With no `bug-code` pass in the window nothing is added: the text report and the JSON keys are those of before.
|
|
34
|
+
|
|
35
|
+
**The link.** Each sub-cause belongs to one link of the chain: `cas-limite` and `test-vide` to the `implement` brief, `affirmation-fausse` and `appelant-oublie` to the self-check before handing back, `test-casse` to the local gate, `contrat-mal-lu` to the sheet, `autre` to none. The **dominant link** is the one whose sub-causes weigh the most passes; it is named only when at least 10 passes are sub-classified and its passes are at least 40 % of them. With fewer, with a link under 40 %, or with two links tied at the top, the line says « aucun » and why. The two thresholds and the table are constants of `src/corrections.ts` (hypotheses: the link is recomputed at each read, nothing stores it). `cadence perf` prints the same count under « causes dominantes » for the current 7-day window — `bug de code (N passes sous-classées sur M) : 1. cas limite … · maillon dominant : …` — reading the cache without writing it: an absent file gives `sous-causes n/d · à classer : cadence corrections --classify`, an unreadable one a warning on stderr (`perf : <file> illisible, sous-causes n/d`) and no line, exit code 0 in both cases (`corrections.causes.current.bugCode` in `--json`). Over 7 days the 10-pass threshold is often not reached: « aucun » is then the usual answer, not an error.
|
package/docs/deliver.md
CHANGED
|
@@ -308,6 +308,8 @@ verify --gate 6b0d9aa : vert
|
|
|
308
308
|
|
|
309
309
|
Nothing runs: no worktree, no copy of the dependencies, exit 0. Otherwise the gate is **replayed** exactly as described above, and the line right after the one telling where it ran gives the reason: `verdict rejoué : aucune porte de vague enregistrée pour ce sha` (no wave state, another sha such as the head after an integration that rebased, a commit that also touches another file (a plan-only commit is crossed, L444), another repository), `arbre différent de celui contrôlé`, `arbre non enregistré` (state written before L397), or `l'enregistrement (vague …, lot …) ne couvre pas en vert test (…)` (a red, missing or changed step). A recorded red is never repeated as a verdict: it is replayed. The verdict read is the one of the wave's gate (with `orchestrate.testReplay` applied), not a new run: to force a replay, ask for a sha the wave did not check.
|
|
310
310
|
|
|
311
|
+
**A test that reads the plan (L450).** Crossing the plan-only commits assumes that no test reads the plan. On 30-10 avatar-pandora's `plan-page.e2e`, which reads `.cadence/raf.yaml`, went red after the `raf done` of L8 while the head gate, taking over the wave's verdict under that commit, saw nothing. A project whose gate has such a test declares it: `orchestrate.gate: { readsPlan: true }` (or `readsPlan: [e2e/plan-page.e2e.ts]`, the paths of the tests that read it; a non-empty list of relative paths without `..`, anything else is refused when `cadence.yaml` is read). The verdict of a sha *below* plan-only commits is then never taken over: the gate is replayed at the sha asked, and the reason line ends with `(orchestrate.gate.readsPlan : un test lit le plan, …)`. A verdict recorded at the very sha asked is still taken over. Absent or `false`: unchanged. The pre-push hook keeps crossing plan-only commits (its rule, L432, is not changed here).
|
|
312
|
+
|
|
311
313
|
```yaml
|
|
312
314
|
# cadence.yaml
|
|
313
315
|
verify:
|
|
@@ -50,11 +50,11 @@
|
|
|
50
50
|
<text x="920" y="171.5" text-anchor="middle" font-weight="600" font-size="13.5" fill="#e6edf3">Delivers</text>
|
|
51
51
|
<text x="920" y="186.5" text-anchor="middle" font-size="11.5" fill="#8b949e">push + deliver</text>
|
|
52
52
|
<rect x="1000" y="146" width="128" height="58" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1.5"/>
|
|
53
|
-
<text x="1064" y="171.5" text-anchor="middle" font-weight="600" font-size="13.5" fill="#e6edf3"><
|
|
53
|
+
<text x="1064" y="171.5" text-anchor="middle" font-weight="600" font-size="13.5" fill="#e6edf3">< limit ?</text>
|
|
54
54
|
<text x="1064" y="186.5" text-anchor="middle" font-size="11.5" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" fill="#8b949e">session context</text>
|
|
55
55
|
<rect x="1144" y="146" width="128" height="58" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1.5"/>
|
|
56
56
|
<text x="1208" y="171.5" text-anchor="middle" font-weight="600" font-size="13.5" fill="#e6edf3">Close</text>
|
|
57
|
-
<text x="1208" y="186.5" text-anchor="middle" font-size="11.5" fill="#8b949e">if
|
|
57
|
+
<text x="1208" y="186.5" text-anchor="middle" font-size="11.5" fill="#8b949e">if limit reached</text>
|
|
58
58
|
<rect x="280" y="246" width="128" height="58" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1.5"/>
|
|
59
59
|
<text x="344" y="271.5" text-anchor="middle" font-weight="600" font-size="13.5" fill="#e6edf3">Lots</text>
|
|
60
60
|
<text x="344" y="286.5" text-anchor="middle" font-size="11.5" fill="#8b949e">figure 2, per lot</text>
|
|
@@ -50,11 +50,11 @@
|
|
|
50
50
|
<text x="920" y="171.5" text-anchor="middle" font-weight="600" font-size="13.5" fill="#1f2328">Delivers</text>
|
|
51
51
|
<text x="920" y="186.5" text-anchor="middle" font-size="11.5" fill="#57606a">push + deliver</text>
|
|
52
52
|
<rect x="1000" y="146" width="128" height="58" rx="6" fill="#ffffff" stroke="#57606a" stroke-width="1.5"/>
|
|
53
|
-
<text x="1064" y="171.5" text-anchor="middle" font-weight="600" font-size="13.5" fill="#1f2328"><
|
|
53
|
+
<text x="1064" y="171.5" text-anchor="middle" font-weight="600" font-size="13.5" fill="#1f2328">< limit ?</text>
|
|
54
54
|
<text x="1064" y="186.5" text-anchor="middle" font-size="11.5" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" fill="#57606a">session context</text>
|
|
55
55
|
<rect x="1144" y="146" width="128" height="58" rx="6" fill="#ffffff" stroke="#57606a" stroke-width="1.5"/>
|
|
56
56
|
<text x="1208" y="171.5" text-anchor="middle" font-weight="600" font-size="13.5" fill="#1f2328">Close</text>
|
|
57
|
-
<text x="1208" y="186.5" text-anchor="middle" font-size="11.5" fill="#57606a">if
|
|
57
|
+
<text x="1208" y="186.5" text-anchor="middle" font-size="11.5" fill="#57606a">if limit reached</text>
|
|
58
58
|
<rect x="280" y="246" width="128" height="58" rx="6" fill="#ffffff" stroke="#57606a" stroke-width="1.5"/>
|
|
59
59
|
<text x="344" y="271.5" text-anchor="middle" font-weight="600" font-size="13.5" fill="#1f2328">Lots</text>
|
|
60
60
|
<text x="344" y="286.5" text-anchor="middle" font-size="11.5" fill="#57606a">figure 2, per lot</text>
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
<text x="1140" y="703.5" text-anchor="middle">findings → the human</text>
|
|
131
131
|
</g>
|
|
132
132
|
<g id="ctx"><rect x="1241" y="337" width="158" height="66" rx="8" fill="#0c2d6b" stroke="#58a6ff" stroke-width="1.5"/>
|
|
133
|
-
<text x="1320" y="358.5" text-anchor="middle" font-weight="600">Context under
|
|
133
|
+
<text x="1320" y="358.5" text-anchor="middle" font-weight="600">Context under limit?</text>
|
|
134
134
|
<text x="1320" y="374.5" text-anchor="middle" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="11.5">session context</text>
|
|
135
135
|
<text x="1320" y="390.5" text-anchor="middle">error = at threshold</text>
|
|
136
136
|
</g>
|
|
@@ -175,7 +175,7 @@
|
|
|
175
175
|
<polyline points="1320,403 1320,650" fill="none" stroke="#8b949e" stroke-width="1.6" marker-end="url(#arr)"/>
|
|
176
176
|
<text x="1326" y="440" text-anchor="start" font-size="11" fill="#8b949e" class="lbl">yes: chain</text>
|
|
177
177
|
<polyline points="1399,370 1421,370" fill="none" stroke="#8b949e" stroke-width="1.6" marker-end="url(#arr)"/>
|
|
178
|
-
<text x="1410" y="329" text-anchor="middle" font-size="11" fill="#8b949e" class="lbl"
|
|
178
|
+
<text x="1410" y="329" text-anchor="middle" font-size="11" fill="#8b949e" class="lbl">limit reached: close</text>
|
|
179
179
|
<polyline points="1500,337 1500,203" fill="none" stroke="#8b949e" stroke-width="1.6" marker-end="url(#arr)"/>
|
|
180
180
|
<polyline points="1320,716 1320,746 420,746 420,716" fill="none" stroke="#8b949e" stroke-width="1.6" marker-end="url(#arr)"/>
|
|
181
181
|
<text x="870" y="740" text-anchor="middle" font-size="11" fill="#8b949e" class="lbl">next wave: back to step 2, the limits still apply</text>
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
<text x="1140" y="703.5" text-anchor="middle">findings → the human</text>
|
|
131
131
|
</g>
|
|
132
132
|
<g id="ctx"><rect x="1241" y="337" width="158" height="66" rx="8" fill="#ddf4ff" stroke="#0969da" stroke-width="1.5"/>
|
|
133
|
-
<text x="1320" y="358.5" text-anchor="middle" font-weight="600">Context under
|
|
133
|
+
<text x="1320" y="358.5" text-anchor="middle" font-weight="600">Context under limit?</text>
|
|
134
134
|
<text x="1320" y="374.5" text-anchor="middle" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="11.5">session context</text>
|
|
135
135
|
<text x="1320" y="390.5" text-anchor="middle">error = at threshold</text>
|
|
136
136
|
</g>
|
|
@@ -175,7 +175,7 @@
|
|
|
175
175
|
<polyline points="1320,403 1320,650" fill="none" stroke="#57606a" stroke-width="1.6" marker-end="url(#arr)"/>
|
|
176
176
|
<text x="1326" y="440" text-anchor="start" font-size="11" fill="#57606a" class="lbl">yes: chain</text>
|
|
177
177
|
<polyline points="1399,370 1421,370" fill="none" stroke="#57606a" stroke-width="1.6" marker-end="url(#arr)"/>
|
|
178
|
-
<text x="1410" y="329" text-anchor="middle" font-size="11" fill="#57606a" class="lbl"
|
|
178
|
+
<text x="1410" y="329" text-anchor="middle" font-size="11" fill="#57606a" class="lbl">limit reached: close</text>
|
|
179
179
|
<polyline points="1500,337 1500,203" fill="none" stroke="#57606a" stroke-width="1.6" marker-end="url(#arr)"/>
|
|
180
180
|
<polyline points="1320,716 1320,746 420,746 420,716" fill="none" stroke="#57606a" stroke-width="1.6" marker-end="url(#arr)"/>
|
|
181
181
|
<text x="870" y="740" text-anchor="middle" font-size="11" fill="#57606a" class="lbl">next wave: back to step 2, the limits still apply</text>
|