booley-rtl 0.1.0__py3-none-any.whl

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.

Potentially problematic release.


This version of booley-rtl might be problematic. Click here for more details.

Files changed (288) hide show
  1. booley/__init__.py +13 -0
  2. booley/adapterlib/__init__.py +89 -0
  3. booley/adapterlib/parsers.py +169 -0
  4. booley/adapterlib/proc.py +181 -0
  5. booley/adapterlib/request.py +184 -0
  6. booley/adapterlib/responses.py +265 -0
  7. booley/adapterlib/runner.py +135 -0
  8. booley/bootstrap_mode.py +60 -0
  9. booley/core/__init__.py +9 -0
  10. booley/core/boundary.py +248 -0
  11. booley/core/btool_adapter_contract.py +259 -0
  12. booley/core/btool_adapter_validation.py +411 -0
  13. booley/core/config_paths.py +36 -0
  14. booley/core/models.py +114 -0
  15. booley/core/run_tool.py +168 -0
  16. booley/core_security.py +401 -0
  17. booley/data/__init__.py +0 -0
  18. booley/data/__pycache__/__init__.cpython-314.pyc +0 -0
  19. booley/data/cheatsheet.md +180 -0
  20. booley/data/criteria.toml +164 -0
  21. booley/data/docker/Dockerfile +227 -0
  22. booley/data/docker/Dockerfile.egress-proxy +7 -0
  23. booley/data/docker/Dockerfile.reaper +9 -0
  24. booley/data/docker/Dockerfile.riscv +140 -0
  25. booley/data/docker/build-riscv.sh +29 -0
  26. booley/data/docker/build.sh +27 -0
  27. booley/data/docker/pdk/NangateOpenCellLibrary_typical_ccs.lib +133314 -0
  28. booley/data/docker/pdk/nangate45/Nangate45.rc +13 -0
  29. booley/data/docker/pdk/nangate45/Nangate45_stdcell.lef +11552 -0
  30. booley/data/docker/pdk/nangate45/Nangate45_tech.lef +779 -0
  31. booley/data/docker/pdk/nangate45/README.md +6 -0
  32. booley/data/refs/booley_vcd_dump.sv +19 -0
  33. booley/data/refs/code_review/rtl/functional.md +62 -0
  34. booley/data/refs/code_review/rtl/ifdef.md +57 -0
  35. booley/data/refs/code_review/rtl/optimization.md +71 -0
  36. booley/data/refs/code_review/rtl/protocol-cdc.md +60 -0
  37. booley/data/refs/code_review/rtl/quality.md +22 -0
  38. booley/data/refs/code_review/rtl/security.md +56 -0
  39. booley/data/refs/code_review/testbench/tb-review.md +69 -0
  40. booley/data/refs/rtl-mutation-testing.md +192 -0
  41. booley/data/refs/rtl_style_guide.md +48 -0
  42. booley/data/refs/sim_result_sentinel.sv +23 -0
  43. booley/data/refs/tb_style_guide.md +312 -0
  44. booley/data/skills/booley-setup/AGENTS_TEMPLATE.md +25 -0
  45. booley/data/skills/booley-setup/BOOLEY_TEMPLATE.toml +35 -0
  46. booley/data/skills/booley-setup/CORE_TEMPLATE.yaml +56 -0
  47. booley/data/skills/booley-setup/SKILL.md +125 -0
  48. booley/data/skills/booley-setup/TESTS_TEMPLATE.toml +12 -0
  49. booley/data/skills/booley-setup/agents/openai.yaml +4 -0
  50. booley/data/skills/booley-setup/steps/2-project-config.md +399 -0
  51. booley/data/skills/booley-setup/steps/3-agents-md.md +95 -0
  52. booley/data/skills/booley-setup/steps/4-simulate.md +79 -0
  53. booley/data/skills/booley-setup/steps/5-lint.md +57 -0
  54. booley/data/skills/booley-setup/steps/6-asic-synthesize.md +111 -0
  55. booley/data/skills/booley-setup/steps/7-doctor.md +58 -0
  56. booley/data/skills/booley-setup/steps/_adapter-common.md +246 -0
  57. booley/data/skills/booley-ticket-create/SKILL.md +105 -0
  58. booley/data/skills/booley-ticket-create/TICKET_TEMPLATE.md +173 -0
  59. booley/data/skills/booley-ticket-create/ticket-creation.md +306 -0
  60. booley/data/skills/booley-ticket-triage/SKILL.md +50 -0
  61. booley/data/skills/booley-ticket-triage/steps/01-board-orphans.md +16 -0
  62. booley/data/skills/booley-ticket-triage/steps/02-blocked.md +76 -0
  63. booley/data/skills/booley-ticket-triage/steps/03-review.md +77 -0
  64. booley/data/skills/booley-ticket-triage/steps/04-summary.md +22 -0
  65. booley/data/skills/booley-ticket-triage/tool-reference.md +35 -0
  66. booley/docker/__init__.py +0 -0
  67. booley/docker/egress_proxy.py +388 -0
  68. booley/docker/proxy_entry.py +86 -0
  69. booley/docker/reaper.py +261 -0
  70. booley/filelock.py +86 -0
  71. booley/filesystem_utils.py +74 -0
  72. booley/find_good_commit.py +650 -0
  73. booley/fusesoc_registry.py +1587 -0
  74. booley/fusesoc_trace_overlay.py +450 -0
  75. booley/guidance_links.py +130 -0
  76. booley/harness/__init__.py +9 -0
  77. booley/harness/__main__.py +151 -0
  78. booley/harness/_backend_config.py +492 -0
  79. booley/harness/_claude_backend.py +884 -0
  80. booley/harness/_claude_transcript_md.py +117 -0
  81. booley/harness/_codex_backend.py +699 -0
  82. booley/harness/_codex_transcript_md.py +224 -0
  83. booley/harness/_cost.py +48 -0
  84. booley/harness/_editor_config.py +38 -0
  85. booley/harness/_limits.py +22 -0
  86. booley/harness/_retry.py +81 -0
  87. booley/harness/_ticket_ops.py +484 -0
  88. booley/harness/agent.py +102 -0
  89. booley/harness/agent_backend.py +68 -0
  90. booley/harness/backends/__init__.py +16 -0
  91. booley/harness/backends/sim_base.py +47 -0
  92. booley/harness/blocking.py +159 -0
  93. booley/harness/booley.py +1089 -0
  94. booley/harness/booley_status_display.py +222 -0
  95. booley/harness/colors.py +126 -0
  96. booley/harness/config.py +175 -0
  97. booley/harness/console/__init__.py +9 -0
  98. booley/harness/console/app.py +342 -0
  99. booley/harness/console/console.tcss +42 -0
  100. booley/harness/console/criteria_format.py +101 -0
  101. booley/harness/console/events.py +86 -0
  102. booley/harness/console/links.py +582 -0
  103. booley/harness/console/path_backtick.py +66 -0
  104. booley/harness/console/widgets.py +784 -0
  105. booley/harness/criteria_acceptance.py +545 -0
  106. booley/harness/debug_journal.py +100 -0
  107. booley/harness/devcontainer.py +464 -0
  108. booley/harness/doctor.py +3745 -0
  109. booley/harness/git_utils.py +476 -0
  110. booley/harness/init_cmd.py +1352 -0
  111. booley/harness/init_common.py +70 -0
  112. booley/harness/init_docker_image.py +408 -0
  113. booley/harness/init_git_hooks.py +347 -0
  114. booley/harness/init_skills.py +164 -0
  115. booley/harness/interactive_docker.py +346 -0
  116. booley/harness/logging_utils.py +97 -0
  117. booley/harness/mcp_config.py +121 -0
  118. booley/harness/models.py +213 -0
  119. booley/harness/orchestrator.py +1633 -0
  120. booley/harness/orchestrator_display.py +406 -0
  121. booley/harness/orchestrator_guardrails.py +90 -0
  122. booley/harness/orchestrator_probe.py +142 -0
  123. booley/harness/orchestrator_prompt.py +571 -0
  124. booley/harness/orphan_handler.py +177 -0
  125. booley/harness/preflight.py +438 -0
  126. booley/harness/project_image.py +340 -0
  127. booley/harness/prompt_artifacts.py +121 -0
  128. booley/harness/render_md.py +390 -0
  129. booley/harness/sandbox.py +327 -0
  130. booley/harness/sim_utils.py +169 -0
  131. booley/harness/steps/__init__.py +13 -0
  132. booley/harness/steps/step_00_parse_validate.py +477 -0
  133. booley/harness/steps/step_01_setup.py +608 -0
  134. booley/harness/steps/worktree_lock_gc.py +126 -0
  135. booley/harness/subscription_limit.py +220 -0
  136. booley/harness/terminal.py +319 -0
  137. booley/harness/ticket_cli.py +193 -0
  138. booley/harness/worktree_health.py +105 -0
  139. booley/heartbeat.py +121 -0
  140. booley/host_tools/__init__.py +1 -0
  141. booley/host_tools/__main__.py +214 -0
  142. booley/host_tools/exceptions.py +43 -0
  143. booley/host_tools/fixtures/vivado_impl_pass.log +89 -0
  144. booley/host_tools/fixtures/vivado_routed_real.rpt +41 -0
  145. booley/host_tools/fixtures/vivado_routed_real_2025_2.rpt +49 -0
  146. booley/host_tools/fixtures/vivado_timing.rpt +53 -0
  147. booley/host_tools/job_manager.py +300 -0
  148. booley/host_tools/job_state.py +98 -0
  149. booley/host_tools/mcp_proxy.py +250 -0
  150. booley/host_tools/path_mapper.py +80 -0
  151. booley/host_tools/result_extractor.py +92 -0
  152. booley/host_tools/server.py +331 -0
  153. booley/host_tools/subprocess_runner.py +247 -0
  154. booley/host_tools/template_loader.py +92 -0
  155. booley/host_tools/template_renderer.py +181 -0
  156. booley/host_tools/template_schema.py +90 -0
  157. booley/host_tools/templates/run_host_command.sh +11 -0
  158. booley/host_tools/templates/run_host_command.yaml +52 -0
  159. booley/host_tools/templates/spyglass_lint.tcl +29 -0
  160. booley/host_tools/templates/spyglass_lint.yaml +37 -0
  161. booley/host_tools/templates/vivado_synth.tcl +44 -0
  162. booley/host_tools/templates/vivado_synth.yaml +52 -0
  163. booley/host_tools/templates/xcelium_sim.sh +29 -0
  164. booley/host_tools/templates/xcelium_sim.yaml +42 -0
  165. booley/incontainer_register.py +325 -0
  166. booley/job_slots.py +617 -0
  167. booley/mcp_server.py +2411 -0
  168. booley/nested_mcp_capabilities.py +67 -0
  169. booley/paths.py +69 -0
  170. booley/platform_paths.py +149 -0
  171. booley/project_config.py +406 -0
  172. booley/project_dir.py +128 -0
  173. booley/run_mutmut.py +528 -0
  174. booley/shared_infra.py +418 -0
  175. booley/sim/bwave_fifo.py +260 -0
  176. booley/sim/iverilog_run.py +456 -0
  177. booley/sim/run_guard.py +149 -0
  178. booley/sim/sim_result.py +285 -0
  179. booley/sim/trace_session.py +710 -0
  180. booley/sim/vcs_run.py +269 -0
  181. booley/sim/verilator_run.py +513 -0
  182. booley/sim/xcelium_run.py +261 -0
  183. booley/sim_links.py +142 -0
  184. booley/ticket_board/__init__.py +272 -0
  185. booley/ticket_board/__main__.py +7 -0
  186. booley/ticket_board/analytics.py +413 -0
  187. booley/ticket_board/archive.py +136 -0
  188. booley/ticket_board/cli.py +394 -0
  189. booley/ticket_board/cli_handlers.py +676 -0
  190. booley/ticket_board/constants.py +99 -0
  191. booley/ticket_board/criteria_markdown.py +295 -0
  192. booley/ticket_board/evidence.py +101 -0
  193. booley/ticket_board/execution.py +248 -0
  194. booley/ticket_board/frontmatter.py +533 -0
  195. booley/ticket_board/git_ops.py +151 -0
  196. booley/ticket_board/helpers.py +225 -0
  197. booley/ticket_board/io.py +718 -0
  198. booley/ticket_board/lifecycle.py +177 -0
  199. booley/ticket_board/logs.py +209 -0
  200. booley/ticket_board/notifications.py +139 -0
  201. booley/ticket_board/operations.py +736 -0
  202. booley/ticket_board/paths.py +145 -0
  203. booley/ticket_board/reporting.py +382 -0
  204. booley/ticket_board/run_metrics.py +177 -0
  205. booley/ticket_board/scanner.py +188 -0
  206. booley/ticket_board/validation.py +752 -0
  207. booley/ticket_board/validation_logs.py +163 -0
  208. booley/tool_lock.py +140 -0
  209. booley/tools/__init__.py +104 -0
  210. booley/tools/_baseline_worktree.py +103 -0
  211. booley/tools/_diff_classify.py +203 -0
  212. booley/tools/_job_records.py +243 -0
  213. booley/tools/_run_lock.py +67 -0
  214. booley/tools/_tool_events.py +176 -0
  215. booley/tools/asic_synthesize.py +1595 -0
  216. booley/tools/base.py +801 -0
  217. booley/tools/bwave.py +892 -0
  218. booley/tools/bwave_sessions.py +269 -0
  219. booley/tools/clock_timing.py +167 -0
  220. booley/tools/commit_git_io.py +150 -0
  221. booley/tools/commit_message_format.py +130 -0
  222. booley/tools/commit_msg_hook.py +107 -0
  223. booley/tools/commit_msg_utils.py +159 -0
  224. booley/tools/compare_vcd.py +562 -0
  225. booley/tools/coverage_analyst.py +2663 -0
  226. booley/tools/coverage_verilog_utils.py +291 -0
  227. booley/tools/criteria.py +712 -0
  228. booley/tools/criteria_reference.py +298 -0
  229. booley/tools/deploy_skills.py +274 -0
  230. booley/tools/edam.py +413 -0
  231. booley/tools/elaborate.py +388 -0
  232. booley/tools/fpga_edam.py +356 -0
  233. booley/tools/fpga_impl.py +847 -0
  234. booley/tools/fpga_metrics.py +154 -0
  235. booley/tools/interface_spec.py +185 -0
  236. booley/tools/lint.py +693 -0
  237. booley/tools/mechanical_tool.py +131 -0
  238. booley/tools/mut_harness_inject.py +257 -0
  239. booley/tools/mutation_lock.py +523 -0
  240. booley/tools/mutation_tester.py +2153 -0
  241. booley/tools/orchestration_state.py +920 -0
  242. booley/tools/pre-commit-ruff.sh +26 -0
  243. booley/tools/project_native_backend.py +374 -0
  244. booley/tools/registry.py +359 -0
  245. booley/tools/reviewer.py +1763 -0
  246. booley/tools/rewrite_commits.py +25 -0
  247. booley/tools/schema_extractor.py +107 -0
  248. booley/tools/scope_precommit_hook.py +97 -0
  249. booley/tools/sim_edam.py +393 -0
  250. booley/tools/simulate.py +2163 -0
  251. booley/tools/source_fingerprint.py +147 -0
  252. booley/tools/specialist.py +646 -0
  253. booley/tools/submit_run_report.py +317 -0
  254. booley/tools/tb_coder.py +746 -0
  255. booley/tools/threshold_eval.py +152 -0
  256. booley/tools/tools_reference.py +175 -0
  257. booley/tools/validate_commit_msg.py +198 -0
  258. booley/tools/workspace_isolation.py +696 -0
  259. booley/tools/worktree_create.sh +508 -0
  260. booley/venue.py +71 -0
  261. booley/vivado/xdc/timing.xdc +7 -0
  262. booley/yosys/abc_config.json +5 -0
  263. booley/yosys/abc_scripts/abc_aggressive.script +21 -0
  264. booley/yosys/abc_scripts/abc_balanced.script +15 -0
  265. booley/yosys/abc_scripts/abc_fast.script +10 -0
  266. booley/yosys/openroad_timing.py +351 -0
  267. booley/yosys/run_yosys_syn.py +494 -0
  268. booley/yosys/sdc/abc_simple.sdc +5 -0
  269. booley/yosys/syn_config.py +33 -0
  270. booley/yosys/syn_core.py +1065 -0
  271. booley/yosys/syn_discovery.py +52 -0
  272. booley/yosys/syn_parse.py +60 -0
  273. booley/yosys/syn_subprocess.py +199 -0
  274. booley/yosys/synthesis_watchdog.py +565 -0
  275. booley/zombie_cleanup.py +183 -0
  276. booley_rtl-0.1.0.dist-info/METADATA +251 -0
  277. booley_rtl-0.1.0.dist-info/RECORD +288 -0
  278. booley_rtl-0.1.0.dist-info/WHEEL +5 -0
  279. booley_rtl-0.1.0.dist-info/entry_points.txt +5 -0
  280. booley_rtl-0.1.0.dist-info/licenses/LICENSE +201 -0
  281. booley_rtl-0.1.0.dist-info/top_level.txt +2 -0
  282. bwave/tests/fixtures/real_cvdp/generate.py +599 -0
  283. bwave/tests/gen_test_vcds.py +418 -0
  284. bwave/tests/gen_xcelium_dialect_vcd.py +152 -0
  285. bwave/tests/simulator_ground_truth_test.py +1723 -0
  286. bwave/tests/test_real_cvdp_fixtures.py +140 -0
  287. bwave/tests/test_xcelium_dialect.py +148 -0
  288. bwave/tools/check_schema.py +147 -0
