sourcecode 3.7.0__py3-none-any.whl → 4.0.0__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.
Files changed (48) hide show
  1. sourcecode/__init__.py +1 -1
  2. sourcecode/archetype.py +43 -16
  3. sourcecode/cache.py +48 -0
  4. sourcecode/cache_model.py +17 -7
  5. sourcecode/cli.py +562 -127
  6. sourcecode/confidence_analyzer.py +7 -2
  7. sourcecode/contract_init.py +419 -0
  8. sourcecode/defect_identity.py +166 -0
  9. sourcecode/degradation.py +52 -0
  10. sourcecode/detectors/java.py +15 -31
  11. sourcecode/environment_resolution.py +361 -0
  12. sourcecode/filter_surface.py +243 -12
  13. sourcecode/git_checkout.py +149 -0
  14. sourcecode/identity_fallback.py +244 -0
  15. sourcecode/migrate_check.py +79 -7
  16. sourcecode/non_coverage.py +198 -0
  17. sourcecode/parse_cache.py +23 -0
  18. sourcecode/posture.py +201 -6
  19. sourcecode/pr_comment_renderer.py +13 -0
  20. sourcecode/prepare_context.py +110 -3
  21. sourcecode/remedies.py +87 -0
  22. sourcecode/repository_ir.py +43 -4
  23. sourcecode/retrieval/result.py +24 -9
  24. sourcecode/retrieval/steps.py +3 -0
  25. sourcecode/retrieval/steps_endpoint.py +5 -5
  26. sourcecode/retrieval/steps_graph.py +5 -5
  27. sourcecode/retrieval/steps_impact.py +19 -7
  28. sourcecode/retrieval/steps_intf.py +4 -4
  29. sourcecode/retrieval/steps_struct.py +5 -5
  30. sourcecode/retrieval/steps_txsec.py +5 -5
  31. sourcecode/risk.py +345 -0
  32. sourcecode/security_config_scan.py +262 -0
  33. sourcecode/security_posture.py +216 -23
  34. sourcecode/serializer.py +39 -5
  35. sourcecode/spring_findings.py +67 -4
  36. sourcecode/spring_profiles.py +199 -3
  37. sourcecode/spring_properties.py +1 -1
  38. sourcecode/spring_security_audit.py +88 -0
  39. sourcecode/spring_tx_analyzer.py +52 -0
  40. sourcecode/summarizer.py +7 -2
  41. sourcecode/test_gap_ranking.py +363 -0
  42. sourcecode/token_estimate.py +36 -0
  43. sourcecode/verify_repo.py +8 -0
  44. {sourcecode-3.7.0.dist-info → sourcecode-4.0.0.dist-info}/METADATA +34 -4
  45. {sourcecode-3.7.0.dist-info → sourcecode-4.0.0.dist-info}/RECORD +48 -38
  46. {sourcecode-3.7.0.dist-info → sourcecode-4.0.0.dist-info}/WHEEL +0 -0
  47. {sourcecode-3.7.0.dist-info → sourcecode-4.0.0.dist-info}/entry_points.txt +0 -0
  48. {sourcecode-3.7.0.dist-info → sourcecode-4.0.0.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py CHANGED
@@ -4,4 +4,4 @@ ASK Engine is the product. ``ask`` is the canonical CLI command; ``sourcecode``
4
4
  the legacy compatibility alias and the Python/PyPI package name. See
5
5
  docs/PRODUCT_IDENTITY.md (normative)."""
6
6
 
7
- __version__ = "3.7.0"
7
+ __version__ = "4.0.0"
sourcecode/archetype.py CHANGED
@@ -175,8 +175,13 @@ class _Features:
175
175
  module_count: int
176
176
  concept_mass: dict[str, float] # concept → fraction of source files
177
177
  entry_kinds: dict[str, int]
178
- endpoint_total: int
179
- endpoint_share: float # endpoints per source file
178
+ # `None` when the endpoint surface was NOT measured (no root to read, or the
179
+ # extraction failed). It used to be 0, which is a different claim: a score
180
+ # that reads "no HTTP surface" off an unknown one refutes `api-first` and
181
+ # actively supports `worker`, so an unmeasured repository was classified as
182
+ # if it exposed nothing (I-3/R9, inside a score).
183
+ endpoint_total: "Optional[int]"
184
+ endpoint_share: "Optional[float]" # endpoints per source file
180
185
  packaging: Optional[str]
181
186
  frameworks: set[str]
182
187
  graph_hubs: list[str]
@@ -250,7 +255,11 @@ class ArchetypeClassifier:
250
255
  signals_missing=f.signals_missing,
251
256
  generated_from=(
252
257
  f"{f.total_files} source files, {f.module_count} modules, "
253
- f"{f.endpoint_total} endpoints"
258
+ + (
259
+ f"{f.endpoint_total} endpoints"
260
+ if f.endpoint_total is not None
261
+ else "endpoint surface not measured"
262
+ )
254
263
  ),
255
264
  graph_metrics=f.graph_metrics,
256
265
  )
@@ -299,8 +308,10 @@ class ArchetypeClassifier:
299
308
  missing.append("entry_points")
300
309
 
301
310
  endpoint_total = self._endpoint_count(root)
302
- endpoint_share = (endpoint_total / total) if total else 0.0
303
- if root is not None:
311
+ endpoint_share = (
312
+ (endpoint_total / total) if (endpoint_total is not None and total) else None
313
+ )
314
+ if endpoint_total is not None:
304
315
  used.append("endpoints")
305
316
  else:
306
317
  missing.append("endpoints")
@@ -519,21 +530,31 @@ class ArchetypeClassifier:
519
530
  # api-first: HTTP surface must be significant relative to size. Ramp so a
520
531
  # rounding-error surface cannot win: 0 below 0.5 endpoints/100 files, linear
521
532
  # to full strength at >=2/100 files. (neo4j: 18/5600 = 0.32/100 -> 0.)
522
- density = f.endpoint_share * 100
523
- api_strength = max(0.0, min(1.0, (density - 0.5) / 1.5)) if f.endpoint_total else 0.0
524
- add("api-first", "endpoint_density",
525
- f"{f.endpoint_total} endpoints over {f.total_files} files "
526
- f"({density:.2f}/100 files)",
527
- 3.0, api_strength, 1.0)
533
+ if f.endpoint_share is None:
534
+ # Unmeasured, not zero: contribute nothing and say which axis is blind.
535
+ add("api-first", "endpoint_density",
536
+ "endpoint surface not measured — this dimension is scored without it",
537
+ 3.0, 0.0, 0.0)
538
+ else:
539
+ density = f.endpoint_share * 100
540
+ api_strength = max(0.0, min(1.0, (density - 0.5) / 1.5)) if f.endpoint_total else 0.0
541
+ add("api-first", "endpoint_density",
542
+ f"{f.endpoint_total} endpoints over {f.total_files} files "
543
+ f"({density:.2f}/100 files)",
544
+ 3.0, api_strength, 1.0)
528
545
  cli_entries = f.entry_kinds.get("cli", 0)
529
546
  add("cli", "cli_entry_points", f"{cli_entries} CLI entry point(s); cli mass ≈ {cm['cli']:.0%}",
530
547
  2.5, 1.0 if cli_entries else (1.0 if cm["cli"] > 0.05 else 0.0), max(cm["cli"], 0.2))
531
548
  add("daemon", "server_bootstrap",
532
549
  f"server/bootstrap entries: {f.entry_kinds.get('server',0)+f.entry_kinds.get('bootstrap',0)}",
533
550
  2.0, 1.0 if ("server" in f.entry_kinds or "bootstrap" in f.entry_kinds) else 0.0, 0.5)
551
+ # `event_no_http` needs the absence of HTTP as a MEASURED fact: firing it
552
+ # on an unmeasured surface is how an unknown became evidence for `worker`.
553
+ _no_http_known = f.endpoint_total == 0
534
554
  add("worker", "event_no_http",
535
- f"event mass ≈ {cm['event']:.0%}, endpoints={f.endpoint_total}",
536
- 1.5, 1.0 if (cm["event"] > 0.05 and f.endpoint_total == 0) else 0.0, max(cm["event"], 0.1))
555
+ f"event mass ≈ {cm['event']:.0%}, endpoints="
556
+ + (str(f.endpoint_total) if f.endpoint_total is not None else "not measured"),
557
+ 1.5, 1.0 if (cm["event"] > 0.05 and _no_http_known) else 0.0, max(cm["event"], 0.1))
537
558
  lib_strength = 1.0 if (f.packaging == "jar" and not f.entry_kinds) else 0.3
538
559
  add("library", "consumed_as_dependency",
539
560
  f"packaging={f.packaging}, entry kinds={list(f.entry_kinds) or 'none'}",
@@ -666,12 +687,18 @@ class ArchetypeClassifier:
666
687
  return head if tail else ""
667
688
 
668
689
  @staticmethod
669
- def _endpoint_count(root: Optional[Path]) -> int:
690
+ def _endpoint_count(root: Optional[Path]) -> "Optional[int]":
691
+ """The measured endpoint count, or None when it was not measured.
692
+
693
+ Both failure modes — no root to read, and an extraction that raised —
694
+ return None rather than 0, because every consumer below treats 0 as the
695
+ measured statement "this repository exposes no HTTP surface".
696
+ """
670
697
  if root is None:
671
- return 0
698
+ return None
672
699
  try:
673
700
  from sourcecode.repository_ir import extract_java_endpoints
674
701
  data = extract_java_endpoints(root)
675
702
  return int(data.get("total", len(data.get("endpoints", []))))
676
703
  except Exception:
677
- return 0
704
+ return None
sourcecode/cache.py CHANGED
@@ -354,6 +354,7 @@ def status(repo_root: Path) -> dict[str, Any]:
354
354
  "cores": 0, "snapshots": 0, "views": 0, "cas_blobs": 0,
355
355
  "total_size_bytes": 0, "total_size_mb": 0.0,
356
356
  "current_git_head": current_head,
357
+ "stores": _store_breakdown(repo_root, 0),
357
358
  **ris_fields,
358
359
  }
359
360
  cores = list(cache_d.glob("core-*.json.gz"))
@@ -371,10 +372,57 @@ def status(repo_root: Path) -> dict[str, Any]:
371
372
  "total_size_bytes": total_bytes,
372
373
  "total_size_mb": round(total_bytes / (1024 * 1024), 2),
373
374
  "current_git_head": current_head,
375
+ # C3-36: `CAS blobs: 0`, `Total size: 0.1 MB` immediately after an 89-second
376
+ # warm of 3 342 files. Every figure above was true and described one store
377
+ # of three — the warm's output mostly lands in the shared CIR and the parse
378
+ # cache, which this command never counted. A status that reports a third of
379
+ # the state reads as a warm that did nothing.
380
+ "stores": _store_breakdown(repo_root, total_bytes),
374
381
  **ris_fields,
375
382
  }
376
383
 
377
384
 
385
+ def _store_breakdown(repo_root: Path, core_bytes: int) -> "dict[str, Any]":
386
+ """What a warm actually populated, by store. Best-effort per store: a store
387
+ that cannot be inspected is reported as unavailable, never as empty."""
388
+ out: "dict[str, Any]" = {
389
+ "core": {
390
+ "cache_dir": str(cache_dir(repo_root)),
391
+ "bytes": core_bytes,
392
+ "scope": "this repository",
393
+ "holds": "core snapshots, rendered views and their CAS blobs",
394
+ },
395
+ }
396
+ try:
397
+ from sourcecode.context_cache import ContextCache # noqa: PLC0415
398
+
399
+ ctx = ContextCache.for_repo(repo_root).stats()
400
+ out["shared_cir"] = {
401
+ "cache_dir": ctx["cache_dir"],
402
+ "entries": ctx["contexts"],
403
+ "bytes": ctx["bytes_stored"],
404
+ "scope": "this repository",
405
+ "holds": "the shared Canonical IR that explain/impact/posture reuse",
406
+ }
407
+ except Exception:
408
+ out["shared_cir"] = {"available": False}
409
+ try:
410
+ from sourcecode import parse_cache as _pc # noqa: PLC0415
411
+
412
+ out["parse"] = {
413
+ **_pc.store_stats(),
414
+ "holds": "per-file parses, content-addressed across every repository",
415
+ }
416
+ except Exception:
417
+ out["parse"] = {"available": False}
418
+ out["note"] = (
419
+ "`cache warm` populates all of these; the core store alone is a third of "
420
+ "the answer, and reading it as the whole is how a completed warm looks like "
421
+ "an empty cache."
422
+ )
423
+ return out
424
+
425
+
378
426
  def clear(repo_root: Path, *, clear_ris: bool = False) -> int:
379
427
  """Delete cache files for *repo_root*. Returns the number of files removed.
380
428
 
sourcecode/cache_model.py CHANGED
@@ -124,16 +124,25 @@ COMMANDS: tuple[CommandCache, ...] = (
124
124
  "`--compact` is what a warm stores by default; `--agent` needs `cache warm --agent`. "
125
125
  "`--env-map`, `--depth N` and `--exclude` change the *analysis*, so they miss the "
126
126
  "warmed core and rescan — this is the 171 s the field measured after a 103 s warm.",
127
- "--compact 17.7 s → 0.3 s; --agent --full --env-map --depth 20 34.7 s → 33.9 s (no gain)"),
127
+ "--compact 13.3 s cold → 0.3 s warm (cold re-measured on 3.7.0: was 19.3 s, C3-6); "
128
+ "--agent --full --env-map --depth 20 34.7 s → 33.9 s (no gain)"),
128
129
  CommandCache("posture", ("cir", "parse"), "shared", False,
129
130
  "Resolves the conditional bean graph on every run, over the shared CIR a warm "
130
131
  "builds — the parse it used to repeat for itself. `--diff` compares two profile "
131
132
  "sets over that one IR, so the second side costs the resolution only.",
132
133
  "10.1 s → 1.6 s"),
133
- CommandCache("endpoints", ("ris", "parse"), "none", False,
134
- "Recomputes the endpoint surface on every run and refreshes the RIS endpoint index. "
135
- "Measured: a warm buys it nothing.",
136
- "2.8 s → 2.9 s"),
134
+ CommandCache("risk", ("cir", "parse"), "shared", False,
135
+ "Composes what the audit, impact-chain and the posture already answer, so it "
136
+ "pays each of their costs once over the shared CIR a warm builds — one parse "
137
+ "for the whole composition, and the reachability query is cached per symbol "
138
+ "within the run.",
139
+ "not measured on the battery yet — the composition is bounded by the "
140
+ "`spring-audit` + `impact-chain` costs listed here, not by new analysis"),
141
+ CommandCache("endpoints", ("ris", "parse"), "shared", False,
142
+ "Recomputes the endpoint surface on every run, over a parse a warm has already "
143
+ "paid for. Until 3.7.0 the extractor parsed every file itself instead of reading "
144
+ "the shared parse cache, and a warm measurably bought it nothing (C3-6).",
145
+ "3.3 s → 1.4 s (re-measured on 3.7.0; was 2.8 s → 2.9 s)"),
137
146
  CommandCache("spring-audit", ("ris", "parse"), "shared", False,
138
147
  "Recomputes every run, but over a parse a warm has already paid for.",
139
148
  "8.8 s → 3.7 s"),
