methodproof 0.7.5__tar.gz → 0.7.7__tar.gz

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 (65) hide show
  1. {methodproof-0.7.5 → methodproof-0.7.7}/CHANGELOG.md +6 -0
  2. {methodproof-0.7.5 → methodproof-0.7.7}/PKG-INFO +4 -4
  3. {methodproof-0.7.5 → methodproof-0.7.7}/README.md +3 -3
  4. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/__init__.py +1 -1
  5. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/analysis.py +3 -2
  6. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/cli.py +86 -1
  7. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/graph.py +13 -13
  8. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/migrate_db.py +3 -4
  9. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/store.py +57 -4
  10. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/sync.py +7 -1
  11. {methodproof-0.7.5 → methodproof-0.7.7}/pyproject.toml +1 -1
  12. {methodproof-0.7.5 → methodproof-0.7.7}/tests/test_security.py +7 -4
  13. {methodproof-0.7.5 → methodproof-0.7.7}/.github/workflows/ci.yml +0 -0
  14. {methodproof-0.7.5 → methodproof-0.7.7}/.gitignore +0 -0
  15. {methodproof-0.7.5 → methodproof-0.7.7}/LICENSE +0 -0
  16. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/__main__.py +0 -0
  17. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/_daemon.py +0 -0
  18. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/agents/__init__.py +0 -0
  19. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/agents/base.py +0 -0
  20. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/agents/music.py +0 -0
  21. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/agents/terminal.py +0 -0
  22. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/agents/watcher.py +0 -0
  23. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/binding.py +0 -0
  24. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/bip39.py +0 -0
  25. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/bridge.py +0 -0
  26. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/config.py +0 -0
  27. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/crypto.py +0 -0
  28. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/e2e.py +0 -0
  29. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/hook.py +0 -0
  30. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/hooks/__init__.py +0 -0
  31. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/hooks/claude_code.py +0 -0
  32. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/hooks/claude_code.sh +0 -0
  33. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/hooks/cline_hook.sh +0 -0
  34. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/hooks/codex_hook.sh +0 -0
  35. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/hooks/gemini_hook.sh +0 -0
  36. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/hooks/install.py +0 -0
  37. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/hooks/kiro_hook.sh +0 -0
  38. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/hooks/mcp_register.py +0 -0
  39. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/hooks/openclaw/HOOK.md +0 -0
  40. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/hooks/openclaw/handler.ts +0 -0
  41. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/hooks/openclaw_install.py +0 -0
  42. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/hooks/opencode_plugin.js +0 -0
  43. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/hooks/wrappers.py +0 -0
  44. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/integrity.py +0 -0
  45. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/kdf.py +0 -0
  46. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/keychain.py +0 -0
  47. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/live.py +0 -0
  48. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/lock.py +0 -0
  49. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/mcp.py +0 -0
  50. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/proxy.py +0 -0
  51. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/proxy_daemon.py +0 -0
  52. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/repos.py +0 -0
  53. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/skills/methodproof/SKILL.md +0 -0
  54. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/viewer.py +0 -0
  55. {methodproof-0.7.5 → methodproof-0.7.7}/methodproof/wordlist.py +0 -0
  56. {methodproof-0.7.5 → methodproof-0.7.7}/test_windows_compat.py +0 -0
  57. {methodproof-0.7.5 → methodproof-0.7.7}/tests/__init__.py +0 -0
  58. {methodproof-0.7.5 → methodproof-0.7.7}/tests/test_analysis.py +0 -0
  59. {methodproof-0.7.5 → methodproof-0.7.7}/tests/test_graph.py +0 -0
  60. {methodproof-0.7.5 → methodproof-0.7.7}/tests/test_hooks.py +0 -0
  61. {methodproof-0.7.5 → methodproof-0.7.7}/tests/test_live.py +0 -0
  62. {methodproof-0.7.5 → methodproof-0.7.7}/tests/test_openclaw_hooks.py +0 -0
  63. {methodproof-0.7.5 → methodproof-0.7.7}/tests/test_store.py +0 -0
  64. {methodproof-0.7.5 → methodproof-0.7.7}/tests/test_wrappers.py +0 -0
  65. {methodproof-0.7.5 → methodproof-0.7.7}/uv.lock +0 -0
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.7.6] — 2026-04-08
4
+
5
+ ### Added
6
+ - **Metadata compression** — event metadata stored as zlib-compressed BLOBs in SQLite (~80% storage reduction for journal-mode sessions). Existing uncompressed rows are silently migrated on next startup.
7
+ - **Gzip transfer encoding** — `mp push` sends gzip-compressed request bodies to the platform. Reduces upload bandwidth ~70% for large batches.
8
+
3
9
  ## [0.7.1] — 2026-04-07
