@imenam/mcp-feature-analyzer 1.0.2 → 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.
- package/README.md +336 -295
- 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/agent-repo.d.ts +4 -0
- package/dist/src/core/agent-repo.js +172 -0
- package/dist/src/core/analysis-store.js +1 -0
- package/dist/src/core/gateway-exec.d.ts +27 -0
- package/dist/src/core/gateway-exec.js +115 -0
- package/dist/src/core/git.d.ts +27 -2
- package/dist/src/core/git.js +48 -34
- 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/core/role.d.ts +11 -0
- package/dist/src/core/role.js +9 -0
- package/dist/src/gui-worker.js +7 -1
- package/dist/src/index.js +139 -19
- 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.d.ts +3 -2
- package/dist/src/tools/create-analysis.js +5 -5
- 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.d.ts +4 -3
- package/dist/src/tools/refresh-analysis.js +13 -12
- 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-BLyQW-EW.js +72 -0
- package/gui/dist/assets/index-CmSZOGwc.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
package/dist/src/index.js
CHANGED
|
@@ -12,11 +12,14 @@ import { resolveDataDir } from "./core/data-dir.js";
|
|
|
12
12
|
import { resolveLogDir } from "./core/log-dir.js";
|
|
13
13
|
import { AnalysisStore } from "./core/analysis-store.js";
|
|
14
14
|
import { AnalysisError, NotFoundError, RevisionConflictError } from "./core/errors.js";
|
|
15
|
-
import { DEFAULT_HEAD_REF } from "./core/git.js";
|
|
16
|
-
import {
|
|
15
|
+
import { DEFAULT_HEAD_REF, localRepo } from "./core/git.js";
|
|
16
|
+
import { agentRepo } from "./core/agent-repo.js";
|
|
17
|
+
import { EXEC_ARG, handleAgentExec } from "./core/gateway-exec.js";
|
|
18
|
+
import { resolveRole } from "./core/role.js";
|
|
19
|
+
import { addNote, deleteNote, setFileReviewed, setFindingStatus, submitReview, updateNote } from "./core/review-actions.js";
|
|
17
20
|
import { GUI_REQUEST_TYPES, IPCMessageSchema } from "../shared/schemas/ipc.schema.js";
|
|
18
|
-
import { AddNoteRequestSchema, AnalysisRequestSchema, DeleteNoteRequestSchema, SetFileReviewedRequestSchema, SetFindingStatusRequestSchema, SubmitReviewRequestSchema, } from "../shared/schemas/api.schema.js";
|
|
19
|
-
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";
|
|
20
23
|
import { errorResult, textResult } from "./tools/types.js";
|
|
21
24
|
import { createAnalysis } from "./tools/create-analysis.js";
|
|
22
25
|
import { listAnalyses } from "./tools/list-analyses.js";
|
|
@@ -27,6 +30,9 @@ import { updateAnalysis } from "./tools/update-analysis.js";
|
|
|
27
30
|
import { addFindings } from "./tools/add-findings.js";
|
|
28
31
|
import { updateFinding } from "./tools/update-finding.js";
|
|
29
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";
|
|
30
36
|
import { DEFAULT_NODE_SHAPE, DEFAULT_NODE_STATUS, setDiagram } from "./tools/set-diagram.js";
|
|
31
37
|
import { deleteDiagram } from "./tools/delete-diagram.js";
|
|
32
38
|
import { getReviewFeedback } from "./tools/get-review-feedback.js";
|
|
@@ -66,6 +72,15 @@ function getConfig() {
|
|
|
66
72
|
return { proxyUrl: process.env.PROXY_URL, appPort: process.env.APP_PORT, version: APP_VERSION };
|
|
67
73
|
}
|
|
68
74
|
getConfig();
|
|
75
|
+
let role;
|
|
76
|
+
try {
|
|
77
|
+
role = resolveRole();
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
logger.error(err.message);
|
|
81
|
+
console.error(err.message);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
69
84
|
const dataDir = resolveDataDir(rootDir);
|
|
70
85
|
const store = new AnalysisStore(dataDir);
|
|
71
86
|
/** Présentation du serveur transmise au client MCP : concepts, déroulé et règles. */
|
|
@@ -76,6 +91,7 @@ Concepts:
|
|
|
76
91
|
- A **project** groups the analyses of one product or feature; the GUI files analyses by project.
|
|
77
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.
|
|
78
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.
|
|
79
95
|
- A **diagram** is a flow, layers or mindmap picture of the feature. You give nodes and links only; the GUI lays them out.
|
|
80
96
|
- The **review** belongs to the reviewer: files marked reviewed, findings ignored, notes, and a submitted decision with a prompt for you.
|
|
81
97
|
|
|
@@ -83,11 +99,19 @@ Workflow:
|
|
|
83
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.
|
|
84
100
|
2. get_diff to read the frozen diff.
|
|
85
101
|
3. update_analysis with the overview (objective, approach, attention_points) and the summary (the functional changes).
|
|
86
|
-
4.
|
|
87
|
-
5.
|
|
88
|
-
6.
|
|
89
|
-
7.
|
|
90
|
-
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.
|
|
91
115
|
|
|
92
116
|
Diagram rules:
|
|
93
117
|
- One subject per diagram, 5 to 12 nodes; split a bigger picture into several diagrams.
|
|
@@ -96,7 +120,7 @@ Diagram rules:
|
|
|
96
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).
|
|
97
121
|
|
|
98
122
|
Rules:
|
|
99
|
-
- 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").
|
|
100
124
|
- Nothing is ever written to the repository: suggestions are displayed to the reviewer only.
|
|
101
125
|
- The reviewer alone decides which findings are ignored and when the review is submitted; you cannot change either.`;
|
|
102
126
|
const server = new Server({ name: "mcp-feature-analyzer", version: APP_VERSION }, { capabilities: { tools: {} }, instructions: INSTRUCTIONS });
|
|
@@ -180,6 +204,12 @@ function handleGuiRequest(msg) {
|
|
|
180
204
|
afterUserMutation(analysis.id);
|
|
181
205
|
return { analysis };
|
|
182
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
|
+
}
|
|
183
213
|
case "DELETE_NOTE": {
|
|
184
214
|
const request = parseRequest(DeleteNoteRequestSchema, msg.data);
|
|
185
215
|
const analysis = deleteNote(store, request.analysisId, request.noteId, request.body);
|
|
@@ -293,6 +323,18 @@ const LOCATION_PROPERTIES = {
|
|
|
293
323
|
start_line: { type: "integer", minimum: 1, description: `First targeted line. ${LINE_NUMBERS_DOC}` },
|
|
294
324
|
end_line: { type: "integer", minimum: 1, description: "Last targeted line, inclusive (default: start_line)." },
|
|
295
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.";
|
|
296
338
|
const SEVERITY_DOC = `Severity: ${SEVERITIES.join(", ")} (from most to least serious).`;
|
|
297
339
|
const FINDING_KIND_DOC = `Kind (default "issue"): "issue" is a problem in the changed code and needs a location; ` +
|
|
298
340
|
`"requirement_gap" is requested behaviour that is missing and may have no location.`;
|
|
@@ -334,7 +376,12 @@ const TOOLS = [
|
|
|
334
376
|
inputSchema: {
|
|
335
377
|
type: "object",
|
|
336
378
|
properties: {
|
|
337
|
-
repo_path: {
|
|
379
|
+
repo_path: {
|
|
380
|
+
type: "string",
|
|
381
|
+
description: role === "remote"
|
|
382
|
+
? "Absolute path of the root of the git repository on your machine: git runs there, through the agent relay of mcp-http-gateway."
|
|
383
|
+
: "Absolute path of the root of the git repository.",
|
|
384
|
+
},
|
|
338
385
|
project: { type: "string", description: PROJECT_DOC },
|
|
339
386
|
title: { type: "string", description: "Short title of the feature, e.g. \"OAuth token refresh\"." },
|
|
340
387
|
mode: {
|
|
@@ -486,6 +533,61 @@ const TOOLS = [
|
|
|
486
533
|
required: ["analysis_id", "finding_ids"],
|
|
487
534
|
},
|
|
488
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
|
+
},
|
|
489
591
|
{
|
|
490
592
|
name: "set_diagram",
|
|
491
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). " +
|
|
@@ -592,9 +694,20 @@ function afterMutation({ result, analysisId }) {
|
|
|
592
694
|
notifyAnalysisChanged(analysisId);
|
|
593
695
|
return { ...result, content: [{ type: "text", text: result.content[0].text + guiHint(analysisId) }] };
|
|
594
696
|
}
|
|
697
|
+
/**
|
|
698
|
+
* Donne à `fn` l'accès aux dépôts du rôle : git local en `standalone` ; en `remote`,
|
|
699
|
+
* commandes exécutées sur la machine de l'agent par le relais, qui annonce cette
|
|
700
|
+
* possibilité dans l'argument `exec`.
|
|
701
|
+
*/
|
|
702
|
+
function withRepoAccess(exec, fn) {
|
|
703
|
+
if (role === "standalone")
|
|
704
|
+
return fn(localRepo);
|
|
705
|
+
return handleAgentExec(exec, (run) => fn((repoPath) => agentRepo(repoPath, run)));
|
|
706
|
+
}
|
|
595
707
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
596
708
|
const { name, arguments: args } = request.params;
|
|
597
|
-
|
|
709
|
+
// L'argument réservé du relais agent accompagne tous les appels d'une route listée dans GATEWAY_EXEC_ROUTES.
|
|
710
|
+
const { [EXEC_ARG]: exec, ...params } = (args ?? {});
|
|
598
711
|
try {
|
|
599
712
|
switch (name) {
|
|
600
713
|
case "list_analyses":
|
|
@@ -605,11 +718,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
605
718
|
return getDiff(store, params);
|
|
606
719
|
case "get_review_feedback":
|
|
607
720
|
return getReviewFeedback(store, params);
|
|
608
|
-
case "create_analysis":
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
721
|
+
case "create_analysis":
|
|
722
|
+
return await withRepoAccess(exec, async (repoFor) => {
|
|
723
|
+
const mutation = await createAnalysis(store, params, repoFor);
|
|
724
|
+
notifyAnalysesListChanged();
|
|
725
|
+
return afterMutation(mutation);
|
|
726
|
+
});
|
|
613
727
|
case "update_analysis":
|
|
614
728
|
return afterMutation(updateAnalysis(store, params));
|
|
615
729
|
case "add_findings":
|
|
@@ -618,12 +732,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
618
732
|
return afterMutation(updateFinding(store, params));
|
|
619
733
|
case "delete_findings":
|
|
620
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));
|
|
621
741
|
case "set_diagram":
|
|
622
742
|
return afterMutation(setDiagram(store, params));
|
|
623
743
|
case "delete_diagram":
|
|
624
744
|
return afterMutation(deleteDiagram(store, params));
|
|
625
745
|
case "refresh_analysis":
|
|
626
|
-
return afterMutation(await refreshAnalysis(store, params));
|
|
746
|
+
return await withRepoAccess(exec, async (repoFor) => afterMutation(await refreshAnalysis(store, params, repoFor)));
|
|
627
747
|
case "delete_analysis": {
|
|
628
748
|
const { result, analysisId } = deleteAnalysis(store, params);
|
|
629
749
|
notifyAnalysisChanged(analysisId);
|
|
@@ -643,7 +763,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
643
763
|
async function main() {
|
|
644
764
|
const transport = new StdioServerTransport();
|
|
645
765
|
await server.connect(transport);
|
|
646
|
-
logger.info(`MCP Server connected and running on stdio (data dir: ${dataDir})`);
|
|
766
|
+
logger.info(`MCP Server connected and running on stdio (role: ${role}, data dir: ${dataDir})`);
|
|
647
767
|
launchGUI();
|
|
648
768
|
}
|
|
649
769
|
main().catch((err) => {
|
|
@@ -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
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AnalysisStore } from "../core/analysis-store.js";
|
|
2
|
+
import { type RepoAccess } from "../core/git.js";
|
|
2
3
|
import { type Args, type MutationResult } from "./types.js";
|
|
3
|
-
/** Calcule et fige le diff du dépôt, puis crée l'analyse. */
|
|
4
|
-
export declare function createAnalysis(store: AnalysisStore, args: Args): Promise<MutationResult>;
|
|
4
|
+
/** Calcule et fige le diff du dépôt, lu par `repoFor(repo_path)` (git local par défaut), puis crée l'analyse. */
|
|
5
|
+
export declare function createAnalysis(store: AnalysisStore, args: Args, repoFor?: (repoPath: string) => RepoAccess): Promise<MutationResult>;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { computeSnapshot } from "../core/git.js";
|
|
1
|
+
import { computeSnapshot, localRepo } from "../core/git.js";
|
|
2
2
|
import { ANALYSIS_MODES } from "../../shared/schemas/analysis.schema.js";
|
|
3
3
|
import { fileLine, fileTotals, snapshotLabel } from "./format.js";
|
|
4
4
|
import { optionalString, rejectUnknownFields, requireEnum, requireString, textResult, } from "./types.js";
|
|
5
5
|
const FIELDS = ["repo_path", "project", "title", "mode", "base", "head", "request_text", "request_source"];
|
|
6
|
-
/** Calcule et fige le diff du dépôt, puis crée l'analyse. */
|
|
7
|
-
export async function createAnalysis(store, args) {
|
|
6
|
+
/** Calcule et fige le diff du dépôt, lu par `repoFor(repo_path)` (git local par défaut), puis crée l'analyse. */
|
|
7
|
+
export async function createAnalysis(store, args, repoFor = localRepo) {
|
|
8
8
|
rejectUnknownFields(args, FIELDS);
|
|
9
9
|
const repoPath = requireString(args, "repo_path");
|
|
10
10
|
const project = requireString(args, "project");
|
|
@@ -17,7 +17,7 @@ export async function createAnalysis(store, args) {
|
|
|
17
17
|
if (requestSource !== undefined && requestText === undefined) {
|
|
18
18
|
throw new Error(`"request_source" describes the initial request: give "request_text" too.`);
|
|
19
19
|
}
|
|
20
|
-
const computed = await computeSnapshot({ repoPath, mode, base, head });
|
|
20
|
+
const computed = await computeSnapshot({ repoPath, mode, base, head }, repoFor(repoPath));
|
|
21
21
|
const analysis = store.create({ project, title, request: requestText === undefined ? null : { text: requestText, source: requestSource ?? null } }, computed);
|
|
22
22
|
const lines = [
|
|
23
23
|
`Created analysis "${analysis.title}" (id: ${analysis.id}) in project "${analysis.project}".`,
|
|
@@ -31,6 +31,6 @@ export async function createAnalysis(store, args) {
|
|
|
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,7 +1,8 @@
|
|
|
1
1
|
import type { AnalysisStore } from "../core/analysis-store.js";
|
|
2
|
+
import { type RepoAccess } from "../core/git.js";
|
|
2
3
|
import { type Args, type MutationResult } from "./types.js";
|
|
3
4
|
/**
|
|
4
|
-
* Recalcule le snapshot avec le dépôt, le mode et les refs de l'analyse
|
|
5
|
-
* revue repart à l'état `pending` sans décision, sélection ni prompt.
|
|
5
|
+
* Recalcule le snapshot avec le dépôt, le mode et les refs de l'analyse, lus par
|
|
6
|
+
* `repoFor` (git local par défaut) ; la revue repart à l'état `pending` sans décision, sélection ni prompt.
|
|
6
7
|
*/
|
|
7
|
-
export declare function refreshAnalysis(store: AnalysisStore, args: Args): Promise<MutationResult>;
|
|
8
|
+
export declare function refreshAnalysis(store: AnalysisStore, args: Args, repoFor?: (repoPath: string) => RepoAccess): Promise<MutationResult>;
|
|
@@ -1,20 +1,15 @@
|
|
|
1
|
-
import { computeSnapshot } from "../core/git.js";
|
|
2
|
-
import { snapshotLabel } from "./format.js";
|
|
1
|
+
import { computeSnapshot, localRepo } from "../core/git.js";
|
|
2
|
+
import { explanationLine, snapshotLabel } from "./format.js";
|
|
3
3
|
import { rejectUnknownFields, requireString, textResult } from "./types.js";
|
|
4
4
|
/**
|
|
5
|
-
* Recalcule le snapshot avec le dépôt, le mode et les refs de l'analyse
|
|
6
|
-
* revue repart à l'état `pending` sans décision, sélection ni prompt.
|
|
5
|
+
* Recalcule le snapshot avec le dépôt, le mode et les refs de l'analyse, lus par
|
|
6
|
+
* `repoFor` (git local par défaut) ; la revue repart à l'état `pending` sans décision, sélection ni prompt.
|
|
7
7
|
*/
|
|
8
|
-
export async function refreshAnalysis(store, args) {
|
|
8
|
+
export async function refreshAnalysis(store, args, repoFor = localRepo) {
|
|
9
9
|
rejectUnknownFields(args, ["analysis_id"]);
|
|
10
10
|
const id = requireString(args, "analysis_id");
|
|
11
11
|
const current = store.get(id);
|
|
12
|
-
const computed = await computeSnapshot({
|
|
13
|
-
repoPath: current.repoPath,
|
|
14
|
-
mode: current.mode,
|
|
15
|
-
base: current.base,
|
|
16
|
-
head: current.head,
|
|
17
|
-
});
|
|
12
|
+
const computed = await computeSnapshot({ repoPath: current.repoPath, mode: current.mode, base: current.base, head: current.head }, repoFor(current.repoPath));
|
|
18
13
|
const { analysis, stats, previousReview } = store.replaceSnapshot(id, "agent", computed);
|
|
19
14
|
const outdated = analysis.findings.filter((finding) => finding.status === "outdated");
|
|
20
15
|
const lines = [
|
|
@@ -24,6 +19,8 @@ export async function refreshAnalysis(store, args) {
|
|
|
24
19
|
`Reviewed files: ${stats.reviewedKept} kept (diff unchanged), ${stats.reviewedReset} reset to unreviewed (diff changed).`,
|
|
25
20
|
`Findings: ${stats.findingsOutdated} became outdated (their lines changed or left the diff), ` +
|
|
26
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.`,
|
|
27
24
|
];
|
|
28
25
|
if (previousReview.state === "submitted") {
|
|
29
26
|
lines.push(`Review: the previous feedback (decision ${previousReview.decision}, submitted at ${previousReview.submittedAt}) was discarded; the review is pending again.`);
|
|
@@ -31,9 +28,13 @@ export async function refreshAnalysis(store, args) {
|
|
|
31
28
|
else {
|
|
32
29
|
lines.push("Review: pending; the previous feedback, decision and selection in progress were discarded.");
|
|
33
30
|
}
|
|
34
|
-
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.");
|
|
35
32
|
if (outdated.length > 0) {
|
|
36
33
|
lines.push("", "Outdated findings:", ...outdated.map((finding) => ` ${finding.id} [${finding.severity}] ${finding.title}`));
|
|
37
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
|
+
}
|
|
38
39
|
return { result: textResult(lines.join("\n")), analysisId: analysis.id };
|
|
39
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;
|