@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,419 @@
1
+ /** Portable result/presentation semantics shared by every checker surface. */
2
+ export const CHECKER_RESULT_RUNTIME_VERSION = "checker-result:2026.09.1";
3
+ export const CYCLE5_MODEL_IDENTITY = {
4
+ identity: "tier3-cycle5-v1",
5
+ serverRegistryIdentity: "tier3-cycle5-full",
6
+ segmentationContract: "segments-v3",
7
+ inputContract: "raw-v1",
8
+ featuresContract: "features-v1",
9
+ scoringContract: "margin-v1",
10
+ flagRule: {
11
+ expression: "max(m1, m2 + 0.34) >= 3.570935",
12
+ primaryMargin: 3.570935,
13
+ secondaryGap: 0.34
14
+ }
15
+ };
16
+ export const CHECKER_SCORE_SCALE = "zero_to_one_pattern_similarity";
17
+ export const CHECKER_HONESTY_LINE = "No AI checker can prove who wrote a text — this is a pattern reading.";
18
+ export const CHECKER_LEVELS = {
19
+ "signal-strongly-ai": {
20
+ name: "Strongly AI",
21
+ support: "This draft very strongly matches AI writing — the kind of match we rarely see in human work."
22
+ },
23
+ "signal-likely-ai": {
24
+ name: "Likely AI",
25
+ support: "Much of this draft reads like AI writing."
26
+ },
27
+ "signal-potentially-ai": {
28
+ name: "Potentially AI",
29
+ support: "Parts of this draft resemble AI writing, but the match is not strong enough to be sure."
30
+ },
31
+ "signal-unclear": {
32
+ name: "Unclear",
33
+ support: "We can't call this one either way. Some passages read slightly machine-like, but people write that way too."
34
+ },
35
+ "signal-likely-human": {
36
+ name: "Likely human",
37
+ support: "This reads like human writing. Nothing here matches the AI patterns we test for — though a heavily disguised AI draft can slip past any checker, ours included."
38
+ }
39
+ };
40
+ const BAND_TO_LEVEL = {
41
+ very_likely_ai: "signal-strongly-ai",
42
+ uncertain: "signal-potentially-ai",
43
+ likely_human: "signal-unclear",
44
+ very_likely_human: "signal-likely-human"
45
+ };
46
+ const METHOD_STATUSES = new Set(["pass", "attention", "fail", "inconclusive", "unsupported", "not_configured", "not_run", "error"]);
47
+ const CONTENT_KEYS = new Set(["content", "text", "passage", "excerpt", "source_uri", "candidate", "route_path", "evidence"]);
48
+ function probability(value, name) {
49
+ if (!Number.isFinite(value) || value < 0 || value > 1)
50
+ throw new RangeError(`${name} must be a finite zero-to-one score.`);
51
+ return value;
52
+ }
53
+ /**
54
+ * Map the model's measured band to the five product bands. The secondary
55
+ * threshold only refines the model's uncertain band; it never decides the
56
+ * underlying model flag, which remains the margin-v1 rule.
57
+ */
58
+ export function levelForCycle5Score(rawScore, bandId, secondaryThreshold) {
59
+ probability(rawScore, "rawScore");
60
+ const level = BAND_TO_LEVEL[bandId];
61
+ if (!level)
62
+ throw new Error(`Unknown Cycle-5 band: ${String(bandId)}`);
63
+ if (bandId === "uncertain" && secondaryThreshold !== undefined) {
64
+ probability(secondaryThreshold, "secondaryThreshold");
65
+ if (rawScore >= secondaryThreshold)
66
+ return "signal-likely-ai";
67
+ }
68
+ return level;
69
+ }
70
+ /**
71
+ * Format every score in one run together. Precision rises only for values
72
+ * which would otherwise print the same number beside different level names.
73
+ */
74
+ export function formatCheckerScoreTexts(entries, startDecimals = 2, maxDecimals = 6) {
75
+ if (!Number.isInteger(startDecimals) || !Number.isInteger(maxDecimals) || startDecimals < 0 || maxDecimals < startDecimals) {
76
+ throw new RangeError("Score precision bounds are invalid.");
77
+ }
78
+ entries.forEach((entry, index) => probability(entry.rawScore, `entries[${index}].rawScore`));
79
+ const decimals = entries.map(() => startDecimals);
80
+ for (let pass = startDecimals; pass < maxDecimals; pass += 1) {
81
+ const groups = new Map();
82
+ entries.forEach((entry, index) => {
83
+ const text = entry.rawScore.toFixed(decimals[index]);
84
+ const group = groups.get(text);
85
+ if (group)
86
+ group.push(index);
87
+ else
88
+ groups.set(text, [index]);
89
+ });
90
+ let changed = false;
91
+ for (const group of groups.values()) {
92
+ if (new Set(group.map((index) => entries[index].level)).size > 1) {
93
+ for (const index of group)
94
+ decimals[index] = decimals[index] + 1;
95
+ changed = true;
96
+ }
97
+ }
98
+ if (!changed)
99
+ break;
100
+ }
101
+ return entries.map((entry, index) => entry.rawScore.toFixed(decimals[index]));
102
+ }
103
+ const DEFAULT_AI_LIMITATIONS = [
104
+ "The score is a zero-to-one pattern-similarity reading, not a percentage of the text written by AI.",
105
+ "This result does not prove authorship, and edited or out-of-register writing may be missed."
106
+ ];
107
+ /** Build the only canonical five-band AI presentation used by pages, reports and shares. */
108
+ export function presentCycle5Result(input) {
109
+ probability(input.rawScore, "rawScore");
110
+ probability(input.primaryDisplayThreshold, "primaryDisplayThreshold");
111
+ probability(input.secondaryDisplayThreshold, "secondaryDisplayThreshold");
112
+ if (!input.sections.length)
113
+ throw new Error("An assessed Cycle-5 result requires at least one scored section.");
114
+ const sections = input.sections.map((section, position) => {
115
+ if (!Number.isInteger(section.index) || section.index !== position)
116
+ throw new Error("Scored sections must be zero-based and in source order.");
117
+ if (!Number.isInteger(section.startUtf16) || !Number.isInteger(section.endUtf16) || section.startUtf16 < 0 || section.endUtf16 <= section.startUtf16) {
118
+ throw new Error(`Section ${section.index} has an invalid UTF-16 range.`);
119
+ }
120
+ if (position > 0 && section.startUtf16 < input.sections[position - 1].endUtf16)
121
+ throw new Error("Scored section ranges must not overlap or move backwards.");
122
+ if (!Number.isInteger(section.wordCount) || section.wordCount < 0)
123
+ throw new Error(`Section ${section.index} has an invalid word count.`);
124
+ if (section.passage === undefined && section.locator === undefined)
125
+ throw new Error(`Section ${section.index} needs a passage or a content-free locator.`);
126
+ return {
127
+ index: section.index,
128
+ start_utf16: section.startUtf16,
129
+ end_utf16: section.endUtf16,
130
+ word_count: section.wordCount,
131
+ raw_score: probability(section.rawScore, `sections[${section.index}].rawScore`),
132
+ raw_margin: finiteNumber(section.rawMargin, `sections[${section.index}].rawMargin`),
133
+ display_score: "",
134
+ level: levelForCycle5Score(section.rawScore, section.bandId, input.secondaryDisplayThreshold),
135
+ band_id: section.bandId,
136
+ ...(section.passage === undefined ? {} : { passage: section.passage }),
137
+ ...(section.locator === undefined ? {} : { locator: { ...section.locator } }),
138
+ evidence: section.evidence.map((item) => ({ ...item }))
139
+ };
140
+ });
141
+ const strongest = sections.reduce((best, section) => section.raw_score > best.raw_score ? section : best, sections[0]);
142
+ if (Math.abs(strongest.raw_score - input.rawScore) > Number.EPSILON)
143
+ throw new Error("The run-wide score must equal the strongest section's raw score.");
144
+ if (Math.abs(strongest.raw_margin - input.rawMargin) > Number.EPSILON)
145
+ throw new Error("The run-wide margin must equal the strongest section's raw margin.");
146
+ const margins = sections.map((section) => section.raw_margin).sort((a, b) => b - a);
147
+ const primaryFired = margins[0] >= CYCLE5_MODEL_IDENTITY.flagRule.primaryMargin;
148
+ const secondaryFired = margins.length > 1 && margins[1] + CYCLE5_MODEL_IDENTITY.flagRule.secondaryGap >= CYCLE5_MODEL_IDENTITY.flagRule.primaryMargin;
149
+ const expectedFlagged = primaryFired || secondaryFired;
150
+ const expectedReason = primaryFired ? "primary" : secondaryFired ? "secondary" : null;
151
+ if (input.flagged !== expectedFlagged || input.flagReason !== expectedReason)
152
+ throw new Error("The supplied flag state contradicts the Cycle-5 margin rule.");
153
+ const level = levelForCycle5Score(input.rawScore, input.bandId, input.secondaryDisplayThreshold);
154
+ const display = formatCheckerScoreTexts([
155
+ { rawScore: input.rawScore, level },
156
+ ...sections.map((section) => ({ rawScore: section.raw_score, level: section.level }))
157
+ ]);
158
+ sections.forEach((section, index) => { section.display_score = display[index + 1]; });
159
+ const limits = [...new Set([...(input.limitations ?? []), ...DEFAULT_AI_LIMITATIONS])];
160
+ const result = {
161
+ ai_pattern: {
162
+ assessment_status: "assessed",
163
+ method_status: input.flagged ? "attention" : "pass",
164
+ source: input.source,
165
+ raw_score: input.rawScore,
166
+ raw_margin: input.rawMargin,
167
+ display_score: display[0],
168
+ score_scale: CHECKER_SCORE_SCALE,
169
+ level,
170
+ primary_display_threshold: input.primaryDisplayThreshold,
171
+ secondary_display_threshold: input.secondaryDisplayThreshold,
172
+ flagged: input.flagged,
173
+ flag_reason: input.flagReason,
174
+ strongest_section_index: strongest.index,
175
+ reason: input.reason ?? CHECKER_LEVELS[level].support,
176
+ limitations: limits
177
+ },
178
+ sections
179
+ };
180
+ return deepFreeze(result);
181
+ }
182
+ export function notAssessedAiPattern(reason = "No trained model ran on this text, so the AI-pattern reading is not assessed.") {
183
+ return deepFreeze({
184
+ assessment_status: "not_assessed",
185
+ method_status: "not_run",
186
+ source: null,
187
+ raw_score: null,
188
+ raw_margin: null,
189
+ display_score: null,
190
+ score_scale: CHECKER_SCORE_SCALE,
191
+ level: null,
192
+ primary_display_threshold: null,
193
+ secondary_display_threshold: null,
194
+ flagged: null,
195
+ flag_reason: null,
196
+ strongest_section_index: null,
197
+ reason,
198
+ limitations: [...DEFAULT_AI_LIMITATIONS]
199
+ });
200
+ }
201
+ /**
202
+ * Reuse the compiled core's independent integrity/editorial axes without
203
+ * letting either one manufacture an AI result.
204
+ */
205
+ export function composeCheckerAxes(combined, presented) {
206
+ if (presented) {
207
+ if (combined.ai_probability.source !== presented.ai_pattern.source || combined.ai_probability.value !== presented.ai_pattern.raw_score) {
208
+ throw new Error("The trained-model reading and the five-band presentation describe different runs.");
209
+ }
210
+ }
211
+ else if (combined.ai_probability.reading !== "not_assessed" || combined.ai_probability.value !== null) {
212
+ throw new Error("A model result cannot be discarded and replaced with not_assessed.");
213
+ }
214
+ const textStatus = combined.text_integrity.status;
215
+ const editorialLevel = combined.editorial.suggestion_level;
216
+ return deepFreeze({
217
+ ai_pattern: presented?.ai_pattern ?? notAssessedAiPattern(),
218
+ text_integrity: {
219
+ method_status: textStatus === "clean" ? "pass" : "attention",
220
+ reading: textStatus,
221
+ reason: combined.text_integrity.reason,
222
+ findings: combined.text_integrity.findings.map((finding) => ({ ...finding })),
223
+ limitations: [...combined.limitations]
224
+ },
225
+ editorial: {
226
+ method_status: editorialLevel === "none" ? "pass" : "attention",
227
+ reading: editorialLevel,
228
+ reason: combined.editorial.reason,
229
+ findings: (combined.editorial.categories_hit ?? []).map((category) => ({ category })),
230
+ limitations: [...combined.limitations]
231
+ }
232
+ });
233
+ }
234
+ /** Build a share/export object whose type cannot accept draft text or evidence. */
235
+ export function buildContentFreeSharePayload(input) {
236
+ if (!Number.isInteger(input.wordCount) || input.wordCount < 0)
237
+ throw new Error("Share word count must be a non-negative integer.");
238
+ const date = new Date(input.generatedAt);
239
+ if (!Number.isFinite(date.valueOf()))
240
+ throw new Error("Share generation time must be a valid date-time.");
241
+ const payload = {
242
+ version: 1,
243
+ result_id: input.resultId,
244
+ level: input.presented.ai_pattern.level,
245
+ display_score: input.presented.ai_pattern.display_score,
246
+ sections: input.presented.sections.map((section) => ({
247
+ index: section.index,
248
+ raw_score: section.raw_score,
249
+ display_score: section.display_score,
250
+ level: section.level
251
+ })),
252
+ word_count: input.wordCount,
253
+ date: date.toISOString().slice(0, 10),
254
+ model_version: input.modelVersion,
255
+ honesty_line: CHECKER_HONESTY_LINE,
256
+ contains_content: false
257
+ };
258
+ assertContentFree(payload);
259
+ return deepFreeze(payload);
260
+ }
261
+ function assertContentFree(value, path = "share") {
262
+ if (Array.isArray(value)) {
263
+ value.forEach((item, index) => assertContentFree(item, `${path}[${index}]`));
264
+ return;
265
+ }
266
+ if (typeof value !== "object" || value === null)
267
+ return;
268
+ for (const [key, item] of Object.entries(value)) {
269
+ if (CONTENT_KEYS.has(key.toLowerCase()))
270
+ throw new Error(`${path}.${key} is content-bearing and cannot appear in a share payload.`);
271
+ assertContentFree(item, `${path}.${key}`);
272
+ }
273
+ }
274
+ /**
275
+ * Producer-side semantic checks complement the additive-field-tolerant JSON
276
+ * Schema. Run this immediately before serialising, sharing or rendering.
277
+ */
278
+ export function assertCheckerResultInvariants(result) {
279
+ if (result.schema_version !== "1.0" || !/^1\./u.test(result.contract_version))
280
+ throw new Error("Unsupported checker result contract.");
281
+ for (const method of result.methods)
282
+ if (!METHOD_STATUSES.has(method.status))
283
+ throw new Error(`Unknown method status: ${String(method.status)}`);
284
+ if (result.exports.receipt.contains_content !== false)
285
+ throw new Error("The default receipt must be content-free.");
286
+ if (result.exports.share.contains_content !== false)
287
+ throw new Error("The share export must be content-free.");
288
+ if (result.exports.share.payload)
289
+ assertContentFree(result.exports.share.payload);
290
+ if (result.provenance.safe_fixes.preview_first !== true || result.provenance.safe_fixes.explicit_approval_required !== true || result.provenance.safe_fixes.automatic_homoglyph_replacement !== false || result.provenance.safe_fixes.c2pa_wrapper_protected !== true) {
291
+ throw new Error("Safe-fix and C2PA wrapper invariants were weakened.");
292
+ }
293
+ for (const watermark of result.provenance.watermarks) {
294
+ if (watermark.method_id === "watermark.anthropic" && (watermark.method_status !== "unsupported" || !["not_available", "not_supported"].includes(watermark.outcome))) {
295
+ throw new Error("watermark.anthropic must remain unsupported until an official interface exists.");
296
+ }
297
+ }
298
+ const ai = result.axes.ai_pattern;
299
+ if (ai.assessment_status === "assessed") {
300
+ if (!result.route.model)
301
+ throw new Error("Only a trained model may set the AI-pattern reading.");
302
+ assertCycle5Identity(result.route.model);
303
+ if (!result.sections.length)
304
+ throw new Error("An assessed result needs scored sections.");
305
+ const ordered = [...result.sections].sort((a, b) => a.index - b.index);
306
+ ordered.forEach((section, index) => {
307
+ if (section.index !== index || section !== result.sections[index])
308
+ throw new Error("Scored sections must remain in source order.");
309
+ if (!section.band_id)
310
+ throw new Error(`Section ${section.index} has no measured band identity.`);
311
+ const bandId = section.band_id;
312
+ const expectedLevel = ai.secondary_display_threshold === null && bandId === "uncertain"
313
+ ? section.level
314
+ : levelForCycle5Score(section.raw_score, bandId, ai.secondary_display_threshold ?? undefined);
315
+ if (section.level !== expectedLevel || (bandId === "uncertain" && ai.secondary_display_threshold === null && !["signal-potentially-ai", "signal-likely-ai"].includes(section.level))) {
316
+ throw new Error(`Section ${section.index} level does not match its raw score and measured band.`);
317
+ }
318
+ });
319
+ const strongest = result.sections.reduce((best, section) => section.raw_score > best.raw_score ? section : best, result.sections[0]);
320
+ if (ai.strongest_section_index !== strongest.index || ai.raw_score !== strongest.raw_score || ai.raw_margin !== strongest.raw_margin)
321
+ throw new Error("The run-wide result does not identify the strongest section.");
322
+ if (ai.level !== strongest.level)
323
+ throw new Error("The run-wide level does not match the strongest section.");
324
+ if (ai.source !== result.route.model.identity)
325
+ throw new Error("The AI-pattern source does not match the executed model.");
326
+ if (result.source.section_count !== result.sections.length)
327
+ throw new Error("The source section count does not match the scored section set.");
328
+ if (ai.method_status !== (ai.flagged ? "attention" : "pass"))
329
+ throw new Error("The AI method status contradicts its flag state.");
330
+ result.sections.forEach((section, index) => {
331
+ if (index > 0 && section.start_utf16 < result.sections[index - 1].end_utf16)
332
+ throw new Error("Scored section ranges overlap or move backwards.");
333
+ if (section.locator && (section.locator.content_hash !== result.source.content_hash || section.locator.start_utf16 !== section.start_utf16 || section.locator.end_utf16 !== section.end_utf16)) {
334
+ throw new Error(`Section ${section.index} locator does not match its source or range.`);
335
+ }
336
+ });
337
+ const margins = result.sections.map((section) => section.raw_margin).sort((a, b) => b - a);
338
+ const primaryFired = margins[0] >= CYCLE5_MODEL_IDENTITY.flagRule.primaryMargin;
339
+ const secondaryFired = margins.length > 1 && margins[1] + CYCLE5_MODEL_IDENTITY.flagRule.secondaryGap >= CYCLE5_MODEL_IDENTITY.flagRule.primaryMargin;
340
+ const expectedFlagged = primaryFired || secondaryFired;
341
+ const expectedReason = primaryFired ? "primary" : secondaryFired ? "secondary" : null;
342
+ if (ai.flagged !== expectedFlagged || ai.flag_reason !== expectedReason)
343
+ throw new Error("The AI flag state contradicts the recorded Cycle-5 margins.");
344
+ const expected = formatCheckerScoreTexts([
345
+ { rawScore: ai.raw_score, level: ai.level },
346
+ ...result.sections.map((section) => ({ rawScore: section.raw_score, level: section.level }))
347
+ ]);
348
+ if (ai.display_score !== expected[0] || result.sections.some((section, index) => section.display_score !== expected[index + 1])) {
349
+ throw new Error("Visible/exported scores were not produced by the run-wide formatter.");
350
+ }
351
+ if (result.profile === "full_checker" && (!result.exports.report.available || !result.exports.report.complete_evidence)) {
352
+ throw new Error("An assessed full-checker result requires a complete report export.");
353
+ }
354
+ }
355
+ else {
356
+ if (ai.raw_score !== null || ai.raw_margin !== null || ai.display_score !== null || ai.level !== null || ai.flagged !== null || ai.strongest_section_index !== null) {
357
+ throw new Error("An unassessed, withheld or errored AI axis cannot publish a score or level.");
358
+ }
359
+ if (result.sections.length)
360
+ throw new Error("An unassessed result cannot publish model-scored sections.");
361
+ }
362
+ if (result.route.model === null && ai.assessment_status === "assessed")
363
+ throw new Error("Deterministic evidence cannot set the AI-pattern reading.");
364
+ assertRouteConsistency(result);
365
+ if (result.abuse_controls.request_body_logging !== "excluded" && result.abuse_controls.request_body_logging !== "not_applicable") {
366
+ throw new Error("A checker route may not claim readiness while request-body logging is unconfigured.");
367
+ }
368
+ }
369
+ function assertRouteConsistency(result) {
370
+ const { route, abuse_controls: controls } = result;
371
+ if (route.kind === "browser_model") {
372
+ if (route.content_transfer !== "none" || route.privacy_route !== "browser" || route.model?.precision !== "int8")
373
+ throw new Error("Browser-model route metadata is inconsistent.");
374
+ }
375
+ else if (route.kind === "eu_server") {
376
+ if (route.content_transfer !== "eu_server" || route.consent !== "explicit" || route.model?.precision !== "fp32")
377
+ throw new Error("EU-server route metadata is inconsistent.");
378
+ if (!["browser_pow_token", "chrome_extension_challenge_token", "wordpress_challenge_token"].includes(controls.channel_authentication))
379
+ throw new Error("EU-server results require their real channel authentication class.");
380
+ }
381
+ else if (route.kind === "wordpress_same_site") {
382
+ if (route.content_transfer !== "same_site" || route.privacy_route !== "wordpress_local" || controls.channel_authentication !== "same_site_nonce")
383
+ throw new Error("WordPress same-site route metadata is inconsistent.");
384
+ }
385
+ else if (route.kind === "loopback_engine") {
386
+ if (route.content_transfer !== "loopback" || route.privacy_route !== "local_service" || controls.channel_authentication !== "loopback_bearer")
387
+ throw new Error("Loopback route metadata is inconsistent.");
388
+ }
389
+ else if (route.kind === "deterministic_only") {
390
+ if (route.content_transfer !== "none" || route.model !== null || result.axes.ai_pattern.assessment_status === "assessed")
391
+ throw new Error("A deterministic-only route cannot publish a model result.");
392
+ }
393
+ }
394
+ function assertCycle5Identity(model) {
395
+ if (model.identity !== CYCLE5_MODEL_IDENTITY.identity ||
396
+ model.segmentation_contract !== CYCLE5_MODEL_IDENTITY.segmentationContract ||
397
+ model.input_contract !== CYCLE5_MODEL_IDENTITY.inputContract ||
398
+ model.features_contract !== CYCLE5_MODEL_IDENTITY.featuresContract ||
399
+ model.scoring_contract !== CYCLE5_MODEL_IDENTITY.scoringContract ||
400
+ model.flag_rule.expression !== CYCLE5_MODEL_IDENTITY.flagRule.expression ||
401
+ model.flag_rule.primary_margin !== CYCLE5_MODEL_IDENTITY.flagRule.primaryMargin ||
402
+ model.flag_rule.secondary_gap !== CYCLE5_MODEL_IDENTITY.flagRule.secondaryGap)
403
+ throw new Error("The assessed result does not identify the current Cycle-5 contracts and margin rule.");
404
+ if (model.precision === "fp32" && model.registry_identity !== CYCLE5_MODEL_IDENTITY.serverRegistryIdentity) {
405
+ throw new Error("The fp32 server route must identify the Cycle-5 server registry entry.");
406
+ }
407
+ }
408
+ function finiteNumber(value, name) {
409
+ if (!Number.isFinite(value))
410
+ throw new RangeError(`${name} must be finite.`);
411
+ return value;
412
+ }
413
+ function deepFreeze(value) {
414
+ if (typeof value !== "object" || value === null || Object.isFrozen(value))
415
+ return value;
416
+ for (const child of Object.values(value))
417
+ deepFreeze(child);
418
+ return Object.freeze(value);
419
+ }
@@ -0,0 +1,34 @@
1
+ export interface OffsetRange {
2
+ [key: string]: unknown;
3
+ start_utf16: number;
4
+ end_utf16: number;
5
+ start_codepoint: number;
6
+ end_codepoint: number;
7
+ }
8
+ /**
9
+ * Snap a UTF-16 span outward to code-point boundaries.
10
+ *
11
+ * A boundary landing inside a surrogate pair is moved to the edge of the
12
+ * enclosing pair: `start` snaps down to the high surrogate, `end` snaps up past
13
+ * the low surrogate. The span therefore always covers whole code points and can
14
+ * never cut an astral character in half. Non-astral text is untouched, and the
15
+ * result is never narrower than the input, so a finding is never dropped.
16
+ *
17
+ * Call this before slicing the matched text at a span-construction site, so the
18
+ * slice, its hash and the recorded offsets all describe the same characters.
19
+ */
20
+ export declare function alignUtf16Range(text: string, start: number, end: number): [number, number];
21
+ /**
22
+ * Count the code points preceding a UTF-16 offset.
23
+ *
24
+ * This stays strict: an offset inside a surrogate pair has no meaningful
25
+ * code-point index, so it throws rather than inventing one. Callers that build
26
+ * spans go through `alignUtf16Range`/`rangeFromUtf16`, which snap the boundary
27
+ * out to the enclosing pair first, so the throw is now reserved for genuinely
28
+ * invalid input rather than for ordinary emoji-bearing text. Unpaired
29
+ * surrogates are rejected earlier by `inspect()` validation
30
+ * (`invalid_unicode_unpaired_surrogate`) and are not a split boundary here, so
31
+ * the alignment path only ever sees well-formed pairs.
32
+ */
33
+ export declare function utf16ToCodePointOffset(text: string, offset: number): number;
34
+ export declare function rangeFromUtf16(text: string, start: number, end: number): OffsetRange;
@@ -0,0 +1,51 @@
1
+ const isHighSurrogate = (code) => code >= 0xd800 && code <= 0xdbff;
2
+ const isLowSurrogate = (code) => code >= 0xdc00 && code <= 0xdfff;
3
+ /**
4
+ * True when `offset` falls between the two halves of a well-formed surrogate
5
+ * pair. A lone surrogate never satisfies both sides, so unpaired input is
6
+ * deliberately not treated as a split boundary here.
7
+ */
8
+ function splitsSurrogatePair(text, offset) {
9
+ return offset > 0 && offset < text.length &&
10
+ isHighSurrogate(text.charCodeAt(offset - 1)) && isLowSurrogate(text.charCodeAt(offset));
11
+ }
12
+ /**
13
+ * Snap a UTF-16 span outward to code-point boundaries.
14
+ *
15
+ * A boundary landing inside a surrogate pair is moved to the edge of the
16
+ * enclosing pair: `start` snaps down to the high surrogate, `end` snaps up past
17
+ * the low surrogate. The span therefore always covers whole code points and can
18
+ * never cut an astral character in half. Non-astral text is untouched, and the
19
+ * result is never narrower than the input, so a finding is never dropped.
20
+ *
21
+ * Call this before slicing the matched text at a span-construction site, so the
22
+ * slice, its hash and the recorded offsets all describe the same characters.
23
+ */
24
+ export function alignUtf16Range(text, start, end) {
25
+ return [splitsSurrogatePair(text, start) ? start - 1 : start, splitsSurrogatePair(text, end) ? end + 1 : end];
26
+ }
27
+ /**
28
+ * Count the code points preceding a UTF-16 offset.
29
+ *
30
+ * This stays strict: an offset inside a surrogate pair has no meaningful
31
+ * code-point index, so it throws rather than inventing one. Callers that build
32
+ * spans go through `alignUtf16Range`/`rangeFromUtf16`, which snap the boundary
33
+ * out to the enclosing pair first, so the throw is now reserved for genuinely
34
+ * invalid input rather than for ordinary emoji-bearing text. Unpaired
35
+ * surrogates are rejected earlier by `inspect()` validation
36
+ * (`invalid_unicode_unpaired_surrogate`) and are not a split boundary here, so
37
+ * the alignment path only ever sees well-formed pairs.
38
+ */
39
+ export function utf16ToCodePointOffset(text, offset) {
40
+ if (!Number.isInteger(offset) || offset < 0 || offset > text.length)
41
+ throw new RangeError("invalid_utf16_offset");
42
+ if (splitsSurrogatePair(text, offset))
43
+ throw new RangeError("split_surrogate");
44
+ return Array.from(text.slice(0, offset)).length;
45
+ }
46
+ export function rangeFromUtf16(text, start, end) {
47
+ if (end <= start)
48
+ throw new RangeError("empty_or_reversed_range");
49
+ const [alignedStart, alignedEnd] = alignUtf16Range(text, start, end);
50
+ return { start_utf16: alignedStart, end_utf16: alignedEnd, start_codepoint: utf16ToCodePointOffset(text, alignedStart), end_codepoint: utf16ToCodePointOffset(text, alignedEnd) };
51
+ }
@@ -0,0 +1,3 @@
1
+ export declare function utf8Bytes(value: string): Uint8Array;
2
+ export declare function sha256Hex(value: string): string;
3
+ export declare const prefixedSha256: (value: string) => string;
@@ -0,0 +1,58 @@
1
+ const K = new Uint32Array([
2
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
3
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
4
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
5
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
6
+ ]);
7
+ const rotr = (x, n) => (x >>> n) | (x << (32 - n));
8
+ export function utf8Bytes(value) {
9
+ return new TextEncoder().encode(value);
10
+ }
11
+ export function sha256Hex(value) {
12
+ const source = utf8Bytes(value);
13
+ const bitLength = source.length * 8;
14
+ const paddedLength = Math.ceil((source.length + 9) / 64) * 64;
15
+ const bytes = new Uint8Array(paddedLength);
16
+ bytes.set(source);
17
+ bytes[source.length] = 0x80;
18
+ const view = new DataView(bytes.buffer);
19
+ view.setUint32(paddedLength - 4, bitLength >>> 0, false);
20
+ view.setUint32(paddedLength - 8, Math.floor(bitLength / 0x100000000), false);
21
+ const h = new Uint32Array([0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]);
22
+ const w = new Uint32Array(64);
23
+ for (let offset = 0; offset < paddedLength; offset += 64) {
24
+ for (let i = 0; i < 16; i++)
25
+ w[i] = view.getUint32(offset + i * 4, false);
26
+ for (let i = 16; i < 64; i++) {
27
+ const a = w[i - 15], b = w[i - 2];
28
+ w[i] = ((rotr(a, 7) ^ rotr(a, 18) ^ (a >>> 3)) + w[i - 16] + (rotr(b, 17) ^ rotr(b, 19) ^ (b >>> 10)) + w[i - 7]) >>> 0;
29
+ }
30
+ let [a, b, c, d, e, f, g, hh] = h;
31
+ for (let i = 0; i < 64; i++) {
32
+ const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
33
+ const ch = (e & f) ^ (~e & g);
34
+ const t1 = (hh + s1 + ch + K[i] + w[i]) >>> 0;
35
+ const s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
36
+ const maj = (a & b) ^ (a & c) ^ (b & c);
37
+ const t2 = (s0 + maj) >>> 0;
38
+ hh = g;
39
+ g = f;
40
+ f = e;
41
+ e = (d + t1) >>> 0;
42
+ d = c;
43
+ c = b;
44
+ b = a;
45
+ a = (t1 + t2) >>> 0;
46
+ }
47
+ h[0] = (h[0] + a) >>> 0;
48
+ h[1] = (h[1] + b) >>> 0;
49
+ h[2] = (h[2] + c) >>> 0;
50
+ h[3] = (h[3] + d) >>> 0;
51
+ h[4] = (h[4] + e) >>> 0;
52
+ h[5] = (h[5] + f) >>> 0;
53
+ h[6] = (h[6] + g) >>> 0;
54
+ h[7] = (h[7] + hh) >>> 0;
55
+ }
56
+ return Array.from(h, n => n.toString(16).padStart(8, "0")).join("");
57
+ }
58
+ export const prefixedSha256 = (value) => `sha256:${sha256Hex(value)}`;
@@ -0,0 +1,12 @@
1
+ export interface SourceMapRun {
2
+ visible_start_utf16: number;
3
+ visible_end_utf16: number;
4
+ source_start_utf16: number;
5
+ source_end_utf16: number;
6
+ }
7
+ export interface VisibleTextProjection {
8
+ text: string;
9
+ runs: SourceMapRun[];
10
+ limitations: string[];
11
+ }
12
+ export declare function projectVisibleText(source: string, contentType: "plain_text" | "html" | "markdown"): VisibleTextProjection;
@@ -0,0 +1,30 @@
1
+ const ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: "\u00a0" };
2
+ const decodeEntity = (raw) => { if (raw[1] !== "#")
3
+ return ENTITIES[raw.slice(1, -1)] ?? raw; const value = raw[2]?.toLowerCase() === "x" ? Number.parseInt(raw.slice(3, -1), 16) : Number.parseInt(raw.slice(2, -1), 10); return Number.isInteger(value) && value >= 0 && value <= 0x10ffff && !(value >= 0xd800 && value <= 0xdfff) ? String.fromCodePoint(value) : raw; };
4
+ export function projectVisibleText(source, contentType) {
5
+ if (contentType === "plain_text")
6
+ return { text: source, runs: source ? [{ visible_start_utf16: 0, visible_end_utf16: source.length, source_start_utf16: 0, source_end_utf16: source.length }] : [], limitations: [] };
7
+ if (contentType === "markdown") {
8
+ const text = source.replace(/```[\s\S]*?```/g, m => m).replace(/!?(\[([^\]]*)\])\(([^)]+)\)/g, (_m, _a, label) => label).replace(/(^|\s)[*_]{1,3}([^*_]+)[*_]{1,3}/g, "$1$2");
9
+ return { text, runs: text ? [{ visible_start_utf16: 0, visible_end_utf16: text.length, source_start_utf16: 0, source_end_utf16: source.length }] : [], limitations: ["Markdown mapping is projection-level; protected extractors retain exact source ranges."] };
10
+ }
11
+ let text = "", cursor = 0;
12
+ const runs = [];
13
+ const excluded = /<(script|style|template|noscript)\b[^>]*>[\s\S]*?<\/\1\s*>|<!--[\s\S]*?-->/gi;
14
+ const safe = source.replace(excluded, m => " ".repeat(m.length));
15
+ const token = /<[^>]*>|&(?:#x?[0-9a-f]+|[a-z]+);/gi;
16
+ let match;
17
+ const append = (value, start, end) => { if (!value)
18
+ return; const v = text.length; text += value; runs.push({ visible_start_utf16: v, visible_end_utf16: text.length, source_start_utf16: start, source_end_utf16: end }); };
19
+ while ((match = token.exec(safe))) {
20
+ append(safe.slice(cursor, match.index), cursor, match.index);
21
+ const raw = match[0];
22
+ if (/^<(br|\/?(?:p|div|section|article|li|h[1-6]|blockquote|tr))\b/i.test(raw))
23
+ append("\n", match.index, token.lastIndex);
24
+ else if (raw[0] === "&")
25
+ append(decodeEntity(raw), match.index, token.lastIndex);
26
+ cursor = token.lastIndex;
27
+ }
28
+ append(safe.slice(cursor), cursor, safe.length);
29
+ return { text, runs, limitations: ["HTML projection is deterministic text extraction, not sanitisation."] };
30
+ }
@@ -0,0 +1,22 @@
1
+ export declare const UNICODE_RULES_VERSION = "unicode:2026.08.2";
2
+ export type CarrierSeverity = "note" | "low" | "medium" | "high";
3
+ export type CarrierFix = "remove" | "space" | "review";
4
+ /** joiner: exempt inside emoji ZWJ sequences and between cursive/Indic letters. variation: exempt after emoji-capable bases, downgraded after Han/Mongolian. variation_sup: downgraded after Han ideographs. */
5
+ export type CarrierContext = "joiner" | "variation" | "variation_sup";
6
+ export interface CarrierRule {
7
+ from: number;
8
+ to?: number;
9
+ name: string | ((cp: number) => string);
10
+ severity: CarrierSeverity;
11
+ fix: CarrierFix;
12
+ message: string;
13
+ context?: CarrierContext;
14
+ limitation?: string;
15
+ }
16
+ export declare const CARRIER_RULES: readonly CarrierRule[];
17
+ export declare const CARRIER_RANGE_RULES: readonly CarrierRule[];
18
+ export interface Confusable {
19
+ name: string;
20
+ latin: string;
21
+ }
22
+ export declare const CONFUSABLES: ReadonlyMap<number, Confusable>;