specfact-cli 0.11.4__tar.gz → 0.12.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/PKG-INFO +1 -1
  2. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/pyproject.toml +1 -1
  3. specfact_cli-0.12.1/resources/prompts/specfact.03-review.md +220 -0
  4. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/__init__.py +1 -1
  5. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/__init__.py +1 -1
  6. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/analyzers/code_analyzer.py +74 -52
  7. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/analyzers/graph_analyzer.py +22 -7
  8. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/analyzers/relationship_mapper.py +8 -2
  9. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/commands/import_cmd.py +169 -111
  10. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/commands/plan.py +8 -2
  11. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/generators/openapi_extractor.py +203 -24
  12. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/generators/test_to_openapi.py +8 -1
  13. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/models/project.py +16 -4
  14. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/utils/incremental_check.py +16 -4
  15. specfact_cli-0.12.1/src/specfact_cli/utils/progress.py +182 -0
  16. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/utils/source_scanner.py +8 -2
  17. specfact_cli-0.11.4/resources/prompts/specfact.03-review.md +0 -112
  18. specfact_cli-0.11.4/src/specfact_cli/utils/progress.py +0 -126
  19. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/.gitignore +0 -0
  20. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/LICENSE.md +0 -0
  21. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/README.md +0 -0
  22. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/resources/mappings/node-async.yaml +0 -0
  23. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/resources/mappings/python-async.yaml +0 -0
  24. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/resources/mappings/speckit-default.yaml +0 -0
  25. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/resources/prompts/shared/cli-enforcement.md +0 -0
  26. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/resources/prompts/specfact.01-import.md +0 -0
  27. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/resources/prompts/specfact.02-plan.md +0 -0
  28. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/resources/prompts/specfact.04-sdd.md +0 -0
  29. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/resources/prompts/specfact.05-enforce.md +0 -0
  30. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/resources/prompts/specfact.06-sync.md +0 -0
  31. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/resources/prompts/specfact.compare.md +0 -0
  32. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/resources/prompts/specfact.validate.md +0 -0
  33. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/resources/schemas/deviation.schema.json +0 -0
  34. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/resources/schemas/plan.schema.json +0 -0
  35. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/resources/schemas/protocol.schema.json +0 -0
  36. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/resources/templates/github-action.yml.j2 +0 -0
  37. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/resources/templates/plan.bundle.yaml.j2 +0 -0
  38. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/resources/templates/pr-template.md.j2 +0 -0
  39. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/resources/templates/protocol.yaml.j2 +0 -0
  40. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/resources/templates/telemetry.yaml.example +0 -0
  41. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/agents/__init__.py +0 -0
  42. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/agents/analyze_agent.py +0 -0
  43. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/agents/base.py +0 -0
  44. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/agents/plan_agent.py +0 -0
  45. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/agents/registry.py +0 -0
  46. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/agents/sync_agent.py +0 -0
  47. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/analyzers/__init__.py +0 -0
  48. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/analyzers/ambiguity_scanner.py +0 -0
  49. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/analyzers/constitution_evidence_extractor.py +0 -0
  50. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/analyzers/contract_extractor.py +0 -0
  51. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/analyzers/control_flow_analyzer.py +0 -0
  52. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/analyzers/requirement_extractor.py +0 -0
  53. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/analyzers/test_pattern_extractor.py +0 -0
  54. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/cli.py +0 -0
  55. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/commands/__init__.py +0 -0
  56. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/commands/analyze.py +0 -0
  57. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/commands/bridge.py +0 -0
  58. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/commands/drift.py +0 -0
  59. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/commands/enforce.py +0 -0
  60. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/commands/generate.py +0 -0
  61. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/commands/implement.py +0 -0
  62. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/commands/init.py +0 -0
  63. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/commands/migrate.py +0 -0
  64. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/commands/repro.py +0 -0
  65. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/commands/run.py +0 -0
  66. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/commands/sdd.py +0 -0
  67. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/commands/spec.py +0 -0
  68. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/commands/sync.py +0 -0
  69. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/common/__init__.py +0 -0
  70. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/common/logger_setup.py +0 -0
  71. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/common/logging_utils.py +0 -0
  72. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/common/text_utils.py +0 -0
  73. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/common/utils.py +0 -0
  74. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/comparators/__init__.py +0 -0
  75. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/comparators/plan_comparator.py +0 -0
  76. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/enrichers/constitution_enricher.py +0 -0
  77. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/enrichers/plan_enricher.py +0 -0
  78. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/generators/__init__.py +0 -0
  79. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/generators/contract_generator.py +0 -0
  80. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/generators/plan_generator.py +0 -0
  81. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/generators/protocol_generator.py +0 -0
  82. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/generators/report_generator.py +0 -0
  83. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/generators/task_generator.py +0 -0
  84. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/generators/workflow_generator.py +0 -0
  85. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/importers/__init__.py +0 -0
  86. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/importers/speckit_converter.py +0 -0
  87. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/importers/speckit_scanner.py +0 -0
  88. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/integrations/__init__.py +0 -0
  89. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/integrations/specmatic.py +0 -0
  90. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/migrations/__init__.py +0 -0
  91. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/migrations/plan_migrator.py +0 -0
  92. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/models/__init__.py +0 -0
  93. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/models/bridge.py +0 -0
  94. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/models/deviation.py +0 -0
  95. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/models/enforcement.py +0 -0
  96. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/models/plan.py +0 -0
  97. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/models/protocol.py +0 -0
  98. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/models/quality.py +0 -0
  99. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/models/sdd.py +0 -0
  100. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/models/source_tracking.py +0 -0
  101. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/models/task.py +0 -0
  102. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/modes/__init__.py +0 -0
  103. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/modes/detector.py +0 -0
  104. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/modes/router.py +0 -0
  105. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/resources/semgrep/async.yml +0 -0
  106. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/resources/semgrep/code-quality.yml +0 -0
  107. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/resources/semgrep/feature-detection.yml +0 -0
  108. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/runtime.py +0 -0
  109. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/sync/__init__.py +0 -0
  110. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/sync/bridge_probe.py +0 -0
  111. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/sync/bridge_sync.py +0 -0
  112. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/sync/bridge_watch.py +0 -0
  113. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/sync/change_detector.py +0 -0
  114. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/sync/code_to_spec.py +0 -0
  115. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/sync/drift_detector.py +0 -0
  116. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/sync/repository_sync.py +0 -0
  117. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/sync/spec_to_code.py +0 -0
  118. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/sync/spec_to_tests.py +0 -0
  119. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/sync/speckit_sync.py +0 -0
  120. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/sync/watcher.py +0 -0
  121. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/telemetry.py +0 -0
  122. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/templates/__init__.py +0 -0
  123. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/templates/bridge_templates.py +0 -0
  124. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/utils/__init__.py +0 -0
  125. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/utils/acceptance_criteria.py +0 -0
  126. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/utils/bundle_loader.py +0 -0
  127. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/utils/console.py +0 -0
  128. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/utils/enrichment_context.py +0 -0
  129. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/utils/enrichment_parser.py +0 -0
  130. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/utils/feature_keys.py +0 -0
  131. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/utils/git.py +0 -0
  132. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/utils/github_annotations.py +0 -0
  133. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/utils/ide_setup.py +0 -0
  134. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/utils/optional_deps.py +0 -0
  135. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/utils/prompts.py +0 -0
  136. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/utils/sdd_discovery.py +0 -0
  137. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/utils/structure.py +0 -0
  138. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/utils/structured_io.py +0 -0
  139. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/utils/yaml_utils.py +0 -0
  140. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/validators/__init__.py +0 -0
  141. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/validators/contract_validator.py +0 -0
  142. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/validators/fsm.py +0 -0
  143. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/src/specfact_cli/validators/repro_checker.py +0 -0
  144. {specfact_cli-0.11.4 → specfact_cli-0.12.1}/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.11.4
