extant 0.12.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- extant-0.12.2.dist-info/METADATA +769 -0
- extant-0.12.2.dist-info/RECORD +8 -0
- extant-0.12.2.dist-info/WHEEL +5 -0
- extant-0.12.2.dist-info/entry_points.txt +2 -0
- extant-0.12.2.dist-info/licenses/LICENSE +21 -0
- extant-0.12.2.dist-info/top_level.txt +2 -0
- extant_collect.py +3165 -0
- extant_config.py +475 -0
|
@@ -0,0 +1,769 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: extant
|
|
3
|
+
Version: 0.12.2
|
|
4
|
+
Summary: Check that the claims in your documentation are still true, against git and the filesystem
|
|
5
|
+
Author: scooter-sensei
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/scooter-sensei/extant
|
|
8
|
+
Project-URL: Changelog, https://github.com/scooter-sensei/extant/blob/main/CHANGELOG.md
|
|
9
|
+
Keywords: documentation,validation,git,pre-commit,ai-agents
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
19
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: tomli>=2; python_version < "3.11"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
<div align="center">
|
|
27
|
+
|
|
28
|
+
# extant
|
|
29
|
+
|
|
30
|
+
**Your documentation makes claims. This checks whether they are still true.**
|
|
31
|
+
|
|
32
|
+
[](https://github.com/scooter-sensei/extant/actions/workflows/tests.yml)
|
|
33
|
+
[](LICENSE)
|
|
34
|
+
[](https://www.python.org)
|
|
35
|
+
[](#requirements)
|
|
36
|
+
|
|
37
|
+
*extant (adj.) - still in existence; surviving.*
|
|
38
|
+
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## The problem
|
|
44
|
+
|
|
45
|
+
Your README says the project needs Node 18. Your `package.json` says 20. Your
|
|
46
|
+
CONTRIBUTING file links to a script deleted in March. An architecture note says
|
|
47
|
+
a rewrite "landed in `8f2a91c`", and that commit no longer exists.
|
|
48
|
+
|
|
49
|
+
Every one of those sentences was true the day it was written.
|
|
50
|
+
|
|
51
|
+
Nothing complains, because documentation is just writing. Tests check code. Type
|
|
52
|
+
checkers check types. Nothing checks whether your prose is still accurate, so it
|
|
53
|
+
drifts quietly until somebody follows it and loses an afternoon.
|
|
54
|
+
|
|
55
|
+
This matters more than it used to. AI assistants read these files and treat them
|
|
56
|
+
as fact. An assistant cannot tell that a line expired, so it plans around
|
|
57
|
+
something untrue and hands back confidently wrong work.
|
|
58
|
+
|
|
59
|
+
## What it looks like
|
|
60
|
+
|
|
61
|
+
An ordinary project. A README, a `package.json`, a CONTRIBUTING file. No status
|
|
62
|
+
document, no new habits.
|
|
63
|
+
|
|
64
|
+
```console
|
|
65
|
+
$ python tools/extant_collect.py --verify
|
|
66
|
+
|
|
67
|
+
line 5: [dead-sha] `deadbeef1234567` does not resolve in this repo
|
|
68
|
+
line 3: [dead-md-link] links to `docs/setup.md`, which does not exist
|
|
69
|
+
line 1: [inconsistent-artifact] `node_version` disagrees across files:
|
|
70
|
+
`18` in README.md; `20` in package.json
|
|
71
|
+
|
|
72
|
+
checked README.md: dead-sha 1, dead-md-link 1, inconsistent-artifact 2
|
|
73
|
+
CONTRIBUTING.md: line 3: [dead-md-link] links to `scripts/gone.sh`
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Four false statements in the docs you already have, with line numbers, in under
|
|
77
|
+
a second.
|
|
78
|
+
|
|
79
|
+
That `checked` line counts what it **looked at**, not what it found. It matters
|
|
80
|
+
as much as the findings, and [there is a section about why](#every-check-reports-its-denominator).
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## What it covers
|
|
85
|
+
|
|
86
|
+
Twelve rules. Every one answers a question git or the filesystem can settle.
|
|
87
|
+
|
|
88
|
+
| Rule | Catches |
|
|
89
|
+
|:---|:---|
|
|
90
|
+
| `dead-sha` | "released in commit `abc1234`" when that commit does not exist |
|
|
91
|
+
| `false-merge-claim` | "merged into `develop` at `abc1234`" when that commit is not on `develop` |
|
|
92
|
+
| `stale-live-claim` | "not merged yet" about something merged last week |
|
|
93
|
+
| `unknown-branch` | "work is on branch X" when git has never seen that name |
|
|
94
|
+
| `dead-release-tag` | "released in v2.1" when no such tag exists, or it never shipped |
|
|
95
|
+
| `dead-path-pointer` | "see the file at this path" when the file moved |
|
|
96
|
+
| `dead-md-link` | `[a link](to/a/file.md)` whose target is gone |
|
|
97
|
+
| `dead-md-anchor` | a `#jump-to-section` link with no such heading |
|
|
98
|
+
| `inconsistent-artifact` | two files in your project stating different values for the same thing |
|
|
99
|
+
| `dead-pinned-ref` | an install snippet pinning a version of your project that does not exist |
|
|
100
|
+
| `raw-lfs-blob` | an asset your `.gitattributes` says is in Git LFS, committed into git as a real binary instead |
|
|
101
|
+
| `possible-secret` | a password or key pasted in by accident |
|
|
102
|
+
|
|
103
|
+
Five details that are easy to miss:
|
|
104
|
+
|
|
105
|
+
**More than one integration branch works.** A merge claim names the branch it
|
|
106
|
+
means, so "merged to `develop` at `abc1234`" is checked against `develop` and
|
|
107
|
+
"merged to `main` at ..." against `main`. Nothing to configure. Gitflow teams
|
|
108
|
+
write both kinds, and against a single configured trunk half of those claims
|
|
109
|
+
were never examined at all - not judged and found true, simply not read.
|
|
110
|
+
|
|
111
|
+
**Case matters, and only on some machines.** Windows and macOS open
|
|
112
|
+
`docs/PLAN.md` happily when the file is `docs/plan.md`. Linux does not. Without
|
|
113
|
+
the check a document passes on your laptop and fails on the server, or worse,
|
|
114
|
+
passes everywhere while misleading every Linux reader. Wrong-case paths are
|
|
115
|
+
reported on every platform.
|
|
116
|
+
|
|
117
|
+
**Examples are left alone.** Claims inside fenced code blocks are not read as
|
|
118
|
+
promises, so a README showing what a claim looks like is not accused of making
|
|
119
|
+
one. A password inside a fence is still reported, because that one is about what
|
|
120
|
+
the file contains rather than what it promises.
|
|
121
|
+
|
|
122
|
+
**Install snippets are the exception.** `dead-pinned-ref` is the one rule that
|
|
123
|
+
reads *inside* code blocks, because an install snippet is the opposite of an
|
|
124
|
+
example: it is the block a reader copies verbatim, and a version that does not
|
|
125
|
+
exist fails for them on first use. It only checks pins whose `repo:` names your
|
|
126
|
+
repository, so documenting somebody else's hook is never flagged.
|
|
127
|
+
|
|
128
|
+
**Renames are followed.** When a file has moved, you get told where:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
line 5: [dead-md-link] links to `docs/old-name.md`, which does not exist;
|
|
132
|
+
git shows it renamed to `docs/new-name.md`
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### What it deliberately ignores
|
|
136
|
+
|
|
137
|
+
It never judges whether a number or a date is *right*. "We had 2238 tests in
|
|
138
|
+
March" was true in March. It is not wrong now, only old, and there is nothing to
|
|
139
|
+
check it against.
|
|
140
|
+
|
|
141
|
+
That restraint is the whole design. A tool that cries wolf gets ignored, and an
|
|
142
|
+
ignored tool is worse than none. This one reports only what it can prove.
|
|
143
|
+
|
|
144
|
+
The single exception proves the rule: it will compare **two files against each
|
|
145
|
+
other**, because that has a definite answer. See
|
|
146
|
+
[files that contradict each other](#files-that-contradict-each-other).
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Is this for you?
|
|
151
|
+
|
|
152
|
+
| | |
|
|
153
|
+
|:---|:---|
|
|
154
|
+
| **Probably yes** | Your project uses git and has documentation: a README, a CONTRIBUTING file, docs, architecture notes. That is enough. It matters more if an AI assistant reads those files, because it cannot tell an expired line from a current one. |
|
|
155
|
+
| **Probably not** | Your project does not use git, or your documentation is one paragraph that never mentions a file, a commit, or a version. |
|
|
156
|
+
|
|
157
|
+
Note what is **not** required: a status file, a changelog, a particular
|
|
158
|
+
workflow, or any change to how you work.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Requirements
|
|
163
|
+
|
|
164
|
+
Git, and Python 3.9 or newer. Check both:
|
|
165
|
+
|
|
166
|
+
```console
|
|
167
|
+
$ git --version
|
|
168
|
+
$ python --version
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
| If missing | Get it |
|
|
172
|
+
|:---|:---|
|
|
173
|
+
| Git | https://git-scm.com |
|
|
174
|
+
| Python 3.9+ | https://www.python.org |
|
|
175
|
+
|
|
176
|
+
On macOS and Linux you may need `python3` rather than `python`.
|
|
177
|
+
|
|
178
|
+
**The tool itself has no third-party dependencies.** Standard library and git,
|
|
179
|
+
nothing else.
|
|
180
|
+
|
|
181
|
+
3.9 and 3.10 are supported for the enterprise distributions that ship them:
|
|
182
|
+
RHEL 9 and Debian 11 are on 3.9, Ubuntu 22.04 LTS on 3.10. The one thing
|
|
183
|
+
newer than 3.9 anywhere in the tool is `tomllib`, which joined the standard
|
|
184
|
+
library in 3.11. Below that the tool runs on its defaults with no parser at
|
|
185
|
+
all, and reading a `.extant.toml` wants `pip install tomli` - the same
|
|
186
|
+
parser under its original name, so a config file is read identically on
|
|
187
|
+
every version.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Install
|
|
192
|
+
|
|
193
|
+
Four ways in. Pick one.
|
|
194
|
+
|
|
195
|
+
### Option A: pre-commit
|
|
196
|
+
|
|
197
|
+
If you already use [pre-commit](https://pre-commit.com), this is the whole
|
|
198
|
+
setup. Add to `.pre-commit-config.yaml`:
|
|
199
|
+
|
|
200
|
+
```yaml
|
|
201
|
+
repos:
|
|
202
|
+
- repo: https://github.com/scooter-sensei/extant
|
|
203
|
+
rev: v0.12.2
|
|
204
|
+
hooks:
|
|
205
|
+
- id: extant
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Then `pre-commit install`, and create a `.extant.toml` naming the document to
|
|
209
|
+
check:
|
|
210
|
+
|
|
211
|
+
```toml
|
|
212
|
+
primary_doc = "README.md"
|
|
213
|
+
extra_docs = ["CONTRIBUTING.md"]
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
It runs on every commit whether or not you touched documentation. That is
|
|
217
|
+
deliberate: merging a branch can make a sentence false without editing a line of
|
|
218
|
+
prose, and that is the case this exists for.
|
|
219
|
+
|
|
220
|
+
A second hook id, `extant-annotate`, emits GitHub Actions annotations instead of
|
|
221
|
+
plain text.
|
|
222
|
+
|
|
223
|
+
### Option B: pip, pipx or uv
|
|
224
|
+
|
|
225
|
+
Installs the validator as a command. Nothing is written into your project, so
|
|
226
|
+
this is the quickest way to see what the tool says before deciding anything:
|
|
227
|
+
|
|
228
|
+
```console
|
|
229
|
+
$ uvx extant --repo . --verify
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
`uvx` downloads and runs it without installing. To keep it around:
|
|
233
|
+
|
|
234
|
+
```console
|
|
235
|
+
$ pipx install extant
|
|
236
|
+
$ extant --repo . --verify
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`pip install extant` works too, though a tool you run against many projects is
|
|
240
|
+
usually happier in its own environment.
|
|
241
|
+
|
|
242
|
+
This is the VALIDATOR only. The git hooks and the `/extant` command have to
|
|
243
|
+
live inside the repository they check, so use Option C or D to install those.
|
|
244
|
+
Point it at a document with `--validate`, or create a `.extant.toml` naming
|
|
245
|
+
one, exactly as in Option A.
|
|
246
|
+
|
|
247
|
+
### Option C: Claude Code
|
|
248
|
+
|
|
249
|
+
Two lines:
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
/plugin marketplace add scooter-sensei/extant
|
|
253
|
+
/plugin install extant@extant
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Then open the project you want to protect and ask:
|
|
257
|
+
|
|
258
|
+
> Set up extant in this project.
|
|
259
|
+
|
|
260
|
+
It inspects your repository, works out the settings, and reports what it found
|
|
261
|
+
and how confident it is about each value.
|
|
262
|
+
|
|
263
|
+
### Option D: by hand
|
|
264
|
+
|
|
265
|
+
Works with or without Claude Code.
|
|
266
|
+
|
|
267
|
+
**1. Get the files.** Download the ZIP from
|
|
268
|
+
[the repository](https://github.com/scooter-sensei/extant) (green **Code**
|
|
269
|
+
button, then **Download ZIP**), or clone it:
|
|
270
|
+
|
|
271
|
+
```console
|
|
272
|
+
$ git clone https://github.com/scooter-sensei/extant
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**2. Preview.** This changes nothing:
|
|
276
|
+
|
|
277
|
+
```console
|
|
278
|
+
$ python extant/plugin/skills/extant/install.py --repo /path/to/your/project --dry-run
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Read what it prints. If it looks wrong, stop, and nothing has happened.
|
|
282
|
+
|
|
283
|
+
**3. Do it for real.** The same command without `--dry-run`, plus a preset:
|
|
284
|
+
|
|
285
|
+
```console
|
|
286
|
+
$ python extant/plugin/skills/extant/install.py --repo /path/to/your/project --preset readme
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**4. Turn on the automatic checks.**
|
|
290
|
+
|
|
291
|
+
```console
|
|
292
|
+
$ cd /path/to/your/project
|
|
293
|
+
$ sh tools/hooks/install
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
They run after a commit is already recorded, print what they found, and never
|
|
297
|
+
stop you doing anything.
|
|
298
|
+
|
|
299
|
+
### Presets
|
|
300
|
+
|
|
301
|
+
A preset picks the documents and the shape, so you are not deriving
|
|
302
|
+
configuration before you have seen the tool work once.
|
|
303
|
+
|
|
304
|
+
| Preset | For |
|
|
305
|
+
|:---|:---|
|
|
306
|
+
| `readme` | any project. Your README and CONTRIBUTING. Nothing else needed. |
|
|
307
|
+
| `node` | the same, plus `package.json` and `CHANGELOG.md` version agreement |
|
|
308
|
+
| `python` | the same, with `pyproject.toml` |
|
|
309
|
+
| `rust` | the same, with `Cargo.toml` |
|
|
310
|
+
| `enterprise` | long-lived projects. Also `SECURITY.md`, `SUPPORT.md`, `UPGRADING.md`, `MIGRATION.md` |
|
|
311
|
+
| `ml` | data and model projects. Also `MODEL_CARD.md` and `DATA_CARD.md`, and that `pyproject.toml` and `environment.yml` pin the same Python |
|
|
312
|
+
| `legacy-web` | older web apps. Also `INSTALL.md`, `DEPLOY.md`, `UPGRADING.md`, and that `.nvmrc` and `package.json` agree on Node |
|
|
313
|
+
| `go` | a Go module. Also `SECURITY.md`, and that `go.mod` and your `Dockerfile` build with the same Go |
|
|
314
|
+
| `jvm` | Gradle or Maven. Also `UPGRADING.md`, `MIGRATION.md`, and `gradle.properties` against the changelog |
|
|
315
|
+
| `k8s` | Helm charts. Also `RUNBOOK.md`, and `Chart.yaml`'s chart version against the changelog |
|
|
316
|
+
| `terraform` | Terraform modules. Also `UPGRADING.md` and `MIGRATION.md`, beside the terraform-docs README |
|
|
317
|
+
| `docker` | images and compose. Also `DEPLOY.md`, `RUNBOOK.md`, `OPERATIONS.md` |
|
|
318
|
+
| `monorepo` | a workspace root. Also `ARCHITECTURE.md`, `docs/README.md`, and the root version |
|
|
319
|
+
| `mobile` | iOS and Android. Also `RELEASE_NOTES.md`, `PRIVACY.md`, and one marketing version across both stores |
|
|
320
|
+
| `unity` | a Unity project. Checks the editor-version badge against `ProjectSettings/ProjectVersion.txt` |
|
|
321
|
+
| `godot` | a Godot project. Checks the version in `doc/setup_instructions.md` against `project.godot` |
|
|
322
|
+
| `agent` | files AI agents read as fact: `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `copilot-instructions.md` |
|
|
323
|
+
| `status` | a running status file with dated entries |
|
|
324
|
+
|
|
325
|
+
**`agent` is worth a note.** [AGENTS.md](https://agents.md) is the cross-tool
|
|
326
|
+
standard for instructing AI coding agents, and `CLAUDE.md` and `GEMINI.md` are
|
|
327
|
+
the vendor-native equivalents. Those files are mostly paths and commands, and an
|
|
328
|
+
agent cannot tell an expired line from a current one. A dead path in a README
|
|
329
|
+
wastes somebody's afternoon; a dead path in an `AGENTS.md` becomes confidently
|
|
330
|
+
wrong work.
|
|
331
|
+
|
|
332
|
+
**Where two files look like they should agree and do not, no check is written.**
|
|
333
|
+
Helm documents `appVersion` as unrelated to the chart `version`. Terraform's
|
|
334
|
+
`required_version` is a constraint (`">= 1.5.0"`) while `.terraform-version` is
|
|
335
|
+
an exact pin. Go's `go` directive is a minimum and `toolchain` is exact. A mobile
|
|
336
|
+
`versionCode` is a build counter, not the version anyone sees. Every one of those
|
|
337
|
+
pairs would report a correct repository, so `docker` and `terraform` carry no
|
|
338
|
+
cross-check at all, and the rest compare only what genuinely must match.
|
|
339
|
+
|
|
340
|
+
The last three exist because long-lived documentation rots in a particular
|
|
341
|
+
place. An enterprise project rarely has a stale README. It has a `MIGRATION.md`
|
|
342
|
+
last edited in 2021 pointing at three files that have since moved.
|
|
343
|
+
|
|
344
|
+
Two rules govern every preset:
|
|
345
|
+
|
|
346
|
+
**A preset never overrides something measured.** Detection reads your repository
|
|
347
|
+
for the trunk name, branch naming and commit conventions. A template would be
|
|
348
|
+
guessing at those, and a preset that quietly replaced your real branch name
|
|
349
|
+
would be the copied-configuration problem this tool was built around. Choosing
|
|
350
|
+
the *document* is your call, which is why passing a preset settles that one.
|
|
351
|
+
|
|
352
|
+
**Checks whose files are absent are skipped and reported.** A preset never opens
|
|
353
|
+
by complaining about a file you do not have. A tool whose first act is a false
|
|
354
|
+
positive has taught a lesson that is very hard to unteach.
|
|
355
|
+
|
|
356
|
+
### Works with any coding agent
|
|
357
|
+
|
|
358
|
+
The checker is Python, git hooks and a pre-commit entry, so it never depended on
|
|
359
|
+
a particular assistant. What did was one line deciding where the agent-facing
|
|
360
|
+
instructions went.
|
|
361
|
+
|
|
362
|
+
Setup now writes them to `.agents/skills/extant/SKILL.md`, the location the
|
|
363
|
+
[Agent Skills](https://agentskills.io) open standard defines. One file, read by
|
|
364
|
+
**OpenAI Codex, Gemini CLI, GitHub Copilot, Cursor, Kimi Code, Claude Code** and
|
|
365
|
+
the twenty-odd other tools that adopted it.
|
|
366
|
+
|
|
367
|
+
It is rendered for your repository rather than copied, so it names your document
|
|
368
|
+
and your paths, and it carries the discipline that matters: read the
|
|
369
|
+
denominator, never make a document pass by deleting the claim.
|
|
370
|
+
|
|
371
|
+
Claude Code additionally gets `/extant`, a slash command for the end-to-end
|
|
372
|
+
workflow. Both are rendered from the same observations, so they cannot end up
|
|
373
|
+
describing different documents.
|
|
374
|
+
|
|
375
|
+
### What lands in your project
|
|
376
|
+
|
|
377
|
+
| | |
|
|
378
|
+
|:---|:---|
|
|
379
|
+
| `tools/` | the checker |
|
|
380
|
+
| `.extant.toml` | settings, written by reading **your** project |
|
|
381
|
+
| git hooks | the automatic checks. They report, they never block |
|
|
382
|
+
| `.agents/skills/extant/SKILL.md` | instructions any agent reads, rendered for your project |
|
|
383
|
+
| `/extant` command | Claude Code only, for the full workflow |
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## Features
|
|
388
|
+
|
|
389
|
+
### Every check reports its denominator
|
|
390
|
+
|
|
391
|
+
The summary line counts what was **examined**, not what was found:
|
|
392
|
+
|
|
393
|
+
```console
|
|
394
|
+
checked STATUS.md: dead-sha 36, stale-live-claim 1, false-merge-claim 2,
|
|
395
|
+
dead-path-pointer 5 (907 lines scanned for secrets)
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
If one of those reads `0`, that check found nothing to look at, which usually
|
|
399
|
+
means a setting is wrong rather than that your file is spotless. Any rule that
|
|
400
|
+
examined nothing is named on a `NOTE:` line.
|
|
401
|
+
|
|
402
|
+
This is the single most important line in the output. "Found no problems" and
|
|
403
|
+
"did not look" print identically otherwise, and only one of them is good news.
|
|
404
|
+
|
|
405
|
+
### It can prove its own checks fire
|
|
406
|
+
|
|
407
|
+
The worry above deserves more than a warning, so there is a command:
|
|
408
|
+
|
|
409
|
+
```console
|
|
410
|
+
$ python tools/extant_collect.py --selftest
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
It takes a real claim from your document, breaks it on purpose, confirms the
|
|
414
|
+
matching rule notices, and puts everything back. Nothing is written.
|
|
415
|
+
|
|
416
|
+
```console
|
|
417
|
+
dead-sha FIRED
|
|
418
|
+
stale-live-claim FIRED
|
|
419
|
+
dead-path-pointer FIRED
|
|
420
|
+
dead-release-tag NO PROBE nothing to corrupt
|
|
421
|
+
possible-secret FIRED
|
|
422
|
+
|
|
423
|
+
4 fired, 1 had nothing to corrupt, 0 stayed silent
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
**A check that stays silent after you break something it should catch is
|
|
427
|
+
broken.** This runs in CI here on every change, so the tool is not merely
|
|
428
|
+
tested, it is watched failing.
|
|
429
|
+
|
|
430
|
+
### Findings inside pull requests
|
|
431
|
+
|
|
432
|
+
```console
|
|
433
|
+
$ python tools/extant_collect.py --verify --format=github
|
|
434
|
+
$ python tools/extant_collect.py --verify --format=sarif
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
**`github`** emits GitHub Actions annotations, so each problem is highlighted on
|
|
438
|
+
its own line in the pull request diff rather than buried in a log nobody opens.
|
|
439
|
+
Add the flag to your existing step. Nothing else is needed, and it requires no
|
|
440
|
+
extra permissions.
|
|
441
|
+
|
|
442
|
+
**`sarif`** emits the standard format code-scanning tools exchange, as pure JSON
|
|
443
|
+
on stdout, so it pipes straight to a file. To get results into GitHub's Security
|
|
444
|
+
tab:
|
|
445
|
+
|
|
446
|
+
```yaml
|
|
447
|
+
- name: Check the documentation
|
|
448
|
+
run: python tools/extant_collect.py --verify --format=sarif > extant.sarif
|
|
449
|
+
continue-on-error: true
|
|
450
|
+
|
|
451
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
452
|
+
with:
|
|
453
|
+
sarif_file: extant.sarif
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
That upload needs `permissions: security-events: write`. This repository uses
|
|
457
|
+
the annotation route instead, so treat the SARIF upload as a starting point
|
|
458
|
+
rather than something proven here.
|
|
459
|
+
|
|
460
|
+
### Files that contradict each other
|
|
461
|
+
|
|
462
|
+
The tool never asks whether a number is *correct*. It will ask whether two files
|
|
463
|
+
disagree, which has a definite answer needing only the filesystem.
|
|
464
|
+
|
|
465
|
+
This project shipped the bug that motivated it: three manifests said the version
|
|
466
|
+
was 0.1.0 while the changelog said 0.3.0, and everyone installing was told they
|
|
467
|
+
were getting the first release.
|
|
468
|
+
|
|
469
|
+
```toml
|
|
470
|
+
[extant.consistency.version]
|
|
471
|
+
"package.json" = '"version":\s*"([^"]+)"'
|
|
472
|
+
"CHANGELOG.md" = '^## (\d+\.\d+\.\d+)'
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
Each line names a file and how to find the value inside it. Disagree, and you
|
|
476
|
+
are told which file says what.
|
|
477
|
+
|
|
478
|
+
Four shapes are refused when the settings load, because each would produce a
|
|
479
|
+
check that can never fail: a check listing one file, a pattern with no capture
|
|
480
|
+
group, a pattern with more than one, and the same file named twice under
|
|
481
|
+
different spellings. A pattern that matches nothing is reported rather than
|
|
482
|
+
passing quietly.
|
|
483
|
+
|
|
484
|
+
### Corrections as a patch, never an edit
|
|
485
|
+
|
|
486
|
+
When a file has been renamed, the tool already tells you where it went. It can
|
|
487
|
+
also write the correction:
|
|
488
|
+
|
|
489
|
+
```console
|
|
490
|
+
$ python tools/extant_collect.py --verify --suggest-fixes
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
This prints a **patch** and changes nothing. Read it, and apply it if you agree:
|
|
494
|
+
|
|
495
|
+
```console
|
|
496
|
+
$ python tools/extant_collect.py --verify --suggest-fixes | git apply -
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
It only offers corrections for files git actually recorded as renamed. If a file
|
|
500
|
+
is simply gone it says nothing, because guessing where it went means writing
|
|
501
|
+
something that might not be true, and that is the one thing this refuses to do.
|
|
502
|
+
Its authority rests on checking claims and never authoring them.
|
|
503
|
+
|
|
504
|
+
### Search across the archive
|
|
505
|
+
|
|
506
|
+
If you keep a status document, old entries get archived so the live file stays
|
|
507
|
+
short. That helps until you need to remember why a decision was made.
|
|
508
|
+
|
|
509
|
+
```console
|
|
510
|
+
$ python tools/extant_collect.py --search "checkout"
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
It searches the live document and the archive together and returns whole
|
|
514
|
+
**entries** rather than matching lines. That is the only reason it beats grep: a
|
|
515
|
+
decision lives in a dated entry with its reasoning, and a line from the middle
|
|
516
|
+
tells you a phrase exists rather than what was decided.
|
|
517
|
+
|
|
518
|
+
### Every other document you keep
|
|
519
|
+
|
|
520
|
+
Your primary document is not the only one that rots. A `CLAUDE.md`, an
|
|
521
|
+
`AGENTS.md`, a `CONTRIBUTING.md` all make the same kinds of claim:
|
|
522
|
+
|
|
523
|
+
```toml
|
|
524
|
+
extra_docs = ["CLAUDE.md", "AGENTS.md", "CONTRIBUTING.md"]
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
They get every rule that does not depend on dated entries, which is most of
|
|
528
|
+
them.
|
|
529
|
+
|
|
530
|
+
### Adopting on a project that already has years of prose
|
|
531
|
+
|
|
532
|
+
Point this at a ten-year-old repository and the first run reports everything at
|
|
533
|
+
once. That is accurate and useless: CI goes red, nobody has a week for
|
|
534
|
+
decade-old documentation, and the tool comes back out.
|
|
535
|
+
|
|
536
|
+
Record what is already there, once:
|
|
537
|
+
|
|
538
|
+
```console
|
|
539
|
+
$ python tools/extant_collect.py --verify --write-baseline
|
|
540
|
+
recorded 47 finding(s) in .extant-baseline.json
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
Then every run checks **new** claims and ignores the recorded ones:
|
|
544
|
+
|
|
545
|
+
```console
|
|
546
|
+
$ python tools/extant_collect.py --verify --baseline
|
|
547
|
+
1 new finding(s), 47 suppressed by .extant-baseline.json
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
New documentation is held to the standard from day one; the backlog waits.
|
|
551
|
+
|
|
552
|
+
**It always says how much it is hiding.** "No findings" and "no new findings, 47
|
|
553
|
+
suppressed" are different facts, and a baseline that concealed its own size
|
|
554
|
+
would be exactly the failure this tool exists to surface, reintroduced by one of
|
|
555
|
+
its own features.
|
|
556
|
+
|
|
557
|
+
**Nothing is ever recorded implicitly.** `--write-baseline` is a separate,
|
|
558
|
+
deliberate command. A baseline that rewrote itself on every run would forgive
|
|
559
|
+
whatever it had just found, and the check would decay to nothing while still
|
|
560
|
+
reporting success.
|
|
561
|
+
|
|
562
|
+
**An amnesty must not outlive its finding:**
|
|
563
|
+
|
|
564
|
+
```console
|
|
565
|
+
$ python tools/extant_collect.py --verify --baseline-check
|
|
566
|
+
baseline: 47 entr(y/ies), 45 still occur, 2 do not
|
|
567
|
+
STALE docs/setup.md: [dead-md-link] links to `scripts/old.sh`, ...
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
Those two were fixed. Their entries now forgive something that is not there, so
|
|
571
|
+
they are reported and should be deleted. A baseline nobody prunes becomes a
|
|
572
|
+
permanent exemption, and it is itself a stale claim.
|
|
573
|
+
|
|
574
|
+
The file is JSON, and deliberately readable: each entry carries the path, the
|
|
575
|
+
rule and the message alongside its fingerprint. It is a list of things your
|
|
576
|
+
project has agreed to leave broken for now, so it belongs in review like any
|
|
577
|
+
other change.
|
|
578
|
+
|
|
579
|
+
### The optional wrong-branch guard
|
|
580
|
+
|
|
581
|
+
One check is **off unless you ask for it**, because unlike everything else here
|
|
582
|
+
it can refuse to save your work:
|
|
583
|
+
|
|
584
|
+
```console
|
|
585
|
+
$ sh tools/hooks/install --with-trunk-guard
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
**What it solves.** Git lets you keep several versions of a project going at
|
|
589
|
+
once. If you lose track of which one you are on, you can commit to the wrong
|
|
590
|
+
branch. The work is not lost, but it is filed in the wrong place, and finding
|
|
591
|
+
out later is unpleasant.
|
|
592
|
+
|
|
593
|
+
**Why it is off by default.** It has nothing to do with your documentation. You
|
|
594
|
+
came for a tool that checks whether your writing is still true, and a tool that
|
|
595
|
+
suddenly refuses to save your work for an unrelated reason is a tool people
|
|
596
|
+
uninstall.
|
|
597
|
+
|
|
598
|
+
**Should you?** If you work on one branch, no. If you juggle several, or have
|
|
599
|
+
ever pushed work and found it on the wrong branch, yes. It is also worth it if
|
|
600
|
+
AI assistants commit in your project, because they are particularly good at
|
|
601
|
+
losing track of which branch they are on.
|
|
602
|
+
|
|
603
|
+
Remove it by deleting `pre-commit` from your project's `.git/hooks`, or bypass
|
|
604
|
+
it once with `git commit --no-verify`.
|
|
605
|
+
|
|
606
|
+
### Any language
|
|
607
|
+
|
|
608
|
+
The checking does not care what your project is written in. Only the optional
|
|
609
|
+
"run the tests" step does, and you name the command:
|
|
610
|
+
|
|
611
|
+
```toml
|
|
612
|
+
suite_command = ["npm", "test"]
|
|
613
|
+
suite_command = ["cargo", "test"]
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
### Fast enough to leave on
|
|
617
|
+
|
|
618
|
+
A 16,000-line document validates in under a second. A 100,000-line document in
|
|
619
|
+
about four. The rules that query git batch their questions, so a document
|
|
620
|
+
naming two thousand distinct commits asks git once per commit rather than twice
|
|
621
|
+
per claim.
|
|
622
|
+
|
|
623
|
+
---
|
|
624
|
+
|
|
625
|
+
## Read the settings it writes
|
|
626
|
+
|
|
627
|
+
Setup works out your project's habits by reading it, and prints each value with
|
|
628
|
+
how confident it is:
|
|
629
|
+
|
|
630
|
+
```console
|
|
631
|
+
trunk [derived ] origin/HEAD -> main
|
|
632
|
+
branch_token [derived ] 128 branches sampled
|
|
633
|
+
entry_prefix [guessed ] highest-scoring header '## Release'
|
|
634
|
+
merge_claim [unknown ] no matching phrasing found
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
Anything it could not work out is left **switched off** rather than guessed.
|
|
638
|
+
|
|
639
|
+
> **This is the part that matters.** If a setting is wrong, that rule quietly
|
|
640
|
+
> does nothing and you get a tool reporting "all clear" forever without looking
|
|
641
|
+
> at anything. It is the one way this fails badly. Read what setup prints, and
|
|
642
|
+
> see [porting.md](plugin/skills/extant/references/porting.md) to fill gaps.
|
|
643
|
+
|
|
644
|
+
---
|
|
645
|
+
|
|
646
|
+
## What it cannot do
|
|
647
|
+
|
|
648
|
+
- Only catches sentences that can be **proven** wrong. "Nearly finished" is
|
|
649
|
+
beyond it, on purpose.
|
|
650
|
+
- Cannot judge whether a summary is a *good* summary.
|
|
651
|
+
- Expects consistent headings for dated entries. A heading that does not match
|
|
652
|
+
is skipped.
|
|
653
|
+
- Assumes one main branch. Gitflow-style release branches are not modelled.
|
|
654
|
+
- **Checks links to your own files, not to the web.** Nothing here touches the
|
|
655
|
+
network. Checking external links would make a passing run depend on someone
|
|
656
|
+
else's uptime and rate limits, turning a definite answer into a coin flip.
|
|
657
|
+
Issue and pull request links go unchecked for the same reason.
|
|
658
|
+
- **Does not complain about a branch merged and then deleted.** That is normal
|
|
659
|
+
tidying, and the branch is still named in the merge commit. Only a name git
|
|
660
|
+
has never seen is reported.
|
|
661
|
+
- **Settings load next to the tool, not next to the folder you point at.**
|
|
662
|
+
Correct once installed in your project, wrong if you run it from elsewhere. It
|
|
663
|
+
says so on stderr rather than quietly using the wrong ones.
|
|
664
|
+
- **It cannot tell a corrected claim from a deleted one.** Removing the sentence
|
|
665
|
+
it complained about makes a document pass. The `/extant` workflow works around
|
|
666
|
+
this by reporting first-run findings even after fixing them, so a deletion is
|
|
667
|
+
visible in both the report and the diff. The checker alone cannot know.
|
|
668
|
+
- **A pattern you write yourself can hang it.** Settings take regular
|
|
669
|
+
expressions, and a badly shaped one can take effectively forever on some text.
|
|
670
|
+
The fix is to simplify the pattern, but the failure looks like a freeze rather
|
|
671
|
+
than a complaint.
|
|
672
|
+
|
|
673
|
+
---
|
|
674
|
+
|
|
675
|
+
<details>
|
|
676
|
+
<summary><b>If something goes wrong</b></summary>
|
|
677
|
+
|
|
678
|
+
<br>
|
|
679
|
+
|
|
680
|
+
**"python is not recognised"** on Windows, or **"command not found"** on macOS
|
|
681
|
+
and Linux: try `python3`, or reinstall Python with the "add to PATH" box ticked.
|
|
682
|
+
|
|
683
|
+
**The checks never run.** Confirm you did step 4, then run `--verify` by hand.
|
|
684
|
+
|
|
685
|
+
**It flags something you think is fine.** Do not silence it by deleting the
|
|
686
|
+
sentence. Either that sentence really is out of date, or a setting needs
|
|
687
|
+
adjusting. Both are worth knowing.
|
|
688
|
+
|
|
689
|
+
**Everything reports 0.** A setting is almost certainly wrong. See
|
|
690
|
+
[Read the settings it writes](#read-the-settings-it-writes).
|
|
691
|
+
|
|
692
|
+
</details>
|
|
693
|
+
|
|
694
|
+
<details>
|
|
695
|
+
<summary><b>Deeper documentation</b></summary>
|
|
696
|
+
|
|
697
|
+
<br>
|
|
698
|
+
|
|
699
|
+
All under `plugin/skills/extant/`:
|
|
700
|
+
|
|
701
|
+
| File | What is in it |
|
|
702
|
+
|:---|:---|
|
|
703
|
+
| `references/porting.md` | Getting the settings right for your project. Read first. |
|
|
704
|
+
| `references/config.md` | Every setting explained. |
|
|
705
|
+
| `references/design.md` | Why each rule works as it does, and the real mistake behind each decision. |
|
|
706
|
+
| `SKILL.md` | What Claude reads. |
|
|
707
|
+
|
|
708
|
+
</details>
|
|
709
|
+
|
|
710
|
+
<details>
|
|
711
|
+
<summary><b>What is in this repository</b></summary>
|
|
712
|
+
|
|
713
|
+
<br>
|
|
714
|
+
|
|
715
|
+
```
|
|
716
|
+
.claude-plugin/marketplace.json lets Claude Code install this
|
|
717
|
+
.pre-commit-hooks.yaml lets pre-commit install this
|
|
718
|
+
plugin/
|
|
719
|
+
.claude-plugin/plugin.json plugin details
|
|
720
|
+
skills/extant/
|
|
721
|
+
SKILL.md what Claude reads
|
|
722
|
+
install.py, detect.py the setup program
|
|
723
|
+
payload/ what gets copied into your project
|
|
724
|
+
references/ the deeper documentation
|
|
725
|
+
tests/ 219 tests
|
|
726
|
+
tests/harnesses/ five slow audits, run by hand
|
|
727
|
+
NEXT_SESSION.md this project's own status document
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
That last one is not decoration. This project runs its own tool on its own
|
|
731
|
+
document, on every change, in CI. If it stopped working, this repository would
|
|
732
|
+
be the first to find out.
|
|
733
|
+
|
|
734
|
+
`tests/harnesses/` holds the audits pytest cannot perform: one that breaks the
|
|
735
|
+
code on purpose to see whether any test notices (57 mutations, all caught), one
|
|
736
|
+
that installs into twenty unlike projects (20 scenarios, 87 assertions), one
|
|
737
|
+
that tries to abuse the tool (18 adversarial probes), and two that measure speed
|
|
738
|
+
and load. Between them they found every defect fixed in 0.3.0, and the stale
|
|
739
|
+
assertion caught in the audit before 0.6.0. The unit suite found none of them,
|
|
740
|
+
because the unit suite was the thing being audited.
|
|
741
|
+
|
|
742
|
+
The bug fixed in 0.6.1 was found by none of them. It took installing the
|
|
743
|
+
published release and using it as a stranger would, which is its own lesson.
|
|
744
|
+
|
|
745
|
+
</details>
|
|
746
|
+
|
|
747
|
+
<details>
|
|
748
|
+
<summary><b>The one idea worth taking elsewhere</b></summary>
|
|
749
|
+
|
|
750
|
+
<br>
|
|
751
|
+
|
|
752
|
+
When you write a rule that checks something, **look at your real data first and
|
|
753
|
+
shape the rule to fit it**, rather than writing what the rule ought to look like
|
|
754
|
+
and hoping. Done the second way, the file-path rule produced 23 false alarms on
|
|
755
|
+
the first project it ran against. Done the first way it produced none, and still
|
|
756
|
+
caught the real problem.
|
|
757
|
+
|
|
758
|
+
And always report how many things you looked at. "Found no problems" and "did
|
|
759
|
+
not look" print exactly the same otherwise.
|
|
760
|
+
|
|
761
|
+
</details>
|
|
762
|
+
|
|
763
|
+
---
|
|
764
|
+
|
|
765
|
+
<div align="center">
|
|
766
|
+
|
|
767
|
+
**MIT licensed.** Use it freely, including commercially. See [LICENSE](LICENSE).
|
|
768
|
+
|
|
769
|
+
</div>
|