specsmith 0.11.5.dev450__tar.gz → 0.11.5.dev452__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 (285) hide show
  1. {specsmith-0.11.5.dev450/src/specsmith.egg-info → specsmith-0.11.5.dev452}/PKG-INFO +1 -1
  2. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/pyproject.toml +1 -1
  3. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/cli.py +7 -2
  4. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/requirements.py +35 -3
  5. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/sync.py +2 -1
  6. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/governance/rules.md.j2 +15 -0
  7. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/python/pyproject.toml.j2 +7 -0
  8. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/validator.py +92 -0
  9. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452/src/specsmith.egg-info}/PKG-INFO +1 -1
  10. specsmith-0.11.5.dev452/tests/test_auditor.py +201 -0
  11. specsmith-0.11.5.dev450/tests/test_auditor.py +0 -89
  12. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/LICENSE +0 -0
  13. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/README.md +0 -0
  14. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/setup.cfg +0 -0
  15. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/epistemic/__init__.py +0 -0
  16. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/epistemic/belief.py +0 -0
  17. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/epistemic/certainty.py +0 -0
  18. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/epistemic/failure_graph.py +0 -0
  19. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/epistemic/py.typed +0 -0
  20. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/epistemic/recovery.py +0 -0
  21. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/epistemic/session.py +0 -0
  22. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/epistemic/stress_tester.py +0 -0
  23. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/epistemic/trace.py +0 -0
  24. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/__init__.py +0 -0
  25. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/__main__.py +0 -0
  26. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/__init__.py +0 -0
  27. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/broker.py +0 -0
  28. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/chat_runner.py +0 -0
  29. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/cleanup.py +0 -0
  30. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/context_seed.py +0 -0
  31. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/core.py +0 -0
  32. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/dispatch/__init__.py +0 -0
  33. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/dispatch/_status.py +0 -0
  34. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/dispatch/dag.py +0 -0
  35. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/dispatch/dispatcher.py +0 -0
  36. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/dispatch/events.py +0 -0
  37. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/dispatch/result.py +0 -0
  38. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/endpoints.py +0 -0
  39. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/events.py +0 -0
  40. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/execution_profiles.py +0 -0
  41. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/fallback.py +0 -0
  42. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/hf_leaderboard.py +0 -0
  43. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/hf_sync.py +0 -0
  44. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/indexer.py +0 -0
  45. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/llm_client.py +0 -0
  46. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/mcp.py +0 -0
  47. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/memory.py +0 -0
  48. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/model_intelligence.py +0 -0
  49. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/model_profiles.py +0 -0
  50. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/orchestrator.py +0 -0
  51. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/permissions.py +0 -0
  52. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/profiles.py +0 -0
  53. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/provider_registry.py +0 -0
  54. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/repl.py +0 -0
  55. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/router.py +0 -0
  56. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/rules.py +0 -0
  57. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/runner.py +0 -0
  58. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/safety.py +0 -0
  59. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/spawner.py +0 -0
  60. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/suggester.py +0 -0
  61. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/teams.py +0 -0
  62. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/tools.py +0 -0
  63. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/verifier.py +0 -0
  64. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/agent/voice.py +0 -0
  65. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/architect.py +0 -0
  66. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/auditor.py +0 -0
  67. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/auth.py +0 -0
  68. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/block_export.py +0 -0
  69. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/channel.py +0 -0
  70. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/ci_manager.py +0 -0
  71. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/commands/__init__.py +0 -0
  72. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/commands/intelligence.py +0 -0
  73. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/compliance/__init__.py +0 -0
  74. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/compliance/_compat.py +0 -0
  75. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/compliance/checker.py +0 -0
  76. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/compliance/evidence.py +0 -0
  77. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/compliance/regulations.py +0 -0
  78. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/compliance/reporter.py +0 -0
  79. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/compressor.py +0 -0
  80. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/config.py +0 -0
  81. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/console_utils.py +0 -0
  82. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/context_orchestrator.py +0 -0
  83. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/context_window.py +0 -0
  84. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/credit_analyzer.py +0 -0
  85. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/credits.py +0 -0
  86. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/datasources/__init__.py +0 -0
  87. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/datasources/base.py +0 -0
  88. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/datasources/citations.py +0 -0
  89. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/datasources/fpd.py +0 -0
  90. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/datasources/odp.py +0 -0
  91. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/datasources/patentsview.py +0 -0
  92. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/datasources/pfw.py +0 -0
  93. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/datasources/ppubs.py +0 -0
  94. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/datasources/ptab.py +0 -0
  95. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/differ.py +0 -0
  96. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/doctor.py +0 -0
  97. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/drive.py +0 -0
  98. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/editor.py +0 -0
  99. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/epistemic/__init__.py +0 -0
  100. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/epistemic/belief.py +0 -0
  101. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/epistemic/certainty.py +0 -0
  102. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/epistemic/failure_graph.py +0 -0
  103. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/epistemic/recovery.py +0 -0
  104. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/epistemic/stress_tester.py +0 -0
  105. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/esdb/__init__.py +0 -0
  106. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/esdb/bridge.py +0 -0
  107. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/eval/__init__.py +0 -0
  108. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/eval/builtins.py +0 -0
  109. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/eval/runner.py +0 -0
  110. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/executor.py +0 -0
  111. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/exporter.py +0 -0
  112. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/governance_logic.py +0 -0
  113. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/governance_store.py +0 -0
  114. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/governance_yaml.py +0 -0
  115. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/gui/__init__.py +0 -0
  116. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/gui/app.py +0 -0
  117. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/gui/main_window.py +0 -0
  118. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/gui/session_tab.py +0 -0
  119. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/gui/theme.py +0 -0
  120. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/gui/widgets/__init__.py +0 -0
  121. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/gui/widgets/chat_view.py +0 -0
  122. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/gui/widgets/input_bar.py +0 -0
  123. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/gui/widgets/provider_bar.py +0 -0
  124. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/gui/widgets/token_meter.py +0 -0
  125. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/gui/widgets/tool_panel.py +0 -0
  126. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/gui/widgets/update_checker.py +0 -0
  127. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/gui/worker.py +0 -0
  128. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/history_search.py +0 -0
  129. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/importer.py +0 -0
  130. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/instinct.py +0 -0
  131. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/integrations/__init__.py +0 -0
  132. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/integrations/agent_skill.py +0 -0
  133. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/integrations/aider.py +0 -0
  134. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/integrations/base.py +0 -0
  135. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/integrations/claude_code.py +0 -0
  136. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/integrations/codity.py +0 -0
  137. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/integrations/copilot.py +0 -0
  138. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/integrations/cursor.py +0 -0
  139. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/integrations/gemini.py +0 -0
  140. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/integrations/windsurf.py +0 -0
  141. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/issue_reporter.py +0 -0
  142. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/languages.py +0 -0
  143. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/ledger.py +0 -0
  144. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/mcp_generator.py +0 -0
  145. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/migrations/__init__.py +0 -0
  146. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/migrations/m001_governance_yaml.py +0 -0
  147. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/migrations/m002_agents_slim.py +0 -0
  148. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/migrations/m003_compliance_init.py +0 -0
  149. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/migrations/m004_ledger_esdb.py +0 -0
  150. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/migrations/m005_agent_run_tool.py +0 -0
  151. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/migrations/m006_session_governance.py +0 -0
  152. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/migrations/runner.py +0 -0
  153. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/ollama_cmds.py +0 -0
  154. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/patent.py +0 -0
  155. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/paths.py +0 -0
  156. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/phase.py +0 -0
  157. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/plugins.py +0 -0
  158. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/profiles.py +0 -0
  159. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/rate_limits.py +0 -0
  160. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/releaser.py +0 -0
  161. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/requirements_parser.py +0 -0
  162. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/retrieval.py +0 -0
  163. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/safe_write.py +0 -0
  164. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/scaffolder.py +0 -0
  165. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/serve.py +0 -0
  166. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/session.py +0 -0
  167. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/session_init.py +0 -0
  168. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/session_store.py +0 -0
  169. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/skills/__init__.py +0 -0
  170. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/skills/cloud.py +0 -0
  171. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/skills/corporate.py +0 -0
  172. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/skills/cross_platform.py +0 -0
  173. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/skills/devops.py +0 -0
  174. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/skills/docs.py +0 -0
  175. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/skills/embedded.py +0 -0
  176. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/skills/governance.py +0 -0
  177. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/skills/hardware.py +0 -0
  178. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/skills/mobile.py +0 -0
  179. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/skills/productivity.py +0 -0
  180. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/skills/ssh.py +0 -0
  181. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/skills_builder.py +0 -0
  182. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/agents.md.j2 +0 -0
  183. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/community/bug_report.md.j2 +0 -0
  184. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/community/code_of_conduct.md.j2 +0 -0
  185. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/community/contributing.md.j2 +0 -0
  186. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/community/feature_request.md.j2 +0 -0
  187. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/community/license-Apache-2.0.j2 +0 -0
  188. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/community/license-MIT.j2 +0 -0
  189. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/community/pull_request_template.md.j2 +0 -0
  190. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/community/security.md.j2 +0 -0
  191. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/docs/architecture.md.j2 +0 -0
  192. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/docs/mkdocs.yml.j2 +0 -0
  193. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/docs/readthedocs.yaml.j2 +0 -0
  194. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/docs/requirements.md.j2 +0 -0
  195. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/docs/test-spec.md.j2 +0 -0
  196. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/editorconfig.j2 +0 -0
  197. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/gitattributes.j2 +0 -0
  198. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/gitignore.j2 +0 -0
  199. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/go/go.mod.j2 +0 -0
  200. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/go/main.go.j2 +0 -0
  201. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/governance/belief-registry.md.j2 +0 -0
  202. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/governance/context-budget.md.j2 +0 -0
  203. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/governance/drift-metrics.md.j2 +0 -0
  204. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/governance/epistemic-axioms.md.j2 +0 -0
  205. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/governance/failure-modes.md.j2 +0 -0
  206. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/governance/lifecycle.md.j2 +0 -0
  207. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/governance/roles.md.j2 +0 -0
  208. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/governance/session-protocol.md.j2 +0 -0
  209. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/governance/uncertainty-map.md.j2 +0 -0
  210. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/governance/verification.md.j2 +0 -0
  211. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/js/package.json.j2 +0 -0
  212. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/ledger.md.j2 +0 -0
  213. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/python/cli.py.j2 +0 -0
  214. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/python/init.py.j2 +0 -0
  215. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/readme.md.j2 +0 -0
  216. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/rust/Cargo.toml.j2 +0 -0
  217. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/rust/main.rs.j2 +0 -0
  218. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/scripts/exec.cmd.j2 +0 -0
  219. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/scripts/exec.sh.j2 +0 -0
  220. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/scripts/run.cmd.j2 +0 -0
  221. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/scripts/run.sh.j2 +0 -0
  222. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/scripts/setup.cmd.j2 +0 -0
  223. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/scripts/setup.sh.j2 +0 -0
  224. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/templates/workflows/release.yml.j2 +0 -0
  225. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/tool_installer.py +0 -0
  226. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/toolrules.py +0 -0
  227. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/tools.py +0 -0
  228. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/trace.py +0 -0
  229. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/updater.py +0 -0
  230. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/upgrader.py +0 -0
  231. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/vcs/__init__.py +0 -0
  232. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/vcs/base.py +0 -0
  233. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/vcs/bitbucket.py +0 -0
  234. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/vcs/github.py +0 -0
  235. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/vcs/gitlab.py +0 -0
  236. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/vcs_commands.py +0 -0
  237. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/wireframes.py +0 -0
  238. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith/workspace.py +0 -0
  239. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith.egg-info/SOURCES.txt +0 -0
  240. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith.egg-info/dependency_links.txt +0 -0
  241. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith.egg-info/entry_points.txt +0 -0
  242. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith.egg-info/requires.txt +0 -0
  243. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/src/specsmith.egg-info/top_level.txt +0 -0
  244. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_CMD_001.py +0 -0
  245. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_agent_profiles.py +0 -0
  246. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_agent_runner_ready.py +0 -0
  247. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_ai_client.py +0 -0
  248. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_ai_intelligence.py +0 -0
  249. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_channel.py +0 -0
  250. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_chat_diff_decision.py +0 -0
  251. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_chat_runner_openai_compat.py +0 -0
  252. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_chat_stdin_protocol.py +0 -0
  253. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_cli.py +0 -0
  254. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_cli_workflows_history_drive.py +0 -0
  255. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_compliance.py +0 -0
  256. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_compressor.py +0 -0
  257. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_dispatch.py +0 -0
  258. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_e2e_nexus.py +0 -0
  259. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_endpoints_cli.py +0 -0
  260. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_endpoints_store.py +0 -0
  261. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_epistemic.py +0 -0
  262. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_fallback_chain.py +0 -0
  263. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_importer.py +0 -0
  264. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_integrations.py +0 -0
  265. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_integrations_codity.py +0 -0
  266. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_intelligence.py +0 -0
  267. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_issue_reporter.py +0 -0
  268. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_mcp_client.py +0 -0
  269. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_new_modules.py +0 -0
  270. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_nexus.py +0 -0
  271. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_permissions.py +0 -0
  272. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_phase1_4_new.py +0 -0
  273. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_phase34_completion.py +0 -0
  274. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_rate_limits.py +0 -0
  275. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_req_248_262.py +0 -0
  276. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_scaffolder.py +0 -0
  277. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_skill_marketplace.py +0 -0
  278. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_skills_mcp.py +0 -0
  279. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_smoke.py +0 -0
  280. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_suggester.py +0 -0
  281. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_tools.py +0 -0
  282. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_validator.py +0 -0
  283. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_vcs.py +0 -0
  284. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_warp_parity.py +0 -0
  285. {specsmith-0.11.5.dev450 → specsmith-0.11.5.dev452}/tests/test_warp_parity_followup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specsmith
