handlers

package
v0.16.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 16, 2026 License: MIT Imports: 27 Imported by: 0

Documentation

Overview

Package handlers is the only place framework Go code is allowed to have side effects. Handlers are the write side of CQRS: they accept a domain command, do any validation against loaded state, perform IO (git, disk, pre-flight via a finder dependency), and return only errors. Results flow back through callbacks defined on the command struct.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Brancher

type Brancher interface {
	Checkout(branch string, create bool) error
	BranchMerged(branch string) bool
	DeleteBranch(branch string, force bool) error
}

Brancher performs git branch operations. Injected so tests can fake checkout/branch deletion without touching real git.

type Committer

type Committer interface {
	Commit(message string, paths ...string) error
}

Committer performs a git commit of the given paths with the given message. Injected so tests can record or no-op commits.

type Handler

type Handler struct {
	// contains filtered or unexported fields
}

Handler holds injected dependencies shared across command methods. Each public method corresponds to one command and lives in its own file (handler_new_entry.go, etc.).

func New

func New(opts Options) *Handler

New constructs a Handler with the given options.

func (*Handler) BuildConnectedIndexes added in v0.15.0

func (h *Handler) BuildConnectedIndexes(ctx context.Context, repoIDs []string, embedder llm.Embedder, fill *command.BuildConnectedIndexesCmd) error

BuildConnectedIndexes freshens the given connected repos' caches and fills each member's index under the shared embedder, excluding embedded entries. Entries indexed under a different fingerprint re-embed here rather than excluding the repo, so one vector space holds across every selected index. It is the shared spine for eager pre-indexing (`sdd index --repo`) and the fill half of a cross-repo search's prepare step. fill's callbacks report progress per repo and aggregate across them; a nil fill runs quiet. When fill.Force is set (only `sdd index --repo/--all-repos --force`), each member store is fully rebuilt rather than lazily reconciled, repairing stale or corrupt connected indexes.

func (*Handler) BumpMinimumVersion added in v0.5.1

func (h *Handler) BumpMinimumVersion(ctx context.Context, cmd *command.BumpMinimumVersionCmd) error

BumpMinimumVersion raises .sdd/meta.json's minimum_version field to the running binary's version when the binary is strictly higher (or no minimum was recorded). Equal versions are a no-op so repeat invocations stay quiet.

Dev builds are rejected with a fixed message — only released binaries may raise the floor, otherwise a developer's local pinning could lock every other contributor out of the graph until that exact dev build is available.

func (*Handler) ConfigSet added in v0.15.0

func (h *Handler) ConfigSet(ctx context.Context, cmd *command.ConfigSetCmd) error

ConfigSet writes one key to the chosen config layer with a comment-preserving upsert. The patched document is validated against the layer's schema before anything lands on disk, so a key that does not belong in that file fails loud and leaves the file untouched. The global file is created (with its parent directory) on first write.

func (*Handler) EnsureReposFresh added in v0.15.0

func (h *Handler) EnsureReposFresh(ctx context.Context, repoIDs []string) (changed bool, err error)

EnsureReposFresh brings the caches of the given connected repos up to date for a read: lazy clone when absent, cooldown-gated pull otherwise. Unconnected repo IDs are skipped — the read side renders them unresolved, which is that state's honest surface. Reports whether any cache changed so a long-lived caller can invalidate its GraphSource.

func (*Handler) FinishWIP

func (h *Handler) FinishWIP(ctx context.Context, cmd *command.FinishWIPCmd) error

FinishWIP removes the WIP marker identified by cmd.MarkerID, commits the removal, and (if the marker referenced a branch) deletes the branch when merged or when --force is set. Unmerged non-force branches are preserved and OnBranchPreserved fires so the CLI can print guidance.

func (*Handler) Init

func (h *Handler) Init(ctx context.Context, cmd *command.InitCmd) error

