Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 27 additions & 8 deletions .claude/skills/drift/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,26 +97,45 @@ Someone changed bound code without updating docs. Read the lint output to see wh
## Anchor syntax

Bindings in `drift.lock`:
```
docs/auth.md -> src/auth/login.ts sig:a1b2c3d4e5f6a7b8
docs/auth.md -> src/auth/provider.ts#AuthConfig sig:c3d4e5f6a7b8a1b2
docs/overview.md -> docs/auth.md#authentication sig:b3c4d5e6f7a8b9c0
```toml
version = 1

[[bindings]]
doc = "docs/auth.md"
target = "src/auth/login.ts"
sig = "a1b2c3d4e5f6a7b8"

[[bindings]]
doc = "docs/auth.md"
target = "src/auth/provider.ts#AuthConfig"
sig = "c3d4e5f6a7b8a1b2"

[[bindings]]
doc = "docs/overview.md"
target = "docs/auth.md#authentication"
sig = "b3c4d5e6f7a8b9c0"
```

Anchors can target code files, code symbols (`file#Symbol`), or doc headings (`doc.md#heading-slug`). Heading fragments use GitHub-style slugs (lowercase, hyphens).

`drift link` writes bindings to `drift.lock` with content signatures (`sig:<hex>`). Content signatures are AST fingerprints of the target, so staleness detection works without querying VCS history. This means `drift link` works on uncommitted files — no need to commit first.
`drift link` writes bindings to `drift.lock` with content signatures (`sig = "<hex>"`). Content signatures are AST fingerprints of the target, so staleness detection works without querying VCS history. This means `drift link` works on uncommitted files — no need to commit first.

When relinking a stale anchor, `drift link` refuses and prints both sides (doc section and current code) so you can review the change. Pass `--doc-is-still-accurate` to confirm the doc doesn't need updates.

`drift lint` also checks all markdown links (`[text](path.md)`) in drift-managed docs for existence — broken links are reported as `BROKEN` without needing a lockfile entry.

## Cross-repo docs (origin)

Docs installed from other repos (like this skill) carry `origin:` on their bindings in `drift.lock` so `drift check` skips their anchors in consumer repos. If you're writing a doc that will be distributed to other repos, add origin to prevent false positives:
Docs installed from other repos (like this skill) carry `origin` on their bindings in `drift.lock` so `drift check` skips their anchors in consumer repos. If you're writing a doc that will be distributed to other repos, add origin to prevent false positives:

```
docs/skill.md -> src/main.ts sig:a1b2c3d4e5f6a7b8 origin:github:your-org/your-repo
```toml
version = 1

[[bindings]]
doc = "docs/skill.md"
target = "src/main.ts"
origin = "github:your-org/your-repo"
sig = "a1b2c3d4e5f6a7b8"
```

## Staleness
Expand Down
35 changes: 25 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,18 +67,27 @@ drift link docs/auth.md

After linking, bindings live in `drift.lock` at the repo root:

```
docs/auth.md -> src/auth/login.ts sig:e4f8a2c10b3d7890
docs/auth.md -> src/auth/provider.ts#AuthConfig sig:1a2b3c4d5e6f7890
```toml
version = 1

[[bindings]]
doc = "docs/auth.md"
target = "src/auth/login.ts"
sig = "e4f8a2c10b3d7890"

[[bindings]]
doc = "docs/auth.md"
target = "src/auth/provider.ts#AuthConfig"
sig = "1a2b3c4d5e6f7890"
```

You can also use inline references in the doc body — `@./src/auth/provider.ts#AuthConfig` — and `drift link` will stamp those in the lockfile too.

Every anchor has three parts:

```
src/auth/provider.ts #AuthConfig sig:1a2b3c4d5e6f7890
└── file path ──────┘ └─ symbol ─┘ └──── signature ────┘
src/auth/provider.ts #AuthConfig sig = "1a2b3c4d5e6f7890"
└── file path ──────┘ └─ symbol ─┘ └──────── signature ────────┘
```

- **Path** — the file you're binding to, relative to the repo root.
Expand All @@ -87,10 +96,16 @@ src/auth/provider.ts #AuthConfig sig:1a2b3c4d5e6f7890