3
- Version: 0.11.5.dev450
3
+ Version: 0.11.5.dev452
4
4
  Summary: Applied Epistemic Engineering toolkit — AEE agent sessions, execution profiles, FPGA/HDL governance, tool installer, 50+ CLI commands.
5
5
  Author: BitConcepts
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "specsmith"
7
- version = "0.11.5.dev450"
7
+ version = "0.11.5.dev452"
8
8
  description = "Applied Epistemic Engineering toolkit — AEE agent sessions, execution profiles, FPGA/HDL governance, tool installer, 50+ CLI commands."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -4778,8 +4778,13 @@ def watch_cmd(project_dir: str, interval: int, no_notify: bool) -> None:
4778
4778
  _has_watchdog = False
4779
4779
  if not _has_watchdog:
4780
4780
  console.print(
4781
- "[dim]watchdog not installed — using polling mode. "
4782
- "For faster detection: pip install watchdog[/dim]\n"
4781
+ "[yellow]⚠[/yellow] [dim]watchdog not installed — using polling mode "
4782
+ f"({interval}s interval).[/dim]\n"
4783
+ " [dim]For native filesystem events, add [bold]watchdog>=4.0[/bold] to your "
4784
+ "project's dev extras:[/dim]\n"
4785
+ " [dim] pip install watchdog[/dim]\n"
4786
+ " [dim] or add to pyproject.toml: "
4787
+ '[bold][project.optional-dependencies] dev = ["watchdog>=4.0"][/bold][/dim]\n'
4783
4788
  )
