Skip to content

chore(release): cut 0.4.6#90

Merged
dongwhee merged 21 commits into
mainfrom
release/0.4.6
Jul 6, 2026
Merged

chore(release): cut 0.4.6#90
dongwhee merged 21 commits into
mainfrom
release/0.4.6

Conversation

@dongwhee

@dongwhee dongwhee commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

trawl 0.4.6

Highlights (full list in CHANGELOG [0.4.6]):

  • rs-trafilatura extraction candidate, default on (TRAWL_RS_TRAF=0 to opt out; optional dependency)
  • Extraction selector fixes: heading-density sqrt smoothing + records-sentinel hard gate → +30 score bonus — WCXB combined F1 0.777 → 0.818
  • Profile mapper DIV→MAIN/ARTICLE LCA promotion (ratio ≤ 8) — profile eval IDEAL 16/36 → 26/37
  • Per-call cache freshness max_cache_age_s on fetch_relevant() and MCP fetch_page

Release gates: pytest 455, parity 15/15, MCP stdio smoke OK.

🤖 Generated with Claude Code

lyla and others added 21 commits June 7, 2026 21:16
…e coding count

Post-tag tick-off of `git log v0.4.4..v0.4.5 --no-merges -- src/`
against CHANGELOG [0.4.5] found one src-shipping commit framed only
as research: f4827c8 ships src/trawl/fetchers/pdf_backends.py and
routes pdf.py through it at runtime (default pymupdf unchanged, heavy
parsers lazy). Add an Added entry; the keep-PyMuPDF research note
stays.

CLAUDE.md: the 2026-04-22 longform bullet still said coding 23/24,
contradicting the new Current status (24/24). Annotate it as a
point-in-time measurement whose arxiv_pdf_lora flake has since
recovered.
docs(release): 0.4.5 follow-ups — pdf_backends CHANGELOG entry + stale coding count
trawl now ships its own docker-compose.yml and scripts/deploy.sh so it
can run independently of any single consumer and serve multiple MCP
clients from one instance. Publishes 127.0.0.1:8765 and mounts ~/.trawl;
LLM endpoints default to host.docker.internal llama-servers and are
overridable via a .env file.

HyDE stays off by default; its env (TRAWL_HYDE_URL) is left blank with a
note that a relayed/offloaded utility LLM may live on a different port
than the embedding/rerank servers. README documents the HTTP/compose
deployment path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wires the already-shipped TRAWL_VLM_SLOT env var into the standalone
docker-compose service (default slot 3) to avoid evicting other
consumers' KV cache on the shared :8080 llama-server. TRAWL_HYDE_SLOT
left as a commented placeholder since HyDE is off by default.
Scores rs-trafilatura (rs_text=main_content, rs_md=html_to_markdown of
content_html) against a fresh Python-Trafilatura baseline (run.py's
_TRAF_KWARGS) on the WCXB dev split with the vendored word-F1 evaluator.

2026-07-03 full-split result (trafilatura 2.0.0, rs-trafilatura 0.1.1):
rs_text F1 0.844 / 0 fail / 10ms median vs baseline 0.750 / 15ms —
+0.094, wins all 7 page types (forum +0.225, collection +0.202). Fresh
baseline reproduces the recorded 0.750 exactly, confirming the 2026-04
number was config-consistent. Details in
notes/rs-trafilatura-verification-outcome.md (local).

Note: ExtractResult.content_markdown is always None in rs-trafilatura
0.1.1; the rs_md variant derives markdown from content_html instead.
Adds rs-trafilatura as an optional 5th entrant in extract_html()'s
score-based candidate selection, gated behind TRAWL_RS_TRAF (default
off, accepts 1/true/yes/on). The module is lazily imported and fully
optional: a missing or failing package logs one debug line and the
candidate is skipped permanently. Candidate output is
html_to_markdown(extract(html).content_html) — the markdown route
keeps headings/tables for the chunker. Appended (not inserted) so the
newest candidate loses exact-score ties to established ones.

Measurement (2026-07-03, rs-trafilatura 0.1.1, trafilatura 2.0.0):
- WCXB dev split 1,497 pages: flag ON combined F1 0.809 vs recorded
  3-way 0.777 (+0.032); rs markdown-variant alone 0.848 over-ok /
  0.814 over-all vs same-session Trafilatura baseline 0.750.
