cli

package module
v0.1.8 Latest Latest
Warning

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

Go to latest
Published: Oct 11, 2026 License: Apache-2.0 Imports: 45 Imported by: 0

Documentation

Overview

Package cli is the `ds` command: every subcommand is a thin call into github.com/ubgo/docsync (docs/SPEC.md §37.5). It owns what the library refuses to: paths, files, git, printing, and exit codes.

Embed it to ship a custom binary with extra extractors and verbs:

func main() {
    cli.Main(cli.WithName("pds"), cli.WithExtractor(mytier.Kotlin()), cli.WithVerb("ticket"))
}

Index

Constants

View Source
const (
	// ClaudeFile and AgentsRootFile are the repo-root instruction files
	// Claude Code and other agents read.
	ClaudeFile     = "CLAUDE.md"
	AgentsRootFile = "AGENTS.md"
	// SkillFile is the Claude Code skill, loadable on demand.
	SkillFile = ".claude/skills/docsync/SKILL.md"
	// ClaudeSettingsFile holds Claude Code hooks for the repo.
	ClaudeSettingsFile = ".claude/settings.json"
)

`ds init --agents` (§26.10): the rules of section 25 land where agents read them, `ds mcp` is registered, and a session-start hook loads the map. Every write is idempotent: fragments sit between markers and are replaced in place, JSON configs are merged, so running it again after a tool upgrade refreshes the rules without touching anything else.

View Source
const (
	CacheDir  = "cache"
	CacheFile = "extract.json"
)

The extraction cache (§16 "incremental by default") lives in .ds/cache/extract.json: per file, the content hash and what extracting those bytes produced. A file whose bytes did not change is not re-extracted. Files with problems are never cached (the library rule), so a fixed directive is always re-read.

View Source
const (
	ExitOK       = 0
	ExitFindings = 1
	ExitError    = 2
)

Exit codes. Findings decide `check`'s code (§17); everything else is a usage or runtime failure.

View Source
const (
	// ExportBlocksFile maps every local id to its rendered block.
	ExportBlocksFile = "blocks.json"
	// ExportStatusFile is `status --json`, so a site can paint freshness
	// dots beside cited sentences.
	ExportStatusFile = "status.json"
)

`ds export hugo --out DIR` (§24 "docs site", build order 8): everything a static site generator needs at build time, as data files. Hugo cannot run a program during a build, so the Hugo module in integrations/hugo reads `data/docsync/blocks.json` from a shortcode and `status.json` from a partial; this command writes both. Docusaurus runs the remark plugin instead, which calls `ds render` directly.

View Source
const (
	WriteDef    = "def"
	WriteAdopt  = "adopt"
	WriteRename = "rename"
	// WriteRepair comments directives an older build left bare, so `undo`
	// can put them back exactly as they were found.
	WriteRepair = "repair"
)

The commands that write source (§22 "source is written only by def and adopt"; rename rewrites docs). The journal records which one made a write so `undo --list` can say what it is offering to reverse.

View Source
const (
	ToolMap     = "map"
	ToolFind    = "find"
	ToolRead    = "read"
	ToolLocate  = "locate"
	ToolFacts   = "facts"
	ToolWhy     = "why"
	ToolContext = "context"
	ToolCheck   = "check"
	ToolImpact  = "impact"
	ToolDef     = "def"
	ToolAck     = "ack"
)

MCP tool names (§26.1).

View Source
const (
	DirName       = scan.StateDir
	ConfigFile    = "config.toml"
	LedgerFile    = ledger.LedgerFile
	RefsFile      = ledger.RefsFile
	AcksFile      = ledger.AcksFile
	CISnippet     = "ci-github.yml"
	AgentsFile    = "AGENTS.md"
	GitignoreFile = ".gitignore"
	// GitattributesFile gives .ds/ files the merge behaviour they need.
	GitattributesFile = ".gitattributes"
)

File names under .ds/.

View Source
const BlocksDir = "blocks"

The body store (§20.1). `check` has to answer "what did this block say when the sentence was acked", and the acked hash can be arbitrarily far behind: many scans back in this repo, or in another repository entirely. Neither the previous ledger nor `git show` can answer that — the ledger holds one row per id at one commit, and a merged row holds no body at all — so bodies are kept content-addressed, one file per hash, written by `scan` under .ds/blocks/ and by `publish` under the index entry's blocks/.

Content-addressing makes the store append-only and self-deduplicating: a block that did not change writes nothing, and a hash that is present is correct by construction. A missing body is not an error; it degrades the finding to block.ClassUnknown, which still flags.

View Source
const (

	// DefaultKeep is how long a dead body is kept anyway. A repo that has
	// not synced lately may be about to ack against it, and a body is
	// cheap: when in doubt it stays.
	DefaultKeep = "30d"
)

`ds prune` removes superseded block bodies (§20.1). The stores are content-addressed and append-only, so a block that churns leaves one body per version in `.ds/blocks/` and another in the index — both committed, so every superseded body is permanent history in two git repositories.