### Cross-repo docs (origin)

Docs that travel across repo boundaries — installed skills, vendored docs, shared templates — can declare where their anchors belong via a trailing `origin:` field:
Docs that travel across repo boundaries — installed skills, vendored docs, shared templates — can declare where their anchors belong via an `origin` field:

```
docs/skill.md -> src/main.zig sig:a1b2c3d4e5f67890 origin:github:fiberplane/drift
```toml
version = 1

[[bindings]]
doc = "docs/skill.md"
target = "src/main.zig"
origin = "github:fiberplane/drift"
sig = "a1b2c3d4e5f67890"
```

When `origin` doesn't match the current repo, `drift check` skips those anchors instead of reporting false "file not found" errors. Anchors without `origin` are always checked.
Expand All @@ -109,7 +124,7 @@ drift refs Reverse lookup — which docs reference a given file

## How staleness works

Each anchor's `sig:` field records a fingerprint of the code at the time it was linked. `drift check` recomputes the fingerprint from the current file and compares. For supported languages (TypeScript, Python, Rust, Go, Zig, Java), comparison is syntax-aware — drift parses with tree-sitter and hashes a normalized AST fingerprint (node kinds + token text, no whitespace or position data). Reformatting won't trigger false positives. Symbol-level anchors (`#AuthConfig`) narrow this to just that declaration's subtree. Unsupported languages fall back to raw content comparison.
Each anchor's `sig` field records a fingerprint of the code at the time it was linked. `drift check` recomputes the fingerprint from the current file and compares. For supported languages (TypeScript, Python, Rust, Go, Zig, Java), comparison is syntax-aware — drift parses with tree-sitter and hashes a normalized AST fingerprint (node kinds + token text, no whitespace or position data). Reformatting won't trigger false positives. Symbol-level anchors (`#AuthConfig`) narrow this to just that declaration's subtree. Unsupported languages fall back to raw content comparison.

No VCS history is needed for staleness detection — `drift check` works entirely from the stored signature and current file content.

Expand Down Expand Up @@ -154,7 +169,7 @@ For faster CI runs, use `--changed` to scope checking to docs affected by the fi
- run: drift check --changed src/auth
```

`fetch-depth: 0` is recommended — drift uses VCS history for blame info on stale anchors. With `sig:` provenance (the default), staleness detection itself doesn't need history. The setup action auto-detects platform, downloads the right binary from GitHub releases, and verifies its checksum before installing.
`fetch-depth: 0` is recommended — drift uses VCS history for blame info on stale anchors. With `sig` provenance (the default), staleness detection itself doesn't need history. The setup action auto-detects platform, downloads the right binary from GitHub releases, and verifies its checksum before installing.

## Development

Expand Down
8 changes: 4 additions & 4 deletions docs/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Check all docs for staleness. The primary command. Exits 1 if any anchor is stal
drift check [--format text|json] [--changed <path>] [--silent]
```

Reads bindings from `drift.lock`, recomputes content signatures for each target, and compares against the stored `sig:` values. Reports stale anchors with reasons.
Reads bindings from `drift.lock`, recomputes content signatures for each target, and compares against the stored `sig` values. Reports stale anchors with reasons.

The `--changed <path>` flag scopes checking to docs whose targets match the given path prefix. This enables efficient CI integration — a pipeline that knows which files changed can check only the affected docs without running a full lint.

Expand Down Expand Up @@ -45,7 +45,7 @@ vendor/shared-skill.md
- `STALE` means a lockfile anchor's target has changed since the signature was recorded.
- `BROKEN` means a plain markdown link in the doc points to a file that doesn't exist — no lockfile entry is needed for this check.

Anchors with an `origin:` field that doesn't match the current repo are skipped — they reference files in a different repository.
Anchors with an `origin` field that doesn't match the current repo are skipped — they reference files in a different repository.

## drift status

Expand All @@ -71,7 +71,7 @@ docs/payments.md (1 anchor)

## drift link

