Summary
Restructure src/ from package-by-layer (commands/, journal/, model/, vscode/, features/, ui/, util/) to package-by-feature with a thin shared kernel. Goal: one feature lives in one place, clear separation of concerns, real dependency injection, and a vscode-free domain layer. No user-facing behavior change.
Aligns with docs/PLAN.md Phase 2+ (DI, named imports, native async/await, vscode.workspace.fs).
📄 Full spec: docs/specs/2026-06-02-234-package-by-feature.md · Plan: docs/plans/2026-06-02-234-package-by-feature.md
Problem
A single feature is smeared across many directories. "Notes" lives in commands/show-note.ts, journal/parser.ts, journal/writer.ts, features/entries/load-note.ts, features/sync/sync-note-links.ts, and vscode/conf.ts — six directories for one capability.
Concrete smells:
J namespace barrel = circular god-import. src/index.ts re-exports every submodule as J.Util, J.Journal, etc. Files do import * as J from '..' then J.Util.Ctrl. Those files are themselves re-exported by index.ts, so index.ts → commands/show-note.ts → index.ts is a cycle. Kills tree-shaking, breaks go-to-definition, hides dependencies.
Ctrl service-locator god-object (util/controller.ts). Every command/provider receives the full Ctrl and reaches ctrl.reader/ctrl.writer/ctrl.ui/ctrl.config/ctrl.inject. Narrow interfaces (IReader, …) exist in model/interfaces.ts but are unused at call sites. Two-phase init forces ! assertions on every field. Controller also lives in util/ — wrong home.
- Domain coupled to
vscode. Reader/Writer/Inject import vscode and return vscode.TextDocument, so core logic can't be unit-tested without the Extension Host.
Configuration god-class — 708 lines (vscode/conf.ts), ~40 public methods mixing raw setting reads, path-pattern resolution, template lookup, scope resolution, windows-path normalization, and local-vs-remote variants.
- Inconsistent buckets. No rule separates
journal/ vs features/ vs ui/. scan-entries is a "feature" but reader is "journal"; codeactions is in ui but sync is in features; two template modules split across layers.
- Orchestration leaks into commands.
show-entry-for-date.ts loadPageForInput runs fire-and-forget weekly sync via nested .then/.catch.
- Mixed async styles —
.then() chains alongside async/await.
Proposed target structure
src/
extension.ts # activate/deactivate only
app/ # composition root (was vscode/startup + util/controller)
container.ts # builds the dependency graph — replaces Ctrl service-locator
register.ts # command/provider registration
startup.ts # syntax highlighting, cache-invalidation wiring
shared/ # kernel — NO feature dependencies
config/ # split Configuration: SettingsReader | PathResolver | TemplateProvider
fs/ # IFileSystem + VscodeFileSystem
logging/ dates/ strings/
templates/ # merge template-engine + template-service
editor/ # the single vscode-TextDocument adapter (open/show/save)
events/ # typed EventEmitter / mediator for cross-feature signals
model/ # shared types: Input, FileEntry, ScopedTemplate
features/
entries/ # today | tomorrow | yesterday | date | input + entry reader/writer
notes/ # show-note + load-note + note-path + sync-note-links
weekly/ # weekly entry + watcher + sync-daily-links
tasks/ # copy/shift + codeactions + migrate codelens
navigation/ # prev/next entry
smart-input/ # match-input + parser + input dialog
tools/ # print-time | print-duration | print-sum
Each feature folder: commands/ (VS Code handlers) · domain/ (pure logic, no vscode) · ui/ (providers).
Separation rules
- A feature may import from
shared/. A feature must never import another feature's internals.
- Cross-feature interaction goes through
shared/events/ — a small typed EventEmitter/mediator. Example: entries emits entryOpened → weekly subscribes and runs daily-link sync. No feature-to-feature imports.
domain/ imports no vscode; it returns data/paths. commands/ plus the shared/editor/ adapter handle TextDocument.
- Inject narrow interfaces (
IReader, not Ctrl) via constructor. app/container.ts builds the graph once.
Service lifecycle
app/container.ts is the single owner of service lifecycles. All services are singletons (one instance per activation), matching the current single-Ctrl model. No separate shared/state/ module unless genuinely mutable cross-feature state appears — out of scope here.
Migration phases (each keeps the suite green)
- Kill the
J barrel → named imports. Mechanical, large diff, zero behavior change. Unblocks everything else.
Ctrl → DI container in app/. Commands take narrow interfaces. Remove two-phase ! init.
- Split
Configuration into SettingsReader / PathResolver / TemplateProvider.
- Move files into feature folders (
git mv + fix imports). Rewire onNotesInjected + inline weekly-sync onto shared/events/.
- Extract
shared/editor/ adapter → vscode-free domain/. Highest payoff, highest effort — may split to a follow-up issue.
Preconditions
Scope / non-goals
- No user-facing behavior change. No new settings, commands, or templates.
- No change to the
journal.* settings schema or file/path patterns in package.json.
- i18n strings unchanged (English-only).
- No new mutable-state module unless a concrete need surfaces.
Acceptance criteria
Summary
Restructure
src/from package-by-layer (commands/,journal/,model/,vscode/,features/,ui/,util/) to package-by-feature with a thin shared kernel. Goal: one feature lives in one place, clear separation of concerns, real dependency injection, and a vscode-free domain layer. No user-facing behavior change.Aligns with
docs/PLAN.mdPhase 2+ (DI, named imports, native async/await,vscode.workspace.fs).📄 Full spec:
docs/specs/2026-06-02-234-package-by-feature.md· Plan:docs/plans/2026-06-02-234-package-by-feature.mdProblem
A single feature is smeared across many directories. "Notes" lives in
commands/show-note.ts,journal/parser.ts,journal/writer.ts,features/entries/load-note.ts,features/sync/sync-note-links.ts, andvscode/conf.ts— six directories for one capability.Concrete smells:
Jnamespace barrel = circular god-import.src/index.tsre-exports every submodule asJ.Util,J.Journal, etc. Files doimport * as J from '..'thenJ.Util.Ctrl. Those files are themselves re-exported byindex.ts, soindex.ts → commands/show-note.ts → index.tsis a cycle. Kills tree-shaking, breaks go-to-definition, hides dependencies.Ctrlservice-locator god-object (util/controller.ts). Every command/provider receives the fullCtrland reachesctrl.reader/ctrl.writer/ctrl.ui/ctrl.config/ctrl.inject. Narrow interfaces (IReader, …) exist inmodel/interfaces.tsbut are unused at call sites. Two-phase init forces!assertions on every field. Controller also lives inutil/— wrong home.vscode.Reader/Writer/Injectimportvscodeand returnvscode.TextDocument, so core logic can't be unit-tested without the Extension Host.Configurationgod-class — 708 lines (vscode/conf.ts), ~40 public methods mixing raw setting reads, path-pattern resolution, template lookup, scope resolution, windows-path normalization, and local-vs-remote variants.journal/vsfeatures/vsui/.scan-entriesis a "feature" butreaderis "journal";codeactionsis inuibutsyncis infeatures; two template modules split across layers.show-entry-for-date.tsloadPageForInputruns fire-and-forget weekly sync via nested.then/.catch..then()chains alongsideasync/await.Proposed target structure
Each feature folder:
commands/(VS Code handlers) ·domain/(pure logic, no vscode) ·ui/(providers).Separation rules
shared/. A feature must never import another feature's internals.shared/events/— a small typedEventEmitter/mediator. Example:entriesemitsentryOpened→weeklysubscribes and runs daily-link sync. No feature-to-feature imports.domain/imports novscode; it returns data/paths.commands/plus theshared/editor/adapter handleTextDocument.IReader, notCtrl) via constructor.app/container.tsbuilds the graph once.Service lifecycle
app/container.tsis the single owner of service lifecycles. All services are singletons (one instance per activation), matching the current single-Ctrlmodel. No separateshared/state/module unless genuinely mutable cross-feature state appears — out of scope here.Migration phases (each keeps the suite green)
Jbarrel → named imports. Mechanical, large diff, zero behavior change. Unblocks everything else.Ctrl→ DI container inapp/. Commands take narrow interfaces. Remove two-phase!init.Configurationinto SettingsReader / PathResolver / TemplateProvider.git mv+ fix imports). RewireonNotesInjected+ inline weekly-sync ontoshared/events/.shared/editor/adapter → vscode-freedomain/. Highest payoff, highest effort — may split to a follow-up issue.Preconditions
Scope / non-goals
journal.*settings schema or file/path patterns inpackage.json.Acceptance criteria
npm run checkgreen after every phase.import * as J fromremaining;src/index.tsbarrel removed or reduced to type-only re-exports.Ctrl; all take narrow interfaces wired inapp/container.ts.Configurationsplit into ≤3 focused classes, none over ~300 lines.shared/events/.docs/PLAN.mdupdated to reflect the new structure.