The safety property that matters is not "usually removes the right ones" but "cannot remove a live one". Liveness is computed as a set and only its complement is removed; anything that cannot be read is a refusal, never an assumption that the hashes it held were dead.

View Source
const DefaultName = "ds"

DefaultName is the binary name when WithName is not given.

View Source
const HashesFile = "hashes.json"

Secret resolution under `check --resolve` (§12): each provider is a process plugin `ds-resolve-<provider>` on PATH speaking the procplugin protocol, so any language can add one and no provider SDK enters this module. GitHub answers existence only; providers that can read a value return its hash and never the value, which the host enforces. Truth hashes are kept in .ds/hashes.json when resolve.store_hash is on, so the next run can report `rotated`.

View Source
const (
	IndexDir = "index"
)

Workspace index handling (§21). The repo config's `workspace` names the index: a directory on disk, or a git URL cloned into .ds/index/.

View Source
const JournalFile = "journal.tsv"

JournalFile records every source write so `undo` can reverse the last one (§33 safety). One line per edit: batch, kind, time, file, line, old, new; tabs, newlines, and backslashes in the text fields are escaped the way the ledger does.

The file is machine-local and never committed (§6): `old` and `new` are raw source lines, which on a file under `[secret] paths` means the line holds a value. It is listed in `.ds/.gitignore` so that copy never enters history, where rotating the credential could not remove it.

View Source
const LedgerDir = "ledger"

LedgerDir holds sharded ledgers when ledger shard = true.

View Source
const LockFile = "lock"

LockFile is the machine-local file .ds/ writers lock (see withLock).

View Source
const MCPConfigFile = ".mcp.json"

MCPConfigFile is the repo-level MCP registration agents read (§26.10), the one MCPTargets always writes.

View Source
const MetricsFile = "metrics.json"

MetricsFile accumulates what the agent surface served (§26.11): bytes handed out by `context` and `read` against the bytes of the files those items came from. `report --metrics` prints the ratio.

View Source
const (
	NotifiedFile = "notified.json"
)

`ds notify` (§22, §24 nightly): routes open error findings to owners with dedupe and escalation. A finding is sent once; it is sent again, marked escalated, when it is still open after notify.escalate_after. State lives in .ds/notified.json. The only built-in channel is a Slack incoming webhook (notify.slack, `$VAR` expanded from the environment by the CLI); without one the digest is printed, which is what a cron job mails.

View Source
const (
	RunsFile = "runs.json"
)

`ds:run` execution (§9.4): only under `check --run`, only where run.enabled is true, `cmd=` only in docs matching run.allow, `id=` only with runnable=true. Results are kept in .ds/runs.json so `render` can show the command with its last outcome. This is code execution from a text file and is treated as such: every guard here is deliberate.

View Source
const (
	URLCacheFile = "urls.json"
)

`ds:url` checking (§9.7): under `check --resolve` only, cached per url.ttl in .ds/urls.json, rate limited by url.rate_per_minute. Redirects are followed and the final URL recorded; the page title is read from the first titleReadCap bytes.

Variables

View Source
var (
	// ErrNotInitialised means no directory at or above where the command
	// started holds a config. Below one that does, the root is found
	// instead (App.locate), so this never sends someone to create a second
	// .ds inside an initialised repository.
	ErrNotInitialised = errors.New("no .ds/config.toml here or in any parent directory; run `init` first")
	// ErrNestedInit refuses `init` under an initialised repository unless
	// --force says a separate docsync root is wanted there.
	ErrNestedInit = errors.New("a parent directory already has .ds/config.toml")
	ErrExists     = errors.New(".ds/config.toml already exists; pass --force to overwrite")
	ErrUsage      = errors.New("usage")
)

Errors.

View Source
var (
	// ErrNothingToUndo is returned when the journal is empty.
	ErrNothingToUndo = errors.New("nothing to undo")
	// ErrUndoCommitted is returned when the next entry is already in git
	// history and --force was not given.
	ErrUndoCommitted = errors.New("nothing to undo since the last commit")
	// ErrUndoOrphans is returned when undoing would remove a def that
	// sentences still cite and --orphan was not given.
	ErrUndoOrphans = errors.New("undo would orphan a cited def")
)

Errors.

View Source
var (
	ErrNoWorkspace   = errors.New("no workspace configured; set `workspace` in .ds/config.toml")
	ErrNotDefault    = errors.New("publish runs only on the default branch (§21); pass --force to override")
	ErrIndexOffline  = errors.New("workspace index unreachable and no cached copy")
	ErrIndexNoRemote = errors.New("index is a local directory; nothing to push")
)

Errors.

View Source
var DoctorStatusValues = []string{doctorOK, doctorWarn, doctorFail}

DoctorStatusValues is the canonical order.

View Source
var ErrCommitAckKey = fmt.Errorf("%w: an ack in a commit message takes only %s", ErrUsage, strings.Join(commitAckKeys, "= and ")+"=")

