specfact-cli 0.49.1__tar.gz → 0.50.1__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 (281) hide show
  1. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/PKG-INFO +1 -1
  2. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/pyproject.toml +1 -1
  3. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/__init__.py +1 -1
  4. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/__init__.py +1 -1
  5. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/models/project.py +10 -1
  6. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/registry/module_grouping.py +8 -1
  7. specfact_cli-0.50.1/src/specfact_cli/requirements/__init__.py +28 -0
  8. specfact_cli-0.50.1/src/specfact_cli/requirements/context.py +330 -0
  9. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/.gitignore +0 -0
  10. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/LICENSE +0 -0
  11. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/README.md +0 -0
  12. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/resources/bundled-module-registry/index.json +0 -0
  13. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/resources/keys/README.md +0 -0
  14. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/resources/keys/module-signing-public.pem +0 -0
  15. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/resources/mappings/node-async.yaml +0 -0
  16. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/resources/mappings/python-async.yaml +0 -0
  17. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/resources/mappings/speckit-default.yaml +0 -0
  18. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/resources/schemas/deviation.schema.json +0 -0
  19. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/resources/schemas/plan.schema.json +0 -0
  20. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/resources/schemas/protocol.schema.json +0 -0
  21. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/resources/templates/github-action.yml.j2 +0 -0
  22. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/resources/templates/persona/architect.md.j2 +0 -0
  23. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/resources/templates/persona/developer.md.j2 +0 -0
  24. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/resources/templates/persona/product-owner.md.j2 +0 -0
  25. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/resources/templates/plan.bundle.yaml.j2 +0 -0
  26. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/resources/templates/policies/kanban.yaml +0 -0
  27. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/resources/templates/policies/mixed.yaml +0 -0
  28. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/resources/templates/policies/safe.yaml +0 -0
  29. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/resources/templates/policies/scrum.yaml +0 -0
  30. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/resources/templates/pr-template.md.j2 +0 -0
  31. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/resources/templates/protocol.yaml.j2 +0 -0
  32. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/resources/templates/telemetry.yaml.example +0 -0
  33. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/__main__.py +0 -0
  34. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/adapters/__init__.py +0 -0
  35. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/adapters/ado.py +0 -0
  36. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/adapters/backlog_base.py +0 -0
  37. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/adapters/base.py +0 -0
  38. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/adapters/github.py +0 -0
  39. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/adapters/openspec.py +0 -0
  40. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/adapters/openspec_parser.py +0 -0
  41. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/adapters/registry.py +0 -0
  42. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/adapters/speckit.py +0 -0
  43. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/agents/__init__.py +0 -0
  44. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/agents/analyze_agent.py +0 -0
  45. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/agents/base.py +0 -0
  46. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/agents/plan_agent.py +0 -0
  47. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/agents/registry.py +0 -0
  48. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/agents/sync_agent.py +0 -0
  49. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/analyzers/__init__.py +0 -0
  50. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/analyzers/ambiguity_scanner.py +0 -0
  51. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/analyzers/code_analyzer.py +0 -0
  52. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/analyzers/constitution_evidence_extractor.py +0 -0
  53. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/analyzers/contract_extractor.py +0 -0
  54. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/analyzers/control_flow_analyzer.py +0 -0
  55. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/analyzers/graph_analyzer.py +0 -0
  56. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/analyzers/relationship_mapper.py +0 -0
  57. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/analyzers/requirement_extractor.py +0 -0
  58. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/analyzers/test_pattern_extractor.py +0 -0
  59. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/backlog/__init__.py +0 -0
  60. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/backlog/adapters/__init__.py +0 -0
  61. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/backlog/adapters/base.py +0 -0
  62. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/backlog/converter.py +0 -0
  63. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/backlog/filters.py +0 -0
  64. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/backlog/mappers/__init__.py +0 -0
  65. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/backlog/mappers/ado_mapper.py +0 -0
  66. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/backlog/mappers/base.py +0 -0
  67. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/backlog/mappers/github_mapper.py +0 -0
  68. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/backlog/mappers/template_config.py +0 -0
  69. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/cli.py +0 -0
  70. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/commands/__init__.py +0 -0
  71. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/commands/_bundle_shim.py +0 -0
  72. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/commands/analyze.py +0 -0
  73. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/commands/contract_cmd.py +0 -0
  74. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/commands/drift.py +0 -0
  75. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/commands/enforce.py +0 -0
  76. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/commands/generate.py +0 -0
  77. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/commands/import_cmd.py +0 -0
  78. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/commands/init.py +0 -0
  79. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/commands/migrate.py +0 -0
  80. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/commands/plan.py +0 -0
  81. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/commands/project_cmd.py +0 -0
  82. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/commands/repro.py +0 -0
  83. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/commands/sdd.py +0 -0
  84. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/commands/spec.py +0 -0
  85. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/commands/sync.py +0 -0
  86. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/commands/update.py +0 -0
  87. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/commands/validate.py +0 -0
  88. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/common/__init__.py +0 -0
  89. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/common/bundle_factory.py +0 -0
  90. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/common/logger_setup.py +0 -0
  91. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/common/logging_utils.py +0 -0
  92. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/common/text_utils.py +0 -0
  93. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/common/utils.py +0 -0
  94. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/comparators/__init__.py +0 -0
  95. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/comparators/plan_comparator.py +0 -0
  96. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/contracts/__init__.py +0 -0
  97. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/contracts/crosshair_props.py +0 -0
  98. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/contracts/module_interface.py +0 -0
  99. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/enrichers/constitution_enricher.py +0 -0
  100. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/enrichers/plan_enricher.py +0 -0
  101. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/generators/__init__.py +0 -0
  102. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/generators/contract_generator.py +0 -0
  103. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/generators/openapi_extractor.py +0 -0
  104. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/generators/persona_exporter.py +0 -0
  105. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/generators/plan_generator.py +0 -0
  106. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/generators/protocol_generator.py +0 -0
  107. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/generators/report_generator.py +0 -0
  108. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/generators/task_generator.py +0 -0
  109. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/generators/test_to_openapi.py +0 -0
  110. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/generators/workflow_generator.py +0 -0
  111. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/groups/__init__.py +0 -0
  112. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/groups/codebase_group.py +0 -0
  113. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/groups/govern_group.py +0 -0
  114. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/groups/member_group.py +0 -0
  115. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/groups/project_group.py +0 -0
  116. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/groups/spec_group.py +0 -0
  117. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/importers/__init__.py +0 -0
  118. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/importers/speckit_converter.py +0 -0
  119. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/importers/speckit_scanner.py +0 -0
  120. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/integrations/__init__.py +0 -0
  121. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/integrations/specmatic.py +0 -0
  122. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/merge/__init__.py +0 -0
  123. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/merge/resolver.py +0 -0
  124. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/migrations/__init__.py +0 -0
  125. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/migrations/plan_migrator.py +0 -0
  126. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/models/__init__.py +0 -0
  127. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/models/backlog_item.py +0 -0
  128. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/models/bridge.py +0 -0
  129. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/models/capabilities.py +0 -0
  130. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/models/change.py +0 -0
  131. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/models/contract.py +0 -0
  132. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/models/deviation.py +0 -0
  133. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/models/dor_config.py +0 -0
  134. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/models/enforcement.py +0 -0
  135. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/models/module_package.py +0 -0
  136. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/models/persona_template.py +0 -0
  137. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/models/plan.py +0 -0
  138. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/models/protocol.py +0 -0
  139. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/models/quality.py +0 -0
  140. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/models/requirements.py +0 -0
  141. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/models/sdd.py +0 -0
  142. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/models/source_tracking.py +0 -0
  143. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/models/task.py +0 -0
  144. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/models/validation.py +0 -0
  145. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/modes/__init__.py +0 -0
  146. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/modes/detector.py +0 -0
  147. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/modes/router.py +0 -0
  148. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/modules/__init__.py +0 -0
  149. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/modules/_bundle_import.py +0 -0
  150. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/modules/init/module-package.yaml +0 -0
  151. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/modules/init/src/__init__.py +0 -0
  152. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/modules/init/src/app.py +0 -0
  153. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/modules/init/src/commands.py +0 -0
  154. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/modules/init/src/first_run_selection.py +0 -0
  155. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/modules/module_io_shim.py +0 -0
  156. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/modules/module_registry/module-package.yaml +0 -0
  157. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/modules/module_registry/src/__init__.py +0 -0
  158. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/modules/module_registry/src/app.py +0 -0
  159. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/modules/module_registry/src/commands.py +0 -0
  160. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/modules/upgrade/module-package.yaml +0 -0
  161. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/modules/upgrade/src/__init__.py +0 -0
  162. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/modules/upgrade/src/app.py +0 -0
  163. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/modules/upgrade/src/commands.py +0 -0
  164. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/parsers/__init__.py +0 -0
  165. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/parsers/persona_importer.py +0 -0
  166. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/registry/__init__.py +0 -0
  167. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/registry/alias_manager.py +0 -0
  168. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/registry/bootstrap.py +0 -0
  169. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/registry/bridge_registry.py +0 -0
  170. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/registry/crypto_validator.py +0 -0
  171. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/registry/custom_registries.py +0 -0
  172. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/registry/dependency_resolver.py +0 -0
  173. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/registry/extension_registry.py +0 -0
  174. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/registry/help_cache.py +0 -0
  175. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/registry/marketplace_client.py +0 -0
  176. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/registry/metadata.py +0 -0
  177. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/registry/module_availability.py +0 -0
  178. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/registry/module_discovery.py +0 -0
  179. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/registry/module_installer.py +0 -0
  180. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/registry/module_lifecycle.py +0 -0
  181. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/registry/module_packages.py +0 -0
  182. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/registry/module_security.py +0 -0
  183. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/registry/module_state.py +0 -0
  184. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/registry/registry.py +0 -0
  185. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/resources/semgrep/async.yml +0 -0
  186. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/resources/semgrep/code-quality.yml +0 -0
  187. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/resources/semgrep/feature-detection.yml +0 -0
  188. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/runtime.py +0 -0
  189. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/sync/__init__.py +0 -0
  190. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/sync/bridge_probe.py +0 -0
  191. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/sync/bridge_sync.py +0 -0
  192. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/sync/bridge_sync_openspec_md_parse.py +0 -0
  193. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/sync/bridge_sync_requirement_from_proposal.py +0 -0
  194. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/sync/bridge_sync_requirement_helpers.py +0 -0
  195. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/sync/bridge_sync_tasks_from_proposal.py +0 -0
  196. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/sync/bridge_sync_what_changes_format.py +0 -0
  197. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/sync/bridge_sync_write_openspec_from_proposal.py +0 -0
  198. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/sync/bridge_watch.py +0 -0
  199. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/sync/change_detector.py +0 -0
  200. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/sync/code_to_spec.py +0 -0
  201. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/sync/drift_detector.py +0 -0
  202. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/sync/repository_sync.py +0 -0
  203. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/sync/spec_to_code.py +0 -0
  204. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/sync/spec_to_tests.py +0 -0
  205. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/sync/watcher.py +0 -0
  206. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/sync/watcher_enhanced.py +0 -0
  207. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/telemetry.py +0 -0
  208. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/templates/__init__.py +0 -0
  209. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/templates/defaults/defect_v1.yaml +0 -0
  210. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/templates/defaults/enabler_v1.yaml +0 -0
  211. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/templates/defaults/spike_v1.yaml +0 -0
  212. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/templates/defaults/user_story_v1.yaml +0 -0
  213. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/templates/frameworks/scrum/user_story_v1.yaml +0 -0
  214. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/templates/personas/product-owner/user_story_v1.yaml +0 -0
  215. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/templates/providers/ado/work_item_v1.yaml +0 -0
  216. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/templates/registry.py +0 -0
  217. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/templates/specification_templates.py +0 -0
  218. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/__init__.py +0 -0
  219. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/acceptance_criteria.py +0 -0
  220. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/auth_tokens.py +0 -0
  221. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/bundle_converters.py +0 -0
  222. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/bundle_loader.py +0 -0
  223. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/code_change_detector.py +0 -0
  224. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/console.py +0 -0
  225. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/content_sanitizer.py +0 -0
  226. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/context_detection.py +0 -0
  227. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/contract_predicates.py +0 -0
  228. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/enrichment_context.py +0 -0
  229. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/enrichment_parser.py +0 -0
  230. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/env_manager.py +0 -0
  231. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/feature_keys.py +0 -0
  232. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/git.py +0 -0
  233. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/github_annotations.py +0 -0
  234. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/icontract_helpers.py +0 -0
  235. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/ide_setup.py +0 -0
  236. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/incremental_check.py +0 -0
  237. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/metadata.py +0 -0
  238. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/optional_deps.py +0 -0
  239. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/performance.py +0 -0
  240. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/persona_ownership.py +0 -0
  241. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/progress.py +0 -0
  242. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/progressive_disclosure.py +0 -0
  243. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/project_artifact_write.py +0 -0
  244. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/prompts.py +0 -0
  245. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/sdd_discovery.py +0 -0
  246. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/source_scanner.py +0 -0
  247. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/startup_checks.py +0 -0
  248. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/structure.py +0 -0
  249. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/structured_io.py +0 -0
  250. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/suggestions.py +0 -0
  251. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/terminal.py +0 -0
  252. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/utils/yaml_utils.py +0 -0
  253. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/validation/__init__.py +0 -0
  254. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/validation/command_audit.py +0 -0
  255. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/validators/__init__.py +0 -0
  256. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/validators/agile_validation.py +0 -0
  257. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/validators/change_proposal_integration.py +0 -0
  258. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/validators/cli_first_validator.py +0 -0
  259. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/validators/contract_validator.py +0 -0
  260. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/validators/fsm.py +0 -0
  261. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/validators/repro_checker.py +0 -0
  262. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/validators/schema.py +0 -0
  263. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/validators/sidecar/__init__.py +0 -0
  264. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/validators/sidecar/contract_populator.py +0 -0
  265. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/validators/sidecar/crosshair_runner.py +0 -0
  266. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/validators/sidecar/crosshair_summary.py +0 -0
  267. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/validators/sidecar/dependency_installer.py +0 -0
  268. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/validators/sidecar/framework_detector.py +0 -0
  269. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/validators/sidecar/frameworks/__init__.py +0 -0
  270. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/validators/sidecar/frameworks/base.py +0 -0
  271. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/validators/sidecar/frameworks/django.py +0 -0
  272. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/validators/sidecar/frameworks/drf.py +0 -0
  273. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/validators/sidecar/frameworks/fastapi.py +0 -0
  274. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/validators/sidecar/frameworks/flask.py +0 -0
  275. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/validators/sidecar/harness_generator.py +0 -0
  276. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/validators/sidecar/models.py +0 -0
  277. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/validators/sidecar/orchestrator.py +0 -0
  278. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/validators/sidecar/specmatic_runner.py +0 -0
  279. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/validators/sidecar/unannotated_detector.py +0 -0
  280. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/src/specfact_cli/versioning/__init__.py +0 -0
  281. {specfact_cli-0.49.1 → specfact_cli-0.50.1}/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.49.1
