113113# 墓標に添える理由の長さ。**1 行で足りる** —— なぜ外したかが読めればよく、
114114# 本文はそのまま残る(理由で上書きしない)ので、長く書かせる先はこちらではない
115115MAX_REMOVED_REASON_CHARS = 400
116+ # 直す前の本文を控えておく長さ(`_before_of`)。この層の本文は数百字で、
117+ # 実測でも 100〜200 字に収まる —— 天井は「切られずに全文が残る」側に置く。
118+ # 1 件につき 1 回分しか持たないので、これだけあっても増え続けはしない
119+ MAX_BEFORE_BODY_CHARS = 4000
116120
117121# 1 回で見る区画の上限。**区画ごとに AI を 1 回呼ぶ**(素材をその区画のぶんに
118122# 絞るのが区画の意味なので、まとめて聞くと絞った意味が消える)ため、
@@ -2423,23 +2427,30 @@ def changed_here(name: str, sources: dict, sweep: str, limit: int = 100) -> list
24232427
24242428
24252429def doc_versions (name : str , sources : dict , title : str ) -> dict :
2426- """1 件の見出しについて、**いまの世代と 1 つ前の世代**の中身を返す 。
2430+ """1 件の見出しについて、**いまの中身と、直す前の中身**を返す 。
24272431
24282432 「直近の変更」に並ぶのは動いた見出しの名前までで、**何がどう変わったのかは
24292433 そこからは読めない** —— 件数と名前が分かっても、プロンプトを直す判断には
24302434 「どう書き換わったか」が要る。
24312435
2432- **比べられるのは 1 つ前まで**(ブルーグリーンが残す世代がそこまで)。
2433- それより古い回の行から来ても、出せるのは最新の焼き直しのぶん —— 読む人が
2434- 取り違えないよう、**どの世代どうしを比べたかを一緒に返す**。
2436+ **直す前の中身は 1 件の側が持っている**(`_before_of`)。持っていれば
2437+ `kept` に入れて返す —— こちらは**その 1 件を最後に直した回の直前**なので、
2438+ 後から別の回が何度焼き直そうと残る。
2439+
2440+ 世代どうしの比較も一緒に返すが、**あちらは 1 つ前の焼き直しまで**
2441+ (ブルーグリーンが残す世代がそこまで)。間隔の短い回が 1 度走れば比べる相手は
2442+ 入れ替わるので、控えを持たない 1 件のための補助として使う。読む人が取り違え
2443+ ないよう、**どの世代どうしを比べたかも一緒に返す**。
24352444 """
24362445 src = sources .get (name )
24372446 if src is None :
2438- return {"now" : None , "before" : None , "now_stamp" : "" , "before_stamp" : "" }
2447+ return {"now" : None , "before" : None , "kept" : {}, " now_stamp" : "" , "before_stamp" : "" }
24392448 before_path = _previous_generation (src .path )
2449+ now = _doc_at (src .path , title )
24402450 return {
2441- "now" : _doc_at ( src . path , title ) ,
2451+ "now" : now ,
24422452 "before" : _doc_at (before_path , title ) if before_path else None ,
2453+ "kept" : before_of (now ) if now else {},
24432454 "now_stamp" : src .dump_date or "" ,
24442455 "before_stamp" : _stamp_of (before_path ) if before_path else "" ,
24452456 }
@@ -2473,7 +2484,8 @@ def _doc_at(path: Path, title: str) -> dict | None:
24732484 with suppress (Exception ):
24742485 rows = db .query (
24752486 path ,
2476- "SELECT doc_id, title, body, tags, updated_at FROM docs WHERE title = ? LIMIT 1" ,
2487+ "SELECT doc_id, title, body, tags, updated_at, extra FROM docs"
2488+ " WHERE title = ? LIMIT 1" ,
24772489 (title ,),
24782490 )
24792491 if rows :
@@ -2490,6 +2502,7 @@ def _doc_at(path: Path, title: str) -> dict | None:
24902502 "body" : row ["body" ] or "" ,
24912503 "tags" : [str (t ) for t in tags ] if isinstance (tags , list ) else [],
24922504 "updated_at" : row ["updated_at" ] or "" ,
2505+ "extra" : load_json (row ["extra" ]),
24932506 }
24942507 return None
24952508
@@ -2704,6 +2717,7 @@ def stream_docs(
27042717 `diff` を渡すと、数えた結果をそこへ書く(戻り値にできないため)。
27052718
27062719 **動かした 1 件には、どの回が動かしたかを脇書きに残す**(`_stamped`)。
2720+ 直した 1 件には、**直す前の中身も 1 回分だけ**控える(`_before_of`)。
27072721
27082722 `unreviewed` は「この回で入るものに、まだ AI が目を通していない印を付ける」
27092723 (機械で引く回・外の道具で引く回)。`reviewed` は**この回で AI に差し込んだ見出し**で、
@@ -2780,7 +2794,9 @@ def stream_docs(
27802794 skipped += 1
27812795 yield {** doc , "doc_id" : before ["doc_id" ]}
27822796 continue
2783- yield _stamped (_reviewed ({** doc , "doc_id" : before ["doc_id" ]}), sweep , "updated" )
2797+ yield _stamped (
2798+ _reviewed ({** doc , "doc_id" : before ["doc_id" ]}), sweep , "updated" , before ,
2799+ )
27842800
27852801 next_id += 1
27862802 for raw in edits_of .rest ():
@@ -2933,7 +2949,7 @@ def _to_doc(raw: dict, now: str, web: bool) -> dict | None:
29332949 # **運ばれてきた事実を先に置く。** 集める側が元の記事から写した値(知名度など)が
29342950 # ここに入る —— 下で入れるものが鍵を持っていたら、そちらを優先する。
29352951 # **この時点では「消して」の印(null)も混じる**(重ねるときに解く)
2936- extra = {** _carried (raw .get ("extra" )), "collected_at" : now , "web" : bool (web )}
2952+ extra = {** _carried (raw .get ("extra" )), COLLECTED_AT_KEY : now , "web" : bool (web )}
29372953 if url := (raw .get ("url" ) or "" ).strip ():
29382954 extra ["url" ] = url
29392955 # **配信日は、集めた日と別に持つ**。フィードから機械的に溜めるときに入る ——
@@ -2990,9 +3006,17 @@ def _buried(doc: dict, raw: dict, now: str) -> dict:
29903006# 脇書きに残す「最後に動かした回」の鍵。
29913007CHANGED_BY_KEY = "changed_by"
29923008CHANGE_KEY = "change"
3009+ # 直す前の中身を控えておく鍵(`_before_of`)。
3010+ BEFORE_KEY = "before"
3011+ # その 1 件を最後に集めた時刻(`_to_doc`)。AI が触った回には必ず入り直す
3012+ COLLECTED_AT_KEY = "collected_at"
3013+ # 前と比べない脇書き。**こちらが回ごとに押す印なので、混ぜると毎回「変わった」に
3014+ # なる** —— 本当に動いた事実が埋もれる。控えに入れると、さらに控えの中に控えが入り、
3015+ # 焼き直すたびに入れ子が 1 段深くなる(1 件が際限なく伸びる)
3016+ MARGIN_KEYS = (CHANGE_KEY , CHANGED_BY_KEY , BEFORE_KEY , COLLECTED_AT_KEY )
29933017
29943018
2995- def _stamped (doc : dict , sweep : str , change : str ) -> dict :
3019+ def _stamped (doc : dict , sweep : str , change : str , before : dict | None = None ) -> dict :
29963020 """動かした 1 件に、**どの回が・どう動かしたか**を脇書きとして押す。
29973021
29983022 **変更履歴を別に持つだけでは、読みたいほうが読めない。** 控え
@@ -3013,12 +3037,65 @@ def _stamped(doc: dict, sweep: str, change: str) -> dict:
30133037 **鍵の数の天井(`MAX_EXTRA_KEYS`)より後に押す。** あの天井は AI が運んでくる
30143038 事実が際限なく増えないためのもので、こちらが押す印まで落とすと、
30153039 脇書きの多い 1 件だけ印が付かないことになる(いちばん読みたい 1 件がそれになる)。
3040+
3041+ `before` を渡すと、**直す前の中身も 1 回分だけ一緒に控える**(`_before_of`)。
30163042 """
30173043 extra = doc .get ("extra" ) if isinstance (doc .get ("extra" ), dict ) else {}
30183044 stamp = {CHANGE_KEY : change }
30193045 if sweep :
30203046 stamp [CHANGED_BY_KEY ] = sweep
3021- return {** doc , "extra" : {** extra , ** stamp }}
3047+ merged = {** extra , ** stamp }
3048+ # **印と控えは必ず揃える。** 前の回の控えを残したまま印だけ新しくすると、
3049+ # 画面には「この回が直す前」として前の回の中身が出る
3050+ if kept := _before_of (doc , before ):
3051+ merged [BEFORE_KEY ] = kept
3052+ else :
3053+ merged .pop (BEFORE_KEY , None )
3054+ return {** doc , "extra" : merged }
3055+
3056+
3057+ def _before_of (doc : dict , before : dict | None ) -> dict :
3058+ """直す前の中身のうち、**この回で変わったところだけ**。
3059+
3060+ 世代の比較(`doc_versions` の `_previous_generation`)では、
3061+ **その回が直したものを見に行った頃にはもう流れている** —— 残る世代は 1 つ前
3062+ までで、巡回は回るたびに焼き直すので、間隔の短い回が 1 度走っただけで
3063+ 比べる相手が入れ替わる。1 件の側に控えておけば、次に同じ 1 件が動くまで残る。
3064+
3065+ **本文だけではない。** この層の直しは、本文の書き換えと同じくらいタグの
3066+ 付け替え(分類そのもの)と脇書きの差し替え(出典・配信日・座標)で起きる ——
3067+ 本文しか控えないと、「何も変わっていないのに動いた 1 件」が並ぶ。
3068+
3069+ **変わっていないものは入れない。** 丸ごと控えると 1 件の大きさが倍になり、
3070+ 焼き直すたびに全件がそのぶん重くなる。
3071+
3072+ **見出しは控えない。** この層で 1 件を指す鍵は見出しで、直す回は見出しで
3073+ 引き当てている(`stream_docs`)—— 書き換えた見出しは別の 1 件として通るので、
3074+ ここへ来る時点で見出しは必ず同じ。
3075+ """
3076+ if not isinstance (before , dict ):
3077+ return {}
3078+ kept : dict = {}
3079+ was = (before .get ("body" ) or "" ).strip ()
3080+ if was != (doc .get ("body" ) or "" ).strip ():
3081+ kept ["body" ] = was [:MAX_BEFORE_BODY_CHARS ]
3082+ if sorted (before .get ("tags" ) or []) != sorted (doc .get ("tags" ) or []):
3083+ kept ["tags" ] = [str (t ) for t in (before .get ("tags" ) or [])]
3084+ if (facts := facts_of (before )) != facts_of (doc ):
3085+ kept ["extra" ] = facts
3086+ return kept
3087+
3088+
3089+ def facts_of (doc : dict ) -> dict :
3090+ """脇書きのうち、**その 1 件についての事実だけ**(こちらが押す印を外す)。
3091+
3092+ 印(`MARGIN_KEYS`)は回ごとに必ず書き換わるので、混ぜたまま前と比べると
3093+ **毎回「脇書きが変わった」になる** —— 本当に変わった事実が埋もれる。
3094+ """
3095+ extra = doc .get ("extra" )
3096+ if not isinstance (extra , dict ):
3097+ return {}
3098+ return {k : v for k , v in extra .items () if k not in MARGIN_KEYS }
30223099
30233100
30243101def changed_by (doc : dict ) -> str :
@@ -3027,6 +3104,17 @@ def changed_by(doc: dict) -> str:
30273104 return str ((extra or {}).get (CHANGED_BY_KEY ) or "" ) if isinstance (extra , dict ) else ""
30283105
30293106
3107+ def before_of (doc : dict ) -> dict :
3108+ """その 1 件を最後に動かした回が、**直す前の中身**(変わったところだけ)。
3109+
3110+ 持っていなければ空。入っているのは `body` / `tags` / `extra` のうち、
3111+ その回で実際に動いたものだけ。
3112+ """
3113+ extra = doc .get ("extra" )
3114+ kept = extra .get (BEFORE_KEY ) if isinstance (extra , dict ) else None
3115+ return kept if isinstance (kept , dict ) else {}
3116+
3117+
30303118def _unreviewed (doc : dict ) -> dict :
30313119 """まだ AI が目を通していない印を付ける(`notes.UNREVIEWED_TAG`)。"""
30323120 tags = [t for t in (doc .get ("tags" ) or []) if t != notes .UNREVIEWED_TAG ]
0 commit comments