wp2txt 2.3.1 → 2.3.3

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.
data/README.md CHANGED
@@ -84,7 +84,7 @@ docker run -it -v /path/to/localdata:/data ghcr.io/yohasebe/wp2txt
84
84
 
85
85
  The `wp2txt` command is available inside the container. Use `/data` for input/output files.
86
86
 
87
- Images are published to GitHub Container Registry (`ghcr.io/yohasebe/wp2txt`). The former Docker Hub repository (`yohasebe/wp2txt`) is no longer updated as of 2.3.1.
87
+ Images are published to GitHub Container Registry (`ghcr.io/yohasebe/wp2txt`). The former Docker Hub repository has been removed pull from GHCR.
88
88
 
89
89
  **MCP server (no Ruby required on the host):**
90
90
 
@@ -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
@@ -80,7 +80,7 @@ docker run -it -v /path/to/localdata:/data ghcr.io/yohasebe/wp2txt
80
80
 
81
81
  `wp2txt`コマンドはコンテナ内で使用可能です。入出力には`/data`ディレクトリを使用してください。
82
82
 
83
- イメージはGitHub Container Registry(`ghcr.io/yohasebe/wp2txt`)で公開されています。旧 Docker Hub リポジトリ(`yohasebe/wp2txt`)は 2.3.1 以降更新されません。
83
+ イメージはGitHub Container Registry(`ghcr.io/yohasebe/wp2txt`)で公開されています。旧 Docker Hub リポジトリは削除済みです GHCR から pull してください。
84
84
 
85
85
  ## 基本的な使い方
86
86
 
@@ -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
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
+ require "open3"
4
5
  require "rspec/core"
5
6
  require "rspec/core/rake_task"
6
7
  require_relative "./lib/wp2txt/version"
@@ -28,20 +29,25 @@ end
28
29
 
29
30
  Rake::Task["build"].enhance([:normalize_permissions])
30
31
 
32
+ # Pre-release gate: verify the built gem's payload against spec.files and scan
33
+ # it for names, content, and modes that must never ship (code-security protocol).
34
+ Rake::Task["build"].enhance { sh "ruby", "scripts/verify_gem.rb" }
35
+
31
36
  # =============================================================================
32
37
  # Docker
33
38
  # =============================================================================
34
39
 
35
40
  # Paths that must never reach a published image. The image is built from the
36
41
  # 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.
38
- IMAGE_FORBIDDEN_PATHS = %w[/wp2txt/research-notes /wp2txt/tmp /wp2txt/.git /wp2txt/CLAUDE.md /wp2txt/.claude].freeze
42
+ # would otherwise ride along.
43
+ IMAGE_FORBIDDEN_PATHS = %w[/wp2txt/research-notes /wp2txt/tmp /wp2txt/.git /wp2txt/CLAUDE.md /wp2txt/.claude /wp2txt/.private-doc-tokens].freeze
39
44
 
40
45
  desc "Verify a built image contains no private material (run before pushing)"
41
46
  task :verify_image, [:tag] do |_t, args|
42
47
  tag = args[:tag] || "wp2txt-verify:local"
43
48
  checks = IMAGE_FORBIDDEN_PATHS.map { |p| "test -e #{p} && echo LEAK:#{p}" }.join("; ")
44
- out = `docker run --rm #{tag} sh -c '#{checks}; true' 2>&1`
49
+ out, status = Open3.capture2e("docker", "run", "--rm", tag, "sh", "-c", "#{checks}; true")
50
+ abort "Image verification failed for #{tag}: #{out}" unless status.success?
45
51
  leaks = out.lines.grep(/^LEAK:/).map(&:strip)
46
52
  abort "Image #{tag} contains private paths:\n #{leaks.join("\n ")}" unless leaks.empty?
47
53
 
@@ -54,18 +60,18 @@ task :check_image do
54
60
  Rake::Task[:verify_image].invoke
55
61
  end
56
62
 
