@sylad/cadence 1.9.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 +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +24 -67
- package/agents/doc-keeper.md +2 -2
- package/agents/precheck-reader.md +1 -1
- package/agents/reference-analyst.md +25 -0
- package/agents/reference-verifier.md +19 -0
- package/dist/audit.js +3 -1
- package/dist/check.js +19 -3
- package/dist/cli.js +28 -5
- package/dist/corrections.js +2 -1
- package/dist/deliver.js +47 -16
- package/dist/lead.js +8 -0
- package/dist/orchestrate/briefs.js +19 -1
- package/dist/orchestrate/changelog-merge.js +94 -0
- package/dist/orchestrate/command.js +108 -60
- package/dist/orchestrate/cycle.js +226 -36
- package/dist/orchestrate/integrate.js +52 -8
- package/dist/orchestrate/launch.js +14 -4
- package/dist/orchestrate/no-test.js +41 -0
- package/dist/orchestrate/registry.js +23 -0
- package/dist/orchestrate/snapshot.js +29 -1
- package/dist/orchestrate/state.js +14 -9
- package/dist/orchestrate/table.js +13 -1
- package/dist/perf.js +109 -7
- package/dist/plan.js +20 -1
- package/dist/proc.js +6 -0
- package/dist/ref/add.js +24 -11
- package/dist/ref/analyse.js +220 -0
- package/dist/ref/command.js +15 -1
- package/dist/ref/diff.js +262 -0
- package/dist/ref/evaluate.js +2 -2
- package/dist/ref/figures.js +86 -13
- package/dist/ref/pdf.js +20 -8
- package/dist/ref/regard.js +102 -0
- package/dist/ref/session.js +34 -0
- package/dist/state.js +17 -2
- package/docs/deliver.md +19 -3
- package/docs/fonctionnement.md +3 -3
- package/docs/lead.md +8 -4
- package/docs/news.md +6 -0
- package/docs/orchestrate.md +71 -40
- package/docs/overview.md +65 -1
- package/docs/perf.md +3 -2
- package/docs/raf.md +2 -1
- package/docs/ref.md +102 -3
- package/docs/releasing.md +3 -3
- package/docs/skills.md +12 -0
- package/docs/spec.md +1 -1
- package/docs/whats-new.md +28 -1
- package/package.json +1 -1
- package/skills/lead/SKILL.md +6 -6
- package/templates/orchestrate/fix-docs.md +2 -2
- package/templates/orchestrate/implement.md +1 -1
- package/templates/orchestrate/precheck.md +1 -1
package/dist/ref/figures.js
CHANGED
|
@@ -11,11 +11,18 @@ const WORDS = {
|
|
|
11
11
|
planches: { kind: 'planche', word: 'Planche' },
|
|
12
12
|
};
|
|
13
13
|
const NUMBER = String.raw `[A-Z]{0,2}[-.]?\d+(?:[.-](?:\d+|[A-Z](?![a-z])))*[a-z]?`;
|
|
14
|
-
/** Ce qui
|
|
15
|
-
const
|
|
14
|
+
/** Ce qui joint les deux bornes d'une plage : « 4 to 6 », « 4 à 6 », « 2–4 » (demi-cadratin collé aux numéros). Un tiret entouré d'espaces ou un cadratin (« 3—10 stations ») est une incise de prose. */
|
|
15
|
+
const RANGE = String.raw `(?:\s+(?:to|à)\s+|–)`;
|
|
16
|
+
/** Ce qui sépare deux numéros d'une énumération au pluriel : « 2, 3 », « 2 and 3 », plus les plages. */
|
|
17
|
+
const SEPARATOR = String.raw `(?:\s*,\s*(?:(?:and|et)\s+)?|\s+(?:and|et|&)\s+|${RANGE})`;
|
|
16
18
|
const REFERENCE = new RegExp(String.raw `\b(Figures?|Figs?|Tables?|Tableaux|Tableau|Planches?)\.?\s+(${NUMBER}(?:${SEPARATOR}${NUMBER})*)(?![\w])`, 'gi');
|
|
17
19
|
const PLURAL = /^(?:figures|figs|tables|tableaux|planches)$/i;
|
|
18
20
|
const NUMBER_ONLY = new RegExp(NUMBER, 'g');
|
|
21
|
+
/** Ce qui précède une référence qui n'en est pas une : « code figure 3 », « indicator figures 1 to 6 » des normes OMM sont des valeurs de code, pas des figures. */
|
|
22
|
+
const CODE_FIGURE = /\b(?:code|indicator)\s+$/i;
|
|
23
|
+
/** « code figure 17 » plus haut dans la même phrase (la phrase finit à un point, un point-virgule ou un deux-points suivi d'un blanc) : il porte une plage au demi-cadratin (« … over figures 20–49 »). */
|
|
24
|
+
const CODE_FIGURE_IN_SENTENCE = /\bcode\s+figures?\b/i;
|
|
25
|
+
const SENTENCE_END = /[.;:!?]\s/;
|
|
19
26
|
/** « Figure n » en début de ligne, puis le séparateur éventuel (tiret, deux-points, point) et le reste de la ligne. */
|
|
20
27
|
const CAPTION = new RegExp(String.raw `^(Figure|Fig|Table|Tableau|Planche)\.?\s+(${NUMBER})(?![\w])\s*([:.–—-])?\s*(.*)$`, 'i');
|
|
21
28
|
/** Le reste d'une ligne qui n'est qu'une parenthèse fermée : « (in accordance with 4.3.11) ». */
|
|
@@ -25,6 +32,37 @@ const DEICTIC = /\b(as shown (?:below|above)|shown (?:below|above)|illustrated (
|
|
|
25
32
|
const LEADER = /\.{4,}|(?:\.\s){4,}/;
|
|
26
33
|
/** Numéro de page en fin de titre d'un sommaire sans points de conduite : « Figure 1 Titre 12 » (le titre est « Titre 12 », le numéro de la figure n'en est pas un). */
|
|
27
34
|
const TRAILING_PAGE = /(?:^|\s)(\d{1,4})\s*$/;
|
|
35
|
+
/** Lignes courantes du document : en-têtes et pieds de page, c'est-à-dire les deux premières et les deux dernières lignes d'une page, chiffres mis à part, qui reviennent sur trois pages au moins. */
|
|
36
|
+
function runningLines(pages) {
|
|
37
|
+
const normal = (text) => text.trim().replace(/\d+/g, '#');
|
|
38
|
+
const pagesOf = new Map();
|
|
39
|
+
for (const p of pages)
|
|
40
|
+
for (const t of new Set([...p.lines.slice(0, 2), ...p.lines.slice(-2)].map((l) => normal(l.text))))
|
|
41
|
+
pagesOf.set(t, (pagesOf.get(t) ?? 0) + 1);
|
|
42
|
+
return (text) => (pagesOf.get(normal(text)) ?? 0) >= 3;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Dernière ligne de texte avant la ligne `i` de la page `pi`, sans les lignes courantes ni les lignes vides ; elle remonte sur la page
|
|
46
|
+
* qui précède immédiatement (numéro consécutif) : « code » peut finir une page, suivi de son pied de page et de l'en-tête de la suivante.
|
|
47
|
+
*/
|
|
48
|
+
function precedingText(pages, pi, i, running) {
|
|
49
|
+
let page = pages[pi];
|
|
50
|
+
for (let k = pi, j = i - 1;; j--) {
|
|
51
|
+
if (j < 0) {
|
|
52
|
+
const prev = pages[k - 1];
|
|
53
|
+
if (!prev || prev.n !== page.n - 1)
|
|
54
|
+
return '';
|
|
55
|
+
page = prev;
|
|
56
|
+
k--;
|
|
57
|
+
j = prev.lines.length - 1;
|
|
58
|
+
if (j < 0)
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const text = page.lines[j].text;
|
|
62
|
+
if (text.trim() !== '' && !running(text))
|
|
63
|
+
return text;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
28
66
|
const labelOf = (word, num) => {
|
|
29
67
|
const w = WORDS[word.toLowerCase()];
|
|
30
68
|
return { label: `${w.word} ${num}`, kind: w.kind };
|
|
@@ -67,16 +105,41 @@ function tocLines(page, lastPage) {
|
|
|
67
105
|
const contents = candidates >= 2 && candidates * 2 > page.lines.length;
|
|
68
106
|
return leaders.map((e) => e || contents);
|
|
69
107
|
}
|
|
70
|
-
|
|
108
|
+
const RANGE_ONLY = new RegExp(`^${RANGE}$`, 'i');
|
|
109
|
+
const named = (num) => ({ num, interior: false });
|
|
110
|
+
/** Un numéro divisé en préfixe (« 1.B. », « 3- », « A- », « A ») et chiffres finaux. */
|
|
111
|
+
const TRAILING_DIGITS = /^(.*\D)?(\d+)$/;
|
|
112
|
+
/**
|
|
113
|
+
* Numéros d'une plage : « 4 » et « 6 » donnent 4, 5, 6 (« 01 » et « 03 » donnent 01, 02, 03 : le zéro de tête de la première borne fixe la largeur) ; « 1.1 » et « 1.3 » donnent 1.1, 1.2, 1.3, de même « 3-1 »/« 3-3 » et « A1 »/« A3 ». Les bornes sont nommées, le milieu non.
|
|
114
|
+
* Sinon les bornes seules : préfixes différents, plage décroissante, 50 numéros ou plus. Une borne nue après une borne à préfixe
|
|
115
|
+
* (« 4.8–10 », « A3–1 ») n'est pas une figure à part : seule la première est gardée.
|
|
116
|
+
*/
|
|
117
|
+
function expand(from, to) {
|
|
118
|
+
const a = TRAILING_DIGITS.exec(from);
|
|
119
|
+
const b = TRAILING_DIGITS.exec(to);
|
|
120
|
+
if (b && b[1] === undefined && a && a[1] !== undefined)
|
|
121
|
+
return [named(from)];
|
|
122
|
+
if (!a || !b || (a[1] ?? '') !== (b[1] ?? ''))
|
|
123
|
+
return [named(from), named(to)];
|
|
124
|
+
const [lo, hi] = [Number(a[2]), Number(b[2])];
|
|
125
|
+
if (hi < lo || hi - lo >= 50)
|
|
126
|
+
return [named(from), named(to)];
|
|
127
|
+
return Array.from({ length: hi - lo + 1 }, (_, i) => ({ num: `${a[1] ?? ''}${String(lo + i).padStart(a[2].length, '0')}`, interior: i > 0 && i < hi - lo }));
|
|
128
|
+
}
|
|
129
|
+
/** Numéros nommés par une référence : le seul numéro au singulier ; au pluriel la liste, chaque plage (« 4 to 6 », « 2–4 ») développée quand ses bornes le permettent. */
|
|
71
130
|
function numbersOf(head, list) {
|
|
72
|
-
const
|
|
131
|
+
const found = [...list.matchAll(NUMBER_ONLY)];
|
|
73
132
|
if (!PLURAL.test(head))
|
|
74
|
-
return
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
133
|
+
return found.slice(0, 1).map((m) => named(m[0]));
|
|
134
|
+
const out = [];
|
|
135
|
+
found.forEach((m, i) => {
|
|
136
|
+
const prev = found[i - 1];
|
|
137
|
+
if (prev && RANGE_ONLY.test(list.slice(prev.index + prev[0].length, m.index)))
|
|
138
|
+
out.splice(out.length - 1, 1, ...expand(prev[0], m[0]));
|
|
139
|
+
else
|
|
140
|
+
out.push(named(m[0]));
|
|
141
|
+
});
|
|
142
|
+
return out;
|
|
80
143
|
}
|
|
81
144
|
/**
|
|
82
145
|
* Légendes, citations et liens non résolus, d'après le texte seul ; les images viennent après (`attachRegions`).
|
|
@@ -114,7 +177,8 @@ export function indexText(pages, imagePages = new Set()) {
|
|
|
114
177
|
const figures = new Map(chosen.map((c) => [key(c.label), { label: c.label, kind: c.kind, page: c.page, line: c.line, caption: c.text, box: c.box, regions: [], citations: [] }]));
|
|
115
178
|
const captionLine = new Set(chosen.map((c) => `${c.page}:${c.line}`));
|
|
116
179
|
const unresolved = [];
|
|
117
|
-
|
|
180
|
+
const running = runningLines(pages);
|
|
181
|
+
for (const [pi, p] of pages.entries()) {
|
|
118
182
|
const captions = [...figures.values()].filter((f) => f.page === p.n);
|
|
119
183
|
p.lines.forEach((l, i) => {
|
|
120
184
|
if (toc.get(p.n)[i])
|
|
@@ -126,12 +190,21 @@ export function indexText(pages, imagePages = new Set()) {
|
|
|
126
190
|
// la légende ne se cite pas elle-même
|
|
127
191
|
if (own && m.index === 0)
|
|
128
192
|
continue;
|
|
129
|
-
|
|
193
|
+
// « code » peut finir la ligne précédente : l'extrait PDF coupe les lignes, pas le texte
|
|
194
|
+
const head = l.text.slice(0, m.index);
|
|
195
|
+
const carried = head.trim() === '' ? `${precedingText(pages, pi, i, running)} ${head}` : head;
|
|
196
|
+
if (labelOf(m[1], '').kind === 'figure' && (CODE_FIGURE.test(carried) || (m[2].includes('–') && CODE_FIGURE_IN_SENTENCE.test(carried.split(SENTENCE_END).pop()))))
|
|
197
|
+
continue;
|
|
198
|
+
for (const { num, interior } of numbersOf(m[1], m[2])) {
|
|
130
199
|
const { label } = labelOf(m[1], num);
|
|
200
|
+
const fig = figures.get(key(label));
|
|
201
|
+
// le milieu d'une plage sans légende n'est pas un lien à résoudre (« figures 11–20 » n'en désigne aucune) et ne masque pas
|
|
202
|
+
// une citation explicite du même numéro plus loin sur la ligne
|
|
203
|
+
if (interior && !fig)
|
|
204
|
+
continue;
|
|
131
205
|
if (seen.has(key(label)))
|
|
132
206
|
continue;
|
|
133
207
|
seen.add(key(label));
|
|
134
|
-
const fig = figures.get(key(label));
|
|
135
208
|
if (fig)
|
|
136
209
|
fig.citations.push({ page: p.n, line, extrait: l.text.trim(), type: 'nommee' });
|
|
137
210
|
else
|
package/dist/ref/pdf.js
CHANGED
|
@@ -7,8 +7,20 @@ import { RafError } from '../plan.js';
|
|
|
7
7
|
export const DPI = 200;
|
|
8
8
|
const SCALE = DPI / 72;
|
|
9
9
|
const px = (pt) => Math.round(pt * SCALE);
|
|
10
|
-
function exec(file, args
|
|
11
|
-
return execFileSync(file, args, {
|
|
10
|
+
function exec(file, args) {
|
|
11
|
+
return execFileSync(file, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 1024 * 1024 * 512, env: { ...process.env, LC_ALL: 'C' } });
|
|
12
|
+
}
|
|
13
|
+
/** Lance un outil poppler ; son échec (code non nul, sortie trop grosse, sortie illisible) devient une RafError qui le nomme, avec la raison. */
|
|
14
|
+
function poppler(name, tools, args, file) {
|
|
15
|
+
try {
|
|
16
|
+
return exec(tools[name], args);
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
const err = e;
|
|
20
|
+
const reason = String(err.stderr ?? '').split('\n').find((l) => l.trim() !== '' && !/^Syntax Warning/.test(l)) ?? String(err.message ?? '').split('\n')[0] ?? '';
|
|
21
|
+
const clean = reason.replace(/^Syntax Error[^:]*: /, '').trim();
|
|
22
|
+
throw new RafError(`ref : ${name} a échoué sur ${file}${clean ? ` (${clean})` : ''}`);
|
|
23
|
+
}
|
|
12
24
|
}
|
|
13
25
|
/** Nombre de pages ; un PDF chiffré, abîmé ou sans page est refusé avec la raison. */
|
|
14
26
|
export function pageCount(tools, file) {
|
|
@@ -33,7 +45,7 @@ const attr = (tag, name) => new RegExp(`${name}="([^"]*)"`).exec(tag)?.[1] ?? ''
|
|
|
33
45
|
const unescapeXml = (s) => s.replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, '&');
|
|
34
46
|
/** Texte positionné de tout le document (`pdftotext -bbox-layout` : mots regroupés en lignes), en pixels de la page rendue. */
|
|
35
47
|
export function textLayout(tools, file) {
|
|
36
|
-
const xml =
|
|
48
|
+
const xml = poppler('pdftotext', tools, ['-bbox-layout', file, '-'], file);
|
|
37
49
|
const pages = [];
|
|
38
50
|
for (const chunk of xml.split('<page ').slice(1)) {
|
|
39
51
|
const head = chunk.slice(0, chunk.indexOf('>'));
|
|
@@ -55,7 +67,7 @@ export function listImages(tools, file) {
|
|
|
55
67
|
const out = [];
|
|
56
68
|
const slots = new Map();
|
|
57
69
|
const ranks = new Map();
|
|
58
|
-
for (const row of
|
|
70
|
+
for (const row of poppler('pdfimages', tools, ['-list', file], file).split('\n').slice(2)) {
|
|
59
71
|
const c = row.trim().split(/\s+/);
|
|
60
72
|
if (c.length < 5)
|
|
61
73
|
continue;
|
|
@@ -74,7 +86,7 @@ export function listImages(tools, file) {
|
|
|
74
86
|
export function renderPages(tools, file, pages, dir) {
|
|
75
87
|
mkdirSync(dir, { recursive: true });
|
|
76
88
|
for (const n of pages)
|
|
77
|
-
|
|
89
|
+
poppler('pdftoppm', tools, ['-r', String(DPI), '-png', '-singlefile', '-f', String(n), '-l', String(n), file, join(dir, `page-${n}`)], file);
|
|
78
90
|
}
|
|
79
91
|
/** Résolution des vignettes de toutes les pages (à parcourir pour repérer les planches d'un document sans texte). */
|
|
80
92
|
export const THUMB_DPI = 40;
|
|
@@ -82,19 +94,19 @@ export const THUMB_DPI = 40;
|
|
|
82
94
|
export function renderThumbnails(tools, file, total, dir) {
|
|
83
95
|
mkdirSync(dir, { recursive: true });
|
|
84
96
|
for (let n = 1; n <= total; n++)
|
|
85
|
-
|
|
97
|
+
poppler('pdftoppm', tools, ['-r', String(THUMB_DPI), '-png', '-singlefile', '-f', String(n), '-l', String(n), file, join(dir, `page-${n}`)], file);
|
|
86
98
|
}
|
|
87
99
|
/** Extrait les images d'une page dans `dir/p<page>-<slot sur 3 chiffres>.png`. */
|
|
88
100
|
export function extractImages(tools, file, page, dir) {
|
|
89
101
|
mkdirSync(dir, { recursive: true });
|
|
90
|
-
|
|
102
|
+
poppler('pdfimages', tools, ['-png', '-f', String(page), '-l', String(page), file, join(dir, `p${page}`)], file);
|
|
91
103
|
}
|
|
92
104
|
export const imageFile = (page, slot) => `p${page}-${String(slot).padStart(3, '0')}.png`;
|
|
93
105
|
/** Régions des images d'une page (ordre du document), en pixels de la page rendue : `pdftohtml -xml` au zoom 200/72. */
|
|
94
106
|
export function imageRegions(tools, file, page) {
|
|
95
107
|
const work = mkdtempSync(join(tmpdir(), 'cadence-ref-'));
|
|
96
108
|
try {
|
|
97
|
-
|
|
109
|
+
poppler('pdftohtml', tools, ['-xml', '-q', '-nodrm', '-zoom', String(SCALE), '-f', String(page), '-l', String(page), file, join(work, 'p')], file);
|
|
98
110
|
const xml = readFileSync(join(work, 'p.xml'), 'utf8');
|
|
99
111
|
return [...xml.matchAll(/<image ([^>]*)\/>/g)].map((m) => ({ page, x: Number(attr(m[1], 'left')), y: Number(attr(m[1], 'top')), l: Number(attr(m[1], 'width')), h: Number(attr(m[1], 'height')) }));
|
|
100
112
|
}
|
|
@@ -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
|
+
}
|
package/dist/state.js
CHANGED
|
@@ -218,6 +218,19 @@ export function readDeliveries(dir) {
|
|
|
218
218
|
return null;
|
|
219
219
|
}
|
|
220
220
|
}
|
|
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
|
+
}
|
|
221
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. */
|
|
222
235
|
export function readFailedEntries(dir) {
|
|
223
236
|
try {
|
|
@@ -230,9 +243,11 @@ export function readFailedEntries(dir) {
|
|
|
230
243
|
return [];
|
|
231
244
|
let reason = m[3] ?? '';
|
|
232
245
|
const c = /^\[(\w+)\] ?/.exec(reason);
|
|
233
|
-
const
|
|
234
|
-
if (
|
|
246
|
+
const written = c && FAIL_CLASSES.includes(c[1]) ? c[1] : null;
|
|
247
|
+
if (written)
|
|
235
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));
|
|
236
251
|
return [{ day: m[1], sha: m[2] ?? '', cls, reason, tail }];
|
|
237
252
|
});
|
|
238
253
|
}
|
package/docs/deliver.md
CHANGED
|
@@ -69,7 +69,8 @@ cadence deliver # 0 delivered and verified · 1 a step failed · 2
|
|
|
69
69
|
This is a list of likely patterns, not a guarantee: a secret of another shape printed by a
|
|
70
70
|
command can still land in the log, which stays local (`<git common dir>/cadence/`). `cadence perf` reads classes and projects from it
|
|
71
71
|
([perf](perf.md#delivery-quality-and-the-tour-header-l227)); a line written before L282
|
|
72
|
-
has no class
|
|
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.
|
|
73
74
|
- Every command is killed when it exceeds its budget (CI, `deployTimeout`, what is
|
|
74
75
|
left of `verifyTimeout`) and reported as "délai dépassé". Killed means the
|
|
75
76
|
whole tree, followed while the command runs: every 200 ms cadence lists the
|
|
@@ -92,6 +93,20 @@ cadence deliver # 0 delivered and verified · 1 a step failed · 2
|
|
|
92
93
|
is still failing when the kill comes (the last listing is not trusted after
|
|
93
94
|
1 second): only the command is killed, its descendants survive, and cadence
|
|
94
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.
|
|
95
110
|
A delay overrun and `SIGTERM` give no grace: a script's own `trap` does not
|
|
96
111
|
get to finish there. The lock is held until the followed set is empty: when
|
|
97
112
|
cadence dies of the signal it dies after the tree, and its stale lock, which
|
|
@@ -100,8 +115,9 @@ cadence deliver # 0 delivered and verified · 1 a step failed · 2
|
|
|
100
115
|
a process that leaves the tree before cadence first sees it (a daemon's
|
|
101
116
|
double fork, `setsid`, `nohup … &` from a shell that exits, all within less
|
|
102
117
|
than 200 ms), a process of another user that cadence may not signal (`sudo`),
|
|
103
|
-
a background process still running when the command returns normally (
|
|
104
|
-
|
|
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
|
|
105
121
|
with `SIGKILL` (`kill -9`, the OOM killer).
|
|
106
122
|
Where the process list cannot be read (no `/proc` and `ps` failing), cadence
|
|
107
123
|
can only kill the root command (`sh`): it leads no group of its own, since it
|
package/docs/fonctionnement.md
CHANGED
|
@@ -25,7 +25,7 @@ while a sub-task is open. A delivery is finished only when its effect checks pas
|
|
|
25
25
|
<img alt="A lot without commits whose title cites a finding or another lot starts with a precheck (unless disabled), then by type through spec, mockup or sheet review and sealed acceptance tests, or straight to the implementation; then the UX pass if visible, the code review, corrections and an automatic raf review" src="fonctionnement-wave-light.svg">
|
|
26
26
|
</picture>
|
|
27
27
|
|
|
28
|
-
A lot without commits yet whose title cites a finding (`constat`, `revue`, `review`, `audit`) or another lot (its `goal:` counts like its title) starts with the **precheck** (unless `orchestrate.precheck: false`; a lot that already has commits, or whose title (or `goal:`) cites neither, is never pre-checked — L288): is the deliverable already there? « Yes » hands the lot back
|
|
28
|
+
A lot without commits yet whose title cites a finding (`constat`, `revue`, `review`, `audit`) or another lot (its `goal:` counts like its title) starts with the **precheck** (unless `orchestrate.precheck: false`; a lot that already has commits, or whose title (or `goal:`) cites neither, is never pre-checked — L288; nor is a lot created after the last code commit of the repository whose title and `goal:` cite no lot that already has commits of its own, which has nothing to find — L322): is the deliverable already there? « Yes » hands the lot back
|
|
29
29
|
to the lead; « partial » passes what exists to the implementation. Then:
|
|
30
30
|
|
|
31
31
|
- `screen: page` only: **spec** (mockup and screen sheet), **mockup review** (`ux-reviewer`, two
|
|
@@ -73,7 +73,7 @@ working repository's `dist/`: they read a separate worktree under `~/.local/shar
|
|
|
73
73
|
only by `npm run service -- <sha>`. The command refuses a sha that is not an ancestor of `HEAD` and a
|
|
74
74
|
sha whose cited lots are not `done`, so a wave cannot put unreviewed code in service. In the
|
|
75
75
|
working repository, `npm run build` is a typecheck only. A `dropped` lot whose every commit is undone
|
|
76
|
-
by a `git revert` under the sha no longer blocks.
|
|
76
|
+
by a `git revert` under the sha no longer blocks. The command keeps the copy in service and the 3 put in service before it (rollback), removes the older ones unless a live process still uses one, and says what it purged (number of copies, space freed).
|
|
77
77
|
|
|
78
78
|
## Budget of a lot
|
|
79
79
|
|
|
@@ -96,7 +96,7 @@ The final review is always played; the budget bounds the writing passes. A `scre
|
|
|
96
96
|
|
|
97
97
|
- **Session cap.** As many sessions or subagents running at once as the cap (2 by default, set by the
|
|
98
98
|
human with `CADENCE_MAX_SESSIONS` or `--max-sessions`). The lead never raises it itself.
|
|
99
|
-
- Never two sessions in the same repository at the same time. (
|
|
99
|
+
- Never two sessions in the same repository at the same time. (The worktree mode, `orchestrate.worktrees: true` — on in cadence's own `cadence.yaml`, off where the key is absent — lets two independent lots of one repository play at once, each in its own worktree — [docs/orchestrate.md](orchestrate.md#one-worktree-per-lot-orchestrateworktrees-l279).)
|
|
100
100
|
- A session cannot ask the human anything: it stops and reports, and the lead brings the question back.
|
|
101
101
|
- A sealed acceptance test that contradicts its sheet is corrected by the lead with
|
|
102
102
|
`cadence spec reseal <lot> <test> --reason "…"`, never by editing `acceptation.json`.
|
package/docs/lead.md
CHANGED
|
@@ -53,7 +53,9 @@ picture, not its source.
|
|
|
53
53
|
3. **Relay.** The lead follows the wave's journal (`tail -n +1 -F .cadence/runs/<id>/journal.log`, one
|
|
54
54
|
line per transition) and tells the human each line in one sentence, with no polling loop. A
|
|
55
55
|
question from a session comes back to the human at once; the answer goes in with
|
|
56
|
-
`cadence orchestrate --resume --answer <project>:<lot> "…"
|
|
56
|
+
`cadence orchestrate --resume --wave <wave> --answer <project>:<lot> "…"` (the wave is named: the
|
|
57
|
+
command is printed under each question of the table, ready to run; without a wave, `--resume` is refused as
|
|
58
|
+
soon as the folder keeps several unfinished waves). The lead never answers in the
|
|
57
59
|
human's place. `--drop` and `--stop-after-current` steer a wave that is running.
|
|
58
60
|
4. **Review.** The final table of the wave lists the lots ready, handed back, and the line
|
|
59
61
|
« livrable jusqu'à <sha> » for a ready lot stacked under a handed-back one. Minor findings come back in
|
|
@@ -81,8 +83,8 @@ picture, not its source.
|
|
|
81
83
|
| Rule | Why |
|
|
82
84
|
|---|---|
|
|
83
85
|
| At most **as many subagents as the session cap** running at once (2 by default; the human sets it with `CADENCE_MAX_SESSIONS`), orchestrate sessions included | `cadence orchestrate --status` and `cadence lead tour` print `créneaux libres : N sur M`, M being the configured cap and the sessions the lead started by hand (`cadence lead hold`, L255) being subtracted; the lead never raises `--max-sessions` or `CADENCE_MAX_SESSIONS` itself |
|
|
84
|
-
| Never two sessions in the same repository at the same time; the lead does not commit where a wave runs (
|
|
85
|
-
| A locked acceptance test that contradicts the sheet is corrected by the lead with `cadence spec reseal <lot> <test> --reason "…"` (lot in progress, outside a wave and with no wave that is not `done` keeping open a lot of the repository still `doing` in its plan, the resealed lot or another, e.g. a question waiting: `--resume --answer` or `--drop` first, the one error lists every wave concerned, the live one that holds the lock (to wait for) and the stopped ones, and gives for each stopped wave the single `cd <launch folder> && cadence orchestrate --resume <wave> --drop <project>:<lot> [--drop …]` to run, a `--drop` per open lot of the repository in that wave, the queued ones that are not `doing` in the plan included, since `--resume` would relaunch them and they would take the lock back, L238 L239; an amended sheet is accepted once committed, every commit that touched it citing the lot, L217, never put it back to the red state; a delivered lot with an unstable test: add `--porteur <lot in progress>`, L212), then a new `cadence orchestrate <project>:<lot>` for a lot handed back (`--resume` skips it) | never by a session, never by editing `acceptation.json` |
|
|
86
|
+
| Never two sessions in the same repository at the same time; the lead does not commit where a wave runs (with `orchestrate.worktrees: true` two lots of one repository play at once, each in its own worktree: not a rule for the lead) | a wave's lock refuses `cadence deliver` while a lot still works there |
|
|
87
|
+
| A locked acceptance test that contradicts the sheet is corrected by the lead with `cadence spec reseal <lot> <test> --reason "…"` (lot in progress, outside a wave and with no wave that is not `done` keeping open a lot of the repository still `doing` in its plan, the resealed lot or another, e.g. a question waiting: `--resume --wave <wave> --answer` or `--drop` first, the one error lists every wave concerned, the live one that holds the lock (to wait for) and the stopped ones, and gives for each stopped wave the single `cd <launch folder> && cadence orchestrate --resume --wave <wave> --drop <project>:<lot> [--drop …]` to run, a `--drop` per open lot of the repository in that wave, the queued ones that are not `doing` in the plan included, since `--resume` would relaunch them and they would take the lock back, L238 L239; an amended sheet is accepted once committed, every commit that touched it citing the lot, L217, never put it back to the red state; a delivered lot with an unstable test: add `--porteur <lot in progress>`, L212), then a new `cadence orchestrate <project>:<lot>` for a lot handed back (`--resume` skips it) | never by a session, never by editing `acceptation.json` |
|
|
86
88
|
| Deliveries and the `raf ux` / `raf review` verdicts belong to the lead, one project at a time | never to a subagent, never two deliveries in parallel |
|
|
87
89
|
| The UX verdict is the human's unless `lead.ux: lead` (parent `cadence.yaml`, default `human`) | then the lead decides from the report and the captures, and brings any doubt to the human |
|
|
88
90
|
| A session cannot ask the human anything | it stops and reports; the lead brings the question back |
|
|
@@ -145,7 +147,9 @@ beta · en cours rien · dérive aucune · notes : aucune · prochain T2 Second
|
|
|
145
147
|
| `attend` | only when a lot waits on its `gate`: `attend L190` |
|
|
146
148
|
| `dépôt` | `non commité` (modified or untracked files), `non poussé` (commits ahead of the upstream), `livraison en cours` (live delivery lock); `propre` otherwise |
|
|
147
149
|
|
|
148
|
-
After
|
|
150
|
+
After them, one line per project that does not declare `orchestrate.test` (L327): `alpha · porte de tests absente : orchestrate.test n'est pas déclaré, la vague ne lancera pas la suite (seule la revue le fera si elle le décide) · commande probable : npm test (à déclarer en orchestrate.test)`. The probable command is read from `package.json` (`scripts.test`), `pyproject.toml` (pytest) or `pom.xml` (`mvn test`) and omitted when none exists; the lead declares `test:` and `build:` in that project's `cadence.yaml` after checking the command is green, one config commit per project. `--json`: `noTest: { probable }` on the project's row, absent when the key is declared. Same line in [`orchestrate --dry-run`](orchestrate.md).
|
|
151
|
+
|
|
152
|
+
After the project lines the tour lists the **worktrees kept by waves** (L279, `orchestrate.worktrees: true`, on by default in cadence since L320; [`orchestrate.worktrees`](orchestrate.md#one-worktree-per-lot-orchestrateworktrees-l279)): a title, then one line per kept worktree — repository, lot, age, number of unintegrated commits, path — so that a lot handed back with unintegrated commits is never forgotten; nothing is printed when there is none (`--json`: `worktrees` on the project's row). `cadence orchestrate --status` and the final table of a wave print the same lines; the lead removes one with `cadence orchestrate --remove-worktree <project>:<lot> [--abandon-commits]`.
|
|
149
153
|
|
|
150
154
|
Before the project lines the tour prints the **perf header** (L227): one line per `⚠` of `cadence perf` for the folder, in the order of `perf`, each ending with its decision and, for a trend, the value of its quality twin (speed and quality side by side); the net-bottleneck `⚠ goulot` line is relayed as is. Nothing is printed when no `⚠` stands, and the output is then the one of a tour without header. The header is read with `perf`'s own reading of the folder (projects up to two levels, whatever `--depth`) and only in the text output: `--json` is unchanged. When `perf` cannot read the folder (no `.cadence/runs`, malformed price grid, unreadable journals or plans) the tour prints no header, reports no perf error and exits 0. The [lead skill](skills.md) reads the header before proposing the three lots and opens an improvement lot only on a trend confirmed on two windows or a net bottleneck.
|
|
151
155
|
|
package/docs/news.md
CHANGED
|
@@ -150,6 +150,8 @@ to be reported as one **major** finding per line — a stale document is not lef
|
|
|
150
150
|
memory of a sentence. Touching a document is what the program checks; whether its text is true is
|
|
151
151
|
still the review's reading. The commits of neighbouring repositories (`repos:`) are not audited.
|
|
152
152
|
|
|
153
|
+
A gap is closed by writing the document, or **acknowledged** when the documents really stay true: `raf docs-ok <lot> --reason "why"` (L317; the reason is required) writes a dated `docs_ok:` key on the lot, which `raf check`, `cadence lead tour` and `cadence orchestrate` all read. There is no silent third way: a lot with an open gap does not end ready in a wave.
|
|
154
|
+
|
|
153
155
|
### The librarian: `doc-keeper`
|
|
154
156
|
|
|
155
157
|
`docs.sync` says *that* a document is behind; the `doc-keeper` agent (L306, `agents/doc-keeper.md`, generic: it knows no
|
|
@@ -162,6 +164,10 @@ out, then:
|
|
|
162
164
|
- **read-only session** (no write tool, or a brief that says so): changes nothing and lists, document by document, the
|
|
163
165
|
passage to write.
|
|
164
166
|
|
|
167
|
+
**Where it writes** (L326): a lot that changes how the tool works is written in the pages of `docs/`, in full.
|
|
168
|
+
The README is a short home page: it receives only a link to that page, never the detail (an option, a key, a path, a flag).
|
|
169
|
+
The test `readme-accueil` holds the README to that (size, short lines, no detailed option).
|
|
170
|
+
|
|
165
171
|
`cadence orchestrate` calls it by itself when `docs.sync` reports a document behind the commits of the lot, **before the
|
|
166
172
|
review** (see « Librarian before the review » in [orchestrate](orchestrate.md)); the lead calls it by hand otherwise
|
|
167
173
|
([lead](lead.md)).
|