Skip to content

Commit b444036

Browse files
committed
Add graduation detection, scoped staging, and reorder-commits script
1 parent 2a73e0d commit b444036

File tree

5 files changed

+217
-23
lines changed

5 files changed

+217
-23
lines changed

.cursor/rules/typescript-standards.mdc

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,29 @@ Exception: Empty handlers are acceptable ONLY when the rejection is an expected
130130
Exception: Auto-load effects where infinite retry on persistent failure is undesirable — keep `== null` there and allow retry only via explicit user action.
131131
</standard>
132132

133+
<standard id="file-structure">Component files (`.tsx`) and utility files (`.ts`) follow a consistent section ordering.
134+
135+
**File-level ordering:**
136+
1. Imports
137+
2. Types / Interfaces — exported types first, then internal `Props`
138+
3. Constants
139+
4. Main component (`export const Scene: React.FC<Props>`)
140+
5. Sub-components (internal, non-exported)
141+
6. Styles (`getStyles` / `cacheStyles`)
142+
7. Helpers / utility functions — pure functions at the very end of the file
143+
144+
**Component body ordering:**
145+
1. Props destructuring
146+
2. Theme / styles (`useTheme`, `getStyles`)
147+
3. State (`useState`)
148+
4. Refs (`useRef`)
149+
5. Selectors (`useSelector`, `useWatch`)
150+
6. Derived values / `useMemo`
151+
7. Handlers (`useHandler`)
152+
8. Effects (`useEffect`, `useBackEvent`)
153+
9. Return JSX
154+
</standard>
155+
133156
</standards>
134157

135158
<lint-fix-patterns description="Common ESLint fix recipes for this project's config. Apply directly — do not search node_modules for types. The `rule` attribute maps to ESLint rule IDs for automatic matching by `lint-warnings.sh`.">

.cursor/skills/im/SKILL.md

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -63,26 +63,26 @@ If the task spans multiple repos, note the additional repos but implement in the
6363
This script:
6464

6565
1. Runs `eslint --fix`
66-
2. Shows any remaining lint findings grouped by rule
67-
3. Outputs matching fix patterns from `~/.cursor/rules/typescript-standards.mdc`
68-
4. Flags unmatched rules that need new patterns added
66+
2. Detects files that will be "graduated" from the warning suppression list on commit, promoting their suppressed-rule warnings to errors in the output
67+
3. Shows any remaining findings grouped by rule (with graduation promotions already applied)
68+
4. Outputs matching fix patterns from `~/.cursor/rules/typescript-standards.mdc`
69+
5. Flags unmatched rules that need new patterns added
6970

7071
If the script auto-fixes files or remaining findings exist:
7172

72-
1. Apply fixes for the remaining findings using the matched patterns in the output
73-
2. For **unmatched rules**: After fixing, add a new `<pattern id="..." rule="...">` to `typescript-standards.mdc` so future occurrences have guidance
74-
3. Commit the pre-existing lint changes separately:
73+
1. Fix all reported **errors** first — these include graduation-promoted warnings that will block `lint-commit.sh` after the file is removed from the suppression list
74+
2. Fix remaining **warnings** using the matched patterns in the output
75+
3. For **unmatched rules**: After fixing, add a new `<pattern id="..." rule="...">` to `typescript-standards.mdc` so future occurrences have guidance
76+
4. Commit the pre-existing lint changes separately:
7577
```bash
7678
~/.cursor/skills/lint-commit.sh -m "Fix lint warnings in <ComponentName>" <file1> <file2> ...
7779
```
7880

7981
**Architectural vs mechanical fixes**: If a pattern notes "architectural change" (e.g., `styled()` refactoring), flag to user rather than fixing inline — these changes have broader impact and may warrant separate discussion.
8082

81-
`lint-commit.sh` treats passed file arguments as the primary commit scope, auto-includes generated companion files like `src/locales/strings`, `eslint.config.mjs`, and snapshots, and reports any additional non-generated files it stages.
83+
`lint-commit.sh` treats passed file arguments as the primary commit scope and only stages those files plus generated companion files (`src/locales/strings`, `eslint.config.mjs`, snapshots). It does not stage unrelated dirty files in the working tree.
8284

8385
This ensures the subsequent feature commit introduces zero pre-existing lint findings. This is the initial pass — if you discover additional files to modify during Step 3, the same check applies (see Step 3).
84-
85-
**Warning graduation (edge-react-gui)**: `edge-react-gui` has a suppression list in `eslint.config.mjs` that turns `explicit-function-return-type`, `strict-boolean-expressions`, `use-unknown-in-catch-callback-variable`, and `ban-ts-comment` to warning severity for ~300+ files. When `lint-commit.sh` commits a file, `update-eslint-warnings.ts` removes it from this list — "graduating" the file to full error enforcement. If a graduated file has pre-existing violations on lines you touched, `lint-commit.sh` Step 2b will block the commit. Running `lint-warnings.sh` here in Step 2 catches and fixes these issues before graduation occurs.
8686
</step>
8787