57
- desc "Build and push Docker images to GHCR (verifies a local build first)"
58
- task push: :check_image do
59
- # Docker Hub was retired after 2.3.0; GHCR (ghcr.io/yohasebe/wp2txt) is the
60
- # only registry. Requires `docker buildx use multiarch` and a ghcr.io login.
61
- sh <<-SCRIPT.strip_heredoc, { verbose: false }
62
- /bin/bash -xeu <<'BASH'
63
- # docker buildx create --name multiarch
64
- # docker buildx use multiarch
65
- # docker buildx inspect --bootstrap
66
- docker buildx build --platform linux/amd64,linux/arm64 \
67
- -t ghcr.io/yohasebe/wp2txt:#{Wp2txt::VERSION} -t ghcr.io/yohasebe/wp2txt:latest \
68
- . --push
69
- BASH
70
- SCRIPT
63
+ desc "Explain how images are published (they are built and pushed by CI)"
64
+ task :push do
65
+ abort <<~MESSAGE
66
+ Images are published by GitHub Actions, not from here.
67
+
68
+ A local build sends this working tree as the build context, so untracked
69
+ files ride along; a runner starts from a clean checkout, where they do not
70
+ exist. Push a v* tag and .github/workflows/publish-image.yml takes over:
71
+
72
+ rake release # tags and pushes (also publishes the gem)
73
+
74
+ To rehearse without publishing, run the workflow from the Actions tab with
75
+ "push" left off. To check a local build, run `rake check_image`.
76
+ MESSAGE
71
77
  end
data/bin/wp2txt CHANGED
@@ -217,7 +217,8 @@ class WpApp
217
217
  next unless title_match
218
218
 
219
219
  title = title_match[1]
220
- next if title.nil? || title.empty? || title.include?(":")
220
+ next if title.nil? || title.empty?
221
+ next unless Wp2txt.namespace_id(page_xml[%r{<ns>(-?\d+)</ns>}, 1]).zero?
221
222
 
222
223
  # Extract text content
223
224
  text_match = TEXT_REGEX.match(page_xml)
@@ -609,7 +610,8 @@ class WpApp
609
610
  next unless title_match
610
611
 
611
612
  title = title_match[1]
612
- next if title.nil? || title.empty? || title.include?(":")
613
+ next if title.nil? || title.empty?
614
+ next unless Wp2txt.namespace_id(page_xml[%r{<ns>(-?\d+)</ns>}, 1]).zero?
613
615
 
614
616
  text_match = TEXT_REGEX.match(page_xml)
615
617
  next unless text_match
data/bin/wp2txt-mcp CHANGED
@@ -73,7 +73,7 @@ def respond(&block)
73
73
  result = block.call
74
74
  MCP::Tool::Response.new([{ type: "text", text: JSON.generate(result) }])
75
75
  rescue ArgumentError => e
76
- MCP::Tool::Response.new([{ type: "text", text: JSON.generate({ error: e.message }) }], error: true)
76
+ MCP::Tool::Response.new([{ type: "text", text: JSON.generate({ error: e.message, code: (e.code if e.respond_to?(:code)) }.compact) }], error: true)
77
77
  rescue StandardError => e
78
78
  MCP::Tool::Response.new([{ type: "text", text: JSON.generate({ error: "#{e.class}: #{e.message}" }) }], error: true)
79
79
  end
@@ -312,7 +312,8 @@ def extract_params(output_path:, content:, titles: nil, sections: nil, alias_set
312
312
  content: content, titles: titles,
313
313
  sections: sections, alias_set: alias_set, category: category, depth: depth,
314
314
  categories: categories, category_match: category_match,
315
- title_match: title_match, limit: limit, chunk_size: chunk_size, chunk_overlap: chunk_overlap }
315
+ title_match: title_match, limit: limit, chunk_size: chunk_size, chunk_overlap: chunk_overlap,
316
+ overwrite: overwrite }
316
317
  end
317
318
 
