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.
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/PKG-INFO +49 -37
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/README.md +48 -36
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/pyproject.toml +1 -1
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/__init__.py +1 -1
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/__init__.py +1 -1
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/analyzers/code_analyzer.py +74 -52
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/analyzers/graph_analyzer.py +22 -7
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/analyzers/relationship_mapper.py +8 -2
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/commands/analyze.py +53 -143
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/commands/enforce.py +46 -31
- specfact_cli-0.13.0/src/specfact_cli/commands/generate.py +1391 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/commands/import_cmd.py +255 -116
- specfact_cli-0.13.0/src/specfact_cli/commands/spec.py +861 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/commands/sync.py +69 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/generators/openapi_extractor.py +203 -24
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/generators/test_to_openapi.py +8 -1
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/models/project.py +16 -4
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/incremental_check.py +16 -4
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/source_scanner.py +8 -2
- specfact_cli-0.11.5/src/specfact_cli/commands/generate.py +0 -515
- specfact_cli-0.11.5/src/specfact_cli/commands/spec.py +0 -407
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/.gitignore +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/LICENSE.md +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/mappings/node-async.yaml +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/mappings/python-async.yaml +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/mappings/speckit-default.yaml +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/prompts/shared/cli-enforcement.md +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/prompts/specfact.01-import.md +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/prompts/specfact.02-plan.md +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/prompts/specfact.03-review.md +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/prompts/specfact.04-sdd.md +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/prompts/specfact.05-enforce.md +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/prompts/specfact.06-sync.md +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/prompts/specfact.compare.md +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/prompts/specfact.validate.md +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/schemas/deviation.schema.json +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/schemas/plan.schema.json +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/schemas/protocol.schema.json +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/templates/github-action.yml.j2 +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/templates/plan.bundle.yaml.j2 +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/templates/pr-template.md.j2 +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/templates/protocol.yaml.j2 +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/resources/templates/telemetry.yaml.example +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/agents/__init__.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/agents/analyze_agent.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/agents/base.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/agents/plan_agent.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/agents/registry.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/agents/sync_agent.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/analyzers/__init__.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/analyzers/ambiguity_scanner.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/analyzers/constitution_evidence_extractor.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/analyzers/contract_extractor.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/analyzers/control_flow_analyzer.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/analyzers/requirement_extractor.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/analyzers/test_pattern_extractor.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/cli.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/commands/__init__.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/commands/bridge.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/commands/drift.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/commands/implement.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/commands/init.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/commands/migrate.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/commands/plan.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/commands/repro.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/commands/run.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/commands/sdd.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/common/__init__.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/common/logger_setup.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/common/logging_utils.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/common/text_utils.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/common/utils.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/comparators/__init__.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/comparators/plan_comparator.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/enrichers/constitution_enricher.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/enrichers/plan_enricher.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/generators/__init__.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/generators/contract_generator.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/generators/plan_generator.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/generators/protocol_generator.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/generators/report_generator.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/generators/task_generator.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/generators/workflow_generator.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/importers/__init__.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/importers/speckit_converter.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/importers/speckit_scanner.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/integrations/__init__.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/integrations/specmatic.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/migrations/__init__.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/migrations/plan_migrator.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/models/__init__.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/models/bridge.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/models/deviation.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/models/enforcement.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/models/plan.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/models/protocol.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/models/quality.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/models/sdd.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/models/source_tracking.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/models/task.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/modes/__init__.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/modes/detector.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/modes/router.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/resources/semgrep/async.yml +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/resources/semgrep/code-quality.yml +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/resources/semgrep/feature-detection.yml +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/runtime.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/sync/__init__.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/sync/bridge_probe.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/sync/bridge_sync.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/sync/bridge_watch.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/sync/change_detector.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/sync/code_to_spec.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/sync/drift_detector.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/sync/repository_sync.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/sync/spec_to_code.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/sync/spec_to_tests.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/sync/speckit_sync.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/sync/watcher.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/telemetry.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/templates/__init__.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/templates/bridge_templates.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/__init__.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/acceptance_criteria.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/bundle_loader.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/console.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/enrichment_context.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/enrichment_parser.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/feature_keys.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/git.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/github_annotations.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/ide_setup.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/optional_deps.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/progress.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/prompts.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/sdd_discovery.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/structure.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/structured_io.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/utils/yaml_utils.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/validators/__init__.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/validators/contract_validator.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/validators/fsm.py +0 -0
- {specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/validators/repro_checker.py +0 -0
- {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.
|
|
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
|
-
> **
|
|
279
|
-
>
|
|
278
|
+
> **Stop vibe coding. Start shipping quality code with contracts.**
|
|
279
|
+
> Brownfield-first CLI: Reverse engineer legacy Python → specs → enforced contracts
|
|
280
280
|
|
|
281
|
+
[](https://pypi.org/project/specfact-cli/)
|
|
282
|
+
[](https://pypi.org/project/specfact-cli/)
|
|
281
283
|
[](LICENSE.md)
|
|
282
|
-
[](https://www.python.org/)
|
|
283
284
|
[](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
|
|
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
|
-
>
|
|
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
|
-
> **
|
|
4
|
-
>
|
|
3
|
+
> **Stop vibe coding. Start shipping quality code with contracts.**
|
|
4
|
+
> Brownfield-first CLI: Reverse engineer legacy Python → specs → enforced contracts
|
|
5
5
|
|
|
6
|
+
[](https://pypi.org/project/specfact-cli/)
|
|
7
|
+
[](https://pypi.org/project/specfact-cli/)
|
|
6
8
|
[](LICENSE.md)
|
|
7
|
-
[](https://www.python.org/)
|
|
8
9
|
[](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
|
|
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
|
-
>
|
|
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.
|
|
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"
|
|
@@ -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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
#
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
#
|
|
248
|
+
# Gracefully shutdown executor on interrupt (cancel pending tasks, don't wait)
|
|
218
249
|
interrupted = True
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
155
|
-
1, min(
|
|
156
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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
|
-
|
|
203
|
+
executor2 = ThreadPoolExecutor(max_workers=max_workers)
|
|
204
|
+
try:
|
|
192
205
|
future_to_file = {
|
|
193
|
-
|
|
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
|
{specfact_cli-0.11.5 → specfact_cli-0.13.0}/src/specfact_cli/analyzers/relationship_mapper.py
RENAMED
|
@@ -380,10 +380,16 @@ class RelationshipMapper:
|
|
|
380
380
|
}
|
|
381
381
|
|
|
382
382
|
# Use ThreadPoolExecutor for parallel processing
|
|
383
|
-
|
|
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=
|
|
433
|
+
executor.shutdown(wait=wait_on_shutdown)
|
|
428
434
|
else:
|
|
429
435
|
executor.shutdown(wait=False)
|
|
430
436
|
|