sourcecode 1.30.17__py3-none-any.whl → 1.30.18__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.17"
3
+ __version__ = "1.30.18"
@@ -623,6 +623,8 @@ def _find_symbol_files(
623
623
  cwd=str(root),
624
624
  capture_output=True,
625
625
  text=True,
626
+ encoding="utf-8",
627
+ errors="replace",
626
628
  timeout=20,
627
629
  )
628
630
  for line in result.stdout.splitlines():
@@ -561,17 +561,23 @@ _ARTIFACT_CHANGE_EFFECT: dict[str, str] = {
561
561
  # Maps frontend symptom keywords → backend terms likely to contain the root cause.
562
562
  # Used to boost service/interceptor files when the symptom is UI-only.
563
563
  _FRONTEND_SYMPTOM_MAP: dict[str, list[str]] = {
564
- "spinner": ["loading", "setloading", "finalize", "httpinterceptor", "interceptor", "service"],
565
- "loading": ["loading", "setloading", "finalize", "httpinterceptor", "interceptor", "service"],
566
- "login": ["authcontroller", "securityconfig", "filterconfig", "jwtfilter", "auth", "authentication"],
567
- "logout": ["authcontroller", "securityconfig", "jwtfilter", "auth", "session"],
568
- "dropdown": ["getmapping", "findall", "obtenertodos", "listall", "findby"],
569
- "modal": ["controller", "getmapping", "findby", "search"],
570
- "popup": ["controller", "getmapping", "findby", "search"],
571
- "table": ["paginated", "findby", "search", "getmapping", "listall"],
572
- "grid": ["paginated", "findby", "search", "getmapping"],
573
- "button": ["postmapping", "putmapping", "deletemapping", "controller", "service"],
574
- "form": ["postmapping", "putmapping", "controller", "service", "dto"],
564
+ "spinner": ["loading", "setloading", "finalize", "httpinterceptor", "interceptor", "service"],
565
+ "loading": ["loading", "setloading", "finalize", "httpinterceptor", "interceptor", "service"],
566
+ "login": ["authcontroller", "securityconfig", "filterconfig", "jwtfilter", "auth", "authentication"],
567
+ "logout": ["authcontroller", "securityconfig", "jwtfilter", "auth", "session"],
568
+ "dropdown": ["getmapping", "findall", "obtenertodos", "listall", "findby"],
569
+ "modal": ["controller", "getmapping", "findby", "search"],
570
+ "popup": ["controller", "getmapping", "findby", "search"],
571
+ "table": ["paginated", "findby", "search", "getmapping", "listall"],
572
+ "grid": ["paginated", "findby", "search", "getmapping"],
573
+ "button": ["postmapping", "putmapping", "deletemapping", "controller", "service"],
574
+ "form": ["postmapping", "putmapping", "controller", "service", "dto"],
575
+ # session-related symptoms (Spanish + English)
576
+ "sesion": ["httpsession", "sessionmanager", "sessionservice", "sessionrepository", "sessionfactory", "authentication"],
577
+ "sesiones": ["httpsession", "sessionmanager", "sessionservice", "sessionrepository", "sessionfactory", "authentication"],
578
+ "session": ["httpsession", "sessionmanager", "sessionservice", "sessionrepository", "sessionfactory", "authentication"],
579
+ # worker/assignment domain terms (common in RRHH/HR systems)
580
+ "trabajador": ["trabajador", "empleado", "worker", "asignacion", "trabajadordao", "trabajadorservice"],
575
581
  }
576
582
 
577
583
 
@@ -826,12 +832,14 @@ class TaskContextBuilder:
826
832
  # ── 5b. Git signals for ranking ────────────────────────────────────
827
833
  git_hotspots: dict[str, int] = {}
828
834
  uncommitted_files: set[str] = set()
835
+ _recent_commits_for_symptom: list = []
829
836
  try:
830
837
  from sourcecode.git_analyzer import GitAnalyzer
831
838
  _gc = GitAnalyzer().analyze(self.root, depth=30, days=90)
832
839
  _bad = {"no_git_repo", "git_not_found", "git_timeout"}
833
840
  if _gc and not (_bad & set(_gc.limitations)):
834
841
  git_hotspots = {h.file: h.commit_count for h in _gc.change_hotspots}
842
+ _recent_commits_for_symptom = list(_gc.recent_commits)
835
843
  if _gc.uncommitted_changes:
836
844
  _uc = _gc.uncommitted_changes
837
845
  uncommitted_files = set(_uc.staged) | set(_uc.unstaged)
@@ -1208,19 +1216,52 @@ class TaskContextBuilder:
1208
1216
  if len(w) > 2
1209
1217
  ]
1210
1218
  if symptom_keywords:
1211
- # Surface code notes whose text contains any keyword
1219
+ # Pass 1: surface code notes whose text contains any keyword
1220
+ # Also track which file paths have matching notes (for score boost below).
1221
+ _note_matched_paths: dict[str, int] = {} # path → count of matching notes
1212
1222
  for _n in cn_notes_for_ranking:
1213
1223
  _text = (getattr(_n, "text", "") or "").lower()
1214
1224
  if any(kw in _text for kw in symptom_keywords):
1225
+ _np = getattr(_n, "path", "")
1215
1226
  related_notes.append({
1216
1227
  "kind": getattr(_n, "kind", ""),
1217
- "path": getattr(_n, "path", ""),
1228
+ "path": _np,
1218
1229
  "line": getattr(_n, "line", None),
1219
1230
  "text": getattr(_n, "text", ""),
1220
1231
  })
1221
- # Secondary pass: inject files whose path matches symptom keywords
1222
- # but weren't in the candidate pool (no structural/git signals).
1232
+ _note_matched_paths[_np] = _note_matched_paths.get(_np, 0) + 1
1233
+
1234
+ # Pass 2: build commit message index — files touched in commits whose
1235
+ # message matches a symptom keyword get a strong recency signal.
1236
+ # This is the primary signal for functional keywords like "sesiones"
1237
+ # that don't appear in file paths but do appear in commit messages.
1238
+ _commit_file_hits: dict[str, int] = {} # path → n matching commits
1239
+ for _cr in _recent_commits_for_symptom:
1240
+ _msg_lower = (_cr.message or "").lower()
1241
+ if any(kw in _msg_lower for kw in symptom_keywords):
1242
+ for _cf in (_cr.files_changed or []):
1243
+ _cf_norm = _cf.replace("\\", "/")
1244
+ _commit_file_hits[_cf_norm] = _commit_file_hits.get(_cf_norm, 0) + 1
1245
+
1246
+ # Pass 3: inject files from commit index not yet in candidate pool
1223
1247
  _existing_paths = {rf.path for rf in relevant_files}
1248
+ for _cp, _nhits in _commit_file_hits.items():
1249
+ if _cp in _existing_paths:
1250
+ continue
1251
+ if Path(_cp).suffix.lower() not in _ALL_EXTENSIONS:
1252
+ continue
1253
+ _ci_score = round(min(0.5 + 0.15 * _nhits, 0.85), 2)
1254
+ relevant_files.append(RelevantFile(
1255
+ path=_cp,
1256
+ role="symptom_match",
1257
+ score=_ci_score,
1258
+ reason=f"commit message matches symptom ({_nhits} commit{'s' if _nhits > 1 else ''})",
1259
+ why=f"symptom commit-index: {', '.join(symptom_keywords)}",
1260
+ ))
1261
+ _existing_paths.add(_cp)
1262
+
1263
+ # Pass 4: inject files whose path matches symptom keywords
1264
+ # but weren't in the candidate pool (no structural/git signals).
1224
1265
  for _p in all_paths:
1225
1266
  if _p in _existing_paths:
1226
1267
  continue
@@ -1242,17 +1283,38 @@ class TaskContextBuilder:
1242
1283
  ))
1243
1284
  _existing_paths.add(_p)
1244
1285
 
1245
- # Re-rank all relevant_files: boost files whose path matches keywords
1246
- def _symptom_score(rf: "RelevantFile") -> float:
1247
- path_lower = rf.path.lower()
1248
- return rf.score + 0.2 * sum(1.0 for kw in symptom_keywords if kw in path_lower)
1249
- relevant_files = sorted(relevant_files, key=lambda rf: -_symptom_score(rf))
1250
-
1251
- # Content scan boost: read file body for symptom keywords
1286
+ # Pass 5: multi-signal boost apply commit, note, content, and path
1287
+ # signals in one pass to avoid redundant file reads.
1252
1288
  _src_exts = frozenset({".java", ".py", ".ts", ".js", ".kt", ".go"})
1253
- _content_boosted: list[RelevantFile] = []
1289
+ _boosted: list[RelevantFile] = []
1254
1290
  for _rf in relevant_files:
1255
1291
  _extra = 0.0