Init executes an InitCmd. The operation is idempotent:

  • On an empty tree, it creates .sdd/, writes config.yaml and meta.json, creates the graph dir, updates .gitignore, and installs the embedded skill bundle.
  • On an existing tree, it leaves config.yaml and meta.json alone, ensures the expected directories are in place, and runs the skill install pass to refresh whatever's drifted (user-modified files are routed through cmd.PromptOverwrite).

The skill install step and the meta-write step are implemented as nested commands (h.InstallSkills and h.WriteSchemaMeta), keeping each side effect in its own handler method.

func (*Handler) InstallSkills added in v0.2.0

func (h *Handler) InstallSkills(ctx context.Context, cmd *command.InstallSkillsCmd) error

InstallSkills writes the embedded skill bundle to the target agent's skill directory. Side effects are limited to filesystem writes — the skill status classification itself comes from the injected reader (SkillStatus query).

func (*Handler) LintFix

func (h *Handler) LintFix(ctx context.Context, cmd *command.LintFixCmd) error

LintFix applies mechanical fixes to graph entries: loads the graph, identifies fixable issues, patches files in place, and commits.

func (*Handler) NewEntry

func (h *Handler) NewEntry(ctx context.Context, cmd *command.NewEntryCmd) (retErr error)

NewEntry executes a NewEntryCmd: validates, loads the graph, runs pre-flight, writes the entry + attachments, commits, and fires cmd.OnNewEntry with the new ID. Stdin attachments are persisted to .sdd-tmp/ on pre-flight rejection and on every --dry-run invocation so the user can iterate without re-piping heredocs.

Returns only errors. On dry-run success, returns nil without invoking the callback (no entry was created).

func (*Handler) PrepareCrossRepoSearch added in v0.15.0

func (h *Handler) PrepareCrossRepoSearch(ctx context.Context, q query.SearchQuery, embedder llm.Embedder, fill *command.BuildConnectedIndexesCmd) error

PrepareCrossRepoSearch runs the side-effect half of a cross-graph search, mirroring how the local lazy-fill precedes the search finder: resolve the query's repo selection, bring those caches up to date, and — when vector mode is in play — lazy-fill each repo's index with the shared embedder. The finder read (finders.MultiSearch) then runs pure. fill is optional: the CLI passes progress callbacks so the member builds render through the output coordinator; the MCP server passes nil (non-TTY slog).

func (*Handler) RepoAdd added in v0.15.0

func (h *Handler) RepoAdd(ctx context.Context, cmd *command.RepoAddCmd) error

RepoAdd executes a RepoAddCmd with its two writes: clone the target, establish its canonical identity from the repo_id it declares in its committed config, register the connection in the user-global config (the per-user resolution), and declare the dependency in the current repo's committed .sdd/config.yaml (the portable record of what this graph needs). When the clone URL itself derives an identity (ssh/https forms), the declared value must match it — that cross-check catches pointing at a fork or mirror under the wrong name. A URL with no host/path form (a local path, an offline mirror) skips the cross-check and trusts the declared identity, which is canonical by design. The clone runs before registration so a failed verification leaves the config untouched. Re-running against an already-connected URL skips the clone and only ensures the declaration — the upgrade path for connections made before dependencies existed.

func (*Handler) RepoRemove added in v0.15.0

func (h *Handler) RepoRemove(ctx context.Context, cmd *command.RepoRemoveCmd) error

RepoRemove executes a RepoRemoveCmd: it drops repoID from the committed dependencies of the current repo's .sdd/config.yaml and commits that change. Project-scoped and reference-safety-guarded — it refuses when any local entry still holds a cross-repo ref into the target (dropping the declaration would strand those refs and retroactively break resolve-or-block), unless Force is set, in which case the stranded refs are named through OnStranded before the removal proceeds. The per-user global connection and its cache are left untouched — machine-level teardown is a separate command surface, out of scope here.

func (*Handler) RepoSync added in v0.15.0

func (h *Handler) RepoSync(ctx context.Context, cmd *command.RepoSyncCmd) error

