@imenam/mcp-feature-analyzer 1.0.0
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/LICENSE +15 -0
- package/README.md +295 -0
- package/dist/shared/diagram-geometry.d.ts +45 -0
- package/dist/shared/diagram-geometry.js +120 -0
- package/dist/shared/diagram-layout.d.ts +98 -0
- package/dist/shared/diagram-layout.js +1156 -0
- package/dist/shared/diagram-quality.d.ts +30 -0
- package/dist/shared/diagram-quality.js +26 -0
- package/dist/shared/labels.d.ts +31 -0
- package/dist/shared/labels.js +53 -0
- package/dist/shared/mindmap-tree.d.ts +28 -0
- package/dist/shared/mindmap-tree.js +62 -0
- package/dist/shared/prompt.d.ts +17 -0
- package/dist/shared/prompt.js +97 -0
- package/dist/shared/review-state.d.ts +10 -0
- package/dist/shared/review-state.js +17 -0
- package/dist/shared/schemas/analysis.schema.d.ts +372 -0
- package/dist/shared/schemas/analysis.schema.js +152 -0
- package/dist/shared/schemas/api.schema.d.ts +92 -0
- package/dist/shared/schemas/api.schema.js +46 -0
- package/dist/shared/schemas/diff.schema.d.ts +91 -0
- package/dist/shared/schemas/diff.schema.js +37 -0
- package/dist/shared/schemas/ipc.schema.d.ts +29 -0
- package/dist/shared/schemas/ipc.schema.js +30 -0
- package/dist/shared/severity.d.ts +11 -0
- package/dist/shared/severity.js +17 -0
- package/dist/shared/text.d.ts +12 -0
- package/dist/shared/text.js +20 -0
- package/dist/src/cli/setup-mcp.d.ts +24 -0
- package/dist/src/cli/setup-mcp.js +145 -0
- package/dist/src/core/analysis-store.d.ts +95 -0
- package/dist/src/core/analysis-store.js +387 -0
- package/dist/src/core/data-dir.d.ts +5 -0
- package/dist/src/core/data-dir.js +11 -0
- package/dist/src/core/diff-parser.d.ts +22 -0
- package/dist/src/core/diff-parser.js +225 -0
- package/dist/src/core/errors.d.ts +16 -0
- package/dist/src/core/errors.js +28 -0
- package/dist/src/core/git.d.ts +35 -0
- package/dist/src/core/git.js +257 -0
- package/dist/src/core/ids.d.ts +16 -0
- package/dist/src/core/ids.js +41 -0
- package/dist/src/core/invariants.d.ts +24 -0
- package/dist/src/core/invariants.js +157 -0
- package/dist/src/core/locations.d.ts +30 -0
- package/dist/src/core/locations.js +66 -0
- package/dist/src/core/log-dir.d.ts +6 -0
- package/dist/src/core/log-dir.js +13 -0
- package/dist/src/core/migrate.d.ts +24 -0
- package/dist/src/core/migrate.js +53 -0
- package/dist/src/core/refresh.d.ts +30 -0
- package/dist/src/core/refresh.js +113 -0
- package/dist/src/core/request-guard.d.ts +32 -0
- package/dist/src/core/request-guard.js +72 -0
- package/dist/src/core/review-actions.d.ts +27 -0
- package/dist/src/core/review-actions.js +99 -0
- package/dist/src/gui-worker.d.ts +1 -0
- package/dist/src/gui-worker.js +401 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +658 -0
- package/dist/src/tools/add-findings.d.ts +9 -0
- package/dist/src/tools/add-findings.js +106 -0
- package/dist/src/tools/create-analysis.d.ts +4 -0
- package/dist/src/tools/create-analysis.js +36 -0
- package/dist/src/tools/delete-analysis.d.ts +4 -0
- package/dist/src/tools/delete-analysis.js +11 -0
- package/dist/src/tools/delete-diagram.d.ts +4 -0
- package/dist/src/tools/delete-diagram.js +22 -0
- package/dist/src/tools/delete-findings.d.ts +7 -0
- package/dist/src/tools/delete-findings.js +27 -0
- package/dist/src/tools/format.d.ts +27 -0
- package/dist/src/tools/format.js +92 -0
- package/dist/src/tools/get-analysis.d.ts +4 -0
- package/dist/src/tools/get-analysis.js +87 -0
- package/dist/src/tools/get-diff.d.ts +4 -0
- package/dist/src/tools/get-diff.js +67 -0
- package/dist/src/tools/get-review-feedback.d.ts +4 -0
- package/dist/src/tools/get-review-feedback.js +71 -0
- package/dist/src/tools/inputs.d.ts +14 -0
- package/dist/src/tools/inputs.js +40 -0
- package/dist/src/tools/list-analyses.d.ts +8 -0
- package/dist/src/tools/list-analyses.js +71 -0
- package/dist/src/tools/refresh-analysis.d.ts +7 -0
- package/dist/src/tools/refresh-analysis.js +39 -0
- package/dist/src/tools/set-diagram.d.ts +16 -0
- package/dist/src/tools/set-diagram.js +207 -0
- package/dist/src/tools/types.d.ts +52 -0
- package/dist/src/tools/types.js +133 -0
- package/dist/src/tools/update-analysis.d.ts +4 -0
- package/dist/src/tools/update-analysis.js +121 -0
- package/dist/src/tools/update-finding.d.ts +8 -0
- package/dist/src/tools/update-finding.js +91 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +2 -0
- package/gui/dist/assets/index-CU9pNYDp.js +72 -0
- package/gui/dist/assets/index-Du5eL78F.css +1 -0
- package/gui/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/gui/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/gui/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/gui/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/gui/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/gui/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/gui/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/gui/dist/index.html +13 -0
- package/package.json +56 -0
- package/scripts/generate-version.js +20 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { summarize } from "../core/analysis-store.js";
|
|
2
|
+
import { AnalysisError } from "../core/errors.js";
|
|
3
|
+
import { itemId } from "../core/ids.js";
|
|
4
|
+
import { resolveLocation } from "../core/locations.js";
|
|
5
|
+
import { FINDING_KINDS, SEVERITIES } from "../../shared/schemas/analysis.schema.js";
|
|
6
|
+
import { findingLine, severityCountsLabel } from "./format.js";
|
|
7
|
+
import { readItemId, readLocation } from "./inputs.js";
|
|
8
|
+
import { nullableText, optionalEnum, rejectUnknownFields, requireArray, requireEnum, requireObject, requireString, requireText, textResult, } from "./types.js";
|
|
9
|
+
export const FINDING_INPUT_FIELDS = [
|
|
10
|
+
"id",
|
|
11
|
+
"severity",
|
|
12
|
+
"kind",
|
|
13
|
+
"title",
|
|
14
|
+
"body",
|
|
15
|
+
"path",
|
|
16
|
+
"start_line",
|
|
17
|
+
"end_line",
|
|
18
|
+
"suggestion",
|
|
19
|
+
"prompt",
|
|
20
|
+
];
|
|
21
|
+
/**
|
|
22
|
+
* Ajoute un lot de constats. Tout le lot est validé (champs, identifiants,
|
|
23
|
+
* emplacements résolus sur le snapshot) avant l'écriture : un seul constat
|
|
24
|
+
* invalide et rien n'est ajouté.
|
|
25
|
+
*/
|
|
26
|
+
export function addFindings(store, args) {
|
|
27
|
+
rejectUnknownFields(args, ["analysis_id", "findings"]);
|
|
28
|
+
const id = requireString(args, "analysis_id");
|
|
29
|
+
const items = requireArray(args, "findings", 1);
|
|
30
|
+
const analysis = store.get(id);
|
|
31
|
+
const snapshot = store.getSnapshot(id);
|
|
32
|
+
const taken = new Set(analysis.findings.map((finding) => finding.id));
|
|
33
|
+
const batchIds = new Set();
|
|
34
|
+
const prepared = [];
|
|
35
|
+
const errors = [];
|
|
36
|
+
items.forEach((raw, index) => {
|
|
37
|
+
try {
|
|
38
|
+
const item = requireObject(raw, `findings[${index}]`);
|
|
39
|
+
rejectUnknownFields(item, FINDING_INPUT_FIELDS);
|
|
40
|
+
const findingId = readItemId(item, "id");
|
|
41
|
+
if (findingId !== undefined) {
|
|
42
|
+
if (taken.has(findingId))
|
|
43
|
+
throw new Error(`id "${findingId}" is already used by another finding of this analysis.`);
|
|
44
|
+
if (batchIds.has(findingId))
|
|
45
|
+
throw new Error(`id "${findingId}" appears twice in this batch.`);
|
|
46
|
+
batchIds.add(findingId);
|
|
47
|
+
}
|
|
48
|
+
const severity = requireEnum(item, "severity", SEVERITIES);
|
|
49
|
+
const kind = optionalEnum(item, "kind", FINDING_KINDS) ?? "issue";
|
|
50
|
+
const title = requireString(item, "title");
|
|
51
|
+
const body = requireText(item, "body");
|
|
52
|
+
const suggestion = nullableText(item, "suggestion", "", true) ?? null;
|
|
53
|
+
const prompt = nullableText(item, "prompt") ?? null;
|
|
54
|
+
const location = readLocation(item);
|
|
55
|
+
if (location === null)
|
|
56
|
+
throw new Error(`"path" cannot be null here: omit it for a finding without location.`);
|
|
57
|
+
if (location === undefined && kind === "issue") {
|
|
58
|
+
throw new Error(`an issue needs a location ("path" and "start_line"); only requirement_gap findings may omit it.`);
|
|
59
|
+
}
|
|
60
|
+
if (location === undefined && suggestion !== null) {
|
|
61
|
+
throw new Error(`"suggestion" replaces the targeted lines, so it needs a location ("path" and "start_line").`);
|
|
62
|
+
}
|
|
63
|
+
const resolved = location === undefined ? null : resolveLocation(snapshot, analysis.files, location);
|
|
64
|
+
prepared.push({
|
|
65
|
+
id: findingId,
|
|
66
|
+
severity,
|
|
67
|
+
kind,
|
|
68
|
+
title,
|
|
69
|
+
body,
|
|
70
|
+
location: resolved && { path: resolved.path, startLine: resolved.startLine, endLine: resolved.endLine },
|
|
71
|
+
anchorText: resolved?.anchorText ?? null,
|
|
72
|
+
suggestion,
|
|
73
|
+
prompt,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
errors.push(`- findings[${index}]: ${err.message}`);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
if (errors.length > 0) {
|
|
81
|
+
throw new AnalysisError(`No finding was added: ${errors.length} of ${items.length} finding(s) are invalid. Fix them and send the whole batch again.\n` +
|
|
82
|
+
errors.join("\n"));
|
|
83
|
+
}
|
|
84
|
+
const added = [];
|
|
85
|
+
const updated = store.mutate(id, "agent", (draft) => {
|
|
86
|
+
if (draft.snapshotAt !== analysis.snapshotAt) {
|
|
87
|
+
throw new AnalysisError(`The diff of analysis "${id}" was recomputed while the findings were validated. Send the batch again.`);
|
|
88
|
+
}
|
|
89
|
+
const ids = new Set([...draft.findings.map((finding) => finding.id), ...batchIds]);
|
|
90
|
+
const now = new Date().toISOString();
|
|
91
|
+
for (const finding of prepared) {
|
|
92
|
+
const newId = finding.id ?? itemId("finding", ids);
|
|
93
|
+
ids.add(newId);
|
|
94
|
+
const created = { ...finding, id: newId, status: "open", createdAt: now };
|
|
95
|
+
added.push(created);
|
|
96
|
+
draft.findings.push(created);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
const lines = [
|
|
100
|
+
`Added ${added.length} finding(s) to "${updated.title}" (${updated.id}):`,
|
|
101
|
+
...added.map((finding) => ` ${findingLine(finding)}`),
|
|
102
|
+
`Open findings: ${severityCountsLabel(summarize(updated).openFindings)}.`,
|
|
103
|
+
"Next: draw a diagram with set_diagram if a picture helps, then ask the user to review the analysis in the GUI.",
|
|
104
|
+
];
|
|
105
|
+
return { result: textResult(lines.join("\n")), analysisId: updated.id };
|
|
106
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AnalysisStore } from "../core/analysis-store.js";
|
|
2
|
+
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>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { computeSnapshot } from "../core/git.js";
|
|
2
|
+
import { ANALYSIS_MODES } from "../../shared/schemas/analysis.schema.js";
|
|
3
|
+
import { fileLine, fileTotals, snapshotLabel } from "./format.js";
|
|
4
|
+
import { optionalString, rejectUnknownFields, requireEnum, requireString, textResult, } from "./types.js";
|
|
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) {
|
|
8
|
+
rejectUnknownFields(args, FIELDS);
|
|
9
|
+
const repoPath = requireString(args, "repo_path");
|
|
10
|
+
const project = requireString(args, "project");
|
|
11
|
+
const title = requireString(args, "title");
|
|
12
|
+
const mode = requireEnum(args, "mode", ANALYSIS_MODES);
|
|
13
|
+
const base = optionalString(args, "base");
|
|
14
|
+
const head = optionalString(args, "head");
|
|
15
|
+
const requestText = optionalString(args, "request_text");
|
|
16
|
+
const requestSource = optionalString(args, "request_source");
|
|
17
|
+
if (requestSource !== undefined && requestText === undefined) {
|
|
18
|
+
throw new Error(`"request_source" describes the initial request: give "request_text" too.`);
|
|
19
|
+
}
|
|
20
|
+
const computed = await computeSnapshot({ repoPath, mode, base, head });
|
|
21
|
+
const analysis = store.create({ project, title, request: requestText === undefined ? null : { text: requestText, source: requestSource ?? null } }, computed);
|
|
22
|
+
const lines = [
|
|
23
|
+
`Created analysis "${analysis.title}" (id: ${analysis.id}) in project "${analysis.project}".`,
|
|
24
|
+
`Repository: ${analysis.repoPath}`,
|
|
25
|
+
`Diff: ${snapshotLabel(analysis)}`,
|
|
26
|
+
"",
|
|
27
|
+
];
|
|
28
|
+
if (analysis.files.length === 0) {
|
|
29
|
+
lines.push("Changed files: none — the diff is empty. Check the mode and refs; delete this analysis with delete_analysis and create it again with the right ones.");
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
lines.push(`Changed files (${fileTotals(analysis.files)}):`, ...analysis.files.map((file) => ` ${fileLine(file)}`));
|
|
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.");
|
|
35
|
+
return { result: textResult(lines.join("\n")), analysisId: analysis.id };
|
|
36
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { rejectUnknownFields, requireString, textResult } from "./types.js";
|
|
2
|
+
/** Supprime une analyse et son snapshot de diff. */
|
|
3
|
+
export function deleteAnalysis(store, args) {
|
|
4
|
+
rejectUnknownFields(args, ["analysis_id"]);
|
|
5
|
+
const id = requireString(args, "analysis_id");
|
|
6
|
+
store.delete(id);
|
|
7
|
+
return {
|
|
8
|
+
result: textResult(`Deleted analysis ${id} and its diff snapshot. The repository is untouched.`),
|
|
9
|
+
analysisId: id,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { AnalysisError } from "../core/errors.js";
|
|
2
|
+
import { rejectUnknownFields, requireString, textResult } from "./types.js";
|
|
3
|
+
/** Supprime un schéma d'une analyse. */
|
|
4
|
+
export function deleteDiagram(store, args) {
|
|
5
|
+
rejectUnknownFields(args, ["analysis_id", "diagram_id"]);
|
|
6
|
+
const id = requireString(args, "analysis_id");
|
|
7
|
+
const diagramId = requireString(args, "diagram_id");
|
|
8
|
+
let title = "";
|
|
9
|
+
const updated = store.mutate(id, "agent", (draft) => {
|
|
10
|
+
const diagram = draft.diagrams.find((entry) => entry.id === diagramId);
|
|
11
|
+
if (!diagram) {
|
|
12
|
+
const existing = draft.diagrams.map((entry) => entry.id).join(", ") || "none";
|
|
13
|
+
throw new AnalysisError(`Diagram "${diagramId}" not found in analysis "${id}". Existing diagrams: ${existing}.`);
|
|
14
|
+
}
|
|
15
|
+
title = diagram.title;
|
|
16
|
+
draft.diagrams = draft.diagrams.filter((entry) => entry.id !== diagramId);
|
|
17
|
+
});
|
|
18
|
+
return {
|
|
19
|
+
result: textResult(`Deleted diagram ${diagramId} ("${title}") from "${updated.title}" (${updated.id}). ${updated.diagrams.length} diagram(s) remain.`),
|
|
20
|
+
analysisId: updated.id,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AnalysisStore } from "../core/analysis-store.js";
|
|
2
|
+
import { type Args, type MutationResult } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Supprime des constats par identifiant et les retire de la sélection de la
|
|
5
|
+
* revue. Un identifiant inconnu fait échouer l'appel sans rien supprimer.
|
|
6
|
+
*/
|
|
7
|
+
export declare function deleteFindings(store: AnalysisStore, args: Args): MutationResult;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { AnalysisError } from "../core/errors.js";
|
|
2
|
+
import { rejectUnknownFields, requireString, requireStringArray, textResult } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Supprime des constats par identifiant et les retire de la sélection de la
|
|
5
|
+
* revue. Un identifiant inconnu fait échouer l'appel sans rien supprimer.
|
|
6
|
+
*/
|
|
7
|
+
export function deleteFindings(store, args) {
|
|
8
|
+
rejectUnknownFields(args, ["analysis_id", "finding_ids"]);
|
|
9
|
+
const id = requireString(args, "analysis_id");
|
|
10
|
+
const ids = [...new Set(requireStringArray(args, "finding_ids", 1))];
|
|
11
|
+
const updated = store.mutate(id, "agent", (draft) => {
|
|
12
|
+
const known = new Set(draft.findings.map((finding) => finding.id));
|
|
13
|
+
const unknown = ids.filter((findingId) => !known.has(findingId));
|
|
14
|
+
if (unknown.length > 0) {
|
|
15
|
+
const existing = draft.findings.map((finding) => finding.id).join(", ") || "none";
|
|
16
|
+
throw new AnalysisError(`No finding was deleted: unknown finding id(s) ${unknown.join(", ")} in analysis "${id}". Existing findings: ${existing}.`);
|
|
17
|
+
}
|
|
18
|
+
const removed = new Set(ids);
|
|
19
|
+
draft.findings = draft.findings.filter((finding) => !removed.has(finding.id));
|
|
20
|
+
draft.review.selectedFindingIds = draft.review.selectedFindingIds.filter((findingId) => !removed.has(findingId));
|
|
21
|
+
});
|
|
22
|
+
return {
|
|
23
|
+
result: textResult(`Deleted ${ids.length} finding(s) from "${updated.title}" (${updated.id}): ${ids.join(", ")}. ` +
|
|
24
|
+
`${updated.findings.length} finding(s) remain.`),
|
|
25
|
+
analysisId: updated.id,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type Analysis, type FileEntry, type Finding, type Note, type Overview, type ReviewProgress, type ReviewProgressState, type Severity } from "../../shared/schemas/analysis.schema.js";
|
|
2
|
+
/** Sha abrégé à 7 caractères. */
|
|
3
|
+
export declare function shortSha(sha: string | null): string;
|
|
4
|
+
/** Refs d'une analyse : `branch master...HEAD` ou `working_tree (changes against HEAD)`. */
|
|
5
|
+
export declare function refsLabel(analysis: Pick<Analysis, "mode" | "base" | "head">): string;
|
|
6
|
+
/** Refs et commits figés d'une analyse. */
|
|
7
|
+
export declare function snapshotLabel(analysis: Analysis): string;
|
|
8
|
+
/** Ligne d'un fichier modifié : statut, chemin (ancien chemin si renommé), +/-. */
|
|
9
|
+
export declare function fileLine(file: FileEntry): string;
|
|
10
|
+
/** Totaux +/- des fichiers. */
|
|
11
|
+
export declare function fileTotals(files: FileEntry[]): string;
|
|
12
|
+
/** `path:12` ou `path:12-14`, ou `no location`. */
|
|
13
|
+
export declare function locationLabel(finding: Pick<Finding, "location">): string;
|
|
14
|
+
/** `path`, `path:12`, ou `whole analysis` pour une remarque sans emplacement. */
|
|
15
|
+
export declare function noteLocationLabel(note: Pick<Note, "location">): string;
|
|
16
|
+
/** Ligne d'un constat : id, gravité, nature, statut, emplacement, titre. */
|
|
17
|
+
export declare function findingLine(finding: Finding): string;
|
|
18
|
+
/** `1 critical, 2 major` ; `none` quand tous les compteurs sont à zéro. */
|
|
19
|
+
export declare function severityCountsLabel(counts: Record<Severity, number>): string;
|
|
20
|
+
/** Préfixe chaque ligne non vide de `indent`. */
|
|
21
|
+
export declare function indentBlock(text: string, indent: string): string;
|
|
22
|
+
/** Nom anglais d'un état de revue : `not started`, `in progress`… */
|
|
23
|
+
export declare function progressStateLabel(state: ReviewProgressState): string;
|
|
24
|
+
/** `in progress (2/7 files reviewed)` ou `submitted, decision approve (7/7 files reviewed)`. */
|
|
25
|
+
export declare function progressLabel(progress: ReviewProgress): string;
|
|
26
|
+
/** Bloc « Overview » : objectif, approche et points d'attention ; une ligne d'invitation quand il n'y en a pas. */
|
|
27
|
+
export declare function overviewLines(overview: Overview | null): string[];
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { SEVERITIES, } from "../../shared/schemas/analysis.schema.js";
|
|
2
|
+
import { lineRange, noteLocationText } from "../../shared/text.js";
|
|
3
|
+
/** Lettre de statut d'un fichier, à la manière de `git status --short`. */
|
|
4
|
+
const FILE_STATUS_LETTERS = {
|
|
5
|
+
added: "A",
|
|
6
|
+
modified: "M",
|
|
7
|
+
deleted: "D",
|
|
8
|
+
renamed: "R",
|
|
9
|
+
};
|
|
10
|
+
/** Sha abrégé à 7 caractères. */
|
|
11
|
+
export function shortSha(sha) {
|
|
12
|
+
return sha === null ? "-" : sha.slice(0, 7);
|
|
13
|
+
}
|
|
14
|
+
/** Refs d'une analyse : `branch master...HEAD` ou `working_tree (changes against HEAD)`. */
|
|
15
|
+
export function refsLabel(analysis) {
|
|
16
|
+
if (analysis.mode === "branch")
|
|
17
|
+
return `branch ${analysis.base}...${analysis.head}`;
|
|
18
|
+
return "working_tree (uncommitted changes against HEAD)";
|
|
19
|
+
}
|
|
20
|
+
/** Refs et commits figés d'une analyse. */
|
|
21
|
+
export function snapshotLabel(analysis) {
|
|
22
|
+
const commits = analysis.mode === "branch"
|
|
23
|
+
? `base ${shortSha(analysis.baseCommit)}, head ${shortSha(analysis.headCommit)}`
|
|
24
|
+
: `HEAD ${shortSha(analysis.baseCommit)}`;
|
|
25
|
+
return `${refsLabel(analysis)} (${commits}, snapshot at ${analysis.snapshotAt})`;
|
|
26
|
+
}
|
|
27
|
+
/** Ligne d'un fichier modifié : statut, chemin (ancien chemin si renommé), +/-. */
|
|
28
|
+
export function fileLine(file) {
|
|
29
|
+
const name = file.oldPath === null ? file.path : `${file.oldPath} -> ${file.path}`;
|
|
30
|
+
const counts = file.binary ? "binary" : `+${file.additions} -${file.deletions}`;
|
|
31
|
+
const extra = !file.binary && !file.contentAvailable && file.status !== "deleted" ? ", content too large to anchor findings" : "";
|
|
32
|
+
return `${FILE_STATUS_LETTERS[file.status]} ${name} ${counts}${extra}`;
|
|
33
|
+
}
|
|
34
|
+
/** Totaux +/- des fichiers. */
|
|
35
|
+
export function fileTotals(files) {
|
|
36
|
+
const additions = files.reduce((sum, file) => sum + file.additions, 0);
|
|
37
|
+
const deletions = files.reduce((sum, file) => sum + file.deletions, 0);
|
|
38
|
+
return `${files.length} file(s), +${additions} -${deletions}`;
|
|
39
|
+
}
|
|
40
|
+
/** `path:12` ou `path:12-14`, ou `no location`. */
|
|
41
|
+
export function locationLabel(finding) {
|
|
42
|
+
const location = finding.location;
|
|
43
|
+
if (location === null)
|
|
44
|
+
return "no location";
|
|
45
|
+
return `${location.path}:${lineRange(location.startLine, location.endLine)}`;
|
|
46
|
+
}
|
|
47
|
+
/** `path`, `path:12`, ou `whole analysis` pour une remarque sans emplacement. */
|
|
48
|
+
export function noteLocationLabel(note) {
|
|
49
|
+
return note.location ? noteLocationText(note.location) : "whole analysis";
|
|
50
|
+
}
|
|
51
|
+
/** Ligne d'un constat : id, gravité, nature, statut, emplacement, titre. */
|
|
52
|
+
export function findingLine(finding) {
|
|
53
|
+
return `${finding.id} [${finding.severity}] ${finding.kind}, ${finding.status} — ${locationLabel(finding)} — ${finding.title}`;
|
|
54
|
+
}
|
|
55
|
+
/** `1 critical, 2 major` ; `none` quand tous les compteurs sont à zéro. */
|
|
56
|
+
export function severityCountsLabel(counts) {
|
|
57
|
+
const parts = SEVERITIES.filter((severity) => counts[severity] > 0).map((severity) => `${counts[severity]} ${severity}`);
|
|
58
|
+
return parts.length === 0 ? "none" : parts.join(", ");
|
|
59
|
+
}
|
|
60
|
+
/** Préfixe chaque ligne non vide de `indent`. */
|
|
61
|
+
export function indentBlock(text, indent) {
|
|
62
|
+
return text
|
|
63
|
+
.split("\n")
|
|
64
|
+
.map((line) => (line === "" ? "" : `${indent}${line}`))
|
|
65
|
+
.join("\n");
|
|
66
|
+
}
|
|
67
|
+
const REVIEW_PROGRESS_TEXT = {
|
|
68
|
+
not_started: "not started",
|
|
69
|
+
in_progress: "in progress",
|
|
70
|
+
files_reviewed: "all files reviewed, not submitted",
|
|
71
|
+
submitted: "submitted",
|
|
72
|
+
};
|
|
73
|
+
/** Nom anglais d'un état de revue : `not started`, `in progress`… */
|
|
74
|
+
export function progressStateLabel(state) {
|
|
75
|
+
return REVIEW_PROGRESS_TEXT[state];
|
|
76
|
+
}
|
|
77
|
+
/** `in progress (2/7 files reviewed)` ou `submitted, decision approve (7/7 files reviewed)`. */
|
|
78
|
+
export function progressLabel(progress) {
|
|
79
|
+
const decision = progress.state === "submitted" ? `, decision ${progress.decision}` : "";
|
|
80
|
+
return `${REVIEW_PROGRESS_TEXT[progress.state]}${decision} (${progress.reviewedFiles}/${progress.totalFiles} files reviewed)`;
|
|
81
|
+
}
|
|
82
|
+
/** Bloc « Overview » : objectif, approche et points d'attention ; une ligne d'invitation quand il n'y en a pas. */
|
|
83
|
+
export function overviewLines(overview) {
|
|
84
|
+
if (overview === null)
|
|
85
|
+
return ["Overview: none yet (write objective and approach with update_analysis)."];
|
|
86
|
+
const lines = ["Overview:", ` Objective: ${indentBlock(overview.objective, " ").trimStart()}`, ` Approach: ${indentBlock(overview.approach, " ").trimStart()}`];
|
|
87
|
+
if (overview.attentionPoints.length === 0)
|
|
88
|
+
lines.push(" Attention points: none");
|
|
89
|
+
else
|
|
90
|
+
lines.push(" Attention points:", ...overview.attentionPoints.map((point) => ` - ${point}`));
|
|
91
|
+
return lines;
|
|
92
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { summarize } from "../core/analysis-store.js";
|
|
2
|
+
import { diagramQuality } from "../../shared/diagram-quality.js";
|
|
3
|
+
import { reviewProgress } from "../../shared/review-state.js";
|
|
4
|
+
import { sortBySeverity } from "../../shared/severity.js";
|
|
5
|
+
import { fileLine, fileTotals, findingLine, noteLocationLabel, overviewLines, progressLabel, severityCountsLabel, snapshotLabel, } from "./format.js";
|
|
6
|
+
import { rejectUnknownFields, requireString, textResult } from "./types.js";
|
|
7
|
+
function noteLine(note) {
|
|
8
|
+
return `${note.id} — ${noteLocationLabel(note)} — ${note.text.replace(/\n/g, " / ")}`;
|
|
9
|
+
}
|
|
10
|
+
function reviewLines(analysis) {
|
|
11
|
+
const review = analysis.review;
|
|
12
|
+
const progress = progressLabel(reviewProgress(analysis));
|
|
13
|
+
if (review.state === "submitted") {
|
|
14
|
+
return [
|
|
15
|
+
`Review: ${progress}, submitted at ${review.submittedAt}, ` +
|
|
16
|
+
`${review.selectedFindingIds.length} finding(s) and ${review.selectedNoteIds.length} note(s) selected. ` +
|
|
17
|
+
`Read it with get_review_feedback.`,
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
return [`Review: ${progress} — the reviewer has not submitted feedback yet.`];
|
|
21
|
+
}
|
|
22
|
+
/** Ligne d'un schéma, avec un avertissement quand son tracé a des croisements ou ne peut pas être calculé. */
|
|
23
|
+
function diagramLine(diagram) {
|
|
24
|
+
const head = ` ${diagram.id} — "${diagram.title}" (${diagram.kind}, ${diagram.nodes.length} node(s), ${diagram.links.length} link(s))`;
|
|
25
|
+
let quality;
|
|
26
|
+
try {
|
|
27
|
+
quality = diagramQuality(diagram);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
return `${head} — WARNING: cannot be laid out (${err.message}). Fix it with set_diagram.`;
|
|
31
|
+
}
|
|
32
|
+
const problems = [];
|
|
33
|
+
if (quality.crossings.length > 0)
|
|
34
|
+
problems.push(`${quality.crossings.length} link crossing(s)`);
|
|
35
|
+
if (quality.nodeOverlaps.length > 0)
|
|
36
|
+
problems.push(`${quality.nodeOverlaps.length} link(s) through a node`);
|
|
37
|
+
if (problems.length === 0)
|
|
38
|
+
return head;
|
|
39
|
+
return `${head} — WARNING: ${problems.join(", ")}. Send it again with set_diagram to see the details and fix it.`;
|
|
40
|
+
}
|
|
41
|
+
/** Vue complète et lisible d'une analyse. */
|
|
42
|
+
export function getAnalysis(store, args) {
|
|
43
|
+
rejectUnknownFields(args, ["analysis_id"]);
|
|
44
|
+
const analysis = store.get(requireString(args, "analysis_id"));
|
|
45
|
+
const lines = [
|
|
46
|
+
`# ${analysis.title} (id: ${analysis.id})`,
|
|
47
|
+
`Project: ${analysis.project}`,
|
|
48
|
+
`Repository: ${analysis.repoPath}`,
|
|
49
|
+
`Diff: ${snapshotLabel(analysis)}`,
|
|
50
|
+
`Revision ${analysis.revision}, last edited by ${analysis.lastEditor} at ${analysis.updatedAt}`,
|
|
51
|
+
"",
|
|
52
|
+
];
|
|
53
|
+
if (analysis.request) {
|
|
54
|
+
lines.push(`Initial request${analysis.request.source ? ` (${analysis.request.source})` : ""}:`, analysis.request.text, "");
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
lines.push("Initial request: none recorded (set it with update_analysis).", "");
|
|
58
|
+
}
|
|
59
|
+
lines.push(...overviewLines(analysis.overview), "");
|
|
60
|
+
if (analysis.summary.length > 0) {
|
|
61
|
+
lines.push("Summary:", ...analysis.summary.map((bullet) => `- ${bullet}`), "");
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
lines.push("Summary: none yet (write it with update_analysis).", "");
|
|
65
|
+
}
|
|
66
|
+
const reviewed = analysis.files.filter((file) => file.reviewed).length;
|
|
67
|
+
lines.push(`Files (${fileTotals(analysis.files)}; reviewed ${reviewed}/${analysis.files.length}):`);
|
|
68
|
+
lines.push(...analysis.files.map((file) => ` [${file.reviewed ? "x" : " "}] ${fileLine(file)}`), "");
|
|
69
|
+
const counts = { open: 0, ignored: 0, outdated: 0 };
|
|
70
|
+
for (const finding of analysis.findings)
|
|
71
|
+
counts[finding.status]++;
|
|
72
|
+
lines.push(`Findings (${analysis.findings.length}: ${counts.open} open, ${counts.ignored} ignored, ${counts.outdated} outdated; ` +
|
|
73
|
+
`open by severity: ${severityCountsLabel(summarize(analysis).openFindings)}):`);
|
|
74
|
+
if (analysis.findings.length === 0)
|
|
75
|
+
lines.push(" none (record them with add_findings)");
|
|
76
|
+
lines.push(...sortBySeverity(analysis.findings).map((finding) => ` ${findingLine(finding)}`), "");
|
|
77
|
+
lines.push(`Reviewer notes (${analysis.notes.length}):`);
|
|
78
|
+
if (analysis.notes.length === 0)
|
|
79
|
+
lines.push(" none");
|
|
80
|
+
lines.push(...analysis.notes.map((note) => ` ${noteLine(note)}`), "");
|
|
81
|
+
lines.push(`Diagrams (${analysis.diagrams.length}):`);
|
|
82
|
+
if (analysis.diagrams.length === 0)
|
|
83
|
+
lines.push(" none (draw one with set_diagram when a picture helps)");
|
|
84
|
+
lines.push(...analysis.diagrams.map(diagramLine), "");
|
|
85
|
+
lines.push(...reviewLines(analysis));
|
|
86
|
+
return textResult(lines.join("\n"));
|
|
87
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AnalysisStore } from "../core/analysis-store.js";
|
|
2
|
+
import { type Args, type ToolResult } from "./types.js";
|
|
3
|
+
/** Diff figé d'un fichier ou de tous les fichiers de l'analyse, avec numéros de ligne ancien et nouveau. */
|
|
4
|
+
export declare function getDiff(store: AnalysisStore, args: Args): ToolResult;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { AnalysisError } from "../core/errors.js";
|
|
2
|
+
import { fileLine, snapshotLabel } from "./format.js";
|
|
3
|
+
import { optionalString, rejectUnknownFields, requireString, textResult } from "./types.js";
|
|
4
|
+
const MARKERS = { context: " ", add: "+", del: "-" };
|
|
5
|
+
/** Lignes d'un fichier : en-tête, marqueurs binaire / contenu indisponible, puis hunks numérotés `ancien | nouveau`. */
|
|
6
|
+
function renderFile(entry, diff) {
|
|
7
|
+
const out = [`=== ${fileLine(entry)}${entry.reviewed ? " (reviewed)" : ""}`];
|
|
8
|
+
if (entry.binary) {
|
|
9
|
+
out.push("[binary file: no line-level diff, findings cannot be anchored on its lines]");
|
|
10
|
+
return out;
|
|
11
|
+
}
|
|
12
|
+
if (entry.status === "deleted") {
|
|
13
|
+
out.push("[deleted file: no new side, findings cannot be anchored on its lines]");
|
|
14
|
+
}
|
|
15
|
+
else if (!entry.contentAvailable) {
|
|
16
|
+
out.push("[new-side content not kept (file too large): findings cannot be anchored on its lines]");
|
|
17
|
+
}
|
|
18
|
+
if (diff.hunks.length === 0) {
|
|
19
|
+
out.push(entry.status === "renamed" ? "[renamed without content changes]" : "[no textual changes]");
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
let width = 1;
|
|
23
|
+
for (const hunk of diff.hunks) {
|
|
24
|
+
for (const line of hunk.lines) {
|
|
25
|
+
width = Math.max(width, String(line.oldNo ?? "").length, String(line.newNo ?? "").length);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const column = (value) => String(value ?? "").padStart(width);
|
|
29
|
+
for (const hunk of diff.hunks) {
|
|
30
|
+
out.push(hunk.header);
|
|
31
|
+
for (const line of hunk.lines) {
|
|
32
|
+
out.push(`${column(line.oldNo)} | ${column(line.newNo)} ${MARKERS[line.type]} ${line.text}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
/** Diff figé d'un fichier ou de tous les fichiers de l'analyse, avec numéros de ligne ancien et nouveau. */
|
|
38
|
+
export function getDiff(store, args) {
|
|
39
|
+
rejectUnknownFields(args, ["analysis_id", "path"]);
|
|
40
|
+
const analysis = store.get(requireString(args, "analysis_id"));
|
|
41
|
+
const path = optionalString(args, "path");
|
|
42
|
+
const snapshot = store.getSnapshot(analysis.id);
|
|
43
|
+
let entries = analysis.files;
|
|
44
|
+
if (path !== undefined) {
|
|
45
|
+
const entry = analysis.files.find((file) => file.path === path);
|
|
46
|
+
if (!entry) {
|
|
47
|
+
const known = analysis.files.map((file) => file.path).join(", ") || "none";
|
|
48
|
+
throw new AnalysisError(`File "${path}" is not part of analysis "${analysis.id}". Changed files: ${known}.`);
|
|
49
|
+
}
|
|
50
|
+
entries = [entry];
|
|
51
|
+
}
|
|
52
|
+
const lines = [
|
|
53
|
+
`Frozen diff of "${analysis.title}" (${analysis.id}): ${snapshotLabel(analysis)}.`,
|
|
54
|
+
"Columns: old line | new line, then + (added), - (removed) or a space (context). " +
|
|
55
|
+
"Anchor findings on the NEW-side numbers (second column).",
|
|
56
|
+
];
|
|
57
|
+
if (entries.length === 0)
|
|
58
|
+
lines.push("", "The analysis has no changed files.");
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
const diff = snapshot.files.find((file) => file.path === entry.path);
|
|
61
|
+
if (!diff) {
|
|
62
|
+
throw new AnalysisError(`The diff snapshot of analysis "${analysis.id}" has no entry for "${entry.path}". Recompute it with refresh_analysis.`);
|
|
63
|
+
}
|
|
64
|
+
lines.push("", ...renderFile(entry, diff));
|
|
65
|
+
}
|
|
66
|
+
return textResult(lines.join("\n"));
|
|
67
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AnalysisStore } from "../core/analysis-store.js";
|
|
2
|
+
import { type Args, type ToolResult } from "./types.js";
|
|
3
|
+
/** Retour du relecteur : décision, points retenus et prompt ; sinon l'avancement de la revue. Lecture seule. */
|
|
4
|
+
export declare function getReviewFeedback(store: AnalysisStore, args: Args): ToolResult;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { reviewProgress } from "../../shared/review-state.js";
|
|
2
|
+
import { sortBySeverity } from "../../shared/severity.js";
|
|
3
|
+
import { findingLine, indentBlock, noteLocationLabel, overviewLines, progressLabel } from "./format.js";
|
|
4
|
+
import { rejectUnknownFields, requireString, textResult } from "./types.js";
|
|
5
|
+
function findingDetails(finding) {
|
|
6
|
+
const lines = [`- ${findingLine(finding)}`];
|
|
7
|
+
if (finding.body.trim() !== "")
|
|
8
|
+
lines.push(indentBlock(finding.body, " "));
|
|
9
|
+
if (finding.suggestion !== null)
|
|
10
|
+
lines.push(" Proposed replacement:", indentBlock(finding.suggestion, " "));
|
|
11
|
+
return lines;
|
|
12
|
+
}
|
|
13
|
+
function noteDetails(note) {
|
|
14
|
+
return `- ${note.id} — ${noteLocationLabel(note)}\n${indentBlock(note.text, " ")}`;
|
|
15
|
+
}
|
|
16
|
+
/** Avancement d'une revue non soumise. */
|
|
17
|
+
function progressLines(analysis) {
|
|
18
|
+
const progress = reviewProgress(analysis);
|
|
19
|
+
const ignored = analysis.findings.filter((finding) => finding.status === "ignored").length;
|
|
20
|
+
return [
|
|
21
|
+
`The review of "${analysis.title}" (${analysis.id}, project "${analysis.project}") is not submitted yet.`,
|
|
22
|
+
`Review state: ${progressLabel(progress)}.`,
|
|
23
|
+
`Progress: ${progress.reviewedFiles}/${progress.totalFiles} file(s) reviewed, ${ignored}/${analysis.findings.length} finding(s) ignored, ` +
|
|
24
|
+
`${analysis.notes.length} reviewer note(s).`,
|
|
25
|
+
"",
|
|
26
|
+
...overviewLines(analysis.overview),
|
|
27
|
+
"",
|
|
28
|
+
"Ask the user to finish the review in the GUI and save the feedback, then call get_review_feedback again.",
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
/** Retour du relecteur : décision, points retenus et prompt ; sinon l'avancement de la revue. Lecture seule. */
|
|
32
|
+
export function getReviewFeedback(store, args) {
|
|
33
|
+
rejectUnknownFields(args, ["analysis_id"]);
|
|
34
|
+
const analysis = store.get(requireString(args, "analysis_id"));
|
|
35
|
+
const review = analysis.review;
|
|
36
|
+
if (review.state !== "submitted")
|
|
37
|
+
return textResult(progressLines(analysis).join("\n"));
|
|
38
|
+
const selectedFindings = new Set(review.selectedFindingIds);
|
|
39
|
+
const selectedNotes = new Set(review.selectedNoteIds);
|
|
40
|
+
const findings = sortBySeverity(analysis.findings.filter((finding) => selectedFindings.has(finding.id)));
|
|
41
|
+
const notes = analysis.notes.filter((note) => selectedNotes.has(note.id));
|
|
42
|
+
const lines = [
|
|
43
|
+
`Review of "${analysis.title}" (${analysis.id}, project "${analysis.project}") submitted at ${review.submittedAt}.`,
|
|
44
|
+
`Decision: ${review.decision}`,
|
|
45
|
+
"",
|
|
46
|
+
...overviewLines(analysis.overview),
|
|
47
|
+
"",
|
|
48
|
+
`Selected findings (${findings.length}):`,
|
|
49
|
+
];
|
|
50
|
+
lines.push(...(findings.length === 0 ? [" none"] : findings.flatMap(findingDetails)), "");
|
|
51
|
+
lines.push(`Selected reviewer notes (${notes.length}):`);
|
|
52
|
+
lines.push(...(notes.length === 0 ? [" none"] : notes.map(noteDetails)), "");
|
|
53
|
+
if (review.prompt === null) {
|
|
54
|
+
lines.push("Prompt: none was recorded with this submission.");
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
lines.push("Prompt from the reviewer (verbatim):", "-----", review.prompt, "-----");
|
|
58
|
+
}
|
|
59
|
+
lines.push("");
|
|
60
|
+
if (review.decision === "reject") {
|
|
61
|
+
lines.push("Next steps: the reviewer rejected the feature. Ask the user how to proceed before changing the code.");
|
|
62
|
+
}
|
|
63
|
+
else if (review.decision === "approve" && findings.length === 0 && notes.length === 0) {
|
|
64
|
+
lines.push("Next steps: the reviewer approved the feature with no points to address.");
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
lines.push("Next steps: address the points above in the repository, then call refresh_analysis to recompute the diff " +
|
|
68
|
+
"(it discards this feedback and starts a new review round).");
|
|
69
|
+
}
|
|
70
|
+
return textResult(lines.join("\n"));
|
|
71
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Args } from "./types.js";
|
|
2
|
+
/** Emplacement demandé par l'agent : `null` pour retirer l'emplacement, `undefined` s'il n'en donne pas. */
|
|
3
|
+
export type LocationInput = {
|
|
4
|
+
path: string;
|
|
5
|
+
startLine: number;
|
|
6
|
+
endLine: number | undefined;
|
|
7
|
+
} | null | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* Lit `path`, `start_line` et `end_line`. `path` et `start_line` vont ensemble ;
|
|
10
|
+
* `end_line` vaut `start_line` quand il est omis. `path: null` retire l'emplacement.
|
|
11
|
+
*/
|
|
12
|
+
export declare function readLocation(args: Args, prefix?: string): LocationInput;
|
|
13
|
+
/** Lit un identifiant d'élément facultatif fourni par l'agent. */
|
|
14
|
+
export declare function readItemId(args: Args, field: string, prefix?: string): string | undefined;
|