prreviewbuddy 0.92.0 → 0.95.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +101 -0
- package/dist/main.js +597 -136
- package/dist/{mcp_tools-BKPpd9dN.js → mcp_tools-DDbcXys2.js} +1274 -571
- package/dist/server.js +317 -413
- package/package.json +1 -1
- package/static/{chunk-CIJm20ER.js → chunk-CQIPqX7U.js} +1 -1
- package/static/chunk-Cs33-tV3.js +1 -0
- package/static/{chunk-Ahis0VaG.js → chunk-Cum6sHaC.js} +1 -1
- package/static/{chunk-DMxtqzNj.js → chunk-D1LBeOJc.js} +1 -1
- 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 +11 -0
- package/static/workspace.js +64 -64
- package/static/chunk-DRtin0n-.js +0 -1
package/dist/server.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { $
|
|
2
|
-
import {
|
|
3
|
-
import { execFile, spawn } from "node:child_process";
|
|
1
|
+
import { $ as statusPillLabel, $i as lifecycleItemLabel, $n as writeJsonAtomically, $r as findExistingReview, $t as statedWorkspacePort, A as anythingRunning, Aa as currentGradeIn, Ai as indexReviewGroups, An as BUILD_VERSION, Ao as deleteCollection, Ar as chatConversationSection, At as isRequestId, B as completedLabel, Ba as statusLookup, Bi as previousKissResult, Bn as checkArchitecture, Bt as readServerRecord, C as parsePullRequestLink, Ca as isExplanationAction, Cn as readEvents, Cr as ASK_REREAD_CEILING_MS, Ct as findingRefs, D as INDEX_SECTIONS, Da as analysisRefOf, Di as followedRefName, Dn as recordTelemetryUploadConsent, Do as collectionsStamp, Dr as isUnchanged, Dt as endAskRequest, E as relativeTime$1, Ea as NO_STATUS, Ei as decideKissRecommendation, Er as storedConversation, Et as reviewTagsIn, F as collectionFold, Fa as isGradeFor, Fi as loadWorkspace, Fn as recordRuntimeBestEffort, Fo as STORE_ROOT, Fr as renderMarkdown, Ft as readIndexToken, G as markItemLabel, Ga as extractJsonText, Gi as touchWorkspace, Gn as storeArchitecture, Gr as createWorktree, H as homeSection, Ha as followUpFindingId, Hi as reviewedCommit, Hn as partFiles, Hr as isBusy, Ht as serverEntryExists, Ia as isOffered, In as RUNTIME_WORDS, Ir as CODE_BLOCK_CLASS, J as queueItemLabel, Ja as WITHHELD_REASON, Ji as LIFECYCLES, Jn as ownersOf, Jr as readMarker, Jt as DEFAULT_WORKSPACE_PORT, K as omittedText, Ka as objectWithAnyOf, Ki as workspaceRevision, Kn as storedScopes, Kr as holdWorktree, L as collectionPlacement, La as promptGrade, Li as mutateWorkspace, Ln as describeRuntimes, Lr as CODE_COPY_CLASS, Lt as clearServerRecordOwnedBy, M as authorOptions, Ma as findingIn, Mi as kissDecisionsOf, Mn as feedbackUrl, Mo as readCollections, Mr as authorPreview, Mt as markAskRunning, N as collectionCountLabel, Na as findingStatus, Ni as latestKissRun, Nn as waitingHistory, No as renameCollection, Nr as hasAuthorContent, O as INDEX_SECTION_NAMES, Oa as assessmentLookup, Oi as groupByLineage, On as telemetryUploadConsent, Oo as createCollection, Or as agentEnvOf, P as collectionEmptyText, Pa as gradeScale, Pi as lineagePosition, Pn as mapRuntimes, Po as setMembership, Pr as GITHUB_MARKDOWN, Q as statusLabel, Qa as DetachedHeadError, Qi as lifecycleChip, Qn as isArchitectureId, Qt as resolveWorkspacePort, R as collectionsLine, Ra as reassessIn, Rn as formatDuration, Rr as escapeHtml$1, S as extractPullRequestLinks, Sa as explanationOf, So as versionFrom, Sr as existingPaths, St as findingRef, T as runBatch, Ta as ANALYSIS_ONLY, To as AgentUnavailableError, Tr as saveRefresh, Tt as reviewTag, U as isCollectionId, Ua as SEVERITIES, Un as readArchitecture, Ur as withReviewLock, Ut as spawnServer, V as countLabel, Va as wasReassessed, Vi as recentWorkspaces, Vn as loadArchitecture, Vr as ReviewBeingDeletedError, W as isFoldKey, Wa as asString, Wn as repositoryKey, Wr as MANAGED_ROOT, Wt as stopChild, X as sectionOpen, Xa as resolveTarget, Xi as activityLabel, Xn as REPOSITORY_SCOPE, Xr as requestJobStop, Xt as MIN_WORKSPACE_PORT, Y as queueOptionLabel, Ya as kissOutcome, Yi as activityAttributes, Yn as QUOTE_LIMITS, Yr as removeWorktree, Yt as MAX_WORKSPACE_PORT, Z as selectAllLabel, Za as originRemote, Zi as isLifecycle, Zn as ancestorsOf, Zt as parseWorkspacePort, _ as endRun, _a as reviewerDispositions, _i as isLiveJob, _n as prepareReview, _o as agentIdentityOf, _r as retireNotification, _t as modelProvenanceOf, a as lineageIds, aa as sectionOf$1, ai as forgeResolver, an as cancellableHere, ao as git, ar as createPrState, at as lineageKeysFor, b as stopRun, ba as recommendationsOf, bi as progressSteps, bn as runJob, bo as parkedReason, br as unreadCount$1, bt as humanizeModelId, c as discardJob, ca as queueRemove, ci as allJobs, cn as recheckConversation, co as resolveModel, cr as listNotifications, ct as compactQueue, d as startKissJob, da as doneVerb, di as fail$1, dn as findingsChanged, do as readAgentPreference, dr as markReadForReview, ea as lifecycleName, ei as checkCodeFreshness, en as writePortPreference, eo as isCommitReview, er as announceJob, et as deliver, f as isDriving, fa as isIssueOutstanding, fi as isTerminal, fn as UpdateAlreadyRunningError, fo as writeAgentPreference, fr as moveIds, g as completeRun, ga as resolvedByConversation, gn as observeReview, go as agentFor$1, gr as removeNotification, h as beginRun, ha as questionsOutstanding, hi as saveJob, hn as failureOf$1, ho as agentById, hr as readStateOf, i as deleteReview, ia as parseLifecycle, ii as fetchHead, in as cancelUpdate, io as shorthandOf, ir as createArchitectureNotification, it as retryableMiss, j as authorOptionLabel, ja as findingAssessment, ji as kissDecisionRefusal, jo as nameProblemText, jr as UNESTABLISHED_MOTIVATION, k as PR_FILTER_OPTIONS, ka as closesFinding, ki as indexKeptIds, ko as createWithMembers, kr as EXPLAIN_SIMPLY_PROMPT, l as kissAgentId, la as queueStamp, li as attemptIdOf, ln as updateReview, lo as agentIdOf, lr as markAllRead, m as RUN_KINDS, ma as issuesOutstanding, mi as pruneJobResidue, mn as runningJobs, mo as AGENT_IDS, mr as readNotification, n as MCP_TOOL_NAMES, na as lifecyclePillLabel, ni as followedBranch, nn as FETCH_TIMEOUT_MS, no as AmbiguousRefError, nr as subjectOf, o as liveJobsFor, oa as queueAdd, oi as fillFileUrlTemplate, on as mergeUpdateInto, oo as refExists, or as createPrUpdate, ot as settleCollections, p as track, pa as isQuestionOutstanding, pi as loadJob, pn as liveUpdateFor, pr as pruneNotifications, pt as cliEntry, q as pinItemLabel, qa as describeAuthorship, qi as workspaceStamps, qn as trackedFiles, qr as isWorktreeHeld, qt as writeServerRecord, r as deleteLineage, ra as offersLifecycle, ri as reviewedShaOf, rn as RefreshUnavailableError, ro as shortNameOf, rr as categoryOf, rt as openDestination, s as wasBlocked, sa as queueAddMany, si as RUN_RECORD_RETENTION_MS, so as resolveAgent, sr as eventName, st as activateQueued, ta as lifecyclePillAction, ti as checkReview, to as resolveBase, tr as branchOf, ua as readQueue, ui as endedAt, un as askCheckout, uo as clearAgentPreference, ur as markRead, ut as settleQueue, v as heldRuns, va as verdictClosure, vi as isTerminalJobPhase, vn as settleReview, vo as detectAgents, vr as toneOf, vt as provenanceDetailLines, w as unsupportedReason, wa as withExplanation, wi as isClaimed, wn as record, wo as AgentCancelledError, wr as prepareConversationWithin, wt as refOf, x as batchIdentity, xa as explanationActionOf, xi as purposeOf, xo as runnableAgents, xr as withUsageRecorded, xt as findingAtRef, y as reportRun, ya as processDiscussion, yi as phasesFor, yn as reanalyseReview, yo as isAgentId, yr as unreadAround, yt as agentDisplayName, z as completedFold, za as sourcePhrase, zi as presentedKissRunOf, zn as ARCHITECTURE_DIR, zr as renderInlineText } from "./mcp_tools-DDbcXys2.js";
|
|
2
|
+
import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
|
|
4
3
|
import { accessSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { basename, delimiter, isAbsolute, join, relative, resolve } from "node:path";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
|
-
import {
|
|
6
|
+
import { execFile, spawn } from "node:child_process";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { createServer } from "node:http";
|
|
9
9
|
//#region ../../packages/review-harness/src/domain/ai_review/review_composition.ts
|
|
@@ -477,317 +477,6 @@ function renderIntegrationsNotice(names) {
|
|
|
477
477
|
</div>`;
|
|
478
478
|
}
|
|
479
479
|
//#endregion
|
|
480
|
-
//#region ../../packages/review-harness/src/workspace/reviews_index.ts
|
|
481
|
-
/**
|
|
482
|
-
* The sections of the list, top to bottom.
|
|
483
|
-
*
|
|
484
|
-
* Pinned is not a lifecycle state. It is where a card is drawn, on top of a lifecycle it keeps: a
|
|
485
|
-
* pinned review can be Active, Waiting or Done, and unpinning it puts it back in the section that
|
|
486
|
-
* says. That is why it lives here, beside the other placement rules, and not in `LIFECYCLES`.
|
|
487
|
-
*/
|
|
488
|
-
var INDEX_SECTIONS = [
|
|
489
|
-
"pinned",
|
|
490
|
-
"active",
|
|
491
|
-
"waiting",
|
|
492
|
-
"done"
|
|
493
|
-
];
|
|
494
|
-
/**
|
|
495
|
-
* Every section the list draws, top to bottom: In progress, then the card sections.
|
|
496
|
-
*
|
|
497
|
-
* In progress is not in `INDEX_SECTIONS` because no card is ever placed there. It lists work, one
|
|
498
|
-
* entry per running job or waiting row (`deriveOperations`), and a review being updated stays a
|
|
499
|
-
* card in its own section while its update is listed here. Windows, Load more, the selection and
|
|
500
|
-
* the counts are all about cards, so they keep reading `INDEX_SECTIONS`; folding, its memory and its
|
|
501
|
-
* keyboard commands are about headings, so they read this.
|
|
502
|
-
*/
|
|
503
|
-
var LIST_SECTIONS = ["in-progress", ...INDEX_SECTIONS];
|
|
504
|
-
/**
|
|
505
|
-
* Each section's name, as its heading says it, and the key that folds it from the keyboard
|
|
506
|
-
* (⌘K L and this key). The commands are made from this table, so a section added here is a section
|
|
507
|
-
* the keyboard can fold, with no second list to remember.
|
|
508
|
-
*/
|
|
509
|
-
var lifecycleName = (id) => LIFECYCLES.find((lifecycle) => lifecycle.id === id).name;
|
|
510
|
-
var INDEX_SECTION_NAMES = {
|
|
511
|
-
"in-progress": {
|
|
512
|
-
title: "In progress",
|
|
513
|
-
key: "i"
|
|
514
|
-
},
|
|
515
|
-
pinned: {
|
|
516
|
-
title: "Pinned",
|
|
517
|
-
key: "p"
|
|
518
|
-
},
|
|
519
|
-
active: {
|
|
520
|
-
title: lifecycleName("active"),
|
|
521
|
-
key: "a"
|
|
522
|
-
},
|
|
523
|
-
waiting: {
|
|
524
|
-
title: lifecycleName("waiting"),
|
|
525
|
-
key: "w"
|
|
526
|
-
},
|
|
527
|
-
done: {
|
|
528
|
-
title: lifecycleName("done"),
|
|
529
|
-
key: "d"
|
|
530
|
-
}
|
|
531
|
-
};
|
|
532
|
-
/**
|
|
533
|
-
* Whether a section starts open. The reviewer's choice wins, and otherwise everything is open but
|
|
534
|
-
* the archives: Done, and each collection's completed reviews. A new collection starts open. The
|
|
535
|
-
* one place that default is written.
|
|
536
|
-
*/
|
|
537
|
-
function sectionOpen(section, collapsed) {
|
|
538
|
-
return !(collapsed?.[section] ?? (section === "done" || section.startsWith("collection-done:")));
|
|
539
|
-
}
|
|
540
|
-
/** A section the list draws a heading for, and so one whose fold can be stored. */
|
|
541
|
-
function isListSection(value) {
|
|
542
|
-
return typeof value === "string" && LIST_SECTIONS.includes(value);
|
|
543
|
-
}
|
|
544
|
-
/** A fold the store accepts: a list section, or a collection's, by an id shaped like one. */
|
|
545
|
-
function isFoldKey(value) {
|
|
546
|
-
if (isListSection(value)) return true;
|
|
547
|
-
if (typeof value !== "string") return false;
|
|
548
|
-
const match = /^collection(?:-done)?:(.*)$/.exec(value);
|
|
549
|
-
return match !== null && isCollectionId(match[1]);
|
|
550
|
-
}
|
|
551
|
-
function collectionFold(id) {
|
|
552
|
-
return `collection:${id}`;
|
|
553
|
-
}
|
|
554
|
-
function completedFold(id) {
|
|
555
|
-
return `collection-done:${id}`;
|
|
556
|
-
}
|
|
557
|
-
var COLLECTION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
558
|
-
/**
|
|
559
|
-
* Whether a value is shaped like a collection's id. Here, in the module both halves share, because
|
|
560
|
-
* the hash and the fold store check it in the browser and on the server alike.
|
|
561
|
-
*/
|
|
562
|
-
function isCollectionId(value) {
|
|
563
|
-
return typeof value === "string" && COLLECTION_ID.test(value);
|
|
564
|
-
}
|
|
565
|
-
/**
|
|
566
|
-
* Which section a card is drawn in: Pinned when pinned, and otherwise where its lifecycle says.
|
|
567
|
-
*
|
|
568
|
-
* Pinning controls prominence in the general list and never overrides an ordering the reviewer
|
|
569
|
-
* asked for. Under such a filter a pinned card stays in its lifecycle section, in the order the
|
|
570
|
-
* filter gives, and only its star says it is pinned.
|
|
571
|
-
*/
|
|
572
|
-
function indexSectionOf(pinned, lifecycle, activity, filter) {
|
|
573
|
-
return pinned && !ordersExplicitly(filter) ? "pinned" : sectionOf$1(lifecycle, activity);
|
|
574
|
-
}
|
|
575
|
-
/**
|
|
576
|
-
* Where a review's own card is drawn, or absent when it is drawn only inside its collections.
|
|
577
|
-
*
|
|
578
|
-
* Pinned first and exclusive, as it always was: a pinned review is drawn in Pinned and nowhere else,
|
|
579
|
-
* collected or not, and unpinning it puts it back where the rest of these rules say. A review in a
|
|
580
|
-
* collection is drawn there instead of in Active, Waiting or Done, so those three list what nobody
|
|
581
|
-
* has grouped. Under the Queue filter neither applies: the list is the queue in its order, each card
|
|
582
|
-
* in its lifecycle section, the same reason `indexSectionOf` gives for Pinned.
|
|
583
|
-
*/
|
|
584
|
-
function homeSection(pinned, lifecycle, activity, collected, filter) {
|
|
585
|
-
if (collected && !pinned && !ordersExplicitly(filter)) return void 0;
|
|
586
|
-
return indexSectionOf(pinned, lifecycle, activity, filter);
|
|
587
|
-
}
|
|
588
|
-
/**
|
|
589
|
-
* Where a review is drawn inside each collection it belongs to: with the collection's reviews still
|
|
590
|
-
* needing attention, under its completed reviews, or not at all.
|
|
591
|
-
*
|
|
592
|
-
* Completed means Done, by the same derivation that places the card everywhere else, so Waiting is
|
|
593
|
-
* still in play and the Reviewed mark has nothing to do with it. One state for every collection the
|
|
594
|
-
* review is in: marking it Done moves it in all of them at once. Not drawn while pinned (Pinned is
|
|
595
|
-
* exclusive) or under the Queue filter (see `homeSection`).
|
|
596
|
-
*/
|
|
597
|
-
function collectionPlacement(pinned, lifecycle, activity, filter) {
|
|
598
|
-
if (pinned || ordersExplicitly(filter)) return void 0;
|
|
599
|
-
return sectionOf$1(lifecycle, activity) === "done" ? "completed" : "active";
|
|
600
|
-
}
|
|
601
|
-
/** A collection heading's count: what still needs attention, and the members drawn in Pinned. */
|
|
602
|
-
function collectionCountLabel(active, pinned) {
|
|
603
|
-
const label = `${active} active`;
|
|
604
|
-
return pinned === 0 ? label : `${label} · ${pinned} pinned`;
|
|
605
|
-
}
|
|
606
|
-
/**
|
|
607
|
-
* The quiet line on a card naming its collections, or absent when it would say nothing new.
|
|
608
|
-
*
|
|
609
|
-
* Inside a collection the heading already names that one, so the line says only where else the
|
|
610
|
-
* review is ("Also in Release 0.82"), and nothing for a review in one collection. Outside every
|
|
611
|
-
* collection heading, which is Pinned and the Queue filter, it names them all.
|
|
612
|
-
*/
|
|
613
|
-
function collectionsLine(memberOf, collections, current) {
|
|
614
|
-
const names = collections.filter((collection) => collection.id !== current && memberOf.includes(collection.id)).map((collection) => collection.name);
|
|
615
|
-
if (names.length === 0) return void 0;
|
|
616
|
-
return `${current === void 0 ? "In" : "Also in"} ${names.join(", ")}`;
|
|
617
|
-
}
|
|
618
|
-
/**
|
|
619
|
-
* What a collection says when its main list is empty, or absent when it is not. A collection is
|
|
620
|
-
* never hidden for having nothing active: it is still somewhere its finished reviews live, and
|
|
621
|
-
* somewhere to put the next one.
|
|
622
|
-
*/
|
|
623
|
-
function collectionEmptyText(counts, filtered) {
|
|
624
|
-
if (counts.active > 0) return void 0;
|
|
625
|
-
if (filtered) return "Nothing in this collection matches the filters.";
|
|
626
|
-
if (counts.members === 0) return "Nothing here yet. Add reviews from a card’s menu, or select several and choose Manage collections.";
|
|
627
|
-
if (counts.pinned > 0 && counts.completed === 0) return "Its reviews are pinned, so they are listed under Pinned.";
|
|
628
|
-
return "No active reviews";
|
|
629
|
-
}
|
|
630
|
-
/** Said under a collection whose oldest members are not drawn, so it never looks smaller than it is. */
|
|
631
|
-
function omittedText(omitted) {
|
|
632
|
-
return omitted === 1 ? "1 older review in this collection is not listed." : `${omitted} older reviews in this collection are not listed.`;
|
|
633
|
-
}
|
|
634
|
-
/** The disclosure holding a collection's Done reviews. */
|
|
635
|
-
function completedLabel(completed) {
|
|
636
|
-
return `Show completed (${completed})`;
|
|
637
|
-
}
|
|
638
|
-
/**
|
|
639
|
-
* Whether a filter replaces the activity order with one the reviewer set. Queue is the only one:
|
|
640
|
-
* the list is the queue, in its order.
|
|
641
|
-
* `orderIndex` and `indexSectionOf` both ask this, so the ordering and the placement cannot disagree.
|
|
642
|
-
*/
|
|
643
|
-
function ordersExplicitly(filter) {
|
|
644
|
-
return filter === "queue";
|
|
645
|
-
}
|
|
646
|
-
/**
|
|
647
|
-
* "45 reviews · 13 branches", and "13 reviews" when those are the same thirteen things.
|
|
648
|
-
*
|
|
649
|
-
* The two numbers are different questions and only both worth asking once they differ. A page of
|
|
650
|
-
* thirteen branches reviewed once each would read "13 reviews · 13 branches", which invites the
|
|
651
|
-
* reader to work out what the distinction is on the one page where there isn't one.
|
|
652
|
-
*
|
|
653
|
-
* `of` is every review on the page, given while the search or a filter is narrowing the list:
|
|
654
|
-
* "12 of 71 reviews" says the list is filtered and by how much, where "12 reviews" reads as all
|
|
655
|
-
* there are.
|
|
656
|
-
*/
|
|
657
|
-
function countLabel(reviews, branches, of) {
|
|
658
|
-
const label = of === void 0 ? `${reviews} review${reviews === 1 ? "" : "s"}` : `${reviews} of ${of} review${of === 1 ? "" : "s"}`;
|
|
659
|
-
if (reviews === branches) return label;
|
|
660
|
-
return `${label} · ${branches} branch${branches === 1 ? "" : "es"}`;
|
|
661
|
-
}
|
|
662
|
-
/**
|
|
663
|
-
* Every login the author dropdown offers, once each, in alphabetical order of what the reader sees:
|
|
664
|
-
* the name where there is one, the login otherwise, regardless of case. Read from the cards the page
|
|
665
|
-
* is showing, so it never lists somebody with nothing to show.
|
|
666
|
-
*
|
|
667
|
-
* A login's name is the one on its newest review that has a name, chosen here by `createdAt` rather
|
|
668
|
-
* than by the order the caller happens to pass, so an account renamed upstream reads as it does now.
|
|
669
|
-
*/
|
|
670
|
-
function authorOptions(cards) {
|
|
671
|
-
const newest = /* @__PURE__ */ new Map();
|
|
672
|
-
for (const card of cards) {
|
|
673
|
-
if (!card.handle) continue;
|
|
674
|
-
const seen = newest.get(card.handle);
|
|
675
|
-
if (!seen) newest.set(card.handle, {
|
|
676
|
-
name: card.name || void 0,
|
|
677
|
-
createdAt: card.createdAt
|
|
678
|
-
});
|
|
679
|
-
else if (card.name && (!seen.name || card.createdAt > seen.createdAt)) newest.set(card.handle, {
|
|
680
|
-
name: card.name,
|
|
681
|
-
createdAt: card.createdAt
|
|
682
|
-
});
|
|
683
|
-
}
|
|
684
|
-
return [...newest].map(([handle, { name }]) => name && name !== handle ? {
|
|
685
|
-
handle,
|
|
686
|
-
name
|
|
687
|
-
} : { handle }).sort((a, b) => (a.name ?? a.handle).localeCompare(b.name ?? b.handle, void 0, { sensitivity: "base" }));
|
|
688
|
-
}
|
|
689
|
-
/** An author option as the dropdown spells it: the name and the login, or the login alone. */
|
|
690
|
-
function authorOptionLabel(option) {
|
|
691
|
-
return option.name ? `${option.name} · @${option.handle}` : `@${option.handle}`;
|
|
692
|
-
}
|
|
693
|
-
/**
|
|
694
|
-
* A card menu's status item, which sets the state it names. The bulk bar's words for the same two
|
|
695
|
-
* actions, so a card and a selection never disagree. Here rather than in the page renderer because
|
|
696
|
-
* the browser relabels the item after a change, and that renderer reaches the store.
|
|
697
|
-
*/
|
|
698
|
-
function markItemLabel(mark) {
|
|
699
|
-
return mark === "complete" ? "Mark reviewed" : "Mark not reviewed";
|
|
700
|
-
}
|
|
701
|
-
/**
|
|
702
|
-
* The review page's Pin item, which says what pressing it will do. The words of the index's star,
|
|
703
|
-
* so pinning from either place reads as the same act. Here because the browser relabels the item
|
|
704
|
-
* after the change, for the reason `markItemLabel` is here.
|
|
705
|
-
*/
|
|
706
|
-
function pinItemLabel(pinned) {
|
|
707
|
-
return pinned ? "Unpin review" : "Pin review";
|
|
708
|
-
}
|
|
709
|
-
/**
|
|
710
|
-
* The queue item, on a card and on the review's own page. "review queue" rather than "queue",
|
|
711
|
-
* because a menu is where somebody meets the idea for the first time. Here because the browser
|
|
712
|
-
* relabels it after a change, on both pages.
|
|
713
|
-
*/
|
|
714
|
-
function queueItemLabel(queued) {
|
|
715
|
-
return queued ? "Remove from review queue" : "Add to review queue";
|
|
716
|
-
}
|
|
717
|
-
/**
|
|
718
|
-
* The Queue pill, which carries a number only once there is one.
|
|
719
|
-
*
|
|
720
|
-
* "Queue 0" is a count of something that is not there, and reads as a fault. Bare "Queue" reads as
|
|
721
|
-
* the place it is -- empty, and still the thing you would click to put something in it.
|
|
722
|
-
*/
|
|
723
|
-
function queueOptionLabel(queued) {
|
|
724
|
-
return queued === 0 ? "Queue" : `Queue (${queued})`;
|
|
725
|
-
}
|
|
726
|
-
/**
|
|
727
|
-
* The words on the status chip.
|
|
728
|
-
*
|
|
729
|
-
* Here rather than in the renderer because the browser now writes this chip too: marking a
|
|
730
|
-
* selection reviewed repaints it without asking the server for the page again, and two copies of
|
|
731
|
-
* these three words is how a page ends up saying "Reviewed" in one place and "Complete" in the
|
|
732
|
-
* other about the same review.
|
|
733
|
-
*/
|
|
734
|
-
function statusLabel(status) {
|
|
735
|
-
if (status === "complete") return "Reviewed";
|
|
736
|
-
return status === "in-progress" ? "Not reviewed" : "New";
|
|
737
|
-
}
|
|
738
|
-
/**
|
|
739
|
-
* The status pill's accessible name: where the reviewer is, then what pressing it does. The visible
|
|
740
|
-
* words are the state, so a screen reader hears both rather than only a state it cannot act on.
|
|
741
|
-
*/
|
|
742
|
-
function statusPillLabel(status) {
|
|
743
|
-
return `${statusLabel(status)}. Mark as ${status === "complete" ? "not reviewed" : "reviewed"}`;
|
|
744
|
-
}
|
|
745
|
-
/**
|
|
746
|
-
* The bulk bar's way to tick everything the filters show, across pages.
|
|
747
|
-
*
|
|
748
|
-
* Counted ("Select all 7") only while the headline is not already saying "of 7", so the number is
|
|
749
|
-
* on the bar once. With PR set to Merged this is how a reviewer clears the merged reviews in one go,
|
|
750
|
-
* by way of the existing Mark reviewed.
|
|
751
|
-
*/
|
|
752
|
-
function selectAllLabel(inView, scopeShown = false) {
|
|
753
|
-
return inView === 0 || scopeShown ? "Select all" : `Select all ${inView}`;
|
|
754
|
-
}
|
|
755
|
-
/** The PR filter's options, in order, as the select shows them. */
|
|
756
|
-
var PR_FILTER_OPTIONS = [
|
|
757
|
-
{
|
|
758
|
-
value: "any",
|
|
759
|
-
label: "Any"
|
|
760
|
-
},
|
|
761
|
-
{
|
|
762
|
-
value: "open",
|
|
763
|
-
label: "Open"
|
|
764
|
-
},
|
|
765
|
-
{
|
|
766
|
-
value: "merged",
|
|
767
|
-
label: "Merged"
|
|
768
|
-
},
|
|
769
|
-
{
|
|
770
|
-
value: "closed",
|
|
771
|
-
label: "Closed"
|
|
772
|
-
}
|
|
773
|
-
];
|
|
774
|
-
/**
|
|
775
|
-
* Whether any of that is work still going on, which is what sets the polling cadence.
|
|
776
|
-
*
|
|
777
|
-
* Here rather than beside `analysisStates` because this module is the DOM-free, filesystem-free
|
|
778
|
-
* arithmetic both halves of the index share, and the renderer needs this one answer without
|
|
779
|
-
* needing an import that reads the jobs directory to get it.
|
|
780
|
-
*
|
|
781
|
-
* A stopped analysis is a state, not an activity. Counting it as live would leave the index asking
|
|
782
|
-
* once a second about a review that finished failing an hour ago, for as long as the tab stayed
|
|
783
|
-
* open.
|
|
784
|
-
*/
|
|
785
|
-
function anythingRunning(states, operations = []) {
|
|
786
|
-
if (operations.length > 0) return true;
|
|
787
|
-
for (const state of states.values()) if (state.phase !== "failed") return true;
|
|
788
|
-
return false;
|
|
789
|
-
}
|
|
790
|
-
//#endregion
|
|
791
480
|
//#region ../../packages/review-harness/src/workspace/fetch_updates_view.ts
|
|
792
481
|
/** A card's and a review page's item. Named for what it reaches: a remote branch, not the list. */
|
|
793
482
|
var FETCH_REVIEW_LABEL = "Fetch remote updates";
|
|
@@ -1033,7 +722,7 @@ function statusPillHtml(status, attributes = "") {
|
|
|
1033
722
|
*/
|
|
1034
723
|
function lifecycleControlHtml(lifecycle, activity, options) {
|
|
1035
724
|
const state = lifecycle?.state ?? "active";
|
|
1036
|
-
const name = escapeHtml$1(lifecycleName
|
|
725
|
+
const name = escapeHtml$1(lifecycleName(lifecycle));
|
|
1037
726
|
const attributes = options.attributes ?? "";
|
|
1038
727
|
if (!options.interactive) return `<span class="prb-state-pill prb-lifecycle-pill" data-lifecycle-pill="${state}"${attributes}>${name}</span>`;
|
|
1039
728
|
const action = lifecyclePillAction(lifecycle);
|
|
@@ -5015,13 +4704,12 @@ ${renderDecisions(processed)}
|
|
|
5015
4704
|
${processed.all.length === 0 ? "<p class=\"prb-pr-empty\">This pull request has no comments yet.</p>" : buckets}`;
|
|
5016
4705
|
}
|
|
5017
4706
|
/**
|
|
5018
|
-
* Re-
|
|
4707
|
+
* Re-checking the discussion, and what the last re-read found.
|
|
5019
4708
|
*
|
|
5020
|
-
*
|
|
5021
|
-
*
|
|
5022
|
-
*
|
|
5023
|
-
*
|
|
5024
|
-
* re-review would be worse off than one who never saw the button.
|
|
4709
|
+
* Two things, said in the order they happen: the threads are fetched and every "already raised"
|
|
4710
|
+
* note corrected, then the findings are reassessed against whatever in the discussion is new to
|
|
4711
|
+
* them. The copy keeps it narrow in the one way that still matters: the code is the commit the
|
|
4712
|
+
* review read, so this is not a re-review and a comment claiming a fix cannot settle it here.
|
|
5025
4713
|
*/
|
|
5026
4714
|
function renderRefresh(refresh) {
|
|
5027
4715
|
if (!refresh?.enabled) return "";
|
|
@@ -5029,10 +4717,12 @@ function renderRefresh(refresh) {
|
|
|
5029
4717
|
return `<div class="prb-pr-refresh">
|
|
5030
4718
|
<button type="button" data-pr-recheck>Re-check the conversation</button>
|
|
5031
4719
|
<span class="prb-pr-refresh-note">
|
|
5032
|
-
Fetches
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
4720
|
+
Fetches the description and every thread again, corrects the "Already raised" notes, then
|
|
4721
|
+
reassesses the issues and questions against anything new. A reply can answer a question,
|
|
4722
|
+
resolve or reopen an issue, or suggest a different severity, and a concern it raises becomes a
|
|
4723
|
+
new issue only once it is confirmed in the reviewed code. A comment saying something is fixed
|
|
4724
|
+
is not taken on trust: the issue stays open until an update sees the fix in the code. Newer
|
|
4725
|
+
commits are left for Update review.
|
|
5036
4726
|
</span>
|
|
5037
4727
|
${last ? `<p class="prb-pr-refresh-last" data-ok="${last.ok}">
|
|
5038
4728
|
Last re-checked ${escapeHtml$1(relativeTime$1(last.at))}. ${escapeHtml$1(last.message)}
|
|
@@ -6050,6 +5740,7 @@ function renderFreshAlternative(command) {
|
|
|
6050
5740
|
*/
|
|
6051
5741
|
function renderUpdateOutcome(outcome) {
|
|
6052
5742
|
if (!outcome) return "";
|
|
5743
|
+
if (outcome.scope === "conversation") return renderRecheckOutcome(outcome);
|
|
6053
5744
|
const state = outcome.ok ? "done" : outcome.stop === "refused" ? "refused" : outcome.stop === "cancelled" ? "cancelled" : "stopped";
|
|
6054
5745
|
const title = outcome.ok ? "Review updated" : outcome.stop === "refused" ? "There was nothing to update against" : outcome.stop === "cancelled" ? "Update cancelled" : "The review was not updated";
|
|
6055
5746
|
const facts = outcome.ok ? updateFacts(outcome) : null;
|
|
@@ -6062,6 +5753,72 @@ function renderUpdateOutcome(outcome) {
|
|
|
6062
5753
|
</div>`;
|
|
6063
5754
|
}
|
|
6064
5755
|
/**
|
|
5756
|
+
* What a Re-check of the PR discussion did, in the same notice an update reports through.
|
|
5757
|
+
*
|
|
5758
|
+
* Five outcomes, each with its own headline, because the one the reviewer most needs to tell apart
|
|
5759
|
+
* is the partial: the discussion came in, so the "already raised" lines are current, but the
|
|
5760
|
+
* findings were not judged against it. That one says why and, when trying again could help, offers it.
|
|
5761
|
+
*/
|
|
5762
|
+
function renderRecheckOutcome(outcome) {
|
|
5763
|
+
const retry = outcome.stop === "analysis-failed" && outcome.conversationOk === true;
|
|
5764
|
+
const { state, title, body } = recheckWords(outcome);
|
|
5765
|
+
const facts = outcome.ok && outcome.stop === null ? recheckFacts(outcome) : null;
|
|
5766
|
+
return `<div class="prb-update-result" data-state="${state}" data-update-result data-recheck-result role="status">
|
|
5767
|
+
<div class="prb-update-result-body">
|
|
5768
|
+
${facts ? `<p class="prb-update-facts"><strong>${escapeHtml$1(title)}</strong>${facts.map((fact) => `<span class="prb-update-fact">${escapeHtml$1(fact)}</span>`).join("")}</p>` : `<p><strong>${escapeHtml$1(title)}</strong> ${escapeHtml$1(body)}</p>`}
|
|
5769
|
+
${retry ? "<p class=\"prb-update-footnote\"><button type=\"button\" class=\"prb-fresh-quiet\" data-pr-recheck>Try again</button></p>" : ""}
|
|
5770
|
+
</div>
|
|
5771
|
+
<button type="button" class="prb-fresh-dismiss" data-update-close>Close</button>
|
|
5772
|
+
</div>`;
|
|
5773
|
+
}
|
|
5774
|
+
function recheckWords(outcome) {
|
|
5775
|
+
if (outcome.stop === "cancelled") return {
|
|
5776
|
+
state: "cancelled",
|
|
5777
|
+
title: "Re-check cancelled",
|
|
5778
|
+
body: "The findings and the discussion shown are exactly as they were."
|
|
5779
|
+
};
|
|
5780
|
+
if (outcome.conversationOk !== true) return {
|
|
5781
|
+
state: "refused",
|
|
5782
|
+
title: "The PR discussion was not re-checked",
|
|
5783
|
+
body: outcome.message
|
|
5784
|
+
};
|
|
5785
|
+
if (outcome.stop === "already-current") return {
|
|
5786
|
+
state: "done",
|
|
5787
|
+
title: "No findings changed",
|
|
5788
|
+
body: outcome.message
|
|
5789
|
+
};
|
|
5790
|
+
if (outcome.stop !== null) return {
|
|
5791
|
+
state: "stopped",
|
|
5792
|
+
title: "Discussion refreshed, findings not reassessed",
|
|
5793
|
+
body: `The latest comments are in, and the notes about who raised what are current. ${outcome.message}`
|
|
5794
|
+
};
|
|
5795
|
+
if (outcome.counts && !findingsChanged(outcome.counts)) return {
|
|
5796
|
+
state: "done",
|
|
5797
|
+
title: "No findings changed",
|
|
5798
|
+
body: "The new comments were read against every open issue and question, and each one stands as it was."
|
|
5799
|
+
};
|
|
5800
|
+
return {
|
|
5801
|
+
state: "done",
|
|
5802
|
+
title: "Findings reassessed against the PR discussion",
|
|
5803
|
+
body: outcome.message
|
|
5804
|
+
};
|
|
5805
|
+
}
|
|
5806
|
+
/** What a Re-check changed, largest news first, in the words an update uses. No commit: none moved. */
|
|
5807
|
+
function recheckFacts(outcome) {
|
|
5808
|
+
const counts = outcome.counts;
|
|
5809
|
+
if (!counts || !findingsChanged(counts)) return null;
|
|
5810
|
+
const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
5811
|
+
return [
|
|
5812
|
+
counts.questionsAnswered ? `${plural(counts.questionsAnswered, "question", "questions")} answered` : "",
|
|
5813
|
+
counts.resolved ? `${plural(counts.resolved, "issue", "issues")} resolved by the discussion` : "",
|
|
5814
|
+
counts.reopened ? `${plural(counts.reopened, "issue", "issues")} reopened` : "",
|
|
5815
|
+
counts.affected ? `${counts.affected} to look at again` : "",
|
|
5816
|
+
counts.newIssues ? plural(counts.newIssues, "new issue", "new issues") : "",
|
|
5817
|
+
counts.gradeChanges ? `${plural(counts.gradeChanges, "grade change", "grade changes")} suggested` : "",
|
|
5818
|
+
counts.decisionsChallenged ? `${plural(counts.decisionsChallenged, "accepted answer", "accepted answers")} put back to you` : ""
|
|
5819
|
+
].filter(Boolean);
|
|
5820
|
+
}
|
|
5821
|
+
/**
|
|
6065
5822
|
* A successful update as a row of facts: the commit it reached, then what changed, largest news first.
|
|
6066
5823
|
*
|
|
6067
5824
|
* Read at a glance rather than parsed. The sentence it replaces ("Updated to 7bc233f: 2 fixed,
|
|
@@ -6446,14 +6203,14 @@ function comparisonNote(code, hasPullRequest) {
|
|
|
6446
6203
|
* button is the subject of the sentence now, so there is one name for one thing and the reviewer
|
|
6447
6204
|
* presses the words they just read.
|
|
6448
6205
|
*
|
|
6449
|
-
*
|
|
6450
|
-
*
|
|
6206
|
+
* It reads no new code: the findings are reassessed against the discussion at the reviewed commit,
|
|
6207
|
+
* and newer commits stay the Update button's.
|
|
6451
6208
|
*/
|
|
6452
6209
|
function conversationNote(report, actions) {
|
|
6453
6210
|
if (!report.conversation.attached) return "";
|
|
6454
6211
|
const when = escapeHtml$1(relativeTime$1(report.conversation.importedAt));
|
|
6455
6212
|
if (!actions) return `The PR conversation was last checked ${when}.`;
|
|
6456
|
-
return `<button type="button" class="prb-fresh-quiet" data-pr-recheck>Re-check the conversation</button> to
|
|
6213
|
+
return `<button type="button" class="prb-fresh-quiet" data-pr-recheck>Re-check the conversation</button> to fetch new comments and reassess issues and questions against them, at the reviewed commit. Last checked ${when}.`;
|
|
6457
6214
|
}
|
|
6458
6215
|
//#endregion
|
|
6459
6216
|
//#region ../../packages/review-harness/src/workspace/progress_view.ts
|
|
@@ -6551,7 +6308,7 @@ function renderProgressView(job, fileCount, cancellable = false, history) {
|
|
|
6551
6308
|
* and can be carried on; an update has the previous review, which is why the two are worded apart.
|
|
6552
6309
|
*/
|
|
6553
6310
|
function renderElapsed(job, cancel, history) {
|
|
6554
|
-
const button = !cancel ? "" : cancel === "Cancel review" ? `<button type="button" class="prb-progress-cancel" data-review-cancel="${escapeHtml$1(job.id)}">Cancel review</button>` : `<button type="button" class="prb-progress-cancel" data-update-cancel="${escapeHtml$1(job.id)}"
|
|
6311
|
+
const button = !cancel ? "" : cancel === "Cancel review" ? `<button type="button" class="prb-progress-cancel" data-review-cancel="${escapeHtml$1(job.id)}">Cancel review</button>` : `<button type="button" class="prb-progress-cancel" data-update-cancel="${escapeHtml$1(job.id)}">${cancel}</button>`;
|
|
6555
6312
|
return `<div class="prb-progress-foot">
|
|
6556
6313
|
<div class="prb-progress-runtime">
|
|
6557
6314
|
<p class="prb-progress-elapsed" data-started-at="${job.startedAt}">${escapeHtml$1(runningFor(job.startedAt))}</p>${renderHistory$1(history)}
|
|
@@ -6599,11 +6356,14 @@ function renderHistory$1(history) {
|
|
|
6599
6356
|
*/
|
|
6600
6357
|
function renderUpdateProgress(job, cancellable = false, history) {
|
|
6601
6358
|
if (!isLiveJob(job)) return "";
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6359
|
+
const recheck = job.scope === "conversation";
|
|
6360
|
+
const title = recheck ? "Re-checking the PR discussion" : "Updating this review";
|
|
6361
|
+
const lead = recheck ? "Issues and questions will be updated from the discussion when reassessment finishes." : "The previous review will be replaced when reassessment finishes.";
|
|
6362
|
+
return `<div class="prb-update-progress" data-update-phase="${escapeHtml$1(job.phase)}"${recheck ? " data-update-scope=\"conversation\"" : ""} role="status" aria-live="polite">
|
|
6363
|
+
<h3 class="prb-doc-title"><span class="prb-spinner" aria-hidden="true"></span>${title}</h3>
|
|
6364
|
+
<p class="prb-progress-lead">${lead}</p>
|
|
6605
6365
|
${renderSteps(job)}
|
|
6606
|
-
${renderElapsed(job, cancellable ? "Cancel update" : null, history)}
|
|
6366
|
+
${renderElapsed(job, cancellable ? recheck ? "Cancel re-check" : "Cancel update" : null, history)}
|
|
6607
6367
|
</div>`;
|
|
6608
6368
|
}
|
|
6609
6369
|
//#endregion
|
|
@@ -7009,7 +6769,7 @@ function renderAttributionAge(view) {
|
|
|
7009
6769
|
return `<div class="prb-discussion-age">
|
|
7010
6770
|
<details class="prb-discussion-age-detail">
|
|
7011
6771
|
<summary class="prb-discussion-age-summary">PR discussion checked ${escapeHtml$1(imported)}</summary>
|
|
7012
|
-
<p class="prb-discussion-age-body">
|
|
6772
|
+
<p class="prb-discussion-age-body">The issues and questions below, their “already raised” notes and whether each thread was still open describe the pull request as it stood when its discussion was fetched. A reply or a resolved thread since then is not reflected here until the conversation is re-checked, which also reassesses the issues and questions against it.</p>
|
|
7013
6773
|
</details>
|
|
7014
6774
|
<span class="prb-discussion-age-sep" aria-hidden="true">·</span>
|
|
7015
6775
|
<button type="button" class="prb-doc-note-link" data-view-nav="prcontext">Re-check in PR context</button>
|
|
@@ -10324,7 +10084,8 @@ function renderReviewsPage(view) {
|
|
|
10324
10084
|
const collectedHome = /* @__PURE__ */ new Set();
|
|
10325
10085
|
for (const group of ordered) {
|
|
10326
10086
|
const collected = (memberOf.get(group.key) ?? []).length > 0;
|
|
10327
|
-
const
|
|
10087
|
+
const pinned = group.latest.pinned === true;
|
|
10088
|
+
const home = homeSection(pinned, group.latest.lifecycle, activityOf(group), collected, "all");
|
|
10328
10089
|
if (home === void 0) collectedHome.add(group);
|
|
10329
10090
|
sections[home ?? sectionOf$1(group.latest.lifecycle, activityOf(group))].push(group);
|
|
10330
10091
|
}
|
|
@@ -12746,8 +12507,15 @@ feature: E</pre>
|
|
|
12746
12507
|
matters and what is needed, with the original question and its files under <b>Technical
|
|
12747
12508
|
context</b>. Any answer so far stays on the card.</p>
|
|
12748
12509
|
<p>When the review came with a pull request discussion, the line above the issues says when it
|
|
12749
|
-
was last checked.
|
|
12750
|
-
in PR context</b> takes you to bring it up to date.</p>
|
|
12510
|
+
was last checked. The issues, the questions and their “already raised” notes describe the
|
|
12511
|
+
discussion as it was then; <b>Re-check in PR context</b> takes you to bring it up to date.</p>
|
|
12512
|
+
<p>Re-checking fetches the latest comments and reassesses every issue and question against
|
|
12513
|
+
anything new in them. A reply can answer a question, resolve or reopen an issue, or suggest a
|
|
12514
|
+
different severity or importance, and a concern raised in a comment becomes a new issue once it
|
|
12515
|
+
is confirmed in the reviewed code. A thread marked resolved, or a comment saying something is
|
|
12516
|
+
fixed, does not resolve an issue on its own: it stays open until an Update sees the fix in the
|
|
12517
|
+
code. Re-checking reads no new commits; Update review does. When the comments only change how
|
|
12518
|
+
much one finding matters, <b>Reassess</b> on its card asks about that one alone.</p>
|
|
12751
12519
|
<p>Resolved and dismissed issues are in the closed list at the bottom. One the new commits fixed
|
|
12752
12520
|
leads with what the code now does, when the update could see it.</p>`
|
|
12753
12521
|
},
|
|
@@ -12760,7 +12528,7 @@ feature: E</pre>
|
|
|
12760
12528
|
<li><b>Update</b> brings the review up to date with commits added since it was made. Its
|
|
12761
12529
|
findings are checked against the new commits rather than thrown away, and each says whether
|
|
12762
12530
|
the new commits affected it. In the workspace, Update review appears on a review whose
|
|
12763
|
-
branch has moved on.</li>
|
|
12531
|
+
branch has moved on. It also reads the pull request discussion, as Re-check does.</li>
|
|
12764
12532
|
<li><b>Reanalyse</b> analyses the same commit again, for a fresh judgement of the same change,
|
|
12765
12533
|
for example with a different model.</li>
|
|
12766
12534
|
<li><b>Fresh review</b> reviews the target again as a new review, and keeps the existing one
|
|
@@ -12825,7 +12593,14 @@ feature: E</pre>
|
|
|
12825
12593
|
a time unless <code>--concurrency</code> says otherwise.</p>
|
|
12826
12594
|
<p>One that fails does not stop the others. Ctrl-C stops them all, and running the same command
|
|
12827
12595
|
again carries each one on from where it got to. Add <code>--queue</code> to put each review on
|
|
12828
|
-
your review queue as soon as it exists, rather than when it is ready.</p
|
|
12596
|
+
your review queue as soon as it exists, rather than when it is ready.</p>
|
|
12597
|
+
<p>To keep a batch together, add it to a collection: <code>--collection "Release review"</code>
|
|
12598
|
+
for one you have, or <code>--new-collection "Friday batch"</code> to make one, with
|
|
12599
|
+
<code>--also-queue</code> to put the reviews on your review queue as well. In New review, choose
|
|
12600
|
+
the same from <b>Add to</b>: <b>Reviews list only</b>, <b>Review queue</b>, a collection, or
|
|
12601
|
+
<b>Create collection…</b>, and tick <b>Also add to review queue</b> for both. When the batch
|
|
12602
|
+
ends, <b>Open collection</b> shows it. A retry adds only what is missing, to the same
|
|
12603
|
+
collection.</p>`
|
|
12829
12604
|
},
|
|
12830
12605
|
{
|
|
12831
12606
|
id: "review-lenses",
|
|
@@ -13118,50 +12893,6 @@ function renderNotificationRow(notification, gone) {
|
|
|
13118
12893
|
</li>`;
|
|
13119
12894
|
}
|
|
13120
12895
|
//#endregion
|
|
13121
|
-
//#region ../../packages/review-harness/src/workspace/collections_live.ts
|
|
13122
|
-
/**
|
|
13123
|
-
* Collections as they relate to reviews that actually exist.
|
|
13124
|
-
*
|
|
13125
|
-
* `collections.ts` stores lineage keys and must not learn to read the review store, for the cycle
|
|
13126
|
-
* `queue_live.ts` describes: the prune in `store.ts` imports it. Anything that asks both questions
|
|
13127
|
-
* lives here, which imports both and is imported by neither.
|
|
13128
|
-
*/
|
|
13129
|
-
/**
|
|
13130
|
-
* Drop members whose lineage has no review any more, deleted elsewhere or pruned by age, and store
|
|
13131
|
-
* the result if that changed anything. Read against every group, not the windowed list, so a branch
|
|
13132
|
-
* the recency window happens to hide keeps its place.
|
|
13133
|
-
*
|
|
13134
|
-
* Compacted again under the lock rather than written from what was read, so a member somebody added
|
|
13135
|
-
* since is kept. On a failed write the compacted copy still stands in, so the page draws what exists.
|
|
13136
|
-
*/
|
|
13137
|
-
function settleCollections(groups) {
|
|
13138
|
-
const live = new Set(groups.map((group) => group.key));
|
|
13139
|
-
const compact = (collections) => collections.some((collection) => collection.members.some((key) => !live.has(key))) ? collections.map((collection) => ({
|
|
13140
|
-
...collection,
|
|
13141
|
-
members: collection.members.filter((key) => live.has(key))
|
|
13142
|
-
})) : null;
|
|
13143
|
-
const read = readCollections();
|
|
13144
|
-
const compacted = compact(read);
|
|
13145
|
-
if (compacted === null) return read;
|
|
13146
|
-
return updateCollections((current) => compact(current)) ?? compacted;
|
|
13147
|
-
}
|
|
13148
|
-
/**
|
|
13149
|
-
* The lineage each review id belongs to, for the routes: the page sends the ids on its cards, and
|
|
13150
|
-
* the lineage is the server's to derive. An id with no review, or one that cannot be summarised, is
|
|
13151
|
-
* left out, and the caller says so.
|
|
13152
|
-
*/
|
|
13153
|
-
function lineageKeysFor(ids) {
|
|
13154
|
-
const keys = /* @__PURE__ */ new Map();
|
|
13155
|
-
for (const id of new Set(ids)) {
|
|
13156
|
-
const workspace = loadWorkspace(id);
|
|
13157
|
-
if (!workspace) continue;
|
|
13158
|
-
try {
|
|
13159
|
-
keys.set(id, lineageKey(summarise(workspace)));
|
|
13160
|
-
} catch {}
|
|
13161
|
-
}
|
|
13162
|
-
return keys;
|
|
13163
|
-
}
|
|
13164
|
-
//#endregion
|
|
13165
12896
|
//#region ../../packages/review-harness/src/workspace/working_folders.ts
|
|
13166
12897
|
/**
|
|
13167
12898
|
* The local Git working folders PR Review Buddy can find right now.
|
|
@@ -13370,7 +13101,13 @@ function realAddReviewDeps() {
|
|
|
13370
13101
|
} : null;
|
|
13371
13102
|
},
|
|
13372
13103
|
agentName: agentNameOf,
|
|
13373
|
-
home: homedir()
|
|
13104
|
+
home: homedir(),
|
|
13105
|
+
collections: () => readCollections().map((collection) => ({
|
|
13106
|
+
id: collection.id,
|
|
13107
|
+
name: collection.name
|
|
13108
|
+
})),
|
|
13109
|
+
openDestination,
|
|
13110
|
+
deliver
|
|
13374
13111
|
};
|
|
13375
13112
|
}
|
|
13376
13113
|
function modelHelpOf(agentId) {
|
|
@@ -13511,7 +13248,8 @@ async function addReviewOptions(deps) {
|
|
|
13511
13248
|
kind: "refused",
|
|
13512
13249
|
error: unresolvedAgent(resolution)
|
|
13513
13250
|
},
|
|
13514
|
-
canChooseFolder: deps.chooseFolder !== null
|
|
13251
|
+
canChooseFolder: deps.chooseFolder !== null,
|
|
13252
|
+
collections: deps.collections()
|
|
13515
13253
|
}
|
|
13516
13254
|
};
|
|
13517
13255
|
}
|
|
@@ -13625,12 +13363,13 @@ function pendingBatchRows() {
|
|
|
13625
13363
|
function forgetFinished(now) {
|
|
13626
13364
|
for (const [id, batch] of batches) if (batch.finishedAt !== null && now - Math.max(batch.finishedAt, batch.seenAt) > FINISHED_BATCH_TTL_MS) batches.delete(id);
|
|
13627
13365
|
}
|
|
13628
|
-
function newBatch(rows) {
|
|
13366
|
+
function newBatch(rows, destination = { queue: false }) {
|
|
13629
13367
|
forgetFinished(Date.now());
|
|
13630
13368
|
const now = Date.now();
|
|
13631
13369
|
const batch = {
|
|
13632
13370
|
id: randomUUID(),
|
|
13633
13371
|
rows,
|
|
13372
|
+
destination,
|
|
13634
13373
|
finishedAt: now,
|
|
13635
13374
|
seenAt: now,
|
|
13636
13375
|
running: 0,
|
|
@@ -13657,7 +13396,14 @@ function fail(row, error) {
|
|
|
13657
13396
|
row.reason = refusal.error;
|
|
13658
13397
|
if (refusal.detail) row.detail = refusal.detail;
|
|
13659
13398
|
}
|
|
13660
|
-
var rowsOf = (batch) => batch.rows.map((row) => ({
|
|
13399
|
+
var rowsOf = (batch) => batch.rows.map((row) => ({
|
|
13400
|
+
...row,
|
|
13401
|
+
...missToRetry(row, batch) ? { addAgain: true } : {}
|
|
13402
|
+
}));
|
|
13403
|
+
/** What of the batch's destination this row's review is still worth adding to, or null. */
|
|
13404
|
+
function missToRetry(row, batch) {
|
|
13405
|
+
return row.reviewId && row.delivered ? retryableMiss(row.delivered, batch.destination) : null;
|
|
13406
|
+
}
|
|
13661
13407
|
/**
|
|
13662
13408
|
* Create the review for a row's target, or find the one it has, under the per-target lock. The
|
|
13663
13409
|
* caller holds an analysis slot: nothing is created without one, so nothing created waits.
|
|
@@ -13717,6 +13463,7 @@ async function stillThere(row, target, what, deps) {
|
|
|
13717
13463
|
*/
|
|
13718
13464
|
async function retryReview(row, batch, deps) {
|
|
13719
13465
|
const reviewId = row.reviewId;
|
|
13466
|
+
deliverRow(row, batch, deps);
|
|
13720
13467
|
const kept = deps.resumeOf(reviewId);
|
|
13721
13468
|
begin(row);
|
|
13722
13469
|
if (kept) {
|
|
@@ -13730,6 +13477,14 @@ async function retryReview(row, batch, deps) {
|
|
|
13730
13477
|
* target, the old review, and still `stale`. So the row keeps offering the fresh start, and a plain
|
|
13731
13478
|
* Retry finds the old commit gone again, rather than resuming the old review against it.
|
|
13732
13479
|
*/
|
|
13480
|
+
/**
|
|
13481
|
+
* Put a row's review where its batch goes, as soon as it has one. Idempotent, so it runs again on
|
|
13482
|
+
* every retry and fills in only what is missing. A batch for the reviews list alone does nothing.
|
|
13483
|
+
*/
|
|
13484
|
+
function deliverRow(row, batch, deps) {
|
|
13485
|
+
if (!row.reviewId || !batch.destination.queue && !batch.destination.collection) return;
|
|
13486
|
+
row.delivered = deps.deliver(row.reviewId, batch.destination);
|
|
13487
|
+
}
|
|
13733
13488
|
function keepFreshPending(row, replaced) {
|
|
13734
13489
|
if (row.status === "failed" && !replaced) row.stale = true;
|
|
13735
13490
|
}
|
|
@@ -13768,13 +13523,18 @@ async function startPullRequestBatch(body, deps) {
|
|
|
13768
13523
|
}
|
|
13769
13524
|
const model = modelOf(body?.model);
|
|
13770
13525
|
if (isAnswer(model)) return model;
|
|
13526
|
+
const requested = destinationRequestOf(body?.destination);
|
|
13527
|
+
if (isAnswer(requested)) return requested;
|
|
13771
13528
|
const agent = await agentFor(body?.agent, deps);
|
|
13772
13529
|
if (isAnswer(agent)) return agent;
|
|
13530
|
+
const opened = deps.openDestination(requested);
|
|
13531
|
+
if (!opened.ok) return destinationRefusal(opened, deps);
|
|
13532
|
+
const destination = opened.destination;
|
|
13773
13533
|
const batch = newBatch(items.map((item) => ({
|
|
13774
13534
|
key: item.key,
|
|
13775
13535
|
label: `${item.owner}/${item.repo} #${item.number}`,
|
|
13776
13536
|
status: "waiting"
|
|
13777
|
-
})));
|
|
13537
|
+
})), destination);
|
|
13778
13538
|
batch.rows.forEach((row, index) => noteRow(row, {
|
|
13779
13539
|
repoPath: items[index].folder,
|
|
13780
13540
|
prNumber: items[index].number
|
|
@@ -13813,6 +13573,7 @@ async function startPullRequestBatch(body, deps) {
|
|
|
13813
13573
|
}
|
|
13814
13574
|
claimed.set(identity, index);
|
|
13815
13575
|
const prepared = await prepareRow(row, target, agent.agentId, model, deps, fresh);
|
|
13576
|
+
deliverRow(row, batch, deps);
|
|
13816
13577
|
if (fresh) {
|
|
13817
13578
|
resolved[index] = target;
|
|
13818
13579
|
replaced = true;
|
|
@@ -13834,15 +13595,79 @@ async function startPullRequestBatch(body, deps) {
|
|
|
13834
13595
|
status: 200,
|
|
13835
13596
|
body: {
|
|
13836
13597
|
batchId: batch.id,
|
|
13837
|
-
rows: rowsOf(batch)
|
|
13598
|
+
rows: rowsOf(batch),
|
|
13599
|
+
destination
|
|
13838
13600
|
}
|
|
13839
13601
|
};
|
|
13840
13602
|
}
|
|
13841
13603
|
/**
|
|
13604
|
+
* What the panel's Add to asked for. A collection it picked comes as an id and is matched as one,
|
|
13605
|
+
* never as a name; one it is to make comes as a name. Nothing, or the reviews list, is no
|
|
13606
|
+
* destination at all.
|
|
13607
|
+
*/
|
|
13608
|
+
function destinationRequestOf(raw) {
|
|
13609
|
+
const choose = () => refuse(400, "Choose where the reviews go from Add to.", { field: "destination" });
|
|
13610
|
+
if (raw === void 0 || raw === null) return { queue: false };
|
|
13611
|
+
if (typeof raw !== "object") return choose();
|
|
13612
|
+
const value = raw;
|
|
13613
|
+
const queue = value.queue === true;
|
|
13614
|
+
switch (value.kind) {
|
|
13615
|
+
case "list": return { queue: false };
|
|
13616
|
+
case "queue": return { queue: true };
|
|
13617
|
+
case "collection": return typeof value.id === "string" && value.id !== "" ? {
|
|
13618
|
+
collectionId: value.id,
|
|
13619
|
+
queue
|
|
13620
|
+
} : choose();
|
|
13621
|
+
case "new-collection": return typeof value.name === "string" ? {
|
|
13622
|
+
newCollection: value.name,
|
|
13623
|
+
queue
|
|
13624
|
+
} : choose();
|
|
13625
|
+
default: return choose();
|
|
13626
|
+
}
|
|
13627
|
+
}
|
|
13628
|
+
/** A destination that could not be opened, said beside Add to, with the collections as they are now. */
|
|
13629
|
+
function destinationRefusal(refusal, deps) {
|
|
13630
|
+
const extra = {
|
|
13631
|
+
field: "destination",
|
|
13632
|
+
collections: deps.collections()
|
|
13633
|
+
};
|
|
13634
|
+
switch (refusal.reason) {
|
|
13635
|
+
case "missing": return refuse(409, "That collection no longer exists. Choose again from Add to. Nothing was started.", extra);
|
|
13636
|
+
case "empty":
|
|
13637
|
+
case "too-long":
|
|
13638
|
+
case "duplicate": return refuse(400, `${nameProblemText(refusal.reason)} Nothing was started.`, extra);
|
|
13639
|
+
case "full": return refuse(409, `You have 100 collections, which is as many as there can be. Delete one, or add these to one you have. Nothing was started.`, extra);
|
|
13640
|
+
default: return refuse(500, "The collection could not be saved. Nothing was started. Try again.", extra);
|
|
13641
|
+
}
|
|
13642
|
+
}
|
|
13643
|
+
/**
|
|
13644
|
+
* The batch's destination as the panel shows it, under the collection's current name: a rename
|
|
13645
|
+
* keeps the id, so the batch still delivers to it, and the result should call it what it is now.
|
|
13646
|
+
*/
|
|
13647
|
+
function destinationNow(batch, deps) {
|
|
13648
|
+
const collection = batch.destination.collection;
|
|
13649
|
+
if (!collection || !deps) return batch.destination;
|
|
13650
|
+
const current = deps.collections().find((one) => one.id === collection.id);
|
|
13651
|
+
if (!current) return {
|
|
13652
|
+
...batch.destination,
|
|
13653
|
+
collection: {
|
|
13654
|
+
...collection,
|
|
13655
|
+
deleted: true
|
|
13656
|
+
}
|
|
13657
|
+
};
|
|
13658
|
+
return current.name !== collection.name ? {
|
|
13659
|
+
...batch.destination,
|
|
13660
|
+
collection: {
|
|
13661
|
+
...collection,
|
|
13662
|
+
name: current.name
|
|
13663
|
+
}
|
|
13664
|
+
} : batch.destination;
|
|
13665
|
+
}
|
|
13666
|
+
/**
|
|
13842
13667
|
* A batch's rows as they stand. Asking counts as watching: a batch an open panel is still asking
|
|
13843
13668
|
* about is never forgotten, so its Retry stays good for as long as the panel is open.
|
|
13844
13669
|
*/
|
|
13845
|
-
function batchRows(id) {
|
|
13670
|
+
function batchRows(id, deps) {
|
|
13846
13671
|
const batch = batches.get(id);
|
|
13847
13672
|
if (!batch) return lostBatch();
|
|
13848
13673
|
batch.seenAt = Date.now();
|
|
@@ -13850,7 +13675,34 @@ function batchRows(id) {
|
|
|
13850
13675
|
status: 200,
|
|
13851
13676
|
body: {
|
|
13852
13677
|
rows: rowsOf(batch),
|
|
13853
|
-
finished: batch.finishedAt !== null
|
|
13678
|
+
finished: batch.finishedAt !== null,
|
|
13679
|
+
destination: destinationNow(batch, deps)
|
|
13680
|
+
}
|
|
13681
|
+
};
|
|
13682
|
+
}
|
|
13683
|
+
/**
|
|
13684
|
+
* Put one row's review where the batch goes again, and nothing else: for a review that exists but
|
|
13685
|
+
* missed its collection or the queue (a full queue, a write that failed). The review is not run
|
|
13686
|
+
* again. A row with no review yet is Retry's business, which delivers once the review exists.
|
|
13687
|
+
*/
|
|
13688
|
+
function deliverBatchRow(id, key, deps) {
|
|
13689
|
+
const batch = batches.get(id);
|
|
13690
|
+
if (!batch) return lostBatch();
|
|
13691
|
+
const row = batch.rows.find((one) => one.key === key);
|
|
13692
|
+
if (!row) return refuse(404, "That review is not part of this batch.");
|
|
13693
|
+
const again = missToRetry(row, batch);
|
|
13694
|
+
if (!again || !row.reviewId) return refuse(409, "Only a review that missed somewhere it can still be added can be added again.");
|
|
13695
|
+
row.delivered = {
|
|
13696
|
+
...row.delivered,
|
|
13697
|
+
...deps.deliver(row.reviewId, again)
|
|
13698
|
+
};
|
|
13699
|
+
batch.seenAt = Date.now();
|
|
13700
|
+
return {
|
|
13701
|
+
status: 200,
|
|
13702
|
+
body: {
|
|
13703
|
+
rows: rowsOf(batch),
|
|
13704
|
+
finished: batch.finishedAt !== null,
|
|
13705
|
+
destination: destinationNow(batch, deps)
|
|
13854
13706
|
}
|
|
13855
13707
|
};
|
|
13856
13708
|
}
|
|
@@ -17867,7 +17719,7 @@ async function handleAddReview(req, res, pathname, options) {
|
|
|
17867
17719
|
if (route === "options") return addReviewOptions(deps);
|
|
17868
17720
|
if (route === "folders") return listFolders(deps);
|
|
17869
17721
|
const batch = /^batches\/([0-9a-f-]{36})$/.exec(route);
|
|
17870
|
-
if (batch) return batchRows(batch[1]);
|
|
17722
|
+
if (batch) return batchRows(batch[1], deps);
|
|
17871
17723
|
return null;
|
|
17872
17724
|
}
|
|
17873
17725
|
if (req.method !== "POST") return {
|
|
@@ -17882,6 +17734,8 @@ async function handleAddReview(req, res, pathname, options) {
|
|
|
17882
17734
|
if (route === "branch") return startBranchReview(body, deps);
|
|
17883
17735
|
const retry = /^batches\/([0-9a-f-]{36})\/retry$/.exec(route);
|
|
17884
17736
|
if (retry) return retryBatchRow(retry[1], body?.key, body?.fresh);
|
|
17737
|
+
const again = /^batches\/([0-9a-f-]{36})\/deliver$/.exec(route);
|
|
17738
|
+
if (again) return deliverBatchRow(again[1], body?.key, deps);
|
|
17885
17739
|
return null;
|
|
17886
17740
|
};
|
|
17887
17741
|
const result = await answer();
|
|
@@ -18336,10 +18190,15 @@ async function sendWorkspacePage(res, workspace) {
|
|
|
18336
18190
|
const updateJob = liveUpdateFor(workspace.id);
|
|
18337
18191
|
const reviewJob = workspace.jobId ? loadJob(workspace.jobId) : void 0;
|
|
18338
18192
|
const kissJob = kissRun ? loadJob(kissRun.jobId) : void 0;
|
|
18339
|
-
const
|
|
18340
|
-
|
|
18341
|
-
|
|
18342
|
-
|
|
18193
|
+
const unseen = [workspace.lastUpdate, workspace.lastRecheck].filter((outcome) => outcome !== void 0 && !outcome.acknowledged);
|
|
18194
|
+
const notice = unseen.sort((a, b) => b.at - a.at)[0];
|
|
18195
|
+
if (notice) {
|
|
18196
|
+
const read = new Set(unseen.map((outcome) => outcome.at));
|
|
18197
|
+
mutateWorkspace(workspace.id, (current) => {
|
|
18198
|
+
if (current.lastUpdate && read.has(current.lastUpdate.at)) current.lastUpdate.acknowledged = true;
|
|
18199
|
+
if (current.lastRecheck && read.has(current.lastRecheck.at)) current.lastRecheck.acknowledged = true;
|
|
18200
|
+
});
|
|
18201
|
+
}
|
|
18343
18202
|
const everyReview = recentWorkspaces(Number.MAX_SAFE_INTEGER);
|
|
18344
18203
|
const html = renderWorkspace({
|
|
18345
18204
|
session: workspace.session,
|
|
@@ -18568,33 +18427,78 @@ async function kissFreshnessOrNull(workspace) {
|
|
|
18568
18427
|
}
|
|
18569
18428
|
}
|
|
18570
18429
|
/**
|
|
18571
|
-
* Re-
|
|
18430
|
+
* Re-check the pull request discussion, at the reviewer's request: re-read it, then reconcile the
|
|
18431
|
+
* findings with whatever in it is new to them, at the commit the review read.
|
|
18572
18432
|
*
|
|
18573
|
-
* Answers 200 with `ok: false` rather than an error status when it does not work
|
|
18574
|
-
*
|
|
18575
|
-
*
|
|
18576
|
-
*
|
|
18433
|
+
* Answers 200 with `ok: false` rather than an error status when it does not work, for the reason the
|
|
18434
|
+
* update route does: a failure is an outcome the page has to explain in a sentence. 409 when an update
|
|
18435
|
+
* or another Re-check holds the review, which the reloaded page then shows running.
|
|
18436
|
+
*
|
|
18437
|
+
* Saved in two ways, because two different things can come of it. A reassessment that was applied
|
|
18438
|
+
* goes on the way an update does, onto the review as it is on disk. Anything short of that changed
|
|
18439
|
+
* only the conversation and the "already raised" lines, and goes on the way a re-read always has,
|
|
18440
|
+
* through `saveRefresh`, which will not put an older conversation over a newer one.
|
|
18577
18441
|
*/
|
|
18578
18442
|
async function handleRefreshPr(res, workspace) {
|
|
18579
18443
|
const reattributing = JSON.stringify(workspace.session.result);
|
|
18580
|
-
|
|
18581
|
-
const startedAt = Date.now();
|
|
18444
|
+
const begun = Date.now();
|
|
18582
18445
|
let outcome;
|
|
18583
18446
|
try {
|
|
18584
|
-
outcome = await
|
|
18585
|
-
} catch {
|
|
18586
|
-
|
|
18447
|
+
outcome = await track(recheckConversation(workspace, { source: "workspace" }));
|
|
18448
|
+
} catch (error) {
|
|
18449
|
+
if (error instanceof UpdateAlreadyRunningError) return send(res, 409, { error: error.message });
|
|
18450
|
+
outcome = {
|
|
18451
|
+
at: Date.now(),
|
|
18452
|
+
ok: false,
|
|
18453
|
+
message: error instanceof RefreshUnavailableError || error instanceof ReviewBeingDeletedError ? error.message : "The findings could not be reassessed. Nothing about them has changed.",
|
|
18454
|
+
stop: "analysis-failed",
|
|
18455
|
+
fetchFailed: false,
|
|
18456
|
+
moved: false,
|
|
18457
|
+
...rereadSince(workspace, begun) ? { conversationOk: workspace.lastPrRefresh.ok } : {},
|
|
18458
|
+
fromSha: null,
|
|
18459
|
+
toSha: null,
|
|
18460
|
+
scope: "conversation"
|
|
18461
|
+
};
|
|
18587
18462
|
}
|
|
18588
|
-
|
|
18463
|
+
const applied = outcome.ok && outcome.stop === null;
|
|
18464
|
+
const cancelled = outcome.stop === "cancelled";
|
|
18465
|
+
workspace.lastRecheck = outcome;
|
|
18466
|
+
const reread = !applied && !cancelled ? rereadSince(workspace, begun) : void 0;
|
|
18467
|
+
if (reread) saveRefresh(workspace, reread, reattributing);
|
|
18468
|
+
const saved = mutateWorkspace(workspace.id, (fresh) => applied ? {
|
|
18469
|
+
...mergeUpdateInto(fresh, workspace),
|
|
18470
|
+
lastRecheck: outcome
|
|
18471
|
+
} : {
|
|
18472
|
+
...fresh,
|
|
18473
|
+
lastRecheck: outcome
|
|
18474
|
+
});
|
|
18475
|
+
const refresh = reread ?? (applied ? workspace.lastPrRefresh : void 0);
|
|
18589
18476
|
record({
|
|
18590
18477
|
event: "pr_conversation_rechecked",
|
|
18591
18478
|
reviewId: workspace.id,
|
|
18592
|
-
ok: outcome.
|
|
18593
|
-
changed:
|
|
18594
|
-
attributionsRestated:
|
|
18595
|
-
attributionsWithdrawn:
|
|
18479
|
+
ok: outcome.conversationOk === true,
|
|
18480
|
+
changed: refresh?.changes ? !isUnchanged(refresh.changes) : false,
|
|
18481
|
+
attributionsRestated: refresh?.attributionsRestated ?? 0,
|
|
18482
|
+
attributionsWithdrawn: refresh?.attributionsWithdrawn ?? 0,
|
|
18483
|
+
reassessment: reassessmentOf(outcome),
|
|
18484
|
+
resolved: outcome.counts?.resolved ?? 0,
|
|
18485
|
+
questionsAnswered: outcome.counts?.questionsAnswered ?? 0,
|
|
18486
|
+
newIssues: outcome.counts?.newIssues ?? 0
|
|
18596
18487
|
});
|
|
18597
|
-
send(res, 200, outcome);
|
|
18488
|
+
send(res, 200, saved?.lastRecheck ?? outcome);
|
|
18489
|
+
}
|
|
18490
|
+
/** The re-read this request made, if it made one: the copy's record, begun no earlier than the request. */
|
|
18491
|
+
function rereadSince(workspace, begun) {
|
|
18492
|
+
const refresh = workspace.lastPrRefresh;
|
|
18493
|
+
return refresh && (refresh.startedAt ?? refresh.at) >= begun ? refresh : void 0;
|
|
18494
|
+
}
|
|
18495
|
+
/** What a Re-check did to the findings, as the closed set the log records. */
|
|
18496
|
+
function reassessmentOf(outcome) {
|
|
18497
|
+
if (outcome.stop === "cancelled") return "cancelled";
|
|
18498
|
+
if (outcome.stop === "already-current") return "not-needed";
|
|
18499
|
+
if (outcome.stop === "analysis-failed") return "failed";
|
|
18500
|
+
if (outcome.stop !== null) return "skipped";
|
|
18501
|
+
return outcome.counts && findingsChanged(outcome.counts) ? "changed" : "unchanged";
|
|
18598
18502
|
}
|
|
18599
18503
|
/**
|
|
18600
18504
|
* Bring the whole review up to date with the checkout, at the reviewer's request.
|