specfact-cli 0.24.0__tar.gz → 0.24.1__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.
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/.gitignore +2 -1
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/PKG-INFO +1 -1
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/pyproject.toml +1 -1
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/__init__.py +1 -1
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/__init__.py +1 -1
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/commands/validate.py +117 -2
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/sidecar/contract_populator.py +37 -31
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/sidecar/crosshair_runner.py +15 -4
- specfact_cli-0.24.1/src/specfact_cli/validators/sidecar/crosshair_summary.py +268 -0
- specfact_cli-0.24.1/src/specfact_cli/validators/sidecar/dependency_installer.py +234 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/sidecar/framework_detector.py +2 -2
- specfact_cli-0.24.1/src/specfact_cli/validators/sidecar/frameworks/flask.py +298 -0
- specfact_cli-0.24.1/src/specfact_cli/validators/sidecar/harness_generator.py +698 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/sidecar/models.py +15 -3
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/sidecar/orchestrator.py +95 -5
- specfact_cli-0.24.0/src/specfact_cli/validators/sidecar/crosshair_summary.py +0 -164
- specfact_cli-0.24.0/src/specfact_cli/validators/sidecar/harness_generator.py +0 -148
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/LICENSE.md +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/README.md +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/mappings/node-async.yaml +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/mappings/python-async.yaml +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/mappings/speckit-default.yaml +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/prompts/shared/cli-enforcement.md +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/prompts/specfact.01-import.md +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/prompts/specfact.02-plan.md +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/prompts/specfact.03-review.md +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/prompts/specfact.04-sdd.md +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/prompts/specfact.05-enforce.md +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/prompts/specfact.06-sync.md +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/prompts/specfact.07-contracts.md +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/prompts/specfact.compare.md +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/prompts/specfact.sync-backlog.md +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/prompts/specfact.validate.md +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/schemas/deviation.schema.json +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/schemas/plan.schema.json +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/schemas/protocol.schema.json +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/github-action.yml.j2 +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/persona/architect.md.j2 +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/persona/developer.md.j2 +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/persona/product-owner.md.j2 +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/plan.bundle.yaml.j2 +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/pr-template.md.j2 +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/protocol.yaml.j2 +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/STRUCTURE.md +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/__init__.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/common/README.md +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/common/adapters.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/common/bindings.yaml.example +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/common/crosshair_plugin.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/common/generate_harness.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/common/harness_contracts.py.example +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/common/populate_contracts.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/common/run_sidecar.sh +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/common/sidecar-init.sh +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/frameworks/django/__init__.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/frameworks/django/crosshair_django_wrapper.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/frameworks/django/django_form_extractor.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/frameworks/django/django_url_extractor.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/frameworks/drf/__init__.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/frameworks/drf/drf_serializer_extractor.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/frameworks/fastapi/__init__.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/frameworks/fastapi/fastapi_route_extractor.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/telemetry.yaml.example +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/adapters/__init__.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/adapters/base.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/adapters/github.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/adapters/openspec.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/adapters/openspec_parser.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/adapters/registry.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/adapters/speckit.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/agents/__init__.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/agents/analyze_agent.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/agents/base.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/agents/plan_agent.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/agents/registry.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/agents/sync_agent.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/analyzers/__init__.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/analyzers/ambiguity_scanner.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/analyzers/code_analyzer.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/analyzers/constitution_evidence_extractor.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/analyzers/contract_extractor.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/analyzers/control_flow_analyzer.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/analyzers/graph_analyzer.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/analyzers/relationship_mapper.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/analyzers/requirement_extractor.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/analyzers/test_pattern_extractor.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/cli.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/commands/__init__.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/commands/analyze.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/commands/contract_cmd.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/commands/drift.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/commands/enforce.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/commands/generate.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/commands/import_cmd.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/commands/init.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/commands/migrate.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/commands/plan.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/commands/project_cmd.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/commands/repro.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/commands/sdd.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/commands/spec.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/commands/sync.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/common/__init__.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/common/logger_setup.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/common/logging_utils.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/common/text_utils.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/common/utils.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/comparators/__init__.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/comparators/plan_comparator.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/enrichers/constitution_enricher.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/enrichers/plan_enricher.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/generators/__init__.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/generators/contract_generator.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/generators/openapi_extractor.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/generators/persona_exporter.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/generators/plan_generator.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/generators/protocol_generator.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/generators/report_generator.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/generators/task_generator.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/generators/test_to_openapi.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/generators/workflow_generator.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/importers/__init__.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/importers/speckit_converter.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/importers/speckit_scanner.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/integrations/__init__.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/integrations/specmatic.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/merge/__init__.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/merge/resolver.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/migrations/__init__.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/migrations/plan_migrator.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/models/__init__.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/models/bridge.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/models/capabilities.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/models/change.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/models/contract.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/models/deviation.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/models/enforcement.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/models/persona_template.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/models/plan.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/models/project.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/models/protocol.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/models/quality.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/models/sdd.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/models/source_tracking.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/models/task.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/modes/__init__.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/modes/detector.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/modes/router.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/parsers/__init__.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/parsers/persona_importer.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/resources/semgrep/async.yml +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/resources/semgrep/code-quality.yml +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/resources/semgrep/feature-detection.yml +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/runtime.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/sync/__init__.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/sync/bridge_probe.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/sync/bridge_sync.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/sync/bridge_watch.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/sync/change_detector.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/sync/code_to_spec.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/sync/drift_detector.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/sync/repository_sync.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/sync/spec_to_code.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/sync/spec_to_tests.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/sync/watcher.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/sync/watcher_enhanced.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/telemetry.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/templates/__init__.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/templates/bridge_templates.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/templates/specification_templates.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/__init__.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/acceptance_criteria.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/bundle_loader.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/code_change_detector.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/console.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/content_sanitizer.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/context_detection.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/enrichment_context.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/enrichment_parser.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/env_manager.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/feature_keys.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/git.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/github_annotations.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/ide_setup.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/incremental_check.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/optional_deps.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/performance.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/progress.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/progressive_disclosure.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/prompts.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/sdd_discovery.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/source_scanner.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/structure.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/structured_io.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/suggestions.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/terminal.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/yaml_utils.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/__init__.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/agile_validation.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/cli_first_validator.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/contract_validator.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/fsm.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/repro_checker.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/schema.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/sidecar/__init__.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/sidecar/frameworks/__init__.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/sidecar/frameworks/base.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/sidecar/frameworks/django.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/sidecar/frameworks/drf.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/sidecar/frameworks/fastapi.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/sidecar/specmatic_runner.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/sidecar/unannotated_detector.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/versioning/__init__.py +0 -0
- {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/versioning/analyzer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: specfact-cli
|
|
3
|
-
Version: 0.24.
|
|
3
|
+
Version: 0.24.1
|
|
4
4
|
Summary: Brownfield-first CLI: Reverse engineer legacy Python → specs → enforced contracts. Automate legacy code documentation and prevent modernization regressions.
|
|
5
5
|
Project-URL: Homepage, https://github.com/nold-ai/specfact-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/nold-ai/specfact-cli.git
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "specfact-cli"
|
|
7
|
-
version = "0.24.
|
|
7
|
+
version = "0.24.1"
|
|
8
8
|
description = "Brownfield-first CLI: Reverse engineer legacy Python → specs → enforced contracts. Automate legacy code documentation and prevent modernization regressions."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -6,6 +6,7 @@ This module provides validation commands including sidecar validation.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import re
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
|
|
11
12
|
import typer
|
|
@@ -21,6 +22,92 @@ from specfact_cli.validators.sidecar.orchestrator import initialize_sidecar_work
|
|
|
21
22
|
app = typer.Typer(name="validate", help="Validation commands", suggest_commands=False)
|
|
22
23
|
console = get_configured_console()
|
|
23
24
|
|
|
25
|
+
|
|
26
|
+
@beartype
|
|
27
|
+
def _format_crosshair_error(stderr: str, stdout: str) -> str:
|
|
28
|
+
"""
|
|
29
|
+
Format CrossHair error messages into user-friendly text.
|
|
30
|
+
|
|
31
|
+
Filters out technical errors (like Rich markup errors) and provides
|
|
32
|
+
actionable error messages.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
stderr: CrossHair stderr output
|
|
36
|
+
stdout: CrossHair stdout output
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
User-friendly error message or empty string if no actionable error
|
|
40
|
+
"""
|
|
41
|
+
combined = (stderr + "\n" + stdout).strip()
|
|
42
|
+
if not combined:
|
|
43
|
+
return ""
|
|
44
|
+
|
|
45
|
+
# Filter out Rich markup errors - these are internal errors, not user-facing
|
|
46
|
+
error_lower = combined.lower()
|
|
47
|
+
if "closing tag" in error_lower and "doesn't match any open tag" in error_lower:
|
|
48
|
+
# This is a Rich internal error - ignore it completely
|
|
49
|
+
return ""
|
|
50
|
+
|
|
51
|
+
# Detect common error patterns and provide user-friendly messages
|
|
52
|
+
# Python shared library issue (venv Python can't load libraries)
|
|
53
|
+
if "error while loading shared libraries" in error_lower or "libpython" in error_lower:
|
|
54
|
+
return (
|
|
55
|
+
"Python environment issue detected. CrossHair is using system Python instead. "
|
|
56
|
+
"This is usually harmless - validation will continue with system Python."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# CrossHair not found
|
|
60
|
+
if "not found" in error_lower and ("crosshair" in error_lower or "command" in error_lower):
|
|
61
|
+
return "CrossHair is not installed or not in PATH. Install it with: pip install crosshair-tool"
|
|
62
|
+
|
|
63
|
+
# Timeout
|
|
64
|
+
if "timeout" in error_lower or "timed out" in error_lower:
|
|
65
|
+
return (
|
|
66
|
+
"CrossHair analysis timed out. This is expected for complex applications with many routes. "
|
|
67
|
+
"Some routes were analyzed before timeout. Check the summary file for partial results. "
|
|
68
|
+
"To analyze more routes, increase --crosshair-timeout or --crosshair-per-path-timeout."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Import errors
|
|
72
|
+
if "importerror" in error_lower or "module not found" in error_lower:
|
|
73
|
+
module_match = re.search(r"no module named ['\"]([^'\"]+)['\"]", error_lower)
|
|
74
|
+
if module_match:
|
|
75
|
+
module_name = module_match.group(1)
|
|
76
|
+
return (
|
|
77
|
+
f"Missing Python module: {module_name}. "
|
|
78
|
+
"Ensure all dependencies are installed in the sidecar environment."
|
|
79
|
+
)
|
|
80
|
+
return "Missing Python module. Ensure all dependencies are installed."
|
|
81
|
+
|
|
82
|
+
# Syntax errors in harness
|
|
83
|
+
if "syntaxerror" in error_lower or "syntax error" in error_lower:
|
|
84
|
+
return (
|
|
85
|
+
"Syntax error in generated harness. This may indicate an issue with contract generation. "
|
|
86
|
+
"Check the harness file for errors."
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Generic error - show a sanitized version (remove paths, technical details)
|
|
90
|
+
# Only show first line and remove technical details
|
|
91
|
+
lines = combined.split("\n")
|
|
92
|
+
first_line = lines[0].strip() if lines else ""
|
|
93
|
+
|
|
94
|
+
# Remove common technical noise
|
|
95
|
+
first_line = re.sub(r"Error: closing tag.*", "", first_line, flags=re.IGNORECASE)
|
|
96
|
+
first_line = re.sub(r"at position \d+", "", first_line, flags=re.IGNORECASE)
|
|
97
|
+
first_line = re.sub(r"\.specfact/venv/bin/python.*", "", first_line)
|
|
98
|
+
first_line = re.sub(r"error while loading shared libraries.*", "", first_line, flags=re.IGNORECASE)
|
|
99
|
+
|
|
100
|
+
# If we have a clean message, show it (limited length)
|
|
101
|
+
if first_line and len(first_line) > 10:
|
|
102
|
+
# Limit to reasonable length
|
|
103
|
+
if len(first_line) > 150:
|
|
104
|
+
first_line = first_line[:147] + "..."
|
|
105
|
+
return first_line
|
|
106
|
+
|
|
107
|
+
# Fallback: generic message
|
|
108
|
+
return "CrossHair execution failed. Check logs for details."
|
|
109
|
+
|
|
110
|
+
|
|
24
111
|
# Create sidecar subcommand group
|
|
25
112
|
sidecar_app = typer.Typer(name="sidecar", help="Sidecar validation commands", suggest_commands=False)
|
|
26
113
|
app.add_typer(sidecar_app)
|
|
@@ -136,15 +223,43 @@ def run(
|
|
|
136
223
|
status = "[green]✓[/green]" if success else "[red]✗[/red]"
|
|
137
224
|
console.print(f" {status} {key}")
|
|
138
225
|
|
|
226
|
+
# Display user-friendly error messages if CrossHair failed
|
|
227
|
+
if not success:
|
|
228
|
+
stderr = value.get("stderr", "")
|
|
229
|
+
stdout = value.get("stdout", "")
|
|
230
|
+
error_message = _format_crosshair_error(stderr, stdout)
|
|
231
|
+
if error_message:
|
|
232
|
+
# Use markup=False to prevent Rich from parsing brackets in error messages
|
|
233
|
+
# This prevents Rich markup errors when error messages contain brackets
|
|
234
|
+
try:
|
|
235
|
+
console.print(" [red]Error:[/red]", end=" ")
|
|
236
|
+
console.print(error_message, markup=False)
|
|
237
|
+
except Exception:
|
|
238
|
+
# If Rich itself fails (shouldn't happen with markup=False, but be safe)
|
|
239
|
+
# Fall back to plain print
|
|
240
|
+
print(f" Error: {error_message}")
|
|
241
|
+
|
|
139
242
|
# Display summary if available
|
|
140
243
|
if results.get("crosshair_summary"):
|
|
141
244
|
summary = results["crosshair_summary"]
|
|
142
245
|
summary_line = format_summary_line(summary)
|
|
143
|
-
|
|
246
|
+
# Use try/except to catch Rich parsing errors
|
|
247
|
+
try:
|
|
248
|
+
console.print(f" {summary_line}")
|
|
249
|
+
except Exception:
|
|
250
|
+
# Fall back to plain print if Rich fails
|
|
251
|
+
print(f" {summary_line}")
|
|
144
252
|
|
|
145
253
|
# Show summary file location if generated
|
|
146
254
|
if results.get("crosshair_summary_file"):
|
|
147
|
-
|
|
255
|
+
summary_file_path = results["crosshair_summary_file"]
|
|
256
|
+
# Use markup=False for paths to prevent Rich from parsing brackets
|
|
257
|
+
try:
|
|
258
|
+
console.print(" Summary file: ", end="")
|
|
259
|
+
console.print(str(summary_file_path), markup=False)
|
|
260
|
+
except Exception:
|
|
261
|
+
# Fall back to plain print if Rich fails
|
|
262
|
+
print(f" Summary file: {summary_file_path}")
|
|
148
263
|
|
|
149
264
|
if results.get("specmatic_skipped"):
|
|
150
265
|
console.print(
|
|
@@ -37,6 +37,7 @@ def populate_contracts(contracts_dir: Path, routes: list[RouteInfo], schemas: di
|
|
|
37
37
|
return 0
|
|
38
38
|
|
|
39
39
|
populated_count = 0
|
|
40
|
+
total_paths = 0
|
|
40
41
|
|
|
41
42
|
for contract_file in contract_files:
|
|
42
43
|
try:
|
|
@@ -44,11 +45,16 @@ def populate_contracts(contracts_dir: Path, routes: list[RouteInfo], schemas: di
|
|
|
44
45
|
if populate_contract(contract_data, routes, schemas):
|
|
45
46
|
save_contract(contract_file, contract_data)
|
|
46
47
|
populated_count += 1
|
|
48
|
+
paths_after = len(contract_data.get("paths", {}))
|
|
49
|
+
# Count total paths in contract (whether newly added or already existed)
|
|
50
|
+
total_paths = max(total_paths, paths_after)
|
|
47
51
|
except Exception:
|
|
48
52
|
# Skip contracts that can't be processed
|
|
49
53
|
continue
|
|
50
54
|
|
|
51
|
-
|
|
55
|
+
# Return total number of paths in contracts (gives better indication of what was populated)
|
|
56
|
+
# If no paths found, return number of contracts modified as fallback
|
|
57
|
+
return total_paths if total_paths > 0 else populated_count
|
|
52
58
|
|
|
53
59
|
|
|
54
60
|
@beartype
|
|
@@ -108,38 +114,38 @@ def populate_contract(
|
|
|
108
114
|
|
|
109
115
|
for route in routes:
|
|
110
116
|
route_id = f"{route.method}:{route.path}"
|
|
111
|
-
if
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
"
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
117
|
+
# Add route to paths if not already present
|
|
118
|
+
if route.path not in contract_data["paths"]:
|
|
119
|
+
contract_data["paths"][route.path] = {}
|
|
120
|
+
|
|
121
|
+
method_lower = route.method.lower()
|
|
122
|
+
if method_lower not in contract_data["paths"][route.path]:
|
|
123
|
+
operation = {
|
|
124
|
+
"operationId": route.operation_id,
|
|
125
|
+
"summary": f"{route.method} {route.path}",
|
|
126
|
+
"responses": {
|
|
127
|
+
"200": {"description": "Success"},
|
|
128
|
+
"400": {"description": "Bad request"},
|
|
129
|
+
"500": {"description": "Internal server error"},
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if route.path_params:
|
|
134
|
+
operation["parameters"] = route.path_params
|
|
135
|
+
|
|
136
|
+
# Add requestBody only if schema is available for POST/PUT/PATCH methods
|
|
137
|
+
if route.method.upper() in ("POST", "PUT", "PATCH"):
|
|
138
|
+
schema = schemas.get(route_id, {})
|
|
139
|
+
if schema:
|
|
140
|
+
operation["requestBody"] = {
|
|
141
|
+
"content": {
|
|
142
|
+
"application/json": {
|
|
143
|
+
"schema": schema,
|
|
139
144
|
}
|
|
140
145
|
}
|
|
146
|
+
}
|
|
141
147
|
|
|
142
|
-
|
|
143
|
-
|
|
148
|
+
contract_data["paths"][route.path][method_lower] = operation
|
|
149
|
+
modified = True
|
|
144
150
|
|
|
145
151
|
return modified
|
{specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/sidecar/crosshair_runner.py
RENAMED
|
@@ -30,6 +30,7 @@ def run_crosshair(
|
|
|
30
30
|
inputs_path: Path | None = None,
|
|
31
31
|
per_path_timeout: int | None = None,
|
|
32
32
|
per_condition_timeout: int | None = None,
|
|
33
|
+
python_cmd: str | None = None,
|
|
33
34
|
) -> dict[str, Any]:
|
|
34
35
|
"""
|
|
35
36
|
Run CrossHair on source code or harness.
|
|
@@ -40,6 +41,10 @@ def run_crosshair(
|
|
|
40
41
|
pythonpath: PYTHONPATH for execution
|
|
41
42
|
verbose: Enable verbose output
|
|
42
43
|
repo_path: Optional repository path for environment manager detection
|
|
44
|
+
inputs_path: Optional path to deterministic inputs JSON file
|
|
45
|
+
per_path_timeout: Optional timeout per execution path
|
|
46
|
+
per_condition_timeout: Optional timeout per condition
|
|
47
|
+
python_cmd: Optional Python command to use (e.g., venv Python path)
|
|
43
48
|
|
|
44
49
|
Returns:
|
|
45
50
|
Dictionary with execution results
|
|
@@ -49,14 +54,20 @@ def run_crosshair(
|
|
|
49
54
|
if pythonpath:
|
|
50
55
|
env["PYTHONPATH"] = pythonpath
|
|
51
56
|
|
|
52
|
-
# Build command using
|
|
53
|
-
|
|
57
|
+
# Build command using venv Python if available, otherwise use system CrossHair
|
|
58
|
+
python_cmd_path = Path(python_cmd) if python_cmd else None
|
|
59
|
+
if python_cmd_path and python_cmd_path.exists():
|
|
60
|
+
# Use venv Python to run CrossHair module
|
|
61
|
+
base_cmd = [str(python_cmd_path), "-m", "crosshair", "check", str(source_path)]
|
|
62
|
+
else:
|
|
63
|
+
# Fall back to system CrossHair
|
|
64
|
+
base_cmd = ["crosshair", "check", str(source_path)]
|
|
54
65
|
if verbose:
|
|
55
66
|
base_cmd.append("--verbose")
|
|
56
67
|
if per_path_timeout:
|
|
57
|
-
base_cmd.extend(["--
|
|
68
|
+
base_cmd.extend(["--per_path_timeout", str(per_path_timeout)])
|
|
58
69
|
if per_condition_timeout:
|
|
59
|
-
base_cmd.extend(["--
|
|
70
|
+
base_cmd.extend(["--per_condition_timeout", str(per_condition_timeout)])
|
|
60
71
|
# Note: CrossHair doesn't directly support inputs.json, but deterministic inputs
|
|
61
72
|
# can be embedded in the harness file itself
|
|
62
73
|
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CrossHair summary parser for sidecar validation.
|
|
3
|
+
|
|
4
|
+
This module parses CrossHair output to extract summary statistics
|
|
5
|
+
(confirmed, not confirmed, violations counts).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from beartype import beartype
|
|
16
|
+
from icontract import ensure
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@beartype
|
|
20
|
+
@ensure(lambda result: isinstance(result, dict), "Must return dict")
|
|
21
|
+
@ensure(lambda result: "confirmed" in result, "Must include confirmed count")
|
|
22
|
+
@ensure(lambda result: "not_confirmed" in result, "Must include not_confirmed count")
|
|
23
|
+
@ensure(lambda result: "violations" in result, "Must include violations count")
|
|
24
|
+
def parse_crosshair_output(stdout: str, stderr: str) -> dict[str, Any]:
|
|
25
|
+
"""
|
|
26
|
+
Parse CrossHair output to extract summary statistics and detailed violations.
|
|
27
|
+
|
|
28
|
+
CrossHair output format:
|
|
29
|
+
- By default, only reports "Rejected" (violations)
|
|
30
|
+
- With --report_all, reports "Confirmed", "Rejected", and "Unknown"
|
|
31
|
+
- Output format: "FunctionName: <status>" or "FunctionName: <status> <details>"
|
|
32
|
+
- Counterexamples: "FunctionName: Rejected (counterexample: x=5, result=-5)"
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
stdout: CrossHair stdout output
|
|
36
|
+
stderr: CrossHair stderr output
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Dictionary with summary statistics and detailed violations:
|
|
40
|
+
- confirmed: int - Number of confirmed contracts
|
|
41
|
+
- not_confirmed: int - Number of not confirmed (unknown) contracts
|
|
42
|
+
- violations: int - Number of violations (rejected) contracts
|
|
43
|
+
- total: int - Total number of contracts analyzed
|
|
44
|
+
- violation_details: list[dict] - Detailed violation information with counterexamples
|
|
45
|
+
"""
|
|
46
|
+
confirmed = 0
|
|
47
|
+
not_confirmed = 0
|
|
48
|
+
violations = 0
|
|
49
|
+
violation_details: list[dict[str, Any]] = []
|
|
50
|
+
|
|
51
|
+
# Combine stdout and stderr for parsing
|
|
52
|
+
combined_output = stdout + "\n" + stderr
|
|
53
|
+
|
|
54
|
+
# Pattern for CrossHair output lines
|
|
55
|
+
# Examples:
|
|
56
|
+
# "function_name: Confirmed" or "function_name: Confirmed over all paths"
|
|
57
|
+
# "function_name: Rejected (counterexample: ...)"
|
|
58
|
+
# "function_name: Unknown" or "function_name: Not confirmed"
|
|
59
|
+
# "function_name: <status>"
|
|
60
|
+
confirmed_pattern = re.compile(r":\s*Confirmed", re.IGNORECASE)
|
|
61
|
+
rejected_pattern = re.compile(r":\s*Rejected\b", re.IGNORECASE)
|
|
62
|
+
unknown_pattern = re.compile(r":\s*(Unknown|Not confirmed)", re.IGNORECASE)
|
|
63
|
+
|
|
64
|
+
# Pattern for extracting function name and counterexample
|
|
65
|
+
# Format: "function_name: Rejected (counterexample: x=5, result=-5)"
|
|
66
|
+
counterexample_pattern = re.compile(
|
|
67
|
+
r"^([^:]+):\s*Rejected\s*\(counterexample:\s*(.+?)\)", re.IGNORECASE | re.MULTILINE
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Pattern for extracting function name from status lines
|
|
71
|
+
function_name_pattern = re.compile(r"^([^:]+):", re.MULTILINE)
|
|
72
|
+
|
|
73
|
+
# Extract counterexamples first
|
|
74
|
+
counterexamples = counterexample_pattern.findall(combined_output)
|
|
75
|
+
for func_name, counterexample_str in counterexamples:
|
|
76
|
+
# Parse counterexample string (e.g., "x=5, result=-5")
|
|
77
|
+
counterexample_dict: dict[str, Any] = {}
|
|
78
|
+
for part in counterexample_str.split(","):
|
|
79
|
+
part = part.strip()
|
|
80
|
+
if "=" in part:
|
|
81
|
+
key, value = part.split("=", 1)
|
|
82
|
+
key = key.strip()
|
|
83
|
+
value = value.strip()
|
|
84
|
+
# Try to parse value as appropriate type
|
|
85
|
+
try:
|
|
86
|
+
if value.startswith('"') and value.endswith('"'):
|
|
87
|
+
counterexample_dict[key] = value[1:-1] # String
|
|
88
|
+
elif value.lower() in ("true", "false"):
|
|
89
|
+
counterexample_dict[key] = value.lower() == "true"
|
|
90
|
+
elif "." in value:
|
|
91
|
+
counterexample_dict[key] = float(value)
|
|
92
|
+
else:
|
|
93
|
+
counterexample_dict[key] = int(value)
|
|
94
|
+
except (ValueError, AttributeError):
|
|
95
|
+
counterexample_dict[key] = value # Keep as string if parsing fails
|
|
96
|
+
|
|
97
|
+
violation_details.append(
|
|
98
|
+
{
|
|
99
|
+
"function": func_name.strip(),
|
|
100
|
+
"counterexample": counterexample_dict,
|
|
101
|
+
"raw": f"{func_name}: Rejected (counterexample: {counterexample_str})",
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Count by status
|
|
106
|
+
lines = combined_output.split("\n")
|
|
107
|
+
for line in lines:
|
|
108
|
+
if confirmed_pattern.search(line):
|
|
109
|
+
confirmed += 1
|
|
110
|
+
elif rejected_pattern.search(line):
|
|
111
|
+
violations += 1
|
|
112
|
+
# If we haven't captured this violation yet, try to extract function name
|
|
113
|
+
if not any(v["function"] in line for v in violation_details):
|
|
114
|
+
match = function_name_pattern.match(line)
|
|
115
|
+
if match:
|
|
116
|
+
func_name = match.group(1).strip()
|
|
117
|
+
# Filter out paths - only keep valid function names
|
|
118
|
+
# Skip if it looks like a path (contains / or starts with /)
|
|
119
|
+
if "/" not in func_name and not func_name.startswith("/"):
|
|
120
|
+
violation_details.append(
|
|
121
|
+
{
|
|
122
|
+
"function": func_name,
|
|
123
|
+
"counterexample": {},
|
|
124
|
+
"raw": line.strip(),
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
elif unknown_pattern.search(line):
|
|
128
|
+
not_confirmed += 1
|
|
129
|
+
|
|
130
|
+
# If no explicit status found but there's output, check for error patterns
|
|
131
|
+
# CrossHair may report violations in different formats
|
|
132
|
+
if confirmed == 0 and not_confirmed == 0 and violations == 0:
|
|
133
|
+
# Check for error/violation indicators
|
|
134
|
+
if any(
|
|
135
|
+
keyword in combined_output.lower()
|
|
136
|
+
for keyword in ["error", "violation", "counterexample", "failed", "rejected"]
|
|
137
|
+
):
|
|
138
|
+
# Likely violations but not in standard format
|
|
139
|
+
violations = 1
|
|
140
|
+
# Try to extract function name from error
|
|
141
|
+
match = function_name_pattern.search(combined_output)
|
|
142
|
+
if match:
|
|
143
|
+
func_name = match.group(1).strip()
|
|
144
|
+
# Filter out paths - only keep valid function names
|
|
145
|
+
# Skip if it looks like a path (contains / or starts with /)
|
|
146
|
+
if "/" not in func_name and not func_name.startswith("/"):
|
|
147
|
+
violation_details.append(
|
|
148
|
+
{
|
|
149
|
+
"function": func_name,
|
|
150
|
+
"counterexample": {},
|
|
151
|
+
"raw": combined_output.strip()[:200], # First 200 chars
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
elif combined_output.strip() and "not found" not in combined_output.lower():
|
|
155
|
+
# Has output but no clear status - likely unknown/not confirmed
|
|
156
|
+
not_confirmed = 1
|
|
157
|
+
|
|
158
|
+
total = confirmed + not_confirmed + violations
|
|
159
|
+
|
|
160
|
+
result: dict[str, Any] = {
|
|
161
|
+
"confirmed": confirmed,
|
|
162
|
+
"not_confirmed": not_confirmed,
|
|
163
|
+
"violations": violations,
|
|
164
|
+
"total": total,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
# Add violation details if any were found
|
|
168
|
+
if violation_details:
|
|
169
|
+
result["violation_details"] = violation_details
|
|
170
|
+
|
|
171
|
+
return result
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@beartype
|
|
175
|
+
@ensure(lambda result: result.exists() if result else True, "Summary file path must be valid")
|
|
176
|
+
def generate_summary_file(
|
|
177
|
+
summary: dict[str, Any],
|
|
178
|
+
reports_dir: Path,
|
|
179
|
+
timestamp: str | None = None,
|
|
180
|
+
) -> Path:
|
|
181
|
+
"""
|
|
182
|
+
Generate CrossHair summary JSON file.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
summary: Summary statistics dictionary
|
|
186
|
+
reports_dir: Directory to save summary file (will be created if it doesn't exist)
|
|
187
|
+
timestamp: Optional timestamp for filename (defaults to current time)
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Path to generated summary file
|
|
191
|
+
"""
|
|
192
|
+
from datetime import datetime
|
|
193
|
+
|
|
194
|
+
if timestamp is None:
|
|
195
|
+
timestamp = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
|
|
196
|
+
|
|
197
|
+
# Ensure reports directory exists (creates parent directories if needed)
|
|
198
|
+
reports_dir.mkdir(parents=True, exist_ok=True)
|
|
199
|
+
|
|
200
|
+
# Create summary file path
|
|
201
|
+
summary_file = reports_dir / f"crosshair-summary-{timestamp}.json"
|
|
202
|
+
|
|
203
|
+
# Add metadata to summary
|
|
204
|
+
summary_with_metadata = {
|
|
205
|
+
"timestamp": timestamp,
|
|
206
|
+
"summary": summary,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
# Include violation details if present
|
|
210
|
+
if "violation_details" in summary:
|
|
211
|
+
summary_with_metadata["violation_details"] = summary["violation_details"]
|
|
212
|
+
|
|
213
|
+
# Write summary file
|
|
214
|
+
with summary_file.open("w") as f:
|
|
215
|
+
json.dump(summary_with_metadata, f, indent=2)
|
|
216
|
+
|
|
217
|
+
return summary_file
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@beartype
|
|
221
|
+
@ensure(lambda result: isinstance(result, str), "Must return string")
|
|
222
|
+
def format_summary_line(summary: dict[str, Any]) -> str:
|
|
223
|
+
"""
|
|
224
|
+
Format summary statistics as a single line for console display.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
summary: Summary statistics dictionary (may include violation_details)
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Formatted summary line string
|
|
231
|
+
"""
|
|
232
|
+
confirmed = summary.get("confirmed", 0)
|
|
233
|
+
not_confirmed = summary.get("not_confirmed", 0)
|
|
234
|
+
violations = summary.get("violations", 0)
|
|
235
|
+
total = summary.get("total", 0)
|
|
236
|
+
violation_details = summary.get("violation_details", [])
|
|
237
|
+
|
|
238
|
+
parts = []
|
|
239
|
+
if confirmed > 0:
|
|
240
|
+
parts.append(f"{confirmed} confirmed")
|
|
241
|
+
if not_confirmed > 0:
|
|
242
|
+
parts.append(f"{not_confirmed} not confirmed")
|
|
243
|
+
if violations > 0:
|
|
244
|
+
parts.append(f"{violations} violations")
|
|
245
|
+
# Add violation details if available
|
|
246
|
+
if violation_details:
|
|
247
|
+
# Filter out paths and invalid function names (only keep valid Python identifiers)
|
|
248
|
+
violation_funcs = []
|
|
249
|
+
for v in violation_details[:3]:
|
|
250
|
+
func_name = v.get("function", "unknown")
|
|
251
|
+
# Skip if it looks like a path (contains / or starts with /)
|
|
252
|
+
# Only include if it looks like a valid function name (alphanumeric + underscore)
|
|
253
|
+
if (
|
|
254
|
+
"/" not in func_name
|
|
255
|
+
and not func_name.startswith("/")
|
|
256
|
+
and func_name != "unknown"
|
|
257
|
+
and (func_name.replace("_", "").replace(".", "").isalnum() or func_name.startswith("harness_"))
|
|
258
|
+
):
|
|
259
|
+
violation_funcs.append(func_name)
|
|
260
|
+
|
|
261
|
+
if violation_funcs:
|
|
262
|
+
if len(violation_details) > 3:
|
|
263
|
+
violation_funcs.append(f"... ({len(violation_details) - 3} more)")
|
|
264
|
+
parts.append(f"({', '.join(violation_funcs)})")
|
|
265
|
+
if total == 0:
|
|
266
|
+
parts.append("no contracts analyzed")
|
|
267
|
+
|
|
268
|
+
return f"CrossHair: {', '.join(parts)}" if parts else "CrossHair: no results"
|