Skip to content

Commit ebeb215

Browse files
pajomaclaude
andcommitted
docs: spec+plan for vscode-free domain (#239)
Phase 5 of the #234 restructure. shared/editor adapter, path-returning Reader/Writer, Inject split (pure compute vs editor apply), removal of the 3 remaining cross-feature edges, node-only unit tests. 5 staged steps. Refs #239. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 4b182d4 commit ebeb215

2 files changed

Lines changed: 118 additions & 0 deletions

File tree

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Plan — Issue #239: vscode-free domain + `shared/editor` adapter
2+
3+
> **Spec:** [docs/specs/2026-06-02-239-vscode-free-domain.md](../specs/2026-06-02-239-vscode-free-domain.md)
4+
> **Issue:** [pajoma/vscode-journal#239](https://github.com/pajoma/vscode-journal/issues/239)
5+
> **Branch:** `refactor/239-vscode-free-domain` (off `develop`)
6+
> **Created:** 2026-06-02
7+
8+
## Approach
9+
10+
Five staged steps, each test-green and independently shippable. Edges first (cheap, unblocks reasoning about ownership), then the editor adapter, then domain return-type changes, then tests. Keep `IEditor` on the `JournalController` so commands reach it via the interface (consistent with Phase 2 DI).
11+
12+
## Steps
13+
14+
### Step 1 — Remove cross-feature edges
15+
- **smart-input → entries:** move `Dialogues` to `shared/` (it is a generic UI service, already used broadly) OR move `ScanEntries` to `shared/`. Prefer moving `Dialogues``shared/dialogues/` since it implements `IDialogues` (a shared interface). Re-point `Container`.
16+
- **navigation → entries:** move `AbstractLoadEntryForDateCommand` to `shared/commands/` (or `features/entries` export consumed only via a shared base). Navigation extends the shared base.
17+
- **entries → notes:** in `loadPageForInput`, replace the direct `new LoadNotes(...)` branch with a note-input handler resolved from the `JournalController` (register a `noteLoader` in the container, or move the `NoteInput` branch up into the command-registration layer). Entries no longer imports `notes`.
18+
- Verify: cross-feature grep (the #234 Phase-4 script) returns zero edges; suite green.
19+
20+
### Step 2 — `shared/editor/` adapter
21+
- Add `IEditor` to `shared/model/interfaces.ts`: `open(path)`, `show(doc)`, `save(doc)`, `createAndOpen(path, content)`, `applyEdit(doc, edits)` / `insert(doc, position, text)`.
22+
- Implement `EditorAdapter` in `shared/editor/`, moving the bodies from `Dialogues.openDocument/showDocument/saveDocument`, `Writer.createSaveLoadTextDocument`, and `Inject.injectString/injectInlineString`.
23+
- Add `editor: IEditor` to `JournalController` + `Container`.
24+
- Verify: suite green (behavior identical; just relocated).
25+
26+
### Step 3 — Writer / Reader return data
27+
- `Writer.createEntryForPath/createWeeklyForPath` → return the resolved path + content (or a small `CreatedFile` record); the command/reader calls `editor.createAndOpen`.
28+
- `Reader.loadEntryFor*` → return `{ path, created: boolean }` (plain data, no `vscode`); the command opens via `editor.open`. The `onNotesInjected`/`entryOpened` event still fires from the command layer.
29+
- Drop `import * as vscode` from `reader.ts` / `writer.ts`.
30+
- Verify: `grep "vscode" reader.ts writer.ts` empty; suite green.
31+
32+
### Step 4 — Inject split
33+
- Keep pure: `buildInlineString` (returns `InlineString`), position computation → return a plain `{ line, character }` instead of `vscode.Position`.
34+
- Move apply (`injectString`, `injectInlineString`, `injectInput`'s edit application) to `EditorAdapter`.
35+
- `inject.ts` pure half drops `vscode` import; the editor adapter owns `WorkspaceEdit`.
36+
- Verify: pure half has no `vscode`; suite green.
37+
38+
### Step 5 — Node-only unit tests
39+
- Add `src/test/unit/` (plain node, not Extension Host) covering: path resolution (`PathResolver`), `buildInlineString`, position computation, `inferType`.
40+
- Wire into the test pipeline (separate `node --test` or extend compile-tests + a node runner). Document the command in AGENTS.md.
41+
- Verify: node-only tests pass without a display; full Extension-Host suite still green.
42+
43+
## Test scenarios
44+
45+
| Scenario | Check |
46+
|----------|-------|
47+
| Each step | `npm run check` green |
48+
| Step 1 | cross-feature edge grep → 0; commands-prev-next, commands-note, scan/quickpick tests green |
49+
| Step 2 | open/show/save + create-and-open behavior unchanged (commands-entry real-fs, issue-51 remote) |
50+
| Step 3 | `grep vscode reader.ts writer.ts` empty; entry/weekly create+open tests green |
51+
| Step 4 | `grep vscode` on Inject pure half empty; commands-inject (memo/task insertion) green |
52+
| Step 5 | node-only unit tests pass with no Extension Host |
53+
| Regression | full suite (238+) green after every step |
54+
55+
## Risks
56+
57+
| Risk | Mitigation |
58+
|------|-----------|
59+
| Return-type change ripples to many call sites | Stage 3/4 isolated; `IEditor` on `JournalController` keeps call sites uniform |
60+
| Remote-path edge cases (local-vs-remote open) regress | issue-51 + remote tests cover; keep `getResolvedEntryPathForLocalOpen` flow intact |
61+
| Inject apply semantics (WorkspaceEdit ordering) change | Move bodies verbatim into adapter; commands-inject asserts inserted text/position |
62+
| Moving `Dialogues` to shared churns imports | Mechanical; `Container` is the only constructor site |
63+
| Note-dispatch indirection adds complexity | Keep it minimal (a `noteLoader` callback or command-layer branch), not a generic plugin system |
64+
65+
## Rollback
66+
67+
Each step is a separate PR. Revert the offending PR; earlier steps remain valid. No settings/schema/data migration.
68+
69+
## Reference spec
70+
71+
[docs/specs/2026-06-02-239-vscode-free-domain.md](../specs/2026-06-02-239-vscode-free-domain.md)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Spec: vscode-free domain + `shared/editor` adapter (#239)
2+
3+
> Follow-up to #234 (package-by-feature). Phases 1–4 merged via #235#238.
4+
5+
## Goal
6+
7+
Separate **pure domain logic** from **VS Code editor I/O** so the bulk of `features/*/` can be unit-tested without an Extension Host. Introduce a single `shared/editor/` adapter that owns every `vscode.TextDocument`/`WorkspaceEdit`/`window` interaction; domain code works with paths, strings, and plain data. Also remove the 3 cross-feature import edges left after Phase 4.
8+
9+
## Why now
10+
11+
`Reader`, `Writer`, `Inject` (`src/features/entries/`) all import `vscode` and return `vscode.TextDocument`. Tests therefore need a real Extension Host (`@vscode/test-cli`, ~30 s) instead of fast node-only runs (`node -e`, ~ms). Pulling editor I/O behind one seam makes the create/resolve/format logic directly testable and confines the host dependency to one reviewed module — the highest-leverage testability win of the restructure (maintainer concurred this is its own issue).
12+
13+
## Honest scope note
14+
15+
Full purity is not uniform across the three services:
16+
- **Reader / Writer** — mostly path resolution + create-or-open; cleanly made path-returning.
17+
- **Inject** — deeply editor-bound (`TextDocument`, `Position`, `WorkspaceEdit`, line scanning). The *computation* (which template, what string, what position) is pure; the *application* (apply edit, save) is not. Split accordingly; the applier lives in `shared/editor/`.
18+
19+
This issue does **not** chase 100% `vscode`-free across every file — it isolates the editor surface to `shared/editor/` and makes the pure parts pure and node-testable. Inject's pure half (`buildInlineString`, position computation) becomes testable; its apply half stays in the adapter.
20+
21+
## In scope
22+
23+
1. **`shared/editor/`** — an `EditorAdapter` (interface `IEditor`) owning:
24+
- `open(path)` / `show(doc)` / `save(doc)` (currently `Dialogues.openDocument/showDocument/saveDocument`),
25+
- create-and-open (currently `Writer.createSaveLoadTextDocument`),
26+
- apply-edit / insert-at-position (currently `Inject.injectString`/`injectInlineString`).
27+
2. **Reader / Writer** return resolved paths + a created/loaded flag (plain data); commands call the editor adapter to materialize the `TextDocument`.
28+
3. **Inject** keeps pure `buildInlineString` / `computePositionForInput` (plain `InlineString` + line/col data, no `vscode.Position`); the apply step moves to the adapter.
29+
4. **Remove cross-feature edges:**
30+
- `entries → notes``LoadNotes` dispatch in `loadPageForInput`: route note-input handling so entries does not import `notes` (e.g. an input-kind handler registry, or move dispatch up to the command layer).
31+
- `navigation → entries` — move `AbstractLoadEntryForDateCommand` to a neutral location (e.g. `shared/` or `features/entries` exposing it as public API consumed via interface).
32+
- `smart-input → entries``Dialogues` uses `ScanEntries`; relocate `Dialogues` (and/or `ScanEntries`) so neither feature imports the other.
33+
5. **Node-only unit tests** for the now-pure logic (path resolution, inline-string building, position computation, type inference).
34+
35+
## Out of scope
36+
37+
- No user-facing behavior change. No settings/schema/template changes.
38+
- Caching/`onDidChangeConfiguration` for config (tracked in PLAN.md 2.6).
39+
- Rewriting `Dialogues` QuickPick UX.
40+
41+
## Acceptance criteria
42+
43+
1. `grep -rn "from 'vscode'\|import \* as vscode" src/features/*/` shows `vscode` only in `commands/`, `ui/`, and explicitly editor-facing files — **not** in the pure domain modules (reader/writer + Inject's pure half).
44+
2. All `vscode.TextDocument` / `WorkspaceEdit` / `window` access flows through `shared/editor/`.
45+
3. No cross-feature internal imports remain (grep/lint verifiable): the 3 edges above are gone.
46+
4. New node-only unit tests cover path resolution, `buildInlineString`, position computation, and `inferType` — runnable without the Extension Host.
47+
5. `npm run check` (lint + compile + full suite) green.

0 commit comments

Comments
 (0)