8888
<step id="3" name="Implementation">
@@ -138,9 +138,13 @@ Other repos only have `## Unreleased` — no staging distinction.
138138
1. **Check for an open PR**: Run `gh pr view --json url,reviews 2>/dev/null` to determine if a PR exists and whether it has human review comments.
139139
2. **If a PR exists with human review comments**, skip cleanup — rewriting history would lose review context. Note the pending cleanup in the retrospective.
140140
3. **Otherwise (no PR, or PR with no human reviews)**, always perform ALL applicable cleanup automatically:
141-
- **Fixup commits exist**: Autosquash with `GIT_SEQUENCE_EDITOR=true git rebase -i --autosquash <base-branch>`. Do this immediately — never leave fixup commits unsquashed.
142-
- **Structural issues** (add-then-remove cycles, misplaced changes, commits that should be squashed, CHANGELOG in intermediate commits): Use scripted `GIT_SEQUENCE_EDITOR` to drop, reorder, or squash commits, resolving conflicts as needed. Verify the final tree matches the pre-restructure state with `git diff`.
143-
- **Git lock conflicts**: VSCode's built-in git integration may race with rebase operations, creating `.git/index.lock` files. Always run `rm -f .git/index.lock` before any `git rebase` command to prevent stalls. If a rebase step fails with "index.lock: File exists", remove the lock and `git rebase --continue`.
141+
- **Fixup commits exist**: Autosquash with `rm -f .git/index.lock && GIT_SEQUENCE_EDITOR=true git rebase -i --autosquash <base-branch>`. Do this immediately — never leave fixup commits unsquashed.
142+
- **Reorder commits**: Use the companion script to reorder commits to the desired order. Hashes are oldest-to-newest:
143+
```bash
144+
~/.cursor/skills/im/scripts/reorder-commits.sh <base-branch> <hash1> <hash2> ...
145+
```
146+
The script handles index lock cleanup, awk-based reordering, and verifies the tree is unchanged afterward.
147+
- **Structural issues** (add-then-remove cycles, misplaced changes, commits that should be squashed, CHANGELOG in intermediate commits): Use `reorder-commits.sh` for reordering. For squash/drop operations, use `rm -f .git/index.lock && GIT_SEQUENCE_EDITOR="..." git rebase -i <base-branch>` with an awk or sed script. Verify the final tree matches the pre-restructure state with `git diff`.
144148
</step>
145149

146150
<step id="5" name="Verification">

