Skip to content

TwoWells/Lattice

Repository files navigation

Lattice

crates.io docs.rs CI license

A markdown predicate linter and backlink reconciler, shipped as an LSP server.

Lattice encodes link predicates in CommonMark title text and maintains backlinks in YAML frontmatter. The graph lives in the files themselves — no database, everything in git.

Installation

cargo install lattice

Or download a prebuilt binary (Linux, macOS, Windows) from the latest release. Building from source needs Rust 1.95 or newer.

Usage

# Lint all markdown files in the current project (exit 1 on errors)
lattice lint

# Run as an LSP server (used by editors)
lattice serve

# Print the full configuration reference (every option, with defaults)
lattice config

# Print the version (with git commit and dirty state)
lattice --version

Neovim

Lattice is not shipped by nvim-lspconfig, so configure it directly. On Neovim 0.11+:

vim.lsp.config.lattice = {
  cmd = { "lattice", "serve" },
  filetypes = { "markdown" },
  root_markers = { ".lattice.toml", ".git" },
}
vim.lsp.enable("lattice")

Diagnostics appear inline on open, change, and save. Lattice is diagnostic-first, but the server is a full markdown LSP: completion (paths, headings, predicates, reference labels, footnotes), hover, document and workspace symbols, references, rename, folding, document links, formatting, and go-to-definition/declaration/type/implementation plus call and type hierarchy over the predicate graph.

On Neovim older than 0.11, start it per buffer instead:

vim.api.nvim_create_autocmd("FileType", {
  pattern = "markdown",
  callback = function(args)
    vim.lsp.start({
      name = "lattice",
      cmd = { "lattice", "serve" },
      root_dir = vim.fs.root(args.buf, { ".lattice.toml", ".git" }),
    })
  end,
})

Git hook

Add to .githooks/pre-commit or .git/hooks/pre-commit:

#!/bin/sh
lattice lint

Commits with broken links, unknown predicates, or missing backlinks will be rejected.

Configuration

An optional .lattice.toml at the project root overrides defaults:

[predicates]
supersedes = "superseded_by"
implements = "implemented_by"
depends_on = "dependency_of"
amends = "amended_by"
blocks = "blocked_by"
references = "referenced_by"

[policy]
predicates = "optional"    # or "required"
backlinks = true
bare_paths = "warn"        # or "deny", "disabled"
stale_references = "warn"  # or "hint", "deny", "disabled" — dangling `.md` references
# fragments = "github"    # or "gitlab", "vscode"; omit to try all
# connectivity = "off"    # or "no-orphans", "no-islands", "reachable"
# roots = ["README.md"]   # entry points for "reachable"; default = root README

# Opt-in convention checks (off by default — they flag valid CommonMark, not defects):
# code_block_language = "disabled"  # or "hint", "warn", "deny" — flag untagged code fences
# multiple_h1 = false               # flag more than one H1 heading
# skipped_heading_level = false     # flag a skipped heading level (e.g. H1 -> H3)
# image_empty_alt = false           # flag images with empty alt text

# Cross-repo references: a `{Name}/path` citation (backtick/quoted/bare, not a
# link) is checked existence-only against an aliased directory — never read,
# indexed, or treated as a graph edge. An undefined alias, or one whose
# directory is absent, is exempt; a present directory with a missing file is a
# stale reference. Relative (sibling-checkout) values are preferred.
# [external]
# Catenary = "../Catenary"

Per-reference exceptions

A path-shaped string that is deliberately not a live reference (a worked example, a counterexample, a knowingly-dead link) can be excepted from the stale_references / bare_paths lints in the document's own frontmatter, keyed by the literal reference with a required reason as the value:

---
exceptions:
  stale_references:
    "tickets/acquire/DESIGN.md": "hypothetical path in the worked example"
    "{Catenary}/old/layout.md": "pre-refactor path, kept for the changelog note"
  bare_paths:
    "README.md": "naming the file, deliberately not a link"
---

Exceptions are reconciled like backlinks, not silenced: an entry that matches no live diagnostic — its reference gone, or now resolving — is itself flagged as an unused exception whose message echoes the stored reason, and an entry with an empty reason is flagged too. Lattice flags, never auto-removes — the reason is the surviving record of a vanished reference's intent. A {Name}/… key flows through identically. An exception is never a graph edge and imposes no backlink obligation.

Agent instructions

Add this to your project's AGENTS or CLAUDE file:

Markdown links follow Lattice conventions: predicates are encoded in title text, e.g. [Doc](doc.md "supersedes"). The predicate vocabulary is: supersedes, implements, depends_on, amends, blocks, references. Backlinks are maintained in YAML frontmatter.

License

AGPL-3.0-or-later. Commercial license available — contact Two Wells.

About

No description, website, or topics provided.

Resources

License

AGPL-3.0, Unknown licenses found

Licenses found

AGPL-3.0
LICENSE
Unknown
LICENSE-COMMERCIAL

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages