@sylad/cadence 0.2.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.
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "cadence",
3
+ "description": "cadence — a repo-native working method for Claude Code sessions",
4
+ "owner": { "name": "Sylvain Ladoire" },
5
+ "plugins": [
6
+ {
7
+ "name": "cadence",
8
+ "description": "Session start and close rituals driven by a versioned plan (raf), and deliveries proven by their effect. Needs the cadence CLI (npm i -g @sylad/cadence).",
9
+ "version": "0.2.0",
10
+ "source": "./",
11
+ "author": { "name": "Sylvain Ladoire" }
12
+ }
13
+ ]
14
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "cadence",
3
+ "description": "A repo-native working method: session start and close rituals driven by a versioned plan (raf), deliveries proven by their effect, and a UX reviewer agent.",
4
+ "version": "0.2.0",
5
+ "author": { "name": "Sylvain Ladoire" },
6
+ "homepage": "https://github.com/Sylad/cadence",
7
+ "repository": "https://github.com/Sylad/cadence",
8
+ "license": "MIT",
9
+ "keywords": ["planning", "workflow", "delivery", "changelog", "sessions"]
10
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sylvain Ladoire
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,234 @@
1
+ # cadence
2
+
3
+ A small working method that lives in your repository. Solo developers and
4
+ AI-assisted sessions lose track of *what is left to do* and *what each commit
5
+ was for*; cadence keeps both in plain files next to the code.
6
+
7
+ Four tools:
8
+
9
+ - **raf** (French *reste à faire*, "what is left to do"): the plan, linked to your commits;
10
+ - **news**: a user-facing changelog with screenshots, tied to the plan;
11
+ - **session**: the facts to start and to close a work session;
12
+ - **deliver**: wait for the CI of the pushed commit, deploy, then **verify the effect**.
13
+
14
+ And three [Claude Code](https://claude.com/claude-code) skills that turn them
15
+ into rituals — `session-start`, `session-close`, `deliver` — plus a `ux-reviewer`
16
+ agent: no user-facing change is done before its usability review.
17
+
18
+ ## raf
19
+
20
+ - The plan is a YAML file in the repo (`docs/plan/raf.yaml`), edited by the CLI.
21
+ Your comments and hand edits are preserved.
22
+ - A commit belongs to a lot when its message cites the id: `feat(L3): …`,
23
+ `fix: L3/t1 …`. The link is **computed from `git log`**, never stored, so
24
+ committing never dirties the plan.
25
+ - `raf check` audits drift between the plan and the history.
26
+ - `raf gantt` writes a single self-contained HTML page (no server, no CDN).
27
+
28
+ ```sh
29
+ npm install -g @sylad/cadence # or: npx -p @sylad/cadence raf …
30
+
31
+ raf init # docs/plan/raf.yaml + post-commit hook
32
+ raf add "Monthly dedup on merge" --estimate 2
33
+ raf add "Typo in footer" --quickwin
34
+ raf add "Loan cache" --after L1
35
+ raf add "write the migration" --parent L1 # → L1/t1
36
+ raf start L1
37
+ git commit -m "feat(L1): dedup by calendar month"
38
+ raf now # in progress, next up (quickwins first), recently done
39
+ raf done L1/t1 && raf done L1
40
+ raf check # exit 1 on drift
41
+ raf gantt # docs/plan/gantt.html
42
+ ```
43
+
44
+ ### Commands
45
+
46
+ | Command | Effect |
47
+ |---|---|
48
+ | `raf init [--project name] [--prefix L] [--no-hook]` | create the plan and install the hook |
49
+ | `raf add "title" [--estimate d] [--quickwin] [--visible] [--after L2,L4] [--parent L3]` | add a lot or a sub-task, print its id |
50
+ | `raf start <id>` · `raf done <id> [--force]` · `raf drop <id> [--reason text]` | dated transitions (`done` refuses open sub-tasks unless `--force`) |
51
+ | `raf note <id> "text"` | dated note — keep decisions next to the work |
52
+ | `raf now` | what to do next |
53
+ | `raf list [--status s]` | flat list |
54
+ | `raf check [--since date] [--idle 7]` | since the plan's adoption date by default: commits without a lot (commits touching only the plan are exempt), unknown ids, `todo` lots that already have commits, idle lots, `done` lots with open sub-tasks, bad or circular dependencies |
55
+ | `raf gantt [-o file]` | standalone Gantt page |
56
+ | `raf hook install` | add the (non-blocking, read-only) post-commit hook |
57
+
58
+ Global options: `--file path` or `RAF_FILE`; `RAF_TODAY=YYYY-MM-DD` to freeze
59
+ the date. `cadence raf …` is the same as `raf …`.
60
+
61
+ ### Plan file
62
+
63
+ ```yaml
64
+ version: 1
65
+ project: my-app
66
+ prefix: L
67
+ since: 2026-09-28 # commits before this date are not audited
68
+ lots:
69
+ - id: L1
70
+ title: Monthly dedup on merge
71
+ status: doing # todo | doing | done | dropped
72
+ estimate: 2 # working days
73
+ quickwin: false
74
+ visible: true # user-facing: a news entry is expected when done
75
+ after: [L0]
76
+ created: 2026-09-28
77
+ started: 2026-09-29
78
+ notes:
79
+ - { date: 2026-09-29, text: "keep the bank line when both sources exist" }
80
+ tasks:
81
+ - { id: t1, title: write the migration, status: done }
82
+ ```
83
+
84
+ ### Gantt scheduling
85
+
86
+ One lane of work. Finished lots use their real dates (or their commits' dates);
87
+ lots in progress run until `max(start + estimate, today)`; lots to do follow in
88
+ file order, after their dependencies, on working days.
89
+
90
+ ## news
91
+
92
+ What changed *for the user*, one entry per visible lot, each with a screenshot.
93
+
94
+ - Mark a lot as user-facing with `raf add … --visible` (or `visible: true` in
95
+ the YAML). `raf done` and the post-commit hook remind you to write its entry;
96
+ `raf check` fails while a visible lot is done without one.
97
+ - An entry is a Markdown file in `docs/nouveautes/`, with a YAML header.
98
+ Screenshots are files you take yourself, stored next to the entries.
99
+ - `cadence news build` writes `nouveautes.json` for your app to display, a
100
+ self-contained `index.html`, and copies the screenshots.
101
+
102
+ ```sh
103
+ raf add "Amounts like 3.000 read as three thousand" --visible # → L8
104
+ raf start L8 && git commit -m "fix(L8): thousands separator" && raf done L8
105
+ cadence news new L8 # docs/nouveautes/2026-09-29-amounts-like-3-000-….md
106
+ # add docs/nouveautes/captures/l8.png, list it under `captures:`, write the text
107
+ cadence news check # exit 1 on drift (also part of raf check)
108
+ cadence news build -o frontend/public/nouveautes
109
+ ```
110
+
111
+ ```markdown
112
+ ---
113
+ title: Amounts like 3.000 read as three thousand
114
+ date: 2026-09-29
115
+ lots: [L8]
116
+ captures: [captures/l8.png]
117
+ # nocapture: reason, when a screenshot makes no sense
118
+ ---
119
+ Imported statements now read **3.000** as three thousand, not three.
120
+ ```
121
+
122
+ | Command | Effect |
123
+ |---|---|
124
+ | `cadence news new <lot…> [--title t]` | entry skeleton, dated today, titled after the lot |
125
+ | `cadence news list` | entries, newest first |
126
+ | `cadence news check` | visible lots done without entry, unknown lots, missing or undeclared screenshots, bad headers |
127
+ | `cadence news build [-o dir]` | `nouveautes.json` + `index.html` + screenshots (default `docs/nouveautes/site`) |
128
+
129
+ `--dir` changes the entries folder (default `docs/nouveautes` at the git root).
130
+ The Markdown is deliberately small: paragraphs, `-` lists, `**bold**`,
131
+ `` `code` ``, `[links](url)`; everything else is escaped text. The JSON holds
132
+ `{ project, generated, entries: [{ slug, title, date, lots, captures, html }] }`,
133
+ with screenshot paths relative to the JSON file.
134
+
135
+ ### UX review
136
+
137
+ ```sh
138
+ raf ux enable # from today, a --visible lot needs a UX review before done
139
+ raf ux L4 "compliant after 2 fixes" # record the verdict (from the ux-reviewer agent)
140
+ raf ux L8 "no screen: calculation fix"
141
+ ```
142
+
143
+ With the rule on, `raf done` refuses a visible lot without a review (`--force`
144
+ to override) and `raf check` reports visible lots finished after the `uxSince`
145
+ day without one. Plans without `uxSince` are not affected.
146
+
147
+ ## session
148
+
149
+ ```sh
150
+ cadence session start # notes from the last close, lots in progress (silent ones flagged),
151
+ # work done since yesterday by lot, drift, repo state, 3 proposals
152
+ cadence session start --since "3 days ago" --idle 2
153
+ cadence session close # today's commits by lot, commits without a lot, lots in progress
154
+ # with no commit today, drift, uncommitted / unpushed work
155
+ # exit 1 while something is still open
156
+ cadence session next "finish L3" "review L4" # shown by the next session start
157
+ ```
158
+
159
+ Proposals come from the plan only: lots in progress, then ready lots (dependencies
160
+ done), quick wins first. Local state lives in the git directory, never committed:
161
+ the close notes per worktree, the delivery lock and log in `.git/cadence/`, shared
162
+ by all the worktrees of a clone.
163
+
164
+ ## deliver
165
+
166
+ A delivery is done when its checks pass, not when a tool says "success".
167
+
168
+ ```yaml
169
+ # cadence.yaml, at the repository root
170
+ deliver:
171
+ ci: github # github | none | { command: "…" } (default: none)
172
+ ciTimeout: 1800 # seconds
173
+ deploy: # sh commands, in order, at the repo root
174
+ - ./scripts/deploy.sh "$CADENCE_SHORT"
175
+ verify: # at least one; retried every 10 s until verifyTimeout
176
+ - url: https://app.example.com/api/health
177
+ status: 200 # default 200
178
+ - url: https://app.example.com/version.txt
179
+ contains: "${SHORT}"
180
+ - command: kubectl rollout status deploy/app --timeout=60s
181
+ verifyTimeout: 300
182
+ deployTimeout: 1800 # seconds, per deploy command
183
+ ```
184
+
185
+ ```sh
186
+ cadence deliver --dry-run # preconditions, then the resolved steps; nothing runs
187
+ cadence deliver # 0 delivered and verified · 1 a step failed · 2 refused before acting
188
+ ```
189
+
190
+ - **Preconditions**: no modified tracked file; `HEAD` is on a remote branch (the
191
+ CI can only build what was pushed); no other delivery running (a lock whose
192
+ process died is removed with a warning); with `ci: github`, `gh` installed and
193
+ logged in.
194
+ - Every command is killed when it exceeds its budget (CI, `deployTimeout`, what is
195
+ left of `verifyTimeout`) and reported as "délai dépassé".
196
+ - **CI** `github`: polls `gh run list --commit <sha>` every 15 s; no run after
197
+ 5 minutes is a failure (you probably pushed another commit than the one you
198
+ deliver); every run must end `success`, `skipped` or `neutral`. `gh` errors
199
+ are retried, and reported with their cause after 5 minutes.
200
+ - Commands get `CADENCE_SHA`, `CADENCE_SHORT` (7 characters) and `CADENCE_BRANCH`;
201
+ `${SHA}` and `${SHORT}` are replaced in `url` and `contains`.
202
+ - On success the lots cited by the commits since the previous delivery are
203
+ listed, so you can `raf done` those whose effect you have seen.
204
+
205
+ ## Claude Code skills
206
+
207
+ As a plugin:
208
+
209
+ ```
210
+ /plugin marketplace add Sylad/cadence
211
+ /plugin install cadence@cadence
212
+ ```
213
+
214
+ gives `/cadence:session-start`, `/cadence:session-close`, `/cadence:deliver` and
215
+ the `ux-reviewer` agent. Or copy them into the repository with
216
+ `cadence skills install` (to `.claude/skills/cadence-*` and
217
+ `.claude/agents/cadence-ux-reviewer.md`; `--dir` for another `.claude` folder,
218
+ `--force` to overwrite local edits).
219
+
220
+ - **session-start**: reports the facts briefly, proposes three lots from the
221
+ plan, then waits for your priority — nothing starts before your answer.
222
+ - **session-close**: plan hygiene, clean repository, memory limited to what the
223
+ repository does not say, new skills or agents proposed but never created, three
224
+ lines for next time.
225
+ - **deliver**: dry run, delivery, and on failure the cause fixed rather than a
226
+ blind retry.
227
+ - **ux-reviewer** (agent): captures at 1440 and 390 px, findings grounded in a
228
+ named rule (Nielsen, WCAG 2.2 AA) or a measurement, ranked, turned into
229
+ `raf add --parent` sub-tasks, and a one-line verdict for `raf ux`. It never
230
+ edits code.
231
+
232
+ ## License
233
+
234
+ MIT
@@ -0,0 +1,49 @@
1
+ ---
2
+ name: ux-reviewer
3
+ description: Usability and accessibility reviewer for any web interface — reviews a page, a screen or a user-facing change before it is marked done, and runs the planned reviews of existing screens. Grounds every finding in a named rule (Nielsen heuristics, WCAG 2.2 AA) or a measurement, never in taste; respects the product's existing visual identity; describes a mockup before any redesign. Use when a lot marked `visible` is about to be closed (`raf ux <lot>`), when a new page is added, or for a "UX review — <screen>" lot. Does not modify code.
4
+ ---
5
+
6
+ You review the usability and accessibility of a web interface. You report; you never edit code.
7
+
8
+ ## Inputs
9
+
10
+ The lot or screen to review, and how to reach it (local dev server, deployed URL, route). If the
11
+ screen needs data or a login, ask for — or find in the project docs — the way to get a realistic state
12
+ (demo data, stubbed API). Read the project's CLAUDE.md and design notes first: the product's existing
13
+ identity (colours, density, tone) is a constraint, not something to "fix".
14
+
15
+ ## Method
16
+
17
+ 1. **Look before judging.** Capture the screen with the browser tool available (Playwright or
18
+ equivalent) at **1440 px** and **390 px** wide, in its main states: empty, loaded, error, loading,
19
+ and the key interaction. Look at every capture. Store them in the project's temporary folder.
20
+ 2. **Walk the main task** a real user comes for (find, read, compare, act, undo) and count the steps.
21
+ 3. **Check, and measure where a number exists:**
22
+ - Nielsen's 10 heuristics — especially visibility of system status, match with the user's words,
23
+ consistency, error prevention and recovery, recognition rather than recall;
24
+ - WCAG 2.2 AA — text contrast ≥ 4.5:1 (≥ 3:1 for large text and UI parts), everything usable with
25
+ the keyboard, visible focus, targets ≥ 24×24 px (44 px recommended on touch), text alternatives,
26
+ no information carried by colour alone, content that reflows at 320 px without horizontal scroll;
27
+ - data display — units shown, numbers aligned and formatted for the locale, charts readable without
28
+ their legend colours alone, empty states that say what to do;
29
+ - on the phone width, a dense control-heavy page is often better **hidden or reduced** than squeezed.
30
+ 4. **Rank** each finding: *blocking* (a user cannot complete the task, or an accessibility failure),
31
+ *major* (slows or misleads), *minor* (polish).
32
+
33
+ ## Output
34
+
35
+ A short report:
36
+
37
+ - **Verdict** in one line, suitable for `raf ux <lot> "…"` — e.g. "compliant", "compliant after 2 fixes",
38
+ "not compliant: 1 blocking".
39
+ - **Findings**, most severe first, each with: what (with the capture), the rule or the measure, the
40
+ proposed change, the effort (S ≤ a session, M ≈ a day).
41
+ - **Proposed sub-tasks**: one `raf add --parent <lot> "…"` line per finding worth doing.
42
+ - For any redesign, a **described mockup** (layout, hierarchy, what moves where) to be approved before
43
+ anyone codes it.
44
+
45
+ ## Do not
46
+
47
+ - Judge on taste, or propose a new visual identity.
48
+ - Report a finding you have not seen in a capture or measured.
49
+ - Edit code, commit, or record `raf ux` yourself: the session that owns the lot does it.
package/bin/cadence.js ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+ import { run } from '../dist/cli.js';
4
+
5
+ const [tool, ...args] = process.argv.slice(2);
6
+ const io = {
7
+ cwd: process.cwd(),
8
+ env: process.env,
9
+ out: (l) => console.log(l),
10
+ err: (l) => console.error(l),
11
+ now: () => new Date(),
12
+ };
13
+
14
+ if (tool === 'raf') {
15
+ process.exitCode = await run(args, io);
16
+ } else if (['news', 'session', 'deliver', 'skills'].includes(tool)) {
17
+ process.exitCode = await run([tool, ...args], io);
18
+ } else if (tool === '--version' || tool === '-v') {
19
+ const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
20
+ console.log(pkg.version);
21
+ } else {
22
+ console.log(`cadence — une méthode de travail qui vit dans le dépôt
23
+
24
+ cadence raf … plan « reste à faire » relié aux commits (aussi disponible en « raf »)
25
+ cadence news … Nouveautés : une entrée avec capture par lot visible, JSON + page autonome
26
+ cadence session start [--since "24 hours ago"] [--idle 2]
27
+ faits de reprise : notes de la veille, en cours, fait depuis, écarts, propositions
28
+ cadence session close [--since …] faits de clôture ; code 1 tant que ce n'est pas fermé
29
+ cadence session next "ligne" … notes pour la prochaine session (sans argument : efface)
30
+ cadence deliver [--dry-run] [--config cadence.yaml]
31
+ CI du sha poussé → déploiement → vérifications de l'effet
32
+ cadence skills install [--dir .claude] [--force]
33
+ installe les skills Claude Code session-start, session-close, deliver et l'agent ux-reviewer`);
34
+ process.exitCode = !tool || ['help', '--help', '-h'].includes(tool) ? 0 : 2;
35
+ }
package/bin/raf.js ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { run } from '../dist/cli.js';
3
+
4
+ process.exitCode = await run(process.argv.slice(2), {
5
+ cwd: process.cwd(),
6
+ env: process.env,
7
+ out: (l) => console.log(l),
8
+ err: (l) => console.error(l),
9
+ now: () => new Date(),
10
+ });
package/dist/audit.js ADDED
@@ -0,0 +1,54 @@
1
+ import { dirname, join, relative } from 'node:path';
2
+ import { check } from './check.js';
3
+ import { changedFiles, readCommits } from './git.js';
4
+ import { linkCommits } from './link.js';
5
+ import { loadEntries, newsIssues } from './news.js';
6
+ import { isOpen } from './plan.js';
7
+ /** Un commit qui ne touche que le plan (ou la page Gantt) n'a pas besoin de citer un lot. */
8
+ export function exemptPlanOnly(linked, plan, root) {
9
+ const own = new Set([relative(root, plan.path), relative(root, join(dirname(plan.path), 'gantt.html'))]);
10
+ const orphans = linked.orphans.filter((c) => {
11
+ const files = changedFiles(root, c.sha);
12
+ return files.length === 0 || !files.every((f) => own.has(f));
13
+ });
14
+ return { ...linked, orphans };
15
+ }
16
+ /** Fenêtre de l'audit : --since, sinon la date d'adoption du plan, sinon 30 jours. */
17
+ export function auditSince(plan, explicit) {
18
+ if (explicit)
19
+ return explicit;
20
+ // Une date seule vaudrait « ce jour-là à l'heure actuelle » pour git : minuit explicite.
21
+ return plan.since ? `${plan.since} 00:00` : '30 days ago';
22
+ }
23
+ /** Écarts entre le plan, l'historique et les Nouveautés — ce que `raf check` affiche. */
24
+ export function audit(plan, root, newsDir, today, opts = {}) {
25
+ const lots = plan.lots();
26
+ const linked = exemptPlanOnly(linkCommits(lots, readCommits(root, { since: auditSince(plan, opts.since) }), plan.prefix), plan, root);
27
+ // L'inactivité se mesure sur tout l'historique, pas seulement la fenêtre --since.
28
+ const all = linkCommits(lots, readCommits(root), plan.prefix);
29
+ return [...check(lots, { ...linked, byLot: all.byLot }, today, opts.idle ?? 7), ...newsIssues(lots, loadEntries(newsDir), newsDir), ...uxIssues(plan)];
30
+ }
31
+ /**
32
+ * Lots visibles terminés APRÈS le jour d'activation sans revue UX enregistrée. Le jour même est exclu :
33
+ * à la journée près, on ne distingue pas un lot fermé avant l'activation d'un lot fermé après
34
+ * (et `raf done` refuse de toute façon dès l'activation).
35
+ */
36
+ export function uxIssues(plan) {
37
+ const since = plan.uxSince;
38
+ if (!since)
39
+ return [];
40
+ return plan
41
+ .lots()
42
+ .filter((l) => l.visible && l.status === 'done' && !l.ux && (!l.finished || l.finished > since))
43
+ .map((l) => ({ message: `${l.id} est visible et terminé sans revue UX — raf ux ${l.id} "verdict"` }));
44
+ }
45
+ /** Ce qui vient ensuite : lots en cours, puis lots prêts (dépendances closes), gains rapides d'abord. */
46
+ export function nextUp(lots) {
47
+ const byId = new Map(lots.map((l) => [l.id, l]));
48
+ const doing = lots.filter((l) => l.status === 'doing');
49
+ const ready = lots
50
+ .filter((l) => l.status === 'todo' && l.after.every((d) => !byId.has(d) || !isOpen(byId.get(d).status)))
51
+ .sort((a, b) => Number(b.quickwin) - Number(a.quickwin));
52
+ const blocked = lots.filter((l) => l.status === 'todo' && !ready.includes(l));
53
+ return { doing, ready, blocked };
54
+ }
package/dist/check.js ADDED
@@ -0,0 +1,60 @@
1
+ import { diffDays } from './dates.js';
2
+ import { isOpen } from './plan.js';
3
+ const short = (c) => `${c.sha.slice(0, 7)} ${c.subject}`;
4
+ export function check(lots, linked, today, idleDays = 7) {
5
+ const issues = [];
6
+ const ids = new Set(lots.map((l) => l.id));
7
+ for (const c of linked.orphans)
8
+ issues.push({ kind: 'orphan-commit', message: `commit sans lot : ${short(c)}` });
9
+ for (const u of linked.unknown) {
10
+ issues.push({ kind: 'unknown-ref', message: `${u.ref} inconnu, cité par ${short(u.commit)}` });
11
+ }
12
+ for (const lot of lots) {
13
+ const commits = linked.byLot.get(lot.id) ?? [];
14
+ if (lot.status === 'todo' && commits.length > 0) {
15
+ issues.push({ kind: 'todo-with-commits', message: `${lot.id} a ${commits.length} commit(s) mais est encore todo — raf start ${lot.id}` });
16
+ }
17
+ if (lot.status === 'doing') {
18
+ const last = commits[0]?.day ?? lot.started;
19
+ if (last && diffDays(last, today) > idleDays) {
20
+ issues.push({ kind: 'idle', message: `${lot.id} en cours sans commit depuis ${diffDays(last, today)} j (${lot.title})` });
21
+ }
22
+ }
23
+ if (lot.status === 'done') {
24
+ const open = lot.tasks.filter((t) => isOpen(t.status));
25
+ if (open.length > 0) {
26
+ issues.push({ kind: 'done-open-tasks', message: `${lot.id} terminé avec sous-tâche(s) ouverte(s) : ${open.map((t) => t.id).join(', ')}` });
27
+ }
28
+ }
29
+ for (const p of lot.problems)
30
+ issues.push({ kind: 'bad-field', message: `${lot.id} : ${p}` });
31
+ for (const d of lot.after) {
32
+ if (!ids.has(d))
33
+ issues.push({ kind: 'bad-dependency', message: `${lot.id} dépend de ${d}, absent du plan` });
34
+ }
35
+ }
36
+ for (const cycle of findCycles(lots))
37
+ issues.push({ kind: 'cycle', message: `dépendances circulaires : ${cycle.join(' → ')}` });
38
+ return issues;
39
+ }
40
+ function findCycles(lots) {
41
+ const deps = new Map(lots.map((l) => [l.id, l.after]));
42
+ const state = new Map();
43
+ const cycles = [];
44
+ const visit = (id, path) => {
45
+ if (state.get(id) === 'done')
46
+ return;
47
+ if (state.get(id) === 'visiting') {
48
+ cycles.push([...path.slice(path.indexOf(id)), id]);
49
+ return;
50
+ }
51
+ state.set(id, 'visiting');
52
+ for (const d of deps.get(id) ?? [])
53
+ if (deps.has(d))
54
+ visit(d, [...path, id]);
55
+ state.set(id, 'done');
56
+ };
57
+ for (const l of lots)
58
+ visit(l.id, []);
59
+ return cycles;
60
+ }