diff --git a/openspec/changes/use-platform-native-graph-paths/.openspec.yaml b/openspec/changes/use-platform-native-graph-paths/.openspec.yaml new file mode 100644 index 0000000..e08b5f8 --- /dev/null +++ b/openspec/changes/use-platform-native-graph-paths/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-03 diff --git a/openspec/changes/use-platform-native-graph-paths/design.md b/openspec/changes/use-platform-native-graph-paths/design.md new file mode 100644 index 0000000..999c4c2 --- /dev/null +++ b/openspec/changes/use-platform-native-graph-paths/design.md @@ -0,0 +1,153 @@ +## Context + +`default_graph_dir()` currently returns `TRAWLER_GRAPH_DIR`, then +`%APPDATA%\trawler\graph`, then relative `trawler-graph`. The last branch makes the +default depend on process cwd and can create real data in a checkout. Resolution happens +before GPUI starts, while storage is opened synchronously inside `TrawlerApp::new()`. + +The selected graph path is already rendered in the titlebar. Startup and devtools failures +use stderr, but successful startup does not state how the path was selected. There is no +general startup-dialog or error-view architecture. `GraphStorage::exists()` currently checks +for `snapshot.loro` but collapses inspection errors into absence, so it is not sufficient for +a conservative compatibility decision. + +The merged Nix preview workflow always sets an isolated `TRAWLER_GRAPH_DIR`; preserving +override precedence keeps that contract independent from default-path selection. Graham chose +compatibility-first legacy handling (open legacy-only with a warning, refuse dual graphs) during +spec shaping; that deliberately supersedes the issue draft's earlier suggestion of a chooser. + +## Goals / Non-Goals + +**Goals:** + +- Give every supported desktop platform one stable native default. +- Never create a default graph relative to cwd. +- Preserve explicit overrides, including intentional relative overrides. +- Keep an existing valid cwd-relative graph accessible without moving or copying data. +- Refuse ambiguous selection when native and legacy graphs both exist. +- Make selection and failures diagnosable and deterministically testable. + +**Non-Goals:** + +- A graph picker, recent-graph registry, or persisted selected-graph preference. +- Automatic graph copy, move, merge, deletion, or format migration. +- Graph locking or changes to `trawler-core` storage semantics. +- A general graphical startup-error framework. +- Protection against a local actor replacing graph paths between selection and storage open; + startup inherits the application's existing local filesystem trust model. + +## Decisions + +### D1 — Resolve platform data roots with `dirs::data_dir` + +The `trawler` app crate will directly depend on the already locked `dirs` 5.0.1 crate. +`dirs::data_dir()` maps to roaming AppData on Windows, Application Support on macOS, +and XDG data home with the standard home fallback on Linux. Appending `trawler/graph` +produces the issue's explicit paths without encoding platform environment rules in Trawler. + +The returned root must be absolute. If it is absent or relative, startup fails with +instructions to set `TRAWLER_GRAPH_DIR`; it never falls back to cwd. Native-root resolution +precedes legacy compatibility because the warning and ambiguity decision require a trustworthy +preferred path. + +Alternatives considered: + +- `directories::ProjectDirs` adds qualifier/organization naming policy and a new lockfile + dependency for no benefit here. +- Manual environment parsing would duplicate native-directory policy and known-folder logic. + +### D2 — Put resolution in an app-local pure seam + +A small `graph_path` module will own path selection. Production code acquires only the inputs +needed by the active branch: an absolute override returns without consulting cwd, native roots, +or graph probes; a relative override acquires cwd; no override acquires cwd and the native root. +The decision core accepts injected providers, returns an absolute selected path plus a source +enum, and is unit-tested without mutating process environment or cwd. + +Default selection remains in `trawler`, while `trawler-core` gains one additive read-only +marker probe so the app does not duplicate storage's private filename and can distinguish +absence from an OS inspection error. + +### D3 — Explicit overrides bypass default and legacy policy + +`std::env::var_os` preserves non-UTF-8 paths. A present non-empty absolute override wins before +cwd, native-root, or legacy acquisition. An explicit relative override requires cwd, is joined +to it without dereferencing symlinks, and then opens the same target through an absolute path. +If cwd cannot be obtained for a relative override or default selection, startup fails closed. +A present empty override is an intentional compatibility break and an actionable error rather +than an accidental cwd selection. + +### D4 — Compatibility-first legacy handling never moves data + +Without an override, the resolver compares recognized graph markers at the native path and +`/trawler-graph`, using a new `GraphStorage::probe_exists()`-style API rather than opening +either graph. The probe uses filesystem metadata, accepts only a regular `snapshot.loro`, returns +`Ok(false)` only for not-found, and propagates permission, traversal, wrong-file-type, and other +inspection errors with the affected path. + +| Native marker | Legacy marker | Result | +|---|---|---| +| absent | absent | select/create native | +| present | absent | select native | +| absent | present | select legacy and warn with both paths | +| present | present | fail as ambiguous and name explicit override choices | + +Selecting legacy is compatibility behavior, not a continuing default: Trawler never creates +the cwd-relative path. A directory named `trawler-graph` without a regular snapshot marker does +not count as legacy data. An existing non-empty native directory without a graph marker is +refused rather than adopted; an empty native directory may be initialized normally. No branch +copies, renames, deletes, opens, or otherwise mutates a graph during resolution. + +If both candidate paths have markers, existing directories are canonicalized for identity only. +Aliases to the same directory select the native spelling and warn instead of reporting two +graphs. Canonicalization or inspection failures fail closed. Non-existing selected paths are +absolutized lexically and are not canonicalized. + +Automatic move was rejected because paths may cross filesystems and interrupted recursive +moves can lose data. Automatic copy was rejected because Trawler has no graph lock and two +writable copies can diverge. A graphical chooser was rejected because it introduces startup +state and overlaps the separately scoped graph-picker feature. + +### D5 — Diagnostics are pre-open stderr plus the existing titlebar + +Before storage opens, successful startup prints the absolute graph path and source +(`override`, `platform default`, or `legacy compatibility`). Legacy warnings and ambiguity +errors name both absolute paths and the `TRAWLER_GRAPH_DIR` escape hatch. The existing +titlebar continues to show the selected path after startup. The underlying `PathBuf` remains +exact for non-UTF-8 values; stderr uses an escaped/debug representation while the existing +human-facing titlebar may be lossy. + +Startup orchestration will expose an injected diagnostic sink and storage-launch callback so +tests can assert `probe → diagnostic → open/create` ordering and prove that error branches never +start GPUI or storage. This is not a new user-facing CLI. + +Storage error handling beyond path selection remains out of scope; this change only ensures +resolution errors fail cleanly before GPUI and that the attempted path is known. + +## Risks / Trade-offs + +- **A legacy-only launch remains cwd-dependent** → only a pre-existing valid graph can take + this branch; startup warns every time and documentation explains manual migration. +- **Warnings may be hidden for graphical desktop launches** → the titlebar still exposes the + selected legacy path after launch; a future graph-picker/migration UI can improve this. +- **Two existing graphs stop startup** → refusing is safer than silently opening the wrong + graph, and the error names two explicit commands/paths. +- **Native-directory behavior is delegated to a dependency** → pin the direct dependency, + require an absolute result, use platform-gated smoke assertions, and test Trawler's suffix, + precedence, and decision matrix with injected roots. +- **Relative overrides become absolute internally** → their target remains identical for the + launch, while later cwd changes cannot make displayed/storage paths ambiguous. + +## Migration Plan + +No data is migrated automatically. New installations create the native graph. Existing +legacy-only installations continue opening their legacy graph with guidance to close Trawler, +make a backup, copy or move into the native location, verify the destination with an explicit +`TRAWLER_GRAPH_DIR`, and then rename/archive the cwd directory so it is no longer detected—or +continue using an explicit override. If both paths contain graph markers, the user selects one +explicitly before launching. Rolling back the binary restores the old resolver and does not +require data changes. + +## Open Questions + +None. A graphical graph picker and migration flow remain a separate future issue. diff --git a/openspec/changes/use-platform-native-graph-paths/proposal.md b/openspec/changes/use-platform-native-graph-paths/proposal.md new file mode 100644 index 0000000..0b8d488 --- /dev/null +++ b/openspec/changes/use-platform-native-graph-paths/proposal.md @@ -0,0 +1,48 @@ +## Why + +On macOS and Linux, a launch without `TRAWLER_GRAPH_DIR` currently falls back to +the relative directory `./trawler-graph`. That can create user data inside a source +checkout, and launching from another working directory can silently select a different +graph. Trawler needs one stable platform-native default while preserving a recognized +legacy graph in the current launch directory. + +## What Changes + +- Preserve a non-empty `TRAWLER_GRAPH_DIR` as the highest-precedence explicit override. +- **BREAKING**: Reject a present empty `TRAWLER_GRAPH_DIR`; it currently resolves to cwd. +- Otherwise resolve the default under the platform's native per-user data directory: + roaming AppData (normally `%APPDATA%`) under `trawler\graph` on Windows, + `~/Library/Application Support/trawler/graph` on macOS, and + `$XDG_DATA_HOME/trawler/graph` (or `~/.local/share/trawler/graph`) on Linux. +- Never create a new default graph relative to the process working directory. +- Keep opening a recognized legacy `./trawler-graph` in the current launch directory + when it is the only existing graph, + but emit an actionable warning naming both the legacy and native paths. +- Refuse to choose silently when valid legacy and native graphs both exist; direct the + user to select one explicitly with `TRAWLER_GRAPH_DIR`. +- Report the selected absolute graph path and its source before opening storage, while + retaining the existing titlebar path display. +- Add deterministic path-resolution tests and document platform defaults, legacy + behavior, explicit overrides, and manual migration. + +## Capabilities + +### New Capabilities + +- `graph-location`: Selection, compatibility handling, diagnostics, and documentation + for Trawler's active graph directory. + +### Modified Capabilities + +None. Development preview isolation continues to use an explicit scratch +`TRAWLER_GRAPH_DIR` under the existing `nix-development-environment` contract. + +## Impact + +- `crates/trawler`: a small app-local graph-location resolver and startup integration. +- `trawler-core`: an additive, read-only graph-marker probe that distinguishes absence + from inspection failure without opening storage. +- Direct dependency on the already locked `dirs` crate for native user data directories. +- README data-location and migration guidance. +- No graph-format change, automatic data movement, graph-picker UI, or change to + graph persistence semantics. diff --git a/openspec/changes/use-platform-native-graph-paths/specs/graph-location/spec.md b/openspec/changes/use-platform-native-graph-paths/specs/graph-location/spec.md new file mode 100644 index 0000000..9bd9230 --- /dev/null +++ b/openspec/changes/use-platform-native-graph-paths/specs/graph-location/spec.md @@ -0,0 +1,127 @@ +## ADDED Requirements + +### Requirement: Explicit graph override has highest precedence +Trawler SHALL use a present, non-empty `TRAWLER_GRAPH_DIR` instead of platform-default or +legacy graph selection. It SHALL preserve arbitrary OS-native path values, resolve an explicit +relative value against the launch working directory without dereferencing symlinks, and reject a +present empty value with an actionable error before opening or creating storage. + +#### Scenario: Absolute override wins +- **WHEN** `TRAWLER_GRAPH_DIR` names an absolute path and native or legacy graphs also exist +- **THEN** Trawler selects the override without acquiring cwd or the native data root +- **AND** does not inspect default graph candidates + +#### Scenario: Relative override is intentional +- **WHEN** `TRAWLER_GRAPH_DIR` contains a non-empty relative path +- **THEN** Trawler selects its absolute equivalent under the launch working directory +- **AND** does not treat it as an accidental default + +#### Scenario: Relative override requires cwd +- **WHEN** `TRAWLER_GRAPH_DIR` is relative and the launch working directory cannot be obtained +- **THEN** Trawler exits before graph storage starts +- **AND** reports that an absolute override can be used instead + +#### Scenario: Empty override is refused +- **WHEN** `TRAWLER_GRAPH_DIR` is present but empty +- **THEN** Trawler exits before GPUI or graph storage starts +- **AND** reports that the override must be removed or set to a non-empty path + +### Requirement: Default graph uses the platform data directory +Without an explicit override or selected legacy compatibility graph, Trawler SHALL use +an absolute `/trawler/graph`: roaming AppData on Windows, Application Support on +macOS, and XDG data home with the standard home fallback on Linux. Trawler MUST NOT create a +default graph relative to the process working directory. + +#### Scenario: First launch creates the native default +- **WHEN** no override, native graph, or valid legacy graph exists +- **THEN** Trawler selects the platform-native `trawler/graph` path for creation +- **AND** leaves `/trawler-graph` absent + +#### Scenario: Existing native graph opens normally +- **WHEN** no override is present and the native default contains a valid graph while no valid legacy graph exists +- **THEN** Trawler selects the native graph + +#### Scenario: Native data root is unavailable or relative +- **WHEN** no override is present and the operating system cannot provide an absolute native data directory +- **THEN** Trawler exits before graph storage starts +- **AND** instructs the user to set `TRAWLER_GRAPH_DIR` +- **AND** does not select a legacy graph as a substitute + +#### Scenario: Default selection requires cwd +- **WHEN** no override is present and the launch working directory cannot be obtained for legacy detection +- **THEN** Trawler exits before inspecting or opening graph storage +- **AND** instructs the user to set an absolute `TRAWLER_GRAPH_DIR` + +#### Scenario: Occupied native directory is not adopted +- **WHEN** the native graph directory is non-empty but has no recognized graph marker +- **THEN** Trawler exits before creating or modifying files there +- **AND** identifies the occupied path + +### Requirement: Legacy cwd graphs remain accessible without automatic migration +Without an explicit override, Trawler SHALL recognize a graph candidate only when its +`snapshot.loro` marker is a regular file. Candidate inspection MUST distinguish not-found from +filesystem errors and MUST fail closed on permission, traversal, wrong-file-type, canonicalization, +or other inspection failures. Resolution MUST NOT copy, move, delete, open, or otherwise mutate +either candidate while deciding which path to select. + +#### Scenario: Legacy-only graph remains accessible +- **WHEN** a recognized legacy graph exists and no recognized native graph exists +- **THEN** Trawler selects the legacy graph +- **AND** warns that compatibility behavior was used +- **AND** names both the legacy path and the preferred native path + +#### Scenario: Native and legacy graphs are ambiguous +- **WHEN** recognized graphs exist at both native and legacy paths and resolve to different existing directories +- **THEN** Trawler exits before opening either graph +- **AND** reports both absolute paths +- **AND** instructs the user to select one explicitly with `TRAWLER_GRAPH_DIR` + +#### Scenario: Aliased candidates are one graph +- **WHEN** native and legacy paths both have graph markers and canonicalize to the same directory +- **THEN** Trawler selects the native path spelling +- **AND** warns that the legacy path aliases the selected graph + +#### Scenario: Merely named directory is not legacy data +- **WHEN** `/trawler-graph` exists without a regular graph marker +- **THEN** Trawler does not select or mutate it as a legacy graph +- **AND** selects the native default according to ordinary rules + +#### Scenario: Candidate inspection fails closed +- **WHEN** Trawler cannot inspect or canonicalize a candidate needed for selection +- **THEN** it exits before opening or creating either graph +- **AND** reports the affected path and operating-system error + +### Requirement: Active graph selection is diagnosable +Trawler SHALL report the selected absolute graph path and resolution source before opening +storage, and SHALL continue to show the selected path in the application titlebar after +startup. Selection failures MUST identify the relevant candidate paths and corrective action. +The selected `PathBuf` MUST preserve non-UTF-8 values exactly even if a human-facing display is +lossy; startup diagnostics SHALL use an escaped representation that distinguishes underlying bytes. + +#### Scenario: Successful startup reports source +- **WHEN** Trawler selects an override, platform default, or legacy compatibility graph +- **THEN** startup diagnostics state the absolute path and corresponding source + +#### Scenario: Ambiguity is actionable +- **WHEN** path selection refuses an ambiguous native-plus-legacy state +- **THEN** diagnostics provide enough path and override information to relaunch without guessing + +#### Scenario: Diagnostics precede storage access +- **WHEN** startup resolves a graph path successfully +- **THEN** it emits the selected path and source before opening or creating graph storage +- **AND** any resolution error exits before GPUI or graph storage starts + +### Requirement: Graph-location behavior is documented and regression-tested +The repository SHALL document platform-native paths, override precedence, legacy warnings, +ambiguous-state recovery, and manual migration. Deterministic tests MUST cover the resolver's +precedence and lazy acquisition, native suffix and absolute-root validation, empty/missing inputs, +candidate probe errors and aliases, the legacy decision matrix, startup ordering, and diagnostic +source without mutating shared process environment or cwd. + +#### Scenario: Contributor verifies path policy +- **WHEN** the graph-location test suite runs in parallel with other tests +- **THEN** it exercises synthetic resolver inputs without changing global environment variables or working directory + +#### Scenario: User follows migration documentation +- **WHEN** a user has a legacy cwd graph or both legacy and native graphs +- **THEN** the documentation explains how to close Trawler, back up, move or copy, verify via an explicit override, retire the detected legacy marker, and select a graph without ambiguity diff --git a/openspec/changes/use-platform-native-graph-paths/tasks.md b/openspec/changes/use-platform-native-graph-paths/tasks.md new file mode 100644 index 0000000..b2e1f9a --- /dev/null +++ b/openspec/changes/use-platform-native-graph-paths/tasks.md @@ -0,0 +1,30 @@ +## 1. Graph marker and location resolver + +- [ ] 1.1 Add an additive `trawler-core` read-only graph-marker probe that accepts only a regular `snapshot.loro`, distinguishes not-found from inspection errors, and never opens or mutates storage. +- [ ] 1.2 Add the direct pinned `dirs` dependency and an app-local `graph_path` module with typed selection sources, candidate states, and actionable errors. +- [ ] 1.3 Implement lazy override/cwd/native-root providers, lexical relative-path absolutization, absolute native `trawler/graph` composition, occupied-native refusal, canonical identity for existing aliases, and the complete native/legacy decision matrix. +- [ ] 1.4 Wire production `var_os`, `current_dir`, `dirs::data_dir`, and marker-probe inputs into the resolver while preserving non-UTF-8 `PathBuf` values end to end. + +## 2. Startup and diagnostics + +- [ ] 2.1 Replace `default_graph_dir()` with fail-cleanly pre-GPUI resolution and a testable orchestration seam that emits the selected escaped path plus `override`, `platform default`, or `legacy compatibility` source before storage opens. +- [ ] 2.2 Emit actionable legacy-only warnings and native-plus-legacy ambiguity errors naming both paths and the `TRAWLER_GRAPH_DIR` recovery path. +- [ ] 2.3 Preserve the existing titlebar path display and verify that explicit scratch/devtools launches bypass native and legacy default selection. + +## 3. Deterministic tests + +- [ ] 3.1 Test that absolute overrides call no cwd/native/probe providers; test relative override cwd failure, non-UTF-8 path preservation where supported, empty override refusal, absolute native-root validation, and native suffix composition through injected inputs. +- [ ] 3.2 Test every native/legacy marker combination, occupied native directories, non-regular markers, probe/canonicalization failures, and same-directory aliases, asserting selected source or actionable failure without changing shared environment or cwd. +- [ ] 3.3 Use spy probes, diagnostic sinks, and storage-launch callbacks to verify non-mutation, branch laziness, escaped diagnostics, and `probe → diagnostic → open/create` ordering; error branches MUST never invoke storage launch. +- [ ] 3.4 Add platform-gated assertions that the production native data root is absolute and maps to the documented OS family, without attempting to mock another OS's known-folder implementation. + +## 4. Documentation + +- [ ] 4.1 Update README data-location guidance with Windows, macOS, and Linux defaults; the empty-override compatibility break; legacy warnings; ambiguous-state recovery; and the complete close → back up → move/copy → explicitly verify → retire legacy marker migration sequence. +- [ ] 4.2 Cross-reference the isolated Nix preview workflow so development scratch graphs remain clearly separate from user-default graph policy. + +## 5. Verification + +- [ ] 5.1 Run formatting, full-workspace check, clippy with warnings denied, and the non-ignored workspace tests inside the pinned Nix development shell. +- [ ] 5.2 Run the isolated preview smoke path and assert its manifest/application log use the helper-owned explicit scratch graph rather than the production native path. +- [ ] 5.3 Validate the OpenSpec change strictly and review the final diff against Tangled issue #5 with no graph-picker, automatic migration, or graph-format scope creep.