[spark-compete] Narrow bare except in git_summary + run_git subprocess helpers in system_map.py to (subprocess.SubprocessError, OSError)#534
Conversation
QA write-up — for reviewer eyesTL;DR. Beforedef git_summary(path: Path) -> dict[str, Any]:
if not (path / ".git").exists():
return {"available": False}
try:
result = subprocess.run(
["git", "-C", str(path), "rev-parse", "--short", "HEAD"],
capture_output=True, text=True, timeout=2, check=False,
)
except Exception:
return {"available": True, "head_short": None}
...
def run_git(path: Path, args: list[str], timeout: int = 3) -> tuple[int, str]:
try:
result = subprocess.run(
["git", "-C", str(path), *args],
capture_output=True, text=True, timeout=timeout, check=False,
)
except Exception:
return 1, ""
...If a typo lands on the subprocess.run call, the AttributeError gets swallowed and the helper silently returns After except (subprocess.SubprocessError, OSError):
return {"available": True, "head_short": None} except (subprocess.SubprocessError, OSError):
return 1, ""Same silent-return short-circuit for the documented failure modes (subprocess timeout, missing or unexecutable git binary). Programming bugs propagate so the operator gets a real traceback. SmokeWhat didn't changeOne file, two lines changed. Except bodies byte-identical pre/post for both wrappers. The non-git-directory fast-path in Pattern-match: same shape as our own #164 (whoami subprocess wrapper in spark-intelligence-builder), narrower because |
…ss helpers to (subprocess.SubprocessError, OSError)
git_summary (L411) and run_git (L427) in src/spark_cli/system_map.py
are paired subprocess wrappers used across the system-map pipeline
to capture per-repo git head info. Both call subprocess.run with
check=False -- so the success path returns regardless of git's exit
code, and only TimeoutExpired (subprocess.SubprocessError) or
FileNotFoundError/PermissionError (OSError) on the binary itself can
raise. The bare 'except Exception:' folds any unrelated runtime error
(AttributeError on a typo, NameError from a refactor, KeyboardInterrupt
during a long scan) into a silent return: git_summary returns
head_short=None, run_git returns (1, '') -- the operator never finds
out whether git was unavailable, the repo was timing out, or the
helper itself broke.
Narrow each to (subprocess.SubprocessError, OSError) -- exhaustive
for the documented failure modes (subprocess timeout, missing or
unexecutable git binary). The except body is byte-identical pre/post;
only the catch tuple changes.
Smoke: non-git directory returns {available:False} early (unchanged);
TimeoutExpired and FileNotFoundError both flow through the narrow
catch; AttributeError propagates past it.
88b6256 to
ac289f6
Compare
|
Thanks, this was useful. We maintainer-adopted this exception-boundary improvement in #609 and merged it into What shipped:
Verification on the adopted PR:
Source credit is recorded for this PR, and #609 itself should not receive participant credit. Points are still gated because this PR body had Closing this PR as adopted into #609. No code action is needed unless you believe #609 missed part of the issue. |
|
R22/R23 final scoring update: this PR did receive final R22/R23 public leaderboard credit. Earlier gate, credit-review, or points-lock wording described the pre-final review state, not a final rejection or permanent zero. Final R22/R23 credit for this PR: 48 points. Thanks for the contribution. |
{
"schema": "spark-compete-hotfix-v1",
"event": "spark-compete-first-event",
"submission_mode": "public_repo_pr",
"submission_target_url": "#534",
"team": {
"name": "SparkThisUp",
"members": ["ValHallaBuilder", "Baz707", "DanFireDash"],
"github_accounts": ["4gjnbzb4zf-sudo"],
"llm_device_holder": "ValHallaBuilder",
"device_holder_github": "4gjnbzb4zf-sudo"
},
"target_repo": {
"id": "vibeforge1111/spark-cli",
"source": "https://github.com/vibeforge1111/spark-cli",
"owner_surface": "spark-cli"
},
"issue": {
"type": "bug",
"severity": "low",
"title": "git_summary and run_git subprocess helpers in system_map.py swallow programming bugs as generic Exception",
"actual_behavior": "src/spark_cli/system_map.py exposes two paired subprocess wrappers used across the system-map pipeline to capture per-repo git head info:
git_summary(path)at L411 andrun_git(path, args)at L427. Both invokesubprocess.run([...], check=False, timeout=N)and wrap the call inexcept Exception: return {available:True, head_short:None}/return 1, \"\". With check=False on subprocess.run, the only exceptions that call can raise are subprocess.TimeoutExpired (a SubprocessError subclass) when the timeout fires and FileNotFoundError/PermissionError (OSError subclasses) when git is missing or unexecutable. The bare Exception folds any unrelated runtime error (AttributeError on a typo, NameError from a refactor, KeyboardInterrupt during a long scan) into the same silent return -- the operator never finds out whether git was unavailable, the repo was timing out, or the helper itself broke.","expected_behavior": "The same silent-return short-circuit happens only for the documented failure modes: subprocess timeout (subprocess.SubprocessError) and missing/unexecutable git binary (OSError). Programming bugs propagate naturally so the operator sees them as a real traceback.",
"repro_steps": [
"gh pr checkout ",
"PYTHONPATH=src python -c "from spark_cli.system_map import git_summary, run_git; from pathlib import Path; import tempfile; d = Path(tempfile.mkdtemp()); print('non-git:', git_summary(d)); print('bogus:', run_git(Path('/'), ['rev-parse','--short','HEAD'], timeout=2))" -> non-git: {'available': False}; bogus: (128, '')",
"PYTHONPATH=src python -c "import subprocess; print(issubclass(subprocess.TimeoutExpired, subprocess.SubprocessError), issubclass(FileNotFoundError, OSError))" -> True True (narrow tuple is exhaustive)",
"Inverse: edit a typo into the subprocess.run call (e.g.
subprocess.runZZZ([...])) -- before the change, the AttributeError gets swallowed and the helper silently returns head_short=None / (1, ''). After the change, the AttributeError propagates so the operator sees the real bug."],
"affected_workflow": "system_map.py is the system-map compile module driven by
spark report system-map. git_summary and run_git are called across the per-repo board snapshot pipeline. When the wrappers silently swallow a programming bug, downstream system-map output lies about git head state -- the operator seeshead_short: nulland assumes git is missing when really a code bug in the wrapper hid the real failure."},
"evidence": {
"safe_links_only": true,
"before_after_proof": "Two paired try/except patterns. Before:
except Exception: return {available:True, head_short:None}andexcept Exception: return 1, ''. After:except (subprocess.SubprocessError, OSError): return {available:True, head_short:None}andexcept (subprocess.SubprocessError, OSError): return 1, ''. The except bodies are byte-identical -- only the catch tuple changes. subprocess.SubprocessError is the base class for TimeoutExpired (the only exception subprocess.run with check=False can raise from the run itself); OSError covers FileNotFoundError and PermissionError raised when the git binary is missing or unexecutable. Diff: 2 insertions, 2 deletions, one file.","links": ["https://github.com//pull/534"],
"forbidden": ["pdf","zip","exe","unknown downloads","shortened links","archives","binaries","tokens","browser cookies","wallet material","raw logs","raw conversations","raw memory","raw patches","private repo maps","private scoring details"]
},
"proposed_fix": {
"approach": "Replace
except Exception:withexcept (subprocess.SubprocessError, OSError):in both wrappers. The narrow tuple is exhaustive for subprocess.run with check=False -- TimeoutExpired (via SubprocessError) when the timeout fires, FileNotFoundError/PermissionError (via OSError) when the binary is missing. The catch tuple is the only thing that changes; the except body is byte-identical.","files_expected": ["src/spark_cli/system_map.py"],
"tests_or_smoke": "Manual smoke verified: non-git directory still returns {available:False} early (path check before subprocess); a real subprocess call with bogus args returns (128, '') as expected; TimeoutExpired and FileNotFoundError both flow through the narrow tuple; AttributeError propagates past it. AST parse of system_map.py is clean."
},
"pr": {
"branch": "sentinel/exception-narrow/system-map-git-subprocess-helpers",
"title_prefix": "[spark-compete]",
"author_github": "4gjnbzb4zf-sudo",
"body_must_include": ["packet","team","pr_author","repo","actual_behavior","expected_behavior","repro_steps","before_after_proof","tests_or_smoke","duplicate_notes","risk_notes","review_claim"],
"url": "#534"
},
"review_claim": {
"impact_claim": "low",
"evidence_types": ["redacted_terminal_excerpt"],
"duplicate_notes": "Pre-flight
gh pr list --search 'git_summary run_git narrow'returned zero hits. Our own #526 narrowed six sqlite-introspection helpers in this same file, pending #532 narrows read_json/read_toml at L306/L314, pending #533 narrows the inspect_spawner_prd_auto_trace JSONL scanner at L865-L887. git_summary and run_git are at L411 and L427, distinct from all of those. No competitor PR touches these helpers. Same shape as our own #164 (whoami subprocess wrapper in spark-intelligence-builder), which narrows to (CalledProcessError, FileNotFoundError, OSError) -- this PR uses (SubprocessError, OSError) because check=False means CalledProcessError is unreachable here.","risk_notes": "Local scope: one file, two lines changed. The except bodies are byte-identical pre/post for both wrappers. The non-git-directory fast-path is unchanged. Subprocess timeout and missing-binary conditions still trigger the documented silent-return short-circuit. The only observable behavior change is that programming bugs in the wrappers no longer get silently swallowed -- the goal of the fix.",
"review_state_requested": "pr_review"
}
}