@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,30 @@
|
|
|
1
|
+
import { type DiagramLayout } from "./diagram-layout.js";
|
|
2
|
+
import type { Diagram } from "./schemas/analysis.schema.js";
|
|
3
|
+
/** Lien désigné par ses extrémités et sa position dans `diagram.links`. */
|
|
4
|
+
export interface LinkRef {
|
|
5
|
+
index: number;
|
|
6
|
+
from: string;
|
|
7
|
+
to: string;
|
|
8
|
+
}
|
|
9
|
+
export interface LinkCrossing {
|
|
10
|
+
a: LinkRef;
|
|
11
|
+
b: LinkRef;
|
|
12
|
+
}
|
|
13
|
+
export interface NodeOverlap {
|
|
14
|
+
link: LinkRef;
|
|
15
|
+
/** Identifiant du nœud traversé. */
|
|
16
|
+
node: string;
|
|
17
|
+
}
|
|
18
|
+
export interface DiagramQuality {
|
|
19
|
+
crossings: LinkCrossing[];
|
|
20
|
+
nodeOverlaps: NodeOverlap[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Croisements et traversées de nœuds du tracé de `diagram`. Deux liens qui
|
|
24
|
+
* partent du même nœud ou y arrivent ne se croisent pas en s'y rejoignant ;
|
|
25
|
+
* seul un croisement franc entre eux compte. `layout` évite de recalculer un
|
|
26
|
+
* placement déjà connu ; il doit provenir de `layoutDiagram(diagram)`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function diagramQuality(diagram: Diagram, layout?: DiagramLayout): DiagramQuality;
|
|
29
|
+
/** Vrai quand le schéma ne présente ni croisement ni traversée de nœud. */
|
|
30
|
+
export declare function isClean(quality: DiagramQuality): boolean;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lisibilité d'un schéma placé : paires de liens qui se croisent et liens qui
|
|
3
|
+
* traversent un nœud qu'ils ne relient pas, mesurées sur le tracé de
|
|
4
|
+
* `layoutDiagram`. Partagé par les outils (`set_diagram`, `get_analysis`) et la GUI.
|
|
5
|
+
*/
|
|
6
|
+
import { routeIssues } from "./diagram-geometry.js";
|
|
7
|
+
import { layoutDiagram } from "./diagram-layout.js";
|
|
8
|
+
/**
|
|
9
|
+
* Croisements et traversées de nœuds du tracé de `diagram`. Deux liens qui
|
|
10
|
+
* partent du même nœud ou y arrivent ne se croisent pas en s'y rejoignant ;
|
|
11
|
+
* seul un croisement franc entre eux compte. `layout` évite de recalculer un
|
|
12
|
+
* placement déjà connu ; il doit provenir de `layoutDiagram(diagram)`.
|
|
13
|
+
*/
|
|
14
|
+
export function diagramQuality(diagram, layout = layoutDiagram(diagram)) {
|
|
15
|
+
const index = new Map(diagram.nodes.map((node, position) => [node.id, position]));
|
|
16
|
+
const refs = diagram.links.map((link, position) => ({ index: position, from: link.from, to: link.to }));
|
|
17
|
+
const issues = routeIssues(layout.nodes, layout.links.map((link) => ({ u: index.get(link.from), v: index.get(link.to), points: link.points })));
|
|
18
|
+
return {
|
|
19
|
+
crossings: issues.crossings.map(([a, b]) => ({ a: refs[a], b: refs[b] })),
|
|
20
|
+
nodeOverlaps: issues.overlaps.map(({ link, node }) => ({ link: refs[link], node: diagram.nodes[node].id })),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** Vrai quand le schéma ne présente ni croisement ni traversée de nœud. */
|
|
24
|
+
export function isClean(quality) {
|
|
25
|
+
return quality.crossings.length === 0 && quality.nodeOverlaps.length === 0;
|
|
26
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { AnalysisMode, FileStatus, FindingKind, FindingStatus, NodeStatus, ReviewDecision, ReviewProgressState, Severity } from "./schemas/analysis.schema.js";
|
|
2
|
+
/** Libellé et couleurs d'une pastille : texte `color` sur fond `background`. */
|
|
3
|
+
export interface BadgeStyle {
|
|
4
|
+
label: string;
|
|
5
|
+
color: string;
|
|
6
|
+
background: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const SEVERITY_STYLES: Record<Severity, BadgeStyle>;
|
|
9
|
+
/** Pastille des remarques du relecteur. */
|
|
10
|
+
export declare const NOTE_STYLE: BadgeStyle;
|
|
11
|
+
export declare const FINDING_STATUS_LABELS: Record<FindingStatus, string>;
|
|
12
|
+
export declare const FINDING_KIND_LABELS: Record<FindingKind, string>;
|
|
13
|
+
export declare const FILE_STATUS_LABELS: Record<FileStatus, string>;
|
|
14
|
+
export declare const MODE_LABELS: Record<AnalysisMode, string>;
|
|
15
|
+
export declare const DECISION_LABELS: Record<ReviewDecision, string>;
|
|
16
|
+
/** Pastille de l'état d'avancement d'une revue. */
|
|
17
|
+
export declare const REVIEW_PROGRESS_STYLES: Record<ReviewProgressState, BadgeStyle>;
|
|
18
|
+
/** Pastille de la décision d'une revue soumise, affichée à côté de « Soumise ». */
|
|
19
|
+
export declare const DECISION_STYLES: Record<ReviewDecision, BadgeStyle>;
|
|
20
|
+
/**
|
|
21
|
+
* Apparence d'un nœud de schéma : fond (`fill` null : contour seul), contour,
|
|
22
|
+
* couleur du libellé et couleur de l'icône.
|
|
23
|
+
*/
|
|
24
|
+
export interface NodeStyle {
|
|
25
|
+
label: string;
|
|
26
|
+
fill: string | null;
|
|
27
|
+
stroke: string;
|
|
28
|
+
text: string;
|
|
29
|
+
accent: string;
|
|
30
|
+
}
|
|
31
|
+
export declare const NODE_STATUS_STYLES: Record<NodeStatus, NodeStyle>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export const SEVERITY_STYLES = {
|
|
2
|
+
critical: { label: "Critique", color: "#f0625a", background: "#34191c" },
|
|
3
|
+
major: { label: "Majeur", color: "#e3a33b", background: "#33270f" },
|
|
4
|
+
minor: { label: "Mineur", color: "#6fb3ff", background: "#13243a" },
|
|
5
|
+
trivial: { label: "Détail", color: "#9aa1b1", background: "#1c2029" },
|
|
6
|
+
};
|
|
7
|
+
/** Pastille des remarques du relecteur. */
|
|
8
|
+
export const NOTE_STYLE = { label: "Remarque", color: "#c4a8ff", background: "#241d3a" };
|
|
9
|
+
export const FINDING_STATUS_LABELS = {
|
|
10
|
+
open: "Ouvert",
|
|
11
|
+
ignored: "Ignoré",
|
|
12
|
+
outdated: "Obsolète",
|
|
13
|
+
};
|
|
14
|
+
export const FINDING_KIND_LABELS = {
|
|
15
|
+
issue: "Problème",
|
|
16
|
+
requirement_gap: "Exigence manquante",
|
|
17
|
+
};
|
|
18
|
+
export const FILE_STATUS_LABELS = {
|
|
19
|
+
added: "Ajouté",
|
|
20
|
+
modified: "Modifié",
|
|
21
|
+
deleted: "Supprimé",
|
|
22
|
+
renamed: "Renommé",
|
|
23
|
+
};
|
|
24
|
+
export const MODE_LABELS = {
|
|
25
|
+
branch: "Branche",
|
|
26
|
+
working_tree: "Copie de travail",
|
|
27
|
+
};
|
|
28
|
+
export const DECISION_LABELS = {
|
|
29
|
+
approve: "Approuver",
|
|
30
|
+
request_changes: "Demander des corrections",
|
|
31
|
+
reject: "Rejeter",
|
|
32
|
+
};
|
|
33
|
+
/** Pastille de l'état d'avancement d'une revue. */
|
|
34
|
+
export const REVIEW_PROGRESS_STYLES = {
|
|
35
|
+
not_started: { label: "Non commencée", color: "#9aa1b1", background: "#1c2029" },
|
|
36
|
+
in_progress: { label: "En cours", color: "#6fb3ff", background: "#13243a" },
|
|
37
|
+
files_reviewed: { label: "Fichiers revus", color: "#8b97ff", background: "#1b1f3d" },
|
|
38
|
+
submitted: { label: "Soumise", color: "#3fb950", background: "#11201a" },
|
|
39
|
+
};
|
|
40
|
+
/** Pastille de la décision d'une revue soumise, affichée à côté de « Soumise ». */
|
|
41
|
+
export const DECISION_STYLES = {
|
|
42
|
+
approve: { label: "Approuvée", color: "#3fb950", background: "#11201a" },
|
|
43
|
+
request_changes: { label: "Corrections demandées", color: "#e3a33b", background: "#33270f" },
|
|
44
|
+
reject: { label: "Rejetée", color: "#f0625a", background: "#34191c" },
|
|
45
|
+
};
|
|
46
|
+
export const NODE_STATUS_STYLES = {
|
|
47
|
+
new: { label: "Nouveau", fill: "#11201a", stroke: "#2c6b3a", text: "#bfeccd", accent: "#3fb950" },
|
|
48
|
+
modified: { label: "Modifié", fill: "#151a2b", stroke: "#36407a", text: "#d5daff", accent: "#8b97ff" },
|
|
49
|
+
impacted: { label: "Impacté, non modifié", fill: "#13161f", stroke: "#2a2f3d", text: "#7b8396", accent: "#646b7b" },
|
|
50
|
+
existing: { label: "Existant", fill: "#13161f", stroke: "#2a2f3d", text: "#9aa1b1", accent: "#646b7b" },
|
|
51
|
+
finding: { label: "Constat", fill: "#221416", stroke: "#6b2a2d", text: "#f0a09a", accent: "#f0625a" },
|
|
52
|
+
missing: { label: "Attendu, absent", fill: null, stroke: "#6b2a2d", text: "#f0776f", accent: "#b0605b" },
|
|
53
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Règle des cartes mentales : les liens forment un arbre. Une seule racine
|
|
3
|
+
* (nœud sans lien entrant), un seul parent pour chaque autre nœud, aucun
|
|
4
|
+
* cycle, tous les nœuds atteignables depuis la racine.
|
|
5
|
+
*/
|
|
6
|
+
import type { Diagram } from "./schemas/analysis.schema.js";
|
|
7
|
+
export type MindmapProblem = {
|
|
8
|
+
kind: "self_loop";
|
|
9
|
+
node: string;
|
|
10
|
+
} | {
|
|
11
|
+
kind: "no_root";
|
|
12
|
+
} | {
|
|
13
|
+
kind: "several_roots";
|
|
14
|
+
roots: string[];
|
|
15
|
+
} | {
|
|
16
|
+
kind: "several_parents";
|
|
17
|
+
node: string;
|
|
18
|
+
parents: string[];
|
|
19
|
+
} | {
|
|
20
|
+
kind: "cycle";
|
|
21
|
+
nodes: string[];
|
|
22
|
+
} | {
|
|
23
|
+
kind: "unreachable";
|
|
24
|
+
root: string;
|
|
25
|
+
nodes: string[];
|
|
26
|
+
};
|
|
27
|
+
/** Problèmes qui empêchent `diagram` d'être une carte mentale ; liste vide pour un arbre. */
|
|
28
|
+
export declare function mindmapProblems(diagram: Pick<Diagram, "nodes" | "links">): MindmapProblem[];
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** Problèmes qui empêchent `diagram` d'être une carte mentale ; liste vide pour un arbre. */
|
|
2
|
+
export function mindmapProblems(diagram) {
|
|
3
|
+
const ids = diagram.nodes.map((node) => node.id);
|
|
4
|
+
const known = new Set(ids);
|
|
5
|
+
const parents = new Map(ids.map((id) => [id, []]));
|
|
6
|
+
const children = new Map(ids.map((id) => [id, []]));
|
|
7
|
+
const problems = [];
|
|
8
|
+
for (const link of diagram.links) {
|
|
9
|
+
if (!known.has(link.from) || !known.has(link.to))
|
|
10
|
+
continue;
|
|
11
|
+
if (link.from === link.to) {
|
|
12
|
+
problems.push({ kind: "self_loop", node: link.from });
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
parents.get(link.to).push(link.from);
|
|
16
|
+
children.get(link.from).push(link.to);
|
|
17
|
+
}
|
|
18
|
+
const roots = ids.filter((id) => parents.get(id).length === 0);
|
|
19
|
+
if (roots.length === 0 && ids.length > 0)
|
|
20
|
+
problems.push({ kind: "no_root" });
|
|
21
|
+
if (roots.length > 1)
|
|
22
|
+
problems.push({ kind: "several_roots", roots });
|
|
23
|
+
for (const id of ids) {
|
|
24
|
+
const list = parents.get(id);
|
|
25
|
+
if (list.length > 1)
|
|
26
|
+
problems.push({ kind: "several_parents", node: id, parents: list });
|
|
27
|
+
}
|
|
28
|
+
// Cycles : parcours en profondeur, un cycle relevé par lien de retour.
|
|
29
|
+
const state = new Map(ids.map((id) => [id, 0]));
|
|
30
|
+
const path = [];
|
|
31
|
+
const visit = (id) => {
|
|
32
|
+
state.set(id, 1);
|
|
33
|
+
path.push(id);
|
|
34
|
+
for (const next of children.get(id)) {
|
|
35
|
+
if (state.get(next) === 1)
|
|
36
|
+
problems.push({ kind: "cycle", nodes: [...path.slice(path.indexOf(next)), next] });
|
|
37
|
+
else if (state.get(next) === 0)
|
|
38
|
+
visit(next);
|
|
39
|
+
}
|
|
40
|
+
path.pop();
|
|
41
|
+
state.set(id, 2);
|
|
42
|
+
};
|
|
43
|
+
for (const id of [...roots, ...ids])
|
|
44
|
+
if (state.get(id) === 0)
|
|
45
|
+
visit(id);
|
|
46
|
+
if (roots.length === 1) {
|
|
47
|
+
const reached = new Set([roots[0]]);
|
|
48
|
+
const queue = [roots[0]];
|
|
49
|
+
while (queue.length > 0) {
|
|
50
|
+
for (const next of children.get(queue.shift())) {
|
|
51
|
+
if (!reached.has(next)) {
|
|
52
|
+
reached.add(next);
|
|
53
|
+
queue.push(next);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const unreachable = ids.filter((id) => !reached.has(id));
|
|
58
|
+
if (unreachable.length > 0)
|
|
59
|
+
problems.push({ kind: "unreachable", root: roots[0], nodes: unreachable });
|
|
60
|
+
}
|
|
61
|
+
return problems;
|
|
62
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Analysis, ReviewDecision } from "./schemas/analysis.schema.js";
|
|
2
|
+
/** Points retenus pour le prompt et décision du relecteur. */
|
|
3
|
+
export interface PromptSelection {
|
|
4
|
+
findingIds: string[];
|
|
5
|
+
noteIds: string[];
|
|
6
|
+
decision: ReviewDecision | null;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Prompt de correction destiné à l'agent : en-tête (titre, refs, objectif de la
|
|
10
|
+
* feature quand la vue d'ensemble existe), décision,
|
|
11
|
+
* liste numérotée des constats retenus (du plus grave au moins grave, puis dans
|
|
12
|
+
* l'ordre de l'analyse) suivie des remarques du relecteur. Le `prompt` propre
|
|
13
|
+
* à un constat remplace son corps, ses lignes actuelles et son correctif.
|
|
14
|
+
*/
|
|
15
|
+
export declare function buildAgentPrompt(analysis: Analysis, selection: PromptSelection): string;
|
|
16
|
+
/** Prompt de tous les constats ouverts, sans décision ni remarque. */
|
|
17
|
+
export declare function buildAllOpenFindingsPrompt(analysis: Analysis): string;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { sortBySeverity } from "./severity.js";
|
|
2
|
+
import { lineRange, noteLocationText } from "./text.js";
|
|
3
|
+
const SEVERITY_NAMES = {
|
|
4
|
+
critical: "Critical",
|
|
5
|
+
major: "Major",
|
|
6
|
+
minor: "Minor",
|
|
7
|
+
trivial: "Trivial",
|
|
8
|
+
};
|
|
9
|
+
const DECISION_LINES = {
|
|
10
|
+
approve: "Decision: Approve. The feature can be merged.",
|
|
11
|
+
request_changes: "Decision: Request changes. Rework the feature and address the points below.",
|
|
12
|
+
reject: "Decision: Reject. The feature is abandoned.",
|
|
13
|
+
};
|
|
14
|
+
const INDENT = " ";
|
|
15
|
+
function indent(text) {
|
|
16
|
+
return text.split("\n").map((line) => (line === "" ? "" : `${INDENT}${line}`));
|
|
17
|
+
}
|
|
18
|
+
function fenced(label, text) {
|
|
19
|
+
return [`${INDENT}${label}`, `${INDENT}\`\`\``, ...indent(text), `${INDENT}\`\`\``];
|
|
20
|
+
}
|
|
21
|
+
function refsLabel(analysis) {
|
|
22
|
+
if (analysis.mode === "branch")
|
|
23
|
+
return `branch ${analysis.base}...${analysis.head}`;
|
|
24
|
+
return "working tree changes against HEAD";
|
|
25
|
+
}
|
|
26
|
+
function findingLocation(finding) {
|
|
27
|
+
const location = finding.location;
|
|
28
|
+
if (!location)
|
|
29
|
+
return null;
|
|
30
|
+
return `${location.path}:${lineRange(location.startLine, location.endLine)}`;
|
|
31
|
+
}
|
|
32
|
+
function findingBlock(number, finding) {
|
|
33
|
+
const location = findingLocation(finding);
|
|
34
|
+
const title = finding.kind === "requirement_gap" ? `Missing requirement: ${finding.title}` : finding.title;
|
|
35
|
+
const head = `${number}. [${SEVERITY_NAMES[finding.severity]}] ${location ? `${location} — ` : ""}${title}`;
|
|
36
|
+
if (finding.prompt !== null)
|
|
37
|
+
return [head, ...indent(finding.prompt)];
|
|
38
|
+
const lines = [head];
|
|
39
|
+
if (finding.body.trim() !== "")
|
|
40
|
+
lines.push(...indent(finding.body));
|
|
41
|
+
if (finding.anchorText !== null)
|
|
42
|
+
lines.push(...fenced("Current lines:", finding.anchorText));
|
|
43
|
+
if (finding.suggestion !== null)
|
|
44
|
+
lines.push(...fenced("Proposed replacement:", finding.suggestion));
|
|
45
|
+
return lines;
|
|
46
|
+
}
|
|
47
|
+
function noteBlock(number, note) {
|
|
48
|
+
const where = note.location ? `${noteLocationText(note.location)} — ` : "";
|
|
49
|
+
const [first, ...rest] = note.text.split("\n");
|
|
50
|
+
return [`${number}. [Reviewer note] ${where}${first}`, ...rest.map((line) => (line === "" ? "" : `${INDENT}${line}`))];
|
|
51
|
+
}
|
|
52
|
+
function pick(items, ids, label) {
|
|
53
|
+
const known = new Set(items.map((item) => item.id));
|
|
54
|
+
const unknown = ids.filter((id) => !known.has(id));
|
|
55
|
+
if (unknown.length > 0)
|
|
56
|
+
throw new Error(`Unknown ${label} id(s): ${unknown.join(", ")}.`);
|
|
57
|
+
const wanted = new Set(ids);
|
|
58
|
+
return items.filter((item) => wanted.has(item.id));
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Prompt de correction destiné à l'agent : en-tête (titre, refs, objectif de la
|
|
62
|
+
* feature quand la vue d'ensemble existe), décision,
|
|
63
|
+
* liste numérotée des constats retenus (du plus grave au moins grave, puis dans
|
|
64
|
+
* l'ordre de l'analyse) suivie des remarques du relecteur. Le `prompt` propre
|
|
65
|
+
* à un constat remplace son corps, ses lignes actuelles et son correctif.
|
|
66
|
+
*/
|
|
67
|
+
export function buildAgentPrompt(analysis, selection) {
|
|
68
|
+
const findings = sortBySeverity(pick(analysis.findings, selection.findingIds, "finding"));
|
|
69
|
+
const notes = pick(analysis.notes, selection.noteIds, "note");
|
|
70
|
+
const out = [`Review feedback on "${analysis.title}" (${refsLabel(analysis)}).`];
|
|
71
|
+
if (analysis.overview !== null)
|
|
72
|
+
out.push(`Feature objective: ${analysis.overview.objective}`);
|
|
73
|
+
if (selection.decision !== null)
|
|
74
|
+
out.push(DECISION_LINES[selection.decision]);
|
|
75
|
+
out.push("");
|
|
76
|
+
if (findings.length === 0 && notes.length === 0) {
|
|
77
|
+
out.push("No points to address.");
|
|
78
|
+
return out.join("\n");
|
|
79
|
+
}
|
|
80
|
+
out.push("Address the following points:");
|
|
81
|
+
let number = 1;
|
|
82
|
+
for (const finding of findings) {
|
|
83
|
+
out.push("", ...findingBlock(number++, finding));
|
|
84
|
+
}
|
|
85
|
+
for (const note of notes) {
|
|
86
|
+
out.push("", ...noteBlock(number++, note));
|
|
87
|
+
}
|
|
88
|
+
return out.join("\n");
|
|
89
|
+
}
|
|
90
|
+
/** Prompt de tous les constats ouverts, sans décision ni remarque. */
|
|
91
|
+
export function buildAllOpenFindingsPrompt(analysis) {
|
|
92
|
+
return buildAgentPrompt(analysis, {
|
|
93
|
+
findingIds: analysis.findings.filter((finding) => finding.status === "open").map((finding) => finding.id),
|
|
94
|
+
noteIds: [],
|
|
95
|
+
decision: null,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Analysis, ReviewProgress } from "./schemas/analysis.schema.js";
|
|
2
|
+
/**
|
|
3
|
+
* Avancement de la revue d'une analyse :
|
|
4
|
+
* - `submitted` : revue soumise, avec sa décision ;
|
|
5
|
+
* - `files_reviewed` : tous les fichiers revus, revue non soumise ;
|
|
6
|
+
* - `in_progress` : au moins un fichier revu, revue non soumise ;
|
|
7
|
+
* - `not_started` : aucun fichier revu, revue non soumise. Une analyse sans
|
|
8
|
+
* fichier reste `not_started` jusqu'à la soumission.
|
|
9
|
+
*/
|
|
10
|
+
export declare function reviewProgress(analysis: Pick<Analysis, "files" | "review">): ReviewProgress;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Avancement de la revue d'une analyse :
|
|
3
|
+
* - `submitted` : revue soumise, avec sa décision ;
|
|
4
|
+
* - `files_reviewed` : tous les fichiers revus, revue non soumise ;
|
|
5
|
+
* - `in_progress` : au moins un fichier revu, revue non soumise ;
|
|
6
|
+
* - `not_started` : aucun fichier revu, revue non soumise. Une analyse sans
|
|
7
|
+
* fichier reste `not_started` jusqu'à la soumission.
|
|
8
|
+
*/
|
|
9
|
+
export function reviewProgress(analysis) {
|
|
10
|
+
const totalFiles = analysis.files.length;
|
|
11
|
+
const reviewedFiles = analysis.files.filter((file) => file.reviewed).length;
|
|
12
|
+
if (analysis.review.state === "submitted") {
|
|
13
|
+
return { state: "submitted", reviewedFiles, totalFiles, decision: analysis.review.decision };
|
|
14
|
+
}
|
|
15
|
+
const state = reviewedFiles === 0 ? "not_started" : reviewedFiles === totalFiles ? "files_reviewed" : "in_progress";
|
|
16
|
+
return { state, reviewedFiles, totalFiles, decision: null };
|
|
17
|
+
}
|