Skip to content

Restructure src/ to package-by-feature with shared kernel + DI #234

Description

@pajoma

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. Orchestration leaks into commands. show-entry-for-date.ts loadPageForInput runs fire-and-forget weekly sync via nested .then/.catch.
  7. 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 entryOpenedweekly 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)

  1. Kill the J barrel → named imports. Mechanical, large diff, zero behavior change. Unblocks everything else.
  2. Ctrl → DI container in app/. Commands take narrow interfaces. Remove two-phase ! init.
  3. Split Configuration into SettingsReader / PathResolver / TemplateProvider.
  4. Move files into feature folders (git mv + fix imports). Rewire onNotesInjected + inline weekly-sync onto shared/events/.
  5. 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

  • npm run check green after every phase.
  • No import * as J from remaining; src/index.ts barrel removed or reduced to type-only re-exports.
  • No command/provider depends on concrete Ctrl; all take narrow interfaces wired in app/container.ts.
  • Configuration split into ≤3 focused classes, none over ~300 lines.
  • Each feature directory self-contained; no cross-feature internal imports (grep/lint verifiable).
  • Cross-feature signals flow through shared/events/.
  • docs/PLAN.md updated to reflect the new structure.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions