specfact-cli 0.46.4__tar.gz → 0.46.16__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 (278) hide show
  1. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/PKG-INFO +1 -1
  2. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/pyproject.toml +2 -1
  3. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/__init__.py +1 -1
  4. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/__init__.py +1 -1
  5. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/analyzers/code_analyzer.py +20 -0
  6. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/cli.py +127 -26
  7. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/modules/init/module-package.yaml +3 -3
  8. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/modules/init/src/commands.py +33 -13
  9. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/modules/module_registry/module-package.yaml +3 -3
  10. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/modules/module_registry/src/commands.py +72 -7
  11. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/modules/upgrade/module-package.yaml +2 -3
  12. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/modules/upgrade/src/commands.py +152 -87
  13. specfact_cli-0.46.16/src/specfact_cli/registry/module_availability.py +214 -0
  14. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/registry/module_discovery.py +123 -41
  15. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/registry/module_packages.py +22 -5
  16. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/optional_deps.py +7 -1
  17. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/.gitignore +0 -0
  18. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/LICENSE +0 -0
  19. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/README.md +0 -0
  20. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/resources/bundled-module-registry/index.json +0 -0
  21. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/resources/keys/README.md +0 -0
  22. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/resources/keys/module-signing-public.pem +0 -0
  23. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/resources/mappings/node-async.yaml +0 -0
  24. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/resources/mappings/python-async.yaml +0 -0
  25. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/resources/mappings/speckit-default.yaml +0 -0
  26. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/resources/schemas/deviation.schema.json +0 -0
  27. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/resources/schemas/plan.schema.json +0 -0
  28. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/resources/schemas/protocol.schema.json +0 -0
  29. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/resources/templates/github-action.yml.j2 +0 -0
  30. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/resources/templates/persona/architect.md.j2 +0 -0
  31. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/resources/templates/persona/developer.md.j2 +0 -0
  32. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/resources/templates/persona/product-owner.md.j2 +0 -0
  33. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/resources/templates/plan.bundle.yaml.j2 +0 -0
  34. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/resources/templates/policies/kanban.yaml +0 -0
  35. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/resources/templates/policies/mixed.yaml +0 -0
  36. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/resources/templates/policies/safe.yaml +0 -0
  37. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/resources/templates/policies/scrum.yaml +0 -0
  38. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/resources/templates/pr-template.md.j2 +0 -0
  39. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/resources/templates/protocol.yaml.j2 +0 -0
  40. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/resources/templates/telemetry.yaml.example +0 -0
  41. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/__main__.py +0 -0
  42. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/adapters/__init__.py +0 -0
  43. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/adapters/ado.py +0 -0
  44. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/adapters/backlog_base.py +0 -0
  45. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/adapters/base.py +0 -0
  46. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/adapters/github.py +0 -0
  47. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/adapters/openspec.py +0 -0
  48. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/adapters/openspec_parser.py +0 -0
  49. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/adapters/registry.py +0 -0
  50. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/adapters/speckit.py +0 -0
  51. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/agents/__init__.py +0 -0
  52. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/agents/analyze_agent.py +0 -0
  53. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/agents/base.py +0 -0
  54. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/agents/plan_agent.py +0 -0
  55. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/agents/registry.py +0 -0
  56. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/agents/sync_agent.py +0 -0
  57. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/analyzers/__init__.py +0 -0
  58. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/analyzers/ambiguity_scanner.py +0 -0
  59. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/analyzers/constitution_evidence_extractor.py +0 -0
  60. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/analyzers/contract_extractor.py +0 -0
  61. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/analyzers/control_flow_analyzer.py +0 -0
  62. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/analyzers/graph_analyzer.py +0 -0
  63. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/analyzers/relationship_mapper.py +0 -0
  64. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/analyzers/requirement_extractor.py +0 -0
  65. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/analyzers/test_pattern_extractor.py +0 -0
  66. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/backlog/__init__.py +0 -0
  67. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/backlog/adapters/__init__.py +0 -0
  68. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/backlog/adapters/base.py +0 -0
  69. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/backlog/converter.py +0 -0
  70. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/backlog/filters.py +0 -0
  71. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/backlog/mappers/__init__.py +0 -0
  72. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/backlog/mappers/ado_mapper.py +0 -0
  73. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/backlog/mappers/base.py +0 -0
  74. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/backlog/mappers/github_mapper.py +0 -0
  75. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/backlog/mappers/template_config.py +0 -0
  76. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/commands/__init__.py +0 -0
  77. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/commands/_bundle_shim.py +0 -0
  78. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/commands/analyze.py +0 -0
  79. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/commands/contract_cmd.py +0 -0
  80. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/commands/drift.py +0 -0
  81. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/commands/enforce.py +0 -0
  82. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/commands/generate.py +0 -0
  83. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/commands/import_cmd.py +0 -0
  84. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/commands/init.py +0 -0
  85. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/commands/migrate.py +0 -0
  86. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/commands/plan.py +0 -0
  87. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/commands/project_cmd.py +0 -0
  88. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/commands/repro.py +0 -0
  89. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/commands/sdd.py +0 -0
  90. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/commands/spec.py +0 -0
  91. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/commands/sync.py +0 -0
  92. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/commands/update.py +0 -0
  93. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/commands/validate.py +0 -0
  94. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/common/__init__.py +0 -0
  95. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/common/bundle_factory.py +0 -0
  96. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/common/logger_setup.py +0 -0
  97. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/common/logging_utils.py +0 -0
  98. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/common/text_utils.py +0 -0
  99. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/common/utils.py +0 -0
  100. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/comparators/__init__.py +0 -0
  101. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/comparators/plan_comparator.py +0 -0
  102. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/contracts/__init__.py +0 -0
  103. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/contracts/crosshair_props.py +0 -0
  104. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/contracts/module_interface.py +0 -0
  105. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/enrichers/constitution_enricher.py +0 -0
  106. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/enrichers/plan_enricher.py +0 -0
  107. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/generators/__init__.py +0 -0
  108. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/generators/contract_generator.py +0 -0
  109. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/generators/openapi_extractor.py +0 -0
  110. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/generators/persona_exporter.py +0 -0
  111. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/generators/plan_generator.py +0 -0
  112. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/generators/protocol_generator.py +0 -0
  113. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/generators/report_generator.py +0 -0
  114. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/generators/task_generator.py +0 -0
  115. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/generators/test_to_openapi.py +0 -0
  116. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/generators/workflow_generator.py +0 -0
  117. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/groups/__init__.py +0 -0
  118. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/groups/codebase_group.py +0 -0
  119. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/groups/govern_group.py +0 -0
  120. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/groups/member_group.py +0 -0
  121. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/groups/project_group.py +0 -0
  122. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/groups/spec_group.py +0 -0
  123. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/importers/__init__.py +0 -0
  124. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/importers/speckit_converter.py +0 -0
  125. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/importers/speckit_scanner.py +0 -0
  126. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/integrations/__init__.py +0 -0
  127. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/integrations/specmatic.py +0 -0
  128. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/merge/__init__.py +0 -0
  129. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/merge/resolver.py +0 -0
  130. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/migrations/__init__.py +0 -0
  131. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/migrations/plan_migrator.py +0 -0
  132. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/models/__init__.py +0 -0
  133. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/models/backlog_item.py +0 -0
  134. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/models/bridge.py +0 -0
  135. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/models/capabilities.py +0 -0
  136. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/models/change.py +0 -0
  137. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/models/contract.py +0 -0
  138. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/models/deviation.py +0 -0
  139. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/models/dor_config.py +0 -0
  140. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/models/enforcement.py +0 -0
  141. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/models/module_package.py +0 -0
  142. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/models/persona_template.py +0 -0
  143. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/models/plan.py +0 -0
  144. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/models/project.py +0 -0
  145. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/models/protocol.py +0 -0
  146. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/models/quality.py +0 -0
  147. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/models/sdd.py +0 -0
  148. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/models/source_tracking.py +0 -0
  149. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/models/task.py +0 -0
  150. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/models/validation.py +0 -0
  151. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/modes/__init__.py +0 -0
  152. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/modes/detector.py +0 -0
  153. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/modes/router.py +0 -0
  154. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/modules/__init__.py +0 -0
  155. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/modules/_bundle_import.py +0 -0
  156. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/modules/init/src/__init__.py +0 -0
  157. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/modules/init/src/app.py +0 -0
  158. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/modules/init/src/first_run_selection.py +0 -0
  159. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/modules/module_io_shim.py +0 -0
  160. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/modules/module_registry/src/__init__.py +0 -0
  161. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/modules/module_registry/src/app.py +0 -0
  162. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/modules/upgrade/src/__init__.py +0 -0
  163. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/modules/upgrade/src/app.py +0 -0
  164. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/parsers/__init__.py +0 -0
  165. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/parsers/persona_importer.py +0 -0
  166. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/registry/__init__.py +0 -0
  167. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/registry/alias_manager.py +0 -0
  168. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/registry/bootstrap.py +0 -0
  169. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/registry/bridge_registry.py +0 -0
  170. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/registry/crypto_validator.py +0 -0
  171. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/registry/custom_registries.py +0 -0
  172. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/registry/dependency_resolver.py +0 -0
  173. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/registry/extension_registry.py +0 -0
  174. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/registry/help_cache.py +0 -0
  175. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/registry/marketplace_client.py +0 -0
  176. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/registry/metadata.py +0 -0
  177. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/registry/module_grouping.py +0 -0
  178. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/registry/module_installer.py +0 -0
  179. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/registry/module_lifecycle.py +0 -0
  180. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/registry/module_security.py +0 -0
  181. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/registry/module_state.py +0 -0
  182. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/registry/registry.py +0 -0
  183. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/resources/semgrep/async.yml +0 -0
  184. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/resources/semgrep/code-quality.yml +0 -0
  185. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/resources/semgrep/feature-detection.yml +0 -0
  186. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/runtime.py +0 -0
  187. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/sync/__init__.py +0 -0
  188. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/sync/bridge_probe.py +0 -0
  189. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/sync/bridge_sync.py +0 -0
  190. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/sync/bridge_sync_openspec_md_parse.py +0 -0
  191. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/sync/bridge_sync_requirement_from_proposal.py +0 -0
  192. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/sync/bridge_sync_requirement_helpers.py +0 -0
  193. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/sync/bridge_sync_tasks_from_proposal.py +0 -0
  194. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/sync/bridge_sync_what_changes_format.py +0 -0
  195. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/sync/bridge_sync_write_openspec_from_proposal.py +0 -0
  196. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/sync/bridge_watch.py +0 -0
  197. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/sync/change_detector.py +0 -0
  198. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/sync/code_to_spec.py +0 -0
  199. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/sync/drift_detector.py +0 -0
  200. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/sync/repository_sync.py +0 -0
  201. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/sync/spec_to_code.py +0 -0
  202. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/sync/spec_to_tests.py +0 -0
  203. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/sync/watcher.py +0 -0
  204. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/sync/watcher_enhanced.py +0 -0
  205. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/telemetry.py +0 -0
  206. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/templates/__init__.py +0 -0
  207. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/templates/defaults/defect_v1.yaml +0 -0
  208. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/templates/defaults/enabler_v1.yaml +0 -0
  209. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/templates/defaults/spike_v1.yaml +0 -0
  210. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/templates/defaults/user_story_v1.yaml +0 -0
  211. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/templates/frameworks/scrum/user_story_v1.yaml +0 -0
  212. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/templates/personas/product-owner/user_story_v1.yaml +0 -0
  213. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/templates/providers/ado/work_item_v1.yaml +0 -0
  214. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/templates/registry.py +0 -0
  215. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/templates/specification_templates.py +0 -0
  216. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/__init__.py +0 -0
  217. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/acceptance_criteria.py +0 -0
  218. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/auth_tokens.py +0 -0
  219. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/bundle_converters.py +0 -0
  220. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/bundle_loader.py +0 -0
  221. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/code_change_detector.py +0 -0
  222. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/console.py +0 -0
  223. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/content_sanitizer.py +0 -0
  224. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/context_detection.py +0 -0
  225. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/contract_predicates.py +0 -0
  226. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/enrichment_context.py +0 -0
  227. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/enrichment_parser.py +0 -0
  228. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/env_manager.py +0 -0
  229. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/feature_keys.py +0 -0
  230. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/git.py +0 -0
  231. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/github_annotations.py +0 -0
  232. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/icontract_helpers.py +0 -0
  233. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/ide_setup.py +0 -0
  234. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/incremental_check.py +0 -0
  235. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/metadata.py +0 -0
  236. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/performance.py +0 -0
  237. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/persona_ownership.py +0 -0
  238. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/progress.py +0 -0
  239. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/progressive_disclosure.py +0 -0
  240. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/project_artifact_write.py +0 -0
  241. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/prompts.py +0 -0
  242. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/sdd_discovery.py +0 -0
  243. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/source_scanner.py +0 -0
  244. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/startup_checks.py +0 -0
  245. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/structure.py +0 -0
  246. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/structured_io.py +0 -0
  247. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/suggestions.py +0 -0
  248. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/terminal.py +0 -0
  249. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/utils/yaml_utils.py +0 -0
  250. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/validation/__init__.py +0 -0
  251. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/validation/command_audit.py +0 -0
  252. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/validators/__init__.py +0 -0
  253. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/validators/agile_validation.py +0 -0
  254. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/validators/change_proposal_integration.py +0 -0
  255. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/validators/cli_first_validator.py +0 -0
  256. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/validators/contract_validator.py +0 -0
  257. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/validators/fsm.py +0 -0
  258. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/validators/repro_checker.py +0 -0
  259. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/validators/schema.py +0 -0
  260. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/validators/sidecar/__init__.py +0 -0
  261. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/validators/sidecar/contract_populator.py +0 -0
  262. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/validators/sidecar/crosshair_runner.py +0 -0
  263. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/validators/sidecar/crosshair_summary.py +0 -0
  264. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/validators/sidecar/dependency_installer.py +0 -0
  265. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/validators/sidecar/framework_detector.py +0 -0
  266. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/validators/sidecar/frameworks/__init__.py +0 -0
  267. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/validators/sidecar/frameworks/base.py +0 -0
  268. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/validators/sidecar/frameworks/django.py +0 -0
  269. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/validators/sidecar/frameworks/drf.py +0 -0
  270. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/validators/sidecar/frameworks/fastapi.py +0 -0
  271. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/validators/sidecar/frameworks/flask.py +0 -0
  272. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/validators/sidecar/harness_generator.py +0 -0
  273. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/validators/sidecar/models.py +0 -0
  274. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/validators/sidecar/orchestrator.py +0 -0
  275. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/validators/sidecar/specmatic_runner.py +0 -0
  276. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/validators/sidecar/unannotated_detector.py +0 -0
  277. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/versioning/__init__.py +0 -0
  278. {specfact_cli-0.46.4 → specfact_cli-0.46.16}/src/specfact_cli/versioning/analyzer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specfact-cli
