drft-cli 0.3.0

A structural integrity checker for linked file systems
Documentation

drft

A structural integrity checker for linked file systems. drft treats a directory of files as a dependency graph -- files are nodes, links are edges -- and validates the graph against configurable rules.

Install

cargo install drft-cli    # via Cargo
npm install -g drft-cli   # via npm

Or download a prebuilt binary from GitHub Releases.

The binary is called drft.

Quick start

# Check a directory for issues (no setup required)
drft check

# Initialize a config file
drft init

# Snapshot the current state
drft lock

# Check for staleness (files changed since last lock)
drft check

# Verify lockfile is current (for CI)
drft lock --check

What it does

drft discovers files, runs configurable parsers to extract links between them, and builds a dependency graph. It then validates that graph against a set of rules:

Rule Default Description
broken-link warn Link target does not exist
cycle warn Circular dependency detected
directory-link warn Link points to a directory, not a file
stale error Dependency changed since last lock
containment warn Link escapes the graph boundary
encapsulation warn Link reaches into a child graph's non-interface files
orphan off File has no inbound links
indirect-link off Link target is a symlink

See the full documentation for details on parsers, analyses, and rules.

Commands

drft check

Validate the graph against all enabled rules.

drft check                    # check current directory
drft check -C path/to/docs   # check a different directory
drft check --recursive        # include child graphs
drft check --rule orphan      # run only specific rules
drft check --watch            # re-check on file changes
drft check --format json      # machine-readable output

drft lock

Snapshot file hashes to drft.lock. This enables staleness detection -- when a file changes, its dependents are flagged.

drft lock                     # create/update drft.lock
drft lock --check             # verify lockfile is current (CI)
drft lock --recursive         # lock all graphs bottom-up

drft graph

Export the dependency graph.

drft graph                    # JSON Graph Format output
drft graph --format dot       # Graphviz DOT output
drft graph --recursive        # include child graphs

drft impact

Show what depends on the given files (transitively).

drft impact setup.md              # text output
drft impact --format json setup.md  # JSON with fix instructions
drft impact config.md faq.md      # multiple files

drft init

Create a default drft.toml config file.

drft init                              # write default drft.toml
drft init --interface-from README.md   # derive interface from file's outbound links
drft init --no-interface               # config without interface (open graph)

Configuration

drft.toml in the graph root:

# Glob patterns for files to exclude from discovery
ignore = ["drafts/*", "archive/*"]

# Public interface — nodes accessible from parent graphs.
# Presence of this section enables encapsulation.
[interface]
nodes = ["overview.md", "api/*.md"]

# Parsers — which file types to parse and how
[parsers]
markdown = true                    # built-in, all defaults

[parsers.tsx]                      # custom (has command)
glob = "*.tsx"
command = "./scripts/parse-tsx-links.sh"

# Rule severities: "error", "warn", or "off"
# Table form for per-rule options or custom rules
[rules]
broken-link = "error"
cycle = "error"
stale = "error"

[rules.orphan]
severity = "warn"
ignore = ["README.md", "CLAUDE.md"]

[rules.max-fan-out]
command = "./scripts/max-fan-out.sh"
severity = "warn"

drft automatically respects .gitignore.

Graph nesting

A directory with a drft.toml or drft.lock becomes a graph. Child directories with their own config are child graphs -- they appear as Graph nodes in the parent graph and are checked independently.

project/
  drft.lock              # root graph
  drft.toml
  index.md
  docs/
    overview.md
  research/
    drft.toml            # child graph (Graph node in parent)
    drft.lock
    overview.md
    internal.md

Use --recursive to check or lock all graphs in one command. Use [interface] in drft.toml to declare a graph's public interface and control which files are visible to the parent.

Output

Text format (default):

error[broken-link]: index.md -> gone.md (file not found)
error[stale]: index.md (stale via setup.md)
warn[cycle]: cycle detected: a.md -> b.md -> c.md -> a.md

JSON format (--format json):

{"rule":"broken-link","severity":"error","source":"index.md","target":"gone.md","message":"file not found","fix":"gone.md does not exist -- either create it or update the link in index.md"}

Every JSON diagnostic includes a fix field with actionable instructions.

DOT format (--format dot) for graph export:

digraph {
  "index.md"
  "setup.md"
  "index.md" -> "setup.md"
}

Graph JSON follows the JSON Graph Format specification.

Exit codes

Code Meaning
0 Clean (warnings may be present)
1 Rule violations at error severity, or lock --check found lockfile out of date
2 Usage or configuration error

Custom rules

Custom rules are scripts that receive the dependency graph as JSON on stdin and emit diagnostics as newline-delimited JSON on stdout:

[rules.max-fan-out]
command = "./scripts/max-fan-out.sh"
severity = "warn"
#!/bin/sh
# Flag nodes with more than 5 outbound links
python3 -c "
import json, sys
data = json.load(sys.stdin)
for node, count in ...:
    print(json.dumps({'message': '...', 'node': node, 'fix': '...'}))
"

See examples/custom-rules for complete examples.

Security note: Custom rules and custom parsers execute arbitrary shell commands defined in drft.toml. Review the [rules] and [parsers] sections before running drft in untrusted repositories, the same as you would review npm scripts or Makefiles.

LLM integration

drft is designed to work with LLMs as both a validation tool during editing sessions and a CI gate.

JSON output

All commands support --format json. The check command returns a summary envelope:

{
  "status": "warn",
  "total": 2,
  "errors": 0,
  "warnings": 2,
  "diagnostics": [
    {
      "rule": "stale",
      "severity": "warn",
      "message": "stale via",
      "node": "design.md",
      "via": "observations.md",
      "fix": "observations.md has changed — review design.md to ensure it still accurately reflects observations.md, then run drft lock"
    }
  ]
}

Every diagnostic includes a fix field with actionable instructions an LLM can follow directly.

Impact analysis

Before editing a file, check what depends on it:

drft impact observations.md --format json
{
  "files": ["observations.md"],
  "total": 2,
  "impacted": [
    { "node": "problem-statement.md", "via": "observations.md", "fix": "..." },
    { "node": "design.md", "via": "problem-statement.md", "fix": "..." }
  ]
}

Claude Code hooks

Add to your project's .claude/settings.json to run drft automatically after markdown edits:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "if [[ \"$TOOL_INPUT_FILE_PATH\" == *.md ]] || [[ \"$TOOL_INPUT_file_path\" == *.md ]]; then npx drft check --format json 2>/dev/null; fi"
          }
        ]
      }
    ]
  }
}

If drft is installed globally (cargo install drft-cli), use drft instead of npx drft. For npm projects with drft-cli as a devDependency, npx drft ensures it resolves from node_modules.

CI

npx drft check --recursive          # catch violations and staleness
npx drft lock --check --recursive   # verify lockfiles are committed

Run check first — it catches broken links, cycles, and stale files. Then lock --check verifies the lockfile is committed (structural consistency). Both exit with code 1 on failure.

The workflow: edit → drft check (see what's stale) → review impacted files → drft lock (acknowledge the changes) → commit. Lock is the "I've reviewed the impacts" step.

License

MIT