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

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

Or download a prebuilt binary from [GitHub Releases](https://github.com/johnmdonahue/drft-cli/releases).

The binary is called `drft`.

## Quick start

```bash
# 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](docs/README.md) for details on parsers, analyses, and rules.

## Commands

### `drft check`

Validate the graph against all enabled rules.

```bash
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.

```bash
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.

```bash
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).

```bash
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.

```bash
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:

```toml
# 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`):

```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:

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

Graph JSON follows the [JSON Graph Format](https://github.com/jsongraph/json-graph-specification) 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:

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

```sh
#!/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](examples/custom-rules/drft.toml) 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:

```json
{
  "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:

```bash
drft impact observations.md --format json
```

```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:

```json
{
  "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

```bash
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