claude-smart 0.2.46 → 0.2.48

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.
Files changed (170) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +19 -11
  3. package/bin/claude-smart.js +290 -68
  4. package/package.json +1 -1
  5. package/plugin/.claude-plugin/plugin.json +1 -1
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/README.md +11 -10
  8. package/plugin/dashboard/app/layout.tsx +20 -0
  9. package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +36 -2
  10. package/plugin/dashboard/package-lock.json +61 -390
  11. package/plugin/dashboard/package.json +2 -2
  12. package/plugin/opencode/dist/server.mjs +76 -2
  13. package/plugin/opencode/server.mts +79 -2
  14. package/plugin/pyproject.toml +6 -2
  15. package/plugin/scripts/smart-install.sh +7 -1
  16. package/plugin/src/claude_smart/cli.py +210 -22
  17. package/plugin/src/claude_smart/context_format.py +9 -9
  18. package/plugin/src/claude_smart/cs_cite.py +66 -19
  19. package/plugin/uv.lock +5 -5
  20. package/plugin/vendor/reflexio/.env.example +7 -0
  21. package/plugin/vendor/reflexio/README.md +3 -3
  22. package/plugin/vendor/reflexio/pyproject.toml +2 -1
  23. package/plugin/vendor/reflexio/reflexio/README.md +11 -6
  24. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +1 -1
  25. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +2 -2
  26. package/plugin/vendor/reflexio/reflexio/cli/utils.py +44 -1
  27. package/plugin/vendor/reflexio/reflexio/client/client.py +97 -0
  28. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +8 -0
  29. package/plugin/vendor/reflexio/reflexio/lib/_base.py +15 -0
  30. package/plugin/vendor/reflexio/reflexio/lib/_config.py +23 -18
  31. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +9 -8
  32. package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +16 -1
  33. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +27 -16
  34. package/plugin/vendor/reflexio/reflexio/lib/_search.py +27 -5
  35. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +9 -0
  36. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +1 -0
  37. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +1 -0
  38. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/governance.py +117 -0
  39. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +45 -3
  40. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +37 -0
  41. package/plugin/vendor/reflexio/reflexio/server/README.md +38 -9
  42. package/plugin/vendor/reflexio/reflexio/server/__init__.py +21 -2
  43. package/plugin/vendor/reflexio/reflexio/server/api.py +274 -3267
  44. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +4 -3
  45. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +16 -1
  46. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +1 -1
  47. package/plugin/vendor/reflexio/reflexio/server/{_auth.py → auth.py} +2 -0
  48. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +62 -36
  49. package/plugin/vendor/reflexio/reflexio/server/deployment_profile.py +69 -0
  50. package/plugin/vendor/reflexio/reflexio/server/extensions.py +213 -0
  51. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +424 -0
  52. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_json_extraction.py +249 -0
  53. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_structured_output.py +195 -0
  54. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +152 -0
  55. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +980 -0
  56. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_types.py +110 -0
  57. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +73 -1819
  58. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +4 -4
  59. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +57 -5
  60. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +12 -1
  61. package/plugin/vendor/reflexio/reflexio/server/middleware.py +244 -0
  62. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +9 -1
  63. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +14 -2
  64. package/plugin/vendor/reflexio/reflexio/server/rate_limit.py +79 -0
  65. package/plugin/vendor/reflexio/reflexio/server/routes/__init__.py +6 -0
  66. package/plugin/vendor/reflexio/reflexio/server/routes/_common.py +25 -0
  67. package/plugin/vendor/reflexio/reflexio/server/routes/_metering.py +98 -0
  68. package/plugin/vendor/reflexio/reflexio/server/routes/braintrust.py +129 -0
  69. package/plugin/vendor/reflexio/reflexio/server/routes/config.py +210 -0
  70. package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +549 -0
  71. package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +259 -0
  72. package/plugin/vendor/reflexio/reflexio/server/routes/playbooks.py +578 -0
  73. package/plugin/vendor/reflexio/reflexio/server/routes/profiles.py +423 -0
  74. package/plugin/vendor/reflexio/reflexio/server/routes/provenance.py +345 -0
  75. package/plugin/vendor/reflexio/reflexio/server/routes/search.py +349 -0
  76. package/plugin/vendor/reflexio/reflexio/server/routes/system.py +261 -0
  77. package/plugin/vendor/reflexio/reflexio/server/scheduling.py +132 -0
  78. package/plugin/vendor/reflexio/reflexio/server/services/README.md +5 -2
  79. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/__init__.py +38 -0
  80. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_batch_progress.py +298 -0
  81. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_config_filter.py +152 -0
  82. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +243 -0
  83. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +299 -0
  84. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_status_change.py +273 -0
  85. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +258 -0
  86. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +17 -1183
  87. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +8 -1
  88. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +26 -41
  89. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +8 -0
  90. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +232 -123
  91. package/plugin/vendor/reflexio/reflexio/server/services/governance/config.py +52 -0
  92. package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +378 -0
  93. package/plugin/vendor/reflexio/reflexio/server/services/governance/subject_refs.py +34 -0
  94. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +385 -78
  95. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +9 -1
  96. package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_prompt_processing.py +100 -0
  97. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +121 -525
  98. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_clustering.py +184 -0
  99. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_postprocessing.py +130 -0
  100. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_prompt_formatting.py +212 -0
  101. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +258 -69
  102. package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +9 -9
  103. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +3 -2
  104. package/plugin/vendor/reflexio/reflexio/server/services/publish_learning_worker.py +288 -0
  105. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/recency.py +211 -0
  106. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +29 -13
  107. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +4 -0
  108. package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +681 -0
  109. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +43 -6
  110. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +10 -1167
  111. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +58 -351
  112. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +49 -19
  113. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +452 -0
  114. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +11 -4
  115. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +1 -2133
  116. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +7 -1126
  117. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +73 -33
  118. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +30 -0
  119. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/__init__.py +9 -0
  120. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_agent_run_store.py +506 -0
  121. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_pending_tool_call_store.py +704 -0
  122. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_run_tool_dependency_store.py +123 -0
  123. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/__init__.py +6 -0
  124. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +263 -0
  125. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +132 -0
  126. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/__init__.py +13 -0
  127. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_audit.py +122 -0
  128. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +465 -0
  129. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_purge.py +387 -0
  130. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_rebuild_hide.py +332 -0
  131. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +511 -0
  132. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/__init__.py +13 -0
  133. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +955 -0
  134. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +189 -0
  135. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_optimization.py +247 -0
  136. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +145 -0
  137. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +844 -0
  138. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/__init__.py +9 -0
  139. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +263 -0
  140. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +896 -0
  141. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_search.py +270 -0
  142. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +50 -9
  143. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +64 -370
  144. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +4 -4
  145. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +0 -909
  146. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +2 -0
  147. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +20 -0
  148. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/__init__.py +9 -0
  149. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_agent_run_store.py +86 -0
  150. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +195 -0
  151. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_pending_tool_call_store.py +148 -0
  152. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_run_tool_dependency_store.py +38 -0
  153. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/__init__.py +13 -0
  154. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_audit.py +29 -0
  155. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_erase_execution.py +30 -0
  156. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_purge.py +74 -0
  157. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_rebuild_hide.py +32 -0
  158. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_subject_barrier.py +49 -0
  159. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/__init__.py +13 -0
  160. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +365 -0
  161. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +124 -0
  162. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_optimization.py +85 -0
  163. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_source_linkage.py +47 -0
  164. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +333 -0
  165. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/__init__.py +9 -0
  166. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +73 -0
  167. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/{_profiles.py → profiles/_profile_store.py} +57 -86
  168. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_search.py +32 -0
  169. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +153 -12
  170. package/plugin/vendor/reflexio/reflexio/server/services/playbook/user_detail_stripping.py +0 -84