- Parity matrix 15/15 flag OFF and flag ON (fetch cache disabled for
  the ON run — cached extractions would otherwise mask the candidate).
- agent_patterns coding shard 24/24 flag ON.
- Missing-package behavior verified: extraction falls through to the
  existing candidates without error.

Default stays OFF pending a default-flip decision.

Also: gitignore benchmarks/wcxb/results/, .env.example entry,
CHANGELOG Unreleased entry.
feat(extraction): opt-in rs-trafilatura candidate (TRAWL_RS_TRAF)
rs-trafilatura extraction candidate is now default on (opt out via
TRAWL_RS_TRAF=0), matching the _RECORDS_ENABLED default-on convention.
Still an optional dependency: missing package logs one debug line and
the candidate is skipped.

Gates (2026-07-04):
- Parity 15/15 default env (fetch cache disabled to exercise the
  candidate) and 15/15 with TRAWL_RS_TRAF=0.
- agent_patterns coding shard 24/24 default env.
- tests/test_extraction.py pass; unset-env import / =0 no-import
  verified offline.

Docs: .env.example opt-out semantics, CHANGELOG entry amended,
CLAUDE.md default-on feature list entry (includes the fetch-cache
A/B measurement caveat).
feat(extraction): flip TRAWL_RS_TRAF default on
Adds a keyword-only max_cache_age_s (int | None, default None) to
fetch_relevant() and the MCP fetch_page tool, threading through
fetch_cache.get/get_with_state/_read as max_age_s:
- None: follow TRAWL_FETCH_CACHE_TTL (unchanged default behavior)
- 0: treat any cached entry as stale; flows into the existing
  conditional-GET revalidation branch (may still 304-reuse)
- N: accept cached entries younger than N seconds

Motivated by the field converging on per-call freshness knobs
(Exa maxAgeHours, Feb 2026). Known documented limitation: profile
fast/transfer paths and passthrough/PDF branches do not consult the
fetch cache, so the parameter has no effect there.

MCP: optional integer property on fetch_page inputSchema, passed via
the _supports_keyword feature-detection pattern.

