beeverforge 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.
Files changed (433) hide show
  1. beeverforge/__init__.py +7 -0
  2. beeverforge/actions/__init__.py +506 -0
  3. beeverforge/actions/base.py +490 -0
  4. beeverforge/actions/catalog.py +9549 -0
  5. beeverforge/actions/floor_intents.py +432 -0
  6. beeverforge/adapters/__init__.py +0 -0
  7. beeverforge/adapters/agent/__init__.py +71 -0
  8. beeverforge/adapters/agent/base.py +1101 -0
  9. beeverforge/adapters/agent/claude_code.py +1326 -0
  10. beeverforge/adapters/agent/codex.py +449 -0
  11. beeverforge/adapters/agent/kimi.py +442 -0
  12. beeverforge/adapters/agent/opencode.py +520 -0
  13. beeverforge/adapters/agent/registry.py +550 -0
  14. beeverforge/adapters/agent/roles.py +326 -0
  15. beeverforge/adapters/agent/routes.py +255 -0
  16. beeverforge/adapters/agent/session_store.py +221 -0
  17. beeverforge/adapters/agent/stream.py +367 -0
  18. beeverforge/adapters/agent/techlead.py +108 -0
  19. beeverforge/adapters/agent/token_pool.py +67 -0
  20. beeverforge/adapters/azure_devops.py +466 -0
  21. beeverforge/adapters/board/__init__.py +10 -0
  22. beeverforge/adapters/board/azure_devops.py +831 -0
  23. beeverforge/adapters/board/base.py +545 -0
  24. beeverforge/adapters/board/columns.py +261 -0
  25. beeverforge/adapters/board/factory.py +388 -0
  26. beeverforge/adapters/board/jira.py +487 -0
  27. beeverforge/adapters/board/local.py +360 -0
  28. beeverforge/adapters/board_db.py +423 -0
  29. beeverforge/adapters/board_setup/__init__.py +8 -0
  30. beeverforge/adapters/board_setup/base.py +60 -0
  31. beeverforge/adapters/board_setup/local.py +147 -0
  32. beeverforge/adapters/board_setup/registry.py +41 -0
  33. beeverforge/adapters/channel/__init__.py +5 -0
  34. beeverforge/adapters/channel/base.py +135 -0
  35. beeverforge/adapters/channel/panel.py +107 -0
  36. beeverforge/adapters/channel/registry.py +115 -0
  37. beeverforge/adapters/credential/__init__.py +8 -0
  38. beeverforge/adapters/credential/registry.py +342 -0
  39. beeverforge/adapters/credential/short_lived.py +69 -0
  40. beeverforge/adapters/credential_store/__init__.py +10 -0
  41. beeverforge/adapters/credential_store/base.py +39 -0
  42. beeverforge/adapters/credential_store/env.py +21 -0
  43. beeverforge/adapters/credential_store/file.py +329 -0
  44. beeverforge/adapters/credential_store/registry.py +413 -0
  45. beeverforge/adapters/embed/__init__.py +8 -0
  46. beeverforge/adapters/embed/base.py +39 -0
  47. beeverforge/adapters/embed/local.py +216 -0
  48. beeverforge/adapters/embed/registry.py +136 -0
  49. beeverforge/adapters/environment/__init__.py +30 -0
  50. beeverforge/adapters/environment/azure_pipelines.py +412 -0
  51. beeverforge/adapters/environment/base.py +35 -0
  52. beeverforge/adapters/environment/github_actions.py +108 -0
  53. beeverforge/adapters/environment/none.py +75 -0
  54. beeverforge/adapters/environment/registry.py +210 -0
  55. beeverforge/adapters/extract/__init__.py +6 -0
  56. beeverforge/adapters/extract/base.py +86 -0
  57. beeverforge/adapters/extract/mail.py +83 -0
  58. beeverforge/adapters/extract/markup.py +278 -0
  59. beeverforge/adapters/extract/model.py +94 -0
  60. beeverforge/adapters/extract/office.py +269 -0
  61. beeverforge/adapters/extract/pdf.py +332 -0
  62. beeverforge/adapters/extract/registry.py +218 -0
  63. beeverforge/adapters/extract/text.py +251 -0
  64. beeverforge/adapters/extract/vision.py +82 -0
  65. beeverforge/adapters/forge/__init__.py +30 -0
  66. beeverforge/adapters/forge/azure_devops.py +1961 -0
  67. beeverforge/adapters/forge/base.py +911 -0
  68. beeverforge/adapters/forge/github.py +1428 -0
  69. beeverforge/adapters/forge/local.py +894 -0
  70. beeverforge/adapters/forge/registry.py +252 -0
  71. beeverforge/adapters/github_app.py +173 -0
  72. beeverforge/adapters/github_cli.py +81 -0
  73. beeverforge/adapters/gitworktrees.py +183 -0
  74. beeverforge/adapters/notify/__init__.py +12 -0
  75. beeverforge/adapters/notify/base.py +45 -0
  76. beeverforge/adapters/notify/panel.py +43 -0
  77. beeverforge/adapters/notify/registry.py +301 -0
  78. beeverforge/adapters/preview/__init__.py +23 -0
  79. beeverforge/adapters/preview/base.py +221 -0
  80. beeverforge/adapters/preview/compose.py +1464 -0
  81. beeverforge/adapters/preview/none.py +60 -0
  82. beeverforge/adapters/preview/registry.py +125 -0
  83. beeverforge/adapters/reviewer/__init__.py +30 -0
  84. beeverforge/adapters/reviewer/base.py +75 -0
  85. beeverforge/adapters/reviewer/claude_code.py +195 -0
  86. beeverforge/adapters/reviewer/harness.py +169 -0
  87. beeverforge/adapters/sandbox/__init__.py +33 -0
  88. beeverforge/adapters/sandbox/base.py +470 -0
  89. beeverforge/adapters/sandbox/container.py +903 -0
  90. beeverforge/adapters/sandbox/pull.py +144 -0
  91. beeverforge/adapters/sandbox/registry.py +596 -0
  92. beeverforge/adapters/sandbox/timeouts.py +76 -0
  93. beeverforge/adapters/sandbox/worktree.py +1223 -0
  94. beeverforge/adapters/tracker/__init__.py +30 -0
  95. beeverforge/adapters/tracker/azure_devops.py +1058 -0
  96. beeverforge/adapters/tracker/base.py +876 -0
  97. beeverforge/adapters/tracker/github.py +732 -0
  98. beeverforge/adapters/tracker/github_board_setup.py +241 -0
  99. beeverforge/adapters/tracker/github_project.py +811 -0
  100. beeverforge/adapters/tracker/jira.py +1124 -0
  101. beeverforge/adapters/tracker/local.py +578 -0
  102. beeverforge/adapters/tracker/parse.py +573 -0
  103. beeverforge/adapters/tracker/registry.py +255 -0
  104. beeverforge/after_merge.py +123 -0
  105. beeverforge/agent_answer.py +199 -0
  106. beeverforge/api/__init__.py +4 -0
  107. beeverforge/api/app.py +4603 -0
  108. beeverforge/api/events.py +489 -0
  109. beeverforge/api/metrics_view.py +390 -0
  110. beeverforge/api/openapi.py +55 -0
  111. beeverforge/api/panel.html +6049 -0
  112. beeverforge/api/product_chat.py +709 -0
  113. beeverforge/api/project_route.py +89 -0
  114. beeverforge/api/read_models.py +774 -0
  115. beeverforge/approvals.py +341 -0
  116. beeverforge/box_prove.py +2150 -0
  117. beeverforge/certify/__init__.py +34 -0
  118. beeverforge/certify/controls.py +732 -0
  119. beeverforge/certify/pack.py +779 -0
  120. beeverforge/certify/pack.schema.json +226 -0
  121. beeverforge/certify/redact.py +336 -0
  122. beeverforge/certify/schema.py +144 -0
  123. beeverforge/certify/thresholds.yaml +61 -0
  124. beeverforge/certify/verify.py +469 -0
  125. beeverforge/cli.py +4642 -0
  126. beeverforge/cli_refusals.py +255 -0
  127. beeverforge/conformance/__init__.py +20 -0
  128. beeverforge/conformance/adapters.py +776 -0
  129. beeverforge/contracts/__init__.py +65 -0
  130. beeverforge/contracts/aliases.py +106 -0
  131. beeverforge/contracts/bot.py +42 -0
  132. beeverforge/contracts/checks.py +340 -0
  133. beeverforge/contracts/commands.py +183 -0
  134. beeverforge/contracts/decision.py +286 -0
  135. beeverforge/contracts/document.py +160 -0
  136. beeverforge/contracts/item_space.py +86 -0
  137. beeverforge/contracts/manifest.py +921 -0
  138. beeverforge/contracts/product.py +265 -0
  139. beeverforge/contracts/profile.py +214 -0
  140. beeverforge/contracts/project.py +597 -0
  141. beeverforge/contracts/refs.py +217 -0
  142. beeverforge/contracts/review.py +66 -0
  143. beeverforge/contracts/run.py +456 -0
  144. beeverforge/contracts/state.py +101 -0
  145. beeverforge/contracts/ticket.py +146 -0
  146. beeverforge/credentials.py +853 -0
  147. beeverforge/doctor.py +3504 -0
  148. beeverforge/doors.py +311 -0
  149. beeverforge/envfile.py +55 -0
  150. beeverforge/environ.py +278 -0
  151. beeverforge/factory.py +579 -0
  152. beeverforge/floor/__init__.py +31 -0
  153. beeverforge/floor/ladder.py +725 -0
  154. beeverforge/floor/reading.py +628 -0
  155. beeverforge/gate_seal.py +177 -0
  156. beeverforge/identity/__init__.py +14 -0
  157. beeverforge/identity/base.py +113 -0
  158. beeverforge/identity/local.py +324 -0
  159. beeverforge/identity/oidc.py +654 -0
  160. beeverforge/identity/people.py +444 -0
  161. beeverforge/identity/registry.py +97 -0
  162. beeverforge/knowledge/__init__.py +76 -0
  163. beeverforge/knowledge/bundle.py +220 -0
  164. beeverforge/knowledge/check.py +226 -0
  165. beeverforge/knowledge/contracts.py +407 -0
  166. beeverforge/knowledge/experiment.py +118 -0
  167. beeverforge/knowledge/flows.py +335 -0
  168. beeverforge/knowledge/gaps.py +154 -0
  169. beeverforge/knowledge/gate.py +352 -0
  170. beeverforge/knowledge/gather.py +156 -0
  171. beeverforge/knowledge/generator.py +822 -0
  172. beeverforge/knowledge/inventory.py +557 -0
  173. beeverforge/knowledge/okf.py +370 -0
  174. beeverforge/knowledge/pipeline.py +428 -0
  175. beeverforge/knowledge/render.py +146 -0
  176. beeverforge/knowledge/requests.py +173 -0
  177. beeverforge/knowledge/service.py +53 -0
  178. beeverforge/knowledge/staleness.py +112 -0
  179. beeverforge/knowledge/system/__init__.py +22 -0
  180. beeverforge/knowledge/system/adrs.py +91 -0
  181. beeverforge/knowledge/system/contracts.py +386 -0
  182. beeverforge/knowledge/system/deployments.py +564 -0
  183. beeverforge/knowledge/system/derive.py +887 -0
  184. beeverforge/knowledge/system/interfaces.py +321 -0
  185. beeverforge/knowledge/system/refresh.py +255 -0
  186. beeverforge/knowledge/system/render.py +300 -0
  187. beeverforge/knowledge/system/schemas.py +818 -0
  188. beeverforge/knowledge/system/text.py +201 -0
  189. beeverforge/knowledge/system/tree.py +210 -0
  190. beeverforge/language/__init__.py +15 -0
  191. beeverforge/language/assent.py +135 -0
  192. beeverforge/language/written.py +73 -0
  193. beeverforge/lifecycle/__init__.py +43 -0
  194. beeverforge/lifecycle/card.py +292 -0
  195. beeverforge/lifecycle/executor.py +255 -0
  196. beeverforge/lifecycle/handed_back.py +206 -0
  197. beeverforge/lifecycle/loops.py +404 -0
  198. beeverforge/lifecycle/observed.py +168 -0
  199. beeverforge/lifecycle/ports.py +433 -0
  200. beeverforge/lifecycle/reading.py +100 -0
  201. beeverforge/lifecycle/record.py +211 -0
  202. beeverforge/lifecycle/table.py +673 -0
  203. beeverforge/listeners.py +245 -0
  204. beeverforge/loader.py +173 -0
  205. beeverforge/memory/__init__.py +29 -0
  206. beeverforge/memory/ledger.py +237 -0
  207. beeverforge/memory/messages.py +500 -0
  208. beeverforge/memory/recall.py +414 -0
  209. beeverforge/memory/store.py +157 -0
  210. beeverforge/memory/transcript.py +697 -0
  211. beeverforge/namespace.py +278 -0
  212. beeverforge/observability/__init__.py +23 -0
  213. beeverforge/observability/autonomy.py +233 -0
  214. beeverforge/observability/events.py +195 -0
  215. beeverforge/observability/health.py +533 -0
  216. beeverforge/observability/job_record.py +141 -0
  217. beeverforge/observability/logs.py +368 -0
  218. beeverforge/observability/metrics.py +276 -0
  219. beeverforge/observability/query.py +565 -0
  220. beeverforge/observability/registry.py +260 -0
  221. beeverforge/observability/sqlite_metrics.py +333 -0
  222. beeverforge/observability/trajectory.py +243 -0
  223. beeverforge/onboarding/__init__.py +64 -0
  224. beeverforge/onboarding/concepts.py +450 -0
  225. beeverforge/onboarding/context.py +2428 -0
  226. beeverforge/onboarding/cover.py +97 -0
  227. beeverforge/onboarding/deployment.py +1040 -0
  228. beeverforge/onboarding/firstrun.py +1660 -0
  229. beeverforge/onboarding/history.py +363 -0
  230. beeverforge/onboarding/infer.py +2779 -0
  231. beeverforge/onboarding/live_ci.py +162 -0
  232. beeverforge/onboarding/onboard.py +1101 -0
  233. beeverforge/onboarding/preview_infer.py +1518 -0
  234. beeverforge/onboarding/preview_product.py +354 -0
  235. beeverforge/onboarding/preview_propose.py +1210 -0
  236. beeverforge/onboarding/propose_manifest.py +411 -0
  237. beeverforge/onboarding/questions.py +137 -0
  238. beeverforge/onboarding/readiness.py +1147 -0
  239. beeverforge/onboarding/renew.py +239 -0
  240. beeverforge/onboarding/spend.py +76 -0
  241. beeverforge/onboarding/starter.py +75 -0
  242. beeverforge/ops/__init__.py +6 -0
  243. beeverforge/ops/impediment.py +420 -0
  244. beeverforge/orchestrator/__init__.py +26 -0
  245. beeverforge/orchestrator/context.py +791 -0
  246. beeverforge/orchestrator/errors.py +27 -0
  247. beeverforge/orchestrator/explain.py +430 -0
  248. beeverforge/orchestrator/machine.py +4177 -0
  249. beeverforge/orchestrator/merge_policy.py +244 -0
  250. beeverforge/orchestrator/operator_guidelines.py +406 -0
  251. beeverforge/orchestrator/outcomes.py +39 -0
  252. beeverforge/orchestrator/promotion.py +369 -0
  253. beeverforge/orchestrator/read_only_pass.py +219 -0
  254. beeverforge/orchestrator/risk.py +191 -0
  255. beeverforge/orchestrator/validation.py +172 -0
  256. beeverforge/org_defaults/cards/defect-template.md +23 -0
  257. beeverforge/org_defaults/cards/defect-template.pt-BR.md +23 -0
  258. beeverforge/org_defaults/cards/rubric.yaml +140 -0
  259. beeverforge/org_defaults/cards/template.md +23 -0
  260. beeverforge/org_defaults/cards/template.pt-BR.md +23 -0
  261. beeverforge/org_defaults/engineering.md +92 -0
  262. beeverforge/org_defaults/floor.yaml +192 -0
  263. beeverforge/org_defaults/profiles/prototype.yaml +16 -0
  264. beeverforge/org_defaults/profiles/regulated.yaml +35 -0
  265. beeverforge/org_defaults/requirements/0000-template.md +39 -0
  266. beeverforge/org_defaults/roles/coordinator.md +48 -0
  267. beeverforge/org_defaults/roles/executor.md +100 -0
  268. beeverforge/org_defaults/roles/planner.md +117 -0
  269. beeverforge/org_defaults/roles/product.md +114 -0
  270. beeverforge/org_defaults/roles/recovery.md +35 -0
  271. beeverforge/org_defaults/roles/sizer.md +62 -0
  272. beeverforge/org_defaults/roles/techlead.md +61 -0
  273. beeverforge/org_defaults/tdd.md +10 -0
  274. beeverforge/own_work.py +41 -0
  275. beeverforge/paths.py +144 -0
  276. beeverforge/plugins.py +307 -0
  277. beeverforge/policy/__init__.py +10 -0
  278. beeverforge/policy/authz.py +105 -0
  279. beeverforge/policy/census.py +161 -0
  280. beeverforge/policy/conformance.py +300 -0
  281. beeverforge/policy/context_writes.py +123 -0
  282. beeverforge/policy/floor.py +42 -0
  283. beeverforge/policy/presets.py +174 -0
  284. beeverforge/policy/profiles.py +286 -0
  285. beeverforge/policy/protected.py +175 -0
  286. beeverforge/policy/test_work.py +70 -0
  287. beeverforge/preflight.py +772 -0
  288. beeverforge/presets/node.yaml +6 -0
  289. beeverforge/presets/python.yaml +9 -0
  290. beeverforge/presets/security-oss.yaml +26 -0
  291. beeverforge/presets/terraform.yaml +8 -0
  292. beeverforge/preview/__init__.py +600 -0
  293. beeverforge/preview/admit.py +599 -0
  294. beeverforge/preview/assemble.py +468 -0
  295. beeverforge/preview/demand.py +463 -0
  296. beeverforge/preview/live.py +185 -0
  297. beeverforge/preview/own.py +163 -0
  298. beeverforge/preview/plan.py +205 -0
  299. beeverforge/preview/product.py +219 -0
  300. beeverforge/preview/read.py +379 -0
  301. beeverforge/preview/reap.py +182 -0
  302. beeverforge/preview/siblings.py +140 -0
  303. beeverforge/preview/steps.py +475 -0
  304. beeverforge/preview/unit.py +56 -0
  305. beeverforge/product/__init__.py +56 -0
  306. beeverforge/product/accept.py +276 -0
  307. beeverforge/product/addressing.py +102 -0
  308. beeverforge/product/adjust.py +449 -0
  309. beeverforge/product/agenda.py +329 -0
  310. beeverforge/product/asked.py +270 -0
  311. beeverforge/product/attachments.py +433 -0
  312. beeverforge/product/authoring.py +2727 -0
  313. beeverforge/product/before_code.py +482 -0
  314. beeverforge/product/board.py +634 -0
  315. beeverforge/product/breaking.py +174 -0
  316. beeverforge/product/briefing.py +545 -0
  317. beeverforge/product/brownfield.py +240 -0
  318. beeverforge/product/cap.py +141 -0
  319. beeverforge/product/capabilities.py +438 -0
  320. beeverforge/product/cards.py +1187 -0
  321. beeverforge/product/case.py +545 -0
  322. beeverforge/product/chain.py +389 -0
  323. beeverforge/product/channel.py +215 -0
  324. beeverforge/product/clock.py +102 -0
  325. beeverforge/product/config.py +422 -0
  326. beeverforge/product/confirm.py +1394 -0
  327. beeverforge/product/conversation.py +133 -0
  328. beeverforge/product/corpus.py +624 -0
  329. beeverforge/product/distil.py +597 -0
  330. beeverforge/product/documents/__init__.py +19 -0
  331. beeverforge/product/documents/ingest.py +702 -0
  332. beeverforge/product/documents/reading.py +125 -0
  333. beeverforge/product/documents/record.py +263 -0
  334. beeverforge/product/documents/store.py +146 -0
  335. beeverforge/product/domain.py +283 -0
  336. beeverforge/product/door.py +686 -0
  337. beeverforge/product/engine.py +2686 -0
  338. beeverforge/product/evaluation/__init__.py +26 -0
  339. beeverforge/product/evaluation/__main__.py +80 -0
  340. beeverforge/product/evaluation/battery.py +252 -0
  341. beeverforge/product/evaluation/run.py +428 -0
  342. beeverforge/product/evaluation/score.py +326 -0
  343. beeverforge/product/events.py +968 -0
  344. beeverforge/product/facts.py +371 -0
  345. beeverforge/product/followup.py +761 -0
  346. beeverforge/product/forget.py +718 -0
  347. beeverforge/product/importing.py +1508 -0
  348. beeverforge/product/index/__init__.py +22 -0
  349. beeverforge/product/index/items.py +343 -0
  350. beeverforge/product/index/retrieval.py +577 -0
  351. beeverforge/product/index/search.py +538 -0
  352. beeverforge/product/index/store.py +298 -0
  353. beeverforge/product/index/sync.py +272 -0
  354. beeverforge/product/intents.py +180 -0
  355. beeverforge/product/key.py +43 -0
  356. beeverforge/product/loader.py +302 -0
  357. beeverforge/product/model.py +1591 -0
  358. beeverforge/product/module.py +6800 -0
  359. beeverforge/product/needs_action.py +209 -0
  360. beeverforge/product/onboard.py +402 -0
  361. beeverforge/product/page.py +177 -0
  362. beeverforge/product/progress.py +73 -0
  363. beeverforge/product/queue.py +343 -0
  364. beeverforge/product/reading.py +183 -0
  365. beeverforge/product/refresh.py +402 -0
  366. beeverforge/product/release.py +268 -0
  367. beeverforge/product/requester.py +55 -0
  368. beeverforge/product/role.py +2386 -0
  369. beeverforge/product/semaphore.py +497 -0
  370. beeverforge/product/sessions.py +171 -0
  371. beeverforge/product/sight.py +359 -0
  372. beeverforge/product/sources.py +351 -0
  373. beeverforge/product/speaker.py +166 -0
  374. beeverforge/product/staging.py +992 -0
  375. beeverforge/product/template.py +252 -0
  376. beeverforge/product/triage.py +338 -0
  377. beeverforge/product/unfiled.py +77 -0
  378. beeverforge/product/voice.py +5597 -0
  379. beeverforge/product/waiting.py +113 -0
  380. beeverforge/product/workspace.py +536 -0
  381. beeverforge/registry.py +891 -0
  382. beeverforge/review/__init__.py +11 -0
  383. beeverforge/review/evidence.py +45 -0
  384. beeverforge/review/threads.py +84 -0
  385. beeverforge/review/verdict.py +335 -0
  386. beeverforge/runtime/__init__.py +0 -0
  387. beeverforge/runtime/boxed_job.py +475 -0
  388. beeverforge/runtime/card_repo.py +163 -0
  389. beeverforge/runtime/host.py +398 -0
  390. beeverforge/runtime/repairable.py +232 -0
  391. beeverforge/runtime/repo_cache.py +911 -0
  392. beeverforge/runtime/temporal/__init__.py +26 -0
  393. beeverforge/runtime/temporal/activities.py +7015 -0
  394. beeverforge/runtime/temporal/connection.py +553 -0
  395. beeverforge/runtime/temporal/conversation.py +617 -0
  396. beeverforge/runtime/temporal/deadlines.py +86 -0
  397. beeverforge/runtime/temporal/interceptors.py +81 -0
  398. beeverforge/runtime/temporal/io.py +1042 -0
  399. beeverforge/runtime/temporal/poller.py +185 -0
  400. beeverforge/runtime/temporal/schedule.py +543 -0
  401. beeverforge/runtime/temporal/standing.py +141 -0
  402. beeverforge/runtime/temporal/starter.py +55 -0
  403. beeverforge/runtime/temporal/view.py +1708 -0
  404. beeverforge/runtime/temporal/vocabulary.py +95 -0
  405. beeverforge/runtime/temporal/worker.py +446 -0
  406. beeverforge/runtime/temporal/workflow.py +3550 -0
  407. beeverforge/runtime/toolbox.py +163 -0
  408. beeverforge/scheduler.py +75 -0
  409. beeverforge/semver.py +35 -0
  410. beeverforge/techlead/__init__.py +42 -0
  411. beeverforge/techlead/classify.py +514 -0
  412. beeverforge/techlead/conversation.py +1437 -0
  413. beeverforge/techlead/diagnosis.py +251 -0
  414. beeverforge/techlead/memory.py +292 -0
  415. beeverforge/techlead/pack.py +169 -0
  416. beeverforge/techlead/voice.py +892 -0
  417. beeverforge/techlead/watch.py +647 -0
  418. beeverforge/testing/__init__.py +1 -0
  419. beeverforge/testing/local_flow.py +287 -0
  420. beeverforge/util/__init__.py +0 -0
  421. beeverforge/util/bounded.py +77 -0
  422. beeverforge/util/causes.py +125 -0
  423. beeverforge/util/filelock.py +176 -0
  424. beeverforge/util/scratch.py +80 -0
  425. beeverforge/util/single_flight.py +107 -0
  426. beeverforge/util/yaml_io.py +252 -0
  427. beeverforge-0.1.0.dist-info/METADATA +187 -0
  428. beeverforge-0.1.0.dist-info/RECORD +433 -0
  429. beeverforge-0.1.0.dist-info/WHEEL +5 -0
  430. beeverforge-0.1.0.dist-info/entry_points.txt +2 -0
  431. beeverforge-0.1.0.dist-info/licenses/LICENSE +202 -0
  432. beeverforge-0.1.0.dist-info/licenses/NOTICE +22 -0
  433. beeverforge-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,490 @@
