sourcecode 1.30.22__py3-none-any.whl → 1.30.23__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sourcecode/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "1.30.22"
3
+ __version__ = "1.30.23"
@@ -25,6 +25,7 @@ class DiffSourceType(str, Enum):
25
25
  WORKTREE_STAGED = "WORKTREE_STAGED" # git diff --cached
26
26
  GIT_SINCE_REF = "GIT_SINCE_REF" # git diff ref HEAD
27
27
  GIT_RANGE = "GIT_RANGE" # git diff refA refB
28
+ HEAD_MINUS_1 = "HEAD_MINUS_1" # git diff HEAD~1 HEAD (auto-fallback when tree clean)
28
29
 
29
30
 
30
31
  # ─────────────────────────────────────────────────────────────────────────────
@@ -601,6 +602,83 @@ _FRONTEND_SYMPTOM_MAP: dict[str, list[str]] = {
601
602
  }
602
603
 
603
604
 
605
+ def _build_analysis_scope(
606
+ *,
607
+ task_name: str,
608
+ since: Optional[str],
609
+ delta_baseline: dict,
610
+ pr_scope_source: str,
611
+ pr_uncommitted_files: list,
612
+ ) -> dict:
613
+ """Build analysis_scope metadata from actual git resolution — never hardcoded."""
614
+ if task_name == "review-pr":
615
+ sources = pr_scope_source.split(",") if pr_scope_source else []
616
+ # Derive git_equivalent_command from sources
617
+ cmds: list[str] = []
618
+ if DiffSourceType.GIT_RANGE.value in sources or DiffSourceType.GIT_SINCE_REF.value in sources:
619
+ cmds.append(f"git diff --name-only {since} HEAD")
620
+ if DiffSourceType.HEAD_MINUS_1.value in sources:
621
+ cmds.append("git diff --name-only HEAD~1 HEAD")
622
+ if DiffSourceType.WORKTREE_UNSTAGED.value in sources:
623
+ cmds.append("git diff --name-only")
624
+ if DiffSourceType.WORKTREE_STAGED.value in sources:
625
+ cmds.append("git diff --name-only --cached")
626
+ git_cmd = " + ".join(cmds) if cmds else "git diff --name-only"
627
+ return {
628
+ "sources_used": sources,
629
+ "git_equivalent_command": git_cmd,
630
+ "includes_uncommitted": bool(pr_uncommitted_files),
631
+ "git_resolution": {
632
+ "source_of_truth": "RANGE" if since else (
633
+ "HEAD" if DiffSourceType.HEAD_MINUS_1.value in sources else "WORKTREE"
634
+ ),
635
+ "resolved_diff_strategy": pr_scope_source,
636
+ "commit_count_analyzed": 1 if DiffSourceType.HEAD_MINUS_1.value in sources else (
637
+ 0 if not since else None
638
+ ),
639
+ },
640
+ }
641
+
642
+ # delta
643
+ rpath = delta_baseline.get("resolution_path", "")
644
+ rref = delta_baseline.get("resolved_ref", "HEAD")
645
+
646
+ _COMMITTED_PATHS = {"exact_local_ref", "remote_tracking_ref", "symbolic_ref"}
647
+ if rpath in _COMMITTED_PATHS:
648
+ sources = [DiffSourceType.GIT_SINCE_REF.value]
649
+ git_cmd = f"git diff --name-only {rref} HEAD"
650
+ source_of_truth = "RANGE"
651
+ includes_uncommitted = False
652
+ elif rpath == "head_minus_1_fallback":
653
+ sources = [DiffSourceType.HEAD_MINUS_1.value]
654
+ git_cmd = "git diff --name-only HEAD~1 HEAD"
655
+ source_of_truth = "HEAD"
656
+ includes_uncommitted = False
657
+ elif rpath == "uncommitted_staged":
658
+ sources = [DiffSourceType.WORKTREE_STAGED.value]
659
+ git_cmd = "git diff --name-only --cached"
660
+ source_of_truth = "STAGED"
661
+ includes_uncommitted = True
662
+ else: # uncommitted_unstaged, uncommitted_changes, no_changes_confirmed
663
+ sources = [DiffSourceType.WORKTREE_UNSTAGED.value]
664
+ git_cmd = "git diff --name-only"
665
+ source_of_truth = "WORKTREE"
666
+ includes_uncommitted = True
667
+
668
+ return {
669
+ "sources_used": sources,
670
+ "git_equivalent_command": git_cmd,
671
+ "includes_uncommitted": includes_uncommitted,
672
+ "git_resolution": {
673
+ "source_of_truth": source_of_truth,
674
+ "resolved_diff_strategy": rpath or "unknown",
675
+ "commit_count_analyzed": 1 if rpath == "head_minus_1_fallback" else (
676
+ 0 if rpath == "no_changes_confirmed" else None
677
+ ),
678
+ },
679
+ }
680
+
681
+
604
682
  class TaskContextBuilder:
605
683
  def __init__(self, root: Path) -> None:
606
684
  self.root = root
@@ -870,9 +948,11 @@ class TaskContextBuilder:
870
948
  # For delta task, relevant_files must rank only files changed in the
871
949
  # specified git range, not the full repo by generic entrypoint scoring.
872
950
  _delta_files: Optional[set[str]] = None
951
+ _delta_baseline: dict = {} # resolution metadata threaded into analysis_scope
873
952
  if task_name == "delta":
874
- _delta_raw = self._get_git_changed_files(since=since)
875
- if _delta_raw is None:
953
+ _baseline = self._resolve_git_baseline(since=since)
954
+ _delta_baseline = _baseline
955
+ if _baseline["error"]:
876
956
  # Explicit --since ref couldn't be resolved — hard error, no fallback
877
957
  _avail_branches, _suggested = self._get_available_refs(since or "")
878
958
  _hints: list[str] = []
@@ -899,8 +979,11 @@ class TaskContextBuilder:
899
979
  error_hints=_hints,
900
980
  gaps=[f"Cannot compute delta: git ref '{since}' not found."] + _hints,
901
981
  ci_decision="git_ref_error",
982
+ resolution_path=_baseline.get("resolution_path"),
983
+ diff_validation_status=_baseline.get("diff_validation_status"),
902
984
  )
903
- elif _delta_raw:
985
+ _delta_raw = _baseline["files"]
986
+ if _delta_raw:
904
987
  _delta_files = set(_delta_raw)
905
988
 
906
989
  # ── 5d. review-pr: set _delta_files from pre-resolved git scope ──────────
@@ -1515,7 +1598,11 @@ class TaskContextBuilder:
1515
1598
  changed_files: list[str] = []
1516
1599
  affected_entry_points: list[str] = []
1517
1600
  if task_name in ("delta", "review-pr"):
1518
- changed_files = sorted(_delta_files) if _delta_files else (self._get_git_changed_files(since=since) or [])
1601
+ # For delta: _delta_files already resolved via _resolve_git_baseline no second git call.
1602
+ # For review-pr: _get_git_changed_files fallback still valid as last resort.
1603
+ changed_files = sorted(_delta_files) if _delta_files else (
1604
+ [] if task_name == "delta" else (self._get_git_changed_files(since=since) or [])
1605
+ )
1519
1606
  _ep_set = {ep.path for ep in entry_points}
1520
1607
  # include framework-detected entry points AND files classified as
1521
1608
  # entrypoint/controller/security by artifact taxonomy
@@ -1582,21 +1669,16 @@ class TaskContextBuilder:
1582
1669
  committed_changes=_pr_committed_changes,
1583
1670
  uncommitted_changes=_pr_uncommitted_changes,
1584
1671
  # transparency: explicit diff scope
1585
- analysis_scope={
1586
- "sources_used": _pr_scope_source.split(",") if task_name == "review-pr" and _pr_scope_source else (
1587
- [DiffSourceType.GIT_SINCE_REF.value] if task_name == "delta" and since else
1588
- [DiffSourceType.WORKTREE_UNSTAGED.value] if task_name == "delta" else
1589
- []
1590
- ),
1591
- "git_equivalent_command": (
1592
- f"git diff --name-only {since} HEAD" if since else
1593
- "git diff --name-only"
1594
- ) if task_name in ("delta", "review-pr") else None,
1595
- "includes_uncommitted": bool(
1596
- (task_name == "review-pr" and _pr_uncommitted_files) or
1597
- (task_name == "delta" and not since)
1598
- ),
1599
- } if task_name in ("delta", "review-pr") else {},
1672
+ analysis_scope=_build_analysis_scope(
1673
+ task_name=task_name,
1674
+ since=since,
1675
+ delta_baseline=_delta_baseline,
1676
+ pr_scope_source=_pr_scope_source,
1677
+ pr_uncommitted_files=_pr_uncommitted_files,
1678
+ ) if task_name in ("delta", "review-pr") else {},
1679
+ resolved_since_ref=_delta_baseline.get("resolved_ref") if task_name == "delta" else None,
1680
+ resolution_path=_delta_baseline.get("resolution_path") if task_name == "delta" else None,
1681
+ diff_validation_status=_delta_baseline.get("diff_validation_status") if task_name == "delta" else None,
1600
1682
  )
