@imenam/mcp-feature-analyzer 1.0.17 → 1.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +357 -351
  2. package/dist/shared/labels.d.ts +2 -0
  3. package/dist/shared/labels.js +2 -0
  4. package/dist/shared/review-state.d.ts +32 -2
  5. package/dist/shared/review-state.js +74 -3
  6. package/dist/shared/schemas/analysis.schema.d.ts +100 -0
  7. package/dist/shared/schemas/analysis.schema.js +24 -0
  8. package/dist/shared/schemas/ipc.schema.d.ts +17 -2
  9. package/dist/shared/schemas/ipc.schema.js +2 -2
  10. package/dist/src/core/analysis-store.d.ts +0 -2
  11. package/dist/src/core/analysis-store.js +1 -5
  12. package/dist/src/core/invariants.js +3 -0
  13. package/dist/src/core/migrate.d.ts +5 -0
  14. package/dist/src/core/migrate.js +14 -1
  15. package/dist/src/core/review-actions.d.ts +9 -10
  16. package/dist/src/core/review-actions.js +22 -29
  17. package/dist/src/gui-worker.js +48 -46
  18. package/dist/src/index.js +36 -13
  19. package/dist/src/tools/format.d.ts +4 -1
  20. package/dist/src/tools/format.js +6 -2
  21. package/dist/src/tools/get-review-feedback.d.ts +5 -1
  22. package/dist/src/tools/get-review-feedback.js +59 -19
  23. package/dist/src/tools/list-analyses.d.ts +7 -1
  24. package/dist/src/tools/list-analyses.js +39 -14
  25. package/dist/src/tools/mark-feedback-processed.d.ts +8 -0
  26. package/dist/src/tools/mark-feedback-processed.js +34 -0
  27. package/gui/dist/assets/{angular-html-mqETY8Ta.js → angular-html-khSGtcDV.js} +1 -1
  28. package/gui/dist/assets/{angular-ts-Dyua8-1t.js → angular-ts-CQZskKbG.js} +1 -1
  29. package/gui/dist/assets/{apl-3jbxnvrH.js → apl-Dk2A1KW8.js} +1 -1
  30. package/gui/dist/assets/{astro-Co4C3aDz.js → astro-CDNLxpIR.js} +1 -1
  31. package/gui/dist/assets/{blade-B4EPDbIQ.js → blade-HVRxvZlI.js} +1 -1
  32. package/gui/dist/assets/{c-B6Zq6Std.js → c-BAogwQrx.js} +1 -1
  33. package/gui/dist/assets/{chapel-BQd1_20m.js → chapel-CXJ1GKiJ.js} +1 -1
  34. package/gui/dist/assets/{cobol-m9aPmaKL.js → cobol-CUpuiRry.js} +1 -1
  35. package/gui/dist/assets/{coffee-9RfP0zJM.js → coffee-Bvuk4CPF.js} +1 -1
  36. package/gui/dist/assets/{cpp-BVBdUNUD.js → cpp-DrhGCibo.js} +1 -1
  37. package/gui/dist/assets/{crystal-BPUdd6PR.js → crystal-CoGIF_h4.js} +1 -1
  38. package/gui/dist/assets/{css-LrCWcNdJ.js → css-CUXMpymc.js} +1 -1
  39. package/gui/dist/assets/{edge-D-Q1P4iH.js → edge-MTcLzslY.js} +1 -1
  40. package/gui/dist/assets/{elixir-BvGDaF1W.js → elixir-DpYpNuu6.js} +1 -1
  41. package/gui/dist/assets/{elm-DcLIpqA8.js → elm-CAdLQ8j2.js} +1 -1
  42. package/gui/dist/assets/{erb-BNIJdl1V.js → erb-wGT1uBBo.js} +1 -1
  43. package/gui/dist/assets/{git-rebase-9T7mb2tK.js → git-rebase-D7SHvGwZ.js} +1 -1
  44. package/gui/dist/assets/{glimmer-js-7oEVZM3L.js → glimmer-js-DEF7JSd6.js} +1 -1
  45. package/gui/dist/assets/{glimmer-ts-B9lGBZyd.js → glimmer-ts-CM94Mbx8.js} +1 -1
  46. package/gui/dist/assets/{glsl-C59JLFCt.js → glsl-DvB1v13j.js} +1 -1
  47. package/gui/dist/assets/{graphql-DhT3P-M_.js → graphql-CpBeqKe2.js} +1 -1
  48. package/gui/dist/assets/{hack-B22EZN7R.js → hack-CBlJ1Su7.js} +1 -1
  49. package/gui/dist/assets/{haml-DOHFovSe.js → haml-Db0uDFny.js} +1 -1
  50. package/gui/dist/assets/{handlebars-ChdRsCo8.js → handlebars-B9jQcMgj.js} +1 -1
  51. package/gui/dist/assets/{html-Cvii8U8Z.js → html-DTSojrQm.js} +1 -1
  52. package/gui/dist/assets/{html-derivative-DvPQ-VHx.js → html-derivative-7jGgl4AX.js} +1 -1
  53. package/gui/dist/assets/{http-DrT8XJXI.js → http-DJbcEzJc.js} +1 -1
  54. package/gui/dist/assets/{hurl-B-_5p73n.js → hurl-BjBdeErH.js} +1 -1
  55. package/gui/dist/assets/index-Bj1iisUG.css +1 -0
  56. package/gui/dist/assets/index-CR3WhYFz.js +87 -0
  57. package/gui/dist/assets/{java-DX7zTQhc.js → java-Dn64Xe0Z.js} +1 -1
  58. package/gui/dist/assets/{javascript-Db2TOoX-.js → javascript-BbkSu_9c.js} +1 -1
  59. package/gui/dist/assets/{jinja-BlzjXyBG.js → jinja-DqC5t5sF.js} +1 -1
  60. package/gui/dist/assets/{jison-CYH9v_vD.js → jison-DpuZbYSp.js} +1 -1
  61. package/gui/dist/assets/{json-DvgESUmx.js → json-B2a7n5JV.js} +1 -1
  62. package/gui/dist/assets/{jsx-BxUu3NOl.js → jsx-ClgcRv-g.js} +1 -1
  63. package/gui/dist/assets/{julia-BD6shmUv.js → julia-Do1-bHs6.js} +1 -1
  64. package/gui/dist/assets/{just-B58vJyzZ.js → just-DHt2GVD3.js} +1 -1
  65. package/gui/dist/assets/{latex-DpXG4qlp.js → latex-B_f2UeRI.js} +1 -1
  66. package/gui/dist/assets/{liquid-CKXKEzQm.js → liquid-Ds2eU_MP.js} +1 -1
  67. package/gui/dist/assets/{lua-CrR2yS38.js → lua-S7tZuNGt.js} +1 -1
  68. package/gui/dist/assets/{marko-DIY1RQi3.js → marko-udM4lCVd.js} +1 -1
  69. package/gui/dist/assets/{mdc-CSxBBNi0.js → mdc-Cp5CPiQh.js} +1 -1
  70. package/gui/dist/assets/{nginx-BR5UiLsj.js → nginx-fGikazrY.js} +1 -1
  71. package/gui/dist/assets/{nim-81Kxf6HC.js → nim-ByS27m8u.js} +1 -1
  72. package/gui/dist/assets/{org-YkzSiTpH.js → org-CWef6EjL.js} +1 -1
  73. package/gui/dist/assets/{perl-BKAf7ctD.js → perl-B3b8PFq6.js} +1 -1
  74. package/gui/dist/assets/{php-D60BkLMS.js → php-BgIqtqOt.js} +1 -1
  75. package/gui/dist/assets/{pug-CeojeKqv.js → pug-BRImkfAG.js} +1 -1
  76. package/gui/dist/assets/{qml-vJFVB1sq.js → qml-CzXZbZNQ.js} +1 -1
  77. package/gui/dist/assets/{r-buvgYjbV.js → r-Nteo2x9j.js} +1 -1
  78. package/gui/dist/assets/{razor-Dz_7Cjz-.js → razor-D_Afw34M.js} +1 -1
  79. package/gui/dist/assets/{regexp-B68ulszt.js → regexp-CKIOMRkq.js} +1 -1
  80. package/gui/dist/assets/{rst-BJ9SfN4C.js → rst-BKqSLPk8.js} +1 -1
  81. package/gui/dist/assets/{ruby-C_OOgGF-.js → ruby-e08qQ71e.js} +1 -1
  82. package/gui/dist/assets/{sas-DyKx0IvO.js → sas-CBP-Z7IW.js} +1 -1
  83. package/gui/dist/assets/{scss-D2FME1KT.js → scss-D_FoTbGY.js} +1 -1
  84. package/gui/dist/assets/{shellscript-DKhUkHWJ.js → shellscript-Ck8Pcu_n.js} +1 -1
  85. package/gui/dist/assets/{shellsession-B9VkeV-t.js → shellsession-ClnG1wCa.js} +1 -1
  86. package/gui/dist/assets/{soy-DT_8USFA.js → soy-SRwHcO2G.js} +1 -1
  87. package/gui/dist/assets/{sql-CTY7Kq3S.js → sql-CjUCW4z6.js} +1 -1
  88. package/gui/dist/assets/{stata-BXc7Lm53.js → stata-DbpO8t8N.js} +1 -1
  89. package/gui/dist/assets/{surrealql-Dc_YL61u.js → surrealql-BvdjWgRW.js} +1 -1
  90. package/gui/dist/assets/{svelte-fq0JF0B3.js → svelte-Bce_bVba.js} +1 -1
  91. package/gui/dist/assets/{templ-B20S0R9K.js → templ-D1Wmfx4s.js} +1 -1
  92. package/gui/dist/assets/{tex-BhmGjqVa.js → tex-DAUoYbei.js} +1 -1
  93. package/gui/dist/assets/{ts-tags-D_pLLOmi.js → ts-tags-DeHdW_lm.js} +1 -1
  94. package/gui/dist/assets/{tsx-DULdqnBJ.js → tsx-BtUXi9Pr.js} +1 -1
  95. package/gui/dist/assets/{twig-DIYYhsx1.js → twig-NY4Ref0L.js} +1 -1
  96. package/gui/dist/assets/{typescript-0qGmyQ6p.js → typescript-BRVtVFtH.js} +1 -1
  97. package/gui/dist/assets/{typst-qJ1kgBh8.js → typst-CEx4fnEq.js} +1 -1
  98. package/gui/dist/assets/{vue-BZCq4N1_.js → vue-CzSBuAS4.js} +1 -1
  99. package/gui/dist/assets/{vue-html-DtRFW8u7.js → vue-html-3GkraVg2.js} +1 -1
  100. package/gui/dist/assets/{vue-vine-CoFObNiz.js → vue-vine-un5mayXW.js} +1 -1
  101. package/gui/dist/assets/{xml-DjvoX99Q.js → xml-DYFbmBqP.js} +1 -1
  102. package/gui/dist/assets/{xsl-B0Obrqq8.js → xsl-CVaXn7rF.js} +1 -1
  103. package/gui/dist/assets/{yaml-Dbi9c2aj.js → yaml-CCwQ0s68.js} +1 -1
  104. package/gui/dist/index.html +2 -2
  105. package/package.json +1 -1
  106. package/gui/dist/assets/index-B_Us1tSN.css +0 -1
  107. package/gui/dist/assets/index-D7dShIx3.js +0 -87
