@sylad/cadence 1.33.0 → 1.35.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/NOTICE +28 -0
- package/README.md +2 -2
- package/agents/qa-reviewer.md +5 -4
- package/agents/spec-writer.md +2 -0
- package/agents/ux-reviewer.md +28 -0
- package/dist/cli.js +13 -29
- package/dist/config.js +16 -30
- package/dist/corrections-sans-code.js +12 -0
- package/dist/corrections.js +4 -5
- package/dist/deliver.js +4 -4
- package/dist/gate-config.js +47 -0
- package/dist/orchestrate/accept.js +5 -2
- package/dist/orchestrate/baseline.js +57 -0
- package/dist/orchestrate/briefs.js +5 -1
- package/dist/orchestrate/command.js +6 -6
- package/dist/orchestrate/cycle.js +119 -140
- package/dist/orchestrate/gate-notes.js +27 -0
- package/dist/orchestrate/gate-parts.js +135 -0
- package/dist/orchestrate/gate-place.js +66 -0
- package/dist/orchestrate/precheck.js +79 -0
- package/dist/orchestrate/probe.js +124 -0
- package/dist/orchestrate/schemas.js +10 -1
- package/dist/orchestrate/state.js +1 -1
- package/dist/orchestrate/workers.js +31 -4
- package/dist/perf.js +1 -1
- package/dist/plan-commit.js +4 -2
- package/dist/plan-uncommitted.js +26 -0
- package/dist/release.js +9 -7
- package/dist/remote-gate.js +278 -0
- package/dist/verify-gate-remote.js +55 -0
- package/dist/verify-gate.js +37 -28
- package/dist/yaml-keys.js +6 -0
- package/docs/corrections.md +4 -0
- package/docs/deliver.md +4 -2
- package/docs/news.md +2 -4
- package/docs/orchestrate.md +40 -2
- package/docs/overview.md +1 -0
- package/docs/raf.md +3 -3
- package/docs/releasing.md +16 -7
- package/docs/remote-gate.md +89 -0
- package/docs/skills.md +5 -2
- package/docs/whats-new.md +9 -0
- package/package.json +3 -1
- package/skills/lead/SKILL.md +1 -1
- package/templates/orchestrate/probe.md +20 -0
- package/templates/orchestrate/spec.md +3 -1
|
@@ -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,14 +10,19 @@ 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';
|
|
16
18
|
import { repoShas, repoWork } from '../repos.js';
|
|
17
19
|
import { isOpen, RafError } from '../plan.js';
|
|
18
20
|
import { isPlanOnly } from '../audit.js';
|
|
19
|
-
import { compareBaseline, failedTests
|
|
20
|
-
import {
|
|
21
|
+
import { compareBaseline, failedTests } from './baseline.js';
|
|
22
|
+
import { noteTight, noteUnstable } from './gate-notes.js';
|
|
23
|
+
import { TEST_WORKERS_ENV, testWorkersText } from './workers.js';
|
|
24
|
+
import { baselineCandidates, gateLabel, gateReplayable, gateUnnamed, redAloneGate, redCandidates, replayGate, runGate } from './gate-parts.js';
|
|
25
|
+
import { withPlace } from './gate-place.js';
|
|
21
26
|
import { APP_TIMEOUT_S, startApp } from './app.js';
|
|
22
27
|
import { capturesText, gateText, headline, integrationText, localReading, newsText, objective, renderBrief, reposText, shotsText, TEMPLATES_DIR, tiersText, tmpText } from './briefs.js';
|
|
23
28
|
import { SlotAbandoned } from './registry.js';
|
|
@@ -25,7 +30,7 @@ import { journalTokens, peakContext, mcpServersFor, playwrightDir, runSession, t
|
|
|
25
30
|
import { cleanPlaywrightOutput, pushed, remoteAdvances, snapshot } from './guard.js';
|
|
26
31
|
import { sumTokens } from './result.js';
|
|
27
32
|
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';
|
|
33
|
+
import { ACCEPT_SCHEMA, agentOf, checkShape, PRECHECK_SCHEMA, REVIEW_SCHEMA, schemaFor, SPEC_SCHEMA, WORK_SCHEMA } from './schemas.js';
|
|
29
34
|
import { acceptationFile, acceptCommands, isAcceptList, acceptText, checkAcceptation, findAcceptation, isAcceptationFile, lockViolations, lotStartHead, parseAcceptation, planAcceptRuns, redSha, resealedRouge, sealAcceptation, setVert } from './accept.js';
|
|
30
35
|
import { lotIds, referenceIdsText } from '../ref/coverage.js';
|
|
31
36
|
import { avalFicheText, ficheContracts, ficheRules, hasScreenPart } from './aval.js';
|
|
@@ -194,33 +199,49 @@ const startSha = (c) => lotStartHead(wd(c), lotWork(histPlan(c), wd(c), c.lot.lo
|
|
|
194
199
|
* pour que les paquets de workspace résolvent dans le worktree (`mirrorNodeModules`). Avec `orchestrate.testReplay`, un rouge nommé y est rejoué seul (L354) avant d'être retenu.
|
|
195
200
|
* Null quand le worktree ne peut être monté : l'appelant garde alors les tests pour rouges.
|
|
196
201
|
*/
|
|
197
|
-
async function baselineFailures(c,
|
|
202
|
+
async function baselineFailures(c, test) {
|
|
198
203
|
const l = c.lot;
|
|
199
204
|
const sha = startSha(c);
|
|
205
|
+
const command = gateLabel(test); // clé de cache ; pour des parties (L512) le libellé « dir : run ; … », stable d'une lecture à l'autre
|
|
200
206
|
if (!sha)
|
|
201
207
|
return null;
|
|
202
208
|
if (l.baseline?.verified && l.baseline.sha === sha && l.baseline.command === command)
|
|
203
209
|
return l.baseline.failures;
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
try {
|
|
207
|
-
git(l.repo, 'worktree', 'add', '--detach', '-f', dir, sha);
|
|
208
|
-
links.push(...linkIgnored(l.repo, dir));
|
|
209
|
-
// Le build d'abord, comme dans la porte (L333) : les tests lisent ce qu'il produit (`dist`), le départ sans lui rougirait tout. Un build rouge au départ
|
|
210
|
-
// ne dit rien des tests : pas de ligne de base, l'appelant garde les tests pour rouges.
|
|
211
|
-
if (c.config.build && (await sh(c, c.config.build, 30 * 60_000, {}, dir)).code !== 0)
|
|
212
|
-
return null;
|
|
213
|
-
const r = await sh(c, command, 30 * 60_000, {}, dir);
|
|
210
|
+
return atStartSha(c, sha, async (place) => {
|
|
211
|
+
const r = await runGate(place.run, test);
|
|
214
212
|
// Ligne de base comprise (L354) : un rouge du sha de départ est rejoué seul dans le même worktree avant d'être retenu. Sous charge, un test
|
|
215
|
-
// qui passe seul au départ rougirait ici et excuserait une vraie régression du lot (« déjà rouge au départ »). Un échec sans nom reste tel quel.
|
|
213
|
+
// qui passe seul au départ rougirait ici et excuserait une vraie régression du lot (« déjà rouge au départ »). Un échec sans nom reste tel quel ; en parties (L512), partie par partie.
|
|
216
214
|
const found = r.code === 0 ? [] : failedTests(r.output);
|
|
217
|
-
const named = c.config.testReplay && found.length
|
|
218
|
-
const { red: failures, unstable } = named.length ? await
|
|
215
|
+
const named = gateReplayable(test, c.config.testReplay) && found.length ? baselineCandidates(r) : [];
|
|
216
|
+
const { red: failures, unstable } = named.length ? await replayGate(place.run, c.config.testReplay, named, r) : { red: found, unstable: [] };
|
|
219
217
|
if (unstable.length)
|
|
220
218
|
c.wave.log(`${lotKey(l.project, l.lot)} · ligne de base ${sha.slice(0, 7)} : ${unstable.length} test(s) rouge(s) sous charge, vert(s) rejoué(s) seul(s), non retenu(s) comme déjà rouge(s) : ${unstable.join(' ; ')}`);
|
|
221
219
|
l.baseline = { sha, command, failures, verified: true };
|
|
222
220
|
save(c);
|
|
223
221
|
return failures;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
/** Parmi les tests rouges `names` du lot, ceux rouges SEULS aussi au sha de départ (L510) : la suite entière du départ a pu les voir verts (ordre, charge), ils n'en sont pas moins déjà rouges avant le lot. Vide sans `testReplay`, sans sha de départ, build du départ rouge ou worktree non monté. */
|
|
225
|
+
async function redAtStart(c, names, ran) {
|
|
226
|
+
const sha = startSha(c);
|
|
227
|
+
return !sha || !gateReplayable(c.config.test, c.config.testReplay) || !names.length ? [] : (await atStartSha(c, sha, (place) => redAloneGate(place.run, c.config.testReplay, names, ran))) ?? [];
|
|
228
|
+
}
|
|
229
|
+
/** Joue `fn` dans un worktree jetable du sha de départ (ligne de base L213, rouges au départ L510), liens vers les dossiers ignorés posés puis retirés avec lui. Null si le worktree ne se monte pas, si le build du départ est rouge ou si `fn` lève. */
|
|
230
|
+
async function atStartSha(c, sha, fn) {
|
|
231
|
+
const l = c.lot;
|
|
232
|
+
const dir = mkdtempSync(join(tmpdir(), 'cadence-baseline-'));
|
|
233
|
+
const links = [];
|
|
234
|
+
try {
|
|
235
|
+
git(l.repo, 'worktree', 'add', '--detach', '-f', dir, sha);
|
|
236
|
+
links.push(...linkIgnored(l.repo, dir));
|
|
237
|
+
// Sur l'hôte `gate.host` quand il y en a un (L403) ; le worktree local reste le lieu du rejeu si la connexion tombe.
|
|
238
|
+
return await withPlace({ config: c.config, repo: l.repo, sha, dir, needClean: false, label: `porte du lot ${l.lot}`, detail: `(ligne de base ${sha.slice(0, 7)})`, local: (cmd) => sh(c, cmd, 30 * 60_000, {}, dir), say: (line) => c.wave.log(`${lotKey(l.project, l.lot)} · ${line}`) }, async (place) => {
|
|
239
|
+
// Le build d'abord, comme dans la porte (L333) : les tests lisent ce qu'il produit (`dist`), le départ sans lui rougirait tout. Un build rouge au départ
|
|
240
|
+
// ne dit rien des tests : rien à rendre, l'appelant garde les tests pour rouges.
|
|
241
|
+
if (c.config.build && (await place.run(c.config.build)).code !== 0)
|
|
242
|
+
return null;
|
|
243
|
+
return fn(place);
|
|
244
|
+
});
|
|
224
245
|
}
|
|
225
246
|
catch {
|
|
226
247
|
return null;
|
|
@@ -244,9 +265,10 @@ async function baselineFailures(c, command) {
|
|
|
244
265
|
}
|
|
245
266
|
}
|
|
246
267
|
/** Des tests rouges comparés à la ligne de base (L213) : tous déjà rouges au départ, ou au moins un nouveau, ou rien de comparable. */
|
|
247
|
-
async function againstBaseline(c, command,
|
|
248
|
-
const now = failedTests(output);
|
|
249
|
-
|
|
268
|
+
async function againstBaseline(c, command, ran) {
|
|
269
|
+
const now = failedTests(ran.output);
|
|
270
|
+
// Un échec sans test nommé (en chaîne, ou dans une partie rouge, L512) : rien n'est comparable ni excusable, les tests restent rouges.
|
|
271
|
+
if (!now.length || gateUnnamed(ran))
|
|
250
272
|
return { kind: 'unknown' };
|
|
251
273
|
const base = await baselineFailures(c, command);
|
|
252
274
|
if (!base?.length)
|
|
@@ -485,6 +507,8 @@ function avalOf(c, kind) {
|
|
|
485
507
|
// la revue courte d'un petit lot visible est sa seule passe : elle porte aussi la conformité à la maquette
|
|
486
508
|
return kind === 'review-small' && l.small && l.visible && screen ? `${text}\n${avalFicheText('ux', o)}` : text;
|
|
487
509
|
}
|
|
510
|
+
/** 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. */
|
|
511
|
+
const writesCode = (kind) => kind === 'implement' || kind === 'fix' || kind === 'probe';
|
|
488
512
|
/** Consigne de la part de machine de la session (L350) ; vide quand la vague n'en a pas calculé. */
|
|
489
513
|
const workersText = (c) => (c.wave.testWorkers ? testWorkersText(c.wave.testWorkers) : '');
|
|
490
514
|
function briefFor(c, kind, local = false) {
|
|
@@ -492,7 +516,7 @@ function briefFor(c, kind, local = false) {
|
|
|
492
516
|
const l = c.lot;
|
|
493
517
|
const lot = plan.lot(l.lot);
|
|
494
518
|
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
|
|
519
|
+
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
520
|
if (l.visible) {
|
|
497
521
|
const pwDir = playwrightDir(c.wave.store.lotDir(l.project, l.lot));
|
|
498
522
|
vars.news = newsText(l.lot, pwDir);
|
|
@@ -504,6 +528,8 @@ function briefFor(c, kind, local = false) {
|
|
|
504
528
|
// 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
529
|
if (kind === 'ux' || (kind === 'review-small' && l.small && l.visible))
|
|
506
530
|
vars.ux = [vars.ux, referenceIdsText('ux', lotIds({ ...lot, goal: l.goal ?? lot.goal }))].filter(Boolean).join('\n');
|
|
531
|
+
if (kind === 'probe')
|
|
532
|
+
Object.assign(vars, probeVars(ficheOf(c), lotCommitLines(c), startSha(c), l.lot));
|
|
507
533
|
if (kind === 'implement' || kind === 'fix')
|
|
508
534
|
vars.decisions = decisionsText(humanDecisions(lot, lot.parent ? plan.lots().find((x) => x.id === lot.parent) : undefined));
|
|
509
535
|
if (kind === 'precheck' && local)
|
|
@@ -574,7 +600,7 @@ function briefFor(c, kind, local = false) {
|
|
|
574
600
|
}
|
|
575
601
|
const brief = renderBrief(briefName(l, kind, !!c.integration), vars, c.wave.templates);
|
|
576
602
|
// 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
|
|
603
|
+
return ['implement', 'fix', 'spec', 'accept', 'probe'].includes(kind) ? `${brief}\n${tiersText(l.project, l.lot)}\n` : brief;
|
|
578
604
|
}
|
|
579
605
|
/** Tokens comptés par les sessions d'un lot (un lot repris compte ses étapes d'avant). */
|
|
580
606
|
export const lotSpent = (l) => l.steps.reduce((n, s) => n + (s.tokens?.counted ?? 0), 0);
|
|
@@ -625,8 +651,8 @@ export function budgetStep(l) {
|
|
|
625
651
|
const step = named ?? l.next;
|
|
626
652
|
return step && step !== 'integrate' && WRITE_STEPS.includes(step) ? step : null;
|
|
627
653
|
}
|
|
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));
|
|
654
|
+
/** 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). */
|
|
655
|
+
const fixAffordable = (l, escalating = false, extra = 0) => l.budget === undefined || lotSpent(l) + extra + REVIEW_RESERVE < l.budget - (escalating ? 0 : heldForEscalation(l));
|
|
630
656
|
/** Cause d'une suspension demandée par `--stop-after-current` (L79). */
|
|
631
657
|
export const STOP_REQUESTED = 'arrêt demandé (--stop-after-current)';
|
|
632
658
|
/** Le lot a-t-il été écarté de la vague par `--drop` (L79) ? */
|
|
@@ -677,11 +703,11 @@ async function session(c, kind, local = false) {
|
|
|
677
703
|
const halt = halted(w, l);
|
|
678
704
|
if (halt)
|
|
679
705
|
return suspend(c, halt);
|
|
680
|
-
const write = kind === '
|
|
706
|
+
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
707
|
// 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 (
|
|
708
|
+
if (WRITE_STEPS.includes(kind) && (lotOver(l) || (kind === 'fix' && !fixAffordable(l))))
|
|
683
709
|
return overBudget(c, kind === 'fix' && !lotOver(l));
|
|
684
|
-
const model =
|
|
710
|
+
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
711
|
const effort = c.config.effort[kind === 'review-small' ? 'review' : kind];
|
|
686
712
|
const before = await snapshot(wd(c));
|
|
687
713
|
const neighbours = l.repos ?? [];
|
|
@@ -691,7 +717,7 @@ async function session(c, kind, local = false) {
|
|
|
691
717
|
const sessionId = randomUUID();
|
|
692
718
|
const step = { n, kind, model, ...(effort !== 'default' ? { effort } : {}), status: 'running', sessionId, started: new Date().toISOString(), headBefore: before.head ?? undefined };
|
|
693
719
|
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' };
|
|
720
|
+
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
721
|
transition(c, label[kind]);
|
|
696
722
|
w.log(`${lotKey(l.project, l.lot)} · session ${n} ${kind} (${model})`);
|
|
697
723
|
let brief;
|
|
@@ -742,7 +768,7 @@ async function session(c, kind, local = false) {
|
|
|
742
768
|
effort,
|
|
743
769
|
local,
|
|
744
770
|
schema: schemaFor(kind),
|
|
745
|
-
agent: kind === '
|
|
771
|
+
agent: kind === 'fix' && l.docsFix ? 'doc-keeper' : agentOf(kind),
|
|
746
772
|
cwd: wd(c),
|
|
747
773
|
wave: w.id,
|
|
748
774
|
lot: lotKey(l.project, l.lot),
|
|
@@ -882,6 +908,19 @@ async function session(c, kind, local = false) {
|
|
|
882
908
|
return null;
|
|
883
909
|
step.status = 'failed';
|
|
884
910
|
step.cause = outcome.cause;
|
|
911
|
+
if (kind === 'probe') {
|
|
912
|
+
// 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.
|
|
913
|
+
// 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).
|
|
914
|
+
// Un fichier à la fois : un chemin inconnu de l'index (non indexé) fait refuser toute la commande, les autres resteraient indexés.
|
|
915
|
+
const left = dirtBetween(before, await snapshot(wd(c)));
|
|
916
|
+
if (left.length) {
|
|
917
|
+
for (const f of left)
|
|
918
|
+
tryGit(wd(c), ['restore', '--staged', '--', f]);
|
|
919
|
+
restoreWorktree(wd(c), left);
|
|
920
|
+
l.warnings.push(`sonde de cas limites : fichiers non commités laissés par la session retirés (${left.join(', ')})`);
|
|
921
|
+
}
|
|
922
|
+
return PROBE_FAILED;
|
|
923
|
+
}
|
|
885
924
|
if (write && outcome.cause === 'délai dépassé') {
|
|
886
925
|
// 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
926
|
const repos = [{ rel: '', path: wd(c), before }, ...neighbours.map((r, i) => ({ rel: r.rel, path: r.path, before: beforeOthers[i] }))];
|
|
@@ -1282,43 +1321,6 @@ function badReport(c, step, e) {
|
|
|
1282
1321
|
step.cause = e.message;
|
|
1283
1322
|
return stop(c, 'failed', `${step.kind} : ${e.message}`);
|
|
1284
1323
|
}
|
|
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
1324
|
/**
|
|
1323
1325
|
* 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
1326
|
* 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 +1337,7 @@ export function needsPrecheck(config, plan, repo, lot, repos = [], goal) {
|
|
|
1335
1337
|
const text = [l.title, l.goal, goal].filter(Boolean).join('\n');
|
|
1336
1338
|
return citesPriorWork(text, lot, plan.lots().map((x) => x.id));
|
|
1337
1339
|
}
|
|
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
|
-
}
|
|
1340
|
+
export { precheckNothingToFind };
|
|
1376
1341
|
/**
|
|
1377
1342
|
* 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
1343
|
* autre lot). « oui » rend le lot sans ouvrir d'implémentation ; « partiel » joint le constat au brief de l'implémentation.
|
|
@@ -1884,7 +1849,7 @@ async function acceptProblems(c, fiche, json, headBefore) {
|
|
|
1884
1849
|
l.warnings.push(NO_TEST_ACCEPT);
|
|
1885
1850
|
}
|
|
1886
1851
|
else {
|
|
1887
|
-
const ordinary = await sh(c, c.config.test);
|
|
1852
|
+
const ordinary = await runGate((cmd) => sh(c, cmd), c.config.test); // porte en parties (L512) : chaque partie dans son dossier, sortie fusionnée
|
|
1888
1853
|
if (ordinary.code !== 0)
|
|
1889
1854
|
out.push(`orchestrate.test échoue (code ${ordinary.code}) avec les tests rouges commités : les fichiers listés dans ${json} ne sont pas exclus de la config du lanceur (partagée avec la CI) tant que vert est false\n${tail(ordinary.output, 15)}`);
|
|
1890
1855
|
}
|
|
@@ -2035,7 +2000,8 @@ async function acceptGreen(c) {
|
|
|
2035
2000
|
*/
|
|
2036
2001
|
async function programChecks(c, red, runs) {
|
|
2037
2002
|
try {
|
|
2038
|
-
|
|
2003
|
+
// Sur l'hôte `gate.host` (L403) quand il y en a un : le HEAD du worktree du lot, s'il est propre ; sinon, ou sans hôte, en local.
|
|
2004
|
+
await withPlace({ config: c.config, repo: c.lot.repo, sha: tryGit(wd(c), ['rev-parse', 'HEAD']), dir: wd(c), needClean: true, label: `porte du lot ${c.lot.lot}`, local: (cmd) => sh(c, cmd), say: (line) => c.wave.log(`${lotKey(c.lot.project, c.lot.lot)} · ${line}`) }, (place) => runChecks(c, red, runs, place));
|
|
2039
2005
|
}
|
|
2040
2006
|
finally {
|
|
2041
2007
|
// L395 : la porte se lit dans l'état du lot tant qu'une commande (ou le rejeu qui la suit) tourne, jamais après.
|
|
@@ -2043,19 +2009,19 @@ async function programChecks(c, red, runs) {
|
|
|
2043
2009
|
save(c);
|
|
2044
2010
|
}
|
|
2045
2011
|
}
|
|
2046
|
-
async function runChecks(c, red, runs) {
|
|
2012
|
+
async function runChecks(c, red, runs, place) {
|
|
2047
2013
|
const l = c.lot;
|
|
2048
2014
|
for (const [label, command] of [['build', c.config.build], ['tests', c.config.test]]) {
|
|
2049
2015
|
if (!command)
|
|
2050
2016
|
continue;
|
|
2051
2017
|
l.gate = { label, started: new Date().toISOString() };
|
|
2052
2018
|
save(c);
|
|
2053
|
-
const r = await
|
|
2054
|
-
const run = { label, command, code: r.code };
|
|
2019
|
+
const r = await runGate(place.run, command); // porte en parties (L512) : toutes jouées, chacune dans son dossier ; le CheckRun porte le libellé « dir : run ; … »
|
|
2020
|
+
const run = { label, command: gateLabel(command), code: r.code, ...place.stamp() };
|
|
2055
2021
|
runs.push(run);
|
|
2056
2022
|
if (r.code === 0)
|
|
2057
2023
|
continue;
|
|
2058
|
-
const vs = label === 'tests' ? await againstBaseline(c, command, r
|
|
2024
|
+
const vs = label === 'tests' ? await againstBaseline(c, command, r) : { kind: 'unknown' };
|
|
2059
2025
|
const onlyExisting = (n) => {
|
|
2060
2026
|
run.dejaRouges = n;
|
|
2061
2027
|
const note = `${n} test(s) déjà rouge(s) au sha de départ ${(l.baseline?.sha ?? '').slice(0, 7)}, pas un défaut du lot : listés à part, la revue a lieu`;
|
|
@@ -2068,11 +2034,11 @@ async function runChecks(c, red, runs) {
|
|
|
2068
2034
|
}
|
|
2069
2035
|
// Un rouge absent de la ligne de base est rejoué SEUL avant d'être compté (L354) : sous charge, des tests qui passent seuls rougissent ensemble.
|
|
2070
2036
|
// Sans `orchestrate.testReplay` rien n'est rejoué : le rouge est compté tel quel et rien n'est tenu pour instable (L354).
|
|
2071
|
-
const
|
|
2072
|
-
const
|
|
2073
|
-
const { red: still, unstable } = named.length ? await replayAlone(c, replay, named, r.output) : { red: [], unstable: [] };
|
|
2037
|
+
const named = label !== 'tests' || !gateReplayable(c.config.test, c.config.testReplay) ? [] : vs.kind === 'new' ? vs.nouveaux : redCandidates(r);
|
|
2038
|
+
const { red: still, unstable, tight } = named.length ? await replayGate(place.run, c.config.testReplay, named, r) : { red: [], unstable: [], tight: [] };
|
|
2074
2039
|
if (unstable.length) {
|
|
2075
|
-
noteUnstable(c, unstable);
|
|
2040
|
+
noteUnstable(c, unstable.filter((t) => !tight.some((k) => k.test === t)));
|
|
2041
|
+
noteTight(c, tight);
|
|
2076
2042
|
run.instables = unstable;
|
|
2077
2043
|
}
|
|
2078
2044
|
if (unstable.length && !still.length) {
|
|
@@ -2082,37 +2048,30 @@ async function runChecks(c, red, runs) {
|
|
|
2082
2048
|
run.code = 0;
|
|
2083
2049
|
continue;
|
|
2084
2050
|
}
|
|
2051
|
+
// L510 : un rouge qui reste rouge seul dans le lot mais que la suite entière du départ avait vu vert (ordre, charge) peut être rouge seul au départ aussi : déjà rouge, ni correction ni escalade.
|
|
2052
|
+
const atStart = still.length ? await redAtStart(c, still, r) : [];
|
|
2053
|
+
if (atStart.length) {
|
|
2054
|
+
const note = `${atStart.length} test(s) déjà rouge(s) au sha de départ ${startSha(c)?.slice(0, 7)} (rouge(s) seuls aussi, absent(s) de la ligne de base), pas un défaut du lot : ${atStart.join(' ; ')}`;
|
|
2055
|
+
if (!l.warnings.includes(note))
|
|
2056
|
+
l.warnings.push(note);
|
|
2057
|
+
if (atStart.length === still.length) {
|
|
2058
|
+
run.dejaRouges = atStart.length + (vs.kind === 'new' ? vs.existants : 0);
|
|
2059
|
+
continue;
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
const fresh = still.filter((t) => !atStart.includes(t));
|
|
2085
2063
|
const alone = unstable.length ? `\n(${unstable.length} test(s) rouge(s) sous charge, verts rejoués seuls : instables, à ne pas corriger)` : '';
|
|
2086
2064
|
if (vs.kind === 'new') {
|
|
2087
|
-
const left = vs.nouveaux.filter((t) => !unstable.includes(t));
|
|
2088
|
-
red.push({ source: 'tests', gravite: 'bloquant', texte: `${command} en échec (code ${r.code}) : ${left.length} test(s) rouge(s) qui ne l'étaient pas au sha de départ :\n${left.map((t) => `- ${t}`).join('\n')}${vs.existants ? `\n(${vs.existants} autre(s) test(s) déjà rouge(s) au sha de départ : à ne pas corriger)` : ''}${alone}` });
|
|
2065
|
+
const left = vs.nouveaux.filter((t) => !unstable.includes(t) && !atStart.includes(t));
|
|
2066
|
+
red.push({ source: 'tests', gravite: 'bloquant', texte: `${run.command} en échec (code ${r.code}) : ${left.length} test(s) rouge(s) qui ne l'étaient pas au sha de départ :\n${left.map((t) => `- ${t}`).join('\n')}${vs.existants ? `\n(${vs.existants} autre(s) test(s) déjà rouge(s) au sha de départ : à ne pas corriger)` : ''}${alone}` });
|
|
2089
2067
|
return;
|
|
2090
2068
|
}
|
|
2091
2069
|
// Des instables sont écartés : la sortie de la suite entière les montre encore en FAIL, le constat ne nomme que ceux qui restent rouges.
|
|
2092
|
-
const shown = unstable.length ? `${
|
|
2093
|
-
red.push({ source: 'tests', gravite: 'bloquant', texte: `${command} en échec (code ${r.code}) :\n${shown}${alone}` });
|
|
2070
|
+
const shown = unstable.length || atStart.length ? `${fresh.length} test(s) rouge(s) après rejeu seul :\n${fresh.map((t) => `- ${t}`).join('\n')}` : r.output.trim().split('\n').slice(-40).join('\n');
|
|
2071
|
+
red.push({ source: 'tests', gravite: 'bloquant', texte: `${run.command} en échec (code ${r.code}) :\n${shown}${alone}` });
|
|
2094
2072
|
return;
|
|
2095
2073
|
}
|
|
2096
2074
|
}
|
|
2097
|
-
/**
|
|
2098
|
-
* Rejoue SEULS (la commande `orchestrate.testReplay` où `{files}` devient les fichiers de ces tests, dans `cwd` : le worktree du lot, ou celui de la ligne de base) les tests rouges d'une suite entière (L354). Le 26-10, vitest à 31 workers par suite sur une machine à 4 sessions rougissait des tests
|
|
2099
|
-
* qui passaient seuls : quatre corrections vides (4 à 11 min chacune). Un test vert à un rejeu est `unstable` ; rouge aux rejeux, ou sans fichier désignable (rien à rejouer seul), il reste dans `red`. Le rejeu lui-même est `replayRed` (workers.ts), partagé avec `verify --gate` (L387).
|
|
2100
|
-
*/
|
|
2101
|
-
function replayAlone(c, template, names, output, cwd = wd(c)) {
|
|
2102
|
-
return replayRed((command) => sh(c, command, undefined, undefined, cwd), template, names, output);
|
|
2103
|
-
}
|
|
2104
|
-
/** Trace des tests instables sous charge (L354) : le lot (`instables`, journal et `cadence perf`), un avertissement du lot (tableau de fin de vague) et une ligne du journal de la vague. */
|
|
2105
|
-
function noteUnstable(c, tests) {
|
|
2106
|
-
const l = c.lot;
|
|
2107
|
-
const fresh = tests.filter((t) => !(l.instables ?? []).includes(t));
|
|
2108
|
-
if (!fresh.length)
|
|
2109
|
-
return;
|
|
2110
|
-
l.instables = [...(l.instables ?? []), ...fresh];
|
|
2111
|
-
const text = `${fresh.length} test(s) instable(s) sous charge (rouge(s) dans la suite, vert(s) rejoué(s) seul(s)), pas de passe de correction : ${fresh.join(' ; ')}`;
|
|
2112
|
-
l.warnings.push(text);
|
|
2113
|
-
c.wave.log(`${lotKey(l.project, l.lot)} · ${text}`);
|
|
2114
|
-
save(c);
|
|
2115
|
-
}
|
|
2116
2075
|
/**
|
|
2117
2076
|
* Contrôles du sommet intégré (L328) : un rouge est relancé une fois avant de rendre le lot. Le 25-10, L326 a été rendu pour un rouge de charge (L325 tournait en même
|
|
2118
2077
|
* temps) alors que le même sommet rejoué seul était vert. Rouge deux fois = rendu, avec le résultat de la seconde passe ; vert à la relance = une alerte sur le lot et une ligne du journal.
|
|
@@ -2144,6 +2103,8 @@ export function redExcerpt(k) {
|
|
|
2144
2103
|
const more = named.length > EXCERPT_LINES ? [`… (+${named.length - EXCERPT_LINES})`] : [];
|
|
2145
2104
|
return [head.replace(/\s*:$/, ''), ...shown, ...more].join(' ; ');
|
|
2146
2105
|
}
|
|
2106
|
+
/** Violations du verrou des tests d'acceptation (L173) par les commits faits depuis `head` ; aucune sans HEAD de départ. */
|
|
2107
|
+
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
2108
|
/** Étape d'écriture (implémentation, correction) : contrôles, puis étape suivante décidée. */
|
|
2148
2109
|
async function work(c, kind) {
|
|
2149
2110
|
const l = c.lot;
|
|
@@ -2206,7 +2167,7 @@ async function work(c, kind) {
|
|
|
2206
2167
|
save(c);
|
|
2207
2168
|
// 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
2169
|
// 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 =
|
|
2170
|
+
const locked = sealedViolations(c, done.step.headBefore);
|
|
2210
2171
|
if (locked.length) {
|
|
2211
2172
|
stop(c, 'handed-back', `verrou des tests d'acceptation : ${locked.join(' ; ')}`);
|
|
2212
2173
|
return;
|
|
@@ -2322,7 +2283,7 @@ async function work(c, kind) {
|
|
|
2322
2283
|
const gateGreen = runs.some((r) => r.label === 'tests' && r.code === 0);
|
|
2323
2284
|
const baseOnly = declared.length > 0 && !!newFailures && !newFailures.length;
|
|
2324
2285
|
if (deferred && red.length === 0 && gateGreen && baseOnly)
|
|
2325
|
-
l.warnings.push(`tests annoncés rouges par la session, mais la porte du programme verte (${c.config.test}, code 0) et aucun nouvel échec déclaré (nouveauxEchecs vide) : les échecs sont ceux de la base qu'elle déclare (${declared.length}) ; pas de correction`);
|
|
2286
|
+
l.warnings.push(`tests annoncés rouges par la session, mais la porte du programme verte (${gateLabel(c.config.test ?? '')}, code 0) et aucun nouvel échec déclaré (nouveauxEchecs vide) : les échecs sont ceux de la base qu'elle déclare (${declared.length}) ; pas de correction`);
|
|
2326
2287
|
else if (deferred && red.length === 0 && !runs.some((r) => r.label === 'tests' && (r.dejaRouges || r.instables?.length)))
|
|
2327
2288
|
red.push(claimed);
|
|
2328
2289
|
if (red.length) {
|
|
@@ -2350,7 +2311,18 @@ async function work(c, kind) {
|
|
|
2350
2311
|
}
|
|
2351
2312
|
}
|
|
2352
2313
|
}
|
|
2353
|
-
//
|
|
2314
|
+
// Sonde de cas limites (L497) : une fois par lot, après la porte « code sans test », avant le bibliothécaire et la revue.
|
|
2315
|
+
if (scheduleProbe(c.config, l, { skip: minorsPass || !alreadyDone, late: l.pass >= MAX_PASSES - 1, pays: fixAffordable(l, false, PROBE_RESERVE), count: budgetCount(l) })) {
|
|
2316
|
+
// 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.
|
|
2317
|
+
l.checks = runs.length ? { head: git(wd(c), 'rev-parse', 'HEAD'), tree: git(wd(c), 'rev-parse', 'HEAD^{tree}'), runs } : undefined;
|
|
2318
|
+
return save(c);
|
|
2319
|
+
}
|
|
2320
|
+
await toReview(c, runs, minorsPass);
|
|
2321
|
+
}
|
|
2322
|
+
/** 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. */
|
|
2323
|
+
async function toReview(c, runs, minorsPass) {
|
|
2324
|
+
const l = c.lot;
|
|
2325
|
+
// 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
2326
|
if (!l.docsGate && !minorsPass) {
|
|
2355
2327
|
const { gaps } = docGaps(c);
|
|
2356
2328
|
if (gaps.length) {
|
|
@@ -2372,6 +2344,11 @@ async function work(c, kind) {
|
|
|
2372
2344
|
l.next = firstReview(c);
|
|
2373
2345
|
save(c);
|
|
2374
2346
|
}
|
|
2347
|
+
/** Ce que la sonde (probe.ts) emprunte au cycle. */
|
|
2348
|
+
const probeIo = (c) => ({
|
|
2349
|
+
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),
|
|
2350
|
+
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(),
|
|
2351
|
+
});
|
|
2375
2352
|
/** 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
2353
|
const noCommitNote = (l) => (l.warnings.some((w) => w.startsWith('passe des mineurs sans commit')) ? ' sans commit' : '');
|
|
2377
2354
|
/** Revue de code qui clôt le cycle : complète, ou courte une fois la passe des mineurs jouée. */
|
|
@@ -3104,6 +3081,8 @@ export async function runLot(c) {
|
|
|
3104
3081
|
await acceptStep(c);
|
|
3105
3082
|
else if (kind === 'implement' || kind === 'fix')
|
|
3106
3083
|
await work(c, kind);
|
|
3084
|
+
else if (kind === 'probe')
|
|
3085
|
+
await probeStep(probeIo(c));
|
|
3107
3086
|
else if (kind === 'integrate')
|
|
3108
3087
|
await integrateLot(c);
|
|
3109
3088
|
else
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Ce que la porte de tests dit d'un rouge qu'elle n'envoie pas en correction : instable sous charge (L354), délai trop juste (L510).
|
|
2
|
+
import { lotKey } from './state.js';
|
|
3
|
+
/** Trace des tests instables sous charge (L354) : le lot (`instables`, journal et `cadence perf`), un avertissement du lot (tableau de fin de vague) et une ligne du journal de la vague. */
|
|
4
|
+
export function noteUnstable(c, tests) {
|
|
5
|
+
const l = c.lot;
|
|
6
|
+
const fresh = tests.filter((t) => !(l.instables ?? []).includes(t));
|
|
7
|
+
if (!fresh.length)
|
|
8
|
+
return;
|
|
9
|
+
l.instables = [...(l.instables ?? []), ...fresh];
|
|
10
|
+
const text = `${fresh.length} test(s) instable(s) sous charge (rouge(s) dans la suite, vert(s) rejoué(s) seul(s)), pas de passe de correction : ${fresh.join(' ; ')}`;
|
|
11
|
+
l.warnings.push(text);
|
|
12
|
+
c.wave.log(`${lotKey(l.project, l.lot)} · ${text}`);
|
|
13
|
+
c.wave.store.writeLot(l);
|
|
14
|
+
}
|
|
15
|
+
const seconds = (ms) => `${Math.round(ms / 100) / 10} s`.replace(/\.0 s$/, ' s');
|
|
16
|
+
/** Trace des tests au « délai trop juste » (L510) : rouges dans la suite pour avoir expiré, verts seuls mais près de leur délai. Le lot (`delaisJustes`) et un avertissement qui nomme chaque test, sa durée seul et son délai ; jamais de passe de correction. */
|
|
17
|
+
export function noteTight(c, tests) {
|
|
18
|
+
const l = c.lot;
|
|
19
|
+
const fresh = tests.filter((t) => !(l.delaisJustes ?? []).some((k) => k.test === t.test));
|
|
20
|
+
if (!fresh.length)
|
|
21
|
+
return;
|
|
22
|
+
l.delaisJustes = [...(l.delaisJustes ?? []), ...fresh];
|
|
23
|
+
const text = `délai trop juste, pas un bug (pas de passe de correction) : ${fresh.map((t) => `${t.test} (${seconds(t.ms)} seul pour ${seconds(t.limit)} de délai)`).join(' ; ')} — relever le délai du test`;
|
|
24
|
+
l.warnings.push(text);
|
|
25
|
+
c.wave.log(`${lotKey(l.project, l.lot)} · ${text}`);
|
|
26
|
+
c.wave.store.writeLot(l);
|
|
27
|
+
}
|