@opacedev/astro-ai-content-checker 0.3.1

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.
Files changed (185) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/LICENSE +9 -0
  3. package/README.md +236 -0
  4. package/SBOM.cdx.json +172 -0
  5. package/SECURITY.md +9 -0
  6. package/THIRD_PARTY_NOTICES.md +36 -0
  7. package/dist/build-report-html.d.ts +3 -0
  8. package/dist/highlight.js +155 -0
  9. package/dist/index.d.ts +6 -0
  10. package/dist/index.js +43 -0
  11. package/dist/options.d.ts +21 -0
  12. package/dist/options.js +6 -0
  13. package/dist/receipt.d.ts +15 -0
  14. package/dist/receipt.js +100 -0
  15. package/dist/report.d.ts +55 -0
  16. package/dist/report.js +12 -0
  17. package/dist/sections.js +181 -0
  18. package/dist/share.d.ts +47 -0
  19. package/dist/share.js +72 -0
  20. package/dist/shared-EX54KI6H.js +1503 -0
  21. package/dist/shared-MMXWTX2U.js +43 -0
  22. package/dist/toolbar.js +13478 -0
  23. package/node_modules/@opacedev/ai-content-checker-browser/LICENSE +21 -0
  24. package/node_modules/@opacedev/ai-content-checker-browser/README.md +145 -0
  25. package/node_modules/@opacedev/ai-content-checker-browser/dist/dom/visible-text.d.ts +14 -0
  26. package/node_modules/@opacedev/ai-content-checker-browser/dist/dom/visible-text.js +47 -0
  27. package/node_modules/@opacedev/ai-content-checker-browser/dist/index.d.ts +3 -0
  28. package/node_modules/@opacedev/ai-content-checker-browser/dist/index.js +99 -0
  29. package/node_modules/@opacedev/ai-content-checker-browser/dist/worker/client.d.ts +12 -0
  30. package/node_modules/@opacedev/ai-content-checker-browser/dist/worker/client.js +17 -0
  31. package/node_modules/@opacedev/ai-content-checker-browser/dist/worker/entry.d.ts +1 -0
  32. package/node_modules/@opacedev/ai-content-checker-browser/dist/worker/entry.js +3984 -0
  33. package/node_modules/@opacedev/ai-content-checker-browser/dist/worker/protocol.d.ts +30 -0
  34. package/node_modules/@opacedev/ai-content-checker-browser/dist/worker/protocol.js +1 -0
  35. package/node_modules/@opacedev/ai-content-checker-browser/package.json +60 -0
  36. package/node_modules/@opacedev/ai-content-checker-contracts/LICENSE +21 -0
  37. package/node_modules/@opacedev/ai-content-checker-contracts/README.md +93 -0
  38. package/node_modules/@opacedev/ai-content-checker-contracts/dist/index.d.ts +58 -0
  39. package/node_modules/@opacedev/ai-content-checker-contracts/dist/index.js +17 -0
  40. package/node_modules/@opacedev/ai-content-checker-contracts/package.json +57 -0
  41. package/node_modules/@opacedev/ai-content-checker-contracts/src/generated/analysis-request.schema.d.ts +44 -0
  42. package/node_modules/@opacedev/ai-content-checker-contracts/src/generated/analysis-result.schema.d.ts +122 -0
  43. package/node_modules/@opacedev/ai-content-checker-contracts/src/generated/candidate.schema.d.ts +81 -0
  44. package/node_modules/@opacedev/ai-content-checker-contracts/src/generated/capabilities.schema.d.ts +24 -0
  45. package/node_modules/@opacedev/ai-content-checker-contracts/src/generated/checker-result.schema.d.ts +342 -0
  46. package/node_modules/@opacedev/ai-content-checker-contracts/src/generated/common.schema.d.ts +10 -0
  47. package/node_modules/@opacedev/ai-content-checker-contracts/src/generated/envelope.schema.d.ts +14 -0
  48. package/node_modules/@opacedev/ai-content-checker-contracts/src/generated/gate-result.schema.d.ts +19 -0
  49. package/node_modules/@opacedev/ai-content-checker-contracts/src/generated/integrity-receipt.schema.d.ts +122 -0
  50. package/node_modules/@opacedev/ai-content-checker-contracts/src/generated/job.schema.d.ts +188 -0
  51. package/node_modules/@opacedev/ai-content-checker-contracts/src/generated/method-result.schema.d.ts +39 -0
  52. package/node_modules/@opacedev/ai-content-checker-contracts/src/generated/pattern-finding.schema.d.ts +26 -0
  53. package/node_modules/@opacedev/ai-content-checker-contracts/src/generated/protected-span.schema.d.ts +35 -0
  54. package/node_modules/@opacedev/ai-content-checker-contracts/src/generated/rewrite-request.schema.d.ts +65 -0
  55. package/node_modules/@opacedev/ai-content-checker-contracts/src/index.ts +63 -0
  56. package/node_modules/@opacedev/ai-content-checker-contracts/src/semantic-validation.mjs +32 -0
  57. package/node_modules/@opacedev/ai-content-checker-core/LICENSE +21 -0
  58. package/node_modules/@opacedev/ai-content-checker-core/README.md +186 -0
  59. package/node_modules/@opacedev/ai-content-checker-core/dist/bundle.js +4744 -0
  60. package/node_modules/@opacedev/ai-content-checker-core/dist/capabilities.d.ts +16 -0
  61. package/node_modules/@opacedev/ai-content-checker-core/dist/capabilities.js +7 -0
  62. package/node_modules/@opacedev/ai-content-checker-core/dist/diff/diff.d.ts +17 -0
  63. package/node_modules/@opacedev/ai-content-checker-core/dist/diff/diff.js +76 -0
  64. package/node_modules/@opacedev/ai-content-checker-core/dist/fixes/preview.d.ts +27 -0
  65. package/node_modules/@opacedev/ai-content-checker-core/dist/fixes/preview.js +47 -0
  66. package/node_modules/@opacedev/ai-content-checker-core/dist/gates/policy.d.ts +14 -0
  67. package/node_modules/@opacedev/ai-content-checker-core/dist/gates/policy.js +16 -0
  68. package/node_modules/@opacedev/ai-content-checker-core/dist/index.d.ts +15 -0
  69. package/node_modules/@opacedev/ai-content-checker-core/dist/index.js +15 -0
  70. package/node_modules/@opacedev/ai-content-checker-core/dist/inspect.d.ts +8 -0
  71. package/node_modules/@opacedev/ai-content-checker-core/dist/inspect.js +84 -0
  72. package/node_modules/@opacedev/ai-content-checker-core/dist/patterns/en-gb-v1.d.ts +12 -0
  73. package/node_modules/@opacedev/ai-content-checker-core/dist/patterns/en-gb-v1.js +68 -0
  74. package/node_modules/@opacedev/ai-content-checker-core/dist/patterns/en-signals-v2-data.d.ts +73 -0
  75. package/node_modules/@opacedev/ai-content-checker-core/dist/patterns/en-signals-v2-data.js +499 -0
  76. package/node_modules/@opacedev/ai-content-checker-core/dist/patterns/en-signals-v2.d.ts +76 -0
  77. package/node_modules/@opacedev/ai-content-checker-core/dist/patterns/en-signals-v2.js +1067 -0
  78. package/node_modules/@opacedev/ai-content-checker-core/dist/patterns/en-signals-v3-data.d.ts +165 -0
  79. package/node_modules/@opacedev/ai-content-checker-core/dist/patterns/en-signals-v3-data.js +614 -0
  80. package/node_modules/@opacedev/ai-content-checker-core/dist/patterns/en-signals-v3.d.ts +19 -0
  81. package/node_modules/@opacedev/ai-content-checker-core/dist/patterns/en-signals-v3.js +542 -0
  82. package/node_modules/@opacedev/ai-content-checker-core/dist/patterns/en-signals-v4-corpus.d.ts +40 -0
  83. package/node_modules/@opacedev/ai-content-checker-core/dist/patterns/en-signals-v4-corpus.js +40 -0
  84. package/node_modules/@opacedev/ai-content-checker-core/dist/patterns/en-signals-v4-data.d.ts +138 -0
  85. package/node_modules/@opacedev/ai-content-checker-core/dist/patterns/en-signals-v4-data.js +200 -0
  86. package/node_modules/@opacedev/ai-content-checker-core/dist/patterns/en-signals-v4.d.ts +71 -0
  87. package/node_modules/@opacedev/ai-content-checker-core/dist/patterns/en-signals-v4.js +388 -0
  88. package/node_modules/@opacedev/ai-content-checker-core/dist/protected/extract.d.ts +17 -0
  89. package/node_modules/@opacedev/ai-content-checker-core/dist/protected/extract.js +75 -0
  90. package/node_modules/@opacedev/ai-content-checker-core/dist/protected/validate.d.ts +3 -0
  91. package/node_modules/@opacedev/ai-content-checker-core/dist/protected/validate.js +17 -0
  92. package/node_modules/@opacedev/ai-content-checker-core/dist/provenance/c2pa-text.d.ts +55 -0
  93. package/node_modules/@opacedev/ai-content-checker-core/dist/provenance/c2pa-text.js +86 -0
  94. package/node_modules/@opacedev/ai-content-checker-core/dist/receipts/build.d.ts +48 -0
  95. package/node_modules/@opacedev/ai-content-checker-core/dist/receipts/build.js +61 -0
  96. package/node_modules/@opacedev/ai-content-checker-core/dist/report/checker-result.d.ts +241 -0
  97. package/node_modules/@opacedev/ai-content-checker-core/dist/report/checker-result.js +419 -0
  98. package/node_modules/@opacedev/ai-content-checker-core/dist/source/offsets.d.ts +34 -0
  99. package/node_modules/@opacedev/ai-content-checker-core/dist/source/offsets.js +51 -0
  100. package/node_modules/@opacedev/ai-content-checker-core/dist/source/utf8.d.ts +3 -0
  101. package/node_modules/@opacedev/ai-content-checker-core/dist/source/utf8.js +58 -0
  102. package/node_modules/@opacedev/ai-content-checker-core/dist/source/visible-text.d.ts +12 -0
  103. package/node_modules/@opacedev/ai-content-checker-core/dist/source/visible-text.js +30 -0
  104. package/node_modules/@opacedev/ai-content-checker-core/dist/unicode/data.d.ts +22 -0
  105. package/node_modules/@opacedev/ai-content-checker-core/dist/unicode/data.js +137 -0
  106. package/node_modules/@opacedev/ai-content-checker-core/dist/unicode/inspect.d.ts +16 -0
  107. package/node_modules/@opacedev/ai-content-checker-core/dist/unicode/inspect.js +84 -0
  108. package/node_modules/@opacedev/ai-content-checker-core/dist/verdict/combine.d.ts +162 -0
  109. package/node_modules/@opacedev/ai-content-checker-core/dist/verdict/combine.js +501 -0
  110. package/node_modules/@opacedev/ai-content-checker-core/package.json +62 -0
  111. package/node_modules/canonicalize/LICENSE +201 -0
  112. package/node_modules/canonicalize/README.md +65 -0
  113. package/node_modules/canonicalize/bin/canonicalize.js +17 -0
  114. package/node_modules/canonicalize/lib/canonicalize.d.ts +2 -0
  115. package/node_modules/canonicalize/lib/canonicalize.js +74 -0
  116. package/node_modules/canonicalize/package.json +64 -0
  117. package/node_modules/entities/LICENSE +11 -0
  118. package/node_modules/entities/lib/decode.d.ts +211 -0
  119. package/node_modules/entities/lib/decode.d.ts.map +1 -0
  120. package/node_modules/entities/lib/decode.js +536 -0
  121. package/node_modules/entities/lib/decode.js.map +1 -0
  122. package/node_modules/entities/lib/decode_codepoint.d.ts +19 -0
  123. package/node_modules/entities/lib/decode_codepoint.d.ts.map +1 -0
  124. package/node_modules/entities/lib/decode_codepoint.js +76 -0
  125. package/node_modules/entities/lib/decode_codepoint.js.map +1 -0
  126. package/node_modules/entities/lib/encode.d.ts +22 -0
  127. package/node_modules/entities/lib/encode.d.ts.map +1 -0
  128. package/node_modules/entities/lib/encode.js +77 -0
  129. package/node_modules/entities/lib/encode.js.map +1 -0
  130. package/node_modules/entities/lib/escape.d.ts +43 -0
  131. package/node_modules/entities/lib/escape.d.ts.map +1 -0
  132. package/node_modules/entities/lib/escape.js +122 -0
  133. package/node_modules/entities/lib/escape.js.map +1 -0
  134. package/node_modules/entities/lib/esm/decode.d.ts +211 -0
  135. package/node_modules/entities/lib/esm/decode.d.ts.map +1 -0
  136. package/node_modules/entities/lib/esm/decode.js +496 -0
  137. package/node_modules/entities/lib/esm/decode.js.map +1 -0
  138. package/node_modules/entities/lib/esm/decode_codepoint.d.ts +19 -0
  139. package/node_modules/entities/lib/esm/decode_codepoint.d.ts.map +1 -0
  140. package/node_modules/entities/lib/esm/decode_codepoint.js +71 -0
  141. package/node_modules/entities/lib/esm/decode_codepoint.js.map +1 -0
  142. package/node_modules/entities/lib/esm/encode.d.ts +22 -0
  143. package/node_modules/entities/lib/esm/encode.d.ts.map +1 -0
  144. package/node_modules/entities/lib/esm/encode.js +69 -0
  145. package/node_modules/entities/lib/esm/encode.js.map +1 -0
  146. package/node_modules/entities/lib/esm/escape.d.ts +43 -0
  147. package/node_modules/entities/lib/esm/escape.d.ts.map +1 -0
  148. package/node_modules/entities/lib/esm/escape.js +116 -0
  149. package/node_modules/entities/lib/esm/escape.js.map +1 -0
  150. package/node_modules/entities/lib/esm/generated/decode-data-html.d.ts +3 -0
  151. package/node_modules/entities/lib/esm/generated/decode-data-html.d.ts.map +1 -0
  152. package/node_modules/entities/lib/esm/generated/decode-data-html.js +7 -0
  153. package/node_modules/entities/lib/esm/generated/decode-data-html.js.map +1 -0
  154. package/node_modules/entities/lib/esm/generated/decode-data-xml.d.ts +3 -0
  155. package/node_modules/entities/lib/esm/generated/decode-data-xml.d.ts.map +1 -0
  156. package/node_modules/entities/lib/esm/generated/decode-data-xml.js +7 -0
  157. package/node_modules/entities/lib/esm/generated/decode-data-xml.js.map +1 -0
  158. package/node_modules/entities/lib/esm/generated/encode-html.d.ts +8 -0
  159. package/node_modules/entities/lib/esm/generated/encode-html.d.ts.map +1 -0
  160. package/node_modules/entities/lib/esm/generated/encode-html.js +10 -0
  161. package/node_modules/entities/lib/esm/generated/encode-html.js.map +1 -0
  162. package/node_modules/entities/lib/esm/index.d.ts +96 -0
  163. package/node_modules/entities/lib/esm/index.d.ts.map +1 -0
  164. package/node_modules/entities/lib/esm/index.js +99 -0
  165. package/node_modules/entities/lib/esm/index.js.map +1 -0
  166. package/node_modules/entities/lib/esm/package.json +1 -0
  167. package/node_modules/entities/lib/generated/decode-data-html.d.ts +3 -0
  168. package/node_modules/entities/lib/generated/decode-data-html.d.ts.map +1 -0
  169. package/node_modules/entities/lib/generated/decode-data-html.js +9 -0
  170. package/node_modules/entities/lib/generated/decode-data-html.js.map +1 -0
  171. package/node_modules/entities/lib/generated/decode-data-xml.d.ts +3 -0
  172. package/node_modules/entities/lib/generated/decode-data-xml.d.ts.map +1 -0
  173. package/node_modules/entities/lib/generated/decode-data-xml.js +9 -0
  174. package/node_modules/entities/lib/generated/decode-data-xml.js.map +1 -0
  175. package/node_modules/entities/lib/generated/encode-html.d.ts +8 -0
  176. package/node_modules/entities/lib/generated/encode-html.d.ts.map +1 -0
  177. package/node_modules/entities/lib/generated/encode-html.js +12 -0
  178. package/node_modules/entities/lib/generated/encode-html.js.map +1 -0
  179. package/node_modules/entities/lib/index.d.ts +96 -0
  180. package/node_modules/entities/lib/index.d.ts.map +1 -0
  181. package/node_modules/entities/lib/index.js +126 -0
  182. package/node_modules/entities/lib/index.js.map +1 -0
  183. package/node_modules/entities/package.json +90 -0
  184. package/node_modules/entities/readme.md +122 -0
  185. package/package.json +91 -0
