specfact-cli 0.20.5__tar.gz → 0.20.6__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 (180) hide show
  1. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/PKG-INFO +1 -1
  2. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/pyproject.toml +1 -1
  3. specfact_cli-0.20.6/resources/templates/sidecar/populate_contracts.py +543 -0
  4. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/sidecar/run_sidecar.sh +14 -1
  5. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/__init__.py +1 -1
  6. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/__init__.py +1 -1
  7. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/contract_cmd.py +17 -7
  8. specfact_cli-0.20.5/resources/templates/sidecar/populate_contracts.py +0 -278
  9. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/.gitignore +0 -0
  10. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/LICENSE.md +0 -0
  11. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/README.md +0 -0
  12. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/mappings/node-async.yaml +0 -0
  13. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/mappings/python-async.yaml +0 -0
  14. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/mappings/speckit-default.yaml +0 -0
  15. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/prompts/shared/cli-enforcement.md +0 -0
  16. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/prompts/specfact.01-import.md +0 -0
  17. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/prompts/specfact.02-plan.md +0 -0
  18. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/prompts/specfact.03-review.md +0 -0
  19. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/prompts/specfact.04-sdd.md +0 -0
  20. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/prompts/specfact.05-enforce.md +0 -0
  21. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/prompts/specfact.06-sync.md +0 -0
  22. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/prompts/specfact.07-contracts.md +0 -0
  23. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/prompts/specfact.compare.md +0 -0
  24. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/prompts/specfact.validate.md +0 -0
  25. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/schemas/deviation.schema.json +0 -0
  26. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/schemas/plan.schema.json +0 -0
  27. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/schemas/protocol.schema.json +0 -0
  28. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/github-action.yml.j2 +0 -0
  29. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/persona/architect.md.j2 +0 -0
  30. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/persona/developer.md.j2 +0 -0
  31. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/persona/product-owner.md.j2 +0 -0
  32. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/plan.bundle.yaml.j2 +0 -0
  33. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/pr-template.md.j2 +0 -0
  34. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/protocol.yaml.j2 +0 -0
  35. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/sidecar/README.md +0 -0
  36. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/sidecar/__init__.py +0 -0
  37. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/sidecar/adapters.py +0 -0
  38. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/sidecar/bindings.yaml +0 -0
  39. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/sidecar/bindings.yaml.example +0 -0
  40. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/sidecar/crosshair_django_wrapper.py +0 -0
  41. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/sidecar/crosshair_plugin.py +0 -0
  42. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/sidecar/django_form_extractor.py +0 -0
  43. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/sidecar/django_url_extractor.py +0 -0
  44. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/sidecar/generate_harness.py +0 -0
  45. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/sidecar/harness_contracts.py +0 -0
  46. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/sidecar/sidecar-init.sh +0 -0
  47. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/telemetry.yaml.example +0 -0
  48. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/agents/__init__.py +0 -0
  49. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/agents/analyze_agent.py +0 -0
  50. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/agents/base.py +0 -0
  51. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/agents/plan_agent.py +0 -0
  52. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/agents/registry.py +0 -0
  53. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/agents/sync_agent.py +0 -0
  54. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/analyzers/__init__.py +0 -0
  55. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/analyzers/ambiguity_scanner.py +0 -0
  56. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/analyzers/code_analyzer.py +0 -0
  57. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/analyzers/constitution_evidence_extractor.py +0 -0
  58. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/analyzers/contract_extractor.py +0 -0
  59. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/analyzers/control_flow_analyzer.py +0 -0
  60. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/analyzers/graph_analyzer.py +0 -0
  61. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/analyzers/relationship_mapper.py +0 -0
  62. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/analyzers/requirement_extractor.py +0 -0
  63. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/analyzers/test_pattern_extractor.py +0 -0
  64. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/cli.py +0 -0
  65. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/__init__.py +0 -0
  66. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/analyze.py +0 -0
  67. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/bridge.py +0 -0
  68. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/drift.py +0 -0
  69. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/enforce.py +0 -0
  70. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/generate.py +0 -0
  71. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/implement.py +0 -0
  72. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/import_cmd.py +0 -0
  73. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/init.py +0 -0
  74. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/migrate.py +0 -0
  75. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/plan.py +0 -0
  76. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/project_cmd.py +0 -0
  77. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/repro.py +0 -0
  78. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/sdd.py +0 -0
  79. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/spec.py +0 -0
  80. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/sync.py +0 -0
  81. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/common/__init__.py +0 -0
  82. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/common/logger_setup.py +0 -0
  83. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/common/logging_utils.py +0 -0
  84. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/common/text_utils.py +0 -0
  85. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/common/utils.py +0 -0
  86. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/comparators/__init__.py +0 -0
  87. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/comparators/plan_comparator.py +0 -0
  88. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/enrichers/constitution_enricher.py +0 -0
  89. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/enrichers/plan_enricher.py +0 -0
  90. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/generators/__init__.py +0 -0
  91. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/generators/contract_generator.py +0 -0
  92. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/generators/openapi_extractor.py +0 -0
  93. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/generators/persona_exporter.py +0 -0
  94. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/generators/plan_generator.py +0 -0
  95. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/generators/protocol_generator.py +0 -0
  96. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/generators/report_generator.py +0 -0
  97. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/generators/task_generator.py +0 -0
  98. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/generators/test_to_openapi.py +0 -0
  99. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/generators/workflow_generator.py +0 -0
  100. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/importers/__init__.py +0 -0
  101. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/importers/speckit_converter.py +0 -0
  102. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/importers/speckit_scanner.py +0 -0
  103. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/integrations/__init__.py +0 -0
  104. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/integrations/specmatic.py +0 -0
  105. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/merge/__init__.py +0 -0
  106. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/merge/resolver.py +0 -0
  107. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/migrations/__init__.py +0 -0
  108. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/migrations/plan_migrator.py +0 -0
  109. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/models/__init__.py +0 -0
  110. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/models/bridge.py +0 -0
  111. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/models/contract.py +0 -0
  112. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/models/deviation.py +0 -0
  113. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/models/enforcement.py +0 -0
  114. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/models/persona_template.py +0 -0
  115. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/models/plan.py +0 -0
  116. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/models/project.py +0 -0
  117. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/models/protocol.py +0 -0
  118. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/models/quality.py +0 -0
  119. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/models/sdd.py +0 -0
  120. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/models/source_tracking.py +0 -0
  121. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/models/task.py +0 -0
  122. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/modes/__init__.py +0 -0
  123. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/modes/detector.py +0 -0
  124. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/modes/router.py +0 -0
  125. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/parsers/__init__.py +0 -0
  126. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/parsers/persona_importer.py +0 -0
  127. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/resources/semgrep/async.yml +0 -0
  128. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/resources/semgrep/code-quality.yml +0 -0
  129. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/resources/semgrep/feature-detection.yml +0 -0
  130. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/runtime.py +0 -0
  131. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/sync/__init__.py +0 -0
  132. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/sync/bridge_probe.py +0 -0
  133. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/sync/bridge_sync.py +0 -0
  134. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/sync/bridge_watch.py +0 -0
  135. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/sync/change_detector.py +0 -0
  136. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/sync/code_to_spec.py +0 -0
  137. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/sync/drift_detector.py +0 -0
  138. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/sync/repository_sync.py +0 -0
  139. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/sync/spec_to_code.py +0 -0
  140. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/sync/spec_to_tests.py +0 -0
  141. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/sync/speckit_sync.py +0 -0
  142. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/sync/watcher.py +0 -0
  143. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/sync/watcher_enhanced.py +0 -0
  144. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/telemetry.py +0 -0
  145. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/templates/__init__.py +0 -0
  146. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/templates/bridge_templates.py +0 -0
  147. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/templates/specification_templates.py +0 -0
  148. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/__init__.py +0 -0
  149. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/acceptance_criteria.py +0 -0
  150. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/bundle_loader.py +0 -0
  151. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/console.py +0 -0
  152. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/context_detection.py +0 -0
  153. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/enrichment_context.py +0 -0
  154. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/enrichment_parser.py +0 -0
  155. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/env_manager.py +0 -0
  156. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/feature_keys.py +0 -0
  157. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/git.py +0 -0
  158. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/github_annotations.py +0 -0
  159. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/ide_setup.py +0 -0
  160. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/incremental_check.py +0 -0
  161. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/optional_deps.py +0 -0
  162. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/performance.py +0 -0
  163. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/progress.py +0 -0
  164. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/progressive_disclosure.py +0 -0
  165. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/prompts.py +0 -0
  166. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/sdd_discovery.py +0 -0
  167. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/source_scanner.py +0 -0
  168. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/structure.py +0 -0
  169. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/structured_io.py +0 -0
  170. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/suggestions.py +0 -0
  171. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/yaml_utils.py +0 -0
  172. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/validators/__init__.py +0 -0
  173. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/validators/agile_validation.py +0 -0
  174. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/validators/cli_first_validator.py +0 -0
  175. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/validators/contract_validator.py +0 -0
  176. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/validators/fsm.py +0 -0
  177. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/validators/repro_checker.py +0 -0
  178. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/validators/schema.py +0 -0
  179. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/versioning/__init__.py +0 -0
  180. {specfact_cli-0.20.5 → specfact_cli-0.20.6}/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.20.5
