specfact-cli 0.46.0__tar.gz → 0.46.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 (276) hide show
  1. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/PKG-INFO +16 -5
  2. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/README.md +15 -4
  3. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/pyproject.toml +5 -4
  4. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/__init__.py +1 -1
  5. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/__init__.py +1 -1
  6. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/adapters/ado.py +9 -9
  7. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/adapters/github.py +22 -3
  8. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/adapters/speckit.py +14 -11
  9. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/analyzers/code_analyzer.py +40 -15
  10. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/analyzers/graph_analyzer.py +23 -26
  11. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/common/logger_setup.py +2 -1
  12. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/generators/persona_exporter.py +2 -2
  13. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/generators/test_to_openapi.py +13 -7
  14. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/modules/init/module-package.yaml +3 -3
  15. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/modules/init/src/first_run_selection.py +1 -1
  16. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/registry/module_installer.py +3 -1
  17. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/sync/bridge_watch.py +6 -2
  18. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/source_scanner.py +0 -4
  19. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/validators/cli_first_validator.py +20 -12
  20. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/validators/sidecar/crosshair_runner.py +5 -1
  21. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/validators/sidecar/crosshair_summary.py +2 -2
  22. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/validators/sidecar/frameworks/fastapi.py +11 -6
  23. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/.gitignore +0 -0
  24. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/LICENSE +0 -0
  25. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/resources/keys/README.md +0 -0
  26. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/resources/keys/module-signing-public.pem +0 -0
  27. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/resources/mappings/node-async.yaml +0 -0
  28. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/resources/mappings/python-async.yaml +0 -0
  29. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/resources/mappings/speckit-default.yaml +0 -0
  30. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/resources/schemas/deviation.schema.json +0 -0
  31. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/resources/schemas/plan.schema.json +0 -0
  32. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/resources/schemas/protocol.schema.json +0 -0
  33. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/resources/templates/github-action.yml.j2 +0 -0
  34. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/resources/templates/persona/architect.md.j2 +0 -0
  35. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/resources/templates/persona/developer.md.j2 +0 -0
  36. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/resources/templates/persona/product-owner.md.j2 +0 -0
  37. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/resources/templates/plan.bundle.yaml.j2 +0 -0
  38. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/resources/templates/policies/kanban.yaml +0 -0
  39. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/resources/templates/policies/mixed.yaml +0 -0
  40. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/resources/templates/policies/safe.yaml +0 -0
  41. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/resources/templates/policies/scrum.yaml +0 -0
  42. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/resources/templates/pr-template.md.j2 +0 -0
  43. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/resources/templates/protocol.yaml.j2 +0 -0
  44. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/resources/templates/telemetry.yaml.example +0 -0
  45. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/__main__.py +0 -0
  46. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/adapters/__init__.py +0 -0
  47. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/adapters/backlog_base.py +0 -0
  48. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/adapters/base.py +0 -0
  49. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/adapters/openspec.py +0 -0
  50. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/adapters/openspec_parser.py +0 -0
  51. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/adapters/registry.py +0 -0
  52. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/agents/__init__.py +0 -0
  53. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/agents/analyze_agent.py +0 -0
  54. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/agents/base.py +0 -0
  55. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/agents/plan_agent.py +0 -0
  56. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/agents/registry.py +0 -0
  57. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/agents/sync_agent.py +0 -0
  58. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/analyzers/__init__.py +0 -0
  59. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/analyzers/ambiguity_scanner.py +0 -0
  60. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/analyzers/constitution_evidence_extractor.py +0 -0
  61. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/analyzers/contract_extractor.py +0 -0
  62. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/analyzers/control_flow_analyzer.py +0 -0
  63. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/analyzers/relationship_mapper.py +0 -0
  64. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/analyzers/requirement_extractor.py +0 -0
  65. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/analyzers/test_pattern_extractor.py +0 -0
  66. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/backlog/__init__.py +0 -0
  67. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/backlog/adapters/__init__.py +0 -0
  68. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/backlog/adapters/base.py +0 -0
  69. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/backlog/converter.py +0 -0
  70. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/backlog/filters.py +0 -0
  71. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/backlog/mappers/__init__.py +0 -0
  72. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/backlog/mappers/ado_mapper.py +0 -0
  73. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/backlog/mappers/base.py +0 -0
  74. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/backlog/mappers/github_mapper.py +0 -0
  75. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/backlog/mappers/template_config.py +0 -0
  76. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/cli.py +0 -0
  77. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/commands/__init__.py +0 -0
  78. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/commands/_bundle_shim.py +0 -0
  79. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/commands/analyze.py +0 -0
  80. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/commands/contract_cmd.py +0 -0
  81. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/commands/drift.py +0 -0
  82. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/commands/enforce.py +0 -0
  83. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/commands/generate.py +0 -0
  84. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/commands/import_cmd.py +0 -0
  85. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/commands/init.py +0 -0
  86. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/commands/migrate.py +0 -0
  87. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/commands/plan.py +0 -0
  88. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/commands/project_cmd.py +0 -0
  89. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/commands/repro.py +0 -0
  90. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/commands/sdd.py +0 -0
  91. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/commands/spec.py +0 -0
  92. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/commands/sync.py +0 -0
  93. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/commands/update.py +0 -0
  94. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/commands/validate.py +0 -0
  95. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/common/__init__.py +0 -0
  96. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/common/bundle_factory.py +0 -0
  97. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/common/logging_utils.py +0 -0
  98. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/common/text_utils.py +0 -0
  99. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/common/utils.py +0 -0
  100. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/comparators/__init__.py +0 -0
  101. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/comparators/plan_comparator.py +0 -0
  102. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/contracts/__init__.py +0 -0
  103. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/contracts/crosshair_props.py +0 -0
  104. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/contracts/module_interface.py +0 -0
  105. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/enrichers/constitution_enricher.py +0 -0
  106. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/enrichers/plan_enricher.py +0 -0
  107. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/generators/__init__.py +0 -0
  108. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/generators/contract_generator.py +0 -0
  109. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/generators/openapi_extractor.py +0 -0
  110. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/generators/plan_generator.py +0 -0
  111. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/generators/protocol_generator.py +0 -0
  112. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/generators/report_generator.py +0 -0
  113. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/generators/task_generator.py +0 -0
  114. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/generators/workflow_generator.py +0 -0
  115. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/groups/__init__.py +0 -0
  116. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/groups/codebase_group.py +0 -0
  117. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/groups/govern_group.py +0 -0
  118. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/groups/member_group.py +0 -0
  119. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/groups/project_group.py +0 -0
  120. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/groups/spec_group.py +0 -0
  121. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/importers/__init__.py +0 -0
  122. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/importers/speckit_converter.py +0 -0
  123. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/importers/speckit_scanner.py +0 -0
  124. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/integrations/__init__.py +0 -0
  125. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/integrations/specmatic.py +0 -0
  126. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/merge/__init__.py +0 -0
  127. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/merge/resolver.py +0 -0
  128. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/migrations/__init__.py +0 -0
  129. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/migrations/plan_migrator.py +0 -0
  130. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/models/__init__.py +0 -0
  131. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/models/backlog_item.py +0 -0
  132. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/models/bridge.py +0 -0
  133. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/models/capabilities.py +0 -0
  134. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/models/change.py +0 -0
  135. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/models/contract.py +0 -0
  136. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/models/deviation.py +0 -0
  137. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/models/dor_config.py +0 -0
  138. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/models/enforcement.py +0 -0
  139. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/models/module_package.py +0 -0
  140. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/models/persona_template.py +0 -0
  141. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/models/plan.py +0 -0
  142. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/models/project.py +0 -0
  143. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/models/protocol.py +0 -0
  144. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/models/quality.py +0 -0
  145. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/models/sdd.py +0 -0
  146. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/models/source_tracking.py +0 -0
  147. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/models/task.py +0 -0
  148. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/models/validation.py +0 -0
  149. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/modes/__init__.py +0 -0
  150. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/modes/detector.py +0 -0
  151. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/modes/router.py +0 -0
  152. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/modules/__init__.py +0 -0
  153. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/modules/_bundle_import.py +0 -0
  154. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/modules/init/src/__init__.py +0 -0
  155. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/modules/init/src/app.py +0 -0
  156. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/modules/init/src/commands.py +0 -0
  157. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/modules/module_io_shim.py +0 -0
  158. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/modules/module_registry/module-package.yaml +0 -0
  159. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/modules/module_registry/src/__init__.py +0 -0
  160. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/modules/module_registry/src/app.py +0 -0
  161. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/modules/module_registry/src/commands.py +0 -0
  162. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/modules/upgrade/module-package.yaml +0 -0
  163. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/modules/upgrade/src/__init__.py +0 -0
  164. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/modules/upgrade/src/app.py +0 -0
  165. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/modules/upgrade/src/commands.py +0 -0
  166. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/parsers/__init__.py +0 -0
  167. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/parsers/persona_importer.py +0 -0
  168. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/registry/__init__.py +0 -0
  169. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/registry/alias_manager.py +0 -0
  170. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/registry/bootstrap.py +0 -0
  171. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/registry/bridge_registry.py +0 -0
  172. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/registry/crypto_validator.py +0 -0
  173. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/registry/custom_registries.py +0 -0
  174. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/registry/dependency_resolver.py +0 -0
  175. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/registry/extension_registry.py +0 -0
  176. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/registry/help_cache.py +0 -0
  177. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/registry/marketplace_client.py +0 -0
  178. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/registry/metadata.py +0 -0
  179. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/registry/module_discovery.py +0 -0
  180. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/registry/module_grouping.py +0 -0
  181. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/registry/module_lifecycle.py +0 -0
  182. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/registry/module_packages.py +0 -0
  183. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/registry/module_security.py +0 -0
  184. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/registry/module_state.py +0 -0
  185. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/registry/registry.py +0 -0
  186. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/resources/semgrep/async.yml +0 -0
  187. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/resources/semgrep/code-quality.yml +0 -0
  188. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/resources/semgrep/feature-detection.yml +0 -0
  189. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/runtime.py +0 -0
  190. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/sync/__init__.py +0 -0
  191. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/sync/bridge_probe.py +0 -0
  192. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/sync/bridge_sync.py +0 -0
  193. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/sync/bridge_sync_openspec_md_parse.py +0 -0
  194. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/sync/bridge_sync_requirement_from_proposal.py +0 -0
  195. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/sync/bridge_sync_requirement_helpers.py +0 -0
  196. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/sync/bridge_sync_tasks_from_proposal.py +0 -0
  197. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/sync/bridge_sync_what_changes_format.py +0 -0
  198. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/sync/bridge_sync_write_openspec_from_proposal.py +0 -0
  199. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/sync/change_detector.py +0 -0
  200. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/sync/code_to_spec.py +0 -0
  201. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/sync/drift_detector.py +0 -0
  202. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/sync/repository_sync.py +0 -0
  203. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/sync/spec_to_code.py +0 -0
  204. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/sync/spec_to_tests.py +0 -0
  205. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/sync/watcher.py +0 -0
  206. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/sync/watcher_enhanced.py +0 -0
  207. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/telemetry.py +0 -0
  208. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/templates/__init__.py +0 -0
  209. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/templates/defaults/defect_v1.yaml +0 -0
  210. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/templates/defaults/enabler_v1.yaml +0 -0
  211. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/templates/defaults/spike_v1.yaml +0 -0
  212. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/templates/defaults/user_story_v1.yaml +0 -0
  213. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/templates/frameworks/scrum/user_story_v1.yaml +0 -0
  214. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/templates/personas/product-owner/user_story_v1.yaml +0 -0
  215. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/templates/providers/ado/work_item_v1.yaml +0 -0
  216. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/templates/registry.py +0 -0
  217. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/templates/specification_templates.py +0 -0
  218. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/__init__.py +0 -0
  219. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/acceptance_criteria.py +0 -0
  220. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/auth_tokens.py +0 -0
  221. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/bundle_converters.py +0 -0
  222. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/bundle_loader.py +0 -0
  223. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/code_change_detector.py +0 -0
  224. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/console.py +0 -0
  225. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/content_sanitizer.py +0 -0
  226. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/context_detection.py +0 -0
  227. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/contract_predicates.py +0 -0
  228. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/enrichment_context.py +0 -0
  229. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/enrichment_parser.py +0 -0
  230. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/env_manager.py +0 -0
  231. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/feature_keys.py +0 -0
  232. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/git.py +0 -0
  233. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/github_annotations.py +0 -0
  234. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/icontract_helpers.py +0 -0
  235. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/ide_setup.py +0 -0
  236. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/incremental_check.py +0 -0
  237. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/metadata.py +0 -0
  238. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/optional_deps.py +0 -0
  239. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/performance.py +0 -0
  240. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/persona_ownership.py +0 -0
  241. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/progress.py +0 -0
  242. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/progressive_disclosure.py +0 -0
  243. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/project_artifact_write.py +0 -0
  244. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/prompts.py +0 -0
  245. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/sdd_discovery.py +0 -0
  246. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/startup_checks.py +0 -0
  247. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/structure.py +0 -0
  248. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/structured_io.py +0 -0
  249. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/suggestions.py +0 -0
  250. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/terminal.py +0 -0
  251. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/utils/yaml_utils.py +0 -0
  252. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/validation/__init__.py +0 -0
  253. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/validation/command_audit.py +0 -0
  254. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/validators/__init__.py +0 -0
  255. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/validators/agile_validation.py +0 -0
  256. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/validators/change_proposal_integration.py +0 -0
  257. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/validators/contract_validator.py +0 -0
  258. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/validators/fsm.py +0 -0
  259. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/validators/repro_checker.py +0 -0
  260. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/validators/schema.py +0 -0
  261. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/validators/sidecar/__init__.py +0 -0
  262. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/validators/sidecar/contract_populator.py +0 -0
  263. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/validators/sidecar/dependency_installer.py +0 -0
  264. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/validators/sidecar/framework_detector.py +0 -0
  265. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/validators/sidecar/frameworks/__init__.py +0 -0
  266. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/validators/sidecar/frameworks/base.py +0 -0
  267. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/validators/sidecar/frameworks/django.py +0 -0
  268. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/validators/sidecar/frameworks/drf.py +0 -0
  269. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/validators/sidecar/frameworks/flask.py +0 -0
  270. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/validators/sidecar/harness_generator.py +0 -0
  271. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/validators/sidecar/models.py +0 -0
  272. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/validators/sidecar/orchestrator.py +0 -0
  273. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/validators/sidecar/specmatic_runner.py +0 -0
  274. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/validators/sidecar/unannotated_detector.py +0 -0
  275. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/versioning/__init__.py +0 -0
  276. {specfact_cli-0.46.0 → specfact_cli-0.46.2}/src/specfact_cli/versioning/analyzer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specfact-cli
