Skip to content

Commit dcfebe2

Browse files
Add scripts/ + docs/REFACTOR_PLAYBOOK.md — long-term refactor cost reduction
Goal: same operation never costs more than the first time. scripts/: - rewrite-import.sh OLD NEW — bulk import path rewrite, refuses dirty working tree. - lean-grep.sh — grep -r restricted to .lean (excludes .lake/, .git/). - README.md — convention: lift into scripts/ after 3+ repetitions. docs/REFACTOR_PLAYBOOK.md: - Decision tree mapping refactor type → tool (LSP rename / scripts / Lake script / manual). - Pre-flight checklist + atomic-commit discipline. - Lake script vs bash matrix (Lake for AST-aware ops). - Pitfall log from this lib's history: sed-corrupts-docstrings, open-scoped-namespace-missing, notation-parser-conflicts, eta in notation RHS, typeclass inference stuck, force-pushed trailers persisting in GitHub cache. - Self-extending: 3+-repeat operations promote to playbook entries.
1 parent 6f59873 commit dcfebe2

4 files changed

Lines changed: 303 additions & 0 deletions

File tree

docs/REFACTOR_PLAYBOOK.md

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# Refactor Playbook
2+
3+
Source of truth for OpenGALib refactor workflows. Sister to `scripts/`
4+
(reusable codemods) and the project's CLAUDE.md (architectural stance).
5+
6+
The goal: **same operation never costs more than the first time**.
7+
8+
---
9+
10+
## Decision tree
11+
12+
```
13+
What's the refactor about?
14+
├─ Rename a single identifier (def, theorem, structure field)?
15+
│ └─ VSCode F2 (Rename Symbol).
16+
│ Lean LSP scans the import graph semantically.
17+
│ Doesn't touch docstrings/comments. Safest possible.
18+
19+
├─ Rewrite an import path prefix across many files?
20+
│ └─ scripts/rewrite-import.sh OLD_PREFIX NEW_PREFIX
21+
│ Idempotent, refuses dirty working tree, prefix-match.
22+
23+
├─ Move file or directory?
24+
│ └─ git mv first, then scripts/rewrite-import.sh.
25+
│ git mv preserves rename detection in history.
26+
27+
├─ Introduce or migrate notation?
28+
│ └─ Hand-write a 5-line example file FIRST. Verify parsing,
29+
│ typeclass inference, simp interaction — before any sweep.
30+
│ THEN bulk-migrate via perl/sed AND audit docstrings
31+
│ (text replace catches them too — see Pitfalls #2).
32+
33+
├─ Change a typeclass cascade or generic signature?
34+
│ └─ Manual. No bulk tool helps. Build incrementally.
35+
│ Use git checkpoints between each step.
36+
37+
├─ Need an AST-aware operation (rename only in code, not in
38+
│ docstrings; find all theorems referencing X; etc.)?
39+
│ └─ Lake script. See "Lake script vs bash" below.
40+
41+
└─ Bulk delete dead content (e.g., a sub-package being removed)?
42+
└─ git rm -r + scripts/lean-grep.sh to find dangling refs +
43+
manual cleanup of those refs.
44+
```
45+
46+
---
47+
48+
## Pre-flight checklist (always)
49+
50+
1. **`git status` clean.** No uncommitted changes. The bulk operation
51+
should be a single revertible step.
52+
2. **Snapshot commit** of the current good state if there's any pending
53+
work: `git commit -am "snapshot before X refactor"`.
54+
3. **One refactor concern per commit.** Don't bundle "rename + reorganize
55+
+ add deprecation alias" into one diff. Three separate commits.
56+
4. **`lake build` after each commit.** Catches silent breakage early
57+
when revert is still cheap.
58+
59+
If anything fails: `git reset --hard HEAD~1` and retry. The atomic-commit
60+
discipline makes rollback one command.
61+
62+
---
63+
64+
## Lake script vs bash — when to use which
65+
66+
| Need | Tool | Reason |
67+
|------|------|--------|
68+
| File text replacement | bash + sed/perl | Milliseconds, well-understood |
69+
| Import path rewrite | `scripts/rewrite-import.sh` | Already written |
70+
| `grep` on Lean source only | `scripts/lean-grep.sh` | Excludes `.lake/`, `.git/` |
71+
| Rename identifier in code only (skip docstrings) | Lake script | Needs Lean syntax tree |
72+
| Find all theorems whose statement uses X | Lake script | Needs `Lean.Environment` API |
73+
| Audit `@[simp]` lemma RHS shapes | Lake script | Needs elaborator |
74+
| Generate a typeclass dependency graph | Lake script | Needs full elab info |
75+
| One-off file munging | Inline shell command | Don't bother formalizing |
76+
77+
**Rule of thumb:** if the codemod would need to *understand* Lean
78+
syntax or semantics, write it in Lean (Lake script). Otherwise bash is
79+
faster to write and run.
80+
81+
### Lake script template
82+
83+
In `lakefile.lean`:
84+
85+
```lean
86+
script myCodemod (args : List String) do
87+
match args with
88+
| [arg1, arg2] =>
89+
-- ... do work using IO + Lean APIs ...
90+
return 0
91+
| _ =>
92+
IO.eprintln "Usage: lake script run myCodemod ARG1 ARG2"
93+
return 1
94+
```
95+
96+
Invoke: `lake script run myCodemod foo bar`.
97+
98+
For AST-level work, `import Lean` and use `Lean.Environment`,
99+
`Lean.Syntax`, `Lean.Elab.*`. Mathlib's `scripts/` directory has good
100+
examples.
101+
102+
---
103+
104+
## Pitfalls (encountered, in this lib's history)
105+
106+
1. **Text-level sed corrupts docstrings.** `sed 's/X/Y/g'` on `.lean`
107+
files matches `X` inside `/-- ... -/` blocks too. After bulk
108+
migration, search docstrings with `scripts/lean-grep.sh '<old form>'`
109+
and clean residual mentions. Or use VSCode F2 (semantic) when
110+
available.
111+
112+
2. **`open scoped X` requires the namespace to exist via imports.**
113+
Adding `open scoped X` to a file whose import graph doesn't reach a
114+
`namespace X` declaration produces "unknown namespace X" build
115+
error. After any sweep that adds scoped opens, verify with
116+
`scripts/lean-grep.sh 'open scoped'`.
117+
118+
3. **Notation prefix conflicts with built-in syntax.**
119+
- `T[x]` clashes with Lean's array indexing (`term[term]`)
120+
- `T x` (where `T` is an identifier) is parsed as function
121+
application, beating a `notation:max "T " x:max` pattern
122+
- Solutions that work: paren form (`Tan(x)`, like `Ric(X, Y)`),
123+
bracket form with non-identifier prefix (`∇[X]`, since `` is in
124+
Unicode category `Sm`, not `Lu`).
125+
126+
4. **Notation requires careful eta-reduction.** `fun x => f x`
127+
wrappers in notation RHS create lambdas that don't beta-reduce in
128+
simp's normal form, breaking pattern matches in subsequent rewrites.
129+
Always eta-reduce: `notation X => f` not `notation X => fun x => f x`.
130+
131+
5. **Typeclass inference can fail through `_` in notation.** A
132+
`notation:max "Tan(" x ")" => TangentSpace _ x` gets stuck when
133+
Lean can't pin the implicit `I : ModelWithCorners` from
134+
surrounding context. If this happens repeatedly, either:
135+
(a) keep the original verbose form `TangentSpace I x`, or
136+
(b) make `I` explicit in the notation.
137+
`abbrev`-based shorthands hit similar issues.
138+
139+
6. **Force-pushing to remove an oversight is partially effective.**
140+
`Co-Authored-By: ...` trailers, once pushed to a public repo,
141+
remain in GitHub's contributor cache even after the commit is
142+
force-pushed away. The orphan commit is still server-side. Lesson:
143+
**don't push to a public repo with a trailer you'd regret**.
144+
Memory `feedback_release_repo_attribution.md` enforces this for
145+
MathNetwork/OpenGA going forward.
146+
147+
---
148+
149+
## Adding to this playbook
150+
151+
When a refactor pattern is performed **3+ times** with the same
152+
manual workaround, lift it into:
153+
154+
* a `scripts/` entry (if shell-tool-shaped), or
155+
* a `lake script` entry (if AST-aware), and
156+
* a row in the decision tree above + a Pitfall note if it has a known
157+
failure mode.
158+
159+
The playbook is dogfood. Trust accumulates over commits.

