@imenam/mcp-feature-analyzer 1.0.13 → 1.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +351 -350
- package/dist/shared/context-expansion.d.ts +20 -10
- package/dist/shared/context-expansion.js +69 -29
- package/dist/shared/inline-diff.d.ts +26 -0
- package/dist/shared/inline-diff.js +116 -0
- package/dist/shared/schemas/analysis.schema.d.ts +2 -0
- package/dist/shared/schemas/analysis.schema.js +2 -0
- package/dist/shared/schemas/api.schema.d.ts +6 -1
- package/dist/shared/schemas/api.schema.js +5 -1
- package/dist/src/core/invariants.d.ts +1 -0
- package/dist/src/core/invariants.js +6 -0
- package/dist/src/core/migrate.d.ts +2 -0
- package/dist/src/core/migrate.js +11 -1
- package/dist/src/core/review-actions.d.ts +4 -1
- package/dist/src/core/review-actions.js +9 -1
- package/dist/src/index.js +2 -1
- package/dist/src/tools/delete-findings.d.ts +3 -2
- package/dist/src/tools/delete-findings.js +10 -3
- package/gui/dist/assets/{angular-html-C8yUP2eH.js → angular-html-B6_NRnCN.js} +1 -1
- package/gui/dist/assets/{angular-ts-DzcU1T2K.js → angular-ts-B9erDs6c.js} +1 -1
- package/gui/dist/assets/{apl-Is94CrNC.js → apl-DjvcB9sT.js} +1 -1
- package/gui/dist/assets/{astro-CZVpOEMl.js → astro-CZhgIppg.js} +1 -1
- package/gui/dist/assets/{blade-B8DVzBXI.js → blade-e8oMNvMd.js} +1 -1
- package/gui/dist/assets/{c-DwvJqxYQ.js → c-BrK5UjTM.js} +1 -1
- package/gui/dist/assets/{chapel-B0A7h2gJ.js → chapel-BYqvw2uH.js} +1 -1
- package/gui/dist/assets/{cobol-ixuUpb8j.js → cobol-CwH5fqfp.js} +1 -1
- package/gui/dist/assets/{coffee-B6Gt7KW6.js → coffee-TLGXwX2-.js} +1 -1
- package/gui/dist/assets/{cpp-DdTMcCH3.js → cpp-BvSDADNj.js} +1 -1
- package/gui/dist/assets/{crystal-DtwB5KRO.js → crystal-iHjso4ux.js} +1 -1
- package/gui/dist/assets/{css-r05hkkmY.js → css-CBe1Qi0-.js} +1 -1
- package/gui/dist/assets/{edge-BUCnVtWw.js → edge-CyZLnGaJ.js} +1 -1
- package/gui/dist/assets/{elixir-Dj0uUBSU.js → elixir-D2tsTgpS.js} +1 -1
- package/gui/dist/assets/{elm-DX0-ItU4.js → elm-M0avAcen.js} +1 -1
- package/gui/dist/assets/{erb-BNe-5sal.js → erb-BLSscFWl.js} +1 -1
- package/gui/dist/assets/{git-rebase-xOi6nkoo.js → git-rebase-CqSWXxzq.js} +1 -1
- package/gui/dist/assets/{glimmer-js-DsDY8Yny.js → glimmer-js-SVN1ZH84.js} +1 -1
- package/gui/dist/assets/{glimmer-ts-Dy87T0gH.js → glimmer-ts-Cwgq9uxW.js} +1 -1
- package/gui/dist/assets/{glsl-C9ZlHNoz.js → glsl-BrioHYM5.js} +1 -1
- package/gui/dist/assets/{graphql-B4nECVov.js → graphql-DDdKMtYR.js} +1 -1
- package/gui/dist/assets/{hack-BSLHBRVi.js → hack-BHZ0geAY.js} +1 -1
- package/gui/dist/assets/{haml-D2SEwaEa.js → haml-WIiyynmN.js} +1 -1
- package/gui/dist/assets/{handlebars-Dd8lUU5h.js → handlebars-BQroB-k3.js} +1 -1
- package/gui/dist/assets/{html-vn9Wh61S.js → html-B79Efp59.js} +1 -1
- package/gui/dist/assets/{html-derivative-BeVUcZEC.js → html-derivative-QmHc8kDj.js} +1 -1
- package/gui/dist/assets/{http-CeXJu1sw.js → http-yRG3c5rf.js} +1 -1
- package/gui/dist/assets/{hurl-B1QPHVeP.js → hurl-v973ESCN.js} +1 -1
- package/gui/dist/assets/index-B6olMl4d.css +1 -0
- package/gui/dist/assets/{index-BTbIxBJv.js → index-O0__xdj-.js} +12 -12
- package/gui/dist/assets/{java-DCIVTTpQ.js → java-BVWDaPHa.js} +1 -1
- package/gui/dist/assets/{javascript-P08pLFac.js → javascript-F6vvVyv2.js} +1 -1
- package/gui/dist/assets/{jinja-BdCJo5BJ.js → jinja-DWb3onNx.js} +1 -1
- package/gui/dist/assets/{jison-Bz2hFddv.js → jison-BwYJk9jQ.js} +1 -1
- package/gui/dist/assets/{json-C8EM4NNA.js → json-D4GGWn-t.js} +1 -1
- package/gui/dist/assets/{jsx-CkeBmlYM.js → jsx-DRy2cnLL.js} +1 -1
- package/gui/dist/assets/{julia-DdRWaHNt.js → julia-DZbd-p26.js} +1 -1
- package/gui/dist/assets/{just-Bw9kWd7J.js → just-f5z-QSLh.js} +1 -1
- package/gui/dist/assets/{latex-8jtj5txb.js → latex-x5U08LY6.js} +1 -1
- package/gui/dist/assets/{liquid-1NrB1sFV.js → liquid-YlLxXWQU.js} +1 -1
- package/gui/dist/assets/{lua-zyROpOUJ.js → lua-BETZr4ZG.js} +1 -1
- package/gui/dist/assets/{marko-B4Gx1REj.js → marko-Ci2LOIA3.js} +1 -1
- package/gui/dist/assets/{mdc-hlQzSEED.js → mdc-CNgtEUaF.js} +1 -1
- package/gui/dist/assets/{nginx-2oeh5xP6.js → nginx-CjN1FzJ1.js} +1 -1
- package/gui/dist/assets/{nim-5hoXcnXR.js → nim--Tw7Q7l1.js} +1 -1
- package/gui/dist/assets/{org-0LYtlaYk.js → org-D-0ign4q.js} +1 -1
- package/gui/dist/assets/{perl-DqUdWKZG.js → perl-CpGWpAqK.js} +1 -1
- package/gui/dist/assets/{php-BC56Y6Zn.js → php-DZMELnJP.js} +1 -1
- package/gui/dist/assets/{pug-Do_wQNPI.js → pug-DnnE3CD9.js} +1 -1
- package/gui/dist/assets/{qml-BwknWxId.js → qml-1YULs32S.js} +1 -1
- package/gui/dist/assets/{r-C6bHxkgQ.js → r-BKebVmGE.js} +1 -1
- package/gui/dist/assets/{razor-BEyuM7MM.js → razor-B6Rp2GI9.js} +1 -1
- package/gui/dist/assets/{regexp-CxH6WQ4V.js → regexp-BSdZUJch.js} +1 -1
- package/gui/dist/assets/{rst-DVGF9bH2.js → rst-CDfh7Yx8.js} +1 -1
- package/gui/dist/assets/{ruby-DHmaoIVS.js → ruby-DH2yECCv.js} +1 -1
- package/gui/dist/assets/{sas-h0gvSTcH.js → sas-D4xSfkgc.js} +1 -1
- package/gui/dist/assets/{scss-BnHxKO0L.js → scss-DKXhKwKk.js} +1 -1
- package/gui/dist/assets/{shellscript-C8GolBeA.js → shellscript-Cdvelqxj.js} +1 -1
- package/gui/dist/assets/{shellsession-BRGv8dCp.js → shellsession-BIsjLWb8.js} +1 -1
- package/gui/dist/assets/{soy-kgRIk1Av.js → soy-DAHoyBkO.js} +1 -1
- package/gui/dist/assets/{sql-D2zf6QGW.js → sql-Cw_MygQA.js} +1 -1
- package/gui/dist/assets/{stata-Cv5nQVjB.js → stata-XN5LKohx.js} +1 -1
- package/gui/dist/assets/{surrealql-Dyy-Mx5h.js → surrealql-BFZZr6ln.js} +1 -1
- package/gui/dist/assets/{svelte-Qv7HyAj6.js → svelte-PSD0_NIJ.js} +1 -1
- package/gui/dist/assets/{templ-GwFzX2N-.js → templ-DR1BQIMZ.js} +1 -1
- package/gui/dist/assets/{tex-BwmU6aP3.js → tex-FZ5ZE2Wi.js} +1 -1
- package/gui/dist/assets/{ts-tags-BdoQY3Va.js → ts-tags-J0uFOrqJ.js} +1 -1
- package/gui/dist/assets/{tsx-DQhkUiIl.js → tsx-CKQtNFtf.js} +1 -1
- package/gui/dist/assets/{twig-DypZ0Z28.js → twig-BaV21FxL.js} +1 -1
- package/gui/dist/assets/{typescript-BwW-Y5sm.js → typescript-DrRvnRdv.js} +1 -1
- package/gui/dist/assets/{typst-YWpvbo8x.js → typst-CyyoMVtl.js} +1 -1
- package/gui/dist/assets/{vue-C6O-uzDz.js → vue-C6CzC5nh.js} +1 -1
- package/gui/dist/assets/{vue-html-C3DSxeyo.js → vue-html-BhqRmn6o.js} +1 -1
- package/gui/dist/assets/{vue-vine-TAhDNl0X.js → vue-vine-D-akwyey.js} +1 -1
- package/gui/dist/assets/{xml-B2vOHzhB.js → xml-BSC8IfpC.js} +1 -1
- package/gui/dist/assets/{xsl-DlDYp1Ks.js → xsl-hlSzALu7.js} +1 -1
- package/gui/dist/assets/{yaml-BegRJSBb.js → yaml-vbTiXN5G.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/gui/dist/assets/index-oME7W1eA.css +0 -1
|
@@ -1,29 +1,39 @@
|
|
|
1
1
|
import type { FileDiff } from "./schemas/diff.schema.js";
|
|
2
2
|
/** Nombre de lignes de contexte ajoutées, ou retirées, au-dessus et en dessous d'un bloc par clic. */
|
|
3
3
|
export declare const CONTEXT_STEP = 5;
|
|
4
|
-
/** État du contexte d'un bloc
|
|
4
|
+
/** État du contexte d'un bloc affiché. */
|
|
5
5
|
export interface HunkContext {
|
|
6
6
|
/** Lignes de contexte affichées au-dessus de la première ligne modifiée du bloc. */
|
|
7
7
|
context: number;
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
/** Indices des blocs du diff d'origine réunis dans ce bloc. */
|
|
9
|
+
members: number[];
|
|
10
|
+
/** Le bloc porte des lignes de contexte ajoutées. */
|
|
11
|
+
canShrink: boolean;
|
|
11
12
|
/** Le fichier a encore des lignes à ajouter au-dessus ou en dessous du bloc. */
|
|
12
13
|
canGrow: boolean;
|
|
13
14
|
}
|
|
14
15
|
export interface ExpandedDiff {
|
|
15
16
|
fileDiff: FileDiff;
|
|
16
|
-
/** Contexte de chaque bloc
|
|
17
|
+
/** Contexte de chaque bloc affiché, dans l'ordre des blocs. */
|
|
17
18
|
hunks: HunkContext[];
|
|
19
|
+
/** Lignes ajoutées autour de chaque bloc du diff d'origine, du côté qui en compte le plus. */
|
|
20
|
+
extras: number[];
|
|
18
21
|
/** Numéros, côté « nouveau », des lignes de contexte ajoutées. */
|
|
19
22
|
added: Set<number>;
|
|
20
23
|
}
|
|
21
24
|
/**
|
|
22
25
|
* Diff de `fileDiff` dont chaque bloc `i` gagne jusqu'à `extras[i]` lignes de
|
|
23
|
-
* contexte au-dessus et autant en dessous, tirées de `newContent`. Les
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
* contexte au-dessus et autant en dessous, tirées de `newContent`. Les lignes
|
|
27
|
+
* entre deux blocs vont d'abord au bloc du dessus, puis au bloc du dessous,
|
|
28
|
+
* sans jamais se recouvrir ; le début et la fin du fichier bornent aussi
|
|
29
|
+
* l'élargissement. Deux blocs que plus aucune ligne masquée ne sépare n'en
|
|
30
|
+
* forment plus qu'un. Un bloc sans ligne ajoutée ni voisin réuni reste
|
|
31
|
+
* inchangé. Null quand le snapshot ne conserve pas le contenu du fichier.
|
|
28
32
|
*/
|
|
29
33
|
export declare function expandContext(fileDiff: FileDiff, extras: readonly number[]): ExpandedDiff | null;
|
|
34
|
+
/**
|
|
35
|
+
* Élargissement de chaque bloc d'origine après un clic sur « +5 » (`delta`
|
|
36
|
+
* `1`) ou « −5 » (`-1`) dans l'en-tête du bloc affiché `hunkIndex` : chacun
|
|
37
|
+
* des blocs qu'il réunit gagne ou perd une tranche de lignes.
|
|
38
|
+
*/
|
|
39
|
+
export declare function stepContext(expanded: ExpandedDiff, hunkIndex: number, delta: 1 | -1): number[];
|
|
@@ -5,6 +5,20 @@ export const CONTEXT_STEP = 5;
|
|
|
5
5
|
function headerRange(start, count) {
|
|
6
6
|
return count === 1 ? `${start}` : `${start},${count}`;
|
|
7
7
|
}
|
|
8
|
+
/** Bloc couvrant les plages données, en-tête recalculé à partir de celui de `source`. */
|
|
9
|
+
function rangedHunk(source, oldStart, newStart, lines) {
|
|
10
|
+
const oldLines = lines.filter((line) => line.type !== "add").length;
|
|
11
|
+
const newLines = lines.filter((line) => line.type !== "del").length;
|
|
12
|
+
const rest = source.header.replace(/^@@[^@]*@@/, "");
|
|
13
|
+
return {
|
|
14
|
+
header: `@@ -${headerRange(oldStart, oldLines)} +${headerRange(newStart, newLines)} @@${rest}`,
|
|
15
|
+
oldStart,
|
|
16
|
+
oldLines,
|
|
17
|
+
newStart,
|
|
18
|
+
newLines,
|
|
19
|
+
lines,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
8
22
|
/** Nombre de lignes de contexte en tête du bloc, avant sa première ligne modifiée. */
|
|
9
23
|
function leadingContext(hunk) {
|
|
10
24
|
const index = hunk.lines.findIndex((line) => line.type !== "context");
|
|
@@ -12,19 +26,19 @@ function leadingContext(hunk) {
|
|
|
12
26
|
}
|
|
13
27
|
/**
|
|
14
28
|
* Diff de `fileDiff` dont chaque bloc `i` gagne jusqu'à `extras[i]` lignes de
|
|
15
|
-
* contexte au-dessus et autant en dessous, tirées de `newContent`. Les
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
29
|
+
* contexte au-dessus et autant en dessous, tirées de `newContent`. Les lignes
|
|
30
|
+
* entre deux blocs vont d'abord au bloc du dessus, puis au bloc du dessous,
|
|
31
|
+
* sans jamais se recouvrir ; le début et la fin du fichier bornent aussi
|
|
32
|
+
* l'élargissement. Deux blocs que plus aucune ligne masquée ne sépare n'en
|
|
33
|
+
* forment plus qu'un. Un bloc sans ligne ajoutée ni voisin réuni reste
|
|
34
|
+
* inchangé. Null quand le snapshot ne conserve pas le contenu du fichier.
|
|
20
35
|
*/
|
|
21
36
|
export function expandContext(fileDiff, extras) {
|
|
22
37
|
if (fileDiff.newContent === null)
|
|
23
38
|
return null;
|
|
24
39
|
const content = splitLines(fileDiff.newContent);
|
|
25
40
|
const added = new Set();
|
|
26
|
-
const
|
|
27
|
-
const hunks = [];
|
|
41
|
+
const parts = [];
|
|
28
42
|
// Écart entre les numéros des deux côtés avant le bloc courant, et dernière ligne déjà affichée.
|
|
29
43
|
let offset = 0;
|
|
30
44
|
let shownUntil = 0;
|
|
@@ -44,32 +58,58 @@ export function expandContext(fileDiff, extras) {
|
|
|
44
58
|
added.add(newNo);
|
|
45
59
|
return { type: "context", oldNo: newNo + shift, newNo, text: content[newNo - 1] };
|
|
46
60
|
};
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
else {
|
|
51
|
-
const lines = [
|
|
61
|
+
const expanded = above === 0 && below === 0
|
|
62
|
+
? hunk
|
|
63
|
+
: rangedHunk(hunk, (hunk.oldLines === 0 ? hunk.oldStart + 1 : hunk.oldStart) - above, first - above, [
|
|
52
64
|
...Array.from({ length: above }, (_, k) => contextLine(first - above + k, offset)),
|
|
53
65
|
...hunk.lines,
|
|
54
66
|
...Array.from({ length: below }, (_, k) => contextLine(last + 1 + k, after)),
|
|
55
|
-
];
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
oldStart,
|
|
64
|
-
oldLines,
|
|
65
|
-
newStart,
|
|
66
|
-
newLines,
|
|
67
|
-
lines,
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
contexts.push({ context: leadingContext(hunk) + above, above, below, canGrow: above < aboveRoom || below < belowRoom });
|
|
67
|
+
]);
|
|
68
|
+
parts.push({
|
|
69
|
+
hunk: expanded,
|
|
70
|
+
above,
|
|
71
|
+
below,
|
|
72
|
+
canGrow: above < aboveRoom || below < belowRoom,
|
|
73
|
+
joinsPrevious: index > 0 && above === aboveRoom,
|
|
74
|
+
});
|
|
71
75
|
offset = after;
|
|
72
76
|
shownUntil = last + below;
|
|
73
77
|
});
|
|
74
|
-
|
|
78
|
+
const groups = [];
|
|
79
|
+
parts.forEach((part, index) => {
|
|
80
|
+
if (part.joinsPrevious)
|
|
81
|
+
groups[groups.length - 1].push(index);
|
|
82
|
+
else
|
|
83
|
+
groups.push([index]);
|
|
84
|
+
});
|
|
85
|
+
const hunks = groups.map((members) => {
|
|
86
|
+
if (members.length === 1)
|
|
87
|
+
return parts[members[0]].hunk;
|
|
88
|
+
const head = parts[members[0]].hunk;
|
|
89
|
+
return rangedHunk(head, head.oldLines === 0 ? head.oldStart + 1 : head.oldStart, head.newLines === 0 ? head.newStart + 1 : head.newStart, members.flatMap((member) => parts[member].hunk.lines));
|
|
90
|
+
});
|
|
91
|
+
const contexts = groups.map((members) => ({
|
|
92
|
+
context: leadingContext(fileDiff.hunks[members[0]]) + parts[members[0]].above,
|
|
93
|
+
members,
|
|
94
|
+
canShrink: members.some((member) => parts[member].above > 0 || parts[member].below > 0),
|
|
95
|
+
canGrow: members.some((member) => parts[member].canGrow),
|
|
96
|
+
}));
|
|
97
|
+
return {
|
|
98
|
+
fileDiff: { ...fileDiff, hunks },
|
|
99
|
+
hunks: contexts,
|
|
100
|
+
extras: parts.map((part) => Math.max(part.above, part.below)),
|
|
101
|
+
added,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Élargissement de chaque bloc d'origine après un clic sur « +5 » (`delta`
|
|
106
|
+
* `1`) ou « −5 » (`-1`) dans l'en-tête du bloc affiché `hunkIndex` : chacun
|
|
107
|
+
* des blocs qu'il réunit gagne ou perd une tranche de lignes.
|
|
108
|
+
*/
|
|
109
|
+
export function stepContext(expanded, hunkIndex, delta) {
|
|
110
|
+
const next = [...expanded.extras];
|
|
111
|
+
for (const member of expanded.hunks[hunkIndex].members) {
|
|
112
|
+
next[member] = Math.max(0, next[member] + delta * CONTEXT_STEP);
|
|
113
|
+
}
|
|
114
|
+
return next;
|
|
75
115
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { DiffLine } from "./schemas/diff.schema.js";
|
|
2
|
+
import type { MergedLine } from "./reindent.js";
|
|
3
|
+
/** Plage de caractères d'une ligne, `start` inclus, `end` exclu. */
|
|
4
|
+
export interface CharRange {
|
|
5
|
+
start: number;
|
|
6
|
+
end: number;
|
|
7
|
+
}
|
|
8
|
+
/** Parties d'une ligne supprimée et de la ligne ajoutée qui la remplace qui diffèrent de l'autre. */
|
|
9
|
+
export interface InlineChanges {
|
|
10
|
+
removed: CharRange[];
|
|
11
|
+
added: CharRange[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Parties qui diffèrent entre une ligne supprimée et la ligne ajoutée qui la
|
|
15
|
+
* remplace, comparées mot à mot. Null quand les lignes sont identiques, trop
|
|
16
|
+
* longues, ou trop différentes pour qu'un surlignage partiel ait un sens :
|
|
17
|
+
* moins de la moitié de leurs mots, hors espaces, en commun.
|
|
18
|
+
*/
|
|
19
|
+
export declare function inlineChanges(oldText: string, newText: string): InlineChanges | null;
|
|
20
|
+
/**
|
|
21
|
+
* Parties modifiées des lignes d'un bloc. Dans chaque suite de lignes supprimées
|
|
22
|
+
* suivie de lignes ajoutées, hors lignes dont seule l'indentation change, la
|
|
23
|
+
* i-ème ligne supprimée est comparée à la i-ème ligne ajoutée. Seules les lignes
|
|
24
|
+
* dont la comparaison aboutit figurent dans le résultat.
|
|
25
|
+
*/
|
|
26
|
+
export declare function inlineChangesOfLines(lines: MergedLine[]): Map<DiffLine, CharRange[]>;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/** Nombre maximal de mots d'une ligne au-delà duquel la comparaison n'est pas faite. */
|
|
2
|
+
const MAX_TOKENS = 400;
|
|
3
|
+
/** Part minimale des mots, hors espaces, communs aux deux lignes pour les comparer mot à mot. */
|
|
4
|
+
const MIN_SIMILARITY = 0.5;
|
|
5
|
+
/** Mots d'une ligne : identifiants et nombres, suites d'espaces, et chaque autre caractère seul. */
|
|
6
|
+
function tokenize(text) {
|
|
7
|
+
const tokens = [];
|
|
8
|
+
for (const match of text.matchAll(/[\p{L}\p{N}_$]+|\s+|[^\p{L}\p{N}_$\s]/gu)) {
|
|
9
|
+
tokens.push({ text: match[0], start: match.index });
|
|
10
|
+
}
|
|
11
|
+
return tokens;
|
|
12
|
+
}
|
|
13
|
+
function isBlank(token) {
|
|
14
|
+
return token.text.trim() === "";
|
|
15
|
+
}
|
|
16
|
+
/** Indices des mots de `a` et de `b` qui appartiennent à leur plus longue suite commune. */
|
|
17
|
+
function commonTokens(a, b) {
|
|
18
|
+
const width = b.length + 1;
|
|
19
|
+
// lengths[i * width + j] : longueur de la plus longue suite commune de a[i..] et b[j..].
|
|
20
|
+
const lengths = new Uint32Array((a.length + 1) * width);
|
|
21
|
+
for (let i = a.length - 1; i >= 0; i--) {
|
|
22
|
+
for (let j = b.length - 1; j >= 0; j--) {
|
|
23
|
+
lengths[i * width + j] =
|
|
24
|
+
a[i].text === b[j].text ? lengths[(i + 1) * width + j + 1] + 1 : Math.max(lengths[(i + 1) * width + j], lengths[i * width + j + 1]);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const inA = new Set();
|
|
28
|
+
const inB = new Set();
|
|
29
|
+
let i = 0;
|
|
30
|
+
let j = 0;
|
|
31
|
+
while (i < a.length && j < b.length) {
|
|
32
|
+
if (a[i].text === b[j].text) {
|
|
33
|
+
inA.add(i++);
|
|
34
|
+
inB.add(j++);
|
|
35
|
+
}
|
|
36
|
+
else if (lengths[(i + 1) * width + j] >= lengths[i * width + j + 1])
|
|
37
|
+
i++;
|
|
38
|
+
else
|
|
39
|
+
j++;
|
|
40
|
+
}
|
|
41
|
+
return { inA, inB };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Plages couvertes par les mots absents de `common`, les mots voisins réunis en
|
|
45
|
+
* une plage ; les espaces en bord de plage n'en font pas partie.
|
|
46
|
+
*/
|
|
47
|
+
function changedRanges(tokens, common) {
|
|
48
|
+
const runs = [];
|
|
49
|
+
tokens.forEach((token, index) => {
|
|
50
|
+
if (common.has(index))
|
|
51
|
+
return;
|
|
52
|
+
const last = runs[runs.length - 1];
|
|
53
|
+
if (last && last[last.length - 1] === tokens[index - 1])
|
|
54
|
+
last.push(token);
|
|
55
|
+
else
|
|
56
|
+
runs.push([token]);
|
|
57
|
+
});
|
|
58
|
+
return runs.flatMap((run) => {
|
|
59
|
+
const first = run.findIndex((token) => !isBlank(token));
|
|
60
|
+
if (first === -1)
|
|
61
|
+
return [];
|
|
62
|
+
const last = run.length - 1 - [...run].reverse().findIndex((token) => !isBlank(token));
|
|
63
|
+
return [{ start: run[first].start, end: run[last].start + run[last].text.length }];
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Parties qui diffèrent entre une ligne supprimée et la ligne ajoutée qui la
|
|
68
|
+
* remplace, comparées mot à mot. Null quand les lignes sont identiques, trop
|
|
69
|
+
* longues, ou trop différentes pour qu'un surlignage partiel ait un sens :
|
|
70
|
+
* moins de la moitié de leurs mots, hors espaces, en commun.
|
|
71
|
+
*/
|
|
72
|
+
export function inlineChanges(oldText, newText) {
|
|
73
|
+
if (oldText === newText)
|
|
74
|
+
return null;
|
|
75
|
+
const oldTokens = tokenize(oldText);
|
|
76
|
+
const newTokens = tokenize(newText);
|
|
77
|
+
if (oldTokens.length > MAX_TOKENS || newTokens.length > MAX_TOKENS)
|
|
78
|
+
return null;
|
|
79
|
+
const { inA, inB } = commonTokens(oldTokens, newTokens);
|
|
80
|
+
const shared = [...inA].filter((index) => !isBlank(oldTokens[index])).length;
|
|
81
|
+
const longest = Math.max(oldTokens.filter((token) => !isBlank(token)).length, newTokens.filter((token) => !isBlank(token)).length);
|
|
82
|
+
if (longest === 0 || shared / longest < MIN_SIMILARITY)
|
|
83
|
+
return null;
|
|
84
|
+
return { removed: changedRanges(oldTokens, inA), added: changedRanges(newTokens, inB) };
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Parties modifiées des lignes d'un bloc. Dans chaque suite de lignes supprimées
|
|
88
|
+
* suivie de lignes ajoutées, hors lignes dont seule l'indentation change, la
|
|
89
|
+
* i-ème ligne supprimée est comparée à la i-ème ligne ajoutée. Seules les lignes
|
|
90
|
+
* dont la comparaison aboutit figurent dans le résultat.
|
|
91
|
+
*/
|
|
92
|
+
export function inlineChangesOfLines(lines) {
|
|
93
|
+
const result = new Map();
|
|
94
|
+
const single = (index, type) => lines[index]?.oldLine === null && lines[index].line.type === type;
|
|
95
|
+
let index = 0;
|
|
96
|
+
while (index < lines.length) {
|
|
97
|
+
if (!single(index, "del")) {
|
|
98
|
+
index++;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const removed = [];
|
|
102
|
+
const added = [];
|
|
103
|
+
while (single(index, "del"))
|
|
104
|
+
removed.push(lines[index++].line);
|
|
105
|
+
while (single(index, "add"))
|
|
106
|
+
added.push(lines[index++].line);
|
|
107
|
+
for (let pair = 0; pair < Math.min(removed.length, added.length); pair++) {
|
|
108
|
+
const changes = inlineChanges(removed[pair].text, added[pair].text);
|
|
109
|
+
if (!changes)
|
|
110
|
+
continue;
|
|
111
|
+
result.set(removed[pair], changes.removed);
|
|
112
|
+
result.set(added[pair], changes.added);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
@@ -114,6 +114,7 @@ export declare const NoteSchema: z.ZodObject<{
|
|
|
114
114
|
path: z.ZodString;
|
|
115
115
|
line: z.ZodNullable<z.ZodNumber>;
|
|
116
116
|
}, z.core.$strip>>;
|
|
117
|
+
findingId: z.ZodNullable<z.ZodString>;
|
|
117
118
|
text: z.ZodString;
|
|
118
119
|
createdAt: z.ZodString;
|
|
119
120
|
}, z.core.$strip>;
|
|
@@ -310,6 +311,7 @@ export declare const AnalysisSchema: z.ZodObject<{
|
|
|
310
311
|
path: z.ZodString;
|
|
311
312
|
line: z.ZodNullable<z.ZodNumber>;
|
|
312
313
|
}, z.core.$strip>>;
|
|
314
|
+
findingId: z.ZodNullable<z.ZodString>;
|
|
313
315
|
text: z.ZodString;
|
|
314
316
|
createdAt: z.ZodString;
|
|
315
317
|
}, z.core.$strip>>;
|
|
@@ -88,6 +88,8 @@ export const NoteLocationSchema = z.object({
|
|
|
88
88
|
export const NoteSchema = z.object({
|
|
89
89
|
id: ItemIdSchema,
|
|
90
90
|
location: NoteLocationSchema.nullable(),
|
|
91
|
+
/** Constat auquel la remarque répond ; null pour une remarque libre. Supprimer le constat supprime la remarque. */
|
|
92
|
+
findingId: ItemIdSchema.nullable(),
|
|
91
93
|
text: z.string().min(1),
|
|
92
94
|
createdAt: IsoDateSchema,
|
|
93
95
|
});
|
|
@@ -12,10 +12,14 @@ export declare const SetFindingStatusBodySchema: z.ZodObject<{
|
|
|
12
12
|
}>;
|
|
13
13
|
baseRevision: z.ZodNumber;
|
|
14
14
|
}, z.core.$strict>;
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Remarque sur une ligne du côté « nouveau » (`line`), ou sur le fichier entier (`line` absent ou null).
|
|
17
|
+
* `findingId` lie la remarque au constat auquel elle répond.
|
|
18
|
+
*/
|
|
16
19
|
export declare const AddNoteBodySchema: z.ZodObject<{
|
|
17
20
|
path: z.ZodString;
|
|
18
21
|
line: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
22
|
+
findingId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
19
23
|
text: z.ZodString;
|
|
20
24
|
baseRevision: z.ZodNumber;
|
|
21
25
|
}, z.core.$strict>;
|
|
@@ -71,6 +75,7 @@ export declare const AddNoteRequestSchema: z.ZodObject<{
|
|
|
71
75
|
body: z.ZodObject<{
|
|
72
76
|
path: z.ZodString;
|
|
73
77
|
line: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
78
|
+
findingId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
74
79
|
text: z.ZodString;
|
|
75
80
|
baseRevision: z.ZodNumber;
|
|
76
81
|
}, z.core.$strict>;
|
|
@@ -15,10 +15,14 @@ export const SetFindingStatusBodySchema = z.strictObject({
|
|
|
15
15
|
status: z.enum(FINDING_STATUSES),
|
|
16
16
|
baseRevision: BaseRevisionSchema,
|
|
17
17
|
});
|
|
18
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Remarque sur une ligne du côté « nouveau » (`line`), ou sur le fichier entier (`line` absent ou null).
|
|
20
|
+
* `findingId` lie la remarque au constat auquel elle répond.
|
|
21
|
+
*/
|
|
19
22
|
export const AddNoteBodySchema = z.strictObject({
|
|
20
23
|
path: z.string().min(1),
|
|
21
24
|
line: z.number().int().nullable().optional(),
|
|
25
|
+
findingId: z.string().min(1).nullable().optional(),
|
|
22
26
|
text: z.string().trim().min(1, "the note text is empty"),
|
|
23
27
|
baseRevision: BaseRevisionSchema,
|
|
24
28
|
});
|
|
@@ -13,6 +13,7 @@ import type { Analysis } from "../../shared/schemas/analysis.schema.js";
|
|
|
13
13
|
* disponible et endLine ≤ nombre de lignes ;
|
|
14
14
|
* - les liens d'un schéma relient des nœuds existants ; en `layers`, chaque nœud a une couche
|
|
15
15
|
* déclarée dans `layers`, sans doublon ;
|
|
16
|
+
* - une remarque liée à un constat vise un constat existant ;
|
|
16
17
|
* - la revue ne sélectionne que des constats et remarques existants, et son état est cohérent.
|
|
17
18
|
*/
|
|
18
19
|
export declare function findInvariantViolations(analysis: Analysis): string[];
|
|
@@ -24,6 +24,7 @@ function duplicates(ids) {
|
|
|
24
24
|
* disponible et endLine ≤ nombre de lignes ;
|
|
25
25
|
* - les liens d'un schéma relient des nœuds existants ; en `layers`, chaque nœud a une couche
|
|
26
26
|
* déclarée dans `layers`, sans doublon ;
|
|
27
|
+
* - une remarque liée à un constat vise un constat existant ;
|
|
27
28
|
* - la revue ne sélectionne que des constats et remarques existants, et son état est cohérent.
|
|
28
29
|
*/
|
|
29
30
|
export function findInvariantViolations(analysis) {
|
|
@@ -136,6 +137,11 @@ export function findInvariantViolations(analysis) {
|
|
|
136
137
|
if (!noteIds.has(id))
|
|
137
138
|
problems.push(`review selects unknown note "${id}"`);
|
|
138
139
|
}
|
|
140
|
+
for (const note of analysis.notes) {
|
|
141
|
+
if (note.findingId !== null && !findingIds.has(note.findingId)) {
|
|
142
|
+
problems.push(`note "${note.id}" answers unknown finding "${note.findingId}"`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
139
145
|
if (review.state === "submitted" && (review.decision === null || review.submittedAt === null)) {
|
|
140
146
|
problems.push("a submitted review needs a decision and a submittedAt date");
|
|
141
147
|
}
|
|
@@ -14,6 +14,8 @@ export declare const addProject: MigrationStep;
|
|
|
14
14
|
export declare const addOverview: MigrationStep;
|
|
15
15
|
/** Ajoute `explanations` vide : l'analyse n'a pas encore d'explication. */
|
|
16
16
|
export declare const addExplanations: MigrationStep;
|
|
17
|
+
/** Ajoute `findingId` à null sur les remarques qui n'en ont pas : elles restent des remarques libres. */
|
|
18
|
+
export declare const addNoteFindingIds: MigrationStep;
|
|
17
19
|
/** Étapes appliquées aux analyses (`analyses/<id>.json`), dans l'ordre. */
|
|
18
20
|
export declare const ANALYSIS_MIGRATIONS: readonly MigrationStep[];
|
|
19
21
|
/** Étapes appliquées aux snapshots de diff (`diffs/<id>.json`), dans l'ordre. */
|
package/dist/src/core/migrate.js
CHANGED
|
@@ -26,8 +26,18 @@ export const addProject = (doc) => {
|
|
|
26
26
|
export const addOverview = (doc) => (doc.overview !== undefined ? null : { ...doc, overview: null });
|
|
27
27
|
/** Ajoute `explanations` vide : l'analyse n'a pas encore d'explication. */
|
|
28
28
|
export const addExplanations = (doc) => (doc.explanations !== undefined ? null : { ...doc, explanations: [] });
|
|
29
|
+
/** Ajoute `findingId` à null sur les remarques qui n'en ont pas : elles restent des remarques libres. */
|
|
30
|
+
export const addNoteFindingIds = (doc) => {
|
|
31
|
+
if (!Array.isArray(doc.notes))
|
|
32
|
+
return null;
|
|
33
|
+
const notes = doc.notes;
|
|
34
|
+
const missing = (note) => !!note && typeof note === "object" && note.findingId === undefined;
|
|
35
|
+
if (!notes.some(missing))
|
|
36
|
+
return null;
|
|
37
|
+
return { ...doc, notes: notes.map((note) => (missing(note) ? { ...note, findingId: null } : note)) };
|
|
38
|
+
};
|
|
29
39
|
/** Étapes appliquées aux analyses (`analyses/<id>.json`), dans l'ordre. */
|
|
30
|
-
export const ANALYSIS_MIGRATIONS = [addProject, addOverview, addExplanations];
|
|
40
|
+
export const ANALYSIS_MIGRATIONS = [addProject, addOverview, addExplanations, addNoteFindingIds];
|
|
31
41
|
/** Étapes appliquées aux snapshots de diff (`diffs/<id>.json`), dans l'ordre. */
|
|
32
42
|
export const SNAPSHOT_MIGRATIONS = [];
|
|
33
43
|
/** Applique `steps` à la suite. Renvoie null si aucune étape n'a modifié le document. */
|
|
@@ -13,7 +13,10 @@ export declare function setFileReviewed(store: AnalysisStore, analysisId: string
|
|
|
13
13
|
* le réancre.
|
|
14
14
|
*/
|
|
15
15
|
export declare function setFindingStatus(store: AnalysisStore, analysisId: string, findingId: string, body: SetFindingStatusBody): Analysis;
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Ajoute une remarque du relecteur sur une ligne du côté « nouveau » ou sur un fichier entier,
|
|
18
|
+
* liée au constat `findingId` quand elle y répond.
|
|
19
|
+
*/
|
|
17
20
|
export declare function addNote(store: AnalysisStore, analysisId: string, body: AddNoteBody): Analysis;
|
|
18
21
|
/**
|
|
19
22
|
* Remplace le texte d'une remarque. Quand elle fait partie d'une revue soumise,
|
|
@@ -34,14 +34,22 @@ export function setFindingStatus(store, analysisId, findingId, body) {
|
|
|
34
34
|
finding.status = body.status;
|
|
35
35
|
}, { baseRevision: body.baseRevision });
|
|
36
36
|
}
|
|
37
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Ajoute une remarque du relecteur sur une ligne du côté « nouveau » ou sur un fichier entier,
|
|
39
|
+
* liée au constat `findingId` quand elle y répond.
|
|
40
|
+
*/
|
|
38
41
|
export function addNote(store, analysisId, body) {
|
|
39
42
|
return store.mutate(analysisId, "user", (draft) => {
|
|
40
43
|
const location = { path: body.path, line: body.line ?? null };
|
|
41
44
|
assertNoteLocation(draft.files, location);
|
|
45
|
+
const findingId = body.findingId ?? null;
|
|
46
|
+
if (findingId !== null && !draft.findings.some((finding) => finding.id === findingId)) {
|
|
47
|
+
throw new NotFoundError(`Finding "${findingId}" not found in analysis "${analysisId}".`);
|
|
48
|
+
}
|
|
42
49
|
draft.notes.push({
|
|
43
50
|
id: itemId("note", new Set(draft.notes.map((note) => note.id))),
|
|
44
51
|
location,
|
|
52
|
+
findingId,
|
|
45
53
|
text: body.text,
|
|
46
54
|
createdAt: new Date().toISOString(),
|
|
47
55
|
});
|
package/dist/src/index.js
CHANGED
|
@@ -523,7 +523,8 @@ const TOOLS = [
|
|
|
523
523
|
},
|
|
524
524
|
{
|
|
525
525
|
name: "delete_findings",
|
|
526
|
-
description: "Delete findings by id, typically once refresh_analysis shows them solved.
|
|
526
|
+
description: "Delete findings by id, typically once refresh_analysis shows them solved. The reviewer notes written on these findings are deleted with them. " +
|
|
527
|
+
"If any id is unknown, nothing is deleted.",
|
|
527
528
|
inputSchema: {
|
|
528
529
|
type: "object",
|
|
529
530
|
properties: {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { AnalysisStore } from "../core/analysis-store.js";
|
|
2
2
|
import { type Args, type MutationResult } from "./types.js";
|
|
3
3
|
/**
|
|
4
|
-
* Supprime des constats par identifiant
|
|
5
|
-
*
|
|
4
|
+
* Supprime des constats par identifiant, avec les remarques du relecteur qui
|
|
5
|
+
* leur répondent, et les retire de la sélection de la revue. Un identifiant
|
|
6
|
+
* inconnu fait échouer l'appel sans rien supprimer.
|
|
6
7
|
*/
|
|
7
8
|
export declare function deleteFindings(store: AnalysisStore, args: Args): MutationResult;
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { AnalysisError } from "../core/errors.js";
|
|
2
2
|
import { rejectUnknownFields, requireString, requireStringArray, textResult } from "./types.js";
|
|
3
3
|
/**
|
|
4
|
-
* Supprime des constats par identifiant
|
|
5
|
-
*
|
|
4
|
+
* Supprime des constats par identifiant, avec les remarques du relecteur qui
|
|
5
|
+
* leur répondent, et les retire de la sélection de la revue. Un identifiant
|
|
6
|
+
* inconnu fait échouer l'appel sans rien supprimer.
|
|
6
7
|
*/
|
|
7
8
|
export function deleteFindings(store, args) {
|
|
8
9
|
rejectUnknownFields(args, ["analysis_id", "finding_ids"]);
|
|
9
10
|
const id = requireString(args, "analysis_id");
|
|
10
11
|
const ids = [...new Set(requireStringArray(args, "finding_ids", 1))];
|
|
12
|
+
let removedNoteCount = 0;
|
|
11
13
|
const updated = store.mutate(id, "agent", (draft) => {
|
|
12
14
|
const known = new Set(draft.findings.map((finding) => finding.id));
|
|
13
15
|
const unknown = ids.filter((findingId) => !known.has(findingId));
|
|
@@ -18,10 +20,15 @@ export function deleteFindings(store, args) {
|
|
|
18
20
|
const removed = new Set(ids);
|
|
19
21
|
draft.findings = draft.findings.filter((finding) => !removed.has(finding.id));
|
|
20
22
|
draft.review.selectedFindingIds = draft.review.selectedFindingIds.filter((findingId) => !removed.has(findingId));
|
|
23
|
+
const removedNotes = new Set(draft.notes.filter((note) => note.findingId !== null && removed.has(note.findingId)).map((note) => note.id));
|
|
24
|
+
removedNoteCount = removedNotes.size;
|
|
25
|
+
draft.notes = draft.notes.filter((note) => !removedNotes.has(note.id));
|
|
26
|
+
draft.review.selectedNoteIds = draft.review.selectedNoteIds.filter((noteId) => !removedNotes.has(noteId));
|
|
21
27
|
});
|
|
28
|
+
const notes = removedNoteCount > 0 ? ` ${removedNoteCount} reviewer note(s) answering them were deleted too.` : "";
|
|
22
29
|
return {
|
|
23
30
|
result: textResult(`Deleted ${ids.length} finding(s) from "${updated.title}" (${updated.id}): ${ids.join(", ")}. ` +
|
|
24
|
-
`${updated.findings.length} finding(s) remain
|
|
31
|
+
`${updated.findings.length} finding(s) remain.${notes}`),
|
|
25
32
|
analysisId: updated.id,
|
|
26
33
|
};
|
|
27
34
|
}
|