1292
+ _reasons: list[str] = []
1293
+ _p_lower = _rf.path.lower()
1294
+
1295
+ # Commit message boost: +0.25/commit, cap +0.40
1296
+ _c_hits = _commit_file_hits.get(_rf.path, 0)
1297
+ if _c_hits:
1298
+ _cb = min(0.40, _c_hits * 0.25)
1299
+ _extra += _cb
1300
+ _reasons.append(f"commit-msg symptom ×{_c_hits} (+{_cb:.2f})")
1301
+
1302
+ # Code note boost: +0.20/note, cap +0.30
1303
+ _n_hits = _note_matched_paths.get(_rf.path, 0)
1304
+ if _n_hits:
1305
+ _nb = min(0.30, _n_hits * 0.20)
1306
+ _extra += _nb
1307
+ _reasons.append(f"note-match symptom ×{_n_hits} (+{_nb:.2f})")
1308
+
1309
+ # Path keyword boost: +0.20/keyword already in score for injected
1310
+ # files; re-apply for pre-existing candidates whose path matches.
1311
+ _path_kws = [kw for kw in symptom_keywords if kw in _p_lower]
1312
+ if _path_kws and _rf.role != "symptom_match":
1313
+ _pb = 0.20 * len(_path_kws)
1314
+ _extra += _pb
1315
+ _reasons.append(f"path-kw symptom ({', '.join(_path_kws)}) (+{_pb:.2f})")
1316
+
1317
+ # Content scan boost: +0.05/hit, cap +0.50 (was +0.02, cap +0.30)
1256
1318
  if Path(_rf.path).suffix.lower() in _src_exts:
1257
1319
  try:
1258
1320
  _lines = (self.root / _rf.path).read_text(
@@ -1260,26 +1322,33 @@ class TaskContextBuilder:
1260
1322
  ).splitlines()[:300]
1261
1323
  _body = "\n".join(_lines).lower()
1262
1324
  _hits = sum(_body.count(kw) for kw in symptom_keywords)
1263
- _extra = min(0.30, _hits * 0.02)
1325
+ _content_b = min(0.50, _hits * 0.05)
1326
+ if _content_b > 0:
1327
+ _extra += _content_b
1328
+ _reasons.append(f"content-match symptom ×{_hits} (+{_content_b:.2f})")
1264
1329
  except OSError:
1265
1330
  pass
