Skip to content

fix(pipeline): hold the diff baseline until the state mirror lands [pipeline, index, docs, tests] - #212

Merged
liplus-lin-lay merged 1 commit into
mainfrom
209-rows-indexed-while-open-keep-a-stale-open-state-after-the-item-is-closed
Aug 3, 2026
Merged

fix(pipeline): hold the diff baseline until the state mirror lands [pipeline, index, docs, tests]#212
liplus-lin-lay merged 1 commit into
mainfrom
209-rows-indexed-while-open-keep-a-stale-open-state-after-the-item-is-closed

Conversation

@liplus-lin-lay

Copy link
Copy Markdown
Member

概要

索引済みの issue / PR 行が、GitHub 側で close された後も state: "open" のまま残る問題(#209)に対処する。恒久修正(発生源の順序)と backfill(既存の取り残し)を同じ PR に置いた。

原因

src/pipeline/embed-issue.ts の hash-skip path は、自分が守っている mirror 書き込み(Vectorize / D1)より先に IssueStore の record を更新していた。この record が metadataChanged の判定基準なので、mirror が失敗すると次回以降 basis が既に GitHub と一致し、差分が二度と検出されない。catch のコメントが言う「次の body 変更で追いつく」は embed 経路では成り立つが、state だけが変わった行には body 変更が来ないので成り立たない。

変更

  • src/pipeline/embed-issue.ts — mirror 書き込みを先に実行し、どちらかが失敗した場合は IssueStore の upsert をスキップする。stale な record が残ることが、次の poll / webhook 配信で差分を再検出させる仕掛けになる。あわせて FTS mirror を「vector が存在する」branch の外へ出した(内側にネストしていたため、vector が欠けている行では sparse 側の state 更新ごと落ちていた。vector 欠落自体は Index covers only about 45 percent of issue and PR history #210 の範囲)。
  • src/backfill-issue-state.ts + src/index.tsPOST /admin/backfill-issue-state?repo=owner/repo を追加。repo ごとに 1 回の state=open 一覧を正とし、そこに無い索引済み open 行を dense / sparse 両側で closed にする。embedding 呼び出しは無い(dense 側は getByIds で取り直した既存 values をそのまま再 upsert し、metadata の state だけ差し替える)ので従量コストは発生しない。/admin/reset-hashes は full re-embedding を起こすため使えない。
  • docs0-requirements.md / .ja.md に mirror 失敗時の扱い(embed 経路 = best-effort / metadata のみの経路 = 基準を保持)と修復 endpoint、installation.md / .ja.md に運用手順を追記。

安全側の設計

  • open 一覧が 50 ページを超える場合、何も閉じずにエラーで中断する。「一覧に無いこと」が close の根拠なので、部分的な一覧を使うと生きている項目を閉じてしまう。
  • dense 側の書き込みが失敗した場合、D1 に触れる前に呼び出し全体が失敗する。中途半端な修復を残さず、同じ cursor で再実行できる。
  • 修復方向は open -> closed の一方向のみ。欠陥が生んだ方向であり、走査対象が索引全体でなく open 集合の大きさに比例する。

テスト

  • src/pipeline/embed-issue.test.ts(新規)— mirror 成功時に基準が進むこと、sparse / dense いずれの失敗でも基準を保持して次回再試行できること、vector 欠落時も sparse 側は更新されること。
  • src/backfill-issue-state.test.ts(新規)— open 集合のページング、truncation ガード、stale 判定、dry run、cursor 再開、Vectorize batch、vector 欠落、dense 失敗時に D1 が無傷であること。
  • src/backfill-issue-state.workers.test.ts(新規)— 実 D1 + migration に対して SELECT / UPDATE を実行し、UPDATE trigger 経由でも FTS5 index が壊れず state filter が正しく切り替わることを確認。

Closes #209

…ipeline, index, docs, tests]

The metadata-only path in embed-issue advanced its own diff basis — the IssueStore
record — before the Vectorize / D1 mirror writes it guards. A mirror write that
failed was therefore never retried: the next poll compared GitHub against an
already-updated baseline and saw nothing to do. The catch comment promised the
next body change would reconcile it, which is true on the embed path and false
here, because a state-only change never brings one. Rows indexed while open kept
answering searches as live items after the item closed.

Two changes at the source, plus a repair for the rows already stranded:

- src/pipeline/embed-issue.ts: mirror writes run first, and the IssueStore upsert
  is skipped when either side failed, so the stale record is what makes the next
  delivery detect the diff again. The FTS mirror also moves out of the "vector
  exists" branch — nesting it there dropped the sparse state update for exactly
  the rows already missing a vector (#210).
- src/backfill-issue-state.ts + src/index.ts: POST /admin/backfill-issue-state,
  a resumable per-repo repair. One paginated state=open listing supplies the
  truth; indexed open rows absent from it are closed on both sides. No embedding
  — the dense side re-upserts the existing values with only `state` replaced —
  so /admin/reset-hashes (full re-embed) is not needed.

The backfill aborts rather than act on a truncated open listing, since absence
from that listing is what marks a row closed, and it fails before touching D1 if
the dense write throws, so a failed call leaves the window unrepaired rather than
half-repaired.

恒久修正と backfill を同じ PR に置いたのは、順序を直しても既存の取り残しは
自力で治らないため。差分検出の基準が既に GitHub と一致していて、通常の poll
では二度と到達しない。修復方向を open -> closed の一方向に限ったのは、欠陥が
生んだ方向であり、閉じた判断が生きた検討事項として再供給される害の方向でもある
から。

Closes #209

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@liplus-lin-lay liplus-lin-lay self-assigned this Aug 3, 2026
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
github-rag-mcp 560e4c1 Aug 03 2026, 03:04 AM

@liplus-lin-lay liplus-lin-lay left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI self-review (auto mode)

受入基準の充足

issue #209 の修正方針 3 点すべてに対応している。

方針 結果
恒久(欠陥 2): mirror の成否が確定するまで差分検出の基準を進めない 充足。src/pipeline/embed-issue.ts の hash-skip path で IssueStore upsert を mirror 書き込みのへ移し、いずれかが失敗した場合は基準を進めず failed: true を返す
FTS mirror を Vectorize 成功 branch の外へ出す 充足。dense / sparse を独立して書くようになり、vector 欠落行でも sparse 側の state は更新される
backfill を embedding 無しで実装 充足。POST /admin/backfill-issue-state。dense は getByIds の既存 values を state だけ差し替えて再 upsert、sparse は UPDATE。embed 呼び出しはゼロ

CI green(node 164 / workers 58)。docs は同一 PR 内で更新済み(0-requirements 両言語 + installation 両言語 §14)。

diff 実読で確認した点

  • cursor の安全性: selectIndexedOpenRowsnumber > cursor で再開する。1 issue が複数行に分割されていると最後の番号の残り行を飛ばすが、本番 D1 で COUNT(*) = COUNT(DISTINCT number) = COUNT(DISTINCT vector_id)(全 repo・全 type で一致)を確認した。issue / PR 行は 1 番号 1 行であり、GitHub の番号空間は issue と PR で共有されるため ORDER BY number は全順序になる。飛ばしは起きない。
  • 書き込み順序: dense → sparse。dense が途中で落ちると D1 は未変更のまま呼び出し全体が失敗し、同じ window を再実行できる。両側が割れた状態を残さない。
  • 打ち切りガード: 50 ページ超過で何も閉じずに throw。「一覧に無いこと」が close の根拠である以上、部分一覧での判定を拒否するのは正しい。
  • vector 欠落行: mirrorFailed を立てない。#210 側の面であり、embedding 無しでは復旧できないものに基準を人質に取らせない扱いで妥当。
  • 返り値の意味変更: metadata mirror 失敗が skippedUnchanged から failed: true になった。poller の stats と webhook レスポンスに現れる。#210 の watermark 修正はこの failed を未取り込み境界として扱う必要がある(#210 側の要件に含まれている)。

scope の逸脱

無し。範囲外として明示的に残したのは 1 点。

  • 逆方向(reopen された項目が closed のまま残る)を backfill の対象外とした。 欠陥の形は同じだが、恒久修正が今後をカバーする。この判断を実測で裏取りした: 全 6 repo の実 open 項目(liplus-language 33 / webhook-mcp 2 / rag-mcp 4 / dipper_ai 5 / liplus-desktop 5 / neuron-graph-rag 1)に対し、索引側が closed になっている行は 0 件。既存データに逆方向の取り残しは存在しないため、一方向スコープで欠落は生じない。

次のステップ

auto mode のため human gate 無し。self-review pass → merge。merge + deploy 後に parent が /admin/backfill-issue-state を全 repo に対して実行する。

@liplus-lin-lay
liplus-lin-lay merged commit fc5a2e3 into main Aug 3, 2026
3 checks passed
@liplus-lin-lay
liplus-lin-lay deleted the 209-rows-indexed-while-open-keep-a-stale-open-state-after-the-item-is-closed branch August 3, 2026 03:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rows indexed while open keep a stale open state after the item is closed

1 participant