@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,137 @@
1
+ // Carrier and confusable tables for the deterministic Unicode inspection.
2
+ // Data adapted in part from the MIT-licensed `watermarks-remover` (Guillaume Meyer and
3
+ // contributors) carrier tables and the MIT-licensed `avoid-ai-writing` lookalike maps,
4
+ // extended from Unicode 16 format-character and confusables data. Future coverage
5
+ // changes should be edits to these constants, not to the inspection logic.
6
+ export const UNICODE_RULES_VERSION = "unicode:2026.08.2";
7
+ const MSG_INVISIBLE = (what) => `An invisible ${what} is present.`;
8
+ const MSG_BIDI = "A bidirectional formatting control is present.";
9
+ const BIDI_LIMIT = "Bidirectional controls are required for correct display of mixed right-to-left and left-to-right text.";
10
+ export const CARRIER_RULES = [
11
+ // Soft hyphen and combining grapheme joiner
12
+ { from: 0x00ad, name: "SOFT HYPHEN", severity: "low", fix: "remove", message: "A discretionary soft hyphen is present." },
13
+ { from: 0x034f, name: "COMBINING GRAPHEME JOINER", severity: "medium", fix: "remove", message: MSG_INVISIBLE("combining grapheme joiner"), limitation: "The combining grapheme joiner has rare legitimate uses in collation and diacritic ordering." },
14
+ // Script-specific format characters
15
+ { from: 0x061c, name: "ARABIC LETTER MARK", severity: "medium", fix: "review", message: MSG_BIDI, limitation: BIDI_LIMIT },
16
+ { from: 0x070f, name: "SYRIAC ABBREVIATION MARK", severity: "medium", fix: "review", message: "A Syriac abbreviation mark format character is present.", limitation: "This character is legitimate inside Syriac-script text." },
17
+ { from: 0x0890, to: 0x0891, name: cp => cp === 0x0890 ? "ARABIC POUND MARK ABOVE" : "ARABIC PIASTRE MARK ABOVE", severity: "medium", fix: "review", message: "An Arabic prepended format mark is present.", limitation: "This character is legitimate inside Arabic-script text." },
18
+ { from: 0x08e2, name: "ARABIC DISPUTED END OF AYAH", severity: "medium", fix: "review", message: "An Arabic prepended format mark is present.", limitation: "This character is legitimate inside Arabic-script text." },
19
+ { from: 0x180b, to: 0x180d, name: cp => `MONGOLIAN FREE VARIATION SELECTOR-${cp - 0x180a}`, severity: "medium", fix: "remove", message: MSG_INVISIBLE("Mongolian free variation selector"), context: "variation", limitation: "Free variation selectors are legitimate inside Mongolian-script text." },
20
+ { from: 0x180e, name: "MONGOLIAN VOWEL SEPARATOR", severity: "medium", fix: "remove", message: MSG_INVISIBLE("Mongolian vowel separator"), limitation: "This character is legitimate inside Mongolian-script text." },
21
+ { from: 0x180f, name: "MONGOLIAN FREE VARIATION SELECTOR-4", severity: "medium", fix: "remove", message: MSG_INVISIBLE("Mongolian free variation selector"), context: "variation", limitation: "Free variation selectors are legitimate inside Mongolian-script text." },
22
+ // Zero-width and joining controls
23
+ { from: 0x200b, name: "ZERO WIDTH SPACE", severity: "medium", fix: "remove", message: MSG_INVISIBLE("zero-width space") },
24
+ { from: 0x200c, name: "ZERO WIDTH NON-JOINER", severity: "medium", fix: "remove", message: MSG_INVISIBLE("zero-width non-joiner"), context: "joiner", limitation: "The zero-width non-joiner is standard orthography in Persian and several Indic languages." },
25
+ { from: 0x200d, name: "ZERO WIDTH JOINER", severity: "medium", fix: "remove", message: MSG_INVISIBLE("zero-width joiner"), context: "joiner", limitation: "The zero-width joiner is standard in emoji sequences and several complex scripts." },
26
+ // Bidirectional controls
27
+ { from: 0x200e, name: "LEFT-TO-RIGHT MARK", severity: "medium", fix: "review", message: MSG_BIDI, limitation: BIDI_LIMIT },
28
+ { from: 0x200f, name: "RIGHT-TO-LEFT MARK", severity: "medium", fix: "review", message: MSG_BIDI, limitation: BIDI_LIMIT },
29
+ { from: 0x202a, to: 0x202e, name: cp => ["LEFT-TO-RIGHT EMBEDDING", "RIGHT-TO-LEFT EMBEDDING", "POP DIRECTIONAL FORMATTING", "LEFT-TO-RIGHT OVERRIDE", "RIGHT-TO-LEFT OVERRIDE"][cp - 0x202a], severity: "medium", fix: "review", message: MSG_BIDI, limitation: BIDI_LIMIT },
30
+ { from: 0x2066, to: 0x2069, name: cp => ["LEFT-TO-RIGHT ISOLATE", "RIGHT-TO-LEFT ISOLATE", "FIRST STRONG ISOLATE", "POP DIRECTIONAL ISOLATE"][cp - 0x2066], severity: "medium", fix: "review", message: MSG_BIDI, limitation: BIDI_LIMIT },
31
+ // Word joiner, invisible operators and deprecated format characters
32
+ { from: 0x2060, name: "WORD JOINER", severity: "medium", fix: "remove", message: MSG_INVISIBLE("word joiner") },
33
+ { from: 0x2061, to: 0x2064, name: cp => ["FUNCTION APPLICATION", "INVISIBLE TIMES", "INVISIBLE SEPARATOR", "INVISIBLE PLUS"][cp - 0x2061], severity: "medium", fix: "remove", message: MSG_INVISIBLE("mathematical operator character"), limitation: "Invisible operators are legitimate inside machine-generated mathematical notation." },
34
+ { from: 0x206a, to: 0x206f, name: cp => ["INHIBIT SYMMETRIC SWAPPING", "ACTIVATE SYMMETRIC SWAPPING", "INHIBIT ARABIC FORM SHAPING", "ACTIVATE ARABIC FORM SHAPING", "NATIONAL DIGIT SHAPES", "NOMINAL DIGIT SHAPES"][cp - 0x206a], severity: "medium", fix: "remove", message: "A deprecated invisible format character is present.", limitation: "These characters are deprecated by Unicode and have no place in modern interchange text." },
35
+ // BOM, interlinear annotation, replacement
36
+ { from: 0xfeff, name: "BYTE ORDER MARK", severity: "low", fix: "remove", message: "A byte-order mark is present." },
37
+ { from: 0xfff9, to: 0xfffb, name: cp => ["INTERLINEAR ANNOTATION ANCHOR", "INTERLINEAR ANNOTATION SEPARATOR", "INTERLINEAR ANNOTATION TERMINATOR"][cp - 0xfff9], severity: "medium", fix: "review", message: "An interlinear annotation control is present.", limitation: "Interlinear annotation controls are legitimate in Japanese ruby markup pipelines." },
38
+ { from: 0xfffd, name: "REPLACEMENT CHARACTER", severity: "high", fix: "review", message: "A replacement character may indicate damaged text." },
39
+ // Supplementary-plane format characters
40
+ { from: 0x110bd, name: "KAITHI NUMBER SIGN", severity: "medium", fix: "review", message: "A Kaithi number-sign format character is present.", limitation: "This character is legitimate inside Kaithi-script text." },
41
+ { from: 0x110cd, name: "KAITHI NUMBER SIGN ABOVE", severity: "medium", fix: "review", message: "A Kaithi number-sign format character is present.", limitation: "This character is legitimate inside Kaithi-script text." },
42
+ // Tag characters: the classic covert payload carrier
43
+ { from: 0xe0001, name: "LANGUAGE TAG", severity: "high", fix: "remove", message: "A deprecated invisible language tag is present." },
44
+ { from: 0xe0020, to: 0xe007f, name: cp => cp === 0xe007f ? "CANCEL TAG" : `TAG ${String.fromCodePoint(cp - 0xe0000) === " " ? "SPACE" : `CHARACTER '${String.fromCodePoint(cp - 0xe0000)}'`}`, severity: "high", fix: "remove", message: "An invisible tag character is present; tag runs are a known covert payload carrier.", limitation: "Tag characters have no legitimate use in ordinary interchange text outside emoji flag sequences." },
45
+ // Variation selectors
46
+ { from: 0xfe00, to: 0xfe0f, name: cp => `VARIATION SELECTOR-${cp - 0xfe00 + 1}`, severity: "medium", fix: "remove", message: MSG_INVISIBLE("variation selector"), context: "variation", limitation: "Variation selectors are legitimate after emoji-capable and CJK base characters." },
47
+ { from: 0xe0100, to: 0xe01ef, name: cp => `VARIATION SELECTOR-${cp - 0xe0100 + 17}`, severity: "medium", fix: "review", message: MSG_INVISIBLE("supplementary variation selector"), context: "variation_sup", limitation: "Supplementary variation selectors are legitimate after CJK ideographs to select registered glyph variants." },
48
+ // Space separators beyond U+0020
49
+ { from: 0x00a0, name: "NO-BREAK SPACE", severity: "note", fix: "space", message: "A non-breaking space is present.", limitation: "Non-breaking spaces are standard French and general typographic practice." },
50
+ { from: 0x1680, name: "OGHAM SPACE MARK", severity: "low", fix: "space", message: "An unusual space character is present.", limitation: "This character is legitimate inside Ogham-script text." },
51
+ { from: 0x2000, to: 0x2008, name: cp => ["EN QUAD", "EM QUAD", "EN SPACE", "EM SPACE", "THREE-PER-EM SPACE", "FOUR-PER-EM SPACE", "SIX-PER-EM SPACE", "FIGURE SPACE", "PUNCTUATION SPACE"][cp - 0x2000], severity: "low", fix: "space", message: "A typographic space that substitutes for an ordinary space is present.", limitation: "Fixed-width spaces are legitimate in carefully typeset material." },
52
+ { from: 0x2009, name: "THIN SPACE", severity: "note", fix: "space", message: "A thin space is present.", limitation: "Thin spaces are standard French and general typographic practice." },
53
+ { from: 0x200a, name: "HAIR SPACE", severity: "low", fix: "space", message: "A hair space that substitutes for an ordinary space is present.", limitation: "Hair spaces are legitimate in carefully typeset material." },
54
+ { from: 0x202f, name: "NARROW NO-BREAK SPACE", severity: "note", fix: "space", message: "A narrow no-break space is present.", limitation: "Narrow no-break spaces are standard French punctuation spacing and appear in Mongolian text." },
55
+ { from: 0x205f, name: "MEDIUM MATHEMATICAL SPACE", severity: "note", fix: "space", message: "A medium mathematical space is present.", limitation: "This space is legitimate inside typeset mathematical notation." },
56
+ { from: 0x3000, name: "IDEOGRAPHIC SPACE", severity: "note", fix: "space", message: "An ideographic space is present.", limitation: "Ideographic spaces are standard in Chinese and Japanese text." },
57
+ // Line and paragraph separators
58
+ { from: 0x2028, name: "LINE SEPARATOR", severity: "low", fix: "review", message: "A Unicode line separator is present instead of a conventional line break." },
59
+ { from: 0x2029, name: "PARAGRAPH SEPARATOR", severity: "low", fix: "review", message: "A Unicode paragraph separator is present instead of a conventional line break." }
60
+ ];
61
+ // Large carrier ranges. These are checked by range scan rather than expanded
62
+ // into the per-code-point lookup table, which would cost ~137k entries and blow
63
+ // the browser inspection budget. Coverage changes still belong here, not in the
64
+ // inspection logic.
65
+ export const CARRIER_RANGE_RULES = [
66
+ { from: 0xe000, to: 0xf8ff, name: "PRIVATE USE CHARACTER", severity: "medium", fix: "review", message: "A private-use area character is present; its meaning is defined only by a private agreement between sender and receiver.", limitation: "Private-use characters are legitimate in icon fonts and legacy vendor symbols such as platform logos; a single one is routine." },
67
+ { from: 0xf0000, to: 0xffffd, name: "SUPPLEMENTARY PRIVATE USE CHARACTER", severity: "medium", fix: "review", message: "A supplementary private-use area character is present; its meaning is defined only by a private agreement between sender and receiver.", limitation: "Private-use characters are legitimate in icon fonts and legacy vendor symbols; a single one is routine." },
68
+ { from: 0x100000, to: 0x10fffd, name: "SUPPLEMENTARY PRIVATE USE CHARACTER", severity: "medium", fix: "review", message: "A supplementary private-use area character is present; its meaning is defined only by a private agreement between sender and receiver.", limitation: "Private-use characters are legitimate in icon fonts and legacy vendor symbols; a single one is routine." }
69
+ ];
70
+ // Latin-lookalike confusables. Floor: avoid-ai-writing CYRILLIC_LOOKALIKES/GREEK_LOOKALIKES and
71
+ // watermarks-remover LATIN_CONFUSABLES; extended from Unicode confusables data.
72
+ export const CONFUSABLES = new Map([
73
+ // Cyrillic lower case
74
+ [0x0430, { name: "CYRILLIC SMALL LETTER A", latin: "a" }],
75
+ [0x0432, { name: "CYRILLIC SMALL LETTER VE", latin: "b" }],
76
+ [0x0435, { name: "CYRILLIC SMALL LETTER IE", latin: "e" }],
77
+ [0x043a, { name: "CYRILLIC SMALL LETTER KA", latin: "k" }],
78
+ [0x043c, { name: "CYRILLIC SMALL LETTER EM", latin: "m" }],
79
+ [0x043d, { name: "CYRILLIC SMALL LETTER EN", latin: "h" }],
80
+ [0x043e, { name: "CYRILLIC SMALL LETTER O", latin: "o" }],
81
+ [0x0440, { name: "CYRILLIC SMALL LETTER ER", latin: "p" }],
82
+ [0x0441, { name: "CYRILLIC SMALL LETTER ES", latin: "c" }],
83
+ [0x0442, { name: "CYRILLIC SMALL LETTER TE", latin: "t" }],
84
+ [0x0443, { name: "CYRILLIC SMALL LETTER U", latin: "y" }],
85
+ [0x0445, { name: "CYRILLIC SMALL LETTER HA", latin: "x" }],
86
+ [0x0455, { name: "CYRILLIC SMALL LETTER DZE", latin: "s" }],
87
+ [0x0456, { name: "CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I", latin: "i" }],
88
+ [0x0458, { name: "CYRILLIC SMALL LETTER JE", latin: "j" }],
89
+ [0x0461, { name: "CYRILLIC SMALL LETTER OMEGA", latin: "w" }],
90
+ [0x0475, { name: "CYRILLIC SMALL LETTER IZHITSA", latin: "v" }],
91
+ [0x04bb, { name: "CYRILLIC SMALL LETTER SHHA", latin: "h" }],
92
+ [0x0501, { name: "CYRILLIC SMALL LETTER KOMI DE", latin: "d" }],
93
+ [0x051b, { name: "CYRILLIC SMALL LETTER QA", latin: "q" }],
94
+ [0x051d, { name: "CYRILLIC SMALL LETTER WE", latin: "w" }],
95
+ // Cyrillic upper case
96
+ [0x0405, { name: "CYRILLIC CAPITAL LETTER DZE", latin: "S" }],
97
+ [0x0406, { name: "CYRILLIC CAPITAL LETTER BYELORUSSIAN-UKRAINIAN I", latin: "I" }],
98
+ [0x0408, { name: "CYRILLIC CAPITAL LETTER JE", latin: "J" }],
99
+ [0x0410, { name: "CYRILLIC CAPITAL LETTER A", latin: "A" }],
100
+ [0x0412, { name: "CYRILLIC CAPITAL LETTER VE", latin: "B" }],
101
+ [0x0415, { name: "CYRILLIC CAPITAL LETTER IE", latin: "E" }],
102
+ [0x041a, { name: "CYRILLIC CAPITAL LETTER KA", latin: "K" }],
103
+ [0x041c, { name: "CYRILLIC CAPITAL LETTER EM", latin: "M" }],
104
+ [0x041d, { name: "CYRILLIC CAPITAL LETTER EN", latin: "H" }],
105
+ [0x041e, { name: "CYRILLIC CAPITAL LETTER O", latin: "O" }],
106
+ [0x0420, { name: "CYRILLIC CAPITAL LETTER ER", latin: "P" }],
107
+ [0x0421, { name: "CYRILLIC CAPITAL LETTER ES", latin: "C" }],
108
+ [0x0422, { name: "CYRILLIC CAPITAL LETTER TE", latin: "T" }],
109
+ [0x0423, { name: "CYRILLIC CAPITAL LETTER U", latin: "Y" }],
110
+ [0x0425, { name: "CYRILLIC CAPITAL LETTER HA", latin: "X" }],
111
+ [0x0500, { name: "CYRILLIC CAPITAL LETTER KOMI DE", latin: "D" }],
112
+ // Greek lower case
113
+ [0x03b1, { name: "GREEK SMALL LETTER ALPHA", latin: "a" }],
114
+ [0x03b9, { name: "GREEK SMALL LETTER IOTA", latin: "i" }],
115
+ [0x03ba, { name: "GREEK SMALL LETTER KAPPA", latin: "k" }],
116
+ [0x03bd, { name: "GREEK SMALL LETTER NU", latin: "v" }],
117
+ [0x03bf, { name: "GREEK SMALL LETTER OMICRON", latin: "o" }],
118
+ [0x03c1, { name: "GREEK SMALL LETTER RHO", latin: "p" }],
119
+ [0x03c5, { name: "GREEK SMALL LETTER UPSILON", latin: "u" }],
120
+ [0x03f2, { name: "GREEK LUNATE SIGMA SYMBOL", latin: "c" }],
121
+ // Greek upper case
122
+ [0x0391, { name: "GREEK CAPITAL LETTER ALPHA", latin: "A" }],
123
+ [0x0392, { name: "GREEK CAPITAL LETTER BETA", latin: "B" }],
124
+ [0x0395, { name: "GREEK CAPITAL LETTER EPSILON", latin: "E" }],
125
+ [0x0396, { name: "GREEK CAPITAL LETTER ZETA", latin: "Z" }],
126
+ [0x0397, { name: "GREEK CAPITAL LETTER ETA", latin: "H" }],
127
+ [0x0399, { name: "GREEK CAPITAL LETTER IOTA", latin: "I" }],
128
+ [0x039a, { name: "GREEK CAPITAL LETTER KAPPA", latin: "K" }],
129
+ [0x039c, { name: "GREEK CAPITAL LETTER MU", latin: "M" }],
130
+ [0x039d, { name: "GREEK CAPITAL LETTER NU", latin: "N" }],
131
+ [0x039f, { name: "GREEK CAPITAL LETTER OMICRON", latin: "O" }],
132
+ [0x03a1, { name: "GREEK CAPITAL LETTER RHO", latin: "P" }],
133
+ [0x03a4, { name: "GREEK CAPITAL LETTER TAU", latin: "T" }],
134
+ [0x03a5, { name: "GREEK CAPITAL LETTER UPSILON", latin: "Y" }],
135
+ [0x03a7, { name: "GREEK CAPITAL LETTER CHI", latin: "X" }],
136
+ [0x03f9, { name: "GREEK CAPITAL LUNATE SIGMA SYMBOL", latin: "C" }]
137
+ ]);
@@ -0,0 +1,16 @@
1
+ import { rangeFromUtf16 } from "../source/offsets.js";
2
+ import { UNICODE_RULES_VERSION } from "./data.js";
3
+ export { UNICODE_RULES_VERSION };
4
+ export interface UnicodeFinding {
5
+ id: string;
6
+ code_point: string;
7
+ name: string;
8
+ severity: "note" | "low" | "medium" | "high";
9
+ message: string;
10
+ suggestion: string;
11
+ span: ReturnType<typeof rangeFromUtf16>;
12
+ matched_text_hash: string;
13
+ fix: "remove" | "space" | "review";
14
+ limitations: string[];
15
+ }
16
+ export declare function inspectUnicode(text: string): UnicodeFinding[];
@@ -0,0 +1,84 @@
1
+ import { rangeFromUtf16 } from "../source/offsets.js";
2
+ import { prefixedSha256 } from "../source/utf8.js";
3
+ import { CARRIER_RANGE_RULES, CARRIER_RULES, CONFUSABLES, UNICODE_RULES_VERSION } from "./data.js";
4
+ export { UNICODE_RULES_VERSION };
5
+ const TABLE = new Map();
6
+ for (const rule of CARRIER_RULES) {
7
+ for (let cp = rule.from; cp <= (rule.to ?? rule.from); cp++) {
8
+ TABLE.set(cp, { name: typeof rule.name === "function" ? rule.name(cp) : rule.name, severity: rule.severity, fix: rule.fix, message: rule.message, context: rule.context, limitation: rule.limitation });
9
+ }
10
+ }
11
+ const BASE_LIMIT = "Unicode controls can be legitimate in multilingual text; this finding is not evidence of authorship.";
12
+ // Emoji-capable neighbours for ZWJ sequences and variation selectors: pictographs, skin-tone modifiers, VS16, keycap bases.
13
+ const EMOJI_SIDE = /[\p{Extended_Pictographic}\u{1F3FB}-\u{1F3FF}\uFE0F0-9#*]/u;
14
+ // Scripts whose orthography legitimately uses ZWJ/ZWNJ between letters (cursive joining and Indic conjunct control).
15
+ const JOINING_SCRIPT = /[\p{sc=Arabic}\p{sc=Syriac}\p{sc=Nko}\p{sc=Mongolian}\p{sc=Devanagari}\p{sc=Bengali}\p{sc=Gurmukhi}\p{sc=Gujarati}\p{sc=Oriya}\p{sc=Tamil}\p{sc=Telugu}\p{sc=Kannada}\p{sc=Malayalam}\p{sc=Sinhala}\p{sc=Myanmar}\p{sc=Khmer}\p{sc=Tibetan}]/u;
16
+ const CJK_BASE = /[\p{sc=Han}\p{sc=Hiragana}\p{sc=Katakana}\p{sc=Mongolian}]/u;
17
+ function contextualise(rule, cp, prev, next) {
18
+ if (rule.context === "joiner") {
19
+ if ((cp === 0x200d) && prev && next && EMOJI_SIDE.test(prev) && EMOJI_SIDE.test(next))
20
+ return undefined; // legitimate emoji ZWJ sequence
21
+ if (prev && next && JOINING_SCRIPT.test(prev) && JOINING_SCRIPT.test(next))
22
+ return undefined; // legitimate cursive/Indic joining control
23
+ return rule;
24
+ }
25
+ if (rule.context === "variation") {
26
+ if (prev && EMOJI_SIDE.test(prev))
27
+ return undefined; // presentation selector on an emoji-capable base
28
+ if (prev && CJK_BASE.test(prev))
29
+ return { ...rule, severity: "note", fix: "review", message: `${rule.message} It follows a base character that commonly takes glyph variants.` };
30
+ return rule;
31
+ }
32
+ if (rule.context === "variation_sup") {
33
+ if (prev && CJK_BASE.test(prev))
34
+ return { ...rule, severity: "note", message: `${rule.message} It follows a CJK ideograph and may select a registered glyph variant.` };
35
+ return rule;
36
+ }
37
+ return rule;
38
+ }
39
+ /** Range-scanned carriers (private-use areas) that are too large to expand into TABLE. */
40
+ function rangeRule(cp) {
41
+ for (const rule of CARRIER_RANGE_RULES) {
42
+ if (cp >= rule.from && cp <= (rule.to ?? rule.from))
43
+ return { name: typeof rule.name === "function" ? rule.name(cp) : rule.name, severity: rule.severity, fix: rule.fix, message: rule.message, context: rule.context, limitation: rule.limitation };
44
+ }
45
+ return undefined;
46
+ }
47
+ export function inspectUnicode(text) {
48
+ const findings = [];
49
+ let prev = "";
50
+ for (let i = 0; i < text.length;) {
51
+ const cp = text.codePointAt(i);
52
+ const width = cp > 0xffff ? 2 : 1;
53
+ const raw = text.slice(i, i + width);
54
+ const base = TABLE.get(cp) ?? (cp >= 0xe000 ? rangeRule(cp) : undefined);
55
+ if (base) {
56
+ const next = i + width < text.length ? String.fromCodePoint(text.codePointAt(i + width)) : "";
57
+ const rule = contextualise(base, cp, prev, next);
58
+ if (rule) {
59
+ findings.push({ id: `unicode_${i}_${cp.toString(16)}`, code_point: `U+${cp.toString(16).toUpperCase().padStart(4, "0")}`, name: rule.name, severity: rule.severity, message: rule.message, suggestion: rule.fix === "review" ? "Review the surrounding script and direction before editing." : "Preview the deterministic change before approval.", span: rangeFromUtf16(text, i, i + width), matched_text_hash: prefixedSha256(raw), fix: rule.fix, limitations: rule.limitation ? [BASE_LIMIT, rule.limitation] : [BASE_LIMIT] });
60
+ }
61
+ }
62
+ if (cp >= 0xd800 && cp <= 0xdfff) {
63
+ findings.push({ id: `unicode_${i}_surrogate`, code_point: `U+${cp.toString(16).toUpperCase()}`, name: "UNPAIRED SURROGATE", severity: "high", message: "An unpaired UTF-16 surrogate cannot be encoded as valid UTF-8.", suggestion: "Replace or remove it after checking the source encoding.", span: { start_utf16: i, end_utf16: i + 1, start_codepoint: Array.from(text.slice(0, i)).length, end_codepoint: Array.from(text.slice(0, i)).length + 1 }, matched_text_hash: prefixedSha256("�"), fix: "review", limitations: ["The displayed replacement may differ from the original invalid code unit."] });
64
+ }
65
+ prev = raw;
66
+ i += width;
67
+ }
68
+ const tokens = [...text.matchAll(/[\p{L}\p{N}_-]+/gu)];
69
+ for (const token of tokens) {
70
+ const value = token[0];
71
+ if (!/\p{Script=Latin}/u.test(value) || !/\p{Script=Cyrillic}|\p{Script=Greek}/u.test(value))
72
+ continue;
73
+ let local = 0;
74
+ for (const char of value) {
75
+ const cp = char.codePointAt(0), entry = CONFUSABLES.get(cp);
76
+ if (entry) {
77
+ const start = token.index + local;
78
+ findings.push({ id: `unicode_${start}_homoglyph_${cp.toString(16)}`, code_point: `U+${cp.toString(16).toUpperCase().padStart(4, "0")}`, name: entry.name, severity: "medium", message: `A mixed-script token contains a character visually confusable with Latin '${entry.latin}'.`, suggestion: "Verify the intended spelling; homoglyphs are never replaced automatically.", span: rangeFromUtf16(text, start, start + char.length), matched_text_hash: prefixedSha256(char), fix: "review", limitations: ["Mixed scripts can be legitimate in names and multilingual text; this is contextual evidence only."] });
79
+ }
80
+ local += char.length;
81
+ }
82
+ }
83
+ return findings;
84
+ }
@@ -0,0 +1,162 @@
1
+ import type { EditorialSignalsResult } from "../patterns/en-signals-v2.js";
2
+ import type { UnicodeFinding } from "../unicode/inspect.js";
3
+ export declare const COMBINED_VERDICT_VERSION = "combined:2026.08.8";
4
+ /** Evidence tier assigned to a single character finding. */
5
+ export type EvidenceTier = "deliberate" | "supporting" | "excluded";
6
+ /** Watermark scan outcome. Only "detected" is treated as evidence. */
7
+ export type WatermarkOutcome = "detected" | "not_detected" | "not_available" | "not_supported";
8
+ /**
9
+ * AXIS A. The only authorship reading the engine publishes, and the only place
10
+ * the words "AI" and "human" may appear as a verdict. `not_assessed` means no
11
+ * trained model ran; it never means "human".
12
+ */
13
+ export type AiReading = "ai_like" | "uncertain" | "human_like" | "not_assessed";
14
+ /**
15
+ * AXIS B. What was done to the text.
16
+ * - `clean` nothing reportable, or only characters with a documented
17
+ * legitimate use.
18
+ * - `attention` something is present that has no ordinary authoring
19
+ * explanation and is worth an editor's eye.
20
+ * - `manipulated` the shape of a deliberate payload or a generator's mark:
21
+ * something wrote into this text on purpose.
22
+ */
23
+ export type IntegrityStatus = "clean" | "attention" | "manipulated";
24
+ /** AXIS C. How much editorial feedback the named writing rules produced. */
25
+ export type SuggestionLevel = "none" | "some" | "many";
26
+ export type ConfidenceBand = "low" | "medium" | "high";
27
+ export interface CombinedEvidenceItem {
28
+ /** The finding id it came from, so a UI can link the verdict back to the row. */
29
+ finding_id: string;
30
+ code_point: string;
31
+ name: string;
32
+ tier: EvidenceTier;
33
+ /** Why this character landed in this tier. */
34
+ rationale: string;
35
+ }
36
+ /** One integrity finding on axis B. Never carries an authorship claim. */
37
+ export interface IntegrityFinding {
38
+ /** Stable id of the rule that fired, so a UI can link back to the row. */
39
+ applied: string;
40
+ /** The status this finding argues for on its own. */
41
+ status: IntegrityStatus;
42
+ /** Plain-English description of what was found in the text. */
43
+ reason: string;
44
+ }
45
+ /** A trained-model reading. The ONLY permitted source of an AI probability. */
46
+ export interface ModelReading {
47
+ /** Model identifier, e.g. "local-signals-cycle2". */
48
+ name: string;
49
+ /** Model/artefact version string. */
50
+ version?: string;
51
+ /** Probability in [0,1] that a machine composed this text. */
52
+ probability: number;
53
+ /** The operating point the model was calibrated to, in the shipped runtime. */
54
+ threshold: number;
55
+ /** The band below which the model declines to give a reading, e.g. a word floor. */
56
+ below_reliable_range?: boolean;
57
+ }
58
+ export interface CombinedVerdictInput {
59
+ /** The writing-signals result. Omitted when style.patterns was not requested. */
60
+ signals?: EditorialSignalsResult;
61
+ /** Every Unicode finding, carriers and homoglyphs alike. */
62
+ unicodeFindings?: readonly UnicodeFinding[];
63
+ /** The inspected text. Required to grade homoglyphs and carrier runs; without
64
+ * it homoglyphs are graded down to supporting and runs are not detected. */
65
+ text?: string;
66
+ /** The watermark scan outcome, when a watermark method ran. */
67
+ watermark?: {
68
+ outcome: WatermarkOutcome;
69
+ };
70
+ /** A trained-model reading, when one ran. Nothing else may set axis A. */
71
+ model?: ModelReading;
72
+ }
73
+ export interface CombinedVerdictResult {
74
+ /**
75
+ * AXIS A — the AI reading. Set only from `input.model`. Character evidence,
76
+ * writing rules and the watermark scan can never reach this object.
77
+ */
78
+ ai_probability: {
79
+ /** The model that produced the reading, or null when none ran. */
80
+ source: string | null;
81
+ /** The model's probability, or null when no model ran. */
82
+ value: number | null;
83
+ /** The operating point the reading was compared against. */
84
+ threshold: number | null;
85
+ reading: AiReading;
86
+ confidence: ConfidenceBand | "not_assessed";
87
+ reason: string;
88
+ };
89
+ /**
90
+ * AXIS B — text integrity and provenance. Reports manipulation of the text.
91
+ * It may say the text contains hidden characters. It never says the text is AI.
92
+ */
93
+ text_integrity: {
94
+ status: IntegrityStatus;
95
+ /** The finding that set the status, or null when the status is `clean`. */
96
+ applied: string | null;
97
+ reason: string;
98
+ /** Every integrity finding that fired, in precedence order. */
99
+ findings: IntegrityFinding[];
100
+ character_evidence: {
101
+ /** Deliberate carriers with near-zero innocent explanation. */
102
+ deliberate: CombinedEvidenceItem[];
103
+ /** Characters that corroborate but never raise a status on their own. */
104
+ supporting: CombinedEvidenceItem[];
105
+ /** Characters with a documented legitimate use, excluded from the status. */
106
+ excluded: CombinedEvidenceItem[];
107
+ /** Homoglyph substitutions inside an otherwise-Latin word. */
108
+ interior_homoglyph_count: number;
109
+ /** Longest run of adjacent invisible carriers with no visible character between them. */
110
+ longest_carrier_run: number;
111
+ };
112
+ watermark: {
113
+ outcome: WatermarkOutcome;
114
+ counted_as_evidence: boolean;
115
+ };
116
+ confidence: ConfidenceBand;
117
+ };
118
+ /**
119
+ * AXIS C — editorial suggestions from the named writing rules. Feedback on
120
+ * phrasing and structure. Never an authorship judgement, never counted as one.
121
+ */
122
+ editorial: {
123
+ suggestion_level: SuggestionLevel;
124
+ /** The rules' own score, carried through and never rewritten here. */
125
+ score: number | null;
126
+ /** The distinct writing-rule categories that fired, verbatim from the rules tier. */
127
+ categories_hit: readonly string[] | null;
128
+ finding_count: number | null;
129
+ confidence: ConfidenceBand | "not_assessed";
130
+ reason: string;
131
+ /**
132
+ * The rules' internal three-way distribution, published verbatim for
133
+ * transparency. Its `ai_like` member is a rules-tier artefact and is NOT an
134
+ * AI probability; axis A is the only AI probability the engine publishes.
135
+ */
136
+ rule_probabilities: EditorialSignalsResult["probabilities"] | null;
137
+ };
138
+ /** Which streams were available to this verdict. */
139
+ inputs_considered: Array<"model" | "writing_signals" | "invisible_unicode" | "homoglyphs" | "watermark">;
140
+ version: string;
141
+ /** Plain-English claim boundaries. Always includes the axis and authorship boundaries. */
142
+ limitations: [string, ...string[]];
143
+ description: string;
144
+ }
145
+ export declare function isPrivateUse(cp: number): boolean;
146
+ /**
147
+ * Compute the three independent readings. Nothing here merges them: each axis
148
+ * is derived only from its own evidence, and `assertAxisIndependence` throws
149
+ * rather than let a collapsed verdict be published.
150
+ */
151
+ export declare function computeCombinedVerdict(input: CombinedVerdictInput): CombinedVerdictResult;
152
+ /**
153
+ * Contract 1, enforced rather than documented. Publishing a collapsed verdict is
154
+ * a correctness failure, so it throws instead of degrading quietly.
155
+ *
156
+ * The three things that must never happen again:
157
+ * 1. an AI reading that no model produced;
158
+ * 2. an integrity or editorial string that talks about AI or human authorship;
159
+ * 3. an integrity status that character evidence and the watermark scan do not
160
+ * independently justify.
161
+ */
162
+ export declare function assertAxisIndependence(result: CombinedVerdictResult, input: CombinedVerdictInput): void;