Declare a dependency graph between arbitrary files and have CI fail when a
source changes but its dependents were not re-confirmed. Language- and
format-agnostic: code, docs, .docx, configs — anything with bytes.
Unlike build systems (Task, Bazel, Buck2) it runs no commands, and unlike file integrity monitors (AIDE) it models dependencies between files. It only answers one question: did a source change without its dependents being confirmed?
- A YAML manifest declares groups: each couples
sourceartifacts to thedependentsthat must stay in sync with them. outdatty updaterecords ablake3hash of every file into a committed lockfile (outdatty.lock) — this is the developer explicitly confirming the group is in sync.outdatty check(in CI) recomputes hashes. If a source changed since the last confirmation, the group is stale and the check fails with exit code 1. Editing only a dependent is allowed (setbidirectional: trueto flag that too).- The developer reviews, updates the dependents, and runs
outdatty updateto re-confirm.
cargo install outdattyRun without installing:
nix run github:mlavrinenko/outdatty -- checkOr add it to your flake inputs:
# flake.nix
outdatty.url = "github:mlavrinenko/outdatty";Download a pre-built binary from the latest release.
outdatty init # write a starter outdatty.yaml
outdatty update # confirm: record current hashes into outdatty.lock
outdatty check # CI gate: exit 1 if a source changed without re-confirmation
outdatty status # show every group without failing
outdatty update --group docs # confirm only one group
outdatty schema # print the manifest JSON schemaGlobal flags: --manifest <path>, --lock <path>, --format plain|json|quiet,
--color auto|always|never. Plain output is colorized on an interactive
terminal; auto (the default) disables color when piped or when NO_COLOR is
set.
Exit codes: 0 in sync, 1 drift (check only), 2 operational error
(unknown group, duplicate group name, unparseable manifest, incompatible
lockfile). A missing lockfile is not an operational error: check treats every
group as new and exits 1, so run outdatty update to create it.
# yaml-language-server: $schema=https://raw.githubusercontent.com/mlavrinenko/outdatty/main/schema/outdatty.schema.json
# gitignore: false # match every file; default skips .gitignored paths
groups:
- name: feature
source: [src/feature.rs] # literal paths or globs like src/**/*.rs
dependents: [docs/feature.mdx, tests/feature.rs]
# bidirectional: true # also flag the group when a dependent changesSee examples/outdatty.yaml. A path that disappears —
a deleted literal, or a glob that no longer matches a previously locked file —
counts as a change to confirm. Glob expansion skips files ignored by git — the
.gitignore files (root and nested), the global excludes, and
.git/info/exclude — so build output never enters a group; set
gitignore: false at the top of the manifest to match every file. The .git
directory is never traversed, and symlinks are not followed during glob
expansion.
Gate a repository by running check in CI; it exits non-zero on drift.
# .github/workflows/outdatty.yml
- run: outdatty check --format quietFor a local guard, add a pre-commit hook:
# .git/hooks/pre-commit
outdatty check --format quiet || {
echo "outdatty: a source changed without its dependents; run 'outdatty update'" >&2
exit 1
}Development setup and coding conventions live in CONTRIBUTING.md.
MIT