Add or refresh bindings in `drift.lock`. `drift link` computes a content signature (`sig:`) from the target file's current syntax fingerprint and writes it to the lockfile. Creates `drift.lock` if it doesn't exist. The lockfile is discovered by walking up from the doc's directory, not from cwd — if a nested `drift.lock` exists closer to the doc, that lockfile is used.
Add or refresh bindings in `drift.lock`. `drift link` computes a content signature (`sig`) from the target file's current syntax fingerprint and writes it to the TOML lockfile. Creates `drift.lock` if it doesn't exist. The lockfile is discovered by walking up from the doc's directory, not from cwd — if a nested `drift.lock` exists closer to the doc, that lockfile is used.

```
drift link <doc-path> <file> [--doc-is-still-accurate]
Expand All @@ -92,7 +92,7 @@ $ drift link docs/overview.md docs/auth.md#Authentication
added docs/overview.md -> docs/auth.md#Authentication sig:d4e5f6a7b8c9d0e1
```

**Blanket mode** — refreshes all `sig:` values for that doc in `drift.lock`:
**Blanket mode** — refreshes all `sig` values for that doc in `drift.lock`:

```
$ drift link docs/auth.md
Expand Down
35 changes: 19 additions & 16 deletions docs/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ The tradeoff is that docs are no longer self-contained — the lockfile must tra

## 4. Content signatures in lockfile

`drift link` stores provenance as `sig:<16-char-hex>` — a content-addressed fingerprint of the anchor's target. The fingerprint is the same normalized syntax hash that staleness detection computes (XxHash3 of the tree-sitter AST walk, or raw XxHash3 for unsupported languages).
`drift link` stores provenance as a `sig` lockfile field containing a 16-character content-addressed fingerprint of the anchor's target. The fingerprint is the same normalized syntax hash that staleness detection computes (XxHash3 of the tree-sitter AST walk, or raw XxHash3 for unsupported languages).

The original design (Decision 4 in prior versions) rejected lockfiles on the grounds that they introduce an escape hatch — `drift lock` could silence lint warnings without updating the doc. This concern doesn't apply to the current design because:

- The lockfile stores signatures, not "this is fine" flags. A stale signature still reports as stale. There is no way to mark an anchor as "accepted" — the only way to make it pass is to run `drift link`, which recomputes the signature from current code.
- The lockfile is deterministically generated by `drift link` — it's not a manual override mechanism.
- Content signatures make drift VCS-independent. `drift lint` with `sig:` never shells out to git for staleness — it reads the file, hashes it, compares. This makes shallow clones, fresh clones, and detached-HEAD states work without history.
- Content signatures make drift VCS-independent. `drift lint` with `sig` provenance never shells out to git for staleness — it reads the file, hashes it, compares. This makes shallow clones, fresh clones, and detached-HEAD states work without history.
- Detached HEAD, rebases, and force pushes don't invalidate provenance. A git SHA can become unreachable; a content fingerprint is always recomputable from the current file.
- The staleness check is a pure function of the file's content, not of VCS state. Behavior is deterministic.

Expand Down Expand Up @@ -76,29 +76,32 @@ Starting with 6 languages: TypeScript, Python, Rust, Go, Zig, Java. More can be

## 9. Content signatures as provenance format

Content signatures (`sig:<16-char-hex>`) are the provenance format for all anchors. See Decision 4 for why they live in the lockfile and how they make drift VCS-independent for staleness detection.
Content signatures (`sig = "<16-char-hex>"`) are the provenance format for all lockfile anchors. See Decision 4 for why they live in the lockfile and how they make drift VCS-independent for staleness detection.

## 10. Origin-qualified anchors

Anchors can carry an `origin:github:owner/repo` field in the lockfile. At lint time, drift resolves the current repo's identity from `git remote get-url origin`, normalizes it to `github:owner/repo`, and compares. If an anchor's origin doesn't match, it is skipped — it belongs to a different repository.
Anchors can carry an `origin = "github:owner/repo"` field in the lockfile. At lint time, drift resolves the current repo's identity from `git remote get-url origin`, normalizes it to `github:owner/repo`, and compares. If an anchor's origin doesn't match, it is skipped — it belongs to a different repository.

This solves the problem of docs traveling across repository boundaries. Shared skill files, vendored documentation, and monorepo imports all contain anchors that point at files in the source repo, not the consuming repo. Without origin qualification, `drift lint` would report these as STALE (file not found) every time, creating noise. With it, foreign anchors are silently skipped and only local anchors are checked.

Origin is opt-in. Anchors without `origin:` are always checked. The normalized format (`github:owner/repo`) is derived from the git remote URL, handling SSH, HTTPS, and SSH URL formats uniformly.
Origin is opt-in. Anchors without `origin` are always checked. The normalized format (`github:owner/repo`) is derived from the git remote URL, handling SSH, HTTPS, and SSH URL formats uniformly.

## 11. Flat line-oriented lockfile format
## 11. TOML array-of-tables lockfile format

`drift.lock` uses a flat, line-oriented format where each line is one binding: `<doc> -> <target> <key:value>...`. Alternatives considered:
`drift.lock` version 1 uses TOML `[[bindings]]` tables. The file starts with `version = 1`; each table contains `doc`, `target`, and metadata keys such as `sig` and `origin`:

- **YAML** — would require a YAML parser drift doesn't have. Adds a dependency for a config file format, not a data format.
- **JSON** — noisy diffs (trailing commas, bracket lines), painful merge conflicts. JSON merge conflicts require understanding structure to resolve.
- **INI-style sections** — grouping bindings under `[doc-path]` headers complicates conflict resolution because a conflict in the header affects all entries in the section.
```toml
version = 1

