From d18a54dabc17b96763007138f0bb575181a61b66 Mon Sep 17 00:00:00 2001 From: squid-protocol Date: Fri, 11 Sep 2026 20:25:19 -0400 Subject: [PATCH 1/2] fix(chronometer): resolve git history for subdirectory scans (#2976) The git gate was `(self.root / ".git").exists()`, and `.git` only exists at the repository root -- any scan rooted at a SUBDIRECTORY of a repo (galaxyscope path/to/repo/src, every keyword-rosetta corpus folder) silently lost the whole temporal pipeline: the mtime fallback fired, a fresh checkout's uniform mtimes tripped the TEMPORAL COLLAPSE guard, and every file read the neutral (stability 50, churn 0). - Gate on `git rev-parse --is-inside-work-tree` instead: resolves upward from any subdirectory; a linked worktree's `.git` FILE pointer passes identically; a non-git directory lands in the OS-walk fallback exactly as before. - The history stream gains `--relative -- .`: from a subdirectory, `git log --name-only` emits repo-relative paths while `git ls-files` emits cwd-relative ones, so the churn/mtime maps would have been keyed differently from both the tracked-files denominator and the scanner's scan-root-relative rel_path. Both flags are byte-identical no-ops at the repository root. - The 50% early-exit stopped a 6-file corpus folder after mapping 3 files; full coverage at or below 200 tracked files, monorepo math unchanged above. - Determinism-sensitive harnesses opt out EXPLICITLY now: the golden-crucible test sets GITGALAXY_DISABLE_GIT_HISTORY=1 (config key CHRONOMETER_CONFIG.DISABLE_GIT_HISTORY also honoured) -- before this fix it got temporal neutrality by accident of the broken root check. Both golden legs verified byte-identical (full_precision and zero_dependency venvs, 1 passed each). Closes #2976 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JaGQfW7WGbGXg54iyBg23j --- gitgalaxy/metrics/chronometer.py | 60 +++++++++++++++++++++++---- tests/core_engine/test_chronometer.py | 9 ++-- tests/test_golden_crucible.py | 10 ++++- 3 files changed, 65 insertions(+), 14 deletions(-) diff --git a/gitgalaxy/metrics/chronometer.py b/gitgalaxy/metrics/chronometer.py index 6ce9db25c..a1bfaa21c 100644 --- a/gitgalaxy/metrics/chronometer.py +++ b/gitgalaxy/metrics/chronometer.py @@ -107,14 +107,42 @@ def _initialize_history_scan(self): """Dispatches the survey engines to establish boundaries and churn cache.""" t_start = time.time() - # Step A: Git Binary Verification - if (self.root / ".git").exists(): + # Step A: Git Worktree Verification + # #2976: `.git` only exists at the repository ROOT, so the old check + # `(self.root / ".git").exists()` silently disabled the entire temporal + # pipeline for any scan rooted at a SUBDIRECTORY of a repo (galaxyscope + # path/to/repo/src, or a control-corpus language folder) -- the mtime + # fallback then fired, a fresh checkout's uniform mtimes tripped the + # TEMPORAL COLLAPSE guard below, and every file read the neutral + # (stability 50, churn 0). Ask git itself: rev-parse resolves upward + # from any subdirectory, and a linked worktree's `.git` FILE pointer + # passes the same way. A non-git directory still lands in the OS-walk + # fallback exactly as before. + # + # Determinism-sensitive harnesses (the golden-crucible pins scan a + # subdirectory of a pinned checkout whose git history is NOT part of + # the measured structure) opt out EXPLICITLY here -- before the fix + # they got temporal neutrality by accident of the broken root check. + if self.chrono_config.get("DISABLE_GIT_HISTORY", False) or os.environ.get( + "GITGALAXY_DISABLE_GIT_HISTORY", "" + ) == "1": + # is_git_enabled stays False: Steps B/C below run the same OS-walk + # fallback (including the collapse guard) a non-git directory gets. + self.logger.info("Git history disabled by configuration. Using OS-walk fallback.") + else: try: - subprocess.run([_GIT_BIN, "--version"], capture_output=True, check=True) # noqa: S603 - self.is_git_enabled = True - self.logger.debug("Git binary verified. Commencing Deep Boundary Survey.") - except (subprocess.CalledProcessError, FileNotFoundError): - self.logger.warning("Git binary not found. Falling back to OS Walk.") + res = subprocess.run( # noqa: S603 -- _GIT_BIN resolved absolute, fixed args + [_GIT_BIN, "rev-parse", "--is-inside-work-tree"], + cwd=self.root, + capture_output=True, + text=True, + check=True, + ) + if res.stdout.strip() == "true": + self.is_git_enabled = True + self.logger.debug("Git worktree verified. Commencing Deep Boundary Survey.") + except (subprocess.CalledProcessError, FileNotFoundError, OSError): + self.logger.warning("Not inside a git worktree (or git unavailable). Falling back to OS Walk.") # Step B: Establish Absolute Project Boundaries (Min/Max Time) self._determine_commit_bounds() @@ -268,7 +296,12 @@ def _scan_git_history(self): # Axis 1 (Volume): Stop scanning once 50% of active files are mapped (max 5000). # Axis 2 (Time): Hard abort after 'timeout_limit' seconds. # ====================================================================== - required_files = min(int(total_files * 0.50), 5000) + # #2976: a tiny subtree's year-log parses in milliseconds -- the 50% early + # exit only ever protected monorepo scans, and on a 6-file control-corpus + # folder it stopped after mapping 3 files, leaving the rest with no history + # at all. Full coverage at or below 200 files; the monorepo math is + # unchanged above that, and the Axis-2 time budget still applies to both. + required_files = total_files if total_files <= 200 else min(int(total_files * 0.50), 5000) timeout_limit = self.chrono_config.get("STREAM_TIMEOUT_SECONDS", 15.0) self.logger.info(f"Chronometer: Engaging 1-Year Historical Sweep. Budget: {timeout_limit}s") @@ -277,13 +310,24 @@ def _scan_git_history(self): # 3. The Command: Limit Git to the last year of commits. # This generates massive churn spikes without getting bogged down in decade-old bedrock. + # #2976: `--relative -- .` scopes the stream to the scanned subtree and names + # files relative to cwd. Without it, a subdirectory scan streams the WHOLE + # repo's log (burning the time budget on unrelated files) and `--name-only` + # emits repo-relative paths while `git ls-files` above emits cwd-relative + # ones -- so churn_map/mtime_map would be keyed differently from both the + # tracked-files denominator and the scanner's scan-root-relative rel_path, + # and every lookup would miss. At the repository root both flags are no-ops: + # the output is byte-identical to the previous command. cmd = [ _GIT_BIN, "log", "--since=1.year", "--name-only", + "--relative", "--pretty=format:@@GIT_COMMIT@@|%H|%at|%an", "--no-merges", + "--", + ".", ] # Execute the stream diff --git a/tests/core_engine/test_chronometer.py b/tests/core_engine/test_chronometer.py index 70db83c9b..67b839f4a 100644 --- a/tests/core_engine/test_chronometer.py +++ b/tests/core_engine/test_chronometer.py @@ -37,14 +37,13 @@ def test_chronometer_no_git_fallback(mock_getmtime, mock_walk, mock_run, tmp_pat def test_chronometer_git_boundaries(mock_run, tmp_path): """Proves the boundary scanner correctly extracts min/max times from git logs.""" - # Simulate the .git directory existing so the hardware check fires - (tmp_path / ".git").mkdir() - def git_side_effect(cmd, **kwargs): m = MagicMock() m.stdout = "" - if "--version" in cmd: - m.stdout = "git version 2.40.0\n" + if "rev-parse" in cmd: + # #2976: the gate asks git itself rather than testing (root/.git).exists(), + # so a subdirectory of a repo (and a linked worktree) passes too. + m.stdout = "true\n" elif "log" in cmd and "-1" in cmd and "HEAD" not in cmd and len(cmd) == 4: m.stdout = "5000\n" # Max Time elif "rev-list" in cmd: diff --git a/tests/test_golden_crucible.py b/tests/test_golden_crucible.py index 5a2d47866..065531b3a 100644 --- a/tests/test_golden_crucible.py +++ b/tests/test_golden_crucible.py @@ -66,7 +66,15 @@ def test_golden_crucible_matches_baseline(tmp_path): ], check=True, timeout=180, - env={**os.environ, "GITGALAXY_LICENSE_KEY": "COMMUNITY_FREE_TIER"}, + env={ + **os.environ, + "GITGALAXY_LICENSE_KEY": "COMMUNITY_FREE_TIER", + # #2976: the golden masters pin a corpus whose git history is not part + # of the measured structure. Before the chronometer's subdir fix they + # got temporal neutrality by accident (the scan root data/ has no .git); + # this makes the same neutrality explicit and deterministic. + "GITGALAXY_DISABLE_GIT_HISTORY": "1", + }, ) actual_path = output_dir / "data_galaxy_audit.json" From 29c25541fbe7fd60603537a76dbb4797e41bb28e Mon Sep 17 00:00:00 2001 From: squid-protocol Date: Fri, 11 Sep 2026 20:39:52 -0400 Subject: [PATCH 2/2] fix(chronometer): declare DISABLE_GIT_HISTORY + ruff format (#2976 CI) The dead-key audit requires a read key to be written somewhere: DISABLE_GIT_HISTORY joins CHRONOMETER_CONFIG's defaults (False), and the GITGALAXY_DISABLE_GIT_HISTORY env var joins the audit ALLOWLIST on the GITGALAXY_LICENSE_KEY precedent. ruff format reflows the gate condition. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JaGQfW7WGbGXg54iyBg23j --- gitgalaxy/metrics/chronometer.py | 7 ++++--- gitgalaxy/standards/gitgalaxy_config.py | 6 ++++++ tests/dead_key_audit.py | 1 + 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/gitgalaxy/metrics/chronometer.py b/gitgalaxy/metrics/chronometer.py index a1bfaa21c..33dcbfacf 100644 --- a/gitgalaxy/metrics/chronometer.py +++ b/gitgalaxy/metrics/chronometer.py @@ -123,9 +123,10 @@ def _initialize_history_scan(self): # subdirectory of a pinned checkout whose git history is NOT part of # the measured structure) opt out EXPLICITLY here -- before the fix # they got temporal neutrality by accident of the broken root check. - if self.chrono_config.get("DISABLE_GIT_HISTORY", False) or os.environ.get( - "GITGALAXY_DISABLE_GIT_HISTORY", "" - ) == "1": + if ( + self.chrono_config.get("DISABLE_GIT_HISTORY", False) + or os.environ.get("GITGALAXY_DISABLE_GIT_HISTORY", "") == "1" + ): # is_git_enabled stays False: Steps B/C below run the same OS-walk # fallback (including the collapse guard) a non-git directory gets. self.logger.info("Git history disabled by configuration. Using OS-walk fallback.") diff --git a/gitgalaxy/standards/gitgalaxy_config.py b/gitgalaxy/standards/gitgalaxy_config.py index 6c153c015..d24a983e4 100644 --- a/gitgalaxy/standards/gitgalaxy_config.py +++ b/gitgalaxy/standards/gitgalaxy_config.py @@ -699,6 +699,12 @@ # Consumed by: chronometer.py # ------------------------------------------------------------------------------ CHRONOMETER_CONFIG = { + # #2976: force the OS-walk fallback even inside a git worktree. For + # determinism-sensitive harnesses (the golden-crucible pins) whose + # corpus git history is not part of the measured structure. The + # GITGALAXY_DISABLE_GIT_HISTORY=1 environment variable is the same + # switch for subprocess invocations. + "DISABLE_GIT_HISTORY": False, # The absolute ceiling for OS-level fallback scanning "FALLBACK_SCAN_LIMIT": 25000, # Process management diff --git a/tests/dead_key_audit.py b/tests/dead_key_audit.py index 8a99c91cb..4aca91123 100644 --- a/tests/dead_key_audit.py +++ b/tests/dead_key_audit.py @@ -95,6 +95,7 @@ # --- External YAML/env config --- "galaxyscope": "top-level section name in a user's .galaxyscope.yml project config file", "GITGALAXY_LICENSE_KEY": "environment variable (os.environ.get), not a repo-produced dict", + "GITGALAXY_DISABLE_GIT_HISTORY": "environment variable (os.environ.get), not a repo-produced dict (#2976)", "vulnerability_density_min": "optional risk_tuning YAML key (signal_processor.py risk-equation-style tuning)", "asymptotic_dampener": "optional risk_tuning YAML key (signal_processor.py)", "quarantine": "STATIC_ARCHETYPES app-config constant, read with a graceful string fallback",