Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions src/whygraph/cli/commands/scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from rich.text import Text

from whygraph.scan import (
AuthorResolver,
CodeGraphCrawler,
Crawler,
GitCrawler,
Expand All @@ -43,6 +44,7 @@
# Kept in one place so the whole set is trivially swappable (plan §10.5).
_ICON_STRUCTURAL = "🔎"
_ICON_PR_ORIGINS = "🔗"
_ICON_AUTHORS = "👥"
_ICON_LLM = "🧠"
_ICON_CODEGRAPH = "🕸"

Expand Down Expand Up @@ -164,7 +166,9 @@ def scan_cmd(
# headers can be numbered against the count of phases that actually run.
run_pr_origins = enrich_pr_origins and github_client is not None
run_analyze = descriptor is not None
phase_total = 1 + int(run_pr_origins) + int(run_analyze) # Phase 1 always runs
# Author resolution always runs: local-only, no network, no token, so it
# is valid under --no-remote and inside the auto-rescan git hooks.
phase_total = 1 + int(run_pr_origins) + 1 + int(run_analyze) # Phase 1 always runs

scan_log_path = db_path.parent / "scan.log"
phase_timings: dict[str, float] = {}
Expand Down Expand Up @@ -247,7 +251,26 @@ def scan_cmd(
ok=enricher.error is None,
)

# ── Phase 3 · LLM descriptions — the slow, token-heavy long pole,
# ── Phase 3 · Author identity — needs Phase 1's commits AND Phase 2's
# PR-origin rows: an address can appear ONLY in on_default_branch=0
# commits, so running before Phase 2 would miss that identity. ──
n += 1
console.rule(
f"{_ICON_AUTHORS} Phase {n}/{phase_total} · Author identity", style="cyan"
)
t0 = time.monotonic()
resolver = AuthorResolver(progress, repository=repository)
ran.append(resolver)
resolver.start()
resolver.join()
phase_timings["Author identity"] = time.monotonic() - t0
_print_phase_done(
"Author identity",
phase_timings["Author identity"],
ok=resolver.error is None,
)

# ── Phase 4 · LLM descriptions — the slow, token-heavy long pole,
# run strictly last and alone. Only ever describes main-walk
# commits, so the recovered on_default_branch=0 rows stay lazy. ──
if run_analyze:
Expand Down Expand Up @@ -364,6 +387,7 @@ def _render_results_panel(
git = by_name.get("git")
github = by_name.get("github")
enricher = by_name.get("pr-origins")
resolver = by_name.get("authors")
analyzer = by_name.get("analyze")

def _timing(title: str) -> str:
Expand Down Expand Up @@ -392,6 +416,14 @@ def _timing(title: str) -> str:
"PR-origin recovery",
*_optional_phase_cells(enricher, _timing("PR-origin recovery")),
)
# Author resolution always runs, so this row never renders "— skipped"
# in practice; it still goes through _optional_phase_cells so the panel
# keeps its promise of never crashing on an unexpected `ran` list.
grid.add_row(
_ICON_AUTHORS,
"Author identity",
*_optional_phase_cells(resolver, _timing("Author identity")),
)
grid.add_row(
_ICON_LLM,
"LLM descriptions",
Expand Down
22 changes: 14 additions & 8 deletions src/whygraph/scan/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,24 @@
:class:`GitHubCrawler` for GitHub pull requests and issues,
:class:`CodeGraphCrawler` which refreshes the CodeGraph index,
:class:`AnalyzeCrawler` which describes each commit's diff with an LLM,
and :class:`PROriginEnricher` which recovers a squash-merged PR's original
commits.
:class:`PROriginEnricher` which recovers a squash-merged PR's original
commits, and :class:`AuthorResolver` which collapses raw identities into
one row per human.

The CLI drives them in three ordered phases against the shared SQLite
database: **Phase 1** runs :class:`GitCrawler` and :class:`GitHubCrawler`
The CLI drives them in ordered phases against the shared SQLite database:
**Phase 1** runs :class:`GitCrawler` and :class:`GitHubCrawler`
concurrently; **Phase 2** runs :class:`PROriginEnricher` (which needs the
git + PR rows Phase 1 persisted); **Phase 3** runs :class:`AnalyzeCrawler`
last and alone (the slow, token-heavy LLM pass). :class:`CodeGraphCrawler`
is a best-effort background task started before Phase 1 and joined after
Phase 3 — it has no data dependency on the DB, so it spans the whole scan.
git + PR rows Phase 1 persisted); **Phase 3** runs :class:`AuthorResolver`
(which needs Phase 2's rows — an address can appear *only* in the
``on_default_branch=0`` commits that phase writes); **Phase 4** runs
:class:`AnalyzeCrawler` last and alone (the slow, token-heavy LLM pass).
:class:`CodeGraphCrawler` is a best-effort background task started before
Phase 1 and joined after the last phase — it has no data dependency on
the DB, so it spans the whole scan.
"""

from whygraph.scan.analyze_crawler import AnalyzeCrawler
from whygraph.scan.authors import AuthorResolver
from whygraph.scan.codegraph_crawler import CodeGraphCrawler
from whygraph.scan.crawler import Crawler
from whygraph.scan.git_crawler import GitCrawler
Expand All @@ -26,6 +31,7 @@

__all__ = [
"AnalyzeCrawler",
"AuthorResolver",
"CodeGraphCrawler",
"Crawler",
"GitCrawler",
Expand Down
Loading
Loading