1
+ """What an action IS — the vocabulary the front ends and the catalog share (C-23).
2
+
3
+ Three types and nothing else, because every one of them exists to stop a specific thing from
4
+ leaking across the seam:
5
+
6
+ Actor who asked, so an action never has to guess and a front end never has to decide
7
+ Outcome values out — no HTTP status, no mrkdwn, no emoji, no exception
8
+ ActionSpec a row in a table, so "which actions exist" is data a test can walk
9
+
10
+ **Outcome, not exceptions.** Both front ends already had a hard rule that they must always reply —
11
+ the Slack bot's `act_job` docstring says "NEVER raises (the bot must always reply)" and the panel
12
+ turns everything into an `HTTPException` by hand. Two front ends, two hand-written translations of
13
+ the same failures, and they disagreed: a job that is not parked is a 409 in the panel and the
14
+ sentence "não está parado esperando ação" in Slack, with no shared notion that those are the same
15
+ answer. `code` is that shared notion; rendering it stays each front end's job, which is the only
16
+ part that is genuinely theirs.
17
+
18
+ **`ok` is about the platform, not about the answer.** `scan` finding nothing in TO-DO is `ok=True`
19
+ with a message saying so — nothing went wrong, the queue is empty. `ok=False` means the platform
20
+ could not do the thing it was asked to do. Getting this backwards would make every empty queue
21
+ look like an outage in whatever dashboard reads these.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import secrets
27
+ from collections.abc import Awaitable, Callable, Mapping
28
+ from dataclasses import dataclass, field
29
+ from types import MappingProxyType
30
+
31
+ # ── the codes a front end maps ───────────────────────────────────────────────────────────────────
32
+ #
33
+ # A CLOSED SET, and `beeverforge/api/app.py` holds a status for every member — asserted by a test,
34
+ # because
35
+ # a code with no mapping is a 500 wearing an action's name. Adding one here without adding it there
36
+ # fails the suite rather than production.
37
+
38
+ OK = ""
39
+ #: the caller asked for something malformed — a missing parameter, a ref no tracker could issue
40
+ INVALID = "invalid"
41
+ #: the caller is not allowed to do this
42
+ DENIED = "denied"
43
+ #: the project, ticket or job named does not exist
44
+ NOT_FOUND = "not_found"
45
+ #: the world is not in a state where this makes sense — a job not parked, one already running
46
+ CONFLICT = "conflict"
47
+ #: a dependency the platform needs is not answering — the durable engine, the forge
48
+ UNAVAILABLE = "unavailable"
49
+ #: it was attempted and it did not work
50
+ FAILED = "failed"
51
+ #: the action is catalogued but its implementation has not been moved into the layer yet
52
+ UNIMPLEMENTED = "unimplemented"
53
+
54
+ CODES = frozenset({OK, INVALID, DENIED, NOT_FOUND, CONFLICT, UNAVAILABLE, FAILED, UNIMPLEMENTED})
55
+
56
+
57
+ # ── why, in a word a program reads (#678, part 3) ────────────────────────────────────────────────
58
+ #
59
+ # A CODE IS WHAT A FRONT END MAPS; A REASON IS WHAT A PROGRAM BRANCHES ON. They are the same word
60
+ # wherever nothing finer was said — a row's own refusal states its code as its reason — and they
61
+ # part where one code covers answers a caller must tell apart: `denied` is both "this credential is
62
+ # scoped away from that area" and "you are not an admin", and only one of them is fixed by asking
63
+ # an admin. Every refusing Outcome carries one in `data["reason"]`, and every row declares in the
64
+ # catalogue which it can say (`ActionSpec.refuses`, `actions.reasons`).
65
+
66
+ #: `perform` was asked for a name no row has. Not any row's reason: there is no row to declare it.
67
+ UNKNOWN_ACTION = "unknown_action"
68
+ #: a required parameter is missing, or one the row does not take was passed
69
+ BAD_PARAMETERS = "bad_parameters"
70
+ #: the ticket ref is not one any tracker could issue
71
+ BAD_REF = "bad_ref"
72
+ #: the credential is scoped away from the area the row belongs to
73
+ OUT_OF_SCOPE = "out_of_scope"
74
+ #: the row changes the installation, and the credential is scoped to an area (#741) — DENIED like
75
+ #: `out_of_scope`, and told apart from it because the remedy differs: a credential of another area
76
+ #: will not do, only the deployment's administrator will
77
+ NOT_THE_INSTALLATIONS = "not_the_installations"
78
+ #: the row needs an admin and the actor is not one
79
+ NOT_ADMIN = "not_admin"
80
+ #: the row needs the project's code and the project started before it (#992): its product has a
81
+ #: context repository and no code repository yet
82
+ NO_CODE_YET = "no_code_yet"
83
+ #: the durable engine did not answer in time
84
+ ENGINE_UNREACHABLE = "engine_unreachable"
85
+ #: the row's implementation raised — the answer's `data["reference"]` names its log line (#908)
86
+ RAISED = "raised"
87
+ #: the row's implementation returned something that is not an Outcome
88
+ REPORTED_NOTHING = "reported_nothing"
89
+
90
+ #: What `perform` itself may refuse ANY row with, before or around its runner — in the order it
91
+ #: checks them. `actions.reasons` narrows it per row: `bad_ref` only for a row that takes an
92
+ #: `issue`, `not_the_installations` only for one that changes the `installation`, `not_admin` only
93
+ #: for one that `needs_admin`, `no_code_yet` only for one that `needs_code` and takes a `project`.
94
+ AROUND_EVERY_ROW = (BAD_PARAMETERS, BAD_REF, OUT_OF_SCOPE, NOT_THE_INSTALLATIONS, NOT_ADMIN,
95
+ NO_CODE_YET, ENGINE_UNREACHABLE, RAISED, REPORTED_NOTHING)
96
+
97
+ #: Every reason a refusal may carry. Closed, like `CODES`, so a typo is an error at the refusal
98
+ #: rather than a word no program was told to expect.
99
+ REASONS = frozenset(CODES - {OK}) | {UNKNOWN_ACTION, *AROUND_EVERY_ROW}
100
+
101
+
102
+ # ── where an action lives, and where a credential may go ─────────────────────────────────────────
103
+ #
104
+ # TWO AREAS TODAY, and the split is the one the product owner drew: *"if the person wants to write
105
+ # the tickets and drop them in TO-DO, fine — but the product role is available ON THE PLATFORM"*.
106
+ # A business analyst writing requirements is not an operator of the floor, and the panel's posture
107
+ # until now was that holding its credential made you both.
108
+
109
+ #: Jobs, the board, merges, the engine — the operator's console. The default for every row.
110
+ FLOOR = "floor"
111
+ #: The product role: requirements, their drafting, sign-off and withdrawal.
112
+ PRODUCT = "product"
113
+
114
+ #: Every area a row may declare. A registry rather than a free string, for the reason every other
115
+ #: registry in this codebase exists: a typo'd scope would otherwise be an area nobody can reach and
116
+ #: nothing would say so.
117
+ SCOPES = frozenset({FLOOR, PRODUCT})
118
+
119
+
120
+ # ── who asked ────────────────────────────────────────────────────────────────────────────────────
121
+
122
+ @dataclass(frozen=True)
123
+ class Actor:
124
+ """The human (or bot) on whose behalf an action runs.
125
+
126
+ EVERY ACTION TAKES ONE, including the read-only ones. Not because they all check it today —
127
+ most do not — but because the parameter is the seam C-26 fills, and a parameter added later to
128
+ forty call sites is a migration, while a parameter carried from the start is a rename. It is
129
+ also already useful: `perform` writes one audit line per action naming this, which is the first
130
+ time this platform can answer *who approved that production release*.
131
+
132
+ `admin` IS DECIDED BY THE TRANSPORT TODAY, and that is a stated limitation rather than a
133
+ design. Slack knows its answer (`project.admins`), the panel's answer is "did they hold the
134
+ panel token", and the CLI's is "they have a shell on the host, which outranks every action
135
+ here". C-26 (#55) moves the decision behind this field into a policy the Core owns; until then
136
+ the field is where the three front ends' three different answers meet, which is already one
137
+ place fewer than before.
138
+ """
139
+
140
+ #: Stable id in whatever namespace the transport owns: a Slack user id (`U04…`), a panel
141
+ #: principal, `cli`. Not required to be globally unique across transports — `via` disambiguates.
142
+ id: str
143
+ #: What to write in a note a human reads. Falls back to `id` when the transport has no name.
144
+ display: str = ""
145
+ #: Which front end carried the request: `panel`, `slack`, `cli`, `test`.
146
+ via: str = ""
147
+ #: Whether this actor may run an action marked `needs_admin`. See the caveat above.
148
+ admin: bool = False
149
+ #: WHICH AREAS OF THE PLATFORM this credential may act in at all, or `None` for one that is
150
+ #: not scoped. `None` and `frozenset()` are different answers and the distinction is the whole
151
+ #: point: `None` means nobody restricted this caller (the CLI, a Slack admin, the panel's own
152
+ #: token — every actor that existed before scopes did, which is why the default cannot be
153
+ #: `frozenset()` without denying all of them); `frozenset()` means somebody scoped it to
154
+ #: nothing. The same `None` vs `[]` rule this codebase pays for everywhere else.
155
+ #:
156
+ #: WHY THIS IS NOT `admin`. A business analyst who writes requirements needs `needs_admin` to
157
+ #: be TRUE for `product_accept` — accepting is the most consequential act on that surface — and
158
+ #: needs `merge` to be refused outright. Those are two different questions: *how much authority
159
+ #: within an area*, and *which areas at all*. Folding them into one flag is how handing a BA a
160
+ #: credential to write a requirement also handed them the button that lands a pull request.
161
+ scopes: frozenset[str] | None = None
162
+ #: THE CONVERSATION THIS ACTOR IS IN, when the transport keys one per person (#33). On Slack
163
+ #: a thread comes free and the rows take it as `thread`; on the web nothing did, so every
164
+ #: person who typed into the panel's box wrote into ONE conversation keyed by the project's
165
+ #: name, and the product role read A and B as one person. The panel fills this from the
166
+ #: subject it resolved — `person:<id>` for somebody known, `visitor:<cookie>` for a browser
167
+ #: nobody has identified yet — and a product row uses it when the caller passed no thread.
168
+ #: Empty means the transport keys nothing, which is every actor that predates this.
169
+ conversation: str = ""
170
+ #: WHAT AN ANONYMOUS CALLER SAYS IT IS — a script's or a bot's own name for itself, sent on a
171
+ #: shared token. A DISPLAY HINT AND NOTHING ELSE: it is not `id`, not `display`, and `str()`
172
+ #: leaves it out, because those are what a row records as who acted, vouched or decided. Only
173
+ #: `perform`'s audit line prints it, as `label=`, beside the credential that was really used.
174
+ #: Empty for a named person, whose name is the provider's and not theirs to change.
175
+ label: str = ""
176
+
177
+ def may_enter(self, scope: str) -> bool:
178
+ """Whether this actor may act in `scope`. Unscoped actors may enter anywhere."""
179
+ return self.scopes is None or scope in self.scopes
180
+
181
+ def __str__(self) -> str:
182
+ who = self.display or self.id or "anonymous"
183
+ return f"{who} (via {self.via})" if self.via else who
184
+
185
+
186
+ #: For the paths that genuinely have no subject yet — a scheduled poll, an internal call. Kept as a
187
+ #: named constant rather than `Actor("")` at each site so a grep for it finds every place the
188
+ #: platform acts on nobody's behalf, which is exactly the list C-26 will want.
189
+ SYSTEM = Actor(id="system", display="the factory", via="internal", admin=True)
190
+
191
+
192
+ # ── what comes back ──────────────────────────────────────────────────────────────────────────────
193
+
194
+ @dataclass(frozen=True)
195
+ class Outcome:
196
+ """Values out. No status code, no markup, no vendor.
197
+
198
+ `message` is ONE human sentence in plain text. It may be shown to a client, so it says what
199
+ happened and — when something is wrong — what to do about it, in the register the rest of this
200
+ platform uses. It must not contain mrkdwn, HTML, or a provider's link syntax: the front end
201
+ that knows the surface decorates it, and a message pre-decorated for Slack renders as literal
202
+ asterisks in the panel (which is how the first version of the impediment alert shipped).
203
+ """
204
+
205
+ ok: bool
206
+ message: str
207
+ #: Values for a machine — what a panel renders as a table, what a script greps. Free-form per
208
+ #: action and documented on its spec.
209
+ data: Mapping[str, object] = field(default_factory=dict)
210
+ #: One of CODES. Empty when `ok`.
211
+ code: str = OK
212
+
213
+
214
+ def done(message: str, **data: object) -> Outcome:
215
+ """It worked. `data` carries whatever the caller may want to render."""
216
+ return Outcome(ok=True, message=message, data=data, code=OK)
217
+
218
+
219
+ def refused(code: str, message: str, *, reason: str = "", **data: object) -> Outcome:
220
+ """It did not work, and this is why — in a word a front end can map and a sentence a human can
221
+ read. Both are required: the word alone gives an operator nothing, and the sentence alone makes
222
+ every front end parse prose to decide a status.
223
+
224
+ `reason` lands in `data["reason"]`, and is `code` when nothing finer is said — so a refusal
225
+ never lacks one. It must be one of `REASONS`, and one the row declares (`ActionSpec.refuses`).
226
+ """
227
+ if code not in CODES or code == OK:
228
+ raise ValueError(f"unknown outcome code {code!r} — one of {sorted(CODES - {OK})}")
229
+ why = reason or code
230
+ if why not in REASONS:
231
+ raise ValueError(f"unknown refusal reason {why!r} — one of {sorted(REASONS)}")
232
+ return Outcome(ok=False, message=message, data={**data, "reason": why}, code=code)
233
+
234
+
235
+ def failure_reference() -> str:
236
+ """A short reference for one failure, put in BOTH its log line and its answer (`data.reference`,
237
+ #908), so the person who reports "it said it could not" hands an operator the line to read.
238
+ Eight hex digits: a reference to quote, not an identifier to guess — the line it names carries
239
+ nothing the answer did not already say to the same person."""
240
+ return secrets.token_hex(4)
241
+
242
+
243
+ # ── a row of several steps (#741) ────────────────────────────────────────────────────────────────
244
+ #
245
+ # SOME ROWS ARE SEVERAL ACTS WITH THE NETWORK BETWEEN THEM. `project_init` registers a project,
246
+ # creates its board and writes its manifest; `product_init` creates a context repository and
247
+ # proposes a declaration in it. One sentence about the last thing that broke says nothing about
248
+ # the half that worked, and a retry that redoes that half makes a second board or a second
249
+ # repository. So such a row says, in `data["steps"]`, what each step came to — in order, each a
250
+ # `step(...)` — and every step asks first whether its result is already there, so running the row
251
+ # again does only what is missing.
252
+
253
+ #: this run did it
254
+ STEP_DONE = "done"
255
+ #: it was there before this run, and nothing was done
256
+ STEP_ALREADY = "already"
257
+ #: it is not this project's to do here — a tracker that brings its own board
258
+ STEP_SKIPPED = "skipped"
259
+ #: it was tried and did not happen, and `said` is why. The row is then not `ok`, and running it
260
+ #: again starts from what is still missing
261
+ STEP_FAILED = "failed"
262
+
263
+ STEP_STATES = frozenset({STEP_DONE, STEP_ALREADY, STEP_SKIPPED, STEP_FAILED})
264
+
265
+
266
+ def step(name: str, state: str, said: str, **data: object) -> dict[str, object]:
267
+ """One step of a several-step row, as `data["steps"]` carries it: `{"step", "state", "said"}`
268
+ and whatever the step adds. A state outside `STEP_STATES` is refused here, for the reason
269
+ `refused` refuses a code outside `CODES`."""
270
+ if state not in STEP_STATES:
271
+ raise ValueError(f"unknown step state {state!r} — one of {sorted(STEP_STATES)}")
272
+ return {"step": name, "state": state, "said": said, **data}
273
+
274
+
275
+ # ── the table ────────────────────────────────────────────────────────────────────────────────────
276
+
277
+ #: The signature every action has. Keyword-only, always async, always returns an Outcome.
278
+ Runner = Callable[..., Awaitable[Outcome]]
279
+
280
+
281
+ #: WHAT TO PUT IN EACH PARAMETER, keyed by its name — the vocabulary of the action layer (#172).
282
+ #:
283
+ #: SHARED RATHER THAN PER ROW, because `project` is required by 38 of the 40 rows and `issue` by
284
+ #: 12. Forty copies of "the project this concerns" is thirty-nine chances to drift, and the one
285
+ #: that drifts is the one somebody reads. A row that means something DIFFERENT by a shared word
286
+ #: says so on itself, in `ActionSpec.params`, and that override is the signal to a reader that this
287
+ #: row is unusual.
288
+ #:
289
+ #: THE AUDIENCE IS SOMEBODY WHO MUST FILL THE VALUE IN, which is why these say what goes in rather
290
+ #: than what the word denotes. Since #170 that audience includes a model composing a proposal for a
291
+ #: button a human presses: `adjust` required an `instruction` and nothing anywhere said what an
292
+ #: instruction should contain, so the only guidance was the English of the word.
293
+ PARAMS: dict[str, str] = {
294
+ # the two nearly every row takes
295
+ "project": "the project's name in this deployment's registry, e.g. `podbeam`",
296
+ "issue": "the ticket's number on the client's board, digits only, e.g. `87`",
297
+ # confirmation and shape
298
+ "yes": "`true` to actually do it — without it the action reports what it WOULD do",
299
+ "number": "the ticket's number on the client's board, digits only",
300
+ "numbers": "the ticket numbers, comma-separated, e.g. `87,88,91`",
301
+ "limit": "how many rows to return at most; the default is this deployment's",
302
+ "force": "`true` to proceed past a check that would otherwise stop it — say why in `reason`",
303
+ # words a human will read afterwards
304
+ "reason": "one sentence a person will read later, saying WHY — it is stored and shown",
305
+ "comment": "a sentence posted on the pull request as this platform's own voice",
306
+ "instruction": "WHAT TO CHANGE, in one or two concrete sentences the coding pass will work "
307
+ "from: name the file or behaviour and the change wanted, not the symptom. It "
308
+ "is shown on the button a person presses, so it must be readable by them too",
309
+ "message": "what you want to say, in your own words",
310
+ "question": "the question, in one sentence",
311
+ "query": "what to look for — a few words, a card number, a name",
312
+ "answer": "the answer, in your own words",
313
+ "answers": "answers to the questions asked, one per line as `field: value`",
314
+ "choice": "which of the options the parked job offered — its exact label",
315
+ "enabled": "`true` to let this project pick work up, `false` to hold it",
316
+ # the release gate
317
+ "version": "the version being released, exactly as it is tagged",
318
+ "approver": "who is approving, by name — it is written into the release record",
319
+ "password": "the release password this deployment was configured with",
320
+ "promote": "`true` to carry the change on through the promotion chain after it lands",
321
+ "sandbox": "`true` to run the job without letting it write anywhere real",
322
+ "durable": "`true` to run on the durable engine rather than in this process",
323
+ # the product role
324
+ "token": "the token identifying the proposal being answered — copy it from the proposal",
325
+ "thread": "the conversation this turn belongs to, if continuing one",
326
+ "before": "the cursor of an earlier page of a conversation — the `earlier` value the page "
327
+ "after it handed back; empty for the newest page",
328
+ "context": "what the person is looking at: `{page, project, card}` as the panel's page says "
329
+ "it — checked against who they are before the role reads it",
330
+ "message_id": "this message's own id, so a retry of it is the same message and its answer "
331
+ "can be matched to it — letters, digits, `-` and `_`",
332
+ "wait": "`false` to hand the message over and return at once, the answer arriving where the "
333
+ "conversation is shown; the default waits for it",
334
+ "mentioned": "`false` when the message was said in the room to the people there and does "
335
+ "not name the product role — it is kept, and starts no turn; the default is "
336
+ "that calling this row names the role",
337
+ "in_favour_of": "the number of the card that STAYS — the one this duplicates",
338
+ "requirement": "the requirement's id in its register, e.g. `REQ-014`",
339
+ "decision": "the decision, stated as what will now be true",
340
+ "term": "the word or phrase being defined, as the business says it",
341
+ "body": "what it means, in the business's own words",
342
+ "restated": "the broken promise, restated as what should happen and what happens instead",
343
+ "text": ("what the card should say instead — it replaces what was asked, or what is "
344
+ "happening, on a card the product role opened"),
345
+ "title": "what it is called, in the person's own words — short",
346
+ "head": ("the head the card's preview was built from, as the card showed it — the version the "
347
+ "person tried; empty takes the one the preview shows now"),
348
+ "criteria": ("what must be true for the card to be done, one statement per line — it replaces "
349
+ "the card's acceptance criteria; empty leaves them as they are"),
350
+ "session": "which of your conversations — its id, as the page lists it; empty is your first",
351
+ "attachments": "the ids of the files sent with the message, as the upload answered them",
352
+ "attachment": "the id of one file sent in the conversation, as the upload answered it",
353
+ "room": "yes for the project's room, no for one of your own conversations",
354
+ "column": "the column's name as it is written on the board, e.g. `TO-DO`",
355
+ "violates": "the id of the requirement this breaks, if one is known",
356
+ "severity": "how bad it is: `low`, `medium` or `high`",
357
+ # reading a repository
358
+ "target": "the repository to read, as a URL or an `owner/name`",
359
+ "ask": "a specific question to answer about the repository, if you have one",
360
+ "write": "`true` to write the result into the repository rather than only report it",
361
+ "accept": "which proposed fields to keep, comma-separated — the rest are dropped",
362
+ "out": "where to write the file, if not the default place",
363
+ "pr": "`true` to open a pull request with the change instead of committing directly",
364
+ # a preview of the product (ADR-0050)
365
+ "unit": "what the preview is of: a card's number, e.g. `34`, or a requirement as `req0012`",
366
+ # the installation itself (#741)
367
+ "note": "why, and for whom — recorded on the poller's schedule and read back by whoever "
368
+ "looks next, so a held queue can be told from an outage",
369
+ "docs_repo": "the repository that holds the product's requirements, spelled the way this "
370
+ "project's forge names one of its repositories",
371
+ }
372
+
373
+
374
+ @dataclass(frozen=True)
375
+ class ActionSpec:
376
+ """One row of the catalog: a name, what it needs, and the one implementation.
377
+
378
+ `required`/`optional` are declared rather than read off the function signature by
379
+ introspection. Introspection would be shorter and would also mean `perform` cannot tell a
380
+ parameter it forgot to pass from one the action does not want — and the whole reason the
381
+ parameters are checked centrally is that the two front ends validated different things.
382
+ """
383
+
384
+ name: str
385
+ #: One line, present tense, for `beeverforge act --list` and `GET /api/actions`. This is the
386
+ #: text an operator reads when deciding what to run, so it says what happens, not what it is
387
+ #: called.
388
+ summary: str
389
+ run: Runner
390
+ required: tuple[str, ...] = ()
391
+ optional: tuple[str, ...] = ()
392
+ #: Whether `Actor.admin` is required. Default True: an action exists to CHANGE something, and
393
+ #: the read-only ones say so explicitly. A default of False would mean a new action is
394
+ #: unguarded unless somebody remembers, which is the wrong way round for this class of mistake.
395
+ needs_admin: bool = True
396
+ #: WHICH AREA of the platform this row belongs to — see `Actor.scopes`. Defaults to the floor
397
+ #: (jobs, the board, merges, the engine), so a row added without thinking about scope lands in
398
+ #: the area a scoped credential CANNOT reach. Same direction as `needs_admin`'s default: the
399
+ #: forgetful case is the closed one.
400
+ scope: str = FLOOR
401
+ #: WHETHER THIS ROW NEEDS THE PROJECT'S CODE (#992). A product may start before its code — a
402
+ #: context repository and no code repository (`contracts.project.before_its_code`) — and on
403
+ #: such a project `perform` refuses a row that needs one, by name and before it runs, rather
404
+ #: than letting it fail three layers down in a sentence about a clone. Default True, the
405
+ #: direction of `needs_admin`'s and `scope`'s: a row added without thinking about it waits for
406
+ #: the code, and the rows that work without it — the product owner's conversation, its
407
+ #: requirements, its context — say so on themselves.
408
+ needs_code: bool = True
409
+ #: WHETHER THIS ROW CHANGES THE INSTALLATION ITSELF (#741) — the registry's projects, the
410
+ #: people the deployment names, the poller's schedule — rather than work inside it. Such a row
411
+ #: is THE ADMINISTRATOR'S, and `perform` refuses it to any credential scoped to an area: a
412
+ #: board token is the floor's operator and a product token the requirement-writer's, and
413
+ #: neither may name a product, pause pickup or invite somebody, whichever area the row is in.
414
+ #:
415
+ #: DECLARED ON THE ROW AND ASKED ONCE, IN `perform`. `people_invite` carried this check in its
416
+ #: own body (#562), and one copy per installation row is how the row added next comes to
417
+ #: forget it — the reason `needs_admin` and `scope` are declarations rather than lines each
418
+ #: runner remembers. Default False: work on the floor or the product is the ordinary case, and
419
+ #: the rows that change the installation say so.
420
+ installation: bool = False
421
+ #: WHEN A PROPOSER SHOULD PICK THIS ROW over its neighbours — the judgment, not the definition
422
+ #: (#172). `summary` says what the row does; this says when it is the right answer, which is a
423
+ #: different question and the one that actually decides. It lived in a private map inside
424
+ #: `techlead/conversation.py`, where it could disagree with the row it described — and did:
425
+ #: `adjust` had no entry at all, so the verb that most needed the guidance had the least.
426
+ #: Empty is honest for the rows nobody may propose; `proposable` is a much smaller set than
427
+ #: the catalogue and only it is offered as a choice.
428
+ choose_when: str = ""
429
+ #: WHAT THIS ROW means by a parameter, when the shared `PARAMS` word is wrong for it. An
430
+ #: override rather than a copy: a row that agrees says nothing, so the diff of this field is a
431
+ #: list of the rows that are unusual.
432
+ params: Mapping[str, str] = MappingProxyType({})
433
+ #: WHAT THIS ROW'S OWN CODE PATHS REFUSE WITH — the reasons its runner can put in
434
+ #: `data["reason"]`. Written on the row rather than discovered, because it is a promise to
435
+ #: whoever reads `/api/actions`; `tests/test_the_action_layer.py` reads the runner's source and
436
+ #: fails when the two disagree in either direction. What `perform` adds around every row is
437
+ #: not repeated here: `actions.reasons` is the whole list.
438
+ refuses: tuple[str, ...] = ()
439
+ #: THE ROWS WHOSE OUTCOME THIS ONE HANDS BACK AS ITS OWN, reason included — `ask` performs the
440
+ #: row a sentence names. Their reasons are this row's too.
441
+ relays: tuple[str, ...] = ()
442
+
443
+ def prose_for(self, param: str) -> str:
444
+ """What to put in `param`, or `""` when nobody has said (#172).
445
+
446
+ THREE ANSWERS COLLAPSED TO TWO ON PURPOSE, unlike the reads elsewhere in the platform: a
447
+ parameter nobody wrote prose for and a parameter that needs no explanation are the same
448
+ thing to the person filling it in. What must NOT happen is inventing a sentence from the
449
+ name, which is what every front end would do on its own."""
450
+ return self.params.get(param) or PARAMS.get(param, "")
451
+
452
+ @property
453
+ def described(self) -> dict[str, str]:
454
+ """Every parameter this row takes, each with what to put in it — the shape a front end
455
+ renders and `/api/actions` serves. Includes the ones with no prose, as empty strings: a
456
+ parameter missing from this mapping would read as one the action does not take."""
457
+ return {p: self.prose_for(p) for p in self.parameters}
458
+
459
+ @property
460
+ def pending(self) -> str:
461
+ """Where the implementation still lives, or `""` when it has been moved into the layer.
462
+
463
+ Not a boolean: an operator who hits `unimplemented` needs to know which old route still
464
+ works, and a test needs to see the migration shrink."""
465
+ return getattr(self.run, "moved_from", "")
466
+
467
+ @property
468
+ def parameters(self) -> tuple[str, ...]:
469
+ return self.required + self.optional
470
+
471
+
472
+ def not_moved_yet(name: str, *, still_in: str) -> Runner:
473
+ """A placeholder implementation that REFUSES, naming where the working path is.
474
+
475
+ WHY A REFUSING RUNNER RATHER THAN AN ABSENT ROW. The catalog is what the guards walk, what the
476
+ panel lists and what `beeverforge act --list` prints. An action left out of it until somebody
477
+ moves the code is invisible: no test can assert it is missing, and the migration has no
478
+ measurable end. A row that refuses is visible in all three places and its refusal is a sentence
479
+ somebody can act on, which is the same standard every other wait in this platform is held to.
480
+ """
481
+
482
+ async def run(**_params: object) -> Outcome:
483
+ return refused(
484
+ UNIMPLEMENTED,
485
+ f"'{name}' is catalogued but its implementation has not moved into the action layer "
486
+ f"yet — it still lives in {still_in}, which is where it works today (C-23).",
487
+ )
488
+
489
+ run.moved_from = still_in # type: ignore[attr-defined]
490
+ return run