@@ -158,8 +167,8 @@ COMMANDS: tuple[CommandCache, ...] = (
158
167
  "1.1 s → 2.3 s (slower)"),
159
168
  CommandCache("plan", ("parse",), "shared", False, "", "9.3 s → 3.8 s"),
160
169
  CommandCache("compare", ("parse",), "shared", False, ""),
161
- CommandCache("delta", (), "none", False, "Analyses two checkouts; neither is the tree the cache describes."),
162
- CommandCache("contract-diff", (), "none", False, "Analyses two checkouts."),
170
+ CommandCache("delta", (), "none", False, "Analyses two states — two checkouts, or two refs materialised into temporary trees; neither is the tree the cache describes."),
171
+ CommandCache("contract-diff", (), "none", False, "Analyses two states (checkouts or refs); neither is the tree the cache describes."),
163
172
  CommandCache("fix-bug", ("task",), "none", True,
164
173
  "Shorthand for `prepare-context fix-bug`; caches its own answer, which a warm never runs."),
165
174
  CommandCache("rename-class", (), "none", False, ""),
@@ -183,6 +192,7 @@ COMMANDS: tuple[CommandCache, ...] = (
183
192
  "Reads the RIS a warm rebuilds — that is all it does. Without one it answers "
184
193
  "`no_ris` instead of a snapshot.",
185
194
  "0.2 s either way"),
195
+ CommandCache("trend", (), "none", False, "Reads stored baseline artifacts from disk; analyses no source, so no cache layer applies. Same command as `baseline trend`."),
186
196
  CommandCache("baseline", ("parse",), "shared", False, "`capture`/`diff`/`trend` over architectural metrics.",
187
197
  "capture 8.7 s → 3.6 s"),
188
198
  CommandCache("retrieve", ("cir", "parse"), "shared", False,