Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,3 +298,8 @@ wrangler.toml

- [Liplus-Project/github-webhook-mcp](https://github.com/Liplus-Project/github-webhook-mcp)
- [Liplus-Project/liplus-language](https://github.com/Liplus-Project/liplus-language)


Doc/wiki full sparse source preparation is a default-off operator foundation, independent of normal search. See the [storage contract and bounded plan/apply procedure](docs/6-full-sparse-sources.ja.md). Migration 0009 precedes operator preparation and enable; default-off ingest/search remains usable without it.

The two new sparse operator endpoints require the dedicated `DOC_SPARSE_ADMIN_TOKEN` secret and `Authorization: Bearer <token>`; they fail closed without it and do not accept GitHub credentials. Provision via `wrangler secret put DOC_SPARSE_ADMIN_TOKEN` after reviewed merge.
7 changes: 7 additions & 0 deletions docs/0-requirements.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -683,3 +683,10 @@ source の `docs/_Sidebar.md` は既存 Feedback Memory EN/JA ページを参照
### Offline document-tail comparison (#279)

固定公開文書の後半発見を検討する opt-in sparse-only 実験は [Document Tail Replay](5-document-tail-replay.ja.md) の契約に従う。prefix8000 / fullbody / nonoverlap8000 の比較、入力・source hash、named target と exact literal source の別採点、静的rerank予算、節長分布を記録する。既定の検索・embedding・reranker・live索引は変更しない。


### Doc/wiki 独立全文 sparse source

#284 は既定 off の保存・operator 基盤を追加する。[全文 sparse source 契約](6-full-sparse-sources.ja.md)に従い prefix corpus、既定検索、graph、vector 数を維持する。matched 検索は #281 の別作業。

新しい sparse operator の 2 経路は専用 `DOC_SPARSE_ADMIN_TOKEN` bearer secret で認証し、未設定・空は fail closed。GitHub source 取得は従来の `GITHUB_TOKEN` を使う。既定 off の更新を含め、prefix revision CAS により版修復と owner 保存を同一 D1 batch に結ぶ。
7 changes: 7 additions & 0 deletions docs/0-requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -688,3 +688,10 @@ The source `docs/_Sidebar.md` must link the existing Feedback Memory EN/JA pages
### Offline document-tail comparison (#279)

固定公開文書の後半発見を検討する opt-in sparse-only 実験は [Document Tail Replay](5-document-tail-replay.ja.md) の契約に従う。prefix8000 / fullbody / nonoverlap8000 の比較、入力・source hash、named target と exact literal source の別採点、静的rerank予算、節長分布を記録する。既定の検索・embedding・reranker・live索引は変更しない。


### Independent full sparse doc/wiki sources

Issue #284 adds a default-off storage/operator foundation; its contract is [Full sparse sources](6-full-sparse-sources.ja.md). It preserves the prefix corpus, default retrieval, graph and vector count. Matched retrieval remains #281.

The new sparse operator endpoints require the dedicated `DOC_SPARSE_ADMIN_TOKEN` bearer secret and fail closed without it; GitHub source fetching retains `GITHUB_TOKEN`. Version repair and owner writes share a D1 batch guarded by the prefix revision, including default-off updates.
27 changes: 27 additions & 0 deletions docs/6-full-sparse-sources.ja.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Doc/wiki 全文 sparse source 基盤

Issue #284 の保存・運用契約。通常検索への接続は #281 の別作業。

`DOC_SPARSE_CONTEXT_ENABLED` は省略時 false。false では ingest は全文の生成・取得・追加 D1 呼出しを行わない。8000 UTF-16 unit の prefix、通常 nat FTS corpus/IDF、1 資料 1 vector、fetch、graph は維持する。既定 off の ingest と検索は migration 0009 の新 schema にアクセスしない。migration 0009 は operator 経路と enable より先に適用する。検索だけの旧経路は全文テーブルを読まない。

`search_docs` が metadata 正本。`doc_sparse_sources` は doc/wiki 専用で vector_id に一意、source_version(doc は Git blob SHA、wiki は body SHA-256)、prefix の取得時点、索引版、raw/body/source hash、UTF-8 bytes、coverage と理由を持つ。全文は path(wiki は slug)+ 2 改行 + body。同じ Worker の segmenter で作った文字列をそのまま所有し、位置表は保存しない。独立した external-content FTS は full coverage の行のみを索引する。

raw 256 KiB、segmented 512 KiB、owner 行の文字列 UTF-8 bytes と固定 overhead の合計 1 MiB を上限とする。超過は `out_of_range` として処理済みで prefix-only。全文/hash を取得できなかった場合は null とし、取得済みとしない。同じ版は skip、次の版または operator の `force=true` で再判定する。予算は CPU/heap の安全証明ではない。

有効 ingest は doc の blob hash を検証し、prefix と全文を D1 batch transaction で書いてから DO の版を進める。FTS 失敗は版を進めず、既存の ETag 保持・再試行に従う。prefix 更新は旧 owner を無効化し、削除 trigger は owner/独立 FTS を削除する。prefix 更新と backfill の競合は source_version、prefix 本文・type・repo・path・updated_at・indexed_at・source_revision の compare-and-swap で上書きを防ぐ。

migration の source_revision は prefix の source に関わる UPDATE ごとにちょうど 1 増える。既定 off の旧 SQL 形を保ち、同じ prefix・timestamp・古い source_version が残る更新も識別する。source_revision 自体の UPDATE は generation trigger を再発火させない。owner の prefix_revision は実際の prefix generation を所有し、統計と同版 skip もこの一致を確認する。

既定 off で A→B が取り込まれると source_version は A のままになり得る。backfill はこれだけで永久 conflict にせず、store.updatedAt と prefix、取得した doc blob/wiki body の hash と exact prefix、store の再読をすべて確認する。plan は版修復の予定だけを返し書かない。apply は source_revision を含む snapshot CAS で canonical source_version を B へ修復し、その UPDATE の成功だけを条件に owner 保存を同一 D1 batch で行う。修復は revision を 1 増やし旧 owner を無効化してから新 owner を結ぶ。CAS が負ければ両方 0 行、書込み失敗なら版修復も rollback。legacy の空版もこの手順で結ぶ。embedding/vector は呼ばない。

## Operator

この 2 経路だけは専用 secret `DOC_SPARSE_ADMIN_TOKEN` を使い、`Authorization: Bearer <token>` が完全一致した時だけ認証する。未設定・空なら fail closed。`GITHUB_TOKEN` への fallback はなく、GitHub source の取得だけは従来の `GITHUB_TOKEN` を使う。既存の他 admin 経路は変更しない。reviewed merge 後、operator が `wrangler secret put DOC_SPARSE_ADMIN_TOKEN` で secret を provision する。値は [vars]・repository・chat に置かない。`POST /admin/backfill-doc-sparse?repo=owner/repo&type=doc|wiki_doc` は plan が既定。書込みは `apply=true` のみ。`limit` は既定 5、最大 10、`max_bytes` は既定/最大 1 MiB、`cursor` は search_docs rowid。1 回の source 取得も bounded reader で制限する。doc は DO の blobSHA の Git Blob API から取得・hash 検証し、wiki は DO の extension の raw 本文を取得して bodySHA を照合する。過去 wiki を hash から取得できるとは仮定しない。store と prefix の版が異なるものや検証できないものは conflict、再取得候補として返す。embedding/Vectorize を呼ばず、unchanged/ETag を回避して準備する。

応答は対象 repo/type、取得時刻、各 source の版、coverage/bytes、成功・範囲外・欠損・競合・retryable error、次 cursor を持つ。retryable error または取得前に残り byte 予算が無い場合、cursor はその source を越えない。残り予算だけで取得が打ち切られた source は `byte_budget_deferred` として cursor を保持し、次の call で再試行する。call の初期予算でも取得上限で原文/hash を確認できなかった source は `source_byte_limit_unverified` conflict として進め、残りの取得を止める。`source_bytes` は取得・decode が完了した body の UTF-8 bytes、`budget_exhausted` は未取得分の上限到達も区別する。Git Blob JSON の network reader は残り decoded 予算 × 1.4 + 4096 bytes(base64 と envelope)で制限する。missing/conflict は明示報告して次へ進むため、競合解消後は cursor を戻して再実行する。plan は source を取得・検証するが保存しない。apply は同じ source の再実行で owner/FTS/vector を増やさない。

`POST /admin/doc-sparse-stats?repo=owner/repo&type=doc|wiki_doc` は indexed prefix population の coverage、未準備、版不一致、legacy prefix の版未検証件数、raw/segmented/行 bytes と索引版別件数を返す。これは repo tree 全体や現在の Git source 本体の件数ではない。

本番 apply は merge 後の別 operator 判断。対象 repo/type と plan、復旧境界を確認して migration/apply を行う。2026-10-13 の operator 確認では、全文 D1 export は FTS/virtual table 未対応で失敗しバックアップファイルは作れなかった。Time Travel info は成功し bookmark を控えたが、restore は実行していない。migration/apply 前には Time Travel info を再確認し bookmark と影響対象の metadata を控える。全 repo 一括取得は行わず cursor ごとに観察する。migration は旧 prefix/FTS/vector を変更しない。通常の fallback は enable を false に戻して全文書込みを止める。既定検索と prefix ingest は継続できる。DB 復旧は別操作であり、info で確認した Time Travel bookmark と restore の影響範囲を operator が apply 前に確認する。全 DB の restore は他 surface にも影響し、in-flight query/transaction を取り消す別判断であり、小さい全文 backfill の通常 fallback と混同しない。実行時は webhook/cron/admin の D1 書込みを静止させる。D1 Time Travel は DO/Vectorize を独立して復元するものではなく、復旧後の store 版・vector・prefix との整合を別途確認する。空の復旧用 D1 への import/binding 切替は、完全なバックアップが得られ手順を検証した場合に限る。既存の populated D1 への export SQL import だけを復元保証にしない。この PR では DB 復元を試行・検証していない。既定 off の ingest・検索は migration 0009 が無くても使用できる。operator 経路と enable は migration 0009 が必要。報告は merge・配置・本番 plan・apply 観測を分ける。

一次資料: [D1 Time Travel](https://developers.cloudflare.com/d1/reference/time-travel/)、[D1 import/export](https://developers.cloudflare.com/d1/best-practices/import-export-data/)。
7 changes: 7 additions & 0 deletions docs/installation.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -506,3 +506,10 @@ GitHub App の callback URL が worker 側 URL と完全一致していない。
### Webhook verification fails

`GITHUB_WEBHOOK_SECRET` が GitHub App 側設定と一致していない。


## Doc/wiki 全文 sparse 準備

operator 経路と enable より先に migration 0009 を適用し(既定 off の ingest・検索は新 schema 不要)、準備中は `DOC_SPARSE_CONTEXT_ENABLED="false"` を保つ。認証済み `POST /admin/backfill-doc-sparse?repo=owner/repo&type=doc` は plan が既定。計画と Time Travel bookmark と復旧の影響対象を確認した後(2026-10-13 の full export は FTS/virtual table 未対応で失敗、restore 未実行)だけ `apply=true` を指定する。1 回は最大 10 資料・decoded source 1 MiB、返る `next_cursor` を `cursor` に渡し、競合・error を確認する。wiki は `type=wiki_doc` で別処理。`POST /admin/doc-sparse-stats?repo=owner/repo&type=doc` で索引済み prefix owner の coverage を観察する。[完全な契約](6-full-sparse-sources.ja.md)に予算と rollout 境界を固定している。embedding と検索応答は変わらない。

reviewed merge 後、この 2 経路の呼出しより先に `wrangler secret put DOC_SPARSE_ADMIN_TOKEN` で専用 secret を provision し、`Authorization: Bearer <token>` を送る。未設定・空 secret、`GITHUB_TOKEN` ヘッダ、GitHub bearer credential は認証に使わない。source 取得だけは従来の `GITHUB_TOKEN` を使い、既存 admin 経路は変えない。secret の値を設定ファイルや chat に置かない。
7 changes: 7 additions & 0 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -506,3 +506,10 @@ The GitHub App callback URL does not exactly match the worker callback URL.
### Webhook verification fails

`GITHUB_WEBHOOK_SECRET` does not match the value configured in the GitHub App.


## Full sparse doc/wiki preparation

Migration 0009 must precede these operator endpoints and enable. Default-off ingest and search need no new schema. Keep `DOC_SPARSE_CONTEXT_ENABLED="false"` while preparing. The authenticated `POST /admin/backfill-doc-sparse?repo=owner/repo&type=doc` endpoint defaults to plan; use `apply=true` only after reading the plan and confirming the Time Travel bookmark/recovery scope (full export failed on FTS/virtual tables in the 2026-10-13 operator check; no restore was executed). Process at most 10 sources and 1 MiB decoded source bytes per call, pass the returned `next_cursor` as `cursor`, and examine conflicts/errors before continuing. Use `type=wiki_doc` separately. `POST /admin/doc-sparse-stats?repo=owner/repo&type=doc` reports coverage of indexed prefix owners. See [the complete contract](6-full-sparse-sources.ja.md) for limits and rollout boundaries. Neither operation embeds or changes retrieval.

Provision `DOC_SPARSE_ADMIN_TOKEN` with `wrangler secret put DOC_SPARSE_ADMIN_TOKEN` after reviewed merge and before these operator calls. Send `Authorization: Bearer <token>` to both new endpoints. They reject unset/empty secrets, a `GITHUB_TOKEN` header, and GitHub bearer credentials. GitHub source acquisition still uses `GITHUB_TOKEN`; existing older admin authentication is unchanged. Keep secret values out of configuration files and chat.
68 changes: 68 additions & 0 deletions migrations/0009_doc_sparse_sources.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
-- Independent doc/wiki owner. Prefix FTS tables and their corpus stay unchanged.
ALTER TABLE search_docs ADD COLUMN source_version TEXT NOT NULL DEFAULT '';
ALTER TABLE search_docs ADD COLUMN source_revision INTEGER NOT NULL DEFAULT 0;

CREATE TABLE doc_sparse_sources (
vector_id TEXT NOT NULL UNIQUE,
source_type TEXT NOT NULL CHECK(source_type IN ('doc', 'wiki_doc')),
source_version TEXT NOT NULL,
prefix_indexed_at TEXT NOT NULL,
prefix_revision INTEGER NOT NULL,
index_version TEXT NOT NULL,
source_hash TEXT,
body_hash TEXT,
raw_hash TEXT,
raw TEXT,
segmented TEXT,
raw_bytes INTEGER,
segmented_bytes INTEGER,
row_bytes INTEGER NOT NULL CHECK(row_bytes BETWEEN 0 AND 1048576),
coverage TEXT NOT NULL CHECK(coverage IN ('full', 'out_of_range')),
reason TEXT NOT NULL,
observed_at TEXT NOT NULL,
CHECK(coverage <> 'full' OR (raw IS NOT NULL AND segmented IS NOT NULL
AND source_hash IS NOT NULL AND body_hash IS NOT NULL AND raw_hash IS NOT NULL
AND raw_bytes BETWEEN 0 AND 262144 AND segmented_bytes BETWEEN 0 AND 524288))
);

CREATE VIEW doc_sparse_content_v1 AS
SELECT rowid, segmented AS content FROM doc_sparse_sources WHERE coverage = 'full';
CREATE VIRTUAL TABLE doc_sparse_fts_v1 USING fts5(
content, tokenize = 'porter unicode61 remove_diacritics 2',
content = 'doc_sparse_content_v1', content_rowid = 'rowid'
);
CREATE TRIGGER doc_sparse_ai AFTER INSERT ON doc_sparse_sources BEGIN
INSERT INTO doc_sparse_fts_v1(rowid, content)
SELECT new.rowid, new.segmented WHERE new.coverage = 'full';
END;
CREATE TRIGGER doc_sparse_ad AFTER DELETE ON doc_sparse_sources BEGIN
INSERT INTO doc_sparse_fts_v1(doc_sparse_fts_v1, rowid, content)
SELECT 'delete', old.rowid, old.segmented WHERE old.coverage = 'full';
END;
CREATE TRIGGER doc_sparse_au AFTER UPDATE ON doc_sparse_sources BEGIN
INSERT INTO doc_sparse_fts_v1(doc_sparse_fts_v1, rowid, content)
SELECT 'delete', old.rowid, old.segmented WHERE old.coverage = 'full';
INSERT INTO doc_sparse_fts_v1(rowid, content)
SELECT new.rowid, new.segmented WHERE new.coverage = 'full';
END;
CREATE TRIGGER doc_sparse_prefix_delete AFTER DELETE ON search_docs BEGIN
DELETE FROM doc_sparse_sources WHERE vector_id = old.vector_id;
END;
-- Even a default-off prefix update invalidates an old full snapshot.
CREATE TRIGGER doc_sparse_prefix_update AFTER UPDATE ON search_docs BEGIN
DELETE FROM doc_sparse_sources WHERE vector_id = old.vector_id
AND (old.source_revision <> new.source_revision OR old.source_version <> new.source_version OR old.content <> new.content
OR old.indexed_at <> new.indexed_at OR old.updated_at <> new.updated_at
OR old.repo <> new.repo OR old.type <> new.type OR old.doc_path <> new.doc_path);
END;

-- The default-off application keeps its pre-0009 SQL shape. This generation
-- therefore lives in the migrated DB, not in the application upsert. UPDATE OF
-- excludes source_revision itself, so the increment does not recurse. Even a
-- same-text/same-timestamp prefix update is observable to operator CAS.
CREATE TRIGGER doc_sparse_prefix_revision
AFTER UPDATE OF repo, type, doc_path, content, updated_at, indexed_at, source_version ON search_docs
WHEN old.type IN ('doc', 'wiki_doc') OR new.type IN ('doc', 'wiki_doc')
BEGIN
UPDATE search_docs SET source_revision = old.source_revision + 1 WHERE rowid = new.rowid;
END;
Loading
Loading