sourcecode 4.9.0__py3-none-any.whl → 4.9.1__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.

Potentially problematic release.


This version of sourcecode might be problematic. Click here for more details.

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__ = "4.9.0"
7
+ __version__ = "4.9.1"
@@ -78,6 +78,17 @@ def build_audit_report(
78
78
  "non_coverage": (risk.get("non_coverage") or {}).get("items", []),
79
79
  },
80
80
  }
81
+ from sourcecode.provenance import build_evidence_manifest
82
+ unsigned["evidence_manifest"] = build_evidence_manifest(
83
+ unsigned,
84
+ command="audit-report",
85
+ root=root,
86
+ inputs={
87
+ "profiles": sorted(profiles) if profiles else None,
88
+ "risk_limit": risk_limit,
89
+ "signed": bool(sign_key),
90
+ },
91
+ )
81
92
  # Sign and publish the same JSON-normalized material. Some embedded evidence
82
93
  # may contain tuples or other JSON-coercible values; normalizing before signing
83
94
  # avoids a signature over an in-memory shape the user never receives.
sourcecode/cli.py CHANGED
@@ -345,7 +345,7 @@ of files) in minutes. Semantic analysis itself is sub-second; repo indexing domi
345
345
  posture . --diff dev:prod [dim]# effective access, two profile sets (exp.)[/dim]
346
346
  endpoints . [dim]# endpoints + effective path + policy[/dim]
347
347
  spring-audit . [dim]# TX anomalies + security surface[/dim]
348
- migrate-check . --compact [dim]# Boot 2→3: located blockers + effort[/dim]
348
+ migrate-check . --compact [dim]# Boot 2→3 + Java LTS/licensing inventory[/dim]
349
349
  risk . [dim]# defects ranked by reach × access (exp.)[/dim]
350
350
 
351
351
  {_remedy_help_block()}
