buvard 0.1.1 → 0.1.2
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 +4 -2
- package/bin/buvard.js +116 -15
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -11,8 +11,10 @@ repo `git.nech.pl/pln/buvard` (visibility toggle is the owner's call).
|
|
|
11
11
|
structure -8, servile/meta -10; punctuation budgets (1 tiret cadratin/500w,
|
|
12
12
|
1 !/1000w). **Voice rule: hits are review candidates, never verdicts — the
|
|
13
13
|
author's voice always wins.** buvard is a style linter, never an AI detector.
|
|
14
|
-
- **`bin/buvard.js`** — CLI (`check`, `--json
|
|
15
|
-
`
|
|
14
|
+
- **`bin/buvard.js`** — CLI (`check`, `score`, `--json`, `--hidden`, `--help`,
|
|
15
|
+
`--version`), frontmatter stripped. Walks skip dot-directories and never
|
|
16
|
+
follow directory symlinks; unreadable dirs warn, never crash. Tests:
|
|
17
|
+
`npm test` (node:test, 24 passing).
|
|
16
18
|
- **`docs/research.md`** — the clustering study. **`docs/research-litreview.md`**
|
|
17
19
|
— literature review (2026-11-07): detection literature unreliable on short/
|
|
18
20
|
paraphrased text; 61.22% FP rate of commercial detectors on non-native essays
|
package/bin/buvard.js
CHANGED
|
@@ -2,14 +2,18 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* buvard — l'anti-slop qui tache. Blotting paper for AI-flavored prose.
|
|
4
4
|
*
|
|
5
|
-
* buvard check
|
|
6
|
-
* buvard score <file>
|
|
5
|
+
* buvard check [files|dirs] [--json] [--hidden] run the catalog over markdown/text
|
|
6
|
+
* buvard score <file> [--json] single-file score, machine-readable
|
|
7
|
+
* buvard --help | --version
|
|
7
8
|
*
|
|
8
9
|
* Scoring starts at 100; each hit costs its weight (lexique 5, structure 8,
|
|
9
10
|
* servile 10; punctuation charges per occurrence past the budget: one tiret
|
|
10
11
|
* cadratin per 500 words, one exclamation per 1000). Every hit is a review
|
|
11
12
|
* candidate, not a verdict — the author's voice always wins.
|
|
12
13
|
*
|
|
14
|
+
* Walks skip dot-directories (--hidden to include) and never follow directory
|
|
15
|
+
* symlinks; unreadable directories are warned about, not fatal.
|
|
16
|
+
*
|
|
13
17
|
* Known gaps, deliberately manual: règle de trois, synonyme acrobatique and
|
|
14
18
|
* sentence-length variance need document-level analysis that regex fakes
|
|
15
19
|
* badly. Read the text too.
|
|
@@ -20,26 +24,123 @@ import path from 'path';
|
|
|
20
24
|
import { CATALOG_VERSION, lint, bodyOf } from '../lib/index.mjs';
|
|
21
25
|
|
|
22
26
|
const TEXT_EXT = /\.(md|txt|mdx)$/i;
|
|
27
|
+
const PKG_VERSION = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
23
28
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
const USAGE = `buvard — l'anti-slop qui tache. Blotting paper for AI-flavored prose.
|
|
30
|
+
|
|
31
|
+
Usage:
|
|
32
|
+
buvard check [files|dirs...] [--json] [--hidden] lint markdown/text files
|
|
33
|
+
buvard score <file> [--json] single-file score
|
|
34
|
+
buvard --help this help
|
|
35
|
+
buvard --version package version
|
|
36
|
+
|
|
37
|
+
Options:
|
|
38
|
+
--json machine-readable output
|
|
39
|
+
--hidden include dot-directories when walking (skipped by default)
|
|
40
|
+
|
|
41
|
+
Scoring: 100 base; each hit costs its weight (lexique -5, structure -8,
|
|
42
|
+
servile -10; punctuation past budget charges per occurrence: one tiret
|
|
43
|
+
cadratin per 500 words, one exclamation per 1000). Hits are review
|
|
44
|
+
candidates, never verdicts — the author's voice always wins.`;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Collect lintable files from a target path. Text files go straight in;
|
|
48
|
+
* directories are walked (skipping dot-entries unless `hidden`, and never
|
|
49
|
+
* following directory symlinks — readdir Dirents mark them as symlinks, not
|
|
50
|
+
* directories). Unresolvable targets and unreadable directories land in
|
|
51
|
+
* `errors` as warnings; they never throw.
|
|
52
|
+
*/
|
|
53
|
+
function collect(target, files, opts, errors) {
|
|
54
|
+
let st;
|
|
55
|
+
try {
|
|
56
|
+
st = fs.statSync(target);
|
|
57
|
+
} catch {
|
|
58
|
+
errors.push(`buvard: no file or directory for "${target}"`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (st.isFile()) {
|
|
62
|
+
if (TEXT_EXT.test(target)) files.push(path.resolve(target));
|
|
63
|
+
else errors.push(`buvard: "${target}" is not .md, .txt or .mdx — skipped`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (!st.isDirectory()) {
|
|
67
|
+
errors.push(`buvard: "${target}" is neither file nor directory — skipped`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
let entries;
|
|
71
|
+
try {
|
|
72
|
+
entries = fs.readdirSync(target, { withFileTypes: true });
|
|
73
|
+
} catch (e) {
|
|
74
|
+
errors.push(`buvard: cannot read "${target}" (${e.code}) — skipped`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
for (const ent of entries) {
|
|
78
|
+
if (!opts.hidden && ent.name.startsWith('.')) continue;
|
|
79
|
+
const p = path.join(target, ent.name);
|
|
80
|
+
if (ent.isDirectory()) collect(p, files, opts, errors);
|
|
81
|
+
else if (ent.isFile() && TEXT_EXT.test(ent.name)) files.push(path.resolve(p));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function scoreReport(file, json) {
|
|
86
|
+
const r = { file, ...lint(bodyOf(fs.readFileSync(file, 'utf8'))) };
|
|
87
|
+
if (json) {
|
|
88
|
+
console.log(JSON.stringify({ buvard: CATALOG_VERSION, ...r }, null, 1));
|
|
32
89
|
return;
|
|
33
90
|
}
|
|
34
|
-
console.
|
|
91
|
+
console.log(`${r.score}/100 ${path.relative(process.cwd(), r.file)} (${r.words} mots, ${r.hits.length} candidat${r.hits.length === 1 ? '' : 's'})`);
|
|
35
92
|
}
|
|
36
93
|
|
|
37
94
|
function main(argv) {
|
|
38
|
-
const
|
|
39
|
-
const
|
|
95
|
+
const flags = new Set();
|
|
96
|
+
const positional = [];
|
|
97
|
+
for (const a of argv) {
|
|
98
|
+
if (!a.startsWith('-')) {
|
|
99
|
+
positional.push(a);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (a === '--json' || a === '--hidden') {
|
|
103
|
+
flags.add(a);
|
|
104
|
+
} else if (a === '-h' || a === '--help') {
|
|
105
|
+
console.log(USAGE);
|
|
106
|
+
return 0;
|
|
107
|
+
} else if (a === '--version') {
|
|
108
|
+
console.log(`buvard v${PKG_VERSION} (catalog v${CATALOG_VERSION})`);
|
|
109
|
+
return 0;
|
|
110
|
+
} else {
|
|
111
|
+
console.error(`buvard: unknown option "${a}"\n\n${USAGE}`);
|
|
112
|
+
return 2;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let cmd = 'check';
|
|
117
|
+
if (positional[0] === 'check' || positional[0] === 'score') cmd = positional.shift();
|
|
118
|
+
|
|
119
|
+
const json = flags.has('--json');
|
|
120
|
+
const opts = { hidden: flags.has('--hidden') };
|
|
121
|
+
const errors = [];
|
|
122
|
+
|
|
123
|
+
if (cmd === 'score') {
|
|
124
|
+
if (positional.length !== 1) {
|
|
125
|
+
console.error(`buvard score takes exactly one file\n\n${USAGE}`);
|
|
126
|
+
return 2;
|
|
127
|
+
}
|
|
128
|
+
const files = [];
|
|
129
|
+
collect(positional[0], files, opts, errors);
|
|
130
|
+
if (errors.length > 0 || files.length === 0) {
|
|
131
|
+
errors.forEach((e) => console.error(e));
|
|
132
|
+
return 2;
|
|
133
|
+
}
|
|
134
|
+
scoreReport(files[0], json);
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
40
138
|
const files = [];
|
|
41
|
-
if (
|
|
42
|
-
else collect('.', files);
|
|
139
|
+
if (positional.length > 0) positional.forEach((t) => collect(t, files, opts, errors));
|
|
140
|
+
else collect('.', files, opts, errors);
|
|
141
|
+
|
|
142
|
+
errors.forEach((e) => console.error(e));
|
|
143
|
+
if (files.length === 0 && positional.length > 0) return 2;
|
|
43
144
|
|
|
44
145
|
const results = files.map((file) => ({ file, ...lint(bodyOf(fs.readFileSync(file, 'utf8'))) }));
|
|
45
146
|
|
package/package.json
CHANGED