1601
1683
 
1602
1684
  def render_prompt(self, output: TaskOutput) -> str:
@@ -1962,6 +2044,17 @@ class TaskContextBuilder:
1962
2044
  if DiffSourceType.WORKTREE_STAGED.value not in sources:
1963
2045
  sources.append(DiffSourceType.WORKTREE_STAGED.value)
1964
2046
 
2047
+ # ── HEAD~1 fallback — working tree clean, surface last commit ────────
2048
+ # Without --since: if no unstaged/staged changes exist, fall back to the
2049
+ # last committed diff so a clean tree never silently returns no_changes.
2050
+ if since is None and not committed_files and not uncommitted_files:
2051
+ head_ok = _run("git", "rev-parse", "--verify", "HEAD~1")
2052
+ if head_ok is not None: # HEAD~1 exists
2053
+ head_committed = _run("git", "diff", "--name-only", "--relative", "HEAD~1", "HEAD")
2054
+ if head_committed:
2055
+ committed_files = head_committed
2056
+ sources.append(DiffSourceType.HEAD_MINUS_1.value)
2057
+
1965
2058
  # ── Drop paths outside self.root ──────────────────────────────────────
1966
2059
  def _drop_outside(lst: list[str]) -> list[str]:
1967
2060
  return [f for f in lst if not f.startswith("../") and not f.startswith("..\\")]
@@ -3164,25 +3257,33 @@ class TaskContextBuilder:
3164
3257
  }
3165
3258
 
3166
3259
  else:
3167
- # No since: uncommitted changes first
3260
+ # No since: unstaged staged → HEAD~1 — never error, clean tree is valid
3168
3261
  ok, out = _run("diff", "--name-only", "--relative", timeout=10)
3169
3262
  if ok:
3170
3263
  files = [line.strip() for line in out.splitlines() if line.strip()]
3171
3264
  if files:
3172
- return _make(files, "HEAD", "uncommitted_changes")
3265
+ return _make(files, "HEAD", "uncommitted_unstaged")
3266
+
3267
+ # Staged (committed to index but not yet to history)
3268
+ ok, out = _run("diff", "--name-only", "--cached", "--relative", timeout=10)
3269
+ if ok:
3270
+ files = [line.strip() for line in out.splitlines() if line.strip()]
3271
+ if files:
3272
+ return _make(files, "HEAD", "uncommitted_staged")
3173
3273
 
3174
- # HEAD~1 fallback
3274
+ # HEAD~1 fallback — working tree clean, surface last commit
3175
3275
  if _verify("HEAD~1"):
3176
3276
  files = _diff("HEAD~1")
3177
3277
  if files is not None:
3178
3278
  return _make(files or [], "HEAD~1", "head_minus_1_fallback")
3179
3279
 
3280
+ # Confirmed no changes: first commit or empty repo
3180
3281
  return {
3181
3282
  "files": [],
3182
3283
  "resolved_ref": "HEAD",
3183
- "resolution_path": "unresolvable",
3184
- "diff_validation_status": "invalid_ref",
3185
- "error": True,
3284
+ "resolution_path": "no_changes_confirmed",
3285
+ "diff_validation_status": "valid_empty",
3286
+ "error": False,
3186
3287
  }
3187
3288
 
3188
3289
  def _get_uncommitted_changed_files(self) -> list[str]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.30.22
3
+ Version: 1.30.23
4
4
  Summary: Deterministic codebase context for AI coding agents
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -221,7 +221,7 @@ Description-Content-Type: text/markdown
221
221
 
222
222
  **Deterministic, behavior-aware codebase context for AI agents and PR review.**
