openlanes 3.0.0
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.
- checksums.yaml +7 -0
- data/agents/hansolo-reviewer.md +59 -0
- data/agents/lando-broker.md +70 -0
- data/agents/leah-researcher.md +60 -0
- data/agents/luke-backend.md +73 -0
- data/agents/palpatine-planner.md +61 -0
- data/agents/r2d2-mechanic.md +57 -0
- data/agents/rey-frontend.md +68 -0
- data/agents/yoda-writer.md +67 -0
- data/bin/factory-e2e +157 -0
- data/bin/plan-number +164 -0
- data/bin/setup +26 -0
- data/evals/cases/hansolo-reviewer/review-medium/fail/plan/plan.md +3 -0
- data/evals/cases/hansolo-reviewer/review-medium/fail/plan/pull-requests.md +12 -0
- data/evals/cases/hansolo-reviewer/review-medium/fail/plan/spec.md +5 -0
- data/evals/cases/hansolo-reviewer/review-medium/fail/repo/lib/cart.rb +9 -0
- data/evals/cases/hansolo-reviewer/review-medium/fail/repo/spec/cart_spec.rb +7 -0
- data/evals/cases/hansolo-reviewer/review-medium/fail/run.json +1 -0
- data/evals/cases/hansolo-reviewer/review-medium/pass/plan/plan.md +3 -0
- data/evals/cases/hansolo-reviewer/review-medium/pass/plan/pull-requests.md +15 -0
- data/evals/cases/hansolo-reviewer/review-medium/pass/plan/spec.md +5 -0
- data/evals/cases/hansolo-reviewer/review-medium/pass/repo/lib/cart.rb +9 -0
- data/evals/cases/hansolo-reviewer/review-medium/pass/repo/spec/cart_spec.rb +7 -0
- data/evals/cases/hansolo-reviewer/review-medium/pass/run.json +1 -0
- data/evals/cases/hansolo-reviewer/review-medium.yml +60 -0
- data/evals/cases/lando-broker/unblock-fast/fail/plan/blocked.md +9 -0
- data/evals/cases/lando-broker/unblock-fast/fail/plan/spec.md +16 -0
- data/evals/cases/lando-broker/unblock-fast/fail/run.json +1 -0
- data/evals/cases/lando-broker/unblock-fast/pass/plan/spec.md +18 -0
- data/evals/cases/lando-broker/unblock-fast/pass/run.json +1 -0
- data/evals/cases/lando-broker/unblock-fast.yml +44 -0
- data/evals/cases/leah-researcher/research-deep/fail/plan/spec.md +26 -0
- data/evals/cases/leah-researcher/research-deep/fail/run.json +1 -0
- data/evals/cases/leah-researcher/research-deep/pass/plan/spec.md +39 -0
- data/evals/cases/leah-researcher/research-deep/pass/run.json +1 -0
- data/evals/cases/leah-researcher/research-deep.yml +51 -0
- data/evals/cases/leah-researcher/research-fast/fail/plan/spec.md +27 -0
- data/evals/cases/leah-researcher/research-fast/fail/run.json +1 -0
- data/evals/cases/leah-researcher/research-fast/pass/plan/spec.md +23 -0
- data/evals/cases/leah-researcher/research-fast/pass/run.json +1 -0
- data/evals/cases/leah-researcher/research-fast.yml +55 -0
- data/evals/cases/luke-backend/build-medium/fail/plan/plan-backend.md +7 -0
- data/evals/cases/luke-backend/build-medium/fail/plan/plan-frontend.md +1 -0
- data/evals/cases/luke-backend/build-medium/fail/plan/plan.md +3 -0
- data/evals/cases/luke-backend/build-medium/fail/plan/pull-requests.md +8 -0
- data/evals/cases/luke-backend/build-medium/fail/plan/spec.md +8 -0
- data/evals/cases/luke-backend/build-medium/fail/repo/Gemfile +2 -0
- data/evals/cases/luke-backend/build-medium/fail/repo/lib/cart.rb +11 -0
- data/evals/cases/luke-backend/build-medium/fail/repo/spec/cart_spec.rb +7 -0
- data/evals/cases/luke-backend/build-medium/fail/run.json +1 -0
- data/evals/cases/luke-backend/build-medium/pass/plan/plan-backend.md +7 -0
- data/evals/cases/luke-backend/build-medium/pass/plan/plan-frontend.md +1 -0
- data/evals/cases/luke-backend/build-medium/pass/plan/plan.md +3 -0
- data/evals/cases/luke-backend/build-medium/pass/plan/pull-requests.md +8 -0
- data/evals/cases/luke-backend/build-medium/pass/plan/spec.md +8 -0
- data/evals/cases/luke-backend/build-medium/pass/repo/Gemfile +2 -0
- data/evals/cases/luke-backend/build-medium/pass/repo/lib/cart.rb +11 -0
- data/evals/cases/luke-backend/build-medium/pass/repo/spec/cart_spec.rb +11 -0
- data/evals/cases/luke-backend/build-medium/pass/run.json +1 -0
- data/evals/cases/luke-backend/build-medium.yml +68 -0
- data/evals/cases/palpatine-planner/plan-deep/fail/plan/plan-backend.md +6 -0
- data/evals/cases/palpatine-planner/plan-deep/fail/plan/plan.md +8 -0
- data/evals/cases/palpatine-planner/plan-deep/fail/plan/spec.md +14 -0
- data/evals/cases/palpatine-planner/plan-deep/fail/run.json +1 -0
- data/evals/cases/palpatine-planner/plan-deep/pass/plan/plan-backend.md +32 -0
- data/evals/cases/palpatine-planner/plan-deep/pass/plan/plan-frontend.md +1 -0
- data/evals/cases/palpatine-planner/plan-deep/pass/plan/plan.md +28 -0
- data/evals/cases/palpatine-planner/plan-deep/pass/plan/spec.md +14 -0
- data/evals/cases/palpatine-planner/plan-deep/pass/run.json +1 -0
- data/evals/cases/palpatine-planner/plan-deep.yml +50 -0
- data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan-backend.md +48 -0
- data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan-frontend.md +1 -0
- data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan.md +12 -0
- data/evals/cases/palpatine-planner/plan-fast/fail/plan/spec.md +12 -0
- data/evals/cases/palpatine-planner/plan-fast/fail/run.json +1 -0
- data/evals/cases/palpatine-planner/plan-fast/pass/plan/plan-backend.md +8 -0
- data/evals/cases/palpatine-planner/plan-fast/pass/plan/plan-frontend.md +1 -0
- data/evals/cases/palpatine-planner/plan-fast/pass/plan/plan.md +12 -0
- data/evals/cases/palpatine-planner/plan-fast/pass/plan/spec.md +12 -0
- data/evals/cases/palpatine-planner/plan-fast/pass/run.json +1 -0
- data/evals/cases/palpatine-planner/plan-fast.yml +47 -0
- data/evals/cases/r2d2-mechanic/task-fast/fail/plan/plan.md +7 -0
- data/evals/cases/r2d2-mechanic/task-fast/fail/plan/pull-requests.md +6 -0
- data/evals/cases/r2d2-mechanic/task-fast/fail/plan/spec.md +6 -0
- data/evals/cases/r2d2-mechanic/task-fast/fail/repo/README.md +3 -0
- data/evals/cases/r2d2-mechanic/task-fast/fail/repo/lib/greeter/formatter.rb +12 -0
- data/evals/cases/r2d2-mechanic/task-fast/fail/repo/lib/greeter.rb +15 -0
- data/evals/cases/r2d2-mechanic/task-fast/fail/repo/spec/greeter_spec.rb +7 -0
- data/evals/cases/r2d2-mechanic/task-fast/fail/run.json +1 -0
- data/evals/cases/r2d2-mechanic/task-fast/pass/plan/plan.md +7 -0
- data/evals/cases/r2d2-mechanic/task-fast/pass/plan/pull-requests.md +6 -0
- data/evals/cases/r2d2-mechanic/task-fast/pass/plan/spec.md +6 -0
- data/evals/cases/r2d2-mechanic/task-fast/pass/repo/README.md +3 -0
- data/evals/cases/r2d2-mechanic/task-fast/pass/repo/lib/greeter.rb +3 -0
- data/evals/cases/r2d2-mechanic/task-fast/pass/repo/spec/greeter_spec.rb +7 -0
- data/evals/cases/r2d2-mechanic/task-fast/pass/run.json +1 -0
- data/evals/cases/r2d2-mechanic/task-fast.yml +59 -0
- data/evals/cases/r2d2-mechanic/task-medium/fail/plan/plan.md +7 -0
- data/evals/cases/r2d2-mechanic/task-medium/fail/plan/pull-requests.md +6 -0
- data/evals/cases/r2d2-mechanic/task-medium/fail/plan/spec.md +6 -0
- data/evals/cases/r2d2-mechanic/task-medium/fail/repo/Gemfile +2 -0
- data/evals/cases/r2d2-mechanic/task-medium/fail/repo/lib/greeter.rb +6 -0
- data/evals/cases/r2d2-mechanic/task-medium/fail/repo/spec/greeter_spec.rb +11 -0
- data/evals/cases/r2d2-mechanic/task-medium/fail/run.json +1 -0
- data/evals/cases/r2d2-mechanic/task-medium/pass/plan/plan.md +7 -0
- data/evals/cases/r2d2-mechanic/task-medium/pass/plan/pull-requests.md +6 -0
- data/evals/cases/r2d2-mechanic/task-medium/pass/plan/spec.md +6 -0
- data/evals/cases/r2d2-mechanic/task-medium/pass/repo/Gemfile +2 -0
- data/evals/cases/r2d2-mechanic/task-medium/pass/repo/lib/greeter.rb +6 -0
- data/evals/cases/r2d2-mechanic/task-medium/pass/repo/spec/greeter_spec.rb +11 -0
- data/evals/cases/r2d2-mechanic/task-medium/pass/run.json +1 -0
- data/evals/cases/r2d2-mechanic/task-medium.yml +58 -0
- data/evals/cases/rey-frontend/ui-medium/fail/plan/plan-frontend.md +7 -0
- data/evals/cases/rey-frontend/ui-medium/fail/plan/plan.md +4 -0
- data/evals/cases/rey-frontend/ui-medium/fail/plan/pull-requests.md +8 -0
- data/evals/cases/rey-frontend/ui-medium/fail/plan/spec.md +5 -0
- data/evals/cases/rey-frontend/ui-medium/fail/repo/lib/cart.rb +5 -0
- data/evals/cases/rey-frontend/ui-medium/fail/repo/web/cart_badge.js +4 -0
- data/evals/cases/rey-frontend/ui-medium/fail/repo/web/cart_badge.test.js +3 -0
- data/evals/cases/rey-frontend/ui-medium/fail/run.json +1 -0
- data/evals/cases/rey-frontend/ui-medium/pass/plan/plan-frontend.md +7 -0
- data/evals/cases/rey-frontend/ui-medium/pass/plan/plan.md +4 -0
- data/evals/cases/rey-frontend/ui-medium/pass/plan/pull-requests.md +8 -0
- data/evals/cases/rey-frontend/ui-medium/pass/plan/spec.md +5 -0
- data/evals/cases/rey-frontend/ui-medium/pass/repo/lib/cart.rb +3 -0
- data/evals/cases/rey-frontend/ui-medium/pass/repo/web/cart_badge.js +4 -0
- data/evals/cases/rey-frontend/ui-medium/pass/repo/web/cart_badge.test.js +3 -0
- data/evals/cases/rey-frontend/ui-medium/pass/run.json +1 -0
- data/evals/cases/rey-frontend/ui-medium.yml +55 -0
- data/evals/cases/yoda-writer/spec-medium/fail/plan/plan.md +5 -0
- data/evals/cases/yoda-writer/spec-medium/fail/plan/spec.md +45 -0
- data/evals/cases/yoda-writer/spec-medium/fail/run.json +1 -0
- data/evals/cases/yoda-writer/spec-medium/pass/plan/plan.md +0 -0
- data/evals/cases/yoda-writer/spec-medium/pass/plan/spec.md +63 -0
- data/evals/cases/yoda-writer/spec-medium/pass/run.json +1 -0
- data/evals/cases/yoda-writer/spec-medium.yml +40 -0
- data/exe/depot +1 -0
- data/exe/lanes +1 -0
- data/exe/openlanes +32 -0
- data/lib/openlanes/adapters/base.rb +69 -0
- data/lib/openlanes/adapters/claude.rb +124 -0
- data/lib/openlanes/adapters/codex.rb +33 -0
- data/lib/openlanes/adapters/model.rb +37 -0
- data/lib/openlanes/adapters/pi.rb +22 -0
- data/lib/openlanes/adapters/stream.rb +196 -0
- data/lib/openlanes/adapters.rb +92 -0
- data/lib/openlanes/agents/agent.rb +103 -0
- data/lib/openlanes/agents/profile.rb +57 -0
- data/lib/openlanes/agents/registry.rb +151 -0
- data/lib/openlanes/agents/roster.rb +103 -0
- data/lib/openlanes/agents/routing.rb +88 -0
- data/lib/openlanes/agents.rb +20 -0
- data/lib/openlanes/cli/agents/subcommands/describe.rb +66 -0
- data/lib/openlanes/cli/agents/subcommands/list.rb +20 -0
- data/lib/openlanes/cli/base.rb +263 -0
- data/lib/openlanes/cli/create/create.rb +354 -0
- data/lib/openlanes/cli/docs/docs.rb +40 -0
- data/lib/openlanes/cli/eval/eval.rb +110 -0
- data/lib/openlanes/cli/index/index.rb +40 -0
- data/lib/openlanes/cli/init/init.rb +145 -0
- data/lib/openlanes/cli/linear/linear.rb +36 -0
- data/lib/openlanes/cli/linear/subcommands/import.rb +174 -0
- data/lib/openlanes/cli/linear/subcommands/projects.rb +55 -0
- data/lib/openlanes/cli/list_plans/list_plans.rb +21 -0
- data/lib/openlanes/cli/mail/mail.rb +31 -0
- data/lib/openlanes/cli/mail/subcommands/read.rb +43 -0
- data/lib/openlanes/cli/mail/subcommands/send.rb +43 -0
- data/lib/openlanes/cli/resync/subcommands/dirs.rb +48 -0
- data/lib/openlanes/cli/resync/subcommands/prs.rb +111 -0
- data/lib/openlanes/cli/run/run.rb +544 -0
- data/lib/openlanes/cli/skills/subcommands/install.rb +92 -0
- data/lib/openlanes/cli/start/start.rb +110 -0
- data/lib/openlanes/cli/state/state.rb +26 -0
- data/lib/openlanes/cli/state/subcommands/show.rb +30 -0
- data/lib/openlanes/cli/state/subcommands/sign.rb +48 -0
- data/lib/openlanes/cli/states/states.rb +15 -0
- data/lib/openlanes/cli/unblock/unblock.rb +235 -0
- data/lib/openlanes/cli/version/version.rb +13 -0
- data/lib/openlanes/cli/web/web.rb +100 -0
- data/lib/openlanes/cli/worktree/worktree.rb +85 -0
- data/lib/openlanes/cli.rb +162 -0
- data/lib/openlanes/engine/board.rb +104 -0
- data/lib/openlanes/engine/dispatcher.rb +1297 -0
- data/lib/openlanes/engine/progress_log.rb +125 -0
- data/lib/openlanes/engine/runner.rb +224 -0
- data/lib/openlanes/engine/state_file.rb +190 -0
- data/lib/openlanes/engine/tally.rb +177 -0
- data/lib/openlanes/engine.rb +19 -0
- data/lib/openlanes/evals/case.rb +191 -0
- data/lib/openlanes/evals/changes.rb +67 -0
- data/lib/openlanes/evals/check.rb +224 -0
- data/lib/openlanes/evals/evidence.rb +71 -0
- data/lib/openlanes/evals/live.rb +140 -0
- data/lib/openlanes/evals/recording.rb +38 -0
- data/lib/openlanes/evals/run.rb +34 -0
- data/lib/openlanes/evals/score.rb +24 -0
- data/lib/openlanes/evals/scorer.rb +42 -0
- data/lib/openlanes/evals/verdict.rb +18 -0
- data/lib/openlanes/evals.rb +56 -0
- data/lib/openlanes/execution/checks.rb +73 -0
- data/lib/openlanes/execution/child.rb +228 -0
- data/lib/openlanes/execution/clock.rb +121 -0
- data/lib/openlanes/execution/control.rb +194 -0
- data/lib/openlanes/execution/executor.rb +849 -0
- data/lib/openlanes/execution/prompt.rb +455 -0
- data/lib/openlanes/execution/transcript.rb +531 -0
- data/lib/openlanes/execution.rb +19 -0
- data/lib/openlanes/launcher.rb +69 -0
- data/lib/openlanes/lifecycle/adoption.rb +197 -0
- data/lib/openlanes/lifecycle/brief.rb +288 -0
- data/lib/openlanes/lifecycle/completion.rb +58 -0
- data/lib/openlanes/lifecycle/creator.rb +126 -0
- data/lib/openlanes/lifecycle/lanes.rb +74 -0
- data/lib/openlanes/lifecycle/resync.rb +428 -0
- data/lib/openlanes/lifecycle/unblocker.rb +236 -0
- data/lib/openlanes/lifecycle.rb +28 -0
- data/lib/openlanes/linear/api.rb +224 -0
- data/lib/openlanes/linear/attribution.rb +185 -0
- data/lib/openlanes/linear/fuzzy.rb +68 -0
- data/lib/openlanes/linear/import.rb +314 -0
- data/lib/openlanes/linear/issue.rb +187 -0
- data/lib/openlanes/linear/mapping.rb +116 -0
- data/lib/openlanes/linear/push.rb +197 -0
- data/lib/openlanes/linear/survey.rb +174 -0
- data/lib/openlanes/linear/unit.rb +274 -0
- data/lib/openlanes/linear.rb +41 -0
- data/lib/openlanes/live/claims.rb +71 -0
- data/lib/openlanes/live/command.rb +122 -0
- data/lib/openlanes/live/commands.rb +505 -0
- data/lib/openlanes/live/keys.rb +93 -0
- data/lib/openlanes/live/memory_store.rb +265 -0
- data/lib/openlanes/live/meter.rb +51 -0
- data/lib/openlanes/live/node.rb +88 -0
- data/lib/openlanes/live/publisher.rb +179 -0
- data/lib/openlanes/live/repo.rb +60 -0
- data/lib/openlanes/live/run_record.rb +133 -0
- data/lib/openlanes/live/run_state.rb +101 -0
- data/lib/openlanes/live/session.rb +107 -0
- data/lib/openlanes/live/snapshot.rb +322 -0
- data/lib/openlanes/live/stations.rb +67 -0
- data/lib/openlanes/live/store.rb +277 -0
- data/lib/openlanes/live.rb +109 -0
- data/lib/openlanes/plans/dev_work.rb +56 -0
- data/lib/openlanes/plans/feature.rb +131 -0
- data/lib/openlanes/plans/index.rb +208 -0
- data/lib/openlanes/plans/ledger.rb +255 -0
- data/lib/openlanes/plans/mailbox.rb +164 -0
- data/lib/openlanes/plans/ordinal.rb +92 -0
- data/lib/openlanes/plans/plan_state.rb +291 -0
- data/lib/openlanes/plans/pull_request.rb +29 -0
- data/lib/openlanes/plans/pull_requests.rb +229 -0
- data/lib/openlanes/plans/spec.rb +213 -0
- data/lib/openlanes/plans/state_machine.rb +433 -0
- data/lib/openlanes/plans/status.rb +365 -0
- data/lib/openlanes/plans/subject.rb +133 -0
- data/lib/openlanes/plans/tree.rb +79 -0
- data/lib/openlanes/plans.rb +39 -0
- data/lib/openlanes/presentation/diagram.rb +149 -0
- data/lib/openlanes/presentation/documentation.rb +438 -0
- data/lib/openlanes/presentation/reporter.rb +181 -0
- data/lib/openlanes/presentation/viewer.rb +62 -0
- data/lib/openlanes/presentation.rb +23 -0
- data/lib/openlanes/skills/installer.rb +68 -0
- data/lib/openlanes/skills/skill.rb +60 -0
- data/lib/openlanes/skills.rb +25 -0
- data/lib/openlanes/support/config.rb +217 -0
- data/lib/openlanes/support/frontmatter.rb +38 -0
- data/lib/openlanes/support/markdown.rb +56 -0
- data/lib/openlanes/support/project.rb +79 -0
- data/lib/openlanes/support/ui.rb +671 -0
- data/lib/openlanes/vcs/github.rb +171 -0
- data/lib/openlanes/vcs/publisher.rb +221 -0
- data/lib/openlanes/vcs/worktree.rb +248 -0
- data/lib/openlanes/vcs.rb +19 -0
- data/lib/openlanes/version.rb +10 -0
- data/lib/openlanes/web/app.rb +176 -0
- data/lib/openlanes/web/context.rb +66 -0
- data/lib/openlanes/web/demo.rb +169 -0
- data/lib/openlanes/web/demo_board.rb +286 -0
- data/lib/openlanes/web/events.rb +104 -0
- data/lib/openlanes/web/launcher.rb +143 -0
- data/lib/openlanes/web/server.rb +77 -0
- data/lib/openlanes/web/stores.rb +36 -0
- data/lib/openlanes/web.rb +92 -0
- data/lib/openlanes.rb +151 -0
- data/openlanes.gemspec +87 -0
- data/schemas/plan-state.schema.json +101 -0
- data/skills/openlanes/SKILL.md +48 -0
- data/skills/openlanes-documents/SKILL.md +61 -0
- data/skills/openlanes-documents/references/blocked-template.md +49 -0
- data/skills/openlanes-documents/references/closing-templates.md +59 -0
- data/skills/openlanes-documents/references/plan-template.md +61 -0
- data/skills/openlanes-documents/references/retroactive-spec.md +43 -0
- data/skills/openlanes-documents/references/spec-template.md +45 -0
- data/skills/openlanes-run/SKILL.md +90 -0
- data/skills/openlanes-setup/SKILL.md +64 -0
- data/skills/openlanes-spec/SKILL.md +90 -0
- data/skills/openlanes-status/SKILL.md +32 -0
- data/skills/openlanes-sync/SKILL.md +22 -0
- data/web/dist/assets/apl-B4CMkyY2.js +2 -0
- data/web/dist/assets/asciiarmor-Df11BRmG.js +2 -0
- data/web/dist/assets/asn1-EdZsLKOL.js +2 -0
- data/web/dist/assets/asterisk-B-8jnY81.js +2 -0
- data/web/dist/assets/barlow-condensed-latin-600-normal-BFJEwTuo.woff +0 -0
- data/web/dist/assets/barlow-condensed-latin-600-normal-DepVgxBB.woff2 +0 -0
- data/web/dist/assets/barlow-condensed-latin-ext-600-normal-18ESti3H.woff2 +0 -0
- data/web/dist/assets/barlow-condensed-latin-ext-600-normal-Clv9cIcR.woff +0 -0
- data/web/dist/assets/barlow-condensed-vietnamese-600-normal-A5AYRdjN.woff2 +0 -0
- data/web/dist/assets/barlow-condensed-vietnamese-600-normal-CNlPk46_.woff +0 -0
- data/web/dist/assets/brainfuck-C4LP7Hcl.js +2 -0
- data/web/dist/assets/clike-B9uivgTg.js +2 -0
- data/web/dist/assets/clojure-BMjYHr_A.js +2 -0
- data/web/dist/assets/cmake-BQqOBYOt.js +2 -0
- data/web/dist/assets/cobol-CWcv1MsR.js +2 -0
- data/web/dist/assets/coffeescript-S37ZYGWr.js +2 -0
- data/web/dist/assets/commonlisp-DBKNyK5s.js +2 -0
- data/web/dist/assets/crystal-SjHAIU92.js +2 -0
- data/web/dist/assets/css-BnMrqG3P.js +2 -0
- data/web/dist/assets/cypher-C_CwsFkJ.js +2 -0
- data/web/dist/assets/d-pRatUO7H.js +2 -0
- data/web/dist/assets/diff-DbItnlRl.js +2 -0
- data/web/dist/assets/dockerfile-BKs6k2Af.js +2 -0
- data/web/dist/assets/dtd-DF_7sFjM.js +2 -0
- data/web/dist/assets/dylan-DwRh75JA.js +2 -0
- data/web/dist/assets/ebnf-CDyGwa7X.js +2 -0
- data/web/dist/assets/ecl-Cabwm37j.js +2 -0
- data/web/dist/assets/eiffel-CnydiIhH.js +2 -0
- data/web/dist/assets/elm-vLlmbW-K.js +2 -0
- data/web/dist/assets/erlang-BNw1qcRV.js +2 -0
- data/web/dist/assets/factor-kuTfRLto.js +2 -0
- data/web/dist/assets/favicon-C8pAuqqr.png +0 -0
- data/web/dist/assets/fcl-Kvtd6kyn.js +2 -0
- data/web/dist/assets/forth-Ffai-XNe.js +2 -0
- data/web/dist/assets/fortran-DYz_wnZ1.js +2 -0
- data/web/dist/assets/gas-Bneqetm1.js +2 -0
- data/web/dist/assets/gherkin-heZmZLOM.js +2 -0
- data/web/dist/assets/groovy-D9Dt4D0W.js +2 -0
- data/web/dist/assets/haskell-Cw1EW3IL.js +2 -0
- data/web/dist/assets/haxe-H-WmDvRZ.js +2 -0
- data/web/dist/assets/http-DBlCnlav.js +2 -0
- data/web/dist/assets/idl-BEugSyMb.js +2 -0
- data/web/dist/assets/index-2xPC-eHt.js +2 -0
- data/web/dist/assets/index-8dNhRmiO.js +8 -0
- data/web/dist/assets/index-B7-O9ujQ.js +2 -0
- data/web/dist/assets/index-BdR34cXC.js +2 -0
- data/web/dist/assets/index-Bp7W-ZTq.js +2 -0
- data/web/dist/assets/index-Byyxbh_z.js +4 -0
- data/web/dist/assets/index-C0VmkrO-.js +2 -0
- data/web/dist/assets/index-CI74IkWP.js +198 -0
- data/web/dist/assets/index-CYxcbpRM.js +3 -0
- data/web/dist/assets/index-D0rmJN3h.js +2 -0
- data/web/dist/assets/index-DB4QwuYn.js +2 -0
- data/web/dist/assets/index-DDI7zoAS.js +2 -0
- data/web/dist/assets/index-DGLUuEXi.js +2 -0
- data/web/dist/assets/index-OR-odsl9.js +2 -0
- data/web/dist/assets/index-P3vSkiU-.js +2 -0
- data/web/dist/assets/index-a4EHR-u4.css +1 -0
- data/web/dist/assets/index-mRV4XhJZ.js +2 -0
- data/web/dist/assets/index-popk9wbW.js +2 -0
- data/web/dist/assets/index-uM88Njdj.js +2 -0
- data/web/dist/assets/javascript-iXu5QeM3.js +2 -0
- data/web/dist/assets/julia-DuME0IfC.js +2 -0
- data/web/dist/assets/livescript-BwQOo05w.js +2 -0
- data/web/dist/assets/logo-on-dark-DI9gCdN4.png +0 -0
- data/web/dist/assets/logo-on-light-BciwqxcJ.png +0 -0
- data/web/dist/assets/lua-VAEuO923.js +2 -0
- data/web/dist/assets/mathematica-DTrFuWx2.js +2 -0
- data/web/dist/assets/mbox-CNhZ1qSd.js +2 -0
- data/web/dist/assets/mirc-CjQqDB4T.js +2 -0
- data/web/dist/assets/mllike-CXdrOF99.js +2 -0
- data/web/dist/assets/modelica-Dc1JOy9r.js +2 -0
- data/web/dist/assets/mscgen-BA5vi2Kp.js +2 -0
- data/web/dist/assets/mumps-BT43cFF4.js +2 -0
- data/web/dist/assets/nginx-DdIZxoE0.js +2 -0
- data/web/dist/assets/nsis-LY0VT_u5.js +2 -0
- data/web/dist/assets/ntriples-BfvgReVJ.js +2 -0
- data/web/dist/assets/octave-Ck1zUtKM.js +2 -0
- data/web/dist/assets/overpass-cyrillic-400-normal-CZsFKZCk.woff +0 -0
- data/web/dist/assets/overpass-cyrillic-400-normal-tXDxWGRy.woff2 +0 -0
- data/web/dist/assets/overpass-cyrillic-600-normal-B-hc8bdn.woff2 +0 -0
- data/web/dist/assets/overpass-cyrillic-600-normal-CXB2Mrig.woff +0 -0
- data/web/dist/assets/overpass-cyrillic-800-normal-CP6RWG8X.woff2 +0 -0
- data/web/dist/assets/overpass-cyrillic-800-normal-PJW4A9xW.woff +0 -0
- data/web/dist/assets/overpass-cyrillic-ext-400-normal-AKUz2ZI5.woff2 +0 -0
- data/web/dist/assets/overpass-cyrillic-ext-400-normal-C2ZjKngx.woff +0 -0
- data/web/dist/assets/overpass-cyrillic-ext-600-normal-CW4Fknsm.woff2 +0 -0
- data/web/dist/assets/overpass-cyrillic-ext-600-normal-Zx-mtRSD.woff +0 -0
- data/web/dist/assets/overpass-cyrillic-ext-800-normal-CIth-cA7.woff2 +0 -0
- data/web/dist/assets/overpass-cyrillic-ext-800-normal-hx3cL3ZN.woff +0 -0
- data/web/dist/assets/overpass-latin-400-normal-BpeLJ0bs.woff2 +0 -0
- data/web/dist/assets/overpass-latin-400-normal-c_C8zulu.woff +0 -0
- data/web/dist/assets/overpass-latin-600-normal-25RhTNCi.woff2 +0 -0
- data/web/dist/assets/overpass-latin-600-normal-DhYQrzaY.woff +0 -0
- data/web/dist/assets/overpass-latin-800-normal-Bzx_XP5l.woff2 +0 -0
- data/web/dist/assets/overpass-latin-800-normal-D6-q4f0l.woff +0 -0
- data/web/dist/assets/overpass-latin-ext-400-normal-BIkXRwl6.woff2 +0 -0
- data/web/dist/assets/overpass-latin-ext-400-normal-DUq_h-_z.woff +0 -0
- data/web/dist/assets/overpass-latin-ext-600-normal-Cyj4Nio0.woff2 +0 -0
- data/web/dist/assets/overpass-latin-ext-600-normal-DQKKMGzp.woff +0 -0
- data/web/dist/assets/overpass-latin-ext-800-normal-D6WKWGNs.woff +0 -0
- data/web/dist/assets/overpass-latin-ext-800-normal-DZ60AB77.woff2 +0 -0
- data/web/dist/assets/overpass-mono-cyrillic-400-normal-B144qALP.woff2 +0 -0
- data/web/dist/assets/overpass-mono-cyrillic-400-normal-XK9_jwT8.woff +0 -0
- data/web/dist/assets/overpass-mono-cyrillic-ext-400-normal-Dgejvo4A.woff2 +0 -0
- data/web/dist/assets/overpass-mono-cyrillic-ext-400-normal-DgxwtUZy.woff +0 -0
- data/web/dist/assets/overpass-mono-latin-400-normal-DqAOLzCO.woff +0 -0
- data/web/dist/assets/overpass-mono-latin-400-normal-VINZG6Js.woff2 +0 -0
- data/web/dist/assets/overpass-mono-latin-ext-400-normal-5YIw-E8g.woff +0 -0
- data/web/dist/assets/overpass-mono-latin-ext-400-normal-DBdJzBs_.woff2 +0 -0
- data/web/dist/assets/overpass-mono-vietnamese-400-normal-DrLp_cHj.woff +0 -0
- data/web/dist/assets/overpass-mono-vietnamese-400-normal-jmOeL0rj.woff2 +0 -0
- data/web/dist/assets/overpass-vietnamese-400-normal-BKRCEiXy.woff2 +0 -0
- data/web/dist/assets/overpass-vietnamese-400-normal-CCWAWVG7.woff +0 -0
- data/web/dist/assets/overpass-vietnamese-600-normal-CjS-4vQf.woff2 +0 -0
- data/web/dist/assets/overpass-vietnamese-600-normal-DlMMlPfM.woff +0 -0
- data/web/dist/assets/overpass-vietnamese-800-normal-BB8K9-Vo.woff2 +0 -0
- data/web/dist/assets/overpass-vietnamese-800-normal-DZUygvj_.woff +0 -0
- data/web/dist/assets/oz-BzwKVEFT.js +2 -0
- data/web/dist/assets/pascal--L3eBynH.js +2 -0
- data/web/dist/assets/perl-CdXCOZ3F.js +2 -0
- data/web/dist/assets/pig-CevX1Tat.js +2 -0
- data/web/dist/assets/powershell-CFHJl5sT.js +2 -0
- data/web/dist/assets/properties-C78fOPTZ.js +2 -0
- data/web/dist/assets/protobuf-ChK-085T.js +2 -0
- data/web/dist/assets/pug-DeIclll2.js +2 -0
- data/web/dist/assets/puppet-DMA9R1ak.js +2 -0
- data/web/dist/assets/python-BuPzkPfP.js +2 -0
- data/web/dist/assets/q-pXgVlZs6.js +2 -0
- data/web/dist/assets/r-B6wPVr8A.js +2 -0
- data/web/dist/assets/rpm-CTu-6PCP.js +2 -0
- data/web/dist/assets/ruby-BK5aLJev.js +2 -0
- data/web/dist/assets/sas-B4kiWyti.js +2 -0
- data/web/dist/assets/scheme-C41bIUwD.js +2 -0
- data/web/dist/assets/shell-CjFT_Tl9.js +2 -0
- data/web/dist/assets/sieve-C3Gn_uJK.js +2 -0
- data/web/dist/assets/simple-mode-GW_nhZxv.js +2 -0
- data/web/dist/assets/smalltalk-CnHTOXQT.js +2 -0
- data/web/dist/assets/solr-DehyRSwq.js +2 -0
- data/web/dist/assets/sparql-DkYu6x3z.js +2 -0
- data/web/dist/assets/spreadsheet-BCZA_wO0.js +2 -0
- data/web/dist/assets/sql-D0XecflT.js +2 -0
- data/web/dist/assets/stex-C3f8Ysf7.js +2 -0
- data/web/dist/assets/stylus-B533Al4x.js +2 -0
- data/web/dist/assets/swift-BzpIVaGY.js +2 -0
- data/web/dist/assets/tcl-DVfN8rqt.js +2 -0
- data/web/dist/assets/textile-CnDTJFAw.js +2 -0
- data/web/dist/assets/tiddlywiki-DO-Gjzrf.js +2 -0
- data/web/dist/assets/tiki-DGYXhP31.js +2 -0
- data/web/dist/assets/toml-Bm5Em-hy.js +2 -0
- data/web/dist/assets/troff-wAsdV37c.js +2 -0
- data/web/dist/assets/ttcn-CfJYG6tj.js +2 -0
- data/web/dist/assets/ttcn-cfg-B9xdYoR4.js +2 -0
- data/web/dist/assets/turtle-B1tBg_DP.js +2 -0
- data/web/dist/assets/vb-CmGdzxic.js +2 -0
- data/web/dist/assets/vbscript-BuJXcnF6.js +2 -0
- data/web/dist/assets/velocity-D8B20fx6.js +2 -0
- data/web/dist/assets/verilog-C6RDOZhf.js +2 -0
- data/web/dist/assets/vhdl-lSbBsy5d.js +2 -0
- data/web/dist/assets/webidl-ZXfAyPTL.js +2 -0
- data/web/dist/assets/xquery-DzFWVndE.js +2 -0
- data/web/dist/assets/yacas-BJ4BC0dw.js +2 -0
- data/web/dist/assets/z80-Hz9HOZM7.js +2 -0
- data/web/dist/index.html +14 -0
- data/web/dist/vite.svg +1 -0
- data/web/src/api/fixtures/board.json +512 -0
- metadata +907 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Plans
|
|
5
|
+
# What the author of `spec.md` asked of the harness, in its frontmatter:
|
|
6
|
+
#
|
|
7
|
+
# ---
|
|
8
|
+
# lane: quick # full (default) | plan | quick
|
|
9
|
+
# frontend: false # true forces rey-frontend, false forbids it
|
|
10
|
+
# depth: fast # fast | medium | deep
|
|
11
|
+
# phases:
|
|
12
|
+
# build: { adapter: codex, model: gpt-5-codex, effort: medium }
|
|
13
|
+
# review: { model: opus }
|
|
14
|
+
# implementation_suggestions:
|
|
15
|
+
# - Ruby, with dry-cli for the command line
|
|
16
|
+
# - Reuse the gems already in the Gemfile before adding new ones
|
|
17
|
+
# implementation_requirements:
|
|
18
|
+
# - The database is PG-strict
|
|
19
|
+
# task-completed-when:
|
|
20
|
+
# - `openlanes create` opens an editor and writes no draft
|
|
21
|
+
# how-to-verify:
|
|
22
|
+
# - bundle exec rspec spec/openlanes/cli/create_spec.rb
|
|
23
|
+
# ---
|
|
24
|
+
#
|
|
25
|
+
# The body is the specification; this is how it should be worked. A value
|
|
26
|
+
# this does not recognise is ignored and named in {#problems}, rather than
|
|
27
|
+
# stopping the run: a typo in `lane:` should cost the author their lane,
|
|
28
|
+
# not their plan.
|
|
29
|
+
#
|
|
30
|
+
# @!attribute [r] lane
|
|
31
|
+
# @return [Symbol] :full, :plan or :quick
|
|
32
|
+
# @!attribute [r] frontend
|
|
33
|
+
# @return [Boolean, nil] nil when the plan decides
|
|
34
|
+
# @!attribute [r] depth
|
|
35
|
+
# @return [Symbol, nil] :fast, :medium or :deep
|
|
36
|
+
# @!attribute [r] phases
|
|
37
|
+
# @return [Hash{String => Hash{String => String}}] per-phase
|
|
38
|
+
# `adapter`, `model` and `effort`
|
|
39
|
+
# @!attribute [r] suggestions
|
|
40
|
+
# @return [Array<String>] the author's `implementation_suggestions`:
|
|
41
|
+
# language, libraries, an approach. Offered to the agents, never
|
|
42
|
+
# imposed on them.
|
|
43
|
+
# @!attribute [r] requirements
|
|
44
|
+
# @return [Array<String>] the author's `implementation_requirements`:
|
|
45
|
+
# constraints the work must meet exactly as written.
|
|
46
|
+
# @!attribute [r] completed_when
|
|
47
|
+
# @return [Array<String>] the author's `task-completed-when`: what
|
|
48
|
+
# must be true for the task to count as finished.
|
|
49
|
+
# @!attribute [r] how_to_verify
|
|
50
|
+
# @return [Array<String>] the author's `how-to-verify`: the commands or
|
|
51
|
+
# checks that show it is.
|
|
52
|
+
# @!attribute [r] problems
|
|
53
|
+
# @return [Array<String>] what was ignored, and why
|
|
54
|
+
Spec = Data.define(:lane,
|
|
55
|
+
:frontend,
|
|
56
|
+
:depth,
|
|
57
|
+
:phases,
|
|
58
|
+
:suggestions,
|
|
59
|
+
:requirements,
|
|
60
|
+
:completed_when,
|
|
61
|
+
:how_to_verify,
|
|
62
|
+
:problems) do
|
|
63
|
+
def initialize(lane: :full, frontend: nil, depth: nil, phases: {}, suggestions: [], requirements: [],
|
|
64
|
+
completed_when: [], how_to_verify: [], problems: [])
|
|
65
|
+
super
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @param phase [String, Symbol, nil]
|
|
69
|
+
# @return [Hash{String => String}] what the author set for that phase
|
|
70
|
+
def override_for(phase) = phases.fetch(phase.to_s, {})
|
|
71
|
+
|
|
72
|
+
# @return [Boolean]
|
|
73
|
+
def default? = self == Spec.new
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
class Spec
|
|
77
|
+
# Every route a plan can take through the agents.
|
|
78
|
+
# full — research, specification, planning, build, review
|
|
79
|
+
# plan — the spec is already what the author wants: plan, build, review
|
|
80
|
+
# quick — a short mechanical task: one agent builds it, then review
|
|
81
|
+
LANES = %i[full plan quick].freeze
|
|
82
|
+
|
|
83
|
+
# How much each phase should think, and what an eval holds it to.
|
|
84
|
+
DEPTHS = %i[fast medium deep].freeze
|
|
85
|
+
|
|
86
|
+
# What a depth means as an effort, when nothing more specific is set.
|
|
87
|
+
EFFORT_FOR_DEPTH = { fast: "low", medium: "medium", deep: "high" }.freeze
|
|
88
|
+
|
|
89
|
+
# The keys a `phases:` entry may set.
|
|
90
|
+
PHASE_KEYS = %w[adapter model effort].freeze
|
|
91
|
+
|
|
92
|
+
# The frontmatter key for what the author would build it with. Optional,
|
|
93
|
+
# and a suggestion: an agent that finds a better route takes it and says
|
|
94
|
+
# why, so nothing here is validated against what is installed.
|
|
95
|
+
SUGGESTIONS_KEY = "implementation_suggestions"
|
|
96
|
+
|
|
97
|
+
# The other side of {SUGGESTIONS_KEY}: constraints the agents must meet
|
|
98
|
+
# exactly as written (`The database is PG-strict`). They are not weighed
|
|
99
|
+
# against anything, so an agent that cannot meet one signs Blocked
|
|
100
|
+
# instead of substituting its own.
|
|
101
|
+
REQUIREMENTS_KEY = "implementation_requirements"
|
|
102
|
+
|
|
103
|
+
# How the author says the task is finished, and how to tell. Hyphenated
|
|
104
|
+
# because that is how the author named them; they are copied into
|
|
105
|
+
# `plan.md` by {OpenLanes::Lifecycle::Completion}.
|
|
106
|
+
COMPLETED_WHEN_KEY = "task-completed-when"
|
|
107
|
+
HOW_TO_VERIFY_KEY = "how-to-verify"
|
|
108
|
+
|
|
109
|
+
FILENAME = "spec.md"
|
|
110
|
+
|
|
111
|
+
class << self
|
|
112
|
+
# @param subject [#read]
|
|
113
|
+
# @return [OpenLanes::Plans::Spec]
|
|
114
|
+
def for(subject) = parse(subject.read(FILENAME).to_s)
|
|
115
|
+
|
|
116
|
+
# @param dir [String] a plan folder
|
|
117
|
+
# @return [OpenLanes::Plans::Spec]
|
|
118
|
+
def load(dir)
|
|
119
|
+
path = File.join(dir, FILENAME)
|
|
120
|
+
File.file?(path) ? parse(File.read(path, encoding: "UTF-8")) : new
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# @param text [String] a whole spec.md
|
|
124
|
+
# @return [OpenLanes::Plans::Spec]
|
|
125
|
+
def parse(text)
|
|
126
|
+
meta, = Frontmatter.split(text)
|
|
127
|
+
from(meta)
|
|
128
|
+
rescue Psych::Exception => e
|
|
129
|
+
new(problems: ["spec.md frontmatter is not valid YAML (#{e.message.lines.first.strip})"])
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# @param meta [Hash]
|
|
133
|
+
# @return [OpenLanes::Plans::Spec]
|
|
134
|
+
def from(meta)
|
|
135
|
+
problems = []
|
|
136
|
+
new(lane: choice(meta, "lane", LANES, :full, problems),
|
|
137
|
+
frontend: switch(meta, "frontend", problems),
|
|
138
|
+
depth: choice(meta, "depth", DEPTHS, nil, problems),
|
|
139
|
+
phases: phases(meta["phases"], problems),
|
|
140
|
+
suggestions: notes(meta, SUGGESTIONS_KEY, problems),
|
|
141
|
+
requirements: notes(meta, REQUIREMENTS_KEY, problems),
|
|
142
|
+
completed_when: notes(meta, COMPLETED_WHEN_KEY, problems),
|
|
143
|
+
how_to_verify: notes(meta, HOW_TO_VERIFY_KEY, problems),
|
|
144
|
+
problems:)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private
|
|
148
|
+
|
|
149
|
+
# @return [Symbol, nil]
|
|
150
|
+
def choice(meta, key, allowed, default, problems)
|
|
151
|
+
return default unless meta.key?(key)
|
|
152
|
+
|
|
153
|
+
value = meta[key].to_s.strip.downcase.to_sym
|
|
154
|
+
return value if allowed.include?(value)
|
|
155
|
+
|
|
156
|
+
problems << "#{key}: #{meta[key].inspect} is not one of #{allowed.join(", ")}"
|
|
157
|
+
default
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# @return [Boolean, nil]
|
|
161
|
+
def switch(meta, key, problems)
|
|
162
|
+
return nil unless meta.key?(key)
|
|
163
|
+
return meta[key] if [true, false].include?(meta[key])
|
|
164
|
+
|
|
165
|
+
problems << "#{key}: #{meta[key].inspect} is not true or false"
|
|
166
|
+
nil
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# A bare string is one item; a list is several.
|
|
170
|
+
#
|
|
171
|
+
# @param key [String] one of the keys above that holds free text
|
|
172
|
+
# @return [Array<String>]
|
|
173
|
+
def notes(meta, key, problems)
|
|
174
|
+
return [] if meta[key].nil?
|
|
175
|
+
|
|
176
|
+
items = meta[key].is_a?(Array) ? meta[key] : [meta[key]]
|
|
177
|
+
unless items.all? { |item| item.is_a?(String) || item.is_a?(Numeric) }
|
|
178
|
+
problems << "#{key}: must be text or a list of text"
|
|
179
|
+
return []
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
items.map { |item| item.to_s.strip }.reject(&:empty?)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# @return [Hash]
|
|
186
|
+
def phases(value, problems)
|
|
187
|
+
return {} if value.nil?
|
|
188
|
+
|
|
189
|
+
unless value.is_a?(Hash)
|
|
190
|
+
problems << "phases: must be a mapping of phase to adapter, model and effort"
|
|
191
|
+
return {}
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
value.each_with_object({}) do |(phase, settings), out|
|
|
195
|
+
next problems << "phases.#{phase}: must be a mapping" unless settings.is_a?(Hash)
|
|
196
|
+
|
|
197
|
+
kept = settings.transform_keys(&:to_s).slice(*PHASE_KEYS).transform_values(&:to_s)
|
|
198
|
+
(settings.keys.map(&:to_s) - PHASE_KEYS).each { |k| problems << "phases.#{phase}.#{k}: unknown key" }
|
|
199
|
+
if kept.key?("effort") && !Adapters.effort?(kept["effort"])
|
|
200
|
+
problems << "phases.#{phase}.effort: #{kept["effort"].inspect} is not one of #{Adapters::EFFORTS.join(", ")}"
|
|
201
|
+
kept.delete("effort")
|
|
202
|
+
end
|
|
203
|
+
if kept.key?("adapter") && !Adapters.known?(kept["adapter"])
|
|
204
|
+
problems << "phases.#{phase}.adapter: #{kept["adapter"].inspect} is not one of #{Adapters.names.join(", ")}"
|
|
205
|
+
kept.delete("adapter")
|
|
206
|
+
end
|
|
207
|
+
out[phase.to_s] = kept
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "aasm"
|
|
4
|
+
|
|
5
|
+
module OpenLanes
|
|
6
|
+
module Plans
|
|
7
|
+
# The state machine for one plan folder.
|
|
8
|
+
#
|
|
9
|
+
# The whole topology is the `aasm` block below, and it is nowhere else. To
|
|
10
|
+
# change what may follow what, edit an event's `from:` list. To add a state,
|
|
11
|
+
# add it to {STATUSES} and give it an event. There is no second table to keep
|
|
12
|
+
# in step: {inbound}, {outbound}, {edge?} and `terminal?` are all read back
|
|
13
|
+
# off these declarations.
|
|
14
|
+
#
|
|
15
|
+
# Two rules hold the rest together:
|
|
16
|
+
#
|
|
17
|
+
# 1. **The guard for entering a state is that state's own invariant.** There
|
|
18
|
+
# is one guard method, {#justified?}, and it reads the destination off the
|
|
19
|
+
# transition in flight — so a destination is named once, in `to:`.
|
|
20
|
+
# 2. **The state lives in the folder name.** There is no column and no
|
|
21
|
+
# database. A successful transition renames the directory, which is why
|
|
22
|
+
# firing an event is a real side effect and asking a question is not.
|
|
23
|
+
class StateMachine
|
|
24
|
+
include AASM
|
|
25
|
+
|
|
26
|
+
# Raised when a transition is refused, carrying the reason rather than
|
|
27
|
+
# AASM's "failed callback #17".
|
|
28
|
+
class Refused < Error; end
|
|
29
|
+
|
|
30
|
+
# The forward path a bare {#promote!} walks. Everything off this spine —
|
|
31
|
+
# blocking, deferring, discarding — has to be named explicitly, which is
|
|
32
|
+
# the entire reason for having a machine rather than a rename.
|
|
33
|
+
#
|
|
34
|
+
# 🔴 rejoins at 🎨 rather than continuing: fixing review comments puts the
|
|
35
|
+
# work back through both halves of building, it does not skip the reviewer
|
|
36
|
+
# and it does not assume the half nobody complained about still holds.
|
|
37
|
+
SPINE = {
|
|
38
|
+
retroactive: :planned,
|
|
39
|
+
new: :researched,
|
|
40
|
+
researched: :ready_for_planning,
|
|
41
|
+
ready_for_planning: :planned,
|
|
42
|
+
planned: :building,
|
|
43
|
+
building: :building_ui,
|
|
44
|
+
building_ui: :ready_for_review,
|
|
45
|
+
ready_for_review: :in_review,
|
|
46
|
+
in_review: :approved,
|
|
47
|
+
approved: :deployed,
|
|
48
|
+
rejected: :building_ui,
|
|
49
|
+
rolled_back: :ready_for_review,
|
|
50
|
+
shit: :planned
|
|
51
|
+
}.freeze
|
|
52
|
+
|
|
53
|
+
# One step back along the spine, for the one move that goes backwards: a
|
|
54
|
+
# person discarding an interrupted round (W9) sends the plan back to the
|
|
55
|
+
# state the work started from. Derived from {SPINE}, walking it from ⚪️:
|
|
56
|
+
# each state on that path steps back to the one before it, as far as 👀,
|
|
57
|
+
# the last state an agent works in. States off the path (🔴 💩 ⏪ and
|
|
58
|
+
# the birth state) have no single state behind them and step back
|
|
59
|
+
# nowhere.
|
|
60
|
+
RETREAT = Enumerator.produce(:new) { SPINE.fetch(it) { raise StopIteration } }
|
|
61
|
+
.take_while { it != :approved }
|
|
62
|
+
.each_cons(2).to_h { |before, after| [after, before] }
|
|
63
|
+
.freeze
|
|
64
|
+
|
|
65
|
+
# Preference order when several states fit a folder's contents at once.
|
|
66
|
+
# Read top to bottom: the loudest fact wins. A discard outranks everything,
|
|
67
|
+
# a block outranks progress, and `retroactive` is last because it is the
|
|
68
|
+
# absence of documents rather than the presence of any.
|
|
69
|
+
PREFERENCE = %i[
|
|
70
|
+
discarded rolled_back shit deferred blocked product_blocked
|
|
71
|
+
deployed approved rejected in_review ready_for_review building_ui building planned ready_for_planning
|
|
72
|
+
researched new
|
|
73
|
+
retroactive
|
|
74
|
+
].freeze
|
|
75
|
+
|
|
76
|
+
# States whose members a folder's *contents* cannot tell apart. Within a
|
|
77
|
+
# family the current name always wins, because re-deriving it would be a
|
|
78
|
+
# guess dressed up as a correction.
|
|
79
|
+
#
|
|
80
|
+
# ⭕️ and 🅱️ both mean "a human must decide before this moves"; *which*
|
|
81
|
+
# human is recorded nowhere but the emoji.
|
|
82
|
+
#
|
|
83
|
+
# ⭐️ 🟡 🎨 look identical on disk: a `spec.md` and a `plan.md` with work
|
|
84
|
+
# units in it. Whether nobody has started, the back end is under way, or
|
|
85
|
+
# the interface is, is recorded only by the agent that renamed the folder
|
|
86
|
+
# when it started. 🟢 👀 🔴 add an open pull request and are just as
|
|
87
|
+
# mute about whether a reviewer has seen it. So `resync` never moves
|
|
88
|
+
# between any of them; they advance by events alone.
|
|
89
|
+
#
|
|
90
|
+
# Order matters: the members run weakest claim first, and a folder
|
|
91
|
+
# arriving from outside lands on the weakest member its contents fit.
|
|
92
|
+
# Contents that fit the family justify only its floor, never its ceiling.
|
|
93
|
+
FAMILIES = [
|
|
94
|
+
%i[blocked product_blocked],
|
|
95
|
+
%i[planned building building_ui ready_for_review in_review rejected]
|
|
96
|
+
].freeze
|
|
97
|
+
|
|
98
|
+
# States the agent loop leaves alone: work that is finished (✅ 😎), work
|
|
99
|
+
# that was dropped (❌), and work waiting on a human (⭕️ 🅱️ ☢️).
|
|
100
|
+
# Everything else is fair game for a specialist.
|
|
101
|
+
#
|
|
102
|
+
# ✅ Approved is here deliberately. `hansolo-reviewer` advancing a plan to
|
|
103
|
+
# it is the end of the loop, not a step in it: nothing merges, and no agent
|
|
104
|
+
# handles `approved`. Merging is the one act in this lifecycle that changes
|
|
105
|
+
# a branch everyone else builds on, and an autonomous loop that does it
|
|
106
|
+
# unattended has no way to be wrong quietly.
|
|
107
|
+
#
|
|
108
|
+
# Moving `approved` out of this list is therefore a decision about blast
|
|
109
|
+
# radius rather than about topology. If it ever moves, something has to own
|
|
110
|
+
# `approved -> deployed`, and today nothing does.
|
|
111
|
+
SETTLED = %i[approved deployed discarded blocked product_blocked deferred].freeze
|
|
112
|
+
|
|
113
|
+
# Every state an agent that may sign `Blocked` works in. A block the
|
|
114
|
+
# machine refuses is reported as a failed attempt and parks nothing, so a
|
|
115
|
+
# state missing here turns an honest "I need a decision" into a failure.
|
|
116
|
+
# The agents spec checks every prompt that describes blocking against it.
|
|
117
|
+
#
|
|
118
|
+
# Review is absent: a reviewer rules, it does not ask.
|
|
119
|
+
BLOCKABLE = %i[new researched retroactive ready_for_planning planned building building_ui rejected].freeze
|
|
120
|
+
|
|
121
|
+
# Rerouting a transition below makes the hand-drawn
|
|
122
|
+
# `docs/img/plan-spec-build.png` stale — `just docs` will show you, because
|
|
123
|
+
# the mermaid source in the generated document is derived from this block.
|
|
124
|
+
aasm do
|
|
125
|
+
# The vocabulary is defined once, in {STATUSES}. This machine may not
|
|
126
|
+
# quietly know a different set.
|
|
127
|
+
OpenLanes::Plans::STATUSES.each { |status| state status.key }
|
|
128
|
+
|
|
129
|
+
event :specify, guard: :justified? do
|
|
130
|
+
transitions from: %i[retroactive blocked product_blocked deferred], to: :new
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
event :research, guard: :justified? do
|
|
134
|
+
transitions from: %i[new retroactive blocked product_blocked deferred], to: :researched
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# The specification is done and the planner has not started. Entered
|
|
138
|
+
# by the harness when `yoda-writer` completes; `resync` reaches it on
|
|
139
|
+
# a blank `plan.md`. Without it ⭐️ Planned required a file only the
|
|
140
|
+
# agent handling ⭐️ Planned writes, and nothing could enter it.
|
|
141
|
+
event :ready_to_plan, guard: :justified? do
|
|
142
|
+
transitions from: %i[researched new retroactive shit blocked product_blocked deferred],
|
|
143
|
+
to: :ready_for_planning
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# `new` stays in this list. The spine routes a plan through research, and
|
|
147
|
+
# that is what the agent loop follows — but a specification somebody has
|
|
148
|
+
# already researched by hand should not have to pretend otherwise to get
|
|
149
|
+
# planned. What research buys is not enforced here; it is enforced by
|
|
150
|
+
# `yoda-writer` handling `researched` and nothing else.
|
|
151
|
+
event :plan, guard: :justified? do
|
|
152
|
+
transitions from: %i[ready_for_planning researched new retroactive shit blocked product_blocked deferred],
|
|
153
|
+
to: :planned
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
event :build, guard: :justified? do
|
|
157
|
+
transitions from: %i[planned shit approved rolled_back retroactive blocked product_blocked deferred],
|
|
158
|
+
to: :building
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Building hands off rather than finishing. A plan with no interface work
|
|
162
|
+
# still passes through Building UI: `rey-frontend` says there is nothing
|
|
163
|
+
# to build and moves it on, which costs one cheap round and keeps the
|
|
164
|
+
# spine one shape instead of two. `rejected` re-enters here rather than
|
|
165
|
+
# going straight back to review, because a change request reopens the
|
|
166
|
+
# implementation and neither half can assume the other still holds.
|
|
167
|
+
event :build_ui, guard: :justified? do
|
|
168
|
+
transitions from: %i[building rejected], to: :building_ui
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# The pair lands one pull request, and whichever half finishes last
|
|
172
|
+
# carries the plan to review from wherever the folder stands.
|
|
173
|
+
event :submit, guard: :justified? do
|
|
174
|
+
transitions from: %i[building building_ui rejected rolled_back], to: :ready_for_review
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
event :review, guard: :justified? do
|
|
178
|
+
transitions from: %i[ready_for_review], to: :in_review
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
event :approve, guard: :justified? do
|
|
182
|
+
transitions from: %i[in_review retroactive], to: :approved
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
event :request_changes, guard: :justified? do
|
|
186
|
+
transitions from: %i[in_review], to: :rejected
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
event :slop, guard: :justified? do
|
|
190
|
+
transitions from: %i[in_review], to: :shit
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
event :deploy, guard: :justified? do
|
|
194
|
+
transitions from: %i[approved], to: :deployed
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# 🚀 is not the end of the line. A release that breaks in production comes
|
|
198
|
+
# back through ⏪, which needs its own proof on disk: by the time work is
|
|
199
|
+
# deployed every pull request is merged, so 🟢's "at least one open pull
|
|
200
|
+
# request" guard would refuse a direct return, permanently.
|
|
201
|
+
event :rollback, guard: :justified? do
|
|
202
|
+
transitions from: %i[deployed], to: :rolled_back
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
event :block, guard: :justified? do
|
|
206
|
+
transitions from: BLOCKABLE, to: :blocked
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
event :block_on_product, guard: :justified? do
|
|
210
|
+
transitions from: BLOCKABLE, to: :product_blocked
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
event :defer, guard: :justified? do
|
|
214
|
+
transitions from: BLOCKABLE + %i[blocked product_blocked], to: :deferred
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Anything, from anywhere, may be dropped for good.
|
|
218
|
+
event :discard, guard: :justified? do
|
|
219
|
+
transitions from: OpenLanes::Plans::STATUSES.map(&:key) - %i[discarded], to: :discarded
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Back one step, fired only by {#retreat!}. It is the one event with
|
|
223
|
+
# several destinations, and it is left out of {inbound}, {outbound}
|
|
224
|
+
# and {event_for}: those describe how work moves on, which is what
|
|
225
|
+
# the documentation, the diagram and {#may?} are about, and a person
|
|
226
|
+
# sending work back is not a way for it to move on.
|
|
227
|
+
event :step_back, guard: :justified? do
|
|
228
|
+
RETREAT.each { |from, to| transitions from:, to: }
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# A transition is not a bookkeeping entry: it is the folder moving.
|
|
232
|
+
after_all_transitions :rename!
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
class << self
|
|
236
|
+
# Destination => the event that reaches it. Every event above has exactly
|
|
237
|
+
# one `to:`, which is what lets `--to <state>` name a destination rather
|
|
238
|
+
# than making callers learn the verbs.
|
|
239
|
+
#
|
|
240
|
+
# @return [Hash{Symbol => Symbol}]
|
|
241
|
+
def event_for
|
|
242
|
+
@event_for ||= forward_events.each_with_object({}) { |event, map|
|
|
243
|
+
event.transitions.each { |t| map[t.to] ||= event.name }
|
|
244
|
+
}.freeze
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Destination => the states that may legitimately reach it.
|
|
248
|
+
#
|
|
249
|
+
# `retroactive` is absent: it is a birth state, produced by
|
|
250
|
+
# `create --after` for work that shipped undocumented.
|
|
251
|
+
#
|
|
252
|
+
# @return [Hash{Symbol => Array<Symbol>}]
|
|
253
|
+
def inbound
|
|
254
|
+
@inbound ||= forward_events.each_with_object({}) { |event, map|
|
|
255
|
+
event.transitions.each { |t| (map[t.to] ||= []).concat(Array(t.from)) }
|
|
256
|
+
}.transform_values { |froms| froms.uniq.freeze }.freeze
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Source => the states it may reach.
|
|
260
|
+
#
|
|
261
|
+
# @return [Hash{Symbol => Array<Symbol>}]
|
|
262
|
+
def outbound_map
|
|
263
|
+
@outbound_map ||= inbound.each_with_object({}) { |(to, froms), map|
|
|
264
|
+
froms.each { |from| (map[from] ||= []) << to }
|
|
265
|
+
}.transform_values(&:freeze).freeze
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# @param key [Symbol]
|
|
269
|
+
# @return [Array<Symbol>]
|
|
270
|
+
def outbound(key) = outbound_map.fetch(key, [])
|
|
271
|
+
|
|
272
|
+
# @param from [Symbol]
|
|
273
|
+
# @param to [Symbol]
|
|
274
|
+
# @return [Boolean] whether the topology permits this edge at all
|
|
275
|
+
def edge?(from, to) = outbound(from).include?(to)
|
|
276
|
+
|
|
277
|
+
# Every event but {RETREAT}'s.
|
|
278
|
+
#
|
|
279
|
+
# @return [Array<AASM::Core::Event>]
|
|
280
|
+
def forward_events = aasm.events.reject { it.name == :step_back }
|
|
281
|
+
|
|
282
|
+
# @param key [Symbol]
|
|
283
|
+
# @return [Array<Symbol>] the family +key+ belongs to, empty when it has none
|
|
284
|
+
def family_of(key) = FAMILIES.find { |family| family.include?(key) } || []
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# @param subject [#status, #file?, #read, #pull_requests, #rename_to]
|
|
288
|
+
def initialize(subject)
|
|
289
|
+
@subject = subject
|
|
290
|
+
@key = subject.status.key
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# @return [Object] the plan folder this machine speaks for
|
|
294
|
+
attr_reader :subject
|
|
295
|
+
|
|
296
|
+
# @return [Symbol] the state right now
|
|
297
|
+
attr_reader :key
|
|
298
|
+
|
|
299
|
+
# @return [OpenLanes::Plans::Status]
|
|
300
|
+
def status = STATUS_BY_KEY.fetch(key)
|
|
301
|
+
|
|
302
|
+
# AASM keeps state in an ORM column; we keep it in a directory name. These
|
|
303
|
+
# three methods are the whole of that adaptation.
|
|
304
|
+
#
|
|
305
|
+
# @return [Symbol]
|
|
306
|
+
def aasm_read_state(_name = :default) = @key
|
|
307
|
+
|
|
308
|
+
# @return [Boolean]
|
|
309
|
+
def aasm_write_state(new_state, name = :default) = aasm_write_state_without_persistence(new_state, name)
|
|
310
|
+
|
|
311
|
+
# @return [Boolean]
|
|
312
|
+
def aasm_write_state_without_persistence(new_state, _name = :default)
|
|
313
|
+
@key = new_state
|
|
314
|
+
true
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# States reachable right now, guards applied. A step back is not among
|
|
318
|
+
# them: only {#retreat!} takes it.
|
|
319
|
+
#
|
|
320
|
+
# @return [Array<Symbol>]
|
|
321
|
+
def allowed = aasm.states(permitted: true).map(&:name) & self.class.outbound(key)
|
|
322
|
+
|
|
323
|
+
# @param to [Symbol]
|
|
324
|
+
# @return [Boolean] whether that move is permitted right now
|
|
325
|
+
def may?(to) = allowed.include?(to)
|
|
326
|
+
|
|
327
|
+
# The next state along the spine, whether or not its guard passes.
|
|
328
|
+
#
|
|
329
|
+
# @return [Symbol, nil]
|
|
330
|
+
def spine_next = SPINE[key]
|
|
331
|
+
|
|
332
|
+
# Move the folder. With no argument it walks one step along the {SPINE}.
|
|
333
|
+
#
|
|
334
|
+
# A refusal is information — it means the phase has not actually happened
|
|
335
|
+
# yet — so it arrives as {Refused} carrying the reason, never as `false`.
|
|
336
|
+
#
|
|
337
|
+
# @param to [Symbol] destination, defaulting to the next spine state
|
|
338
|
+
# @return [OpenLanes::Plans::Status] the state now occupied
|
|
339
|
+
# @raise [OpenLanes::Plans::StateMachine::Refused]
|
|
340
|
+
def promote!(to = spine_next)
|
|
341
|
+
raise Refused, "#{describe(key)} is terminal — nothing follows it" if to.nil?
|
|
342
|
+
raise Refused, "nothing can reach #{describe(to)}" unless self.class.event_for.key?(to)
|
|
343
|
+
raise Refused, refusal(to) unless may?(to)
|
|
344
|
+
|
|
345
|
+
aasm.fire!(self.class.event_for.fetch(to))
|
|
346
|
+
status
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# @return [Symbol, nil] the state one step back, nil when there is none
|
|
350
|
+
def previous = RETREAT[key]
|
|
351
|
+
|
|
352
|
+
# Move the folder one step back along the spine (W9).
|
|
353
|
+
#
|
|
354
|
+
# @return [OpenLanes::Plans::Status] the state now occupied
|
|
355
|
+
# @raise [OpenLanes::Plans::StateMachine::Refused] when nothing is
|
|
356
|
+
# behind this state, or the folder's contents do not justify it
|
|
357
|
+
def retreat!
|
|
358
|
+
refusal = retreat_refusal and raise Refused, refusal
|
|
359
|
+
|
|
360
|
+
aasm.fire!(:step_back)
|
|
361
|
+
status
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# @return [String, nil] why {#retreat!} would be refused, nil when it would not
|
|
365
|
+
def retreat_refusal
|
|
366
|
+
to = previous or return "#{describe(key)} has no state behind it to go back to"
|
|
367
|
+
return nil if aasm.may_fire_event?(:step_back)
|
|
368
|
+
|
|
369
|
+
STATUS_BY_KEY.fetch(to).violation(subject) || "#{describe(key)} cannot go back to #{describe(to)}"
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# The state a folder's contents justify — the answer to "well, what should
|
|
373
|
+
# it be, then?". This is what `resync dirs` renames toward.
|
|
374
|
+
#
|
|
375
|
+
# Invariants are *minimum* requirements, not exact matches: a ⚪️ folder
|
|
376
|
+
# that has since grown a `plan.md` still satisfies ⚪️, and is nonetheless
|
|
377
|
+
# ⭐️ now. So the furthest-justified state wins, and the caller compares it
|
|
378
|
+
# against the current one to decide whether anything should move.
|
|
379
|
+
#
|
|
380
|
+
# The exception is a {FAMILIES family}, whose members contents cannot tell
|
|
381
|
+
# apart. Two rules cover it:
|
|
382
|
+
#
|
|
383
|
+
# - **Already inside one** — the current name wins. Re-deriving it would be
|
|
384
|
+
# a guess dressed up as a correction, and every ⭕️ would become 🅱️.
|
|
385
|
+
# - **Arriving from outside** — the *weakest* member the contents fit
|
|
386
|
+
# wins, not the furthest. A 📋 folder whose `plan.md` grew work units
|
|
387
|
+
# lands on ⭐️, not 🟡, because nobody has started. A ✅ folder found
|
|
388
|
+
# with an open pull request is demonstrably back in the PR phase, and
|
|
389
|
+
# ⭐️ refuses an open pull request, so it lands on 🟡 rather than
|
|
390
|
+
# claiming 🔴.
|
|
391
|
+
#
|
|
392
|
+
# @return [OpenLanes::Plans::Status, nil] nil when nothing fits at all
|
|
393
|
+
def best_fit
|
|
394
|
+
fitting = STATUSES.select { |s| s.satisfied_by?(subject) }
|
|
395
|
+
return nil if fitting.empty?
|
|
396
|
+
|
|
397
|
+
best = PREFERENCE.filter_map { |k| fitting.find { |s| s.key == k } }.first || fitting.first
|
|
398
|
+
return status if self.class.family_of(key).include?(best.key)
|
|
399
|
+
|
|
400
|
+
family = self.class.family_of(best.key)
|
|
401
|
+
return best if family.empty?
|
|
402
|
+
|
|
403
|
+
floor = family.find { |k| fitting.any? { |s| s.key == k } }
|
|
404
|
+
STATUS_BY_KEY.fetch(floor)
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
private
|
|
408
|
+
|
|
409
|
+
# The one guard. Entering a state is permitted exactly when that state's
|
|
410
|
+
# invariant holds for this folder's contents.
|
|
411
|
+
#
|
|
412
|
+
# @return [Boolean]
|
|
413
|
+
def justified? = STATUS_BY_KEY.fetch(aasm.to_state).satisfied_by?(subject)
|
|
414
|
+
|
|
415
|
+
# @param to [Symbol]
|
|
416
|
+
# @return [String]
|
|
417
|
+
def refusal(to)
|
|
418
|
+
return "#{describe(key)} cannot become #{describe(to)}" unless self.class.edge?(key, to)
|
|
419
|
+
|
|
420
|
+
STATUS_BY_KEY.fetch(to).violation(subject)
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# @param key [Symbol]
|
|
424
|
+
# @return [String]
|
|
425
|
+
def describe(key) = STATUS_BY_KEY.fetch(key).to_s
|
|
426
|
+
|
|
427
|
+
# The folder moves. {Subject} owns the path, so it owns the move.
|
|
428
|
+
#
|
|
429
|
+
# @return [void]
|
|
430
|
+
def rename! = subject.rename_to(STATUS_BY_KEY.fetch(aasm.to_state))
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
end
|