@sylad/cadence 1.8.0 → 1.10.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 +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +27 -54
- package/agents/code-reviewer.md +2 -2
- package/agents/doc-keeper.md +19 -0
- package/agents/precheck-reader.md +1 -1
- package/agents/qa-reviewer.md +2 -2
- package/agents/reference-analyst.md +25 -0
- package/agents/reference-verifier.md +19 -0
- package/agents/spec-writer.md +1 -1
- package/agents/ux-reviewer.md +1 -1
- package/bin/cadence.js +9 -1
- package/dist/audit.js +16 -4
- package/dist/check.js +24 -2
- package/dist/cli.js +258 -30
- package/dist/config.js +101 -5
- package/dist/corrections.js +24 -2
- package/dist/deliver.js +125 -37
- package/dist/docsync.js +19 -3
- package/dist/git.js +16 -3
- package/dist/lead.js +45 -12
- package/dist/migrate.js +166 -0
- package/dist/orchestrate/briefs.js +46 -6
- package/dist/orchestrate/changelog-merge.js +94 -0
- package/dist/orchestrate/command.js +358 -41
- package/dist/orchestrate/cycle.js +922 -285
- package/dist/orchestrate/guard.js +2 -2
- package/dist/orchestrate/integrate.js +129 -0
- package/dist/orchestrate/launch.js +48 -6
- package/dist/orchestrate/mutex.js +30 -0
- package/dist/orchestrate/no-test.js +41 -0
- package/dist/orchestrate/pool.js +74 -36
- package/dist/orchestrate/registry.js +146 -14
- package/dist/orchestrate/result.js +1 -1
- package/dist/orchestrate/snapshot.js +54 -1
- package/dist/orchestrate/spec.js +89 -5
- package/dist/orchestrate/state.js +56 -15
- package/dist/orchestrate/table.js +74 -3
- package/dist/orchestrate/timeouts.js +40 -0
- package/dist/orchestrate/worktree.js +368 -0
- package/dist/perf.js +188 -25
- package/dist/plan.js +112 -7
- package/dist/proc.js +6 -0
- package/dist/ready-free.js +109 -0
- package/dist/ref/add.js +129 -0
- package/dist/ref/analyse.js +220 -0
- package/dist/ref/check.js +338 -0
- package/dist/ref/command.js +92 -0
- package/dist/ref/corpus-render.js +83 -0
- package/dist/ref/corpus.js +201 -0
- package/dist/ref/diff.js +262 -0
- package/dist/ref/evaluate.js +241 -0
- package/dist/ref/figures.js +252 -0
- package/dist/ref/measure.js +330 -0
- package/dist/ref/pdf.js +116 -0
- package/dist/ref/png.js +125 -0
- package/dist/ref/regard.js +102 -0
- package/dist/ref/session.js +34 -0
- package/dist/ref/store.js +40 -0
- package/dist/ref/tools.js +51 -0
- package/dist/resources.js +51 -0
- package/dist/session.js +5 -2
- package/dist/state.js +75 -7
- package/dist/testgate.js +15 -0
- package/docs/cadence-hud.md +12 -0
- package/docs/cycle-vague-dark.svg +1 -1
- package/docs/cycle-vague-light.svg +1 -1
- package/docs/deliver.md +76 -5
- package/docs/fonctionnement-wave-dark.svg +1 -1
- package/docs/fonctionnement-wave-light.svg +1 -1
- package/docs/fonctionnement.md +6 -6
- package/docs/lead.md +40 -13
- package/docs/news.md +30 -1
- package/docs/orchestrate.md +275 -33
- package/docs/overview.md +65 -1
- package/docs/perf.md +14 -8
- package/docs/raf.md +90 -11
- package/docs/ref.md +415 -0
- package/docs/releasing.md +3 -3
- package/docs/session.md +2 -2
- package/docs/skills.md +21 -2
- package/docs/spec.md +1 -1
- package/docs/stabilite.md +4 -4
- package/docs/whats-new.md +30 -1
- package/package.json +2 -1
- package/skills/deliver/SKILL.md +6 -1
- package/skills/lead/SKILL.md +28 -10
- package/skills/session-close/SKILL.md +1 -1
- package/skills/session-start/SKILL.md +2 -2
- package/templates/inventaire.json +96 -0
- package/templates/orchestrate/accept-back.md +1 -0
- package/templates/orchestrate/accept.md +1 -0
- package/templates/orchestrate/fiche.md +1 -0
- package/templates/orchestrate/fix-docs.md +18 -0
- package/templates/orchestrate/fix-minors.md +2 -1
- package/templates/orchestrate/fix.md +5 -2
- package/templates/orchestrate/implement.md +7 -4
- package/templates/orchestrate/maquette.md +2 -1
- package/templates/orchestrate/precheck.md +2 -1
- package/templates/orchestrate/review-recheck.md +2 -0
- package/templates/orchestrate/review-small.md +5 -1
- package/templates/orchestrate/review.md +2 -0
- package/templates/orchestrate/spec-back.md +2 -1
- package/templates/orchestrate/spec.md +4 -3
- package/templates/orchestrate/ux.md +3 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { PACKAGE_ROOT } from '../orchestrate/snapshot.js';
|
|
4
|
+
import { decodePng, encodePng } from './png.js';
|
|
5
|
+
import { sessionDeRef } from './session.js';
|
|
6
|
+
/** Marge (pixels de la page rendue) ajoutée de chaque côté de la zone d'un élément dans sa découpe : de quoi lire ce qui l'entoure. */
|
|
7
|
+
export const MARGE_DECOUPE = 24;
|
|
8
|
+
/** Sortie structurée du troisième regard : un verdict par numéro d'élément ; le programme ne retient que les verdicts valides. */
|
|
9
|
+
export const REGARD_SCHEMA = {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
decisions: {
|
|
13
|
+
type: 'array',
|
|
14
|
+
items: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
numero: { type: 'integer', description: 'The number of the element in the brief.' },
|
|
18
|
+
verdict: { type: 'string', enum: ['confirme', 'ecarte'] },
|
|
19
|
+
raison: { type: 'string', description: 'One sentence: what the crop shows.' },
|
|
20
|
+
},
|
|
21
|
+
required: ['numero', 'verdict', 'raison'],
|
|
22
|
+
additionalProperties: false,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
questions: { type: 'array', items: { type: 'string' }, description: 'What a human must settle.' },
|
|
26
|
+
},
|
|
27
|
+
required: ['decisions', 'questions'],
|
|
28
|
+
additionalProperties: false,
|
|
29
|
+
};
|
|
30
|
+
/** Découpe la zone d'un élément (marge comprise, bornée à la page) dans `crops/regard-<numero>.png` ; null quand la page n'est pas rendue ou que la zone est hors d'elle. */
|
|
31
|
+
function decouper(docDir, e, numero, pages) {
|
|
32
|
+
const page = join(docDir, 'pages', `page-${e.source.page}.png`);
|
|
33
|
+
const z = e.source.zone;
|
|
34
|
+
if (!existsSync(page) || !z)
|
|
35
|
+
return null;
|
|
36
|
+
if (!pages.has(e.source.page))
|
|
37
|
+
pages.set(e.source.page, decodePng(readFileSync(page), `page ${e.source.page}`)); // une page se décode une fois, quel que soit le nombre de ses éléments
|
|
38
|
+
const r = pages.get(e.source.page);
|
|
39
|
+
const x0 = Math.max(0, z.x - MARGE_DECOUPE);
|
|
40
|
+
const y0 = Math.max(0, z.y - MARGE_DECOUPE);
|
|
41
|
+
const x1 = Math.min(r.width, z.x + z.l + MARGE_DECOUPE);
|
|
42
|
+
const y1 = Math.min(r.height, z.y + z.h + MARGE_DECOUPE);
|
|
43
|
+
if (x1 <= x0 || y1 <= y0)
|
|
44
|
+
return null;
|
|
45
|
+
const rgba = Buffer.alloc((x1 - x0) * (y1 - y0) * 4);
|
|
46
|
+
for (let y = y0; y < y1; y++)
|
|
47
|
+
r.data.copy(rgba, (y - y0) * (x1 - x0) * 4, (y * r.width + x0) * 4, (y * r.width + x1) * 4);
|
|
48
|
+
const dir = join(docDir, 'crops');
|
|
49
|
+
mkdirSync(dir, { recursive: true });
|
|
50
|
+
const file = join(dir, `regard-${numero}.png`);
|
|
51
|
+
writeFileSync(file, encodePng(x1 - x0, y1 - y0, rgba));
|
|
52
|
+
return file;
|
|
53
|
+
}
|
|
54
|
+
/** Ce qu'une lecture a écrit d'un élément, sans ce qui renvoie à sa propre numérotation ni à sa confiance : le troisième regard juge ce qui est dit, pas qui le dit. */
|
|
55
|
+
const POSE = (e) => JSON.stringify(Object.fromEntries(Object.entries(e).filter(([k]) => !['id', 'confiance', 'cause', 'ocr', 'usage', 'ecran', 'cible', 'couvre'].includes(k))));
|
|
56
|
+
/**
|
|
57
|
+
* Le troisième regard : une session de `reference-verifier` qui ne voit que les éléments d'une seule lecture et leur découpe, et dit pour chacun
|
|
58
|
+
* s'il est confirmé ou écarté. Rend une décision par élément (dans l'ordre de `seuls`) ; un verdict sans raison ou inconnu, ou un numéro
|
|
59
|
+
* hors de la liste, vaut une absence de réponse (`undefined`) ; un numéro répété garde son premier verdict. Un silence du troisième regard n'est jamais un `lu`.
|
|
60
|
+
*/
|
|
61
|
+
export async function troisiemeRegard(o) {
|
|
62
|
+
for (const f of existsSync(join(o.docDir, 'crops')) ? readdirSync(join(o.docDir, 'crops')) : [])
|
|
63
|
+
if (/^regard-\d+\.png$/.test(f))
|
|
64
|
+
rmSync(join(o.docDir, 'crops', f));
|
|
65
|
+
const cadence = `node ${join(PACKAGE_ROOT, 'bin', 'cadence.js')}`;
|
|
66
|
+
const pages = new Map();
|
|
67
|
+
const blocs = o.seuls.map((item, i) => {
|
|
68
|
+
const e = item.element;
|
|
69
|
+
const crop = decouper(o.docDir, e, i + 1, pages);
|
|
70
|
+
return [
|
|
71
|
+
`${i + 1}. ${POSE(e)}`,
|
|
72
|
+
` page : ${join(o.docDir, 'pages', `page-${e.source.page}.png`)}`,
|
|
73
|
+
crop ? ` découpe : ${crop}` : ' pas de découpe : la page n\'est pas rendue ou la zone est hors d\'elle, regarde la page entière',
|
|
74
|
+
].join('\n');
|
|
75
|
+
});
|
|
76
|
+
const brief = [
|
|
77
|
+
`Troisième regard sur le document de référence « ${o.slug} » : ${o.seuls.length} élément${o.seuls.length > 1 ? 's' : ''} qu'un seul des deux lecteurs a écrit${o.seuls.length > 1 ? 's' : ''}.`,
|
|
78
|
+
'',
|
|
79
|
+
`Dossier du document : ${o.docDir}`,
|
|
80
|
+
'',
|
|
81
|
+
...blocs,
|
|
82
|
+
'',
|
|
83
|
+
`Pour chacun : confirme, ou ecarte, avec la raison. Mesurer une zone (pixels de la page rendue) : ${cadence} ref measure ${o.slug} <page> <page | x,y | x,y,l,h> --quoi couleurs|contraste|boites|texte --config ${o.configPath}`,
|
|
84
|
+
`Ne lis que les découpes et les pages ci-dessus ; n'écris aucun fichier.`,
|
|
85
|
+
].join('\n');
|
|
86
|
+
const r = await sessionDeRef({ ...o, agent: 'reference-verifier', brief, schema: REGARD_SCHEMA, echec: 'la session du troisième regard a échoué' });
|
|
87
|
+
if (r.kind === 'quota')
|
|
88
|
+
return r;
|
|
89
|
+
const structured = r.structured;
|
|
90
|
+
const decisions = o.seuls.map(() => undefined);
|
|
91
|
+
for (const d of Array.isArray(structured?.decisions) ? structured.decisions : []) {
|
|
92
|
+
const i = Number(d?.numero) - 1;
|
|
93
|
+
const raison = typeof d?.raison === 'string' ? d.raison.trim() : '';
|
|
94
|
+
if (!Number.isInteger(i) || i < 0 || i >= decisions.length || decisions[i] !== undefined)
|
|
95
|
+
continue;
|
|
96
|
+
if ((d.verdict !== 'confirme' && d.verdict !== 'ecarte') || raison === '')
|
|
97
|
+
continue;
|
|
98
|
+
decisions[i] = { verdict: d.verdict, raison };
|
|
99
|
+
}
|
|
100
|
+
const questions = Array.isArray(structured?.questions) ? structured.questions.map(String) : [];
|
|
101
|
+
return { kind: 'ok', decisions, tokens: r.tokens, session: r.session, questions };
|
|
102
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { readOrchestrateConfig } from '../config.js';
|
|
3
|
+
import { journalTokens, runSession } from '../orchestrate/launch.js';
|
|
4
|
+
import { sumTokens } from '../orchestrate/result.js';
|
|
5
|
+
import { RafError } from '../plan.js';
|
|
6
|
+
/**
|
|
7
|
+
* Une session de la chaîne `ref` hors vague (mêmes sortie JSON, schéma, relance de mise en forme et relance d'une session coupée qu'orchestrate),
|
|
8
|
+
* avec les jetons comptés comme le budget d'orchestrate : ceux de la session coupée (son journal) s'ajoutent à ceux de la relance.
|
|
9
|
+
* Un échec est une `RafError` (code 2), un quota un résultat.
|
|
10
|
+
*/
|
|
11
|
+
export async function sessionDeRef(o) {
|
|
12
|
+
const config = readOrchestrateConfig(o.configPath);
|
|
13
|
+
const outcome = await runSession({
|
|
14
|
+
kind: 'ref',
|
|
15
|
+
sessionId: randomUUID(),
|
|
16
|
+
brief: o.brief,
|
|
17
|
+
model: o.model,
|
|
18
|
+
effort: config.effort.review,
|
|
19
|
+
schema: o.schema,
|
|
20
|
+
agent: o.agent,
|
|
21
|
+
cwd: o.docDir,
|
|
22
|
+
wave: `ref-${o.slug}`,
|
|
23
|
+
permissionMode: config.permissionMode,
|
|
24
|
+
addDirs: [o.docDir],
|
|
25
|
+
timeoutMs: config.timeouts.work,
|
|
26
|
+
}, { claude: o.claude, agents: o.agents, onRelaunch: o.onRelaunch });
|
|
27
|
+
if (outcome.kind === 'quota')
|
|
28
|
+
return { kind: 'quota', message: outcome.message };
|
|
29
|
+
if (outcome.kind === 'failed')
|
|
30
|
+
throw new RafError(`ref analyse : ${o.echec} : ${outcome.cause}`);
|
|
31
|
+
const cut = outcome.relaunched ? journalTokens(o.claudeHome, o.docDir, outcome.relaunched.sessionId) : null;
|
|
32
|
+
const tokens = cut ? sumTokens(cut.tokens, outcome.result.tokens).counted : outcome.result.tokens.counted;
|
|
33
|
+
return { kind: 'ok', structured: outcome.result.structured, tokens, session: outcome.result.sessionId };
|
|
34
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { readReferencesConfig } from '../config.js';
|
|
5
|
+
import { gitRoot } from '../git.js';
|
|
6
|
+
import { RafError } from '../plan.js';
|
|
7
|
+
/**
|
|
8
|
+
* Dossier des documents : `references.dir` de cadence.yaml (relatif à son dossier), sinon `$XDG_DATA_HOME/cadence/references`.
|
|
9
|
+
* Un `references.dir` dans un dépôt Git est refusé : les documents de référence peuvent être privés, ils ne se commitent pas.
|
|
10
|
+
*/
|
|
11
|
+
export function referencesDir(root, configPath, env) {
|
|
12
|
+
const dir = readReferencesConfig(configPath).dir;
|
|
13
|
+
if (dir === undefined)
|
|
14
|
+
return join(env.XDG_DATA_HOME || join(env.HOME || homedir(), '.local', 'share'), 'cadence', 'references');
|
|
15
|
+
const abs = resolve(dirname(configPath), dir);
|
|
16
|
+
let probe = abs;
|
|
17
|
+
while (!existsSync(probe) && dirname(probe) !== probe)
|
|
18
|
+
probe = dirname(probe);
|
|
19
|
+
if (gitRoot(probe))
|
|
20
|
+
throw new RafError(`ref : references.dir (${abs}) est dans un dépôt Git : les documents de référence restent hors du dépôt (choisir un dossier ailleurs, ou retirer la clé pour $XDG_DATA_HOME/cadence/references)`);
|
|
21
|
+
return abs;
|
|
22
|
+
}
|
|
23
|
+
export const documentFile = (docDir) => join(docDir, 'document.json');
|
|
24
|
+
export function readDocument(docDir) {
|
|
25
|
+
const file = documentFile(docDir);
|
|
26
|
+
if (!existsSync(file))
|
|
27
|
+
return null;
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
throw new RafError(`ref : ${file} illisible : ${e.message.split('\n')[0]}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function writeDocument(docDir, doc) {
|
|
36
|
+
mkdirSync(docDir, { recursive: true });
|
|
37
|
+
const tmp = `${documentFile(docDir)}.tmp`;
|
|
38
|
+
writeFileSync(tmp, `${JSON.stringify(doc, null, 2)}\n`);
|
|
39
|
+
renameSync(tmp, documentFile(docDir));
|
|
40
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { accessSync, constants, statSync } from 'node:fs';
|
|
2
|
+
import { delimiter, join } from 'node:path';
|
|
3
|
+
import { RafError } from '../plan.js';
|
|
4
|
+
const POPPLER = 'sudo apt install poppler-utils';
|
|
5
|
+
/** Les outils du système dont `cadence ref` dépend, dans l'ordre où l'erreur les nomme. */
|
|
6
|
+
export const TOOLS = [
|
|
7
|
+
{ name: 'pdftoppm', commands: ['pdftoppm'], install: POPPLER },
|
|
8
|
+
{ name: 'pdftotext', commands: ['pdftotext'], install: POPPLER },
|
|
9
|
+
{ name: 'pdfimages', commands: ['pdfimages'], install: POPPLER },
|
|
10
|
+
{ name: 'pdfinfo', commands: ['pdfinfo'], install: POPPLER },
|
|
11
|
+
{ name: 'pdftohtml', commands: ['pdftohtml'], install: POPPLER },
|
|
12
|
+
{ name: 'magick', commands: ['magick', 'convert'], install: 'sudo apt install imagemagick' },
|
|
13
|
+
{ name: 'tesseract', commands: ['tesseract'], install: 'sudo apt install tesseract-ocr' },
|
|
14
|
+
];
|
|
15
|
+
function executable(file) {
|
|
16
|
+
try {
|
|
17
|
+
if (!statSync(file).isFile())
|
|
18
|
+
return false;
|
|
19
|
+
accessSync(file, constants.X_OK);
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function which(command, path) {
|
|
27
|
+
for (const dir of path.split(delimiter)) {
|
|
28
|
+
if (dir && executable(join(dir, command)))
|
|
29
|
+
return join(dir, command);
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Chemin absolu de chaque outil, cherché dans le PATH de l'environnement de la commande. Un outil absent est nommé avec sa
|
|
35
|
+
* commande d'installation (une ligne par outil), la commande sort en 2 : rien n'est installé d'office.
|
|
36
|
+
*/
|
|
37
|
+
export function requireTools(env) {
|
|
38
|
+
const path = env.PATH ?? '';
|
|
39
|
+
const found = {};
|
|
40
|
+
const missing = [];
|
|
41
|
+
for (const t of TOOLS) {
|
|
42
|
+
const hit = t.commands.map((c) => which(c, path)).find((p) => p !== null);
|
|
43
|
+
if (hit)
|
|
44
|
+
found[t.name] = hit;
|
|
45
|
+
else
|
|
46
|
+
missing.push(` ${t.commands.join(' ou ')} : ${t.install}`);
|
|
47
|
+
}
|
|
48
|
+
if (missing.length > 0)
|
|
49
|
+
throw new RafError(`ref : outil absent du PATH, à installer (rien n'est installé d'office) :\n${missing.join('\n')}`);
|
|
50
|
+
return found;
|
|
51
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { parse } from 'yaml';
|
|
4
|
+
import { isOpen, RafError, RESOURCE_NAME, RESOURCES_BAD_LIST } from './plan.js';
|
|
5
|
+
/**
|
|
6
|
+
* Clé `resources:` du cadence.yaml le plus proche en remontant depuis le dossier parent de `root` : le premier fichier qui porte la clé
|
|
7
|
+
* (même vide) l'emporte. Aucun ne la porte : liste vide, attribuée au cadence.yaml du parent direct. Un fichier illisible ou une clé
|
|
8
|
+
* qui n'est pas une liste de noms valides est une `RafError` nommant le fichier, jamais une liste vide silencieuse.
|
|
9
|
+
*/
|
|
10
|
+
export function readResources(root) {
|
|
11
|
+
const fallback = join(dirname(root), 'cadence.yaml');
|
|
12
|
+
for (let dir = dirname(root);; dir = dirname(dir)) {
|
|
13
|
+
const file = join(dir, 'cadence.yaml');
|
|
14
|
+
if (existsSync(file) && statSync(file).isFile()) {
|
|
15
|
+
let raw;
|
|
16
|
+
try {
|
|
17
|
+
raw = parse(readFileSync(file, 'utf8'));
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
throw new RafError(`${file} illisible : ${e.message.split('\n')[0]}`);
|
|
21
|
+
}
|
|
22
|
+
if (raw != null && typeof raw === 'object' && Object.hasOwn(raw, 'resources')) {
|
|
23
|
+
const list = raw.resources ?? [];
|
|
24
|
+
if (!Array.isArray(list) || !list.every((n) => typeof n === 'string' && RESOURCE_NAME.test(n)))
|
|
25
|
+
throw new RafError(`${file} : resources ${RESOURCES_BAD_LIST}`);
|
|
26
|
+
return { file, names: [...new Set(list)] };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (dirname(dir) === dir)
|
|
30
|
+
return { file: fallback, names: [] };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Un écart par ressource d'un lot ouvert que le cadence.yaml des dossiers parents ne déclare pas. Le fichier n'est lu que si un lot ouvert
|
|
35
|
+
* porte `uses:` : un plan qui n'en porte pas donne le contrôle d'avant, quel que soit l'état du dossier parent.
|
|
36
|
+
*/
|
|
37
|
+
export function resourceIssues(lots, root) {
|
|
38
|
+
const using = lots.filter((l) => isOpen(l.status) && l.uses?.length);
|
|
39
|
+
if (using.length === 0)
|
|
40
|
+
return [];
|
|
41
|
+
let declared;
|
|
42
|
+
try {
|
|
43
|
+
declared = readResources(root);
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
if (e instanceof RafError)
|
|
47
|
+
return [{ message: e.message }];
|
|
48
|
+
throw e;
|
|
49
|
+
}
|
|
50
|
+
return using.flatMap((l) => l.uses.filter((n) => !declared.names.includes(n)).map((n) => ({ message: `${l.id} utilise la ressource « ${n} », non déclarée dans ${declared.file} (resources:)` })));
|
|
51
|
+
}
|
package/dist/session.js
CHANGED
|
@@ -81,6 +81,8 @@ export function lastActivity(lot, commits) {
|
|
|
81
81
|
const days = [commits[0]?.day, lot.started, ...lot.notes.map((n) => n.date)].filter((d) => !!d);
|
|
82
82
|
return days.length ? days.reduce(maxDay) : undefined;
|
|
83
83
|
}
|
|
84
|
+
/** « 0 commit », « 1 commit », « 3 commits » : le zéro se dit, une note de clôture sur un lot sans commit ne se lit pas comme « fini ». */
|
|
85
|
+
export const commitCount = (n) => `${n} commit${n > 1 ? 's' : ''}`;
|
|
84
86
|
const hhmm = (d) => `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
|
85
87
|
/**
|
|
86
88
|
* Fenêtre de la clôture : --since s'il est donné ; sinon depuis la dernière ouverture du
|
|
@@ -114,10 +116,11 @@ export function sessionStart(ctx, opts) {
|
|
|
114
116
|
if (doing.length === 0)
|
|
115
117
|
out(' (rien)');
|
|
116
118
|
for (const l of doing) {
|
|
117
|
-
const
|
|
119
|
+
const commits = all.byLot.get(l.id) ?? [];
|
|
120
|
+
const last = lastActivity(l, commits);
|
|
118
121
|
const idle = last ? diffDays(last, today) : 0;
|
|
119
122
|
const state = last ? `dernière activité ${last}${idle > opts.idle ? `, silencieux depuis ${idle} j` : ''}` : 'aucune activité';
|
|
120
|
-
out(` ${l.id} ${l.title} (${state})`);
|
|
123
|
+
out(` ${l.id} ${l.title} (${commitCount(commits.length)}, ${state})`);
|
|
121
124
|
}
|
|
122
125
|
section(out, 'En attente', gateWaiting(lots, blocked).map(({ lot, text }) => `${lot.id} ${lot.title} (${text})`));
|
|
123
126
|
const recent = period(ctx, opts.since);
|
package/dist/state.js
CHANGED
|
@@ -156,9 +156,48 @@ export function pidAlive(pid) {
|
|
|
156
156
|
export function appendDelivery(dir, day, sha) {
|
|
157
157
|
appendFileSync(join(dir, 'deliveries.log'), `${day} ${sha}\n`);
|
|
158
158
|
}
|
|
159
|
-
/**
|
|
160
|
-
export
|
|
161
|
-
|
|
159
|
+
/** Classe d'une livraison rouge : où elle a cassé. `refus` : le script a refusé avant d'agir (code 2). */
|
|
160
|
+
export const FAIL_CLASSES = ['refus', 'ci', 'deploiement', 'verification'];
|
|
161
|
+
const TAIL_SEP = ' ⏎ ';
|
|
162
|
+
const TAIL_LINES = 5;
|
|
163
|
+
const LINE_MAX = 200;
|
|
164
|
+
const REASON_MAX = 300;
|
|
165
|
+
// eslint-disable-next-line no-control-regex
|
|
166
|
+
const ANSI = /\u001b\[[0-9;?]*[ -/]*[@-~]/g;
|
|
167
|
+
// eslint-disable-next-line no-control-regex
|
|
168
|
+
const CONTROL = /[\u0000-\u001f\u007f]/g;
|
|
169
|
+
const MASK = '***';
|
|
170
|
+
const SECRET_WORDS = new Set(['token', 'secret', 'password', 'passwd', 'pwd', 'pass', 'key', 'credential', 'credentials', 'auth', 'authorization', 'signature', 'sig', 'jwt', 'cookie']);
|
|
171
|
+
// Mots sensibles qui, collés à un autre (APIKEY, ACCESSTOKEN, GITHUBTOKEN), terminent encore un nom : pas « pass », « sig » ou « auth » (BYPASS, ...).
|
|
172
|
+
const SECRET_SUFFIXES = ['token', 'secret', 'password', 'passwd', 'key', 'credential', 'credentials', 'signature', 'pwd'];
|
|
173
|
+
// Un nom de variable ou de paramètre d'URL est sensible quand l'un de ses mots (séparés par _ - . ou une majuscule) en est un, ou finit par l'un des suffixes.
|
|
174
|
+
const sensitiveName = (name) => name
|
|
175
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
176
|
+
.split(/[_.-]/)
|
|
177
|
+
.some((w) => SECRET_WORDS.has(w.toLowerCase()) || SECRET_SUFFIXES.some((x) => w.toLowerCase().endsWith(x)));
|
|
178
|
+
const TOKEN_SHAPES = /\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|glpat-[A-Za-z0-9_-]{16,}|sk-[A-Za-z0-9_-]{16,}|xox[abprs]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|npm_[A-Za-z0-9]{30,})\b/g;
|
|
179
|
+
const BEARER = /\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi;
|
|
180
|
+
const URL_USERINFO = /(\b[a-z][a-z0-9+.-]*:\/\/)[^\s/@]+@/gi;
|
|
181
|
+
const ASSIGNMENT = /([A-Za-z][A-Za-z0-9_.-]*)=("[^"]*"|'[^']*'|[^\s&"']+)/g;
|
|
182
|
+
/** Masque ce qui ressemble à un secret (jeton connu, Bearer, identifiants d'URL, VAR=valeur ou paramètre d'URL au nom sensible) : motifs probables, pas une garantie. */
|
|
183
|
+
export const maskSecrets = (line) => line
|
|
184
|
+
.replace(BEARER, `$1 ${MASK}`)
|
|
185
|
+
.replace(URL_USERINFO, `$1${MASK}@`)
|
|
186
|
+
.replace(TOKEN_SHAPES, MASK)
|
|
187
|
+
.replace(ASSIGNMENT, (m, name) => (sensitiveName(name) ? `${name}=${MASK}` : m));
|
|
188
|
+
const clean = (line, max) => {
|
|
189
|
+
const t = maskSecrets(line.replace(ANSI, '').replace(CONTROL, ' ')).replaceAll('⏎', ' ').trim();
|
|
190
|
+
return t.length > max ? `${t.slice(0, max - 1)}…` : t;
|
|
191
|
+
};
|
|
192
|
+
/**
|
|
193
|
+
* Ajoute une livraison rouge à `deliveries-failed.log` : une ligne « jour sha [classe] raison ⏎ ligne ⏎ ligne ».
|
|
194
|
+
* La raison est la première ligne du message d'échec ; `tail` (sortie de la commande en échec) est borné à ses
|
|
195
|
+
* 5 dernières lignes non vides de 200 caractères, sans séquence ANSI, secrets probables masqués (raison comprise).
|
|
196
|
+
*/
|
|
197
|
+
export function appendFailedDelivery(dir, day, sha, reason, cls, tail = []) {
|
|
198
|
+
const kept = tail.flatMap((l) => clean(l, LINE_MAX) || []).slice(-TAIL_LINES);
|
|
199
|
+
const head = `${day} ${sha} ${cls ? `[${cls}] ` : ''}${clean(reason.split('\n')[0], REASON_MAX)}`;
|
|
200
|
+
appendFileSync(join(dir, 'deliveries-failed.log'), `${[head, ...kept].join(TAIL_SEP)}\n`);
|
|
162
201
|
}
|
|
163
202
|
export function lastDelivery(dir) {
|
|
164
203
|
const file = join(dir, 'deliveries.log');
|
|
@@ -179,15 +218,44 @@ export function readDeliveries(dir) {
|
|
|
179
218
|
return null;
|
|
180
219
|
}
|
|
181
220
|
}
|
|
182
|
-
/**
|
|
183
|
-
|
|
221
|
+
/** Classe d'une ligne écrite avant la classe [cls] (L282), déduite du libellé que `deliver` écrit lui-même ; null si le libellé n'est pas l'un des siens. */
|
|
222
|
+
function classFromReason(reason) {
|
|
223
|
+
const script = /^script de livraison en échec \((?:code (\d+)|[^)]*)\)/.exec(reason);
|
|
224
|
+
if (script)
|
|
225
|
+
return script[1] === '2' ? 'refus' : 'deploiement';
|
|
226
|
+
if (/^(?:déploiement en échec|CADENCE_DEPLOYED_FILE )/.test(reason))
|
|
227
|
+
return 'deploiement';
|
|
228
|
+
if (/^(?:CI en échec|CI non terminée|gh run list en échec|aucun run CI pour ce sha)/.test(reason))
|
|
229
|
+
return 'ci';
|
|
230
|
+
if (/^vérification en échec/.test(reason))
|
|
231
|
+
return 'verification';
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
/** Les livraisons rouges de `deliveries-failed.log`, dans l'ordre du fichier ; une ligne illisible est ignorée, un fichier absent ou illisible donne aucune livraison rouge. Lecture seule : ne crée rien. */
|
|
235
|
+
export function readFailedEntries(dir) {
|
|
184
236
|
try {
|
|
185
237
|
return readFileSync(join(dir, 'deliveries-failed.log'), 'utf8')
|
|
186
238
|
.split('\n')
|
|
187
|
-
.
|
|
188
|
-
.
|
|
239
|
+
.flatMap((line) => {
|
|
240
|
+
const [head = '', ...tail] = line.split(TAIL_SEP);
|
|
241
|
+
const m = /^(\S+)(?: (\S+))?(?: (.*))?$/.exec(head);
|
|
242
|
+
if (!m || !isDay(m[1]))
|
|
243
|
+
return [];
|
|
244
|
+
let reason = m[3] ?? '';
|
|
245
|
+
const c = /^\[(\w+)\] ?/.exec(reason);
|
|
246
|
+
const written = c && FAIL_CLASSES.includes(c[1]) ? c[1] : null;
|
|
247
|
+
if (written)
|
|
248
|
+
reason = reason.slice(c[0].length);
|
|
249
|
+
// Sans [cls] écrit : le fichier n'est jamais réécrit, la classe se déduit du libellé à la lecture.
|
|
250
|
+
const cls = written ?? (c ? null : classFromReason(reason));
|
|
251
|
+
return [{ day: m[1], sha: m[2] ?? '', cls, reason, tail }];
|
|
252
|
+
});
|
|
189
253
|
}
|
|
190
254
|
catch {
|
|
191
255
|
return [];
|
|
192
256
|
}
|
|
193
257
|
}
|
|
258
|
+
/** Les jours de `deliveries-failed.log` (une ligne par livraison rouge), dans l'ordre du fichier. */
|
|
259
|
+
export function readFailedDeliveries(dir) {
|
|
260
|
+
return readFailedEntries(dir).map((e) => e.day);
|
|
261
|
+
}
|
package/dist/testgate.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { docSyncSet } from './docsync.js';
|
|
2
|
+
export const DEFAULT_TEST_PATTERNS = ['**/*.test.*', '**/*.spec.*', 'test/**', 'tests/**', '**/__tests__/**'];
|
|
3
|
+
/**
|
|
4
|
+
* Les fichiers de code que le lot a modifiés sans toucher le moindre fichier de test (triés), ou rien : un test touché, ou
|
|
5
|
+
* moins de `threshold` fichiers de code. Un fichier de test n'est jamais du code, même quand il correspond à `code`.
|
|
6
|
+
*/
|
|
7
|
+
export function testGateMissing(gate, files) {
|
|
8
|
+
const isTest = docSyncSet(gate.tests);
|
|
9
|
+
if ([...files].some(isTest))
|
|
10
|
+
return [];
|
|
11
|
+
const isCode = docSyncSet(gate.code);
|
|
12
|
+
const isException = gate.except.length ? docSyncSet(gate.except) : () => false;
|
|
13
|
+
const code = [...files].filter((f) => isCode(f) && !isException(f)).sort();
|
|
14
|
+
return code.length >= gate.threshold ? code : [];
|
|
15
|
+
}
|
package/docs/cadence-hud.md
CHANGED
|
@@ -27,6 +27,18 @@ ol ▮▮▮▮▮▯▯▯▯▯ 39/78 · 0 en cours · +3 cette semaine
|
|
|
27
27
|
cadence ▮▮▮▮▮▯▯▯▯▯ 10/20 · 2 en cours : L149 L153
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
The **sessions of the lead** (L276) — the ones started by hand and declared with `cadence lead hold <label>` — are drawn
|
|
31
|
+
under the waves, even when no wave is running: the slots line of `cadence orchestrate --status` and one line per session
|
|
32
|
+
(label, age, time left):
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
sessions du lead : 1 · créneaux libres : 2 sur 3
|
|
36
|
+
correctif-L12 depuis 4 min · reste 56 min
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
With a live wave and no held session the line reads `créneaux libres : 1 sur 2`; with neither, nothing. A session whose
|
|
40
|
+
time limit has run out stops counting in the slots but is not dropped (L278): a row `⚠ session du lead échue : correctif-B28 échue depuis 2 h 10` stays under the others (in red) until `cadence lead release <label>` or a renewed `lead hold`, since nothing stops the session itself. They are read from disk (`holds/` of the cadence home), like the waves.
|
|
41
|
+
|
|
30
42
|
The bar is lots done over lots done + in progress + to do (dropped and recurring lots left out); the count of lots in progress is followed by their ids (`2 en cours : L149 L153`), cut to three then `…` (`5 en cours : L1 L2 L3 …`), the count alone at zero;
|
|
31
43
|
`+N cette semaine` counts the lots created in the last 7 days (today and the six before) and is left out at zero. The source is
|
|
32
44
|
`cadence lead tour <folder> --json` (the folder a wave was launched from, otherwise the session's: a folder of projects gives one line per project, a project with no sub-project gives its own single line),
|
|
@@ -38,5 +38,5 @@
|
|
|
38
38
|
<line x1="981" y1="127" x2="981" y2="68" stroke="#58a6ff" stroke-width="1.2" stroke-dasharray="3 3" marker-end="url(#a)"/>
|
|
39
39
|
<text x="73" y="216" font-size="11.5" fill="#8b949e">dashed: only for a lot with a page or a behaviour to pin down (screen: page, spec: true)</text>
|
|
40
40
|
<text x="661" y="216" font-size="11.5" fill="#8b949e">each step in a fresh session, driven by cadence orchestrate</text>
|
|
41
|
-
<text x="20" y="236" font-size="11.5" fill="#8b949e">Every verdict is written to the plan (
|
|
41
|
+
<text x="20" y="236" font-size="11.5" fill="#8b949e">Every verdict is written to the plan (.cadence/raf.yaml); a delivery counts only when its checks pass.</text>
|
|
42
42
|
</svg>
|
|
@@ -38,5 +38,5 @@
|
|
|
38
38
|
<line x1="981" y1="127" x2="981" y2="68" stroke="#0969da" stroke-width="1.2" stroke-dasharray="3 3" marker-end="url(#a)"/>
|
|
39
39
|
<text x="73" y="216" font-size="11.5" fill="#57606a">dashed: only for a lot with a page or a behaviour to pin down (screen: page, spec: true)</text>
|
|
40
40
|
<text x="661" y="216" font-size="11.5" fill="#57606a">each step in a fresh session, driven by cadence orchestrate</text>
|
|
41
|
-
<text x="20" y="236" font-size="11.5" fill="#57606a">Every verdict is written to the plan (
|
|
41
|
+
<text x="20" y="236" font-size="11.5" fill="#57606a">Every verdict is written to the plan (.cadence/raf.yaml); a delivery counts only when its checks pass.</text>
|
|
42
42
|
</svg>
|
package/docs/deliver.md
CHANGED
|
@@ -43,6 +43,34 @@ cadence deliver # 0 delivered and verified · 1 a step failed · 2
|
|
|
43
43
|
CI can only build what was pushed); no other delivery running (a lock whose
|
|
44
44
|
process died is removed with a warning); with `ci: github`, `gh` installed and
|
|
45
45
|
logged in.
|
|
46
|
+
- **Red delivery log** (L282): a failure after the lock is taken appends one line to
|
|
47
|
+
`<git common dir>/cadence/deliveries-failed.log`: `day sha [class] reason ⏎ line ⏎ line`.
|
|
48
|
+
The class says where it broke: `refus` (the project script exited with code 2 — the
|
|
49
|
+
convention of `deliver` itself for « refused before acting »), `ci`, `deploiement`
|
|
50
|
+
(the project script with any other code, a deploy command, an unusable
|
|
51
|
+
`CADENCE_DEPLOYED_FILE`) or `verification`. The reason is the first line of the failure
|
|
52
|
+
message, followed by the **last 5 non-empty lines** (200 characters each, ANSI sequences
|
|
53
|
+
removed) of the output of the command that failed: the project script, the CI command, the
|
|
54
|
+
deploy command or the check command. A GitHub CI run or an URL check has no output of its
|
|
55
|
+
own, so no line follows. cadence now relays the output of those commands through itself
|
|
56
|
+
(same bytes on its own stdout and stderr, but the command no longer sees a terminal there,
|
|
57
|
+
so it may drop colours or progress bars). A line rewritten with `\r` (a progress bar that
|
|
58
|
+
did not drop) keeps only its last non-empty frame, not 5 frames of the bar. **Probable
|
|
59
|
+
secrets are masked** (`***`) in the reason and in those lines before the file is written:
|
|
60
|
+
known token shapes (GitHub, GitLab, npm, Slack, AWS key id, `sk-…`), `Bearer`/`Basic`
|
|
61
|
+
values, `user:password@` in a URL, and any `NAME=value` (shell variable or URL query
|
|
62
|
+
parameter) whose name has a word such as token, secret, password, key, auth, signature, or
|
|
63
|
+
ends with one of token, secret, password, pwd, key, credential, signature (`APIKEY`,
|
|
64
|
+
`ACCESSTOKEN`, `GITHUBTOKEN`, `MYSQLPWD`) (L294). The 8192 characters kept are the END of the
|
|
65
|
+
output (`OUTPUT_KEPT`, `src/deliver.ts`), so the bound cannot cut the end of a value; it can cut
|
|
66
|
+
its beginning (the name and the opening quote gone), and that case is **not handled** — the log
|
|
67
|
+
stays local. A quoted value never closed is not treated either (L294 tried, and each fix opened a
|
|
68
|
+
new leak): only closed values and unquoted values are matched.
|
|
69
|
+
This is a list of likely patterns, not a guarantee: a secret of another shape printed by a
|
|
70
|
+
command can still land in the log, which stays local (`<git common dir>/cadence/`). `cadence perf` reads classes and projects from it
|
|
71
|
+
([perf](perf.md#delivery-quality-and-the-tour-header-l227)); a line written before L282
|
|
72
|
+
has no `[class]`: it is read by its wording (the one `deliver` writes itself, see
|
|
73
|
+
[perf](perf.md#delivery-quality-and-the-tour-header-l227)) and counts as « non classée » only when the wording is not recognised. The file is never rewritten.
|
|
46
74
|
- Every command is killed when it exceeds its budget (CI, `deployTimeout`, what is
|
|
47
75
|
left of `verifyTimeout`) and reported as "délai dépassé". Killed means the
|
|
48
76
|
whole tree, followed while the command runs: every 200 ms cadence lists the
|
|
@@ -65,6 +93,20 @@ cadence deliver # 0 delivered and verified · 1 a step failed · 2
|
|
|
65
93
|
is still failing when the kill comes (the last listing is not trusted after
|
|
66
94
|
1 second): only the command is killed, its descendants survive, and cadence
|
|
67
95
|
says so once on stderr.
|
|
96
|
+
The 2 seconds are a constant of the real dependencies (`realDeps`, the `graceMs`
|
|
97
|
+
argument), injectable so that the tests measure the kill, not the load of the machine.
|
|
98
|
+
A Ctrl-C goes to the whole foreground group, so the command can die and its
|
|
99
|
+
exit be processed before the signal addressed to cadence: while followed
|
|
100
|
+
processes are still alive after the command exits, or the command itself died
|
|
101
|
+
of a termination signal (SIGINT, SIGTERM, SIGHUP, or exited 128 plus one of
|
|
102
|
+
them: a `curl` killed by the Ctrl-C), cadence waits up to
|
|
103
|
+
2 seconds more (`SIGNAL_PENDING_MS`, the `pendingMs` argument) for that signal,
|
|
104
|
+
and plays the grace above if it comes. Without it a background child that
|
|
105
|
+
ignores Ctrl-C would survive, and a second Ctrl-C would kill cadence without
|
|
106
|
+
releasing the lock. A command that leaves a daemon behind, or that exits 130
|
|
107
|
+
without any signal coming, therefore returns up to 2 seconds later. The termination listener is installed before
|
|
108
|
+
the command is spawned, so a Ctrl-C received while cadence first reads the
|
|
109
|
+
process list is handled too, not killed by the default action.
|
|
68
110
|
A delay overrun and `SIGTERM` give no grace: a script's own `trap` does not
|
|
69
111
|
get to finish there. The lock is held until the followed set is empty: when
|
|
70
112
|
cadence dies of the signal it dies after the tree, and its stale lock, which
|
|
@@ -73,8 +115,9 @@ cadence deliver # 0 delivered and verified · 1 a step failed · 2
|
|
|
73
115
|
a process that leaves the tree before cadence first sees it (a daemon's
|
|
74
116
|
double fork, `setsid`, `nohup … &` from a shell that exits, all within less
|
|
75
117
|
than 200 ms), a process of another user that cadence may not signal (`sudo`),
|
|
76
|
-
a background process still running when the command returns normally (
|
|
77
|
-
|
|
118
|
+
a background process still running when the command returns normally (cadence
|
|
119
|
+
waits up to 2 seconds for a signal on its way, see above, then carries on
|
|
120
|
+
without killing it), and everything if cadence itself is killed
|
|
78
121
|
with `SIGKILL` (`kill -9`, the OOM killer).
|
|
79
122
|
Where the process list cannot be read (no `/proc` and `ps` failing), cadence
|
|
80
123
|
can only kill the root command (`sh`): it leads no group of its own, since it
|
|
@@ -90,7 +133,7 @@ cadence deliver # 0 delivered and verified · 1 a step failed · 2
|
|
|
90
133
|
5 minutes is a failure (you probably pushed another commit than the one you
|
|
91
134
|
deliver); every run must end `success`, `skipped` or `neutral`. `gh` errors
|
|
92
135
|
are retried, and reported with their cause after 5 minutes.
|
|
93
|
-
- Commands get `CADENCE_SHA`, `CADENCE_SHORT` (7 characters) and `CADENCE_BRANCH
|
|
136
|
+
- Commands get `CADENCE_SHA`, `CADENCE_SHORT` (7 characters) and `CADENCE_BRANCH` (a `deliver.script` also gets `CADENCE_DEPLOYED_FILE`, see below);
|
|
94
137
|
`${SHA}` and `${SHORT}` are replaced in `url` and `contains`.
|
|
95
138
|
- On success the lots cited by the commits since the previous delivery are
|
|
96
139
|
listed, so you can `raf done` those whose effect you have seen. With a
|
|
@@ -124,13 +167,41 @@ cadence deliver --sha 6b0d9aa -- api # an earli
|
|
|
124
167
|
`--sha` (any mode) delivers a pushed commit other than `HEAD` — for a CI that
|
|
125
168
|
builds each service only on the commit that touched it. `allowDirty` suits a
|
|
126
169
|
working tree shared by several sessions when the delivery starts from a pushed
|
|
127
|
-
sha and never from local files; without it a modified tree is refused.
|
|
170
|
+
sha and never from local files; without it a modified tree is refused. `--sha`
|
|
171
|
+
itself never reads the working tree: a tracked file modified outside the delivered
|
|
172
|
+
sha (the plan after `raf done`, say) neither refuses nor is reported, with or
|
|
173
|
+
without `allowDirty`.
|
|
174
|
+
|
|
175
|
+
**While a wave runs.** `cadence orchestrate` holds the repository: a plain `cadence deliver` is refused until every lot of the
|
|
176
|
+
repository in the wave is ready or handed back. `--sha` is the exception (L277): it is accepted while the wave runs when no
|
|
177
|
+
commit in the history of `<sha>` belongs to a lot of the wave that is not ready (running, queued, in question, suspended,
|
|
178
|
+
handed back, failed), that is when `<sha>` is « livrable jusqu'à » (the line the wave's final table prints). A ready lot is
|
|
179
|
+
then delivered while the wave runs, without waiting for the slowest lot. The refusal names the lots that hold the sha back. A lot's commits
|
|
180
|
+
are what git attributes to it in the repository (the commits that cite it) **and** what its sessions' reports noted, for the project's own
|
|
181
|
+
repository as for a neighbour one (the lot's `repos:` key): git sees at once the commit of a session still running, cut by the quota or failed,
|
|
182
|
+
which the reports only note when the session ends validly. A lot of the wave with no commit yet blocks nothing. What cannot be verified is
|
|
183
|
+
refused: an unreadable wave state, an unknown sha, or a lot that is not ready whose commits cannot be read (its project's plan is unreadable or
|
|
184
|
+
does not know the lot) while none of the commits already known holds the sha back. Push the sha alone first (`git push origin <sha>:main`).
|
|
185
|
+
|
|
186
|
+
**What is logged as delivered.** cadence no longer guesses which commit a project script deployed. Before running `deliver.script` it creates an
|
|
187
|
+
empty file and hands its path to the script in `CADENCE_DEPLOYED_FILE` (next to `CADENCE_SHA`, `CADENCE_SHORT`, `CADENCE_BRANCH`). A script
|
|
188
|
+
that deploys a commit other than `CADENCE_SHA` (it committed a `--news` timestamp itself, say) writes that sha into the file. After the script:
|
|
189
|
+
|
|
190
|
+
- the file holds a hexadecimal sha (7 to 40 characters) that resolves to a commit: that is the sha logged, and `✓ livré :` names it (« sha déclaré par le script ») — it wins over
|
|
191
|
+
HEAD, during a wave or not;
|
|
192
|
+
- the file holds anything else (not a commit, several lines, `HEAD` or a branch name — a revision would be resolved after the script, possibly at another lot's commit, so only a sha is accepted): the delivery fails with a message that says so, nothing is logged;
|
|
193
|
+
- the file is empty and the delivery runs **while a wave runs**: the log keeps `<sha>`, and if HEAD moved during the delivery a warning says
|
|
194
|
+
so (« la tête a avancé pendant la livraison ; sans sha déclaré par le script (CADENCE_DEPLOYED_FILE), le journal garde <sha> ») — the new
|
|
195
|
+
commit may be the script's, a running lot's, or a plan commit, and cadence cannot tell them apart;
|
|
196
|
+
- the file is empty and **no wave runs**: the historical rule stays, a script that committed and pushed during the delivery has delivered the new
|
|
197
|
+
HEAD, which is logged (« head moved by the script »).
|
|
128
198
|
|
|
129
199
|
cadence keeps what the script usually lacks: the preconditions (clean tree, pushed
|
|
130
200
|
`HEAD`), the lock (never two deliveries at once), the delivery log and the lots
|
|
131
201
|
delivered. Arguments are quoted for `sh`, so spaces and quotes reach the script
|
|
132
202
|
intact. If the script commits and pushes during the delivery (stamping a
|
|
133
|
-
changelog entry, say), the new `HEAD` is the sha recorded as delivered
|
|
203
|
+
changelog entry, say), outside a wave the new `HEAD` is the sha recorded as delivered; during a wave, or to be exact
|
|
204
|
+
about it anywhere, the script declares it in `CADENCE_DEPLOYED_FILE` (above). Exit code
|
|
134
205
|
0 of the script means delivered; `verify` checks, if any, run after it.
|
|
135
206
|
|
|
136
207
|
## verify: replay the effect checks, outside a delivery
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1320 350" width="1320" height="350" role="img" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif" font-size="13" fill="#e6edf3">
|
|
2
2
|
<title>A lot inside an orchestrate wave</title>
|
|
3
|
-
<desc>A lot without commits starts with a precheck (unless orchestrate.precheck is false), then by type through spec, mockup or sheet review and sealed acceptance tests, or straight to the implementation; then the code review, corrections (two passes, a third if the review converges), the UX pass if visible, once, after the compliant code review, and an automatic raf review.</desc>
|
|
3
|
+
<desc>A lot without commits whose title cites a finding or an earlier lot starts with a precheck (unless orchestrate.precheck is false), then by type through spec, mockup or sheet review and sealed acceptance tests, or straight to the implementation; then the code review, corrections (two passes, a third if the review converges), the UX pass if visible, once, after the compliant code review, and an automatic raf review.</desc>
|
|
4
4
|
<defs><marker id="arr2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#8b949e"/></marker></defs>
|
|
5
5
|
<rect width="1320" height="350" fill="#0d1117"/>
|
|
6
6
|
<rect x="20" y="140" width="120" height="52" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1.5" stroke-dasharray="6 4"/>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1320 350" width="1320" height="350" role="img" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif" font-size="13" fill="#1f2328">
|
|
2
2
|
<title>A lot inside an orchestrate wave</title>
|
|
3
|
-
<desc>A lot without commits starts with a precheck (unless orchestrate.precheck is false), then by type through spec, mockup or sheet review and sealed acceptance tests, or straight to the implementation; then the code review, corrections (two passes, a third if the review converges), the UX pass if visible, once, after the compliant code review, and an automatic raf review.</desc>
|
|
3
|
+
<desc>A lot without commits whose title cites a finding or an earlier lot starts with a precheck (unless orchestrate.precheck is false), then by type through spec, mockup or sheet review and sealed acceptance tests, or straight to the implementation; then the code review, corrections (two passes, a third if the review converges), the UX pass if visible, once, after the compliant code review, and an automatic raf review.</desc>
|
|
4
4
|
<defs><marker id="arr2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#57606a"/></marker></defs>
|
|
5
5
|
<rect width="1320" height="350" fill="#ffffff"/>
|
|
6
6
|
<rect x="20" y="140" width="120" height="52" rx="6" fill="#ffffff" stroke="#57606a" stroke-width="1.5" stroke-dasharray="6 4"/>
|