@plannotator/pi-extension 0.27.10 → 0.27.11
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/generated/ai/providers/opencode-sdk.ts +70 -46
- package/generated/ai/types.ts +1 -1
- package/generated/config.ts +29 -0
- package/generated/feedback-archive.ts +500 -0
- package/package.json +1 -1
- package/plannotator-browser.ts +1 -0
- package/plannotator.html +132 -132
- package/review-editor.html +2 -2
- package/server/ai-runtime.ts +12 -5
- package/server/feedback-archive.test.ts +357 -0
- package/server/serverAnnotate.ts +71 -12
- package/server/serverPlan.ts +41 -1
- package/server/serverReview.ts +104 -5
package/server/serverReview.ts
CHANGED
|
@@ -6,7 +6,8 @@ import { basename, resolve as resolvePath } from "node:path";
|
|
|
6
6
|
|
|
7
7
|
import { SingleFlight } from "../generated/single-flight.ts";
|
|
8
8
|
import { contentHash, deleteDraft } from "../generated/draft.ts";
|
|
9
|
-
import { loadConfig, saveConfig, detectGitUser, getServerConfig, parseReviewAnalysisConfig, resolveAIEnabled, resolveSharingEnabled, resolveCursorSandbox, resolveGuideHistory, resolveGuideShareUrl } from "../generated/config.ts";
|
|
9
|
+
import { loadConfig, saveConfig, detectGitUser, getServerConfig, parseReviewAnalysisConfig, resolveAIEnabled, resolveSharingEnabled, resolveCursorSandbox, resolveFeedbackHistory, resolveGuideHistory, resolveGuideShareUrl } from "../generated/config.ts";
|
|
10
|
+
import { appendFeedbackRecord, countChangedFiles, deriveFeedbackProject, type FeedbackDecision, type FeedbackReviewTarget } from "../generated/feedback-archive.ts";
|
|
10
11
|
import { isFaviconStyle, type FaviconStyle } from "../generated/favicon.ts";
|
|
11
12
|
|
|
12
13
|
export type {
|
|
@@ -295,6 +296,15 @@ export async function startReviewServer(options: {
|
|
|
295
296
|
* once a pool checkout is ready.
|
|
296
297
|
*/
|
|
297
298
|
prPatchIncomplete?: boolean;
|
|
299
|
+
/**
|
|
300
|
+
* Detected project name, used to key the durable feedback archive
|
|
301
|
+
* (`feedback/{project}/`). Mirrors the annotate server's `project` option.
|
|
302
|
+
* Callers should pass `detectProjectName()`; without it the server falls
|
|
303
|
+
* back to deriving a name from the review's working directory, which is
|
|
304
|
+
* wrong in PR mode (no `gitContext`, and `--local` points `agentCwd` at a
|
|
305
|
+
* `pool/pr-<n>` checkout, so records would bucket under `pr-123`).
|
|
306
|
+
*/
|
|
307
|
+
project?: string;
|
|
298
308
|
/** Working directory for agent processes (e.g., --local worktree). Independent of diff pipeline. */
|
|
299
309
|
agentCwd?: string;
|
|
300
310
|
/** Local parent directory containing multiple child VCS repositories. */
|
|
@@ -828,6 +838,77 @@ export async function startReviewServer(options: {
|
|
|
828
838
|
return `${draftKey}:${currentDiffType}${isPRMode ? `:${currentPRDiffScope}` : ""}${currentContextRevision ? `:${currentContextRevision}` : ""}`;
|
|
829
839
|
}
|
|
830
840
|
|
|
841
|
+
// --- Durable feedback archive (Node mirror of packages/server/review.ts) ---
|
|
842
|
+
//
|
|
843
|
+
// Code review persisted nothing on submit: the draft was deleted, the
|
|
844
|
+
// decision promise settled, and a timed-out agent meant the review existed
|
|
845
|
+
// nowhere. Every submission now appends one record to
|
|
846
|
+
// feedback/{project}/index.jsonl BEFORE the draft is deleted, carrying diff
|
|
847
|
+
// IDENTITY (refs, view, snapshot id, size) but never the patch bytes.
|
|
848
|
+
// Project bucketing: prefer the caller's detected project name. The cwd
|
|
849
|
+
// fallback is only right for a plain local review — PR mode has no
|
|
850
|
+
// gitContext, and `--local` sets agentCwd to a `pool/pr-<n>` checkout, so
|
|
851
|
+
// deriving from cwd there would file every PR review under `pr-123`.
|
|
852
|
+
//
|
|
853
|
+
// Known limitation, deliberately not chased here: a caller that passes no
|
|
854
|
+
// project AND reviews a moved/renamed working directory buckets under the
|
|
855
|
+
// new directory name, exactly like the rest of the data dir does.
|
|
856
|
+
const feedbackProject = (): string =>
|
|
857
|
+
options.project?.trim()
|
|
858
|
+
? options.project
|
|
859
|
+
: deriveFeedbackProject(options.gitContext?.cwd ?? options.agentCwd ?? process.cwd());
|
|
860
|
+
|
|
861
|
+
const feedbackReviewTarget = (): FeedbackReviewTarget => {
|
|
862
|
+
const target: FeedbackReviewTarget = {
|
|
863
|
+
diffType: String(currentDiffType),
|
|
864
|
+
base: currentBase,
|
|
865
|
+
gitRef: currentGitRef,
|
|
866
|
+
snapshotId: currentSnapshotId(),
|
|
867
|
+
changedFiles: countChangedFiles(currentPatch),
|
|
868
|
+
patchBytes: currentPatch.length,
|
|
869
|
+
};
|
|
870
|
+
if (sessionVcsType) target.vcsType = sessionVcsType;
|
|
871
|
+
else if (workspace) target.vcsType = "workspace";
|
|
872
|
+
const cwd = options.gitContext?.cwd ?? options.agentCwd;
|
|
873
|
+
if (cwd) target.cwd = cwd;
|
|
874
|
+
if (prMeta) {
|
|
875
|
+
target.pr = {
|
|
876
|
+
provider: prMeta.platform,
|
|
877
|
+
repo:
|
|
878
|
+
prMeta.platform === "github"
|
|
879
|
+
? `${prMeta.owner}/${prMeta.repo}`
|
|
880
|
+
: prMeta.projectPath,
|
|
881
|
+
number: prMeta.platform === "github" ? prMeta.number : prMeta.iid,
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
return target;
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Returns whether the draft delete may proceed: false only when a durable
|
|
889
|
+
* write was expected and failed, so the draft stays as the recovery copy.
|
|
890
|
+
*/
|
|
891
|
+
const archiveReviewSubmission = (
|
|
892
|
+
feedback: unknown,
|
|
893
|
+
annotations: unknown,
|
|
894
|
+
decision: FeedbackDecision,
|
|
895
|
+
): boolean => {
|
|
896
|
+
if (!resolveFeedbackHistory(loadConfig())) return true;
|
|
897
|
+
const feedbackText = typeof feedback === "string" ? feedback : "";
|
|
898
|
+
const annotationList = Array.isArray(annotations) ? annotations : [];
|
|
899
|
+
const hasContent = feedbackText.trim().length > 0 || annotationList.length > 0;
|
|
900
|
+
const written = appendFeedbackRecord({
|
|
901
|
+
project: feedbackProject(),
|
|
902
|
+
origin: options.origin ?? "pi",
|
|
903
|
+
surface: "review",
|
|
904
|
+
decision,
|
|
905
|
+
target: { review: feedbackReviewTarget() },
|
|
906
|
+
feedback: feedbackText,
|
|
907
|
+
annotations: annotationList,
|
|
908
|
+
});
|
|
909
|
+
return written !== null || !hasContent;
|
|
910
|
+
};
|
|
911
|
+
|
|
831
912
|
function buildCurrentAiReviewContext(
|
|
832
913
|
patch: string = currentPatch,
|
|
833
914
|
base: string = currentBase,
|
|
@@ -3269,17 +3350,35 @@ export async function startReviewServer(options: {
|
|
|
3269
3350
|
handleApiNotFound(res, url.pathname);
|
|
3270
3351
|
return;
|
|
3271
3352
|
} else if (url.pathname === "/api/exit" && req.method === "POST") {
|
|
3353
|
+
// Decision-only line: dismissal rate is behavior data, and a
|
|
3354
|
+
// contentless failure must not change the legacy draft behavior.
|
|
3355
|
+
archiveReviewSubmission("", [], "dismissed");
|
|
3272
3356
|
deleteDraft(draftKey, readDraftGenerationFromUrl(req));
|
|
3273
3357
|
resolveDecision({ approved: false, feedback: '', annotations: [], exit: true });
|
|
3274
3358
|
json(res, { ok: true });
|
|
3275
3359
|
} else if (url.pathname === "/api/feedback" && req.method === "POST") {
|
|
3276
3360
|
try {
|
|
3277
3361
|
const body = await parseBody(req);
|
|
3278
|
-
|
|
3362
|
+
// Archive BEFORE the draft delete: a failed write keeps the
|
|
3363
|
+
// draft as the reviewer's recovery copy (#678 ordering).
|
|
3364
|
+
// Defensive on the body's own types: a malformed value must
|
|
3365
|
+
// degrade to the legacy behavior (settle + 200), never throw.
|
|
3366
|
+
const approved = (body.approved as boolean) ?? false;
|
|
3367
|
+
const feedbackText = (body.feedback as string) || "";
|
|
3368
|
+
const annotationList = (body.annotations as unknown[]) || [];
|
|
3369
|
+
const hasContent =
|
|
3370
|
+
(typeof feedbackText === "string" && feedbackText.trim().length > 0) ||
|
|
3371
|
+
(Array.isArray(annotationList) && annotationList.length > 0);
|
|
3372
|
+
const durable = archiveReviewSubmission(
|
|
3373
|
+
feedbackText,
|
|
3374
|
+
annotationList,
|
|
3375
|
+
approved ? (hasContent ? "approved-with-notes" : "lgtm") : "feedback",
|
|
3376
|
+
);
|
|
3377
|
+
if (durable) deleteDraft(draftKey, readDraftGenerationFromBody(body));
|
|
3279
3378
|
resolveDecision({
|
|
3280
|
-
approved
|
|
3281
|
-
feedback:
|
|
3282
|
-
annotations:
|
|
3379
|
+
approved,
|
|
3380
|
+
feedback: feedbackText,
|
|
3381
|
+
annotations: annotationList,
|
|
3283
3382
|
agentSwitch: body.agentSwitch as string | undefined,
|
|
3284
3383
|
});
|
|
3285
3384
|
json(res, { ok: true });
|