Skip to content

Commit fd2cf49

Browse files
committed
Add automated problem notes workflow
1 parent 06f8bbd commit fd2cf49

8 files changed

Lines changed: 558 additions & 92 deletions

File tree

.github/workflows/update-progress.yml

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Update Progress
1+
name: Update Progress And Notes
22

33
on:
44
push:
@@ -23,17 +23,20 @@ jobs:
2323
with:
2424
python-version: "3.x"
2525

26+
- name: Refresh knowledge notes
27+
run: python3 scripts/sync_problem_notes.py
28+
2629
- name: Refresh README progress
2730
run: python3 scripts/update_progress.py
2831

29-
- name: Commit README updates
32+
- name: Commit generated updates
3033
run: |
31-
if git diff --quiet -- README.md; then
32-
echo "No README changes to commit."
34+
if git diff --quiet -- README.md notes data/problem_metadata.json; then
35+
echo "No generated changes to commit."
3336
exit 0
3437
fi
3538
git config user.name "github-actions[bot]"
3639
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
37-
git add README.md
38-
git commit -m "Update DSA progress"
40+
git add README.md notes data/problem_metadata.json
41+
git commit -m "Update DSA notes and progress"
3942
git push

README.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,34 @@ Tracked unique problems solved across all sheets: `0 / 293`
3030

3131
- Solve on `LeetCode`
3232
- Let `LeetSync` push the accepted submission into this repository
33-
- The `Update Progress` GitHub Action scans the synced solution names and refreshes the progress table in this README
33+
- The GitHub Action scans the synced solution names and refreshes the progress table in this README
34+
- The same workflow creates or updates per-problem notes in [`notes/problems/`](notes/problems) and refreshes the index at [`notes/INDEX.md`](notes/INDEX.md)
35+
36+
## Knowledge Capture
37+
38+
- Your solution code is saved by `LeetSync`
39+
- A note stub is generated automatically for each synced problem with:
40+
- LeetCode link
41+
- difficulty
42+
- topic tags
43+
- tracked sheet membership
44+
- synced solution paths
45+
- Your personal summary, chosen data structures, and exact approach go into the problem note file
46+
47+
To save your own approach quickly after solving, use:
48+
49+
```bash
50+
python3 scripts/update_problem_note.py two-sum \
51+
--summary "Find two indices whose values add up to the target." \
52+
--data-structures "Array, Hash Map" \
53+
--approach "Use a one-pass hash map to store seen values and check complements." \
54+
--time "O(n)" \
55+
--space "O(n)"
56+
```
3457

3558
## Notes
3659

3760
- `NeetCode 150` is a subset of `NeetCode 250`, so those counts intentionally overlap
3861
- `Striver's SDE Sheet` tracking only covers the LeetCode-backed problems from the official sheet
3962
- If a Striver problem is not solved on LeetCode, `LeetSync` cannot sync it into this repository
63+
- The repo can save question metadata automatically, but your exact reasoning is only accurate if you add it to the generated note

data/problem_metadata.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

notes/INDEX.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Problem Notes Index
2+
3+
This index is generated automatically from synced solutions.
4+
5+
No synced solutions detected yet.

scripts/repo_tools.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
#!/usr/bin/env python3
2+
from __future__ import annotations
3+
4+
import json
5+
import re
6+
from pathlib import Path
7+
8+
ROOT = Path(__file__).resolve().parents[1]
9+
TRACKS_PATH = ROOT / "data" / "tracks.json"
10+
11+
TRACK_LABELS = {
12+
"neetcode150": "NeetCode 150",
13+
"neetcode250": "NeetCode 250",
14+
"striverSdeSheetLeetCode": "Striver's SDE Sheet (LeetCode-backed)",
15+
}
16+
17+
# Striver's sheet still contains a few legacy LeetCode URLs.
18+
ALIASES = {
19+
"coin-change-2": "coin-change-ii",
20+
"implement-strstr": "find-the-index-of-the-first-occurrence-in-a-string",
21+
}
22+
23+
IGNORED_TOP_LEVEL = {
24+
".git",
25+
".github",
26+
".venv",
27+
"__pycache__",
28+
".pytest_cache",
29+
".ruff_cache",
30+
"data",
31+
"notes",
32+
"node_modules",
33+
"scripts",
34+
"venv",
35+
}
36+
37+
CODE_EXTENSIONS = {
38+
".c",
39+
".cc",
40+
".cpp",
41+
".cs",
42+
".go",
43+
".java",
44+
".js",
45+
".jsx",
46+
".kt",
47+
".kts",
48+
".php",
49+
".py",
50+
".rb",
51+
".rs",
52+
".scala",
53+
".sql",
54+
".swift",
55+
".ts",
56+
".tsx",
57+
}
58+
59+
60+
def normalize_candidate(value: str) -> str:
61+
value = value.strip().lower()
62+
value = value.replace("_", "-")
63+
value = re.sub(r"^\d+[.\-_\s]+", "", value)
64+
value = re.sub(r"[^a-z0-9]+", "-", value)
65+
value = re.sub(r"-{2,}", "-", value).strip("-")
66+
return value
67+
68+
69+
def canonical_slug(slug: str) -> str:
70+
normalized = normalize_candidate(slug)
71+
return ALIASES.get(normalized, normalized)
72+
73+
74+
def load_tracks() -> dict[str, list[str]]:
75+
raw = json.loads(TRACKS_PATH.read_text())
76+
tracks: dict[str, list[str]] = {}
77+
for name, slugs in raw.items():
78+
deduped: list[str] = []
79+
seen: set[str] = set()
80+
for slug in slugs:
81+
canonical = canonical_slug(slug)
82+
if canonical in seen:
83+
continue
84+
seen.add(canonical)
85+
deduped.append(canonical)
86+
tracks[name] = deduped
87+
return tracks
88+
89+
90+
def problem_track_memberships(slug: str, tracks: dict[str, list[str]] | None = None) -> list[str]:
91+
tracks = tracks or load_tracks()
92+
memberships = [
93+
TRACK_LABELS[name]
94+
for name, slugs in tracks.items()
95+
if slug in slugs
96+
]
97+
return memberships or ["General Practice"]
98+
99+
100+
def _top_level_problem_entries() -> list[Path]:
101+
entries: list[Path] = []
102+
for path in sorted(ROOT.iterdir()):
103+
if path.name.startswith(".") and path.name not in {".leetcode"}:
104+
continue
105+
if path.name in IGNORED_TOP_LEVEL:
106+
continue
107+
if path.is_dir():
108+
entries.append(path)
109+
continue
110+
if path.is_file() and path.suffix.lower() in CODE_EXTENSIONS:
111+
entries.append(path)
112+
return entries
113+
114+
115+
def _solution_files_for_entry(entry: Path) -> list[str]:
116+
if entry.is_file():
117+
return [str(entry.relative_to(ROOT))]
118+
119+
solution_paths: list[str] = []
120+
for path in sorted(entry.rglob("*")):
121+
if not path.is_file():
122+
continue
123+
if any(part.startswith(".") for part in path.parts):
124+
continue
125+
if path.suffix.lower() not in CODE_EXTENSIONS:
126+
continue
127+
solution_paths.append(str(path.relative_to(ROOT)))
128+
return solution_paths
129+
130+
131+
def discover_problem_solutions() -> dict[str, list[str]]:
132+
discovered: dict[str, set[str]] = {}
133+
for entry in _top_level_problem_entries():
134+
slug_source = entry.stem if entry.is_file() else entry.name
135+
slug = canonical_slug(slug_source)
136+
if not slug:
137+
continue
138+
discovered.setdefault(slug, set()).update(_solution_files_for_entry(entry))
139+
140+
return {slug: sorted(paths) for slug, paths in discovered.items()}
141+
142+
143+
def collect_solved_slugs(tracked_slugs: set[str]) -> set[str]:
144+
return set(discover_problem_solutions()).intersection(tracked_slugs)

0 commit comments

Comments
 (0)