ErrCommitAckKey is a key on a commit-message ack that is not in commitAckKeys. note= used to be read past without a word, so the note a developer wrote never reached the ack log (bug 73); any key that does nothing is refused rather than ignored.

View Source
var ErrDefCap = errors.New("mcp: def cap for this session reached (agents.max_defs_per_run); ask a human to raise it or run `def` themselves")

ErrDefCap is returned when an MCP session exceeds agents.max_defs_per_run.

View Source
var ErrForkPR = errors.New("--run and --resolve are disabled on pull requests from forks")

ErrForkPR refuses --run and --resolve on a fork pull request.

View Source
var ErrForkPRReview = errors.New("review --ai is disabled on pull requests from forks: it runs the [review] command from committed configuration")

ErrForkPRReview refuses `review --ai` on a fork pull request: it runs the [review] command out of .ds/config.toml, a committed file the pull request can change, so on a fork's PR it would run the fork's command with the repository's credentials.

View Source
var ErrGitHubEnv = errors.New("github comment needs GITHUB_TOKEN and GITHUB_REPOSITORY, and a pull request number (--pr or GITHUB_EVENT_PATH); --dry-run needs none of them")

ErrGitHubEnv names the missing environment.

View Source
var ErrHTTPRecords = errors.New("records: http source failed")

ErrHTTPRecords wraps a failed record request.

View Source
var ErrNoDir = fmt.Errorf("%w: --%s is not a directory", ErrUsage, flagDir)

ErrNoDir is a --dir that is not a directory.

View Source
var ErrNoReviewCommand = fmt.Errorf("%w: --ai needs [review] command in .ds/config.toml", ErrUsage)

ErrNoReviewCommand is returned by --ai without [review] command.

View Source
var ErrNoShell = errors.New("the shell is not on PATH")

ErrNoShell is returned when a ds:run or the [review] command is about to execute and the shell it runs under is not on PATH. It is an error rather than a skip: a check that was asked to run commands and ran none must not look like one whose commands passed.

View Source
var ErrNoSnapshot = fmt.Errorf("%s/%s not found; run `ds sync` to record the foreign blocks this repo cites", DirName, ledger.ForeignFile)

ErrNoSnapshot is returned by --frozen when no snapshot has been written. It never degrades to "no foreign blocks": a frozen check with nothing to resolve against would pass vacuously, reporting green for citations it never looked at.

View Source
var ErrNoTree = errors.New("render --at needs a VCS that lists a commit's files (cli.TreeVCS)")

ErrNoTree is returned by `render --at` when the VCS cannot list a commit's tree (it does not implement TreeVCS).

View Source
var ErrNoVCS = errors.New("git: not a repository or git not installed")

ErrNoVCS is returned by Git when the directory is not a repository or git is not installed; commands degrade to no permalinks and no diffs.

View Source
var ErrNotDir = errors.New("exists but is not a directory")

ErrNotDir is a path docsync keeps a directory at that holds something else.

View Source
var ErrNotMember = errors.New("this repository is not one of the workspace's repos")

ErrNotMember refuses a publish from a repository the workspace does not list: a fork or mirror of a listed one, or a repository that is not in the workspace at all.

View Source
var ErrOutsideRepo = fmt.Errorf("%w: path is outside the repository", ErrUsage)

ErrOutsideRepo refuses a path argument that leaves the repository.

View Source
var ErrPruneIndex = fmt.Errorf("prune --index needs the default branch and a fresh sync")

ErrPruneIndex is returned when `--index` is asked for from a position that cannot see who cites what.

View Source
var ErrPruneState = fmt.Errorf("prune: cannot read the state that says which bodies are live")

ErrPruneState is returned when a file the live set depends on cannot be read. Pruning on a partial view is how a live body gets deleted, so it is refused outright rather than degraded.

View Source
var ErrSettings = errors.New("settings.json is not a JSON object; fix it or remove it")

ErrSettings is returned when .claude/settings.json is not a JSON object.

View Source
var Exit = os.Exit

Exit is what Main calls with the exit code. It is a variable so a test or an embedder that must not terminate the process can replace it; nothing else in the package exits.

View Source
var MCPTargets = []MCPTarget{
	{File: MCPConfigFile, Key: mcpServersKey},
	{File: ".cursor/mcp.json", Key: mcpServersKey, Dir: ".cursor"},
	{File: ".vscode/mcp.json", Key: vscodeKey, Dir: ".vscode", Stdio: true},
	{File: ".gemini/settings.json", Key: mcpServersKey, Dir: ".gemini"},
}

MCPTargets are the clients `init --agents` registers `ds mcp` with. Only .mcp.json was written before, which most clients do not read (bug 108).

WriteKinds is the canonical order.

Functions

func ConfigTOML

func ConfigTOML(c config.Config) string

ConfigTOML renders a config as the TOML `init` writes. Only the keys a new repo needs appear; the rest fall back to defaults and are documented in the spec.

func Main

func Main(opts ...Option)

Main runs with os.Args and exits with the resulting code.

