sourcecode 2.6.2__tar.gz → 2.6.3__tar.gz

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 (172) hide show
  1. {sourcecode-2.6.2 → sourcecode-2.6.3}/CHANGELOG.md +53 -0
  2. {sourcecode-2.6.2 → sourcecode-2.6.3}/PKG-INFO +1 -1
  3. {sourcecode-2.6.2 → sourcecode-2.6.3}/pyproject.toml +1 -1
  4. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/cli.py +81 -0
  6. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/mcp/registry.py +49 -1
  7. sourcecode-2.6.3/src/sourcecode/parse_cache.py +138 -0
  8. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/repository_ir.py +46 -12
  9. sourcecode-2.6.3/src/sourcecode/verify_edit.py +588 -0
  10. sourcecode-2.6.3/src/sourcecode/verify_rules.py +304 -0
  11. {sourcecode-2.6.2 → sourcecode-2.6.3}/.github/workflows/build-windows.yml +0 -0
  12. {sourcecode-2.6.2 → sourcecode-2.6.3}/.gitignore +0 -0
  13. {sourcecode-2.6.2 → sourcecode-2.6.3}/.ruff.toml +0 -0
  14. {sourcecode-2.6.2 → sourcecode-2.6.3}/CONTRIBUTING.md +0 -0
  15. {sourcecode-2.6.2 → sourcecode-2.6.3}/LICENSE +0 -0
  16. {sourcecode-2.6.2 → sourcecode-2.6.3}/README.md +0 -0
  17. {sourcecode-2.6.2 → sourcecode-2.6.3}/SECURITY.md +0 -0
  18. {sourcecode-2.6.2 → sourcecode-2.6.3}/raw +0 -0
  19. {sourcecode-2.6.2 → sourcecode-2.6.3}/scripts/compare_integration_engines.py +0 -0
  20. {sourcecode-2.6.2 → sourcecode-2.6.3}/scripts/customer_smoke_test.sh +0 -0
  21. {sourcecode-2.6.2 → sourcecode-2.6.3}/scripts/generate_jdk_exports.py +0 -0
  22. {sourcecode-2.6.2 → sourcecode-2.6.3}/scripts/perf_harness.py +0 -0
  23. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/adaptive_scanner.py +0 -0
  24. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/archetype.py +0 -0
  25. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/architectural_baseline.py +0 -0
  26. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/architectural_delta.py +0 -0
  27. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/architecture_analyzer.py +0 -0
  28. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/architecture_summary.py +0 -0
  29. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/ast_extractor.py +0 -0
  30. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/cache.py +0 -0
  31. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/call_surface.py +0 -0
  32. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/caller_metrics.py +0 -0
  33. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/canonical_ir.py +0 -0
  34. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/change_plan.py +0 -0
  35. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/cir_graphs.py +0 -0
  36. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/classifier.py +0 -0
  37. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/code_notes_analyzer.py +0 -0
  38. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/compare.py +0 -0
  39. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/confidence_analyzer.py +0 -0
  40. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/constraint_diff.py +0 -0
  41. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/context_cache.py +0 -0
  42. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/context_graph.py +0 -0
  43. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/context_scorer.py +0 -0
  44. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/context_summarizer.py +0 -0
  45. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/contract_diff.py +0 -0
  46. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/contract_model.py +0 -0
  47. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/contract_pipeline.py +0 -0
  48. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/coverage_parser.py +0 -0
  49. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/dependency_analyzer.py +0 -0
  50. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/detectors/__init__.py +0 -0
  51. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/detectors/base.py +0 -0
  52. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/detectors/csproj_parser.py +0 -0
  53. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/detectors/dart.py +0 -0
  54. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/detectors/dotnet.py +0 -0
  55. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/detectors/elixir.py +0 -0
  56. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/detectors/go.py +0 -0
  57. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/detectors/heuristic.py +0 -0
  58. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/detectors/hybrid.py +0 -0
  59. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/detectors/java.py +0 -0
  60. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/detectors/jvm_ext.py +0 -0
  61. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/detectors/nodejs.py +0 -0
  62. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/detectors/parsers.py +0 -0
  63. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/detectors/php.py +0 -0
  64. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/detectors/project.py +0 -0
  65. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/detectors/python.py +0 -0
  66. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/detectors/ruby.py +0 -0
  67. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/detectors/rust.py +0 -0
  68. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/detectors/systems.py +0 -0
  69. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/detectors/terraform.py +0 -0
  70. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/detectors/tooling.py +0 -0
  71. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/doc_analyzer.py +0 -0
  72. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/dynamic_argument_surface.py +0 -0
  73. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/endpoint_literals.py +0 -0
  74. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/endpoint_metrics.py +0 -0
  75. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/entrypoint_classifier.py +0 -0
  76. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/env_analyzer.py +0 -0
  77. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/error_schema.py +0 -0
  78. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/evidence_provider.py +0 -0
  79. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/explain.py +0 -0
  80. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/file_chunker.py +0 -0
  81. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/file_classifier.py +0 -0
  82. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/filter_surface.py +0 -0
  83. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/format_contract.py +0 -0
  84. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/fqn_utils.py +0 -0
  85. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/git_analyzer.py +0 -0
  86. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/graph_analyzer.py +0 -0
  87. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/graph_evidence.py +0 -0
  88. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/hibernate_strat.py +0 -0
  89. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/jdk_exports.py +0 -0
  90. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/license.py +0 -0
  91. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/mcp/__init__.py +0 -0
  92. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  93. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  94. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  95. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  96. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  97. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/mcp/orchestrator.py +0 -0
  98. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/mcp/runner.py +0 -0
  99. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/mcp/server.py +0 -0
  100. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/mcp_nudge.py +0 -0
  101. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/metrics_analyzer.py +0 -0
  102. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/migrate_check.py +0 -0
  103. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/openapi_surface.py +0 -0
  104. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/output_budget.py +0 -0
  105. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/path_filters.py +0 -0
  106. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/perf.py +0 -0
  107. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/pr_comment_renderer.py +0 -0
  108. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/pr_impact.py +0 -0
  109. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/prepare_context.py +0 -0
  110. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/progress.py +0 -0
  111. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/ranking_engine.py +0 -0
  112. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/reconciliation.py +0 -0
  113. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/redactor.py +0 -0
  114. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/relevance_scorer.py +0 -0
  115. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/rename_refactor.py +0 -0
  116. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/repo_classifier.py +0 -0
  117. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/retrieval/__init__.py +0 -0
  118. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/retrieval/context.py +0 -0
  119. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/retrieval/errors.py +0 -0
  120. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/retrieval/executor.py +0 -0
  121. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/retrieval/planner.py +0 -0
  122. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/retrieval/query.py +0 -0
  123. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/retrieval/request.py +0 -0
  124. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/retrieval/resolution.py +0 -0
  125. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/retrieval/result.py +0 -0
  126. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/retrieval/retriever.py +0 -0
  127. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/retrieval/runtime.py +0 -0
  128. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/retrieval/steps.py +0 -0
  129. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
  130. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/retrieval/steps_graph.py +0 -0
  131. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/retrieval/steps_impact.py +0 -0
  132. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/retrieval/steps_intf.py +0 -0
  133. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/retrieval/steps_struct.py +0 -0
  134. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/retrieval/steps_txsec.py +0 -0
  135. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/ris.py +0 -0
  136. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/runtime_classifier.py +0 -0
  137. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/scanner.py +0 -0
  138. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/schema.py +0 -0
  139. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/security_config.py +0 -0
  140. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/security_posture.py +0 -0
  141. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/semantic_analyzer.py +0 -0
  142. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/semantic_impact_engine.py +0 -0
  143. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/semantic_integration_engine.py +0 -0
  144. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/semantic_services.py +0 -0
  145. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/serializer.py +0 -0
  146. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/spring_event_topology.py +0 -0
  147. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/spring_findings.py +0 -0
  148. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/spring_impact.py +0 -0
  149. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/spring_model.py +0 -0
  150. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/spring_security_audit.py +0 -0
  151. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/spring_semantic.py +0 -0
  152. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/spring_tx_analyzer.py +0 -0
  153. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/summarizer.py +0 -0
  154. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/telemetry/__init__.py +0 -0
  155. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/telemetry/config.py +0 -0
  156. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/telemetry/consent.py +0 -0
  157. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/telemetry/events.py +0 -0
  158. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/telemetry/filters.py +0 -0
  159. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/telemetry/transport.py +0 -0
  160. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/tree_utils.py +0 -0
  161. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/type_usage_surface.py +0 -0
  162. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/validation_inference.py +0 -0
  163. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/validation_surface.py +0 -0
  164. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/version_check.py +0 -0
  165. {sourcecode-2.6.2 → sourcecode-2.6.3}/src/sourcecode/workspace.py +0 -0
  166. {sourcecode-2.6.2 → sourcecode-2.6.3}/supabase/functions/README.md +0 -0
  167. {sourcecode-2.6.2 → sourcecode-2.6.3}/supabase/functions/get-license/index.ts +0 -0
  168. {sourcecode-2.6.2 → sourcecode-2.6.3}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  169. {sourcecode-2.6.2 → sourcecode-2.6.3}/supabase/functions/telemetry/index.ts +0 -0
  170. {sourcecode-2.6.2 → sourcecode-2.6.3}/supabase/sql/license_event_ordering.sql +0 -0
  171. {sourcecode-2.6.2 → sourcecode-2.6.3}/supabase/sql/licensing_schema.sql +0 -0
  172. {sourcecode-2.6.2 → sourcecode-2.6.3}/supabase/sql/telemetry_events.sql +0 -0
