Skip to content

Commit c7f259c

Browse files
committed
直した 1 件に、直す前の中身を 1 回分だけ控える
世代どうしの比較では、その回が何を直したのかを見に行った頃にはもう流れている。 残る世代は 1 つ前までで、巡回は回るたびに焼き直すので、間隔の短い回が 1 度走れば 比べる相手は入れ替わっている。1 件の側に控えれば、次に同じ 1 件が動くまで残る。 控えるのは本文・タグ・脇書きのうち、その回で実際に動いたものだけ(extra.before)。 この層の直しは本文の書き換えと同じくらいタグの付け替え(分類そのもの)と脇書きの 差し替え(出典・座標)で起きるので、本文しか控えないと「何も変わっていないのに 動いた 1 件」が並ぶ。丸ごと控えないのは、1 件の大きさが倍になるため。 見出しは控えない。直す回は見出しで引き当てるので、ここへ来る時点で必ず同じ。 こちらが回ごとに押す脇書き(change / changed_by / before / collected_at)は 比べない。混ぜると毎回「脇書きが変わった」になって本当に動いた事実が埋もれ、 控えの中に控えが入って焼き直すたびに 1 段深くなる。 印と控えは必ず揃えて書き換える。前の回の控えを残したまま印だけ新しくすると、 画面には「この回が直す前」として前の回の中身が出る。 差分の画面は本文・タグ・脇書きを別々に出し、控えと世代のどちらと比べたものかを 書く。出所の違う 2 つを黙って並べると、同じ回のものとして読まれる。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 2123136 commit c7f259c

5 files changed

Lines changed: 451 additions & 25 deletions

File tree

CLAUDE.md

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -713,10 +713,14 @@ GeoNames 全世界地名辞典 = `geonames`(いずれも 348 言語版・195 か
713713
同じ名前での作り直しは普通に起きる
714714
**動いた見出しは押せる**(`/admin/collect/{name}/doc?title=…`)—— 名前だけ
715715
並べても「どう書き換わったか」は読めず、プロンプトを直す判断に要るのは中身の
716-
変化のほう。出すのは**いまの世代と 1 つ前の世代の差分**(本文は行単位、タグは
717-
出入りを別に)で、**比べられるのは 1 つ前まで**(ブルーグリーンが残す世代が
718-
そこまで)。古い回の行から来ても出せるのは最新の焼き直しのぶんなので、
719-
**どの世代どうしを比べたかを画面に書く** —— 書かないと、その回の変更として読まれる。
716+
変化のほう。**比べるのは 1 件が控えている「直す前」**(`extra.before`)——
717+
その 1 件を最後に直した回の直前なので、後から何度焼き直しても残る。
718+
**本文・タグ・脇書きの 3 つを別々に出す** —— この層の直しは本文の書き換えと
719+
同じくらいタグの付け替え(分類そのもの)と脇書きの差し替え(出典・座標)で
720+
起きるので、混ぜると何が動いたのか読み取れない。控えを持たない 1 件だけ
721+
世代どうしの比較に落ちる(**あちらは 1 つ前まで**)。
722+
**どちらと比べたものかを画面に必ず書く** —— 書かないと、世代の差分を
723+
その回の変更として読まれる。
720724
**1 行が答えるのは「どの回が・誰に頼んで・どこを見て・何を動かしたか」**
721725
(`sweep` / `backend` / `model` / `effort` / `scope`)—— 件数だけでは、
722726
ざっとの回なのか割り込みなのかも、どの範囲の話かも読めない。**頼んだ相手まで
@@ -782,6 +786,18 @@ GeoNames 全世界地名辞典 = `geonames`(いずれも 348 言語版・195 か
782786
回の間隔と関係なく読める**(画面の「その回が動かしたもの」/ `collect.changed_here`)。
783787
**残すのは 1 回分だけ**(上書きする)—— 溜めると 1 件ごとに際限なく伸び、
784788
焼き直すたびに全件がそのぶん重くなる。「いつ」は `updated_at` が既に持っている。
789+
**直した回は、直す前の中身も一緒に控える**(`extra.before` /
790+
`collect._before_of`)。世代どうしの比較では間に合わない —— 残る世代は
791+
1 つ前までで、巡回は回るたびに焼き直すので、間隔の短い回が 1 度走れば
792+
比べる相手はもう入れ替わっている。**控えるのは本文・タグ・脇書きのうち、
793+
その回で実際に動いたものだけ**(本文は `MAX_BEFORE_BODY_CHARS` まで)——
794+
丸ごと控えると 1 件の大きさが倍になる。**見出しは控えない**(直す回は
795+
見出しで引き当てるので、ここへ来る時点で必ず同じ)。**印と控えは必ず
796+
揃えて書き換える** —— 前の回の控えを残したまま印だけ新しくすると、画面には
797+
「この回が直す前」として前の回の中身が出る。**こちらが回ごとに押す脇書きは
798+
比べない**(`MARGIN_KEYS``change` / `changed_by` / `before` /
799+
`collected_at`)—— 混ぜると毎回「脇書きが変わった」になるうえ、
800+
控えの中に控えが入って焼き直すたびに 1 段深くなる。
785801
**置くのは脇書き** —— 全文検索が索引するのは見出しと本文だけ、AI へ差し込む
786802
一覧に載るのは配信日と出典だけなので、ここへ入れたものはどちらにも出てこない
787803
(本文やタグへ書くと、読む人にも AI にも「文書の中身」として見える)。

app/collect.py

Lines changed: 99 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@
113113
# 墓標に添える理由の長さ。**1 行で足りる** —— なぜ外したかが読めればよく、
114114
# 本文はそのまま残る(理由で上書きしない)ので、長く書かせる先はこちらではない
115115
MAX_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

24252429
def 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
# 脇書きに残す「最後に動かした回」の鍵。
29913007
CHANGED_BY_KEY = "changed_by"
29923008
CHANGE_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

30243101
def 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+
30303118
def _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

Comments
 (0)