@imenam/mcp-feature-analyzer 1.0.16 → 1.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +357 -351
- package/dist/shared/labels.d.ts +2 -0
- package/dist/shared/labels.js +2 -0
- package/dist/shared/review-state.d.ts +32 -2
- package/dist/shared/review-state.js +74 -3
- package/dist/shared/schemas/analysis.schema.d.ts +100 -0
- package/dist/shared/schemas/analysis.schema.js +24 -0
- package/dist/shared/schemas/ipc.schema.d.ts +17 -2
- package/dist/shared/schemas/ipc.schema.js +2 -2
- package/dist/src/core/analysis-store.d.ts +0 -2
- package/dist/src/core/analysis-store.js +1 -5
- package/dist/src/core/invariants.js +3 -0
- package/dist/src/core/migrate.d.ts +5 -0
- package/dist/src/core/migrate.js +14 -1
- package/dist/src/core/review-actions.d.ts +9 -10
- package/dist/src/core/review-actions.js +22 -29
- package/dist/src/gui-worker.js +48 -46
- package/dist/src/index.js +36 -13
- package/dist/src/tools/format.d.ts +4 -1
- package/dist/src/tools/format.js +6 -2
- package/dist/src/tools/get-review-feedback.d.ts +5 -1
- package/dist/src/tools/get-review-feedback.js +59 -19
- package/dist/src/tools/list-analyses.d.ts +7 -1
- package/dist/src/tools/list-analyses.js +39 -14
- package/dist/src/tools/mark-feedback-processed.d.ts +8 -0
- package/dist/src/tools/mark-feedback-processed.js +34 -0
- package/gui/dist/assets/{angular-html-D9jx7QhQ.js → angular-html-khSGtcDV.js} +1 -1
- package/gui/dist/assets/{angular-ts-lRa7XExS.js → angular-ts-CQZskKbG.js} +1 -1
- package/gui/dist/assets/{apl-BGTQM7xQ.js → apl-Dk2A1KW8.js} +1 -1
- package/gui/dist/assets/{astro-Bwmn-sCs.js → astro-CDNLxpIR.js} +1 -1
- package/gui/dist/assets/{blade-hTi6eO8T.js → blade-HVRxvZlI.js} +1 -1
- package/gui/dist/assets/{c-DFDjWpAg.js → c-BAogwQrx.js} +1 -1
- package/gui/dist/assets/{chapel-CWFio9Xh.js → chapel-CXJ1GKiJ.js} +1 -1
- package/gui/dist/assets/{cobol-BHU0Z5KW.js → cobol-CUpuiRry.js} +1 -1
- package/gui/dist/assets/{coffee-CKPZNC4M.js → coffee-Bvuk4CPF.js} +1 -1
- package/gui/dist/assets/{cpp-CxXoHMA_.js → cpp-DrhGCibo.js} +1 -1
- package/gui/dist/assets/{crystal-CNza3l-0.js → crystal-CoGIF_h4.js} +1 -1
- package/gui/dist/assets/{css-8gvvitn7.js → css-CUXMpymc.js} +1 -1
- package/gui/dist/assets/{edge-C1DFjpLE.js → edge-MTcLzslY.js} +1 -1
- package/gui/dist/assets/{elixir--TQLjEik.js → elixir-DpYpNuu6.js} +1 -1
- package/gui/dist/assets/{elm-DrYZoelB.js → elm-CAdLQ8j2.js} +1 -1
- package/gui/dist/assets/{erb-OBChVdU7.js → erb-wGT1uBBo.js} +1 -1
- package/gui/dist/assets/{git-rebase-CAzekMLe.js → git-rebase-D7SHvGwZ.js} +1 -1
- package/gui/dist/assets/{glimmer-js-BS_md-6w.js → glimmer-js-DEF7JSd6.js} +1 -1
- package/gui/dist/assets/{glimmer-ts-B_QJmXQ4.js → glimmer-ts-CM94Mbx8.js} +1 -1
- package/gui/dist/assets/{glsl-DjgOn4nr.js → glsl-DvB1v13j.js} +1 -1
- package/gui/dist/assets/{graphql-BFiJ0m0c.js → graphql-CpBeqKe2.js} +1 -1
- package/gui/dist/assets/{hack-CxIP_X0a.js → hack-CBlJ1Su7.js} +1 -1
- package/gui/dist/assets/{haml-CKxmidr0.js → haml-Db0uDFny.js} +1 -1
- package/gui/dist/assets/{handlebars-WxLsOfA6.js → handlebars-B9jQcMgj.js} +1 -1
- package/gui/dist/assets/{html-C_OsZKHB.js → html-DTSojrQm.js} +1 -1
- package/gui/dist/assets/{html-derivative-BizsWMHo.js → html-derivative-7jGgl4AX.js} +1 -1
- package/gui/dist/assets/{http-DSve4M-x.js → http-DJbcEzJc.js} +1 -1
- package/gui/dist/assets/{hurl-DjwMLP-9.js → hurl-BjBdeErH.js} +1 -1
- package/gui/dist/assets/index-Bj1iisUG.css +1 -0
- package/gui/dist/assets/index-CR3WhYFz.js +87 -0
- package/gui/dist/assets/{java-DEsG-sIR.js → java-Dn64Xe0Z.js} +1 -1
- package/gui/dist/assets/{javascript-B0LxjmDy.js → javascript-BbkSu_9c.js} +1 -1
- package/gui/dist/assets/{jinja-AHNYNU8x.js → jinja-DqC5t5sF.js} +1 -1
- package/gui/dist/assets/{jison-DPKgPW2s.js → jison-DpuZbYSp.js} +1 -1
- package/gui/dist/assets/{json-CfSzWViX.js → json-B2a7n5JV.js} +1 -1
- package/gui/dist/assets/{jsx-DsRmZ9io.js → jsx-ClgcRv-g.js} +1 -1
- package/gui/dist/assets/{julia-CJnq2HdG.js → julia-Do1-bHs6.js} +1 -1
- package/gui/dist/assets/{just-CyEs4EQs.js → just-DHt2GVD3.js} +1 -1
- package/gui/dist/assets/{latex-Dyl9mIMm.js → latex-B_f2UeRI.js} +1 -1
- package/gui/dist/assets/{liquid-B_K8Ey2K.js → liquid-Ds2eU_MP.js} +1 -1
- package/gui/dist/assets/{lua-D3hp8QG9.js → lua-S7tZuNGt.js} +1 -1
- package/gui/dist/assets/{marko-D5WWZxgb.js → marko-udM4lCVd.js} +1 -1
- package/gui/dist/assets/{mdc-Bsafi4nS.js → mdc-Cp5CPiQh.js} +1 -1
- package/gui/dist/assets/{nginx-CFjr0ay9.js → nginx-fGikazrY.js} +1 -1
- package/gui/dist/assets/{nim-OEXFPi0c.js → nim-ByS27m8u.js} +1 -1
- package/gui/dist/assets/{org-_N4GCNDw.js → org-CWef6EjL.js} +1 -1
- package/gui/dist/assets/{perl-CaHGtAaL.js → perl-B3b8PFq6.js} +1 -1
- package/gui/dist/assets/{php-DWg8LThs.js → php-BgIqtqOt.js} +1 -1
- package/gui/dist/assets/{pug-DPXgiXjr.js → pug-BRImkfAG.js} +1 -1
- package/gui/dist/assets/{qml-CkRfU2Je.js → qml-CzXZbZNQ.js} +1 -1
- package/gui/dist/assets/{r-CIwhurN9.js → r-Nteo2x9j.js} +1 -1
- package/gui/dist/assets/{razor-BCzbbAAh.js → razor-D_Afw34M.js} +1 -1
- package/gui/dist/assets/{regexp-k8FwpwYm.js → regexp-CKIOMRkq.js} +1 -1
- package/gui/dist/assets/{rst-OK_2yiPK.js → rst-BKqSLPk8.js} +1 -1
- package/gui/dist/assets/{ruby-Ck3XpZV5.js → ruby-e08qQ71e.js} +1 -1
- package/gui/dist/assets/{sas-G8VC4-0V.js → sas-CBP-Z7IW.js} +1 -1
- package/gui/dist/assets/{scss-BEUUAqNf.js → scss-D_FoTbGY.js} +1 -1
- package/gui/dist/assets/{shellscript-Bx-Qg8XB.js → shellscript-Ck8Pcu_n.js} +1 -1
- package/gui/dist/assets/{shellsession-C6Mfw_X6.js → shellsession-ClnG1wCa.js} +1 -1
- package/gui/dist/assets/{soy-Z0t7nhWy.js → soy-SRwHcO2G.js} +1 -1
- package/gui/dist/assets/{sql-B36wN64p.js → sql-CjUCW4z6.js} +1 -1
- package/gui/dist/assets/{stata-BZjTb6SA.js → stata-DbpO8t8N.js} +1 -1
- package/gui/dist/assets/{surrealql-B3qccGfS.js → surrealql-BvdjWgRW.js} +1 -1
- package/gui/dist/assets/{svelte-CK43mxUL.js → svelte-Bce_bVba.js} +1 -1
- package/gui/dist/assets/{templ-Dt8h0yO2.js → templ-D1Wmfx4s.js} +1 -1
- package/gui/dist/assets/{tex-CtGYb-zm.js → tex-DAUoYbei.js} +1 -1
- package/gui/dist/assets/{ts-tags-BuuXR8Ma.js → ts-tags-DeHdW_lm.js} +1 -1
- package/gui/dist/assets/{tsx-DyD3fsUw.js → tsx-BtUXi9Pr.js} +1 -1
- package/gui/dist/assets/{twig-DUJZSmAn.js → twig-NY4Ref0L.js} +1 -1
- package/gui/dist/assets/{typescript-Ca5kRpOq.js → typescript-BRVtVFtH.js} +1 -1
- package/gui/dist/assets/{typst-Drnd8Va8.js → typst-CEx4fnEq.js} +1 -1
- package/gui/dist/assets/{vue-Ch4nWmi-.js → vue-CzSBuAS4.js} +1 -1
- package/gui/dist/assets/{vue-html-B_1wM4n0.js → vue-html-3GkraVg2.js} +1 -1
- package/gui/dist/assets/{vue-vine-uWK-VD9C.js → vue-vine-un5mayXW.js} +1 -1
- package/gui/dist/assets/{xml-oeyoZtW9.js → xml-DYFbmBqP.js} +1 -1
- package/gui/dist/assets/{xsl-4I4JS35R.js → xsl-CVaXn7rF.js} +1 -1
- package/gui/dist/assets/{yaml-C8gb-d6g.js → yaml-CCwQ0s68.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/gui/dist/assets/index-8qLYikfa.css +0 -1
- package/gui/dist/assets/index-BfqnaF_D.js +0 -87
package/dist/src/gui-worker.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { serve } from "@hono/node-server";
|
|
2
2
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
3
3
|
import { Hono } from "hono";
|
|
4
|
-
import { streamSSE } from "hono/streaming";
|
|
5
4
|
import { GUI_LISTEN_HOST, ProxyClient, createLogger, honoGuiGuard, setupLogging } from "@imenam/mcp-gui-interface";
|
|
6
5
|
import path from "path";
|
|
7
6
|
import fs from "fs";
|
|
8
7
|
import { fileURLToPath } from "url";
|
|
8
|
+
import { randomUUID } from "crypto";
|
|
9
9
|
import { IPCMessageSchema, PUSHED_EVENT_TYPES } from "../shared/schemas/ipc.schema.js";
|
|
10
10
|
import { AddNoteBodySchema, DeleteNoteBodySchema, SetFileReviewedBodySchema, SetFindingStatusBodySchema, SubmitReviewBodySchema, UpdateNoteBodySchema, } from "../shared/schemas/api.schema.js";
|
|
11
11
|
import { resolveLogDir } from "./core/log-dir.js";
|
|
@@ -79,7 +79,6 @@ const app = new Hono();
|
|
|
79
79
|
// Contrôle de l'hôte appelé et des requêtes de modification, avant toute route.
|
|
80
80
|
app.use("*", honoGuiGuard({ proxyUrl: guiMode.kind === "proxy" ? guiMode.proxyUrl : undefined }));
|
|
81
81
|
let proxyClient = null;
|
|
82
|
-
const sseStreams = new Set();
|
|
83
82
|
const pendingRequests = new Map();
|
|
84
83
|
function safeSend(message) {
|
|
85
84
|
if (process.send && process.connected) {
|
|
@@ -116,6 +115,38 @@ function ipcRequest(type, data, timeout = 5000) {
|
|
|
116
115
|
});
|
|
117
116
|
}
|
|
118
117
|
const PUSHED_EVENTS = new Set(PUSHED_EVENT_TYPES);
|
|
118
|
+
/** Nombre d'évènements conservés dans le journal servi par `/api/changes`. */
|
|
119
|
+
const CHANGES_JOURNAL_SIZE = 200;
|
|
120
|
+
/** Identifiant de cette instance du worker : un client qui en voit changer recharge tout. */
|
|
121
|
+
const epoch = randomUUID();
|
|
122
|
+
/** Journal des évènements poussés par le master, numérotés par `seq` croissant. */
|
|
123
|
+
const changesJournal = [];
|
|
124
|
+
let lastSeq = 0;
|
|
125
|
+
function recordChange(event) {
|
|
126
|
+
lastSeq += 1;
|
|
127
|
+
changesJournal.push({ seq: lastSeq, event });
|
|
128
|
+
if (changesJournal.length > CHANGES_JOURNAL_SIZE)
|
|
129
|
+
changesJournal.shift();
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Évènements postérieurs à `since`, ou `reset` quand le client ne peut pas
|
|
133
|
+
* reprendre : autre instance du worker, évènements sortis du journal, ou
|
|
134
|
+
* `since` au-delà du dernier évènement.
|
|
135
|
+
*/
|
|
136
|
+
function changesSince(since, clientEpoch) {
|
|
137
|
+
if (since === undefined || clientEpoch !== epoch || since > lastSeq) {
|
|
138
|
+
return { epoch, seq: lastSeq, reset: true };
|
|
139
|
+
}
|
|
140
|
+
const oldestKept = changesJournal.length > 0 ? changesJournal[0].seq : lastSeq + 1;
|
|
141
|
+
if (since < oldestKept - 1)
|
|
142
|
+
return { epoch, seq: lastSeq, reset: true };
|
|
143
|
+
return {
|
|
144
|
+
epoch,
|
|
145
|
+
seq: lastSeq,
|
|
146
|
+
reset: false,
|
|
147
|
+
events: changesJournal.filter((entry) => entry.seq > since).map((entry) => entry.event),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
119
150
|
function setupIpcHandlers() {
|
|
120
151
|
process.on("error", (err) => {
|
|
121
152
|
if (err.code === "ERR_IPC_CHANNEL_CLOSED")
|
|
@@ -123,7 +154,7 @@ function setupIpcHandlers() {
|
|
|
123
154
|
logger.error(`Process error: ${err.message}`);
|
|
124
155
|
});
|
|
125
156
|
process.on("disconnect", () => {
|
|
126
|
-
logger.warn("IPC disconnected from MCP master. GUI worker exits
|
|
157
|
+
logger.warn("IPC disconnected from MCP master. GUI worker exits.");
|
|
127
158
|
process.exit(0);
|
|
128
159
|
});
|
|
129
160
|
process.on("message", (raw) => {
|
|
@@ -140,12 +171,7 @@ function setupIpcHandlers() {
|
|
|
140
171
|
return;
|
|
141
172
|
}
|
|
142
173
|
if (PUSHED_EVENTS.has(msg.type)) {
|
|
143
|
-
|
|
144
|
-
stream.writeSSE({ data: JSON.stringify(msg), event: "message" }).catch((err) => {
|
|
145
|
-
logger.warn(`SSE push ${msg.type} failed, stream dropped: ${err?.message ?? err}`);
|
|
146
|
-
sseStreams.delete(stream);
|
|
147
|
-
});
|
|
148
|
-
}
|
|
174
|
+
recordChange(msg);
|
|
149
175
|
return;
|
|
150
176
|
}
|
|
151
177
|
logger.debug(`Unhandled IPC message type: ${msg.type}`);
|
|
@@ -163,45 +189,21 @@ app.get("/proxy/health", (c) => {
|
|
|
163
189
|
timestamp: new Date().toISOString(),
|
|
164
190
|
});
|
|
165
191
|
});
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
try {
|
|
178
|
-
await stream.writeSSE({ data: "connected", event: "ping" });
|
|
179
|
-
}
|
|
180
|
-
catch (err) {
|
|
181
|
-
logger.warn(`SSE /api/events: initial writeSSE failed (id=${clientId}): ${err?.message ?? err}`);
|
|
182
|
-
sseStreams.delete(stream);
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
while (!stream.aborted) {
|
|
186
|
-
await stream.sleep(30000);
|
|
187
|
-
try {
|
|
188
|
-
await stream.writeSSE({ data: "ping", event: "ping" });
|
|
189
|
-
}
|
|
190
|
-
catch (err) {
|
|
191
|
-
logger.warn(`SSE /api/events: keepalive ping failed (id=${clientId}): ${err?.message ?? err}`);
|
|
192
|
-
sseStreams.delete(stream);
|
|
193
|
-
break;
|
|
192
|
+
/**
|
|
193
|
+
* Changements depuis `since` pour l'instance `epoch`. Répond immédiatement ;
|
|
194
|
+
* sans `since` ni `epoch`, la réponse est un `reset`.
|
|
195
|
+
*/
|
|
196
|
+
app.get("/api/changes", (c) => {
|
|
197
|
+
const rawSince = c.req.query("since");
|
|
198
|
+
const rawEpoch = c.req.query("epoch");
|
|
199
|
+
let since;
|
|
200
|
+
if (rawSince !== undefined && rawSince !== "") {
|
|
201
|
+
if (!/^\d+$/.test(rawSince) || !Number.isSafeInteger(Number(rawSince))) {
|
|
202
|
+
return c.json({ error: `Invalid "since" query parameter: ${JSON.stringify(rawSince)} — expected a non-negative integer.` }, 400);
|
|
194
203
|
}
|
|
204
|
+
since = Number(rawSince);
|
|
195
205
|
}
|
|
196
|
-
|
|
197
|
-
app.post("/api/client-diagnostics", async (c) => {
|
|
198
|
-
try {
|
|
199
|
-
logger.warn(`Client diagnostics (SSE/EventSource): ${JSON.stringify(await c.req.json())}`);
|
|
200
|
-
}
|
|
201
|
-
catch (e) {
|
|
202
|
-
logger.warn(`Client diagnostics: invalid JSON body (${e?.message ?? e})`);
|
|
203
|
-
}
|
|
204
|
-
return c.json({ ok: true });
|
|
206
|
+
return c.json(changesSince(since, rawEpoch || undefined));
|
|
205
207
|
});
|
|
206
208
|
/** Code HTTP d'une erreur renvoyée par le master ; une erreur locale (timeout, IPC absent) est une erreur serveur. */
|
|
207
209
|
const HTTP_STATUS_BY_ERROR_KIND = {
|
package/dist/src/index.js
CHANGED
|
@@ -22,7 +22,7 @@ import { AddNoteRequestSchema, AnalysisRequestSchema, DeleteNoteRequestSchema, S
|
|
|
22
22
|
import { ANALYSIS_MODES, DIAGRAM_KINDS, EXPLANATION_SIDES, FINDING_KINDS, NODE_SHAPES, NODE_STATUSES, SEVERITIES, } from "../shared/schemas/analysis.schema.js";
|
|
23
23
|
import { errorResult, textResult } from "./tools/types.js";
|
|
24
24
|
import { createAnalysis } from "./tools/create-analysis.js";
|
|
25
|
-
import { listAnalyses } from "./tools/list-analyses.js";
|
|
25
|
+
import { listAnalyses, REVIEW_STATE_FILTERS } from "./tools/list-analyses.js";
|
|
26
26
|
import { getAnalysis } from "./tools/get-analysis.js";
|
|
27
27
|
import { getDiff } from "./tools/get-diff.js";
|
|
28
28
|
import { refreshAnalysis } from "./tools/refresh-analysis.js";
|
|
@@ -36,6 +36,7 @@ import { deleteExplanations } from "./tools/delete-explanations.js";
|
|
|
36
36
|
import { DEFAULT_NODE_SHAPE, DEFAULT_NODE_STATUS, setDiagram } from "./tools/set-diagram.js";
|
|
37
37
|
import { deleteDiagram } from "./tools/delete-diagram.js";
|
|
38
38
|
import { getReviewFeedback } from "./tools/get-review-feedback.js";
|
|
39
|
+
import { markFeedbackProcessed } from "./tools/mark-feedback-processed.js";
|
|
39
40
|
import { deleteAnalysis } from "./tools/delete-analysis.js";
|
|
40
41
|
const __filename = fileURLToPath(import.meta.url);
|
|
41
42
|
const __dirname = path.dirname(__filename);
|
|
@@ -103,8 +104,8 @@ Workflow:
|
|
|
103
104
|
5. add_findings for every problem, including requirement_gap findings for requested behaviour that is missing.
|
|
104
105
|
6. set_diagram when a picture helps (impacted layers, process flow, concept tree).
|
|
105
106
|
7. Tell the user to review the analysis in the GUI (the link is in the tool results).
|
|
106
|
-
8. get_review_feedback to read the decision, the selected points and the prompt.
|
|
107
|
-
9. Fix the code, then refresh_analysis to recompute the diff for a new review round; re-anchor or rewrite the outdated findings and explanations.
|
|
107
|
+
8. get_review_feedback to read the decision, the selected points and the prompt. list_analyses with review_state "to_process" lists the submitted reviews you have not handled yet.
|
|
108
|
+
9. Fix the code, then refresh_analysis to recompute the diff for a new review round; re-anchor or rewrite the outdated findings and explanations. When the diff must stay as it is (an analysis of one commit, fixes in other commits, answers given in the conversation), call mark_feedback_processed instead, with a note saying how the feedback was handled.
|
|
108
109
|
|
|
109
110
|
Explanation rules:
|
|
110
111
|
- Write one for every added or removed block that is long (a function or a block of about 30 lines or more) or complex (non-trivial algorithm, state machine, concurrency, recursion, tricky regular expression or data transformation). A 100-line function always gets one.
|
|
@@ -245,13 +246,6 @@ function handleIPCMessage(raw) {
|
|
|
245
246
|
case "ALREADY_RUNNING":
|
|
246
247
|
logger.info(`GUI already served by another instance at ${msg.data?.url}`);
|
|
247
248
|
return;
|
|
248
|
-
case "GET_INITIAL_STATE":
|
|
249
|
-
broadcastToGUI({
|
|
250
|
-
type: "INITIAL_STATE",
|
|
251
|
-
data: { version: APP_VERSION, analyses: store.list().analyses },
|
|
252
|
-
timestamp: new Date().toISOString(),
|
|
253
|
-
});
|
|
254
|
-
return;
|
|
255
249
|
default:
|
|
256
250
|
if (GUI_REQUESTS.has(msg.type)) {
|
|
257
251
|
try {
|
|
@@ -358,13 +352,20 @@ const TOOLS = [
|
|
|
358
352
|
{
|
|
359
353
|
name: "list_analyses",
|
|
360
354
|
description: "List the analyses grouped by project, with their refs, open findings by severity, reviewed files, diagram count and review state " +
|
|
361
|
-
"(not started, in progress, all files reviewed, submitted
|
|
355
|
+
"(not started, in progress, all files reviewed, submitted; a submitted review is pending processing until mark_feedback_processed or refresh_analysis). " +
|
|
356
|
+
"Start here: when the project already has an analysis of the feature, " +
|
|
362
357
|
"continue it (update_analysis to rewrite the overview and summary, refresh_analysis after changing the code) instead of creating a duplicate; " +
|
|
363
358
|
"create a new one with create_analysis for another feature or another diff.",
|
|
364
359
|
inputSchema: {
|
|
365
360
|
type: "object",
|
|
366
361
|
properties: {
|
|
367
362
|
project: { type: "string", description: "Only list the analyses of this project (exact name). An unknown name lists the existing projects." },
|
|
363
|
+
review_state: {
|
|
364
|
+
type: "string",
|
|
365
|
+
enum: [...REVIEW_STATE_FILTERS],
|
|
366
|
+
description: "Only list the analyses in this review state. to_process = submitted reviews not marked processed yet: the feedback you still have to handle; " +
|
|
367
|
+
"processed = submitted reviews marked processed.",
|
|
368
|
+
},
|
|
368
369
|
},
|
|
369
370
|
},
|
|
370
371
|
},
|
|
@@ -659,14 +660,34 @@ const TOOLS = [
|
|
|
659
660
|
},
|
|
660
661
|
{
|
|
661
662
|
name: "get_review_feedback",
|
|
662
|
-
description: "Read the reviewer's feedback once the review is submitted: decision, selected findings and notes, and the prompt to act on. " +
|
|
663
|
-
"
|
|
663
|
+
description: "Read the reviewer's feedback once the review is submitted: decision, whether you already processed it, selected findings and notes, and the prompt to act on. " +
|
|
664
|
+
"After a resubmission it also lists what changed since the previous submitted version and recalls how that one was processed. " +
|
|
665
|
+
"Before submission, or while the reviewer is changing a submitted review, it reports the review progress only. " +
|
|
666
|
+
"Read-only. Always read the feedback before calling refresh_analysis, which discards it.",
|
|
664
667
|
inputSchema: {
|
|
665
668
|
type: "object",
|
|
666
669
|
properties: { analysis_id: { type: "string", description: ANALYSIS_ID_DOC } },
|
|
667
670
|
required: ["analysis_id"],
|
|
668
671
|
},
|
|
669
672
|
},
|
|
673
|
+
{
|
|
674
|
+
name: "mark_feedback_processed",
|
|
675
|
+
description: "Mark the submitted feedback of an analysis as processed once you handled it, when refresh_analysis does not fit: the analysis freezes one commit " +
|
|
676
|
+
"and the fixes are in other commits, or the notes were answered in the conversation. Records the date and an optional note; the feedback stays readable " +
|
|
677
|
+
"with get_review_feedback and the GUI shows the review as processed. Refused while the review is not submitted. " +
|
|
678
|
+
"If the reviewer changes the review afterwards, it becomes pending again until it is resubmitted.",
|
|
679
|
+
inputSchema: {
|
|
680
|
+
type: "object",
|
|
681
|
+
properties: {
|
|
682
|
+
analysis_id: { type: "string", description: ANALYSIS_ID_DOC },
|
|
683
|
+
note: {
|
|
684
|
+
type: "string",
|
|
685
|
+
description: 'How the feedback was handled, shown to the reviewer, e.g. "fixed by 12785d6e3 and 1ff5baaad" or "answers given in the conversation".',
|
|
686
|
+
},
|
|
687
|
+
},
|
|
688
|
+
required: ["analysis_id"],
|
|
689
|
+
},
|
|
690
|
+
},
|
|
670
691
|
{
|
|
671
692
|
name: "refresh_analysis",
|
|
672
693
|
description: "Recompute the frozen diff with the same repository, mode and refs, after fixing the code. " +
|
|
@@ -743,6 +764,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
743
764
|
return afterMutation(setDiagram(store, params));
|
|
744
765
|
case "delete_diagram":
|
|
745
766
|
return afterMutation(deleteDiagram(store, params));
|
|
767
|
+
case "mark_feedback_processed":
|
|
768
|
+
return afterMutation(markFeedbackProcessed(store, params));
|
|
746
769
|
case "refresh_analysis":
|
|
747
770
|
return await withRepoAccess(exec, async (repoFor) => afterMutation(await refreshAnalysis(store, params, repoFor)));
|
|
748
771
|
case "delete_analysis": {
|
|
@@ -23,7 +23,10 @@ export declare function severityCountsLabel(counts: Record<Severity, number>): s
|
|
|
23
23
|
export declare function indentBlock(text: string, indent: string): string;
|
|
24
24
|
/** Nom anglais d'un état de revue : `not started`, `in progress`… */
|
|
25
25
|
export declare function progressStateLabel(state: ReviewProgressState): string;
|
|
26
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* `in progress (2/7 files reviewed)`, `submitted, decision approve, pending processing (7/7 files reviewed)`
|
|
28
|
+
* ou `submitted, decision approve, processed at <date> (7/7 files reviewed)`.
|
|
29
|
+
*/
|
|
27
30
|
export declare function progressLabel(progress: ReviewProgress): string;
|
|
28
31
|
/** Bloc « Overview » : objectif, approche et points d'attention ; une ligne d'invitation quand il n'y en a pas. */
|
|
29
32
|
export declare function overviewLines(overview: Overview | null): string[];
|
package/dist/src/tools/format.js
CHANGED
|
@@ -73,9 +73,13 @@ const REVIEW_PROGRESS_TEXT = {
|
|
|
73
73
|
export function progressStateLabel(state) {
|
|
74
74
|
return REVIEW_PROGRESS_TEXT[state];
|
|
75
75
|
}
|
|
76
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* `in progress (2/7 files reviewed)`, `submitted, decision approve, pending processing (7/7 files reviewed)`
|
|
78
|
+
* ou `submitted, decision approve, processed at <date> (7/7 files reviewed)`.
|
|
79
|
+
*/
|
|
77
80
|
export function progressLabel(progress) {
|
|
78
|
-
const
|
|
81
|
+
const processing = progress.processed === null ? "pending processing" : `processed at ${progress.processed.at}`;
|
|
82
|
+
const decision = progress.state === "submitted" ? `, decision ${progress.decision}, ${processing}` : "";
|
|
79
83
|
return `${REVIEW_PROGRESS_TEXT[progress.state]}${decision} (${progress.reviewedFiles}/${progress.totalFiles} files reviewed)`;
|
|
80
84
|
}
|
|
81
85
|
/** Bloc « Overview » : objectif, approche et points d'attention ; une ligne d'invitation quand il n'y en a pas. */
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { AnalysisStore } from "../core/analysis-store.js";
|
|
2
2
|
import { type Args, type ToolResult } from "./types.js";
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Retour du relecteur : décision, état du traitement, écarts avec la version
|
|
5
|
+
* soumise précédente, points retenus et prompt ; sinon l'avancement de la revue.
|
|
6
|
+
* Lecture seule.
|
|
7
|
+
*/
|
|
4
8
|
export declare function getReviewFeedback(store: AnalysisStore, args: Args): ToolResult;
|
|
@@ -1,7 +1,41 @@
|
|
|
1
|
-
import { reviewProgress } from "../../shared/review-state.js";
|
|
1
|
+
import { hasNoChanges, recordSubmission, reviewProgress, submissionChanges } from "../../shared/review-state.js";
|
|
2
2
|
import { sortBySeverity } from "../../shared/severity.js";
|
|
3
3
|
import { findingLine, indentBlock, noteLocationLabel, overviewLines, progressLabel } from "./format.js";
|
|
4
4
|
import { rejectUnknownFields, requireString, textResult } from "./types.js";
|
|
5
|
+
/** `processed at <date> (note: …)` ou `not processed`. */
|
|
6
|
+
function processingLabel(processed) {
|
|
7
|
+
if (processed === null)
|
|
8
|
+
return "not processed";
|
|
9
|
+
return `processed at ${processed.at}${processed.note === null ? "" : ` (note: ${processed.note})`}`;
|
|
10
|
+
}
|
|
11
|
+
/** `submitted at <date>, decision <d>, processed at <date>…` : rappel d'une version soumise. */
|
|
12
|
+
function submissionLabel(record) {
|
|
13
|
+
return `submitted at ${record.submittedAt}, decision ${record.decision}, ${processingLabel(record.processed)}`;
|
|
14
|
+
}
|
|
15
|
+
function recordedNoteLine(note) {
|
|
16
|
+
return `${note.id} — ${noteLocationLabel(note)}\n${indentBlock(note.text, " ")}`;
|
|
17
|
+
}
|
|
18
|
+
/** Écarts entre la version soumise précédente et la soumission en cours. */
|
|
19
|
+
function changeLines(previous, current) {
|
|
20
|
+
const changes = submissionChanges(previous, current);
|
|
21
|
+
const lines = [`Changes since the previous submission (${submissionLabel(previous)}):`];
|
|
22
|
+
if (hasNoChanges(changes))
|
|
23
|
+
return [...lines, " none: same decision and same points."];
|
|
24
|
+
if (changes.decision)
|
|
25
|
+
lines.push(` Decision changed: ${changes.decision.from} → ${changes.decision.to}`);
|
|
26
|
+
for (const finding of changes.addedFindings)
|
|
27
|
+
lines.push(` + finding ${finding.id} — ${finding.title}`);
|
|
28
|
+
for (const finding of changes.removedFindings)
|
|
29
|
+
lines.push(` - finding ${finding.id} — ${finding.title}`);
|
|
30
|
+
for (const note of changes.addedNotes)
|
|
31
|
+
lines.push(` + note ${recordedNoteLine(note)}`);
|
|
32
|
+
for (const { before, after } of changes.modifiedNotes) {
|
|
33
|
+
lines.push(` ~ note ${recordedNoteLine(after)}`, ` was: ${noteLocationLabel(before)}`, indentBlock(before.text, " "));
|
|
34
|
+
}
|
|
35
|
+
for (const note of changes.removedNotes)
|
|
36
|
+
lines.push(` - note ${recordedNoteLine(note)}`);
|
|
37
|
+
return lines;
|
|
38
|
+
}
|
|
5
39
|
function findingDetails(finding) {
|
|
6
40
|
const lines = [`- ${findingLine(finding)}`];
|
|
7
41
|
if (finding.body.trim() !== "")
|
|
@@ -13,22 +47,27 @@ function findingDetails(finding) {
|
|
|
13
47
|
function noteDetails(note) {
|
|
14
48
|
return `- ${note.id} — ${noteLocationLabel(note)}\n${indentBlock(note.text, " ")}`;
|
|
15
49
|
}
|
|
16
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Avancement d'une revue non soumise. Une revue rouverte par le relecteur rappelle
|
|
52
|
+
* la version soumise qu'elle remplacera, sans en donner le contenu.
|
|
53
|
+
*/
|
|
17
54
|
function progressLines(analysis) {
|
|
18
55
|
const progress = reviewProgress(analysis);
|
|
19
56
|
const ignored = analysis.findings.filter((finding) => finding.status === "ignored").length;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
`
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
...overviewLines(analysis.overview),
|
|
27
|
-
|
|
28
|
-
"Ask the user to finish the review in the GUI and save the feedback, then call get_review_feedback again.",
|
|
29
|
-
];
|
|
57
|
+
const previous = analysis.review.previousSubmission;
|
|
58
|
+
const lines = [`The review of "${analysis.title}" (${analysis.id}, project "${analysis.project}") is not submitted yet.`];
|
|
59
|
+
if (previous) {
|
|
60
|
+
lines.push(`The reviewer reopened the feedback ${submissionLabel(previous)}, and is changing it: do not act on it until it is submitted again.`);
|
|
61
|
+
}
|
|
62
|
+
lines.push(`Review state: ${progressLabel(progress)}.`, `Progress: ${progress.reviewedFiles}/${progress.totalFiles} file(s) reviewed, ${ignored}/${analysis.findings.length} finding(s) ignored, ` +
|
|
63
|
+
`${analysis.notes.length} reviewer note(s).`, "", ...overviewLines(analysis.overview), "", "Ask the user to finish the review in the GUI and save the feedback, then call get_review_feedback again.");
|
|
64
|
+
return lines;
|
|
30
65
|
}
|
|
31
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* Retour du relecteur : décision, état du traitement, écarts avec la version
|
|
68
|
+
* soumise précédente, points retenus et prompt ; sinon l'avancement de la revue.
|
|
69
|
+
* Lecture seule.
|
|
70
|
+
*/
|
|
32
71
|
export function getReviewFeedback(store, args) {
|
|
33
72
|
rejectUnknownFields(args, ["analysis_id"]);
|
|
34
73
|
const analysis = store.get(requireString(args, "analysis_id"));
|
|
@@ -42,11 +81,11 @@ export function getReviewFeedback(store, args) {
|
|
|
42
81
|
const lines = [
|
|
43
82
|
`Review of "${analysis.title}" (${analysis.id}, project "${analysis.project}") submitted at ${review.submittedAt}.`,
|
|
44
83
|
`Decision: ${review.decision}`,
|
|
45
|
-
""
|
|
46
|
-
...overviewLines(analysis.overview),
|
|
47
|
-
"",
|
|
48
|
-
`Selected findings (${findings.length}):`,
|
|
84
|
+
`Processing: ${review.processed === null ? "to process (call mark_feedback_processed once handled)" : processingLabel(review.processed)}`,
|
|
49
85
|
];
|
|
86
|
+
if (review.previousSubmission)
|
|
87
|
+
lines.push("", ...changeLines(review.previousSubmission, recordSubmission(analysis)));
|
|
88
|
+
lines.push("", ...overviewLines(analysis.overview), "", `Selected findings (${findings.length}):`);
|
|
50
89
|
lines.push(...(findings.length === 0 ? [" none"] : findings.flatMap(findingDetails)), "");
|
|
51
90
|
lines.push(`Selected reviewer notes (${notes.length}):`);
|
|
52
91
|
lines.push(...(notes.length === 0 ? [" none"] : notes.map(noteDetails)), "");
|
|
@@ -64,8 +103,9 @@ export function getReviewFeedback(store, args) {
|
|
|
64
103
|
lines.push("Next steps: the reviewer approved the feature with no points to address.");
|
|
65
104
|
}
|
|
66
105
|
else {
|
|
67
|
-
lines.push("Next steps: address the points above in the repository
|
|
68
|
-
"(it discards this feedback and starts a new review round)
|
|
106
|
+
lines.push("Next steps: address the points above in the repository. Then either call refresh_analysis to recompute the diff " +
|
|
107
|
+
"(it discards this feedback and starts a new review round), or, when the diff stays as it is (an analysis of one commit, " +
|
|
108
|
+
"fixes made in other commits, answers given in the conversation), call mark_feedback_processed with a note saying how it was handled.");
|
|
69
109
|
}
|
|
70
110
|
return textResult(lines.join("\n"));
|
|
71
111
|
}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import type { AnalysisStore } from "../core/analysis-store.js";
|
|
2
2
|
import { type Args, type ToolResult } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Filtre `review_state` : un état d'avancement, ou parmi les revues soumises
|
|
5
|
+
* celles qui restent à traiter (`to_process`) ou déjà traitées (`processed`).
|
|
6
|
+
*/
|
|
7
|
+
export declare const REVIEW_STATE_FILTERS: readonly ["not_started", "in_progress", "files_reviewed", "submitted", "to_process", "processed"];
|
|
8
|
+
export type ReviewStateFilter = (typeof REVIEW_STATE_FILTERS)[number];
|
|
3
9
|
/**
|
|
4
10
|
* Liste les analyses regroupées par projet (projet le plus récemment modifié en
|
|
5
11
|
* premier, puis analyses de la plus récente à la plus ancienne), éventuellement
|
|
6
|
-
* limitées à un projet, et les fichiers illisibles.
|
|
12
|
+
* limitées à un projet et à un état de revue, et les fichiers illisibles.
|
|
7
13
|
*/
|
|
8
14
|
export declare function listAnalyses(store: AnalysisStore, args: Args): ToolResult;
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { REVIEW_PROGRESS_STATES } from "../../shared/schemas/analysis.schema.js";
|
|
2
2
|
import { progressLabel, progressStateLabel, refsLabel, severityCountsLabel } from "./format.js";
|
|
3
|
-
import { optionalString, rejectUnknownFields, textResult } from "./types.js";
|
|
3
|
+
import { optionalEnum, optionalString, rejectUnknownFields, textResult } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Filtre `review_state` : un état d'avancement, ou parmi les revues soumises
|
|
6
|
+
* celles qui restent à traiter (`to_process`) ou déjà traitées (`processed`).
|
|
7
|
+
*/
|
|
8
|
+
export const REVIEW_STATE_FILTERS = [...REVIEW_PROGRESS_STATES, "to_process", "processed"];
|
|
9
|
+
function matchesReviewState(summary, filter) {
|
|
10
|
+
const { state, processed } = summary.progress;
|
|
11
|
+
if (filter === "to_process")
|
|
12
|
+
return state === "submitted" && processed === null;
|
|
13
|
+
if (filter === "processed")
|
|
14
|
+
return state === "submitted" && processed !== null;
|
|
15
|
+
return state === filter;
|
|
16
|
+
}
|
|
4
17
|
/** Analyses regroupées par projet, les projets triés par leur analyse la plus récente. */
|
|
5
18
|
function groupByProject(analyses) {
|
|
6
19
|
const groups = new Map();
|
|
@@ -21,41 +34,53 @@ function projectCounts(analyses) {
|
|
|
21
34
|
return `${analyses.length} analysis(es): ${counts.join(", ")}`;
|
|
22
35
|
}
|
|
23
36
|
function analysisLines(summary) {
|
|
24
|
-
const
|
|
25
|
-
|
|
37
|
+
const { state, processed } = summary.progress;
|
|
38
|
+
const feedback = state !== "submitted" ? "" : processed === null ? " — read it with get_review_feedback" : "";
|
|
39
|
+
const lines = [
|
|
26
40
|
`- ${summary.id} — "${summary.title}"`,
|
|
27
41
|
` ${refsLabel(summary)} | files reviewed ${summary.reviewedCount}/${summary.fileCount} | ` +
|
|
28
42
|
`open findings: ${severityCountsLabel(summary.openFindings)} | diagrams: ${summary.diagramCount}`,
|
|
29
43
|
` review: ${progressLabel(summary.progress)}${feedback} | updated ${summary.updatedAt}`,
|
|
30
44
|
];
|
|
45
|
+
if (processed?.note)
|
|
46
|
+
lines.push(` processing note: ${processed.note}`);
|
|
47
|
+
return lines;
|
|
31
48
|
}
|
|
32
49
|
/**
|
|
33
50
|
* Liste les analyses regroupées par projet (projet le plus récemment modifié en
|
|
34
51
|
* premier, puis analyses de la plus récente à la plus ancienne), éventuellement
|
|
35
|
-
* limitées à un projet, et les fichiers illisibles.
|
|
52
|
+
* limitées à un projet et à un état de revue, et les fichiers illisibles.
|
|
36
53
|
*/
|
|
37
54
|
export function listAnalyses(store, args) {
|
|
38
|
-
rejectUnknownFields(args, ["project"]);
|
|
55
|
+
rejectUnknownFields(args, ["project", "review_state"]);
|
|
39
56
|
const project = optionalString(args, "project");
|
|
40
|
-
const
|
|
57
|
+
const reviewState = optionalEnum(args, "review_state", REVIEW_STATE_FILTERS);
|
|
58
|
+
const { analyses: all, unreadable } = store.list();
|
|
59
|
+
const known = new Set(all.map((summary) => summary.project));
|
|
60
|
+
const analyses = reviewState === undefined ? all : all.filter((summary) => matchesReviewState(summary, reviewState));
|
|
41
61
|
const groups = groupByProject(analyses);
|
|
42
62
|
const lines = [];
|
|
43
|
-
if (project !== undefined && !
|
|
44
|
-
const
|
|
45
|
-
throw new Error(
|
|
63
|
+
if (project !== undefined && !known.has(project)) {
|
|
64
|
+
const names = [...groupByProject(all).keys()];
|
|
65
|
+
throw new Error(names.length === 0
|
|
46
66
|
? `Unknown project "${project}": there are no analyses yet. Create one with create_analysis.`
|
|
47
|
-
: `Unknown project "${project}". Existing projects: ${
|
|
67
|
+
: `Unknown project "${project}". Existing projects: ${names.map((name) => `"${name}"`).join(", ")}. ` +
|
|
48
68
|
`Call list_analyses without project to see them all, or create_analysis to start a new project.`);
|
|
49
69
|
}
|
|
50
|
-
|
|
70
|
+
const shown = project === undefined ? [...groups] : groups.has(project) ? [[project, groups.get(project)]] : [];
|
|
71
|
+
const scope = project === undefined ? "" : ` in project "${project}"`;
|
|
72
|
+
const filtered = reviewState === undefined ? "" : ` with review state ${reviewState}`;
|
|
73
|
+
if (all.length === 0) {
|
|
51
74
|
lines.push("No analyses yet. Create one with create_analysis once the feature is developed: it freezes the diff of a branch or of the working tree for human review.");
|
|
52
75
|
}
|
|
76
|
+
else if (shown.length === 0) {
|
|
77
|
+
lines.push(`No analysis${scope}${filtered}. Call list_analyses without review_state to see them all.`);
|
|
78
|
+
}
|
|
53
79
|
else {
|
|
54
|
-
const shown = project === undefined ? [...groups] : [[project, groups.get(project)]];
|
|
55
80
|
const total = shown.reduce((sum, [, list]) => sum + list.length, 0);
|
|
56
81
|
lines.push(project === undefined
|
|
57
|
-
? `${total} analysis(es) in ${groups.size} project(s), most recently updated first:`
|
|
58
|
-
: `${total} analysis(es)
|
|
82
|
+
? `${total} analysis(es)${filtered} in ${groups.size} project(s), most recently updated first:`
|
|
83
|
+
: `${total} analysis(es)${filtered}${scope}, most recently updated first:`);
|
|
59
84
|
for (const [name, list] of shown) {
|
|
60
85
|
lines.push("", `## Project "${name}" (${projectCounts(list)})`);
|
|
61
86
|
for (const summary of list)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AnalysisStore } from "../core/analysis-store.js";
|
|
2
|
+
import { type Args, type MutationResult } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Marque traité le retour soumis d'une analyse, avec la date et une note
|
|
5
|
+
* facultative. Le retour reste lisible avec get_review_feedback. Refusé tant
|
|
6
|
+
* que la revue n'est pas soumise ; un nouveau marquage remplace le précédent.
|
|
7
|
+
*/
|
|
8
|
+
export declare function markFeedbackProcessed(store: AnalysisStore, args: Args): MutationResult;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { AnalysisError } from "../core/errors.js";
|
|
2
|
+
import { optionalString, rejectUnknownFields, requireString, textResult } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Marque traité le retour soumis d'une analyse, avec la date et une note
|
|
5
|
+
* facultative. Le retour reste lisible avec get_review_feedback. Refusé tant
|
|
6
|
+
* que la revue n'est pas soumise ; un nouveau marquage remplace le précédent.
|
|
7
|
+
*/
|
|
8
|
+
export function markFeedbackProcessed(store, args) {
|
|
9
|
+
rejectUnknownFields(args, ["analysis_id", "note"]);
|
|
10
|
+
const id = requireString(args, "analysis_id");
|
|
11
|
+
const note = optionalString(args, "note") ?? null;
|
|
12
|
+
let replaced = null;
|
|
13
|
+
const updated = store.mutate(id, "agent", (draft) => {
|
|
14
|
+
const review = draft.review;
|
|
15
|
+
if (review.state !== "submitted") {
|
|
16
|
+
const reopened = review.previousSubmission;
|
|
17
|
+
throw new AnalysisError(reopened
|
|
18
|
+
? `The review of analysis "${id}" is not submitted: the reviewer reopened the feedback submitted at ${reopened.submittedAt} and is changing it. ` +
|
|
19
|
+
`Wait until it is submitted again, then read it with get_review_feedback.`
|
|
20
|
+
: `The review of analysis "${id}" is not submitted yet: there is no feedback to mark processed.`);
|
|
21
|
+
}
|
|
22
|
+
replaced = review.processed?.at ?? null;
|
|
23
|
+
review.processed = { at: new Date().toISOString(), note };
|
|
24
|
+
});
|
|
25
|
+
const processed = updated.review.processed;
|
|
26
|
+
const lines = [
|
|
27
|
+
`Marked the feedback of "${updated.title}" (${updated.id}), submitted at ${updated.review.submittedAt}, as processed at ${processed.at}.`,
|
|
28
|
+
`Note: ${processed.note ?? "none"}.`,
|
|
29
|
+
];
|
|
30
|
+
if (replaced !== null)
|
|
31
|
+
lines.push(`This replaces the processing mark of ${replaced}.`);
|
|
32
|
+
lines.push("The feedback stays readable with get_review_feedback. If the reviewer changes the review, it becomes pending again.");
|
|
33
|
+
return { result: textResult(lines.join("\n")), analysisId: updated.id };
|
|
34
|
+
}
|