3
+ Version: 0.12.1
4
4
  Summary: Brownfield-first CLI: Reverse engineer legacy Python → specs → enforced contracts. Automate legacy code documentation and prevent modernization regressions.
5
5
  Project-URL: Homepage, https://github.com/nold-ai/specfact-cli
6
6
  Project-URL: Repository, https://github.com/nold-ai/specfact-cli.git
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "specfact-cli"
7
- version = "0.11.4"
7
+ version = "0.12.1"
8
8
  description = "Brownfield-first CLI: Reverse engineer legacy Python → specs → enforced contracts. Automate legacy code documentation and prevent modernization regressions."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -0,0 +1,220 @@
1
+ ---
2
+ description: Review project bundle to identify ambiguities, resolve gaps, and prepare for promotion.
3
+ ---
4
+
5
+ # SpecFact Review Command
6
+
7
+ ## User Input
8
+
9
+ ```text
10
+ $ARGUMENTS
11
+ ```
12
+
13
+ You **MUST** consider the user input before proceeding (if not empty).
14
+
15
+ ## Purpose
16
+
17
+ Review project bundle to identify/resolve ambiguities and missing information. Asks targeted questions for promotion readiness.
18
+
19
+ **When to use:** After import/creation, before promotion, when clarification needed.
20
+
21
+ **Quick:** `/specfact.03-review` (uses active plan) or `/specfact.03-review legacy-api`
22
+
23
+ ## Parameters
24
+
25
+ ### Target/Input
26
+
27
+ - `bundle NAME` (optional argument) - Project bundle name (e.g., legacy-api, auth-module). Default: active plan (set via `plan select`)
28
+ - `--category CATEGORY` - Focus on specific taxonomy category. Default: None (all categories)
29
+
30
+ ### Output/Results
31
+
32
+ - `--list-questions` - Output questions in JSON format. Default: False
33
+ - `--list-findings` - Output all findings in structured format. Default: False
34
+ - `--findings-format FORMAT` - Output format: json, yaml, or table. Default: json for non-interactive, table for interactive
35
+
36
+ ### Behavior/Options
37
+
38
+ - `--no-interactive` - Non-interactive mode (for CI/CD). Default: False (interactive mode)
39
+ - `--answers JSON` - JSON object with question_id -> answer mappings. Default: None
40
+ - `--auto-enrich` - Automatically enrich vague acceptance criteria. Default: False
41
+
42
+ ### Advanced/Configuration
43
+
44
+ - `--max-questions INT` - Maximum questions per session. Default: 5 (range: 1-10)
45
+
46
+ ## Workflow
47
+
48
+ ### Step 1: Parse Arguments
49
+
50
+ - Extract bundle name (defaults to active plan if not specified)
51
+ - Extract optional parameters (max-questions, category, etc.)
52
+
53
+ ### Step 2: Execute CLI to Get Findings
54
+
55
+ **First, get findings to understand what needs enrichment:**
56
+
57
+ ```bash
58
+ specfact plan review [<bundle-name>] --list-findings --findings-format json
59
+ # Uses active plan if bundle not specified
60
+ ```
61
+
62
+ This outputs all ambiguities and missing information in structured format.
63
+
64
+ ### Step 3: Create Enrichment Report (if needed)
65
+
66
+ Based on the findings, create a Markdown enrichment report that addresses:
67
+
68
+ - **Business Context**: Priorities, constraints, unknowns
69
+ - **Confidence Adjustments**: Feature confidence score updates (if needed)
70
+ - **Missing Features**: New features to add (if any)
71
+ - **Manual Updates**: Guidance for updating `idea.yaml` fields like `target_users`, `value_hypothesis`, `narrative`
72
+
73
+ **Enrichment Report Format:**
74
+
75
+ ```markdown
76
+ ## Business Context
77
+
78
+ ### Priorities
79
+ - Priority 1
80
+ - Priority 2
81
+
82
+ ### Constraints
83
+ - Constraint 1
84
+ - Constraint 2
85
+
86
+ ### Unknowns
87
+ - Unknown 1
88
+ - Unknown 2
89
+
90
+ ## Confidence Adjustments
91
+
92
+ FEATURE-KEY → 0.95
93
+ FEATURE-OTHER → 0.8
94
+
95
+ ## Missing Features
96
+
97
+ (If any features are missing)
98
+
99
+ ## Recommendations for Manual Updates
100
+
101
+ ### idea.yaml Updates Required
102
+
103
+ **target_users:**
104
+ - Primary: [description]
105
+ - Secondary: [description]
106
+
107
+ **value_hypothesis:**
108
+ [Value proposition]
109
+
110
+ **narrative:**
111
+ [Improved narrative]
112
+ ```
113
+
114
+ ### Step 4: Apply Enrichment
115
+
116
+ #### Option A: Use enrichment to answer review questions
117
+
118
+ Create answers JSON from enrichment report and use with review:
119
+
120
+ ```bash
121
+ specfact plan review [<bundle-name>] --answers '{"Q001": "answer1", "Q002": "answer2"}'
122
+ ```
123
+
124
+ #### Option B: Update idea fields directly via CLI
125
+
126
+ Use `plan update-idea` to update idea fields from enrichment recommendations:
127
+
128
+ ```bash
129
+ specfact plan update-idea --bundle [<bundle-name>] --value-hypothesis "..." --narrative "..." --target-users "..."
130
+ ```
131
+
132
+ #### Option C: Apply enrichment via import (only if bundle needs regeneration)
133
+
134
+ ```bash
135
+ specfact import from-code [<bundle-name>] --repo . --enrichment enrichment-report.md
136
+ ```
137
+
138
+ **Note:**
139
+
140
+ - **Preferred**: Use Option A (answers) or Option B (update-idea) for most cases
141
+ - Only use Option C if you need to regenerate the bundle
142
+ - Never manually edit `.specfact/` files directly - always use CLI commands
143
+
144
+ ### Step 5: Present Results
145
+
146
+ - Display Q&A, sections touched, coverage summary (initial/updated)
147
+ - Note: Clarifications don't affect hash (stable across review sessions)
148
+ - If enrichment report was created, summarize what was addressed
149
+
150
+ ## CLI Enforcement
151
+
152
+ **CRITICAL**: Always use SpecFact CLI commands. See [CLI Enforcement Rules](./shared/cli-enforcement.md) for details.
153
+
154
+ **Rules:** Execute CLI first, use `--no-interactive` in CI/CD, never modify `.specfact/` directly, use CLI output as grounding.
155
+
156
+ ## Expected Output
157
+
158
+ ### Success
159
+
160
+ ```text
161
+ ✓ Review complete: 5 question(s) answered
162
+
163
+ Project Bundle: legacy-api
164
+ Questions Asked: 5
165
+
166
+ Sections Touched:
167
+ • idea.narrative
168
+ • features[FEATURE-001].acceptance
169
+ • features[FEATURE-002].outcomes
170
+
171
+ Coverage Summary:
172
+ ✅ Functional Scope: clear
173
+ ✅ Technical Constraints: clear
174
+ ⚠️ Business Context: partial
175
+ ```
176
+
177
+ ### Error (Missing Bundle)
178
+
179
+ ```text
180
+ ✗ Project bundle 'legacy-api' not found
181
+ Create one with: specfact plan init legacy-api
182
+ ```
183
+
184
+ ## Common Patterns
185
+
186
+ ```bash
187
+ # Get findings first
188
+ /specfact.03-review --list-findings # List all findings
189
+ /specfact.03-review --list-findings --findings-format json # JSON format for enrichment
190
+
191
+ # Interactive review
192
+ /specfact.03-review # Uses active plan
193
+ /specfact.03-review legacy-api # Specific bundle
194
+ /specfact.03-review --max-questions 3 # Limit questions
195
+ /specfact.03-review --category "Functional Scope" # Focus category
196
+
197
+ # Non-interactive with answers
198
+ /specfact.03-review --answers '{"Q001": "answer"}' # Provide answers directly
199
+ /specfact.03-review --list-questions # Output questions as JSON
200
+
201
+ # Auto-enrichment
202
+ /specfact.03-review --auto-enrich # Auto-enrich vague criteria
203
+ ```
204
+
205
+ ## Enrichment Workflow
206
+
207
+ **Typical workflow when enrichment is needed:**
208
+
209
+ 1. **Get findings**: `specfact plan review --list-findings --findings-format json`
210
+ 2. **Analyze findings**: Review missing information (target_users, value_hypothesis, etc.)
211
+ 3. **Create enrichment report**: Write Markdown file addressing findings
212
+ 4. **Apply enrichment**:
213
+ - **Preferred**: Use enrichment to create `--answers` JSON and run `plan review --answers`
214
+ - **Alternative**: Use `plan update-idea` to update idea fields directly
215
+ - **Last resort**: If bundle needs regeneration, use `import from-code --enrichment`
216
+ 5. **Verify**: Run `plan review` again to confirm improvements
217
+
218
+ ## Context
219
+
220
+ {ARGS}
@@ -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.11.4"
6
+ __version__ = "0.12.1"
@@ -9,6 +9,6 @@ This package provides command-line tools for:
9
9
  - Validating reproducibility