3
- Version: 0.46.4
3
+ Version: 0.46.16
4
4
  Summary: The swiss knife CLI for agile DevOps teams. Keep backlog, specs, tests, and code in sync with validation and contract enforcement for new projects and long-lived codebases.
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.46.4"
7
+ version = "0.46.16"
8
8
  description = "The swiss knife CLI for agile DevOps teams. Keep backlog, specs, tests, and code in sync with validation and contract enforcement for new projects and long-lived codebases."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -221,6 +221,7 @@ test-cov = "pytest --cov=src --cov-report=term-missing {args}"
221
221
  type-check = "basedpyright --pythonpath $(python -c 'import sys; print(sys.executable)') {args}"
222
222
  # basedpyright --level error: suppress warning noise in pre-commit (Block 1 runs `hatch run lint`).
223
223
  lint = "ruff format . --check && basedpyright --level error --pythonpath $(python -c 'import sys; print(sys.executable)') && ruff check . && pylint src tests tools && python scripts/verify_safe_project_writes.py"
224
+ lint-changed = "python scripts/run_changed_lint.py {args}"
224
225
  governance = "pylint src tests tools --reports=y --output-format=parseable"
225
226
  format = "ruff check . --fix && ruff format ."
226
227
 
@@ -3,4 +3,4 @@ SpecFact CLI - Spec→Contract→Sentinel tool for contract-driven development.
3
3
  """
4
4
 
5
5
  # Package version: keep in sync with pyproject.toml, setup.py, src/specfact_cli/__init__.py
6
- __version__ = "0.46.4"
6
+ __version__ = "0.46.16"
@@ -45,6 +45,6 @@ def _bootstrap_bundle_paths() -> None:
45
45
 
46
46
  _bootstrap_bundle_paths()
47
47
 
48
- __version__ = "0.46.4"
48
+ __version__ = "0.46.16"
49
49
 
50
50
  __all__ = ["__version__"]
@@ -32,6 +32,24 @@ from specfact_cli.utils.feature_keys import to_classname_key, to_sequential_key
32
32
  console = Console()
33
33
 
34
34
 
35
+ def _ensure_semgrep_runtime_dir(repo_path: Path, relative: str) -> str:
36
+ """Create and return a stable repo-local runtime directory for Semgrep."""
37
+ path = (repo_path / relative).resolve()
38
+ path.mkdir(parents=True, exist_ok=True)
39
+ return str(path)
40
+
41
+
42
+ def _build_semgrep_env(repo_path: Path) -> dict[str, str]:
43
+ """Build a repo-local Semgrep runtime env so CLI startup stays deterministic."""
44
+ env = dict(os.environ)
45
+ env["XDG_CONFIG_HOME"] = _ensure_semgrep_runtime_dir(repo_path, ".specfact/config")
46
+ env["XDG_CACHE_HOME"] = _ensure_semgrep_runtime_dir(repo_path, ".specfact/cache")
47
+ env["SEMGREP_VERSION_CACHE_PATH"] = _ensure_semgrep_runtime_dir(repo_path, ".specfact/cache/semgrep")
48
+ semgrep_log_dir = _ensure_semgrep_runtime_dir(repo_path, ".specfact/logs")
49
+ env["SEMGREP_LOG_FILE"] = str((Path(semgrep_log_dir) / "semgrep.log").resolve())
50
+ return env
51
+
52
+
35
53
  @dataclass
36
54
  class _SemgrepFeatureBuckets:
37
55
  api_endpoints: list[str] = field(default_factory=list)
@@ -457,6 +475,7 @@ class CodeAnalyzer:
457
475
  capture_output=True,
458
476
  text=True,
459
477
  timeout=5, # Increased timeout to 5s (Semgrep may need time to initialize)
478
+ env=_build_semgrep_env(self.repo_path),
460
479
  )
461
480
  return result.returncode == 0
462
481
  except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
@@ -566,6 +585,7 @@ class CodeAnalyzer:
566
585
  capture_output=True,
567
586
  text=True,
568
587
  timeout=timeout,
588
+ env=_build_semgrep_env(self.repo_path),
569
589
  )
570
590
 
571
591
  # Semgrep may return non-zero for valid findings
@@ -14,7 +14,7 @@ from collections.abc import Callable, Mapping
14
14
  from dataclasses import dataclass
15
15
  from datetime import datetime
16
16
  from pathlib import Path
17
- from typing import Annotated, Any, cast
17
+ from typing import Annotated, Any, NoReturn, cast
18
18
 
19
19
 
20
20
  _DetectShellFn = Callable[..., tuple[str | None, str | None]]
@@ -66,6 +66,7 @@ from specfact_cli.registry import CommandRegistry
66
66
  from specfact_cli.registry.alias_manager import resolve_command
67
67
  from specfact_cli.registry.bootstrap import register_builtin_commands
68
68
  from specfact_cli.registry.metadata import CommandMetadata
69
+ from specfact_cli.registry.module_availability import ModuleAvailabilityStatus, classify_module_availability
69
70
  from specfact_cli.runtime import get_configured_console, init_debug_log_file, set_debug_mode
70
71
  from specfact_cli.utils.progressive_disclosure import ProgressiveDisclosureGroup
71
72
  from specfact_cli.utils.structured_io import StructuredFormat
@@ -124,6 +125,28 @@ def _print_missing_bundle_command_help(invoked: str) -> None:
124
125
  module_id = _INVOKED_TO_MARKETPLACE_MODULE.get(invoked)
125
126
  console = get_configured_console()
126
127
  if module_id is not None:
128
+ availability = classify_module_availability(module_id=module_id, command_name=invoked)
129
+ if availability.status is ModuleAvailabilityStatus.DISABLED:
130
+ console.print(
131
+ f"[bold red]Module '{availability.module_id or module_id}' is installed but disabled.[/bold red]\n"
132
+ f"The [bold]{invoked}[/bold] command group is provided by that module. "
133
+ f"Enable with [bold]{availability.recovery_command}[/bold]."
134
+ )
135
+ return
136
+ if availability.status is ModuleAvailabilityStatus.SKIPPED:
137
+ console.print(
138
+ f"[bold red]Module '{availability.module_id or module_id}' is installed but skipped.[/bold red]\n"
139
+ f"Reason: {availability.reason}. "
140
+ "Inspect with [bold]specfact module list --show-origin[/bold]."
141
+ )
142
+ return
143
+ if availability.status is ModuleAvailabilityStatus.SHADOWED:
144
+ console.print(
145
+ f"[bold red]Module '{availability.module_id or module_id}' is shadowed in this workspace.[/bold red]\n"
146
+ f"Shadowed by: {availability.shadowed_by}. "
147
+ "Inspect with [bold]specfact module list --show-origin[/bold]."
148
+ )
149
+ return
127
150
  console.print(
128
151
  f"[bold red]Module '{module_id}' is not installed.[/bold red]\n"
129
152
  f"The [bold]{invoked}[/bold] command group is provided by that module. "
@@ -509,6 +532,84 @@ def _lazy_delegate_cmd_name_ready(self: _LazyDelegateGroup) -> bool:
509
532
  return len(self._lazy_cmd_name) > 0
510
533
 
511
534
 
535
+ def _args_request_help(args: tuple[str, ...] | list[str]) -> bool:
536
+ """Return True when delegated args are asking only for command help."""
537
+ return any(arg in ("--help", "-h", "--help-advanced", "-ha") for arg in args)
538
+
539
+
540
+ def _delegated_help_path(cmd_name: str, args: tuple[str, ...] | list[str]) -> str:
541
+ """Build a stable command path for fallback help output."""
542
+ path_parts = [cmd_name]
543
+ for arg in args:
544
+ if arg.startswith("-"):
545
+ continue
546
+ path_parts.append(arg)
547
+ return " ".join(path_parts)
548
+
549
+
550
+ def _print_lazy_help_fallback(cmd_name: str, args: tuple[str, ...] | list[str]) -> None:
551
+ """Print minimal help when Typer cannot materialize a command for a loaded bundle."""
552
+ command_path = _delegated_help_path(cmd_name, args)
553
+ get_configured_console().print(
554
+ f"[bold]{command_path}[/bold]\n\n"
555
+ "Help is available for this installed command path, but the command metadata could not be "
556
+ "materialized in this runtime. Reinstall the providing module or run the command without "
557
+ "`--help` to execute it."
558
+ )
559
+
560
+
561
+ def _raise_lazy_delegate_click_exception(exc: Exception) -> NoReturn:
562
+ raise click.ClickException(str(exc)) from exc
563
+
564
+
565
+ def _load_lazy_delegate_typer(cmd_name: str) -> typer.Typer:
566
+ resolved_name = resolve_command(cmd_name)
567
+ try:
568
+ return CommandRegistry.get_typer(resolved_name)
569
+ except ValueError as exc:
570
+ if cmd_name in KNOWN_BUNDLE_GROUP_OR_SHIM_NAMES:
571
+ _print_missing_bundle_command_help(cmd_name)
572
+ raise SystemExit(1) from None
573
+ _raise_lazy_delegate_click_exception(exc)
574
+ raise AssertionError("unreachable") from None
575
+
576
+
577
+ def _build_lazy_delegate_click_command(cmd_name: str, args: tuple[str, ...], real_typer: typer.Typer) -> click.Command:
578
+ from typer.main import get_command
579
+
580
+ try:
581
+ return get_command(real_typer)
582
+ except (RuntimeError, ValueError) as exc:
583
+ if _args_request_help(args):
584
+ _print_lazy_help_fallback(cmd_name, args)
585
+ raise SystemExit(0) from None
586
+ _raise_lazy_delegate_click_exception(exc)
587
+ raise AssertionError("unreachable") from None
588
+
589
+
590
+ def _lazy_delegate_prog_name(ctx: click.Context, cmd_name: str) -> str:
591
+ parts: list[str] = []
592
+ parent = ctx.parent
593
+ while parent and getattr(parent, "command", None):
594
+ name = getattr(parent.command, "name", None)
595
+ if name and name != "__delegate__":
596
+ parts.append(name)
597
+ parent = getattr(parent, "parent", None)
598
+ if parts:
599
+ return " ".join(reversed(parts))
600
+ original_prog_name = ctx.meta.get("original_prog_name")
601
+ if isinstance(original_prog_name, str) and original_prog_name:
602
+ return original_prog_name
603
+ return cmd_name
604
+
605
+
606
+ def _strip_redundant_single_command_arg(click_cmd: click.Command, args: tuple[str, ...]) -> list[str]:
607
+ args_list = list(args)
608
+ if not isinstance(click_cmd, click.Group) and args_list and args_list[0] == getattr(click_cmd, "name", None):
609
+ return args_list[1:]
610
+ return args_list
611
+
612
+
512
613
  class _LazyDelegateGroup(click.Group):
513
614
  """Click Group that delegates all args to the real command (lazy-loaded)."""
514
615
 
@@ -521,6 +622,8 @@ class _LazyDelegateGroup(click.Group):
521
622
  name=name or cmd_name,
522
623
  help=help or help_str,
523
624
  context_settings={"ignore_unknown_options": True},
625
+ invoke_without_command=True,
626
+ no_args_is_help=False,
524
627
  )
525
628
  self._lazy_cmd_name = cmd_name
526
629
  self._lazy_help_str = help_str
@@ -530,31 +633,15 @@ class _LazyDelegateGroup(click.Group):
530
633
  cmd_name = self._lazy_cmd_name
531
634
 
532
635
  def _invoke(args: tuple[str, ...]) -> None:
533
- from typer.main import get_command
534
-
535
636
  ctx = click.get_current_context()
536
- resolved_name = resolve_command(cmd_name)
537
- real_typer = CommandRegistry.get_typer(resolved_name)
538
- click_cmd = get_command(real_typer)
637
+ real_typer = _load_lazy_delegate_typer(cmd_name)
638
+ click_cmd = _build_lazy_delegate_click_command(cmd_name, args, real_typer)
539
639
  # Build full prog name from root (e.g. "specfact sync") so usage shows "specfact sync bridge", not "sync sync bridge"
540
- parts: list[str] = []
541
- p = ctx.parent
542
- while p and getattr(p, "command", None):
543
- name = getattr(p.command, "name", None)
544
- if name and name != "__delegate__":
545
- parts.append(name)
546
- p = getattr(p, "parent", None)
547
- prog_name = " ".join(reversed(parts)) if parts else cmd_name
548
- args_list = list(args)
640
+ prog_name = _lazy_delegate_prog_name(ctx, cmd_name)
549
641
  # When the real app is a single command (e.g. drift has only "detect"), Typer
550
642
  # builds a TyperCommand, not a Group. Then args are ["detect", "bundle", "--repo", ...]
551
643
  # and the command expects ["bundle", "--repo", ...] (no leading "detect").
552
- if (
553
- not isinstance(click_cmd, click.Group)
554
- and args_list
555
- and args_list[0] == getattr(click_cmd, "name", None)
556
- ):
557
- args_list = args_list[1:]
644
+ args_list = _strip_redundant_single_command_arg(click_cmd, args)
558
645
  exit_code = click_cmd.main(args=args_list, prog_name=prog_name, standalone_mode=False)
559
646
  if exit_code and exit_code != 0:
560
647
  raise SystemExit(exit_code)
@@ -567,6 +654,14 @@ class _LazyDelegateGroup(click.Group):
567
654
  add_help_option=False, # Pass --help through to real Typer so "specfact backlog daily ado --help" shows correct usage
568
655
  )
569
656
 
657
+ @require(lambda ctx: ctx is not None, "ctx must not be None")
658
+ @ensure(lambda result: result is None or isinstance(result, int), "result must be None or an exit code")
659
+ def invoke(self, ctx: click.Context) -> Any:
660
+ if ctx.invoked_subcommand is None and not ctx.args:
661
+ ctx.meta["original_prog_name"] = ctx.command_path
662
+ return self._delegate_cmd.main(args=[], prog_name=ctx.command_path, standalone_mode=False)
663
+ return super().invoke(ctx)
664
+
570
665
  @require(_lazy_delegate_cmd_name_ready, "lazy command name must be set")
571
666
  @ensure(lambda result: isinstance(result, tuple) and len(result) == 3, "result must be a 3-tuple")
572
667
  def resolve_command(
@@ -574,7 +669,7 @@ class _LazyDelegateGroup(click.Group):
574
669
  ) -> tuple[str | None, click.Command | None, list[str]]:
575
670
  # Pass through all args to the delegate so "plan init bundle" becomes args for the real plan Typer.
576
671
  if not args:
577
- return None, None, []
672
+ return self._delegate_cmd.name, self._delegate_cmd, []
578
673
  return self._delegate_cmd.name, self._delegate_cmd, list(args)
579
674
 
580
675
  @ensure(lambda result: isinstance(result, list), "result must be a list of command names")
@@ -598,8 +693,11 @@ class _LazyDelegateGroup(click.Group):
598
693
  from typer.main import get_command
599
694
 
600
695
  resolved_name = resolve_command(self._lazy_cmd_name)
601
- real_typer = CommandRegistry.get_typer(resolved_name)
602
- click_cmd = get_command(real_typer)
696
+ try:
697
+ real_typer = CommandRegistry.get_typer(resolved_name)
698
+ click_cmd = get_command(real_typer)
699
+ except (RuntimeError, ValueError):
700
+ return None
603
701
  if isinstance(click_cmd, click.Group):
604
702
  return click_cmd
605
703
  return None
@@ -610,8 +708,11 @@ class _LazyDelegateGroup(click.Group):
610
708
  from typer.main import get_command
611
709
 
612
710
  resolved_name = resolve_command(self._lazy_cmd_name)
613
- real_typer = CommandRegistry.get_typer(resolved_name)
614
- click_cmd = get_command(real_typer)
711
+ try:
712
+ real_typer = CommandRegistry.get_typer(resolved_name)
713
+ click_cmd = get_command(real_typer)
714
+ except (RuntimeError, ValueError):
715
+ return
615
716
  prog_name = (
616
717
  f"{ctx.parent.command.name} {self._lazy_cmd_name}"
617
718
  if ctx.parent and ctx.parent.command
@@ -1,5 +1,5 @@
1
1
  name: init
2
- version: 0.1.30
2
+ version: 0.1.31
3
3
  commands:
4
4
  - init
5
5
  category: core
@@ -17,5 +17,5 @@ publisher:
17
17
  description: Initialize SpecFact workspace and bootstrap local configuration.
18
18
  license: Apache-2.0
19
19
  integrity:
20
- checksum: sha256:9e03421972d3254082307834b474e7673957de8c11ffacc563f2da3f35e7cf05
21
- signature: ikUYhUJ8AFU9RkB+ZBnp0lKrDLT7ZIqkauRYUMRY/swrIjRCTRzHIpNcvCmujK6h1w6EtT/+wGG1v5bZtZB8CQ==
20
+ checksum: sha256:0f7bc54a823bea14033fcb143ecb6c83d2bca2b5da661f03a0b545100acebe5b
21
+ signature: dTatkqgBUtti4tL/pmcFBZY9bsJ61gY/V0lP9gZU8Y5W3YWK+wpgRx1oewlAmfKzkxca2NhalKcjLACQjTNvAA==
@@ -106,7 +106,7 @@ def _resolve_field_mapping_templates_dir(repo_path: Path) -> Path | None:
106
106
  package_templates_dir = Path(str(templates_ref)).resolve()
107
107
  if package_templates_dir.exists():
108
108
  return package_templates_dir
109
- except Exception:
109
+ except (ImportError, OSError, ValueError):
110
110
  try:
111
111
  import importlib.util
112
112
 
@@ -118,8 +118,8 @@ def _resolve_field_mapping_templates_dir(repo_path: Path) -> Path | None:
118
118
  ).resolve()
119
119
  if package_templates_dir.exists():
120
120
  return package_templates_dir
121
- except Exception:
122
- pass
121
+ except (ImportError, OSError, ValueError):
122
+ return None
123
123
  return None
124
124
 
125
125
 
@@ -434,7 +434,15 @@ def _is_valid_repo_path(repo: Path) -> bool:
434
434
 
435
435
 
436
436
  @beartype
437
- def _install_profile_bundles(profile: str, install_root: Path, non_interactive: bool) -> None:
437
+ def _marketplace_ids_for_bundles(bundle_ids: list[str]) -> list[str]:
438
+ return [
439
+ first_run_selection.MARKETPLACE_ONLY_BUNDLES[bid]
440
+ for bid in bundle_ids
441
+ if bid in first_run_selection.MARKETPLACE_ONLY_BUNDLES
442
+ ]
443
+
444
+
445
+ def _install_profile_bundles(profile: str, install_root: Path, non_interactive: bool) -> list[str]:
438
446
  """Resolve profile to bundle list and install via module installer."""
439
447
  bundle_ids = first_run_selection.resolve_profile_bundles(profile)
440
448
  if bundle_ids:
@@ -444,10 +452,11 @@ def _install_profile_bundles(profile: str, install_root: Path, non_interactive:
444
452
  install_root,
445
453
  non_interactive=non_interactive,
446
454
  )
455
+ return _marketplace_ids_for_bundles(bundle_ids)
447
456
 
448
457
 
449
458
  @beartype
450
- def _install_bundle_list(install_arg: str, install_root: Path, non_interactive: bool) -> None:
459
+ def _install_bundle_list(install_arg: str, install_root: Path, non_interactive: bool) -> list[str]:
451
460
  """Parse comma-separated or 'all' and install bundles via module installer."""
452
461
  bundle_ids = first_run_selection.resolve_install_bundles(install_arg)
453
462
  if bundle_ids:
@@ -457,20 +466,32 @@ def _install_bundle_list(install_arg: str, install_root: Path, non_interactive:
457
466
  install_root,
458
467
  non_interactive=non_interactive,
459
468
  )
469
+ return _marketplace_ids_for_bundles(bundle_ids)
460
470
 
461
471
 
462
- def _apply_profile_or_install_bundles(profile: str | None, install: str | None) -> None:
472
+ def _apply_profile_or_install_bundles(profile: str | None, install: str | None) -> list[str]:
463
473
  try:
464
474
  non_interactive = is_non_interactive()
465
475
  if profile is not None:
466
- _install_profile_bundles(profile, INIT_USER_MODULES_ROOT, non_interactive=non_interactive)
467
- else:
468
- _install_bundle_list(install or "", INIT_USER_MODULES_ROOT, non_interactive=non_interactive)
476
+ return _install_profile_bundles(profile, INIT_USER_MODULES_ROOT, non_interactive=non_interactive)
477
+ return _install_bundle_list(install or "", INIT_USER_MODULES_ROOT, non_interactive=non_interactive)
469
478
  except ValueError as e:
470
479
  console.print(f"[red]Error:[/red] {e}")
471
480
  raise typer.Exit(1) from e
472
481
 
473
482
 
483
+ def _refresh_init_module_state(repo_path: Path, enabled_module_ids: list[str]) -> list[dict[str, Any]]:
484
+ modules_list = get_discovered_modules_for_state(
485
+ enable_ids=enabled_module_ids,
486
+ disable_ids=[],
487
+ base_path=repo_path,
488
+ preserve_existing=True,
489
+ )
490
+ if modules_list:
491
+ write_modules_state(modules_list)
492
+ return modules_list
493
+
494
+
474
495
  def _run_interactive_first_run_install() -> None:
475
496
  try:
476
497
  bundle_ids = _interactive_first_run_bundle_selection()
@@ -702,8 +723,9 @@ def init(
702
723
 
703
724
  repo_path = repo.resolve()
704
725
 
726
+ enabled_module_ids: list[str] = []
705
727
  if profile is not None or install is not None:
706
- _apply_profile_or_install_bundles(profile, install)
728
+ enabled_module_ids = _apply_profile_or_install_bundles(profile, install)
707
729
  elif is_first_run(user_root=INIT_USER_MODULES_ROOT) and is_non_interactive():
708
730
  console.print(
709
731
  "[red]Error:[/red] In CI/CD (non-interactive) mode, first-run init requires "
@@ -718,9 +740,7 @@ def init(
718
740
  _run_interactive_first_run_install()
719
741
 
720
742
  _init_user_visible_step("[cyan]→[/cyan] Discovering installed modules and writing registry state…")
721
- modules_list = get_discovered_modules_for_state(enable_ids=[], disable_ids=[])
722
- if modules_list:
723
- write_modules_state(modules_list)
743
+ modules_list = _refresh_init_module_state(repo_path, enabled_module_ids)
724
744
 
725
745
  _init_user_visible_step("[cyan]→[/cyan] Indexing CLI commands for help cache…")
726
746
  run_discovery_and_write_cache(__version__)
@@ -1,5 +1,5 @@
1
1
  name: module-registry
2
- version: 0.1.20
2
+ version: 0.1.23
3
3
  commands:
4
4
  - module
5
5
  category: core
@@ -17,5 +17,5 @@ publisher:
17
17
  description: 'Manage modules: search, list, show, install, and upgrade.'
18
18
  license: Apache-2.0
19
19
  integrity:
20
- checksum: sha256:a92afa757a54ee63b84ae4a5f5b232cb5dbddacfcb31fcde3abcdb4927eada8c
21
- signature: jelDGPZyCLLpyzqH4+S2t7V9ICy/puYEzdUu/GX0oPiWgMZRg8aZlnECZGy+m+sHvS0XX3hPAXCSp3IJf6hHDg==
20
+ checksum: sha256:f500281d2249d712be23a1b25b5660374694dfa47634f60ae4378eb2cdb753ca
21
+ signature: cunuat95bD44IcNUBs35NTlHYOR8atydVz4ZyZTjIzwJL6Wz5YOfBYGq/WtgDnOFv3KplfvhRZbTo3CwmS/KBQ==
@@ -20,12 +20,14 @@ from packaging.version import InvalidVersion, Version
20
20
  from rich.console import Console
21
21
  from rich.table import Table
22
22
 
23
+ from specfact_cli import __version__
23
24
  from specfact_cli.models.module_package import ModulePackageMetadata
24
25
  from specfact_cli.modules import module_io_shim
25
26
  from specfact_cli.registry.alias_manager import create_alias, list_aliases, remove_alias
26
27
  from specfact_cli.registry.custom_registries import add_registry, fetch_all_indexes, list_registries, remove_registry
28
+ from specfact_cli.registry.help_cache import run_discovery_and_write_cache
27
29
  from specfact_cli.registry.marketplace_client import fetch_registry_index
28
- from specfact_cli.registry.module_discovery import discover_all_modules
30
+ from specfact_cli.registry.module_discovery import discover_all_modules, discover_all_modules_for_project
29
31
  from specfact_cli.registry.module_installer import (
30
32
  REGISTRY_ID_FILE,
31
33
  USER_MODULES_ROOT,
@@ -42,7 +44,9 @@ from specfact_cli.registry.module_lifecycle import (
42
44
  render_modules_table,
43
45
  select_module_ids_interactive,
44
46
  )
47
+ from specfact_cli.registry.module_packages import get_discovered_modules_for_state
45
48
  from specfact_cli.registry.module_security import ensure_publisher_trusted, is_official_publisher
49
+ from specfact_cli.registry.module_state import read_modules_state, write_modules_state
46
50
  from specfact_cli.registry.registry import CommandRegistry
47
51
  from specfact_cli.runtime import is_non_interactive
48
52
 
@@ -178,15 +182,64 @@ def _resolve_install_target_root(scope_normalized: str, repo: Path | None) -> Pa
178
182
  return USER_MODULES_ROOT if scope_normalized == "user" else repo_path / ".specfact" / "modules"
179
183
 
180
184
 
185
+ def _normalize_project_repo(repo: Path | None) -> Path | None:
186
+ """Resolve a project-scoped repo argument to the nearest workspace root."""
187
+ if repo is None:
188
+ return None
189
+ repo_path = repo.resolve()
190
+ for candidate in [repo_path, *repo_path.parents]:
191
+ if (candidate / ".git").exists():
192
+ return candidate
193
+ return repo_path
194
+
195
+
196
+ def _read_installed_manifest_id(module_dir: Path, fallback_name: str) -> str:
197
+ manifest_path = module_dir / "module-package.yaml"
198
+ try:
199
+ raw = yaml.safe_load(manifest_path.read_text(encoding="utf-8"))
200
+ except (OSError, yaml.YAMLError):
201
+ return fallback_name
202
+ if isinstance(raw, dict):
203
+ manifest = cast(dict[str, Any], raw)
204
+ if manifest.get("name"):
205
+ return str(manifest["name"])
206
+ return fallback_name
207
+
208
+
209
+ def _enable_if_disabled(module_id: str, base_path: Path | None = None) -> bool:
210
+ state = read_modules_state()
211
+ if state.get(module_id, {}).get("enabled", True) is not False:
212
+ return False
213
+ modules = get_discovered_modules_for_state(
214
+ enable_ids=[module_id],
215
+ disable_ids=[],
216
+ base_path=base_path,
217
+ preserve_existing=True,
218
+ )
219
+ write_modules_state(modules)
220
+ run_discovery_and_write_cache(__version__)
221
+ return any(str(row.get("id", "")) == module_id and bool(row.get("enabled", True)) for row in modules)
222
+
223
+
181
224
  def _install_skip_if_already_satisfied(
182
225
  scope_normalized: str,
183
226
  requested_name: str,
184
227
  target_root: Path,
228
+ repo: Path | None,
185
229
  reinstall: bool,
186
230
  discovered_by_name: dict[str, Any],
187
231
  ) -> bool:
188
- if (target_root / requested_name / "module-package.yaml").exists() and not reinstall:
189
- console.print(f"[yellow]Module '{requested_name}' is already installed in {target_root}.[/yellow]")
232
+ installed_dir = target_root / requested_name
233
+ if (installed_dir / "module-package.yaml").exists() and not reinstall:
234
+ module_id = _read_installed_manifest_id(installed_dir, requested_name)
235
+ enabled = _enable_if_disabled(module_id, base_path=repo if scope_normalized == "project" else None)
236
+ if enabled:
237
+ console.print(
238
+ f"[yellow]Module '{module_id}' is already installed in {target_root}; "
239
+ "enabled it in module state.[/yellow]"
240
+ )
241
+ else:
242
+ console.print(f"[yellow]Module '{module_id}' is already installed in {target_root}.[/yellow]")
190
243
  return True
191
244
  skip_sources = {"builtin", "project", "user", "custom"}
192
245
  if scope_normalized == "project":
@@ -195,9 +248,14 @@ def _install_skip_if_already_satisfied(
195
248
  skip_sources.discard("project")
196
249
  existing = discovered_by_name.get(requested_name)
197
250
  if existing is not None and existing.source in skip_sources:
251
+ enabled = _enable_if_disabled(
252
+ existing.metadata.name,
253
+ base_path=repo if scope_normalized == "project" else None,
254
+ )
255
+ state_hint = " Enabled it in module state." if enabled else ""
198
256
  console.print(
199
- f"[yellow]Module '{requested_name}' is already available from source '{existing.source}'. "
200
- "No marketplace install needed.[/yellow]"
257
+ f"[yellow]Module '{existing.metadata.name}' is already available from source '{existing.source}'. "
258
+ f"No marketplace install needed.{state_hint}[/yellow]"
201
259
  )
202
260
  return True
203
261
  return False
@@ -274,6 +332,7 @@ class _InstallOneParams:
274
332
  scope_normalized: str
275
333
  source_normalized: str
276
334
  target_root: Path
335
+ repo: Path | None
277
336
  version: str | None
278
337
  reinstall: bool
279
338
  trust_non_official: bool
@@ -289,6 +348,7 @@ def _install_one(module_id: str, params: _InstallOneParams) -> bool:
289
348
  params.scope_normalized,
290
349
  requested_name,
291
350
  params.target_root,
351
+ params.repo,
292
352
  params.reinstall,
293
353
  params.discovered_by_name,
294
354
  ):
@@ -392,12 +452,17 @@ def _install_impl(module_ids: list[str], **kwargs: Any) -> None:
392
452
  )
393
453
  raise typer.Exit(1)
394
454
  scope_normalized, source_normalized = _parse_install_scope_and_source(scope, source)
395
- target_root = _resolve_install_target_root(scope_normalized, repo)
396
- discovered_by_name = {entry.metadata.name: entry for entry in discover_all_modules()}
455
+ normalized_repo = _normalize_project_repo(repo) if scope_normalized == "project" else None
456
+ target_root = _resolve_install_target_root(scope_normalized, normalized_repo)
457
+ discovered = (
458
+ discover_all_modules_for_project(normalized_repo) if normalized_repo is not None else discover_all_modules()
459
+ )
460
+ discovered_by_name = {entry.metadata.name: entry for entry in discovered}
397
461
  params = _InstallOneParams(
398
462
  scope_normalized=scope_normalized,
399
463
  source_normalized=source_normalized,
400
464
  target_root=target_root,
465
+ repo=normalized_repo,
401
466
  version=version,
402
467
  reinstall=reinstall,
403
468
  trust_non_official=trust_non_official,
@@ -1,5 +1,5 @@
1
1
  name: upgrade
2
- version: 0.1.4
2
+ version: 0.1.12
3
3
  commands:
4
4
  - upgrade
5
5
  category: core
@@ -17,5 +17,4 @@ publisher:
17
17
  description: Check and apply SpecFact CLI version upgrades.
18
18
  license: Apache-2.0
19
19
  integrity:
20
- checksum: sha256:0648be45eb877287ebef717d38c71d48c1e17191dfb24b0c8dde57015f7ba144
21
- signature: ZMw8ljS+0f4TYg2WVAqQCpgaae1d8z7wT/1r2yxuM6ZeZjMejhgeBuOyXopda5LOXjioxTxOlWZmGN94cCC3Ag==
20
+ checksum: sha256:5bc01c11370e696da4fa365188c3d223a7bd3a99ffc36e878b841a4081d6bb62