@imenam/mcp-feature-analyzer 1.0.3 → 1.0.5
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 +18 -4
- package/dist/shared/explanation-request.d.ts +9 -0
- package/dist/shared/explanation-request.js +55 -0
- package/dist/shared/schemas/analysis.schema.d.ts +61 -0
- package/dist/shared/schemas/analysis.schema.js +26 -0
- package/dist/shared/schemas/api.schema.d.ts +13 -0
- package/dist/shared/schemas/api.schema.js +8 -0
- package/dist/shared/schemas/ipc.schema.d.ts +1 -1
- package/dist/shared/schemas/ipc.schema.js +1 -0
- package/dist/src/core/analysis-store.js +1 -0
- package/dist/src/core/ids.d.ts +1 -0
- package/dist/src/core/ids.js +1 -0
- package/dist/src/core/invariants.d.ts +3 -1
- package/dist/src/core/invariants.js +27 -1
- package/dist/src/core/locations.d.ts +22 -4
- package/dist/src/core/locations.js +88 -4
- package/dist/src/core/migrate.d.ts +2 -0
- package/dist/src/core/migrate.js +3 -1
- package/dist/src/core/refresh.d.ts +8 -2
- package/dist/src/core/refresh.js +19 -1
- package/dist/src/core/review-actions.d.ts +6 -1
- package/dist/src/core/review-actions.js +20 -0
- package/dist/src/gui-worker.js +7 -1
- package/dist/src/index.js +100 -9
- package/dist/src/tools/add-explanations.d.ts +9 -0
- package/dist/src/tools/add-explanations.js +73 -0
- package/dist/src/tools/create-analysis.js +1 -1
- package/dist/src/tools/delete-explanations.d.ts +4 -0
- package/dist/src/tools/delete-explanations.js +23 -0
- package/dist/src/tools/format.d.ts +3 -1
- package/dist/src/tools/format.js +5 -0
- package/dist/src/tools/get-analysis.js +6 -1
- package/dist/src/tools/get-diff.js +2 -1
- package/dist/src/tools/inputs.d.ts +14 -0
- package/dist/src/tools/inputs.js +20 -2
- package/dist/src/tools/refresh-analysis.js +8 -2
- package/dist/src/tools/update-explanation.d.ts +7 -0
- package/dist/src/tools/update-explanation.js +57 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/gui/dist/assets/index-BKGeP-g-.js +74 -0
- package/gui/dist/assets/index-CtYu4TyO.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/gui/dist/assets/index-Db7Hu3tM.js +0 -72
- package/gui/dist/assets/index-Du5eL78F.css +0 -1
|
@@ -14,9 +14,13 @@ export interface RefreshStats {
|
|
|
14
14
|
findingsOutdated: number;
|
|
15
15
|
/** Constats `outdated` revenus à `open`. */
|
|
16
16
|
findingsRestored: number;
|
|
17
|
+
/** Explications `current` passées à `outdated`. */
|
|
18
|
+
explanationsOutdated: number;
|
|
19
|
+
/** Explications `outdated` revenues à `current`. */
|
|
20
|
+
explanationsRestored: number;
|
|
17
21
|
}
|
|
18
22
|
export interface RefreshResult {
|
|
19
|
-
fields: Pick<Analysis, "baseCommit" | "headCommit" | "snapshotAt" | "files" | "findings">;
|
|
23
|
+
fields: Pick<Analysis, "baseCommit" | "headCommit" | "snapshotAt" | "files" | "findings" | "explanations">;
|
|
20
24
|
stats: RefreshStats;
|
|
21
25
|
}
|
|
22
26
|
/**
|
|
@@ -25,6 +29,8 @@ export interface RefreshResult {
|
|
|
25
29
|
* - un constat `open` dont les lignes ne correspondent plus à `anchorText`, ou dont le
|
|
26
30
|
* fichier a quitté le diff, passe à `outdated` ;
|
|
27
31
|
* - un constat `outdated` dont les lignes correspondent de nouveau revient à `open` ;
|
|
28
|
-
* - un constat `ignored` reste `ignored`, un constat sans emplacement ne change pas
|
|
32
|
+
* - un constat `ignored` reste `ignored`, un constat sans emplacement ne change pas ;
|
|
33
|
+
* - une explication dont les lignes ne correspondent plus à `anchorText` passe à
|
|
34
|
+
* `outdated`, et revient à `current` quand elles correspondent de nouveau.
|
|
29
35
|
*/
|
|
30
36
|
export declare function applyRefresh(analysis: Analysis, oldSnapshot: DiffSnapshot, next: ComputedSnapshot): RefreshResult;
|
package/dist/src/core/refresh.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { splitLines } from "../../shared/text.js";
|
|
2
2
|
import { AnalysisError } from "./errors.js";
|
|
3
|
+
import { explanationText } from "./locations.js";
|
|
3
4
|
function hunksEqual(a, b) {
|
|
4
5
|
if (a.length !== b.length)
|
|
5
6
|
return false;
|
|
@@ -48,7 +49,9 @@ function currentText(finding, diffs) {
|
|
|
48
49
|
* - un constat `open` dont les lignes ne correspondent plus à `anchorText`, ou dont le
|
|
49
50
|
* fichier a quitté le diff, passe à `outdated` ;
|
|
50
51
|
* - un constat `outdated` dont les lignes correspondent de nouveau revient à `open` ;
|
|
51
|
-
* - un constat `ignored` reste `ignored`, un constat sans emplacement ne change pas
|
|
52
|
+
* - un constat `ignored` reste `ignored`, un constat sans emplacement ne change pas ;
|
|
53
|
+
* - une explication dont les lignes ne correspondent plus à `anchorText` passe à
|
|
54
|
+
* `outdated`, et revient à `current` quand elles correspondent de nouveau.
|
|
52
55
|
*/
|
|
53
56
|
export function applyRefresh(analysis, oldSnapshot, next) {
|
|
54
57
|
if (next.repoPath !== analysis.repoPath || next.mode !== analysis.mode || next.base !== analysis.base || next.head !== analysis.head) {
|
|
@@ -65,6 +68,8 @@ export function applyRefresh(analysis, oldSnapshot, next) {
|
|
|
65
68
|
reviewedReset: 0,
|
|
66
69
|
findingsOutdated: 0,
|
|
67
70
|
findingsRestored: 0,
|
|
71
|
+
explanationsOutdated: 0,
|
|
72
|
+
explanationsRestored: 0,
|
|
68
73
|
};
|
|
69
74
|
const files = next.files.map((entry) => {
|
|
70
75
|
const previous = oldEntries.get(entry.path);
|
|
@@ -100,6 +105,18 @@ export function applyRefresh(analysis, oldSnapshot, next) {
|
|
|
100
105
|
}
|
|
101
106
|
return finding;
|
|
102
107
|
});
|
|
108
|
+
const explanations = analysis.explanations.map((explanation) => {
|
|
109
|
+
const matches = explanationText(newDiffs.get(explanation.location.path), explanation.location) === explanation.anchorText;
|
|
110
|
+
if (explanation.status === "current" && !matches) {
|
|
111
|
+
stats.explanationsOutdated++;
|
|
112
|
+
return { ...explanation, status: "outdated" };
|
|
113
|
+
}
|
|
114
|
+
if (explanation.status === "outdated" && matches) {
|
|
115
|
+
stats.explanationsRestored++;
|
|
116
|
+
return { ...explanation, status: "current" };
|
|
117
|
+
}
|
|
118
|
+
return explanation;
|
|
119
|
+
});
|
|
103
120
|
return {
|
|
104
121
|
fields: {
|
|
105
122
|
baseCommit: next.baseCommit,
|
|
@@ -107,6 +124,7 @@ export function applyRefresh(analysis, oldSnapshot, next) {
|
|
|
107
124
|
snapshotAt: next.snapshotAt,
|
|
108
125
|
files,
|
|
109
126
|
findings,
|
|
127
|
+
explanations,
|
|
110
128
|
},
|
|
111
129
|
stats,
|
|
112
130
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Analysis } from "../../shared/schemas/analysis.schema.js";
|
|
2
|
-
import type { AddNoteBody, DeleteNoteBody, SetFileReviewedBody, SetFindingStatusBody, SubmitReviewBody } from "../../shared/schemas/api.schema.js";
|
|
2
|
+
import type { AddNoteBody, DeleteNoteBody, SetFileReviewedBody, SetFindingStatusBody, SubmitReviewBody, UpdateNoteBody } from "../../shared/schemas/api.schema.js";
|
|
3
3
|
import type { AnalysisStore } from "./analysis-store.js";
|
|
4
4
|
/**
|
|
5
5
|
* Modifications faites par le relecteur depuis la GUI. Chacune passe par
|
|
@@ -15,6 +15,11 @@ export declare function setFileReviewed(store: AnalysisStore, analysisId: string
|
|
|
15
15
|
export declare function setFindingStatus(store: AnalysisStore, analysisId: string, findingId: string, body: SetFindingStatusBody): Analysis;
|
|
16
16
|
/** Ajoute une remarque du relecteur sur une ligne du côté « nouveau » ou sur un fichier entier. */
|
|
17
17
|
export declare function addNote(store: AnalysisStore, analysisId: string, body: AddNoteBody): Analysis;
|
|
18
|
+
/**
|
|
19
|
+
* Remplace le texte d'une remarque. Quand elle fait partie d'une revue soumise,
|
|
20
|
+
* le prompt enregistré est recalculé avec le nouveau texte.
|
|
21
|
+
*/
|
|
22
|
+
export declare function updateNote(store: AnalysisStore, analysisId: string, noteId: string, body: UpdateNoteBody): Analysis;
|
|
18
23
|
/**
|
|
19
24
|
* Supprime une remarque. Quand elle fait partie d'une revue soumise, elle est
|
|
20
25
|
* retirée de la sélection et le prompt enregistré est recalculé sans elle.
|
|
@@ -47,6 +47,26 @@ export function addNote(store, analysisId, body) {
|
|
|
47
47
|
});
|
|
48
48
|
}, { baseRevision: body.baseRevision });
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Remplace le texte d'une remarque. Quand elle fait partie d'une revue soumise,
|
|
52
|
+
* le prompt enregistré est recalculé avec le nouveau texte.
|
|
53
|
+
*/
|
|
54
|
+
export function updateNote(store, analysisId, noteId, body) {
|
|
55
|
+
return store.mutate(analysisId, "user", (draft) => {
|
|
56
|
+
const note = draft.notes.find((entry) => entry.id === noteId);
|
|
57
|
+
if (!note)
|
|
58
|
+
throw new NotFoundError(`Note "${noteId}" not found in analysis "${analysisId}".`);
|
|
59
|
+
note.text = body.text;
|
|
60
|
+
const review = draft.review;
|
|
61
|
+
if (review.state === "submitted" && review.selectedNoteIds.includes(noteId)) {
|
|
62
|
+
review.prompt = buildAgentPrompt(draft, {
|
|
63
|
+
findingIds: review.selectedFindingIds,
|
|
64
|
+
noteIds: review.selectedNoteIds,
|
|
65
|
+
decision: review.decision,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}, { baseRevision: body.baseRevision });
|
|
69
|
+
}
|
|
50
70
|
/**
|
|
51
71
|
* Supprime une remarque. Quand elle fait partie d'une revue soumise, elle est
|
|
52
72
|
* retirée de la sélection et le prompt enregistré est recalculé sans elle.
|
package/dist/src/gui-worker.js
CHANGED
|
@@ -7,7 +7,7 @@ import path from "path";
|
|
|
7
7
|
import fs from "fs";
|
|
8
8
|
import { fileURLToPath } from "url";
|
|
9
9
|
import { IPCMessageSchema, PUSHED_EVENT_TYPES } from "../shared/schemas/ipc.schema.js";
|
|
10
|
-
import { AddNoteBodySchema, DeleteNoteBodySchema, SetFileReviewedBodySchema, SetFindingStatusBodySchema, SubmitReviewBodySchema, } from "../shared/schemas/api.schema.js";
|
|
10
|
+
import { AddNoteBodySchema, DeleteNoteBodySchema, SetFileReviewedBodySchema, SetFindingStatusBodySchema, SubmitReviewBodySchema, UpdateNoteBodySchema, } from "../shared/schemas/api.schema.js";
|
|
11
11
|
import { resolveLogDir } from "./core/log-dir.js";
|
|
12
12
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
13
|
function findProjectRoot(startDir) {
|
|
@@ -256,6 +256,12 @@ app.post("/api/analyses/:id/notes", async (c) => {
|
|
|
256
256
|
return read.response;
|
|
257
257
|
return forward(c, "ADD_NOTE", { analysisId: c.req.param("id"), body: read.body });
|
|
258
258
|
});
|
|
259
|
+
app.patch("/api/analyses/:id/notes/:noteId", async (c) => {
|
|
260
|
+
const read = await readBody(c, UpdateNoteBodySchema);
|
|
261
|
+
if ("response" in read)
|
|
262
|
+
return read.response;
|
|
263
|
+
return forward(c, "UPDATE_NOTE", { analysisId: c.req.param("id"), noteId: c.req.param("noteId"), body: read.body });
|
|
264
|
+
});
|
|
259
265
|
app.delete("/api/analyses/:id/notes/:noteId", async (c) => {
|
|
260
266
|
const read = await readBody(c, DeleteNoteBodySchema);
|
|
261
267
|
if ("response" in read)
|
package/dist/src/index.js
CHANGED
|
@@ -16,10 +16,10 @@ import { DEFAULT_HEAD_REF, localRepo } from "./core/git.js";
|
|
|
16
16
|
import { agentRepo } from "./core/agent-repo.js";
|
|
17
17
|
import { EXEC_ARG, handleAgentExec } from "./core/gateway-exec.js";
|
|
18
18
|
import { resolveRole } from "./core/role.js";
|
|
19
|
-
import { addNote, deleteNote, setFileReviewed, setFindingStatus, submitReview } from "./core/review-actions.js";
|
|
19
|
+
import { addNote, deleteNote, setFileReviewed, setFindingStatus, submitReview, updateNote } from "./core/review-actions.js";
|
|
20
20
|
import { GUI_REQUEST_TYPES, IPCMessageSchema } from "../shared/schemas/ipc.schema.js";
|
|
21
|
-
import { AddNoteRequestSchema, AnalysisRequestSchema, DeleteNoteRequestSchema, SetFileReviewedRequestSchema, SetFindingStatusRequestSchema, SubmitReviewRequestSchema, } from "../shared/schemas/api.schema.js";
|
|
22
|
-
import { ANALYSIS_MODES, DIAGRAM_KINDS, FINDING_KINDS, NODE_SHAPES, NODE_STATUSES, SEVERITIES, } from "../shared/schemas/analysis.schema.js";
|
|
21
|
+
import { AddNoteRequestSchema, AnalysisRequestSchema, DeleteNoteRequestSchema, SetFileReviewedRequestSchema, SetFindingStatusRequestSchema, SubmitReviewRequestSchema, UpdateNoteRequestSchema, } from "../shared/schemas/api.schema.js";
|
|
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
25
|
import { listAnalyses } from "./tools/list-analyses.js";
|
|
@@ -30,6 +30,9 @@ import { updateAnalysis } from "./tools/update-analysis.js";
|
|
|
30
30
|
import { addFindings } from "./tools/add-findings.js";
|
|
31
31
|
import { updateFinding } from "./tools/update-finding.js";
|
|
32
32
|
import { deleteFindings } from "./tools/delete-findings.js";
|
|
33
|
+
import { addExplanations } from "./tools/add-explanations.js";
|
|
34
|
+
import { updateExplanation } from "./tools/update-explanation.js";
|
|
35
|
+
import { deleteExplanations } from "./tools/delete-explanations.js";
|
|
33
36
|
import { DEFAULT_NODE_SHAPE, DEFAULT_NODE_STATUS, setDiagram } from "./tools/set-diagram.js";
|
|
34
37
|
import { deleteDiagram } from "./tools/delete-diagram.js";
|
|
35
38
|
import { getReviewFeedback } from "./tools/get-review-feedback.js";
|
|
@@ -88,6 +91,7 @@ Concepts:
|
|
|
88
91
|
- A **project** groups the analyses of one product or feature; the GUI files analyses by project.
|
|
89
92
|
- The **overview** is your global analysis of the feature: its objective (what it lets users do), its approach (architecture choices, main flow) and the attention points the reviewer should check first. The **summary** lists the functional changes of the feature, one bullet per change, not one bullet per file.
|
|
90
93
|
- A **finding** is a point for the reviewer: an issue anchored on changed lines, or a requirement_gap for requested behaviour that is missing (location optional). Severity: ${SEVERITIES.join(", ")}.
|
|
94
|
+
- An **explanation** describes what a block of added or removed code does and how it works, so that the reviewer understands it before judging it. It is not a finding: it judges nothing and asks for nothing. It is anchored on new-side lines (added code) or old-side lines (removed code), and shown in the diff above the lines it describes.
|
|
91
95
|
- A **diagram** is a flow, layers or mindmap picture of the feature. You give nodes and links only; the GUI lays them out.
|
|
92
96
|
- The **review** belongs to the reviewer: files marked reviewed, findings ignored, notes, and a submitted decision with a prompt for you.
|
|
93
97
|
|
|
@@ -95,11 +99,19 @@ Workflow:
|
|
|
95
99
|
1. list_analyses (with project) to find an analysis of the same feature: reuse it with update_analysis and refresh_analysis instead of creating a duplicate. Otherwise create_analysis with its project once the feature is developed.
|
|
96
100
|
2. get_diff to read the frozen diff.
|
|
97
101
|
3. update_analysis with the overview (objective, approach, attention_points) and the summary (the functional changes).
|
|
98
|
-
4.
|
|
99
|
-
5.
|
|
100
|
-
6.
|
|
101
|
-
7.
|
|
102
|
-
8.
|
|
102
|
+
4. add_explanations for the long or complex blocks of added or removed code (see Explanation rules).
|
|
103
|
+
5. add_findings for every problem, including requirement_gap findings for requested behaviour that is missing.
|
|
104
|
+
6. set_diagram when a picture helps (impacted layers, process flow, concept tree).
|
|
105
|
+
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.
|
|
108
|
+
|
|
109
|
+
Explanation rules:
|
|
110
|
+
- 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.
|
|
111
|
+
- Cover the whole block, from its first to its last line. Title: the name of the block and its role, e.g. "parseUnifiedDiff: diff text to hunks".
|
|
112
|
+
- Body: what the block does (inputs, outputs, effects), then how it works, step by step, in the order of the code; name the key variables and the edge cases it handles. For removed code, say what it did and what replaces it, if anything.
|
|
113
|
+
- Describe, never judge: problems go in findings.
|
|
114
|
+
- Skip trivial code (imports, getters, simple wiring): an explanation there only adds noise.
|
|
103
115
|
|
|
104
116
|
Diagram rules:
|
|
105
117
|
- One subject per diagram, 5 to 12 nodes; split a bigger picture into several diagrams.
|
|
@@ -108,7 +120,7 @@ Diagram rules:
|
|
|
108
120
|
- flow for a process with steps and decisions; layers for the impact perimeter across architecture layers; mindmap for a tree of concepts (one root, one parent per node).
|
|
109
121
|
|
|
110
122
|
Rules:
|
|
111
|
-
- Line numbers are
|
|
123
|
+
- Line numbers are NEW-side numbers, as shown by get_diff, except for explanations of removed code, which use OLD-side numbers (side "old").
|
|
112
124
|
- Nothing is ever written to the repository: suggestions are displayed to the reviewer only.
|
|
113
125
|
- The reviewer alone decides which findings are ignored and when the review is submitted; you cannot change either.`;
|
|
114
126
|
const server = new Server({ name: "mcp-feature-analyzer", version: APP_VERSION }, { capabilities: { tools: {} }, instructions: INSTRUCTIONS });
|
|
@@ -192,6 +204,12 @@ function handleGuiRequest(msg) {
|
|
|
192
204
|
afterUserMutation(analysis.id);
|
|
193
205
|
return { analysis };
|
|
194
206
|
}
|
|
207
|
+
case "UPDATE_NOTE": {
|
|
208
|
+
const request = parseRequest(UpdateNoteRequestSchema, msg.data);
|
|
209
|
+
const analysis = updateNote(store, request.analysisId, request.noteId, request.body);
|
|
210
|
+
afterUserMutation(analysis.id);
|
|
211
|
+
return { analysis };
|
|
212
|
+
}
|
|
195
213
|
case "DELETE_NOTE": {
|
|
196
214
|
const request = parseRequest(DeleteNoteRequestSchema, msg.data);
|
|
197
215
|
const analysis = deleteNote(store, request.analysisId, request.noteId, request.body);
|
|
@@ -305,6 +323,18 @@ const LOCATION_PROPERTIES = {
|
|
|
305
323
|
start_line: { type: "integer", minimum: 1, description: `First targeted line. ${LINE_NUMBERS_DOC}` },
|
|
306
324
|
end_line: { type: "integer", minimum: 1, description: "Last targeted line, inclusive (default: start_line)." },
|
|
307
325
|
};
|
|
326
|
+
const EXPLANATION_LOCATION_PROPERTIES = {
|
|
327
|
+
path: LOCATION_PROPERTIES.path,
|
|
328
|
+
side: {
|
|
329
|
+
type: "string",
|
|
330
|
+
enum: [...EXPLANATION_SIDES],
|
|
331
|
+
description: 'Side of the diff the lines belong to (default "new"): "new" for added or kept code, with NEW-side numbers (second column of get_diff); ' +
|
|
332
|
+
'"old" for removed code, with OLD-side numbers (first column of get_diff), all shown in the diff and at least one removed.',
|
|
333
|
+
},
|
|
334
|
+
start_line: { type: "integer", minimum: 1, description: "First described line, on the side given by side." },
|
|
335
|
+
end_line: { type: "integer", minimum: 1, description: "Last described line, inclusive (default: start_line)." },
|
|
336
|
+
};
|
|
337
|
+
const EXPLANATION_BODY_DOC = "What the code does (inputs, outputs, effects), then how it works, step by step in the order of the code. Describe, never judge. Plain text; line breaks are kept.";
|
|
308
338
|
const SEVERITY_DOC = `Severity: ${SEVERITIES.join(", ")} (from most to least serious).`;
|
|
309
339
|
const FINDING_KIND_DOC = `Kind (default "issue"): "issue" is a problem in the changed code and needs a location; ` +
|
|
310
340
|
`"requirement_gap" is requested behaviour that is missing and may have no location.`;
|
|
@@ -503,6 +533,61 @@ const TOOLS = [
|
|
|
503
533
|
required: ["analysis_id", "finding_ids"],
|
|
504
534
|
},
|
|
505
535
|
},
|
|
536
|
+
{
|
|
537
|
+
name: "add_explanations",
|
|
538
|
+
description: "Explain blocks of added or removed code to the reviewer, as a batch: what each block does and how it works. " +
|
|
539
|
+
"Write one for every long (about 30 lines or more) or complex block; skip trivial code. An explanation judges nothing: problems go in findings. " +
|
|
540
|
+
"Each location is checked against the frozen diff and its lines are copied; if any explanation is invalid, nothing is added and every problem is reported.",
|
|
541
|
+
inputSchema: {
|
|
542
|
+
type: "object",
|
|
543
|
+
properties: {
|
|
544
|
+
analysis_id: { type: "string", description: ANALYSIS_ID_DOC },
|
|
545
|
+
explanations: {
|
|
546
|
+
type: "array",
|
|
547
|
+
minItems: 1,
|
|
548
|
+
items: {
|
|
549
|
+
type: "object",
|
|
550
|
+
properties: {
|
|
551
|
+
id: { type: "string", description: "Optional id (letters, digits, '_' or '-'); generated when omitted." },
|
|
552
|
+
title: { type: "string", description: 'Name of the block and its role, e.g. "parseUnifiedDiff: diff text to hunks".' },
|
|
553
|
+
body: { type: "string", description: EXPLANATION_BODY_DOC },
|
|
554
|
+
...EXPLANATION_LOCATION_PROPERTIES,
|
|
555
|
+
},
|
|
556
|
+
required: ["title", "body", "path", "start_line"],
|
|
557
|
+
},
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
required: ["analysis_id", "explanations"],
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
name: "update_explanation",
|
|
565
|
+
description: "Change one explanation: title, body or location. A new location (path and start_line, with side for removed code) is checked against the diff " +
|
|
566
|
+
"and makes an outdated explanation current again; rewrite the body too when the code changed.",
|
|
567
|
+
inputSchema: {
|
|
568
|
+
type: "object",
|
|
569
|
+
properties: {
|
|
570
|
+
analysis_id: { type: "string", description: ANALYSIS_ID_DOC },
|
|
571
|
+
explanation_id: { type: "string", description: "Id of the explanation, as shown by get_analysis." },
|
|
572
|
+
title: { type: "string", description: "New title." },
|
|
573
|
+
body: { type: "string", description: EXPLANATION_BODY_DOC },
|
|
574
|
+
...EXPLANATION_LOCATION_PROPERTIES,
|
|
575
|
+
},
|
|
576
|
+
required: ["analysis_id", "explanation_id"],
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
name: "delete_explanations",
|
|
581
|
+
description: "Delete explanations by id, e.g. when the code they describe is gone. If any id is unknown, nothing is deleted.",
|
|
582
|
+
inputSchema: {
|
|
583
|
+
type: "object",
|
|
584
|
+
properties: {
|
|
585
|
+
analysis_id: { type: "string", description: ANALYSIS_ID_DOC },
|
|
586
|
+
explanation_ids: { type: "array", minItems: 1, items: { type: "string" }, description: "Ids of the explanations to delete." },
|
|
587
|
+
},
|
|
588
|
+
required: ["analysis_id", "explanation_ids"],
|
|
589
|
+
},
|
|
590
|
+
},
|
|
506
591
|
{
|
|
507
592
|
name: "set_diagram",
|
|
508
593
|
description: "Create a diagram, or fully replace the one with diagram_id, when a picture helps the reviewer (impact perimeter, process flow, concept map). " +
|
|
@@ -647,6 +732,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
647
732
|
return afterMutation(updateFinding(store, params));
|
|
648
733
|
case "delete_findings":
|
|
649
734
|
return afterMutation(deleteFindings(store, params));
|
|
735
|
+
case "add_explanations":
|
|
736
|
+
return afterMutation(addExplanations(store, params));
|
|
737
|
+
case "update_explanation":
|
|
738
|
+
return afterMutation(updateExplanation(store, params));
|
|
739
|
+
case "delete_explanations":
|
|
740
|
+
return afterMutation(deleteExplanations(store, params));
|
|
650
741
|
case "set_diagram":
|
|
651
742
|
return afterMutation(setDiagram(store, params));
|
|
652
743
|
case "delete_diagram":
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AnalysisStore } from "../core/analysis-store.js";
|
|
2
|
+
import { type Args, type MutationResult } from "./types.js";
|
|
3
|
+
export declare const EXPLANATION_INPUT_FIELDS: readonly ["id", "title", "body", "path", "side", "start_line", "end_line"];
|
|
4
|
+
/**
|
|
5
|
+
* Ajoute un lot d'explications. Tout le lot est validé (champs, identifiants,
|
|
6
|
+
* emplacements résolus sur le snapshot) avant l'écriture : une seule explication
|
|
7
|
+
* invalide et rien n'est ajouté.
|
|
8
|
+
*/
|
|
9
|
+
export declare function addExplanations(store: AnalysisStore, args: Args): MutationResult;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { AnalysisError } from "../core/errors.js";
|
|
2
|
+
import { itemId } from "../core/ids.js";
|
|
3
|
+
import { resolveExplanationLocation } from "../core/locations.js";
|
|
4
|
+
import { explanationLine } from "./format.js";
|
|
5
|
+
import { readExplanationLocation, readItemId } from "./inputs.js";
|
|
6
|
+
import { rejectUnknownFields, requireArray, requireObject, requireString, requireText, textResult, } from "./types.js";
|
|
7
|
+
export const EXPLANATION_INPUT_FIELDS = ["id", "title", "body", "path", "side", "start_line", "end_line"];
|
|
8
|
+
/**
|
|
9
|
+
* Ajoute un lot d'explications. Tout le lot est validé (champs, identifiants,
|
|
10
|
+
* emplacements résolus sur le snapshot) avant l'écriture : une seule explication
|
|
11
|
+
* invalide et rien n'est ajouté.
|
|
12
|
+
*/
|
|
13
|
+
export function addExplanations(store, args) {
|
|
14
|
+
rejectUnknownFields(args, ["analysis_id", "explanations"]);
|
|
15
|
+
const id = requireString(args, "analysis_id");
|
|
16
|
+
const items = requireArray(args, "explanations", 1);
|
|
17
|
+
const analysis = store.get(id);
|
|
18
|
+
const snapshot = store.getSnapshot(id);
|
|
19
|
+
const taken = new Set(analysis.explanations.map((explanation) => explanation.id));
|
|
20
|
+
const batchIds = new Set();
|
|
21
|
+
const prepared = [];
|
|
22
|
+
const errors = [];
|
|
23
|
+
items.forEach((raw, index) => {
|
|
24
|
+
try {
|
|
25
|
+
const item = requireObject(raw, `explanations[${index}]`);
|
|
26
|
+
rejectUnknownFields(item, EXPLANATION_INPUT_FIELDS);
|
|
27
|
+
const explanationId = readItemId(item, "id");
|
|
28
|
+
if (explanationId !== undefined) {
|
|
29
|
+
if (taken.has(explanationId))
|
|
30
|
+
throw new Error(`id "${explanationId}" is already used by another explanation of this analysis.`);
|
|
31
|
+
if (batchIds.has(explanationId))
|
|
32
|
+
throw new Error(`id "${explanationId}" appears twice in this batch.`);
|
|
33
|
+
batchIds.add(explanationId);
|
|
34
|
+
}
|
|
35
|
+
const title = requireString(item, "title");
|
|
36
|
+
const body = requireText(item, "body");
|
|
37
|
+
const location = readExplanationLocation(item);
|
|
38
|
+
if (location === undefined) {
|
|
39
|
+
throw new Error(`an explanation describes lines of code: give "path" and "start_line" (and "side": "old" for removed code).`);
|
|
40
|
+
}
|
|
41
|
+
const { anchorText, ...resolved } = resolveExplanationLocation(snapshot, analysis.files, location);
|
|
42
|
+
prepared.push({ id: explanationId, title, body, location: resolved, anchorText });
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
errors.push(`- explanations[${index}]: ${err.message}`);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
if (errors.length > 0) {
|
|
49
|
+
throw new AnalysisError(`No explanation was added: ${errors.length} of ${items.length} explanation(s) are invalid. Fix them and send the whole batch again.\n` +
|
|
50
|
+
errors.join("\n"));
|
|
51
|
+
}
|
|
52
|
+
const added = [];
|
|
53
|
+
const updated = store.mutate(id, "agent", (draft) => {
|
|
54
|
+
if (draft.snapshotAt !== analysis.snapshotAt) {
|
|
55
|
+
throw new AnalysisError(`The diff of analysis "${id}" was recomputed while the explanations were validated. Send the batch again.`);
|
|
56
|
+
}
|
|
57
|
+
const ids = new Set([...draft.explanations.map((explanation) => explanation.id), ...batchIds]);
|
|
58
|
+
const now = new Date().toISOString();
|
|
59
|
+
for (const explanation of prepared) {
|
|
60
|
+
const newId = explanation.id ?? itemId("explanation", ids);
|
|
61
|
+
ids.add(newId);
|
|
62
|
+
const created = { ...explanation, id: newId, status: "current", createdAt: now };
|
|
63
|
+
added.push(created);
|
|
64
|
+
draft.explanations.push(created);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
const lines = [
|
|
68
|
+
`Added ${added.length} explanation(s) to "${updated.title}" (${updated.id}):`,
|
|
69
|
+
...added.map((explanation) => ` ${explanationLine(explanation)}`),
|
|
70
|
+
`The analysis has ${updated.explanations.length} explanation(s). The reviewer reads them in the diff, above the lines they describe.`,
|
|
71
|
+
];
|
|
72
|
+
return { result: textResult(lines.join("\n")), analysisId: updated.id };
|
|
73
|
+
}
|
|
@@ -31,6 +31,6 @@ export async function createAnalysis(store, args, repoFor = localRepo) {
|
|
|
31
31
|
else {
|
|
32
32
|
lines.push(`Changed files (${fileTotals(analysis.files)}):`, ...analysis.files.map((file) => ` ${fileLine(file)}`));
|
|
33
33
|
}
|
|
34
|
-
lines.push("", "Next steps:", `1. Read the frozen diff with get_diff (analysis_id "${analysis.id}"); anchor findings on its new-side line numbers.`, "2. Write the overview (objective, approach, attention_points) and the summary (the functional changes of the feature) with update_analysis.", "3. Record problems with add_findings, including requirement_gap findings for requested behaviour that is missing.", "
|
|
34
|
+
lines.push("", "Next steps:", `1. Read the frozen diff with get_diff (analysis_id "${analysis.id}"); anchor findings on its new-side line numbers.`, "2. Write the overview (objective, approach, attention_points) and the summary (the functional changes of the feature) with update_analysis.", "3. Explain every long (about 30 lines or more) or complex block of added or removed code with add_explanations: what it does and how it works.", "4. Record problems with add_findings, including requirement_gap findings for requested behaviour that is missing.", "5. Add diagrams with set_diagram when a picture helps the reviewer: one subject each, 5 to 12 nodes, no crossing.", "6. Ask the user to review the analysis in the GUI, then read the outcome with get_review_feedback.");
|
|
35
35
|
return { result: textResult(lines.join("\n")), analysisId: analysis.id };
|
|
36
36
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AnalysisStore } from "../core/analysis-store.js";
|
|
2
|
+
import { type Args, type MutationResult } from "./types.js";
|
|
3
|
+
/** Supprime des explications par identifiant. Un identifiant inconnu fait échouer l'appel sans rien supprimer. */
|
|
4
|
+
export declare function deleteExplanations(store: AnalysisStore, args: Args): MutationResult;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AnalysisError } from "../core/errors.js";
|
|
2
|
+
import { rejectUnknownFields, requireString, requireStringArray, textResult } from "./types.js";
|
|
3
|
+
/** Supprime des explications par identifiant. Un identifiant inconnu fait échouer l'appel sans rien supprimer. */
|
|
4
|
+
export function deleteExplanations(store, args) {
|
|
5
|
+
rejectUnknownFields(args, ["analysis_id", "explanation_ids"]);
|
|
6
|
+
const id = requireString(args, "analysis_id");
|
|
7
|
+
const ids = [...new Set(requireStringArray(args, "explanation_ids", 1))];
|
|
8
|
+
const updated = store.mutate(id, "agent", (draft) => {
|
|
9
|
+
const known = new Set(draft.explanations.map((explanation) => explanation.id));
|
|
10
|
+
const unknown = ids.filter((explanationId) => !known.has(explanationId));
|
|
11
|
+
if (unknown.length > 0) {
|
|
12
|
+
const existing = draft.explanations.map((explanation) => explanation.id).join(", ") || "none";
|
|
13
|
+
throw new AnalysisError(`No explanation was deleted: unknown explanation id(s) ${unknown.join(", ")} in analysis "${id}". Existing explanations: ${existing}.`);
|
|
14
|
+
}
|
|
15
|
+
const removed = new Set(ids);
|
|
16
|
+
draft.explanations = draft.explanations.filter((explanation) => !removed.has(explanation.id));
|
|
17
|
+
});
|
|
18
|
+
return {
|
|
19
|
+
result: textResult(`Deleted ${ids.length} explanation(s) from "${updated.title}" (${updated.id}): ${ids.join(", ")}. ` +
|
|
20
|
+
`${updated.explanations.length} explanation(s) remain.`),
|
|
21
|
+
analysisId: updated.id,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Analysis, type FileEntry, type Finding, type Note, type Overview, type ReviewProgress, type ReviewProgressState, type Severity } from "../../shared/schemas/analysis.schema.js";
|
|
1
|
+
import { type Analysis, type Explanation, type FileEntry, type Finding, type Note, type Overview, type ReviewProgress, type ReviewProgressState, type Severity } from "../../shared/schemas/analysis.schema.js";
|
|
2
2
|
/** Sha abrégé à 7 caractères. */
|
|
3
3
|
export declare function shortSha(sha: string | null): string;
|
|
4
4
|
/** Refs d'une analyse : `branch master...HEAD` ou `working_tree (changes against HEAD)`. */
|
|
@@ -15,6 +15,8 @@ export declare function locationLabel(finding: Pick<Finding, "location">): strin
|
|
|
15
15
|
export declare function noteLocationLabel(note: Pick<Note, "location">): string;
|
|
16
16
|
/** Ligne d'un constat : id, gravité, nature, statut, emplacement, titre. */
|
|
17
17
|
export declare function findingLine(finding: Finding): string;
|
|
18
|
+
/** Ligne d'une explication : id, statut, emplacement avec son côté, titre. */
|
|
19
|
+
export declare function explanationLine(explanation: Explanation): string;
|
|
18
20
|
/** `1 critical, 2 major` ; `none` quand tous les compteurs sont à zéro. */
|
|
19
21
|
export declare function severityCountsLabel(counts: Record<Severity, number>): string;
|
|
20
22
|
/** Préfixe chaque ligne non vide de `indent`. */
|
package/dist/src/tools/format.js
CHANGED
|
@@ -52,6 +52,11 @@ export function noteLocationLabel(note) {
|
|
|
52
52
|
export function findingLine(finding) {
|
|
53
53
|
return `${finding.id} [${finding.severity}] ${finding.kind}, ${finding.status} — ${locationLabel(finding)} — ${finding.title}`;
|
|
54
54
|
}
|
|
55
|
+
/** Ligne d'une explication : id, statut, emplacement avec son côté, titre. */
|
|
56
|
+
export function explanationLine(explanation) {
|
|
57
|
+
const { path, side, startLine, endLine } = explanation.location;
|
|
58
|
+
return `${explanation.id} ${explanation.status} — ${path}:${lineRange(startLine, endLine)} (${side} side) — ${explanation.title}`;
|
|
59
|
+
}
|
|
55
60
|
/** `1 critical, 2 major` ; `none` quand tous les compteurs sont à zéro. */
|
|
56
61
|
export function severityCountsLabel(counts) {
|
|
57
62
|
const parts = SEVERITIES.filter((severity) => counts[severity] > 0).map((severity) => `${counts[severity]} ${severity}`);
|
|
@@ -2,7 +2,7 @@ import { summarize } from "../core/analysis-store.js";
|
|
|
2
2
|
import { diagramQuality } from "../../shared/diagram-quality.js";
|
|
3
3
|
import { reviewProgress } from "../../shared/review-state.js";
|
|
4
4
|
import { sortBySeverity } from "../../shared/severity.js";
|
|
5
|
-
import { fileLine, fileTotals, findingLine, noteLocationLabel, overviewLines, progressLabel, severityCountsLabel, snapshotLabel, } from "./format.js";
|
|
5
|
+
import { fileLine, explanationLine, fileTotals, findingLine, noteLocationLabel, overviewLines, progressLabel, severityCountsLabel, snapshotLabel, } from "./format.js";
|
|
6
6
|
import { rejectUnknownFields, requireString, textResult } from "./types.js";
|
|
7
7
|
function noteLine(note) {
|
|
8
8
|
return `${note.id} — ${noteLocationLabel(note)} — ${note.text.replace(/\n/g, " / ")}`;
|
|
@@ -74,6 +74,11 @@ export function getAnalysis(store, args) {
|
|
|
74
74
|
if (analysis.findings.length === 0)
|
|
75
75
|
lines.push(" none (record them with add_findings)");
|
|
76
76
|
lines.push(...sortBySeverity(analysis.findings).map((finding) => ` ${findingLine(finding)}`), "");
|
|
77
|
+
const outdatedExplanations = analysis.explanations.filter((explanation) => explanation.status === "outdated").length;
|
|
78
|
+
lines.push(`Explanations (${analysis.explanations.length}: ${outdatedExplanations} outdated):`);
|
|
79
|
+
if (analysis.explanations.length === 0)
|
|
80
|
+
lines.push(" none (explain long or complex blocks with add_explanations)");
|
|
81
|
+
lines.push(...analysis.explanations.map((explanation) => ` ${explanationLine(explanation)}`), "");
|
|
77
82
|
lines.push(`Reviewer notes (${analysis.notes.length}):`);
|
|
78
83
|
if (analysis.notes.length === 0)
|
|
79
84
|
lines.push(" none");
|
|
@@ -52,7 +52,8 @@ export function getDiff(store, args) {
|
|
|
52
52
|
const lines = [
|
|
53
53
|
`Frozen diff of "${analysis.title}" (${analysis.id}): ${snapshotLabel(analysis)}.`,
|
|
54
54
|
"Columns: old line | new line, then + (added), - (removed) or a space (context). " +
|
|
55
|
-
"Anchor findings on the NEW-side numbers (second column)
|
|
55
|
+
"Anchor findings and explanations of added code on the NEW-side numbers (second column); " +
|
|
56
|
+
'explanations of removed code use the OLD-side numbers (first column) with "side": "old".',
|
|
56
57
|
];
|
|
57
58
|
if (entries.length === 0)
|
|
58
59
|
lines.push("", "The analysis has no changed files.");
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ExplanationSide } from "../../shared/schemas/analysis.schema.js";
|
|
1
2
|
import { type Args } from "./types.js";
|
|
2
3
|
/** Emplacement demandé par l'agent : `null` pour retirer l'emplacement, `undefined` s'il n'en donne pas. */
|
|
3
4
|
export type LocationInput = {
|
|
@@ -10,5 +11,18 @@ export type LocationInput = {
|
|
|
10
11
|
* `end_line` vaut `start_line` quand il est omis. `path: null` retire l'emplacement.
|
|
11
12
|
*/
|
|
12
13
|
export declare function readLocation(args: Args, prefix?: string): LocationInput;
|
|
14
|
+
/** Emplacement d'une explication demandé par l'agent ; `undefined` s'il n'en donne pas. */
|
|
15
|
+
export type ExplanationLocationInput = {
|
|
16
|
+
path: string;
|
|
17
|
+
side: ExplanationSide;
|
|
18
|
+
startLine: number;
|
|
19
|
+
endLine: number | undefined;
|
|
20
|
+
} | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Lit `path`, `side`, `start_line` et `end_line` d'une explication. `path` et
|
|
23
|
+
* `start_line` vont ensemble ; `side` vaut `new` quand il est omis ; `end_line`
|
|
24
|
+
* vaut `start_line` quand il est omis. Une explication a toujours un emplacement.
|
|
25
|
+
*/
|
|
26
|
+
export declare function readExplanationLocation(args: Args, prefix?: string): ExplanationLocationInput;
|
|
13
27
|
/** Lit un identifiant d'élément facultatif fourni par l'agent. */
|
|
14
28
|
export declare function readItemId(args: Args, field: string, prefix?: string): string | undefined;
|
package/dist/src/tools/inputs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ITEM_ID_PATTERN } from "../../shared/schemas/analysis.schema.js";
|
|
2
|
-
import { has, nullableString, optionalPositiveInt } from "./types.js";
|
|
1
|
+
import { EXPLANATION_SIDES, ITEM_ID_PATTERN } from "../../shared/schemas/analysis.schema.js";
|
|
2
|
+
import { has, nullableString, optionalEnum, optionalPositiveInt } from "./types.js";
|
|
3
3
|
/**
|
|
4
4
|
* Lit `path`, `start_line` et `end_line`. `path` et `start_line` vont ensemble ;
|
|
5
5
|
* `end_line` vaut `start_line` quand il est omis. `path: null` retire l'emplacement.
|
|
@@ -28,6 +28,24 @@ export function readLocation(args, prefix = "") {
|
|
|
28
28
|
}
|
|
29
29
|
return { path, startLine, endLine };
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Lit `path`, `side`, `start_line` et `end_line` d'une explication. `path` et
|
|
33
|
+
* `start_line` vont ensemble ; `side` vaut `new` quand il est omis ; `end_line`
|
|
34
|
+
* vaut `start_line` quand il est omis. Une explication a toujours un emplacement.
|
|
35
|
+
*/
|
|
36
|
+
export function readExplanationLocation(args, prefix = "") {
|
|
37
|
+
const side = optionalEnum(args, "side", EXPLANATION_SIDES, prefix);
|
|
38
|
+
const location = readLocation(args, prefix);
|
|
39
|
+
if (location === null) {
|
|
40
|
+
throw new Error(`"${prefix}path" cannot be null: an explanation always describes lines of a changed file.`);
|
|
41
|
+
}
|
|
42
|
+
if (location === undefined) {
|
|
43
|
+
if (side !== undefined)
|
|
44
|
+
throw new Error(`"${prefix}side" needs "${prefix}path" and "${prefix}start_line": give the lines it refers to.`);
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
return { ...location, side: side ?? "new" };
|
|
48
|
+
}
|
|
31
49
|
/** Lit un identifiant d'élément facultatif fourni par l'agent. */
|
|
32
50
|
export function readItemId(args, field, prefix = "") {
|
|
33
51
|
if (!has(args, field))
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { computeSnapshot, localRepo } from "../core/git.js";
|
|
2
|
-
import { snapshotLabel } from "./format.js";
|
|
2
|
+
import { explanationLine, snapshotLabel } from "./format.js";
|
|
3
3
|
import { rejectUnknownFields, requireString, textResult } from "./types.js";
|
|
4
4
|
/**
|
|
5
5
|
* Recalcule le snapshot avec le dépôt, le mode et les refs de l'analyse, lus par
|
|
@@ -19,6 +19,8 @@ export async function refreshAnalysis(store, args, repoFor = localRepo) {
|
|
|
19
19
|
`Reviewed files: ${stats.reviewedKept} kept (diff unchanged), ${stats.reviewedReset} reset to unreviewed (diff changed).`,
|
|
20
20
|
`Findings: ${stats.findingsOutdated} became outdated (their lines changed or left the diff), ` +
|
|
21
21
|
`${stats.findingsRestored} back to open (their lines match again); ${outdated.length} outdated in total.`,
|
|
22
|
+
`Explanations: ${stats.explanationsOutdated} became outdated (the code they describe changed or left the diff), ` +
|
|
23
|
+
`${stats.explanationsRestored} current again.`,
|
|
22
24
|
];
|
|
23
25
|
if (previousReview.state === "submitted") {
|
|
24
26
|
lines.push(`Review: the previous feedback (decision ${previousReview.decision}, submitted at ${previousReview.submittedAt}) was discarded; the review is pending again.`);
|
|
@@ -26,9 +28,13 @@ export async function refreshAnalysis(store, args, repoFor = localRepo) {
|
|
|
26
28
|
else {
|
|
27
29
|
lines.push("Review: pending; the previous feedback, decision and selection in progress were discarded.");
|
|
28
30
|
}
|
|
29
|
-
lines.push("", "Next steps:", "- Re-read the changed files with get_diff.", "- For each outdated finding: re-anchor it with update_finding (a new location reopens it), or remove it with delete_findings if it is solved.", "- Ask the user to review the analysis again in the GUI.");
|
|
31
|
+
lines.push("", "Next steps:", "- Re-read the changed files with get_diff.", "- For each outdated finding: re-anchor it with update_finding (a new location reopens it), or remove it with delete_findings if it is solved.", "- For each outdated explanation: re-anchor it and rewrite its body with update_explanation, or remove it with delete_explanations.", "- Explain the new long or complex blocks with add_explanations.", "- Ask the user to review the analysis again in the GUI.");
|
|
30
32
|
if (outdated.length > 0) {
|
|
31
33
|
lines.push("", "Outdated findings:", ...outdated.map((finding) => ` ${finding.id} [${finding.severity}] ${finding.title}`));
|
|
32
34
|
}
|
|
35
|
+
const outdatedExplanations = analysis.explanations.filter((explanation) => explanation.status === "outdated");
|
|
36
|
+
if (outdatedExplanations.length > 0) {
|
|
37
|
+
lines.push("", "Outdated explanations:", ...outdatedExplanations.map((explanation) => ` ${explanationLine(explanation)}`));
|
|
38
|
+
}
|
|
33
39
|
return { result: textResult(lines.join("\n")), analysisId: analysis.id };
|
|
34
40
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AnalysisStore } from "../core/analysis-store.js";
|
|
2
|
+
import { type Args, type MutationResult } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Modifie une explication. Un nouvel emplacement est résolu sur le snapshot et
|
|
5
|
+
* remet une explication `outdated` à `current`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function updateExplanation(store: AnalysisStore, args: Args): MutationResult;
|