sourcecode 4.10.4__tar.gz → 4.10.5__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-4.10.4 → sourcecode-4.10.5}/CHANGELOG.md +64 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/PKG-INFO +3 -3
- {sourcecode-4.10.4 → sourcecode-4.10.5}/README.md +2 -2
- {sourcecode-4.10.4 → sourcecode-4.10.5}/pyproject.toml +1 -1
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/__init__.py +1 -1
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/cli.py +50 -9
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/pr_impact.py +38 -4
- sourcecode-4.10.5/src/sourcecode/progress.py +218 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/repository_ir.py +38 -1
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/security_config_scan.py +30 -22
- sourcecode-4.10.5/src/sourcecode/text_input.py +93 -0
- sourcecode-4.10.4/src/sourcecode/progress.py +0 -125
- {sourcecode-4.10.4 → sourcecode-4.10.5}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/.gitignore +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/.ruff.toml +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/CLAUDE.md +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/CONTRIBUTING.md +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/LICENSE +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/SECURITY.md +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/raw +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/scripts/compare_integration_engines.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/scripts/customer_smoke_test.sh +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/scripts/generate_jdk_exports.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/scripts/perf_harness.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/archetype.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/architectural_baseline.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/architectural_delta.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/audit_report.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/baseline_autocapture.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/cache.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/cache_model.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/call_surface.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/caller_metrics.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/caller_reach.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/chain_rules.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/change_plan.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/classifier.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/client_calls.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/compare.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/constraint_diff.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/context_cache.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/context_graph.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/contract_diff.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/contract_init.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/data_exposure.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/data_labels.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/defect_identity.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/degradation.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/deployment_prefix.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/dynamic_argument_surface.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/endpoint_literals.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/endpoint_metrics.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/envelope.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/environment_resolution.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/evidence_provider.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/explain.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/facts/__init__.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/facts/registry.json +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/filter_surface.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/format_contract.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/git_checkout.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/graph_evidence.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/hibernate_strat.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/identity_fallback.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/integration_coordinates.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/jdk_exports.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/license.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/migrate_check.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/migration_blast.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/non_coverage.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/openapi_surface.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/openrewrite_recipe.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/parse_cache.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/path_admission.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/perf.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/pipe_contract.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/posture.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/provenance.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/readiness_timeline.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/reconciliation.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/redactor.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/reference_facts.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/release_info.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/remedies.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/__init__.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/context.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/errors.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/executor.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/planner.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/query.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/request.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/resolution.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/result.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/retriever.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/runtime.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/steps.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/steps_graph.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/steps_impact.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/steps_intf.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/steps_struct.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/steps_txsec.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/ris.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/risk.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/rule_catalog.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/runs.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/sarif.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/scanner.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/schema.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/schemas/envelope-v1.schema.json +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/security_config.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/security_posture.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/semantic_impact_engine.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/semantic_integration_engine.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/semantic_services.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/serializer.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/servlet_surface.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/source_text.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/spring_impact.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/spring_profiles.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/spring_properties.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/spring_tx_analyzer.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/target_admission.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/test_gap_ranking.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/test_sources.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/token_estimate.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/type_usage_surface.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/validation_inference.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/validation_surface.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/verify_edit.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/verify_repo.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/verify_rules.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/version_check.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/workspace.py +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/supabase/functions/README.md +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/supabase/functions/telemetry/index.ts +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/supabase/sql/license_event_ordering.sql +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/supabase/sql/licensing_schema.sql +0 -0
- {sourcecode-4.10.4 → sourcecode-4.10.5}/supabase/sql/telemetry_events.sql +0 -0
|
@@ -2,6 +2,70 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [4.10.5] — 2026-08-07
|
|
6
|
+
|
|
7
|
+
Second release of the field-evaluation #13 queue. Three rows close and one closes
|
|
8
|
+
in part; the suite is 6 691 tests green.
|
|
9
|
+
|
|
10
|
+
⚠ **Two behaviour changes to read before upgrading a pipeline.** Both are the fix
|
|
11
|
+
working, and both can move a gate:
|
|
12
|
+
|
|
13
|
+
- On Windows, `pr-impact --files -` now resolves paths it previously mangled, so a
|
|
14
|
+
pipeline that was passing because its diff decoded to nothing can start failing —
|
|
15
|
+
with the verdict the diff always deserved (C3-58).
|
|
16
|
+
- A security finding that existed only in build output (`target/classes`) is no
|
|
17
|
+
longer reported, so a repository audited mid-build can report fewer findings
|
|
18
|
+
(C3-64).
|
|
19
|
+
|
|
20
|
+
### Performance
|
|
21
|
+
|
|
22
|
+
- **The security configuration scan discovers its files 8.7× faster** (C3-64):
|
|
23
|
+
0.61 s → 0.07 s on keycloak, 0.45 s → 0.05 s on alfresco. Five `rglob` calls
|
|
24
|
+
became one pruned walk. `rglob` cannot skip a subtree, so the old cost was set by
|
|
25
|
+
the build output and the VCS metadata rather than by the source: 48.4 % of
|
|
26
|
+
openmrs-core's tree entries and 53.3 % of this repository's live under directories
|
|
27
|
+
that hold no source at all. On a clean checkout the end-to-end scan moves 1.73 s →
|
|
28
|
+
1.43 s (the rest is reading Java sources); the win grows with how built the tree
|
|
29
|
+
is, which is the state a developer's repository is normally in.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **A path list is decoded by its bytes, never by the platform locale** (C3-58).
|
|
34
|
+
`ask pr-impact --files -` read stdin through Python's locale decoding, which on
|
|
35
|
+
Windows is the ANSI codepage: `git diff --name-only | ask pr-impact --files -`
|
|
36
|
+
returned `risk: UNKNOWN` and 0 endpoints where the same list without the shell's
|
|
37
|
+
default byte-order mark returned `CRITICAL` and 1 144 — a green pipeline on the
|
|
38
|
+
gating command. The BOM was the visible half; `AñoService.java` arriving as
|
|
39
|
+
`AñoService.java` was the cause. The new authority `sourcecode/text_input.py`
|
|
40
|
+
reads `sys.stdin.buffer` and lets the bytes name the encoding, and the list-file
|
|
41
|
+
shape goes through it too: PowerShell 5.1 writes UTF-16-LE by default, which the
|
|
42
|
+
previous UTF-8 read raised on. Undecodable bytes now yield an unusable path with
|
|
43
|
+
encoding named as a cause, never a traceback.
|
|
44
|
+
- **No command claims `done` before its answer exists** (C3-54, part 2). The
|
|
45
|
+
completion line moved out of `Progress.finish()` entirely: `finish()` claims the
|
|
46
|
+
work is done, and the line is printed when the answer has actually been handed
|
|
47
|
+
over. A run killed in between prints nothing, and a run that raised prints
|
|
48
|
+
nothing — previously ~20 commands emitted `done (Xs)` while their `--output` file
|
|
49
|
+
did not exist yet, which in the field meant 6.8 minutes of analysis reported as
|
|
50
|
+
success over an absent file. Output files are also durable now: the shared writer
|
|
51
|
+
fsyncs data and directory entry before anything reports a write, and `repo-ir` —
|
|
52
|
+
the one `-o` path that bypassed that writer — goes through it.
|
|
53
|
+
- **A rejected progress write no longer silences progress for the rest of the run**
|
|
54
|
+
(C3-56, part). The heartbeat treated any write failure as terminal; it now retries
|
|
55
|
+
and gives up only after five consecutive failures. The reported 393 s of silence
|
|
56
|
+
did not reproduce: the cadence is timer-driven and measured exact on a cold
|
|
57
|
+
29.9 s run (5.0 / 10.0 / 15.0 / 20.0 / 25.1 s). The progress denominator and ETA
|
|
58
|
+
the row also asks for are still open.
|
|
59
|
+
- **A build copy is no longer a second security finding** (C3-64). The configuration
|
|
60
|
+
scan walked the whole tree five times and could not skip a subtree, so a compiled
|
|
61
|
+
copy of `application.properties` under `target/classes` was reported beside the
|
|
62
|
+
source file it came from. It now walks once, pruning build output and VCS metadata
|
|
63
|
+
with the same ignore set Java discovery uses.
|
|
64
|
+
- **An unresolved changed file names the right thing to check** (C3-58). One message
|
|
65
|
+
covered both causes and pointed at the wrong one: a path that is *not on disk* is a
|
|
66
|
+
path problem (form and encoding), while a path that *is* there but maps to no class
|
|
67
|
+
is a fact about the file. They now say so separately.
|
|
68
|
+
|
|
5
69
|
## [4.10.4] — 2026-08-07
|
|
6
70
|
|
|
7
71
|
First release of the queue opened by field evaluation #13. Four of its rows close,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 4.10.
|
|
3
|
+
Version: 4.10.5
|
|
4
4
|
Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Keywords: agents,ai,codebase,context,developer-tools,llm
|
|
@@ -42,7 +42,7 @@ Description-Content-Type: text/markdown
|
|
|
42
42
|
|
|
43
43
|
**Context · Impact · Migration · Architecture · Review — everything from one structural model.**
|
|
44
44
|
|
|
45
|
-

|
|
46
46
|

|
|
47
47
|
|
|
48
48
|
> **ASK Engine** is the product. The CLI command is **`ask`**. The legacy **`sourcecode`**
|
|
@@ -106,7 +106,7 @@ brew tap haroundominique/sourcecode && brew install sourcecode
|
|
|
106
106
|
# pip / pipx
|
|
107
107
|
pipx install sourcecode # or: pip install sourcecode
|
|
108
108
|
|
|
109
|
-
ask version # ask 4.10.
|
|
109
|
+
ask version # ask 4.10.5 — and, on a build that has aged,
|
|
110
110
|
# how many releases have probably shipped since
|
|
111
111
|
```
|
|
112
112
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
**Context · Impact · Migration · Architecture · Review — everything from one structural model.**
|
|
6
6
|
|
|
7
|
-

|
|
8
8
|

|
|
9
9
|
|
|
10
10
|
> **ASK Engine** is the product. The CLI command is **`ask`**. The legacy **`sourcecode`**
|
|
@@ -68,7 +68,7 @@ brew tap haroundominique/sourcecode && brew install sourcecode
|
|
|
68
68
|
# pip / pipx
|
|
69
69
|
pipx install sourcecode # or: pip install sourcecode
|
|
70
70
|
|
|
71
|
-
ask version # ask 4.10.
|
|
71
|
+
ask version # ask 4.10.5 — and, on a build that has aged,
|
|
72
72
|
# how many releases have probably shipped since
|
|
73
73
|
```
|
|
74
74
|
|
|
@@ -18,6 +18,7 @@ from sourcecode import __version__
|
|
|
18
18
|
from sourcecode.error_schema import EXECUTION_FAILED_CODE, INVALID_INPUT_CODE, build_error_envelope
|
|
19
19
|
from sourcecode.entrypoint_classifier import is_production_entry_point, normalize_entry_point
|
|
20
20
|
from sourcecode.progress import Progress
|
|
21
|
+
from sourcecode.progress import output_emitted as _progress_output_emitted
|
|
21
22
|
from sourcecode import perf
|
|
22
23
|
from sourcecode.caller_metrics import (
|
|
23
24
|
CALLER_METRIC_RECONCILIATION,
|
|
@@ -888,8 +889,27 @@ def _safe_write_file(path: "Path", content: str) -> None:
|
|
|
888
889
|
path = Path(path)
|
|
889
890
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
890
891
|
tmp = path.with_name(f".{path.name}.tmp-{os.getpid()}-{time.time_ns()}")
|
|
891
|
-
|
|
892
|
+
# C3-54 part 2: the rename is atomic, the bytes were not durable. A
|
|
893
|
+
# process torn down between `write_text` and the OS flushing its page
|
|
894
|
+
# cache leaves a file that exists at full length and holds nothing —
|
|
895
|
+
# the same total loss the false `done` line reported as success, one
|
|
896
|
+
# layer down. fsync the data before the rename, then the directory
|
|
897
|
+
# entry, so a run that reports an output file has one.
|
|
898
|
+
with open(tmp, "w", encoding="utf-8") as fh:
|
|
899
|
+
fh.write(content)
|
|
900
|
+
fh.flush()
|
|
901
|
+
os.fsync(fh.fileno())
|
|
892
902
|
tmp.replace(path)
|
|
903
|
+
try:
|
|
904
|
+
dir_fd = os.open(str(path.parent), os.O_RDONLY)
|
|
905
|
+
try:
|
|
906
|
+
os.fsync(dir_fd)
|
|
907
|
+
finally:
|
|
908
|
+
os.close(dir_fd)
|
|
909
|
+
except (OSError, AttributeError):
|
|
910
|
+
# Directory fsync is POSIX-only; on Windows the rename is already
|
|
911
|
+
# ordered against the data write. Never fail a good run over it.
|
|
912
|
+
pass
|
|
893
913
|
try:
|
|
894
914
|
from sourcecode.runs import active_run
|
|
895
915
|
run = active_run()
|
|
@@ -1213,6 +1233,9 @@ def _emit_command_output(
|
|
|
1213
1233
|
# The payload is on stdout here, so this cannot join it; it is a
|
|
1214
1234
|
# notice and takes the terminal gate rather than dirtying a capture.
|
|
1215
1235
|
_notice("✓ copied to clipboard")
|
|
1236
|
+
# The answer has been handed over. Any `done (Xs)` a command deferred is now
|
|
1237
|
+
# a true claim and prints here — never before this line (C3-54).
|
|
1238
|
+
_progress_output_emitted()
|
|
1216
1239
|
|
|
1217
1240
|
|
|
1218
1241
|
def _emit_text_output(text: str, copy: bool) -> None:
|
|
@@ -1228,6 +1251,7 @@ def _emit_text_output(text: str, copy: bool) -> None:
|
|
|
1228
1251
|
typer.echo(text)
|
|
1229
1252
|
if copy and _copy_to_clipboard(text):
|
|
1230
1253
|
_notice("✓ copied to clipboard")
|
|
1254
|
+
_progress_output_emitted()
|
|
1231
1255
|
|
|
1232
1256
|
|
|
1233
1257
|
class _TextAnswer:
|
|
@@ -1257,6 +1281,7 @@ class _TextAnswer:
|
|
|
1257
1281
|
def done(self, copy: bool) -> None:
|
|
1258
1282
|
if copy and self._lines and _copy_to_clipboard(self.text):
|
|
1259
1283
|
_notice("✓ copied to clipboard")
|
|
1284
|
+
_progress_output_emitted()
|
|
1260
1285
|
|
|
1261
1286
|
|
|
1262
1287
|
# H-06: Intercept Click-level UsageError (unknown options, bad args) and emit JSON.
|
|
@@ -4825,8 +4850,15 @@ def repo_ir_cmd(
|
|
|
4825
4850
|
size_bytes = len(raw_bytes)
|
|
4826
4851
|
if gzip_output:
|
|
4827
4852
|
import gzip as _gzip
|
|
4853
|
+
# Same durability rule as `_safe_write_file`: the bytes are on disk
|
|
4854
|
+
# before anything says they are (C3-54).
|
|
4828
4855
|
with _gzip.open(output_path, "wb") as _gz:
|
|
4829
4856
|
_gz.write(raw_bytes)
|
|
4857
|
+
_gz.flush()
|
|
4858
|
+
try:
|
|
4859
|
+
os.fsync(_gz.fileno())
|
|
4860
|
+
except (OSError, AttributeError):
|
|
4861
|
+
pass
|
|
4830
4862
|
compressed_kb = output_path.stat().st_size // 1024
|
|
4831
4863
|
size_kb = size_bytes // 1024
|
|
4832
4864
|
# C3-26: when the payload went to a file, this confirmation IS the
|
|
@@ -4837,7 +4869,9 @@ def repo_ir_cmd(
|
|
|
4837
4869
|
f"IR written to {output_path} ({compressed_kb}KB gzip, {size_kb}KB uncompressed)"
|
|
4838
4870
|
)
|
|
4839
4871
|
else:
|
|
4840
|
-
|
|
4872
|
+
# Was a bare `write_bytes`: neither atomic nor durable, and the only
|
|
4873
|
+
# `-o` write in the CLI that skipped the shared seam (C3-54).
|
|
4874
|
+
_safe_write_file(output_path, output)
|
|
4841
4875
|
size_kb = size_bytes // 1024
|
|
4842
4876
|
if summary_only:
|
|
4843
4877
|
typer.echo(
|
|
@@ -4850,6 +4884,7 @@ def repo_ir_cmd(
|
|
|
4850
4884
|
f"IR written to {output_path} "
|
|
4851
4885
|
f"({size_kb}KB, {n_nodes} nodes, {n_edges} edges)"
|
|
4852
4886
|
)
|
|
4887
|
+
_progress_output_emitted()
|
|
4853
4888
|
else:
|
|
4854
4889
|
if gzip_output:
|
|
4855
4890
|
_emit_error_json(
|
|
@@ -9461,18 +9496,21 @@ def pr_impact_cmd(
|
|
|
9461
9496
|
# --files accepts three shapes: a list file, '-' for stdin, or an inline
|
|
9462
9497
|
# comma-separated list. The list-file shape was the only documented one and
|
|
9463
9498
|
# the only one the error hint mentioned.
|
|
9464
|
-
|
|
9499
|
+
# Both byte-carrying shapes decode through `text_input`: stdin and list files
|
|
9500
|
+
# arrive as somebody else's shell wrote them, and the locale codepage is not
|
|
9501
|
+
# evidence of their encoding (C3-58).
|
|
9502
|
+
from sourcecode import text_input as _text_input
|
|
9503
|
+
|
|
9504
|
+
_files_raw = str(files).replace(_text_input.BOM_CHAR, "").strip()
|
|
9465
9505
|
if _files_raw == "-":
|
|
9466
|
-
changed_files =
|
|
9506
|
+
changed_files = _text_input.parse_path_lines(_text_input.read_stdin_text())
|
|
9467
9507
|
_files_desc = "stdin"
|
|
9468
9508
|
else:
|
|
9469
9509
|
_files_path = Path(_files_raw)
|
|
9470
9510
|
if _files_path.is_file():
|
|
9471
|
-
changed_files =
|
|
9472
|
-
|
|
9473
|
-
|
|
9474
|
-
if line.strip()
|
|
9475
|
-
]
|
|
9511
|
+
changed_files = _text_input.parse_path_lines(
|
|
9512
|
+
_text_input.read_text_file(_files_path)
|
|
9513
|
+
)
|
|
9476
9514
|
_files_desc = str(_files_path)
|
|
9477
9515
|
elif all(
|
|
9478
9516
|
p.strip().lower().endswith(".java")
|
|
@@ -10309,6 +10347,9 @@ def modernize_cmd(
|
|
|
10309
10347
|
sys.stdout.buffer.flush()
|
|
10310
10348
|
except AttributeError:
|
|
10311
10349
|
sys.stdout.write(output + "\n")
|
|
10350
|
+
# This branch predates `_emit_command_output`, so it also has to release the
|
|
10351
|
+
# deferred completion line itself (C3-54).
|
|
10352
|
+
_progress_output_emitted()
|
|
10312
10353
|
|
|
10313
10354
|
if copy:
|
|
10314
10355
|
_copy_to_clipboard(output)
|
|
@@ -228,6 +228,43 @@ def _suffix_candidates(norm: str, file_class_index: dict[str, list[str]]) -> lis
|
|
|
228
228
|
return shorter_given or longer_given
|
|
229
229
|
|
|
230
230
|
|
|
231
|
+
def _exists_on_disk(norm: str, root: Path) -> bool:
|
|
232
|
+
"""True when the given path names a file that is actually there."""
|
|
233
|
+
try:
|
|
234
|
+
candidate = Path(norm)
|
|
235
|
+
if candidate.is_absolute():
|
|
236
|
+
return candidate.is_file()
|
|
237
|
+
return (root / norm).is_file()
|
|
238
|
+
except OSError:
|
|
239
|
+
# A path the OS refuses to even stat (illegal characters after a bad
|
|
240
|
+
# decode, a name past MAX_PATH) is not on disk as far as we can tell.
|
|
241
|
+
return False
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _unresolved_warning(path_str: str, norm: str, root: Path) -> str:
|
|
245
|
+
"""Name the right thing to check for a changed file that mapped to nothing.
|
|
246
|
+
|
|
247
|
+
The two causes need opposite investigations and the single old message sent
|
|
248
|
+
every operator to the wrong one. A path that is *not there* is a path
|
|
249
|
+
problem — the field case was a BOM and cp1252 mojibake reaching us through
|
|
250
|
+
stdin (C3-58), and the message said "not in CIR", so the operator went
|
|
251
|
+
looking at analysis coverage. A path that *is* there carries no class
|
|
252
|
+
symbols, which is a fact about the file.
|
|
253
|
+
"""
|
|
254
|
+
if _exists_on_disk(norm, root):
|
|
255
|
+
return (
|
|
256
|
+
f"No Spring classes found for '{path_str}' — the file exists but "
|
|
257
|
+
"contributes no class symbols to the CIR (interface-free package-info, "
|
|
258
|
+
"an empty file, or a file excluded from analysis)."
|
|
259
|
+
)
|
|
260
|
+
return (
|
|
261
|
+
f"No file at '{path_str}' under the analyzed repository — nothing was "
|
|
262
|
+
"matched, and this is a path problem, not an analysis gap. Check the path "
|
|
263
|
+
"form (relative to the repo root) and its encoding: a byte-order mark or a "
|
|
264
|
+
"non-UTF-8 pipe corrupts accented names."
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
231
268
|
def _resolve_changed_files(
|
|
232
269
|
file_list: list[str],
|
|
233
270
|
file_class_index: dict[str, list[str]],
|
|
@@ -293,10 +330,7 @@ def _resolve_changed_files(
|
|
|
293
330
|
|
|
294
331
|
if not candidates:
|
|
295
332
|
unresolved_java.append(norm)
|
|
296
|
-
warnings.append(
|
|
297
|
-
f"No Spring classes found for '{path_str}' — "
|
|
298
|
-
"file not in CIR or has no class symbols."
|
|
299
|
-
)
|
|
333
|
+
warnings.append(_unresolved_warning(path_str, norm, root))
|
|
300
334
|
|
|
301
335
|
for cls in candidates:
|
|
302
336
|
if cls not in seen_classes:
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Lightweight progress indicator for long-running commands.
|
|
2
|
+
|
|
3
|
+
Writes only to stderr. Zero-cost when stderr is not a TTY or CI is detected,
|
|
4
|
+
unless ASK_PROGRESS=1 explicitly requests line heartbeats for supervised jobs.
|
|
5
|
+
Thread-safe; stop() is idempotent.
|
|
6
|
+
|
|
7
|
+
`done (Xs)` is a claim, and C3-54 is what happens when it is made early: the
|
|
8
|
+
field log printed `[ask] done (6.8m)` and the output directory held no
|
|
9
|
+
`f3-audit.json`, because the completion line was emitted when the *analysis*
|
|
10
|
+
finished — before the render and the write — and the process was torn down in
|
|
11
|
+
that window. 6.8 minutes of CPU, zero recoverable bytes, reported as success.
|
|
12
|
+
|
|
13
|
+
So the line is not printed where `finish()` is called. `finish()` claims the
|
|
14
|
+
work is done; the line is emitted when the **answer has actually been handed
|
|
15
|
+
over**, which the CLI signals through `output_emitted()` at its output seams. A
|
|
16
|
+
run that is killed between the two prints nothing, which is the honest outcome:
|
|
17
|
+
no answer, no completion line.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
import threading
|
|
24
|
+
import time
|
|
25
|
+
from typing import Optional
|
|
26
|
+
|
|
27
|
+
_FRAMES = ("⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏")
|
|
28
|
+
|
|
29
|
+
#: Consecutive failed heartbeat writes before the loop gives up on the stream.
|
|
30
|
+
_MAX_WRITE_FAILURES = 5
|
|
31
|
+
|
|
32
|
+
# --- Completion registry ----------------------------------------------------
|
|
33
|
+
# Module state, not per-instance: the emitter of the answer (the CLI output
|
|
34
|
+
# seam) and the owner of the completion line (a Progress somewhere up the call
|
|
35
|
+
# stack) never see each other.
|
|
36
|
+
_registry_lock = threading.Lock()
|
|
37
|
+
_output_events = 0
|
|
38
|
+
_pending_done: "list[Progress]" = []
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def output_emitted() -> None:
|
|
42
|
+
"""The command's answer has been written. Release any deferred `done` line.
|
|
43
|
+
|
|
44
|
+
Called from every CLI output seam. Safe to call when no Progress is running
|
|
45
|
+
and safe to call more than once per run.
|
|
46
|
+
"""
|
|
47
|
+
global _output_events
|
|
48
|
+
with _registry_lock:
|
|
49
|
+
_output_events += 1
|
|
50
|
+
released = list(_pending_done)
|
|
51
|
+
_pending_done.clear()
|
|
52
|
+
for prog in released:
|
|
53
|
+
prog._print_done()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _reset_for_tests() -> None:
|
|
57
|
+
"""Drop registry state between in-process test runs."""
|
|
58
|
+
global _output_events
|
|
59
|
+
with _registry_lock:
|
|
60
|
+
_output_events = 0
|
|
61
|
+
_pending_done.clear()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _terminal_capable() -> bool:
|
|
65
|
+
if os.environ.get("ASK_PROGRESS", "").lower() in ("1", "true", "yes", "on"):
|
|
66
|
+
return True
|
|
67
|
+
if not sys.stderr.isatty():
|
|
68
|
+
return False
|
|
69
|
+
if os.environ.get("TERM") in ("dumb", ""):
|
|
70
|
+
return False
|
|
71
|
+
if os.environ.get("NO_COLOR"):
|
|
72
|
+
return False
|
|
73
|
+
if os.environ.get("CI"):
|
|
74
|
+
return False
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class Progress:
|
|
79
|
+
"""In-place spinner on stderr.
|
|
80
|
+
|
|
81
|
+
Usage::
|
|
82
|
+
|
|
83
|
+
p = Progress()
|
|
84
|
+
p.start("scanning files")
|
|
85
|
+
...
|
|
86
|
+
p.update("extracting contracts")
|
|
87
|
+
...
|
|
88
|
+
p.finish() # clears line, prints "✓ done (3.2s)"
|
|
89
|
+
|
|
90
|
+
Always call stop() or finish() — both are idempotent.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
def __init__(self) -> None:
|
|
94
|
+
self._enabled = _terminal_capable()
|
|
95
|
+
self._line_mode = self._enabled and not sys.stderr.isatty()
|
|
96
|
+
try:
|
|
97
|
+
self._interval = max(0.5, float(os.environ.get("ASK_PROGRESS_INTERVAL", "5")))
|
|
98
|
+
except ValueError:
|
|
99
|
+
self._interval = 5.0
|
|
100
|
+
self._phase = ""
|
|
101
|
+
self._t0 = time.monotonic()
|
|
102
|
+
self._lock = threading.Lock()
|
|
103
|
+
self._stop_event = threading.Event()
|
|
104
|
+
self._thread: Optional[threading.Thread] = None
|
|
105
|
+
self._stopped = False
|
|
106
|
+
self._done_printed = False
|
|
107
|
+
self._outputs_at_start = 0
|
|
108
|
+
self._elapsed_at_finish = 0.0
|
|
109
|
+
self._write_failures = 0
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def elapsed(self) -> float:
|
|
113
|
+
return time.monotonic() - self._t0
|
|
114
|
+
|
|
115
|
+
def start(self, phase: str = "initializing") -> "Progress":
|
|
116
|
+
self._t0 = time.monotonic()
|
|
117
|
+
with self._lock:
|
|
118
|
+
self._phase = phase
|
|
119
|
+
self._stopped = False
|
|
120
|
+
self._done_printed = False
|
|
121
|
+
with _registry_lock:
|
|
122
|
+
self._outputs_at_start = _output_events
|
|
123
|
+
if not self._enabled:
|
|
124
|
+
return self
|
|
125
|
+
self._stop_event.clear()
|
|
126
|
+
self._thread = threading.Thread(target=self._loop, daemon=True)
|
|
127
|
+
self._thread.start()
|
|
128
|
+
return self
|
|
129
|
+
|
|
130
|
+
def update(self, phase: str) -> None:
|
|
131
|
+
with self._lock:
|
|
132
|
+
self._phase = phase
|
|
133
|
+
|
|
134
|
+
def stop(self) -> float:
|
|
135
|
+
"""Stop and clear spinner. Returns elapsed seconds. Idempotent."""
|
|
136
|
+
elapsed = self.elapsed
|
|
137
|
+
if self._stopped:
|
|
138
|
+
return elapsed
|
|
139
|
+
self._stopped = True
|
|
140
|
+
if not self._enabled:
|
|
141
|
+
return elapsed
|
|
142
|
+
self._stop_event.set()
|
|
143
|
+
if self._thread:
|
|
144
|
+
self._thread.join(timeout=1.0)
|
|
145
|
+
if not self._line_mode:
|
|
146
|
+
sys.stderr.write("\r\033[K")
|
|
147
|
+
sys.stderr.flush()
|
|
148
|
+
return elapsed
|
|
149
|
+
|
|
150
|
+
def finish(self) -> None:
|
|
151
|
+
"""Stop the spinner and claim completion — as soon as that is true.
|
|
152
|
+
|
|
153
|
+
If the command has already handed over its answer (the `spring-audit`
|
|
154
|
+
shape: `finish()` called after the write), the line prints here. If it
|
|
155
|
+
has not, the line waits for `output_emitted()`. It is never printed on a
|
|
156
|
+
run that dies in between, and never on a run that raised: `finally:
|
|
157
|
+
finish()` on an exception path leaves a claim pending that nothing
|
|
158
|
+
releases (C3-54).
|
|
159
|
+
"""
|
|
160
|
+
self._elapsed_at_finish = self.stop()
|
|
161
|
+
if not self._enabled:
|
|
162
|
+
return
|
|
163
|
+
with _registry_lock:
|
|
164
|
+
if _output_events > self._outputs_at_start:
|
|
165
|
+
already_emitted = True
|
|
166
|
+
else:
|
|
167
|
+
already_emitted = False
|
|
168
|
+
if self not in _pending_done:
|
|
169
|
+
_pending_done.append(self)
|
|
170
|
+
if already_emitted:
|
|
171
|
+
self._print_done()
|
|
172
|
+
|
|
173
|
+
def _print_done(self) -> None:
|
|
174
|
+
"""Write the completion line once, for an answer that now exists."""
|
|
175
|
+
if self._done_printed or not self._enabled:
|
|
176
|
+
return
|
|
177
|
+
self._done_printed = True
|
|
178
|
+
elapsed = getattr(self, "_elapsed_at_finish", self.elapsed)
|
|
179
|
+
t = f"{elapsed:.1f}s" if elapsed < 60 else f"{elapsed / 60:.1f}m"
|
|
180
|
+
prefix = "[ask] " if self._line_mode else "✓ "
|
|
181
|
+
try:
|
|
182
|
+
sys.stderr.write(f"{prefix}done ({t})\n")
|
|
183
|
+
sys.stderr.flush()
|
|
184
|
+
except Exception:
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
def _loop(self) -> None:
|
|
188
|
+
idx = 0
|
|
189
|
+
interval = self._interval if self._line_mode else 0.08
|
|
190
|
+
while not self._stop_event.wait(timeout=interval):
|
|
191
|
+
frame = _FRAMES[idx % len(_FRAMES)]
|
|
192
|
+
elapsed = time.monotonic() - self._t0
|
|
193
|
+
with self._lock:
|
|
194
|
+
phase = self._phase
|
|
195
|
+
if self._line_mode:
|
|
196
|
+
t = f"{elapsed:.1f}s" if elapsed < 60 else f"{elapsed / 60:.1f}m"
|
|
197
|
+
line = f"[ask] progress phase={phase} elapsed={t}\n"
|
|
198
|
+
else:
|
|
199
|
+
line = f"\r{frame} {phase} ({elapsed:.1f}s)"
|
|
200
|
+
try:
|
|
201
|
+
sys.stderr.write(line)
|
|
202
|
+
sys.stderr.flush()
|
|
203
|
+
self._write_failures = 0
|
|
204
|
+
except ValueError:
|
|
205
|
+
# The stream is closed — the run is ending. Nothing to retry.
|
|
206
|
+
break
|
|
207
|
+
except Exception:
|
|
208
|
+
# C3-56: a single failed write used to end the heartbeat for the
|
|
209
|
+
# rest of the run, and the operator's only remaining way to tell
|
|
210
|
+
# work from a hang was reading the worker's CPU time. One bad
|
|
211
|
+
# write is not evidence that the next one fails: on Windows a
|
|
212
|
+
# redirected stderr can reject a write transiently. Give up only
|
|
213
|
+
# when the stream refuses repeatedly.
|
|
214
|
+
self._write_failures += 1
|
|
215
|
+
if self._write_failures >= _MAX_WRITE_FAILURES:
|
|
216
|
+
break
|
|
217
|
+
continue
|
|
218
|
+
idx += 1
|
|
@@ -21,7 +21,7 @@ import subprocess
|
|
|
21
21
|
from collections import deque
|
|
22
22
|
from dataclasses import dataclass, field
|
|
23
23
|
from pathlib import Path
|
|
24
|
-
from typing import Any, Iterable, Optional
|
|
24
|
+
from typing import Any, Iterable, Iterator, Optional
|
|
25
25
|
|
|
26
26
|
from sourcecode.caller_metrics import CALLER_METRIC_RECONCILIATION, caller_classes
|
|
27
27
|
from sourcecode.degradation import cap_effect as _cap_effect
|
|
@@ -5634,6 +5634,43 @@ _VENDOR_DIRS: frozenset[str] = frozenset({
|
|
|
5634
5634
|
"generated-resources",
|
|
5635
5635
|
})
|
|
5636
5636
|
|
|
5637
|
+
#: Metadata trees that hold no repository source at all. Kept apart from
|
|
5638
|
+
#: `_VENDOR_DIRS` because that set answers "is this file part of the product";
|
|
5639
|
+
#: this one answers "is there anything here worth opening". `.git` alone holds
|
|
5640
|
+
#: 5 542 loose objects in this repository, and every unpruned `rglob` visits
|
|
5641
|
+
#: every one of them.
|
|
5642
|
+
_METADATA_DIRS: frozenset[str] = frozenset({
|
|
5643
|
+
".git", ".hg", ".svn", ".idea", ".vscode", "__pycache__",
|
|
5644
|
+
})
|
|
5645
|
+
|
|
5646
|
+
#: One ignore set for every tree walk that is looking for source (C3-64). A
|
|
5647
|
+
#: second, hand-kept list beside this one is how two surfaces come to disagree
|
|
5648
|
+
#: about what counts as source.
|
|
5649
|
+
PRUNED_TREE_DIRS: frozenset[str] = _VENDOR_DIRS | _METADATA_DIRS
|
|
5650
|
+
|
|
5651
|
+
|
|
5652
|
+
def walk_source_tree(
|
|
5653
|
+
root: Path, *, prune: frozenset[str] = PRUNED_TREE_DIRS,
|
|
5654
|
+
) -> "Iterator[Path]":
|
|
5655
|
+
"""Yield every file under `root`, never descending into a pruned directory.
|
|
5656
|
+
|
|
5657
|
+
`Path.rglob` has no way to skip a subtree: it walks the build output and the
|
|
5658
|
+
VCS metadata and then filters by name, so the cost of a scan is set by what
|
|
5659
|
+
the build wrote rather than by what the developer did. Measured on
|
|
5660
|
+
openmrs-core, 47 % of all tree entries live under directories in `prune`
|
|
5661
|
+
(C3-64).
|
|
5662
|
+
|
|
5663
|
+
Yields absolute paths, and never raises: an unreadable directory is skipped
|
|
5664
|
+
the same way the previous `rglob` calls swallowed `OSError`.
|
|
5665
|
+
"""
|
|
5666
|
+
import os
|
|
5667
|
+
|
|
5668
|
+
for dirpath, dirnames, filenames in os.walk(root, onerror=lambda _e: None):
|
|
5669
|
+
dirnames[:] = [d for d in dirnames if d not in prune]
|
|
5670
|
+
base = Path(dirpath)
|
|
5671
|
+
for name in filenames:
|
|
5672
|
+
yield base / name
|
|
5673
|
+
|
|
5637
5674
|
|
|
5638
5675
|
def apply_ir_size_limits(
|
|
5639
5676
|
ir: dict,
|
|
@@ -609,13 +609,36 @@ def scan_security_configuration(
|
|
|
609
609
|
out.extend(_scan_commented_controls(source, rel, xml=False))
|
|
610
610
|
out.extend(_scan_java(blank_java_comments(source), rel))
|
|
611
611
|
|
|
612
|
+
# C3-64 — one pruned walk for all three populations. This used to be five
|
|
613
|
+
# `rglob` calls (one per config suffix, one for descriptors, one for
|
|
614
|
+
# mappers), none of which could skip a subtree: the cost was set by the size
|
|
615
|
+
# of `target/`, `node_modules/` and `.git` rather than by the source. The
|
|
616
|
+
# ignore set is the walker's own (`repository_ir.PRUNED_TREE_DIRS`), so this
|
|
617
|
+
# surface and Java discovery cannot disagree about what is source.
|
|
618
|
+
from sourcecode.repository_ir import walk_source_tree
|
|
619
|
+
|
|
620
|
+
_walked_config: "list[str]" = []
|
|
621
|
+
descriptors: "list[str]" = []
|
|
622
|
+
mappers: "list[str]" = []
|
|
623
|
+
for path in walk_source_tree(root):
|
|
624
|
+
name = path.name
|
|
625
|
+
lower = name.lower()
|
|
626
|
+
try:
|
|
627
|
+
rel_posix = path.relative_to(root).as_posix()
|
|
628
|
+
except ValueError:
|
|
629
|
+
continue
|
|
630
|
+
if name.startswith("application") and name.endswith(_CONFIG_SUFFIXES):
|
|
631
|
+
_walked_config.append(rel_posix)
|
|
632
|
+
elif lower.endswith(".xml"):
|
|
633
|
+
# A descriptor that also ends in `Mapper.xml` stays a descriptor —
|
|
634
|
+
# the old code took the same side by subtracting one set from the other.
|
|
635
|
+
if lower in _DESCRIPTOR_NAMES or lower.endswith(_DESCRIPTOR_SUFFIXES):
|
|
636
|
+
descriptors.append(rel_posix)
|
|
637
|
+
elif name.endswith("Mapper.xml"):
|
|
638
|
+
mappers.append(rel_posix)
|
|
639
|
+
|
|
612
640
|
if config_files is None:
|
|
613
|
-
config_files =
|
|
614
|
-
for suffix in _CONFIG_SUFFIXES:
|
|
615
|
-
config_files += [
|
|
616
|
-
p.relative_to(root).as_posix()
|
|
617
|
-
for p in root.rglob(f"application*{suffix}")
|
|
618
|
-
]
|
|
641
|
+
config_files = _walked_config
|
|
619
642
|
for rel in sorted(set(config_files)):
|
|
620
643
|
try:
|
|
621
644
|
text = (root / rel).read_text(encoding="utf-8", errors="replace")
|
|
@@ -624,18 +647,10 @@ def scan_security_configuration(
|
|
|
624
647
|
out.extend(_scan_commented_config_controls(text, rel))
|
|
625
648
|
out.extend(_scan_config(blank_hash_comments(text), rel))
|
|
626
649
|
|
|
627
|
-
# CL-10 — the descriptors.
|
|
650
|
+
# CL-10 — the descriptors. Classified the same way `environment_resolution`
|
|
628
651
|
# classifies them, so the two surfaces cannot disagree about which files are
|
|
629
652
|
# deployment descriptors, and a per-environment overlay tree is covered
|
|
630
653
|
# because the walk finds the file wherever the build put it.
|
|
631
|
-
descriptors: "list[str]" = []
|
|
632
|
-
try:
|
|
633
|
-
for path in root.rglob("*.xml"):
|
|
634
|
-
name = path.name.lower()
|
|
635
|
-
if name in _DESCRIPTOR_NAMES or name.endswith(_DESCRIPTOR_SUFFIXES):
|
|
636
|
-
descriptors.append(path.relative_to(root).as_posix())
|
|
637
|
-
except OSError:
|
|
638
|
-
pass
|
|
639
654
|
for rel in sorted(set(descriptors)):
|
|
640
655
|
try:
|
|
641
656
|
text = (root / rel).read_text(encoding="utf-8", errors="replace")
|
|
@@ -644,13 +659,6 @@ def scan_security_configuration(
|
|
|
644
659
|
out.extend(_scan_commented_controls(text, rel, xml=True))
|
|
645
660
|
out.extend(_scan_descriptor(blank_xml_comments(text), rel))
|
|
646
661
|
|
|
647
|
-
mappers: "list[str]" = []
|
|
648
|
-
try:
|
|
649
|
-
for path in root.rglob("*.xml"):
|
|
650
|
-
if path.name.endswith("Mapper.xml"):
|
|
651
|
-
mappers.append(path.relative_to(root).as_posix())
|
|
652
|
-
except OSError:
|
|
653
|
-
pass
|
|
654
662
|
for rel in sorted(set(mappers) - set(descriptors)):
|
|
655
663
|
try:
|
|
656
664
|
text = (root / rel).read_text(encoding="utf-8", errors="replace")
|