booley/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """Booley — orchestrated RTL development harness."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ # Distribution name on PyPI is "booley-rtl"; the import package is "booley".
7
+ __version__ = version("booley-rtl")
8
+ except PackageNotFoundError:
9
+ try:
10
+ # Pre-rename dev/editable installs registered the old distribution name.
11
+ __version__ = version("booley")
12
+ except PackageNotFoundError:
13
+ __version__ = "0.0.0-dev"
@@ -0,0 +1,89 @@
1
+ """Public helper library for Project-native B-Tool adapters.
2
+
3
+ This package is the ONE sanctioned Booley import for project adapters
4
+ (``.booley_project/adapters/*.py``). Everything else under ``booley.*`` stays
5
+ private to adapters: the JSON request/response files remain the only contract
6
+ (docs/BTOOL-ADAPTER-CONTRACTS.md), and this library only makes satisfying that
7
+ contract mechanical. Adapters work without it; with it, the recurring blocks —
8
+ request parsing, response envelopes, identity echoes, stale-artifact guards,
9
+ budgeted subprocess runs, finding/metric normalization, and pre-write
10
+ self-validation against Booley's own validator — come from tested code instead
11
+ of being re-authored per project.
12
+
13
+ Import surface is guaranteed available wherever adapters run: Booley invokes
14
+ adapters with its own interpreter (``sys.executable``), both in the sandbox
15
+ and on the host, so the installed ``booley`` package is importable by
16
+ construction and can never version-skew against the invoking side.
17
+
18
+ Typical adapter::
19
+
20
+ from booley import adapterlib as al
21
+
22
+ def build(req: al.AdapterRequest) -> dict:
23
+ command = ["make", "sim", f"CONFIG={req.target_name}", f"TEST={req.test}"]
24
+ if req.dry_run:
25
+ return al.dry_run_response(req, " ".join(command))
26
+ report = al.fresh_artifact(req.work_dir / "build" / "run_case.report")
27
+ run = al.run_command(command, req)
28
+ verdict = al.derive_sim_verdict(returncode=run.returncode,
29
+ log_text=run.output, timed_out=run.timed_out)
30
+ return al.response(req, verdict, command=run.command,
31
+ log_path=run.log_path, elapsed_s=run.elapsed_s)
32
+
33
+ if __name__ == "__main__":
34
+ raise SystemExit(al.main(build))
35
+ """
36
+
37
+ from .parsers import (
38
+ SIM_RESULT_FAILED,
39
+ SIM_RESULT_PASSED,
40
+ VERILATOR_ERROR_RE,
41
+ VERILATOR_WARNING_RE,
42
+ area_to_kge,
43
+ count_sva_errors,
44
+ derive_sim_verdict,
45
+ extract_error_gist,
46
+ first_error_line,
47
+ parse_area_from_stat,
48
+ parse_sim_verdict,
49
+ parse_verilator_findings,
50
+ )
51
+ from .proc import CommandResult, fresh_artifact, run_command
52
+ from .request import AdapterRequest
53
+ from .responses import (
54
+ AdapterError,
55
+ clock_timing,
56
+ dry_run_response,
57
+ finding,
58
+ normalize_severity,
59
+ response,
60
+ synthesis_metrics,
61
+ )
62
+ from .runner import main
63
+
64
+ __all__ = [
65
+ "SIM_RESULT_FAILED",
66
+ "SIM_RESULT_PASSED",
67
+ "VERILATOR_ERROR_RE",
68
+ "VERILATOR_WARNING_RE",
69
+ "AdapterError",
70
+ "AdapterRequest",
71
+ "CommandResult",
72
+ "area_to_kge",
73
+ "clock_timing",
74
+ "count_sva_errors",
75
+ "derive_sim_verdict",
76
+ "dry_run_response",
77
+ "extract_error_gist",
78
+ "finding",
79
+ "first_error_line",
80
+ "fresh_artifact",
81
+ "main",
82
+ "normalize_severity",
83
+ "parse_area_from_stat",
84
+ "parse_sim_verdict",
85
+ "parse_verilator_findings",
86
+ "response",
87
+ "run_command",
88
+ "synthesis_metrics",
89
+ ]
@@ -0,0 +1,169 @@
1
+ """Log/report parsers shared by the built-in flows and Project-native adapters.
2
+
3
+ WHAT: The pure text-extraction blocks of the built-in tools, importable by
4
+ adapters: sim sentinel verdicts (re-exported from ``booley.sim.sim_result``),
5
+ Verilator warning/error parsing (single source of truth — ``booley.tools.lint``
6
+ imports these regexes), compiler-error gists (shared with
7
+ ``booley.tools.elaborate``), and Yosys area extraction (re-exported from
8
+ ``booley.yosys.syn_parse``).
9
+
10
+ WHY: An adapter wrapping a Verilator/Icarus/Yosys-based project flow should
11
+ not re-derive parsing the built-in flows already hardened (full-scan sentinel
12
+ rule, QA-7 %Error-means-not-clean, location-less-finding dropping).
13
+
14
+ CONSUMERS: project adapters via ``booley.adapterlib``; ``booley.tools.lint``
15
+ (warning/error regexes) and ``booley.tools.elaborate`` (error gist).
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import re
21
+ from typing import Any
22
+
23
+ # Re-exports: sentinel-based sim verdict + SVA counting (leaf module).
24
+ from booley.sim.sim_result import ( # noqa: F401 — public adapterlib surface
25
+ SIM_RESULT_FAILED,
26
+ SIM_RESULT_PASSED,
27
+ count_sva_errors,
28
+ parse_sim_verdict,
29
+ )
30
+
31
+ # Re-exports: Yosys stat-file area extraction + kGE conversion (leaf module).
32
+ from booley.yosys.syn_parse import ( # noqa: F401 — public adapterlib surface
33
+ area_to_kge,
34
+ parse_area_from_stat,
35
+ )
36
+
37
+ from .responses import finding
38
+
39
+ # Verilator warning lines (also the built-in lint parser's regex):
40
+ # %Warning-UNUSEDSIGNAL: module_a.sv:42:5: Signal is not used: 'foo'
41
+ VERILATOR_WARNING_RE = re.compile(
42
+ r"%Warning-(?P<rule>[A-Z0-9_]+):\s+"
43
+ r"(?P<file>[^:]+):(?P<line>\d+):(?P<col>\d+):\s+"
44
+ r"(?P<message>.+)"
45
+ )
46
+
47
+ # Verilator emits ``%Error`` (and the summary ``%Error: Exiting due to N
48
+ # error(s)``) for hard failures — undeclared signals, parse errors — and exits
49
+ # non-zero. A warnings-only parser yields zero findings on such a run and
50
+ # would score it as a clean PASS (QA-7); scan for errors separately.
51
+ VERILATOR_ERROR_RE = re.compile(r"^%Error.*", re.MULTILINE)
52
+
53
+ # Located %Error lines — the ones that may become findings. The rule suffix is
54
+ # optional (``%Error-BLKANDNBLK: ...`` vs plain ``%Error: file:line:col: ...``);
55
+ # summary/epilogue errors carry no location and must NOT become findings.
56
+ _VERILATOR_LOCATED_ERROR_RE = re.compile(
57
+ r"%Error(?:-(?P<rule>[A-Z0-9_]+))?:\s+"
58
+ r"(?P<file>[^\s:][^:]*):(?P<line>\d+):(?:(?P<col>\d+):)?\s+"
59
+ r"(?P<message>.+)"
60
+ )
61
+
62
+
63
+ def first_error_line(output: str) -> str | None:
64
+ """Return the first Verilator ``%Error`` line in *output*, if any."""
65
+ match = VERILATOR_ERROR_RE.search(output)
66
+ return match.group(0).strip() if match else None
67
+
68
+
69
+ def parse_verilator_findings(output: str) -> list[dict[str, Any]]:
70
+ """Parse Verilator output into contract-shaped lint findings.
71
+
72
+ ``%Warning-RULE: file:line:col: msg`` → ``warning`` findings;
73
+ ``%Error[-RULE]: file:line[:col]: msg`` → ``error`` findings. Location-less
74
+ error/summary lines (``%Error: Exiting due to N error(s)``) are dropped —
75
+ Booley rejects findings without a ``file``. Presence of any ``%Error`` line
76
+ should still flip the verdict to ``fail``/``tool_error``; check
77
+ :func:`first_error_line` for that, independently of the findings list.
78
+ """
79
+ findings: list[dict[str, Any]] = []
80
+ for match in VERILATOR_WARNING_RE.finditer(output):
81
+ findings.append(finding(
82
+ rule=match.group("rule"),
83
+ severity="warning",
84
+ file=match.group("file"),
85
+ line=int(match.group("line")),
86
+ col=int(match.group("col")),
87
+ message=match.group("message"),
88
+ ))
89
+ for match in _VERILATOR_LOCATED_ERROR_RE.finditer(output):
90
+ col = match.group("col")
91
+ findings.append(finding(
92
+ rule=match.group("rule") or "ERROR",
93
+ severity="error",
94
+ file=match.group("file"),
95
+ line=int(match.group("line")),
96
+ col=int(col) if col else None,
97
+ message=match.group("message"),
98
+ ))
99
+ return findings
100
+
101
+
102
+ def extract_error_gist(error_output: str) -> str:
103
+ """Extract a one-line error gist from compiler output.
104
+
105
+ Looks for common error patterns (Verilator, Icarus, sv2v) and returns the
106
+ first meaningful error line, truncated for display — a ready-made
107
+ ``first_error`` for simulate responses. (Also the built-in elaborate
108
+ tool's extractor.)
109
+ """
110
+ if not error_output:
111
+ return ""
112
+ error_re = re.compile(
113
+ r"(?:%Error[^:]*:\s*\S+:\d+:\s*(.+)" # Verilator
114
+ r"|(?:^|\n)\s*(?:error|Error):\s*(.+)" # generic
115
+ r"|(?:^|\n)\s*(\S+:\d+:\s*(?:error|syntax error).+))", # Icarus/sv2v
116
+ )
117
+ m = error_re.search(error_output)
118
+ if m:
119
+ msg = next(g for g in m.groups() if g)
120
+ return msg.strip()[:80]
121
+ # Fallback: last non-empty line.
122
+ for line in reversed(error_output.splitlines()):
123
+ stripped = line.strip()
124
+ if stripped and not stripped.startswith(("-", "=")):
125
+ return stripped[:80]
126
+ return ""
127
+
128
+
129
+ def derive_sim_verdict(
130
+ *,
131
+ returncode: int | None,
132
+ log_text: str,
133
+ timed_out: bool = False,
134
+ pass_sentinels: list[str] | None = None,
135
+ fail_sentinels: list[str] | None = None,
136
+ ) -> str:
137
+ """Derive a simulate verdict — exit code first, sentinels second.
138
+
139
+ Encodes the contract's "Verdict integrity" ordering for the common
140
+ self-checking-testbench case:
141
+
142
+ - ``timed_out`` → ``timeout``;
143
+ - any FAIL sentinel in the log → ``fail`` (a TB that prints FAIL failed,
144
+ whatever the exit code);
145
+ - non-zero exit with no fail sentinel → ``elab_error`` (the build/elab
146
+ died before the test could judge itself — never a functional ``fail``,
147
+ never a ``pass`` inherited from a stale artifact);
148
+ - clean exit + PASS sentinel → ``pass``;
149
+ - clean exit, no sentinel → ``inconclusive`` (a silent run is not
150
+ evidence).
151
+
152
+ Sentinels default to the built-in ``[SIM_RESULT] PASSED/FAILED`` markers;
153
+ pass the project's own wording (booley.toml ``[tools.simulate]``
154
+ pass_sentinels/fail_sentinels) to keep the testbench untouched. Flows with
155
+ richer semantics (SVA counting, per-test reports) should derive their own
156
+ verdict from the lower-level pieces instead.
157
+ """
158
+ if timed_out:
159
+ return "timeout"
160
+ sentinel = parse_sim_verdict(
161
+ log_text, pass_sentinels=pass_sentinels, fail_sentinels=fail_sentinels
162
+ )
163
+ if sentinel is False:
164
+ return "fail"
165
+ if returncode != 0:
166
+ return "elab_error"
167
+ if sentinel is True:
168
+ return "pass"
169
+ return "inconclusive"
@@ -0,0 +1,181 @@
1
+ """Command execution for Project-native adapters: log capture, timeout, freshness.
2
+
3
+ WHAT: ``run_command`` runs one project flow command with its combined output
4
+ streamed straight to a log file (never buffered whole in adapter memory), a
5
+ timeout derived from the request budget, and process-tree kill on expiry.
6
+ ``fresh_artifact`` deletes result files a build/run rewrites, so a verdict can
7
+ never be inherited from a previous run's artifact.
8
+
9
+ WHY: These two blocks are where hand-rolled adapters produced false PASSes
10
+ (contract, "Verdict integrity: never trust a stale artifact") and unstructured
11
+ timeouts (Booley's hard kill leaves no evidence; a budget-aware command timeout
12
+ returns a structured ``timeout`` verdict with a log instead).
13
+
14
+ CONSUMERS: project adapters via ``booley.adapterlib``.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import shlex
20
+ import subprocess
21
+ import sys
22
+ import time
23
+ from dataclasses import dataclass
24
+ from pathlib import Path
25
+ from typing import Any
26
+
27
+ from booley.platform_paths import kill_process_tree, popen_new_group_kwargs
28
+
29
+ from .request import AdapterRequest
30
+
31
+ # How much of the log tail to retain in CommandResult.output (and echo on
32
+ # request) — enough for sentinel scans and error extraction without holding a
33
+ # multi-GB simulation log in memory.
34
+ _KEEP_OUTPUT_BYTES = 2_000_000
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class CommandResult:
39
+ """Outcome of one ``run_command`` invocation."""
40
+
41
+ command: str
42
+ returncode: int | None
43
+ timed_out: bool
44
+ elapsed_s: float
45
+ log_path: Path
46
+ # Tail of the combined stdout+stderr (capped; full text is in log_path).
47
+ output: str
48
+
49
+ @property
50
+ def ok(self) -> bool:
51
+ """True when the command exited 0 within its budget."""
52
+ return self.returncode == 0 and not self.timed_out
53
+
54
+ def tail(self, lines: int = 50) -> str:
55
+ """Last *lines* lines of the captured output — for ``error_tail`` fields."""
56
+ return "\n".join(self.output.splitlines()[-lines:])
57
+
58
+
59
+ def run_command(
60
+ command: list[str] | str,
61
+ req: AdapterRequest | None = None,
62
+ *,
63
+ log_path: str | Path | None = None,
64
+ cwd: str | Path | None = None,
65
+ env: dict[str, str] | None = None,
66
+ timeout_s: float | None = None,
67
+ echo_tail: bool = True,
68
+ ) -> CommandResult:
69
+ """Run one project flow command with contract-friendly defaults.
70
+
71
+ - *command*: an argv list (exec'd directly), or a string (run through the
72
+ shell — needed for Makefiles that assume bash, cf. contract "Common
73
+ Pitfalls").
74
+ - *log_path*: combined stdout+stderr destination. Defaults to the request's
75
+ ``expected_log_path`` (where Booley looks for evidence), else
76
+ ``<artifact_dir>/<tool>.log``.
77
+ - *cwd*: defaults to the request's ``work_dir``.
78
+ - *env*: overlaid on the process environment after the request's ``env``.
79
+ - *timeout_s*: defaults to the remaining request budget minus a margin
80
+ (``req.remaining_s()``), so the adapter still has time to parse results
81
+ and write a structured ``timeout`` response before Booley's hard kill.
82
+ On expiry the whole process tree is killed.
83
+ - *echo_tail*: reprint the log tail to stdout afterwards, so the
84
+ ``adapter.stdout.log`` Booley archives shows what happened.
85
+ """
86
+ if log_path is None:
87
+ if req is None:
88
+ raise ValueError("run_command needs log_path when no request is given")
89
+ log_path = req.expected_log_path or (req.artifact_dir / f"{req.tool}.log")
90
+ log = Path(log_path)
91
+ log.parent.mkdir(parents=True, exist_ok=True)
92
+
93
+ shell = isinstance(command, str)
94
+ command_str = command if shell else shlex.join(command)
95
+ if timeout_s is None and req is not None:
96
+ timeout_s = req.remaining_s()
97
+
98
+ run_env = _merged_env(req, env)
99
+ start = time.monotonic()
100
+ # Stream output straight to the log file: no pipe to deadlock on, no
101
+ # multi-GB simulation log held in adapter memory.
102
+ with log.open("w", encoding="utf-8") as sink:
103
+ proc = subprocess.Popen(
104
+ command,
105
+ shell=shell,
106
+ cwd=str(cwd) if cwd is not None else (str(req.work_dir) if req else None),
107
+ stdout=sink,
108
+ stderr=subprocess.STDOUT,
109
+ env=run_env,
110
+ **popen_new_group_kwargs(),
111
+ )
112
+ timed_out = _wait(proc, timeout_s)
113
+ elapsed = time.monotonic() - start
114
+
115
+ output = _read_tail(log)
116
+ if timed_out:
117
+ output += f"\n[adapterlib] command timed out after {timeout_s:.0f}s: {command_str}\n"
118
+ if echo_tail:
119
+ print(f"[adapterlib] $ {command_str}\n{output}", flush=True)
120
+ return CommandResult(
121
+ command=command_str,
122
+ returncode=proc.returncode,
123
+ timed_out=timed_out,
124
+ elapsed_s=elapsed,
125
+ log_path=log,
126
+ output=output,
127
+ )
128
+
129
+
130
+ def fresh_artifact(*paths: str | Path) -> Path | tuple[Path, ...]:
131
+ """Delete result files the upcoming build/run will (re)write.
132
+
133
+ Call BEFORE launching the flow for every artifact the verdict is read
134
+ from (a ``run_case.report``, a JUnit XML, a log sentinel file): anything
135
+ present afterwards then belongs to *this* invocation only, so a build that
136
+ fails to compile can never inherit the previous run's PASS (contract,
137
+ "Verdict integrity"). Returns the Path(s) for the post-run read-back.
138
+ """
139
+ cleared = tuple(Path(p) for p in paths)
140
+ for path in cleared:
141
+ path.unlink(missing_ok=True)
142
+ return cleared[0] if len(cleared) == 1 else cleared
143
+
144
+
145
+ def _merged_env(req: AdapterRequest | None, env: dict[str, str] | None) -> dict[str, str] | None:
146
+ """Process env + request env + explicit overrides (None = inherit as-is)."""
147
+ if req is None and env is None:
148
+ return None
149
+ import os
150
+
151
+ merged = os.environ.copy()
152
+ if req is not None:
153
+ merged.update(req.env)
154
+ if env is not None:
155
+ merged.update({str(k): str(v) for k, v in env.items()})
156
+ return merged
157
+
158
+
159
+ def _wait(proc: subprocess.Popen[Any], timeout_s: float | None) -> bool:
160
+ try:
161
+ proc.wait(timeout=timeout_s)
162
+ except subprocess.TimeoutExpired:
163
+ kill_process_tree(proc)
164
+ proc.wait()
165
+ return True
166
+ except KeyboardInterrupt:
167
+ kill_process_tree(proc)
168
+ raise
169
+ return False
170
+
171
+
172
+ def _read_tail(log: Path, keep_bytes: int = _KEEP_OUTPUT_BYTES) -> str:
173
+ try:
174
+ size = log.stat().st_size
175
+ with log.open("rb") as handle:
176
+ if size > keep_bytes:
177
+ handle.seek(size - keep_bytes)
178
+ return handle.read().decode("utf-8", errors="replace")
179
+ except OSError as exc: # log unreadable — surface why instead of crashing
180
+ print(f"[adapterlib] could not read log tail {log}: {exc}", file=sys.stderr)
181
+ return ""
@@ -0,0 +1,184 @@
1
+ """Typed read-only view of a Project-native adapter request.
2
+
3
+ WHAT: Wraps the ``request.json`` a B-Tool writes for a Project-native adapter
4
+ (see docs/BTOOL-ADAPTER-CONTRACTS.md "Common Request") behind named accessors,
5
+ so adapter code reads ``req.target_name`` instead of chasing raw dict keys —
6
+ and gets the renamed-key mistakes (``config`` vs ``target``) for free.
7
+
8
+ WHY: Every adapter used to hand-parse the request dict; typos in nested keys
9
+ surfaced only as downstream ``contract_error``. A single typed view makes the
10
+ request shape discoverable and keeps adapters resilient to additive schema
11
+ growth (unknown keys are simply not exposed).
12
+
13
+ CONSUMERS: project adapters via ``booley.adapterlib``; ``runner.main`` builds
14
+ one per invocation.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import time
21
+ from dataclasses import dataclass, field
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ # The tool-specific request section is not always named after the tool:
26
+ # asic_synthesize nests its extras under "synthesis" (contract, "Common Request").
27
+ _TOOL_SECTION_KEY = {
28
+ "simulate": "simulate",
29
+ "lint": "lint",
30
+ "asic_synthesize": "synthesis",
31
+ }
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class AdapterRequest:
36
+ """One adapter invocation's request, plus where the response must go."""
37
+
38
+ raw: dict[str, Any]
39
+ request_path: Path
40
+ response_path: Path
41
+ # Monotonic clock at load time — anchors ``remaining_s`` so command
42
+ # timeouts leave room to write the response before Booley's hard kill.
43
+ loaded_at: float = field(default_factory=time.monotonic)
44
+
45
+ @classmethod
46
+ def load(cls, request_path: str | Path, response_path: str | Path) -> AdapterRequest:
47
+ """Read ``request.json`` and wrap it (raises on missing/invalid JSON)."""
48
+ path = Path(request_path)
49
+ raw = json.loads(path.read_text(encoding="utf-8"))
50
+ if not isinstance(raw, dict):
51
+ raise ValueError(f"adapter request must be a JSON object: {path}")
52
+ return cls(raw=raw, request_path=path, response_path=Path(response_path))
53
+
54
+ # -- common envelope ----------------------------------------------------
55
+
56
+ @property
57
+ def schema_version(self) -> int:
58
+ return int(self.raw.get("schema_version", 1))
59
+
60
+ @property
61
+ def tool(self) -> str:
62
+ return str(self.raw.get("tool", ""))
63
+
64
+ @property
65
+ def work_dir(self) -> Path:
66
+ return Path(str(self.raw.get("work_dir", ".")))
67
+
68
+ @property
69
+ def artifact_dir(self) -> Path:
70
+ return Path(str(self.raw.get("artifact_dir", ".")))
71
+
72
+ @property
73
+ def timeout_s(self) -> float:
74
+ try:
75
+ return float(self.raw.get("timeout_s", 600))
76
+ except (TypeError, ValueError):
77
+ return 600.0
78
+
79
+ @property
80
+ def dry_run(self) -> bool:
81
+ return bool(self.raw.get("dry_run", False))
82
+
83
+ @property
84
+ def env(self) -> dict[str, str]:
85
+ """Explicit environment additions supplied by Booley."""
86
+ raw = self.raw.get("env")
87
+ return {str(k): str(v) for k, v in raw.items()} if isinstance(raw, dict) else {}
88
+
89
+ # -- resolved Target ----------------------------------------------------
90
+
91
+ @property
92
+ def target(self) -> dict[str, Any]:
93
+ """The Booley-resolved Target object (``name``, ``defines``, ...)."""
94
+ raw = self.raw.get("target")
95
+ return dict(raw) if isinstance(raw, dict) else {}
96
+
97
+ @property
98
+ def target_name(self) -> str:
99
+ """The requested config name — echo back verbatim as ``resolved_target``."""
100
+ return str(self.target.get("name", ""))
101
+
102
+ @property
103
+ def top_module(self) -> str:
104
+ return str(self.raw.get("top_module", self.target.get("top_module", "")))
105
+
106
+ @property
107
+ def tb_top(self) -> str:
108
+ return str(self.raw.get("tb_top", self.target.get("tb_top", "")))
109
+
110
+ @property
111
+ def defines(self) -> list[str]:
112
+ raw = self.raw.get("defines", self.target.get("defines", []))
113
+ return [str(d) for d in raw] if isinstance(raw, list) else []
114
+
115
+ @property
116
+ def parameters(self) -> dict[str, Any]:
117
+ raw = self.raw.get("parameters", self.target.get("parameters", {}))
118
+ return dict(raw) if isinstance(raw, dict) else {}
119
+
120
+ @property
121
+ def tests(self) -> list[str]:
122
+ raw = self.raw.get("tests", self.target.get("tests", []))
123
+ return [str(t) for t in raw] if isinstance(raw, list) else []
124
+
125
+ # -- tool-specific section ----------------------------------------------
126
+
127
+ @property
128
+ def tool_section(self) -> dict[str, Any]:
129
+ """The tool-specific request block (``simulate``/``lint``/``synthesis``)."""
130
+ key = _TOOL_SECTION_KEY.get(self.tool, self.tool)
131
+ raw = self.raw.get(key)
132
+ return dict(raw) if isinstance(raw, dict) else {}
133
+
134
+ @property
135
+ def test(self) -> str | None:
136
+ """simulate: the resolved test name, or ``None`` when no test was selected.
137
+
138
+ When ``None``, the response must omit ``resolved_test`` (or return it as
139
+ null) — the contract forbids inventing one. ``responses.response`` obeys
140
+ this automatically.
141
+ """
142
+ value = self.tool_section.get("test")
143
+ return None if value is None else str(value)
144
+
145
+ @property
146
+ def trace(self) -> bool:
147
+ """simulate: whether a waveform trace was requested."""
148
+ return bool(self.tool_section.get("trace", False))
149
+
150
+ @property
151
+ def scope(self) -> str:
152
+ """lint: comma-separated file filter from ``--scope`` (empty = all files)."""
153
+ return str(self.tool_section.get("scope", ""))
154
+
155
+ @property
156
+ def expected_log_path(self) -> Path | None:
157
+ """Where Booley expects the tool log; a good default for ``run_command``."""
158
+ value = self.tool_section.get("expected_log_path")
159
+ return Path(str(value)) if value else None
160
+
161
+ @property
162
+ def expected_trace_path(self) -> Path | None:
163
+ """simulate: where Booley expects the trace when ``trace`` is true."""
164
+ value = self.tool_section.get("expected_trace_path")
165
+ return Path(str(value)) if value else None
166
+
167
+ @property
168
+ def expected_report_dir(self) -> Path | None:
169
+ """asic_synthesize: where Booley expects report files."""
170
+ value = self.tool_section.get("expected_report_dir")
171
+ return Path(str(value)) if value else None
172
+
173
+ # -- budget ---------------------------------------------------------------
174
+
175
+ def remaining_s(self, *, margin_s: float = 10.0, floor_s: float = 5.0) -> float:
176
+ """Seconds left of the request budget, minus *margin_s* to finish up.
177
+
178
+ Booley hard-kills the adapter process tree at ``timeout_s``; commands
179
+ launched with this budget leave the adapter time to parse results and
180
+ write the response, so a slow tool becomes a structured ``timeout``
181
+ verdict instead of a synthetic one with no evidence.
182
+ """
183
+ elapsed = time.monotonic() - self.loaded_at
184
+ return max(floor_s, self.timeout_s - elapsed - margin_s)