Skip to content

Commit 2492f93

Browse files
committed
fix: address data mode review findings
1 parent ad65ba0 commit 2492f93

39 files changed

Lines changed: 708 additions & 173 deletions
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# Data Mode Review Fixes Implementation Plan
2+
3+
> **For Codex:** Follow the repository's test-first and verification rules for
4+
> each task. Keep the final implementation in one focused follow-up commit.
5+
6+
**Goal:** Resolve every verified PR #1166 review finding and restore merge
7+
readiness without changing Data mode's core architecture.
8+
9+
**Architecture:** Preserve the existing SQLite/PostgreSQL/DuckDB shared data
10+
models and candidate pipeline. Extend inventory identity metadata to retain
11+
merged keys, keep filtered PostgreSQL publications privacy-safe, and reuse the
12+
incremental append's existing session read.
13+
14+
**Tech stack:** Go, SQLite, PostgreSQL, DuckDB, Svelte 5, TypeScript, Paraglide,
15+
Vitest.
16+
17+
______________________________________________________________________
18+
19+
### Task 1: Lock down the critical regressions
20+
21+
**Files:**
22+
23+
- Modify: `internal/postgres/push_pgtest_test.go`
24+
- Modify: `internal/db/orphaned_test.go`
25+
- Modify: `internal/db/worktree_mappings_test.go`
26+
- Modify: `internal/sync/engine_bench_test.go`
27+
28+
1. Add or tighten tests that reproduce the PostgreSQL INSERT failure, the v70
29+
snapshot overwrite, normalized persisted paths, and incremental allocation
30+
regression.
31+
1. Run each focused test or benchmark and confirm the expected failure.
32+
1. Repair the placeholder sequence and upgrade gate.
33+
1. Normalize cross-platform expectations.
34+
1. Remove the duplicate incremental session read and rerun the benchmark.
35+
36+
### Task 2: Preserve merged project selection
37+
38+
**Files:**
39+
40+
- Modify: `internal/db/project_inventory.go`
41+
- Modify: `internal/db/project_inventory_test.go`
42+
- Modify: `internal/db/worktree_candidates.go`
43+
- Modify: `internal/db/worktree_candidates_test.go`
44+
- Modify: PostgreSQL and DuckDB parity implementations/tests as needed
45+
- Regenerate: frontend API models
46+
- Modify: `frontend/src/lib/stores/data.svelte.ts`
47+
- Modify: `frontend/src/lib/stores/data.test.ts`
48+
49+
1. Add tests for colliding display labels with distinct project keys and a deep
50+
link through a non-canonical key.
51+
1. Add `project_keys` while retaining the canonical `project_key`.
52+
1. Select candidate sessions for every key in a validated merged row.
53+
1. Update frontend row resolution and refresh behavior.
54+
1. Run backend parity and frontend store/component tests.
55+
56+
### Task 3: Repair publication scope and query bounds
57+
58+
**Files:**
59+
60+
- Modify: `internal/postgres/worktree_mappings_push.go`
61+
- Modify: `internal/postgres/worktree_mappings_push_pgtest_test.go`
62+
- Modify: `internal/db/project_inventory.go`
63+
- Modify: `internal/db/project_inventory_test.go`
64+
- Modify: `internal/db/project_identity.go`
65+
- Modify: `internal/db/project_identity_test.go`
66+
67+
1. Add a filtered-push test proving unrelated mapping paths are not published.
68+
1. Skip mapping publication for filtered pushes.
69+
1. Add cardinality tests over backend bind limits.
70+
1. Chunk machine candidate reads and aggregate rebuilds transactionally.
71+
1. Run SQLite and PostgreSQL focused suites.
72+
73+
### Task 4: Align behavior and error handling
74+
75+
**Files:**
76+
77+
- Modify: `internal/db/worktree_candidates.go`
78+
- Modify: `internal/db/worktree_reclassification.go`
79+
- Modify: `internal/server/huma_routes_settings.go`
80+
- Modify: associated Go tests
81+
82+
1. Add tests for zero-message candidates, writer-closed preview, legacy update
83+
duplicate handling, and token changes.
84+
1. Align session predicates to visible sessions.
85+
1. Map writer closure and uniqueness errors correctly.
86+
1. Include `original_project` in tokens and split the evaluator helper.
87+
1. Run focused DB and server tests.
88+
89+
### Task 5: Surface stale inventory and fix localized copy
90+
91+
**Files:**
92+
93+
- Modify: `frontend/src/lib/stores/data.svelte.ts`
94+
- Modify: `frontend/src/lib/components/data/DataPage.svelte`
95+
- Modify: associated frontend tests
96+
- Modify: all `frontend/messages/*.json`
97+
98+
1. Add a component regression test for a failed foreground reload after a
99+
successful load.
100+
1. Render the error and retry control above retained stale inventory.
101+
1. Replace the Activity-scoped no-candidate text with archive-wide wording in
102+
every locale.
103+
1. Run `npm run i18n:compile`, focused Vitest tests, and `npm run check`.
104+
105+
### Task 6: Finish parity and maintenance cleanup
106+
107+
**Files:**
108+
109+
- Modify: inventory timestamp handling and tests across backends
110+
- Modify: PostgreSQL mapping ordering and parity tests
111+
- Modify: stale comments identified by review
112+
113+
1. Add malformed-timestamp and deterministic-order regressions.
114+
1. Make SQLite and PostgreSQL observable behavior match.
115+
1. Correct stale bind-limit and collation comments.
116+
1. Run focused parity suites.
117+
118+
### Task 7: Verify and publish
119+
120+
1. Run Go formatting and vet.
121+
1. Run targeted and broad Go tests, PostgreSQL integration tests, frontend
122+
tests/checks, and benchmark comparison.
123+
1. Inspect the final diff and run the private-data scrub.
124+
1. Use the commit workflow to create one conventional follow-up commit.
125+
1. Push the current branch.
126+
1. Replace the PR body with a concise rationale-first summary with no test
127+
section or checklist.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Data Mode Review Fixes
2+
3+
## Goal
4+
5+
Make PR #1166 mergeable by repairing its verified correctness, upgrade,
6+
cross-platform, publication-scope, user-feedback, and performance regressions
7+
without changing the core Data mode architecture.
8+
9+
## Decisions
10+
11+
### Merged project inventory rows
12+
13+
Keep one inventory row per sanitized display label. Add every underlying project
14+
identity key to the row while retaining `project_key` as the stable canonical
15+
key. Candidate lookup validates that the requested key belongs to the displayed
16+
row, then includes sessions for every raw project merged into that row. The
17+
frontend resolves deep links against both the canonical key and the complete key
18+
list.
19+
20+
This preserves the intentional privacy-safe label merge while making every
21+
counted session reachable.
22+
23+
### Filtered PostgreSQL pushes
24+
25+
Do not publish worktree mappings during project-filtered pushes. Mapping rules
26+
are archive-scoped configuration and cannot be partitioned safely by project:
27+
dynamic rules have no fixed project, and independently filtered cursors share
28+
the same archive key in PostgreSQL. Publishing a partial set could either leak
29+
unrelated paths or delete rules belonging to another filtered scope.
30+
31+
Unfiltered pushes continue to publish the complete mapping set.
32+
33+
### Session eligibility
34+
35+
Data inventory, candidate discovery, preview, and apply all operate on visible
36+
sessions (`deleted_at IS NULL`), including sessions with zero messages. This
37+
keeps counts consistent across the three screens.
38+
39+
### Failed reloads
40+
41+
A failed foreground reload keeps the last successful inventory visible but also
42+
renders the error and retry action. Background refresh failures continue to
43+
preserve the current view without interrupting the user.
44+
45+
### Incremental append performance
46+
47+
Return the post-mapping project from the existing single-session mapping path so
48+
`writeIncremental` does not perform a second `GetSession` point read. The
49+
benchmark gate remains unchanged; the implementation must recover enough
50+
allocations to pass it.
51+
52+
## Other corrections
53+
54+
- Repair the PostgreSQL session INSERT placeholder sequence.
55+
- Move the source-snapshot upgrade gate to data version 71 and cover a v70
56+
source archive.
57+
- Normalize persisted mapping-path expectations in cross-platform tests.
58+
- Chunk inventory-machine and aggregate-rebuild `IN` queries.
59+
- Map writer shutdown to the established 503 response.
60+
- Map legacy-row uniqueness conflicts to the duplicate sentinel.
61+
- Include `original_project` in reclassification tokens.
62+
- Split the oversized mapping evaluator.
63+
- Fail consistently on malformed inventory timestamps.
64+
- Use byte-order collation for PostgreSQL mapping ordering.
65+
- Correct stale comments and localized archive-wide candidate copy.
66+
- Rewrite the PR description as a concise rationale-first summary.
67+
68+
## Validation
69+
70+
Use behavior-level regression tests before implementation. Run targeted Go,
71+
PostgreSQL integration, frontend, localization, formatting, vet, and benchmark
72+
checks, followed by the broadest practical suites. Scrub publication text and
73+
the final diff before pushing.

