specfact-cli 0.47.6__tar.gz → 0.48.2__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.47.6 → specfact_cli-0.48.2}/.gitignore +1 -0
  2. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/PKG-INFO +1 -1
  3. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/pyproject.toml +3 -1
  4. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/resources/bundled-module-registry/index.json +6 -6
  5. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/__init__.py +1 -1
  6. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/__init__.py +1 -1
  7. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/adapters/ado.py +2 -2
  8. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/generators/persona_exporter.py +1 -1
  9. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/generators/plan_generator.py +1 -1
  10. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/generators/protocol_generator.py +1 -1
  11. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/generators/report_generator.py +1 -1
  12. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/generators/workflow_generator.py +1 -1
  13. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/modules/init/module-package.yaml +3 -3
  14. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/modules/init/src/commands.py +21 -10
  15. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/modules/init/src/first_run_selection.py +280 -2
  16. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/modules/module_registry/module-package.yaml +3 -3
  17. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/modules/module_registry/src/commands.py +12 -4
  18. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/registry/dependency_resolver.py +11 -8
  19. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/registry/module_installer.py +1 -1
  20. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/telemetry.py +10 -4
  21. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/suggestions.py +4 -4
  22. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/LICENSE +0 -0
  23. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/README.md +0 -0
  24. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/resources/keys/README.md +0 -0
  25. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/resources/keys/module-signing-public.pem +0 -0
  26. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/resources/mappings/node-async.yaml +0 -0
  27. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/resources/mappings/python-async.yaml +0 -0
  28. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/resources/mappings/speckit-default.yaml +0 -0
  29. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/resources/schemas/deviation.schema.json +0 -0
  30. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/resources/schemas/plan.schema.json +0 -0
  31. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/resources/schemas/protocol.schema.json +0 -0
  32. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/resources/templates/github-action.yml.j2 +0 -0
  33. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/resources/templates/persona/architect.md.j2 +0 -0
  34. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/resources/templates/persona/developer.md.j2 +0 -0
  35. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/resources/templates/persona/product-owner.md.j2 +0 -0
  36. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/resources/templates/plan.bundle.yaml.j2 +0 -0
  37. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/resources/templates/policies/kanban.yaml +0 -0
  38. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/resources/templates/policies/mixed.yaml +0 -0
  39. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/resources/templates/policies/safe.yaml +0 -0
  40. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/resources/templates/policies/scrum.yaml +0 -0
  41. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/resources/templates/pr-template.md.j2 +0 -0
  42. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/resources/templates/protocol.yaml.j2 +0 -0
  43. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/resources/templates/telemetry.yaml.example +0 -0
  44. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/__main__.py +0 -0
  45. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/adapters/__init__.py +0 -0
  46. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/adapters/backlog_base.py +0 -0
  47. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/adapters/base.py +0 -0
  48. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/adapters/github.py +0 -0
  49. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/adapters/openspec.py +0 -0
  50. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/adapters/openspec_parser.py +0 -0
  51. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/adapters/registry.py +0 -0
  52. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/adapters/speckit.py +0 -0
  53. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/agents/__init__.py +0 -0
  54. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/agents/analyze_agent.py +0 -0
  55. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/agents/base.py +0 -0
  56. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/agents/plan_agent.py +0 -0
  57. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/agents/registry.py +0 -0
  58. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/agents/sync_agent.py +0 -0
  59. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/analyzers/__init__.py +0 -0
  60. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/analyzers/ambiguity_scanner.py +0 -0
  61. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/analyzers/code_analyzer.py +0 -0
  62. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/analyzers/constitution_evidence_extractor.py +0 -0
  63. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/analyzers/contract_extractor.py +0 -0
  64. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/analyzers/control_flow_analyzer.py +0 -0
  65. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/analyzers/graph_analyzer.py +0 -0
  66. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/analyzers/relationship_mapper.py +0 -0
  67. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/analyzers/requirement_extractor.py +0 -0
  68. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/analyzers/test_pattern_extractor.py +0 -0
  69. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/backlog/__init__.py +0 -0
  70. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/backlog/adapters/__init__.py +0 -0
  71. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/backlog/adapters/base.py +0 -0
  72. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/backlog/converter.py +0 -0
  73. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/backlog/filters.py +0 -0
  74. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/backlog/mappers/__init__.py +0 -0
  75. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/backlog/mappers/ado_mapper.py +0 -0
  76. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/backlog/mappers/base.py +0 -0
  77. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/backlog/mappers/github_mapper.py +0 -0
  78. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/backlog/mappers/template_config.py +0 -0
  79. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/cli.py +0 -0
  80. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/commands/__init__.py +0 -0
  81. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/commands/_bundle_shim.py +0 -0
  82. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/commands/analyze.py +0 -0
  83. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/commands/contract_cmd.py +0 -0
  84. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/commands/drift.py +0 -0
  85. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/commands/enforce.py +0 -0
  86. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/commands/generate.py +0 -0
  87. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/commands/import_cmd.py +0 -0
  88. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/commands/init.py +0 -0
  89. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/commands/migrate.py +0 -0
  90. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/commands/plan.py +0 -0
  91. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/commands/project_cmd.py +0 -0
  92. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/commands/repro.py +0 -0
  93. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/commands/sdd.py +0 -0
  94. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/commands/spec.py +0 -0
  95. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/commands/sync.py +0 -0
  96. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/commands/update.py +0 -0
  97. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/commands/validate.py +0 -0
  98. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/common/__init__.py +0 -0
  99. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/common/bundle_factory.py +0 -0
  100. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/common/logger_setup.py +0 -0
  101. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/common/logging_utils.py +0 -0
  102. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/common/text_utils.py +0 -0
  103. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/common/utils.py +0 -0
  104. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/comparators/__init__.py +0 -0
  105. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/comparators/plan_comparator.py +0 -0
  106. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/contracts/__init__.py +0 -0
  107. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/contracts/crosshair_props.py +0 -0
  108. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/contracts/module_interface.py +0 -0
  109. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/enrichers/constitution_enricher.py +0 -0
  110. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/enrichers/plan_enricher.py +0 -0
  111. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/generators/__init__.py +0 -0
  112. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/generators/contract_generator.py +0 -0
  113. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/generators/openapi_extractor.py +0 -0
  114. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/generators/task_generator.py +0 -0
  115. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/generators/test_to_openapi.py +0 -0
  116. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/groups/__init__.py +0 -0
  117. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/groups/codebase_group.py +0 -0
  118. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/groups/govern_group.py +0 -0
  119. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/groups/member_group.py +0 -0
  120. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/groups/project_group.py +0 -0
  121. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/groups/spec_group.py +0 -0
  122. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/importers/__init__.py +0 -0
  123. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/importers/speckit_converter.py +0 -0
  124. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/importers/speckit_scanner.py +0 -0
  125. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/integrations/__init__.py +0 -0
  126. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/integrations/specmatic.py +0 -0
  127. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/merge/__init__.py +0 -0
  128. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/merge/resolver.py +0 -0
  129. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/migrations/__init__.py +0 -0
  130. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/migrations/plan_migrator.py +0 -0
  131. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/models/__init__.py +0 -0
  132. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/models/backlog_item.py +0 -0
  133. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/models/bridge.py +0 -0
  134. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/models/capabilities.py +0 -0
  135. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/models/change.py +0 -0
  136. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/models/contract.py +0 -0
  137. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/models/deviation.py +0 -0
  138. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/models/dor_config.py +0 -0
  139. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/models/enforcement.py +0 -0
  140. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/models/module_package.py +0 -0
  141. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/models/persona_template.py +0 -0
  142. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/models/plan.py +0 -0
  143. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/models/project.py +0 -0
  144. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/models/protocol.py +0 -0
  145. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/models/quality.py +0 -0
  146. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/models/sdd.py +0 -0
  147. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/models/source_tracking.py +0 -0
  148. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/models/task.py +0 -0
  149. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/models/validation.py +0 -0
  150. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/modes/__init__.py +0 -0
  151. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/modes/detector.py +0 -0
  152. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/modes/router.py +0 -0
  153. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/modules/__init__.py +0 -0
  154. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/modules/_bundle_import.py +0 -0
  155. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/modules/init/src/__init__.py +0 -0
  156. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/modules/init/src/app.py +0 -0
  157. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/modules/module_io_shim.py +0 -0
  158. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/modules/module_registry/src/__init__.py +0 -0
  159. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/modules/module_registry/src/app.py +0 -0
  160. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/modules/upgrade/module-package.yaml +0 -0
  161. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/modules/upgrade/src/__init__.py +0 -0
  162. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/modules/upgrade/src/app.py +0 -0
  163. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/modules/upgrade/src/commands.py +0 -0
  164. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/parsers/__init__.py +0 -0
  165. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/parsers/persona_importer.py +0 -0
  166. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/registry/__init__.py +0 -0
  167. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/registry/alias_manager.py +0 -0
  168. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/registry/bootstrap.py +0 -0
  169. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/registry/bridge_registry.py +0 -0
  170. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/registry/crypto_validator.py +0 -0
  171. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/registry/custom_registries.py +0 -0
  172. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/registry/extension_registry.py +0 -0
  173. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/registry/help_cache.py +0 -0
  174. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/registry/marketplace_client.py +0 -0
  175. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/registry/metadata.py +0 -0
  176. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/registry/module_availability.py +0 -0
  177. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/registry/module_discovery.py +0 -0
  178. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/registry/module_grouping.py +0 -0
  179. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/registry/module_lifecycle.py +0 -0
  180. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/registry/module_packages.py +0 -0
  181. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/registry/module_security.py +0 -0
  182. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/registry/module_state.py +0 -0
  183. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/registry/registry.py +0 -0
  184. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/resources/semgrep/async.yml +0 -0
  185. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/resources/semgrep/code-quality.yml +0 -0
  186. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/resources/semgrep/feature-detection.yml +0 -0
  187. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/runtime.py +0 -0
  188. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/sync/__init__.py +0 -0
  189. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/sync/bridge_probe.py +0 -0
  190. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/sync/bridge_sync.py +0 -0
  191. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/sync/bridge_sync_openspec_md_parse.py +0 -0
  192. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/sync/bridge_sync_requirement_from_proposal.py +0 -0
  193. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/sync/bridge_sync_requirement_helpers.py +0 -0
  194. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/sync/bridge_sync_tasks_from_proposal.py +0 -0
  195. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/sync/bridge_sync_what_changes_format.py +0 -0
  196. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/sync/bridge_sync_write_openspec_from_proposal.py +0 -0
  197. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/sync/bridge_watch.py +0 -0
  198. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/sync/change_detector.py +0 -0
  199. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/sync/code_to_spec.py +0 -0
  200. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/sync/drift_detector.py +0 -0
  201. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/sync/repository_sync.py +0 -0
  202. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/sync/spec_to_code.py +0 -0
  203. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/sync/spec_to_tests.py +0 -0
  204. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/sync/watcher.py +0 -0
  205. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/sync/watcher_enhanced.py +0 -0
  206. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/templates/__init__.py +0 -0
  207. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/templates/defaults/defect_v1.yaml +0 -0
  208. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/templates/defaults/enabler_v1.yaml +0 -0
  209. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/templates/defaults/spike_v1.yaml +0 -0
  210. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/templates/defaults/user_story_v1.yaml +0 -0
  211. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/templates/frameworks/scrum/user_story_v1.yaml +0 -0
  212. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/templates/personas/product-owner/user_story_v1.yaml +0 -0
  213. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/templates/providers/ado/work_item_v1.yaml +0 -0
  214. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/templates/registry.py +0 -0
  215. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/templates/specification_templates.py +0 -0
  216. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/__init__.py +0 -0
  217. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/acceptance_criteria.py +0 -0
  218. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/auth_tokens.py +0 -0
  219. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/bundle_converters.py +0 -0
  220. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/bundle_loader.py +0 -0
  221. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/code_change_detector.py +0 -0
  222. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/console.py +0 -0
  223. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/content_sanitizer.py +0 -0
  224. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/context_detection.py +0 -0
  225. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/contract_predicates.py +0 -0
  226. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/enrichment_context.py +0 -0
  227. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/enrichment_parser.py +0 -0
  228. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/env_manager.py +0 -0
  229. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/feature_keys.py +0 -0
  230. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/git.py +0 -0
  231. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/github_annotations.py +0 -0
  232. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/icontract_helpers.py +0 -0
  233. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/ide_setup.py +0 -0
  234. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/incremental_check.py +0 -0
  235. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/metadata.py +0 -0
  236. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/optional_deps.py +0 -0
  237. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/performance.py +0 -0
  238. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/persona_ownership.py +0 -0
  239. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/progress.py +0 -0
  240. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/progressive_disclosure.py +0 -0
  241. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/project_artifact_write.py +0 -0
  242. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/prompts.py +0 -0
  243. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/sdd_discovery.py +0 -0
  244. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/source_scanner.py +0 -0
  245. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/startup_checks.py +0 -0
  246. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/structure.py +0 -0
  247. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/structured_io.py +0 -0
  248. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/terminal.py +0 -0
  249. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/utils/yaml_utils.py +0 -0
  250. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/validation/__init__.py +0 -0
  251. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/validation/command_audit.py +0 -0
  252. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/validators/__init__.py +0 -0
  253. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/validators/agile_validation.py +0 -0
  254. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/validators/change_proposal_integration.py +0 -0
  255. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/validators/cli_first_validator.py +0 -0
  256. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/validators/contract_validator.py +0 -0
  257. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/validators/fsm.py +0 -0
  258. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/validators/repro_checker.py +0 -0
  259. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/validators/schema.py +0 -0
  260. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/validators/sidecar/__init__.py +0 -0
  261. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/validators/sidecar/contract_populator.py +0 -0
  262. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/validators/sidecar/crosshair_runner.py +0 -0
  263. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/validators/sidecar/crosshair_summary.py +0 -0
  264. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/validators/sidecar/dependency_installer.py +0 -0
  265. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/validators/sidecar/framework_detector.py +0 -0
  266. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/validators/sidecar/frameworks/__init__.py +0 -0
  267. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/validators/sidecar/frameworks/base.py +0 -0
  268. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/validators/sidecar/frameworks/django.py +0 -0
  269. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/validators/sidecar/frameworks/drf.py +0 -0
  270. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/validators/sidecar/frameworks/fastapi.py +0 -0
  271. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/validators/sidecar/frameworks/flask.py +0 -0
  272. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/validators/sidecar/harness_generator.py +0 -0
  273. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/validators/sidecar/models.py +0 -0
  274. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/validators/sidecar/orchestrator.py +0 -0
  275. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/validators/sidecar/specmatic_runner.py +0 -0
  276. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/validators/sidecar/unannotated_detector.py +0 -0
  277. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/versioning/__init__.py +0 -0
  278. {specfact_cli-0.47.6 → specfact_cli-0.48.2}/src/specfact_cli/versioning/analyzer.py +0 -0
