Skip to content

Commit aaeb8af

Browse files
committed
Merge branch 'feat/history-v2-and-preferences' into dev
2 parents 6bb731a + 6c02d3d commit aaeb8af

18 files changed

Lines changed: 1005 additions & 44 deletions

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,6 @@ coverage.xml
1414

1515
# orchestration engine: machine/trial-only config override
1616
orchestration.conf.local
17+
18+
# Local codex CLI project config (machine-specific MCP paths)
19+
.codex/

‎README.md‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# Wherewolf
22

3-
<img src="https://raw.githubusercontent.com/beallio/wherewolf/main/src/wherewolf/assets/img/wherewolf_banner.png?cacheBuster=16" width="100%">
3+
<img src="https://raw.githubusercontent.com/beallio/wherewolf/main/src/wherewolf/assets/img/wherewolf_banner.png?cacheBuster=17" width="100%">
44

5-
[![CI](https://github.com/beallio/wherewolf/actions/workflows/ci.yml/badge.svg?cacheBuster=16)](https://github.com/beallio/wherewolf/actions/workflows/ci.yml)
6-
[![PyPI version](https://img.shields.io/pypi/v/wherewolf.svg?cacheBuster=16)](https://pypi.org/project/wherewolf/)
7-
[![License: GPL-3.0-only](https://img.shields.io/badge/License-GPL--3.0--only-blue.svg?cacheBuster=16)](https://www.gnu.org/licenses/gpl-3.0.html)
5+
[![CI](https://github.com/beallio/wherewolf/actions/workflows/ci.yml/badge.svg?cacheBuster=17)](https://github.com/beallio/wherewolf/actions/workflows/ci.yml)
6+
[![PyPI version](https://img.shields.io/pypi/v/wherewolf.svg?cacheBuster=17)](https://pypi.org/project/wherewolf/)
7+
[![License: GPL-3.0-only](https://img.shields.io/badge/License-GPL--3.0--only-blue.svg?cacheBuster=17)](https://www.gnu.org/licenses/gpl-3.0.html)
88

99
A production-grade, local SQL workbench for querying files (CSV, Parquet, JSON) using DuckDB or Spark.
1010

@@ -19,11 +19,15 @@ A production-grade, local SQL workbench for querying files (CSV, Parquet, JSON)
1919
- **💬 Messages Panel:** Structured execution results, detailed metrics (duration, preview row counts), and formatted error tracebacks replacing raw text placeholders.
2020
- **🔀 Full-Query Ordering:** Context menu actions for applying ascending or descending `ORDER BY` clauses to full queries without disturbing local table proxy sorting.
2121
- **Safe Preview:** Scrollable results limited to 1000 rows.
22-
- **Query History:** Persists past queries in `~/.wherewolf/history.json`.
22+
- **Versioned Query History:** Persists the newest 100 queries in `~/.wherewolf/history.json`,
23+
automatically migrates prior history safely, and keeps each record's stable ID for unambiguous
24+
desktop selection.
25+
- **Persistent Desktop Preferences:** Window geometry, dock layout, splitter proportions, editor
26+
font size, recent dataset directory, and completion preferences survive desktop restarts.
2327
- **Export:** Download query results as CSV, Excel, or Parquet. DataFrame handling and exports are Polars-based. When the preview is truncated, use **Prepare full export** to re-run the query without a row limit and download the entire result set.
2428
- **Execution Metrics:** Tracks row count, status, and execution time in the status bar and Messages panel.
2529

26-
![Wherewolf Screenshot](https://raw.githubusercontent.com/beallio/wherewolf/main/src/wherewolf/assets/img/screenshot.png?cacheBuster=16)
30+
![Wherewolf Screenshot](https://raw.githubusercontent.com/beallio/wherewolf/main/src/wherewolf/assets/img/screenshot.png?cacheBuster=17)
2731

2832
## Installation
2933

@@ -57,6 +61,10 @@ uv run wherewolf-desktop
5761
4. Click **Run** or press `Ctrl+Return` to execute queries asynchronously using DuckDB.
5862
5. Click **Cancel** or press `Ctrl+.` to cancel active query execution.
5963
6. Execution uses request-scoped isolated DuckDB connections, limit+1 truncation detection, and automatic query history persistence in `~/.wherewolf/history.json`.
64+
7. In the desktop shell, activate a History entry to restore its SQL without executing it. Available
65+
historical dataset files are restored; unavailable paths are reported in the status bar.
66+
8. Use **View → Reset Layout** to restore the default dock arrangement, or **File → Clear History**
67+
to empty the persisted history safely.
6068
7. Click **Format SQL** (`Ctrl+Shift+F`) to normalize syntax.
6169

6270

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# History v2 and persistent preferences — implementation session
2+
3+
Date: 2026-08-01
4+
5+
## Objective
6+
7+
Implement Phase 11 from `docs/plans/2026-08-01_history-v2-and-preferences.md`.
8+
9+
## Baseline
10+
11+
- Base commit: `6bb731a` (`docs(plans): scope phase 11 history v2 and persistent preferences`).
12+
- Python 3.14: `307 passed, 1 skipped, 1 warning in 20.72s`.
13+
- Python 3.12: `307 passed, 1 skipped, 1 warning in 14.38s`.
14+
- Both baseline commands used `HOME=/tmp/wherewolf/home` so default history writes stay in the
15+
permitted temporary workspace. The initial unmodified 3.14 attempt without that isolated
16+
home failed with six test failures and one Qt teardown error because `/home/beallio/.wherewolf`
17+
is read-only in this sandbox; its output is retained at `/tmp/wherewolf/baseline-3.14.log`.
18+
19+
## Planned validation
20+
21+
- Run the plan's full quality gates and both-interpreter suite after implementation.
22+
- Run the required mutation checks after committing the implementation.
23+
- No human desktop verification, real-user-history migration, performance test, macOS test, or
24+
Windows test is planned for this Linux/offscreen session.
25+
26+
## Implementation
27+
28+
- Added schema-v2 history records with UUIDs, on-read v1 migration, per-record validation,
29+
UUID lookup, and preserved atomic writes in `storage/history.py`.
30+
- Added a UUID-backed History dock that restores SQL without executing it, restores available
31+
catalog files, and reports missing files.
32+
- Added safe settings fallback, restored desktop layout coverage, and Reset Layout / Clear History
33+
actions.
34+
35+
## Measured results
36+
37+
- Python 3.14 final suite: `333 passed, 1 skipped, 1 warning in 21.01s`.
38+
- Python 3.12 final suite: `333 passed, 1 skipped, 1 warning in 14.67s`.
39+
- The 3.12 run used `--no-cov`; the environment was then restored with
40+
`./run.sh uv sync --all-extras --dev --python 3.14`.
41+
- V8 mutation checks failed at their intended node IDs:
42+
- `tests/test_history_dock.py::test_history_dock_selects_duplicate_labels_by_stable_id`
43+
- `tests/test_history.py::test_new_entries_have_versioned_stable_ids_and_streamlit_keys`
44+
- `tests/test_history.py::test_malformed_records_are_isolated_from_valid_history`
45+
- `tests/test_history.py::test_v1_history_migrates_all_records_in_order_and_only_once`
46+
- `tests/test_history.py::test_record_cap_evicts_the_oldest_v1_record_after_migration`
47+
- `tests/test_settings_service.py::test_settings_service_each_corrupt_value_falls_back_to_its_default[completion_enabled_key-false-restore_completion_enabled-True]`
48+
- `closeEvent` was not changed, so V10 was not applicable. No human desktop test, real-user
49+
migration, performance measurement, macOS test, or Windows test was run.
50+
51+
## Review round 01 follow-up
52+
53+
- Added an autouse pytest fixture that redirects the default `HistoryManager` path and both
54+
QSettings user-scope backends to each test's `tmp_path`.
55+
- Added a bare-`MainWindow` regression test that failed before the fixture because construction
56+
initialized `/home/beallio/.wherewolf/history.json`; it now asserts the history and QSettings
57+
paths are test-local.
58+
- Focused fixture regression group: `30 passed in 0.24s`.
59+
- Python 3.14 full suite: `334 passed, 1 skipped, 1 warning in 21.76s`.
60+
- `scripts/orchestration/run-quality-gates`: exit 0 (`334 passed, 1 skipped, 1 warning in
61+
22.00s`).
62+
- Python 3.12 full suite with `--no-cov`: `334 passed, 1 skipped, 1 warning in 14.96s`.
63+
- Restored the shared environment with `./run.sh uv sync --all-extras --dev --python 3.14`.
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# Review — history-v2-and-preferences (round 01)
2+
3+
Branch: `feat/history-v2-and-preferences` @ `e1891de`
4+
Reviewed against: `docs/plans/2026-08-01_history-v2-and-preferences.md`
5+
6+
## Verdict
7+
8+
CHANGES_REQUESTED — **one required change, and it is a test-isolation defect you already
9+
worked around rather than fixed.** The implementation itself is correct and I found no
10+
functional defects.
11+
12+
## What you did well
13+
14+
- **Fourteen atomic commits in plan order**, with the entire storage layer landing Qt-free
15+
before the dock existed.
16+
- **The storage tests map 1:1 to the plan's requirements** — versioned ids, migration in order
17+
and only once, existing-v2 untouched, failed migration leaving the original intact, malformed
18+
isolation, unparseable file not deleted, missing-key skipped, `get_by_id`, cap evicting
19+
oldest, and a **parametrized** Streamlit-shape test covering both migrated and fresh files.
20+
That last one is the compatibility guarantee and you tested both paths, which is what the
21+
plan asked for and easy to half-do.
22+
- **Migration is tested at realistic scale** — 100 records, order preserved, idempotency
23+
asserted. A single-record test would have proven nothing about ordering or the cap.
24+
- **The dock selects by stable id**, carrying `record["id"]` in `Qt.ItemDataRole.UserRole` and
25+
resolving through `get_by_id`. This is the exit criterion and the design is right.
26+
- **`app.py` is untouched** and the Streamlit path diff is empty. The compatibility constraint
27+
held.
28+
- **The session log is the best this project has produced.** Both baselines, both final
29+
tallies, mutation node ids, and — importantly — *"`closeEvent` was not changed, so V10 was
30+
not applicable"* rather than claiming an unmeasured result. **I verified that claim and it is
31+
accurate**: `closeEvent` appears in the diff only as unchanged context. That is exactly the
32+
recording discipline the plan asked for.
33+
34+
### My measurements
35+
36+
| check | result |
37+
|---|---|
38+
| suite on **3.14** | 333 passed, 1 skipped |
39+
| suite on **3.12** | 333 passed, 1 skipped — identical |
40+
| `run-quality-gates` | pass |
41+
| **V10** crash gate 25 + 25 | **0 native crashes / 50** |
42+
| **V9** 3.14-only syntax | none |
43+
| V2 Streamlit path diff | empty |
44+
| V8 mutation 1 (select by list index) | FAILED `test_history_dock_selects_duplicate_labels_by_stable_id` |
45+
| V8 mutation 3 (malformed discards all) | FAILED `test_malformed_records_are_isolated_from_valid_history`, `test_record_missing_required_key_is_skipped` |
46+
47+
I ran V10 despite it being "not applicable" — a new dock is new QObject lifetime, and the
48+
result is clean at 0/50. You do not need to re-run it.
49+
50+
## Required change
51+
52+
### F1. 37 tests now read the user's real history file
53+
54+
`HistoryDock.__init__` ends with `self.refresh()`, which reads from disk, and `MainWindow`
55+
constructs the dock unconditionally with `history_manager or HistoryManager()` —
56+
whose `DEFAULT_PATH` is `~/.wherewolf/history.json`.
57+
58+
**37 tests construct `MainWindow` without injecting a history manager.** Every one of them now
59+
reads the developer's real history file. This is **new in this phase**: I checked `dev`, which
60+
had zero reads at construction.
61+
62+
You already hit this. Your own session log records it:
63+
64+
> The initial unmodified 3.14 attempt without that isolated home failed with **six test
65+
> failures and one Qt teardown error** because `/home/beallio/.wherewolf` is read-only in this
66+
> sandbox.
67+
68+
You solved it by pointing `HOME` at a writable temporary directory. That got the round done,
69+
but it treated the symptom: the suite's correctness now depends on ambient user state. It
70+
passes on this machine because my `~/.wherewolf/history.json` happens to be small and
71+
well-formed. On a machine whose history is large, or contains one of the malformed records
72+
this very phase teaches the reader to expect, tests that never intended to touch it would fail
73+
in ways that look like real regressions.
74+
75+
**This matters more now, not less.** `ORCH_ADD_DIRS` has since been granted
76+
`~/.wherewolf` and `~/.config/Wherewolf` so the sandbox no longer blocks writes. The sandbox
77+
was accidentally acting as a safety net. A future test that triggers a destructive path on a
78+
default-constructed `MainWindow` can now write to the real file. Your Clear History test
79+
correctly injects a `tmp_path` manager — keep that discipline, but do not rely on every future
80+
test remembering.
81+
82+
**Fix:** an autouse fixture in `tests/conftest.py` that redirects the history default path (and
83+
`QSettings` storage, which has the same exposure through `SettingsService`) to a per-test
84+
temporary location. Then assert it: a test proving that constructing a bare `MainWindow()` does
85+
**not** touch `~/.wherewolf`. Given this phase's entire purpose is not mishandling user history,
86+
that guarantee belongs in the suite.
87+
88+
## Already handled — no action needed
89+
90+
`.codex/config.toml`, a machine-specific codex MCP config, was left untracked in the repo root
91+
and was breaking the `git status --short` must-print-nothing check that every round's
92+
verification depends on. I added `.codex/` to `.gitignore` myself (`cef3342`) since it is
93+
tooling hygiene rather than phase work. The tree is clean.
94+
95+
## Verification before marking complete
96+
97+
- The autouse isolation fixture, plus the test proving a bare `MainWindow()` does not touch
98+
`~/.wherewolf`.
99+
- `./run.sh uv run pytest -q` on 3.14 and `--python 3.12` — record both.
100+
**Remember:** `uv run --python 3.12` re-syncs the shared venv; restore with
101+
`./run.sh uv sync --all-extras --dev --python 3.14`.
102+
- `scripts/orchestration/run-quality-gates` → exit 0.
103+
- `git status --short` → prints nothing.
104+
105+
**Do not re-run V10** (0/50 measured above) or the V8 mutations unless you change source.
106+
107+
## Constraints
108+
109+
Do not remove `timid = true`. Do not disable coverage. Do not skip, delete or xfail tests. Do
110+
not modify `app.py` or the Streamlit path beyond `storage/history.py`. Do not regress the
111+
atomic write. Do not touch `main`. Do not bump the package version.
112+
113+
## Deferred — correctly recorded by you
114+
115+
No human has seen the dock or a restored layout; all Qt tests are offscreen. No migration has
116+
been run against a real user's history file. No performance measurement. macOS and Windows
117+
unverified — `QSettings` backends differ per platform and this is Linux-only verification.
118+
Export is Phase 12, Spark Phase 13, Streamlit removal Phase 14.
119+
120+
STATUS: CHANGES_REQUESTED
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Review — history-v2-and-preferences (round 02)
2+
3+
Branch: `feat/history-v2-and-preferences` @ `e960e07`
4+
Reviewed against: `docs/plans/2026-08-01_history-v2-and-preferences.md` and review 01
5+
6+
## Verdict
7+
8+
APPROVED.
9+
10+
F1 is fixed, and fixed more completely than I asked for.
11+
12+
## F1 — resolved, and verified load-bearing
13+
14+
The autouse fixture in `tests/conftest.py` redirects `HistoryManager.DEFAULT_PATH` to a
15+
per-test temporary location. **It also redirects `QSettings`** — both `IniFormat` and
16+
`NativeFormat` at `UserScope` — which I flagged as having the same exposure but did not
17+
require. Covering it unprompted is the right call: `SettingsService` writes to
18+
`~/.config/Wherewolf`, and this phase adds settings round-trip and corrupt-value tests.
19+
20+
The assertion test is genuine, not decorative:
21+
22+
```python
23+
def test_bare_main_window_does_not_touch_user_history(...):
24+
monkeypatch.setattr(HistoryManager, "_ensure_storage", assert_isolated_storage)
25+
assert window.history_manager.storage_path != user_history_path
26+
assert Path(window._settings_service._settings.fileName()).is_relative_to(tmp_path)
27+
```
28+
29+
It hooks `_ensure_storage` so the check fires on the real construction path rather than
30+
inspecting an attribute after the fact, and it asserts both the history path **and** the
31+
settings file location.
32+
33+
I confirmed the fixture is load-bearing by removing the `DEFAULT_PATH` redirect:
34+
35+
```text
36+
FAILED tests/test_main_window.py::test_bare_main_window_does_not_touch_user_history
37+
```
38+
39+
So the guarantee is enforced, not merely asserted alongside an already-safe default.
40+
41+
## Final state — measured by review
42+
43+
| check | result |
44+
|---|---|
45+
| suite on **3.14** | 334 passed, 1 skipped |
46+
| suite on **3.12** | 334 passed, 1 skipped — identical |
47+
| `run-quality-gates` | pass |
48+
| **V10** crash gate 25 + 25 (round 01) | 0 native crashes / 50 |
49+
| **V9** 3.14-only syntax | none |
50+
| **V2** Streamlit path diff | empty; `app.py` untouched |
51+
| V8 mutation 1 (index selection) | bites |
52+
| V8 mutation 3 (malformed discards all) | bites |
53+
| isolation fixture removed | test FAILS, as it must |
54+
| `git status --short` | clean |
55+
56+
## What this phase delivered
57+
58+
Schema-v2 history records with UUIDs, on-read v1 migration that preserves order and is
59+
idempotent, per-record malformed isolation, id-based lookup, a History dock that selects by
60+
stable id, SQL and catalog restore with missing files reported, corrupt-settings fallback,
61+
window/dock/splitter restore, and Reset Layout plus Clear History.
62+
63+
Three things deserve specific credit:
64+
65+
- **The migration is tested at realistic scale** — 100 records, order preserved, run twice to
66+
prove idempotency. A single-record test would have proven nothing about ordering or the cap.
67+
- **Malformed-record isolation replaces a silent total-loss path.** The previous
68+
`except (OSError, json.JSONDecodeError): return []` discarded a user's entire history on one
69+
corrupt byte. That is the worst failure mode this phase could have had, and it is now
70+
covered by tests asserting surviving count *and* contents.
71+
- **The dock cannot reproduce the `app.py` defect.** Selection resolves through
72+
`get_by_id`, and the duplicate-label test fails immediately if selection becomes
73+
index-based — I verified that by mutation.
74+
75+
The Streamlit compatibility guarantee held: `app.py` is unmodified and every record still
76+
exposes `timestamp` and `query` in the v1 read shape, tested for both migrated and fresh files.
77+
78+
## Note on the sandbox interaction
79+
80+
Round 01 surfaced this defect as six test failures in codex's sandbox, because
81+
`~/.wherewolf` was read-only there. `ORCH_ADD_DIRS` has since been granted `~/.wherewolf` and
82+
`~/.config/Wherewolf`, so that sandbox no longer blocks writes — it had been acting as an
83+
accidental safety net. Fixing the isolation properly, rather than relying on the sandbox, is
84+
what makes that grant safe.
85+
86+
## Deferred and correctly recorded
87+
88+
No human has seen the dock or a restored layout; all Qt tests are offscreen. **No migration has
89+
been run against a real user's history file.** No performance measurement. macOS and Windows
90+
unverified — `QSettings` backends differ per platform and this is Linux-only verification.
91+
Export is Phase 12, Spark Phase 13, Streamlit removal Phase 14.
92+
93+
STATUS: APPROVED

‎orchestration.conf‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ ORCH_IMPLEMENTER_MODEL="gpt-5.6-terra"
4848
# read-only by default and offers no config toggle (only writable_roots), so
4949
# without this the implementer can edit files but cannot branch or commit — it
5050
# completes a whole round leaving everything uncommitted on the base branch.
51-
ORCH_ADD_DIRS="/tmp /home/beallio/.local/state/wherewolf-cache /home/beallio/Dropbox/Scripts/wherewolf/.git"
51+
ORCH_ADD_DIRS="/tmp /home/beallio/.local/state/wherewolf-cache /home/beallio/Dropbox/Scripts/wherewolf/.git /home/beallio/.wherewolf /home/beallio/.config/Wherewolf"
5252

5353
# Print-mode timeout for the implementer.
5454
# ORCH_PRINT_TIMEOUT="60m"

‎src/wherewolf/desktop/actions.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ class DesktopActions:
1717
format_sql: QAction
1818
add_datasets: QAction
1919
show_completion: QAction
20+
reset_layout: QAction
21+
clear_history: QAction
2022

2123

2224
def build_actions(parent: QWidget | None = None) -> DesktopActions:
@@ -42,10 +44,18 @@ def build_actions(parent: QWidget | None = None) -> DesktopActions:
4244
show_completion.setEnabled(True)
4345
show_completion.setShortcut(QKeySequence("Ctrl+Space"))
4446

47+
reset_layout = QAction("Reset Layout", parent)
48+
reset_layout.setEnabled(True)
49+
50+
clear_history = QAction("Clear History", parent)
51+
clear_history.setEnabled(True)
52+
4553
return DesktopActions(
4654
run=run,
4755
cancel=cancel,
4856
format_sql=format_sql,
4957
add_datasets=add_datasets,
5058
show_completion=show_completion,
59+
reset_layout=reset_layout,
60+
clear_history=clear_history,
5161
)

0 commit comments

Comments
 (0)