[{"data":1,"prerenderedAt":706},["ShallowReactive",2],{"content-/2026-04-29-restructure-book-turso-migration":3,"all-pages-for-dir":704,"og-image-/2026-04-29-restructure-book-turso-migration":705},{"id":4,"title":5,"body":6,"category":685,"description":686,"extension":687,"meta":688,"navigation":405,"path":689,"project_name":690,"published":691,"publishedAt":692,"seo":693,"stem":694,"tags":695,"todo":702,"updatedAt":702,"__hash__":703},"pages/2026-04/2026-04-29/restructure-book-turso-migration.md","/restructure-book を Turso API に書き換えて専門書2冊（752チャンク→126セクション）に再構造化を流した",{"type":7,"value":8,"toc":675},"minimark",[9,13,35,40,49,138,172,282,293,297,304,318,326,333,340,344,351,358,364,370,374,389,392,436,456,513,519,523,536,543,547,557,568,578,581,630,633,671],[10,11,5],"h1",{"id":12},"restructure-book-を-turso-api-に書き換えて専門書2冊752チャンク126セクションに再構造化を流した",[14,15,16,17,21,22,26,27,30,31,34],"p",{},"book-knowledge-base リポジトリで、yomitoku が吐き出したページ単位のチャンクを ",[18,19,20],"strong",{},"目次に沿って節レベルへ統合する"," スラッシュコマンド ",[23,24,25],"code",{},"/restructure-book"," を、Turso API 対応に書き換えた。旧コマンドはローカル SQLite を ",[23,28,29],{},"better-sqlite3"," で直接叩く前提で書かれていて、Embedded Replica + Turso クラウドに移った今のスキーマでは1行も動かなかった。書き換えた後、専門書2冊に通して ",[18,32,33],{},"752チャンクを126セクションに圧縮","（うち626チャンク削除）した。",[36,37,39],"h2",{"id":38},"旧コマンドが-turso-では動かなかった","旧コマンドが Turso では動かなかった",[14,41,42,44,45,48],{},[23,43,25],{}," は2026年2月頃に書いたまま放置していて、当時はチャンクをローカルの ",[23,46,47],{},"data/books.db"," に入れていた。今は Turso のクラウドDBに移したので、旧コマンドの中身を見たら全部ローカルDB前提だった。",[50,51,56],"pre",{"className":52,"code":53,"language":54,"meta":55,"style":55},"language-ts shiki shiki-themes vitesse-light vitesse-light","// 旧: better-sqlite3 で直接ファイルを開く\nconst db = new Database('./data/books.db')\ndb.prepare('SELECT * FROM book_chunks WHERE book_id = ?').all(bookId)\n","ts","",[23,57,58,67,105],{"__ignoreMap":55},[59,60,63],"span",{"class":61,"line":62},"line",1,[59,64,66],{"class":65},"sxvE3","// 旧: better-sqlite3 で直接ファイルを開く\n",[59,68,70,74,78,82,85,89,92,96,100,102],{"class":61,"line":69},2,[59,71,73],{"class":72},"stQ0i","const ",[59,75,77],{"class":76},"s4oTP","db",[59,79,81],{"class":80},"shFtX"," =",[59,83,84],{"class":72}," new ",[59,86,88],{"class":87},"senZ8","Database",[59,90,91],{"class":80},"(",[59,93,95],{"class":94},"sMJiu","'",[59,97,99],{"class":98},"sdGka","./data/books.db",[59,101,95],{"class":94},[59,103,104],{"class":80},")\n",[59,106,108,110,113,116,118,120,123,125,128,131,133,136],{"class":61,"line":107},3,[59,109,77],{"class":76},[59,111,112],{"class":80},".",[59,114,115],{"class":87},"prepare",[59,117,91],{"class":80},[59,119,95],{"class":94},[59,121,122],{"class":98},"SELECT * FROM book_chunks WHERE book_id = ?",[59,124,95],{"class":94},[59,126,127],{"class":80},").",[59,129,130],{"class":87},"all",[59,132,91],{"class":80},[59,134,135],{"class":76},"bookId",[59,137,104],{"class":80},[14,139,140,141,144,145,148,149,152,153,155,156,159,160,163,164,167,168,171],{},"Embedded Replica のクライアント（",[23,142,143],{},"@libsql/client"," の ",[23,146,147],{},"createClient"," + ",[23,150,151],{},"syncUrl","）に置き換えると、API 形態が ",[23,154,88],{}," から ",[23,157,158],{},"Client"," に変わり、",[23,161,162],{},"prepare().all()"," が ",[23,165,166],{},"execute({ sql, args })"," に変わる。SQL 自体は SQLite 互換なので変えなくてよかったが、",[18,169,170],{},"呼び出し方が全部変わる","。",[50,173,175],{"className":52,"code":174,"language":54,"meta":55,"style":55},"// 新: Embedded Replica クライアント\nconst client = createClient({ url, syncUrl, authToken })\nconst { rows } = await client.execute({\n  sql: 'SELECT * FROM book_chunks WHERE book_id = ?',\n  args: [bookId],\n})\n",[23,176,177,182,213,243,262,276],{"__ignoreMap":55},[59,178,179],{"class":61,"line":62},[59,180,181],{"class":65},"// 新: Embedded Replica クライアント\n",[59,183,184,186,189,191,194,197,200,203,205,207,210],{"class":61,"line":69},[59,185,73],{"class":72},[59,187,188],{"class":76},"client",[59,190,81],{"class":80},[59,192,193],{"class":87}," createClient",[59,195,196],{"class":80},"({ ",[59,198,199],{"class":76},"url",[59,201,202],{"class":80},", ",[59,204,151],{"class":76},[59,206,202],{"class":80},[59,208,209],{"class":76},"authToken",[59,211,212],{"class":80}," })\n",[59,214,215,217,220,223,226,228,232,235,237,240],{"class":61,"line":107},[59,216,73],{"class":72},[59,218,219],{"class":80},"{",[59,221,222],{"class":76}," rows",[59,224,225],{"class":80}," }",[59,227,81],{"class":80},[59,229,231],{"class":230},"sHkkW"," await",[59,233,234],{"class":76}," client",[59,236,112],{"class":80},[59,238,239],{"class":87},"execute",[59,241,242],{"class":80},"({\n",[59,244,246,250,253,255,257,259],{"class":61,"line":245},4,[59,247,249],{"class":248},"sz8Xr","  sql",[59,251,252],{"class":80},": ",[59,254,95],{"class":94},[59,256,122],{"class":98},[59,258,95],{"class":94},[59,260,261],{"class":80},",\n",[59,263,265,268,271,273],{"class":61,"line":264},5,[59,266,267],{"class":248},"  args",[59,269,270],{"class":80},": [",[59,272,135],{"class":76},[59,274,275],{"class":80},"],\n",[59,277,279],{"class":61,"line":278},6,[59,280,281],{"class":80},"})\n",[14,283,284,285,288,289,292],{},"書き換え範囲は読み取り（チャンク取得）と書き込み（マージ後の挿入＋元チャンク削除）の両方。トランザクション境界も ",[23,286,287],{},"client.transaction('write')"," に置き換えた。スラッシュコマンドの ",[23,290,291],{},".md"," 内に埋め込んだ手順スクリプトを丸ごと一度ゴミ箱に入れて、Turso 用に書き直す形になった。",[36,294,296],{"id":295},"_1冊目-連結cfマニュアル341-31セクション","1冊目: 連結CFマニュアル（341 → 31セクション）",[14,298,299,300,303],{},"最初に流したのは「連結キャッシュ・フロー計算書の作成マニュアル」、341チャンク。yomitoku が",[18,301,302],{},"ページ単位","でチャンクを切っていたので、p.1〜p.341 がそのまま341行ぶんDBに並んでいた。",[14,305,306,307,310,311,310,314,317],{},"Step 1〜3 で目次（p.4〜p.14）を読み取って章節構造を把握する。",[23,308,309],{},"1-1","、",[23,312,313],{},"1-2",[23,315,316],{},"3-2"," といった節番号が拾えたので、節をセクションの単位に決めた。完全性検証スクリプトで「全341チャンクが必ず1つの節に含まれているか」をチェックして、漏れがないことを確認してから Step 4 のマージに進んだ。",[50,319,324],{"className":320,"code":322,"language":323},[321],"language-text","[検証OK] 341チャンク → 31セクション（漏れ0、重複0）\n","text",[23,325,322],{"__ignoreMap":55},[14,327,328,329,332],{},"Step 4 のマージ実行は10秒くらいで終わり、Step 5 で ",[18,330,331],{},"FTS検索が新セクション粒度で正しく動くか"," を「連結キャッシュ」で全文検索して確認。検索結果が節単位で返ってきて、マニュアルの該当章にジャンプできる動線になっていた。",[14,334,335,336,339],{},"翌朝に著者取得リトライ（CAPTCHA で中断していた残444件）を回す予定だったので、引き継ぎプロンプトを ",[23,337,338],{},"memo/2026-04-29/progress.md"," に書いて1日目のセッションを閉じた。",[36,341,343],{"id":342},"_2冊目-設例cf-qa411-95セクション","2冊目: 設例CF Q&A（411 → 95セクション）",[14,345,346,347,350],{},"午後から「設例 連結キャッシュ・フロー計算書Q&A」（411チャンク）に同じコマンドを流した。こちらは Q1〜Q95 の ",[18,348,349],{},"問答形式","で、Qごとにページ範囲が変わる構造。",[14,352,353,354,357],{},"ここで yomitoku の OCR 結果に1つ問題が見つかった。",[18,355,356],{},"Q1-2 の見出しを OCR が拾えていなかった","。p.15 から始まるはずなのに、目次から推定したページとチャンク冒頭の見出しが噛み合わない。「Q1-2 が抜けている」と思ってチャンク本体を p.15 から目視で読んだら、Q1-2 の本文自体は普通に入っていて、ただ見出し行が画像認識から落ちていただけだった。",[50,359,362],{"className":360,"code":361,"language":323},[321],"[OCR崩れ] Q1-2 見出し欠損 → p.15 本文を目視確認 → セクション境界を手動で指定\n",[23,363,361],{"__ignoreMap":55},[14,365,366,367,171],{},"セクション境界を Q1-1 → p.13、Q1-2 → p.15 と手で書き戻して、マージスクリプトを流した。",[18,368,369],{},"411チャンク → 95セクション、316チャンク削除",[36,371,373],{"id":372},"蔵書カラムに整済を並べて進捗を一目で読めるようにした","蔵書カラムに「整」「済」を並べて進捗を一目で読めるようにした",[14,375,376,377,380,381,384,385,388],{},"2冊の再構造化が終わったところで、ユーザーから「書籍ページの蔵書カラムに進捗が出てほしい」と要望が来た。",[23,378,379],{},"/books"," の URL を見ると ",[23,382,383],{},"cleanup_status"," のタグ付き書籍が緑バッジで出ているのに、",[23,386,387],{},"restructure_status"," 側が UI に出ていない。",[14,390,391],{},"要望を整理すると4つあった。",[393,394,397,412,418,428],"ul",{"className":395},[396],"contains-task-list",[398,399,402,407,408,411],"li",{"className":400},[401],"task-list-item",[403,404],"input",{"checked":405,"disabled":405,"type":406},true,"checkbox"," ",[23,409,410],{},"consolidated-cash-flow-manual"," に「済」タグを付与",[398,413,415,417],{"className":414},[401],[403,416],{"checked":405,"disabled":405,"type":406}," 蔵書カラムで「整」（黄色: cleanup_status）と「済」（緑: restructure_status）を区別表示",[398,419,421,423,424,427],{"className":420},[401],[403,422],{"checked":405,"disabled":405,"type":406}," 書籍ページに ",[23,425,426],{},"/shelf"," への動線を右寄せで追加（本棚アイコン風）",[398,429,431,407,433,435],{"className":430},[401],[403,432],{"checked":405,"disabled":405,"type":406},[23,434,25],{}," コマンドの末尾に「済」タグ自動付与処理を組み込み",[14,437,438,440,441,444,445,447,448,451,452,455],{},[23,439,383],{}," バッジは既に実装されていて、",[23,442,443],{},"books"," テーブルに DB 列があった。",[23,446,387],{}," は DB 列がまだなくて、",[23,449,450],{},"/api/books"," 側で ",[23,453,454],{},"data/restructure-history/{bookId}.md"," の存在チェックで動的に判定する形にした。これで履歴ファイルが生成された瞬間に蔵書一覧へバッジが反映される。",[50,457,461],{"className":458,"code":459,"language":460,"meta":55,"style":55},"language-vue shiki shiki-themes vitesse-light vitesse-light","\u003C!-- 蔵書カラム: 整（黄）と 済（緑）を縦に並べる -->\n\u003Cdiv class=\"status-badges\">\n  \u003Cspan v-if=\"book.cleanup_status === 'done'\" class=\"badge yellow\">整\u003C/span>\n  \u003Cspan v-if=\"book.restructure_status === 'done'\" class=\"badge green\">済\u003C/span>\n\u003C/div>\n","vue",[23,462,463,468,493,499,504],{"__ignoreMap":55},[59,464,465],{"class":61,"line":62},[59,466,467],{"class":65},"\u003C!-- 蔵書カラム: 整（黄）と 済（緑）を縦に並べる -->\n",[59,469,470,473,476,479,482,485,488,490],{"class":61,"line":69},[59,471,472],{"class":80},"\u003C",[59,474,475],{"class":230},"div",[59,477,478],{"class":76}," class",[59,480,481],{"class":80},"=",[59,483,484],{"class":94},"\"",[59,486,487],{"class":98},"status-badges",[59,489,484],{"class":94},[59,491,492],{"class":80},">\n",[59,494,495],{"class":61,"line":107},[59,496,498],{"class":497},"sG7-3","  \u003Cspan v-if=\"book.cleanup_status === 'done'\" class=\"badge yellow\">整\u003C/span>\n",[59,500,501],{"class":61,"line":245},[59,502,503],{"class":497},"  \u003Cspan v-if=\"book.restructure_status === 'done'\" class=\"badge green\">済\u003C/span>\n",[59,505,506,509,511],{"class":61,"line":264},[59,507,508],{"class":80},"\u003C/",[59,510,475],{"class":230},[59,512,492],{"class":80},[14,514,515,516,518],{},"書籍ページ右上には本棚アイコン付きで ",[23,517,426],{}," リンクを置いた。蔵書一覧へすぐ戻れる動線が消えていたので、左側の蔵書カウンタと対称になる位置に右寄せした。",[36,520,522],{"id":521},"shelf-画面のカバー画像にも済バッジを左上に乗せた","/shelf 画面のカバー画像にも「済」バッジを左上に乗せた",[14,524,525,527,528,531,532,535],{},[23,526,426],{}," を再読み込みしたら、連結CFマニュアルのカードに「済」バッジが",[18,529,530],{},"出ていなかった","。実装を見たら ",[18,533,534],{},"著者がNULL の本ではバッジ表示自体が消える"," ロジックになっていた。著者バッジと「済」バッジを同じ条件分岐に入れていたのが原因。",[14,537,538,539,542],{},"カバー画像の左上に",[18,540,541],{},"白文字 + 緑背景","でバッジを乗せる形に書き直して、著者の有無に依存しないよう分岐を分けた。これで著者未取得の本でも進捗だけは読める。",[36,544,546],{"id":545},"_3冊目は-ocr-マージ失敗で保留","3冊目は OCR マージ失敗で保留",[14,548,549,550,553,554,556],{},"ユーザーから「『この取引でB/S・P/L はどう動く？ 財務数値への影響がわかるケース100』も処理してほしい」と追加依頼が来たが、",[23,551,552],{},"bs-pl-impact-100-cases","（180チャンク）に ",[23,555,25],{}," を流したところで止まった。",[14,558,559,560,563,564,567],{},"タイトルは「ケース100」なのに",[18,561,562],{},"ケース41あたりから始まっていて","、ケース1〜40がDBに入っていない。yomitoku の OCR を上下分割で回した時の",[18,565,566],{},"マージ失敗","が原因だった。下半分のチャンクだけが残って、上半分が捨てられていた。",[14,569,570,571,574,575,577],{},"ユーザーが「分割マージを失敗したやつだから、とりあえずいいです」と判断したので、この本は ",[18,572,573],{},"再OCR待ちで再構造化を保留","した。",[23,576,25],{}," は完全なチャンクが揃っている本でしか動かない。",[36,579,580],{"id":580},"学び",[393,582,583,600,606,615,621],{},[398,584,585,588,589,591,592,595,596,599],{},[18,586,587],{},"スラッシュコマンドはDB API変更に弱い",": ローカルDB前提のコードを ",[23,590,291],{}," に埋め込んでいると、DB引っ越しで全文書き直しになる。Turso クライアントの呼び出し API 差分は ",[23,593,594],{},"prepare/all"," → ",[23,597,598],{},"execute({sql, args})"," 1パターンだったので、半日で書き換えが終わった",[398,601,602,605],{},[18,603,604],{},"OCR の見出し欠損はチャンク本体を目視で読むまで気付けない",": Q1-2 の見出しが抜けていた件は、目次の推定ページと本文冒頭が噛み合わないことから露見した。セクション統合スクリプトの完全性検証で「未割り当てチャンクが0」を確認するチェックが効いた",[398,607,608,611,612,614],{},[18,609,610],{},"進捗バッジは履歴ファイルの存在で動的判定にする",": DB列を増やすと migrations が要るが、",[23,613,454],{}," の存在チェックなら API 側だけで完結する。バッジが付くタイミング = 履歴ファイル生成時、で揃う",[398,616,617,620],{},[18,618,619],{},"著者NULL でも進捗バッジは出す",": 蔵書カバーのバッジ表示条件に著者の有無を混ぜると、未取得の本がUI上で消える。状態バッジと著者バッジは別レイヤーに分ける",[398,622,623,626,627,629],{},[18,624,625],{},"OCR マージ失敗は再構造化の前段で気付ける",": チャンク数が想定の半分以下、開始ページがおかしい、という兆候で再OCR を判断できる。",[23,628,25],{}," を入口の検証ステップとしても使える",[36,631,632],{"id":632},"次にやりたいこと",[393,634,636,642,653,662],{"className":635},[396],[398,637,639,641],{"className":638},[401],[403,640],{"disabled":405,"type":406}," 著者取得リトライの残444件を CAPTCHA クールダウン明けに再実行する",[398,643,645,407,647,649,650,652],{"className":644},[401],[403,646],{"disabled":405,"type":406},[23,648,552],{}," を yomitoku の上下分割マージから流し直して再OCR、その後 ",[23,651,25],{}," を再実行する",[398,654,656,658,659,661],{"className":655},[401],[403,657],{"disabled":405,"type":406}," 再構造化済み2冊の節セクションに対して、書籍横断のFTS検索（章タイトル + 節本文）を ",[23,660,426],{}," 検索ボックスから引けるようにする",[398,663,665,667,668,670],{"className":664},[401],[403,666],{"disabled":405,"type":406}," 残りの蔵書（税効果会計の教科書、連結精算表の入門書など）を順次 ",[23,669,25],{}," に流して「済」バッジを揃える",[672,673,674],"style",{},"html pre.shiki code .sxvE3, html code.shiki .sxvE3{--shiki-default:#A0ADA0;--shiki-dark:#A0ADA0}html pre.shiki code .stQ0i, html code.shiki .stQ0i{--shiki-default:#AB5959;--shiki-dark:#AB5959}html pre.shiki code .s4oTP, html code.shiki .s4oTP{--shiki-default:#B07D48;--shiki-dark:#B07D48}html pre.shiki code .shFtX, html code.shiki .shFtX{--shiki-default:#999999;--shiki-dark:#999999}html pre.shiki code .senZ8, html code.shiki .senZ8{--shiki-default:#59873A;--shiki-dark:#59873A}html pre.shiki code .sMJiu, html code.shiki .sMJiu{--shiki-default:#B5695977;--shiki-dark:#B5695977}html pre.shiki code .sdGka, html code.shiki .sdGka{--shiki-default:#B56959;--shiki-dark:#B56959}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHkkW, html code.shiki .sHkkW{--shiki-default:#1E754F;--shiki-dark:#1E754F}html pre.shiki code .sz8Xr, html code.shiki .sz8Xr{--shiki-default:#998418;--shiki-dark:#998418}html pre.shiki code .sG7-3, html code.shiki .sG7-3{--shiki-default:#393A34;--shiki-dark:#393A34}",{"title":55,"searchDepth":69,"depth":69,"links":676},[677,678,679,680,681,682,683,684],{"id":38,"depth":69,"text":39},{"id":295,"depth":69,"text":296},{"id":342,"depth":69,"text":343},{"id":372,"depth":69,"text":373},{"id":521,"depth":69,"text":522},{"id":545,"depth":69,"text":546},{"id":580,"depth":69,"text":580},{"id":632,"depth":69,"text":632},"diary","ローカルDB前提だった旧 /restructure-book コマンドを Turso API 用に全面書き換えし、連結CFマニュアル（341→31）と設例CF Q&A（411→95）の2冊を再構造化。蔵書UIに「整」「済」バッジを並べて進捗を一目で読めるようにし、書籍ページから /shelf へのリンクも右上に追加した。","md",{},"/2026-04-29-restructure-book-turso-migration","book-knowledge-base",false,"2026-04-29T00:00:00.000Z",{"title":5,"description":686},"2026-04/2026-04-29/restructure-book-turso-migration",[696,697,698,699,700,701],"OCR","yomitoku","TursoDB","書籍デジタル化","Vue","Claude Code",null,"lA_i3Us6jYgvFVl51na-vJuRnRtE2zEnWQ8_40m4opU",[],"https://log.eurekapu.com/favicon.svg",1777533703506]