@postalsys/email-content 0.0.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/LICENSE +21 -0
- package/README.md +288 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/analyze.d.ts +91 -0
- package/dist/analyze.d.ts.map +1 -0
- package/dist/analyze.js +274 -0
- package/dist/analyze.js.map +1 -0
- package/dist/annotate/client.d.ts +64 -0
- package/dist/annotate/client.d.ts.map +1 -0
- package/dist/annotate/client.js +139 -0
- package/dist/annotate/client.js.map +1 -0
- package/dist/annotate/llm.d.ts +46 -0
- package/dist/annotate/llm.d.ts.map +1 -0
- package/dist/annotate/llm.js +107 -0
- package/dist/annotate/llm.js.map +1 -0
- package/dist/annotate/prompt.d.ts +38 -0
- package/dist/annotate/prompt.d.ts.map +1 -0
- package/dist/annotate/prompt.js +165 -0
- package/dist/annotate/prompt.js.map +1 -0
- package/dist/annotate/store.d.ts +43 -0
- package/dist/annotate/store.d.ts.map +1 -0
- package/dist/annotate/store.js +46 -0
- package/dist/annotate/store.js.map +1 -0
- package/dist/canonical/html.d.ts +33 -0
- package/dist/canonical/html.d.ts.map +1 -0
- package/dist/canonical/html.js +156 -0
- package/dist/canonical/html.js.map +1 -0
- package/dist/canonical/text.d.ts +75 -0
- package/dist/canonical/text.d.ts.map +1 -0
- package/dist/canonical/text.js +217 -0
- package/dist/canonical/text.js.map +1 -0
- package/dist/classify/confidence.d.ts +38 -0
- package/dist/classify/confidence.d.ts.map +1 -0
- package/dist/classify/confidence.js +59 -0
- package/dist/classify/confidence.js.map +1 -0
- package/dist/classify/reconcile.d.ts +52 -0
- package/dist/classify/reconcile.d.ts.map +1 -0
- package/dist/classify/reconcile.js +100 -0
- package/dist/classify/reconcile.js.map +1 -0
- package/dist/classify/rules.d.ts +51 -0
- package/dist/classify/rules.d.ts.map +1 -0
- package/dist/classify/rules.js +648 -0
- package/dist/classify/rules.js.map +1 -0
- package/dist/cli/annotate.d.ts +23 -0
- package/dist/cli/annotate.d.ts.map +1 -0
- package/dist/cli/annotate.js +328 -0
- package/dist/cli/annotate.js.map +1 -0
- package/dist/cli/args.d.ts +17 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +72 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/benchmark.d.ts +13 -0
- package/dist/cli/benchmark.d.ts.map +1 -0
- package/dist/cli/benchmark.js +172 -0
- package/dist/cli/benchmark.js.map +1 -0
- package/dist/cli/corpus.d.ts +9 -0
- package/dist/cli/corpus.d.ts.map +1 -0
- package/dist/cli/corpus.js +152 -0
- package/dist/cli/corpus.js.map +1 -0
- package/dist/cli/disagreements.d.ts +14 -0
- package/dist/cli/disagreements.d.ts.map +1 -0
- package/dist/cli/disagreements.js +133 -0
- package/dist/cli/disagreements.js.map +1 -0
- package/dist/cli/evaluate.d.ts +15 -0
- package/dist/cli/evaluate.d.ts.map +1 -0
- package/dist/cli/evaluate.js +215 -0
- package/dist/cli/evaluate.js.map +1 -0
- package/dist/cli/extract.d.ts +10 -0
- package/dist/cli/extract.d.ts.map +1 -0
- package/dist/cli/extract.js +139 -0
- package/dist/cli/extract.js.map +1 -0
- package/dist/cli/main.d.ts +9 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +147 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/corpus/analyze.d.ts +66 -0
- package/dist/corpus/analyze.d.ts.map +1 -0
- package/dist/corpus/analyze.js +214 -0
- package/dist/corpus/analyze.js.map +1 -0
- package/dist/corpus/migrations/index.d.ts +14 -0
- package/dist/corpus/migrations/index.d.ts.map +1 -0
- package/dist/corpus/migrations/index.js +158 -0
- package/dist/corpus/migrations/index.js.map +1 -0
- package/dist/corpus/sample.d.ts +66 -0
- package/dist/corpus/sample.d.ts.map +1 -0
- package/dist/corpus/sample.js +98 -0
- package/dist/corpus/sample.js.map +1 -0
- package/dist/corpus/scanner.d.ts +79 -0
- package/dist/corpus/scanner.d.ts.map +1 -0
- package/dist/corpus/scanner.js +256 -0
- package/dist/corpus/scanner.js.map +1 -0
- package/dist/corpus/stats.d.ts +76 -0
- package/dist/corpus/stats.d.ts.map +1 -0
- package/dist/corpus/stats.js +240 -0
- package/dist/corpus/stats.js.map +1 -0
- package/dist/corpus/store.d.ts +67 -0
- package/dist/corpus/store.d.ts.map +1 -0
- package/dist/corpus/store.js +196 -0
- package/dist/corpus/store.js.map +1 -0
- package/dist/corpus/thread-index.d.ts +31 -0
- package/dist/corpus/thread-index.d.ts.map +1 -0
- package/dist/corpus/thread-index.js +182 -0
- package/dist/corpus/thread-index.js.map +1 -0
- package/dist/corpus/worker.d.ts +21 -0
- package/dist/corpus/worker.d.ts.map +1 -0
- package/dist/corpus/worker.js +74 -0
- package/dist/corpus/worker.js.map +1 -0
- package/dist/detect/client.d.ts +28 -0
- package/dist/detect/client.d.ts.map +1 -0
- package/dist/detect/client.js +121 -0
- package/dist/detect/client.js.map +1 -0
- package/dist/detect/disclaimer.d.ts +12 -0
- package/dist/detect/disclaimer.d.ts.map +1 -0
- package/dist/detect/disclaimer.js +28 -0
- package/dist/detect/disclaimer.js.map +1 -0
- package/dist/detect/forward-header.d.ts +10 -0
- package/dist/detect/forward-header.d.ts.map +1 -0
- package/dist/detect/forward-header.js +35 -0
- package/dist/detect/forward-header.js.map +1 -0
- package/dist/detect/patterns/disclaimers.json +113 -0
- package/dist/detect/patterns/forward-headers.json +40 -0
- package/dist/detect/patterns/html-clients.json +157 -0
- package/dist/detect/patterns/quoted-headers.json +41 -0
- package/dist/detect/patterns/reply-headers.json +139 -0
- package/dist/detect/patterns/signatures.json +58 -0
- package/dist/detect/patterns.d.ts +43 -0
- package/dist/detect/patterns.d.ts.map +1 -0
- package/dist/detect/patterns.js +125 -0
- package/dist/detect/patterns.js.map +1 -0
- package/dist/detect/reply-header.d.ts +44 -0
- package/dist/detect/reply-header.d.ts.map +1 -0
- package/dist/detect/reply-header.js +134 -0
- package/dist/detect/reply-header.js.map +1 -0
- package/dist/detect/signature.d.ts +28 -0
- package/dist/detect/signature.d.ts.map +1 -0
- package/dist/detect/signature.js +77 -0
- package/dist/detect/signature.js.map +1 -0
- package/dist/detect/traffic.d.ts +26 -0
- package/dist/detect/traffic.d.ts.map +1 -0
- package/dist/detect/traffic.js +98 -0
- package/dist/detect/traffic.js.map +1 -0
- package/dist/eval/metrics.d.ts +87 -0
- package/dist/eval/metrics.d.ts.map +1 -0
- package/dist/eval/metrics.js +185 -0
- package/dist/eval/metrics.js.map +1 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +163 -0
- package/dist/index.js.map +1 -0
- package/dist/match/history.d.ts +62 -0
- package/dist/match/history.d.ts.map +1 -0
- package/dist/match/history.js +128 -0
- package/dist/match/history.js.map +1 -0
- package/dist/match/normalize.d.ts +33 -0
- package/dist/match/normalize.d.ts.map +1 -0
- package/dist/match/normalize.js +63 -0
- package/dist/match/normalize.js.map +1 -0
- package/dist/match/shingle.d.ts +44 -0
- package/dist/match/shingle.d.ts.map +1 -0
- package/dist/match/shingle.js +78 -0
- package/dist/match/shingle.js.map +1 -0
- package/dist/mime/charset.d.ts +33 -0
- package/dist/mime/charset.d.ts.map +1 -0
- package/dist/mime/charset.js +118 -0
- package/dist/mime/charset.js.map +1 -0
- package/dist/mime/headers.d.ts +32 -0
- package/dist/mime/headers.d.ts.map +1 -0
- package/dist/mime/headers.js +127 -0
- package/dist/mime/headers.js.map +1 -0
- package/dist/mime/model.d.ts +111 -0
- package/dist/mime/model.d.ts.map +1 -0
- package/dist/mime/model.js +17 -0
- package/dist/mime/model.js.map +1 -0
- package/dist/mime/parse.d.ts +27 -0
- package/dist/mime/parse.d.ts.map +1 -0
- package/dist/mime/parse.js +343 -0
- package/dist/mime/parse.js.map +1 -0
- package/dist/segment/features.d.ts +29 -0
- package/dist/segment/features.d.ts.map +1 -0
- package/dist/segment/features.js +102 -0
- package/dist/segment/features.js.map +1 -0
- package/dist/segment/html-blocks.d.ts +28 -0
- package/dist/segment/html-blocks.d.ts.map +1 -0
- package/dist/segment/html-blocks.js +596 -0
- package/dist/segment/html-blocks.js.map +1 -0
- package/dist/segment/text-blocks.d.ts +30 -0
- package/dist/segment/text-blocks.d.ts.map +1 -0
- package/dist/segment/text-blocks.js +204 -0
- package/dist/segment/text-blocks.js.map +1 -0
- package/dist/segment/types.d.ts +88 -0
- package/dist/segment/types.d.ts.map +1 -0
- package/dist/segment/types.js +11 -0
- package/dist/segment/types.js.map +1 -0
- package/dist/types.d.ts +85 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +17 -0
- package/dist/types.js.map +1 -0
- package/dist/util/hash.d.ts +49 -0
- package/dist/util/hash.d.ts.map +1 -0
- package/dist/util/hash.js +0 -0
- package/dist/util/hash.js.map +1 -0
- package/dist/util/lang.d.ts +25 -0
- package/dist/util/lang.d.ts.map +1 -0
- package/dist/util/lang.js +135 -0
- package/dist/util/lang.js.map +1 -0
- package/dist/util/worker-pool.d.ts +51 -0
- package/dist/util/worker-pool.d.ts.map +1 -0
- package/dist/util/worker-pool.js +155 -0
- package/dist/util/worker-pool.js.map +1 -0
- package/package.json +74 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Stage 2: plain text canonicalisation.
|
|
4
|
+
*
|
|
5
|
+
* Offsets in the rest of the pipeline are character offsets into the canonical string produced
|
|
6
|
+
* here, not byte offsets into the raw message. Traceability back to the file is via the owning MIME
|
|
7
|
+
* part's byte range. That split is deliberate: transfer decoding, charset conversion and line
|
|
8
|
+
* ending normalisation all change lengths, and maintaining a byte level offset map through three
|
|
9
|
+
* lossy transforms would be fragile for no practical gain.
|
|
10
|
+
*
|
|
11
|
+
* What this module does NOT do is unwrap hard wrapped lines. Unwrapping changes offsets and can
|
|
12
|
+
* merge a quoted line with a new one, which is exactly the mistake that loses content. Wrapping is
|
|
13
|
+
* recorded as a per line property and resolved later, in the matching projection only.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.splitQuotePrefix = splitQuotePrefix;
|
|
17
|
+
exports.canonicaliseText = canonicaliseText;
|
|
18
|
+
exports.profileQuoting = profileQuoting;
|
|
19
|
+
/**
|
|
20
|
+
* Quote prefix matcher.
|
|
21
|
+
*
|
|
22
|
+
* Accepts the common forms: ">", "> ", ">>", "> > ", and up to three leading spaces before the
|
|
23
|
+
* first marker, which several clients add when re-wrapping. It deliberately does not accept "|"
|
|
24
|
+
* or ":" as quote markers: both appear far too often in ordinary text, tables and code, and a
|
|
25
|
+
* false positive here promotes new content into history.
|
|
26
|
+
*/
|
|
27
|
+
const QUOTE_PREFIX = /^[ \t]{0,3}((?:>[ \t]?)+)/;
|
|
28
|
+
/** Split a line into its quote depth and remaining content. */
|
|
29
|
+
function splitQuotePrefix(line) {
|
|
30
|
+
const match = QUOTE_PREFIX.exec(line);
|
|
31
|
+
if (!match || !match[1]) {
|
|
32
|
+
return { depth: 0, content: line };
|
|
33
|
+
}
|
|
34
|
+
const prefix = match[1];
|
|
35
|
+
let depth = 0;
|
|
36
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
37
|
+
if (prefix[i] === '>') {
|
|
38
|
+
depth++;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { depth, content: line.slice(match[0].length) };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Normalise decoded part text into the canonical form used by every later stage.
|
|
45
|
+
*
|
|
46
|
+
* Line endings become LF, a BOM is dropped, NUL bytes and lone carriage returns are removed, and
|
|
47
|
+
* the result is NFC normalised so that visually identical text compares equal during history
|
|
48
|
+
* matching. Nothing else is altered: no trimming, no whitespace collapsing, no unwrapping.
|
|
49
|
+
*/
|
|
50
|
+
function canonicaliseText(input) {
|
|
51
|
+
let text = input;
|
|
52
|
+
if (text.charCodeAt(0) === 0xfeff) {
|
|
53
|
+
text = text.slice(1);
|
|
54
|
+
}
|
|
55
|
+
text = text.replace(/\r\n?/g, '\n').replace(/\0/g, '');
|
|
56
|
+
try {
|
|
57
|
+
text = text.normalize('NFC');
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Extremely long or malformed input can defeat NFC. The unnormalised text is still usable.
|
|
61
|
+
}
|
|
62
|
+
const lines = [];
|
|
63
|
+
let offset = 0;
|
|
64
|
+
let index = 0;
|
|
65
|
+
while (offset <= text.length) {
|
|
66
|
+
let newline = text.indexOf('\n', offset);
|
|
67
|
+
if (newline === -1) {
|
|
68
|
+
newline = text.length;
|
|
69
|
+
}
|
|
70
|
+
const raw = text.slice(offset, newline);
|
|
71
|
+
const { depth, content } = splitQuotePrefix(raw);
|
|
72
|
+
lines.push({
|
|
73
|
+
index,
|
|
74
|
+
start: offset,
|
|
75
|
+
end: newline,
|
|
76
|
+
raw,
|
|
77
|
+
content,
|
|
78
|
+
depth,
|
|
79
|
+
blank: content.trim().length === 0
|
|
80
|
+
});
|
|
81
|
+
index++;
|
|
82
|
+
if (newline === text.length) {
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
offset = newline + 1;
|
|
86
|
+
}
|
|
87
|
+
reattachWrapContinuations(lines);
|
|
88
|
+
return { text, lines };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Re-attach quoted lines whose wrapped continuation lost its quote prefix.
|
|
92
|
+
*
|
|
93
|
+
* Some clients hard wrap a quoted paragraph and fail to re-apply the ">" marker to the continuation:
|
|
94
|
+
*
|
|
95
|
+
* > First of all, sorry for the delay in giving some final information
|
|
96
|
+
* about the
|
|
97
|
+
* > course. After carefully seeing your preferences, plus my
|
|
98
|
+
*
|
|
99
|
+
* "about the" is the tail of the quoted sentence, not something the sender wrote. Left alone it
|
|
100
|
+
* looks like unquoted content sitting between two quoted lines, which reads as an interleaved reply
|
|
101
|
+
* and makes the whole quoted message look like inline context that must stay visible. One message
|
|
102
|
+
* in the working corpus produced 30 such fragments and defeated the classifier completely.
|
|
103
|
+
*
|
|
104
|
+
* The test is deliberately narrow, because promoting real content into a quote is the expensive
|
|
105
|
+
* direction of error. All of these must hold: the line is short, it is immediately adjacent to
|
|
106
|
+
* quoted lines above and below with no blank line between, both neighbours are at the same depth,
|
|
107
|
+
* and the quoted line above ends mid sentence. A genuine inline reply is separated by a blank line
|
|
108
|
+
* and follows a quote that ended properly.
|
|
109
|
+
*
|
|
110
|
+
* Only the depth is adjusted. Offsets and raw text are untouched, so block tiling still reproduces
|
|
111
|
+
* the input exactly.
|
|
112
|
+
*/
|
|
113
|
+
function reattachWrapContinuations(lines) {
|
|
114
|
+
const MAX_CONTINUATION_CHARS = 60;
|
|
115
|
+
for (let i = 1; i < lines.length - 1; i++) {
|
|
116
|
+
const line = lines[i];
|
|
117
|
+
if (line.depth !== 0 || line.blank || line.raw.length > MAX_CONTINUATION_CHARS) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const previous = lines[i - 1];
|
|
121
|
+
const next = lines[i + 1];
|
|
122
|
+
if (previous.depth === 0 || previous.blank || next.depth !== previous.depth || next.blank) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
// A quoted line that ended a sentence was not cut off mid flow, so what follows is new.
|
|
126
|
+
if (/[.!?:;)\]"']\s*$/.test(previous.content)) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
line.depth = previous.depth;
|
|
130
|
+
line.wrapContinuation = true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Describe how quoting is laid out in a plain text body.
|
|
135
|
+
*
|
|
136
|
+
* The style categories are positional only. They describe where unquoted content sits relative to
|
|
137
|
+
* quoted content and make no claim about intent, because a trailing block after a quote is just as
|
|
138
|
+
* likely to be a signature as a genuine bottom posted answer. Deciding that is the classifier's
|
|
139
|
+
* job, not this function's.
|
|
140
|
+
*/
|
|
141
|
+
function profileQuoting(lines) {
|
|
142
|
+
const totalLines = lines.length;
|
|
143
|
+
let quotedLines = 0;
|
|
144
|
+
let maxDepth = 0;
|
|
145
|
+
let nonBlank = 0;
|
|
146
|
+
let firstQuote = -1;
|
|
147
|
+
let lastQuote = -1;
|
|
148
|
+
for (const line of lines) {
|
|
149
|
+
if (line.depth > 0) {
|
|
150
|
+
quotedLines++;
|
|
151
|
+
maxDepth = Math.max(maxDepth, line.depth);
|
|
152
|
+
if (firstQuote === -1) {
|
|
153
|
+
firstQuote = line.index;
|
|
154
|
+
}
|
|
155
|
+
lastQuote = line.index;
|
|
156
|
+
}
|
|
157
|
+
if (!line.blank) {
|
|
158
|
+
nonBlank++;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (firstQuote === -1) {
|
|
162
|
+
return {
|
|
163
|
+
maxDepth: 0,
|
|
164
|
+
quotedLines: 0,
|
|
165
|
+
totalLines,
|
|
166
|
+
quotedRatio: 0,
|
|
167
|
+
style: 'none',
|
|
168
|
+
contentBefore: nonBlank,
|
|
169
|
+
contentBetween: 0,
|
|
170
|
+
contentAfter: 0
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
let contentBefore = 0;
|
|
174
|
+
let contentBetween = 0;
|
|
175
|
+
let contentAfter = 0;
|
|
176
|
+
for (const line of lines) {
|
|
177
|
+
if (line.depth > 0 || line.blank) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (line.index < firstQuote) {
|
|
181
|
+
contentBefore++;
|
|
182
|
+
}
|
|
183
|
+
else if (line.index > lastQuote) {
|
|
184
|
+
contentAfter++;
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
contentBetween++;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
let style;
|
|
191
|
+
if (contentBetween > 0) {
|
|
192
|
+
style = 'interleaved';
|
|
193
|
+
}
|
|
194
|
+
else if (contentBefore > 0 && contentAfter > 0) {
|
|
195
|
+
style = 'top-and-trailing';
|
|
196
|
+
}
|
|
197
|
+
else if (contentBefore > 0) {
|
|
198
|
+
style = 'top';
|
|
199
|
+
}
|
|
200
|
+
else if (contentAfter > 0) {
|
|
201
|
+
style = 'bottom';
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
style = 'quote-only';
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
maxDepth,
|
|
208
|
+
quotedLines,
|
|
209
|
+
totalLines,
|
|
210
|
+
quotedRatio: nonBlank ? quotedLines / nonBlank : 0,
|
|
211
|
+
style,
|
|
212
|
+
contentBefore,
|
|
213
|
+
contentBetween,
|
|
214
|
+
contentAfter
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
//# sourceMappingURL=text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text.js","sourceRoot":"","sources":["../../src/canonical/text.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage 8: confidence and visibility.
|
|
3
|
+
*
|
|
4
|
+
* All thresholds live here so that calibration against the labelled set can move them without
|
|
5
|
+
* touching detector or rule code. The numbers below are the deterministic baseline's starting
|
|
6
|
+
* point, chosen conservatively; Phase 6 replaces them with calibrated values.
|
|
7
|
+
*/
|
|
8
|
+
import { type Label, type Visibility } from '../types';
|
|
9
|
+
export interface ConfidenceConfig {
|
|
10
|
+
/** At or above this, high confidence history may be hidden outright. */
|
|
11
|
+
hideThreshold: number;
|
|
12
|
+
/** At or above this, content is collapsed but one interaction away. */
|
|
13
|
+
collapseThreshold: number;
|
|
14
|
+
/** Minimum anchor strength before hiding is permitted at all. */
|
|
15
|
+
minAnchorStrength: number;
|
|
16
|
+
}
|
|
17
|
+
export declare const DEFAULT_CONFIDENCE: ConfidenceConfig;
|
|
18
|
+
/**
|
|
19
|
+
* Map a label and confidence to a visibility band.
|
|
20
|
+
*
|
|
21
|
+
* The asymmetry is deliberate and is the core safety mechanism:
|
|
22
|
+
* - only REPLY_HISTORY can ever be hidden outright
|
|
23
|
+
* - FORWARDED_CONTENT is collapsed at most, because it was included on purpose
|
|
24
|
+
* - INLINE_QUOTE stays visible, because it is the context an answer depends on
|
|
25
|
+
* - UNKNOWN always stays visible
|
|
26
|
+
*/
|
|
27
|
+
export declare function visibilityFor(label: Label, confidence: number, config?: ConfidenceConfig): Visibility;
|
|
28
|
+
/**
|
|
29
|
+
* Message level confidence.
|
|
30
|
+
*
|
|
31
|
+
* The weakest hide or collapse decision, not the average. An average would let a long run of easy
|
|
32
|
+
* blocks mask one risky decision, and the risky decision is the one that loses content.
|
|
33
|
+
*/
|
|
34
|
+
export declare function overallConfidence(decisions: readonly {
|
|
35
|
+
confidence: number;
|
|
36
|
+
visibility: Visibility;
|
|
37
|
+
}[]): number;
|
|
38
|
+
//# sourceMappingURL=confidence.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"confidence.d.ts","sourceRoot":"","sources":["../../src/classify/confidence.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAEvD,MAAM,WAAW,gBAAgB;IAC7B,wEAAwE;IACxE,aAAa,EAAE,MAAM,CAAC;IACtB,uEAAuE;IACvE,iBAAiB,EAAE,MAAM,CAAC;IAC1B,iEAAiE;IACjE,iBAAiB,EAAE,MAAM,CAAC;CAC7B;AAED,eAAO,MAAM,kBAAkB,EAAE,gBAIhC,CAAC;AAEF;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAE,gBAAqC,GAAG,UAAU,CAkBzH;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,SAAS;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,UAAU,CAAA;CAAE,EAAE,GAAG,MAAM,CAM9G"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Stage 8: confidence and visibility.
|
|
4
|
+
*
|
|
5
|
+
* All thresholds live here so that calibration against the labelled set can move them without
|
|
6
|
+
* touching detector or rule code. The numbers below are the deterministic baseline's starting
|
|
7
|
+
* point, chosen conservatively; Phase 6 replaces them with calibrated values.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.DEFAULT_CONFIDENCE = void 0;
|
|
11
|
+
exports.visibilityFor = visibilityFor;
|
|
12
|
+
exports.overallConfidence = overallConfidence;
|
|
13
|
+
exports.DEFAULT_CONFIDENCE = {
|
|
14
|
+
hideThreshold: 0.95,
|
|
15
|
+
collapseThreshold: 0.75,
|
|
16
|
+
minAnchorStrength: 0.85
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Map a label and confidence to a visibility band.
|
|
20
|
+
*
|
|
21
|
+
* The asymmetry is deliberate and is the core safety mechanism:
|
|
22
|
+
* - only REPLY_HISTORY can ever be hidden outright
|
|
23
|
+
* - FORWARDED_CONTENT is collapsed at most, because it was included on purpose
|
|
24
|
+
* - INLINE_QUOTE stays visible, because it is the context an answer depends on
|
|
25
|
+
* - UNKNOWN always stays visible
|
|
26
|
+
*/
|
|
27
|
+
function visibilityFor(label, confidence, config = exports.DEFAULT_CONFIDENCE) {
|
|
28
|
+
switch (label) {
|
|
29
|
+
case 'REPLY_HISTORY':
|
|
30
|
+
if (confidence >= config.hideThreshold) {
|
|
31
|
+
return 'hide';
|
|
32
|
+
}
|
|
33
|
+
return confidence >= config.collapseThreshold ? 'collapse' : 'show';
|
|
34
|
+
case 'FORWARDED_CONTENT':
|
|
35
|
+
return confidence >= config.collapseThreshold ? 'collapse' : 'show';
|
|
36
|
+
case 'SIGNATURE':
|
|
37
|
+
case 'DISCLAIMER':
|
|
38
|
+
return confidence >= config.collapseThreshold ? 'collapse' : 'show';
|
|
39
|
+
case 'INLINE_QUOTE':
|
|
40
|
+
case 'NEW_CONTENT':
|
|
41
|
+
case 'UNKNOWN':
|
|
42
|
+
default:
|
|
43
|
+
return 'show';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Message level confidence.
|
|
48
|
+
*
|
|
49
|
+
* The weakest hide or collapse decision, not the average. An average would let a long run of easy
|
|
50
|
+
* blocks mask one risky decision, and the risky decision is the one that loses content.
|
|
51
|
+
*/
|
|
52
|
+
function overallConfidence(decisions) {
|
|
53
|
+
const risky = decisions.filter(decision => decision.visibility !== 'show');
|
|
54
|
+
if (!risky.length) {
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
return Math.min(...risky.map(decision => decision.confidence));
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=confidence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"confidence.js","sourceRoot":"","sources":["../../src/classify/confidence.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;;AA4BH,sCAkBC;AAQD,8CAMC;AA/CY,QAAA,kBAAkB,GAAqB;IAChD,aAAa,EAAE,IAAI;IACnB,iBAAiB,EAAE,IAAI;IACvB,iBAAiB,EAAE,IAAI;CAC1B,CAAC;AAEF;;;;;;;;GAQG;AACH,SAAgB,aAAa,CAAC,KAAY,EAAE,UAAkB,EAAE,SAA2B,0BAAkB;IACzG,QAAQ,KAAK,EAAE,CAAC;QACZ,KAAK,eAAe;YAChB,IAAI,UAAU,IAAI,MAAM,CAAC,aAAa,EAAE,CAAC;gBACrC,OAAO,MAAM,CAAC;YAClB,CAAC;YACD,OAAO,UAAU,IAAI,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC;QACxE,KAAK,mBAAmB;YACpB,OAAO,UAAU,IAAI,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC;QACxE,KAAK,WAAW,CAAC;QACjB,KAAK,YAAY;YACb,OAAO,UAAU,IAAI,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC;QACxE,KAAK,cAAc,CAAC;QACpB,KAAK,aAAa,CAAC;QACnB,KAAK,SAAS,CAAC;QACf;YACI,OAAO,MAAM,CAAC;IACtB,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,SAAgB,iBAAiB,CAAC,SAAoE;IAClG,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,UAAU,KAAK,MAAM,CAAC,CAAC;IAC3E,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;QAChB,OAAO,CAAC,CAAC;IACb,CAAC;IACD,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC;AACnE,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage 7: plain text and HTML reconciliation.
|
|
3
|
+
*
|
|
4
|
+
* Measured over the full corpus, 79.2 percent of messages carry both representations, and among
|
|
5
|
+
* human replies that carry both, a quarter disagree about whether the message contains quoted
|
|
6
|
+
* history at all. The disagreement is almost always in the same direction: the HTML part has a
|
|
7
|
+
* quote container and the plain part is a flattened render where the quote markers are simply gone.
|
|
8
|
+
*
|
|
9
|
+
* So HTML is the primary representation when both exist and the HTML actually carries structure.
|
|
10
|
+
* The plain part is still classified independently, and its verdict is used two ways: to confirm,
|
|
11
|
+
* which raises nothing but costs nothing, and to contradict, which caps confidence so that a
|
|
12
|
+
* disputed region is collapsed rather than hidden.
|
|
13
|
+
*/
|
|
14
|
+
import { type Representation } from '../types';
|
|
15
|
+
import { type BlockDecision } from './rules';
|
|
16
|
+
export interface RepresentationResult {
|
|
17
|
+
representation: Representation;
|
|
18
|
+
decisions: BlockDecision[];
|
|
19
|
+
/** Total characters of authored content, used to spot a stub representation. */
|
|
20
|
+
contentChars: number;
|
|
21
|
+
/** True when the representation contains at least one structural anchor. */
|
|
22
|
+
hasAnchor: boolean;
|
|
23
|
+
/** Blocks matched against known history, if any was supplied. */
|
|
24
|
+
matches?: readonly {
|
|
25
|
+
blockIndex: number;
|
|
26
|
+
containment: number;
|
|
27
|
+
exact: boolean;
|
|
28
|
+
isParent: boolean;
|
|
29
|
+
}[];
|
|
30
|
+
}
|
|
31
|
+
export interface ReconcileOutcome {
|
|
32
|
+
primary: RepresentationResult;
|
|
33
|
+
secondary: RepresentationResult | null;
|
|
34
|
+
disagrees: boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Choose which representation drives the output.
|
|
38
|
+
*
|
|
39
|
+
* HTML wins by default when both exist, because it is where the structure survives. Plain text
|
|
40
|
+
* wins when the HTML is a stub, which happens with clients that send a token HTML wrapper around a
|
|
41
|
+
* text body, and when only the plain part has an anchor.
|
|
42
|
+
*/
|
|
43
|
+
export declare function pickPrimary(text: RepresentationResult | null, html: RepresentationResult | null): ReconcileOutcome;
|
|
44
|
+
/**
|
|
45
|
+
* Apply cross representation evidence to the primary decisions.
|
|
46
|
+
*
|
|
47
|
+
* The only adjustment made is downward. Raising confidence because the other representation agrees
|
|
48
|
+
* would let two copies of the same mistake reinforce each other, and the cost of a wrong hide is
|
|
49
|
+
* much higher than the cost of a collapse that could have been a hide.
|
|
50
|
+
*/
|
|
51
|
+
export declare function reconcile(outcome: ReconcileOutcome, collapseCeiling?: number): BlockDecision[];
|
|
52
|
+
//# sourceMappingURL=reconcile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../../src/classify/reconcile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAe,KAAK,cAAc,EAAE,MAAM,UAAU,CAAC;AAC5D,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7C,MAAM,WAAW,oBAAoB;IACjC,cAAc,EAAE,cAAc,CAAC;IAC/B,SAAS,EAAE,aAAa,EAAE,CAAC;IAC3B,gFAAgF;IAChF,YAAY,EAAE,MAAM,CAAC;IACrB,4EAA4E;IAC5E,SAAS,EAAE,OAAO,CAAC;IACnB,iEAAiE;IACjE,OAAO,CAAC,EAAE,SAAS;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAA;KAAE,EAAE,CAAC;CACvG;AAED,MAAM,WAAW,gBAAgB;IAC7B,OAAO,EAAE,oBAAoB,CAAC;IAC9B,SAAS,EAAE,oBAAoB,GAAG,IAAI,CAAC;IACvC,SAAS,EAAE,OAAO,CAAC;CACtB;AAgBD;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,oBAAoB,GAAG,IAAI,EAAE,IAAI,EAAE,oBAAoB,GAAG,IAAI,GAAG,gBAAgB,CA8BlH;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,gBAAgB,EAAE,eAAe,SAAM,GAAG,aAAa,EAAE,CA8B3F"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Stage 7: plain text and HTML reconciliation.
|
|
4
|
+
*
|
|
5
|
+
* Measured over the full corpus, 79.2 percent of messages carry both representations, and among
|
|
6
|
+
* human replies that carry both, a quarter disagree about whether the message contains quoted
|
|
7
|
+
* history at all. The disagreement is almost always in the same direction: the HTML part has a
|
|
8
|
+
* quote container and the plain part is a flattened render where the quote markers are simply gone.
|
|
9
|
+
*
|
|
10
|
+
* So HTML is the primary representation when both exist and the HTML actually carries structure.
|
|
11
|
+
* The plain part is still classified independently, and its verdict is used two ways: to confirm,
|
|
12
|
+
* which raises nothing but costs nothing, and to contradict, which caps confidence so that a
|
|
13
|
+
* disputed region is collapsed rather than hidden.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.pickPrimary = pickPrimary;
|
|
17
|
+
exports.reconcile = reconcile;
|
|
18
|
+
/** Fraction of characters classified as hidden or collapsed history. */
|
|
19
|
+
function historyShare(result) {
|
|
20
|
+
let history = 0;
|
|
21
|
+
let total = 0;
|
|
22
|
+
for (const decision of result.decisions) {
|
|
23
|
+
const chars = decision.block.content.length;
|
|
24
|
+
total += chars;
|
|
25
|
+
if (decision.label === 'REPLY_HISTORY') {
|
|
26
|
+
history += chars;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return total > 0 ? history / total : 0;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Choose which representation drives the output.
|
|
33
|
+
*
|
|
34
|
+
* HTML wins by default when both exist, because it is where the structure survives. Plain text
|
|
35
|
+
* wins when the HTML is a stub, which happens with clients that send a token HTML wrapper around a
|
|
36
|
+
* text body, and when only the plain part has an anchor.
|
|
37
|
+
*/
|
|
38
|
+
function pickPrimary(text, html) {
|
|
39
|
+
if (text && !html) {
|
|
40
|
+
return { primary: text, secondary: null, disagrees: false };
|
|
41
|
+
}
|
|
42
|
+
if (html && !text) {
|
|
43
|
+
return { primary: html, secondary: null, disagrees: false };
|
|
44
|
+
}
|
|
45
|
+
if (!text || !html) {
|
|
46
|
+
throw new Error('reconcile called with no representations');
|
|
47
|
+
}
|
|
48
|
+
// A representation with almost no content is a stub, not a real alternative.
|
|
49
|
+
const htmlIsStub = html.contentChars * 4 < text.contentChars;
|
|
50
|
+
const textIsStub = text.contentChars * 4 < html.contentChars;
|
|
51
|
+
let primary = html;
|
|
52
|
+
let secondary = text;
|
|
53
|
+
if (htmlIsStub && !textIsStub) {
|
|
54
|
+
primary = text;
|
|
55
|
+
secondary = html;
|
|
56
|
+
}
|
|
57
|
+
else if (!html.hasAnchor && text.hasAnchor && !textIsStub) {
|
|
58
|
+
primary = text;
|
|
59
|
+
secondary = html;
|
|
60
|
+
}
|
|
61
|
+
// Disagreement is measured on the share of content treated as history, not on block counts,
|
|
62
|
+
// because the two representations rarely segment into the same number of blocks.
|
|
63
|
+
const disagrees = Math.abs(historyShare(primary) - historyShare(secondary)) > 0.25;
|
|
64
|
+
return { primary, secondary, disagrees };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Apply cross representation evidence to the primary decisions.
|
|
68
|
+
*
|
|
69
|
+
* The only adjustment made is downward. Raising confidence because the other representation agrees
|
|
70
|
+
* would let two copies of the same mistake reinforce each other, and the cost of a wrong hide is
|
|
71
|
+
* much higher than the cost of a collapse that could have been a hide.
|
|
72
|
+
*/
|
|
73
|
+
function reconcile(outcome, collapseCeiling = 0.9) {
|
|
74
|
+
const { primary, secondary, disagrees } = outcome;
|
|
75
|
+
const reason = primary.representation === 'html' ? 'html_primary_representation' : 'text_primary_representation';
|
|
76
|
+
for (const decision of primary.decisions) {
|
|
77
|
+
if (!decision.reasons.includes(reason)) {
|
|
78
|
+
decision.reasons.push(reason);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (!secondary || !disagrees) {
|
|
82
|
+
return primary.decisions;
|
|
83
|
+
}
|
|
84
|
+
for (const decision of primary.decisions) {
|
|
85
|
+
if (decision.label !== 'REPLY_HISTORY') {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (decision.reasons.includes('matched_parent_message')) {
|
|
89
|
+
// History matching is evidence about the world, not about markup. A disagreement
|
|
90
|
+
// between two renderings of the same message does not weaken it.
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
decision.confidence = Math.min(decision.confidence, collapseCeiling);
|
|
94
|
+
if (!decision.reasons.includes('representations_disagree')) {
|
|
95
|
+
decision.reasons.push('representations_disagree');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return primary.decisions;
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=reconcile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconcile.js","sourceRoot":"","sources":["../../src/classify/reconcile.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage 6: rule based sequence classification.
|
|
3
|
+
*
|
|
4
|
+
* Blocks are classified in context, never independently. The rules encode the four sequences that
|
|
5
|
+
* matter, in the order they are resolved:
|
|
6
|
+
*
|
|
7
|
+
* NEW_CONTENT, SIGNATURE, REPLY_HEADER, REPLY_HISTORY top posting, the common case
|
|
8
|
+
* INLINE_QUOTE, NEW_CONTENT, INLINE_QUOTE, NEW_CONTENT interleaved answers
|
|
9
|
+
* NEW_CONTENT, FORWARD_HEADER, FORWARDED_CONTENT forwarding with a comment
|
|
10
|
+
* REPLY_HISTORY, NEW_CONTENT bottom posting
|
|
11
|
+
*
|
|
12
|
+
* The load bearing rule is the anchor requirement. A quote region may only become REPLY_HISTORY
|
|
13
|
+
* when something independent of the quoting itself says that it is history: an attribution line, a
|
|
14
|
+
* client quote container, or a match against a message that already existed. Quote depth alone
|
|
15
|
+
* gives INLINE_QUOTE or UNKNOWN, both of which stay visible.
|
|
16
|
+
*
|
|
17
|
+
* The reason a bare quote is not treated as history is the difference between the two examples in
|
|
18
|
+
* the brief. Quoted questions with answers under them are context the reader needs. A trailing
|
|
19
|
+
* quoted copy of the previous mail is noise. Only the surrounding structure tells them apart.
|
|
20
|
+
*/
|
|
21
|
+
import { type Label, type Reason } from '../types';
|
|
22
|
+
import { type Block } from '../segment/types';
|
|
23
|
+
import { type ConfidenceConfig } from './confidence';
|
|
24
|
+
export interface BlockDecision {
|
|
25
|
+
block: Block;
|
|
26
|
+
label: Label;
|
|
27
|
+
confidence: number;
|
|
28
|
+
reasons: Reason[];
|
|
29
|
+
}
|
|
30
|
+
export interface ClassifyOptions {
|
|
31
|
+
config?: ConfidenceConfig;
|
|
32
|
+
/**
|
|
33
|
+
* Indices of blocks whose text was found in a known earlier message. Supplies the anchor for
|
|
34
|
+
* blocks that have no structural evidence of their own.
|
|
35
|
+
*/
|
|
36
|
+
historyMatchedIndices?: ReadonlySet<number>;
|
|
37
|
+
/**
|
|
38
|
+
* Display name and local part of the sender, lowercased. A trailing short block containing the
|
|
39
|
+
* sender's own name is strong evidence of a signature: it is how nearly every signature begins,
|
|
40
|
+
* and unlike a greeting phrase it is language independent.
|
|
41
|
+
*/
|
|
42
|
+
senderTokens?: readonly string[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Classify one representation's blocks.
|
|
46
|
+
*
|
|
47
|
+
* Runs in four passes so that later passes can see the whole picture: footers first, because they
|
|
48
|
+
* bound where history can start; then anchors; then regions; then defaults.
|
|
49
|
+
*/
|
|
50
|
+
export declare function classifyBlocks(blocks: readonly Block[], options?: ClassifyOptions): BlockDecision[];
|
|
51
|
+
//# sourceMappingURL=rules.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../src/classify/rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,MAAM,EAAE,MAAM,UAAU,CAAC;AACnD,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAsB,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEzE,MAAM,WAAW,aAAa;IAC1B,KAAK,EAAE,KAAK,CAAC;IACb,KAAK,EAAE,KAAK,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC5B,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B;;;OAGG;IACH,qBAAqB,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC5C;;;;OAIG;IACH,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC;AAqED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,EAAE,OAAO,GAAE,eAAoB,GAAG,aAAa,EAAE,CA4lBvG"}
|