4
10
 
5
11
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: methodproof
3
- Version: 0.7.5
3
+ Version: 0.7.7
4
4
  Summary: See how you code. Capture and visualize your engineering process.
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -60,7 +60,7 @@ methodproof view # explore your session in the browser
60
60
  - **Environment profiling** — structural analysis of your AI dev environment (instruction files, tool counts, MCP servers) captured at session start
61
61
  - **Outcome metrics** — first-shot apply rate, follow-up sequences, phase transitions computed at session end
62
62
  - **Granular consent** — 10 standard capture categories + 1 premium, each independently toggled. Nothing records without your opt-in
63
- - **Local-first** — SQLite database at `~/.methodproof/`, `chmod 600`. No network calls unless you choose
63
+ - **Local-first** — SQLite database at `~/.methodproof/`, `chmod 600`, zlib-compressed metadata. No network calls unless you choose
64
64
  - **Live streaming** — `methodproof start --live` streams events to the platform in real-time over WebSocket
65
65
  - **Integrity verification** — hash-chained events + Ed25519 attestation prove sessions haven't been tampered with
66
66
  - **E2E encryption** — optional company-held AES-256-GCM encryption the platform cannot decrypt
@@ -99,7 +99,7 @@ flowchart TB
99
99
  CHAIN --> BUF["Batched Flush"]
100
100
  end
101
101
 
102
- BUF --> DB[("SQLite WAL")]
102
+ BUF -->|"zlib compress"| DB[("SQLite WAL")]
103
103
  BUF -.->|"--live"| WS["WebSocket Stream"]
104
104
 
105
105
  subgraph KEYS["Key Vault"]
@@ -115,7 +115,7 @@ flowchart TB
115
115
 
116
116
  subgraph PUSH["PUSH PROTOCOL"]
117
117
  direction TB
118
- DB --> BATCH["Batched Upload"]
118
+ DB -->|"gzip"| BATCH["Batched Upload"]
119
119
  BATCH --> BIND["Session Binding: HMAC over session metadata"]
120
120
  BIND --> SIGN["Ed25519 Sign: session summary"]
121
121
  end
@@ -45,7 +45,7 @@ methodproof view # explore your session in the browser
45
45
  - **Environment profiling** — structural analysis of your AI dev environment (instruction files, tool counts, MCP servers) captured at session start
46
46
  - **Outcome metrics** — first-shot apply rate, follow-up sequences, phase transitions computed at session end
47
47
  - **Granular consent** — 10 standard capture categories + 1 premium, each independently toggled. Nothing records without your opt-in
48
- - **Local-first** — SQLite database at `~/.methodproof/`, `chmod 600`. No network calls unless you choose
48
+ - **Local-first** — SQLite database at `~/.methodproof/`, `chmod 600`, zlib-compressed metadata. No network calls unless you choose
49
49
  - **Live streaming** — `methodproof start --live` streams events to the platform in real-time over WebSocket
50
50
  - **Integrity verification** — hash-chained events + Ed25519 attestation prove sessions haven't been tampered with
51
51
  - **E2E encryption** — optional company-held AES-256-GCM encryption the platform cannot decrypt
@@ -84,7 +84,7 @@ flowchart TB
84
84
  CHAIN --> BUF["Batched Flush"]
85
85
  end
86
86
 
87
- BUF --> DB[("SQLite WAL")]
87
+ BUF -->|"zlib compress"| DB[("SQLite WAL")]
88
88
  BUF -.->|"--live"| WS["WebSocket Stream"]
89
89
 
90
90
  subgraph KEYS["Key Vault"]
@@ -100,7 +100,7 @@ flowchart TB
100
100
 
101
101
  subgraph PUSH["PUSH PROTOCOL"]
102
102
  direction TB
103
- DB --> BATCH["Batched Upload"]
103
+ DB -->|"gzip"| BATCH["Batched Upload"]
104
104
  BATCH --> BIND["Session Binding: HMAC over session metadata"]
105
105
  BIND --> SIGN["Ed25519 Sign: session summary"]
106
106
  end
