Metadata-Version: 2.5
Name: livedocs
Version: 0.2.2
Summary: Notes that know when the code moved on: hash-anchored live docs with a git commit gate, over Fiberplane drift.
Project-URL: Repository, https://github.com/GusEllerm/vault-drift
License-Expression: MIT
License-File: LICENSE
Keywords: agents,documentation,drift,git-hooks,obsidian
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Documentation
Classifier: Topic :: Software Development :: Version Control :: Git
Requires-Python: >=3.12
Requires-Dist: tree-sitter-typescript==0.23.2
Requires-Dist: tree-sitter>=0.23
Description-Content-Type: text/markdown

# livedocs

Notes that know when the code moved on.

`livedocs` binds Markdown notes (an Obsidian vault, or any folder of `.md` files inside a repo) to the
code they mention, and makes the git commit the place where notes and code are reconciled. It is a
thin layer over [Fiberplane drift](https://github.com/fiberplane/drift) (which fingerprints symbols)
that adds what an agent-written vault needs:

- **Anchors derived from the note's own backtick mentions** — nothing to declare by hand. Python,
  TypeScript/JavaScript (`.ts .tsx .js .jsx .mjs .cjs .mts .cts`), Astro components, and keys in JSON
  config files ([Languages](#languages)).
- **A note hash and member-level hashes in append-only stamps**, so "this note was verified against
  this code" is a recorded fact, and only changes to what the note actually mentions count.
- **A git pre-commit gate**: a commit that changes code a note mentions is blocked until the note is
  updated or acked with a reason. Provably benign changes (comment-only edits, pure moves, changes to
  members the note doesn't mention) are acked mechanically. `unknown` never blocks.
- **A read-time report** for agents: `CHANGED` with *was / now* pinned to the note lines that mention
  the symbol — a fact, not a verdict. The agent that made the change decides what the note should say.
- **Coverage**: which of a note's code claims are anchored, and which names it uses that no longer exist.

The guarantee is a mechanism of the development framework (git), not a capability of any agent:
it holds for Claude Code, Codex, a human at a terminal, or anything else that commits.

## New project? One command

```sh
livedocs new-vault docs/vault --scaffold-modules
```

creates an Obsidian vault (`Home.md`, `Modules/ Concepts/ Reference/ Sessions/ Templates/`, a minimal
`.obsidian/`), one empty note per source module, the livedocs config (sessions, reviews and templates
are snapshots), the git gate, the Claude Code adapter, and an `AGENTS.md` block — then commit. Open the
folder in Obsidian as a vault. From there: write notes, commit, reconcile when the gate says so.

## Install

```sh
brew install fiberplane/tap/drift          # or: curl -fsSL https://drift.fp.dev/install.sh | sh
```

Then `livedocs` itself, from PyPI (its only dependencies are the tree-sitter TypeScript parser wheels):

```sh
uv tool install livedocs                   # or: pipx install livedocs
```

To hack on it: `uv tool install --editable /path/to/vault-drift`. Every repository's gate then runs
whatever that checkout holds, half-finished branches included, so develop in a separate worktree or
keep the editable install off your everyday PATH.

Then, in the repository that holds the vault:

```sh
livedocs init --vault docs/vault           # git gate (.githooks + core.hooksPath), .livedocs/config.json,
                                           # Claude Code Stop heads-up (settings.local.json), AGENTS.md block
```

That is the whole setup. Notes are stamped by the commit that adds or edits them: write a note, commit
it, and the gate binds its code mentions and records the stamp in the same commit. For an existing vault,
`git add` the notes and commit once (or run `livedocs stamp <note>` per note to see what each binds).
Each clone activates the versioned hook with `git config core.hooksPath .githooks` (`init` does it).

**Committing from Obsidian or another GUI app.** Apps like the Obsidian Git plugin run hooks with a minimal
PATH (`/usr/bin:/bin`). `init` records where `livedocs` and `drift` live in the clone's own git config
(`livedocs.launcher`, `livedocs.drift`; never versioned), and the hook also looks in `~/.local/bin`,
`/opt/homebrew/bin` and `/usr/local/bin`. A commit that touches nothing a note is bound to (site content,
an asset, a new file no note mentions yet) passes without running anything. In a clone set up by an
older livedocs, run `livedocs init` once more and commit the updated `.githooks/pre-commit`.
`--repo` defaults to the current git repository and `--vault` to the configured one, so inside the repo
the commands need no arguments.

## The commit flow

```
edit code ──► git commit
              │  gate: notes that mention the changed code, not yet reconciled?
              ├─ none / benign ────────────────────────────────► commit passes (benign ones auto-acked;
              │                                                   new or edited notes stamped)
              └─ some ──► "CHANGED: <note> … was: … now: … note lines 13, 30"
                          edit the note and commit again          (the commit stamps it)
                          or, still correct:  livedocs stamp <note> --ack --reason "…"; git add -A; commit
```

`livedocs affected` lists the notes your uncommitted changes touch before you get there.

## Commands

| Command | Does |
|---|---|
| `livedocs new-vault <path> [--scaffold-modules]` | scaffold an Obsidian vault and wire everything in |
| `livedocs init --vault <path> [--read-gate] [--shared]` | install the gate, config and harness adapter for an existing vault |
| `livedocs stamp <note> [--ack --reason R]` | bind a note's mentions with drift and record a stamp |
| `livedocs check <note> [--json]` | one note's state: `fresh`, `changed`, `broken`, `unknown`, `snapshot` |
| `livedocs affected [--cached]` | notes bound to files changed in the working tree (or the index) |
| `livedocs verify` | CI: every stamped note against the checked-out tree; exit 1 on changed/broken |
| `livedocs coverage` | per-note anchored / dangling mentions, and mentions waiting on files not yet `git add`ed |
| `livedocs rebind` | after an upgrade: re-stamp every unedited note under the current binding rules |
| `livedocs signatures [--cached \| --rev C] [--json]` | exported API signatures changed, added or removed: staged against HEAD, or commit `C` against its first parent |
| `livedocs survey` | resolution statistics over a vault (diagnostics) |

## Languages

| Language | Files | A mention can name |
|---|---|---|
| Python | `.py` | functions, classes, methods, class attributes, module constants |
| TypeScript / JavaScript | `.ts .tsx .mts .cts .js .jsx .mjs .cjs` | functions and arrow-function consts, classes and their methods/fields, interfaces and their members, type aliases, enums, constants, keys of top-level object literals (`SET_TOKENS.kitchen.background`, also inside `defineConfig({…})` and `z.object({…})`), default exports |
| Astro | `.astro` | the component (by its file name: `ProjectCard`, `<ProjectCard />`) and its frontmatter's declarations (`Props`, `getStaticPaths`) |
| JSON config | `package.json`, `tsconfig.json`, `wrangler.jsonc`, `*.config.json`, any `.jsonc` | key paths: `wrangler.jsonc#assets.directory`, `compilerOptions.paths` |

How TypeScript mentions resolve: `<Header />` names a component; `@/components/Header` follows
tsconfig `paths`; a barrel's `export { default as Header } from './Header'` resolves to the definition,
never to the barrel; a default export is also found by its file's name. Every mentioned member carries a
hash that ignores comments, whitespace, quote style, optional semicolons, trailing commas and redundant
parentheses, so formatting and comment edits are acked mechanically at commit. drift fingerprints
top-level symbols in `.ts` and `.js`; everywhere else (JSX, `.mjs`, Astro, `default`, keys) the file is
bound and the member hashes decide.

**A path names a file, and a big file is not a claim.** A backtick path to a TypeScript/JavaScript/Astro
module binds the whole file when the module has at most 600 lines: any change to it, other than
comments and formatting, flags the note. A larger module binds only its default export (a component, a
`defineConfig({…})` object) by path, so name the functions and types a note describes. On one
TypeScript project, whole-file anchors on every named module cost 931 hand reconciliations in 338
commits, half of them from a single 1,600-line hub; at a 600-line limit the gate asked 362 times and
still caught 5 of the 6 sampled note-invalidating changes that a default-export-only rule missed. Set `"path_anchors"` in `.livedocs/config.json` to a different line
limit, to `"file"` (every named file binds whole) or to `"default-export"` (never the whole file). A
path to a Python file binds nothing by itself, as before; paths to config files, styles, scripts and
docs bind the file. A path never binds another note in the vault, an image or other media, a font, an
archive or a lockfile. To take a directory out of livedocs' scope entirely (site content an editor
commits from Obsidian, generated data), list it in `"ignore_paths": ["content/**"]`: nothing in it is
indexed or bound, and a commit that touches only those paths passes the gate untouched.

Notes that stay cheap to keep anchored:
- name symbols: `getProjects()`, `projectSchema.title`, `<ProjectCard />`. A path to a small module is
  fine (it means "anything in this file"), but every change to that file will then ask about the note;
- give exported symbols names that are unique across the repo: a bare `load` defined in every
  `src/modules/*/loader.ts` binds all of them (safe, but each change flags every note that says `load`);
- title a module note with its path (`# src/modules/projects/schema.ts`): ambiguous names then prefer
  that module;
- one default-exported component per file, named after the file.

In a repository with both Python and TypeScript, a note resolves in the language it is about (the module
it is named after, or most of what it names) before falling back to both.

**Upgrading from 0.1.x** in a TypeScript project: notes stamped by 0.1 hold whole-file anchors. Run
`livedocs rebind`, then commit `drift.lock` and the stamps file. Every unedited note is acked under the
new rules; edited notes are re-bound by the commit that contains them. In any project, 0.1 stamps keep
working, but their Python member hashes depend on the CPython minor version (a stamp made on 3.13 read as
changed in CI on 3.12); 0.2's are stable across versions, and `rebind` moves existing notes to them.

## API signature changes

`livedocs signatures --cached --json` lists the exported API a staged commit changes (`--rev <commit>`:
that commit against its first parent), for tools that gate API changes:

```json
[{"path": "src/api.ts", "qualname": "fileCard", "kind": "signature",
  "was": "function fileCard(id: string, opts = { draft: false }): Card",
  "now": "function fileCard(id: string, opts = { draft: true }): Card"}]
```

`kind` is `signature`, `added` or `removed`. Exported means `export`ed in TS/JS (declarations, `export
{ … }` clauses under their public names, the default export, non-private members of exported classes) and
listed in `__all__`, or public by name, in Python. A signature is the name, parameters with their
defaults, return and type parameters, async/generator and class heritage; field types; all of an
interface, type alias or enum; a constant's declared type. Bodies, comments, formatting, decorators and
field values never count. The exit status is 0 whatever is found, and nonzero only on an error.

## Snapshot notes

Dated records — reviews, session logs, reports — describe the code as it *was*. Mark them with
`livedocs: snapshot` in the frontmatter, or list globs in `.livedocs/config.json`:

```json
{ "vault": "docs/vault", "snapshot_globs": ["Reference/Review *", "Sessions/*"] }
```

They bind nothing, report `snapshot`, and never block.

## Secret scanners

`drift.lock` and `<vault>/.livedocs/stamps.jsonl` hold fingerprints — hashes of your own code's syntax
trees. Anyone who can read them can read the code they were computed from; they grant access to
nothing. Secret scanners can still misread a hash next to a name like `get_access_token` as a leaked
token. Since 0.1.3, stamps tag every fingerprint (`drift:…`, `ast:…`) and store members as pairs so they
don't have that shape, and `init` / `new-vault` add both files to `.gitguardian.yaml`'s
`secret.ignored_paths`, which ggshield (pre-commit, CI) reads. If you use the GitGuardian dashboard's
repository monitoring, add the same two paths as exclusions in its workspace settings, and mark any
earlier alert on these files as a false positive. `drift.lock` itself is drift's format and still holds
bare `sig = "<hex>"` values.

## CI

Pre-commit is skipped by `--no-verify`, merges and rebases. Run the same check in CI:

```yaml
# .github/workflows/livedocs.yml
name: livedocs
on: [push, pull_request]
jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: curl -fsSL https://drift.fp.dev/install.sh | sh && echo "$HOME/.local/bin" >> "$GITHUB_PATH"
      - uses: astral-sh/setup-uv@v5
      - run: uv tool install livedocs
      - run: livedocs verify --repo . --vault docs/vault
```

Or with the [pre-commit](https://pre-commit.com) framework, add to `.pre-commit-config.yaml`:

```yaml
- repo: https://github.com/GusEllerm/vault-drift
  rev: v0.2.1
  hooks: [{ id: livedocs-gate }]
```

## What it can and cannot guarantee

- It guarantees that a *committed* note has been vouched for against the code it mentions, at that
  commit. A read-time `changed` means the working tree has moved since.
- It sees code the note names in backticks. A prose claim with nothing to anchor ("tokens rotate every
  15 minutes") is reported `unknown`, never `fresh`. `livedocs coverage` shows how much of each note that is.
- It does not follow the call graph: a note's claim about what a function *does* can go stale when a
  callee changes. That is the residual miss class: 1 of the 23 note-invalidating changes an anchor could
  have caught on the Python reference history (hpc-bridge, run 1). On the TypeScript one (gravity-works)
  such changes were half the sampled misses until a path to a small module bound the whole file.
- Python, TypeScript/JavaScript, Astro and JSON config keys (see [Languages](#languages)). Module
  resolution reads tsconfig/jsconfig `paths`, not bundler aliases (Vite `resolve.alias`). An Astro
  template is compared as normalised text (comments and whitespace removed, `<script>` as TS tokens), not
  parsed. YAML and TOML files bind as whole files.
- Code is discovered wherever it lives — `src/`, `benchmark/src/`, a flat package — excluding tests,
  environments, build output and docs. To pin it, set `"code_roots": ["lib", "services/api"]` in
  `.livedocs/config.json`.

## Background

Design, decisions and the measurements behind every choice live in the vault at `live-docs-vault/`
(start at `Start Here.md`). The short version:

- **Python** (hpc-bridge, 141 commits, livedocs before its first release; `Results/1a Run 1.md` and
  `1a Run 2.md`): the hash flagged 22 of the 23 note-invalidating changes an anchor covered, but only
  2.7% of its flags (22 of 830) marked a change that made a note wrong in run 1, and 5.1% after run 2's
  fixes. That is why the signal is a fact delivered at commit time to the agent that has the context,
  never a verdict. Agents reconciling at the gate did it honestly and well (`Results/1b Run 1.md`).
- **TypeScript** (gravity-works, 338 commits, livedocs 0.2.0; `Results/TS Run 1.md`): the gate asks for
  an estimated 362 hand reconciliations where 0.1.4 asked for 931, with a graded sample behind the
  600-line path rule.