scripts/README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# `scripts/`
2+
3+
Reusable codemods + dev helpers for OpenGALib refactor workflows.
4+
Sister to `docs/REFACTOR_PLAYBOOK.md` (which documents the decision
5+
tree for *which* tool to reach for).
6+
7+
## Available scripts
8+
9+
* **`rewrite-import.sh OLD_PATH NEW_PATH`** — rewrite every
10+
`import OLD_PATH...` line to `import NEW_PATH...` across all `.lean`
11+
files. Prefix match, so paths nested under `OLD_PATH` come along
12+
automatically. Refuses to run on a dirty working tree.
13+
14+
* **`lean-grep.sh [grep_flags] PATTERN`**`grep -r` restricted to
15+
`.lean` files, excluding `.lake/` and `.git/`. Use this instead of
16+
raw `grep` to avoid matching Mathlib source under `.lake/`.
17+
18+
## Convention for adding new scripts
19+
20+
1. Triggered by **3+** repetitions of the same manual command in past
21+
refactor sessions. One-off pain doesn't justify a script.
22+
2. Self-contained shell scripts (no Python / Node deps). `bash` + standard
23+
Unix tools (`grep`, `sed`, `perl`, `find`).
24+
3. **Refuse to run on dirty working tree** if the script makes bulk
25+
modifications. Bulk edits must be one revertible step.
26+
4. Document inputs / examples in the script header.
27+
5. Add an entry to this README's "Available scripts" list.
28+
29+
## Why scripts and not just inline commands?
30+
31+
Each script is a **frozen decision** about how to handle a recurring
32+
operation safely. Inline `sed` / `find` commands accumulate
33+
context-specific bugs (forget to exclude `.lake/`, miss a `git status`
34+
check, etc.). Lifting them into named scripts gives:
35+
36+
* **Repeatability**`scripts/rewrite-import.sh A B` always behaves the
37+
same way, no edge case rediscovered.
38+
* **Documentation** — the script header is the spec.
39+
* **Auditability**`git log scripts/` shows how the workflow has
40+
evolved.

