specfact-cli 0.46.4__tar.gz → 0.46.9__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.9}/PKG-INFO +1 -1
  2. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/pyproject.toml +2 -1
  3. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/__init__.py +1 -1
  4. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/__init__.py +1 -1
  5. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/analyzers/code_analyzer.py +20 -0
  6. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/cli.py +23 -0
  7. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/init/module-package.yaml +3 -3
  8. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/init/src/commands.py +33 -13
  9. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/module_registry/module-package.yaml +3 -3
  10. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/module_registry/src/commands.py +72 -7
  11. specfact_cli-0.46.9/src/specfact_cli/registry/module_availability.py +214 -0
  12. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/module_discovery.py +104 -33
  13. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/module_packages.py +22 -5
  14. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/optional_deps.py +7 -1
  15. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/.gitignore +0 -0
  16. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/LICENSE +0 -0
  17. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/README.md +0 -0
  18. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/bundled-module-registry/index.json +0 -0
  19. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/keys/README.md +0 -0
  20. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/keys/module-signing-public.pem +0 -0
  21. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/mappings/node-async.yaml +0 -0
  22. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/mappings/python-async.yaml +0 -0
  23. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/mappings/speckit-default.yaml +0 -0
  24. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/schemas/deviation.schema.json +0 -0
  25. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/schemas/plan.schema.json +0 -0
  26. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/schemas/protocol.schema.json +0 -0
  27. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/templates/github-action.yml.j2 +0 -0
  28. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/templates/persona/architect.md.j2 +0 -0
  29. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/templates/persona/developer.md.j2 +0 -0
  30. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/templates/persona/product-owner.md.j2 +0 -0
  31. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/templates/plan.bundle.yaml.j2 +0 -0
  32. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/templates/policies/kanban.yaml +0 -0
  33. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/templates/policies/mixed.yaml +0 -0
  34. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/templates/policies/safe.yaml +0 -0
  35. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/templates/policies/scrum.yaml +0 -0
  36. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/templates/pr-template.md.j2 +0 -0
  37. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/templates/protocol.yaml.j2 +0 -0
  38. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/resources/templates/telemetry.yaml.example +0 -0
  39. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/__main__.py +0 -0
  40. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/adapters/__init__.py +0 -0
  41. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/adapters/ado.py +0 -0
  42. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/adapters/backlog_base.py +0 -0
  43. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/adapters/base.py +0 -0
  44. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/adapters/github.py +0 -0
  45. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/adapters/openspec.py +0 -0
  46. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/adapters/openspec_parser.py +0 -0
  47. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/adapters/registry.py +0 -0
  48. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/adapters/speckit.py +0 -0
  49. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/agents/__init__.py +0 -0
  50. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/agents/analyze_agent.py +0 -0
  51. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/agents/base.py +0 -0
  52. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/agents/plan_agent.py +0 -0
  53. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/agents/registry.py +0 -0
  54. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/agents/sync_agent.py +0 -0
  55. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/analyzers/__init__.py +0 -0
  56. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/analyzers/ambiguity_scanner.py +0 -0
  57. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/analyzers/constitution_evidence_extractor.py +0 -0
  58. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/analyzers/contract_extractor.py +0 -0
  59. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/analyzers/control_flow_analyzer.py +0 -0
  60. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/analyzers/graph_analyzer.py +0 -0
  61. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/analyzers/relationship_mapper.py +0 -0
  62. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/analyzers/requirement_extractor.py +0 -0
  63. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/analyzers/test_pattern_extractor.py +0 -0
  64. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/backlog/__init__.py +0 -0
  65. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/backlog/adapters/__init__.py +0 -0
  66. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/backlog/adapters/base.py +0 -0
  67. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/backlog/converter.py +0 -0
  68. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/backlog/filters.py +0 -0
  69. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/backlog/mappers/__init__.py +0 -0
  70. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/backlog/mappers/ado_mapper.py +0 -0
  71. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/backlog/mappers/base.py +0 -0
  72. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/backlog/mappers/github_mapper.py +0 -0
  73. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/backlog/mappers/template_config.py +0 -0
  74. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/__init__.py +0 -0
  75. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/_bundle_shim.py +0 -0
  76. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/analyze.py +0 -0
  77. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/contract_cmd.py +0 -0
  78. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/drift.py +0 -0
  79. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/enforce.py +0 -0
  80. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/generate.py +0 -0
  81. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/import_cmd.py +0 -0
  82. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/init.py +0 -0
  83. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/migrate.py +0 -0
  84. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/plan.py +0 -0
  85. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/project_cmd.py +0 -0
  86. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/repro.py +0 -0
  87. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/sdd.py +0 -0
  88. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/spec.py +0 -0
  89. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/sync.py +0 -0
  90. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/update.py +0 -0
  91. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/commands/validate.py +0 -0
  92. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/common/__init__.py +0 -0
  93. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/common/bundle_factory.py +0 -0
  94. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/common/logger_setup.py +0 -0
  95. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/common/logging_utils.py +0 -0
  96. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/common/text_utils.py +0 -0
  97. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/common/utils.py +0 -0
  98. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/comparators/__init__.py +0 -0
  99. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/comparators/plan_comparator.py +0 -0
  100. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/contracts/__init__.py +0 -0
  101. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/contracts/crosshair_props.py +0 -0
  102. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/contracts/module_interface.py +0 -0
  103. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/enrichers/constitution_enricher.py +0 -0
  104. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/enrichers/plan_enricher.py +0 -0
  105. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/generators/__init__.py +0 -0
  106. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/generators/contract_generator.py +0 -0
  107. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/generators/openapi_extractor.py +0 -0
  108. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/generators/persona_exporter.py +0 -0
  109. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/generators/plan_generator.py +0 -0
  110. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/generators/protocol_generator.py +0 -0
  111. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/generators/report_generator.py +0 -0
  112. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/generators/task_generator.py +0 -0
  113. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/generators/test_to_openapi.py +0 -0
  114. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/generators/workflow_generator.py +0 -0
  115. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/groups/__init__.py +0 -0
  116. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/groups/codebase_group.py +0 -0
  117. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/groups/govern_group.py +0 -0
  118. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/groups/member_group.py +0 -0
  119. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/groups/project_group.py +0 -0
  120. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/groups/spec_group.py +0 -0
  121. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/importers/__init__.py +0 -0
  122. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/importers/speckit_converter.py +0 -0
  123. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/importers/speckit_scanner.py +0 -0
  124. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/integrations/__init__.py +0 -0
  125. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/integrations/specmatic.py +0 -0
  126. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/merge/__init__.py +0 -0
  127. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/merge/resolver.py +0 -0
  128. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/migrations/__init__.py +0 -0
  129. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/migrations/plan_migrator.py +0 -0
  130. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/__init__.py +0 -0
  131. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/backlog_item.py +0 -0
  132. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/bridge.py +0 -0
  133. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/capabilities.py +0 -0
  134. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/change.py +0 -0
  135. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/contract.py +0 -0
  136. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/deviation.py +0 -0
  137. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/dor_config.py +0 -0
  138. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/enforcement.py +0 -0
  139. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/module_package.py +0 -0
  140. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/persona_template.py +0 -0
  141. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/plan.py +0 -0
  142. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/project.py +0 -0
  143. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/protocol.py +0 -0
  144. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/quality.py +0 -0
  145. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/sdd.py +0 -0
  146. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/source_tracking.py +0 -0
  147. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/task.py +0 -0
  148. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/models/validation.py +0 -0
  149. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modes/__init__.py +0 -0
  150. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modes/detector.py +0 -0
  151. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modes/router.py +0 -0
  152. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/__init__.py +0 -0
  153. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/_bundle_import.py +0 -0
  154. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/init/src/__init__.py +0 -0
  155. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/init/src/app.py +0 -0
  156. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/init/src/first_run_selection.py +0 -0
  157. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/module_io_shim.py +0 -0
  158. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/module_registry/src/__init__.py +0 -0
  159. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/module_registry/src/app.py +0 -0
  160. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/upgrade/module-package.yaml +0 -0
  161. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/upgrade/src/__init__.py +0 -0
  162. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/upgrade/src/app.py +0 -0
  163. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/modules/upgrade/src/commands.py +0 -0
  164. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/parsers/__init__.py +0 -0
  165. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/parsers/persona_importer.py +0 -0
  166. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/__init__.py +0 -0
  167. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/alias_manager.py +0 -0
  168. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/bootstrap.py +0 -0
  169. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/bridge_registry.py +0 -0
  170. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/crypto_validator.py +0 -0
  171. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/custom_registries.py +0 -0
  172. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/dependency_resolver.py +0 -0
  173. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/extension_registry.py +0 -0
  174. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/help_cache.py +0 -0
  175. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/marketplace_client.py +0 -0
  176. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/metadata.py +0 -0
  177. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/module_grouping.py +0 -0
  178. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/module_installer.py +0 -0
  179. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/module_lifecycle.py +0 -0
  180. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/module_security.py +0 -0
  181. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/module_state.py +0 -0
  182. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/registry/registry.py +0 -0
  183. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/resources/semgrep/async.yml +0 -0
  184. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/resources/semgrep/code-quality.yml +0 -0
  185. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/resources/semgrep/feature-detection.yml +0 -0
  186. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/runtime.py +0 -0
  187. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/__init__.py +0 -0
  188. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/bridge_probe.py +0 -0
  189. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/bridge_sync.py +0 -0
  190. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/bridge_sync_openspec_md_parse.py +0 -0
  191. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/bridge_sync_requirement_from_proposal.py +0 -0
  192. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/bridge_sync_requirement_helpers.py +0 -0
  193. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/bridge_sync_tasks_from_proposal.py +0 -0
  194. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/bridge_sync_what_changes_format.py +0 -0
  195. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/bridge_sync_write_openspec_from_proposal.py +0 -0
  196. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/bridge_watch.py +0 -0
  197. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/change_detector.py +0 -0
  198. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/code_to_spec.py +0 -0
  199. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/drift_detector.py +0 -0
  200. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/repository_sync.py +0 -0
  201. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/spec_to_code.py +0 -0
  202. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/spec_to_tests.py +0 -0
  203. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/watcher.py +0 -0
  204. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/sync/watcher_enhanced.py +0 -0
  205. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/telemetry.py +0 -0
  206. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/templates/__init__.py +0 -0
  207. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/templates/defaults/defect_v1.yaml +0 -0
  208. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/templates/defaults/enabler_v1.yaml +0 -0
  209. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/templates/defaults/spike_v1.yaml +0 -0
  210. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/templates/defaults/user_story_v1.yaml +0 -0
  211. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/templates/frameworks/scrum/user_story_v1.yaml +0 -0
  212. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/templates/personas/product-owner/user_story_v1.yaml +0 -0
  213. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/templates/providers/ado/work_item_v1.yaml +0 -0
  214. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/templates/registry.py +0 -0
  215. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/templates/specification_templates.py +0 -0
  216. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/__init__.py +0 -0
  217. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/acceptance_criteria.py +0 -0
  218. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/auth_tokens.py +0 -0
  219. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/bundle_converters.py +0 -0
  220. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/bundle_loader.py +0 -0
  221. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/code_change_detector.py +0 -0
  222. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/console.py +0 -0
  223. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/content_sanitizer.py +0 -0
  224. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/context_detection.py +0 -0
  225. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/contract_predicates.py +0 -0
  226. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/enrichment_context.py +0 -0
  227. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/enrichment_parser.py +0 -0
  228. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/env_manager.py +0 -0
  229. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/feature_keys.py +0 -0
  230. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/git.py +0 -0
  231. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/github_annotations.py +0 -0
  232. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/icontract_helpers.py +0 -0
  233. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/ide_setup.py +0 -0
  234. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/incremental_check.py +0 -0
  235. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/metadata.py +0 -0
  236. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/performance.py +0 -0
  237. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/persona_ownership.py +0 -0
  238. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/progress.py +0 -0
  239. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/progressive_disclosure.py +0 -0
  240. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/project_artifact_write.py +0 -0
  241. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/prompts.py +0 -0
  242. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/sdd_discovery.py +0 -0
  243. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/source_scanner.py +0 -0
  244. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/startup_checks.py +0 -0
  245. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/structure.py +0 -0
  246. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/structured_io.py +0 -0
  247. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/suggestions.py +0 -0
  248. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/terminal.py +0 -0
  249. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/utils/yaml_utils.py +0 -0
  250. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validation/__init__.py +0 -0
  251. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validation/command_audit.py +0 -0
  252. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/__init__.py +0 -0
  253. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/agile_validation.py +0 -0
  254. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/change_proposal_integration.py +0 -0
  255. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/cli_first_validator.py +0 -0
  256. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/contract_validator.py +0 -0
  257. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/fsm.py +0 -0
  258. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/repro_checker.py +0 -0
  259. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/schema.py +0 -0
  260. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/__init__.py +0 -0
  261. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/contract_populator.py +0 -0
  262. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/crosshair_runner.py +0 -0
  263. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/crosshair_summary.py +0 -0
  264. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/dependency_installer.py +0 -0
  265. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/framework_detector.py +0 -0
  266. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/frameworks/__init__.py +0 -0
  267. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/frameworks/base.py +0 -0
  268. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/frameworks/django.py +0 -0
  269. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/frameworks/drf.py +0 -0
  270. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/frameworks/fastapi.py +0 -0
  271. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/frameworks/flask.py +0 -0
  272. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/harness_generator.py +0 -0
  273. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/models.py +0 -0
  274. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/orchestrator.py +0 -0
  275. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/specmatic_runner.py +0 -0
  276. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/validators/sidecar/unannotated_detector.py +0 -0
  277. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/src/specfact_cli/versioning/__init__.py +0 -0
  278. {specfact_cli-0.46.4 → specfact_cli-0.46.9}/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.9
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.9"
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.9"
@@ -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.9"
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
@@ -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. "
@@ -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,
@@ -0,0 +1,214 @@
1
+ """Metadata-only module availability classification for user-facing diagnostics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from enum import StrEnum
7
+ from pathlib import Path
8
+
9
+ from beartype import beartype
10
+ from icontract import ensure, require
11
+
12
+ from specfact_cli import __version__ as cli_version
13
+ from specfact_cli.registry.module_discovery import DiscoveredModule, discover_all_modules_for_project_with_shadowed
14
+ from specfact_cli.registry.module_packages import (
15
+ _check_core_compatibility,
16
+ _validate_module_dependencies,
17
+ merge_module_state,
18
+ )
19
+ from specfact_cli.registry.module_state import read_modules_state
20
+
21
+
22
+ class ModuleAvailabilityStatus(StrEnum):
23
+ """User-facing module availability states."""
24
+
25
+ ABSENT = "absent"
26
+ AVAILABLE = "available"
27
+ DISABLED = "disabled"
28
+ SKIPPED = "skipped"
29
+ SHADOWED = "shadowed"
30
+ AMBIGUOUS = "ambiguous"
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class ModuleAvailability:
35
+ """Availability classification without importing module command code."""
36
+
37
+ status: ModuleAvailabilityStatus
38
+ module_id: str | None = None
39
+ source: str | None = None
40
+ package_dir: Path | None = None
41
+ reason: str = ""
42
+ recovery_command: str = ""
43
+ shadowed_by: Path | None = None
44
+
45
+
46
+ def _module_id_tail(module_id: str) -> str:
47
+ """Return the final path segment of a module id."""
48
+ return module_id.rsplit("/", 1)[-1].strip()
49
+
50
+
51
+ def _module_id_matches(requested: str | None, discovered_id: str) -> bool:
52
+ """Return True when a requested module id refers to a discovered manifest id."""
53
+ if requested is None:
54
+ return False
55
+ requested_clean = requested.strip()
56
+ if "/" in requested_clean:
57
+ return requested_clean == discovered_id
58
+ return requested_clean == discovered_id or _module_id_tail(requested_clean) == _module_id_tail(discovered_id)
59
+
60
+
61
+ def _entry_matches(entry: DiscoveredModule, *, module_id: str | None, command_name: str | None) -> bool:
62
+ meta = entry.metadata
63
+ if _module_id_matches(module_id, meta.name):
64
+ return True
65
+ if command_name is None:
66
+ return False
67
+ return command_name in set(meta.commands) or getattr(meta, "bundle_group_command", None) == command_name
68
+
69
+
70
+ def _availability_matches(
71
+ discovered: list[DiscoveredModule],
72
+ *,
73
+ module_id: str | None,
74
+ command_name: str | None,
75
+ ) -> list[DiscoveredModule]:
76
+ module_matches = [entry for entry in discovered if _module_id_matches(module_id, entry.metadata.name)]
77
+ if module_matches:
78
+ return module_matches
79
+ if module_id is not None and "/" in module_id.strip():
80
+ return []
81
+ return [entry for entry in discovered if _entry_matches(entry, module_id=module_id, command_name=command_name)]
82
+
83
+
84
+ def _ambiguous_bare_module_id_match(
85
+ module_id: str | None,
86
+ matches: list[DiscoveredModule],
87
+ ) -> bool:
88
+ if module_id is None:
89
+ return False
90
+ requested = module_id.strip()
91
+ if not requested or "/" in requested:
92
+ return False
93
+ matched_ids = {entry.metadata.name for entry in matches}
94
+ return len(matched_ids) > 1
95
+
96
+
97
+ def _recovery_command(status: ModuleAvailabilityStatus, module_id: str) -> str:
98
+ if status is ModuleAvailabilityStatus.DISABLED:
99
+ return f"specfact module enable {module_id}"
100
+ if status is ModuleAvailabilityStatus.ABSENT:
101
+ return f"specfact module install {module_id}"
102
+ return ""
103
+
104
+
105
+ def _skip_reason(entry: DiscoveredModule, enabled_map: dict[str, bool]) -> str:
106
+ meta = entry.metadata
107
+ if not _check_core_compatibility(meta, cli_version):
108
+ return f"requires {meta.core_compatibility}, cli is {cli_version}"
109
+ deps_ok, missing = _validate_module_dependencies(meta, enabled_map)
110
+ if not deps_ok:
111
+ return f"missing dependencies: {', '.join(missing)}"
112
+ return ""
113
+
114
+
115
+ def _absent_availability(module_id: str | None, requested_id: str) -> ModuleAvailability:
116
+ return ModuleAvailability(
117
+ status=ModuleAvailabilityStatus.ABSENT,
118
+ module_id=module_id,
119
+ reason="not installed",
120
+ recovery_command=_recovery_command(ModuleAvailabilityStatus.ABSENT, requested_id) if requested_id else "",
121
+ )
122
+
123
+
124
+ def _ambiguous_availability(module_id: str) -> ModuleAvailability:
125
+ return ModuleAvailability(
126
+ status=ModuleAvailabilityStatus.AMBIGUOUS,
127
+ module_id=module_id,
128
+ reason="multiple installed modules share this short id; use namespace/name",
129
+ )
130
+
131
+
132
+ def _shadowed_duplicate(primary: DiscoveredModule, matches: list[DiscoveredModule]) -> DiscoveredModule | None:
133
+ duplicate = next((entry for entry in matches[1:] if entry.metadata.name == primary.metadata.name), None)
134
+ if duplicate is None:
135
+ return None
136
+ if primary.source == "project" and duplicate.source in {"user", "marketplace"}:
137
+ return duplicate
138
+ return None
139
+
140
+
141
+ def _shadowed_availability(primary: DiscoveredModule, duplicate: DiscoveredModule) -> ModuleAvailability:
142
+ return ModuleAvailability(
143
+ status=ModuleAvailabilityStatus.SHADOWED,
144
+ module_id=duplicate.metadata.name,
145
+ source=duplicate.source,
146
+ package_dir=duplicate.package_dir,
147
+ reason=f"shadowed by {primary.source} scope",
148
+ shadowed_by=primary.package_dir,
149
+ )
150
+
151
+
152
+ def _disabled_availability(primary: DiscoveredModule) -> ModuleAvailability:
153
+ module_name = primary.metadata.name
154
+ return ModuleAvailability(
155
+ status=ModuleAvailabilityStatus.DISABLED,
156
+ module_id=module_name,
157
+ source=primary.source,
158
+ package_dir=primary.package_dir,
159
+ reason="disabled in modules.json",
160
+ recovery_command=_recovery_command(ModuleAvailabilityStatus.DISABLED, module_name),
161
+ )
162
+
163
+
164
+ def _available_or_skipped_availability(
165
+ primary: DiscoveredModule,
166
+ enabled_map: dict[str, bool],
167
+ ) -> ModuleAvailability:
168
+ reason = _skip_reason(primary, enabled_map)
169
+ if reason:
170
+ return ModuleAvailability(
171
+ status=ModuleAvailabilityStatus.SKIPPED,
172
+ module_id=primary.metadata.name,
173
+ source=primary.source,
174
+ package_dir=primary.package_dir,
175
+ reason=reason,
176
+ )
177
+ return ModuleAvailability(
178
+ status=ModuleAvailabilityStatus.AVAILABLE,
179
+ module_id=primary.metadata.name,
180
+ source=primary.source,
181
+ package_dir=primary.package_dir,
182
+ )
183
+
184
+
185
+ @beartype
186
+ @require(
187
+ lambda module_id, command_name, base_path: bool(module_id or command_name), "module_id or command_name required"
188
+ )
189
+ @ensure(lambda result: isinstance(result, ModuleAvailability), "must return module availability")
190
+ def classify_module_availability(
191
+ *,
192
+ module_id: str | None = None,
193
+ command_name: str | None = None,
194
+ base_path: Path | None = None,
195
+ ) -> ModuleAvailability:
196
+ """Classify module availability using manifests and modules.json only."""
197
+ discovered = discover_all_modules_for_project_with_shadowed(base_path)
198
+ matches = _availability_matches(discovered, module_id=module_id, command_name=command_name)
199
+ requested_id = module_id or command_name or ""
200
+ if not matches:
201
+ return _absent_availability(module_id, requested_id)
202
+ if _ambiguous_bare_module_id_match(module_id, matches):
203
+ return _ambiguous_availability(requested_id.strip())
204
+
205
+ discovered_list = [(entry.metadata.name, entry.metadata.version) for entry in discovered]
206
+ enabled_map = merge_module_state(discovered_list, read_modules_state(), [], [])
207
+ primary = matches[0]
208
+ module_name = primary.metadata.name
209
+ duplicate = _shadowed_duplicate(primary, matches)
210
+ if duplicate is not None:
211
+ return _shadowed_availability(primary, duplicate)
212
+ if not enabled_map.get(module_name, True):
213
+ return _disabled_availability(primary)
214
+ return _available_or_skipped_availability(primary, enabled_map)