ai-text-check 1.0.0 → 1.2.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/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
- import 'fs';
2
- import 'path';
1
+ import { lintText } from 'ai-slop-linter';
2
+ import { readFileSync } from 'fs';
3
+ import { join, dirname } from 'path';
3
4
  import 'url';
4
5
  import { analyze } from 'is-this-ai-slop';
5
6
  import { createRequire } from 'module';
@@ -565,6 +566,9 @@ var ENGINE_PRESETS = {
565
566
  isThisAiSlop: true,
566
567
  veldica: true,
567
568
  cleanWriting: true,
569
+ aiSlopLinter: true,
570
+ slopDetector: true,
571
+ proseSlop: true,
568
572
  vale: true
569
573
  },
570
574
  strict: {
@@ -575,6 +579,9 @@ var ENGINE_PRESETS = {
575
579
  isThisAiSlop: true,
576
580
  veldica: true,
577
581
  cleanWriting: true,
582
+ aiSlopLinter: true,
583
+ slopDetector: true,
584
+ proseSlop: true,
578
585
  vale: true
579
586
  },
580
587
  ai: {
@@ -585,6 +592,9 @@ var ENGINE_PRESETS = {
585
592
  isThisAiSlop: true,
586
593
  veldica: true,
587
594
  cleanWriting: true,
595
+ aiSlopLinter: true,
596
+ slopDetector: true,
597
+ proseSlop: true,
588
598
  vale: true
589
599
  },
590
600
  grammar: {
@@ -595,6 +605,9 @@ var ENGINE_PRESETS = {
595
605
  isThisAiSlop: false,
596
606
  veldica: false,
597
607
  cleanWriting: true,
608
+ aiSlopLinter: false,
609
+ slopDetector: false,
610
+ proseSlop: false,
598
611
  vale: true
599
612
  }
600
613
  };
@@ -615,6 +628,9 @@ var ENGINE_KEYS = [
615
628
  "isThisAiSlop",
616
629
  "veldica",
617
630
  "cleanWriting",
631
+ "aiSlopLinter",
632
+ "slopDetector",
633
+ "proseSlop",
618
634
  "vale"
619
635
  ];
620
636
  function isObject(value) {
@@ -734,6 +750,14 @@ function validateUserConfig(value) {
734
750
  config.ignoreRules = value.ignoreRules;
735
751
  }
736
752
  }
753
+ const ignoreEmojis = expectBoolean(
754
+ value.ignoreEmojis,
755
+ "ignoreEmojis",
756
+ errors
757
+ );
758
+ if (ignoreEmojis !== void 0) {
759
+ config.ignoreEmojis = ignoreEmojis;
760
+ }
737
761
  if (value.include !== void 0) {
738
762
  if (!isStringArray(value.include)) {
739
763
  errors.push("include must be an array of strings");
@@ -867,6 +891,7 @@ function validateUserConfig(value) {
867
891
  "failOn",
868
892
  "minScore",
869
893
  "ignoreRules",
894
+ "ignoreEmojis",
870
895
  "include",
871
896
  "exclude",
872
897
  "extensions",
@@ -890,7 +915,14 @@ function resolveConfig(user = {}) {
890
915
  format: user.format ?? "auto",
891
916
  failOn: user.failOn ?? "warn",
892
917
  minScore: user.minScore ?? 0,
893
- ignoreRules: user.ignoreRules ?? [],
918
+ ignoreRules: (() => {
919
+ const rules = [...user.ignoreRules ?? []];
920
+ if (user.ignoreEmojis ?? false) {
921
+ rules.push("ai-emojis");
922
+ }
923
+ return rules;
924
+ })(),
925
+ ignoreEmojis: user.ignoreEmojis ?? false,
894
926
  include: user.include ?? [],
895
927
  exclude: user.exclude ?? [],
896
928
  extensions: (user.extensions ?? DEFAULT_EXTENSIONS).map(
@@ -927,6 +959,27 @@ function defineConfig(config) {
927
959
  function isStrictPreset(config) {
928
960
  return config.preset === "strict";
929
961
  }
962
+ function toSeverity(value) {
963
+ if (value === "error") return "error";
964
+ if (value === "warning" || value === "warn") return "warn";
965
+ return "info";
966
+ }
967
+ function runAiSlopLinter(text, options = {}) {
968
+ const result = lintText(options.source ?? "<text>", text, { floor: 0 });
969
+ return result.findings.map((finding) => {
970
+ const issue = {
971
+ rule: `asl-${finding.rule}`,
972
+ engine: "ai-slop-linter",
973
+ category: "ai",
974
+ severity: toSeverity(finding.severity),
975
+ message: finding.message,
976
+ line: finding.line,
977
+ column: finding.column,
978
+ excerpt: finding.excerpt
979
+ };
980
+ return issue;
981
+ });
982
+ }
930
983
 
931
984
  // node_modules/clean-writing-lint/src/mask.js
932
985
  function blank(text) {
@@ -1896,7 +1949,7 @@ var AI_RULES = /* @__PURE__ */ new Set([
1896
1949
  "banned-construction",
1897
1950
  "em-dash"
1898
1951
  ]);
1899
- function toSeverity(value) {
1952
+ function toSeverity2(value) {
1900
1953
  if (value === "error") return "error";
1901
1954
  if (value === "warn") return "warn";
1902
1955
  return "info";
@@ -1916,7 +1969,7 @@ function runCleanWriting(text) {
1916
1969
  rule: `cws-${violation.rule}`,
1917
1970
  engine: "clean-writing",
1918
1971
  category: AI_RULES.has(violation.rule) ? "ai" : "grammar",
1919
- severity: toSeverity(violation.severity),
1972
+ severity: toSeverity2(violation.severity),
1920
1973
  message: violation.message,
1921
1974
  line: violation.line,
1922
1975
  column: violation.column,
@@ -1943,6 +1996,33 @@ function runIsThisAiSlop(text) {
1943
1996
  return issue;
1944
1997
  });
1945
1998
  }
1999
+ var require2 = createRequire(import.meta.url);
2000
+ var { check } = require2("prose-slop");
2001
+ function toSeverity3(value) {
2002
+ if (value === "error") return "error";
2003
+ if (value === "warning" || value === "warn") return "warn";
2004
+ return "info";
2005
+ }
2006
+ function runProseSlop(text, options = {}) {
2007
+ const findings = check(text, {
2008
+ format: options.format ?? "plain",
2009
+ lang: "en"
2010
+ });
2011
+ return findings.map((finding) => {
2012
+ const issue = {
2013
+ rule: `prose-${finding.ruleId}`,
2014
+ engine: "prose-slop",
2015
+ category: "ai",
2016
+ severity: toSeverity3(finding.severity),
2017
+ message: finding.message,
2018
+ line: finding.line,
2019
+ column: finding.column,
2020
+ excerpt: excerptAround(text, finding.index),
2021
+ match: finding.match
2022
+ };
2023
+ return issue;
2024
+ });
2025
+ }
1946
2026
 
1947
2027
  // node_modules/nlcst-to-string/lib/index.js
1948
2028
  var emptyNodes = [];
@@ -2904,10 +2984,10 @@ function anyFactory(tests) {
2904
2984
  return false;
2905
2985
  }
2906
2986
  }
2907
- function propertiesFactory(check) {
2987
+ function propertiesFactory(check2) {
2908
2988
  const checkAsRecord = (
2909
2989
  /** @type {Record<string, unknown>} */
2910
- check
2990
+ check2
2911
2991
  );
2912
2992
  return castFactory(all);
2913
2993
  function all(node) {
@@ -2917,21 +2997,21 @@ function propertiesFactory(check) {
2917
2997
  node
2918
2998
  );
2919
2999
  let key;
2920
- for (key in check) {
3000
+ for (key in check2) {
2921
3001
  if (nodeAsRecord[key] !== checkAsRecord[key]) return false;
2922
3002
  }
2923
3003
  return true;
2924
3004
  }
2925
3005
  }
2926
- function typeFactory(check) {
3006
+ function typeFactory(check2) {
2927
3007
  return castFactory(type);
2928
3008
  function type(node) {
2929
- return node && node.type === check;
3009
+ return node && node.type === check2;
2930
3010
  }
2931
3011
  }
2932
3012
  function castFactory(testFunction) {
2933
- return check;
2934
- function check(value, index2, parent) {
3013
+ return check2;
3014
+ function check2(value, index2, parent) {
2935
3015
  return Boolean(
2936
3016
  looksLikeANode(value) && testFunction.call(
2937
3017
  this,
@@ -2960,14 +3040,14 @@ var CONTINUE = true;
2960
3040
  var EXIT = false;
2961
3041
  var SKIP = "skip";
2962
3042
  function visitParents(tree, test, visitor, reverse) {
2963
- let check;
3043
+ let check2;
2964
3044
  if (typeof test === "function" && typeof visitor !== "function") {
2965
3045
  reverse = visitor;
2966
3046
  visitor = test;
2967
3047
  } else {
2968
- check = test;
3048
+ check2 = test;
2969
3049
  }
2970
- const is2 = convert(check);
3050
+ const is2 = convert(check2);
2971
3051
  const step = reverse ? -1 : 1;
2972
3052
  factory(tree, void 0, [])();
2973
3053
  function factory(node, index2, parents) {
@@ -9860,9 +9940,9 @@ var VFile = class {
9860
9940
  * @returns {undefined}
9861
9941
  * Nothing.
9862
9942
  */
9863
- set dirname(dirname3) {
9943
+ set dirname(dirname4) {
9864
9944
  assertPath2(this.basename, "dirname");
9865
- this.path = minpath.join(dirname3 || "", this.basename);
9945
+ this.path = minpath.join(dirname4 || "", this.basename);
9866
9946
  }
9867
9947
  /**
9868
9948
  * Get the extname (including dot) (example: `'.js'`).
@@ -10885,6 +10965,160 @@ function runRetext(text) {
10885
10965
  return issue;
10886
10966
  });
10887
10967
  }
10968
+ var WEIGHT_WORD = 0.5;
10969
+ var WEIGHT_PATTERN = 0.2;
10970
+ var WEIGHT_TRIGRAM = 0.12;
10971
+ var WEIGHT_TROPE = 0.18;
10972
+ var WORD_MULTIPLIER = 1e3;
10973
+ var TRIGRAM_MULTIPLIER = 1e3;
10974
+ var CONTRAST_PATTERN = /\b(?:it(?:'s| is)|this is|that(?:'s| is)|(?:it|this|that) isn'?t)\s+(?:just |only |merely |simply )?[a-z][\s\S]{0,80}?\b(?:it(?:'s| is)|this is|that(?:'s| is)|but)\b/gi;
10975
+ var NOT_BUT_PATTERN = /\bnot\b[\s\S]{0,60}?\bbut\b/gi;
10976
+ var require3 = createRequire(import.meta.url);
10977
+ var assetsDir = join(
10978
+ dirname(require3.resolve("slop-detector/package.json")),
10979
+ "dist",
10980
+ "assets"
10981
+ );
10982
+ var cachedAssets = null;
10983
+ function computeRange(values) {
10984
+ if (values.length === 0) return { min: 0, max: 1 };
10985
+ const min = Math.min(...values);
10986
+ const max = Math.max(...values);
10987
+ const range = max - min || 1;
10988
+ return { min: min - range * 0.1, max: max + range * 0.1 };
10989
+ }
10990
+ function loadAssets() {
10991
+ if (cachedAssets) return cachedAssets;
10992
+ const wordsRaw = JSON.parse(
10993
+ readFileSync(join(assetsDir, "slop_list.json"), "utf8")
10994
+ );
10995
+ const trigramsRaw = JSON.parse(
10996
+ readFileSync(join(assetsDir, "slop_list_trigrams.json"), "utf8")
10997
+ );
10998
+ const leaderboard = JSON.parse(
10999
+ readFileSync(join(assetsDir, "leaderboard_results.json"), "utf8")
11000
+ );
11001
+ const words = /* @__PURE__ */ new Set();
11002
+ for (const item of wordsRaw) {
11003
+ if (Array.isArray(item) && item[0]) words.add(item[0].toLowerCase());
11004
+ }
11005
+ const trigrams = /* @__PURE__ */ new Set();
11006
+ for (const item of trigramsRaw) {
11007
+ if (Array.isArray(item) && item[0]) trigrams.add(item[0].toLowerCase());
11008
+ }
11009
+ const metrics = {
11010
+ slop_words: [],
11011
+ slop_trigrams: [],
11012
+ contrast: []
11013
+ };
11014
+ for (const res of leaderboard.results ?? []) {
11015
+ if (!res.metrics) continue;
11016
+ metrics.slop_words.push(res.metrics.slop_list_matches_per_1k_words || 0);
11017
+ metrics.slop_trigrams.push(
11018
+ res.metrics.slop_trigram_matches_per_1k_words || 0
11019
+ );
11020
+ metrics.contrast.push(res.metrics.not_x_but_y_per_1k_chars || 0);
11021
+ }
11022
+ cachedAssets = {
11023
+ words,
11024
+ trigrams,
11025
+ normalizationRanges: {
11026
+ slop_words: computeRange(metrics.slop_words),
11027
+ slop_trigrams: computeRange(metrics.slop_trigrams),
11028
+ contrast: computeRange(metrics.contrast)
11029
+ }
11030
+ };
11031
+ return cachedAssets;
11032
+ }
11033
+ function normalizeValue(value, range) {
11034
+ const normalized = (value - range.min) / (range.max - range.min);
11035
+ return Math.max(0, Math.min(1, normalized));
11036
+ }
11037
+ function toPlainText(text) {
11038
+ return text.replace(/```[\s\S]*?```/g, " ").replace(/`[^`\n]+`/g, " ").replace(/<[^>]+>/g, " ").replace(/!\[[^\]]*]\([^)]*\)/g, " ").replace(/\[[^\]]*]\([^)]*\)/g, " ").replace(/^#{1,6}\s+/gm, "").replace(/[*_~>#-]+/g, " ");
11039
+ }
11040
+ function tokenize(text) {
11041
+ return text.toLowerCase().match(/[a-z]+(?:'[a-z]+)?/g) ?? [];
11042
+ }
11043
+ function collectMatches(pattern, text) {
11044
+ return [...text.matchAll(pattern)].map((match) => match[0] ?? "");
11045
+ }
11046
+ function runSlopDetector(text) {
11047
+ if (!text.trim()) return [];
11048
+ const assets = loadAssets();
11049
+ const plain = toPlainText(text);
11050
+ const tokens = tokenize(plain);
11051
+ const nTokens = tokens.length || 1;
11052
+ let wordHitCount = 0;
11053
+ const wordHitMap = /* @__PURE__ */ new Map();
11054
+ for (const token of tokens) {
11055
+ if (!assets.words.has(token)) continue;
11056
+ wordHitCount++;
11057
+ wordHitMap.set(token, (wordHitMap.get(token) || 0) + 1);
11058
+ }
11059
+ let trigramHitCount = 0;
11060
+ if (tokens.length >= 3) {
11061
+ for (let i = 0; i < tokens.length - 2; i++) {
11062
+ const tg = `${tokens[i]} ${tokens[i + 1]} ${tokens[i + 2]}`;
11063
+ if (assets.trigrams.has(tg)) trigramHitCount++;
11064
+ }
11065
+ }
11066
+ const contrastMatches = [
11067
+ .../* @__PURE__ */ new Set([
11068
+ ...collectMatches(CONTRAST_PATTERN, plain),
11069
+ ...collectMatches(NOT_BUT_PATTERN, plain)
11070
+ ])
11071
+ ];
11072
+ const emDashCount = (text.match(/—/g) ?? []).length;
11073
+ const tropeRate = emDashCount / Math.max(plain.length, 1) * 1e3;
11074
+ const wordScore = wordHitCount / nTokens * WORD_MULTIPLIER;
11075
+ const trigramScore = trigramHitCount / nTokens * TRIGRAM_MULTIPLIER;
11076
+ const contrastScore = contrastMatches.length / Math.max(plain.length, 1) * 1e3;
11077
+ const rawSlopScore = (normalizeValue(wordScore, assets.normalizationRanges.slop_words) * WEIGHT_WORD + normalizeValue(contrastScore, assets.normalizationRanges.contrast) * WEIGHT_PATTERN + normalizeValue(trigramScore, assets.normalizationRanges.slop_trigrams) * WEIGHT_TRIGRAM + normalizeValue(tropeRate, { min: 0, max: 5 }) * WEIGHT_TROPE) * 100;
11078
+ const slopScore = Math.round(rawSlopScore * 10) / 10;
11079
+ const issues = [];
11080
+ if (slopScore >= 40) {
11081
+ issues.push({
11082
+ rule: "eqbench-slop-score",
11083
+ engine: "slop-detector",
11084
+ category: "ai",
11085
+ severity: slopScore >= 60 ? "warn" : "info",
11086
+ message: `EQBench SLOP score ${slopScore}/100 (higher = more AI-like).`,
11087
+ suggestion: slopScore >= 60 ? "Rewrite stock phrasing and contrast constructions." : "Review flagged vocabulary and contrast patterns."
11088
+ });
11089
+ }
11090
+ for (const [word2, count] of [...wordHitMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8)) {
11091
+ issues.push({
11092
+ rule: `eqbench-word-${word2.replace(/\s+/g, "-")}`,
11093
+ engine: "slop-detector",
11094
+ category: "ai",
11095
+ severity: "info",
11096
+ message: `EQBench slop vocabulary: \u201C${word2}\u201D (${count}\xD7).`,
11097
+ match: word2
11098
+ });
11099
+ }
11100
+ for (const match of contrastMatches.slice(0, 5)) {
11101
+ issues.push({
11102
+ rule: "eqbench-contrast",
11103
+ engine: "slop-detector",
11104
+ category: "ai",
11105
+ severity: "info",
11106
+ message: `EQBench contrast pattern: \u201C${match.trim()}\u201D`,
11107
+ match
11108
+ });
11109
+ }
11110
+ if (emDashCount >= 3) {
11111
+ issues.push({
11112
+ rule: "eqbench-trope-em-dash",
11113
+ engine: "slop-detector",
11114
+ category: "ai",
11115
+ severity: "info",
11116
+ message: `EQBench typography trope: ${emDashCount} em dash(es).`,
11117
+ match: "\u2014"
11118
+ });
11119
+ }
11120
+ return issues;
11121
+ }
10888
11122
 
10889
11123
  // src/engines/tag.ts
10890
11124
  function tagEngine(issues, engine) {
@@ -11256,7 +11490,7 @@ function countListItems(input) {
11256
11490
  function countBlockquotes(input) {
11257
11491
  return getLineMetadata(input).blockquote_count;
11258
11492
  }
11259
- function tokenize(text) {
11493
+ function tokenize2(text) {
11260
11494
  const blocks = splitParagraphBlocks(text);
11261
11495
  const paragraphs2 = blocks.map((block) => block.text);
11262
11496
  const sentences2 = splitSentencesFromBlocks(blocks);
@@ -11278,7 +11512,7 @@ function tokenize(text) {
11278
11512
  }
11279
11513
  };
11280
11514
  }
11281
- var tokenizeProse = tokenize;
11515
+ var tokenizeProse = tokenize2;
11282
11516
 
11283
11517
  // node_modules/@veldica/prose-analyzer/dist/utils.js
11284
11518
  function round(value, decimals = 4) {
@@ -12022,7 +12256,7 @@ function inventoryMarkers(text, options = {}) {
12022
12256
  }
12023
12257
 
12024
12258
  // src/engines/veldica.ts
12025
- function toSeverity2(value) {
12259
+ function toSeverity4(value) {
12026
12260
  if (value === "high") return "warn";
12027
12261
  return "info";
12028
12262
  }
@@ -12037,7 +12271,7 @@ function runVeldica(text) {
12037
12271
  rule: `veldica-${match.category || "marker"}`,
12038
12272
  engine: "veldica",
12039
12273
  category: "ai",
12040
- severity: toSeverity2(match.severity),
12274
+ severity: toSeverity4(match.severity),
12041
12275
  message: `AI-style marker (${match.category}): \u201C${match.matched_text}\u201D`,
12042
12276
  excerpt: excerptAround(text, offset),
12043
12277
  match: match.matched_text
@@ -12050,8 +12284,8 @@ function runVeldica(text) {
12050
12284
  return issue;
12051
12285
  });
12052
12286
  }
12053
- var require2 = createRequire(import.meta.url);
12054
- var writeGood = require2("write-good");
12287
+ var require4 = createRequire(import.meta.url);
12288
+ var writeGood = require4("write-good");
12055
12289
  function runWriteGood(text) {
12056
12290
  const suggestions = writeGood(text, {
12057
12291
  so: false,
@@ -12275,6 +12509,14 @@ var AI_PATTERN_RULES = [
12275
12509
  severity: "info",
12276
12510
  minCount: ({ words }) => Math.max(6, words / 250)
12277
12511
  },
12512
+ {
12513
+ id: "ai-emojis",
12514
+ category: "ai",
12515
+ pattern: new RegExp("\\p{Extended_Pictographic}", "gu"),
12516
+ message: "Emoji can read as chatty or AI-styled in formal prose.",
12517
+ severity: "info",
12518
+ suggestion: "Prefer plain prose, or set ignoreEmojis: true to allow them."
12519
+ },
12278
12520
  {
12279
12521
  id: "double-space",
12280
12522
  category: "typography",
@@ -13143,7 +13385,7 @@ function computeStats(body, titleLength, descriptionLength, issues) {
13143
13385
  descriptionLength
13144
13386
  };
13145
13387
  }
13146
- function collectIssues(body, frontmatter, format, config) {
13388
+ function collectIssues(body, frontmatter, format, config, source2) {
13147
13389
  const ignoreRules = new Set(config.ignoreRules);
13148
13390
  const issues = [];
13149
13391
  const enginesRun = [];
@@ -13216,6 +13458,18 @@ function collectIssues(body, frontmatter, format, config) {
13216
13458
  enginesRun.push("clean-writing");
13217
13459
  issues.push(...runCleanWriting(body));
13218
13460
  }
13461
+ if (config.engines.aiSlopLinter && config.checks.ai) {
13462
+ enginesRun.push("ai-slop-linter");
13463
+ issues.push(...runAiSlopLinter(body, { source: source2 }));
13464
+ }
13465
+ if (config.engines.slopDetector && config.checks.ai) {
13466
+ enginesRun.push("slop-detector");
13467
+ issues.push(...runSlopDetector(body));
13468
+ }
13469
+ if (config.engines.proseSlop && config.checks.ai) {
13470
+ enginesRun.push("prose-slop");
13471
+ issues.push(...runProseSlop(body, { format }));
13472
+ }
13219
13473
  return {
13220
13474
  issues: issues.filter((issue) => !ignoreRules.has(issue.rule)),
13221
13475
  enginesRun: uniqueEngines(enginesRun)
@@ -13231,7 +13485,8 @@ function checkText(text, options = {}) {
13231
13485
  body,
13232
13486
  parsed.frontmatter,
13233
13487
  format,
13234
- config
13488
+ config,
13489
+ source2
13235
13490
  );
13236
13491
  const title = String(parsed.frontmatter.title ?? pathBasename(source2));
13237
13492
  const report = {