@@ -0,0 +1,19 @@
1
+ export interface V3Ctx {
2
+ /** Normalised document text (same coordinate space the v2 push expects). */
3
+ text: string;
4
+ wordCount: number;
5
+ paragraphs: Array<{
6
+ text: string;
7
+ start: number;
8
+ }>;
9
+ sentences: string[];
10
+ push: (category: string, key: string, nStart: number | null, nEnd: number | null, suggestion?: string, count?: number) => void;
11
+ pushEx: (category: string, key: string, nStart: number | null, nEnd: number | null, opts?: {
12
+ suggestion?: string;
13
+ count?: number;
14
+ extra?: Record<string, unknown>;
15
+ severityOverride?: "note" | "low" | "medium" | "high";
16
+ }) => void;
17
+ pushPatterns: (patterns: readonly RegExp[], category: string) => void;
18
+ }
19
+ export declare function collectV3Issues(ctx: V3Ctx): void;
@@ -0,0 +1,542 @@
1
+ /**
2
+ * en-signals 2026.08.3 harvest-merge rule logic.
3
+ *
4
+ * Implements the Tier A/B tells from the 2026.08 research harvest
5
+ * (research/AI-TELLS-MEGA-PACK.md + ai-tells-pack-seed.json + research/
6
+ * OWNER-DOCS-TELLS.md) on top of the v2 engine. Called from
7
+ * en-signals-v2.ts inside analyse(); all findings flow through the same
8
+ * dedup, weighting, scoring and envelope as v2 rules.
9
+ *
10
+ * Calibration notes are inline per rule. Thresholds are deliberately
11
+ * conservative: the engine keeps its false-negative bias (BRIEF.md §21
12
+ * Tier B — editorial evidence, never authorship proof).
13
+ */
14
+ import { AI_CITATION_TOKENS, ARROW_CONNECTOR_RE, BOLD_LABEL_BULLET_RE, BUZZWORD_PHRASES, BY_VING_TEMPLATE_RE, CONCLUSION_CTA_RE, COPULA_ALTERNATIVE_RE, DESPITE_CHALLENGES_RE, DIDACTIC_NOTE_RE, DIRECTIVE_COLON_BULLET_RE, EMOJI_DECOR_RE, ESCAPED_MARKUP_LITERALS, FAUX_INSIGHT_RE, FICTION_CLAUDEISM_RE, FICTION_PROMPTONYM_RE, FICTION_SLOP_RE, FOCAL_WORD_RE, KOBAK_CLUSTER_RE, LEGACY_FRAMING_RE, LIANG_CLUSTER_RE, MATH_ALPHANUMERIC_RE, METAPHOR_CLUSTER_RES, NARRATIVE_CLICHE_RE, NEG_PARALLELISM_RE, NOTABILITY_CANNED_RE, OUTCOME_TAIL_RE, OWNER_PHRASES, OWNER_PHRASES_B, OWNER_VOCAB_B_RE, PARTICIPIAL_TAIL_RE, PASSIVE_RE, PIVOTAL_ROLE_RE, PLACEHOLDER_TOKENS, POWER_VERB_COMPOUND_RE, PROMO_TRAVEL_RE, PUA_RANGE_RE, REASONING_LEAKS, RHETORICAL_QA_RE, RITUAL_HEADING_RE, STACCATO_MAX_WORDS, TEACH_PREACH_HEADING_RE, TRANSITION_OPENER_RE, TRIPLED_NEGATION_RE, V6_FURNITURE_THRESHOLDS, VALUABLE_INSIGHTS_RE, } from "./en-signals-v3-data.js";
15
+ function execAll(pattern, text) {
16
+ const re = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g");
17
+ const out = [];
18
+ let m;
19
+ while ((m = re.exec(text)) !== null) {
20
+ out.push(m);
21
+ if (m[0].length === 0)
22
+ re.lastIndex += 1;
23
+ }
24
+ return out;
25
+ }
26
+ function countWords(text) {
27
+ return (text.match(/\S+/g) ?? []).length;
28
+ }
29
+ /** Push every match of each pattern only when the DISTINCT match count meets a floor. */
30
+ function pushDistinctCluster(ctx, pattern, category, minDistinct) {
31
+ const matches = execAll(pattern, ctx.text);
32
+ const distinct = new Set(matches.map((m) => m[0].toLowerCase()));
33
+ if (distinct.size < minDistinct)
34
+ return;
35
+ const seen = new Set();
36
+ for (const m of matches) {
37
+ const lower = m[0].toLowerCase();
38
+ if (seen.has(lower))
39
+ continue;
40
+ seen.add(lower);
41
+ ctx.push(category, lower, m.index, m.index + m[0].length);
42
+ }
43
+ }
44
+ /** Push every match only when the TOTAL match count meets a floor. */
45
+ function pushCountThreshold(ctx, pattern, category, minCount) {
46
+ const matches = execAll(pattern, ctx.text);
47
+ if (matches.length < minCount)
48
+ return;
49
+ for (const m of matches)
50
+ ctx.push(category, m[0], m.index, m.index + m[0].length);
51
+ }
52
+ // ─── ISBN checksum (offline-computable fabricated-reference signal) ──
53
+ function isbn10Valid(digits) {
54
+ let sum = 0;
55
+ for (let i = 0; i < 10; i += 1) {
56
+ const ch = digits[i];
57
+ const val = ch === "X" || ch === "x" ? 10 : ch.charCodeAt(0) - 48;
58
+ if (val < 0 || val > 10)
59
+ return false;
60
+ sum += val * (10 - i);
61
+ }
62
+ return sum % 11 === 0;
63
+ }
64
+ function isbn13Valid(digits) {
65
+ let sum = 0;
66
+ for (let i = 0; i < 13; i += 1) {
67
+ const val = digits.charCodeAt(i) - 48;
68
+ if (val < 0 || val > 9)
69
+ return false;
70
+ sum += val * (i % 2 === 0 ? 1 : 3);
71
+ }
72
+ return sum % 10 === 0;
73
+ }
74
+ export function collectV3Issues(ctx) {
75
+ const { text, wordCount, paragraphs, sentences, push, pushEx, pushPatterns } = ctx;
76
+ const perThousand = (n) => n / (wordCount / 1000);
77
+ // ── Artefact forensics (near-zero FP; model-attributing) ──
78
+ for (const { pattern, attribution } of AI_CITATION_TOKENS) {
79
+ for (const m of execAll(pattern, text)) {
80
+ pushEx("ai-citation-token", m[0], m.index, m.index + m[0].length, {
81
+ extra: { attribution },
82
+ });
83
+ }
84
+ }
85
+ pushPatterns(REASONING_LEAKS, "reasoning-leak");
86
+ pushPatterns(PLACEHOLDER_TOKENS, "placeholder-token");
87
+ pushPatterns(ESCAPED_MARKUP_LITERALS, "escaped-markup-literal");
88
+ // Pure codepoint rules kept in the patterns layer (unicode/ is another
89
+ // workstream's module): one collapsed finding each, anchored on the first
90
+ // occurrence, with the total count in evidence.
91
+ {
92
+ const pua = execAll(PUA_RANGE_RE, text);
93
+ if (pua.length > 0) {
94
+ const first = pua[0];
95
+ pushEx("pua-character", `${pua.length} private-use character(s)`, first.index, first.index + first[0].length, { count: pua.length });
96
+ }
97
+ const math = execAll(MATH_ALPHANUMERIC_RE, text);
98
+ if (math.length > 0) {
99
+ const first = math[0];
100
+ pushEx("math-alphanumeric", `${math.length} mathematical-alphanumeric character(s)`, first.index, first.index + first[0].length, { count: math.length });
101
+ }
102
+ // Arrows only at 3+ as prose connectors — technical docs legitimately use
103
+ // one or two (calibration: seed pun-unicode-decoration, gate to density).
104
+ const arrows = execAll(ARROW_CONNECTOR_RE, text);
105
+ if (arrows.length >= 3) {
106
+ const first = arrows[0];
107
+ pushEx("arrow-decoration", `${arrows.length} arrow connectors`, first.index, first.index + first[0].length, { count: arrows.length });
108
+ }
109
+ }
110
+ // ── Tier A phrase/structural rules ──
111
+ // Negative parallelism: 2+ per document (Pew: ~3x human rate; single use is
112
+ // ordinary rhetoric — calibration per seed phr-neg-parallelism).
113
+ pushCountThreshold(ctx, NEG_PARALLELISM_RE, "neg-parallelism", 2);
114
+ pushPatterns([TRIPLED_NEGATION_RE], "tripled-negation");
115
+ pushPatterns([DESPITE_CHALLENGES_RE], "despite-challenges-arc");
116
+ // Metaphor cluster: 2+ DISTINCT stock metaphors (seed lex-metaphor-cluster).
117
+ {
118
+ const found = [];
119
+ for (const re of METAPHOR_CLUSTER_RES) {
120
+ const m = execAll(re, text)[0];
121
+ if (m)
122
+ found.push({ key: m[0].toLowerCase(), index: m.index, len: m[0].length });
123
+ }
124
+ if (found.length >= 2) {
125
+ for (const f of found)
126
+ push("metaphor-cluster", f.key, f.index, f.index + f.len);
127
+ }
128
+ }
129
+ // Participial significance tails: 3+ per document AND ≥3/1,000 words for
130
+ // long texts (PNAS 2025 rate; one tail is normal English).
131
+ {
132
+ const tails = execAll(PARTICIPIAL_TAIL_RE, text);
133
+ if (tails.length >= 3 && (wordCount < 1000 || perThousand(tails.length) >= 3)) {
134
+ for (const m of tails)
135
+ push("participial-tail", m[0].slice(0, 60), m.index, m.index + m[0].length);
136
+ }
137
+ }
138
+ // Focal-word density: one summary finding at 3+ hits and ≥3/1,000 words.
139
+ // Density, never presence — each word is legitimate English alone.
140
+ {
141
+ const hits = execAll(FOCAL_WORD_RE, text);
142
+ if (hits.length >= 3 && perThousand(hits.length) >= 3) {
143
+ const first = hits[0];
144
+ pushEx("focal-density", `${hits.length} focal-lexicon hits in ${wordCount} words`, first.index, first.index + first[0].length, {
145
+ count: hits.length,
146
+ extra: { rate_per_1000_words: Math.round(perThousand(hits.length) * 10) / 10 },
147
+ });
148
+ }
149
+ }
150
+ pushPatterns(OWNER_PHRASES, "owner-phrase");
151
+ pushPatterns([POWER_VERB_COMPOUND_RE], "power-verb-compound");
152
+ pushPatterns([OUTCOME_TAIL_RE], "outcome-tail");
153
+ pushPatterns([CONCLUSION_CTA_RE], "conclusion-cta");
154
+ // ── Tier B rules (low severity, corroboration-weight) ──
155
+ pushDistinctCluster(ctx, LIANG_CLUSTER_RE, "liang-cluster", 3);
156
+ pushDistinctCluster(ctx, KOBAK_CLUSTER_RE, "kobak-density", 4);
157
+ pushDistinctCluster(ctx, PROMO_TRAVEL_RE, "promo-travel", 2);
158
+ pushPatterns([PIVOTAL_ROLE_RE], "pivotal-role");
159
+ pushDistinctCluster(ctx, LEGACY_FRAMING_RE, "legacy-framing", 2);
160
+ pushPatterns([NOTABILITY_CANNED_RE], "notability-canned");
161
+ pushPatterns(BUZZWORD_PHRASES, "buzzword-phrase");
162
+ pushPatterns([FAUX_INSIGHT_RE], "faux-insight");
163
+ pushCountThreshold(ctx, RHETORICAL_QA_RE, "rhetorical-qa", 2);
164
+ pushPatterns([DIDACTIC_NOTE_RE], "didactic-note");
165
+ pushPatterns([NARRATIVE_CLICHE_RE], "narrative-cliche");
166
+ pushPatterns([VALUABLE_INSIGHTS_RE], "valuable-insights");
167
+ pushDistinctCluster(ctx, FICTION_CLAUDEISM_RE, "fiction-claudeism", 2);
168
+ pushPatterns([FICTION_PROMPTONYM_RE], "fiction-promptonym");
169
+ pushDistinctCluster(ctx, FICTION_SLOP_RE, "fiction-slop-phrase", 2);
170
+ pushPatterns(OWNER_PHRASES_B, "owner-phrase-b");
171
+ pushDistinctCluster(ctx, OWNER_VOCAB_B_RE, "owner-vocab-b", 2);
172
+ pushPatterns([TEACH_PREACH_HEADING_RE], "teach-preach-headings");
173
+ // "By V-ing X, you can Y": 2+ per document (owner §3f; humans use singles).
174
+ pushCountThreshold(ctx, BY_VING_TEMPLATE_RE, "by-ving-template", 2);
175
+ // Copula avoidance: ratio rule. 3+ alternatives AND alternatives making up
176
+ // >25% of copula opportunities (Geng & Trotta measured ~10% is/are drop;
177
+ // 25% share is a conservative floor well above ordinary prose).
178
+ {
179
+ const alts = execAll(COPULA_ALTERNATIVE_RE, text);
180
+ if (alts.length >= 3) {
181
+ const copulas = (text.match(/\b(?:is|are)\b/gi) ?? []).length;
182
+ const ratio = alts.length / (alts.length + copulas);
183
+ if (ratio > 0.25) {
184
+ const first = alts[0];
185
+ pushEx("copula-avoidance", `${alts.length} copula alternatives vs ${copulas} is/are (${Math.round(ratio * 100)}%)`, first.index, first.index + first[0].length, {
186
+ count: alts.length,
187
+ extra: { copula_count: copulas, alternative_ratio: Math.round(ratio * 100) / 100 },
188
+ });
189
+ }
190
+ }
191
+ }
192
+ // ── Line-based structural rules ──
193
+ const lines = text.split(/\r?\n/);
194
+ {
195
+ let offset = 0;
196
+ let boldRun = 0;
197
+ let boldRunStart = -1;
198
+ let boldRunEnd = -1;
199
+ let emojiLines = 0;
200
+ let firstEmojiAt = -1;
201
+ let firstEmojiEnd = -1;
202
+ let directiveHits = 0;
203
+ let firstDirectiveAt = -1;
204
+ let firstDirectiveEnd = -1;
205
+ // Span rule (FIX-SPAN): every anchored finding covers the text it matched.
206
+ // A bold-label run is a block, so the span runs from the first bullet's
207
+ // first character to the end of the last bullet in the run.
208
+ const flushBold = () => {
209
+ if (boldRun >= 3) {
210
+ pushEx("bold-label-bullets", `${boldRun} bold-label bullets`, boldRunStart, boldRunEnd, { count: boldRun });
211
+ }
212
+ boldRun = 0;
213
+ boldRunStart = -1;
214
+ boldRunEnd = -1;
215
+ };
216
+ for (const line of lines) {
217
+ if (BOLD_LABEL_BULLET_RE.test(line)) {
218
+ if (boldRun === 0)
219
+ boldRunStart = offset + (line.length - line.trimStart().length);
220
+ boldRunEnd = offset + line.replace(/\s+$/, "").length;
221
+ boldRun += 1;
222
+ }
223
+ else if (line.trim() !== "") {
224
+ flushBold();
225
+ }
226
+ const isHeadingOrBullet = /^\s*(?:#{1,6}[ \t]|[-*+•]\s|\d+[.)]\s)/.test(line);
227
+ if (isHeadingOrBullet) {
228
+ // Match rather than test: the emoji itself is the finding, and it can
229
+ // be two UTF-16 code units, so its real length has to be measured.
230
+ const emoji = EMOJI_DECOR_RE.exec(line);
231
+ if (emoji !== null) {
232
+ emojiLines += 1;
233
+ if (firstEmojiAt < 0) {
234
+ firstEmojiAt = offset + emoji.index;
235
+ firstEmojiEnd = firstEmojiAt + emoji[0].length;
236
+ }
237
+ }
238
+ }
239
+ const directive = DIRECTIVE_COLON_BULLET_RE.exec(line);
240
+ if (directive !== null) {
241
+ directiveHits += 1;
242
+ if (firstDirectiveAt < 0) {
243
+ firstDirectiveAt = offset + directive.index;
244
+ firstDirectiveEnd = firstDirectiveAt + directive[0].length;
245
+ }
246
+ }
247
+ offset += line.length + 1;
248
+ }
249
+ flushBold();
250
+ // Emoji decoration: 3+ decorated headings/bullets (genre-gate proxy —
251
+ // singles are normal on social surfaces). Span: the first decorating
252
+ // emoji, whole, never half a surrogate pair.
253
+ if (emojiLines >= 3) {
254
+ pushEx("emoji-decoration", `${emojiLines} emoji-decorated headings/bullets`, firstEmojiAt, firstEmojiEnd, { count: emojiLines });
255
+ }
256
+ // Directive-verb+colon bullets: 3+ items (owner §3h; genuine technical
257
+ // checklists exist, hence Tier B). Span: the first matched directive,
258
+ // bullet marker through the colon.
259
+ if (directiveHits >= 3) {
260
+ pushEx("directive-colon-bullets", `${directiveHits} directive-colon list items`, firstDirectiveAt, firstDirectiveEnd, { count: directiveHits });
261
+ }
262
+ }
263
+ // Heading inflation: 4+ headings at >3 per 300 words (seed str-heading-inflation).
264
+ {
265
+ const headings = execAll(RITUAL_HEADING_RE, text);
266
+ if (headings.length >= 4 && wordCount >= 60 && headings.length / (wordCount / 300) > 3) {
267
+ const first = headings[0];
268
+ pushEx("heading-inflation", `${headings.length} headings in ${wordCount} words`, first.index, first.index + first[0].length, {
269
+ count: headings.length,
270
+ extra: { headings_per_300_words: Math.round((headings.length / (wordCount / 300)) * 10) / 10 },
271
+ });
272
+ }
273
+ }
274
+ // Staccato fragments: 3+ consecutive ≤4-word sentences inside one paragraph
275
+ // (seed str-staccato-fragments; ad copy does this deliberately → Tier B).
276
+ {
277
+ for (const para of paragraphs) {
278
+ const paraSents = para.text.split(/(?<=[.!?])\s+/).map((s) => s.trim()).filter((s) => s.length > 0);
279
+ // Locate each fragment inside the paragraph so the finding can span the
280
+ // run it actually found rather than the paragraph's first character.
281
+ let cursor = 0;
282
+ const located = paraSents.map((s) => {
283
+ const at = para.text.indexOf(s, cursor);
284
+ const start = at >= 0 ? at : cursor;
285
+ cursor = start + s.length;
286
+ return { text: s, start: para.start + start, end: para.start + start + s.length };
287
+ });
288
+ let run = 0;
289
+ let runStart = -1;
290
+ let fired = false;
291
+ for (const s of located) {
292
+ const words = countWords(s.text);
293
+ if (words > 0 && words <= STACCATO_MAX_WORDS && /[.!?]$/.test(s.text)) {
294
+ run += 1;
295
+ if (run === 1)
296
+ runStart = s.start;
297
+ if (run >= 3 && !fired) {
298
+ pushEx("staccato-fragments", `${run}+ consecutive short fragments`, runStart, s.end, { count: run });
299
+ fired = true;
300
+ }
301
+ }
302
+ else {
303
+ run = 0;
304
+ runStart = -1;
305
+ }
306
+ }
307
+ }
308
+ }
309
+ // Tricolon density: 4+ "a, b, and c" triads at >10/1,000 words. Classical
310
+ // rhetoric FP is high (seed str-rule-of-three) → conservative double gate.
311
+ {
312
+ const triads = execAll(/\b\w+,\s+\w+,\s+and\s+\w+\b/g, text);
313
+ if (triads.length >= 4 && perThousand(triads.length) > 10) {
314
+ const first = triads[0];
315
+ pushEx("tricolon-density", `${triads.length} balanced triads in ${wordCount} words`, first.index, first.index + first[0].length, { count: triads.length });
316
+ }
317
+ }
318
+ // Transition stacking: 4+ paragraphs with >50% opening on a formal
319
+ // connective, or 3+ consecutive (seed str-transition-stacking + owner §3c).
320
+ {
321
+ if (paragraphs.length >= 3) {
322
+ let openers = 0;
323
+ let consecutive = 0;
324
+ let maxConsecutive = 0;
325
+ let firstAt = -1;
326
+ let firstEnd = -1;
327
+ for (const p of paragraphs) {
328
+ // Match, not test: the span is the connective itself, so its offset
329
+ // and length inside the paragraph both matter.
330
+ const opener = TRANSITION_OPENER_RE.exec(p.text);
331
+ if (opener !== null) {
332
+ openers += 1;
333
+ consecutive += 1;
334
+ maxConsecutive = Math.max(maxConsecutive, consecutive);
335
+ if (firstAt < 0) {
336
+ const lead = opener[0].length - opener[0].trimStart().length;
337
+ firstAt = p.start + opener.index + lead;
338
+ // The opener is the connective plus the comma that separates it
339
+ // from the clause ("Moreover,"), which is also what keeps this
340
+ // finding distinct from style.transition_density: inspectPatterns
341
+ // drops a v2 finding whose span is byte-identical to a v1 one, and
342
+ // that rule anchors the bare word.
343
+ const tail = /^[,;:]/.exec(p.text.slice(opener.index + opener[0].length));
344
+ firstEnd = p.start + opener.index + opener[0].length + (tail === null ? 0 : 1);
345
+ }
346
+ }
347
+ else {
348
+ consecutive = 0;
349
+ }
350
+ }
351
+ const majority = paragraphs.length >= 4 && openers / paragraphs.length > 0.5;
352
+ if (majority || maxConsecutive >= 3) {
353
+ pushEx("transition-stacking", `${openers}/${paragraphs.length} paragraphs open with a formal connective`, firstAt >= 0 ? firstAt : null, firstAt >= 0 ? firstEnd : null, { count: openers });
354
+ }
355
+ }
356
+ }
357
+ // Quote inconsistency: curly AND straight double quotes mixed (2+ each).
358
+ // Word processors cause the same shape → Tier B (seed pun-quote-inconsistency).
359
+ {
360
+ const curly = (text.match(/[“”]/g) ?? []).length;
361
+ const straight = (text.match(/"/g) ?? []).length;
362
+ if (curly >= 2 && straight >= 2) {
363
+ // Document-level by construction: the finding is the co-existence of two
364
+ // quote styles across the whole text, and no single quote character
365
+ // demonstrates it. Anchoring on the first curly quote pointed a reader
366
+ // at one arbitrary quote out of many, so both ends are null.
367
+ pushEx("quote-inconsistency", `${curly} curly + ${straight} straight double quotes mixed`, null, null, {
368
+ count: curly + straight,
369
+ extra: { curly_double_quotes: curly, straight_double_quotes: straight },
370
+ });
371
+ }
372
+ }
373
+ // Token cutoff: document of 100+ words ending mid-sentence (seed
374
+ // art-token-cutoff; paste-mangling looks identical → Tier B).
375
+ {
376
+ const trimmed = text.replace(/\s+$/, "");
377
+ const lastLine = trimmed.slice(trimmed.lastIndexOf("\n") + 1);
378
+ const looksStructural = /^\s*(?:#{1,6}[ \t]|[-*+•]\s|\d+[.)]\s|\|)/.test(lastLine) || /^```|^~~~/.test(lastLine.trim());
379
+ if (wordCount >= 100 && trimmed.length > 0 && /[a-z,;]$/.test(trimmed) && !looksStructural && countWords(lastLine) >= 5) {
380
+ // Span rule (FIX-SPAN): the finding is the sentence that never finished,
381
+ // so the span runs from the start of that trailing fragment to the end
382
+ // of the text. The old anchor was the final code unit — one character,
383
+ // which showed the reader nothing about where the text broke off.
384
+ // Bounded by the last line so a cut-off mid-paragraph does not drag the
385
+ // whole document into the highlight.
386
+ const lineStart = trimmed.length - lastLine.length;
387
+ const lastStop = lastLine.search(/[.!?](?=[^.!?]*$)/);
388
+ const fragment = lastStop >= 0 ? lastLine.slice(lastStop + 1) : lastLine;
389
+ const start = trimmed.length - fragment.length + (fragment.length - fragment.trimStart().length);
390
+ pushEx("token-cutoff", "text ends mid-sentence", Math.max(lineStart, start), trimmed.length, {});
391
+ }
392
+ }
393
+ // ── Stylometric measures (all capped as a group in scoring) ──
394
+ const sentenceWordCounts = sentences.map(countWords);
395
+ // Setup-and-expansion cadence (owner §3g). Adjacent pairs where one side is
396
+ // ≤6 words and the other ≥3x its length; flag at ≥3 pairs AND ≥20% of
397
+ // adjacent pairs (owner suggested ~15%; 20% + a floor of 3 is the
398
+ // conservative calibration adopted — natural prose sampled during
399
+ // development sits well under 10%).
400
+ {
401
+ if (sentenceWordCounts.length >= 8) {
402
+ let hits = 0;
403
+ for (let i = 0; i < sentenceWordCounts.length - 1; i += 1) {
404
+ const a = sentenceWordCounts[i];
405
+ const b = sentenceWordCounts[i + 1];
406
+ if ((a > 0 && a <= 6 && b >= 3 * a && b >= 12) || (b > 0 && b <= 6 && a >= 3 * b && a >= 12))
407
+ hits += 1;
408
+ }
409
+ const ratio = hits / (sentenceWordCounts.length - 1);
410
+ if (hits >= 3 && ratio >= 0.2) {
411
+ pushEx("setup-expansion-cadence", `${hits} setup/expansion sentence pairs (${Math.round(ratio * 100)}%)`, null, null, {
412
+ count: hits, extra: { pair_ratio: Math.round(ratio * 100) / 100 },
413
+ });
414
+ }
415
+ }
416
+ }
417
+ // Passive-voice ratio (owner §4). Heuristic be+participle per sentence;
418
+ // fires above 40% of sentences with 10+ sentences — the owner's ~25%
419
+ // marketing-register threshold raised to 40% because the engine has no
420
+ // register signal and academic prose is legitimately passive.
421
+ {
422
+ if (sentences.length >= 10) {
423
+ let passiveSentences = 0;
424
+ for (const s of sentences) {
425
+ PASSIVE_RE.lastIndex = 0;
426
+ if (PASSIVE_RE.test(s))
427
+ passiveSentences += 1;
428
+ }
429
+ const ratio = passiveSentences / sentences.length;
430
+ if (ratio > 0.4) {
431
+ pushEx("passive-ratio", `${passiveSentences}/${sentences.length} sentences read as passive (${Math.round(ratio * 100)}%)`, null, null, {
432
+ count: passiveSentences, extra: { passive_ratio: Math.round(ratio * 100) / 100 },
433
+ });
434
+ }
435
+ }
436
+ }
437
+ // Specificity score (owner §4 "no concrete numbers or named entities").
438
+ // Counts digit tokens, currency/percent and mid-sentence capitalised words;
439
+ // fires only on 300+ words with under 2 specifics per 1,000 words — a
440
+ // deliberately extreme floor so ordinary prose with any names or figures
441
+ // never trips it.
442
+ {
443
+ if (wordCount >= 300) {
444
+ const digitTokens = (text.match(/(?<![\w.])[£$€]?\d[\d,.]*%?/g) ?? []).length;
445
+ const midCaps = (text.match(/(?<=[a-z,;]\s)[A-Z][a-z]{2,}/g) ?? []).length;
446
+ const specifics = digitTokens + midCaps;
447
+ if (perThousand(specifics) < 2) {
448
+ pushEx("low-specificity", `${specifics} concrete specifics in ${wordCount} words`, null, null, {
449
+ count: specifics, extra: { specifics_per_1000_words: Math.round(perThousand(specifics) * 10) / 10 },
450
+ });
451
+ }
452
+ }
453
+ }
454
+ // Adjacent-sentence lemma repetition (owner §4, sharper than document TTR).
455
+ // Content words (6+ letters, non-function) shared by adjacent sentences;
456
+ // fires when 45%+ of adjacent pairs repeat with 10+ sentences — topical
457
+ // repetition in short human passages stays under the double gate.
458
+ {
459
+ if (sentences.length >= 10) {
460
+ const contentSets = sentences.map((s) => new Set((s.toLowerCase().match(/\b[a-z]{6,}\b/g) ?? [])));
461
+ let repeats = 0;
462
+ for (let i = 0; i < contentSets.length - 1; i += 1) {
463
+ const a = contentSets[i];
464
+ const b = contentSets[i + 1];
465
+ let shared = false;
466
+ for (const w of a) {
467
+ if (b.has(w)) {
468
+ shared = true;
469
+ break;
470
+ }
471
+ }
472
+ if (shared)
473
+ repeats += 1;
474
+ }
475
+ const ratio = repeats / (contentSets.length - 1);
476
+ if (ratio >= 0.45) {
477
+ pushEx("adjacent-lemma-repeat", `${repeats}/${contentSets.length - 1} adjacent sentence pairs repeat a content word`, null, null, {
478
+ count: repeats, extra: { repeat_ratio: Math.round(ratio * 100) / 100 },
479
+ });
480
+ }
481
+ }
482
+ }
483
+ // Proximity clustering (owner §3k): a focal/buzz word repeating within
484
+ // ~300 characters of itself. Modifier-style corroboration signal.
485
+ {
486
+ const hits = execAll(FOCAL_WORD_RE, text);
487
+ const byWord = new Map();
488
+ for (const m of hits) {
489
+ const w = m[0].toLowerCase();
490
+ (byWord.get(w) ?? byWord.set(w, []).get(w)).push(m.index);
491
+ }
492
+ for (const [word, positions] of byWord) {
493
+ for (let i = 0; i < positions.length - 1; i += 1) {
494
+ if (positions[i + 1] - positions[i] <= 300) {
495
+ push("proximity-cluster", `"${word}" repeats within 300 chars`, positions[i], positions[i] + word.length, undefined, positions.length);
496
+ break;
497
+ }
498
+ }
499
+ }
500
+ }
501
+ // Invalid ISBN checksum (offline part of seed art-fabricated-refs).
502
+ {
503
+ for (const m of execAll(/\bISBN(?:-1[03])?:?\s*((?:97[89][- ]?)?(?:\d[- ]?){9,12}[\dXx])\b/g, text)) {
504
+ const digits = m[1].replace(/[- ]/g, "");
505
+ const valid = digits.length === 10 ? isbn10Valid(digits) : digits.length === 13 ? isbn13Valid(digits) : false;
506
+ if (!valid && (digits.length === 10 || digits.length === 13)) {
507
+ push("invalid-isbn", m[0], m.index, m.index + m[0].length);
508
+ }
509
+ }
510
+ }
511
+ // ── 2026.08.6 provider-eval furniture rules (PROVIDER-EVAL §4.1 R3/R4/R5) ──
512
+ // Bold runs and markdown heading lines each occurred in 0/169 held-out
513
+ // human documents, so ANY occurrence fires; the combined gate adds R5's
514
+ // measured bullet-density threshold. All three are corroboration-weight —
515
+ // format-stripped paste removes the signal, so absence never counts.
516
+ {
517
+ const boldRuns = execAll(/\*\*[^*\n]{1,120}\*\*/g, text);
518
+ if (boldRuns.length >= 1) {
519
+ const first = boldRuns[0];
520
+ pushEx("markdown-bold", `${boldRuns.length} literal **bold** run(s)`, first.index, first.index + first[0].length, { count: boldRuns.length });
521
+ }
522
+ const mdHeadings = execAll(/^#{1,6}[ \t]+\S/gm, text);
523
+ if (mdHeadings.length >= 1) {
524
+ const first = mdHeadings[0];
525
+ pushEx("markdown-heading", `${mdHeadings.length} markdown heading line(s)`, first.index, first.index + first[0].length, { count: mdHeadings.length });
526
+ }
527
+ const bulletLines = (text.match(/^\s*[-*•]\s+/gm) ?? []).length;
528
+ const bulletsPer1000 = wordCount > 0 ? bulletLines / (wordCount / 1000) : 0;
529
+ const gateOpen = boldRuns.length >= 1 || mdHeadings.length >= 1
530
+ || bulletsPer1000 > V6_FURNITURE_THRESHOLDS.bulletsPer1000;
531
+ if (gateOpen) {
532
+ pushEx("markdown-furniture", `${boldRuns.length} bold / ${mdHeadings.length} headings / ${Math.round(bulletsPer1000 * 10) / 10} bullets per 1000 words`, null, null, {
533
+ count: boldRuns.length + mdHeadings.length + bulletLines,
534
+ extra: {
535
+ bold_runs: boldRuns.length,
536
+ heading_lines: mdHeadings.length,
537
+ bullets_per_1000_words: Math.round(bulletsPer1000 * 10) / 10,
538
+ },
539
+ });
540
+ }
541
+ }
542
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * en-signals 2026.08.5 — shipped human reference corpus.
3
+ *
4
+ * A ~50KB varied-register corpus of verified-HUMAN English prose used as the
5
+ * conditional prior for the conditional-compression signal and as the source
6
+ * of the lexical-register reference profile (see en-signals-v4.ts). It ships
7
+ * inside the engine so both measurements run fully offline and byte-identically
8
+ * on every surface.
9
+ *
10
+ * Provenance (all public domain, published pre-1929 — and therefore also
11
+ * pre-2022, i.e. contamination-proof against LLM output; assembled 2026-08-28
12
+ * from Project Gutenberg plain-text files, fetched over HTTPS; scheme omitted below for the no-network source gate):
13
+ * - austen-pride-1813: Jane Austen, “Pride and Prejudice” (1813); register: fiction/dialogue;
14
+ * source file www.gutenberg.org/cache/epub/1342/pg1342.txt, slice from char offset 90000 (length 9600, 9144 chars after cleanup).
15
+ * - darwin-origin-1859: Charles Darwin, “On the Origin of Species” (1859); register: scientific argument;
16
+ * source file www.gutenberg.org/cache/epub/2009/pg2009.txt, slice from char offset 100000 (length 7600, 6900 chars after cleanup).
17
+ * - franklin-autobio-1791: Benjamin Franklin, “Autobiography” (1791); register: first-person memoir;
18
+ * source file www.gutenberg.org/cache/epub/148/pg148.txt, slice from char offset 30000 (length 7600, 6482 chars after cleanup).
19
+ * - twain-innocents-1869: Mark Twain, “The Innocents Abroad” (1869); register: travel journalism;
20
+ * source file www.gutenberg.org/cache/epub/3176/pg3176.txt, slice from char offset 60000 (length 7600, 7324 chars after cleanup).
21
+ * - federalist-1788: Hamilton/Madison/Jay, “The Federalist Papers” (1788); register: formal argument;
22
+ * source file www.gutenberg.org/cache/epub/1404/pg1404.txt, slice from char offset 32000 (length 7600, 7369 chars after cleanup).
23
+ * - beeton-household-1861: Isabella Beeton, “The Book of Household Management” (1861); register: practical instruction;
24
+ * source file www.gutenberg.org/cache/epub/10136/pg10136.txt, slice from char offset 150000 (length 7600, 6487 chars after cleanup).
25
+ * - smith-wealth-1776: Adam Smith, “The Wealth of Nations” (1776); register: economics;
26
+ * source file www.gutenberg.org/cache/epub/3300/pg3300.txt, slice from char offset 130000 (length 7600, 6558 chars after cleanup).
27
+ *
28
+ * Each slice was taken from a fixed character offset in the source file,
29
+ * snapped inward to paragraph boundaries, hard-wrap unwrapped, whitespace
30
+ * normalised, and illustration markers / chapter headings removed. The
31
+ * assembly script is retained at tests/battery/README-corpus provenance note;
32
+ * regeneration is deterministic given the source files.
33
+ *
34
+ * Known register caveat (documented, deliberate): every source predates 1929,
35
+ * so the corpus skews to formal period prose. The signals built on it are
36
+ * calibrated against MODERN human fixtures (see tests/battery/calibrate.mjs)
37
+ * precisely so that this register gap cannot by itself flag modern human
38
+ * writing; thresholds may only be tightened after re-calibration.
39
+ */
40
+ export declare const REFERENCE_CORPUS: string;