specfact-cli 0.26.11__tar.gz → 0.26.14__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.11 → specfact_cli-0.26.14}/PKG-INFO +2 -1
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/README.md +1 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/pyproject.toml +1 -1
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/__init__.py +1 -1
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/__init__.py +1 -1
- specfact_cli-0.26.14/src/specfact_cli/__main__.py +6 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/adapters/ado.py +189 -92
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/adapters/github.py +8 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/cli.py +4 -2
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/analyze.py +28 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/auth.py +57 -1
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/backlog_commands.py +16 -6
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/drift.py +26 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/enforce.py +71 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/generate.py +142 -1
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/import_cmd.py +64 -1
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/init.py +10 -1
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/migrate.py +69 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/plan.py +34 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/project_cmd.py +50 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/repro.py +34 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/sdd.py +11 -1
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/spec.py +35 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/sync.py +44 -4
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/update.py +37 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/validate.py +42 -1
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/common/logger_setup.py +68 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/runtime.py +126 -4
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/sync/bridge_sync.py +11 -6
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/.gitignore +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/LICENSE.md +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/mappings/node-async.yaml +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/mappings/python-async.yaml +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/mappings/speckit-default.yaml +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/prompts/shared/cli-enforcement.md +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/prompts/specfact.01-import.md +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/prompts/specfact.02-plan.md +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/prompts/specfact.03-review.md +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/prompts/specfact.04-sdd.md +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/prompts/specfact.05-enforce.md +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/prompts/specfact.06-sync.md +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/prompts/specfact.07-contracts.md +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/prompts/specfact.backlog-refine.md +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/prompts/specfact.compare.md +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/prompts/specfact.sync-backlog.md +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/prompts/specfact.validate.md +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/schemas/deviation.schema.json +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/schemas/plan.schema.json +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/schemas/protocol.schema.json +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/backlog/defaults/defect_v1.yaml +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/backlog/defaults/enabler_v1.yaml +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/backlog/defaults/spike_v1.yaml +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/backlog/defaults/user_story_v1.yaml +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/backlog/field_mappings/ado_agile.yaml +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/backlog/field_mappings/ado_default.yaml +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/backlog/field_mappings/ado_kanban.yaml +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/backlog/field_mappings/ado_safe.yaml +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/backlog/field_mappings/ado_scrum.yaml +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/backlog/frameworks/safe/safe_feature_v1.yaml +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/backlog/frameworks/scrum/user_story_v1.yaml +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/backlog/personas/developer/developer_task_v1.yaml +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/backlog/personas/product-owner/user_story_v1.yaml +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/backlog/providers/ado/work_item_v1.yaml +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/github-action.yml.j2 +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/persona/architect.md.j2 +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/persona/developer.md.j2 +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/persona/product-owner.md.j2 +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/plan.bundle.yaml.j2 +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/pr-template.md.j2 +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/protocol.yaml.j2 +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/telemetry.yaml.example +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/adapters/__init__.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/adapters/backlog_base.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/adapters/base.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/adapters/openspec.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/adapters/openspec_parser.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/adapters/registry.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/adapters/speckit.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/agents/__init__.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/agents/analyze_agent.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/agents/base.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/agents/plan_agent.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/agents/registry.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/agents/sync_agent.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/analyzers/__init__.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/analyzers/ambiguity_scanner.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/analyzers/code_analyzer.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/analyzers/constitution_evidence_extractor.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/analyzers/contract_extractor.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/analyzers/control_flow_analyzer.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/analyzers/graph_analyzer.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/analyzers/relationship_mapper.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/analyzers/requirement_extractor.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/analyzers/test_pattern_extractor.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/__init__.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/adapters/__init__.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/adapters/base.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/adapters/local_yaml_adapter.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/ai_refiner.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/converter.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/filters.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/format_detector.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/formats/__init__.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/formats/base.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/formats/markdown_format.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/formats/structured_format.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/mappers/__init__.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/mappers/ado_mapper.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/mappers/base.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/mappers/github_mapper.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/mappers/template_config.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/template_detector.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/__init__.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/contract_cmd.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/common/__init__.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/common/logging_utils.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/common/text_utils.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/common/utils.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/comparators/__init__.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/comparators/plan_comparator.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/contracts/__init__.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/contracts/crosshair_props.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/enrichers/constitution_enricher.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/enrichers/plan_enricher.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/generators/__init__.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/generators/contract_generator.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/generators/openapi_extractor.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/generators/persona_exporter.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/generators/plan_generator.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/generators/protocol_generator.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/generators/report_generator.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/generators/task_generator.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/generators/test_to_openapi.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/generators/workflow_generator.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/importers/__init__.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/importers/speckit_converter.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/importers/speckit_scanner.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/integrations/__init__.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/integrations/specmatic.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/merge/__init__.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/merge/resolver.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/migrations/__init__.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/migrations/plan_migrator.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/__init__.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/backlog_item.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/bridge.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/capabilities.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/change.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/contract.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/deviation.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/dor_config.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/enforcement.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/persona_template.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/plan.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/project.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/protocol.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/quality.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/sdd.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/source_tracking.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/task.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/modes/__init__.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/modes/detector.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/modes/router.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/parsers/__init__.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/parsers/persona_importer.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/resources/semgrep/async.yml +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/resources/semgrep/code-quality.yml +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/resources/semgrep/feature-detection.yml +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/sync/__init__.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/sync/bridge_probe.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/sync/bridge_watch.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/sync/change_detector.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/sync/code_to_spec.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/sync/drift_detector.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/sync/repository_sync.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/sync/spec_to_code.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/sync/spec_to_tests.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/sync/watcher.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/sync/watcher_enhanced.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/telemetry.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/templates/__init__.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/templates/bridge_templates.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/templates/defaults/defect_v1.yaml +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/templates/defaults/enabler_v1.yaml +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/templates/defaults/spike_v1.yaml +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/templates/defaults/user_story_v1.yaml +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/templates/frameworks/scrum/user_story_v1.yaml +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/templates/personas/product-owner/user_story_v1.yaml +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/templates/providers/ado/work_item_v1.yaml +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/templates/registry.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/templates/specification_templates.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/__init__.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/acceptance_criteria.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/auth_tokens.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/bundle_loader.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/code_change_detector.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/console.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/content_sanitizer.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/context_detection.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/enrichment_context.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/enrichment_parser.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/env_manager.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/feature_keys.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/git.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/github_annotations.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/ide_setup.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/incremental_check.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/metadata.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/optional_deps.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/performance.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/progress.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/progressive_disclosure.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/prompts.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/sdd_discovery.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/source_scanner.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/startup_checks.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/structure.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/structured_io.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/suggestions.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/terminal.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/yaml_utils.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/__init__.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/agile_validation.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/change_proposal_integration.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/cli_first_validator.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/contract_validator.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/fsm.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/repro_checker.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/schema.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/__init__.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/contract_populator.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/crosshair_runner.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/crosshair_summary.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/dependency_installer.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/framework_detector.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/frameworks/__init__.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/frameworks/base.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/frameworks/django.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/frameworks/drf.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/frameworks/fastapi.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/frameworks/flask.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/harness_generator.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/models.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/orchestrator.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/specmatic_runner.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/unannotated_detector.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/versioning/__init__.py +0 -0
- {specfact_cli-0.26.11 → specfact_cli-0.26.14}/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.14
|
|
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
|
|
@@ -617,6 +617,7 @@ hatch run contract-test-full
|
|
|
617
617
|
|
|
618
618
|
- 💬 **Questions?** [GitHub Discussions](https://github.com/nold-ai/specfact-cli/discussions)
|
|
619
619
|
- 🐛 **Found a bug?** [GitHub Issues](https://github.com/nold-ai/specfact-cli/issues)
|
|
620
|
+
- 🔍 **Debugging I/O or API issues?** Run with `--debug`; logs are written to `~/.specfact/logs/specfact-debug.log`. With `--debug`, ADO API errors include response snippet and patch paths in the log. See [Debug Logging](docs/reference/debug-logging.md).
|
|
620
621
|
- 📧 **Need help?** [hello@noldai.com](mailto:hello@noldai.com)
|
|
621
622
|
- 🌐 **Learn more:** [specfact.com](https://specfact.com) • [specfact.io](https://specfact.io) • [specfact.dev](https://specfact.dev)
|
|
622
623
|
|
|
@@ -339,6 +339,7 @@ hatch run contract-test-full
|
|
|
339
339
|
|
|
340
340
|
- 💬 **Questions?** [GitHub Discussions](https://github.com/nold-ai/specfact-cli/discussions)
|
|
341
341
|
- 🐛 **Found a bug?** [GitHub Issues](https://github.com/nold-ai/specfact-cli/issues)
|
|
342
|
+
- 🔍 **Debugging I/O or API issues?** Run with `--debug`; logs are written to `~/.specfact/logs/specfact-debug.log`. With `--debug`, ADO API errors include response snippet and patch paths in the log. See [Debug Logging](docs/reference/debug-logging.md).
|
|
342
343
|
- 📧 **Need help?** [hello@noldai.com](mailto:hello@noldai.com)
|
|
343
344
|
- 🌐 **Learn more:** [specfact.com](https://specfact.com) • [specfact.io](https://specfact.io) • [specfact.dev](https://specfact.dev)
|
|
344
345
|
|
|
@@ -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.14"
|
|
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"
|
|
@@ -27,17 +27,77 @@ from specfact_cli.adapters.base import BridgeAdapter
|
|
|
27
27
|
from specfact_cli.backlog.adapters.base import BacklogAdapter
|
|
28
28
|
from specfact_cli.backlog.filters import BacklogFilters
|
|
29
29
|
from specfact_cli.backlog.mappers.ado_mapper import AdoFieldMapper
|
|
30
|
+
from specfact_cli.common.logger_setup import LoggerSetup
|
|
30
31
|
from specfact_cli.models.backlog_item import BacklogItem
|
|
31
32
|
from specfact_cli.models.bridge import BridgeConfig
|
|
32
33
|
from specfact_cli.models.capabilities import ToolCapabilities
|
|
33
34
|
from specfact_cli.models.change import ChangeProposal, ChangeTracking
|
|
34
|
-
from specfact_cli.runtime import debug_print
|
|
35
|
+
from specfact_cli.runtime import debug_log_operation, debug_print, is_debug_mode
|
|
35
36
|
from specfact_cli.utils.auth_tokens import get_token, set_token
|
|
36
37
|
|
|
37
38
|
|
|
39
|
+
_MAX_RESPONSE_BODY_LOG = 2048
|
|
40
|
+
|
|
38
41
|
console = Console()
|
|
39
42
|
|
|
40
43
|
|
|
44
|
+
def _log_ado_patch_failure(
|
|
45
|
+
response: requests.Response | None,
|
|
46
|
+
operations: list[dict[str, Any]],
|
|
47
|
+
url: str,
|
|
48
|
+
context: str = "",
|
|
49
|
+
) -> str:
|
|
50
|
+
"""
|
|
51
|
+
Log ADO PATCH failure to debug log (when debug on) and return user-facing message.
|
|
52
|
+
|
|
53
|
+
Parses response body (JSON message or truncated text), extracts patch paths,
|
|
54
|
+
redacts/truncates for debug log, and builds a user message with ADO text and hint.
|
|
55
|
+
"""
|
|
56
|
+
paths = [op.get("path", "") for op in operations if isinstance(op, dict)]
|
|
57
|
+
snippet = ""
|
|
58
|
+
if response is not None:
|
|
59
|
+
try:
|
|
60
|
+
body = response.json()
|
|
61
|
+
snippet = str(body.get("message", response.text[:500]))
|
|
62
|
+
except Exception:
|
|
63
|
+
snippet = (response.text or "")[:_MAX_RESPONSE_BODY_LOG]
|
|
64
|
+
snippet = snippet[:_MAX_RESPONSE_BODY_LOG]
|
|
65
|
+
snippet = str(LoggerSetup.redact_secrets(snippet))
|
|
66
|
+
|
|
67
|
+
if is_debug_mode():
|
|
68
|
+
debug_log_operation(
|
|
69
|
+
"ado_patch",
|
|
70
|
+
url,
|
|
71
|
+
"failed",
|
|
72
|
+
error=context or snippet[:500],
|
|
73
|
+
extra={"response_body": snippet, "patch_paths": paths},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return _build_ado_user_message(response)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _build_ado_user_message(response: requests.Response | None) -> str:
|
|
80
|
+
"""Build user-facing error message from ADO response and append mapping hint."""
|
|
81
|
+
hint = " Check custom field mapping; see ado_custom.yaml or documentation."
|
|
82
|
+
if response is None:
|
|
83
|
+
return f"Azure DevOps request failed.{hint}"
|
|
84
|
+
try:
|
|
85
|
+
body = response.json()
|
|
86
|
+
msg = body.get("message", "") or (response.text or "")[:500]
|
|
87
|
+
except Exception:
|
|
88
|
+
msg = (response.text or "")[:500]
|
|
89
|
+
if not msg:
|
|
90
|
+
return f"Azure DevOps request failed (HTTP {getattr(response, 'status_code', '')}).{hint}"
|
|
91
|
+
|
|
92
|
+
m = re.search(r"Cannot find field\s+([^\s]+)", msg, re.IGNORECASE)
|
|
93
|
+
if m:
|
|
94
|
+
field = m.group(1).strip().rstrip(".")
|
|
95
|
+
user_msg = f"Field '{field}' not found.{hint}"
|
|
96
|
+
else:
|
|
97
|
+
user_msg = f"{msg}{hint}"
|
|
98
|
+
return user_msg
|
|
99
|
+
|
|
100
|
+
|
|
41
101
|
class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
42
102
|
"""
|
|
43
103
|
Azure DevOps bridge adapter implementing BridgeAdapter interface.
|
|
@@ -372,6 +432,8 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
372
432
|
rationale = ""
|
|
373
433
|
impact = ""
|
|
374
434
|
|
|
435
|
+
import re
|
|
436
|
+
|
|
375
437
|
# Parse markdown sections (Why, What Changes)
|
|
376
438
|
if description_raw:
|
|
377
439
|
# Extract "Why" section (stop at What Changes or OpenSpec footer)
|
|
@@ -696,6 +758,8 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
696
758
|
ValueError: If required configuration is missing
|
|
697
759
|
requests.RequestException: If Azure DevOps API call fails
|
|
698
760
|
"""
|
|
761
|
+
import re as _re
|
|
762
|
+
|
|
699
763
|
if not self.api_token:
|
|
700
764
|
msg = (
|
|
701
765
|
"Azure DevOps API token required. Options:\n"
|
|
@@ -751,7 +815,7 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
751
815
|
parsed = urlparse(source_url)
|
|
752
816
|
if parsed.hostname and parsed.hostname.lower() == "dev.azure.com":
|
|
753
817
|
target_org = target_repo.split("/")[0]
|
|
754
|
-
ado_org_match =
|
|
818
|
+
ado_org_match = _re.search(r"dev\.azure\.com/([^/]+)/", source_url)
|
|
755
819
|
if ado_org_match and ado_org_match.group(1) == target_org:
|
|
756
820
|
# Org matches - this is likely the same ADO organization
|
|
757
821
|
work_item_id = entry.get("source_id")
|
|
@@ -773,8 +837,8 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
773
837
|
# 3. AND (project is unknown in entry OR project is unknown in target OR both contain GUIDs)
|
|
774
838
|
# This prevents matching org/project-a with org/project-b when both have known project names
|
|
775
839
|
source_url = entry.get("source_url", "")
|
|
776
|
-
entry_has_guid = source_url and
|
|
777
|
-
r"dev\.azure\.com/[^/]+/[0-9a-f-]{36}", source_url,
|
|
840
|
+
entry_has_guid = source_url and _re.search(
|
|
841
|
+
r"dev\.azure\.com/[^/]+/[0-9a-f-]{36}", source_url, _re.IGNORECASE
|
|
778
842
|
)
|
|
779
843
|
project_unknown = (
|
|
780
844
|
not entry_project # Entry has no project part
|
|
@@ -967,8 +1031,10 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
967
1031
|
Returns:
|
|
968
1032
|
Tuple of (org, project, work_item_id)
|
|
969
1033
|
"""
|
|
1034
|
+
import re as _re
|
|
1035
|
+
|
|
970
1036
|
cleaned = item_ref.strip().lstrip("#")
|
|
971
|
-
url_match =
|
|
1037
|
+
url_match = _re.search(r"dev\.azure\.com/([^/]+)/([^/]+)/.*?/(\d+)", cleaned, _re.IGNORECASE)
|
|
972
1038
|
if url_match:
|
|
973
1039
|
return url_match.group(1), url_match.group(2), int(url_match.group(3))
|
|
974
1040
|
|
|
@@ -1439,6 +1505,13 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
1439
1505
|
|
|
1440
1506
|
try:
|
|
1441
1507
|
response = requests.post(url, json=wiql, headers=headers, timeout=10)
|
|
1508
|
+
if is_debug_mode():
|
|
1509
|
+
debug_log_operation(
|
|
1510
|
+
"ado_wiql",
|
|
1511
|
+
url,
|
|
1512
|
+
str(response.status_code),
|
|
1513
|
+
error=None if response.ok else (response.text[:200] if response.text else None),
|
|
1514
|
+
)
|
|
1442
1515
|
if response.status_code != 200:
|
|
1443
1516
|
return None
|
|
1444
1517
|
work_items = response.json().get("workItems", [])
|
|
@@ -1453,7 +1526,9 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
1453
1526
|
"source_type": "ado",
|
|
1454
1527
|
"source_repo": f"{org}/{project}",
|
|
1455
1528
|
}
|
|
1456
|
-
except requests.RequestException:
|
|
1529
|
+
except requests.RequestException as e:
|
|
1530
|
+
if is_debug_mode():
|
|
1531
|
+
debug_log_operation("ado_wiql", url, "error", error=str(e))
|
|
1457
1532
|
return None
|
|
1458
1533
|
|
|
1459
1534
|
def _create_work_item_from_proposal(
|
|
@@ -1473,6 +1548,8 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
1473
1548
|
Returns:
|
|
1474
1549
|
Dict with work item data: {"work_item_id": int, "work_item_url": str, "state": str}
|
|
1475
1550
|
"""
|
|
1551
|
+
import re as _re
|
|
1552
|
+
|
|
1476
1553
|
title = proposal_data.get("title", "Untitled Change Proposal")
|
|
1477
1554
|
description = proposal_data.get("description", "")
|
|
1478
1555
|
rationale = proposal_data.get("rationale", "")
|
|
@@ -1489,7 +1566,7 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
1489
1566
|
else:
|
|
1490
1567
|
body_parts = []
|
|
1491
1568
|
|
|
1492
|
-
display_title =
|
|
1569
|
+
display_title = _re.sub(r"^\[change\]\s*", "", title, flags=_re.IGNORECASE).strip()
|
|
1493
1570
|
if display_title:
|
|
1494
1571
|
body_parts.append(f"# {display_title}")
|
|
1495
1572
|
body_parts.append("")
|
|
@@ -1572,6 +1649,13 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
1572
1649
|
|
|
1573
1650
|
try:
|
|
1574
1651
|
response = requests.patch(url, json=patch_document, headers=headers, timeout=30)
|
|
1652
|
+
if is_debug_mode():
|
|
1653
|
+
debug_log_operation(
|
|
1654
|
+
"ado_patch",
|
|
1655
|
+
url,
|
|
1656
|
+
str(response.status_code),
|
|
1657
|
+
error=None if response.ok else (response.text[:200] if response.text else None),
|
|
1658
|
+
)
|
|
1575
1659
|
response.raise_for_status()
|
|
1576
1660
|
work_item_data = response.json()
|
|
1577
1661
|
|
|
@@ -1617,8 +1701,10 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
1617
1701
|
"state": ado_state,
|
|
1618
1702
|
}
|
|
1619
1703
|
except requests.RequestException as e:
|
|
1620
|
-
|
|
1621
|
-
|
|
1704
|
+
resp = getattr(e, "response", None)
|
|
1705
|
+
user_msg = _log_ado_patch_failure(resp, patch_document, url)
|
|
1706
|
+
e.ado_user_message = user_msg
|
|
1707
|
+
console.print(f"[bold red]✗[/bold red] {user_msg}")
|
|
1622
1708
|
raise
|
|
1623
1709
|
|
|
1624
1710
|
def _update_work_item_status(
|
|
@@ -1717,8 +1803,9 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
1717
1803
|
"state": ado_state,
|
|
1718
1804
|
}
|
|
1719
1805
|
except requests.RequestException as e:
|
|
1720
|
-
|
|
1721
|
-
|
|
1806
|
+
resp = getattr(e, "response", None)
|
|
1807
|
+
user_msg = _log_ado_patch_failure(resp, patch_document, url)
|
|
1808
|
+
console.print(f"[bold red]✗[/bold red] {user_msg}")
|
|
1722
1809
|
raise
|
|
1723
1810
|
|
|
1724
1811
|
def _update_work_item_body(
|
|
@@ -1740,6 +1827,8 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
1740
1827
|
Returns:
|
|
1741
1828
|
Dict with updated work item data: {"work_item_id": int, "work_item_url": str, "state": str}
|
|
1742
1829
|
"""
|
|
1830
|
+
import re as _re
|
|
1831
|
+
|
|
1743
1832
|
title = proposal_data.get("title", "Untitled Change Proposal")
|
|
1744
1833
|
description = proposal_data.get("description", "")
|
|
1745
1834
|
rationale = proposal_data.get("rationale", "")
|
|
@@ -1756,7 +1845,7 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
1756
1845
|
else:
|
|
1757
1846
|
body_parts = []
|
|
1758
1847
|
|
|
1759
|
-
display_title =
|
|
1848
|
+
display_title = _re.sub(r"^\[change\]\s*", "", title, flags=_re.IGNORECASE).strip()
|
|
1760
1849
|
if display_title:
|
|
1761
1850
|
body_parts.append(f"# {display_title}")
|
|
1762
1851
|
body_parts.append("")
|
|
@@ -1847,8 +1936,9 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
1847
1936
|
"state": ado_state,
|
|
1848
1937
|
}
|
|
1849
1938
|
except requests.RequestException as e:
|
|
1850
|
-
|
|
1851
|
-
|
|
1939
|
+
resp = getattr(e, "response", None)
|
|
1940
|
+
user_msg = _log_ado_patch_failure(resp, patch_document, url)
|
|
1941
|
+
console.print(f"[bold red]✗[/bold red] {user_msg}")
|
|
1852
1942
|
raise
|
|
1853
1943
|
|
|
1854
1944
|
@beartype
|
|
@@ -1954,8 +2044,10 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
1954
2044
|
"new_state": ado_state,
|
|
1955
2045
|
}
|
|
1956
2046
|
except requests.RequestException as e:
|
|
1957
|
-
|
|
1958
|
-
|
|
2047
|
+
resp = getattr(e, "response", None)
|
|
2048
|
+
user_msg = _log_ado_patch_failure(resp, patch_document, url)
|
|
2049
|
+
e.ado_user_message = user_msg
|
|
2050
|
+
console.print(f"[bold red]✗[/bold red] {user_msg}")
|
|
1959
2051
|
raise
|
|
1960
2052
|
|
|
1961
2053
|
@beartype
|
|
@@ -2332,8 +2424,10 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
2332
2424
|
"comment_added": True,
|
|
2333
2425
|
}
|
|
2334
2426
|
except requests.RequestException as e:
|
|
2335
|
-
|
|
2336
|
-
|
|
2427
|
+
resp = getattr(e, "response", None)
|
|
2428
|
+
user_msg = _log_ado_patch_failure(resp, [], url)
|
|
2429
|
+
e.ado_user_message = user_msg
|
|
2430
|
+
console.print(f"[bold red]✗[/bold red] {user_msg}")
|
|
2337
2431
|
raise
|
|
2338
2432
|
|
|
2339
2433
|
@beartype
|
|
@@ -2908,8 +3002,22 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
2908
3002
|
|
|
2909
3003
|
try:
|
|
2910
3004
|
response = requests.get(url, headers=workitems_headers, params=params, timeout=30)
|
|
3005
|
+
if is_debug_mode():
|
|
3006
|
+
debug_log_operation(
|
|
3007
|
+
"ado_workitems_get",
|
|
3008
|
+
url,
|
|
3009
|
+
str(response.status_code),
|
|
3010
|
+
error=None if response.ok else (response.text[:200] if response.text else None),
|
|
3011
|
+
)
|
|
2911
3012
|
response.raise_for_status()
|
|
2912
3013
|
except requests.HTTPError as e:
|
|
3014
|
+
if is_debug_mode():
|
|
3015
|
+
debug_log_operation(
|
|
3016
|
+
"ado_workitems_get",
|
|
3017
|
+
url,
|
|
3018
|
+
"error",
|
|
3019
|
+
error=str(e.response.status_code) if e.response is not None else str(e),
|
|
3020
|
+
)
|
|
2913
3021
|
# Provide better error message with URL details
|
|
2914
3022
|
error_detail = ""
|
|
2915
3023
|
if e.response is not None:
|
|
@@ -3095,11 +3203,12 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
3095
3203
|
|
|
3096
3204
|
# Update description (body_markdown) - always use System.Description
|
|
3097
3205
|
if update_fields is None or "body" in update_fields or "body_markdown" in update_fields:
|
|
3098
|
-
# Convert TODO markers to proper Markdown checkboxes for ADO rendering
|
|
3099
3206
|
import re
|
|
3100
3207
|
|
|
3101
|
-
|
|
3102
|
-
|
|
3208
|
+
# Never send null: ADO rejects null for /fields/System.Description (HTTP 400)
|
|
3209
|
+
raw_body = item.body_markdown
|
|
3210
|
+
markdown_content = raw_body if raw_body is not None else ""
|
|
3211
|
+
# Convert TODO markers to proper Markdown checkboxes for ADO rendering
|
|
3103
3212
|
todo_pattern = r"^(\s*)[-*]\s*\[TODO[:\s]+([^\]]+)\](.*)$"
|
|
3104
3213
|
markdown_content = re.sub(
|
|
3105
3214
|
todo_pattern,
|
|
@@ -3108,11 +3217,9 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
3108
3217
|
flags=re.MULTILINE | re.IGNORECASE,
|
|
3109
3218
|
)
|
|
3110
3219
|
|
|
3111
|
-
# Get mapped description field name (honors custom mappings)
|
|
3112
3220
|
description_field = reverse_mappings.get("description", "System.Description")
|
|
3113
|
-
# Set multiline field format to Markdown
|
|
3221
|
+
# Set multiline field format to Markdown first (optional; many ADO instances return 400 for this path)
|
|
3114
3222
|
operations.append({"op": "add", "path": f"/multilineFieldsFormat/{description_field}", "value": "Markdown"})
|
|
3115
|
-
# Then set description content with Markdown format
|
|
3116
3223
|
operations.append({"op": "replace", "path": f"/fields/{description_field}", "value": markdown_content})
|
|
3117
3224
|
|
|
3118
3225
|
# Update acceptance criteria using mapped field name (honors custom mappings)
|
|
@@ -3158,7 +3265,9 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
3158
3265
|
response = requests.patch(url, headers=headers, json=operations, timeout=30)
|
|
3159
3266
|
response.raise_for_status()
|
|
3160
3267
|
except requests.HTTPError as e:
|
|
3161
|
-
|
|
3268
|
+
user_msg = _log_ado_patch_failure(e.response, operations, url)
|
|
3269
|
+
e.ado_user_message = user_msg
|
|
3270
|
+
response = None
|
|
3162
3271
|
if e.response and e.response.status_code in (400, 422):
|
|
3163
3272
|
error_message = ""
|
|
3164
3273
|
try:
|
|
@@ -3167,91 +3276,79 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
3167
3276
|
except Exception:
|
|
3168
3277
|
pass
|
|
3169
3278
|
|
|
3170
|
-
#
|
|
3171
|
-
|
|
3172
|
-
|
|
3279
|
+
# First retry: omit multilineFieldsFormat entirely (only /fields/ updates).
|
|
3280
|
+
# Many ADO instances reject /multilineFieldsFormat/ path with 400 Bad Request.
|
|
3281
|
+
operations_no_format = [
|
|
3282
|
+
op for op in operations if not (op.get("path") or "").startswith("/multilineFieldsFormat/")
|
|
3283
|
+
]
|
|
3284
|
+
if operations_no_format != operations:
|
|
3285
|
+
try:
|
|
3286
|
+
resp = requests.patch(url, headers=headers, json=operations_no_format, timeout=30)
|
|
3287
|
+
resp.raise_for_status()
|
|
3288
|
+
response = resp
|
|
3289
|
+
except requests.HTTPError as retry_error:
|
|
3290
|
+
_log_ado_patch_failure(
|
|
3291
|
+
retry_error.response,
|
|
3292
|
+
operations_no_format,
|
|
3293
|
+
url,
|
|
3294
|
+
context=str(retry_error),
|
|
3295
|
+
)
|
|
3296
|
+
|
|
3297
|
+
if response is None and (
|
|
3298
|
+
"already exists" in error_message.lower() or "cannot add" in error_message.lower()
|
|
3299
|
+
):
|
|
3300
|
+
# Second: try "replace" instead of "add" for multilineFieldsFormat
|
|
3173
3301
|
operations_replace = []
|
|
3174
3302
|
for op in operations:
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
operations_replace.append({"op": "replace", "path":
|
|
3303
|
+
path = op.get("path") or ""
|
|
3304
|
+
if path.startswith("/multilineFieldsFormat/"):
|
|
3305
|
+
operations_replace.append({"op": "replace", "path": path, "value": op["value"]})
|
|
3178
3306
|
else:
|
|
3179
3307
|
operations_replace.append(op)
|
|
3180
|
-
|
|
3181
3308
|
try:
|
|
3182
|
-
|
|
3183
|
-
|
|
3309
|
+
resp = requests.patch(url, headers=headers, json=operations_replace, timeout=30)
|
|
3310
|
+
resp.raise_for_status()
|
|
3311
|
+
response = resp
|
|
3184
3312
|
except requests.HTTPError:
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
# Convert TODO markers to checkboxes first
|
|
3198
|
-
todo_pattern = r"^(\s*)[-*]\s*\[TODO[:\s]+([^\]]+)\](.*)$"
|
|
3199
|
-
markdown_for_html = re.sub(
|
|
3200
|
-
todo_pattern,
|
|
3201
|
-
r"\1- [ ] \2",
|
|
3202
|
-
markdown_for_html,
|
|
3203
|
-
flags=re.MULTILINE | re.IGNORECASE,
|
|
3204
|
-
)
|
|
3205
|
-
# Simple markdown to HTML conversion (basic)
|
|
3206
|
-
try:
|
|
3207
|
-
import markdown
|
|
3208
|
-
|
|
3209
|
-
html_body = markdown.markdown(
|
|
3210
|
-
markdown_for_html, extensions=["fenced_code", "tables"]
|
|
3211
|
-
)
|
|
3212
|
-
op["value"] = html_body
|
|
3213
|
-
except ImportError:
|
|
3214
|
-
# markdown library not available - use raw text
|
|
3215
|
-
console.print("[yellow]⚠ markdown library not available, using raw text[/yellow]")
|
|
3216
|
-
# Keep original markdown as-is (ADO may still render it)
|
|
3217
|
-
break
|
|
3218
|
-
|
|
3219
|
-
response = requests.patch(url, headers=headers, json=operations_html, timeout=30)
|
|
3220
|
-
response.raise_for_status()
|
|
3221
|
-
else:
|
|
3222
|
-
# Other 400/422 errors - try HTML fallback
|
|
3223
|
-
console.print("[yellow]⚠ Markdown format not supported, converting to HTML[/yellow]")
|
|
3224
|
-
operations_html = [op for op in operations if "/multilineFieldsFormat/" not in op.get("path", "")]
|
|
3225
|
-
# Find description operation and convert markdown to HTML
|
|
3313
|
+
pass
|
|
3314
|
+
|
|
3315
|
+
if response is None:
|
|
3316
|
+
# Third: HTML fallback (no multilineFieldsFormat, description as HTML)
|
|
3317
|
+
import re as _re
|
|
3318
|
+
|
|
3319
|
+
console.print("[yellow]⚠ Markdown format not supported, converting description to HTML[/yellow]")
|
|
3320
|
+
operations_html = [
|
|
3321
|
+
op for op in operations if not (op.get("path") or "").startswith("/multilineFieldsFormat/")
|
|
3322
|
+
]
|
|
3323
|
+
description_field = reverse_mappings.get("description", "System.Description")
|
|
3324
|
+
desc_path = f"/fields/{description_field}"
|
|
3226
3325
|
for op in operations_html:
|
|
3227
|
-
if op.get("path") ==
|
|
3228
|
-
|
|
3229
|
-
import re
|
|
3230
|
-
|
|
3231
|
-
markdown_for_html = op["value"]
|
|
3232
|
-
# Convert TODO markers to checkboxes first
|
|
3326
|
+
if op.get("path") == desc_path:
|
|
3327
|
+
markdown_for_html = op.get("value") or ""
|
|
3233
3328
|
todo_pattern = r"^(\s*)[-*]\s*\[TODO[:\s]+([^\]]+)\](.*)$"
|
|
3234
|
-
markdown_for_html =
|
|
3329
|
+
markdown_for_html = _re.sub(
|
|
3235
3330
|
todo_pattern,
|
|
3236
3331
|
r"\1- [ ] \2",
|
|
3237
3332
|
markdown_for_html,
|
|
3238
|
-
flags=
|
|
3333
|
+
flags=_re.MULTILINE | _re.IGNORECASE,
|
|
3239
3334
|
)
|
|
3240
|
-
# Simple markdown to HTML conversion (basic)
|
|
3241
3335
|
try:
|
|
3242
3336
|
import markdown
|
|
3243
3337
|
|
|
3244
|
-
|
|
3245
|
-
op["value"] = html_body
|
|
3338
|
+
op["value"] = markdown.markdown(markdown_for_html, extensions=["fenced_code", "tables"])
|
|
3246
3339
|
except ImportError:
|
|
3247
|
-
|
|
3248
|
-
console.print("[yellow]⚠ markdown library not available, using raw text[/yellow]")
|
|
3249
|
-
# Keep original markdown as-is (ADO may still render it)
|
|
3340
|
+
pass
|
|
3250
3341
|
break
|
|
3342
|
+
try:
|
|
3343
|
+
resp = requests.patch(url, headers=headers, json=operations_html, timeout=30)
|
|
3344
|
+
resp.raise_for_status()
|
|
3345
|
+
response = resp
|
|
3346
|
+
except requests.HTTPError:
|
|
3347
|
+
console.print(f"[bold red]✗[/bold red] {user_msg}")
|
|
3348
|
+
raise
|
|
3251
3349
|
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
else:
|
|
3350
|
+
if response is None:
|
|
3351
|
+
console.print(f"[bold red]✗[/bold red] {user_msg}")
|
|
3255
3352
|
raise
|
|
3256
3353
|
|
|
3257
3354
|
updated_work_item = response.json()
|
|
@@ -35,6 +35,7 @@ from specfact_cli.models.backlog_item import BacklogItem
|
|
|
35
35
|
from specfact_cli.models.bridge import BridgeConfig
|
|
36
36
|
from specfact_cli.models.capabilities import ToolCapabilities
|
|
37
37
|
from specfact_cli.models.change import ChangeProposal, ChangeTracking
|
|
38
|
+
from specfact_cli.runtime import debug_log_operation, is_debug_mode
|
|
38
39
|
from specfact_cli.utils.auth_tokens import get_token
|
|
39
40
|
|
|
40
41
|
|
|
@@ -817,6 +818,13 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
817
818
|
"Accept": "application/vnd.github.v3+json",
|
|
818
819
|
}
|
|
819
820
|
response = requests.get(url, headers=headers, timeout=30)
|
|
821
|
+
if is_debug_mode():
|
|
822
|
+
debug_log_operation(
|
|
823
|
+
"github_api_get",
|
|
824
|
+
url,
|
|
825
|
+
str(response.status_code),
|
|
826
|
+
error=None if response.ok else (response.text[:200] if response.text else None),
|
|
827
|
+
)
|
|
820
828
|
response.raise_for_status()
|
|
821
829
|
return response.json()
|
|
822
830
|
|
|
@@ -75,7 +75,7 @@ from specfact_cli.commands import (
|
|
|
75
75
|
validate,
|
|
76
76
|
)
|
|
77
77
|
from specfact_cli.modes import OperationalMode, detect_mode
|
|
78
|
-
from specfact_cli.runtime import get_configured_console, set_debug_mode
|
|
78
|
+
from specfact_cli.runtime import get_configured_console, init_debug_log_file, set_debug_mode
|
|
79
79
|
from specfact_cli.utils.progressive_disclosure import ProgressiveDisclosureGroup
|
|
80
80
|
from specfact_cli.utils.structured_io import StructuredFormat
|
|
81
81
|
|
|
@@ -246,7 +246,7 @@ def main(
|
|
|
246
246
|
debug: bool = typer.Option(
|
|
247
247
|
False,
|
|
248
248
|
"--debug",
|
|
249
|
-
help="Enable debug output (
|
|
249
|
+
help="Enable debug output: console diagnostics and log file at ~/.specfact/logs/specfact-debug.log (operation metadata for file I/O and API calls)",
|
|
250
250
|
),
|
|
251
251
|
skip_checks: bool = typer.Option(
|
|
252
252
|
False,
|
|
@@ -297,6 +297,8 @@ def main(
|
|
|
297
297
|
|
|
298
298
|
# Set debug mode
|
|
299
299
|
set_debug_mode(debug)
|
|
300
|
+
if debug:
|
|
301
|
+
init_debug_log_file()
|
|
300
302
|
|
|
301
303
|
runtime.configure_io_formats(input_format=input_format, output_format=output_format)
|
|
302
304
|
# Invert logic: --interactive means not non-interactive, --no-interactive means non-interactive
|
|
@@ -17,6 +17,7 @@ from rich.console import Console
|
|
|
17
17
|
from rich.table import Table
|
|
18
18
|
|
|
19
19
|
from specfact_cli.models.quality import CodeQuality, QualityTracking
|
|
20
|
+
from specfact_cli.runtime import debug_log_operation, debug_print, is_debug_mode
|
|
20
21
|
from specfact_cli.telemetry import telemetry
|
|
21
22
|
from specfact_cli.utils import print_error, print_success
|
|
22
23
|
from specfact_cli.utils.progress import load_bundle_with_progress
|
|
@@ -63,12 +64,23 @@ def analyze_contracts(
|
|
|
63
64
|
**Examples:**
|
|
64
65
|
specfact analyze contracts --repo . --bundle legacy-api
|
|
65
66
|
"""
|
|
67
|
+
if is_debug_mode():
|
|
68
|
+
debug_log_operation("command", "analyze contracts", "started", extra={"repo": str(repo), "bundle": bundle})
|
|
69
|
+
debug_print("[dim]analyze contracts: started[/dim]")
|
|
66
70
|
console = Console()
|
|
67
71
|
|
|
68
72
|
# Use active plan as default if bundle not provided
|
|
69
73
|
if bundle is None:
|
|
70
74
|
bundle = SpecFactStructure.get_active_bundle_name(repo)
|
|
71
75
|
if bundle is None:
|
|
76
|
+
if is_debug_mode():
|
|
77
|
+
debug_log_operation(
|
|
78
|
+
"command",
|
|
79
|
+
"analyze contracts",
|
|
80
|
+
"failed",
|
|
81
|
+
error="Bundle name required",
|
|
82
|
+
extra={"reason": "no_bundle"},
|
|
83
|
+
)
|
|
72
84
|
console.print("[bold red]✗[/bold red] Bundle name required")
|
|
73
85
|
console.print("[yellow]→[/yellow] Use --bundle option or run 'specfact plan select' to set active plan")
|
|
74
86
|
raise typer.Exit(1)
|
|
@@ -78,6 +90,14 @@ def analyze_contracts(
|
|
|
78
90
|
bundle_dir = SpecFactStructure.project_dir(base_path=repo_path, bundle_name=bundle)
|
|
79
91
|
|
|
80
92
|
if not bundle_dir.exists():
|
|
93
|
+
if is_debug_mode():
|
|
94
|
+
debug_log_operation(
|
|
95
|
+
"command",
|
|
96
|
+
"analyze contracts",
|
|
97
|
+
"failed",
|
|
98
|
+
error=f"Bundle not found: {bundle_dir}",
|
|
99
|
+
extra={"reason": "bundle_missing"},
|
|
100
|
+
)
|
|
81
101
|
print_error(f"Project bundle not found: {bundle_dir}")
|
|
82
102
|
raise typer.Exit(1)
|
|
83
103
|
|
|
@@ -200,6 +220,14 @@ def analyze_contracts(
|
|
|
200
220
|
"files_with_crosshair": files_with_crosshair,
|
|
201
221
|
}
|
|
202
222
|
)
|
|
223
|
+
if is_debug_mode():
|
|
224
|
+
debug_log_operation(
|
|
225
|
+
"command",
|
|
226
|
+
"analyze contracts",
|
|
227
|
+
"success",
|
|
228
|
+
extra={"files_analyzed": files_analyzed, "bundle": bundle},
|
|
229
|
+
)
|
|
230
|
+
debug_print("[dim]analyze contracts: success[/dim]")
|
|
203
231
|
|
|
204
232
|
|
|
205
233
|
def _analyze_file_quality(file_path: Path) -> CodeQuality:
|