@sylad/cadence 1.32.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.
@@ -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 === 'implement' || kind === 'fix' ? workersText(c) : '', gate: kind === 'implement' || kind === 'fix' ? gateText(c.config) : '', repos: reposText(kind === 'implement' || kind === 'fix' ? 'write' : 'read', l.lot, l.repos ?? []), integration: c.integration ? integrationText(c.integration.base) : '' };
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 kind === 'implement' || kind === 'fix' || kind === 'spec' || kind === 'accept' ? `${brief}\n${tiersText(l.project, l.lot)}\n` : brief;
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 === 'implement' || kind === 'fix' || kind === 'spec' || kind === 'accept';
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 (write && (lotOver(l) || (kind === 'fix' && !fixAffordable(l))))
688
+ if (WRITE_STEPS.includes(kind) && (lotOver(l) || (kind === 'fix' && !fixAffordable(l))))
683
689
  return overBudget(c, kind === 'fix' && !lotOver(l));
684
- const model = kind === 'spec' || kind === 'accept' || kind === 'fiche' ? '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
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 === 'spec' ? 'spec-writer' : kind === 'accept' ? 'acceptance-writer' : kind === 'ux' || kind === 'maquette' ? 'ux-reviewer' : kind === 'precheck' ? 'precheck-reader' : kind === 'fix' && l.docsFix ? 'doc-keeper' : write ? undefined : 'code-reviewer',
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 = done.step.headBefore ? lockViolations(wd(c), c.config.spec.dir, done.step.headBefore, { rouge: l.accept?.rouge ? { [l.accept.acceptation]: l.accept.rouge } : {}, lot: l.lot, lotBase: l.steps.find((s) => s.headBefore)?.headBefore, lotStart: lotStartHead(wd(c), lotWork(histPlan(c), wd(c), l.lot).map((k) => k.sha)), lotCommits: lotCommits(histPlan(c), wd(c), l.lot).map((k) => k.sha) }) : [];
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
- // 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 ci-dessus. Si elle ne touche pas le document, la revue part et porte l'écart (brief docs.sync, constat majeur).
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;
@@ -4,13 +4,13 @@ import { tmpdir } from 'node:os';
4
4
  import { isAbsolute, join, resolve } from 'node:path';
5
5
  import { tryGit } from './git.js';
6
6
  /** Gabarit par défaut du message d'un commit de plan écrit dans une vague (L343 : inchangé depuis toujours). */
7
- export const PLAN_COMMIT_WAVE = 'plan: {lot} {action} (orchestrate {vague})';
7
+ const PLAN_COMMIT_WAVE = 'plan: {lot} {action} (orchestrate {vague})';
8
8
  /** Gabarit par défaut hors vague (`spec reseal`) : pas de vague à citer. */
9
- export const PLAN_COMMIT_PLAIN = 'plan: {lot} {action}';
9
+ const PLAN_COMMIT_PLAIN = 'plan: {lot} {action}';
10
10
  /** Gabarit par défaut du commit de la fiche sourcée par la réponse du lead : type `docs`, déjà conventionnel (L343). */
11
- export const LEAD_ANSWER_COMMIT = 'docs({lot}): {action} (orchestrate {vague})';
11
+ const LEAD_ANSWER_COMMIT = 'docs({lot}): {action} (orchestrate {vague})';
12
12
  /** Les mots que le gabarit `plan.commit` de cadence.yaml peut employer. */
13
- export const PLAN_COMMIT_FIELDS = ['lot', 'action', 'vague'];
13
+ const PLAN_COMMIT_FIELDS = ['lot', 'action', 'vague'];
14
14
  /** Ce que cadence écrit dans un commit de plan, par action : une seule source pour la vague et pour le dry-run. */
