Every document in the tree audited against the source, the tracker and git
history, each finding then given to a second reader tasked with refuting it.
74 raised, 12 refuted, 57 applied. No code changed.
THE README DESCRIBED A SKELETON
Its Status table — the one place a claim about what is built is allowed to live
— still read "there is no usable app yet", with Not built against Room, the four
core screens, fertility and notifications, and No round run against QA. Five
batches had shipped and three QA rounds had run.
TWO DOCUMENTS WERE SILENTLY NEVER FIRING
architecture/README.md and design/README.md wrote Governs: as prose ("the Gradle
module graph", "the design tokens in core/designsystem"). Neither contains a
path token, so doc-triggers.py reduced them to globs matching nothing, and one
real glob apiece made them look path-governing rather than subject-governing —
the state the script's own header calls invisible. Editing a Room entity never
fired the document owning the migration table. Both now fire, proved by running
the script.
SECURITY.md CLAIMED FOUR UNBUILT PROTECTIONS
App lock listed among what works offline; biometric/PIN gating described as
protecting app launch; the incognito launcher as existing; Play Billing in the
third parties table without the "not yet integrated" marker its neighbours
carry. All are Batch 06/07 work.
The advertising boundary was overstated in SECURITY.md and the README alike:
both said the ads module declares no dependency and a guard proves it. There is
no ads module. The pre-declared ":core:ads" to emptySet() rule is stricter than
the sentence it replaced and matches nothing until Batch 07, which is why the
guard is proved by injection rather than trusted.
SMALLER, EACH A REAL TRAP
WORK_CYCLE.md pointed at docs/architecture/scripts/forgejo-issue.py, a template
path absent here — missed by doc-claims.sh, which reads backticked prose and not
fenced blocks. ClaudeReport.md's Round notes said "No rounds yet" after three
rounds because ClaudeQAPlan.md's after-a-round list never named that section;
the playbook is fixed first. The instrumented-test count was eight in three
places and is four. HISTORY.md said the repository had no code and that nothing
had been tried and dropped, when three approaches had.
DELIBERATELY UNCHANGED
ClaudeReport.md's last verified build SHA stays at
|
||
|---|---|---|
| .. | ||
| README.md | ||
README.md
Git hooks
Status: Current
Owner: _null
Last reviewed: 2026-08-18
Governs: .githooks/ — what runs before and after a commit
Review trigger: A new guard the repository wants run before a commit; any change
to what a commit message must contain; any change to which
Gradle tasks pre-commit runs
Three hooks, and the reason they live in the repository rather than in
.git/hooks: that directory is not versioned, so a hook living there protects
exactly one clone on exactly one machine.
The hooks are in .githooks/, and only there
The template this repository adopted keeps a master copy under
docs/architecture/githooks/ and installs copies into .githooks/. This
repository does not, deliberately: pre-commit here is adapted for Gradle rather than
npm, so a second copy would be a second version of a file somebody edits once
and forgets — the exact failure ../../DOC_TRUST_MAP.md exists to prevent.
So .githooks/ holds the hooks and this document
describes them. One copy of the code, one copy of the explanation.
Install
git config core.hooksPath .githooks
That is the whole setup, and it is per clone — every checkout runs it once, including a fresh clone on the same machine. An uninstalled hook fails silently, which is the same class of problem the hooks exist to prevent.
What each one does
| Hook | Guard |
|---|---|
pre-commit |
the staged-diff secret scan; then :domain:cycle:test and :domain:prediction:test when .kt/.kts or a build file is staged; then scripts/schema-guard.sh when a Room entity or the schema export is staged |
commit-msg |
refuses a message with no conventional type — the closed vocabulary is in the hook's own header |
post-commit |
pushes to origin, so a guarded commit does not sit unpushed |
Two things worth knowing before you rely on them
pre-commit does not compile the Android modules. It runs the two pure-JVM
suites, which need no SDK and take about a second. Compiling :app needs the
Android SDK and half a minute, and a hook people reach for --no-verify to
avoid is worse than one that checks less. ./gradlew assembleRelease belongs to
../../security/SECURITY_CHECKLIST.md,
which is where it is.
The suite it does run is not an arbitrary subset: it is the prediction
acceptance cases from
../../planning/PRODUCT_PLAN.md §51, which
guard the one claim this product is built on.
The schema guard is here rather than in the suite for a reason worth knowing.
Room rewrites the schema export during compilation, so by the time any test
runs, both sides of any in-process comparison describe the changed entity and
agree. Only git can see that an already-committed schema file changed, and only
a hook can ask git before the commit exists. scripts/schema-guard.sh carries
the proof in its header — the check it replaced was watched staying green over
exactly the failure it claimed to catch.
It exits 2 for "nothing was checked", and the hook treats 2 as a refusal.
A missing schema directory is not a clean schema.
The secret scan's 2 is handled differently, and deliberately. A commit that
only deletes files gives it nothing to scan, which is the right answer rather
than a fault — so the hook accepts a 2 when the staged diff adds no lines, and
refuses it whenever the diff adds any. Before that distinction existed, git rm
was impossible and the refusal claimed a credential had been found.
post-commit pushes. That is the intent — the commit that first added a
pre-commit hook to the project this came from sat unpushed for a day, guarded
and invisible — but it is a surprise if you were not expecting it. It never
forces, stays out of the way mid-rebase, and SKIP_PUSH=1 opts out loudly.
It has a second consequence worth knowing about: the push is what the Command
Center reads, so whatever documentation was not in that commit is now behind
the code by one push. That is the mechanical reason
../../WORK_CYCLE.md asks for doc edits in the same
commit as the change rather than in a tidy-up afterwards — with this hook
installed, "I will document it next commit" means the site has already published
the version without it.
Escape hatches, and why they are loud
SKIP_GUARDS=1 git commit ... # skips the scan, the tests and the message check, and says so
SKIP_PUSH=1 git commit ... # commits without publishing, and says so
git commit --no-verify ... # skips the hooks entirely, silently
Prefer the first two. They leave a line in the terminal saying the guard did not run, which is the difference between a deliberate exception and a habit.
Why a hook and not CI
Both, eventually. These are the guards that must run before the artifact exists: a check that fires after a push, or after a Play upload, catches the problem once it is already somewhere it cannot be taken back from. CI is the second opinion; this is the one that runs first.