@@ -118,6 +118,7 @@ docs/internal/
118
118
  .codex/skills/openspec-*/
119
119
  !.codex/skills/openspec-workflows/
120
120
  .agents/skills/
121
+ !.agents/skills/openspec-workflows/
121
122
  skills-lock.json
122
123
 
123
124
  .vibe/skills/openspec-*/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specfact-cli
3
- Version: 0.47.6
3
+ Version: 0.48.2
4
4
  Summary: AI-bloat defense CLI for Python teams. Run deterministic code review, cleanup forecasts, and spec/contract evidence for AI-assisted and brownfield delivery.
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.47.6"
7
+ version = "0.48.2"
8
8
  description = "AI-bloat defense CLI for Python teams. Run deterministic code review, cleanup forecasts, and spec/contract evidence for AI-assisted and brownfield delivery."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -234,6 +234,8 @@ format = "ruff check . --fix && ruff format ."
234
234
  # Code scanning (Semgrep)
235
235
  # Semgrep 1.38+ deprecated implicit .semgrep.yml discovery; always pass --config explicitly.
236
236
  # Script must not be named `semgrep` (Hatch treats that as circular expansion with the semgrep CLI).
237
+ semgrep-sast = "semgrep scan --config tools/semgrep/sast.yml {args}"
238
+ semgrep-sast-gate = "python scripts/semgrep_sast_gate.py {args}"
237
239
  semgrep-full = "semgrep --config tools/semgrep {args}"
