Skip to content

GRP-002 Circular references

Treats Markdown links between documents as a graph and validates that no cycle (a closed path such as A → B → C → A) exists. Detected cycles are reported as error. This is a project-scope rule and is evaluated across all documents loaded via include.

Only relative links to .md files are considered. Pure anchor links (#section) and external URLs are ignored.

A cycle where document A references B, B references C, and C references A cannot be spotted by a human reading a single file. To keep dependencies coherent, the document graph should be a DAG (directed acyclic graph) — cycles confuse readers and make impact analysis on updates much harder. When AI generates documents in bulk, links can become unintentionally bidirectional and form cycles. This rule detects them.

Field Type Required Description
files string Glob of files included in cycle detection. If omitted, every document is included
exclude string[] Array of globs for files to exclude from the graph
siteRouter object Resolves routed URLs (e.g. /docs/x/) so cycle detection can follow links emitted by SSGs like Starlight

The rule works even with no options at all. Use exclude to skip files that intentionally hold links in multiple directions, such as table-of-contents pages or index files.

Without siteRouter, absolute URLs like /docs/x/ are skipped during graph construction, so cycles that pass through SSG-routed links go undetected. The shape is identical to REF-001’s siteRouter.

Sub-field Type Required Description
preset string Known SSG preset. Currently "starlight" only
contentDir string Content directory (e.g. "packages/site/src/content/docs")
defaultLocale string Default locale identifier. Use "root" for Starlight’s prefix-less root locale
locales string[] Supported locale identifiers (e.g. ["root", "ja", "ko", "zh"])
urlPrefix string (Generic, no preset) URL prefix to strip before resolving
indexFile string Index file name to try first (default: "index.md")

Starlight (i18n) example:

{
"rule": "grp002",
"options": {
"siteRouter": {
"preset": "starlight",
"contentDir": "packages/site/src/content/docs",
"defaultLocale": "root",
"locales": ["root", "ja", "ko", "zh"]
}
}
}
docs/a.md:
# A
See [B](./b.md).
docs/b.md:
# B
See [C](./c.md).
docs/c.md:
# C
See [A](./a.md).

The cycle a.md → b.md → c.md → a.md triggers a violation.

docs/a.md
line 2 error Circular reference detected: docs/a.md -> docs/b.md -> docs/c.md -> docs/a.md GRP-002

Cut “the weakest dependency” in the cycle. For example, replace the link from c.md to a.md with prose.

docs/c.md:
# C
This module is consumed by upstream documents.
{
"rule": "grp002",
"options": {
"files": "docs/**/*.md",
"exclude": ["docs/index.md", "docs/sitemap.md"]
}
}

Files listed in exclude are removed from the graph entirely as nodes. Cycles that pass through them are no longer detected, so reserve exclude for table-of-contents-style files that need bidirectional links.