@ericrisco/rsc 3.7.4 → 4.0.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/README.md +16 -9
- package/manifest.json +49 -40
- package/package.json +1 -1
- package/scripts/lib/domains.js +1 -1
- package/scripts/lib/onboarding-apply.js +1 -1
- package/scripts/lib/onboarding.js +7 -7
- package/scripts/lib/retired-skills.js +4 -0
- package/scripts/lib/review-findings.js +3 -0
- package/scripts/lib/review-ledger.js +185 -0
- package/scripts/lib/risk.js +65 -0
- package/scripts/lib/spec-gate.js +10 -1
- package/scripts/rsc.js +91 -6
- package/skills/analyze/SKILL.md +3 -3
- package/skills/asdd/SKILL.md +159 -0
- package/skills/{sdd → asdd}/evals/README.md +10 -10
- package/skills/{sdd → asdd}/evals/cases.yaml +4 -4
- package/skills/{sdd → asdd}/references/model-routing.md +2 -2
- package/skills/building-agents/SKILL.md +1 -1
- package/skills/clarify/SKILL.md +48 -55
- package/skills/clarify/evals/cases.yaml +6 -7
- package/skills/connect-tool/SKILL.md +119 -21
- package/skills/connect-tool/THIRD_PARTY.md +43 -0
- package/skills/connect-tool/bridge-kit/01-TOOLS-plantilla/test_connection.sh +33 -0
- package/skills/connect-tool/bridge-kit/LICENSE +27 -0
- package/skills/connect-tool/bridge-kit/README.md +48 -0
- package/skills/connect-tool/bridge-kit/UPSTREAM.json +8 -0
- package/skills/connect-tool/bridge-kit/access/access_a_sqlite.py +82 -0
- package/skills/connect-tool/bridge-kit/dbhub/dbhub.toml.ejemplo +65 -0
- package/skills/connect-tool/bridge-kit/red/cloudflared-config.yml.ejemplo +18 -0
- package/skills/connect-tool/bridge-kit/red/tailscale-grants.hujson +22 -0
- package/skills/connect-tool/bridge-kit/sqlserver/01-login-servidor.sql +25 -0
- package/skills/connect-tool/bridge-kit/sqlserver/02-usuario-y-vistas.sql +69 -0
- package/skills/connect-tool/bridge-kit/sqlserver/03-prueba-solo-lectura.sql +65 -0
- package/skills/connect-tool/bridge-kit/sqlserver/copia-nocturna.ps1 +68 -0
- package/skills/connect-tool/evals/cases.yaml +41 -4
- package/skills/connect-tool/references/connect-or-migrate.md +96 -0
- package/skills/connect-tool/references/database-bridge.md +159 -0
- package/skills/connect-tool/references/file-exchange.md +103 -0
- package/skills/connect-tool/references/messages.md +120 -0
- package/skills/connect-tool/references/program-types.md +97 -0
- package/skills/connect-tool/references/spain.md +103 -0
- package/skills/connect-tool/references/what-to-find-out.md +57 -0
- package/skills/constitution/SKILL.md +21 -8
- package/skills/constitution/evals/README.md +1 -1
- package/skills/debug/SKILL.md +1 -1
- package/skills/fastapi/SKILL.md +1 -1
- package/skills/flutter/SKILL.md +1 -1
- package/skills/ftd/SKILL.md +26 -10
- package/skills/ftd/evals/cases.yaml +1 -1
- package/skills/ftd/evals/routing.json +4 -4
- package/skills/go/SKILL.md +1 -1
- package/skills/implement/SKILL.md +126 -113
- package/skills/implement/evals/cases.yaml +4 -2
- package/skills/implement/references/per-task-review.md +19 -26
- package/skills/nextjs/SKILL.md +1 -1
- package/skills/parallel/SKILL.md +1 -1
- package/skills/plan/SKILL.md +2 -2
- package/skills/postgresdb/SKILL.md +1 -1
- package/skills/react/SKILL.md +1 -1
- package/skills/review/SKILL.md +58 -31
- package/skills/sdd-init/SKILL.md +6 -6
- package/skills/ship/SKILL.md +38 -18
- package/skills/specify/SKILL.md +32 -33
- package/skills/specify/evals/cases.yaml +1 -1
- package/skills/specify/references/spec-template.md +27 -5
- package/skills/suggest/SKILL.md +4 -4
- package/skills/tasks/SKILL.md +37 -33
- package/skills/tasks/evals/README.md +1 -1
- package/skills/tasks/evals/cases.yaml +2 -2
- package/skills/verify/SKILL.md +30 -15
- package/skills/verify/evals/README.md +2 -2
- package/skills/verify/evals/cases.yaml +1 -0
- package/skills/worktrees/SKILL.md +1 -1
- package/targets/clone-bootstrap.mjs +1 -1
- package/targets/hook-once.mjs +2 -2
- package/targets/sello.mjs +1 -1
- package/skills/sdd/SKILL.md +0 -250
package/README.md
CHANGED
|
@@ -85,7 +85,7 @@ remove the need to check the result.
|
|
|
85
85
|
1. **Describe the project.** Choose the assistants you use and how technical the conversation should be.
|
|
86
86
|
2. **Review the setup.** See each proposed component and its reason, then accept the plan you want applied.
|
|
87
87
|
3. **Ask for the work.** The agent names the appropriate lane: an answer, a focused change (FTD), or the
|
|
88
|
-
specification-led chain (
|
|
88
|
+
specification-led chain (ASDD) for decisions that affect each other.
|
|
89
89
|
4. **Keep the result and its evidence.** Project knowledge goes into `02-DOCS/`; implementation stays in
|
|
90
90
|
your code. Future sessions can consult both.
|
|
91
91
|
|
|
@@ -109,7 +109,7 @@ npx @ericrisco/rsc@latest onboard
|
|
|
109
109
|
|
|
110
110
|
The first run asks **one** question about conversation (technical, or with analogies), then what the
|
|
111
111
|
project is for, its goal and the assistants to target. Then it presents the complete plan. A small
|
|
112
|
-
website can defer
|
|
112
|
+
website can defer ASDD, agents and code guards; an operations harness does not receive them merely
|
|
113
113
|
because it lives in a repository. Deferred components record the evidence that would make rsc
|
|
114
114
|
recommend them later, and `rsc reassess` reports it without installing anything.
|
|
115
115
|
|
|
@@ -186,10 +186,13 @@ Every turn takes exactly one lane, and the agent names it in one line, in your l
|
|
|
186
186
|
2. **FTD, Fast-Track Development.** The request authorises a change. This is the default for ordinary
|
|
187
187
|
work. One feature document holds intent, scope, a checklist, the evidence and the next step. Tasks
|
|
188
188
|
are checked off against observed proof, never against intention.
|
|
189
|
-
3. **
|
|
190
|
-
**The agent enters it on its own**, and you still
|
|
191
|
-
|
|
192
|
-
|
|
189
|
+
3. **ASDD, Agent Spec-Driven Development.** For the big or complex: several decisions that affect
|
|
190
|
+
each other, and work an agent may run for hours. **The agent enters it on its own**, and you still
|
|
191
|
+
decide *what* gets built: the spec answers four questions (what the user can do, what result they
|
|
192
|
+
get, what it must respect, how it proves it) and you approve the spec. Then every task is reviewed inside
|
|
193
|
+
the build, scaled by `rsc risk`; what the agent must not decide alone waits in a decision queue
|
|
194
|
+
while independent work continues. Low-risk work lands on green CI; auth, billing, CI and migrations
|
|
195
|
+
wait for you.
|
|
193
196
|
|
|
194
197
|
Ask for the other lane and it switches.
|
|
195
198
|
|
|
@@ -229,7 +232,7 @@ project. rsc handles four things by default:
|
|
|
229
232
|
| --- | --- | --- |
|
|
230
233
|
| **You choose how the project works** | The install asks once: straight on `main`, or branches and pull requests? With branches, a commit on `main` is refused and the agent asks before each change: this branch, a new one, or unlock `main`? It never opens a branch on its own. | «unlock main», or `rsc main unlock` |
|
|
231
234
|
| **One workspace per agent** | If another session works in the same folder, new work goes to `.worktrees/<branch>/` at the root you work in (never inside a project folder) and is never committed. A `post-merge` hook retires it once the branch lands. | `rsc isolation off` |
|
|
232
|
-
| **The agent picks the method** | FTD for simple changes,
|
|
235
|
+
| **The agent picks the method** | FTD for simple changes, ASDD for complex ones. The agent decides and says why. | Ask for the other lane |
|
|
233
236
|
| **Knowledge reaches everyone** | `01-TOOLS/`, `02-DOCS/wiki/` and `02-DOCS/attachments/` travel through one exchange branch, `rsc/knowledge`, and reach `main` inside your normal pull requests. | `rsc knowledge-sync off` |
|
|
234
237
|
|
|
235
238
|
**How it knows a project is complex.** A hook counts what anyone can check: a CI setup, a deployment
|
|
@@ -318,7 +321,11 @@ git-excluded local path, keeps 30 days, and injects at most 4,096 bytes. `rsc me
|
|
|
318
321
|
|
|
319
322
|
### Review that only brings you what is real
|
|
320
323
|
|
|
321
|
-
In
|
|
324
|
+
In ASDD, review runs inside every task, as deep as `rsc risk` says: none for docs, one fresh-context
|
|
325
|
+
reviewer for ordinary code, three adversarial reviewers (correctness, security, tests) for auth,
|
|
326
|
+
billing, CI or migrations. `rsc review ledger` caps the fix loop at three rounds and never lets a
|
|
327
|
+
finding be raised twice, and `rsc review escapes` turns every revert into a case for the reviewer. The
|
|
328
|
+
three reviewers
|
|
322
329
|
often report the same defect in different words, and a reviewer hunting for problems can overstate
|
|
323
330
|
one. So `rsc review consolidate` merges the duplicates, and every serious finding left goes to a
|
|
324
331
|
fresh `finding-verifier` agent whose starting position is that the finding is false. It blocks the
|
|
@@ -513,7 +520,7 @@ its own when a task matches.
|
|
|
513
520
|
Two lanes for change: `ftd` for ordinary work, and the ten-phase chain when durable artifacts would
|
|
514
521
|
settle a real ambiguity. `idea-refinement` runs inside `specify`; `decision-challenge` is on demand.
|
|
515
522
|
|
|
516
|
-
[ftd](skills/ftd/) · [
|
|
523
|
+
[ftd](skills/ftd/) · [asdd](skills/asdd/) · [constitution](skills/constitution/) · [idea-refinement](skills/idea-refinement/) · [specify](skills/specify/) · [clarify](skills/clarify/) · [plan](skills/plan/) · [tasks](skills/tasks/) · [analyze](skills/analyze/) · [decision-challenge](skills/decision-challenge/) · [implement](skills/implement/) · [source-grounded-development](skills/source-grounded-development/) · [verify](skills/verify/) · [review](skills/review/) · [simplify-code](skills/simplify-code/) · [ship](skills/ship/) · [debug](skills/debug/) · [worktrees](skills/worktrees/) · [parallel](skills/parallel/) · [tidy](skills/tidy/)
|
|
517
524
|
|
|
518
525
|
### 💼 Run a business
|
|
519
526
|
|
package/manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "
|
|
2
|
+
"version": "4.0.0",
|
|
3
3
|
"counts": {
|
|
4
4
|
"skills": 274,
|
|
5
5
|
"agents": 34,
|
|
@@ -260,6 +260,27 @@
|
|
|
260
260
|
],
|
|
261
261
|
"profiles": []
|
|
262
262
|
},
|
|
263
|
+
{
|
|
264
|
+
"id": "asdd",
|
|
265
|
+
"description": "Use when a feature or substantial change should go from idea to shipped, verified software and an agent may run it for hours on its own — the ASDD (Agent Spec-Driven Development) dispatcher and front door, in any language. A spec that must prove itself, review inside every task scaled by `rsc risk`, a decision queue for what the agent must not decide alone. Routes constitution -> specify -> clarify -> plan -> tasks -> analyze -> implement (verify + review per task) -> closing -> ship. Successor of `sdd`. NOT a single phase (it dispatches), NOT small authorised changes (that is `ftd`), NOT the workspace harness (that is `harness`).",
|
|
266
|
+
"tags": [
|
|
267
|
+
"sdd",
|
|
268
|
+
"asdd",
|
|
269
|
+
"spec",
|
|
270
|
+
"workflow",
|
|
271
|
+
"plan",
|
|
272
|
+
"review"
|
|
273
|
+
],
|
|
274
|
+
"recommends": [
|
|
275
|
+
"sdd-init",
|
|
276
|
+
"constitution",
|
|
277
|
+
"specify"
|
|
278
|
+
],
|
|
279
|
+
"profiles": [
|
|
280
|
+
"core",
|
|
281
|
+
"full"
|
|
282
|
+
]
|
|
283
|
+
},
|
|
263
284
|
{
|
|
264
285
|
"id": "astro",
|
|
265
286
|
"description": "Use when building a content-driven or marketing site with Astro 6: static-first pages, islands and partial hydration, content collections, server islands, per-route on-demand rendering, deploy adapters, and Astro 5→6 migration. NOT app-router React with server actions and heavy client interactivity (that is `nextjs`).",
|
|
@@ -589,7 +610,7 @@
|
|
|
589
610
|
},
|
|
590
611
|
{
|
|
591
612
|
"id": "clarify",
|
|
592
|
-
"description": "Use when a spec exists and must be de-risked before planning —
|
|
613
|
+
"description": "Use when a spec exists and must be de-risked before planning — run the gap checklist over its four ASDD questions and out-of-scope, ask only what changes what gets built (one question at a time, recommendation first), record every other call as an assumption, bake the answers back into the spec. The second half of ASDD stage 2 (Spec), between `specify` (writes the spec) and `plan` (designs the build). NOT the cross-artifact consistency check (that is `analyze`).",
|
|
593
614
|
"tags": [
|
|
594
615
|
"sdd",
|
|
595
616
|
"clarify",
|
|
@@ -813,7 +834,7 @@
|
|
|
813
834
|
},
|
|
814
835
|
{
|
|
815
836
|
"id": "connect-tool",
|
|
816
|
-
"description": "Use when the user names a specific tool to connect to the agent (an ERP, accounting or desktop program, CRM, WhatsApp, a spreadsheet, a database): research its real docs,
|
|
837
|
+
"description": "Use when the user names a specific tool to connect to the agent (an ERP, accounting or desktop program, CRM, WhatsApp, a spreadsheet, a database): research its real docs first, and when none exist, diagnose the program with the user and keep going. Picks the safest route, builds `01-TOOLS/<TOOL>/` with a test that proves it. NOT a full typed API client (that is `api-connector-builder`), NOT a whole-workspace tooling scan (that is `harness`).",
|
|
817
838
|
"tags": [
|
|
818
839
|
"connect",
|
|
819
840
|
"integration",
|
|
@@ -825,6 +846,8 @@
|
|
|
825
846
|
"mcp",
|
|
826
847
|
"api",
|
|
827
848
|
"export",
|
|
849
|
+
"import",
|
|
850
|
+
"file-exchange",
|
|
828
851
|
"least-privilege",
|
|
829
852
|
"onboarding"
|
|
830
853
|
],
|
|
@@ -842,11 +865,12 @@
|
|
|
842
865
|
},
|
|
843
866
|
{
|
|
844
867
|
"id": "constitution",
|
|
845
|
-
"description": "Use when setting or amending a project's non-negotiables — stack canon, quality bars, conventions, security/a11y floors — as numbered, testable rules later phases obey.
|
|
868
|
+
"description": "Use when setting or amending a project's non-negotiables — stack canon, quality bars, conventions, security/a11y floors — as numbered, testable rules later phases obey, and drafting the REVIEW.md the reviewer reads. ASDD's base stage; writes 02-DOCS/wiki/sdd/constitution.md and REVIEW.md at the project root. NOT a feature spec (that is `specify`), NOT the technical plan (that is `plan`), NOT the wiki itself (that is `harness`).",
|
|
846
869
|
"tags": [
|
|
847
|
-
"
|
|
870
|
+
"asdd",
|
|
848
871
|
"constitution",
|
|
849
|
-
"principles"
|
|
872
|
+
"principles",
|
|
873
|
+
"review"
|
|
850
874
|
],
|
|
851
875
|
"recommends": [
|
|
852
876
|
"specify"
|
|
@@ -1734,7 +1758,6 @@
|
|
|
1734
1758
|
"epistemics"
|
|
1735
1759
|
],
|
|
1736
1760
|
"recommends": [
|
|
1737
|
-
"sdd",
|
|
1738
1761
|
"verify",
|
|
1739
1762
|
"debug",
|
|
1740
1763
|
"review"
|
|
@@ -1927,7 +1950,7 @@
|
|
|
1927
1950
|
},
|
|
1928
1951
|
{
|
|
1929
1952
|
"id": "ftd",
|
|
1930
|
-
"description": "Use for ordinary authorised change — the default lane once the decisor has ruled out a question and not selected the chain. Keeps one feature document per feature in 02-DOCS holding intent, scope, checklist, evidence and next step, checks tasks off only against observed proof,
|
|
1953
|
+
"description": "Use for ordinary authorised change — the default lane once the decisor has ruled out a question and not selected the chain. Keeps one feature document per feature in 02-DOCS holding intent, scope, checklist, evidence and next step, checks tasks off only against observed proof, asks whether to branch before the work writes code, and runs a minimal risk loop (self-check, `rsc risk`, one reviewer from tier 1). NOT the full method (that is `asdd`), NOT the classification itself (that is the decisor in `suggest`), NOT a request for information, which writes nothing.",
|
|
1931
1954
|
"tags": [
|
|
1932
1955
|
"ftd",
|
|
1933
1956
|
"lane",
|
|
@@ -2429,14 +2452,16 @@
|
|
|
2429
2452
|
},
|
|
2430
2453
|
{
|
|
2431
2454
|
"id": "implement",
|
|
2432
|
-
"description": "Use when an approved plan and task list exist and it is time to turn them into working,
|
|
2455
|
+
"description": "Use when an approved plan and task list exist and it is time to turn them into working, reviewed code — ASDD stage 4, the per-task loop after `analyze`: TDD build and self-check, `rsc risk` on the diff, review scaled by tier (0/1/3 reviewers), mandatory controls (tests, build, security, traceability), at most 3 fix rounds, then fix-and-record or park in the decision queue. Progress ledger survives compaction; disjoint tasks fan out via `parallel`. NOT spec writing (that is `specify`), NOT planning (that is `plan`), NOT the closing whole-branch gate (stage 5: `verify` + `review`).",
|
|
2433
2456
|
"tags": [
|
|
2457
|
+
"asdd",
|
|
2434
2458
|
"sdd",
|
|
2435
2459
|
"implement",
|
|
2436
2460
|
"code"
|
|
2437
2461
|
],
|
|
2438
2462
|
"recommends": [
|
|
2439
|
-
"verify"
|
|
2463
|
+
"verify",
|
|
2464
|
+
"review"
|
|
2440
2465
|
],
|
|
2441
2466
|
"profiles": [
|
|
2442
2467
|
"core",
|
|
@@ -4132,11 +4157,12 @@
|
|
|
4132
4157
|
},
|
|
4133
4158
|
{
|
|
4134
4159
|
"id": "review",
|
|
4135
|
-
"description": "Use when a
|
|
4160
|
+
"description": "Use when a diff needs adversarial judgment in rsc ASDD — per task inside implement's loop (depth set by `rsc risk`: none, one fresh-context reviewer, or the three-refuter panel) or as the closing review of the whole branch against the spec — keyed to the spec/plan/constitution in 02-DOCS/wiki/sdd/ and the project's REVIEW.md; or receive a review and verify each comment before agreeing. Converges via `rsc review ledger` (max 3 rounds, no repeated finding). NOT running lint/type/tests (that is `verify`), NOT the spec-less standalone pass (that is `code-review`), NOT merging (that is `ship`).",
|
|
4136
4161
|
"tags": [
|
|
4137
|
-
"
|
|
4162
|
+
"asdd",
|
|
4138
4163
|
"review",
|
|
4139
|
-
"code-review"
|
|
4164
|
+
"code-review",
|
|
4165
|
+
"risk"
|
|
4140
4166
|
],
|
|
4141
4167
|
"recommends": [
|
|
4142
4168
|
"ship"
|
|
@@ -4339,28 +4365,9 @@
|
|
|
4339
4365
|
"full"
|
|
4340
4366
|
]
|
|
4341
4367
|
},
|
|
4342
|
-
{
|
|
4343
|
-
"id": "sdd",
|
|
4344
|
-
"description": "Use when you want a disciplined, spec-driven path from a feature idea to shipped, verified software — the SDD dispatcher and front door, in any language. States the method, reads the user's register from 02-DOCS, and routes to the right phase: constitution -> specify -> clarify -> plan -> tasks -> analyze -> implement -> verify -> review -> ship, with debug / worktrees / parallel on demand. Use it to start a feature, to govern the whole flow, or when unsure which phase you are in. NOT a single phase itself (it dispatches), NOT the workspace harness (that is `harness`), NOT a stack build skill.",
|
|
4345
|
-
"tags": [
|
|
4346
|
-
"sdd",
|
|
4347
|
-
"spec",
|
|
4348
|
-
"workflow",
|
|
4349
|
-
"plan"
|
|
4350
|
-
],
|
|
4351
|
-
"recommends": [
|
|
4352
|
-
"sdd-init",
|
|
4353
|
-
"constitution",
|
|
4354
|
-
"specify"
|
|
4355
|
-
],
|
|
4356
|
-
"profiles": [
|
|
4357
|
-
"core",
|
|
4358
|
-
"full"
|
|
4359
|
-
]
|
|
4360
|
-
},
|
|
4361
4368
|
{
|
|
4362
4369
|
"id": "sdd-init",
|
|
4363
|
-
"description": "Use when calibrating an existing repo for the rsc SDD chain: detect stack, package manager, test runners and apply/verify commands, refresh the skill registry, and write the runtime config at 02-DOCS/wiki/sdd/config.yaml. NOT the dispatcher that routes SDD phases (that is `
|
|
4370
|
+
"description": "Use when calibrating an existing repo for the rsc SDD chain: detect stack, package manager, test runners and apply/verify commands, refresh the skill registry, and write the runtime config at 02-DOCS/wiki/sdd/config.yaml. NOT the dispatcher that routes SDD phases (that is `asdd`), NOT first-contact user/workspace bootstrap (that is `init`).",
|
|
4364
4371
|
"tags": [
|
|
4365
4372
|
"sdd",
|
|
4366
4373
|
"init",
|
|
@@ -4369,7 +4376,6 @@
|
|
|
4369
4376
|
"registry"
|
|
4370
4377
|
],
|
|
4371
4378
|
"recommends": [
|
|
4372
|
-
"sdd",
|
|
4373
4379
|
"specify",
|
|
4374
4380
|
"implement",
|
|
4375
4381
|
"verify"
|
|
@@ -4437,12 +4443,13 @@
|
|
|
4437
4443
|
},
|
|
4438
4444
|
{
|
|
4439
4445
|
"id": "ship",
|
|
4440
|
-
"description": "Use when a verified
|
|
4446
|
+
"description": "Use when a verified branch that passed its closing review has to land — the close of rsc ASDD: safety checks, an explainer for the human (what changed, what the checks proved and would not catch), then three landing options (merge, PR with Testing/Risk/How to revert, park/discard). Risk tier 0-1 with CI green may self-merge; tier 2 waits for human approval. Authorship always the human, never an AI trailer. NOT running the gates (that is `verify`), NOT reading the diff for defects (that is `review`), NOT shipping to a server (that is `deployment`).",
|
|
4441
4447
|
"tags": [
|
|
4442
|
-
"
|
|
4448
|
+
"asdd",
|
|
4443
4449
|
"ship",
|
|
4444
4450
|
"release",
|
|
4445
|
-
"pr"
|
|
4451
|
+
"pr",
|
|
4452
|
+
"risk"
|
|
4446
4453
|
],
|
|
4447
4454
|
"recommends": [],
|
|
4448
4455
|
"profiles": [
|
|
@@ -4689,7 +4696,7 @@
|
|
|
4689
4696
|
},
|
|
4690
4697
|
{
|
|
4691
4698
|
"id": "specify",
|
|
4692
|
-
"description": "Use when a feature, change, or product idea is still fuzzy and must become an approved WHAT/WHY spec before any planning or code — the brainstorming front door of
|
|
4699
|
+
"description": "Use when a feature, change, or product idea is still fuzzy and must become an approved WHAT/WHY spec before any planning or code — the brainstorming front door of ASDD stage 2 (Spec). Turns a one-line intent into problem, goals, users, out-of-scope, the four ASDD questions (what the user can do, what result they get, what it must respect, how it proves it) and acceptance criteria, via frontier-round dialogue and 2-3 approaches with a recommendation, carrying zero implementation detail. Fires whenever someone jumps to HOW before WHAT is agreed, in any language. NOT the technical plan (that is `plan`), NOT the de-risking ambiguity sweep (that is `clarify`), NOT project-wide principles (that is `constitution`).",
|
|
4693
4700
|
"tags": [
|
|
4694
4701
|
"sdd",
|
|
4695
4702
|
"spec",
|
|
@@ -4911,8 +4918,9 @@
|
|
|
4911
4918
|
},
|
|
4912
4919
|
{
|
|
4913
4920
|
"id": "tasks",
|
|
4914
|
-
"description": "Use when an approved plan needs slicing into an ordered task list before any code — the
|
|
4921
|
+
"description": "Use when an approved plan needs slicing into an ordered task list before any code — the `tasks` step of ASDD stage 3 (Plan). Each row carries the spec criterion it covers, a risk forecast (tier 0/1/2), a literal done-check, dependencies and a parallel-safe marker, appended to the plan artifact. NOT writing the plan (that is `plan`), NOT the consistency gate (that is `analyze`), NOT executing the tasks (that is `implement`).",
|
|
4915
4922
|
"tags": [
|
|
4923
|
+
"asdd",
|
|
4916
4924
|
"sdd",
|
|
4917
4925
|
"tasks",
|
|
4918
4926
|
"breakdown"
|
|
@@ -5409,8 +5417,9 @@
|
|
|
5409
5417
|
},
|
|
5410
5418
|
{
|
|
5411
5419
|
"id": "verify",
|
|
5412
|
-
"description": "Use when
|
|
5420
|
+
"description": "Use when work is about to be called done — the ASDD evidence gate, in two modes: per task inside `implement`'s loop (tests, build, security, traceability) and at closing over the whole branch against main (stack scripts/verify.sh, every done-check and acceptance criterion, rollback plan, dated verdict). A check that did not execute is reported not run, never passed. NOT judging the diff by eye (that is `review`, spec-less `code-review`), NOT diagnosis (that is `debug`).",
|
|
5413
5421
|
"tags": [
|
|
5422
|
+
"asdd",
|
|
5414
5423
|
"sdd",
|
|
5415
5424
|
"verify",
|
|
5416
5425
|
"test"
|
package/package.json
CHANGED
package/scripts/lib/domains.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
export const DOMAINS = [
|
|
7
7
|
{ title: 'Core & control plane', ids: ['init', 'harness', 'orient', 'suggest', 'unslop', 'author-skill', 'sdd-init'] },
|
|
8
|
-
{ title: 'Spec-Driven Development', ids: ['ftd', '
|
|
8
|
+
{ title: 'Spec-Driven Development', ids: ['ftd', 'asdd', 'constitution', 'idea-refinement', 'specify', 'clarify', 'plan', 'tasks', 'analyze', 'decision-challenge', 'implement', 'source-grounded-development', 'verify', 'review', 'simplify-code', 'ship', 'debug', 'worktrees', 'parallel', 'tidy'] },
|
|
9
9
|
{ title: 'Run a business', ids: ['finance-ops', 'invoicing', 'bookkeeping', 'pricing', 'sales-pipeline', 'lead-gen', 'cold-outreach', 'proposals', 'contracts', 'customer-support', 'client-onboarding', 'retention', 'hiring', 'people-ops', 'inventory', 'logistics-ops', 'procurement', 'meeting-notes', 'sop-builder', 'project-ops'] },
|
|
10
10
|
{ title: 'Raise & model money', ids: ['pitch-deck', 'investor-materials', 'financial-model', 'fundraising', 'unit-economics', 'grants'] },
|
|
11
11
|
{ title: 'Legal, privacy & compliance', ids: ['gdpr-privacy', 'terms-conditions', 'compliance', 'data-policy', 'ip-trademark'] },
|
|
@@ -111,7 +111,7 @@ export function renderOnboardingDocuments(plan, planId) {
|
|
|
111
111
|
const profile = `---\ntechnical_level: ${plan.record.technicalLevel}\nproject_kind: ${plan.record.projectKind}\n---\n\n# User profile\n\nGoal: ${plan.record.goal}\n`;
|
|
112
112
|
const rows = plan.decisions.map((d) => `| ${d.kind} | ${d.id} | ${d.state} | ${d.reason} | ${d.reevaluateWhen.join('; ') || '—'} |`).join('\n');
|
|
113
113
|
const installation = `# Accepted harness plan\n\nPlan id: \`${planId}\`\n\n| Kind | Component | Decision | Reason | Reevaluate when |\n| --- | --- | --- | --- | --- |\n${rows}\n`;
|
|
114
|
-
const decisions = `# Harness decisions\n\n- Accepted plan \`${planId}\`.\n- Project kind: ${plan.record.projectKind}.\n- SDD: ${plan.decisions.find((d) => d.id === '
|
|
114
|
+
const decisions = `# Harness decisions\n\n- Accepted plan \`${planId}\`.\n- Project kind: ${plan.record.projectKind}.\n- SDD: ${plan.decisions.find((d) => d.id === 'asdd')?.state || 'selected through profile'}.\n`;
|
|
115
115
|
return { profile, installation, decisions };
|
|
116
116
|
}
|
|
117
117
|
|
|
@@ -276,10 +276,10 @@ export function buildOnboardingPlan(record, evidence) {
|
|
|
276
276
|
// `sdd` is installed everywhere now, but its DECISION is about practice, not presence — so where
|
|
277
277
|
// the chain is not practised the explicit `deferred` entry below must be the only one, or the
|
|
278
278
|
// generic "installed → selected" mapping would shadow it and `reassess` would see nothing to watch.
|
|
279
|
-
const decisions = skills.filter((id) => practisesSdd || id !== '
|
|
279
|
+
const decisions = skills.filter((id) => practisesSdd || id !== 'asdd').map((id) => detectedSkills.includes(id)
|
|
280
280
|
? selected(id, 'skill', `Detected ${id} evidence inside the selected project root.`, 'workspace-evidence')
|
|
281
281
|
: selected(id, 'skill', needsSdd
|
|
282
|
-
? (id === '
|
|
282
|
+
? (id === 'asdd' && complexitySignals.length
|
|
283
283
|
? `Included because the accepted work has ${complexitySignals.join(', ')} complexity.`
|
|
284
284
|
: `Included in the development workflow for ${normalized.softwareScope} software.`)
|
|
285
285
|
: `Included in the lightweight foundation for this ${normalized.projectKind} project.`));
|
|
@@ -293,16 +293,16 @@ export function buildOnboardingPlan(record, evidence) {
|
|
|
293
293
|
// so the chain is always one request away; this decision is what `reassess` watches, and what the
|
|
294
294
|
// floor reads. Collapsing the two would have deleted reassess's reason to exist as a side effect
|
|
295
295
|
// of an installer change, which is not a decision an installer change gets to make.
|
|
296
|
-
// The reason says both halves, because the plan lists `.claude/skills/
|
|
296
|
+
// The reason says both halves, because the plan lists `.claude/skills/asdd` under managed paths
|
|
297
297
|
// and a bare "deferred" next to it read as a contradiction (E2E 2026-10-07). Only the wording
|
|
298
|
-
// changes: the decision stays the one deferred `workflow/
|
|
298
|
+
// changes: the decision stays the one deferred `workflow/asdd` entry that reassess watches.
|
|
299
299
|
const whyDeferred = isSoftware
|
|
300
300
|
? 'the software scope is small, so specification overhead is not justified yet.'
|
|
301
301
|
: 'SDD applies to substantial software work, which is not the declared project purpose.';
|
|
302
|
-
const sddReason = skills.includes('
|
|
303
|
-
? `The
|
|
302
|
+
const sddReason = skills.includes('asdd')
|
|
303
|
+
? `The asdd skill is installed, so the chain is one request away; the practice is deferred: ${whyDeferred}`
|
|
304
304
|
: whyDeferred.charAt(0).toUpperCase() + whyDeferred.slice(1);
|
|
305
|
-
if (!practisesSdd) decisions.push(deferred('
|
|
305
|
+
if (!practisesSdd) decisions.push(deferred('asdd', 'workflow', sddReason, sddTriggers, softwareTriggers));
|
|
306
306
|
if (baseAgents) {
|
|
307
307
|
for (const id of agents) decisions.push(selected(id, 'agent', 'The accepted substantial software workflow requires this implementation or review role.'));
|
|
308
308
|
} else {
|
|
@@ -14,10 +14,14 @@
|
|
|
14
14
|
//
|
|
15
15
|
// `ui-engineering` folded into `ux` in 3.4.0: the screen states and form behaviour it owned are
|
|
16
16
|
// `ux`'s state matrix now, and where component state lives is `react`'s.
|
|
17
|
+
//
|
|
18
|
+
// `sdd` became `asdd` in 4.0.0 (spec asdd-v4): the same phases, with the review moved inside each
|
|
19
|
+
// task of `implement` and scaled by `rsc risk`. Artifacts stay under 02-DOCS/wiki/sdd/.
|
|
17
20
|
export const RETIRED_SKILLS = Object.freeze({
|
|
18
21
|
eli5: 'orient', 'show-me': 'orient', bro: 'unslop',
|
|
19
22
|
'design-loop': 'design', 'design-dna': 'design', 'motion-craft': 'design',
|
|
20
23
|
'ui-engineering': 'ux',
|
|
24
|
+
sdd: 'asdd',
|
|
21
25
|
});
|
|
22
26
|
|
|
23
27
|
const retiredTo = (id) => (Object.hasOwn(RETIRED_SKILLS, id) ? RETIRED_SKILLS[id] : id);
|
|
@@ -68,6 +68,7 @@ export function normalizeFinding(f) {
|
|
|
68
68
|
claim: text(f.claim) || text(f.title) || text(f.summary),
|
|
69
69
|
failure_scenario: text(f.failure_scenario) || text(f.repro),
|
|
70
70
|
evidence: text(f.evidence),
|
|
71
|
+
class: text(f.class) || text(f.bug_class) || text(f.defect_class),
|
|
71
72
|
notes: [],
|
|
72
73
|
};
|
|
73
74
|
if (!out.severity) {
|
|
@@ -140,6 +141,8 @@ export function consolidate(rawFindings, { window = DEFAULT_WINDOW } = {}) {
|
|
|
140
141
|
claims: uniq(members.map((m) => m.claim)),
|
|
141
142
|
failure_scenarios: uniq(members.map((m) => m.failure_scenario)),
|
|
142
143
|
evidence: uniq(members.map((m) => m.evidence)),
|
|
144
|
+
// Carried through so `review ledger record` can fingerprint by bug class.
|
|
145
|
+
class: uniq(members.map((m) => m.class))[0] || '',
|
|
143
146
|
notes: uniq(members.flatMap((m) => m.notes)),
|
|
144
147
|
reported: members.length,
|
|
145
148
|
verify: rank(severity) <= rank('should-fix'),
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// The review ledger — the deterministic half of ASDD's fix loop and of the reviewer's learning.
|
|
2
|
+
//
|
|
3
|
+
// Anthropic's reviewer and author once looped more than fifty rounds on one PR, and about 59% of what
|
|
4
|
+
// the reviewer kept flagging was code it had already seen. Three rules stopped that: cap the rounds,
|
|
5
|
+
// never repeat a finding, and record what happened to each finding so noise can be measured. A rule
|
|
6
|
+
// that only lives in a skill's prose is the decorative gate (P2), so the three live here.
|
|
7
|
+
// Spec: 02-DOCS/wiki/sdd/specs/asdd-v4.md.
|
|
8
|
+
//
|
|
9
|
+
// One JSONL file, append-only: `finding`, `outcome` and `round` entries. It sits under .rsc/ and is
|
|
10
|
+
// excluded from the sello's candidate like every other journal the harness writes (targets/sello.mjs).
|
|
11
|
+
|
|
12
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { spawnSync } from 'node:child_process';
|
|
15
|
+
|
|
16
|
+
export const LEDGER_PATH = '.rsc/review-ledger.jsonl';
|
|
17
|
+
export const OUTCOMES = ['fixed', 'false', 'intentional', 'irrelevant'];
|
|
18
|
+
export const MAX_ROUNDS = 3;
|
|
19
|
+
export const EVALS_DIR = '02-DOCS/wiki/sdd/review-evals';
|
|
20
|
+
|
|
21
|
+
// Two findings are the same claim when they sit in the same file and share most of their words, or
|
|
22
|
+
// name the same bug class AND still share some. A class alone is not enough: two different
|
|
23
|
+
// null-check bugs in one file are two findings, and suppressing the second hides a real defect.
|
|
24
|
+
// The line is left out on purpose: it moves between rounds, and a repeat must survive the fix
|
|
25
|
+
// that moved it.
|
|
26
|
+
export const SAME_CLAIM = 0.5;
|
|
27
|
+
export const SAME_CLASS_CLAIM = 0.2;
|
|
28
|
+
const STOP = new Set('the and for with not that this its are was from any only one but has have into than then when which will can'.split(' '));
|
|
29
|
+
const words = (s) => new Set(String(s || '').toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 2 && !STOP.has(w)));
|
|
30
|
+
function overlap(a, b) {
|
|
31
|
+
const A = words(a); const B = words(b);
|
|
32
|
+
let shared = 0;
|
|
33
|
+
for (const w of A) if (B.has(w)) shared += 1;
|
|
34
|
+
const union = A.size + B.size - shared;
|
|
35
|
+
return union ? shared / union : 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const text = (v) => (typeof v === 'string' ? v.trim() : '');
|
|
39
|
+
const normFile = (f) => text(f).replace(/\\/g, '/').replace(/^\.\//, '');
|
|
40
|
+
|
|
41
|
+
// Accepts what the lenses emit and what `review consolidate --json` prints.
|
|
42
|
+
export function normalize(f) {
|
|
43
|
+
return {
|
|
44
|
+
file: normFile(f.file),
|
|
45
|
+
class: text(f.class) || text(f.bug_class) || text(f.defect_class),
|
|
46
|
+
claim: text(f.claim) || (Array.isArray(f.claims) ? text(f.claims[0]) : '') || text(f.title) || text(f.summary),
|
|
47
|
+
severity: text(f.severity) || 'question',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function sameClaim(a, b) {
|
|
52
|
+
if (a.file !== b.file) return false;
|
|
53
|
+
const o = overlap(a.claim, b.claim);
|
|
54
|
+
if (a.class && b.class && a.class.toLowerCase() === b.class.toLowerCase()) return o >= SAME_CLASS_CLAIM;
|
|
55
|
+
return o >= SAME_CLAIM;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function readLedger(root) {
|
|
59
|
+
const path = join(root, LEDGER_PATH);
|
|
60
|
+
if (!existsSync(path)) return [];
|
|
61
|
+
return readFileSync(path, 'utf8').split('\n').filter(Boolean).flatMap((line) => {
|
|
62
|
+
try { return [JSON.parse(line)]; } catch { return []; }
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function append(root, entries) {
|
|
67
|
+
mkdirSync(join(root, '.rsc'), { recursive: true });
|
|
68
|
+
appendFileSync(join(root, LEDGER_PATH), entries.map((e) => `${JSON.stringify(e)}\n`).join(''));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// → [{id, status: 'new'|'repeat', repeatOf?, ...finding}]. A repeat is recorded too (it is the noise
|
|
72
|
+
// being measured) but points at the original, and the caller must not raise it again.
|
|
73
|
+
export function record(root, rawFindings, { task = '', now = new Date().toISOString() } = {}) {
|
|
74
|
+
const ledger = readLedger(root);
|
|
75
|
+
// A finding closed as fixed that shows up again is a regression, not noise: it is raised anew.
|
|
76
|
+
const fixed = new Set(ledger.filter((e) => e.type === 'outcome' && e.outcome === 'fixed').map((e) => e.id));
|
|
77
|
+
const seen = ledger.filter((e) => e.type === 'finding' && !e.repeatOf && !fixed.has(e.id));
|
|
78
|
+
let n = ledger.filter((e) => e.type === 'finding').length;
|
|
79
|
+
const out = [];
|
|
80
|
+
for (const raw of rawFindings) {
|
|
81
|
+
if (!raw || typeof raw !== 'object') continue;
|
|
82
|
+
const f = normalize(raw);
|
|
83
|
+
n += 1;
|
|
84
|
+
const original = seen.find((s) => sameClaim(s, f));
|
|
85
|
+
const entry = { type: 'finding', id: `L${n}`, task, at: now, ...f, ...(original ? { repeatOf: original.id } : {}) };
|
|
86
|
+
if (!original) seen.push(entry);
|
|
87
|
+
out.push(entry);
|
|
88
|
+
}
|
|
89
|
+
append(root, out);
|
|
90
|
+
return out.map((e) => ({ ...e, status: e.repeatOf ? 'repeat' : 'new' }));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function outcome(root, id, value, { now = new Date().toISOString() } = {}) {
|
|
94
|
+
if (!OUTCOMES.includes(value)) {
|
|
95
|
+
return { ok: false, message: `rsc: unknown outcome "${value}". Use one of: ${OUTCOMES.join(', ')}.` };
|
|
96
|
+
}
|
|
97
|
+
if (!readLedger(root).some((e) => e.type === 'finding' && e.id === id)) {
|
|
98
|
+
return { ok: false, message: `rsc: no finding "${id}" in ${LEDGER_PATH}. Run \`npx @ericrisco/rsc review ledger stats\` to see the recorded ids.` };
|
|
99
|
+
}
|
|
100
|
+
append(root, [{ type: 'outcome', id, outcome: value, at: now }]);
|
|
101
|
+
return { ok: true, message: `${id}: ${value}` };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Counts one more review round for a task. The fourth is refused, with the way out in the message
|
|
105
|
+
// (P6): rethink the approach, or park the task in the decision queue and go on with independent work.
|
|
106
|
+
// The task is qualified by its spec (`<slug>/T001`): every plan numbers its tasks from T001, and a
|
|
107
|
+
// bare id would let one feature inherit another's rounds and park work never reviewed.
|
|
108
|
+
export const QUALIFIED_TASK = /^[^/\s]+\/[^/\s]+$/;
|
|
109
|
+
export function round(root, task, { now = new Date().toISOString() } = {}) {
|
|
110
|
+
if (!QUALIFIED_TASK.test(task)) {
|
|
111
|
+
return { ok: false, round: 0, message: `rsc: qualify the task with its spec slug, e.g. \`review ledger round <slug>/${task || 'T001'}\` — task ids repeat across plans.` };
|
|
112
|
+
}
|
|
113
|
+
const n = readLedger(root).filter((e) => e.type === 'round' && e.task === task).length + 1;
|
|
114
|
+
if (n > MAX_ROUNDS) {
|
|
115
|
+
return {
|
|
116
|
+
ok: false, round: n,
|
|
117
|
+
message: `rsc: task "${task}" has had ${MAX_ROUNDS} review rounds and has not converged. Stop fixing line by line: rethink the approach, or park the task in the decision queue (02-DOCS/wiki/sdd/progress/<slug>.md, "## Decision queue") and continue with independent tasks.`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
append(root, [{ type: 'round', task, n, at: now }]);
|
|
121
|
+
return { ok: true, round: n, message: `task "${task}": review round ${n} of ${MAX_ROUNDS}` };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function stats(root) {
|
|
125
|
+
const ledger = readLedger(root);
|
|
126
|
+
const findings = ledger.filter((e) => e.type === 'finding');
|
|
127
|
+
const originals = findings.filter((f) => !f.repeatOf);
|
|
128
|
+
const last = new Map();
|
|
129
|
+
for (const e of ledger) if (e.type === 'outcome') last.set(e.id, e.outcome);
|
|
130
|
+
const outcomes = Object.fromEntries([...OUTCOMES, 'open'].map((k) => [k, 0]));
|
|
131
|
+
for (const f of originals) outcomes[last.get(f.id) || 'open'] += 1;
|
|
132
|
+
const closed = originals.length - outcomes.open;
|
|
133
|
+
const rounds = new Map();
|
|
134
|
+
for (const e of ledger) if (e.type === 'round') rounds.set(e.task, Math.max(rounds.get(e.task) || 0, e.n));
|
|
135
|
+
const totalRounds = [...rounds.values()].reduce((a, b) => a + b, 0);
|
|
136
|
+
return {
|
|
137
|
+
findings: originals.length,
|
|
138
|
+
repeats: findings.length - originals.length,
|
|
139
|
+
outcomes,
|
|
140
|
+
falseRate: closed ? outcomes.false / closed : null,
|
|
141
|
+
tasks: rounds.size,
|
|
142
|
+
meanRounds: rounds.size ? totalRounds / rounds.size : null,
|
|
143
|
+
open: originals.filter((f) => !last.has(f.id)).map((f) => f.id),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function renderStats(s) {
|
|
148
|
+
const pct = (x) => (x === null ? 'n/a' : `${Math.round(x * 100)}%`);
|
|
149
|
+
return [
|
|
150
|
+
`findings: ${s.findings} unique, ${s.repeats} repeat(s) caught`,
|
|
151
|
+
`outcomes: ${OUTCOMES.map((k) => `${k} ${s.outcomes[k]}`).join(' · ')} · open ${s.outcomes.open}`,
|
|
152
|
+
`false rate: ${pct(s.falseRate)} of closed findings`,
|
|
153
|
+
`rounds: ${s.tasks} task(s), mean ${s.meanRounds === null ? 'n/a' : s.meanRounds.toFixed(2)} round(s) per task`,
|
|
154
|
+
...(s.open.length ? [`still open: ${s.open.join(', ')} — close each with \`review ledger outcome <id> <${OUTCOMES.join('|')}>\``] : []),
|
|
155
|
+
].join('\n');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Escapes: reverts that reached the trunk. Each one should become a case the reviewer is scored
|
|
159
|
+
// against (the diff, where the bug was, what the review should have said). Only the conventional
|
|
160
|
+
// shapes are visible — `Revert "…"` from git, and a `revert:` commit type — and the output says so.
|
|
161
|
+
const REVERT = /^(?:\S+\s+)?(?:Revert "|revert(?:\([^)]*\))?!?:)/;
|
|
162
|
+
export function escapes(root, { since = '' } = {}) {
|
|
163
|
+
const range = since ? [`${since}..HEAD`] : [];
|
|
164
|
+
const r = spawnSync('git', ['-C', root, 'log', '--format=%H%x00%s', ...range], { encoding: 'utf8', windowsHide: true, maxBuffer: 64 * 1024 * 1024 });
|
|
165
|
+
if (r.status !== 0) return { ok: false, message: `rsc: cannot read git history${since ? ` since ${since}` : ''} (${(r.stderr || '').trim() || 'not a git repository'}).`, reverts: [] };
|
|
166
|
+
let cases = [];
|
|
167
|
+
try { cases = readdirSync(join(root, EVALS_DIR)).filter((f) => f.endsWith('.md')); } catch { cases = []; }
|
|
168
|
+
const reverts = r.stdout.split('\n').filter(Boolean).map((line) => {
|
|
169
|
+
const [sha, subject] = line.split('\0');
|
|
170
|
+
return { sha: sha.slice(0, 7), subject };
|
|
171
|
+
}).filter((c) => REVERT.test(c.subject)).map((c) => ({ ...c, covered: cases.some((f) => f.startsWith(c.sha)) }));
|
|
172
|
+
return { ok: true, reverts };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function renderEscapes(res) {
|
|
176
|
+
if (!res.reverts.length) return 'No reverts found (only `Revert "…"` subjects and `revert:` commits are recognised).';
|
|
177
|
+
const pending = res.reverts.filter((c) => !c.covered).length;
|
|
178
|
+
return [
|
|
179
|
+
...res.reverts.map((c) => `${c.covered ? 'case ' : 'NO CASE'} ${c.sha} ${c.subject}`),
|
|
180
|
+
'',
|
|
181
|
+
pending
|
|
182
|
+
? `${pending} revert(s) without a case. Write each as ${EVALS_DIR}/<sha>.md: the diff, where the bug was, what the review should have said.`
|
|
183
|
+
: 'Every revert has its case.',
|
|
184
|
+
].join('\n');
|
|
185
|
+
}
|