Depending on id-less documents
Point a depends_on entry at a document with no id – a README, a guide, a
campaign brief. Then pick exactly which upstream statuses count as done
enough to depend on.
Before you start
Some documents live under one namespace and one file: a README, a
getting-started guide, a lone campaign brief. They carry no id: field, and
they never need one. Until now, depending on one meant giving it an id it did
not need.
An entry in depends_on now resolves as exactly one of three forms. ctxgrd
picks the right one from how you write the entry – no prefix, no extra
syntax.
Step 1: Write the address that fits the target
- A document id –
ADR-7. Works as it always has. - A bare namespace name –
README. Works only when that namespace bindscore.singleton, the declaration that it holds one document. - A repo-relative path, including the
.mdextension –docs/positioning/001-launch.md. Works for any claimed document, id-less or not.
Configure the two namespaces:
[README]
paths = ["README.md"]
rules = ["core.required-headings", "core.singleton"]
[POSITIONING]
paths = ["docs/positioning/**"]
rules = ["core.required-headings"]Depend on both, each in the form that fits it:
depends_on: [README, docs/positioning/001-launch.md]README addresses the whole namespace, because [README] binds
core.singleton. docs/positioning/001-launch.md addresses one file
directly. POSITIONING does not bind core.singleton, so its bare name
would not resolve – and a directory of launch documents needs the path to
pick one anyway.
$ ctxgrd
ok: 3 documents · 7 rules · 0 diagnosticsStep 2: Recognize the three ways an address fails
Naming a namespace that doesn’t bind core.singleton. ctxgrd names the
missing binding and points you at the forms that work:
error[core.dep-resolved]: depends_on entry 'POSITIONING' does not resolve to a document in the run
--> docs/adrs/001-example.md:5:0
|
5 | depends_on: [README, POSITIONING]
| ^
|
help: add `core.singleton` to [POSITIONING].rules, or write the document's path instead: `docs/positioning/001-launch.md`
note: `POSITIONING` addresses a namespace as a whole, which resolves only when [POSITIONING] binds `core.singleton` — the declaration that it holds one documentPointing at a file no namespace claims. The path form only reaches
documents. A .md file with no id: that no [<NS>].paths glob covers is
not a document, and ctxgrd says why:
error[core.dep-resolved]: depends_on entry 'docs/notes/scratch.md' does not resolve to a document in the run
--> docs/adrs/001-example.md:5:0
|
5 | depends_on: [README, docs/positioning/001-launch.md, docs/notes/scratch.md]
| ^
|
help: add a `paths` glob covering it to a namespace in ctxgrd.toml, or an `id:` to its frontmatter
note: the file exists but no namespace claims it — an unclaimed .md file is not a document (DOC-001)Name a path that does not exist on disk at all, and the error stays. The note drops – there is no file left to explain.
error[core.dep-resolved]: depends_on entry 'docs/nowhere/does-not-exist.md' does not resolve to a document in the run
--> docs/adrs/001-example.md:5:0
|
5 | depends_on: [docs/nowhere/does-not-exist.md]
| ^
|
help: no document in the run has location `docs/nowhere/does-not-exist.md` — check the spelling, and note the path is repo-relative and includes the `.md` extensionGuessing an id for a document that never had one. An id-less document
carries no id at all – not even an internal placeholder. Writing
CAMPAIGN-0 in depends_on behaves like naming any id that does not exist:
error[core.dep-resolved]: depends_on entry 'CAMPAIGN-0' does not resolve to a document in the run
--> docs/adrs/001-example.md:5:0
|
5 | depends_on: [README, docs/positioning/001-launch.md, CAMPAIGN-0]
| ^
|
help: create the missing document (`ctxgrd new CAMPAIGN "..."`) or remove `CAMPAIGN-0` from `depends_on`Use the namespace name or the path instead.
Step 3: Bind core.singleton, and watch for two files claiming one namespace
The bare-namespace form only works once the target namespace binds
core.singleton. paths can still list several literal filenames for one
document. A CONTRIBUTING.md a repository keeps at its root or under
.github/ is one example:
[CONTRIBUTING]
paths = ["CONTRIBUTING.md", ".github/CONTRIBUTING.md"]
rules = ["core.required-headings", "core.singleton"]If both files exist at once, core.singleton reports the one it does not
treat as authoritative:
error[core.singleton]: [CONTRIBUTING] holds one document, but this file and `CONTRIBUTING.md` both claim it
--> .github/CONTRIBUTING.md
help: delete this file, or remove `core.singleton` from [CONTRIBUTING].rules if the namespace holds many documents
note: `CONTRIBUTING.md` matches an earlier [CONTRIBUTING].paths entry, so it is the document `depends_on: [CONTRIBUTING]` addressesThis defaults to an error, so the exit code goes to 1. Set severity = "warning" under [CONTRIBUTING."core.singleton"] to keep it from blocking
CI – the exit code then stays 0. The file matching the earlier entry in
paths is the one README-style addresses reach. Delete the extra file, or
drop core.singleton if the namespace really does hold many documents.
Step 4: Control which upstream statuses count as done
core.dep-status blocks a document at a settled status from depending on one
that is not. Its terminal parameter lists the statuses your own namespace
treats as settled. By default, every upstream is checked against that same
list.
That single list breaks down once you depend on two upstreams with different
vocabularies. A checklist’s real end states are sealed and abandoned –
both mean the work stopped. Widen terminal to accept abandoned, and a
sealed checklist passes. So does an abandoned one, which was never the
intent.
satisfied-by names, per upstream namespace, which of its statuses actually
count:
[CAMPAIGN."core.dep-status"]
terminal = ["live", "killed"]
satisfied-by = { CHECKLIST = ["sealed"] }An upstream named in satisfied-by is checked only against its own list. Any
upstream left out falls back to terminal, exactly as before you added the
table. The check works the same whether or not the documents involved carry
an id. This example gives both an id, so the message below stays readable:
error[core.dep-status]: CAMPAIGN-1 is `live` but depends on CHECKLIST-1, whose status `abandoned` is not one of `sealed`
--> docs/campaigns/001-launch.md:5:0
|
5 | depends_on: [CHECKLIST-1]
| ^
|
help: move CHECKLIST-1 to a status [CAMPAIGN."core.dep-status"].satisfied-by accepts for CHECKLIST, or widen that entryMove the checklist to sealed, and the same configuration reports clean:
$ ctxgrd
ok: 2 documents · 5 rules · 0 diagnosticsterminal keeps its other job. It still decides whether your own document is
settled enough for the rule to check its dependencies at all.
Verify what a rule accepts
ctxgrd rules <code> lists every parameter a rule declares, whether you have
used it yet or not:
$ ctxgrd rules core.dep-statusAttribute terminal (config, optional)
Attribute satisfied-by (config, optional)
Attribute severity (config, optional: error|warning)Run it before writing a parameter table, not after. A key it does not list is a typo. ctxgrd now reports that at config-load time, instead of leaving the rule silently inert.
Further reading
- How ctxgrd decides what to lint – the id-claim and path-claim model these address forms build on.
- CLI reference – the full
ctxgrd rulescommand surface. - Polling a feature done-signal in an agent loop
– driving
status --exit-codefrom the same dependency graph.