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.
- {sourcecode-2.6.1 → sourcecode-2.6.3}/CHANGELOG.md +82 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/PKG-INFO +1 -1
- {sourcecode-2.6.1 → sourcecode-2.6.3}/pyproject.toml +1 -1
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/__init__.py +1 -1
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/archetype.py +12 -1
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/cli.py +310 -38
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/context_cache.py +40 -0
- sourcecode-2.6.3/src/sourcecode/filter_surface.py +173 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/mcp/registry.py +49 -1
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/migrate_check.py +10 -0
- sourcecode-2.6.3/src/sourcecode/parse_cache.py +138 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/prepare_context.py +21 -1
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/repository_ir.py +46 -12
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/errors.py +0 -7
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/steps_endpoint.py +1 -7
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/steps_impact.py +1 -7
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/security_posture.py +96 -0
- sourcecode-2.6.3/src/sourcecode/verify_edit.py +588 -0
- sourcecode-2.6.3/src/sourcecode/verify_rules.py +304 -0
- sourcecode-2.6.1/src/sourcecode/retrieval/composition.py +0 -496
- {sourcecode-2.6.1 → sourcecode-2.6.3}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/.gitignore +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/.ruff.toml +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/CONTRIBUTING.md +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/LICENSE +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/README.md +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/SECURITY.md +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/raw +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/scripts/compare_integration_engines.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/scripts/customer_smoke_test.sh +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/scripts/generate_jdk_exports.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/scripts/perf_harness.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/architectural_baseline.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/architectural_delta.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/cache.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/call_surface.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/caller_metrics.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/change_plan.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/classifier.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/compare.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/constraint_diff.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/context_graph.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/contract_diff.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/dynamic_argument_surface.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/endpoint_literals.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/endpoint_metrics.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/evidence_provider.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/explain.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/format_contract.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/graph_evidence.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/hibernate_strat.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/jdk_exports.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/license.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/openapi_surface.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/perf.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/progress.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/reconciliation.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/redactor.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/__init__.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/context.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/executor.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/planner.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/query.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/request.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/resolution.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/result.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/retriever.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/runtime.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/steps.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/steps_graph.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/steps_intf.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/steps_struct.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/retrieval/steps_txsec.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/ris.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/scanner.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/schema.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/security_config.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/semantic_impact_engine.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/semantic_integration_engine.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/semantic_services.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/serializer.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/spring_impact.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/spring_tx_analyzer.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/type_usage_surface.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/validation_inference.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/validation_surface.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/version_check.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/src/sourcecode/workspace.py +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/supabase/functions/README.md +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/supabase/functions/telemetry/index.ts +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/supabase/sql/license_event_ordering.sql +0 -0
- {sourcecode-2.6.1 → sourcecode-2.6.3}/supabase/sql/licensing_schema.sql +0 -0
- {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
|
|
@@ -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)
|
|
@@ -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
|
|
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("\
|
|
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
|
-
#
|
|
1487
|
-
#
|
|
1488
|
-
#
|
|
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
|
-
|
|
1491
|
-
|
|
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
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
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
|
-
|
|
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} |")
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
# ---------------------------------------------------------------------------
|