func Run

func Run(args []string, opts ...Option) int

Run executes args and returns the exit code. It never calls os.Exit, so embedders and tests can drive it.

Types

type App

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

App is one configured invocation.

type BuildInfo

type BuildInfo struct {
	// Version is the module version when installed with `go install …@v`,
	// else "(devel)" for a build from a checkout.
	Version string `json:"version"`
	// Commit is the full revision the binary was built from.
	Commit string `json:"commit"`
	// Dirty says the checkout had uncommitted changes, so Commit alone does
	// not describe the code that is running.
	Dirty bool `json:"dirty"`
	// Time is the commit's time, not the build's.
	Time string `json:"time"`
	// Go is the toolchain that built it.
	Go string `json:"go"`
}

BuildInfo is what `ds version` reports about the running binary.

func (BuildInfo) Line added in v0.1.6

func (b BuildInfo) Line(name string) string

Line is the one line `ds version` and `ds --version` print, naming the binary as it was built (cli.WithName): a custom build reported itself as ds, so its users could not tell which binary they were running.

func (BuildInfo) String

func (b BuildInfo) String() string

String is the version line under the default name.

type CommitCounter added in v0.1.6

type CommitCounter interface {
	CommitsBetween(from, to string) (int, error)
}

CommitCounter is the optional VCS upgrade behind `workspace.stale_after_commits` (bug 101): how many commits `to` is ahead of `from`. A VCS without it gets no commit-distance warning, never a guessed one.

type Git

type Git struct {
	Dir string
}

Git shells out to the `git` program, which must be on PATH, 2.24 or later for --end-of-options. Documented at the point of use per the library rules; the root module never does this.

