specfact-cli 0.26.0__tar.gz → 0.26.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/PKG-INFO +1 -1
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/pyproject.toml +1 -1
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/prompts/specfact.backlog-refine.md +30 -1
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/__init__.py +1 -1
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/__init__.py +1 -1
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/adapters/ado.py +91 -5
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/adapters/backlog_base.py +66 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/adapters/github.py +143 -8
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/backlog_commands.py +39 -2
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/sync/bridge_sync.py +20 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/.gitignore +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/LICENSE.md +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/README.md +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/mappings/node-async.yaml +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/mappings/python-async.yaml +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/mappings/speckit-default.yaml +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/prompts/shared/cli-enforcement.md +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/prompts/specfact.01-import.md +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/prompts/specfact.02-plan.md +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/prompts/specfact.03-review.md +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/prompts/specfact.04-sdd.md +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/prompts/specfact.05-enforce.md +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/prompts/specfact.06-sync.md +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/prompts/specfact.07-contracts.md +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/prompts/specfact.compare.md +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/prompts/specfact.sync-backlog.md +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/prompts/specfact.validate.md +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/schemas/deviation.schema.json +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/schemas/plan.schema.json +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/schemas/protocol.schema.json +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/backlog/defaults/defect_v1.yaml +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/backlog/defaults/enabler_v1.yaml +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/backlog/defaults/spike_v1.yaml +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/backlog/defaults/user_story_v1.yaml +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/backlog/frameworks/safe/safe_feature_v1.yaml +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/backlog/frameworks/scrum/user_story_v1.yaml +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/backlog/personas/developer/developer_task_v1.yaml +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/backlog/personas/product-owner/user_story_v1.yaml +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/backlog/providers/ado/work_item_v1.yaml +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/github-action.yml.j2 +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/persona/architect.md.j2 +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/persona/developer.md.j2 +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/persona/product-owner.md.j2 +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/plan.bundle.yaml.j2 +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/pr-template.md.j2 +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/protocol.yaml.j2 +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/telemetry.yaml.example +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/adapters/__init__.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/adapters/base.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/adapters/openspec.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/adapters/openspec_parser.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/adapters/registry.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/adapters/speckit.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/agents/__init__.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/agents/analyze_agent.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/agents/base.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/agents/plan_agent.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/agents/registry.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/agents/sync_agent.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/analyzers/__init__.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/analyzers/ambiguity_scanner.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/analyzers/code_analyzer.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/analyzers/constitution_evidence_extractor.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/analyzers/contract_extractor.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/analyzers/control_flow_analyzer.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/analyzers/graph_analyzer.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/analyzers/relationship_mapper.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/analyzers/requirement_extractor.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/analyzers/test_pattern_extractor.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/backlog/__init__.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/backlog/adapters/__init__.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/backlog/adapters/base.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/backlog/adapters/local_yaml_adapter.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/backlog/ai_refiner.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/backlog/converter.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/backlog/filters.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/backlog/format_detector.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/backlog/formats/__init__.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/backlog/formats/base.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/backlog/formats/markdown_format.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/backlog/formats/structured_format.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/backlog/template_detector.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/cli.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/__init__.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/analyze.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/auth.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/contract_cmd.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/drift.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/enforce.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/generate.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/import_cmd.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/init.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/migrate.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/plan.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/project_cmd.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/repro.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/sdd.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/spec.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/sync.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/validate.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/common/__init__.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/common/logger_setup.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/common/logging_utils.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/common/text_utils.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/common/utils.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/comparators/__init__.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/comparators/plan_comparator.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/contracts/__init__.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/contracts/crosshair_props.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/enrichers/constitution_enricher.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/enrichers/plan_enricher.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/generators/__init__.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/generators/contract_generator.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/generators/openapi_extractor.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/generators/persona_exporter.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/generators/plan_generator.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/generators/protocol_generator.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/generators/report_generator.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/generators/task_generator.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/generators/test_to_openapi.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/generators/workflow_generator.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/importers/__init__.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/importers/speckit_converter.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/importers/speckit_scanner.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/integrations/__init__.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/integrations/specmatic.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/merge/__init__.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/merge/resolver.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/migrations/__init__.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/migrations/plan_migrator.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/__init__.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/backlog_item.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/bridge.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/capabilities.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/change.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/contract.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/deviation.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/dor_config.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/enforcement.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/persona_template.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/plan.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/project.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/protocol.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/quality.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/sdd.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/source_tracking.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/task.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/modes/__init__.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/modes/detector.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/modes/router.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/parsers/__init__.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/parsers/persona_importer.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/resources/semgrep/async.yml +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/resources/semgrep/code-quality.yml +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/resources/semgrep/feature-detection.yml +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/runtime.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/sync/__init__.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/sync/bridge_probe.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/sync/bridge_watch.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/sync/change_detector.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/sync/code_to_spec.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/sync/drift_detector.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/sync/repository_sync.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/sync/spec_to_code.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/sync/spec_to_tests.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/sync/watcher.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/sync/watcher_enhanced.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/telemetry.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/templates/__init__.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/templates/bridge_templates.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/templates/defaults/defect_v1.yaml +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/templates/defaults/enabler_v1.yaml +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/templates/defaults/spike_v1.yaml +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/templates/defaults/user_story_v1.yaml +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/templates/frameworks/scrum/user_story_v1.yaml +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/templates/personas/product-owner/user_story_v1.yaml +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/templates/providers/ado/work_item_v1.yaml +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/templates/registry.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/templates/specification_templates.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/__init__.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/acceptance_criteria.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/auth_tokens.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/bundle_loader.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/code_change_detector.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/console.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/content_sanitizer.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/context_detection.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/enrichment_context.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/enrichment_parser.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/env_manager.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/feature_keys.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/git.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/github_annotations.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/ide_setup.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/incremental_check.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/optional_deps.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/performance.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/progress.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/progressive_disclosure.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/prompts.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/sdd_discovery.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/source_scanner.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/structure.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/structured_io.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/suggestions.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/terminal.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/yaml_utils.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/__init__.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/agile_validation.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/change_proposal_integration.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/cli_first_validator.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/contract_validator.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/fsm.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/repro_checker.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/schema.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/__init__.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/contract_populator.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/crosshair_runner.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/crosshair_summary.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/dependency_installer.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/framework_detector.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/frameworks/__init__.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/frameworks/base.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/frameworks/django.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/frameworks/drf.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/frameworks/fastapi.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/frameworks/flask.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/harness_generator.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/models.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/orchestrator.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/specmatic_runner.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/unannotated_detector.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/versioning/__init__.py +0 -0
- {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/versioning/analyzer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: specfact-cli
|
|
3
|
-
Version: 0.26.
|
|
3
|
+
Version: 0.26.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.26.
|
|
7
|
+
version = "0.26.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"
|
|
@@ -155,20 +155,39 @@ Display refinement results:
|
|
|
155
155
|
|
|
156
156
|
- `assignees`: Preserved
|
|
157
157
|
- `tags`: Preserved
|
|
158
|
-
- `state`: Preserved
|
|
158
|
+
- `state`: Preserved (original state maintained)
|
|
159
159
|
- `priority`: Preserved (if present in provider_fields)
|
|
160
160
|
- `due_date`: Preserved (if present in provider_fields)
|
|
161
161
|
- `story_points`: Preserved (if present in provider_fields)
|
|
162
162
|
- `sprint`: Preserved (if present)
|
|
163
163
|
- `release`: Preserved (if present)
|
|
164
|
+
- `source_state`: Preserved for cross-adapter state mapping (stored in bundle entries)
|
|
164
165
|
- All other metadata: Preserved in provider_fields
|
|
165
166
|
|
|
167
|
+
**Cross-Adapter State Preservation**:
|
|
168
|
+
|
|
169
|
+
- When items are imported into bundles, the original `source_state` (e.g., "open", "closed", "New", "Active") is stored in `source_metadata["source_state"]`
|
|
170
|
+
- During cross-adapter export (e.g., GitHub → ADO), the `source_state` is used to determine the correct target state
|
|
171
|
+
- Generic state mapping ensures state is correctly translated between any adapter pair using OpenSpec as intermediate format
|
|
172
|
+
- This ensures closed GitHub issues sync to ADO as "Closed", and open GitHub issues sync to ADO as "New"
|
|
173
|
+
|
|
166
174
|
**OpenSpec Comment Integration**:
|
|
167
175
|
|
|
168
176
|
- When `--openspec-comment` is used, a structured comment is added to the backlog item
|
|
169
177
|
- The comment includes: Change ID, template used, confidence score, refinement timestamp
|
|
170
178
|
- Original body is preserved; comment provides OpenSpec reference for cross-sync
|
|
171
179
|
|
|
180
|
+
**Cross-Adapter State Mapping**:
|
|
181
|
+
|
|
182
|
+
- When refining items that will be synced across adapters (e.g., GitHub ↔ ADO), state is preserved using generic mapping
|
|
183
|
+
- Generic state mapping uses OpenSpec as intermediate format:
|
|
184
|
+
- Source adapter state → OpenSpec status → Target adapter state
|
|
185
|
+
- Example: GitHub "open" → OpenSpec "proposed" → ADO "New"
|
|
186
|
+
- Example: GitHub "closed" → OpenSpec "applied" → ADO "Closed"
|
|
187
|
+
- State preservation: Original `source_state` is stored in bundle entries and used during cross-adapter export
|
|
188
|
+
- Bidirectional mapping: Works in both directions (GitHub → ADO and ADO → GitHub)
|
|
189
|
+
- State mapping is automatic during `sync bridge` operations when `source_state` and `source_type` are present
|
|
190
|
+
|
|
172
191
|
## Architecture Note
|
|
173
192
|
|
|
174
193
|
SpecFact CLI follows a CLI-first architecture:
|
|
@@ -232,6 +251,16 @@ Items updated in remote backlog:
|
|
|
232
251
|
|
|
233
252
|
# Refine and import to OpenSpec bundle
|
|
234
253
|
/specfact.backlog-refine --adapter github --repo-owner nold-ai --repo-name specfact-cli --bundle my-project --auto-bundle --state open
|
|
254
|
+
|
|
255
|
+
# Cross-adapter sync workflow: Refine GitHub → Sync to ADO (with state preservation)
|
|
256
|
+
/specfact.backlog-refine --adapter github --repo-owner nold-ai --repo-name specfact-cli --write --labels feature
|
|
257
|
+
# Then sync to ADO (state will be automatically mapped: open → New, closed → Closed)
|
|
258
|
+
# specfact sync bridge --adapter ado --ado-org my-org --ado-project my-project --mode bidirectional
|
|
259
|
+
|
|
260
|
+
# Cross-adapter sync workflow: Refine ADO → Sync to GitHub (with state preservation)
|
|
261
|
+
/specfact.backlog-refine --adapter ado --ado-org my-org --ado-project my-project --write --state Active
|
|
262
|
+
# Then sync to GitHub (state will be automatically mapped: New → open, Closed → closed)
|
|
263
|
+
# specfact sync bridge --adapter github --repo-owner my-org --repo-name my-repo --mode bidirectional
|
|
235
264
|
```
|
|
236
265
|
|
|
237
266
|
## Context
|
|
@@ -200,6 +200,12 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
200
200
|
|
|
201
201
|
Note:
|
|
202
202
|
This implements the tool-agnostic metadata extraction pattern for Azure DevOps.
|
|
203
|
+
Future backlog adapters should implement similar parsing for their tools.
|
|
204
|
+
|
|
205
|
+
Change ID extraction priority:
|
|
206
|
+
1. Description footer (legacy format): *OpenSpec Change Proposal: `id`*
|
|
207
|
+
2. Comments (new format): **Change ID**: `id` in OpenSpec Change Proposal Reference comment
|
|
208
|
+
3. Work item ID (fallback)
|
|
203
209
|
"""
|
|
204
210
|
if not isinstance(item_data, dict):
|
|
205
211
|
msg = "ADO work item data must be dict"
|
|
@@ -256,15 +262,40 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
256
262
|
if impact_match:
|
|
257
263
|
impact = impact_match.group(1).strip()
|
|
258
264
|
|
|
259
|
-
# Extract change ID from OpenSpec metadata footer or work item ID
|
|
265
|
+
# Extract change ID from OpenSpec metadata footer, comments, or work item ID
|
|
260
266
|
change_id = None
|
|
267
|
+
|
|
268
|
+
# First, check description for OpenSpec metadata footer (legacy format)
|
|
261
269
|
if description_raw:
|
|
262
270
|
# Look for OpenSpec metadata footer: *OpenSpec Change Proposal: `{change_id}`*
|
|
263
271
|
change_id_match = re.search(r"OpenSpec Change Proposal:\s*`([^`]+)`", description_raw, re.IGNORECASE)
|
|
264
272
|
if change_id_match:
|
|
265
273
|
change_id = change_id_match.group(1)
|
|
274
|
+
|
|
275
|
+
# If not found in description, check comments (new format - OpenSpec info in comments)
|
|
276
|
+
if not change_id:
|
|
277
|
+
work_item_id = item_data.get("id")
|
|
278
|
+
if work_item_id and self.org and self.project:
|
|
279
|
+
comments = self._get_work_item_comments(self.org, self.project, work_item_id)
|
|
280
|
+
# Look for OpenSpec Change Proposal Reference comment
|
|
281
|
+
openspec_patterns = [
|
|
282
|
+
r"\*\*Change ID\*\*[:\s]+`([a-z0-9-]+)`",
|
|
283
|
+
r"Change ID[:\s]+`([a-z0-9-]+)`",
|
|
284
|
+
r"OpenSpec Change Proposal[:\s]+`?([a-z0-9-]+)`?",
|
|
285
|
+
r"\*OpenSpec Change Proposal:\s*`([a-z0-9-]+)`",
|
|
286
|
+
]
|
|
287
|
+
for comment in comments:
|
|
288
|
+
comment_text = comment.get("text", "") or comment.get("body", "")
|
|
289
|
+
for pattern in openspec_patterns:
|
|
290
|
+
match = re.search(pattern, comment_text, re.IGNORECASE | re.DOTALL)
|
|
291
|
+
if match:
|
|
292
|
+
change_id = match.group(1)
|
|
293
|
+
break
|
|
294
|
+
if change_id:
|
|
295
|
+
break
|
|
296
|
+
|
|
297
|
+
# Fallback to work item ID if still not found
|
|
266
298
|
if not change_id:
|
|
267
|
-
# Use work item ID as fallback
|
|
268
299
|
change_id = str(item_data.get("id", "unknown"))
|
|
269
300
|
|
|
270
301
|
# Extract status from System.State
|
|
@@ -1298,7 +1329,15 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
1298
1329
|
work_item_type = self._get_work_item_type(org, project)
|
|
1299
1330
|
|
|
1300
1331
|
# Map status to ADO state
|
|
1301
|
-
|
|
1332
|
+
# Check if source_state and source_type are provided (from cross-adapter sync)
|
|
1333
|
+
source_state = proposal_data.get("source_state")
|
|
1334
|
+
source_type = proposal_data.get("source_type")
|
|
1335
|
+
if source_state and source_type and source_type != "ado":
|
|
1336
|
+
# Use generic cross-adapter state mapping (preserves original state from source adapter)
|
|
1337
|
+
ado_state = self.map_backlog_state_between_adapters(source_state, source_type, self)
|
|
1338
|
+
else:
|
|
1339
|
+
# Use OpenSpec status mapping (default behavior)
|
|
1340
|
+
ado_state = self.map_openspec_status_to_backlog(status)
|
|
1302
1341
|
|
|
1303
1342
|
# Ensure API token is available
|
|
1304
1343
|
if not self.api_token:
|
|
@@ -1436,7 +1475,15 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
1436
1475
|
status = proposal_data.get("status", "proposed")
|
|
1437
1476
|
|
|
1438
1477
|
# Map status to ADO state
|
|
1439
|
-
|
|
1478
|
+
# Check if source_state and source_type are provided (from cross-adapter sync)
|
|
1479
|
+
source_state = proposal_data.get("source_state")
|
|
1480
|
+
source_type = proposal_data.get("source_type")
|
|
1481
|
+
if source_state and source_type and source_type != "ado":
|
|
1482
|
+
# Use generic cross-adapter state mapping (preserves original state from source adapter)
|
|
1483
|
+
ado_state = self.map_backlog_state_between_adapters(source_state, source_type, self)
|
|
1484
|
+
else:
|
|
1485
|
+
# Use OpenSpec status mapping (default behavior)
|
|
1486
|
+
ado_state = self.map_openspec_status_to_backlog(status)
|
|
1440
1487
|
|
|
1441
1488
|
# Ensure API token is available
|
|
1442
1489
|
if not self.api_token:
|
|
@@ -1546,7 +1593,15 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
1546
1593
|
body = "\n".join(body_parts)
|
|
1547
1594
|
|
|
1548
1595
|
# Map status to ADO state
|
|
1549
|
-
|
|
1596
|
+
# Check if source_state and source_type are provided (from cross-adapter sync)
|
|
1597
|
+
source_state = proposal_data.get("source_state")
|
|
1598
|
+
source_type = proposal_data.get("source_type")
|
|
1599
|
+
if source_state and source_type and source_type != "ado":
|
|
1600
|
+
# Use generic cross-adapter state mapping (preserves original state from source adapter)
|
|
1601
|
+
ado_state = self.map_backlog_state_between_adapters(source_state, source_type, self)
|
|
1602
|
+
else:
|
|
1603
|
+
# Use OpenSpec status mapping (default behavior)
|
|
1604
|
+
ado_state = self.map_openspec_status_to_backlog(status)
|
|
1550
1605
|
|
|
1551
1606
|
# Ensure API token is available
|
|
1552
1607
|
if not self.api_token:
|
|
@@ -1980,6 +2035,37 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
1980
2035
|
self.console.log(f"[bold yellow]Warning:[/bold yellow] Error checking branch existence: {e}")
|
|
1981
2036
|
return False
|
|
1982
2037
|
|
|
2038
|
+
def _get_work_item_comments(self, org: str, project: str, work_item_id: int) -> list[dict[str, Any]]:
|
|
2039
|
+
"""
|
|
2040
|
+
Fetch comments for an Azure DevOps work item.
|
|
2041
|
+
|
|
2042
|
+
Args:
|
|
2043
|
+
org: Azure DevOps organization
|
|
2044
|
+
project: Azure DevOps project
|
|
2045
|
+
work_item_id: Work item ID
|
|
2046
|
+
|
|
2047
|
+
Returns:
|
|
2048
|
+
List of comment dicts with 'text' or 'body' field, or empty list on error
|
|
2049
|
+
"""
|
|
2050
|
+
if not self.api_token:
|
|
2051
|
+
return []
|
|
2052
|
+
|
|
2053
|
+
url = f"{self.base_url}/{org}/{project}/_apis/wit/workitems/{work_item_id}/comments?api-version=7.1"
|
|
2054
|
+
headers = {
|
|
2055
|
+
"Accept": "application/json",
|
|
2056
|
+
**self._auth_headers(),
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
try:
|
|
2060
|
+
response = requests.get(url, headers=headers, timeout=30)
|
|
2061
|
+
response.raise_for_status()
|
|
2062
|
+
# ADO API returns comments in a 'comments' array within the response
|
|
2063
|
+
response_data = response.json()
|
|
2064
|
+
return response_data.get("comments", [])
|
|
2065
|
+
except requests.RequestException:
|
|
2066
|
+
# Return empty list on error - comments are optional
|
|
2067
|
+
return []
|
|
2068
|
+
|
|
1983
2069
|
@beartype
|
|
1984
2070
|
@require(lambda org: isinstance(org, str) and org, "Organization must be non-empty string")
|
|
1985
2071
|
@require(lambda project: isinstance(project, str) and project, "Project must be non-empty string")
|
|
@@ -74,6 +74,72 @@ class BacklogAdapterMixin(ABC):
|
|
|
74
74
|
tool-specific status mapping logic.
|
|
75
75
|
"""
|
|
76
76
|
|
|
77
|
+
@beartype
|
|
78
|
+
@require(
|
|
79
|
+
lambda source_state: isinstance(source_state, str) and len(source_state) > 0,
|
|
80
|
+
"Source state must be non-empty string",
|
|
81
|
+
)
|
|
82
|
+
@require(
|
|
83
|
+
lambda source_adapter_type: isinstance(source_adapter_type, str) and len(source_adapter_type) > 0,
|
|
84
|
+
"Source adapter type must be non-empty string",
|
|
85
|
+
)
|
|
86
|
+
@require(
|
|
87
|
+
lambda target_adapter: isinstance(target_adapter, BacklogAdapterMixin),
|
|
88
|
+
"Target adapter must implement BacklogAdapterMixin",
|
|
89
|
+
)
|
|
90
|
+
@ensure(lambda result: isinstance(result, str), "Must return status string")
|
|
91
|
+
def map_backlog_state_between_adapters(
|
|
92
|
+
self, source_state: str, source_adapter_type: str, target_adapter: BacklogAdapterMixin
|
|
93
|
+
) -> str:
|
|
94
|
+
"""
|
|
95
|
+
Map backlog state from one adapter to another using OpenSpec as intermediate format.
|
|
96
|
+
|
|
97
|
+
This method provides generic cross-adapter state mapping by:
|
|
98
|
+
1. Getting the source adapter instance
|
|
99
|
+
2. Mapping source state to OpenSpec status using source adapter's mapping
|
|
100
|
+
3. Mapping OpenSpec status to target state using target adapter's mapping
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
source_state: State from source adapter (e.g., "open", "closed", "New", "Active")
|
|
104
|
+
source_adapter_type: Source adapter type (e.g., "github", "ado", "jira")
|
|
105
|
+
target_adapter: Target adapter instance (must implement BacklogAdapterMixin)
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Target adapter state string
|
|
109
|
+
|
|
110
|
+
Note:
|
|
111
|
+
This is a generic method that works for any adapter pair by using OpenSpec
|
|
112
|
+
as the intermediate format. It requires the source adapter to be registered
|
|
113
|
+
in AdapterRegistry to retrieve its mapping methods.
|
|
114
|
+
"""
|
|
115
|
+
from specfact_cli.adapters.registry import AdapterRegistry
|
|
116
|
+
|
|
117
|
+
# Get source adapter instance to use its mapping methods
|
|
118
|
+
source_adapter = AdapterRegistry.get_adapter(source_adapter_type)
|
|
119
|
+
if not source_adapter or not isinstance(source_adapter, BacklogAdapterMixin):
|
|
120
|
+
# Fallback: if source adapter not found, try to map directly
|
|
121
|
+
# This handles cases where source adapter might not be registered
|
|
122
|
+
# In this case, we'll use the target adapter's default mapping
|
|
123
|
+
openspec_status = "proposed" # Default fallback
|
|
124
|
+
else:
|
|
125
|
+
# Step 1: Map source state to OpenSpec status using source adapter
|
|
126
|
+
openspec_status = source_adapter.map_backlog_status_to_openspec(source_state)
|
|
127
|
+
|
|
128
|
+
# Step 2: Map OpenSpec status to target state using target adapter
|
|
129
|
+
# Special handling for GitHub adapter: use issue state method instead of labels
|
|
130
|
+
if hasattr(target_adapter, "map_openspec_status_to_issue_state"):
|
|
131
|
+
# GitHub adapter: use issue state mapping (open/closed)
|
|
132
|
+
return target_adapter.map_openspec_status_to_issue_state(openspec_status)
|
|
133
|
+
|
|
134
|
+
target_state = target_adapter.map_openspec_status_to_backlog(openspec_status)
|
|
135
|
+
|
|
136
|
+
# Handle list return type (some adapters return lists)
|
|
137
|
+
if isinstance(target_state, list):
|
|
138
|
+
# Use first element if list (typically the primary state)
|
|
139
|
+
return target_state[0] if target_state else "New"
|
|
140
|
+
|
|
141
|
+
return target_state
|
|
142
|
+
|
|
77
143
|
@abstractmethod
|
|
78
144
|
@beartype
|
|
79
145
|
@require(lambda item_data: isinstance(item_data, dict), "Item data must be dict")
|
|
@@ -169,6 +169,30 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
169
169
|
# Default: treat as proposed
|
|
170
170
|
return "proposed"
|
|
171
171
|
|
|
172
|
+
@beartype
|
|
173
|
+
@require(lambda status: isinstance(status, str) and len(status) > 0, "Status must be non-empty string")
|
|
174
|
+
@ensure(lambda result: isinstance(result, str), "Must return issue state string")
|
|
175
|
+
def map_openspec_status_to_issue_state(self, status: str) -> str:
|
|
176
|
+
"""
|
|
177
|
+
Map OpenSpec change status to GitHub issue state (open/closed).
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
status: OpenSpec change status (proposed, in-progress, applied, deprecated, discarded)
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
GitHub issue state: "open" or "closed"
|
|
184
|
+
|
|
185
|
+
Note:
|
|
186
|
+
This method is used for cross-adapter state mapping where we need the
|
|
187
|
+
actual issue state, not labels. For label mapping, use map_openspec_status_to_backlog().
|
|
188
|
+
"""
|
|
189
|
+
# Map OpenSpec status to GitHub issue state
|
|
190
|
+
# "applied", "deprecated", "discarded" → closed
|
|
191
|
+
# "proposed", "in-progress" → open
|
|
192
|
+
if status in ("applied", "deprecated", "discarded"):
|
|
193
|
+
return "closed"
|
|
194
|
+
return "open"
|
|
195
|
+
|
|
172
196
|
@beartype
|
|
173
197
|
@require(lambda status: isinstance(status, str) and len(status) > 0, "Status must be non-empty string")
|
|
174
198
|
@ensure(lambda result: isinstance(result, list), "Must return list of label strings")
|
|
@@ -185,6 +209,8 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
185
209
|
Note:
|
|
186
210
|
This implements the tool-agnostic status mapping pattern for GitHub.
|
|
187
211
|
Future backlog adapters should implement similar mappings for their tools.
|
|
212
|
+
|
|
213
|
+
For cross-adapter state mapping (issue state, not labels), use map_openspec_status_to_issue_state().
|
|
188
214
|
"""
|
|
189
215
|
labels = ["openspec"]
|
|
190
216
|
|
|
@@ -210,6 +236,7 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
210
236
|
- Title (from issue title)
|
|
211
237
|
- Description (What Changes section)
|
|
212
238
|
- Rationale (Why section)
|
|
239
|
+
- Change ID (from body footer or comments)
|
|
213
240
|
- Other optional fields (timeline, owner, stakeholders, dependencies)
|
|
214
241
|
|
|
215
242
|
Args:
|
|
@@ -220,6 +247,7 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
220
247
|
- title: str
|
|
221
248
|
- description: str (What Changes section)
|
|
222
249
|
- rationale: str (Why section)
|
|
250
|
+
- change_id: str (extracted from body footer or comments)
|
|
223
251
|
- status: str (mapped to OpenSpec status)
|
|
224
252
|
- Other optional fields
|
|
225
253
|
|
|
@@ -229,6 +257,11 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
229
257
|
Note:
|
|
230
258
|
This implements the tool-agnostic metadata extraction pattern for GitHub.
|
|
231
259
|
Future backlog adapters should implement similar parsing for their tools.
|
|
260
|
+
|
|
261
|
+
Change ID extraction priority:
|
|
262
|
+
1. Body footer (legacy format): *OpenSpec Change Proposal: `id`*
|
|
263
|
+
2. Comments (new format): **Change ID**: `id` in OpenSpec Change Proposal Reference comment
|
|
264
|
+
3. Issue number (fallback)
|
|
232
265
|
"""
|
|
233
266
|
if not isinstance(item_data, dict):
|
|
234
267
|
msg = "GitHub issue data must be dict"
|
|
@@ -278,15 +311,41 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
278
311
|
if impact_match:
|
|
279
312
|
impact = impact_match.group(1).strip()
|
|
280
313
|
|
|
281
|
-
# Extract change ID from OpenSpec metadata footer or issue number
|
|
314
|
+
# Extract change ID from OpenSpec metadata footer, comments, or issue number
|
|
282
315
|
change_id = None
|
|
316
|
+
|
|
317
|
+
# First, check body for OpenSpec metadata footer (legacy format)
|
|
283
318
|
if body:
|
|
284
319
|
# Look for OpenSpec metadata footer: *OpenSpec Change Proposal: `{change_id}`*
|
|
285
320
|
change_id_match = re.search(r"OpenSpec Change Proposal:\s*`([^`]+)`", body, re.IGNORECASE)
|
|
286
321
|
if change_id_match:
|
|
287
322
|
change_id = change_id_match.group(1)
|
|
323
|
+
|
|
324
|
+
# If not found in body, check comments (new format - OpenSpec info in comments)
|
|
325
|
+
if not change_id:
|
|
326
|
+
issue_number = item_data.get("number")
|
|
327
|
+
if issue_number and self.repo_owner and self.repo_name:
|
|
328
|
+
comments = self._get_issue_comments(self.repo_owner, self.repo_name, issue_number)
|
|
329
|
+
# Look for OpenSpec Change Proposal Reference comment
|
|
330
|
+
# Pattern 1: Structured comment format with "**Change ID**: `id`"
|
|
331
|
+
openspec_patterns = [
|
|
332
|
+
r"\*\*Change ID\*\*[:\s]+`([a-z0-9-]+)`",
|
|
333
|
+
r"Change ID[:\s]+`([a-z0-9-]+)`",
|
|
334
|
+
r"OpenSpec Change Proposal[:\s]+`?([a-z0-9-]+)`?",
|
|
335
|
+
r"\*OpenSpec Change Proposal:\s*`([a-z0-9-]+)`",
|
|
336
|
+
]
|
|
337
|
+
for comment in comments:
|
|
338
|
+
comment_body = comment.get("body", "")
|
|
339
|
+
for pattern in openspec_patterns:
|
|
340
|
+
match = re.search(pattern, comment_body, re.IGNORECASE | re.DOTALL)
|
|
341
|
+
if match:
|
|
342
|
+
change_id = match.group(1)
|
|
343
|
+
break
|
|
344
|
+
if change_id:
|
|
345
|
+
break
|
|
346
|
+
|
|
347
|
+
# Fallback to issue number if still not found
|
|
288
348
|
if not change_id:
|
|
289
|
-
# Use issue number as fallback
|
|
290
349
|
change_id = str(item_data.get("number", "unknown"))
|
|
291
350
|
|
|
292
351
|
# Extract status from labels
|
|
@@ -491,12 +550,17 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
491
550
|
source_metadata.setdefault("source_repo", source_repo)
|
|
492
551
|
|
|
493
552
|
entry_id = artifact_path.get("number") or artifact_path.get("id")
|
|
553
|
+
# Extract GitHub issue state (open/closed) for cross-adapter sync state preservation
|
|
554
|
+
github_state = artifact_path.get("state", "open").lower()
|
|
494
555
|
entry = {
|
|
495
556
|
"source_id": str(entry_id) if entry_id is not None else None,
|
|
496
557
|
"source_url": artifact_path.get("html_url") or artifact_path.get("url") or "",
|
|
497
558
|
"source_type": "github",
|
|
498
559
|
"source_repo": source_repo or "",
|
|
499
|
-
"source_metadata": {
|
|
560
|
+
"source_metadata": {
|
|
561
|
+
"last_synced_status": proposal.status,
|
|
562
|
+
"source_state": github_state, # Preserve GitHub state for cross-adapter sync
|
|
563
|
+
},
|
|
500
564
|
}
|
|
501
565
|
entries = source_metadata.get("backlog_entries")
|
|
502
566
|
if not isinstance(entries, list):
|
|
@@ -1011,6 +1075,17 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
1011
1075
|
|
|
1012
1076
|
body = "\n".join(body_parts)
|
|
1013
1077
|
|
|
1078
|
+
# Check for API token before making request
|
|
1079
|
+
if not self.api_token:
|
|
1080
|
+
msg = (
|
|
1081
|
+
"GitHub API token required to create issues. Options:\n"
|
|
1082
|
+
" 1. Set GITHUB_TOKEN environment variable\n"
|
|
1083
|
+
" 2. Use --github-token option\n"
|
|
1084
|
+
" 3. Use GitHub CLI authentication (gh auth login)\n"
|
|
1085
|
+
" 4. Store token via specfact auth github"
|
|
1086
|
+
)
|
|
1087
|
+
raise ValueError(msg)
|
|
1088
|
+
|
|
1014
1089
|
# Create issue via GitHub API
|
|
1015
1090
|
url = f"{self.base_url}/repos/{repo_owner}/{repo_name}/issues"
|
|
1016
1091
|
headers = {
|
|
@@ -1018,9 +1093,24 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
1018
1093
|
"Accept": "application/vnd.github.v3+json",
|
|
1019
1094
|
}
|
|
1020
1095
|
# Determine issue state based on proposal status
|
|
1021
|
-
#
|
|
1022
|
-
|
|
1023
|
-
|
|
1096
|
+
# Check if source_state and source_type are provided (from cross-adapter sync)
|
|
1097
|
+
source_state = proposal_data.get("source_state")
|
|
1098
|
+
source_type = proposal_data.get("source_type")
|
|
1099
|
+
if source_state and source_type and source_type != "github":
|
|
1100
|
+
# Use generic cross-adapter state mapping (preserves original state from source adapter)
|
|
1101
|
+
from specfact_cli.adapters.registry import AdapterRegistry
|
|
1102
|
+
|
|
1103
|
+
source_adapter = AdapterRegistry.get_adapter(source_type)
|
|
1104
|
+
if source_adapter and hasattr(source_adapter, "map_backlog_state_between_adapters"):
|
|
1105
|
+
issue_state = source_adapter.map_backlog_state_between_adapters(source_state, source_type, self)
|
|
1106
|
+
else:
|
|
1107
|
+
# Fallback: map via OpenSpec status
|
|
1108
|
+
should_close = status in ("applied", "deprecated", "discarded")
|
|
1109
|
+
issue_state = "closed" if should_close else "open"
|
|
1110
|
+
else:
|
|
1111
|
+
# Use OpenSpec status mapping (default behavior)
|
|
1112
|
+
should_close = status in ("applied", "deprecated", "discarded")
|
|
1113
|
+
issue_state = "closed" if should_close else "open"
|
|
1024
1114
|
|
|
1025
1115
|
# Map status to GitHub state_reason
|
|
1026
1116
|
state_reason = None
|
|
@@ -1120,7 +1210,23 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
1120
1210
|
title = proposal_data.get("title", "Untitled")
|
|
1121
1211
|
|
|
1122
1212
|
# Map status to GitHub issue state and comment
|
|
1123
|
-
|
|
1213
|
+
# Check if source_state and source_type are provided (from cross-adapter sync)
|
|
1214
|
+
source_state = proposal_data.get("source_state")
|
|
1215
|
+
source_type = proposal_data.get("source_type")
|
|
1216
|
+
if source_state and source_type and source_type != "github":
|
|
1217
|
+
# Use generic cross-adapter state mapping (preserves original state from source adapter)
|
|
1218
|
+
from specfact_cli.adapters.registry import AdapterRegistry
|
|
1219
|
+
|
|
1220
|
+
source_adapter = AdapterRegistry.get_adapter(source_type)
|
|
1221
|
+
if source_adapter and hasattr(source_adapter, "map_backlog_state_between_adapters"):
|
|
1222
|
+
issue_state = source_adapter.map_backlog_state_between_adapters(source_state, source_type, self)
|
|
1223
|
+
should_close = issue_state == "closed"
|
|
1224
|
+
else:
|
|
1225
|
+
# Fallback: map via OpenSpec status
|
|
1226
|
+
should_close = status in ("applied", "deprecated", "discarded")
|
|
1227
|
+
else:
|
|
1228
|
+
# Use OpenSpec status mapping (default behavior)
|
|
1229
|
+
should_close = status in ("applied", "deprecated", "discarded")
|
|
1124
1230
|
source_tracking = proposal_data.get("source_tracking", {})
|
|
1125
1231
|
# Note: code_repo_path not available in _update_issue_status context
|
|
1126
1232
|
comment_text = self._get_status_comment(status, title, source_tracking, None)
|
|
@@ -1161,6 +1267,35 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
1161
1267
|
console.print(f"[bold red]✗[/bold red] {msg}")
|
|
1162
1268
|
raise
|
|
1163
1269
|
|
|
1270
|
+
def _get_issue_comments(self, repo_owner: str, repo_name: str, issue_number: int) -> list[dict[str, Any]]:
|
|
1271
|
+
"""
|
|
1272
|
+
Fetch comments for a GitHub issue.
|
|
1273
|
+
|
|
1274
|
+
Args:
|
|
1275
|
+
repo_owner: GitHub repository owner
|
|
1276
|
+
repo_name: GitHub repository name
|
|
1277
|
+
issue_number: Issue number
|
|
1278
|
+
|
|
1279
|
+
Returns:
|
|
1280
|
+
List of comment dicts with 'body' field, or empty list on error
|
|
1281
|
+
"""
|
|
1282
|
+
if not self.api_token:
|
|
1283
|
+
return []
|
|
1284
|
+
|
|
1285
|
+
url = f"{self.base_url}/repos/{repo_owner}/{repo_name}/issues/{issue_number}/comments"
|
|
1286
|
+
headers = {
|
|
1287
|
+
"Authorization": f"token {self.api_token}",
|
|
1288
|
+
"Accept": "application/vnd.github.v3+json",
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
try:
|
|
1292
|
+
response = requests.get(url, headers=headers, timeout=30)
|
|
1293
|
+
response.raise_for_status()
|
|
1294
|
+
return response.json()
|
|
1295
|
+
except requests.RequestException:
|
|
1296
|
+
# Return empty list on error - comments are optional
|
|
1297
|
+
return []
|
|
1298
|
+
|
|
1164
1299
|
def _add_issue_comment(self, repo_owner: str, repo_name: str, issue_number: int, comment: str) -> None:
|
|
1165
1300
|
"""
|
|
1166
1301
|
Add comment to GitHub issue.
|
|
@@ -2419,7 +2554,7 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
2419
2554
|
@beartype
|
|
2420
2555
|
@require(lambda item: isinstance(item, BacklogItem), "Item must be BacklogItem")
|
|
2421
2556
|
@require(
|
|
2422
|
-
lambda update_fields: update_fields is None or isinstance(update_fields, list),
|
|
2557
|
+
lambda item, update_fields: update_fields is None or isinstance(update_fields, list),
|
|
2423
2558
|
"Update fields must be None or list",
|
|
2424
2559
|
)
|
|
2425
2560
|
@ensure(lambda result: isinstance(result, BacklogItem), "Must return BacklogItem")
|
|
@@ -97,6 +97,35 @@ def _apply_filters(
|
|
|
97
97
|
return filtered
|
|
98
98
|
|
|
99
99
|
|
|
100
|
+
def _extract_openspec_change_id(body: str) -> str | None:
|
|
101
|
+
"""
|
|
102
|
+
Extract OpenSpec change proposal ID from issue body.
|
|
103
|
+
|
|
104
|
+
Looks for patterns like:
|
|
105
|
+
- *OpenSpec Change Proposal: `id`*
|
|
106
|
+
- OpenSpec Change Proposal: `id`
|
|
107
|
+
- OpenSpec.*proposal: `id`
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
body: Issue body text
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Change proposal ID if found, None otherwise
|
|
114
|
+
"""
|
|
115
|
+
import re
|
|
116
|
+
|
|
117
|
+
openspec_patterns = [
|
|
118
|
+
r"OpenSpec Change Proposal[:\s]+`?([a-z0-9-]+)`?",
|
|
119
|
+
r"\*OpenSpec Change Proposal:\s*`([a-z0-9-]+)`",
|
|
120
|
+
r"OpenSpec.*proposal[:\s]+`?([a-z0-9-]+)`?",
|
|
121
|
+
]
|
|
122
|
+
for pattern in openspec_patterns:
|
|
123
|
+
match = re.search(pattern, body, re.IGNORECASE)
|
|
124
|
+
if match:
|
|
125
|
+
return match.group(1)
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
100
129
|
def _build_adapter_kwargs(
|
|
101
130
|
adapter: str,
|
|
102
131
|
repo_owner: str | None = None,
|
|
@@ -623,8 +652,12 @@ def refine(
|
|
|
623
652
|
|
|
624
653
|
# Add OpenSpec comment if requested
|
|
625
654
|
if openspec_comment:
|
|
655
|
+
# Extract OpenSpec change proposal ID from original body if present
|
|
656
|
+
original_body = item.body_markdown or ""
|
|
657
|
+
openspec_change_id = _extract_openspec_change_id(original_body)
|
|
658
|
+
|
|
626
659
|
# Generate OpenSpec change proposal reference
|
|
627
|
-
change_id = f"backlog-refine-{item.id}"
|
|
660
|
+
change_id = openspec_change_id or f"backlog-refine-{item.id}"
|
|
628
661
|
comment_text = (
|
|
629
662
|
f"## OpenSpec Change Proposal Reference\n\n"
|
|
630
663
|
f"This backlog item was refined using SpecFact CLI template-driven refinement.\n\n"
|
|
@@ -671,8 +704,12 @@ def refine(
|
|
|
671
704
|
|
|
672
705
|
# Add OpenSpec comment if requested
|
|
673
706
|
if openspec_comment:
|
|
707
|
+
# Extract OpenSpec change proposal ID from original body if present
|
|
708
|
+
original_body = item.body_markdown or ""
|
|
709
|
+
openspec_change_id = _extract_openspec_change_id(original_body)
|
|
710
|
+
|
|
674
711
|
# Generate OpenSpec change proposal reference
|
|
675
|
-
change_id = f"backlog-refine-{item.id}"
|
|
712
|
+
change_id = openspec_change_id or f"backlog-refine-{item.id}"
|
|
676
713
|
comment_text = (
|
|
677
714
|
f"## OpenSpec Change Proposal Reference\n\n"
|
|
678
715
|
f"This backlog item was refined using SpecFact CLI template-driven refinement.\n\n"
|
|
@@ -1912,6 +1912,26 @@ class BridgeSync:
|
|
|
1912
1912
|
"source_tracking": entries,
|
|
1913
1913
|
}
|
|
1914
1914
|
|
|
1915
|
+
# Extract source state from backlog entries (for cross-adapter sync state preservation)
|
|
1916
|
+
# Check for source backlog entry from a different adapter (generic approach)
|
|
1917
|
+
source_state = None
|
|
1918
|
+
source_type = None
|
|
1919
|
+
for entry in entries:
|
|
1920
|
+
if isinstance(entry, dict):
|
|
1921
|
+
entry_type = entry.get("source_type", "").lower()
|
|
1922
|
+
# Look for entry from a different adapter (not the target adapter)
|
|
1923
|
+
if entry_type and entry_type != adapter_type.lower():
|
|
1924
|
+
source_metadata = entry.get("source_metadata", {})
|
|
1925
|
+
entry_source_state = source_metadata.get("source_state")
|
|
1926
|
+
if entry_source_state:
|
|
1927
|
+
source_state = entry_source_state
|
|
1928
|
+
source_type = entry_type
|
|
1929
|
+
break
|
|
1930
|
+
|
|
1931
|
+
if source_state and source_type:
|
|
1932
|
+
proposal_dict["source_state"] = source_state
|
|
1933
|
+
proposal_dict["source_type"] = source_type
|
|
1934
|
+
|
|
1915
1935
|
if isinstance(proposal.source_tracking.source_metadata, dict):
|
|
1916
1936
|
raw_title = proposal.source_tracking.source_metadata.get("raw_title")
|
|
1917
1937
|
raw_body = proposal.source_tracking.source_metadata.get("raw_body")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|