3
+ Version: 0.50.1
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.49.1"
7
+ version = "0.50.1"
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"
@@ -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.49.1"
6
+ __version__ = "0.50.1"
@@ -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.49.1"
79
+ __version__ = "0.50.1"
80
80
 
81
81
  __all__ = ["__version__"]
@@ -559,7 +559,13 @@ class ProjectBundle(BaseModel):
559
559
  if progress_callback:
560
560
  progress_callback(current + 1, total_artifacts, "bundle.manifest.yaml")
561
561
  manifest_data = load_structured_file(manifest_path)
562
- manifest = BundleManifest.model_validate(manifest_data)
562
+ if not isinstance(manifest_data, dict):
563
+ raise ValueError("Bundle manifest must be a mapping")
564
+ manifest_mapping = cast(dict[str, Any], manifest_data)
565
+ extensions_data = manifest_mapping.get("extensions", {})
566
+ if not isinstance(extensions_data, dict):
567
+ raise ValueError("ProjectBundle extensions must be a mapping")
568
+ manifest = BundleManifest.model_validate(manifest_mapping)
563
569
  current += 1
564
570
 
565
571
  slots = _BundleLoadSlots()
@@ -585,6 +591,7 @@ class ProjectBundle(BaseModel):
585
591
  features=slots.features,