4784
4789
 
4785
4790
  from specsmith.auditor import run_audit
@@ -9,15 +9,18 @@ from pathlib import Path
9
9
 
10
10
  # Flexible patterns that handle both two-part (REQ-001) and three-part
11
11
  # (REQ-CLI-001, REG-012) identifiers used across projects.
12
+ # Letter suffixes (e.g. TEST-NN-002a, TEST-NN-002b) are supported via [a-z]* —
13
+ # without this, the \b word boundary after \d+ would not match when a letter
14
+ # follows digits, causing the ID to be silently skipped (#183).
12
15
  _FLEX_REQ = r"REQ-(?:[A-Z][A-Z0-9_]*-)?\d+"
13
- _FLEX_TEST = r"TEST-(?:[A-Z][A-Z0-9_]*-)?\d+"
16
+ _FLEX_TEST = r"TEST-(?:[A-Z][A-Z0-9_]*-)?\d+[a-z]*"
14
17
 
15
18
  _REQ_PATTERN = re.compile(r"\b(" + _FLEX_REQ + r")\b")
16
19
  _TEST_COVERS_PATTERN = re.compile(
17
20
  r"(?:Covers|\*\*Requirement(?:\s+ID)?:?\*\*|Requirement(?:\s+ID)?):?\s*"
18
21
  r"(" + _FLEX_REQ + r"(?:\s*,\s*" + _FLEX_REQ + r")*)"
19
22
  )
