docsync

package module
v0.1.6 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: 29 Imported by: 1

README

docsync

Know the moment a sentence in your docs stops being true.

Documentation drift detection for teams working with AI coding agents: an open-source Go CLI, library, MCP server and language server that binds docs to the code, config and facts they describe.

Go 1.26 Works with MCP clients

Keep documentation bound to the code, configuration, and facts it describes, and find out the moment the thing behind a sentence changes. docsync is a docs-as-code tool for stale documentation: it works with any AI coding agent that speaks MCP (Claude Code, Cursor, and others), in editors through its language server and the VS Code extension, in CI through a GitHub Action and pre-commit hooks, and in Hugo and Docusaurus sites at build time. It is open source, runs locally, and needs no account or API key.

You mark a block once, in the file that owns it, with a comment. You cite it from prose with a link. ds check tells you which sentences are now about code that changed, moved, or vanished, with the diff and the exact command that clears each finding. Nothing is copied into the repository; permalinks, values, and code snippets are produced at build time from live source.

// ds:def id=sess-save-k7m2p4xq owner=@auth stability=api
func (s *Store) SaveSession(ctx context.Context, sess Session) error {
auth:
  port: 8081   # ds:def id=auth-port-h3v8n2wd
Every write goes through [`SaveSession`](ds:block?id=sess-save-k7m2p4xq). It listens on [8081](ds:cfg?id=auth-port-h3v8n2wd).

Documentation: the guide covers setup, every directive, command, config key, language and integration, with real output throughout; start with docsync at a glance or Getting started. The full design is in docs/SPEC.md. It is normative; the conformance fixtures under testdata/conformance are the tie-breaker where prose is ambiguous.

Contents

Install

macOS (Apple silicon), Linux and Windows, amd64 or arm64. On macOS and Linux:

curl -fsSL https://raw.githubusercontent.com/ubgo/docsync/main/install.sh | sh

It downloads the newest ds release for your machine, checks it against the release's checksums.txt, and installs ds and its six secret-resolver plugins (ds-resolve-aws, -env, -gcp, -github, -onepassword, -vault) to /usr/local/bin, using sudo if it needs to. No Go toolchain is involved. Options go on the sh side of the pipe:

curl -fsSL https://raw.githubusercontent.com/ubgo/docsync/main/install.sh | INSTALL_DIR=$HOME/.local/bin sh   # no sudo
curl -fsSL https://raw.githubusercontent.com/ubgo/docsync/main/install.sh | VERSION=v0.1.8 sh                # a specific release

On Windows (PowerShell), which installs to %LOCALAPPDATA%\ds and adds it to your PATH:

irm https://raw.githubusercontent.com/ubgo/docsync/main/install.ps1 | iex

With Go 1.26 or later and a C compiler (ds includes the tree-sitter parsers, which are C). This is the way to install on an Intel Mac, for which there is no prebuilt binary:

go install github.com/ubgo/docsync/cli/cmd/ds@latest
go install github.com/ubgo/docsync/cli/cmd/ds-resolve-aws@latest   # a secret-resolver plugin, only if you use `ds check --resolve`: -aws -env -gcp -github -onepassword -vault

Or download an archive from the releases page: the ds/v… releases carry ds and the resolver plugins in one archive for darwin/arm64, linux/amd64, linux/arm64, windows/amd64 and windows/arm64, with a checksums.txt. The Windows builds are run on real Windows machines by .github/workflows/windows.yml (the Go tests of every module, then scripts/windows-smoke.ps1 against the published binary); ds:run and ds review --ai run their commands under sh, which Git for Windows provides; name another shell with [run] shell = "pwsh", and if the shell is missing ds says so and stops rather than skipping. Check what you are running with ds version.

To update, run ds update, which installs the newest release and its plugins after the same checksum check. A release build also checks once a day by itself, before a command run at a terminal, and installs a newer release first; it stays out of CI, piped output and --json, and DS_UPDATE=notify or DS_UPDATE=off turns it down (ds update).

The library, for Go programs that embed docsync (standard library only):

go get github.com/ubgo/docsync@latest
go get github.com/ubgo/docsync/ext/structured@latest    # YAML, TOML, HCL tier
go get github.com/ubgo/docsync/ext/treesitter@latest    # Go, TypeScript, Python, SQL tier (cgo)

In CI, the GitHub Action installs ds itself: uses: ubgo/docsync/integrations/github@main (see integrations/github). For pre-commit, install ds first, then add:

- repo: https://github.com/ubgo/docsync
  rev: v0.1.0   # any tag or commit; the hooks run the ds on your PATH
  hooks: [{id: docsync-impact}, {id: docsync-literals}]

Why docsync?

An AI coding agent can rewrite a function in seconds; the paragraph that explains it stays exactly as it was, and nothing tells anyone. Code review sees the diff, not the page three directories away that now describes behaviour that no longer exists. docsync closes that gap: a sentence cites the code it is about, and when that code changes, ds check names the sentence, shows the diff, and prints the command that clears it — for a human, or for the agent that made the change.

  • Cites by id, not by path and line. A block is marked once in the file that owns it; the id travels with it through moves, renames and reformatting, so a doc keeps pointing at the right code.
  • Nothing is copied. Values, code snippets and permalinks are rendered from live source at build time, so a doc cannot hold a stale copy.
  • Every finding says what to do. Each one carries the diff since the sentence was last approved and the exact command to approve it or the place to edit it.
  • An approval is for one sentence, and it is recorded. An ack names the sentence, the block's hash, and who approved it; rewriting the sentence needs a new one. Over MCP an agent may ack only by naming the human who delegated it.
  • Built for agents. ds mcp gives agents map, find, read, context, impact, check, def and ack, with every payload fenced so repository text is never read as an instruction; ds init --agents wires it up.
  • Boring core. The library is Go's standard library only, never writes a file or opens the network itself, holds 100% statement coverage, and is specified by a conformance suite rather than by its own behaviour.

What is here

This repository is the root library: github.com/ubgo/docsync, standard library only, pure over its inputs. It never writes a file, reads the environment, or touches the network. A CLI, an MCP server, and the tree-sitter and structured-format extractors are separate modules that build on it.

Package Holds
directive the one directive grammar: ds:<verb> key=value…, comment and link carriers, continuation folding, parse and format round-trip
id ids as <label>-<8-char suffix>; the suffix is the identity, the label is a hint
block blocks, references, kinds, carriers, stability policies, change classes
extract the built-in tiers: markdown and HTML, AsciiDoc and reStructuredText (the document tier), config (yaml, toml, ini, env, properties), heuristic code, plain text; Locate for binding a symbol or line without a directive; exported hooks (ScanCode, Prelude, NewDef) for grammar tiers outside the root
pick pick= schemes for narrowing a block to one value or range
sentence binding a link to the sentence, list item, or table cell that contains it
scan walking an fs.FS under globs, remote file= defs, duplicate detection
ledger the committed TSV state: ledger, reverse index, append-only acks
match previous ledger against current scan: ok, moved, changed, moved-unmarked, rewritten, deleted, new; change classification
check the findings engine: every state in the spec's table with severity, remedy, and exit code
render build-time expansion of directives to permalinks, values, code, chains, tables
config .ds/config.toml in the TOML subset the spec uses
procplugin both sides of the process-plugin protocol for ds-pick-*, ds-resolve-*, ds-records-*, ds-<verb> executables
workspace merging published ledgers and refs across repositories, the index layout, JUnit outcomes for assert=
records the frontmatter record source for ds:table: a directory of markdown files, filtered by kind, where, sort, cols, limit
ext/structured (own module) the structured tier: YAML (yaml.v3), TOML (go-toml), and HCL (hcl/v2) extractors that bind keys, tables, blocks, and attributes by parsed extent; structured.All() registers the three
ext/treesitter (own module, cgo) the syntax tier: Go, TypeScript, TSX, JavaScript, Python, and SQL through tree-sitter; a def binds to the next declaration as the grammar sees it, nested declarations become Class.method, single literals are values, and hashes are taken over the token stream so formatters never cry wolf; treesitter.New(Grammar{…}) registers any other grammar
ext/records/sqlite (own module) the SQLite record source for ds:table on modernc.org/sqlite (pure Go); kind= names the table, the rest of the query is the shared records language
integrations/github the composite GitHub Action and the three workflows from spec §24: pull request comments, publish after merge, the nightly job that alone enables --run and --resolve
integrations/hugo, integrations/docusaurus, editors/vscode, .pre-commit-hooks.yaml the Hugo module reading ds export hugo data, the Docusaurus remark plugin over ds render plus freshness from ds status --json, the VS Code client for ds lsp, and pre-commit hooks for impact --staged and report --literals (at the repository root, which is the only place pre-commit looks)
root New with functional options; Scan, Check, Snapshot, Render, Define (returns an Edit), Facts, Why, Map, Context, Impact, Find, Read, Ack, Triage, Audit, Rename, Graph, Blame, Report, Adopt

Using the CLI

ds init                   # .ds/config.toml, empty ledgers, a CI snippet; scan.docs is docs/** + README.md if docs/ exists, else **/*.md
ds def internal/store/write.go#Store.Save --owner @auth   # prints the id; inserts the directive
ds scan                   # writes .ds/ledger.tsv and .ds/refs.tsv; commit them
ds check                  # exit 1 on unacked, broken, expired…; --json for machines
ds ack <id> --doc docs/a.md --line 12 --note "still true"
ds render docs/a.md       # plain markdown with permalinks, values, and live code
ds map | ds context docs/a.md --since ack | ds facts | ds why <id> --chain --history | ds impact
ds triage                 # group unacked findings by diff; --ack-group N acks one group with one note
ds adopt                  # turn hand-written path#L10-L20 links into defs and cites; ds undo reverses it
ds report --gaps          # what to document next; --literals finds facts typed by hand
ds audit | ds blame docs/a.md 12 | ds graph --dot | ds rename sess session
ds publish --tests junit.xml   # default branch only: this repo's ledger, refs, and test outcomes into the workspace index
ds sync                        # fetch the index; ds check does this first when `workspace` is set in .ds/config.toml
ds mcp                         # the agent surface over MCP on stdin/stdout: map find read locate facts why context check impact def ack
ds check --run --resolve       # execute ds:run where allowed; check ds:url with a TTL cache; ask ds-resolve-<provider> plugins about secret addresses
ds render docs/a.md --at v1.2  # the page and its blocks as they were at a commit
ds impact --staged             # pre-commit: only what the staged files will flag
ds ack --from-commit HEAD      # honour `ds:ack id=…` written in the commit message (your configured prefix, if not ds)
ds notify                      # digest open findings per owner; Slack webhook when notify.slack is set
ds lsp                         # editor server: code lens with dependents on every def, hover with block and diff, go-to-definition, rename/delete warnings
ds review                      # the review worklist; --ai pipes it as JSON into [review] command and prints the patch it returns; never acks
ds check --expand --full       # list every finding instead of collapsing heavily cited ids; bypass the extraction cache
ds github comment              # in Actions: one PR comment per doc with findings and diffs; applying the docs-acked label acks them for the reviewer who applied it
ds export hugo --out data/docsync   # blocks.json and status.json for the Hugo module; Docusaurus uses the remark plugin instead
ds publish --branch            # publish a release branch under repos/<name>/@<branch>; docs select it with branch=
ds repair                      # mend directives an older build left bare: comment them, or delete them where the format has no comments; --apply writes, ds undo reverses it
ds version                     # the commit this binary was built from, and whether the tree was dirty; --json for machines
ds init --agents               # CLAUDE.md and AGENTS.md rules, a Claude Code skill, .mcp.json, and a SessionStart hook loading `ds map`

Over MCP, def is capped per session by agents.max_defs_per_run, ack is accepted only with delegated_by and is recorded as an agent ack, and run, resolve, undo, publish, and adopt are not exposed. Every payload sits behind a data: delimiter so scanned text is never read as an instruction.

docsync on docsync

The repo tracks its own docs, under prefix = "dsself" in .ds/config.toml. The distinct prefix is not decoration: docsync's source and fixtures are full of ds: directives written as examples, and with the default prefix the scanner reads them as real, so the tool could not be pointed at itself at all. task dogfood (part of task) runs ds check --full with the binary built from the tree, and it checks rather than scans, so the gate never rewrites committed state. Updating a baseline stays a deliberate ds scan plus an ds ack.

What it binds today is small on purpose: the claims in these docs that restate a value in the code, such as the machine-local ignore list below and the extraction-rule constant. Adding one is ds def <file>#<symbol> --owner @docsync --stability stable, then an HTML comment <!-- dsself:block id=… --> above the sentence that depends on it. Pick the stability from what you want to hear about — api deliberately stays silent on a body-only change, which is the right answer for a signature and the wrong one for a list whose contents are the contract.

It has already paid for itself twice. The extraction cache was keyed on file content alone, so editing prefix served entries extracted under the old one and reported defs and citations that existed nowhere in the tree; and an unrecognised stability= fell back to stable in silence, although the code comment claimed the scanner reported it, so a typo in frozen quietly relaxed the policy the author asked for.

What in .ds/ is committed

.ds/ holds two kinds of file and they are not interchangeable. Commit config.toml, ledger.tsv, refs.tsv, acks.tsv, blocks/ and, once a workspace is set up, foreign.tsv: these are shared facts, and a check is only reproducible on another machine because they are in the tree. Never commit cache/, index/, journal.tsv, urls.json, runs.json, notified.json, hashes.json, metrics.json or lock (the file concurrent ds commands lock while they write): they are per-machine and would conflict on every merge, and two of them are worse than noise — cache/ stores extracted block bodies and journal.tsv stores the source lines an edit replaced, so on a repo with [secret] paths either can hold the value of a secret block, in history, where rotating the credential cannot remove it.

ds init writes a .ds/.gitignore covering exactly that second list, so a repo started with it is correct by default. A repo initialised before a name was added to the list is not, and that is what ds doctor checks: the gitignore row names the missing lines, and adding them is the whole fix — never init --force, which would overwrite the config and the ledgers to repair a one-line omission. If a local file was already committed before the line existed, git rm --cached it as well; the ignore alone does not untrack what git is already tracking.

ds init also writes .ds/.gitattributes with acks.tsv merge=union. The ack log is append-only, so without it any two branches that each record an ack conflict when merged; with it git keeps both sides' rows, and the newest ack for a sentence wins by its timestamp. Nothing else in .ds/ is union-merged, because the other files are rewritten, not appended. ds doctor checks for the attribute, and a merge that still conflicts is reported as a conflict with the right remedy for that file — for refs.tsv, keep both sides.

ds doctor reports config, globs that match nothing, extractors, ledger format, the extraction rule it records, and git, and exits non-zero when any row is FAIL. Every command is a thin call into the library; the cli package exports Main and Run so an organisation can ship its own binary with extra extractors and verbs.

Using the library

sys, err := docsync.New(
    docsync.WithFS(os.DirFS(repo)),
    docsync.WithConfig(cfg),               // parsed by you; the library never reads files it was not given
    docsync.WithPrevious(ledger, refs),    // the committed state to diff against; zero for a first run
    docsync.WithAcks(acks),
    docsync.WithOldContent(gitShow),       // func(ledger.Row) (string, bool): the body a previous row had, for diffs and change classes
)
report, err := sys.Check(ctx, docsync.CheckOptions{})
for _, f := range report.Findings {
    fmt.Println(f.State, f.Doc, f.Line, f.Remedy)
}
res, err := sys.Define(ctx, "internal/store/write.go#Store.Persist", docsync.DefineOptions{Owner: "@auth"})
// res.Edit is a proposed change to the source file; you decide whether to apply it

Every operation returns values. The caller applies edits, touches git, and prints.

Development

Requirements: Go 1.26, Task, gofumpt, staticcheck, and a C compiler for the tree-sitter tier.

The repository holds several Go modules: the root library, cli, ext/structured, ext/treesitter, ext/records/sqlite and integrations/hugo. The root go.work joins them, so a change in the library is seen by the cli build without a release. Each module's own go.mod requires the others at published versions, which is what go install and go get read.

task install            # build ds and the resolver plugins into bin/, symlinked onto PATH (DEST=~/.local/bin)
task uninstall          # remove those symlinks
task                    # the full gate: fmt, vet, staticcheck, race tests, 100% coverage, e2e, guide examples, corpus, extract:diff
task docs:test          # run every example in docs/guide against the built ds (PAGE=../docs/guide/<page>.md for one)
task conformance        # only the conformance suite and the JSON contract goldens
task conformance:update # regenerate expectations after an intentional rule change, then read the diff
task fuzz -- ./directive FuzzParse

task install symlinks rather than copies, so a later task cli:build is live immediately with no reinstall; it refuses to overwrite anything in the destination that is not already one of its own links, and task install DEST=/some/dir picks another directory.

Every package holds 100% statement coverage and the gate fails otherwise. CONTRIBUTING.md lists every check task runs and the class of bug each one catches, and how the guide pages are tested. A rule without a conformance fixture is not yet a rule: TestConformanceStatesCovered fails when a finding state has no fixture producing it.

Releasing

Releases are cut with volt (volt status lists what is unreleased). Modules release in dependency order, because a module's go.mod may only require versions that already exist:

  1. the root library: volt release . vX.Y.Z (tag vX.Y.Z);
  2. ext/structured, ext/treesitter, ext/records/sqlite after their go.mod requires the new root version: volt release ext/structured vX.Y.Z and so on (tags ext/structured/vX.Y.Z);
  3. cli after its go.mod requires those: volt release cli vX.Y.Z, then the binary, volt release cli/cmd/ds vX.Y.Z (tag ds/vX.Y.Z).

Pin with GOWORK=off go mod tidy in the module, so the checksum lines come from the published versions rather than the workspace. cli/cmd/ds/.volt.yml builds ds with cgo, using zig cc for Linux and Windows and the host compiler for darwin/arm64 (the release is cut on an Apple silicon Mac, with volt v0.2.0 or later); an Intel Mac binary cannot be cross-built with cgo, which is why there is none. Its extra_binaries packs the six resolver plugins into the same archive, so a release that changes only ds or only a plugin is still one volt release cli/cmd/ds. install.sh and install.ps1 are generated by volt gen install cli/cmd/ds; do not edit them by hand.

Limits

What docsync cannot do, stated here rather than discovered in use. Each of these is a real boundary, not a todo list; where there is a workaround it is named.

It cannot tell you that meaning changed while text did not. The whole mechanism is a hash over bytes. A comment rewritten to say the opposite of what it said, a constant renamed with its value kept, a function whose contract changed without its signature changing — none of these flag, because none of them change what is hashed. stability = frozen is the blunt instrument for a block where this matters: it flags on any change at all, including a comment.

stability = api does not flag a body-only change. That is the point of it — a signature is the contract and the body is not — but it is the wrong choice for a block whose contents are the contract, such as a list of allowed values or an ignore list. Use stable there. The full table is section 20 of the spec, and the cost of choosing wrong is silence, not noise.

Symbol lookup is less precise than scanning. ds def path#Name, ds adopt and ds locate resolve names through a line-based matcher in the root library, which imports only the standard library and so has no grammar. Scanning uses the tree-sitter tiers. So a name that binds cleanly on a scan may not be findable by #Name, and the two disagree on the classification of a line more often than is comfortable. Concretely: the grammarless matcher will not synthesise a name for a body member, because the leading identifier is the name in Go and TypeScript and a modifier in the C family — private int x; would become a declaration called private, and path#Name would then bind the wrong line. Lookup therefore compares against the name you asked for, which can be right or absent but never wrong.

A directive is written only where it will bind. ds def applies its edit in memory and extracts the result with the same tier a scan uses; if the new id does not bind cleanly, it refuses with that tier's reason and writes nothing, so --dry-run predicts the refusal instead of a scan discovering it after the file was edited. A computed key in an object literal ([k]: 1) binds but has no name, since there is nothing a reader could search for. A directive that cannot bind the declaration directly below it reports ds:def cannot bind the declaration below it rather than binding something further down.

A file with no comment syntax cannot hold a directive. JSON, CSV, lockfiles and any type docsync has no carrier for are refused with no comment carrier for this file type and left byte-for-byte untouched; bind a value in one from a file that can carry a comment, with a remote def (file=config.json pick=json:$.port). This is a refusal rather than a fallback because the fallback shipped once — an unrecognised type received the directive as a bare line, which broke a Go workspace's build and would have made a JSON file unparseable. If your repository was touched by that, ds scan reports every bare directive sitting in such a file, with the file and line, and ds repair --apply mends them all -- commenting each one where the format allows, deleting it where it does not -- reversibly, through ds undo. Plain text is the one exception: a .txt file carries a directive as a line of its own, which is what the text tier reads.

Remote defs do not follow a moved target. A def pointing at another file with file= hashes the extracted value and reports pick failed when the key disappears, but it will not notice that the key moved and re-bind. Sidecar mode — keeping directives out of source entirely — inherits that weakness in full.

Secret and local blocks are checked without being read. A block matched by [secret] paths, or carrying secret= or local=, publishes a hash and no body. A change to one is therefore reported as class unknown rather than described, and unknown flags wherever api flags: you learn that something changed and not what. The same applies after ds prune removes a body an old baseline still referenced — the 30-day grace period makes that unlikely, not impossible.

An extraction-rule change makes affected citations report drift once. What bytes a block covers is versioned (extract= in the ledger header). When that version bumps, the citations of every affected block report as changed and are acked once with a note; there is no automatic re-baselining, because proving a stored hash was in sync before the change needs the old bytes and the old rule, and a repo cannot be assumed to have either. task extract:diff exists so a rule change is a decision rather than a surprise.

The evidence that an upgrade is safe covers only the repositories it was run against. task extract:diff compares two builds block by block and fails when a block naming the same symbol over the same lines hashes differently — but it can only compare the trees in CORPUS, which defaults to the sibling repositories present on the machine. A repository with defs on constructs neither corpus contains could still move. Widen it before a release: CORPUS=/path/a:/path/b task extract:diff. That is the honest limit of the claim, and no amount of local green changes it.

ds version reports only what the toolchain recorded. It reads the revision and a dirty flag the Go toolchain stamps into every build made inside a git checkout, so a binary built outside one says unknown for those fields rather than guessing. The development build is a symlink into this repo's bin/, so ds version saying dirty is how you tell a running binary does not match any commit — and task corpus still refuses to run when a source file is newer than the binary, because a stale build has produced misleading results here more than once.

Notify escalation moves only when the nightly runs. The shipped nightly workflow is manual by default, for the reason below. ds notify still keeps its dedupe memory between runs through the Actions cache, but escalate_after is judged when a run happens, so an item escalates at the first run after its interval, not on the day the interval ends. Restore the schedule from the workflow's header where minutes are not metered.

The CI template is not wired for metered runners. ds init writes .ds/ci-github.yml with push and pull_request triggers. On a private repository those minutes are billed, and a workflow that fails for billing teaches everyone to ignore the gate. Change the triggers to workflow_dispatch: before copying it into .github/workflows/, and treat the local task as the real gate.

Documenting a tool whose subject matter is directives needs a different prefix. docsync's own source is full of ds: directives written as examples; scanning it with the default prefix read 40 of them as real. Set prefix in .ds/config.toml to something else, as this repo does with dsself.

Status

The library is at v0.1.6 and the ds binary at v0.1.8 (see CHANGELOG.md). Every module is built and gated at 100% statement coverage: the root library, the cli module with init doctor def scan check ack refresh render map context facts why find read locate impact status triage audit rename graph blame report adopt undo publish sync mcp lsp notify review github export update, the ext/structured tier (YAML, TOML, HCL), the ext/treesitter tier (Go, TypeScript, TSX, JavaScript, Python, SQL), the ext/records/sqlite source, and the JavaScript integrations (Docusaurus plugin, VS Code client) under node --test with 100% line, branch, and function coverage. Secret resolvers ship as process plugins under cli/cmd/: ds-resolve-github (existence through gh), and ds-resolve-onepassword, ds-resolve-aws, ds-resolve-gcp, ds-resolve-vault (existence and a hash through op, aws, gcloud, vault; the value never leaves the plugin), and ds-resolve-env (a hash of an environment variable in the environment ds runs in); any provider can add one in any language by speaking the procplugin protocol. Scale features are in: parallel extraction with a persisted cache under .ds/cache/ (check --full bypasses it), a per-directory sharded ledger with [ledger] shard = true, collapsed findings for heavily cited ids, and report --metrics printing bytes served to agents against source bytes plus busy id prefixes. Repo mode (include.mode = "repo"), the HTTP record source, fork pull request refusal of --run and --resolve, and doc rename detection keeping acks are built too.

A citation keeps its baselines when it moves — a line inserted above it, its doc renamed — by matching the sentence it sits in, and where a move is ambiguous every candidate takes the baseline that still reports a change. A file the scan cannot read (too large, binary, or a line past the limit in a tier that is not prose) keeps its last recorded state and fails the check as unscanned rather than taking its citations with it. Coverage of a citation is decided against that citation's own baseline — the hash it was acked at, or the hash recorded the first time it was seen — and never against the scan-to-scan change table. Blocks bodies are kept content-addressed under .ds/blocks/ and, for a workspace, under repos/<name>/blocks/ in the index, so a change can still be classified when its baseline is many scans old or lives in another repository; secret and local blocks publish a hash and no body, and a body the store cannot supply yields the class unknown, which flags wherever api flags. refs.tsv is format = 2 (it gained seen_hash); format = 1 files are still read and are upgraded by the next scan. A citing repo also commits .ds/foreign.tsv, the snapshot of the foreign blocks it cites, written by ds sync; ds check --frozen resolves against it without syncing, so the same commit gives the same answer on any machine and with no network, and it is the default when CI is set. ds status reports how far that snapshot is behind upstream, per repo and per block with the change class, without ever changing the exit code, and [check] snapshot_max_age adds an opt-in warning. ds prune removes block bodies no ledger, ack, citation, or snapshot still needs, with a 30-day grace period, a --dry-run, an --index mode gated on the default branch and a fresh sync, and a targeted --hash … --force for a value that should never have been stored.

Where the implementation extends the spec

These are deliberate and recorded so the spec can catch up rather than drift:

  • The .ds/ directory is never scanned, since the ledger, refs, acks, and journal quote directives and would define and cite themselves.
  • triage groups diff-less findings by block id, so a first run without git history still yields one decision per block.
  • Merged workspace references carry doc_repo in findings, additively, so a repo can tell which other repo's page depends on its block.
  • Config keys can be one def per environment: duplicates are keyed by id and env= together.
  • ds:run outcomes live in .ds/runs.json, url checks in .ds/urls.json, notify state in .ds/notified.json, stored truth hashes in .ds/hashes.json, and the source-write journal in .ds/journal.tsv; the spec named the behaviours and left the files to the implementation.
  • Repo-mode copies end with <!-- /ds:block hash=… --> carrying the short block hash, which is how check tells stale (source moved on) from tampered (copy edited) without storing anything else; permalinks inside a committed copy are relative to the page and carry no commit, so a new commit does not touch every copy.
  • Secret resolvers are process plugins only (ds-resolve-<provider>); resolve.providers in config limits which are consulted, and a plugin reply carrying anything but existence and a hash is refused by the host.
  • The MCP server lives in the cli module (ds mcp) rather than a separate mcp module; it is a thin JSON-RPC loop over the same library calls and would only add a module boundary today.
  • The HTTP record source lives in the cli module, since the root must stay free of net/http; the SQLite source is its own module because of its driver.
  • The extraction cache carries block content and region text beside the public JSON shape (which hides both) so a cache hit is byte-identical to a fresh extraction; a test pins the set of hidden fields.
  • ds github comment under the docs-acked label records the acks itself (actor github:<login>) and prints what to commit; the spec left how the reviewer's ack --all is recorded to the implementation. The ack happens only on the labeled event, credited to whoever applied the label: a later push to a pull request that still carries it acks nothing and says to re-apply it, because the sender of a push is its author, and acking there let every change after the review through unreviewed.
  • Hugo cannot run a program at build time, so ds export hugo writes the rendered blocks and freshness as data files and the Hugo module reads them from a shortcode and a partial; Docusaurus, which can, calls ds render from a remark plugin.

FAQ

Does docsync send my code or docs anywhere? No. The library never opens the network, and scanning, checking, acking and rendering work offline. The CLI reaches the network only for features you turn on: syncing a shared workspace index through git, check --resolve for URLs and secret-existence plugins, ds notify posting to a Slack webhook you configure, ds github comment commenting on a pull request inside GitHub Actions, and a ds:table record source you point at an HTTP URL.

Does it need an API key or an account? No. It is a local binary and a Go library. ds review --ai can pipe its worklist to a command you choose, but nothing calls a model on its own.

Which AI coding agents does it work with? Any agent that speaks MCP can use ds mcp. ds init --agents writes CLAUDE.md and AGENTS.md rules, a Claude Code skill, .mcp.json, and a hook that loads ds map at the start of a session.

Can an agent approve its own changes? Not silently. Over MCP, ack is refused unless it names the human who delegated the run, and it is recorded as an agent ack under that name; run, resolve, undo, publish and adopt are not exposed to agents at all.

What happens when I move or rename code? The id is in a comment beside the code, so it moves with it; ds check reports the citation as moved rather than broken. A citation that moves inside a doc, or a doc that is renamed, keeps its approval too.

Which languages and formats does it understand? Docs in Markdown, HTML, AsciiDoc and reStructuredText; code in Go, TypeScript, TSX, JavaScript, Python and SQL through tree-sitter, and any other language through a heuristic tier; config in YAML, TOML, HCL, INI, .env and properties files.

How is this different from reviewing docs by hand? A reviewer sees the files in a diff, not every page that describes them. docsync knows which sentences cite which blocks, so a change to a block names each sentence that depends on it, across the repository and across repositories that share a workspace.

Will it slow down CI? A check reads only the files that changed since the last run: on a generated repository of 100,000 files an incremental ds check takes under a second, and task perf measures it against the targets in the spec.

License

docsync is licensed under the Apache License 2.0. You may use, modify and distribute it, including in closed-source and commercial work, as long as you keep the license and copyright notices; the license also grants a patent license from contributors.

Documentation

Overview

Package docsync is the library behind the `ds` tool: it keeps documentation bound to the code, configuration, and facts it describes, and reports when the thing behind a sentence changed (docs/SPEC.md).

The package is stdlib-only and pure over its inputs (§37): a System is built from an fs.FS, a parsed config, the previous ledger, and optional hooks; every operation returns values and never writes a file, reads the environment, or touches the network. The CLI owns paths, git, and printing.

Index

Constants

View Source
const (
	ModeAuto  = "auto"
	ModeFull  = "full"
	ModeDiff  = "diff"
	ModeValue = "value"
	ModeLine  = "line"
)

Content modes for Context items (§26.4).

View Source
const (
	ReasonOverBudget = "over budget"
	ReasonUnchanged  = "unchanged since ack; over budget"
)

Omission reasons.

View Source
const (
	NodeDef  = "def"
	NodeDoc  = "doc"
	EdgeCite = "cites"
	EdgeFrom = "from"
	EdgeCov  = "covers"
	EdgeAbt  = "about"
)

Graph node and edge kinds (§22 `ds graph`).

View Source
const BytesPerToken = 4

BytesPerToken is the estimate behind every `tokens` field (§26.2): four bytes per token is the usual English-and-code average and errs high for prose, so a budget is never exceeded by the estimate being optimistic.

View Source
const JSONFormat = 1

JSONFormat is the version of the JSON contract (§26.2). It changes only additively within a major version.

View Source
const SinceAck = "ack"

SinceAck asks Context for diffs since the last ack; any other non-empty Since value is a commit, answered through ContextOptions.OldBody.

View Source
const SpecVersion = "1.0"

SpecVersion is the specification this library implements (§37.7).

View Source
const TriageThreshold = match.DefaultFuzzyThreshold

TriageThreshold is the diff similarity at or above which two unacked findings are one decision (§19 `ds triage`). It reuses the fuzzy-match ratio so "similar" means the same thing everywhere in the tool.

Variables

View Source
var (
	ErrNoFS               = errors.New("docsync: WithFS is required")
	ErrNotFound           = errors.New("docsync: not found")
	ErrDelegationRequired = errors.New("docsync: an agent ack needs delegated_by (§26.7)")
	// ErrDelegateNotOwner refuses an agent ack whose delegated_by is not a
	// person listed in [owners] (§26.7): the delegate is who judged, and a
	// name nobody vouches for records no judgment at all.
	ErrDelegateNotOwner = errors.New("docsync: delegated_by must name a person listed in [owners] (§26.7)")
	ErrNoReference      = errors.New("docsync: no reference at that doc line")
	// ErrNoCarrier is returned instead of writing a directive into a file whose
	// type has no comment syntax in extract.Styles. It exists because the
	// alternative that shipped was writing the directive bare, which is invalid
	// syntax in every such format.
	ErrNoCarrier = errors.New("docsync: no comment carrier for this file type")
	// ErrGeneratedPath refuses to mint a def in a file under [scan] generated
	// (Part VII: "Generated paths refuse minting"). The next generation
	// rewrites the file and the directive with it, so the id would vanish
	// while every sentence citing it stayed; scan reports such a def after
	// the fact (scan.ErrDefInGenerated), and minting one is how it gets there.
	ErrGeneratedPath = errors.New("docsync: generated file; a def written here is overwritten by the next generation")
	// ErrWouldNotBind is returned instead of writing a directive the scan
	// could not bind. Define checks by extracting the edited source in memory
	// with the scan's own extractor, so the refusal comes before the file is
	// touched and `--dry-run` reports it too.
	ErrWouldNotBind = errors.New("docsync: the directive would not bind")
	// ErrRange is a `path:start-end` target that no def can bind exactly:
	// the tier's block from start ends elsewhere and no span= moves it to
	// end. Refused rather than narrowed, because the old reading -- the
	// first line only, silently -- defined a block nobody asked for.
	ErrRange = errors.New("docsync: no def binds exactly that line range")
	// ErrBadLabel is a rename target that is not a label: lowercase
	// words joined by single dashes. A label with a space was written into
	// every def and citation, and each came out as a broken directive.
	ErrBadLabel     = errors.New("docsync: not a label")
	ErrHashMismatch = errors.New("docsync: reference is not to that id")
	// ErrBadLines is a `lines` fragment outside the grammar a citation
	// uses: `a` or `a-b`, digits only, 1 <= a <= b. A well-formed range
	// past the block's end is ErrNotFound instead: those lines do not exist.
	ErrBadLines = errors.New("docsync: bad lines")
	// ErrNoPageReview is a page review requested for a doc that declares
	// no review_every, or that the scan does not know.
	ErrNoPageReview = errors.New("docsync: page has no review_every to record a review against")
	// ErrNewerRule refuses this repo's ledger or refs when they record an
	// extraction rule newer than this build implements (§33 Versioning: an
	// older tool refuses a newer format by name, never by misparsing). Those
	// hashes cannot be compared with the ones this build computes: reading them
	// would report unchanged blocks as drift, and a scan would rewrite the
	// files under the older rule. The remedy is a newer ds.
	//
	// Pre-release builds stamped extract=2 to 4 for what is now rule 1 (the
	// counter was reset for the first release). Such a repository sees this
	// refusal once; setting extract=1 on the header line of .ds/ledger.tsv and
	// .ds/refs.tsv, then running `ds scan`, is the whole fix.
	ErrNewerRule = errors.New("docsync: recorded under a newer extraction rule than this build implements")
	// ErrOtherRule reports a ledger or refs recorded under an older extraction
	// rule, which is the upgrade path: unchanged blocks can report drift until
	// the next scan restamps both files, so hosts warn rather than refuse.
	ErrOtherRule = errors.New("docsync: recorded under an older extraction rule; unchanged blocks may report drift until the next scan")
	// ErrUnknownEnv refuses an environment name that [env] known does not
	// list, when it lists any. A misspelled --env used to run the whole
	// command against an environment nothing defines, and every citation
	// then reported a missing definition instead of the one typo.
	ErrUnknownEnv = errors.New("docsync: environment not in env.known")
)

Sentinel errors.

View Source
var ErrContextMode = errors.New("context: unknown mode")

ErrContextMode is a ContextOptions.Mode outside ModeValues. It used to be accepted and served as a bare location, so a typo returned less than asked for without a word.

View Source
var ErrNoStore = errors.New("docsync: no store configured (WithStore)")

ErrNoStore is returned by SaveState when no Store was given.

View Source
var ErrSinceCommit = errors.New("context: since a commit needs OldBody, the body of a block at that commit")

ErrSinceCommit is a Since that names a commit with no OldBody hook to read the commit. Since was a commit nothing ever read: any value was accepted and the diff was the one since the last scan (bug 61).

ModeValues is the canonical list callers may pass.

Functions

func CheckRule

func CheckRule(l ledger.Ledger, r ledger.Refs) error

CheckRule compares the extraction rule the ledger and refs record with extract.Rule. A newer rule is ErrNewerRule, which WithPrevious refuses; an older one is ErrOtherRule, which hosts print as a warning (`ds check`, `ds scan`, `ds doctor`). Both name the file and the two rules. It is exported so a host that inspects the state without building a System (`ds doctor`) reports the same thing.

The ack log is not checked. Its header is written once and never restamped, so its rule says nothing about its rows, and each row carries its own rule, which check already reports by name ("ack was recorded under sentence rule N", section 18).

func LineCR

func LineCR(line string) string

LineCR returns the "\r" a line split on "\n" ends with, or "". It is how a source write keeps each line's ending as it found it.

func Tokens

func Tokens(s string) int

Tokens estimates the token count of s.

Types

type AckRequest

type AckRequest struct {
	ID          string
	Doc         string
	Line        int
	Env         string
	Actor       string
	ActorKind   ledger.ActorKind
	DelegatedBy string
	Note        string
	// Claim renews the claim on Doc:Line rather than acking a citation:
	// ID is then ignored, and a line with no claim is ErrNoReference.
	Claim bool
	// Preview builds the row a real ack would record and notifies no
	// observer, for `ack --dry-run`: an embedder watching acks must not be
	// told of one that was never written.
	Preview bool
	// Page records that the whole of Doc was reread, for its review_every
	// schedule: ID and Line are ignored, and the row carries no hashes.
	// Before, a page review could only be recorded by acking a citation on
	// the page, so a page with review_every and nothing cited had a finding
	// nothing could clear.
	Page bool
}

AckRequest describes one approval (§19, §26.7).

type AdoptResult

type AdoptResult struct {
	Envelope
	// Edits insert defs into source and rewrite doc links to cites. Source
	// edits come first so the ids exist before the cites do.
	Edits []Edit `json:"edits"`
	// Adopted counts links converted.
	Adopted int `json:"adopted"`
	// Unresolved lists links whose target could not be located, with why.
	Unresolved []Unresolved `json:"unresolved"`
}

AdoptResult is what Adopt proposes.

type AuditOptions

type AuditOptions struct {
	Since     time.Time
	ActorKind ledger.ActorKind
	ID        string
}

AuditOptions filters the ack log.

type BlameResult

type BlameResult struct {
	Envelope
	Reference block.Reference `json:"reference"`
	Block     block.Block     `json:"block"`
	Change    match.Change    `json:"change"`
	Finding   check.Finding   `json:"finding"`
	Acks      []ledger.Ack    `json:"acks"`
}

BlameResult is the block behind a reference and what happened to it.

type CheckOptions

type CheckOptions struct {
	Strict bool
	// Env overrides env.default for this run.
	Env string
	// Run and Resolve say the caller will execute directives and reach
	// providers; the library itself still runs nothing.
	Run     bool
	Resolve bool
	// Full ignores the extraction cache.
	Full bool
}

CheckOptions selects the passes of `ds check` (§16, §22).

type Classifier

type Classifier = match.Classifier

Classifier replaces change classification (§20); it is match.Classifier.

type ContextItem

type ContextItem struct {
	Rank    int    `json:"rank"`
	Why     string `json:"why"`
	ID      string `json:"id"`
	File    string `json:"file"`
	Lines   [2]int `json:"lines"`
	Tokens  int    `json:"tokens"`
	Mode    string `json:"mode"`
	Content string `json:"content"`
}

ContextItem is one ranked piece of context.

type ContextOptions

type ContextOptions struct {
	Budget int
	// Since is the baseline a diff is taken against: "" (the last scan),
	// SinceAck (the last ack, the same diff `check` reports), or a commit,
	// which needs OldBody.
	Since string
	Mode  string
	// OldBody returns a cited block's body at the commit Since names, in
	// the form the scan gives it (false when the block did not exist
	// there). The library reads no history; the caller's VCS does.
	OldBody func(b block.Block) (string, bool)
}

ContextOptions bounds `ds context` (§26.4).

type ContextResult

type ContextResult struct {
	Envelope
	Target       string        `json:"target"`
	BudgetTokens int           `json:"budget_tokens"`
	UsedTokens   int           `json:"used_tokens"`
	Items        []ContextItem `json:"items"`
	Omitted      []Omitted     `json:"omitted"`
}

ContextResult is the bundle.

type CopyNote added in v0.1.6

type CopyNote struct {
	Doc  string
	Line int
	ID   string
	// Skipped is true when no copy was written; false for a copy written
	// with a caption only.
	Skipped bool
	Reason  string
}

CopyNote is one thing refresh has to say about a repo-mode copy: that it was skipped, and why, or that it was written with less than the code (only the caption and link), and why.

Fences used to drop every such reason. A copy it could not render kept its old closer, check then called it `stale` with "run ds refresh" as the fix, and refresh said "0 repo-mode copies rewritten" and nothing else, so the one command named as the fix never said why it did nothing (bug 136).

type DefineOptions

type DefineOptions struct {
	// Label is the id prefix; empty derives one from the symbol or file.
	Label     string
	Owner     string
	Stability string
	Tags      string
	Desc      string
	Env       string
}

DefineOptions are the directive keys `ds def` may set (§9.1).

type DefineResult

type DefineResult struct {
	ID       string `json:"id"`
	Existing bool   `json:"existing"`
	// Block is the block as the scanning tier binds it. For a new def its
	// Pos is in the file as it was read, before Edit; DirectivePos is the
	// line Edit writes the directive at.
	Block block.Block `json:"block"`
	Edit  Edit        `json:"edit"`
}

DefineResult is what Define returns.

type Edit

type Edit struct {
	File string `json:"file"`
	Line int    `json:"line"`
	Old  string `json:"old,omitempty"`
	New  string `json:"new"`
	// Delete removes the line instead of rewriting it. It exists for the one
	// write that cannot be expressed as a replacement: taking a directive out
	// of a format with no comment syntax, where commenting it is impossible.
	Delete bool `json:"delete,omitempty"`
	// Next is the line that followed the deleted one when the edit was
	// computed, or "" when it was the last. It is what makes a delete
	// reversible: undo finds that line where the deleted one was, and only
	// then puts the deleted line back above it, so an edit made since is never
	// overwritten or misplaced.
	Next string `json:"next,omitempty"`
}

Edit is a proposed change to one source file: the library never writes (§37.2). It has three shapes, told apart by its fields:

  • insert: Old empty -- New goes in as a new line before Line.
  • replace: Old set -- that exact line becomes New.
  • delete: Delete set -- the line Old at Line is removed, and Next is the line that followed it.

Apply refuses when the file no longer has Old at Line, so an edit computed against a stale read cannot land on the wrong line.

func (Edit) Apply

func (e Edit) Apply(src []byte) ([]byte, error)

Apply performs the edit on src. Every line it does not touch comes back byte for byte, each keeping its own ending, and a leading byte order mark stays in front of line 1. A zero edit returns src unchanged.

func (Edit) IsZero

func (e Edit) IsZero() bool

IsZero reports an edit that changes nothing (Define found an existing id).

type Envelope

type Envelope struct {
	JSONFormat  int       `json:"json_format"`
	GeneratedAt time.Time `json:"generated_at"`
	Repo        string    `json:"repo,omitempty"`
	Commit      string    `json:"commit,omitempty"`
}

Envelope is the header every JSON result carries (§26.2).

type Fact

type Fact struct {
	ID      string           `json:"id"`
	Value   string           `json:"value"`
	Type    string           `json:"type,omitempty"`
	File    string           `json:"file"`
	Line    int              `json:"line"`
	Env     string           `json:"env,omitempty"`
	Owner   string           `json:"owner,omitempty"`
	Secret  bool             `json:"secret,omitempty"`
	CitedBy []block.Position `json:"cited_by"`
}

Fact is a one-line def with its current value (§11, `ds facts`).

type FindOptions

type FindOptions struct {
	Query string
	File  string
	Tag   string
}

FindOptions narrows Find (§26.5 `ds find <query> | --file path | --tag t`). Query matches id and symbol by case-insensitive substring; File is a path prefix; Tag is exact. Set fields combine with and; an empty set matches everything.

type Graph

type Graph struct {
	Envelope
	Nodes []GraphNode `json:"nodes"`
	Edges []GraphEdge `json:"edges"`
}

Graph is defs, docs, cites, chains, covers, and claims as a graph.

func (Graph) DOT

func (g Graph) DOT() string

DOT renders the graph for Graphviz.

type GraphEdge

type GraphEdge struct {
	From string `json:"from"`
	To   string `json:"to"`
	Kind string `json:"kind"`
	Line int    `json:"line,omitempty"`
}

GraphEdge links two nodes.

type GraphNode

type GraphNode struct {
	ID    string      `json:"id"`
	Kind  string      `json:"kind"`
	Label string      `json:"label"`
	State check.State `json:"state,omitempty"`
}

GraphNode is a def or a doc.

type ImpactGroup

type ImpactGroup struct {
	Key      string          `json:"key"`
	Findings []check.Finding `json:"findings"`
}

ImpactGroup is findings grouped by one key.

type ImpactResult

type ImpactResult struct {
	Envelope
	Total   int           `json:"total"`
	ByDoc   []ImpactGroup `json:"by_doc"`
	ByOwner []ImpactGroup `json:"by_owner"`
	ByRepo  []ImpactGroup `json:"by_repo"`
}

ImpactResult is what a change will flag (§22 `ds impact`).

type KeySpec

type KeySpec struct {
	Required []string
	Known    []string
}

KeySpec declares a verb's keys so unknown keys warn and missing required keys are problems, the same rule the built-in verbs follow.

type Literal

type Literal struct {
	Doc   string `json:"doc"`
	Line  int    `json:"line"`
	ID    string `json:"id"`
	Value string `json:"value"`
}

Literal is a fact value typed by hand in prose instead of cited.

type ManualFix

type ManualFix struct {
	File   string `json:"file"`
	Line   int    `json:"line"`
	Reason string `json:"reason"`
}

ManualFix is one line Repair found and left for a person.

func (ManualFix) String

func (m ManualFix) String() string

String is the one-line form `ds repair` prints for a manual fix.

type MapChain

type MapChain struct {
	Root  string      `json:"root"`
	Hops  int         `json:"hops"`
	State check.State `json:"state"`
}

MapChain is one chain row.

type MapDef

type MapDef struct {
	ID        string          `json:"id"`
	Desc      string          `json:"desc,omitempty"`
	File      string          `json:"file"`
	Lines     [2]int          `json:"lines"`
	CitedBy   int             `json:"cited_by"`
	Stability block.Stability `json:"stability"`
	State     check.State     `json:"state"`
	Tokens    int             `json:"tokens"`
}

MapDef is one def row.

type MapOptions

type MapOptions struct {
	Budget int
}

MapOptions bounds `ds map` (§26.3).

type MapPage

type MapPage struct {
	Path   string              `json:"path"`
	Covers int                 `json:"covers"`
	Cites  int                 `json:"cites"`
	State  map[check.State]int `json:"state"`
	Owner  string              `json:"owner,omitempty"`
	Tokens int                 `json:"tokens"`
}

MapPage is one page row.

type MapResult

type MapResult struct {
	Envelope
	Pages        []MapPage           `json:"pages"`
	Defs         []MapDef            `json:"defs"`
	Chains       []MapChain          `json:"chains"`
	Freshness    map[check.State]int `json:"freshness"`
	BudgetTokens int                 `json:"budget_tokens"`
	UsedTokens   int                 `json:"used_tokens"`
	Omitted      int                 `json:"omitted"`
}

MapResult is the table of contents an agent reads instead of the tree.

type Notifier

type Notifier interface {
	Notify(ctx context.Context, findings []check.Finding) error
}

Notifier delivers findings somewhere.

type Observer

type Observer interface {
	OnFinding(check.Finding)
	OnAck(ledger.Ack)
}

Observer sees every finding and every ack as it happens: audit sinks and metrics.

type Omitted

type Omitted struct {
	ID     string `json:"id"`
	Reason string `json:"reason"`
}

Omitted names an item that did not fit.

type Option

type Option func(*System) error

Option configures a System. Functional options only (§37.2).

func WithAcks

func WithAcks(a ledger.Acks) Option

WithAcks supplies the append-only ack log.

func WithBodyAt

func WithBodyAt(f func(hash string) (string, bool)) Option

WithBodyAt supplies block bodies by content hash from the body store (§20.1). It is what answers "what did this block say when the sentence was acked" across several scans and across repositories, which WithOldContent — keyed to the previous ledger — structurally cannot. Without it a drift the change table does not span is reported with block.ClassUnknown.

func WithClassifier

func WithClassifier(c Classifier) Option

WithClassifier replaces change classification entirely.

func WithClock

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

WithClock replaces time.Now, for reproducible output.

func WithCommandName added in v0.1.3

func WithCommandName(name string) Option

WithCommandName names the binary that findings tell the reader to run, for a custom build (cli.WithName): its remedies say "pds ack", not a "ds ack" its users do not have. Empty keeps check.DefaultCommand.

func WithCommit

func WithCommit(sha string) Option

WithCommit records the commit the tree is at, for permalinks and headers.

func WithCommitLookup

func WithCommitLookup(f func(sha string) bool) Option

WithCommitLookup answers `at=` pins.

func WithConfig

func WithConfig(c config.Config) Option

WithConfig sets the parsed config; the library never reads the file itself. Omitted, config.Default() with `docs/**` and `**` as scan globs is used so a zero-config call still scans something.

func WithExtractCache

func WithExtractCache(c scan.Cache) Option

WithExtractCache makes scans incremental: files whose bytes did not change reuse the cached extraction (§16), and a cache that also implements scan.StatCache spares unchanged files the read itself. CheckOptions.Full bypasses it.

func WithExtractor

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

WithExtractor adds tiers ahead of the built-in ones; the last given wins for a path both match.

func WithFS

func WithFS(fsys fs.FS) Option

WithFS sets the tree to scan. Required.

func WithForeignLink(f func(b block.Block) (string, bool)) Option

WithForeignLink supplies the URL of a block another repository in the workspace defines, for `Render` (render.Options.ForeignLink). Repo-mode copies never use it: see render.Fragment. The library knows the defining repository's name and the block's path inside it, not where that repository is hosted, so the URL is the caller's decision. Without it such a block renders its location unlinked, with the repository named, rather than a link relative to this repository (bug 105).

func WithForeignSnapshot

func WithForeignSnapshot() Option

WithForeignSnapshot says the merged blocks came from a committed snapshot rather than the live workspace index, so a citation this run cannot resolve is reported as unrecorded rather than as undefined (§21).

func WithLocalReader added in v0.1.3

func WithLocalReader(f func(path string) ([]byte, error)) Option

WithLocalReader lets check look at `local=true` targets (§9.1, §12): f reads a def's file= path on the machine running check. A target that f reads, and whose pick= finds something, is `ok` there; one f cannot read stays `unverifiable`, which is what CI reports. Without it every local def is unverifiable everywhere (bug 85). Nothing read is hashed or stored: the ledger is shared and the file exists on one machine.

func WithMerged

func WithMerged(blocks ...block.Block) Option

WithMerged supplies blocks from other repos in the workspace, as produced by sync (§21). They can be cited and rendered but are not this repo's.

func WithMergedRefs

func WithMergedRefs(rows ...ledger.RefRow) Option

WithMergedRefs supplies references published by other repositories that point into this one (§21), so deleting a block here reports the docs elsewhere that depend on it.

func WithNotifier

func WithNotifier(n Notifier) Option

WithNotifier adds a delivery channel used by Notify.

func WithObserver

func WithObserver(o Observer) Option

WithObserver adds a sink for findings and acks.

func WithOldContent

func WithOldContent(f func(row ledger.Row) (string, bool)) Option

WithOldContent supplies the body a block had in a previous ledger row, for diffs and change classes; see match.Options.OldContent for why it takes the row. Typically backed by `git show` in the CLI.

func WithPicker

func WithPicker(p Picker) Option

WithPicker registers a `pick=` scheme.

func WithPrevious

func WithPrevious(l ledger.Ledger, r ledger.Refs) Option

WithPrevious supplies the committed ledger and refs to diff against. Both zero means a first run: everything is new and ok. Either one recorded under a newer extraction rule is refused with ErrNewerRule.

func WithPreviousForeign

func WithPreviousForeign(rows ...ledger.Row) Option

WithPreviousForeign supplies the committed snapshot's rows as the previous position of every foreign block, so a block that moved in another repository since the last sync is reported as `moved` rather than in silence (§21).

func WithRecords

func WithRecords(f func(args map[string]string) ([]map[string]string, error)) Option

WithRecords supplies a record source for `ds:table`.

func WithRegistry

func WithRegistry(r *extract.Registry) Option

WithRegistry replaces tier selection entirely; the escape hatch (§37.3).

func WithRemoved

func WithRemoved(ids map[string]string) Option

WithRemoved names ids published by repositories that left the workspace, so citations report "repo removed" rather than "deleted" (§21).

func WithRenderer

func WithRenderer(r Renderer) Option

WithRenderer replaces the markdown output of Render.

func WithRepo

func WithRepo(name string) Option

WithRepo names this repository inside a workspace (§21).

func WithResolver

func WithResolver(r check.Resolver) Option

WithResolver supplies the secret resolver used under --resolve (§12).

func WithSnapshot

func WithSnapshot(f func(id, sha string) (string, bool)) Option

WithSnapshot supplies block bodies at a commit for `at=` rendering.

func WithSnapshotBlock added in v0.1.3

func WithSnapshotBlock(f func(id, sha string) (block.Block, bool)) Option

WithSnapshotBlock supplies the whole block at a commit for `at=` rendering: its body and where it was, so the caption and link of a snapshot name the lines the code occupied at that commit rather than today's. It takes precedence over WithSnapshot when both are given.

func WithStore

func WithStore(st Store) Option

WithStore loads the previous ledger, refs, and acks from st and keeps it for SaveState. It replaces WithPrevious and WithAcks.

func WithStoredHashes

func WithStoredHashes(m map[string]string) Option

WithStoredHashes supplies truth hashes kept from an earlier resolve, so a changed truth reports as rotated.

func WithTestResults

func WithTestResults(m map[string]check.TestOutcome) Option

WithTestResults supplies the last published CI outcomes for `assert=`.

func WithURLCheck

func WithURLCheck(f func(href string) check.URLResult) Option

WithURLCheck answers `ds:url` under --resolve.

func WithVerb

func WithVerb(names ...string) Option

WithVerb registers plugin verb names so references to them are not reported as unknown. Their check and render behaviour is the plugin's.

func WithVerbHandler

func WithVerbHandler(v Verb) Option

WithVerbHandler registers a verb implementation. Its name is also added to the known verbs, so `WithVerb` is not needed alongside.

type OwnerMetrics

type OwnerMetrics struct {
	Key   string              `json:"key"`
	State map[check.State]int `json:"state"`
}

OwnerMetrics is freshness per owner or page.

type Picker

type Picker interface {
	Scheme() string
	Pick(arg, content string) (pick.Result, error)
}

Picker is a plugin `pick=` scheme.

type Prefix

type Prefix struct {
	Prefix string `json:"prefix"`
	Count  int    `json:"count"`
}

Prefix is an id label with how many ids use it (Part VII "busy prefixes").

type RemoteCiter added in v0.1.3

type RemoteCiter struct {
	Repo string `json:"repo"`
	Doc  string `json:"doc"`
	Line int    `json:"line"`
	Verb string `json:"verb"`
}

RemoteCiter is one citation published by another repository: where it is, and the verb it cites with. The sentence itself is not published, so it is not here.

type RenameResult

type RenameResult struct {
	// Mapping is old id to new id for every id whose label changed.
	Mapping map[string]string `json:"mapping"`
	// Edits rewrite every def and reference line; one edit per line even
	// when a line holds several ids.
	Edits []Edit `json:"edits"`
}

RenameResult is what Rename proposes.

type RenderOptions

type RenderOptions struct {
	Env string
	// Runs supplies recorded `ds:run` results by doc line.
	Runs map[int]render.RunResult
	// Source replaces the document's bytes, for `render --at` where the
	// caller has the page as it was at a commit; nil reads the tree.
	Source []byte
}

RenderOptions tunes Render.

type Renderer

type Renderer interface {
	Render(nodes []render.Node) ([]byte, error)
}

Renderer turns the rendered node sequence into bytes: markdown by default, html or a site generator's own tree when replaced.

type RepairResult

type RepairResult struct {
	Envelope
	// Edits either comment a bare directive in the file's own syntax, which
	// keeps its id so every citation still resolves, or -- in a format with no
	// comment syntax -- delete it. A delete removes a def that was never valid
	// there; its citations then report the id missing, which is the truth.
	Edits []Edit `json:"edits"`
	// Manual is damage no edit could be computed for: a file that could not be
	// read back when the repair was built.
	Manual []ManualFix `json:"manual"`
}

RepairResult is what Repair proposes: edits that mend each damaged directive, and the damage it could not reach.

type Report

type Report struct {
	Envelope
	Summary  map[check.Severity]int `json:"summary"`
	States   map[check.State]int    `json:"states"`
	Findings []check.Finding        `json:"findings"`
	Changes  []match.Change         `json:"-"`
	Scan     scan.Result            `json:"-"`
	ExitCode int                    `json:"exit_code"`
	// TruthHashes are resolved truth hashes for the caller to store.
	TruthHashes map[string]string `json:"-"`
}

Report is the result of Check: the JSON contract plus the scan it came from, so callers can build further views without rescanning.

func (Report) ApplyRuns added in v0.1.3

func (r Report) ApplyRuns(runs []check.RunResult) Report

ApplyRuns records the outcomes of `ds:run` directives the caller executed as their findings, and recounts the summary and exit code: a failed run is a `run failed` error, a run not executed is `skipped` with the reason. The library runs nothing itself; see check.ApplyRuns.

type ReportOptions

type ReportOptions struct {
	// Churn is commits touching each file, supplied by the CLI from git;
	// nil disables the most-changed-files-without-defs view.
	Churn map[string]int
	// Limit caps each list; 0 means everything.
	Limit int
	// ServedBytes and SourceBytes are what agents received through
	// `context` and `read` against the size of the files those items came
	// from (§26.11), recorded by the host; zero means not measured.
	ServedBytes, SourceBytes int64
}

ReportOptions selects the hygiene views of `ds report` (§22, §26.9).

type ReportResult

type ReportResult struct {
	Envelope
	Uncovered      []block.Block  `json:"uncovered"`
	Unmarked       []Unmarked     `json:"unmarked"`
	Stalest        []Stale        `json:"stalest"`
	Literals       []Literal      `json:"literals"`
	OrphanedOwners []string       `json:"orphaned_owners"`
	Gaps           []string       `json:"gaps"`
	PerPage        []OwnerMetrics `json:"per_page"`
	PerOwner       []OwnerMetrics `json:"per_owner"`
	MeanTimeToAck  time.Duration  `json:"mean_time_to_ack_ns"`
	BusyPrefixes   []Prefix       `json:"busy_prefixes"`
	// ServedBytes and SourceBytes echo the options; ContextSavings is the
	// fraction of file bytes an agent did not have to read.
	ServedBytes    int64   `json:"served_bytes"`
	SourceBytes    int64   `json:"source_bytes"`
	ContextSavings float64 `json:"context_savings"`
}

ReportResult is every view; the CLI prints the ones asked for.

type Stale

type Stale struct {
	Doc       string    `json:"doc"`
	OldestAck time.Time `json:"oldest_ack"`
	Cites     int       `json:"cites"`
	NeverAck  bool      `json:"never_acked"`
}

Stale is a page ordered by its oldest live ack.

type Store

type Store interface {
	Load(ctx context.Context) (ledger.Ledger, ledger.Refs, ledger.Acks, error)
	Save(ctx context.Context, l ledger.Ledger, r ledger.Refs, a ledger.Acks) error
}

Store is where committed state lives. The default is the `.ds/` TSV files in the CLI; a database or the workspace index can stand in.

type System

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

System is a configured instance. Build one with New; it is safe to share between goroutines because every method is read-only over its fields.

func New

func New(opts ...Option) (*System, error)

New builds a System.

func (*System) Ack

func (s *System) Ack(res scan.Result, req AckRequest) (ledger.Ack, error)

Ack builds the ack row for a reference against the block's current hash and the sentence's current hash. The caller appends it to the log. An agent actor must name who delegated; the library refuses otherwise so no tool path can approve its own work silently.

func (*System) Adopt

func (s *System) Adopt(_ context.Context, res scan.Result) (AdoptResult, error)

Adopt converts hand-written `path#L10-L20` and `path#symbol` links in scanned docs into defs and cites (§22 `ds adopt`). A link whose target already has a def gets a cite to the existing id; otherwise an id is minted and a directive edit is proposed above the block. Every edit is returned, none applied; the caller applies source edits first, then doc edits, then rescans.

func (*System) Audit

func (s *System) Audit(opts AuditOptions) []ledger.Ack

Audit returns ack events matching the filter, oldest first; the log is already append-only so no other ordering is needed (§33).

func (*System) Blame

func (s *System) Blame(rep Report, doc string, line int) (BlameResult, error)

Blame answers `ds blame <doc> <line>`: the reference on that line, the block it points at, the change since the ledger, the current finding, and every ack recorded for that line (§22). History beyond the previous ledger needs the VCS and belongs to the CLI.

func (*System) Bodies

func (s *System) Bodies(res scan.Result) map[string]string

Bodies is what the caller should add to the body store after a scan: every def's body keyed by its content hash (§20.1). Content-addressed, so re-storing an unchanged block is a no-op and history accumulates without duplication; the caller writes, because the library never does.

Secret and local blocks are excluded and this is enforced here rather than left to the caller: the workspace index is readable by people who may not read the source repository, and a body written there cannot be recalled. A block with no content (a merged row, which carries a hash but no body) contributes nothing.

func (*System) Check

func (s *System) Check(ctx context.Context, opts CheckOptions) (Report, error)

Check scans and evaluates.

func (*System) Config

func (s *System) Config() config.Config

Config returns the effective configuration.

func (*System) Context

func (s *System) Context(ctx context.Context, target string, opts ContextOptions) (ContextResult, error)

Context returns a page with every block it cites, or a block with every sentence about it, ranked and budgeted. Order is deterministic for identical inputs so prompt caches hit.

func (*System) ContextFor

func (s *System) ContextFor(rep Report, target string, opts ContextOptions) ContextResult

ContextFor is Context over an existing report, for callers that already checked and want several bundles without rescanning.

func (*System) Define

func (s *System) Define(_ context.Context, target string, opts DefineOptions) (DefineResult, error)

Define returns the id for `path#Symbol` or `path:line`, minting one and proposing the directive insertion when the block has none (§22 `ds def`). One def per block: an existing directive on that block is returned as is.

func (*System) ExtractFile

func (s *System) ExtractFile(ctx context.Context, path string, src []byte) ([]block.Block, error)

ExtractFile runs the scan pipeline over one file's bytes -- the tier that owns the path, `pick`, the key-to-value step and secret redaction -- and returns the defs it finds, exactly as a scan of that content would.

Why it exists: an OldContent hook has to hand back a block's previous body in the same form as its current one, or the diff compares two different things. The CLI's hook cut the raw lines out of the old file, so a config value changed from 443 to 8443 was diffed as the whole line "port: 443 # ds:def id=…" against "8443", directive comment included, and a secret's old value came back unredacted. Going through the same pipeline makes old and new comparable by construction.

The extraction cache is not used: old bytes must never be stored or served as though they were the current file's.

func (*System) Facts

func (s *System) Facts(res scan.Result) []Fact

Facts lists every one-line def and who cites it. Secrets are addresses and render as such; the value is the visible name, never a credential.

func (*System) Fences

func (s *System) Fences(res scan.Result, doc string, src []byte) ([]byte, int)

Fences rewrites the repo-mode copies in one document (§9.2 include.mode = repo): every block-position `ds:block` gets, or has refreshed, the fragment `refresh` maintains, closed by a line carrying the block's short hash. src is the document's current bytes (the caller read them, so the caller decides what a read failure means); it returns the new bytes and how many regions changed. Nothing is written. FencesReport also says which copies could not be written and why.

func (*System) FencesReport added in v0.1.6

func (s *System) FencesReport(res scan.Result, doc string, src []byte) ([]byte, int, []CopyNote)

FencesReport is Fences plus a note for every copy in doc that was skipped or written without its code, in line order. A skip is never silent: the copy keeps whatever it held, and the note says why (bug 136).

It follows [include] mode, where Fences writes every copy whatever the mode. In repo mode every block-position citation gets a copy. In build mode a page is rendered at build time and owes no copy, so none is inserted, but a copy already in the page (left from repo mode, or pasted) is still brought up to date. check compares such a copy with its block in either mode, and before this a build-mode refresh never touched it, so check reported it stale with "run ds refresh" as the fix, forever (bug 141).

func (*System) Find

func (s *System) Find(res scan.Result, query string) []block.Block

Find returns defs matching a free query: substring on id or symbol, file prefix, or exact tag, whichever hits.

func (*System) FindBy

func (s *System) FindBy(res scan.Result, opts FindOptions) []block.Block

FindBy applies each set option as a filter.

func (*System) FixDuplicates

func (s *System) FixDuplicates(res scan.Result) (RenameResult, error)

FixDuplicates re-mints every def after the first for each id defined more than once (Part VII "Ids and branches"): the first occurrence in file order keeps the id, the tool never guesses which one is the original, and the caller prints old→new. Edits rewrite only the directive lines; the caller applies them and rescans.

func (*System) Graph

func (s *System) Graph(rep Report) Graph

Graph builds the dependency graph from a report.

func (*System) Impact

func (s *System) Impact(ctx context.Context) (ImpactResult, error)

Impact checks the working tree against the previous ledger and returns only the findings a change causes: unacked, translation-stale, broken, expired-by-about. Everything else is hygiene, not impact.

func (*System) LocateID

func (s *System) LocateID(res scan.Result, id string) (block.Block, bool)

LocateID returns the block for an id, this repo's defs first.

func (*System) Map

func (s *System) Map(ctx context.Context, opts MapOptions) (MapResult, error)

Map runs a check and ranks pages and defs under the budget: anything not ok first, then most cited, then by path so output is deterministic.

func (*System) Notify

func (s *System) Notify(ctx context.Context, rep Report) error

Notify sends the report's non-ok findings to every notifier; the first failure is returned after the rest were tried.

func (*System) Read

func (s *System) Read(res scan.Result, id, lines string) (string, error)

Read returns a block's body, or a `lines=a-b` fragment of it.

func (*System) Registry

func (s *System) Registry() *extract.Registry

Registry returns the extractor registry in use, so a host that has a document in memory (an editor buffer) can extract it the same way a scan would.

func (*System) Rename

func (s *System) Rename(res scan.Result, oldPrefix, newPrefix string) (RenameResult, error)

Rename relabels ids whose label starts with oldPrefix so it starts with newPrefix instead (§22 `ds rename`). Identity is the suffix, so the ledger keeps matching after the edit; the old rows simply carry the old label and match by suffix on the next scan. Nothing is written: the caller applies the edits and rescans.

func (*System) Render

func (s *System) Render(ctx context.Context, doc string, opts RenderOptions) ([]byte, []render.Note, error)

Render expands one document to plain markdown (§22 `ds render`).

func (*System) Repair

func (s *System) Repair(res scan.Result) RepairResult

Repair proposes the fix for every directive written as a bare line into a file whose format does not accept one -- the damage `ds def` left behind before it learned to refuse. It returns edits and never writes (§22).

A format with comments gets the line commented: a commented directive is still a def, so its id and every sentence citing it keep working, and the file parses again. A format with no comment syntax -- JSON, CSV, go.sum -- has nothing to comment into, so the line is deleted. That delete is reversible like every other source write: it carries the line that followed it, so `ds undo` can find the place and put it back, and refuses if the file has moved around it since.

Edits in a file are ordered bottom-up, so applying them in order never shifts a line a later edit points at. A directive continued onto following lines is mended whole; a deleted one records as its Next the first line after it that survives, since the line straight after may be going too.

func (*System) Report

func (s *System) Report(rep Report, opts ReportOptions) ReportResult

Report computes the hygiene views from a report (§26.9 "proactive rather than reactive").

func (*System) SaveState

func (s *System) SaveState(ctx context.Context, res scan.Result, acks ledger.Acks) error

SaveState writes the snapshot of res and the given ack log through the Store.

func (*System) Scan

func (s *System) Scan(ctx context.Context) (scan.Result, error)

Scan extracts every def and reference under the config's globs, using the extraction cache when one is configured.

func (*System) ScanFull

func (s *System) ScanFull(ctx context.Context) (scan.Result, error)

ScanFull ignores the extraction cache.

func (*System) Snapshot

func (s *System) Snapshot(res scan.Result) (ledger.Ledger, ledger.Refs)

Snapshot turns a scan into the ledger and refs to commit (§15). Refs carry the latest acked hash per line so a fresh checkout can check without the ack log.

func (*System) Why

func (s *System) Why(res scan.Result, target string) (Why, error)

Why answers `ds why <id>` from a scan and the ack log.

type TriageGroup

type TriageGroup struct {
	Group    int             `json:"group"`
	Diff     string          `json:"diff"`
	Classes  []block.Class   `json:"class,omitempty"`
	Findings []check.Finding `json:"findings"`
}

TriageGroup is a set of unacked findings whose diffs look alike.

func Triage

func Triage(rep Report) []TriageGroup

Triage groups a report's unacked findings by diff similarity so a mechanical change across many blocks is one decision with one note. Findings without a diff (no old content was available) group by block id instead: the same block changed is still the same decision. Group numbers are assigned in finding order, so they are stable for identical input.

type Unmarked

type Unmarked struct {
	File   string `json:"file"`
	Symbol string `json:"symbol,omitempty"`
	Line   int    `json:"line,omitempty"`
	Churn  int    `json:"churn,omitempty"`
}

Unmarked is a file with change history and no definitions, or an exported declaration nobody cites.

type Unresolved

type Unresolved struct {
	Doc    string `json:"doc"`
	Line   int    `json:"line"`
	Target string `json:"target"`
	Reason string `json:"reason"`
}

Unresolved is a link Adopt left alone.

type Verb

type Verb interface {
	Name() string
	Carriers() []block.Carrier
	Keys() KeySpec
	Check(ref block.Reference, st *VerbState) []check.Finding
	Render(ref block.Reference, st *VerbState, inline bool) (text string, ok bool, err error)
}

Verb is a plugin directive verb (§9.9). Check returns findings for one reference; Render returns the text that replaces the directive, inline for a link in prose and as a block otherwise. ok=false means "leave the source as written".

type VerbState

type VerbState struct {
	Repo string
	Defs map[string]block.Block
}

VerbState is the read-only view a verb gets: the defs of the tree and the merged workspace, by id.

type Why

type Why struct {
	ID        string            `json:"id"`
	Defs      []block.Block     `json:"defs"`
	Refs      []block.Reference `json:"refs"`
	CoveredBy []string          `json:"covered_by"`
	// Chain is the from= path from the id to its truth, first element the
	// id itself; empty when the id has no from= and nothing points at it.
	Chain []block.Block `json:"chain"`
	// Copies are defs whose from= is this id.
	Copies  []block.Block `json:"copies"`
	History []ledger.Ack  `json:"history"`
	// RemoteRefs are citations of the id that other repositories in the
	// workspace published (WithMergedRefs). Refs holds only this
	// repository's scan, so a block cited from another repository read as
	// cited by nobody, the one question `why` exists to answer (bug 103).
	RemoteRefs []RemoteCiter `json:"remote_refs,omitempty"`
}

Why is everything that depends on an id (§22 `ds why`).

Directories

Path Synopsis
Package block holds the data model shared by every docsync stage: a defined Block, a Reference to one, and the closed-set constants (kind, carrier, stability, change class) that the ledger, findings, and JSON contract use.
Package block holds the data model shared by every docsync stage: a defined Block, a Reference to one, and the closed-set constants (kind, carrier, stability, change class) that the ledger, findings, and JSON contract use.
Package check turns a scan, the previous ledger, and the ack log into findings: one row per reference or per problem, with a state from the spec's table, a severity, the change class and diff where relevant, and a remedy an agent can execute verbatim (docs/SPEC.md §16 passes 3 to 5, §17).
Package check turns a scan, the previous ledger, and the ack log into findings: one row per reference or per problem, with a state from the spec's table, a severity, the change class and diff where relevant, and a remedy an agent can execute verbatim (docs/SPEC.md §16 passes 3 to 5, §17).
Package config reads `.ds/config.toml` (docs/SPEC.md §23) and the workspace file, with defaults for everything the spec gives a default.
Package config reads `.ds/config.toml` (docs/SPEC.md §23) and the workspace file, with defaults for everything the spec gives a default.
Package directive parses and formats the single docsync directive shape,
Package directive parses and formats the single docsync directive shape,
ext
structured module
treesitter module
Package extract finds directives in a file and binds `ds:def` directives to the block they define (docs/SPEC.md §7.1, §10).
Package extract finds directives in a file and binds `ds:def` directives to the block they define (docs/SPEC.md §7.1, §10).
Package id mints, validates, and splits docsync block identifiers.
Package id mints, validates, and splits docsync block identifiers.
internal
difflib
Package difflib computes line-level diffs and a similarity ratio between two texts, with no dependencies.
Package difflib computes line-level diffs and a similarity ratio between two texts, with no dependencies.
duration
Package duration parses the duration shape docsync writes in directives and in configuration: an integer followed by a unit.
Package duration parses the duration shape docsync writes in directives and in configuration: an integer followed by a unit.
glob
Package glob matches slash-separated paths against patterns with `*`, `?`, `[...]`, and the `**` segment that spans directories.
Package glob matches slash-separated paths against patterns with `*`, `?`, `[...]`, and the `**` segment that spans directories.
keypath
Package keypath is the one grammar for config key paths, shared by the config tier that binds keys in a file (extract) and the pick that reads a key out of a remote def's target (pick).
Package keypath is the one grammar for config key paths, shared by the config tier that binds keys in a file (extract) and the pick that reads a key out of a remote def's target (pick).
linerange
Package linerange parses the `lines=` fragment a citation uses to show part of a block: `a-b` or `a`, 1-based and inclusive.
Package linerange parses the `lines=` fragment a citation uses to show part of a block: `a-b` or `a`, 1-based and inclusive.
mdspan
Package mdspan finds the inline spans of a markdown line that are not running prose: code spans and links.
Package mdspan finds the inline spans of a markdown line that are not running prose: code spans and links.
textnorm
Package textnorm normalizes text before hashing so that changes which carry no meaning never register as changes.
Package textnorm normalizes text before hashing so that changes which carry no meaning never register as changes.
Package ledger reads and writes the three committed state files (docs/SPEC.md §15, §19): the ledger of every defined block, the reverse index of every reference, and the append-only ack log.
Package ledger reads and writes the three committed state files (docs/SPEC.md §15, §19): the ledger of every defined block, the reverse index of every reference, and the append-only ack log.
Package match compares the blocks a scan found against the previous ledger and decides, per id, what happened (docs/SPEC.md §16 pass 3): ok, moved, changed, moved-unmarked, rewritten?, deleted, or new.
Package match compares the blocks a scan found against the previous ledger and decides, per id, what happened (docs/SPEC.md §16 pass 3): ok, moved, changed, moved-unmarked, rewritten?, deleted, or new.
Package pick evaluates `pick=` expressions: how a single value or a single contiguous range is taken out of a bound block (docs/SPEC.md §10).
Package pick evaluates `pick=` expressions: how a single value or a single contiguous range is taken out of a bound block (docs/SPEC.md §10).
Package procplugin implements both sides of the process-plugin protocol (docs/SPEC.md §37.4): executables named `ds-<kind>-<name>` on PATH that speak one JSON object per line over stdin and stdout, so a plugin can be written in any language and a Go plugin can be shipped as a process with no extra code.
Package procplugin implements both sides of the process-plugin protocol (docs/SPEC.md §37.4): executables named `ds-<kind>-<name>` on PATH that speak one JSON object per line over stdin and stdout, so a plugin can be written in any language and a Go plugin can be shipped as a process with no extra code.
Package records is the first record source for `ds:table` (docs/SPEC.md §9.5): a directory of markdown files whose YAML frontmatter holds the fields.
Package records is the first record source for `ds:table` (docs/SPEC.md §9.5): a directory of markdown files whose YAML frontmatter holds the fields.
Package render expands directives in one markdown document into plain markdown at build time (docs/SPEC.md §9, §22 `ds render`).
Package render expands directives in one markdown document into plain markdown at build time (docs/SPEC.md §9, §22 `ds render`).
Package scan walks a file tree, hands each eligible file to the extractor registry, and merges the results into one Result with repo-relative positions, resolved remote defs, and duplicate-id detection (docs/SPEC.md §16 passes 1 and 2).
Package scan walks a file tree, hands each eligible file to the extractor registry, and merges the results into one Result with repo-relative positions, resolved remote defs, and duplicate-id detection (docs/SPEC.md §16 passes 1 and 2).
Package sentence decides what prose a reference binds to (docs/SPEC.md §18).
Package sentence decides what prose a reference binds to (docs/SPEC.md §18).
Package workspace merges what every repository in a workspace published into one id table and one reverse index (docs/SPEC.md §21), and reads the index layout that `publish` writes and `sync` reads:
Package workspace merges what every repository in a workspace published into one id table and one reverse index (docs/SPEC.md §21), and reads the index layout that `publish` writes and `sync` reads:

Jump to

Keyboard shortcuts

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