Tests: 6 new offline cases (age boundary, age=0, None-default at both
fetch_cache and pipeline level); full cache suites 42 pytest + MCP
stdio smoke pass. Parity 14/15 — hada_news fails identically on
develop with and without this diff and with rs on/off (live content
drift, markers absent from today's front page), i.e. environmental,
not diff-caused.
- MCP fetch_page: coerce max_cache_age_s to int with a clean error
  response on non-integer or negative input instead of a deep
  TypeError in fetch_cache.
- fetch_cache.get(): a per-call max_age_s stale judgement no longer
  deletes the record — per-call freshness preference must not destroy
  entries still fresh under the global TTL. Deletion with
  max_age_s=None unchanged.

44 checks pass (43 pytest + MCP smoke).
The 2026-07-06 oracle-gap diagnostic (notes/selector-oracle-gap-
diagnostic.md) showed heading_score's linear density term with a 0.25
cap creates an all-or-nothing cliff that drives 57.7% of wrong
candidate picks among selectable candidates. sqrt dampening (cap 0.35,
weight 40) was the winning variant (V1) of a 4-variant offline matrix
re-scored over all 1,497 WCXB pages: +0.005 full-set F1, +0.024 on the
487-page scoring-failure bucket, all 7 page types net-positive, only
43 cheap regressions (F1 -5.3 vs +12.9 gained). Combining with
length-score dampening (V3) measured strictly worse than V1 alone.

Gates: WCXB end-to-end 0.809 -> 0.814 (matches offline prediction);
parity 14/14 develop-passing cases (hada_news fails identically on
develop - live front-page drift); coding shard 24/24.
feat(cache): per-call freshness override max_cache_age_s
fix(extraction): smooth heading-density score term with sqrt
…okens

news.hada.io front-page drift (2026-07-06): the GeekNews brand text no
longer appears in the extracted body and topic?id hrefs are not
rendered as text, so the 2026-05-19 markers failed while extraction
quality remained good (all news cards present). Replaced with an
any-group of per-card structural tokens (vote arrow / 'points by' /
comment label), following the wanted_jobs precedent (PR #72). Failure
was isolated as environmental before this change: identical FAIL on
develop with and without the recent cache/extraction diffs and with
TRAWL_RS_TRAF on/off.

Full parity matrix re-run after the change: 15/15.
test(parity): replace drifted hada_news markers with card-structure tokens
The hard filter (any records → only sentinel-bearing candidates
selectable) caused beautifulsoup to win by forfeit on 231/1497 WCXB
pages — 52.8% of the measured selector oracle gap (notes/
selector-oracle-gap-diagnostic.md). Replaced with a single max() over
all candidates where sentinel-bearing candidates get SENTINEL_BONUS
when records are present; stored score stays raw.

Bonus calibration was measured, not guessed:
- Offline sweep over 1,497 pages: B=10 (+0.0146) and B=15 (+0.0056)
  pass per-type gates offline, but BOTH fail the aladin_bestsellers
  parity hard gate end-to-end — the measured non-sentinel-vs-sentinel
  gap on that page is 15.84 (rs-trafilatura 145.78 vs beautifulsoup
  129.95, query-dependent).
- B=30 keeps a 14-point margin against live drift at nearly the same
  gain as B=20 (+0.0037 vs +0.0046) and zero page-type regressions.

Gates at B=30: parity 15/15 (aladin/wanted/hada record cases intact),
coding shard 24/24, WCXB 0.814 -> 0.818 (+0.004, matches offline
prediction). 98 unit tests (extraction/records/chunking) pass.
fix(extraction): relax records-sentinel gate to a +30 score bonus
…capped)

Fixes the pypi-class narrow-selector failure: the VLM's deep anchors
plus the mapper's depth-outlier filter collapsed pypi.org profiles to
div[class*="project"] (the description card, 1,818 of 10,903 main
chars). Root cause chain: 8/10 anchors inside one nested code block,
the 2 shallow legit anchors (tagline, install command) dropped as
depth outliers, LCA computed over the deep cluster only.

Fix: after LCA computation, if the LCA is a DIV and a MAIN/ARTICLE
ancestor exists with innerText ratio <= PROMOTION_MAX_RATIO (8.0;
pypi measured 6.0), promote to it. Only fires on DIV LCAs, so
semantic-tag LCAs (TABLE/ARTICLE/SECTION/TBODY) are never touched.

A prompt-side fix (anchor-allocation diversity rules) was tried first
and REJECTED by same-day A/B: it did not move pypi (the outlier drop,
not allocation, is decisive) and it regressed reddit_sub + imdb_movie
from IDEAL to OK. prompts.py is unchanged.

Gates:
- Profile eval 37 cases (pypi_project case added): IDEAL 16/36 -> 26/37,
  0 too-wide, success 92%. 12 promotions, ratios 1.0-5.1, no
  degenerate whole-page grabs. pypi_project: IDEAL, selector #content
  (MAIN/3). At-risk IDEAL sites (tailwind_flex, reddit_sub,
  gh_fastapi, imdb_movie) unchanged with non-DIV LCAs. 3 fails are
  live-site bot blocks / error pages (NYT, Guardian, OpenAI), not
  rule-caused.
- Parity 15/15; mapper unit tests 22 pass.
feat(profiles): promote DIV-level LCA to nearest MAIN/ARTICLE (ratio-capped)
- CHANGELOG [Unreleased] -> [0.4.6] (rs-trafilatura candidate
  default-on, selector fixes: heading sqrt + sentinel bonus, profile
  mapper DIV->MAIN/ARTICLE promotion, per-call max_cache_age_s)
- pyproject + trawl.__version__ -> 0.4.6
- CLAUDE.md Current status: WCXB 0.818, profile eval 26/37 IDEAL
- README WCXB table updated to 0.818 (v0.4.6 measurement)
- test(parity): hada_news any-group gains the markdown-link token
  '](http' — candidate near-tie on the rotating front page flips
  between trafilatura-recall (titles+links) and bs/rs (card
  metadata); the link token is present in ALL candidates' output.
  Diagnosed as content-driven, not code: winner identical under the
  old hard sentinel gate.

Release gates: pytest 455, parity 15/15, MCP stdio smoke OK.
@dongwhee dongwhee merged commit 05733fd into main Jul 6, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant