@ericrisco/rsc 3.7.5 → 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 +46 -39
- 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/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",
|
|
@@ -844,11 +865,12 @@
|
|
|
844
865
|
},
|
|
845
866
|
{
|
|
846
867
|
"id": "constitution",
|
|
847
|
-
"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`).",
|
|
848
869
|
"tags": [
|
|
849
|
-
"
|
|
870
|
+
"asdd",
|
|
850
871
|
"constitution",
|
|
851
|
-
"principles"
|
|
872
|
+
"principles",
|
|
873
|
+
"review"
|
|
852
874
|
],
|
|
853
875
|
"recommends": [
|
|
854
876
|
"specify"
|
|
@@ -1736,7 +1758,6 @@
|
|
|
1736
1758
|
"epistemics"
|
|
1737
1759
|
],
|
|
1738
1760
|
"recommends": [
|
|
1739
|
-
"sdd",
|
|
1740
1761
|
"verify",
|
|
1741
1762
|
"debug",
|
|
1742
1763
|
"review"
|
|
@@ -1929,7 +1950,7 @@
|
|
|
1929
1950
|
},
|
|
1930
1951
|
{
|
|
1931
1952
|
"id": "ftd",
|
|
1932
|
-
"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.",
|
|
1933
1954
|
"tags": [
|
|
1934
1955
|
"ftd",
|
|
1935
1956
|
"lane",
|
|
@@ -2431,14 +2452,16 @@
|
|
|
2431
2452
|
},
|
|
2432
2453
|
{
|
|
2433
2454
|
"id": "implement",
|
|
2434
|
-
"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`).",
|
|
2435
2456
|
"tags": [
|
|
2457
|
+
"asdd",
|
|
2436
2458
|
"sdd",
|
|
2437
2459
|
"implement",
|
|
2438
2460
|
"code"
|
|
2439
2461
|
],
|
|
2440
2462
|
"recommends": [
|
|
2441
|
-
"verify"
|
|
2463
|
+
"verify",
|
|
2464
|
+
"review"
|
|
2442
2465
|
],
|
|
2443
2466
|
"profiles": [
|
|
2444
2467
|
"core",
|
|
@@ -4134,11 +4157,12 @@
|
|
|
4134
4157
|
},
|
|
4135
4158
|
{
|
|
4136
4159
|
"id": "review",
|
|
4137
|
-
"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`).",
|
|
4138
4161
|
"tags": [
|
|
4139
|
-
"
|
|
4162
|
+
"asdd",
|
|
4140
4163
|
"review",
|
|
4141
|
-
"code-review"
|
|
4164
|
+
"code-review",
|
|
4165
|
+
"risk"
|
|
4142
4166
|
],
|
|
4143
4167
|
"recommends": [
|
|
4144
4168
|
"ship"
|
|
@@ -4341,28 +4365,9 @@
|
|
|
4341
4365
|
"full"
|
|
4342
4366
|
]
|
|
4343
4367
|
},
|
|
4344
|
-
{
|
|
4345
|
-
"id": "sdd",
|
|
4346
|
-
"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.",
|
|
4347
|
-
"tags": [
|
|
4348
|
-
"sdd",
|
|
4349
|
-
"spec",
|
|
4350
|
-
"workflow",
|
|
4351
|
-
"plan"
|
|
4352
|
-
],
|
|
4353
|
-
"recommends": [
|
|
4354
|
-
"sdd-init",
|
|
4355
|
-
"constitution",
|
|
4356
|
-
"specify"
|
|
4357
|
-
],
|
|
4358
|
-
"profiles": [
|
|
4359
|
-
"core",
|
|
4360
|
-
"full"
|
|
4361
|
-
]
|
|
4362
|
-
},
|
|
4363
4368
|
{
|
|
4364
4369
|
"id": "sdd-init",
|
|
4365
|
-
"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`).",
|
|
4366
4371
|
"tags": [
|
|
4367
4372
|
"sdd",
|
|
4368
4373
|
"init",
|
|
@@ -4371,7 +4376,6 @@
|
|
|
4371
4376
|
"registry"
|
|
4372
4377
|
],
|
|
4373
4378
|
"recommends": [
|
|
4374
|
-
"sdd",
|
|
4375
4379
|
"specify",
|
|
4376
4380
|
"implement",
|
|
4377
4381
|
"verify"
|
|
@@ -4439,12 +4443,13 @@
|
|
|
4439
4443
|
},
|
|
4440
4444
|
{
|
|
4441
4445
|
"id": "ship",
|
|
4442
|
-
"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`).",
|
|
4443
4447
|
"tags": [
|
|
4444
|
-
"
|
|
4448
|
+
"asdd",
|
|
4445
4449
|
"ship",
|
|
4446
4450
|
"release",
|
|
4447
|
-
"pr"
|
|
4451
|
+
"pr",
|
|
4452
|
+
"risk"
|
|
4448
4453
|
],
|
|
4449
4454
|
"recommends": [],
|
|
4450
4455
|
"profiles": [
|
|
@@ -4691,7 +4696,7 @@
|
|
|
4691
4696
|
},
|
|
4692
4697
|
{
|
|
4693
4698
|
"id": "specify",
|
|
4694
|
-
"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`).",
|
|
4695
4700
|
"tags": [
|
|
4696
4701
|
"sdd",
|
|
4697
4702
|
"spec",
|
|
@@ -4913,8 +4918,9 @@
|
|
|
4913
4918
|
},
|
|
4914
4919
|
{
|
|
4915
4920
|
"id": "tasks",
|
|
4916
|
-
"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`).",
|
|
4917
4922
|
"tags": [
|
|
4923
|
+
"asdd",
|
|
4918
4924
|
"sdd",
|
|
4919
4925
|
"tasks",
|
|
4920
4926
|
"breakdown"
|
|
@@ -5411,8 +5417,9 @@
|
|
|
5411
5417
|
},
|
|
5412
5418
|
{
|
|
5413
5419
|
"id": "verify",
|
|
5414
|
-
"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`).",
|
|
5415
5421
|
"tags": [
|
|
5422
|
+
"asdd",
|
|
5416
5423
|
"sdd",
|
|
5417
5424
|
"verify",
|
|
5418
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
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// `rsc risk` — how much review and whose approval a change needs, decided by WHAT it touches.
|
|
2
|
+
//
|
|
3
|
+
// ASDD reviews inside every task, so the depth of that review cannot be the agent's own call: the
|
|
4
|
+
// change sets the tier, never the author (Anthropic, "How the Claude Code team does code review").
|
|
5
|
+
// The table is the sello's (targets/sello.mjs), reused as is, and it works with the sello off.
|
|
6
|
+
// Spec: 02-DOCS/wiki/sdd/specs/asdd-v4.md.
|
|
7
|
+
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import { SELLO_STATE_PATHS, classifyRisk, lensesRequired, readEffectiveConfig, trunkRef } from '../../targets/sello.mjs';
|
|
10
|
+
|
|
11
|
+
function git(root, args) {
|
|
12
|
+
const r = spawnSync('git', ['-C', root, '-c', 'core.quotePath=false', ...args], { encoding: 'utf8', windowsHide: true, maxBuffer: 64 * 1024 * 1024 });
|
|
13
|
+
return r.status === 0 ? r.stdout : null;
|
|
14
|
+
}
|
|
15
|
+
const nul = (out) => (out || '').split('\0').filter(Boolean);
|
|
16
|
+
|
|
17
|
+
// Every path that differs from the base (committed, staged, unstaged or untracked). The base is the
|
|
18
|
+
// merge-base with `--base`, or with the trunk when none is given.
|
|
19
|
+
export function changedPaths(root, base = null) {
|
|
20
|
+
const cfg = readEffectiveConfig(root);
|
|
21
|
+
const ref = base || trunkRef(root, cfg);
|
|
22
|
+
if (!ref) return { ok: false, message: 'rsc: no base to compare against. Pass --base <ref> or the paths themselves: npx @ericrisco/rsc risk <path…>' };
|
|
23
|
+
const head = git(root, ['rev-parse', 'HEAD']);
|
|
24
|
+
const mb = head ? (git(root, ['merge-base', 'HEAD', ref]) || '').trim() : '';
|
|
25
|
+
if (!mb) return { ok: false, message: `rsc: cannot find a common ancestor with ${ref}. Pass --base <ref> or the paths themselves: npx @ericrisco/rsc risk <path…>` };
|
|
26
|
+
// --no-renames: a rename reports both paths, so moving a file OUT of a tier-2 place still counts.
|
|
27
|
+
const diff = (from) => new Set([...nul(git(root, ['diff', '--name-only', '--no-renames', '-z', from])), ...nul(git(root, ['ls-files', '--others', '--exclude-standard', '-z']))]);
|
|
28
|
+
let names = diff(mb);
|
|
29
|
+
let shown = ref;
|
|
30
|
+
// Working straight on the trunk (FTD on an open main), the merge-base IS HEAD, and a change
|
|
31
|
+
// already committed would read as no change at all: tier 0, no reviewer, no escalation. With
|
|
32
|
+
// nothing uncommitted, the last commit is the change being asked about.
|
|
33
|
+
if (!base && mb === head.trim() && !names.size && git(root, ['rev-parse', '--verify', '--quiet', 'HEAD~1']) !== null) {
|
|
34
|
+
names = diff('HEAD~1');
|
|
35
|
+
shown = 'HEAD~1 (on the trunk: the last commit)';
|
|
36
|
+
}
|
|
37
|
+
for (const p of SELLO_STATE_PATHS) names.delete(p);
|
|
38
|
+
return { ok: true, base: shown, paths: [...names].sort() };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function assess(paths, cfg = null) {
|
|
42
|
+
const { tier, classes, configError } = classifyRisk(paths, cfg);
|
|
43
|
+
return {
|
|
44
|
+
tier,
|
|
45
|
+
classes: [...new Set(Object.values(classes))].sort(),
|
|
46
|
+
reviewers: lensesRequired(tier, cfg),
|
|
47
|
+
humanApproval: tier >= 2,
|
|
48
|
+
paths: classes,
|
|
49
|
+
...(configError ? { configError } : {}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function render(r) {
|
|
54
|
+
const top = Object.entries(r.paths).filter(([, c]) => r.tier === 0 || c !== 'docs');
|
|
55
|
+
return [
|
|
56
|
+
`tier ${r.tier}`,
|
|
57
|
+
`classes: ${r.classes.join(', ') || 'none'}`,
|
|
58
|
+
`reviewers: ${r.reviewers}`,
|
|
59
|
+
`human approval: ${r.humanApproval ? 'required' : 'not required'}`,
|
|
60
|
+
...(r.base ? [`base: ${r.base}`] : []),
|
|
61
|
+
...(r.configError ? [`config ignored: ${r.configError}`] : []),
|
|
62
|
+
...top.slice(0, 20).map(([p, c]) => ` ${c.padEnd(10)} ${p}`),
|
|
63
|
+
...(top.length > 20 ? [` … ${top.length - 20} more`] : []),
|
|
64
|
+
].join('\n');
|
|
65
|
+
}
|