@sylad/cadence 1.40.0 → 1.42.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/spec-writer.md +1 -0
  4. package/agents/ux-reviewer.md +2 -0
  5. package/dist/archive.js +103 -6
  6. package/dist/audit.js +2 -2
  7. package/dist/clean.js +0 -4
  8. package/dist/cli/context.js +129 -0
  9. package/dist/cli/deliver-commands.js +114 -0
  10. package/dist/cli/gates.js +234 -0
  11. package/dist/cli/lead-commands.js +161 -0
  12. package/dist/cli/lot-commands.js +257 -0
  13. package/dist/cli/read-commands.js +227 -0
  14. package/dist/cli/session-commands.js +152 -0
  15. package/dist/cli/setup-commands.js +72 -0
  16. package/dist/cli/spec-commands.js +298 -0
  17. package/dist/cli/stats-commands.js +138 -0
  18. package/dist/cli-add.js +37 -0
  19. package/dist/cli.js +85 -1669
  20. package/dist/config.js +21 -108
  21. package/dist/corrections.js +28 -42
  22. package/dist/deliver-lots.js +37 -0
  23. package/dist/deliver.js +15 -39
  24. package/dist/docs-ok-file.js +25 -0
  25. package/dist/docsync.js +15 -3
  26. package/dist/lead.js +6 -24
  27. package/dist/lot-lines.js +51 -0
  28. package/dist/orchestrate/attente.js +82 -0
  29. package/dist/orchestrate/command.js +26 -25
  30. package/dist/orchestrate/continue.js +6 -14
  31. package/dist/orchestrate/cycle.js +18 -27
  32. package/dist/orchestrate/integrate.js +7 -2
  33. package/dist/orchestrate/integration-vivante.js +138 -0
  34. package/dist/orchestrate/launch.js +6 -18
  35. package/dist/orchestrate/lock.js +3 -20
  36. package/dist/orchestrate/lot-budget.js +2 -2
  37. package/dist/orchestrate/maquette.js +3 -0
  38. package/dist/orchestrate/registry.js +8 -1
  39. package/dist/orchestrate/schemas.js +5 -1
  40. package/dist/orchestrate/snapshot.js +13 -0
  41. package/dist/orchestrate/spec.js +51 -6
  42. package/dist/orchestrate/state.js +17 -0
  43. package/dist/plan-extras.js +119 -0
  44. package/dist/plan-ids.js +95 -0
  45. package/dist/plan-read.js +146 -0
  46. package/dist/plan-states.js +119 -0
  47. package/dist/plan.js +119 -190
  48. package/dist/raf-close-checks.js +25 -0
  49. package/dist/raf-status.js +28 -0
  50. package/dist/ref/command.js +2 -3
  51. package/dist/resources.js +3 -9
  52. package/dist/state.js +31 -8
  53. package/dist/verify-gate.js +33 -2
  54. package/dist/verify.js +20 -3
  55. package/dist/yaml-keys.js +21 -0
  56. package/docs/cadence-hud.md +1 -1
  57. package/docs/deliver.md +25 -3
  58. package/docs/fonctionnement.md +9 -5
  59. package/docs/lead.md +9 -7
  60. package/docs/news.md +2 -0
  61. package/docs/orchestrate.md +24 -7
  62. package/docs/perf.md +1 -0
  63. package/docs/qualite.md +14 -4
  64. package/docs/raf.md +62 -7
  65. package/docs/spec.md +5 -2
  66. package/docs/whats-new.md +9 -0
  67. package/package.json +1 -1
  68. package/skills/lead/SKILL.md +1 -1
  69. package/templates/orchestrate/spec.md +2 -2