.cursor/skills/im/scripts/lint-warnings.sh

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
#!/usr/bin/env bash
22
# lint-warnings.sh
33
# Run eslint --fix on files and match any remaining findings to documented fix
4-
# patterns.
4+
# patterns. Detects files that will be "graduated" from the ESLint warning
5+
# suppression list when committed, promoting their suppressed-rule warnings to
6+
# errors so they can be fixed before commit.
57
#
68
# Usage:
79
# lint-warnings.sh <file1> [file2] ...
810
#
911
# Output:
1012
# 1. Summary of auto-fixes applied (if any)
11-
# 2. Summary of remaining findings per rule/severity
12-
# 3. Matched patterns from typescript-standards.mdc (full XML blocks)
13-
# 4. Unmatched rules (need new patterns added)
13+
# 2. Graduation warnings (files that will be promoted to error severity)
14+
# 3. Summary of remaining findings per rule/severity
15+
# 4. Matched patterns from typescript-standards.mdc (full XML blocks)
16+
# 5. Unmatched rules (need new patterns added)
1417
#
1518
# Exit codes:
1619
# 0 - No remaining lint findings after auto-fix
@@ -92,23 +95,59 @@ if (!Array.isArray(results)) {
9295
process.exit(2);
9396
}
9497
98+
// --- Graduation detection ---
99+
// Parse eslint.config.mjs to find files in the warning-suppression list.
100+
// These files currently have certain rules at "warn" severity, but committing
101+
// them removes them from the list (via update-eslint-warnings), promoting
102+
// those rules to "error". We detect this ahead of time so the agent can fix
103+
// them in a lint-fix commit before the feature commit.
104+
const GRADUATED_RULES = new Set([
105+
"@typescript-eslint/ban-ts-comment",
106+
"@typescript-eslint/explicit-function-return-type",
107+
"@typescript-eslint/strict-boolean-expressions",
108+
"@typescript-eslint/use-unknown-in-catch-callback-variable"
109+
]);
110+
111+
const suppressedFiles = new Set();
112+
try {
113+
const configPath = path.join(process.cwd(), "eslint.config.mjs");
114+
const configContent = fs.readFileSync(configPath, "utf8");
115+
// Extract file paths from the suppression block (single-quoted strings)
116+
for (const m of configContent.matchAll(/^\s+\x27([^\x27]+)\x27,?\s*$/gm)) {
117+
suppressedFiles.add(m[1]);
118+
}
119+
} catch (error) {
120+
// No eslint.config.mjs or parse failure — skip graduation detection
121+
}
122+
95123
const findingsBySeverity = new Map([
96124
[2, new Map()],
97125
[1, new Map()]
98126
]);
99127
let totalErrors = 0;
100128
let totalWarnings = 0;
129+
let graduatedCount = 0;
101130
let autoFixedFiles = 0;
102131
103132
for (const file of results) {
104133
if (file != null && typeof file.output === "string") autoFixedFiles += 1;
105134
106135
const rel = path.relative(process.cwd(), file.filePath);
136+
const willGraduate = suppressedFiles.has(rel);
137+
107138
for (const message of file.messages) {
108139
if (message.severity !== 1 && message.severity !== 2) continue;
109140
110-
const findingsForSeverity = findingsBySeverity.get(message.severity);
111141
const rule = message.ruleId || "unknown";
142+
143+
// Promote suppressed-rule warnings to errors for files that will graduate
144+
let effectiveSeverity = message.severity;
145+
if (willGraduate && message.severity === 1 && GRADUATED_RULES.has(rule)) {
146+
effectiveSeverity = 2;
147+
graduatedCount += 1;
148+
}
149+
150+
const findingsForSeverity = findingsBySeverity.get(effectiveSeverity);
112151
if (!findingsForSeverity.has(rule)) {
113152
findingsForSeverity.set(rule, []);
114153
}
@@ -118,8 +157,8 @@ for (const file of results) {
118157
message: message.message
119158
});
120159
121-
if (message.severity === 2) totalErrors += 1;
122-
if (message.severity === 1) totalWarnings += 1;
160+
if (effectiveSeverity === 2) totalErrors += 1;
161+
else totalWarnings += 1;
123162
}
124163
}
125164
@@ -133,6 +172,10 @@ if (autoFixedFiles > 0) {
133172
console.log(`>> Auto-fixed ${autoFixedFiles} file(s)`);
134173
}
135174
175+
if (graduatedCount > 0) {
176+
console.log(`>> ${graduatedCount} warning(s) promoted to errors (graduation: file will be removed from suppression list on commit)`);
177+
}
178+
136179
if (totalErrors === 0 && totalWarnings === 0) {
137180
console.log(">> No remaining lint findings");
138181
process.exit(0);
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#!/usr/bin/env bash
2+
# reorder-commits.sh
3+
# Reorder commits on a branch to a specified order using non-interactive rebase.
4+
#
5+
# Usage:
6+
# reorder-commits.sh <base-branch> <hash1> <hash2> ...
7+
#
8+
# Arguments:
9+
# base-branch The branch/ref to rebase onto (e.g., origin/develop)
10+
# hash1..N Commit hashes in desired order (oldest to newest)
11+
#
12+
# The script verifies all hashes exist in base..HEAD, writes an awk-based
13+
# GIT_SEQUENCE_EDITOR to reorder the pick lines, and runs git rebase -i.
14+
# It verifies the tree is unchanged after rebase.
15+
#
16+
# Exit codes:
17+
# 0 - Reorder successful
18+
# 1 - Reorder failed (conflict, missing commits, tree mismatch)
19+
set -euo pipefail
20+
21+
if [[ $# -lt 3 ]]; then
22+
echo "Usage: reorder-commits.sh <base-branch> <hash1> <hash2> ..." >&2
23+
exit 1
24+
fi
25+
26+
BASE="$1"
27+
shift
28+
DESIRED_ORDER=("$@")
29+
30+
# Remove stale index locks
31+
rm -f .git/index.lock
32+
33+
# Get short hashes for matching rebase todo lines
34+
BRANCH_COMMITS=$(git log --reverse --format='%h' "$BASE..HEAD")
35+
BRANCH_COUNT=$(echo "$BRANCH_COMMITS" | wc -l | tr -d ' ')
36+
DESIRED_COUNT=${#DESIRED_ORDER[@]}
37+
38+
if [[ "$BRANCH_COUNT" -ne "$DESIRED_COUNT" ]]; then
39+
echo "Error: Branch has $BRANCH_COUNT commits but $DESIRED_COUNT hashes were provided" >&2
40+
echo "Branch commits: $BRANCH_COMMITS" >&2
41+
exit 1
42+
fi
43+
44+
# Resolve desired hashes to short hashes and verify they're on the branch
45+
DESIRED_SHORT=()
46+
for hash in "${DESIRED_ORDER[@]}"; do
47+
short=$(git rev-parse --short "$hash" 2>/dev/null) || {
48+
echo "Error: Cannot resolve hash '$hash'" >&2
49+
exit 1
50+
}
51+
if ! echo "$BRANCH_COMMITS" | grep -q "^${short}$"; then
52+
echo "Error: Commit $short is not in $BASE..HEAD" >&2
53+
exit 1
54+
fi
55+
DESIRED_SHORT+=("$short")
56+
done
57+
58+
# Capture pre-rebase tree for verification
59+
PRE_TREE=$(git rev-parse HEAD^{tree})
60+
61+
# Build awk script that reorders pick lines to match desired order
62+
# The awk program collects all pick lines, then outputs them in the order
63+
# specified by the DESIRED env var (space-separated short hashes)
64+
EDITOR_SCRIPT=$(mktemp)
65+
trap 'rm -f "$EDITOR_SCRIPT"' EXIT
66+
67+
cat > "$EDITOR_SCRIPT" << 'AWKSCRIPT'
68+
#!/usr/bin/env bash
69+
exec awk -v desired="$DESIRED" '
70+
BEGIN {
71+
n = split(desired, order, " ")
72+
}
73+
/^pick / {
74+
hash = $2
75+
lines[hash] = $0
76+
next
77+
}
78+
/^$/ || /^#/ { next }
79+
END {
80+
for (i = 1; i <= n; i++) {
81+
for (h in lines) {
82+
if (index(h, order[i]) == 1 || index(order[i], h) == 1) {
83+
print lines[h]
84+
break
85+
}
86+
}
87+
}
88+
}
89+
' "$1" > "$1.tmp" && mv "$1.tmp" "$1"
90+
AWKSCRIPT
91+
chmod +x "$EDITOR_SCRIPT"
92+
93+
export DESIRED="${DESIRED_SHORT[*]}"
94+
if GIT_SEQUENCE_EDITOR="$EDITOR_SCRIPT" git rebase -i "$BASE" 2>/dev/null; then
95+
POST_TREE=$(git rev-parse HEAD^{tree})
96+
if [[ "$PRE_TREE" == "$POST_TREE" ]]; then
97+
echo ">> Commits reordered successfully"
98+
git log --oneline "$BASE..HEAD"
99+
else
100+
echo "Error: Tree changed after reorder (pre: $PRE_TREE, post: $POST_TREE)" >&2
101+
echo "This indicates content was lost or modified during rebase." >&2
102+
exit 1
103+
fi
104+
else
105+
git rebase --abort 2>/dev/null || true
106+
echo "Error: Rebase failed (likely conflict). Aborted." >&2
107+
exit 1
108+
fi

.cursor/skills/lint-commit.sh

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -182,9 +182,25 @@ if node -e "process.exit(require('./package.json').scripts?.localize ? 0 : 1)" 2
182182
yarn localize
183183
fi
184184

185-
# Step 4: Stage everything and report effective commit scope
186-
echo ">> git add -A && git commit"
187-
git add -A
185+
# Step 4: Stage files and report effective commit scope
186+
if [[ "$PRIMARY_SCOPE_DECLARED" == "true" ]]; then
187+
echo ">> git add (scoped) && git commit"
188+
git add -- "${FILES[@]}"
189+
# Stage generated companion files if they have changes
190+
for companion in eslint.config.mjs; do
191+
if [[ -f "$companion" ]] && ! git diff --quiet -- "$companion" 2>/dev/null; then
192+
git add -- "$companion"
193+
fi
194+
done
195+
# Stage locales/strings if yarn localize changed them (already git-added by
196+
# yarn localize in some repos, but ensure they're staged)
197+
if git diff --quiet --cached -- src/locales/strings 2>/dev/null; then
198+
git diff --quiet -- src/locales/strings 2>/dev/null || git add -- src/locales/strings/ 2>/dev/null || true
199+
fi
200+
else
201+
echo ">> git add -A && git commit"
202+
git add -A
203+
fi
188204

189205
# Graduate files from eslint warning-override list if the repo has the script
190206
if node -e "process.exit(require('./package.json').scripts?.['update-eslint-warnings'] ? 0 : 1)" 2>/dev/null; then
@@ -267,7 +283,7 @@ if [[ ${#LINT_FILES[@]} -gt 0 && -x ./node_modules/.bin/jest ]]; then
267283
echo ">> Auto-generated companion files staged:"
268284
echo "$SNAP_CHANGES"
269285
fi
270-
git add -A
286+
git add -- $SNAP_CHANGES
271287
git commit --amend --no-edit --no-verify
272288
else
273289
echo ">> No snapshot changes"

0 commit comments

Comments
 (0)