claude-code-session-sync 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,133 @@
1
+ """§4.1 分類:安全閘 → identity-collision → identical/ff/superset-branch/fork。
2
+
3
+ 依據 DESIGN §4.1 + 附錄 B + PLAN v0.4 §2.3,並納入 codex round 4(程式碼審查)修正:
4
+ - **fast-forward 必須是「單一新 genuine tip 的純延伸」**:bigger 恰有一個 genuine leaf、
5
+ 它由 smaller 的 tip 延伸而來、active_tip 指向它、且確有新增 uuid 行。多 genuine leaf /
6
+ 只差內容 no-uuid / compact 子樹 → superset-branch(不可自動 ff)。
7
+ - **active_tip 必須解析到「存在的 genuine leaf」**;指向 missing/fan-out/sidechain → 不可 ff。
8
+ - **跨檔同 uuid 不同 hash → DAMAGED**(歷史行被改寫/損壞,不可當普通 fork 進合併)。
9
+ - 多個非-system 對話根(任一側)→ needs-decision(結構異常/已被合併過)。
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass
14
+ from enum import Enum
15
+
16
+ from .lineset import PRESERVE_META_TYPES, SessionShape, is_ancestor
17
+
18
+
19
+ class Klass(str, Enum):
20
+ IDENTICAL = "identical"
21
+ FAST_FORWARD = "fast-forward"
22
+ SUPERSET_BRANCH = "superset-branch"
23
+ FORK = "fork"
24
+ DAMAGED = "damaged"
25
+ IDENTITY_COLLISION = "identity-collision"
26
+ NEEDS_DECISION = "needs-decision"
27
+
28
+
29
+ @dataclass
30
+ class Classification:
31
+ klass: Klass
32
+ direction: str | None # 'hub->local' | 'local->hub' | None
33
+ reason: str
34
+ metadata_differs: bool = False # IDENTICAL 但揮發 meta(title 等)不同 → 由上層決定是否提示
35
+
36
+
37
+ def _genuine_tip(shape: SessionShape) -> str | None:
38
+ """回唯一可信賴的 genuine tip uuid;無法確定(指標無效/多葉無指標)回 None。"""
39
+ leaf_uuids = {leaf.uuid for leaf in shape.genuine_leaves}
40
+ if shape.active_tip is not None:
41
+ # active_tip 必須是「存在的 genuine leaf」,否則視為無效(不可 ff)
42
+ return shape.active_tip if shape.active_tip in leaf_uuids else None
43
+ if len(leaf_uuids) == 1:
44
+ return next(iter(leaf_uuids))
45
+ return None
46
+
47
+
48
+ def _nonsystem_roots(shape: SessionShape) -> list:
49
+ return [r for r in shape.roots if r.type != "system"]
50
+
51
+
52
+ def _classify_superset(bigger: SessionShape, smaller: SessionShape, direction: str) -> Classification:
53
+ """bigger ⊋ smaller:判 fast-forward vs superset-branch vs needs-decision。"""
54
+ # 新增的「非-system 根」= 疑似注入不相關對話 → 不可自動套用(H4 / codex r3)
55
+ if any(r.type != "system" for r in bigger.roots if r.uuid not in smaller.uuids):
56
+ return Classification(Klass.NEEDS_DECISION, None, "superset 含新增非-system 根(疑似注入不相關對話)")
57
+
58
+ big_tip = _genuine_tip(bigger)
59
+ if big_tip is None:
60
+ return Classification(
61
+ Klass.NEEDS_DECISION, None,
62
+ "active-tip 無法解析到單一存在的 genuine leaf(指向 missing/fan-out/sidechain 或多葉無指標)",
63
+ )
64
+
65
+ extra_uuids = bigger.uuids - smaller.uuids
66
+ small_tip = _genuine_tip(smaller)
67
+ # fast-forward 嚴格條件(codex r4):有新增 uuid 行、bigger 恰一個 genuine leaf、
68
+ # 它就是 active tip、且由 smaller 的 tip 延伸而來。
69
+ if (
70
+ extra_uuids
71
+ and len(bigger.genuine_leaves) == 1
72
+ and small_tip is not None
73
+ and is_ancestor(bigger, small_tip, big_tip)
74
+ ):
75
+ # ff local->hub 會**覆蓋** hub(smaller);若 hub 有 local(bigger) 缺的標題行(custom/ai-title),
76
+ # 覆蓋會靜默丟使用者設定 → 交人決策(codex r19;hub->local 走 keep-both 不覆蓋、不丟)。
77
+ if direction == "local->hub":
78
+ bigger_ids = set(bigger.order)
79
+ dropped = [ln for ln in smaller.lines
80
+ if ln.type in PRESERVE_META_TYPES and ln.identity not in bigger_ids]
81
+ if dropped:
82
+ return Classification(
83
+ Klass.NEEDS_DECISION, None,
84
+ "ff 覆蓋 hub 會丟失 hub 端標題(custom-title/ai-title)→ 交人決策(不靜默丟使用者設定)",
85
+ )
86
+ return Classification(Klass.FAST_FORWARD, direction, "純延伸:唯一 genuine leaf 由 active tip 延伸")
87
+ return Classification(
88
+ Klass.SUPERSET_BRANCH, None,
89
+ "整行集合包含但非純延伸(多 genuine leaf / 僅內容 no-uuid 差異 / compact 子樹),不可自動 ff",
90
+ )
91
+
92
+
93
+ def classify(local: SessionShape, hub: SessionShape) -> Classification:
94
+ # 1) 安全閘:檔級 damaged / 壞行 / 單檔內同 uuid 異 hash
95
+ if local.is_damaged or hub.is_damaged:
96
+ which = "+".join(w for w, s in (("local", local), ("hub", hub)) if s.is_damaged)
97
+ return Classification(Klass.DAMAGED, None, f"damaged: {which}(壞檔/壞行/同檔同 uuid 異 hash)")
98
+
99
+ # 2) 身分基礎:兩側都要有對話 uuid
100
+ if not local.uuids or not hub.uuids:
101
+ return Classification(Klass.NEEDS_DECISION, None, "至少一側無對話 uuid 行,無法建立同一性")
102
+
103
+ common = local.uuids & hub.uuids
104
+ if not common:
105
+ return Classification(Klass.IDENTITY_COLLISION, None, "零共同 uuid(撞 sessionId / 錯夾 / 不同對話)")
106
+
107
+ # 3) 跨檔同 uuid 不同 hash → DAMAGED(歷史行被改寫/損壞,不可當普通 fork)
108
+ for u in common:
109
+ lh, hh = local.uuid_hashes.get(u, set()), hub.uuid_hashes.get(u, set())
110
+ if lh and hh and lh.isdisjoint(hh):
111
+ return Classification(Klass.DAMAGED, None, f"跨檔同 uuid 不同 hash({u[:8]} 歷史行被改寫/損壞)")
112
+
113
+ # 4) 多個非-system 對話根(任一側)→ 結構異常(已被合併過 / 注入),交人
114
+ if len(_nonsystem_roots(local)) > 1 or len(_nonsystem_roots(hub)) > 1:
115
+ return Classification(Klass.NEEDS_DECISION, None, "出現多個非-system 對話根(結構異常),交人決策")
116
+
117
+ # 5) 比較(排除揮發 meta 的多重集合)
118
+ la, lb = local.compare_multiset, hub.compare_multiset
119
+ if la == lb:
120
+ if local.active_tip == hub.active_tip:
121
+ meta = local.multiset != hub.multiset
122
+ note = "行多重集合相同且 active-tip 相同" + ("(但揮發 meta 不同)" if meta else "")
123
+ return Classification(Klass.IDENTICAL, None, note, metadata_differs=meta)
124
+ return Classification(Klass.NEEDS_DECISION, None, "內容相同但 active-tip 不同(指標歧異)")
125
+
126
+ hub_superset = la <= lb
127
+ local_superset = lb <= la
128
+ if hub_superset and not local_superset:
129
+ return _classify_superset(hub, local, "hub->local")
130
+ if local_superset and not hub_superset:
131
+ return _classify_superset(local, hub, "local->hub")
132
+
133
+ return Classification(Klass.FORK, None, "互有對方沒有的行、有共同祖先(fork)")