@@ -1,5 +1,58 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.6.3] — 2026-07-24
4
+
5
+ **`ask verify-edit` — the in-loop semantic diff gate lands (the read-context → write-loop
6
+ verification pivot), plus a content-addressed parse cache and one field-test correctness
7
+ fix.** New user-facing capability; no change to any existing command's contract. Suite:
8
+ **3731 passed, 5 skipped, 1 deselected** (the deselected chunker test is a pre-existing
9
+ external-fixture flake).
10
+
11
+ ### Added
12
+
13
+ - **`ask verify-edit` — a deterministic, in-loop semantic diff gate.** Verdict over the
14
+ diff of the working tree against HEAD, built for the agent write-loop and CI. Each axis
15
+ is a diff over two Canonical IRs (working vs a throwaway `git worktree` checkout of
16
+ HEAD), reporting only regressions the edit **introduced** — pre-existing debt never
17
+ fails an unrelated commit:
18
+ - `contract_broken` — a public symbol removed, its visibility downgraded, or (when
19
+ unambiguous) its signature changed.
20
+ - `tx_boundary_changed` — a `@Transactional` proxy boundary broken (e.g. a
21
+ `@Transactional` method flipped `public` → `private` → a silently inert CGLIB proxy).
22
+ - `security_delta` — a security-posture finding the edit introduced.
23
+ - `new_orphan_bean` — a bean that lost all its injection wiring.
24
+ - `custom_rule_violated` — the client's own invariants, declared in
25
+ `.ask/verify-rules.json` (below).
26
+ - plus an informational `blast_radius` (entry points reachable from the changed files).
27
+
28
+ Exit codes under `--ci` (default): `0` pass, `1` break, `2` unverified (a blocking axis
29
+ could not be computed — never a silent pass). JSON verdict to stdout, prose to stderr.
30
+ - **Custom domain rules (`.ask/verify-rules.json`).** Declare invariants of *your*
31
+ codebase and have `verify-edit` enforce them — `forbidden_edge` ("no `@RestController`
32
+ injects a `*DaoJpa`") and `require_endpoint_security` ("every `/payroll` endpoint
33
+ requires role X"). Selectors reason by structure (annotation / fqn-regex / kind /
34
+ modifier / source-file); the concrete names are config data, never branched on in the
35
+ engine. Zero-config: no rules file ⇒ nothing to enforce; a malformed ruleset degrades
36
+ to *unverified*, never a silent pass.
37
+ - **MCP tool `verify_edit`** (report-only) so an agent can call the gate after each edit,
38
+ and **`ask verify-edit --install-hook pre-commit|pre-push`** to install a git hook that
39
+ runs the same binary and blocks on a break (bypass with `git --no-verify`).
40
+ - **Content-addressed per-file parse cache.** Symbol extraction and the body-fact bundle
41
+ of an unchanged file are pure functions of its bytes, so they are memoised by a content
42
+ hash — a 1-file edit re-parses 1 file, not the whole repo. It **never changes the
43
+ result** (a miss just re-parses; the CIR is byte-identical whether the cache is cold,
44
+ warm, or disabled) and it accelerates every CIR consumer, not only `verify-edit`.
45
+ Measured on a 1076-file repo: the working-CIR build drops ~59% warm.
46
+
47
+ ### Fixed
48
+
49
+ - **`verify-edit` contract axis no longer false-breaks on overloaded methods** (found in
50
+ a field-test of a real repo with a clean tree). An fqn carries no signature, so
51
+ overloaded methods/constructors share one id across several graph nodes; the axis now
52
+ aggregates visibility per fqn as *any overload public* instead of picking an
53
+ order-dependent representative, so a public name that also has a non-public overload no
54
+ longer reads as a downgrade on an unchanged tree.
55
+
3
56
  ## [2.6.2] — 2026-07-23
