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.
- beeverforge/__init__.py +7 -0
- beeverforge/actions/__init__.py +506 -0
- beeverforge/actions/base.py +490 -0
- beeverforge/actions/catalog.py +9549 -0
- beeverforge/actions/floor_intents.py +432 -0
- beeverforge/adapters/__init__.py +0 -0
- beeverforge/adapters/agent/__init__.py +71 -0
- beeverforge/adapters/agent/base.py +1101 -0
- beeverforge/adapters/agent/claude_code.py +1326 -0
- beeverforge/adapters/agent/codex.py +449 -0
- beeverforge/adapters/agent/kimi.py +442 -0
- beeverforge/adapters/agent/opencode.py +520 -0
- beeverforge/adapters/agent/registry.py +550 -0
- beeverforge/adapters/agent/roles.py +326 -0
- beeverforge/adapters/agent/routes.py +255 -0
- beeverforge/adapters/agent/session_store.py +221 -0
- beeverforge/adapters/agent/stream.py +367 -0
- beeverforge/adapters/agent/techlead.py +108 -0
- beeverforge/adapters/agent/token_pool.py +67 -0
- beeverforge/adapters/azure_devops.py +466 -0
- beeverforge/adapters/board/__init__.py +10 -0
- beeverforge/adapters/board/azure_devops.py +831 -0
- beeverforge/adapters/board/base.py +545 -0
- beeverforge/adapters/board/columns.py +261 -0
- beeverforge/adapters/board/factory.py +388 -0
- beeverforge/adapters/board/jira.py +487 -0
- beeverforge/adapters/board/local.py +360 -0
- beeverforge/adapters/board_db.py +423 -0
- beeverforge/adapters/board_setup/__init__.py +8 -0
- beeverforge/adapters/board_setup/base.py +60 -0
- beeverforge/adapters/board_setup/local.py +147 -0
- beeverforge/adapters/board_setup/registry.py +41 -0
- beeverforge/adapters/channel/__init__.py +5 -0
- beeverforge/adapters/channel/base.py +135 -0
- beeverforge/adapters/channel/panel.py +107 -0
- beeverforge/adapters/channel/registry.py +115 -0
- beeverforge/adapters/credential/__init__.py +8 -0
- beeverforge/adapters/credential/registry.py +342 -0
- beeverforge/adapters/credential/short_lived.py +69 -0
- beeverforge/adapters/credential_store/__init__.py +10 -0
- beeverforge/adapters/credential_store/base.py +39 -0
- beeverforge/adapters/credential_store/env.py +21 -0
- beeverforge/adapters/credential_store/file.py +329 -0
- beeverforge/adapters/credential_store/registry.py +413 -0
- beeverforge/adapters/embed/__init__.py +8 -0
- beeverforge/adapters/embed/base.py +39 -0
- beeverforge/adapters/embed/local.py +216 -0
- beeverforge/adapters/embed/registry.py +136 -0
- beeverforge/adapters/environment/__init__.py +30 -0
- beeverforge/adapters/environment/azure_pipelines.py +412 -0
- beeverforge/adapters/environment/base.py +35 -0
- beeverforge/adapters/environment/github_actions.py +108 -0
- beeverforge/adapters/environment/none.py +75 -0
- beeverforge/adapters/environment/registry.py +210 -0
- beeverforge/adapters/extract/__init__.py +6 -0
- beeverforge/adapters/extract/base.py +86 -0
- beeverforge/adapters/extract/mail.py +83 -0
- beeverforge/adapters/extract/markup.py +278 -0
- beeverforge/adapters/extract/model.py +94 -0
- beeverforge/adapters/extract/office.py +269 -0
- beeverforge/adapters/extract/pdf.py +332 -0
- beeverforge/adapters/extract/registry.py +218 -0
- beeverforge/adapters/extract/text.py +251 -0
- beeverforge/adapters/extract/vision.py +82 -0
- beeverforge/adapters/forge/__init__.py +30 -0
- beeverforge/adapters/forge/azure_devops.py +1961 -0
- beeverforge/adapters/forge/base.py +911 -0
- beeverforge/adapters/forge/github.py +1428 -0
- beeverforge/adapters/forge/local.py +894 -0
- beeverforge/adapters/forge/registry.py +252 -0
- beeverforge/adapters/github_app.py +173 -0
- beeverforge/adapters/github_cli.py +81 -0
- beeverforge/adapters/gitworktrees.py +183 -0
- beeverforge/adapters/notify/__init__.py +12 -0
- beeverforge/adapters/notify/base.py +45 -0
- beeverforge/adapters/notify/panel.py +43 -0
- beeverforge/adapters/notify/registry.py +301 -0
- beeverforge/adapters/preview/__init__.py +23 -0
- beeverforge/adapters/preview/base.py +221 -0
- beeverforge/adapters/preview/compose.py +1464 -0
- beeverforge/adapters/preview/none.py +60 -0
- beeverforge/adapters/preview/registry.py +125 -0
- beeverforge/adapters/reviewer/__init__.py +30 -0
- beeverforge/adapters/reviewer/base.py +75 -0
- beeverforge/adapters/reviewer/claude_code.py +195 -0
- beeverforge/adapters/reviewer/harness.py +169 -0
- beeverforge/adapters/sandbox/__init__.py +33 -0
- beeverforge/adapters/sandbox/base.py +470 -0
- beeverforge/adapters/sandbox/container.py +903 -0
- beeverforge/adapters/sandbox/pull.py +144 -0
- beeverforge/adapters/sandbox/registry.py +596 -0
- beeverforge/adapters/sandbox/timeouts.py +76 -0
- beeverforge/adapters/sandbox/worktree.py +1223 -0
- beeverforge/adapters/tracker/__init__.py +30 -0
- beeverforge/adapters/tracker/azure_devops.py +1058 -0
- beeverforge/adapters/tracker/base.py +876 -0
- beeverforge/adapters/tracker/github.py +732 -0
- beeverforge/adapters/tracker/github_board_setup.py +241 -0
- beeverforge/adapters/tracker/github_project.py +811 -0
- beeverforge/adapters/tracker/jira.py +1124 -0
- beeverforge/adapters/tracker/local.py +578 -0
- beeverforge/adapters/tracker/parse.py +573 -0
- beeverforge/adapters/tracker/registry.py +255 -0
- beeverforge/after_merge.py +123 -0
- beeverforge/agent_answer.py +199 -0
- beeverforge/api/__init__.py +4 -0
- beeverforge/api/app.py +4603 -0
- beeverforge/api/events.py +489 -0
- beeverforge/api/metrics_view.py +390 -0
- beeverforge/api/openapi.py +55 -0
- beeverforge/api/panel.html +6049 -0
- beeverforge/api/product_chat.py +709 -0
- beeverforge/api/project_route.py +89 -0
- beeverforge/api/read_models.py +774 -0
- beeverforge/approvals.py +341 -0
- beeverforge/box_prove.py +2150 -0
- beeverforge/certify/__init__.py +34 -0
- beeverforge/certify/controls.py +732 -0
- beeverforge/certify/pack.py +779 -0
- beeverforge/certify/pack.schema.json +226 -0
- beeverforge/certify/redact.py +336 -0
- beeverforge/certify/schema.py +144 -0
- beeverforge/certify/thresholds.yaml +61 -0
- beeverforge/certify/verify.py +469 -0
- beeverforge/cli.py +4642 -0
- beeverforge/cli_refusals.py +255 -0
- beeverforge/conformance/__init__.py +20 -0
- beeverforge/conformance/adapters.py +776 -0
- beeverforge/contracts/__init__.py +65 -0
- beeverforge/contracts/aliases.py +106 -0
- beeverforge/contracts/bot.py +42 -0
- beeverforge/contracts/checks.py +340 -0
- beeverforge/contracts/commands.py +183 -0
- beeverforge/contracts/decision.py +286 -0
- beeverforge/contracts/document.py +160 -0
- beeverforge/contracts/item_space.py +86 -0
- beeverforge/contracts/manifest.py +921 -0
- beeverforge/contracts/product.py +265 -0
- beeverforge/contracts/profile.py +214 -0
- beeverforge/contracts/project.py +597 -0
- beeverforge/contracts/refs.py +217 -0
- beeverforge/contracts/review.py +66 -0
- beeverforge/contracts/run.py +456 -0
- beeverforge/contracts/state.py +101 -0
- beeverforge/contracts/ticket.py +146 -0
- beeverforge/credentials.py +853 -0
- beeverforge/doctor.py +3504 -0
- beeverforge/doors.py +311 -0
- beeverforge/envfile.py +55 -0
- beeverforge/environ.py +278 -0
- beeverforge/factory.py +579 -0
- beeverforge/floor/__init__.py +31 -0
- beeverforge/floor/ladder.py +725 -0
- beeverforge/floor/reading.py +628 -0
- beeverforge/gate_seal.py +177 -0
- beeverforge/identity/__init__.py +14 -0
- beeverforge/identity/base.py +113 -0
- beeverforge/identity/local.py +324 -0
- beeverforge/identity/oidc.py +654 -0
- beeverforge/identity/people.py +444 -0
- beeverforge/identity/registry.py +97 -0
- beeverforge/knowledge/__init__.py +76 -0
- beeverforge/knowledge/bundle.py +220 -0
- beeverforge/knowledge/check.py +226 -0
- beeverforge/knowledge/contracts.py +407 -0
- beeverforge/knowledge/experiment.py +118 -0
- beeverforge/knowledge/flows.py +335 -0
- beeverforge/knowledge/gaps.py +154 -0
- beeverforge/knowledge/gate.py +352 -0
- beeverforge/knowledge/gather.py +156 -0
- beeverforge/knowledge/generator.py +822 -0
- beeverforge/knowledge/inventory.py +557 -0
- beeverforge/knowledge/okf.py +370 -0
- beeverforge/knowledge/pipeline.py +428 -0
- beeverforge/knowledge/render.py +146 -0
- beeverforge/knowledge/requests.py +173 -0
- beeverforge/knowledge/service.py +53 -0
- beeverforge/knowledge/staleness.py +112 -0
- beeverforge/knowledge/system/__init__.py +22 -0
- beeverforge/knowledge/system/adrs.py +91 -0
- beeverforge/knowledge/system/contracts.py +386 -0
- beeverforge/knowledge/system/deployments.py +564 -0
- beeverforge/knowledge/system/derive.py +887 -0
- beeverforge/knowledge/system/interfaces.py +321 -0
- beeverforge/knowledge/system/refresh.py +255 -0
- beeverforge/knowledge/system/render.py +300 -0
- beeverforge/knowledge/system/schemas.py +818 -0
- beeverforge/knowledge/system/text.py +201 -0
- beeverforge/knowledge/system/tree.py +210 -0
- beeverforge/language/__init__.py +15 -0
- beeverforge/language/assent.py +135 -0
- beeverforge/language/written.py +73 -0
- beeverforge/lifecycle/__init__.py +43 -0
- beeverforge/lifecycle/card.py +292 -0
- beeverforge/lifecycle/executor.py +255 -0
- beeverforge/lifecycle/handed_back.py +206 -0
- beeverforge/lifecycle/loops.py +404 -0
- beeverforge/lifecycle/observed.py +168 -0
- beeverforge/lifecycle/ports.py +433 -0
- beeverforge/lifecycle/reading.py +100 -0
- beeverforge/lifecycle/record.py +211 -0
- beeverforge/lifecycle/table.py +673 -0
- beeverforge/listeners.py +245 -0
- beeverforge/loader.py +173 -0
- beeverforge/memory/__init__.py +29 -0
- beeverforge/memory/ledger.py +237 -0
- beeverforge/memory/messages.py +500 -0
- beeverforge/memory/recall.py +414 -0
- beeverforge/memory/store.py +157 -0
- beeverforge/memory/transcript.py +697 -0
- beeverforge/namespace.py +278 -0
- beeverforge/observability/__init__.py +23 -0
- beeverforge/observability/autonomy.py +233 -0
- beeverforge/observability/events.py +195 -0
- beeverforge/observability/health.py +533 -0
- beeverforge/observability/job_record.py +141 -0
- beeverforge/observability/logs.py +368 -0
- beeverforge/observability/metrics.py +276 -0
- beeverforge/observability/query.py +565 -0
- beeverforge/observability/registry.py +260 -0
- beeverforge/observability/sqlite_metrics.py +333 -0
- beeverforge/observability/trajectory.py +243 -0
- beeverforge/onboarding/__init__.py +64 -0
- beeverforge/onboarding/concepts.py +450 -0
- beeverforge/onboarding/context.py +2428 -0
- beeverforge/onboarding/cover.py +97 -0
- beeverforge/onboarding/deployment.py +1040 -0
- beeverforge/onboarding/firstrun.py +1660 -0
- beeverforge/onboarding/history.py +363 -0
- beeverforge/onboarding/infer.py +2779 -0
- beeverforge/onboarding/live_ci.py +162 -0
- beeverforge/onboarding/onboard.py +1101 -0
- beeverforge/onboarding/preview_infer.py +1518 -0
- beeverforge/onboarding/preview_product.py +354 -0
- beeverforge/onboarding/preview_propose.py +1210 -0
- beeverforge/onboarding/propose_manifest.py +411 -0
- beeverforge/onboarding/questions.py +137 -0
- beeverforge/onboarding/readiness.py +1147 -0
- beeverforge/onboarding/renew.py +239 -0
- beeverforge/onboarding/spend.py +76 -0
- beeverforge/onboarding/starter.py +75 -0
- beeverforge/ops/__init__.py +6 -0
- beeverforge/ops/impediment.py +420 -0
- beeverforge/orchestrator/__init__.py +26 -0
- beeverforge/orchestrator/context.py +791 -0
- beeverforge/orchestrator/errors.py +27 -0
- beeverforge/orchestrator/explain.py +430 -0
- beeverforge/orchestrator/machine.py +4177 -0
- beeverforge/orchestrator/merge_policy.py +244 -0
- beeverforge/orchestrator/operator_guidelines.py +406 -0
- beeverforge/orchestrator/outcomes.py +39 -0
- beeverforge/orchestrator/promotion.py +369 -0
- beeverforge/orchestrator/read_only_pass.py +219 -0
- beeverforge/orchestrator/risk.py +191 -0
- beeverforge/orchestrator/validation.py +172 -0
- beeverforge/org_defaults/cards/defect-template.md +23 -0
- beeverforge/org_defaults/cards/defect-template.pt-BR.md +23 -0
- beeverforge/org_defaults/cards/rubric.yaml +140 -0
- beeverforge/org_defaults/cards/template.md +23 -0
- beeverforge/org_defaults/cards/template.pt-BR.md +23 -0
- beeverforge/org_defaults/engineering.md +92 -0
- beeverforge/org_defaults/floor.yaml +192 -0
- beeverforge/org_defaults/profiles/prototype.yaml +16 -0
- beeverforge/org_defaults/profiles/regulated.yaml +35 -0
- beeverforge/org_defaults/requirements/0000-template.md +39 -0
- beeverforge/org_defaults/roles/coordinator.md +48 -0
- beeverforge/org_defaults/roles/executor.md +100 -0
- beeverforge/org_defaults/roles/planner.md +117 -0
- beeverforge/org_defaults/roles/product.md +114 -0
- beeverforge/org_defaults/roles/recovery.md +35 -0
- beeverforge/org_defaults/roles/sizer.md +62 -0
- beeverforge/org_defaults/roles/techlead.md +61 -0
- beeverforge/org_defaults/tdd.md +10 -0
- beeverforge/own_work.py +41 -0
- beeverforge/paths.py +144 -0
- beeverforge/plugins.py +307 -0
- beeverforge/policy/__init__.py +10 -0
- beeverforge/policy/authz.py +105 -0
- beeverforge/policy/census.py +161 -0
- beeverforge/policy/conformance.py +300 -0
- beeverforge/policy/context_writes.py +123 -0
- beeverforge/policy/floor.py +42 -0
- beeverforge/policy/presets.py +174 -0
- beeverforge/policy/profiles.py +286 -0
- beeverforge/policy/protected.py +175 -0
- beeverforge/policy/test_work.py +70 -0
- beeverforge/preflight.py +772 -0
- beeverforge/presets/node.yaml +6 -0
- beeverforge/presets/python.yaml +9 -0
- beeverforge/presets/security-oss.yaml +26 -0
- beeverforge/presets/terraform.yaml +8 -0
- beeverforge/preview/__init__.py +600 -0
- beeverforge/preview/admit.py +599 -0
- beeverforge/preview/assemble.py +468 -0
- beeverforge/preview/demand.py +463 -0
- beeverforge/preview/live.py +185 -0
- beeverforge/preview/own.py +163 -0
- beeverforge/preview/plan.py +205 -0
- beeverforge/preview/product.py +219 -0
- beeverforge/preview/read.py +379 -0
- beeverforge/preview/reap.py +182 -0
- beeverforge/preview/siblings.py +140 -0
- beeverforge/preview/steps.py +475 -0
- beeverforge/preview/unit.py +56 -0
- beeverforge/product/__init__.py +56 -0
- beeverforge/product/accept.py +276 -0
- beeverforge/product/addressing.py +102 -0
- beeverforge/product/adjust.py +449 -0
- beeverforge/product/agenda.py +329 -0
- beeverforge/product/asked.py +270 -0
- beeverforge/product/attachments.py +433 -0
- beeverforge/product/authoring.py +2727 -0
- beeverforge/product/before_code.py +482 -0
- beeverforge/product/board.py +634 -0
- beeverforge/product/breaking.py +174 -0
- beeverforge/product/briefing.py +545 -0
- beeverforge/product/brownfield.py +240 -0
- beeverforge/product/cap.py +141 -0
- beeverforge/product/capabilities.py +438 -0
- beeverforge/product/cards.py +1187 -0
- beeverforge/product/case.py +545 -0
- beeverforge/product/chain.py +389 -0
- beeverforge/product/channel.py +215 -0
- beeverforge/product/clock.py +102 -0
- beeverforge/product/config.py +422 -0
- beeverforge/product/confirm.py +1394 -0
- beeverforge/product/conversation.py +133 -0
- beeverforge/product/corpus.py +624 -0
- beeverforge/product/distil.py +597 -0
- beeverforge/product/documents/__init__.py +19 -0
- beeverforge/product/documents/ingest.py +702 -0
- beeverforge/product/documents/reading.py +125 -0
- beeverforge/product/documents/record.py +263 -0
- beeverforge/product/documents/store.py +146 -0
- beeverforge/product/domain.py +283 -0
- beeverforge/product/door.py +686 -0
- beeverforge/product/engine.py +2686 -0
- beeverforge/product/evaluation/__init__.py +26 -0
- beeverforge/product/evaluation/__main__.py +80 -0
- beeverforge/product/evaluation/battery.py +252 -0
- beeverforge/product/evaluation/run.py +428 -0
- beeverforge/product/evaluation/score.py +326 -0
- beeverforge/product/events.py +968 -0
- beeverforge/product/facts.py +371 -0
- beeverforge/product/followup.py +761 -0
- beeverforge/product/forget.py +718 -0
- beeverforge/product/importing.py +1508 -0
- beeverforge/product/index/__init__.py +22 -0
- beeverforge/product/index/items.py +343 -0
- beeverforge/product/index/retrieval.py +577 -0
- beeverforge/product/index/search.py +538 -0
- beeverforge/product/index/store.py +298 -0
- beeverforge/product/index/sync.py +272 -0
- beeverforge/product/intents.py +180 -0
- beeverforge/product/key.py +43 -0
- beeverforge/product/loader.py +302 -0
- beeverforge/product/model.py +1591 -0
- beeverforge/product/module.py +6800 -0
- beeverforge/product/needs_action.py +209 -0
- beeverforge/product/onboard.py +402 -0
- beeverforge/product/page.py +177 -0
- beeverforge/product/progress.py +73 -0
- beeverforge/product/queue.py +343 -0
- beeverforge/product/reading.py +183 -0
- beeverforge/product/refresh.py +402 -0
- beeverforge/product/release.py +268 -0
- beeverforge/product/requester.py +55 -0
- beeverforge/product/role.py +2386 -0
- beeverforge/product/semaphore.py +497 -0
- beeverforge/product/sessions.py +171 -0
- beeverforge/product/sight.py +359 -0
- beeverforge/product/sources.py +351 -0
- beeverforge/product/speaker.py +166 -0
- beeverforge/product/staging.py +992 -0
- beeverforge/product/template.py +252 -0
- beeverforge/product/triage.py +338 -0
- beeverforge/product/unfiled.py +77 -0
- beeverforge/product/voice.py +5597 -0
- beeverforge/product/waiting.py +113 -0
- beeverforge/product/workspace.py +536 -0
- beeverforge/registry.py +891 -0
- beeverforge/review/__init__.py +11 -0
- beeverforge/review/evidence.py +45 -0
- beeverforge/review/threads.py +84 -0
- beeverforge/review/verdict.py +335 -0
- beeverforge/runtime/__init__.py +0 -0
- beeverforge/runtime/boxed_job.py +475 -0
- beeverforge/runtime/card_repo.py +163 -0
- beeverforge/runtime/host.py +398 -0
- beeverforge/runtime/repairable.py +232 -0
- beeverforge/runtime/repo_cache.py +911 -0
- beeverforge/runtime/temporal/__init__.py +26 -0
- beeverforge/runtime/temporal/activities.py +7015 -0
- beeverforge/runtime/temporal/connection.py +553 -0
- beeverforge/runtime/temporal/conversation.py +617 -0
- beeverforge/runtime/temporal/deadlines.py +86 -0
- beeverforge/runtime/temporal/interceptors.py +81 -0
- beeverforge/runtime/temporal/io.py +1042 -0
- beeverforge/runtime/temporal/poller.py +185 -0
- beeverforge/runtime/temporal/schedule.py +543 -0
- beeverforge/runtime/temporal/standing.py +141 -0
- beeverforge/runtime/temporal/starter.py +55 -0
- beeverforge/runtime/temporal/view.py +1708 -0
- beeverforge/runtime/temporal/vocabulary.py +95 -0
- beeverforge/runtime/temporal/worker.py +446 -0
- beeverforge/runtime/temporal/workflow.py +3550 -0
- beeverforge/runtime/toolbox.py +163 -0
- beeverforge/scheduler.py +75 -0
- beeverforge/semver.py +35 -0
- beeverforge/techlead/__init__.py +42 -0
- beeverforge/techlead/classify.py +514 -0
- beeverforge/techlead/conversation.py +1437 -0
- beeverforge/techlead/diagnosis.py +251 -0
- beeverforge/techlead/memory.py +292 -0
- beeverforge/techlead/pack.py +169 -0
- beeverforge/techlead/voice.py +892 -0
- beeverforge/techlead/watch.py +647 -0
- beeverforge/testing/__init__.py +1 -0
- beeverforge/testing/local_flow.py +287 -0
- beeverforge/util/__init__.py +0 -0
- beeverforge/util/bounded.py +77 -0
- beeverforge/util/causes.py +125 -0
- beeverforge/util/filelock.py +176 -0
- beeverforge/util/scratch.py +80 -0
- beeverforge/util/single_flight.py +107 -0
- beeverforge/util/yaml_io.py +252 -0
- beeverforge-0.1.0.dist-info/METADATA +187 -0
- beeverforge-0.1.0.dist-info/RECORD +433 -0
- beeverforge-0.1.0.dist-info/WHEEL +5 -0
- beeverforge-0.1.0.dist-info/entry_points.txt +2 -0
- beeverforge-0.1.0.dist-info/licenses/LICENSE +202 -0
- beeverforge-0.1.0.dist-info/licenses/NOTICE +22 -0
- 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
|