This page is the reference for everything you write into source and docs for docsync to read: the directive syntax, ids, each verb with every argument it takes, and what ds check reports for it. It is for authors who already ran ds init (see Getting started) and want to know exactly what to type; the per-language details of where a ds:def goes and what it binds are in Languages.
Every example below is run against the ds binary by the guide's test harness, each section in a fresh repository, and the output shown is what ds prints.
- Anatomy of a directive
- Where a directive can sit
- Ids
- ds:def — give something an identity
- Facts: inline defs in prose
- Remote defs: files that cannot hold a comment
- pick — take one value or range out of a block
- ds:block — cite or show a block
- ds:cfg — put a value in a sentence
- ds:claim — a sentence that must be re-reviewed
- ds:url — an outside link that is watched
- ds:run — run something and record the result
- ds:table — a table over records
- Secrets and chains
- Environments
- Deprecation and sunset
- Translations
- Page frontmatter: covers and review_every
- Mistakes ds check catches
- Cheat sheet
ds:<verb> key=value key=value …
- Prefix.
dsfollowed by a colon, glued to the verb. The colon is what keeps the English word "ds" in a comment from ever starting a directive. The prefix is set per repository withprefix = "…"in.ds/config.toml; withprefix = "acme"the scanner readsacme:defandacme:blockand ignoresds:def, andds defwrites// acme:def id=…. - Verb. Lowercase,
[a-z][a-z0-9_]*. The built-in verbs aredef,block,cfg,run,table,claim,urlandchain. - Arguments.
key=valueonly, no positional arguments. Keys are lowercase identifiers. A key may appear once; a repeated key is aproblemfinding. - Values. Bare up to the next whitespace. Use
"…"for a value with spaces and'…'for a value that contains double quotes. Lists are comma separated inside one value:tags=auth,session. There is no escape character. - Continuation. In a line comment, a comment line directly below that starts with whitespace and
key=valuefolds into the directive above it:
package store
// ds:def id=cont-func-b5c6d7e8 owner=@auth
// desc="dual-write guard, remove after task-120"
func Cont() {}The def carries the keys from both lines:
$ ds scan
1 files, 1 defs, 0 refs, 0 problems, 0 skipped
$ ds find cont-func --json
…
"args": {
"desc": "dual-write guard, remove after task-120",
"id": "cont-func-b5c6d7e8",
"owner": "@auth"
}
}
]
Continuation works for line comments (//, #, --). Do not split one HTML comment (<!-- … -->) across lines in markdown: a directive broken that way is not read at all and nothing is reported, so keep markdown directives on one line.
- Unknown verbs and keys are warnings, so an older
dscan read files written for a newer one.ds check --strictturns them into errors:
# Verbs
<!-- ds:frob id=x -->
[guard](ds:block?id=cont-func-b5c6d7e8&bogus=1)$ ds check
docs/verbs.md
3 warning unknown unknown verb "frob"
fix: frob is not a registered verb; register a handler or fix the directive at docs/verbs.md:3
5 warning unknown unknown key(s) bogus on ds:block
fix: unknown key(s) bogus on ds:block at docs/verbs.md:5; check the spelling against the verb's key table
2 warning, 1 ok
The same text works in three carriers. ds check --explain prints every directive the scan matched, with the tier that read it and the carrier, which is the quickest way to confirm a directive is being seen.
| Carrier | Looks like | Used for |
|---|---|---|
| comment | // ds:def id=…, # ds:def id=…, -- ds:def id=…, /* ds:def id=… */, <!-- ds:block id=… -->, {/* ds:def id=… */} in MDX |
defs in code and config; block-position cites in docs |
| link | [text](ds:block?id=…&lines=1-6) |
inline cites, facts, values in prose |
| bare line | ds:def id=… span=+2 on a line of its own |
plain text files only (.txt, .text, unknown extensions) |
The link form is a markdown link whose target is the directive with ? after the verb and & between arguments. A value in the link form cannot contain a space, because markdown ends the link target at whitespace; write %20 instead (title=The%20Go%20Programming%20Language%20Specification). A link whose target contains a raw space is not a link and is silently not a directive.
A comment anywhere in code can cite a block too, which puts the code in the reverse index next to the docs:
package store
// implements ds:block?id=cont-func-b5c6d7e8
func Rotate() {}$ ds check --explain
WHERE TIER WHAT CARRIER ID
store/cont.go:3 go def func comment cont-func-b5c6d7e8
docs/verbs.md:3 markdown ds:frob block x
docs/verbs.md:5 markdown ds:block link cont-func-b5c6d7e8
store/impl.go:3 go ds:block link cont-func-b5c6d7e8
docs/verbs.md
3 warning unknown unknown verb "frob"
fix: frob is not a registered verb; register a handler or fix the directive at docs/verbs.md:3
5 warning unknown unknown key(s) bogus on ds:block
fix: unknown key(s) bogus on ds:block at docs/verbs.md:5; check the spelling against the verb's key table
2 warning, 2 ok
Directives inside code fences, indented code blocks and inline code spans are not directives, which is why this page can show them. In code, a whole-line directive inside a string literal is not one either, because a string holding // ds:def … is a fixture or an example. A link [x](ds:…) inside a string literal is a citation, because strings in code hold prose such as help text, errors and UI text, and a citation missed there would drift silently. In a JSON, CSV or go.sum file a link is data and a whole-line directive is reported as damage, because those formats have no prose. SPEC "Reading directives" has the full table.
An id looks like sess-save-k7m2p4xq: a human label, a dash, and an eight-character suffix from 23456789abcdefghjkmnpqrstuvwxyz.
Let ds def mint ids rather than typing them. It derives the label from the symbol (or --label), generates the suffix, inserts the directive, and prints the id. Given this file, where SaveSession already has a def:
package store
import "context"
// Limits for sessions.
const (
MaxSessions = 5
TTLDays = 30
)
type Session struct {
ID string
User string
}
type Store struct{}
// SaveSession writes the session.
// ds:def id=store-savesession-m6twuucd
func (s *Store) SaveSession(ctx context.Context, sess Session) error {
if sess.ID == "" {
return nil
}
return nil
}$ ds def store/store.go#MaxSessions --owner @auth
maxsessions-n4kpvzvf
$ ds def store/store.go#TTLDays --label sess-ttl --dry-run
sess-ttl-9hz97fhy
would insert at store/store.go:9:
// ds:def id=sess-ttl-9hz97fhy
Run on a block that already has a def, ds def prints the existing id and changes nothing, so it is safe to call to look an id up:
$ ds def store/store.go#Store.SaveSession
store-savesession-m6twuucd
ds def --fix re-mints every copy after the first when an id has been duplicated (for example by copying a function).
Ids you type by hand are not validated for shape: id=Foo_Bar scans without complaint. Follow the lowercase label-suffix form anyway, because ds rename and the "did you mean" suggestions work on it.
ds rename relabels an id everywhere it appears, in source and docs. Either the full id or just the label works:
$ ds rename store-savesession sess-save --dry-run
store-savesession-m6twuucd -> sess-save-m6twuucd
1 line(s) would change (--dry-run)
$ ds rename store-savesession sess-save
store-savesession-m6twuucd -> sess-save-m6twuucd
1 line(s) changed; run ds scan
$ ds scan
1 files, 2 defs, 0 refs, 0 problems, 0 skipped
Acks and first-seen hashes carry over because the suffix is unchanged. A citation still has to use the current full id; one written with an old label is reported as broken, with the right id suggested:
# Label
The [guard](ds:block?id=old-label-m6twuucd) works.$ ds check
docs/label.md
3 error broken old-label-m6twuucd is not defined; did you mean sess-save-m6twuucd
…
A ds:def sits directly above the thing it names (or at the end of the line, for a config key), and binds the block below it: a function, a type, a constant, a config key, a markdown section, a paragraph. What counts as "the block" depends on the language; see Languages.
package store
// ds:def id=store-save-k7m2p4xq
func Save(id string) error {
return nil
}server:
port: 8081 # ds:def id=server-port-6btxuz6q-- ds:def id=delete-u3e84e69
DELETE FROM sessions WHERE expires_at < now() - interval '30 days';# Policy
<!-- ds:def id=sess-policy-h2n8wq4t -->
## Session policy
Sessions live thirty days and rotate on refresh.ds:def id=span-two-c2d3e4f5 span=+2
Week 37 alex
Week 38 someone
Week 39 third
ds map shows the line range each def bound (they are uncovered because no page cites them yet):
$ ds scan
5 files, 5 defs, 0 refs, 0 problems, 0 skipped
$ ds map
PAGE COVERS CITES STATE
DEF FILE CITED BY STATE
server-port-6btxuz6q config/app.yaml:2-2 0 uncovered
delete-u3e84e69 db/sweep.sql:2-2 0 uncovered
sess-policy-h2n8wq4t docs/policy.md:4-6 0 uncovered
span-two-c2d3e4f5 docs/rota.txt:2-3 0 uncovered
store-save-k7m2p4xq store/save.go:4-6 0 uncovered
66 tokens used, 0 omitted
One def per block and one block per id. Deleting the directive line deletes the block, and every sentence citing it becomes broken.
There is deliberately no value= key. The value is the visible text at the def's location, so a reader and the tool see the same thing.
| Key | Meaning |
|---|---|
id |
required; the identity |
owner |
a person or team (@auth); recorded in the ledger, used by ds notify and reports |
tags |
comma list, recorded in the ledger; ds find --tag auth lists the defs carrying a tag |
stability |
which kinds of change flag the prose that cites it: frozen, stable (default), api, volatile; see below |
span |
+N: widen the block. In plain text it binds exactly N lines below the directive; in code, markdown and INI-style config it binds the first line plus N more. YAML, TOML and HCL ignore it; see Languages |
pick |
take one value or one range out of the bound text; see pick |
type |
the kind of value; type=url makes the fact render as a link |
file |
a remote def: the target is in another file; see Remote defs |
local |
true: the target only exists on some machines; elsewhere it is unverifiable (a warning), never broken |
env |
the environment this def belongs to; the same id may be defined once per environment |
secret |
true: the value is never stored or shown, and ds:block refuses to render it |
source |
the provider of a secret whose address does not say: env, github, 1password, aws, gcp, vault, file |
from |
the id this value is copied from; builds a chain |
truth |
true marks the single authoritative root of a chain |
sync |
the script or job that copies this hop from its from |
runnable |
true lets ds:run id=… execute this block |
deprecated |
a date (YYYY-MM-DD); citations get an info finding from then on |
sunset |
a date; after it every citation is an error |
desc |
one line, shown in hovers and the reverse index |
doc |
path#anchor: the def's home page, declared from the defining side |
Each change to a cited block is classified (body, signature, type, renamed, moved, value, comment, unknown; whitespace is never reported). A one-line constant changes by value when its value changes and by type when what it declares changes. The def's stability decides which classes flag the sentences citing it:
stability |
Flags on |
|---|---|
frozen |
any change except whitespace |
stable (default) |
everything except whitespace and comment-only changes |
api |
signature, type, renamed, value, and changes it could not classify — not a body-only change |
volatile |
nothing |
Three functions, cited from one sentence:
package store
// ds:def id=stab-api-t3u4v5w6 stability=api
func Api(a int) int {
return a + 1
}
// ds:def id=stab-volatile-u3v4w5x6 stability=volatile
func Vol(a int) int {
return a + 1
}
// ds:def id=stab-frozen-v3w4x5y6 stability=frozen
func Frozen(a int) int {
return a + 1
}# Stab
[api](ds:block?id=stab-api-t3u4v5w6) and [vol](ds:block?id=stab-volatile-u3v4w5x6) and [frozen](ds:block?id=stab-frozen-v3w4x5y6) add one.A body-only edit to each flags only the frozen one:
$ sed -i.bak 's/return a + 1/return a + 2/' store/stab.go && rm store/stab.go.bak
$ ds check
docs/stab.md
3 error unacked stab-frozen-v3w4x5y6 changed (body) since this sentence was first cited
| - return a + 1
| + return a + 2
still true: ds ack stab-frozen-v3w4x5y6 --doc docs/stab.md --line 3 --note '…'
otherwise: edit the sentence at docs/stab.md:3, then ack
1 error, 2 ok
Changing the parameter type of the api function is a signature change and is flagged, but only after ds scan has recorded the new block; run ds scan before ds check when relying on stability=api:
$ sed -i.bak 's/func Api(a int)/func Api(a int64)/' store/stab.go && rm store/stab.go.bak
$ ds scan
2 files, 3 defs, 3 refs, 0 problems, 0 skipped
$ ds check
docs/stab.md
3 error unacked stab-api-t3u4v5w6 changed (signature, body) since this sentence was first cited
…
The full classification rules are in SPEC §20.
A fact is a def whose text is one line. In markdown, write it as a link whose text is the value. The first place a fact is written is its home; other pages cite it with ds:cfg.
# Facts
- The API runs on port [8081](ds:def?id=api-port-h3v8n2wd&type=int).
- Sessions expire after [30 days](ds:def?id=sess-ttl-p2c4y7mk&type=duration).
- The app is hosted at [https://example.com](ds:def?id=app-host-d4k8w2mn&type=url).ds facts lists every one-line def in the repository, from prose and from code and config alike, with its current value and how many places cite it:
$ ds facts
ID VALUE WHERE CITED BY
api-port-h3v8n2wd 8081 docs/facts.md:3 0
sess-ttl-p2c4y7mk 30 days docs/facts.md:4 0
app-host-d4k8w2mn https://example.com docs/facts.md:5 0
ds render prints each inline def as its plain text; type=url renders as a link:
$ ds render docs/facts.md
# Facts
- The API runs on port 8081.
- Sessions expire after 30 days.
- The app is hosted at [https://example.com](https://example.com).
JSON, CSV and go.sum have no comment syntax, so a directive cannot go inside them. Put the def in any file that can hold one (usually the doc that talks about the value) and point at the target with file= and pick=:
{"server": {"port": 8081, "host": "api.example.com"}}# Port
<!-- ds:def id=json-port-k3m4n5p6 file=config/app.json pick=json:$.server.port type=int -->
Port is [8081](ds:cfg?id=json-port-k3m4n5p6).$ ds render docs/port.md
# Port
Port is 8081.
A remote def hashes the picked value, so the citation is flagged when server.port changes. If the key disappears, the citation is pick failed. Unlike a def in the file itself, a remote def does not follow its target if the value moves to another file.
ds def refuses to write into a file with no comment syntax, and says so:
$ ds def config/app.json:1
ds: docsync: no comment carrier for this file type: .json has no comment syntax docsync knows, so a directive cannot be written into it; bind it from a file that does with a remote def (`file=config/app.json pick=…`)…
A bare ds:def line already present in such a file is reported as directive is not inside a comment, and ds repair --apply removes it; in a file that does have comments, ds repair --apply comments it instead.
pick= on a def narrows what is extracted. It returns exactly one line (a value, what ds:cfg shows) or one contiguous range (what ds:block shows). Most defs need no pick: a config key picks its value, a markdown fact picks its link text, a code symbol picks its block, a plain-text def picks to the next blank line.
pick= |
Takes |
|---|---|
json:$.server.port |
a JSON value by path |
yaml:server.port, toml:server.port, ini:server.port, env:API_KEY, hcl:resource.aws_instance.web.instance_type |
a key's value |
yaml:tasks.deploy, toml:server, json:$.scripts, hcl:resource.aws_instance.web |
a key that holds a mapping, list, table, object or block: the key and everything under it, as a block for ds:block |
symbol:Manifest, symbol:Store.Save |
a declaration in a code file, by name: the block ds def file#Name would bind, with the same hash |
csv:r2c2, csv:col=port |
a CSV cell |
line:3 |
one line of the target |
regex:'secrets\.(\w+)' |
the first capture group of the first match |
after:'…', between:'(',')' |
the text after a marker, or between two |
url |
the first URL in the target |
heading, section:"Session policy", paragraph:2, link:1 |
parts of a markdown target |
file |
the whole file, hashed (for images, PDFs and generated files) |
These targets:
server:
port: 8081[server]
port = 8082[server]
port = 8083API_KEY=abcresource "aws_instance" "web" {
instance_type = "t3.micro"
}name,port
api,8084see https://example.com/x for more
version: 1.2.3 (stable)
# T
## Install
Run it.
Second para.and these remote defs, one per target line:
# Pick
<!-- ds:def id=p-yaml-a2a2a2a2 file=t/a.yaml pick=yaml:server.port -->
<!-- ds:def id=p-toml-b2b2b2b2 file=t/a.toml pick=toml:server.port -->
<!-- ds:def id=p-ini-c2c2c2c2 file=t/a.ini pick=ini:server.port -->
<!-- ds:def id=p-env-d2d2d2d2 file=t/a.env pick=env:API_KEY -->
<!-- ds:def id=p-hcl-e2e2e2e2 file=t/a.tf pick=hcl:resource.aws_instance.web.instance_type -->
<!-- ds:def id=p-csvc-f2f2f2f2 file=t/a.csv pick=csv:col=port -->
<!-- ds:def id=p-url-g2g2g2g2 file=t/a.txt pick=url -->
<!-- ds:def id=p-after-h2h2h2h2 file=t/a.txt pick="after:'version: '" -->
<!-- ds:def id=p-head-k2k2k2k2 file=t/b.md pick=heading -->
<!-- ds:def id=p-para-n2n2n2n2 file=t/b.md pick=paragraph:2 -->
<!-- ds:def id=p-sect-m2m2m2m2 file=t/b.md pick=section:"Install" -->give these values:
$ ds facts
ID VALUE WHERE CITED BY
p-yaml-a2a2a2a2 8081 t/a.yaml:2 0
p-toml-b2b2b2b2 8082 t/a.toml:2 0
p-ini-c2c2c2c2 8083 t/a.ini:2 0
p-env-d2d2d2d2 abc t/a.env:1 0
p-hcl-e2e2e2e2 t3.micro t/a.tf:2 0
p-csvc-f2f2f2f2 8084 t/a.csv:2 0
p-url-g2g2g2g2 https://example.com/x t/a.txt:1 0
p-after-h2h2h2h2 1.2.3 (stable) t/a.txt:2 0
p-head-k2k2k2k2 T t/b.md:1 0
p-para-n2n2n2n2 Second para. t/b.md:7 0
$ ds read p-sect-m2m2m2m2
## Install
Run it.
Second para.
A regex pick in a code comment:
// ds:def id=app-stripe-key-m4w8k2qn secret=true source=env from=gh-stripe-key-r4t6x2mb pick=regex:'"(\w+)"'
key := os.Getenv("STRIPE_KEY")picks STRIPE_KEY out of the line (see Secrets and chains).
A pick argument that contains a space must have the whole value quoted, because a value is only quoted when it starts with a quote: write pick="after:'version: '", not pick=after:'version: '; the second is a problem, key must match [a-z][a-z0-9_]*.
Two remote defs whose different picks land on the same line of the same file are two blocks; only the same pick twice is reported as two ids bound to the same block:
<!-- ds:def id=p-betw-j2j2j2j2 file=t/a.txt pick=between:'(',')' -->$ ds scan
9 files, 12 defs, 0 refs, 0 problems, 0 skipped
A remote def can bind a block, not only a value, so a runbook can cite a Taskfile task or a Go declaration without a directive written into that file. That matters when the file belongs to someone else, or when a comment in it would break a tool that reads it.
version: '3'
tasks:
deploy:
desc: Run one image tag
cmds:
- lath run deploy releasepackage params
// Manifest lists every key.
var Manifest = []string{
"APP_KEY",
"DB_PASSWORD",
}# Run
<!-- ds:def id=task-deploy-a1a1a1a1 file=Taskfile.yml pick=yaml:tasks.deploy -->
<!-- ds:def id=manifest-b2b2b2b2 file=manifest.go pick=symbol:Manifest -->
Deploy with [the deploy task](ds:block?id=task-deploy-a1a1a1a1). Keys are [the manifest](ds:block?id=manifest-b2b2b2b2).$ ds scan
3 files, 2 defs, 2 refs, 0 problems, 0 skipped
$ ds locate task-deploy-a1a1a1a1
Taskfile.yml:3-6 @ …
$ ds read manifest-b2b2b2b2
var Manifest = []string{
"APP_KEY",
"DB_PASSWORD",
}
yaml:tasks.deploy binds the task's key and body; symbol:Manifest binds the declaration the Go parser finds under that name, exactly as an in-file ds def manifest.go#Manifest would, and hashes it the same way. Neither file was changed. Any edit inside the task or the list flags the sentences that cite it, with a diff. Because these picks are by key and by name, moving the task or the declaration within its file is reported as moved and still resolves; line:N is the one pick that does not follow its block, so it is the fragile choice for anything that may gain lines above it.
ds:block has two shapes, chosen by the carrier. Both examples cite this function:
package store
import "context"
type Session struct {
ID string
}
type Store struct{}
var ErrEmpty error
// SaveSession writes the session.
// ds:def id=store-savesession-m6twuucd
func (s *Store) SaveSession(ctx context.Context, sess Session) error {
if sess.ID == "" {
return ErrEmpty
}
return nil
}As a link, it is a citation: the sentence around it depends on the block, and is flagged when the block changes. In block position (an HTML comment on its own line), it shows the code itself when the page is rendered:
# Sessions
The guard is [`SaveSession`](ds:block?id=store-savesession-m6twuucd). It returns early when the id is empty.
<!-- ds:block id=store-savesession-m6twuucd lines=1-3 title="the guard" -->$ ds render docs/sessions.md
# Sessions
The guard is [`SaveSession`](../store/store.go#L15-L20). It returns early when the id is empty.
**the guard** · [`store/store.go:15-20`](../store/store.go#L15-L20)
```go
func (s *Store) SaveSession(ctx context.Context, sess Session) error {
if sess.ID == "" {
return ErrEmpty
```
The repository never holds the copy; it is produced at build time from live source.
| Key | Meaning |
|---|---|
id |
required; the block |
lines |
a fragment a-b or a, counted from 1 within the block |
title |
the caption shown above a rendered block |
at |
a commit: a deliberate snapshot, rendered with an "as of" badge and never flagged |
translates |
true on a translated paragraph; see Translations |
assert |
true when the cited block is a test, so the sentence also depends on that test passing in the last published CI run |
label |
a short name for the claim, to make ack messages readable |
branch |
the block as published from a named branch, in a workspace |
env |
which environment's def to cite |
What ds check reports:
okwhile the block is unchanged,moved(no severity) when it only moved.unackedwhen it changed since the sentence was acked or first cited. Read the sentence; if it is still true, ack it with the command shown, otherwise edit it and then ack:
$ sed -i.bak 's/return nil/return save(sess)/' store/store.go && rm store/store.go.bak
$ ds check
docs/sessions.md
3 error unacked store-savesession-m6twuucd changed (body) since this sentence was first cited
| - return nil
| + return save(sess)
still true: ds ack store-savesession-m6twuucd --doc docs/sessions.md --line 3 --note '…'
otherwise: edit the sentence at docs/sessions.md:3, then ack
5 error unacked store-savesession-m6twuucd changed (body) since this sentence was first cited
| - return nil
| + return save(sess)
still true: ds ack store-savesession-m6twuucd --doc docs/sessions.md --line 5 --note '…'
otherwise: edit the sentence at docs/sessions.md:5, then ack
2 error
$ ds ack store-savesession-m6twuucd --all --note 'still returns early'
acked store-savesession-m6twuucd at docs/sessions.md:3 (human)
acked store-savesession-m6twuucd at docs/sessions.md:5 (human)
$ ds check
2 ok
brokenwhen the id is not defined,rangewhenlines=falls outside the block or is not a range (a warning, and an error in repo mode, whereds refreshcannot write that copy), andtoo-largewhen a block-position cite would render more than[include] max_lineslines (default 40):
# More
[missing](ds:block?id=nothing-here-abcdefgh)
<!-- ds:block id=store-savesession-m6twuucd lines=4-30 -->$ ds check
docs/more.md
3 error broken nothing-here-abcdefgh is not defined
fix: the id nothing-here-abcdefgh is not defined; fix the id in docs/more.md:3 or re-add the ds:def on the block it meant
5 warning range lines=4-30 outside the block's 6 lines
fix: adjust lines= at docs/more.md:5 to fit the block's 6 lines
1 error, 1 warning, 2 ok
Deleting a def breaks its citations and says where it was last seen:
$ sed -i.bak '/ds:def id=store-savesession/d' store/store.go && rm store/store.go.bak
$ ds check
docs/more.md
3 error broken nothing-here-abcdefgh is not defined
fix: the id nothing-here-abcdefgh is not defined; fix the id in docs/more.md:3 or re-add the ds:def on the block it meant
5 error broken store-savesession-m6twuucd was deleted (last seen store/store.go:15)
fix: the id store-savesession-m6twuucd is not defined; fix the id in docs/more.md:5 or re-add the ds:def on the block it meant
docs/sessions.md
3 error broken store-savesession-m6twuucd was deleted (last seen store/store.go:15)
fix: the id store-savesession-m6twuucd is not defined; fix the id in docs/sessions.md:3 or re-add the ds:def on the block it meant
5 error broken store-savesession-m6twuucd was deleted (last seen store/store.go:15)
fix: the id store-savesession-m6twuucd is not defined; fix the id in docs/sessions.md:5 or re-add the ds:def on the block it meant
4 error
at= is a snapshot and ds check reports it as ok with "snapshot pinned". ds render shows the block as it was at that commit, with an "as of" badge and a link to the lines it occupied then; it reads the file and .ds/ledger.tsv from that commit, so it needs the ledger committed there. Where either is missing it prints the badge and the link and notes no snapshot for … rendering the link only.
ds:cfg inlines the current value of a one-line def. Link form only: <!-- ds:cfg id=… --> on a line of its own is a scan problem that fails ds check, because ds render cannot put a value there. The link text is the last known value, so the raw markdown still reads well; the build replaces it.
package store
const (
// ds:def id=maxsessions-k8p2w4rd owner=@auth
MaxSessions = 5
)# Limits
A user may hold at most [5](ds:cfg?id=maxsessions-k8p2w4rd) sessions.$ ds render docs/limits.md
# Limits
A user may hold at most 5 sessions.
A Go constant, a YAML key, a TOML key, a fact and a remote def all work as the target.
| Key | Meaning |
|---|---|
id |
required; a def whose extracted value is one line |
format |
raw (default), code, quote, host, link, compact |
env |
which environment's def; default from [env] default or ds render --env |
Formats, all from one def holding https://example.com and one holding 8081. Put each inline def on its own line: two facts on one line are reported as two ids bound to the same block.
# Fmt
- The API runs on port [8081](ds:def?id=api-port-h3v8n2wd&type=int).
- It is hosted at [https://example.com](ds:def?id=app-host-d4k8w2mn&type=url).
- raw [x](ds:cfg?id=app-host-d4k8w2mn)
- code [x](ds:cfg?id=app-host-d4k8w2mn&format=code)
- quote [x](ds:cfg?id=app-host-d4k8w2mn&format=quote)
- host [x](ds:cfg?id=app-host-d4k8w2mn&format=host)
- link [x](ds:cfg?id=app-host-d4k8w2mn&format=link)
- compact [x](ds:cfg?id=api-port-h3v8n2wd&format=compact)$ ds render docs/fmt.md
# Fmt
- The API runs on port 8081.
- It is hosted at [https://example.com](https://example.com).
- raw https://example.com
- code `https://example.com`
- quote "https://example.com"
- host example.com
- link [https://example.com](https://example.com)
- compact 8.1K
Citing a block that yields more than one line is refused, and a changed value is unacked, like a changed block:
package store
// ds:def id=save-func-w2x3y4z5
func Save() error {
return nil
}Saving is [this](ds:cfg?id=save-func-w2x3y4z5).$ sed -i.bak 's/MaxSessions = 5/MaxSessions = 8/' store/limits.go && rm store/limits.go.bak
$ ds check
docs/limits.md
3 error unacked maxsessions-k8p2w4rd changed (value) since this sentence was first cited
| -5
| +8
still true: ds ack maxsessions-k8p2w4rd --doc docs/limits.md --line 3 --note '…'
otherwise: edit the sentence at docs/limits.md:3, then ack
5 warning range ds:cfg on a 3-line block
fix: the def yields 3 lines; ds:cfg needs one line, use ds:block or a narrower pick=
1 error, 1 warning, 6 ok
A claim needs no def. It puts a review date on a sentence, for statements you want to be reminded about even when no code changed.
| Key | Meaning |
|---|---|
owner |
who reviews it |
reviewed |
the last review date, YYYY-MM-DD |
expires |
how long a review lasts, such as 90d |
about |
comma list of ids; a change to any of them also expires the claim |
package store
const (
// ds:def id=maxsessions-k8p2w4rd
MaxSessions = 5
)# Claims
We chose Postgres over Redis because ops already runs Postgres. <!-- ds:claim owner=@platform reviewed=2026-09-06 expires=3650d -->
The cache is warmed nightly. <!-- ds:claim owner=@platform reviewed=2026-01-01 expires=30d -->
Sessions are capped. <!-- ds:claim owner=@auth reviewed=2026-09-20 expires=3650d about=maxsessions-k8p2w4rd -->The first claim is within its window; the second is past it:
$ ds check
docs/claims.md
5 error expired claim reviewed 2026-01-01 expired after 30d
fix: review the claim at docs/claims.md:5 and run ds ack --doc docs/claims.md --line 5 to renew it
1 error, 2 ok
With about=, a change to a listed block expires the claim at once:
$ sed -i.bak 's/MaxSessions = 5/MaxSessions = 9/' store/limits.go && rm store/limits.go.bak
$ ds check
docs/claims.md
5 error expired claim reviewed 2026-01-01 expired after 30d
fix: review the claim at docs/claims.md:5 and run ds ack --doc docs/claims.md --line 5 to renew it
7 error expired claim is about maxsessions-k8p2w4rd, which changed (value)
fix: review the claim at docs/claims.md:7 and run ds ack --doc docs/claims.md --line 7 to renew it
2 error, 1 ok
Renew a claim with ds ack --doc <doc> --line <n> --note '…'. For an about= claim the ack also records what the listed blocks are now, so the change it accepts stops expiring the claim, and the next change to them expires it again:
$ ds ack --doc docs/claims.md --line 7 --note 'cap still applies'
renewed the claim at docs/claims.md:7 (human)
$ ds check
docs/claims.md
5 error expired claim reviewed 2026-01-01 expired after 30d
fix: review the claim at docs/claims.md:5 and run ds ack --doc docs/claims.md --line 5 to renew it
1 error, 2 ok
# Links
See the [Go spec](ds:url?href=https://go.dev/ref/spec&title=Rust).
The [old address](ds:url?href=http://go.dev/ref/spec) still works.
This [page](ds:url?href=https://go.dev/this-page-does-not-exist-xyz) is gone.| Key | Meaning |
|---|---|
href |
required; the external URL |
title |
the expected page title, or a substring of it; a change means the page moved or was rewritten |
expect |
the HTTP status the final response must have, after redirects; without it any status below 400 is alive. A value that is not a status from 100 to 599 is a problem |
ds render turns it into an ordinary link. Without network access the check is skipped with a warning:
$ ds check
docs/url.md
3 warning unverifiable external link not checked
fix: external link checks need --resolve with network access
5 warning unverifiable external link not checked
fix: external link checks need --resolve with network access
7 warning unverifiable external link not checked
fix: external link checks need --resolve with network access
3 warning
With [resolve] enabled = true in the config and ds check --resolve, ds fetches each page and reports dead (an error), retitled and url moved (warnings). A request that gets no answer at all, such as on a machine with no network, stays unverifiable and is not cached:
$ ds check --resolve
docs/url.md
3 warning retitled title is now "The Go Programming Language Specification - The Go Programming Language"
fix: the page at https://go.dev/ref/spec no longer has title "Rust"; confirm it is still the right page
5 warning url moved http://go.dev/ref/spec redirects to https://go.dev/ref/spec
fix: the link http://go.dev/ref/spec now redirects to https://go.dev/ref/spec; update it at docs/url.md:5
7 error dead https://go.dev/this-page-does-not-exist-xyz returned 404
Remember the link-form rule: a title with spaces must be written with %20.
| Key | Meaning |
|---|---|
exactly one of id, cmd, file |
what to run; id needs runnable=true on the def |
expect |
every mode needs exit status zero, then: ok nothing more; rows some output; an HTTP status such as 200, the last status the output reports (the last HTTP/… status line, else the last non-blank line); anything else a substring the output must contain |
timeout |
a positive duration such as 90s that replaces run.timeout for this command; one that does not parse is a problem and the command does not run |
env, show |
environment, and what the renderer shows (output, command, both, none) |
The def for an id= run is a block marked runnable:
# ds:def id=hello-task-q3r4s5t6 runnable=true
echo hello from task
# ds:def id=not-runnable-r3s4t5u6
echo nope# Run
<!-- ds:run id=hello-task-q3r4s5t6 expect=ok -->
<!-- ds:run id=not-runnable-r3s4t5u6 expect=ok -->
<!-- ds:run cmd="echo hi" expect="hi" -->Nothing runs unless the config enables it and the doc matches allow:
[run]
enabled = true
allow = ["docs/**"]and then only with ds check --run. Without --run each directive is skipped (info). With it:
$ ds check --run
docs/run.md:3 run ok: echo hello from task
docs/run.md:5 run skipped: not-runnable-r3s4t5u6 is not runnable=true
docs/run.md:7 run ok: echo hi
docs/run.md
5 info skipped run not executed: not-runnable-r3s4t5u6 is not runnable=true
1 info, 2 ok
Secrets and runs covers the run configuration and its safety rules.
# Tasks
<!-- ds:table kind=task where="state!=done" cols=title,owner -->Keys: kind, where, cols, sort, limit, empty. It needs a record source under [records] in the config (see Configuration); without one it is reported and renders nothing:
$ ds check
…
docs/tasks.md
3 warning unverifiable no record source configured
fix: register a record source in [records] to render ds:table
…
Secrets reach the repository as addresses (${{ secrets.STRIPE_KEY }}, op://…, an env var name), and docsync works on those addresses only. Mark the def secret=true; link copies of one value with from= and mark the single root truth=true. ds:chain renders the path:
STRIPE_KEY=op://Platform/stripe-prod/credential # ds:def id=op-stripe-key-p9c2v7ld secret=true truth=trueenv:
STRIPE_KEY: ${{ secrets.STRIPE_KEY }} # ds:def id=gh-stripe-key-r4t6x2mb secret=true from=op-stripe-key-p9c2v7ld sync=scripts/sync-secrets.shpackage store
import "os"
func Key() string {
// ds:def id=app-stripe-key-m4w8k2qn secret=true source=env from=gh-stripe-key-r4t6x2mb pick=regex:'"(\w+)"'
key := os.Getenv("STRIPE_KEY")
return key
}# Secrets
Stripe: <!-- ds:chain id=app-stripe-key-m4w8k2qn -->$ ds why app-stripe-key-m4w8k2qn --chain
app-stripe-key-m4w8k2qn stmt store/pay.go:7-7
docs/sec.md:3 ds:chain Stripe:
app-stripe-key-m4w8k2qn STRIPE_KEY store/pay.go:7
from gh-stripe-key-r4t6x2mb ${{ secrets.STRIPE_KEY }} .github/workflows/deploy.yml:2
from op-stripe-key-p9c2v7ld op://Platform/stripe-prod/credential .env.tpl:1 TRUTH
ds:chain takes id and env. A ds:block of a secret def renders nothing (ds:block refuses to render it). Provider checks, rotation, and the chain findings (unsourced, chain broken, out of sync, stale copy) are in Secrets and runs and SPEC §12.
The same id can be defined once per environment with env=, and cites choose one with env=:
API_HOST=api.example.com # ds:def id=api-host-r2s3t4u5 env=prodAPI_HOST=staging.example.com # ds:def id=api-host-r2s3t4u5 env=staging# Hosts
Prod is [api](ds:cfg?id=api-host-r2s3t4u5&env=prod), staging is [s](ds:cfg?id=api-host-r2s3t4u5&env=staging).$ ds render docs/hosts.md
# Hosts
Prod is api.example.com, staging is staging.example.com.
A cite without env= uses [env] default, or --env on ds check and ds render; set the default rather than relying on whichever def is found first. Citing an environment with no def is broken:
# Dev
Dev is [d](ds:cfg?id=api-host-r2s3t4u5&env=dev).$ ds check
docs/dev.md
3 error broken api-host-r2s3t4u5 is not defined for env=dev
fix: api-host-r2s3t4u5 has no definition for env=dev; add one or cite a defined environment
1 error, 2 ok
package store
// ds:def id=legacy-save-q2w3e4r5 deprecated=2026-09-01 desc="old writer"
func LegacySave() {}
// ds:def id=older-save-t6y7u8i9 sunset=2026-09-01
func OlderSave() {}# Old
Use [legacy](ds:block?id=legacy-save-q2w3e4r5) or [older](ds:block?id=older-save-t6y7u8i9).Every citation of the first gets an info finding from its date; every citation of the second fails after its date:
$ ds check
docs/old.md
3 info deprecated legacy-save-q2w3e4r5 deprecated since 2026-09-01
fix: legacy-save-q2w3e4r5 is deprecated since 2026-09-01; plan to move the reference at docs/old.md:3
3 error sunset older-save-t6y7u8i9 reached sunset 2026-09-01
fix: older-save-t6y7u8i9 passed its sunset date 2026-09-01; remove the reference at docs/old.md:3
1 error, 1 info
Define the source paragraph, then mark the translated paragraph with translates=true:
# Policy
<!-- ds:def id=retention-para-w4x5y6z7 -->
Logs are kept for ninety days.# Política
<!-- ds:block id=retention-para-w4x5y6z7 translates=true -->
Los registros se guardan noventa días.When the source changes:
$ sed -i.bak 's/ninety days/sixty days/' docs/policy.md && rm docs/policy.md.bak
$ ds check
docs/es/policy.md
3 error translation stale source retention-para-w4x5y6z7 changed (body)
| -Logs are kept for ninety days.
| +Logs are kept for sixty days.
fix: the source paragraph retention-para-w4x5y6z7 changed; update the translation at docs/es/policy.md:3 and ack
1 error
A markdown page can declare itself the home of ids in its frontmatter:
package store
type Session struct {
// ds:def id=session-user-yk7dsbtd
User string
}---
title: Store
ds:
covers: [session-user-yk7dsbtd, gone-thing-abcdefgh]
review_every: 180d
---
# Store
Each session belongs to one user.A covered id no longer counts as uncovered. A covered id that is not defined is an orphan:
$ ds check
docs/store.md
1 warning orphan covers gone-thing-abcdefgh, which is not defined
fix: the page docs/store.md covers gone-thing-abcdefgh, which is not defined; remove it from covers or restore the def
1 warning
review_every asks for the whole page to be re-read on a schedule even when nothing it cites changed; ds ack --doc <page> records the review. A def that nothing cites or covers is reported as uncovered (info), which never fails a run.
# Bad
<!-- ds:block id=x-abcdefgh id=y -->
<!-- ds:def owner=@x -->
Para with no id.package store
ds:def id=bad-go-e3f4g5h6
var C = 1$ ds check
docs/bad.md
3 error problem column 23: directive: duplicate key
fix: fix the directive at docs/bad.md:3: column 23: directive: duplicate key
5 error problem extract: ds:def without id=
fix: fix the directive at docs/bad.md:5: extract: ds:def without id=
store/bad.go
3 error problem scan: directive is not inside a comment: store/bad.go carries comments, so a bare directive line is probably not valid there; ds repair --apply comments it
fix: fix the directive at store/bad.go:3: scan: directive is not inside a comment: store/bad.go carries comments, so a bare directive line is probably not valid there; ds repair --apply comments it
3 info uncovered defined but never cited or covered
fix: bad-go-e3f4g5h6 is defined but nothing cites or covers it; cite it from a page or remove the def
3 error, 1 info
$ ds repair
store/bad.go:3
- ds:def id=bad-go-e3f4g5h6
+ // ds:def id=bad-go-e3f4g5h6
1 line(s) would be repaired, 0 need a person (run with --apply to write)
| You wrote | ds check says |
|---|---|
a def with no id= |
error problem extract: ds:def without id= |
| the same key twice | error problem column N: directive: duplicate key |
| an id nobody defines | error broken nothing-here-abcdefgh is not defined |
| a misspelled verb or key | warning unknown (an error with --strict) |
a bare ds:def line in Go or JSON |
error problem scan: directive is not inside a comment: … (ds repair --apply comments it, or removes it in JSON) |
| two defs over the same lines | error problem scan: two ids bound to the same block: … |
| a def with nothing below it | error problem extract: ds:def has nothing after it to bind to |
a pick= that matches nothing |
error pick failed scan: remote def pick failed: …: pick: nothing matched: … |
The full list of finding states and severities is in SPEC §17.
| Want | Write |
|---|---|
| give code an identity | ds def path#Symbol or ds def path:line, which writes // ds:def id=name-xxxxxxxx |
| give a config line an identity | key: v # ds:def id=name-xxxxxxxx |
| define a fact in prose | [8081](ds:def?id=name-xxxxxxxx&type=int) |
| define a value in a JSON or CSV file | <!-- ds:def id=… file=path pick=json:$.a.b --> |
| cite a block | [text](ds:block?id=…) |
| show a block | <!-- ds:block id=… lines=1-6 title="…" --> |
| freeze a block | <!-- ds:block id=… at=<commit> --> |
| show a value | [8081](ds:cfg?id=…&format=code) |
| bind a sentence to a test | [test](ds:block?id=…&assert=true) |
| a translated paragraph | <!-- ds:block id=… translates=true --> |
| run something | <!-- ds:run id=… expect=ok --> |
| a table over records | <!-- ds:table kind=task where="…" --> |
| a sentence that must age out | <!-- ds:claim owner=@t reviewed=2026-09-06 expires=90d --> |
| an external link | [text](ds:url?href=…&title=…) |
| a secret's path | <!-- ds:chain id=… --> |
| cite from a code comment | // implements ds:block?id=… |