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,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Lifecycle
|
|
5
|
+
# Moves a plan past the phases its lane skips.
|
|
6
|
+
#
|
|
7
|
+
# A plan in the `plan` lane has a specification its author already
|
|
8
|
+
# trusts, so research and rewriting are skipped: the harness writes the
|
|
9
|
+
# blank `plan.md` yoda-writer would have left and moves the folder to 📋.
|
|
10
|
+
# A plan in the `quick` lane is a short mechanical task: the harness
|
|
11
|
+
# writes a one-unit `plan.md` and moves the folder to ⭐️, where
|
|
12
|
+
# r2d2-mechanic is the only builder that serves the lane.
|
|
13
|
+
#
|
|
14
|
+
# Both moves are ordinary transitions through the state machine, so the
|
|
15
|
+
# invariants still decide; the harness only supplies the file the
|
|
16
|
+
# skipped agent would have written.
|
|
17
|
+
module Lanes
|
|
18
|
+
# The state each lane jumps to.
|
|
19
|
+
TARGET = { plan: :ready_for_planning, quick: :planned }.freeze
|
|
20
|
+
|
|
21
|
+
# The states a lane jumps from. Anything later has already been worked.
|
|
22
|
+
FROM = %i[new researched].freeze
|
|
23
|
+
|
|
24
|
+
# What the harness writes into `plan.md` for each lane.
|
|
25
|
+
STUB = {
|
|
26
|
+
plan: "",
|
|
27
|
+
quick: <<~MARKDOWN
|
|
28
|
+
# Plan
|
|
29
|
+
|
|
30
|
+
## Task
|
|
31
|
+
|
|
32
|
+
Quick lane: the task is the whole of `spec.md`, done in one pass by `r2d2-mechanic`.
|
|
33
|
+
MARKDOWN
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
module_function
|
|
37
|
+
|
|
38
|
+
# Where this plan's lane says it should be, when that is somewhere other
|
|
39
|
+
# than where it is.
|
|
40
|
+
#
|
|
41
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
42
|
+
# @param spec [OpenLanes::Plans::Spec]
|
|
43
|
+
# @return [OpenLanes::Plans::Status, nil]
|
|
44
|
+
def target(subject, spec = Plans::Spec.for(subject))
|
|
45
|
+
key = TARGET[spec.lane] or return nil
|
|
46
|
+
return nil unless FROM.include?(subject.status.key)
|
|
47
|
+
|
|
48
|
+
Plans.status(key)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Write the skipped agent's file and move the folder.
|
|
52
|
+
#
|
|
53
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
54
|
+
# @param commit [Boolean] false only reports what would happen
|
|
55
|
+
# @return [Array(Symbol, Symbol), nil] from and to, when it moved
|
|
56
|
+
def advance(subject, commit:)
|
|
57
|
+
spec = Plans::Spec.for(subject)
|
|
58
|
+
to = target(subject, spec) or return nil
|
|
59
|
+
from = subject.status.key
|
|
60
|
+
return [from, to.key] unless commit
|
|
61
|
+
|
|
62
|
+
path = File.join(subject.feature.path, "plan.md")
|
|
63
|
+
unless File.exist?(path)
|
|
64
|
+
File.write(path, [STUB.fetch(spec.lane).rstrip, Completion.markdown(spec)].reject(&:empty?).join("\n\n"))
|
|
65
|
+
end
|
|
66
|
+
machine = subject.machine
|
|
67
|
+
return nil unless machine.may?(to.key)
|
|
68
|
+
|
|
69
|
+
machine.promote!(to.key)
|
|
70
|
+
[from, to.key]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Lifecycle
|
|
5
|
+
# Reconciling what is recorded with what is true.
|
|
6
|
+
#
|
|
7
|
+
# Both resyncs are dry-run by default and both refuse to act where they are
|
|
8
|
+
# not certain, because both write to things other people join on: folder
|
|
9
|
+
# names, and pull request titles.
|
|
10
|
+
module Resync
|
|
11
|
+
# `resync dirs` — makes every folder's name say what the folder is.
|
|
12
|
+
#
|
|
13
|
+
# Two things can be wrong with a name, and both are repaired here:
|
|
14
|
+
#
|
|
15
|
+
# - **The emoji is wrong** — the contents justify a different state. A
|
|
16
|
+
# folder whose status already holds is left alone, which is what stops
|
|
17
|
+
# ⭕️ Blocked and 🅱️ Product Blocked — deliberately identical invariants,
|
|
18
|
+
# distinguished only by the name — from collapsing into one.
|
|
19
|
+
# - **The spelling is not canonical** — a folder written `018-⚪️--foo`
|
|
20
|
+
# before the `NNN.MM` padding rule or the ` → ` separator, or
|
|
21
|
+
# `18.1-⚪️--foo` by hand. Unpadded numbers read fine and sort wrong —
|
|
22
|
+
# `018.09` < `018.1` < `018.10` puts a single mixed-width folder in the
|
|
23
|
+
# middle of the range. Both are renamed, to `018.00-⚪️ → foo` and
|
|
24
|
+
# `018.01-⚪️ → foo`.
|
|
25
|
+
#
|
|
26
|
+
# The repairs are applied in that order reversed, as **up to two renames
|
|
27
|
+
# per folder**: first the spelling, under the status the folder already
|
|
28
|
+
# claims — which is how a `--`-era folder migrates to the ` → ` form —
|
|
29
|
+
# and only then the status its contents justify. Both renames aim at
|
|
30
|
+
# {Feature.plan_dirname}, so they cannot disagree about the final name;
|
|
31
|
+
# splitting them keeps each auditable on its own, instead of one report
|
|
32
|
+
# line where a spelling migration also appears to have opinions about
|
|
33
|
+
# the state of the work.
|
|
34
|
+
class Dirs
|
|
35
|
+
# A proposed rename.
|
|
36
|
+
#
|
|
37
|
+
# @!attribute [r] dirname
|
|
38
|
+
# @return [String] the folder as it stands
|
|
39
|
+
# @!attribute [r] from
|
|
40
|
+
# @return [Symbol] the state it claims
|
|
41
|
+
# @!attribute [r] to
|
|
42
|
+
# @return [Symbol] the state its contents justify
|
|
43
|
+
# @!attribute [r] source
|
|
44
|
+
# @return [String] absolute path now
|
|
45
|
+
# @!attribute [r] target
|
|
46
|
+
# @return [String] absolute path after
|
|
47
|
+
# @!attribute [r] reason
|
|
48
|
+
# @return [String] why the current name is wrong
|
|
49
|
+
Change = Data.define(:dirname, :from, :to, :source, :target, :reason) do
|
|
50
|
+
# @return [String] a single auditable line — `⇒` rather than `→`,
|
|
51
|
+
# which the folder names themselves now contain
|
|
52
|
+
def to_s = "#{dirname} ⇒ #{File.basename(target)} (#{reason})"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The state a folder's contents justify, or the one it claims when
|
|
56
|
+
# nothing fits. What a rename moves toward, and what the dispatcher
|
|
57
|
+
# reads on a dry run, which renames nothing, so the preview still
|
|
58
|
+
# names the agents a real run would start.
|
|
59
|
+
#
|
|
60
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
61
|
+
# @return [OpenLanes::Plans::Status]
|
|
62
|
+
def self.target(subject) = subject.best_fit || subject.status
|
|
63
|
+
|
|
64
|
+
# @param tree [OpenLanes::Plans::Tree]
|
|
65
|
+
# @param except [Array<String>] ordinals to leave alone however wrong
|
|
66
|
+
# their names are: the dispatcher passes the plans an agent is
|
|
67
|
+
# working in right now
|
|
68
|
+
def initialize(tree:, except: [])
|
|
69
|
+
@tree = tree
|
|
70
|
+
@except = except.map(&:to_s)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @return [OpenLanes::Plans::Tree]
|
|
74
|
+
attr_reader :tree
|
|
75
|
+
|
|
76
|
+
# What would change, without changing anything. A folder can appear
|
|
77
|
+
# twice: its spelling migration first, then its status correction.
|
|
78
|
+
#
|
|
79
|
+
# @return [Array<OpenLanes::Lifecycle::Resync::Dirs::Change>]
|
|
80
|
+
def plan
|
|
81
|
+
tree.subjects.flat_map do |subject|
|
|
82
|
+
@except.include?(subject.feature.ordinal.to_s) ? [] : changes_for(subject)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# @param commit [Boolean] actually rename
|
|
87
|
+
# @return [Array<OpenLanes::Lifecycle::Resync::Dirs::Change>] what was proposed
|
|
88
|
+
def call(commit: false)
|
|
89
|
+
changes = plan
|
|
90
|
+
return changes unless commit
|
|
91
|
+
|
|
92
|
+
UI.stepping(changes, "Renaming") { |change| rename(change) }
|
|
93
|
+
tree.reload
|
|
94
|
+
changes
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
# A folder moves when the name it has is not the name it should have.
|
|
100
|
+
# {target} says what that name is; this says how to get there, as up
|
|
101
|
+
# to two renames applied in order. The status rename starts from where
|
|
102
|
+
# the spelling rename left the folder, not from where it stands now,
|
|
103
|
+
# so committing the steps sequentially never renames a path that has
|
|
104
|
+
# already moved out from under it.
|
|
105
|
+
#
|
|
106
|
+
# @param subject [OpenLanes::Plans::Subject]
|
|
107
|
+
# @return [Array<OpenLanes::Lifecycle::Resync::Dirs::Change>] empty when the name
|
|
108
|
+
# is already right
|
|
109
|
+
def changes_for(subject)
|
|
110
|
+
feature = subject.feature
|
|
111
|
+
fit = self.class.target(subject)
|
|
112
|
+
steps = []
|
|
113
|
+
|
|
114
|
+
spelled = feature.dirname_as(subject.status)
|
|
115
|
+
if spelled != feature.dirname
|
|
116
|
+
steps << step(feature,
|
|
117
|
+
from: subject.status,
|
|
118
|
+
to: subject.status,
|
|
119
|
+
source: feature.path,
|
|
120
|
+
reason: spelling_reason(feature))
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
if fit.key != subject.status.key
|
|
124
|
+
source = steps.empty? ? feature.path : steps.last.target
|
|
125
|
+
steps << step(feature,
|
|
126
|
+
from: subject.status,
|
|
127
|
+
to: fit,
|
|
128
|
+
source:,
|
|
129
|
+
reason: subject.violation || "contents now justify #{fit.label}")
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
steps
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# @param feature [OpenLanes::Plans::Feature]
|
|
136
|
+
# @param from [OpenLanes::Plans::Status]
|
|
137
|
+
# @param to [OpenLanes::Plans::Status]
|
|
138
|
+
# @param source [String] absolute path the rename starts from
|
|
139
|
+
# @param reason [String]
|
|
140
|
+
# @return [OpenLanes::Lifecycle::Resync::Dirs::Change]
|
|
141
|
+
def step(feature, from:, to:, source:, reason:)
|
|
142
|
+
Change.new(
|
|
143
|
+
dirname: File.basename(source),
|
|
144
|
+
from: from.key,
|
|
145
|
+
to: to.key,
|
|
146
|
+
source:,
|
|
147
|
+
target: File.join(File.dirname(feature.path), feature.dirname_as(to)),
|
|
148
|
+
reason:
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# @param feature [OpenLanes::Plans::Feature]
|
|
153
|
+
# @return [String] why the spelling is wrong, status aside
|
|
154
|
+
def spelling_reason(feature)
|
|
155
|
+
return "#{feature.dirname_ordinal} is not padded to #{feature.ordinal}" unless feature.padded?
|
|
156
|
+
|
|
157
|
+
"name is not in canonical NNN.MM-<emoji> → <slug> form"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# A rename that finds its target occupied has not happened, and saying
|
|
161
|
+
# nothing about it would leave the folder misnamed with a report
|
|
162
|
+
# claiming otherwise. Two folders can want the same canonical name —
|
|
163
|
+
# `018-⚪️--foo` and `018.00-⚪️ → foo` are the same plan written twice.
|
|
164
|
+
#
|
|
165
|
+
# @param change [OpenLanes::Lifecycle::Resync::Dirs::Change]
|
|
166
|
+
# @return [void]
|
|
167
|
+
# @raise [OpenLanes::Error] when the target name is already taken
|
|
168
|
+
def rename(change)
|
|
169
|
+
return if OpenLanes.move_directory(change.source, change.target)
|
|
170
|
+
|
|
171
|
+
raise Error, "cannot rename #{change.dirname} — #{File.basename(change.target)} already exists"
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# `resync prs` — puts an `[NNN.MM]` prefix on every pull request title that
|
|
176
|
+
# lacks one, and `[DEV.00]` on the ones that implement no plan.
|
|
177
|
+
#
|
|
178
|
+
# The resolution order is the branch name first, then the diff, and only
|
|
179
|
+
# when the diff touches exactly one plan. Anything else is reported for a
|
|
180
|
+
# human and never edited: `pull-requests.md` is generated from these
|
|
181
|
+
# titles, and a wrong number files work under a plan that did not do it,
|
|
182
|
+
# leaving the plan that did looking untouched.
|
|
183
|
+
class Prs
|
|
184
|
+
# The marker this tool used to write, now replaced. A title wearing it is
|
|
185
|
+
# rewritten in place: the assertion it makes — "this belongs to no
|
|
186
|
+
# specification" — is still true and still somebody's deliberate call,
|
|
187
|
+
# so it is restamped rather than re-resolved.
|
|
188
|
+
STALE = /\A\[#{Regexp.escape(OpenLanes::STALE_NO_PLAN_PREFIX)}\]\s*/
|
|
189
|
+
|
|
190
|
+
# Titles that already carry a prefix — a plan number, the no-plan marker,
|
|
191
|
+
# or the legacy `[XXX]`.
|
|
192
|
+
PREFIXED = /\A\[(?:\d{3}(?:\.\d{2})?|#{OpenLanes::NO_PLAN_PREFIX}|XXX)\](?:\([A-Z]\))?\s/
|
|
193
|
+
|
|
194
|
+
# Finds a plan number in a branch name: `kig/018.01-verify`, `002-slug`.
|
|
195
|
+
BRANCH_PATTERN = %r{(?:\A|[/\-_])(\d{3}(?:\.\d{2})?)(?:\z|[-_])}
|
|
196
|
+
|
|
197
|
+
# A proposed retitle.
|
|
198
|
+
#
|
|
199
|
+
# @!attribute [r] number
|
|
200
|
+
# @return [Integer] the pull request
|
|
201
|
+
# @!attribute [r] title
|
|
202
|
+
# @return [String] as it stands
|
|
203
|
+
# @!attribute [r] new_title
|
|
204
|
+
# @return [String, nil] nil when nothing may safely be done
|
|
205
|
+
# @!attribute [r] ordinal
|
|
206
|
+
# @return [OpenLanes::Plans::Ordinal, nil]
|
|
207
|
+
# @!attribute [r] reason
|
|
208
|
+
# @return [String] how it was resolved, or why it was not
|
|
209
|
+
# @!attribute [r] ambiguous
|
|
210
|
+
# @return [Boolean] a human must decide; never edited
|
|
211
|
+
# @!attribute [r] assumed
|
|
212
|
+
# @return [Boolean] "no plan" was inferred, not asserted by the author
|
|
213
|
+
Change = Data.define(:number, :title, :new_title, :ordinal, :reason, :ambiguous, :assumed, :adopted) do
|
|
214
|
+
# @return [Boolean]
|
|
215
|
+
def ambiguous? = ambiguous
|
|
216
|
+
|
|
217
|
+
# @return [Boolean]
|
|
218
|
+
def assumed? = assumed
|
|
219
|
+
|
|
220
|
+
# @return [Boolean] whether a plan folder was minted for this one
|
|
221
|
+
def adopted? = adopted
|
|
222
|
+
|
|
223
|
+
# @return [Boolean] safe to apply without a human looking
|
|
224
|
+
def applicable? = !ambiguous && !new_title.nil?
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# @param tree [OpenLanes::Plans::Tree]
|
|
228
|
+
# @param github [OpenLanes::Vcs::GitHub]
|
|
229
|
+
# @param adopt [Boolean] give a plan folder to every pull request that
|
|
230
|
+
# resolves to none, rather than flagging it and moving on
|
|
231
|
+
# @param root [String, nil] repository root, for reading branches
|
|
232
|
+
def initialize(tree:, github: Vcs::GitHub.new, adopt: true, root: nil)
|
|
233
|
+
@tree = tree
|
|
234
|
+
@github = github
|
|
235
|
+
@adopt = adopt
|
|
236
|
+
@root = root
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# @return [Boolean]
|
|
240
|
+
def adopt? = @adopt
|
|
241
|
+
|
|
242
|
+
# @return [OpenLanes::Plans::Tree]
|
|
243
|
+
attr_reader :tree
|
|
244
|
+
|
|
245
|
+
# @return [OpenLanes::Vcs::GitHub]
|
|
246
|
+
attr_reader :github
|
|
247
|
+
|
|
248
|
+
# What would change, without changing anything.
|
|
249
|
+
#
|
|
250
|
+
# @return [Array<OpenLanes::Lifecycle::Resync::Prs::Change>]
|
|
251
|
+
def plan
|
|
252
|
+
restamps + resolve(candidates).then { |changes| adopt? ? with_adoptions(changes) : changes }
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# @return [Array<OpenLanes::Lifecycle::Resync::Prs::Change>]
|
|
256
|
+
def restamps = stale.map { |pull| restamped(pull) }
|
|
257
|
+
|
|
258
|
+
# @param commit [Boolean] actually retitle, and mint the folders
|
|
259
|
+
# @return [Array<OpenLanes::Lifecycle::Resync::Prs::Change>] what was proposed
|
|
260
|
+
def call(commit: false)
|
|
261
|
+
changes = resolve(candidates)
|
|
262
|
+
return restamps + (adopt? ? with_adoptions(changes) : changes) unless commit
|
|
263
|
+
|
|
264
|
+
changes = with_adoptions(changes, create: true) if adopt?
|
|
265
|
+
changes = restamps + changes
|
|
266
|
+
applicable = changes.select(&:applicable?)
|
|
267
|
+
UI.stepping(applicable, "Retitling") { |c| github.retitle(number: c.number, title: c.new_title) }
|
|
268
|
+
changes
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# @return [Array<OpenLanes::Lifecycle::Adoption>] the adopter, memoized
|
|
272
|
+
def adoption = @adoption ||= Adoption.new(tree:, github:, root: @root)
|
|
273
|
+
|
|
274
|
+
private
|
|
275
|
+
|
|
276
|
+
# @return [Array<Hash>] pull requests with no prefix yet
|
|
277
|
+
def candidates
|
|
278
|
+
github.pulls.reject { |pr| pr[:title].to_s.match?(PREFIXED) || pr[:title].to_s.match?(STALE) }
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# @return [Array<Hash>] pull requests still wearing the old marker
|
|
282
|
+
def stale = github.pulls.select { |pr| pr[:title].to_s.match?(STALE) }
|
|
283
|
+
|
|
284
|
+
# @param pull [Hash]
|
|
285
|
+
# @return [OpenLanes::Lifecycle::Resync::Prs::Change]
|
|
286
|
+
def restamped(pull)
|
|
287
|
+
Change.new(number: pull[:number],
|
|
288
|
+
title: pull[:title],
|
|
289
|
+
ordinal: nil,
|
|
290
|
+
ambiguous: false,
|
|
291
|
+
new_title: pull[:title].sub(STALE, "[#{OpenLanes::NO_PLAN_PREFIX}] "),
|
|
292
|
+
reason: "the no-plan marker is now [#{OpenLanes::NO_PLAN_PREFIX}]",
|
|
293
|
+
assumed: false,
|
|
294
|
+
adopted: false)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# @param pulls [Array<Hash>]
|
|
298
|
+
# @return [Array<OpenLanes::Lifecycle::Resync::Prs::Change>]
|
|
299
|
+
def resolve(pulls) = pulls.map { |pr| change_for(pr) }
|
|
300
|
+
|
|
301
|
+
# Replace every flagged change with one that points at a plan folder the
|
|
302
|
+
# pull request now owns. The unresolvable ones were unresolvable because
|
|
303
|
+
# no plan described them — so the answer is a plan, not a shrug.
|
|
304
|
+
#
|
|
305
|
+
# @param changes [Array<OpenLanes::Lifecycle::Resync::Prs::Change>]
|
|
306
|
+
# @param create [Boolean] mint the folders, rather than only saying so
|
|
307
|
+
# @return [Array<OpenLanes::Lifecycle::Resync::Prs::Change>]
|
|
308
|
+
def with_adoptions(changes, create: false)
|
|
309
|
+
orphans = changes.select(&:ambiguous?)
|
|
310
|
+
return changes if orphans.empty?
|
|
311
|
+
|
|
312
|
+
pulls = orphans.map { |c| pull_by_number.fetch(c.number) }
|
|
313
|
+
adoptees = create ? adoption.call(pulls) : adoption.plan(pulls)
|
|
314
|
+
by_number = adoptees.to_h { |a| [a.pull[:number], a] }
|
|
315
|
+
|
|
316
|
+
changes.map { |c| c.ambiguous? && by_number[c.number] ? adopted(c, by_number[c.number]) : c }
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# @return [Hash{Integer => Hash}]
|
|
320
|
+
def pull_by_number = @pull_by_number ||= candidates.to_h { |pr| [pr[:number], pr] }
|
|
321
|
+
|
|
322
|
+
# @param change [OpenLanes::Lifecycle::Resync::Prs::Change]
|
|
323
|
+
# @param adoptee [OpenLanes::Lifecycle::Adoption::Adoptee]
|
|
324
|
+
# @return [OpenLanes::Lifecycle::Resync::Prs::Change]
|
|
325
|
+
def adopted(change, adoptee)
|
|
326
|
+
change.with(
|
|
327
|
+
ordinal: adoptee.ordinal,
|
|
328
|
+
new_title: "#{adoptee.ordinal.to_prefix} #{change.title}",
|
|
329
|
+
reason: "#{change.reason} — adopted into #{adoptee.dirname}",
|
|
330
|
+
ambiguous: false,
|
|
331
|
+
adopted: true
|
|
332
|
+
)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# @param pull [Hash]
|
|
336
|
+
# @return [OpenLanes::Lifecycle::Resync::Prs::Change]
|
|
337
|
+
def change_for(pull)
|
|
338
|
+
from_branch(pull) || from_files(pull) || no_plan(pull)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# 1. The branch name — the one moment the author certainly knew.
|
|
342
|
+
#
|
|
343
|
+
# @param pull [Hash]
|
|
344
|
+
# @return [OpenLanes::Lifecycle::Resync::Prs::Change, nil]
|
|
345
|
+
def from_branch(pull)
|
|
346
|
+
match = BRANCH_PATTERN.match(pull[:branch].to_s) or return nil
|
|
347
|
+
ordinal = Plans::Ordinal.parse(match[1])
|
|
348
|
+
|
|
349
|
+
unless tree.include?(ordinal)
|
|
350
|
+
return flag(pull, "branch names #{ordinal}, which has no folder in #{File.basename(tree.dir)}")
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
resolved(pull, ordinal, "branch #{pull[:branch]}")
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# 2. The diff, and only when it touches exactly one plan.
|
|
357
|
+
#
|
|
358
|
+
# @param pull [Hash]
|
|
359
|
+
# @return [OpenLanes::Lifecycle::Resync::Prs::Change, nil]
|
|
360
|
+
def from_files(pull)
|
|
361
|
+
touched = Array(pull[:files]).filter_map { |path| ordinal_for_path(path) }.uniq
|
|
362
|
+
case touched.size
|
|
363
|
+
when 1 then resolved(pull, touched.first, "diff touches only #{touched.first}")
|
|
364
|
+
when 0 then nil
|
|
365
|
+
else flag(pull, "diff touches #{touched.join(", ")} and none is obviously primary")
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# @param path [String] a path from the diff
|
|
370
|
+
# @return [OpenLanes::Plans::Ordinal, nil]
|
|
371
|
+
def ordinal_for_path(path)
|
|
372
|
+
parts = path.to_s.split("/")
|
|
373
|
+
index = parts.index(OpenLanes::PLANS_DIR) or return nil
|
|
374
|
+
folder = parts[index + 1] or return nil
|
|
375
|
+
return nil if parts.length <= index + 2 # a file directly under .plans belongs to no plan
|
|
376
|
+
|
|
377
|
+
ordinal = Plans::Ordinal.from_dirname(folder)
|
|
378
|
+
ordinal if ordinal && tree.include?(ordinal)
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# @param pull [Hash]
|
|
382
|
+
# @param ordinal [OpenLanes::Plans::Ordinal]
|
|
383
|
+
# @param why [String]
|
|
384
|
+
# @return [OpenLanes::Lifecycle::Resync::Prs::Change]
|
|
385
|
+
def resolved(pull, ordinal, why)
|
|
386
|
+
Change.new(number: pull[:number],
|
|
387
|
+
title: pull[:title],
|
|
388
|
+
ordinal:,
|
|
389
|
+
reason: why,
|
|
390
|
+
new_title: "#{ordinal.to_prefix} #{pull[:title]}",
|
|
391
|
+
ambiguous: false,
|
|
392
|
+
assumed: false,
|
|
393
|
+
adopted: false)
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# Nothing resolved, so this is developer work — but that is an assertion
|
|
397
|
+
# about intent, so it is marked as assumed rather than stated as fact.
|
|
398
|
+
#
|
|
399
|
+
# @param pull [Hash]
|
|
400
|
+
# @return [OpenLanes::Lifecycle::Resync::Prs::Change]
|
|
401
|
+
def no_plan(pull)
|
|
402
|
+
Change.new(number: pull[:number],
|
|
403
|
+
title: pull[:title],
|
|
404
|
+
ordinal: nil,
|
|
405
|
+
new_title: "[#{OpenLanes::NO_PLAN_PREFIX}] #{pull[:title]}",
|
|
406
|
+
reason: "no plan resolved from the branch or the diff",
|
|
407
|
+
ambiguous: false,
|
|
408
|
+
assumed: true,
|
|
409
|
+
adopted: false)
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# @param pull [Hash]
|
|
413
|
+
# @param why [String]
|
|
414
|
+
# @return [OpenLanes::Lifecycle::Resync::Prs::Change]
|
|
415
|
+
def flag(pull, why)
|
|
416
|
+
Change.new(number: pull[:number],
|
|
417
|
+
title: pull[:title],
|
|
418
|
+
new_title: nil,
|
|
419
|
+
ordinal: nil,
|
|
420
|
+
reason: why,
|
|
421
|
+
ambiguous: true,
|
|
422
|
+
assumed: false,
|
|
423
|
+
adopted: false)
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
end
|