specfact-cli 0.11.5__tar.gz → 0.13.0__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.5 → specfact_cli-0.13.0}/PKG-INFO +49 -37
  2. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/README.md +48 -36
  3. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/pyproject.toml +1 -1
  4. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/__init__.py +1 -1
  5. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/__init__.py +1 -1
  6. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/analyzers/code_analyzer.py +74 -52
  7. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/analyzers/graph_analyzer.py +22 -7
  8. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/analyzers/relationship_mapper.py +8 -2
  9. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/commands/analyze.py +53 -143
  10. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/commands/enforce.py +46 -31
  11. specfact_cli-0.13.0/src/specfact_cli/commands/generate.py +1391 -0
  12. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/commands/import_cmd.py +255 -116
  13. specfact_cli-0.13.0/src/specfact_cli/commands/spec.py +861 -0
  14. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/commands/sync.py +69 -0
  15. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/generators/openapi_extractor.py +203 -24
  16. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/generators/test_to_openapi.py +8 -1
  17. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/models/project.py +16 -4
  18. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/incremental_check.py +16 -4
  19. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/source_scanner.py +8 -2
  20. specfact_cli-0.11.5/src/specfact_cli/commands/generate.py +0 -515
  21. specfact_cli-0.11.5/src/specfact_cli/commands/spec.py +0 -407
  22. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/.gitignore +0 -0
  23. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/LICENSE.md +0 -0
  24. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/mappings/node-async.yaml +0 -0
  25. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/mappings/python-async.yaml +0 -0
  26. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/mappings/speckit-default.yaml +0 -0
  27. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/prompts/shared/cli-enforcement.md +0 -0
  28. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/prompts/specfact.01-import.md +0 -0
  29. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/prompts/specfact.02-plan.md +0 -0
  30. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/prompts/specfact.03-review.md +0 -0
  31. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/prompts/specfact.04-sdd.md +0 -0
  32. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/prompts/specfact.05-enforce.md +0 -0
  33. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/prompts/specfact.06-sync.md +0 -0
  34. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/prompts/specfact.compare.md +0 -0
  35. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/prompts/specfact.validate.md +0 -0
  36. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/schemas/deviation.schema.json +0 -0
  37. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/schemas/plan.schema.json +0 -0
  38. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/schemas/protocol.schema.json +0 -0
  39. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/templates/github-action.yml.j2 +0 -0
  40. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/templates/plan.bundle.yaml.j2 +0 -0
  41. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/templates/pr-template.md.j2 +0 -0
  42. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/templates/protocol.yaml.j2 +0 -0
  43. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/templates/telemetry.yaml.example +0 -0
  44. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/agents/__init__.py +0 -0
  45. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/agents/analyze_agent.py +0 -0
  46. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/agents/base.py +0 -0
  47. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/agents/plan_agent.py +0 -0
  48. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/agents/registry.py +0 -0
  49. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/agents/sync_agent.py +0 -0
  50. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/analyzers/__init__.py +0 -0
  51. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/analyzers/ambiguity_scanner.py +0 -0
  52. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/analyzers/constitution_evidence_extractor.py +0 -0
  53. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/analyzers/contract_extractor.py +0 -0
  54. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/analyzers/control_flow_analyzer.py +0 -0
  55. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/analyzers/requirement_extractor.py +0 -0
  56. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/analyzers/test_pattern_extractor.py +0 -0
  57. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/cli.py +0 -0
  58. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/commands/__init__.py +0 -0
  59. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/commands/bridge.py +0 -0
  60. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/commands/drift.py +0 -0
  61. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/commands/implement.py +0 -0
  62. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/commands/init.py +0 -0
  63. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/commands/migrate.py +0 -0
  64. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/commands/plan.py +0 -0
  65. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/commands/repro.py +0 -0
  66. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/commands/run.py +0 -0
  67. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/commands/sdd.py +0 -0
  68. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/common/__init__.py +0 -0
  69. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/common/logger_setup.py +0 -0
  70. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/common/logging_utils.py +0 -0
  71. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/common/text_utils.py +0 -0
  72. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/common/utils.py +0 -0
  73. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/comparators/__init__.py +0 -0
  74. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/comparators/plan_comparator.py +0 -0
  75. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/enrichers/constitution_enricher.py +0 -0
  76. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/enrichers/plan_enricher.py +0 -0
  77. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/generators/__init__.py +0 -0
  78. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/generators/contract_generator.py +0 -0
  79. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/generators/plan_generator.py +0 -0
  80. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/generators/protocol_generator.py +0 -0
  81. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/generators/report_generator.py +0 -0
  82. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/generators/task_generator.py +0 -0
  83. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/generators/workflow_generator.py +0 -0
  84. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/importers/__init__.py +0 -0
  85. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/importers/speckit_converter.py +0 -0
  86. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/importers/speckit_scanner.py +0 -0
  87. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/integrations/__init__.py +0 -0
  88. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/integrations/specmatic.py +0 -0
  89. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/migrations/__init__.py +0 -0
  90. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/migrations/plan_migrator.py +0 -0
  91. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/models/__init__.py +0 -0
  92. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/models/bridge.py +0 -0
  93. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/models/deviation.py +0 -0
  94. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/models/enforcement.py +0 -0
  95. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/models/plan.py +0 -0
  96. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/models/protocol.py +0 -0
  97. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/models/quality.py +0 -0
  98. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/models/sdd.py +0 -0
  99. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/models/source_tracking.py +0 -0
  100. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/models/task.py +0 -0
  101. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/modes/__init__.py +0 -0
  102. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/modes/detector.py +0 -0
  103. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/modes/router.py +0 -0
  104. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/resources/semgrep/async.yml +0 -0
  105. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/resources/semgrep/code-quality.yml +0 -0
  106. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/resources/semgrep/feature-detection.yml +0 -0
  107. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/runtime.py +0 -0
  108. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/sync/__init__.py +0 -0
  109. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/sync/bridge_probe.py +0 -0
  110. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/sync/bridge_sync.py +0 -0
  111. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/sync/bridge_watch.py +0 -0
  112. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/sync/change_detector.py +0 -0
  113. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/sync/code_to_spec.py +0 -0
  114. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/sync/drift_detector.py +0 -0
  115. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/sync/repository_sync.py +0 -0
  116. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/sync/spec_to_code.py +0 -0
  117. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/sync/spec_to_tests.py +0 -0
  118. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/sync/speckit_sync.py +0 -0
  119. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/sync/watcher.py +0 -0
  120. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/telemetry.py +0 -0
  121. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/templates/__init__.py +0 -0
  122. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/templates/bridge_templates.py +0 -0
  123. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/__init__.py +0 -0
  124. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/acceptance_criteria.py +0 -0
  125. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/bundle_loader.py +0 -0
  126. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/console.py +0 -0
  127. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/enrichment_context.py +0 -0
  128. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/enrichment_parser.py +0 -0
  129. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/feature_keys.py +0 -0
  130. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/git.py +0 -0
  131. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/github_annotations.py +0 -0
  132. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/ide_setup.py +0 -0
  133. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/optional_deps.py +0 -0
  134. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/progress.py +0 -0
  135. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/prompts.py +0 -0
  136. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/sdd_discovery.py +0 -0
  137. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/structure.py +0 -0
  138. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/structured_io.py +0 -0
  139. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/yaml_utils.py +0 -0
  140. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/validators/__init__.py +0 -0
  141. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/validators/contract_validator.py +0 -0
  142. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/validators/fsm.py +0 -0
  143. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/validators/repro_checker.py +0 -0
  144. {specfact_cli-0.11.5 → specfact_cli-0.13.0}/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.5
