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
25 changes: 17 additions & 8 deletions ARCHITECTURE.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ packages/

> **Note (ディレクトリツリー read model、issue #107)**: `viewer_directory_nodes` / `viewer_directory_pages` は `viewer_pages` を返す `sourceRows` を再利用し `buildDirectoryTreeRows` が純粋関数としてメモリ上に構築する。**root_key はホスト単位、ただし internal ページを 1 件も持たないホストは除外**(外部リンク先ドメインの無意味な 1 ページツリーを防ぐ)。**ディレクトリ/ページ境界は末尾スラッシュで判定**(`/blog/2024/post-1` と `/blog/2024/` は同じ `/blog/2024/` ノードに着地)。**`has_children` は `direct_child_dir_count > 0` のみ**(`direct_page_count` を含めると構築ロジック上絶対に `false` にならないため、UI の展開矢印が意味を持つよう子ディレクトリの有無だけを見る)。この機能に legacy フォールバックは存在しないため、3関数(`getDirectoryTree`/`listDirectoryChildren`/`listDirectoryPages`)とも `hasViewerReadModel` ではなく `isViewerReadModelCurrent` を guard に使う。詳細は ARCHITECTURE.md の `@nitpicker/viewer` 節「設計注意(ディレクトリツリー read model...)」を正とする。

> **Note (外部リンク read model)**: `listExternalLinks`(PR #153)は `anchors` の JOIN + `COALESCE` 計算列での `GROUP BY` + `COUNT(DISTINCT source.id)` をリクエストごとに(`total` 用と data 用で)2 回実行していた。SQLite の `COUNT(DISTINCT ...)` は既存 index を使わず別 b-tree を都度構築する既知のパフォーマンス病理を持つため(実測: 単体 6.4 秒、他の集約と混ぜると 55.2 秒まで悪化する例が SQLite forum に報告されている)、`viewer_pages`/`viewer_directory_nodes` と同じ read model パターンに乗せた。`viewer_external_links`(`dest_page_id` PK / `dest_url` / `status` / `referrer_count`)は `buildViewerReadModel` 内で `computeExternalLinkRows` が `anchors` への専用クエリ(`sourceRows` 再利用不可 — リンク情報は `pages` にはない)で1回だけ集計して構築する。集計ロジック自体(`COALESCE` 解決、referrer 重複排除)は `listExternalLinks` から無変更で移植 — `getPageDetail.inboundLinks`(#71)とのカウント粒度契約を崩さないため。ページネーションは keyset cursor ではなく `paginateQuery`(offset ベース、REST 契約が offset のままなので不要な複雑さを持ち込まない)。`register-links-route.ts` は `/api/pages` と同じ二層構成で `isViewerReadModelCurrent` を見て `listViewerExternalLinks`(fast path)↔ `listExternalLinks`(legacy、無変更で残存)を切り替える。スキーマ変更のため `VIEWER_READ_MODEL_SCHEMA_VERSION` を 4→5 に bump。詳細は ARCHITECTURE.md の `@nitpicker/viewer` 節「設計注意(外部リンク read model)」を正とする。
> **Note (viewer_anchor_facts read model、issue #114)**: `listExternalLinks`(PR #153)は `anchors` の JOIN + `COALESCE` 計算列での `GROUP BY` + `COUNT(DISTINCT source.id)` を、`listLinks(type:'broken')` はfast pathなしの13-16秒級anchorスキャン+offsetページネーションのまま、それぞれ抱えていた。issue #114 は broken/external 両方を `viewer_anchor_facts` に載せる設計を提示していたが、実装は「read/write/storageのいずれも妥協しない」基準で再検討し、ドキュメント通りのref-table(`url_refs`/`content_items`、issue #139)方式は採用しなかった(#139はまだ未着手で `#103` の実行順序上も `#114` より後)。代わりに `source_url_sort_key`/`dest_url_sort_key` を `viewer_pages.url_sort_key` と同じ発想でインライン複製するのみに絞った。`viewer_anchor_facts`(`edge_id` PK、`(source_page_id, dest_page_id)` ペア単位でdedupし`count`で重複anchorを吸収、`is_broken`/`is_external_link`フラグ、`status_sort_key`/`status_desc_key`)は `compute-anchor-fact-rows.ts` が `anchors` を1回だけスキャンして構築する。`viewer_external_links` はこの1回のスキャン結果から `derive-external-link-summary-rows.ts`(純粋関数、DBアクセス無し)が導出するよう変更——旧 `compute-external-link-rows.ts`(独自の2回目の`anchors`スキャン)は廃止。`listViewerBrokenLinks` は `listViewerPages` と同じ4系統cursorページネーションを実装し、`/api/links?type=broken` の応答契約もoffsetのみから `nextCursor`/`prevCursor` 付きに変更した(`#103`の"large OFFSETを使うな"に対応、フロントのUI見た目は無変更)。`register-links-route.ts` は `external`/`broken` 両方で `isViewerReadModelCurrent` による二層dispatchを持つ(`broken`は`urlPattern`/`includeRedirectSources`指定時に強制legacy)。スキーマ変更のため `VIEWER_READ_MODEL_SCHEMA_VERSION` を 5→6 に bump。5万ページ・40万anchor規模の実測で `viewer_anchor_facts` はwarm p50 1.2ms(旧13-16秒から数千倍改善)。詳細は ARCHITECTURE.md の `@nitpicker/viewer` 節「設計注意(viewer_anchor_facts read model、issue #114)」を正とする。

## CLI コマンド

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ npx @nitpicker/cli viewer-build <archive.nitpicker> [--force]
- **MPA**: Prev / Next + ページ番号 + ジャンプ入力。現在ページとページサイズはどちらも URL クエリ(`?page=N` / `?pageSize=N`、ともに 1-indexed)に乗るため deep-link / 共有 / ブラウザ戻る/進むが完全に成立する(ページサイズが URL に無いと、`?page=5` を共有しても受け手側のサイズ次第で別の行が見えてしまう)。表示件数は 50 / 100 / 200。フィルタ変更で `?page=` は自動クリア、ページサイズ変更時も `?page=` を 1 に戻す(旧オフセットは新しい窓では意味を持たない)。デフォルト値(page=1, pageSize=100)は URL から省略
- **仮想スクロール**: TanStack Query infinite query + TanStack Virtual。**10 万行規模をクライアント全件ロードせず一定メモリで表示**するため、deep-link は捨てて巨大データの探索性を優先したいときの opt-in

モード本体は localStorage(`nitpicker-pagination-mode`)。ページサイズも localStorage(`nitpicker-page-size`)に保存されるが、これは新規タブ・直 URL 訪問時の hint であり、URL の `?pageSize=` が常に優先される。両モードとも backend は同じ `limit`/`offset` API(無改修)
モード本体は localStorage(`nitpicker-pagination-mode`)。ページサイズも localStorage(`nitpicker-page-size`)に保存されるが、これは新規タブ・直 URL 訪問時の hint であり、URL の `?pageSize=` が常に優先される。両モードとも同じ REST エンドポイントを叩くが、継続方法はビュー次第: MPA は常に `?page=`/`?pageSize=` から `limit`/`offset` を組み立てる一方、仮想スクロールは Pages / Broken Links では read model のキーセット `nextCursor` を、それ以外のビューでは `limit`/`offset` を使う

### Errors ビュー

Expand Down
Loading
Loading