scripts/lean-grep.sh

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/usr/bin/env bash
2+
# Smart grep restricted to Lean source files in the project.
3+
# Excludes .lake/, .git/, and only matches .lean files.
4+
#
5+
# Usage:
6+
# scripts/lean-grep.sh [GREP_FLAGS...] PATTERN [PATH...]
7+
#
8+
# Examples:
9+
# scripts/lean-grep.sh -n 'TangentSpace I' # show line numbers
10+
# scripts/lean-grep.sh -l 'open scoped Riemannian' # list files only
11+
# scripts/lean-grep.sh -E 'covDeriv|mlieBracket' # extended regex
12+
#
13+
# Equivalent to:
14+
# grep -r --include='*.lean' --exclude-dir=.lake --exclude-dir=.git "$@"
15+
# but always with the right exclusions so you never accidentally match
16+
# Mathlib source under .lake/ or stale build artifacts.
17+
18+
set -euo pipefail
19+
20+
if [[ $# -eq 0 ]]; then
21+
echo "Usage: $0 [GREP_FLAGS...] PATTERN [PATH...]" >&2
22+
echo "If no PATH given, searches from cwd." >&2
23+
exit 1
24+
fi
25+
26+
# Default search root if no path given: current directory.
27+
HAS_PATH=0
28+
for arg in "$@"; do
29+
case "$arg" in
30+
-*) ;;
31+
*) HAS_PATH=1; break ;;
32+
esac
33+
done
34+
35+
if [[ $HAS_PATH -eq 0 ]]; then
36+
exec grep -r --include='*.lean' --exclude-dir=.lake --exclude-dir=.git "$@" .
37+
else
38+
exec grep -r --include='*.lean' --exclude-dir=.lake --exclude-dir=.git "$@"
39+
fi

scripts/rewrite-import.sh

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#!/usr/bin/env bash
2+
# Rewrite Lean import path prefixes across the project.
3+
#
4+
# Usage:
5+
# scripts/rewrite-import.sh OLD_PATH NEW_PATH
6+
#
7+
# Example:
8+
# scripts/rewrite-import.sh OpenGALib.Riemannian.Util OpenGALib.Util
9+
#
10+
# This rewrites every `import OLD_PATH...` line to `import NEW_PATH...`
11+
# across all .lean files (excluding .lake/, .git/). Prefix-only match,
12+
# so `import OLD_PATH.X.Y` becomes `import NEW_PATH.X.Y` automatically.
13+
#
14+
# Refuses to run on a dirty working tree — commit or stash first so the
15+
# bulk edit is one revertible step.
16+
17+
set -euo pipefail
18+
19+
if [[ $# -ne 2 ]]; then
20+
cat >&2 <<EOF
21+
Usage: $0 OLD_PATH NEW_PATH
22+
23+
Example:
24+
$0 OpenGALib.Riemannian.Util OpenGALib.Util
25+
EOF
26+
exit 1
27+
fi
28+
29+
OLD="$1"
30+
NEW="$2"
31+
32+
# Refuse to run on dirty working tree.
33+
if [[ -n "$(git status --porcelain --untracked-files=no)" ]]; then
34+
echo "error: working tree not clean (modified files exist)." >&2
35+
echo "Commit or stash first; bulk edits should be a single revertible step." >&2
36+
git status --short >&2
37+
exit 1
38+
fi
39+
40+
# Escape regex metacharacters in OLD path.
41+
OLD_ESCAPED=$(printf '%s' "$OLD" | sed 's/[][\\.*^$/]/\\&/g')
42+
43+
# Find files containing the import.
44+
FILES=$(grep -rlE "^import ${OLD_ESCAPED}" \
45+
--include="*.lean" \
46+
--exclude-dir=.lake --exclude-dir=.git \
47+
. || true)
48+
49+
if [[ -z "$FILES" ]]; then
50+
echo "No files import '$OLD'. Nothing to do." >&2
51+
exit 0
52+
fi
53+
54+
# Show what will change.
55+
COUNT=$(echo "$FILES" | wc -l | tr -d ' ')
56+
echo "Rewriting 'import $OLD...' → 'import $NEW...' in $COUNT file(s):"
57+
echo "$FILES" | sed 's/^/ /'
58+
echo
59+
60+
# Apply.
61+
echo "$FILES" | xargs perl -i -pe "s|^import ${OLD_ESCAPED}|import ${NEW}|"
62+
63+
echo "Done. Verify with:"
64+
echo " lake build"
65+
echo "If green, commit. If broken, 'git reset --hard' to revert."

0 commit comments

Comments
 (0)