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.
Files changed (150) hide show
  1. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/PKG-INFO +1 -1
  2. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/pyproject.toml +1 -1
  3. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/prompts/specfact.03-review.md +9 -2
  4. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/__init__.py +1 -1
  5. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/__init__.py +1 -1
  6. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/commands/plan.py +283 -45
  7. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/acceptance_criteria.py +7 -11
  8. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/.gitignore +0 -0
  9. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/LICENSE.md +0 -0
  10. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/README.md +0 -0
  11. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/mappings/node-async.yaml +0 -0
  12. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/mappings/python-async.yaml +0 -0
  13. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/mappings/speckit-default.yaml +0 -0
  14. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/prompts/shared/cli-enforcement.md +0 -0
  15. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/prompts/specfact.01-import.md +0 -0
  16. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/prompts/specfact.02-plan.md +0 -0
  17. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/prompts/specfact.04-sdd.md +0 -0
  18. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/prompts/specfact.05-enforce.md +0 -0
  19. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/prompts/specfact.06-sync.md +0 -0
  20. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/prompts/specfact.07-contracts.md +0 -0
  21. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/prompts/specfact.compare.md +0 -0
  22. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/prompts/specfact.validate.md +0 -0
  23. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/schemas/deviation.schema.json +0 -0
  24. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/schemas/plan.schema.json +0 -0
  25. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/schemas/protocol.schema.json +0 -0
  26. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/templates/github-action.yml.j2 +0 -0
  27. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/templates/plan.bundle.yaml.j2 +0 -0
  28. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/templates/pr-template.md.j2 +0 -0
  29. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/templates/protocol.yaml.j2 +0 -0
  30. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/resources/templates/telemetry.yaml.example +0 -0
  31. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/agents/__init__.py +0 -0
  32. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/agents/analyze_agent.py +0 -0
  33. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/agents/base.py +0 -0
  34. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/agents/plan_agent.py +0 -0
  35. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/agents/registry.py +0 -0
  36. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/agents/sync_agent.py +0 -0
  37. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/analyzers/__init__.py +0 -0
  38. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/analyzers/ambiguity_scanner.py +0 -0
  39. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/analyzers/code_analyzer.py +0 -0
  40. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/analyzers/constitution_evidence_extractor.py +0 -0
  41. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/analyzers/contract_extractor.py +0 -0
  42. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/analyzers/control_flow_analyzer.py +0 -0
  43. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/analyzers/graph_analyzer.py +0 -0
  44. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/analyzers/relationship_mapper.py +0 -0
  45. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/analyzers/requirement_extractor.py +0 -0
  46. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/analyzers/test_pattern_extractor.py +0 -0
  47. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/cli.py +0 -0
  48. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/commands/__init__.py +0 -0
  49. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/commands/analyze.py +0 -0
  50. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/commands/bridge.py +0 -0
  51. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/commands/drift.py +0 -0
  52. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/commands/enforce.py +0 -0
  53. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/commands/generate.py +0 -0
  54. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/commands/implement.py +0 -0
  55. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/commands/import_cmd.py +0 -0
  56. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/commands/init.py +0 -0
  57. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/commands/migrate.py +0 -0
  58. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/commands/repro.py +0 -0
  59. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/commands/run.py +0 -0
  60. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/commands/sdd.py +0 -0
  61. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/commands/spec.py +0 -0
  62. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/commands/sync.py +0 -0
  63. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/common/__init__.py +0 -0
  64. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/common/logger_setup.py +0 -0
  65. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/common/logging_utils.py +0 -0
  66. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/common/text_utils.py +0 -0
  67. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/common/utils.py +0 -0
  68. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/comparators/__init__.py +0 -0
  69. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/comparators/plan_comparator.py +0 -0
  70. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/enrichers/constitution_enricher.py +0 -0
  71. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/enrichers/plan_enricher.py +0 -0
  72. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/generators/__init__.py +0 -0
  73. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/generators/contract_generator.py +0 -0
  74. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/generators/openapi_extractor.py +0 -0
  75. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/generators/plan_generator.py +0 -0
  76. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/generators/protocol_generator.py +0 -0
  77. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/generators/report_generator.py +0 -0
  78. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/generators/task_generator.py +0 -0
  79. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/generators/test_to_openapi.py +0 -0
  80. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/generators/workflow_generator.py +0 -0
  81. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/importers/__init__.py +0 -0
  82. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/importers/speckit_converter.py +0 -0
  83. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/importers/speckit_scanner.py +0 -0
  84. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/integrations/__init__.py +0 -0
  85. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/integrations/specmatic.py +0 -0
  86. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/migrations/__init__.py +0 -0
  87. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/migrations/plan_migrator.py +0 -0
  88. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/models/__init__.py +0 -0
  89. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/models/bridge.py +0 -0
  90. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/models/deviation.py +0 -0
  91. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/models/enforcement.py +0 -0
  92. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/models/plan.py +0 -0
  93. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/models/project.py +0 -0
  94. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/models/protocol.py +0 -0
  95. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/models/quality.py +0 -0
  96. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/models/sdd.py +0 -0
  97. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/models/source_tracking.py +0 -0
  98. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/models/task.py +0 -0
  99. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/modes/__init__.py +0 -0
  100. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/modes/detector.py +0 -0
  101. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/modes/router.py +0 -0
  102. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/resources/semgrep/async.yml +0 -0
  103. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/resources/semgrep/code-quality.yml +0 -0
  104. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/resources/semgrep/feature-detection.yml +0 -0
  105. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/runtime.py +0 -0
  106. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/sync/__init__.py +0 -0
  107. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/sync/bridge_probe.py +0 -0
  108. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/sync/bridge_sync.py +0 -0
  109. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/sync/bridge_watch.py +0 -0
  110. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/sync/change_detector.py +0 -0
  111. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/sync/code_to_spec.py +0 -0
  112. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/sync/drift_detector.py +0 -0
  113. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/sync/repository_sync.py +0 -0
  114. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/sync/spec_to_code.py +0 -0
  115. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/sync/spec_to_tests.py +0 -0
  116. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/sync/speckit_sync.py +0 -0
  117. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/sync/watcher.py +0 -0
  118. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/sync/watcher_enhanced.py +0 -0
  119. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/telemetry.py +0 -0
  120. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/templates/__init__.py +0 -0
  121. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/templates/bridge_templates.py +0 -0
  122. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/templates/specification_templates.py +0 -0
  123. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/__init__.py +0 -0
  124. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/bundle_loader.py +0 -0
  125. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/console.py +0 -0
  126. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/context_detection.py +0 -0
  127. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/enrichment_context.py +0 -0
  128. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/enrichment_parser.py +0 -0
  129. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/feature_keys.py +0 -0
  130. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/git.py +0 -0
  131. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/github_annotations.py +0 -0
  132. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/ide_setup.py +0 -0
  133. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/incremental_check.py +0 -0
  134. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/optional_deps.py +0 -0
  135. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/performance.py +0 -0
  136. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/progress.py +0 -0
  137. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/progressive_disclosure.py +0 -0
  138. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/prompts.py +0 -0
  139. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/sdd_discovery.py +0 -0
  140. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/source_scanner.py +0 -0
  141. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/structure.py +0 -0
  142. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/structured_io.py +0 -0
  143. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/suggestions.py +0 -0
  144. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/utils/yaml_utils.py +0 -0
  145. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/validators/__init__.py +0 -0
  146. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/validators/cli_first_validator.py +0 -0
  147. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/validators/contract_validator.py +0 -0
  148. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/validators/fsm.py +0 -0
  149. {specfact_cli-0.15.2 → specfact_cli-0.15.5}/src/specfact_cli/validators/repro_checker.py +0 -0
  150. {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.2
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.2"
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
- specfact plan review [<bundle-name>] --list-findings --findings-format json --no-interactive > /tmp/findings.json
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-format json --no-interactive > /tmp/findings.json
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):
@@ -3,4 +3,4 @@ SpecFact CLI - Spec→Contract→Sentinel tool for contract-driven development.
3
3
  """
4
4
 
5
5
  # Define the package version (kept in sync with pyproject.toml and setup.py)
6
- __version__ = "0.14.2"
6
+ __version__ = "0.15.4"
@@ -9,6 +9,6 @@ This package provides command-line tools for:
9
9
  - Validating reproducibility
10
10
  """
11
11
 
12
- __version__ = "0.15.2"
12
+ __version__ = "0.15.5"
13
13
 
14
14
  __all__ = ["__version__"]
@@ -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
- console.print(f" {status_icon} {cat.value}: {status.value}")
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
- sys.stdout.write(json.dumps(findings_data, indent=2))
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
- sys.stdout.write(output.getvalue())
3084
- sys.stdout.write("\n")
3085
- sys.stdout.flush()
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
- console.print(f" {status_icon} {cat.value}: {status.value}")
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
- console.print(f" {status_icon} {cat.value}: {status.value}")
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
- console.print(f" {status_icon} {cat.value}: {status.value}")
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
- console.print(f" {status_icon} {cat.value}: {status.value}")
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 (TaxonomyCategory.EDGE_CASES, TaxonomyCategory.COMPLETION_SIGNALS):
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(story.acceptance, finding)
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: