@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.
- package/CHANGELOG.md +138 -0
- package/LICENSE +9 -0
- package/README.md +236 -0
- package/SBOM.cdx.json +172 -0
- package/SECURITY.md +9 -0
- package/THIRD_PARTY_NOTICES.md +36 -0
- package/dist/build-report-html.d.ts +3 -0
- package/dist/highlight.js +155 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +43 -0
- package/dist/options.d.ts +21 -0
- package/dist/options.js +6 -0
- package/dist/receipt.d.ts +15 -0
- package/dist/receipt.js +100 -0
- package/dist/report.d.ts +55 -0
- package/dist/report.js +12 -0
- package/dist/sections.js +181 -0
- package/dist/share.d.ts +47 -0
- package/dist/share.js +72 -0
- package/dist/shared-EX54KI6H.js +1503 -0
- package/dist/shared-MMXWTX2U.js +43 -0
- package/dist/toolbar.js +13478 -0
- package/node_modules/@opacedev/ai-content-checker-browser/LICENSE +21 -0
- package/node_modules/@opacedev/ai-content-checker-browser/README.md +145 -0
- package/node_modules/@opacedev/ai-content-checker-browser/dist/dom/visible-text.d.ts +14 -0
- package/node_modules/@opacedev/ai-content-checker-browser/dist/dom/visible-text.js +47 -0
- package/node_modules/@opacedev/ai-content-checker-browser/dist/index.d.ts +3 -0
- package/node_modules/@opacedev/ai-content-checker-browser/dist/index.js +99 -0
- package/node_modules/@opacedev/ai-content-checker-browser/dist/worker/client.d.ts +12 -0
- package/node_modules/@opacedev/ai-content-checker-browser/dist/worker/client.js +17 -0
- package/node_modules/@opacedev/ai-content-checker-browser/dist/worker/entry.d.ts +1 -0
- package/node_modules/@opacedev/ai-content-checker-browser/dist/worker/entry.js +3984 -0
- package/node_modules/@opacedev/ai-content-checker-browser/dist/worker/protocol.d.ts +30 -0
- package/node_modules/@opacedev/ai-content-checker-browser/dist/worker/protocol.js +1 -0
- package/node_modules/@opacedev/ai-content-checker-browser/package.json +60 -0
- package/node_modules/@opacedev/ai-content-checker-contracts/LICENSE +21 -0
- package/node_modules/@opacedev/ai-content-checker-contracts/README.md +93 -0
- package/node_modules/@opacedev/ai-content-checker-contracts/dist/index.d.ts +58 -0
- package/node_modules/@opacedev/ai-content-checker-contracts/dist/index.js +17 -0
- package/node_modules/@opacedev/ai-content-checker-contracts/package.json +57 -0
- package/node_modules/@opacedev/ai-content-checker-contracts/src/generated/analysis-request.schema.d.ts +44 -0
- package/node_modules/@opacedev/ai-content-checker-contracts/src/generated/analysis-result.schema.d.ts +122 -0
- package/node_modules/@opacedev/ai-content-checker-contracts/src/generated/candidate.schema.d.ts +81 -0
- package/node_modules/@opacedev/ai-content-checker-contracts/src/generated/capabilities.schema.d.ts +24 -0
- package/node_modules/@opacedev/ai-content-checker-contracts/src/generated/checker-result.schema.d.ts +342 -0
- package/node_modules/@opacedev/ai-content-checker-contracts/src/generated/common.schema.d.ts +10 -0
- package/node_modules/@opacedev/ai-content-checker-contracts/src/generated/envelope.schema.d.ts +14 -0
- package/node_modules/@opacedev/ai-content-checker-contracts/src/generated/gate-result.schema.d.ts +19 -0
- package/node_modules/@opacedev/ai-content-checker-contracts/src/generated/integrity-receipt.schema.d.ts +122 -0
- package/node_modules/@opacedev/ai-content-checker-contracts/src/generated/job.schema.d.ts +188 -0
- package/node_modules/@opacedev/ai-content-checker-contracts/src/generated/method-result.schema.d.ts +39 -0
- package/node_modules/@opacedev/ai-content-checker-contracts/src/generated/pattern-finding.schema.d.ts +26 -0
- package/node_modules/@opacedev/ai-content-checker-contracts/src/generated/protected-span.schema.d.ts +35 -0
- package/node_modules/@opacedev/ai-content-checker-contracts/src/generated/rewrite-request.schema.d.ts +65 -0
- package/node_modules/@opacedev/ai-content-checker-contracts/src/index.ts +63 -0
- package/node_modules/@opacedev/ai-content-checker-contracts/src/semantic-validation.mjs +32 -0
- package/node_modules/@opacedev/ai-content-checker-core/LICENSE +21 -0
- package/node_modules/@opacedev/ai-content-checker-core/README.md +186 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/bundle.js +4744 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/capabilities.d.ts +16 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/capabilities.js +7 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/diff/diff.d.ts +17 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/diff/diff.js +76 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/fixes/preview.d.ts +27 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/fixes/preview.js +47 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/gates/policy.d.ts +14 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/gates/policy.js +16 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/index.d.ts +15 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/index.js +15 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/inspect.d.ts +8 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/inspect.js +84 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/patterns/en-gb-v1.d.ts +12 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/patterns/en-gb-v1.js +68 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/patterns/en-signals-v2-data.d.ts +73 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/patterns/en-signals-v2-data.js +499 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/patterns/en-signals-v2.d.ts +76 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/patterns/en-signals-v2.js +1067 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/patterns/en-signals-v3-data.d.ts +165 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/patterns/en-signals-v3-data.js +614 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/patterns/en-signals-v3.d.ts +19 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/patterns/en-signals-v3.js +542 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/patterns/en-signals-v4-corpus.d.ts +40 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/patterns/en-signals-v4-corpus.js +40 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/patterns/en-signals-v4-data.d.ts +138 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/patterns/en-signals-v4-data.js +200 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/patterns/en-signals-v4.d.ts +71 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/patterns/en-signals-v4.js +388 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/protected/extract.d.ts +17 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/protected/extract.js +75 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/protected/validate.d.ts +3 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/protected/validate.js +17 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/provenance/c2pa-text.d.ts +55 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/provenance/c2pa-text.js +86 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/receipts/build.d.ts +48 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/receipts/build.js +61 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/report/checker-result.d.ts +241 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/report/checker-result.js +419 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/source/offsets.d.ts +34 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/source/offsets.js +51 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/source/utf8.d.ts +3 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/source/utf8.js +58 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/source/visible-text.d.ts +12 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/source/visible-text.js +30 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/unicode/data.d.ts +22 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/unicode/data.js +137 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/unicode/inspect.d.ts +16 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/unicode/inspect.js +84 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/verdict/combine.d.ts +162 -0
- package/node_modules/@opacedev/ai-content-checker-core/dist/verdict/combine.js +501 -0
- package/node_modules/@opacedev/ai-content-checker-core/package.json +62 -0
- package/node_modules/canonicalize/LICENSE +201 -0
- package/node_modules/canonicalize/README.md +65 -0
- package/node_modules/canonicalize/bin/canonicalize.js +17 -0
- package/node_modules/canonicalize/lib/canonicalize.d.ts +2 -0
- package/node_modules/canonicalize/lib/canonicalize.js +74 -0
- package/node_modules/canonicalize/package.json +64 -0
- package/node_modules/entities/LICENSE +11 -0
- package/node_modules/entities/lib/decode.d.ts +211 -0
- package/node_modules/entities/lib/decode.d.ts.map +1 -0
- package/node_modules/entities/lib/decode.js +536 -0
- package/node_modules/entities/lib/decode.js.map +1 -0
- package/node_modules/entities/lib/decode_codepoint.d.ts +19 -0
- package/node_modules/entities/lib/decode_codepoint.d.ts.map +1 -0
- package/node_modules/entities/lib/decode_codepoint.js +76 -0
- package/node_modules/entities/lib/decode_codepoint.js.map +1 -0
- package/node_modules/entities/lib/encode.d.ts +22 -0
- package/node_modules/entities/lib/encode.d.ts.map +1 -0
- package/node_modules/entities/lib/encode.js +77 -0
- package/node_modules/entities/lib/encode.js.map +1 -0
- package/node_modules/entities/lib/escape.d.ts +43 -0
- package/node_modules/entities/lib/escape.d.ts.map +1 -0
- package/node_modules/entities/lib/escape.js +122 -0
- package/node_modules/entities/lib/escape.js.map +1 -0
- package/node_modules/entities/lib/esm/decode.d.ts +211 -0
- package/node_modules/entities/lib/esm/decode.d.ts.map +1 -0
- package/node_modules/entities/lib/esm/decode.js +496 -0
- package/node_modules/entities/lib/esm/decode.js.map +1 -0
- package/node_modules/entities/lib/esm/decode_codepoint.d.ts +19 -0
- package/node_modules/entities/lib/esm/decode_codepoint.d.ts.map +1 -0
- package/node_modules/entities/lib/esm/decode_codepoint.js +71 -0
- package/node_modules/entities/lib/esm/decode_codepoint.js.map +1 -0
- package/node_modules/entities/lib/esm/encode.d.ts +22 -0
- package/node_modules/entities/lib/esm/encode.d.ts.map +1 -0
- package/node_modules/entities/lib/esm/encode.js +69 -0
- package/node_modules/entities/lib/esm/encode.js.map +1 -0
- package/node_modules/entities/lib/esm/escape.d.ts +43 -0
- package/node_modules/entities/lib/esm/escape.d.ts.map +1 -0
- package/node_modules/entities/lib/esm/escape.js +116 -0
- package/node_modules/entities/lib/esm/escape.js.map +1 -0
- package/node_modules/entities/lib/esm/generated/decode-data-html.d.ts +3 -0
- package/node_modules/entities/lib/esm/generated/decode-data-html.d.ts.map +1 -0
- package/node_modules/entities/lib/esm/generated/decode-data-html.js +7 -0
- package/node_modules/entities/lib/esm/generated/decode-data-html.js.map +1 -0
- package/node_modules/entities/lib/esm/generated/decode-data-xml.d.ts +3 -0
- package/node_modules/entities/lib/esm/generated/decode-data-xml.d.ts.map +1 -0
- package/node_modules/entities/lib/esm/generated/decode-data-xml.js +7 -0
- package/node_modules/entities/lib/esm/generated/decode-data-xml.js.map +1 -0
- package/node_modules/entities/lib/esm/generated/encode-html.d.ts +8 -0
- package/node_modules/entities/lib/esm/generated/encode-html.d.ts.map +1 -0
- package/node_modules/entities/lib/esm/generated/encode-html.js +10 -0
- package/node_modules/entities/lib/esm/generated/encode-html.js.map +1 -0
- package/node_modules/entities/lib/esm/index.d.ts +96 -0
- package/node_modules/entities/lib/esm/index.d.ts.map +1 -0
- package/node_modules/entities/lib/esm/index.js +99 -0
- package/node_modules/entities/lib/esm/index.js.map +1 -0
- package/node_modules/entities/lib/esm/package.json +1 -0
- package/node_modules/entities/lib/generated/decode-data-html.d.ts +3 -0
- package/node_modules/entities/lib/generated/decode-data-html.d.ts.map +1 -0
- package/node_modules/entities/lib/generated/decode-data-html.js +9 -0
- package/node_modules/entities/lib/generated/decode-data-html.js.map +1 -0
- package/node_modules/entities/lib/generated/decode-data-xml.d.ts +3 -0
- package/node_modules/entities/lib/generated/decode-data-xml.d.ts.map +1 -0
- package/node_modules/entities/lib/generated/decode-data-xml.js +9 -0
- package/node_modules/entities/lib/generated/decode-data-xml.js.map +1 -0
- package/node_modules/entities/lib/generated/encode-html.d.ts +8 -0
- package/node_modules/entities/lib/generated/encode-html.d.ts.map +1 -0
- package/node_modules/entities/lib/generated/encode-html.js +12 -0
- package/node_modules/entities/lib/generated/encode-html.js.map +1 -0
- package/node_modules/entities/lib/index.d.ts +96 -0
- package/node_modules/entities/lib/index.d.ts.map +1 -0
- package/node_modules/entities/lib/index.js +126 -0
- package/node_modules/entities/lib/index.js.map +1 -0
- package/node_modules/entities/package.json +90 -0
- package/node_modules/entities/readme.md +122 -0
- package/package.json +91 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface MethodDescriptor {
|
|
2
|
+
id: string;
|
|
3
|
+
category: "unicode" | "pattern" | "watermark";
|
|
4
|
+
version: string;
|
|
5
|
+
state: "available" | "unsupported";
|
|
6
|
+
privacy_routes: readonly ["browser"];
|
|
7
|
+
limitations: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
export declare const listMethods: () => Readonly<{
|
|
10
|
+
privacy_routes: readonly "browser"[];
|
|
11
|
+
limitations: readonly string[];
|
|
12
|
+
id: string;
|
|
13
|
+
category: "unicode" | "pattern" | "watermark";
|
|
14
|
+
version: string;
|
|
15
|
+
state: "available" | "unsupported";
|
|
16
|
+
}>[];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
const METHODS = Object.freeze([
|
|
2
|
+
Object.freeze({ id: "unicode.invisible", category: "unicode", version: "unicode:2026.08.1", state: "available", privacy_routes: ["browser"], limitations: ["Controls can be legitimate in multilingual text."] }),
|
|
3
|
+
Object.freeze({ id: "unicode.homoglyph", category: "unicode", version: "unicode:2026.08.1", state: "available", privacy_routes: ["browser"], limitations: ["Mixed scripts require contextual human review."] }),
|
|
4
|
+
Object.freeze({ id: "style.patterns", category: "pattern", version: "en-gb:2026.08.1", state: "available", privacy_routes: ["browser"], limitations: ["Editorial pattern findings are not authorship evidence."] }),
|
|
5
|
+
Object.freeze({ id: "watermark.anthropic", category: "watermark", version: "adapter-placeholder/1", state: "unsupported", privacy_routes: ["browser"], limitations: ["No official detector interface is available."] })
|
|
6
|
+
]);
|
|
7
|
+
export const listMethods = () => METHODS.map(method => Object.freeze({ ...method, privacy_routes: Object.freeze([...method.privacy_routes]), limitations: Object.freeze([...method.limitations]) }));
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface DiffSegment {
|
|
2
|
+
type: "equal" | "insert" | "delete";
|
|
3
|
+
text: string;
|
|
4
|
+
source_start: number;
|
|
5
|
+
source_end: number;
|
|
6
|
+
candidate_start: number;
|
|
7
|
+
candidate_end: number;
|
|
8
|
+
}
|
|
9
|
+
export interface ContentDiff {
|
|
10
|
+
version: "lcs-token/1.0.0";
|
|
11
|
+
source_hash: string;
|
|
12
|
+
candidate_hash: string;
|
|
13
|
+
change_count: number;
|
|
14
|
+
segments: DiffSegment[];
|
|
15
|
+
fallback: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare function diff(source: string, candidate: string): ContentDiff;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { prefixedSha256 } from "../source/utf8.js";
|
|
2
|
+
const tokens = (s) => s.match(/\s+|[^\s]+/g) ?? [];
|
|
3
|
+
export function diff(source, candidate) {
|
|
4
|
+
if (source.length + candidate.length > 80_000)
|
|
5
|
+
return lineDiff(source, candidate);
|
|
6
|
+
const a = tokens(source), b = tokens(candidate);
|
|
7
|
+
if (a.length * b.length > 2_000_000)
|
|
8
|
+
return lineDiff(source, candidate);
|
|
9
|
+
const matches = hirschberg(a, b);
|
|
10
|
+
let i = 0, j = 0, sp = 0, cp = 0, mi = 0;
|
|
11
|
+
const out = [];
|
|
12
|
+
const push = (type, text, ss, se, cs, ce) => { const last = out.at(-1); if (last?.type === type && last.source_end === ss && last.candidate_end === cs) {
|
|
13
|
+
last.text += text;
|
|
14
|
+
last.source_end = se;
|
|
15
|
+
last.candidate_end = ce;
|
|
16
|
+
}
|
|
17
|
+
else
|
|
18
|
+
out.push({ type, text, source_start: ss, source_end: se, candidate_start: cs, candidate_end: ce }); };
|
|
19
|
+
while (i < a.length || j < b.length) {
|
|
20
|
+
const match = matches[mi];
|
|
21
|
+
if (match && i === match[0] && j === match[1]) {
|
|
22
|
+
const t = a[i];
|
|
23
|
+
push("equal", t, sp, sp + t.length, cp, cp + t.length);
|
|
24
|
+
sp += t.length;
|
|
25
|
+
cp += t.length;
|
|
26
|
+
i++;
|
|
27
|
+
j++;
|
|
28
|
+
mi++;
|
|
29
|
+
}
|
|
30
|
+
else if (j < b.length && (!match || j < match[1])) {
|
|
31
|
+
const t = b[j];
|
|
32
|
+
push("insert", t, sp, sp, cp, cp + t.length);
|
|
33
|
+
cp += t.length;
|
|
34
|
+
j++;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const t = a[i];
|
|
38
|
+
push("delete", t, sp, sp + t.length, cp, cp);
|
|
39
|
+
sp += t.length;
|
|
40
|
+
i++;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { version: "lcs-token/1.0.0", source_hash: prefixedSha256(source), candidate_hash: prefixedSha256(candidate), change_count: out.filter(x => x.type !== "equal").length, segments: out, fallback: false };
|
|
44
|
+
}
|
|
45
|
+
function rowScores(a, a0, a1, b, b0, b1, reverse = false) { const width = b1 - b0; let previous = new Uint32Array(width + 1), current = new Uint32Array(width + 1); for (let ai = 0; ai < a1 - a0; ai++) {
|
|
46
|
+
const av = a[reverse ? a1 - 1 - ai : a0 + ai];
|
|
47
|
+
for (let bj = 0; bj < width; bj++) {
|
|
48
|
+
const bv = b[reverse ? b1 - 1 - bj : b0 + bj];
|
|
49
|
+
current[bj + 1] = av === bv ? previous[bj] + 1 : Math.max(previous[bj + 1], current[bj]);
|
|
50
|
+
}
|
|
51
|
+
const swap = previous;
|
|
52
|
+
previous = current;
|
|
53
|
+
current = swap;
|
|
54
|
+
current.fill(0);
|
|
55
|
+
} return previous; }
|
|
56
|
+
function hirschberg(a, b) { const out = []; const walk = (a0, a1, b0, b1) => { if (a0 >= a1 || b0 >= b1)
|
|
57
|
+
return; if (a1 - a0 === 1) {
|
|
58
|
+
for (let j = b0; j < b1; j++)
|
|
59
|
+
if (a[a0] === b[j]) {
|
|
60
|
+
out.push([a0, j]);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
} const mid = (a0 + a1) >> 1; let left = rowScores(a, a0, mid, b, b0, b1), right = rowScores(a, mid, a1, b, b0, b1, true); let split = 0, best = -1; for (let j = 0; j <= b1 - b0; j++) {
|
|
65
|
+
const score = left[j] + right[b1 - b0 - j];
|
|
66
|
+
if (score > best) {
|
|
67
|
+
best = score;
|
|
68
|
+
split = j;
|
|
69
|
+
}
|
|
70
|
+
} left = undefined; right = undefined; walk(a0, mid, b0, b0 + split); walk(mid, a1, b0 + split, b1); }; walk(0, a.length, 0, b.length); return out; }
|
|
71
|
+
function lineDiff(source, candidate) { const segments = source === candidate ? (source ? [{ type: "equal", text: source, source_start: 0, source_end: source.length, candidate_start: 0, candidate_end: candidate.length }] : []) : []; if (source !== candidate) {
|
|
72
|
+
if (source)
|
|
73
|
+
segments.push({ type: "delete", text: source, source_start: 0, source_end: source.length, candidate_start: 0, candidate_end: 0 });
|
|
74
|
+
if (candidate)
|
|
75
|
+
segments.push({ type: "insert", text: candidate, source_start: source.length, source_end: source.length, candidate_start: 0, candidate_end: candidate.length });
|
|
76
|
+
} return { version: "lcs-token/1.0.0", source_hash: prefixedSha256(source), candidate_hash: prefixedSha256(candidate), change_count: segments.filter(item => item.type !== "equal").length, segments, fallback: true }; }
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ProtectedSpan } from "@opacedev/ai-content-checker-contracts";
|
|
2
|
+
import type { UnicodeFinding } from "../unicode/inspect.js";
|
|
3
|
+
import { type ContentDiff } from "../diff/diff.js";
|
|
4
|
+
export interface FixPreview {
|
|
5
|
+
source_hash: string;
|
|
6
|
+
candidate_hash: string;
|
|
7
|
+
candidate: string;
|
|
8
|
+
applied_finding_ids: string[];
|
|
9
|
+
skipped: Array<{
|
|
10
|
+
id: string;
|
|
11
|
+
reason: string;
|
|
12
|
+
}>;
|
|
13
|
+
diff: ContentDiff;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A C2PA text credential (C2PA 2.4 §A.8) is carried in the same variation
|
|
17
|
+
* selectors the hidden-character check flags, and U+FE00-U+FE0F carry
|
|
18
|
+
* fix:"remove". Removing them destroys the credential permanently and it cannot
|
|
19
|
+
* be rebuilt from the visible text, so the safe-fix path holds those characters
|
|
20
|
+
* back by default. Detection is untouched: every carrier is still found, still
|
|
21
|
+
* counted and still reported. Set allow_c2pa_credential_removal when the caller
|
|
22
|
+
* has taken a deliberate, confirmed decision to destroy the credential.
|
|
23
|
+
*/
|
|
24
|
+
export interface FixPreviewOptions {
|
|
25
|
+
allow_c2pa_credential_removal?: boolean;
|
|
26
|
+
}
|
|
27
|
+
export declare function previewSafeFixes(source: string, findings: UnicodeFinding[], selectedFindingIds: string[], protectedSpans?: ProtectedSpan[], options?: FixPreviewOptions): FixPreview;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { diff } from "../diff/diff.js";
|
|
2
|
+
import { prefixedSha256 } from "../source/utf8.js";
|
|
3
|
+
import { detectC2paTextCredentials, withinCredential } from "../provenance/c2pa-text.js";
|
|
4
|
+
export function previewSafeFixes(source, findings, selectedFindingIds, protectedSpans = [], options = {}) {
|
|
5
|
+
const credentials = options.allow_c2pa_credential_removal ? [] : detectC2paTextCredentials(source);
|
|
6
|
+
const selected = new Set(selectedFindingIds), edits = [], skipped = [];
|
|
7
|
+
for (const f of findings) {
|
|
8
|
+
if (!selected.has(f.id))
|
|
9
|
+
continue;
|
|
10
|
+
const raw = source.slice(f.span.start_utf16, f.span.end_utf16);
|
|
11
|
+
if (!f.id.startsWith("unicode_") || f.span.end_utf16 <= f.span.start_utf16 || f.matched_text_hash !== prefixedSha256(raw)) {
|
|
12
|
+
skipped.push({ id: f.id, reason: "invalid_finding_provenance" });
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (f.code_point === "U+FEFF" && f.span.start_utf16 !== 0) {
|
|
16
|
+
skipped.push({ id: f.id, reason: "invalid_bom_position" });
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (f.fix === "review") {
|
|
20
|
+
skipped.push({ id: f.id, reason: "user_review" });
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (withinCredential(credentials, f.span)) {
|
|
24
|
+
skipped.push({ id: f.id, reason: "c2pa_text_credential" });
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (protectedSpans.some(p => f.span.start_utf16 < p.end_utf16 && f.span.end_utf16 > p.start_utf16)) {
|
|
28
|
+
skipped.push({ id: f.id, reason: "protected_span" });
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (edits.some(e => f.span.start_utf16 < e.end && f.span.end_utf16 > e.start)) {
|
|
32
|
+
skipped.push({ id: f.id, reason: "overlapping_edit" });
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
edits.push({ start: f.span.start_utf16, end: f.span.end_utf16, value: f.fix === "space" ? " " : "", id: f.id });
|
|
36
|
+
}
|
|
37
|
+
edits.sort((a, b) => b.start - a.start);
|
|
38
|
+
let candidate = source;
|
|
39
|
+
for (const e of edits)
|
|
40
|
+
candidate = candidate.slice(0, e.start) + e.value + candidate.slice(e.end);
|
|
41
|
+
return deepFreeze({ source_hash: prefixedSha256(source), candidate_hash: prefixedSha256(candidate), candidate, applied_finding_ids: edits.map(e => e.id).reverse(), skipped, diff: diff(source, candidate) });
|
|
42
|
+
}
|
|
43
|
+
function deepFreeze(value) { if (value && typeof value === "object" && !Object.isFrozen(value)) {
|
|
44
|
+
Object.freeze(value);
|
|
45
|
+
for (const child of Object.values(value))
|
|
46
|
+
deepFreeze(child);
|
|
47
|
+
} return value; }
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { GateResult, ProtectedSpan } from "@opacedev/ai-content-checker-contracts";
|
|
2
|
+
export interface GatePolicy {
|
|
3
|
+
mode?: "strict" | "manual_review";
|
|
4
|
+
expected_source_hash?: string;
|
|
5
|
+
language?: string;
|
|
6
|
+
max_length_ratio?: number;
|
|
7
|
+
allow_html?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function validateCandidate(source: {
|
|
10
|
+
content: string;
|
|
11
|
+
content_hash?: string;
|
|
12
|
+
content_type?: string;
|
|
13
|
+
language?: string;
|
|
14
|
+
}, candidate: string, spans: ProtectedSpan[], policy?: GatePolicy): GateResult[];
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { validateAdditions, validateProtected } from "../protected/validate.js";
|
|
2
|
+
import { prefixedSha256 } from "../source/utf8.js";
|
|
3
|
+
export function validateCandidate(source, candidate, spans, policy = {}) {
|
|
4
|
+
const gates = [validateProtected(source.content, candidate, spans), validateAdditions(source.content, candidate)];
|
|
5
|
+
const current = prefixedSha256(source.content), expected = policy.expected_source_hash ?? source.content_hash ?? current;
|
|
6
|
+
gates.push({ id: "source_version", version: "1.0.0", status: current === expected ? "pass" : "fail", hard: true, summary: current === expected ? "Source hash matches" : "Source changed since protection", failures: current === expected ? [] : [{ expected_hash: expected, observed: current }], limitations: [] });
|
|
7
|
+
const executable = /<\s*(?:script|iframe|object|embed|form)\b|\son\w+\s*=|javascript:/i.test(candidate);
|
|
8
|
+
gates.push({ id: "html_safety", version: "1.0.0", status: executable ? "fail" : "pass", hard: true, summary: executable ? "Executable or unsafe HTML found" : "No executable HTML found", failures: executable ? [{ observed: "executable_markup" }] : [], limitations: ["This deterministic gate is not a complete HTML sanitiser."] });
|
|
9
|
+
const max = policy.max_length_ratio ?? 2;
|
|
10
|
+
const ratio = source.content.length ? candidate.length / source.content.length : 1;
|
|
11
|
+
gates.push({ id: "language_length", version: "1.0.0", status: ratio > max ? "fail" : "pass", hard: true, summary: ratio > max ? "Candidate exceeds the configured length bound" : "Candidate is within the configured length bound", failures: ratio > max ? [{ observed_ratio: ratio, max_ratio: max }] : [], limitations: ["CORE-10 does not infer language; it preserves the requested language as a host-reviewed constraint."] });
|
|
12
|
+
const leakage = /\b(?:protected_span_id|system prompt|<\|(?:system|assistant|user)\|>)\b/i.test(candidate);
|
|
13
|
+
gates.push({ id: "output_safety", version: "1.0.0", status: leakage ? "fail" : "pass", hard: true, summary: leakage ? "Prompt or protected-ID leakage found" : "No prompt/control-token leakage found", failures: leakage ? [{ observed: "control_or_prompt_marker" }] : [], limitations: [] });
|
|
14
|
+
gates.push({ id: "semantic_entailment", version: "unconfigured/1", status: "not_configured", hard: true, summary: "Semantic entailment is not configured in the deterministic core", failures: [], limitations: ["Strict policy must treat this required semantic gate as blocking when generation depends on semantic fidelity."] });
|
|
15
|
+
return gates;
|
|
16
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { inspect, type InspectOptions } from "./inspect.js";
|
|
2
|
+
export { extractProtectedSpans } from "./protected/extract.js";
|
|
3
|
+
export { validateCandidate, type GatePolicy } from "./gates/policy.js";
|
|
4
|
+
export { diff, type ContentDiff, type DiffSegment } from "./diff/diff.js";
|
|
5
|
+
export { previewSafeFixes, type FixPreview, type FixPreviewOptions } from "./fixes/preview.js";
|
|
6
|
+
export { detectC2paTextCredentials, variationSelectorToByte, isCredentialCarrier, withinCredential, credentialNotice, type C2paTextCredential } from "./provenance/c2pa-text.js";
|
|
7
|
+
export { buildReceipt, verifyReceipt, type ReceiptInput } from "./receipts/build.js";
|
|
8
|
+
export { registerPatternPack, type PatternPack } from "./patterns/en-gb-v1.js";
|
|
9
|
+
export { listMethods, type MethodDescriptor } from "./capabilities.js";
|
|
10
|
+
export { projectVisibleText, type VisibleTextProjection, type SourceMapRun } from "./source/visible-text.js";
|
|
11
|
+
export { inspectUnicode, UNICODE_RULES_VERSION, type UnicodeFinding } from "./unicode/inspect.js";
|
|
12
|
+
export { inspectSignalsV2, computeEditorialSignals, EN_SIGNALS_PATTERN_VERSION, type EditorialSignalsResult, type SignalsClassification } from "./patterns/en-signals-v2.js";
|
|
13
|
+
export { prefixedSha256, sha256Hex, utf8Bytes } from "./source/utf8.js";
|
|
14
|
+
export { computeCombinedVerdict, assertAxisIndependence, COMBINED_VERDICT_VERSION, type CombinedVerdictInput, type CombinedVerdictResult, type CombinedEvidenceItem, type EvidenceTier, type WatermarkOutcome, type ModelReading, type IntegrityFinding, type AiReading, type IntegrityStatus, type SuggestionLevel, type ConfidenceBand } from "./verdict/combine.js";
|
|
15
|
+
export { CHECKER_RESULT_RUNTIME_VERSION, CYCLE5_MODEL_IDENTITY, CHECKER_SCORE_SCALE, CHECKER_HONESTY_LINE, CHECKER_LEVELS, levelForCycle5Score, formatCheckerScoreTexts, presentCycle5Result, notAssessedAiPattern, composeCheckerAxes, buildContentFreeSharePayload, assertCheckerResultInvariants, type CheckerLevelId, type Cycle5BandId, type CheckerEvidenceInput, type Cycle5SectionInput, type Cycle5PresentationInput, type PresentedCycle5Section, type PresentedCycle5Result, type SharePayloadInput } from "./report/checker-result.js";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { inspect } from "./inspect.js";
|
|
2
|
+
export { extractProtectedSpans } from "./protected/extract.js";
|
|
3
|
+
export { validateCandidate } from "./gates/policy.js";
|
|
4
|
+
export { diff } from "./diff/diff.js";
|
|
5
|
+
export { previewSafeFixes } from "./fixes/preview.js";
|
|
6
|
+
export { detectC2paTextCredentials, variationSelectorToByte, isCredentialCarrier, withinCredential, credentialNotice } from "./provenance/c2pa-text.js";
|
|
7
|
+
export { buildReceipt, verifyReceipt } from "./receipts/build.js";
|
|
8
|
+
export { registerPatternPack } from "./patterns/en-gb-v1.js";
|
|
9
|
+
export { listMethods } from "./capabilities.js";
|
|
10
|
+
export { projectVisibleText } from "./source/visible-text.js";
|
|
11
|
+
export { inspectUnicode, UNICODE_RULES_VERSION } from "./unicode/inspect.js";
|
|
12
|
+
export { inspectSignalsV2, computeEditorialSignals, EN_SIGNALS_PATTERN_VERSION } from "./patterns/en-signals-v2.js";
|
|
13
|
+
export { prefixedSha256, sha256Hex, utf8Bytes } from "./source/utf8.js";
|
|
14
|
+
export { computeCombinedVerdict, assertAxisIndependence, COMBINED_VERDICT_VERSION } from "./verdict/combine.js";
|
|
15
|
+
export { CHECKER_RESULT_RUNTIME_VERSION, CYCLE5_MODEL_IDENTITY, CHECKER_SCORE_SCALE, CHECKER_HONESTY_LINE, CHECKER_LEVELS, levelForCycle5Score, formatCheckerScoreTexts, presentCycle5Result, notAssessedAiPattern, composeCheckerAxes, buildContentFreeSharePayload, assertCheckerResultInvariants } from "./report/checker-result.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AnalysisRequest, AnalysisResult } from "@opacedev/ai-content-checker-contracts";
|
|
2
|
+
export interface InspectOptions {
|
|
3
|
+
now?: () => string;
|
|
4
|
+
analysisId?: () => string;
|
|
5
|
+
onProgress?: (phase: "validating" | "mapping_text" | "unicode_checks" | "protected_spans" | "writing_patterns" | "complete") => void;
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
}
|
|
8
|
+
export declare function inspect(request: AnalysisRequest, options?: InspectOptions): Promise<AnalysisResult>;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { projectVisibleText } from "./source/visible-text.js";
|
|
2
|
+
import { prefixedSha256 } from "./source/utf8.js";
|
|
3
|
+
import { inspectUnicode, UNICODE_RULES_VERSION } from "./unicode/inspect.js";
|
|
4
|
+
import { extractProtectedSpans } from "./protected/extract.js";
|
|
5
|
+
import { inspectPatterns, runRegisteredPacks } from "./patterns/en-gb-v1.js";
|
|
6
|
+
import { computeEditorialSignals, EN_SIGNALS_PATTERN_VERSION } from "./patterns/en-signals-v2.js";
|
|
7
|
+
import { computeCombinedVerdict } from "./verdict/combine.js";
|
|
8
|
+
const limits = (message) => [message, "Authorship cannot be proved from this check."];
|
|
9
|
+
const WRITING_SIGNAL_RULES_RUN = 116; // 3 en-gb-v1 rules + 113 weighted signal categories (v2 51 + v3 55 + v4 rhythm 7)
|
|
10
|
+
export async function inspect(request, options = {}) {
|
|
11
|
+
const now = options.now ?? (() => new Date().toISOString()), started = now();
|
|
12
|
+
const progress = (p) => { if (options.signal?.aborted)
|
|
13
|
+
throw new DOMException("Inspection cancelled", "AbortError"); options.onProgress?.(p); };
|
|
14
|
+
progress("validating");
|
|
15
|
+
if (request.schema_version !== "1.0" || !request.contract_version.startsWith("1."))
|
|
16
|
+
throw new Error("contract_incompatible");
|
|
17
|
+
if (request.source.content.length > 250000)
|
|
18
|
+
throw new Error("request_too_large");
|
|
19
|
+
if (hasUnpairedSurrogate(request.source.content))
|
|
20
|
+
throw new Error("invalid_unicode_unpaired_surrogate");
|
|
21
|
+
const sourceHash = prefixedSha256(request.source.content);
|
|
22
|
+
progress("mapping_text");
|
|
23
|
+
const projection = projectVisibleText(request.source.content, request.source.content_type);
|
|
24
|
+
progress("unicode_checks");
|
|
25
|
+
const unicode = inspectUnicode(request.source.content);
|
|
26
|
+
progress("protected_spans");
|
|
27
|
+
const protectedSpans = extractProtectedSpans({ ...request.source, content_hash: sourceHash });
|
|
28
|
+
progress("writing_patterns");
|
|
29
|
+
const patternFindings = [...inspectPatterns(projection.text), ...runRegisteredPacks(projection.text)];
|
|
30
|
+
const editorialSignals = request.checks.includes("style.patterns") ? computeEditorialSignals(projection.text) : undefined;
|
|
31
|
+
const methods = [];
|
|
32
|
+
for (const id of request.checks) {
|
|
33
|
+
const methodStarted = now();
|
|
34
|
+
if (id.startsWith("unicode.")) {
|
|
35
|
+
methods.push(method(id, "unicode", "Opace deterministic Unicode inspection", UNICODE_RULES_VERSION, unicode.length ? "attention" : "pass", unicode.map(x => ({ type: "unicode_finding", ...x })), limits("Unicode controls can be legitimate in multilingual text."), methodStarted, now(), "browser"));
|
|
36
|
+
}
|
|
37
|
+
else if (id === "style.patterns") {
|
|
38
|
+
const signalsAttention = patternFindings.length > 0 || (editorialSignals?.status === "scored" && editorialSignals.classification !== "human_like");
|
|
39
|
+
const evidence = patternFindings.length ? patternFindings.map(x => ({ type: "pattern_finding", rule_id: x.rule_id, span: x.span })) : [{ type: "scope_note", rules_run: WRITING_SIGNAL_RULES_RUN, note: "No selected writing-signal rule matched. This is not evidence of human authorship." }];
|
|
40
|
+
if (editorialSignals)
|
|
41
|
+
evidence.push({ type: "editorial_signals", ...editorialSignals });
|
|
42
|
+
methods.push(method(id, "pattern", "Opace writing-signal rules", EN_SIGNALS_PATTERN_VERSION, signalsAttention ? "attention" : "pass", evidence, limits("Writing patterns are editorial prompts, not detector or watermark evidence."), methodStarted, now(), "browser"));
|
|
43
|
+
}
|
|
44
|
+
else if (id === "watermark.anthropic") {
|
|
45
|
+
methods.push({ ...method(id, "watermark", "Anthropic official text-watermark detector", "unavailable-2026-08-26", "unsupported", [], ["No official detector call was available. Local style or public SynthID tests are not substitutes.", "Not yet available in this release."], methodStarted, now(), "browser"), availability: "not_available", native_outcome: "not_available" });
|
|
46
|
+
}
|
|
47
|
+
else
|
|
48
|
+
methods.push(method(id, "detector", id, "unsupported/1", "unsupported", [], ["This requested method is not implemented in the deterministic browser core.", "Not yet available in this release."], methodStarted, now(), "browser"));
|
|
49
|
+
}
|
|
50
|
+
// 2026.08.8: the three-axis verdict. Character forensics used to be discarded
|
|
51
|
+
// at verdict time, then (2026.08.7) briefly allowed to escalate the verdict to
|
|
52
|
+
// ai_like, which was the opposite error: a hidden character proves text
|
|
53
|
+
// MANIPULATION, not AI ORIGIN. verdict/combine.ts now publishes three readings
|
|
54
|
+
// that are never merged — the AI probability (a trained model only, and no
|
|
55
|
+
// model runs in this deterministic core), the text-integrity and provenance
|
|
56
|
+
// findings, and the editorial suggestions. Protected spans are not an input.
|
|
57
|
+
const watermarkOutcome = request.checks.includes("watermark.anthropic") ? "not_available" : "not_supported";
|
|
58
|
+
const combinedVerdict = computeCombinedVerdict({ signals: editorialSignals, unicodeFindings: unicode, text: request.source.content, watermark: { outcome: watermarkOutcome } });
|
|
59
|
+
const combinedRaised = combinedVerdict.text_integrity.status !== "clean";
|
|
60
|
+
const summary = { pass: 0, attention: 0, fail: 0, inconclusive: 0, unsupported: 0, not_configured: 0, not_run: 0, error: 0 };
|
|
61
|
+
for (const item of methods)
|
|
62
|
+
summary[item.status]++;
|
|
63
|
+
progress("complete");
|
|
64
|
+
const result = { schema_version: "1.0", contract_version: "1.0.0", request_id: request.request_id, analysis_id: options.analysisId?.() ?? `analysis_${sourceHash.slice(7, 23)}`, source: { content_hash: sourceHash, normalised_hash: prefixedSha256(projection.text.normalize("NFC")), content_type: request.source.content_type, language: request.source.language, word_count: (projection.text.trim().match(/\S+/g) ?? []).length }, protected_spans: protectedSpans, pattern_findings: patternFindings, methods, summary, combined_verdict: combinedVerdict, limitations: [...new Set(["Authorship cannot be proved from these checks.", ...projection.limitations, ...(combinedRaised ? combinedVerdict.limitations : [])])], started_at: started, completed_at: now() };
|
|
65
|
+
return deepFreeze(result);
|
|
66
|
+
}
|
|
67
|
+
function method(id, category, provider, version, status, evidence, limitations, started_at, completed_at, privacy_route) { if (!limitations.length)
|
|
68
|
+
throw new Error("method_limitations_required"); return { id, category, provider_or_method: provider, version, status, score: null, threshold: null, segments: [], evidence, limitations: limitations, started_at, completed_at, privacy_route }; }
|
|
69
|
+
function deepFreeze(value) { if (value && typeof value === "object" && !Object.isFrozen(value)) {
|
|
70
|
+
Object.freeze(value);
|
|
71
|
+
for (const child of Object.values(value))
|
|
72
|
+
deepFreeze(child);
|
|
73
|
+
} return value; }
|
|
74
|
+
function hasUnpairedSurrogate(value) { for (let i = 0; i < value.length; i++) {
|
|
75
|
+
const code = value.charCodeAt(i);
|
|
76
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
77
|
+
const next = value.charCodeAt(i + 1);
|
|
78
|
+
if (!(next >= 0xdc00 && next <= 0xdfff))
|
|
79
|
+
return true;
|
|
80
|
+
i++;
|
|
81
|
+
}
|
|
82
|
+
else if (code >= 0xdc00 && code <= 0xdfff)
|
|
83
|
+
return true;
|
|
84
|
+
} return false; }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PatternFinding } from "@opacedev/ai-content-checker-contracts";
|
|
2
|
+
export { inspectSignalsV2, computeEditorialSignals, EN_SIGNALS_PATTERN_VERSION, type EditorialSignalsResult, type SignalsClassification } from "./en-signals-v2.js";
|
|
3
|
+
export declare const EN_GB_PATTERN_VERSION = "en-gb:2026.08.1";
|
|
4
|
+
export declare function inspectPatterns(text: string): PatternFinding[];
|
|
5
|
+
export interface PatternPack {
|
|
6
|
+
id: string;
|
|
7
|
+
version: string;
|
|
8
|
+
rules: readonly string[];
|
|
9
|
+
inspect(text: string): PatternFinding[];
|
|
10
|
+
}
|
|
11
|
+
export declare function registerPatternPack(pack: PatternPack): () => void;
|
|
12
|
+
export declare const runRegisteredPacks: (text: string) => PatternFinding[];
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { alignUtf16Range, rangeFromUtf16 } from "../source/offsets.js";
|
|
2
|
+
import { prefixedSha256 } from "../source/utf8.js";
|
|
3
|
+
import { inspectSignalsV2 } from "./en-signals-v2.js";
|
|
4
|
+
export { inspectSignalsV2, computeEditorialSignals, EN_SIGNALS_PATTERN_VERSION } from "./en-signals-v2.js";
|
|
5
|
+
export const EN_GB_PATTERN_VERSION = "en-gb:2026.08.1";
|
|
6
|
+
const PHRASES = ["in today's rapidly evolving landscape", "game-changer", "in conclusion", "it is important to note", "delve into"];
|
|
7
|
+
// A rule's match length is measured on a lower-cased or trimmed copy of the
|
|
8
|
+
// line, so the end boundary can land inside a surrogate pair when the match
|
|
9
|
+
// runs up to an emoji (arena texts open list lines with one). Align the span
|
|
10
|
+
// outward to whole code points BEFORE slicing, so the recorded offsets, the
|
|
11
|
+
// evidence text and `matched_text_hash` all describe the same characters.
|
|
12
|
+
const finding = (text, start, rule, severity, message, suggestion, evidence) => {
|
|
13
|
+
const raw = evidence.matched === undefined ? text.slice(start, start + 1) : String(evidence.matched);
|
|
14
|
+
const [alignedStart, alignedEnd] = alignUtf16Range(text, start, start + raw.length);
|
|
15
|
+
const matched = evidence.matched === undefined ? undefined : text.slice(alignedStart, alignedEnd);
|
|
16
|
+
return { rule_id: rule, rule_version: EN_GB_PATTERN_VERSION, severity, message, suggestion, span: rangeFromUtf16(text, alignedStart, alignedEnd), matched_text_hash: prefixedSha256(matched ?? ""), evidence: matched === undefined ? evidence : { ...evidence, matched } };
|
|
17
|
+
};
|
|
18
|
+
// Combined entry point: en-gb v1 rules plus the en-signals v2 pack adapted
|
|
19
|
+
// from the MIT `avoid-ai-writing` engine (see en-signals-v2.ts). A v2 finding
|
|
20
|
+
// whose span exactly duplicates a v1 finding's span is dropped so the same
|
|
21
|
+
// phrase is not reported twice.
|
|
22
|
+
export function inspectPatterns(text) {
|
|
23
|
+
const v1 = inspectPatternsV1(text);
|
|
24
|
+
const v1Spans = new Set(v1.map(f => `${f.span.start_utf16}:${f.span.end_utf16}`));
|
|
25
|
+
const v2 = inspectSignalsV2(text).filter(f => !v1Spans.has(`${f.span.start_utf16}:${f.span.end_utf16}`));
|
|
26
|
+
return [...v1, ...v2].sort((a, b) => a.span.start_utf16 - b.span.start_utf16 || a.rule_id.localeCompare(b.rule_id));
|
|
27
|
+
}
|
|
28
|
+
function inspectPatternsV1(text) {
|
|
29
|
+
const findings = [];
|
|
30
|
+
const lower = text.toLocaleLowerCase("en-GB");
|
|
31
|
+
for (const phrase of PHRASES) {
|
|
32
|
+
let at = 0, count = 0;
|
|
33
|
+
while ((at = lower.indexOf(phrase, at)) >= 0) {
|
|
34
|
+
count++;
|
|
35
|
+
at += phrase.length;
|
|
36
|
+
}
|
|
37
|
+
if (count >= 1) {
|
|
38
|
+
const start = lower.indexOf(phrase);
|
|
39
|
+
findings.push(finding(text, start, "style.overused_phrase", count > 1 ? "medium" : "low", "A stock phrase may make the passage feel generic.", "Review whether a more specific statement would be clearer.", { matched: text.slice(start, start + phrase.length), count, threshold: 1 }));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const sentences = text.split(/(?<=[.!?])\s+/).filter(Boolean);
|
|
43
|
+
const openings = new Map();
|
|
44
|
+
let cursor = 0;
|
|
45
|
+
for (const sentence of sentences) {
|
|
46
|
+
const start = text.indexOf(sentence, cursor);
|
|
47
|
+
cursor = start + sentence.length;
|
|
48
|
+
const opening = sentence.trim().split(/\s+/).slice(0, 3).join(" ").toLocaleLowerCase("en-GB");
|
|
49
|
+
if (opening.split(" ").length >= 2)
|
|
50
|
+
(openings.get(opening) ?? (openings.set(opening, []), openings.get(opening))).push(start);
|
|
51
|
+
}
|
|
52
|
+
for (const [opening, starts] of openings)
|
|
53
|
+
if (starts.length >= 3)
|
|
54
|
+
findings.push(finding(text, starts[0], "style.repeated_opening", "medium", "Several sentences begin the same way.", "Vary only the openings that genuinely benefit from it.", { matched: text.slice(starts[0], starts[0] + opening.length), count: starts.length, threshold: 3 }));
|
|
55
|
+
const transitions = (lower.match(/\b(?:moreover|furthermore|additionally|consequently|therefore|however)\b/g) ?? []).length;
|
|
56
|
+
const wordCount = text.trim() ? text.trim().split(/\s+/).length : 0;
|
|
57
|
+
if (wordCount >= 40 && transitions / wordCount > 0.04) {
|
|
58
|
+
const m = /\b(?:moreover|furthermore|additionally|consequently|therefore|however)\b/i.exec(text);
|
|
59
|
+
findings.push(finding(text, m.index, "style.transition_density", "low", "Transition words are unusually dense.", "Remove transitions that do not clarify the relationship between sentences.", { matched: m[0], count: transitions, word_count: wordCount, threshold_ratio: 0.04 }));
|
|
60
|
+
}
|
|
61
|
+
return findings.sort((a, b) => a.span.start_utf16 - b.span.start_utf16 || a.rule_id.localeCompare(b.rule_id));
|
|
62
|
+
}
|
|
63
|
+
const packs = new Map();
|
|
64
|
+
export function registerPatternPack(pack) { if (!/^[a-z][a-z0-9.-]+$/.test(pack.id) || !pack.version || !pack.rules.length)
|
|
65
|
+
throw new Error("invalid_pattern_pack"); if (packs.has(pack.id))
|
|
66
|
+
throw new Error("duplicate_pattern_pack"); const frozen = Object.freeze({ ...pack, rules: Object.freeze([...pack.rules]) }); packs.set(pack.id, frozen); return () => { if (packs.get(pack.id) === frozen)
|
|
67
|
+
packs.delete(pack.id); }; }
|
|
68
|
+
export const runRegisteredPacks = (text) => [...packs.values()].flatMap(pack => pack.inspect(text));
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule data for the en-signals v2 editorial-signals engine.
|
|
3
|
+
*
|
|
4
|
+
* Adapted to TypeScript from the MIT-licensed `avoid-ai-writing` detector
|
|
5
|
+
* (`detector/patterns.js`, Conor Bronsdon and contributors) recorded in
|
|
6
|
+
* `source-snapshots/avoid-ai-writing/`. See THIRD_PARTY_NOTICES.md.
|
|
7
|
+
*
|
|
8
|
+
* Everything in this module is a documented writing-pattern rule or a
|
|
9
|
+
* stylometric threshold. All of it is Tier B evidence in BRIEF.md §21 terms:
|
|
10
|
+
* editorial hints about style, never proof of authorship.
|
|
11
|
+
*/
|
|
12
|
+
export interface Tier1Phrase {
|
|
13
|
+
pattern: RegExp;
|
|
14
|
+
replace: string;
|
|
15
|
+
clarity?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface Tier2Conditional {
|
|
18
|
+
word: string;
|
|
19
|
+
pattern: RegExp;
|
|
20
|
+
suggestion: string;
|
|
21
|
+
}
|
|
22
|
+
export declare const CYRILLIC_LOOKALIKES: Record<string, string>;
|
|
23
|
+
export declare const GREEK_LOOKALIKES: Record<string, string>;
|
|
24
|
+
export declare const ROLEPLAY_VERBS: RegExp;
|
|
25
|
+
export declare const TIER1: Record<string, string>;
|
|
26
|
+
export declare const TIER1_PHRASES: readonly Tier1Phrase[];
|
|
27
|
+
export declare const TIER2: Record<string, string>;
|
|
28
|
+
export declare const TIER2_CONDITIONAL: readonly Tier2Conditional[];
|
|
29
|
+
export declare const TIER3: readonly string[];
|
|
30
|
+
export declare const TIER3_PHRASES: readonly RegExp[];
|
|
31
|
+
export declare const TRANSITIONS: readonly RegExp[];
|
|
32
|
+
export declare const CHATBOT_ARTIFACTS: readonly RegExp[];
|
|
33
|
+
export declare const SYCOPHANTIC: readonly RegExp[];
|
|
34
|
+
export declare const FILLERS: readonly RegExp[];
|
|
35
|
+
export declare const GENERIC_CONCLUSIONS: readonly RegExp[];
|
|
36
|
+
export declare const LETS_PATTERNS: readonly RegExp[];
|
|
37
|
+
export declare const REASONING_ARTIFACTS: readonly RegExp[];
|
|
38
|
+
export declare const ACKNOWLEDGMENT_LOOPS: readonly RegExp[];
|
|
39
|
+
export declare const SIGNIFICANCE_INFLATION: readonly RegExp[];
|
|
40
|
+
export declare const VAGUE_ATTRIBUTIONS: readonly RegExp[];
|
|
41
|
+
export declare const HOLLOW_INTENSIFIERS: readonly RegExp[];
|
|
42
|
+
export declare const EMOTIONAL_FLATLINE: readonly RegExp[];
|
|
43
|
+
export declare const LINGERING_ATTENTION: readonly RegExp[];
|
|
44
|
+
export declare const NOVELTY_INFLATION: readonly RegExp[];
|
|
45
|
+
export declare const CUTOFF_DISCLAIMERS: readonly RegExp[];
|
|
46
|
+
export declare const AI_PLACEHOLDERS: readonly RegExp[];
|
|
47
|
+
export declare const AI_CITATION_MARKUP: readonly RegExp[];
|
|
48
|
+
export declare const AI_UTM_SOURCE: readonly RegExp[];
|
|
49
|
+
export declare const TEMPLATE_PHRASES: readonly RegExp[];
|
|
50
|
+
export declare const FALSE_CONCESSION: readonly RegExp[];
|
|
51
|
+
export declare const RHETORICAL_QUESTIONS: readonly RegExp[];
|
|
52
|
+
export declare const HEDGE_STACK: readonly RegExp[];
|
|
53
|
+
export declare const FUTURE_NARRATIVE: readonly RegExp[];
|
|
54
|
+
export declare const REAL_ACTUAL_INFLATION: readonly RegExp[];
|
|
55
|
+
export declare const FORMULAIC_OPENERS: readonly RegExp[];
|
|
56
|
+
export declare const SPECULATIVE_OPENERS: readonly RegExp[];
|
|
57
|
+
export declare const PARENTHETICAL_HEDGE: readonly RegExp[];
|
|
58
|
+
export declare const CONFIDENCE_CALIBRATION: readonly RegExp[];
|
|
59
|
+
export declare const SOCIAL_CTA_CLOSER: readonly RegExp[];
|
|
60
|
+
export declare const NOT_JUST_CONTRAST: readonly RegExp[];
|
|
61
|
+
export declare const FUNCTION_WORD_IN_TITLE: RegExp;
|
|
62
|
+
export declare const MD_HEADING_PREFIX: RegExp;
|
|
63
|
+
export declare const TITLE_CASE_HEADER: RegExp;
|
|
64
|
+
export declare const SEPARATOR_DASH_RE: RegExp;
|
|
65
|
+
export declare const VERSION_HEADING_DASH_RE: RegExp;
|
|
66
|
+
export declare const FUNC_WORDS: ReadonlySet<string>;
|
|
67
|
+
export declare const ISSUE_WEIGHTS: Record<string, number>;
|
|
68
|
+
export interface CategoryMeta {
|
|
69
|
+
severity: "note" | "low" | "medium" | "high";
|
|
70
|
+
message: string;
|
|
71
|
+
suggestion: string;
|
|
72
|
+
}
|
|
73
|
+
export declare const CATEGORY_META: Record<string, CategoryMeta>;
|