2026-08-18 15:00:17 -05:00
|
|
|
# Tools — where the scripts are, and which ones can stop you
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
Status: Current
|
|
|
|
|
Owner: _null
|
|
|
|
|
Last reviewed: 2026-08-18
|
|
|
|
|
Governs: scripts/**, .githooks/**
|
|
|
|
|
Review trigger: Any script added to, removed from or repurposed in scripts/; any
|
|
|
|
|
change to which of them gates a commit or a release
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
> A signpost, deliberately. Every project that adopts this template has a
|
|
|
|
|
> `docs/TOOLS.md`, so "read `docs/TOOLS.md` first" is an instruction that works
|
|
|
|
|
> without knowing anything about the project — which is the whole reason this
|
|
|
|
|
> file exists at a fixed path.
|
|
|
|
|
|
|
|
|
|
## The list is not here
|
|
|
|
|
|
|
|
|
|
**[`architecture/README.md`](architecture/README.md)** holds the table of what
|
|
|
|
|
this project has and what each script is. That is the one copy.
|
|
|
|
|
|
|
|
|
|
A second table here would be two records of one fact, and the other one would
|
|
|
|
|
never hear that a script was renamed. So this file answers the questions that
|
|
|
|
|
table does not, and points at it for everything else.
|
|
|
|
|
|
docs: correct 57 claims the code and tracker disagreed with
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 0451fbe — no round has run
since, and moving it would claim a verification nobody performed. Every
DEVELOPMENT_LOG entry stays as written.
Guards: ./gradlew check, schema-guard.sh, doc-claims.sh (235 claimed paths, all
present), doc-triggers.py, and a link sweep over 21 markdown files.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:33:14 -05:00
|
|
|
## This project has seven of them, and that is deliberate
|
2026-08-18 15:00:17 -05:00
|
|
|
|
docs: correct 57 claims the code and tracker disagreed with
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 0451fbe — no round has run
since, and moving it would claim a verification nobody performed. Every
DEVELOPMENT_LOG entry stays as written.
Guards: ./gradlew check, schema-guard.sh, doc-claims.sh (235 claimed paths, all
present), doc-triggers.py, and a link sweep over 21 markdown files.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:33:14 -05:00
|
|
|
The template this repository adopted ships around twenty scripts. Privacy:
|
|
|
|
|
Period Tracker took six of them and wrote one of its own. `scaffold.sh` copies
|
|
|
|
|
none of them on purpose — *"an unconfigured `release.sh` landing in every new
|
|
|
|
|
repository is a loaded gun, not a head start"* — so each one is taken having been
|
|
|
|
|
read and configured.
|
2026-08-18 15:00:17 -05:00
|
|
|
|
|
|
|
|
**The ones not taken were not forgotten.** `release.sh`, `deploy.py`,
|
|
|
|
|
`backup.sh`, `restore-check.sh`, `preflight.sh`, `healthcheck.sh`, `migrate.sh`,
|
docs: correct 57 claims the code and tracker disagreed with
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 0451fbe — no round has run
since, and moving it would claim a verification nobody performed. Every
DEVELOPMENT_LOG entry stays as written.
Guards: ./gradlew check, schema-guard.sh, doc-claims.sh (235 claimed paths, all
present), doc-triggers.py, and a link sweep over 21 markdown files.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:33:14 -05:00
|
|
|
`status.sh`, `dev.sh`, `audit-gate.mjs` and `release-notes.mjs` all assume a
|
|
|
|
|
deployed Node or Postgres service with an npm dependency tree and a URL.
|
|
|
|
|
Privacy: Period Tracker is an Android app that ships through Google Play and has
|
|
|
|
|
no server at all. A release here is an AAB and a Play Console submission, so
|
|
|
|
|
[`security/SECURITY_CHECKLIST.md`](security/SECURITY_CHECKLIST.md) carries what
|
|
|
|
|
`release.sh` would have gated.
|
|
|
|
|
|
|
|
|
|
`duplication.py` and `dead-code.py` need no server, but both read a JavaScript or
|
|
|
|
|
TypeScript source tree: `dead-code.py` walks an export graph and excludes
|
|
|
|
|
Next.js entry points, and `duplication.py` does not scan `.kt` at all. Either
|
|
|
|
|
would have to be adapted before it said anything true about Kotlin, so neither is
|
|
|
|
|
taken yet.
|
|
|
|
|
|
|
|
|
|
`controls.sh` reports which operational controls a project has — backups, a
|
|
|
|
|
proven restore, somewhere errors go, an environment that is not production — and
|
|
|
|
|
exits `2` until some are declared expected. An app with nothing running has none
|
|
|
|
|
to declare, so taking it would install a check that could only report that it did
|
|
|
|
|
not run.
|
2026-08-18 15:00:17 -05:00
|
|
|
|
|
|
|
|
Two were deferred rather than declined:
|
|
|
|
|
|
|
|
|
|
- **`verify.sh`** — worth having once there is a real check suite to aggregate.
|
|
|
|
|
Today `./gradlew check` is the whole answer and wrapping it would add a layer
|
|
|
|
|
that could only be wrong.
|
|
|
|
|
- **`check-env.sh`** — its SPEC ships empty and exits `2` until it has entries,
|
docs: correct 57 claims the code and tracker disagreed with
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 0451fbe — no round has run
since, and moving it would claim a verification nobody performed. Every
DEVELOPMENT_LOG entry stays as written.
Guards: ./gradlew check, schema-guard.sh, doc-claims.sh (235 claimed paths, all
present), doc-triggers.py, and a link sweep over 21 markdown files.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:33:14 -05:00
|
|
|
and this project has no environment variables yet. It gets taken with the first
|
2026-08-18 15:00:17 -05:00
|
|
|
signing or Play credential, which is exactly the moment it becomes worth
|
|
|
|
|
running.
|
|
|
|
|
|
|
|
|
|
**The seventh is `schema-guard.sh`, and it is this project's own.** It clears
|
|
|
|
|
the bar in *Adding one* below the hard way: it exists because the check it
|
|
|
|
|
replaces was proved green over exactly the failure it claimed to catch. Its
|
|
|
|
|
header carries that proof.
|
|
|
|
|
|
|
|
|
|
If this project later grows a script the template already has, take that one
|
|
|
|
|
rather than writing a new one — the arguments in its header are the part that
|
|
|
|
|
took the longest.
|
|
|
|
|
|
|
|
|
|
## Which ones can stop you
|
|
|
|
|
|
|
|
|
|
Not in a table, because the honest answer lives in each script's own header and
|
|
|
|
|
would go stale here. The rule that matters:
|
|
|
|
|
|
|
|
|
|
**Exit code `2` is never a pass.** These scripts distinguish "the check ran and
|
|
|
|
|
found nothing" from "the check did not run", because those look identical from
|
|
|
|
|
the outside and only one of them is evidence. A hook or a CI step that treats a
|
|
|
|
|
`2` as success has quietly turned the check off. Each script states its codes at
|
|
|
|
|
the top; read them there.
|
|
|
|
|
|
2026-08-18 15:00:49 -05:00
|
|
|
**And a `2` is not automatically a failure either — it is a question.** The
|
|
|
|
|
pre-commit hook learned this the hard way: `secrets.sh` exits `2` on a commit
|
|
|
|
|
that only *deletes* files, because a deletion has no added lines to scan, and
|
|
|
|
|
that is the correct answer. The hook treated every non-zero as a refusal and
|
|
|
|
|
printed *"possible credential in the staged changes"* over a plain `git rm`, so
|
|
|
|
|
deletion-only commits were impossible and the reason given was a security
|
|
|
|
|
problem that did not exist.
|
|
|
|
|
|
|
|
|
|
The rule that came out of it: **on a `2`, ask whether the check could have had
|
|
|
|
|
anything to look at.** The hook now refuses a `2` whenever the staged diff adds
|
|
|
|
|
lines — because then the scanner did have input and checking nothing is exactly
|
|
|
|
|
the failure `2` reports — and accepts it, saying so, when the diff only removes
|
|
|
|
|
content. Both directions are proved with `prove-guard.sh`-style injections
|
|
|
|
|
before either is believed.
|
|
|
|
|
|
2026-08-18 15:00:17 -05:00
|
|
|
The hooks are the other place work gets stopped:
|
|
|
|
|
[`architecture/githooks/README.md`](architecture/githooks/README.md) has the one
|
|
|
|
|
install command and the table of what each hook runs. Note that **`post-commit`
|
|
|
|
|
pushes**, and that `pre-commit` here runs Gradle rather than the template's
|
|
|
|
|
TypeScript typecheck.
|
|
|
|
|
|
|
|
|
|
## Where to start in a fresh clone
|
|
|
|
|
|
|
|
|
|
1. `git config core.hooksPath .githooks` — per clone, every time, and an
|
|
|
|
|
uninstalled hook fails silently.
|
|
|
|
|
2. `bash scripts/secrets.sh --tracked` — the one-time audit of what is already
|
|
|
|
|
committed. The staged-diff mode is for the hook; this mode is for the day you
|
|
|
|
|
clone.
|
|
|
|
|
3. `./gradlew test` — the prediction acceptance tests run on the JVM, so this
|
|
|
|
|
needs no emulator and should be fast.
|
|
|
|
|
4. [`architecture/GUARDS.md`](architecture/GUARDS.md) — how to write a check that
|
|
|
|
|
can actually fail, before you write one. `scripts/prove-guard.sh` performs its
|
|
|
|
|
first rule.
|
|
|
|
|
|
|
|
|
|
## Adding one
|
|
|
|
|
|
|
|
|
|
Put it in `scripts/`, give it a header saying what it does and **which incident
|
|
|
|
|
motivated it**, state its exit codes, and add a row to
|
|
|
|
|
[`architecture/README.md`](architecture/README.md)'s table — this file's
|
|
|
|
|
`Review trigger` fires on exactly that.
|
|
|
|
|
|
|
|
|
|
The bar, from the scripts that are already here: **done by hand three times, or
|
|
|
|
|
once with a consequence.** A script written before either of those has no
|
|
|
|
|
failure to describe in its header, which is the part that stops the next person
|
|
|
|
|
deleting it.
|