-
Notifications
You must be signed in to change notification settings - Fork 1
scan
- src/repodocs/scan.py
- tests/test_fix_scan_util.py
repodocs.scan performs a single, deterministic filesystem walk of the target repository and produces the inventory artifact that the planning stage consumes. It exposes three functions: scan(), which walks the tree and collects raw facts; readme_headings(), which extracts heading text from the repo's README; and scan_inventory(), which combines both into the final structured record.
Sources: src/repodocs/scan.py:L1-L11
scan(repo, out=None) walks repo with os.walk, skipping repodocs' own generated trees (repo-docs/, graphify-out/, and whatever directory out points to) so that a rerun never ingests its own vendored output. Directories are also pruned by SKIP_DIRS, dotfile directories, and a MAX_DEPTH cutoff. For each candidate directory it resolves the --out directory (if any) and compares it against each subdirectory's resolved path so the pruning works regardless of how deeply nested out is under repo.
Sources: src/repodocs/scan.py:L11-L34
For files, scan() checks fp.is_symlink() first and drops any symlink whose resolved target falls outside the repository root (repo_real) — this prevents a planted symlink from smuggling an out-of-tree file into the wiki. Remaining files are tested with is_source(); matches are added to src_files, bucketed by top-level directory in top_dirs, and line-counted via count_lines() into line_counts.
Sources: src/repodocs/scan.py:L35-L47
CI workflow files are gathered separately by globbing .github/workflows/* and filtering each entry through safe_repo_file(), which drops symlinks that escape the repo or loop back on themselves. The list is sorted before being returned.
Sources: src/repodocs/scan.py:L49-L56
scan() returns a dict with the following keys:
| Key | Description |
|---|---|
src_files |
Sorted list of repo-relative source file paths |
line_counts |
Dict mapping each source file to its line count |
top_dirs |
Dict mapping top-level directory name (or ".") to its list of source files |
has_readme |
Whether README.md or readme.md exists and isn't an escaping symlink |
manifests |
Subset of MANIFESTS present in the repo |
has_contributing / has_changelog / has_security
|
Presence of CONTRIBUTING.md, CHANGELOG.md, SECURITY.md
|
ci |
Sorted list of .github/workflows/* file paths |
tests |
Subset of src_files identified as test files by is_test()
|
All presence checks use the has() helper, which calls safe_repo_file(repo, name) so that escaping or looping symlinks never register as present.
Sources: src/repodocs/scan.py:L49-L68
readme_headings(repo) reads README.md (or readme.md) line by line, toggling a fenced flag on lines beginning with triple backticks, and collects lines matching ^#{1,6}\s+\S outside of fenced code blocks. This avoids picking up #-prefixed text inside code samples (e.g. shell comments) as headings. Returns an empty list if no README exists.
Sources: src/repodocs/scan.py:L71-L82
scan_inventory(repo, out=None) calls scan() and readme_headings() and assembles the combined inventory dict that downstream planning consumes:
{
"name": <repo directory name>,
"source_file_count": <int>,
"source_files": [...],
"manifests": [...],
"readme_headings": [...],
"has_readme": bool,
"has_contributing": bool,
"has_changelog": bool,
"has_security": bool,
"ci": [...],
"tests": [...],
}
Note that line_counts and top_dirs from the raw scan() facts are not carried into scan_inventory()'s output; only source_files, source_file_count, and the presence/list fields above are exposed.
Sources: src/repodocs/scan.py:L85-L99

Scanning treats three distinct symlink hazards, each covered by a regression test:
-
Escaping source symlinks: a symlink resolving outside
repo_realis excluded fromsrc_filesbefore it's ever considered a source file. -
Escaping metadata symlinks:
README.md, manifest files, and CI workflow files that are symlinks pointing outside the repo are excluded viasafe_repo_file(), sohas_readme,manifests, andcidon't report them as present. -
Symlink loops: a self-referencing symlink (e.g.
loop -> loop) causesPath.resolve()to raiseOSError(ELOOP);safe_repo_file()catches this and returnsNonerather than propagating the exception.
Sources: tests/test_fix_scan_util.py:L13-L23, tests/test_fix_scan_util.py:L51-L72, tests/test_fix_scan_util.py:L106-L112
Dangling symlinks (pointing to a nonexistent target) and non-regular files such as FIFOs are excluded from src_files and line_counts, since scan() only processes entries where fp.is_file() is true. A --out directory nested arbitrarily deep under the repo (e.g. src/generated/) is pruned during the walk while sibling directories (src/actual/) are scanned normally, since the out-directory comparison is resolved against each candidate subdirectory rather than only at the repo root.
Sources: tests/test_fix_scan_util.py:L13-L48
The ci list returned by scan() is always sorted lexicographically, regardless of the order files are created or returned by the filesystem, and excludes any workflow file that is a symlink escaping the repo.
Sources: tests/test_fix_scan_util.py:L75-L103
- Home
- Installation & Setup
- Architecture
- CLI Reference
- LLM Backend Selection
- Repository Scanning
- Wiki Page Planning
- Page Generation
- Source Citations
- Translation
- HTML Rendering
- Diagram Rendering
- Diagram Poster Tool
- Git Remote Link Resolution
- Publishing
- GitHub Wiki Integration
- Shared Utilities
- Environment Configuration
- Security & Trust Boundaries
- Limitations & Non-goals
- Testing
- Development
- Contributing
- Upgrading
- Changelog