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.
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/PKG-INFO +1 -1
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/pyproject.toml +1 -1
- specfact_cli-0.20.6/resources/templates/sidecar/populate_contracts.py +543 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/sidecar/run_sidecar.sh +14 -1
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/__init__.py +1 -1
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/__init__.py +1 -1
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/contract_cmd.py +17 -7
- specfact_cli-0.20.5/resources/templates/sidecar/populate_contracts.py +0 -278
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/.gitignore +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/LICENSE.md +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/README.md +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/mappings/node-async.yaml +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/mappings/python-async.yaml +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/mappings/speckit-default.yaml +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/prompts/shared/cli-enforcement.md +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/prompts/specfact.01-import.md +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/prompts/specfact.02-plan.md +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/prompts/specfact.03-review.md +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/prompts/specfact.04-sdd.md +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/prompts/specfact.05-enforce.md +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/prompts/specfact.06-sync.md +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/prompts/specfact.07-contracts.md +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/prompts/specfact.compare.md +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/prompts/specfact.validate.md +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/schemas/deviation.schema.json +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/schemas/plan.schema.json +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/schemas/protocol.schema.json +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/github-action.yml.j2 +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/persona/architect.md.j2 +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/persona/developer.md.j2 +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/persona/product-owner.md.j2 +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/plan.bundle.yaml.j2 +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/pr-template.md.j2 +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/protocol.yaml.j2 +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/sidecar/README.md +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/sidecar/__init__.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/sidecar/adapters.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/sidecar/bindings.yaml +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/sidecar/bindings.yaml.example +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/sidecar/crosshair_django_wrapper.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/sidecar/crosshair_plugin.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/sidecar/django_form_extractor.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/sidecar/django_url_extractor.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/sidecar/generate_harness.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/sidecar/harness_contracts.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/sidecar/sidecar-init.sh +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/resources/templates/telemetry.yaml.example +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/agents/__init__.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/agents/analyze_agent.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/agents/base.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/agents/plan_agent.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/agents/registry.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/agents/sync_agent.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/analyzers/__init__.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/analyzers/ambiguity_scanner.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/analyzers/code_analyzer.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/analyzers/constitution_evidence_extractor.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/analyzers/contract_extractor.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/analyzers/control_flow_analyzer.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/analyzers/graph_analyzer.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/analyzers/relationship_mapper.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/analyzers/requirement_extractor.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/analyzers/test_pattern_extractor.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/cli.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/__init__.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/analyze.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/bridge.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/drift.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/enforce.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/generate.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/implement.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/import_cmd.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/init.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/migrate.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/plan.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/project_cmd.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/repro.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/sdd.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/spec.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/commands/sync.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/common/__init__.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/common/logger_setup.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/common/logging_utils.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/common/text_utils.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/common/utils.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/comparators/__init__.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/comparators/plan_comparator.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/enrichers/constitution_enricher.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/enrichers/plan_enricher.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/generators/__init__.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/generators/contract_generator.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/generators/openapi_extractor.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/generators/persona_exporter.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/generators/plan_generator.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/generators/protocol_generator.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/generators/report_generator.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/generators/task_generator.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/generators/test_to_openapi.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/generators/workflow_generator.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/importers/__init__.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/importers/speckit_converter.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/importers/speckit_scanner.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/integrations/__init__.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/integrations/specmatic.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/merge/__init__.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/merge/resolver.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/migrations/__init__.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/migrations/plan_migrator.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/models/__init__.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/models/bridge.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/models/contract.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/models/deviation.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/models/enforcement.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/models/persona_template.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/models/plan.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/models/project.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/models/protocol.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/models/quality.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/models/sdd.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/models/source_tracking.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/models/task.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/modes/__init__.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/modes/detector.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/modes/router.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/parsers/__init__.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/parsers/persona_importer.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/resources/semgrep/async.yml +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/resources/semgrep/code-quality.yml +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/resources/semgrep/feature-detection.yml +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/runtime.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/sync/__init__.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/sync/bridge_probe.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/sync/bridge_sync.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/sync/bridge_watch.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/sync/change_detector.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/sync/code_to_spec.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/sync/drift_detector.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/sync/repository_sync.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/sync/spec_to_code.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/sync/spec_to_tests.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/sync/speckit_sync.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/sync/watcher.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/sync/watcher_enhanced.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/telemetry.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/templates/__init__.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/templates/bridge_templates.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/templates/specification_templates.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/__init__.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/acceptance_criteria.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/bundle_loader.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/console.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/context_detection.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/enrichment_context.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/enrichment_parser.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/env_manager.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/feature_keys.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/git.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/github_annotations.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/ide_setup.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/incremental_check.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/optional_deps.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/performance.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/progress.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/progressive_disclosure.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/prompts.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/sdd_discovery.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/source_scanner.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/structure.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/structured_io.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/suggestions.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/utils/yaml_utils.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/validators/__init__.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/validators/agile_validation.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/validators/cli_first_validator.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/validators/contract_validator.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/validators/fsm.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/validators/repro_checker.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/validators/schema.py +0 -0
- {specfact_cli-0.20.5 → specfact_cli-0.20.6}/src/specfact_cli/versioning/__init__.py +0 -0
- {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.
|
|
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.
|
|
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}
|
|
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
|
|
@@ -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
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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
|