@sylad/cadence 1.39.0 → 1.41.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 +3 -3
- package/agents/spec-writer.md +1 -0
- package/agents/ux-reviewer.md +2 -0
- package/dist/cli.js +49 -62
- package/dist/config-set.js +2 -0
- package/dist/config.js +8 -8
- package/dist/deliver-lots.js +37 -0
- package/dist/deliver.js +15 -39
- package/dist/docs-ok-file.js +25 -0
- package/dist/lot-lines.js +51 -0
- package/dist/memory-index.js +113 -0
- package/dist/orchestrate/command.js +8 -8
- package/dist/orchestrate/continue.js +4 -2
- package/dist/orchestrate/cycle.js +23 -29
- package/dist/orchestrate/deja-traitee.js +76 -0
- package/dist/orchestrate/integrate.js +7 -2
- package/dist/orchestrate/lot-budget.js +2 -2
- package/dist/orchestrate/maquette.js +3 -0
- package/dist/orchestrate/spec.js +51 -6
- package/dist/orchestrate/table.js +29 -11
- package/dist/plan-extras.js +118 -0
- package/dist/plan-read.js +144 -0
- package/dist/plan-states.js +119 -0
- package/dist/plan.js +91 -157
- package/dist/raf-close-checks.js +25 -0
- package/dist/raf-status.js +28 -0
- package/dist/session.js +8 -0
- package/dist/state.js +23 -0
- package/dist/verify.js +20 -3
- package/dist/yaml-keys.js +2 -0
- package/docs/concepts-livraison-dark.svg +32 -0
- package/docs/concepts-livraison-light.svg +32 -0
- package/docs/concepts-lot-dark.svg +52 -0
- package/docs/concepts-lot-light.svg +52 -0
- package/docs/concepts-vague-dark.svg +39 -0
- package/docs/concepts-vague-light.svg +39 -0
- package/docs/concepts-worktrees-dark.svg +45 -0
- package/docs/concepts-worktrees-light.svg +45 -0
- package/docs/concepts.md +159 -0
- package/docs/deliver.md +24 -2
- package/docs/fonctionnement.md +11 -5
- package/docs/lead.md +10 -6
- package/docs/orchestrate.md +22 -5
- package/docs/overview.md +1 -1
- package/docs/perf.md +1 -0
- package/docs/qualite.md +13 -3
- package/docs/raf.md +42 -5
- package/docs/session.md +32 -0
- package/docs/spec.md +5 -2
- package/docs/whats-new.md +6 -0
- package/package.json +10 -1
- package/skills/lead/SKILL.md +6 -0
- package/skills/session-close/SKILL.md +6 -0
- package/templates/orchestrate/spec.md +2 -2
package/dist/plan.js
CHANGED
|
@@ -3,7 +3,11 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync } from 'node
|
|
|
3
3
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
4
4
|
import { dirtyContents } from './git.js';
|
|
5
5
|
import { Document, isMap, isSeq, parseDocument } from 'yaml';
|
|
6
|
-
import {
|
|
6
|
+
import { toDay } from './dates.js';
|
|
7
|
+
import { normalizeLot } from './plan-read.js';
|
|
8
|
+
import { currentState, defaultState, rebase, stateProblems, transitionProblem } from './plan-states.js';
|
|
9
|
+
import { DOCS_OK_FILE, readDocsOkFile } from './docs-ok-file.js';
|
|
10
|
+
import { checkedExtras, withExtras } from './plan-extras.js';
|
|
7
11
|
import { ARCHIVE_DIR, archiveFiles, archiveLots, archiveYear, mergeArchived, newArchiveDoc, readArchiveDoc, selectArchivable } from './archive.js';
|
|
8
12
|
/** Délai de la commande `plan.onWrite` : une republication du plan, pas un build. */
|
|
9
13
|
const ON_WRITE_TIMEOUT_MS = 120_000;
|
|
@@ -24,7 +28,6 @@ export function setPlanNotice(fn) {
|
|
|
24
28
|
notice = fn;
|
|
25
29
|
}
|
|
26
30
|
export const STATUSES = ['todo', 'doing', 'done', 'dropped'];
|
|
27
|
-
const GATE_INVALID = 'gate invalide : count: entier ≥ 1 et un critère spec: true ou screen: page attendus';
|
|
28
31
|
export class RafError extends Error {
|
|
29
32
|
}
|
|
30
33
|
/** Un nom de ressource partagée (L251) : un mot de lettres minuscules, chiffres, « - » et « _ ». */
|
|
@@ -391,7 +394,14 @@ export class Plan {
|
|
|
391
394
|
}
|
|
392
395
|
// L396 : l'archive est un détail de stockage — les lots archivés reprennent leur place parmi ceux du fichier actif, comme un seul plan.
|
|
393
396
|
const archived = this.loadArchives().flatMap((a) => archiveLots(a.doc).toJSON());
|
|
394
|
-
|
|
397
|
+
const read = (r) => withExtras(normalizeLot(r), r, this.settings.extras);
|
|
398
|
+
const lots = mergeArchived(archived.map(read), this.activeRaw().map(read));
|
|
399
|
+
const model = this.settings.states;
|
|
400
|
+
return model ? lots.map((l) => ({ ...l, problems: [...l.problems, ...stateProblems(model, l)] })) : lots;
|
|
401
|
+
}
|
|
402
|
+
/** Les états détaillés déclarés (`plan.states`), ou undefined. */
|
|
403
|
+
get stateModel() {
|
|
404
|
+
return this.settings.states;
|
|
395
405
|
}
|
|
396
406
|
lot(id) {
|
|
397
407
|
const found = this.lots().find((l) => l.id === id);
|
|
@@ -560,6 +570,7 @@ export class Plan {
|
|
|
560
570
|
if (opts.every !== undefined && !(Number.isInteger(opts.every) && opts.every > 0)) {
|
|
561
571
|
throw new RafError(`périodicité invalide : ${opts.every} (un nombre entier de jours, 1 au moins)`);
|
|
562
572
|
}
|
|
573
|
+
const extras = checkedExtras(this.settings.extras, opts.extras);
|
|
563
574
|
const entry = { id, title };
|
|
564
575
|
if (pub !== undefined)
|
|
565
576
|
entry.public = pub;
|
|
@@ -573,6 +584,7 @@ export class Plan {
|
|
|
573
584
|
entry.after = opts.after;
|
|
574
585
|
if (opts.uses?.length)
|
|
575
586
|
entry.uses = cleanUses(opts.uses);
|
|
587
|
+
Object.assign(entry, extras);
|
|
576
588
|
entry.created = today;
|
|
577
589
|
const node = this.doc.createNode(entry);
|
|
578
590
|
for (const key of ['after', 'uses']) {
|
|
@@ -720,6 +732,11 @@ export class Plan {
|
|
|
720
732
|
if (!isOpen(lot.status)) {
|
|
721
733
|
throw new RafError(`${lotId} est ${lot.status} ; le rouvrir si c'est voulu : raf todo ${lotId}`);
|
|
722
734
|
}
|
|
735
|
+
const model = this.settings.states;
|
|
736
|
+
const target = model ? defaultState(model, status) : undefined;
|
|
737
|
+
const refused = model && target !== undefined && !opts.force ? transitionProblem(model, currentState(model, lot), target) : null;
|
|
738
|
+
if (refused)
|
|
739
|
+
throw new RafError(`${lotId} : ${refused} — raf status ${lotId} <état> (--force pour passer outre)`);
|
|
723
740
|
if (status === 'done') {
|
|
724
741
|
const open = lot.tasks.filter((t) => isOpen(t.status)).map((t) => `${lotId}/${t.id}`);
|
|
725
742
|
if (open.length > 0 && !opts.force) {
|
|
@@ -738,6 +755,7 @@ export class Plan {
|
|
|
738
755
|
}
|
|
739
756
|
}
|
|
740
757
|
node.set('status', status);
|
|
758
|
+
this.syncState(node, status);
|
|
741
759
|
if (!lot.started && status !== 'dropped')
|
|
742
760
|
node.set('started', today);
|
|
743
761
|
if (status === 'done' || status === 'dropped')
|
|
@@ -760,6 +778,7 @@ export class Plan {
|
|
|
760
778
|
if (lot.status === 'done' || lot.status === 'dropped') {
|
|
761
779
|
const node = this.lotNode(ref);
|
|
762
780
|
node.set('status', 'todo');
|
|
781
|
+
this.syncState(node, 'todo');
|
|
763
782
|
node.delete('finished');
|
|
764
783
|
this.note(ref, reason.trim() ? `rouvert (était ${lot.status}) : ${reason.trim()}` : `rouvert (était ${lot.status})`, today);
|
|
765
784
|
return;
|
|
@@ -769,9 +788,78 @@ export class Plan {
|
|
|
769
788
|
throw new RafError(`${ref} a ${n} commit(s) qui le citent : le remettre en attente perdrait leur lien — raf drop ou le laisser doing`);
|
|
770
789
|
const node = this.lotNode(ref);
|
|
771
790
|
node.set('status', 'todo');
|
|
791
|
+
this.syncState(node, 'todo');
|
|
772
792
|
node.delete('started');
|
|
773
793
|
this.note(ref, reason.trim() ? `remis en attente : ${reason.trim()}` : 'remis en attente', today);
|
|
774
794
|
}
|
|
795
|
+
/**
|
|
796
|
+
* Pose le `state:` d'un lot (`raf status`, L463) sous la garde de `plan.transitions`, et note le message daté. Le statut de base en est
|
|
797
|
+
* dérivé : seuls todo → doing (le lot démarre) et doing → todo (`started:` retiré ; refusé dès qu'un commit cite le lot, la porte de
|
|
798
|
+
* `raf todo`) se font ici, pour que l'état bloqué se quitte vers l'état d'avant quel que soit son statut de base ; les autres changements
|
|
799
|
+
* de base gardent leur commande et leurs portes (`raf done`, `drop`, `todo`). Entrer dans l'état bloqué mémorise l'état d'avant
|
|
800
|
+
* (`state_before:`), `back` y revient sans garde de transition. `commits` compte les commits qui citent le lot (lu seulement pour doing → todo).
|
|
801
|
+
*/
|
|
802
|
+
setState(ref, target, today, opts = {}, commits = () => 0) {
|
|
803
|
+
this.writable();
|
|
804
|
+
const model = this.settings.states;
|
|
805
|
+
if (!model)
|
|
806
|
+
throw new RafError('aucun état déclaré : cadence.yaml plan.states');
|
|
807
|
+
if (ref.includes('/'))
|
|
808
|
+
throw new RafError(`raf status se pose sur un lot, pas une sous-tâche : ${ref}`);
|
|
809
|
+
const lot = this.lot(ref);
|
|
810
|
+
const from = currentState(model, lot);
|
|
811
|
+
let to = target ?? '';
|
|
812
|
+
if (opts.back) {
|
|
813
|
+
if (!model.blocked)
|
|
814
|
+
throw new RafError('aucun état bloqué déclaré : cadence.yaml plan.blockedState');
|
|
815
|
+
if (from !== model.blocked)
|
|
816
|
+
throw new RafError(`${ref} n'est pas bloqué (état ${from})`);
|
|
817
|
+
if (!lot.stateBefore)
|
|
818
|
+
throw new RafError(`${ref} : état d'avant introuvable (state_before: absent) — raf status ${ref} <état>`);
|
|
819
|
+
if (!Object.hasOwn(model.states, lot.stateBefore))
|
|
820
|
+
throw new RafError(`${ref} : l'état d'avant « ${lot.stateBefore} » n'est plus déclaré — raf status ${ref} <état>`);
|
|
821
|
+
to = lot.stateBefore;
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
if (!Object.hasOwn(model.states, to))
|
|
825
|
+
throw new RafError(`état inconnu : ${to} (déclarés : ${Object.keys(model.states).join(', ')})`);
|
|
826
|
+
if (to === from)
|
|
827
|
+
throw new RafError(`${ref} est déjà ${to}`);
|
|
828
|
+
const refused = transitionProblem(model, from, to);
|
|
829
|
+
if (refused)
|
|
830
|
+
throw new RafError(`${ref} : ${refused}`);
|
|
831
|
+
}
|
|
832
|
+
const node = this.lotNode(ref);
|
|
833
|
+
const base = model.states[to];
|
|
834
|
+
if (base !== lot.status) {
|
|
835
|
+
const refused = rebase(node, lot, { ref, to, base }, today, commits);
|
|
836
|
+
if (refused)
|
|
837
|
+
throw new RafError(refused);
|
|
838
|
+
}
|
|
839
|
+
this.writeState(node, to, to === model.blocked ? from : undefined);
|
|
840
|
+
const message = opts.message?.trim();
|
|
841
|
+
if (message)
|
|
842
|
+
this.note(ref, `état ${to} : ${message}`, today);
|
|
843
|
+
return to;
|
|
844
|
+
}
|
|
845
|
+
/** Met l'état d'un lot au diapason de son nouveau statut de base (`start`, `done`, `drop`, `todo`) ; sans `plan.states`, ne touche à rien. */
|
|
846
|
+
syncState(node, status) {
|
|
847
|
+
if (this.settings.states)
|
|
848
|
+
this.writeState(node, defaultState(this.settings.states, status));
|
|
849
|
+
}
|
|
850
|
+
/** Écrit `state:` (et `state_before:`) juste après `status:` ; `undefined` retire la clé. */
|
|
851
|
+
writeState(node, state, before) {
|
|
852
|
+
for (const [key, value] of [['state', state], ['state_before', before]]) {
|
|
853
|
+
if (value === undefined)
|
|
854
|
+
node.delete(key);
|
|
855
|
+
else if (node.has(key))
|
|
856
|
+
node.set(key, value);
|
|
857
|
+
else {
|
|
858
|
+
const keys = node.items.map((p) => String(p.key?.value ?? p.key));
|
|
859
|
+
node.items.splice(Math.max(keys.indexOf('status'), keys.indexOf('state')) + 1, 0, this.doc.createPair(key, value));
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
775
863
|
/** Remplace le titre d'un lot, de tout statut, et note l'ancien titre ; refuse un titre vide ou identique. */
|
|
776
864
|
setTitle(ref, text, today) {
|
|
777
865
|
this.writable();
|
|
@@ -902,28 +990,6 @@ export class Plan {
|
|
|
902
990
|
}
|
|
903
991
|
}
|
|
904
992
|
/** Lit une liste écrite dans un autre format : champs traduits, entrées à parent repliées en sous-tâches. */
|
|
905
|
-
/** Fichier d'acquittement docs-ok d'un plan en lecture seule, relatif à la racine du dépôt (L386). */
|
|
906
|
-
const DOCS_OK_FILE = '.cadence/docs-ok.yaml';
|
|
907
|
-
/** Les acquittements du fichier : lot → { date, reason }. Une entrée sans raison n'acquitte rien ; un fichier illisible est une erreur qui le nomme. */
|
|
908
|
-
function readDocsOkFile(file) {
|
|
909
|
-
if (!file || !existsSync(file))
|
|
910
|
-
return {};
|
|
911
|
-
const doc = parseDocument(readFileSync(file, 'utf8'));
|
|
912
|
-
if (doc.errors.length > 0)
|
|
913
|
-
throw new RafError(`${file} : ${doc.errors[0].message}`);
|
|
914
|
-
const raw = doc.toJSON();
|
|
915
|
-
if (raw == null)
|
|
916
|
-
return {};
|
|
917
|
-
if (typeof raw !== 'object' || Array.isArray(raw))
|
|
918
|
-
throw new RafError(`${file} : la racine doit être un objet (lot → { date, reason })`);
|
|
919
|
-
const out = {};
|
|
920
|
-
for (const [id, v] of Object.entries(raw)) {
|
|
921
|
-
const e = v;
|
|
922
|
-
if (e && typeof e === 'object' && typeof e.reason === 'string' && e.reason.trim() !== '')
|
|
923
|
-
out[id] = { date: String(e.date ?? ''), reason: e.reason.trim() };
|
|
924
|
-
}
|
|
925
|
-
return out;
|
|
926
|
-
}
|
|
927
993
|
function foreignLots(raw, format) {
|
|
928
994
|
const entries = raw.map((r, i) => {
|
|
929
995
|
if (!r || typeof r !== 'object' || Array.isArray(r))
|
|
@@ -993,25 +1059,6 @@ function foreignLots(raw, format) {
|
|
|
993
1059
|
}
|
|
994
1060
|
return lots;
|
|
995
1061
|
}
|
|
996
|
-
/** Lit `gate: { count: N, spec: true }` ou `{ count: N, screen: page }` ; toute autre forme est un problème du lot et une condition jamais remplie. */
|
|
997
|
-
function readGate(raw, problems) {
|
|
998
|
-
if (raw == null)
|
|
999
|
-
return undefined;
|
|
1000
|
-
const invalid = () => {
|
|
1001
|
-
problems.push(GATE_INVALID);
|
|
1002
|
-
return { invalid: true };
|
|
1003
|
-
};
|
|
1004
|
-
if (typeof raw !== 'object' || Array.isArray(raw))
|
|
1005
|
-
return invalid();
|
|
1006
|
-
const g = raw;
|
|
1007
|
-
if (!Number.isInteger(g.count) || g.count < 1)
|
|
1008
|
-
return invalid();
|
|
1009
|
-
const criteria = [...(g.spec === true ? ['spec'] : []), ...(g.screen === 'page' ? ['screen'] : [])];
|
|
1010
|
-
const others = Object.keys(g).filter((k) => k !== 'count' && k !== 'spec' && k !== 'screen');
|
|
1011
|
-
if (criteria.length !== 1 || others.length || ('spec' in g && g.spec !== true) || ('screen' in g && g.screen !== 'page'))
|
|
1012
|
-
return invalid();
|
|
1013
|
-
return { count: g.count, criterion: criteria[0] };
|
|
1014
|
-
}
|
|
1015
1062
|
/** Un titre public vide ne dirait rien au public : refusé plutôt qu'écrit. */
|
|
1016
1063
|
function cleanPublic(text) {
|
|
1017
1064
|
const t = text.trim();
|
|
@@ -1019,116 +1066,3 @@ function cleanPublic(text) {
|
|
|
1019
1066
|
throw new RafError('titre public vide — raf public <id> "titre" (ou --clear pour l\'effacer)');
|
|
1020
1067
|
return t;
|
|
1021
1068
|
}
|
|
1022
|
-
function normalizeLot(raw, problems = []) {
|
|
1023
|
-
const status = STATUSES.includes(raw.status) ? raw.status : 'todo';
|
|
1024
|
-
const asDay = (field) => {
|
|
1025
|
-
const v = raw[field];
|
|
1026
|
-
if (v == null)
|
|
1027
|
-
return undefined;
|
|
1028
|
-
if (isDay(v))
|
|
1029
|
-
return v;
|
|
1030
|
-
problems.push(`${field} « ${String(v)} » n'est pas une date AAAA-MM-JJ`);
|
|
1031
|
-
return undefined;
|
|
1032
|
-
};
|
|
1033
|
-
const after = raw.after == null ? [] : Array.isArray(raw.after) ? raw.after.map(String) : [String(raw.after)];
|
|
1034
|
-
const last = asDay('last');
|
|
1035
|
-
let uses;
|
|
1036
|
-
if (raw.uses != null) {
|
|
1037
|
-
if (Array.isArray(raw.uses) && raw.uses.every((n) => typeof n === 'string' && RESOURCE_NAME.test(n)))
|
|
1038
|
-
uses = [...new Set(raw.uses)];
|
|
1039
|
-
else
|
|
1040
|
-
problems.push(`uses ${RESOURCES_BAD_LIST}`);
|
|
1041
|
-
}
|
|
1042
|
-
let screen;
|
|
1043
|
-
if (raw.screen != null) {
|
|
1044
|
-
if (SCREENS.includes(raw.screen))
|
|
1045
|
-
screen = raw.screen;
|
|
1046
|
-
else
|
|
1047
|
-
problems.push(`screen « ${String(raw.screen)} » inconnu (${SCREENS.join(', ')})`);
|
|
1048
|
-
if (screen && raw.visible !== true)
|
|
1049
|
-
problems.push(`screen « ${screen} » sans visible : un écran est un changement visible`);
|
|
1050
|
-
}
|
|
1051
|
-
let spec;
|
|
1052
|
-
if (raw.spec != null) {
|
|
1053
|
-
if (raw.spec === true)
|
|
1054
|
-
spec = true;
|
|
1055
|
-
else if (raw.spec !== false)
|
|
1056
|
-
problems.push(`spec : true ou false attendu (reçu « ${String(raw.spec)} »)`);
|
|
1057
|
-
}
|
|
1058
|
-
const gate = readGate(raw.gate, problems);
|
|
1059
|
-
const okRaw = raw.docs_ok;
|
|
1060
|
-
const docsOk = okRaw && typeof okRaw === 'object' && typeof okRaw.reason === 'string' && okRaw.reason.trim() !== '' ? { date: String(okRaw.date ?? ''), reason: okRaw.reason.trim() } : undefined;
|
|
1061
|
-
return {
|
|
1062
|
-
id: String(raw.id),
|
|
1063
|
-
title: String(raw.title ?? ''),
|
|
1064
|
-
status,
|
|
1065
|
-
estimate: typeof raw.estimate === 'number' && raw.estimate > 0 ? raw.estimate : 1,
|
|
1066
|
-
quickwin: raw.quickwin === true,
|
|
1067
|
-
visible: raw.visible === true,
|
|
1068
|
-
...(screen ? { screen } : {}),
|
|
1069
|
-
...(spec ? { spec } : {}), ...(raw.local === true ? { local: true } : {}),
|
|
1070
|
-
...(gate ? { gate } : {}),
|
|
1071
|
-
...(typeof raw.public === 'string' && raw.public.trim() !== '' ? { public: raw.public.trim() } : {}),
|
|
1072
|
-
...(typeof raw.goal === 'string' && raw.goal.trim() !== '' ? { goal: raw.goal.trim() } : {}),
|
|
1073
|
-
...(Number.isInteger(raw.every) && raw.every > 0 ? { every: raw.every } : {}),
|
|
1074
|
-
...(last ? { last } : {}),
|
|
1075
|
-
after,
|
|
1076
|
-
...(uses?.length ? { uses } : {}),
|
|
1077
|
-
created: asDay('created'),
|
|
1078
|
-
started: asDay('started'),
|
|
1079
|
-
finished: asDay('finished'),
|
|
1080
|
-
notes: Array.isArray(raw.notes)
|
|
1081
|
-
? raw.notes.map((n) => ({ date: String(n.date ?? ''), text: String(n.text ?? '') }))
|
|
1082
|
-
: [],
|
|
1083
|
-
tasks: Array.isArray(raw.tasks)
|
|
1084
|
-
? raw.tasks.map((t) => ({
|
|
1085
|
-
id: String(t.id),
|
|
1086
|
-
title: String(t.title ?? ''),
|
|
1087
|
-
status: STATUSES.includes(t.status) ? t.status : 'todo',
|
|
1088
|
-
}))
|
|
1089
|
-
: [],
|
|
1090
|
-
...(repoList(raw.repos).length ? { repos: repoList(raw.repos) } : {}),
|
|
1091
|
-
...(asVerdict(raw.ux) ? { ux: asVerdict(raw.ux) } : {}),
|
|
1092
|
-
...(asVerdict(raw.review) ? { review: asVerdict(raw.review) } : {}),
|
|
1093
|
-
...(docsOk ? { docsOk } : {}),
|
|
1094
|
-
problems,
|
|
1095
|
-
};
|
|
1096
|
-
}
|
|
1097
|
-
/**
|
|
1098
|
-
* Dépôts voisins déclarés : une chaîne (le chemin) ou un objet `{ path, cite }`, seuls ou en liste (une valeur seule vaut une
|
|
1099
|
-
* liste d'un dépôt). Une entrée sans chemin textuel est écartée ; un `cite` vide ou non textuel vaut pas de `cite`.
|
|
1100
|
-
*/
|
|
1101
|
-
function repoList(raw) {
|
|
1102
|
-
const items = Array.isArray(raw) ? raw : raw == null ? [] : [raw];
|
|
1103
|
-
const out = [];
|
|
1104
|
-
for (const item of items) {
|
|
1105
|
-
const path = typeof item === 'string' ? item : item && typeof item === 'object' && typeof item.path === 'string' ? item.path : '';
|
|
1106
|
-
if (path.trim() === '')
|
|
1107
|
-
continue;
|
|
1108
|
-
const cite = item && typeof item === 'object' ? item.cite : undefined;
|
|
1109
|
-
out.push({ path: path.trim(), ...(typeof cite === 'string' && cite.trim() !== '' ? { cite: cite.trim() } : {}) });
|
|
1110
|
-
}
|
|
1111
|
-
return out;
|
|
1112
|
-
}
|
|
1113
|
-
function asVerdict(raw) {
|
|
1114
|
-
if (!raw || typeof raw !== 'object')
|
|
1115
|
-
return undefined;
|
|
1116
|
-
const v = raw;
|
|
1117
|
-
// Écrit à la main sans verdict (clé absente, vide ou blanche) : rien n'a été dit de la revue, la porte
|
|
1118
|
-
// reste fermée — comme `raf ux` et `raf review` refusent d'enregistrer un verdict vide.
|
|
1119
|
-
if (String(v.verdict ?? '').trim() === '')
|
|
1120
|
-
return undefined;
|
|
1121
|
-
const verdict = { date: String(v.date ?? ''), verdict: String(v.verdict) };
|
|
1122
|
-
// Champ présent mais vide : relu « jusqu'à rien », comme un verdict noté sans commit.
|
|
1123
|
-
if ('commit' in v)
|
|
1124
|
-
verdict.commit = v.commit == null || String(v.commit).trim() === '' ? null : String(v.commit).trim();
|
|
1125
|
-
if (v.repos && typeof v.repos === 'object' && !Array.isArray(v.repos)) {
|
|
1126
|
-
const repos = Object.entries(v.repos).map(([k, sha]) => [k, sha == null || String(sha).trim() === '' ? null : String(sha).trim()]);
|
|
1127
|
-
if (repos.length)
|
|
1128
|
-
verdict.repos = Object.fromEntries(repos);
|
|
1129
|
-
}
|
|
1130
|
-
const r = v.reporte;
|
|
1131
|
-
if (r && typeof r === 'object' && !Array.isArray(r) && r.de != null && r.vers != null)
|
|
1132
|
-
verdict.reporte = { de: String(r.de), vers: String(r.vers), patchId: String(r.patchId ?? '') };
|
|
1133
|
-
return verdict;
|
|
1134
|
-
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { readOrchestrateConfig } from './config.js';
|
|
2
|
+
import { gitRoot } from './git.js';
|
|
3
|
+
import { redAcceptations } from './orchestrate/accept.js';
|
|
4
|
+
import { RafError } from './plan.js';
|
|
5
|
+
/**
|
|
6
|
+
* Un lot clos avec une acceptation à vert: false bloque les vagues suivantes du dépôt (verrou des tests d'acceptation, L366) :
|
|
7
|
+
* `raf done` le refuse, ou avec `--force` n'en fait qu'un avertissement sur `warn`.
|
|
8
|
+
*/
|
|
9
|
+
export function refuseRedAcceptation(root, configPath, lotId, force, warn) {
|
|
10
|
+
const repo = gitRoot(root) ?? root;
|
|
11
|
+
let dir = null;
|
|
12
|
+
try {
|
|
13
|
+
dir = readOrchestrateConfig(configPath).spec.dir;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// un cadence.yaml illisible ne doit pas empêcher de clore un lot : `cadence check` en parle
|
|
17
|
+
}
|
|
18
|
+
const red = dir === null ? undefined : redAcceptations(repo, dir).find((a) => a.lot === lotId);
|
|
19
|
+
if (!red)
|
|
20
|
+
return;
|
|
21
|
+
const text = `${lotId} : ${red.path} est à vert: false — clos ainsi, le lot bloquerait les vagues suivantes du dépôt (verrou des tests d'acceptation) ; cadence spec green ${lotId} lance la commande d'acceptation et pose vert: true`;
|
|
22
|
+
if (!force)
|
|
23
|
+
throw new RafError(`${text} (--force pour passer outre)`);
|
|
24
|
+
warn(`avertissement : ${text}`);
|
|
25
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { RafError } from './plan.js';
|
|
2
|
+
import { currentState } from './plan-states.js';
|
|
3
|
+
/** Une ligne par état déclaré : son statut de base, le nombre de lots qui y sont et, si `plan.transitions` est déclaré, les états permis ensuite. */
|
|
4
|
+
function listStates(plan, out) {
|
|
5
|
+
const model = plan.stateModel;
|
|
6
|
+
if (!model)
|
|
7
|
+
throw new RafError('aucun état déclaré : cadence.yaml plan.states');
|
|
8
|
+
const lots = plan.lots();
|
|
9
|
+
for (const [name, base] of Object.entries(model.states)) {
|
|
10
|
+
const next = model.transitions ? ` → ${model.transitions[name]?.join(', ') || '(terminal)'}` : '';
|
|
11
|
+
out(`${name} ${base} ${lots.filter((l) => currentState(model, l) === name).length} lot(s)${next}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** `raf status` (L463) : sans argument la liste des états ; `<id> <état> ["message"]` ou `<id> --back ["message"]` pose l'état d'un lot. */
|
|
15
|
+
export function statusCommand(plan, rest, ctx) {
|
|
16
|
+
const { back, today, out } = ctx;
|
|
17
|
+
if (rest.length === 0 && !back) {
|
|
18
|
+
listStates(plan, out);
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
const first = back ? 1 : 2;
|
|
22
|
+
if (rest.length < first)
|
|
23
|
+
throw new RafError('usage : raf status <id> <état> ["message"] | raf status <id> --back ["message"]');
|
|
24
|
+
const state = plan.setState(rest[0], back ? null : rest[1], today, { back, message: rest.slice(first).join(' ') }, () => ctx.commits(plan, rest[0]));
|
|
25
|
+
plan.save();
|
|
26
|
+
out(`${rest[0]} → ${state}`);
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
package/dist/session.js
CHANGED
|
@@ -146,8 +146,14 @@ export function sessionStart(ctx, opts) {
|
|
|
146
146
|
writeSessionMark(ctx.state, { kind: 'start', at: (ctx.now ?? new Date()).toISOString() });
|
|
147
147
|
return 0;
|
|
148
148
|
}
|
|
149
|
+
/** La section « Mémoire » (L442), calculée avant tout rapport ; vide sans mesure demandée. */
|
|
150
|
+
function memoryLines(ctx) {
|
|
151
|
+
return ctx.memory?.() ?? [];
|
|
152
|
+
}
|
|
149
153
|
export function sessionClose(ctx, opts) {
|
|
150
154
|
const { plan, out, today } = ctx;
|
|
155
|
+
// Mesuré avant tout rapport : une clé memory.indexMaxBytes invalide arrête la clôture sans rien imprimer.
|
|
156
|
+
const memory = memoryLines(ctx);
|
|
151
157
|
out(`${plan.project} — clôture du ${today}`);
|
|
152
158
|
const window = closeWindow(ctx, opts.since);
|
|
153
159
|
const recent = period(ctx, window.since);
|
|
@@ -169,6 +175,8 @@ export function sessionClose(ctx, opts) {
|
|
|
169
175
|
const lock = lockStatus(ctx.shared);
|
|
170
176
|
section(out, 'Dépôt', [repo.line, ...(lock ? [lock.line] : [])]);
|
|
171
177
|
section(out, 'Faits propres au projet', projectFacts(ctx, window.since));
|
|
178
|
+
// Comme le nettoyage : un avertissement qui propose, hors des points ouverts ; l'index n'est jamais écrit ici.
|
|
179
|
+
memory.forEach((l) => out(l));
|
|
172
180
|
// Une proposition, pas une condition : rien n'est supprimé ici, le skill demande l'accord. Le
|
|
173
181
|
// nettoyage ne change jamais le verdict et ne fait jamais tomber la clôture.
|
|
174
182
|
if (ctx.clean?.patterns.length) {
|
package/dist/state.js
CHANGED
|
@@ -203,6 +203,29 @@ export function appendFailedDelivery(dir, day, sha, reason, cls, tail = []) {
|
|
|
203
203
|
const head = `${day} ${sha} ${cls ? `[${cls}] ` : ''}${clean(reason.split('\n')[0], REASON_MAX)}`;
|
|
204
204
|
appendFileSync(join(dir, 'deliveries-failed.log'), `${[head, ...kept].join(TAIL_SEP)}\n`);
|
|
205
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* `cadence verify` vert au sha de la DERNIÈRE livraison rouge (L435) : l'effet est bien là, la livraison est donc
|
|
208
|
+
* faite — une ligne « jour sha » rejoint `deliveries.log`, ce qui efface la ligne rouge du panneau (le journal vert
|
|
209
|
+
* modifié après le rouge la remplace) sans relivrer. Rien n'est écrit quand la dernière livraison rouge est celle
|
|
210
|
+
* d'un autre sha (elle resterait cachée), quand elle n'est pas de classe « vérification » (une CI rouge, un refus ou
|
|
211
|
+
* un déploiement raté ne se lavent pas : les vérifications ne lisent pas toujours le sha), quand il n'y en a pas, ou quand le journal vert est déjà postérieur au
|
|
212
|
+
* rouge (les dates de modification des fichiers décident, comme au panneau). Retourne si une ligne a été écrite.
|
|
213
|
+
*/
|
|
214
|
+
export function recordVerifiedDelivery(dir, day, sha) {
|
|
215
|
+
const last = readFailedEntries(dir).at(-1);
|
|
216
|
+
if (!last || last.sha !== sha || last.cls !== 'verification')
|
|
217
|
+
return false;
|
|
218
|
+
try {
|
|
219
|
+
const green = join(dir, 'deliveries.log');
|
|
220
|
+
if (existsSync(green) && statSync(green).mtimeMs > statSync(join(dir, 'deliveries-failed.log')).mtimeMs)
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
appendDelivery(dir, day, sha);
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
206
229
|
export function lastDelivery(dir) {
|
|
207
230
|
const file = join(dir, 'deliveries.log');
|
|
208
231
|
if (!existsSync(file))
|
package/dist/verify.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { constants } from 'node:os';
|
|
3
3
|
import { headSha, repoStatus, upstreamHead, upstreamIsDefaultBranch } from './git.js';
|
|
4
|
-
import { lastDelivery, sharedStateDir } from './state.js';
|
|
4
|
+
import { lastDelivery, recordVerifiedDelivery, sharedStateDir } from './state.js';
|
|
5
5
|
import { onTermination } from './proc.js';
|
|
6
|
-
import { describeCheck, realDeps, retryCheck, TIMED_OUT } from './deliver.js';
|
|
6
|
+
import { announceDelivered, describeCheck, realDeps, retryCheck, TIMED_OUT } from './deliver.js';
|
|
7
7
|
function killGroup(pgid, sig) {
|
|
8
8
|
try {
|
|
9
9
|
process.kill(-pgid, sig);
|
|
@@ -115,7 +115,24 @@ export async function verifyCommand(ctx, deps) {
|
|
|
115
115
|
for (const r of results)
|
|
116
116
|
out(resultLine(r));
|
|
117
117
|
out(summaryLine(results));
|
|
118
|
-
|
|
118
|
+
if (results.some((r) => r.reason !== null))
|
|
119
|
+
return 1;
|
|
120
|
+
if (ctx.journal) {
|
|
121
|
+
const prev = lastDelivery(ctx.journal.dir); // lu avant l'écriture : la livraison précédente, d'où partent les lots annoncés
|
|
122
|
+
if (recordVerifiedDelivery(ctx.journal.dir, ctx.journal.day, ctx.sha)) {
|
|
123
|
+
out(`verify : ${ctx.sha.slice(0, 7)} avait une livraison rouge, l'effet est vert — inscrit au journal des livraisons`);
|
|
124
|
+
let effects;
|
|
125
|
+
try {
|
|
126
|
+
effects = ctx.journal.effects();
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
out(`verify : lots livrés non annoncés — ${e.message.split('\n')[0]}`);
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
announceDelivered(effects, prev, ctx.sha);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return 0;
|
|
119
136
|
}
|
|
120
137
|
/**
|
|
121
138
|
* Délai de chaque vérification de « session start », lancées en parallèle : c'est aussi, à la mise à mort
|
package/dist/yaml-keys.js
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1100 300" width="1100" height="300" role="img" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif" font-size="13" fill="#e6edf3">
|
|
2
|
+
<title>Delivery up to a sha</title>
|
|
3
|
+
<desc>Commits stack on main in order; the deliverable sha is the last commit of the highest ready lot with no unfinished lot below it, so a handed-back lot blocks the commits above it.</desc>
|
|
4
|
+
<defs><marker id="arr" 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
|
+
<rect width="1100" height="300" fill="#0d1117"/>
|
|
6
|
+
<text x="20" y="30" text-anchor="start" font-size="11.5" font-weight="600" fill="#8b949e">main, oldest on the left</text>
|
|
7
|
+
<rect x="40" y="60" width="150" height="60" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1.5"/>
|
|
8
|
+
<text x="115" y="86" text-anchor="middle" font-weight="600" font-size="13.5" fill="#e6edf3">a1 · L1</text>
|
|
9
|
+
<text x="115" y="103" text-anchor="middle" font-size="11.5" fill="#8b949e">ready</text>
|
|
10
|
+
<polyline points="190,90 238,90" fill="none" stroke="#8b949e" stroke-width="1.5" marker-end="url(#arr)"/>
|
|
11
|
+
<rect x="240" y="60" width="150" height="60" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1.5"/>
|
|
12
|
+
<text x="315" y="86" text-anchor="middle" font-weight="600" font-size="13.5" fill="#e6edf3">a2 · L1</text>
|
|
13
|
+
<text x="315" y="103" text-anchor="middle" font-size="11.5" fill="#8b949e">ready</text>
|
|
14
|
+
<polyline points="390,90 438,90" fill="none" stroke="#8b949e" stroke-width="1.5" marker-end="url(#arr)"/>
|
|
15
|
+
<rect x="440" y="60" width="150" height="60" rx="6" fill="#0c2d6b" stroke="#58a6ff" stroke-width="2"/>
|
|
16
|
+
<text x="515" y="86" text-anchor="middle" font-weight="600" font-size="13.5" fill="#e6edf3">b1 · L2</text>
|
|
17
|
+
<text x="515" y="103" text-anchor="middle" font-size="11.5" fill="#8b949e">ready</text>
|
|
18
|
+
<polyline points="590,90 638,90" fill="none" stroke="#8b949e" stroke-width="1.5" marker-end="url(#arr)"/>
|
|
19
|
+
<rect x="640" y="60" width="150" height="60" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1.5" stroke-dasharray="6 4"/>
|
|
20
|
+
<text x="715" y="86" text-anchor="middle" font-weight="600" font-size="13.5" fill="#e6edf3">c1 · L3</text>
|
|
21
|
+
<text x="715" y="103" text-anchor="middle" font-size="11.5" fill="#8b949e">handed back</text>
|
|
22
|
+
<polyline points="790,90 838,90" fill="none" stroke="#8b949e" stroke-width="1.5" marker-end="url(#arr)"/>
|
|
23
|
+
<rect x="840" y="60" width="150" height="60" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1.5" stroke-dasharray="6 4"/>
|
|
24
|
+
<text x="915" y="86" text-anchor="middle" font-weight="600" font-size="13.5" fill="#e6edf3">d1 · L4</text>
|
|
25
|
+
<text x="915" y="103" text-anchor="middle" font-size="11.5" fill="#8b949e">ready</text>
|
|
26
|
+
<rect x="40" y="136" width="550" height="4" fill="#58a6ff"/>
|
|
27
|
+
<text x="315" y="160" text-anchor="middle" font-size="11.5" fill="#e6edf3">delivered together</text>
|
|
28
|
+
<text x="315" y="205" text-anchor="middle" font-size="13" font-weight="600" fill="#e6edf3">« livrable jusqu'à b1 »</text>
|
|
29
|
+
<text x="315" y="227" text-anchor="middle" font-size="11.5" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" fill="#8b949e">git push origin b1:main && cadence deliver --sha b1</text>
|
|
30
|
+
<text x="840" y="160" text-anchor="middle" font-size="11.5" fill="#8b949e">blocked by L3: not pushed, not delivered</text>
|
|
31
|
+
<text x="550" y="270" text-anchor="middle" font-size="11.5" fill="#8b949e">Pushing HEAD would also put the unreviewed lot on main; the CI builds only the tip of a push.</text>
|
|
32
|
+
</svg>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1100 300" width="1100" height="300" role="img" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif" font-size="13" fill="#1f2328">
|
|
2
|
+
<title>Delivery up to a sha</title>
|
|
3
|
+
<desc>Commits stack on main in order; the deliverable sha is the last commit of the highest ready lot with no unfinished lot below it, so a handed-back lot blocks the commits above it.</desc>
|
|
4
|
+
<defs><marker id="arr" 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
|
+
<rect width="1100" height="300" fill="#ffffff"/>
|
|
6
|
+
<text x="20" y="30" text-anchor="start" font-size="11.5" font-weight="600" fill="#57606a">main, oldest on the left</text>
|
|
7
|
+
<rect x="40" y="60" width="150" height="60" rx="6" fill="#ffffff" stroke="#57606a" stroke-width="1.5"/>
|
|
8
|
+
<text x="115" y="86" text-anchor="middle" font-weight="600" font-size="13.5" fill="#1f2328">a1 · L1</text>
|
|
9
|
+
<text x="115" y="103" text-anchor="middle" font-size="11.5" fill="#57606a">ready</text>
|
|
10
|
+
<polyline points="190,90 238,90" fill="none" stroke="#57606a" stroke-width="1.5" marker-end="url(#arr)"/>
|
|
11
|
+
<rect x="240" y="60" width="150" height="60" rx="6" fill="#ffffff" stroke="#57606a" stroke-width="1.5"/>
|
|
12
|
+
<text x="315" y="86" text-anchor="middle" font-weight="600" font-size="13.5" fill="#1f2328">a2 · L1</text>
|
|
13
|
+
<text x="315" y="103" text-anchor="middle" font-size="11.5" fill="#57606a">ready</text>
|
|
14
|
+
<polyline points="390,90 438,90" fill="none" stroke="#57606a" stroke-width="1.5" marker-end="url(#arr)"/>
|
|
15
|
+
<rect x="440" y="60" width="150" height="60" rx="6" fill="#ddf4ff" stroke="#0969da" stroke-width="2"/>
|
|
16
|
+
<text x="515" y="86" text-anchor="middle" font-weight="600" font-size="13.5" fill="#1f2328">b1 · L2</text>
|
|
17
|
+
<text x="515" y="103" text-anchor="middle" font-size="11.5" fill="#57606a">ready</text>
|
|
18
|
+
<polyline points="590,90 638,90" fill="none" stroke="#57606a" stroke-width="1.5" marker-end="url(#arr)"/>
|
|
19
|
+
<rect x="640" y="60" width="150" height="60" rx="6" fill="#ffffff" stroke="#57606a" stroke-width="1.5" stroke-dasharray="6 4"/>
|
|
20
|
+
<text x="715" y="86" text-anchor="middle" font-weight="600" font-size="13.5" fill="#1f2328">c1 · L3</text>
|
|
21
|
+
<text x="715" y="103" text-anchor="middle" font-size="11.5" fill="#57606a">handed back</text>
|
|
22
|
+
<polyline points="790,90 838,90" fill="none" stroke="#57606a" stroke-width="1.5" marker-end="url(#arr)"/>
|
|
23
|
+
<rect x="840" y="60" width="150" height="60" rx="6" fill="#ffffff" stroke="#57606a" stroke-width="1.5" stroke-dasharray="6 4"/>
|
|
24
|
+
<text x="915" y="86" text-anchor="middle" font-weight="600" font-size="13.5" fill="#1f2328">d1 · L4</text>
|
|
25
|
+
<text x="915" y="103" text-anchor="middle" font-size="11.5" fill="#57606a">ready</text>
|
|
26
|
+
<rect x="40" y="136" width="550" height="4" fill="#0969da"/>
|
|
27
|
+
<text x="315" y="160" text-anchor="middle" font-size="11.5" fill="#1f2328">delivered together</text>
|
|
28
|
+
<text x="315" y="205" text-anchor="middle" font-size="13" font-weight="600" fill="#1f2328">« livrable jusqu'à b1 »</text>
|
|
29
|
+
<text x="315" y="227" text-anchor="middle" font-size="11.5" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" fill="#57606a">git push origin b1:main && cadence deliver --sha b1</text>
|
|
30
|
+
<text x="840" y="160" text-anchor="middle" font-size="11.5" fill="#57606a">blocked by L3: not pushed, not delivered</text>
|
|
31
|
+
<text x="550" y="270" text-anchor="middle" font-size="11.5" fill="#57606a">Pushing HEAD would also put the unreviewed lot on main; the CI builds only the tip of a push.</text>
|
|
32
|
+
</svg>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1100 300" width="1100" height="300" role="img" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif" font-size="13" fill="#e6edf3">
|
|
2
|
+
<title>Journey of a lot</title>
|
|
3
|
+
<desc>A lot goes through spec, code, review, correction when the review is not compliant, UX if the lot is visible, then ready; below each step, who does it.</desc>
|
|
4
|
+
<defs><marker id="arr" 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
|
+
<rect width="1100" height="300" fill="#0d1117"/>
|
|
6
|
+
<text x="20" y="26" text-anchor="start" font-size="11.5" font-weight="600" fill="#8b949e">STEP</text>
|
|
7
|
+
<text x="20" y="168" text-anchor="start" font-size="11.5" font-weight="600" fill="#8b949e">WHO</text>
|
|
8
|
+
<rect x="20" y="40" width="160" height="60" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1.5" stroke-dasharray="6 4"/>
|
|
9
|
+
<text x="100" y="66" text-anchor="middle" font-weight="600" font-size="13.5" fill="#e6edf3">spec</text>
|
|
10
|
+
<text x="100" y="83" text-anchor="middle" font-size="11.5" fill="#8b949e">if screen or spec: true</text>
|
|
11
|
+
<rect x="200" y="40" width="160" height="60" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1.5"/>
|
|
12
|
+
<text x="280" y="66" text-anchor="middle" font-weight="600" font-size="13.5" fill="#e6edf3">code</text>
|
|
13
|
+
<text x="280" y="83" text-anchor="middle" font-size="11.5" fill="#8b949e">test first</text>
|
|
14
|
+
<rect x="380" y="40" width="160" height="60" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1.5"/>
|
|
15
|
+
<text x="460" y="66" text-anchor="middle" font-weight="600" font-size="13.5" fill="#e6edf3">review</text>
|
|
16
|
+
<text x="460" y="83" text-anchor="middle" font-size="11.5" fill="#8b949e">code</text>
|
|
17
|
+
<rect x="560" y="40" width="160" height="60" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1.5" stroke-dasharray="6 4"/>
|
|
18
|
+
<text x="640" y="66" text-anchor="middle" font-weight="600" font-size="13.5" fill="#e6edf3">correction</text>
|
|
19
|
+
<text x="640" y="83" text-anchor="middle" font-size="11.5" fill="#8b949e">if not compliant</text>
|
|
20
|
+
<rect x="740" y="40" width="160" height="60" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1.5" stroke-dasharray="6 4"/>
|
|
21
|
+
<text x="820" y="66" text-anchor="middle" font-weight="600" font-size="13.5" fill="#e6edf3">UX</text>
|
|
22
|
+
<text x="820" y="83" text-anchor="middle" font-size="11.5" fill="#8b949e">if visible</text>
|
|
23
|
+
<rect x="920" y="40" width="160" height="60" rx="6" fill="#0c2d6b" stroke="#58a6ff" stroke-width="2"/>
|
|
24
|
+
<text x="1000" y="66" text-anchor="middle" font-weight="600" font-size="13.5" fill="#e6edf3">ready</text>
|
|
25
|
+
<text x="1000" y="83" text-anchor="middle" font-size="11.5" fill="#8b949e">integrated (worktree mode)</text>
|
|
26
|
+
<polyline points="180,70 198,70" fill="none" stroke="#8b949e" stroke-width="1.5" marker-end="url(#arr)"/>
|
|
27
|
+
<polyline points="360,70 378,70" fill="none" stroke="#8b949e" stroke-width="1.5" marker-end="url(#arr)"/>
|
|
28
|
+
<polyline points="540,70 558,70" fill="none" stroke="#8b949e" stroke-width="1.5" marker-end="url(#arr)"/>
|
|
29
|
+
<polyline points="720,70 738,70" fill="none" stroke="#8b949e" stroke-width="1.5" marker-end="url(#arr)"/>
|
|
30
|
+
<polyline points="900,70 918,70" fill="none" stroke="#8b949e" stroke-width="1.5" marker-end="url(#arr)"/>
|
|
31
|
+
<polyline points="640,40 640,22 460,22 460,38" fill="none" stroke="#8b949e" stroke-width="1.5" stroke-dasharray="5 3" marker-end="url(#arr)"/>
|
|
32
|
+
<text x="550" y="16" text-anchor="middle" font-size="11.5" fill="#8b949e">back to the review after each correction</text>
|
|
33
|
+
<rect x="20" y="180" width="160" height="56" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1"/>
|
|
34
|
+
<text x="100" y="204" text-anchor="middle" font-size="12.5" font-weight="600" fill="#e6edf3">spec-writer</text>
|
|
35
|
+
<text x="100" y="222" text-anchor="middle" font-size="11.5" fill="#8b949e">then reviewer</text>
|
|
36
|
+
<rect x="200" y="180" width="160" height="56" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1"/>
|
|
37
|
+
<text x="280" y="204" text-anchor="middle" font-size="12.5" font-weight="600" fill="#e6edf3">fresh session</text>
|
|
38
|
+
<text x="280" y="222" text-anchor="middle" font-size="11.5" fill="#8b949e">of the program</text>
|
|
39
|
+
<rect x="380" y="180" width="160" height="56" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1"/>
|
|
40
|
+
<text x="460" y="204" text-anchor="middle" font-size="12.5" font-weight="600" fill="#e6edf3">code-reviewer</text>
|
|
41
|
+
<text x="460" y="222" text-anchor="middle" font-size="11.5" fill="#8b949e">agent</text>
|
|
42
|
+
<rect x="560" y="180" width="160" height="56" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1"/>
|
|
43
|
+
<text x="640" y="204" text-anchor="middle" font-size="12.5" font-weight="600" fill="#e6edf3">fresh session</text>
|
|
44
|
+
<text x="640" y="222" text-anchor="middle" font-size="11.5" fill="#8b949e">2 passes, 3 if it converges</text>
|
|
45
|
+
<rect x="740" y="180" width="160" height="56" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1"/>
|
|
46
|
+
<text x="820" y="204" text-anchor="middle" font-size="12.5" font-weight="600" fill="#e6edf3">ux-reviewer</text>
|
|
47
|
+
<text x="820" y="222" text-anchor="middle" font-size="11.5" fill="#8b949e">then the human</text>
|
|
48
|
+
<rect x="920" y="180" width="160" height="56" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1"/>
|
|
49
|
+
<text x="1000" y="204" text-anchor="middle" font-size="12.5" font-weight="600" fill="#e6edf3">the lead</text>
|
|
50
|
+
<text x="1000" y="222" text-anchor="middle" font-size="11.5" fill="#8b949e">re-checks, raf done</text>
|
|
51
|
+
<text x="550" y="270" text-anchor="middle" font-size="11.5" fill="#8b949e">Dashed: a conditional step. The program chains the sessions; the verdicts of the review and of the UX pass are recorded in the plan.</text>
|
|
52
|
+
</svg>
|