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.
- claude_code_session_sync-0.1.0.dist-info/METADATA +87 -0
- claude_code_session_sync-0.1.0.dist-info/RECORD +31 -0
- claude_code_session_sync-0.1.0.dist-info/WHEEL +5 -0
- claude_code_session_sync-0.1.0.dist-info/entry_points.txt +2 -0
- claude_code_session_sync-0.1.0.dist-info/licenses/LICENSE +21 -0
- claude_code_session_sync-0.1.0.dist-info/top_level.txt +1 -0
- claude_session_sync/__init__.py +11 -0
- claude_session_sync/acks.py +279 -0
- claude_session_sync/anomaly.py +161 -0
- claude_session_sync/apply.py +874 -0
- claude_session_sync/atomicio.py +621 -0
- claude_session_sync/bootstrap.py +370 -0
- claude_session_sync/canonical.py +185 -0
- claude_session_sync/classify.py +133 -0
- claude_session_sync/cli.py +1065 -0
- claude_session_sync/config.py +128 -0
- claude_session_sync/doctor.py +351 -0
- claude_session_sync/fuzzy.py +136 -0
- claude_session_sync/lineset.py +143 -0
- claude_session_sync/memory.py +953 -0
- claude_session_sync/merge.py +836 -0
- claude_session_sync/pathsafe.py +91 -0
- claude_session_sync/py.typed +0 -0
- claude_session_sync/resolve.py +226 -0
- claude_session_sync/scan.py +485 -0
- claude_session_sync/session_merge.py +214 -0
- claude_session_sync/sidecar.py +238 -0
- claude_session_sync/snapshot.py +136 -0
- claude_session_sync/state.py +240 -0
- claude_session_sync/tombstone.py +330 -0
- claude_session_sync/transfer.py +462 -0
|
@@ -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)")
|