3
+ Version: 0.13.0
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
@@ -275,20 +275,58 @@ Description-Content-Type: text/markdown
275
275
 
276
276
  # SpecFact CLI
277
277
 
278
- > **Understand and Modernize Legacy Code with Confidence**
279
- > Automatically extract specs from existing Python code, then enforce them as contracts
278
+ > **Stop vibe coding. Start shipping quality code with contracts.**
279
+ > Brownfield-first CLI: Reverse engineer legacy Python specs enforced contracts
280
280
 
281
+ [![PyPI version](https://img.shields.io/pypi/v/specfact-cli.svg)](https://pypi.org/project/specfact-cli/)
282
+ [![Python versions](https://img.shields.io/pypi/pyversions/specfact-cli.svg)](https://pypi.org/project/specfact-cli/)
281
283
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE.md)
282
- [![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/)
283
284
  [![Status](https://img.shields.io/badge/status-beta-orange.svg)](https://github.com/nold-ai/specfact-cli)
284
285
 
286
+ <div align="center">
287
+
288
+ **[🌐 Learn More at noldai.com](https://noldai.com)** • **[📚 Documentation](https://nold-ai.github.io/specfact-cli)** • **[💬 Support](mailto:hello@noldai.com)**
289
+
290
+ </div>
291
+
292
+ ---
293
+
294
+ ## 🚀 Quick Start in 60 Seconds
295
+
296
+ ### Install in 10 seconds
297
+
298
+ ```bash
299
+ # Zero-install (recommended)
300
+ uvx --from specfact-cli specfact
301
+
302
+ # Or install with pip
303
+ pip install specfact-cli
304
+ ```
305
+
306
+ ### Your first command (< 60 seconds)
307
+
308
+ ```bash
309
+ # Modernizing legacy code? (Recommended)
310
+ specfact import from-code legacy-api --repo .
311
+
312
+ # Starting a new project?
313
+ specfact plan init legacy-api --interactive
314
+
315
+ # Using GitHub Spec-Kit or other tools?
316
+ specfact import from-bridge --repo . --adapter speckit --write
317
+ ```
318
+
319
+ That's it! 🎉
320
+
321
+ > **Need machine-readable artifacts?** Use `specfact --output-format json …` (or the per-command `--output-format` flag) to emit plan bundles and reports as JSON instead of YAML.
322
+
285
323
  ---
286
324
 
287
325
  ## What is SpecFact CLI?
288
326
 
289
327
  A brownfield-first CLI that **reverse engineers your legacy code** into documented specs, then prevents regressions with runtime contract enforcement.
290
328
 
291
- **Stop guessing what your legacy code does.** SpecFact automatically extracts specs from existing code, then enforces them as you modernize.
329
+ **Stop vibe coding. Start shipping quality code with contracts.** SpecFact automatically extracts specs from existing code, then enforces them as you modernize—preventing bugs before they reach production.
292
330
 
293
331
  **Perfect for:** Teams modernizing legacy Python systems, data pipelines, DevOps scripts
294
332
 
@@ -347,37 +385,6 @@ SpecFact CLI works with your existing tools—no new platform to learn. See real
347
385
 
348
386
  ---
349
387
 
350
- ## Quick Start
351
-
352
- ### Install in 10 seconds
353
-
354
- ```bash
355
- # Zero-install (just run it)
356
- uvx specfact-cli@latest
357
-
358
- # Or install with pip
359
- pip install specfact-cli
360
- ```
361
-
362
- ### Your first command (< 60 seconds)
363
-
364
- ```bash
365
- # Modernizing legacy code? (Recommended)
366
- specfact import from-code legacy-api --repo .
367
-
368
- # Starting a new project?
369
- specfact plan init legacy-api --interactive
370
-
371
- # Using GitHub Spec-Kit or other tools?
372
- specfact import from-bridge --repo . --adapter speckit --write
373
- ```
374
-
375
- That's it! 🎉
376
-
377
- > Need machine-readable artifacts? Use `specfact --output-format json …` (or the per-command `--output-format` flag) to emit plan bundles and reports as JSON instead of YAML.
378
-
379
- ---
380
-
381
388
  ## See It In Action
382
389
 
383
390
  We ran SpecFact CLI **on itself** to prove it works with legacy code:
@@ -477,11 +484,16 @@ SpecFact CLI is licensed under the Apache License 2.0, which means:
477
484
  - 💬 **Questions?** [GitHub Discussions](https://github.com/nold-ai/specfact-cli/discussions)
478
485
  - 🐛 **Found a bug?** [GitHub Issues](https://github.com/nold-ai/specfact-cli/issues)
479
486
  - 📧 **Need help?** [hello@noldai.com](mailto:hello@noldai.com)
487
+ - 🌐 **Learn more:** [noldai.com](https://noldai.com)
480
488
 
481
489
  ---
482
490
 
483
- > **Built with ❤️ by [NOLD AI](https://noldai.com)**
491
+ <div align="center">
492
+
493
+ **Built with ❤️ by [NOLD AI](https://noldai.com)**
484
494
 
485
495
  Copyright © 2025 Nold AI (Owner: Dominikus Nold)
486
496
 
487
497
  **Trademarks**: NOLD AI (NOLDAI) is a registered trademark (wordmark) at the European Union Intellectual Property Office (EUIPO). All other trademarks mentioned in this project are the property of their respective owners. See [TRADEMARKS.md](TRADEMARKS.md) for more information.
498
+
499
+ </div>
@@ -1,19 +1,57 @@
1
1
  # SpecFact CLI
2
2
 
3
- > **Understand and Modernize Legacy Code with Confidence**
4
- > Automatically extract specs from existing Python code, then enforce them as contracts
3
+ > **Stop vibe coding. Start shipping quality code with contracts.**
4
+ > Brownfield-first CLI: Reverse engineer legacy Python specs enforced contracts
5
5
 
6
+ [![PyPI version](https://img.shields.io/pypi/v/specfact-cli.svg)](https://pypi.org/project/specfact-cli/)
7
+ [![Python versions](https://img.shields.io/pypi/pyversions/specfact-cli.svg)](https://pypi.org/project/specfact-cli/)
6
8
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE.md)
7
- [![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/)
8
9
  [![Status](https://img.shields.io/badge/status-beta-orange.svg)](https://github.com/nold-ai/specfact-cli)
9
10
 
11
+ <div align="center">
12
+
13
+ **[🌐 Learn More at noldai.com](https://noldai.com)** • **[📚 Documentation](https://nold-ai.github.io/specfact-cli)** • **[💬 Support](mailto:hello@noldai.com)**
14
+
15
+ </div>
16
+
17
+ ---
18
+
19
+ ## 🚀 Quick Start in 60 Seconds
20
+
21
+ ### Install in 10 seconds
22
+
23
+ ```bash
24
+ # Zero-install (recommended)
25
+ uvx --from specfact-cli specfact
26
+
27
+ # Or install with pip
28
+ pip install specfact-cli
29
+ ```
30
+
31
+ ### Your first command (< 60 seconds)
32
+
33
+ ```bash
34
+ # Modernizing legacy code? (Recommended)
35
+ specfact import from-code legacy-api --repo .
36
+
37
+ # Starting a new project?
38
+ specfact plan init legacy-api --interactive
39
+
40
+ # Using GitHub Spec-Kit or other tools?
41
+ specfact import from-bridge --repo . --adapter speckit --write
42
+ ```
43
+
44
+ That's it! 🎉
45
+
46
+ > **Need machine-readable artifacts?** Use `specfact --output-format json …` (or the per-command `--output-format` flag) to emit plan bundles and reports as JSON instead of YAML.
47
+
10
48
  ---
11
49
 
12
50
  ## What is SpecFact CLI?
13
51
 
14
52
  A brownfield-first CLI that **reverse engineers your legacy code** into documented specs, then prevents regressions with runtime contract enforcement.
15
53
 
16
- **Stop guessing what your legacy code does.** SpecFact automatically extracts specs from existing code, then enforces them as you modernize.
54
+ **Stop vibe coding. Start shipping quality code with contracts.** SpecFact automatically extracts specs from existing code, then enforces them as you modernize—preventing bugs before they reach production.
17
55
 
18
56
  **Perfect for:** Teams modernizing legacy Python systems, data pipelines, DevOps scripts
19
57
 
@@ -72,37 +110,6 @@ SpecFact CLI works with your existing tools—no new platform to learn. See real
72
110
 
73
111
  ---
74
112
 
75
- ## Quick Start
76
-
77
- ### Install in 10 seconds
78
-
79
- ```bash
80
- # Zero-install (just run it)
81
- uvx specfact-cli@latest
82
-
83
- # Or install with pip
84
- pip install specfact-cli
85
- ```
86
-
87
- ### Your first command (< 60 seconds)
88
-
89
- ```bash
90
- # Modernizing legacy code? (Recommended)
91
- specfact import from-code legacy-api --repo .
92
-
93
- # Starting a new project?
94
- specfact plan init legacy-api --interactive
95
-
96
- # Using GitHub Spec-Kit or other tools?
97
- specfact import from-bridge --repo . --adapter speckit --write
98
- ```
99
-
100
- That's it! 🎉
101
-
102
- > Need machine-readable artifacts? Use `specfact --output-format json …` (or the per-command `--output-format` flag) to emit plan bundles and reports as JSON instead of YAML.
103
-
104
- ---
105
-
106
113
  ## See It In Action
107
114
 
108
115
  We ran SpecFact CLI **on itself** to prove it works with legacy code:
@@ -202,11 +209,16 @@ SpecFact CLI is licensed under the Apache License 2.0, which means:
202
209
  - 💬 **Questions?** [GitHub Discussions](https://github.com/nold-ai/specfact-cli/discussions)
203
210
  - 🐛 **Found a bug?** [GitHub Issues](https://github.com/nold-ai/specfact-cli/issues)
204
211
  - 📧 **Need help?** [hello@noldai.com](mailto:hello@noldai.com)
212
+ - 🌐 **Learn more:** [noldai.com](https://noldai.com)
205
213
 
206
214
  ---
207
215
 
208
- > **Built with ❤️ by [NOLD AI](https://noldai.com)**
216
+ <div align="center">
217
+
218
+ **Built with ❤️ by [NOLD AI](https://noldai.com)**
209
219
 
210
220
  Copyright © 2025 Nold AI (Owner: Dominikus Nold)
211
221
 
212
222
  **Trademarks**: NOLD AI (NOLDAI) is a registered trademark (wordmark) at the European Union Intellectual Property Office (EUIPO). All other trademarks mentioned in this project are the property of their respective owners. See [TRADEMARKS.md](TRADEMARKS.md) for more information.
223
+
224
+ </div>
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "specfact-cli"
7
- version = "0.11.5"
7
+ version = "0.13.0"
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"
@@ -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.5"
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.5"
12
+ __version__ = "0.13.0"
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