@@ -3875,6 +3875,11 @@ def prepare_context_cmd(
3875
3875
  ask prepare-context review-pr . --since main --output review.json
3876
3876
  ask prepare-context onboard --llm-prompt
3877
3877
  ask prepare-context --task-help
3878
+
3879
+ \b
3880
+ Provenance:
3881
+ JSON output includes evidence_manifest: claim IDs for context files and
3882
+ source-backed rows, plus command inputs, ASK version, git HEAD and freshness.
3878
3883
  """
3879
3884
  from sourcecode.prepare_context import TASKS, TaskContextBuilder
3880
3885
 
@@ -4419,6 +4424,20 @@ def prepare_context_cmd(
4419
4424
  _skipped.append("test_gap_discovery")
4420
4425
  out["skipped_analyzers"] = _skipped
4421
4426
 
4427
+ from sourcecode.provenance import build_evidence_manifest as _build_evidence_manifest
4428
+ out["evidence_manifest"] = _build_evidence_manifest(
4429
+ out,
4430
+ command="prepare-context",
4431
+ root=target,
4432
+ inputs={
4433
+ "task": task,
4434
+ "since": since,
4435
+ "format": format or "json",
4436
+ "fast": fast,
4437
+ "llm_prompt": llm_prompt,
4438
+ },
4439
+ )
4440
+
4422
4441
  # P0-1: Apply output budget per task — safety net for large repos.
4423
4442
  _pc_budget = _prepare_context_budget(task)
4424
4443
 
@@ -8015,7 +8034,8 @@ def audit_report_cmd(
8015
8034
  This packages the same evidence `risk` and `posture` already publish into a
8016
8035
  report a buyer can read: top risks, effective runtime posture, source-linked
8017
8036
  evidence and non-coverage. It does not certify compliance, choose fixes or
8018
- assign ROI.
8037
+ assign ROI. JSON includes evidence_manifest: claim IDs mapped to source spans,
8038
+ command inputs, ASK version and freshness.
8019
8039
 
8020
8040
  \b
8021
8041
  `--sign-key` adds an HMAC-SHA256 signature over the canonical JSON payload.
@@ -8724,9 +8744,9 @@ def migrate_check_cmd(
8724
8744
  None,
8725
8745
  "--target-jdk",
8726
8746
  help=(
8727
- "The JDK you are actually moving to (e.g. 11, 17, 21). Rules for a "
8728
- "later JDK are not your blockers and are not reported. Without it, "
8729
- "every JDK axis is reported at once."
8747
+ "The JDK you are actually moving to (e.g. 11, 17, 21, 25). Rules "
8748
+ "for a later JDK are not your blockers and are not reported. Also "
8749
+ "sets java_lts_inventory.target_lts."
8730
8750
  ),
8731
8751
  ),
8732
8752
  keep_boot: bool = typer.Option(
@@ -8786,7 +8806,7 @@ def migrate_check_cmd(
8786
8806
  help="Caller BFS depth for --blast-radius (1-8, default: 4).",
8787
8807
  ),
8788
8808
  ) -> None:
8789
- """Spring Boot 2→3 migration readiness: detect javax→jakarta namespace blockers.
8809
+ """Spring Boot 2→3 migration readiness + Java LTS/licensing inventory.
8790
8810
 
8791
8811
  \b
8792
8812
  Detects:
@@ -8815,6 +8835,7 @@ def migrate_check_cmd(
8815
8835
  ask migrate-check . --compact bounded decision summary
8816
8836
  ask migrate-check /path/to/repo --format text
8817
8837
  ask migrate-check . --min-severity high
8838
+ ask migrate-check . --target-jdk 25 Java LTS target inventory
8818
8839
  ask migrate-check . --table --rule MIG-001 --band critical --top-n 20
8819
8840
  ask migrate-check . --output migration.json
8820
8841
  ask migrate-check . --snapshot --ref sprint-12 persist a readiness point
@@ -8837,6 +8858,13 @@ def migrate_check_cmd(
8837
8858
  <repo>/.ask/readiness-history). --trend reads that series and reports
8838
8859
  first→last movement — no improving/degrading label, and readiness_score is
8839
8860
  flagged not-comparable when the applicable dimension set changed.
8861
+
8862
+ \b
8863
+ Java LTS/licensing inventory:
8864
+ The JSON report includes java_lts_inventory: Java 8/11/17/21/25 evidence
8865
+ from Maven/Gradle, local runtime hints and container files; target-LTS
8866
+ blockers; and explicit Oracle JDK/licensing-review signals. This is
8867
+ inventory evidence, not legal advice, and it never changes readiness_score.
8840
8868
  """
8841
8869
  from sourcecode.repository_ir import find_java_files
8842
8870
  from sourcecode.migrate_check import run_migrate_check
@@ -860,6 +860,282 @@ def _parse_major(version: "Optional[str]") -> "Optional[int]":
860
860
  return None
861
861
  m = re.match(r"\s*(\d+)", version)
862
862
  return int(m.group(1)) if m else None
863
+
864
+
865
+ _MAVEN_JAVA_VERSION_RE: re.Pattern = re.compile(
866
+ r"<(java\.version|maven\.compiler\.(?:source|target|release))>\s*([^<${}]+)\s*</\1>",
867
+ re.IGNORECASE,
868
+ )
869
+ _MAVEN_COMPILER_TAG_RE: re.Pattern = re.compile(
870
+ r"<(?:source|target|release)>\s*([^<${}]+)\s*</(?:source|target|release)>",
871
+ re.IGNORECASE,
872
+ )
873
+ _GRADLE_JAVA_ENUM_RE: re.Pattern = re.compile(
874
+ r"JavaVersion\.VERSION_(?:1_)?(\d+)", re.IGNORECASE
875
+ )
876
+ _GRADLE_JAVA_ASSIGN_RE: re.Pattern = re.compile(
877
+ r"(?:sourceCompatibility|targetCompatibility|javaVersion|languageVersion)\s*=?\s*['\"]?(\d+(?:\.\d+)?)",
878
+ re.IGNORECASE,
879
+ )
880
+ _GRADLE_TOOLCHAIN_RE: re.Pattern = re.compile(
881
+ r"JavaLanguageVersion\.of\s*\(\s*(\d+)\s*\)", re.IGNORECASE
882
+ )
883
+ _JAVA_VERSION_LINE_RE: re.Pattern = re.compile(
884
+ r"(?:JAVA_VERSION|JDK_VERSION|java\.version)\s*[=:]\s*['\"]?(\d+(?:\.\d+)?)",
885
+ re.IGNORECASE,
886
+ )
887
+ _DOCKER_FROM_RE: re.Pattern = re.compile(
888
+ r"^\s*FROM\s+(?:--platform=\S+\s+)?(\S+)", re.IGNORECASE | re.MULTILINE
889
+ )
890
+ _DOCKER_JAVA_TAG_RE: re.Pattern = re.compile(
891
+ r"(?:^|[:\-_])(?:jdk|jre|java)?(?:-)?(1\.8|8|11|17|21|25)(?:[.\-_a-z0-9]*|$)",
892
+ re.IGNORECASE,
893
+ )
894
+ _JAVA_LTS_RELEASES: frozenset[int] = frozenset({8, 11, 17, 21, 25})
895
+ _JAVA_LTS_SUPPORT_FILE_NAMES: frozenset[str] = frozenset({
896
+ "gradle.properties", ".java-version", ".sdkmanrc", "Dockerfile",
897
+ "dockerfile", "compose.yaml", "compose.yml", "docker-compose.yaml",
898
+ "docker-compose.yml", "jvm.config",
899
+ })
900
+
901
+
902
+ def _normalize_java_major(value: str) -> Optional[int]:
903
+ """Normalize Java version spellings (`1.8`, `VERSION_17`, `21.0.2`) to major."""
904
+ value = value.strip().strip("'\"")
905
+ if value.startswith("1."):
906
+ parts = value.split(".")
907
+ if len(parts) > 1 and parts[1].isdigit():
908
+ return int(parts[1])
909
+ m = re.match(r"(\d+)", value)
910
+ if not m:
911
+ return None
912
+ return int(m.group(1))
913
+
914
+
915
+ def _java_lts_name(major: Optional[int]) -> str:
916
+ if major is None:
917
+ return "unknown"
918
+ return f"Java {major}" + (" LTS" if major in _JAVA_LTS_RELEASES else "")
919
+
920
+
921
+ def _java_distribution_from_text(text: str) -> Optional[str]:
922
+ low = text.lower()
923
+ for marker, name in (
924
+ ("eclipse-temurin", "Eclipse Temurin"),
925
+ ("adoptium", "Eclipse Temurin"),
926
+ ("amazoncorretto", "Amazon Corretto"),
927
+ ("corretto", "Amazon Corretto"),
928
+ ("liberica", "BellSoft Liberica"),
929
+ ("azul", "Azul Zulu"),
930
+ ("zulu", "Azul Zulu"),
931
+ ("graalvm", "GraalVM"),
932
+ ("oracle", "Oracle JDK"),
933
+ ("openjdk", "OpenJDK"),
934
+ ):
935
+ if marker in low:
936
+ return name
937
+ return None
938
+
939
+
940
+ def _java_evidence(
941
+ *,
942
+ source: str,
943
+ path: str,
944
+ value: str,
945
+ version: Optional[int],
946
+ distribution: Optional[str] = None,
947
+ ) -> dict:
948
+ out = {
949
+ "source": source,
950
+ "path": path,
951
+ "value": value,
952
+ "version": version,
953
+ "version_label": _java_lts_name(version),
954
+ }
955
+ if distribution:
956
+ out["distribution"] = distribution
957
+ return out
958
+
959
+
960
+ def _find_java_lts_support_files(root: Path) -> list[tuple[Path, str]]:
961
+ """Files that may declare build/runtime Java without executing the project."""
962
+ found: list[tuple[Path, str]] = []
963
+ for dirpath, dirnames, filenames in os.walk(root):
964
+ dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS or d == ".mvn"]
965
+ dp = Path(dirpath)
966
+ try:
967
+ rel_dir = dp.relative_to(root)
968
+ except ValueError:
969
+ continue
970
+ for fname in filenames:
971
+ if fname not in _JAVA_LTS_SUPPORT_FILE_NAMES:
972
+ continue
973
+ abs_path = dp / fname
974
+ rel = str(rel_dir / fname) if str(rel_dir) != "." else fname
975
+ found.append((abs_path, rel))
976
+ return found
977
+
978
+
979
+ def _java_lts_inventory(
980
+ root: Path,
981
+ build_texts: "Iterable[tuple[str, str]]",
982
+ *,
983
+ spring_boot_version: Optional[str],
984
+ target_jdk: Optional[int],
985
+ ) -> dict:
986
+ """Java LTS/licensing inventory for migration planning.
987
+
988
+ Bounded evidence only: build descriptors and local deployment hints. No build
989
+ execution, no legal verdict, and no inference from absent runtime metadata.
990
+ """
991
+ target_lts = target_jdk or 25
992
+ evidence: list[dict] = []
993
+ limitations: list[str] = []
994
+
995
+ for rel_path, raw_text in build_texts:
996
+ text = _resolve_maven_properties(raw_text) if rel_path.endswith("pom.xml") else raw_text
997
+ if rel_path.endswith("pom.xml"):
998
+ for m in _MAVEN_JAVA_VERSION_RE.finditer(text):
999
+ major = _normalize_java_major(m.group(2))
1000
+ evidence.append(_java_evidence(
1001
+ source=f"maven_property:{m.group(1)}",
1002
+ path=rel_path,
1003
+ value=m.group(2).strip(),
1004
+ version=major,
1005
+ ))
1006
+ for m in _MAVEN_COMPILER_TAG_RE.finditer(text):
1007
+ major = _normalize_java_major(m.group(1))
1008
+ evidence.append(_java_evidence(
1009
+ source="maven_compiler_plugin",
1010
+ path=rel_path,
1011
+ value=m.group(1).strip(),
1012
+ version=major,
1013
+ ))
1014
+ elif rel_path.endswith((".gradle", ".gradle.kts", "gradle.properties")):
1015
+ for pat, source in (
1016
+ (_GRADLE_JAVA_ENUM_RE, "gradle_java_version_enum"),
1017
+ (_GRADLE_JAVA_ASSIGN_RE, "gradle_java_assignment"),
1018
+ (_GRADLE_TOOLCHAIN_RE, "gradle_toolchain"),
1019
+ (_JAVA_VERSION_LINE_RE, "gradle_property"),
1020
+ ):
1021
+ for m in pat.finditer(text):
1022
+ major = _normalize_java_major(m.group(1))
1023
+ evidence.append(_java_evidence(
1024
+ source=source,
1025
+ path=rel_path,
1026
+ value=m.group(1).strip(),
1027
+ version=major,
1028
+ ))
1029
+
1030
+ for abs_path, rel_path in _find_java_lts_support_files(root):
1031
+ if rel_path.endswith(("pom.xml", "build.gradle", "build.gradle.kts")):
1032
+ continue
1033
+ try:
1034
+ text = abs_path.read_text(encoding="utf-8", errors="replace")
1035
+ except OSError:
1036
+ limitations.append(f"{rel_path}: could not be read")
1037
+ continue
1038
+ if rel_path.endswith((".java-version", ".sdkmanrc", "jvm.config", "gradle.properties")):
1039
+ for m in _JAVA_VERSION_LINE_RE.finditer(text):
1040
+ major = _normalize_java_major(m.group(1))
1041
+ evidence.append(_java_evidence(
1042
+ source="java_version_file",
1043
+ path=rel_path,
1044
+ value=m.group(1).strip(),
1045
+ version=major,
1046
+ ))
1047
+ if rel_path.endswith(".java-version"):
1048
+ major = _normalize_java_major(text.strip())
1049
+ evidence.append(_java_evidence(
1050
+ source="java_version_file",
1051
+ path=rel_path,
1052
+ value=text.strip(),
1053
+ version=major,
1054
+ ))
1055
+ if "docker" in rel_path.lower() or rel_path.lower().endswith(("compose.yml", "compose.yaml")):
1056
+ for m in _DOCKER_FROM_RE.finditer(text):
1057
+ image = m.group(1).strip()
1058
+ tag_major: Optional[int] = None
1059
+ tag_match = _DOCKER_JAVA_TAG_RE.search(image)
1060
+ if tag_match:
1061
+ tag_major = _normalize_java_major(tag_match.group(1))
1062
+ evidence.append(_java_evidence(
1063
+ source="container_base_image",
1064
+ path=rel_path,
1065
+ value=image,
1066
+ version=tag_major,
1067
+ distribution=_java_distribution_from_text(image),
1068
+ ))
1069
+ for m in _JAVA_VERSION_LINE_RE.finditer(text):
1070
+ major = _normalize_java_major(m.group(1))
1071
+ evidence.append(_java_evidence(
1072
+ source="container_java_env",
1073
+ path=rel_path,
1074
+ value=m.group(1).strip(),
1075
+ version=major,
1076
+ distribution=_java_distribution_from_text(text),
1077
+ ))
1078
+
1079
+ versions = sorted({e["version"] for e in evidence if isinstance(e.get("version"), int)})
1080
+ distributions = sorted({e["distribution"] for e in evidence if e.get("distribution")})
1081
+ target_blockers: list[dict] = []
1082
+ for version in versions:
1083
+ if version < target_lts:
1084
+ target_blockers.append({
1085
+ "kind": "version_below_target_lts",
1086
+ "version": version,
1087
+ "target_lts": target_lts,
1088
+ "reason": f"{_java_lts_name(version)} evidence is below target {_java_lts_name(target_lts)}",
1089
+ })
1090
+ if not versions:
1091
+ target_blockers.append({
1092
+ "kind": "java_version_unknown",
1093
+ "version": None,
1094
+ "target_lts": target_lts,
1095
+ "reason": "No Java source/target/runtime version evidence found in build or deployment hints.",
1096
+ })
1097
+
1098
+ licensing_reasons: list[str] = []
1099
+ oracle_evidence = [
1100
+ e for e in evidence
1101
+ if str(e.get("distribution", "")).lower() == "oracle jdk"
1102
+ or ("oracle" in str(e.get("value", "")).lower() and "jdk" in str(e.get("value", "")).lower())
1103
+ ]
1104
+ for e in oracle_evidence:
1105
+ licensing_reasons.append(
1106
+ f"{e['path']} declares {e.get('distribution', 'Oracle JDK')} "
1107
+ f"{e.get('version_label', 'unknown version')} ({e['source']}); review support/licensing terms."
1108
+ )
1109
+
1110
+ return {
1111
+ "schema_version": "java-lts-inventory-v1",
1112
+ "target_lts": target_lts,
1113
+ "target_lts_basis": (
1114
+ "--target-jdk supplied by caller" if target_jdk else
1115
+ "default planning target for the 2026 Java LTS/licensing inventory"
1116
+ ),
1117
+ "versions": {
1118
+ "detected": versions,
1119
+ "lowest": versions[0] if versions else None,
1120
+ "highest": versions[-1] if versions else None,
1121
+ "labels": [_java_lts_name(v) for v in versions],
1122
+ },
1123
+ "distributions": distributions,
1124
+ "evidence": evidence,
1125
+ "spring_boot_version_detected": spring_boot_version,
1126
+ "target_lts_blockers": target_blockers,
1127
+ "licensing_review_needed": bool(licensing_reasons),
1128
+ "licensing_review_reasons": licensing_reasons,
1129
+ "legal_disclaimer": (
1130
+ "Inventory signal only, not legal advice. ASK marks explicit Oracle JDK "
1131
+ "or unknown-version evidence for review; license entitlement must be "
1132
+ "confirmed from contracts and deployed binaries."
1133
+ ),
1134
+ "limitations": limitations + [
1135
+ "No build was executed; Maven/Gradle toolchains and transitive runtime images may be incomplete.",
1136
+ "Distribution detection is based on local build/container text only.",
1137
+ ],
1138
+ }
863
1139
  # Cap on total readiness deduction from JDK-modernization findings (medium/low),
