@frycz/dfa
v0.2.0
Published
Scaffold and maintain a doc index (INDEX.json/INDEX.md over frontmatter-tagged docs) for AI coding agents, wired into Claude Code, Cursor, and other agent adapters.
Maintainers
Readme
dfa — docs for AI
Scaffold and maintain a doc index for AI coding agents: a flat
.dfa/index.json + docs/_index.md over a tree of frontmatter-tagged docs, wired into
whichever agent(s) your project uses (Claude Code, Cursor, AGENTS.md, or
a custom instructions file). Design rationale is in
cli.md; the build plan is in
implementation-plan.md.
Install
# try it without installing
npx @frycz/dfa init
# for regular use — installs the `dfa` command
npm i -g @frycz/dfaQuickstart
# interactive: pick which agent(s) to wire up
dfa init
# scaffold a new doc with frontmatter pre-filled
dfa new auth/api.md
# regenerate docs/_index.md + .dfa/*.json
dfa index
# validate + drift-check; exit 1 on problems (CI)
dfa checkinit writes:
<project root>/
dfa.config.json agent selection + doc/notes dir names
CLAUDE.md if `claude` selected (marker-block managed)
.cursor/rules/dfa.mdc if `cursor` selected
AGENTS.md if `agents-md` selected
.dfa/
index.json every doc root's docs + nested projects, for tooling
diagrams.json diagram registry; populated as Mermaid diagrams appear
docs/ one doc root (add more with --docs-dir)
_index.md the index agents read first
_style.md authoring spec agents must read before writing docs
overview.md starter doc
(add _section.md to a subdirectory to give it its own index)
notes/
.gitkeepRe-running init (or sync) is always safe: it never overwrites existing
doc content, and every file it injects into keeps any content you added
outside its <!-- dfa:start -->…<!-- dfa:end --> markers. (dfa index makes
one narrow exception, pruning a related entry whose target was deleted — see
Doc frontmatter.)
Multiple doc roots
A project can own several doc roots — useful in a monorepo where docs sit beside the code they describe:
dfa init --agent claude --docs-dir "docs,packages/api/docs"dfa.config.json stores them as docsDirs: ["docs", "packages/api/docs"].
One project, one .dfa/index.json, one dfa check. Each root gets its own
_index.md (listing only that root's docs, with pointers to the others) and
its own _style.md, so an agent working in a root finds its index and
conventions next to the docs. In index.json, each entry carries the
docsDir it came from and a path relative to that root, so two roots can
hold same-named docs without colliding. A related entry resolves inside its
own root — a pointer that crossed roots would make one root's validity depend
on another's contents.
dfa new packages/api/docs/auth.md infers the root from the path;
--docs-dir names it explicitly; otherwise the first configured root wins.
The legacy single docsDir: "docs" config is still read and normalized to a
one-element docsDirs.
Sections
When one subtree of a doc root grows big enough to crowd out everything else,
give it its own index. A directory becomes a section by holding an authored
_section.md:
# scaffolds the marker
dfa new docs/api/_section.md
dfa index---
title: HTTP API reference
summary: Per-endpoint reference for the public HTTP API — auth, pagination, error codes.
tags: [api, http, reference]
---Those three fields become the section's row in docs/_index.md, in place of
every row its docs would have taken:
## Sections
| Section | Title | Summary | Tags | Index |
|---|---|---|---|---|
| `api/` | HTTP API reference | Per-endpoint reference… | api, http | `docs/api/_index.md` (42 docs) |dfa index generates docs/api/_index.md alongside the marker, listing the
subtree's docs and linking back to the doc root's index and _style.md.
Sections nest: the nearest enclosing _section.md owns a doc.
Unlike a nested project, a section is not a boundary — it only splits the
markdown. Its docs stay in one .dfa/index.json (each tagged with the section
that owns it), in one dfa check, and related still points across sections
freely. Every path in every index under docs/ stays relative to docs/, so
there is one path convention per doc root, not one per index.
Deleting the _section.md folds the subtree back into the root index; the next
dfa index sweeps up the generated _index.md it leaves behind, and dfa
check fails until you run it. A _index.md dfa did not write is never touched.
Nested projects
A directory with its own dfa.config.json is a separate project, not part
of the parent. Every tree walk stops at that boundary, so the parent never
indexes, validates, or lists a child's docs — and the child is fully
operational from its own root, with its own index, style guide, and dfa
check.
The parent still sees it. dfa index records nested projects in
.dfa/index.json and adds a Nested projects section to each _index.md
pointing at the child's index — so an agent reading the parent index knows the
child exists and can follow into it only if the task is there.
$ dfa list
docs/overview.md — Overview
packages/api/docs/auth.md — Auth
packages/web/ — project (1 doc) — run dfa there, or -r-r, --recursive extends list, tree, index, check, and grams into
every nested project (recursively). Writes always stay per-project: index -r
makes each project write its own files, never a merged one, and check -r
fails if any project fails. grams -r qualifies a child's diagram ids
(packages/api#1), since ids are per-project counters.
What the parent records about a child is identity only — its root and doc roots, never a doc count. A count would make the parent's index go stale every time a child added a doc, which is exactly the coupling these boundaries exist to prevent.
Layout
Two kinds of file, split by who reads them:
docs/_section.mdis the one file here a project authors rather than reads: dropping it into a directory turns that subtree into a section (see above). Only its frontmatter is read.docs/_index.mdanddocs/_style.mdlive with the docs, because agents read them as markdown. The leading_marks them as not-a-doc: they carry no frontmatter, sort to the top of a listing, and every command that walks the tree skips them. That prefix is a general rule, sodocs/_draft.mdis likewise ignored byindex,check,tree, andgrams..dfa/index.jsonand.dfa/diagrams.jsonlive in a hidden directory at the project root, next todfa.config.json. Nothing reads them as prose — they're machine state — so they stay out of the doc tree entirely.
Every _index.md and both JSON files are generated; never hand-edit them. dfa
index rewrites them all.
docs/_style.md
init scaffolds an authoring spec at docs/_style.md, and the block injected
into CLAUDE.md / AGENTS.md / .cursor/rules/dfa.mdc tells the agent to
read it before creating or editing any doc. It covers what each frontmatter
field must contain (especially summary, which is the routing signal in
_index.md), one-subject-per-doc, writing for retrieval rather than for
end-to-end reading, where Mermaid fences go so their anchors resolve, and what
never belongs in a doc.
It is scaffolded once and never overwritten — edit it to match your project's
conventions and init/sync will leave your version alone.
Commands
| Command | What it does |
|---|---|
| dfa init [dir] | Scaffold docs/, notes/, the index files, and the chosen agent config file(s). |
| dfa new <path> | Scaffold a new doc under docsDir with the frontmatter template pre-filled — or, for <dir>/_section.md, the marker that makes that subtree a section. |
| dfa index | Regenerate every _index.md (doc roots and sections), .dfa/index.json, and .dfa/diagrams.json from frontmatter. |
| dfa check | Validate frontmatter and drift-check the generated files; exits 1 on problems — wire into CI. |
| dfa agents | List supported agent adapters and which are configured/detected in this project. |
| dfa sync | Reapply the managed config block to every configured agent's file(s). |
| dfa list | List every doc as a project-root-relative path with its title, one per line, in tree order. |
| dfa tree | Print the doc tree — authored docs only, with their titles; generated files are left out. |
Both take -d, --depth <n> to limit how deep they go: dfa list -d 1 shows
only the docs directly in docsDir, and dfa tree -d 1 collapses each
subdirectory to a single line with a count of the docs beneath it.
| dfa grams | List the Mermaid diagrams found across the docs, with their stable ids and anchors. |
| dfa root | Print the project root — the nearest ancestor directory holding dfa.config.json. |
init covers first-time setup; sync re-runs the same injection logic
later — after editing docs, changing the agent list, or upgrading dfa.
Every command except init finds the project itself: it walks up from the
current directory to the nearest dfa.config.json, the way git and npm
locate their roots. So they work from anywhere inside the project — no path
argument to pass or get wrong. dfa init [dir] still takes a directory,
since that is what creates the config in the first place.
init flags
--agent <ids>— comma-separated agent ids, orall. Skips the interactive picker (needed for non-interactive/CI use).
Doc frontmatter
Every doc under docsDir needs:
---
title: Some Title
summary: One or two sentences an agent can match against a task.
module: some-module
tags: [a, b, c]
related: [path/to/doc.md, other/doc.md]
---dfa check fails the build if a required field is missing or a
related path doesn't resolve to a real doc.
dfa index treats those two differently, because index heals and check
gates. Frontmatter it cannot turn into an index row — missing or unparseable
fields — stops the write entirely, since an index silently missing a doc is
worse than one that is visibly stale.
A dangling related is repaired instead. It is what deleting a doc leaves
behind, so dfa index drops the entries whose target is gone and says which:
$ rm docs/auth/tokens.md && dfa index
Pruned from docs/auth/api.md: auth/tokens.md (no such doc)
Wrote docs/_index.md, .dfa/index.json (12 docs, 1 diagram).This is the only case where dfa writes into a doc you authored, so it edits
as little as it can: just the dead entries, leaving key order, quoting, and the
body byte-identical. A related list it cannot read with certainty — one split
across several lines, say — is reported and left alone for you to fix, and
dfa check keeps failing until you do.
Agent adapters
| id | file(s) written |
|---|---|
| claude | CLAUDE.md |
| cursor | .cursor/rules/dfa.mdc |
| agents-md | AGENTS.md |
| generic | a path you set as genericTarget in dfa.config.json |
The notes/ boundary is an instruction in the injected block, the same
for every adapter. dfa no longer writes a permission deny rule into
.claude/settings.json — permissions are the user's file to own, and a
rule dfa installs on their behalf is a surprise in the one file where
surprises are expensive. sync removes the rules older versions wrote,
leaving any others intact.
Why this pattern
Long specs read fully by an agent burn context on parts that don't matter
to the task at hand. dfa implements a flat index — path | title |
summary | tags — that an agent reads first and uses to pick only the
docs it actually needs, instead of loading everything. See
cli.md for the design and its idea.md/docs-spec.md
references (in the sibling doc-index repo this tool grew out of) for
the full pattern writeup and doc-authoring conventions.
Development
npm install
npm run typecheck
npm test
npm run buildLink package locally
npm run build && npm linksrc/core/ holds the framework-agnostic logic (frontmatter scanning,
index/diagram-registry generation, marker-block injection); src/adapters/
holds one file per supported agent; src/commands/ wires both into the
CLI defined in src/cli.ts.
Publishing
The package is on npm as @frycz/dfa —
public despite the scope, because publishConfig.access says so, which is why
no --access flag appears below. Releases are cut by hand from a clean main;
CI runs the tests but never publishes.
# once per machine — check with `npm whoami`
npm login
git switch main && git pull
# bumps package.json, commits, tags vX.Y.Z
npm version minor
# prepublishOnly runs typecheck + test + build first
npm publish
# push the release commit and its tag
git push --follow-tagsTwo guards make a broken release take deliberate effort: npm version refuses
to run on a dirty tree, and prepublishOnly refuses to publish code that
doesn't typecheck, test, and build.
Choosing the bump
| bump | when |
|---|---|
| patch | fixes that change no flag, no generated file, and no library signature |
| minor | new commands or flags, new generated files, additive exports from src/index.ts |
| major | anything that breaks an existing project: a renamed flag, a changed file layout, a changed library signature |
dfa writes into other people's repos, so weigh a release by what it does to a
project that upgrades and re-runs dfa index, not by how the CLI's surface
looks. A change to what init scaffolds or what index generates reaches every
project that upgrades, even when every command and flag stayed the same.
Before you publish
files: ["dist"] limits the tarball to the build output; npm always adds
README.md, LICENSE, and package.json. To see exactly what would go out —
and run the full prepublishOnly chain against it — without uploading anything:
npm publish --dry-runIt ends with the file list, the total size, and the line
Publishing to https://registry.npmjs.org/ with tag latest and public access
(dry-run). No npm login is needed for that.
After you publish
npm view @frycz/dfa version
# runs the published bits, not your working tree
npx -y @frycz/dfa@latest --version
# a real smoke test, away from this repo
mkdir /tmp/dfa-smoke && cd /tmp/dfa-smoke
npx -y @frycz/dfa@latest init --agent claude && npx -y @frycz/dfa@latest checkIf a release turns out to be broken, prefer publishing a fixed patch version
and npm deprecate @frycz/[email protected] "use X.Y.Z+1" over npm unpublish.
Unpublishing breaks anyone who already installed that version, and npm will not
let you reuse the version number afterwards.
