@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 @@
|
|
|
1
|
+
{"version":3,"file":"llm.js","sourceRoot":"","sources":["../../src/annotate/llm.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The annotation prompt and its response schema.
|
|
3
|
+
*
|
|
4
|
+
* Versioned, because a prompt change invalidates comparability between annotation runs exactly the
|
|
5
|
+
* way a code change does. The version is stored with every label.
|
|
6
|
+
*
|
|
7
|
+
* Two properties are enforced by construction rather than by asking nicely:
|
|
8
|
+
*
|
|
9
|
+
* 1. The model receives numbered blocks and returns labels for those numbers. It is never asked
|
|
10
|
+
* to produce text, so it cannot rewrite, summarise or paraphrase the sender's words. The
|
|
11
|
+
* "select, never generate" rule holds even with a generative model in the loop.
|
|
12
|
+
* 2. The response schema is strict, with a closed label enum, so a malformed or creative answer
|
|
13
|
+
* is rejected by the API rather than parsed hopefully at this end.
|
|
14
|
+
*/
|
|
15
|
+
export declare const PROMPT_VERSION = "annotate-v3";
|
|
16
|
+
export declare const SYSTEM_PROMPT = "You label blocks of an email message so a chat application can decide what to show.\n\nYou are given the message split into numbered blocks, in order. Return exactly one label for every\nblock index you are given. Never return text, only indices and labels.\n\nLABELS\n\nNEW_CONTENT\n Text the sender wrote themselves in this message. This is the default when unsure between\n NEW_CONTENT and anything else.\n\nINLINE_QUOTE\n Old text the sender quoted deliberately in order to answer it here, point by point. The giveaway\n is that the sender's own new text appears after it and responds to it. Inline quotes must stay\n visible, because the answer beside them is meaningless without them.\n\nREPLY_HISTORY\n A copy of an earlier message that the sender's mail client attached automatically. Usually the\n whole previous message, sitting at the end, introduced by an attribution line such as\n \"On <date>, <person> wrote:\", or \"-----Original Message-----\", or an embedded\n From:/Sent:/To:/Subject: block. This is the only label whose content gets hidden.\n\nFORWARDED_CONTENT\n A message the sender forwarded on purpose, usually after \"Begin forwarded message:\" or\n \"---------- Forwarded message ----------\". It is content the sender meant to pass on, so it is\n kept and collapsed, never hidden.\n\nSIGNATURE\n The sender's own sign-off block: name, role, company, phone, links, \"Sent from my phone\".\n\nDISCLAIMER\n Boilerplate attached by a system, gateway or template independently of what the sender wrote:\n confidentiality and liability notices, virus scanning notices, company registration and VAT\n numbers, registered office addresses, unsubscribe and preference footers, \"powered by\" branding,\n and standing promotional footers that repeat on every notification from a service.\n\n Guard: if the boilerplate is the whole message, label it NEW_CONTENT. A newsletter is not a\n disclaimer with nothing attached to it. When there is no other content, the boilerplate is the\n content.\n\nUNKNOWN\n Use when you genuinely cannot tell. Prefer this over guessing REPLY_HISTORY.\n\nHOW TO DECIDE\n\nThe important distinction is INLINE_QUOTE versus REPLY_HISTORY. Both are old text. The difference\nis purpose:\n\n - If quoted text is followed by the sender's answer to it, and especially if this alternates\n several times, it is INLINE_QUOTE.\n - If quoted text runs to the end of the message as one uninterrupted copy of the previous mail,\n it is REPLY_HISTORY.\n\nQuote characters such as \">\" are a hint, not proof. Some clients embed the previous message with no\nquote markers at all, as plain paragraphs after an attribution line: that is still REPLY_HISTORY.\nSome senders paste text that merely looks like an email; that is NEW_CONTENT.\n\nQUOTE DEPTH\n\nEach block is marked with its quote depth, exactly as the message shows it. Depth 0 means the block\nwas not quoted. Depth 1 means it was quoted once, depth 2 means it was quoted inside a quote, and so\non. Depth is structural evidence, not a verdict.\n\nLong threads nest: depth 1 is the message being replied to, depth 2 is the one before that. In a\nnested thread the sender's own new text is almost always at depth 0, and everything at depth 1 or\ndeeper is history, including greetings, thanks, questions and sign-offs. Text reading like a fresh\npersonal message is still REPLY_HISTORY if it is quoted, because someone wrote it in an earlier\nmessage, not in this one.\n\nREGION WINS OVER CONTENT TYPE\n\nA message is made of regions: what the sender wrote now, history their client attached, material\nthey forwarded on purpose. Signatures, greetings and legal footers exist inside all of them.\n\nLabel every block by the region it belongs to, never by what the text looks like. The question is\nalways \"which message does this block belong to?\".\n\n - A signature belonging to this sender, in this message, is SIGNATURE.\n - A signature inside quoted history is REPLY_HISTORY. It was someone else's sign-off, written in\n an earlier message.\n - A legal disclaimer inside a forwarded message is FORWARDED_CONTENT.\n - A \"--\" delimiter inside a quote is part of that quote.\n\nThis applies to every footer type, not just signatures. A confidentiality notice sitting inside a\nforward is FORWARDED_CONTENT, not DISCLAIMER.\n\nCOST OF ERRORS\n\nMarking the sender's own words as REPLY_HISTORY hides them from the reader, who may never find out\nwhat was said. Leaving old text visible is only untidy. Treat the first mistake as ten times worse\nthan the second. When you are torn between NEW_CONTENT and REPLY_HISTORY, choose NEW_CONTENT. When\nyou are torn between INLINE_QUOTE and REPLY_HISTORY, choose INLINE_QUOTE.\n\nCONFIDENCE\n\nGive each label a confidence between 0 and 1. Use below 0.7 whenever the block is ambiguous, short,\nor could plausibly take another label.";
|
|
17
|
+
export declare const RESPONSE_SCHEMA: Record<string, unknown>;
|
|
18
|
+
export interface PromptBlock {
|
|
19
|
+
index: number;
|
|
20
|
+
text: string;
|
|
21
|
+
truncated: boolean;
|
|
22
|
+
/** Quote nesting depth, as the message shows it. Zero means not quoted. */
|
|
23
|
+
quoteDepth: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Render the user message.
|
|
27
|
+
*
|
|
28
|
+
* Deliberately minimal context: block text, quote depth, and whether the message is a reply. No
|
|
29
|
+
* subject, no addresses, no headers. Those would help a little and would send noticeably more
|
|
30
|
+
* personal data to a third party, which is the wrong trade for a marginal accuracy gain.
|
|
31
|
+
*
|
|
32
|
+
* Quote depth is shown because a human annotator reading the raw message would see the ">" markers
|
|
33
|
+
* and the visual nesting. Hiding it made the first version of this prompt label quoted greetings
|
|
34
|
+
* inside a five deep Estonian thread as freshly written text, which is exactly the mistake the
|
|
35
|
+
* depth marker prevents.
|
|
36
|
+
*/
|
|
37
|
+
export declare function renderUserPrompt(blocks: readonly PromptBlock[], isReply: boolean): string;
|
|
38
|
+
//# sourceMappingURL=prompt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../../src/annotate/prompt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,eAAO,MAAM,cAAc,gBAAgB,CAAC;AAE5C,eAAO,MAAM,aAAa,gzJAgGa,CAAC;AAExC,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAsBnD,CAAC;AAEF,MAAM,WAAW,WAAW;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,OAAO,CAAC;IACnB,2EAA2E;IAC3E,UAAU,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAYzF"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The annotation prompt and its response schema.
|
|
4
|
+
*
|
|
5
|
+
* Versioned, because a prompt change invalidates comparability between annotation runs exactly the
|
|
6
|
+
* way a code change does. The version is stored with every label.
|
|
7
|
+
*
|
|
8
|
+
* Two properties are enforced by construction rather than by asking nicely:
|
|
9
|
+
*
|
|
10
|
+
* 1. The model receives numbered blocks and returns labels for those numbers. It is never asked
|
|
11
|
+
* to produce text, so it cannot rewrite, summarise or paraphrase the sender's words. The
|
|
12
|
+
* "select, never generate" rule holds even with a generative model in the loop.
|
|
13
|
+
* 2. The response schema is strict, with a closed label enum, so a malformed or creative answer
|
|
14
|
+
* is rejected by the API rather than parsed hopefully at this end.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.RESPONSE_SCHEMA = exports.SYSTEM_PROMPT = exports.PROMPT_VERSION = void 0;
|
|
18
|
+
exports.renderUserPrompt = renderUserPrompt;
|
|
19
|
+
exports.PROMPT_VERSION = 'annotate-v3';
|
|
20
|
+
exports.SYSTEM_PROMPT = `You label blocks of an email message so a chat application can decide what to show.
|
|
21
|
+
|
|
22
|
+
You are given the message split into numbered blocks, in order. Return exactly one label for every
|
|
23
|
+
block index you are given. Never return text, only indices and labels.
|
|
24
|
+
|
|
25
|
+
LABELS
|
|
26
|
+
|
|
27
|
+
NEW_CONTENT
|
|
28
|
+
Text the sender wrote themselves in this message. This is the default when unsure between
|
|
29
|
+
NEW_CONTENT and anything else.
|
|
30
|
+
|
|
31
|
+
INLINE_QUOTE
|
|
32
|
+
Old text the sender quoted deliberately in order to answer it here, point by point. The giveaway
|
|
33
|
+
is that the sender's own new text appears after it and responds to it. Inline quotes must stay
|
|
34
|
+
visible, because the answer beside them is meaningless without them.
|
|
35
|
+
|
|
36
|
+
REPLY_HISTORY
|
|
37
|
+
A copy of an earlier message that the sender's mail client attached automatically. Usually the
|
|
38
|
+
whole previous message, sitting at the end, introduced by an attribution line such as
|
|
39
|
+
"On <date>, <person> wrote:", or "-----Original Message-----", or an embedded
|
|
40
|
+
From:/Sent:/To:/Subject: block. This is the only label whose content gets hidden.
|
|
41
|
+
|
|
42
|
+
FORWARDED_CONTENT
|
|
43
|
+
A message the sender forwarded on purpose, usually after "Begin forwarded message:" or
|
|
44
|
+
"---------- Forwarded message ----------". It is content the sender meant to pass on, so it is
|
|
45
|
+
kept and collapsed, never hidden.
|
|
46
|
+
|
|
47
|
+
SIGNATURE
|
|
48
|
+
The sender's own sign-off block: name, role, company, phone, links, "Sent from my phone".
|
|
49
|
+
|
|
50
|
+
DISCLAIMER
|
|
51
|
+
Boilerplate attached by a system, gateway or template independently of what the sender wrote:
|
|
52
|
+
confidentiality and liability notices, virus scanning notices, company registration and VAT
|
|
53
|
+
numbers, registered office addresses, unsubscribe and preference footers, "powered by" branding,
|
|
54
|
+
and standing promotional footers that repeat on every notification from a service.
|
|
55
|
+
|
|
56
|
+
Guard: if the boilerplate is the whole message, label it NEW_CONTENT. A newsletter is not a
|
|
57
|
+
disclaimer with nothing attached to it. When there is no other content, the boilerplate is the
|
|
58
|
+
content.
|
|
59
|
+
|
|
60
|
+
UNKNOWN
|
|
61
|
+
Use when you genuinely cannot tell. Prefer this over guessing REPLY_HISTORY.
|
|
62
|
+
|
|
63
|
+
HOW TO DECIDE
|
|
64
|
+
|
|
65
|
+
The important distinction is INLINE_QUOTE versus REPLY_HISTORY. Both are old text. The difference
|
|
66
|
+
is purpose:
|
|
67
|
+
|
|
68
|
+
- If quoted text is followed by the sender's answer to it, and especially if this alternates
|
|
69
|
+
several times, it is INLINE_QUOTE.
|
|
70
|
+
- If quoted text runs to the end of the message as one uninterrupted copy of the previous mail,
|
|
71
|
+
it is REPLY_HISTORY.
|
|
72
|
+
|
|
73
|
+
Quote characters such as ">" are a hint, not proof. Some clients embed the previous message with no
|
|
74
|
+
quote markers at all, as plain paragraphs after an attribution line: that is still REPLY_HISTORY.
|
|
75
|
+
Some senders paste text that merely looks like an email; that is NEW_CONTENT.
|
|
76
|
+
|
|
77
|
+
QUOTE DEPTH
|
|
78
|
+
|
|
79
|
+
Each block is marked with its quote depth, exactly as the message shows it. Depth 0 means the block
|
|
80
|
+
was not quoted. Depth 1 means it was quoted once, depth 2 means it was quoted inside a quote, and so
|
|
81
|
+
on. Depth is structural evidence, not a verdict.
|
|
82
|
+
|
|
83
|
+
Long threads nest: depth 1 is the message being replied to, depth 2 is the one before that. In a
|
|
84
|
+
nested thread the sender's own new text is almost always at depth 0, and everything at depth 1 or
|
|
85
|
+
deeper is history, including greetings, thanks, questions and sign-offs. Text reading like a fresh
|
|
86
|
+
personal message is still REPLY_HISTORY if it is quoted, because someone wrote it in an earlier
|
|
87
|
+
message, not in this one.
|
|
88
|
+
|
|
89
|
+
REGION WINS OVER CONTENT TYPE
|
|
90
|
+
|
|
91
|
+
A message is made of regions: what the sender wrote now, history their client attached, material
|
|
92
|
+
they forwarded on purpose. Signatures, greetings and legal footers exist inside all of them.
|
|
93
|
+
|
|
94
|
+
Label every block by the region it belongs to, never by what the text looks like. The question is
|
|
95
|
+
always "which message does this block belong to?".
|
|
96
|
+
|
|
97
|
+
- A signature belonging to this sender, in this message, is SIGNATURE.
|
|
98
|
+
- A signature inside quoted history is REPLY_HISTORY. It was someone else's sign-off, written in
|
|
99
|
+
an earlier message.
|
|
100
|
+
- A legal disclaimer inside a forwarded message is FORWARDED_CONTENT.
|
|
101
|
+
- A "--" delimiter inside a quote is part of that quote.
|
|
102
|
+
|
|
103
|
+
This applies to every footer type, not just signatures. A confidentiality notice sitting inside a
|
|
104
|
+
forward is FORWARDED_CONTENT, not DISCLAIMER.
|
|
105
|
+
|
|
106
|
+
COST OF ERRORS
|
|
107
|
+
|
|
108
|
+
Marking the sender's own words as REPLY_HISTORY hides them from the reader, who may never find out
|
|
109
|
+
what was said. Leaving old text visible is only untidy. Treat the first mistake as ten times worse
|
|
110
|
+
than the second. When you are torn between NEW_CONTENT and REPLY_HISTORY, choose NEW_CONTENT. When
|
|
111
|
+
you are torn between INLINE_QUOTE and REPLY_HISTORY, choose INLINE_QUOTE.
|
|
112
|
+
|
|
113
|
+
CONFIDENCE
|
|
114
|
+
|
|
115
|
+
Give each label a confidence between 0 and 1. Use below 0.7 whenever the block is ambiguous, short,
|
|
116
|
+
or could plausibly take another label.`;
|
|
117
|
+
exports.RESPONSE_SCHEMA = {
|
|
118
|
+
type: 'object',
|
|
119
|
+
additionalProperties: false,
|
|
120
|
+
required: ['labels'],
|
|
121
|
+
properties: {
|
|
122
|
+
labels: {
|
|
123
|
+
type: 'array',
|
|
124
|
+
items: {
|
|
125
|
+
type: 'object',
|
|
126
|
+
additionalProperties: false,
|
|
127
|
+
required: ['index', 'label', 'confidence'],
|
|
128
|
+
properties: {
|
|
129
|
+
index: { type: 'integer', description: 'The block number being labelled.' },
|
|
130
|
+
label: {
|
|
131
|
+
type: 'string',
|
|
132
|
+
enum: ['NEW_CONTENT', 'INLINE_QUOTE', 'REPLY_HISTORY', 'FORWARDED_CONTENT', 'SIGNATURE', 'DISCLAIMER', 'UNKNOWN']
|
|
133
|
+
},
|
|
134
|
+
confidence: { type: 'number' }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Render the user message.
|
|
142
|
+
*
|
|
143
|
+
* Deliberately minimal context: block text, quote depth, and whether the message is a reply. No
|
|
144
|
+
* subject, no addresses, no headers. Those would help a little and would send noticeably more
|
|
145
|
+
* personal data to a third party, which is the wrong trade for a marginal accuracy gain.
|
|
146
|
+
*
|
|
147
|
+
* Quote depth is shown because a human annotator reading the raw message would see the ">" markers
|
|
148
|
+
* and the visual nesting. Hiding it made the first version of this prompt label quoted greetings
|
|
149
|
+
* inside a five deep Estonian thread as freshly written text, which is exactly the mistake the
|
|
150
|
+
* depth marker prevents.
|
|
151
|
+
*/
|
|
152
|
+
function renderUserPrompt(blocks, isReply) {
|
|
153
|
+
const lines = [];
|
|
154
|
+
lines.push(isReply ? 'This message is a reply to an earlier message.' : 'This message is not marked as a reply.');
|
|
155
|
+
lines.push(`It has ${blocks.length} block(s), in the order they appear. Label every index listed below.`);
|
|
156
|
+
lines.push('');
|
|
157
|
+
for (const block of blocks) {
|
|
158
|
+
const depth = block.quoteDepth > 0 ? `, quoted depth ${block.quoteDepth}` : ', not quoted';
|
|
159
|
+
lines.push(`[${block.index}${depth}${block.truncated ? ', truncated' : ''}]`);
|
|
160
|
+
lines.push(block.text);
|
|
161
|
+
lines.push('');
|
|
162
|
+
}
|
|
163
|
+
return lines.join('\n');
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=prompt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt.js","sourceRoot":"","sources":["../../src/annotate/prompt.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;GAaG;;;AAkJH,4CAYC;AA5JY,QAAA,cAAc,GAAG,aAAa,CAAC;AAE/B,QAAA,aAAa,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uCAgGU,CAAC;AAE3B,QAAA,eAAe,GAA4B;IACpD,IAAI,EAAE,QAAQ;IACd,oBAAoB,EAAE,KAAK;IAC3B,QAAQ,EAAE,CAAC,QAAQ,CAAC;IACpB,UAAU,EAAE;QACR,MAAM,EAAE;YACJ,IAAI,EAAE,OAAO;YACb,KAAK,EAAE;gBACH,IAAI,EAAE,QAAQ;gBACd,oBAAoB,EAAE,KAAK;gBAC3B,QAAQ,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,YAAY,CAAC;gBAC1C,UAAU,EAAE;oBACR,KAAK,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,kCAAkC,EAAE;oBAC3E,KAAK,EAAE;wBACH,IAAI,EAAE,QAAQ;wBACd,IAAI,EAAE,CAAC,aAAa,EAAE,cAAc,EAAE,eAAe,EAAE,mBAAmB,EAAE,WAAW,EAAE,YAAY,EAAE,SAAS,CAAC;qBACpH;oBACD,UAAU,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;iBACjC;aACJ;SACJ;KACJ;CACJ,CAAC;AAUF;;;;;;;;;;;GAWG;AACH,SAAgB,gBAAgB,CAAC,MAA8B,EAAE,OAAgB;IAC7E,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,gDAAgD,CAAC,CAAC,CAAC,wCAAwC,CAAC,CAAC;IAClH,KAAK,CAAC,IAAI,CAAC,UAAU,MAAM,CAAC,MAAM,sEAAsE,CAAC,CAAC;IAC1G,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,KAAK,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,kBAAkB,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,cAAc,CAAC;QAC3F,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;QAC9E,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACvB,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACnB,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC5B,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Annotation persistence.
|
|
3
|
+
*
|
|
4
|
+
* Every label carries its source, the model and prompt version that produced it, and the
|
|
5
|
+
* deterministic baseline's verdict on the same block captured at the same moment. Storing the
|
|
6
|
+
* baseline alongside is what makes a run reproducible as evidence: a later rule change cannot
|
|
7
|
+
* silently rewrite what the disagreement was.
|
|
8
|
+
*/
|
|
9
|
+
import { type CorpusStore } from '../corpus/store';
|
|
10
|
+
import { type Label, type Reason } from '../types';
|
|
11
|
+
export interface AnnotationRow {
|
|
12
|
+
path: string;
|
|
13
|
+
representation: 'text' | 'html';
|
|
14
|
+
blockIndex: number;
|
|
15
|
+
mimePart: string;
|
|
16
|
+
blockStart: number;
|
|
17
|
+
blockEnd: number;
|
|
18
|
+
charCount: number;
|
|
19
|
+
source: 'llm' | 'human' | 'weak' | 'distant';
|
|
20
|
+
label: Label;
|
|
21
|
+
confidence: number;
|
|
22
|
+
baselineLabel: Label | null;
|
|
23
|
+
baselineConfidence: number | null;
|
|
24
|
+
baselineReasons: readonly Reason[];
|
|
25
|
+
}
|
|
26
|
+
export interface AnnotationRunInfo {
|
|
27
|
+
source: string;
|
|
28
|
+
model: string | null;
|
|
29
|
+
promptVersion: string | null;
|
|
30
|
+
notes?: string;
|
|
31
|
+
}
|
|
32
|
+
export declare function startAnnotationRun(store: CorpusStore, info: AnnotationRunInfo): number;
|
|
33
|
+
export declare function finishAnnotationRun(store: CorpusStore, runId: number, counts: {
|
|
34
|
+
messages: number;
|
|
35
|
+
blocks: number;
|
|
36
|
+
failures: number;
|
|
37
|
+
promptTokens: number;
|
|
38
|
+
completionTokens: number;
|
|
39
|
+
}): void;
|
|
40
|
+
export declare function writeAnnotations(store: CorpusStore, runId: number, rows: readonly AnnotationRow[]): void;
|
|
41
|
+
/** Paths already annotated by this source and model, so a run can resume. */
|
|
42
|
+
export declare function annotatedPaths(store: CorpusStore, source: string, model: string | null): Set<string>;
|
|
43
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/annotate/store.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAqB,KAAK,KAAK,EAAE,KAAK,MAAM,EAAE,MAAM,UAAU,CAAC;AAEtE,MAAM,WAAW,aAAa;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,KAAK,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;IAC7C,KAAK,EAAE,KAAK,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,KAAK,GAAG,IAAI,CAAC;IAC5B,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;CACtC;AAED,MAAM,WAAW,iBAAiB;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,iBAAiB,GAAG,MAAM,CAYtF;AAED,wBAAgB,mBAAmB,CAC/B,KAAK,EAAE,WAAW,EAClB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAA;CAAE,GAC/G,IAAI,CAaN;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,aAAa,EAAE,GAAG,IAAI,CA6BxG;AAED,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,CASpG"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Annotation persistence.
|
|
4
|
+
*
|
|
5
|
+
* Every label carries its source, the model and prompt version that produced it, and the
|
|
6
|
+
* deterministic baseline's verdict on the same block captured at the same moment. Storing the
|
|
7
|
+
* baseline alongside is what makes a run reproducible as evidence: a later rule change cannot
|
|
8
|
+
* silently rewrite what the disagreement was.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.startAnnotationRun = startAnnotationRun;
|
|
12
|
+
exports.finishAnnotationRun = finishAnnotationRun;
|
|
13
|
+
exports.writeAnnotations = writeAnnotations;
|
|
14
|
+
exports.annotatedPaths = annotatedPaths;
|
|
15
|
+
const types_1 = require("../types");
|
|
16
|
+
function startAnnotationRun(store, info) {
|
|
17
|
+
const result = store.run(`INSERT INTO annotation_runs (source, model, prompt_version, extractor_version, started_at, notes)
|
|
18
|
+
VALUES (?, ?, ?, ?, ?, ?)`, info.source, info.model, info.promptVersion, types_1.EXTRACTOR_VERSION, Date.now(), info.notes ?? null);
|
|
19
|
+
return Number(result.lastInsertRowid);
|
|
20
|
+
}
|
|
21
|
+
function finishAnnotationRun(store, runId, counts) {
|
|
22
|
+
store.run(`UPDATE annotation_runs
|
|
23
|
+
SET finished_at = ?, messages = ?, blocks = ?, failures = ?, prompt_tokens = ?, completion_tokens = ?
|
|
24
|
+
WHERE id = ?`, Date.now(), counts.messages, counts.blocks, counts.failures, counts.promptTokens, counts.completionTokens, runId);
|
|
25
|
+
}
|
|
26
|
+
function writeAnnotations(store, runId, rows) {
|
|
27
|
+
if (!rows.length) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
store.transaction(() => {
|
|
31
|
+
for (const row of rows) {
|
|
32
|
+
store.run(`INSERT INTO annotations (
|
|
33
|
+
run_id, path, representation, block_index, mime_part, block_start, block_end, char_count,
|
|
34
|
+
source, label, confidence, baseline_label, baseline_conf, baseline_reasons, agrees
|
|
35
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, runId, row.path, row.representation, row.blockIndex, row.mimePart, row.blockStart, row.blockEnd, row.charCount, row.source, row.label, row.confidence, row.baselineLabel, row.baselineConfidence, row.baselineReasons.join(','), row.baselineLabel === row.label ? 1 : 0);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** Paths already annotated by this source and model, so a run can resume. */
|
|
40
|
+
function annotatedPaths(store, source, model) {
|
|
41
|
+
const rows = model
|
|
42
|
+
? store.query('SELECT DISTINCT a.path AS path FROM annotations a JOIN annotation_runs r ON r.id = a.run_id WHERE a.source = ? AND r.model = ?', source, model)
|
|
43
|
+
: store.query('SELECT DISTINCT path FROM annotations WHERE source = ?', source);
|
|
44
|
+
return new Set(rows.map(row => row.path));
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/annotate/store.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;AA4BH,gDAYC;AAED,kDAiBC;AAED,4CA6BC;AAGD,wCASC;AAnGD,oCAAsE;AAyBtE,SAAgB,kBAAkB,CAAC,KAAkB,EAAE,IAAuB;IAC1E,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CACpB;mCAC2B,EAC3B,IAAI,CAAC,MAAM,EACX,IAAI,CAAC,KAAK,EACV,IAAI,CAAC,aAAa,EAClB,yBAAiB,EACjB,IAAI,CAAC,GAAG,EAAE,EACV,IAAI,CAAC,KAAK,IAAI,IAAI,CACrB,CAAC;IACF,OAAO,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;AAC1C,CAAC;AAED,SAAgB,mBAAmB,CAC/B,KAAkB,EAClB,KAAa,EACb,MAA8G;IAE9G,KAAK,CAAC,GAAG,CACL;;sBAEc,EACd,IAAI,CAAC,GAAG,EAAE,EACV,MAAM,CAAC,QAAQ,EACf,MAAM,CAAC,MAAM,EACb,MAAM,CAAC,QAAQ,EACf,MAAM,CAAC,YAAY,EACnB,MAAM,CAAC,gBAAgB,EACvB,KAAK,CACR,CAAC;AACN,CAAC;AAED,SAAgB,gBAAgB,CAAC,KAAkB,EAAE,KAAa,EAAE,IAA8B;IAC9F,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;QACf,OAAO;IACX,CAAC;IACD,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE;QACnB,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACrB,KAAK,CAAC,GAAG,CACL;;;wEAGwD,EACxD,KAAK,EACL,GAAG,CAAC,IAAI,EACR,GAAG,CAAC,cAAc,EAClB,GAAG,CAAC,UAAU,EACd,GAAG,CAAC,QAAQ,EACZ,GAAG,CAAC,UAAU,EACd,GAAG,CAAC,QAAQ,EACZ,GAAG,CAAC,SAAS,EACb,GAAG,CAAC,MAAM,EACV,GAAG,CAAC,KAAK,EACT,GAAG,CAAC,UAAU,EACd,GAAG,CAAC,aAAa,EACjB,GAAG,CAAC,kBAAkB,EACtB,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,EAC7B,GAAG,CAAC,aAAa,KAAK,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAC1C,CAAC;QACN,CAAC;IACL,CAAC,CAAC,CAAC;AACP,CAAC;AAED,6EAA6E;AAC7E,SAAgB,cAAc,CAAC,KAAkB,EAAE,MAAc,EAAE,KAAoB;IACnF,MAAM,IAAI,GAAG,KAAK;QACd,CAAC,CAAC,KAAK,CAAC,KAAK,CACP,gIAAgI,EAChI,MAAM,EACN,KAAK,CACR;QACH,CAAC,CAAC,KAAK,CAAC,KAAK,CAAmB,wDAAwD,EAAE,MAAM,CAAC,CAAC;IACtG,OAAO,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;AAC9C,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML helpers used by the corpus scanner.
|
|
3
|
+
*
|
|
4
|
+
* Full DOM based block segmentation belongs to the extraction pipeline and is intentionally not
|
|
5
|
+
* done here. A corpus pass over 320k files runs this code once per message, and parsing every
|
|
6
|
+
* newsletter into a DOM to compute aggregate statistics would dominate the whole scan. The client
|
|
7
|
+
* hints this module looks for are literal class and id names, so a scan over the source is both
|
|
8
|
+
* cheaper and more faithful than a DOM query, which would silently normalise malformed markup.
|
|
9
|
+
*/
|
|
10
|
+
/** Decode HTML entities. Unknown entities are left as written rather than dropped. */
|
|
11
|
+
export declare function decodeEntities(input: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Cheap text projection of an HTML body.
|
|
14
|
+
*
|
|
15
|
+
* Good enough for language estimation, marker detection and length statistics. It is not a
|
|
16
|
+
* replacement for the extraction pipeline's DOM walk, and it deliberately keeps blank lines so that
|
|
17
|
+
* paragraph structure survives.
|
|
18
|
+
*/
|
|
19
|
+
export declare function stripTagsToText(html: string): string;
|
|
20
|
+
/** Structural hints found in an HTML body, keyed by the id used in html-clients.json. */
|
|
21
|
+
export interface HtmlHints {
|
|
22
|
+
ids: string[];
|
|
23
|
+
hasBlockquote: boolean;
|
|
24
|
+
blockquoteCount: number;
|
|
25
|
+
hasTable: boolean;
|
|
26
|
+
tableCount: number;
|
|
27
|
+
hasImage: boolean;
|
|
28
|
+
/** True when the markup contains an unclosed tag pattern that suggests malformed HTML. */
|
|
29
|
+
suspectMalformed: boolean;
|
|
30
|
+
}
|
|
31
|
+
/** Detect client structure hints without building a DOM. */
|
|
32
|
+
export declare function detectHtmlHints(html: string): HtmlHints;
|
|
33
|
+
//# sourceMappingURL=html.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../src/canonical/html.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAgDH,sFAAsF;AACtF,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAoBpD;AAQD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAgBpD;AAED,yFAAyF;AACzF,MAAM,WAAW,SAAS;IACtB,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,aAAa,EAAE,OAAO,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,0FAA0F;IAC1F,gBAAgB,EAAE,OAAO,CAAC;CAC7B;AAyBD,4DAA4D;AAC5D,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAqBvD"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* HTML helpers used by the corpus scanner.
|
|
4
|
+
*
|
|
5
|
+
* Full DOM based block segmentation belongs to the extraction pipeline and is intentionally not
|
|
6
|
+
* done here. A corpus pass over 320k files runs this code once per message, and parsing every
|
|
7
|
+
* newsletter into a DOM to compute aggregate statistics would dominate the whole scan. The client
|
|
8
|
+
* hints this module looks for are literal class and id names, so a scan over the source is both
|
|
9
|
+
* cheaper and more faithful than a DOM query, which would silently normalise malformed markup.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.decodeEntities = decodeEntities;
|
|
13
|
+
exports.stripTagsToText = stripTagsToText;
|
|
14
|
+
exports.detectHtmlHints = detectHtmlHints;
|
|
15
|
+
/** Entities common in mail. Numeric references are handled separately. */
|
|
16
|
+
const NAMED_ENTITIES = {
|
|
17
|
+
amp: '&',
|
|
18
|
+
lt: '<',
|
|
19
|
+
gt: '>',
|
|
20
|
+
quot: '"',
|
|
21
|
+
apos: "'",
|
|
22
|
+
nbsp: ' ',
|
|
23
|
+
ndash: '–',
|
|
24
|
+
mdash: '—',
|
|
25
|
+
lsquo: '‘',
|
|
26
|
+
rsquo: '’',
|
|
27
|
+
ldquo: '“',
|
|
28
|
+
rdquo: '”',
|
|
29
|
+
hellip: '…',
|
|
30
|
+
bull: '•',
|
|
31
|
+
middot: '·',
|
|
32
|
+
laquo: '«',
|
|
33
|
+
raquo: '»',
|
|
34
|
+
copy: '©',
|
|
35
|
+
reg: '®',
|
|
36
|
+
trade: '™',
|
|
37
|
+
euro: '€',
|
|
38
|
+
pound: '£',
|
|
39
|
+
deg: '°',
|
|
40
|
+
szlig: 'ß',
|
|
41
|
+
auml: 'ä',
|
|
42
|
+
ouml: 'ö',
|
|
43
|
+
uuml: 'ü',
|
|
44
|
+
Auml: 'Ä',
|
|
45
|
+
Ouml: 'Ö',
|
|
46
|
+
Uuml: 'Ü',
|
|
47
|
+
aring: 'å',
|
|
48
|
+
Aring: 'Å',
|
|
49
|
+
aelig: 'æ',
|
|
50
|
+
oslash: 'ø',
|
|
51
|
+
eacute: 'é',
|
|
52
|
+
egrave: 'è',
|
|
53
|
+
agrave: 'à',
|
|
54
|
+
ccedil: 'ç',
|
|
55
|
+
ntilde: 'ñ',
|
|
56
|
+
shy: '',
|
|
57
|
+
zwnj: '',
|
|
58
|
+
zwj: ''
|
|
59
|
+
};
|
|
60
|
+
/** Decode HTML entities. Unknown entities are left as written rather than dropped. */
|
|
61
|
+
function decodeEntities(input) {
|
|
62
|
+
if (!input.includes('&')) {
|
|
63
|
+
return input;
|
|
64
|
+
}
|
|
65
|
+
return input.replace(/&(#[xX]?[0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]{1,31});/g, (whole, body) => {
|
|
66
|
+
if (body.charCodeAt(0) === 35) {
|
|
67
|
+
const isHex = body[1] === 'x' || body[1] === 'X';
|
|
68
|
+
const code = parseInt(isHex ? body.slice(2) : body.slice(1), isHex ? 16 : 10);
|
|
69
|
+
if (!Number.isFinite(code) || code < 0 || code > 0x10ffff) {
|
|
70
|
+
return whole;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
return String.fromCodePoint(code);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return whole;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const named = NAMED_ENTITIES[body];
|
|
80
|
+
return named === undefined ? whole : named;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/** Elements whose content is never user visible text. */
|
|
84
|
+
const INVISIBLE = /<(script|style|head|title|noscript|template)\b[^>]*>[\s\S]*?<\/\1\s*>/gi;
|
|
85
|
+
/** Elements that introduce a line break in the text projection. */
|
|
86
|
+
const BLOCK_BREAK = /<\/?(p|div|br|tr|li|h[1-6]|table|blockquote|pre|hr|section|article|ul|ol|dd|dt)\b[^>]*>/gi;
|
|
87
|
+
/**
|
|
88
|
+
* Cheap text projection of an HTML body.
|
|
89
|
+
*
|
|
90
|
+
* Good enough for language estimation, marker detection and length statistics. It is not a
|
|
91
|
+
* replacement for the extraction pipeline's DOM walk, and it deliberately keeps blank lines so that
|
|
92
|
+
* paragraph structure survives.
|
|
93
|
+
*/
|
|
94
|
+
function stripTagsToText(html) {
|
|
95
|
+
let text = html.replace(INVISIBLE, ' ');
|
|
96
|
+
text = text.replace(/<!--[\s\S]*?-->/g, ' ');
|
|
97
|
+
// Word and Outlook emit downlevel-revealed conditional comments, which are not HTML comments
|
|
98
|
+
// and survive both the comment strip and the tag strip: <![if !supportLists]>. Left in, they
|
|
99
|
+
// appear verbatim in the extracted text of every Word authored bullet list.
|
|
100
|
+
text = text.replace(/<!\[[^\]]*\]>/g, ' ');
|
|
101
|
+
text = text.replace(BLOCK_BREAK, '\n');
|
|
102
|
+
text = text.replace(/<[^>]*>/g, '');
|
|
103
|
+
text = decodeEntities(text);
|
|
104
|
+
text = text.replace(/\r\n?/g, '\n');
|
|
105
|
+
// Includes U+00A0, which arrives constantly as and must collapse like a normal space.
|
|
106
|
+
text = text.replace(/[ \t\u00a0]+/g, ' ');
|
|
107
|
+
text = text.replace(/ *\n */g, '\n');
|
|
108
|
+
text = text.replace(/\n{3,}/g, '\n\n');
|
|
109
|
+
return text.trim();
|
|
110
|
+
}
|
|
111
|
+
const HINT_PATTERNS = [
|
|
112
|
+
{ id: 'gmail_quote', regex: /class="[^"]*\bgmail_quote\b/i },
|
|
113
|
+
{ id: 'gmail_quote_container', regex: /class="[^"]*\bgmail_quote_container\b/i },
|
|
114
|
+
{ id: 'gmail_attr', regex: /class="[^"]*\bgmail_attr\b/i },
|
|
115
|
+
{ id: 'gmail_signature', regex: /class="[^"]*\bgmail_signature\b/i },
|
|
116
|
+
{ id: 'owa_reply_separator', regex: /id="?divRplyFwdMsg/i },
|
|
117
|
+
{ id: 'owa_append_on_send', regex: /id="?appendonsend/i },
|
|
118
|
+
{ id: 'owa_element_to_proof', regex: /class="[^"]*elementToProof/i },
|
|
119
|
+
{ id: 'outlook_mso_normal', regex: /class="?[^"]*\bMsoNormal\b/i },
|
|
120
|
+
{ id: 'outlook_word_section', regex: /\bWordSection1\b/i },
|
|
121
|
+
{ id: 'outlook_nested_x_class', regex: /class="[^"]*\bx_[A-Za-z]/ },
|
|
122
|
+
{ id: 'apple_mail_cite', regex: /<blockquote[^>]+type="?cite/i },
|
|
123
|
+
{ id: 'apple_interchange_newline', regex: /Apple-interchange-newline/i },
|
|
124
|
+
{ id: 'apple_mail_signature', regex: /AppleMailSignature/i },
|
|
125
|
+
{ id: 'thunderbird_moz_cite', regex: /moz-cite-prefix/i },
|
|
126
|
+
{ id: 'thunderbird_signature', regex: /moz-signature/i },
|
|
127
|
+
{ id: 'proton_quote', regex: /protonmail_quote/i },
|
|
128
|
+
{ id: 'yahoo_quoted', regex: /yahoo_quoted/i },
|
|
129
|
+
{ id: 'zimbra_separator', regex: /id="?zwchr/i },
|
|
130
|
+
{ id: 'outlook_border_separator', regex: /border-top:\s*(solid|1px|1\.0pt)/i },
|
|
131
|
+
{ id: 'ms_outlook_mobile', regex: /ms-outlook-mobile|Outlook for (iOS|Android)/i }
|
|
132
|
+
];
|
|
133
|
+
/** Detect client structure hints without building a DOM. */
|
|
134
|
+
function detectHtmlHints(html) {
|
|
135
|
+
const head = html.length > 262144 ? html.slice(0, 262144) : html;
|
|
136
|
+
const ids = [];
|
|
137
|
+
for (const { id, regex } of HINT_PATTERNS) {
|
|
138
|
+
if (regex.test(head)) {
|
|
139
|
+
ids.push(id);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const blockquoteCount = (head.match(/<blockquote\b/gi) || []).length;
|
|
143
|
+
const tableCount = (head.match(/<table\b/gi) || []).length;
|
|
144
|
+
return {
|
|
145
|
+
ids,
|
|
146
|
+
hasBlockquote: blockquoteCount > 0,
|
|
147
|
+
blockquoteCount,
|
|
148
|
+
hasTable: tableCount > 0,
|
|
149
|
+
tableCount,
|
|
150
|
+
hasImage: /<img\b/i.test(head),
|
|
151
|
+
// A body element inside a table cell, or a closing tag with no opener nearby, is a decent
|
|
152
|
+
// proxy for markup that needs a forgiving parser.
|
|
153
|
+
suspectMalformed: /<\/(td|tr|table)>\s*<\/(html|body)>/i.test(head) || blockquoteCount !== (head.match(/<\/blockquote>/gi) || []).length
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=html.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html.js","sourceRoot":"","sources":["../../src/canonical/html.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage 2: plain text canonicalisation.
|
|
3
|
+
*
|
|
4
|
+
* Offsets in the rest of the pipeline are character offsets into the canonical string produced
|
|
5
|
+
* here, not byte offsets into the raw message. Traceability back to the file is via the owning MIME
|
|
6
|
+
* part's byte range. That split is deliberate: transfer decoding, charset conversion and line
|
|
7
|
+
* ending normalisation all change lengths, and maintaining a byte level offset map through three
|
|
8
|
+
* lossy transforms would be fragile for no practical gain.
|
|
9
|
+
*
|
|
10
|
+
* What this module does NOT do is unwrap hard wrapped lines. Unwrapping changes offsets and can
|
|
11
|
+
* merge a quoted line with a new one, which is exactly the mistake that loses content. Wrapping is
|
|
12
|
+
* recorded as a per line property and resolved later, in the matching projection only.
|
|
13
|
+
*/
|
|
14
|
+
/** A single canonical line with its quote depth stripped out. */
|
|
15
|
+
export interface TextLine {
|
|
16
|
+
/** Zero based index into the line array. */
|
|
17
|
+
index: number;
|
|
18
|
+
/** Character offset of the line start within the canonical text. */
|
|
19
|
+
start: number;
|
|
20
|
+
/** Character offset just past the line content, excluding the newline. */
|
|
21
|
+
end: number;
|
|
22
|
+
/** The line exactly as it appears in the canonical text, including any quote prefix. */
|
|
23
|
+
raw: string;
|
|
24
|
+
/** The line with its quote prefix removed. */
|
|
25
|
+
content: string;
|
|
26
|
+
/** Number of leading quote markers. Zero for unquoted lines. */
|
|
27
|
+
depth: number;
|
|
28
|
+
/** True when the line has no non whitespace content after prefix removal. */
|
|
29
|
+
blank: boolean;
|
|
30
|
+
/** True when this unquoted line was re-attached as the wrapped tail of the quote above it. */
|
|
31
|
+
wrapContinuation?: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface CanonicalText {
|
|
34
|
+
/** Normalised text. All offsets in the pipeline refer to this string. */
|
|
35
|
+
text: string;
|
|
36
|
+
lines: TextLine[];
|
|
37
|
+
}
|
|
38
|
+
export type ReplyStyle = 'none' | 'top' | 'bottom' | 'interleaved' | 'top-and-trailing' | 'quote-only';
|
|
39
|
+
export interface QuoteProfile {
|
|
40
|
+
maxDepth: number;
|
|
41
|
+
quotedLines: number;
|
|
42
|
+
totalLines: number;
|
|
43
|
+
/** Share of non blank lines that carry a quote prefix. */
|
|
44
|
+
quotedRatio: number;
|
|
45
|
+
style: ReplyStyle;
|
|
46
|
+
/** Non blank unquoted lines before the first quoted line. */
|
|
47
|
+
contentBefore: number;
|
|
48
|
+
/** Non blank unquoted lines between the first and last quoted line. */
|
|
49
|
+
contentBetween: number;
|
|
50
|
+
/** Non blank unquoted lines after the last quoted line. */
|
|
51
|
+
contentAfter: number;
|
|
52
|
+
}
|
|
53
|
+
/** Split a line into its quote depth and remaining content. */
|
|
54
|
+
export declare function splitQuotePrefix(line: string): {
|
|
55
|
+
depth: number;
|
|
56
|
+
content: string;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Normalise decoded part text into the canonical form used by every later stage.
|
|
60
|
+
*
|
|
61
|
+
* Line endings become LF, a BOM is dropped, NUL bytes and lone carriage returns are removed, and
|
|
62
|
+
* the result is NFC normalised so that visually identical text compares equal during history
|
|
63
|
+
* matching. Nothing else is altered: no trimming, no whitespace collapsing, no unwrapping.
|
|
64
|
+
*/
|
|
65
|
+
export declare function canonicaliseText(input: string): CanonicalText;
|
|
66
|
+
/**
|
|
67
|
+
* Describe how quoting is laid out in a plain text body.
|
|
68
|
+
*
|
|
69
|
+
* The style categories are positional only. They describe where unquoted content sits relative to
|
|
70
|
+
* quoted content and make no claim about intent, because a trailing block after a quote is just as
|
|
71
|
+
* likely to be a signature as a genuine bottom posted answer. Deciding that is the classifier's
|
|
72
|
+
* job, not this function's.
|
|
73
|
+
*/
|
|
74
|
+
export declare function profileQuoting(lines: readonly TextLine[]): QuoteProfile;
|
|
75
|
+
//# sourceMappingURL=text.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../src/canonical/text.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,iEAAiE;AACjE,MAAM,WAAW,QAAQ;IACrB,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,GAAG,EAAE,MAAM,CAAC;IACZ,wFAAwF;IACxF,GAAG,EAAE,MAAM,CAAC;IACZ,8CAA8C;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,KAAK,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,KAAK,EAAE,OAAO,CAAC;IACf,8FAA8F;IAC9F,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,MAAM,WAAW,aAAa;IAC1B,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,QAAQ,EAAE,CAAC;CACrB;AAED,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,aAAa,GAAG,kBAAkB,GAAG,YAAY,CAAC;AAEvG,MAAM,WAAW,YAAY;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,0DAA0D;IAC1D,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,UAAU,CAAC;IAClB,6DAA6D;IAC7D,aAAa,EAAE,MAAM,CAAC;IACtB,uEAAuE;IACvE,cAAc,EAAE,MAAM,CAAC;IACvB,2DAA2D;IAC3D,YAAY,EAAE,MAAM,CAAC;CACxB;AAYD,+DAA+D;AAC/D,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAajF;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,CAwC7D;AA+CD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,GAAG,YAAY,CA0EvE"}
|