methodproof 0.7.5__tar.gz → 0.7.6__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.
- {methodproof-0.7.5 → methodproof-0.7.6}/CHANGELOG.md +6 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/PKG-INFO +4 -4
- {methodproof-0.7.5 → methodproof-0.7.6}/README.md +3 -3
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/__init__.py +1 -1
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/analysis.py +3 -2
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/graph.py +13 -13
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/migrate_db.py +3 -4
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/store.py +57 -4
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/sync.py +7 -1
- {methodproof-0.7.5 → methodproof-0.7.6}/pyproject.toml +1 -1
- {methodproof-0.7.5 → methodproof-0.7.6}/tests/test_security.py +7 -4
- {methodproof-0.7.5 → methodproof-0.7.6}/.github/workflows/ci.yml +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/.gitignore +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/LICENSE +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/__main__.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/_daemon.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/agents/__init__.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/agents/base.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/agents/music.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/agents/terminal.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/agents/watcher.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/binding.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/bip39.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/bridge.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/cli.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/config.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/crypto.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/e2e.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/hook.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/hooks/__init__.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/hooks/claude_code.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/hooks/claude_code.sh +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/hooks/cline_hook.sh +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/hooks/codex_hook.sh +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/hooks/gemini_hook.sh +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/hooks/install.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/hooks/kiro_hook.sh +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/hooks/mcp_register.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/hooks/openclaw/HOOK.md +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/hooks/openclaw/handler.ts +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/hooks/openclaw_install.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/hooks/opencode_plugin.js +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/hooks/wrappers.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/integrity.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/kdf.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/keychain.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/live.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/lock.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/mcp.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/proxy.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/proxy_daemon.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/repos.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/skills/methodproof/SKILL.md +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/viewer.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/methodproof/wordlist.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/test_windows_compat.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/tests/__init__.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/tests/test_analysis.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/tests/test_graph.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/tests/test_hooks.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/tests/test_live.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/tests/test_openclaw_hooks.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/tests/test_store.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/tests/test_wrappers.py +0 -0
- {methodproof-0.7.5 → methodproof-0.7.6}/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.
|
|
3
|
+
Version: 0.7.6
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
@@ -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
|
-
|
|
610
|
-
|
|
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")
|
|
@@ -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 =
|
|
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 =
|
|
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,
|
|
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 =
|
|
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((
|
|
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 =
|
|
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
|
-
|
|
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":
|
|
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
|
-
|
|
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.
|
|
3
|
+
version = "0.7.6"
|
|
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
|
-
|
|
109
|
+
# Use fixed entropy so the checksum corruption is deterministic
|
|
110
|
+
phrase = entropy_to_phrase(b"\x00" * 16)
|
|
110
111
|
words = phrase.split()
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|