Declare which files depend on which, and CI fails when a source changes but
nobody re-confirmed its dependents. Language and format don't matter. Code,
docs, .docx, configs, anything with bytes.
Unlike a build system (Task, Bazel, Buck2) it runs no commands, and unlike a file integrity monitor (AIDE) it models links between files. It answers one question. Did a source change without its dependents being confirmed?
- A YAML manifest declares groups. Each couples
sourcefiles to thedependentsthat must stay in sync with them. outdatty updaterecords ablake3hash of every file in a committed lockfile,outdatty.lock. That's you confirming the group is in sync.outdatty checkin CI recomputes the hashes. A source that changed since the last confirmation makes its group stale, and the check exits 1. Editing only a dependent is fine, unless the group setsbidirectional: true.- You review, fix the dependents and run
outdatty updateto re-confirm.
cargo install outdatty # from crates.io
nix run github:mlavrinenko/outdatty -- check # with Nix, nothing installedIn a flake, add outdatty.url = "github:mlavrinenko/outdatty"; to your inputs.
Pre-built binaries live on the
latest release.
outdatty init # write a starter outdatty.yaml
outdatty update # confirm, recording current hashes in outdatty.lock
outdatty check # CI gate, exits 1 if a source changed unconfirmed
outdatty status # show every group without failing
outdatty update --group docs # confirm only one group
outdatty update --group docs --dependent README.md # record one reviewed dependent
outdatty schema # print the manifest JSON schemaPlain check prints only what failed, then one summary line. A passing run
prints outdatty: 5 groups up to date.
[ stale ] cli-docs
source changed: src/report.rs
review dependent: README.md
confirm with: outdatty update --group cli-docs
outdatty: 1 of 5 groups out of date
--dependent (repeatable, needs --group) records only the named dependents'
hashes and leaves the sources and other dependents as locked, so a
dependent-only edit claims review of that file alone. Each path lands in every
named group that declares it, and a path none of them declares is an error.
The global flags are --manifest <path>, --lock <path>,
--format plain|json|quiet|paths|paths0 and --color auto|always|never. Plain
output is colored on an interactive terminal, and auto, the default, drops
color when piped or when NO_COLOR is set.
check exits 0 in sync and 1 on drift. Any command exits 2 on an
operational error, like an unknown group, a duplicate group name, an unparseable
manifest or an incompatible lockfile. A missing lockfile isn't one, since
check treats every group as new and exits 1. Run outdatty update to make it.
--format=paths and --format=paths0 (NUL-delimited) print just the sorted
changed-source and unrecorded-dependent paths. outdatty stores hashes, not
diffs, so pipe them into whatever diff or editor you like.
outdatty status --format=paths0 | xargs -0 -r git diff -- # robust, handles spaces
git diff -- $(outdatty status --format=paths) # simple# 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 deleted literal, or a
glob that no longer matches a locked file, counts as a change to confirm. A file
a dependents glob newly matches, with no hash in the lockfile, fails check as
unrecorded, and outdatty names the command that records it.
[ unrec ] cli-docs
unrecorded: www/content/cli/add.typ
record it: outdatty update --group cli-docs --dependent www/content/cli/add.typ
A group you split, rename or drop leaves its hashes in the lockfile. check
fails on each such orphan and names it, and a plain outdatty update prunes
them. A --group check skips orphans, as it skips coverage.
[orphaned] 1 lock group(s) the manifest no longer declares
orphaned group: old-docs
prune with: outdatty update
Globs skip whatever git ignores (root and nested .gitignore files, the global
excludes, .git/info/exclude), so build output never sneaks into a group. Set
gitignore: false at the top of the manifest to match every file. outdatty
never walks .git and never follows a symlink while expanding a glob.
Every path in the manifest, and every key in the lockfile, resolves from
root, which defaults to the manifest's own directory. Point it at the repo
root and the pair can live in .config/ with paths written as before.
# .config/outdatty.yaml
root: .. # paths resolve from the repo root
groups:
- name: feature
source: [src/feature.rs]
dependents: [docs/feature.mdx]Without --manifest, outdatty tries outdatty.yaml and outdatty.yml, then
the same two under .config/. The lockfile stays beside the manifest, here
.config/outdatty.lock, and its keys don't change. So moving an existing pair
takes that one root: .. line and no update.
A glob source flags a file that changed. Coverage flags a file no group
mentions at all, the brand-new one someone forgot to wire in.
require_tracked lists the files that must appear in some group's source or
dependents, and any that don't fail check as untracked.
# Default when omitted: ["**"] — every git-tracked file must belong to a group.
require_tracked:
- "**" # require everything, then carve out what has no coupling
- "!vendor/**" # last match wins; `!` excludes
- "!LICENSE"
# require_tracked: ["src/**"] # or require only a subtree (no negation needed)
# require_tracked: ["!**"] # or opt out entirelyThe last pattern to match a path decides, and a leading ! excludes it. The
universe is every file under root that git doesn't ignore (honoring
gitignore), and the manifest and lockfile are always exempt. A failing check
names each untracked file, so you can add it to a group or exclude it.
Gate a repo by running check in CI, which exits non-zero on drift. --format quiet prints nothing on a pass. On a failure it prints the failing groups to
stderr just as plain check would, so a red gate explains itself.
# .github/workflows/outdatty.yml
- run: outdatty check --format quietA local pre-commit hook runs the same line, exec outdatty check --format quiet
in .git/hooks/pre-commit.
Two branches that record groups can touch neighbouring lines of outdatty.lock,
which git's text merge calls a conflict. Register outdatty's merge driver once
per clone and the lockfile merges key by key.
git config merge.outdatty.driver 'outdatty merge-lock %O %A %B'
echo 'outdatty.lock merge=outdatty' >> .gitattributes # commit this oneEach hash takes the side that changed it, and a group one side dropped stays
gone. A hash both sides changed gets one no file has, so check fails its
group until you review it. The merge never stops on the lockfile.
Setup and conventions live in CONTRIBUTING.md.
MIT