@@ -17,6 +17,8 @@ export declare const MODE_LABELS: Record<AnalysisMode, string>;
17
17
  export declare const DECISION_LABELS: Record<ReviewDecision, string>;
18
18
  /** Pastille de l'état d'avancement d'une revue. */
19
19
  export declare const REVIEW_PROGRESS_STYLES: Record<ReviewProgressState, BadgeStyle>;
20
+ /** Pastille d'une revue soumise dont l'agent a marqué le retour traité. */
21
+ export declare const PROCESSED_STYLE: BadgeStyle;
20
22
  /** Pastille de la décision d'une revue soumise, affichée à côté de « Soumise ». */
21
23
  export declare const DECISION_STYLES: Record<ReviewDecision, BadgeStyle>;
22
24
  /**
@@ -46,6 +46,8 @@ export const REVIEW_PROGRESS_STYLES = {
46
46
  files_reviewed: { label: "Fichiers revus", color: "#8b97ff", background: "#1b1f3d" },
47
47
  submitted: { label: "Soumise", color: "#3fb950", background: "#11201a" },
48
48
  };
49
+ /** Pastille d'une revue soumise dont l'agent a marqué le retour traité. */
50
+ export const PROCESSED_STYLE = { label: "Traitée", color: "#5fd4c4", background: "#10272a" };
49
51
  /** Pastille de la décision d'une revue soumise, affichée à côté de « Soumise ». */
