wp2txt 2.3.1 → 2.3.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4062c47480d0801e7a07d8e3bec7c65be8efeeda2eae30d398f9872810d321cd
4
- data.tar.gz: 25bb3aa8a4d5baefc4616a8a4b754ec63e92d3e0b86b22ccc0ead2929a39739e
3
+ metadata.gz: 874d5f0c6463fc612080339cbbcbe560b51aba31a8a5e9478d95719c53744051
4
+ data.tar.gz: e8377c0df46cd7d35ca8a85cea4f52e6dc4ca57330004ce5fc6f11913aa1abcf
5
5
  SHA512:
6
- metadata.gz: ff61afbb989613e286af8bea784e4494c8f71b0dd52d24d3d038e72c5d77f9196d899039d9ff8c5e4b16461c044670d8cca6bc3feda55aa8cdbc31cbc30e012b
7
- data.tar.gz: ddffdfb2452a3710d527bc7deeae52818cb9c2b4dedd433b2c7ecb06db74e6e389edf127976567a3269583531acf0aabc2ea9058d7422b5b675de9bae2e227fa
6
+ metadata.gz: ce2ca7660379c37a3f36d6d731aa3a9c1798998c10dfb5ff012503f571653b26aef6324e579c1a8af7f0d2ff52cf90fc8c6a50f44f21d40b73d132277cb50a26
7
+ data.tar.gz: d8197213c46510742d015dcea77c8ff5c40d48dac7beff8cc409ce1e4ce5f3e138e47d7df9ce675a0614785b21bb266664f8c082c94080c4f3a29c712ffeab07
data/.gitignore CHANGED
@@ -36,3 +36,4 @@ benchmark_results/
36
36
 
37
37
  # Developer-specific Ruby version
38
38
  .ruby-version
39
+ .private-doc-tokens
data/CHANGELOG.md CHANGED
@@ -5,321 +5,118 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ Entries were rewritten in August 2026 to describe what changed for people using
9
+ wp2txt, rather than how it was implemented. The changes themselves are unaltered.
10
+
11
+ ## [2.3.2] - 2026-08-13
12
+
13
+ - **Documentation release — no code changes.** The changelog and guides bundled with the gem and the container image are rewritten to say what each change means for someone using wp2txt, rather than how it was implemented. The guide formerly at `docs/RESEARCH.md` is now [docs/INDEXES.md](docs/INDEXES.md) ("Offline Indexes, Queries, and the MCP Server")
14
+
8
15
  ## [2.3.1] - 2026-08-12
9
16
 