4
57
 
5
58
  **Field-eval Fase 3 — cache reuse (C) + cold-scan honesty (E). No
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 2.6.2
3
+ Version: 2.6.3
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "2.6.2"
7
+ version = "2.6.3"
8
8
  description = "Persistent structural context and ultra-fast repeated analysis for AI coding agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -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__ = "2.6.2"
7
+ __version__ = "2.6.3"
@@ -254,6 +254,8 @@ _SUBCOMMANDS: frozenset[str] = frozenset(
254
254
  "explain",
255
255
  # Spring Boot 2→3 migration readiness
256
256
  "migrate-check",
257
+ # In-loop semantic diff gate (working tree vs HEAD)
258
+ "verify-edit",
257
259
  # Native file rename (BLOCKER-A)
258
260
  "rename-class",
259
261
  # Large file semantic chunking (BLOCKER-B)
@@ -5678,6 +5680,85 @@ def spring_audit_cmd(
5678
5680
  raise typer.Exit(code=1)
5679
5681
 
5680
5682
 
5683
+ # ── verify-edit: in-loop semantic diff gate ───────────────────────────────────
5684
+
5685
+
5686
+ @app.command("verify-edit")
5687
+ def verify_edit_cmd(
5688
+ path: Path = typer.Argument(
5689
+ Path("."),
5690
+ help="Repository path (default: current directory). Diffs the working tree vs HEAD.",
5691
+ ),
5692
+ output_path: Optional[Path] = typer.Option(
5693
+ None, "--output", "-o", help="Write the verdict JSON to a file instead of stdout."
5694
+ ),
5695
+ format: str = typer.Option("json", "--format", help="Output format: json."),
5696
+ ci: bool = typer.Option(
5697
+ True,
5698
+ "--ci/--no-ci",
5699
+ help="Exit non-zero on a broken/unverified verdict (default on — for gates and hooks).",
5700
+ ),
5701
+ install_hook: Optional[str] = typer.Option(
5702
+ None,
5703
+ "--install-hook",
5704
+ help="Install a git hook (pre-commit|pre-push) that runs this gate, then exit. Does not verify.",
5705
+ ),
5706
+ ) -> None:
5707
+ """Semantic diff gate: did the working-tree edits change runtime behavior?
5708
+
5709
+ Deterministic, in-loop verdict over the diff of the working tree against HEAD:
5710
+ public contract, @Transactional proxy boundary, security surface, bean wiring,
5711
+ custom domain rules, and blast radius. Built for the agent write-loop (MCP) and
5712
+ git hooks. Only regressions the edit INTRODUCED (vs HEAD) block — pre-existing
5713
+ debt never fails an unrelated commit.
5714
+
5715
+ Custom rules: drop a `.ask/verify-rules.json` at the repo root to enforce your own
5716
+ invariants (e.g. "no @RestController injects a *DaoJpa", "every /payroll endpoint
5717
+ requires role X"). They gate the same way as the built-in axes.
5718
+
5719
+ Exit codes (with --ci, the default): 0 = pass, 1 = break (a blocking axis proved
5720
+ a regression), 2 = unverified (a blocking axis could not be computed — never a
5721
+ silent pass) or the path is not a git repository.
5722
+
5723
+ Examples:
5724
+ ask verify-edit # verify the current working tree
5725
+ ask verify-edit /path/to/repo
5726
+ ask verify-edit --no-ci # always exit 0 (report only)
5727
+ ask verify-edit --install-hook pre-commit
5728
+ """
5729
+ from sourcecode import verify_edit as _ve
5730
+
5731
+ if install_hook is not None:
5732
+ try:
5733
+ _hook_path = _ve.install_git_hook(path, install_hook)
5734
+ except _ve.VerifyEditError as exc:
5735
+ _emit_error_json(
5736
+ INVALID_INPUT_CODE,
5737
+ str(exc),
5738
+ hint=f"Choose a hook type: {' | '.join(_ve.HOOK_TYPES)}. The repo must be a git repository.",
5739
+ )
5740
+ raise typer.Exit(code=2)
5741
+ typer.echo(f"Installed verify-edit {install_hook} hook: {_hook_path}", err=True)
5742
+ typer.echo(f"It runs `ask verify-edit --ci` and blocks on a break; bypass with git --no-verify.", err=True)
5743
+ return
5744
+
5745
+ try:
5746
+ v = _ve.verify_edit(path)
5747
+ except _ve.VerifyEditError as exc:
5748
+ _emit_error_json(
5749
+ INVALID_INPUT_CODE,
5750
+ str(exc),
5751
+ hint="verify-edit needs a git repository with a HEAD commit (it diffs the working tree vs HEAD).",
5752
+ )
5753
+ raise typer.Exit(code=2 if ci else 0)
5754
+
5755
+ output = _serialize_dict(v.to_dict(), format)
5756
+ _emit_command_output(output, output_path, False,
5757
+ success_msg=f"verify-edit verdict written to {output_path}")
5758
+ if ci and v.exit_code != 0:
5759
+ raise typer.Exit(code=v.exit_code)
5760
+
5761
+
5681
5762
  # ── Spring Boot Migration Check ───────────────────────────────────────────────
5682
5763
 
5683
5764
 
@@ -1269,6 +1269,7 @@ _MCP_HIDDEN_CANONICAL_TOOLS: frozenset[str] = frozenset({
1269
1269
  "spring_audit", # curated: repo_path + scope + min_severity only (strips output_path/format/copy)
1270
1270
  "impact_chain", # curated: repo_path + symbol + depth + query_type with choices
1271
1271
  "migrate_check", # curated: repo_path + min_severity only (strips output_path/format/copy/ci)
1272
+ "verify_edit", # curated: repo_path only (report-only --no-ci; strips output_path/format/ci)
1272
1273
  # MCP self-management (an agent is not the MCP client admin)
1273
1274
  "mcp_init",
1274
1275
  "mcp_serve",
@@ -1450,7 +1451,54 @@ min_severity: "low" (default) | "medium" | "high" | "critical" — filter thresh
1450
1451
  docstring_override=_MIGRATE_CHECK_DOC,
1451
1452
  )
1452
1453
 
1453
- return [spring_audit, impact_chain, migrate_check]
1454
+ _VERIFY_EDIT_DOC = """\
1455
+ Semantic diff gate: did the working-tree edits change runtime behavior? JAVA/SPRING.
1456
+
1457
+ When to call: right AFTER editing Java source, BEFORE proposing a commit — the
1458
+ in-loop guardrail. Deterministic verdict over the diff of the working tree vs HEAD.
1459
+
1460
+ Report-only over MCP: it ALWAYS returns the verdict payload (never errors on a
1461
+ break) so you can read the result and auto-correct. Read `verdict`:
1462
+ "pass" — no runtime-behavior regression proven; safe to proceed.
1463
+ "break" — a blocking axis proved a regression; fix before committing. See
1464
+ `axes` + `reasons`.
1465
+ "unverified" — an axis could not be computed (e.g. parse failure); do not treat
1466
+ as safe.
1467
+
1468
+ Axes (each a diff HEAD→working):
1469
+ contract_broken — public API removed / visibility-downgraded / signature changed
1470
+ tx_boundary_changed — @Transactional now on a private/final method = silent CGLIB
1471
+ proxy bypass (TX ignored at runtime)
1472
+ security_delta — a new security finding the edit introduced
1473
+ new_orphan_bean — a bean that lost all its injection wiring
1474
+ blast_radius — informational: entry points reached by the changed files
1475
+
1476
+ Needs a git repository with a HEAD commit (it diffs the working tree vs HEAD).
1477
+ """
1478
+
1479
+ verify_edit = _alias_spec(
1480
+ "verify_edit",
1481
+ "Semantic diff gate over working-tree vs HEAD: contract / TX / security / bean-wiring. JAVA/SPRING.",
1482
+ ("verify-edit",),
1483
+ (
1484
+ ToolParamSpec("repo_path", "argument", str, required=False, default=".", is_path=True,
1485
+ help="Absolute path to the git repository (default: current directory)."),
1486
+ ),
1487
+ # Report-only: --no-ci so the tool always returns the verdict JSON (a break is
1488
+ # data for the agent, not a command failure that would surface as an error).
1489
+ lambda inputs: [
1490
+ "verify-edit",
1491
+ str(inputs.get("repo_path", ".")),
1492
+ "--no-ci",
1493
+ "--format", "json",
1494
+ ],
1495
+ supported_targets=("repo_path",),
1496
+ unsupported_targets=("file_path",),
1497
+ validator=validate_repo_path,
1498
+ docstring_override=_VERIFY_EDIT_DOC,
1499
+ )
1500
+
1501
+ return [spring_audit, impact_chain, migrate_check, verify_edit]
1454
1502
 
1455
1503
 
1456
1504
  @lru_cache(maxsize=1)
@@ -0,0 +1,138 @@
1
+ """Content-addressed per-file parse cache (verify-edit Fase V4 — sub-second loop).
2
+
3
+ The bottleneck, measured: on a 1-file edit in a large repo, ``build_repo_ir`` re-parses
4
+ *every* file, and per-file symbol extraction dominates the working-tree CIR build (~85%
5
+ of warm time; ~1.0s of 1.16s at 4.8k symbols). Yet the parse of an unchanged file is a
6
+ **pure function of its bytes** — so it can be memoised across builds.
7
+
8
+ This cache stores the output of ``_extract_symbols`` — ``(package, imports, symbols)`` —
9
+ keyed by a content hash of ``(rel_path, capture_signature, source)``. Properties that
10
+ make it safe:
11
+
12
+ * **Content-addressed ⇒ never stale.** The key *is* the content; a changed file yields
13
+ a different key and misses (re-parses), it never serves an old parse. There is no
14
+ invalidation to get wrong — unlike a signature-keyed cache.
15
+ * **Correctness never depends on it.** A miss, a corrupt entry, or an unwritable dir all
16
+ degrade to "parse it" — the CIR is byte-identical whether the cache hit or missed
17
+ (proven by the cir_hash-equivalence test, not assumed).
18
+ * **Isolated from the whole-CIR cache.** Its own directory tree; it can never poison the
19
+ ``java-cir`` knowledge entry that ``explain``/``impact``/``review-pr`` share.
20
+
21
+ Deserialisation was measured at ~5× cheaper than re-parsing (38ms vs 197ms for 4.8k
22
+ symbols), so the round-trip is a real win, not a wash.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import dataclasses
27
+ import hashlib
28
+ import json
29
+ import os
30
+ from pathlib import Path
31
+ from typing import TYPE_CHECKING, Optional
32
+
33
+ from sourcecode.context_cache import _atomic_write, _base_dir
34
+
35
+ if TYPE_CHECKING:
36
+ from sourcecode.repository_ir import SymbolRecord
37
+
38
+ _CACHE_SUBDIR = "parse-cache-v1" # bump when the SymbolRecord shape or extractor changes
39
+
40
+
41
+ def is_enabled() -> bool:
42
+ """On unless explicitly disabled (shares the context-cache kill switch). Correctness
43
+ is identical either way; this only trades disk for CPU."""
44
+ return os.environ.get("SOURCECODE_CONTEXT_CACHE_DISABLE", "") not in ("1", "true", "yes")
45
+
46
+
47
+ def _cache_root() -> Path:
48
+ return _base_dir() / _CACHE_SUBDIR
49
+
50
+
51
+ def file_key(rel_path: str, source: str, capture_sig: str) -> str:
52
+ """Content hash of everything ``_extract_symbols`` depends on. Two identical inputs
53
+ ⇒ same key (reuse); any change ⇒ different key (miss, re-parse)."""
54
+ h = hashlib.sha256()
55
+ h.update(rel_path.encode("utf-8", "replace"))
56
+ h.update(b"\x00")
57
+ h.update(capture_sig.encode("utf-8", "replace"))
58
+ h.update(b"\x00")
59
+ h.update(source.encode("utf-8", "replace"))
60
+ return h.hexdigest()[:32]
61
+
62
+
63
+ def _path_for(key: str) -> Path:
64
+ # Shard by the first 2 hex chars so no single directory grows unbounded.
65
+ return _cache_root() / key[:2] / f"{key}.json"
66
+
67
+
68
+ def get(key: str) -> "Optional[tuple[str, list[str], list[SymbolRecord]]]":
69
+ """Return the cached ``(package, imports, symbols)`` for *key*, or None on any miss.
70
+ Best-effort: a missing file, unreadable bytes, or a decode/shape error all read as a
71
+ miss (the caller then parses) — never an exception."""
72
+ if not is_enabled():
73
+ return None
74
+ from sourcecode.repository_ir import SymbolRecord
75
+
76
+ p = _path_for(key)
77
+ try:
78
+ raw = p.read_bytes()
79
+ except OSError:
80
+ return None
81
+ try:
82
+ d = json.loads(raw)
83
+ pkg = d["p"]
84
+ imports = list(d["i"])
85
+ symbols = [SymbolRecord(**sd) for sd in d["s"]]
86
+ except (ValueError, KeyError, TypeError):
87
+ return None # corrupt or shape-drifted entry — treat as absent, will rebuild
88
+ return pkg, imports, symbols
89
+
90
+
91
+ def put(key: str, package: str, imports: list[str], symbols: "list[SymbolRecord]") -> None:
92
+ """Store a parse result. Best-effort and side-effect-free on failure: caching is a
93
+ performance aid, never a correctness dependency."""
94
+ if not is_enabled():
95
+ return
96
+ try:
97
+ payload = {
98
+ "p": package,
99
+ "i": list(imports),
100
+ "s": [dataclasses.asdict(s) for s in symbols],
101
+ }
102
+ data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
103
+ p = _path_for(key)
104
+ p.parent.mkdir(parents=True, exist_ok=True)
105
+ _atomic_write(p, data)
106
+ except Exception:
107
+ pass # unwritable / serialisation fault — silently skip; correctness unaffected
108
+
109
+
110
+ # ── generic JSON blob (V4b: the per-file *body-fact* bundle) ─────────────────────
111
+ # The Pass-2 fact extractors (body/literal/guard/field-type/…) are, like symbol
112
+ # extraction, pure functions of the file's bytes, so their combined output is memoised
113
+ # under the SAME content key with a distinct suffix. Values are already plain JSON dicts
114
+ # (atomic-fact IR), so no dataclass reconstruction is needed. Same best-effort contract:
115
+ # a miss/corrupt entry recomputes; the CIR is byte-identical either way.
116
+ def _blob_path(key: str) -> Path:
117
+ return _cache_root() / key[:2] / f"{key}.blob.json"
118
+
119
+
120
+ def get_blob(key: str) -> Optional[dict]:
121
+ if not is_enabled():
122
+ return None
123
+ try:
124
+ return json.loads(_blob_path(key).read_bytes())
125
+ except (OSError, ValueError):
126
+ return None
127
+
128
+
129
+ def put_blob(key: str, data: dict) -> None:
130
+ if not is_enabled():
131
+ return
132
+ try:
133
+ raw = json.dumps(data, ensure_ascii=False).encode("utf-8")
134
+ p = _blob_path(key)
135
+ p.parent.mkdir(parents=True, exist_ok=True)
136
+ _atomic_write(p, raw)
137
+ except Exception:
138
+ pass
@@ -14,6 +14,7 @@ No inference, approximation, or heuristics.
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
+ import hashlib
17
18
  import random
18
19
  import re
19
20
  import subprocess
@@ -4968,6 +4969,15 @@ def build_repo_ir(
4968
4969
  custom_security = _load_custom_security(root)
4969
4970
  _custom_sec_tuple = tuple(custom_security)
4970
4971
  _extra_capture = _capture_markers(custom_security)
4972
+ # V4: content-addressed per-file parse cache. The symbol extraction of an unchanged
4973
+ # file is a pure function of its bytes (+ the capture markers), so it is memoised
4974
+ # across builds — a 1-file edit re-parses 1 file, not the whole repo. The cache never
4975
+ # changes the result (a miss just re-parses); the capture signature keys entries so a
4976
+ # different custom-security config cannot serve a parse produced under another.
4977
+ from sourcecode import parse_cache as _pc
4978
+ _capture_sig = hashlib.sha256(
4979
+ repr(sorted(_extra_capture)).encode("utf-8", "replace")
4980
+ ).hexdigest()[:16] if _extra_capture else "-"
4971
4981
 
4972
4982
  all_symbols: list[SymbolRecord] = []
4973
4983
  all_relations: list[RelationEdge] = []
@@ -5029,9 +5039,15 @@ def build_repo_ir(
5029
5039
  _meta_files_read += 1
5030
5040
  _meta_lines_read += source.count("\n") + (1 if source and not source.endswith("\n") else 0)
5031
5041
  _meta_chars_read += len(source)
5032
- package, symbols, raw_imports = _extract_symbols(
5033
- source, rel_path, extra_capture=_extra_capture
5034
- )
5042
+ _pkey = _pc.file_key(rel_path, source, _capture_sig)
5043
+ _cached = _pc.get(_pkey)
5044
+ if _cached is not None:
5045
+ package, raw_imports, symbols = _cached
5046
+ else:
5047
+ package, symbols, raw_imports = _extract_symbols(
5048
+ source, rel_path, extra_capture=_extra_capture
5049
+ )
5050
+ _pc.put(_pkey, package, raw_imports, symbols)
5035
5051
  all_symbols.extend(symbols)
5036
5052
  # P1-E (parse-coverage reconciliation): a file that declares a type but
5037
5053
  # yielded zero symbols is silently invisible to the graph. Record the
@@ -5060,15 +5076,33 @@ def build_repo_ir(
5060
5076
  pkg_type_map=_same_pkg_map,
5061
5077
  )
5062
5078
  if emit_body_facts:
5063
- _bf, _sf = _extract_body_facts(symbols, source, rel_path)
5064
- _all_body_facts.update(_bf)
5065
- _all_span_facts.update(_sf)
5066
- _all_literal_facts.update(_extract_literal_facts(symbols, source))
5067
- _all_guard_facts.update(_extract_guard_facts(symbols, source))
5068
- _all_class_type_refs.update(_extract_class_type_refs(symbols))
5069
- _all_annotation_arg_values.update(_parse_annotation_arg_values(symbols))
5070
- _f_imports = _file_import_map(source)
5071
- _all_field_types.update(_extract_field_types(source, imports=_f_imports))
5079
+ # V4b: the per-file body-fact extractors are pure functions of the file's
5080
+ # bytes, so their combined output is memoised under the same content key.
5081
+ # A warm hit skips all of them; the CIR is byte-identical (equivalence test).
5082
+ _fkey = _pc.file_key(rel_path, source, _capture_sig)
5083
+ _blob = _pc.get_blob(_fkey)
5084
+ if _blob is None:
5085
+ _bf, _sf = _extract_body_facts(symbols, source, rel_path)
5086
+ _f_imports = _file_import_map(source)
5087
+ _blob = {
5088
+ "body": _bf,
5089
+ "span": _sf,
5090
+ "literal": _extract_literal_facts(symbols, source),
5091
+ "guard": _extract_guard_facts(symbols, source),
5092
+ "ctref": _extract_class_type_refs(symbols),
5093
+ "annarg": _parse_annotation_arg_values(symbols),
5094
+ "ftypes": _extract_field_types(source, imports=_f_imports),
5095
+ "fimports": _f_imports,
5096
+ }
5097
+ _pc.put_blob(_fkey, _blob)
5098
+ _f_imports = _blob["fimports"]
5099
+ _all_body_facts.update(_blob["body"])
5100
+ _all_span_facts.update(_blob["span"])
5101
+ _all_literal_facts.update(_blob["literal"])
5102
+ _all_guard_facts.update(_blob["guard"])
5103
+ _all_class_type_refs.update(_blob["ctref"])
5104
+ _all_annotation_arg_values.update(_blob["annarg"])
5105
+ _all_field_types.update(_blob["ftypes"])
5072
5106
  # Every type declared in this file resolves simple names through this
5073
5107
  # file's imports — the fact `_static_typed_call_edges` needs to bind a
5074
5108
  # `Type.method(...)` receiver to a type (P1-A's rule, applied to statics).