10
10
  """
11
11
 
12
- __version__ = "0.11.4"
12
+ __version__ = "0.12.1"
13
13
 
14
14
  __all__ = ["__version__"]
@@ -175,9 +175,13 @@ class CodeAnalyzer:
175
175
  files_to_analyze = [f for f in python_files if not self._should_skip_file(f)]
176
176
 
177
177
  # Process files in parallel
178
- max_workers = max(
179
- 1, min(os.cpu_count() or 4, 8, len(files_to_analyze))
180
- ) # Cap at 8 workers, ensure at least 1
178
+ # In test mode, use fewer workers to avoid resource contention
179
+ if os.environ.get("TEST_MODE") == "true":
180
+ max_workers = max(1, min(2, len(files_to_analyze))) # Max 2 workers in test mode
181
+ else:
182
+ max_workers = max(
183
+ 1, min(os.cpu_count() or 4, 8, len(files_to_analyze))
184
+ ) # Cap at 8 workers, ensure at least 1
181
185
  completed_count = 0
182
186
 
183
187
  def analyze_file_safe(file_path: Path) -> dict[str, Any]:
@@ -185,58 +189,76 @@ class CodeAnalyzer:
185
189
  return self._analyze_file_parallel(file_path)
186
190
 
187
191
  if files_to_analyze:
188
- executor = ThreadPoolExecutor(max_workers=max_workers)
189
- interrupted = False
190
- try:
191
- # Submit all tasks
192
- future_to_file = {executor.submit(analyze_file_safe, f): f for f in files_to_analyze}
193
-
194
- # Collect results as they complete
192
+ # In test mode, use sequential processing to avoid ThreadPoolExecutor deadlocks
193
+ is_test_mode = os.environ.get("TEST_MODE") == "true"
194
+ if is_test_mode:
195
+ # Sequential processing in test mode - avoids ThreadPoolExecutor deadlocks entirely
196
+ for file_path in files_to_analyze:
197
+ try:
198
+ results = analyze_file_safe(file_path)
199
+ self._merge_analysis_results(results)
200
+ completed_count += 1
201
+ progress.update(task3, completed=completed_count)
202
+ except Exception as e:
203
+ console.print(f"[dim]⚠ Warning: Failed to analyze {file_path}: {e}[/dim]")
204
+ completed_count += 1
205
+ progress.update(task3, completed=completed_count)
206
+ else:
207
+ executor = ThreadPoolExecutor(max_workers=max_workers)
208
+ interrupted = False
209
+ # In test mode, use wait=False to avoid hanging on shutdown
210
+ wait_on_shutdown = not is_test_mode
195
211
  try:
196
- for future in as_completed(future_to_file):
197
- try:
198
- results = future.result()
199
- # Merge results into instance variables (sequential merge is fast)
200
- self._merge_analysis_results(results)
201
- completed_count += 1
202
- progress.update(task3, completed=completed_count)
203
- except KeyboardInterrupt:
204
- # Cancel remaining tasks and break out of loop immediately
205
- interrupted = True
206
- for f in future_to_file:
207
- if not f.done():
208
- f.cancel()
209
- break
210
- except Exception as e:
211
- # Log error but continue processing
212
- file_path = future_to_file[future]
213
- console.print(f"[dim]⚠ Warning: Failed to analyze {file_path}: {e}[/dim]")
214
- completed_count += 1
215
- progress.update(task3, completed=completed_count)
212
+ # Submit all tasks
213
+ future_to_file = {executor.submit(analyze_file_safe, f): f for f in files_to_analyze}
214
+
215
+ # Collect results as they complete
216
+ try:
217
+ for future in as_completed(future_to_file):
218
+ try:
219
+ results = future.result()
220
+ # Merge results into instance variables (sequential merge is fast)
221
+ self._merge_analysis_results(results)
222
+ completed_count += 1
223
+ progress.update(task3, completed=completed_count)
224
+ except KeyboardInterrupt:
225
+ # Cancel remaining tasks and break out of loop immediately
226
+ interrupted = True
227
+ for f in future_to_file:
228
+ if not f.done():
229
+ f.cancel()
230
+ break
231
+ except Exception as e:
232
+ # Log error but continue processing
233
+ file_path = future_to_file[future]
234
+ console.print(f"[dim]⚠ Warning: Failed to analyze {file_path}: {e}[/dim]")
235
+ completed_count += 1
236
+ progress.update(task3, completed=completed_count)
237
+ except KeyboardInterrupt:
238
+ # Also catch KeyboardInterrupt from as_completed() itself
239
+ interrupted = True
240
+ for f in future_to_file:
241
+ if not f.done():
242
+ f.cancel()
243
+
244
+ # If interrupted, re-raise KeyboardInterrupt after breaking out of loop
245
+ if interrupted:
246
+ raise KeyboardInterrupt
216
247
  except KeyboardInterrupt:
217
- # Also catch KeyboardInterrupt from as_completed() itself
248
+ # Gracefully shutdown executor on interrupt (cancel pending tasks, don't wait)
218
249
  interrupted = True
219
- for f in future_to_file:
220
- if not f.done():
221
- f.cancel()
222
-
223
- # If interrupted, re-raise KeyboardInterrupt after breaking out of loop
224
- if interrupted:
225
- raise KeyboardInterrupt
226
- except KeyboardInterrupt:
227
- # Gracefully shutdown executor on interrupt (cancel pending tasks, don't wait)
228
- interrupted = True
229
- executor.shutdown(wait=False, cancel_futures=True)
230
- raise
231
- finally:
232
- # Ensure executor is properly shutdown
233
- # If interrupted, don't wait for tasks (they're already cancelled)
234
- # shutdown() is safe to call multiple times
235
- if not interrupted:
236
- executor.shutdown(wait=True)
237
- else:
238
- # Already shutdown with wait=False, just ensure cleanup
239
- executor.shutdown(wait=False)
250
+ executor.shutdown(wait=False, cancel_futures=True)
251
+ raise
252
+ finally:
253
+ # Ensure executor is properly shutdown
254
+ # If interrupted, don't wait for tasks (they're already cancelled)
255
+ # shutdown() is safe to call multiple times
256
+ # In test mode, use wait=False to avoid hanging
257
+ if not interrupted:
258
+ executor.shutdown(wait=wait_on_shutdown)
259
+ else:
260
+ # Already shutdown with wait=False, just ensure cleanup
261
+ executor.shutdown(wait=False)
240
262
 
241
263
  # Update progress for skipped files
242
264
  skipped_count = len(python_files) - len(files_to_analyze)
@@ -149,11 +149,17 @@ class GraphAnalyzer:
149
149
 
150
150
  # Add edges from AST imports (parallelized for performance)
151
151
  import multiprocessing
152
+
153
+ # In test mode, use fewer workers to avoid resource contention
154
+ import os
152
155
  from concurrent.futures import ThreadPoolExecutor, as_completed
153
156
 
154
- max_workers = max(
155
- 1, min(multiprocessing.cpu_count() or 4, 16, len(python_files))
156
- ) # Increased for faster processing, ensure at least 1
157
+ if os.environ.get("TEST_MODE") == "true":
158
+ max_workers = max(1, min(2, len(python_files))) # Max 2 workers in test mode
159
+ else:
160
+ max_workers = max(
161
+ 1, min(multiprocessing.cpu_count() or 4, 16, len(python_files))
162
+ ) # Increased for faster processing, ensure at least 1
157
163
 
158
164
  # Get list of known modules for matching (needed for parallel processing)
159
165
  known_modules = list(graph.nodes())
@@ -176,8 +182,12 @@ class GraphAnalyzer:
176
182
  return edges
177
183
 
178
184
  # Process AST imports in parallel
179
- with ThreadPoolExecutor(max_workers=max_workers) as executor:
180
- future_to_file = {executor.submit(process_imports, file_path): file_path for file_path in python_files}
185
+ import os
186
+
187
+ executor1 = ThreadPoolExecutor(max_workers=max_workers)
188
+ wait_on_shutdown = os.environ.get("TEST_MODE") != "true"
189
+ try:
190
+ future_to_file = {executor1.submit(process_imports, file_path): file_path for file_path in python_files}
181
191
 
182
192
  for future in as_completed(future_to_file):
183
193
  try:
@@ -186,11 +196,14 @@ class GraphAnalyzer:
186
196
  graph.add_edge(module_name, matching_module)
187
197
  except Exception:
188
198
  continue
199
+ finally:
200
+ executor1.shutdown(wait=wait_on_shutdown)
189
201
 
190
202
  # Extract call graphs using pyan (if available) - parallelized for performance
191
- with ThreadPoolExecutor(max_workers=max_workers) as executor:
203
+ executor2 = ThreadPoolExecutor(max_workers=max_workers)
204
+ try:
192
205
  future_to_file = {
193
- executor.submit(self.extract_call_graph, file_path): file_path for file_path in python_files
206
+ executor2.submit(self.extract_call_graph, file_path): file_path for file_path in python_files
194
207
  }
195
208
 
196
209
  for future in as_completed(future_to_file):
@@ -206,6 +219,8 @@ class GraphAnalyzer:
206
219
  except Exception:
207
220
  # Skip if call graph extraction fails for this file
208
221
  continue
222
+ finally:
223
+ executor2.shutdown(wait=wait_on_shutdown)
209
224
 
210
225
  self.dependency_graph = graph
211
226
  return graph
@@ -380,10 +380,16 @@ class RelationshipMapper:
380
380
  }
381
381
 
382
382
  # Use ThreadPoolExecutor for parallel processing
383
- max_workers = min(os.cpu_count() or 4, 16, len(python_files)) # Cap at 16 workers for faster processing
383
+ # In test mode, use fewer workers to avoid resource contention
384
+ if os.environ.get("TEST_MODE") == "true":
385
+ max_workers = max(1, min(2, len(python_files))) # Max 2 workers in test mode
386
+ else:
387
+ max_workers = min(os.cpu_count() or 4, 16, len(python_files)) # Cap at 16 workers for faster processing
384
388
 
385
389
  executor = ThreadPoolExecutor(max_workers=max_workers)
386
390
  interrupted = False
391
+ # In test mode, use wait=False to avoid hanging on shutdown
392
+ wait_on_shutdown = os.environ.get("TEST_MODE") != "true"
387
393
  try:
388
394
  # Submit all tasks
389
395
  future_to_file = {executor.submit(self._analyze_file_parallel, f): f for f in python_files}
@@ -424,7 +430,7 @@ class RelationshipMapper:
424
430
  raise
425
431
  finally:
426
432
  if not interrupted:
427
- executor.shutdown(wait=True)
433
+ executor.shutdown(wait=wait_on_shutdown)
428
434
  else:
429
435
  executor.shutdown(wait=False)
430
436