@@ -1,3 +1,3 @@
1
1
  """MethodProof — see how you code."""
2
2
 
3
- __version__ = "0.7.5"
3
+ __version__ = "0.7.7"
@@ -606,8 +606,9 @@ def compute_outcomes(session_id: str) -> dict[str, Any]:
606
606
  if e["type"] not in prompt_types:
607
607
  continue
608
608
  try:
609
- meta = json.loads(e["metadata"])
610
- except (json.JSONDecodeError, TypeError) as exc:
609
+ from methodproof.store import _decompress_meta
610
+ meta = _decompress_meta(e["metadata"])
611
+ except (json.JSONDecodeError, TypeError, Exception) as exc:
611
612
  _warn("analysis.metadata_parse_failed", event_id=e["id"] if hasattr(e, "__getitem__") else "?", error=str(exc))
612
613
  continue
613
614
  intent = meta.get("sa_intent")
@@ -1293,6 +1293,89 @@ def cmd_log(args: argparse.Namespace) -> None:
1293
1293
  print(f" {s['id'][:8]} {dt} {dur} {s['total_events']} events{suffix}")
1294
1294
 
1295
1295
 
1296
+ def cmd_status(args: argparse.Namespace) -> None:
1297
+ """Show auth, session, and config status at a glance."""
1298
+ from methodproof import __version__
1299
+ cfg = config.load()
1300
+ token = cfg.get("token", "")
1301
+ claims = _decode_jwt_claims(token) if token else {}
1302
+ sessions = store.list_sessions()
1303
+ active = cfg.get("active_session")
1304
+ capture = cfg.get("capture", {})
1305
+ enabled = [k for k, v in capture.items() if v]
1306
+
1307
+ print(f"\n methodproof v{__version__}")
1308
+ print(f" api: {cfg.get('api_url', '—')}\n")
1309
+
1310
+ # Auth
1311
+ if not token:
1312
+ print(" auth: not signed in")
1313
+ else:
1314
+ account_id = claims.get("user_id", "—")
1315
+ role = claims.get("role", "—")
1316
+ acct_type = claims.get("account_type", "—")
1317
+ exp = claims.get("exp", 0)
1318
+ if exp and time.time() > exp:
1319
+ expiry = "expired"
1320
+ elif exp:
1321
+ remaining = int(exp - time.time())
1322
+ hours = remaining // 3600
1323
+ mins = (remaining % 3600) // 60
1324
+ expiry = f"{hours}h {mins}m remaining"
1325
+ else:
1326
+ expiry = "unknown"
1327
+ print(f" auth: signed in")
1328
+ print(f" account: {account_id[:8]}...{account_id[-4:]}")
1329
+ print(f" role: {role} | type: {acct_type} | token: {expiry}")
1330
+ if claims.get("is_superadmin"):
1331
+ print(" superadmin: yes")
1332
+
1333
+ # Session
1334
+ print()
1335
+ if active:
1336
+ sess = store.get_session(active)
1337
+ if sess:
1338
+ dt = datetime.fromtimestamp(sess["created_at"], tz=UTC).strftime("%H:%M")
1339
+ print(f" session: RECORDING {active[:8]} started {dt} ({sess['total_events']} events)")
1340
+ else:
1341
+ print(f" session: RECORDING {active[:8]}")
1342
+ else:
1343
+ print(" session: idle")
1344
+
1345
+ # Local sessions
1346
+ total = len(sessions)
1347
+ unsynced = len([s for s in sessions if not s["synced"] and s.get("completed_at") and s["total_events"] > 0])
1348
+ print(f" local: {total} session{'s' if total != 1 else ''}", end="")
1349
+ if unsynced:
1350
+ print(f" ({unsynced} unsynced)")
1351
+ else:
1352
+ print()
1353
+
1354
+ # Capture config
1355
+ print(f"\n consent: {len(enabled)}/11 categories")
1356
+ full_spectrum = len(enabled) >= 10
1357
+ if full_spectrum:
1358
+ print(" spectrum: FULL")
1359
+
1360
+ # Modes
1361
+ modes = []
1362
+ if cfg.get("journal_mode"):
1363
+ credits = cfg.get("journal_credits", 0)
1364
+ modes.append(f"journal ({credits} credits)")
1365
+ if cfg.get("e2e_mode"):
1366
+ fp = cfg.get("e2e_fingerprint", "")
1367
+ modes.append(f"e2e ({fp[:8]})" if fp else "e2e")
1368
+ if modes:
1369
+ print(f" modes: {' | '.join(modes)}")
1370
+
1371
+ # Research
1372
+ if cfg.get("research_consent"):
1373
+ level = cfg.get("contribution_level", "structural")
1374
+ print(f" research: opted in ({level})")
1375
+
1376
+ print()
1377
+
1378
+
1296
1379
  def cmd_logout(args: argparse.Namespace) -> None:
