sourcecode 2.6.1__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 (173) hide show
  1. {sourcecode-2.6.1 → sourcecode-2.6.3}/CHANGELOG.md +82 -0
  2. {sourcecode-2.6.1 → sourcecode-2.6.3}/PKG-INFO +1 -1
  3. {sourcecode-2.6.1 → sourcecode-2.6.3}/pyproject.toml +1 -1
  4. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/archetype.py +12 -1
  6. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/cli.py +310 -38
  7. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/context_cache.py +40 -0
  8. sourcecode-2.6.3/src/sourcecode/filter_surface.py +173 -0
  9. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/mcp/registry.py +49 -1
  10. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/migrate_check.py +10 -0
  11. sourcecode-2.6.3/src/sourcecode/parse_cache.py +138 -0
  12. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/prepare_context.py +21 -1
  13. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/repository_ir.py +46 -12
  14. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/errors.py +0 -7
  15. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/steps_endpoint.py +1 -7
  16. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/steps_impact.py +1 -7
  17. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/security_posture.py +96 -0
  18. sourcecode-2.6.3/src/sourcecode/verify_edit.py +588 -0
  19. sourcecode-2.6.3/src/sourcecode/verify_rules.py +304 -0
  20. sourcecode-2.6.1/src/sourcecode/retrieval/composition.py +0 -496
  21. {sourcecode-2.6.1 → sourcecode-2.6.3}/.github/workflows/build-windows.yml +0 -0
  22. {sourcecode-2.6.1 → sourcecode-2.6.3}/.gitignore +0 -0
  23. {sourcecode-2.6.1 → sourcecode-2.6.3}/.ruff.toml +0 -0
  24. {sourcecode-2.6.1 → sourcecode-2.6.3}/CONTRIBUTING.md +0 -0
  25. {sourcecode-2.6.1 → sourcecode-2.6.3}/LICENSE +0 -0
  26. {sourcecode-2.6.1 → sourcecode-2.6.3}/README.md +0 -0
  27. {sourcecode-2.6.1 → sourcecode-2.6.3}/SECURITY.md +0 -0
  28. {sourcecode-2.6.1 → sourcecode-2.6.3}/raw +0 -0
  29. {sourcecode-2.6.1 → sourcecode-2.6.3}/scripts/compare_integration_engines.py +0 -0
  30. {sourcecode-2.6.1 → sourcecode-2.6.3}/scripts/customer_smoke_test.sh +0 -0
  31. {sourcecode-2.6.1 → sourcecode-2.6.3}/scripts/generate_jdk_exports.py +0 -0
  32. {sourcecode-2.6.1 → sourcecode-2.6.3}/scripts/perf_harness.py +0 -0
  33. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/adaptive_scanner.py +0 -0
  34. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/architectural_baseline.py +0 -0
  35. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/architectural_delta.py +0 -0
  36. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/architecture_analyzer.py +0 -0
  37. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/architecture_summary.py +0 -0
  38. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/ast_extractor.py +0 -0
  39. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/cache.py +0 -0
  40. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/call_surface.py +0 -0
  41. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/caller_metrics.py +0 -0
  42. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/canonical_ir.py +0 -0
  43. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/change_plan.py +0 -0
  44. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/cir_graphs.py +0 -0
  45. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/classifier.py +0 -0
  46. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/code_notes_analyzer.py +0 -0
  47. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/compare.py +0 -0
  48. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/confidence_analyzer.py +0 -0
  49. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/constraint_diff.py +0 -0
  50. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/context_graph.py +0 -0
  51. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/context_scorer.py +0 -0
  52. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/context_summarizer.py +0 -0
  53. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/contract_diff.py +0 -0
  54. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/contract_model.py +0 -0
  55. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/contract_pipeline.py +0 -0
  56. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/coverage_parser.py +0 -0
  57. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/dependency_analyzer.py +0 -0
  58. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/__init__.py +0 -0
  59. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/base.py +0 -0
  60. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/csproj_parser.py +0 -0
  61. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/dart.py +0 -0
  62. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/dotnet.py +0 -0
  63. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/elixir.py +0 -0
  64. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/go.py +0 -0
  65. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/heuristic.py +0 -0
  66. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/hybrid.py +0 -0
  67. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/java.py +0 -0
  68. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/jvm_ext.py +0 -0
  69. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/nodejs.py +0 -0
  70. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/parsers.py +0 -0
  71. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/php.py +0 -0
  72. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/project.py +0 -0
  73. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/python.py +0 -0
  74. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/ruby.py +0 -0
  75. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/rust.py +0 -0
  76. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/systems.py +0 -0
  77. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/terraform.py +0 -0
  78. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/tooling.py +0 -0
  79. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/doc_analyzer.py +0 -0
  80. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/dynamic_argument_surface.py +0 -0
  81. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/endpoint_literals.py +0 -0
  82. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/endpoint_metrics.py +0 -0
  83. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/entrypoint_classifier.py +0 -0
  84. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/env_analyzer.py +0 -0
  85. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/error_schema.py +0 -0
  86. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/evidence_provider.py +0 -0
  87. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/explain.py +0 -0
  88. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/file_chunker.py +0 -0
  89. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/file_classifier.py +0 -0
  90. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/format_contract.py +0 -0
  91. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/fqn_utils.py +0 -0
  92. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/git_analyzer.py +0 -0
  93. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/graph_analyzer.py +0 -0
  94. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/graph_evidence.py +0 -0
  95. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/hibernate_strat.py +0 -0
  96. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/jdk_exports.py +0 -0
  97. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/license.py +0 -0
  98. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/mcp/__init__.py +0 -0
  99. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  100. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  101. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  102. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  103. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  104. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/mcp/orchestrator.py +0 -0
  105. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/mcp/runner.py +0 -0
  106. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/mcp/server.py +0 -0
  107. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/mcp_nudge.py +0 -0
  108. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/metrics_analyzer.py +0 -0
  109. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/openapi_surface.py +0 -0
  110. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/output_budget.py +0 -0
  111. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/path_filters.py +0 -0
  112. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/perf.py +0 -0
  113. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/pr_comment_renderer.py +0 -0
  114. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/pr_impact.py +0 -0
  115. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/progress.py +0 -0
  116. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/ranking_engine.py +0 -0
  117. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/reconciliation.py +0 -0
  118. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/redactor.py +0 -0
  119. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/relevance_scorer.py +0 -0
  120. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/rename_refactor.py +0 -0
  121. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/repo_classifier.py +0 -0
  122. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/__init__.py +0 -0
  123. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/context.py +0 -0
  124. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/executor.py +0 -0
  125. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/planner.py +0 -0
  126. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/query.py +0 -0
  127. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/request.py +0 -0
  128. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/resolution.py +0 -0
  129. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/result.py +0 -0
  130. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/retriever.py +0 -0
  131. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/runtime.py +0 -0
  132. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/steps.py +0 -0
  133. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/steps_graph.py +0 -0
  134. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/steps_intf.py +0 -0
  135. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/steps_struct.py +0 -0
  136. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/steps_txsec.py +0 -0
  137. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/ris.py +0 -0
  138. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/runtime_classifier.py +0 -0
  139. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/scanner.py +0 -0
  140. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/schema.py +0 -0
  141. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/security_config.py +0 -0
  142. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/semantic_analyzer.py +0 -0
  143. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/semantic_impact_engine.py +0 -0
  144. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/semantic_integration_engine.py +0 -0
  145. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/semantic_services.py +0 -0
  146. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/serializer.py +0 -0
  147. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/spring_event_topology.py +0 -0
  148. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/spring_findings.py +0 -0
  149. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/spring_impact.py +0 -0
  150. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/spring_model.py +0 -0
  151. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/spring_security_audit.py +0 -0
  152. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/spring_semantic.py +0 -0
  153. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/spring_tx_analyzer.py +0 -0
  154. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/summarizer.py +0 -0
  155. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/telemetry/__init__.py +0 -0
  156. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/telemetry/config.py +0 -0
  157. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/telemetry/consent.py +0 -0
  158. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/telemetry/events.py +0 -0
  159. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/telemetry/filters.py +0 -0
  160. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/telemetry/transport.py +0 -0
  161. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/tree_utils.py +0 -0
  162. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/type_usage_surface.py +0 -0
  163. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/validation_inference.py +0 -0
  164. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/validation_surface.py +0 -0
  165. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/version_check.py +0 -0
  166. {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/workspace.py +0 -0
  167. {sourcecode-2.6.1 → sourcecode-2.6.3}/supabase/functions/README.md +0 -0
  168. {sourcecode-2.6.1 → sourcecode-2.6.3}/supabase/functions/get-license/index.ts +0 -0
  169. {sourcecode-2.6.1 → sourcecode-2.6.3}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  170. {sourcecode-2.6.1 → sourcecode-2.6.3}/supabase/functions/telemetry/index.ts +0 -0
  171. {sourcecode-2.6.1 → sourcecode-2.6.3}/supabase/sql/license_event_ordering.sql +0 -0
  172. {sourcecode-2.6.1 → sourcecode-2.6.3}/supabase/sql/licensing_schema.sql +0 -0
  173. {sourcecode-2.6.1 → sourcecode-2.6.3}/supabase/sql/telemetry_events.sql +0 -0
@@ -1,5 +1,87 @@
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
+
56
+ ## [2.6.2] — 2026-07-23
57
+
58
+ **Field-eval Fase 3 — cache reuse (C) + cold-scan honesty (E). No
59
+ functional-contract change; both are safe, measured additions.** Suite:
60
+ **3679 passed, 5 skipped, 1 deselected** (the deselected chunker test is a
61
+ pre-existing external-fixture flake).
62
+
63
+ ### Changed
64
+
65
+ - **C — `review-pr` reuses the shared repo-wide CIR when it is already warm.**
66
+ The `review-pr` behavioral-impact path builds a *deliberately bounded* scope
67
+ (changed files + sibling directories, git-first, no full-repo traversal — the
68
+ bound is what keeps it fast). It now consults the shared Canonical IR through
69
+ a new get-only `context_cache.peek_cir`: on a warm cache the impact engine
70
+ runs over the full repo-wide graph for free (strictly more callers — closes
71
+ the DI-mediated blind spot) at O(1); on a miss it keeps its own scoped build.
72
+ `peek_cir` **never builds and never writes**, so a scoped caller can never
73
+ poison the repo-wide knowledge key that `explain`/`impact` read from — the
74
+ reason the building `get_or_build_cir` was the wrong tool for this path.
75
+
76
+ ### Added
77
+
78
+ - **E — cold-scan advisory on large repositories.** A cache hit short-circuits
79
+ before the file scan, so a full *cold* scan is where the wait lives. On a
80
+ large repo (≥ `_LARGE_REPO_ADVISORY_FILES` files) the CLI now prints a one-line
81
+ advisory that cold analysis scales with repo size and is cached afterwards.
82
+ Honest by construction: it states the *shape* of the cost, never an absolute
83
+ latency, and is **TTY-gated** so it never reaches stdout (JSON) or an agent.
84
+
3
85
  ## [2.6.1] — 2026-07-22
4
86
 
5
87
  **Stability patch — extraction/label fidelity on the 2.6.x line. No new
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 2.6.1
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.1"
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.1"
7
+ __version__ = "2.6.3"
@@ -599,7 +599,18 @@ class ArchetypeClassifier:
599
599
  # winner barely above the evidence floor is "undetermined-ish" and must
600
600
  # not read as high/medium however lonely it is.
601
601
  confident_enough = top.score >= _MIN_CONFIDENT_SCORE
602
- if confident_enough and rel_margin >= 0.4 and mass_backed and graph_backed:
602
+ # Signal diversity: "high" requires the label to be backed by MORE THAN ONE
603
+ # distinct signal. A lone dominant signal — however strong (e.g. a fan-in
604
+ # Gini alone labeling a REST API an "engine") — is a single point of view and
605
+ # must not read as high confidence (field-eval Fase 0).
606
+ distinct_signals = len({e.signal for e in top.evidence if e.contribution > 0})
607
+ if (
608
+ confident_enough
609
+ and rel_margin >= 0.4
610
+ and mass_backed
611
+ and graph_backed
612
+ and distinct_signals >= 2
613
+ ):
603
614
  confidence = "high"
604
615
  elif confident_enough and rel_margin >= 0.25 and mass_backed:
605
616
  confidence = "medium"
@@ -160,10 +160,11 @@ def _build_help_text() -> str:
160
160
  text = f"""\
161
161
  [bold]ASK Engine[/bold] [dim]· CLI: ask[/dim] {plan_badge}
162
162
 
163
- Persistent structural context and ultra-fast repeated analysis for AI coding agents.
163
+ Deterministic Java/Spring semantics and reusable structural context for AI coding agents.
164
164
 
165
- Cache warms on first scan; every subsequent call returns pre-built context in milliseconds.
166
- Cold scan: 2–10s depending on repo size. Warm cache: 0.3–0.6s.
165
+ Cache warms on first scan; later calls reuse pre-built context instead of rescanning.
166
+ Scan and warm time scale with repo size small repos in seconds, large repos (thousands
167
+ of files) in minutes. Semantic analysis itself is sub-second; repo indexing dominates.
167
168
 
168
169
  [bold]Primary usage:[/bold]
169
170
  ask --compact high-signal summary (~2,500–4,000 tokens)
@@ -253,6 +254,8 @@ _SUBCOMMANDS: frozenset[str] = frozenset(
253
254
  "explain",
254
255
  # Spring Boot 2→3 migration readiness
255
256
  "migrate-check",
257
+ # In-loop semantic diff gate (working tree vs HEAD)
258
+ "verify-edit",
256
259
  # Native file rename (BLOCKER-A)
257
260
  "rename-class",
258
261
  # Large file semantic chunking (BLOCKER-B)
@@ -725,6 +728,31 @@ DOCS_DEPTH_CHOICES = ["module", "symbols", "full"]
725
728
  # ── Module-level constants ─────────────────────────────────────────────────────
726
729
  _FREE_TIER_NODE_CAP: int = 50 # graph/semantic node cap — applies only to large repos on free tier
727
730
  _JAVA_MIN_SCAN_DEPTH: int = 12 # Maven src/main/java/<pkg>/<module>/File depth floor
731
+ _LARGE_REPO_ADVISORY_FILES: int = 3000 # cold-scan advisory threshold (files); honest expectation-setting, not a speed claim
732
+
733
+
734
+ def _cold_scan_advisory_message(
735
+ file_tree: dict,
736
+ *,
737
+ file_count: Optional[int] = None,
738
+ ) -> Optional[str]:
739
+ """Return the large-repo cold-scan advisory line, or ``None`` when the repo is
740
+ below :data:`_LARGE_REPO_ADVISORY_FILES`.
741
+
742
+ Honest expectation-setting (field-eval Fase 3, item E): it states the *shape* of
743
+ the cost — cold analysis scales with repo size, cached afterwards — and never
744
+ asserts a duration the tool cannot keep. Pure and TTY-agnostic: the caller owns
745
+ the ``sys.stderr.isatty()`` gate so this never reaches stdout (JSON) or an agent.
746
+ """
747
+ if file_count is None:
748
+ from sourcecode.tree_utils import flatten_file_tree as _flatten_ft
749
+ file_count = sum(1 for _p in _flatten_ft(file_tree) if Path(_p).suffix)
750
+ if file_count < _LARGE_REPO_ADVISORY_FILES:
751
+ return None
752
+ return (
753
+ f"[scan] large repository (~{file_count:,} files) — cold analysis scales with "
754
+ "repo size and may take a while; the result is cached, so later runs are fast."
755
+ )
728
756
  _JVM_STACKS: frozenset[str] = frozenset({"java", "kotlin", "scala", "groovy"})
729
757
  _IMPACT_PRIORITY_THRESHOLDS: list[tuple[float, str]] = [
730
758
  (0.60, "high"),
@@ -762,7 +790,7 @@ def _print_welcome_plain(tier: str) -> None:
762
790
  """Plain-text welcome — fallback when rich is unavailable."""
763
791
  lines = ["", f" ASK Engine {__version__} · {tier} · CLI: ask",
764
792
  " Actionable Software Knowledge Engine", "",
765
- " AI coding-agent context, instant.", "", " Get started:"]
793
+ " Deep Java/Spring semantics for AI coding agents.", "", " Get started:"]
766
794
  for cmd, desc in _WELCOME_CMDS:
767
795
  lines.append(f" {cmd.ljust(34)}{desc}")
768
796
  lines.append("")
@@ -812,7 +840,7 @@ def _print_welcome() -> None:
812
840
  t.append(_WELCOME_ENGINE, style="bold dim cyan")
813
841
  t.append(" Actionable Software Knowledge\n", style="dim")
814
842
 
815
- t.append("\nAI coding-agent context, instant.\n\n", style="white")
843
+ t.append("\nDeep Java/Spring semantics for AI coding agents.\n\n", style="white")
816
844
 
817
845
  for cmd, desc in _WELCOME_CMDS:
818
846
  t.append("▸ ", style="cyan")
@@ -1483,23 +1511,60 @@ def main(
1483
1511
  # Step 1: try L1 to obtain the core_hash needed for L2 key
1484
1512
  _l1_result = _cache_mod.read_core(target, _core_key)
1485
1513
 
1486
- # P1-A: --env-map misses L1 when base (em=False) exists.
1487
- # Try the base key so env analysis can be injected lazily (<1 s)
1488
- # instead of triggering a 17 s full rescan.
1514
+ # Additive overlays (--env-map / --git-context) miss L1 because they sit in
1515
+ # the core key, yet neither changes the semantic core env walks config
1516
+ # files, git walks history; both only ATTACH a block. So when the base
1517
+ # (em=False,gc=False) core exists, reuse it and inject the overlay lazily
1518
+ # (<1 s each) instead of triggering a full rescan. (--changed-only is a
1519
+ # different subsystem and is handled separately.)
1520
+ # Try candidate bases that differ from the current flags only by having
1521
+ # some subset of the requested overlays OFF, fewest flips first — so the
1522
+ # RICHEST already-cached core wins (e.g. with --compact --git-context, the
1523
+ # plain --compact core has em=True,gc=False, so we flip only gc and keep em).
1524
+ # Injecting a flag whose base already has it ON would corrupt the reuse, so
1525
+ # we inject exactly the overlays we flipped to land the hit.
1489
1526
  _l1_needs_env_inject = False
1490
- if _l1_result is None and env_map:
1491
- _base_flags = _core_flags_str.replace(",em=True,", ",em=False,")
1492
- _base_h8 = _hashlib.sha256(_base_flags.encode()).hexdigest()[:8]
1527
+ _l1_needs_git_inject = False
1528
+ if _l1_result is None and (env_map or git_context):
1493
1529
  _sha_prefix = _git_sha if _git_sha else "nogit"
1494
- _base_key = f"{_sha_prefix}-{_base_h8}"
1495
- _base_result = _cache_mod.read_core(target, _base_key)
1496
- if _base_result is not None:
1497
- _l1_result = _base_result
1498
- _l1_needs_env_inject = True
1530
+ _flippable = []
1531
+ if git_context:
1532
+ _flippable.append("gc") # inject git is cheap + additive
1533
+ if env_map:
1534
+ _flippable.append("em")
1535
+ # non-empty subsets, ascending size (fewest flips = richest base first)
1536
+ import itertools as _it
1537
+ _subsets = [
1538
+ s for n in range(1, len(_flippable) + 1)
1539
+ for s in _it.combinations(_flippable, n)
1540
+ ]
1541
+ for _subset in _subsets:
1542
+ _bf = _core_flags_str
1543
+ if "gc" in _subset:
1544
+ _bf = _bf.replace(",gc=True,", ",gc=False,")
1545
+ if "em" in _subset:
1546
+ _bf = _bf.replace(",em=True,", ",em=False,")
1547
+ _bk = f"{_sha_prefix}-{_hashlib.sha256(_bf.encode()).hexdigest()[:8]}"
1548
+ _br = _cache_mod.read_core(target, _bk)
1549
+ if _br is not None:
1550
+ _l1_result = _br
1551
+ _l1_needs_git_inject = "gc" in _subset
1552
+ _l1_needs_env_inject = "em" in _subset
1553
+ break
1499
1554
 
1500
1555
  if _l1_result is not None:
1501
1556
  _core_dict_l1, _core_hash = _l1_result
1502
1557
  _view_key = f"{_core_hash}-{_view_h}"
1558
+ # On the lazy-inject path the core is a REUSED base whose _core_hash is
1559
+ # shared with the base's overlay-free view — suffix the view key with the
1560
+ # injected overlays so the injected body never collides with (or is served
1561
+ # from) the base view. Non-inject runs keep their exact historical key, so
1562
+ # existing cached views stay valid (no cross-version churn).
1563
+ if _l1_needs_git_inject or _l1_needs_env_inject:
1564
+ _view_key = (
1565
+ f"{_view_key}-ov:gc{int(_l1_needs_git_inject)}"
1566
+ f"em{int(_l1_needs_env_inject)}"
1567
+ )
1503
1568
 
1504
1569
  # Step 2: try L2 (exact view match).
1505
1570
  # Skip L2 for --changed-only: the stored view is a previous
@@ -1553,6 +1618,23 @@ def main(
1553
1618
  ]
1554
1619
  except Exception:
1555
1620
  pass # env inject failed — continue without env data
1621
+ # P-gc: inject git context when a gc=False base L1 was reused.
1622
+ # GitAnalyzer walks history only — typically <1 s. Reuses the
1623
+ # serializer's own compact shaping (no duplication/drift).
1624
+ if _rebuilt is not None and _l1_needs_git_inject and compact:
1625
+ try:
1626
+ from types import SimpleNamespace as _NS_gc
1627
+ from sourcecode.git_analyzer import GitAnalyzer as _GitA_gc
1628
+ from sourcecode.serializer import _compact_git_context as _cgc
1629
+ _gc_obj = _GitA_gc().analyze(
1630
+ target, depth=git_depth, days=git_days
1631
+ )
1632
+ _gc_block = _cgc(_NS_gc(git_context=_gc_obj))
1633
+ if _gc_block:
1634
+ _rebuilt = dict(_rebuilt)
1635
+ _rebuilt["git_context"] = _gc_block
1636
+ except Exception:
1637
+ pass # git inject failed — continue without git data
1556
1638
  if _rebuilt is not None:
1557
1639
  # Apply redaction
1558
1640
  if not no_redact:
@@ -1729,6 +1811,17 @@ def main(
1729
1811
  # 2. Filter .env and *.secret entries from file tree (SEC-02, all levels)
1730
1812
  file_tree = filter_sensitive_files(raw_tree, redactor)
1731
1813
  perf.stop("discovery", _perf_discovery)
1814
+
1815
+ # Cold-scan advisory (honest expectation-setting, NOT a speed claim). A cache
1816
+ # hit short-circuits above (line ~1647) before this scan, so reaching here means
1817
+ # a cold run: the full parse+analysis below scales with repo size. On a large
1818
+ # repo, warn once on the TTY so the wait is expected — never on stdout (JSON) and
1819
+ # never for agents/pipes. This does not assert a duration; it states the shape.
1820
+ if sys.stderr.isatty() and not no_cache:
1821
+ _advisory = _cold_scan_advisory_message(file_tree)
1822
+ if _advisory:
1823
+ typer.echo(_advisory, err=True)
1824
+
1732
1825
  _perf_detection = perf.start()
1733
1826
  detector = ProjectDetector(build_default_detectors())
1734
1827
  workspace_analysis = WorkspaceAnalyzer().analyze(target, manifests)
@@ -5295,22 +5388,23 @@ def _render_spring_audit_github_comment(result: "SpringAuditResult", min_severit
5295
5388
  lines.append("")
5296
5389
 
5297
5390
  if not visible:
5391
+ # No findings at/above severity — but a gate-coverage escape set (a separate
5392
+ # signal, not a "finding") may still be worth surfacing before the footer.
5298
5393
  lines.append(f"_No findings at or above `{min_severity}` severity._")
5299
- return "\n".join(lines)
5300
-
5301
- lines += [
5302
- "| Sev | Pattern | File | Symbol | Title |",
5303
- "|-----|---------|------|--------|-------|",
5304
- ]
5305
- for f in sorted(visible, key=lambda x: (SEVERITY_ORDER.get(x.severity, 3), x.source_file)):
5306
- icon = _ICONS.get(f.severity, "")
5307
- label = _LABELS.get(f.severity, f.severity.upper())
5308
- short_file = f.source_file.split("/")[-1] if "/" in f.source_file else f.source_file
5309
- short_sym = f.symbol.split(".")[-1] if "." in f.symbol else f.symbol
5310
- title_escaped = f.title.replace("|", "\\|")
5311
- lines.append(f"| {icon} {label} | `{f.pattern_id}` | `{short_file}` | `{short_sym}` | {title_escaped} |")
5394
+ else:
5395
+ lines += [
5396
+ "| Sev | Pattern | File | Symbol | Title |",
5397
+ "|-----|---------|------|--------|-------|",
5398
+ ]
5399
+ for f in sorted(visible, key=lambda x: (SEVERITY_ORDER.get(x.severity, 3), x.source_file)):
5400
+ icon = _ICONS.get(f.severity, "")
5401
+ label = _LABELS.get(f.severity, f.severity.upper())
5402
+ short_file = f.source_file.split("/")[-1] if "/" in f.source_file else f.source_file
5403
+ short_sym = f.symbol.split(".")[-1] if "." in f.symbol else f.symbol
5404
+ title_escaped = f.title.replace("|", "\\|")
5405
+ lines.append(f"| {icon} {label} | `{f.pattern_id}` | `{short_file}` | `{short_sym}` | {title_escaped} |")
5312
5406
 
5313
- lines.append("")
5407
+ lines.append("")
5314
5408
 
5315
5409
  if visible:
5316
5410
  lines.append("<details>")
@@ -5327,6 +5421,8 @@ def _render_spring_audit_github_comment(result: "SpringAuditResult", min_severit
5327
5421
  lines.append("")
5328
5422
  lines.append("</details>")
5329
5423
 
5424
+ lines += _render_gate_coverage_section(result)
5425
+
5330
5426
  lines += [
5331
5427
  "",
5332
5428
  f"_Generated by [sourcecode](https://github.com/sourcecode-ai/sourcecode) · "
@@ -5335,6 +5431,57 @@ def _render_spring_audit_github_comment(result: "SpringAuditResult", min_severit
5335
5431
  return "\n".join(lines)
5336
5432
 
5337
5433
 
5434
+ _GATE_COVERAGE_RENDER_CAP = 25
5435
+
5436
+
5437
+ def _render_gate_coverage_section(result: "SpringAuditResult") -> list[str]: # type: ignore[name-defined]
5438
+ """Human-readable gate-coverage block: the controller handlers that do NOT carry
5439
+ the auto-detected custom authorization gate — the escape set the JSON already
5440
+ computes, surfaced so a reviewer sees it at a glance. Never says "unsecured": a
5441
+ handler a servlet filter pattern covers is marked "possibly filter-covered", and the
5442
+ unmatched ones are flagged for review, not condemned."""
5443
+ gc = (result.security_posture or {}).get("gate_coverage")
5444
+ if not gc:
5445
+ return []
5446
+ not_covered = gc.get("not_carrying_gate", 0)
5447
+ gates = ", ".join(f"`{g}`" for g in gc.get("gate_annotations", [])) or "the detected gate"
5448
+ total = gc.get("total_controller_handlers", 0)
5449
+ lines: list[str] = ["", "---", ""]
5450
+ if not_covered == 0:
5451
+ lines.append(f"✅ **Gate coverage** — all {total} controller handlers carry {gates}.")
5452
+ return lines
5453
+
5454
+ covered = gc.get("possibly_filter_covered", 0)
5455
+ lines.append(
5456
+ f"🔓 **Gate coverage** — {not_covered} of {total} controller handlers do not carry "
5457
+ f"{gates}."
5458
+ )
5459
+ if gc.get("reconstructed_filter_patterns"):
5460
+ lines.append(
5461
+ f"_{covered} of those match a reconstructed servlet filter pattern "
5462
+ f"(possibly filter-covered); {gc.get('no_matching_filter_pattern', 0)} match none._"
5463
+ )
5464
+ lines += ["", "<details>", "<summary>Handlers without the gate</summary>", ""]
5465
+ lines += [
5466
+ "| Method | Path | Filter pattern | Policy |",
5467
+ "|--------|------|----------------|--------|",
5468
+ ]
5469
+ handlers = gc.get("handlers_without_gate", [])
5470
+ for h in handlers[:_GATE_COVERAGE_RENDER_CAP]:
5471
+ method = (h.get("method") or "").replace("|", "\\|")
5472
+ path = (h.get("path") or "").replace("|", "\\|")
5473
+ fpat = h.get("filter_pattern_match")
5474
+ fcell = f"`{fpat}`" if fpat else "—"
5475
+ policy = (h.get("policy") or "").replace("|", "\\|")
5476
+ lines.append(f"| {method} | `{path}` | {fcell} | {policy} |")
5477
+ if len(handlers) > _GATE_COVERAGE_RENDER_CAP:
5478
+ lines.append(f"| … | _+{len(handlers) - _GATE_COVERAGE_RENDER_CAP} more_ | | |")
5479
+ lines += ["", "</details>", ""]
5480
+ if gc.get("note"):
5481
+ lines.append(f"> {gc['note']}")
5482
+ return lines
5483
+
5484
+
5338
5485
  @app.command("spring-audit")
5339
5486
  def spring_audit_cmd(
5340
5487
  path: Path = typer.Argument(
@@ -5533,6 +5680,85 @@ def spring_audit_cmd(
5533
5680
  raise typer.Exit(code=1)
5534
5681
 
5535
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
+
5536
5762
  # ── Spring Boot Migration Check ───────────────────────────────────────────────
5537
5763
 
5538
5764
 
@@ -5804,7 +6030,14 @@ def impact_chain_cmd(
5804
6030
 
5805
6031
  _prog = Progress()
5806
6032
  _prog.start(f"analyzing impact ({len(file_list)} files)")
5807
- cir = ContextGraph.build(file_list, target).cir
6033
+ # Reuse the shared context-cache CIR (same entry `cache warm` and `explain` use) so
6034
+ # a warmed repo skips the expensive Java parse. Best-effort — any fault falls back to
6035
+ # a fresh build, exactly as explain does, so impact-chain never breaks.
6036
+ from sourcecode import context_cache as _ctxcache
6037
+ try:
6038
+ cir, _ = _ctxcache.get_or_build_cir(_resolve_repo_root(target), target, file_list)
6039
+ except Exception:
6040
+ cir = ContextGraph.build(file_list, target).cir
5808
6041
  _model = SpringSemanticModel.build(cir)
5809
6042
 
5810
6043
  if query_type == "events":
@@ -8215,6 +8448,22 @@ def cache_clear_cmd(
8215
8448
  typer.echo(f"Removed {removed} file(s).", err=True)
8216
8449
 
8217
8450
 
8451
+ def _warm_shared_cir(target: Path):
8452
+ """Pre-populate the shared Canonical IR in the context cache (the entry explain and
8453
+ other knowledge commands reuse). Returns the KnowledgeLookup, or None on any fault —
8454
+ warming is best-effort and never fails the warm command."""
8455
+ try:
8456
+ from sourcecode.repository_ir import find_java_files as _fjf
8457
+ from sourcecode import context_cache as _ctxcache
8458
+ files = _fjf(target)
8459
+ if not files:
8460
+ return None
8461
+ _cir, look = _ctxcache.get_or_build_cir(_resolve_repo_root(target), target, files)
8462
+ return look
8463
+ except Exception:
8464
+ return None
8465
+
8466
+
8218
8467
  @cache_app.command("warm")
8219
8468
  def cache_warm_cmd(
8220
8469
  path: Path = typer.Argument(Path("."), help="Repository path to warm (default: current directory)"),
@@ -8245,6 +8494,18 @@ def cache_warm_cmd(
8245
8494
  typer.echo(result.stderr.strip(), err=True)
8246
8495
  raise typer.Exit(code=result.returncode)
8247
8496
 
8497
+ # Also pre-populate the shared Canonical IR (context cache) — a DIFFERENT cache from
8498
+ # the L1/L2 core cache warmed above. explain (and, going forward, other knowledge
8499
+ # commands) reuse this CIR; without warming it here, the first explain rebuilds the
8500
+ # expensive Java parse despite a "warm" cache (field-eval: explain MISS after warm).
8501
+ _look = _warm_shared_cir(target)
8502
+ if _look is not None:
8503
+ typer.echo(
8504
+ f"Shared CIR {'reused' if _look.hit else 'built'} "
8505
+ "(explain/impact reuse this).",
8506
+ err=True,
8507
+ )
8508
+
8248
8509
 
8249
8510
  @cache_app.command("freshness")
8250
8511
  def cache_freshness_cmd(
@@ -8386,6 +8647,23 @@ def _stderr_is_interactive() -> bool:
8386
8647
  return False
8387
8648
 
8388
8649
 
8650
+ def _force_utf8_streams() -> None:
8651
+ """Force UTF-8 on stdout AND stderr so Unicode characters (em-dash, arrows, box
8652
+ drawing) survive on Windows where the default console codec is cp1252 (BUG-1).
8653
+
8654
+ stderr matters as much as stdout: progress/warn/gap status text is emitted there,
8655
+ and a cp1252 encoder turned its em-dashes into the replacement char (field-eval).
8656
+ Best-effort and idempotent — a stream without ``reconfigure`` (already wrapped, or a
8657
+ test buffer) is skipped silently.
8658
+ """
8659
+ for stream in (sys.stdout, sys.stderr):
8660
+ if hasattr(stream, "reconfigure"):
8661
+ try:
8662
+ stream.reconfigure(encoding="utf-8")
8663
+ except Exception:
8664
+ pass
8665
+
8666
+
8389
8667
  def main_entry() -> None:
8390
8668
  """CLI entry point for both the canonical ``ask`` command and the deprecated
8391
8669
  ``sourcecode`` compat alias — one implementation, no duplication.
@@ -8395,13 +8673,7 @@ def main_entry() -> None:
8395
8673
  can consume them as positional arguments (which would prevent subcommand
8396
8674
  routing for tokens like 'version' or 'config').
8397
8675
  """
8398
- # Force UTF-8 on stdout so Unicode characters (arrows, etc.) survive on
8399
- # Windows where the default console codec is cp1252 (BUG-1).
8400
- if hasattr(sys.stdout, "reconfigure"):
8401
- try:
8402
- sys.stdout.reconfigure(encoding="utf-8")
8403
- except Exception:
8404
- pass
8676
+ _force_utf8_streams()
8405
8677
  # Deprecation notice when invoked through the legacy `sourcecode` alias.
8406
8678
  # One line, and only on an interactive terminal — pipes/agents that still call
8407
8679
  # `sourcecode` keep clean stderr (error envelopes are JSON on stderr), so the
@@ -620,6 +620,46 @@ def get_or_build_cir(
620
620
  return cir, KnowledgeLookup(hit=False, enabled=True, build_ms=build_ms)
621
621
 
622
622
 
623
+ def peek_cir(
624
+ repo_root: Path,
625
+ *,
626
+ since: Optional[str] = None,
627
+ ) -> Optional[Any]:
628
+ """Return the shared ``CanonicalRepositoryIR`` **only if already cached**.
629
+
630
+ Get-only sibling of :func:`get_or_build_cir`: it never builds and never
631
+ writes. A caller that holds only a *bounded scope* of the repo (e.g. the
632
+ review-pr git-first path) must use this — routing a scoped build through
633
+ ``get_or_build_cir`` would store a truncated CIR under the repo-wide
634
+ knowledge key and poison every command that reads it. On a hit the caller
635
+ gets the full repo-wide CIR for free (O(1) reconstruction from raw IR); on
636
+ a miss it gets ``None`` and keeps its own scoped build, unshared.
637
+
638
+ The reconstructed CIR is left repo-consistent (``file_paths=None`` derives
639
+ the file list from the cached nodes, not from any caller scope).
640
+ """
641
+ from sourcecode.canonical_ir import ir_dict_to_canonical, validate_canonical_ir
642
+
643
+ cache = ContextCache.for_repo(repo_root)
644
+ if not cache.enabled:
645
+ return None
646
+ key = cache.knowledge_key(SCOPE_JAVA_CIR, options={"since": since} if since else None)
647
+ cached = cache.get(key)
648
+ if cached is None:
649
+ return None
650
+ raw = cached.payload.get("raw_ir")
651
+ if not isinstance(raw, dict):
652
+ return None
653
+ try:
654
+ cir = ir_dict_to_canonical(raw, file_paths=None)
655
+ # validate_canonical_ir returns a LIST of problems — empty means valid.
656
+ if not validate_canonical_ir(cir):
657
+ return cir
658
+ except Exception:
659
+ pass # corrupt/incompatible payload — treat as miss
660
+ return None
661
+
662
+
623
663
  # ---------------------------------------------------------------------------
624
664
  # Module-level observability helper
625
665
  # ---------------------------------------------------------------------------