The flat format was chosen because:
- Every line is independent — `sort -u` resolves merge conflicts mechanically.
- No parser dependencies — `splitSequence(" -> ")` for the doc/rest boundary, `splitScalar(' ')` for target and key:value pairs.
- Both paths on every line — grep works in both directions for quick shell queries (`grep 'src/auth' drift.lock` finds all bindings involving auth code).
- Trailing key:value pairs extend naturally without format version bumps. New metadata fields (e.g. a future `hash-algo:`) are just another pair on the line.
[[bindings]]
doc = "docs/auth.md"
target = "src/auth/login.ts"
sig = "a1b2c3d4e5f6a7b8"
```

The TOML format replaced the original one-line `<doc> -> <target> <key:value>...` format after property tests showed it substantially reduces spurious merge conflicts on disjoint edits while remaining parseable by standard TOML tools. Bindings are serialized canonically with metadata fields sorted by key and blocks sorted by `(doc, target)` (with metadata as a deterministic tie-breaker for duplicate bindings).

Parser rules intentionally stay small and lockfile-specific: `version = 1` is mandatory for TOML lockfiles; blank lines and full-line `#` comments are ignored; block order is not significant on read; keys are bare `[A-Za-z0-9_-]+`; and values use single-line TOML basic strings with the common escapes (`\\b`, `\\t`, `\\n`, `\\f`, `\\r`, `\\\"`, `\\\\`). General TOML features such as inline comments, dotted keys, multiline strings, arbitrary top-level keys, and non-`bindings` tables are rejected. The reader can still import the legacy line format so existing lockfiles are upgraded to TOML on the next write.

## 12. Arena-oriented memory management

Expand Down Expand Up @@ -149,7 +152,7 @@ This matters because drift aims to support multiple VCS backends. Asking git for
Markdown docs are parsed with tree-sitter during lint using the two-parser `tree-sitter-markdown` grammar (block grammar for document structure, inline grammar for link extraction). This serves two purposes:

- **Broken link detection**: all `[text](relative/path.md)` links in drift-managed docs are extracted from `inline_link` nodes and checked for file existence. Missing targets are reported as `BROKEN` — no lockfile entry needed.
- **Doc-to-doc anchors**: a lockfile binding like `docs/overview.md -> docs/auth.md#Authentication sig:...` resolves the heading in the target doc via the block grammar's `section`/`atx_heading` nodes, then fingerprints the section content for staleness detection. Headings are structurally equivalent to code symbols.
- **Doc-to-doc anchors**: a lockfile binding whose `target` is `docs/auth.md#Authentication` resolves the heading in the target doc via the block grammar's `section`/`atx_heading` nodes, then fingerprints the section content for staleness detection. Headings are structurally equivalent to code symbols.

We use tree-sitter for link extraction rather than regex because:
- The inline grammar handles all link variants (inline links, reference links, autolinks) and naturally excludes links inside code blocks and code spans
Expand Down
Loading
Loading