223
223
 
224
- ![Version](https://img.shields.io/badge/version-1.30.22-blue)
224
+ ![Version](https://img.shields.io/badge/version-1.30.23-blue)
225
225
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
226
226
 
227
227
  ---
@@ -257,7 +257,7 @@ pipx install sourcecode
257
257
 
258
258
  ```bash
259
259
  sourcecode version
260
- # sourcecode 1.30.22
260
+ # sourcecode 1.30.23
261
261
  ```
262
262
 
263
263
  ---
@@ -1,4 +1,4 @@
1
- sourcecode/__init__.py,sha256=d5lEITIIf1zIOm689tAJlsrD77TynMzXXXt1H2tvxjk,104
1
+ sourcecode/__init__.py,sha256=4VsjXZcGaVFIDFx4kI8Hlzw0_vN-CPBmQkBXMb29s-I,104
2
2
  sourcecode/adaptive_scanner.py,sha256=RTNExwWPXzjgLaRueT7UuxkPj5ZEToWjGbx1j0LSZ9E,10250
3
3
  sourcecode/architecture_analyzer.py,sha256=MyBa0Hf5HmkudZQDLKrjcWDKETXETXl0mQX1swtTwAA,39091
4
4
  sourcecode/architecture_summary.py,sha256=z34_6v7cSwy98cof2UVciGho7SCrZ93tiqMmq5WNzRQ,20405
@@ -22,7 +22,7 @@ sourcecode/git_analyzer.py,sha256=0Gyj-vMpIIN4nfriKXVRouNYBeJ59s6pQDX2Xu9Pq-U,13
22
22
  sourcecode/graph_analyzer.py,sha256=iUK-7pSV-cvGqqD2hENdYmhnm0wcXFEyK-xnu5ul8OU,62515
23
23
  sourcecode/metrics_analyzer.py,sha256=m0ENgtqKeBL17kUIK3fmGkgo7UfXBNHxCMj0H_Y5K7c,22750
24
24
  sourcecode/pr_comment_renderer.py,sha256=k5pCIP6iBNwy5UYPxu47CAq-62j4E9QZZOPyL3trH80,14799
25
- sourcecode/prepare_context.py,sha256=g36iiJ_7XnDR1-M68vp0Pyb9v_n387G8JdcxXYj0TSI,160472
25
+ sourcecode/prepare_context.py,sha256=biZaw1T_D5cQWSUnWBQmdLLdhe-7HClkHtHJgBvqFDs,165387
26
26
  sourcecode/progress.py,sha256=qn30sWaHOkjTgXsSBmiPkz7Rsbwc5oSlIe6JNEMYp_k,3149
27
27
  sourcecode/ranking_engine.py,sha256=virVglafZufioHpZpwktjMvUiL0TZELWQCQnQNV8dFo,9360
28
28
  sourcecode/redactor.py,sha256=xuGcadGEHaPw4qZXlMDvzMCsr4VOkdp3oBQptHyJk8c,2884
@@ -64,8 +64,8 @@ sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWn
64
64
  sourcecode/telemetry/events.py,sha256=oEvvulfsv5GIDWG2174gSS6tNB95w38AIYiYeifGKlE,2294
65
65
  sourcecode/telemetry/filters.py,sha256=Asa71oRl7q3Wt_FMwuufIZJFzSYdgRNKS8LHCIyFeYE,4805
66
66
  sourcecode/telemetry/transport.py,sha256=KJeIPCPWMdmbCP3ySGs2iUlia34U6vWne2dZsUezesw,1560
67
- sourcecode-1.30.22.dist-info/METADATA,sha256=01gxhfG0_4V4qosEA-V3HnFungaPYvRLR2wTs3aVsz0,28956
68
- sourcecode-1.30.22.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
69
- sourcecode-1.30.22.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
70
- sourcecode-1.30.22.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
71
- sourcecode-1.30.22.dist-info/RECORD,,
67
+ sourcecode-1.30.23.dist-info/METADATA,sha256=K9iUA2PpGOnHY1vF9hupWySK0WzXVyvweT4TS_XIYms,28956
68
+ sourcecode-1.30.23.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
69
+ sourcecode-1.30.23.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
70
+ sourcecode-1.30.23.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
71
+ sourcecode-1.30.23.dist-info/RECORD,,