1297
1380
  """Clear login credentials only. Keeps consent, sessions, and hooks."""
1298
1381
  cfg = config.load()
@@ -1779,6 +1862,7 @@ def main() -> None:
1779
1862
  v = sub.add_parser("view", help="Inspect captured session data")
1780
1863
  v.add_argument("session_id", nargs="?")
1781
1864
  sub.add_parser("log", help="List sessions")
1865
+ sub.add_parser("status", help="Auth, session, and config status")
1782
1866
  l = sub.add_parser("login", help="Connect to platform")
1783
1867
  l.add_argument("--api-url")
1784
1868
  l.add_argument("--force", "-f", action="store_true", help="Skip switch-account prompt")
@@ -1843,7 +1927,8 @@ def main() -> None:
1843
1927
  args = p.parse_args()
1844
1928
  cmds = {
1845
1929
  "init": cmd_init, "start": cmd_start, "stop": cmd_stop,
1846
- "view": cmd_view, "log": cmd_log, "login": cmd_login, "logout": cmd_logout,
1930
+ "view": cmd_view, "log": cmd_log, "status": cmd_status,
1931
+ "login": cmd_login, "logout": cmd_logout,
1847
1932
  "push": cmd_push, "tag": cmd_tag, "publish": cmd_publish,
1848
1933
  "delete": cmd_delete, "review": cmd_review, "consent": cmd_consent,
1849
1934
  "update": cmd_update, "lock": cmd_lock, "reset": cmd_reset, "uninstall": cmd_uninstall,
@@ -5,7 +5,7 @@ import time
5
5
  import uuid
6
6
  from typing import Any
7
7
 
8
- from methodproof.store import _db
8
+ from methodproof.store import _compress_meta, _db, _decompress_meta
9
9
 
10
10
 
11
11
  def build(session_id: str) -> dict[str, int]:
@@ -53,7 +53,7 @@ def build(session_id: str) -> dict[str, int]:
53
53
 
54
54
  # Resources
55
55
  for e in events:
56
- meta = json.loads(e["metadata"])
56
+ meta = _decompress_meta(e["metadata"])
57
57
  if e["type"] in ("llm_prompt", "llm_completion") and "model" in meta:
58
58
  _ensure_resource(db, "llm_model", meta["model"])
59
59
  stats["resources"] += 1
@@ -63,7 +63,7 @@ def build(session_id: str) -> dict[str, int]:
63
63
 
64
64
  # Artifacts
65
65
  for e in events:
66
- meta = json.loads(e["metadata"])
66
+ meta = _decompress_meta(e["metadata"])
67
67
  if e["type"] in ("file_create", "file_edit") and "path" in meta:
68
68
  _ensure_artifact(db, meta["path"], meta.get("size", 0))
69
69
  stats["artifacts"] += 1
@@ -84,7 +84,7 @@ def build(session_id: str) -> dict[str, int]:
84
84
  "(id, session_id, type, timestamp, duration_ms, metadata) "
85
85
  "VALUES (?, ?, ?, ?, ?, ?)",
86
86
  (uuid.uuid4().hex, session_id, "prompt_outcomes",
87
- time.time(), 0, json.dumps(outcomes)),
87
+ time.time(), 0, _compress_meta(outcomes)),
88
88
  )
89
89
  stats["outcomes"] = 1
90
90
  except Exception as exc:
@@ -100,7 +100,7 @@ def _link(
100
100
  rel: str, window_sec: int, match_model: bool = False,
101
101
  ) -> int:
102
102
  model_clause = (
103
- "AND json_extract(s.metadata, '$.model') = json_extract(t.metadata, '$.model')"
103
+ "AND json_extract(mp_json(s.metadata), '$.model') = json_extract(mp_json(t.metadata), '$.model')"
104
104
  if match_model else ""
105
105
  )