238
240
  scan = "semgrep --config tools/semgrep/async.yml {args}"
239
241
  scan-all = "semgrep --config tools/semgrep/async.yml ."
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "modules": [
3
3
  {
4
- "checksum_sha256": "3a563335d7a5e16e6e9ec975d6e5c77ff550d8880474eca03713b43e97d39a4a",
5
- "download_url": "https://github.com/nold-ai/specfact-cli/releases/download/init-0.1.37.tar.gz",
4
+ "checksum_sha256": "2ffe401e068883086f2df2beed8e9d328db9f0802deb3c69212b0594bfa1be75",
5
+ "download_url": "https://github.com/nold-ai/specfact-cli/releases/download/init-0.1.41.tar.gz",
6
6
  "id": "init",
7
- "latest_version": "0.1.37"
7
+ "latest_version": "0.1.41"
8
8
  },
9
9
  {
10
10
  "checksum_sha256": "35c592374b209224af9a826c21efa66947cc16bf680b20ee0463f44bee4f9b59",
@@ -13,10 +13,10 @@
13
13
  "latest_version": "0.1.21"
14
14
  },
15
15
  {
16
- "checksum_sha256": "79359da27cb1c734a928a453a786b626e7b06654f515f32ead0cf96fa8e70bc2",
17
- "download_url": "https://github.com/nold-ai/specfact-cli/releases/download/module-registry-0.1.26.tar.gz",
16
+ "checksum_sha256": "2552567a090d15826d8439ffb9242e0bf002bbe92296bc894e17ca0ba6a773f8",
17
+ "download_url": "https://github.com/nold-ai/specfact-cli/releases/download/module-registry-0.1.32.tar.gz",
18
18
  "id": "module-registry",
19
- "latest_version": "0.1.26"
19
+ "latest_version": "0.1.32"
20
20
  },
21
21
  {
22
22
  "id": "bundle-mapper",
@@ -3,4 +3,4 @@ SpecFact CLI - AI-bloat defense CLI for Python teams.
3
3
  """
4
4
 
5
5
  # Package version: keep in sync with pyproject.toml, setup.py, src/specfact_cli/__init__.py
6
- __version__ = "0.47.6"
6
+ __version__ = "0.48.2"
@@ -76,6 +76,6 @@ def _install_progressive_disclosure() -> None:
76
76
  # keeps missing-command and missing-parameter UX consistent outside the root CLI too.
77
77
  _install_progressive_disclosure()
78
78
 
79
- __version__ = "0.47.6"
79
+ __version__ = "0.48.2"
80
80
 
81
81
  __all__ = ["__version__"]
@@ -1986,7 +1986,7 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
1986
1986
  return cast(
1987
1987
  Any,
1988
1988
  self._request_with_retry(
1989
- lambda: requests.post(url, **request_kwargs),
1989
+ lambda: requests.post(url, **request_kwargs), # nosec B113 - request_kwargs always includes timeout.
1990
1990
  retry_on_ambiguous_transport=retry_on_ambiguous_transport,
1991
1991
  ),
1992
1992
  )
@@ -2129,7 +2129,7 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
2129
2129
  change_id_escaped = change_id.replace("'", "''")
2130
2130
  wiql = {
2131
2131
  "query": (
2132
- "Select [System.Id] From WorkItems "
2132
+ "Select [System.Id] From WorkItems " # nosec B608 - WIQL uses escaped literals, not SQL.
2133
2133
  f"Where [System.TeamProject] = '{project_escaped}' "
2134
2134
  f"And [System.Description] Contains 'OpenSpec Change Proposal: `{change_id_escaped}`'"
2135
2135
  )
@@ -90,7 +90,7 @@ class PersonaExporter:
90
90
  self.project_templates_dir = project_templates_dir
91
91
 
92
92
  # Create Jinja2 environment with fallback support
93
- self.env = Environment(
93
+ self.env = Environment( # nosec B701 - exports trusted Markdown templates, not HTML.
94
94
  loader=FileSystemLoader(
95
95
  [str(self.templates_dir)] + ([str(self.project_templates_dir)] if project_templates_dir else [])
96
96
  ),
@@ -33,7 +33,7 @@ class PlanGenerator:
33
33
  templates_dir = Path(__file__).parent.parent.parent.parent / "resources" / "templates"
34
34
 
35
35
  self.templates_dir = Path(templates_dir)
36
- self.env = Environment(
36
+ self.env = Environment( # nosec B701 - generates trusted text/YAML workflow templates, not HTML.
37
37
  loader=FileSystemLoader(self.templates_dir),
38
38
  # Must be False: trim_blocks removes the newline after {% endraw %}, merging `if:` with `run:`.
39
39
  trim_blocks=False,
@@ -32,7 +32,7 @@ class ProtocolGenerator:
32
32
  templates_dir = Path(__file__).parent.parent.parent.parent / "resources" / "templates"
33
33
 
34
34
  self.templates_dir = Path(templates_dir)
35
- self.env = Environment(
35
+ self.env = Environment( # nosec B701 - generates trusted protocol Markdown, not HTML.
36
36
  loader=FileSystemLoader(self.templates_dir),
37
37
  trim_blocks=True,
38
38
  lstrip_blocks=True,
@@ -43,7 +43,7 @@ class ReportGenerator:
43
43
  templates_dir = Path(__file__).parent.parent.parent.parent / "resources" / "templates"
44
44
 
45
45
  self.templates_dir = Path(templates_dir)
46
- self.env = Environment(
46
+ self.env = Environment( # nosec B701 - generates trusted validation reports, not HTML.
47
47
  loader=FileSystemLoader(self.templates_dir),
48
48
  trim_blocks=True,
49
49
  lstrip_blocks=True,
@@ -38,7 +38,7 @@ class WorkflowGenerator:
38
38
  templates_dir = Path(__file__).parent.parent.parent.parent / "resources" / "templates"
39
39
 
40
40
  self.templates_dir = Path(templates_dir)
41
- self.env = Environment(
41
+ self.env = Environment( # nosec B701 - generates trusted text/YAML workflow templates, not HTML.
42
42
  loader=FileSystemLoader(self.templates_dir),
43
43
  # Must be False: trim_blocks removes the newline after {% endraw %}, merging `if:` with `run:`.
44
44
  trim_blocks=False,
@@ -1,5 +1,5 @@
1
1
  name: init
2
- version: 0.1.37
2
+ version: 0.1.41
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:8741e8fa3408bd38e7d65d0784a25466369955dd9f16f1981a52c0e1550afbe0
21
- signature: kPAMdqpcAr92gr+DwxWibXfD0mgCLSb78EwGzoXH3fHct7xss8zt/SFvJLVZ0qkYHA8/4Nu6ls0iihRN9DXhCw==
20
+ checksum: sha256:7ad9c0b623f52a55b45f35adf9576bea1fd47edcd3005ed2354188a6a6ea20f5
21
+ signature: 7ED3ulYnu2VsJvPN/rEVLFHdVdBoiqJg1a8kR1FpWTmz9ZILrT/+NYq3CtnwGIvZscD/8CEBLBqKt1+0fCqZCQ==
@@ -48,14 +48,7 @@ from specfact_cli.utils.ide_setup import (
48
48
  )
49
49
 
50
50
 
51
- VALID_PROFILES: frozenset[str] = frozenset(
52
- {
53
- "solo-developer",
54
- "backlog-team",
55
- "api-first-team",
56
- "enterprise-full-stack",
57
- }
58
- )
51
+ VALID_PROFILES: frozenset[str] = frozenset(first_run_selection.get_valid_profile_names())
59
52
  PROFILE_BUNDLES: dict[str, list[str]] = first_run_selection.PROFILE_PRESETS
60
53
 
61
54
  install_bundles_for_init = first_run_selection.install_bundles_for_init
@@ -709,6 +702,21 @@ def init_ide(
709
702
  console.print(f"[green]Updated VS Code settings:[/green] {settings_path}")
710
703
 
711
704
 
705
+ def _write_profile_config_or_exit(repo_path: Path, profile: str) -> None:
706
+ try:
707
+ first_run_selection.write_profile_config(repo_path, profile)
708
+ except ValueError as e:
709
+ console.print(f"[red]Error:[/red] {e}")
710
+ raise typer.Exit(1) from e
711
+
712
+
713
+ def _apply_explicit_profile_or_install(repo_path: Path, profile: str | None, install: str | None) -> list[str]:
714
+ enabled_module_ids = _apply_profile_or_install_bundles(profile, install)
715
+ if profile is not None:
716
+ _write_profile_config_or_exit(repo_path, profile)
717
+ return enabled_module_ids
718
+
719
+
712
720
  @app.callback(invoke_without_command=True)
713
721
  @require(lambda repo: _is_valid_repo_path(repo), "Repo path must exist and be directory")
714
722
  @ensure(lambda result: result is None, "Command should return None")
@@ -725,7 +733,10 @@ def init(
725
733
  profile: str | None = typer.Option(
726
734
  None,
727
735
  "--profile",
728
- help="First-run profile preset: solo-developer, backlog-team, api-first-team, enterprise-full-stack",
736
+ help=(
737
+ "Validation tier or legacy workflow preset: solo, startup, mid_size, enterprise, "
738
+ "solo-developer, backlog-team, api-first-team, enterprise-full-stack"
739
+ ),
729
740
  ),
730
741
  install: str | None = typer.Option(
731
742
  None,
@@ -750,7 +761,7 @@ def init(
750
761
 
751
762
  enabled_module_ids: list[str] = []
752
763
  if profile is not None or install is not None:
753
- enabled_module_ids = _apply_profile_or_install_bundles(profile, install)
764
+ enabled_module_ids = _apply_explicit_profile_or_install(repo_path, profile, install)
754
765
  elif is_first_run(user_root=INIT_USER_MODULES_ROOT) and is_non_interactive():
755
766
  console.print(
756
767
  "[red]Error:[/red] In CI/CD (non-interactive) mode, first-run init requires "
@@ -3,10 +3,13 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import os
6
+ from collections.abc import Mapping
7
+ from copy import deepcopy
6
8
  from dataclasses import dataclass
7
9
  from pathlib import Path
8
- from typing import Any
10
+ from typing import Any, cast
9
11
 
12
+ import yaml
10
13
  from beartype import beartype
11
14
  from icontract import ensure, require
12
15
 
@@ -25,8 +28,118 @@ PROFILE_PRESETS: dict[str, list[str]] = {
25
28
  "specfact-spec",
26
29
  "specfact-govern",
27
30
  ],
31
+ "solo": ["specfact-codebase", "specfact-code-review"],
32
+ "startup": ["specfact-project", "specfact-backlog", "specfact-codebase", "specfact-code-review"],
33
+ "mid_size": ["specfact-project", "specfact-backlog", "specfact-codebase", "specfact-spec", "specfact-code-review"],
34
+ "enterprise": [
35
+ "specfact-project",
36
+ "specfact-backlog",
37
+ "specfact-codebase",
38
+ "specfact-spec",
39
+ "specfact-govern",
40
+ "specfact-code-review",
41
+ ],
42
+ }
43
+
44
+ VALIDATION_TIER_PROFILES: tuple[str, ...] = ("solo", "startup", "mid_size", "enterprise")
45
+
46
+ LEGACY_PROFILE_TO_VALIDATION_TIER: dict[str, str] = {
47
+ "solo-developer": "solo",
48
+ "backlog-team": "startup",
49
+ "api-first-team": "mid_size",
50
+ "enterprise-full-stack": "enterprise",
51
+ }
52
+
53
+
54
+ def _canonical_module_ids(bundle_ids: list[str]) -> list[str]:
55
+ return [f"nold-ai/{bundle_id}" for bundle_id in bundle_ids]
56
+
57
+
58
+ _PROFILE_DEFAULTS: dict[str, dict[str, Any]] = {
59
+ "solo": {
60
+ "profile": "solo",
61
+ "validation": {
62
+ "severity": "advisory",
63
+ "policy_mode": "advisory",
64
+ "evidence_persistence": "local",
65
+ },
66
+ "clean_code": {
67
+ "mode": "advisory",
68
+ },
69
+ "modules": {
70
+ "enabled": _canonical_module_ids(PROFILE_PRESETS["solo"]),
71
+ },
72
+ "requirements_schema": {
73
+ "required_fields": ["id", "title", "acceptance"],
74
+ },
75
+ },
76
+ "startup": {
77
+ "profile": "startup",
78
+ "validation": {
79
+ "severity": "mixed",
80
+ "policy_mode": "mixed",
81
+ "evidence_persistence": "repo",
82
+ },
83
+ "clean_code": {
84
+ "mode": "advisory_then_mixed",
85
+ },
86
+ "modules": {
87
+ "enabled": _canonical_module_ids(PROFILE_PRESETS["startup"]),
88
+ },
89
+ "requirements_schema": {
90
+ "required_fields": ["id", "title", "owner", "acceptance"],
91
+ },
92
+ },
93
+ "mid_size": {
94
+ "profile": "mid_size",
95
+ "validation": {
96
+ "severity": "mixed",
97
+ "policy_mode": "mixed",
98
+ "evidence_persistence": "repo",
99
+ },
100
+ "clean_code": {
101
+ "mode": "mixed",
102
+ },
103
+ "modules": {
104
+ "enabled": _canonical_module_ids(PROFILE_PRESETS["mid_size"]),
105
+ },
106
+ "requirements_schema": {
107
+ "required_fields": ["id", "title", "owner", "acceptance", "trace_links"],
108
+ },
109
+ },
110
+ "enterprise": {
111
+ "profile": "enterprise",
112
+ "validation": {
113
+ "severity": "hard",
114
+ "policy_mode": "hard",
115
+ "evidence_persistence": "required",
116
+ },
117
+ "clean_code": {
118
+ "mode": "hard",
119
+ },
120
+ "modules": {
121
+ "enabled": _canonical_module_ids(PROFILE_PRESETS["enterprise"]),
122
+ },
123
+ "requirements_schema": {
124
+ "required_fields": [
125
+ "id",
126
+ "title",
127
+ "owner",
128
+ "acceptance",
129
+ "trace_links",
130
+ "risk_classification",
131
+ "exception_evidence",
132
+ ],
133
+ },
134
+ },
28
135
  }
29
136
 
137
+ _RESERVED_CONFIG_KEYS: frozenset[str] = frozenset({"source_annotations", "profile_warnings"})
138
+ _PROFILE_GENERATED_CONFIG_KEYS: frozenset[str] = frozenset(
139
+ {"validation", "clean_code", "modules", "requirements_schema"}
140
+ )
141
+ _POLICY_STRENGTH: dict[str, int] = {"advisory": 1, "advisory_then_mixed": 2, "mixed": 3, "hard": 4}
142
+
30
143
  _INSTALL_ALL_BUNDLES: tuple[str, ...] = (
31
144
  "specfact-project",
32
145
  "specfact-backlog",
@@ -53,6 +166,8 @@ BUNDLE_ALIAS_TO_CANONICAL: dict[str, str] = {
53
166
  "backlog": "specfact-backlog",
54
167
  "codebase": "specfact-codebase",
55
168
  "code": "specfact-codebase",
169
+ "code-review": "specfact-code-review",
170
+ "code_review": "specfact-code-review",
56
171
  "spec": "specfact-spec",
57
172
  "govern": "specfact-govern",
58
173
  }
@@ -83,6 +198,15 @@ BUNDLE_DISPLAY: dict[str, str] = {
83
198
  }
84
199
 
85
200
 
201
+ @dataclass(frozen=True)
202
+ class ResolvedProfileConfig:
203
+ """Resolved profile config with source annotations for each winning value."""
204
+
205
+ values: dict[str, Any]
206
+ sources: dict[str, Any]
207
+ warnings: list[str]
208
+
209
+
86
210
  def _emit_init_bundle_progress() -> bool:
87
211
  """Return True when init should print progress (suppressed during pytest)."""
88
212
  return os.environ.get("PYTEST_CURRENT_TEST") is None
@@ -107,6 +231,160 @@ def _expand_bundle_install_order(bundle_ids: list[str]) -> list[str]:
107
231
  return to_install
108
232
 
109
233
 
234
+ def _normalize_profile_key(profile: str) -> str:
235
+ key = profile.strip().lower()
236
+ return "mid_size" if key in {"mid-size", "mid_size"} else key
237
+
238
+
239
+ @require(lambda profile: isinstance(profile, str) and profile.strip() != "", "profile must be non-empty string")
240
+ @ensure(lambda result: result in VALIDATION_TIER_PROFILES, "result must be a validation tier")
241
+ @beartype
242
+ def resolve_validation_tier(profile: str) -> str:
243
+ """Map a validation tier or legacy workflow preset to a validation config tier."""
244
+ key = profile.strip().lower()
245
+ normalized = _normalize_profile_key(profile)
246
+ if normalized in VALIDATION_TIER_PROFILES:
247
+ return normalized
248
+ if key in LEGACY_PROFILE_TO_VALIDATION_TIER:
249
+ return LEGACY_PROFILE_TO_VALIDATION_TIER[key]
250
+ valid = ", ".join(get_valid_profile_names())
251
+ raise ValueError(f"Unknown profile {profile!r}. Valid profiles: {valid}")
252
+
253
+
254
+ def _source_tree(value: Any, source: str) -> Any:
255
+ if isinstance(value, Mapping):
256
+ return {str(key): _source_tree(child, source) for key, child in value.items()}
257
+ return source
258
+
259
+
260
+ def _merge_config_layer(target: dict[str, Any], sources: dict[str, Any], layer: Mapping[str, Any], source: str) -> None:
261
+ for raw_key, raw_value in layer.items():
262
+ key = str(raw_key)
263
+ if key in _RESERVED_CONFIG_KEYS:
264
+ continue
265
+ value = deepcopy(raw_value)
266
+ if isinstance(value, Mapping) and isinstance(target.get(key), dict):
267
+ existing_source = sources.get(key)
268
+ if not isinstance(existing_source, dict):
269
+ sources[key] = {}
270
+ _merge_config_layer(target[key], sources[key], value, source)
271
+ continue
272
+ target[key] = value
273
+ sources[key] = _source_tree(value, source)
274
+
275
+
276
+ def _extract_policy_value(layer: Mapping[str, Any] | None, key: str) -> str | None:
277
+ if not layer:
278
+ return None
279
+ validation = layer.get("validation")
280
+ if not isinstance(validation, Mapping):
281
+ return None
282
+ validation_values: dict[str, Any] = {str(raw_key): raw_value for raw_key, raw_value in validation.items()}
283
+ value = validation_values.get(key)
284
+ return value if isinstance(value, str) else None
285
+
286
+
287
+ def _local_policy_weakened(org_baseline: Mapping[str, Any] | None, developer_local: Mapping[str, Any] | None) -> bool:
288
+ for key in ("severity", "policy_mode"):
289
+ org_value = _extract_policy_value(org_baseline, key)
290
+ local_value = _extract_policy_value(developer_local, key)
291
+ if org_value is None or local_value is None:
292
+ continue
293
+ if _POLICY_STRENGTH.get(local_value, 0) < _POLICY_STRENGTH.get(org_value, 0):
294
+ return True
295
+ return False
296
+
297
+
298
+ @require(lambda profile: isinstance(profile, str) and profile.strip() != "", "profile must be non-empty string")
299
+ @ensure(lambda result: isinstance(result, ResolvedProfileConfig), "result must be a resolved profile config")
300
+ @beartype
301
+ def resolve_profile_config(
302
+ profile: str,
303
+ *,
304
+ org_baseline: Mapping[str, Any] | None = None,
305
+ repo_overlay: Mapping[str, Any] | None = None,
306
+ developer_local: Mapping[str, Any] | None = None,
307
+ ) -> ResolvedProfileConfig:
308
+ """Resolve profile defaults, org baseline, repo overlay, and developer-local config layers."""
309
+ tier = resolve_validation_tier(profile)
310
+ values: dict[str, Any] = {}
311
+ sources: dict[str, Any] = {}
312
+ _merge_config_layer(values, sources, _PROFILE_DEFAULTS[tier], f"profile:{tier}")
313
+ for layer, source in (
314
+ (org_baseline, "org_baseline"),
315
+ (repo_overlay, "repo_overlay"),
316
+ (developer_local, "developer_local"),
317
+ ):
318
+ if layer:
319
+ _merge_config_layer(values, sources, layer, source)
320
+
321
+ warnings: list[str] = []
322
+ if _local_policy_weakened(org_baseline, developer_local):
323
+ warnings.append("developer_local weakens org validation policy")
324
+ return ResolvedProfileConfig(values=values, sources=sources, warnings=warnings)
325
+
326
+
327
+ def _read_yaml_mapping(path: Path) -> dict[str, Any]:
328
+ if not path.exists():
329
+ return {}
330
+ raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
331
+ if not isinstance(raw, dict):
332
+ raise ValueError(f"Config file must contain a mapping: {path}")
333
+ return raw
334
+
335
+
336
+ def _source_tree_contains_profile(value: Any) -> bool:
337
+ if isinstance(value, str):
338
+ return value.startswith("profile:")
339
+ if isinstance(value, Mapping):
340
+ return any(_source_tree_contains_profile(child) for child in value.values())
341
+ return False
342
+
343
+
344
+ def _repo_overlay_without_generated_profile_values(raw_overlay: Mapping[str, Any]) -> dict[str, Any]:
345
+ overlay = {str(key): deepcopy(value) for key, value in raw_overlay.items()}
346
+ overlay.pop("profile", None)
347
+ source_annotations = overlay.pop("source_annotations", {})
348
+ overlay.pop("profile_warnings", None)
349
+ if not isinstance(source_annotations, Mapping):
350
+ return overlay
351
+ sources_by_key = {str(key): cast(Any, value) for key, value in source_annotations.items()}
352
+ for key in _PROFILE_GENERATED_CONFIG_KEYS:
353
+ if _source_tree_contains_profile(sources_by_key.get(key)):
354
+ overlay.pop(key, None)
355
+ return overlay
356
+
357
+
358
+ @require(lambda repo_path: isinstance(repo_path, Path), "repo_path must be Path")
359
+ @require(lambda profile: isinstance(profile, str) and profile.strip() != "", "profile must be non-empty string")
360
+ @ensure(lambda result: isinstance(result, ResolvedProfileConfig), "result must be a resolved profile config")
361
+ @beartype
362
+ def write_profile_config(repo_path: Path, profile: str) -> ResolvedProfileConfig:
363
+ """Write `.specfact/config.yaml` for the selected profile with source annotations."""
364
+ specfact_dir = repo_path / ".specfact"
365
+ config_path = specfact_dir / "config.yaml"
366
+ local_path = specfact_dir / "config.local.yaml"
367
+ org_path = Path.home() / ".specfact" / "config.yaml"
368
+
369
+ repo_overlay = _repo_overlay_without_generated_profile_values(_read_yaml_mapping(config_path))
370
+ resolved = resolve_profile_config(
371
+ profile,
372
+ org_baseline=_read_yaml_mapping(org_path),
373
+ repo_overlay=repo_overlay,
374
+ developer_local=_read_yaml_mapping(local_path),
375
+ )
376
+
377
+ payload = deepcopy(resolved.values)
378
+ payload["profile"] = resolve_validation_tier(profile)
379
+ payload["source_annotations"] = resolved.sources
380
+ if resolved.warnings:
381
+ payload["profile_warnings"] = resolved.warnings
382
+
383
+ specfact_dir.mkdir(parents=True, exist_ok=True)
384
+ config_path.write_text(yaml.safe_dump(payload, sort_keys=False, allow_unicode=False), encoding="utf-8")
385
+ return resolved
386
+
387
+
110
388
  @dataclass
111
389
  class _InitBundleInstallDeps:
112
390
  root: Path
@@ -235,7 +513,7 @@ def _process_one_bundle_install_row(bid: str, deps: _InitBundleInstallDeps) -> N
235
513
  @beartype
236
514
  def resolve_profile_bundles(profile: str) -> list[str]:
237
515
  """Resolve a profile name to the list of canonical bundle ids to install."""
238
- key = profile.strip().lower()
516
+ key = _normalize_profile_key(profile)
239
517
  if key not in PROFILE_PRESETS:
240
518
  valid = ", ".join(sorted(PROFILE_PRESETS))
241
519
  raise ValueError(f"Unknown profile {profile!r}. Valid profiles: {valid}")
@@ -1,5 +1,5 @@
1
1
  name: module-registry
2
- version: 0.1.26
2
+ version: 0.1.32
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:52a65869fb3d2d36910308c954e0ba199cb1c73f24136ee0d4aa7254f0996a22
21
- signature: Uo2eJ40HiWBqe77N8gjiDg7zGhsvJE/hN82+Jcto7yUoqbH7bidLw9DfVQ0CrJfgM9Y0XHAUS1+LMyPr1jAaBg==
20
+ checksum: sha256:a707de59cb2a706bad96a30415b693df0905cc67edf9238c0e9d601cc5b6af70
21
+ signature: GREOnCQAXmZvwN0s+2T6wwhPFTYiDchy/V7uut9uMHZjevK7ZDBxbTPZJhGjb1ndX7pkaL+/TfyNOuh/9AUDDw==
@@ -16,7 +16,7 @@ import typer
16
16
  import yaml
17
17
  from beartype import beartype
18
18
  from click.exceptions import Exit as ClickExit
19
- from icontract import require
19
+ from icontract import ensure, require
20
20
  from packaging.version import InvalidVersion, Version
21
21
  from rich.console import Console
22
22
  from rich.table import Table
@@ -54,13 +54,21 @@ from specfact_cli.registry.module_packages import get_discovered_modules_for_sta
54
54
  from specfact_cli.registry.module_security import ensure_publisher_trusted, is_official_publisher
55
55
  from specfact_cli.registry.module_state import read_modules_state, write_modules_state
56
56
  from specfact_cli.registry.registry import CommandRegistry
57
- from specfact_cli.runtime import is_non_interactive
57
+ from specfact_cli.runtime import is_non_interactive, refresh_loaded_module_consoles
58
58
 
59
59
 
60
60
  app = typer.Typer(help="Manage marketplace modules")
61
61
  console = Console()
62
62
 
63
63
 
64
+ @app.callback()
65
+ @beartype
66
+ @ensure(lambda result: result is None, "module registry callback returns None")
67
+ def module_registry_callback() -> None:
68
+ """Prepare invocation-local output streams for direct module command tests."""
69
+ refresh_loaded_module_consoles()
70
+
71
+
64
72
  def _module_upgrade_show_spinner() -> bool:
65
73
  """Rich Live/spinner breaks some tests; mirror ``utils.progress`` test-mode detection."""
66
74
  return os.environ.get("TEST_MODE") != "true" and os.environ.get("PYTEST_CURRENT_TEST") is None
@@ -386,10 +394,10 @@ def _install_one(module_id: str, params: _InstallOneParams) -> bool:
386
394
  except Exception as exc:
387
395
  console.print(f"[red]Failed installing {normalized}: {exc}[/red]")
388
396
  return False
389
- console.print(f"[green]Installed[/green] {normalized} -> {installed_path}")
397
+ typer.echo(f"Installed {normalized} -> {installed_path}")
390
398
  publisher = _publisher_from_module_id(normalized)
391
399
  if is_official_publisher(publisher):
392
- console.print(f"Verified: official ({publisher})")
400
+ typer.echo(f"Verified: official ({publisher})")
393
401
  return True
394
402
 
395
403
 
@@ -127,15 +127,18 @@ def _collect_constraints(modules: list[ModulePackageMetadata]) -> list[str]:
127
127
  seen: set[str] = set()
128
128
  for meta in modules:
129
129
  for d in meta.pip_dependencies or []:
130
- if d.strip() and d not in seen:
131
- constraints.append(d.strip())
132
- seen.add(d)
130
+ normalized = d.strip()
131
+ if normalized and normalized not in seen:
132
+ constraints.append(normalized)
133
+ seen.add(normalized)
133
134
  for vd in meta.pip_dependencies_versioned or []:
134
- spec = vd.version_specifier or ""
135
- s = f"{vd.name}{spec}" if spec else vd.name
136
- if s not in seen:
137
- constraints.append(s)
138
- seen.add(s)
135
+ name = vd.name.strip()
136
+ spec = (vd.version_specifier or "").strip()
137
+ s = f"{name}{spec}" if spec else name
138
+ normalized = s.strip()
139
+ if normalized and normalized not in seen:
140
+ constraints.append(normalized)
141
+ seen.add(normalized)
139
142
  return constraints
140
143
 
141
144
 
@@ -825,7 +825,7 @@ def _extract_marketplace_archive(archive_path: Path, extract_root: Path) -> None
825
825
  try:
826
826
  archive.extractall(path=extract_root, members=members, filter="data")
827
827
  except TypeError:
828
- archive.extractall(path=extract_root, members=members)
828
+ archive.extractall(path=extract_root, members=members) # nosec B202 - members were validated above.
829
829
 
830
830
 
831
831
  def _load_first_extracted_module_manifest(extract_root: Path) -> tuple[Path, dict[str, Any]]: