@hraness/wordcell 0.26.3 → 0.27.0
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/README.md +36 -21
- package/dist/capture.js +15 -7
- package/dist/cli-bin.js +2 -2
- package/dist/cli.js +21 -19
- package/dist/clip/acquire.js +4 -3
- package/dist/clip/bundle-reader.js +3 -2
- package/dist/clip/cli.js +8 -7
- package/dist/clip/defuddle-worker.js +4 -1
- package/dist/clip/doctor.js +5 -4
- package/dist/clip/jobs.js +2 -1
- package/dist/clip/persist.js +2 -1
- package/dist/clip/refresh.js +4 -3
- package/dist/evaluation-builder-bin.js +2 -2
- package/dist/evaluation-builder.js +3 -3
- package/dist/evaluation-kb.js +3 -3
- package/dist/{index-mp5y3ty3.js → index-30v9w4cy.js} +36 -20
- package/dist/{index-9paxge84.js → index-3hts6yy9.js} +1 -1
- package/dist/{index-h9vyd03z.js → index-58azscg5.js} +7 -6
- package/dist/{index-9zkba8hr.js → index-7bqwzjba.js} +7 -6
- package/dist/{index-j4zgmzjr.js → index-8djdwjjd.js} +1 -1
- package/dist/{index-5h5awwh4.js → index-b40e2dkx.js} +2 -2
- package/dist/index-e2wstc7t.js +323 -0
- package/dist/{index-hacpnaew.js → index-fd8j527z.js} +1 -1
- package/dist/{index-mxxxytys.js → index-g63d3937.js} +4 -0
- package/dist/{index-k86wepd8.js → index-h2w7syy2.js} +5 -1
- package/dist/{index-9fywek7g.js → index-ht14xne3.js} +1 -1
- package/dist/{index-82r6g4py.js → index-k9yzc4vn.js} +1 -1
- package/dist/{index-m9p2a4mj.js → index-my50xsa3.js} +221 -36
- package/dist/{index-ncwzmsge.js → index-nwdzhj1h.js} +2 -2
- package/dist/{index-9rf81m0p.js → index-pbz3qw1s.js} +35 -20
- package/dist/index-pn14a2m5.js +59 -0
- package/dist/{index-nd8v7r0z.js → index-sp43012q.js} +1 -1
- package/dist/{index-8pabwzqg.js → index-t4hjm1kk.js} +2 -1
- package/dist/{index-npg9z1a4.js → index-xn5q4kne.js} +5 -1
- package/dist/{index-j2jt49gr.js → index-yatn62qq.js} +1 -1
- package/dist/index.js +14 -4
- package/dist/pdf.js +438 -4
- package/dist/portfolio.js +3 -3
- package/dist/rerank-clef.js +16 -0
- package/dist/rerank-typesafe.js +3 -1
- package/dist/sdk.js +2 -2
- package/dist/{support-9qdr4hb8.js → support-s059eytv.js} +20 -18
- package/dist/url-intelligence.js +4 -3
- package/dist/workflows/decision-context.js +3 -3
- package/dist/workflows/index.js +3 -3
- package/package.json +23 -9
- package/skills/wordcell/SKILL.md +1 -1
- package/skills/wordcell/references/query.md +11 -6
- package/src/capture.ts +5 -0
- package/src/cli-help.ts +2 -1
- package/src/cli-program.ts +23 -8
- package/src/clip/bundle-reader.ts +3 -0
- package/src/clip/capture.ts +1 -0
- package/src/clip/extract.ts +2 -0
- package/src/clip/lib.ts +3 -0
- package/src/clip/metadata-search-tool/Cargo.lock +6 -6
- package/src/clip/metadata.ts +232 -0
- package/src/clip/persist.ts +3 -0
- package/src/index.ts +2 -1
- package/src/pdf/bundle-reader.ts +457 -0
- package/src/pdf/capture.ts +6 -2
- package/src/pdf/cli.ts +2 -1
- package/src/pdf/errors.ts +11 -0
- package/src/pdf/extract.ts +16 -13
- package/src/pdf/layout.ts +136 -3
- package/src/pdf/markdown.ts +7 -1
- package/src/pdf/model.ts +9 -0
- package/src/pdf/persist.ts +8 -2
- package/src/pdf/source.ts +5 -4
- package/src/pdf/tools.ts +8 -6
- package/src/pdf.ts +10 -0
- package/src/rerank-clef.ts +52 -0
- package/src/rerank-credentials.ts +11 -2
- package/src/rerank-typesafe.ts +44 -20
- package/src/rerank.ts +1 -1
- package/src/sdk.ts +4 -2
- /package/dist/{index-cd75vky9.js → index-jbtyp6q5.js} +0 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { decodeHTML } from "entities";
|
|
2
|
+
|
|
3
|
+
export const metadataLimits = { htmlBytes: 8 * 1024 * 1024, claims: 256, value: 8192, totalBytes: 256 * 1024, jsonBytes: 128 * 1024, jsonNodes: 2048, jsonDepth: 12 } as const;
|
|
4
|
+
export type MetadataSource = "html-title" | "html-meta" | "html-link" | "html-language" | "json-ld" | "extractor";
|
|
5
|
+
export type MetadataValue = string | readonly string[];
|
|
6
|
+
export type ArticleMetadataClaim = {
|
|
7
|
+
readonly field: string;
|
|
8
|
+
readonly observed: MetadataValue;
|
|
9
|
+
readonly value: MetadataValue;
|
|
10
|
+
readonly source: MetadataSource;
|
|
11
|
+
readonly locator: string;
|
|
12
|
+
readonly rule: string;
|
|
13
|
+
readonly selected: boolean;
|
|
14
|
+
readonly datePrecision?: "date" | "datetime" | "invalid";
|
|
15
|
+
readonly timezone?: "explicit" | "unknown" | "not-applicable";
|
|
16
|
+
};
|
|
17
|
+
export type ArticleMetadata = {
|
|
18
|
+
readonly schemaVersion: 1;
|
|
19
|
+
readonly extractorVersion: "wordcell-metadata-v1";
|
|
20
|
+
readonly claims: readonly ArticleMetadataClaim[];
|
|
21
|
+
readonly warnings: readonly ("input-limit" | "claim-limit" | "value-limit" | "json-limit" | "json-invalid")[];
|
|
22
|
+
};
|
|
23
|
+
const record = (v: unknown): v is Record<string, unknown> => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
24
|
+
const sources = new Set<MetadataSource>(["html-title", "html-meta", "html-link", "html-language", "json-ld", "extractor"]);
|
|
25
|
+
const warningKinds = new Set(["input-limit", "claim-limit", "value-limit", "json-limit", "json-invalid"]);
|
|
26
|
+
const credentialName = /(?:csrf|xsrf|token|nonce|password|secret|session|authorization|api[-_]?key|signature)/i;
|
|
27
|
+
const urlFields = new Set(["canonical", "alternate", "image", "url"]);
|
|
28
|
+
const dateFields = new Set(["published", "modified"]);
|
|
29
|
+
|
|
30
|
+
/** Metadata is descriptive evidence, never an authenticated resource locator. */
|
|
31
|
+
function publicUrl(value: string, base?: string): string | null {
|
|
32
|
+
try {
|
|
33
|
+
const url = new URL(value, base);
|
|
34
|
+
if (!/^https?:$/.test(url.protocol)) return null;
|
|
35
|
+
url.username = ""; url.password = ""; url.search = ""; url.hash = "";
|
|
36
|
+
return url.href;
|
|
37
|
+
} catch { return null; }
|
|
38
|
+
}
|
|
39
|
+
function cleanText(value: string): string {
|
|
40
|
+
return value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/gu, "")
|
|
41
|
+
.replace(/https?:\/\/[^\s<>"']+/giu, (url) => publicUrl(url) ?? "[invalid URL]")
|
|
42
|
+
.replace(/\s+/gu, " ").trim();
|
|
43
|
+
}
|
|
44
|
+
function dateValue(value: string): Pick<ArticleMetadataClaim, "value" | "datePrecision" | "timezone"> {
|
|
45
|
+
const dateOnly = /^\d{4}-\d{2}-\d{2}$/.test(value);
|
|
46
|
+
const zoned = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:\d{2})$/i.test(value);
|
|
47
|
+
const local = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?$/.test(value);
|
|
48
|
+
const parsed = Date.parse(dateOnly ? value + "T00:00:00Z" : local ? value + "Z" : value);
|
|
49
|
+
const validDay = Number.isFinite(parsed) && new Date(Date.UTC(Number(value.slice(0, 4)), Number(value.slice(5, 7)) - 1, Number(value.slice(8, 10)))).toISOString().slice(0, 10) === value.slice(0, 10);
|
|
50
|
+
if ((!dateOnly && !zoned && !local) || !validDay) return { value, datePrecision: "invalid", timezone: "unknown" };
|
|
51
|
+
if (dateOnly) return { value, datePrecision: "date", timezone: "not-applicable" };
|
|
52
|
+
return { value: zoned ? new Date(parsed).toISOString() : value, datePrecision: "datetime", timezone: zoned ? "explicit" : "unknown" };
|
|
53
|
+
}
|
|
54
|
+
function rank(claim: ArticleMetadataClaim): number {
|
|
55
|
+
if (claim.datePrecision === "invalid") return 1000;
|
|
56
|
+
if (claim.source === "json-ld") return claim.rule.includes("jsonld-type=WebPage") ? 15 : 10;
|
|
57
|
+
if (claim.source === "html-link") return 5;
|
|
58
|
+
if (claim.locator.includes("property=article:")) return 20;
|
|
59
|
+
if (claim.locator.includes("property=og:")) return 30;
|
|
60
|
+
if (claim.locator.includes("name=twitter:")) return 40;
|
|
61
|
+
if (claim.source === "html-meta") return 50;
|
|
62
|
+
if (claim.source === "html-title" || claim.source === "html-language") return 60;
|
|
63
|
+
return 90;
|
|
64
|
+
}
|
|
65
|
+
function select(claims: readonly ArticleMetadataClaim[]): ArticleMetadataClaim[] {
|
|
66
|
+
const ranks = new Map<string, number>();
|
|
67
|
+
for (const claim of claims) ranks.set(claim.field, Math.min(ranks.get(claim.field) ?? 1000, rank(claim)));
|
|
68
|
+
const chosen = new Set<string>();
|
|
69
|
+
return claims.map((claim) => {
|
|
70
|
+
const multiple = ["authors", "tags", "alternate"].includes(claim.field);
|
|
71
|
+
const selected = rank(claim) < 1000 && rank(claim) === ranks.get(claim.field) && (multiple || !chosen.has(claim.field));
|
|
72
|
+
if (selected) chosen.add(claim.field);
|
|
73
|
+
return { ...claim, selected, rule: `metadata-precedence-v1:${claim.rule}` };
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Validate an optional additive envelope from a worker or a stored bundle. */
|
|
78
|
+
export function parseArticleMetadata(input: unknown, redact: (text: string) => string = cleanText): ArticleMetadata {
|
|
79
|
+
if (!record(input) || input.schemaVersion !== 1 || input.extractorVersion !== "wordcell-metadata-v1" || !Array.isArray(input.claims) || input.claims.length > metadataLimits.claims || !Array.isArray(input.warnings)) throw new Error("Unsupported or malformed article metadata envelope");
|
|
80
|
+
let bytes = 0;
|
|
81
|
+
const text = (value: unknown, limit: number = metadataLimits.value): string => {
|
|
82
|
+
if (typeof value !== "string" || value.length > limit) throw new Error("Invalid article metadata text");
|
|
83
|
+
const result = cleanText(redact(value));
|
|
84
|
+
bytes += Buffer.byteLength(result);
|
|
85
|
+
if (bytes > metadataLimits.totalBytes) throw new Error("Article metadata exceeds byte limit");
|
|
86
|
+
return result;
|
|
87
|
+
};
|
|
88
|
+
const value = (v: unknown): MetadataValue => Array.isArray(v)
|
|
89
|
+
? v.length <= 32 ? v.map((part) => text(part)) : (() => { throw new Error("Article metadata list exceeds limit"); })()
|
|
90
|
+
: text(v);
|
|
91
|
+
const claims = input.claims.map((raw): ArticleMetadataClaim => {
|
|
92
|
+
if (!record(raw) || !sources.has(raw.source as MetadataSource) || typeof raw.selected !== "boolean") throw new Error("Invalid article metadata claim");
|
|
93
|
+
const field = text(raw.field, 128);
|
|
94
|
+
if (!/^[a-z][a-z0-9:_-]{0,127}$/.test(field) || credentialName.test(field)) throw new Error("Invalid article metadata field");
|
|
95
|
+
let observed = value(raw.observed), normalized = value(raw.value);
|
|
96
|
+
if (urlFields.has(field)) {
|
|
97
|
+
const urls = (input: MetadataValue): MetadataValue => {
|
|
98
|
+
const normalize = (part: string): string => {
|
|
99
|
+
const parsed = publicUrl(part);
|
|
100
|
+
if (parsed === null) throw new Error("Invalid article metadata URL");
|
|
101
|
+
return parsed;
|
|
102
|
+
};
|
|
103
|
+
return typeof input === "string" ? normalize(input) : input.map(normalize);
|
|
104
|
+
};
|
|
105
|
+
observed = urls(observed); normalized = urls(normalized);
|
|
106
|
+
}
|
|
107
|
+
const temporal = raw.datePrecision === undefined ? {} : dateFields.has(field) && typeof observed === "string"
|
|
108
|
+
? dateValue(observed) : (() => { throw new Error("Invalid article metadata date"); })();
|
|
109
|
+
if (raw.datePrecision !== undefined && (raw.datePrecision !== (temporal as ArticleMetadataClaim).datePrecision || raw.timezone !== (temporal as ArticleMetadataClaim).timezone)) throw new Error("Invalid article metadata date precision");
|
|
110
|
+
return { field, observed, value: normalized, source: raw.source as MetadataSource, locator: text(raw.locator, 512), rule: text(raw.rule, 256), selected: raw.selected, ...temporal };
|
|
111
|
+
});
|
|
112
|
+
if (input.warnings.length > warningKinds.size || input.warnings.some((warning) => !warningKinds.has(warning))) throw new Error("Invalid article metadata warnings");
|
|
113
|
+
return { schemaVersion: 1, extractorVersion: "wordcell-metadata-v1", claims, warnings: [...input.warnings] as ArticleMetadata["warnings"] };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const fields: Readonly<Record<string, string>> = {
|
|
117
|
+
"og:title": "title", "twitter:title": "title", title: "title", "citation_title": "title", "dc.title": "title",
|
|
118
|
+
description: "description", "og:description": "description", "twitter:description": "description", "dc.description": "description",
|
|
119
|
+
author: "authors", "article:author": "authors", "citation_author": "authors", "dc.creator": "authors",
|
|
120
|
+
"article:published_time": "published", "datepublished": "published", "citation_publication_date": "published", "dc.date": "published",
|
|
121
|
+
"article:modified_time": "modified", "datemodified": "modified", "last-modified": "modified",
|
|
122
|
+
"og:url": "canonical", "twitter:url": "canonical", "og:image": "image", "og:image:url": "image", "twitter:image": "image", "twitter:image:src": "image",
|
|
123
|
+
"og:site_name": "site", publisher: "publisher", "dc.publisher": "publisher", "citation_journal_title": "publisher",
|
|
124
|
+
"og:locale": "language", language: "language", "dc.language": "language", "og:type": "type", "twitter:card": "card",
|
|
125
|
+
"article:tag": "tags", keywords: "tags", "news_keywords": "tags", "citation_keywords": "tags",
|
|
126
|
+
"citation_doi": "doi", "dc.identifier": "identifier", "citation_pdf_url": "alternate",
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/** Parse inert HTML locally. JSON-LD contexts are evidence strings and are never resolved. */
|
|
130
|
+
export async function extractArticleMetadata(html: string, base: string, fallback: { readonly title?: unknown; readonly author?: unknown; readonly published?: unknown; readonly description?: unknown } = {}): Promise<ArticleMetadata> {
|
|
131
|
+
const claims: ArticleMetadataClaim[] = [];
|
|
132
|
+
const warnings = new Set<ArticleMetadata["warnings"][number]>();
|
|
133
|
+
let totalBytes = 0, title = "", script = "", scriptIndex = 0, activeScript = false, jsonBytes = 0;
|
|
134
|
+
const add = (field: string, raw: unknown, source: MetadataSource, locator: string, decode = false, evidenceRule?: string): void => {
|
|
135
|
+
if (claims.length >= metadataLimits.claims) { warnings.add("claim-limit"); return; }
|
|
136
|
+
let values = Array.isArray(raw) ? raw : [raw];
|
|
137
|
+
if (values.length > 32) { warnings.add("value-limit"); values = values.slice(0, 32); }
|
|
138
|
+
const strings: string[] = [];
|
|
139
|
+
for (const value of values) {
|
|
140
|
+
if (typeof value !== "string") continue;
|
|
141
|
+
if (value.length > metadataLimits.value) { warnings.add("value-limit"); continue; }
|
|
142
|
+
const decoded = decode ? decodeHTML(value) : value;
|
|
143
|
+
const cleaned = cleanText(urlFields.has(field) ? publicUrl(decoded, base) ?? "" : decoded);
|
|
144
|
+
if (cleaned) strings.push(cleaned);
|
|
145
|
+
}
|
|
146
|
+
if (!strings.length) return;
|
|
147
|
+
const observed: MetadataValue = Array.isArray(raw) ? strings : strings[0]!;
|
|
148
|
+
let value: MetadataValue = observed;
|
|
149
|
+
if (urlFields.has(field)) {
|
|
150
|
+
const urls = strings.map((part) => publicUrl(part, base)).filter((part): part is string => part !== null);
|
|
151
|
+
if (!urls.length) return;
|
|
152
|
+
value = Array.isArray(raw) ? urls : urls[0]!;
|
|
153
|
+
}
|
|
154
|
+
const temporal = dateFields.has(field) && typeof observed === "string" ? dateValue(observed) : {};
|
|
155
|
+
const claim: ArticleMetadataClaim = { field, observed, value, source, locator, rule: `trim-whitespace-url-query-redaction${evidenceRule === undefined ? "" : `:${evidenceRule}`}`, selected: false, ...temporal };
|
|
156
|
+
const bytes = Buffer.byteLength(JSON.stringify(claim));
|
|
157
|
+
if (totalBytes + bytes > metadataLimits.totalBytes / 2) { warnings.add("claim-limit"); return; }
|
|
158
|
+
totalBytes += bytes; claims.push(claim);
|
|
159
|
+
};
|
|
160
|
+
const parseLd = (raw: string, index: number): void => {
|
|
161
|
+
if (!raw.trim()) return;
|
|
162
|
+
if (Buffer.byteLength(raw) > metadataLimits.jsonBytes || (jsonBytes += Buffer.byteLength(raw)) > metadataLimits.jsonBytes * 4) { warnings.add("json-limit"); return; }
|
|
163
|
+
let parsed: unknown;
|
|
164
|
+
try { parsed = JSON.parse(raw); } catch { warnings.add("json-invalid"); return; }
|
|
165
|
+
let nodes = 0;
|
|
166
|
+
// A full bounded walk rejects deep or wide hostile payloads before extracting facts.
|
|
167
|
+
const stack: { value: unknown; depth: number }[] = [{ value: parsed, depth: 0 }];
|
|
168
|
+
while (stack.length) {
|
|
169
|
+
const node = stack.pop()!;
|
|
170
|
+
if (++nodes > metadataLimits.jsonNodes || node.depth > metadataLimits.jsonDepth) { warnings.add("json-limit"); return; }
|
|
171
|
+
const children = Array.isArray(node.value) ? node.value : record(node.value) ? Object.values(node.value) : [];
|
|
172
|
+
if (children.length + nodes + stack.length > metadataLimits.jsonNodes) { warnings.add("json-limit"); return; }
|
|
173
|
+
for (const child of children) stack.push({ value: child, depth: node.depth + 1 });
|
|
174
|
+
}
|
|
175
|
+
const items: { value: unknown; path: string }[] = Array.isArray(parsed) ? parsed.map((value, i) => ({ value, path: `$[${i}]` })) : [{ value: parsed, path: "$" }];
|
|
176
|
+
for (let cursor = 0; cursor < items.length && cursor < metadataLimits.jsonNodes; cursor++) {
|
|
177
|
+
const item = items[cursor]!;
|
|
178
|
+
if (!record(item.value)) continue;
|
|
179
|
+
const object = item.value;
|
|
180
|
+
if (Array.isArray(object["@graph"])) items.push(...object["@graph"].map((value, i) => ({ value, path: `${item.path}.@graph[${i}]` })));
|
|
181
|
+
const types = Array.isArray(object["@type"]) ? object["@type"] : [object["@type"]];
|
|
182
|
+
const entityTypes = types.flatMap((type) => {
|
|
183
|
+
const match = typeof type === "string" ? /^(?:(?:https?:\/\/)?schema\.org\/)?(Article|NewsArticle|BlogPosting|ScholarlyArticle|TechArticle|Report|WebPage|VideoObject|AudioObject|PodcastEpisode)$/.exec(type) : null;
|
|
184
|
+
return match?.[1] === undefined ? [] : [match[1]];
|
|
185
|
+
});
|
|
186
|
+
const entityType = entityTypes.find(type => type !== "WebPage") ?? entityTypes[0];
|
|
187
|
+
if (entityType === undefined) continue;
|
|
188
|
+
const names = (v: unknown): unknown => Array.isArray(v) ? v.slice(0, 32).map((entry) => record(entry) ? entry.name : entry) : record(v) ? v.name : v;
|
|
189
|
+
const props: Readonly<Record<string, string>> = { headline: "title", name: "title", description: "description", author: "authors", creator: "authors", datePublished: "published", dateModified: "modified", inLanguage: "language", publisher: "publisher", keywords: "tags", image: "image", thumbnailUrl: "image", url: "canonical", mainEntityOfPage: "canonical", duration: "duration", episodeNumber: "episode", identifier: "identifier" };
|
|
190
|
+
for (const [key, field] of Object.entries(props)) {
|
|
191
|
+
let value = object[key];
|
|
192
|
+
if (value === undefined) continue;
|
|
193
|
+
if (["author", "creator", "publisher"].includes(key)) value = names(value);
|
|
194
|
+
if (key === "image") value = Array.isArray(value) ? value.map((v) => record(v) ? v.url ?? v.contentUrl : v) : record(value) ? value.url ?? value.contentUrl : value;
|
|
195
|
+
if (key === "mainEntityOfPage" && record(value)) value = value["@id"] ?? value.url;
|
|
196
|
+
if (key === "episodeNumber" && typeof value === "number" && Number.isFinite(value)) value = String(value);
|
|
197
|
+
add(field, value, "json-ld", `script[${index}]${item.path}.${key}`, false, `jsonld-type=${entityType}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
let input = html;
|
|
202
|
+
if (Buffer.byteLength(input) > metadataLimits.htmlBytes) { input = Buffer.from(input).subarray(0, metadataLimits.htmlBytes).toString("utf8"); warnings.add("input-limit"); }
|
|
203
|
+
await new HTMLRewriter()
|
|
204
|
+
.on("html", { element(element) { add("language", element.getAttribute("lang"), "html-language", "html@lang", true); } })
|
|
205
|
+
.on("title", { text(chunk) { if (title.length <= metadataLimits.value) title += chunk.text; } })
|
|
206
|
+
.on("meta", { element(element) {
|
|
207
|
+
const attribute = element.getAttribute("property") ? "property" : "name";
|
|
208
|
+
const key = decodeHTML(element.getAttribute(attribute) ?? "").toLowerCase();
|
|
209
|
+
if (!/^[a-z][a-z0-9:._-]{0,100}$/.test(key) || credentialName.test(key)) return;
|
|
210
|
+
const field = Object.hasOwn(fields, key) ? fields[key]! : `meta:${key.replaceAll(".", "_")}`;
|
|
211
|
+
add(field, element.getAttribute("content"), "html-meta", `meta[${attribute}=${key}]@content`, true);
|
|
212
|
+
} })
|
|
213
|
+
.on("link", { element(element) {
|
|
214
|
+
const rel = (element.getAttribute("rel") ?? "").toLowerCase().split(/\s+/);
|
|
215
|
+
const field = rel.includes("canonical") ? "canonical" : rel.includes("alternate") ? "alternate" : null;
|
|
216
|
+
if (field) add(field, element.getAttribute("href"), "html-link", `link[rel=${field}]@href`, true);
|
|
217
|
+
} })
|
|
218
|
+
.on("script", { element(element) {
|
|
219
|
+
activeScript = (element.getAttribute("type") ?? "").toLowerCase().trim() === "application/ld+json";
|
|
220
|
+
script = "";
|
|
221
|
+
if (activeScript) {
|
|
222
|
+
const index = scriptIndex++;
|
|
223
|
+
if (index >= 64) { activeScript = false; warnings.add("json-limit"); return; }
|
|
224
|
+
element.onEndTag(() => { parseLd(script, index); activeScript = false; });
|
|
225
|
+
}
|
|
226
|
+
}, text(chunk) { if (activeScript && script.length <= metadataLimits.jsonBytes) script += chunk.text; } })
|
|
227
|
+
.transform(new Response(input)).text();
|
|
228
|
+
if (activeScript) warnings.add("json-invalid");
|
|
229
|
+
add("title", title, "html-title", "title", true);
|
|
230
|
+
for (const [key, field] of Object.entries({ title: "title", author: "authors", published: "published", description: "description" })) add(field, fallback[key as keyof typeof fallback], "extractor", `defuddle.${key}`);
|
|
231
|
+
return parseArticleMetadata({ schemaVersion: 1, extractorVersion: "wordcell-metadata-v1", claims: select(claims), warnings: [...warnings] });
|
|
232
|
+
}
|
package/src/clip/persist.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseArticleMetadata, type ArticleMetadata } from "./metadata.js";
|
|
1
2
|
import {
|
|
2
3
|
chmodSync,
|
|
3
4
|
existsSync,
|
|
@@ -48,6 +49,7 @@ export type VideoContextManifestMetadata = {
|
|
|
48
49
|
};
|
|
49
50
|
|
|
50
51
|
export type CaptureManifestInput = {
|
|
52
|
+
readonly metadata?: ArticleMetadata;
|
|
51
53
|
readonly sourceUrl: string;
|
|
52
54
|
readonly canonicalUrl: string;
|
|
53
55
|
readonly capturedAt: string;
|
|
@@ -1248,6 +1250,7 @@ function normalizeManifest(
|
|
|
1248
1250
|
return {
|
|
1249
1251
|
schemaVersion: CAPTURE_MANIFEST_SCHEMA_VERSION,
|
|
1250
1252
|
document,
|
|
1253
|
+
...(input.metadata === undefined ? {} : { metadata: parseArticleMetadata(input.metadata, redactSensitiveText) }),
|
|
1251
1254
|
sourceUrl: sanitizeArtifactUrl(input.sourceUrl),
|
|
1252
1255
|
canonicalUrl: sanitizeArtifactUrl(input.canonicalUrl),
|
|
1253
1256
|
capturedAt: new Date(capturedAtTime).toISOString(),
|
package/src/index.ts
CHANGED
|
@@ -15,7 +15,8 @@ export * from "./publish.js";
|
|
|
15
15
|
export * from "./publish-model.js";
|
|
16
16
|
export * from "./query.js";
|
|
17
17
|
export * from "./rerank.js";
|
|
18
|
-
export
|
|
18
|
+
export { createTypeSafeReranker, DEFAULT_SYSTEMONE_ENDPOINT, DEFAULT_SYSTEMONE_MODEL, type TypeSafeRerankerOptions, type SystemOneTransport } from "./rerank-typesafe.js";
|
|
19
|
+
export * from "./rerank-clef.js";
|
|
19
20
|
export * from "./repository-memory.js";
|
|
20
21
|
export * from "./search.js";
|
|
21
22
|
export * from "./semantic.js";
|