106
106
  sql = f"""
@@ -123,9 +123,9 @@ def _link_pasted(db: object, sid: str) -> int:
123
123
  FROM events s JOIN events t ON t.session_id = s.session_id
124
124
  WHERE s.session_id = ? AND s.type = 'browser_copy' AND t.type = 'file_edit'
125
125
  AND t.timestamp > s.timestamp AND (t.timestamp - s.timestamp) <= 30
126
- AND abs(json_extract(t.metadata, '$.lines_added') * 40.0
127
- - json_extract(s.metadata, '$.text_length'))
128
- < json_extract(s.metadata, '$.text_length') * 0.2
126
+ AND abs(json_extract(mp_json(t.metadata), '$.lines_added') * 40.0
127
+ - json_extract(mp_json(s.metadata), '$.text_length'))
128
+ < json_extract(mp_json(s.metadata), '$.text_length') * 0.2
129
129
  """
130
130
  return db.execute(sql, (sid,)).rowcount
131
131
 
@@ -135,20 +135,20 @@ def _link_action_resources(db: object, sid: str) -> None:
135
135
  db.execute("""
136
136
  INSERT OR IGNORE INTO action_resources (action_id, resource_id, relation_type, metadata)
137
137
  SELECT e.id, r.id, 'SENT_TO', '{}'
138
- FROM events e JOIN resources r ON r.identifier = json_extract(e.metadata, '$.model')
138
+ FROM events e JOIN resources r ON r.identifier = json_extract(mp_json(e.metadata), '$.model')
139
139
  WHERE e.session_id = ? AND e.type = 'llm_prompt' AND r.type = 'llm_model'
140
140
  """, (sid,))
141
141
  db.execute("""
142
142
  INSERT OR IGNORE INTO action_resources (action_id, resource_id, relation_type, metadata)
143
143
  SELECT e.id, r.id, 'CONSUMED', '{}'
144
- FROM events e JOIN resources r ON r.identifier = json_extract(e.metadata, '$.model')
144
+ FROM events e JOIN resources r ON r.identifier = json_extract(mp_json(e.metadata), '$.model')
145
145
  WHERE e.session_id = ? AND e.type = 'llm_completion' AND r.type = 'llm_model'
146
146
  """, (sid,))
147
147
  # Agent gateway links
148
148
  db.execute("""
149
149
  INSERT OR IGNORE INTO action_resources (action_id, resource_id, relation_type, metadata)
150
150
  SELECT e.id, r.id, 'SENT_TO', '{}'
151
- FROM events e JOIN resources r ON r.identifier = json_extract(e.metadata, '$.gateway')
151
+ FROM events e JOIN resources r ON r.identifier = json_extract(mp_json(e.metadata), '$.gateway')
152
152
  WHERE e.session_id = ? AND e.type = 'agent_prompt' AND r.type = 'agent_gateway'
153
153
  """, (sid,))
154
154
 
@@ -158,13 +158,13 @@ def _link_action_artifacts(db: object, sid: str) -> None:
158
158
  db.execute("""
159
159
  INSERT OR IGNORE INTO action_artifacts (action_id, artifact_id, relation_type)
160
160
  SELECT e.id, a.id, 'PRODUCED'
161
- FROM events e JOIN artifacts a ON a.path = json_extract(e.metadata, '$.path')
161
+ FROM events e JOIN artifacts a ON a.path = json_extract(mp_json(e.metadata), '$.path')
162
162
  WHERE e.session_id = ? AND e.type = 'file_create'
163
163
  """, (sid,))
164
164
  db.execute("""
165
165
  INSERT OR IGNORE INTO action_artifacts (action_id, artifact_id, relation_type)
166
166
  SELECT e.id, a.id, 'MODIFIED'
167
- FROM events e JOIN artifacts a ON a.path = json_extract(e.metadata, '$.path')
167
+ FROM events e JOIN artifacts a ON a.path = json_extract(mp_json(e.metadata), '$.path')
168
168
  WHERE e.session_id = ? AND e.type = 'file_edit'
169
169
  """, (sid,))
170
170
 
@@ -1,9 +1,8 @@
1
1
  """Encrypt existing plaintext events in local DB after key setup."""
2
2
 
3
- import json
4
-
5
3
  from methodproof import store
6
4
  from methodproof.crypto import SENSITIVE_FIELDS, encrypt_field
5
+ from methodproof.store import _compress_meta, _decompress_meta
7
6
 
8
7
 
9
8
  def migrate_encrypt(db_key: bytes) -> int:
