buvard 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.
- package/AGENTS.md +41 -0
- package/LICENSE +674 -0
- package/README.md +95 -2
- package/SKILL.md +34 -0
- package/bin/buvard.js +68 -0
- package/docs/anatomie-du-slop.svg +76 -0
- package/docs/research-litreview.md +282 -0
- package/docs/research.md +68 -0
- package/lib/catalog.mjs +72 -0
- package/lib/index.mjs +4 -0
- package/lib/lint.mjs +86 -0
- package/package.json +12 -4
package/lib/catalog.mjs
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* buvard catalog v2 — the slop patterns, as data.
|
|
3
|
+
*
|
|
4
|
+
* Every entry: [id, weight, sources, languages, regex]. Weights follow the
|
|
5
|
+
* barème ratified in docs/research.md (lexique 5, structure 8, servile 10);
|
|
6
|
+
* punctuation budgets live in the CLI, not here (they are per-document
|
|
7
|
+
* arithmetic, not patterns).
|
|
8
|
+
*
|
|
9
|
+
* Sources: fr1 = JulienSnsnCode/anti-slop-fr, fr2 = captain-marketing/
|
|
10
|
+
* anti-slop-fr, étape = letape-dapres.fr writing-unslop, no-slop = the
|
|
11
|
+
* creatoreconomy 20-pattern skill, ui-sk = ui-skills.com anti-ai-slop-writing.
|
|
12
|
+
* See docs/research.md for the clustering and the vote table.
|
|
13
|
+
*
|
|
14
|
+
* High-precision by design: every hit is a review candidate, not a verdict.
|
|
15
|
+
* The author's voice always wins — the sources agree on this unanimously.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export const CATALOG_VERSION = 2;
|
|
19
|
+
|
|
20
|
+
export const CATALOG = [
|
|
21
|
+
// --- ouvertures à vide (fr1 §1, étape FR)
|
|
22
|
+
['ouverture-vide', 5, 'fr1 fr2 étape', 'fr',
|
|
23
|
+
/\b(soyons honn[êe]tes|soyons clairs|on ne va pas se mentir|la v[ée]rit[ée],? c'est que|parlons peu,? parlons bien|il faut le dire|spoiler\s*:|petit rappel\s*:|petite pr[ée]cision\s*:|autant vous le dire tout de suite)\b/gi],
|
|
24
|
+
// --- décors grandioses (fr1 §2, étape FR)
|
|
25
|
+
['décor grandiose', 5, 'fr1 étape', 'fr',
|
|
26
|
+
/\b(dans un monde (trépidant|en constante évolution|[a-zéèà-ÿ]+)|[àa] l'[èe]re de|[àa] l'heure [oùo]|dans le paysage (du|de la|des)|au c[œo]ur de|au coeur de|aujourd'hui plus que jamais|face aux défis de|dans un contexte o[ùu]|dans un monde o[ùu])\b/gi],
|
|
27
|
+
// --- impératifs impersonnels (fr1 §5, étape FR)
|
|
28
|
+
['impératif impersonnel', 5, 'fr1 fr2 étape', 'fr',
|
|
29
|
+
/\bil est (essentiel|important|crucial|imp[ée]ratif|int[ée]ressant) de\b|\bil convient de (noter|souligner|rappeler|pr[ée]ciser)\b|\bforce est de constater\b|\bil va sans dire\b|\bil appert que\b/gi],
|
|
30
|
+
// --- anti-copule (fr1 §6, étape EN "copula avoidance", flaviocopes)
|
|
31
|
+
['anti-copule', 5, 'fr1 étape flaviocopes', 'fr',
|
|
32
|
+
/\b(constitue|repr[ée]sente|s'impose comme|se positionne comme|incarne|se r[ée]v[èe]le [êe]tre|t[ée]moigne de|s'inscrit dans|se veut [a-zéèà-ÿ]+|se traduit par)\b/gi],
|
|
33
|
+
// --- connecteurs en chaîne (fr1 §7, étape FR)
|
|
34
|
+
['connecteur en début', 5, 'fr1 étape', 'fr',
|
|
35
|
+
/^(de plus|en outre|par ailleurs|en effet|ainsi|de ce fait|dès lors|néanmoins|cependant|toutefois|qui plus est|de surcroît|dans cette optique|à cet égard)\b *[,:]?/gim],
|
|
36
|
+
// --- conclusions-bilans (fr1 §8, étape, no-slop)
|
|
37
|
+
['conclusion-bilan', 5, 'fr1 étape no-slop', 'fr',
|
|
38
|
+
/\b(en conclusion|en résumé|en somme|en définitive|au final|pour conclure|en bref|vous l'aurez compris|voilà pour (cet|cette)|l'avenir nous le dira|seul le temps nous dira|the future isn'?t coming)\b/gi],
|
|
39
|
+
// --- attribution vague (fr1 §10, étape, no-slop)
|
|
40
|
+
['attribution vague', 8, 'fr1 étape no-slop', 'fr en',
|
|
41
|
+
/\b(les experts s'accordent|de nombreuses études montrent|on observe que|on constate que|il est reconnu que|il est admis que|d'aucuns|selon certains|experts (agree|argue|say)|studies show)\b/gi],
|
|
42
|
+
// --- faux poids (fr1 §11, étape "significance inflation")
|
|
43
|
+
['faux poids', 8, 'fr1 étape no-slop', 'fr en',
|
|
44
|
+
/\b(marque un tournant|moment charnière|change la donne|game-?chang(er|ing)|le potentiel est immense|les enjeux (sont )?(considérables|énormes)|un tournant décisif|plus que jamais|stands as|is a testament to|marks a pivotal|pivotal moment)\b/gi],
|
|
45
|
+
// --- servilité et méta-commentaire (fr1 §12, étape "sycophancy")
|
|
46
|
+
['servilité/méta', 10, 'fr1 fr2 étape no-slop', 'fr en',
|
|
47
|
+
/\b(excellente question|n'hésitez pas [àa]|n'hésitez plus|plot twist|cerise sur le gâteau|dans cet article,? nous (allons|allions)|au programme\s*:|installez-?vous confortablement|c'est parti !|que vous soyez (débutant|[a-zéèà-ÿ]+) ou|i hope this (helps|this helps)|as an? (ai|assistant|language model)\b)/gi],
|
|
48
|
+
// --- adjectifs gonflés (fr1 §4, fr2 hyperboles, étape EN, flaviocopes)
|
|
49
|
+
['adjectif gonflé', 5, 'fr1 fr2 étape flaviocopes', 'fr en',
|
|
50
|
+
/\b(incontournable|r[ée]volutionnaire|disruptif|innovant|captivant|fascinant|redoutable|holistique|granulaire|sublimer|débloquer le potentiel|cutting-edge|groundbreaking|revolutioniz\w+|seamless\w*|robuste|robust)\b/gi],
|
|
51
|
+
// --- adverbes de remplissage (fr1 §9, fr2) — à relire, FP possibles
|
|
52
|
+
['adverbe remplissage', 3, 'fr1 fr2', 'fr',
|
|
53
|
+
/\b(vraiment|littéralement|fondamentalement|intrinsèquement|résolument|crucialement|absolument|incroyablement|extrêmement|parfaitement|totalement|véritablement)\b/gi],
|
|
54
|
+
// --- contraste binaire (fr1 st§1, fr2 règle 2, étape, no-slop, flaviocopes)
|
|
55
|
+
['contraste binaire', 8, 'fr1 fr2 étape no-slop flaviocopes', 'fr en',
|
|
56
|
+
/\bce n'est pas\b[^.!?]{1,90}?\b(c'est|mais)\b|[Cc]e n'est pas [^.!?]{1,60}[.!?]\s*[^\n]{0,20}\bc'est\b|not (just|merely|only)? ?[^.!?]{1,60}\bbut\b/gi],
|
|
57
|
+
// --- question-réponse artificielle (fr1 st§2, étape EN rhetorical openers)
|
|
58
|
+
['question-réponse', 8, 'fr1 étape no-slop', 'fr en',
|
|
59
|
+
/\b(le problème\s*\?|la bonne nouvelle\s*\?|le résultat\s*:|résultat\s*\?|mon conseil\s*\?|la réponse\s*:)\b/gi],
|
|
60
|
+
// --- fragments dramatiques (fr1 st§3, no-slop)
|
|
61
|
+
['fragment dramatique', 8, 'fr1 no-slop', 'fr en',
|
|
62
|
+
/\b(un mot\.|simple\. efficace\.|c'est tout\.|that'?s it\. that'?s)\b/gi],
|
|
63
|
+
// --- EN lexical slop (étape EN, flaviocopes, no-slop)
|
|
64
|
+
['EN slop lexique', 5, 'étape flaviocopes no-slop', 'en',
|
|
65
|
+
/\b(delve into|unpack\w*|navigate the complexit\w+|ever-changing landscape|the landscape of|synerg\w+|leverage\w*|holistic approach|it'?s worth noting|lean into|foster a culture|at the end of the day|moving forward|in today'?s world|unlock the potential|harness the power|deep dive|key takeaways|north star|double down on|at scale|boasts a|vibrant|profound)\b/gi],
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
/** Punctuation budgets (ui-sk): 1 tiret cadratin / 500 words, 1 ! / 1000. */
|
|
69
|
+
export const BUDGETS = [
|
|
70
|
+
{ re: /—/g, perWords: 500, floor: 1, id: 'tiret cadratin', weight: 5 },
|
|
71
|
+
{ re: /!/g, perWords: 1000, floor: 1, id: 'exclamation', weight: 5 },
|
|
72
|
+
];
|
package/lib/index.mjs
ADDED
package/lib/lint.mjs
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* buvard lint core — score a text against the catalog.
|
|
3
|
+
*
|
|
4
|
+
* Scoring starts at 100; each hit costs its weight (lexique 5, structure 8,
|
|
5
|
+
* servile 10); punctuation charges per occurrence past the budget: one tiret
|
|
6
|
+
* cadratin per 500 words, one exclamation per 1000. Every hit is a review
|
|
7
|
+
* candidate, not a verdict — the author's voice always wins.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { CATALOG, BUDGETS } from './catalog.mjs';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Quoted spans are exempt: quoted speech, documentation quoting slop on
|
|
14
|
+
* purpose, code. A match counts only if it starts outside every span.
|
|
15
|
+
* Spans are bounded so an unpaired opener cannot swallow the document.
|
|
16
|
+
*/
|
|
17
|
+
const QUOTED_SPANS = [
|
|
18
|
+
/```[\s\S]*?(?:```|$)/g, // fenced code blocks
|
|
19
|
+
/`[^`\n]+`/g, // inline code
|
|
20
|
+
/«[^»]{0,400}»/g, // guillemets
|
|
21
|
+
/“[^”\n]{0,400}?”/g, // typographic double quotes
|
|
22
|
+
/"[^"\n]*"/g, // straight double quotes, same line only
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function quotedRanges(text) {
|
|
26
|
+
const ranges = [];
|
|
27
|
+
for (const re of QUOTED_SPANS) {
|
|
28
|
+
re.lastIndex = 0;
|
|
29
|
+
for (const m of text.matchAll(re)) ranges.push([m.index, m.index + m[0].length]);
|
|
30
|
+
}
|
|
31
|
+
return ranges.sort((a, b) => a[0] - b[0]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Membership test by binary search over the sorted spans. */
|
|
35
|
+
function outsideTest(ranges) {
|
|
36
|
+
return (index) => {
|
|
37
|
+
let lo = 0;
|
|
38
|
+
let hi = ranges.length - 1;
|
|
39
|
+
while (lo <= hi) {
|
|
40
|
+
const mid = (lo + hi) >> 1;
|
|
41
|
+
const [s, e] = ranges[mid];
|
|
42
|
+
if (index < s) hi = mid - 1;
|
|
43
|
+
else if (index >= e) lo = mid + 1;
|
|
44
|
+
else return false; // inside a quoted span
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Body = everything after the frontmatter fence (frontmatter is data, not prose). */
|
|
51
|
+
export function bodyOf(text) {
|
|
52
|
+
const parts = text.split('---');
|
|
53
|
+
return parts.length >= 3 && text.startsWith('---') ? parts.slice(2).join('---').trim() : text.trim();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function excerpt(text, index) {
|
|
57
|
+
return '…' + text.slice(Math.max(0, index - 38), index + 52).replace(/\s+/g, ' ') + '…';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function lint(text) {
|
|
61
|
+
const words = text.split(/\s+/).filter(Boolean).length;
|
|
62
|
+
const outside = outsideTest(quotedRanges(text));
|
|
63
|
+
const hits = [];
|
|
64
|
+
let score = 100;
|
|
65
|
+
let masked = 0;
|
|
66
|
+
for (const [id, w, , , re] of CATALOG) {
|
|
67
|
+
for (const m of text.matchAll(re)) {
|
|
68
|
+
if (!outside(m.index)) { masked++; continue; }
|
|
69
|
+
hits.push({ id, w, at: excerpt(text, m.index) });
|
|
70
|
+
score -= w;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
for (const { re, perWords, floor, id, weight } of BUDGETS) {
|
|
74
|
+
const budget = Math.max(floor, Math.floor(words / perWords));
|
|
75
|
+
let excess = 0;
|
|
76
|
+
for (const m of text.matchAll(re)) {
|
|
77
|
+
if (!outside(m.index)) { masked++; continue; }
|
|
78
|
+
if (excess >= budget) {
|
|
79
|
+
hits.push({ id: `budget ${id} (>${budget}/${words} mots)`, w: weight, at: `occurrence ${excess + 1}` });
|
|
80
|
+
score -= weight;
|
|
81
|
+
}
|
|
82
|
+
excess++;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return { words, hits, masked, score: Math.max(0, score) };
|
|
86
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "buvard",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
}
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "buvard — l'anti-slop qui tache. Blotting paper for AI-flavored prose: a weighted multi-score linter for French and English slop patterns.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": { "buvard": "bin/buvard.js" },
|
|
7
|
+
"main": "lib/index.mjs",
|
|
8
|
+
"files": ["bin", "lib", "docs", "AGENTS.md", "SKILL.md", "README.md", "LICENSE"],
|
|
9
|
+
"scripts": { "test": "node --test test/" },
|
|
10
|
+
"keywords": ["slop", "linter", "prose", "french", "writing", "ai-writing", "style"],
|
|
11
|
+
"license": "GPL-3.0-only",
|
|
12
|
+
"repository": { "type": "git", "url": "https://git.nech.pl/pln/buvard.git" },
|
|
13
|
+
"engines": { "node": ">=18" }
|
|
14
|
+
}
|