Every value that is not a flag — a commit, a path, a URL — goes after --end-of-options or "--". They come from committed files a pull request can edit (the ledger header's commit, an at= in a doc, the workspace URL), and git reads anything starting with "-" as an option: a commit of `--output=<path>` made `git show` write that file.

func (Git) Branch

func (g Git) Branch() (string, error)

Branch returns the current branch, or an error on a detached HEAD.

func (Git) Churn

func (g Git) Churn() (map[string]int, error)

Churn counts commits per file over the last churnCommits commits.

func (Git) Clone

func (g Git) Clone(url, dir string) error

Clone clones url into dir.

func (Git) CommitIndex added in v0.1.6

func (g Git) CommitIndex(dir, message string) (bool, error)

CommitIndex implements IndexCommitter.

func (Git) CommitsBetween added in v0.1.6

func (g Git) CommitsBetween(from, to string) (int, error)

CommitsBetween implements CommitCounter: the number of commits reachable from to and not from from. An unknown commit -- one this clone never fetched -- is an error, so the caller says nothing rather than guess.

func (Git) Exists

func (g Git) Exists(commit string) bool

Exists reports whether commit resolves.

func (Git) Head

func (g Git) Head() (string, error)

Head returns the short sha of HEAD.

func (Git) Introduced added in v0.1.6

func (g Git) Introduced(path, text string) (string, error)

Introduced returns the short sha of the oldest commit that changed how many times text occurs in path (git's pickaxe), which for a line written once is the commit that added it.

func (Git) Message

func (g Git) Message(commit string) (string, error)

Message returns the full commit message.

func (Git) Pull

func (g Git) Pull(dir string) error

Pull fast-forwards the checkout at dir.

func (Git) Push

func (g Git) Push(dir, message string) error

Push commits everything under dir and pushes. A clean tree is not an error: publishing the same ledger twice is a no-op.

func (Git) Reachable added in v0.1.6

func (g Git) Reachable(url string) error

Reachable asks the remote for its HEAD with `git ls-remote`, never prompting. It reads nothing into the working tree and writes nothing.

func (Git) RemoteURL

func (g Git) RemoteURL() string

RemoteURL returns origin's URL or "".

func (Git) Resolve added in v0.1.6

func (g Git) Resolve(commit string) (string, error)

Resolve returns the short sha commit names.

func (Git) Show

func (g Git) Show(commit, path string) ([]byte, error)

Show returns path as of commit.

func (Git) Staged

func (g Git) Staged() ([]string, error)

Staged lists paths staged for the next commit.

func (Git) Tree added in v0.1.6

func (g Git) Tree(commit string) (map[string]int64, error)

Tree lists every blob in commit's tree with its size. Submodules (commit entries) carry no size and are left out, as a scan of the work tree does not descend into them either.

func (Git) User

func (g Git) User() string

User returns git's user.name, or "" when unset.

type IndexCommitter added in v0.1.6

type IndexCommitter interface {
	// CommitIndex commits everything under dir when dir is the top of its
	// own git work tree, and reports whether it made a commit. A directory
	// that is not a work tree's top -- a plain directory, or one nested
	// inside an unrelated repository -- is not committed and is not an
	// error: committing there would sweep up the enclosing repository's
	// unrelated changes.
	CommitIndex(dir, message string) (bool, error)
}

IndexCommitter is the optional VCS upgrade `publish` uses for an index that is a local directory rather than a managed clone (bug 100). Writing the files and stopping left them untracked in an index that is itself a git repository, so every later publish piled onto one uncommitted change and nothing recorded which repository wrote what, at which commit. A VCS that does not implement it is left alone: the files are written and nothing else happens.

type MCPTarget added in v0.1.6

type MCPTarget struct {
	File string
	Key  string
	Dir  string
	// Stdio adds "type": "stdio", which VS Code's mcp.json requires.
	Stdio bool
}

MCPTarget is one MCP client's project-level registration file: where it is, the top-level key holding its servers, and the directory whose presence says the client is in use. The first target has no directory and is always written; the others are written only where their client has already left its directory, which is how "the configs it can find" (§26.10) is decided without guessing at what someone has installed.

type Notified

type Notified struct {
	FirstSent time.Time `json:"first_sent"`
	LastSent  time.Time `json:"last_sent"`
	Escalated bool      `json:"escalated"`
	// Tier is the urgency a snapshot alert was last sent at, so a drift
	// that worsens is news and a second edit at the same tier is not.
	// Empty for an ordinary finding.
	Tier string `json:"tier,omitempty"`
	// Misses counts consecutive runs that could not compare a repo against
	// the index. Not comparing is not the same as nothing to report: a
	// broken index would otherwise hide staleness indefinitely, which is
	// the vacuous pass this whole series has been about.
	Misses int `json:"misses,omitempty"`
}

Notified is one delivered finding.

type NotifyState

type NotifyState interface {
	// Load reports the remembered state and whether any was found. The
	// second value is not len(state) > 0: a repo that is green records an
	// empty state, and treating that as "no memory" would make every later
	// run announce itself as a first run.
	Load(ctx context.Context) (state map[string]Notified, found bool, err error)
	Save(ctx context.Context, state map[string]Notified) error
}

NotifyState is where `ds notify` keeps the memory that makes it quiet: which findings it has already sent, and when, so a second run says nothing new and `escalate_after` can measure from a first sighting.

It is an interface because that memory has to outlive a process, and where it outlives it is the operator's decision. A CI runner checks the repository out fresh every night and cannot write anything back, so the default file is empty on every run there and dedupe silently never holds — a notifier that repeats itself nightly is one people mute. The shipped nightly template restores the file from a cache; a workspace index or a database can implement this instead without the command changing.

A missing state is an empty map and no error: that is a first run. An unreadable one is an error, because treating a corrupt file as empty would silently reset dedupe, which looks exactly like working.

type Option

type Option func(*App)

Option configures an App.

func Standard

func Standard() Option

Standard is the built-in set; it exists so a custom binary reads `cli.WithDefaults(cli.Standard())` and gains new built-ins by upgrading.

func WithClock

func WithClock(f func() time.Time) Option

WithClock replaces time.Now.

func WithConfigDefaults

func WithConfigDefaults(f func(*config.Config)) Option

WithConfigDefaults adjusts the defaults of an organisation-wide binary: every repository's config is read on top of them, so a key its file does not set falls back to the organisation's value, and `init` writes them into the keys it writes. Before, config was read on top of the built-in defaults only, so everything the hook set outside init's few keys was silently lost — an agent cap of 7 ran as 20.

func WithDefaults

func WithDefaults(o Option) Option

WithDefaults applies a preset such as Standard.

func WithDir

func WithDir(dir string) Option

WithDir sets the repository root; default is the working directory.

func WithExtractor

func WithExtractor(es ...extract.Extractor) Option

WithExtractor registers extra tiers ahead of the built-in ones.

func WithHTTPClient

func WithHTTPClient(c *http.Client) Option

WithHTTPClient replaces the client `check --resolve` uses for ds:url.

func WithIO

func WithIO(in io.Reader, out, errw io.Writer) Option

WithIO replaces stdin, stdout, and stderr.

func WithName

func WithName(name string) Option

WithName sets the binary name shown in help and remedies.

func WithNotifyState

func WithNotifyState(s NotifyState) Option

WithNotifyState replaces where `ds notify` keeps its dedupe and escalation memory. The default is `.ds/notified.json`, which a CI runner discards between runs; an organisation whose CI cannot cache that file supplies a store that survives instead.

func WithPicker

func WithPicker(ps ...docsync.Picker) Option

WithPicker registers Go `pick=` schemes for a custom binary; process plugins (`ds-pick-<scheme>`) need no registration.

func WithPluginLookup

func WithPluginLookup(f func(name string) (string, error)) Option

WithPluginLookup replaces PATH lookup for `ds-*` process plugins.

func WithRegistry

func WithRegistry(r *extract.Registry) Option

WithRegistry replaces tier selection entirely (§37.3); WithExtractor then prepends to it. The escape hatch for a binary that wants no built-in tier.

func WithVCS

func WithVCS(v VCS) Option

WithVCS replaces the git adapter.

func WithVerb

func WithVerb(names ...string) Option

WithVerb registers plugin verb names.

type Prober added in v0.1.6

type Prober interface {
	Reachable(url string) error
}

Prober is an optional upgrade of VCS: it answers whether a remote can be reached without fetching anything, which is what `ds doctor` asks of the workspace index (SPEC §22). A VCS without it is not forced to pretend; the doctor row says reachability was not checked instead.

type RunRecord

type RunRecord struct {
	Command string    `json:"command"`
	Output  string    `json:"output"`
	OK      bool      `json:"ok"`
	Expect  string    `json:"expect,omitempty"`
	At      time.Time `json:"at"`
}

RunRecord is one stored outcome.

type StaleBlock

type StaleBlock struct {
	ID   string `json:"id"`
	From string `json:"from"`
	To   string `json:"to"`
	// Classes describe the difference when both bodies are available, so a
	// reader can tell a signature change from a comment before deciding
	// whether syncing is urgent.
	Classes []block.Class `json:"class,omitempty"`
	// Gone says the publishing repo no longer defines this id. It is the
	// most urgent state, not the quietest: the next `ds sync` turns every
	// citation of it `broken`, so hearing about it before that sync is the
	// whole value. It must never share a branch with "unchanged" — doing so
	// reported a repo whose cited block had been deleted as up to date.
	Gone bool `json:"gone,omitempty"`
}

StaleBlock is one cited block whose upstream has moved past the snapshot.

type StaleRepo

type StaleRepo struct {
	Repo string `json:"repo"`
	// Cited is how many of this repo's blocks the snapshot records; Behind
	// how many of those have moved on.
	Cited  int `json:"cited"`
	Behind int `json:"behind"`
	// SnapshotCommit is what the snapshot recorded; IndexCommit what the
	// index publishes now. Either may be empty when a repo published
	// without a commit.
	SnapshotCommit string       `json:"snapshot_commit,omitempty"`
	IndexCommit    string       `json:"index_commit,omitempty"`
	Blocks         []StaleBlock `json:"blocks,omitempty"`
	// Compared is false when the index publishes nothing for this repo, so
	// its blocks could not be looked up at all. Reporting that as "up to
	// date" would be a claim about blocks nobody read — the same silent
	// pass the whole snapshot section exists to prevent.
	Compared bool `json:"compared"`
}

StaleRepo is one upstream repository's position relative to the snapshot.

type Staleness

type Staleness struct {
	// Present is false when this repo has no snapshot, which is the normal
	// state for a repo with no workspace.
	Present bool `json:"present"`
	// SyncedAt is when the snapshot was written; Age its age.
	SyncedAt time.Time `json:"synced_at,omitempty"`
	Age      string    `json:"age,omitempty"`
	// Compared is false when no index was reachable. The repos list is then
	// empty and the reader is told the comparison was skipped, rather than
	// being shown a silent "up to date" that was never checked.
	Compared bool        `json:"compared"`
	Repos    []StaleRepo `json:"repos,omitempty"`
}

Staleness is the snapshot section of `ds status`.

type Store

type Store struct {
	Root string
	// Now stamps journal entries; nil means time.Now().UTC().
	Now func() time.Time
	// contains filtered or unexported fields
}

Store is the `.ds/` directory: config plus the three committed TSV files (§6). It is the default `Store` capability; a database or the workspace index can replace it later without touching commands.

func NewStore

func NewStore(dir string) *Store

NewStore points at dir/.ds.

func (*Store) AddMetrics

func (s *Store) AddMetrics(served, source int64) error

AddMetrics adds one served item to the totals.

func (*Store) AppendAcks

func (s *Store) AppendAcks(rows []ledger.Ack) error

AppendAcks adds rows to the ack log on disk, under the .ds/ lock: it re-reads the log as it is now, appends the rows it does not already hold, and writes it back. It is the only way the log is written.

Why re-read: an ack command loads the log when it starts and used to write that copy back with its own rows appended, so two acks recorded at once — an agent's parallel tool calls, a triage group alongside a manual ack — each wrote a log without the other's row. Ten concurrent acks recorded one, and every command reported success. Rows already on disk are skipped by their encoded form, so a caller may pass the whole log it holds.

func (*Store) ApplyEdit

func (s *Store) ApplyEdit(e docsync.Edit) error

ApplyEdit applies a library Edit to its file in the tree: the only source write the tool makes (§33 safety), and only from `def`. The file is read again here rather than trusting an earlier read, so an edit computed against stale content is refused by Edit.Apply instead of landing on the wrong line.

func (*Store) BodyAt

func (s *Store) BodyAt(hash string) (string, bool)

BodyAt reads one body from the local store.

func (*Store) Exists

func (s *Store) Exists() bool

Exists reports whether the config file is present.

func (*Store) Journal

func (s *Store) Journal(kind string, edits []docsync.Edit) error

Journal appends a batch of applied edits. Batch numbers increase by one per call so `undo` reverses one command's writes together. kind names the command that made them, one of WriteKinds.

func (*Store) Load

Load implements docsync.Store over the .ds/ files.

func (*Store) LoadCache

func (s *Store) LoadCache(inputs string, now func() time.Time) (*extractCache, error)

LoadCache reads the cache; a missing file, a foreign format, and entries extracted under different inputs are all empty, so a stale entry is never preferred to re-reading the file. now is the clock Stamp measures modification times against.

func (*Store) LoadConfig

func (s *Store) LoadConfig() (config.Config, error)

LoadConfig parses .ds/config.toml on top of the built-in defaults.

func (*Store) LoadConfigOnto

func (s *Store) LoadConfigOnto(base config.Config) (config.Config, error)

LoadConfigOnto parses .ds/config.toml on top of base: what the file sets wins, and everything else keeps base's value (config.ParseOnto).

func (*Store) LoadForeign

func (s *Store) LoadForeign() (ledger.Foreign, bool, error)

LoadForeign reads the snapshot; ok is false when the file is absent.

func (*Store) LoadHashes

func (s *Store) LoadHashes() (map[string]string, error)

LoadHashes reads stored truth hashes; missing means none.

func (*Store) LoadMetrics

func (s *Store) LoadMetrics() (served, source int64, err error)

LoadMetrics reads the totals; missing means zero.

func (*Store) LoadNotified

func (s *Store) LoadNotified() (map[string]Notified, error)

LoadNotified reads the dedupe state.

func (*Store) LoadRuns

func (s *Store) LoadRuns() (map[string]RunRecord, error)

LoadRuns reads .ds/runs.json; missing means empty.

func (*Store) LoadState

func (s *Store) LoadState() (ledger.Ledger, ledger.Refs, ledger.Acks, error)

LoadState reads the ledger, refs, and acks; a missing file is an empty value, a malformed one is an error. A sharded ledger directory is read when present and takes precedence over a single file.

func (*Store) LoadURLs

func (s *Store) LoadURLs() (map[string]URLEntry, error)

LoadURLs reads the cache; missing means empty.

func (*Store) Save

func (s *Store) Save(_ context.Context, l ledger.Ledger, r ledger.Refs, a ledger.Acks) error

Save implements docsync.Store: ledger, refs, and the whole ack log.

func (*Store) SaveCache

func (s *Store) SaveCache(c *extractCache) error

SaveCache writes the cache when anything changed.

func (*Store) SaveForeign

func (s *Store) SaveForeign(f ledger.Foreign) error

SaveForeign writes the snapshot.

func (*Store) SaveHashes

func (s *Store) SaveHashes(m map[string]string) error

SaveHashes writes them.

func (*Store) SaveLedger

func (s *Store) SaveLedger(l ledger.Ledger, r ledger.Refs) error

SaveLedger writes ledger and refs atomically. With shard set the ledger goes to .ds/ledger/<top-level dir>.tsv, one file per directory, and the single-file ledger is removed so the two never disagree.

func (*Store) SaveLedgerSharded

func (s *Store) SaveLedgerSharded(l ledger.Ledger, r ledger.Refs, shard bool) error

SaveLedgerSharded is SaveLedger with the layout chosen by shard.

func (*Store) SaveNotified

func (s *Store) SaveNotified(m map[string]Notified) error

SaveNotified writes it.

func (*Store) SaveRuns

func (s *Store) SaveRuns(runs map[string]RunRecord) error

SaveRuns writes the records.

func (*Store) SaveURLs

func (s *Store) SaveURLs(m map[string]URLEntry) error

SaveURLs writes the cache.

func (*Store) Undo

func (s *Store) Undo() ([]docsync.Edit, error)

Undo reverses the last batch: edits are undone in reverse order, each checked against the file so a later hand edit is never overwritten. It returns the reversed edits.

func (*Store) Write

func (s *Store) Write(name string, data []byte) error

Write stores a file under .ds/ atomically — temp file then rename, so a crash mid-write never leaves a truncated ledger for the next check — and under the .ds/ lock, so two commands writing the same file cannot share its temp path: without the lock one rename failed, and interleaved writes to the shared temp file could rename a mixture of both into place.

func (*Store) WriteBodies

func (s *Store) WriteBodies(bodies map[string]string) error

WriteBodies adds bodies to the local store, skipping any already present: the file name is the content hash, so an existing file cannot disagree with what would be written. Writing goes through Store.Write so there is exactly one atomic-write path in the CLI, and a body is never observed half-written by a concurrent check.

type TreeVCS added in v0.1.6

type TreeVCS interface {
	// Resolve returns the short sha that commit (a sha, tag or branch) names.
	Resolve(commit string) (string, error)
	// Tree maps every file path in commit's tree to its size in bytes.
	Tree(commit string) (map[string]int64, error)
}

TreeVCS is the optional upgrade to VCS that `render --at` needs to render a page as of a commit rather than the page's old text over today's code: the commit a ref names, and every file in that commit's tree with its size. Git implements it; a VCS that does not makes `render --at` refuse rather than mix two commits in one page. It is an optional interface, not a VCS method, so a host's existing VCS keeps compiling.

type URLEntry

type URLEntry struct {
	Status    int       `json:"status"`
	Final     string    `json:"final"`
	Title     string    `json:"title,omitempty"`
	Error     string    `json:"error,omitempty"`
	CheckedAt time.Time `json:"checked_at"`
}

URLEntry is one cached outcome.

type UpdateReport added in v0.1.7

type UpdateReport struct {
	// Current is the running version; Latest the release it was compared
	// with (or the one --version named).
	Current string `json:"current"`
	Latest  string `json:"latest"`
	// Available says Latest is newer than Current.
	Available bool `json:"update_available"`
	// Updated says files were replaced; Installed lists them.
	Updated   bool     `json:"updated"`
	Installed []string `json:"installed,omitempty"`
	// URL is the release page with its notes.
	URL string `json:"url"`
	// AutoUpdate is the automatic mode in effect (auto, notify, off) and
	// AutoUpdateFrom what decided it: "default", "DS_UPDATE", the
	// repository's config, or "not a release build".
	AutoUpdate     string `json:"auto_update"`
	AutoUpdateFrom string `json:"auto_update_from"`
}

UpdateReport is `ds update --json`.

type VCS

type VCS interface {
	Head() (string, error)
	Show(commit, path string) ([]byte, error)
	Exists(commit string) bool
	// User is the configured author name, for the ack log's actor.
	User() string
	// Churn is commits touching each file over recent history, for
	// `report --unmarked`; the count is a ranking signal, not an audit.
	Churn() (map[string]int, error)
	// Branch is the current branch name, for publish's default-branch rule.
	Branch() (string, error)
	// RemoteURL is origin's URL, which names the repo in a workspace.
	RemoteURL() string
	// Clone, Pull, and Push manage the workspace index checkout.
	Clone(url, dir string) error
	Pull(dir string) error
	Push(dir, message string) error
	// Staged lists files in the index, for `impact --staged`.
	Staged() ([]string, error)
	// Message returns a commit's message, for `ack --from-commit`.
	Message(commit string) (string, error)
}

VCS is what the CLI needs from version control: the current commit, a file at a commit, and whether a commit exists. Git is the only built-in; the interface exists so tests and other hosts can supply their own.

Directories

Path Synopsis
cmd
ds command
Command ds is the standard docsync binary: the built-in verbs and tiers plus the shipped ext modules: the structured tier (YAML, TOML, HCL, with the `hcl:` pick scheme) and the tree-sitter syntax tier (Go, TypeScript, TSX, JavaScript, Python, SQL).
Command ds is the standard docsync binary: the built-in verbs and tiers plus the shipped ext modules: the structured tier (YAML, TOML, HCL, with the `hcl:` pick scheme) and the tree-sitter syntax tier (Go, TypeScript, TSX, JavaScript, Python, SQL).
ds-resolve-aws command
Command ds-resolve-aws is the AWS Secrets Manager resolver plugin (§12, §37.4).
Command ds-resolve-aws is the AWS Secrets Manager resolver plugin (§12, §37.4).
ds-resolve-env command
Command ds-resolve-env is the environment-variable resolver plugin (§12, §37.4), for the hops a def marks `source=env`: code that reads a variable such as STRIPE_KEY from its process environment.
Command ds-resolve-env is the environment-variable resolver plugin (§12, §37.4), for the hops a def marks `source=env`: code that reads a variable such as STRIPE_KEY from its process environment.
ds-resolve-gcp command
Command ds-resolve-gcp is the Google Secret Manager resolver plugin (§12, §37.4).
Command ds-resolve-gcp is the Google Secret Manager resolver plugin (§12, §37.4).
ds-resolve-github command
Command ds-resolve-github is the GitHub secret resolver plugin (§12, §37.4).
Command ds-resolve-github is the GitHub secret resolver plugin (§12, §37.4).
ds-resolve-onepassword command
Command ds-resolve-onepassword is the 1Password resolver plugin (§12, §37.4).
Command ds-resolve-onepassword is the 1Password resolver plugin (§12, §37.4).
ds-resolve-vault command
Command ds-resolve-vault is the HashiCorp Vault resolver plugin (§12, §37.4).
Command ds-resolve-vault is the HashiCorp Vault resolver plugin (§12, §37.4).
internal
doctest command
Command doctest runs the examples in docsync's guide pages against the built ds binary and fails when what a page shows is not what ds prints.
Command doctest runs the examples in docsync's guide pages against the built ds binary and fails when what a page shows is not what ds prints.
Package selfupdate replaces a running program with a newer release of itself: it finds the newest release of one binary stream, downloads that platform's archive, verifies it against the release's checksums.txt, and swaps the program and its companion executables in place.
Package selfupdate replaces a running program with a newer release of itself: it finds the newest release of one binary stream, downloads that platform's archive, verifies it against the release's checksums.txt, and swaps the program and its companion executables in place.

Jump to

Keyboard shortcuts

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