@@ -18,7 +17,7 @@ def migrate_encrypt(db_key: bytes) -> int:
18
17
  encrypted = 0
19
18
  batch = []
20
19
  for row in rows:
21
- meta = json.loads(row["metadata"])
20
+ meta = _decompress_meta(row["metadata"])
22
21
  changed = False
23
22
  for field in SENSITIVE_FIELDS:
24
23
  val = meta.get(field)
@@ -26,7 +25,7 @@ def migrate_encrypt(db_key: bytes) -> int:
26
25
  meta[field] = encrypt_field(val, db_key)
27
26
  changed = True
28
27
  if changed:
29
- batch.append((json.dumps(meta), row["id"]))
28
+ batch.append((_compress_meta(meta), row["id"]))
30
29
  encrypted += 1
31
30
  if len(batch) >= 500:
32
31
  _flush_batch(db, batch)
@@ -4,6 +4,7 @@ import json
4
4
  import sqlite3
5
5
  import time
6
6
  import uuid
7
+ import zlib
7
8
  from typing import Any
8
9
 
9
10
  from methodproof import config
@@ -62,12 +63,25 @@ CREATE TABLE IF NOT EXISTS action_artifacts (
62
63
  _conn: sqlite3.Connection | None = None
63
64
 
64
65
 
66
+ def _sqlite_decompress(raw: bytes | str | None) -> str | None:
67
+ """SQLite UDF: decompress zlib BLOBs to JSON text for json_extract()."""
68
+ if raw is None:
69
+ return None
70
+ if isinstance(raw, bytes):
71
+ try:
72
+ return zlib.decompress(raw).decode()
73
+ except zlib.error:
74
+ return raw.decode() if raw else "{}"
75
+ return raw
76
+
77
+
65
78
  def _db() -> sqlite3.Connection:
66
79
  global _conn
67
80
  if _conn is None:
68
81
  _conn = sqlite3.connect(str(config.DB_PATH), check_same_thread=False, timeout=10)
69
82
  _conn.execute("PRAGMA journal_mode=WAL")
70
83
  _conn.row_factory = sqlite3.Row
84
+ _conn.create_function("mp_json", 1, _sqlite_decompress)
71
85
  return _conn
72
86
 
73
87
 
@@ -113,9 +127,30 @@ def _migrate() -> None:
113
127
  db.execute("DELETE FROM action_artifacts")
114
128
  db.execute("DELETE FROM artifacts WHERE rowid NOT IN (SELECT MIN(rowid) FROM artifacts GROUP BY path)")
115
129
  db.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_artifacts_path ON artifacts(path)")
130
+ # Compress legacy uncompressed metadata (TEXT → zlib BLOB)
131
+ _migrate_compress_metadata(db)
116
132
  db.commit()
117
133
 
118
134
 
135
+ def _migrate_compress_metadata(db: sqlite3.Connection) -> None:
136
+ """Silently compress any uncompressed TEXT metadata rows to zlib BLOBs."""
137
+ rows = db.execute("SELECT id, metadata FROM events WHERE typeof(metadata) = 'text'").fetchall()
138
+ if not rows:
139
+ return
140
+ batch, skipped = [], 0
141
+ for r in rows:
142
+ try:
143
+ compressed = zlib.compress(r["metadata"].encode())
144
+ batch.append((compressed, r["id"]))
145
+ except Exception:
146
+ skipped += 1
147
+ if batch:
148
+ db.executemany("UPDATE events SET metadata = ? WHERE id = ?", batch)
149
+ if skipped:
150
+ import sys
151
+ sys.stderr.write(f"[methodproof] migration: compressed {len(batch)} events, skipped {skipped}\n")
152
+
153
+
119
154
  def create_session(
120
155
  session_id: str, watch_dir: str,
121
156
  repo_url: str | None = None, tags: str = "[]", visibility: str = "private",
@@ -142,14 +177,27 @@ def complete_session(session_id: str) -> None:
142
177
  db.commit()
143
178
 
144
179
 
180
+ def _compress_meta(meta: dict[str, Any]) -> bytes:
181
+ return zlib.compress(json.dumps(meta, default=str).encode())
182
+
183
+
184
+ def _decompress_meta(raw: bytes | str) -> dict[str, Any]:
185
+ if isinstance(raw, str):
186
+ return json.loads(raw)
187
+ try:
188
+ return json.loads(zlib.decompress(raw))
189
+ except zlib.error:
190
+ return json.loads(raw)
191
+
192
+
145
193
  def insert_events(session_id: str, events: list[dict[str, Any]]) -> None:
146
194
  db = _db()
147
195
  rows = []
148
196
  for e in events:
149
197
  try:
150
- meta = json.dumps(e.get("metadata", {}), default=str)
198
+ meta = _compress_meta(e.get("metadata", {}))
151
199
  except (TypeError, ValueError):
152
- meta = "{}"
200
+ meta = _compress_meta({})
153
201
  rows.append((
154
202
  e["id"], session_id, e["type"], e["timestamp"],
155
203
  e.get("duration_ms", 0), meta,
@@ -176,7 +224,12 @@ def get_events(session_id: str) -> list[dict[str, Any]]:
176
224
  "SELECT * FROM events WHERE session_id = ? ORDER BY timestamp",
177
225
  (session_id,),
178
226
  ).fetchall()
179
- return [dict(r) for r in rows]
227
+ result = []
228
+ for r in rows:
229
+ d = dict(r)
230
+ d["metadata"] = json.dumps(_decompress_meta(d["metadata"]))
231
+ result.append(d)
232
+ return result
180
233
 
181
234
 
182
235
  def get_graph(session_id: str) -> dict[str, Any]:
@@ -184,7 +237,7 @@ def get_graph(session_id: str) -> dict[str, Any]:
184
237
  events = get_events(session_id)
185
238
  nodes = [{"id": e["id"], "type": "Action", "label": e["type"],
186
239
  "properties": {"timestamp": e["timestamp"], "duration_ms": e["duration_ms"],
187
- "metadata": json.loads(e["metadata"])}}
240
+ "metadata": _decompress_meta(e["metadata"])}}
188
241
  for e in events]
189
242
 
190
243
  edges = []
@@ -1,5 +1,6 @@
1
1
  """Push local sessions to the MethodProof platform."""
2
2
 
3
+ import gzip
3
4
  import json
4
5
  import urllib.error
5
6
  import urllib.request
@@ -30,8 +31,13 @@ def _raw_request(
30
31
  method: str, url: str, token: str,
31
32
  body: dict[str, Any] | None = None,
32
33
  ) -> dict[str, Any]:
33
- data = json.dumps(body).encode() if body else None
34
+ if body is not None:
35
+ data = gzip.compress(json.dumps(body).encode())
36
+ else:
37
+ data = None
34
38
  headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}"}
