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

  1. A document idADR-7. Works as it always has.
  2. A bare namespace nameREADME. Works only when that namespace’s paths names one literal file, with no * or ** in it.
  3. A repo-relative path, including the .md extensiondocs/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"]

[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].paths names one file. docs/positioning/001-launch.md addresses one file directly. Its namespace’s paths is a glob, and no bare name could pick one target from a directory of them.

$ ctxgrd
ok: 5 documents · 12 rules · 0 diagnostics

Step 2: Recognize the three ways an address fails

Naming a glob namespace by its bare name. ctxgrd names the namespace’s paths 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:6:0
      |
    6 | depends_on: [README, POSITIONING]
      | ^
      |
  help: name one document instead — its path (`docs/.../001-slug.md`) or its id (`POSITIONING-<n>`)
  note: `POSITIONING` addresses a namespace as a whole, which resolves only when its `paths` names a single file; [POSITIONING].paths is `docs/positioning/**`

Pointing 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:6:0
      |
    6 | 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:6:0
      |
    6 | 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` extension

Writing the internal placeholder id. An id-less document is tracked internally as <NAMESPACE>-0. That value is never a real address. CAMPAIGN-0 in depends_on behaves like naming any other 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:6:0
      |
    6 | 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: Watch for two files claiming one namespace

The bare-namespace form only works while exactly one file claims the namespace. paths can 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"]

If both files exist at once, ctxgrd warns on the one it does not treat as authoritative:

warning[cfg.singleton-conflict]: [CONTRIBUTING] names a single document, but this file and `CONTRIBUTING.md` both claim it
  --> .github/CONTRIBUTING.md

  help: delete this file, or widen [CONTRIBUTING].paths to a glob if the namespace really holds many documents
  note: [CONTRIBUTING].paths is `CONTRIBUTING.md` or `.github/CONTRIBUTING.md` — a list of literal filenames names one document in whichever of those locations a repository prefers, not one per entry. `CONTRIBUTING.md` matches the earlier pattern, so it is the one `CONTRIBUTING` addresses

This is a warning, so the exit code stays 0. The file matching the earlier entry in paths is the one README-style addresses reach. Delete the extra file, or widen paths to a glob 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 entry

Move the checklist to sealed, and the same configuration reports clean:

$ ctxgrd
ok: 2 documents · 5 rules · 0 diagnostics

terminal 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-status
Attribute   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