prreviewbuddy 0.50.0 → 0.52.2
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/CHANGELOG.md +97 -0
- package/dist/{delete_review-BIXqUnpx.js → delete_review-BiV1aBpx.js} +152 -42
- package/dist/main.js +7 -6
- package/dist/server.js +230 -104
- package/package.json +1 -1
- package/static/{chunk-BeL0x-wt.js → chunk-B5CvMZtt.js} +1 -1
- package/static/chunk-BeqgAIrQ.js +1 -1
- package/static/chunk-CofqJ-Qn.js +1 -0
- package/static/help.js +1 -1
- package/static/notifications.js +1 -1
- package/static/reviews.js +1 -1
- package/static/settings.js +1 -1
- package/static/shell.css +98 -56
- package/static/sidebar.css +250 -34
- package/static/workspace.js +59 -59
- package/static/chunk-vvGSmhmc.js +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,102 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.52.2
|
|
4
|
+
|
|
5
|
+
**Continue reviewing is gone from the Reviews page.** The banner opened whatever was first in your
|
|
6
|
+
review queue, which the Queue view already shows as its first card. The queue itself is unchanged:
|
|
7
|
+
the **Queue** filter lists your queued reviews in order, marking one Reviewed takes it out, and the
|
|
8
|
+
card menus and select mode still add and remove reviews.
|
|
9
|
+
|
|
10
|
+
## 0.52.1
|
|
11
|
+
|
|
12
|
+
**Fetching remote updates moved to where it applies.** A fetch only refreshes reviews that follow a
|
|
13
|
+
remote branch, such as `origin/feature`. Pull request reviews are checked automatically, and a fetch
|
|
14
|
+
cannot move a local branch. So the **Fetch updates** button is gone from the Reviews toolbar, where
|
|
15
|
+
it read as the way the list checks for updates and sat disabled for most people. Instead:
|
|
16
|
+
|
|
17
|
+
- A card that follows a remote branch has **Fetch remote updates** in its `...` menu.
|
|
18
|
+
- A review that follows one has **Fetch remote updates** in its `...` menu, beside Check for
|
|
19
|
+
updates. Check for updates still only reads what is on your machine. After the fetch the review
|
|
20
|
+
checks again, so the freshness banner reflects what came in.
|
|
21
|
+
- In select mode, **Fetch selected** appears only when the selection includes a review that can be
|
|
22
|
+
fetched.
|
|
23
|
+
|
|
24
|
+
What a fetch did, such as "Fetched 1 repository", now appears as a notice in the corner.
|
|
25
|
+
|
|
26
|
+
## 0.52.0
|
|
27
|
+
|
|
28
|
+
**Saved is now Pinned, and pinned reviews have their own section.** The Reviews page lists
|
|
29
|
+
**Pinned**, **Active**, **Waiting** and **Done** in that order, each under a heading with its count.
|
|
30
|
+
Pinning a review moves it into Pinned straight away, and unpinning moves it back. A pinned review
|
|
31
|
+
keeps its lifecycle: a pinned Waiting or Done review says so on its card, and a Waiting one that
|
|
32
|
+
changes says "Changed while waiting" there instead of jumping to Active. Pinning no longer changes
|
|
33
|
+
the order. Every section, Pinned included, is sorted by what last happened to each change.
|
|
34
|
+
|
|
35
|
+
The Status filter no longer has a Saved option, since the Pinned section does that job. A link to
|
|
36
|
+
the old Saved view opens the whole list. On a review's own page, the menu item is now **Pin review**
|
|
37
|
+
or **Unpin review**. Pinned reviews are still kept for ninety days.
|
|
38
|
+
|
|
39
|
+
Under the **Queue** filter the list stays in the order you queued it: a pinned review keeps its
|
|
40
|
+
place in the queue and shows its star. Select mode acts on reviews top to bottom as they are shown,
|
|
41
|
+
so **Add to review queue** queues them in that order. The star cannot be pressed again until the
|
|
42
|
+
last press has been saved, so quick clicks can no longer leave it showing the opposite of what was
|
|
43
|
+
stored.
|
|
44
|
+
|
|
45
|
+
## 0.51.2
|
|
46
|
+
|
|
47
|
+
**The review queue counts what it shows.** When a queued branch was analysed again, the queue kept
|
|
48
|
+
pointing at the older review. The Queue count included it, the Queue view had no card for it,
|
|
49
|
+
Continue reviewing opened the older review, and the card's menu could not take it out. A new review
|
|
50
|
+
of a queued branch now takes that branch's place in the queue. Opening the Reviews page repairs a
|
|
51
|
+
queue that already has older reviews in it, and a branch queued twice keeps its earlier place.
|
|
52
|
+
|
|
53
|
+
## 0.51.1
|
|
54
|
+
|
|
55
|
+
**The workspace fits a phone-width window.** Below about 600px the review's top bar ran off the
|
|
56
|
+
right edge and the whole page scrolled sideways; below about 450px the thin bar above it did too.
|
|
57
|
+
The review bar now wraps onto a second row (what the review is of, then its facts and controls),
|
|
58
|
+
and the thin bar keeps every control and the version by tightening its spacing and showing Send
|
|
59
|
+
feedback as an icon.
|
|
60
|
+
|
|
61
|
+
Buddy AI, opened in a narrow window, now covers exactly the review below the bars instead of
|
|
62
|
+
assuming how tall they are, so it no longer sits over a banner such as the freshness notice. The
|
|
63
|
+
review's `...` menu now opens over the Buddy AI tab rather than under it.
|
|
64
|
+
|
|
65
|
+
## 0.51.0
|
|
66
|
+
|
|
67
|
+
**KISS recommendations read in seconds.** Each card now leads with the point: a sentence or two on
|
|
68
|
+
what the change adds and why the requirements do not need it, then the simpler approach, a short
|
|
69
|
+
line of what acting on it gains, and where to start. The full reasoning (the evidence, why the
|
|
70
|
+
requirements do not call for it, the trade-offs and the other files involved) is one click away
|
|
71
|
+
under **Evidence & trade-offs**. A trade-off that could change your decision is shown on the card
|
|
72
|
+
itself.
|
|
73
|
+
|
|
74
|
+
Titles now say what to do rather than naming the idea, such as "Remove key readback until hardware
|
|
75
|
+
supports it" instead of "Defer device-reported key support".
|
|
76
|
+
|
|
77
|
+
A recommendation is shown only when it comes with a summary, a simpler approach, the evidence and
|
|
78
|
+
reasoning behind it, and a file that exists in the reviewed commit. One that falls short, including
|
|
79
|
+
one that points only at files that do not exist, is no longer shown as a card. The review says how many were held back, both in the workspace and
|
|
80
|
+
in the terminal. If every recommendation came back incomplete, the review says the analysis was
|
|
81
|
+
incomplete rather than calling the change appropriately simple.
|
|
82
|
+
|
|
83
|
+
KISS reviews you already have keep working, in the new layout.
|
|
84
|
+
|
|
85
|
+
## 0.50.1
|
|
86
|
+
|
|
87
|
+
**Copy and Ask Buddy look and behave as a pair.** On every issue and question, both icons are the
|
|
88
|
+
same grey at rest and share one soft highlight when you point at them. Copy no longer turns purple on
|
|
89
|
+
hover. Ask Buddy turns purple only while its menu is open. After you copy, the icon turns into a green
|
|
90
|
+
check for a moment and "Copied" appears just below it. It used to appear beside it, on top of Ask
|
|
91
|
+
Buddy. "Copy failed" shows in the same place.
|
|
92
|
+
|
|
93
|
+
The questions you can ask Buddy AI about a finding or a selection, and the wording that tells Buddy
|
|
94
|
+
which theme a question was asked in, now live in the shared review engine, so the browser extension
|
|
95
|
+
offers the same ones.
|
|
96
|
+
|
|
97
|
+
In a short window, the list of questions you can ask about a selection now scrolls instead of being
|
|
98
|
+
cut off at the bottom. Escape closes a menu again after the page has redrawn around it.
|
|
99
|
+
|
|
3
100
|
## 0.50.0
|
|
4
101
|
|
|
5
102
|
**Every review says which commits it covers.** Beside the branch at the top of a review is its
|
|
@@ -3187,6 +3187,20 @@ async function hasCommit(root, sha) {
|
|
|
3187
3187
|
}
|
|
3188
3188
|
}
|
|
3189
3189
|
//#endregion
|
|
3190
|
+
//#region ../../packages/review-harness/src/domain/kiss/outcome.ts
|
|
3191
|
+
/**
|
|
3192
|
+
* What a recommendation needs in order to be shown, said the same way wherever a withheld one is.
|
|
3193
|
+
*
|
|
3194
|
+
* The page and the terminal both explain why something was held back, and the rule behind it lives
|
|
3195
|
+
* in two places (`parse.ts` for the fields, `validate.ts` for the files). One sentence, so the
|
|
3196
|
+
* explanation cannot fall behind either of them.
|
|
3197
|
+
*/
|
|
3198
|
+
var WITHHELD_REASON = "missing a summary, its evidence and reasoning, a simpler approach, or a file that exists in the reviewed commit";
|
|
3199
|
+
function kissOutcome(result, audit) {
|
|
3200
|
+
if (result.recommendations.length > 0) return "recommendations";
|
|
3201
|
+
return (audit.withheldRecommendations ?? 0) > 0 ? "incomplete" : "proportionate";
|
|
3202
|
+
}
|
|
3203
|
+
//#endregion
|
|
3190
3204
|
//#region ../../packages/review-harness/src/workspace/authorship.ts
|
|
3191
3205
|
function describeAuthorship(changeSet, pullRequest) {
|
|
3192
3206
|
const author = pullRequest?.author;
|
|
@@ -4371,10 +4385,24 @@ function createWorkspace(input) {
|
|
|
4371
4385
|
...input
|
|
4372
4386
|
};
|
|
4373
4387
|
writeWorkspace(workspace);
|
|
4388
|
+
forwardQueueTo(workspace);
|
|
4374
4389
|
pruneOldWorkspaces();
|
|
4375
4390
|
return workspace;
|
|
4376
4391
|
}
|
|
4377
4392
|
/**
|
|
4393
|
+
* A new review of a queued branch takes the branch's place in the queue. See `forwardQueue`.
|
|
4394
|
+
*
|
|
4395
|
+
* Reads only the queued reviews, not the store: the queue is capped and the store is not. A failed
|
|
4396
|
+
* queue write is left alone, because the reviews index settles the same thing when it next loads.
|
|
4397
|
+
*/
|
|
4398
|
+
function forwardQueueTo(workspace) {
|
|
4399
|
+
updateQueue((current) => {
|
|
4400
|
+
const queued = current.map(loadWorkspace).filter((one) => one !== null);
|
|
4401
|
+
if (queued.length === 0) return null;
|
|
4402
|
+
return forwardQueue(current, groupByLineage([...queued, workspace].map(summarise)));
|
|
4403
|
+
});
|
|
4404
|
+
}
|
|
4405
|
+
/**
|
|
4378
4406
|
* Save a review that already exists, and nothing else.
|
|
4379
4407
|
*
|
|
4380
4408
|
* The existence check is what makes deleting a review stick. Every caller here is holding a
|
|
@@ -4733,6 +4761,24 @@ function groupByLineage(reviews) {
|
|
|
4733
4761
|
}).sort((a, b) => b.latest.createdAt - a.latest.createdAt);
|
|
4734
4762
|
}
|
|
4735
4763
|
/**
|
|
4764
|
+
* The queue with every entry naming the newest review of its branch, or null when it already does.
|
|
4765
|
+
*
|
|
4766
|
+
* The queue holds a branch, written down as one review's id, and a card takes its queue position
|
|
4767
|
+
* from its newest review only. An entry left on an older review is counted by the pill and drawn by
|
|
4768
|
+
* no card, and the card's Remove cannot reach it. So a superseded id
|
|
4769
|
+
* becomes the newest one, and a branch queued twice keeps the earlier of its places.
|
|
4770
|
+
*
|
|
4771
|
+
* An id no group knows is left where it is. Dropping deleted reviews is `compactQueue`'s job, and
|
|
4772
|
+
* the groups can be older than the file the result is applied to: a review queued since they were
|
|
4773
|
+
* read is not in them.
|
|
4774
|
+
*/
|
|
4775
|
+
function forwardQueue(queue, groups) {
|
|
4776
|
+
const newest = /* @__PURE__ */ new Map();
|
|
4777
|
+
for (const group of groups) for (const review of group.history) newest.set(review.id, group.latest.id);
|
|
4778
|
+
const forwarded = [...new Set(queue.map((id) => newest.get(id) ?? id))];
|
|
4779
|
+
return forwarded.length === queue.length && forwarded.every((id, at) => id === queue[at]) ? null : forwarded;
|
|
4780
|
+
}
|
|
4781
|
+
/**
|
|
4736
4782
|
* Where one review sits in its branch's history.
|
|
4737
4783
|
*
|
|
4738
4784
|
* Answered from the store rather than from anything written on the workspace, for the reason
|
|
@@ -4915,7 +4961,8 @@ function startKissRun(workspace, start) {
|
|
|
4915
4961
|
error: null,
|
|
4916
4962
|
audit: {
|
|
4917
4963
|
unknownPaths: [],
|
|
4918
|
-
unlocatedRecommendations: 0
|
|
4964
|
+
unlocatedRecommendations: 0,
|
|
4965
|
+
withheldRecommendations: 0
|
|
4919
4966
|
}
|
|
4920
4967
|
};
|
|
4921
4968
|
workspace.kissRuns = [...workspace.kissRuns ?? [], run];
|
|
@@ -7733,6 +7780,20 @@ function compactQueue(stored) {
|
|
|
7733
7780
|
return updateQueue((current) => livePart(current)) ?? stored;
|
|
7734
7781
|
}
|
|
7735
7782
|
/**
|
|
7783
|
+
* The queue with every entry on the newest review of its branch, stored when that changed it.
|
|
7784
|
+
*
|
|
7785
|
+
* `createWorkspace` keeps this true from the moment a branch is analysed again; this is for the
|
|
7786
|
+
* entries a store written before that already holds. Forwarded again under the lock rather than
|
|
7787
|
+
* written from `read`, so an id somebody queued since is kept. On a failed write the forwarded copy
|
|
7788
|
+
* still stands in, so the page counts what it can draw.
|
|
7789
|
+
*/
|
|
7790
|
+
function settleQueue(groups) {
|
|
7791
|
+
const read = readQueue();
|
|
7792
|
+
const forwarded = forwardQueue(read, groups);
|
|
7793
|
+
if (forwarded === null) return read;
|
|
7794
|
+
return updateQueue((current) => forwardQueue(current, groups)) ?? forwarded;
|
|
7795
|
+
}
|
|
7796
|
+
/**
|
|
7736
7797
|
* Put one review on the queue, the way the index's Add does, and say how that went.
|
|
7737
7798
|
*
|
|
7738
7799
|
* **A Reviewed review is queued like any other.** Reviewed says what the reviewer has done with it,
|
|
@@ -9344,6 +9405,17 @@ Emit a recommendation only when you can do all five of these. If you cannot, say
|
|
|
9344
9405
|
5. Say what acting on it is worth, and what it would cost.
|
|
9345
9406
|
Never emit a recommendation resting on naming preference, personal taste, or a requirement nobody has yet.`;
|
|
9346
9407
|
/**
|
|
9408
|
+
* The five checks above are how to reason, not how to write.
|
|
9409
|
+
*
|
|
9410
|
+
* Schema 1 gave each check its own output field, and the result was the reasoning printed five
|
|
9411
|
+
* times over: one argument restated under five headings, with the conclusion fourth. The checks stay
|
|
9412
|
+
* as the gate; this block asks for the conclusion first and keeps the checks as the audit trail.
|
|
9413
|
+
*/
|
|
9414
|
+
var DISTIL = `How to write a recommendation:
|
|
9415
|
+
Work through all five checks first. Then distil what you found. A reviewer should understand the recommendation from "title", "gist", "simplerDirection" and "gains" alone, in a few seconds, without reading anything else.
|
|
9416
|
+
"evidence", "whyNotJustified" and "payoffAndTradeoffs" are the audit trail for somebody who wants to check or challenge you. They carry the detail. They must not restate the gist. "evidence" and "whyNotJustified" are required: a recommendation without a basis somebody can check is withheld, however good its summary.
|
|
9417
|
+
Name each piece of machinery where it matters and do not repeat the same argument in every field. One argument, stated once, then supported.`;
|
|
9418
|
+
/**
|
|
9347
9419
|
* How to read the pull request conversation, for a lens that is not attributing anything to it.
|
|
9348
9420
|
*
|
|
9349
9421
|
* KISS needs the conversation for one reason and it is the load-bearing one: `whyNotJustified`
|
|
@@ -9382,6 +9454,9 @@ var RUBRIC = `What to look at:
|
|
|
9382
9454
|
- repeated wrappers or conversions that could collapse
|
|
9383
9455
|
- abstractions justified only by a hypothetical second implementation
|
|
9384
9456
|
|
|
9457
|
+
Fewer lines is evidence, not the objective. A twenty-line explicit implementation can be simpler than a five-line generic one. Judge simplicity by fewer concepts, fewer states, fewer sources of truth, fewer speculative abstractions, fewer indirections and less machinery needed to understand the behaviour.
|
|
9458
|
+
This is not a style review. Naming preferences, "extract this method", small tidy-ups and repetition that costs nobody any understanding are not recommendations.
|
|
9459
|
+
|
|
9385
9460
|
This is a list of things to look at, not a list of things to report. Every item here is sometimes exactly right. One implementation does not make an interface wrong: it may be a real external boundary, a testing seam, or a repository convention. More files do not make code worse. Judge what each thing is for and what it costs.`;
|
|
9386
9461
|
var WORTH_KEEPING = `Complexity worth keeping:
|
|
9387
9462
|
Look actively for complexity that is doing a job, and say so. This is not a politeness section — it is how the reader can tell you understood the change rather than counted its abstractions.
|
|
@@ -9415,14 +9490,16 @@ var KISS_OUTPUT_INSTRUCTIONS = `Respond with a single JSON object, no markdown a
|
|
|
9415
9490
|
"id": string,
|
|
9416
9491
|
"title": string,
|
|
9417
9492
|
"payoff": "high_value" | "worth_considering" | "nit",
|
|
9418
|
-
"
|
|
9493
|
+
"gist": string,
|
|
9494
|
+
"simplerDirection": string,
|
|
9495
|
+
"gains": [string],
|
|
9496
|
+
"reductionEstimate": string,
|
|
9497
|
+
"caveat": string,
|
|
9419
9498
|
"evidence": string,
|
|
9420
9499
|
"whyNotJustified": string,
|
|
9421
|
-
"simplerDirection": string,
|
|
9422
9500
|
"payoffAndTradeoffs": string,
|
|
9423
9501
|
"commentWorthiness": "strong" | "consider" | "leave",
|
|
9424
9502
|
"draftComment": string,
|
|
9425
|
-
"reductionEstimate": string,
|
|
9426
9503
|
"locations": [{ "path": string, "line": number, "primary": boolean }]
|
|
9427
9504
|
}],
|
|
9428
9505
|
"justifiedComplexity": [{
|
|
@@ -9439,17 +9516,19 @@ Field guidance:
|
|
|
9439
9516
|
- "summary": what drives that assessment and where the largest opportunities are, if there are any. A few sentences.
|
|
9440
9517
|
- "recommendations": ordered by what acting on them is worth — reduction in concepts and code, maintenance impact, your confidence, and what the change would cost to make. Never ordered by file, and never by line count alone. An empty list is a valid and complete answer; return one rather than finding something to say.
|
|
9441
9518
|
- "id": short, stable and unique within this response, e.g. "pdf-source-layers".
|
|
9442
|
-
- "title": the simplification, in a few words. "Collapse the four PDF source types into one".
|
|
9519
|
+
- "title": the simplification itself, in a few words, as the action where that is accurate. "Collapse the four PDF source types into one", "Use the existing retry helper instead of adding a second", "Remove the cache; the store already memoises", "Keep the formatter local until it has a second caller". Name what to do, not the architectural concept: "Remove key readback until a device supports it", not "Defer device-reported key support".
|
|
9443
9520
|
- "payoff": "high_value" for a substantial reduction in code, concepts, risk or maintenance with strong evidence. "worth_considering" for a credible simplification with meaningful but non-critical payoff, or a real trade-off. "nit" for small and cheap. Use "nit" sparingly: a report that is mostly nits buries the recommendation that mattered.
|
|
9444
|
-
- "
|
|
9445
|
-
- "
|
|
9446
|
-
- "
|
|
9447
|
-
- "
|
|
9448
|
-
- "
|
|
9521
|
+
- "gist": one or two sentences, at most 45 words: what this change adds and why the current requirements do not need it. The reader's first contact with the recommendation, so it must carry the point on its own. Name the machinery as a concept ("a dismissal flow", "a readback capability"), not as a list of identifiers: the identifiers belong in "simplerDirection", and listing them in both is the repetition this card exists to avoid.
|
|
9522
|
+
- "simplerDirection": the simpler implementation, concretely, in at most 50 words: what to remove, collapse or replace, naming the identifiers, and what to keep. "Simplify this" or "consider consolidating" is not a direction. Required: without one there is no recommendation.
|
|
9523
|
+
- "gains": one to four short phrases, at most six words each, naming what acting on it gains: "one less abstraction", "fewer states", "one source of truth", "no hypothetical capability". Every one must be supported by the evidence. No numbers here.
|
|
9524
|
+
- "reductionEstimate": optional and explicitly approximate, at most eight words, e.g. "~35–50 lines" or "four types become one". Omit it when you are not confident. It will be shown as an estimate and must never read as a measurement.
|
|
9525
|
+
- "caveat": optional, one sentence. Only a trade-off or uncertainty, shown by the evidence, that could change whether somebody acts on this. Never a hypothetical ("if readers later find…"). When there is none, omit the key entirely rather than writing "None"; routine trade-offs belong in "payoffAndTradeoffs".
|
|
9526
|
+
- "evidence": required. What in this change shows it, in detail. Point at the code, not at the idea.
|
|
9527
|
+
- "whyNotJustified": required. Why the current requirements and this repository do not call for it, in detail. This is the field that separates a recommendation from a preference.
|
|
9528
|
+
- "payoffAndTradeoffs": optional. What is gained and, honestly, what is lost or risked, in detail. If there is a good reason somebody might keep it as it is, say so here.
|
|
9449
9529
|
- "commentWorthiness": REVIEWER FRAMING ONLY — omit the field entirely on an author review. "strong" for worth raising directly, "consider" for worth mentioning, "leave" for a valid observation not worth a comment.
|
|
9450
9530
|
- "draftComment": REVIEWER FRAMING ONLY, and only where a comment is worth making. Omit it otherwise. A short, constructive pull request comment in the reviewer's voice.
|
|
9451
|
-
- "
|
|
9452
|
-
- "locations": where this complexity lives. Prefer the changed files responsible for it, named exactly as they appear in the diff. If an unchanged repository file is where the complexity actually sits and no changed file represents it, cite that file too, but never describe an unchanged file as part of this change. "line" is optional. Set "primary": true on exactly one location — the one place a reader should open first, which should be a changed file wherever one represents the concern — and false on the rest.
|
|
9531
|
+
- "locations": required, at least one. Where this complexity lives. Prefer the changed files responsible for it, named exactly as they appear in the diff. If an unchanged repository file is where the complexity actually sits and no changed file represents it, cite that file too, but never describe an unchanged file as part of this change. "line" is optional. Set "primary": true on exactly one location — the one place a reader should open first, which should be a changed file wherever one represents the concern — and false on the rest.
|
|
9453
9532
|
- "justifiedComplexity": complexity in this change that is doing a real job. Empty list when there is nothing worth the note.
|
|
9454
9533
|
- "aggregateEstimate": optional, one phrase, across the whole change. Omit it unless it is defensible.`;
|
|
9455
9534
|
function buildKissPrompt(pr, options) {
|
|
@@ -9475,6 +9554,8 @@ ${SCOPE_BOUNDARY}
|
|
|
9475
9554
|
|
|
9476
9555
|
${EVIDENCE_THRESHOLD}
|
|
9477
9556
|
|
|
9557
|
+
${DISTIL}
|
|
9558
|
+
|
|
9478
9559
|
---
|
|
9479
9560
|
|
|
9480
9561
|
${RUBRIC}
|
|
@@ -9731,7 +9812,7 @@ var WORTHINESSES = [
|
|
|
9731
9812
|
"consider",
|
|
9732
9813
|
"leave"
|
|
9733
9814
|
];
|
|
9734
|
-
function
|
|
9815
|
+
function parseKissAnswer(raw) {
|
|
9735
9816
|
const jsonText = extractJsonText(raw);
|
|
9736
9817
|
let parsed;
|
|
9737
9818
|
try {
|
|
@@ -9744,13 +9825,17 @@ function parseKissResult(raw) {
|
|
|
9744
9825
|
const assessment = asString(obj.assessment);
|
|
9745
9826
|
const summary = asString(obj.summary);
|
|
9746
9827
|
if (!assessment && !summary && !Array.isArray(obj.recommendations) && !Array.isArray(obj.justifiedComplexity)) throw new Error("The agent returned valid JSON, but not in the shape a KISS review takes (missing assessment/summary/recommendations/justifiedComplexity). This usually means the model ignored the requested schema.");
|
|
9828
|
+
const { kept, withheld } = normalizeRecommendations(obj.recommendations);
|
|
9747
9829
|
return {
|
|
9748
|
-
|
|
9749
|
-
|
|
9750
|
-
|
|
9751
|
-
|
|
9752
|
-
|
|
9753
|
-
|
|
9830
|
+
result: {
|
|
9831
|
+
schemaVersion: 2,
|
|
9832
|
+
assessment,
|
|
9833
|
+
summary,
|
|
9834
|
+
recommendations: kept,
|
|
9835
|
+
justifiedComplexity: normalizeJustified(obj.justifiedComplexity),
|
|
9836
|
+
...optional("aggregateEstimate", asString(obj.aggregateEstimate))
|
|
9837
|
+
},
|
|
9838
|
+
withheldRecommendations: withheld
|
|
9754
9839
|
};
|
|
9755
9840
|
}
|
|
9756
9841
|
/** Absent rather than empty, so "the model said nothing here" has one representation. */
|
|
@@ -9758,36 +9843,53 @@ function optional(key, value) {
|
|
|
9758
9843
|
return value ? { [key]: value } : {};
|
|
9759
9844
|
}
|
|
9760
9845
|
/**
|
|
9761
|
-
* A recommendation
|
|
9846
|
+
* A recommendation is kept only when it can stand on the card by itself.
|
|
9762
9847
|
*
|
|
9763
|
-
*
|
|
9764
|
-
*
|
|
9765
|
-
*
|
|
9766
|
-
*
|
|
9848
|
+
* That takes a title, a gist, a simpler direction and somewhere to open, which is what the card
|
|
9849
|
+
* shows without expanding anything, and the evidence and requirements reasoning behind it, which is
|
|
9850
|
+
* what makes it auditable. A polished summary with no basis a reader can check is a claim rather
|
|
9851
|
+
* than a recommendation. The trade-offs stay optional: some recommendations genuinely cost nothing. What fails is withheld and counted,
|
|
9852
|
+
* never padded or truncated, so the page can say that something came back incomplete.
|
|
9767
9853
|
*/
|
|
9768
9854
|
function normalizeRecommendations(value) {
|
|
9769
|
-
if (!Array.isArray(value)) return
|
|
9770
|
-
|
|
9771
|
-
|
|
9772
|
-
|
|
9855
|
+
if (!Array.isArray(value)) return {
|
|
9856
|
+
kept: [],
|
|
9857
|
+
withheld: 0
|
|
9858
|
+
};
|
|
9859
|
+
const entries = value.filter((item) => typeof item === "object" && item !== null);
|
|
9860
|
+
const kept = entries.map((item, index) => {
|
|
9773
9861
|
const worthiness = WORTHINESSES.includes(item.commentWorthiness) ? item.commentWorthiness : void 0;
|
|
9774
|
-
const
|
|
9775
|
-
const reductionEstimate = asString(item.reductionEstimate);
|
|
9862
|
+
const gains = asStringArray(item.gains).map((gain) => gain.trim()).filter((gain) => gain !== "");
|
|
9776
9863
|
return {
|
|
9777
9864
|
id: asString(item.id) || `kiss-${index + 1}`,
|
|
9778
|
-
title,
|
|
9865
|
+
title: asString(item.title),
|
|
9779
9866
|
payoff: PAYOFFS.includes(item.payoff) ? item.payoff : "worth_considering",
|
|
9780
|
-
|
|
9867
|
+
gist: asString(item.gist),
|
|
9781
9868
|
evidence: asString(item.evidence),
|
|
9782
9869
|
whyNotJustified: asString(item.whyNotJustified),
|
|
9783
|
-
simplerDirection,
|
|
9870
|
+
simplerDirection: asString(item.simplerDirection),
|
|
9871
|
+
...gains.length > 0 ? { gains } : {},
|
|
9872
|
+
...optional("caveat", unlessPlaceholder(asString(item.caveat))),
|
|
9784
9873
|
payoffAndTradeoffs: asString(item.payoffAndTradeoffs),
|
|
9785
9874
|
...worthiness ? { commentWorthiness: worthiness } : {},
|
|
9786
|
-
...draftComment
|
|
9787
|
-
...reductionEstimate
|
|
9875
|
+
...optional("draftComment", asString(item.draftComment)),
|
|
9876
|
+
...optional("reductionEstimate", asString(item.reductionEstimate)),
|
|
9788
9877
|
locations: normalizeLocations(item.locations)
|
|
9789
9878
|
};
|
|
9790
|
-
}).filter((rec) => rec.title !== ""
|
|
9879
|
+
}).filter((rec) => rec.title !== "" && rec.gist !== "" && rec.simplerDirection !== "" && rec.evidence !== "" && rec.whyNotJustified !== "" && rec.locations.length > 0);
|
|
9880
|
+
return {
|
|
9881
|
+
kept,
|
|
9882
|
+
withheld: entries.length - kept.length
|
|
9883
|
+
};
|
|
9884
|
+
}
|
|
9885
|
+
/**
|
|
9886
|
+
* An optional sentence the model filled with a word for "nothing".
|
|
9887
|
+
*
|
|
9888
|
+
* Seen on real runs: asked to omit the caveat when there is none, the model wrote "None", which the
|
|
9889
|
+
* card would show as a trade-off called None. Only a bare placeholder is caught, never a sentence.
|
|
9890
|
+
*/
|
|
9891
|
+
function unlessPlaceholder(value) {
|
|
9892
|
+
return /^(none|n\/a|null|nil|-+|no caveat|not applicable)\.?$/i.test(value.trim()) ? "" : value;
|
|
9791
9893
|
}
|
|
9792
9894
|
function normalizeJustified(value) {
|
|
9793
9895
|
if (!Array.isArray(value)) return [];
|
|
@@ -9860,8 +9962,9 @@ var KissRejected = class extends Error {
|
|
|
9860
9962
|
};
|
|
9861
9963
|
async function validateKiss(raw, changedPaths, existingPaths) {
|
|
9862
9964
|
let result;
|
|
9965
|
+
let withheldRecommendations;
|
|
9863
9966
|
try {
|
|
9864
|
-
result =
|
|
9967
|
+
({result, withheldRecommendations} = parseKissAnswer(raw));
|
|
9865
9968
|
} catch (error) {
|
|
9866
9969
|
throw new KissRejected(error instanceof Error ? error.message : String(error), raw);
|
|
9867
9970
|
}
|
|
@@ -9896,13 +9999,20 @@ async function validateKiss(raw, changedPaths, existingPaths) {
|
|
|
9896
9999
|
}
|
|
9897
10000
|
return surviving;
|
|
9898
10001
|
};
|
|
9899
|
-
|
|
10002
|
+
let withheldForLocations = 0;
|
|
10003
|
+
result.recommendations = result.recommendations.filter((recommendation) => {
|
|
10004
|
+
recommendation.locations = keep(recommendation.locations);
|
|
10005
|
+
if (recommendation.locations.length > 0) return true;
|
|
10006
|
+
withheldForLocations++;
|
|
10007
|
+
return false;
|
|
10008
|
+
});
|
|
9900
10009
|
for (const entry of result.justifiedComplexity) entry.locations = keep(entry.locations);
|
|
9901
10010
|
return {
|
|
9902
10011
|
result,
|
|
9903
10012
|
audit: {
|
|
9904
10013
|
unknownPaths: [...unknownPaths].sort(),
|
|
9905
|
-
unlocatedRecommendations:
|
|
10014
|
+
unlocatedRecommendations: 0,
|
|
10015
|
+
withheldRecommendations: withheldRecommendations + withheldForLocations
|
|
9906
10016
|
}
|
|
9907
10017
|
};
|
|
9908
10018
|
}
|
|
@@ -10743,7 +10853,7 @@ async function startJob(input) {
|
|
|
10743
10853
|
jobId: job.id,
|
|
10744
10854
|
audience: review.audience,
|
|
10745
10855
|
reviewedSha: target.sha,
|
|
10746
|
-
promptVersion: "
|
|
10856
|
+
promptVersion: "2",
|
|
10747
10857
|
agentId: agent.id,
|
|
10748
10858
|
...input.requestedModel ? { requestedModel: input.requestedModel } : {}
|
|
10749
10859
|
});
|
|
@@ -11348,7 +11458,7 @@ async function recordKissRun(input) {
|
|
|
11348
11458
|
jobId: job.id,
|
|
11349
11459
|
audience: input.audience,
|
|
11350
11460
|
reviewedSha: sha,
|
|
11351
|
-
promptVersion: "
|
|
11461
|
+
promptVersion: "2",
|
|
11352
11462
|
agentId: agent.id,
|
|
11353
11463
|
...input.requestedModel ? { requestedModel: input.requestedModel } : {}
|
|
11354
11464
|
});
|
|
@@ -12881,4 +12991,4 @@ function lineageIds(id) {
|
|
|
12881
12991
|
return [.../* @__PURE__ */ new Set([id, ...lineage.map((review) => review.id)])];
|
|
12882
12992
|
}
|
|
12883
12993
|
//#endregion
|
|
12884
|
-
export { compactQueue as $,
|
|
12994
|
+
export { compactQueue as $, loadWorkspace as $t, createPrState as A, reviewerDispositions as An, followedBranch as At, toneOf as B, resolveModel as Bn, phasesFor as Bt, readEvents as C, queueStamp as Cn, claimRequestUrl as Ct, announceJob as D, isQuestionOutstanding as Dn, fillFileUrlTemplate as Dt, telemetryUploadConsent as E, isIssueOutstanding as En, remoteHost as Et, markAllRead as F, resolveTarget as Fn, isTerminal as Ft, extractJsonText as G, STORE_ROOT as Gn, isClaimed as Gt, withUsageRecorded as H, readAgentPreference as Hn, purposeOf as Ht, markRead as I, displayRef as In, loadJob as It, readMarker as J, agentById as Jn, indexKeptIds as Jt, ReviewBeingDeletedError as K, AGENT_IDS as Kn, followedRefName as Kt, pruneNotifications as L, isCommitReview as Ln, saveJob as Lt, eventName as M, describeAuthorship as Mn, allJobs as Mt, headline as N, WITHHELD_REASON as Nn, endedAt as Nt, branchOf as O, issuesOutstanding as On, checkCodeFreshness as Ot, listNotifications as P, kissOutcome as Pn, fail as Pt, activateQueued as Q, AgentUnavailableError as Qn, lineagePosition as Qt, readNotification as R, git as Rn, PHASES as Rt, startJob as S, queueRemove as Sn, writePortPreference as St, recordTelemetryUploadConsent as T, doneVerb as Tn, forgeFor as Tt, EXPLAIN_SIMPLY_PROMPT as U, writeAgentPreference as Un, clearClaim as Ut, unreadCount as V, clearAgentPreference as Vn, progressSteps as Vt, asString as W, CONFIG_PATH as Wn, driverGone as Wt, modelHelpLines as X, detectAgents as Xn, latestKissRun as Xt, removeWorktree as Y, agentFor as Yn, indexReviewGroups as Yt, relativeTime as Z, AgentCancelledError as Zn, lineageKeyFor as Zt, agentEnvOf as _, offersLifecycle as _n, MAX_WORKSPACE_PORT as _t, wasBlocked as a, reviewedCommit as an, ensureServer as at, reanalyseReview as b, queueAdd as bn, resolveWorkspacePort as bt, RefreshUnavailableError as c, touchWorkspace as cn, serverEntryExists as ct, refreshPrContext as d, LIFECYCLES as dn, workspaceUrl as dt, matchingWorkspaceIds as en, queueForReview as et, isUnchanged as f, activityLabel as fn, writeServerRecord as ft, runningJobs as g, lifecyclePill as gn, DEFAULT_WORKSPACE_PORT as gt, liveUpdateFor as h, lifecycleChip as hn, feedbackUrl as ht, liveJobsFor as i, recentWorkspaces as in, clearServerRecord as it, createPrUpdate as j, processDiscussion as jn, reviewedShaOf as jt, subjectOf as k, questionsOutstanding as kn, checkReview as kt, mergeUpdateInto as l, workspaceRevision as ln, spawnServer as lt, UpdateAlreadyRunningError as m, lifecycleAction as mn, PACKAGE_NAME as mt, deleteReview as n, positionInLineage as nn, readIndexToken as nt, discardJob as o, reviewedRepositories as on, readServerRecord as ot, askCheckout as p, isLifecycle as pn, BUILD_VERSION as pt, MANAGED_ROOT as q, DEFAULT_AGENT_ID as qn, groupByLineage as qt, lineageIds as r, previousKissResult as rn, bootstrapUrl as rt, FETCH_TIMEOUT_MS as s, summarise as sn, reviewsUrl as st, deleteLineage as t, mutateWorkspace as tn, settleQueue as tt, updateReview as u, workspaceStamps as un, stopServer as ut, recordKissRun as v, parseLifecycle as vn, MIN_WORKSPACE_PORT as vt, record as w, readQueue as wn, forgeResolver as wt, runJob as x, queueAddMany as xn, statedWorkspacePort as xt, startKissJob as y, sectionOf as yn, parseWorkspacePort as yt, removeNotification as z, resolveAgent as zn, isLiveJob as zt };
|
package/dist/main.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $t as
|
|
2
|
+
import { $t as loadWorkspace, Bn as resolveModel, E as telemetryUploadConsent, Fn as resolveTarget, Ft as isTerminal, Gn as STORE_ROOT, Gt as isClaimed, Hn as readAgentPreference, Ht as purposeOf, In as displayRef, It as loadJob, J as readMarker, Jn as agentById, Kn as AGENT_IDS, Kt as followedRefName, Mt as allJobs, Nn as WITHHELD_REASON, Pn as kissOutcome, Rn as git, Rt as PHASES, S as startJob, St as writePortPreference, T as recordTelemetryUploadConsent, Un as writeAgentPreference, Ut as clearClaim, Vt as progressSteps, Wn as CONFIG_PATH, Wt as driverGone, X as modelHelpLines, Xn as detectAgents, Y as removeWorktree, Z as relativeTime, Zt as lineageKeyFor, _t as MAX_WORKSPACE_PORT, a as wasBlocked, at as ensureServer, b as reanalyseReview, dt as workspaceUrl, en as matchingWorkspaceIds, et as queueForReview, g as runningJobs, gt as DEFAULT_WORKSPACE_PORT, h as liveUpdateFor, i as liveJobsFor, in as recentWorkspaces, kt as checkReview, l as mergeUpdateInto, m as UpdateAlreadyRunningError, mt as PACKAGE_NAME, n as deleteReview$1, nn as positionInLineage, nt as readIndexToken, on as reviewedRepositories, pt as BUILD_VERSION, q as MANAGED_ROOT, qn as DEFAULT_AGENT_ID, qt as groupByLineage, rt as bootstrapUrl, sn as summarise, st as reviewsUrl, tn as mutateWorkspace, u as updateReview, ut as stopServer, v as recordKissRun, vt as MIN_WORKSPACE_PORT, w as record$1, wt as forgeResolver, x as runJob, xt as statedWorkspacePort, yt as parseWorkspacePort, zn as resolveAgent } from "./delete_review-BiV1aBpx.js";
|
|
3
3
|
import { basename, dirname, join, resolve } from "node:path";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
import { existsSync, mkdirSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
@@ -1386,8 +1386,8 @@ function reviewFinished(url, summary, conversation) {
|
|
|
1386
1386
|
* worth saying plainly: somebody who ran this wants to know the change is proportionate, and a
|
|
1387
1387
|
* bare zero leaves them wondering whether the analysis ran.
|
|
1388
1388
|
*/
|
|
1389
|
-
function kissFinished(url, recommendations) {
|
|
1390
|
-
return `\nKISS review ready · ${
|
|
1389
|
+
function kissFinished(url, outcome, recommendations, withheld = 0) {
|
|
1390
|
+
return `\nKISS review ready · ${outcome === "proportionate" ? "No material simplifications were identified." : outcome === "incomplete" ? `${count(withheld, "recommendation")} came back incomplete and ${withheld === 1 ? "was" : "were"} withheld (${WITHHELD_REASON}). This is not a finding that the change is simple; run it again.` : `${count(recommendations, "recommendation")}${withheld > 0 ? ` (${withheld} more withheld as incomplete)` : ""}`}\n${url}`;
|
|
1391
1391
|
}
|
|
1392
1392
|
function cap(word) {
|
|
1393
1393
|
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
@@ -2746,12 +2746,13 @@ async function drive(workspaceId, url, kissRun) {
|
|
|
2746
2746
|
}
|
|
2747
2747
|
const done = loadWorkspace(workspaceId);
|
|
2748
2748
|
if (kissRun) {
|
|
2749
|
-
const
|
|
2750
|
-
|
|
2749
|
+
const finished = (done.kissRuns ?? []).find((candidate) => candidate.jobId === kissRun.jobId);
|
|
2750
|
+
const answer = finished?.result;
|
|
2751
|
+
if (!finished || !answer) {
|
|
2751
2752
|
err(reviewFailed("The analysis finished without recording an answer.", link));
|
|
2752
2753
|
return 1;
|
|
2753
2754
|
}
|
|
2754
|
-
out(kissFinished(link, answer.recommendations.length));
|
|
2755
|
+
out(kissFinished(link, kissOutcome(answer, finished.audit), answer.recommendations.length, finished.audit.withheldRecommendations ?? 0));
|
|
2755
2756
|
return 0;
|
|
2756
2757
|
}
|
|
2757
2758
|
out(reviewFinished(link, summarise(done), settled.job.conversation));
|