@heyramzi/lint 0.0.0-stage → 1.32.4
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/LICENSE +21 -0
- package/README.md +277 -2
- package/bin/heyramzi-comment-length.js +99 -0
- package/bin/heyramzi-commit-msg.js +31 -0
- package/bin/heyramzi-file-tokens.js +66 -0
- package/bin/heyramzi-inline-path.js +79 -0
- package/bin/heyramzi-jev-code.js +107 -0
- package/bin/heyramzi-push-diff.js +93 -0
- package/bin/heyramzi-repo-rules.js +70 -0
- package/bin/heyramzi-slop.js +314 -0
- package/bin/heyramzi-svelte-markup.js +50 -0
- package/bin/heyramzi-ui-rules.js +56 -0
- package/comment-length.js +368 -0
- package/commit-msg.js +57 -0
- package/config/oxlintrc.json +87 -0
- package/data/invisibles.js +218 -0
- package/data/jev-bench.js +115 -0
- package/data/jev-code-bench.js +313 -0
- package/data/slop-words.d.ts +64 -0
- package/data/slop-words.js +2681 -0
- package/file-tokens.js +122 -0
- package/gate-files.js +14 -0
- package/index.js +116 -0
- package/inline-path.js +90 -0
- package/jev-code.js +194 -0
- package/jev.js +341 -0
- package/package.json +81 -4
- package/processors/markdown.js +22 -0
- package/push-diff.js +55 -0
- package/register.d.ts +43 -0
- package/register.js +255 -0
- package/repo-rules/packages.js +164 -0
- package/repo-rules/paths.js +101 -0
- package/repo-rules/toolchain.js +172 -0
- package/repo-rules/tree.js +51 -0
- package/repo-rules/workflows.js +116 -0
- package/repo-rules.js +54 -0
- package/rules/convex-ast.js +66 -0
- package/rules/convex-index-name.js +40 -0
- package/rules/convex-internal-only.js +59 -0
- package/rules/convex-no-date-now-in-query.js +48 -0
- package/rules/convex-no-deprecated-crons.js +40 -0
- package/rules/convex-no-filter.js +41 -0
- package/rules/convex-no-get-metadata.js +26 -0
- package/rules/convex-no-v-any.js +24 -0
- package/rules/convex-react-client-module-scope.js +37 -0
- package/rules/convex-require-args.js +39 -0
- package/rules/convex-require-returns.js +31 -0
- package/rules/convex-runtime-imports.js +48 -0
- package/rules/max-function-lines.js +105 -0
- package/rules/max-object-methods.js +86 -0
- package/rules/no-ai-words.js +78 -0
- package/rules/no-app-stores.js +25 -0
- package/rules/no-chained-type-assertions.js +91 -0
- package/rules/no-class.js +47 -0
- package/rules/no-conditional-empty-object-spread.js +64 -0
- package/rules/no-deep-phosphor-imports.js +29 -0
- package/rules/no-default-export.js +70 -0
- package/rules/no-em-dash.js +198 -0
- package/rules/no-emoji.js +56 -0
- package/rules/no-hardcoded-models.js +63 -0
- package/rules/no-inline-path.js +46 -0
- package/rules/no-inline-svg.js +67 -0
- package/rules/no-known-value-widening.js +289 -0
- package/rules/no-legacy-compat.js +35 -0
- package/rules/no-overkill-fallback.js +77 -0
- package/rules/no-raw-try-catch.js +26 -0
- package/rules/no-redundant-prop-fallback.js +153 -0
- package/rules/no-relative-imports.js +213 -0
- package/rules/no-relative-type-imports.js +40 -0
- package/rules/no-rune-outside-svelte-file.js +56 -0
- package/rules/no-rune-shadowing-binding.js +85 -0
- package/rules/no-skipped-or-focused-tests.js +74 -0
- package/rules/no-snake-case-props.js +45 -0
- package/rules/no-svelte4-syntax.js +33 -0
- package/rules/no-vitest-import-in-vite-plus.js +64 -0
- package/rules/props-interface.js +44 -0
- package/rules/redirect-external-needs-option.js +45 -0
- package/rules/require-safety-comment-for-type-assertion.js +110 -0
- package/rules/svelte-section-order.js +115 -0
- package/rules/test-file.js +8 -0
- package/scripts/count-hits.mjs +213 -0
- package/scripts/harvest-voice.mjs +161 -0
- package/scripts/jev-bench.mjs +41 -0
- package/scripts/jev-code-bench.mjs +61 -0
- package/scripts/measure-register.mjs +61 -0
- package/slop-views.js +122 -0
- package/slop.d.ts +53 -0
- package/slop.js +534 -0
- package/svelte-markup.js +107 -0
- package/svelte-template.js +258 -0
- package/ui-rules.js +324 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Scores every held-out line against every Jev shape. Exits 1 when a bad line fails nothing or a plain line fails something. Run after any change to SHAPES: `node scripts/jev-bench.mjs`.
|
|
3
|
+
|
|
4
|
+
// Needs KILO_API_KEY, from the env or Proton Pass (Dev vault, item Kilo).
|
|
5
|
+
|
|
6
|
+
import { PARAGRAPHS, SENTENCES } from "#lint/data/jev-bench.js";
|
|
7
|
+
import { PARAGRAPH_SHAPES, SHAPES, auditShapes, kiloKey } from "#lint/jev.js";
|
|
8
|
+
|
|
9
|
+
// Measuring the model, so no cached score stands in for a fresh answer.
|
|
10
|
+
process.env.HEYRAMZI_SLOP_CACHE = "off";
|
|
11
|
+
|
|
12
|
+
const FAIL = 0.75;
|
|
13
|
+
let wrong = 0;
|
|
14
|
+
const key = kiloKey();
|
|
15
|
+
|
|
16
|
+
async function score(rows, shapes, wrap) {
|
|
17
|
+
for (const row of rows) {
|
|
18
|
+
const { findings, review } = await auditShapes(wrap(row.text), { key });
|
|
19
|
+
const scores = Object.fromEntries(Object.keys(shapes).map((id) => [id, 0]));
|
|
20
|
+
for (const hit of [...findings, ...review])
|
|
21
|
+
if (hit.id in scores) scores[hit.id] = Math.max(scores[hit.id], hit.p);
|
|
22
|
+
const failed = Object.entries(scores)
|
|
23
|
+
.filter(([, p]) => p >= FAIL)
|
|
24
|
+
.map(([id]) => id);
|
|
25
|
+
const ok = row.shape ? failed.length > 0 : failed.length === 0;
|
|
26
|
+
if (!ok) wrong += 1;
|
|
27
|
+
const label =
|
|
28
|
+
ok && row.shape && !failed.includes(row.shape) ? ` (caught as ${failed.join(", ")})` : "";
|
|
29
|
+
const shown = Object.entries(scores)
|
|
30
|
+
.map(([id, p]) => `${id} ${p.toFixed(2)}`)
|
|
31
|
+
.join(" ");
|
|
32
|
+
console.log(
|
|
33
|
+
`${ok ? "ok " : "MISS"} ${(row.shape ?? "plain").padEnd(11)} ${row.text.slice(0, 60)}${label}\n ${shown}`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
await score(SENTENCES, SHAPES, (text) => text);
|
|
39
|
+
await score(PARAGRAPHS, PARAGRAPH_SHAPES, (text) => text);
|
|
40
|
+
console.log(`\n${wrong} on the wrong side of ${FAIL}`);
|
|
41
|
+
process.exit(wrong ? 1 : 0);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Scores the Jev code rules on the synthetic set, per tier. Exits 1 when HELDOUT misses its bar.
|
|
3
|
+
// `node scripts/jev-code-bench.mjs [train|heldout|all]`. Needs KILO_API_KEY (env or Proton Pass). Why: TECHLOG.md.
|
|
4
|
+
|
|
5
|
+
import { HELDOUT, TRAIN } from "#lint/data/jev-code-bench.js";
|
|
6
|
+
import { auditCode } from "#lint/jev-code.js";
|
|
7
|
+
import { kiloKey } from "#lint/jev.js";
|
|
8
|
+
|
|
9
|
+
const set = process.argv[2] ?? "all";
|
|
10
|
+
const key = kiloKey();
|
|
11
|
+
|
|
12
|
+
async function score(name, cases) {
|
|
13
|
+
const tally = { bad: 0, high: 0, medium: 0, plain: 0, fpHigh: 0, fpMedium: 0 };
|
|
14
|
+
const rows = await Promise.all(
|
|
15
|
+
cases.map(async (c) => ({ c, r: await auditCode(c.code, c.file, { key }) })),
|
|
16
|
+
);
|
|
17
|
+
for (const { c, r } of rows) {
|
|
18
|
+
const high = new Set(r.findings.map((h) => h.id));
|
|
19
|
+
const medium = new Set(r.review.map((h) => h.id).filter((id) => !high.has(id)));
|
|
20
|
+
const fired =
|
|
21
|
+
[...high]
|
|
22
|
+
.map((id) => `${id}!`)
|
|
23
|
+
.concat([...medium].map((id) => `${id}?`))
|
|
24
|
+
.join(" ") || "-";
|
|
25
|
+
let verdict;
|
|
26
|
+
if (c.rule) {
|
|
27
|
+
tally.bad++;
|
|
28
|
+
if (high.has(c.rule)) {
|
|
29
|
+
tally.high++;
|
|
30
|
+
verdict = "high";
|
|
31
|
+
} else if (medium.has(c.rule)) {
|
|
32
|
+
tally.medium++;
|
|
33
|
+
verdict = "med ";
|
|
34
|
+
} else verdict = "MISS";
|
|
35
|
+
} else {
|
|
36
|
+
tally.plain++;
|
|
37
|
+
if (high.size) {
|
|
38
|
+
tally.fpHigh++;
|
|
39
|
+
verdict = "FP! ";
|
|
40
|
+
} else if (medium.size) {
|
|
41
|
+
tally.fpMedium++;
|
|
42
|
+
verdict = "fp? ";
|
|
43
|
+
} else verdict = "ok ";
|
|
44
|
+
}
|
|
45
|
+
console.log(`${verdict} ${(c.rule ?? "plain").padEnd(11)} ${c.file.padEnd(36)} ${fired}`);
|
|
46
|
+
}
|
|
47
|
+
const pct = (n, d) => `${Math.round((100 * n) / d)}%`;
|
|
48
|
+
console.log(
|
|
49
|
+
`\n${name}: caught ${pct(tally.high, tally.bad)} at high, ${pct(tally.high + tally.medium, tally.bad)} with medium; ` +
|
|
50
|
+
`false alarms on plain code ${pct(tally.fpHigh, tally.plain)} at high, ${pct(tally.fpHigh + tally.fpMedium, tally.plain)} with medium\n`,
|
|
51
|
+
);
|
|
52
|
+
return tally;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (set !== "heldout") await score("train", TRAIN);
|
|
56
|
+
if (set !== "train") {
|
|
57
|
+
const t = await score("heldout", HELDOUT);
|
|
58
|
+
// The bar: medium tier catches 3 in 4, and a high-tier alarm never fires on plain code.
|
|
59
|
+
const ok = (t.high + t.medium) / t.bad >= 0.75 && t.fpHigh === 0;
|
|
60
|
+
process.exit(ok ? 0 : 1);
|
|
61
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Recount a corpus, so a band in `register.js` is never a typed number.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* node scripts/measure-register.mjs <file|dir> [more...]
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* A directory is read one level deep for .txt and .md. A `.json` file is read
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
* as an array of posts and every string field longer than 40 characters is treated as body text, which is the shape `voice-corpus` writes.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { measureRegister } from "#lint/register.js";
|
|
21
|
+
|
|
22
|
+
function bodies(target) {
|
|
23
|
+
const st = statSync(target);
|
|
24
|
+
if (st.isDirectory()) {
|
|
25
|
+
return readdirSync(target)
|
|
26
|
+
.filter((f) => /\.(txt|md)$/i.test(f))
|
|
27
|
+
.flatMap((f) => bodies(join(target, f)));
|
|
28
|
+
}
|
|
29
|
+
if (target.endsWith(".json")) {
|
|
30
|
+
const data = JSON.parse(readFileSync(target, "utf8"));
|
|
31
|
+
if (!Array.isArray(data)) throw new Error(`${target} is not a JSON array of posts.`);
|
|
32
|
+
return data
|
|
33
|
+
.map((r) =>
|
|
34
|
+
typeof r === "string"
|
|
35
|
+
? r
|
|
36
|
+
: Object.values(r).find((v) => typeof v === "string" && v.length > 40),
|
|
37
|
+
)
|
|
38
|
+
.filter(Boolean);
|
|
39
|
+
}
|
|
40
|
+
return [readFileSync(target, "utf8")];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const targets = process.argv.slice(2);
|
|
44
|
+
if (targets.length === 0) {
|
|
45
|
+
process.stderr.write("usage: measure-register.mjs <file|dir|json> [more...]\n");
|
|
46
|
+
process.exit(2);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const m = measureRegister(targets.flatMap(bodies).join("\n\n"));
|
|
50
|
+
const row = (k, v) => console.log(` ${k.padEnd(16)} ${v.toFixed(1)}`);
|
|
51
|
+
console.log(`${m.words} words, ${m.sentences} sentences`);
|
|
52
|
+
row("meanSentence", m.meanSentence);
|
|
53
|
+
row("medianSentence", m.medianSentence);
|
|
54
|
+
row("fragmentShare", m.fragmentShare);
|
|
55
|
+
row("longShare", m.longShare);
|
|
56
|
+
row("lengthVariation", m.lengthVariation);
|
|
57
|
+
row("contractions", m.contractions);
|
|
58
|
+
row("secondPerson", m.secondPerson);
|
|
59
|
+
row("firstPerson", m.firstPerson);
|
|
60
|
+
row("chaining", m.chaining);
|
|
61
|
+
row("egoRatio", m.egoRatio);
|
package/slop-views.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The views of a draft the audit judges: code, comments, strings and FAQ sections cut away, and the names its prose keeps.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Code is not copy.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* `#if`, `#endif`, `#Preview` and `#available` are Swift, `#newlink` is a DOM id and `#sales-alerts` is a Slack channel: all 16 hashtag hits in the first corpus run were one of
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
* those, every one of them inside a fence or a backtick span. Only the hashtag check reads this, because only it is confused by code.
|
|
15
|
+
*/
|
|
16
|
+
export function withoutCode(text) {
|
|
17
|
+
return (
|
|
18
|
+
text
|
|
19
|
+
.replace(/```[\s\S]*?```/g, " ")
|
|
20
|
+
.replace(/`[^`\n]*`/g, " ")
|
|
21
|
+
// A tool directive is syntax the file has to carry: `/* eslint-enable ... */`
|
|
22
|
+
// failed the banned word `enable` in 2 files that contain no copy at all.
|
|
23
|
+
.replace(
|
|
24
|
+
/\/[*/][ \t]*(?:eslint|oxlint|prettier|biome|c8|v8|ts-(?:ignore|expect-error|nocheck))[^\n]*/g,
|
|
25
|
+
" ",
|
|
26
|
+
)
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A source file with its comments blanked. Comments are notes to the next developer, not copy; `comment-length` gates them. */
|
|
31
|
+
export function withoutComments(text) {
|
|
32
|
+
return text
|
|
33
|
+
.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "))
|
|
34
|
+
.replace(/(^|[\s;{}(,])\/\/[^\n]*/g, "$1");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The copy inside a source file: string literals of 2+ words, comments gone. The ledger read `get
|
|
39
|
+
* ours()` and `winner: "ours"` as first person: 24 false hits.
|
|
40
|
+
*/
|
|
41
|
+
export function copyStrings(text) {
|
|
42
|
+
const literals =
|
|
43
|
+
withoutComments(text).match(/"(?:[^"\\\n]|\\.)*"|`(?:[^`\\]|\\.)*`|'(?:[^'\\\n]|\\.)*'/g) ?? [];
|
|
44
|
+
return literals.filter((s) => /\p{L}{2,}\s+\p{L}{2,}/u.test(s)).join("\n");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A string literal's first sentence is sentence-initial, and every anchored pattern used to miss it.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/*
|
|
52
|
+
* The anchors in `patterns` accept `^`, a newline, or `.!?` plus a space.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/*
|
|
56
|
+
* In a copy module the sentence opens after `key: "`, which is none of the three, so `nobody-opener` and the 4 other anchored patterns never fired on the first sentence
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/*
|
|
60
|
+
* of any string. sitesmiths keeps every word of the page in `copy.ts`, ran the gate as its AGENTS.md tells it to, and shipped "Nobody
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/*
|
|
64
|
+
* pays us to start." past a pattern that had been lint-enforced since 5 Sep 2026. Opening the quote into a line break makes the anchors true again.
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
/*
|
|
68
|
+
* Only for files the caller says are source, because in markdown `he said, "Nobody knows."` is a quotation rather than a key, and it stays one.
|
|
69
|
+
*/
|
|
70
|
+
export function withStringStarts(text) {
|
|
71
|
+
return text.replace(/([:(,=[]\s*)["'`]/g, "$1\n");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* An FAQ is the one section where a question heading is the right heading: it is written to match the query the reader typed,
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
/*
|
|
79
|
+
* and it holds its own subheadings by design. `question-heading` and `empty-section` skip it rather than asking a page to hide the question it answers.
|
|
80
|
+
*/
|
|
81
|
+
export const FAQ_TITLE =
|
|
82
|
+
/^(?:FAQ|F\.A\.Q\.?|Frequently asked questions|よくある質問|Questions? fréquentes?)$/i;
|
|
83
|
+
|
|
84
|
+
export function withoutFaqSections(text) {
|
|
85
|
+
let depth = 0;
|
|
86
|
+
return text
|
|
87
|
+
.split("\n")
|
|
88
|
+
.map((line) => {
|
|
89
|
+
const heading = /^(#{1,6})[ \t]+(.+?)[ \t]*#*$/.exec(line);
|
|
90
|
+
if (heading) {
|
|
91
|
+
const level = heading[1].length;
|
|
92
|
+
if (depth && level <= depth) depth = 0;
|
|
93
|
+
if (!depth && FAQ_TITLE.test(heading[2].trim())) depth = level;
|
|
94
|
+
}
|
|
95
|
+
return depth ? "" : line;
|
|
96
|
+
})
|
|
97
|
+
.join("\n");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The words the body capitalises mid-sentence, which are names rather than styling.
|
|
102
|
+
*/
|
|
103
|
+
|
|
104
|
+
/*
|
|
105
|
+
* Without it every review page naming 2 products failed `title-case-heading`: "What Wispr Flow does better than Wavenote" counts 2 capitals and none of them is a styling choice.
|
|
106
|
+
*/
|
|
107
|
+
|
|
108
|
+
/*
|
|
109
|
+
* Headings are excluded from the scan so a title-cased heading cannot vouch for itself.
|
|
110
|
+
*/
|
|
111
|
+
export function properNouns(body) {
|
|
112
|
+
const found = new Set();
|
|
113
|
+
for (const line of body.replace(/^#{1,6}[ \t]+.*$/gm, " ").split("\n")) {
|
|
114
|
+
let start = true;
|
|
115
|
+
for (const raw of line.trim().split(/\s+/)) {
|
|
116
|
+
const word = raw.replace(/^[^\p{L}]+/u, "").replace(/[^\p{L}\p{N}'’-]+$/u, "");
|
|
117
|
+
if (word && !start && /^[A-ZÀ-Þ]/.test(word)) found.add(word);
|
|
118
|
+
start = /[.!?:][""''”’)\]]?$/.test(raw);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return found;
|
|
122
|
+
}
|
package/slop.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export type SlopFinding = {
|
|
2
|
+
/** `word`, `phrase`, `pattern`, `artifact`, `structure`, `dash`, `emoji`, `hashtag` or `ledger`. */
|
|
3
|
+
kind: string;
|
|
4
|
+
/** The banned word, the phrase, the pattern id, or the check name. */
|
|
5
|
+
id: string;
|
|
6
|
+
/** The offending text, trimmed to something readable in a terminal. */
|
|
7
|
+
detail: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type SlopAudit = {
|
|
11
|
+
/** Every hit. A non-empty list fails the gate. */
|
|
12
|
+
findings: SlopFinding[];
|
|
13
|
+
/** Shapes a regex cannot judge. Reported, never failed. */
|
|
14
|
+
review: SlopFinding[];
|
|
15
|
+
/** The language the checks ran in. English on a tie. */
|
|
16
|
+
language: "en" | "fr";
|
|
17
|
+
/** First-person against second-person word counts, in that language. */
|
|
18
|
+
ledger: { first: number; second: number };
|
|
19
|
+
words: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type SlopLint = SlopAudit & {
|
|
23
|
+
/** The text with every invisible character removed. Always safe to send. */
|
|
24
|
+
text: string;
|
|
25
|
+
/** What was removed, one row per character kind. */
|
|
26
|
+
removed: { what: string; count: number }[];
|
|
27
|
+
/** True when `findings` is empty. Says whether the words are good enough. */
|
|
28
|
+
ok: boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** HTML in, plain text out, so an email body audits the same way a post does. */
|
|
32
|
+
export declare function stripMarkup(raw: string): string;
|
|
33
|
+
|
|
34
|
+
/** Remove the invisible characters and normalise the fake spaces. */
|
|
35
|
+
export declare function stripInvisibles(
|
|
36
|
+
text: string,
|
|
37
|
+
options?: { normalizeSpaces?: boolean },
|
|
38
|
+
): { text: string; removed: { what: string; count: number }[] };
|
|
39
|
+
|
|
40
|
+
/** True when the text carries nothing invisible. */
|
|
41
|
+
export declare function isClean(text: string): boolean;
|
|
42
|
+
|
|
43
|
+
/** Which language the draft is in, by stopword count. English on a tie. */
|
|
44
|
+
export declare function detectLanguage(text: string): "en" | "fr";
|
|
45
|
+
|
|
46
|
+
/** The enumerable half of the humanizer, over one string. Never rewrites. */
|
|
47
|
+
export declare function auditSlop(text: string): SlopAudit;
|
|
48
|
+
|
|
49
|
+
/** Clean, then audit. The one call a publishing path makes. */
|
|
50
|
+
export declare function slopGate(
|
|
51
|
+
raw: string,
|
|
52
|
+
options?: { markup?: boolean; normalizeSpaces?: boolean },
|
|
53
|
+
): SlopLint;
|