@sylad/cadence 1.33.0 → 1.34.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/dist/config.js +8 -9
- package/dist/corrections.js +4 -5
- package/dist/orchestrate/briefs.js +1 -0
- package/dist/orchestrate/command.js +6 -6
- package/dist/orchestrate/cycle.js +52 -87
- package/dist/orchestrate/precheck.js +79 -0
- package/dist/orchestrate/probe.js +124 -0
- package/dist/orchestrate/schemas.js +10 -1
- package/dist/perf.js +1 -1
- package/dist/yaml-keys.js +6 -0
- package/docs/orchestrate.md +21 -2
- package/docs/whats-new.md +6 -0
- package/package.json +1 -1
- package/templates/orchestrate/probe.md +20 -0
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
{
|
|
9
9
|
"name": "cadence",
|
|
10
10
|
"description": "Session start and close rituals driven by a versioned plan (raf), and deliveries proven by their effect. Needs the cadence CLI (npm i -g @sylad/cadence).",
|
|
11
|
-
"version": "1.
|
|
11
|
+
"version": "1.34.1",
|
|
12
12
|
"source": "./",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "Sylvain Ladoire"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cadence",
|
|
3
3
|
"description": "A repo-native working method: session start and close rituals driven by a versioned plan (raf), deliveries proven by their effect, and three reviewer agents (UX, code, QA).",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.34.1",
|
|
5
5
|
"author": { "name": "Sylvain Ladoire" },
|
|
6
6
|
"homepage": "https://github.com/Sylad/cadence",
|
|
7
7
|
"repository": "https://github.com/Sylad/cadence",
|
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ Details: [docs/skills.md](docs/skills.md) and [docs/cadence-hud.md](docs/cadence
|
|
|
48
48
|
## Where to read more
|
|
49
49
|
|
|
50
50
|
- [docs/raf.md](docs/raf.md): the plan, its commands and its file.
|
|
51
|
-
- [docs/orchestrate.md](docs/orchestrate.md): waves
|
|
51
|
+
- [docs/orchestrate.md](docs/orchestrate.md): waves, budgets, and [the edge-case probe](docs/orchestrate.md#the-edge-case-probe-orchestrateprobe-l497).
|
|
52
52
|
- [docs/deliver.md](docs/deliver.md): wait for the CI, deploy, verify the effect.
|
|
53
53
|
- [docs/spec.md](docs/spec.md): screen and back-end sheets.
|
|
54
54
|
- [docs/ref.md](docs/ref.md): reference documents (specs, norms, mock-ups) and the coverage of their elements by the lots.
|
package/dist/config.js
CHANGED
|
@@ -6,6 +6,7 @@ import { DEFAULT_TEST_PATTERNS } from './testgate.js';
|
|
|
6
6
|
import { gitRoot } from './git.js';
|
|
7
7
|
import { planCommitProblem } from './plan-commit.js';
|
|
8
8
|
import { isComposedGate } from './orchestrate/workers.js';
|
|
9
|
+
import { booleanKey } from './yaml-keys.js';
|
|
9
10
|
import { FIELDS, Plan, RafError, STATUSES } from './plan.js';
|
|
10
11
|
const KEYS = ['path', 'project', 'since', 'ignore', 'files', 'lots', 'fields', 'statuses', 'estimates', 'add', 'onWrite', 'commit'];
|
|
11
12
|
const ON_WRITE_KEYS = ['command', 'files'];
|
|
@@ -311,7 +312,7 @@ export function readSessionConfig(file) {
|
|
|
311
312
|
return config;
|
|
312
313
|
}
|
|
313
314
|
const EFFORTS = ['default', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
314
|
-
const EFFORT_STEPS = ['precheck', 'spec', 'maquette', 'fiche', 'accept', 'implement', 'fix', 'review', 'ux'];
|
|
315
|
+
const EFFORT_STEPS = ['precheck', 'spec', 'maquette', 'fiche', 'accept', 'implement', 'probe', 'fix', 'review', 'ux'];
|
|
315
316
|
/** Clé `verify:` de cadence.yaml. Absente : `{}`. */
|
|
316
317
|
export function readVerifyConfig(file) {
|
|
317
318
|
if (!existsSync(file))
|
|
@@ -338,10 +339,10 @@ export function readVerifyConfig(file) {
|
|
|
338
339
|
return { inPlace: v.inPlace };
|
|
339
340
|
}
|
|
340
341
|
const REVIEW_KEYS = ['threshold', 'light', 'full'];
|
|
341
|
-
const ORCH_KEYS = ['start', 'verdict', 'test', 'testReplay', 'build', 'worktreeDeps', 'gate', 'accept', 'maquette', 'maquetteReviews', 'fiche', 'ux', 'precheck', 'prodAccess', 'worktrees', 'testGate', 'spec', 'review', 'escalation', 'continue', 'effort', 'permissionMode', 'addDirs', 'timeouts'];
|
|
342
|
+
const ORCH_KEYS = ['start', 'verdict', 'test', 'testReplay', 'build', 'worktreeDeps', 'gate', 'accept', 'maquette', 'maquetteReviews', 'fiche', 'ux', 'precheck', 'probe', 'prodAccess', 'worktrees', 'testGate', 'spec', 'review', 'escalation', 'continue', 'effort', 'permissionMode', 'addDirs', 'timeouts'];
|
|
342
343
|
/** Clé `orchestrate:` de cadence.yaml. Absente : les défauts (auto, 45 min d'implémentation, 25 min de revue). */
|
|
343
344
|
export function readOrchestrateConfig(file) {
|
|
344
|
-
const config = { precheck: true, maquette: true, maquetteReviews: 3, fiche: true, precheckLocal: false, prodAccess: false, worktrees: false, spec: { dir: 'docs/specs' }, review: { threshold: 0.25, light: 'sonnet', full: 'opus' }, effort: { precheck: 'low', spec: 'medium', maquette: 'high', fiche: 'medium', accept: 'medium', implement: 'medium', fix: 'medium', review: 'high', ux: 'high' }, permissionMode: 'auto', addDirs: [], timeouts: { work: 45 * 60_000, review: 25 * 60_000 } };
|
|
345
|
+
const config = { precheck: true, maquette: true, maquetteReviews: 3, fiche: true, precheckLocal: false, prodAccess: false, worktrees: false, spec: { dir: 'docs/specs' }, review: { threshold: 0.25, light: 'sonnet', full: 'opus' }, effort: { precheck: 'low', spec: 'medium', maquette: 'high', fiche: 'medium', accept: 'medium', implement: 'medium', probe: 'medium', fix: 'medium', review: 'high', ux: 'high' }, permissionMode: 'auto', addDirs: [], timeouts: { work: 45 * 60_000, review: 25 * 60_000 } };
|
|
345
346
|
if (!existsSync(file))
|
|
346
347
|
return config;
|
|
347
348
|
let raw;
|
|
@@ -417,11 +418,9 @@ export function readOrchestrateConfig(file) {
|
|
|
417
418
|
throw bad('maquetteReviews : entier supérieur ou égal à 1 attendu');
|
|
418
419
|
config.maquetteReviews = o.maquetteReviews;
|
|
419
420
|
}
|
|
420
|
-
|
|
421
|
-
if (
|
|
422
|
-
|
|
423
|
-
config.fiche = o.fiche;
|
|
424
|
-
}
|
|
421
|
+
for (const k of ['fiche', 'probe'])
|
|
422
|
+
if (o[k] != null)
|
|
423
|
+
config[k] = booleanKey(o[k], bad(`${k} : true ou false attendu`));
|
|
425
424
|
if (o.precheck != null) {
|
|
426
425
|
if (typeof o.precheck !== 'boolean' && o.precheck !== 'local')
|
|
427
426
|
throw bad('precheck : true, false ou local attendu');
|
|
@@ -534,7 +533,7 @@ export function readOrchestrateConfig(file) {
|
|
|
534
533
|
}
|
|
535
534
|
if (o.effort != null) {
|
|
536
535
|
if (!isObject(o.effort))
|
|
537
|
-
throw bad('effort doit être un objet { precheck, spec, maquette, fiche, accept, implement, fix, review, ux }');
|
|
536
|
+
throw bad('effort doit être un objet { precheck, spec, maquette, fiche, accept, implement, probe, fix, review, ux }');
|
|
538
537
|
for (const [k, v] of Object.entries(o.effort)) {
|
|
539
538
|
if (!EFFORT_STEPS.includes(k))
|
|
540
539
|
throw bad(`effort.${k} inconnu (attendu : ${EFFORT_STEPS.join(', ')})`);
|
package/dist/corrections.js
CHANGED
|
@@ -99,11 +99,10 @@ export function readJournals(runsDir, until, since) {
|
|
|
99
99
|
const name = basename(file, '.json');
|
|
100
100
|
const fixSteps = steps.filter((s) => s.kind === 'fix' && done(s));
|
|
101
101
|
const passes = fixSteps.map((fix) => {
|
|
102
|
-
const review = [...steps].reverse().find((s) => s.n < fix.n && isReview(s.kind) && s.report);
|
|
103
|
-
// Une passe fix entre
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
return { key: `${wave}/${name}/${fix.n}`, project: rec.project, lot: rec.lot, visible: !!rec.visible, findings, commits: fix.commits ?? [], ...(fix.ended ? { fixEnded: fix.ended } : {}), ...(findings.length > 0 && review ? { reviewKind: review.kind } : {}) };
|
|
102
|
+
const review = [...steps].reverse().find((s) => s.n < fix.n && ((isReview(s.kind) && s.report) || (s.kind === 'probe' && Array.isArray(s.constats) && s.constats.length > 0)));
|
|
103
|
+
// La revue (ou la sonde, L497, qui porte ses constats dans son étape) qui précède. Une passe fix entre elle et celle-ci a déjà traité ces constats : cette passe-ci (tests rouges) n'en a pas.
|
|
104
|
+
const findings = !review || fixSteps.some((f) => f.n > review.n && f.n < fix.n) ? [] : review.kind === 'probe' ? review.constats.filter((t) => typeof t === 'string') : review.report ? findingsOf(readJson(join(waveDir, name, review.report))) : [];
|
|
105
|
+
return { key: `${wave}/${name}/${fix.n}`, project: rec.project, lot: rec.lot, visible: !!rec.visible, findings, commits: fix.commits ?? [], ...(fix.ended ? { fixEnded: fix.ended } : {}), ...(findings.length > 0 && review && review.kind !== 'probe' ? { reviewKind: review.kind } : {}) };
|
|
107
106
|
});
|
|
108
107
|
const precheck = precheckVerdictOf(rec, steps);
|
|
109
108
|
const esc = rec.escalation;
|
|
@@ -33,7 +33,7 @@ import { activeLock, REPO_LOCK, releaseLock, takeLock } from './lock.js';
|
|
|
33
33
|
import { groupLots, runPool } from './pool.js';
|
|
34
34
|
import { repoBranch } from './integrate.js';
|
|
35
35
|
import { baseBranchOf, keptLines, keptTrees, lotBranch, lotTreeDir, planFileSet, recoverTree, removeTree } from './worktree.js';
|
|
36
|
-
import { schemaFor } from './schemas.js';
|
|
36
|
+
import { agentOf, schemaFor } from './schemas.js';
|
|
37
37
|
import { resolveLotRepos } from '../repos.js';
|
|
38
38
|
import { excludeState, lotFinished, lotKey, lotRepoPaths, lotSlug, neighbourDirs, newLot, RunStore } from './state.js';
|
|
39
39
|
import { linkNodeBin, nvmVersionsDir, resolveNode } from './node-env.js';
|
|
@@ -302,7 +302,7 @@ async function preflight(args, targets, io, deps, launch, opts = {}) {
|
|
|
302
302
|
refusals.push(`${key} : ${e.message}`);
|
|
303
303
|
continue;
|
|
304
304
|
}
|
|
305
|
-
if (info && olderThanEffort(info.version) && Object.
|
|
305
|
+
if (info && olderThanEffort(info.version) && Object.entries(env.config.effort).some(([step, e]) => e !== 'default' && (step !== 'probe' || env.config.probe))) {
|
|
306
306
|
refusals.push(`${key} : claude ${info.version} n'a pas --effort (2.1.284 requise) : mettre claude à jour, ou orchestrate.effort à « default » pour chaque passe`);
|
|
307
307
|
continue;
|
|
308
308
|
}
|
|
@@ -518,6 +518,7 @@ function dryRun(lots, io, deps, budget, id) {
|
|
|
518
518
|
else
|
|
519
519
|
steps.unshift({ kind: 'precheck', model: env.config.precheckLocal ? 'local' : 'sonnet' });
|
|
520
520
|
}
|
|
521
|
+
steps.push(...(env.config.probe ? [{ kind: 'probe', model: 'sonnet' }] : [])); // L497 : entre l'écriture et la revue
|
|
521
522
|
const { light, full } = env.config.review;
|
|
522
523
|
if (l.small)
|
|
523
524
|
steps.push({ kind: l.visible ? 'review-small' : 'review', model: l.light ? light : full });
|
|
@@ -550,7 +551,7 @@ function dryRun(lots, io, deps, budget, id) {
|
|
|
550
551
|
io.out(l.light ? ' revue conforme avec mineurs : pas de passe des mineurs, ils sont rendus en notes' : ' revue conforme avec mineurs : une passe de correction des mineurs (session neuve), puis une revue courte ; les mineurs refusés sont rendus en « choix »');
|
|
551
552
|
const pwDir = join(resolve(RunStore.runsDir(io.cwd)), id, lotSlug(l.project, l.lot), 'playwright');
|
|
552
553
|
// brief écrit par --dry-run = base d'une délégation à la main : aucun dossier de vague (il n'existe pas)
|
|
553
|
-
const vars = { chemin: l.repo, lot: l.lot, titre: headline(lot, l.goal), objectif: objective(lot, l.goal), commits: '', reponse: '', constats: '', ux: '', choix: '', checks: '', news: l.visible ? newsText(l.lot) : '', captures: '' };
|
|
554
|
+
const vars = { chemin: l.repo, lot: l.lot, titre: headline(lot, l.goal), objectif: objective(lot, l.goal), commits: '', reponse: '', constats: '', ux: '', choix: '', checks: '', news: l.visible ? newsText(l.lot) : '', captures: '', diff: 'git diff <sha de départ>..HEAD' };
|
|
554
555
|
vars.decisions = decisionsText(humanDecisions(lot, lot.parent ? env.loadPlan().lots().find((x) => x.id === lot.parent) : undefined));
|
|
555
556
|
if (specStage)
|
|
556
557
|
vars.spec = specText({ dir: env.config.spec.dir, render: env.config.spec.render, screen: l.screen, back: !!l.specLot });
|
|
@@ -564,11 +565,10 @@ function dryRun(lots, io, deps, budget, id) {
|
|
|
564
565
|
}
|
|
565
566
|
for (const s of steps) {
|
|
566
567
|
const file = join(tmp, `${lotSlug(l.project, l.lot)}--${s.kind}.md`);
|
|
567
|
-
|
|
568
|
-
writeFileSync(file, brief);
|
|
568
|
+
writeFileSync(file, renderBrief(briefName(l, s.kind), s.kind === 'fiche' || vars.fiche === undefined ? vars : { ...vars, fiche: '' }, deps.templatesDir)); // la consigne de revue de fiche ne sert qu'au brief de la fiche : implement et review en sont la base d'une délégation à la main
|
|
569
569
|
const playwright = !!mcpServersFor(s.kind, l.visible, '', l.small, !!l.screen).playwright;
|
|
570
570
|
const local = s.model === 'local';
|
|
571
|
-
const args = (local ? buildLocalArgs : buildArgs)({ kind: s.kind, sessionId: '<uuid>', brief: '<brief>', model: s.model === 'local' ? 'sonnet' : s.model, local, effort: env.config.effort[s.kind === 'review-small' ? 'review' : s.kind], schema: schemaFor(s.kind), agent: s.kind
|
|
571
|
+
const args = (local ? buildLocalArgs : buildArgs)({ kind: s.kind, sessionId: '<uuid>', brief: '<brief>', model: s.model === 'local' ? 'sonnet' : s.model, local, effort: env.config.effort[s.kind === 'review-small' ? 'review' : s.kind], schema: schemaFor(s.kind), agent: agentOf(s.kind), cwd: l.repo, wave: id, permissionMode: env.config.permissionMode, addDirs: [...env.config.addDirs, ...(playwright && (s.kind === 'implement' || s.kind === 'spec') ? [pwDir] : []), ...neighbourDirs(l)], timeoutMs: 0, mcpConfig: '<mcp>', playwright, disablePlugins: enabledPlugins(settingsFiles(deps.claudeHome, l.repo)) }, agents).map((a) => (a.startsWith('{') ? '<json>' : a));
|
|
572
572
|
io.out(` ${s.kind} : ${local ? 'claude-local' : 'claude'} ${args.join(' ')}`);
|
|
573
573
|
io.out(` brief : ${file}`);
|
|
574
574
|
const servers = Object.keys(mcpServersFor(s.kind, l.visible, '', l.small, !!l.screen));
|
|
@@ -10,6 +10,8 @@ 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';
|
|
13
|
+
import { citesPriorWork, precheckNothingToFind } from './precheck.js';
|
|
14
|
+
import { PROBE_FAILED, PROBE_RESERVE, probeStep, probeVars, scheduleProbe } from './probe.js';
|
|
13
15
|
import { changedFiles, dirtyPaths, gitCommonDir, isAncestor, readCommits, resolveCommit, restoreWorktree, tryGit } from '../git.js';
|
|
14
16
|
import { blockingLots, gitCommitsIn } from './table.js';
|
|
15
17
|
import { citedRefs } from '../link.js';
|
|
@@ -25,7 +27,7 @@ import { journalTokens, peakContext, mcpServersFor, playwrightDir, runSession, t
|
|
|
25
27
|
import { cleanPlaywrightOutput, pushed, remoteAdvances, snapshot } from './guard.js';
|
|
26
28
|
import { sumTokens } from './result.js';
|
|
27
29
|
import { cutReport, EXTENSION_MS, MAX_EXTENSIONS, workTimeoutMs } from './timeouts.js';
|
|
28
|
-
import { ACCEPT_SCHEMA, checkShape, PRECHECK_SCHEMA, REVIEW_SCHEMA, schemaFor, SPEC_SCHEMA, WORK_SCHEMA } from './schemas.js';
|
|
30
|
+
import { ACCEPT_SCHEMA, agentOf, checkShape, PRECHECK_SCHEMA, REVIEW_SCHEMA, schemaFor, SPEC_SCHEMA, WORK_SCHEMA } from './schemas.js';
|
|
29
31
|
import { acceptationFile, acceptCommands, isAcceptList, acceptText, checkAcceptation, findAcceptation, isAcceptationFile, lockViolations, lotStartHead, parseAcceptation, planAcceptRuns, redSha, resealedRouge, sealAcceptation, setVert } from './accept.js';
|
|
30
32
|
import { lotIds, referenceIdsText } from '../ref/coverage.js';
|
|
31
33
|
import { avalFicheText, ficheContracts, ficheRules, hasScreenPart } from './aval.js';
|
|
@@ -485,6 +487,8 @@ function avalOf(c, kind) {
|
|
|
485
487
|
// la revue courte d'un petit lot visible est sa seule passe : elle porte aussi la conformité à la maquette
|
|
486
488
|
return kind === 'review-small' && l.small && l.visible && screen ? `${text}\n${avalFicheText('ux', o)}` : text;
|
|
487
489
|
}
|
|
490
|
+
/** Les étapes dont le programme rejoue la porte après la session (L399) : elles reçoivent ses commandes exactes et la part de machine (L350). La sonde écrit des tests : sans la porte, elle joue le runner seul et commite un test que `tsc` ou la qualité mettent au rouge, d'où une passe de correction pour rien. */
|
|
491
|
+
const writesCode = (kind) => kind === 'implement' || kind === 'fix' || kind === 'probe';
|
|
488
492
|
/** Consigne de la part de machine de la session (L350) ; vide quand la vague n'en a pas calculé. */
|
|
489
493
|
const workersText = (c) => (c.wave.testWorkers ? testWorkersText(c.wave.testWorkers) : '');
|
|
490
494
|
function briefFor(c, kind, local = false) {
|
|
@@ -492,7 +496,7 @@ function briefFor(c, kind, local = false) {
|
|
|
492
496
|
const l = c.lot;
|
|
493
497
|
const lot = plan.lot(l.lot);
|
|
494
498
|
const tmpDir = c.wave.store.tmpDir(l.project, l.lot);
|
|
495
|
-
const vars = { chemin: wd(c), lot: l.lot, titre: headline(lot, l.goal), objectif: objective(lot, l.goal), commits: '', reponse: '', constats: '', spec: '', ux: uxText(c), choix: choixText(c.lot.choix ?? []), checks: kind === 'review' || kind === 'review-small' ? checksText(c) : '', docs: kind === 'review' || kind === 'review-small' ? docsText(c) : '', news: '', captures: '', tmp: tmpText(tmpDir), workers: kind
|
|
499
|
+
const vars = { chemin: wd(c), lot: l.lot, titre: headline(lot, l.goal), objectif: objective(lot, l.goal), commits: '', reponse: '', constats: '', spec: '', ux: uxText(c), choix: choixText(c.lot.choix ?? []), checks: kind === 'review' || kind === 'review-small' ? checksText(c) : '', docs: kind === 'review' || kind === 'review-small' ? docsText(c) : '', news: '', captures: '', tmp: tmpText(tmpDir), workers: writesCode(kind) ? workersText(c) : '', gate: writesCode(kind) ? gateText(c.config) : '', repos: reposText(kind === 'implement' || kind === 'fix' ? 'write' : 'read', l.lot, l.repos ?? []), integration: c.integration ? integrationText(c.integration.base) : '' };
|
|
496
500
|
if (l.visible) {
|
|
497
501
|
const pwDir = playwrightDir(c.wave.store.lotDir(l.project, l.lot));
|
|
498
502
|
vars.news = newsText(l.lot, pwDir);
|
|
@@ -504,6 +508,8 @@ function briefFor(c, kind, local = false) {
|
|
|
504
508
|
// la vérification des ids d'un document de référence sur l'écran fini : la passe ux, ou la revue courte d'un petit lot visible qui n'a que celle-là (comme la conformité, avalOf)
|
|
505
509
|
if (kind === 'ux' || (kind === 'review-small' && l.small && l.visible))
|
|
506
510
|
vars.ux = [vars.ux, referenceIdsText('ux', lotIds({ ...lot, goal: l.goal ?? lot.goal }))].filter(Boolean).join('\n');
|
|
511
|
+
if (kind === 'probe')
|
|
512
|
+
Object.assign(vars, probeVars(ficheOf(c), lotCommitLines(c), startSha(c), l.lot));
|
|
507
513
|
if (kind === 'implement' || kind === 'fix')
|
|
508
514
|
vars.decisions = decisionsText(humanDecisions(lot, lot.parent ? plan.lots().find((x) => x.id === lot.parent) : undefined));
|
|
509
515
|
if (kind === 'precheck' && local)
|
|
@@ -574,7 +580,7 @@ function briefFor(c, kind, local = false) {
|
|
|
574
580
|
}
|
|
575
581
|
const brief = renderBrief(briefName(l, kind, !!c.integration), vars, c.wave.templates);
|
|
576
582
|
// Les dépôts tiers (L456) : la consigne s'ajoute à tout brief d'écriture, sans passer par un gabarit (un gabarit modifié change l'empreinte que d'autres tests gardent).
|
|
577
|
-
return
|
|
583
|
+
return ['implement', 'fix', 'spec', 'accept', 'probe'].includes(kind) ? `${brief}\n${tiersText(l.project, l.lot)}\n` : brief;
|
|
578
584
|
}
|
|
579
585
|
/** Tokens comptés par les sessions d'un lot (un lot repris compte ses étapes d'avant). */
|
|
580
586
|
export const lotSpent = (l) => l.steps.reduce((n, s) => n + (s.tokens?.counted ?? 0), 0);
|
|
@@ -625,8 +631,8 @@ export function budgetStep(l) {
|
|
|
625
631
|
const step = named ?? l.next;
|
|
626
632
|
return step && step !== 'integrate' && WRITE_STEPS.includes(step) ? step : null;
|
|
627
633
|
}
|
|
628
|
-
/** Le budget du lot paie-t-il la passe de correction et la revue courte qui la suit ? Sans budget de lot : toujours. La part de l'escalade (L349) n'est pas à dépenser, sauf pour décider l'escalade elle-même (`escalating`). */
|
|
629
|
-
const fixAffordable = (l, escalating = false) => l.budget === undefined || lotSpent(l) + REVIEW_RESERVE < l.budget - (escalating ? 0 : heldForEscalation(l));
|
|
634
|
+
/** Le budget du lot paie-t-il la passe de correction et la revue courte qui la suit ? Sans budget de lot : toujours. La part de l'escalade (L349) n'est pas à dépenser, sauf pour décider l'escalade elle-même (`escalating`). `extra` : le coût d'une étape de plus devant elles (la sonde, L497). */
|
|
635
|
+
const fixAffordable = (l, escalating = false, extra = 0) => l.budget === undefined || lotSpent(l) + extra + REVIEW_RESERVE < l.budget - (escalating ? 0 : heldForEscalation(l));
|
|
630
636
|
/** Cause d'une suspension demandée par `--stop-after-current` (L79). */
|
|
631
637
|
export const STOP_REQUESTED = 'arrêt demandé (--stop-after-current)';
|
|
632
638
|
/** Le lot a-t-il été écarté de la vague par `--drop` (L79) ? */
|
|
@@ -677,11 +683,11 @@ async function session(c, kind, local = false) {
|
|
|
677
683
|
const halt = halted(w, l);
|
|
678
684
|
if (halt)
|
|
679
685
|
return suspend(c, halt);
|
|
680
|
-
const write = kind === '
|
|
686
|
+
const write = kind === 'probe' || WRITE_STEPS.includes(kind); // la sonde écrit (ses tests) mais le budget ne la borne pas : il est vérifié avant de la décider
|
|
681
687
|
// Le budget du lot borne l'écriture, jamais la revue (L145) : une revue est jouée dès que le code est à relire (sauf tests rouges après une écriture, cf. REVIEW_RESERVE), la passe fix réserve son coût.
|
|
682
|
-
if (
|
|
688
|
+
if (WRITE_STEPS.includes(kind) && (lotOver(l) || (kind === 'fix' && !fixAffordable(l))))
|
|
683
689
|
return overBudget(c, kind === 'fix' && !lotOver(l));
|
|
684
|
-
const model =
|
|
690
|
+
const model = ['spec', 'accept', 'fiche', 'probe'].includes(kind) ? 'sonnet' : write ? (kind === 'fix' && l.escalation?.stage === 'fix' ? l.escalation.model : l.model) : local ? 'local' : kind === 'precheck' ? 'sonnet' : reviewModel(c, kind); // le contrôle préalable ne fait que lire : pas d'Opus
|
|
685
691
|
const effort = c.config.effort[kind === 'review-small' ? 'review' : kind];
|
|
686
692
|
const before = await snapshot(wd(c));
|
|
687
693
|
const neighbours = l.repos ?? [];
|
|
@@ -691,7 +697,7 @@ async function session(c, kind, local = false) {
|
|
|
691
697
|
const sessionId = randomUUID();
|
|
692
698
|
const step = { n, kind, model, ...(effort !== 'default' ? { effort } : {}), status: 'running', sessionId, started: new Date().toISOString(), headBefore: before.head ?? undefined };
|
|
693
699
|
l.steps.push(step);
|
|
694
|
-
const label = { implement: 'implementing', fix: 'fixing', review: 'reviewing', ux: 'reviewing', 'review-small': 'reviewing', precheck: 'implementing', spec: 'implementing', accept: 'implementing', maquette: 'reviewing', fiche: 'reviewing' };
|
|
700
|
+
const label = { implement: 'implementing', fix: 'fixing', review: 'reviewing', ux: 'reviewing', 'review-small': 'reviewing', precheck: 'implementing', spec: 'implementing', accept: 'implementing', maquette: 'reviewing', fiche: 'reviewing', probe: 'implementing' };
|
|
695
701
|
transition(c, label[kind]);
|
|
696
702
|
w.log(`${lotKey(l.project, l.lot)} · session ${n} ${kind} (${model})`);
|
|
697
703
|
let brief;
|
|
@@ -742,7 +748,7 @@ async function session(c, kind, local = false) {
|
|
|
742
748
|
effort,
|
|
743
749
|
local,
|
|
744
750
|
schema: schemaFor(kind),
|
|
745
|
-
agent: kind === '
|
|
751
|
+
agent: kind === 'fix' && l.docsFix ? 'doc-keeper' : agentOf(kind),
|
|
746
752
|
cwd: wd(c),
|
|
747
753
|
wave: w.id,
|
|
748
754
|
lot: lotKey(l.project, l.lot),
|
|
@@ -882,6 +888,19 @@ async function session(c, kind, local = false) {
|
|
|
882
888
|
return null;
|
|
883
889
|
step.status = 'failed';
|
|
884
890
|
step.cause = outcome.cause;
|
|
891
|
+
if (kind === 'probe') {
|
|
892
|
+
// R9 : une sonde en échec ou coupée ne rend pas le lot. Ce qu'elle a laissé non commité est remis (suivis restaurés, nouveaux fichiers retirés), un warning le nomme.
|
|
893
|
+
// Coupée entre `git add` et son commit, elle laisse aussi l'index : il est désindexé d'abord, `restoreWorktree` ne remet que l'arbre de travail (sinon « AD q.spec.ts » et le lot rendu pour arbre sale).
|
|
894
|
+
// Un fichier à la fois : un chemin inconnu de l'index (non indexé) fait refuser toute la commande, les autres resteraient indexés.
|
|
895
|
+
const left = dirtBetween(before, await snapshot(wd(c)));
|
|
896
|
+
if (left.length) {
|
|
897
|
+
for (const f of left)
|
|
898
|
+
tryGit(wd(c), ['restore', '--staged', '--', f]);
|
|
899
|
+
restoreWorktree(wd(c), left);
|
|
900
|
+
l.warnings.push(`sonde de cas limites : fichiers non commités laissés par la session retirés (${left.join(', ')})`);
|
|
901
|
+
}
|
|
902
|
+
return PROBE_FAILED;
|
|
903
|
+
}
|
|
885
904
|
if (write && outcome.cause === 'délai dépassé') {
|
|
886
905
|
// L298 : le travail laissé par une étape d'écriture coupée au délai est signalé (commits, fichiers, tests), pas un « échec » sec ; L303 : dans les dépôts voisins aussi.
|
|
887
906
|
const repos = [{ rel: '', path: wd(c), before }, ...neighbours.map((r, i) => ({ rel: r.rel, path: r.path, before: beforeOthers[i] }))];
|
|
@@ -1282,43 +1301,6 @@ function badReport(c, step, e) {
|
|
|
1282
1301
|
step.cause = e.message;
|
|
1283
1302
|
return stop(c, 'failed', `${step.kind} : ${e.message}`);
|
|
1284
1303
|
}
|
|
1285
|
-
/**
|
|
1286
|
-
* Mots du titre ou du goal qui disent que le lot reprend un constat : le livrable peut déjà exister. Mots entiers (« review » ne
|
|
1287
|
-
* se lit pas dans `qa-reviewer` ni `review-small`), au pluriel ou accordés pour « constat ».
|
|
1288
|
-
*/
|
|
1289
|
-
const CITES_FINDING = /(?<![\w-])(?:constat\w*|revues?|reviews?|audits?)(?![\w-])/i;
|
|
1290
|
-
/**
|
|
1291
|
-
* Le texte du lot (titre, goal, objectif donné) cite un constat, ou un autre lot dont le travail a pu être fait. Pour un lot
|
|
1292
|
-
* `L<n>`, « L12 » compte dans le texte de `L30` (aussi dans « L12-L15 », « L12-bis », « L12.3 ») et non celui de `L12` ni d'un lot postérieur ; tout identifiant du plan qui n'est
|
|
1293
|
-
* pas `L<n>` (`Q-CAN5`, `B28.1`, `Q4/qa-expectations`) compte cité en entier (`B28.1` n'est pas `B28.10`), pour un lot `L<n>`
|
|
1294
|
-
* comme pour un autre ; un lot d'un autre projet (`cadence:L68`) compte toujours.
|
|
1295
|
-
*/
|
|
1296
|
-
function citesPriorWork(text, lot, ids) {
|
|
1297
|
-
if (CITES_FINDING.test(text))
|
|
1298
|
-
return true;
|
|
1299
|
-
const { ids: cited, foreign } = citedLots(text, lot, ids);
|
|
1300
|
-
return foreign || cited.length > 0;
|
|
1301
|
-
}
|
|
1302
|
-
/**
|
|
1303
|
-
* Les lots que le texte cite (même reconnaissance que `citesPriorWork`, L288/L292) : les identifiants du plan cités, et `foreign` quand il cite un lot d'un autre projet
|
|
1304
|
-
* (`cadence:L68`, dont on ne lit pas les commits ici).
|
|
1305
|
-
*/
|
|
1306
|
-
function citedLots(text, lot, ids) {
|
|
1307
|
-
const foreign = /(?<![\w-])[a-z][\w-]*:L\d+(?!\w)/i.test(text);
|
|
1308
|
-
const own = /^L(\d+)$/.exec(lot);
|
|
1309
|
-
const cited = (id) => new RegExp(`(?<![\\w.-])${id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\w-]|\\.\\w)`).test(text);
|
|
1310
|
-
return {
|
|
1311
|
-
foreign,
|
|
1312
|
-
ids: ids.filter((id) => {
|
|
1313
|
-
if (id === lot)
|
|
1314
|
-
return false;
|
|
1315
|
-
const n = /^L(\d+)$/.exec(id);
|
|
1316
|
-
if (own && n)
|
|
1317
|
-
return Number(n[1]) < Number(own[1]) && new RegExp(`(?<![\\w-])${id}(?!\\w)`).test(text);
|
|
1318
|
-
return cited(id);
|
|
1319
|
-
}),
|
|
1320
|
-
};
|
|
1321
|
-
}
|
|
1322
1304
|
/**
|
|
1323
1305
|
* Le contrôle préalable ne vaut que pour un lot sans commit (avec des commits, l'implémentation reprend légitimement) dont le
|
|
1324
1306
|
* titre ou le goal (clé `goal:` du plan, ou `goal` donné au lancement) cite un constat ou un autre lot (L288, L292) : sans cela
|
|
@@ -1335,44 +1317,7 @@ export function needsPrecheck(config, plan, repo, lot, repos = [], goal) {
|
|
|
1335
1317
|
const text = [l.title, l.goal, goal].filter(Boolean).join('\n');
|
|
1336
1318
|
return citesPriorWork(text, lot, plan.lots().map((x) => x.id));
|
|
1337
1319
|
}
|
|
1338
|
-
|
|
1339
|
-
* Un lot créé après le dernier commit de code du dépôt n'a rien à retrouver (L322) : tout ce que le dépôt contient date d'avant le lot, donc aucun autre lot ne l'a livré.
|
|
1340
|
-
* Rend la raison à écrire au journal, ou null quand le contrôle garde un sens. Le lot est daté par le commit qui a ajouté son id au plan, borné par sa clé `created` (une
|
|
1341
|
-
* migration ou un déplacement du plan ne rajeunit pas un vieux lot) ; le dernier commit de code est le dernier qui ne touche pas que le plan (`isPlanOnly`). Prudent : lot
|
|
1342
|
-
* qui travaille dans des dépôts voisins, id introuvable dans l'historique du plan, dépôt sans commit de code ou instants égaux → le contrôle reste.
|
|
1343
|
-
* Le contrôle reste aussi quand le titre ou le goal (ou le goal donné au lancement) cite un lot qui a déjà ses propres commits, ou un lot d'un autre projet : une « suite de revue L<n> » écrite
|
|
1344
|
-
* juste après le dernier correctif de L<n> liste des constats que les passes de correction de L<n> ont pu régler avant la création du lot (rejoué depuis le 24-10 : 5 lots sautés à tort,
|
|
1345
|
-
* dont 2 avec un vrai « partiel »).
|
|
1346
|
-
*/
|
|
1347
|
-
export function precheckNothingToFind(plan, root, lot, repos = [], goal) {
|
|
1348
|
-
if (repos.length)
|
|
1349
|
-
return null;
|
|
1350
|
-
try {
|
|
1351
|
-
const rel = relative(root, plan.path);
|
|
1352
|
-
const idLine = `^\\s*- id: ["']?${lot.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&')}["']?\\s*$`;
|
|
1353
|
-
// Pas de `--reverse` : avec `--follow`, git 2.43 ne rend rien sur un fichier renommé (`raf migrate`) ; le plus ancien est la dernière ligne.
|
|
1354
|
-
const added = git(root, '-c', 'core.quotepath=off', 'log', '--follow', '--format=%ct', '-G', idLine, '--', rel).split('\n').filter(Boolean).pop();
|
|
1355
|
-
if (!added)
|
|
1356
|
-
return null;
|
|
1357
|
-
const created = plan.lot(lot).created;
|
|
1358
|
-
const endOfCreated = created ? Date.parse(`${created}T23:59:59`) / 1000 : Infinity;
|
|
1359
|
-
const createdAt = Math.min(Number(added), Number.isNaN(endOfCreated) ? Infinity : endOfCreated);
|
|
1360
|
-
const log = git(root, 'log', '--format=%H %ct').split('\n').filter(Boolean);
|
|
1361
|
-
const code = log.map((x) => x.split(' ')).find(([sha]) => !isPlanOnly(sha, plan, root));
|
|
1362
|
-
if (!code)
|
|
1363
|
-
return null;
|
|
1364
|
-
if (createdAt <= Number(code[1]))
|
|
1365
|
-
return null;
|
|
1366
|
-
const l = plan.lot(lot);
|
|
1367
|
-
const { ids, foreign } = citedLots([l.title, l.goal, goal].filter(Boolean).join('\n'), lot, plan.lots().map((x) => x.id));
|
|
1368
|
-
if (foreign || ids.some((id) => lotWork(plan, root, id).length > 0))
|
|
1369
|
-
return null;
|
|
1370
|
-
return `lot créé après le dernier commit de code du dépôt (${code[0].slice(0, 7)}) : rien à retrouver`;
|
|
1371
|
-
}
|
|
1372
|
-
catch {
|
|
1373
|
-
return null;
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1320
|
+
export { precheckNothingToFind };
|
|
1376
1321
|
/**
|
|
1377
1322
|
* Contrôle préalable (L77) : une session de lecture seule cherche si le livrable du lot est déjà dans le dépôt (fait par un
|
|
1378
1323
|
* autre lot). « oui » rend le lot sans ouvrir d'implémentation ; « partiel » joint le constat au brief de l'implémentation.
|
|
@@ -2144,6 +2089,8 @@ export function redExcerpt(k) {
|
|
|
2144
2089
|
const more = named.length > EXCERPT_LINES ? [`… (+${named.length - EXCERPT_LINES})`] : [];
|
|
2145
2090
|
return [head.replace(/\s*:$/, ''), ...shown, ...more].join(' ; ');
|
|
2146
2091
|
}
|
|
2092
|
+
/** Violations du verrou des tests d'acceptation (L173) par les commits faits depuis `head` ; aucune sans HEAD de départ. */
|
|
2093
|
+
const sealedViolations = (c, head) => (head ? lockViolations(wd(c), c.config.spec.dir, head, { rouge: c.lot.accept?.rouge ? { [c.lot.accept.acceptation]: c.lot.accept.rouge } : {}, lot: c.lot.lot, lotBase: c.lot.steps.find((s) => s.headBefore)?.headBefore, lotStart: lotStartHead(wd(c), lotWork(histPlan(c), wd(c), c.lot.lot).map((k) => k.sha)), lotCommits: lotCommits(histPlan(c), wd(c), c.lot.lot).map((k) => k.sha) }) : []);
|
|
2147
2094
|
/** Étape d'écriture (implémentation, correction) : contrôles, puis étape suivante décidée. */
|
|
2148
2095
|
async function work(c, kind) {
|
|
2149
2096
|
const l = c.lot;
|
|
@@ -2206,7 +2153,7 @@ async function work(c, kind) {
|
|
|
2206
2153
|
save(c);
|
|
2207
2154
|
// Verrou des tests d'acceptation (L173) : ceux de TOUS les lots du dépôt, pas seulement du lot courant ; une modification est une question au lead. Contrôlé à CHAQUE fin de session, une question comprise (L200) :
|
|
2208
2155
|
// une session qui remplace un test scellé puis pose une question ne passe pas par la reprise du lead pour se faire adopter.
|
|
2209
|
-
const locked =
|
|
2156
|
+
const locked = sealedViolations(c, done.step.headBefore);
|
|
2210
2157
|
if (locked.length) {
|
|
2211
2158
|
stop(c, 'handed-back', `verrou des tests d'acceptation : ${locked.join(' ; ')}`);
|
|
2212
2159
|
return;
|
|
@@ -2350,7 +2297,18 @@ async function work(c, kind) {
|
|
|
2350
2297
|
}
|
|
2351
2298
|
}
|
|
2352
2299
|
}
|
|
2353
|
-
//
|
|
2300
|
+
// Sonde de cas limites (L497) : une fois par lot, après la porte « code sans test », avant le bibliothécaire et la revue.
|
|
2301
|
+
if (scheduleProbe(c.config, l, { skip: minorsPass || !alreadyDone, late: l.pass >= MAX_PASSES - 1, pays: fixAffordable(l, false, PROBE_RESERVE), count: budgetCount(l) })) {
|
|
2302
|
+
// Les contrôles de la porte restent au lot pendant la sonde : sans nouveau commit de sa part, la revue les reprend pour ce HEAD.
|
|
2303
|
+
l.checks = runs.length ? { head: git(wd(c), 'rev-parse', 'HEAD'), tree: git(wd(c), 'rev-parse', 'HEAD^{tree}'), runs } : undefined;
|
|
2304
|
+
return save(c);
|
|
2305
|
+
}
|
|
2306
|
+
await toReview(c, runs, minorsPass);
|
|
2307
|
+
}
|
|
2308
|
+
/** Fin d'une écriture dont les portes sont passées : le bibliothécaire (L306), puis la première revue. Aussi la suite d'une sonde (L497) qui n'a rien à corriger. */
|
|
2309
|
+
async function toReview(c, runs, minorsPass) {
|
|
2310
|
+
const l = c.lot;
|
|
2311
|
+
// Bibliothécaire (L306) : docs.sync signale un document en retard sur les commits du lot → une passe doc-keeper avant la revue, une seule fois par lot, aux mêmes conditions de passe et de budget que la porte « code sans test ». Si elle ne touche pas le document, la revue part et porte l'écart (brief docs.sync, constat majeur).
|
|
2354
2312
|
if (!l.docsGate && !minorsPass) {
|
|
2355
2313
|
const { gaps } = docGaps(c);
|
|
2356
2314
|
if (gaps.length) {
|
|
@@ -2372,6 +2330,11 @@ async function work(c, kind) {
|
|
|
2372
2330
|
l.next = firstReview(c);
|
|
2373
2331
|
save(c);
|
|
2374
2332
|
}
|
|
2333
|
+
/** Ce que la sonde (probe.ts) emprunte au cycle. */
|
|
2334
|
+
const probeIo = (c) => ({
|
|
2335
|
+
lot: c.lot, config: c.config, cwd: wd(c), play: () => session(c, 'probe'), take: (step, rep) => takeReport(c, { step }, rep), sealed: (head) => sealedViolations(c, head), dirt: dirtAfter, gate: (red, runs) => programChecks(c, red, runs),
|
|
2336
|
+
fix: (constats) => toFix(c, constats, 'sonde de cas limites'), review: (runs) => toReview(c, runs, false), handBack: (why) => void stop(c, 'handed-back', why), pays: () => fixAffordable(c.lot), head: () => git(wd(c), 'rev-parse', 'HEAD').trim(),
|
|
2337
|
+
});
|
|
2375
2338
|
/** Mention « sans commit » du verdict quand la passe des mineurs n'a rien commité (l'UX finale conclut alors à la place de la passe). */
|
|
2376
2339
|
const noCommitNote = (l) => (l.warnings.some((w) => w.startsWith('passe des mineurs sans commit')) ? ' sans commit' : '');
|
|
2377
2340
|
/** Revue de code qui clôt le cycle : complète, ou courte une fois la passe des mineurs jouée. */
|
|
@@ -3104,6 +3067,8 @@ export async function runLot(c) {
|
|
|
3104
3067
|
await acceptStep(c);
|
|
3105
3068
|
else if (kind === 'implement' || kind === 'fix')
|
|
3106
3069
|
await work(c, kind);
|
|
3070
|
+
else if (kind === 'probe')
|
|
3071
|
+
await probeStep(probeIo(c));
|
|
3107
3072
|
else if (kind === 'integrate')
|
|
3108
3073
|
await integrateLot(c);
|
|
3109
3074
|
else
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Les critères du contrôle préalable (L77, L288, L292, L322) : un lot qui cite un constat ou un autre lot, et un lot créé après le dernier commit de code, qui n'a rien à retrouver.
|
|
2
|
+
import { relative } from 'node:path';
|
|
3
|
+
import { isPlanOnly, lotWork } from '../audit.js';
|
|
4
|
+
import { git } from './worktree.js';
|
|
5
|
+
/**
|
|
6
|
+
* Mots du titre ou du goal qui disent que le lot reprend un constat : le livrable peut déjà exister. Mots entiers (« review » ne
|
|
7
|
+
* se lit pas dans `qa-reviewer` ni `review-small`), au pluriel ou accordés pour « constat ».
|
|
8
|
+
*/
|
|
9
|
+
const CITES_FINDING = /(?<![\w-])(?:constat\w*|revues?|reviews?|audits?)(?![\w-])/i;
|
|
10
|
+
/**
|
|
11
|
+
* Le texte du lot (titre, goal, objectif donné) cite un constat, ou un autre lot dont le travail a pu être fait. Pour un lot
|
|
12
|
+
* `L<n>`, « L12 » compte dans le texte de `L30` (aussi dans « L12-L15 », « L12-bis », « L12.3 ») et non celui de `L12` ni d'un lot postérieur ; tout identifiant du plan qui n'est
|
|
13
|
+
* pas `L<n>` (`Q-CAN5`, `B28.1`, `Q4/qa-expectations`) compte cité en entier (`B28.1` n'est pas `B28.10`), pour un lot `L<n>`
|
|
14
|
+
* comme pour un autre ; un lot d'un autre projet (`cadence:L68`) compte toujours.
|
|
15
|
+
*/
|
|
16
|
+
export function citesPriorWork(text, lot, ids) {
|
|
17
|
+
if (CITES_FINDING.test(text))
|
|
18
|
+
return true;
|
|
19
|
+
const { ids: cited, foreign } = citedLots(text, lot, ids);
|
|
20
|
+
return foreign || cited.length > 0;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Les lots que le texte cite (même reconnaissance que `citesPriorWork`, L288/L292) : les identifiants du plan cités, et `foreign` quand il cite un lot d'un autre projet
|
|
24
|
+
* (`cadence:L68`, dont on ne lit pas les commits ici).
|
|
25
|
+
*/
|
|
26
|
+
function citedLots(text, lot, ids) {
|
|
27
|
+
const foreign = /(?<![\w-])[a-z][\w-]*:L\d+(?!\w)/i.test(text);
|
|
28
|
+
const own = /^L(\d+)$/.exec(lot);
|
|
29
|
+
const cited = (id) => new RegExp(`(?<![\\w.-])${id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\w-]|\\.\\w)`).test(text);
|
|
30
|
+
return {
|
|
31
|
+
foreign,
|
|
32
|
+
ids: ids.filter((id) => {
|
|
33
|
+
if (id === lot)
|
|
34
|
+
return false;
|
|
35
|
+
const n = /^L(\d+)$/.exec(id);
|
|
36
|
+
if (own && n)
|
|
37
|
+
return Number(n[1]) < Number(own[1]) && new RegExp(`(?<![\\w-])${id}(?!\\w)`).test(text);
|
|
38
|
+
return cited(id);
|
|
39
|
+
}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Un lot créé après le dernier commit de code du dépôt n'a rien à retrouver (L322) : tout ce que le dépôt contient date d'avant le lot, donc aucun autre lot ne l'a livré.
|
|
44
|
+
* Rend la raison à écrire au journal, ou null quand le contrôle garde un sens. Le lot est daté par le commit qui a ajouté son id au plan, borné par sa clé `created` (une
|
|
45
|
+
* migration ou un déplacement du plan ne rajeunit pas un vieux lot) ; le dernier commit de code est le dernier qui ne touche pas que le plan (`isPlanOnly`). Prudent : lot
|
|
46
|
+
* qui travaille dans des dépôts voisins, id introuvable dans l'historique du plan, dépôt sans commit de code ou instants égaux → le contrôle reste.
|
|
47
|
+
* Le contrôle reste aussi quand le titre ou le goal (ou le goal donné au lancement) cite un lot qui a déjà ses propres commits, ou un lot d'un autre projet : une « suite de revue L<n> » écrite
|
|
48
|
+
* juste après le dernier correctif de L<n> liste des constats que les passes de correction de L<n> ont pu régler avant la création du lot (rejoué depuis le 24-10 : 5 lots sautés à tort,
|
|
49
|
+
* dont 2 avec un vrai « partiel »).
|
|
50
|
+
*/
|
|
51
|
+
export function precheckNothingToFind(plan, root, lot, repos = [], goal) {
|
|
52
|
+
if (repos.length)
|
|
53
|
+
return null;
|
|
54
|
+
try {
|
|
55
|
+
const rel = relative(root, plan.path);
|
|
56
|
+
const idLine = `^\\s*- id: ["']?${lot.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&')}["']?\\s*$`;
|
|
57
|
+
// Pas de `--reverse` : avec `--follow`, git 2.43 ne rend rien sur un fichier renommé (`raf migrate`) ; le plus ancien est la dernière ligne.
|
|
58
|
+
const added = git(root, '-c', 'core.quotepath=off', 'log', '--follow', '--format=%ct', '-G', idLine, '--', rel).split('\n').filter(Boolean).pop();
|
|
59
|
+
if (!added)
|
|
60
|
+
return null;
|
|
61
|
+
const created = plan.lot(lot).created;
|
|
62
|
+
const endOfCreated = created ? Date.parse(`${created}T23:59:59`) / 1000 : Infinity;
|
|
63
|
+
const createdAt = Math.min(Number(added), Number.isNaN(endOfCreated) ? Infinity : endOfCreated);
|
|
64
|
+
const log = git(root, 'log', '--format=%H %ct').split('\n').filter(Boolean);
|
|
65
|
+
const code = log.map((x) => x.split(' ')).find(([sha]) => !isPlanOnly(sha, plan, root));
|
|
66
|
+
if (!code)
|
|
67
|
+
return null;
|
|
68
|
+
if (createdAt <= Number(code[1]))
|
|
69
|
+
return null;
|
|
70
|
+
const l = plan.lot(lot);
|
|
71
|
+
const { ids, foreign } = citedLots([l.title, l.goal, goal].filter(Boolean).join('\n'), lot, plan.lots().map((x) => x.id));
|
|
72
|
+
if (foreign || ids.some((id) => lotWork(plan, root, id).length > 0))
|
|
73
|
+
return null;
|
|
74
|
+
return `lot créé après le dernier commit de code du dépôt (${code[0].slice(0, 7)}) : rien à retrouver`;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { docSyncSet, filesOf } from '../docsync.js';
|
|
2
|
+
import { readCommits } from '../git.js';
|
|
3
|
+
import { checkShape, PROBE_SCHEMA } from './schemas.js';
|
|
4
|
+
/** Rendu par la session de la sonde quand elle n'a rien donné (échec, coupure, délai) : la sonde ne bloque jamais le lot, un warning et la revue part. */
|
|
5
|
+
export const PROBE_FAILED = Symbol('probe-failed');
|
|
6
|
+
/**
|
|
7
|
+
* Coût réservé à la sonde en plus de la correction et de la revue qui la suit : une session Sonnet d'écriture. ESTIMATION, aucune mesure (la sonde n'a pas encore joué) :
|
|
8
|
+
* le p90 d'une passe fix Sonnet, 65 k tokens comptés (mesure du 2026-10-15, voir REVIEW_RESERVE). À remesurer sur les premières sondes (`cadence perf`, étape probe).
|
|
9
|
+
*/
|
|
10
|
+
export const PROBE_RESERVE = 65_000;
|
|
11
|
+
/** Préfixe du texte d'un constat de la sonde : il distingue, dans le journal, la passe de correction qu'elle déclenche. */
|
|
12
|
+
const PROBE_FINDING = 'sonde de cas limites : ';
|
|
13
|
+
/**
|
|
14
|
+
* Décide la sonde d'un lot (R3, R6) : vrai quand elle est la prochaine étape. Décidée une seule fois par lot (`l.probe`), jouée ou écartée avec un warning qui dit pourquoi.
|
|
15
|
+
* Sans la clé `orchestrate.probe`, rien : ni champ ni warning.
|
|
16
|
+
*/
|
|
17
|
+
export function scheduleProbe(config, l, o) {
|
|
18
|
+
if (!config.probe || l.probe || o.skip)
|
|
19
|
+
return false;
|
|
20
|
+
l.probe = true;
|
|
21
|
+
if (!o.late && o.pays) {
|
|
22
|
+
l.next = 'probe';
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
l.warnings.push(`sonde de cas limites non jouée : ${l.pass} passe(s) de correction déjà faite(s) ou budget du lot (${o.count}) insuffisant`);
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
/** Les variables du brief de la sonde : la fiche du lot quand il en a une, ses commits, et la commande du diff (jamais le diff collé). */
|
|
29
|
+
export function probeVars(fiche, commits, from, lot) {
|
|
30
|
+
return {
|
|
31
|
+
fiche: fiche ? `The lot has a spec sheet: \`${fiche}\`. Read it first: each of its rules (Rn) states a behaviour to attack, and its contracts give the exact errors the code must raise.` : '',
|
|
32
|
+
commits,
|
|
33
|
+
diff: from ? `git diff ${from}..HEAD` : `git log -p --reverse --grep=${lot}`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const unplayed = (l, why) => l.warnings.push(`sonde de cas limites non jouée : ${why}`);
|
|
37
|
+
/** Le rapport de la session, ou un warning « non jouée » quand il est illisible (la revue part quand même). */
|
|
38
|
+
function readReport(l, report) {
|
|
39
|
+
try {
|
|
40
|
+
return checkShape(report, PROBE_SCHEMA);
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
unplayed(l, e.message);
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Les fichiers commités par la sonde qui ne sont pas des tests (R8) : un warning les nomme, rien n'est retiré. Sans `testGate.tests` déclaré, rien ne distingue un test d'un autre. */
|
|
48
|
+
function warnOutsideTests(io, headBefore) {
|
|
49
|
+
const tests = io.config.testGate?.tests;
|
|
50
|
+
if (!tests)
|
|
51
|
+
return;
|
|
52
|
+
const isTest = docSyncSet(tests);
|
|
53
|
+
const outside = [...filesOf(io.cwd, readCommits(io.cwd, { range: headBefore ? `${headBefore}..HEAD` : undefined }))].filter((f) => !isTest(f)).sort();
|
|
54
|
+
if (outside.length)
|
|
55
|
+
io.lot.warnings.push(`sonde de cas limites : la session a modifié du code hors tests (${outside.join(', ')})`);
|
|
56
|
+
}
|
|
57
|
+
/** Les contrôles faits après une session qui a commité, avec ou sans rapport : verrou, dépôt propre (`dirt`), fichiers hors tests. Vrai quand le lot est rendu. */
|
|
58
|
+
function handedBack(io, dirt, headBefore) {
|
|
59
|
+
const locked = io.sealed(headBefore);
|
|
60
|
+
const why = locked.length ? `verrou des tests d'acceptation : ${locked.join(' ; ')}` : dirt.length ? `dépôt sale après probe : ${dirt.join(', ')}` : '';
|
|
61
|
+
if (why)
|
|
62
|
+
io.handBack(why);
|
|
63
|
+
else
|
|
64
|
+
warnOutsideTests(io, headBefore);
|
|
65
|
+
return !!why;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Les constats de la sonde (R4, R5) : les rouges NOUVEAUX de la porte du programme (jamais un échec déjà rouge au départ), en gravité majeure, avec ce que la session annonce
|
|
69
|
+
* (« test — borne — attendu ») pour que la correction voie la borne. Sans `orchestrate.test`, les échecs annoncés tels quels, dits non vérifiés par le programme.
|
|
70
|
+
*/
|
|
71
|
+
function findings(l, red, claimed, gated) {
|
|
72
|
+
const said = claimed.length ? `\nLa sonde annonce : ${claimed.join(' ; ')}` : '';
|
|
73
|
+
const out = red.map((k) => ({ ...k, gravite: 'majeur', texte: `${PROBE_FINDING}${k.texte}${said}` }));
|
|
74
|
+
if (gated || !claimed.length)
|
|
75
|
+
return out;
|
|
76
|
+
l.warnings.push('sonde de cas limites : échecs annoncés par la session, non vérifié par le programme (orchestrate.test absent)');
|
|
77
|
+
return [...out, ...claimed.map((e) => ({ source: 'tests', gravite: 'majeur', texte: `${PROBE_FINDING}${e}` }))];
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Joue la sonde (R3 à R9). Elle ne bloque jamais le lot : échec, coupure, délai ou rapport illisible sont un warning et la revue part. Seuls un quota (suspension : la reprise rejoue la sonde),
|
|
81
|
+
* un verrou d'acceptation violé ou un dépôt sale le rendent, comme après toute session d'écriture. Un test rouge nouveau repart en correction ; sinon le cycle reprend (bibliothécaire, revue).
|
|
82
|
+
*/
|
|
83
|
+
export async function probeStep(io) {
|
|
84
|
+
const l = io.lot;
|
|
85
|
+
const done = await io.play();
|
|
86
|
+
if (!done)
|
|
87
|
+
return;
|
|
88
|
+
const step = l.steps[l.steps.length - 1];
|
|
89
|
+
const rep = done === PROBE_FAILED ? cutSession(io, step) : readSession(io, done, step);
|
|
90
|
+
if (rep === 'rendu')
|
|
91
|
+
return;
|
|
92
|
+
// La porte du programme après la sonde comme après toute écriture, mais seulement si elle a commité : sans nouveau commit il n'y a rien de neuf à rejouer.
|
|
93
|
+
const red = [];
|
|
94
|
+
const runs = [];
|
|
95
|
+
const moved = !!step.headBefore && io.head() !== step.headBefore;
|
|
96
|
+
if (moved)
|
|
97
|
+
await io.gate(red, runs);
|
|
98
|
+
const found = moved ? findings(l, red, (rep?.echecs ?? []).map((e) => e.trim()).filter(Boolean), !!io.config.test) : [];
|
|
99
|
+
if (found.length) {
|
|
100
|
+
// Une correction que le budget ne paie plus n'est pas sautée : l'étape fix refuse et le lot revient au lead sans revue, tests rouges (L145, cycle.ts overBudget).
|
|
101
|
+
if (io.pays())
|
|
102
|
+
l.warnings.push(`${PROBE_FINDING}${found.length} constat(s), passe de correction avant la revue`);
|
|
103
|
+
step.constats = found.map((k) => k.texte);
|
|
104
|
+
io.fix(found);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
// Sans nouveau commit, les contrôles de la porte jouée après l'écriture valent toujours pour ce HEAD : la revue les garde (sinon elle relance la suite et le build).
|
|
108
|
+
await io.review(moved ? runs : [...(l.checks?.runs ?? [])]);
|
|
109
|
+
}
|
|
110
|
+
/** La session n'a rien donné (échec, coupure, délai) : un warning, pas de rapport. Ce qu'elle a commité avant d'être coupée passe les mêmes contrôles qu'après un rapport (commits au journal, verrou, fichiers hors tests) ; le dépôt a déjà été remis propre. */
|
|
111
|
+
function cutSession(io, step) {
|
|
112
|
+
unplayed(io.lot, step.cause ?? 'échec');
|
|
113
|
+
if (!step.headBefore || io.head() === step.headBefore)
|
|
114
|
+
return undefined;
|
|
115
|
+
io.take(step, { questions: [] });
|
|
116
|
+
return handedBack(io, [], step.headBefore) ? 'rendu' : undefined;
|
|
117
|
+
}
|
|
118
|
+
/** Le rapport d'une session qui a répondu : questions notées, verrou et dépôt contrôlés. `'rendu'` quand le lot est rendu. */
|
|
119
|
+
function readSession(io, done, step) {
|
|
120
|
+
const rep = readReport(io.lot, done.report);
|
|
121
|
+
for (const q of io.take(step, rep ?? { questions: [] }))
|
|
122
|
+
io.lot.warnings.push(`sonde de cas limites : question sans réponse (la sonde ne bloque pas le lot) : « ${q.trim()} »`);
|
|
123
|
+
return handedBack(io, io.dirt(done), step.headBefore) ? 'rendu' : rep;
|
|
124
|
+
}
|
|
@@ -53,8 +53,17 @@ export const PRECHECK_SCHEMA = {
|
|
|
53
53
|
properties: { dejaPresent: { type: 'string', enum: ['oui', 'partiel', 'non'] }, preuves: strings, resume: str },
|
|
54
54
|
required: ['dejaPresent', 'preuves', 'resume'],
|
|
55
55
|
};
|
|
56
|
+
/** Sortie de la sonde de cas limites (L497) : les tests commités, et `echecs` les tests qu'elle annonce rouges (« test — borne — attendu », un par entrée). */
|
|
57
|
+
export const PROBE_SCHEMA = {
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: { commits: WORK_SCHEMA.properties.commits, echecs: strings, nonVerifie: strings, questions: strings, choix: strings, resume: str },
|
|
60
|
+
required: ['commits', 'echecs', 'nonVerifie', 'questions', 'resume'],
|
|
61
|
+
};
|
|
56
62
|
/** Une revue de maquette rend le même rapport qu'une revue d'ergonomie (REVIEW_SCHEMA). */
|
|
57
|
-
export const schemaFor = (kind) => (kind === 'precheck' ? PRECHECK_SCHEMA : kind === 'spec' ? SPEC_SCHEMA : kind === 'accept' ? ACCEPT_SCHEMA : kind === 'implement' || kind === 'fix' ? WORK_SCHEMA : REVIEW_SCHEMA);
|
|
63
|
+
export const schemaFor = (kind) => (kind === 'precheck' ? PRECHECK_SCHEMA : kind === 'spec' ? SPEC_SCHEMA : kind === 'accept' ? ACCEPT_SCHEMA : kind === 'probe' ? PROBE_SCHEMA : kind === 'implement' || kind === 'fix' ? WORK_SCHEMA : REVIEW_SCHEMA);
|
|
64
|
+
/** L'agent du paquet de chaque étape qui en a un, ou `undefined` pour une écriture sans agent ; les revues et la fiche n'y sont pas : `code-reviewer`. */
|
|
65
|
+
const STEP_AGENT = { implement: undefined, fix: undefined, probe: undefined, spec: 'spec-writer', accept: 'acceptance-writer', ux: 'ux-reviewer', maquette: 'ux-reviewer', precheck: 'precheck-reader' };
|
|
66
|
+
export const agentOf = (kind) => (kind in STEP_AGENT ? STEP_AGENT[kind] : 'code-reviewer');
|
|
58
67
|
/** Contrôle la forme d'une sortie structurée : un champ absent ou mal typé est une erreur nommée. */
|
|
59
68
|
export function checkShape(value, schema) {
|
|
60
69
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
package/dist/perf.js
CHANGED
|
@@ -48,7 +48,7 @@ function planLot(plans, project, lot) {
|
|
|
48
48
|
return lots.find((l) => l.id === lot) ?? (lot.includes('/') ? lots.find((l) => l.id === lot.slice(0, lot.indexOf('/'))) : undefined);
|
|
49
49
|
}
|
|
50
50
|
/** Rang d'affichage d'une sorte d'étape : l'ordre du cycle, les sortes inconnues après. */
|
|
51
|
-
const KIND_ORDER = ['spec', 'maquette', 'fiche', 'precheck', 'implement', 'review', 'review-small', 'ux', 'fix', 'accept'];
|
|
51
|
+
const KIND_ORDER = ['spec', 'maquette', 'fiche', 'precheck', 'implement', 'probe', 'review', 'review-small', 'ux', 'fix', 'accept'];
|
|
52
52
|
const kindRank = (kind) => (KIND_ORDER.includes(kind) ? KIND_ORDER.indexOf(kind) : KIND_ORDER.length);
|
|
53
53
|
/** Part de `counted` rangée en entrée quand une étape ne porte que ce compteur (journal ancien) : le reste est en sortie (R8 b). */
|
|
54
54
|
const COUNTED_INPUT_SHARE = 0.9;
|
package/docs/orchestrate.md
CHANGED
|
@@ -262,7 +262,7 @@ table at the end of the wave, and is the lead's to take into the plan of the oth
|
|
|
262
262
|
|
|
263
263
|
**Effort level per pass** (L137). Each session is launched with `claude -p --effort <level>` (Claude Code 2.1.284; an
|
|
264
264
|
older `claude` is refused at launch unless every pass is `default`). Defaults: `precheck` low (it only reads),
|
|
265
|
-
`implement` and `fix` medium, `review` high (`review-small` follows `review`), `ux` high. A project overrides any of them
|
|
265
|
+
`implement`, `probe` and `fix` medium, `review` high (`review-small` follows `review`), `ux` high. A project overrides any of them
|
|
266
266
|
in `cadence.yaml`; the levels are `low`, `medium`, `high`, `xhigh`, `max`, and `default` sends no `--effort` (the
|
|
267
267
|
level of the session, as before this key). The format-retry session (`--resume`) keeps the level of its pass, and
|
|
268
268
|
`--dry-run` shows the flag in each command line, except for a `precheck: local` session: `claude-local` is launched
|
|
@@ -270,7 +270,7 @@ without `--effort` (see below), so `effort.precheck` has no effect on it and the
|
|
|
270
270
|
|
|
271
271
|
```yaml
|
|
272
272
|
orchestrate:
|
|
273
|
-
effort: { precheck: low, spec: medium, maquette: high, fiche: medium, accept: medium, implement: medium, fix: medium, review: high, ux: high } # defaults
|
|
273
|
+
effort: { precheck: low, spec: medium, maquette: high, fiche: medium, accept: medium, implement: medium, probe: medium, fix: medium, review: high, ux: high } # defaults
|
|
274
274
|
```
|
|
275
275
|
|
|
276
276
|
To measure what a level costs, run the same kind of wave before and after changing the key: each step of
|
|
@@ -535,6 +535,25 @@ except a copy that is still needed, which stays and is said: a copy that a live
|
|
|
535
535
|
The waves that count are those of the launch folders the script knows (`<folder>/.cadence/runs/<wave>/wave.json`): the repository itself and the working directory of every wave the orchestrator's registry still lists (`~/.cadence/orchestrate/waves/<pid>.json`; the registry drops a dead wave when another registers, so it does not know suspended waves by itself). There is no search over the whole disk: a suspended wave launched from another folder is not protected here, and `--resume` repairs its snapshot instead (above). A wave never marked `done` keeps its copy until it is, or until its folder is removed. A failure of the purge (unreadable copy, busy file) once `service` and the links have switched is a warning on the error output (`avertissement : purge des anciennes copies interrompue …`): the new version is in service, the fingerprint is printed, the exit code is 0, the next put in service tries again. The same run updates the **local copy of the `cadence-hud` mod** (L419), the one `CLAUDE_CODE_PLUGIN_DIRS` makes Claude Code load (`~/.claude/mods/cadence-hud`, `--mod-dir` for another folder): `plugins/cadence-hud` of the copy just put in service (not the working repository) is synchronised into it with `rsync -a --delete`, which leaves out `.claude-plugin/types` (generated by Claude Code in the copy). The output says `mod cadence-hud : copie locale mise à jour (<dir>)`. A machine without that folder gets no copy created (`copie locale absente … non mise à jour`), and a failed `rsync` is a warning on the error output (`avertissement : mod cadence-hud non mis à jour …`), exit code 0 like the purge: the version is in service. Before L419 the copy was updated by hand at each release and a forgotten one left the old mod loaded. The first run is also what installs the links in `~/.local/bin`. It prints the fingerprint of the copy
|
|
536
536
|
(bin, dist, package.json).
|
|
537
537
|
|
|
538
|
+
## The edge-case probe (`orchestrate.probe`, L497)
|
|
539
|
+
|
|
540
|
+
Code bugs are the first cause of correction passes, and an edge case is the first sub-cause of those (64 % of the `bug-code` passes since the instruction of L447, 69 % before: writing it in the brief changed nothing). `orchestrate.probe: true` adds a step between the writing and the review: a **fresh Sonnet session** (`probe`, brief `templates/orchestrate/probe.md`, `effort.probe` medium, the delay of a write step `timeouts.work`) that has not written the code, reads the lot's diff, and tries to break it with tests at the edges: empty, absent, twice, wrong type, refused, the real shape of the data, the other callers. The diff is given as a command (`git diff <start sha>..HEAD`), never pasted; the sheet of the lot is named when there is one. Like `implement` and `fix`, the brief gives the program's exact gate (L399: the probe commits tests the gate replays, `tsc` and the quality script included) and the session's machine share (L350). The session writes its tests in new files and commits them all, the passing and the failing, with `probe(L<n>): …`; it does not touch the application code nor the sealed acceptance tests.
|
|
541
|
+
|
|
542
|
+
```yaml
|
|
543
|
+
orchestrate:
|
|
544
|
+
probe: true # true or false; absent = nothing changes (no session, no field in the journal, no line in the output)
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
**When it plays.** Once per lot, after a writing session whose gates are green (tests, build, acceptance) and after the « code sans test » gate, before the librarian and the review; never during the minors pass, never for a lot with no commit. It is recorded in the lot's state (`probe`): a resume does not replay it, except after a quota, where it is the step that was interrupted. `--dry-run` lists the `probe (sonnet)` step between `implement` and `review`.
|
|
548
|
+
|
|
549
|
+
**What a red test does.** After the session the program plays its own gate (`orchestrate.build`, `orchestrate.test`) as after any writing. A test red **now and not at the start sha** (never a failure already there, L213) sends the lot to a correction pass (`fix`) before the review, with a finding of severity *major* whose text starts with `sonde de cas limites : ` and names the test; the probe's own report (`echecs`, « test — edge — expected ») is added to it so the fix session sees the edge. Without `orchestrate.test` there is no gate to read: the failures the session announces are taken as they are, with the warning `sonde de cas limites : échecs annoncés par la session, non vérifié par le programme (orchestrate.test absent)`. All the probe's tests stay in the lot. A test outside `orchestrate.testGate.tests` committed by the probe is a warning that names the files (`sonde de cas limites : la session a modifié du code hors tests (…)`); nothing is removed.
|
|
550
|
+
|
|
551
|
+
**Cost and limits.** The probe's session counts in the lot's tokens and budget. The correction it triggers is one of the two ordinary passes (`MAX_PASSES`): after it, one pass is left for the review, so the librarian is not called (the `bibliothécaire non appelé` warning says so, and the review carries the documentation gap). The probe does not play when a correction pass is already done, or when the budget does not pay the probe (estimated at 65 k tokens, `PROBE_RESERVE`, no measure yet), the correction and the review that follows: the warning `sonde de cas limites non jouée : <n> passe(s) de correction déjà faite(s) ou budget du lot (<count>) insuffisant` says so and the review goes ahead. If the red tests cannot be corrected for lack of budget after the probe, the lot goes back to the lead without a review, with its findings, as for any red tests after a writing (L145). Without a new commit from the probe, the review keeps the results of the gate played after the writing (it does not rerun the suite and the build).
|
|
552
|
+
|
|
553
|
+
**It never blocks the lot.** A failed, cut or timed-out session, or an unreadable report, is the warning `sonde de cas limites non jouée : <cause>` and the review goes ahead; the questions of the report become warnings, nobody waits. A quota suspends the wave like any session and the resume replays the probe. What a cut session committed before the cut goes through the same checks as an answered one (commits in the journal, lock on sealed tests, warning for files outside the tests). What a failed or cut session leaves uncommitted, staged or not (a cut between `git add` and the commit is ordinary), is put back (index unstaged, tracked files restored, new files removed) with a warning that names the files, so a timed-out probe never hands the lot back for a dirty tree. Only a violation of the lock on sealed acceptance tests, or a dirty repository after a session that did answer, hands the lot back, as after any writing session.
|
|
554
|
+
|
|
555
|
+
**Measuring it.** The key is set in cadence's own `cadence.yaml` only, as a trial, and the lead sets it to `true` after the release is in service. Keep or drop it on figures: `cadence corrections <folder> --since <date>` (share of `bug-code` and of `cas limite` among the correction passes, lots with 2 passes or more, L430) and `cadence perf` (the `probe` step has its own row; tokens per lot). The `probe` step keeps the texts of the findings it sent to correction (`constats`, each starting with `sonde de cas limites : `): `cadence corrections` reads them as the findings of the `fix` pass that follows, in place of a review's. The pass is also told apart by the `probe` step that precedes the `fix` step and by the lot's warning `sonde de cas limites : <n> constat(s), passe de correction avant la revue`; the finding given to the fix session starts with `sonde de cas limites : `.
|
|
556
|
+
|
|
538
557
|
## The subject of the plan commits (`plan.commit`, L343)
|
|
539
558
|
|
|
540
559
|
The commits the program writes for the plan in a wave (`plan: <lot> démarré (orchestrate <wave>)`, `… revue de code enregistrée`, `… revue de code notée`, `… hypothèses de la fiche <sujet> notées`, `… mineurs de la revue de <revue> notés`) take the template of the `plan.commit` key of `cadence.yaml`
|
package/docs/whats-new.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
## In two lines
|
|
6
6
|
|
|
7
|
+
- **1.34.1**: republishes 1.34.0, whose npm publication failed after the tag; same features (edge-case probe, steadier `mise-en-service` tests).
|
|
8
|
+
- **1.34.0**: an optional edge-case probe (`orchestrate.probe: true`) tries to break a lot's diff with tests at the edges before the code review; flaky tests of `mise-en-service` made deterministic.
|
|
7
9
|
- **1.33.0**: the lots panel shows each project's progress bar in its « ordre des projets » section, aligned in the desktop app; a simplification audit of the code, and dead exports removed.
|
|
8
10
|
- **1.32.0**: code-quality guard rails in a ratchet (ESLint, sonarjs, knip, jscpd vs a versioned baseline), a recipe for TS, Python, Java, shell; `raf done` refuses a lot whose recorded review is `non conforme`.
|
|
9
11
|
- **1.31.0**: `deliver --sha` refuses, outside a wave too, a sha that carries commits of a lot neither done nor ready (a `non conforme` verdict is not ready); `raf add/note/done` warn when a wave session writes in the main tree.
|
|
@@ -60,6 +62,10 @@ Each is a paragraph in [docs/whats-new.md](whats-new.md) and a full section in [
|
|
|
60
62
|
|
|
61
63
|
The long form of « In two lines » above (the README no longer carries a « What's new » section since L326: it links here). The full history, with the date of each version, is in [CHANGELOG.md](../CHANGELOG.md).
|
|
62
64
|
|
|
65
|
+
**1.34.1**: 1.34.0 was tagged but its publication to npm failed (a CHANGELOG entry cited a lot with no commit in the tag); 1.34.1 publishes the same content, with that entry reworded. Install 1.34.1 to get the edge-case probe and the steadier tests of 1.34.0.
|
|
66
|
+
|
|
67
|
+
**1.34.0**: `cadence orchestrate` can play an edge-case probe between the implementation and the code review (L497, `orchestrate.probe: true`, `docs/orchestrate.md`): a fresh Sonnet session reads the lot's diff, and the sheet when there is one, and writes tests at the edges — empty, absent, twice, wrong type, refused, the real shape of the data, the other callers; a red test sends the lot back to the implementation as a finding before the Opus review, which costs more. The probe's findings never block the lot (a failed or timed-out probe is a warning and the review goes on), while the checks common to every writing session — a dirty repository, the acceptance-test lock — apply to it too. Absent, nothing changes. It answers the « bug de code » bottleneck: edge cases were 64 % of the code-bug correction passes after two written instructions in the implementation brief. The test suite also stops blocking a Vitest worker for more than 60 s: `mise-en-service` compiles once per test file instead of five synchronous `tsc` runs (L480).
|
|
68
|
+
|
|
63
69
|
**1.33.0**: the lots panel (`/hud lots`) shows, on each row of its « ordre des projets » section, the progress bar of the band — closed lots over total, coloured by the share done, the lots in progress and `+N cette semaine` (L481, `docs/cadence-hud.md`); the bars all start at the same column, the project name sitting in a box as wide as the longest name, which a proportional font aligns where spaces did not (L494, L495). Dragging and Alt+↑ / Alt+↓ are unchanged. A read-only simplification audit of the code (L461, `docs/mesures/audit-simplification-l461.md`) ranks dead code, copies and split plans by gain and risk; its first cleanups remove exports nothing imports and a dead type (L483, L484), with no change of behaviour.
|
|
64
70
|
|
|
65
71
|
**1.32.0**: code-quality guard rails for cadence itself and a reference recipe for any project (L462, `docs/qualite.md`): ESLint with typescript-eslint and sonarjs (file and function length, depth, parameters, cyclomatic and cognitive complexity), knip (dead code, unused exports and dependencies) and jscpd (duplicates), compared by `npm run qualite` to a versioned baseline — existing code passes, nothing may get worse, a file a lot touches can only improve; `--migrer` is the admitted path to move or split a file of the baseline, and a changed tool configuration or a new suppression directive is refused. The recipe gives the same ratchet for Python, Java and shell. `raf done` refuses, without `--force`, a lot whose recorded code-review verdict is non-compliant, even when the review covers all its commits (L476, `docs/raf.md`), the predicate `cadence deliver --sha` uses since 1.31.0.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sylad/cadence",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.34.1",
|
|
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",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Edge-case probe for lot `{{lot}}` — "{{titre}}" — of the repository `{{chemin}}`. You are a fresh session: you did not write this code, and the review that follows reads it as it stands. Your job is to try to break it, with tests at its edges, before that review.
|
|
2
|
+
Goal of the lot: {{objectif}}
|
|
3
|
+
{{repos}}
|
|
4
|
+
{{fiche}}
|
|
5
|
+
Commits of the lot:
|
|
6
|
+
{{commits}}
|
|
7
|
+
The diff of the lot is not pasted here: read it with `{{diff}}` (add `--stat` first for a large lot, then read it file by file), and open the code around each change.
|
|
8
|
+
For every function, parser, pattern, threshold or command the diff adds or changes, try the inputs at its edge:
|
|
9
|
+
- empty (empty string, list, object, file, zero);
|
|
10
|
+
- absent (missing key, null, undefined, a file that does not exist);
|
|
11
|
+
- twice (the same item or the same call twice, a duplicate, a repeat);
|
|
12
|
+
- wrong type (a string where a number is expected, an object for a list);
|
|
13
|
+
- refused (a rejected input, a failing dependency, the error path: the exact message and code the lot or its fiche states);
|
|
14
|
+
- the real shape of the data (a real file or a real call, never only a fixture you built to fit the code);
|
|
15
|
+
- other callers of what the diff changed (find them with `grep`; each must still work).
|
|
16
|
+
Rules: write the tests in NEW test files only (never edit an existing test), named after the rule they attack when the lot has a fiche. Run them, and commit them all, the ones that pass and the ones that fail, with explicit paths (never `git add -A` or `commit -a`) and a message that cites the lot (`probe({{lot}}): …`). Do not modify the application code: you find defects, you do not fix them. Do not modify any existing test, and never an `acceptation-<lot>.json` or the tests it seals. Do not push, deliver, run `raf done`, `raf ux` or `raf review`. Read the project's CLAUDE.md first.
|
|
17
|
+
Report in `echecs` one entry per test of yours that fails, written « <test name> — <the edge tried> — <what was expected> »; a test that passes is not listed. Leave `echecs` empty when none fails. What you could not try goes under `nonVerifie`. A question is only for a real blocker: the program writes it down and does not wait for an answer.
|
|
18
|
+
{{gate}}
|
|
19
|
+
{{workers}}
|
|
20
|
+
{{tmp}}
|