@@ -0,0 +1,146 @@
1
+ import { isDay } from './dates.js';
2
+ import { RESOURCE_NAME, RESOURCES_BAD_LIST, SCREENS, STATUSES } from './plan.js';
3
+ import { readParent } from './plan-ids.js';
4
+ const GATE_INVALID = 'gate invalide : count: entier ≥ 1 et un critère spec: true ou screen: page attendus';
5
+ /** 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. */
6
+ function readGate(raw, problems) {
7
+ if (raw == null)
8
+ return undefined;
9
+ const invalid = () => {
10
+ problems.push(GATE_INVALID);
11
+ return { invalid: true };
12
+ };
13
+ if (typeof raw !== 'object' || Array.isArray(raw))
14
+ return invalid();
15
+ const g = raw;
16
+ if (!Number.isInteger(g.count) || g.count < 1)
17
+ return invalid();
18
+ const criteria = [...(g.spec === true ? ['spec'] : []), ...(g.screen === 'page' ? ['screen'] : [])];
19
+ const others = Object.keys(g).filter((k) => k !== 'count' && k !== 'spec' && k !== 'screen');
20
+ if (criteria.length !== 1 || others.length || ('spec' in g && g.spec !== true) || ('screen' in g && g.screen !== 'page'))
21
+ return invalid();
22
+ return { count: g.count, criterion: criteria[0] };
23
+ }
24
+ /** `state:` et `state_before:` écrits à la main avec un non-texte : signalés, la clé étant écartée. */
25
+ function checkStateNames(raw, problems) {
26
+ for (const k of ['state', 'state_before'])
27
+ if (raw[k] != null && typeof raw[k] !== 'string')
28
+ problems.push(`${k} : un nom d'état attendu (reçu « ${String(raw[k])} »)`);
29
+ }
30
+ export function normalizeLot(raw, problems = []) {
31
+ const status = STATUSES.includes(raw.status) ? raw.status : 'todo';
32
+ const asDay = (field) => {
33
+ const v = raw[field];
34
+ if (v == null)
35
+ return undefined;
36
+ if (isDay(v))
37
+ return v;
38
+ problems.push(`${field} « ${String(v)} » n'est pas une date AAAA-MM-JJ`);
39
+ return undefined;
40
+ };
41
+ const after = raw.after == null ? [] : Array.isArray(raw.after) ? raw.after.map(String) : [String(raw.after)];
42
+ const last = asDay('last');
43
+ let uses;
44
+ if (raw.uses != null) {
45
+ if (Array.isArray(raw.uses) && raw.uses.every((n) => typeof n === 'string' && RESOURCE_NAME.test(n)))
46
+ uses = [...new Set(raw.uses)];
47
+ else
48
+ problems.push(`uses ${RESOURCES_BAD_LIST}`);
49
+ }
50
+ let screen;
51
+ if (raw.screen != null) {
52
+ if (SCREENS.includes(raw.screen))
53
+ screen = raw.screen;
54
+ else
55
+ problems.push(`screen « ${String(raw.screen)} » inconnu (${SCREENS.join(', ')})`);
56
+ if (screen && raw.visible !== true)
57
+ problems.push(`screen « ${screen} » sans visible : un écran est un changement visible`);
58
+ }
59
+ let spec;
60
+ if (raw.spec != null) {
61
+ if (raw.spec === true)
62
+ spec = true;
63
+ else if (raw.spec !== false)
64
+ problems.push(`spec : true ou false attendu (reçu « ${String(raw.spec)} »)`);
65
+ }
66
+ checkStateNames(raw, problems);
67
+ const gate = readGate(raw.gate, problems);
68
+ const okRaw = raw.docs_ok;
69
+ const docsOk = okRaw && typeof okRaw === 'object' && typeof okRaw.reason === 'string' && okRaw.reason.trim() !== '' ? { date: String(okRaw.date ?? ''), reason: okRaw.reason.trim() } : undefined;
70
+ return {
71
+ id: String(raw.id),
72
+ ...readParent(raw.parent, problems),
73
+ title: String(raw.title ?? ''),
74
+ status,
75
+ ...(typeof raw.state === 'string' && raw.state.trim() !== '' ? { state: raw.state.trim() } : {}),
76
+ ...(typeof raw.state_before === 'string' && raw.state_before.trim() !== '' ? { stateBefore: raw.state_before.trim() } : {}),
77
+ estimate: typeof raw.estimate === 'number' && raw.estimate > 0 ? raw.estimate : 1,
78
+ quickwin: raw.quickwin === true,
79
+ visible: raw.visible === true,
80
+ ...(screen ? { screen } : {}),
81
+ ...(spec ? { spec } : {}), ...(raw.local === true ? { local: true } : {}),
82
+ ...(gate ? { gate } : {}),
83
+ ...(typeof raw.public === 'string' && raw.public.trim() !== '' ? { public: raw.public.trim() } : {}),
84
+ ...(typeof raw.goal === 'string' && raw.goal.trim() !== '' ? { goal: raw.goal.trim() } : {}),
85
+ ...(Number.isInteger(raw.every) && raw.every > 0 ? { every: raw.every } : {}),
86
+ ...(last ? { last } : {}),
87
+ after,
88
+ ...(uses?.length ? { uses } : {}),
89
+ created: asDay('created'),
90
+ started: asDay('started'),
91
+ finished: asDay('finished'),
92
+ notes: Array.isArray(raw.notes)
93
+ ? raw.notes.map((n) => ({ date: String(n.date ?? ''), text: String(n.text ?? '') }))
94
+ : [],
95
+ tasks: Array.isArray(raw.tasks)
96
+ ? raw.tasks.map((t) => ({
97
+ id: String(t.id),
98
+ title: String(t.title ?? ''),
99
+ status: STATUSES.includes(t.status) ? t.status : 'todo',
100
+ }))
101
+ : [],
102
+ ...(repoList(raw.repos).length ? { repos: repoList(raw.repos) } : {}),
103
+ ...(asVerdict(raw.ux) ? { ux: asVerdict(raw.ux) } : {}),
104
+ ...(asVerdict(raw.review) ? { review: asVerdict(raw.review) } : {}),
105
+ ...(docsOk ? { docsOk } : {}),
106
+ problems,
107
+ };
108
+ }
109
+ /**
110
+ * 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
111
+ * 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`.
112
+ */
113
+ function repoList(raw) {
114
+ const items = Array.isArray(raw) ? raw : raw == null ? [] : [raw];
115
+ const out = [];
116
+ for (const item of items) {
117
+ const path = typeof item === 'string' ? item : item && typeof item === 'object' && typeof item.path === 'string' ? item.path : '';
118
+ if (path.trim() === '')
119
+ continue;
120
+ const cite = item && typeof item === 'object' ? item.cite : undefined;
121
+ out.push({ path: path.trim(), ...(typeof cite === 'string' && cite.trim() !== '' ? { cite: cite.trim() } : {}) });
122
+ }
123
+ return out;
124
+ }
125
+ function asVerdict(raw) {
126
+ if (!raw || typeof raw !== 'object')
127
+ return undefined;
128
+ const v = raw;
129
+ // Écrit à la main sans verdict (clé absente, vide ou blanche) : rien n'a été dit de la revue, la porte
130
+ // reste fermée — comme `raf ux` et `raf review` refusent d'enregistrer un verdict vide.
131
+ if (String(v.verdict ?? '').trim() === '')
132
+ return undefined;
133
+ const verdict = { date: String(v.date ?? ''), verdict: String(v.verdict) };
134
+ // Champ présent mais vide : relu « jusqu'à rien », comme un verdict noté sans commit.
135
+ if ('commit' in v)
136
+ verdict.commit = v.commit == null || String(v.commit).trim() === '' ? null : String(v.commit).trim();
137
+ if (v.repos && typeof v.repos === 'object' && !Array.isArray(v.repos)) {
138
+ const repos = Object.entries(v.repos).map(([k, sha]) => [k, sha == null || String(sha).trim() === '' ? null : String(sha).trim()]);
139
+ if (repos.length)
140
+ verdict.repos = Object.fromEntries(repos);
141
+ }
142
+ const r = v.reporte;
143
+ if (r && typeof r === 'object' && !Array.isArray(r) && r.de != null && r.vers != null)
144
+ verdict.reporte = { de: String(r.de), vers: String(r.vers), patchId: String(r.patchId ?? '') };
145
+ return verdict;
146
+ }
@@ -0,0 +1,119 @@
1
+ import { isObject, list } from './yaml-keys.js';
2
+ import { STATUSES } from './plan.js';
3
+ function parseBases(raw, bad) {
4
+ if (!isObject(raw))
5
+ throw bad('states doit être un objet { état: statut de base }');
6
+ const states = {};
7
+ for (const [name, base] of Object.entries(raw)) {
8
+ if (!STATUSES.includes(base))
9
+ throw bad(`states.${name} : statut de base « ${String(base)} » inconnu (attendu : ${STATUSES.join(', ')})`);
10
+ states[name] = base;
11
+ }
12
+ const bases = Object.values(states);
13
+ if (!['todo', 'doing', 'done'].every((b) => bases.includes(b)))
14
+ throw bad('states doit déclarer au moins un état todo, un doing et un done');
15
+ return states;
16
+ }
17
+ function parseTransitions(raw, states, bad) {
18
+ if (!isObject(raw))
19
+ throw bad('transitions doit être un objet { état: [états permis] }');
20
+ const transitions = {};
21
+ for (const [from, tos] of Object.entries(raw)) {
22
+ if (!Object.hasOwn(states, from))
23
+ throw bad(`transitions : « ${from} » n'est pas un état déclaré`);
24
+ for (const to of list(tos))
25
+ if (!Object.hasOwn(states, to))
26
+ throw bad(`transitions.${from} : « ${to} » n'est pas un état déclaré`);
27
+ transitions[from] = list(tos);
28
+ }
29
+ return transitions;
30
+ }
31
+ function requireNames(p, bad) {
32
+ for (const k of ['doneState', 'blockedState'])
33
+ if (p[k] != null && typeof p[k] !== 'string')
34
+ throw bad(`${k} : un nom d'état attendu`);
35
+ }
36
+ /**
37
+ * Lit `plan.states`, `plan.transitions`, `plan.doneState` et `plan.blockedState` de cadence.yaml : `{ states }` ou `{}` quand rien n'est déclaré.
38
+ * `foreign` : le plan est tenu par un autre outil (lots, fields, statuses, estimates), dont les états sont les siens.
39
+ */
40
+ export function parseStates(p, bad, foreign) {
41
+ if (p.states == null) {
42
+ for (const k of ['transitions', 'doneState', 'blockedState'])
43
+ if (p[k] != null)
44
+ throw bad(`${k} demande plan.states`);
45
+ return {};
46
+ }
47
+ if (foreign)
48
+ throw bad('states ne va pas avec un plan tenu par un autre outil (lots, fields, statuses, estimates) : ses états sont les siens');
49
+ const states = parseBases(p.states, bad);
50
+ const model = { states };
51
+ if (p.transitions != null)
52
+ model.transitions = parseTransitions(p.transitions, states, bad);
53
+ requireNames(p, bad);
54
+ if (p.doneState != null) {
55
+ if (states[String(p.doneState)] !== 'done')
56
+ throw bad(`doneState : « ${String(p.doneState)} » n'est pas un état done`);
57
+ model.done = String(p.doneState);
58
+ }
59
+ if (p.blockedState != null) {
60
+ const base = states[String(p.blockedState)];
61
+ if (base !== 'todo' && base !== 'doing')
62
+ throw bad(`blockedState : « ${String(p.blockedState)} » doit être un état todo ou doing`);
63
+ model.blocked = String(p.blockedState);
64
+ }
65
+ return { states: model };
66
+ }
67
+ /** L'état que prend un lot qui entre dans ce statut de base : `doneState` pour done, sinon le premier état déclaré de ce statut. */
68
+ export function defaultState(model, base) {
69
+ if (base === 'done' && model.done)
70
+ return model.done;
71
+ return Object.keys(model.states).find((s) => model.states[s] === base);
72
+ }
73
+ /** L'état d'un lot : celui qu'il porte, sinon l'état par défaut de son statut (lot écrit avant la déclaration). */
74
+ export function currentState(model, lot) {
75
+ return lot.state ?? defaultState(model, lot.status) ?? lot.status;
76
+ }
77
+ /** Pourquoi `from → to` est refusé par `plan.transitions` ; `null` quand c'est permis ou que rien n'est déclaré. */
78
+ export function transitionProblem(model, from, to) {
79
+ if (!model.transitions)
80
+ return null;
81
+ const allowed = model.transitions[from];
82
+ if (allowed?.includes(to))
83
+ return null;
84
+ return `${from} → ${to} n'est pas permis (${allowed?.length ? `depuis ${from} : ${allowed.join(', ')}` : 'état terminal'})`;
85
+ }
86
+ /** Les écarts d'un état écrit à la main : état non déclaré, ou statut de base qui contredit `status:`. */
87
+ export function stateProblems(model, lot) {
88
+ if (lot.state === undefined)
89
+ return [];
90
+ const base = Object.hasOwn(model.states, lot.state) ? model.states[lot.state] : undefined;
91
+ if (base === undefined)
92
+ return [`état « ${lot.state} » non déclaré (cadence.yaml : plan.states)`];
93
+ return base === lot.status ? [] : [`état « ${lot.state} » (${base}) contredit status: ${lot.status}`];
94
+ }
95
+ /**
96
+ * Applique à `node` le changement de statut de base de `raf status` : todo → doing démarre le lot, doing → todo le remet en attente
97
+ * (`started:` retiré) — refusé, comme `raf todo`, dès qu'un commit cite le lot : le remettre en attente perdrait leur lien. Rend le refus
98
+ * (`move` nomme le lot, l'état visé et son statut de base), `undefined` quand le changement est fait. Autre changement : rien n'est touché,
99
+ * le refus renvoie à sa commande.
100
+ */
101
+ export function rebase(node, lot, move, today, commits) {
102
+ const { ref, to, base } = move;
103
+ if (lot.status === 'todo' && base === 'doing') {
104
+ node.set('status', 'doing');
105
+ if (!lot.started)
106
+ node.set('started', today);
107
+ return undefined;
108
+ }
109
+ if (lot.status === 'doing' && base === 'todo') {
110
+ const n = commits();
111
+ if (n > 0)
112
+ return `${ref} a ${n} commit(s) qui le citent : le remettre en attente perdrait leur lien — un état doing (raf status ${ref} <état>), ou raf drop ${ref}`;
113
+ node.set('status', 'todo');
114
+ node.delete('started');
115
+ return undefined;
116
+ }
117
+ const command = base === 'done' ? 'done' : base === 'dropped' ? 'drop' : 'todo';
118
+ return `${ref} : ${to} a pour statut de base ${base}, ${lot.status} aujourd'hui — passer par raf ${command} ${ref}`;
119
+ }