Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 4e9763e04c776c7841fe42abc2bbab08501a7a60
parent 2da702073bcdf8dd3e2add17ca5d05c52c16cde3
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon,  5 Oct 2026 03:34:42 -0400

common: converter tests — links, spans from cues, the markdown engine, /sweep, /ask, manifest both ways and the round trips

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
Acommon/lib/report/convert.test.ts | 618+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcommon/lib/report/convertShared.ts | 2+-
2 files changed, 619 insertions(+), 1 deletion(-)

diff --git a/common/lib/report/convert.test.ts b/common/lib/report/convert.test.ts @@ -0,0 +1,618 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { + DEFAULT_SPAN_SECONDS, + IdAllocator, + parseCitationHref, + spanAt, + structureMarkdown, + type Cue, + type CuesOf, +} from "./convertShared"; +import { sweepToReport } from "./convertSweep"; +import { askToReport, parseClock, readAskAnswer } from "./convertAsk"; +import { manifestToReport, reportToManifest, STARTER_RENDER } from "./convertManifest"; +import { validateReport } from "./validate"; +import type { Report } from "./schema"; + +// ─── Fixtures: one channel of two records, one post channel ─── + +const CUES: Record<string, Cue[]> = { + "demo-channel/abc123": [ + { start: 58.2, end: 60.1, text: "This is where it starts." }, + { start: 60.1, end: 62.0, text: "and then the host says" }, + { start: 62.4, end: 64.9, text: "we never said that" }, + { start: 64.9, end: 67.3, text: "not once, not ever." }, + { start: 67.3, end: 70.0, text: "Next topic." }, + ], +}; + +const cuesOf: CuesOf = async (channel, id) => CUES[`${channel}/${id}`] ?? null; + +const ORIGIN = "https://archive.example"; +const moment = (channel: string, id: string, t: number, encode = true) => + `${ORIGIN}/?v=${encode ? encodeURIComponent(`${channel}/${id}`) : `${channel}/${id}`}&t=${t}`; + +// ─── Links ─── + +test("parseCitationHref reads viewer moments, archive posts, moment pages and platform posts", () => { + assert.deepEqual(parseCitationHref(moment("demo-channel", "abc123", 62)), { + kind: "span", + channel: "demo-channel", + id: "abc123", + seconds: 62, + }); + assert.deepEqual(parseCitationHref(moment("demo-channel", "-def456", 5, false)), { + kind: "span", + channel: "demo-channel", + id: "-def456", + seconds: 5, + }); + assert.deepEqual(parseCitationHref(`${ORIGIN}/?v=demo-social%2F1234567890&vm=post`), { + kind: "post", + channel: "demo-social", + id: "1234567890", + }); + assert.deepEqual(parseCitationHref(`${ORIGIN}/m/demo-channel/abc123/12.50-20.00/`), { + kind: "span", + channel: "demo-channel", + id: "abc123", + seconds: 12.5, + end: 20, + }); + assert.deepEqual(parseCitationHref("https://x.com/demo/status/987654321"), { + kind: "post-original", + platform: "x", + id: "987654321", + handle: "demo", + }); + assert.deepEqual(parseCitationHref("https://bsky.app/profile/demo.example/post/3abcxyz"), { + kind: "post-original", + platform: "bluesky", + id: "3abcxyz", + handle: "demo.example", + }); + assert.equal(parseCitationHref("https://www.youtube.com/watch?v=abc123&t=5s"), null); + assert.equal(parseCitationHref("https://example.test/article"), null); + assert.equal(parseCitationHref("cite:c01"), null); +}); + +// ─── Spans ─── + +test("spanAt: no cues is the second plus the default span", async () => { + assert.deepEqual(await spanAt("demo-channel", "nocues", 30, "anything", {}), { + start: 30, + end: 30 + DEFAULT_SPAN_SECONDS, + from: "default", + }); + assert.deepEqual(await spanAt("demo-channel", "nocues", 30, "x", { spanSeconds: 4 }), { start: 30, end: 34, from: "default" }); +}); + +test("spanAt: the cited cue, run on to the quote's length, widened to whole sentences", async () => { + // t=62 cites the cue starting at 62.4; the quote is two cues long; the + // sentence opens after "This is where it starts." and closes at "not ever.". + const span = await spanAt("demo-channel", "abc123", 62, "we never said that, not once, not ever", { cuesOf }); + assert.deepEqual(span, { start: 60.1, end: 67.3, from: "cues" }); +}); + +test("spanAt: an end the link gives is kept", async () => { + assert.deepEqual(await spanAt("demo-channel", "abc123", 12.5, "q", { cuesOf }, 20), { start: 12.5, end: 20, from: "link" }); +}); + +test("IdAllocator keeps a sound free id, repairs and suffixes the rest", () => { + const ids = new IdAllocator(); + assert.equal(ids.claim("c01"), "c01"); + assert.equal(ids.claim("c01"), "c01-2"); + assert.equal(ids.claim("-x y"), "x-y"); + assert.equal(ids.next("c"), "c02"); + assert.equal(ids.next("c"), "c03"); +}); + +// ─── The markdown engine ─── + +test("structureMarkdown: title, summary, sections, claims and bodies; code is not citing", async () => { + const md = [ + "# The title", + "", + "A summary [citing](x:1).", + "", + "## Section one", + "", + "- A claim [cite](x:2)", + "- Not a claim.", + "", + "```", + "[not](x:3)", + "```", + "", + "### A sub-heading stays in the body", + "", + "## Section one", + "", + "Plain paragraph with `[code](x:4)`.", + ].join("\n"); + const seen: string[] = []; + const doc = await structureMarkdown( + md, + async ({ href }) => { + seen.push(href); + return `c${href.slice(2)}`; + }, + { defaultSection: "Default" }, + ); + assert.deepEqual(seen, ["x:1", "x:2"]); + assert.equal(doc.title, "The title"); + assert.equal(doc.summary, "A summary [citing](cite:c1)."); + assert.deepEqual( + doc.sections.map((s) => s.id), + ["section-one", "section-one-2"], + ); + assert.deepEqual(doc.sections[0].claims, [{ id: "k01", text: "A claim", citations: ["c2"] }]); + assert.equal(doc.sections[0].body, "- Not a claim.\n\n```\n[not](x:3)\n```\n\n### A sub-heading stays in the body"); + assert.equal(doc.sections[1].body, "Plain paragraph with `[code](x:4)`."); +}); + +test("structureMarkdown: no section headings is one section of the default title", async () => { + const doc = await structureMarkdown("One [a](x:1).\n\nTwo.", async () => "c1", { defaultSection: "Answer" }); + assert.equal(doc.title, null); + assert.equal(doc.summary, ""); + assert.equal(doc.sections.length, 1); + assert.equal(doc.sections[0].title, "Answer"); + assert.equal(doc.sections[0].claims?.[0].text, "One."); + assert.equal(doc.sections[0].body, "Two."); +}); + +// ─── /sweep ─── + +const SWEEP_MD = [ + "# Demo sweep", + "", + `What the sweep looked for; [the method](https://example.test/method).`, + "", + "## First topic", + "", + `- "We never said that." — [Demo episode @ 1:02](${moment("demo-channel", "abc123", 62)})`, + "- Background with no citation.", + "", + `> "Another line here."`, + "", + `— [Demo episode @ 2:00](${moment("demo-channel", "abc123", 120, false)})`, + "", + "## Posts", + "", + `- The account posted it: "the post words" [post by demo, 2024-05-01](${ORIGIN}/?v=demo-social%2F1234567890&vm=post)`, + `- On X: "a platform post" [post by demo](https://x.com/demo/status/987654321)`, + `- "first" [A @ 0:05](${moment("demo-channel", "abc123", 5)}) and later "second" [B @ 0:30](${moment("demo-channel", "-def456", 30)}).`, +].join("\n"); + +test("sweepToReport: sections, claims, citations, quotes and a sound report", async () => { + const { report, warnings, problems } = await sweepToReport(SWEEP_MD, { + postChannelOf: async ({ id }) => (id === "987654321" ? "demo-social" : null), + }); + assert.deepEqual(problems, []); + assert.deepEqual(warnings, []); + assert.equal(report.id, "demo-sweep"); + assert.equal(report.kind, "sweep"); + assert.equal(report.title, "Demo sweep"); + assert.equal(report.summary, "What the sweep looked for; [the method](https://example.test/method)."); + assert.deepEqual( + report.sections.map((s) => [s.id, s.title]), + [ + ["first-topic", "First topic"], + ["posts", "Posts"], + ], + ); + const [first, posts] = report.sections; + assert.equal(first.body, "- Background with no citation."); + assert.deepEqual(first.claims, [ + { id: "k01", text: '"We never said that."', citations: ["c01"] }, + { id: "k02", text: '"Another line here."', citations: ["c02"] }, + ]); + assert.deepEqual(report.citations?.c01, { + kind: "video", + channel: "demo-channel", + id: "abc123", + start: 62, + end: 62 + DEFAULT_SPAN_SECONDS, + quote: "We never said that.", + label: "Demo episode", + }); + assert.equal(report.citations?.c02.quote, "Another line here."); + assert.deepEqual(report.citations?.p01, { + kind: "post", + channel: "demo-social", + id: "1234567890", + quote: "the post words", + label: "post by demo, 2024-05-01", + date: "2024-05-01", + }); + assert.deepEqual(report.citations?.p02, { + kind: "post", + channel: "demo-social", + id: "987654321", + quote: "a platform post", + label: "post by demo", + }); + // Two citations with words between them keep their places in findings. + const both = posts.claims?.[2]; + assert.deepEqual(both?.citations, ["c03", "c04"]); + assert.equal(both?.findings, '"first" [A @ 0:05](cite:c03) and later "second" [B @ 0:30](cite:c04).'); + assert.equal(report.citations?.c03.quote, "first"); + assert.equal(report.citations?.c04.quote, "second"); + assert.deepEqual(validateReport(report), []); +}); + +test("sweepToReport: cues widen the spans; a platform post with no channel stays a plain link", async () => { + const { report, warnings, problems } = await sweepToReport(SWEEP_MD, { cuesOf }); + assert.deepEqual(problems, []); + const c01 = report.citations?.c01; + assert.ok(c01 && c01.kind === "video"); + assert.equal(c01.start, 60.1); + assert.equal(c01.end, 67.3); + assert.ok(warnings.some((w) => w.startsWith("https://x.com/demo/status/987654321: no archive channel"))); + assert.ok(warnings.some((w) => w.startsWith("demo-channel/-def456: no cues"))); + assert.match(JSON.stringify(report.sections), /\[post by demo\]\(https:\/\/x\.com\/demo\/status\/987654321\)/); +}); + +// ─── /ask ─── + +const ASK = { + question: "What did the host say?", + answer: "The host said it [1 @ 1:01], then posted about it [2].\n\nA stray marker [9].\n\n`[1]` in code.", + sources: [ + { + key: "demo-channel/abc123", + title: "Demo episode", + snippets: [ + { clock: "1:02", seconds: 62, text: "we never said that" }, + { clock: "5:00", seconds: 300, text: "something else" }, + ], + }, + { key: "demo-social/1234567890", title: "a post", isPost: true, uploadDate: "20240501", snippets: [{ clock: "", seconds: 0, text: "the post words" }] }, + ], +}; + +test("parseClock reads mm:ss and h:mm:ss", () => { + assert.equal(parseClock("1:02"), 62); + assert.equal(parseClock("1:00:05"), 3605); + assert.equal(parseClock("x:10"), null); +}); + +test("readAskAnswer takes an answer, a saved chat and an assistant message", () => { + assert.deepEqual(readAskAnswer(ASK), ASK); + const chat = { + messages: [ + { role: "user", content: "the question" }, + { role: "assistant", content: "the answer [1]", sources: ASK.sources }, + ], + }; + assert.deepEqual(readAskAnswer(chat), { answer: "the answer [1]", sources: ASK.sources, question: "the question" }); + assert.deepEqual(readAskAnswer({ ...chat, report: "the report [1]", reportSources: ASK.sources }), { + answer: "the report [1]", + sources: ASK.sources, + question: undefined, + }); + assert.equal(readAskAnswer({ role: "assistant", content: "x" })?.answer, "x"); + assert.equal(readAskAnswer({ nothing: true }), null); + assert.equal(readAskAnswer([]), null); +}); + +test("askToReport: markers become citations snapped to excerpt lines; a stray marker is a warning", async () => { + const { report, warnings, problems } = await askToReport(ASK, { cuesOf }); + assert.deepEqual(problems, []); + assert.equal(report.title, "What did the host say?"); + assert.equal(report.id, "what-did-the-host-say"); + assert.equal(report.subtitle, undefined); + assert.equal(report.sections.length, 1); + assert.equal(report.sections[0].title, "Answer"); + const claim = report.sections[0].claims?.[0]; + assert.deepEqual(claim?.citations, ["c01", "p01"]); + assert.equal( + claim?.findings, + "The host said it [1 @ 1:01](cite:c01), then posted about it [2](cite:p01).", + ); + // 1:01 snaps to the excerpt line at 62; the cues widen it to the sentence. + assert.deepEqual(report.citations?.c01, { + kind: "video", + channel: "demo-channel", + id: "abc123", + start: 60.1, + end: 67.3, + quote: "we never said that", + label: "Demo episode", + }); + assert.deepEqual(report.citations?.p01, { + kind: "post", + channel: "demo-social", + id: "1234567890", + quote: "the post words", + date: "2024-05-01", + }); + assert.equal(report.sections[0].body, "A stray marker [9].\n\n`[1]` in code."); + assert.deepEqual(warnings, ["[9] names no source (the answer has 2)"]); +}); + +// ─── report-to-video manifests ─── + +function demoManifest() { + return { + schemaVersion: 1, + slug: "demo-factcheck", + title: "A demo fact-check", + subtitle: "What was said, against the record", + provenance: { siteOrigin: ORIGIN, channelSlug: "demo-channel" }, + render: { width: 1920 }, + timeline: [ + { type: "card", id: "t0", style: "title", heading: "A demo fact-check" }, + { type: "card", id: "ch1", style: "chapter", heading: "Chapter one", sub: "The first part" }, + { + type: "image", + id: "a01", + src: "stills/a01.png", + title: "A demo article", + quote: "The article's claim.", + date: "2026-01-02", + citeUrl: "https://example.test/article", + claim: { id: "k1", verdict: "CONTRADICTED" }, + }, + { type: "clip", id: "c01", video: "abc123", start: 60.1, end: 67.3, cite: 60, quote: "we never said that", claim: { id: "k1", verdict: "CONTRADICTED" } }, + { type: "clip", id: "c02", channel: "demo-other", video: "-def456", start: 10, end: 20, cite: 10, quote: "a second record", claim: { id: "k1", verdict: "CONTRADICTED" } }, + { type: "card", id: "ch2", style: "chapter", heading: "Chapter two" }, + { type: "clip", id: "c03", video: "abc123", start: 100, end: 110, cite: 100, quote: "context, no claim" }, + { type: "clip", id: "c04", video: "abc123", start: 200, end: 210, cite: 200, quote: "pinned by the ledger" }, + { type: "clip", id: "x01", src: "media/episode.mp3", start: 1, end: 5 }, + ], + posts: [ + { + id: "pst1", + platform: "x", + date: "2026-01-03", + text: "the post words", + url: "https://x.com/i/status/1234567890", + attachTo: "c01", + siteChannel: "demo-social", + }, + ], + ledger: [ + { id: "L1", date: "2026-01-04", label: "a ledger label", quote: "the ledger's words", channel: "demo-channel", video: "abc123", cite: 200, entryId: "c04" }, + { id: "L2", date: "2026-01-05", quote: "never clipped", channel: "demo-channel", video: "abc123", cite: 62 }, + ], + }; +} + +test("manifestToReport: chapters, claims with verdicts, the source sentence, posts, the ledger", async () => { + const { report, warnings, problems } = await manifestToReport(demoManifest(), { cuesOf }); + assert.deepEqual(problems, []); + assert.equal(report.id, "demo-factcheck"); + assert.equal(report.kind, "factcheck"); + assert.equal(report.subtitle, "What was said, against the record"); + assert.deepEqual( + report.sections.map((s) => [s.id, s.title, s.body]), + [ + ["ch1", "Chapter one", "The first part"], + ["ch2", "Chapter two", "- “context, no claim” [abc123](cite:c03)"], + ["ledger", "Not clipped", undefined], + ], + ); + assert.deepEqual(report.sections[0].claims, [ + { + id: "k1", + text: "The article's claim.", + verdict: "CONTRADICTED", + sourceQuote: { citation: "a01" }, + citations: ["c01", "pst1", "c02"], + }, + ]); + assert.deepEqual(report.sections[1].claims, [ + { id: "L1", title: "a ledger label", text: "the ledger's words", citations: ["c04"] }, + ]); + assert.deepEqual(report.sections[2].claims, [{ id: "L2", text: "never clipped", citations: ["l-L2"] }]); + assert.deepEqual(report.citations?.["l-L2"], { + kind: "video", + channel: "demo-channel", + id: "abc123", + start: 60.1, + end: 67.3, + quote: "never clipped", + }); + assert.deepEqual(report.citations?.c02, { + kind: "video", + channel: "demo-other", + id: "-def456", + start: 10, + end: 20, + quote: "a second record", + }); + assert.deepEqual(report.citations?.a01, { + kind: "source", + source: "src-a01", + quote: "The article's claim.", + image: "stills/a01.png", + }); + assert.deepEqual(report.sources?.["src-a01"], { + kind: "other", + title: "A demo article", + url: "https://example.test/article", + date: "2026-01-02", + }); + assert.deepEqual(report.citations?.pst1, { + kind: "post", + channel: "demo-social", + id: "1234567890", + quote: "the post words", + date: "2026-01-03", + }); + assert.deepEqual(warnings, ["timeline[8] x01: a clip of its own media (src) cites no record — left out"]); +}); + +test("reportToManifest: a starter cut — title card, a chapter per section, the claim's still and clips with its verdict, its posts", async () => { + const { report } = await manifestToReport(demoManifest()); + const result = await reportToManifest(report, { siteOrigin: ORIGIN, today: "2026-10-05" }); + assert.ok(result.ok); + const m = result.manifest; + assert.equal(m.slug, "demo-factcheck"); + assert.equal(m.generatedOn, "2026-10-05"); + assert.deepEqual(m.render, STARTER_RENDER); + assert.deepEqual(m.provenance, { siteOrigin: ORIGIN, channelSlug: "demo-channel", channel: "" }); + const timeline = m.timeline as Record<string, unknown>[]; + assert.deepEqual( + timeline.map((e) => `${e.type}:${e.id}`), + ["card:title", "card:ch1", "image:a01", "clip:c01", "clip:c02", "card:ch2", "clip:c03", "clip:c04", "card:ledger", "clip:l-L2"], + ); + assert.deepEqual(timeline[3], { + type: "clip", + id: "c01", + channel: "demo-channel", + video: "abc123", + start: 60.1, + end: 67.3, + cite: 60, + quote: "we never said that", + claim: { id: "k1", verdict: "CONTRADICTED" }, + }); + // A claim without a verdict is not stamped. + assert.equal(timeline[7].claim, undefined); + assert.deepEqual(m.posts, [ + { + id: "pst1", + platform: "x", + date: "2026-01-03", + text: "the post words", + url: "https://x.com/i/status/1234567890", + attachTo: "c01", + siteChannel: "demo-social", + postId: "1234567890", + }, + ]); +}); + +test("round trip: manifest → report → manifest keeps every clip, still, chapter, claim and post", async () => { + const before = demoManifest(); + const { report, problems } = await manifestToReport(before); + assert.deepEqual(problems, []); + const result = await reportToManifest(report); + assert.ok(result.ok); + const after = result.manifest; + const project = (timeline: Record<string, unknown>[]) => + timeline + .filter((e) => (e.type === "clip" && !e.src) || e.type === "image" || (e.type === "card" && e.style === "chapter")) + .map((e) => + e.type === "card" + ? { type: "card", heading: e.heading } + : e.type === "image" + ? { type: "image", id: e.id, src: e.src, quote: e.quote, title: e.title, claim: e.claim } + : { + type: "clip", + id: e.id, + channel: e.channel ?? "demo-channel", + video: e.video, + start: e.start, + end: e.end, + quote: e.quote, + claim: e.claim, + }, + ); + assert.deepEqual(project(after.timeline as Record<string, unknown>[]), [ + ...project(before.timeline), + // The ledger's claim that no clip pinned gains a clip of its own moment. + { type: "card", heading: "Not clipped" }, + { type: "clip", id: "l-L2", channel: "demo-channel", video: "abc123", start: 62, end: 72, quote: "never clipped", claim: undefined }, + ]); + const posts = (after.posts as Record<string, unknown>[]).map(({ id, platform, date, text, url, attachTo, siteChannel }) => ({ + id, + platform, + date, + text, + url, + attachTo, + siteChannel, + })); + assert.deepEqual(posts, before.posts); +}); + +function demoReport(): Report { + return { + format: "archilyzer-report", + version: 1, + id: "demo-report", + kind: "factcheck", + title: "A demo report", + sources: { s0: { kind: "article", title: "A demo article", url: "https://example.test/a", date: "2026-01-02" } }, + citations: { + a01: { kind: "source", source: "s0", quote: "The first claim.", image: "stills/a01.png" }, + c01: { kind: "video", channel: "demo-channel", id: "abc123", start: 10, end: 20, quote: "q1", pad: { before: 5, after: 5 } }, + c02: { kind: "audio", channel: "demo-podcast", id: "ep-042", start: 30.5, end: 42, quote: "q2", date: "2025-03-04" }, + p01: { kind: "post", channel: "demo-social", id: "1234567890", quote: "the post words", date: "2026-01-03T10:00:00Z" }, + w01: { kind: "page", url: "https://example.test/page", quote: "q4" }, + }, + sections: [ + { + id: "ch1", + title: "Chapter one", + body: "Context: [here](cite:c02).", + claims: [ + { + id: "k1", + text: "The first claim.", + verdict: "PARTLY", + sourceQuote: { citation: "a01" }, + findings: "See [this](cite:c01) and [the page](cite:w01).", + citations: ["c01", "p01"], + }, + ], + }, + ], + }; +} + +test("round trip: report → manifest → report keeps sections, claims, verdicts, spans and quotes", async () => { + const before = demoReport(); + const result = await reportToManifest(before); + assert.ok(result.ok); + assert.deepEqual(result.warnings, ["w01: a page citation has no manifest entry — left out"]); + const { report: after, problems } = await manifestToReport(result.manifest); + assert.deepEqual(problems, []); + assert.equal(after.kind, "factcheck"); + assert.deepEqual(after.sections[0].claims, [ + { id: "k1", text: "The first claim.", verdict: "PARTLY", sourceQuote: { citation: "a01" }, citations: ["c01", "p01"] }, + ]); + assert.equal(after.sections[0].body, "- “q2” [ep-042](cite:c02)"); + const c = after.citations ?? {}; + // A pad is the site's, not the cut's: it does not make the trip. + assert.deepEqual(c.c01, { kind: "video", channel: "demo-channel", id: "abc123", start: 10, end: 20, quote: "q1" }); + assert.deepEqual(c.c02, { kind: "audio", channel: "demo-podcast", id: "ep-042", start: 30.5, end: 42, quote: "q2", date: "2025-03-04" }); + assert.deepEqual(c.p01, before.citations?.p01); + assert.equal(c.a01.kind === "source" && c.a01.image, "stills/a01.png"); + assert.equal(c.a01.quote, "The first claim."); +}); + +test("reportToManifest refuses a report that does not validate", async () => { + const bad = { ...demoReport(), citations: {} }; + const result = await reportToManifest(bad); + assert.equal(result.ok, false); + assert.ok(!result.ok && result.problems.some((p) => p.path === "sections[0].claims[0].citations[0]")); +}); + +test("reportToManifest: a post with no known platform or link is left out with a warning", async () => { + const r = demoReport(); + r.citations!.p01 = { kind: "post", channel: "demo-social", id: "3abcxyz", quote: "words", date: "2026-01-03" }; + const result = await reportToManifest(r); + assert.ok(result.ok); + assert.equal(result.manifest.posts, undefined); + assert.ok(result.warnings.some((w) => w.startsWith("p01: post demo-social/3abcxyz — its platform and link are unknown"))); + const withRecord = await reportToManifest(r, { + postOf: async () => ({ platform: "bluesky", url: "https://bsky.app/profile/demo.example/post/3abcxyz", author: "demo.example" }), + }); + assert.ok(withRecord.ok); + assert.deepEqual((withRecord.manifest.posts as Record<string, unknown>[])[0], { + id: "p01", + platform: "bluesky", + handle: "demo.example", + date: "2026-01-03", + text: "words", + url: "https://bsky.app/profile/demo.example/post/3abcxyz", + attachTo: "c01", + siteChannel: "demo-social", + postId: "3abcxyz", + }); +}); diff --git a/common/lib/report/convertShared.ts b/common/lib/report/convertShared.ts @@ -392,7 +392,7 @@ async function rewriteBlock(md: string, previousText: string, resolve: CiteResol } out += md.slice(last); withoutLinks.push(md.slice(last)); - const text = plainText(withoutLinks.join("")).replace(TRIM_SEP_RE, ""); + const text = plainText(withoutLinks.join("")).replace(/\s+([.,;:!?])/g, "$1").replace(TRIM_SEP_RE, ""); return { md: out, text, ids: [...new Set(ids)], interleaved: between > 0 }; }