3
- Version: 0.46.0
3
+ Version: 0.46.2
4
4
  Summary: The swiss knife CLI for agile DevOps teams. Keep backlog, specs, tests, and code in sync with validation and contract enforcement for new projects and long-lived codebases.
5
5
  Project-URL: Homepage, https://github.com/nold-ai/specfact-cli
6
6
  Project-URL: Repository, https://github.com/nold-ai/specfact-cli.git
@@ -308,7 +308,7 @@ uvx specfact-cli code review run --path . --scope full
308
308
  **Sample output:**
309
309
 
310
310
  ```text
311
- SpecFact CLI - v0.46.0
311
+ SpecFact CLI - v0.46.1
312
312
 
313
313
  Running Ruff checks...
314
314
  Running Radon complexity checks...
@@ -361,16 +361,27 @@ It exists because delivery drifts in predictable ways:
361
361
 
362
362
  ## Add SpecFact to your workflow
363
363
 
364
- **Pre-commit hook**
364
+ ### Pre-commit hook
365
+
366
+ This repository uses a **modular** local hook layout (parity with `specfact-cli-modules`: `fail_fast`,
367
+ separate verify / format / YAML / Markdown / workflow / lint / Block 2 hooks). If you copy
368
+ [`.pre-commit-config.yaml`](.pre-commit-config.yaml) into another repo, you must also vendor the
369
+ referenced `scripts/*.sh` entrypoints (at minimum `scripts/pre-commit-quality-checks.sh`,
370
+ `scripts/pre-commit-verify-modules.sh`, and `scripts/git-branch-module-signature-flag.sh`) so hook
371
+ `entry:` paths resolve. Alternatively, skip vendoring the modular file and use the remote hook below.
372
+
373
+ For a **single-hook** setup in downstream repos, keep using the stable id and script shim:
365
374
 
366
375
  ```yaml
367
376
  - repo: https://github.com/nold-ai/specfact-cli
368
- rev: v0.46.0
377
+ rev: v0.46.1
369
378
  hooks:
370
379
  - id: specfact-smart-checks
371
380
  ```
372
381
 
373
- **GitHub Actions**
382
+ The shim runs `scripts/pre-commit-quality-checks.sh all` (full pipeline including module verify).
383
+
384
+ ### GitHub Actions
374
385
 
375
386
  ```yaml
376
387
  - name: SpecFact Gate
@@ -27,7 +27,7 @@ uvx specfact-cli code review run --path . --scope full
27
27
  **Sample output:**
28
28
 
29
29
  ```text
30
- SpecFact CLI - v0.46.0
30
+ SpecFact CLI - v0.46.1
31
31
 
32
32
  Running Ruff checks...
33
33
  Running Radon complexity checks...
@@ -80,16 +80,27 @@ It exists because delivery drifts in predictable ways:
80
80
 
81
81
  ## Add SpecFact to your workflow
82
82
 
83
- **Pre-commit hook**
83
+ ### Pre-commit hook
84
+
85
+ This repository uses a **modular** local hook layout (parity with `specfact-cli-modules`: `fail_fast`,
86
+ separate verify / format / YAML / Markdown / workflow / lint / Block 2 hooks). If you copy
87
+ [`.pre-commit-config.yaml`](.pre-commit-config.yaml) into another repo, you must also vendor the
88
+ referenced `scripts/*.sh` entrypoints (at minimum `scripts/pre-commit-quality-checks.sh`,
89
+ `scripts/pre-commit-verify-modules.sh`, and `scripts/git-branch-module-signature-flag.sh`) so hook
90
+ `entry:` paths resolve. Alternatively, skip vendoring the modular file and use the remote hook below.
91
+
92
+ For a **single-hook** setup in downstream repos, keep using the stable id and script shim:
84
93
 
85
94
  ```yaml
86
95
  - repo: https://github.com/nold-ai/specfact-cli
87
- rev: v0.46.0
96
+ rev: v0.46.1
88
97
  hooks:
89
98
  - id: specfact-smart-checks
90
99
  ```
91
100
 
92
- **GitHub Actions**
101
+ The shim runs `scripts/pre-commit-quality-checks.sh all` (full pipeline including module verify).
102
+
103
+ ### GitHub Actions
93
104
 
94
105
  ```yaml
95
106
  - name: SpecFact Gate
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "specfact-cli"
7
- version = "0.46.0"
7
+ version = "0.46.2"
8
8
  description = "The swiss knife CLI for agile DevOps teams. Keep backlog, specs, tests, and code in sync with validation and contract enforcement for new projects and long-lived codebases."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -209,7 +209,8 @@ validate-prompts = "python tools/validate_prompts.py"
209
209
  test = "pytest {args}"
210
210
  test-cov = "pytest --cov=src --cov-report=term-missing {args}"
211
211
  type-check = "basedpyright --pythonpath $(python -c 'import sys; print(sys.executable)') {args}"
212
- lint = "ruff format . --check && basedpyright --pythonpath $(python -c 'import sys; print(sys.executable)') && ruff check . && pylint src tests tools && python scripts/verify_safe_project_writes.py"
212
+ # basedpyright --level error: suppress warning noise in pre-commit (Block 1 runs `hatch run lint`).
213
+ lint = "ruff format . --check && basedpyright --level error --pythonpath $(python -c 'import sys; print(sys.executable)') && ruff check . && pylint src tests tools && python scripts/verify_safe_project_writes.py"
213
214
  governance = "pylint src tests tools --reports=y --output-format=parseable"
214
215
  format = "ruff check . --fix && ruff format ."
215
216
 
@@ -236,7 +237,8 @@ check-cross-site-links = "python scripts/check-cross-site-links.py"
236
237
  doc-frontmatter-check = "python scripts/check_doc_frontmatter.py"
237
238
  validate-agent-rule-signals = "python scripts/validate_agent_rule_applies_when.py"
238
239
  check-version-sources = "python scripts/check_version_sources.py"
239
- release = "python scripts/check_version_sources.py"
240
+ check-pypi-ahead = "python scripts/check_local_version_ahead_of_pypi.py"
241
+ release = "python scripts/check_local_version_ahead_of_pypi.py && python scripts/check_version_sources.py"
240
242
  docs-validate = "python scripts/check-docs-commands.py && python scripts/check-cross-site-links.py --warn-only && python scripts/check_doc_frontmatter.py && python scripts/validate_agent_rule_applies_when.py"
241
243
 
242
244
  # Legacy entry (kept for compatibility); prefer `workflows-lint` above
@@ -286,7 +288,6 @@ contract-prune = "python tools/migrate_tests_to_contracts.py --prune --dry-run"
286
288
  # Pre-commit hooks
287
289
  pre-commit-install = "bash scripts/setup-git-hooks.sh"
288
290
  pre-commit-checks = "bash scripts/pre-commit-smart-checks.sh"
289
- pre-commit-test = "bash scripts/pre-commit-smart-test.sh"
290
291
 
291
292
  [tool.hatch.envs.py311]
292
293
  python = "3.11"
@@ -3,4 +3,4 @@ SpecFact CLI - Spec→Contract→Sentinel tool for contract-driven development.
3
3
  """
4
4
 
5
5
  # Package version: keep in sync with pyproject.toml, setup.py, src/specfact_cli/__init__.py
6
- __version__ = "0.46.0"
6
+ __version__ = "0.46.2"
@@ -45,6 +45,6 @@ def _bootstrap_bundle_paths() -> None:
45
45
 
46
46
  _bootstrap_bundle_paths()
47
47
 
48
- __version__ = "0.46.0"
48
+ __version__ = "0.46.2"
49
49
 
50
50
  __all__ = ["__version__"]
@@ -54,7 +54,7 @@ console = Console()
54
54
 
55
55
  @dataclass(frozen=True, slots=True)
56
56
  class _AdoCreatedWorkItemRef:
57
- work_item_id: Any
57
+ work_item_id: int | str
58
58
  work_item_url: str
59
59
  org: str
60
60
  project: str
@@ -744,7 +744,10 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
744
744
  },
745
745
  }
746
746
  source_tracking = proposal_data.get("source_tracking")
747
- if source_tracking is None:
747
+ if isinstance(source_tracking, list):
748
+ cast(list[dict[str, Any]], source_tracking).append(tracking_update)
749
+ return
750
+ if source_tracking is None or (isinstance(source_tracking, dict) and len(source_tracking) == 0):
748
751
  proposal_data["source_tracking"] = tracking_update
749
752
  return
750
753
  if isinstance(source_tracking, dict):
@@ -752,11 +755,6 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
752
755
  st.update(tracking_update)
753
756
  proposal_data["source_tracking"] = st
754
757
  return
755
- if isinstance(source_tracking, list):
756
- if not source_tracking:
757
- proposal_data["source_tracking"] = [tracking_update]
758
- return
759
- cast(list[dict[str, Any]], source_tracking).append(tracking_update)
760
758
 
761
759
  def _is_on_premise(self) -> bool:
762
760
  """
@@ -2270,8 +2268,10 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
2270
2268
  Returns:
2271
2269
  Dict with updated work item data: {"work_item_id": int, "work_item_url": str, "state": str}
2272
2270
  """
2273
- target_repo = f"{org}/{project}"
2274
- work_item_id = self._get_source_tracking_work_item_id(proposal_data.get("source_tracking", {}), target_repo)
2271
+ work_item_id = self._get_source_tracking_work_item_id(
2272
+ proposal_data.get("source_tracking", {}),
2273
+ f"{org}/{project}",
2274
+ )
2275
2275
  ado_state = self._resolve_proposal_ado_state(proposal_data)
2276
2276
  work_item_data = self._patch_work_item(
2277
2277
  org,
@@ -159,7 +159,11 @@ def _get_github_token_from_gh_cli() -> str | None:
159
159
  return None
160
160
 
161
161
 
162
- _GITHUB_GIT_CONFIG_URL_RE = re.compile(r"(?im)^\s*url\s*=\s*(https?://\S+|ssh://\S+|git://\S+|git@[^:\s]+:\S+)\s*$")
162
+ # Line-anchored so ``pushurl =`` / ``insteadOf`` lines do not match the ``url`` token inside another key.
163
+ # Matches: https://, http://, ssh://, git://, and git@host:path remotes.
164
+ _GITHUB_GIT_CONFIG_URL_RE = re.compile(
165
+ r"(?m)^\s*url\s*=\s*(https?://[^\s]+|ssh://[^\s]+|git://[^\s]+|git@[^:]+:[^\s]+)"
166
+ )
163
167
 
164
168
 
165
169
  def _git_config_content_indicates_github(config_content: str) -> bool:
@@ -1471,7 +1475,14 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
1471
1475
  title = raw_title
1472
1476
 
1473
1477
  body = self._render_issue_body(
1474
- _IssueBodyRenderInput(title, description, rationale, impact, change_id, raw_body)
1478
+ _IssueBodyRenderInput(
1479
+ title=title,
1480
+ description=description,
1481
+ rationale=rationale,
1482
+ impact=impact,
1483
+ change_id=change_id,
1484
+ raw_body=raw_body,
1485
+ )
1475
1486
  )
1476
1487
 
1477
1488
  # Check for API token before making request
@@ -1735,7 +1746,15 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
1735
1746
  current_body, current_title, current_state = self._fetch_issue_snapshot(repo_owner, repo_name, issue_number)
1736
1747
  preserved_sections = self._preserved_issue_sections(current_body, change_id)
1737
1748
  body = self._render_issue_body(
1738
- _IssueBodyRenderInput(title, description, rationale, impact, change_id, raw_body, preserved_sections)
1749
+ _IssueBodyRenderInput(
1750
+ title=title,
1751
+ description=description,
1752
+ rationale=rationale,
1753
+ impact=impact,
1754
+ change_id=change_id,
1755
+ raw_body=raw_body,
1756
+ preserved_sections=preserved_sections,
1757
+ )
1739
1758
  )
1740
1759
 
1741
1760
  # Update issue body via GitHub API PATCH
@@ -462,15 +462,19 @@ class SpecKitAdapter(BridgeAdapter):
462
462
  story_title = sd.get("title", "Unknown Story")
463
463
  priority = sd.get("priority", "P3")
464
464
  acceptance_raw = sd.get("acceptance", [])
465
- default_acceptance = [f"{story_title} is implemented"]
466
465
  if isinstance(acceptance_raw, list) and acceptance_raw:
467
466
  if all(isinstance(x, str) for x in acceptance_raw):
468
- acceptance = list(acceptance_raw) or default_acceptance
467
+ acceptance = [s.strip() for s in acceptance_raw if isinstance(s, str) and s.strip()]
469
468
  else:
470
- extracted = self._extract_text_list(cast(list[Any], acceptance_raw))
471
- acceptance = extracted or default_acceptance
469
+ acceptance = [
470
+ s.strip() for s in self._extract_text_list(cast(list[Any], acceptance_raw)) if s.strip()
471
+ ]
472
+ if not acceptance:
473
+ acceptance = [f"{story_title} is implemented"]
474
+ if not acceptance:
475
+ acceptance = [f"{story_title} is implemented"]
472
476
  else:
473
- acceptance = default_acceptance
477
+ acceptance = [f"{story_title} is implemented"]
474
478
  story_points = priority_map.get(str(priority), 3)
475
479
  stories.append(
476
480
  Story(
@@ -548,14 +552,13 @@ class SpecKitAdapter(BridgeAdapter):
548
552
 
549
553
  if existing_feature:
550
554
  existing_feature.title = payload.feature_title
551
- existing_feature.outcomes = (
552
- list(payload.outcomes) if payload.outcomes else list(existing_feature.outcomes or [])
553
- )
554
- existing_feature.acceptance = (
555
- list(payload.acceptance) if payload.acceptance else list(existing_feature.acceptance or [])
556
- )
555
+ existing_feature.outcomes = list(payload.outcomes)
556
+ existing_feature.acceptance = list(payload.acceptance)
557
557
  existing_feature.stories = [s.model_copy(deep=True) for s in payload.stories]
558
558
  existing_feature.constraints = list(constraints)
559
+ existing_feature.source_tracking = self._build_speckit_source_tracking(
560
+ payload.spec_path, payload.bridge_config
561
+ )
559
562
  return
560
563
 
561
564
  feature = Feature(
@@ -738,19 +738,8 @@ class CodeAnalyzer:
738
738
  @staticmethod
739
739
  def _themes_for_import_module(module_name: str, theme_keywords: dict[str, str]) -> set[str]:
740
740
  lowered = module_name.lower()
741
- tokens = [t for t in re.split(r"[._-]", lowered) if t]
742
- top_level = lowered.split(".", 1)[0]
743
- found: set[str] = set()
744
- for keyword, theme in theme_keywords.items():
745
- if keyword == lowered or lowered.startswith(f"{keyword}."):
746
- found.add(theme)
747
- continue
748
- if keyword == top_level or top_level.startswith(f"{keyword}."):
749
- found.add(theme)
750
- continue
751
- if keyword in tokens:
752
- found.add(theme)
753
- return found
741
+ segments = {p for p in lowered.replace("-", ".").split(".") if p}
742
+ return {theme for keyword, theme in theme_keywords.items() if keyword in segments}
754
743
 
755
744
  def _themes_for_import_node(self, node: ast.Import | ast.ImportFrom, theme_keywords: dict[str, str]) -> set[str]:
756
745
  if isinstance(node, ast.Import):
@@ -1745,14 +1734,50 @@ class CodeAnalyzer:
1745
1734
  self.async_patterns[module_name].extend(async_methods)
1746
1735
  return async_methods
1747
1736
 
1748
- def _detect_async_patterns_parallel(self, tree: ast.AST, file_path: Path) -> list[str]:
1737
+ @staticmethod
1738
+ def _build_ast_parent_map(tree: ast.AST) -> dict[ast.AST, ast.AST | None]:
1739
+ parents: dict[ast.AST, ast.AST | None] = {}
1740
+
1741
+ def visit(node: ast.AST, parent: ast.AST | None) -> None:
1742
+ parents[node] = parent
1743
+ for child in ast.iter_child_nodes(node):
1744
+ visit(child, node)
1745
+
1746
+ visit(tree, None)
1747
+ return parents
1748
+
1749
+ @staticmethod
1750
+ def _function_name_holding_await(parents: dict[ast.AST, ast.AST | None], await_node: ast.Await) -> str | None:
1751
+ current: ast.AST | None = await_node
1752
+ while current is not None:
1753
+ par = parents.get(current)
1754
+ if par is None:
1755
+ return None
1756
+ if isinstance(par, (ast.FunctionDef, ast.AsyncFunctionDef)):
1757
+ return par.name
1758
+ current = par
1759
+ return None
1760
+
1761
+ def _detect_async_patterns_parallel(self, tree: ast.AST, _file_path: Path) -> list[str]:
1749
1762
  """
1750
1763
  Detect async/await patterns in code (thread-safe version).
1751
1764
 
1752
1765
  Returns:
1753
1766
  List of async method/function names
1754
1767
  """
1755
- return [node.name for node in ast.walk(tree) if isinstance(node, ast.AsyncFunctionDef)]
1768
+ async_methods: list[str] = []
1769
+ parents = self._build_ast_parent_map(tree)
1770
+
1771
+ for node in ast.walk(tree):
1772
+ if isinstance(node, ast.AsyncFunctionDef):
1773
+ async_methods.append(node.name)
1774
+ if not isinstance(node, ast.Await):
1775
+ continue
1776
+ host = self._function_name_holding_await(parents, node)
1777
+ if host and host not in async_methods:
1778
+ async_methods.append(host)
1779
+
1780
+ return async_methods
1756
1781
 
1757
1782
  def _apply_commit_hash_to_matching_features(self, feature_num: str, commit_hash: str) -> None:
1758
1783
  for feature in self.features:
@@ -191,22 +191,12 @@ class GraphAnalyzer:
191
191
  graph: StrDiGraph,
192
192
  module_name: str,
193
193
  call_graph: dict[str, list[str]],
194
- python_files: list[Path],
194
+ loaded_contents: list[tuple[str, str]],
195
195
  ) -> None:
196
- """Add call-graph-derived dependency edges for one module.
197
-
198
- Args:
199
- graph: Dependency graph being populated.
200
- module_name: Source module name for emitted edges.
201
- call_graph: Mapping of caller symbols to callee symbol lists.
202
- python_files: Repository Python files used for callee module resolution.
203
-
204
- Returns:
205
- None.
206
- """
196
+ """Add directed edges from ``module_name`` to callees that resolve to known graph nodes."""
207
197
  for _caller, callees in call_graph.items():
208
198
  for callee in callees:
209
- callee_module = self._resolve_module_from_function(callee, python_files)
199
+ callee_module = self._resolve_module_from_function_preloaded(callee, loaded_contents)
210
200
  if callee_module is None or callee_module not in graph:
211
201
  continue
212
202
  graph.add_edge(module_name, callee_module)
@@ -222,6 +212,7 @@ class GraphAnalyzer:
222
212
  """Populate graph with edges derived from pyan call graphs (parallel phase 2)."""
223
213
  from concurrent.futures import ThreadPoolExecutor, as_completed
224
214
 
215
+ loaded_contents = self._load_python_file_contents_index(python_files)
225
216
  executor = ThreadPoolExecutor(max_workers=max_workers)
226
217
  completed = 0
227
218
  try:
@@ -231,7 +222,7 @@ class GraphAnalyzer:
231
222
  try:
232
223
  call_graph = future.result()
233
224
  module_name = self._path_to_module_name(file_path)
234
- self._add_call_graph_edges_for_module(graph, module_name, call_graph, python_files)
225
+ self._add_call_graph_edges_for_module(graph, module_name, call_graph, loaded_contents)
235
226
  except (OSError, RuntimeError):
236
227
  pass
237
228
  completed += 1
@@ -443,24 +434,30 @@ class GraphAnalyzer:
443
434
  return self._find_matching_module_suffix_overlap(imported, known_modules)
444
435
 
445
436
  @beartype
446
- @require(lambda function_name: isinstance(function_name, str), "Function name must be str")
447
437
  @require(lambda python_files: isinstance(python_files, list), "Python files must be list")
448
- @ensure(lambda result: result is None or isinstance(result, str), "Must return None or str")
449
- def _resolve_module_from_function(self, function_name: str, python_files: list[Path]) -> str | None:
450
- """
451
- Resolve module name from function name.
452
-
453
- This is a heuristic - tries to find the module containing the function.
454
- """
455
- # Simple heuristic: search for function name in files
438
+ @ensure(lambda result: isinstance(result, list), "Must return list")
439
+ def _load_python_file_contents_index(self, python_files: list[Path]) -> list[tuple[str, str]]:
440
+ """Preload (module_name, source_text) pairs once per graph build for callee resolution."""
441
+ loaded: list[tuple[str, str]] = []
456
442
  for file_path in python_files:
457
443
  try:
458
444
  content = file_path.read_text(encoding="utf-8")
459
- if f"def {function_name}" in content or f"class {function_name}" in content:
460
- return self._path_to_module_name(file_path)
461
- except (UnicodeDecodeError, Exception):
445
+ except (OSError, UnicodeDecodeError):
462
446
  continue
447
+ loaded.append((self._path_to_module_name(file_path), content))
448
+ return loaded
463
449
 
450
+ @beartype
451
+ @require(lambda function_name: isinstance(function_name, str), "Function name must be str")
452
+ @require(lambda loaded_contents: isinstance(loaded_contents, list), "Loaded contents must be list")
453
+ @ensure(lambda result: result is None or isinstance(result, str), "Must return None or str")
454
+ def _resolve_module_from_function_preloaded(
455
+ self, function_name: str, loaded_contents: list[tuple[str, str]]
456
+ ) -> str | None:
457
+ """Resolve module for ``function_name`` using preloaded sources (same heuristic as one-file scan)."""
458
+ for module_name, content in loaded_contents:
459
+ if f"def {function_name}" in content or f"class {function_name}" in content:
460
+ return module_name
464
461
  return None
465
462
 
466
463
  @beartype
@@ -283,7 +283,7 @@ class MessageFlowFormatter(logging.Formatter):
283
283
  return formatted_message
284
284
 
285
285
 
286
- @dataclass
286
+ @dataclass(slots=True)
287
287
  class _FileOutputPipelineConfig:
288
288
  logger_name: str
289
289
  logger: logging.Logger
@@ -294,6 +294,7 @@ class _FileOutputPipelineConfig:
294
294
  append_mode: bool
295
295
 
296
296
 
297
+ @beartype
297
298
  @dataclass
298
299
  class LoggerCreateOptions:
299
300
  """Options for :meth:`LoggerSetup.create_logger`."""
@@ -203,10 +203,10 @@ class PersonaExporter:
203
203
  return
204
204
  if spec.use_getattr:
205
205
  val = getattr(feature, spec.field_name, None)
206
- if val:
206
+ if val is not None:
207
207
  feature_dict[spec.field_name] = val
208
208
  return
209
- if spec.value:
209
+ if spec.value is not None:
210
210
  feature_dict[spec.field_name] = spec.value
211
211
 
212
212
  def _merge_feature_optional_sections(
@@ -359,13 +359,7 @@ class OpenAPITestConverter:
359
359
  if isinstance(node, ast.Constant):
360
360
  return node.value
361
361
  if isinstance(node, ast.Dict):
362
- result: dict[str, Any] = {}
363
- for k, v in zip(node.keys, node.values, strict=True):
364
- key = self._extract_ast_value(k) if k else None
365
- val = self._extract_ast_value(v)
366
- if key is not None:
367
- result[str(key)] = val
368
- return result
362
+ return self._coerce_ast_dict_to_plain(node)
369
363
  if isinstance(node, ast.List):
370
364
  return [self._extract_ast_value(item) for item in node.elts]
371
365
  if isinstance(node, ast.Name):
@@ -380,6 +374,18 @@ class OpenAPITestConverter:
380
374
  return None
381
375
 
382
376
  def _examples_from_parsed_test_file(self, tree: ast.AST, func_name: str | None) -> dict[str, dict[str, Any]]:
377
+ """Collect OpenAPI-style examples from test functions in a parsed module.
378
+
379
+ Args:
380
+ tree: Parsed AST for a test module.
381
+ func_name: When set, only this ``test_*`` function is scanned; otherwise every
382
+ ``test_*`` definition is considered.
383
+
384
+ Returns:
385
+ ``operation_id`` → merged example payload. Keys default to ``\"unknown\"`` when
386
+ ``operation_id`` is missing. Later ``test_*`` blocks win on duplicate keys inside
387
+ the merged dict for the same operation.
388
+ """
383
389
  by_operation: dict[str, dict[str, Any]] = {}
384
390
  for node in ast.walk(tree):
385
391
  if not isinstance(node, ast.FunctionDef):
@@ -1,5 +1,5 @@
1
1
  name: init
2
- version: 0.1.25
2
+ version: 0.1.30
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:4b1d6a9399a5417a213f91a9bf0272986ed52ac8cca53f96b9e1899e028f90b4
21
- signature: CfTDUOZ7PrhgJ1+mFg228b+Pvn6crWYaF3hD+jQznXjbn304ONS1g1CePU4JrjBQmeE1RKdoldeAMTHqJ56JCQ==
20
+ checksum: sha256:9e03421972d3254082307834b474e7673957de8c11ffacc563f2da3f35e7cf05
21
+ signature: ikUYhUJ8AFU9RkB+ZBnp0lKrDLT7ZIqkauRYUMRY/swrIjRCTRzHIpNcvCmujK6h1w6EtT/+wGG1v5bZtZB8CQ==
@@ -184,7 +184,7 @@ def _install_marketplace_for_bundle(bid: str, marketplace_id: str, deps: _InitBu
184
184
  try:
185
185
  deps.install_module(
186
186
  marketplace_id,
187
- InstallModuleOptions(
187
+ options=InstallModuleOptions(
188
188
  install_root=deps.root,
189
189
  non_interactive=deps.non_interactive,
190
190
  trust_non_official=deps.trust_non_official,
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import hashlib
6
+ import logging
6
7
  import os
7
8
  import re
8
9
  import shutil
@@ -40,6 +41,7 @@ from specfact_cli.runtime import is_debug_mode
40
41
  USER_MODULES_ROOT = Path.home() / ".specfact" / "modules"
41
42
 
42
43
 
44
+ @beartype
43
45
  @dataclass(slots=True)
44
46
  class InstallModuleOptions:
45
47
  """Options for :func:`install_module`."""
@@ -61,7 +63,7 @@ class _BundleDepsInstallContext:
61
63
  trust_non_official: bool
62
64
  non_interactive: bool
63
65
  force: bool
64
- logger: Any
66
+ logger: logging.Logger
65
67
 
66
68
 
67
69
  MARKETPLACE_MODULES_ROOT = Path.home() / ".specfact" / "marketplace-modules"
@@ -32,10 +32,14 @@ def _match_feature_id_from_pattern_parts(pattern_parts: list[str], file_parts: l
32
32
  feature_id_index = pattern_parts.index("{feature_id}")
33
33
  except ValueError:
34
34
  return None
35
+ if len(file_parts) != len(pattern_parts):
36
+ return None
35
37
  if feature_id_index >= len(file_parts):
36
38
  return None
37
- for i in range(feature_id_index):
38
- if i >= len(file_parts) or pattern_parts[i] != file_parts[i]:
39
+ for i, part in enumerate(pattern_parts):
40
+ if part == "{feature_id}":
41
+ continue
42
+ if i >= len(file_parts) or file_parts[i] != part:
39
43
  return None
40
44
  return file_parts[feature_id_index]
41
45
 
@@ -117,8 +117,6 @@ class SourceArtifactMap:
117
117
  @dataclass(slots=True)
118
118
  class _FeatureLinkingContext:
119
119
  repo_path: Path
120
- impl_files: list[Path]
121
- test_files: list[Path]
122
120
  file_functions_cache: dict[str, list[str]]
123
121
  file_test_functions_cache: dict[str, list[str]]
124
122
  file_hashes_cache: dict[str, str]
@@ -436,8 +434,6 @@ class SourceArtifactScanner:
436
434
 
437
435
  linking_ctx = _FeatureLinkingContext(
438
436
  repo_path=repo_path,
439
- impl_files=impl_files,
440
- test_files=test_files,
441
437
  file_functions_cache=file_functions_cache,
442
438
  file_test_functions_cache=file_test_functions_cache,
443
439
  file_hashes_cache=file_hashes_cache,
@@ -188,21 +188,29 @@ def detect_direct_manipulation(specfact_dir: Path) -> list[Path]:
188
188
 
189
189
  # Check project bundles
190
190
  projects_dir = specfact_dir / "projects"
191
- if not projects_dir.exists():
191
+ if not projects_dir.is_dir():
192
192
  return suspicious_files
193
193
 
194
- for bundle_dir in projects_dir.iterdir():
195
- if not bundle_dir.is_dir():
196
- continue
197
- manifest_file = bundle_dir / "bundle.manifest.yaml"
198
- if manifest_file.exists() and not is_cli_generated(manifest_file):
199
- suspicious_files.append(manifest_file)
194
+ try:
195
+ bundle_iter = list(projects_dir.iterdir())
196
+ except OSError:
197
+ return suspicious_files
200
198
 
201
- features_dir = bundle_dir / "features"
202
- if not features_dir.exists():
199
+ for bundle_dir in bundle_iter:
200
+ try:
201
+ if not bundle_dir.is_dir():
202
+ continue
203
+ manifest_file = bundle_dir / "bundle.manifest.yaml"
204
+ if manifest_file.exists() and not is_cli_generated(manifest_file):
205
+ suspicious_files.append(manifest_file)
206
+
207
+ features_dir = bundle_dir / "features"
208
+ if not features_dir.is_dir():
209
+ continue
210
+ for feature_file in features_dir.glob("*.yaml"):
211
+ if not is_cli_generated(feature_file):
212
+ suspicious_files.append(feature_file)
213
+ except OSError:
203
214
  continue
204
- for feature_file in features_dir.glob("*.yaml"):
205
- if not is_cli_generated(feature_file):
206
- suspicious_files.append(feature_file)
207
215
 
208
216
  return suspicious_files