slopfix 0.0.0-stage → 0.1.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.
@@ -0,0 +1,206 @@
1
+ import { phraseRule, regexRule } from './helpers.js';
2
+
3
+ // Règles françaises. Pour ajouter une expression, ajoutez une ligne dans la bonne liste.
4
+ // "expression" la signale, ["expression", "remplacement"] la corrige aussi avec --fix.
5
+
6
+ const lang = 'fr';
7
+
8
+ export default [
9
+ phraseRule({
10
+ id: 'fr/chatbot-artifact',
11
+ lang,
12
+ category: 'chatbot',
13
+ severity: 'error',
14
+ message: "Reste de chatbot. Ce texte a été copié depuis un assistant IA.",
15
+ phrases: [
16
+ "en tant qu'IA",
17
+ "en tant qu'intelligence artificielle",
18
+ 'en tant que modèle de langage',
19
+ 'en tant que grand modèle de langage',
20
+ 'ma date de coupure',
21
+ 'mes connaissances s\'arrêtent',
22
+ "je n'ai pas accès à internet",
23
+ "j'espère que ce message vous trouve en bonne santé",
24
+ 'je serais ravi de vous aider',
25
+ 'je serais ravie de vous aider',
26
+ 'excellente question',
27
+ 'très bonne question',
28
+ 'voici un résumé détaillé',
29
+ 'voici une analyse détaillée',
30
+ 'voici un aperçu complet',
31
+ ],
32
+ }),
33
+ regexRule({
34
+ id: 'fr/sycophantic-opener',
35
+ lang,
36
+ category: 'chatbot',
37
+ severity: 'error',
38
+ message: "Formule d'ouverture typique de chatbot. Supprimée avec --fix.",
39
+ patterns: [/^[ \t]*(?:bien sûr|absolument|avec plaisir|certainement)[   ]?![ \t]*/gimu],
40
+ fix: '',
41
+ }),
42
+ regexRule({
43
+ id: 'fr/chatbot-sign-off',
44
+ lang,
45
+ category: 'chatbot',
46
+ severity: 'error',
47
+ message: 'Formule de fin de chatbot. La phrase entière est supprimée avec --fix.',
48
+ patterns: [
49
+ /(?:(?<=[.!?:])[ \t]+|^[ \t]*)(?:j['’]espère que (?:cela|ça|ceci) (?:vous |t['’])?(?:aide|aidera|sera utile|a aidé)|n['’]hésitez pas (?:si vous avez|à (?:me|nous) (?:contacter|poser|faire signe|solliciter))|si vous avez (?:d['’]autres|des) questions)[^.!?\n]*[.!?]*/gimu,
50
+ ],
51
+ fix: '',
52
+ }),
53
+ phraseRule({
54
+ id: 'fr/filler-opener',
55
+ lang,
56
+ category: 'filler',
57
+ severity: 'warning',
58
+ message: 'Formule de remplissage. Supprimez-la et allez droit au but.',
59
+ drop: true,
60
+ phrases: [
61
+ 'il est important de noter que',
62
+ 'il est important de souligner que',
63
+ 'il est important de rappeler que',
64
+ 'il est essentiel de noter que',
65
+ 'il est essentiel de souligner que',
66
+ 'il est intéressant de noter que',
67
+ 'il convient de noter que',
68
+ 'il convient de souligner que',
69
+ 'il est à noter que',
70
+ 'force est de constater que',
71
+ "dans le monde d'aujourd'hui,",
72
+ 'dans le monde actuel,',
73
+ 'dans un monde en constante évolution,',
74
+ 'dans un monde en perpétuelle évolution,',
75
+ "à l'ère du numérique,",
76
+ "à l'ère du digital,",
77
+ 'sans plus attendre,',
78
+ 'en conclusion,',
79
+ 'en résumé,',
80
+ 'pour conclure,',
81
+ 'en définitive,',
82
+ 'en somme,',
83
+ ],
84
+ }),
85
+ phraseRule({
86
+ id: 'fr/hype-intro',
87
+ lang,
88
+ category: 'filler',
89
+ severity: 'warning',
90
+ message: "Accroche ou transition grandiloquente typique d'un texte généré.",
91
+ phrases: [
92
+ 'plongeons dans',
93
+ 'plongeons au cœur de',
94
+ 'plongeons ensemble',
95
+ 'plongeons sans plus attendre',
96
+ 'embarquons pour',
97
+ 'découvrons ensemble',
98
+ 'explorons ensemble',
99
+ 'naviguer dans les méandres',
100
+ 'naviguer dans le paysage',
101
+ 'dans le paysage numérique',
102
+ 'dans le paysage actuel',
103
+ 'joue un rôle crucial',
104
+ 'joue un rôle essentiel',
105
+ 'joue un rôle clé',
106
+ 'joue un rôle prépondérant',
107
+ 'témoigne de',
108
+ 'un véritable atout',
109
+ 'un atout majeur',
110
+ 'change la donne',
111
+ 'changer la donne',
112
+ 'révolutionne la manière',
113
+ 'à un tout autre niveau',
114
+ 'passer au niveau supérieur',
115
+ 'libérer tout le potentiel',
116
+ 'exploiter tout le potentiel',
117
+ 'tirer pleinement parti',
118
+ ],
119
+ }),
120
+ phraseRule({
121
+ id: 'fr/ai-vocabulary',
122
+ lang,
123
+ category: 'vocabulary',
124
+ severity: 'warning',
125
+ message: 'Mot ou expression très surutilisé par les modèles de langage.',
126
+ phrases: [
127
+ 'incontournable',
128
+ 'incontournables',
129
+ 'pierre angulaire',
130
+ 'tapisserie',
131
+ 'riche tapisserie',
132
+ 'en constante évolution',
133
+ 'en perpétuelle évolution',
134
+ 'game-changer',
135
+ 'game changer',
136
+ 'véritable mine d\'or',
137
+ 'subtiles nuances',
138
+ 'riche et varié',
139
+ 'riche et variée',
140
+ ["s'avère être", 'est'],
141
+ ["s'avèrent être", 'sont'],
142
+ ['de manière significative', 'nettement'],
143
+ ['de façon significative', 'nettement'],
144
+ ['un large éventail de', 'de nombreux'],
145
+ ['une pléthore de', 'beaucoup de'],
146
+ ['une myriade de', 'beaucoup de'],
147
+ ['une multitude de', 'beaucoup de'],
148
+ ],
149
+ }),
150
+ phraseRule({
151
+ id: 'fr/weasel-vocabulary',
152
+ lang,
153
+ category: 'vocabulary',
154
+ severity: 'info',
155
+ message: "Mot vague sur lequel s'appuient les IA. Acceptable une fois, suspect en série.",
156
+ phrases: [
157
+ 'véritable',
158
+ 'crucial',
159
+ 'cruciale',
160
+ 'cruciaux',
161
+ 'au cœur de',
162
+ 'fascinant',
163
+ 'fascinante',
164
+ 'holistique',
165
+ 'synergie',
166
+ 'levier',
167
+ 'leviers',
168
+ 'booster',
169
+ 'sublimer',
170
+ 'propulser',
171
+ 'optimal',
172
+ 'optimale',
173
+ 'robuste',
174
+ 'harmonieux',
175
+ 'harmonieuse',
176
+ 'dynamique',
177
+ 'innovant',
178
+ 'innovante',
179
+ 'novateur',
180
+ 'novatrice',
181
+ 'en outre',
182
+ 'par ailleurs',
183
+ 'de surcroît',
184
+ ],
185
+ }),
186
+ regexRule({
187
+ id: 'fr/not-x-but-y',
188
+ lang,
189
+ category: 'structure',
190
+ severity: 'warning',
191
+ message: "Construction « ce n'est pas X, c'est Y ». L'un des tics IA les plus reconnaissables.",
192
+ patterns: [
193
+ /(?<![\p{L}])[\p{L}\p{N}-]+\s+n['’](?:est|était)\s+pas\s+(?:seulement\s+|juste\s+|simplement\s+|uniquement\s+|qu['’]une?\s+)?[^.!?\n]{1,80}?[,;.:\u2014]\s*(?:c['’](?:est|était)|il\s+s['’]agit)(?![\p{L}])/giu,
194
+ /(?<![\p{L}])il\s+ne\s+s['’]agit\s+pas\s+(?:seulement\s+|simplement\s+|juste\s+|uniquement\s+)?d(?:e\s+|['’])[^.!?\n]{1,80}?,?\s*mais\s+(?:bien\s+|aussi\s+|surtout\s+)?d(?:e|['’])(?![\p{L}])/giu,
195
+ /(?<![\p{L}])plus\s+qu['’]une?\s+[^.!?\n]{1,60}?,\s*(?:c['’]est|un|une)(?![\p{L}])/giu,
196
+ ],
197
+ }),
198
+ regexRule({
199
+ id: 'fr/que-vous-soyez',
200
+ lang,
201
+ category: 'structure',
202
+ severity: 'info',
203
+ message: "Accroche « Que vous soyez X ou Y ». Cadrage d'audience générique.",
204
+ patterns: [/(?<![\p{L}])que\s+vous\s+soyez(?![\p{L}])[^.!?\n]{1,80}?(?<![\p{L}])ou(?![\p{L}])/giu],
205
+ }),
206
+ ];
@@ -0,0 +1,93 @@
1
+ // Small builders so that adding a rule is one line in a list.
2
+ // Every rule is { id, lang, category, severity, message, find(text, ctx) }.
3
+ // find() yields { index, length, fix?, message? }. fix === undefined or null means "no autofix".
4
+
5
+ const BEFORE = '(?<![\\p{L}\\p{N}_])';
6
+ const AFTER = '(?![\\p{L}\\p{N}_])';
7
+
8
+ export function escapeRegex(s) {
9
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
10
+ }
11
+
12
+ // Turns a plain phrase into a tolerant regex source:
13
+ // any apostrophe matches ' or the typographic one, any space matches any whitespace run.
14
+ export function phraseSource(phrase) {
15
+ return escapeRegex(phrase)
16
+ .replace(/['’]/g, "['’]")
17
+ .replace(/\s+/g, '\\s+');
18
+ }
19
+
20
+ export function isUpper(ch) {
21
+ return !!ch && ch !== ch.toLowerCase() && ch === ch.toUpperCase();
22
+ }
23
+
24
+ export function matchCase(replacement, original) {
25
+ if (!replacement) return replacement;
26
+ if (isUpper(original[0])) return replacement[0].toUpperCase() + replacement.slice(1);
27
+ return replacement;
28
+ }
29
+
30
+ /**
31
+ * A rule built from a list of phrases.
32
+ * Each entry is either "phrase" (report only) or ["phrase", "replacement"].
33
+ * With drop: true, the phrase is removed and the next word is capitalized when the phrase opened a sentence.
34
+ */
35
+ export function phraseRule({ id, lang, category, severity, message, phrases, drop = false }) {
36
+ const entries = phrases
37
+ .map((p) => (Array.isArray(p) ? { phrase: p[0], replacement: p[1] } : { phrase: p, replacement: undefined }))
38
+ .sort((a, b) => b.phrase.length - a.phrase.length);
39
+
40
+ const lookup = new Map(entries.map((e) => [normalize(e.phrase), e]));
41
+ const body = entries.map((e) => phraseSource(e.phrase)).join('|');
42
+ // In drop mode we also eat the trailing spaces and capture the next letter so we can re-case it.
43
+ const source = drop ? `${BEFORE}(${body})(?:${AFTER})([ \\t]*)(\\p{L})?` : `${BEFORE}(${body})${AFTER}`;
44
+
45
+ return {
46
+ id,
47
+ lang,
48
+ category,
49
+ severity,
50
+ message,
51
+ *find(text) {
52
+ const re = new RegExp(source, 'giu');
53
+ for (const m of text.matchAll(re)) {
54
+ const matched = m[1];
55
+ const entry = lookup.get(normalize(matched));
56
+ if (drop) {
57
+ const next = m[3] || '';
58
+ const opensSentence = isUpper(matched[0]);
59
+ const fixedNext = opensSentence ? next.toUpperCase() : next;
60
+ yield { index: m.index, length: m[0].length, fix: fixedNext, fixLabel: '(remove)', quote: matched };
61
+ } else {
62
+ const fix = entry && entry.replacement !== undefined ? matchCase(entry.replacement, matched) : undefined;
63
+ yield { index: m.index, length: matched.length, fix, quote: matched };
64
+ }
65
+ }
66
+ },
67
+ };
68
+ }
69
+
70
+ /** A rule built from one or more regexes. fix can be a string or a function(match, text) returning a string or null. */
71
+ export function regexRule({ id, lang, category, severity, message, patterns, fix }) {
72
+ return {
73
+ id,
74
+ lang,
75
+ category,
76
+ severity,
77
+ message,
78
+ *find(text, ctx) {
79
+ for (const pattern of patterns) {
80
+ const re = new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : pattern.flags + 'g');
81
+ for (const m of text.matchAll(re)) {
82
+ if (m[0].length === 0) continue;
83
+ const f = typeof fix === 'function' ? fix(m, text, ctx) : fix;
84
+ yield { index: m.index, length: m[0].length, fix: f ?? undefined, quote: m[0].trim() };
85
+ }
86
+ }
87
+ },
88
+ };
89
+ }
90
+
91
+ function normalize(s) {
92
+ return s.toLowerCase().replace(/’/g, "'").replace(/\s+/g, ' ');
93
+ }