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
6 changes: 6 additions & 0 deletions docs/0-requirements.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ Responsibilities:

各 invocation は独立した subrequest 予算を持つ。dispatch は `controller.cron` で `handleScheduled` 内で行う。未知の cron 表現は no-op log で silent regression を防止する。

issue / PR poller は `(lastPolledAt, now]` を `updated_at` 昇順で取得する。1 run あたり最大 `MAX_PAGES_PER_RUN` × 100 = 200 件を fetch し、そのうち embed するのは最大 `MAX_EMBEDDINGS_PER_RUN` = 50 件。この surface は commit diff と同じ watermark 不変条件に従う: **その run が retrieval surface に載せられなかった最も古い項目を watermark が追い越さない** — embedding 予算で見送った項目と、embed に失敗した項目の両方が対象。2 つの境界は別物として扱う。fetch がどこまで届いたか(poll 開始時刻、pagination が打ち切られた場合は最後に fetch した項目の `updated_at`)は上限にすぎず、取り込み境界がその下に watermark を留める。留める位置は境界の 1 秒手前で、GitHub の `since` filter が境界の項目自身を再び含むようにするため。

この pin が無い間、この surface は「fetch 件数 − embed 件数」の速度で取りこぼしていた。旧実装の watermark はどちらの分岐でも見送った項目より**新しい**位置に着地する — batch は昇順なので、予算はいつもその新しい端で尽きる — 一方で見送った項目に付く空の `bodyHash` は retry の印であって、以後どの `since` window もその項目を fetch しない。2026-08-03 の実測で、索引対象 repository の issue / pull request 履歴の約 55% が索引に載っていなかった。欠落が連続した番号帯ではなく散発に見えるのは、脱落が番号順ではなく `updated_at` 順に起きるため(issue #210)。**ETag も同じ条件で書き戻さない。** docs poller が tree ETag を保持するのと同じ理由で、ETag を保存すると次 run の条件付き request が 304 を返し、残りを見る前に return してしまう。

commit diff poller は 2-phase 構成:

- **forward phase** — `(lastPolledAt, pollStartTime]` の window を列挙し、その中の**古い側から**取り込む(webhook 取りこぼし時の redundancy)。watermark namespace は `diffs:${repo}`。
Expand Down Expand Up @@ -202,6 +206,8 @@ Responsibilities:
- commit diff は 1 commit 分の file リストを batch embed(Workers AI の `text: string[]` 対応を利用)し、1 回の Vectorize upsert で N vector を書き込む
- batch size は `MAX_EMBEDDING_BATCH_SIZE`(既定 20)で上限。これを超える commit は複数 batch call に分割する

**索引欠落の修復.** watermark の修正は漏れを止めるだけで、既に空いた穴は埋まらない — 取り残された項目が再 fetch されるのは `updated_at` が動いたときだけで、閉じた履歴はもう動かない。`POST /admin/backfill-issue-index?repo=owner/repo`(installation guide 参照)が欠落そのものを走査する。repository の issue 番号空間は密かつ有界なので、`search_docs` に issue / PR 行が無い番号がそのまま欠落集合であり、数値 cursor が「どこまで走査したか」を厳密に表せる。同じ集合を時刻 cursor で辿ると、欠陥が突いた順序をそのまま持ち込むことになる。GitHub 側に既に無い番号(削除・transfer 済み)は 404 を返すので、retry せず計上のみ。取り込みは body-hash 判定を強制的に飛ばす: 候補はいずれも retrieval surface が欠けていると分かっている項目であり、hash が一致していると(embed 成功後に FTS5 mirror が失敗した行がこの状態になる)そのまま恒久的に skip されてしまうため。state 修復と違いこちらは embed を伴うので、1 call の予算は Workers AI の予算であり、呼び出し側が batch を跨いで sweep を進める。`dry_run=true` は予算を使わずに欠落量だけを測る。

**取り残した state の修復.** 上の順序欠陥が残した行は、生きている項目として検索に出続ける。通常の poll では到達できない — 差分検出の基準がすでに GitHub と一致しているため。`POST /admin/backfill-issue-state?repo=owner/repo`(installation guide 参照)が repository 単位でこれを揃える。ページングした `state=open` 一覧を正とし、そこに無い索引済み `open` 行を dense / sparse 両側で `closed` にする。再 embed は伴わない(dense 側は既存の値をそのまま再 upsert し、`state` だけ差し替える)。修復は一方向(`open` → `closed`)で、欠陥が生んだ方向に一致する。open 一覧が打ち切られる場合は何もせず中断する — 「一覧に無いこと」が close の根拠だから。

### 5. Vector Store (Dense)
Expand Down
6 changes: 6 additions & 0 deletions docs/0-requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ The poller runs hourly in the current deployment, split across four cron trigger

Dispatch is performed inside `handleScheduled` by inspecting `controller.cron`. Unknown cron expressions fall through to a no-op log to prevent silent regressions when triggers are added later.

The issue / PR poller fetches `(lastPolledAt, now]` sorted by `updated_at` ascending, at most `MAX_PAGES_PER_RUN` × 100 = 200 items, and embeds at most `MAX_EMBEDDINGS_PER_RUN` = 50 of them. It obeys the same watermark invariant as the commit-diff surface: **the watermark never advances past the earliest item the run left off the retrieval surfaces** — deferred by the embedding budget, or failed to embed. The two bounds are separate: how far the fetch reached (the poll start time, or the last fetched item when pagination capped) is an upper bound, and the ingest boundary pins the watermark below it, one second earlier so GitHub's `since` filter still re-includes the boundary item.

Without that pin the surface leaked at a rate of fetched-minus-embedded per run. Both of the old watermark branches landed *above* every deferred item — the batch is ascending, so the budget always runs out on its newest end — and the deferred item's empty `bodyHash` marked it for a retry that no later `since` window would ever fetch. Measured 2026-08-03, that left about 55% of the issue and pull request history of the indexed repositories absent from the index, scattered rather than in contiguous ranges because the loss follows `updated_at` order and not number order (issue #210). **The ETag is withheld on the same condition**, for the reason the docs poller withholds its tree ETag: a stored ETag makes the next run's conditional request answer 304 and return before it looks at the leftover.

The commit-diff poller runs in two phases:

- **forward phase** — enumerates the window `(lastPolledAt, pollStartTime]` and ingests its **oldest** commits first, acting as redundancy when webhook delivery has stalled. Watermark namespace: `diffs:${repo}`.
Expand Down Expand Up @@ -203,6 +207,8 @@ Responsibilities:
- for commit diffs: batch-embed a commit's file list in a single Workers AI call (`text: string[]`) and upsert the resulting N vectors in one `VECTORIZE.upsert` call
- batch size is capped by `MAX_EMBEDDING_BATCH_SIZE` (default 20); commits exceeding it are split across multiple batch calls

**Missing-entry repair.** The watermark fix stops the leak but does not fill the hole: a stranded item is only re-fetched when its `updated_at` moves, and closed history never moves again. `POST /admin/backfill-issue-index?repo=owner/repo` (see the installation guide) walks the gap directly — the repository's issue-number space is dense and bounded, so the numbers with no `search_docs` issue / PR row are exactly the missing set, and a numeric cursor states how far the sweep has reached. A timestamp cursor over the same set would reintroduce the ordering the defect exploited. Numbers GitHub no longer has (deleted or transferred) answer 404 and are counted rather than retried. The ingest is forced past the body-hash check: every candidate is known to be missing a retrieval surface, and a matching hash — which an embed whose FTS5 mirror failed leaves behind — would otherwise skip it permanently. Unlike the state repair this one embeds, so the per-call budget is a Workers AI budget and the caller drives the sweep one batch at a time; `dry_run=true` measures the gap without spending it.

**Stale-state repair.** Rows left behind by the ordering defect above keep answering searches as live items, and no ordinary poll reaches them: their diff baseline already matches GitHub. `POST /admin/backfill-issue-state?repo=owner/repo` (see the installation guide) reconciles them per repository — one paginated `state=open` listing supplies the truth, indexed `open` rows absent from it are set to `closed` on both sides, and nothing is re-embedded (the dense side re-upserts the existing values with only `state` replaced). The repair is one-way (`open` → `closed`), which is the direction the defect produced; it aborts rather than act on a truncated open listing, since absence from that listing is what marks a row closed.

### 5. Vector Store (Dense)
Expand Down
42 changes: 42 additions & 0 deletions docs/installation.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,48 @@ POST /admin/backfill-issue-state?repo=owner/repo
- dense 側の書き込みが失敗した場合、D1 に触れる前に呼び出し全体が失敗する。中途半端な修復を残さないための設計なので、同じ `cursor` で再実行する
- 確認は close 済みと分かっている項目を `state: "closed"` で検索するか、`SELECT COUNT(*) FROM search_docs WHERE repo = ? AND type IN ('issue','pull_request') AND state = 'open'` が実際の open 数と一致することを見る

## 15. 索引に一度も載らなかった issue / pull request を取り込む

poller は 1 run で最大 200 件を fetch する一方、embed するのは最大 50 件で、旧実装は batch 全体を追い越して watermark を進めていた。embedding 予算で見送った項目には retry の印が付くが、以後どの `since` window もそれを fetch しない。結果として、索引対象 repository の issue / pull request 履歴の約 55% が一度も索引に載っていなかった(issue #210)。watermark は発生源側で修正済みだが、それは漏れを止めるだけ — 取り残された項目が再 fetch されるのは `updated_at` が動いたときだけで、閉じた履歴はもう動かない。この endpoint が欠落そのものを走査する。

実行は watermark 修正の deploy の**後**。順序を逆にすると、backfill 済みの索引に、同じ穴から落ちた新しい項目が混ざる。

Admin endpoint:

```text
POST /admin/backfill-issue-index?repo=owner/repo
```

パラメータ:

- `repo` — `owner/repo`
- `dry_run` — `true` で走査範囲の欠落量だけを測り、GitHub からは何も取得しない
- `limit` — 1 回の呼び出しで試す候補番号の数、`1..100`(既定 `25`)。dry run では無視される
- `cursor` — 再開位置の issue 番号。前回のレスポンスの `nextCursor` をそのまま渡す

認証:

- `GITHUB_TOKEN` ヘッダに worker secret と同じ値を送る

レスポンス:

```json
{ "repo": "owner/repo", "dryRun": false, "cursor": 0, "limit": 25, "maxNumber": 1690,
"scannedTo": 613, "candidates": 25, "attempted": 25, "indexed": 24, "absent": 1,
"failed": 0, "nextCursor": 613, "done": false }
```

運用上の注意:

- まず `dry_run=true` で規模を測る。embedding 予算を使わず、走査した範囲(1 回あたり最大 5000 番)の `candidates` を返す
- `done` が `true` になるまで `nextCursor` を渡して繰り返し呼ぶ。既定の `limit` なら 900 件欠けている repository で 36 回
- 上の 2 つの修復と違い、この endpoint は **embed する**。`limit` は subrequest 予算であると同時に Workers AI の予算でもあり、100 を超える指定は拒否される
- 何度実行しても安全。`search_docs` に行がある番号は fetch すらしないので、完了済みの sweep を再実行すると `candidates: 0` が返る。途中で失敗した呼び出しは同じ `cursor` から再開する
- `absent` は GitHub が 404 を返す番号の数(削除された issue、transfer で repository の外に出た番号)。これらは以後の sweep でも候補に残り続けるので、完了した repository でも `candidates` が小さな非ゼロを返すことがある
- `failed` は embed が着地しなかった候補の数。現在の呼び出し内では再試行せず、同じ範囲を次に sweep したときに拾い直す
- 取り込みは body-hash 判定を強制的に飛ばすので、vector はあるが FTS5 行が無い項目も修復される。次の poll を待つのでは代替できない理由がここ
- 確認は distinct な番号を数える: `SELECT COUNT(DISTINCT number) FROM search_docs WHERE repo = ? AND type IN ('issue','pull_request')` が実際の issue + PR 件数に近づく

## Troubleshooting

### `GITHUB_TOKEN not configured`
Expand Down
42 changes: 42 additions & 0 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,48 @@ Operational notes:
- if the dense write fails, the whole call fails before D1 is touched, leaving the window unrepaired rather than half-repaired. Retry with the same `cursor`
- verify with a `state: "closed"` search for an item you know was closed, or by counting `search_docs` rows: `SELECT COUNT(*) FROM search_docs WHERE repo = ? AND type IN ('issue','pull_request') AND state = 'open'` should match the repository's real open count

## 15. Index the issues and pull requests that never reached the index

The poller embeds at most 50 items per run but fetches up to 200, and it used to move its watermark past the whole batch regardless. Everything the embedding budget deferred was therefore marked for a retry that no later `since` window would fetch, and roughly 55% of the issue and pull request history of the indexed repositories never reached the index (issue #210). The watermark is fixed at the source, but that only stops the leak: a stranded item is re-fetched only when its `updated_at` moves, and closed history never moves again. This endpoint walks the gap directly.

Run it **after** deploying the watermark fix. In the other order, items indexed by the backfill are joined by new ones falling into the same hole.

Admin endpoint:

```text
POST /admin/backfill-issue-index?repo=owner/repo
```

Parameters:

- `repo` — `owner/repo`
- `dry_run` — `true` measures the gap over the scan range and fetches nothing from GitHub
- `limit` — candidate numbers attempted per call, `1..100` (default `25`). Ignored on a dry run
- `cursor` — issue number to resume after; pass back the `nextCursor` of the previous response

Authentication:

- send the same `GITHUB_TOKEN` value in the `GITHUB_TOKEN` header

Response:

```json
{ "repo": "owner/repo", "dryRun": false, "cursor": 0, "limit": 25, "maxNumber": 1690,
"scannedTo": 613, "candidates": 25, "attempted": 25, "indexed": 24, "absent": 1,
"failed": 0, "nextCursor": 613, "done": false }
```

Operational notes:

- start with `dry_run=true` to size the job. It spends no embedding budget and reports `candidates` over the range it scanned (up to 5000 numbers per call)
- call it repeatedly, feeding `nextCursor` back in, until `done` is `true`. At the default `limit` a repository missing 900 items takes 36 calls
- this endpoint **embeds**, unlike the two repairs above. `limit` is a Workers AI budget as much as a subrequest budget; raising it above 100 is refused
- safe to repeat: a number that already carries a `search_docs` row is never fetched, so a re-run of a finished sweep reports `candidates: 0`. A call that fails mid-sweep is resumed from the same `cursor`
- `absent` counts numbers GitHub answers 404 for — deleted issues, and numbers whose item was transferred out. They stay candidates on every future sweep, which is why a finished repository still reports a small non-zero `candidates`
- `failed` counts candidates whose embed did not land. They are retried by the next sweep over the same range, not by the current call
- the ingest is forced past the body-hash check, so an item whose vector exists but whose FTS5 row is missing is repaired too. This is why the endpoint is not equivalent to waiting for the next poll
- verify by counting distinct numbers: `SELECT COUNT(DISTINCT number) FROM search_docs WHERE repo = ? AND type IN ('issue','pull_request')` should approach the repository's real issue + PR count

## Troubleshooting

### `GITHUB_TOKEN not configured`
Expand Down
Loading
Loading