586
592
  clarifications=slots.clarifications,
587
593
  change_tracking=change_tracking,
594
+ extensions=dict(extensions_data),
588
595
  )
589
596
 
590
597
  @beartype
@@ -663,6 +670,8 @@ class ProjectBundle(BaseModel):
663
670
  progress_callback(total_artifacts, total_artifacts, "bundle.manifest.yaml")
664
671
  manifest_path = bundle_dir / "bundle.manifest.yaml"
665
672
  manifest_data = self.manifest.model_dump(mode="json")
673
+ if self.extensions:
674
+ manifest_data["extensions"] = self.extensions
666
675
  if num_features > 1000:
667
676
  import json
668
677
 
@@ -8,17 +8,21 @@ from icontract import ensure, require
8
8
  from specfact_cli.models.module_package import ModulePackageMetadata
9
9
 
10
10
 
11
- VALID_CATEGORIES = frozenset({"core", "project", "backlog", "codebase", "spec", "govern"})
11
+ VALID_CATEGORIES = frozenset({"core", "project", "backlog", "codebase", "spec", "govern", "requirements"})
12
12
  CATEGORY_TO_GROUP_COMMAND: dict[str, str] = {
13
13
  "project": "project",
14
14
  "backlog": "backlog",
15
15
  "codebase": "code",
16
16
  "spec": "spec",
17
17
  "govern": "govern",
18
+ "requirements": "requirements",
18
19
  }