50
52
  export const DECISION_STYLES = {
51
53
  approve: { label: "Approuvée", color: "#3fb950", background: "#11201a" },
@@ -1,10 +1,40 @@
1
- import type { Analysis, ReviewProgress } from "./schemas/analysis.schema.js";
1
+ import type { Analysis, Review, ReviewProgress, SubmissionRecord } from "./schemas/analysis.schema.js";
2
2
  /**
3
3
  * Avancement de la revue d'une analyse :
4
- * - `submitted` : revue soumise, avec sa décision ;
4
+ * - `submitted` : revue soumise, avec sa décision et son traitement par l'agent ;
5
5
  * - `files_reviewed` : tous les fichiers revus, revue non soumise ;
6
6
  * - `in_progress` : au moins un fichier revu, revue non soumise ;
7
7
  * - `not_started` : aucun fichier revu, revue non soumise. Une analyse sans
8
8
  * fichier reste `not_started` jusqu'à la soumission.
9
9
  */
10
10
  export declare function reviewProgress(analysis: Pick<Analysis, "files" | "review">): ReviewProgress;
11
+ /** Copie autonome de la revue soumise de `analysis` : décision, date, traitement, points retenus et points écartés. */
12
+ export declare function recordSubmission(analysis: Pick<Analysis, "review" | "findings" | "notes">): SubmissionRecord;
13
+ /**
14
+ * Repasse une revue soumise à l'état en attente, avant une modification du
15
+ * relecteur : la version soumise est conservée dans `previousSubmission`. Sans
16
+ * effet sur une revue en attente.
17
+ */
18
+ export declare function reopenSubmittedReview(draft: Pick<Analysis, "review" | "findings" | "notes">): void;
19
+ /** Revue vierge : en attente, sans décision, sélection, prompt, traitement ni version précédente. */
20
+ export declare function pendingReview(): Review;
21
+ /** Écarts entre deux versions soumises d'une revue. */
22
+ export interface SubmissionChanges {
23
+ decision: {
24
+ from: SubmissionRecord["decision"];
25
+ to: SubmissionRecord["decision"];
26
+ } | null;
27
+ addedFindings: SubmissionRecord["findings"];
28
+ removedFindings: SubmissionRecord["findings"];
29
+ addedNotes: SubmissionRecord["notes"];
30
+ /** Remarques retenues dans les deux versions dont le texte ou l'emplacement a changé. */
31
+ modifiedNotes: Array<{
32
+ before: SubmissionRecord["notes"][number];
33
+ after: SubmissionRecord["notes"][number];
34
+ }>;
35
+ removedNotes: SubmissionRecord["notes"];
36
+ }
37
+ /** Ce qui a changé de la version soumise `before` à la version `after`. */
38
+ export declare function submissionChanges(before: SubmissionRecord, after: SubmissionRecord): SubmissionChanges;
39
+ /** Vrai quand `submissionChanges` ne relève aucun écart. */
40
+ export declare function hasNoChanges(changes: SubmissionChanges): boolean;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Avancement de la revue d'une analyse :
3
- * - `submitted` : revue soumise, avec sa décision ;
3
+ * - `submitted` : revue soumise, avec sa décision et son traitement par l'agent ;
4
4
  * - `files_reviewed` : tous les fichiers revus, revue non soumise ;
5
5
  * - `in_progress` : au moins un fichier revu, revue non soumise ;
6
6
  * - `not_started` : aucun fichier revu, revue non soumise. Une analyse sans
@@ -10,8 +10,79 @@ export function reviewProgress(analysis) {
10
10
  const totalFiles = analysis.files.length;
11
11
  const reviewedFiles = analysis.files.filter((file) => file.reviewed).length;
12
12
  if (analysis.review.state === "submitted") {
13
- return { state: "submitted", reviewedFiles, totalFiles, decision: analysis.review.decision };
13
+ return { state: "submitted", reviewedFiles, totalFiles, decision: analysis.review.decision, processed: analysis.review.processed };
14
14
  }
15
15
  const state = reviewedFiles === 0 ? "not_started" : reviewedFiles === totalFiles ? "files_reviewed" : "in_progress";
16
- return { state, reviewedFiles, totalFiles, decision: null };
16
+ return { state, reviewedFiles, totalFiles, decision: null, processed: null };
17
+ }
18
+ /** Copie autonome de la revue soumise de `analysis` : décision, date, traitement, points retenus et points écartés. */
19
+ export function recordSubmission(analysis) {
20
+ const review = analysis.review;
21
+ if (review.state !== "submitted" || review.decision === null || review.submittedAt === null) {
22
+ throw new Error("Only a submitted review can be recorded.");
23
+ }
24
+ const findingIds = new Set(review.selectedFindingIds);
25
+ const noteIds = new Set(review.selectedNoteIds);
26
+ return {
27
+ decision: review.decision,
28
+ submittedAt: review.submittedAt,
29
+ processed: review.processed,
30
+ findings: analysis.findings.filter((finding) => findingIds.has(finding.id)).map(({ id, title }) => ({ id, title })),
31
+ notes: analysis.notes.filter((note) => noteIds.has(note.id)).map(({ id, location, text }) => ({ id, location, text })),
32
+ excludedFindingIds: analysis.findings.filter((finding) => finding.status === "open" && !findingIds.has(finding.id)).map(({ id }) => id),
33
+ excludedNoteIds: analysis.notes.filter((note) => !noteIds.has(note.id)).map(({ id }) => id),
34
+ };
35
+ }
36
+ /**
37
+ * Repasse une revue soumise à l'état en attente, avant une modification du
38
+ * relecteur : la version soumise est conservée dans `previousSubmission`. Sans
39
+ * effet sur une revue en attente.
40
+ */
41
+ export function reopenSubmittedReview(draft) {
42
+ if (draft.review.state !== "submitted")
43
+ return;
44
+ draft.review = { ...pendingReview(), previousSubmission: recordSubmission(draft) };
45
+ }
46
+ /** Revue vierge : en attente, sans décision, sélection, prompt, traitement ni version précédente. */
47
+ export function pendingReview() {
48
+ return {
49
+ state: "pending",
50
+ decision: null,
51
+ selectedFindingIds: [],
52
+ selectedNoteIds: [],
53
+ prompt: null,
54
+ submittedAt: null,
55
+ processed: null,
56
+ previousSubmission: null,
57
+ };
58
+ }
59
+ function sameLocation(a, b) {
60
+ return a === null || b === null ? a === b : a.path === b.path && a.line === b.line;
61
+ }
62
+ /** Ce qui a changé de la version soumise `before` à la version `after`. */
63
+ export function submissionChanges(before, after) {
64
+ const afterFindings = new Set(after.findings.map((finding) => finding.id));
65
+ const beforeFindings = new Set(before.findings.map((finding) => finding.id));
66
+ const afterNotes = new Map(after.notes.map((note) => [note.id, note]));
67
+ const beforeNotes = new Map(before.notes.map((note) => [note.id, note]));
68
+ return {
69
+ decision: before.decision === after.decision ? null : { from: before.decision, to: after.decision },
70
+ addedFindings: after.findings.filter((finding) => !beforeFindings.has(finding.id)),
71
+ removedFindings: before.findings.filter((finding) => !afterFindings.has(finding.id)),
72
+ addedNotes: after.notes.filter((note) => !beforeNotes.has(note.id)),
73
+ modifiedNotes: after.notes.flatMap((note) => {
74
+ const previous = beforeNotes.get(note.id);
75
+ return previous && (previous.text !== note.text || !sameLocation(previous.location, note.location)) ? [{ before: previous, after: note }] : [];
76
+ }),
77
+ removedNotes: before.notes.filter((note) => !afterNotes.has(note.id)),
78
+ };
79
+ }
80
+ /** Vrai quand `submissionChanges` ne relève aucun écart. */
81
+ export function hasNoChanges(changes) {
82
+ return (changes.decision === null &&
83
+ changes.addedFindings.length === 0 &&
84
+ changes.removedFindings.length === 0 &&
85
+ changes.addedNotes.length === 0 &&
86
+ changes.modifiedNotes.length === 0 &&
87
+ changes.removedNotes.length === 0);
17
88
  }
@@ -191,6 +191,42 @@ export declare const DiagramSchema: z.ZodObject<{
191
191
  }, z.core.$strip>>;
192
192
  updatedAt: z.ZodString;
193
193
  }, z.core.$strip>;
194
+ /** Traitement d'un retour par l'agent : date et note facultative (commits, réponses données…). */
195
+ export declare const ProcessingSchema: z.ZodObject<{
196
+ at: z.ZodString;
197
+ note: z.ZodNullable<z.ZodString>;
198
+ }, z.core.$strip>;
199
+ /**
200
+ * Dernière version soumise d'une revue, conservée quand le relecteur la modifie
201
+ * ou la soumet à nouveau. Elle recopie les points retenus pour rester lisible
202
+ * même après la suppression de constats ou de remarques.
203
+ */
204
+ export declare const SubmissionRecordSchema: z.ZodObject<{
205
+ decision: z.ZodEnum<{
206
+ approve: "approve";
207
+ reject: "reject";
208
+ request_changes: "request_changes";
209
+ }>;
210
+ submittedAt: z.ZodString;
211
+ processed: z.ZodNullable<z.ZodObject<{
212
+ at: z.ZodString;
213
+ note: z.ZodNullable<z.ZodString>;
214
+ }, z.core.$strip>>;
215
+ findings: z.ZodArray<z.ZodObject<{
216
+ id: z.ZodString;
217
+ title: z.ZodString;
218
+ }, z.core.$strip>>;
219
+ notes: z.ZodArray<z.ZodObject<{
220
+ id: z.ZodString;
221
+ location: z.ZodNullable<z.ZodObject<{
222
+ path: z.ZodString;
223
+ line: z.ZodNullable<z.ZodNumber>;
224
+ }, z.core.$strip>>;
225
+ text: z.ZodString;
226
+ }, z.core.$strip>>;
227
+ excludedFindingIds: z.ZodArray<z.ZodString>;
228
+ excludedNoteIds: z.ZodArray<z.ZodString>;
229
+ }, z.core.$strip>;
194
230
  export declare const ReviewSchema: z.ZodObject<{
195
231
  state: z.ZodEnum<{
196
232
  pending: "pending";
@@ -205,6 +241,36 @@ export declare const ReviewSchema: z.ZodObject<{
205
241
  selectedNoteIds: z.ZodArray<z.ZodString>;
206
242
  prompt: z.ZodNullable<z.ZodString>;
207
243
  submittedAt: z.ZodNullable<z.ZodString>;
244
+ processed: z.ZodNullable<z.ZodObject<{
245
+ at: z.ZodString;
246
+ note: z.ZodNullable<z.ZodString>;
247
+ }, z.core.$strip>>;
248
+ previousSubmission: z.ZodNullable<z.ZodObject<{
249
+ decision: z.ZodEnum<{
250
+ approve: "approve";
251
+ reject: "reject";
252
+ request_changes: "request_changes";
253
+ }>;
254
+ submittedAt: z.ZodString;
255
+ processed: z.ZodNullable<z.ZodObject<{
256
+ at: z.ZodString;
257
+ note: z.ZodNullable<z.ZodString>;
258
+ }, z.core.$strip>>;
259
+ findings: z.ZodArray<z.ZodObject<{
260
+ id: z.ZodString;
261
+ title: z.ZodString;
262
+ }, z.core.$strip>>;
263
+ notes: z.ZodArray<z.ZodObject<{
264
+ id: z.ZodString;
265
+ location: z.ZodNullable<z.ZodObject<{
266
+ path: z.ZodString;
267
+ line: z.ZodNullable<z.ZodNumber>;
268
+ }, z.core.$strip>>;
269
+ text: z.ZodString;
270
+ }, z.core.$strip>>;
271
+ excludedFindingIds: z.ZodArray<z.ZodString>;
272
+ excludedNoteIds: z.ZodArray<z.ZodString>;
273
+ }, z.core.$strip>>;
208
274
  }, z.core.$strip>;
209
275
  export declare const RequestSchema: z.ZodObject<{
210
276
  text: z.ZodString;
@@ -371,6 +437,36 @@ export declare const AnalysisSchema: z.ZodObject<{
371
437
  selectedNoteIds: z.ZodArray<z.ZodString>;
372
438
  prompt: z.ZodNullable<z.ZodString>;
373
439
  submittedAt: z.ZodNullable<z.ZodString>;
440
+ processed: z.ZodNullable<z.ZodObject<{
441
+ at: z.ZodString;
442
+ note: z.ZodNullable<z.ZodString>;
443
+ }, z.core.$strip>>;
444
+ previousSubmission: z.ZodNullable<z.ZodObject<{
445
+ decision: z.ZodEnum<{
446
+ approve: "approve";
447
+ reject: "reject";
448
+ request_changes: "request_changes";
449
+ }>;
450
+ submittedAt: z.ZodString;
451
+ processed: z.ZodNullable<z.ZodObject<{
452
+ at: z.ZodString;
453
+ note: z.ZodNullable<z.ZodString>;
454
+ }, z.core.$strip>>;
455
+ findings: z.ZodArray<z.ZodObject<{
456
+ id: z.ZodString;
457
+ title: z.ZodString;
458
+ }, z.core.$strip>>;
459
+ notes: z.ZodArray<z.ZodObject<{
460
+ id: z.ZodString;
461
+ location: z.ZodNullable<z.ZodObject<{
462
+ path: z.ZodString;
463
+ line: z.ZodNullable<z.ZodNumber>;
464
+ }, z.core.$strip>>;
465
+ text: z.ZodString;
466
+ }, z.core.$strip>>;
467
+ excludedFindingIds: z.ZodArray<z.ZodString>;
468
+ excludedNoteIds: z.ZodArray<z.ZodString>;
469
+ }, z.core.$strip>>;
374
470
  }, z.core.$strip>;
375
471
  revision: z.ZodNumber;
376
472
  createdAt: z.ZodString;
@@ -404,6 +500,8 @@ export type DiagramNodeLocation = z.infer<typeof DiagramNodeLocationSchema>;
404
500
  export type DiagramNode = z.infer<typeof DiagramNodeSchema>;
405
501
  export type DiagramLink = z.infer<typeof DiagramLinkSchema>;
406
502
  export type Diagram = z.infer<typeof DiagramSchema>;
503
+ export type Processing = z.infer<typeof ProcessingSchema>;
504
+ export type SubmissionRecord = z.infer<typeof SubmissionRecordSchema>;
407
505
  export type Review = z.infer<typeof ReviewSchema>;
408
506
  export type AnalysisRequest = z.infer<typeof RequestSchema>;
409
507
  export type Overview = z.infer<typeof OverviewSchema>;
@@ -418,6 +516,8 @@ export interface ReviewProgress {
418
516
  totalFiles: number;
419
517
  /** Décision de la revue soumise ; null tant qu'elle ne l'est pas. */
420
518
  decision: ReviewDecision | null;
519
+ /** Traitement de la revue soumise par l'agent ; null tant qu'il est à faire ou que la revue n'est pas soumise. */
520
+ processed: Processing | null;
421
521
  }
422
522
  /** Vue légère d'une analyse, pour les listes. */
423
523
  export interface AnalysisSummary {
@@ -122,6 +122,26 @@ export const DiagramSchema = z.object({
122
122
  links: z.array(DiagramLinkSchema),
123
123
  updatedAt: IsoDateSchema,
124
124
  });
125
+ /** Traitement d'un retour par l'agent : date et note facultative (commits, réponses données…). */
126
+ export const ProcessingSchema = z.object({
127
+ at: IsoDateSchema,
128
+ note: z.string().min(1).nullable(),
129
+ });
130
+ /**
131
+ * Dernière version soumise d'une revue, conservée quand le relecteur la modifie
132
+ * ou la soumet à nouveau. Elle recopie les points retenus pour rester lisible
133
+ * même après la suppression de constats ou de remarques.
134
+ */
135
+ export const SubmissionRecordSchema = z.object({
136
+ decision: z.enum(REVIEW_DECISIONS),
137
+ submittedAt: IsoDateSchema,
138
+ processed: ProcessingSchema.nullable(),
139
+ findings: z.array(z.object({ id: ItemIdSchema, title: z.string().min(1) })),
140
+ notes: z.array(z.object({ id: ItemIdSchema, location: NoteLocationSchema.nullable(), text: z.string().min(1) })),
141
+ /** Constats ouverts et remarques que le relecteur pouvait transmettre et a écartés. */
142
+ excludedFindingIds: z.array(ItemIdSchema),
143
+ excludedNoteIds: z.array(ItemIdSchema),
144
+ });
125
145
  export const ReviewSchema = z.object({
126
146
  state: z.enum(REVIEW_STATES),
127
147
  decision: z.enum(REVIEW_DECISIONS).nullable(),
@@ -129,6 +149,10 @@ export const ReviewSchema = z.object({
129
149
  selectedNoteIds: z.array(ItemIdSchema),
130
150
  prompt: z.string().nullable(),
131
151
  submittedAt: IsoDateSchema.nullable(),
152
+ /** Traitement de la soumission en cours par l'agent ; null tant qu'il ne l'a pas marquée traitée. */
153
+ processed: ProcessingSchema.nullable(),
154
+ /** Version soumise précédente ; null avant la première modification ou nouvelle soumission. */
155
+ previousSubmission: SubmissionRecordSchema.nullable(),
132
156
  });
133
157
  export const RequestSchema = z.object({
134
158
  text: z.string().min(1),
@@ -21,9 +21,24 @@ export declare const IPCMessageSchema: z.ZodObject<{
21
21
  timestamp: z.ZodString;
22
22
  }, z.core.$strip>;
23
23
  export type IPCMessage = z.infer<typeof IPCMessageSchema>;
24
- /** Types d'IPC envoyés par le maître et relayés tels quels aux clients SSE de la GUI. */
25
- export declare const PUSHED_EVENT_TYPES: readonly ["INITIAL_STATE", "ANALYSES_UPDATED", "ANALYSIS_UPDATED"];
24
+ /** Types d'IPC envoyés par le maître, journalisés par le GUI worker et servis tels quels par `/api/changes`. */
25
+ export declare const PUSHED_EVENT_TYPES: readonly ["ANALYSES_UPDATED", "ANALYSIS_UPDATED"];
26
26
  export type PushedEventType = (typeof PUSHED_EVENT_TYPES)[number];
27
+ /**
28
+ * Réponse de `GET /api/changes`. `epoch` identifie l'instance du GUI worker,
29
+ * `seq` le dernier évènement journalisé. `reset: true` signifie que le client
30
+ * doit tout recharger ; sinon `events` liste les évènements postérieurs à `since`.
31
+ */
32
+ export type ChangesResponse = {
33
+ epoch: string;
34
+ seq: number;
35
+ reset: true;
36
+ } | {
37
+ epoch: string;
38
+ seq: number;
39
+ reset: false;
40
+ events: IPCMessage[];
41
+ };
27
42
  /** Requêtes du GUI worker au maître ; chacune reçoit une réponse de même `correlationId`. */
28
43
  export declare const GUI_REQUEST_TYPES: readonly ["GET_CONFIG", "LIST_ANALYSES", "GET_ANALYSIS", "GET_DIFF", "SET_FILE_REVIEWED", "SET_FINDING_STATUS", "ADD_NOTE", "UPDATE_NOTE", "DELETE_NOTE", "SUBMIT_REVIEW"];
29
44
  export type GuiRequestType = (typeof GUI_REQUEST_TYPES)[number];
@@ -14,8 +14,8 @@ export const IPCMessageSchema = z.object({
14
14
  errorKind: z.enum(IPC_ERROR_KINDS).optional(),
15
15
  timestamp: z.string(),
16
16
  });
17
- /** Types d'IPC envoyés par le maître et relayés tels quels aux clients SSE de la GUI. */
18
- export const PUSHED_EVENT_TYPES = ["INITIAL_STATE", "ANALYSES_UPDATED", "ANALYSIS_UPDATED"];
17
+ /** Types d'IPC envoyés par le maître, journalisés par le GUI worker et servis tels quels par `/api/changes`. */
18
+ export const PUSHED_EVENT_TYPES = ["ANALYSES_UPDATED", "ANALYSIS_UPDATED"];
19
19
  /** Requêtes du GUI worker au maître ; chacune reçoit une réponse de même `correlationId`. */
20
20
  export const GUI_REQUEST_TYPES = [
21
21
  "GET_CONFIG",
@@ -29,8 +29,6 @@ export interface ReplaceSnapshotResult {
29
29
  /** Revue en place avant le recalcul, remplacée par une revue vierge. */
30
30
  previousReview: Review;
31
31
  }
32
- /** Revue vierge : en attente, sans décision, sélection ni prompt. */
33
- export declare function pendingReview(): Review;
34
32
  /**
35
33
  * Persistance des analyses : `<dataDir>/analyses/<id>.json` pour l'analyse et
36
34
  * `<dataDir>/diffs/<id>.json` pour son snapshot de diff. Plusieurs processus
@@ -3,17 +3,13 @@ import fs from "fs";
3
3
  import path from "path";
4
4
  import { ANALYSIS_ID_PATTERN, AnalysisSchema, } from "../../shared/schemas/analysis.schema.js";
5
5
  import { DiffSnapshotSchema } from "../../shared/schemas/diff.schema.js";
6
- import { reviewProgress } from "../../shared/review-state.js";
6
+ import { pendingReview, reviewProgress } from "../../shared/review-state.js";
7
7
  import { emptySeverityCounts } from "../../shared/severity.js";
8
8
  import { AnalysisError, NotFoundError, RevisionConflictError } from "./errors.js";
9
9
  import { analysisId } from "./ids.js";
10
10
  import { assertInvariants, assertSnapshotFieldsUnchanged } from "./invariants.js";
11
11
  import { migrateAnalysis, migrateSnapshot } from "./migrate.js";
12
12
  import { applyRefresh } from "./refresh.js";
13
- /** Revue vierge : en attente, sans décision, sélection ni prompt. */
14
- export function pendingReview() {
15
- return { state: "pending", decision: null, selectedFindingIds: [], selectedNoteIds: [], prompt: null, submittedAt: null };
16
- }
17
13
  /** Au-delà de ce délai, un verrou est réputé abandonné par un processus arrêté en cours d'écriture. */
18
14
  const LOCK_STALE_MS = 10_000;
19
15
  const LOCK_RETRY_MS = 25;
@@ -148,6 +148,9 @@ export function findInvariantViolations(analysis) {
148
148
  if (review.state === "pending" && review.submittedAt !== null) {
149
149
  problems.push("a pending review cannot have a submittedAt date");
150
150
  }
151
+ if (review.state === "pending" && review.processed !== null) {
152
+ problems.push("a pending review cannot be marked processed");
153
+ }
151
154
  return problems;
152
155
  }
153
156
  /** Lève une AnalysisError listant toutes les violations, s'il y en a. */
@@ -16,6 +16,11 @@ export declare const addOverview: MigrationStep;
16
16
  export declare const addExplanations: MigrationStep;
17
17
  /** Ajoute `findingId` à null sur les remarques qui n'en ont pas : elles restent des remarques libres. */
18
18
  export declare const addNoteFindingIds: MigrationStep;
19
+ /**
20
+ * Ajoute à la revue `processed` et `previousSubmission` à null : une revue soumise
21
+ * avant ce format est à traiter, sans version précédente.
22
+ */
23
+ export declare const addReviewProcessing: MigrationStep;
19
24
  /** Étapes appliquées aux analyses (`analyses/<id>.json`), dans l'ordre. */
20
25
  export declare const ANALYSIS_MIGRATIONS: readonly MigrationStep[];
21
26
  /** Étapes appliquées aux snapshots de diff (`diffs/<id>.json`), dans l'ordre. */
@@ -36,8 +36,21 @@ export const addNoteFindingIds = (doc) => {
36
36
  return null;
37
37
  return { ...doc, notes: notes.map((note) => (missing(note) ? { ...note, findingId: null } : note)) };
38
38
  };
39
+ /**
40
+ * Ajoute à la revue `processed` et `previousSubmission` à null : une revue soumise
41
+ * avant ce format est à traiter, sans version précédente.
42
+ */
43
+ export const addReviewProcessing = (doc) => {
44
+ const review = doc.review;
45
+ if (!review || typeof review !== "object" || Array.isArray(review))
46
+ return null;
47
+ const raw = review;
48
+ if (raw.processed !== undefined && raw.previousSubmission !== undefined)
49
+ return null;
50
+ return { ...doc, review: { ...raw, processed: raw.processed ?? null, previousSubmission: raw.previousSubmission ?? null } };
51
+ };
39
52
  /** Étapes appliquées aux analyses (`analyses/<id>.json`), dans l'ordre. */
40
- export const ANALYSIS_MIGRATIONS = [addProject, addOverview, addExplanations, addNoteFindingIds];
53
+ export const ANALYSIS_MIGRATIONS = [addProject, addOverview, addExplanations, addNoteFindingIds, addReviewProcessing];
41
54
  /** Étapes appliquées aux snapshots de diff (`diffs/<id>.json`), dans l'ordre. */
42
55
  export const SNAPSHOT_MIGRATIONS = [];
43
56
  /** Applique `steps` à la suite. Renvoie null si aucune étape n'a modifié le document. */
@@ -4,6 +4,9 @@ import type { AnalysisStore } from "./analysis-store.js";
4
4
  /**
5
5
  * Modifications faites par le relecteur depuis la GUI. Chacune passe par
6
6
  * `store.mutate(id, "user", …, { baseRevision })` et renvoie l'analyse écrite.
7
+ * Changer une remarque ou le statut d'un constat repasse une revue soumise en
8
+ * attente (voir `reopenSubmittedReview`) : son retour n'est plus à traiter tant
9
+ * qu'elle n'est pas soumise à nouveau.
7
10
  */
8
11
  /** Marque un fichier de l'analyse comme revu, ou le repasse à revoir. */
9
12
  export declare function setFileReviewed(store: AnalysisStore, analysisId: string, body: SetFileReviewedBody): Analysis;
@@ -18,18 +21,14 @@ export declare function setFindingStatus(store: AnalysisStore, analysisId: strin
18
21
  * liée au constat `findingId` quand elle y répond.
19
22
  */
20
23
  export declare function addNote(store: AnalysisStore, analysisId: string, body: AddNoteBody): Analysis;
21
- /**
22
- * Remplace le texte d'une remarque. Quand elle fait partie d'une revue soumise,
23
- * le prompt enregistré est recalculé avec le nouveau texte.
24
- */
24
+ /** Remplace le texte d'une remarque. */
25
25
  export declare function updateNote(store: AnalysisStore, analysisId: string, noteId: string, body: UpdateNoteBody): Analysis;
26
- /**
27
- * Supprime une remarque. Quand elle fait partie d'une revue soumise, elle est
28
- * retirée de la sélection et le prompt enregistré est recalculé sans elle.
29
- */
26
+ /** Supprime une remarque et la retire de la sélection de la revue. */
30
27
  export declare function deleteNote(store: AnalysisStore, analysisId: string, noteId: string, body: DeleteNoteBody): Analysis;
31
28
  /**
32
- * Soumet la revue : décision, points retenus et prompt calculé ici. Une
33
- * nouvelle soumission remplace la précédente.
29
+ * Soumet la revue : décision, points retenus et prompt calculé ici. La revue
30
+ * soumise est à traiter par l'agent. Une nouvelle soumission remplace la
31
+ * précédente, conservée dans `previousSubmission` ; une revue rouverte garde la
32
+ * version soumise qu'elle conservait déjà.
34
33
  */
35
34
  export declare function submitReview(store: AnalysisStore, analysisId: string, body: SubmitReviewBody): Analysis;
@@ -1,10 +1,14 @@
1
1
  import { buildAgentPrompt } from "../../shared/prompt.js";
2
+ import { recordSubmission, reopenSubmittedReview } from "../../shared/review-state.js";
2
3
  import { AnalysisError, NotFoundError } from "./errors.js";
3
4
  import { itemId } from "./ids.js";
4
5
  import { assertNoteLocation } from "./locations.js";
5
6
  /**
6
7
  * Modifications faites par le relecteur depuis la GUI. Chacune passe par
7
8
  * `store.mutate(id, "user", …, { baseRevision })` et renvoie l'analyse écrite.
9
+ * Changer une remarque ou le statut d'un constat repasse une revue soumise en
10
+ * attente (voir `reopenSubmittedReview`) : son retour n'est plus à traiter tant
11
+ * qu'elle n'est pas soumise à nouveau.
8
12
  */
9
13
  /** Marque un fichier de l'analyse comme revu, ou le repasse à revoir. */
10
14
  export function setFileReviewed(store, analysisId, body) {
@@ -31,6 +35,9 @@ export function setFindingStatus(store, analysisId, findingId, body) {
31
35
  if (finding.status === "outdated") {
32
36
  throw new AnalysisError(`Finding "${findingId}" is outdated: its lines changed since it was written. The agent must anchor it again with update_finding before it can be ignored or reopened.`);
33
37
  }
38
+ if (finding.status === body.status)
39
+ return;
40
+ reopenSubmittedReview(draft);
34
41
  finding.status = body.status;
35
42
  }, { baseRevision: body.baseRevision });
36
43
  }
@@ -46,6 +53,7 @@ export function addNote(store, analysisId, body) {
46
53
  if (findingId !== null && !draft.findings.some((finding) => finding.id === findingId)) {
47
54
  throw new NotFoundError(`Finding "${findingId}" not found in analysis "${analysisId}".`);
48
55
  }
56
+ reopenSubmittedReview(draft);
49
57
  draft.notes.push({
50
58
  id: itemId("note", new Set(draft.notes.map((note) => note.id))),
51
59
  location,
@@ -55,47 +63,27 @@ export function addNote(store, analysisId, body) {
55
63
  });
56
64
  }, { baseRevision: body.baseRevision });
57
65
  }
58
- /**
59
- * Remplace le texte d'une remarque. Quand elle fait partie d'une revue soumise,
60
- * le prompt enregistré est recalculé avec le nouveau texte.
61
- */
66
+ /** Remplace le texte d'une remarque. */
62
67
  export function updateNote(store, analysisId, noteId, body) {
63
68
  return store.mutate(analysisId, "user", (draft) => {
64
69
  const note = draft.notes.find((entry) => entry.id === noteId);
65
70
  if (!note)
66
71
  throw new NotFoundError(`Note "${noteId}" not found in analysis "${analysisId}".`);
72
+ if (note.text === body.text)
73
+ return;
74
+ reopenSubmittedReview(draft);
67
75
  note.text = body.text;
68
- const review = draft.review;
69
- if (review.state === "submitted" && review.selectedNoteIds.includes(noteId)) {
70
- review.prompt = buildAgentPrompt(draft, {
71
- findingIds: review.selectedFindingIds,
72
- noteIds: review.selectedNoteIds,
73
- decision: review.decision,
74
- });
75
- }
76
76
  }, { baseRevision: body.baseRevision });
77
77
  }
78
- /**
79
- * Supprime une remarque. Quand elle fait partie d'une revue soumise, elle est
80
- * retirée de la sélection et le prompt enregistré est recalculé sans elle.
81
- */
78
+ /** Supprime une remarque et la retire de la sélection de la revue. */
82
79
  export function deleteNote(store, analysisId, noteId, body) {
83
80
  return store.mutate(analysisId, "user", (draft) => {
84
81
  const index = draft.notes.findIndex((note) => note.id === noteId);
85
82
  if (index === -1)
86
83
  throw new NotFoundError(`Note "${noteId}" not found in analysis "${analysisId}".`);
84
+ reopenSubmittedReview(draft);
87
85
  draft.notes.splice(index, 1);
88
- const review = draft.review;
89
- if (!review.selectedNoteIds.includes(noteId))
90
- return;
91
- review.selectedNoteIds = review.selectedNoteIds.filter((id) => id !== noteId);
92
- if (review.state === "submitted") {
93
- review.prompt = buildAgentPrompt(draft, {
94
- findingIds: review.selectedFindingIds,
95
- noteIds: review.selectedNoteIds,
96
- decision: review.decision,
97
- });
98
- }
86
+ draft.review.selectedNoteIds = draft.review.selectedNoteIds.filter((id) => id !== noteId);
99
87
  }, { baseRevision: body.baseRevision });
100
88
  }
101
89
  function assertKnownIds(label, ids, known) {
@@ -108,13 +96,16 @@ function assertKnownIds(label, ids, known) {
108
96
  throw new AnalysisError(`The selection lists ${label} id(s) twice: ${[...new Set(duplicated)].join(", ")}.`);
109
97
  }
110
98
  /**
111
- * Soumet la revue : décision, points retenus et prompt calculé ici. Une
112
- * nouvelle soumission remplace la précédente.
99
+ * Soumet la revue : décision, points retenus et prompt calculé ici. La revue
100
+ * soumise est à traiter par l'agent. Une nouvelle soumission remplace la
101
+ * précédente, conservée dans `previousSubmission` ; une revue rouverte garde la
102
+ * version soumise qu'elle conservait déjà.
113
103
  */
114
104
  export function submitReview(store, analysisId, body) {
115
105
  return store.mutate(analysisId, "user", (draft) => {
116
106
  assertKnownIds("finding", body.findingIds, draft.findings.map((finding) => finding.id));
117
107
  assertKnownIds("note", body.noteIds, draft.notes.map((note) => note.id));
108
+ const previousSubmission = draft.review.state === "submitted" ? recordSubmission(draft) : draft.review.previousSubmission;
118
109
  draft.review = {
119
110
  state: "submitted",
120
111
  decision: body.decision,
@@ -122,6 +113,8 @@ export function submitReview(store, analysisId, body) {
122
113
  selectedNoteIds: [...body.noteIds],
123
114
  prompt: buildAgentPrompt(draft, { findingIds: body.findingIds, noteIds: body.noteIds, decision: body.decision }),
124
115
  submittedAt: new Date().toISOString(),
116
+ processed: null,
117
+ previousSubmission,
125
118
  };
126
119
  }, { baseRevision: body.baseRevision });
127
120
  }