15
15
  export const PLAN_ACTIONS = {
16
16
  started: 'démarré',
package/dist/plan.js CHANGED
@@ -24,7 +24,7 @@ export function setPlanNotice(fn) {
24
24
  notice = fn;
25
25
  }
26
26
  export const STATUSES = ['todo', 'doing', 'done', 'dropped'];
27
- export const GATE_INVALID = 'gate invalide : count: entier ≥ 1 et un critère spec: true ou screen: page attendus';
27
+ const GATE_INVALID = 'gate invalide : count: entier ≥ 1 et un critère spec: true ou screen: page attendus';
28
28
  export class RafError extends Error {
29
29
  }
30
30
  /** Un nom de ressource partagée (L251) : un mot de lettres minuscules, chiffres, « - » et « _ ». */
@@ -32,7 +32,7 @@ export const RESOURCE_NAME = /^[a-z0-9_-]+$/;
32
32
  export const RESOURCES_BAD_LIST = 'doit être une liste de noms de ressources';
33
33
  export const invalidResource = (name) => `ressource invalide : ${name === '' ? '(vide)' : name} (lettres minuscules, chiffres, - et _)`;
34
34
  /** Les noms de ressources de `--uses` ou de `raf uses` : chacun valide (`RafError` sinon), sans doublon, dans l'ordre donné. */
35
- export function cleanUses(names) {
35
+ function cleanUses(names) {
36
36
  for (const n of names)
37
37
  if (!RESOURCE_NAME.test(n))
38
38
  throw new RafError(invalidResource(n));
@@ -54,7 +54,7 @@ export function escapeRe(s) {
54
54
  */
55
55
  const DOTTED = '\\.\\w';
56
56
  /** Matches `L3` or `L3/t1` as whole words. Group 1 = lot id, group 2 = task id. */
57
- export function refPattern(prefix) {
57
+ function refPattern(prefix) {
58
58
  return new RegExp(`(?<![\\w/])(${escapeRe(prefix)}\\d+)(?:/(t\\d+))?(?!\\w|${DOTTED})`, 'g');
59
59
  }
60
60
  export function extractRefs(text, prefix) {
package/dist/proc.js CHANGED
@@ -124,7 +124,7 @@ export function processStart(pid) {
124
124
  /** Âge au-delà duquel un relevé de secours (le relevé suivant a échoué) n'est plus cru : un pid y a pu être repris. */
125
125
  export const MAX_SNAPSHOT_AGE_MS = 1_000;
126
126
  /** Intervalle du relevé des descendants d'une commande de deliver pendant qu'elle tourne. */
127
- export const TRACK_INTERVAL_MS = 200;
127
+ const TRACK_INTERVAL_MS = 200;
128
128
  /**
129
129
  * Suivi continu de l'arbre d'une commande : toutes les TRACK_INTERVAL_MS, ses descendants sont relevés et
130
130
  * chacun est retenu avec son heure de démarrage. Un processus vu une fois reste suivi même si son parent
package/dist/ref/add.js CHANGED
@@ -7,7 +7,7 @@ import { DPI, extractImages, imageFile, imageRegions, listImages, pageCount, ren
7
7
  import { readDocument, writeDocument } from './store.js';
8
8
  import { requireTools } from './tools.js';
9
9
  /** Sans `--sections`, un document de cette taille ou plus petit est rendu en entier ; au-delà, seul l'index est construit. */
10
- export const WHOLE_DOCUMENT_MAX = 30;
10
+ const WHOLE_DOCUMENT_MAX = 30;
11
11
  /** Une page de moins de 20 caractères de texte est tenue pour scannée (recette du skill norme-etude). */
12
12
  const SCANNED_MAX_CHARS = 20;
13
13
  /** [1, 2, 3, 6] → « 1-3, 6 ». */
@@ -23,7 +23,7 @@ function ranges(pages) {
23
23
  return out.join(', ');
24
24
  }
25
25
  /** `3-5,9` → [3, 4, 5, 9] (pages du PDF, 1 = première), sans doublon, dans l'ordre. */
26
- export function parseSections(text, pages) {
26
+ function parseSections(text, pages) {
27
27
  const out = new Set();
28
28
  for (const part of text.split(',')) {
29
29
  const m = /^\s*(\d+)(?:\s*-\s*(\d+))?\s*$/.exec(part);
@@ -9,9 +9,9 @@ import { troisiemeRegard } from './regard.js';
9
9
  import { sessionDeRef } from './session.js';
10
10
  /** Plafond déclaré d'une lecture : au plus 20 pages rendues lues d'un coup (design §5, « Taille »). Au-delà, `--sections` en choisit moins. */
11
11
  export const LECTURE_PAGES_MAX = 20;
12
- export const ANALYSE_MODELS = ['sonnet', 'opus', 'haiku'];
12
+ const ANALYSE_MODELS = ['sonnet', 'opus', 'haiku'];
13
13
  /** Sortie structurée de la session : ce que l'agent dit de sa lecture ; le programme ne s'y fie pas, il relit le fichier. */
14
- export const ANALYSE_SCHEMA = {
14
+ const ANALYSE_SCHEMA = {
15
15
  type: 'object',
16
16
  properties: {
17
17
  resume: { type: 'string', description: 'Two sentences: what the document is and what the reading holds.' },
@@ -20,7 +20,7 @@ export function lotIds(lot) {
20
20
  return idsInText([lot.title, lot.goal ?? '', ...lot.notes.map((n) => n.text).filter((t) => !isProgramNote(t))].join('\n'));
21
21
  }
22
22
  /** Les éléments que la fiche d'un lot justifie comme non pris (`<id> : <raison>` dans `sujet.perimetre.hors_perimetre`), par id. */
23
- export function nonPris(repo, specDir, lotId) {
23
+ function nonPris(repo, specDir, lotId) {
24
24
  const out = new Map();
25
25
  const doc = fichesDuDossier(repo, specDir).find(({ doc }) => doc.sujet?.lot === lotId)?.doc;
26
26
  const sujet = doc?.sujet;
@@ -64,7 +64,7 @@ function where(e) {
64
64
  * Calcule la couverture d'un inventaire valide par les lots du plan (fait, abandonné compté pour rien) et par ses exclusions écrites. Un lot fait dont la fiche
65
65
  * justifie un élément comme non pris (`justifies`) ne le couvre pas : l'élément reste un écart tant que le lead ne l'a pas exclu par écrit ou fait par un autre lot.
66
66
  */
67
- export function computeCoverage(inventaire, lots, justifies = () => new Map()) {
67
+ function computeCoverage(inventaire, lots, justifies = () => new Map()) {
68
68
  const elements = inventaire.elements;
69
69
  const known = new Set(elements.map((e) => e.id));
70
70
  const byId = new Map();
package/dist/ref/diff.js CHANGED
@@ -305,7 +305,7 @@ export function reprendreIds(lecture, precedent) {
305
305
  lecture.exclusions = precedent.exclusions;
306
306
  }
307
307
  /** L'inventaire que `--out` va remplacer, s'il en est un de valide ; sinon rien (un fichier illisible ou d'une autre forme n'est pas un inventaire précédent). */
308
- export function inventairePrecedent(path) {
308
+ function inventairePrecedent(path) {
309
309
  if (!existsSync(path))
310
310
  return undefined;
311
311
  try {