wp2txt 2.3.2 → 2.3.4
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 +4 -4
- data/.dockerignore +3 -0
- data/.github/workflows/publish-image.yml +140 -0
- data/CHANGELOG.md +23 -0
- data/DEVELOPMENT.md +12 -4
- data/DEVELOPMENT_ja.md +2 -3
- data/README.md +6 -3
- data/README_ja.md +6 -3
- data/Rakefile +22 -16
- data/bin/wp2txt +26 -14
- data/bin/wp2txt-mcp +4 -3
- data/docs/INDEXES.md +4 -0
- data/lib/wp2txt/article.rb +1 -1
- data/lib/wp2txt/constants.rb +16 -0
- data/lib/wp2txt/corpus.rb +98 -79
- data/lib/wp2txt/corpus_jobs.rb +15 -13
- data/lib/wp2txt/extractor.rb +6 -0
- data/lib/wp2txt/formatter.rb +18 -0
- data/lib/wp2txt/fts_index.rb +12 -3
- data/lib/wp2txt/metadata_index.rb +3 -3
- data/lib/wp2txt/multistream.rb +31 -2
- data/lib/wp2txt/output_path.rb +72 -12
- data/lib/wp2txt/output_writer.rb +8 -0
- data/lib/wp2txt/stream_processor.rb +45 -15
- data/lib/wp2txt/version.rb +1 -1
- data/lib/wp2txt.rb +7 -5
- data/spec/fts_index_spec.rb +40 -0
- data/spec/output_integrity_spec.rb +146 -0
- data/spec/p1_correctness_spec.rb +372 -0
- data/spec/stream_processor_spec.rb +3 -1
- data/spec/titles_output_path_spec.rb +61 -1
- data/wp2txt.gemspec +1 -1
- metadata +6 -9
- data/image/wp2txt-logo.svg +0 -16
- data/image/wp2txt.svg +0 -31
- data/scripts/benchmark_regex.rb +0 -161
- data/scripts/fetch_html_entities.rb +0 -94
- data/scripts/fetch_language_metadata.rb +0 -180
- data/scripts/fetch_mediawiki_data.rb +0 -334
- data/scripts/fetch_template_data.rb +0 -186
- data/scripts/profile_memory.rb +0 -139
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3972afe52897df60befbb96bed211e4fd05e33cce380ed46ced223637aed6954
|
|
4
|
+
data.tar.gz: 6674325567d9b21cfb8819110827a16327aaee2e7e0e509115953e3b97229b74
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b7e36ac5cc6ab08699f3765768d9629b8cf193d1c134fcaeb9925228739b960bbc71c9ac5056c0bda5add9d1110cf993220b0a5a508e29caa9529eac84830278
|
|
7
|
+
data.tar.gz: abba849021598f7003a6fce9798517b2f497486853ce65522f687ea767eea1a1b91b87046c715f90d7908bff228cc441beec6cf4907c4885adeddd9967335165
|
data/.dockerignore
CHANGED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
name: Publish image
|
|
2
|
+
|
|
3
|
+
# Publishes the multi-arch container image to the GitHub Container Registry.
|
|
4
|
+
#
|
|
5
|
+
# Publishing happens here rather than from a maintainer's machine on purpose. A
|
|
6
|
+
# local build sends the working tree as the build context, so anything sitting
|
|
7
|
+
# there untracked rides along; a runner starts from a clean checkout, where such
|
|
8
|
+
# files do not exist. Twice, private files reached published images this way.
|
|
9
|
+
#
|
|
10
|
+
# Fires on a v* tag. Run it by hand from the Actions tab for a dry run: the
|
|
11
|
+
# default leaves `push` off, so it builds and gates without publishing.
|
|
12
|
+
|
|
13
|
+
on:
|
|
14
|
+
push:
|
|
15
|
+
tags: ['v*']
|
|
16
|
+
workflow_dispatch:
|
|
17
|
+
inputs:
|
|
18
|
+
push:
|
|
19
|
+
description: 'Publish to GHCR (off = build and gate only)'
|
|
20
|
+
type: boolean
|
|
21
|
+
default: false
|
|
22
|
+
|
|
23
|
+
env:
|
|
24
|
+
IMAGE: ghcr.io/yohasebe/wp2txt
|
|
25
|
+
|
|
26
|
+
jobs:
|
|
27
|
+
publish:
|
|
28
|
+
name: Build, gate, publish
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
permissions:
|
|
31
|
+
contents: read
|
|
32
|
+
packages: write
|
|
33
|
+
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@v4
|
|
36
|
+
|
|
37
|
+
- name: Work out the version and whether to publish
|
|
38
|
+
id: plan
|
|
39
|
+
run: |
|
|
40
|
+
set -euo pipefail
|
|
41
|
+
file_version=$(grep -oE '[0-9]+\.[0-9]+\.[0-9]+' lib/wp2txt/version.rb | head -1)
|
|
42
|
+
if [ -z "$file_version" ]; then
|
|
43
|
+
echo "could not read a version from lib/wp2txt/version.rb" >&2
|
|
44
|
+
exit 1
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
if [ "${GITHUB_EVENT_NAME}" = "push" ]; then
|
|
48
|
+
tag_version="${GITHUB_REF_NAME#v}"
|
|
49
|
+
if [ "$tag_version" != "$file_version" ]; then
|
|
50
|
+
echo "tag ${GITHUB_REF_NAME} disagrees with lib/wp2txt/version.rb (${file_version})" >&2
|
|
51
|
+
echo "Publishing would tag the image with a version the code does not claim." >&2
|
|
52
|
+
exit 1
|
|
53
|
+
fi
|
|
54
|
+
publish=true
|
|
55
|
+
else
|
|
56
|
+
publish="${{ inputs.push }}"
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
echo "version=$file_version" >> "$GITHUB_OUTPUT"
|
|
60
|
+
echo "publish=$publish" >> "$GITHUB_OUTPUT"
|
|
61
|
+
echo "Version $file_version; publish=$publish"
|
|
62
|
+
|
|
63
|
+
- uses: ruby/setup-ruby@v1
|
|
64
|
+
with:
|
|
65
|
+
ruby-version: '3.3'
|
|
66
|
+
|
|
67
|
+
- uses: docker/setup-qemu-action@v3
|
|
68
|
+
|
|
69
|
+
- uses: docker/setup-buildx-action@v3
|
|
70
|
+
|
|
71
|
+
# Single arch, loaded into the daemon, so the gate can open it. The
|
|
72
|
+
# multi-arch build below reuses these layers from the builder's cache.
|
|
73
|
+
- name: Build for inspection (linux/amd64)
|
|
74
|
+
uses: docker/build-push-action@v6
|
|
75
|
+
with:
|
|
76
|
+
context: .
|
|
77
|
+
platforms: linux/amd64
|
|
78
|
+
load: true
|
|
79
|
+
push: false
|
|
80
|
+
tags: ${{ env.IMAGE }}:gate-candidate
|
|
81
|
+
|
|
82
|
+
- name: Gate — compare the image against the build context
|
|
83
|
+
run: ruby scripts/verify_image.rb "${IMAGE}:gate-candidate"
|
|
84
|
+
|
|
85
|
+
- name: Log in to GHCR
|
|
86
|
+
if: steps.plan.outputs.publish == 'true'
|
|
87
|
+
uses: docker/login-action@v3
|
|
88
|
+
with:
|
|
89
|
+
registry: ghcr.io
|
|
90
|
+
username: ${{ github.actor }}
|
|
91
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
92
|
+
|
|
93
|
+
- name: Build and push (linux/amd64, linux/arm64)
|
|
94
|
+
id: publish
|
|
95
|
+
if: steps.plan.outputs.publish == 'true'
|
|
96
|
+
uses: docker/build-push-action@v6
|
|
97
|
+
with:
|
|
98
|
+
context: .
|
|
99
|
+
platforms: linux/amd64,linux/arm64
|
|
100
|
+
push: true
|
|
101
|
+
tags: |
|
|
102
|
+
${{ env.IMAGE }}:${{ steps.plan.outputs.version }}
|
|
103
|
+
${{ env.IMAGE }}:latest
|
|
104
|
+
labels: |
|
|
105
|
+
org.opencontainers.image.source=https://github.com/yohasebe/wp2txt
|
|
106
|
+
org.opencontainers.image.version=${{ steps.plan.outputs.version }}
|
|
107
|
+
|
|
108
|
+
# Reading it back without credentials is the only proof that a user can
|
|
109
|
+
# pull what was just published.
|
|
110
|
+
- name: Check the published digest is readable anonymously
|
|
111
|
+
if: steps.plan.outputs.publish == 'true'
|
|
112
|
+
run: |
|
|
113
|
+
set -euo pipefail
|
|
114
|
+
digest="${{ steps.publish.outputs.digest }}"
|
|
115
|
+
docker logout ghcr.io
|
|
116
|
+
for attempt in 1 2 3 4 5; do
|
|
117
|
+
if docker manifest inspect "${IMAGE}@${digest}" > /dev/null 2>&1; then
|
|
118
|
+
echo "readable anonymously: ${IMAGE}@${digest}"
|
|
119
|
+
exit 0
|
|
120
|
+
fi
|
|
121
|
+
echo "attempt ${attempt} failed; ghcr.io can lag a few seconds, retrying in 10s"
|
|
122
|
+
sleep 10
|
|
123
|
+
done
|
|
124
|
+
echo "${IMAGE}@${digest} is not readable anonymously" >&2
|
|
125
|
+
exit 1
|
|
126
|
+
|
|
127
|
+
- name: Summary
|
|
128
|
+
if: always()
|
|
129
|
+
run: |
|
|
130
|
+
{
|
|
131
|
+
echo "### wp2txt image"
|
|
132
|
+
echo
|
|
133
|
+
echo "- version: \`${{ steps.plan.outputs.version }}\`"
|
|
134
|
+
echo "- published: \`${{ steps.plan.outputs.publish }}\`"
|
|
135
|
+
if [ -n "${{ steps.publish.outputs.digest }}" ]; then
|
|
136
|
+
echo "- digest: \`${{ steps.publish.outputs.digest }}\`"
|
|
137
|
+
echo "- pull: \`docker pull ${IMAGE}:${{ steps.plan.outputs.version }}\`"
|
|
138
|
+
fi
|
|
139
|
+
echo "- run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
|
140
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
Entries were rewritten in August 2026 to describe what changed for people using
|
|
9
9
|
wp2txt, rather than how it was implemented. The changes themselves are unaltered.
|
|
10
10
|
|
|
11
|
+
## [2.3.4] - 2026-09-30
|
|
12
|
+
|
|
13
|
+
- **Records are no longer duplicated in `--no-turbo` output or in large `extract_corpus` runs**: the last record written before each batch could appear once more for every worker process — up to eight identical lines in a row. The default mode for `.bz2` dumps was not affected. If you extracted more than 200 articles with `extract_corpus`, or used `--no-turbo`, check earlier output for repeated lines; the repeats are byte-for-byte identical, so removing exact duplicate lines restores it
|
|
14
|
+
- **`--num-procs` is now honoured**: any value smaller than the number of CPU cores was silently replaced with the automatic choice. The value you give is used, and one outside 1 to the core count is adjusted with a warning
|
|
15
|
+
- **`--articles` no longer fails on large dumps**: extracting specific articles could stop with `Invalid argument` (seen on macOS) when the last requested article sat early in a multi-gigabyte dump
|
|
16
|
+
- **JSON records include `page_id` and `revision_id`**: the dump's own IDs for the page and its revision, right after `title`, in every mode — so each record can be traced to the exact version it came from. From Ruby, `StreamProcessor#each_page(with_ids: true)` yields them as a third value; plain `each_page` is unchanged
|
|
17
|
+
- **Corrupt input now stops processing instead of being cleaned silently**: invalid UTF-8 raises `Wp2txt::EncodingError` naming where it was found. Official dumps are valid UTF-8, so this only triggers on damaged files
|
|
18
|
+
- **Articles that fail to render are reported**: the default mode skipped them without a word; each one is now named on stderr with the reason
|
|
19
|
+
|
|
20
|
+
## [2.3.3] - 2026-09-08
|
|
21
|
+
|
|
22
|
+
- **Short searches no longer report a false zero**: in Japanese, Chinese, and Korean indexes a phrase of one or two characters could match nothing and come back as `0 matches`, which reads exactly like a term that is genuinely absent from the dump. Such a search now fails with an explicit error instead. If you recorded a zero result for a short term with an earlier version, re-check it
|
|
23
|
+
- **Characters no longer vanish from extracted text**: a multi-byte character that happened to straddle an internal read boundary was dropped, silently and at unpredictable positions. Any non-ASCII text could be affected. Re-extract if you need the output to be exact
|
|
24
|
+
- **Articles whose title contains a colon are no longer skipped**: `Star Trek: Voyager` and titles like it were mistaken for administrative pages and left out of both extraction and indexes. Rebuild with `--build-index -U` and re-extract to pick them up
|
|
25
|
+
- **`query_sql` no longer loses values when column names repeat**: a query selecting two columns under the same name kept only one of them. Names are now made unique, and the `.meta.json` beside a file result records which output name corresponds to which column of your query
|
|
26
|
+
- **Cell clipping counts bytes, not characters**: a long cell was measured in characters, so it could exceed the limit and still be reported as unclipped — 30,000 Japanese characters are 90,000 bytes. The limit is now 65,536 bytes, cut at a character boundary, and the count of clipped cells is accurate
|
|
27
|
+
- **A result of exactly the requested size is no longer marked truncated**: `truncated` came back true when a result happened to fill the limit exactly, with nothing left behind
|
|
28
|
+
- **Output files stay inside the output directory**: a symbolic link within it could redirect a write outside, the `.meta.json` sidecar was not checked at all, and an existing file could slip past `overwrite: false`. Output and sidecar are now reserved together and written to a temporary file first, so a failed or cancelled run leaves your previous output untouched instead of a half-written file in its place
|
|
29
|
+
- **Index builds no longer grow in memory with the size of the dump**: every batch of extracted text was kept until the build finished
|
|
30
|
+
- **A background job that fails to start is now reported as failed**: it stayed `running` indefinitely and blocked every later job until the server was restarted
|
|
31
|
+
- **The gem no longer ships maintainer scripts or images**. If you were running `scripts/fetch_*.rb` from an installed gem, take them from the repository instead
|
|
32
|
+
- **The Docker Hub repository has been removed**: images are published to GitHub Container Registry only. If you still pull from Docker Hub, switch with `docker pull ghcr.io/yohasebe/wp2txt`
|
|
33
|
+
|
|
11
34
|
## [2.3.2] - 2026-08-13
|
|
12
35
|
|
|
13
36
|
- **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")
|
data/DEVELOPMENT.md
CHANGED
|
@@ -400,13 +400,21 @@ docs/INDEXES.md is checked against the actual server surface by spec/docs_sync_s
|
|
|
400
400
|
|
|
401
401
|
## Docker
|
|
402
402
|
|
|
403
|
-
|
|
403
|
+
Images are published by GitHub Actions when a `v*` tag is pushed
|
|
404
|
+
(`.github/workflows/publish-image.yml`), not from a maintainer's machine: a
|
|
405
|
+
local build sends the working tree as its context, so untracked files ride
|
|
406
|
+
along, while a runner starts from a clean checkout.
|
|
404
407
|
|
|
405
408
|
```bash
|
|
406
|
-
rake check_image
|
|
407
|
-
rake push # Verifies, then builds multi-arch and pushes to GHCR
|
|
409
|
+
rake check_image # Build locally and run the same gate the workflow runs
|
|
408
410
|
```
|
|
409
411
|
|
|
412
|
+
The gate (`scripts/verify_image.rb`) lists what the image holds under `/wp2txt`
|
|
413
|
+
and compares it against what Docker sends as the build context, in both
|
|
414
|
+
directions. Anything unaccounted for fails, so a file the build starts
|
|
415
|
+
producing must be added to `BUILD_ARTIFACTS` deliberately. To rehearse a
|
|
416
|
+
publish, run the workflow from the Actions tab with **push** left off.
|
|
417
|
+
|
|
410
418
|
## Release Process
|
|
411
419
|
|
|
412
420
|
1. Update version in `lib/wp2txt/version.rb`
|
|
@@ -414,7 +422,7 @@ rake push # Verifies, then builds multi-arch and pushes to GHCR
|
|
|
414
422
|
3. Run full test suite: `bundle exec rspec`
|
|
415
423
|
4. Build gem: `gem build wp2txt.gemspec`
|
|
416
424
|
5. Push to RubyGems: `gem push wp2txt-*.gem`
|
|
417
|
-
6. Push
|
|
425
|
+
6. Push the `v*` tag — GitHub Actions builds, gates, and publishes the image
|
|
418
426
|
7. Create GitHub release
|
|
419
427
|
|
|
420
428
|
## Useful Links
|
data/DEVELOPMENT_ja.md
CHANGED
|
@@ -402,8 +402,7 @@ docs/INDEXES.md の MCP ツール表は spec/docs_sync_spec.rb が実際のサ
|
|
|
402
402
|
Dockerイメージのビルドとプッシュ:
|
|
403
403
|
|
|
404
404
|
```bash
|
|
405
|
-
rake check_image
|
|
406
|
-
rake push # 検証したうえでマルチアーキテクチャでビルドしGHCRにプッシュ
|
|
405
|
+
rake check_image # ローカルでビルドし、CI と同じゲートを走らせる
|
|
407
406
|
```
|
|
408
407
|
|
|
409
408
|
## リリースプロセス
|
|
@@ -413,7 +412,7 @@ rake push # 検証したうえでマルチアーキテクチャでビル
|
|
|
413
412
|
3. フルテストスイートを実行: `bundle exec rspec`
|
|
414
413
|
4. gemをビルド: `gem build wp2txt.gemspec`
|
|
415
414
|
5. RubyGemsにプッシュ: `gem push wp2txt-*.gem`
|
|
416
|
-
6.
|
|
415
|
+
6. `v*` タグをプッシュ — GitHub Actions がビルド・検証・公開を行う
|
|
417
416
|
7. GitHubリリースを作成
|
|
418
417
|
|
|
419
418
|
## 便利なリンク
|
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
|
|
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
|
|
|
@@ -172,13 +172,16 @@ CATEGORIES: Category1, Category2, Category3
|
|
|
172
172
|
Each line contains one JSON object:
|
|
173
173
|
|
|
174
174
|
```json
|
|
175
|
-
{"title": "Article Title", "categories": ["Cat1", "Cat2"], "text": "...", "redirect": null}
|
|
175
|
+
{"title": "Article Title", "page_id": 12345, "revision_id": 67890, "categories": ["Cat1", "Cat2"], "text": "...", "redirect": null}
|
|
176
176
|
```
|
|
177
177
|
|
|
178
|
+
`page_id` and `revision_id` are the dump's own `<id>` values for the page and its
|
|
179
|
+
revision, so a record can be traced back to the exact version of the article it came from.
|
|
180
|
+
|
|
178
181
|
For redirect articles:
|
|
179
182
|
|
|
180
183
|
```json
|
|
181
|
-
{"title": "NYC", "categories": [], "text": "", "redirect": "New York City"}
|
|
184
|
+
{"title": "NYC", "page_id": 23456, "revision_id": 78901, "categories": [], "text": "", "redirect": "New York City"}
|
|
182
185
|
```
|
|
183
186
|
|
|
184
187
|
## Cache Management
|
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
|
|
83
|
+
イメージはGitHub Container Registry(`ghcr.io/yohasebe/wp2txt`)で公開されています。旧 Docker Hub リポジトリは削除済みです — GHCR から pull してください。
|
|
84
84
|
|
|
85
85
|
## 基本的な使い方
|
|
86
86
|
|
|
@@ -156,13 +156,16 @@ CATEGORIES: カテゴリ1, カテゴリ2, カテゴリ3
|
|
|
156
156
|
各行に1つのJSONオブジェクト:
|
|
157
157
|
|
|
158
158
|
```json
|
|
159
|
-
{"title": "記事タイトル", "categories": ["カテゴリ1", "カテゴリ2"], "text": "...", "redirect": null}
|
|
159
|
+
{"title": "記事タイトル", "page_id": 12345, "revision_id": 67890, "categories": ["カテゴリ1", "カテゴリ2"], "text": "...", "redirect": null}
|
|
160
160
|
```
|
|
161
161
|
|
|
162
|
+
`page_id` と `revision_id` はダンプに記録されたページと版の `<id>` です。各レコードが
|
|
163
|
+
どの記事のどの版から取り出されたかを後から辿れます。
|
|
164
|
+
|
|
162
165
|
リダイレクト記事の場合:
|
|
163
166
|
|
|
164
167
|
```json
|
|
165
|
-
{"title": "NYC", "categories": [], "text": "", "redirect": "New York City"}
|
|
168
|
+
{"title": "NYC", "page_id": 23456, "revision_id": 78901, "categories": [], "text": "", "redirect": "New York City"}
|
|
166
169
|
```
|
|
167
170
|
|
|
168
171
|
## キャッシュ管理
|
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,6 +29,10 @@ 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
|
# =============================================================================
|
|
@@ -35,13 +40,14 @@ Rake::Task["build"].enhance([:normalize_permissions])
|
|
|
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
42
|
# would otherwise ride along.
|
|
38
|
-
IMAGE_FORBIDDEN_PATHS = %w[/wp2txt/research-notes /wp2txt/tmp /wp2txt/.git /wp2txt/CLAUDE.md /wp2txt/.claude].freeze
|
|
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 =
|
|
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 "
|
|
58
|
-
task push
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
@@ -45,14 +45,14 @@ class WpApp
|
|
|
45
45
|
def calculate_num_processes(opts)
|
|
46
46
|
optimal = Wp2txt::MemoryMonitor.optimal_processes
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
48
|
+
return optimal unless opts[:num_procs]
|
|
49
|
+
|
|
50
|
+
requested = opts[:num_procs].to_i
|
|
51
|
+
used = requested.clamp(1, Etc.nprocessors)
|
|
52
|
+
if used != requested
|
|
53
|
+
print_warning("--num-procs #{requested} is outside 1..#{Etc.nprocessors}; using #{used}.")
|
|
54
|
+
end
|
|
55
|
+
used
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
# Process articles using turbo mode (split-first architecture from v1.x)
|
|
@@ -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?
|
|
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)
|
|
@@ -243,6 +244,9 @@ class WpApp
|
|
|
243
244
|
next if redirect_page?(text)
|
|
244
245
|
|
|
245
246
|
article = Article.new(text, title, strip_tmarker)
|
|
247
|
+
ids = Wp2txt.page_ids(page_xml)
|
|
248
|
+
article.page_id = ids[:page_id]
|
|
249
|
+
article.revision_id = ids[:revision_id]
|
|
246
250
|
result = format_article(article, config)
|
|
247
251
|
next unless result
|
|
248
252
|
|
|
@@ -253,7 +257,9 @@ class WpApp
|
|
|
253
257
|
out.puts(result)
|
|
254
258
|
end
|
|
255
259
|
article_count += 1
|
|
256
|
-
rescue StandardError
|
|
260
|
+
rescue StandardError => e
|
|
261
|
+
# Keep going, but never lose an article silently
|
|
262
|
+
warn "wp2txt: skipped #{title.inspect}: #{e.class}: #{e.message}"
|
|
257
263
|
next
|
|
258
264
|
end
|
|
259
265
|
end
|
|
@@ -354,8 +360,8 @@ class WpApp
|
|
|
354
360
|
end
|
|
355
361
|
puts
|
|
356
362
|
|
|
357
|
-
stream.each_page do |title, text|
|
|
358
|
-
pages << [title, text]
|
|
363
|
+
stream.each_page(with_ids: true) do |title, text, ids|
|
|
364
|
+
pages << [title, text, ids]
|
|
359
365
|
page_count += 1
|
|
360
366
|
|
|
361
367
|
# Process batch when full
|
|
@@ -448,8 +454,13 @@ class WpApp
|
|
|
448
454
|
)
|
|
449
455
|
else
|
|
450
456
|
# Fall back to Parallel gem (process-based parallelism)
|
|
451
|
-
|
|
457
|
+
# Anything the writer still buffers would be inherited by every
|
|
458
|
+
# forked worker and written again as each one exits.
|
|
459
|
+
writer.flush
|
|
460
|
+
Parallel.map(pages, in_processes: num_processes) do |title, text, ids|
|
|
452
461
|
article = Article.new(text, title, strip_tmarker)
|
|
462
|
+
article.page_id = ids&.dig(:page_id)
|
|
463
|
+
article.revision_id = ids&.dig(:revision_id)
|
|
453
464
|
format_article(article, config)
|
|
454
465
|
end
|
|
455
466
|
end
|
|
@@ -609,7 +620,8 @@ class WpApp
|
|
|
609
620
|
next unless title_match
|
|
610
621
|
|
|
611
622
|
title = title_match[1]
|
|
612
|
-
next if title.nil? || title.empty?
|
|
623
|
+
next if title.nil? || title.empty?
|
|
624
|
+
next unless Wp2txt.namespace_id(page_xml[%r{<ns>(-?\d+)</ns>}, 1]).zero?
|
|
613
625
|
|
|
614
626
|
text_match = TEXT_REGEX.match(page_xml)
|
|
615
627
|
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;
|
|
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}" } }
|
data/docs/INDEXES.md
CHANGED
|
@@ -130,6 +130,10 @@ $ claude mcp add wp2txt -- docker run -i --rm -v wp2txt:/root/.wp2txt ghcr.io/yo
|
|
|
130
130
|
seconds, saved alias sets are re-checked before being stored, and files can only be
|
|
131
131
|
written under the server's output directory — worth knowing if you plan to let an
|
|
132
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.
|
|
133
137
|
|
|
134
138
|
## 5. Cross-language SQL
|
|
135
139
|
|
data/lib/wp2txt/article.rb
CHANGED
|
@@ -26,7 +26,7 @@ module Wp2txt
|
|
|
26
26
|
# an article contains elements, each of which is [TYPE, string]
|
|
27
27
|
class Article
|
|
28
28
|
include Wp2txt
|
|
29
|
-
attr_accessor :elements, :title, :categories
|
|
29
|
+
attr_accessor :elements, :title, :categories, :page_id, :revision_id
|
|
30
30
|
|
|
31
31
|
def initialize(text, title = "", strip_tmarker = false)
|
|
32
32
|
@title = title.strip
|
data/lib/wp2txt/constants.rb
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
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
|
+
|
|
9
|
+
# Page and revision IDs from one <page> element of a dump. The page's own
|
|
10
|
+
# <id> precedes <revision>; the revision's <id> is its first child (the
|
|
11
|
+
# contributor's <id> comes later). Only the header before <text> is read.
|
|
12
|
+
def self.page_ids(page_xml)
|
|
13
|
+
head = page_xml[0, page_xml.index("<text") || page_xml.size]
|
|
14
|
+
{
|
|
15
|
+
page_id: head[%r{<page>.*?<id>(\d+)</id>}m, 1]&.to_i,
|
|
16
|
+
revision_id: head[%r{<revision>\s*<id>(\d+)</id>}, 1]&.to_i
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
|
|
4
20
|
# =========================================================================
|
|
5
21
|
# Custom Exception Classes
|
|
6
22
|
# =========================================================================
|