@@ -0,0 +1,387 @@
1
+ """SQLite purge-operation store methods.
2
+
3
+ Extracted verbatim from ``_governance.py`` (the PurgeOperationStore bucket): the
4
+ seven public methods (``begin_purge_operation``, ``record_purge_target``,
5
+ ``list_purge_targets``, ``purge_targets_prepared``,
6
+ ``prepare_governance_erase_targets``, ``fail_purge_operation``,
7
+ ``get_purge_operation``) plus the Purge-owned private
8
+ ``_record_purge_target_locked`` (called cross-bucket by the residual rebuild-hide
9
+ / governance-erase-execution methods, which reach it via MRO co-composition).
10
+
11
+ The residual ``SQLiteGovernanceMixin`` stays composed alongside this mixin and
12
+ permanently holds the shared infra (``conn``, ``_lock``, ``org_id``,
13
+ ``_deps()``), the cross-bucket residents it calls
14
+ (``_owned_user_playbook_ids_locked``, ``_planned_governance_delete_counts``),
15
+ and the module-level helpers (``_json_dumps``, ``_row_to_purge_operation``,
16
+ ``_row_to_purge_target``), which are imported here rather than duplicated.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import sqlite3
22
+ import threading
23
+ from collections.abc import Callable
24
+ from typing import TYPE_CHECKING, Any, Literal, cast
25
+
26
+ from reflexio.models.api_schema.domain.governance import (
27
+ PurgeOperation,
28
+ PurgeOperationTarget,
29
+ )
30
+ from reflexio.server.services.storage.governance_validation import (
31
+ _ALLOWED_PURGE_OPERATION_TYPES,
32
+ _ALLOWED_PURGE_SCOPE_TYPES,
33
+ _ALLOWED_PURGE_TARGET_DETAIL_KEYS,
34
+ _ALLOWED_PURGE_TARGET_NAMES,
35
+ _ALLOWED_PURGE_TARGET_PHASES,
36
+ _ALLOWED_PURGE_TARGET_STATUSES,
37
+ _PREPARE_PHASE,
38
+ _SNAPSHOT_TARGET_NAME,
39
+ _epoch_now,
40
+ _validate_governance_deleted_count,
41
+ _validate_governance_detail,
42
+ _validate_governance_enum,
43
+ _validate_governance_error_code,
44
+ _validate_governance_error_detail,
45
+ _validate_governance_idempotency_key,
46
+ _validate_governance_prefixed_ref,
47
+ _validate_governance_purge_id,
48
+ _validate_governance_target_ref,
49
+ )
50
+
51
+ from .._governance import _json_dumps, _row_to_purge_operation, _row_to_purge_target
52
+
53
+ if TYPE_CHECKING:
54
+ from .._governance import _SQLiteGovernanceDeps
55
+
56
+
57
+ class PurgeOperationStoreMixin:
58
+ """SQLite purge-operation store primitives."""
59
+
60
+ # Type hints for instance attributes/methods provided via MRO by the
61
+ # co-composed residual SQLiteGovernanceMixin / SQLiteStorageBase.
62
+ conn: sqlite3.Connection
63
+ _lock: threading.RLock
64
+ org_id: str
65
+ _deps: Callable[[], _SQLiteGovernanceDeps]
66
+ _owned_user_playbook_ids_locked: Callable[[str], set[int]]
67
+ _planned_governance_delete_counts: Callable[[str, set[int]], dict[str, int]]
68
+
69
+ def _record_purge_target_locked(
70
+ self,
71
+ *,
72
+ purge_id: str,
73
+ target_name: str,
74
+ target_ref: str,
75
+ phase: str,
76
+ status: Literal["pending", "running", "failed", "complete"],
77
+ detail: dict[str, object] | None,
78
+ deleted_count: int,
79
+ error_detail: str | None,
80
+ ) -> None:
81
+ purge_id = _validate_governance_purge_id("purge_id", purge_id)
82
+ _validate_governance_enum(
83
+ "target_name",
84
+ target_name,
85
+ allowed=_ALLOWED_PURGE_TARGET_NAMES,
86
+ )
87
+ _validate_governance_enum(
88
+ "phase",
89
+ phase,
90
+ allowed=_ALLOWED_PURGE_TARGET_PHASES,
91
+ )
92
+ _validate_governance_enum(
93
+ "status",
94
+ status,
95
+ allowed=_ALLOWED_PURGE_TARGET_STATUSES,
96
+ )
97
+ detail = _validate_governance_detail(
98
+ "detail",
99
+ detail,
100
+ allowed_keys=_ALLOWED_PURGE_TARGET_DETAIL_KEYS,
101
+ )
102
+ error_detail = _validate_governance_error_detail(error_detail)
103
+ target_ref = _validate_governance_target_ref(
104
+ target_name=target_name,
105
+ phase=phase,
106
+ target_ref=target_ref,
107
+ )
108
+ deleted_count = _validate_governance_deleted_count(deleted_count)
109
+ now = _epoch_now()
110
+ existing = self.conn.execute(
111
+ """SELECT started_at, completed_at
112
+ FROM purge_operation_targets
113
+ WHERE org_id = ? AND purge_id = ? AND target_name = ? AND target_ref = ? AND phase = ?""",
114
+ (self.org_id, purge_id, target_name, target_ref, phase),
115
+ ).fetchone()
116
+ started_at = existing["started_at"] if existing else None
117
+ completed_at = existing["completed_at"] if existing else None
118
+ if started_at is None and status in {"running", "failed", "complete"}:
119
+ started_at = now
120
+ if status in {"failed", "complete"}:
121
+ completed_at = now
122
+ self.conn.execute(
123
+ """INSERT INTO purge_operation_targets (
124
+ org_id, purge_id, target_name, target_ref, phase, status, detail,
125
+ deleted_count, error_detail, started_at, completed_at
126
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
127
+ ON CONFLICT(org_id, purge_id, target_name, target_ref, phase) DO UPDATE SET
128
+ status = excluded.status,
129
+ detail = COALESCE(excluded.detail, purge_operation_targets.detail),
130
+ deleted_count = excluded.deleted_count,
131
+ error_detail = excluded.error_detail,
132
+ started_at = COALESCE(purge_operation_targets.started_at, excluded.started_at),
133
+ completed_at = excluded.completed_at""",
134
+ (
135
+ self.org_id,
136
+ purge_id,
137
+ target_name,
138
+ target_ref,
139
+ phase,
140
+ status,
141
+ _json_dumps(detail),
142
+ deleted_count,
143
+ error_detail,
144
+ started_at,
145
+ completed_at,
146
+ ),
147
+ )
148
+ self.conn.execute(
149
+ """UPDATE purge_operations
150
+ SET status = CASE
151
+ WHEN status IN ('complete', 'failed') THEN status
152
+ WHEN ? IN ('running', 'complete') THEN 'running'
153
+ ELSE status
154
+ END,
155
+ updated_at = ?
156
+ WHERE purge_id = ? AND org_id = ?""",
157
+ (status, now, purge_id, self.org_id),
158
+ )
159
+
160
+ def begin_purge_operation(
161
+ self,
162
+ purge_id: str,
163
+ idempotency_key: str,
164
+ operation_type: Literal["user_erasure", "org_purge"],
165
+ scope_type: Literal["user", "org"],
166
+ subject_ref: str | None,
167
+ request_ref: str,
168
+ ) -> PurgeOperation:
169
+ _validate_governance_enum(
170
+ "operation_type",
171
+ operation_type,
172
+ allowed=_ALLOWED_PURGE_OPERATION_TYPES,
173
+ )
174
+ _validate_governance_enum(
175
+ "scope_type",
176
+ scope_type,
177
+ allowed=_ALLOWED_PURGE_SCOPE_TYPES,
178
+ )
179
+ _validate_governance_prefixed_ref(
180
+ "subject_ref", subject_ref, prefix="subref_v1_"
181
+ )
182
+ _validate_governance_prefixed_ref(
183
+ "request_ref", request_ref, prefix="reqref_v1_"
184
+ )
185
+ validated_purge_id = _validate_governance_purge_id("purge_id", purge_id)
186
+ validated_idempotency_key = cast(
187
+ str,
188
+ _validate_governance_idempotency_key("idempotency_key", idempotency_key),
189
+ )
190
+ now = _epoch_now()
191
+ with self._lock:
192
+ existing = self.conn.execute(
193
+ """SELECT * FROM purge_operations
194
+ WHERE org_id = ? AND idempotency_key = ?""",
195
+ (self.org_id, validated_idempotency_key),
196
+ ).fetchone()
197
+ if existing is not None:
198
+ existing_operation = _row_to_purge_operation(existing)
199
+ expected_identity = {
200
+ "purge_id": validated_purge_id,
201
+ "operation_type": operation_type,
202
+ "scope_type": scope_type,
203
+ "subject_ref": subject_ref,
204
+ "request_ref": request_ref,
205
+ }
206
+ for field_name, expected_value in expected_identity.items():
207
+ if getattr(existing_operation, field_name) != expected_value:
208
+ raise ValueError(
209
+ "Existing purge operation for idempotency_key has "
210
+ f"mismatched {field_name}"
211
+ )
212
+ return _row_to_purge_operation(existing)
213
+ self.conn.execute(
214
+ """INSERT INTO purge_operations (
215
+ purge_id, org_id, operation_type, scope_type, subject_ref,
216
+ request_ref, idempotency_key, status, created_at, updated_at
217
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?)""",
218
+ (
219
+ validated_purge_id,
220
+ self.org_id,
221
+ operation_type,
222
+ scope_type,
223
+ subject_ref,
224
+ request_ref,
225
+ validated_idempotency_key,
226
+ now,
227
+ now,
228
+ ),
229
+ )
230
+ self.conn.commit()
231
+ return self.get_purge_operation(validated_purge_id)
232
+
233
+ def record_purge_target(
234
+ self,
235
+ purge_id: str,
236
+ target_name: str,
237
+ phase: str,
238
+ status: Literal["pending", "running", "failed", "complete"],
239
+ target_ref: str = "",
240
+ detail: dict[str, object] | None = None,
241
+ deleted_count: int = 0,
242
+ error_detail: str | None = None,
243
+ ) -> None:
244
+ purge_id = _validate_governance_purge_id("purge_id", purge_id)
245
+ _validate_governance_enum(
246
+ "target_name",
247
+ target_name,
248
+ allowed=_ALLOWED_PURGE_TARGET_NAMES,
249
+ )
250
+ _validate_governance_enum(
251
+ "phase",
252
+ phase,
253
+ allowed=_ALLOWED_PURGE_TARGET_PHASES,
254
+ )
255
+ _validate_governance_enum(
256
+ "status",
257
+ status,
258
+ allowed=_ALLOWED_PURGE_TARGET_STATUSES,
259
+ )
260
+ with self._lock:
261
+ self._record_purge_target_locked(
262
+ purge_id=purge_id,
263
+ target_name=target_name,
264
+ target_ref=target_ref,
265
+ phase=phase,
266
+ status=status,
267
+ detail=detail,
268
+ deleted_count=deleted_count,
269
+ error_detail=error_detail,
270
+ )
271
+ self.conn.commit()
272
+
273
+ def list_purge_targets(
274
+ self, purge_id: str, phase: str | None = None
275
+ ) -> list[PurgeOperationTarget]:
276
+ purge_id = _validate_governance_purge_id("purge_id", purge_id)
277
+ deps = self._deps()
278
+ sql = "SELECT * FROM purge_operation_targets WHERE org_id = ? AND purge_id = ?"
279
+ params: list[Any] = [self.org_id, purge_id]
280
+ if phase is not None:
281
+ sql += " AND phase = ?"
282
+ params.append(phase)
283
+ sql += " ORDER BY phase ASC, target_name ASC, target_ref ASC"
284
+ rows = deps._fetchall(sql, params)
285
+ return [_row_to_purge_target(row) for row in rows]
286
+
287
+ def purge_targets_prepared(self, purge_id: str) -> bool:
288
+ purge_id = _validate_governance_purge_id("purge_id", purge_id)
289
+ row = self._deps()._fetchone(
290
+ """SELECT 1 FROM purge_operation_targets
291
+ WHERE org_id = ? AND purge_id = ? AND target_name = ? AND target_ref = 'all'
292
+ AND phase = ? AND status = 'complete'""",
293
+ (self.org_id, purge_id, _SNAPSHOT_TARGET_NAME, _PREPARE_PHASE),
294
+ )
295
+ return row is not None
296
+
297
+ def prepare_governance_erase_targets(
298
+ self,
299
+ purge_id: str,
300
+ user_id: str,
301
+ owned_user_playbook_ids: set[int] | None = None,
302
+ ) -> None:
303
+ purge_id = _validate_governance_purge_id("purge_id", purge_id)
304
+ with self._lock:
305
+ if self.purge_targets_prepared(purge_id):
306
+ return
307
+ try:
308
+ self.conn.execute("BEGIN IMMEDIATE")
309
+ owned_user_playbook_ids = (
310
+ set(owned_user_playbook_ids)
311
+ if owned_user_playbook_ids is not None
312
+ else self._owned_user_playbook_ids_locked(user_id)
313
+ )
314
+ targets = self._planned_governance_delete_counts(
315
+ user_id,
316
+ owned_user_playbook_ids,
317
+ )
318
+ for target_name, count in targets.items():
319
+ self._record_purge_target_locked(
320
+ purge_id=purge_id,
321
+ target_name=target_name,
322
+ target_ref="all",
323
+ phase="delete",
324
+ status="pending",
325
+ detail={"count": count},
326
+ deleted_count=0,
327
+ error_detail=None,
328
+ )
329
+ self._record_purge_target_locked(
330
+ purge_id=purge_id,
331
+ target_name=_SNAPSHOT_TARGET_NAME,
332
+ target_ref="all",
333
+ phase=_PREPARE_PHASE,
334
+ status="complete",
335
+ detail={
336
+ "owned_user_playbook_ids": sorted(owned_user_playbook_ids),
337
+ },
338
+ deleted_count=0,
339
+ error_detail=None,
340
+ )
341
+ self.conn.commit()
342
+ except Exception:
343
+ self.conn.rollback()
344
+ raise
345
+
346
+ def fail_purge_operation(
347
+ self, purge_id: str, error_code: str, error_detail: str
348
+ ) -> PurgeOperation:
349
+ purge_id = _validate_governance_purge_id("purge_id", purge_id)
350
+ validated_error_code = _validate_governance_error_code(error_code)
351
+ validated_error_detail = _validate_governance_error_detail(error_detail)
352
+ now = _epoch_now()
353
+ with self._lock:
354
+ cur = self.conn.execute(
355
+ """UPDATE purge_operations
356
+ SET status = 'failed', error_code = ?, error_detail = ?,
357
+ updated_at = ?, completed_at = ?
358
+ WHERE purge_id = ? AND org_id = ? AND status != 'complete'""",
359
+ (
360
+ validated_error_code,
361
+ validated_error_detail,
362
+ now,
363
+ now,
364
+ purge_id,
365
+ self.org_id,
366
+ ),
367
+ )
368
+ if cur.rowcount == 0:
369
+ existing = self.conn.execute(
370
+ "SELECT status FROM purge_operations WHERE purge_id = ? AND org_id = ?",
371
+ (purge_id, self.org_id),
372
+ ).fetchone()
373
+ if existing is not None and str(existing["status"]) == "complete":
374
+ raise ValueError("Purge operation is already complete")
375
+ raise ValueError(f"Purge operation {purge_id!r} not found")
376
+ self.conn.commit()
377
+ return self.get_purge_operation(purge_id)
378
+
379
+ def get_purge_operation(self, purge_id: str) -> PurgeOperation:
380
+ purge_id = _validate_governance_purge_id("purge_id", purge_id)
381
+ row = self._deps()._fetchone(
382
+ "SELECT * FROM purge_operations WHERE purge_id = ? AND org_id = ?",
383
+ (purge_id, self.org_id),
384
+ )
385
+ if row is None:
386
+ raise ValueError(f"Purge operation {purge_id!r} not found")
387
+ return _row_to_purge_operation(row)