Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions progress.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ Completed work is archived in [archive/](./archive/), one file per calendar mont

## Current State

**Last Updated:** 2026-08-20
**Active Feature:** feat-026 / issue #105 — sync architecture refactor on `refactor/sync-domain-pipeline`. `SyncPlanner` is now the decision source for normal push, batch pull/preview, single pull, and moves. Edited tracked renames with a free destination plan one move instead of being auto-skipped; remote-only changes pull without false conflicts; real two-sided divergence and occupied move destinations remain conflicts. Post-push CI hardening is locally green; real provider CI plus Obsidian desktop/mobile manual verification remain before declaring the feature complete.
**Last Updated:** 2026-08-22
**Active Feature:** feat-026 / issue #105 — Source Control refactor. Stacked PRs on `claude/source-control-foundation` (PR #129 → main): **#129** foundation + integration (`4e647fb`, legacy UI removed, `SourceControlItemView` wired as sole entry); **#130** execution-result + conflict status (`b3379b7`, `OperationStatus 'conflict'` + `ExecutionResult`); **PR131 state-model unification (uncommitted, this session)** — `src/logic/source-control/state/` with `SourceControlState` container + `SelectionState`/`OperationState`/`FilterState`/`ExpandedNodesState`/`SelectedChangeState`; `SourceControlViewModel` is the single mutation facade; `SourceControlView` holds no state and mutates only through the ViewModel. Pure refactor, no behavior change. Gate green: `npx eslint .` 0 errors; `npm run build` PASS (Obsidian 1.11 compat); `npx vitest run` 59 files / 553 tests. **Manual Obsidian verification remains** for #129/#130 (UI surface) before final merge. Phase 1 string purge intentionally skipped (kept `sync-status-view` view type + `open-sync-status` command id for pinned-leaf migration).
**Parallel Work:** PR #87 (4x Dependabot security alerts via npm overrides) and Issue #57 (live-credential smoke test).

## Outstanding Items
Expand All @@ -17,6 +17,8 @@ Completed work is archived in [archive/](./archive/), one file per calendar mont

## Latest Evidence

- [x] PR130 Source Control execution-result + conflict status (2026-08-22), branch `feat/source-control-integration` (uncommitted): added `'conflict'` to `OperationState.OperationStatus` with a `conflict()` method (distinct lifecycle from `'failed'`); new `ExecutionResult` (`{completed, conflicts, failed}` `ChangeId[]`) as a thin UI projection derived in `SourceControlActionService` from existing `PushResults`/`SyncResult`/`RemoteDeleteResult` — no new sync model; `push`/`pull`/`deleteRemote`/`deleteLocal`/`resolveConflict` now return `Promise<ExecutionResult>`, with `push` reading `PushResults.conflictedPaths` → `'conflict'` (conflict takes precedence over error); `SourceControlViewModel` exposes `lastOperationResult` via `setOperationResult`/`clearOperationResult`. Known asymmetry by design: pull `SyncResult` has only a conflict count (no per-path), so pull conflicts surface via change-model reclassification (`kind:'conflict'`), not this projection. TDD: new `ExecutionResult.test.ts` plus extended OperationState/ActionService/ViewModel tests covering conflict mapping, mixed 7/3/1 batch, conflict-precedence, throw→all-failed, and return-value contracts. Verification: `npx eslint .` — 0 errors; `npm run build` — clean incl. Obsidian 1.11.0 compatibility; `npx vitest run` — 66 files / 700 tests (source-control logic 7 files / 60 tests). No UI, no sync-domain changes, per the user's "先把 execution semantics 補完整,不要先做 UI" directive.

- [x] Issue #105 post-push CI hardening (2026-08-20), commit `948df28`: diagnosed run 32336155736 as two exhausted transient-provider attempts rather than a planner regression (GitHub 503/socket close; GitLab deadline exceeded). Increased provider E2E attempts from 2 to 3. A duplicate matrix cancelled by the shared push/PR concurrency group now produces a neutral aggregate gate with `run-ci=false`, so it neither creates a misleading `E2E gate` failure nor starts duplicate downstream CI; real failures still block. SyncManager E2E push preconditions now include `success`, `failed`, and provider `errors` in assertion diagnostics instead of surfacing only a secondary count mismatch. Added workflow contract and diagnostic unit tests and updated the E2E documentation. Verification: `actionlint v1.7.12 .github/workflows/ci.yml` — 0 errors; `npx eslint .` — 0 errors; `npm run build` — clean including Obsidian 1.11 compatibility; `npx vitest run` — 56 files / 613 tests; `npm run test:e2e -- --provider gitea` — 2 files / 14 tests and container cleanup; `git diff --check` — clean. Real CI run 32338116598 passed GitHub/GitLab production E2E, independent verification, cleanup, aggregate gate, Node 22/24 tests, lint, package, and build/release. The initial disabled-Gitea job landed on offline runner `heavenweb-runner-8`; failed-only rerun completed its skip in 11s and the full run concluded success. Provider API checks found no remaining `e2e/pr/127/**` or branch-source E2E refs. AGENTS-required Haiku was unavailable, so verification ran locally and through real CI.

