@sylad/cadence 0.5.0 → 0.7.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 +2 -2
- package/README.md +145 -16
- package/agents/qa-reviewer.md +122 -0
- package/bin/cadence.js +2 -1
- package/dist/audit.js +6 -3
- package/dist/cli.js +23 -4
- package/dist/deliver.js +10 -2
- package/dist/news.js +58 -13
- package/dist/plan.js +13 -3
- package/dist/state.js +10 -7
- package/package.json +1 -1
- package/skills/deliver/SKILL.md +12 -1
- package/skills/lead/SKILL.md +14 -0
- package/skills/session-close/SKILL.md +5 -0
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"name": "cadence",
|
|
8
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.
|
|
9
|
+
"version": "0.7.0",
|
|
10
10
|
"source": "./",
|
|
11
11
|
"author": { "name": "Sylvain Ladoire" }
|
|
12
12
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
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
|
|
4
|
-
"version": "0.
|
|
3
|
+
"description": "A repo-native working method: session start and close rituals driven by a versioned plan (raf), deliveries proven by their effect, and three reviewer agents (UX, code, QA).",
|
|
4
|
+
"version": "0.7.0",
|
|
5
5
|
"author": { "name": "Sylvain Ladoire" },
|
|
6
6
|
"homepage": "https://github.com/Sylad/cadence",
|
|
7
7
|
"repository": "https://github.com/Sylad/cadence",
|
package/README.md
CHANGED
|
@@ -13,9 +13,11 @@ Four tools:
|
|
|
13
13
|
|
|
14
14
|
And four [Claude Code](https://claude.com/claude-code) skills that turn them
|
|
15
15
|
into rituals — `session-start`, `session-close`, `deliver`, and `lead` to pilot
|
|
16
|
-
several projects through subagents — plus
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
several projects through subagents — plus three reviewer agents: `ux-reviewer`
|
|
17
|
+
(no user-facing change is done before its usability review) and `code-reviewer`
|
|
18
|
+
(no lot with commits is done before its code review), each behind an opt-in
|
|
19
|
+
gate, and `qa-reviewer`, which walks the delivered app in a real browser and
|
|
20
|
+
reports a page left empty or in error.
|
|
19
21
|
|
|
20
22
|
## raf
|
|
21
23
|
|
|
@@ -23,8 +25,17 @@ review) and `code-reviewer` (no lot with commits is done before its code review)
|
|
|
23
25
|
Your comments and hand edits are preserved.
|
|
24
26
|
- A commit belongs to a lot when its message cites the id: `feat(L3): …`,
|
|
25
27
|
`fix: L3/t1 …`. The link is **computed from `git log`**, never stored, so
|
|
26
|
-
committing never dirties the plan.
|
|
28
|
+
committing never dirties the plan. The id is read as a whole word: `XL3`,
|
|
29
|
+
`L3x` and `L3.4` do not cite `L3`, while `L3.` at the end of a sentence does.
|
|
27
30
|
- `raf check` audits drift between the plan and the history.
|
|
31
|
+
- Plan upkeep needs no lot. A commit is plan upkeep when **every file it
|
|
32
|
+
touches is a plan file**: the plan itself, its Gantt page, or a file the
|
|
33
|
+
project lists under `plan.files` in `cadence.yaml` (a page it generates from
|
|
34
|
+
the plan, a journal). Such a commit is never a "commit without a lot", and it
|
|
35
|
+
does not count as work on the lots it cites: it is absent from `raf commits`,
|
|
36
|
+
does not start a `todo` lot and does not make a code review stale. The files
|
|
37
|
+
decide, never the subject: a `chore(plan): …` commit that touches a source
|
|
38
|
+
file is a commit like any other.
|
|
28
39
|
- `raf gantt` writes a single self-contained HTML page (no server, no CDN).
|
|
29
40
|
|
|
30
41
|
```sh
|
|
@@ -54,7 +65,7 @@ raf gantt # docs/plan/gantt.html
|
|
|
54
65
|
| `raf commits <id>` | the commits counted for a lot (the set the code review gate uses), one `<sha> <subject>` per line, oldest first |
|
|
55
66
|
| `raf now` | what to do next |
|
|
56
67
|
| `raf list [--status s]` | flat list |
|
|
57
|
-
| `raf check [--since date] [--idle 7]` | since the plan's adoption date by default: commits without a lot (commits touching only
|
|
68
|
+
| `raf check [--since date] [--idle 7]` | since the plan's adoption date by default: commits without a lot (commits touching only plan files are exempt), unknown ids, `todo` lots that already have commits, idle lots, `done` lots with open sub-tasks, bad or circular dependencies |
|
|
58
69
|
| `raf gantt [-o file]` | standalone Gantt page |
|
|
59
70
|
| `raf hook install` | add the (non-blocking, read-only) post-commit hook |
|
|
60
71
|
|
|
@@ -94,6 +105,18 @@ format, and the plan stays writable:
|
|
|
94
105
|
plan: planning/todo.yaml
|
|
95
106
|
```
|
|
96
107
|
|
|
108
|
+
A project that publishes its plan (a JSON generated from it and committed with it)
|
|
109
|
+
declares that file, so a commit touching only the plan and its published copy is
|
|
110
|
+
plan upkeep; the plan keeps raf's format and stays writable:
|
|
111
|
+
|
|
112
|
+
```yaml
|
|
113
|
+
plan:
|
|
114
|
+
files: [frontend/public/plan-data/plan.json]
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Until the file is declared, such a commit is reported as a "commit without a
|
|
118
|
+
lot" when it cites none, and counts as work on the lots it cites.
|
|
119
|
+
|
|
97
120
|
A project that already keeps its plan with its own tool is read **without migrating it**: describe
|
|
98
121
|
the file, and `raf now`, `raf list`, `raf commits`, `raf check`, `raf gantt` and
|
|
99
122
|
`cadence session start|close` work on it. Such a plan is **read-only** —
|
|
@@ -175,6 +198,15 @@ captures: [captures/l8.png]
|
|
|
175
198
|
Imported statements now read **3.000** as three thousand, not three.
|
|
176
199
|
```
|
|
177
200
|
|
|
201
|
+
A screenshot can say what it shows: write it as `{ file, alt }` instead of a
|
|
202
|
+
bare path, one text per screenshot.
|
|
203
|
+
|
|
204
|
+
```yaml
|
|
205
|
+
captures:
|
|
206
|
+
- { file: captures/l8-before.png, alt: "Statement total read as 3 instead of 3,000" }
|
|
207
|
+
- captures/l8-after.png # a bare path still works: no alternative text
|
|
208
|
+
```
|
|
209
|
+
|
|
178
210
|
| Command | Effect |
|
|
179
211
|
|---|---|
|
|
180
212
|
| `cadence news new <lot…> [--title t]` | entry skeleton, dated and timed now (`date`, `created`), titled after the lot |
|
|
@@ -187,7 +219,11 @@ Imported statements now read **3.000** as three thousand, not three.
|
|
|
187
219
|
The Markdown is deliberately small: paragraphs, `-` lists, `**bold**`,
|
|
188
220
|
`` `code` ``, `[links](url)`; everything else is escaped text. The JSON holds
|
|
189
221
|
`{ project, generated, entries: [{ slug, title, date, lots, captures, html }] }`,
|
|
190
|
-
with screenshot paths relative to the JSON file.
|
|
222
|
+
with screenshot paths relative to the JSON file. `captures` is always a list of
|
|
223
|
+
paths; an entry that gives at least one alternative text also carries `alts`,
|
|
224
|
+
the texts in the same order (`""` for a screenshot without one) — an entry
|
|
225
|
+
without any keeps exactly the shape above. The built page puts the text in the
|
|
226
|
+
image's `alt`, and falls back to "Capture : <title>".
|
|
191
227
|
|
|
192
228
|
**Order.** Everywhere (`list`, `build`, the JSON), entries are strictly newest
|
|
193
229
|
first: by `date`, then, on the same day, by creation time. Every entry carries
|
|
@@ -214,8 +250,8 @@ raf ux L8 "no screen: calculation fix"
|
|
|
214
250
|
|
|
215
251
|
With the rule on, `raf done` refuses a visible lot without a review (`--force`
|
|
216
252
|
to override) and `raf check` reports visible lots finished after the `uxSince`
|
|
217
|
-
day without one. An empty verdict is refused
|
|
218
|
-
affected.
|
|
253
|
+
day without one. An empty verdict is refused, and one left empty or blank by hand
|
|
254
|
+
in the YAML counts as no review. Plans without `uxSince` are not affected.
|
|
219
255
|
|
|
220
256
|
### Code review
|
|
221
257
|
|
|
@@ -229,8 +265,8 @@ The counterpart of the UX review, off by default. With the rule on, `raf done`
|
|
|
229
265
|
refuses a lot that has at least one commit citing it and no recorded verdict
|
|
230
266
|
(`--force` to override), and `raf check` reports such lots finished after the
|
|
231
267
|
`reviewSince` day. A lot with no commit has nothing to review; neither does a
|
|
232
|
-
lot whose only commits touch
|
|
233
|
-
match an `ignore:` pattern — `raf commits <id>` prints exactly the counted set.
|
|
268
|
+
lot whose only commits touch plan files alone (the plan, or a file listed under
|
|
269
|
+
`plan.files`), predate the plan's `since` or match an `ignore:` pattern — `raf commits <id>` prints exactly the counted set.
|
|
234
270
|
|
|
235
271
|
The verdict is tied to what was reviewed: `raf review` stores it on the lot with
|
|
236
272
|
the sha of the lot's latest counted commit (`review: { date, verdict, commit }`,
|
|
@@ -239,7 +275,75 @@ makes the review stale: `raf done` refuses (`--force` to override), and
|
|
|
239
275
|
`raf check` reports a finished lot, until the lot is reviewed again and
|
|
240
276
|
`raf review` is rerun. A verdict
|
|
241
277
|
written by hand without a `commit` field is not checked for staleness. An empty
|
|
242
|
-
verdict is refused
|
|
278
|
+
verdict is refused, and one left empty or blank by hand in the YAML counts as no
|
|
279
|
+
review. Plans without `reviewSince` are not affected.
|
|
280
|
+
|
|
281
|
+
### QA review
|
|
282
|
+
|
|
283
|
+
No gate and no command here: the QA review comes **after** a delivery, and
|
|
284
|
+
`raf done` does not wait for it. It follows any delivery that changes what a
|
|
285
|
+
page shows or what it is served (screen, API, data source, configuration of
|
|
286
|
+
either) — in practice every delivery except docs-, plan- or tests-only ones: a
|
|
287
|
+
backend-only lot can empty a page without touching a screen, and the agent then
|
|
288
|
+
starts with the pages that call the changed endpoints. The `qa-reviewer` agent
|
|
289
|
+
opens each page of the running app in a real browser and judges it from the
|
|
290
|
+
user's side. A page can be empty while everything else is green — no code
|
|
291
|
+
changed, a data source went down upstream, the unit tests replace the network,
|
|
292
|
+
`/api/health` answers ok, and the "nothing found" on screen is the message the
|
|
293
|
+
code was written to show.
|
|
294
|
+
|
|
295
|
+
The agent cannot tell such an empty state from a normal one by itself: the
|
|
296
|
+
project says what each page must show, in `docs/qa/expectations.md` — one
|
|
297
|
+
`## <route>` section per page, three kinds of lines:
|
|
298
|
+
|
|
299
|
+
```markdown
|
|
300
|
+
# QA expectations
|
|
301
|
+
|
|
302
|
+
## *
|
|
303
|
+
- shows: the header and the navigation links
|
|
304
|
+
- never: "Loading failed", "Too Many Requests"
|
|
305
|
+
- api: /api/live/current — may be empty when no match is within 24 hours
|
|
306
|
+
|
|
307
|
+
## /players
|
|
308
|
+
- shows: the squad of the last match — at least 11 players
|
|
309
|
+
- shows: the season statistics table, 8 columns — 1440 only
|
|
310
|
+
- never: "No recent line-up found"
|
|
311
|
+
- api: /api/squad/last — a non-empty list
|
|
312
|
+
|
|
313
|
+
## /fixtures/:id (the first match linked from /fixtures)
|
|
314
|
+
- shows: both team names, the date, the score once the match is played
|
|
315
|
+
- never: "Unknown match"
|
|
316
|
+
- api: /api/fixtures/:id
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
- `shows:` — content that must be present and non-empty, with a count where one exists;
|
|
320
|
+
- `never:` — texts that must not appear: error messages, and empty-state messages that mean
|
|
321
|
+
missing data;
|
|
322
|
+
- `api:` — the calls the page depends on: each must answer 2xx with a non-empty body (a 200 with
|
|
323
|
+
`[]`, `{}` or `null` is a failure, unless its line says `may be empty when …`).
|
|
324
|
+
|
|
325
|
+
An optional `## *` section holds what every page must show, never show and call. A line may end
|
|
326
|
+
with a condition in plain words, which the agent honours: `may be empty when …`, `1440 only`,
|
|
327
|
+
`390 only` (a line without a width holds at both). Content hidden on the phone by design is not a
|
|
328
|
+
defect unless a `shows:` line requires it at 390; content pushed outside the visible area (it
|
|
329
|
+
needs a sideways scroll) is reported as suspect and handed to `ux-reviewer` in one line.
|
|
330
|
+
|
|
331
|
+
The rest is free text, written for a reader: a line can be repeated, and a route with a parameter
|
|
332
|
+
names a real value to visit or says where to find one. The file can live elsewhere:
|
|
333
|
+
|
|
334
|
+
```yaml
|
|
335
|
+
# cadence.yaml
|
|
336
|
+
qa:
|
|
337
|
+
expectations: docs/quality/pages.md
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Only the agent reads that key; the CLI does not use it. Without an expectations file the agent walks
|
|
341
|
+
the routes it discovers and still runs its universal checks: an error shown, a failed API call
|
|
342
|
+
whose content is missing on screen, a broken or missing content image are defects with or without a
|
|
343
|
+
file; an empty 2xx body, like whatever else would need an expectation to judge, is suspect at most
|
|
344
|
+
(it may be a normal absence). For a route with a
|
|
345
|
+
parameter, it finds a real value in the app's links or its API responses and says how it built the
|
|
346
|
+
URL. The agent then returns a draft for you to correct — it never writes the file itself.
|
|
243
347
|
|
|
244
348
|
## session
|
|
245
349
|
|
|
@@ -250,9 +354,13 @@ cadence session start --since "3 days ago" --idle 2
|
|
|
250
354
|
cadence session close # today's commits by lot, commits without a lot, lots in progress
|
|
251
355
|
# with no commit today, drift, uncommitted / unpushed work
|
|
252
356
|
# exit 1 while something is still open
|
|
253
|
-
cadence session next "finish L3" "review L4" # shown by the next session start
|
|
357
|
+
cadence session next "finish L3" "review L4" # shown by the next session start; replaces the previous notes
|
|
358
|
+
cadence session next --clear # erase those notes, on purpose
|
|
254
359
|
```
|
|
255
360
|
|
|
361
|
+
`cadence session next` without a line refuses (exit 2) and leaves the notes of the
|
|
362
|
+
last close as they are — it used to erase them silently; erasing is `--clear`.
|
|
363
|
+
|
|
256
364
|
Proposals come from the plan only: lots in progress, then ready lots (dependencies
|
|
257
365
|
done), quick wins first. Local state lives in the git directory, never committed:
|
|
258
366
|
the close notes per worktree, the delivery lock and log in `.git/cadence/`, shared
|
|
@@ -311,7 +419,10 @@ cadence deliver # 0 delivered and verified · 1 a step failed · 2
|
|
|
311
419
|
- Commands get `CADENCE_SHA`, `CADENCE_SHORT` (7 characters) and `CADENCE_BRANCH`;
|
|
312
420
|
`${SHA}` and `${SHORT}` are replaced in `url` and `contains`.
|
|
313
421
|
- On success the lots cited by the commits since the previous delivery are
|
|
314
|
-
listed, so you can `raf done` those whose effect you have seen.
|
|
422
|
+
listed, so you can `raf done` those whose effect you have seen. With a
|
|
423
|
+
read-only plan, only the lots that were in progress when the delivery started
|
|
424
|
+
are listed: an id quoted in a message for context (a finished lot, a
|
|
425
|
+
reservation number that looks like one) is not a delivered lot.
|
|
315
426
|
|
|
316
427
|
### A project with its own delivery script
|
|
317
428
|
|
|
@@ -354,7 +465,7 @@ As a plugin:
|
|
|
354
465
|
```
|
|
355
466
|
|
|
356
467
|
gives `/cadence:session-start`, `/cadence:session-close`, `/cadence:deliver`,
|
|
357
|
-
`/cadence:lead` and the `ux-reviewer` and `
|
|
468
|
+
`/cadence:lead` and the `ux-reviewer`, `code-reviewer` and `qa-reviewer` agents. Or copy them into the
|
|
358
469
|
repository with `cadence skills install` (to `.claude/skills/cadence-*` and
|
|
359
470
|
`.claude/agents/cadence-*.md`; `--dir` for another `.claude` folder,
|
|
360
471
|
`--force` to overwrite local edits).
|
|
@@ -369,12 +480,16 @@ repository with `cadence skills install` (to `.claude/skills/cadence-*` and
|
|
|
369
480
|
morning and evening scripts feed the session report (`session:`), and its own
|
|
370
481
|
skills can become one-line aliases of `session-start` / `session-close`.
|
|
371
482
|
- **deliver**: dry run, delivery, and on failure the cause fixed rather than a
|
|
372
|
-
blind retry
|
|
483
|
+
blind retry; after a green delivery that changes what a page shows or what it
|
|
484
|
+
is served, the `qa-reviewer` agent walks the delivered app.
|
|
373
485
|
- **lead**: from a folder holding several projects, one subagent per project
|
|
374
486
|
gathers the facts, you choose the priorities, each lot is delegated to a
|
|
375
487
|
subagent with a standard brief (test first, commits citing the lot, no push),
|
|
376
488
|
reviewed by the `code-reviewer` agent, re-verified by the lead, then delivered
|
|
377
|
-
one project at a time
|
|
489
|
+
one project at a time; a delivery that changes what a page shows or what it is
|
|
490
|
+
served is then checked in the running app by the `qa-reviewer` agent, whose
|
|
491
|
+
blocking findings come back to you. Two
|
|
492
|
+
subagents at most, never two in the same repository.
|
|
378
493
|
- **ux-reviewer** (agent): captures at 1440 and 390 px, findings grounded in a
|
|
379
494
|
named rule (Nielsen, WCAG 2.2 AA) or a measurement, ranked, turned into
|
|
380
495
|
`raf add --parent` sub-tasks, and a one-line verdict for `raf ux`. It never
|
|
@@ -387,6 +502,20 @@ repository with `cadence skills install` (to `.claude/skills/cadence-*` and
|
|
|
387
502
|
scenario; real defects only, ranked, what it could not verify, and a one-line
|
|
388
503
|
verdict for `raf review`. It takes the lot's commits from `raf commits`, never
|
|
389
504
|
runs a build whose output is used live, and never edits code.
|
|
505
|
+
- **qa-reviewer** (agent): any web app; given a repository and a base URL (and
|
|
506
|
+
optionally a lot id, to start with the pages it touched — for a backend-only
|
|
507
|
+
lot, those that call the changed endpoints), it opens each page of
|
|
508
|
+
the project's expectations file in a real browser at 1440 and 390 px and
|
|
509
|
+
measures: expected content present and non-empty, no error or missing-data
|
|
510
|
+
message, every API call answered 2xx with a non-empty body, no console error,
|
|
511
|
+
no broken content image. Findings are defects (a line of the expectations
|
|
512
|
+
broken, or a universal check failing with a visible effect, with or without an
|
|
513
|
+
expectations file), suspects (it looks like missing or wrong data and no
|
|
514
|
+
expectation settles it) or noise (a console error or a failed request with no
|
|
515
|
+
visible effect, ranked minor), ranked, each with
|
|
516
|
+
the route, what was expected, what was measured and the evidence; pages checked
|
|
517
|
+
N/N, follow-ups as `raf add` lines, what it could not verify, a one-line
|
|
518
|
+
verdict. Read-only: GET only, no login, nothing submitted; it stops at a PIN.
|
|
390
519
|
|
|
391
520
|
## Releasing
|
|
392
521
|
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: qa-reviewer
|
|
3
|
+
description: QA reviewer for any web app — after a delivery, walks the pages of the running app in a real browser, from the user's side, and reports empty states, wrong data, error messages, failed or empty API calls, console errors and broken images. Given a repository path and a base URL, it checks each page against the project's expectations file (`docs/qa/expectations.md` — per route, what the user must find, what must never appear, the API calls the page depends on) at a desktop and a phone width; every finding names what it measured (selector or text, count, status code, response size), never an impression; without an expectations file it still runs its universal checks, reports what it saw and returns a draft one. Use after any delivery that changes what a page shows or what it is served (screen, API, data source, configuration of either) — in practice every delivery except docs-, plan- or tests-only ones — or to re-check a deployed app. Read-only — does not modify code, log in or submit anything.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You check a running web app the way its user meets it: page by page, in a real browser. You
|
|
7
|
+
report; you never edit code, the plan or the expectations.
|
|
8
|
+
|
|
9
|
+
A page can be empty while everything else is green: no code changed, a data source went down
|
|
10
|
+
upstream, the unit tests replace the network, the health endpoint answers ok, and the message on
|
|
11
|
+
screen is exactly the one the code was written to show. Neither a test nor a code review calls
|
|
12
|
+
that a defect. You do: a players page with no players is a defect, whatever the cause.
|
|
13
|
+
|
|
14
|
+
## Inputs
|
|
15
|
+
|
|
16
|
+
The absolute path of the repository and the base URL of the app — deployed, or a local server the
|
|
17
|
+
caller started. Optionally a lot id: then start with the pages that lot touched (its title and
|
|
18
|
+
notes in the plan, and `raf commits <id>`, tell which) — when the lot touched only the backend,
|
|
19
|
+
the pages that call the changed endpoints — and walk the others after. If the path or the URL is
|
|
20
|
+
missing, or the URL does not answer, say so and stop.
|
|
21
|
+
|
|
22
|
+
## Method
|
|
23
|
+
|
|
24
|
+
1. **Read how to reach the app**: the project's CLAUDE.md, then its README — the routes, the demo
|
|
25
|
+
data, what sits behind a PIN or a login.
|
|
26
|
+
2. **Read the expectations**: `docs/qa/expectations.md`, or the file named by `qa.expectations` in
|
|
27
|
+
`cadence.yaml`. One `## <route>` section per page: what the page `shows:` (the content that
|
|
28
|
+
must be present and non-empty, with a count where one exists), what must `never:` appear (error
|
|
29
|
+
texts, empty-state messages that mean missing data), and the `api:` calls it depends on (each
|
|
30
|
+
must answer 2xx with a non-empty body). An optional `## *` section holds what every page must
|
|
31
|
+
show, never show and call. A line may end with a condition in plain words, which you honour:
|
|
32
|
+
`may be empty when …`, `1440 only`, `390 only` (a line without a width holds at both). A route
|
|
33
|
+
with a parameter names a real value to visit, or says where to find one.
|
|
34
|
+
3. **No expectations file: do not guess silently.** Discover the routes (router file, sitemap,
|
|
35
|
+
navigation links); for a route with a parameter, find a real value in the app's links or its API
|
|
36
|
+
responses and say how you built the URL. Walk them as in step 4 and report what you saw: the
|
|
37
|
+
universal checks hold without a file, and anything that would need an expectation to judge is
|
|
38
|
+
*suspect* at most. Return a DRAFT expectations file as text, for the human to correct: you do not
|
|
39
|
+
write it into the repository. Say plainly that without expectations an empty state cannot be told
|
|
40
|
+
from a normal one.
|
|
41
|
+
4. **Open each page in a real browser** (Playwright, or the browser tool available), at **1440 px**
|
|
42
|
+
and **390 px** wide. Let it settle: after `load`, wait a fixed few seconds, scroll through the
|
|
43
|
+
page (lazy images), wait again — never for network idle, which streams and polling never reach.
|
|
44
|
+
Then measure:
|
|
45
|
+
- the expected content is present and non-empty — name the selector or the text found and its
|
|
46
|
+
count (`.player-card` ×14), not "the list looks fine";
|
|
47
|
+
- no `never:` text on screen, and no other error or missing-data message;
|
|
48
|
+
- every API call of the page — those listed, and those you saw it make to its own backend —
|
|
49
|
+
answered 2xx with a non-empty body: note the status and the response size (decoded body bytes;
|
|
50
|
+
streams — SSE, websockets — are exempt from the size rule). A 200 with an empty or null body
|
|
51
|
+
(`[]`, `{}`, `null`, 0 bytes) is a failure, unless its line says `may be empty when …`. This
|
|
52
|
+
takes a tool that listens to responses (e.g. a Playwright `page.on('response')` listener): if
|
|
53
|
+
yours cannot give status and size, say so under "not verified" instead of pretending;
|
|
54
|
+
- no console error: quote the first line of each;
|
|
55
|
+
- no broken image among the content images (a failed request, or `naturalWidth` 0): count the
|
|
56
|
+
items that should carry an image and have no loaded `<img>` — a fallback badge replacing a
|
|
57
|
+
failed image has no `<img>` at all;
|
|
58
|
+
- at 390 px, content hidden on the phone by design is not a defect unless a `shows:` line
|
|
59
|
+
requires it at 390; content pushed outside the visible area (it needs a sideways scroll) is
|
|
60
|
+
reported as suspect and handed to `ux-reviewer` in one line;
|
|
61
|
+
- states behind controls: tabs, filters and other controls that only change the view may be used
|
|
62
|
+
and are part of the page (a tab that triggers its own API call is checked like a page); a
|
|
63
|
+
control that writes is never used;
|
|
64
|
+
- pacing: pause between pages; when a 429 (or any rate-limit answer) appears, re-run that page
|
|
65
|
+
ALONE after a quiet minute before concluding — if it reproduces, an ordinary visitor gets it;
|
|
66
|
+
if not, it was your own pace and it is not a finding;
|
|
67
|
+
- the frontend source may be read to LOCATE a cause after a measurement, never as evidence.
|
|
68
|
+
5. **GET only, and nothing that writes**: never log in, never submit a form that writes, never
|
|
69
|
+
click a control that changes data, never send a POST, PUT, PATCH or DELETE yourself. If a PIN
|
|
70
|
+
or a login wall is met, say so and stop there for those pages: they go under "not verified",
|
|
71
|
+
they are neither a finding nor a page checked.
|
|
72
|
+
6. **Classify** what you see:
|
|
73
|
+
- *defect* — a line of the expectations is broken, or a universal check fails with a visible
|
|
74
|
+
effect on the page: an error message shown, a failed API call whose content is missing on
|
|
75
|
+
screen, a broken or missing content image. Universal checks need no expectations file: such
|
|
76
|
+
a failure is a defect even without one. An API call that answers 2xx with an empty body is a
|
|
77
|
+
defect only when an expectation says data is due there; without one it is suspect at most
|
|
78
|
+
(it may be a normal absence);
|
|
79
|
+
- *suspect* — something that looks like missing or wrong data and that no expectation
|
|
80
|
+
settles: an empty list under a heading, a "nothing found" message, a status or label
|
|
81
|
+
contradicted by the page's own data ("eliminated" beside a won match), a stale season
|
|
82
|
+
label. Say why, and propose the line of expectations that would settle it;
|
|
83
|
+
- *noise* — a console error or a failed request with no visible effect: reported, ranked minor;
|
|
84
|
+
- *out of scope* — usability and accessibility belong to `ux-reviewer`, code quality to
|
|
85
|
+
`code-reviewer`: one line at most, never a finding.
|
|
86
|
+
7. **Rank** each finding: *blocking* (a page's main content is missing, its main information is
|
|
87
|
+
false, or an error is shown to the user), *major* (secondary content missing or wrong, a section
|
|
88
|
+
silently dropped after a failed or empty API call, a broken content image), *minor* (noise).
|
|
89
|
+
|
|
90
|
+
## Output
|
|
91
|
+
|
|
92
|
+
A short report:
|
|
93
|
+
|
|
94
|
+
- **Pages checked N/N**, with the base URL and the date and time of the run, and the two widths. The
|
|
95
|
+
second N is every page of the expectations (or every route discovered): a page you could not open
|
|
96
|
+
is counted and named, never dropped. A page counts as checked when both widths were measured; a
|
|
97
|
+
page checked partially (one width, tabs not opened) is counted and named as partial.
|
|
98
|
+
- **Findings**, most severe first, each with: the route, its kind and rank, what was expected —
|
|
99
|
+
quote the line of the expectations, or name the universal check, or, for a suspect, give the
|
|
100
|
+
expectation line you propose —, what was measured, and the evidence — status code, response
|
|
101
|
+
size, the text on screen, the capture. No finding without a measurement.
|
|
102
|
+
- **Not verified**: pages behind a PIN or a login, states that need data you could not get, a
|
|
103
|
+
browser tool that was missing or could not give status and size — stated plainly.
|
|
104
|
+
- **Proposed follow-ups**: one `raf add "…"` line per finding worth doing; on a read-only plan
|
|
105
|
+
(`cadence.yaml` maps the fields of a file kept by another tool), plain lines for the project's
|
|
106
|
+
own tool instead. Without an expectations file, the draft comes here.
|
|
107
|
+
- **Verdict**, one line, alone — e.g. "6/6 pages as expected", "not as expected: 1 blocking
|
|
108
|
+
(/players shows no player)", "no expectations file: 13 pages walked, 1 defect, 8 suspects, draft
|
|
109
|
+
returned". It is the last line of the report.
|
|
110
|
+
|
|
111
|
+
Captures and temporary files go in a temporary directory outside the repository, or in the one
|
|
112
|
+
the caller names; remove them, or list their paths in the report. The working tree is left as you
|
|
113
|
+
found it.
|
|
114
|
+
|
|
115
|
+
## Do not
|
|
116
|
+
|
|
117
|
+
- Report an impression: a finding you have not measured in the browser is not a finding.
|
|
118
|
+
- Take a green health endpoint, a passing test suite, or "the code shows this message on purpose"
|
|
119
|
+
as proof that a page is fine.
|
|
120
|
+
- Excuse an empty page by its cause: an upstream outage explains a defect, it does not remove it.
|
|
121
|
+
- Edit code, the plan or the expectations file, commit, or mark anything done: the session that
|
|
122
|
+
called you does it.
|
package/bin/cadence.js
CHANGED
|
@@ -26,7 +26,8 @@ if (tool === 'raf') {
|
|
|
26
26
|
cadence session start [--since "24 hours ago"] [--idle 2]
|
|
27
27
|
faits de reprise : notes de la veille, en cours, fait depuis, écarts, propositions
|
|
28
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 (
|
|
29
|
+
cadence session next "ligne" … notes pour la prochaine session (remplacent les précédentes)
|
|
30
|
+
cadence session next --clear efface ces notes ; sans ligne ni --clear, la commande refuse
|
|
30
31
|
cadence deliver [--dry-run] [--sha rév] [--config cadence.yaml] [-- arguments du script du projet]
|
|
31
32
|
CI du sha poussé → déploiement → vérifications de l'effet ;
|
|
32
33
|
ou le script de livraison du projet (deliver.script), sous verrou et journal
|
package/dist/audit.js
CHANGED
|
@@ -14,15 +14,18 @@ export function planCommits(plan, root, opts = {}) {
|
|
|
14
14
|
const commits = readCommits(root, opts);
|
|
15
15
|
return patterns.length ? commits.filter((c) => !patterns.some((re) => re.test(c.subject))) : commits;
|
|
16
16
|
}
|
|
17
|
-
/**
|
|
17
|
+
/** Commit d'entretien du plan : ne touche-t-il que des fichiers du plan (plan, page Gantt, plan.files) ? */
|
|
18
18
|
export function isPlanOnly(sha, plan, root) {
|
|
19
19
|
const own = ownFiles(plan, root);
|
|
20
20
|
const files = changedFiles(root, sha);
|
|
21
21
|
return files.length > 0 && files.every((f) => own.has(f));
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
|
-
* N'ont pas besoin de citer un lot : un commit
|
|
25
|
-
*
|
|
24
|
+
* N'ont pas besoin de citer un lot : un commit d'entretien du plan — TOUS ses fichiers sont des fichiers
|
|
25
|
+
* du plan (le plan, sa page Gantt, ceux que le projet déclare sous plan.files, son plan publié par
|
|
26
|
+
* exemple) — et un commit automatique dont le sujet correspond à un motif `ignore:` du plan.
|
|
27
|
+
* Les fichiers décident, jamais le sujet : « chore(plan): … » qui touche un fichier source est un
|
|
28
|
+
* commit comme un autre.
|
|
26
29
|
*/
|
|
27
30
|
export function exemptPlanOnly(linked, plan, root) {
|
|
28
31
|
const own = ownFiles(plan, root);
|
package/dist/cli.js
CHANGED
|
@@ -15,7 +15,7 @@ import { Plan, RafError, STATUSES } from './plan.js';
|
|
|
15
15
|
import { schedule } from './schedule.js';
|
|
16
16
|
import { AGENTS_DIR, installAgents, installSkills, SKILLS_DIR } from './skills.js';
|
|
17
17
|
import { sessionClose, sessionStart } from './session.js';
|
|
18
|
-
import { sharedStateDir, stateDir, writeNext } from './state.js';
|
|
18
|
+
import { clearNext, readNext, sharedStateDir, stateDir, writeNext } from './state.js';
|
|
19
19
|
const HELP = `raf — plan « reste à faire » versionné dans le dépôt, relié aux commits
|
|
20
20
|
|
|
21
21
|
raf init [--project nom] [--prefix L] [--no-hook]
|
|
@@ -35,6 +35,8 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
|
|
|
35
35
|
raf news new <lot…> [--title t] | list | check | stamp | build [-o dossier] (aussi « cadence news … »)
|
|
36
36
|
|
|
37
37
|
Un commit appartient à un lot quand son message cite l'identifiant : « feat(L3): … », « L3/t1 ».
|
|
38
|
+
Un commit qui ne touche que le plan (et les fichiers déclarés sous plan.files dans cadence.yaml, un plan
|
|
39
|
+
publié par exemple) n'a pas à en citer, et ne compte pas pour les lots qu'il cite ; le sujet n'y change rien.
|
|
38
40
|
Un lot --visible attend une entrée Nouveautés (docs/nouveautes/, --dir) avec capture ; raf check le vérifie.
|
|
39
41
|
Un texte qui commence par « - » se passe après « -- » : raf note L1 -- "-5 %".
|
|
40
42
|
Le plan est docs/plan/raf.yaml, ou celui que nomme « plan: » dans cadence.yaml ; un plan tenu par un
|
|
@@ -85,6 +87,7 @@ function dispatch(argv, io) {
|
|
|
85
87
|
config: { type: 'string' },
|
|
86
88
|
'dry-run': { type: 'boolean' },
|
|
87
89
|
sha: { type: 'string' },
|
|
90
|
+
clear: { type: 'boolean' },
|
|
88
91
|
help: { type: 'boolean', short: 'h' },
|
|
89
92
|
},
|
|
90
93
|
});
|
|
@@ -429,10 +432,26 @@ function session([sub, ...args], ctx, values) {
|
|
|
429
432
|
}
|
|
430
433
|
case 'close':
|
|
431
434
|
return sessionClose(ctx, { since: values.since ?? `${ctx.today} 00:00` });
|
|
432
|
-
case 'next':
|
|
433
|
-
|
|
435
|
+
case 'next': {
|
|
436
|
+
const lines = args.filter((l) => l.trim() !== '');
|
|
437
|
+
if (values.clear) {
|
|
438
|
+
if (lines.length)
|
|
439
|
+
throw new RafError('session next --clear efface les notes : ne pas lui passer de ligne');
|
|
440
|
+
const gone = clearNext(ctx.state);
|
|
441
|
+
ctx.out(gone ? `notes effacées (${gone.lines.length} ligne(s) du ${gone.date})` : 'aucune note à effacer');
|
|
442
|
+
return 0;
|
|
443
|
+
}
|
|
444
|
+
// Lancée sans ligne (variable vide dans un script, agent pressé), la commande effaçait en silence
|
|
445
|
+
// les notes de la dernière clôture : effacer se demande exprès.
|
|
446
|
+
if (lines.length === 0) {
|
|
447
|
+
const kept = readNext(ctx.state);
|
|
448
|
+
throw new RafError(`session next : aucune ligne — ${kept ? `${kept.lines.length} ligne(s) du ${kept.date} conservée(s)` : "rien n'est écrit"} ; ` +
|
|
449
|
+
'usage : cadence session next "ligne" … (pour effacer les notes exprès : cadence session next --clear)');
|
|
450
|
+
}
|
|
451
|
+
writeNext(ctx.state, ctx.today, lines);
|
|
434
452
|
return 0;
|
|
453
|
+
}
|
|
435
454
|
default:
|
|
436
|
-
throw new RafError('usage : cadence session start [--since …] [--idle 2] | close [--since …] | next "ligne" …');
|
|
455
|
+
throw new RafError('usage : cadence session start [--since …] [--idle 2] | close [--since …] | next "ligne" … | next --clear');
|
|
437
456
|
}
|
|
438
457
|
}
|
package/dist/deliver.js
CHANGED
|
@@ -343,14 +343,22 @@ async function verifyAll(ctx, deps, sha, env) {
|
|
|
343
343
|
}
|
|
344
344
|
return null;
|
|
345
345
|
}
|
|
346
|
-
/**
|
|
346
|
+
/**
|
|
347
|
+
* Ligne des lots cités depuis la livraison précédente, ou null (première livraison, rien de cité).
|
|
348
|
+
*
|
|
349
|
+
* Plan en lecture seule : seuls les lots EN COURS sont annoncés. Ses identifiants sont de forme libre, et
|
|
350
|
+
* un message cite volontiers un numéro qui en a la forme (réserve « R1 » d'une revue) ou un lot clos
|
|
351
|
+
* nommé pour le contexte — les annoncer « livrés » ferait fermer à tort. L'état est celui du départ de
|
|
352
|
+
* la livraison : le plan a été lu avant que le script du projet ne ferme lui-même les lots qu'il livre.
|
|
353
|
+
*/
|
|
347
354
|
function deliveredLots(ctx, prev, sha) {
|
|
348
355
|
if (!ctx.plan || !prev || prev === sha)
|
|
349
356
|
return null;
|
|
350
357
|
if (!isAncestor(ctx.root, prev, sha)) {
|
|
351
358
|
return `livraison précédente (${prev.slice(0, 7)}) hors de l'historique de ${sha.slice(0, 7)} (réécrit ?) : lots livrés non calculés`;
|
|
352
359
|
}
|
|
353
|
-
const
|
|
360
|
+
const lots = ctx.plan.lots();
|
|
361
|
+
const known = new Set((ctx.plan.readonly ? lots.filter((l) => l.status === 'doing') : lots).map((l) => l.id));
|
|
354
362
|
const ids = new Set();
|
|
355
363
|
for (const c of readCommits(ctx.root, { range: `${prev}..${sha}` })) {
|
|
356
364
|
for (const r of ctx.plan.refs(`${c.subject}\n${c.body}`))
|
package/dist/news.js
CHANGED
|
@@ -16,8 +16,52 @@ function validStamp(s) {
|
|
|
16
16
|
return (!Number.isNaN(Date.parse(s.replace(' ', 'T'))) && day.getUTCMonth() === mo - 1 && day.getUTCDate() === d && h < 24 && mi < 60 && sec < 60);
|
|
17
17
|
}
|
|
18
18
|
const list = (v) => (v == null ? [] : Array.isArray(v) ? v.map(String) : [String(v)]);
|
|
19
|
+
const CAPTURE_KEYS = ['file', 'alt'];
|
|
20
|
+
const CAPTURE_SHAPE = '{ file: chemin, alt: "texte alternatif" }';
|
|
21
|
+
/**
|
|
22
|
+
* Captures de l'en-tête : un chemin, ou `{ file, alt }` quand l'entrée dit ce que l'image montre.
|
|
23
|
+
* Les chemins restent une liste de chaînes (ce que lit déjà le JSON publié), les textes les suivent un à un.
|
|
24
|
+
*/
|
|
25
|
+
function readCaptures(raw, problems) {
|
|
26
|
+
const captures = [];
|
|
27
|
+
const alts = [];
|
|
28
|
+
for (const [i, item] of (raw == null ? [] : Array.isArray(raw) ? raw : [raw]).entries()) {
|
|
29
|
+
let file;
|
|
30
|
+
let alt = '';
|
|
31
|
+
if (typeof item === 'string' || typeof item === 'number') {
|
|
32
|
+
file = String(item);
|
|
33
|
+
}
|
|
34
|
+
else if (item && typeof item === 'object' && !Array.isArray(item)) {
|
|
35
|
+
const map = item;
|
|
36
|
+
if (map.file == null || String(map.file).trim() === '') {
|
|
37
|
+
problems.push(`capture n° ${i + 1} : fichier absent — ${CAPTURE_SHAPE}`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
file = String(map.file);
|
|
41
|
+
// Une ligne : le texte part tel quel dans un attribut alt.
|
|
42
|
+
alt = map.alt == null ? '' : String(map.alt).replace(/\s+/g, ' ').trim();
|
|
43
|
+
for (const k of Object.keys(map)) {
|
|
44
|
+
if (!CAPTURE_KEYS.includes(k))
|
|
45
|
+
problems.push(`capture ${file} : clé inconnue « ${k} » (attendu : ${CAPTURE_KEYS.join(', ')})`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
problems.push(`capture n° ${i + 1} illisible : un chemin ou ${CAPTURE_SHAPE}`);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
// Copiées telles quelles sous le dossier de build : un chemin qui sort du dossier écrirait ailleurs.
|
|
53
|
+
if (isAbsolute(file) || file.split(/[\\/]/).includes('..'))
|
|
54
|
+
problems.push(`capture hors du dossier des Nouveautés : ${file}`);
|
|
55
|
+
// Noms sages : utilisables tels quels dans une URL, et seulement des images.
|
|
56
|
+
else if (!CAPTURE.test(file))
|
|
57
|
+
problems.push(`capture ${file} : image .png, .jpg, .webp ou .gif, nom en lettres, chiffres, « . _ - / »`);
|
|
58
|
+
captures.push(file);
|
|
59
|
+
alts.push(alt);
|
|
60
|
+
}
|
|
61
|
+
return { captures, alts };
|
|
62
|
+
}
|
|
19
63
|
export function parseEntry(file, text) {
|
|
20
|
-
const entry = { file, slug: basename(file).replace(/\.md$/, ''), title: '', date: '', lots: [], captures: [], body: '', problems: [] };
|
|
64
|
+
const entry = { file, slug: basename(file).replace(/\.md$/, ''), title: '', date: '', lots: [], captures: [], alts: [], body: '', problems: [] };
|
|
21
65
|
const m = FRONT.exec(text.replace(/^\uFEFF/, '')); // BOM des éditeurs Windows
|
|
22
66
|
if (!m) {
|
|
23
67
|
entry.problems.push('en-tête YAML absent (--- … ---)');
|
|
@@ -48,18 +92,10 @@ export function parseEntry(file, text) {
|
|
|
48
92
|
entry.created = created.replace(' ', 'T');
|
|
49
93
|
}
|
|
50
94
|
entry.lots = list(head.lots);
|
|
51
|
-
entry.captures = list(head.captures);
|
|
52
95
|
if (head.nocapture != null && String(head.nocapture).trim())
|
|
53
96
|
entry.nocapture = String(head.nocapture).trim();
|
|
54
97
|
entry.body = m[2].trim();
|
|
55
|
-
|
|
56
|
-
// Copiées telles quelles sous le dossier de build : un chemin qui sort du dossier écrirait ailleurs.
|
|
57
|
-
if (isAbsolute(c) || c.split(/[\\/]/).includes('..'))
|
|
58
|
-
entry.problems.push(`capture hors du dossier des Nouveautés : ${c}`);
|
|
59
|
-
// Noms sages : utilisables tels quels dans une URL, et seulement des images.
|
|
60
|
-
else if (!CAPTURE.test(c))
|
|
61
|
-
entry.problems.push(`capture ${c} : image .png, .jpg, .webp ou .gif, nom en lettres, chiffres, « . _ - / »`);
|
|
62
|
-
}
|
|
98
|
+
Object.assign(entry, readCaptures(head.captures, entry.problems));
|
|
63
99
|
if (!entry.title)
|
|
64
100
|
entry.problems.push('title vide');
|
|
65
101
|
if (!isDay(entry.date))
|
|
@@ -136,7 +172,7 @@ export function newEntry(dir, lots, rawTitle, today, now) {
|
|
|
136
172
|
if (existsSync(path))
|
|
137
173
|
throw new RafError(`${path} existe déjà`);
|
|
138
174
|
mkdirSync(dir, { recursive: true });
|
|
139
|
-
writeFileSync(path, `---\ntitle: ${stringify(title).trimEnd()}\ndate: ${today}\ncreated: ${toStamp(now)}\nlots: [${lots.join(', ')}]\ncaptures: []\n# nocapture: raison, quand une capture n'a pas de sens\n---\nCe qui change pour l'utilisateur.\n`);
|
|
175
|
+
writeFileSync(path, `---\ntitle: ${stringify(title).trimEnd()}\ndate: ${today}\ncreated: ${toStamp(now)}\nlots: [${lots.join(', ')}]\ncaptures: []\n# une capture peut dire ce qu'elle montre : captures: [${CAPTURE_SHAPE.replace('chemin', 'captures/x.png')}]\n# nocapture: raison, quand une capture n'a pas de sens\n---\nCe qui change pour l'utilisateur.\n`);
|
|
140
176
|
return path;
|
|
141
177
|
}
|
|
142
178
|
/**
|
|
@@ -210,7 +246,16 @@ export function newsData(project, entries, generated) {
|
|
|
210
246
|
return {
|
|
211
247
|
project,
|
|
212
248
|
generated,
|
|
213
|
-
entries: entries.map((e) => ({
|
|
249
|
+
entries: entries.map((e) => ({
|
|
250
|
+
slug: e.slug,
|
|
251
|
+
title: e.title,
|
|
252
|
+
date: e.date,
|
|
253
|
+
lots: e.lots,
|
|
254
|
+
captures: e.captures,
|
|
255
|
+
// Clé ajoutée seulement quand elle dit quelque chose : une entrée sans texte garde sa forme d'avant.
|
|
256
|
+
...(e.alts.some(Boolean) ? { alts: e.alts } : {}),
|
|
257
|
+
html: renderMarkdown(e.body),
|
|
258
|
+
})),
|
|
214
259
|
};
|
|
215
260
|
}
|
|
216
261
|
/** Écrit nouveautes.json, index.html et copie les captures sous `out`. */
|
|
@@ -237,7 +282,7 @@ function renderNewsPage(data) {
|
|
|
237
282
|
<h2>${escapeText(e.title)}</h2>
|
|
238
283
|
<p class="meta"><time datetime="${e.date}">${e.date}</time> · ${e.lots.map(escapeText).join(', ')}</p>
|
|
239
284
|
${e.html}
|
|
240
|
-
${e.captures.map((c) => `<a href="${escapeText(c)}"><img src="${escapeText(c)}" alt="Capture : ${
|
|
285
|
+
${e.captures.map((c, i) => `<a href="${escapeText(c)}"><img src="${escapeText(c)}" alt="${escapeText(e.alts?.[i] || `Capture : ${e.title}`)}" loading="lazy"></a>`).join('\n ')}
|
|
241
286
|
</article>`)
|
|
242
287
|
.join('\n');
|
|
243
288
|
return `<!doctype html>
|
package/dist/plan.js
CHANGED
|
@@ -12,9 +12,15 @@ export function isOpen(status) {
|
|
|
12
12
|
function escapeRe(s) {
|
|
13
13
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Ce qui ne peut pas suivre un identifiant cité : un point suivi d'un caractère de mot — « L1.4 » n'est
|
|
17
|
+
* pas le lot L1, « NC2.4 » n'est pas NC2 — alors que le point qui finit une phrase (« voir L1. ») passe.
|
|
18
|
+
* Même garde pour le format de raf et pour les identifiants d'un plan en lecture seule.
|
|
19
|
+
*/
|
|
20
|
+
const DOTTED = '\\.\\w';
|
|
15
21
|
/** Matches `L3` or `L3/t1` as whole words. Group 1 = lot id, group 2 = task id. */
|
|
16
22
|
export function refPattern(prefix) {
|
|
17
|
-
return new RegExp(`(?<![\\w/])(${escapeRe(prefix)}\\d+)(?:/(t\\d+))?(
|
|
23
|
+
return new RegExp(`(?<![\\w/])(${escapeRe(prefix)}\\d+)(?:/(t\\d+))?(?!\\w|${DOTTED})`, 'g');
|
|
18
24
|
}
|
|
19
25
|
export function extractRefs(text, prefix) {
|
|
20
26
|
return [...text.matchAll(refPattern(prefix))].map((m) => ({ lot: m[1], task: m[2] }));
|
|
@@ -149,7 +155,7 @@ export class Plan {
|
|
|
149
155
|
ids: new Set(ids),
|
|
150
156
|
tasks: new Set(lots.flatMap((l) => l.tasks.map((t) => `${l.id}/${t.id}`))),
|
|
151
157
|
refs: ids.length
|
|
152
|
-
? new RegExp(`(?<![\\w/.-])(${ids.map(escapeRe).join('|')})(?:/([\\w-]+(?:\\.[\\w-]+)*))?(?![\\w-]
|
|
158
|
+
? new RegExp(`(?<![\\w/.-])(${ids.map(escapeRe).join('|')})(?:/([\\w-]+(?:\\.[\\w-]+)*))?(?![\\w-]|${DOTTED})`, 'g')
|
|
153
159
|
: null,
|
|
154
160
|
};
|
|
155
161
|
}
|
|
@@ -431,7 +437,11 @@ function asVerdict(raw) {
|
|
|
431
437
|
if (!raw || typeof raw !== 'object')
|
|
432
438
|
return undefined;
|
|
433
439
|
const v = raw;
|
|
434
|
-
|
|
440
|
+
// Écrit à la main sans verdict (clé absente, vide ou blanche) : rien n'a été dit de la revue, la porte
|
|
441
|
+
// reste fermée — comme `raf ux` et `raf review` refusent d'enregistrer un verdict vide.
|
|
442
|
+
if (String(v.verdict ?? '').trim() === '')
|
|
443
|
+
return undefined;
|
|
444
|
+
const verdict = { date: String(v.date ?? ''), verdict: String(v.verdict) };
|
|
435
445
|
// Champ présent mais vide : relu « jusqu'à rien », comme un verdict noté sans commit.
|
|
436
446
|
if ('commit' in v)
|
|
437
447
|
verdict.commit = v.commit == null || String(v.commit).trim() === '' ? null : String(v.commit).trim();
|
package/dist/state.js
CHANGED
|
@@ -21,14 +21,17 @@ export function readNext(dir) {
|
|
|
21
21
|
const lines = rest.map((l) => l.replace(/^- /, '')).filter(Boolean);
|
|
22
22
|
return { date: head.replace(/^# /, '').trim(), lines };
|
|
23
23
|
}
|
|
24
|
-
/** Remplace les notes pour la prochaine session
|
|
24
|
+
/** Remplace les notes pour la prochaine session. Ne les efface jamais : c'est `clearNext`, demandé exprès. */
|
|
25
25
|
export function writeNext(dir, date, lines) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
if (lines.length === 0)
|
|
27
|
+
throw new Error('writeNext : aucune ligne');
|
|
28
|
+
writeFileSync(join(dir, 'next.md'), `# ${date}\n${lines.map((l) => `- ${l.replace(/\n/g, ' ')}`).join('\n')}\n`);
|
|
29
|
+
}
|
|
30
|
+
/** Efface les notes pour la prochaine session ; rend celles qui s'y trouvaient, null s'il n'y en avait pas. */
|
|
31
|
+
export function clearNext(dir) {
|
|
32
|
+
const previous = readNext(dir);
|
|
33
|
+
rmSync(join(dir, 'next.md'), { force: true });
|
|
34
|
+
return previous;
|
|
32
35
|
}
|
|
33
36
|
export function lockPath(dir) {
|
|
34
37
|
return join(dir, 'deliver.lock');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sylad/cadence",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "A small, repo-native working method: a versioned plan linked to your commits, a changelog with screenshots, session rituals and deliveries proven by their effect.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Sylvain Ladoire",
|
package/skills/deliver/SKILL.md
CHANGED
|
@@ -51,7 +51,18 @@ Deliver one project at a time: the one the human names, or ask.
|
|
|
51
51
|
4. On failure: read which step failed and why. Fix the cause, commit, push, deliver again. Never rerun
|
|
52
52
|
blindly, never skip a check to make it pass.
|
|
53
53
|
5. On success: `raf done <id>` (or the project's own tool when its plan is read-only) for the lots it lists **whose effect you have seen**; if one of them is
|
|
54
|
-
`visible`, `cadence news build` and deliver the news too.
|
|
54
|
+
`visible`, `cadence news build` and deliver the news too. With a read-only plan the list holds only
|
|
55
|
+
the lots that were in progress when the delivery started; the project's own tool has the last word
|
|
56
|
+
on what it marked delivered.
|
|
57
|
+
6. After a green delivery that changes what a page shows or what it is served (screen, API, data
|
|
58
|
+
source, configuration of either) — in practice every delivery except docs-, plan- or tests-only
|
|
59
|
+
ones — have the `qa-reviewer` agent walk the delivered app in a real browser, whether the lot
|
|
60
|
+
is `visible` or not: give it the repository path, the base URL and the lot id. When the lot
|
|
61
|
+
touched only the backend, the agent starts with the pages that call the changed endpoints. It
|
|
62
|
+
checks each page against `docs/qa/expectations.md` — what the user must find there — and
|
|
63
|
+
reports a page left empty, an error shown, an API call that failed or came back empty: what
|
|
64
|
+
the checks of `cadence.yaml` do not see. Bring its blocking findings to the human. It is not a
|
|
65
|
+
gate: the delivery stays done, a finding becomes a new lot.
|
|
55
66
|
|
|
56
67
|
## Rules
|
|
57
68
|
|
package/skills/lead/SKILL.md
CHANGED
|
@@ -74,6 +74,20 @@ proposed sub-tasks back to the human.
|
|
|
74
74
|
One project at a time, by the lead: push, then the `deliver` skill (`cadence deliver --dry-run`, then
|
|
75
75
|
`cadence deliver`). Follow the human's standing instructions about confirmation before production.
|
|
76
76
|
|
|
77
|
+
After a green delivery that changes what a page shows or what it is served (screen, API, data
|
|
78
|
+
source, configuration of either) — in practice every delivery except docs-, plan- or tests-only ones
|
|
79
|
+
— have the `qa-reviewer` agent check the delivered app, as a fresh subagent: give it the absolute
|
|
80
|
+
path of the project, the base URL of the delivered app and the lot id. The lot need not be
|
|
81
|
+
`visible`: a backend-only lot can empty a page without changing a screen. When the lot touched only
|
|
82
|
+
the backend, the agent starts with the pages that call the changed endpoints. It walks the pages in
|
|
83
|
+
a real browser against the project's expectations (`docs/qa/expectations.md`: per page, what the
|
|
84
|
+
user must find there) and returns measured findings; it reads only, and never logs in. Bring its
|
|
85
|
+
blocking findings back to the human — a page whose main content is missing, or that shows an error,
|
|
86
|
+
is a defect even when the delivery checks are green — with its proposed follow-up lines. A project
|
|
87
|
+
without an expectations file gets a draft back: show it to the human, who corrects it and decides
|
|
88
|
+
whether it is committed. It is not a gate: `raf done` does not wait for it, and a finding becomes a
|
|
89
|
+
new lot, not a reopened one.
|
|
90
|
+
|
|
77
91
|
## 5. Close
|
|
78
92
|
|
|
79
93
|
At the end, the `session-close` routine in each project touched, and `cadence session next` lines in
|
|
@@ -24,6 +24,9 @@ With no project named, run `cadence session close` in each project touched durin
|
|
|
24
24
|
otherwise `raf note <id> "where it stands, what blocks"`;
|
|
25
25
|
- a commit without a lot that belongs to one → `raf note <id> "commits: <sha> …"`; nothing if it
|
|
26
26
|
is genuinely outside the plan (docs, chores);
|
|
27
|
+
- a plan commit reported because it also touches a file generated from the plan (a published
|
|
28
|
+
plan) → propose to declare that file under `plan.files` in `cadence.yaml`; the files of a commit
|
|
29
|
+
decide whether it is plan upkeep, never its subject;
|
|
27
30
|
- a finished lot marked `visible` without a news entry → `cadence news new <id>`, written for the
|
|
28
31
|
user, with a screenshot;
|
|
29
32
|
- a lot that `raf done` refuses, or that the check reports as finished, for lack of a review —
|
|
@@ -43,6 +46,8 @@ With no project named, run `cadence session close` in each project touched durin
|
|
|
43
46
|
5. **Clean state**: everything committed and pushed, no delivery running. If the command still exits 1,
|
|
44
47
|
say what remains and do NOT say the session is closed.
|
|
45
48
|
6. **Three lines for next time**: `cadence session next "…" "…" "…"` — the next `session-start` shows them.
|
|
49
|
+
The lines replace the previous notes. Without a line the command refuses and keeps them; erase
|
|
50
|
+
them on purpose with `cadence session next --clear`, only when nothing is left to say.
|
|
46
51
|
|
|
47
52
|
## Do not
|
|
48
53
|
|