318
319
  server.define_tool(
@@ -341,7 +342,7 @@ end
341
342
 
342
343
  server.define_tool(
343
344
  name: "cancel_job",
344
- description: "Request cancellation of a running job (takes effect at the next batch boundary; the partial output file remains on disk).",
345
+ description: "Request cancellation of a running job (takes effect at the next batch boundary; staged output is removed).",
345
346
  input_schema: { properties: { job_id: { type: "string" } }, required: ["job_id"] }
346
347
  ) do |job_id:, server_context:|
347
348
  respond { job_manager.cancel(job_id) || { error: "unknown job: #{job_id}" } }
@@ -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,23 @@ $ 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
130
-
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.
120
+ ### What happens when your assistant uses these tools
121
+
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.
133
+ Output confinement assumes a dedicated, trusted output directory: symlink paths are
134
+ rejected, both output and sidecar are exclusively reserved unless overwrite is requested,
135
+ and unique temporary files are renamed on success; concurrent replacement of parent
136
+ directories is outside this guarantee.
140
137
 
141
138
  ## 5. Cross-language SQL
142
139
 
@@ -157,12 +154,12 @@ query_sql(
157
154
  ```
158
155
 
159
156
  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.
157
+ full-text index) and share the main database's schema. You pass language codes, not paths,
158
+ and the SQL you write cannot attach anything itself. The response lists what was attached,
159
+ including each edition's dump name, and flags it when the dates differ.
163
160
 
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.
161
+ This runs entirely offline against pinned dump versions, so a cross-edition comparison can
162
+ be re-run later and produce the same numbers.
166
163
 
167
164
  ## 6. Large results, explicit sets, and reproducibility
168
165
 
@@ -182,26 +179,34 @@ extract_corpus(titles: ["東京物語", "羅生門", ...], content: "summary",
182
179
  redirect hop, and reports unmatched titles in `not_found` — closing the loop
183
180
  *SQL decides the set → the tool materializes it → the LLM reads it*.
184
181
 
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.
182
+ ## 7. Section alias sets
183
+
184
+ Section headings vary by article and by language ("Plot" vs "Synopsis"; 「あらすじ」 vs
185
+ 「ストーリー」), and wp2txt ships no per-language dictionaries. Three tools manage named
186
+ groups of equivalent headings instead:
187
+
188
+ - `section_stats` lists the headings actually used in a scope, with counts.
189
+ - `section_cooccurrence` reports how often two headings appear in the same article.
190
+ Headings that mean the same thing rarely co-occur, so a high ratio is evidence that
191
+ they are *different* sections (「概要」 and 「あらすじ」 co-occur often not synonyms).
192
+ - `save_alias_set` stores a named group. The group is re-checked against co-occurrence
193
+ before saving; a failing group is not stored and the call returns `saved: false`
194
+ rather than an error. Pass `force` to override, and use `list_alias_sets` to see
195
+ what is stored.
196
+
197
+ Queries then accept `alias_set: "name"` in place of a heading list, and extractions
198
+ record the group's exact contents in their `.meta.json`. The bundled `discover_aliases`
199
+ prompt walks an LLM client through building and saving a set.
200
+
201
+ ## 8. Limitations to keep in mind
202
+
203
+ - Searches run over the **cleaned text**: content that extraction replaces with a marker
204
+ (`[MATH]`, `[CODE]`, `[TABLE]`, …) cannot be matched. A count is a count of the cleaned
205
+ text, not of the raw wikitext.
206
+ - Japanese, Chinese, and Korean indexes cannot match queries shorter than 3 characters.
207
+ Word-based languages match exact forms only — `run` does not find `running`. Plan your
208
+ search terms accordingly, especially when you intend to report a zero result.
209
+ - Categories and interlanguage links are read from the dump as editors wrote them.
210
+ Categories added by a template rather than written in the article text are not visible
211
+ to wp2txt, which can make a category look much smaller than it is on the website —
212
+ check against the article text if a count looks wrong.
@@ -1,6 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wp2txt
4
+ # Missing namespace elements use the metadata scanner's historical ns=0 default.
5
+ def self.namespace_id(value)
6
+ (value || "0").to_i
7
+ end
8
+
4
9
  # =========================================================================
5
10
  # Custom Exception Classes
6
11
  # =========================================================================