prreviewbuddy 0.85.0 → 0.86.6
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 +82 -0
- package/dist/{delete_review-BED4mPxB.js → delete_review-CwKZQlMg.js} +306 -42
- package/dist/main.js +1 -1
- package/dist/server.js +986 -101
- package/package.json +1 -1
- package/static/chunk-BUD1RoN4.js +1 -0
- package/static/chunk-D9x9bcKf.js +1 -0
- package/static/{chunk-BL6xJoZm.js → chunk-Di41wu5k.js} +1 -1
- package/static/chunk-huqt-B16.js +1 -0
- package/static/help.js +1 -1
- package/static/notifications.js +1 -1
- package/static/reviews.js +1 -1
- package/static/runs.js +1 -1
- package/static/settings.js +1 -1
- package/static/shell.css +130 -28
- package/static/sidebar.css +15 -0
- package/static/workspace.js +68 -61
- package/static/chunk-B2vcrGTe.js +0 -1
- package/static/chunk-DWTt0jUK.js +0 -1
- package/static/chunk-DwdwtB9r.js +0 -1
package/dist/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as MIN_WORKSPACE_PORT, $i as
|
|
1
|
+
import { $ as MIN_WORKSPACE_PORT, $i as AmbiguousRefError, $n as RUN_RECORD_RETENTION_MS, $r as deleteCollection, $t as markRead, A as batchIdentity, Ai as findingAssessment, An as escapeHtml$1, At as loadArchitecture, Bi as wasReassessed, Bn as readMarker, Br as LIFECYCLES, Bt as ancestorsOf, C as RUN_KINDS, Ci as withExplanation, Cn as UNESTABLISHED_MOTIVATION, Cr as kissDecisionsOf, Ct as mapRuntimes, D as reportRun, Di as assessmentLookup, Dn as renderMarkdown, Dr as loadWorkspace, Dt as formatDuration, E as endRun, Ei as analysisRefOf, En as GITHUB_MARKDOWN, Er as lineagePosition, Et as describeRuntimes, F as activateQueued, Fi as isOffered, Fn as withReviewLock, Ft as storedScopes, G as serverEntryExists, Gi as describeAuthorship, Gn as findExistingReview, Gr as lifecycleItemLabel, Gt as subjectOf, Hi as asString, Hn as requestJobStop, Hr as activityLabel, Ht as writeJsonAtomically, I as compactQueue, Ii as promptGrade, In as MANAGED_ROOT, Ir as summarise, It as trackedFiles, Ji as resolveTarget, Jn as followedBranch, Jr as parseLifecycle, Jt as createPrState, K as spawnServer, Ki as WITHHELD_REASON, Kn as checkCodeFreshness, Kr as lifecyclePill, Kt as categoryOf, Li as reassessIn, Ln as createWorktree, Lr as touchWorkspace, Lt as ownersOf, M as parsePullRequestLink, Mi as findingStatus, Mr as previousKissResult, Mt as readArchitecture, N as unsupportedReason, Ni as gradeScale, Nn as ReviewBeingDeletedError, Nr as recentWorkspaces, Nt as repositoryKey, O as stopRun, Oi as closesFinding, On as CODE_BLOCK_CLASS, Ot as ARCHITECTURE_DIR, P as runBatch, Pi as isGradeFor, Pn as isBusy, Pr as reviewedCommit, Pt as storeArchitecture, Q as MAX_WORKSPACE_PORT, Qi as resolveBase, Qn as fillFileUrlTemplate, Qr as createWithMembers, Qt as markAllRead, R as settleQueue, Ri as sourcePhrase, Rn as holdWorktree, Rr as workspaceRevision, Rt as QUOTE_LIMITS, S as track, Si as isExplanationAction, Sn as chatConversationSection, Sr as kissDecisionRefusal, St as waitingHistory, T as completeRun, Ti as NO_STATUS, Tn as hasAuthorContent, Tr as lineageKey, Tt as RUNTIME_WORDS, U as readServerRecord, Ui as extractJsonText, Ur as isLifecycle, Ut as announceJob, V as clearServerRecordOwnedBy, Vi as followUpFindingId, Vn as removeWorktree, Vr as activityAttributes, Vt as isArchitectureId, Wi as objectWithAnyOf, Wr as lifecycleChip, Wt as branchOf, X as writeServerRecord, Xi as DetachedHeadError, Xn as fetchHead, Xr as collectionsStamp, Xt as eventName, Yi as originRemote, Yn as reviewedShaOf, Yr as sectionOf, Yt as createPrUpdate, Z as DEFAULT_WORKSPACE_PORT, Zi as isCommitReview, Zn as forgeResolver, Zr as createCollection, Zt as listNotifications, _ as runningJobs, _a as isAgentId, _i as verdictClosure, _n as unforeseenFailure, _r as decideKissRecommendation, _t as telemetryUploadConsent, a as wasBlocked, aa as resolveModel, ai as queueAdd, an as removeNotification, ar as loadJob, at as observeReview, b as startKissJob, ba as AgentCancelledError, bi as explanationActionOf, bn as agentEnvOf, br as indexKeptIds, c as RefreshUnavailableError, ca as readAgentPreference, ci as queueStamp, cn as unreadAround, ct as reanalyseReview, d as mergeUpdateInto, da as STORE_ROOT, di as isIssueOutstanding, dn as existingPaths, dr as phasesFor, ea as shortNameOf, ei as nameProblemText, en as markReadForReview, er as allJobs, et as parseWorkspacePort, fa as AGENT_IDS, fi as isQuestionOutstanding, fn as ASK_REREAD_CEILING_MS, fr as progressSteps, ft as readEvents, g as liveUpdateFor, ga as detectAgents, gi as reviewerDispositions, gn as storedConversation, gt as recordTelemetryUploadConsent, h as UpdateAlreadyRunningError, ha as agentIdentityOf, hi as resolvedByConversation, hn as saveRefresh, hr as isClaimed, i as liveJobsFor, ia as resolveAgent, ii as updateCollections, in as readStateOf, ir as isTerminal, it as failureOf$1, j as extractPullRequestLinks, ji as findingIn, jn as renderInlineText, jr as presentedKissRunOf, jt as partFiles, k as relativeTime$1, ki as currentGradeIn, kn as CODE_COPY_CLASS, kr as mutateWorkspace, kt as checkArchitecture, l as cancelUpdate, la as writeAgentPreference, li as readQueue, ln as unreadCount$1, lr as isLiveJob, lt as runJob, m as askCheckout, ma as agentFor$1, mi as questionsOutstanding, mn as prepareConversationWithin, n as deleteReview, na as git, ni as renameCollection, nn as pruneNotifications, nr as endedAt, nt as statedWorkspacePort, o as discardJob, oa as agentIdOf, oi as queueAddMany, on as retireNotification, or as pruneJobResidue, ot as prepareReview, p as updateReview, pa as agentById, pi as issuesOutstanding, pn as adoptLegacyBaseline, pr as purposeOf, pt as record, q as stopChild, qi as kissOutcome, qn as checkReview, qr as offersLifecycle, qt as createArchitectureNotification, r as lineageIds, ra as refExists, ri as setMembership, rn as readNotification, rr as fail$1, rt as writePortPreference, s as FETCH_TIMEOUT_MS, sa as clearAgentPreference, si as queueRemove, sn as toneOf, sr as saveJob, st as settleReview, t as deleteLineage, ta as shorthandOf, ti as readCollections, tn as moveIds, tr as attemptIdOf, tt as resolveWorkspacePort, u as cancellableHere, ui as doneVerb, un as withUsageRecorded, ur as isTerminalJobPhase, v as kissAgentId, va as parkedReason, vi as processDiscussion, vn as refreshPrContext, vr as followedRefName, w as beginRun, wi as ANALYSIS_ONLY, wn as authorPreview, wr as latestKissRun, wt as recordRuntimeBestEffort, x as isDriving, xa as AgentUnavailableError, xi as explanationOf, xn as EXPLAIN_SIMPLY_PROMPT, xr as indexReviewGroups, xt as feedbackUrl, ya as runnableAgents, yi as recommendationsOf, yn as isUnchanged, yr as groupByLineage, yt as BUILD_VERSION, z as readIndexToken, zi as statusLookup, zn as isWorktreeHeld, zr as workspaceStamps, zt as REPOSITORY_SCOPE } from "./delete_review-CwKZQlMg.js";
|
|
2
2
|
import { basename, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
3
|
import { execFile } from "node:child_process";
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
@@ -497,13 +497,14 @@ function statusLabel(status) {
|
|
|
497
497
|
return status === "in-progress" ? "Not reviewed" : "New";
|
|
498
498
|
}
|
|
499
499
|
/**
|
|
500
|
-
* The bulk bar's way to tick everything the filters show, across pages
|
|
500
|
+
* The bulk bar's way to tick everything the filters show, across pages.
|
|
501
501
|
*
|
|
502
|
-
*
|
|
503
|
-
* reviewer clears the merged reviews in one go,
|
|
502
|
+
* Counted ("Select all 7") only while the headline is not already saying "of 7", so the number is
|
|
503
|
+
* on the bar once. With PR set to Merged this is how a reviewer clears the merged reviews in one go,
|
|
504
|
+
* by way of the existing Mark reviewed.
|
|
504
505
|
*/
|
|
505
|
-
function selectAllLabel(inView) {
|
|
506
|
-
return inView === 0 ? "Select all" : `Select all ${inView}`;
|
|
506
|
+
function selectAllLabel(inView, scopeShown = false) {
|
|
507
|
+
return inView === 0 || scopeShown ? "Select all" : `Select all ${inView}`;
|
|
507
508
|
}
|
|
508
509
|
/** The PR filter's options, in order, as the select shows them. */
|
|
509
510
|
var PR_FILTER_OPTIONS = [
|
|
@@ -553,6 +554,8 @@ var MESSAGE_ACTIONS_CLASS = "pr-helper-chat-actions";
|
|
|
553
554
|
var MESSAGE_SOURCE_ATTR = "data-copy-source";
|
|
554
555
|
/** A finding's text for a feedback draft, on its feedback checkbox. Copy's text less the reviewer's own working. */
|
|
555
556
|
var FEEDBACK_SOURCE_ATTR = "data-feedback-source";
|
|
557
|
+
/** The finding's readable reference (`I3`), beside its feedback text, where the host draws references. */
|
|
558
|
+
var FEEDBACK_REF_ATTR = "data-feedback-ref";
|
|
556
559
|
var STATES$1 = {
|
|
557
560
|
idle: {
|
|
558
561
|
icon: "<path d=\"M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z\"></path><path d=\"M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z\"></path>",
|
|
@@ -705,8 +708,8 @@ function renderFeedbackSelectButton() {
|
|
|
705
708
|
* to the review rather than to a pane, so leaving Issues does not take the tray with it, and its
|
|
706
709
|
* counts are the way back to each pane.
|
|
707
710
|
*/
|
|
708
|
-
function renderFeedbackTray() {
|
|
709
|
-
return `<div class="prb-selection-tray" data-feedback-tray role="region" aria-label="Chosen for feedback" hidden>
|
|
711
|
+
function renderFeedbackTray(reviewTag) {
|
|
712
|
+
return `<div class="prb-selection-tray" data-feedback-tray role="region" aria-label="Chosen for feedback"${reviewTag ? ` data-review-tag="${escapeHtml$1(reviewTag)}"` : ""} hidden>
|
|
710
713
|
<p class="prb-selection-count" aria-live="polite">
|
|
711
714
|
<span data-feedback-total>${escapeHtml$1(feedbackSelectionLabel(0, 0))}</span>${renderGoto("issues")}${renderGoto("questions")}
|
|
712
715
|
</p>
|
|
@@ -805,6 +808,12 @@ var PATHS = {
|
|
|
805
808
|
warning: "<path d=\"M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z\"/><path d=\"M12 9v4M12 17h.01\"/>",
|
|
806
809
|
forward: "<path d=\"M5 12h14\"/><path d=\"m12 5 7 7-7 7\"/>",
|
|
807
810
|
plus: "<path d=\"M12 5v14M5 12h14\"/>",
|
|
811
|
+
"queue-add": "<path d=\"M3 6h12M3 12h9M3 18h9\"/><path d=\"M18 12v6M15 15h6\"/>",
|
|
812
|
+
folder: "<path d=\"M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\"/>",
|
|
813
|
+
"check-double": "<path d=\"m2 12 5 5L18 6\"/><path d=\"m13 15 2 2 7-11\"/>",
|
|
814
|
+
more: "<circle cx=\"5\" cy=\"12\" r=\"1\"/><circle cx=\"12\" cy=\"12\" r=\"1\"/><circle cx=\"19\" cy=\"12\" r=\"1\"/>",
|
|
815
|
+
trash: "<path d=\"M3 6h18\"/><path d=\"M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2\"/><path d=\"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6\"/><path d=\"M10 11v6M14 11v6\"/>",
|
|
816
|
+
close: "<path d=\"M18 6 6 18M6 6l12 12\"/>",
|
|
808
817
|
"sidebar-collapse": "<rect x=\"3\" y=\"4\" width=\"18\" height=\"16\" rx=\"2\"/><path d=\"M9 4v16\"/><path d=\"m16 9-3 3 3 3\"/>",
|
|
809
818
|
"sidebar-expand": "<rect x=\"3\" y=\"4\" width=\"18\" height=\"16\" rx=\"2\"/><path d=\"M9 4v16\"/><path d=\"m13 9 3 3-3 3\"/>"
|
|
810
819
|
};
|
|
@@ -1300,6 +1309,63 @@ function renderFindingExplanation(explanation, options = {}) {
|
|
|
1300
1309
|
</details>`;
|
|
1301
1310
|
}
|
|
1302
1311
|
//#endregion
|
|
1312
|
+
//#region ../../packages/review-harness/src/domain/ai_review/finding_ref.ts
|
|
1313
|
+
var PREFIX = {
|
|
1314
|
+
issue: "I",
|
|
1315
|
+
question: "Q"
|
|
1316
|
+
};
|
|
1317
|
+
function findingRef(kind, index) {
|
|
1318
|
+
return `${PREFIX[kind]}${index + 1}`;
|
|
1319
|
+
}
|
|
1320
|
+
/** Every finding's reference by its id, for a renderer that meets the findings out of order. */
|
|
1321
|
+
function findingRefs(result) {
|
|
1322
|
+
const refs = /* @__PURE__ */ new Map();
|
|
1323
|
+
result.issues.forEach((issue, index) => refs.set(`issue:${issue.id}`, findingRef("issue", index)));
|
|
1324
|
+
result.questions.forEach((question, index) => refs.set(`question:${question.id}`, findingRef("question", index)));
|
|
1325
|
+
return refs;
|
|
1326
|
+
}
|
|
1327
|
+
/** The reference of one finding, or undefined when the analysis does not hold it. */
|
|
1328
|
+
function refOf(refs, kind, id) {
|
|
1329
|
+
return refs.get(`${kind}:${id}`);
|
|
1330
|
+
}
|
|
1331
|
+
/**
|
|
1332
|
+
* The finding a reference names, or null. Only the exact form (`I3`, `q2` is accepted as `Q2`):
|
|
1333
|
+
* a reference out of range names nothing rather than the nearest finding.
|
|
1334
|
+
*/
|
|
1335
|
+
function findingAtRef(result, ref) {
|
|
1336
|
+
const match = /^([IQ])([1-9]\d*)$/i.exec(ref.trim());
|
|
1337
|
+
if (!match) return null;
|
|
1338
|
+
const index = Number(match[2]) - 1;
|
|
1339
|
+
if (match[1].toUpperCase() === "I") {
|
|
1340
|
+
const finding = result.issues[index];
|
|
1341
|
+
return finding === void 0 ? null : {
|
|
1342
|
+
kind: "issue",
|
|
1343
|
+
index,
|
|
1344
|
+
finding
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
const finding = result.questions[index];
|
|
1348
|
+
return finding === void 0 ? null : {
|
|
1349
|
+
kind: "question",
|
|
1350
|
+
index,
|
|
1351
|
+
finding
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* The review a set of references belongs to: `PRB-` and the first eight characters of the analysis's
|
|
1356
|
+
* session id. An update keeps the session, and so its references; a fresh analysis or a reanalysis
|
|
1357
|
+
* starts a new session and numbers its findings again, so its tag differs. A feedback draft carries
|
|
1358
|
+
* it, and Reconcile reads it back, so a reply to an older draft is never read against findings that
|
|
1359
|
+
* a later analysis happens to number the same way.
|
|
1360
|
+
*/
|
|
1361
|
+
function reviewTag(sessionId) {
|
|
1362
|
+
return `PRB-${sessionId.replace(/[^a-z0-9]/gi, "").slice(0, 8).toLowerCase()}`;
|
|
1363
|
+
}
|
|
1364
|
+
/** Every review tag in a text, as `reviewTag` writes them. */
|
|
1365
|
+
function reviewTagsIn(text) {
|
|
1366
|
+
return [...new Set([...text.matchAll(/\bPRB-([a-z0-9]{8})\b/gi)].map((match) => `PRB-${match[1].toLowerCase()}`))];
|
|
1367
|
+
}
|
|
1368
|
+
//#endregion
|
|
1303
1369
|
//#region ../../packages/review-harness/src/domain/ui/components/pill_tooltips.ts
|
|
1304
1370
|
/**
|
|
1305
1371
|
* What every pill in the sidebar actually means, in one place.
|
|
@@ -2050,7 +2116,8 @@ function renderReassessmentLine(kind, assessment, now) {
|
|
|
2050
2116
|
/** Who is suggesting it, by the proposal's source. */
|
|
2051
2117
|
function proposer(source) {
|
|
2052
2118
|
switch (source.kind) {
|
|
2053
|
-
case "buddy":
|
|
2119
|
+
case "buddy":
|
|
2120
|
+
case "buddy-conversation": return "Buddy AI";
|
|
2054
2121
|
case "investigation": return "The repository investigation";
|
|
2055
2122
|
case "update": return "The update";
|
|
2056
2123
|
case "reviewer": return "The review";
|
|
@@ -2089,9 +2156,92 @@ function renderWaitingSuggestions(count) {
|
|
|
2089
2156
|
if (count === 0) return "";
|
|
2090
2157
|
return `<span class="pr-helper-closed-findings-waiting" data-closed-findings-waiting> ${count === 1 ? "1 suggestion waiting" : `${count} suggestions waiting`}</span>`;
|
|
2091
2158
|
}
|
|
2159
|
+
/** A verdict as a card names it. */
|
|
2160
|
+
function verdictLabel$1(verdict) {
|
|
2161
|
+
switch (verdict) {
|
|
2162
|
+
case "resolved": return "Resolved";
|
|
2163
|
+
case "needs-investigation": return "Needs investigation";
|
|
2164
|
+
case "open": return "Open again";
|
|
2165
|
+
case "answered": return "Answered";
|
|
2166
|
+
case "partially-answered": return "Partly answered";
|
|
2167
|
+
case "unanswered": return "Unanswered again";
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
/**
|
|
2171
|
+
* The words the evidence rests on, as the reviewer pasted them. Quoted, never paraphrased: they are
|
|
2172
|
+
* someone else's claim, and the card shows the claim rather than vouching for it.
|
|
2173
|
+
*/
|
|
2174
|
+
function renderQuotes(quotes) {
|
|
2175
|
+
if (!quotes?.length) return "";
|
|
2176
|
+
return quotes.map((quote) => `<blockquote class="pr-helper-reassessment-quote">${renderInlineText(quote.text)}</blockquote>`).join("");
|
|
2177
|
+
}
|
|
2178
|
+
/** What a resolution rests on, said before the places that show it. Nothing on any other verdict. */
|
|
2179
|
+
function renderBasis(basis) {
|
|
2180
|
+
if (!basis) return "";
|
|
2181
|
+
return `<p class="pr-helper-reassessment-basis" data-reassessment-basis="${basis}">${escapeHtml$1(basis === "code" ? "Buddy AI read this in the code at the reviewed commit. Check it before accepting:" : "Buddy AI read the reply as showing an assumption the issue made is false.")}</p>`;
|
|
2182
|
+
}
|
|
2183
|
+
function renderAnswer(answer) {
|
|
2184
|
+
return answer ? `<p class="pr-helper-reassessment-answer">${renderInlineText(answer)}</p>` : "";
|
|
2185
|
+
}
|
|
2186
|
+
function renderStatusStep(step, now) {
|
|
2187
|
+
return `<li class="pr-helper-reassessment-step">
|
|
2188
|
+
<span class="pr-helper-reassessment-step-move">${step.from ? `${escapeHtml$1(verdictLabel$1(step.from))} → ` : ""}<span class="pr-helper-reassessment-to">${escapeHtml$1(verdictLabel$1(step.to))}</span></span>
|
|
2189
|
+
<span class="pr-helper-reassessment-step-source">${escapeHtml$1(capitalise(sourcePhrase(step.source)))} · ${escapeHtml$1(formatRelativeTime(step.at, now))}</span>
|
|
2190
|
+
${step.reason ? `<p class="pr-helper-reassessment-reason">${renderInlineText(step.reason)}</p>` : ""}
|
|
2191
|
+
${renderAnswer(step.answer)}
|
|
2192
|
+
${renderQuotes(step.quotes)}
|
|
2193
|
+
${renderBasis(step.basis)}
|
|
2194
|
+
${renderEvidence$1(step.evidence)}
|
|
2195
|
+
</li>`;
|
|
2196
|
+
}
|
|
2197
|
+
/**
|
|
2198
|
+
* The status the reviewer accepted from later evidence, and History behind it. Says whether the
|
|
2199
|
+
* finding is still open, because an accepted status is not a closed one: a partial answer and an
|
|
2200
|
+
* issue that needs investigating stay in the work.
|
|
2201
|
+
*/
|
|
2202
|
+
function renderStatusLine(status, now) {
|
|
2203
|
+
const current = status.current;
|
|
2204
|
+
if (!current) return "";
|
|
2205
|
+
const last = status.history[status.history.length - 1];
|
|
2206
|
+
const open = closesFinding(current) ? "" : " Still open.";
|
|
2207
|
+
return `<div class="pr-helper-reassessment pr-helper-finding-verdict" data-finding-verdict="${escapeHtml$1(current)}">
|
|
2208
|
+
<p class="pr-helper-reassessment-line"><span class="pr-helper-reassessment-to">${escapeHtml$1(verdictLabel$1(current))}</span> <span class="pr-helper-reassessment-source">· Accepted ${escapeHtml$1(sourcePhrase(last.source))}.${open}</span></p>
|
|
2209
|
+
${renderAnswer(last.answer)}
|
|
2210
|
+
<details class="pr-helper-reassessment-history">
|
|
2211
|
+
<summary class="pr-helper-reassessment-history-summary">Status history</summary>
|
|
2212
|
+
<ol class="pr-helper-reassessment-steps">${status.history.map((step) => renderStatusStep(step, now)).join("")}</ol>
|
|
2213
|
+
</details>
|
|
2214
|
+
</div>`;
|
|
2215
|
+
}
|
|
2216
|
+
/**
|
|
2217
|
+
* A status waiting on the reviewer. The same answers as a grade's, under the same attribute, so a
|
|
2218
|
+
* host's Accept and Keep need no second wiring: Accept applies exactly this verdict, Keep declines it.
|
|
2219
|
+
* A proposal that would take back an accepted closure says so first.
|
|
2220
|
+
*/
|
|
2221
|
+
function renderStatusProposal(status, options) {
|
|
2222
|
+
const proposal = status.pending;
|
|
2223
|
+
if (!proposal) return "";
|
|
2224
|
+
const actions = options.editable ? `<div class="pr-helper-reassessment-actions">
|
|
2225
|
+
<button type="button" class="pr-helper-reassessment-accept" data-reassessment-decide="accepted">Accept</button>
|
|
2226
|
+
<button type="button" class="pr-helper-reassessment-keep" data-reassessment-decide="declined">Keep as it is</button>
|
|
2227
|
+
${options.askMore ? "<button type=\"button\" class=\"pr-helper-reassessment-more\" data-reassessment-ask-more>Ask more →</button>" : ""}
|
|
2228
|
+
</div>` : "";
|
|
2229
|
+
const contradiction = proposal.contradicts && proposal.from ? `<p class="pr-helper-reassessment-contradicts" data-reassessment-contradicts>This would take back “${escapeHtml$1(verdictLabel$1(proposal.from))}”, which you accepted earlier.</p>` : "";
|
|
2230
|
+
return `<div class="pr-helper-reassessment-proposal" data-reassessment-proposal="${escapeHtml$1(proposal.id)}" data-status-proposal="${escapeHtml$1(proposal.to)}">
|
|
2231
|
+
<p class="pr-helper-reassessment-proposal-title">${escapeHtml$1(proposer(proposal.source))} suggests: ${escapeHtml$1(verdictLabel$1(proposal.to))}</p>
|
|
2232
|
+
${contradiction}
|
|
2233
|
+
${renderAnswer(proposal.answer)}
|
|
2234
|
+
${proposal.reason ? `<p class="pr-helper-reassessment-reason">${renderInlineText(proposal.reason)}</p>` : ""}
|
|
2235
|
+
${renderQuotes(proposal.quotes)}
|
|
2236
|
+
${renderBasis(proposal.basis)}
|
|
2237
|
+
${renderEvidence$1(proposal.evidence)}
|
|
2238
|
+
${actions}
|
|
2239
|
+
</div>`;
|
|
2240
|
+
}
|
|
2092
2241
|
/** The line, its History and any proposal, in the region a host replaces after a write. */
|
|
2093
|
-
function renderAssessmentRegion(kind, assessment, options = {}) {
|
|
2094
|
-
|
|
2242
|
+
function renderAssessmentRegion(kind, assessment, options = {}, status) {
|
|
2243
|
+
const now = options.now ?? Date.now();
|
|
2244
|
+
return `<div class="pr-helper-reassessment-region" data-assessment-region>${status ? `${renderStatusLine(status, now)}${renderStatusProposal(status, options)}` : ""}${renderReassessmentLine(kind, assessment, now)}${renderProposal(assessment, options)}</div>`;
|
|
2095
2245
|
}
|
|
2096
2246
|
//#endregion
|
|
2097
2247
|
//#region ../../packages/review-harness/src/domain/ui/components/review_tabs.ts
|
|
@@ -2534,7 +2684,7 @@ function possibleReason(summary, hasDescription) {
|
|
|
2534
2684
|
*/
|
|
2535
2685
|
function renderUnclearMotive(motivation, session, questions) {
|
|
2536
2686
|
const intent = questions.find((q) => q.category === "intent");
|
|
2537
|
-
const answered = intent ? !isQuestionOutstanding(intent, new Set(session.answeredQuestionIds ?? [])) : false;
|
|
2687
|
+
const answered = intent ? !isQuestionOutstanding(intent, new Set(session.answeredQuestionIds ?? []), verdictClosure(session).question) : false;
|
|
2538
2688
|
const hide = (yes) => yes ? " hidden" : "";
|
|
2539
2689
|
return `
|
|
2540
2690
|
<section class="pr-helper-why pr-helper-why-unclear" aria-labelledby="pr-helper-why-heading"${intent ? ` data-intent-question="${escapeHtml$1(intent.id)}"` : ""}>
|
|
@@ -2578,7 +2728,7 @@ function renderOverviewTab(session, iconUrl = "", options) {
|
|
|
2578
2728
|
<div class="pr-helper-overview-stats" data-tour="stats">
|
|
2579
2729
|
${renderOverviewStat(metrics.sections, "Themes", "guide")}
|
|
2580
2730
|
${renderIssuesStat(issuesOutstanding(result.issues, reviewerDispositions(session)))}
|
|
2581
|
-
${renderRemainingStat(questionsOutstanding(result.questions, new Set(session.answeredQuestionIds ?? [])), "Questions", "questions", "answered")}
|
|
2731
|
+
${renderRemainingStat(questionsOutstanding(result.questions, new Set(session.answeredQuestionIds ?? []), verdictClosure(session).question), "Questions", "questions", "answered")}
|
|
2582
2732
|
${renderCoverageStat(session, metrics.files)}
|
|
2583
2733
|
</div>
|
|
2584
2734
|
${renderSuggestedPath(result.sections, "Suggested review path")}
|
|
@@ -2621,13 +2771,13 @@ function discussionDetails(session) {
|
|
|
2621
2771
|
if (reviewThreads > 0) {
|
|
2622
2772
|
const outdated = stats.outdatedThreads ?? 0;
|
|
2623
2773
|
const state = stats.unresolvedThreads > 0 ? `${stats.unresolvedThreads} unresolved${outdated > 0 ? `, ${outdated} on outdated lines` : ""}` : "all resolved";
|
|
2624
|
-
parts.push(`${reviewThreads} ${plural$
|
|
2774
|
+
parts.push(`${reviewThreads} ${plural$3(reviewThreads, "review thread")} (${state})`);
|
|
2625
2775
|
}
|
|
2626
|
-
if (conversation > 0) parts.push(`${conversation} ${plural$
|
|
2627
|
-
if (decisions > 0) parts.push(`${decisions} ${plural$
|
|
2776
|
+
if (conversation > 0) parts.push(`${conversation} ${plural$3(conversation, "conversation comment")}`);
|
|
2777
|
+
if (decisions > 0) parts.push(`${decisions} ${plural$3(decisions, "review decision")}`);
|
|
2628
2778
|
return parts;
|
|
2629
2779
|
}
|
|
2630
|
-
function plural$
|
|
2780
|
+
function plural$3(count, word) {
|
|
2631
2781
|
return count === 1 ? word : `${word}s`;
|
|
2632
2782
|
}
|
|
2633
2783
|
function renderExistingDiscussion(ref) {
|
|
@@ -3068,8 +3218,11 @@ function renderIssuesTab(session, investigatingId, options) {
|
|
|
3068
3218
|
const dismissals = new Map((session.dismissedIssues ?? []).map((entry) => [entry.id, entry]));
|
|
3069
3219
|
const dispositions = reviewerDispositions(session);
|
|
3070
3220
|
const assess = assessmentLookup(session);
|
|
3221
|
+
const statusOf = statusLookup(session);
|
|
3071
3222
|
const grading = gradeOptions(options, now);
|
|
3223
|
+
const refs = options?.findingRefs ? findingRefs(result) : null;
|
|
3072
3224
|
const card = (issue) => {
|
|
3225
|
+
const ref = refs ? refOf(refs, "issue", issue.id) : void 0;
|
|
3073
3226
|
const isDismissed = dispositions.isDismissed(issue);
|
|
3074
3227
|
const stale = dispositions.isStale(issue);
|
|
3075
3228
|
const assessment = assess("issue", issue);
|
|
@@ -3077,7 +3230,8 @@ function renderIssuesTab(session, investigatingId, options) {
|
|
|
3077
3230
|
<div class="pr-helper-issue-card ${cardGradeClass("issue", assessment.current)}${issue.reconciliationStatus ? ` pr-helper-issue-${issue.reconciliationStatus}` : ""}${issue.investigationImpact ? ` pr-helper-issue-investigation-${issue.investigationImpact}` : ""}${isDismissed && !stale ? " pr-helper-issue-card-dismissed" : ""}" data-finding-card="${escapeHtml$1(issue.id)}">
|
|
3078
3231
|
<div class="pr-helper-issue-header">
|
|
3079
3232
|
<div class="pr-helper-issue-title-row">
|
|
3080
|
-
${options?.feedbackSelect ? renderFeedbackPick("issue", issue.title, issueFeedbackMarkdown(issue)) : ""}
|
|
3233
|
+
${options?.feedbackSelect ? renderFeedbackPick("issue", issue.title, issueFeedbackMarkdown(issue), ref) : ""}
|
|
3234
|
+
${renderFindingRef(ref)}
|
|
3081
3235
|
<p class="pr-helper-issue-title">${renderInlineText(issue.title)}</p>
|
|
3082
3236
|
${renderFindingCopySource(issueMarkdown(issue, assessment), "issue")}
|
|
3083
3237
|
${options?.askBuddy ? renderAskBuddyMenu("issue") : ""}
|
|
@@ -3092,7 +3246,7 @@ function renderIssuesTab(session, investigatingId, options) {
|
|
|
3092
3246
|
</div>
|
|
3093
3247
|
</div>
|
|
3094
3248
|
${renderStaleDismissal(stale)}
|
|
3095
|
-
${renderAssessmentRegion("issue", assessment, grading)}
|
|
3249
|
+
${renderAssessmentRegion("issue", assessment, grading, statusOf("issue", issue.id))}
|
|
3096
3250
|
<p class="pr-helper-issue-desc">${renderInlineText(issue.description)}</p>${renderCardExplanation(explanationOf(session, "issue", issue.id), options)}
|
|
3097
3251
|
${renderReconciliationNote(issue.reconciliationNote)}
|
|
3098
3252
|
${renderStatusHistory(issue.statusHistory, now)}
|
|
@@ -3103,7 +3257,7 @@ function renderIssuesTab(session, investigatingId, options) {
|
|
|
3103
3257
|
${renderInvestigationProvenance("issue", issue.id, records, now)}
|
|
3104
3258
|
<div class="pr-helper-issue-actions">
|
|
3105
3259
|
${renderInvestigationActions("issue", issue.id, records, investigatingId)}
|
|
3106
|
-
${renderDismissToggle(issue, isDismissed, stale, dismissals.get(issue.id))}
|
|
3260
|
+
${renderDismissToggle(issue, isDismissed, stale, dismissals.get(issue.id), dispositions.isClosedByVerdict(issue))}
|
|
3107
3261
|
</div>
|
|
3108
3262
|
</div>
|
|
3109
3263
|
`;
|
|
@@ -3113,7 +3267,7 @@ function renderIssuesTab(session, investigatingId, options) {
|
|
|
3113
3267
|
const open = result.issues.filter((issue) => isIssueOutstanding(issue, dispositions)).map(card).join("");
|
|
3114
3268
|
const closedIssues = result.issues.filter((issue) => !isIssueOutstanding(issue, dispositions));
|
|
3115
3269
|
const closed = closedIssues.map(card).join("");
|
|
3116
|
-
return `<div class="pr-helper-findings-progress" role="status">${findingsProgressBody(counts.total, counts.open, doneVerb(counts))}</div>${open}${renderClosedFindings(closedFindingsSummary(counts), closed, "issues", counts.open === 0, closedIssues.filter((issue) => assess("issue", issue).pending).length)}`;
|
|
3270
|
+
return `<div class="pr-helper-findings-progress" role="status">${findingsProgressBody(counts.total, counts.open, doneVerb(counts))}</div>${open}${renderClosedFindings(closedFindingsSummary(counts), closed, "issues", counts.open === 0, closedIssues.filter((issue) => assess("issue", issue).pending || statusOf("issue", issue.id).pending).length)}`;
|
|
3117
3271
|
}
|
|
3118
3272
|
/**
|
|
3119
3273
|
* What to call the closed pile, by what is actually in it.
|
|
@@ -3130,7 +3284,9 @@ function closedFindingsSummary(counts) {
|
|
|
3130
3284
|
if (byConversation === counts.done) return `Resolved by the conversation (${counts.done})`;
|
|
3131
3285
|
return `Resolved (${counts.done})`;
|
|
3132
3286
|
}
|
|
3133
|
-
|
|
3287
|
+
const byVerdict = counts.byVerdict ?? 0;
|
|
3288
|
+
if (byCommits === 0 && byVerdict === 0) return `Dismissed by you (${counts.done})`;
|
|
3289
|
+
if (byCommits === 0 && byVerdict === counts.done) return `Accepted as resolved (${counts.done})`;
|
|
3134
3290
|
return `Dealt with (${counts.done})`;
|
|
3135
3291
|
}
|
|
3136
3292
|
/**
|
|
@@ -3169,11 +3325,11 @@ function renderStaleDismissal(stale) {
|
|
|
3169
3325
|
* toggle is. A reviewer agreeing with a fix changes no count, and a control that vanishes on some
|
|
3170
3326
|
* cards is harder to explain than one that occasionally changes nothing.
|
|
3171
3327
|
*/
|
|
3172
|
-
function renderDismissToggle(issue, isDismissed, stale, dismissal) {
|
|
3328
|
+
function renderDismissToggle(issue, isDismissed, stale, dismissal, closedByVerdict = false) {
|
|
3173
3329
|
const label = stale ? "Dismiss again" : isDismissed ? "Reopen" : "Dismiss";
|
|
3174
3330
|
const settled = issue.reconciliationStatus === "resolved";
|
|
3175
3331
|
const settledBy = settled ? ` data-settled-by="${resolvedByConversation(issue) ? "conversation" : "code"}"` : "";
|
|
3176
|
-
return `<button type="button" class="pr-helper-issue-dismiss-toggle" data-issue-dismissed="${escapeHtml$1(issue.id)}" data-dismissed="${isDismissed}"${stale ? " data-stale=\"true\"" : ""}${dismissal ? ` data-dismissed-in="${escapeHtml$1(dismissal.sessionId)}"` : ""}${settled ? ` data-settled="true"${settledBy}` : ""}>${label}</button>`;
|
|
3332
|
+
return `<button type="button" class="pr-helper-issue-dismiss-toggle" data-issue-dismissed="${escapeHtml$1(issue.id)}" data-dismissed="${isDismissed}"${stale ? " data-stale=\"true\"" : ""}${dismissal ? ` data-dismissed-in="${escapeHtml$1(dismissal.sessionId)}"` : ""}${settled ? ` data-settled="true"${settledBy}` : ""}${closedByVerdict ? " data-verdict-closed=\"true\"" : ""}>${label}</button>`;
|
|
3177
3333
|
}
|
|
3178
3334
|
/**
|
|
3179
3335
|
* The group an Intent question lives in, keyed like a theme's so the workspace client finds its
|
|
@@ -3185,19 +3341,21 @@ var INTENT_QUESTION_GROUP = "prb-intent";
|
|
|
3185
3341
|
function questionHome(q) {
|
|
3186
3342
|
return q.category === "intent" ? INTENT_QUESTION_GROUP : q.sectionId;
|
|
3187
3343
|
}
|
|
3188
|
-
function renderQuestionCard(q, sections, answered, records, now = Date.now(), investigatingId, options, explanation, assessment = ANALYSIS_ONLY("question", q)) {
|
|
3344
|
+
function renderQuestionCard(q, sections, answered, records, now = Date.now(), investigatingId, options, explanation, assessment = ANALYSIS_ONLY("question", q), ref, status = NO_STATUS("question", q.id)) {
|
|
3189
3345
|
const grading = gradeOptions(options, now);
|
|
3190
3346
|
const importanceBadge = renderGradeControl("question", assessment, grading);
|
|
3191
3347
|
const jumpLink = renderQuestionJump(q.file, options);
|
|
3192
3348
|
const settled = q.reconciliationStatus === "answered";
|
|
3193
|
-
const
|
|
3349
|
+
const verdictClosed = closesFinding(status.current);
|
|
3350
|
+
const answerToggle = `<button type="button" class="pr-helper-question-answer-toggle" data-question-answered="${escapeHtml$1(q.id)}" data-answered="${answered}"${settled ? " data-settled=\"true\"" : ""}${verdictClosed ? " data-verdict-closed=\"true\"" : ""}>${answered ? "Mark as unanswered" : "Mark answered"}</button>`;
|
|
3194
3351
|
const investigationAnswer = q.investigationAnswer?.trim() ? `<h5>Answer from investigation</h5><p class="pr-helper-issue-desc pr-helper-investigation-answer">${renderInlineText(q.investigationAnswer)}</p>` : "";
|
|
3195
3352
|
const reconciliationAnswer = q.reconciliationAnswer?.trim() ? `<h5>${escapeHtml$1(reconciledAnswerHeading(q.reconciledAgainst))}</h5><p class="pr-helper-issue-desc pr-helper-investigation-answer">${renderInlineText(q.reconciliationAnswer)}</p>` : "";
|
|
3196
3353
|
return `
|
|
3197
3354
|
<div class="pr-helper-question-card${answered ? " pr-helper-question-card-answered" : ""}${q.investigationImpact ? ` pr-helper-issue-investigation-${q.investigationImpact}` : ""}" data-finding-card="${escapeHtml$1(q.id)}"${questionHome(q) ? ` data-section-id="${escapeHtml$1(questionHome(q))}"` : ""}>
|
|
3198
3355
|
<div class="pr-helper-issue-header">
|
|
3199
3356
|
<div class="pr-helper-issue-title-row">
|
|
3200
|
-
${options?.feedbackSelect ? renderFeedbackPick("question", q.question, questionFeedbackMarkdown(q)) : ""}
|
|
3357
|
+
${options?.feedbackSelect ? renderFeedbackPick("question", q.question, questionFeedbackMarkdown(q), ref) : ""}
|
|
3358
|
+
${renderFindingRef(ref)}
|
|
3201
3359
|
<p class="pr-helper-issue-title">${renderInlineText(q.question)}</p>
|
|
3202
3360
|
${renderFindingCopySource(questionMarkdown(q, assessment), "question")}
|
|
3203
3361
|
${options?.askBuddy ? renderAskBuddyMenu("question") : ""}
|
|
@@ -3214,7 +3372,7 @@ function renderQuestionCard(q, sections, answered, records, now = Date.now(), in
|
|
|
3214
3372
|
${importanceBadge}
|
|
3215
3373
|
</div>
|
|
3216
3374
|
</div>
|
|
3217
|
-
${renderAssessmentRegion("question", assessment, grading)}
|
|
3375
|
+
${renderAssessmentRegion("question", assessment, grading, status)}
|
|
3218
3376
|
${q.whyThisMatters ? `<h5>Why this matters</h5><p class="pr-helper-issue-desc">${renderInlineText(q.whyThisMatters)}</p>` : ""}${renderCardExplanation(explanation, options)}
|
|
3219
3377
|
${reconciliationAnswer}
|
|
3220
3378
|
${renderReconciliationNote(q.reconciliationNote)}
|
|
@@ -3245,8 +3403,8 @@ function renderQuestionCard(q, sections, answered, records, now = Date.now(), in
|
|
|
3245
3403
|
function questionsProgressBody(total, outstanding) {
|
|
3246
3404
|
return outstanding === 0 ? `<span class="pr-helper-questions-progress-check" aria-hidden="true">✓</span><span class="pr-helper-questions-progress-count">All ${total} questions answered</span>` : `<span class="pr-helper-questions-progress-count">${outstanding} of ${total} unanswered</span>`;
|
|
3247
3405
|
}
|
|
3248
|
-
function renderQuestionsProgress(questions, answered) {
|
|
3249
|
-
const counts = questionsOutstanding(questions, answered);
|
|
3406
|
+
function renderQuestionsProgress(questions, answered, closedByVerdict) {
|
|
3407
|
+
const counts = questionsOutstanding(questions, answered, closedByVerdict);
|
|
3250
3408
|
if (counts.total < 2) return "";
|
|
3251
3409
|
return `<div class="pr-helper-questions-progress" role="status">${questionsProgressBody(counts.total, counts.open)}</div>`;
|
|
3252
3410
|
}
|
|
@@ -3261,8 +3419,13 @@ function renderQuestionsProgress(questions, answered) {
|
|
|
3261
3419
|
* It carries the text the card gives a feedback draft, which is narrower than its Copy: see
|
|
3262
3420
|
* finding_copy.ts.
|
|
3263
3421
|
*/
|
|
3264
|
-
function renderFeedbackPick(kind, title, feedback) {
|
|
3265
|
-
|
|
3422
|
+
function renderFeedbackPick(kind, title, feedback, ref) {
|
|
3423
|
+
const refAttr = ref ? ` ${FEEDBACK_REF_ATTR}="${ref}"` : "";
|
|
3424
|
+
return `<input type="checkbox" class="pr-helper-feedback-pick" data-feedback-pick="${kind}" ${FEEDBACK_SOURCE_ATTR}="${escapeHtml$1(feedback)}"${refAttr} autocomplete="off" aria-label="${escapeHtml$1(`Include in feedback: ${title.trim()}`)}">`;
|
|
3425
|
+
}
|
|
3426
|
+
/** The card's readable reference, ahead of its title. Drawn only where the host asked for references. */
|
|
3427
|
+
function renderFindingRef(ref) {
|
|
3428
|
+
return ref ? `<span class="pr-helper-finding-ref" data-finding-ref="${ref}">${ref}</span>` : "";
|
|
3266
3429
|
}
|
|
3267
3430
|
function renderQuestionJump(file, options) {
|
|
3268
3431
|
if (!file || options?.suppressFileJump) return "";
|
|
@@ -3279,14 +3442,17 @@ function renderQuestionsTab(session, investigatingId, options) {
|
|
|
3279
3442
|
if (result.questions.length === 0) return emptyState("No open questions.");
|
|
3280
3443
|
const sections = buildSectionLookup(result);
|
|
3281
3444
|
const answered = new Set(session.answeredQuestionIds ?? []);
|
|
3282
|
-
const progress = renderQuestionsProgress(result.questions, answered);
|
|
3445
|
+
const progress = renderQuestionsProgress(result.questions, answered, verdictClosure(session).question);
|
|
3283
3446
|
const records = session.investigations;
|
|
3284
3447
|
const now = Date.now();
|
|
3285
3448
|
const assess = assessmentLookup(session);
|
|
3286
|
-
const
|
|
3287
|
-
const
|
|
3288
|
-
const
|
|
3289
|
-
const
|
|
3449
|
+
const statusOf = statusLookup(session);
|
|
3450
|
+
const closedByVerdict = verdictClosure(session).question;
|
|
3451
|
+
const refs = options?.findingRefs ? findingRefs(result) : null;
|
|
3452
|
+
const card = (q) => renderQuestionCard(q, sections, answered.has(q.id), records, now, investigatingId, options, explanationOf(session, "question", q.id), assess("question", q), refs ? refOf(refs, "question", q.id) : void 0, statusOf("question", q.id));
|
|
3453
|
+
const open = result.questions.filter((q) => isQuestionOutstanding(q, answered, closedByVerdict));
|
|
3454
|
+
const settled = result.questions.filter((q) => !isQuestionOutstanding(q, answered, closedByVerdict));
|
|
3455
|
+
const closed = settled.length === 0 ? "" : renderClosedFindings(`Answered (${settled.length})`, settled.map(card).join(""), "questions", open.length === 0, settled.filter((q) => assess("question", q).pending || statusOf("question", q.id).pending).length);
|
|
3290
3456
|
const intent = open.filter((q) => q.category === "intent");
|
|
3291
3457
|
const intentHtml = result.questions.some((q) => q.category === "intent") ? `
|
|
3292
3458
|
<div class="pr-helper-question-group" data-question-group="${INTENT_QUESTION_GROUP}"${intent.length ? "" : " hidden"}>
|
|
@@ -3474,6 +3640,41 @@ function renderThemePicker(sections, selectedId) {
|
|
|
3474
3640
|
return `<div class="prb-guide-picker">${sections.map((section, index) => `<a class="prb-guide-pick" href="${escapeHtml$1(hashForSection(section.id))}" data-goto-section="${escapeHtml$1(section.id)}" data-theme-index="${index}" data-recommended-start="${section.recommendedStartingPoint}" aria-current="${section.id === selectedId}"><span class="prb-guide-pick-order">${index + 1}</span>${escapeHtml$1(section.title)}</a>`).join("")}</div>`;
|
|
3475
3641
|
}
|
|
3476
3642
|
//#endregion
|
|
3643
|
+
//#region ../../packages/review-harness/src/domain/ui/components/turn_actions.ts
|
|
3644
|
+
/**
|
|
3645
|
+
* What a finished Buddy AI turn offers under its answer, drawn the same by the page and by the client
|
|
3646
|
+
* on a live answer, so a reload never changes it.
|
|
3647
|
+
*
|
|
3648
|
+
* Every completed turn offers Reconcile with findings. Always, rather than when a turn looks like an
|
|
3649
|
+
* author's reply: whether a turn bears on the findings is the judgement the reconcile itself makes,
|
|
3650
|
+
* and a control that appears only sometimes is one a reviewer cannot count on. A reconcile turn
|
|
3651
|
+
* offers Accept all instead, while any suggestion it made is still waiting.
|
|
3652
|
+
*/
|
|
3653
|
+
/**
|
|
3654
|
+
* The supporting sentence, said beside the action. It names the scope, because the action reads only
|
|
3655
|
+
* the message of the turn it sits under: a reply split across several messages is reconciled one
|
|
3656
|
+
* message at a time (the route takes several turns; choosing them on the page is not built yet).
|
|
3657
|
+
*/
|
|
3658
|
+
var RECONCILE_HELP = "Reassesses existing issues and questions using only your message in this turn.";
|
|
3659
|
+
function renderTurnActions(turn, waiting = []) {
|
|
3660
|
+
if (turn.reconciles) return renderAcceptAll(waiting);
|
|
3661
|
+
return `<div class="prb-turn-actions" data-turn-actions="${turn.askedAt}">
|
|
3662
|
+
<button type="button" class="prb-turn-reconcile" data-reconcile-turn="${turn.askedAt}">Reconcile with findings</button>
|
|
3663
|
+
<span class="prb-turn-help">${escapeHtml$1(RECONCILE_HELP)}</span>
|
|
3664
|
+
</div>`;
|
|
3665
|
+
}
|
|
3666
|
+
/**
|
|
3667
|
+
* Accept every suggestion a reconcile made that is still waiting, each on its own card exactly as its
|
|
3668
|
+
* own Accept would. Nothing when none waits.
|
|
3669
|
+
*/
|
|
3670
|
+
function renderAcceptAll(waiting) {
|
|
3671
|
+
if (!waiting.length) return "";
|
|
3672
|
+
const label = waiting.length === 1 ? "Accept the suggestion" : `Accept all ${waiting.length} suggestions`;
|
|
3673
|
+
return `<div class="prb-turn-actions">
|
|
3674
|
+
<button type="button" class="prb-turn-accept-all" data-reconcile-accept-all="${escapeHtml$1(waiting.join(" "))}">${escapeHtml$1(label)}</button>
|
|
3675
|
+
</div>`;
|
|
3676
|
+
}
|
|
3677
|
+
//#endregion
|
|
3477
3678
|
//#region ../../packages/review-harness/src/workspace/diagram_view.ts
|
|
3478
3679
|
/**
|
|
3479
3680
|
* The diagrams a review holds, drawn as cards in one scrolling pane.
|
|
@@ -5582,7 +5783,7 @@ function updateFacts(outcome) {
|
|
|
5582
5783
|
counts.affected ? `${counts.affected} to look at again` : "",
|
|
5583
5784
|
counts.questionsAnswered ? `${plural(counts.questionsAnswered, "question", "questions")} answered` : ""
|
|
5584
5785
|
].filter(Boolean);
|
|
5585
|
-
const grades = counts.gradeChanges ? [`${plural(counts.gradeChanges, "grade change", "grade changes")} suggested`] : [];
|
|
5786
|
+
const grades = [...counts.gradeChanges ? [`${plural(counts.gradeChanges, "grade change", "grade changes")} suggested`] : [], ...counts.decisionsChallenged ? [`${plural(counts.decisionsChallenged, "accepted answer", "accepted answers")} put back to you`] : []];
|
|
5586
5787
|
const basis = outcome.reassessed;
|
|
5587
5788
|
return [...basis && !basis.code ? [escapeHtml$1("Reassessed against the conversation")] : [`<code>${escapeHtml$1(outcome.toSha.slice(0, 7))}</code>`, ...basis?.discussion ? [escapeHtml$1("with the conversation")] : []], ...[...changes.length ? changes : ["No change to the findings"], ...grades].map(escapeHtml$1)];
|
|
5588
5789
|
}
|
|
@@ -6271,7 +6472,7 @@ function renderReviewBody(view, sections) {
|
|
|
6271
6472
|
${renderRail(view, sections, updating(view))}
|
|
6272
6473
|
<main class="prb-doc">
|
|
6273
6474
|
${updating(view) ? renderUpdatingDoc(view) : renderReviewDoc(view, sections)}
|
|
6274
|
-
${offersFeedback(view) ? renderFeedbackTray() : ""}
|
|
6475
|
+
${offersFeedback(view) ? renderFeedbackTray(reviewTag(view.session.id)) : ""}
|
|
6275
6476
|
</main>
|
|
6276
6477
|
${result ? renderAsk(view, sections, updating(view)) : ""}
|
|
6277
6478
|
${result ? renderAskTab() : ""}
|
|
@@ -6446,7 +6647,8 @@ function renderStandardViews(view, sections) {
|
|
|
6446
6647
|
${renderIssuesTab(session, null, {
|
|
6447
6648
|
askBuddy: true,
|
|
6448
6649
|
feedbackSelect: true,
|
|
6449
|
-
reassess: true
|
|
6650
|
+
reassess: true,
|
|
6651
|
+
findingRefs: true
|
|
6450
6652
|
})}
|
|
6451
6653
|
</section>
|
|
6452
6654
|
<section class="prb-view" data-view="questions" hidden>
|
|
@@ -6455,7 +6657,8 @@ function renderStandardViews(view, sections) {
|
|
|
6455
6657
|
suppressFileJump: true,
|
|
6456
6658
|
askBuddy: true,
|
|
6457
6659
|
feedbackSelect: true,
|
|
6458
|
-
reassess: true
|
|
6660
|
+
reassess: true,
|
|
6661
|
+
findingRefs: true
|
|
6459
6662
|
})}
|
|
6460
6663
|
</section>
|
|
6461
6664
|
<section class="prb-view" data-view="prcontext" hidden>
|
|
@@ -6840,7 +7043,7 @@ function renderRail(view, sections, stale = false) {
|
|
|
6840
7043
|
</div>
|
|
6841
7044
|
${railLink("map", "diagrams", "Diagrams", diagramsBadge(diagramCount(view)), void 0, "diagram architecture")}
|
|
6842
7045
|
${railLink("issues", "issues", "Issues", findingsBadge(issuesOutstanding(issues, dispositions)), void 0, void 0, true)}
|
|
6843
|
-
${railLink("questions", "questions", "Questions", remainingBadge(questionsOutstanding(questions, answered), "question", "answered"), void 0, void 0, true)}
|
|
7046
|
+
${railLink("questions", "questions", "Questions", remainingBadge(questionsOutstanding(questions, answered, verdictClosure(view.session).question), "question", "answered"), void 0, void 0, true)}
|
|
6844
7047
|
${railLink("files", "files", "Files", totalBadge(view.changeSet.data.files.length, "files"))}
|
|
6845
7048
|
${view.prContext ? railLink("prcontext", "prcontext", "PR context", totalBadge(view.prContext.processed.stats.totalThreads, "PR comment threads")) : railLink("prcontext", "prcontext", "PR context", null, "No pull request was connected to this review")}`
|
|
6846
7049
|
}) : "";
|
|
@@ -7002,7 +7205,7 @@ function totalBadge(count, noun) {
|
|
|
7002
7205
|
* with all of them", and a tick would claim the second on a review that earned the first.
|
|
7003
7206
|
*/
|
|
7004
7207
|
/** "1 issue", "2 issues". Shared so a row's words and its badge cannot drift apart. */
|
|
7005
|
-
function plural$
|
|
7208
|
+
function plural$2(n, noun) {
|
|
7006
7209
|
return `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
7007
7210
|
}
|
|
7008
7211
|
function remainingBadge(counts, noun, verb) {
|
|
@@ -7012,16 +7215,16 @@ function remainingBadge(counts, noun, verb) {
|
|
|
7012
7215
|
};
|
|
7013
7216
|
if (counts.open === 0) return {
|
|
7014
7217
|
text: "✓",
|
|
7015
|
-
label: `All ${plural$
|
|
7218
|
+
label: `All ${plural$2(counts.total, noun)} ${verb}`,
|
|
7016
7219
|
done: true
|
|
7017
7220
|
};
|
|
7018
7221
|
if (counts.done === 0) return {
|
|
7019
7222
|
text: String(counts.open),
|
|
7020
|
-
label: plural$
|
|
7223
|
+
label: plural$2(counts.total, noun)
|
|
7021
7224
|
};
|
|
7022
7225
|
return {
|
|
7023
7226
|
text: String(counts.open),
|
|
7024
|
-
label: `${counts.open} of ${plural$
|
|
7227
|
+
label: `${counts.open} of ${plural$2(counts.total, noun)} left, ${counts.done} ${verb}`
|
|
7025
7228
|
};
|
|
7026
7229
|
}
|
|
7027
7230
|
/**
|
|
@@ -7045,9 +7248,9 @@ function findingsBadge(counts) {
|
|
|
7045
7248
|
*/
|
|
7046
7249
|
function themeFindingsNote(counts) {
|
|
7047
7250
|
if (counts.total === 0) return null;
|
|
7048
|
-
if (counts.open === 0) return `all ${plural$
|
|
7049
|
-
if (counts.done === 0) return plural$
|
|
7050
|
-
return `${counts.open} of ${plural$
|
|
7251
|
+
if (counts.open === 0) return `all ${plural$2(counts.total, "issue")} ${doneVerb(counts)}`;
|
|
7252
|
+
if (counts.done === 0) return plural$2(counts.open, "issue");
|
|
7253
|
+
return `${counts.open} of ${plural$2(counts.total, "issue")} left`;
|
|
7051
7254
|
}
|
|
7052
7255
|
/**
|
|
7053
7256
|
* `aria-label` rather than `title`: the same sentence, without a native tooltip sitting on top of
|
|
@@ -7298,6 +7501,7 @@ function renderPromptDialog() {
|
|
|
7298
7501
|
<textarea data-prompt-text rows="5" maxlength="2000" placeholder="Check this change for N+1 database queries."></textarea>
|
|
7299
7502
|
</label>
|
|
7300
7503
|
<p class="prb-dialog-reassure">Saved prompts are yours and stay on this machine. They are offered in every review, and on any text you select.</p>
|
|
7504
|
+
<p class="prb-prompt-error" data-prompt-error role="alert" hidden></p>
|
|
7301
7505
|
<div class="prb-dialog-actions">
|
|
7302
7506
|
<button type="button" class="prb-dialog-danger" data-prompt-delete hidden>Delete</button>
|
|
7303
7507
|
<span class="prb-dialog-spacer"></span>
|
|
@@ -7339,7 +7543,8 @@ function renderAsk(view, sections, paused = false) {
|
|
|
7339
7543
|
${paused ? "" : renderPromptBank(view.prompts ?? [], view.promptsCollapsed === true)}
|
|
7340
7544
|
<div class="prb-ask-log" data-ask-log>
|
|
7341
7545
|
${transcript.length > 0 || pending ? transcript.map((exchange) => `<div class="prb-msg prb-msg-user">${escapeHtml$1(exchange.question)}</div>
|
|
7342
|
-
<div class="prb-msg prb-msg-assistant prb-md">${renderMarkdown(exchange.answer)}${renderMessageCopySource(exchange.answer)}</div
|
|
7546
|
+
<div class="prb-msg prb-msg-assistant prb-md">${renderMarkdown(exchange.answer)}${renderMessageCopySource(exchange.answer)}</div>
|
|
7547
|
+
${paused ? "" : renderTurnActions(exchange, waitingProposals(view.session, exchange.reconciles?.proposals))}`).join("") + pendingTurn : `<div class="prb-ask-empty"${paused ? " inert" : ""}>
|
|
7343
7548
|
<p>Ask about this change. Answers come only from the review, the diff and the repository on disk.</p>
|
|
7344
7549
|
${suggestions.map((text) => `<button type="button" class="prb-suggestion" data-suggestion>${escapeHtml$1(text)}</button>`).join("")}
|
|
7345
7550
|
</div>`}
|
|
@@ -7361,6 +7566,12 @@ function renderAsk(view, sections, paused = false) {
|
|
|
7361
7566
|
* The list is always present, even when empty: an empty bank with an Add button explains what
|
|
7362
7567
|
* the feature is, where a hidden one leaves it undiscoverable until someone reads a changelog.
|
|
7363
7568
|
*/
|
|
7569
|
+
/** Of the suggestions a reconcile made, the ones still waiting on the reviewer on this analysis. */
|
|
7570
|
+
function waitingProposals(session, ids) {
|
|
7571
|
+
if (!ids?.length) return [];
|
|
7572
|
+
const wanted = new Set(ids);
|
|
7573
|
+
return (session.findingReassessments ?? []).filter((entry) => wanted.has(entry.id) && isOffered(session, entry)).map((entry) => entry.id);
|
|
7574
|
+
}
|
|
7364
7575
|
function renderPromptBank(prompts, collapsed = false) {
|
|
7365
7576
|
return `<div class="prb-bank" data-prompt-bank data-prompts="${escapeHtml$1(JSON.stringify(prompts))}"${collapsed ? " data-collapsed" : ""}>
|
|
7366
7577
|
<div class="prb-bank-head">
|
|
@@ -8258,7 +8469,7 @@ function readPrStates() {
|
|
|
8258
8469
|
if (parsed.version !== VERSION$2) return {};
|
|
8259
8470
|
const entries = parsed.entries;
|
|
8260
8471
|
if (!entries || typeof entries !== "object" || Array.isArray(entries)) return {};
|
|
8261
|
-
return normalise$
|
|
8472
|
+
return normalise$2(entries);
|
|
8262
8473
|
} catch {
|
|
8263
8474
|
return {};
|
|
8264
8475
|
}
|
|
@@ -8283,7 +8494,7 @@ function prunePrStates(states, referenced) {
|
|
|
8283
8494
|
* state with no time or a time with no state says nothing, and an unknown state must not be read
|
|
8284
8495
|
* as one this build knows.
|
|
8285
8496
|
*/
|
|
8286
|
-
function normalise$
|
|
8497
|
+
function normalise$2(entries) {
|
|
8287
8498
|
const states = {};
|
|
8288
8499
|
for (const [key, value] of Object.entries(entries)) {
|
|
8289
8500
|
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
@@ -8398,7 +8609,7 @@ function readWatchDocument() {
|
|
|
8398
8609
|
if (!entries || typeof entries !== "object" || Array.isArray(entries)) return null;
|
|
8399
8610
|
return {
|
|
8400
8611
|
version,
|
|
8401
|
-
entries: normalise(entries)
|
|
8612
|
+
entries: normalise$1(entries)
|
|
8402
8613
|
};
|
|
8403
8614
|
} catch {
|
|
8404
8615
|
return null;
|
|
@@ -8411,7 +8622,7 @@ function readWatch() {
|
|
|
8411
8622
|
if (parsed.version !== VERSION) return {};
|
|
8412
8623
|
const entries = parsed.entries;
|
|
8413
8624
|
if (!entries || typeof entries !== "object" || Array.isArray(entries)) return {};
|
|
8414
|
-
return normalise(entries);
|
|
8625
|
+
return normalise$1(entries);
|
|
8415
8626
|
} catch {
|
|
8416
8627
|
return {};
|
|
8417
8628
|
}
|
|
@@ -8452,7 +8663,7 @@ function pruneWatch(watched) {
|
|
|
8452
8663
|
* `notifiedSha` that survived as a number would compare unequal to every real sha and announce on
|
|
8453
8664
|
* every cycle for ever.
|
|
8454
8665
|
*/
|
|
8455
|
-
function normalise(entries) {
|
|
8666
|
+
function normalise$1(entries) {
|
|
8456
8667
|
const state = {};
|
|
8457
8668
|
for (const [id, value] of Object.entries(entries)) {
|
|
8458
8669
|
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
@@ -10186,34 +10397,52 @@ function renderOperation(operation, review, positions, prStates, now, collection
|
|
|
10186
10397
|
* The bulk bar, with its counts left out.
|
|
10187
10398
|
*
|
|
10188
10399
|
* Same division as the pager: every word is written here, where the house style tests read it, and
|
|
10189
|
-
* the
|
|
10190
|
-
*
|
|
10400
|
+
* the labels that carry a number are filled in by the browser because only the browser knows what
|
|
10401
|
+
* is ticked. It ships hidden and complete rather than being built on demand, so the sentence
|
|
10191
10402
|
* somebody is asked to agree to before deleting a dozen reviews is not assembled in a client bundle.
|
|
10192
10403
|
*
|
|
10193
|
-
*
|
|
10194
|
-
*
|
|
10195
|
-
*
|
|
10404
|
+
* Two rows. The head says what is selected and holds the ways to change that: Select all, and the
|
|
10405
|
+
* way out (the X, which the command layer gives its Esc). The actions sit under it, and only once
|
|
10406
|
+
* something is ticked (`data-empty`, written by the client), so an empty bar is a sentence and two
|
|
10407
|
+
* controls rather than a row of disabled buttons.
|
|
10408
|
+
*
|
|
10409
|
+
* In the actions, one filled button for the move most selections are made for, the two other
|
|
10410
|
+
* common ones beside it, and the rest under More, which is the shared menu (`menu.ts`). Mark done
|
|
10411
|
+
* is one of the two and Mark reviewed is under More: reviewed is mostly marked on the review
|
|
10412
|
+
* itself, and finishing several at once is what this list is for. Delete is
|
|
10413
|
+
* last and set apart: this bar is mostly a workflow control and occasionally a destructive one, and
|
|
10414
|
+
* the destructive button should never be the one a hand lands on by default.
|
|
10196
10415
|
*
|
|
10197
10416
|
* The confirmation is a second state of the same bar, which is how the card menus already ask.
|
|
10198
10417
|
*/
|
|
10199
10418
|
function renderBulkBar() {
|
|
10200
|
-
|
|
10201
|
-
|
|
10202
|
-
|
|
10419
|
+
const DONE = LIFECYCLES.find((lifecycle) => lifecycle.id === "done");
|
|
10420
|
+
const icon = (name) => `<span class="prb-bulk-icon" aria-hidden="true">${iconSvg(name)}</span>`;
|
|
10421
|
+
return `<div class="prb-bulk" data-bulk data-empty hidden role="region" aria-label="Selected reviews">
|
|
10422
|
+
<div class="prb-bulk-head">
|
|
10423
|
+
<p class="prb-bulk-status" aria-live="polite"><span class="prb-bulk-count" data-bulk-count></span> <span class="prb-bulk-detail" data-bulk-detail></span></p>
|
|
10203
10424
|
<button type="button" class="prb-bulk-button" data-bulk-select-all>${escapeHtml$1(selectAllLabel(0))}</button>
|
|
10425
|
+
<button type="button" class="prb-bulk-exit" data-bulk-exit aria-label="Exit selection">${icon("close")}</button>
|
|
10426
|
+
</div>
|
|
10427
|
+
<div class="prb-bulk-actions" data-bulk-actions>
|
|
10428
|
+
<button type="button" class="prb-bulk-button prb-bulk-primary" data-bulk-queue disabled>${icon("queue-add")}Add to queue</button>
|
|
10429
|
+
<button type="button" class="prb-bulk-button" data-bulk-collections disabled>${icon("folder")}Collections</button>
|
|
10430
|
+
<button type="button" class="prb-bulk-button" data-bulk-lifecycle="${DONE.id}">${icon("check-double")}${escapeHtml$1(DONE.action)}</button>
|
|
10204
10431
|
<button type="button" class="prb-bulk-button" data-bulk-fetch hidden>${escapeHtml$1(FETCH_SELECTED_LABEL)}</button>
|
|
10205
|
-
<
|
|
10206
|
-
|
|
10207
|
-
|
|
10208
|
-
|
|
10209
|
-
|
|
10210
|
-
|
|
10211
|
-
|
|
10432
|
+
<div class="prb-menu prb-bulk-more" data-menu data-bulk-more>
|
|
10433
|
+
<button type="button" class="prb-bulk-button" data-menu-trigger aria-expanded="false" aria-haspopup="true">${icon("more")}More</button>
|
|
10434
|
+
<div class="prb-menu-panel" data-menu-panel hidden>
|
|
10435
|
+
<button type="button" class="prb-menu-item" data-bulk-mark="complete">${markItemLabel("complete")}</button>
|
|
10436
|
+
<button type="button" class="prb-menu-item" data-bulk-mark="in-progress">${markItemLabel("in-progress")}</button>
|
|
10437
|
+
${LIFECYCLES.filter((lifecycle) => lifecycle !== DONE).map((lifecycle) => `<button type="button" class="prb-menu-item" data-bulk-lifecycle="${lifecycle.id}">${escapeHtml$1(lifecycle.action)}</button>`).join("\n ")}
|
|
10438
|
+
</div>
|
|
10439
|
+
</div>
|
|
10440
|
+
<button type="button" class="prb-bulk-button prb-bulk-button-danger" data-bulk-delete>${icon("trash")}<span data-bulk-delete-label></span></button>
|
|
10212
10441
|
</div>
|
|
10213
10442
|
<div class="prb-bulk-confirm" data-bulk-confirm hidden>
|
|
10214
10443
|
<p class="prb-bulk-question" data-bulk-question></p>
|
|
10215
10444
|
<p class="prb-bulk-note">This cannot be undone. Your branches and their commits are untouched.</p>
|
|
10216
|
-
<div class="prb-bulk-actions">
|
|
10445
|
+
<div class="prb-bulk-confirm-actions">
|
|
10217
10446
|
<button type="button" class="prb-bulk-button prb-bulk-button-danger" data-bulk-confirm-delete>Delete</button>
|
|
10218
10447
|
<button type="button" class="prb-bulk-button" data-bulk-cancel-delete>Cancel</button>
|
|
10219
10448
|
</div>
|
|
@@ -11201,7 +11430,7 @@ function renderPrivacySection(settings) {
|
|
|
11201
11430
|
* A record with no facts -- every one written before 0.74.0 -- gets no line, and its row looks as
|
|
11202
11431
|
* it always did.
|
|
11203
11432
|
*/
|
|
11204
|
-
var plural = (count, one, many = `${one}s`) => `${count} ${count === 1 ? one : many}`;
|
|
11433
|
+
var plural$1 = (count, one, many = `${one}s`) => `${count} ${count === 1 ? one : many}`;
|
|
11205
11434
|
function shortSha(sha) {
|
|
11206
11435
|
return sha.slice(0, 7);
|
|
11207
11436
|
}
|
|
@@ -11213,15 +11442,15 @@ function headMoveLine(notification, facts) {
|
|
|
11213
11442
|
const where = notification.source === "branch" ? "locally" : "on the pull request";
|
|
11214
11443
|
const parts = [`${step(facts.fromSha, notification.headSha)} ${where}`];
|
|
11215
11444
|
if (facts.rewritten) parts.push("branch rewritten since the review");
|
|
11216
|
-
else if (typeof facts.behind === "number" && facts.behind > 0) parts.push(`review ${plural(facts.behind, "commit")} behind`);
|
|
11445
|
+
else if (typeof facts.behind === "number" && facts.behind > 0) parts.push(`review ${plural$1(facts.behind, "commit")} behind`);
|
|
11217
11446
|
else parts.push("review behind");
|
|
11218
11447
|
return parts.join(" · ");
|
|
11219
11448
|
}
|
|
11220
11449
|
/** The card's own words for what is left (`whatIsLeft`), so a row and a card never disagree in vocabulary. */
|
|
11221
11450
|
function reviewLine(facts) {
|
|
11222
11451
|
const parts = [];
|
|
11223
|
-
if (facts.openIssues > 0) parts.push(plural(facts.openIssues, "issue"));
|
|
11224
|
-
if (facts.unansweredQuestions > 0) parts.push(plural(facts.unansweredQuestions, "unanswered question"));
|
|
11452
|
+
if (facts.openIssues > 0) parts.push(plural$1(facts.openIssues, "issue"));
|
|
11453
|
+
if (facts.unansweredQuestions > 0) parts.push(plural$1(facts.unansweredQuestions, "unanswered question"));
|
|
11225
11454
|
if (parts.length === 0) parts.push("Nothing flagged");
|
|
11226
11455
|
if (facts.risk) parts.push(`${facts.risk} risk`);
|
|
11227
11456
|
return parts.join(" · ");
|
|
@@ -11244,8 +11473,8 @@ function updateLine(facts) {
|
|
|
11244
11473
|
const delta = updateDelta(facts);
|
|
11245
11474
|
parts.push(...delta.length > 0 ? delta : ["findings unchanged"]);
|
|
11246
11475
|
if (facts.stillOpen > 0) parts.push(`${facts.stillOpen} still open`);
|
|
11247
|
-
if (facts.questionsAnswered > 0) parts.push(`${plural(facts.questionsAnswered, "question")} answered`);
|
|
11248
|
-
if (facts.unansweredQuestions > 0) parts.push(plural(facts.unansweredQuestions, "unanswered question"));
|
|
11476
|
+
if (facts.questionsAnswered > 0) parts.push(`${plural$1(facts.questionsAnswered, "question")} answered`);
|
|
11477
|
+
if (facts.unansweredQuestions > 0) parts.push(plural$1(facts.unansweredQuestions, "unanswered question"));
|
|
11249
11478
|
if (facts.fetchFailed) parts.push("remote not reached");
|
|
11250
11479
|
else if (facts.upToDate) parts.push("up to date");
|
|
11251
11480
|
return parts.join(" · ");
|
|
@@ -11281,7 +11510,7 @@ function toastFact(notification) {
|
|
|
11281
11510
|
return delta.length > 0 ? delta.slice(0, 2).join(", ") : "findings unchanged";
|
|
11282
11511
|
}
|
|
11283
11512
|
const facts = notification.facts;
|
|
11284
|
-
return facts.openIssues > 0 ? plural(facts.openIssues, "issue") : facts.unansweredQuestions > 0 ? void 0 : "nothing flagged";
|
|
11513
|
+
return facts.openIssues > 0 ? plural$1(facts.openIssues, "issue") : facts.unansweredQuestions > 0 ? void 0 : "nothing flagged";
|
|
11285
11514
|
}
|
|
11286
11515
|
/**
|
|
11287
11516
|
* A toast's whole line: what happened, to what, and at most one fact.
|
|
@@ -11652,6 +11881,26 @@ feature: E</pre>
|
|
|
11652
11881
|
<li><b>Delete</b> removes a review and the checkouts it was made in. See
|
|
11653
11882
|
<a href="#reviews-and-ids">Reviews and review IDs</a>.</li>
|
|
11654
11883
|
</ul>`
|
|
11884
|
+
},
|
|
11885
|
+
{
|
|
11886
|
+
id: "author-replies",
|
|
11887
|
+
title: "Asking the author",
|
|
11888
|
+
body: `<p>Every issue and question has a reference, such as <b>I3</b> or <b>Q2</b>, shown on its
|
|
11889
|
+
card. <b>Prepare feedback</b> on Issues or Questions turns the ones you choose into a draft to send
|
|
11890
|
+
the author, each led by its reference, so the author can answer “Q2: …”.</p>
|
|
11891
|
+
<p>When the reply comes back, paste it into Buddy AI as one message and press <b>Reconcile with
|
|
11892
|
+
findings</b> under it. Only that message is read. Buddy AI reads it against the review and
|
|
11893
|
+
suggests what it settles: a question answered or partly answered, an issue resolved, or one that
|
|
11894
|
+
needs investigating. Each suggestion appears on its card with the words it rests on, and nothing
|
|
11895
|
+
changes until you accept it.</p>
|
|
11896
|
+
<p>The draft ends with the review it came from. A reply naming a different review, such as one
|
|
11897
|
+
analysed afresh since, is not reconciled, because its references number different findings.</p>
|
|
11898
|
+
<p>Buddy AI is asked to read the code before suggesting an issue is resolved, and to cite the lines
|
|
11899
|
+
that show it. The review checks that the cited files exist, not that the code fixes anything, so
|
|
11900
|
+
read the cited lines before you accept. A fix the author has not pushed, or has only promised, is
|
|
11901
|
+
never suggested as resolved. A partly answered question and an issue that needs investigating stay
|
|
11902
|
+
open. If a later reply or new commits contradict an answer you accepted, you are asked again
|
|
11903
|
+
rather than the answer being undone.</p>`
|
|
11655
11904
|
},
|
|
11656
11905
|
{
|
|
11657
11906
|
id: "reviews-and-ids",
|
|
@@ -11670,8 +11919,9 @@ feature: E</pre>
|
|
|
11670
11919
|
id: "collections",
|
|
11671
11920
|
title: "Collections",
|
|
11672
11921
|
body: `<p>Collections help you organise related reviews, such as a refactor, release or feature.</p>
|
|
11673
|
-
<p>Create a collection from the Reviews page, then add reviews individually
|
|
11674
|
-
|
|
11922
|
+
<p>Create a collection from the Reviews page, then add reviews individually with <b>Manage
|
|
11923
|
+
collections</b> in a review's menu, or select several and use <b>Collections</b> in the selection
|
|
11924
|
+
bar. A review can belong to multiple collections.</p>
|
|
11675
11925
|
<p>Collections show reviews that still need attention. When you mark a review <b>Done</b>, it moves
|
|
11676
11926
|
under <b>Show completed</b> within its collection, without losing its membership.</p>
|
|
11677
11927
|
<p>You can collapse collections to stay focused, and expand them when you're ready to continue.
|
|
@@ -13769,6 +14019,7 @@ function gradeFragments$1(session, kind, id) {
|
|
|
13769
14019
|
const finding = findingIn(session, kind, id);
|
|
13770
14020
|
if (!finding) return null;
|
|
13771
14021
|
const assessment = findingAssessment(session.findingReassessments, kind, finding, analysisRefOf(session));
|
|
14022
|
+
const status = findingStatus(session.findingReassessments, kind, id, analysisRefOf(session));
|
|
13772
14023
|
const options = {
|
|
13773
14024
|
editable: true,
|
|
13774
14025
|
askMore: true
|
|
@@ -13779,8 +14030,9 @@ function gradeFragments$1(session, kind, id) {
|
|
|
13779
14030
|
current: assessment.current,
|
|
13780
14031
|
cardClass: cardGradeClass(kind, assessment.current),
|
|
13781
14032
|
grade: renderGradeControl(kind, assessment, options),
|
|
13782
|
-
region: renderAssessmentRegion(kind, assessment, options),
|
|
13783
|
-
copy: kind === "issue" ? issueMarkdown(finding, assessment) : questionMarkdown(finding, assessment)
|
|
14033
|
+
region: renderAssessmentRegion(kind, assessment, options, status),
|
|
14034
|
+
copy: kind === "issue" ? issueMarkdown(finding, assessment) : questionMarkdown(finding, assessment),
|
|
14035
|
+
verdictClosed: closesFinding(status.current)
|
|
13784
14036
|
};
|
|
13785
14037
|
}
|
|
13786
14038
|
/** The finding a request is about: named outright, or through the proposal it decides. */
|
|
@@ -13862,7 +14114,7 @@ function applyReassessmentRequest(workspaceId, value, now = Date.now) {
|
|
|
13862
14114
|
//#endregion
|
|
13863
14115
|
//#region ../../packages/review-harness/src/domain/ai_review/reassess_prompt.ts
|
|
13864
14116
|
/** More than any real answer cites; a list longer than this is padding, not evidence. */
|
|
13865
|
-
var MAX_EVIDENCE = 10;
|
|
14117
|
+
var MAX_EVIDENCE$1 = 10;
|
|
13866
14118
|
function scaleText(kind) {
|
|
13867
14119
|
return gradeScale(kind).map((grade) => `"${grade}"`).join(" | ");
|
|
13868
14120
|
}
|
|
@@ -13914,7 +14166,7 @@ function buildReassessInstructions(input) {
|
|
|
13914
14166
|
];
|
|
13915
14167
|
}
|
|
13916
14168
|
var REASSESS_ANSWER = objectWithAnyOf("verdict", "reason");
|
|
13917
|
-
function readEvidence(value) {
|
|
14169
|
+
function readEvidence$1(value) {
|
|
13918
14170
|
if (!Array.isArray(value)) return [];
|
|
13919
14171
|
const out = [];
|
|
13920
14172
|
for (const item of value) {
|
|
@@ -13926,7 +14178,7 @@ function readEvidence(value) {
|
|
|
13926
14178
|
file,
|
|
13927
14179
|
line
|
|
13928
14180
|
});
|
|
13929
|
-
if (out.length === MAX_EVIDENCE) break;
|
|
14181
|
+
if (out.length === MAX_EVIDENCE$1) break;
|
|
13930
14182
|
}
|
|
13931
14183
|
return out;
|
|
13932
14184
|
}
|
|
@@ -13947,7 +14199,7 @@ function parseReassessAnswer(raw, kind, current) {
|
|
|
13947
14199
|
const obj = parsed;
|
|
13948
14200
|
const reason = typeof obj.reason === "string" ? obj.reason.trim() : "";
|
|
13949
14201
|
if (!reason) return null;
|
|
13950
|
-
const evidence = readEvidence(obj.evidence);
|
|
14202
|
+
const evidence = readEvidence$1(obj.evidence);
|
|
13951
14203
|
if (obj.verdict === "keep") return {
|
|
13952
14204
|
verdict: "keep",
|
|
13953
14205
|
reason,
|
|
@@ -14030,6 +14282,267 @@ function reassessTurnText(target, turn, outcome) {
|
|
|
14030
14282
|
const move = `**Suggests ${from} → ${answer.to}.** ${answer.reason}`;
|
|
14031
14283
|
return outcome === "stale" ? `${move}\n\n*The grade changed while this was being answered, so the suggestion was not kept. Reassess again to judge it as it stands.*` : `${move}\n\n*Accept or keep it on the card.*`;
|
|
14032
14284
|
}
|
|
14285
|
+
/** More than any finding a real reply rests on; past this it is padding. */
|
|
14286
|
+
var MAX_QUOTES = 4;
|
|
14287
|
+
var MAX_EVIDENCE = 10;
|
|
14288
|
+
/** The turns, labelled in the order the reviewer wrote them. */
|
|
14289
|
+
function labelTurns(turns) {
|
|
14290
|
+
return [...turns].sort((a, b) => a.askedAt - b.askedAt).map((turn, index) => ({
|
|
14291
|
+
label: `A${index + 1}`,
|
|
14292
|
+
askedAt: turn.askedAt,
|
|
14293
|
+
text: turn.question
|
|
14294
|
+
}));
|
|
14295
|
+
}
|
|
14296
|
+
function issueLine(issue, index, assess, status) {
|
|
14297
|
+
const where = issue.file ? ` [${issue.file}${issue.line ? `:${issue.line}` : ""}]` : "";
|
|
14298
|
+
const severity = ` [severity: ${promptGrade(assess("issue", issue))}]`;
|
|
14299
|
+
return `${findingRef("issue", index)}.${severity}${where}${priorStatus(issue.reconciliationStatus === "resolved" ? "resolved by the code" : void 0)}${acceptedLine(status("issue", issue.id))} ${issue.title.trim()}\n ${issue.description.trim().replace(/\n/g, "\n ")}`;
|
|
14300
|
+
}
|
|
14301
|
+
function questionLine(question, index, assess, status, ticked) {
|
|
14302
|
+
const importance = assess("question", question).current ? ` [importance: ${promptGrade(assess("question", question))}]` : "";
|
|
14303
|
+
const where = question.file ? ` [${question.file}]` : "";
|
|
14304
|
+
const why = question.whyThisMatters?.trim() ? `\n ${question.whyThisMatters.trim()}` : "";
|
|
14305
|
+
const prior = question.reconciliationStatus === "answered" ? "answered by the code" : ticked ? "marked answered by the reviewer" : void 0;
|
|
14306
|
+
return `${findingRef("question", index)}.${importance}${where}${priorStatus(prior)}${acceptedLine(status("question", question.id))} ${question.question.trim()}${why}`;
|
|
14307
|
+
}
|
|
14308
|
+
function priorStatus(text) {
|
|
14309
|
+
return text ? ` [${text}]` : "";
|
|
14310
|
+
}
|
|
14311
|
+
/**
|
|
14312
|
+
* The verdict the reviewer accepted earlier, said as their decision and not as a fact, with what it
|
|
14313
|
+
* rested on, so evidence that contradicts it is recognised as a contradiction.
|
|
14314
|
+
*/
|
|
14315
|
+
function acceptedLine(status) {
|
|
14316
|
+
if (!status.current) return "";
|
|
14317
|
+
const last = status.history[status.history.length - 1];
|
|
14318
|
+
return ` [the reviewer accepted "${status.current}" ${sourcePhrase(last.source)}${last.reason ? `: ${last.reason}` : ""}]`;
|
|
14319
|
+
}
|
|
14320
|
+
var SHAPE = `{
|
|
14321
|
+
"findings": [
|
|
14322
|
+
{
|
|
14323
|
+
"ref": string,
|
|
14324
|
+
"status": "answered" | "partially-answered" | "still-open" | "resolved" | "needs-investigation",
|
|
14325
|
+
"reason": string,
|
|
14326
|
+
"answer"?: string,
|
|
14327
|
+
"basis"?: "premise" | "code",
|
|
14328
|
+
"evidence"?: [{ "file": string, "line"?: number }],
|
|
14329
|
+
"quotes": [{ "turn": string, "text": string }],
|
|
14330
|
+
"severity"?: ${gradeScale("issue").map((grade) => `"${grade}"`).join(" | ")},
|
|
14331
|
+
"importance"?: ${gradeScale("question").map((grade) => `"${grade}"`).join(" | ")},
|
|
14332
|
+
"gradeReason"?: string
|
|
14333
|
+
}
|
|
14334
|
+
],
|
|
14335
|
+
"unmatched": [{ "turn": string, "text": string, "candidates": string[], "why": string }]
|
|
14336
|
+
}`;
|
|
14337
|
+
/** The block a reconcile turn adds to Buddy AI's prompt: the turns, the findings, the rules and the shape. */
|
|
14338
|
+
function buildReplyReconciliationInstructions(input) {
|
|
14339
|
+
const { result, assess, status } = input;
|
|
14340
|
+
const issues = result.issues.length ? result.issues.map((issue, index) => issueLine(issue, index, assess, status)).join("\n") : "(The review raised no issues.)";
|
|
14341
|
+
const questions = result.questions.length ? result.questions.map((question, index) => questionLine(question, index, assess, status, input.answeredQuestionIds.has(question.id))).join("\n") : "(The review raised no questions.)";
|
|
14342
|
+
return [
|
|
14343
|
+
"---",
|
|
14344
|
+
"RECONCILE WITH FINDINGS",
|
|
14345
|
+
"The reviewer has asked you to read the turns below, which they wrote in this conversation, as evidence about the review's existing issues and questions. Most often they are an author's replies to feedback the reviewer sent, pasted in. Nothing you return changes a finding: each judgement becomes a suggestion the reviewer accepts or rejects on its own.",
|
|
14346
|
+
"",
|
|
14347
|
+
"THE TURNS (the reviewer's own messages, labelled; Buddy AI's answers are not evidence):",
|
|
14348
|
+
...input.turns.map((turn) => `[${turn.label}]\n${turn.text.trim()}`),
|
|
14349
|
+
"",
|
|
14350
|
+
input.review.named ? `The turns name this review (${input.review.tag}), so the references in them are this review's.` : `The turns do not name this review (${input.review.tag}). A reference in them may come from a draft of an earlier analysis whose findings were numbered differently: use a reference only where what the passage says matches that finding, and otherwise treat the passage as unmatched.`,
|
|
14351
|
+
"",
|
|
14352
|
+
"THE FINDINGS, by the reference the review's cards and its feedback drafts use:",
|
|
14353
|
+
"Issues:",
|
|
14354
|
+
issues,
|
|
14355
|
+
"Questions:",
|
|
14356
|
+
questions,
|
|
14357
|
+
"",
|
|
14358
|
+
"RULES",
|
|
14359
|
+
"- Match a passage to a finding by its reference where it names one (\"Q2:\", \"I3.\"), and otherwise only by what it unmistakably addresses. Never match a passage in order to produce a judgement. Where a passage could be about more than one finding, or you cannot tell which it answers, put it in \"unmatched\" with its candidate references and leave those findings out. The turns need not follow the order of the findings, one passage may address several findings, and one finding may be addressed across several turns.",
|
|
14360
|
+
"- Include only the findings the turns address. Leave every other finding out: an omitted finding is one the turns do not bear on, not one that is still open.",
|
|
14361
|
+
"- A question is \"answered\" only when the turns answer its substance. A reply that acknowledges it, defers it, restates it or answers a different question does not answer it. Where they answer part of it, use \"partially-answered\" and say in \"answer\" what they establish and what is still missing. Where they address it without answering it, use \"still-open\". For \"answered\" and \"partially-answered\", put the answer itself in \"answer\", in a sentence or two.",
|
|
14362
|
+
"- What an author says about an issue is one of three things, and they are judged differently:",
|
|
14363
|
+
" 1. The concern never held: an assumption the issue made is false (the code concerned is unreachable, is being removed, or a requirement it assumed does not exist). That may be \"resolved\" with \"basis\": \"premise\". Check the claim against the code wherever the code can settle it, and where the code contradicts it, it is not resolved.",
|
|
14364
|
+
" 2. It is already implemented: the code at the reviewed commit is said to handle it. That may be \"resolved\" with \"basis\": \"code\" only after you have opened that code and read it, and only when it addresses this concern, not merely when the file exists or touches the same area. List in \"evidence\" each place, with its line, where the code does so. A file name on its own is not evidence, and neither is code that exists but does not address the concern.",
|
|
14365
|
+
" 3. It is fixed somewhere you cannot read, or will be: fixed locally, in another branch, in a commit not yet pushed, or planned. That is a claim about code, not evidence, and never \"resolved\". Use \"needs-investigation\" where the claim deserves checking once the code exists, otherwise \"still-open\". Agreeing that it is a problem, with a plan to fix it later, is \"still-open\".",
|
|
14366
|
+
"- \"needs-investigation\": the turns raise something that bears on the issue and that only further reading or a further question can settle. Say what in \"reason\".",
|
|
14367
|
+
"- Treat what the author asserts as context, not proof. Where you can check a claim in the repository, check it before relying on it.",
|
|
14368
|
+
"- A verdict the reviewer accepted earlier is marked on its finding. It was their decision, not a fact. Where the turns contradict it, report the finding as \"still-open\", with the contradiction in \"reason\", so it is put to them.",
|
|
14369
|
+
"- Every judgement must quote, in \"quotes\", the exact words from the turns it rests on, copied character for character (a sentence or two each), with the turn's label. A judgement whose words cannot be found in the turn it names is discarded.",
|
|
14370
|
+
`- "severity" on an issue or "importance" on a question is optional: only where the turns change how much the finding matters, never the grade it already has, and always with "gradeReason".`,
|
|
14371
|
+
"- In \"reason\" and \"answer\", name the author by role (\"the author confirms the job is retired\"), and write no references or labels: the reviewer reads those sentences beside the finding.",
|
|
14372
|
+
"",
|
|
14373
|
+
"Respond with a single JSON object (no markdown, no surrounding text) matching exactly this shape:",
|
|
14374
|
+
SHAPE,
|
|
14375
|
+
"\"status\" on a question (a Q reference) is \"answered\", \"partially-answered\" or \"still-open\"; on an issue (an I reference) it is \"resolved\", \"needs-investigation\" or \"still-open\". \"basis\" and \"evidence\" go only with \"resolved\". \"severity\" goes only on an issue and \"importance\" only on a question.",
|
|
14376
|
+
"Empty arrays are valid and far better than judgements the turns do not support."
|
|
14377
|
+
];
|
|
14378
|
+
}
|
|
14379
|
+
var QUESTION_STATUSES = [
|
|
14380
|
+
"answered",
|
|
14381
|
+
"partially-answered",
|
|
14382
|
+
"still-open"
|
|
14383
|
+
];
|
|
14384
|
+
var ISSUE_STATUSES = [
|
|
14385
|
+
"resolved",
|
|
14386
|
+
"needs-investigation",
|
|
14387
|
+
"still-open"
|
|
14388
|
+
];
|
|
14389
|
+
var ANSWER = objectWithAnyOf("findings", "unmatched");
|
|
14390
|
+
/** Whitespace and quote marks as a model may retype them: two texts are the same words if these agree. */
|
|
14391
|
+
function normalise(text) {
|
|
14392
|
+
return text.replace(/[‘’]/g, "'").replace(/[“”]/g, "\"").replace(/\s+/g, " ").trim();
|
|
14393
|
+
}
|
|
14394
|
+
/** The quotes found, word for word, in the turns they name. Nothing else is kept. */
|
|
14395
|
+
function verifiedQuotes(value, turns) {
|
|
14396
|
+
if (!Array.isArray(value)) return [];
|
|
14397
|
+
const byLabel = new Map(turns.map((turn) => [turn.label.toUpperCase(), turn]));
|
|
14398
|
+
const kept = [];
|
|
14399
|
+
for (const item of value) {
|
|
14400
|
+
const quote = item;
|
|
14401
|
+
if (typeof quote?.turn !== "string" || typeof quote.text !== "string") continue;
|
|
14402
|
+
const turn = byLabel.get(quote.turn.trim().replace(/^\[|\]$/g, "").toUpperCase());
|
|
14403
|
+
const text = normalise(quote.text);
|
|
14404
|
+
if (!turn || !text || text.length > 600) continue;
|
|
14405
|
+
if (!normalise(turn.text).includes(text)) continue;
|
|
14406
|
+
if (kept.some((existing) => existing.askedAt === turn.askedAt && existing.text === text)) continue;
|
|
14407
|
+
kept.push({
|
|
14408
|
+
askedAt: turn.askedAt,
|
|
14409
|
+
text
|
|
14410
|
+
});
|
|
14411
|
+
if (kept.length === MAX_QUOTES) break;
|
|
14412
|
+
}
|
|
14413
|
+
return kept;
|
|
14414
|
+
}
|
|
14415
|
+
function readEvidence(value) {
|
|
14416
|
+
if (!Array.isArray(value)) return [];
|
|
14417
|
+
return value.flatMap((item) => {
|
|
14418
|
+
const place = item;
|
|
14419
|
+
if (typeof place?.file !== "string" || !place.file.trim()) return [];
|
|
14420
|
+
const line = typeof place.line === "number" && Number.isInteger(place.line) && place.line > 0 ? place.line : void 0;
|
|
14421
|
+
return [{
|
|
14422
|
+
file: place.file.trim(),
|
|
14423
|
+
...line ? { line } : {}
|
|
14424
|
+
}];
|
|
14425
|
+
}).slice(0, MAX_EVIDENCE);
|
|
14426
|
+
}
|
|
14427
|
+
function text(value) {
|
|
14428
|
+
return typeof value === "string" ? value.trim() : "";
|
|
14429
|
+
}
|
|
14430
|
+
/**
|
|
14431
|
+
* The answer, read strictly. Null when it is not an answer at all; otherwise every judgement that
|
|
14432
|
+
* names a finding the review holds, with a status on that finding's scale, a reason, and at least one
|
|
14433
|
+
* quote found in the turns. A judgement missing any of those is counted and dropped, never repaired.
|
|
14434
|
+
*/
|
|
14435
|
+
function parseReplyReconciliation(raw, result, turns) {
|
|
14436
|
+
let parsed;
|
|
14437
|
+
try {
|
|
14438
|
+
parsed = JSON.parse(extractJsonText(raw, ANSWER));
|
|
14439
|
+
} catch {
|
|
14440
|
+
return null;
|
|
14441
|
+
}
|
|
14442
|
+
if (!ANSWER(parsed)) return null;
|
|
14443
|
+
const obj = parsed;
|
|
14444
|
+
const discarded = {
|
|
14445
|
+
unknownRef: 0,
|
|
14446
|
+
unquoted: 0
|
|
14447
|
+
};
|
|
14448
|
+
const judgements = [];
|
|
14449
|
+
const seen = /* @__PURE__ */ new Set();
|
|
14450
|
+
for (const item of Array.isArray(obj.findings) ? obj.findings : []) {
|
|
14451
|
+
const entry = item;
|
|
14452
|
+
const at = typeof entry?.ref === "string" ? findingAtRef(result, entry.ref) : null;
|
|
14453
|
+
if (!at) {
|
|
14454
|
+
discarded.unknownRef += 1;
|
|
14455
|
+
continue;
|
|
14456
|
+
}
|
|
14457
|
+
const allowed = at.kind === "issue" ? ISSUE_STATUSES : QUESTION_STATUSES;
|
|
14458
|
+
const status = entry.status;
|
|
14459
|
+
const reason = text(entry.reason);
|
|
14460
|
+
if (typeof status !== "string" || !allowed.includes(status) || !reason) {
|
|
14461
|
+
discarded.unknownRef += 1;
|
|
14462
|
+
continue;
|
|
14463
|
+
}
|
|
14464
|
+
const quotes = verifiedQuotes(entry.quotes, turns);
|
|
14465
|
+
if (!quotes.length) {
|
|
14466
|
+
discarded.unquoted += 1;
|
|
14467
|
+
continue;
|
|
14468
|
+
}
|
|
14469
|
+
const key = `${at.kind}:${at.index}`;
|
|
14470
|
+
if (seen.has(key)) continue;
|
|
14471
|
+
seen.add(key);
|
|
14472
|
+
const answer = text(entry.answer);
|
|
14473
|
+
const basis = entry.basis === "premise" || entry.basis === "code" ? entry.basis : void 0;
|
|
14474
|
+
const gradeTo = at.kind === "issue" ? entry.severity : entry.importance;
|
|
14475
|
+
const gradeReason = text(entry.gradeReason);
|
|
14476
|
+
judgements.push({
|
|
14477
|
+
kind: at.kind,
|
|
14478
|
+
index: at.index,
|
|
14479
|
+
findingId: at.finding.id,
|
|
14480
|
+
ref: findingRef(at.kind, at.index),
|
|
14481
|
+
status,
|
|
14482
|
+
reason,
|
|
14483
|
+
...answer ? { answer } : {},
|
|
14484
|
+
...basis ? { basis } : {},
|
|
14485
|
+
evidence: readEvidence(entry.evidence),
|
|
14486
|
+
quotes,
|
|
14487
|
+
...isGradeFor(at.kind, gradeTo) && gradeReason ? { grade: {
|
|
14488
|
+
to: gradeTo,
|
|
14489
|
+
reason: gradeReason
|
|
14490
|
+
} } : {}
|
|
14491
|
+
});
|
|
14492
|
+
}
|
|
14493
|
+
return {
|
|
14494
|
+
judgements,
|
|
14495
|
+
unmatched: (Array.isArray(obj.unmatched) ? obj.unmatched : []).flatMap((item) => {
|
|
14496
|
+
const passage = item;
|
|
14497
|
+
const words = text(passage?.text);
|
|
14498
|
+
if (!words) return [];
|
|
14499
|
+
const candidates = (Array.isArray(passage.candidates) ? passage.candidates : []).filter((ref) => typeof ref === "string" && findingAtRef(result, ref) !== null).map((ref) => ref.trim().toUpperCase());
|
|
14500
|
+
return [{
|
|
14501
|
+
text: words.slice(0, 600),
|
|
14502
|
+
candidates,
|
|
14503
|
+
why: text(passage.why)
|
|
14504
|
+
}];
|
|
14505
|
+
}),
|
|
14506
|
+
discarded
|
|
14507
|
+
};
|
|
14508
|
+
}
|
|
14509
|
+
/** Said beside a resolution the harness could not let stand as one. */
|
|
14510
|
+
var UNSUPPORTED_RESOLUTION = "Treated as needing investigation: the reply was read as resolving it, but cited no line of the reviewed code that shows it.";
|
|
14511
|
+
/**
|
|
14512
|
+
* A resolution, checked against what it says it rests on, as far as the harness can check it.
|
|
14513
|
+
*
|
|
14514
|
+
* `premise` stands as the model judged it. `code` stands only with at least one cited place, with a
|
|
14515
|
+
* line, in a file that exists at the reviewed commit (`existing` is the set of paths found there).
|
|
14516
|
+
* Anything else is downgraded to "needs investigation", with the reason saying so.
|
|
14517
|
+
*
|
|
14518
|
+
* This is citation validation, not verification: a file that exists does not show that the code in it
|
|
14519
|
+
* fixes anything. Whether it does is the model's reading, which the prompt requires it to make by
|
|
14520
|
+
* opening the code, and the reviewer's to confirm before accepting. The card says as much.
|
|
14521
|
+
*/
|
|
14522
|
+
function groundResolution(judgement, existing) {
|
|
14523
|
+
if (judgement.kind !== "issue" || judgement.status !== "resolved") return judgement;
|
|
14524
|
+
const evidence = judgement.evidence.filter((place) => existing.has(place.file));
|
|
14525
|
+
if (judgement.basis === "premise") return {
|
|
14526
|
+
...judgement,
|
|
14527
|
+
evidence
|
|
14528
|
+
};
|
|
14529
|
+
if (judgement.basis === "code" && evidence.some((place) => place.line !== void 0)) return {
|
|
14530
|
+
...judgement,
|
|
14531
|
+
evidence
|
|
14532
|
+
};
|
|
14533
|
+
return {
|
|
14534
|
+
...judgement,
|
|
14535
|
+
status: "needs-investigation",
|
|
14536
|
+
evidence,
|
|
14537
|
+
reason: `${judgement.reason} ${UNSUPPORTED_RESOLUTION}`
|
|
14538
|
+
};
|
|
14539
|
+
}
|
|
14540
|
+
/** A proposal a judgement becomes on the status axis, or null when it is not one. */
|
|
14541
|
+
function verdictFor(judgement, current) {
|
|
14542
|
+
if (judgement.status !== "still-open") return judgement.status;
|
|
14543
|
+
if (!closesFinding(current)) return null;
|
|
14544
|
+
return judgement.kind === "issue" ? "open" : "unanswered";
|
|
14545
|
+
}
|
|
14033
14546
|
//#endregion
|
|
14034
14547
|
//#region ../../packages/review-harness/src/workspace/ask.ts
|
|
14035
14548
|
/** Long enough for a real look at the repository; short enough that a hang is not forever. */
|
|
@@ -14144,6 +14657,7 @@ function buildPrompt$2(workspace, question, sectionId, cwd, context = {}) {
|
|
|
14144
14657
|
"REVIEW GUIDE (JSON):",
|
|
14145
14658
|
JSON.stringify(workspace.session.result),
|
|
14146
14659
|
...reassessedGrades(workspace),
|
|
14660
|
+
...acceptedStatuses(workspace),
|
|
14147
14661
|
"---",
|
|
14148
14662
|
"DIFF:",
|
|
14149
14663
|
workspace.diff,
|
|
@@ -14161,6 +14675,7 @@ function buildPrompt$2(workspace, question, sectionId, cwd, context = {}) {
|
|
|
14161
14675
|
...continuingFrom(workspace, context.continuing),
|
|
14162
14676
|
...explainingFinding(workspace, context.explaining),
|
|
14163
14677
|
...reassessingFinding(workspace, context.reassessing),
|
|
14678
|
+
...reconcilingTurns(workspace, context.reconciling),
|
|
14164
14679
|
"---",
|
|
14165
14680
|
`QUESTION: ${question}`
|
|
14166
14681
|
].join("\n");
|
|
@@ -14224,6 +14739,33 @@ function reassessedGrades(workspace) {
|
|
|
14224
14739
|
];
|
|
14225
14740
|
}
|
|
14226
14741
|
/**
|
|
14742
|
+
* The statuses the reviewer has accepted from later evidence, such as an author's reply. Listed only
|
|
14743
|
+
* when there are some, as the reviewer's decisions rather than facts, so a turn neither re-asks a
|
|
14744
|
+
* question they have accepted an answer to nor treats that answer as beyond question.
|
|
14745
|
+
*/
|
|
14746
|
+
function acceptedStatuses(workspace) {
|
|
14747
|
+
const result = workspace.session.result;
|
|
14748
|
+
if (!result) return [];
|
|
14749
|
+
const status = statusLookup(workspace.session);
|
|
14750
|
+
const lines = [...result.issues.map((issue) => ({
|
|
14751
|
+
name: `ISSUE "${issue.title.trim()}"`,
|
|
14752
|
+
status: status("issue", issue.id)
|
|
14753
|
+
})), ...result.questions.map((q) => ({
|
|
14754
|
+
name: `QUESTION "${q.question.trim()}"`,
|
|
14755
|
+
status: status("question", q.id)
|
|
14756
|
+
}))].flatMap(({ name, status: s }) => {
|
|
14757
|
+
if (!s.current) return [];
|
|
14758
|
+
const last = s.history[s.history.length - 1];
|
|
14759
|
+
return [`- ${name}: accepted as ${s.current}${last.reason ? ` (${last.reason})` : ""}${last.answer ? `. Answer: ${last.answer}` : ""}`];
|
|
14760
|
+
});
|
|
14761
|
+
if (!lines.length) return [];
|
|
14762
|
+
return [
|
|
14763
|
+
"---",
|
|
14764
|
+
"STATUSES THE REVIEWER HAS ACCEPTED FROM LATER EVIDENCE (their decisions, which later evidence may still contradict):",
|
|
14765
|
+
...lines
|
|
14766
|
+
];
|
|
14767
|
+
}
|
|
14768
|
+
/**
|
|
14227
14769
|
* Reassess on a finding: everything the judgement needs, and the exact shape of the answer. The
|
|
14228
14770
|
* thread the analysis tied the finding to goes in when it resolves in the conversation this review
|
|
14229
14771
|
* holds; when it does not, the turn goes ahead without it.
|
|
@@ -14243,12 +14785,290 @@ function reassessingFinding(workspace, reassessing) {
|
|
|
14243
14785
|
...thread ? { thread } : {}
|
|
14244
14786
|
});
|
|
14245
14787
|
}
|
|
14788
|
+
/**
|
|
14789
|
+
* Reconcile with findings: the chosen turns, every finding by its reference with the grade and any
|
|
14790
|
+
* verdict the review holds now, the evidence rules and the exact shape of the answer.
|
|
14791
|
+
*/
|
|
14792
|
+
function reconcilingTurns(workspace, reconciling) {
|
|
14793
|
+
const result = workspace.session.result;
|
|
14794
|
+
if (!reconciling || !result) return [];
|
|
14795
|
+
return buildReplyReconciliationInstructions({
|
|
14796
|
+
result,
|
|
14797
|
+
assess: assessmentLookup(workspace.session),
|
|
14798
|
+
status: statusLookup(workspace.session),
|
|
14799
|
+
answeredQuestionIds: new Set(workspace.session.answeredQuestionIds ?? []),
|
|
14800
|
+
turns: reconciling.turns,
|
|
14801
|
+
review: reconciling.review
|
|
14802
|
+
});
|
|
14803
|
+
}
|
|
14246
14804
|
/** `THE ISSUE "<title>"` or `THE QUESTION "<question>"`, or just the kind when the finding is gone. */
|
|
14247
14805
|
function findingName(workspace, finding) {
|
|
14248
14806
|
const result = workspace.session.result;
|
|
14249
14807
|
const title = finding.kind === "issue" ? result?.issues.find((issue) => issue.id === finding.id)?.title : result?.questions.find((question) => question.id === finding.id)?.question;
|
|
14250
14808
|
return `THE ${finding.kind.toUpperCase()}${title ? ` "${title.trim()}"` : ""}`;
|
|
14251
14809
|
}
|
|
14810
|
+
var TURNS_REFUSED = {
|
|
14811
|
+
none: "Choose at least one turn of this conversation to reconcile with the findings.",
|
|
14812
|
+
unknown: "This conversation has no such turn to reconcile.",
|
|
14813
|
+
"too-many": `Reconcile reads at most 10 turns at once. Choose fewer.`,
|
|
14814
|
+
"too-long": "Those turns are too long to read in one reconcile. Choose fewer, or paste the reply in parts."
|
|
14815
|
+
};
|
|
14816
|
+
/**
|
|
14817
|
+
* The turns a request names, from the transcript as stored. Only the reviewer's own turns with words
|
|
14818
|
+
* in them: a reconcile turn is not evidence, and neither is anything Buddy AI wrote.
|
|
14819
|
+
*/
|
|
14820
|
+
function reconcileTurnsOf(transcript, value) {
|
|
14821
|
+
const asked = value?.exchanges;
|
|
14822
|
+
if (!Array.isArray(asked) || asked.length === 0) return {
|
|
14823
|
+
ok: false,
|
|
14824
|
+
refusal: "none"
|
|
14825
|
+
};
|
|
14826
|
+
const wanted = [...new Set(asked)];
|
|
14827
|
+
if (wanted.length > 10) return {
|
|
14828
|
+
ok: false,
|
|
14829
|
+
refusal: "too-many"
|
|
14830
|
+
};
|
|
14831
|
+
const turns = [];
|
|
14832
|
+
for (const askedAt of wanted) {
|
|
14833
|
+
const turn = typeof askedAt === "number" ? transcript.find((exchange) => exchange.askedAt === askedAt) : void 0;
|
|
14834
|
+
if (!turn || turn.reconciles || !turn.question.trim()) return {
|
|
14835
|
+
ok: false,
|
|
14836
|
+
refusal: "unknown"
|
|
14837
|
+
};
|
|
14838
|
+
turns.push(turn);
|
|
14839
|
+
}
|
|
14840
|
+
if (turns.reduce((total, turn) => total + turn.question.length, 0) > 4e4) return {
|
|
14841
|
+
ok: false,
|
|
14842
|
+
refusal: "too-long"
|
|
14843
|
+
};
|
|
14844
|
+
return {
|
|
14845
|
+
ok: true,
|
|
14846
|
+
turns
|
|
14847
|
+
};
|
|
14848
|
+
}
|
|
14849
|
+
function reviewIdentityOf(turns, sessionId) {
|
|
14850
|
+
const tag = reviewTag(sessionId);
|
|
14851
|
+
const named = [...new Set(turns.flatMap((turn) => reviewTagsIn(turn.question)))];
|
|
14852
|
+
if (!named.length) return {
|
|
14853
|
+
kind: "unnamed",
|
|
14854
|
+
tag
|
|
14855
|
+
};
|
|
14856
|
+
if (named.some((other) => other !== tag)) return {
|
|
14857
|
+
kind: "other",
|
|
14858
|
+
tag,
|
|
14859
|
+
named
|
|
14860
|
+
};
|
|
14861
|
+
return {
|
|
14862
|
+
kind: "named",
|
|
14863
|
+
tag
|
|
14864
|
+
};
|
|
14865
|
+
}
|
|
14866
|
+
/** Why a reply naming another review is refused, in the words the page shows. */
|
|
14867
|
+
function otherReviewRefusal(identity) {
|
|
14868
|
+
return `This reply names review ${identity.named.filter((named) => named !== identity.tag).join(" and ")}, but these findings are numbered for review ${identity.tag}. A fresh analysis numbers its findings again, so its references cannot be read against these ones. Nothing was reconciled. Prepare feedback again from this review, or reconcile the reply on the review it names.`;
|
|
14869
|
+
}
|
|
14870
|
+
/**
|
|
14871
|
+
* The answer, with every resolution checked against what it rests on (`groundResolution`). A check
|
|
14872
|
+
* that fails keeps no code evidence rather than unchecked evidence, so a code-based resolution then
|
|
14873
|
+
* falls to "needs investigation".
|
|
14874
|
+
*/
|
|
14875
|
+
async function readReconcileTurn(raw, result, turns, where, check = existingPaths) {
|
|
14876
|
+
const reading = parseReplyReconciliation(raw, result, turns);
|
|
14877
|
+
if (!reading) return {
|
|
14878
|
+
reading: null,
|
|
14879
|
+
raw
|
|
14880
|
+
};
|
|
14881
|
+
const cited = [...new Set(reading.judgements.flatMap((judgement) => judgement.status === "resolved" ? judgement.evidence.map((place) => place.file) : []))];
|
|
14882
|
+
let existing = /* @__PURE__ */ new Set();
|
|
14883
|
+
if (cited.length) try {
|
|
14884
|
+
existing = await check(where.repoPath, cited, where.commitSha);
|
|
14885
|
+
} catch {
|
|
14886
|
+
existing = /* @__PURE__ */ new Set();
|
|
14887
|
+
}
|
|
14888
|
+
return {
|
|
14889
|
+
reading: {
|
|
14890
|
+
...reading,
|
|
14891
|
+
judgements: reading.judgements.map((judgement) => groundResolution(judgement, existing))
|
|
14892
|
+
},
|
|
14893
|
+
raw
|
|
14894
|
+
};
|
|
14895
|
+
}
|
|
14896
|
+
function sameSuggestion(entry, axis, to) {
|
|
14897
|
+
return entry.axis === "status" === (axis === "status") && entry.to === to;
|
|
14898
|
+
}
|
|
14899
|
+
/** The exchanges a buddy-conversation entry read, or none for any other source. */
|
|
14900
|
+
function exchangesOf(source) {
|
|
14901
|
+
return source.kind === "buddy-conversation" ? source.exchanges : [];
|
|
14902
|
+
}
|
|
14903
|
+
/**
|
|
14904
|
+
* Whether this suggestion is already on the ledger for this finding, in a way that means it should
|
|
14905
|
+
* not be written again: offered on the card now (`isOffered`, the projection's own answer), or
|
|
14906
|
+
* declined by the reviewer on evidence that covered every turn this one read. A proposal still marked
|
|
14907
|
+
* proposed but outdated or overtaken is offered by no card, so it holds nothing back. A decline on
|
|
14908
|
+
* less evidence does not either: later turns are new evidence.
|
|
14909
|
+
*/
|
|
14910
|
+
function heldBack(session, kind, findingId, axis, to, exchanges) {
|
|
14911
|
+
for (const entry of session.findingReassessments ?? []) {
|
|
14912
|
+
if (entry.kind !== kind || entry.findingId !== findingId || !sameSuggestion(entry, axis, to)) continue;
|
|
14913
|
+
if (isOffered(session, entry)) return {
|
|
14914
|
+
kind: "waiting",
|
|
14915
|
+
entry
|
|
14916
|
+
};
|
|
14917
|
+
if (entry.state === "declined") {
|
|
14918
|
+
const read = new Set(exchangesOf(entry.source));
|
|
14919
|
+
if (exchanges.every((askedAt) => read.has(askedAt))) return { kind: "declined-before" };
|
|
14920
|
+
}
|
|
14921
|
+
}
|
|
14922
|
+
return null;
|
|
14923
|
+
}
|
|
14924
|
+
/**
|
|
14925
|
+
* Writes each judgement's proposals onto the session, checked against it as it is now. Run inside
|
|
14926
|
+
* the store write on the fresh copy, so a finding the reviewer decided while Buddy AI was reading is
|
|
14927
|
+
* written against what they decided, or not at all.
|
|
14928
|
+
*/
|
|
14929
|
+
function keepReconcileTurn(session, read, exchanges, askedAt) {
|
|
14930
|
+
const outcome = { judgements: [] };
|
|
14931
|
+
if (!read.reading) return outcome;
|
|
14932
|
+
const source = {
|
|
14933
|
+
kind: "buddy-conversation",
|
|
14934
|
+
exchanges: [...exchanges].sort((a, b) => a - b)
|
|
14935
|
+
};
|
|
14936
|
+
const analysis = analysisRefOf(session);
|
|
14937
|
+
for (const judgement of read.reading.judgements) {
|
|
14938
|
+
const finding = findingIn(session, judgement.kind, judgement.findingId);
|
|
14939
|
+
if (!finding) {
|
|
14940
|
+
outcome.judgements.push({
|
|
14941
|
+
judgement,
|
|
14942
|
+
status: { kind: "stale" },
|
|
14943
|
+
grade: null
|
|
14944
|
+
});
|
|
14945
|
+
continue;
|
|
14946
|
+
}
|
|
14947
|
+
const current = findingStatus(session.findingReassessments, judgement.kind, judgement.findingId, analysis).current;
|
|
14948
|
+
const to = verdictFor(judgement, current);
|
|
14949
|
+
let status;
|
|
14950
|
+
if (!to) status = { kind: "still-open" };
|
|
14951
|
+
else {
|
|
14952
|
+
const held = heldBack(session, judgement.kind, judgement.findingId, "status", to, source.exchanges);
|
|
14953
|
+
if (held) status = held;
|
|
14954
|
+
else {
|
|
14955
|
+
const write = reassessIn(session, {
|
|
14956
|
+
op: "propose-status",
|
|
14957
|
+
kind: judgement.kind,
|
|
14958
|
+
findingId: judgement.findingId,
|
|
14959
|
+
from: current,
|
|
14960
|
+
to,
|
|
14961
|
+
reason: judgement.reason,
|
|
14962
|
+
...judgement.answer ? { answer: judgement.answer } : {},
|
|
14963
|
+
...judgement.basis && to === "resolved" ? { basis: judgement.basis } : {},
|
|
14964
|
+
...judgement.evidence.length ? { evidence: judgement.evidence } : {},
|
|
14965
|
+
quotes: judgement.quotes,
|
|
14966
|
+
source
|
|
14967
|
+
}, askedAt);
|
|
14968
|
+
status = write.ok ? {
|
|
14969
|
+
kind: "proposed",
|
|
14970
|
+
entry: write.entry
|
|
14971
|
+
} : { kind: "stale" };
|
|
14972
|
+
}
|
|
14973
|
+
}
|
|
14974
|
+
let grade = null;
|
|
14975
|
+
if (judgement.grade) {
|
|
14976
|
+
const from = findingAssessment(session.findingReassessments, judgement.kind, finding, analysis).current;
|
|
14977
|
+
grade = heldBack(session, judgement.kind, judgement.findingId, "grade", judgement.grade.to, source.exchanges);
|
|
14978
|
+
if (!grade) {
|
|
14979
|
+
const write = reassessIn(session, {
|
|
14980
|
+
op: "propose",
|
|
14981
|
+
kind: judgement.kind,
|
|
14982
|
+
findingId: judgement.findingId,
|
|
14983
|
+
from,
|
|
14984
|
+
to: judgement.grade.to,
|
|
14985
|
+
reason: judgement.grade.reason,
|
|
14986
|
+
source
|
|
14987
|
+
}, askedAt);
|
|
14988
|
+
grade = write.ok ? {
|
|
14989
|
+
kind: "proposed",
|
|
14990
|
+
entry: write.entry
|
|
14991
|
+
} : write.refusal === "unchanged" ? null : { kind: "stale" };
|
|
14992
|
+
}
|
|
14993
|
+
}
|
|
14994
|
+
outcome.judgements.push({
|
|
14995
|
+
judgement,
|
|
14996
|
+
status,
|
|
14997
|
+
grade
|
|
14998
|
+
});
|
|
14999
|
+
}
|
|
15000
|
+
return outcome;
|
|
15001
|
+
}
|
|
15002
|
+
/** The ids of every proposal the turn wrote, for the transcript and for Accept all. */
|
|
15003
|
+
function proposedIds(outcome) {
|
|
15004
|
+
return outcome.judgements.flatMap(({ status, grade }) => [status, grade].flatMap((result) => result && result.kind === "proposed" ? [result.entry.id] : []));
|
|
15005
|
+
}
|
|
15006
|
+
/** One judgement's line, as the conversation says it: always a suggestion, never a change made. */
|
|
15007
|
+
function statusPhrase(judgement, status) {
|
|
15008
|
+
const to = status.kind === "proposed" || status.kind === "waiting" ? status.entry.to : null;
|
|
15009
|
+
switch (status.kind) {
|
|
15010
|
+
case "still-open": return "still open after this reply";
|
|
15011
|
+
case "declined-before": return "not suggested again: you kept it as it was on the same evidence";
|
|
15012
|
+
case "stale": return "not suggested: it changed while this was being read";
|
|
15013
|
+
case "waiting":
|
|
15014
|
+
case "proposed": {
|
|
15015
|
+
const phrase = to === "answered" ? "may be answered" : to === "partially-answered" ? "may be partly answered" : to === "resolved" ? "may be resolved" : to === "needs-investigation" ? "needs further investigation" : to === "unanswered" ? "may no longer be answered: this reply contradicts what you accepted" : to === "open" ? "may no longer be resolved: this reply contradicts what you accepted" : null;
|
|
15016
|
+
if (!phrase) return null;
|
|
15017
|
+
return status.kind === "waiting" ? `${phrase} (already waiting on the card)` : phrase;
|
|
15018
|
+
}
|
|
15019
|
+
}
|
|
15020
|
+
}
|
|
15021
|
+
function plural(n, noun) {
|
|
15022
|
+
return `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
15023
|
+
}
|
|
15024
|
+
/** "3 questions may be answered · 1 issue may be resolved · 1 needs further investigation". */
|
|
15025
|
+
function headline(outcome) {
|
|
15026
|
+
const count = (kind, to) => outcome.judgements.filter(({ judgement, status }) => judgement.kind === kind && status.kind === "proposed" && status.entry.to === to).length;
|
|
15027
|
+
return [
|
|
15028
|
+
[count("question", "answered"), (n) => `${plural(n, "question")} may be answered`],
|
|
15029
|
+
[count("question", "partially-answered"), (n) => `${plural(n, "question")} may be partly answered`],
|
|
15030
|
+
[count("issue", "resolved"), (n) => `${plural(n, "issue")} may be resolved`],
|
|
15031
|
+
[count("issue", "needs-investigation"), (n) => `${n} ${n === 1 ? "needs" : "need"} further investigation`],
|
|
15032
|
+
[outcome.judgements.filter(({ status }) => status.kind === "proposed" && (status.entry.to === "open" || status.entry.to === "unanswered")).length, (n) => `${n} earlier ${n === 1 ? "decision" : "decisions"} contradicted`],
|
|
15033
|
+
[outcome.judgements.filter(({ grade }) => grade?.kind === "proposed").length, (n) => `${plural(n, "grade change")} suggested`]
|
|
15034
|
+
].flatMap(([n, say]) => n > 0 ? [say(n)] : []).join(" · ");
|
|
15035
|
+
}
|
|
15036
|
+
/**
|
|
15037
|
+
* What the conversation shows for the turn: the suggestions, finding by finding, and nothing that
|
|
15038
|
+
* reads as a change made. Never the JSON. Findings the reply did not address are not mentioned,
|
|
15039
|
+
* because they were not judged.
|
|
15040
|
+
*/
|
|
15041
|
+
function reconcileTurnText(read, outcome, identity) {
|
|
15042
|
+
if (!read.reading) return `${read.raw.trim()}\n\n*This answer was not in the form suggestions need, so nothing was suggested and no finding changed.*`;
|
|
15043
|
+
const lines = outcome.judgements.flatMap(({ judgement, status, grade }) => {
|
|
15044
|
+
const said = [];
|
|
15045
|
+
const phrase = statusPhrase(judgement, status);
|
|
15046
|
+
if (phrase) said.push(phrase);
|
|
15047
|
+
if (grade?.kind === "proposed" || grade?.kind === "waiting") said.push(`its ${judgement.kind === "issue" ? "severity" : "importance"} may change to ${grade.entry.to}`);
|
|
15048
|
+
return said.length ? [`- **${judgement.ref}** ${said.join("; ")}. ${judgement.reason}`] : [];
|
|
15049
|
+
});
|
|
15050
|
+
const unmatched = read.reading.unmatched.map((passage) => `- Could not tell which finding this answers${passage.candidates.length ? ` (${passage.candidates.join(" or ")})` : ""}, so none was changed: “${passage.text}”${passage.why ? ` ${passage.why}` : ""}`);
|
|
15051
|
+
const dropped = read.reading.discarded.unquoted + read.reading.discarded.unknownRef;
|
|
15052
|
+
const droppedLine = dropped ? [`*${dropped === 1 ? "One judgement was" : `${dropped} judgements were`} left out: ${dropped === 1 ? "it" : "each"} named no finding in this review, or quoted words that are not in the turns.*`] : [];
|
|
15053
|
+
const top = headline(outcome);
|
|
15054
|
+
const unnamedLine = identity?.kind === "unnamed" ? [`*This reply does not name review ${identity.tag}, so any I or Q reference in it was trusted only where what it says matches that finding.*`] : [];
|
|
15055
|
+
if (!lines.length && !unmatched.length) return [
|
|
15056
|
+
"**Reconciled with findings.** Nothing in these turns bears on the review’s issues or questions, so nothing is suggested.",
|
|
15057
|
+
...unnamedLine,
|
|
15058
|
+
...droppedLine
|
|
15059
|
+
].join("\n\n");
|
|
15060
|
+
return [
|
|
15061
|
+
`**Reconciled with findings.**${top ? ` ${top}.` : ""}`,
|
|
15062
|
+
"These are suggestions. Nothing changes until you accept one on its card.",
|
|
15063
|
+
[...lines, ...unmatched].join("\n"),
|
|
15064
|
+
...unnamedLine,
|
|
15065
|
+
...droppedLine
|
|
15066
|
+
].join("\n\n");
|
|
15067
|
+
}
|
|
15068
|
+
/** The turns as the prompt labels them. */
|
|
15069
|
+
function evidenceTurns(turns) {
|
|
15070
|
+
return labelTurns(turns);
|
|
15071
|
+
}
|
|
14252
15072
|
//#endregion
|
|
14253
15073
|
//#region ../../packages/review-harness/src/workspace/diagram.ts
|
|
14254
15074
|
/** The same budget a question gets. Drawing is reading, and reading is what takes the time. */
|
|
@@ -16729,10 +17549,10 @@ async function handleUpdate(req, res, workspace) {
|
|
|
16729
17549
|
/**
|
|
16730
17550
|
* Resume a job that stopped, rather than start one over.
|
|
16731
17551
|
*
|
|
16732
|
-
*
|
|
16733
|
-
* route answers a button click, not a session that is waiting for the review. `runJob` records
|
|
16734
|
-
* every outcome, success or failure, on the job itself
|
|
16735
|
-
* nothing left worth doing
|
|
17552
|
+
* Answers as soon as the new attempt is recorded, not when it ends: `runJob` can take minutes, and
|
|
17553
|
+
* this route answers a button click, not a session that is waiting for the review. `runJob` records
|
|
17554
|
+
* every later outcome, success or failure, on the job itself, so a rejection after the answer has
|
|
17555
|
+
* nothing left worth doing except not becoming an unhandled one.
|
|
16736
17556
|
*
|
|
16737
17557
|
* Refused up front when the job is not terminal, rather than left to `runJob`'s own guard: a job
|
|
16738
17558
|
* that is still running has nothing here worth retrying, and saying so is cheaper and clearer than
|
|
@@ -16749,8 +17569,19 @@ async function handleRetry(res, workspace) {
|
|
|
16749
17569
|
if (!workspace.jobId) return send(res, 404, { error: "No job for this review." });
|
|
16750
17570
|
const job = loadJob(workspace.jobId);
|
|
16751
17571
|
if (!job || !isTerminal(job) || isClaimed(job.id)) return send(res, 409, { error: "This review is still being made. Wait for it to finish before retrying." });
|
|
16752
|
-
|
|
16753
|
-
|
|
17572
|
+
await new Promise((resolve) => {
|
|
17573
|
+
const run = runJob(workspace.id, {}, "standard", {
|
|
17574
|
+
source: "workspace",
|
|
17575
|
+
onStarted: () => {
|
|
17576
|
+
send(res, 200, { ok: true });
|
|
17577
|
+
resolve();
|
|
17578
|
+
}
|
|
17579
|
+
});
|
|
17580
|
+
track(run).catch((error) => {
|
|
17581
|
+
if (!res.headersSent) send(res, 409, { error: error instanceof Error ? error.message : String(error) });
|
|
17582
|
+
resolve();
|
|
17583
|
+
});
|
|
17584
|
+
});
|
|
16754
17585
|
}
|
|
16755
17586
|
/**
|
|
16756
17587
|
* Analyse this review again, at the commit it describes, with the agent that made it.
|
|
@@ -16853,6 +17684,22 @@ async function handleAsk(req, res, workspace) {
|
|
|
16853
17684
|
...reassessed,
|
|
16854
17685
|
from: currentGradeIn(workspace.session, reassessed.kind, reassessed.id) ?? null
|
|
16855
17686
|
} : null;
|
|
17687
|
+
let reconcileTurns = null;
|
|
17688
|
+
let reconcileIdentity = null;
|
|
17689
|
+
if (body?.reconciles !== void 0 && !explains && !reassessed) {
|
|
17690
|
+
if (!workspace.session.result) return send(res, 409, { error: "This review has no findings to reconcile with yet." });
|
|
17691
|
+
const chosen = reconcileTurnsOf(workspace.transcript, body.reconciles);
|
|
17692
|
+
if (!chosen.ok) return send(res, 400, {
|
|
17693
|
+
error: TURNS_REFUSED[chosen.refusal],
|
|
17694
|
+
refusal: chosen.refusal
|
|
17695
|
+
});
|
|
17696
|
+
reconcileIdentity = reviewIdentityOf(chosen.turns, workspace.session.id);
|
|
17697
|
+
if (reconcileIdentity.kind === "other") return send(res, 409, {
|
|
17698
|
+
error: otherReviewRefusal(reconcileIdentity),
|
|
17699
|
+
refusal: "other-review"
|
|
17700
|
+
});
|
|
17701
|
+
reconcileTurns = evidenceTurns(chosen.turns);
|
|
17702
|
+
}
|
|
16856
17703
|
if (liveUpdateFor(workspace.id)) return send(res, 409, {
|
|
16857
17704
|
error: "This review is being updated. Ask again when the new review is ready.",
|
|
16858
17705
|
updating: true
|
|
@@ -16893,6 +17740,13 @@ async function handleAsk(req, res, workspace) {
|
|
|
16893
17740
|
...explains ? { explaining: explains } : {},
|
|
16894
17741
|
...continuing ? { continuing } : {},
|
|
16895
17742
|
...reassessed ? { reassessing: reassessed } : {},
|
|
17743
|
+
...reconcileTurns && reconcileIdentity ? { reconciling: {
|
|
17744
|
+
turns: reconcileTurns,
|
|
17745
|
+
review: {
|
|
17746
|
+
tag: reconcileIdentity.tag,
|
|
17747
|
+
named: reconcileIdentity.kind === "named"
|
|
17748
|
+
}
|
|
17749
|
+
} } : {},
|
|
16896
17750
|
conversation: prepared
|
|
16897
17751
|
});
|
|
16898
17752
|
} catch (error) {
|
|
@@ -16915,21 +17769,39 @@ async function handleAsk(req, res, workspace) {
|
|
|
16915
17769
|
commitSha: workspace.session.metadata.commitSha
|
|
16916
17770
|
}) : null;
|
|
16917
17771
|
let reassessOutcome = null;
|
|
17772
|
+
const reconcileRead = reconcileTurns && workspace.session.result ? await readReconcileTurn(answer, workspace.session.result, reconcileTurns, {
|
|
17773
|
+
repoPath: workspace.repoPath,
|
|
17774
|
+
commitSha: workspace.session.metadata.commitSha
|
|
17775
|
+
}) : null;
|
|
17776
|
+
let reconcileOutcome = null;
|
|
17777
|
+
let keptAt = 0;
|
|
16918
17778
|
let explanation;
|
|
16919
17779
|
let shown = answer;
|
|
16920
17780
|
const saved = mutateWorkspace(workspace.id, (fresh) => {
|
|
16921
17781
|
const askedAt = Date.now();
|
|
16922
17782
|
const kept = explains && findingNamed(fresh.session, explains) && answer.trim() ? explains : null;
|
|
17783
|
+
keptAt = askedAt;
|
|
16923
17784
|
if (turn && reassessing) {
|
|
16924
17785
|
reassessOutcome = keepReassessTurn(fresh.session, reassessing, turn, askedAt);
|
|
16925
17786
|
shown = reassessTurnText(reassessing, turn, reassessOutcome);
|
|
16926
17787
|
}
|
|
17788
|
+
let reconciles;
|
|
17789
|
+
if (reconcileRead && reconcileTurns) {
|
|
17790
|
+
const exchanges = reconcileTurns.map((evidence) => evidence.askedAt);
|
|
17791
|
+
reconcileOutcome = keepReconcileTurn(fresh.session, reconcileRead, exchanges, askedAt);
|
|
17792
|
+
shown = reconcileTurnText(reconcileRead, reconcileOutcome, reconcileIdentity ?? void 0);
|
|
17793
|
+
reconciles = {
|
|
17794
|
+
exchanges,
|
|
17795
|
+
proposals: proposedIds(reconcileOutcome)
|
|
17796
|
+
};
|
|
17797
|
+
}
|
|
16927
17798
|
fresh.transcript.push({
|
|
16928
17799
|
question,
|
|
16929
17800
|
answer: shown,
|
|
16930
17801
|
sectionId,
|
|
16931
17802
|
askedAt,
|
|
16932
|
-
...kept ? { explains: kept } : {}
|
|
17803
|
+
...kept ? { explains: kept } : {},
|
|
17804
|
+
...reconciles ? { reconciles } : {}
|
|
16933
17805
|
});
|
|
16934
17806
|
if (kept) {
|
|
16935
17807
|
explanation = {
|
|
@@ -16945,13 +17817,23 @@ async function handleAsk(req, res, workspace) {
|
|
|
16945
17817
|
outcome: reassessOutcome,
|
|
16946
17818
|
...card ? { card } : {}
|
|
16947
17819
|
} } : {};
|
|
16948
|
-
|
|
16949
|
-
|
|
16950
|
-
|
|
16951
|
-
|
|
16952
|
-
|
|
17820
|
+
const reconciliation = reconcileOutcome && saved ? { reconciliation: {
|
|
17821
|
+
proposals: proposedIds(reconcileOutcome),
|
|
17822
|
+
cards: reconcileOutcome.judgements.flatMap(({ judgement }) => {
|
|
17823
|
+
const parts = gradeFragments(saved, judgement.kind, judgement.findingId);
|
|
17824
|
+
return parts ? [parts] : [];
|
|
17825
|
+
})
|
|
17826
|
+
} } : {};
|
|
17827
|
+
const turnRef = saved ? {
|
|
17828
|
+
askedAt: keptAt,
|
|
17829
|
+
...reconcileOutcome ? { reconcileTurn: true } : {}
|
|
17830
|
+
} : {};
|
|
17831
|
+
sendIfOpen(res, 200, {
|
|
16953
17832
|
answer: shown,
|
|
16954
|
-
...
|
|
17833
|
+
...explanation ? { explanation } : {},
|
|
17834
|
+
...reassessment,
|
|
17835
|
+
...reconciliation,
|
|
17836
|
+
...turnRef
|
|
16955
17837
|
});
|
|
16956
17838
|
} finally {
|
|
16957
17839
|
endRun(run);
|
|
@@ -17264,7 +18146,9 @@ async function handleState(req, res, workspace) {
|
|
|
17264
18146
|
ok: true,
|
|
17265
18147
|
completedAt: saved.completedAt ?? null,
|
|
17266
18148
|
pinned: saved.pinned === true,
|
|
17267
|
-
queued: readQueue().includes(saved.id)
|
|
18149
|
+
queued: readQueue().includes(saved.id),
|
|
18150
|
+
answeredQuestionIds: saved.session.answeredQuestionIds ?? [],
|
|
18151
|
+
dismissedIssues: saved.session.dismissedIssues ?? []
|
|
17268
18152
|
});
|
|
17269
18153
|
}
|
|
17270
18154
|
var KISS_DECISION_VALUES = [
|
|
@@ -17362,7 +18246,8 @@ var ASK_SOURCES = [
|
|
|
17362
18246
|
"free_text",
|
|
17363
18247
|
"explain_simply",
|
|
17364
18248
|
"selection",
|
|
17365
|
-
"finding"
|
|
18249
|
+
"finding",
|
|
18250
|
+
"reconcile"
|
|
17366
18251
|
];
|
|
17367
18252
|
function askSource(value) {
|
|
17368
18253
|
return ASK_SOURCES.includes(value) ? value : "free_text";
|