Curation v2 step 6: Behind the paper, stats, run report block, lock

Behind-the-paper chapter (behind.xhtml, both editions) built from the run's
StageCounts and candidate_runs near misses; daily-epub stats [--days N];
StageCounts gains knn/feed gates and verdicts_in_prompt, timings split
into summaries + brief, the four-line §15.4 info block logged once per
run and printed by print_report; src/lock.rs flock guard on
<database_path>.lock for generate, profile rebuild, features backfill and
backfill-social; README updated for the new CLI, env vars and costs.

Implemented by a Claude agent from docs/plans/curation-v2-briefs/step6.md;
reviewed against plan §5, §15.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1rCLQeKBgnBo3oTgHuTMe
This commit is contained in:
2026-09-02 16:21:06 +00:00
co-authored by Claude Fable 5.1
parent 05a74a0dcf
commit d261cd485d
22 changed files with 1583 additions and 113 deletions
+67
View File
@@ -89,6 +89,7 @@ fn standard_edition_is_a_well_formed_epub3_archive() {
"OEBPS/sec-niche-corner.xhtml",
"OEBPS/art-1002.xhtml",
"OEBPS/world.xhtml",
"OEBPS/behind.xhtml",
"OEBPS/colophon.xhtml",
] {
assert!(contains_entry(&zip, entry), "missing {entry}");
@@ -186,11 +187,77 @@ fn chapter_ids_hrefs_and_toc_levels_are_stable() {
),
("art-1002".into(), "art-1002.xhtml".into(), 2),
("world".into(), "world.xhtml".into(), 1),
("behind".into(), "behind.xhtml".into(), 1),
("colophon".into(), "colophon.xhtml".into(), 1),
]
);
}
/// "Behind the paper" sits between the World Briefing and the colophon in both
/// editions, carries the counts, the admission mix, the learned-signal state,
/// the near misses and the models, and has no links (§15.1).
#[test]
fn behind_the_paper_renders_counts_and_near_misses_in_both_editions() {
let issue = fixtures::issue();
for edition in [Edition::Standard, Edition::X4] {
let (_dir, _, zip) = build_edition_to_bytes(&issue, edition);
let behind = read_entry(&zip, "OEBPS/behind.xhtml");
assert!(behind.contains("<h1>Behind the paper</h1>"), "{behind}");
assert!(
behind.contains(
"Considered 412 articles from 1,465 feeds \u{00b7} 398 eligible \u{00b7} 398 triaged \u{00b7} 120 read closely \u{00b7} 60 shortlisted \u{00b7} 2 selected."
),
"{behind}"
);
assert!(
behind.contains(
"Admitted via: triage 60 \u{00b7} interests 20 \u{00b7} your ratings 12 \u{00b7} exploration 5 \u{00b7} blend 23."
),
"{behind}"
);
assert!(
behind.contains(
"Learned signals: 14 rated articles with embeddings (neighbour signal at 35%); feed affinity off."
),
"{behind}"
);
assert!(behind.contains("<h2>Near misses</h2>"), "{behind}");
assert!(
behind.contains(
"<li>The One That Got Away \u{2014} Example Feed \u{00b7} quality 8.0 \u{00b7} fit 6.5 \u{00b7} shortlisted, not selected</li>"
),
"{behind}"
);
assert!(
behind.contains(
"<li>Never Read Closely \u{2014} Other Feed \u{00b7} triaged, not admitted</li>"
),
"{behind}"
);
assert!(
behind.contains(
"Models: triage and assessment deepseek-v4-flash \u{00b7} editor and summaries claude-opus-5 \u{00b7} embeddings voyage-4-lite. Cost $0.81. Generation 23 min."
),
"{behind}"
);
assert!(!behind.contains("<a "), "no links in {edition:?}: {behind}");
assert!(!behind.contains("&nbsp;"));
for tag in ["html", "head", "body", "div", "p", "ul", "li", "h1", "h2"] {
// `<li` would also match `<link`; count real opening tags only.
let opens = behind.matches(&format!("<{tag}>")).count()
+ behind.matches(&format!("<{tag} ")).count();
let closes = behind.matches(&format!("</{tag}>")).count();
assert_eq!(opens, closes, "unbalanced <{tag}> in {edition:?}");
}
// Ordering: world → behind → colophon in the spine.
let opf = read_entry(&zip, "OEBPS/content.opf");
let position = |href: &str| opf.find(&format!("href=\"{href}\"")).expect(href);
assert!(position("world.xhtml") < position("behind.xhtml"));
assert!(position("behind.xhtml") < position("colophon.xhtml"));
}
}
#[test]
fn every_chapter_is_parseable_xhtml() {
let issue = fixtures::issue();