1266
- _content_boosted.append(RelevantFile(
1331
+
1332
+ _new_reason = _rf.reason
1333
+ if _reasons:
1334
+ _new_reason = _rf.reason + ", " + ", ".join(_reasons)
1335
+ _boosted.append(RelevantFile(
1267
1336
  path=_rf.path,
1268
1337
  role=_rf.role,
1269
1338
  score=round(min(_rf.score + _extra, 1.0), 2),
1270
- reason=_rf.reason + (f", content-match symptom (+{_extra:.2f})" if _extra > 0 else ""),
1339
+ reason=_new_reason,
1271
1340
  why=_rf.why,
1272
1341
  ))
1273
- relevant_files = sorted(_content_boosted, key=lambda rf: -rf.score)
1342
+ relevant_files = sorted(_boosted, key=lambda rf: -rf.score)
1274
1343
 
1275
- # Cross-layer synonym boost: frontend keywords → backend equivalents
1344
+ # Pass 6: cross-layer synonym boost frontend keywords → backend equivalents
1276
1345
  _synonym_note: Optional[str] = None
1277
1346
  _frontend_kws = [kw for kw in symptom_keywords if kw in _FRONTEND_SYMPTOM_MAP]
1278
1347
  if _frontend_kws:
1279
1348
  _backend_terms: list[str] = []
1280
1349
  for _fkw in _frontend_kws:
1281
1350
  _backend_terms.extend(_FRONTEND_SYMPTOM_MAP[_fkw])
1282
- _backend_terms_set = list(dict.fromkeys(_backend_terms)) # dedup, preserve order
1351
+ _backend_terms_set = list(dict.fromkeys(_backend_terms))
1283
1352
  _synonym_boosted: list[RelevantFile] = []
1284
1353
  for _rf in relevant_files:
1285
1354
  _extra_syn = 0.0
@@ -997,6 +997,8 @@ def _get_git_old_content(git_root: Path, rel_path: str, since: str) -> Optional[
997
997
  cwd=str(git_root),
998
998
  capture_output=True,
999
999
  text=True,
1000
+ encoding="utf-8",
1001
+ errors="replace",
1000
1002
  timeout=5,
1001
1003
  )
1002
1004
  if result.returncode == 0:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.30.17
3
+ Version: 1.30.18
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.17-blue)
224
+ ![Version](https://img.shields.io/badge/version-1.30.18-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.17
260
+ # sourcecode 1.30.18
261
261
  ```
262
262
 
263
263
  ---
@@ -1,4 +1,4 @@
1
- sourcecode/__init__.py,sha256=C0dqdKsiSL2QvbpNXAjIuRvmS92c_QLcSsSuvX42hRE,104
1
+ sourcecode/__init__.py,sha256=6mHUCAZLGN6amZwcIEqBuvLyiGaJroesk9TR_32ZpWo,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
@@ -10,7 +10,7 @@ sourcecode/confidence_analyzer.py,sha256=xw_Jv8pAd0wd8t2vvQlorw8Ih0rSF3YCoFS8K-_
10
10
  sourcecode/context_scorer.py,sha256=QpChSpsmaAYz91rXA4Ue5xzQmNz_ZboZN09YOHScq1U,14679
11
11
  sourcecode/context_summarizer.py,sha256=CiQrfBEzun949bWvmLabWoj2HhPn6Lw62ofqnsy0FlQ,6503
12
12
  sourcecode/contract_model.py,sha256=nRxJKPMs1VHwFTa8AVXhGmaLjti3Lr2sjHDpWgv1bfE,3917
13
- sourcecode/contract_pipeline.py,sha256=VEOvmj-emKuUT-GUosde9FRYyeH8szNW1WSnisNSs2o,27592
13
+ sourcecode/contract_pipeline.py,sha256=w18t_MdbrkIeLcCW-VMQYeb9hlWenAOB-NMiME_Fo-Y,27652
14
14
  sourcecode/coverage_parser.py,sha256=q0LeZJaX1bnntLu-ImksdBsMlpsVmk_iUfSaB4eaJGo,19702
15
15
  sourcecode/dependency_analyzer.py,sha256=p4ljXhkcGBbFlhaZuPrsjOVjDXaKLTg0Gor2p4qFPP0,56208
16
16
  sourcecode/doc_analyzer.py,sha256=afA4uJFwXZ_uR2l4J0pQwbeTkRkGmKdN9KhRVYePBUw,24331
@@ -22,13 +22,13 @@ sourcecode/git_analyzer.py,sha256=_pCg2V4d2aa17k9hayTzpexAj8syvyk4y9NYNvvgOAI,12
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=pStv1z7bO0_p7YmUTH6NSnc7xjmkNrA9Atdvp6y8UiY,152165
25
+ sourcecode/prepare_context.py,sha256=W4cK5EESAQWyLKWobuzt3X9Ca7kZp1ui8efD0bgTzEE,156227
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
29
29
  sourcecode/relevance_scorer.py,sha256=MYF4FFkveAQps9SmTeTlh6ODiBz2F--_hWNeHMLtUHQ,8405
30
30
  sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
31
- sourcecode/repository_ir.py,sha256=uwi6-mJPh1E2-RYjf-ndqUXTvN32-7Nfv6oFqwHQi1c,57566
31
+ sourcecode/repository_ir.py,sha256=KMB_5sPtV695CaVzH-dOl9j9_zvP45PjzGZJiZzrdAM,57626
32
32
  sourcecode/runtime_classifier.py,sha256=zWX3r3HCKHc-qtIobErOa8aKMmaoPYREtJKvPcBGPjQ,14792
33
33
  sourcecode/scanner.py,sha256=aM3h9-DCQ3xKpeHpHYdo2vX6T5P95HA_YwZbkAVNwmo,8288
34
34
  sourcecode/schema.py,sha256=fj3BZ3IcnNV4j21BFIEvz8Qnw_vZoqIbzzRg-qQ-nd0,24530
@@ -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.17.dist-info/METADATA,sha256=4flsXx7vygTH8S2_G_Dfq1CocZivzyUNQFLBNxY90ZY,28956
68
- sourcecode-1.30.17.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
69
- sourcecode-1.30.17.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
70
- sourcecode-1.30.17.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
71
- sourcecode-1.30.17.dist-info/RECORD,,
67
+ sourcecode-1.30.18.dist-info/METADATA,sha256=lqlAi_PtaPLAr3s8RUw0ULhaKZioUxzdNsVBKKVfRqc,28956
68
+ sourcecode-1.30.18.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
69
+ sourcecode-1.30.18.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
70
+ sourcecode-1.30.18.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
71
+ sourcecode-1.30.18.dist-info/RECORD,,