From 931de7fb546c3b13efa600fbd5a81c7eadbe1c1c Mon Sep 17 00:00:00 2001 From: Yunyue Li Date: Thu, 30 Jul 2026 10:34:43 +0800 Subject: [PATCH 1/4] fix(web): stop the page saying two things twice, in all four languages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The measurement first, because it corrects an earlier claim of mine: the page carries 594 words of body copy across 24 blocks, mean 24.8 words. That is lean for seven sections. "Too wordy" was wrong. The problem was repetition, and it was countable — two clauses appeared twice each, word for word: "claim carries an evidence_span, a literal substring of the source" the hero lead, and the rule section's lead "source text stays in its original script" the alias card, and the reference-set card Each clause keeps one home. The rule section owns the definition, because defining the rule is what that section is for; the hero states the consequence instead, which also folds its third sentence into its second — those two said the same thing, once abstractly and once concretely. The reference-set card owns "original script, nothing transliterated", because that is a fact about the corpus; the alias card was using it as a run-up to its own subject. Two more, not about repetition. The engineering lead's colon promised one thing and delivered another: "researched against Splink, ComEM and GraphRAG:" followed by four features of LoreGraph, not four points of comparison. Naming what the list is fixes the grammar without touching the hedge — the project read those papers, it does not implement them. Each language's list has exactly four items; each language's new opener says four. And the reference set localised four of its five titles per language but left 西游记 in Chinese on the English and French pages, while translating Crime and Punishment out of Russian on both. Five titles, two different rules. The other three pages localise all five, so the exception joins them: Journey to the West, La Pérégrination vers l'Ouest. Nothing is lost by not proving the multilingual claim with a book title — the English page already demonstrates CJK where it earns its place, "forget the Elizabeth Bennet or 孫悟空 it already knows". Result: 0 duplicated clauses of 6+ words on any of the four pages, down from 2 on each. English body copy 594 -> 546 words. The replacements assert exactly-once per page, not merely once somewhere, because these are sentences and a near-miss would ship half an edit. That paid for itself twice while being written: it caught a guard that mistook the four credits pages for landing pages (they carry a .lead of their own), and a French colon preceded by U+202F, the narrow no-break space French typography wants, rather than the plain space I had typed. This belongs in the copy deck upstream — one JSON file of {en, zh, ja, fr} strings — more than any other patch in this file. It is here because the deck lives outside the repo. Move it there and delete the patch. Verified at 390 / 768 / 1440 on all four languages: no overlapping glyph ink, nothing clipped, no horizontal scroll. Co-Authored-By: Claude Opus 5 --- src/loregraph/web/marketing-patch.cjs | 121 ++++++++++++++++++++++++++ src/loregraph/web/marketing/README.md | 22 +++++ 2 files changed, 143 insertions(+) diff --git a/src/loregraph/web/marketing-patch.cjs b/src/loregraph/web/marketing-patch.cjs index e2f05da..f2330c9 100644 --- a/src/loregraph/web/marketing-patch.cjs +++ b/src/loregraph/web/marketing-patch.cjs @@ -637,6 +637,65 @@ html[lang='ja'] .work-copy h2 { line-height: 1.2; } } `; +// Exact-string copy edits, per page language. Every pair must match exactly once +// on its page or the build fails — these are sentences, and a near-miss would +// silently ship half an edit. See the copy-edits patch for what each one is for. +const SPAN = "evidence_span"; +const COPY_EDITS = { + en: [ + [ + `Every claim carries an ${SPAN}, a literal substring of the source. Click any relation and you land on the sentence it came from.`, + `Every claim carries an ${SPAN}, so clicking any relation lands you on the sentence it came from.`, + ], + ["Source text stays in its original script. Entity resolution runs on", "Entity resolution runs on"], + [ + "The engineering was researched against Splink, ComEM and GraphRAG:", + "Reading Splink, ComEM and GraphRAG settled four things:", + ], + ["Pride and Prejudice, 西游记, Crime and Punishment", "Pride and Prejudice, Journey to the West, Crime and Punishment"], + ], + "zh-CN": [ + [ + `每条断言都带一个 ${SPAN},也就是原文里的一段字面文本。点开任意一条关系,就落到它出处的那一句。`, + `每条断言都带一个 ${SPAN},点开任意一条关系就落到它出处的那一句。`, + ], + ["源文保留原文字。实体消解跑在", "实体消解跑在"], + ["工程实现参考了 Splink、ComEM 与 GraphRAG:", "读 Splink、ComEM 与 GraphRAG 定下了四件事:"], + ], + ja: [ + [ + `どの主張にも ${SPAN}、つまり原文そのままの文字列が付きます。関係をクリックすれば、その出典の一文に着きます。`, + `どの主張にも ${SPAN} が付くので、関係をクリックすればその出典の一文に着きます。`, + ], + ["原文はもとの文字体系のまま保つ。実体解決は", "実体解決は"], + [ + "エンジニアリングは Splink・ComEM・GraphRAG を参照して設計した。", + "Splink・ComEM・GraphRAG を読んで、四つのことが決まった。", + ], + ], + fr: [ + [ + `Chaque assertion porte un ${SPAN}, une sous-chaîne littérale de la source. Cliquez sur une relation, vous atterrissez sur la phrase dont elle vient.`, + `Chaque assertion porte un ${SPAN}, donc un clic sur une relation vous mène à la phrase dont elle vient.`, + ], + [ + "Le texte source reste dans son écriture d’origine. La résolution d’entités tourne sur", + "La résolution d’entités tourne sur", + ], + // The export sets French punctuation properly: a narrow no-break space + // before the colon, U+202F, not a plain one. Escaped so it stays visible + // here, and kept in the replacement. + [ + "L\u2019ing\u00e9nierie s\u2019appuie sur les travaux de Splink, ComEM et GraphRAG\u202f:", + "Lire Splink, ComEM et GraphRAG a r\u00e9gl\u00e9 quatre points\u202f:", + ], + [ + "Orgueil et Préjugés, 西游记, Crime et Châtiment", + "Orgueil et Préjugés, La Pérégrination vers l’Ouest, Crime et Châtiment", + ], + ], +}; + const IMG_RE = /]*>/g; // The stylesheet documents itself with prose, and that prose contains tag-shaped @@ -898,6 +957,68 @@ const PATCHES = [ return { html: out, count }; }, }, + { + name: "copy-edits", + why: "one 10-word clause and one 7-word clause were each on the page twice, verbatim", + // The page carries 594 words of body copy across 24 blocks — lean for seven + // sections, so the problem was never length. It was repetition, and it was + // measurable: two clauses appeared twice each, word for word. + // + // "claim carries an evidence_span, a literal substring of the source" + // hero lead + the rule section's lead + // "source text stays in its original script" + // the alias card + the reference-set card + // + // Each clause stays where it belongs and goes everywhere else. The rule + // section owns the definition, because defining the rule is what that + // section is for; the hero states the consequence instead, which also folds + // its third sentence into its second — those two said the same thing, once + // abstractly and once concretely. The reference-set card owns "original + // script, nothing transliterated", because that is a fact about the corpus; + // the alias card was using it as a run-up to its own subject. + // + // Two more, not about repetition: + // + // The engineering lead promised one thing with its colon and delivered + // another — "researched against Splink, ComEM and GraphRAG:" followed by + // four features of LoreGraph, not four points of comparison. Naming what + // the list is fixes the grammar without touching the hedge: the project read + // those papers, it does not implement them. + // + // And the reference set localised four of its five titles per language but + // left 西游记 in Chinese on the English and French pages, while translating + // Crime and Punishment out of Russian on both. Five titles, two different + // rules. The other three pages localise all five, so the exception joins + // them. The English page still demonstrates CJK where it earns its place — + // "forget the Elizabeth Bennet or 孫悟空 it already knows" — so nothing is + // lost by not proving it with a book title. + // + // This belongs in the copy deck upstream, which is one JSON file of + // {en, zh, ja, fr} strings. It is here because the deck lives outside this + // repo; move it there and delete this patch. + run(html) { + // Landing pages only. The credits pages reuse the head and the chrome but + // carry none of this prose — and they do have a .lead of their own, so the + // hero section is the marker, not that class. + if (!html.includes("
]*lang=['"]([^'"]+)/) || [])[1] || "en"; + const pairs = COPY_EDITS[lang]; + if (!pairs) throw new Error(`marketing-patch copy-edits: no edits for lang '${lang}'`); + let out = html; + let count = 0; + for (const [from, to] of pairs) { + const hits = out.split(from).length - 1; + if (hits !== 1) { + throw new Error( + `marketing-patch copy-edits (${lang}): expected exactly 1 match, found ${hits}:\n ${from}`, + ); + } + out = out.split(from).join(to); + count++; + } + return { html: out, count }; + }, + }, { name: "fold-topbar-into-header", why: "two beige bands with one hairline between them read as one 124px block, not as a utility rail plus a header", diff --git a/src/loregraph/web/marketing/README.md b/src/loregraph/web/marketing/README.md index 98dd59a..258bbdf 100644 --- a/src/loregraph/web/marketing/README.md +++ b/src/loregraph/web/marketing/README.md @@ -79,12 +79,34 @@ the measurements behind each one. In short: "在 GitHub 上 Star" became "Star · 5" a moment after load on three of the four pages. +- **Copy said two things twice.** The page carries 594 words of body copy across + 24 blocks, which is lean for seven sections — so the problem was never length. + It was that one 10-word clause ("claim carries an `evidence_span`, a literal + substring of the source") and one 7-word clause ("source text stays in its + original script") each appeared twice, word for word, in different sections. + Each clause now stays where it belongs and goes everywhere else. Two more, not + about repetition: the engineering lead's colon promised four points of + comparison and delivered four LoreGraph features, and the reference set + localised four of its five book titles per language but left 西游记 in Chinese + on the English and French pages while translating *Crime and Punishment* out of + Russian on both. + + **This one belongs upstream more than any of the others.** The copy deck is a + single JSON file of `{en, zh, ja, fr}` strings; these edits are exact-string + replacements standing in for it. Move them into the deck and delete the patch. + **Every patch asserts it matched.** If a future export changes the markup out from under one, the build fails rather than shipping the page with the bug back in it — so a patch that stops matching is either already fixed upstream (delete it) or broken (rewrite it). Fixing any of these on the generating side is the better outcome; deleting the corresponding patch is then the whole job. +The copy edits assert harder: each replacement must match **exactly once** on its +page, because these are sentences and a near-miss would ship half an edit. That +caught two mistakes while it was being written — a guard that mistook the credits +pages for landing pages, and a French colon preceded by U+202F rather than a +plain space. + Two things the patch step is careful *not* to do. The Chinese and Japanese pages carry their own adaptation block — CJK font stacks, `font-synthesis-style: none` so an `` gets no synthetic oblique, and 1.2 leading because a Han glyph fills From 18bba8af55f89417af873869b2f911e7a8b9e13d Mon Sep 17 00:00:00 2001 From: Yunyue Li Date: Thu, 30 Jul 2026 10:39:02 +0800 Subject: [PATCH 2/4] fix(web): the spec strip stops closing four fragments with four full stops MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Closed world. Literal match. Multilingual. Resumable." is four fragments, none of them a sentence, each given a full stop — the same tic as the interpuncts removed earlier, with a different glyph. Spacing separates them now, the way it already does in the header, the section rules and the footer. All four languages. Co-Authored-By: Claude Opus 5 --- src/loregraph/web/marketing-patch.cjs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/loregraph/web/marketing-patch.cjs b/src/loregraph/web/marketing-patch.cjs index f2330c9..616ec0d 100644 --- a/src/loregraph/web/marketing-patch.cjs +++ b/src/loregraph/web/marketing-patch.cjs @@ -641,6 +641,10 @@ html[lang='ja'] .work-copy h2 { line-height: 1.2; } // on its page or the build fails — these are sentences, and a near-miss would // silently ship half an edit. See the copy-edits patch for what each one is for. const SPAN = "evidence_span"; +// The spec strip closed four fragments with four full stops, none of them a +// sentence — the same tic as the interpuncts, with a different glyph. Spacing +// separates them, the way it now does everywhere else on the page. +const GAP = "   "; const COPY_EDITS = { en: [ [ @@ -653,8 +657,17 @@ const COPY_EDITS = { "Reading Splink, ComEM and GraphRAG settled four things:", ], ["Pride and Prejudice, 西游记, Crime and Punishment", "Pride and Prejudice, Journey to the West, Crime and Punishment"], + [ + "Closed world. Literal match. Multilingual. Resumable.", + `Closed world${GAP}Literal match${GAP}Multilingual${GAP}Resumable`, + ], ], "zh-CN": [ + [ + "闭世界。字面匹配。多语种。可续跑。", + `闭世界${GAP}字面匹配${GAP}多语种${GAP}可续跑`, + ], + [ `每条断言都带一个 ${SPAN},也就是原文里的一段字面文本。点开任意一条关系,就落到它出处的那一句。`, `每条断言都带一个 ${SPAN},点开任意一条关系就落到它出处的那一句。`, @@ -663,6 +676,11 @@ const COPY_EDITS = { ["工程实现参考了 Splink、ComEM 与 GraphRAG:", "读 Splink、ComEM 与 GraphRAG 定下了四件事:"], ], ja: [ + [ + "閉世界。文字列一致。多言語。再開可能。", + `閉世界${GAP}文字列一致${GAP}多言語${GAP}再開可能`, + ], + [ `どの主張にも ${SPAN}、つまり原文そのままの文字列が付きます。関係をクリックすれば、その出典の一文に着きます。`, `どの主張にも ${SPAN} が付くので、関係をクリックすればその出典の一文に着きます。`, @@ -674,6 +692,11 @@ const COPY_EDITS = { ], ], fr: [ + [ + "Monde clos. Correspondance littérale. Multilingue. Reprise possible.", + `Monde clos${GAP}Correspondance littérale${GAP}Multilingue${GAP}Reprise possible`, + ], + [ `Chaque assertion porte un ${SPAN}, une sous-chaîne littérale de la source. Cliquez sur une relation, vous atterrissez sur la phrase dont elle vient.`, `Chaque assertion porte un ${SPAN}, donc un clic sur une relation vous mène à la phrase dont elle vient.`, From b2677a47e9d9542ca070c3c3ee5a8fff11674ef8 Mon Sep 17 00:00:00 2001 From: Yunyue Li Date: Thu, 30 Jul 2026 10:43:51 +0800 Subject: [PATCH 3/4] fix(web): close the h2-to-h4 gaps in the heading outline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nine h4s and four h5s sat directly under an h2 with no h3 between them: the five reading-room cards, the four pipeline steps, the four footer columns. Navigating by heading gave a tree with levels missing. An earlier pass left this alone on the grounds that the styling is bound to the tag name, so moving the tag would mean copying its declarations into the corrections sheet — a copy that goes stale silently the next time the export changes them. That reasoning was wrong: the stylesheet is inline in the same file this patch already rewrites, so the selector moves with the tag and nothing is duplicated. Outline is now h1 / h2 / h3 with no skips on all four languages. The three renamed selectors resolve to the same declarations as before, verified on the rendered page: 18px 700 for the cards, 30px 800 flex for the steps, 11px 700 at 0.18em for the footer columns, and the step arrow still absent on the last step. Verified at 390 / 768 / 1440 on all four languages: no overlapping glyph ink, nothing clipped, no horizontal scroll. Co-Authored-By: Claude Opus 5 --- src/loregraph/web/marketing-patch.cjs | 57 +++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/src/loregraph/web/marketing-patch.cjs b/src/loregraph/web/marketing-patch.cjs index 616ec0d..08184ef 100644 --- a/src/loregraph/web/marketing-patch.cjs +++ b/src/loregraph/web/marketing-patch.cjs @@ -11,9 +11,9 @@ // quietly shipping the page with the bug back in it. A patch that stops matching // is either fixed upstream — delete it here — or broken, and both need a human. // -// Anything editorial (heading levels that would need their CSS selectors moved -// with them, alt text that is too long to hear read aloud) is deliberately NOT -// here. Those want a decision on the generating side, not a regex. +// Anything that needs a judgement call rather than a rule belongs on the +// generating side, not here. What is left in that category is the copy voice +// itself; the copy-edits patch only removes repetition it can point at. const fs = require("fs"); const path = require("path"); @@ -1042,6 +1042,57 @@ const PATCHES = [ return { html: out, count }; }, }, + { + name: "heading-levels", + why: "the outline jumped h2 to h4 three times, so heading navigation gave a broken tree", + // Nine h4s and four h5s sit directly under an h2 with no h3 between them: + // the five reading-room cards, the four pipeline steps, and the four footer + // columns. Someone navigating by heading gets a tree with two levels missing + // and no way to tell whether they have skipped something. + // + // An earlier pass left this alone on the grounds that the styling is bound to + // the tag name, so moving the tag would mean copying its declarations into + // the corrections sheet — a copy that goes stale silently the next time the + // export changes them. That was wrong: the stylesheet is inline in the same + // file this patch already rewrites, so the selector moves with the tag and + // nothing is duplicated. + run(html) { + let count = 0; + const selector = (from, to, expect) => { + const hits = html.split(from).length - 1; + if (hits !== expect) { + throw new Error( + `marketing-patch heading-levels: expected ${expect} of '${from}', found ${hits}`, + ); + } + html = html.split(from).join(to); + count += hits; + }; + selector(".lab h4", ".lab h3", 1); + selector(".method-step h4", ".method-step h3", 2); + selector(".method-step:last-child h4", ".method-step:last-child h3", 1); + selector(".foot-col h5", ".foot-col h3", 1); + + // Tags only — mapMarkup steps over