864
1140
  # so reflection/date cleanups cannot collapse a jakarta-ready repo's headline.
865
1141
  _JDK_ADVISORY_DEDUCTION_CAP: int = 15
@@ -1527,6 +1803,10 @@ class MigrationReport:
1527
1803
  # Opt-in — attached by the CLI under --blast-radius; None when not computed,
1528
1804
  # and then absent from the payload so the default report stays byte-identical.
1529
1805
  blast_radius: Optional[dict] = None
1806
+ # F-P: Java LTS/licensing inventory, derived from build/runtime hints. Additive
1807
+ # field: it informs migration planning but does not participate in readiness_score.
1808
+ java_lts_inventory: dict = field(default_factory=dict)
1809
+ evidence_manifest: dict = field(default_factory=dict)
1530
1810
 
1531
1811
  def finalize(self) -> "MigrationReport":
1532
1812
  if not self.generated_at:
@@ -2052,6 +2332,8 @@ class MigrationReport:
2052
2332
  "spring_test_only": self.spring_test_only,
2053
2333
  "spring_boot_2_detected": self.spring_boot_2_detected,
2054
2334
  "spring_boot_version_detected": self.spring_boot_version_detected,
2335
+ "java_lts_inventory": self.java_lts_inventory,
2336
+ **({"evidence_manifest": self.evidence_manifest} if self.evidence_manifest else {}),
2055
2337
  "summary": self.summary,