39
+ if data is not None:
40
+ headers["Content-Encoding"] = "gzip"
35
41
  req = urllib.request.Request(url, data=data, headers=headers, method=method)
36
42
  with urllib.request.urlopen(req, timeout=15) as resp:
37
43
  return json.loads(resp.read())
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "methodproof"
3
- version = "0.7.5"
3
+ version = "0.7.7"
4
4
  description = "See how you code. Capture and visualize your engineering process."
5
5
  requires-python = ">=3.11"
6
6
  dependencies = ["watchdog>=4.0", "websocket-client>=1.7", "cryptography>=43.0", "keyring>=25.0"]
@@ -106,10 +106,12 @@ def test_bip39_known_vector():
106
106
 
107
107
 
108
108
  def test_bip39_bad_checksum():
109
- phrase = entropy_to_phrase(os.urandom(16))
109
+ # Use fixed entropy so the checksum corruption is deterministic
110
+ phrase = entropy_to_phrase(b"\x00" * 16)
110
111
  words = phrase.split()
111
- words[-1] = words[0] # corrupt checksum
112
- with pytest.raises(ValueError, match="checksum|Unknown"):
112
+ # Flip the last word to a word that changes the checksum bits
113
+ words[-1] = "zone"
114
+ with pytest.raises(ValueError, match="checksum"):
113
115
  phrase_to_entropy(" ".join(words))
114
116
 
115
117
 
@@ -257,7 +259,8 @@ def test_migrate_encrypts_plaintext():
257
259
  count = migrate_encrypt(key)
258
260
  assert count == 1
259
261
  row = store._db().execute("SELECT metadata FROM events WHERE id = 'e1'").fetchone()
260
- assert "e2e:v1:" in json.loads(row["metadata"])["prompt_text"]
262
+ meta = store._decompress_meta(row["metadata"])
263
+ assert "e2e:v1:" in meta["prompt_text"]
261
264
 
262
265
 
263
266
  def test_migrate_idempotent():
File without changes
File without changes
File without changes