RepoSync executes a RepoSyncCmd: force-pull the named connected repos' caches (all of them when none are named), cloning lazily where absent.

func (*Handler) RewriteEntry added in v0.2.0

func (h *Handler) RewriteEntry(ctx context.Context, cmd *command.RewriteEntryCmd) error

RewriteEntry executes a RewriteEntryCmd: resolves the target entry, computes its new ID from the requested type, walks the graph for inbound references, renames the file (and attachment directory) when the type changes, rewrites frontmatter on the target and every inbound entry, and commits atomically. Bypasses pre-flight per the plan: rewrite is a mechanical operation, not a capture.

DryRun returns without touching disk after reporting intended changes via the OnRewritten callback. NoCommit writes to disk but skips the git commit.

func (*Handler) StartWIP

func (h *Handler) StartWIP(ctx context.Context, cmd *command.StartWIPCmd) error

StartWIP creates a WIP marker for cmd.EntryID, commits it, and optionally creates + checks out a derived git branch. Validates that the entry exists in the graph and warns (via OnExclusiveCollision) when another exclusive marker already covers the entry.

func (*Handler) Summarize

func (h *Handler) Summarize(ctx context.Context, cmd *command.SummarizeCmd) error

Summarize executes a SummarizeCmd: loads the graph, determines which entries to (re)generate, generates them via the LLM, and writes updated frontmatter back to disk. Summaries are derived on demand with no staleness tracking (d-cpt-4qi): named entries always regenerate, while --all fills only entries that have no summary yet (--force regenerates every entry). Batch runs use an errgroup with SetLimit(concurrency) — for remote providers the factory applies a rate limiter on top. A short-lived mutex guards graph reads and writes; the LLM call itself runs without the lock.

When cmd.ExplicitText is set, the LLM is bypassed entirely: the supplied text is written as the summary on a single named entry.

func (*Handler) SyncPull added in v0.8.0

func (h *Handler) SyncPull(ctx context.Context, cmd *command.SyncPullCmd) error

SyncPull pulls the shared graph from upstream using a merge (never a rebase), so background sync never rewrites the shared history. It refuses on a dirty working tree and defers to the user with an actionable message; on a clean tree it runs the merge pull and reports git's output through the command's OnPulled callback.

func (*Handler) WriteSchemaMeta added in v0.2.0

func (h *Handler) WriteSchemaMeta(ctx context.Context, cmd *command.WriteSchemaMetaCmd) error

WriteSchemaMeta creates .sdd/meta.json when it does not yet exist. Existing files are preserved as-is — both graph_schema_version and minimum_version have write-once semantics in the init flow (schema bumps are migration concerns, minimum_version is set at graph creation time and preserved thereafter).

type IndexHandler added in v0.4.0

type IndexHandler struct {
	// contains filtered or unexported fields
}

IndexHandler is the side-effecting code path for building and lazily filling the search index. Per d-tac-lqr it owns chunking, embedding, and upserting; the SearchFinder is pure-read and consults the index via index.Index directly.

Two operations:

  • Build (sdd index): full warm-up over every entry on disk. Skips entries whose manifest record is up-to-date unless Force is set.

  • LazyFill (sdd search prelude): reconciles the manifest against entries on disk — re-embeds entries that are missing, or whose content hash / embedder fingerprint differs from the stored state.

func NewIndexHandler added in v0.4.0

func NewIndexHandler(opts IndexHandlerOptions) *IndexHandler

NewIndexHandler constructs an IndexHandler with the given dependencies.

func (*IndexHandler) Build added in v0.4.0

func (h *IndexHandler) Build(ctx context.Context, cmd *command.BuildIndexCmd) error

Build is the sdd-index warm-up. It loads the graph, derives chunks for every entry (skipping unchanged ones unless cmd.Force), embeds in batches across entries (one Embed call per outer batch), and upserts per-entry into the index. The manifest is saved after every batch — a crash mid-build leaves a partially-populated index that lazy-fill can finish later.

