sourcecode 2.6.0__tar.gz → 2.6.2__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.
- {sourcecode-2.6.0 → sourcecode-2.6.2}/CHANGELOG.md +64 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/PKG-INFO +1 -1
- {sourcecode-2.6.0 → sourcecode-2.6.2}/pyproject.toml +1 -1
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/__init__.py +1 -1
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/archetype.py +12 -1
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/cli.py +229 -38
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/context_cache.py +40 -0
- sourcecode-2.6.2/src/sourcecode/filter_surface.py +173 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/migrate_check.py +10 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/prepare_context.py +21 -1
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/repository_ir.py +32 -5
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/security_posture.py +96 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/spring_impact.py +16 -8
- {sourcecode-2.6.0 → sourcecode-2.6.2}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/.gitignore +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/.ruff.toml +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/CONTRIBUTING.md +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/LICENSE +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/README.md +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/SECURITY.md +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/raw +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/scripts/compare_integration_engines.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/scripts/customer_smoke_test.sh +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/scripts/generate_jdk_exports.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/scripts/perf_harness.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/architectural_baseline.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/architectural_delta.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/cache.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/call_surface.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/caller_metrics.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/change_plan.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/classifier.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/compare.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/constraint_diff.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/context_graph.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/contract_diff.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/dynamic_argument_surface.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/endpoint_literals.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/endpoint_metrics.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/evidence_provider.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/explain.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/format_contract.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/graph_evidence.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/hibernate_strat.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/jdk_exports.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/license.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/openapi_surface.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/perf.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/progress.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/reconciliation.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/redactor.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/retrieval/__init__.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/retrieval/context.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/retrieval/errors.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/retrieval/executor.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/retrieval/planner.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/retrieval/query.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/retrieval/request.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/retrieval/resolution.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/retrieval/result.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/retrieval/retriever.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/retrieval/runtime.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/retrieval/steps.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/retrieval/steps_graph.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/retrieval/steps_impact.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/retrieval/steps_intf.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/retrieval/steps_struct.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/retrieval/steps_txsec.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/ris.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/scanner.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/schema.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/security_config.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/semantic_impact_engine.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/semantic_integration_engine.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/semantic_services.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/serializer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/spring_tx_analyzer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/type_usage_surface.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/validation_inference.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/validation_surface.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/version_check.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/src/sourcecode/workspace.py +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/supabase/functions/README.md +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/supabase/functions/telemetry/index.ts +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/supabase/sql/license_event_ordering.sql +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/supabase/sql/licensing_schema.sql +0 -0
- {sourcecode-2.6.0 → sourcecode-2.6.2}/supabase/sql/telemetry_events.sql +0 -0
|
@@ -1,5 +1,69 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.6.2] — 2026-07-23
|
|
4
|
+
|
|
5
|
+
**Field-eval Fase 3 — cache reuse (C) + cold-scan honesty (E). No
|
|
6
|
+
functional-contract change; both are safe, measured additions.** Suite:
|
|
7
|
+
**3679 passed, 5 skipped, 1 deselected** (the deselected chunker test is a
|
|
8
|
+
pre-existing external-fixture flake).
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **C — `review-pr` reuses the shared repo-wide CIR when it is already warm.**
|
|
13
|
+
The `review-pr` behavioral-impact path builds a *deliberately bounded* scope
|
|
14
|
+
(changed files + sibling directories, git-first, no full-repo traversal — the
|
|
15
|
+
bound is what keeps it fast). It now consults the shared Canonical IR through
|
|
16
|
+
a new get-only `context_cache.peek_cir`: on a warm cache the impact engine
|
|
17
|
+
runs over the full repo-wide graph for free (strictly more callers — closes
|
|
18
|
+
the DI-mediated blind spot) at O(1); on a miss it keeps its own scoped build.
|
|
19
|
+
`peek_cir` **never builds and never writes**, so a scoped caller can never
|
|
20
|
+
poison the repo-wide knowledge key that `explain`/`impact` read from — the
|
|
21
|
+
reason the building `get_or_build_cir` was the wrong tool for this path.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **E — cold-scan advisory on large repositories.** A cache hit short-circuits
|
|
26
|
+
before the file scan, so a full *cold* scan is where the wait lives. On a
|
|
27
|
+
large repo (≥ `_LARGE_REPO_ADVISORY_FILES` files) the CLI now prints a one-line
|
|
28
|
+
advisory that cold analysis scales with repo size and is cached afterwards.
|
|
29
|
+
Honest by construction: it states the *shape* of the cost, never an absolute
|
|
30
|
+
latency, and is **TTY-gated** so it never reaches stdout (JSON) or an agent.
|
|
31
|
+
|
|
32
|
+
## [2.6.1] — 2026-07-22
|
|
33
|
+
|
|
34
|
+
**Stability patch — extraction/label fidelity on the 2.6.x line. No new
|
|
35
|
+
capability, no functional-contract change.** Two isolated corrections where the
|
|
36
|
+
system produced a correct result but described it imperfectly (P1-H) or gave a
|
|
37
|
+
provably-correct answer a misleading label (SIM-4). `2.6.0` remains the stable
|
|
38
|
+
baseline; this is a fidelity patch on top of it. Full suite: **3637 passed, 5
|
|
39
|
+
skipped, 1 deselected** (the deselected chunker test is a pre-existing external
|
|
40
|
+
fixture flake).
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- **P1-H — lowercase-initial type declarations were silently dropped.**
|
|
45
|
+
`_CLASS_DECL_RE` assumed the PascalCase convention (`[A-Z]\w*`) for the
|
|
46
|
+
type-name start, so valid lowercase-initial Java types never entered the
|
|
47
|
+
symbol graph (e.g. `iFieldMetadata`, `i18nUpdateCartServiceExtensionHandler`;
|
|
48
|
+
JNA C-struct mirrors `passwd`/`group`/`pam_*`). The same assumption was
|
|
49
|
+
duplicated in the multi-line-join gate, so a lowercase declaration whose brace
|
|
50
|
+
sat on a continuation line stayed lost even after widening the primary regex.
|
|
51
|
+
Both name-start charsets widened to `[A-Za-z]\w*`, and a length-preserving
|
|
52
|
+
string-literal blanking guard was added at the declaration-scan sites so a
|
|
53
|
+
`class`/`interface` keyword appearing inside a string literal (e.g. a log
|
|
54
|
+
message `"... advice class for {} ..."`) can no longer mint a phantom type.
|
|
55
|
+
Fleet A/B over six real repositories: +9 real types recovered, zero phantoms,
|
|
56
|
+
zero symbols lost, control repositories byte-stable.
|
|
57
|
+
- **SIM-4 — a method-precise resolution was labelled `class_expanded`.** When a
|
|
58
|
+
`Class#method` query matched one class by short/suffix name and its method
|
|
59
|
+
resolved to exactly one method node, `impact-chain` returned the correct
|
|
60
|
+
method-scoped result but tagged it `class_expanded` — signalling "I broadened
|
|
61
|
+
your query to the whole class" over a narrow, correct answer. A distinct
|
|
62
|
+
resolution value `method_resolved` now describes that case (confidence stays
|
|
63
|
+
high; nothing was widened); the genuine no-method class-expansion case keeps
|
|
64
|
+
`class_expanded`. No behavioural change — the resolved symbol set, confidence,
|
|
65
|
+
and callers are identical; only the exposed label changed to match reality.
|
|
66
|
+
|
|
3
67
|
## [2.6.0] — 2026-07-22
|
|
4
68
|
|
|
5
69
|
**Engineering Decision Support — Part B (capability milestone) lands, plus the
|
|
@@ -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
|
-
|
|
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
|
-
|
|
163
|
+
Deterministic Java/Spring semantics and reusable structural context for AI coding agents.
|
|
164
164
|
|
|
165
|
-
Cache warms on first scan;
|
|
166
|
-
|
|
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)
|
|
@@ -725,6 +726,31 @@ DOCS_DEPTH_CHOICES = ["module", "symbols", "full"]
|
|
|
725
726
|
# ── Module-level constants ─────────────────────────────────────────────────────
|
|
726
727
|
_FREE_TIER_NODE_CAP: int = 50 # graph/semantic node cap — applies only to large repos on free tier
|
|
727
728
|
_JAVA_MIN_SCAN_DEPTH: int = 12 # Maven src/main/java/<pkg>/<module>/File depth floor
|
|
729
|
+
_LARGE_REPO_ADVISORY_FILES: int = 3000 # cold-scan advisory threshold (files); honest expectation-setting, not a speed claim
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def _cold_scan_advisory_message(
|
|
733
|
+
file_tree: dict,
|
|
734
|
+
*,
|
|
735
|
+
file_count: Optional[int] = None,
|
|
736
|
+
) -> Optional[str]:
|
|
737
|
+
"""Return the large-repo cold-scan advisory line, or ``None`` when the repo is
|
|
738
|
+
below :data:`_LARGE_REPO_ADVISORY_FILES`.
|
|
739
|
+
|
|
740
|
+
Honest expectation-setting (field-eval Fase 3, item E): it states the *shape* of
|
|
741
|
+
the cost — cold analysis scales with repo size, cached afterwards — and never
|
|
742
|
+
asserts a duration the tool cannot keep. Pure and TTY-agnostic: the caller owns
|
|
743
|
+
the ``sys.stderr.isatty()`` gate so this never reaches stdout (JSON) or an agent.
|
|
744
|
+
"""
|
|
745
|
+
if file_count is None:
|
|
746
|
+
from sourcecode.tree_utils import flatten_file_tree as _flatten_ft
|
|
747
|
+
file_count = sum(1 for _p in _flatten_ft(file_tree) if Path(_p).suffix)
|
|
748
|
+
if file_count < _LARGE_REPO_ADVISORY_FILES:
|
|
749
|
+
return None
|
|
750
|
+
return (
|
|
751
|
+
f"[scan] large repository (~{file_count:,} files) — cold analysis scales with "
|
|
752
|
+
"repo size and may take a while; the result is cached, so later runs are fast."
|
|
753
|
+
)
|
|
728
754
|
_JVM_STACKS: frozenset[str] = frozenset({"java", "kotlin", "scala", "groovy"})
|
|
729
755
|
_IMPACT_PRIORITY_THRESHOLDS: list[tuple[float, str]] = [
|
|
730
756
|
(0.60, "high"),
|
|
@@ -762,7 +788,7 @@ def _print_welcome_plain(tier: str) -> None:
|
|
|
762
788
|
"""Plain-text welcome — fallback when rich is unavailable."""
|
|
763
789
|
lines = ["", f" ASK Engine {__version__} · {tier} · CLI: ask",
|
|
764
790
|
" Actionable Software Knowledge Engine", "",
|
|
765
|
-
" AI coding
|
|
791
|
+
" Deep Java/Spring semantics for AI coding agents.", "", " Get started:"]
|
|
766
792
|
for cmd, desc in _WELCOME_CMDS:
|
|
767
793
|
lines.append(f" {cmd.ljust(34)}{desc}")
|
|
768
794
|
lines.append("")
|
|
@@ -812,7 +838,7 @@ def _print_welcome() -> None:
|
|
|
812
838
|
t.append(_WELCOME_ENGINE, style="bold dim cyan")
|
|
813
839
|
t.append(" Actionable Software Knowledge\n", style="dim")
|
|
814
840
|
|
|
815
|
-
t.append("\
|
|
841
|
+
t.append("\nDeep Java/Spring semantics for AI coding agents.\n\n", style="white")
|
|
816
842
|
|
|
817
843
|
for cmd, desc in _WELCOME_CMDS:
|
|
818
844
|
t.append("▸ ", style="cyan")
|
|
@@ -1483,23 +1509,60 @@ def main(
|
|
|
1483
1509
|
# Step 1: try L1 to obtain the core_hash needed for L2 key
|
|
1484
1510
|
_l1_result = _cache_mod.read_core(target, _core_key)
|
|
1485
1511
|
|
|
1486
|
-
#
|
|
1487
|
-
#
|
|
1488
|
-
#
|
|
1512
|
+
# Additive overlays (--env-map / --git-context) miss L1 because they sit in
|
|
1513
|
+
# the core key, yet neither changes the semantic core — env walks config
|
|
1514
|
+
# files, git walks history; both only ATTACH a block. So when the base
|
|
1515
|
+
# (em=False,gc=False) core exists, reuse it and inject the overlay lazily
|
|
1516
|
+
# (<1 s each) instead of triggering a full rescan. (--changed-only is a
|
|
1517
|
+
# different subsystem and is handled separately.)
|
|
1518
|
+
# Try candidate bases that differ from the current flags only by having
|
|
1519
|
+
# some subset of the requested overlays OFF, fewest flips first — so the
|
|
1520
|
+
# RICHEST already-cached core wins (e.g. with --compact --git-context, the
|
|
1521
|
+
# plain --compact core has em=True,gc=False, so we flip only gc and keep em).
|
|
1522
|
+
# Injecting a flag whose base already has it ON would corrupt the reuse, so
|
|
1523
|
+
# we inject exactly the overlays we flipped to land the hit.
|
|
1489
1524
|
_l1_needs_env_inject = False
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
_base_h8 = _hashlib.sha256(_base_flags.encode()).hexdigest()[:8]
|
|
1525
|
+
_l1_needs_git_inject = False
|
|
1526
|
+
if _l1_result is None and (env_map or git_context):
|
|
1493
1527
|
_sha_prefix = _git_sha if _git_sha else "nogit"
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1528
|
+
_flippable = []
|
|
1529
|
+
if git_context:
|
|
1530
|
+
_flippable.append("gc") # inject git is cheap + additive
|
|
1531
|
+
if env_map:
|
|
1532
|
+
_flippable.append("em")
|
|
1533
|
+
# non-empty subsets, ascending size (fewest flips = richest base first)
|
|
1534
|
+
import itertools as _it
|
|
1535
|
+
_subsets = [
|
|
1536
|
+
s for n in range(1, len(_flippable) + 1)
|
|
1537
|
+
for s in _it.combinations(_flippable, n)
|
|
1538
|
+
]
|
|
1539
|
+
for _subset in _subsets:
|
|
1540
|
+
_bf = _core_flags_str
|
|
1541
|
+
if "gc" in _subset:
|
|
1542
|
+
_bf = _bf.replace(",gc=True,", ",gc=False,")
|
|
1543
|
+
if "em" in _subset:
|
|
1544
|
+
_bf = _bf.replace(",em=True,", ",em=False,")
|
|
1545
|
+
_bk = f"{_sha_prefix}-{_hashlib.sha256(_bf.encode()).hexdigest()[:8]}"
|
|
1546
|
+
_br = _cache_mod.read_core(target, _bk)
|
|
1547
|
+
if _br is not None:
|
|
1548
|
+
_l1_result = _br
|
|
1549
|
+
_l1_needs_git_inject = "gc" in _subset
|
|
1550
|
+
_l1_needs_env_inject = "em" in _subset
|
|
1551
|
+
break
|
|
1499
1552
|
|
|
1500
1553
|
if _l1_result is not None:
|
|
1501
1554
|
_core_dict_l1, _core_hash = _l1_result
|
|
1502
1555
|
_view_key = f"{_core_hash}-{_view_h}"
|
|
1556
|
+
# On the lazy-inject path the core is a REUSED base whose _core_hash is
|
|
1557
|
+
# shared with the base's overlay-free view — suffix the view key with the
|
|
1558
|
+
# injected overlays so the injected body never collides with (or is served
|
|
1559
|
+
# from) the base view. Non-inject runs keep their exact historical key, so
|
|
1560
|
+
# existing cached views stay valid (no cross-version churn).
|
|
1561
|
+
if _l1_needs_git_inject or _l1_needs_env_inject:
|
|
1562
|
+
_view_key = (
|
|
1563
|
+
f"{_view_key}-ov:gc{int(_l1_needs_git_inject)}"
|
|
1564
|
+
f"em{int(_l1_needs_env_inject)}"
|
|
1565
|
+
)
|
|
1503
1566
|
|
|
1504
1567
|
# Step 2: try L2 (exact view match).
|
|
1505
1568
|
# Skip L2 for --changed-only: the stored view is a previous
|
|
@@ -1553,6 +1616,23 @@ def main(
|
|
|
1553
1616
|
]
|
|
1554
1617
|
except Exception:
|
|
1555
1618
|
pass # env inject failed — continue without env data
|
|
1619
|
+
# P-gc: inject git context when a gc=False base L1 was reused.
|
|
1620
|
+
# GitAnalyzer walks history only — typically <1 s. Reuses the
|
|
1621
|
+
# serializer's own compact shaping (no duplication/drift).
|
|
1622
|
+
if _rebuilt is not None and _l1_needs_git_inject and compact:
|
|
1623
|
+
try:
|
|
1624
|
+
from types import SimpleNamespace as _NS_gc
|
|
1625
|
+
from sourcecode.git_analyzer import GitAnalyzer as _GitA_gc
|
|
1626
|
+
from sourcecode.serializer import _compact_git_context as _cgc
|
|
1627
|
+
_gc_obj = _GitA_gc().analyze(
|
|
1628
|
+
target, depth=git_depth, days=git_days
|
|
1629
|
+
)
|
|
1630
|
+
_gc_block = _cgc(_NS_gc(git_context=_gc_obj))
|
|
1631
|
+
if _gc_block:
|
|
1632
|
+
_rebuilt = dict(_rebuilt)
|
|
1633
|
+
_rebuilt["git_context"] = _gc_block
|
|
1634
|
+
except Exception:
|
|
1635
|
+
pass # git inject failed — continue without git data
|
|
1556
1636
|
if _rebuilt is not None:
|
|
1557
1637
|
# Apply redaction
|
|
1558
1638
|
if not no_redact:
|
|
@@ -1729,6 +1809,17 @@ def main(
|
|
|
1729
1809
|
# 2. Filter .env and *.secret entries from file tree (SEC-02, all levels)
|
|
1730
1810
|
file_tree = filter_sensitive_files(raw_tree, redactor)
|
|
1731
1811
|
perf.stop("discovery", _perf_discovery)
|
|
1812
|
+
|
|
1813
|
+
# Cold-scan advisory (honest expectation-setting, NOT a speed claim). A cache
|
|
1814
|
+
# hit short-circuits above (line ~1647) before this scan, so reaching here means
|
|
1815
|
+
# a cold run: the full parse+analysis below scales with repo size. On a large
|
|
1816
|
+
# repo, warn once on the TTY so the wait is expected — never on stdout (JSON) and
|
|
1817
|
+
# never for agents/pipes. This does not assert a duration; it states the shape.
|
|
1818
|
+
if sys.stderr.isatty() and not no_cache:
|
|
1819
|
+
_advisory = _cold_scan_advisory_message(file_tree)
|
|
1820
|
+
if _advisory:
|
|
1821
|
+
typer.echo(_advisory, err=True)
|
|
1822
|
+
|
|
1732
1823
|
_perf_detection = perf.start()
|
|
1733
1824
|
detector = ProjectDetector(build_default_detectors())
|
|
1734
1825
|
workspace_analysis = WorkspaceAnalyzer().analyze(target, manifests)
|
|
@@ -5295,22 +5386,23 @@ def _render_spring_audit_github_comment(result: "SpringAuditResult", min_severit
|
|
|
5295
5386
|
lines.append("")
|
|
5296
5387
|
|
|
5297
5388
|
if not visible:
|
|
5389
|
+
# No findings at/above severity — but a gate-coverage escape set (a separate
|
|
5390
|
+
# signal, not a "finding") may still be worth surfacing before the footer.
|
|
5298
5391
|
lines.append(f"_No findings at or above `{min_severity}` severity._")
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
lines.append(f"| {icon} {label} | `{f.pattern_id}` | `{short_file}` | `{short_sym}` | {title_escaped} |")
|
|
5392
|
+
else:
|
|
5393
|
+
lines += [
|
|
5394
|
+
"| Sev | Pattern | File | Symbol | Title |",
|
|
5395
|
+
"|-----|---------|------|--------|-------|",
|
|
5396
|
+
]
|
|
5397
|
+
for f in sorted(visible, key=lambda x: (SEVERITY_ORDER.get(x.severity, 3), x.source_file)):
|
|
5398
|
+
icon = _ICONS.get(f.severity, "")
|
|
5399
|
+
label = _LABELS.get(f.severity, f.severity.upper())
|
|
5400
|
+
short_file = f.source_file.split("/")[-1] if "/" in f.source_file else f.source_file
|
|
5401
|
+
short_sym = f.symbol.split(".")[-1] if "." in f.symbol else f.symbol
|
|
5402
|
+
title_escaped = f.title.replace("|", "\\|")
|
|
5403
|
+
lines.append(f"| {icon} {label} | `{f.pattern_id}` | `{short_file}` | `{short_sym}` | {title_escaped} |")
|
|
5312
5404
|
|
|
5313
|
-
|
|
5405
|
+
lines.append("")
|
|
5314
5406
|
|
|
5315
5407
|
if visible:
|
|
5316
5408
|
lines.append("<details>")
|
|
@@ -5327,6 +5419,8 @@ def _render_spring_audit_github_comment(result: "SpringAuditResult", min_severit
|
|
|
5327
5419
|
lines.append("")
|
|
5328
5420
|
lines.append("</details>")
|
|
5329
5421
|
|
|
5422
|
+
lines += _render_gate_coverage_section(result)
|
|
5423
|
+
|
|
5330
5424
|
lines += [
|
|
5331
5425
|
"",
|
|
5332
5426
|
f"_Generated by [sourcecode](https://github.com/sourcecode-ai/sourcecode) · "
|
|
@@ -5335,6 +5429,57 @@ def _render_spring_audit_github_comment(result: "SpringAuditResult", min_severit
|
|
|
5335
5429
|
return "\n".join(lines)
|
|
5336
5430
|
|
|
5337
5431
|
|
|
5432
|
+
_GATE_COVERAGE_RENDER_CAP = 25
|
|
5433
|
+
|
|
5434
|
+
|
|
5435
|
+
def _render_gate_coverage_section(result: "SpringAuditResult") -> list[str]: # type: ignore[name-defined]
|
|
5436
|
+
"""Human-readable gate-coverage block: the controller handlers that do NOT carry
|
|
5437
|
+
the auto-detected custom authorization gate — the escape set the JSON already
|
|
5438
|
+
computes, surfaced so a reviewer sees it at a glance. Never says "unsecured": a
|
|
5439
|
+
handler a servlet filter pattern covers is marked "possibly filter-covered", and the
|
|
5440
|
+
unmatched ones are flagged for review, not condemned."""
|
|
5441
|
+
gc = (result.security_posture or {}).get("gate_coverage")
|
|
5442
|
+
if not gc:
|
|
5443
|
+
return []
|
|
5444
|
+
not_covered = gc.get("not_carrying_gate", 0)
|
|
5445
|
+
gates = ", ".join(f"`{g}`" for g in gc.get("gate_annotations", [])) or "the detected gate"
|
|
5446
|
+
total = gc.get("total_controller_handlers", 0)
|
|
5447
|
+
lines: list[str] = ["", "---", ""]
|
|
5448
|
+
if not_covered == 0:
|
|
5449
|
+
lines.append(f"✅ **Gate coverage** — all {total} controller handlers carry {gates}.")
|
|
5450
|
+
return lines
|
|
5451
|
+
|
|
5452
|
+
covered = gc.get("possibly_filter_covered", 0)
|
|
5453
|
+
lines.append(
|
|
5454
|
+
f"🔓 **Gate coverage** — {not_covered} of {total} controller handlers do not carry "
|
|
5455
|
+
f"{gates}."
|
|
5456
|
+
)
|
|
5457
|
+
if gc.get("reconstructed_filter_patterns"):
|
|
5458
|
+
lines.append(
|
|
5459
|
+
f"_{covered} of those match a reconstructed servlet filter pattern "
|
|
5460
|
+
f"(possibly filter-covered); {gc.get('no_matching_filter_pattern', 0)} match none._"
|
|
5461
|
+
)
|
|
5462
|
+
lines += ["", "<details>", "<summary>Handlers without the gate</summary>", ""]
|
|
5463
|
+
lines += [
|
|
5464
|
+
"| Method | Path | Filter pattern | Policy |",
|
|
5465
|
+
"|--------|------|----------------|--------|",
|
|
5466
|
+
]
|
|
5467
|
+
handlers = gc.get("handlers_without_gate", [])
|
|
5468
|
+
for h in handlers[:_GATE_COVERAGE_RENDER_CAP]:
|
|
5469
|
+
method = (h.get("method") or "").replace("|", "\\|")
|
|
5470
|
+
path = (h.get("path") or "").replace("|", "\\|")
|
|
5471
|
+
fpat = h.get("filter_pattern_match")
|
|
5472
|
+
fcell = f"`{fpat}`" if fpat else "—"
|
|
5473
|
+
policy = (h.get("policy") or "").replace("|", "\\|")
|
|
5474
|
+
lines.append(f"| {method} | `{path}` | {fcell} | {policy} |")
|
|
5475
|
+
if len(handlers) > _GATE_COVERAGE_RENDER_CAP:
|
|
5476
|
+
lines.append(f"| … | _+{len(handlers) - _GATE_COVERAGE_RENDER_CAP} more_ | | |")
|
|
5477
|
+
lines += ["", "</details>", ""]
|
|
5478
|
+
if gc.get("note"):
|
|
5479
|
+
lines.append(f"> {gc['note']}")
|
|
5480
|
+
return lines
|
|
5481
|
+
|
|
5482
|
+
|
|
5338
5483
|
@app.command("spring-audit")
|
|
5339
5484
|
def spring_audit_cmd(
|
|
5340
5485
|
path: Path = typer.Argument(
|
|
@@ -5804,7 +5949,14 @@ def impact_chain_cmd(
|
|
|
5804
5949
|
|
|
5805
5950
|
_prog = Progress()
|
|
5806
5951
|
_prog.start(f"analyzing impact ({len(file_list)} files)")
|
|
5807
|
-
|
|
5952
|
+
# Reuse the shared context-cache CIR (same entry `cache warm` and `explain` use) so
|
|
5953
|
+
# a warmed repo skips the expensive Java parse. Best-effort — any fault falls back to
|
|
5954
|
+
# a fresh build, exactly as explain does, so impact-chain never breaks.
|
|
5955
|
+
from sourcecode import context_cache as _ctxcache
|
|
5956
|
+
try:
|
|
5957
|
+
cir, _ = _ctxcache.get_or_build_cir(_resolve_repo_root(target), target, file_list)
|
|
5958
|
+
except Exception:
|
|
5959
|
+
cir = ContextGraph.build(file_list, target).cir
|
|
5808
5960
|
_model = SpringSemanticModel.build(cir)
|
|
5809
5961
|
|
|
5810
5962
|
if query_type == "events":
|
|
@@ -8215,6 +8367,22 @@ def cache_clear_cmd(
|
|
|
8215
8367
|
typer.echo(f"Removed {removed} file(s).", err=True)
|
|
8216
8368
|
|
|
8217
8369
|
|
|
8370
|
+
def _warm_shared_cir(target: Path):
|
|
8371
|
+
"""Pre-populate the shared Canonical IR in the context cache (the entry explain and
|
|
8372
|
+
other knowledge commands reuse). Returns the KnowledgeLookup, or None on any fault —
|
|
8373
|
+
warming is best-effort and never fails the warm command."""
|
|
8374
|
+
try:
|
|
8375
|
+
from sourcecode.repository_ir import find_java_files as _fjf
|
|
8376
|
+
from sourcecode import context_cache as _ctxcache
|
|
8377
|
+
files = _fjf(target)
|
|
8378
|
+
if not files:
|
|
8379
|
+
return None
|
|
8380
|
+
_cir, look = _ctxcache.get_or_build_cir(_resolve_repo_root(target), target, files)
|
|
8381
|
+
return look
|
|
8382
|
+
except Exception:
|
|
8383
|
+
return None
|
|
8384
|
+
|
|
8385
|
+
|
|
8218
8386
|
@cache_app.command("warm")
|
|
8219
8387
|
def cache_warm_cmd(
|
|
8220
8388
|
path: Path = typer.Argument(Path("."), help="Repository path to warm (default: current directory)"),
|
|
@@ -8245,6 +8413,18 @@ def cache_warm_cmd(
|
|
|
8245
8413
|
typer.echo(result.stderr.strip(), err=True)
|
|
8246
8414
|
raise typer.Exit(code=result.returncode)
|
|
8247
8415
|
|
|
8416
|
+
# Also pre-populate the shared Canonical IR (context cache) — a DIFFERENT cache from
|
|
8417
|
+
# the L1/L2 core cache warmed above. explain (and, going forward, other knowledge
|
|
8418
|
+
# commands) reuse this CIR; without warming it here, the first explain rebuilds the
|
|
8419
|
+
# expensive Java parse despite a "warm" cache (field-eval: explain MISS after warm).
|
|
8420
|
+
_look = _warm_shared_cir(target)
|
|
8421
|
+
if _look is not None:
|
|
8422
|
+
typer.echo(
|
|
8423
|
+
f"Shared CIR {'reused' if _look.hit else 'built'} "
|
|
8424
|
+
"(explain/impact reuse this).",
|
|
8425
|
+
err=True,
|
|
8426
|
+
)
|
|
8427
|
+
|
|
8248
8428
|
|
|
8249
8429
|
@cache_app.command("freshness")
|
|
8250
8430
|
def cache_freshness_cmd(
|
|
@@ -8386,6 +8566,23 @@ def _stderr_is_interactive() -> bool:
|
|
|
8386
8566
|
return False
|
|
8387
8567
|
|
|
8388
8568
|
|
|
8569
|
+
def _force_utf8_streams() -> None:
|
|
8570
|
+
"""Force UTF-8 on stdout AND stderr so Unicode characters (em-dash, arrows, box
|
|
8571
|
+
drawing) survive on Windows where the default console codec is cp1252 (BUG-1).
|
|
8572
|
+
|
|
8573
|
+
stderr matters as much as stdout: progress/warn/gap status text is emitted there,
|
|
8574
|
+
and a cp1252 encoder turned its em-dashes into the replacement char (field-eval).
|
|
8575
|
+
Best-effort and idempotent — a stream without ``reconfigure`` (already wrapped, or a
|
|
8576
|
+
test buffer) is skipped silently.
|
|
8577
|
+
"""
|
|
8578
|
+
for stream in (sys.stdout, sys.stderr):
|
|
8579
|
+
if hasattr(stream, "reconfigure"):
|
|
8580
|
+
try:
|
|
8581
|
+
stream.reconfigure(encoding="utf-8")
|
|
8582
|
+
except Exception:
|
|
8583
|
+
pass
|
|
8584
|
+
|
|
8585
|
+
|
|
8389
8586
|
def main_entry() -> None:
|
|
8390
8587
|
"""CLI entry point for both the canonical ``ask`` command and the deprecated
|
|
8391
8588
|
``sourcecode`` compat alias — one implementation, no duplication.
|
|
@@ -8395,13 +8592,7 @@ def main_entry() -> None:
|
|
|
8395
8592
|
can consume them as positional arguments (which would prevent subcommand
|
|
8396
8593
|
routing for tokens like 'version' or 'config').
|
|
8397
8594
|
"""
|
|
8398
|
-
|
|
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
|
|
8595
|
+
_force_utf8_streams()
|
|
8405
8596
|
# Deprecation notice when invoked through the legacy `sourcecode` alias.
|
|
8406
8597
|
# One line, and only on an interactive terminal — pipes/agents that still call
|
|
8407
8598
|
# `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
|
# ---------------------------------------------------------------------------
|