- [x] Issue #105 unified sync decisions and move regression (2026-08-20): added operation-aware `SyncPlanner.planFor(push|pull)`, `MoveFacts`, and the `move` domain action. Normal push, batch pull and preview, single pull, and tracked moves now consume planner decisions instead of reimplementing SHA conflict checks. Removed `PushCoordinator.queueMove`'s stale-metadata gate, so an edited tracked rename with a free destination appears under Moves and commits once; occupied destinations remain conflicts. Fixed the complementary pull false positive: a remote-only change now pulls, while real two-sided divergence still resolves as conflict. Content-fetched text/binary paths normalize equal bytes to the provider blob SHA before planning, preserving binary and GitLab legacy-baseline behavior. Added planner operation matrix, coordinator move regression, batch pull, and single pull coverage. Verification: `npx eslint .` — 0 errors; `npm run build` — clean including Obsidian 1.11 compatibility; `npx vitest run` — 54 files / 610 tests; `git diff --check` — clean. Manual Obsidian verification remains.
Expand Down
88 changes: 56 additions & 32 deletions session-handoff.md
Original file line number Diff line number Diff line change
@@ -1,44 +1,68 @@
# Session Handoff

**Date:** 2026-08-20
**Branch:** `refactor/sync-domain-pipeline` (PR #127)
**Active Feature:** feat-026 / issue #105 — sync architecture refactor
**Date:** 2026-08-22
**Branch:** `refactor/source-control-state-model` (PR131, base
`feat/source-control-integration` PR #130 `b3379b7`)
**Active Feature:** feat-026 / issue #105 — Source Control refactor
(plan: `docs/source-control-refactor/roadmap.md`)

## Completed This Session
## Completed this session — PR131 (Phase 2: unify state model)

Investigated the failed real-provider CI after the unified planner commit. The move paths passed;
GitHub exhausted two attempts on a 503 and `UND_ERR_SOCKET`, while GitLab exhausted two attempts
on provider deadline errors. The tests then surfaced secondary count/existence assertions that
hid those original request failures.
Reorganized Source Control UI state from scattered View-local fields + direct
store access into one `SourceControlState` container, with the ViewModel as the
single facade the View reads from and mutates through. **No behavior change**
— pure refactor; all 553 tests pass.

Hardened CI with three provider attempts, explicit push-result diagnostics in SyncManager E2E,
and workflow contract coverage. When the shared push/PR concurrency group cancels a duplicate
matrix, its aggregate gate now reports the replacement neutrally and emits `run-ci=false`, so it
does not leave an additional aggregate red check or run downstream CI twice. Real failures remain
blocking. Updated the real-provider E2E documentation to match.
- New `src/logic/source-control/state/`:
- `SourceControlState.ts` — thin container composing the change model + every
UI state slice (selection, operation, filter, expanded nodes, selected
change). Not a god-object: each slice keeps its own invariants.
- `SelectionState.ts` — moved/renamed from `PushSelectionStore` (same API).
- `OperationState.ts` — moved from `src/logic/source-control/` (incl. the
`'conflict'` status from PR #130).
- `FilterState.ts` — new (active filter; was View-local).
- `ExpandedNodesState.ts` — new (collapsed sections + folders; was View-local).
- `SelectedChangeState.ts` — new (selected change id; was View-local).
- `SourceControlViewModel` — now constructed from `SourceControlState`; is the
mutation facade (`setFilter`, `toggleSection/Folder`, `selectForPush`/
`deselectFromPush`, `selectForDiff`/`clearSelection`, `getCollapsedFolders`,
`setOperationResult`/`clear`). The View no longer reaches any store directly.
- `SourceControlView` — thinned: holds **no state** (no `filter`/
`collapsedSections`/`collapsedFolders`/`selectedChangeId`); reads everything
from the ViewModel and mutates only through it. `getFilter`/`getSelected
ChangeId` moved to the ViewModel. Constructor is now `(viewModel, callbacks)`.
- `main.ts` / `SourceControlItemView` — construct `SourceControlState`; plugin
exposes `sourceControlState` (replaces `pushSelectionStore`/`operationState`).
- `SourceControlFilter.matchesFilter` — selection param type is `SelectionState`.
- Tests: moved `OperationState.test.ts`/`PushSelectionStore.test.ts` into
`state/` (renamed to `SelectionState.test.ts`); new `FilterState.test.ts` /
`ExpandedNodesState.test.ts` / `SelectedChangeState.test.ts`; updated
ViewModel/View/ItemView/ActionService tests for the new construction.

Committed as `948df28` (`fix(ci): harden provider e2e failures`) and pushed to
`origin/refactor/sync-domain-pipeline`. The pre-existing untracked `.codex-gitlab.env` remains
untouched.

## Verification Evidence
## Verification evidence

```text
npx eslint . -> PASS, 0 errors
npm run build -> PASS, incl. Obsidian 1.11 compatibility
npx vitest run -> PASS, 56 files / 613 tests
npm run test:e2e -- --provider gitea -> PASS, 2 files / 14 tests; container removed
actionlint v1.7.12 .github/workflows/ci.yml -> PASS, 0 errors
git diff --check -> PASS
real CI run 32338116598 -> PASS after failed-only rerun of a disabled Gitea leg assigned to an offline runner
GitHub/GitLab sandbox branch query -> PASS, no e2e/pr/127 or source-branch refs remain
npx eslint . -> 0 errors
npm run build -> PASS (tsc + Obsidian 1.11.0 compat + esbuild)
npx vitest run -> 59 files / 553 tests
```

The AGENTS-required Haiku verifier was unavailable in this environment, so verification ran
locally in this session.
## Exact next step

1. Review/commit PR131 (uncommitted now) + push; PR base = PR #130 branch.
2. PR #129 (foundation + integration `4e647fb`) and PR #130 (conflict status
`b3379b7`) still need **manual Obsidian verification** before final merge
(DoD for the UI surface). PR131 doesn't change runtime behavior, so it
shares that same manual-verification requirement once stacked.
3. Remaining Clean-Plan items: Phase 3 (action pipeline — already mostly clean,
verify no direct sync in UI: confirmed clean; single/batch already unified),
Phase 5 (view split — mostly done; `ConflictPanel` is Phase C feature, deferred),
Phase 7 (docs reorg into `docs/source-control/*`), Phase 6 (workflow test
coverage — PR135). The user's directive: clean architecture first, then
conflict UX (Phase C).

## Exact Next Step
## Local working tree

Complete the remaining Obsidian desktop/mobile move smoke tests. Verify moving and editing a
tracked file appears under Moves and applies as one remote move, while an occupied remote
destination remains a skipped conflict.
- `package-lock.json` modified (from `npm ci`); not committed. npm caches were
cleared this session to free disk (`~/.npm/_npx`, `~/.npm/_cacache`).
`/tmp/kilo/pr130/node_modules` was removed to free space (regenerable).
28 changes: 28 additions & 0 deletions src/logic/source-control/ExecutionResult.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { ChangeId } from './types';

/**
* UI-facing projection of a batch operation's outcome: which targeted changes
* completed, which need conflict resolution, and which hard-failed.
*
* This is a **projection** derived in `SourceControlActionService` from the
* existing sync-domain results (`PushResults`/`SyncResult`/`RemoteDeleteResult`)
* — not a new sync model. It exists so the Source Control UI can render a batch
* summary ("7 completed / 3 conflicts / 1 failed") instead of only a binary
* success/failed.
*
* Per-change persistence lives in `OperationState`; this type is the transient
* aggregate the ViewModel exposes to the UI for one render cycle.
*/
export interface ExecutionResult {
/** Changes that completed without conflict or error. */
completed: ChangeId[];
/** Changes the executor reported as conflicts (needs-resolution, not failed). */
conflicts: ChangeId[];
/** Changes that hard-failed (network/permission/etc.). */
failed: ChangeId[];
}

/** A fresh, empty result with independent arrays (no shared prototype aliasing). */
export function emptyExecutionResult(): ExecutionResult {
return { completed: [], conflicts: [], failed: [] };
}
Loading
Loading