2056
2338
  "findings": [f.to_dict() for f in _listed],
2057
2339
  "hibernate": self.hibernate.to_dict() if self.hibernate is not None else None,
@@ -2155,6 +2437,9 @@ class MigrationReport:
2155
2437
  f"(readiness_score reflects jakarta/Boot3 only — Hibernate is a separate rewrite axis)"]
2156
2438
  if self.headline_blocker else []),
2157
2439
  f"Spring present: {self.spring_present} Spring Boot 2 detected: {_boot}",
2440
+ f"Java LTS inventory: target {self.java_lts_inventory.get('target_lts', 'unknown')} "
2441
+ f"detected {self.java_lts_inventory.get('versions', {}).get('labels', [])} "
2442
+ f"licensing_review_needed={self.java_lts_inventory.get('licensing_review_needed', False)}",
2158
2443
  f"Migration blockers (readiness dimensions): {self.migration_blocking_count}",
2159
2444
  f"Blocking issues (product code): {self.blocking_count} "
2160
2445
  f"(critical: {main_crit}, high: {main_high})"
@@ -2534,6 +2819,7 @@ def run_migrate_check(
2534
2819
  dep_read_errors = 0
2535
2820
  raw_dep_findings: list[MigrationFinding] = []
2536
2821
  _build_texts: list[str] = []
2822
+ _build_sources: list[tuple[str, str]] = []
2537
2823
  for abs_path, rel_path in build_files:
2538
2824
  try:
2539
2825
  text = abs_path.read_text(encoding="utf-8", errors="replace")
@@ -2541,6 +2827,7 @@ def run_migrate_check(
2541
2827
  dep_read_errors += 1
2542
2828
  continue
2543
2829
  _build_texts.append(text)
2830
+ _build_sources.append((rel_path, text))
2544
2831
  dep_findings = _scan_dep_file(text, rel_path)
2545
2832
  filtered = [f for f in dep_findings if SEVERITY_ORDER.get(f.severity, 3) <= min_order]
2546
2833
  raw_dep_findings.extend(filtered)
@@ -2595,6 +2882,12 @@ def run_migrate_check(
2595
2882
  root, spring_runtime_import_seen, spring_test_import_seen
2596
2883
  )
2597
2884
  spring_boot_present = _detect_spring_boot_present(root)
2885
+ java_lts_inventory = _java_lts_inventory(
2886
+ root,
2887
+ _build_sources,
2888
+ spring_boot_version=spring_boot_version,
2889
+ target_jdk=target_jdk,
2890
+ )
2598
2891
 
2599
2892
  # BUG #8: classify each finding's code context (main / test / generated) so the
2600
2893
  # report can keep test fixtures and autogenerated sources out of blocking_count.
@@ -2645,6 +2938,7 @@ def run_migrate_check(
2645
2938
  jakarta_namespace_adopted=jakarta_import_count > 0,
2646
2939
  findings=all_findings,
2647
2940
  hibernate=hibernate_strat,
2941
+ java_lts_inventory=java_lts_inventory,
2648
2942
  limitations=limitations,
2649
2943
  metadata={
2650
2944
  "java_files_scanned": len(file_paths),
@@ -2703,7 +2997,19 @@ def run_migrate_check(
2703
2997
  except Exception:
2704
2998
  pass
2705
2999
 
2706
- return report.finalize()
3000
+ report = report.finalize()
3001
+ from sourcecode.provenance import build_evidence_manifest
3002
+ report.evidence_manifest = build_evidence_manifest(
3003
+ report.to_dict(),
3004
+ command="migrate-check",
3005
+ root=root,
3006
+ inputs={
3007
+ "min_severity": min_severity,
3008
+ "target_jdk": target_jdk,
3009
+ "keep_boot": keep_boot,
3010
+ },
3011
+ )
3012
+ return report
2707
3013
 
2708
3014
 
2709
3015
  # Remaining static limitations — things that truly require runtime analysis
@@ -0,0 +1,202 @@
1
+ """Claim provenance manifest for ASK outputs.
2
+
3
+ The manifest is an index over claims already present in a command payload. It
4
+ does not re-run analysis and does not certify a claim; it makes the published
5
+ claim traceable to source spans, command inputs, tool version and freshness.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import subprocess
12
+ from datetime import datetime, timezone
13
+ from pathlib import Path
14
+ from typing import Any, Optional
15
+
16
+ from sourcecode import __version__
17
+
18
+ PROVENANCE_SCHEMA = "evidence-manifest-v1"
19
+ _MAX_CLAIMS = 200
20
+
21
+
22
+ def _stable_id(*parts: object) -> str:
23
+ raw = "|".join(str(p) for p in parts)
24
+ return "ev-" + hashlib.sha256(raw.encode("utf-8")).hexdigest()[:16]
25
+
26
+
27
+ def _git_head(root: Path) -> Optional[str]:
28
+ try:
29
+ result = subprocess.run(
30
+ ["git", "-C", str(root), "rev-parse", "--short", "HEAD"],
31
+ capture_output=True,
32
+ text=True,
33
+ timeout=3,
34
+ )
35
+ except Exception:
36
+ return None
37
+ if result.returncode != 0:
38
+ return None
39
+ return result.stdout.strip() or None
40
+
41
+
42
+ def _line(row: dict[str, Any]) -> Optional[int]:
43
+ value = row.get("first_line", row.get("line"))
44
+ return value if isinstance(value, int) else None
45
+
46
+
47
+ def _source_path(row: dict[str, Any]) -> str:
48
+ return str(row.get("source_file") or row.get("file") or row.get("path") or "")
49
+
50
+
51
+ def _claim(
52
+ *,
53
+ command: str,
54
+ claim_type: str,
55
+ row: dict[str, Any],
56
+ text: str,
57
+ confidence: Optional[str] = None,
58
+ ) -> Optional[dict[str, Any]]:
59
+ path = _source_path(row)
60
+ line = _line(row)
61
+ if not path:
62
+ return None
63
+ claim_id = _stable_id(command, claim_type, path, line or "", text)
64
+ out: dict[str, Any] = {
65
+ "id": claim_id,
66
+ "type": claim_type,
67
+ "text": text,
68
+ "source": {"path": path, **({"line": line} if line is not None else {})},
69
+ }
70
+ if confidence:
71
+ out["confidence"] = confidence
72
+ return out
73
+
74
+
75
+ def _claims_from_rows(command: str, payload: dict[str, Any]) -> list[dict[str, Any]]:
76
+ claims: list[dict[str, Any]] = []
77
+ for field, claim_type in (
78
+ ("findings", "finding"),
79
+ ("risks", "risk"),
80
+ ("top_risks", "risk"),
81
+ ("relevant_files", "context_file"),
82
+ ("runtime_changes", "context_file"),
83
+ ("build_changes", "context_file"),
84
+ ):
85
+ rows = payload.get(field)
86
+ if not isinstance(rows, list):
87
+ continue
88
+ for row in rows:
89
+ if not isinstance(row, dict):
90
+ continue
91
+ title = str(row.get("title") or row.get("statement") or row.get("explanation") or field)
92
+ confidence = row.get("confidence")
93
+ if not isinstance(confidence, str):
94
+ confidence = None
95
+ claim = _claim(
96
+ command=command,
97
+ claim_type=claim_type,
98
+ row=row,
99
+ text=title,
100
+ confidence=confidence,
101
+ )
102
+ if claim is not None:
103
+ if row.get("rule_id"):
104
+ claim["rule_id"] = row["rule_id"]
105
+ if row.get("rule_ids"):
106
+ claim["rule_ids"] = row["rule_ids"]
107
+ claims.append(claim)
108
+ if len(claims) >= _MAX_CLAIMS:
109
+ return claims
110
+ return claims
111
+
112
+
113
+ def _claims_from_java_lts(command: str, payload: dict[str, Any]) -> list[dict[str, Any]]:
114
+ inv = payload.get("java_lts_inventory")
115
+ if not isinstance(inv, dict):
116
+ return []
117
+ claims: list[dict[str, Any]] = []
118
+ for row in inv.get("evidence") or []:
119
+ if not isinstance(row, dict):
120
+ continue
121
+ text = (
122
+ f"Java runtime/build evidence {row.get('value')} "
123
+ f"({row.get('source')})"
124
+ )
125
+ claim = _claim(
126
+ command=command,
127
+ claim_type="java_lts_evidence",
128
+ row=row,
129
+ text=text,
130
+ confidence="medium",
131
+ )
132
+ if claim is not None:
133
+ claim["version"] = row.get("version")
134
+ if row.get("distribution"):
135
+ claim["distribution"] = row["distribution"]
136
+ claims.append(claim)
137
+ return claims
138
+
139
+
140
+ def _embedded_manifests(payload: dict[str, Any]) -> list[dict[str, Any]]:
141
+ manifests: list[dict[str, Any]] = []
142
+ for value in payload.values():
143
+ if isinstance(value, dict):
144
+ manifest = value.get("evidence_manifest")
145
+ if isinstance(manifest, dict):
146
+ manifests.append({
147
+ "schema_version": manifest.get("schema_version"),
148
+ "command": (manifest.get("run") or {}).get("command"),
149
+ "claim_count": len(manifest.get("claims") or []),
150
+ })
151
+ manifests.extend(_embedded_manifests(value))
152
+ elif isinstance(value, list):
153
+ for item in value:
154
+ if isinstance(item, dict):
155
+ manifests.extend(_embedded_manifests(item))
156
+ return manifests
157
+
158
+
159
+ def build_evidence_manifest(
160
+ payload: dict[str, Any],
161
+ *,
162
+ command: str,
163
+ root: Path,
164
+ inputs: Optional[dict[str, Any]] = None,
165
+ ) -> dict[str, Any]:
166
+ """Build a machine-readable provenance index for an existing payload."""
167
+ root = Path(root).resolve()
168
+ claims = _claims_from_rows(command, payload)
169
+ claims.extend(_claims_from_java_lts(command, payload))
170
+ claims = claims[:_MAX_CLAIMS]
171
+ sources = sorted({
172
+ str((claim.get("source") or {}).get("path"))
173
+ for claim in claims
174
+ if (claim.get("source") or {}).get("path")
175
+ })
176
+ return {
177
+ "schema_version": PROVENANCE_SCHEMA,
178
+ "run": {
179
+ "command": command,
180
+ "tool": "ask",
181
+ "tool_version": __version__,
182
+ "repository": str(root),
183
+ "git_head": _git_head(root),
184
+ "generated_at": datetime.now(timezone.utc).isoformat(),
185
+ "inputs": inputs or {},
186
+ },
187
+ "claims": claims,
188
+ "sources": [{"path": path} for path in sources],
189
+ "claims_cap": (
190
+ {"total_indexed": _MAX_CLAIMS, "capped": True}
191
+ if len(claims) >= _MAX_CLAIMS else
192
+ {"total_indexed": len(claims), "capped": False}
193
+ ),
194
+ "embedded_manifests": _embedded_manifests(payload),
195
+ "limits": {
196
+ "statement": (
197
+ "This manifest indexes claims already present in the payload. It "
198
+ "does not add analysis, certify correctness, or replace the source "
199
+ "evidence carried by each command."
200
+ )
201
+ },
202
+ }
sourcecode/risk.py CHANGED
@@ -1209,4 +1209,15 @@ def build_risk(
1209
1209
  "omitted": len(kept) - limit,
1210
1210
  **cap_effect("display_list", limit=limit, total=len(kept)),
1211
1211
  }
1212
+ from sourcecode.provenance import build_evidence_manifest
1213
+ payload["evidence_manifest"] = build_evidence_manifest(
1214
+ payload,
1215
+ command="risk",
1216
+ root=root,
1217
+ inputs={
1218
+ "limit": limit,
1219
+ "min_band": min_band,
1220
+ "profiles": sorted(profiles) if profiles is not None else None,
1221
+ },
1222
+ )
1212
1223
  return payload
sourcecode/sarif.py CHANGED
@@ -615,4 +615,16 @@ def enrich_sarif(
615
615
  "omitted": len(kept) - limit,
616
616
  **cap_effect("display_list", limit=limit, total=len(kept)),
617
617
  }
618
+ from sourcecode.provenance import build_evidence_manifest
619
+ payload["evidence_manifest"] = build_evidence_manifest(
620
+ payload,
621
+ command="enrich",
622
+ root=root,
623
+ inputs={
624
+ "sarif": str(sarif_path),
625
+ "limit": limit,
626
+ "min_band": min_band,
627
+ "profiles": sorted(profiles) if profiles is not None else None,
628
+ },
629
+ )
618
630
  return payload
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 4.9.0
3
+ Version: 4.9.1
4
4
  Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
5
5
  License-File: LICENSE
6
6
  Keywords: agents,ai,codebase,context,developer-tools,llm
@@ -42,7 +42,7 @@ Description-Content-Type: text/markdown
42
42
 
43
43
  **Context · Impact · Migration · Architecture · Review — everything from one structural model.**
44
44
 
45
- ![Version](https://img.shields.io/badge/version-4.9.0-blue)
45
+ ![Version](https://img.shields.io/badge/version-4.9.1-blue)
46
46
  ![Python](https://img.shields.io/badge/python-3.9%2B-green)
47
47
 
48
48
  > **ASK Engine** is the product. The CLI command is **`ask`**. The legacy **`sourcecode`**
@@ -97,7 +97,7 @@ brew tap haroundominique/sourcecode && brew install sourcecode
97
97
  # pip / pipx
98
98
  pipx install sourcecode # or: pip install sourcecode
99
99
 
100
- ask version # ask 4.9.0 — and, on a build that has aged,
100
+ ask version # ask 4.9.1 — and, on a build that has aged,
101
101
  # how many releases have probably shipped since
102
102
  ```
103
103
 
@@ -137,8 +137,9 @@ ask endpoints /path/to/repo
137
137
  # CGLIB no-op), security surface, request-body validation.
138
138
  ask spring-audit /path/to/repo
139
139
 
140
- # Spring Boot 2→3 readiness: located blockers, per-dimension score, effort estimate.
140
+ # Spring Boot 2→3 readiness plus Java LTS/licensing inventory.
141
141
  ask migrate-check /path/to/repo --compact
142
+ # Add --target-jdk 25 to make the LTS route explicit.
142
143
  ```
143
144
 
144
145
  **Then the three that finish the sentence the four above start.** A profile-set answer stays
@@ -308,12 +309,12 @@ from it.
308
309
  | `posture` | experimental | which beans a profile set wires, and how effective endpoint access differs between two sets | the most differentiated capability here |
309
310
  | `risk` | experimental | what each defect actually costs, once reach, access, write effect and the shape of the input path are in it | `defect_severity × reachability × auth_verdict × write_effect × query_construction × input_constraints`; every factor names its authority; `--table --rule SEC-008 --band high --top-n 20` gives audit triage without `jq` |
310
311
  | `enrich` | experimental | another scanner's SARIF findings, ranked by what this repository does with them | `--sarif <log>`; same composition as `risk`, with `--table --rule <id> --band high --top-n 20` for triage |
311
- | `audit-report` | experimental | human audit bundle over existing `risk` + `posture` evidence, optionally signed | `--format markdown`; `--sign-key <file>` signs canonical JSON with HMAC-SHA256 |
312
+ | `audit-report` | experimental | human audit bundle over existing `risk` + `posture` evidence, optionally signed | includes `evidence_manifest` claim provenance; `--format markdown`; `--sign-key <file>` signs canonical JSON with HMAC-SHA256 |
312
313
  | `migrate-recipe` | experimental | the migration report as the OpenRewrite recipe that applies it | only recipes a finding named; the manual remainder published beside them; writes nothing without `--write`, and never a runnable command for an empty recipe list |
313
314
  | `data-exposure` | experimental | which routes can carry the data you labelled, and who reaches them | labels declared in `sourcecode.config.json` — never inferred from a name; `signature` and `call_reach` evidence published apart, field-level flow out of scope (NC-008). With nothing declared, `ask data-exposure /path/to/repo` answers `answered: false` and hands back the file, the key and an example to declare — it never reports zero exposed routes |
314
315
  | `endpoints` | core | every REST endpoint, effective path, security policy, confidence | Spring MVC + JAX-RS (~65 % recall on JAX-RS sub-resource locators). `--compact` answers the exposure census without the rows; `--servlets` lists the servlet-mounted surface as its own population; `--client-usage` says which routes the TS/JS client in this repository actually calls |
315
316
  | `spring-audit` | core | transactional anomalies + security surface + validation gaps | `--ci`, `-f github-comment`; `--table --rule SEC-008 --top-n 20` for human-scale review |
316
- | `migrate-check` | core | Boot 2→3 readiness: located blockers, per-dimension score, effort | `--blast-radius` orders the re-test plan; `--table --rule MIG-001 --band critical --top-n 20` for blocker triage |
317
+ | `migrate-check` | core | Boot 2→3 readiness + Java LTS/licensing inventory: located blockers, per-dimension score, effort, detected Java 8/11/17/21/25 evidence and explicit licensing-review signals | `--target-jdk 25` sets the target LTS route; `--blast-radius` orders the re-test plan; `--table --rule MIG-001 --band critical --top-n 20` for blocker triage |
317
318
  | `impact` / `impact-chain` | core | blast radius of a change, to the endpoints it reaches | target the **interface**, not the `Impl` |
318
319
  | `pr-impact` | core | the same, scoped to a PR diff | gating command: `--fail-on`, exit codes |
319
320
  | `verify` | core | does the repo satisfy its declared contracts, **relative to a baseline** | `.ask/contracts.yml`; exit 0/1/2 |
@@ -401,6 +402,16 @@ and that gap is the honest description, not a defect to argue with. `migrate-che
401
402
  the OpenRewrite recipe each finding carries so the executor that does apply changes has its
402
403
  input.
403
404
 
405
+ **Java LTS/licensing inventory.** `migrate-check` emits `java_lts_inventory` beside the migration
406
+ scores. It reads Maven/Gradle, local runtime hints and container files for Java 8/11/17/21/25
407
+ evidence, reports target-LTS blockers with `--target-jdk`, and marks explicit Oracle JDK evidence
408
+ as `licensing_review_needed`. This is inventory evidence, not legal advice.
409
+
410
+ **Claim provenance ledger.** JSON outputs from `risk`, `enrich`, `migrate-check`,
411
+ `audit-report` and `prepare-context` include `evidence_manifest`: stable claim IDs mapped to
412
+ source paths/lines, command inputs, ASK version, git HEAD and freshness metadata. The manifest
413
+ indexes claims already in the payload; it does not add analysis or certify compliance.
414
+
404
415
 
405
416
  ---
406
417
 
@@ -1,4 +1,4 @@
1
- sourcecode/__init__.py,sha256=qmMU4XyVYaQ8oZUh7exFVgj9YNWXVEdccvidG3GXWdg,308
1
+ sourcecode/__init__.py,sha256=-xlrcRnU01z5SLmkXJMeg2C79FevkV3-PlmwssyEWIg,308
2
2
  sourcecode/adaptive_scanner.py,sha256=yJBKjNpkY6bpueYJ2YnRezen3sYZDecEt7WaaNWdqug,9466
3
3
  sourcecode/archetype.py,sha256=CZvRLpkHot_D8D3JFQVorr-EHDJyh0BbS7RnSTqBigM,40499
4
4
  sourcecode/architectural_baseline.py,sha256=agDSwGEakdkgLLh5xnd3hiD_TC6pRujWt4klnuYF1wo,18689
@@ -6,7 +6,7 @@ sourcecode/architectural_delta.py,sha256=E6MkyjWl-1ZgR4KSKnI785gUAfc8nq-lK-jd_ru
6
6
  sourcecode/architecture_analyzer.py,sha256=GFc4ek-s1IHWM7pl-0L32WahZ93AmDgrAMcOuBKA5Dk,61463
7
7
  sourcecode/architecture_summary.py,sha256=BVVRHd952cjRhjHnR6CPrvKgaa-tdM16l-pBi1yDCPs,32395
8
8
  sourcecode/ast_extractor.py,sha256=aXJjZ7XjAdxa99hWNm4SyzPqx4gNWiTbUYjNWGNl-Vc,51213
9
- sourcecode/audit_report.py,sha256=l6xPRSLms4SCVbL-Tu3aCvDtTTHciyPenuih32ngod0,5392
9
+ sourcecode/audit_report.py,sha256=6YCvXR9C9jRTpngjUYJbhE35KxionHK0gM2eBY8ORi4,5758
10
10
  sourcecode/baseline_autocapture.py,sha256=tmMLexQSeaQRRbqxAd7walvGQqnF0mdpxQmkL40rlhI,15623
11
11
  sourcecode/cache.py,sha256=9XJUNSGzqM6DNWy4erUO-OcZY9qkrTCCcOJ-OANVrw8,41409
12
12
  sourcecode/cache_model.py,sha256=hUGWNKBKLV76mGveFffOzr9fqFD1ZydJVakseRsg14s,17401
@@ -18,7 +18,7 @@ sourcecode/chain_rules.py,sha256=Bi6UHfgd-GxWswmnHRcPz5jdbAuqka3Zkz_P-MTvqhw,127
18
18
  sourcecode/change_plan.py,sha256=kFjjp16XYbupgkv1CPkfqo39_SiZPMRQ8OOfC-Vy9eg,7929
19
19
  sourcecode/cir_graphs.py,sha256=9G0HHj1kw2325IDyzo2OpX73BNswEckecf4MZUXB4JM,12078
20
20
  sourcecode/classifier.py,sha256=JBzPwSSrDG-tUHAbcKB678HRbjLpD-ohzbzzO62mgpo,20114
21
- sourcecode/cli.py,sha256=JFmUut4Ux2ziJAwWjfJb0imSvrZACSvvdjR5I1RqI_0,519004
21
+ sourcecode/cli.py,sha256=YUaIQLqKahnKqv9-2QWOTL5qnlMAuDHPZj4kjqqK-7g,520130
22
22
  sourcecode/client_calls.py,sha256=daRTgbXNUOfkzGXJpVb6A737R_Thhka8vw1_YgxCaLg,13548
23
23
  sourcecode/code_notes_analyzer.py,sha256=EJemNCNc9Dn-1RZYu-aNbK0ELzmsyC4s6FdHi3XyNEI,9392
24
24
  sourcecode/compare.py,sha256=xq3zsqAOAw4AWkoD9khb9xDP_O3KvwqO9k-pf8sbi3g,10951
@@ -66,7 +66,7 @@ sourcecode/jdk_exports.py,sha256=fCrlwNAXUT9gge_joq6kMnY3zJxYB2pxqy-0w3o3MJI,874
66
66
  sourcecode/license.py,sha256=vvgE1vBUWh8ozWI7USYuteh6noxntnjuzDQIPF-l4rA,27795
67
67
  sourcecode/mcp_nudge.py,sha256=lKemOqK_wny2u7Ymcr2Idi5Kx8pXY02jCi-_nJYLGMg,2992
68
68
  sourcecode/metrics_analyzer.py,sha256=gLoRWKygF18jLgwsqmGXSWopw-f5iO1VbM7Jjdif6ag,22809
69
- sourcecode/migrate_check.py,sha256=M6Q1kaIK7w9SSjwta34fXOVkeF-bJtpv9Fqnle4VYyw,136690
69
+ sourcecode/migrate_check.py,sha256=1X3iMVhy4PRAryrrYoX1-8sUOpDyZWQkLRdGZRO2_Qk,148992
70
70
  sourcecode/migration_blast.py,sha256=OPryWkM6-PbvcG3eJFdcWUnlpXBje41nu2BkrZnE2hg,9359
71
71
  sourcecode/non_coverage.py,sha256=N4pLd0zKOEdkObzMz3TSMTCDRmV0dwqZtXWFFPR9gSo,12001
72
72
  sourcecode/openapi_surface.py,sha256=BTt0K-woZbkbWTN77IkqeBm_Okag9owR0848fmot8sk,16207
@@ -82,6 +82,7 @@ sourcecode/pr_comment_renderer.py,sha256=239PmJdf95av_ZW236C7_tvq_ahECeuF9AZxrhe
82
82
  sourcecode/pr_impact.py,sha256=eaFgcPMDsjaF8VZvcitFWCPCabMTi-khtGdxpK0wXTQ,22461
83
83
  sourcecode/prepare_context.py,sha256=B8TiBQbazxUOz6D37Dt9zqbOtD5b2S_fR3MB15P9DrI,237206
84
84
  sourcecode/progress.py,sha256=qn30sWaHOkjTgXsSBmiPkz7Rsbwc5oSlIe6JNEMYp_k,3149
85
+ sourcecode/provenance.py,sha256=nAkzUQ6OzvHanhA4PCTRuBgyhrlaJXdN71YjQdcguDE,6590
85
86
  sourcecode/ranking_engine.py,sha256=ZAucq_YX2KkWUuAZf4P0lhtQ_38vEFnUhuGtSZd1S0E,12970
86
87
  sourcecode/readiness_timeline.py,sha256=qqc14vL9nj9E4qw4bVZpGhfMLaVaYr9s6Pak_VUIwt0,8008
87
88
  sourcecode/reconciliation.py,sha256=GU-1PTcVr8zcbtC7BASfpHcZndP9AdboXPNQiBc0fzo,34251
@@ -94,10 +95,10 @@ sourcecode/rename_refactor.py,sha256=h6dNFlB9aZ_3q6heeHBkgXQeXaT03nvPSsYH6P8qxFg
94
95
  sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
95
96
  sourcecode/repository_ir.py,sha256=yrQd8yB9NO0M2SLkI0bQ5ty1RjTldQOhVbr3IoNkXJk,357437
96
97
  sourcecode/ris.py,sha256=SjjDNMcc9Zr9us9G8ikBVkoPXKK3BpM-dBodSGMh_g4,24783
97
- sourcecode/risk.py,sha256=OiHibjaPTlXhmjZNlfcOG88CJj2x1VKjsSf7Kr_CbVs,54137
98
+ sourcecode/risk.py,sha256=UgTVdMc8XBArO6D9e2Xx9I-kKqx-PW4W0hSe1Jy9qjU,54491
98
99
  sourcecode/rule_catalog.py,sha256=xmphuULUCtg_GWTbriwY25D-N2IaeMXXvB5CNU1xSuA,4925
99
100
  sourcecode/runtime_classifier.py,sha256=uTAD6BDCiBLUZEDRfqk718kM4RTT_vAbfkcOI2_Xx58,18432
100
- sourcecode/sarif.py,sha256=_3ggbtV0O2iK5zeUiJSS36KxpPRVAtGy0hzXAFBmww8,25590
101
+ sourcecode/sarif.py,sha256=3hQEegUxIZbojFdY59oB-yKsK-rnafHdTNmYsEP2--A,25984
101
102
  sourcecode/scanner.py,sha256=z3CV0rcGunu0Y8mpNgp07wI7nxT0pxw1BkXRRtI0Rpo,9609
102
103
  sourcecode/schema.py,sha256=aHNXDf8LGyUC8ZDE_VS9kiskC2-Oswhi_WnpdGy6HDw,24897
103
104
  sourcecode/security_config.py,sha256=KblMEoRiEjrIE68YsPaUAFebxFp8UM7MS7lAk5CGD8U,3531
@@ -191,8 +192,8 @@ sourcecode/telemetry/consent.py,sha256=pQdl-QeLl6Gcibn0eWHSKZrm-HYSsjpVqOnjrgFp8
191
192
  sourcecode/telemetry/events.py,sha256=4_yeO58U-Cwc1Qb27VB0_EjhmroY0k91n3_VGxeALB8,2776
192
193
  sourcecode/telemetry/filters.py,sha256=RzxauTz8HliO4BllQnXEXc7zTeqdCZi5MgqGEDuW7OQ,6570
193
194
  sourcecode/telemetry/transport.py,sha256=4gGHsq0WeY9VywEZXA3vUxykfiYnw9uuqfjAAec7F8o,1681
194
- sourcecode-4.9.0.dist-info/METADATA,sha256=zYlJaXbhple_TmmCRkcHt2x_REhzgXnQRR3oNeL-s10,34785
195
- sourcecode-4.9.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
196
- sourcecode-4.9.0.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
197
- sourcecode-4.9.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
198
- sourcecode-4.9.0.dist-info/RECORD,,
195
+ sourcecode-4.9.1.dist-info/METADATA,sha256=4S6TZgY8kS5yoXj8TpekgXe80PBoZmSoilVukkDWg-s,35741
196
+ sourcecode-4.9.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
197
+ sourcecode-4.9.1.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
198
+ sourcecode-4.9.1.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
199
+ sourcecode-4.9.1.dist-info/RECORD,,