10
- - **Container images no longer carry private files**: the image is built from the working tree with `git init && git add -A`, but the build context excluded `.gitignore`, so files ignored locally were tracked inside the container and shipped in the image 2.3.0's images contained the maintainer's private research notes under `/wp2txt/research-notes`. The context now keeps `.gitignore` (so the container's file list matches a local `gem build`), explicitly excludes `research-notes` and `.bundle`, and the throwaway `.git` (a blob copy of every added file) is deleted after the build. `rake check_image` builds and inspects an image for private paths, and `rake push` refuses to publish without that check. The published gems were never affected; 2.3.0 images have been withdrawn
11
- - **Docker Hub retired**: images are published to GitHub Container Registry only (`ghcr.io/yohasebe/wp2txt`). The Docker Hub repository is no longer updated
12
- - **Fix: multi-line `<ref>` references broken by element splitting also fixes `--extract-citations` on multi-line cite templates**: `Article#parse` splits elements at newlines, so any reference written across lines (very common for multi-line `{{cite …}}` templates) had its `[ref]` and `[/ref]` markers land in separate elements, invisible to `remove_ref`. The markers and raw reference markup leaked into the output, and more importantly — `--extract-citations` silently never fired for multi-line cite templates, producing inconsistent output versus their single-line equivalents. `make_reference` now drops empty references outright and flattens the rest onto a single line before element splitting, so multi-line references behave exactly like single-line ones. Ordinary paragraph breaks outside references are unaffected
13
- - **Fix: `[ref]` markers destroyed by external-link processing**: `process_external_links` stripped the brackets of the `[ref]`/`[/ref]` markers produced by `make_reference` (their contents took the single-word branch), so `remove_ref` could no longer find them and tag names plus reference bodies leaked into extracted text — even with `--ref`, the kept markers came out broken. The markers are now hidden behind placeholders for the duration of the bracket scan and restored afterwards. Downstream corpora no longer contain `ref…/ref` residue, which broke tokenization/sentence splitting and mixed bibliographic text into body prose
14
- - **Runaway-query hardening**: a `query_sql` child process now sets its own kernel-enforced CPU limit (`RLIMIT_CPU`, the query timeout plus a small grace) in addition to the parent's wall-clock kill. Previously a child orphaned by the parent's death — an interrupted test run, a closed terminal, a crashed server — kept executing forever: sqlite3 holds the GVL inside `sqlite3_step`, so Ruby never reaches a signal-safe point and even SIGTERM is ignored. Observed in the wild as two processes spinning at 99% CPU for over five days. The CPU limit is strictly more permissive than the existing wall-clock deadline, so no query that would otherwise succeed is affected
15
- - **Test-suite hang guard**: each example now runs under a wall-clock timeout (120s default; `WP2TXT_SPEC_TIMEOUT=0` disables it, the `:no_timeout` tag exempts an example). The suite deliberately exercises runaway queries, so a wedged example must fail rather than spin
17
+ - **Fix: references leaked into extracted text**: reference contents tag remnants like `ref…/ref` and the bibliographic text inside themcould end up in the extracted body text, breaking sentence splitting and mixing citation details into running prose. In a spot check of five English articles this affected about 70 places per article. References are now removed cleanly (or kept intact as `[ref]…[/ref]` with `--ref`), including references written across several lines and empty ones
18
+ - **Fix: `--extract-citations` ignored multi-line citation templates**: a `{{cite …}}` template split over several lines was left as raw markup instead of being formatted, so citations extracted from such references were silently missing. Single-line and multi-line templates now produce the same result
19
+ - **Long-running queries can no longer outlive their timeout**: an interrupted session could leave a `query_sql` process running at full CPU indefinitely. Queries now stop on their own even if the process that started them goes away. Queries that complete within the time limit are unaffected
20
+ - **Container images move to GitHub Container Registry**: images are published to `ghcr.io/yohasebe/wp2txt` only. The Docker Hub repository is no longer updated please switch:
16
21
 
17
- ## [2.3.0] - 2026-07-24
22
+ docker pull ghcr.io/yohasebe/wp2txt
18
23
 
19
- - **Documentation split**: README now focuses on text extraction; the research layer (indexes, exhaustive queries, full-text search, langlinks, cross-language SQL, MCP server) is documented in the new [Research Infrastructure Guide](docs/RESEARCH.md), including the complete MCP tool table
20
- - **Container images on GHCR**: images are now published to `ghcr.io/yohasebe/wp2txt` (Docker Hub `yohasebe/wp2txt` is maintained as a mirror)
24
+ ## [2.3.0] - 2026-07-24
21
25
 
22
- - **`extract_corpus` `titles:` argument**: Extract an explicit set of article titles (e.g. a set determined via `query_sql`) titles are normalized (MediaWiki rules), deduplicated preserving input order, and one redirect hop is resolved; missing titles (including redirects to nowhere) are skipped and reported as `not_found` (count + 20-title sample, also in the `.meta.json` sidecar). Mutually exclusive with the filter arguments (set operations belong in SQL); capped at 10,000 titles. The sidecar records `titles_count` + `titles_sha256` (order-independent) for reproducibility, enumerating the full list when ≤100 titles. Also available via `start_extract_job`
23
- - **`query_sql` `output_path:` argument**: Write ALL rows of a large result to a JSONL file (with a `.meta.json` sidecar recording the SQL, dump version, attach configuration, row count, and tool version) and return only a summary + 3-row sample the extract_corpus D4 pattern generalized to SQL. Writes happen in the forked child (so the 30s SIGKILL deadline covers them) to a `.partial` file that the parent atomically renames on success and removes on every failure path (child crash, timeout kill, query error). Hard cap `SQL_FILE_ROW_LIMIT` = 5M rows (`truncated` flag), cells clipped at 64KB (`cells_clipped` count), duplicate column names are made unique (`_2` suffix), `limit` is ignored in this mode, and existing files require `overwrite: true`. The MCP layer's output-dir confinement is now a shared helper (`Wp2txt::OutputPath.confine`) used by extract_corpus, start_extract_job, and query_sql alike
24
- - **Interlanguage links (`--import-langlinks`)**: Import the official `{lang}wiki-{date}-langlinks.sql.gz` dump into the Tier 1 metadata index as a `langlinks` table (`ll_from` = source page_id, `ll_lang` = target language, `ll_title` = normalized target title). Version pinning is enforced: the langlinks file's dump name must equal the built index's dump version — a mismatch is rejected with no override. Streams the MySQL dump without loading it whole (escape-safe tuple parser, 10k-row transaction batches, indexes created after the load). `--langlinks-langs` restricts imported target languages (e.g. `en,de,fr,zh,ko`); re-import requires `-U` (otherwise a no-op reporting the previous import time). Provenance (source file, size, import time, wp2txt version, language filter, row count) is stamped into the index and reported by `dump_info`; a post-import sanity check samples titles per language and warns when the join rate against a locally installed target edition falls below 90%
25
- - **Multi-dump ATTACH in `query_sql`**: The MCP `query_sql` tool gains an optional `attach` argument (language codes only, never paths): `attach: ["en"]` read-only ATTACHes that language's locally installed metadata DB as `en_meta` and its FTS DB (when built) as `en_fts`, both sharing the main DB's schema. Codes are validated, only installed indexes are accepted, path resolution is server-side (same-date dump preferred; otherwise the latest build with a `dump_mismatch` note in the response's `attached` metadata). User SQL still cannot contain ATTACH/DETACH — attachments are issued by server code only, via `mode=ro` URIs on a read-only connection. Combined with the langlinks table, this enables single-query cross-language comparisons (e.g. section-structure diffs of article pairs)
26
+ - **Extract an explicit list of articles**: `extract_corpus` accepts `titles:` hand it the set you arrived at some other way (a `query_sql` result, your own list) instead of describing it with filters again. Titles are normalized the way MediaWiki treats them, duplicates are dropped, and one redirect hop is followed. Titles with no article are skipped and reported back as `not_found`, so a missing article is visible rather than silently absent. Up to 10,000 titles; cannot be combined with the filter arguments. Also available through `start_extract_job`
27
+ - **Write a large SQL result to a file**: `query_sql` accepts `output_path:` all rows go to a JSONL file and the reply carries a summary plus three sample rows, instead of a row cap. The file is written completely or not at all: an interrupted or failing query leaves no partial output. Up to 5 million rows, cells longer than 64KB are clipped (both are reported back), and an existing file is only replaced with `overwrite: true`. A `.meta.json` beside the output records the SQL, the dump versions involved, and the row count
28
+ - **Interlanguage links (`--import-langlinks`)**: import the official `langlinks` dump into the metadata index to map articles across editions. The langlinks file must carry the same dump date as your index — a mismatch is refused, so links can never be mixed across versions. `--langlinks-langs en,de,fr,zh,ko` limits which target languages are imported; re-importing requires `-U`. After the import, a sample of titles per language is checked against any editions you have installed and the match rate is reported. `dump_info` shows what was imported and when
29
+ - **Query several editions in one statement**: `query_sql` accepts `attach: ["en"]`, which makes that language's index available as `en_meta` (and `en_fts` where a full-text index exists) alongside your own. You pass language codes, not paths, and only editions you have installed are accepted. The reply lists what was attached, including each edition's dump name, and says so when the dates differ. Together with the `langlinks` table this makes cross-edition comparisons a single query
30
+ - **Documentation split**: the README now covers text extraction; the index and MCP features (indexes, offline queries, full-text search, interlanguage links, cross-language SQL, MCP server) moved to a separate guide — now [docs/INDEXES.md](docs/INDEXES.md) — which lists every MCP tool
31
+ - **Container images on GitHub Container Registry**: images are now published to `ghcr.io/yohasebe/wp2txt`
26
32
 
27
33
  ## [2.2.0] - 2026-07-22
28
34
 
29
- - **Index hardening (design review follow-up)**: `ord` now has identical semantics in `page_sections` and `fts_map` (lead = 0, first heading = 1; schema v2 — rebuild indexes with `--build-index -U`); section headings are normalized identically in both indexes (decorated headings like `== '''X''' ==` now match section filters); indexes record the wp2txt version (and the FTS index a rendering-config digest) so `dump_info` can flag indexes built by code whose text cleaning differs; rebuilds are atomic (built alongside, renamed on completion a failed rebuild no longer destroys the working index)
30
- - **MCP safety hardening**: `query_sql` runs in a killable subprocess with a 30s wall-clock cap and returns a query-plan diagnosis on timeout; the keyword screen no longer rejects legitimate values inside string literals; `extract_corpus` output paths are confined to the server output directory and refuse to overwrite existing files without `overwrite: true`; background jobs are serialized (one at a time) and all SQLite connections are closed before forking extraction workers
31
- - **Cold-start parity for LLM clients**: closes the gaps that previously required internal knowledge to work around. `get_categories` tool; `find_articles`/`extract_corpus` accept `categories` (exact multi-category AND) and `category_match` (substring on category names); `extract_corpus` gains `content: "wikitext"` for structure mining (infoboxes/templates); `query_sql` read-only SQL escape hatch (SELECT/WITH only, keyword-screened, read-only connection, row/cell caps) with `describe_schema`; article titles are normalized like MediaWiki (underscores, capitalization); `get_article` truncates at `max_chars` (default 40k) with an explicit flag; `dump_info` reports `fulltext_current`
32
- - **Deferred FTS optimize**: `--skip-fts-optimize` skips the single-threaded segment-merge step of the full-text build (the dominant cost on many-core machines: 40-60+ min on a full dump), leaving a fully searchable index; `--fts-optimize` runs the merge later, standalone and idempotent. Optimize state is recorded in index metadata and reported by `dump_info`
33
- - **Full-text search (Tier 2)**: `--build-index --fulltext` builds a contentless SQLite FTS5 index over cleaned section text (tokenizer auto-selected by language: character-trigram for CJK, unicode61 for space-delimited; `--fts-tokenizer` to override, porter stemming opt-in). Search via `--search` (CLI) or the `search_text` MCP tool: literal phrase or raw FTS5 query modes, composable with category recursion and section filters, `count: "capped"|"exact"` (exact 0 = a verified absence claim for the dump version). Snippets are re-rendered from the dump on demand the index stores no text, keeping disk cost to the inverted index only
34
- - **Background extraction jobs**: `start_extract_job` / `job_status` / `cancel_job` / `list_jobs` MCP tools for extractions beyond the 5000-article synchronous cap. Each job runs in its own thread with isolated resources; extraction now streams to disk in batches (memory-safe at any scale)
35
- - **RAG chunking**: `extract_corpus` accepts `chunk_size` / `chunk_overlap`, emitting one record per chunk with `section_path`, `chunk_index`, and `chunk_count`; chunk boundaries prefer sentence/paragraph breaks
36
- - **Alias guardrail**: `save_alias_set` re-checks co-occurrence server-side and refuses groups containing frequently-coexisting heading pairs (likely distinct roles, not synonyms) unless `force` is passed protocol compliance no longer depends on the calling model's discipline
37
- - **`discover_aliases` MCP prompt**: bundled recipe walking any agent through the alias verification protocol (discover via `section_stats` → verify via `section_cooccurrence` → sample borderline cases save)
38
- - **MCP server (`wp2txt-mcp`)**: New binary exposing a local dump to LLM agents via the Model Context Protocol (stdio). Tools: `dump_info`, `get_article`, `get_sections`, `list_headings`, `find_articles`, `category_tree`, `section_stats`, `section_cooccurrence`, `save_alias_set`/`get_alias_set`/`list_alias_sets`, and `extract_corpus` (writes JSONL + reproducibility `.meta.json` sidecar; returns summary and sample only). Requires the `mcp` gem (`gem install mcp`); wp2txt itself does not depend on it
39
- - **LLM-generated section aliases**: Instead of shipping per-language alias dictionaries, agents discover real heading usage with `section_stats`, verify synonym hypotheses with `section_cooccurrence` (synonymous headings almost never co-occur in one article), and persist named alias sets per dump with `save_alias_set`. Queries reference them via `alias_set`; extraction metadata records the exact set contents used
40
- - **`Wp2txt::Corpus` facade**: Shared query/extraction layer used by the MCP server; lazy SQLite-backed title lookup avoids loading multi-million-entry indexes into memory
41
- - **Local metadata index (`--build-index`)**: New offline index built by scanning a multistream dump in parallel. Stores per-article categories, section headings, redirects, and the category hierarchy in SQLite (`~/.wp2txt/cache/*_meta.sqlite3`), keyed to the dump version. No API access required
42
- - **Offline exhaustive queries (`--find-articles`)**: List articles matching `--in-category` (recursive via `--depth`, powered by dump-derived category hierarchy), `--has-section` (alias-aware), and `--title-match` filters. Supports `--limit` and JSON output (`-j json`); redirects are excluded automatically. Enables queries like "all film articles that have a Plot section" against a version-pinned local dump
35
+ - **Index format change — rebuild existing indexes with `--build-index -U`** (schema v2). Section headings are now recorded consistently, so headings written with decoration (`== '''X''' ==`) match section filters instead of being missed. Each index records the wp2txt version that built it, and `dump_info` warns when an index was built by a version whose text cleaning differs from the one you are running. A rebuild that fails partway no longer destroys the index you already had
36
+ - **Safer defaults for MCP use**: `query_sql` stops after 30 seconds and explains what made the query slow; queries containing words like `Update` inside a quoted value are no longer rejected by mistake. `extract_corpus` can only write under the server's output directory and will not replace an existing file unless you pass `overwrite: true`. Background jobs run one at a time
37
+ - **More ways to select articles from an LLM client**: a `get_categories` tool; `categories` (article must be in all of them) and `category_match` (substring on category names) on `find_articles` and `extract_corpus`; `content: "wikitext"` on `extract_corpus` for reading infoboxes and templates that cleaned text replaces with markers; `query_sql` for read-only SQL when the fixed tools cannot express a question, with `describe_schema` to see the tables. Article titles are matched the way MediaWiki treats them (underscores, capitalization). `get_article` stops at `max_chars` (default 40,000) and says when it truncated; `dump_info` reports whether the full-text index is up to date
38
+ - **Split the full-text build in two**: `--skip-fts-optimize` leaves out the final merge step the slowest part on a full dump, 40–60 minutes and single-threaded and still gives you a searchable index. Run `--fts-optimize` later when convenient; it is safe to repeat. `dump_info` shows whether the merge has been done
39
+ - **Full-text search**: `--build-index --fulltext` adds a searchable index over the cleaned article text. The tokenizer is chosen by language (character trigrams for Japanese, Chinese, Korean; word-based elsewhere, `--fts-tokenizer` to override). Search from the CLI with `--search` or from an LLM client with `search_text`, combined with category and section filters, as a literal phrase or a full-text query. Counts can be capped for speed or exact an exact `0` means the term is absent from that dump. The index stores no article text (snippets are re-read from the dump when needed), so it costs far less disk than a copy of the corpus
40
+ - **Background extraction jobs**: `start_extract_job` / `job_status` / `cancel_job` / `list_jobs` for extractions beyond the 5,000-article limit of a direct call. Extraction writes to disk as it goes, so memory use no longer grows with the size of the job
41
+ - **Chunked output for RAG**: `extract_corpus` accepts `chunk_size` / `chunk_overlap` and emits one record per chunk with its `section_path`, `chunk_index`, and `chunk_count`. Chunks prefer to break at sentence and paragraph boundaries
42
+ - **Alias sets are checked before they are saved**: `save_alias_set` refuses a group whose headings frequently appear together in the same article a sign they are different sections rather than synonyms unless you pass `force`
43
+ - **`discover_aliases` prompt**: a bundled walkthrough of finding heading variants (`section_stats` → `section_cooccurrence` → save), so you do not have to remember the steps
44
+ - **MCP server (`wp2txt-mcp`)**: a new command that exposes a local dump to an LLM client over the Model Context Protocol. Tools cover dump identity, single articles and their sections, filtered listings, category trees, heading statistics, alias sets, and extraction to JSONL with a `.meta.json` recording the dump version and query. Requires the `mcp` gem (`gem install mcp`); wp2txt itself does not depend on it
45
+ - **Section aliases come from the dump, not from a bundled dictionary**: find the headings actually used with `section_stats`, check whether two of them mean the same thing with `section_cooccurrence`, and save the group with `save_alias_set`. Queries then refer to it by name via `alias_set`, and extractions record which group was used
46
+ - **Ruby API**: `Wp2txt::Corpus` provides the query and extraction layer used by the MCP server, for embedding wp2txt in your own code. Title lookup reads from SQLite on demand rather than loading a multi-million-entry index into memory
47
+ - **Offline metadata index (`--build-index`)**: builds a local index of a dump each article's categories, section headings, redirects, and the category hierarchy — by scanning it in parallel. Stored under `~/.wp2txt/cache/` and tied to the dump version. No API access needed
48
+ - **Offline queries over a whole edition (`--find-articles`)**: list articles by `--in-category` (with `--depth` to include subcategories), `--has-section` (alias-aware), and `--title-match`. Supports `--limit` and JSON output (`-j json`); redirects are excluded. This is what makes questions like "all film articles that have a plot section" answerable against a pinned dump
43
49
 
44
50
  ## [2.1.2] - 2026-07-19
45
51
 
46
- - **Fixed gem file permissions**: The published gem contained files with owner-only (0600) permissions inherited from the build machine, making them unreadable after `sudo gem install`. A `normalize_permissions` task now runs before `rake build`, ensuring all packaged files are world-readable (0644, or 0755 for executables)
52
+ - **Fixed unreadable files after `sudo gem install`**: files in the published gem carried owner-only permissions, so wp2txt failed to run for anyone but the installing user. Packaged files are now readable by everyone (0644, and 0755 for executables). If you hit this, reinstall the gem
47
53
 
48
54
  ## [2.1.1] - 2026-02-21
49
55
 
50
- - **Bidirectional alias matching**: Section extraction now supports reverse alias lookup - specifying an alias name (e.g., "Synopsis") as target matches the canonical heading ("Plot") and vice versa
51
- - **Expanded default section aliases**: Increased from 2 to 12 alias groups covering common English Wikipedia sections (Plot, Reception, References, Bibliography, Awards, Legacy, Early life, Career, etc.)
52
- - **Config forwarding fix**: `--pre`, `--ref`, `--expand-templates`, and `--metadata-only` options now correctly forwarded in `--articles` and `--from-category` modes
56
+ - **Section aliases match in both directions**: asking for "Synopsis" now also finds sections headed "Plot", and the other way round, instead of only matching the name you happened to type
57
+ - **More section aliases out of the box**: 12 groups instead of 2, covering common English Wikipedia sections (Plot, Reception, References, Bibliography, Awards, Legacy, Early life, Career, and others)
58
+ - **Fixed options being ignored**: `--pre`, `--ref`, `--expand-templates`, and `--metadata-only` had no effect when used with `--articles` or `--from-category`. They now apply in those modes too
53
59
 
54
60
  ## [2.1.0] - 2026-02-19
55
61
 
56
- - **SQLite-based caching infrastructure**: New high-performance caching using SQLite for faster startup and repeated operations:
57
- - `GlobalDataCache`: Caches parsed JSON data files (templates, MediaWiki aliases, HTML entities)
58
- - Eliminates ~500KB JSON parsing overhead on each startup
59
- - Validates cache against source file modification time and size
60
- - Location: `~/.wp2txt/cache/global_data.sqlite3`
61
- - `CategoryCache`: Caches Wikipedia category hierarchy from API
62
- - Stores category members (pages and subcategories) in SQLite tables
63
- - Supports recursive tree traversal and bulk page retrieval
64
- - Per-language cache files: `~/.wp2txt/cache/categories_en.sqlite3`
65
- - Configurable expiry (default: 7 days)
66
- - `IndexCache`: Caches parsed multistream index (already existed, now with SQLite3 2.x compatibility)
67
- - Reduces index parsing from ~10 minutes to seconds on subsequent runs
68
- - All caches use WAL mode for concurrent read access during parallel processing
69
-
70
- - **Ractor parallel processing (Ruby 4.0+)**: New `--ractor` option for thread-based parallelism:
71
- - Requires Ruby 4.0 or later for stable operation
72
- - Uses map-join-value pattern for reliable Ractor orchestration
73
- - ~2x speedup compared to sequential processing
74
- - Lower memory footprint than process-based parallelism (Parallel gem)
75
- - Automatic fallback to sequential processing on Ruby 3.x
76
- - Performance: Parallel gem (~3x) remains faster, Ractor (~2x) uses less memory
77
-
78
- - **Template expansion**: New `--expand-templates` (`-E`) option expands common templates to readable text:
79
- - Date templates: `{{birth date|1990|5|15}}` → "May 15, 1990"
80
- - Convert templates: `{{convert|100|km|mi}}` → "100 km (62 mi)"
81
- - Coordinate templates: `{{coord|35|41|N|139|41|E}}` → "35°41′N 139°41′E"
82
- - Language templates: `{{lang|ja|日本語}}` → "日本語"
83
- - Quote templates: `{{blockquote|text}}` → "text"
84
- - And 20+ more template types
85
- - **Enabled by default** - use `--no-expand-templates` to disable
86
- - Parser functions support: `{{#if:}}`, `{{#switch:}}`, `{{#ifeq:}}`, `{{#expr:}}`
87
- - Magic words support: `{{PAGENAME}}`, `{{CURRENTYEAR}}`, `{{NAMESPACE}}`
88
-
89
- - **Removed legacy test data**: Deleted obsolete static test files:
90
- - `data/testdata_en.bz2` (2.8MB, from 2022)
91
- - `data/testdata_ja.bz2` (2.6MB, from 2022)
92
- - `data/output_samples/` directory (~20MB)
93
- - Tests now use live Wikipedia data with caching
94
-
95
- - **Incremental dump downloads**: Smart handling of partial dump files when downloading full dumps:
96
- - Detects existing partial downloads and offers to resume (download only remaining data)
97
- - Validates dump dates - if dates match, can resume; if outdated, offers choices
98
- - User options: resume download, download fresh, keep old partial, or use old as-is
99
- - Automatic bz2 validation before and after incremental download
100
- - Falls back to full download if server doesn't support HTTP Range headers
101
-
102
- - **bz2 file validation**: New `Bz2Validator` module detects corrupt or invalid bz2 files before processing:
103
- - Validates magic bytes (`BZ`), version byte (`h`), and block size (`1`-`9`)
104
- - Optional decompression test to verify file integrity
105
- - `StreamProcessor` validates bz2 files by default (configurable via `validate_bz2: false`)
106
- - Detailed error types: `not_found`, `too_small`, `invalid_magic`, `invalid_version`, `invalid_block_size`, `decompression_failed`
107
-
108
- - **Memory monitoring**: New `MemoryMonitor` module for adaptive resource management:
109
- - Cross-platform memory detection (Linux, macOS, Windows)
110
- - Adaptive buffer sizing based on available memory
111
- - Memory statistics: `current_memory_usage`, `available_memory`, `memory_usage_percent`
112
- - Automatic garbage collection when memory is low
113
-
114
- - **Parallel article extraction**: `MultistreamReader` now supports parallel processing:
115
- - `extract_articles_parallel(titles, num_processes: 4)` - Extract multiple articles in parallel
116
- - `each_article_parallel(entries, num_processes: 4)` - Iterate with parallel processing
117
- - Automatically groups articles by stream offset to minimize bz2 decompression overhead
118
-
119
- - **Performance optimizations**:
120
- - Pre-compiled 14 additional regex patterns for text cleanup
121
- - Consolidated gsub chains (3 fewer calls per cleanup operation)
122
- - Adaptive buffer sizing in `StreamProcessor` based on system memory
123
-
124
- - **Cache staleness warnings**: Cache status now shows age and staleness information:
125
- - Displays cache date and age (e.g., "2025-01-05 - 4 days ago")
126
- - Warns when cache exceeds configured `dump_expiry_days` (default: 30 days)
127
- - New `--update-cache` (`-U`) option to force refresh of cached dump files
128
- - Users can choose to use stale cache or force update
129
-
130
- - **Category-based extraction**: New `--from-category` option extracts all articles from a Wikipedia category:
131
- - `wp2txt --lang=ja --from-category="日本の都市" -o ./output` extracts all articles in the category
132
- - `--depth` option for subcategory recursion (e.g., `--depth=2` includes 2 levels of subcategories)
133
- - `--dry-run` for preview mode (shows article counts without downloading)
134
- - `--yes` to skip confirmation prompt for automation
135
- - Circular reference prevention for category hierarchies
136
- - Rate limiting for Wikipedia API requests
137
-
138
- - **Configuration file**: New `--config-init` option creates persistent configuration:
139
- - Settings stored in `~/.wp2txt/config.yml`
140
- - Configurable: `dump_expiry_days`, `category_expiry_days`, `cache.directory`
141
- - Default output format and subcategory depth
142
- - CLI options override config file settings
143
-
144
- - **Deprecated `--markers=none`**: Complete removal of special content is now deprecated
145
- - Removing inline content (e.g., math formulas) makes surrounding text nonsensical
146
- - `--markers=none` now shows a warning and behaves like `--markers=all`
147
- - Use `--markers=math,code` to show only specific marker types
148
-
149
- - **CLI option validation**: Extraction modes are now mutually exclusive with clear error messages:
150
- - `--category-only`, `--summary-only`, `--metadata-only` cannot be combined
151
- - `--sections` cannot be used with extraction modes
152
- - `--section-stats` cannot be combined with extraction modes or `--sections`
153
-
154
- - **Network retry with exponential backoff**: HTTP requests now retry on transient errors:
155
- - Retries up to 3 times with exponential backoff (2, 4, 8 seconds)
156
- - Handles timeouts, connection resets, and DNS failures
157
- - CategoryFetcher API requests now log failures instead of silently returning nil
158
-
159
- - **Disk full error handling**: OutputWriter now handles `Errno::ENOSPC` gracefully:
160
- - Raises `Wp2txt::FileIOError` with descriptive message on disk full or I/O errors
161
- - Properly closes file handles before raising
162
-
163
- - **File rotation at article boundaries**: OutputWriter `write_from_file` now rotates output files only at blank lines (article boundaries):
164
- - Prevents articles from being split across output files
165
- - Eliminates UTF-8 character corruption at file boundaries (e.g., 3-byte Japanese characters split mid-byte)
166
- - Uses line-by-line reading (`each_line` with `"r:UTF-8"`) instead of fixed-size byte chunks
167
- - Verified with full Japanese Wikipedia (1.49M articles) and English Wikipedia (24.2 GB) dumps
168
-
169
- - **HTTP timeout consistency**: All HTTP methods in `DumpManager` now use `DEFAULT_HTTP_TIMEOUT`:
170
- - Added `open_timeout`/`read_timeout` to `download_incremental`, `get_remote_file_size`, `download_file_with_progress`, `download_file_range`
171
- - Previously these methods had no timeout, risking indefinite hangs on network issues
172
-
173
- - **Security: Command injection prevention**: All `IO.popen` calls now use array form:
174
- - Fixed unsafe string interpolation in `wp2txt.rb`, `stream_processor.rb`, `bz2_validator.rb`, `memory_monitor.rb`
175
- - Prevents shell metacharacter interpretation in file paths
176
-
177
- - **Security: SSL certificate verification**: Restored proper TLS certificate validation:
178
- - Removed `verify_callback` that unconditionally returned `true` (7 locations in `multistream.rb`)
179
- - `VERIFY_PEER` now performs actual certificate verification
180
-
181
- - **Security: Temp file handling**: `file_mod` now uses `Tempfile` instead of hardcoded `"temp"` filename:
182
- - Prevents predictable file names and potential race conditions
183
- - Temp files created in same directory as target file
184
-
185
- - **CLI option fixes**:
186
- - Added missing `--table` option (keep wiki table content)
187
- - Added missing `--multiline` option (keep multi-line templates)
188
- - Added missing `--pre` option (keep preformatted text blocks)
189
- - Fixed `--ref` option not being transferred to processing config
190
- - Reference removal is now conditional (respects `--ref` flag)
191
-
192
- - **Ractor turbo mode warning**: Shows explicit warning when `--ractor` is used with turbo mode (unsupported combination)
193
-
194
- - **Constants extraction**: Replaced magic numbers with named constants:
195
- - `DEFAULT_HTTP_TIMEOUT`, `DEFAULT_PROGRESS_INTERVAL`, `INDEX_PROGRESS_THRESHOLD`
196
- - `DEFAULT_TOP_N_SECTIONS`, `RESUME_METADATA_MAX_AGE_DAYS`, `MAX_HTTP_RETRIES`
197
-
198
- - **Marker classification**: Markers now categorized as inline or block
199
- - **Inline markers** (`[MATH]`, `[CODE]`, `[CHEM]`, `[IPA]`): Content that appears mid-sentence; removal would break grammar
200
- - **Block markers** (`[TABLE]`, `[CODEBLOCK]`, `[INFOBOX]`, etc.): Standalone content that can be safely removed
201
- - New `[CODEBLOCK]` marker for `<syntaxhighlight>`, `<source>`, `<pre>` tags (block-level code)
202
- - `[CODE]` marker now only applies to inline `<code>` tags
62
+ - **Faster startup and repeated runs**: parsed data files, the Wikipedia category hierarchy (per language, 7-day expiry), and the multistream dump index are now cached under `~/.wp2txt/cache/`. Re-reading the index of a full dump used to take ~10 minutes; later runs now start in seconds
63
+ - **`--ractor` (Ruby 4.0+)**: thread-based parallel processing about 2× faster than sequential, with a smaller memory footprint than the default process-based mode (which remains the fastest at ~3×). On Ruby 3.x the option falls back to sequential processing
64
+ - **Templates are expanded into readable text by default**: dates (`{{birth date|1990|5|15}}` → "May 15, 1990"), unit conversions (`{{convert|100|km|mi}}` → "100 km (62 mi)"), coordinates, language tags (`{{lang|ja|日本語}}` → 日本語), quotes, and 20+ more, plus common parser functions (`{{#if:}}`, `{{#switch:}}`) and magic words (`{{PAGENAME}}`). Turn off with `--no-expand-templates`
65
+ - **Interrupted downloads resume**: a partial dump download is detected and, when the server supports it, only the remaining data is fetched. If the dump date has changed you are asked whether to resume, redownload, or keep the old file; files are validated before and after
66
+ - **Corrupt dump files are detected before processing** instead of failing partway through, with the specific reason reported (file truncated, not a bz2 file, failed decompression test)
67
+ - **Output files rotate at article boundaries**: an article is never split across two output files, and multi-byte characters are no longer corrupted at file boundaries. Verified against the full Japanese (1.49M articles) and English (24.2 GB) dumps
68
+ - **Downloads can no longer hang indefinitely**: every network operation now has a timeout (several previously had none)
69
+ - **Transient network errors retry automatically** (3 attempts with 2/4/8-second backoff) instead of failing the run, and failed category API requests are logged instead of silently returning nothing
70
+ - **Running out of disk space raises a clear error** instead of leaving corrupt output behind
71
+ - **Cache age is visible**: cache listings show the date and age of each cached dump and warn when it exceeds `dump_expiry_days` (default 30); `--update-cache` (`-U`) forces a refresh
72
+ - **`--from-category`**: extract every article in a Wikipedia category via the Wikipedia API, with `--depth` for subcategory levels, `--dry-run` to preview article counts before downloading, and `--yes` for unattended runs
73
+ - **`--config-init`**: writes a persistent configuration file to `~/.wp2txt/config.yml` (cache lifetimes, cache directory, output defaults). Command-line options still take precedence
74
+ - **`--markers=none` is deprecated**: removing inline content (a formula mid-sentence) leaves broken text, so the option now warns and behaves like `--markers=all`. Use `--markers=math,code` to keep only specific marker types
75
+ - **Markers are classified as inline or block**: inline markers (`[MATH]`, `[CODE]`, `[CHEM]`, `[IPA]`) stand in for content whose removal would break the sentence; block markers (`[TABLE]`, `[INFOBOX]`, …) replace standalone content. New `[CODEBLOCK]` marker for code blocks; `[CODE]` now covers only inline code
76
+ - **Conflicting options are rejected with a clear message**: `--category-only`, `--summary-only`, and `--metadata-only` are mutually exclusive, and `--sections` / `--section-stats` cannot be combined with them
77
+ - **Option fixes**: `--table`, `--multiline`, and `--pre` were missing from the CLI and are now available; `--ref` was accepted but had no effect — reference removal now respects it
78
+ - `--ractor` combined with turbo mode now warns explicitly (the combination is unsupported)
79
+ - **Security fixes**: file paths are no longer passed through a shell, so crafted path names cannot inject commands; TLS certificate verification is actually performed (it was previously disabled by an accept-everything callback); temporary files use unpredictable names instead of a fixed name in the working directory
80
+ - **Performance**: text cleanup is faster (fewer passes over each article), and buffer sizes adapt to the memory actually available on the machine
81
+ - **Ruby API**: articles can be extracted from a multistream dump in parallel (`extract_articles_parallel`, `each_article_parallel`), grouped by stream to avoid decompressing the same block twice
203
82
 
204
83
  ## [2.0.0] - 2026-01-08
205
84
 
206
85
  ### Added
207
86
 
208
- - **Auto-download mode**: New `--lang` option automatically downloads Wikipedia dumps:
209
- - `wp2txt --lang=ja -o ./output` downloads and processes Japanese Wikipedia
210
- - Downloads cached to `~/.wp2txt/cache/` for reuse
211
- - Supports any Wikipedia language code (en, ja, de, fr, zh, etc.)
212
-
213
- - **Article extraction**: New `--articles` option extracts specific articles by title:
214
- - `wp2txt --lang=en --articles="Tokyo,Kyoto,Osaka" -o ./articles`
215
- - Only downloads index + needed data streams (efficient partial download)
216
- - O(1) hash lookup for article search
217
-
218
- - **Cache management**: New options to manage downloaded dumps:
219
- - `--cache-status` - Show cache status for all languages
220
- - `--cache-clear` - Clear all cache
221
- - `--cache-clear --lang=ja` - Clear cache for specific language
222
- - `--cache-dir` - Custom cache directory
223
-
224
- - **Content type markers**: New `--markers` option marks special content:
225
- - Supported types: `[MATH]`, `[CODE]`, `[CHEM]`, `[TABLE]`, `[SCORE]`, `[TIMELINE]`, `[GRAPH]`, `[IPA]`, `[INFOBOX]`, `[NAVBOX]`, `[GALLERY]`, `[SIDEBAR]`, `[MAPFRAME]`, `[IMAGEMAP]`, `[REFERENCES]`
226
- - `--markers=all` (default) - Enable all markers
227
- - `--markers=none` - Disable markers (content removed)
228
- - `--markers=math,code` - Enable specific markers only
229
-
230
- - **Citation extraction**: New `--extract-citations` (`-C`) option for formatted bibliography output:
231
- - Extracts author, title, and year from `{{cite book}}`, `{{cite web}}`, `{{Citation}}` templates
232
- - Formats citations as "Author. \"Title\". Year."
233
- - Available via CLI (`--extract-citations`) and Ruby API (`extract_citations: true`)
234
-
235
- - **Multistream support**: New classes for efficient Wikipedia dump processing:
236
- - `MultistreamIndex` - Parse multistream index files
237
- - `MultistreamReader` - Extract articles from multistream dumps
238
- - `DumpManager` - Download and cache dump files
239
- - Enables targeted article extraction without downloading full dump
240
-
241
- - **Validation framework**: New rake tasks for validating Wikipedia dump processing:
242
- - `testdata:prepare[lang,level]` - Download and cache test data
243
- - `validate:run[lang,level]` - Run validation on cached data
244
- - `validate:full[lang]` - Full dump validation
245
-
246
- - **HTML entity management**: Comprehensive entity support from authoritative sources:
247
- - 2125 entities from WHATWG HTML specification (`html_entities.json`)
248
- - Wikipedia-specific entities (`wikipedia_entities.json`): `&ratio;`, `&dash;`, `&nbso;`
249
- - New script `scripts/fetch_html_entities.rb` to update from WHATWG
250
- - Replaces hardcoded entity list with data-driven approach
251
-
252
- - **MediaWiki data auto-generation**: Magic words and namespace aliases fetched from all Wikipedia APIs:
253
- - New script `scripts/fetch_mediawiki_data.rb` queries 350+ Wikipedia language editions
254
- - Data stored in `lib/wp2txt/data/mediawiki_aliases.json`
255
- - 176 redirect keywords, 231 category aliases, 313 file aliases
256
- - Run `ruby scripts/fetch_mediawiki_data.rb` to update
257
-
258
- - **JSON/JSONL output format**: New `--format json` option outputs articles as JSONL (one JSON object per line) with `title`, `categories`, `text`, and `redirect` fields. Ideal for data pipelines and machine learning workflows.
259
-
260
- - **Streaming processing**: Complete rewrite of the processing architecture:
261
- - No longer creates intermediate XML files
262
- - Directly streams from bz2 compressed files
263
- - Reduced disk I/O and storage requirements
264
- - New `StreamProcessor` and `OutputWriter` classes for modular design
265
-
266
- - **Regex cache**: Dynamic regex patterns are now cached to avoid repeated compilation
267
-
268
- - **Multilingual category support**: Added support for category namespaces in 30+ languages (European, Cyrillic, Asian, Middle Eastern)
269
-
270
- - **Multilingual redirect support**: Added support for redirect keywords in 25+ languages
271
-
272
- - **Comprehensive test suite**: 395 tests covering:
273
- - Unicode handling (CJK, Cyrillic, Arabic, emoji)
274
- - Edge cases (deeply nested templates, malformed markup)
275
- - Multilingual category and redirect extraction
276
- - Text processing utilities
277
- - Integration tests with real Wikipedia content
278
-
279
- - **SimpleCov integration**: Added code coverage reporting for development
280
-
281
- - **Ruby 4.0 compatibility**: Full support for Ruby 4.0
87
+ - **`--lang`**: downloads the Wikipedia dump for any language code and processes it in one step (`wp2txt --lang=ja -o ./output`). Downloads are cached under `~/.wp2txt/cache/` for reuse
88
+ - **`--articles`**: extract specific articles by title (`wp2txt --lang=en --articles="Tokyo,Kyoto,Osaka" -o ./articles`), downloading only the index and the data streams that contain them rather than the whole dump
89
+ - **Cache management**: `--cache-status` (per-language overview), `--cache-clear` (all languages, or one with `--lang`), and `--cache-dir` (custom location)
90
+ - **Content markers (`--markers`)**: special content is replaced by a marker instead of disappearing silently: `[MATH]`, `[CODE]`, `[CHEM]`, `[TABLE]`, `[SCORE]`, `[TIMELINE]`, `[GRAPH]`, `[IPA]`, `[INFOBOX]`, `[NAVBOX]`, `[GALLERY]`, `[SIDEBAR]`, `[MAPFRAME]`, `[IMAGEMAP]`, `[REFERENCES]`. `--markers=all` is the default; `--markers=math,code` keeps only the listed types
91
+ - **Citation extraction (`--extract-citations`, `-C`)**: outputs a formatted bibliography ("Author. \"Title\". Year.") from `{{cite book}}`, `{{cite web}}`, and `{{Citation}}` templates. Also available from the Ruby API (`extract_citations: true`)
92
+ - **JSON/JSONL output (`--format json`)**: one JSON object per line with `title`, `categories`, `text`, and `redirect` fields, for data pipelines and machine-learning workflows
93
+ - **Multistream dumps are handled natively**, which is what makes targeted extraction work without downloading a full dump. Ruby API: `MultistreamIndex`, `MultistreamReader`, `DumpManager`
94
+ - **HTML character entities are converted comprehensively**: 2,125 entities from the WHATWG HTML specification, plus Wikipedia-specific ones (`&ratio;`, `&dash;`, `&nbso;`)
95
+ - **Redirect, category, and file keywords fetched from 350+ Wikipedia language editions** (176 redirect keywords, 231 category aliases, 313 file aliases), replacing a small hardcoded list
96
+ - **Categories are recognized in 30+ languages** (European, Cyrillic, Asian, and Middle Eastern scripts) and **redirects in 25+**
97
+ - **Fully streamed processing**: dumps are processed directly from the compressed file, with no intermediate XML files — far less disk space and I/O than before
98
+ - **Ruby 4.0 support**
282
99
 
283
100
  ### Changed
284
101
 
285
- - **Performance improvements**:
286
- - `format_wiki`: Reduced intermediate string allocations by using `gsub!` for in-place modifications
287
- - `cleanup`: Optimized with `gsub!` to reduce memory allocations
288
- - `remove_complex`, `make_reference`: Optimized with `gsub!`
289
- - Category deduplication: Changed from O(n²) to O(n) by calling `uniq!` once at end instead of every line
290
- - `correct_separator`: Uses `tr` instead of `gsub` for single character replacement
291
- - `remove_inbetween`: Dynamic regex patterns are now cached
292
-
293
- - **BREAKING**: `REMOVE_HR_REGEX` now matches 4 or more hyphens (previously 3+) to align with MediaWiki specification where `----` is the minimum for horizontal rules
294
-
295
- - **`chrref_to_utf` function**: Completely rewritten to support all Unicode codepoints (U+0001 to U+10FFFF), including:
296
- - Supplementary plane characters (emoji, CJK Extension B, etc.)
297
- - Proper handling of invalid codepoints (returns empty string)
298
-
299
- - **`convert_characters` function**: Now uses `String#scrub` for safe handling of invalid UTF-8 sequences instead of calling `exit`
300
-
301
- - **`command_exist?` function**: Updated to use `IO.popen` instead of `open("| ...")` for Ruby 4.0 compatibility
102
+ - **BREAKING: horizontal rules now require 4 or more hyphens** (previously 3), matching the MediaWiki specification — a line of `---` in an article is now treated as content, not as a rule
103
+ - **Character references convert across the entire Unicode range** (U+0001–U+10FFFF), including emoji and supplementary-plane CJK; invalid codepoints become empty strings instead of garbage characters
104
+ - **Faster text processing**: category deduplication went from quadratic to linear time, and string handling allocates far less memory on large articles
302
105
 
303
106
  ### Fixed
304
107
 
305
- - **Unicode BMP limitation**: Fixed `chrref_to_utf` to correctly convert character references beyond the Basic Multilingual Plane (U+FFFF). Previously, emoji like `&#x1F600;` would produce invalid characters.
306
-
307
- - **Encoding error crash**: Fixed `convert_characters` which previously called `exit` on encoding errors, now gracefully handles invalid byte sequences using `scrub`
308
-
309
- - **Horizontal rule detection**: Fixed `REMOVE_HR_REGEX` to correctly match MediaWiki horizontal rules (4+ hyphens)
310
-
311
- - **Heading regex**: Fixed `IN_HEADING_REGEX` to allow trailing whitespace after closing equal signs
312
-
313
- - **Ruby 4.0 compatibility**: Fixed `open("| which cmd")` pattern which no longer works in Ruby 4.0
108
+ - **Character references beyond U+FFFF now produce the right character**: `&#x1F600;` becomes 😀 instead of an invalid character
109
+ - **Invalid UTF-8 in a dump no longer aborts the run**: bad byte sequences are scrubbed and processing continues (the tool previously exited mid-run)
110
+ - **Headings with trailing whitespace** after the closing `==` are now recognized as headings
314
111
 
315
112
  ### Deprecated
316
113
 
317
- - **`--convert` / `-c` option**: No longer needed as streaming processing always converts
318
- - **`--del-interfile` / `-x` option**: No longer needed as intermediate files are no longer created
114
+ - **`--convert` / `-c`**: no longer needed streaming processing always converts
115
+ - **`--del-interfile` / `-x`**: no longer needed intermediate files are no longer created
319
116
 
320
117
  ### Removed
321
118
 
322
- - **Intermediate XML file creation**: The `Splitter` class no longer creates intermediate XML files; processing is now fully streamed
119
+ - **Intermediate XML files**: processing no longer writes intermediate XML next to the output at any point
323
120
 
324
121
  ### Security
325
122
 
data/DEVELOPMENT.md CHANGED
@@ -383,6 +383,14 @@ info = Wp2txt::Bz2Validator.file_info("/path/to/file.bz2")
383
383
  2. Redirect keywords: `data/language_redirects.json`
384
384
  3. Scripts: `scripts/generate_language_data.rb`
385
385
 
386
+ ## Documentation
387
+
388
+ User-facing documents (README, README_ja, CHANGELOG, docs/) describe what a user can
389
+ observe and what they need to do — not how the code works internally or why it is
390
+ designed the way it is. Keep implementation detail in this file and in code comments,
391
+ and keep design rationale out of the public repository entirely. The MCP tool table in
392
+ docs/INDEXES.md is checked against the actual server surface by spec/docs_sync_spec.rb.
393
+
386
394
  ## Code Style
387
395
 
388
396
  - Ruby 2.6+ compatibility
data/DEVELOPMENT_ja.md CHANGED
@@ -383,6 +383,13 @@ info = Wp2txt::Bz2Validator.file_info("/path/to/file.bz2")
383
383
  2. リダイレクトキーワード: `data/language_redirects.json`
384
384
  3. スクリプト: `scripts/generate_language_data.rb`
385
385
 
386
+ ## ドキュメンテーション
387
+
388
+ 利用者向け文書(README, README_ja, CHANGELOG, docs/)には、利用者が観測できることと
389
+ 利用者が取るべき行動だけを書く — コードが内部でどう動くか、なぜそう設計したかは書かない。
390
+ 実装の詳細は本ファイルとコードコメントに、設計の理由づけは公開リポジトリの外に置く。
391
+ docs/INDEXES.md の MCP ツール表は spec/docs_sync_spec.rb が実際のサーバー表面と照合する。
392
+
386
393
  ## コードスタイル
387
394
 
388
395
  - Ruby 2.6+互換性
data/README.md CHANGED
@@ -255,12 +255,12 @@ By default, citation templates are removed. Use `--extract-citations` to extract
255
255
 
256
256
  Supported: `{{cite book}}`, `{{cite web}}`, `{{cite news}}`, `{{cite journal}}`, `{{Citation}}`, etc.
257
257
 
258
- ## Research Infrastructure (Indexes, Exhaustive Queries, MCP)
258
+ ## Offline Indexes, Queries, and the MCP Server
259
259
 
260
- Beyond text extraction, wp2txt can turn a dump into a **local, version-pinned research
261
- database**: SQLite indexes over categories, section headings, redirects, and (optionally)
262
- the full article text, plus interlanguage links for cross-edition comparison — all
263
- queryable offline, exhaustively, and exposed to LLM agents via an MCP server.
260
+ Beyond text extraction, wp2txt can turn a dump into a **local, version-pinned database**:
261
+ SQLite indexes over categories, section headings, redirects, and (optionally) the full
262
+ article text, plus interlanguage links for cross-edition comparison — all queryable
263
+ offline, and usable from an LLM client via an MCP server.
264
264
 
265
265
  ```console
266
266
  $ wp2txt --build-index --fulltext --lang=ja # build the indexes
@@ -270,13 +270,12 @@ $ wp2txt --import-langlinks -L ja --langlinks-langs en,de,fr,zh,ko
270
270
  $ wp2txt-mcp --lang=ja # stdio MCP server for LLM agents
271
271
  ```
272
272
 
273
- Unlike web/API access, these queries scan every article (a `0 matches` result is a
274
- verifiable absence claim for that dump version) and are reproducible: extractions record
275
- the dump version and query in a `.meta.json` sidecar.
273
+ These queries scan every article `0 matches` means the term is absent from that dump
274
+ version and extractions record the dump version and query in a `.meta.json` sidecar,
275
+ so results can be reproduced later.
276
276
 
277
- **→ See the [Research Infrastructure Guide](docs/RESEARCH.md)** for index building,
278
- exhaustive queries, full-text search, interlanguage links, cross-language SQL, the full
279
- MCP tool list, and design principles.
277
+ **→ See [docs/INDEXES.md](docs/INDEXES.md)** for index building, offline queries,
278
+ full-text search, interlanguage links, cross-language SQL, and the full MCP tool list.
280
279
 
281
280
  ## Command Line Options
282
281
 
@@ -354,11 +353,11 @@ MCP tool list, and design principles.
354
353
  ### Research infrastructure options
355
354
 
356
355
  --build-index Build the metadata index (add --fulltext for FTS)
357
- --find-articles / --search Exhaustive offline queries (see docs/RESEARCH.md)
356
+ --find-articles / --search Offline queries over a whole edition (see docs/INDEXES.md)
358
357
  --import-langlinks Import interlanguage links (version-matched)
359
358
  --fts-optimize Optimize an existing full-text index
360
359
 
361
- See the [Research Infrastructure Guide](docs/RESEARCH.md) for details.
360
+ See [docs/INDEXES.md](docs/INDEXES.md) for details.
362
361
 
363
362
  ## Configuration File
364
363
 
data/README_ja.md CHANGED
@@ -333,9 +333,9 @@ defaults:
333
333
 
334
334
  コマンドラインオプションは設定ファイルの設定を上書きします。
335
335
 
336
- ## 研究基盤(索引・悉皆クエリ・MCP
336
+ ## オフライン索引・クエリ・MCPサーバー
337
337
 
338
- テキスト抽出に加えて、wp2txtはダンプを**ローカルな版固定研究データベース**に変換できます。
338
+ テキスト抽出に加えて、wp2txtはダンプを**ローカルな版固定データベース**に変換できます。
339
339
  カテゴリ・節見出し・リダイレクト・(オプションで)記事全文のSQLite索引、言語版横断比較のための
340
340
  言語間リンク(langlinks)— すべてオフラインで悉皆的にクエリでき、MCPサーバー経由で
341
341
  LLMエージェントにも公開できます。
@@ -348,12 +348,12 @@ $ wp2txt --import-langlinks -L ja --langlinks-langs en,de,fr,zh,ko
348
348
  $ wp2txt-mcp --lang=ja # LLMエージェント向けMCPサーバー
349
349
  ```
350
350
 
351
- web/API アクセスと異なり、これらのクエリは全記事を走査します(`0件` は当該ダンプ版に対する
352
- 検証可能な不在の言明になります)。抽出結果には dump 版とクエリを記録した `.meta.json`
353
- サイドカーが付き、再現可能です。
351
+ これらのクエリは全記事を走査します(`0件` は当該ダンプ版にその語が存在しないことを
352
+ 意味します)。抽出結果には dump 版とクエリを記録した `.meta.json` サイドカーが付き、
353
+ あとから再現できます。
354
354
 
355
- **→ 詳細は [Research Infrastructure Guide](docs/RESEARCH.md)(英語)を参照**:
356
- 索引構築、悉皆クエリ、全文検索、言語間リンク、言語版横断SQL、MCPツール一覧、設計原則。
355
+ **→ 詳細は [docs/INDEXES.md](docs/INDEXES.md)(英語)を参照**:
356
+ 索引構築、オフラインクエリ、全文検索、言語間リンク、言語版横断SQL、MCPツール一覧。
357
357
 
358
358
  ## パフォーマンス
359
359
 
data/Rakefile CHANGED
@@ -34,7 +34,7 @@ Rake::Task["build"].enhance([:normalize_permissions])
34
34
 
35
35
  # Paths that must never reach a published image. The image is built from the
36
36
  # working tree, so anything ignored locally (private notes, scratch files)
37
- # would otherwise ride along; 2.3.0's images shipped research-notes/ this way.
37
+ # would otherwise ride along.
38
38
  IMAGE_FORBIDDEN_PATHS = %w[/wp2txt/research-notes /wp2txt/tmp /wp2txt/.git /wp2txt/CLAUDE.md /wp2txt/.claude].freeze
39
39
 
40
40
  desc "Verify a built image contains no private material (run before pushing)"
@@ -1,40 +1,31 @@
1
- # wp2txt Research Infrastructure Guide
1
+ # Offline Indexes, Queries, and the MCP Server
2
2
 
3
- This guide covers the research-oriented layer of wp2txt: local indexes over Wikipedia
4
- dumps, exhaustive offline queries, full-text search, cross-language SQL, and the MCP
5
- server that exposes all of this to LLM agents.
3
+ This guide covers wp2txt's index-based features: local indexes over Wikipedia dumps,
4
+ offline queries across a whole edition, full-text search, interlanguage links,
5
+ cross-language SQL, and the MCP server for connecting an LLM client.
6
6
 
7
7
  For plain-text extraction (the classic wp2txt), see the [README](../README.md).
8
8
 
9
- ## Concept
10
-
11
- Web search and the Wikipedia API operate on ranked, paginated, ever-changing data. They
12
- can show that something *exists*, but they cannot make **exhaustive** claims ("342 of the
13
- 11,486 film articles with a plot section mention X and none of the others do"), and
14
- their answers change from day to day.
15
-
16
- wp2txt takes the opposite approach: build local indexes over an official dump file, so that
17
- every query is
18
-
19
- - **exhaustive** — it scans every article, not search-ranked results;
20
- - **version-pinned** — results are tied to one dump (e.g. `jawiki-20260701`) and
21
- reproducible later;
22
- - **agent-operable** — the MCP server exposes the whole layer to LLM agents, with
23
- guardrails and provenance records designed for autonomous use.
9
+ Everything below runs against a downloaded dump file: queries cover every article of the
10
+ edition rather than a page of search results, results are tied to one dump (e.g.
11
+ `jawiki-20260701`) and can be reproduced later, and nothing goes over the network once
12
+ the dump is downloaded. Typical uses: "which of the 1.5M articles have a plot section",
13
+ "how many film articles mention X, and which ones don't", "how do the section structures
14
+ of the same article differ between the English and Japanese editions".
24
15
 
25
16
  ## 1. Building the indexes
26
17
 
27
18
  ```console
28
- # Tier 1: metadata index (categories, section headings, redirects, category hierarchy)
19
+ # Metadata index (categories, section headings, redirects, category hierarchy)
29
20
  $ wp2txt --build-index --lang=ja
30
21
 
31
- # Tier 1 + Tier 2: add an FTS5 full-text index over the cleaned article text
22
+ # Metadata index + FTS5 full-text index over the cleaned article text
32
23
  $ wp2txt --build-index --fulltext --lang=ja
33
24
  ```
34
25
 
35
26
  The dump is downloaded automatically if needed and everything is cached under
36
27
  `~/.wp2txt/cache/`. Ballpark figures (Apple Silicon laptop): Japanese Wikipedia ~15 min /
37
- ~1.7 GB for the metadata index, ~1.5 h / ~12 GB with full text; English roughly 4× the
28
+ ~1.7 GB for the metadata index, ~1.5 h / ~12 GB with full text; English roughly 2.5–3× the
38
29
  time, ~10 GB / ~12 GB respectively.
39
30
 
40
31
  The full-text tokenizer is selected per language: character trigrams for Japanese,
@@ -58,8 +49,8 @@ $ wp2txt --find-articles --in-category "Films" -D 2 -j json --limit 100 --lang=e
58
49
  $ wp2txt --search "タイムループ" --in-category "映画作品" -D 3 --lang=ja
59
50
  ```
60
51
 
61
- Search totals are exhaustive counts, so `0 matches` is a verifiable **absence claim** for
62
- that dump version — something ranked web search cannot provide.
52
+ Search totals count every match in the edition, so `0 matches` means the term is absent
53
+ from that dump version — a result you can state and re-verify later.
63
54
 
64
55
  ## 3. Interlanguage links (langlinks)
65
56
 
@@ -114,7 +105,7 @@ $ claude mcp add wp2txt -- docker run -i --rm -v wp2txt:/root/.wp2txt ghcr.io/yo
114
105
 
115
106
  | Tool | Purpose |
116
107
  |------|---------|
117
- | `dump_info` | Dump identity, index tiers, corpus statistics, langlinks provenance |
108
+ | `dump_info` | Dump identity, installed indexes, corpus statistics, langlinks provenance |
118
109
  | `get_article` / `get_sections` / `list_headings` / `get_categories` | Single-article access (redirect-aware) |
119
110
  | `find_articles` | Exhaustive filtered listing (category recursion, category AND / pattern match, section headings, title match) |
120
111
  | `category_tree` / `section_stats` | Scope exploration and heading-frequency discovery |
@@ -126,17 +117,19 @@ $ claude mcp add wp2txt -- docker run -i --rm -v wp2txt:/root/.wp2txt ghcr.io/yo
126
117
  | `extract_corpus` | Filtered or explicit-title extraction to JSONL + reproducibility sidecar; optional RAG chunking |
127
118
  | `start_extract_job` / `job_status` / `cancel_job` / `list_jobs` | Background jobs for large extractions |
128
119
 
129
- ### Design principles
120
+ ### What happens when your assistant uses these tools
130
121
 
131
- - **Division of labor**: the tool does mechanical, exhaustive narrowing and counting;
132
- semantic judgment is left to the LLM. The LLM never has to count.
133
- - **Context economy**: large results go to disk; the model receives a summary plus a
134
- 3-record sample, never the full corpus.
135
- - **Reproducibility**: every extraction and file-writing query records the dump version,
136
- the query, and any alias sets in a `.meta.json` sidecar.
137
- - **Guardrails**: SQL is screened and executed read-only in a killable subprocess;
138
- alias sets are re-verified server-side before saving; output paths are confined to the
139
- server's output directory.
122
+ - **Filtering and counting run over the whole dump**, and your assistant reads the result
123
+ rather than tallying articles itself.
124
+ - **Large results are written to a file**; the reply carries a summary and a short sample.
125
+ Your corpus lands on disk intact instead of being paraphrased through the chat.
126
+ - **Extractions are traceable.** Extractions and file-writing queries leave a `.meta.json`
127
+ next to the output recording the dump version, the query, and any alias sets used, so
128
+ you can reproduce or cite the result later.
129
+ - **The tools cannot change your data.** Queries run read-only and are stopped after 30
130
+ seconds, saved alias sets are re-checked before being stored, and files can only be
131
+ written under the server's output directory — worth knowing if you plan to let an
132
+ assistant work unattended.
140
133
 
141
134
  ## 5. Cross-language SQL
142
135
 
@@ -157,12 +150,12 @@ query_sql(
157
150
  ```
158
151
 
159
152
  Attached databases appear as `{lang}_meta` (and `{lang}_fts` when that language has a
160
- full-text tier) and share the main database's schema. Language codes are validated and
161
- resolved server-side; user SQL can never contain ATTACH itself. The response records what
162
- was attached (dump names included), and flags date mismatches between editions.
153
+ full-text index) and share the main database's schema. You pass language codes, not paths,
154
+ and the SQL you write cannot attach anything itself. The response lists what was attached,
155
+ including each edition's dump name, and flags it when the dates differ.
163
156
 
164
- To the best of our knowledge no other system offers version-pinned, cross-edition SQL over
165
- both metadata **and** article text, fully offline.
157
+ This runs entirely offline against pinned dump versions, so a cross-edition comparison can
158
+ be re-run later and produce the same numbers.
166
159
 
167
160
  ## 6. Large results, explicit sets, and reproducibility
168
161
 
@@ -182,26 +175,34 @@ extract_corpus(titles: ["東京物語", "羅生門", ...], content: "summary",
182
175
  redirect hop, and reports unmatched titles in `not_found` — closing the loop
183
176
  *SQL decides the set → the tool materializes it → the LLM reads it*.
184
177
 
185
- ## 7. The alias discovery loop
186
-
187
- Section headings vary ("Plot" vs "Synopsis"; 「あらすじ」 vs 「ストーリー」). wp2txt ships
188
- no per-language dictionaries. Instead, agents discover aliases from the dump itself:
189
-
190
- 1. `section_stats` — find the actual headings used in a scope
191
- 2. LLM proposes synonym groups
192
- 3. `section_cooccurrence` verify mechanically (true synonyms almost never co-occur in
193
- the same article; a high co-occurrence ratio is evidence *against* the hypothesis)
194
- 4. `save_alias_set` persist the verified groups, re-checked server-side, and recorded
195
- in every extraction that uses them
196
-
197
- The bundled `discover_aliases` MCP prompt walks any agent through this protocol.
198
-
199
- ## 8. Honest limitations
200
-
201
- - Queries operate on the **cleaned-text space**: content replaced by markers
202
- (`[MATH]`, `[CODE]`, `[TABLE]`, …) is not searchable.
203
- - Trigram languages (ja/zh/ko) cannot match queries shorter than 3 characters;
204
- word-based languages have no stemming (`run` ≠ `running`). Both are deliberate:
205
- exact counts and absence claims require predictable matching.
206
- - Categories and links come from the dump itself, as written by editors they inherit
207
- Wikipedia's own inconsistencies, which is precisely what makes them worth studying.
178
+ ## 7. Section alias sets
179
+
180
+ Section headings vary by article and by language ("Plot" vs "Synopsis"; 「あらすじ」 vs
181
+ 「ストーリー」), and wp2txt ships no per-language dictionaries. Three tools manage named
182
+ groups of equivalent headings instead:
183
+
184
+ - `section_stats` lists the headings actually used in a scope, with counts.
185
+ - `section_cooccurrence` reports how often two headings appear in the same article.
186
+ Headings that mean the same thing rarely co-occur, so a high ratio is evidence that
187
+ they are *different* sections (「概要」 and 「あらすじ」 co-occur often not synonyms).
188
+ - `save_alias_set` stores a named group. The group is re-checked against co-occurrence
189
+ before saving; a failing group is not stored and the call returns `saved: false`
190
+ rather than an error. Pass `force` to override, and use `list_alias_sets` to see
191
+ what is stored.
192
+
193
+ Queries then accept `alias_set: "name"` in place of a heading list, and extractions
194
+ record the group's exact contents in their `.meta.json`. The bundled `discover_aliases`
195
+ prompt walks an LLM client through building and saving a set.
196
+
197
+ ## 8. Limitations to keep in mind
198
+
199
+ - Searches run over the **cleaned text**: content that extraction replaces with a marker
200
+ (`[MATH]`, `[CODE]`, `[TABLE]`, …) cannot be matched. A count is a count of the cleaned
201
+ text, not of the raw wikitext.
202
+ - Japanese, Chinese, and Korean indexes cannot match queries shorter than 3 characters.
203
+ Word-based languages match exact forms only — `run` does not find `running`. Plan your
204
+ search terms accordingly, especially when you intend to report a zero result.
205
+ - Categories and interlanguage links are read from the dump as editors wrote them.
206
+ Categories added by a template rather than written in the article text are not visible
207
+ to wp2txt, which can make a category look much smaller than it is on the website —
208
+ check against the article text if a count looks wrong.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wp2txt
4
- VERSION = "2.3.1"
4
+ VERSION = "2.3.2"
5
5
  end
@@ -5,7 +5,7 @@ require "spec_helper"
5
5
  # The MCP tool surface and its public documentation drift apart easily
6
6
  # (the 2.2.0 README shipped with a tool table missing four tools). This spec
7
7
  # pins them together: every tool defined in bin/wp2txt-mcp must appear in the
8
- # docs/RESEARCH.md tool table, and the table must not list phantom tools.
8
+ # docs/INDEXES.md tool table, and the table must not list phantom tools.
9
9
  RSpec.describe "documentation surface sync" do
10
10
  repo_root = File.expand_path("..", __dir__)
11
11
 
@@ -15,9 +15,9 @@ RSpec.describe "documentation surface sync" do
15
15
  end
16
16
 
17
17
  define_method(:documented_tools) do
18
- doc = File.read(File.join(repo_root, "docs", "RESEARCH.md"))
18
+ doc = File.read(File.join(repo_root, "docs", "INDEXES.md"))
19
19
  table = doc[/^### Tools\n(.*?)\n\n/m, 1]
20
- raise "Tools table not found in docs/RESEARCH.md" unless table
20
+ raise "Tools table not found in docs/INDEXES.md" unless table
21
21
 
22
22
  # Tool names live in the first column only (the purpose column may
23
23
  # backtick argument names like `attach`)
@@ -30,10 +30,32 @@ RSpec.describe "documentation surface sync" do
30
30
  expect(defined_tools.size).to be >= 15
31
31
  end
32
32
 
33
- it "documents every MCP tool in docs/RESEARCH.md, with no phantom entries" do
33
+ it "documents every MCP tool in docs/INDEXES.md, with no phantom entries" do
34
34
  missing = defined_tools - documented_tools
35
35
  phantom = documented_tools - defined_tools
36
- expect(missing).to be_empty, "tools not documented in docs/RESEARCH.md: #{missing.join(', ')}"
36
+ expect(missing).to be_empty, "tools not documented in docs/INDEXES.md: #{missing.join(', ')}"
37
37
  expect(phantom).to be_empty, "documented tools that do not exist: #{phantom.join(', ')}"
38
38
  end
39
+
40
+ # Tripwire: tracked files must not contain tokens listed in .private-doc-tokens,
41
+ # an untracked, machine-local file (one substring per line; # starts a comment).
42
+ # The file exists only on machines that maintain such a list; everywhere else
43
+ # (CI, other contributors) this example skips — loudly, so a silently dead
44
+ # check cannot be mistaken for a passing one.
45
+ it "keeps machine-local private tokens out of tracked files" do
46
+ token_file = File.join(repo_root, ".private-doc-tokens")
47
+ skip "SKIPPED: no .private-doc-tokens on this machine — tripwire not checked" unless File.exist?(token_file)
48
+
49
+ tokens = File.readlines(token_file, encoding: "UTF-8")
50
+ .map(&:strip).reject { |t| t.empty? || t.start_with?("#") }
51
+ tracked = `git -C #{repo_root} ls-files -z`.split("\x0")
52
+ hits = tracked.flat_map do |f|
53
+ path = File.join(repo_root, f)
54
+ next [] unless File.file?(path)
55
+
56
+ content = File.read(path, encoding: "BINARY")
57
+ tokens.filter_map { |t| "#{f}: #{t}" if content.include?(t.b) }
58
+ end
59
+ expect(hits).to be_empty, "private tokens found in tracked files:\n #{hits.join("\n ")}"
60
+ end
39
61
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wp2txt
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.1
4
+ version: 2.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yoichiro Hasebe
@@ -217,7 +217,7 @@ files:
217
217
  - Rakefile
218
218
  - bin/wp2txt
219
219
  - bin/wp2txt-mcp
220
- - docs/RESEARCH.md
220
+ - docs/INDEXES.md
221
221
  - image/wp2txt-logo.svg
222
222
  - image/wp2txt.svg
223
223
  - lib/wp2txt.rb