@relvehq/agent-ready 0.4.2 → 0.4.3

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.
@@ -0,0 +1,232 @@
1
+ "use strict";
2
+ // FAQ: the page's FAQPage JSON-LD answers, and the FAQ section of a Markdown
3
+ // document (changes/agent-ready-quality).
4
+ //
5
+ // convert fills answers the HTML lacks (accordions that render closed
6
+ // panels empty) under the page's own FAQ heading
7
+ // refine swaps the page's questions and answers into a rewrite, word for
8
+ // word, so a model never rewords them
9
+ //
10
+ // A document without an FAQ heading never gets one from convert: the page
11
+ // shows no FAQ (owner, 2026-11-03). EDGE-SAFE (pure).
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.faqPairs = faqPairs;
14
+ exports.answerHasHtml = answerHasHtml;
15
+ exports.pageShowsFaq = pageShowsFaq;
16
+ exports.hasFaqSection = hasFaqSection;
17
+ exports.fillFaqAnswers = fillFaqAnswers;
18
+ exports.replaceFaqSection = replaceFaqSection;
19
+ exports.addFaqSection = addFaqSection;
20
+ exports.missingFaqAnswers = missingFaqAnswers;
21
+ exports.plainAnswer = plainAnswer;
22
+ exports.plainText = plainText;
23
+ /** Every Question / acceptedAnswer pair of the page's FAQPage blocks, in page order, deduped by question. */
24
+ function faqPairs(jsonLd) {
25
+ const pairs = [];
26
+ const seen = new Set();
27
+ const visit = (node) => {
28
+ if (Array.isArray(node))
29
+ return node.forEach(visit);
30
+ if (!node || typeof node !== "object")
31
+ return;
32
+ const o = node;
33
+ if (Array.isArray(o["@graph"]))
34
+ visit(o["@graph"]);
35
+ const types = [].concat(o["@type"] ?? []).map(String);
36
+ if (!types.includes("FAQPage"))
37
+ return;
38
+ for (const q of [].concat(o.mainEntity ?? [])) {
39
+ if (!q || typeof q !== "object")
40
+ continue;
41
+ const qo = q;
42
+ const question = plainText(String(qo.name ?? qo.text ?? ""));
43
+ const accepted = [].concat(qo.acceptedAnswer ?? [])[0];
44
+ const answer = accepted && typeof accepted === "object" ? String(accepted.text ?? "").trim() : "";
45
+ const answerText = plainText(answer);
46
+ if (!question || !answerText || seen.has(question.toLowerCase()))
47
+ continue;
48
+ seen.add(question.toLowerCase());
49
+ pairs.push({ question, answer, answerText });
50
+ }
51
+ };
52
+ jsonLd.forEach(visit);
53
+ return pairs;
54
+ }
55
+ /** True when the answer holds HTML markup rather than plain text. */
56
+ function answerHasHtml(pair) {
57
+ return /<[a-z][^>]*>/i.test(pair.answer);
58
+ }
59
+ const FAQ_TITLE = /\b(?:faqs?|frequently asked questions?)\b/i;
60
+ /** Does the page's text (one line per block) show an FAQ heading? */
61
+ function pageShowsFaq(text) {
62
+ return text.split("\n").some((line) => line.trim().length <= 80 && FAQ_TITLE.test(line));
63
+ }
64
+ function findSection(lines) {
65
+ let fenced = false;
66
+ let found = null;
67
+ // Front matter is YAML: a "# comment" there is not a heading.
68
+ let start = 0;
69
+ if (lines[0] === "---") {
70
+ const close = lines.indexOf("---", 1);
71
+ if (close > 0)
72
+ start = close + 1;
73
+ }
74
+ for (let i = start; i < lines.length; i++) {
75
+ if (/^\s*(```|~~~)/.test(lines[i]))
76
+ fenced = !fenced;
77
+ if (fenced)
78
+ continue;
79
+ const h = /^(#{1,6})\s+(.+?)\s*#*\s*$/.exec(lines[i]);
80
+ if (!h)
81
+ continue;
82
+ const level = h[1].length;
83
+ if (found) {
84
+ if (level <= found.level) {
85
+ found.end = i;
86
+ return found;
87
+ }
88
+ continue;
89
+ }
90
+ if (FAQ_TITLE.test(h[2]))
91
+ found = { heading: i, level, end: lines.length };
92
+ }
93
+ return found;
94
+ }
95
+ /** Does the Markdown have an FAQ heading? */
96
+ function hasFaqSection(markdown) {
97
+ return findSection(splitLines(markdown)) !== null;
98
+ }
99
+ /**
100
+ * Convert: write each answer the FAQ section lacks, word for word, under its
101
+ * question line; a question with no line is appended at the end of the
102
+ * section as a heading one level down. No FAQ heading: returned unchanged.
103
+ */
104
+ function fillFaqAnswers(markdown, pairs, render) {
105
+ const lines = splitLines(markdown);
106
+ const section = findSection(lines);
107
+ if (!section || !pairs.length)
108
+ return markdown;
109
+ const appended = [];
110
+ for (const pair of pairs) {
111
+ const body = lines.slice(section.heading + 1, section.end).join("\n");
112
+ if (contains(body, pair.answerText))
113
+ continue;
114
+ const at = lines.findIndex((l, i) => i > section.heading && i < section.end && sameQuestion(l, pair.question));
115
+ if (at === -1) {
116
+ appended.push(pair);
117
+ continue;
118
+ }
119
+ const add = ["", render(pair)];
120
+ lines.splice(at + 1, 0, ...add);
121
+ section.end += add.length;
122
+ }
123
+ if (appended.length) {
124
+ const sub = "#".repeat(Math.min(section.level + 1, 6));
125
+ const add = appended.flatMap((p) => ["", `${sub} ${p.question}`, "", render(p)]);
126
+ lines.splice(section.end, 0, ...add, "");
127
+ }
128
+ return tidy(lines.join("\n"));
129
+ }
130
+ /**
131
+ * Refine: the FAQ section holds exactly the page's questions and answers, in
132
+ * the page's order. Null when the document has no FAQ section.
133
+ */
134
+ function replaceFaqSection(markdown, pairs) {
135
+ const lines = splitLines(markdown);
136
+ const section = findSection(lines);
137
+ if (!section)
138
+ return null;
139
+ const sub = "#".repeat(Math.min(section.level + 1, 6));
140
+ lines.splice(section.heading + 1, section.end - section.heading - 1, "", ...renderPairs(pairs, sub), "");
141
+ return tidy(lines.join("\n"));
142
+ }
143
+ /**
144
+ * Refine: add a "## FAQ" section with the page's questions and answers, before
145
+ * a closing "## About…" / "## Source…" section when the document has one,
146
+ * else at the end.
147
+ */
148
+ function addFaqSection(markdown, pairs) {
149
+ const lines = splitLines(markdown);
150
+ let at = lines.length;
151
+ for (let i = lines.length - 1; i >= 0; i--) {
152
+ const h = /^##\s+(.+)$/.exec(lines[i]);
153
+ if (!h)
154
+ continue;
155
+ if (/^(about|source)/i.test(h[1].trim()))
156
+ at = i;
157
+ break;
158
+ }
159
+ lines.splice(at, 0, "", "## FAQ", "", ...renderPairs(pairs, "###"), "");
160
+ return tidy(lines.join("\n"));
161
+ }
162
+ /** Pairs whose answer is not in the FAQ section word for word; null when there is no section. */
163
+ function missingFaqAnswers(markdown, pairs) {
164
+ const lines = splitLines(markdown);
165
+ const section = findSection(lines);
166
+ if (!section)
167
+ return null;
168
+ const body = lines.slice(section.heading + 1, section.end).join("\n");
169
+ return pairs.filter((p) => !contains(body, p.answerText));
170
+ }
171
+ /** A plain answer as a Markdown paragraph: a leading "#", ">", "- " or "1." can't start a block. */
172
+ function plainAnswer(text) {
173
+ return text.replace(/^(#{1,6}\s|>|[-+*]\s)/, "\\$1").replace(/^(\d+)\.(\s)/, "$1\\.$2");
174
+ }
175
+ function renderPairs(pairs, sub) {
176
+ return pairs.flatMap((p, i) => [...(i ? [""] : []), `${sub} ${p.question}`, "", plainAnswer(p.answerText)]);
177
+ }
178
+ // ── text ────────────────────────────────────────────────────────────────
179
+ /** HTML or text -> one line of plain text. */
180
+ function plainText(s) {
181
+ return decodeEntities(s.replace(/<br\s*\/?>|<\/(?:p|li|div|h[1-6])>/gi, " ").replace(/<[^>]+>/g, ""))
182
+ .replace(/\s+/g, " ")
183
+ .trim();
184
+ }
185
+ function decodeEntities(s) {
186
+ return s.replace(/&(#x[0-9a-f]+|#\d+|amp|lt|gt|quot|apos|nbsp|#39);/gi, (m, e) => {
187
+ const k = e.toLowerCase();
188
+ if (k === "amp")
189
+ return "&";
190
+ if (k === "lt")
191
+ return "<";
192
+ if (k === "gt")
193
+ return ">";
194
+ if (k === "quot")
195
+ return '"';
196
+ if (k === "apos" || k === "#39")
197
+ return "'";
198
+ if (k === "nbsp")
199
+ return " ";
200
+ const code = k.startsWith("#x") ? parseInt(k.slice(2), 16) : parseInt(k.slice(1), 10);
201
+ return Number.isFinite(code) && code > 0 && code < 0x110000 ? String.fromCodePoint(code) : m;
202
+ });
203
+ }
204
+ /** Markdown -> comparable text: link text kept, markup and all whitespace dropped. */
205
+ function squash(md) {
206
+ return md
207
+ .normalize("NFKC")
208
+ .replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1")
209
+ .replace(/^\s*(?:[-+*]|\d+\.)\s+/gm, "")
210
+ .replace(/\\([\\`*_{}[\]()#+\-.!|>])/g, "$1")
211
+ .replace(/[*_`#>|]/g, "")
212
+ .replace(/[‘’]/g, "'")
213
+ .replace(/[“”]/g, '"')
214
+ .replace(/[–—]/g, "-")
215
+ .replace(/\s+/g, "")
216
+ .toLowerCase();
217
+ }
218
+ function contains(body, answerText) {
219
+ const needle = squash(answerText);
220
+ return needle.length > 0 && squash(body).includes(needle);
221
+ }
222
+ function sameQuestion(line, question) {
223
+ const q = (s) => squash(s).replace(/[?.:!]+$/, "");
224
+ const l = q(line);
225
+ return l.length > 0 && l === q(question);
226
+ }
227
+ function splitLines(markdown) {
228
+ return markdown.replace(/\r\n?/g, "\n").split("\n");
229
+ }
230
+ function tidy(md) {
231
+ return md.replace(/\n{3,}/g, "\n\n").trim();
232
+ }
@@ -50,7 +50,9 @@ exports.resolveMarkdown = resolveMarkdown;
50
50
  exports.defaultFetchOrigin = defaultFetchOrigin;
51
51
  exports.matchPattern = matchPattern;
52
52
  const config_1 = require("./config");
53
- const MAX_HTML_BYTES = 5 * 1024 * 1024;
53
+ // const MAX_HTML_BYTES = 5 * 1024 * 1024;
54
+ // Now cfg.maxHtmlBytes (default 5 MB, core/config.ts), so a site with one very
55
+ // large page can raise it.
54
56
  async function resolveMarkdown(cfg, input, deps = {}) {
55
57
  const { path, search, request } = input;
56
58
  const pageUrl = cfg.site + (path === "/" ? "/" : path) + search;
@@ -107,11 +109,11 @@ async function resolveMarkdown(cfg, input, deps = {}) {
107
109
  }
108
110
  const contentType = res.headers.get("content-type") ?? "";
109
111
  const declared = Number(res.headers.get("content-length") ?? "0");
110
- if (declared > MAX_HTML_BYTES)
112
+ if (declared > cfg.maxHtmlBytes)
111
113
  return { ...miss, originStatus: res.status };
112
114
  const body = await res.text().catch(() => "");
113
115
  const html = { status: res.status, contentType: contentType || "text/html; charset=utf-8", body };
114
- if (!res.ok || !/text\/html|application\/xhtml/i.test(contentType) || body.length > MAX_HTML_BYTES) {
116
+ if (!res.ok || !/text\/html|application\/xhtml/i.test(contentType) || body.length > cfg.maxHtmlBytes) {
115
117
  return { ...miss, html, originStatus: res.status };
116
118
  }
117
119
  const convert = deps.convert ?? (await Promise.resolve().then(() => __importStar(require("./convert")))).htmlToMarkdown;
@@ -1 +1 @@
1
- export declare const PACKAGE_VERSION = "0.4.2";
1
+ export declare const PACKAGE_VERSION = "0.4.3";
@@ -5,4 +5,4 @@
5
5
  // EDGE-SAFE.
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.PACKAGE_VERSION = void 0;
8
- exports.PACKAGE_VERSION = "0.4.2";
8
+ exports.PACKAGE_VERSION = "0.4.3";
@@ -1,6 +1,6 @@
1
1
  import type { PageInput } from "./types";
2
2
  import type { PromptSettings } from "./prompts";
3
- export type IssueKind = "number" | "url" | "frontmatter" | "hash" | "section" | "h1" | "html" | "placeholder" | "tokens" | "empty" | "plan";
3
+ export type IssueKind = "number" | "url" | "frontmatter" | "hash" | "section" | "h1" | "html" | "placeholder" | "tokens" | "empty" | "plan" | "description" | "faq";
4
4
  export interface CheckIssue {
5
5
  kind: IssueKind;
6
6
  detail: string;
@@ -17,14 +17,20 @@
17
17
  // prices of other products shown on the page; 2026-10-29)
18
18
  // tokens over the prompt's token budget
19
19
  // empty no real body
20
+ // description no `description` in the front matter
21
+ // faq the page shows an FAQ with FAQPage answers, and the document
22
+ // lacks the section or an answer word for word (refine swaps
23
+ // the page's in before checking; this guards other callers)
20
24
  //
21
- // Frontmatter values are not number-checked: they hold the hash, timestamps
22
- // and dates reformatted as YYYY-MM-DD by design.
25
+ // Frontmatter values are not number-checked (they hold the hash, timestamps
26
+ // and dates reformatted as YYYY-MM-DD by design), except the description,
27
+ // which is prose: its numbers and URLs are checked like the body's.
23
28
  // EDGE-SAFE (pure). It cannot catch a reworded claim; people still read.
24
29
  Object.defineProperty(exports, "__esModule", { value: true });
25
30
  exports.checkDoc = checkDoc;
26
31
  exports.planNames = planNames;
27
32
  const enrich_1 = require("../core/enrich");
33
+ const faq_1 = require("../core/faq");
28
34
  const URL_RE = /https?:\/\/[^\s<>()[\]|"'`]+/g;
29
35
  const NUM_RE = /\d+(?:[.,]\d+)*/g;
30
36
  const HTML_RE = /<\/?(?:a|b|br|div|em|h[1-6]|i|img|li|ol|p|script|span|strong|style|table|td|th|tr|ul)\b[^>]*>/i;
@@ -36,6 +42,7 @@ function checkDoc(doc, input, settings, opts = {}) {
36
42
  // Frontmatter.
37
43
  const fm = /^---\n([\s\S]*?)\n---\n?/.exec(md);
38
44
  const body = fm ? md.slice(fm[0].length) : md;
45
+ let description = "";
39
46
  if (!fm) {
40
47
  add("frontmatter", "no frontmatter block");
41
48
  }
@@ -51,6 +58,9 @@ function checkDoc(doc, input, settings, opts = {}) {
51
58
  add("frontmatter", `canonical ${meta.canonical} is not ${input.url}`);
52
59
  if (meta.source_hash !== input.sourceHash)
53
60
  add("hash", `source_hash ${meta.source_hash ?? "(none)"} does not match the input`);
61
+ description = meta.description ?? "";
62
+ if (!description)
63
+ add("description", "no description in the front matter");
54
64
  }
55
65
  const prose = body.replace(/^```[\s\S]*?^```/gm, "");
56
66
  if (prose.replace(/[#|\-\s>*_]/g, "").length < 40)
@@ -69,8 +79,32 @@ function checkDoc(doc, input, settings, opts = {}) {
69
79
  add("placeholder", `left in: ${left[0]}`);
70
80
  if (tokens > settings.tokenBudget)
71
81
  add("tokens", `${tokens} tokens, budget ${settings.tokenBudget}`);
82
+ // FAQ: the page's own answers, word for word.
83
+ const pairs = (0, faq_1.faqPairs)(input.jsonLd);
84
+ if (pairs.length) {
85
+ const missing = (0, faq_1.missingFaqAnswers)(md, pairs);
86
+ if (missing === null) {
87
+ if ((0, faq_1.pageShowsFaq)(input.text))
88
+ add("faq", "the page's FAQ section is missing");
89
+ }
90
+ else {
91
+ for (const p of missing.slice(0, 3))
92
+ add("faq", `the answer to "${p.question.slice(0, 80)}" is not the page's`);
93
+ if (missing.length > 3)
94
+ add("faq", `${missing.length - 3} more answers are not the page's`);
95
+ }
96
+ }
72
97
  // Facts: everything the input contains, as one haystack.
73
- const haystack = [input.text, input.links.map((l) => `${l.url} ${l.text}`).join("\n"), JSON.stringify(input.jsonLd), input.url].join("\n");
98
+ const haystack = [
99
+ input.text,
100
+ input.links.map((l) => `${l.url} ${l.text}`).join("\n"),
101
+ JSON.stringify(input.jsonLd),
102
+ input.url,
103
+ input.description ?? "",
104
+ ].join("\n");
105
+ // What is fact-checked: the body, plus the description (prose, unlike the
106
+ // rest of the front matter).
107
+ const claims = `${body}\n${description}`;
74
108
  const known = new Set();
75
109
  const knownNoQuery = new Set();
76
110
  for (const u of haystack.match(URL_RE) ?? []) {
@@ -90,14 +124,14 @@ function checkDoc(doc, input, settings, opts = {}) {
90
124
  // not a path
91
125
  }
92
126
  }
93
- for (const raw of body.match(URL_RE) ?? []) {
127
+ for (const raw of claims.match(URL_RE) ?? []) {
94
128
  const u = trimUrl(raw);
95
129
  if (!known.has(normUrl(u)) && !knownNoQuery.has(noQuery(u)))
96
130
  add("url", u);
97
131
  }
98
132
  const knownNums = new Set((haystack.match(NUM_RE) ?? []).map(canonNum));
99
133
  const seen = new Set();
100
- for (const n of body.replace(URL_RE, " ").match(NUM_RE) ?? []) {
134
+ for (const n of claims.replace(URL_RE, " ").match(NUM_RE) ?? []) {
101
135
  const c = canonNum(n);
102
136
  if (knownNums.has(c) || seen.has(c))
103
137
  continue;
@@ -9,9 +9,12 @@
9
9
  // jsonLd every JSON-LD block on the page (top-level arrays flattened)
10
10
  // sourceHash SHA-256 of `text + "\n" + JSON.stringify(jsonLd)`: the page's
11
11
  // fingerprint; a new hash means the page changed
12
+ // description the page's meta description (not in the fingerprint)
12
13
  //
13
14
  // Unlike convert, nothing is judged here: buttons and labels stay in, and the
14
- // writing step decides what to leave out. NODE ONLY (linkedom).
15
+ // writing step decides what to leave out. Accordion panels are opened and
16
+ // loading placeholders dropped the same way as convert, so the model sees the
17
+ // answers and not "Loading…". NODE ONLY (linkedom).
15
18
  Object.defineProperty(exports, "__esModule", { value: true });
16
19
  exports.formatLinks = void 0;
17
20
  exports.extractInput = extractInput;
@@ -39,7 +42,14 @@ async function extractInput(html, opts) {
39
42
  // Broken JSON-LD is skipped, as in convert.
40
43
  }
41
44
  }
45
+ const metaContent = (sel) => {
46
+ const v = document.querySelector(sel)?.getAttribute("content");
47
+ return v ? String(v).replace(/\s+/g, " ").trim() : "";
48
+ };
49
+ const description = metaContent('meta[name="description"]') || metaContent('meta[property="og:description"]');
42
50
  (0, convert_1.unfoldNextStreaming)(document);
51
+ (0, convert_1.openDisclosures)(document);
52
+ (0, convert_1.dropPlaceholders)(document);
43
53
  for (const el of Array.from(document.querySelectorAll(DROP)))
44
54
  el.parentNode?.removeChild(el);
45
55
  for (const el of Array.from(document.querySelectorAll('[style*="display:none"], [style*="display: none"]'))) {
@@ -51,7 +61,15 @@ async function extractInput(html, opts) {
51
61
  const mainSource = main ? "main" : article ? "article" : "body";
52
62
  const text = readableText(root);
53
63
  const links = collectLinks(root, opts.url);
54
- return { url: opts.url, text, links, jsonLd, sourceHash: await sourceHashOf(text, jsonLd), mainSource };
64
+ return {
65
+ url: opts.url,
66
+ text,
67
+ links,
68
+ jsonLd,
69
+ sourceHash: await sourceHashOf(text, jsonLd),
70
+ mainSource,
71
+ ...(description ? { description } : {}),
72
+ };
55
73
  }
56
74
  /** The page fingerprint: SHA-256 hex of the text and the JSON-LD. */
57
75
  async function sourceHashOf(text, jsonLd) {
@@ -0,0 +1,2 @@
1
+ import type { PageInput } from "./types";
2
+ export declare function finalizeDoc(doc: string, input: PageInput): string;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ // Refine, between the model and the check: the code's own edits to each reply
3
+ // (changes/agent-ready-quality).
4
+ //
5
+ // description the page's meta description goes into the front matter
6
+ // (replacing the model's); with none, the model's stays and
7
+ // the check fact-checks it
8
+ // FAQ the page's FAQPage questions and answers replace the
9
+ // rewrite's FAQ section, word for word; a rewrite that dropped
10
+ // the FAQ of a page showing one gets it back; a page that
11
+ // shows no FAQ never gets one
12
+ //
13
+ // Done right after the model writes, not after a retry, so no model call is
14
+ // spent on rewording (owner, 2026-11-05). EDGE-SAFE (pure).
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.finalizeDoc = finalizeDoc;
17
+ const enrich_1 = require("../core/enrich");
18
+ const faq_1 = require("../core/faq");
19
+ function finalizeDoc(doc, input) {
20
+ let md = doc;
21
+ if (input.description && (0, enrich_1.hasFrontMatter)(md))
22
+ md = (0, enrich_1.setFrontMatterField)(md, "description", input.description);
23
+ const pairs = (0, faq_1.faqPairs)(input.jsonLd);
24
+ if (pairs.length) {
25
+ const replaced = (0, faq_1.replaceFaqSection)(md, pairs);
26
+ if (replaced !== null)
27
+ md = replaced;
28
+ else if ((0, faq_1.pageShowsFaq)(input.text))
29
+ md = (0, faq_1.addFaqSection)(md, pairs);
30
+ }
31
+ return md;
32
+ }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  // Refine one page: fetch -> extract -> (unchanged? stop) -> fill the prompt ->
3
- // model -> check -> retry once with the issues -> result.
3
+ // model -> finalize (page description, page FAQ) -> check -> retry once with
4
+ // the issues -> result.
4
5
  // Nothing is stored here; the runner decides (refine/run.ts).
5
6
  //
6
7
  // NODE ONLY (extract uses linkedom).
@@ -8,6 +9,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
8
9
  exports.refinePage = refinePage;
9
10
  const extract_1 = require("./extract");
10
11
  const check_1 = require("./check");
12
+ const finalize_1 = require("./finalize");
11
13
  const model_1 = require("./model");
12
14
  const prompts_1 = require("./prompts");
13
15
  async function refinePage(opts) {
@@ -41,7 +43,7 @@ async function refinePage(opts) {
41
43
  const reply = await opts.model({ system: filled.system, user });
42
44
  result.usage.inputTokens += reply.inputTokens ?? 0;
43
45
  result.usage.outputTokens += reply.outputTokens ?? 0;
44
- const doc = (0, model_1.cleanReply)(reply.text);
46
+ const doc = (0, finalize_1.finalizeDoc)((0, model_1.cleanReply)(reply.text), input);
45
47
  const check = (0, check_1.checkDoc)(doc, input, opts.prompt.settings, opts);
46
48
  result.doc = doc;
47
49
  result.issues = check.issues;
@@ -2,7 +2,7 @@ import { PACKAGE_VERSION } from "../core/version";
2
2
  export { PACKAGE_VERSION };
3
3
  export declare const DEFAULT_REFINE_ENDPOINT = "https://relvehq.com/api/agent-ready/v1/refine";
4
4
  export declare const REFINE_MODELS: readonly ["gpt-4o", "gpt-4o-mini", "gpt-4.1", "gpt-4.1-mini"];
5
- export declare const USER_AGENT = "agent-ready/0.4.2 (+https://relvehq.com/agent-ready)";
5
+ export declare const USER_AGENT = "agent-ready/0.4.3 (+https://relvehq.com/agent-ready)";
6
6
  export type RemoteStatus = "ok" | "unchanged" | "rejected" | "error" | "skipped";
7
7
  export interface RemoteItem {
8
8
  path: string;
@@ -12,6 +12,8 @@ export interface PageInput {
12
12
  /** SHA-256 hex of `text + "\n" + JSON.stringify(jsonLd)`. */
13
13
  sourceHash: string;
14
14
  mainSource: "main" | "article" | "body";
15
+ /** The page's meta description (else og:description). Not part of the fingerprint. */
16
+ description?: string;
15
17
  }
16
18
  /** Links as the prompt's {{PAGE_LINKS}} block: "url | text" per line. */
17
19
  export declare function formatLinks(links: PageLink[]): string;