specfact-cli 0.15.2__tar.gz → 0.15.5__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.15.2 → specfact_cli-0.15.5}/PKG-INFO +1 -1
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/pyproject.toml +1 -1
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/prompts/specfact.03-review.md +9 -2
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/__init__.py +1 -1
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/__init__.py +1 -1
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/commands/plan.py +283 -45
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/acceptance_criteria.py +7 -11
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/.gitignore +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/LICENSE.md +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/README.md +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/mappings/node-async.yaml +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/mappings/python-async.yaml +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/mappings/speckit-default.yaml +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/prompts/shared/cli-enforcement.md +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/prompts/specfact.01-import.md +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/prompts/specfact.02-plan.md +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/prompts/specfact.04-sdd.md +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/prompts/specfact.05-enforce.md +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/prompts/specfact.06-sync.md +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/prompts/specfact.07-contracts.md +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/prompts/specfact.compare.md +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/prompts/specfact.validate.md +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/schemas/deviation.schema.json +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/schemas/plan.schema.json +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/schemas/protocol.schema.json +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/templates/github-action.yml.j2 +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/templates/plan.bundle.yaml.j2 +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/templates/pr-template.md.j2 +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/templates/protocol.yaml.j2 +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/templates/telemetry.yaml.example +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/agents/__init__.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/agents/analyze_agent.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/agents/base.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/agents/plan_agent.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/agents/registry.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/agents/sync_agent.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/analyzers/__init__.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/analyzers/ambiguity_scanner.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/analyzers/code_analyzer.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/analyzers/constitution_evidence_extractor.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/analyzers/contract_extractor.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/analyzers/control_flow_analyzer.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/analyzers/graph_analyzer.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/analyzers/relationship_mapper.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/analyzers/requirement_extractor.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/analyzers/test_pattern_extractor.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/cli.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/commands/__init__.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/commands/analyze.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/commands/bridge.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/commands/drift.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/commands/enforce.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/commands/generate.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/commands/implement.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/commands/import_cmd.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/commands/init.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/commands/migrate.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/commands/repro.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/commands/run.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/commands/sdd.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/commands/spec.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/commands/sync.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/common/__init__.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/common/logger_setup.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/common/logging_utils.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/common/text_utils.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/common/utils.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/comparators/__init__.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/comparators/plan_comparator.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/enrichers/constitution_enricher.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/enrichers/plan_enricher.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/generators/__init__.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/generators/contract_generator.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/generators/openapi_extractor.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/generators/plan_generator.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/generators/protocol_generator.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/generators/report_generator.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/generators/task_generator.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/generators/test_to_openapi.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/generators/workflow_generator.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/importers/__init__.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/importers/speckit_converter.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/importers/speckit_scanner.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/integrations/__init__.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/integrations/specmatic.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/migrations/__init__.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/migrations/plan_migrator.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/models/__init__.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/models/bridge.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/models/deviation.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/models/enforcement.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/models/plan.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/models/project.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/models/protocol.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/models/quality.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/models/sdd.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/models/source_tracking.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/models/task.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/modes/__init__.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/modes/detector.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/modes/router.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/resources/semgrep/async.yml +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/resources/semgrep/code-quality.yml +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/resources/semgrep/feature-detection.yml +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/runtime.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/sync/__init__.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/sync/bridge_probe.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/sync/bridge_sync.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/sync/bridge_watch.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/sync/change_detector.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/sync/code_to_spec.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/sync/drift_detector.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/sync/repository_sync.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/sync/spec_to_code.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/sync/spec_to_tests.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/sync/speckit_sync.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/sync/watcher.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/sync/watcher_enhanced.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/telemetry.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/templates/__init__.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/templates/bridge_templates.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/templates/specification_templates.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/__init__.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/bundle_loader.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/console.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/context_detection.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/enrichment_context.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/enrichment_parser.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/feature_keys.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/git.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/github_annotations.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/ide_setup.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/incremental_check.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/optional_deps.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/performance.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/progress.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/progressive_disclosure.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/prompts.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/sdd_discovery.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/source_scanner.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/structure.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/structured_io.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/suggestions.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/yaml_utils.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/validators/__init__.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/validators/cli_first_validator.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/validators/contract_validator.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/validators/fsm.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/validators/repro_checker.py +0 -0
- {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/validators/schema.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: specfact-cli
|
|
3
|
-
Version: 0.15.
|
|
3
|
+
Version: 0.15.5
|
|
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.15.
|
|
7
|
+
version = "0.15.5"
|
|
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"
|
|
@@ -101,6 +101,7 @@ The recommendation helps less-experienced users make informed decisions.
|
|
|
101
101
|
- `--list-questions` - Output questions in JSON format. Default: False
|
|
102
102
|
- `--output-questions PATH` - Save questions directly to file (JSON format). Use with `--list-questions` to save instead of stdout. Default: None
|
|
103
103
|
- `--list-findings` - Output all findings in structured format. Default: False
|
|
104
|
+
- `--output-findings PATH` - Save findings directly to file (JSON/YAML format). Use with `--list-findings` to save instead of stdout. Default: None
|
|
104
105
|
- `--findings-format FORMAT` - Output format: json, yaml, or table. Default: json for non-interactive, table for interactive
|
|
105
106
|
|
|
106
107
|
### Behavior/Options
|
|
@@ -150,7 +151,11 @@ specfact plan review [<bundle-name>] --list-questions --output-questions /tmp/qu
|
|
|
150
151
|
```bash
|
|
151
152
|
# Get findings (saves to stdout - can redirect to /tmp/)
|
|
152
153
|
# Use /tmp/ to avoid polluting the codebase
|
|
154
|
+
# Option 1: Redirect output (includes CLI banner - not recommended)
|
|
153
155
|
specfact plan review [<bundle-name>] --list-findings --findings-format json --no-interactive > /tmp/findings.json
|
|
156
|
+
|
|
157
|
+
# Option 2: Save directly to file (recommended - clean JSON only)
|
|
158
|
+
specfact plan review [<bundle-name>] --list-findings --output-findings /tmp/findings.json --no-interactive
|
|
154
159
|
```
|
|
155
160
|
|
|
156
161
|
**Note**: The `--output-questions` option saves questions directly to a file, avoiding the need for complex JSON parsing. The ambiguity scanner now recognizes the simplified format (e.g., "Must verify X works correctly (see contract examples)") as valid and will not flag it as vague.
|
|
@@ -362,7 +367,8 @@ When in copilot mode, follow this three-phase workflow:
|
|
|
362
367
|
|
|
363
368
|
```bash
|
|
364
369
|
# Option 1: Get findings (redirect to /tmp/ to avoid polluting codebase)
|
|
365
|
-
|
|
370
|
+
# Option 1: Save findings directly to file (recommended - clean JSON only)
|
|
371
|
+
specfact plan review [<bundle-name>] --list-findings --output-findings /tmp/findings.json --no-interactive
|
|
366
372
|
|
|
367
373
|
# Option 2: Get questions and save directly to /tmp/ (recommended - avoids JSON parsing)
|
|
368
374
|
specfact plan review [<bundle-name>] --list-questions --output-questions /tmp/questions.json --no-interactive
|
|
@@ -513,6 +519,7 @@ Create one with: specfact plan init legacy-api
|
|
|
513
519
|
# Get findings first
|
|
514
520
|
/specfact.03-review --list-findings # List all findings
|
|
515
521
|
/specfact.03-review --list-findings --findings-format json # JSON format for enrichment
|
|
522
|
+
/specfact.03-review --list-findings --output-findings /tmp/findings.json # Save findings to file (clean JSON)
|
|
516
523
|
|
|
517
524
|
# Interactive review
|
|
518
525
|
/specfact.03-review # Uses active plan (default: 5 questions per session)
|
|
@@ -557,7 +564,7 @@ Create one with: specfact plan init legacy-api
|
|
|
557
564
|
2. **Get findings** (optional, for comprehensive analysis - use `/tmp/`):
|
|
558
565
|
|
|
559
566
|
```bash
|
|
560
|
-
specfact plan review [<bundle-name>] --list-findings --findings
|
|
567
|
+
specfact plan review [<bundle-name>] --list-findings --output-findings /tmp/findings.json --no-interactive
|
|
561
568
|
```
|
|
562
569
|
|
|
563
570
|
3. **LLM reasoning and user selection** (REQUIRED for partial findings):
|
|
@@ -2985,6 +2985,7 @@ def _output_findings(
|
|
|
2985
2985
|
report: Any, # AmbiguityReport (imported locally to avoid circular dependency)
|
|
2986
2986
|
findings_format: str | None,
|
|
2987
2987
|
is_non_interactive: bool,
|
|
2988
|
+
output_path: Path | None = None,
|
|
2988
2989
|
) -> None:
|
|
2989
2990
|
"""
|
|
2990
2991
|
Output findings in structured format or table.
|
|
@@ -2993,9 +2994,15 @@ def _output_findings(
|
|
|
2993
2994
|
report: Ambiguity report
|
|
2994
2995
|
findings_format: Output format (json, yaml, table)
|
|
2995
2996
|
is_non_interactive: Whether in non-interactive mode
|
|
2997
|
+
output_path: Optional file path to save findings. If None, outputs to stdout.
|
|
2996
2998
|
"""
|
|
2999
|
+
from rich.console import Console
|
|
3000
|
+
from rich.table import Table
|
|
3001
|
+
|
|
2997
3002
|
from specfact_cli.analyzers.ambiguity_scanner import AmbiguityStatus
|
|
2998
3003
|
|
|
3004
|
+
console = Console()
|
|
3005
|
+
|
|
2999
3006
|
# Determine output format
|
|
3000
3007
|
output_format_str = findings_format
|
|
3001
3008
|
if not output_format_str:
|
|
@@ -3038,12 +3045,47 @@ def _output_findings(
|
|
|
3038
3045
|
|
|
3039
3046
|
# Also show coverage summary
|
|
3040
3047
|
if report.coverage:
|
|
3048
|
+
from specfact_cli.analyzers.ambiguity_scanner import TaxonomyCategory
|
|
3049
|
+
|
|
3041
3050
|
console.print("\n[bold]Coverage Summary:[/bold]")
|
|
3051
|
+
# Count findings per category by status
|
|
3052
|
+
total_findings_by_category: dict[TaxonomyCategory, int] = {}
|
|
3053
|
+
clear_findings_by_category: dict[TaxonomyCategory, int] = {}
|
|
3054
|
+
partial_findings_by_category: dict[TaxonomyCategory, int] = {}
|
|
3055
|
+
for finding in findings_list:
|
|
3056
|
+
cat = finding.category
|
|
3057
|
+
total_findings_by_category[cat] = total_findings_by_category.get(cat, 0) + 1
|
|
3058
|
+
# Count by finding status
|
|
3059
|
+
if finding.status == AmbiguityStatus.CLEAR:
|
|
3060
|
+
clear_findings_by_category[cat] = clear_findings_by_category.get(cat, 0) + 1
|
|
3061
|
+
elif finding.status == AmbiguityStatus.PARTIAL:
|
|
3062
|
+
partial_findings_by_category[cat] = partial_findings_by_category.get(cat, 0) + 1
|
|
3063
|
+
|
|
3042
3064
|
for cat, status in report.coverage.items():
|
|
3043
3065
|
status_icon = (
|
|
3044
3066
|
"✅" if status == AmbiguityStatus.CLEAR else "⚠️" if status == AmbiguityStatus.PARTIAL else "❌"
|
|
3045
3067
|
)
|
|
3046
|
-
|
|
3068
|
+
total = total_findings_by_category.get(cat, 0)
|
|
3069
|
+
clear_count = clear_findings_by_category.get(cat, 0)
|
|
3070
|
+
partial_count = partial_findings_by_category.get(cat, 0)
|
|
3071
|
+
# Show format based on status:
|
|
3072
|
+
# - Clear: If no findings (total=0), just show status. Otherwise show clear_count/total
|
|
3073
|
+
# - Partial: Show partial_count/total (count of findings with PARTIAL status = unclear findings)
|
|
3074
|
+
if status == AmbiguityStatus.CLEAR:
|
|
3075
|
+
if total == 0:
|
|
3076
|
+
# No findings - just show status without counts
|
|
3077
|
+
console.print(f" {status_icon} {cat.value}: {status.value}")
|
|
3078
|
+
else:
|
|
3079
|
+
console.print(f" {status_icon} {cat.value}: {clear_count}/{total} {status.value}")
|
|
3080
|
+
elif status == AmbiguityStatus.PARTIAL:
|
|
3081
|
+
# Show count of partial (unclear) findings
|
|
3082
|
+
# If all are unclear, just show the count without the fraction
|
|
3083
|
+
if partial_count == total:
|
|
3084
|
+
console.print(f" {status_icon} {cat.value}: {partial_count} {status.value}")
|
|
3085
|
+
else:
|
|
3086
|
+
console.print(f" {status_icon} {cat.value}: {partial_count}/{total} {status.value}")
|
|
3087
|
+
else: # MISSING
|
|
3088
|
+
console.print(f" {status_icon} {cat.value}: {status.value}")
|
|
3047
3089
|
|
|
3048
3090
|
elif output_format_str in ("json", "yaml"):
|
|
3049
3091
|
# Structured output (JSON or YAML)
|
|
@@ -3069,7 +3111,7 @@ def _output_findings(
|
|
|
3069
3111
|
import sys
|
|
3070
3112
|
|
|
3071
3113
|
if output_format_str == "json":
|
|
3072
|
-
|
|
3114
|
+
formatted_output = json.dumps(findings_data, indent=2) + "\n"
|
|
3073
3115
|
else: # yaml
|
|
3074
3116
|
from ruamel.yaml import YAML
|
|
3075
3117
|
|
|
@@ -3080,9 +3122,20 @@ def _output_findings(
|
|
|
3080
3122
|
|
|
3081
3123
|
output = StringIO()
|
|
3082
3124
|
yaml.dump(findings_data, output)
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3125
|
+
formatted_output = output.getvalue()
|
|
3126
|
+
|
|
3127
|
+
if output_path:
|
|
3128
|
+
# Save to file
|
|
3129
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
3130
|
+
output_path.write_text(formatted_output, encoding="utf-8")
|
|
3131
|
+
from rich.console import Console
|
|
3132
|
+
|
|
3133
|
+
console = Console()
|
|
3134
|
+
console.print(f"[green]✓[/green] Findings saved to: {output_path}")
|
|
3135
|
+
else:
|
|
3136
|
+
# Output to stdout
|
|
3137
|
+
sys.stdout.write(formatted_output)
|
|
3138
|
+
sys.stdout.flush()
|
|
3086
3139
|
else:
|
|
3087
3140
|
print_error(f"Invalid findings format: {findings_format}. Must be 'json', 'yaml', or 'table'")
|
|
3088
3141
|
raise typer.Exit(1)
|
|
@@ -3607,6 +3660,13 @@ def _handle_no_questions_case(
|
|
|
3607
3660
|
if category in critical_categories and status == AmbiguityStatus.MISSING:
|
|
3608
3661
|
missing_critical.append(category)
|
|
3609
3662
|
|
|
3663
|
+
# Count total findings per category (shared for both branches)
|
|
3664
|
+
total_findings_by_category: dict[TaxonomyCategory, int] = {}
|
|
3665
|
+
if report.findings:
|
|
3666
|
+
for finding in report.findings:
|
|
3667
|
+
cat = finding.category
|
|
3668
|
+
total_findings_by_category[cat] = total_findings_by_category.get(cat, 0) + 1
|
|
3669
|
+
|
|
3610
3670
|
if missing_critical:
|
|
3611
3671
|
print_warning(
|
|
3612
3672
|
f"Plan has {len(missing_critical)} critical category(ies) marked as Missing, but no high-priority questions remain"
|
|
@@ -3620,7 +3680,27 @@ def _handle_no_questions_case(
|
|
|
3620
3680
|
status_icon = (
|
|
3621
3681
|
"✅" if status == AmbiguityStatus.CLEAR else "⚠️" if status == AmbiguityStatus.PARTIAL else "❌"
|
|
3622
3682
|
)
|
|
3623
|
-
|
|
3683
|
+
total = total_findings_by_category.get(cat, 0)
|
|
3684
|
+
# Count findings by status
|
|
3685
|
+
clear_count = sum(
|
|
3686
|
+
1 for f in (report.findings or []) if f.category == cat and f.status == AmbiguityStatus.CLEAR
|
|
3687
|
+
)
|
|
3688
|
+
partial_count = sum(
|
|
3689
|
+
1 for f in (report.findings or []) if f.category == cat and f.status == AmbiguityStatus.PARTIAL
|
|
3690
|
+
)
|
|
3691
|
+
# Show format based on status:
|
|
3692
|
+
# - Clear: If no findings (total=0), just show status. Otherwise show clear_count/total
|
|
3693
|
+
# - Partial: Show partial_count/total (count of findings with PARTIAL status)
|
|
3694
|
+
if status == AmbiguityStatus.CLEAR:
|
|
3695
|
+
if total == 0:
|
|
3696
|
+
# No findings - just show status without counts
|
|
3697
|
+
console.print(f" {status_icon} {cat.value}: {status.value}")
|
|
3698
|
+
else:
|
|
3699
|
+
console.print(f" {status_icon} {cat.value}: {clear_count}/{total} {status.value}")
|
|
3700
|
+
elif status == AmbiguityStatus.PARTIAL:
|
|
3701
|
+
console.print(f" {status_icon} {cat.value}: {partial_count}/{total} {status.value}")
|
|
3702
|
+
else: # MISSING
|
|
3703
|
+
console.print(f" {status_icon} {cat.value}: {status.value}")
|
|
3624
3704
|
console.print(
|
|
3625
3705
|
"\n[bold]⚠️ Warning:[/bold] Plan may not be ready for promotion due to missing critical categories"
|
|
3626
3706
|
)
|
|
@@ -3633,7 +3713,27 @@ def _handle_no_questions_case(
|
|
|
3633
3713
|
status_icon = (
|
|
3634
3714
|
"✅" if status == AmbiguityStatus.CLEAR else "⚠️" if status == AmbiguityStatus.PARTIAL else "❌"
|
|
3635
3715
|
)
|
|
3636
|
-
|
|
3716
|
+
total = total_findings_by_category.get(cat, 0)
|
|
3717
|
+
# Count findings by status
|
|
3718
|
+
clear_count = sum(
|
|
3719
|
+
1 for f in (report.findings or []) if f.category == cat and f.status == AmbiguityStatus.CLEAR
|
|
3720
|
+
)
|
|
3721
|
+
partial_count = sum(
|
|
3722
|
+
1 for f in (report.findings or []) if f.category == cat and f.status == AmbiguityStatus.PARTIAL
|
|
3723
|
+
)
|
|
3724
|
+
# Show format based on status:
|
|
3725
|
+
# - Clear: If no findings (total=0), just show status. Otherwise show clear_count/total
|
|
3726
|
+
# - Partial: Show partial_count/total (count of findings with PARTIAL status)
|
|
3727
|
+
if status == AmbiguityStatus.CLEAR:
|
|
3728
|
+
if total == 0:
|
|
3729
|
+
# No findings - just show status without counts
|
|
3730
|
+
console.print(f" {status_icon} {cat.value}: {status.value}")
|
|
3731
|
+
else:
|
|
3732
|
+
console.print(f" {status_icon} {cat.value}: {clear_count}/{total} {status.value}")
|
|
3733
|
+
elif status == AmbiguityStatus.PARTIAL:
|
|
3734
|
+
console.print(f" {status_icon} {cat.value}: {partial_count}/{total} {status.value}")
|
|
3735
|
+
else: # MISSING
|
|
3736
|
+
console.print(f" {status_icon} {cat.value}: {status.value}")
|
|
3637
3737
|
|
|
3638
3738
|
return
|
|
3639
3739
|
|
|
@@ -3664,14 +3764,15 @@ def _handle_list_questions_mode(questions_to_ask: list[tuple[Any, str]], output_
|
|
|
3664
3764
|
"related_sections": finding.related_sections or [],
|
|
3665
3765
|
}
|
|
3666
3766
|
)
|
|
3667
|
-
|
|
3767
|
+
|
|
3668
3768
|
json_output = json.dumps({"questions": questions_json, "total": len(questions_json)}, indent=2)
|
|
3669
|
-
|
|
3769
|
+
|
|
3670
3770
|
if output_path:
|
|
3671
3771
|
# Save to file
|
|
3672
3772
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
3673
3773
|
output_path.write_text(json_output + "\n", encoding="utf-8")
|
|
3674
3774
|
from rich.console import Console
|
|
3775
|
+
|
|
3675
3776
|
console = Console()
|
|
3676
3777
|
console.print(f"[green]✓[/green] Questions saved to: {output_path}")
|
|
3677
3778
|
else:
|
|
@@ -3927,11 +4028,76 @@ def _display_review_summary(
|
|
|
3927
4028
|
# Coverage summary (updated after questions)
|
|
3928
4029
|
console.print("\n[bold]Updated Coverage Summary:[/bold]")
|
|
3929
4030
|
if updated_report.coverage:
|
|
4031
|
+
from specfact_cli.analyzers.ambiguity_scanner import TaxonomyCategory
|
|
4032
|
+
|
|
4033
|
+
# Count findings that can still generate questions (unclear findings)
|
|
4034
|
+
# Use the same logic as _scan_and_prepare_questions to count unclear findings
|
|
4035
|
+
existing_question_ids = set()
|
|
4036
|
+
if plan_bundle.clarifications:
|
|
4037
|
+
for session in plan_bundle.clarifications.sessions:
|
|
4038
|
+
for q in session.questions:
|
|
4039
|
+
existing_question_ids.add(q.id)
|
|
4040
|
+
|
|
4041
|
+
# Prioritize findings by (Impact x Uncertainty) - same as _scan_and_prepare_questions
|
|
4042
|
+
findings_list = updated_report.findings or []
|
|
4043
|
+
prioritized_findings = sorted(
|
|
4044
|
+
findings_list,
|
|
4045
|
+
key=lambda f: f.impact * f.uncertainty,
|
|
4046
|
+
reverse=True,
|
|
4047
|
+
)
|
|
4048
|
+
|
|
4049
|
+
# Count total findings and unclear findings per category
|
|
4050
|
+
# A finding is unclear if it can still generate a question (same logic as _scan_and_prepare_questions)
|
|
4051
|
+
total_findings_by_category: dict[TaxonomyCategory, int] = {}
|
|
4052
|
+
unclear_findings_by_category: dict[TaxonomyCategory, int] = {}
|
|
4053
|
+
clear_findings_by_category: dict[TaxonomyCategory, int] = {}
|
|
4054
|
+
|
|
4055
|
+
question_counter = 1
|
|
4056
|
+
for finding in prioritized_findings:
|
|
4057
|
+
cat = finding.category
|
|
4058
|
+
total_findings_by_category[cat] = total_findings_by_category.get(cat, 0) + 1
|
|
4059
|
+
|
|
4060
|
+
# Count by finding status
|
|
4061
|
+
if finding.status == AmbiguityStatus.CLEAR:
|
|
4062
|
+
clear_findings_by_category[cat] = clear_findings_by_category.get(cat, 0) + 1
|
|
4063
|
+
elif finding.status == AmbiguityStatus.PARTIAL:
|
|
4064
|
+
# A finding is unclear if it can generate a question (same logic as _scan_and_prepare_questions)
|
|
4065
|
+
if finding.question:
|
|
4066
|
+
# Skip to next available question ID if current one is already used
|
|
4067
|
+
while f"Q{question_counter:03d}" in existing_question_ids:
|
|
4068
|
+
question_counter += 1
|
|
4069
|
+
# This finding can generate a question, so it's unclear
|
|
4070
|
+
unclear_findings_by_category[cat] = unclear_findings_by_category.get(cat, 0) + 1
|
|
4071
|
+
question_counter += 1
|
|
4072
|
+
else:
|
|
4073
|
+
# Finding has no question, so it's unclear
|
|
4074
|
+
unclear_findings_by_category[cat] = unclear_findings_by_category.get(cat, 0) + 1
|
|
4075
|
+
|
|
3930
4076
|
for cat, status in updated_report.coverage.items():
|
|
3931
4077
|
status_icon = (
|
|
3932
4078
|
"✅" if status == AmbiguityStatus.CLEAR else "⚠️" if status == AmbiguityStatus.PARTIAL else "❌"
|
|
3933
4079
|
)
|
|
3934
|
-
|
|
4080
|
+
total = total_findings_by_category.get(cat, 0)
|
|
4081
|
+
unclear = unclear_findings_by_category.get(cat, 0)
|
|
4082
|
+
clear_count = clear_findings_by_category.get(cat, 0)
|
|
4083
|
+
# Show format based on status:
|
|
4084
|
+
# - Clear: If no findings (total=0), just show status. Otherwise show clear_count/total
|
|
4085
|
+
# - Partial: Show unclear_count/total (how many findings are still unclear)
|
|
4086
|
+
if status == AmbiguityStatus.CLEAR:
|
|
4087
|
+
if total == 0:
|
|
4088
|
+
# No findings - just show status without counts
|
|
4089
|
+
console.print(f" {status_icon} {cat.value}: {status.value}")
|
|
4090
|
+
else:
|
|
4091
|
+
console.print(f" {status_icon} {cat.value}: {clear_count}/{total} {status.value}")
|
|
4092
|
+
elif status == AmbiguityStatus.PARTIAL:
|
|
4093
|
+
# Show how many findings are still unclear
|
|
4094
|
+
# If all are unclear, just show the count without the fraction
|
|
4095
|
+
if unclear == total:
|
|
4096
|
+
console.print(f" {status_icon} {cat.value}: {unclear} {status.value}")
|
|
4097
|
+
else:
|
|
4098
|
+
console.print(f" {status_icon} {cat.value}: {unclear}/{total} {status.value}")
|
|
4099
|
+
else: # MISSING
|
|
4100
|
+
console.print(f" {status_icon} {cat.value}: {status.value}")
|
|
3935
4101
|
|
|
3936
4102
|
# Next steps
|
|
3937
4103
|
console.print("\n[bold]Next Steps:[/bold]")
|
|
@@ -3987,6 +4153,11 @@ def review(
|
|
|
3987
4153
|
case_sensitive=False,
|
|
3988
4154
|
hidden=True, # Hidden by default, shown with --help-advanced
|
|
3989
4155
|
),
|
|
4156
|
+
output_findings: Path | None = typer.Option(
|
|
4157
|
+
None,
|
|
4158
|
+
"--output-findings",
|
|
4159
|
+
help="Save findings to file (JSON/YAML format). If --list-findings is also set, findings are saved to file instead of stdout. Default: None",
|
|
4160
|
+
),
|
|
3990
4161
|
# Behavior/Options
|
|
3991
4162
|
no_interactive: bool = typer.Option(
|
|
3992
4163
|
False,
|
|
@@ -4031,7 +4202,9 @@ def review(
|
|
|
4031
4202
|
specfact plan review legacy-api
|
|
4032
4203
|
specfact plan review auth-module --max-questions 3 --category "Functional Scope"
|
|
4033
4204
|
specfact plan review legacy-api --list-questions # Output questions as JSON
|
|
4205
|
+
specfact plan review legacy-api --list-questions --output-questions /tmp/questions.json # Save questions to file
|
|
4034
4206
|
specfact plan review legacy-api --list-findings --findings-format json # Output all findings as JSON
|
|
4207
|
+
specfact plan review legacy-api --list-findings --output-findings /tmp/findings.json # Save findings to file
|
|
4035
4208
|
specfact plan review legacy-api --answers '{"Q001": "answer1", "Q002": "answer2"}' # Non-interactive
|
|
4036
4209
|
"""
|
|
4037
4210
|
from rich.console import Console
|
|
@@ -4095,7 +4268,7 @@ def review(
|
|
|
4095
4268
|
|
|
4096
4269
|
# Handle --list-findings mode
|
|
4097
4270
|
if list_findings:
|
|
4098
|
-
_output_findings(report, findings_format, is_non_interactive)
|
|
4271
|
+
_output_findings(report, findings_format, is_non_interactive, output_findings)
|
|
4099
4272
|
raise typer.Exit(0)
|
|
4100
4273
|
|
|
4101
4274
|
# Show initial coverage summary BEFORE questions (so user knows what's missing)
|
|
@@ -4104,6 +4277,51 @@ def review(
|
|
|
4104
4277
|
|
|
4105
4278
|
console.print("\n[bold]Initial Coverage Summary:[/bold]")
|
|
4106
4279
|
if report.coverage:
|
|
4280
|
+
from specfact_cli.analyzers.ambiguity_scanner import TaxonomyCategory
|
|
4281
|
+
|
|
4282
|
+
# Count findings that can still generate questions (unclear findings)
|
|
4283
|
+
# Use the same logic as _scan_and_prepare_questions to count unclear findings
|
|
4284
|
+
existing_question_ids = set()
|
|
4285
|
+
if plan_bundle.clarifications:
|
|
4286
|
+
for session in plan_bundle.clarifications.sessions:
|
|
4287
|
+
for q in session.questions:
|
|
4288
|
+
existing_question_ids.add(q.id)
|
|
4289
|
+
|
|
4290
|
+
# Prioritize findings by (Impact x Uncertainty) - same as _scan_and_prepare_questions
|
|
4291
|
+
findings_list = report.findings or []
|
|
4292
|
+
prioritized_findings = sorted(
|
|
4293
|
+
findings_list,
|
|
4294
|
+
key=lambda f: f.impact * f.uncertainty,
|
|
4295
|
+
reverse=True,
|
|
4296
|
+
)
|
|
4297
|
+
|
|
4298
|
+
# Count total findings and unclear findings per category
|
|
4299
|
+
# A finding is unclear if it can still generate a question (same logic as _scan_and_prepare_questions)
|
|
4300
|
+
total_findings_by_category: dict[TaxonomyCategory, int] = {}
|
|
4301
|
+
unclear_findings_by_category: dict[TaxonomyCategory, int] = {}
|
|
4302
|
+
clear_findings_by_category: dict[TaxonomyCategory, int] = {}
|
|
4303
|
+
|
|
4304
|
+
question_counter = 1
|
|
4305
|
+
for finding in prioritized_findings:
|
|
4306
|
+
cat = finding.category
|
|
4307
|
+
total_findings_by_category[cat] = total_findings_by_category.get(cat, 0) + 1
|
|
4308
|
+
|
|
4309
|
+
# Count by finding status
|
|
4310
|
+
if finding.status == AmbiguityStatus.CLEAR:
|
|
4311
|
+
clear_findings_by_category[cat] = clear_findings_by_category.get(cat, 0) + 1
|
|
4312
|
+
elif finding.status == AmbiguityStatus.PARTIAL:
|
|
4313
|
+
# A finding is unclear if it can generate a question (same logic as _scan_and_prepare_questions)
|
|
4314
|
+
if finding.question:
|
|
4315
|
+
# Skip to next available question ID if current one is already used
|
|
4316
|
+
while f"Q{question_counter:03d}" in existing_question_ids:
|
|
4317
|
+
question_counter += 1
|
|
4318
|
+
# This finding can generate a question, so it's unclear
|
|
4319
|
+
unclear_findings_by_category[cat] = unclear_findings_by_category.get(cat, 0) + 1
|
|
4320
|
+
question_counter += 1
|
|
4321
|
+
else:
|
|
4322
|
+
# Finding has no question, so it's unclear
|
|
4323
|
+
unclear_findings_by_category[cat] = unclear_findings_by_category.get(cat, 0) + 1
|
|
4324
|
+
|
|
4107
4325
|
for cat, status in report.coverage.items():
|
|
4108
4326
|
status_icon = (
|
|
4109
4327
|
"✅"
|
|
@@ -4112,7 +4330,27 @@ def review(
|
|
|
4112
4330
|
if status == AmbiguityStatus.PARTIAL
|
|
4113
4331
|
else "❌"
|
|
4114
4332
|
)
|
|
4115
|
-
|
|
4333
|
+
total = total_findings_by_category.get(cat, 0)
|
|
4334
|
+
unclear = unclear_findings_by_category.get(cat, 0)
|
|
4335
|
+
clear_count = clear_findings_by_category.get(cat, 0)
|
|
4336
|
+
# Show format based on status:
|
|
4337
|
+
# - Clear: If no findings (total=0), just show status. Otherwise show clear_count/total
|
|
4338
|
+
# - Partial: Show unclear_count/total (how many findings are still unclear)
|
|
4339
|
+
if status == AmbiguityStatus.CLEAR:
|
|
4340
|
+
if total == 0:
|
|
4341
|
+
# No findings - just show status without counts
|
|
4342
|
+
console.print(f" {status_icon} {cat.value}: {status.value}")
|
|
4343
|
+
else:
|
|
4344
|
+
console.print(f" {status_icon} {cat.value}: {clear_count}/{total} {status.value}")
|
|
4345
|
+
elif status == AmbiguityStatus.PARTIAL:
|
|
4346
|
+
# Show how many findings are still unclear
|
|
4347
|
+
# If all are unclear, just show the count without the fraction
|
|
4348
|
+
if unclear == total:
|
|
4349
|
+
console.print(f" {status_icon} {cat.value}: {unclear} {status.value}")
|
|
4350
|
+
else:
|
|
4351
|
+
console.print(f" {status_icon} {cat.value}: {unclear}/{total} {status.value}")
|
|
4352
|
+
else: # MISSING
|
|
4353
|
+
console.print(f" {status_icon} {cat.value}: {status.value}")
|
|
4116
4354
|
console.print(f"\n[dim]Found {len(questions_to_ask)} question(s) to resolve[/dim]\n")
|
|
4117
4355
|
|
|
4118
4356
|
# Handle --list-questions mode (must be before no-questions check)
|
|
@@ -4724,52 +4962,51 @@ def _extract_sdd_how(bundle: PlanBundle, is_non_interactive: bool, fallback: SDD
|
|
|
4724
4962
|
def _extract_specific_criteria_from_answer(answer: str) -> list[str]:
|
|
4725
4963
|
"""
|
|
4726
4964
|
Extract specific testable criteria from answer that contains replacement instructions.
|
|
4727
|
-
|
|
4965
|
+
|
|
4728
4966
|
When answer contains "Replace generic 'works correctly' with testable criteria:",
|
|
4729
4967
|
extracts the specific criteria (items in single quotes) and returns them as a list.
|
|
4730
|
-
|
|
4968
|
+
|
|
4731
4969
|
Args:
|
|
4732
4970
|
answer: Answer text that may contain replacement instructions
|
|
4733
|
-
|
|
4971
|
+
|
|
4734
4972
|
Returns:
|
|
4735
4973
|
List of specific criteria strings, or empty list if no extraction possible
|
|
4736
4974
|
"""
|
|
4737
4975
|
import re
|
|
4738
|
-
|
|
4976
|
+
|
|
4739
4977
|
# Check if answer contains replacement instructions
|
|
4740
4978
|
if "testable criteria:" not in answer.lower() and "replace generic" not in answer.lower():
|
|
4741
4979
|
# Answer doesn't contain replacement format, return as single item
|
|
4742
4980
|
return [answer] if answer.strip() else []
|
|
4743
|
-
|
|
4981
|
+
|
|
4744
4982
|
# Find the position after "testable criteria:" to only extract criteria from that point
|
|
4745
4983
|
# This avoids extracting "works correctly" from the instruction text itself
|
|
4746
4984
|
testable_criteria_marker = "testable criteria:"
|
|
4747
4985
|
marker_pos = answer.lower().find(testable_criteria_marker)
|
|
4748
|
-
|
|
4986
|
+
|
|
4749
4987
|
if marker_pos == -1:
|
|
4750
4988
|
# Fallback: try "with testable criteria:"
|
|
4751
4989
|
marker_pos = answer.lower().find("with testable criteria:")
|
|
4752
4990
|
if marker_pos != -1:
|
|
4753
4991
|
marker_pos += len("with testable criteria:")
|
|
4754
|
-
|
|
4992
|
+
|
|
4755
4993
|
if marker_pos != -1:
|
|
4756
4994
|
# Only search for criteria after the marker
|
|
4757
|
-
criteria_section = answer[marker_pos + len(testable_criteria_marker):]
|
|
4995
|
+
criteria_section = answer[marker_pos + len(testable_criteria_marker) :]
|
|
4758
4996
|
# Extract criteria (items in single quotes)
|
|
4759
4997
|
criteria_pattern = r"'([^']+)'"
|
|
4760
4998
|
matches = re.findall(criteria_pattern, criteria_section)
|
|
4761
|
-
|
|
4999
|
+
|
|
4762
5000
|
if matches:
|
|
4763
5001
|
# Filter out "works correctly" if it appears (it's part of instruction, not a criterion)
|
|
4764
5002
|
filtered = [
|
|
4765
5003
|
criterion.strip()
|
|
4766
5004
|
for criterion in matches
|
|
4767
|
-
if criterion.strip()
|
|
4768
|
-
and criterion.strip().lower() not in ("works correctly", "works as expected")
|
|
5005
|
+
if criterion.strip() and criterion.strip().lower() not in ("works correctly", "works as expected")
|
|
4769
5006
|
]
|
|
4770
5007
|
if filtered:
|
|
4771
5008
|
return filtered
|
|
4772
|
-
|
|
5009
|
+
|
|
4773
5010
|
# Fallback: if no quoted criteria found, return original answer
|
|
4774
5011
|
return [answer] if answer.strip() else []
|
|
4775
5012
|
|
|
@@ -4784,11 +5021,11 @@ def _identify_vague_criteria_to_remove(
|
|
|
4784
5021
|
) -> list[str]:
|
|
4785
5022
|
"""
|
|
4786
5023
|
Identify vague acceptance criteria that should be removed when replacing with specific criteria.
|
|
4787
|
-
|
|
5024
|
+
|
|
4788
5025
|
Args:
|
|
4789
5026
|
acceptance_list: Current list of acceptance criteria
|
|
4790
5027
|
finding: Ambiguity finding that triggered the question
|
|
4791
|
-
|
|
5028
|
+
|
|
4792
5029
|
Returns:
|
|
4793
5030
|
List of vague criteria strings to remove
|
|
4794
5031
|
"""
|
|
@@ -4796,9 +5033,9 @@ def _identify_vague_criteria_to_remove(
|
|
|
4796
5033
|
is_code_specific_criteria,
|
|
4797
5034
|
is_simplified_format_criteria,
|
|
4798
5035
|
)
|
|
4799
|
-
|
|
5036
|
+
|
|
4800
5037
|
vague_to_remove: list[str] = []
|
|
4801
|
-
|
|
5038
|
+
|
|
4802
5039
|
# Patterns that indicate vague criteria (from ambiguity scanner)
|
|
4803
5040
|
vague_patterns = [
|
|
4804
5041
|
"is implemented",
|
|
@@ -4808,22 +5045,18 @@ def _identify_vague_criteria_to_remove(
|
|
|
4808
5045
|
"is complete",
|
|
4809
5046
|
"is ready",
|
|
4810
5047
|
]
|
|
4811
|
-
|
|
4812
|
-
# Also check for criteria that match the finding description
|
|
4813
|
-
# (e.g., criteria containing the same vague suggestions mentioned in finding)
|
|
4814
|
-
finding_description_lower = finding.description.lower() if finding.description else ""
|
|
4815
|
-
|
|
5048
|
+
|
|
4816
5049
|
for acc in acceptance_list:
|
|
4817
5050
|
acc_lower = acc.lower()
|
|
4818
|
-
|
|
5051
|
+
|
|
4819
5052
|
# Skip code-specific criteria (should not be removed)
|
|
4820
5053
|
if is_code_specific_criteria(acc):
|
|
4821
5054
|
continue
|
|
4822
|
-
|
|
5055
|
+
|
|
4823
5056
|
# Skip simplified format criteria (valid format)
|
|
4824
5057
|
if is_simplified_format_criteria(acc):
|
|
4825
5058
|
continue
|
|
4826
|
-
|
|
5059
|
+
|
|
4827
5060
|
# ALWAYS remove replacement instruction text (from previous answers)
|
|
4828
5061
|
# These are meta-instructions, not actual acceptance criteria
|
|
4829
5062
|
contains_replacement_instruction = (
|
|
@@ -4831,11 +5064,11 @@ def _identify_vague_criteria_to_remove(
|
|
|
4831
5064
|
or ("should be more specific" in acc_lower and "testable criteria:" in acc_lower)
|
|
4832
5065
|
or ("yes, these should be more specific" in acc_lower)
|
|
4833
5066
|
)
|
|
4834
|
-
|
|
5067
|
+
|
|
4835
5068
|
if contains_replacement_instruction:
|
|
4836
5069
|
vague_to_remove.append(acc)
|
|
4837
5070
|
continue
|
|
4838
|
-
|
|
5071
|
+
|
|
4839
5072
|
# Check for vague patterns (but be more selective)
|
|
4840
5073
|
# Only flag as vague if it contains "works correctly" without "see contract examples"
|
|
4841
5074
|
# or other vague patterns in a standalone context
|
|
@@ -4847,14 +5080,13 @@ def _identify_vague_criteria_to_remove(
|
|
|
4847
5080
|
else:
|
|
4848
5081
|
# Check other vague patterns
|
|
4849
5082
|
is_vague = any(
|
|
4850
|
-
pattern in acc_lower
|
|
4851
|
-
and len(acc.split()) < 10 # Only flag short, vague statements
|
|
5083
|
+
pattern in acc_lower and len(acc.split()) < 10 # Only flag short, vague statements
|
|
4852
5084
|
for pattern in vague_patterns
|
|
4853
5085
|
)
|
|
4854
|
-
|
|
5086
|
+
|
|
4855
5087
|
if is_vague:
|
|
4856
5088
|
vague_to_remove.append(acc)
|
|
4857
|
-
|
|
5089
|
+
|
|
4858
5090
|
return vague_to_remove
|
|
4859
5091
|
|
|
4860
5092
|
|
|
@@ -4935,8 +5167,12 @@ def _integrate_clarification(
|
|
|
4935
5167
|
bundle.idea.constraints.append(answer)
|
|
4936
5168
|
integration_points.append(section)
|
|
4937
5169
|
|
|
4938
|
-
# Edge Cases, Completion Signals → features[].acceptance, stories[].acceptance
|
|
4939
|
-
elif category in (
|
|
5170
|
+
# Edge Cases, Completion Signals, Interaction & UX Flow → features[].acceptance, stories[].acceptance
|
|
5171
|
+
elif category in (
|
|
5172
|
+
TaxonomyCategory.EDGE_CASES,
|
|
5173
|
+
TaxonomyCategory.COMPLETION_SIGNALS,
|
|
5174
|
+
TaxonomyCategory.INTERACTION_UX,
|
|
5175
|
+
):
|
|
4940
5176
|
related_sections = finding.related_sections or []
|
|
4941
5177
|
for section in related_sections:
|
|
4942
5178
|
if section.startswith("features."):
|
|
@@ -4971,7 +5207,9 @@ def _integrate_clarification(
|
|
|
4971
5207
|
# Extract specific criteria from answer
|
|
4972
5208
|
specific_criteria = _extract_specific_criteria_from_answer(answer)
|
|
4973
5209
|
# Identify and remove vague criteria
|
|
4974
|
-
vague_to_remove = _identify_vague_criteria_to_remove(
|
|
5210
|
+
vague_to_remove = _identify_vague_criteria_to_remove(
|
|
5211
|
+
story.acceptance, finding
|
|
5212
|
+
)
|
|
4975
5213
|
# Remove vague criteria
|
|
4976
5214
|
for vague in vague_to_remove:
|
|
4977
5215
|
if vague in story.acceptance:
|