3
+ Version: 0.20.6
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.20.5"
7
+ version = "0.20.6"
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"
@@ -0,0 +1,543 @@
1
+ #!/usr/bin/env python3
2
+ # pyright: reportMissingImports=false, reportImplicitRelativeImport=false
3
+ """
4
+ Populate OpenAPI contract stubs with Django URL patterns.
5
+
6
+ Reads Django URL patterns and populates existing OpenAPI contract files.
7
+
8
+ Note: This is a template file that gets copied to the sidecar workspace.
9
+ The imports work at runtime when the file is in the sidecar directory.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import sys
16
+ from pathlib import Path
17
+ from typing import TYPE_CHECKING, cast
18
+
19
+ import yaml
20
+
21
+
22
+ # Type stubs for template file imports
23
+ # These are template files that get copied to sidecar workspace where imports work at runtime
24
+ if TYPE_CHECKING:
25
+
26
+ def extract_django_urls(repo_path: Path, urls_file: Path | None = None) -> list[dict[str, object]]: ...
27
+ def extract_view_form_schema(repo_path: Path, view_module: str, view_function: str) -> dict[str, object] | None: ...
28
+
29
+
30
+ # Import from same directory (sidecar templates)
31
+ # These scripts are run directly, so we need to handle imports differently
32
+ # Add current directory to path for direct import when run as script
33
+ _script_dir = Path(__file__).parent
34
+ if str(_script_dir) not in sys.path:
35
+ sys.path.insert(0, str(_script_dir))
36
+
37
+ # These imports work at runtime when scripts are run directly from sidecar directory
38
+ # Type checker uses TYPE_CHECKING stubs above; runtime uses actual imports below
39
+ # The sidecar directory has __init__.py, making it a package, so relative imports work at runtime
40
+ try:
41
+ # Try explicit relative imports first (preferred for type checking)
42
+ # These work when the sidecar directory is a proper package (has __init__.py)
43
+ from .django_form_extractor import ( # type: ignore[reportMissingImports]
44
+ extract_view_form_schema,
45
+ )
46
+ from .django_url_extractor import extract_django_urls # type: ignore[reportMissingImports]
47
+ except ImportError:
48
+ # Fallback for when run as script (runtime path manipulation case)
49
+ # This happens when the script is executed directly from the sidecar workspace
50
+ # and sys.path manipulation makes absolute imports work
51
+ from django_form_extractor import ( # type: ignore[reportMissingImports]
52
+ extract_view_form_schema,
53
+ )
54
+ from django_url_extractor import (
55
+ extract_django_urls, # type: ignore[reportImplicitRelativeImport, reportMissingImports]
56
+ )
57
+
58
+
59
+ def _match_url_to_feature(url_pattern: dict[str, object], feature_key: str) -> bool:
60
+ """
61
+ Match URL pattern to feature by operation_id or view name.
62
+
63
+ Args:
64
+ url_pattern: URL pattern dictionary from extractor
65
+ feature_key: Feature key (e.g., 'FEATURE-USER-AUTHENTICATION')
66
+
67
+ Returns:
68
+ True if pattern matches feature
69
+ """
70
+ operation_id = str(url_pattern.get("operation_id", "")).lower()
71
+ view = str(url_pattern.get("view", "")).lower()
72
+ feature_lower = feature_key.lower().replace("feature-", "").replace("-", "_")
73
+
74
+ # Check if operation_id or view contains feature keywords
75
+ keywords = feature_lower.split("_")
76
+ return any(keyword and (keyword in operation_id or keyword in view) for keyword in keywords)
77
+
78
+
79
+ def _create_openapi_operation(
80
+ url_pattern: dict[str, object],
81
+ repo_path: Path,
82
+ form_schema: dict[str, object] | None = None,
83
+ ) -> dict[str, object]:
84
+ """
85
+ Create OpenAPI operation from Django URL pattern.
86
+
87
+ Args:
88
+ url_pattern: URL pattern dictionary from extractor
89
+ repo_path: Path to Django repository (for form extraction)
90
+ form_schema: Optional pre-extracted form schema
91
+
92
+ Returns:
93
+ OpenAPI operation dictionary
94
+ """
95
+ method = str(url_pattern["method"]).lower()
96
+ path = str(url_pattern["path"])
97
+ operation_id = str(url_pattern["operation_id"])
98
+ path_params = url_pattern.get("path_params", [])
99
+ if not isinstance(path_params, list):
100
+ path_params = []
101
+ view_ref = url_pattern.get("view")
102
+
103
+ operation: dict[str, object] = {
104
+ "operationId": operation_id,
105
+ "summary": f"{method.upper()} {path}",
106
+ "responses": {
107
+ "200": {"description": "Success"},
108
+ "400": {"description": "Bad request"},
109
+ "500": {"description": "Internal server error"},
110
+ },
111
+ }
112
+
113
+ # Add path parameters
114
+ if path_params:
115
+ operation["parameters"] = path_params
116
+
117
+ # Add request body for POST/PUT/PATCH
118
+ if method in ("post", "put", "patch"):
119
+ # Try to extract form schema from view
120
+ schema: dict[str, object] | None = form_schema
121
+ if schema is None and view_ref:
122
+ # Try to extract from view function
123
+ view_str = str(view_ref)
124
+ if "." in view_str:
125
+ parts = view_str.split(".")
126
+ if len(parts) >= 2:
127
+ view_module = ".".join(parts[:-1])
128
+ view_function = parts[-1]
129
+ schema = extract_view_form_schema(repo_path, view_module, view_function)
130
+
131
+ # Special case: login view doesn't use a form
132
+ if schema is None and "login" in operation_id.lower():
133
+ schema = {
134
+ "type": "object",
135
+ "properties": {
136
+ "username": {"type": "string", "minLength": 1},
137
+ "password": {"type": "string", "minLength": 1},
138
+ },
139
+ "required": ["username", "password"],
140
+ }
141
+
142
+ # Use extracted schema or default empty schema
143
+ if schema is None:
144
+ schema = {"type": "object", "properties": {}, "required": []}
145
+
146
+ operation["requestBody"] = {
147
+ "required": True,
148
+ "content": {
149
+ "application/x-www-form-urlencoded": {
150
+ "schema": schema,
151
+ }
152
+ },
153
+ }
154
+
155
+ return operation # type: ignore[return-value]
156
+
157
+
158
+ def _get_common_schemas() -> dict[str, dict[str, object]]:
159
+ """
160
+ Get common schema definitions for OpenAPI contracts.
161
+
162
+ Returns:
163
+ Dictionary of schema name to schema definition
164
+ """
165
+ return {
166
+ "Path": {
167
+ "type": "string",
168
+ "description": "File system path",
169
+ "example": "/path/to/file.py",
170
+ },
171
+ "PlanBundle": {
172
+ "type": "object",
173
+ "description": "Plan bundle containing features, stories, and product definition",
174
+ "properties": {
175
+ "version": {"type": "string", "example": "1.0"},
176
+ "idea": {
177
+ "type": "object",
178
+ "properties": {
179
+ "title": {"type": "string"},
180
+ "narrative": {"type": "string"},
181
+ },
182
+ },
183
+ "product": {
184
+ "type": "object",
185
+ "properties": {
186
+ "themes": {"type": "array", "items": {"type": "string"}},
187
+ },
188
+ },
189
+ "features": {
190
+ "type": "array",
191
+ "items": {
192
+ "type": "object",
193
+ "properties": {
194
+ "key": {"type": "string"},
195
+ "title": {"type": "string"},
196
+ "stories": {"type": "array", "items": {"type": "object"}},
197
+ },
198
+ },
199
+ },
200
+ },
201
+ },
202
+ "FileSystemEvent": {
203
+ "type": "object",
204
+ "description": "File system event (created, modified, deleted)",
205
+ "properties": {
206
+ "path": {"type": "string"},
207
+ "event_type": {"type": "string", "enum": ["created", "modified", "deleted"]},
208
+ "timestamp": {"type": "string", "format": "date-time"},
209
+ },
210
+ },
211
+ "SyncResult": {
212
+ "type": "object",
213
+ "description": "Synchronization result",
214
+ "properties": {
215
+ "success": {"type": "boolean"},
216
+ "message": {"type": "string"},
217
+ "changes": {"type": "array", "items": {"type": "object"}},
218
+ },
219
+ },
220
+ "RepositorySyncResult": {
221
+ "type": "object",
222
+ "description": "Repository synchronization result",
223
+ "properties": {
224
+ "success": {"type": "boolean"},
225
+ "synced_files": {"type": "array", "items": {"type": "string"}},
226
+ "conflicts": {"type": "array", "items": {"type": "object"}},
227
+ },
228
+ },
229
+ }
230
+
231
+
232
+ def _resolve_schema_refs(contract: dict[str, object]) -> dict[str, object]:
233
+ """
234
+ Resolve schema references and add missing schema definitions.
235
+
236
+ Args:
237
+ contract: OpenAPI contract dictionary
238
+
239
+ Returns:
240
+ Updated contract with resolved schemas
241
+ """
242
+ # Get common schemas
243
+ common_schemas = _get_common_schemas()
244
+
245
+ # Ensure components.schemas exists
246
+ components = contract.get("components", {})
247
+ if not isinstance(components, dict):
248
+ components = {}
249
+ contract["components"] = components
250
+
251
+ schemas = components.get("schemas", {})
252
+ if not isinstance(schemas, dict):
253
+ schemas = {}
254
+ components["schemas"] = schemas
255
+
256
+ # Find all $ref references in the contract
257
+ def find_refs(obj: object, refs: set[str]) -> None:
258
+ """Recursively find all $ref references."""
259
+ if isinstance(obj, dict):
260
+ if "$ref" in obj:
261
+ ref = str(obj["$ref"])
262
+ if ref.startswith("#/components/schemas/"):
263
+ schema_name = ref.split("/")[-1]
264
+ refs.add(schema_name)
265
+ for value in obj.values():
266
+ find_refs(value, refs)
267
+ elif isinstance(obj, list):
268
+ for item in obj:
269
+ find_refs(item, refs)
270
+
271
+ refs: set[str] = set()
272
+ find_refs(contract, refs)
273
+
274
+ # Add missing schema definitions
275
+ for ref in refs:
276
+ if ref not in schemas and ref in common_schemas:
277
+ schemas[ref] = common_schemas[ref]
278
+ elif ref in schemas and ref in common_schemas:
279
+ # Fix incorrect schema definitions (hotpatch for PlanBundle schema bug)
280
+ # If schema exists but has incorrect structure, replace with correct one
281
+ existing_schema = schemas[ref]
282
+ correct_schema = common_schemas[ref]
283
+
284
+ # Special case: Fix PlanBundle.themes schema bug (array of objects -> array of strings)
285
+ if ref == "PlanBundle" and isinstance(existing_schema, dict) and isinstance(correct_schema, dict):
286
+ existing_props = existing_schema.get("properties", {})
287
+ if not isinstance(existing_props, dict):
288
+ existing_props = {}
289
+ correct_props = correct_schema.get("properties", {})
290
+ if not isinstance(correct_props, dict):
291
+ correct_props = {}
292
+
293
+ # Check if themes schema is incorrect
294
+ existing_product = existing_props.get("product", {})
295
+ if not isinstance(existing_product, dict):
296
+ existing_product = {}
297
+ existing_product_props = existing_product.get("properties", {})
298
+ if not isinstance(existing_product_props, dict):
299
+ existing_product_props = {}
300
+ existing_themes = existing_product_props.get("themes", {})
301
+
302
+ correct_product = correct_props.get("product", {})
303
+ if not isinstance(correct_product, dict):
304
+ correct_product = {}
305
+ correct_product_props = correct_product.get("properties", {})
306
+ if not isinstance(correct_product_props, dict):
307
+ correct_product_props = {}
308
+ correct_themes = correct_product_props.get("themes", {})
309
+
310
+ if (
311
+ isinstance(existing_themes, dict)
312
+ and isinstance(correct_themes, dict)
313
+ and existing_themes.get("items", {}).get("type") == "object"
314
+ and correct_themes.get("items", {}).get("type") == "string"
315
+ ):
316
+ # Fix the themes schema
317
+ if "product" not in existing_props:
318
+ existing_props["product"] = {}
319
+ if "properties" not in existing_props["product"]:
320
+ existing_props["product"]["properties"] = {}
321
+ existing_props["product"]["properties"]["themes"] = correct_themes
322
+
323
+ return contract
324
+
325
+
326
+ def populate_contracts(
327
+ contracts_dir: Path, repo_path: Path, urls_file: Path | None = None, extract_forms: bool = True
328
+ ) -> dict[str, int]:
329
+ """
330
+ Populate OpenAPI contract stubs with Django URL patterns.
331
+
332
+ Args:
333
+ contracts_dir: Directory containing *.openapi.yaml files
334
+ repo_path: Path to Django repository
335
+ urls_file: Path to urls.py file (auto-detected if not provided)
336
+
337
+ Returns:
338
+ Dictionary with statistics (populated, skipped, errors)
339
+ """
340
+ # Extract Django URL patterns
341
+ url_patterns = extract_django_urls(repo_path, urls_file)
342
+
343
+ if not url_patterns:
344
+ return {"populated": 0, "skipped": 0, "errors": 0}
345
+
346
+ # Find all contract files
347
+ contract_files = list(contracts_dir.glob("*.openapi.yaml"))
348
+
349
+ stats = {"populated": 0, "skipped": 0, "errors": 0}
350
+
351
+ for contract_file in contract_files:
352
+ try:
353
+ # Load contract
354
+ with contract_file.open("r", encoding="utf-8") as f:
355
+ contract_data = yaml.safe_load(f) # type: ignore[assignment]
356
+ if not isinstance(contract_data, dict):
357
+ contract_data = {}
358
+ contract = cast(dict[str, object], contract_data)
359
+
360
+ if "paths" not in contract:
361
+ contract["paths"] = {}
362
+
363
+ # Extract feature key from filename
364
+ feature_key = contract_file.stem.replace(".openapi", "").upper()
365
+
366
+ # Find matching URL patterns
367
+ matching_patterns = [p for p in url_patterns if _match_url_to_feature(p, feature_key)]
368
+
369
+ if not matching_patterns:
370
+ stats["skipped"] += 1
371
+ continue
372
+
373
+ # Populate paths
374
+ for pattern in matching_patterns:
375
+ path = str(pattern["path"])
376
+ method = str(pattern["method"]).lower()
377
+
378
+ paths_dict = contract.get("paths", {})
379
+ if not isinstance(paths_dict, dict):
380
+ paths_dict = {}
381
+ contract["paths"] = paths_dict
382
+ if path not in paths_dict:
383
+ paths_dict[path] = {} # type: ignore[assignment]
384
+
385
+ # Extract form schema if enabled
386
+ form_schema: dict[str, object] | None = None
387
+ if extract_forms:
388
+ view_ref = pattern.get("view")
389
+ if view_ref:
390
+ view_str = str(view_ref)
391
+ if "." in view_str:
392
+ parts = view_str.split(".")
393
+ if len(parts) >= 2:
394
+ view_module = ".".join(parts[:-1])
395
+ view_function = parts[-1]
396
+ form_schema = extract_view_form_schema(repo_path, view_module, view_function)
397
+
398
+ operation = _create_openapi_operation(pattern, repo_path, form_schema) # type: ignore[arg-type]
399
+ if isinstance(paths_dict, dict) and isinstance(paths_dict.get(path), dict):
400
+ paths_dict[path][method] = operation # type: ignore[assignment, index]
401
+
402
+ # Resolve schema references and add missing schemas
403
+ contract = _resolve_schema_refs(contract)
404
+
405
+ # Save updated contract
406
+ with contract_file.open("w", encoding="utf-8") as f:
407
+ yaml.dump(contract, f, default_flow_style=False, sort_keys=False, allow_unicode=True)
408
+
409
+ stats["populated"] += 1
410
+
411
+ except Exception as e:
412
+ print(f"Error processing {contract_file}: {e}")
413
+ stats["errors"] += 1
414
+
415
+ return stats
416
+
417
+
418
+ def resolve_schema_refs_in_contracts(contracts_dir: Path) -> dict[str, int]:
419
+ """
420
+ Resolve schema references in all OpenAPI contracts.
421
+
422
+ This function adds missing schema definitions for common types like Path, PlanBundle, etc.
423
+ It can be used for any project type (not just Django).
424
+
425
+ Args:
426
+ contracts_dir: Directory containing *.openapi.yaml files
427
+
428
+ Returns:
429
+ Dictionary with statistics (resolved, skipped, errors)
430
+ """
431
+ contract_files = list(contracts_dir.glob("*.openapi.yaml"))
432
+ stats = {"resolved": 0, "skipped": 0, "errors": 0}
433
+
434
+ for contract_file in contract_files:
435
+ try:
436
+ # Load contract
437
+ with contract_file.open("r", encoding="utf-8") as f:
438
+ contract_data = yaml.safe_load(f) # type: ignore[assignment]
439
+ if not isinstance(contract_data, dict):
440
+ contract_data = {}
441
+ contract = cast(dict[str, object], contract_data)
442
+
443
+ # Resolve schema references
444
+ # Get original schemas BEFORE resolving (make a copy since _resolve_schema_refs modifies in place)
445
+ import json
446
+
447
+ components = contract.get("components")
448
+ original_schemas: dict[str, object] = {}
449
+ original_schemas_str = ""
450
+ if isinstance(components, dict):
451
+ schemas = components.get("schemas")
452
+ if isinstance(schemas, dict):
453
+ original_schemas = schemas.copy() # Make a copy to avoid reference issues
454
+ # Also serialize to string for comparison (to detect schema fixes, not just additions)
455
+ original_schemas_str = json.dumps(original_schemas, sort_keys=True)
456
+
457
+ contract = _resolve_schema_refs(contract)
458
+
459
+ new_schemas: dict[str, object] = {}
460
+ components_after = contract.get("components")
461
+ if isinstance(components_after, dict):
462
+ schemas_after = components_after.get("schemas")
463
+ if isinstance(schemas_after, dict):
464
+ new_schemas = schemas_after
465
+
466
+ # Check if schemas were added OR fixed (hotpatch for PlanBundle schema bug)
467
+ schemas_changed = False
468
+ if len(new_schemas) > len(original_schemas):
469
+ schemas_changed = True
470
+ elif len(new_schemas) == len(original_schemas) and len(original_schemas) > 0 and original_schemas_str:
471
+ # Check if any schemas were modified (e.g., PlanBundle.themes fix)
472
+ new_schemas_str = json.dumps(new_schemas, sort_keys=True)
473
+ if new_schemas_str != original_schemas_str:
474
+ schemas_changed = True
475
+
476
+ if schemas_changed:
477
+ # Save updated contract
478
+ with contract_file.open("w", encoding="utf-8") as f:
479
+ yaml.dump(contract, f, default_flow_style=False, sort_keys=False, allow_unicode=True)
480
+ stats["resolved"] += 1
481
+ else:
482
+ stats["skipped"] += 1
483
+
484
+ except Exception as e:
485
+ print(f"Error processing {contract_file}: {e}")
486
+ stats["errors"] += 1
487
+
488
+ return stats
489
+
490
+
491
+ def main() -> int:
492
+ """Main entry point for contract population."""
493
+ parser = argparse.ArgumentParser(
494
+ description="Populate OpenAPI contracts with Django URL patterns or resolve schema references."
495
+ )
496
+ parser.add_argument("--contracts", required=True, help="Contracts directory containing *.openapi.yaml files")
497
+ parser.add_argument("--repo", help="Path to Django repository (required for URL population)")
498
+ parser.add_argument("--urls", help="Path to urls.py file (auto-detected if not provided)")
499
+ parser.add_argument(
500
+ "--resolve-schemas-only", action="store_true", help="Only resolve schema references, don't populate URLs"
501
+ )
502
+ args = parser.parse_args()
503
+
504
+ contracts_dir = Path(str(args.contracts)).resolve() # type: ignore[arg-type]
505
+
506
+ if not contracts_dir.exists():
507
+ print(f"Error: Contracts directory not found: {contracts_dir}")
508
+ return 1
509
+
510
+ # If --resolve-schemas-only, just resolve schema references
511
+ if args.resolve_schemas_only:
512
+ stats = resolve_schema_refs_in_contracts(contracts_dir)
513
+ print(f"Resolved: {stats['resolved']}, Skipped: {stats['skipped']}, Errors: {stats['errors']}")
514
+ return 0 if stats["errors"] == 0 else 1
515
+
516
+ # Otherwise, do Django URL population (requires --repo)
517
+ if not args.repo:
518
+ print("Error: --repo is required for URL population (or use --resolve-schemas-only)")
519
+ return 1
520
+
521
+ repo_path = Path(str(args.repo)).resolve() # type: ignore[arg-type]
522
+ urls_file = Path(str(args.urls)).resolve() if args.urls else None # type: ignore[arg-type]
523
+
524
+ if not repo_path.exists():
525
+ print(f"Error: Repository path not found: {repo_path}")
526
+ return 1
527
+
528
+ # Populate URLs and resolve schemas
529
+ stats = populate_contracts(contracts_dir, repo_path, urls_file)
530
+
531
+ # Also resolve schema references after population
532
+ schema_stats = resolve_schema_refs_in_contracts(contracts_dir)
533
+ stats["schema_resolved"] = schema_stats["resolved"]
534
+
535
+ print(
536
+ f"Populated: {stats['populated']}, Skipped: {stats['skipped']}, Errors: {stats['errors']}, Schemas resolved: {stats.get('schema_resolved', 0)}"
537
+ )
538
+
539
+ return 0 if stats["errors"] == 0 else 1
540
+
541
+
542
+ if __name__ == "__main__":
543
+ raise SystemExit(main())
@@ -209,6 +209,15 @@ if [[ "${POPULATE_CONTRACTS}" == "1" ]] && [[ -d "${CONTRACTS_DIR}" ]]; then
209
209
  --contracts "${CONTRACTS_DIR}" \
210
210
  --repo "${REPO_PATH}" \
211
211
  || echo "[sidecar] warning: contract population failed (continuing anyway)"
212
+ else
213
+ # For non-Django projects, just resolve schema references
214
+ echo "[sidecar] resolve contract schema references..."
215
+ run_and_log "${TIMEOUT_CROSSHAIR}" \
216
+ "${SIDECAR_REPORTS_DIR}/${TIMESTAMP}-resolve-schemas.log" \
217
+ "${PYTHON_CMD}" populate_contracts.py \
218
+ --contracts "${CONTRACTS_DIR}" \
219
+ --resolve-schemas-only \
220
+ || echo "[sidecar] warning: schema resolution failed (continuing anyway)"
212
221
  fi
213
222
  fi
214
223
 
@@ -401,9 +410,13 @@ if [[ "${RUN_CROSSHAIR}" == "1" ]] && command -v crosshair >/dev/null 2>&1; then
401
410
  if [[ -n "${PYTHONPATH:-}" ]]; then
402
411
  CROSSHAIR_ENV="${CROSSHAIR_ENV}PYTHONPATH=${PYTHONPATH} "
403
412
  fi
413
+ # Change to harness directory to ensure valid module name (avoids hyphenated directory names in module path)
414
+ HARNESS_DIR="$(dirname "${HARNESS_PATH}")"
415
+ HARNESS_FILE="$(basename "${HARNESS_PATH}")"
416
+ HARNESS_MODULE="${HARNESS_FILE%.py}" # Remove .py extension
404
417
  run_and_log "${TIMEOUT_CROSSHAIR}" \
405
418
  "${SIDECAR_REPORTS_DIR}/${TIMESTAMP}-crosshair-harness.log" \
406
- env ${CROSSHAIR_ENV}"${PYTHON_CMD}" -m crosshair check "${CROSSHAIR_ARGS[@]}" "${HARNESS_PATH}"
419
+ bash -c "cd '${HARNESS_DIR}' && env ${CROSSHAIR_ENV}${PYTHON_CMD} -m crosshair check ${CROSSHAIR_ARGS[*]} ${HARNESS_MODULE}"
407
420
  else
408
421
  echo "[sidecar] crosshair harness skipped (${HARNESS_PATH} not found)"
409
422
  fi
@@ -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.20.5"
6
+ __version__ = "0.20.6"
@@ -9,6 +9,6 @@ This package provides command-line tools for:
9
9
  - Validating reproducibility
10
10
  """
