@imenam/mcp-feature-analyzer 1.0.3 → 1.0.4

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.
Files changed (44) hide show
  1. package/README.md +1 -1
  2. package/dist/shared/schemas/analysis.schema.d.ts +61 -0
  3. package/dist/shared/schemas/analysis.schema.js +26 -0
  4. package/dist/shared/schemas/api.schema.d.ts +13 -0
  5. package/dist/shared/schemas/api.schema.js +8 -0
  6. package/dist/shared/schemas/ipc.schema.d.ts +1 -1
  7. package/dist/shared/schemas/ipc.schema.js +1 -0
  8. package/dist/src/core/analysis-store.js +1 -0
  9. package/dist/src/core/ids.d.ts +1 -0
  10. package/dist/src/core/ids.js +1 -0
  11. package/dist/src/core/invariants.d.ts +3 -1
  12. package/dist/src/core/invariants.js +27 -1
  13. package/dist/src/core/locations.d.ts +22 -4
  14. package/dist/src/core/locations.js +88 -4
  15. package/dist/src/core/migrate.d.ts +2 -0
  16. package/dist/src/core/migrate.js +3 -1
  17. package/dist/src/core/refresh.d.ts +8 -2
  18. package/dist/src/core/refresh.js +19 -1
  19. package/dist/src/core/review-actions.d.ts +6 -1
  20. package/dist/src/core/review-actions.js +20 -0
  21. package/dist/src/gui-worker.js +7 -1
  22. package/dist/src/index.js +100 -9
  23. package/dist/src/tools/add-explanations.d.ts +9 -0
  24. package/dist/src/tools/add-explanations.js +73 -0
  25. package/dist/src/tools/create-analysis.js +1 -1
  26. package/dist/src/tools/delete-explanations.d.ts +4 -0
  27. package/dist/src/tools/delete-explanations.js +23 -0
  28. package/dist/src/tools/format.d.ts +3 -1
  29. package/dist/src/tools/format.js +5 -0
  30. package/dist/src/tools/get-analysis.js +6 -1
  31. package/dist/src/tools/get-diff.js +2 -1
  32. package/dist/src/tools/inputs.d.ts +14 -0
  33. package/dist/src/tools/inputs.js +20 -2
  34. package/dist/src/tools/refresh-analysis.js +8 -2
  35. package/dist/src/tools/update-explanation.d.ts +7 -0
  36. package/dist/src/tools/update-explanation.js +57 -0
  37. package/dist/src/version.d.ts +1 -1
  38. package/dist/src/version.js +1 -1
  39. package/gui/dist/assets/index-BLyQW-EW.js +72 -0
  40. package/gui/dist/assets/index-CmSZOGwc.css +1 -0
  41. package/gui/dist/index.html +2 -2
  42. package/package.json +1 -1
  43. package/gui/dist/assets/index-Db7Hu3tM.js +0 -72
  44. package/gui/dist/assets/index-Du5eL78F.css +0 -1
@@ -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. add_findings for every problem, including requirement_gap findings for requested behaviour that is missing.
99
- 5. set_diagram when a picture helps (impacted layers, process flow, concept tree).
100
- 6. Tell the user to review the analysis in the GUI (the link is in the tool results).
101
- 7. get_review_feedback to read the decision, the selected points and the prompt.
102
- 8. Fix the code, then refresh_analysis to recompute the diff for a new review round.
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 always NEW-side numbers, as shown by get_diff.
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.", "4. Add diagrams with set_diagram when a picture helps the reviewer: one subject each, 5 to 12 nodes, no crossing.", "5. Ask the user to review the analysis in the GUI, then read the outcome with get_review_feedback.");
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`. */
@@ -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;
@@ -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;
@@ -0,0 +1,57 @@
1
+ import { AnalysisError } from "../core/errors.js";
2
+ import { resolveExplanationLocation } from "../core/locations.js";
3
+ import { explanationLine } from "./format.js";
4
+ import { readExplanationLocation } from "./inputs.js";
5
+ import { has, optionalString, rejectUnknownFields, requireString, requireText, textResult } from "./types.js";
6
+ const EDITABLE = ["title", "body", "path", "side", "start_line", "end_line"];
7
+ /**
8
+ * Modifie une explication. Un nouvel emplacement est résolu sur le snapshot et
9
+ * remet une explication `outdated` à `current`.
10
+ */
11
+ export function updateExplanation(store, args) {
12
+ rejectUnknownFields(args, ["analysis_id", "explanation_id", ...EDITABLE]);
13
+ const id = requireString(args, "analysis_id");
14
+ const explanationId = requireString(args, "explanation_id");
15
+ const given = EDITABLE.filter((field) => has(args, field));
16
+ if (given.length === 0) {
17
+ throw new Error(`Give at least one field to change: ${EDITABLE.join(", ")}.`);
18
+ }
19
+ const title = optionalString(args, "title");
20
+ const body = has(args, "body") ? requireText(args, "body") : undefined;
21
+ const location = readExplanationLocation(args);
22
+ const analysis = store.get(id);
23
+ const current = analysis.explanations.find((explanation) => explanation.id === explanationId);
24
+ if (!current) {
25
+ const known = analysis.explanations.map((explanation) => explanation.id).join(", ") || "none";
26
+ throw new AnalysisError(`Explanation "${explanationId}" not found in analysis "${id}". Existing explanations: ${known}.`);
27
+ }
28
+ const resolved = location === undefined ? undefined : resolveExplanationLocation(store.getSnapshot(id), analysis.files, location);
29
+ const previousStatus = current.status;
30
+ const updated = store.mutate(id, "agent", (draft) => {
31
+ if (draft.snapshotAt !== analysis.snapshotAt) {
32
+ throw new AnalysisError(`The diff of analysis "${id}" was recomputed while the explanation was validated. Send the update again.`);
33
+ }
34
+ const explanation = draft.explanations.find((entry) => entry.id === explanationId);
35
+ if (!explanation)
36
+ throw new AnalysisError(`Explanation "${explanationId}" was deleted in the meantime from analysis "${id}".`);
37
+ if (title !== undefined)
38
+ explanation.title = title;
39
+ if (body !== undefined)
40
+ explanation.body = body;
41
+ if (resolved) {
42
+ const { anchorText, ...newLocation } = resolved;
43
+ explanation.location = newLocation;
44
+ explanation.anchorText = anchorText;
45
+ explanation.status = "current";
46
+ }
47
+ });
48
+ const explanation = updated.explanations.find((entry) => entry.id === explanationId);
49
+ const lines = [`Updated explanation ${explanationId} (${given.join(", ")}):`, ` ${explanationLine(explanation)}`];
50
+ if (previousStatus === "outdated" && explanation.status === "current") {
51
+ lines.push("It was outdated; the new location made it current again.");
52
+ }
53
+ else if (explanation.status === "outdated") {
54
+ lines.push("It is still outdated: re-read the code with get_diff, then give its new location (path, side, start_line) and rewrite the body if the code changed, or delete it with delete_explanations.");
55
+ }
56
+ return { result: textResult(lines.join("\n")), analysisId: updated.id };
57
+ }
@@ -1 +1 @@
1
- export declare const APP_VERSION = "1.0.3";
1
+ export declare const APP_VERSION = "1.0.4";
@@ -1,2 +1,2 @@
1
1
  // Auto-generated version file - do not edit manually
2
- export const APP_VERSION = '1.0.3';
2
+ export const APP_VERSION = '1.0.4';