19
20
  LEGACY_GROUP_COMMAND_ALIASES: dict[tuple[str, str], str] = {
20
21
  ("codebase", "codebase"): "code",
21
22
  }
23
+ LEGACY_CATEGORY_ALIASES: dict[tuple[str, str], str] = {
24
+ ("project", "requirements"): "requirements",
25
+ }
22
26
 
23
27
 
24
28
  class ModuleManifestError(Exception):
@@ -71,6 +75,9 @@ def normalize_legacy_bundle_group_command(meta: ModulePackageMetadata) -> Module
71
75
  """Normalize known legacy bundle group values to canonical grouped commands."""
72
76
  if meta.category is None or meta.bundle_group_command is None:
73
77
  return meta
78
+ normalized_category = LEGACY_CATEGORY_ALIASES.get((meta.category, meta.bundle_group_command))
79
+ if normalized_category is not None:
80
+ meta.category = normalized_category
74
81
  normalized = LEGACY_GROUP_COMMAND_ALIASES.get((meta.category, meta.bundle_group_command))
75
82
  if normalized is not None:
76
83
  meta.bundle_group_command = normalized
@@ -0,0 +1,28 @@
1
+ """Requirements context adapter helpers."""
2
+
3
+ from specfact_cli.requirements.context import (
4
+ RequirementContextAdapter,
5
+ RequirementContextCoverageSummary,
6
+ RequirementContextDiagnostic,
7
+ RequirementContextDiagnosticSeverity,
8
+ RequirementContextImportResult,
9
+ attach_requirements_to_bundle,
10
+ inspect_requirement_context_coverage,
11
+ load_requirements_from_bundle,
12
+ normalize_requirement_records,
13
+ validate_requirement_context,
14
+ )
15
+
16
+
17
+ __all__ = [
18
+ "RequirementContextAdapter",
19
+ "RequirementContextCoverageSummary",
20
+ "RequirementContextDiagnostic",
21
+ "RequirementContextDiagnosticSeverity",
22
+ "RequirementContextImportResult",
23
+ "attach_requirements_to_bundle",
24
+ "inspect_requirement_context_coverage",
25
+ "load_requirements_from_bundle",
26
+ "normalize_requirement_records",
27
+ "validate_requirement_context",
28
+ ]
@@ -0,0 +1,330 @@
1
+ """Core requirements context adapter contracts and helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping, Sequence
6
+ from enum import StrEnum
7
+ from pathlib import Path
8
+ from typing import Any, Literal, Protocol, cast, get_args
9
+
10
+ from beartype import beartype
11
+ from icontract import ensure, require
12
+ from pydantic import BaseModel, ConfigDict, Field, ValidationError
13
+
14
+ from specfact_cli.models.project import ProjectBundle
15
+ from specfact_cli.models.requirements import (
16
+ RequirementEvidenceLinkType,
17
+ RequirementInput,
18
+ load_requirements_input_extension,
19
+ requirements_input_extension_payload,
20
+ )
21
+ from specfact_cli.models.validation import ValidationReport
22
+
23
+
24
+ RequirementContextValidationProfile = Literal[
25
+ "solo",
26
+ "startup",
27
+ "team",
28
+ "enterprise",
29
+ "strict",
30
+ "solo_developer",
31
+ "api_first_team",
32
+ "enterprise_full_stack",
33
+ ]
34
+ KNOWN_REQUIREMENT_CONTEXT_PROFILES: frozenset[str] = frozenset(get_args(RequirementContextValidationProfile))
35
+ STRICT_REQUIREMENT_CONTEXT_PROFILES: frozenset[str] = frozenset({"enterprise", "strict", "enterprise_full_stack"})
36
+
37
+
38
+ class RequirementContextDiagnosticSeverity(StrEnum):
39
+ """Severity for bounded requirements context diagnostics."""
40
+
41
+ INFO = "info"
42
+ WARNING = "warning"
43
+ ERROR = "error"
44
+
45
+
46
+ @beartype
47
+ class RequirementContextDiagnostic(BaseModel):
48
+ """Bounded diagnostic emitted while importing requirement context."""
49
+
50
+ model_config = ConfigDict(use_enum_values=True)
51
+
52
+ severity: RequirementContextDiagnosticSeverity = Field(..., description="Diagnostic severity")
53
+ code: str = Field(..., min_length=1, description="Stable diagnostic code")
54
+ message: str = Field(..., min_length=1, description="Human-readable diagnostic message")
55
+ source_locator: str | None = Field(default=None, description="Optional upstream source locator")
56
+ requirement_id: str | None = Field(default=None, description="Best-effort requirement identifier")
57
+ record_index: int | None = Field(
58
+ default=None,
59
+ ge=0,
60
+ description="Zero-based source record index when available",
61
+ )
62
+
63
+
64
+ @beartype
65
+ class RequirementContextImportResult(BaseModel):
66
+ """Normalized requirement import result plus bounded diagnostics."""
67
+
68
+ requirements: list[RequirementInput] = Field(default_factory=list, description="Normalized requirement records")
69
+ diagnostics: list[RequirementContextDiagnostic] = Field(
70
+ default_factory=list,
71
+ description="Import diagnostics for records that could not be normalized",
72
+ )
73
+
74
+
75
+ @beartype
76
+ class RequirementContextCoverageSummary(BaseModel):
77
+ """Machine-readable coverage summary for normalized requirement inputs."""
78
+
79
+ total_requirements: int = Field(..., ge=0, description="Total normalized requirements")
80
+ with_business_rules: int = Field(..., ge=0, description="Requirements with business rules")
81
+ with_constraints: int = Field(..., ge=0, description="Requirements with constraints")
82
+ with_evidence_links: int = Field(..., ge=0, description="Requirements with evidence links")
83
+ with_architecture_links: int = Field(..., ge=0, description="Requirements linked to architecture evidence")
84
+ with_code_links: int = Field(..., ge=0, description="Requirements linked to code evidence")
85
+ with_test_links: int = Field(..., ge=0, description="Requirements linked to test evidence")
86
+ missing_evidence_requirement_ids: list[str] = Field(
87
+ default_factory=list,
88
+ description="Requirement IDs missing downstream evidence links",
89
+ )
90
+
91
+
92
+ class RequirementContextAdapter(Protocol):
93
+ """Protocol for module adapters that import requirement-like sources."""
94
+
95
+ @beartype
96
+ @require(lambda source: isinstance(source, Path), "source must be a Path")
97
+ def import_requirements(self, source: Path, config: Mapping[str, Any]) -> RequirementContextImportResult:
98
+ """Import source-attributed requirement records from an upstream source."""
99
+ raise NotImplementedError
100
+
101
+
102
+ def _all_records_supported(records: Sequence[RequirementInput | Mapping[str, Any]]) -> bool:
103
+ return all(isinstance(record, RequirementInput | Mapping) for record in records)
104
+
105
+
106
+ def _normalization_result_valid(result: RequirementContextImportResult) -> bool:
107
+ return all(isinstance(requirement, RequirementInput) for requirement in result.requirements)
108
+
109
+
110
+ def _record_requirement_id(record: RequirementInput | Mapping[str, Any]) -> str | None:
111
+ if isinstance(record, RequirementInput):
112
+ return _optional_text(record.requirement_id)
113
+ value = record.get("requirement_id")
114
+ return _optional_text(value) if isinstance(value, str) and value else None
115
+
116
+
117
+ def _validation_error_message(exc: ValidationError) -> str:
118
+ first_error = exc.errors()[0] if exc.errors() else {"msg": str(exc)}
119
+ location = ".".join(str(part) for part in first_error.get("loc", ()))
120
+ message = str(first_error.get("msg", "invalid requirement input")) or "invalid requirement input"
121
+ return f"{location}: {message}" if location else message
122
+
123
+
124
+ def _optional_text(value: str | None) -> str | None:
125
+ if not value:
126
+ return None
127
+ realize = getattr(value, "__ch_realize__", None)
128
+ if callable(realize):
129
+ realized = realize()
130
+ return realized if isinstance(realized, str) else str(realized)
131
+ return value
132
+
133
+
134
+ def _profile_nonempty(profile: str) -> bool:
135
+ return profile.strip() != ""
136
+
137
+
138
+ def _profile_supported(profile: str) -> bool:
139
+ return profile in KNOWN_REQUIREMENT_CONTEXT_PROFILES
140
+
141
+
142
+ def _source_locator_supported(source_locator: str | None) -> bool:
143
+ return source_locator is None or type(source_locator) is str
144
+
145
+
146
+ def _coverage_summary_consistent(result: RequirementContextCoverageSummary) -> bool:
147
+ return result.total_requirements >= len(result.missing_evidence_requirement_ids)
148
+
149
+
150
+ def _requirements_with_evidence_link_type(
151
+ requirements: Sequence[RequirementInput],
152
+ link_type: RequirementEvidenceLinkType,
153
+ ) -> int:
154
+ count = 0
155
+ for requirement in requirements:
156
+ if any(link.link_type == link_type for link in requirement.evidence_links):
157
+ count += 1
158
+ return max(0, count)
159
+
160
+
161
+ def _missing_evidence_requirement_ids(requirements: Sequence[RequirementInput]) -> list[str]:
162
+ return [requirement.requirement_id for requirement in requirements if not requirement.evidence_links]
163
+
164
+
165
+ def _invalid_requirement_diagnostic(
166
+ exc: ValidationError,
167
+ record: RequirementInput | Mapping[str, Any],
168
+ source_locator: str | None,
169
+ record_index: int,
170
+ ) -> RequirementContextDiagnostic:
171
+ return RequirementContextDiagnostic(
172
+ severity=RequirementContextDiagnosticSeverity.ERROR,
173
+ code="invalid_requirement_input",
174
+ message=_validation_error_message(exc),
175
+ source_locator=_optional_text(source_locator),
176
+ requirement_id=_record_requirement_id(record),
177
+ record_index=record_index,
178
+ )
179
+
180
+
181
+ @beartype
182
+ @require(_all_records_supported, "records must be RequirementInput or mapping values")
183
+ @require(_source_locator_supported, "source_locator must be text when provided")
184
+ @ensure(_normalization_result_valid)
185
+ def normalize_requirement_records(
186
+ records: Sequence[RequirementInput | Mapping[str, Any]],
187
+ *,
188
+ source_locator: str | None = None,
189
+ ) -> RequirementContextImportResult:
190
+ """Normalize source-attributed requirement records with bounded diagnostics."""
191
+ normalized: list[RequirementInput] = []
192
+ diagnostics: list[RequirementContextDiagnostic] = []
193
+
194
+ for record_index, record in enumerate(records):
195
+ if isinstance(record, RequirementInput):
196
+ normalized.append(record)
197
+ continue
198
+ try:
199
+ normalized.append(RequirementInput.model_validate(record))
200
+ except ValidationError as exc:
201
+ diagnostics.append(_invalid_requirement_diagnostic(exc, record, source_locator, record_index))
202
+
203
+ return RequirementContextImportResult(requirements=normalized, diagnostics=diagnostics)
204
+
205
+
206
+ @beartype
207
+ @require(lambda bundle: isinstance(bundle, ProjectBundle), "bundle must be a ProjectBundle")
208
+ @require(lambda requirements: all(isinstance(record, RequirementInput) for record in requirements))
209
+ @ensure(lambda result: isinstance(result, ProjectBundle))
210
+ def attach_requirements_to_bundle(bundle: ProjectBundle, requirements: Sequence[RequirementInput]) -> ProjectBundle:
211
+ """Store normalized requirements on the existing requirements.inputs extension."""
212
+ bundle.set_extension("requirements", "inputs", requirements_input_extension_payload(list(requirements)))
213
+ return bundle
214
+
215
+
216
+ @beartype
217
+ @require(lambda bundle: isinstance(bundle, ProjectBundle), "bundle must be a ProjectBundle")
218
+ @ensure(lambda result: all(isinstance(record, RequirementInput) for record in result))
219
+ def load_requirements_from_bundle(bundle: ProjectBundle) -> list[RequirementInput]:
220
+ """Load normalized requirements from the requirements.inputs extension."""
221
+ payload = bundle.get_extension("requirements", "inputs", default=None)
222
+ if payload is None:
223
+ return []
224
+ if not isinstance(payload, dict):
225
+ raise ValueError("requirements.inputs extension must be a mapping payload")
226
+ return load_requirements_input_extension(cast(dict[str, Any], payload))
227
+
228
+
229
+ @beartype
230
+ @require(lambda requirements: all(isinstance(record, RequirementInput) for record in requirements))
231
+ @ensure(_coverage_summary_consistent)
232
+ def inspect_requirement_context_coverage(
233
+ requirements: Sequence[RequirementInput],
234
+ ) -> RequirementContextCoverageSummary:
235
+ """Return machine-readable coverage counts for normalized requirement inputs."""
236
+ requirement_list = list(requirements)
237
+ return RequirementContextCoverageSummary(
238
+ total_requirements=len(requirement_list),
239
+ with_business_rules=sum(bool(requirement.business_rules) for requirement in requirement_list),
240
+ with_constraints=sum(bool(requirement.constraints) for requirement in requirement_list),
241
+ with_evidence_links=sum(bool(requirement.evidence_links) for requirement in requirement_list),
242
+ with_architecture_links=_requirements_with_evidence_link_type(
243
+ requirement_list,
244
+ RequirementEvidenceLinkType.ARCHITECTURE,
245
+ ),
246
+ with_code_links=_requirements_with_evidence_link_type(requirement_list, RequirementEvidenceLinkType.CODE),
247
+ with_test_links=_requirements_with_evidence_link_type(requirement_list, RequirementEvidenceLinkType.TEST),
248
+ missing_evidence_requirement_ids=_missing_evidence_requirement_ids(requirement_list),
249
+ )
250
+
251
+
252
+ def _missing_evidence_severity(profile: RequirementContextValidationProfile) -> str:
253
+ return "error" if profile in STRICT_REQUIREMENT_CONTEXT_PROFILES else "warning"
254
+
255
+
256
+ def _empty_requirements_report() -> ValidationReport:
257
+ return ValidationReport(
258
+ status="warnings",
259
+ violations=[
260
+ {
261
+ "severity": "warning",
262
+ "message": "No requirements.inputs records are attached to the bundle.",
263
+ "location": "requirements.inputs",
264
+ }
265
+ ],
266
+ summary={"total_checks": 1, "passed": 0, "failed": 0, "warnings": 1},
267
+ )
268
+
269
+
270
+ def _missing_evidence_violations(
271
+ coverage: RequirementContextCoverageSummary,
272
+ *,
273
+ severity: str,
274
+ ) -> list[dict[str, str]]:
275
+ return [
276
+ {
277
+ "severity": severity,
278
+ "message": "Requirement input has no downstream evidence links.",
279
+ "location": f"requirements.inputs[{requirement_id}].evidence_links",
280
+ }
281
+ for requirement_id in coverage.missing_evidence_requirement_ids
282
+ ]
283
+
284
+
285
+ def _validation_status(failed: int, warnings: int) -> Literal["passed", "failed", "warnings"]:
286
+ if failed:
287
+ return "failed"
288
+ if warnings:
289
+ return "warnings"
290
+ return "passed"
291
+
292
+
293
+ def _validation_summary(
294
+ requirements: Sequence[RequirementInput],
295
+ coverage: RequirementContextCoverageSummary,
296
+ violations: Sequence[Mapping[str, str]],
297
+ ) -> dict[str, int]:
298
+ failed = sum(violation["severity"] == "error" for violation in violations)
299
+ warnings = sum(violation["severity"] == "warning" for violation in violations)
300
+ return {
301
+ "total_checks": max(1, len(requirements) * 2),
302
+ "passed": len(requirements) + coverage.with_evidence_links,
303
+ "failed": failed,
304
+ "warnings": warnings,
305
+ }
306
+
307
+
308
+ @beartype
309
+ @require(lambda bundle: isinstance(bundle, ProjectBundle), "bundle must be a ProjectBundle")
310
+ @require(_profile_nonempty, "profile must not be empty")
311
+ @require(_profile_supported, "profile must be a supported requirements context validation profile")
312
+ @ensure(lambda result: isinstance(result, ValidationReport))
313
+ def validate_requirement_context(
314
+ bundle: ProjectBundle,
315
+ *,
316
+ profile: RequirementContextValidationProfile = "startup",
317
+ ) -> ValidationReport:
318
+ """Validate requirements context evidence usefulness for a ProjectBundle."""
319
+ requirements = load_requirements_from_bundle(bundle)
320
+ if not requirements:
321
+ return _empty_requirements_report()
322
+
323
+ coverage = inspect_requirement_context_coverage(requirements)
324
+ violations = _missing_evidence_violations(coverage, severity=_missing_evidence_severity(profile))
325
+ summary = _validation_summary(requirements, coverage, violations)
326
+ return ValidationReport(
327
+ status=_validation_status(summary["failed"], summary["warnings"]),
328
+ violations=violations,
329
+ summary=summary,
330
+ )
File without changes
File without changes
File without changes