Skip to content

Commit 599da3a

Browse files
Jammy2211claude
authored andcommitted
Structure lint validates git-tracked content, not the raw filesystem
Untracked local scratch (.pytest_cache/, in-progress downloads) is out of scope — the layout contract is about what gets committed. Falls back to a filesystem walk outside a git checkout (freshly spawned template). The stale !lensing_wiki/log.md gitignore exception follows the wiki/ move. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent b94ac21 commit 599da3a

3 files changed

Lines changed: 27 additions & 3 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ desktop.ini
3434
*.swp
3535
*~
3636
__pycache__/
37+
.pytest_cache/
3738
*.pyc
3839

3940
# LaTeX build artefacts
@@ -48,4 +49,4 @@ __pycache__/
4849
*.toc
4950

5051
# Exception — the wiki's own log.md is text and must be tracked
51-
!lensing_wiki/log.md
52+
!wiki/*/log.md

scripts/validate_structure.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from __future__ import annotations
1111

1212
import argparse
13+
import subprocess
1314
import sys
1415
from pathlib import Path
1516

@@ -41,6 +42,26 @@
4142

4243

4344
def iter_repo_files(root: Path):
45+
"""Yield the repo's content files — git-tracked when git is available.
46+
47+
Validating `git ls-files` (not the raw filesystem) keeps untracked local
48+
scratch — caches, in-progress downloads — out of scope: the contract is
49+
about what gets committed. Outside a git checkout (e.g. a freshly spawned
50+
template before `git init`) fall back to walking the tree.
51+
"""
52+
try:
53+
tracked = subprocess.run(
54+
["git", "-C", str(root), "ls-files", "-z"],
55+
capture_output=True,
56+
check=True,
57+
).stdout.decode()
58+
for rel in sorted(filter(None, tracked.split("\0"))):
59+
path = root / rel
60+
if path.is_file():
61+
yield path
62+
return
63+
except (OSError, subprocess.CalledProcessError):
64+
pass
4465
for path in sorted(root.rglob("*")):
4566
if ".git" in path.relative_to(root).parts:
4667
continue
@@ -50,11 +71,12 @@ def iter_repo_files(root: Path):
5071

5172
def validate_structure(root: Path) -> list[str]:
5273
errors: list[str] = []
74+
top_level = {p.relative_to(root).parts[0] for p in iter_repo_files(root)}
5375

5476
for entry in sorted(root.iterdir()):
5577
name = entry.name
56-
if name == ".git": # a directory in a normal checkout, a file in a worktree
57-
continue
78+
if name == ".git" or name not in top_level:
79+
continue # untracked local scratch (caches, downloads) is out of scope
5880
if entry.is_dir():
5981
if name not in ALLOWED_TOP_DIRS:
6082
errors.append(

tests/test_validate_structure.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def test_clean_layout_passes(tmp_path):
2222
def test_unexpected_top_level_entries_fail(tmp_path):
2323
root = _minimal_repo(tmp_path)
2424
(root / "DarkMatterModels").mkdir()
25+
(root / "DarkMatterModels" / "Navarro1996").write_text("", encoding="utf-8")
2526
(root / "euclid.sty").write_text("", encoding="utf-8")
2627
errors = validate_structure(root)
2728
assert any("DarkMatterModels" in e for e in errors)

0 commit comments

Comments
 (0)