frontend/messages/en.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -917,7 +917,7 @@
917917
"data_reclassify_original": "Originally shown as {project}",
918918
"data_reclassify_candidates_loading": "Finding worktrees…",
919919
"data_reclassify_candidates_failed": "Failed to find worktrees",
920-
"data_reclassify_no_candidates": "No worktree paths were found for this project in the selected Activity view.",
920+
"data_reclassify_no_candidates": "No worktree paths were found for this project in the archive.",
921921
"data_reclassify_choose_worktree": "Choose a worktree",
922922
"data_reclassify_candidate_sessions": [
923923
{ "declarations": ["input count", "local countPlural = count: plural", "local countNumber = count: number"], "selectors": ["countPlural"], "match": { "countPlural=one": "{countNumber} session", "countPlural=other": "{countNumber} sessions" } }

frontend/messages/fr.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -917,7 +917,7 @@
917917
"data_reclassify_original": "Initialement affiché comme {project}",
918918
"data_reclassify_candidates_loading": "Recherche des arbres de travail…",
919919
"data_reclassify_candidates_failed": "Impossible de trouver les arbres de travail",
920-
"data_reclassify_no_candidates": "Aucun chemin d’arbre de travail trouvé pour ce projet dans la vue Activité sélectionnée.",
920+
"data_reclassify_no_candidates": "Aucun chemin d’arbre de travail trouvé pour ce projet dans l’archive.",
921921
"data_reclassify_choose_worktree": "Choisir un arbre de travail",
922922
"data_reclassify_candidate_sessions": [
923923
{ "declarations": ["input count", "local countPlural = count: plural", "local countNumber = count: number"], "selectors": ["countPlural"], "match": { "countPlural=one": "{countNumber} session", "countPlural=other": "{countNumber} sessions" } }

frontend/messages/ko.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -892,7 +892,7 @@
892892
"data_reclassify_original": "원래 표시 이름: {project}",
893893
"data_reclassify_candidates_loading": "워크트리 찾는 중…",
894894
"data_reclassify_candidates_failed": "워크트리를 찾지 못했습니다",
895-
"data_reclassify_no_candidates": "선택한 활동 보기에서 이 프로젝트의 워크트리 경로를 찾지 못했습니다.",
895+
"data_reclassify_no_candidates": "아카이브에서 이 프로젝트의 워크트리 경로를 찾지 못했습니다.",
896896
"data_reclassify_choose_worktree": "워크트리 선택",
897897
"data_reclassify_candidate_sessions": [
898898
{ "declarations": ["input count", "local countPlural = count: plural", "local countNumber = count: number"], "selectors": ["countPlural"], "match": { "countPlural=other": "세션 {countNumber}개" } }

frontend/messages/zh-CN.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -890,7 +890,7 @@
890890
"data_reclassify_original": "原显示名称:{project}",
891891
"data_reclassify_candidates_loading": "正在查找工作树…",
892892
"data_reclassify_candidates_failed": "无法查找工作树",
893-
"data_reclassify_no_candidates": "在所选活动视图中未找到此项目的工作树路径",
893+
"data_reclassify_no_candidates": "在存档中未找到此项目的工作树路径",
894894
"data_reclassify_choose_worktree": "选择工作树",
895895
"data_reclassify_candidate_sessions": [
896896
{ "declarations": ["input count", "local countPlural = count: plural", "local countNumber = count: number"], "selectors": ["countPlural"], "match": { "countPlural=other": "{countNumber} 个会话" } }

frontend/messages/zh-TW.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -890,7 +890,7 @@
890890
"data_reclassify_original": "原顯示名稱:{project}",
891891
"data_reclassify_candidates_loading": "正在尋找工作樹…",
892892
"data_reclassify_candidates_failed": "無法尋找工作樹",
893-
"data_reclassify_no_candidates": "在所選活動檢視中找不到此項目的工作樹路徑",
893+
"data_reclassify_no_candidates": "在封存中找不到此專案的工作樹路徑",
894894
"data_reclassify_choose_worktree": "選擇工作樹",
895895
"data_reclassify_candidate_sessions": [
896896
{ "declarations": ["input count", "local countPlural = count: plural", "local countNumber = count: number"], "selectors": ["countPlural"], "match": { "countPlural=other": "{countNumber} 個工作階段" } }

frontend/src/lib/api/generated/models/DbProjectInventoryRow.ts

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/src/lib/components/data/DataPage.svelte

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
}
4444
4545
function closeWorkspace() {
46-
const key = data.selectedProjectKey;
46+
const key = data.selectedRow?.project_key ?? data.selectedProjectKey;
4747
data.clearSelection();
4848
requestAnimationFrame(() => {
4949
// Match on dataset instead of an attribute selector so arbitrary
@@ -95,9 +95,15 @@
9595
/>
9696
{/key}
9797
{:else if data.inventory}
98-
<!-- Inventory-first ordering: once inventory has loaded once it keeps
99-
rendering through background reloads; loading/error below only
100-
apply before that first successful load. -->
98+
<!-- Keep the last successful inventory visible during reloads, while
99+
surfacing foreground failures so stale data is never silent. -->
100+
{#if data.error}
101+
<div class="status error">
102+
<span>{data.error}</span>
103+
<button class="retry-btn" onclick={() => data.load()}>{m.shared_retry()}</button>
104+
</div>
105+
{/if}
106+
101107
<div class="summary-strip">
102108
<span>{m.data_summary_projects({ count: data.inventory.total_projects })}</span>
103109
<span>{m.data_summary_sessions({ count: data.inventory.total_sessions })}</span>

frontend/src/lib/components/data/DataPage.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,18 @@ describe("DataPage", () => {
174174
expect(document.body.textContent).toContain(m.data_summary_projects({ count: 1 }));
175175
});
176176

177+
it("shows a foreground reload error above the retained inventory", async () => {
178+
data.inventory = makeInventory([makeRow({ project_key: "k1" })]);
179+
api.getApiV1DataProjects.mockRejectedValueOnce(new Error("server unavailable"));
180+
181+
component = mount(DataPage, { target: document.body });
182+
await flush();
183+
184+
expect(document.body.textContent).toContain("server unavailable");
185+
expect(document.body.textContent).toContain(m.data_summary_projects({ count: 1 }));
186+
expect(document.querySelector<HTMLButtonElement>(".retry-btn")).not.toBeNull();
187+
});
188+
177189
it("shows the empty message and no table when there are no projects", async () => {
178190
const inventory = makeInventory([]);
179191
api.getApiV1DataProjects.mockResolvedValueOnce(inventory);

0 commit comments

Comments
 (0)