20
- _TEST_ID_PATTERN = re.compile(r"\b(" + _FLEX_TEST + r")\b")
23
+ _TEST_ID_PATTERN = re.compile(r"\b(TEST-(?:[A-Z][A-Z0-9_]*-)?\d+[a-z]*)\b")
21
24
 
22
25
  # Heading detectors for REQUIREMENTS.md (two styles supported):
23
26
  # Style A: ## REQ-001 or ## REQ-CLI-001
@@ -111,9 +114,18 @@ def add_req(
111
114
 
112
115
 
113
116
  def trace_reqs(root: Path) -> list[dict[str, object]]:
114
- """Map each REQ to its covering TESTs."""
117
+ """Map each REQ to its covering TESTs.
118
+
119
+ In YAML-first mode, reads .specsmith/testcases.json directly — this avoids
120
+ regex-based ID parsing entirely and correctly handles letter-suffix IDs
121
+ (e.g. TEST-NN-002a, TEST-NN-002b) that were previously misidentified (#183).
122
+ Falls back to TESTS.md regex parsing in legacy Markdown mode.
123
+ """
124
+ import json as _json
125
+
115
126
  req_path = root / "docs" / "REQUIREMENTS.md"
116
127
  test_path = root / "docs" / "TESTS.md"
128
+ testcases_json = root / ".specsmith" / "testcases.json"
117
129
 
118
130
  req_ids: list[str] = []
119
131
  if req_path.exists():
@@ -121,6 +133,26 @@ def trace_reqs(root: Path) -> list[dict[str, object]]:
121
133
 
122
134
  covered_by: dict[str, list[str]] = {r: [] for r in req_ids}
123
135
 
136
+ # YAML mode: prefer machine-readable testcases.json — exact IDs, no regex.
137
+ if testcases_json.is_file():
138
+ try:
139
+ records = _json.loads(testcases_json.read_text(encoding="utf-8"))
140
+ for record in records:
141
+ if (
142
+ isinstance(record, dict)
143
+ and isinstance(record.get("requirement_id"), str)
144
+ and isinstance(record.get("id"), str)
145
+ ):
146
+ rid = record["requirement_id"]
147
+ if rid in covered_by:
148
+ covered_by[rid].append(record["id"])
149
+ return [
150
+ {"req": r, "tests": tests, "covered": len(tests) > 0}
151
+ for r, tests in covered_by.items()
152
+ ]
153
+ except (OSError, ValueError):
154
+ pass # Fall through to TESTS.md regex parsing
155
+
124
156
  if test_path.exists():
125
157
  test_text = test_path.read_text(encoding="utf-8")
126
158
  current_test = ""
@@ -41,7 +41,8 @@ _DIRECT_HEADING = re.compile(r"^#{1,3}\s+(" + _FLEX_REQ_ID + r")\b")
41
41
  _ID_FIELD = re.compile(r"^-\s+\*\*ID:\*\*\s+(" + _FLEX_REQ_ID + r")")
42
42
  _FIELD_LINE = re.compile(r"^-\s+\*\*(.+?):\*\*\s+(.+)")
43
43
 
44
- _FLEX_TEST_ID = r"TEST-(?:[A-Z][A-Z0-9_]*-)?\d+"
44
+ # Letter suffixes (e.g. TEST-NN-002a) are supported via [a-z]* — fixes #183.
45
+ _FLEX_TEST_ID = r"TEST-(?:[A-Z][A-Z0-9_]*-)?\d+[a-z]*"
45
46
  _TEST_NUMBERED_HEADING = re.compile(r"^#{1,3}\s+(?:TEST-[A-Z0-9_-]+\s+)?(.+?)\s*$")
46
47
  _TEST_ID_FIELD = re.compile(r"^-\s+\*\*ID:\*\*\s+(" + _FLEX_TEST_ID + r")")
47
48
 
@@ -55,6 +55,21 @@ All proposals MUST state their epistemic boundaries. A proposal without explicit
55
55
 
56
56
  Hidden assumptions are not acceptable. Declare all epistemic boundaries in the `Assumptions:` field of every proposal.
57
57
 
58
+ ### H23 — No Bare Sleep Delays in Scripts
59
+ Scripts MUST NOT use `sleep N`, `Start-Sleep`, or equivalent blocking waits as a
60
+ standalone timing mechanism without a guaranteed exit path.
61
+
62
+ Every wait that depends on an external condition MUST use a polling loop with:
63
+ 1. A **maximum iteration count** or **wall-clock timeout**
64
+ 2. A **non-zero exit code** on timeout
65
+ 3. An **explicit sleep interval** inside the loop body
66
+
67
+ **Applies to:** `.sh`, `.bash`, `.ps1`, `.cmd`, `.bat`, CI `run:` blocks, Makefile recipes.
68
+ **Does NOT apply to:** production source code (`time.sleep`, `thread::sleep`, etc.).
69
+
70
+ `specsmith validate` checks scripts for bare sleep patterns without loop/retry constructs
71
+ and emits a warning (H23).
72
+
58
73
  ---
59
74
 
60
75
  ## Stop Conditions
@@ -14,5 +14,12 @@ license = {text = "MIT"}
14
14
  {{ project.package_name }} = "{{ project.package_name }}.cli:main"
15
15
  {% endif %}
16
16
 
17
+ [project.optional-dependencies]
18
+ dev = [
19
+ "pytest>=7.0",
20
+ "ruff>=0.4",
21
+ "watchdog>=4.0", # specsmith watch — native filesystem events
22
+ ]
23
+
17
24
  [tool.setuptools.packages.find]
18
25
  where = ["src"]
@@ -63,6 +63,25 @@ _DEADLINE_GUARD_PATTERNS = (
63
63
  # Script file extensions to scan (exclude general source dirs to avoid false positives)
64
64
  _SCRIPT_EXTENSIONS = {".sh", ".cmd", ".ps1", ".bash"}
65
65
 
66
+ # Bare-sleep patterns: standalone timing delays that block indefinitely (H23)
67
+ # Applies to scripts only — NOT to general source code.
68
+ _BARE_SLEEP_PATTERNS = (
69
+ re.compile(r"^\s*sleep\s+\d", re.MULTILINE | re.IGNORECASE), # bash: sleep N
70
+ re.compile(r"^\s*Start-Sleep\b", re.MULTILINE | re.IGNORECASE), # PowerShell
71
+ )
72
+
73
+ # Loop / retry constructs that justify a sleep — if any are present the sleep
74
+ # is inside a polling loop and should not be flagged.
75
+ _SLEEP_LOOP_CONTEXT = (
76
+ re.compile(r"\bfor\b"),
77
+ re.compile(r"\bwhile\b"),
78
+ re.compile(r"\buntil\b"),
79
+ re.compile(r"max_retries", re.IGNORECASE),
80
+ re.compile(r"max_attempts", re.IGNORECASE),
81
+ re.compile(r"\bretry\b", re.IGNORECASE),
82
+ re.compile(r"\bseq\s+\d"), # bash `for i in $(seq N)`
83
+ )
84
+
66
85
 
67
86
  def _check_scaffold_yml(root: Path) -> list[ValidationResult]:
68
87
  """Check that scaffold.yml exists and is valid YAML."""
@@ -333,6 +352,78 @@ def _check_blocking_loops(root: Path) -> list[ValidationResult]:
333
352
  return results
334
353
 
335
354
 
355
+ def _check_bare_sleep(root: Path) -> list[ValidationResult]:
356
+ """Scan script files for bare sleep delays without a loop/retry context (H23).
357
+
358
+ A bare sleep is a ``sleep N`` / ``Start-Sleep`` line that appears in a script
359
+ file that contains NO loop or retry constructs. This is a timing anti-pattern
360
+ that blocks indefinitely when the awaited condition never arrives.
361
+
362
+ Files with a loop/retry context (``for``, ``while``, ``until``, ``max_retries``)
363
+ are skipped — the sleep is part of a legitimate polling loop in those cases.
364
+
365
+ Emits a warning (not a hard error) to allow gradual adoption.
366
+ """
367
+ results: list[ValidationResult] = []
368
+
369
+ candidates: list[Path] = []
370
+ for path in root.iterdir():
371
+ if path.is_file() and path.suffix.lower() in _SCRIPT_EXTENSIONS:
372
+ candidates.append(path)
373
+ scripts_dir = root / "scripts"
374
+ if scripts_dir.is_dir():
375
+ for path in scripts_dir.rglob("*"):
376
+ if path.is_file() and path.suffix.lower() in _SCRIPT_EXTENSIONS:
377
+ candidates.append(path)
378
+
379
+ if not candidates:
380
+ return results
381
+
382
+ flagged: list[str] = []
383
+ for script_path in candidates:
384
+ try:
385
+ text = script_path.read_text(encoding="utf-8", errors="replace")
386
+ except OSError:
387
+ continue
388
+
389
+ has_sleep = any(p.search(text) for p in _BARE_SLEEP_PATTERNS)
390
+ if not has_sleep:
391
+ continue
392
+
393
+ # Sleep inside a loop/retry context is acceptable
394
+ has_loop_context = any(p.search(text) for p in _SLEEP_LOOP_CONTEXT)
395
+ if not has_loop_context:
396
+ try:
397
+ rel = script_path.relative_to(root)
398
+ except ValueError:
399
+ rel = script_path
400
+ flagged.append(str(rel))
401
+
402
+ if flagged:
403
+ for name in flagged:
404
+ results.append(
405
+ ValidationResult(
406
+ name=f"bare-sleep:{name}",
407
+ passed=False,
408
+ message=(
409
+ f"{name}: bare sleep delay without a polling loop (H23 warning). "
410
+ "Replace with a retry loop that has a max iteration count and "
411
+ "non-zero exit on timeout."
412
+ ),
413
+ )
414
+ )
415
+ else:
416
+ results.append(
417
+ ValidationResult(
418
+ name="bare-sleep",
419
+ passed=True,
420
+ message=f"{len(candidates)} script file(s) checked — no bare sleep delays found",
421
+ )
422
+ )
423
+
424
+ return results
425
+
426
+
336
427
  def run_validate(root: Path) -> ValidationReport:
337
428
  """Run all validation checks and return a report."""
338
429
  report = ValidationReport()
@@ -341,4 +432,5 @@ def run_validate(root: Path) -> ValidationReport:
341
432
  report.results.extend(_check_req_ids_unique(root))
342
433
  report.results.extend(_check_architecture_reqs(root))
343
434
  report.results.extend(_check_blocking_loops(root))
435
+ report.results.extend(_check_bare_sleep(root))
344
436
  return report
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specsmith
3
- Version: 0.11.5.dev450
3
+ Version: 0.11.5.dev452
4
4
  Summary: Applied Epistemic Engineering toolkit — AEE agent sessions, execution profiles, FPGA/HDL governance, tool installer, 50+ CLI commands.
5
5
  Author: BitConcepts
6
6
  License-Expression: MIT
@@ -0,0 +1,201 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 BitConcepts, LLC. All rights reserved.
3
+ """Tests for specsmith auditor."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ from specsmith.auditor import run_audit
12
+
13
+
14
+ @pytest.fixture
15
+ def governed_project(tmp_path: Path) -> Path:
16
+ """Create a minimal governed project for audit testing."""
17
+ (tmp_path / "AGENTS.md").write_text("# AGENTS\nShort hub.\n", encoding="utf-8")
18
+ (tmp_path / "LEDGER.md").write_text("# Ledger\n\n## Session 1\nDone.\n", encoding="utf-8")
19
+
20
+ gov = tmp_path / "docs" / "governance"
21
+ gov.mkdir(parents=True)
22
+ for f in (
23
+ "RULES.md",
24
+ "SESSION-PROTOCOL.md",
25
+ "LIFECYCLE.md",
26
+ "ROLES.md",
27
+ "CONTEXT-BUDGET.md",
28
+ "VERIFICATION.md",
29
+ "DRIFT-METRICS.md",
30
+ ):
31
+ (gov / f).write_text(f"# {f}\nContent.\n", encoding="utf-8")
32
+
33
+ docs = tmp_path / "docs"
34
+ (docs / "REQUIREMENTS.md").write_text("# Reqs\n", encoding="utf-8")
35
+ (docs / "TESTS.md").write_text("# Tests\n", encoding="utf-8")
36
+ (docs / "ARCHITECTURE.md").write_text("# Arch\n", encoding="utf-8")
37
+ (tmp_path / "CONTRIBUTING.md").write_text("# Contributing\n", encoding="utf-8")
38
+ (tmp_path / "LICENSE").write_text("MIT License\n", encoding="utf-8")
39
+ # Scaffold config (at root for backward compat — audit now accepts either location)
40
+ (tmp_path / "scaffold.yml").write_text(
41
+ "name: test-project\ntype: cli-python\nspec_version: 0.10.1\nvcs_platform: github\n",
42
+ encoding="utf-8",
43
+ )
44
+
45
+ return tmp_path
46
+
47
+
48
+ class TestAuditHealthy:
49
+ def test_passes_on_governed_project(self, governed_project: Path) -> None:
50
+ report = run_audit(governed_project)
51
+ assert report.healthy
52
+ assert report.failed == 0
53
+
54
+ def test_reports_pass_count(self, governed_project: Path) -> None:
55
+ report = run_audit(governed_project)
56
+ assert report.passed > 0
57
+
58
+
59
+ class TestAuditDetectsIssues:
60
+ def test_missing_agents_md(self, tmp_path: Path) -> None:
61
+ (tmp_path / "LEDGER.md").write_text("# Ledger\n", encoding="utf-8")
62
+ report = run_audit(tmp_path)
63
+ failed_names = [r.name for r in report.results if not r.passed]
64
+ assert "file-exists:AGENTS.md" in failed_names
65
+
66
+ def test_missing_ledger(self, tmp_path: Path) -> None:
67
+ (tmp_path / "AGENTS.md").write_text("# Agents\n", encoding="utf-8")
68
+ report = run_audit(tmp_path)
69
+ failed_names = [r.name for r in report.results if not r.passed]
70
+ assert "file-exists:LEDGER.md" in failed_names
71
+
72
+ def test_oversized_ledger(self, governed_project: Path) -> None:
73
+ big_ledger = "# Ledger\n" + "\n".join(f"Line {i}" for i in range(600))
74
+ (governed_project / "LEDGER.md").write_text(big_ledger, encoding="utf-8")
75
+ report = run_audit(governed_project)
76
+ size_results = [r for r in report.results if r.name == "ledger-size"]
77
+ assert len(size_results) == 1
78
+ assert not size_results[0].passed
79
+
80
+ def test_req_test_coverage(self, governed_project: Path) -> None:
81
+ reqs = "# Reqs\n\nREQ-CLI-001: do things\nREQ-CLI-002: more things\n"
82
+ tests = "# Tests\n\nTEST-CLI-001\nCovers: REQ-CLI-001\n"
83
+ (governed_project / "docs" / "REQUIREMENTS.md").write_text(reqs, encoding="utf-8")
84
+ (governed_project / "docs" / "TESTS.md").write_text(tests, encoding="utf-8")
85
+ report = run_audit(governed_project)
86
+ coverage = [r for r in report.results if r.name == "req-test-coverage"]
87
+ assert len(coverage) == 1
88
+ assert not coverage[0].passed
89
+ assert "REQ-CLI-002" in coverage[0].message
90
+
91
+
92
+ # ---------------------------------------------------------------------------
93
+ # Regression: issue #183 — letter-suffix TEST IDs in req trace
94
+ # ---------------------------------------------------------------------------
95
+
96
+
97
+ class TestReqTraceLetterSuffixRegression:
98
+ """Regression tests for #183 — req trace misidentifies TEST-NN-002a/b.
99
+
100
+ Root cause: _TEST_ID_PATTERN used \\d+\\b which fails to match when a
101
+ letter immediately follows digits (\\b requires a non-word char boundary,
102
+ but letters are word chars). This caused current_test to remain pointing
103
+ at the last successfully-parsed ID (TEST-NN-020), so both TEST-NN-002a and
104
+ TEST-NN-002b coverage lines were erroneously attributed to TEST-NN-020.
105
+ """
106
+
107
+ def test_letter_suffix_ids_captured_from_tests_md(self, tmp_path: Path) -> None:
108
+ """trace_reqs must return TEST-NN-002a / TEST-NN-002b, not TEST-NN-020 twice."""
109
+ from specsmith.requirements import trace_reqs
110
+
111
+ docs = tmp_path / "docs"
112
+ docs.mkdir()
113
+ (docs / "REQUIREMENTS.md").write_text(
114
+ "# Requirements\n\n## REQ-NN-001\n- **Status**: defined\n\n"
115
+ "## REQ-NN-002\n- **Status**: defined\n",
116
+ encoding="utf-8",
117
+ )
118
+ # Simulate the buggy scenario: TEST-NN-020 appears BEFORE TEST-NN-002a/b
119
+ # so it would stick as current_test if letter suffixes aren't parsed.
120
+ (docs / "TESTS.md").write_text(
121
+ "# Tests\n\n"
122
+ "## TEST-NN-020\n- **Requirement ID**: REQ-NN-001\n\n"
123
+ "## TEST-NN-002a\n- **Requirement ID**: REQ-NN-002\n\n"
124
+ "## TEST-NN-002b\n- **Requirement ID**: REQ-NN-002\n",
125
+ encoding="utf-8",
126
+ )
127
+
128
+ traces = trace_reqs(tmp_path)
129
+ by_req = {t["req"]: t["tests"] for t in traces}
130
+
131
+ # REQ-NN-001 should map to TEST-NN-020 only
132
+ assert by_req.get("REQ-NN-001") == ["TEST-NN-020"]
133
+ # REQ-NN-002 must map to TEST-NN-002a and TEST-NN-002b — NOT TEST-NN-020 twice
134
+ assert "TEST-NN-002a" in by_req.get("REQ-NN-002", [])
135
+ assert "TEST-NN-002b" in by_req.get("REQ-NN-002", [])
136
+ assert "TEST-NN-020" not in by_req.get("REQ-NN-002", [])
137
+
138
+ def test_no_duplicate_ids_in_trace(self, tmp_path: Path) -> None:
139
+ """REQ-NN-002 must not have duplicate entries in its test list."""
140
+ from specsmith.requirements import trace_reqs
141
+
142
+ docs = tmp_path / "docs"
143
+ docs.mkdir()
144
+ (docs / "REQUIREMENTS.md").write_text(
145
+ "# Requirements\n\n## REQ-NN-002\n- **Status**: defined\n",
146
+ encoding="utf-8",
147
+ )
148
+ (docs / "TESTS.md").write_text(
149
+ "# Tests\n\n"
150
+ "## TEST-NN-002a\n- **Requirement ID**: REQ-NN-002\n\n"
151
+ "## TEST-NN-002b\n- **Requirement ID**: REQ-NN-002\n",
152
+ encoding="utf-8",
153
+ )
154
+
155
+ traces = trace_reqs(tmp_path)
156
+ tests = traces[0]["tests"]
157
+ assert len(tests) == len(set(tests)), f"Duplicate test IDs: {tests}"
158
+
159
+ def test_yaml_mode_uses_testcases_json(self, tmp_path: Path) -> None:
160
+ """In YAML mode, trace_reqs reads testcases.json directly (no regex)."""
161
+ import json
162
+
163
+ from specsmith.requirements import trace_reqs
164
+
165
+ docs = tmp_path / "docs"
166
+ docs.mkdir()
167
+ (docs / "REQUIREMENTS.md").write_text(
168
+ "# Requirements\n\n## REQ-NN-002\n- **Status**: defined\n",
169
+ encoding="utf-8",
170
+ )
171
+ state = tmp_path / ".specsmith"
172
+ state.mkdir()
173
+ (state / "testcases.json").write_text(
174
+ json.dumps(
175
+ [
176
+ {"id": "TEST-NN-002a", "requirement_id": "REQ-NN-002"},
177
+ {"id": "TEST-NN-002b", "requirement_id": "REQ-NN-002"},
178
+ ]
179
+ ),
180
+ encoding="utf-8",
181
+ )
182
+
183
+ traces = trace_reqs(tmp_path)
184
+ by_req = {t["req"]: t["tests"] for t in traces}
185
+ assert sorted(by_req["REQ-NN-002"]) == ["TEST-NN-002a", "TEST-NN-002b"]
186
+
187
+ def test_sync_parse_tests_md_preserves_letter_suffix(self, tmp_path: Path) -> None:
188
+ """sync.parse_tests_md must not truncate TEST-NN-002a to TEST-NN-002."""
189
+ from specsmith.sync import parse_tests_md
190
+
191
+ text = (
192
+ "## TEST-NN-002a\n"
193
+ "- **Requirement ID**: REQ-NN-002\n\n"
194
+ "## TEST-NN-002b\n"
195
+ "- **Requirement ID**: REQ-NN-002\n"
196
+ )
197
+ records = parse_tests_md(text)
198
+ ids = [r["id"] for r in records]
199
+ assert "TEST-NN-002a" in ids
200
+ assert "TEST-NN-002b" in ids
201
+ assert "TEST-NN-002" not in ids # must not be truncated
@@ -1,89 +0,0 @@
1
- # SPDX-License-Identifier: MIT
2
- # Copyright (c) 2026 BitConcepts, LLC. All rights reserved.
3
- """Tests for specsmith auditor."""
4
-
5
- from __future__ import annotations
6
-
7
- from pathlib import Path
8
-
9
- import pytest
10
-
11
- from specsmith.auditor import run_audit
12
-
13
-
14
- @pytest.fixture
15
- def governed_project(tmp_path: Path) -> Path:
16
- """Create a minimal governed project for audit testing."""
17
- (tmp_path / "AGENTS.md").write_text("# AGENTS\nShort hub.\n", encoding="utf-8")
18
- (tmp_path / "LEDGER.md").write_text("# Ledger\n\n## Session 1\nDone.\n", encoding="utf-8")
19
-
20
- gov = tmp_path / "docs" / "governance"
21
- gov.mkdir(parents=True)
22
- for f in (
23
- "RULES.md",
24
- "SESSION-PROTOCOL.md",
25
- "LIFECYCLE.md",
26
- "ROLES.md",
27
- "CONTEXT-BUDGET.md",
28
- "VERIFICATION.md",
29
- "DRIFT-METRICS.md",
30
- ):
31
- (gov / f).write_text(f"# {f}\nContent.\n", encoding="utf-8")
32
-
33
- docs = tmp_path / "docs"
34
- (docs / "REQUIREMENTS.md").write_text("# Reqs\n", encoding="utf-8")
35
- (docs / "TESTS.md").write_text("# Tests\n", encoding="utf-8")
36
- (docs / "ARCHITECTURE.md").write_text("# Arch\n", encoding="utf-8")
37
- (tmp_path / "CONTRIBUTING.md").write_text("# Contributing\n", encoding="utf-8")
38
- (tmp_path / "LICENSE").write_text("MIT License\n", encoding="utf-8")
39
- # Scaffold config (at root for backward compat — audit now accepts either location)
40
- (tmp_path / "scaffold.yml").write_text(
41
- "name: test-project\ntype: cli-python\nspec_version: 0.10.1\nvcs_platform: github\n",
42
- encoding="utf-8",
43
- )
44
-
45
- return tmp_path
46
-
47
-
48
- class TestAuditHealthy:
49
- def test_passes_on_governed_project(self, governed_project: Path) -> None:
50
- report = run_audit(governed_project)
51
- assert report.healthy
52
- assert report.failed == 0
53
-
54
- def test_reports_pass_count(self, governed_project: Path) -> None:
55
- report = run_audit(governed_project)
56
- assert report.passed > 0
57
-
58
-
59
- class TestAuditDetectsIssues:
60
- def test_missing_agents_md(self, tmp_path: Path) -> None:
61
- (tmp_path / "LEDGER.md").write_text("# Ledger\n", encoding="utf-8")
62
- report = run_audit(tmp_path)
63
- failed_names = [r.name for r in report.results if not r.passed]
64
- assert "file-exists:AGENTS.md" in failed_names
65
-
66
- def test_missing_ledger(self, tmp_path: Path) -> None:
67
- (tmp_path / "AGENTS.md").write_text("# Agents\n", encoding="utf-8")
68
- report = run_audit(tmp_path)
69
- failed_names = [r.name for r in report.results if not r.passed]
70
- assert "file-exists:LEDGER.md" in failed_names
71
-
72
- def test_oversized_ledger(self, governed_project: Path) -> None:
73
- big_ledger = "# Ledger\n" + "\n".join(f"Line {i}" for i in range(600))
74
- (governed_project / "LEDGER.md").write_text(big_ledger, encoding="utf-8")
75
- report = run_audit(governed_project)
76
- size_results = [r for r in report.results if r.name == "ledger-size"]
77
- assert len(size_results) == 1
78
- assert not size_results[0].passed
79
-
80
- def test_req_test_coverage(self, governed_project: Path) -> None:
81
- reqs = "# Reqs\n\nREQ-CLI-001: do things\nREQ-CLI-002: more things\n"
82
- tests = "# Tests\n\nTEST-CLI-001\nCovers: REQ-CLI-001\n"
83
- (governed_project / "docs" / "REQUIREMENTS.md").write_text(reqs, encoding="utf-8")
84
- (governed_project / "docs" / "TESTS.md").write_text(tests, encoding="utf-8")
85
- report = run_audit(governed_project)
86
- coverage = [r for r in report.results if r.name == "req-test-coverage"]
87
- assert len(coverage) == 1
88
- assert not coverage[0].passed
89
- assert "REQ-CLI-002" in coverage[0].message