11
11
 
12
- __version__ = "0.20.5"
12
+ __version__ = "0.20.6"
13
13
 
14
14
  __all__ = ["__version__"]
@@ -63,6 +63,11 @@ def init_contract(
63
63
  "--no-interactive",
64
64
  help="Non-interactive mode (for CI/CD automation). Default: False (interactive mode)",
65
65
  ),
66
+ force: bool = typer.Option(
67
+ False,
68
+ "--force",
69
+ help="Overwrite existing contract file without prompting (useful for updating contracts)",
70
+ ),
66
71
  ) -> None:
67
72
  """
68
73
  Initialize OpenAPI contract for a feature.
@@ -76,11 +81,12 @@ def init_contract(
76
81
  **Parameter Groups:**
77
82
  - **Target/Input**: --repo, --bundle, --feature
78
83
  - **Output/Results**: --title, --version
79
- - **Behavior/Options**: --no-interactive
84
+ - **Behavior/Options**: --no-interactive, --force
80
85
 
81
86
  **Examples:**
82
87
  specfact contract init --bundle legacy-api --feature FEATURE-001
83
88
  specfact contract init --bundle legacy-api --feature FEATURE-001 --title "Authentication API" --version 1.0.0
89
+ specfact contract init --bundle legacy-api --feature FEATURE-001 --force --no-interactive
84
90
  """
85
91
  telemetry_metadata = {
86
92
  "bundle": bundle,
@@ -138,13 +144,17 @@ def init_contract(
138
144
  contract_file = contracts_dir / f"{feature}.openapi.yaml"
139
145
 
140
146
  if contract_file.exists():
141
- print_warning(f"Contract file already exists: {contract_file}")
142
- if not no_interactive:
143
- overwrite = typer.confirm("Overwrite existing contract?")
144
- if not overwrite:
145
- raise typer.Exit(0)
147
+ if force:
148
+ print_warning(f"Overwriting existing contract file: {contract_file}")
146
149
  else:
147
- raise typer.Exit(1)
150
+ print_warning(f"Contract file already exists: {contract_file}")
151
+ if not no_interactive:
152
+ overwrite = typer.confirm("Overwrite existing contract?")
153
+ if not overwrite:
154
+ raise typer.Exit(0)
155
+ else:
156
+ print_error("Use --force to overwrite existing contract in non-interactive mode")
157
+ raise typer.Exit(1)
148
158
 
149
159
  # Generate OpenAPI stub
150
160
  api_title = title or feature_obj.title