func (*IndexHandler) LazyFill added in v0.4.0

func (h *IndexHandler) LazyFill(ctx context.Context, cmd *command.LazyFillIndexCmd) error

LazyFill is the sdd-search prelude — only entries missing from the manifest, or whose hash/fingerprint differs from current, are re-embedded.

type IndexHandlerOptions added in v0.4.0

type IndexHandlerOptions struct {
	GraphDir string
	IndexDir string
	Embedder llm.Embedder
	Splitter *textsplitter.Splitter
	Reader   Reader
	Now      func() time.Time
	Stderr   io.Writer
	// ExcludeEmbedded skips binary-scoped entries — set for connected-repo
	// cache indexes so embedded entries index only locally.
	ExcludeEmbedded bool
}

IndexHandlerOptions configures NewIndexHandler. Required fields are GraphDir, IndexDir, Embedder, Reader. Splitter defaults to textsplitter.NewSplitter() with default options when nil; Now defaults to time.Now. The store is loaded under the exclusive lock at write time (see index.WriteStore), so no pre-opened index is injected.

type LegacySessionMigrator added in v0.16.1

type LegacySessionMigrator interface {
	ListLegacySessions(context.Context) ([]string, error)
	MigrateLegacySession(context.Context, string) error
}

LegacySessionMigrator is the local maintenance seam used by init. Runtime session reads never call it: conversion requires an explicit user gate.

type Mover added in v0.2.0

type Mover interface {
	Move(src, dst string) error
}

Mover renames a path in the working tree and the git index as one operation — the semantics of `git mv`. Injected so tests can fake moves without shelling out. Used by the rewrite handler when the target entry changes type and its file needs a new on-disk location.

type Options

type Options struct {
	GraphDir  string
	SDDDir    string // path to .sdd/ directory; required for commands that write tmp files
	Reader    Reader
	LLMRunner llm.Runner
	Committer Committer
	Brancher  Brancher
	Mover     Mover
	Puller    Puller
	Sessions  LegacySessionMigrator
	// Repos owns the connected-repos side effects (clone, pull, config
	// writes). Nil means no connected-repos support — repo commands fail
	// loud, and cross-repo cache freshening is skipped.
	Repos  *repos.Manager
	Stderr io.Writer
	Now    func() time.Time
}

Options configures a new Handler. Zero-valued fields get sensible defaults.

type Puller added in v0.8.0

type Puller interface {
	// IsClean reports whether the working tree has no uncommitted changes
	// (a `git status --porcelain` with empty output).
	IsClean(ctx context.Context) (bool, error)
	// MergePull runs a merge-only pull (`git pull --no-rebase`) and returns
	// git's combined output. A non-nil error means the pull failed.
	MergePull(ctx context.Context) (string, error)
}

Puller is the git surface for `sdd sync --pull`: a merge-only pull that never rewrites the shared graph's history. Injected so tests can fake the working-tree state and the pull without shelling out to git.

type Reader

type Reader interface {
	// CurrentGraph returns the current graph through the read-side GraphSource
	// seam (a fresh source per call — per-command lifetime, no retained cache).
	// Handlers route through it rather than loading directly so cross-repo
	// assembly reaches the write path for free at the one seam.
	CurrentGraph(dir string) (*model.Graph, error)
	LoadWIPMarkers(graphDir string) ([]*model.WIPMarker, error)
	Preflight(ctx context.Context, q query.PreflightQuery) (*query.PreflightResult, error)
	SkillStatus(ctx context.Context, q query.SkillStatusQuery) (*query.SkillStatusResult, error)
}

Reader is the handler-side view of the finder. It bundles every read operation handlers need (graph loading, pre-flight, WIP markers, install state). Defined here (rather than imported as the concrete *finders.Finder) so consumers can substitute fakes in tests — standard "accept interfaces, return structs" Go pattern. *finders.Finder satisfies this interface.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL