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.
Files changed (214) hide show
  1. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/.gitignore +2 -1
  2. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/PKG-INFO +1 -1
  3. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/pyproject.toml +1 -1
  4. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/__init__.py +1 -1
  5. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/__init__.py +1 -1
  6. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/commands/validate.py +117 -2
  7. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/sidecar/contract_populator.py +37 -31
  8. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/sidecar/crosshair_runner.py +15 -4
  9. specfact_cli-0.24.1/src/specfact_cli/validators/sidecar/crosshair_summary.py +268 -0
  10. specfact_cli-0.24.1/src/specfact_cli/validators/sidecar/dependency_installer.py +234 -0
  11. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/sidecar/framework_detector.py +2 -2
  12. specfact_cli-0.24.1/src/specfact_cli/validators/sidecar/frameworks/flask.py +298 -0
  13. specfact_cli-0.24.1/src/specfact_cli/validators/sidecar/harness_generator.py +698 -0
  14. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/sidecar/models.py +15 -3
  15. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/sidecar/orchestrator.py +95 -5
  16. specfact_cli-0.24.0/src/specfact_cli/validators/sidecar/crosshair_summary.py +0 -164
  17. specfact_cli-0.24.0/src/specfact_cli/validators/sidecar/harness_generator.py +0 -148
  18. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/LICENSE.md +0 -0
  19. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/README.md +0 -0
  20. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/mappings/node-async.yaml +0 -0
  21. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/mappings/python-async.yaml +0 -0
  22. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/mappings/speckit-default.yaml +0 -0
  23. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/prompts/shared/cli-enforcement.md +0 -0
  24. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/prompts/specfact.01-import.md +0 -0
  25. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/prompts/specfact.02-plan.md +0 -0
  26. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/prompts/specfact.03-review.md +0 -0
  27. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/prompts/specfact.04-sdd.md +0 -0
  28. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/prompts/specfact.05-enforce.md +0 -0
  29. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/prompts/specfact.06-sync.md +0 -0
  30. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/prompts/specfact.07-contracts.md +0 -0
  31. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/prompts/specfact.compare.md +0 -0
  32. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/prompts/specfact.sync-backlog.md +0 -0
  33. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/prompts/specfact.validate.md +0 -0
  34. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/schemas/deviation.schema.json +0 -0
  35. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/schemas/plan.schema.json +0 -0
  36. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/schemas/protocol.schema.json +0 -0
  37. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/github-action.yml.j2 +0 -0
  38. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/persona/architect.md.j2 +0 -0
  39. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/persona/developer.md.j2 +0 -0
  40. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/persona/product-owner.md.j2 +0 -0
  41. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/plan.bundle.yaml.j2 +0 -0
  42. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/pr-template.md.j2 +0 -0
  43. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/protocol.yaml.j2 +0 -0
  44. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/STRUCTURE.md +0 -0
  45. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/__init__.py +0 -0
  46. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/common/README.md +0 -0
  47. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/common/adapters.py +0 -0
  48. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/common/bindings.yaml.example +0 -0
  49. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/common/crosshair_plugin.py +0 -0
  50. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/common/generate_harness.py +0 -0
  51. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/common/harness_contracts.py.example +0 -0
  52. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/common/populate_contracts.py +0 -0
  53. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/common/run_sidecar.sh +0 -0
  54. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/common/sidecar-init.sh +0 -0
  55. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/frameworks/django/__init__.py +0 -0
  56. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/frameworks/django/crosshair_django_wrapper.py +0 -0
  57. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/frameworks/django/django_form_extractor.py +0 -0
  58. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/frameworks/django/django_url_extractor.py +0 -0
  59. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/frameworks/drf/__init__.py +0 -0
  60. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/frameworks/drf/drf_serializer_extractor.py +0 -0
  61. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/frameworks/fastapi/__init__.py +0 -0
  62. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/sidecar/frameworks/fastapi/fastapi_route_extractor.py +0 -0
  63. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/resources/templates/telemetry.yaml.example +0 -0
  64. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/adapters/__init__.py +0 -0
  65. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/adapters/base.py +0 -0
  66. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/adapters/github.py +0 -0
  67. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/adapters/openspec.py +0 -0
  68. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/adapters/openspec_parser.py +0 -0
  69. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/adapters/registry.py +0 -0
  70. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/adapters/speckit.py +0 -0
  71. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/agents/__init__.py +0 -0
  72. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/agents/analyze_agent.py +0 -0
  73. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/agents/base.py +0 -0
  74. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/agents/plan_agent.py +0 -0
  75. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/agents/registry.py +0 -0
  76. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/agents/sync_agent.py +0 -0
  77. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/analyzers/__init__.py +0 -0
  78. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/analyzers/ambiguity_scanner.py +0 -0
  79. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/analyzers/code_analyzer.py +0 -0
  80. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/analyzers/constitution_evidence_extractor.py +0 -0
  81. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/analyzers/contract_extractor.py +0 -0
  82. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/analyzers/control_flow_analyzer.py +0 -0
  83. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/analyzers/graph_analyzer.py +0 -0
  84. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/analyzers/relationship_mapper.py +0 -0
  85. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/analyzers/requirement_extractor.py +0 -0
  86. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/analyzers/test_pattern_extractor.py +0 -0
  87. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/cli.py +0 -0
  88. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/commands/__init__.py +0 -0
  89. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/commands/analyze.py +0 -0
  90. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/commands/contract_cmd.py +0 -0
  91. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/commands/drift.py +0 -0
  92. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/commands/enforce.py +0 -0
  93. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/commands/generate.py +0 -0
  94. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/commands/import_cmd.py +0 -0
  95. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/commands/init.py +0 -0
  96. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/commands/migrate.py +0 -0
  97. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/commands/plan.py +0 -0
  98. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/commands/project_cmd.py +0 -0
  99. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/commands/repro.py +0 -0
  100. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/commands/sdd.py +0 -0
  101. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/commands/spec.py +0 -0
  102. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/commands/sync.py +0 -0
  103. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/common/__init__.py +0 -0
  104. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/common/logger_setup.py +0 -0
  105. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/common/logging_utils.py +0 -0
  106. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/common/text_utils.py +0 -0
  107. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/common/utils.py +0 -0
  108. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/comparators/__init__.py +0 -0
  109. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/comparators/plan_comparator.py +0 -0
  110. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/enrichers/constitution_enricher.py +0 -0
  111. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/enrichers/plan_enricher.py +0 -0
  112. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/generators/__init__.py +0 -0
  113. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/generators/contract_generator.py +0 -0
  114. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/generators/openapi_extractor.py +0 -0
  115. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/generators/persona_exporter.py +0 -0
  116. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/generators/plan_generator.py +0 -0
  117. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/generators/protocol_generator.py +0 -0
  118. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/generators/report_generator.py +0 -0
  119. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/generators/task_generator.py +0 -0
  120. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/generators/test_to_openapi.py +0 -0
  121. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/generators/workflow_generator.py +0 -0
  122. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/importers/__init__.py +0 -0
  123. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/importers/speckit_converter.py +0 -0
  124. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/importers/speckit_scanner.py +0 -0
  125. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/integrations/__init__.py +0 -0
  126. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/integrations/specmatic.py +0 -0
  127. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/merge/__init__.py +0 -0
  128. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/merge/resolver.py +0 -0
  129. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/migrations/__init__.py +0 -0
  130. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/migrations/plan_migrator.py +0 -0
  131. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/models/__init__.py +0 -0
  132. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/models/bridge.py +0 -0
  133. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/models/capabilities.py +0 -0
  134. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/models/change.py +0 -0
  135. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/models/contract.py +0 -0
  136. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/models/deviation.py +0 -0
  137. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/models/enforcement.py +0 -0
  138. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/models/persona_template.py +0 -0
  139. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/models/plan.py +0 -0
  140. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/models/project.py +0 -0
  141. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/models/protocol.py +0 -0
  142. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/models/quality.py +0 -0
  143. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/models/sdd.py +0 -0
  144. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/models/source_tracking.py +0 -0
  145. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/models/task.py +0 -0
  146. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/modes/__init__.py +0 -0
  147. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/modes/detector.py +0 -0
  148. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/modes/router.py +0 -0
  149. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/parsers/__init__.py +0 -0
  150. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/parsers/persona_importer.py +0 -0
  151. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/resources/semgrep/async.yml +0 -0
  152. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/resources/semgrep/code-quality.yml +0 -0
  153. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/resources/semgrep/feature-detection.yml +0 -0
  154. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/runtime.py +0 -0
  155. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/sync/__init__.py +0 -0
  156. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/sync/bridge_probe.py +0 -0
  157. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/sync/bridge_sync.py +0 -0
  158. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/sync/bridge_watch.py +0 -0
  159. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/sync/change_detector.py +0 -0
  160. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/sync/code_to_spec.py +0 -0
  161. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/sync/drift_detector.py +0 -0
  162. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/sync/repository_sync.py +0 -0
  163. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/sync/spec_to_code.py +0 -0
  164. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/sync/spec_to_tests.py +0 -0
  165. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/sync/watcher.py +0 -0
  166. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/sync/watcher_enhanced.py +0 -0
  167. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/telemetry.py +0 -0
  168. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/templates/__init__.py +0 -0
  169. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/templates/bridge_templates.py +0 -0
  170. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/templates/specification_templates.py +0 -0
  171. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/__init__.py +0 -0
  172. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/acceptance_criteria.py +0 -0
  173. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/bundle_loader.py +0 -0
  174. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/code_change_detector.py +0 -0
  175. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/console.py +0 -0
  176. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/content_sanitizer.py +0 -0
  177. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/context_detection.py +0 -0
  178. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/enrichment_context.py +0 -0
  179. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/enrichment_parser.py +0 -0
  180. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/env_manager.py +0 -0
  181. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/feature_keys.py +0 -0
  182. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/git.py +0 -0
  183. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/github_annotations.py +0 -0
  184. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/ide_setup.py +0 -0
  185. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/incremental_check.py +0 -0
  186. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/optional_deps.py +0 -0
  187. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/performance.py +0 -0
  188. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/progress.py +0 -0
  189. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/progressive_disclosure.py +0 -0
  190. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/prompts.py +0 -0
  191. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/sdd_discovery.py +0 -0
  192. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/source_scanner.py +0 -0
  193. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/structure.py +0 -0
  194. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/structured_io.py +0 -0
  195. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/suggestions.py +0 -0
  196. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/terminal.py +0 -0
  197. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/utils/yaml_utils.py +0 -0
  198. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/__init__.py +0 -0
  199. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/agile_validation.py +0 -0
  200. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/cli_first_validator.py +0 -0
  201. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/contract_validator.py +0 -0
  202. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/fsm.py +0 -0
  203. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/repro_checker.py +0 -0
  204. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/schema.py +0 -0
  205. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/sidecar/__init__.py +0 -0
  206. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/sidecar/frameworks/__init__.py +0 -0
  207. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/sidecar/frameworks/base.py +0 -0
  208. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/sidecar/frameworks/django.py +0 -0
  209. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/sidecar/frameworks/drf.py +0 -0
  210. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/sidecar/frameworks/fastapi.py +0 -0
  211. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/sidecar/specmatic_runner.py +0 -0
  212. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/validators/sidecar/unannotated_detector.py +0 -0
  213. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/versioning/__init__.py +0 -0
  214. {specfact_cli-0.24.0 → specfact_cli-0.24.1}/src/specfact_cli/versioning/analyzer.py +0 -0
@@ -123,4 +123,5 @@ _site/
123
123
  findings.json
124
124
  answers.json
125
125
  questions.json
126
- docs/project-plans/
126
+ docs/project-plans/
127
+ harness_contracts.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specfact-cli
3
- Version: 0.24.0
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.0"
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"
@@ -3,4 +3,4 @@ SpecFact CLI - Spec→Contract→Sentinel tool for contract-driven development.
3
3
  """
4
4
 
5
5
  # Define the package version (kept in sync with pyproject.toml and setup.py)
6
- __version__ = "0.24.0"
6
+ __version__ = "0.24.1"
@@ -9,6 +9,6 @@ This package provides command-line tools for:
9
9
  - Validating reproducibility
10
10
  """
11
11
 
12
- __version__ = "0.24.0"
12
+ __version__ = "0.24.1"
13
13
 
14
14
  __all__ = ["__version__"]
@@ -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
- console.print(f" {summary_line}")
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
- console.print(f" Summary file: {results['crosshair_summary_file']}")
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
- return populated_count
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 route_id in schemas:
112
- # Add route to paths if not already present
113
- if route.path not in contract_data["paths"]:
114
- contract_data["paths"][route.path] = {}
115
-
116
- method_lower = route.method.lower()
117
- if method_lower not in contract_data["paths"][route.path]:
118
- operation = {
119
- "operationId": route.operation_id,
120
- "summary": f"{route.method} {route.path}",
121
- "responses": {
122
- "200": {"description": "Success"},
123
- "400": {"description": "Bad request"},
124
- "500": {"description": "Internal server error"},
125
- },
126
- }
127
-
128
- if route.path_params:
129
- operation["parameters"] = route.path_params
130
-
131
- if route.method.upper() in ("POST", "PUT", "PATCH"):
132
- schema = schemas.get(route_id, {})
133
- if schema:
134
- operation["requestBody"] = {
135
- "content": {
136
- "application/json": {
137
- "schema": schema,
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
- contract_data["paths"][route.path][method_lower] = operation
143
- modified = True
148
+ contract_data["paths"][route.path][method_lower] = operation
149
+ modified = True
144
150
 
145
151
  return modified
@@ -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 environment manager detection if repo_path provided
53
- base_cmd = ["crosshair", "check", str(source_path)]
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(["--per-path-timeout", str(per_path_timeout)])
68
+ base_cmd.extend(["--per_path_timeout", str(per_path_timeout)])
58
69
  if per_condition_timeout:
59
- base_cmd.extend(["--per-condition-timeout", str(per_condition_timeout)])
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"