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,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Engine
|
|
5
|
+
# What a `run` cost, once it is over.
|
|
6
|
+
#
|
|
7
|
+
# The loop reports movement — which plan went from ⭐️ to 🟡, which agent
|
|
8
|
+
# failed and why — and says nothing about the bill. A round of five agents
|
|
9
|
+
# against a large repository spends several million tokens and the better
|
|
10
|
+
# part of an hour, and until this existed the only record of that was a
|
|
11
|
+
# directory of trace files nobody reads.
|
|
12
|
+
#
|
|
13
|
+
# {#render} returns a string and prints nothing, so the caller decides where
|
|
14
|
+
# it goes, the same way {Reporter} works.
|
|
15
|
+
class Tally
|
|
16
|
+
# One agent's share of the run, added up across every round it ran in.
|
|
17
|
+
#
|
|
18
|
+
# @!attribute [r] name
|
|
19
|
+
# @return [String]
|
|
20
|
+
# @!attribute [r] invocations
|
|
21
|
+
# @return [Integer] times this agent was started
|
|
22
|
+
# @!attribute [r] subagents
|
|
23
|
+
# @return [Integer] sub-agents it spawned, across all of them
|
|
24
|
+
# @!attribute [r] up
|
|
25
|
+
# @return [Integer] tokens sent, sub-agent spend included
|
|
26
|
+
# @!attribute [r] down
|
|
27
|
+
# @return [Integer] tokens generated
|
|
28
|
+
# @!attribute [r] seconds
|
|
29
|
+
# @return [Float] how long it was running, added up
|
|
30
|
+
Row = Data.define(:name, :invocations, :subagents, :up, :down, :seconds)
|
|
31
|
+
|
|
32
|
+
# Cells per column, so the header, the rows and the rule cannot drift.
|
|
33
|
+
WIDTHS = { name: 22, invocations: 6, subagents: 11, up: 9, down: 9, seconds: 8 }.freeze
|
|
34
|
+
|
|
35
|
+
# @param attempts [Array<OpenLanes::Engine::Runner::Attempt>]
|
|
36
|
+
# @param seconds [Float] wall clock for the whole loop, which is not the
|
|
37
|
+
# sum of the agents' own times: several of them run at once
|
|
38
|
+
# @param rounds [Integer, nil] accepted so an older caller still works, and
|
|
39
|
+
# otherwise unused: the loop has no rounds of its own any more, each
|
|
40
|
+
# agent counts its own per plan, so the summary counts invocations
|
|
41
|
+
def initialize(attempts:, seconds:, rounds: nil)
|
|
42
|
+
@attempts = attempts.reject { |a| a.ordinal == "?" }
|
|
43
|
+
@seconds = seconds
|
|
44
|
+
@rounds = rounds
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @return [Array<OpenLanes::Engine::Runner::Attempt>]
|
|
48
|
+
attr_reader :attempts
|
|
49
|
+
|
|
50
|
+
# @return [Float] wall clock
|
|
51
|
+
attr_reader :seconds
|
|
52
|
+
|
|
53
|
+
# @return [Integer, nil]
|
|
54
|
+
attr_reader :rounds
|
|
55
|
+
|
|
56
|
+
# Plans an agent was actually started against, counted once each however
|
|
57
|
+
# many rounds they took.
|
|
58
|
+
#
|
|
59
|
+
# @return [Integer]
|
|
60
|
+
def plans = attempts.map(&:ordinal).uniq.size
|
|
61
|
+
|
|
62
|
+
# @return [Integer] tokens sent, everything included
|
|
63
|
+
def up = attempts.sum(&:up)
|
|
64
|
+
|
|
65
|
+
# @return [Integer] tokens generated
|
|
66
|
+
def down = attempts.sum(&:down)
|
|
67
|
+
|
|
68
|
+
# @return [Integer] sub-agents spawned by every agent in the run
|
|
69
|
+
def subagents = attempts.sum(&:subagents)
|
|
70
|
+
|
|
71
|
+
# Sub-agent spend that arrived as one number. `claude` reports what a
|
|
72
|
+
# sub-agent cost without splitting it between the two directions, so this
|
|
73
|
+
# much of {#up} is really up and down together.
|
|
74
|
+
#
|
|
75
|
+
# @return [Integer]
|
|
76
|
+
def delegated = attempts.sum(&:delegated)
|
|
77
|
+
|
|
78
|
+
# @return [Float] seconds of agent time, added up across all of them
|
|
79
|
+
def busy = attempts.sum(&:seconds)
|
|
80
|
+
|
|
81
|
+
# How many agents were running at any given moment, averaged over the
|
|
82
|
+
# run: agent-seconds divided by wall-clock seconds. Below the `-j` ceiling
|
|
83
|
+
# by however much the round spent waiting for its slowest member.
|
|
84
|
+
#
|
|
85
|
+
# @return [Float]
|
|
86
|
+
def concurrency = seconds.positive? ? busy / seconds : 0.0
|
|
87
|
+
|
|
88
|
+
# @return [Array<OpenLanes::Engine::Tally::Row>] one per agent, dearest first
|
|
89
|
+
def by_agent
|
|
90
|
+
attempts.group_by(&:agent).map { |name, list|
|
|
91
|
+
Row.new(name:,
|
|
92
|
+
invocations: list.size,
|
|
93
|
+
subagents: list.sum(&:subagents),
|
|
94
|
+
up: list.sum(&:up),
|
|
95
|
+
down: list.sum(&:down),
|
|
96
|
+
seconds: list.sum(&:seconds))
|
|
97
|
+
}.sort_by { |row| -row.up }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# @return [String] the table and the summary line under it
|
|
101
|
+
def render
|
|
102
|
+
return summary if attempts.empty?
|
|
103
|
+
|
|
104
|
+
([header, rule] + by_agent.map { |row| line(row) } + [rule, totals, "", summary]).join("\n")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# @return [String] the one-line version, for a box
|
|
108
|
+
def summary
|
|
109
|
+
[
|
|
110
|
+
"#{plans} plan#{"s" unless plans == 1} addressed",
|
|
111
|
+
"#{attempts.size} invocation#{"s" unless attempts.size == 1}",
|
|
112
|
+
duration(seconds),
|
|
113
|
+
"#{format("%.1f", concurrency)} agents at a time",
|
|
114
|
+
"↑ #{UI.abbreviate(up)} ↓ #{UI.abbreviate(down)}"
|
|
115
|
+
].join(" · ") + delegation
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
# @return [String] the caveat, where there is one to make
|
|
121
|
+
def delegation
|
|
122
|
+
return "" if delegated.zero?
|
|
123
|
+
|
|
124
|
+
"\n#{subagents} sub-agent#{"s" unless subagents == 1} reported #{UI.abbreviate(delegated)} " \
|
|
125
|
+
"tokens without splitting them, counted above as ↑."
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# @return [String]
|
|
129
|
+
def header
|
|
130
|
+
cell("Agent", :name) + %i[invocations subagents up down seconds]
|
|
131
|
+
.zip(["runs", "sub-agents", "↑", "↓", "time"])
|
|
132
|
+
.map { |key, text| right(text, key) }.join
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# @return [String]
|
|
136
|
+
def rule = WIDTHS.values.map { |width| ("─" * (width - 1)) + " " }.join
|
|
137
|
+
|
|
138
|
+
# @param row [OpenLanes::Engine::Tally::Row]
|
|
139
|
+
# @return [String]
|
|
140
|
+
def line(row)
|
|
141
|
+
cell(row.name, :name) + right(row.invocations.to_s, :invocations) +
|
|
142
|
+
right(row.subagents.to_s, :subagents) + right(UI.abbreviate(row.up), :up) +
|
|
143
|
+
right(UI.abbreviate(row.down), :down) + right(duration(row.seconds), :seconds)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# @return [String]
|
|
147
|
+
def totals
|
|
148
|
+
cell("", :name) + right(attempts.size.to_s, :invocations) + right(subagents.to_s, :subagents) +
|
|
149
|
+
right(UI.abbreviate(up), :up) + right(UI.abbreviate(down), :down) + right(duration(busy), :seconds)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# @param text [String]
|
|
153
|
+
# @param key [Symbol]
|
|
154
|
+
# @return [String] padded to its column, left-justified
|
|
155
|
+
def cell(text, key) = UI.fit(text, WIDTHS.fetch(key))
|
|
156
|
+
|
|
157
|
+
# @param text [String]
|
|
158
|
+
# @param key [Symbol]
|
|
159
|
+
# @return [String] padded to its column, right-justified
|
|
160
|
+
def right(text, key)
|
|
161
|
+
width = WIDTHS.fetch(key) - 1
|
|
162
|
+
text = UI.fit(text, width).rstrip
|
|
163
|
+
UI.fit("", width - UI.display_width(text)) + text + " "
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# @param seconds [Numeric]
|
|
167
|
+
# @return [String] e.g. "42s", "12m40s", "1h04m"
|
|
168
|
+
def duration(seconds)
|
|
169
|
+
total = seconds.to_i
|
|
170
|
+
return "#{total}s" if total < 60
|
|
171
|
+
return "#{total / 60}m#{format("%02ds", total % 60)}" if total < 3600
|
|
172
|
+
|
|
173
|
+
"#{total / 3600}h#{format("%02dm", (total % 3600) / 60)}"
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
# The run loop: which agent works which plan, in what order, until a round
|
|
5
|
+
# changes nothing. It decides; {Execution} carries the decision out.
|
|
6
|
+
module Engine
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# @return [OpenLanes::Engine::Runner]
|
|
10
|
+
def runner(**) = Runner.new(**)
|
|
11
|
+
|
|
12
|
+
# @param tree [OpenLanes::Plans::Tree]
|
|
13
|
+
# @return [OpenLanes::Engine::StateFile]
|
|
14
|
+
def state_file(tree) = StateFile.new(path: StateFile.for(tree))
|
|
15
|
+
|
|
16
|
+
# @return [OpenLanes::Engine::Tally]
|
|
17
|
+
def tally(**) = Tally.new(**)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module OpenLanes
|
|
6
|
+
module Evals
|
|
7
|
+
# One eval: a fixture plan, the agent given it, the depth asked for, and
|
|
8
|
+
# what the folder must look like afterwards.
|
|
9
|
+
#
|
|
10
|
+
# Validation refuses rather than ignores. A misspelt expectation would
|
|
11
|
+
# otherwise be a check that silently never runs, and an eval that passes
|
|
12
|
+
# because it checked nothing is worse than no eval at all.
|
|
13
|
+
#
|
|
14
|
+
# @!attribute [r] id
|
|
15
|
+
# @return [String] the file's basename, unique within its agent
|
|
16
|
+
# @!attribute [r] agent
|
|
17
|
+
# @return [String] an `agents/*.md` name
|
|
18
|
+
# @!attribute [r] depth
|
|
19
|
+
# @return [Symbol] one of {OpenLanes::Plans::Spec::DEPTHS}
|
|
20
|
+
# @!attribute [r] description
|
|
21
|
+
# @return [String]
|
|
22
|
+
# @!attribute [r] fixture
|
|
23
|
+
# @return [OpenLanes::Evals::Case::Fixture]
|
|
24
|
+
# @!attribute [r] expect
|
|
25
|
+
# @return [Hash{String => Object}] as written, string keys
|
|
26
|
+
# @!attribute [r] path
|
|
27
|
+
# @return [String] the YAML file
|
|
28
|
+
Case = Data.define(:id, :agent, :depth, :description, :fixture, :expect, :path) do
|
|
29
|
+
# The same case held to another depth's bounds.
|
|
30
|
+
#
|
|
31
|
+
# @param depth [Symbol, String, nil] nil keeps the case's own
|
|
32
|
+
# @return [OpenLanes::Evals::Case]
|
|
33
|
+
def at(depth) = depth.nil? ? self : with(depth: Case.depth!(depth, path))
|
|
34
|
+
|
|
35
|
+
# The bounds for {#depth}, or for the one given. A depth the case says
|
|
36
|
+
# nothing about has no bounds, so only the outcome checks apply.
|
|
37
|
+
#
|
|
38
|
+
# @param level [Symbol]
|
|
39
|
+
# @return [Hash{String => Object}]
|
|
40
|
+
def bounds(level = depth) = expect.fetch("depth", {}).fetch(level.to_s, {})
|
|
41
|
+
|
|
42
|
+
# @return [String] where this case keeps its recordings
|
|
43
|
+
def dir = path.delete_suffix(File.extname(path))
|
|
44
|
+
|
|
45
|
+
# @param kind [Symbol, String] :pass or :fail
|
|
46
|
+
# @return [OpenLanes::Evals::Recording, nil]
|
|
47
|
+
def recording(kind) = Recording.at(File.join(dir, kind.to_s))
|
|
48
|
+
|
|
49
|
+
# @return [Array<OpenLanes::Evals::Check>] every check, outcome first
|
|
50
|
+
def checks = Check.for(self)
|
|
51
|
+
|
|
52
|
+
# @return [String] `agent/id`, how the CLI names a case
|
|
53
|
+
def to_s = "#{agent}/#{id}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
class Case
|
|
57
|
+
# What the plan folder, and the repository around it, start as.
|
|
58
|
+
#
|
|
59
|
+
# @!attribute [r] state
|
|
60
|
+
# @return [Symbol] a status key
|
|
61
|
+
# @!attribute [r] slug
|
|
62
|
+
# @return [String]
|
|
63
|
+
# @!attribute [r] files
|
|
64
|
+
# @return [Hash{String => String}] plan-folder file to content
|
|
65
|
+
# @!attribute [r] repo
|
|
66
|
+
# @return [Hash{String => String}] repository path to content
|
|
67
|
+
Fixture = Data.define(:state, :slug, :files, :repo)
|
|
68
|
+
|
|
69
|
+
TOP = %w[id agent depth description fixture expect].freeze
|
|
70
|
+
FIXTURE = %w[state slug files repo].freeze
|
|
71
|
+
EXPECT = %w[state signed files_exist files_absent sections contains excludes changed_paths untouched depth].freeze
|
|
72
|
+
BOUNDS = %w[words count changed_files changed_lines max_seconds max_tokens].freeze
|
|
73
|
+
|
|
74
|
+
# What each bound may carry. `words` and `count` take one mapping or a
|
|
75
|
+
# list of them, so a case can bound two sections of one file.
|
|
76
|
+
BOUND_KEYS = {
|
|
77
|
+
"words" => %w[file section min max],
|
|
78
|
+
"count" => %w[file section pattern min max],
|
|
79
|
+
"changed_files" => %w[min max],
|
|
80
|
+
"changed_lines" => %w[min max]
|
|
81
|
+
}.freeze
|
|
82
|
+
|
|
83
|
+
class << self
|
|
84
|
+
# @param dir [String]
|
|
85
|
+
# @param registry [OpenLanes::Agents::Registry]
|
|
86
|
+
# @return [Array<OpenLanes::Evals::Case>]
|
|
87
|
+
def all(dir, registry:)
|
|
88
|
+
Dir.glob(File.join(File.expand_path(dir), "*", "*.yml")).map { |path| load(path, registry:) }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# @param path [String]
|
|
92
|
+
# @param registry [OpenLanes::Agents::Registry]
|
|
93
|
+
# @return [OpenLanes::Evals::Case]
|
|
94
|
+
# @raise [OpenLanes::Evals::Invalid]
|
|
95
|
+
def load(path, registry:)
|
|
96
|
+
data = YAML.safe_load_file(path)
|
|
97
|
+
refuse(path, "is not a mapping") unless data.is_a?(Hash)
|
|
98
|
+
|
|
99
|
+
from(data, path:, registry:)
|
|
100
|
+
rescue Psych::Exception => e
|
|
101
|
+
refuse(path, "is not valid YAML (#{e.message.lines.first.strip})")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @param data [Hash]
|
|
105
|
+
# @param path [String]
|
|
106
|
+
# @param registry [OpenLanes::Agents::Registry]
|
|
107
|
+
# @return [OpenLanes::Evals::Case]
|
|
108
|
+
def from(data, path:, registry:)
|
|
109
|
+
unknown(path, data, TOP, "")
|
|
110
|
+
id = data["id"].to_s
|
|
111
|
+
refuse(path, "id #{id.inspect} must match the file name") unless id == File.basename(path, ".yml")
|
|
112
|
+
agent = data["agent"].to_s
|
|
113
|
+
refuse(path, "names no agent called #{agent.inspect}") unless registry.find(agent)
|
|
114
|
+
refuse(path, "sits under #{File.basename(File.dirname(path))}/ but names #{agent}") unless File.basename(File.dirname(path)) == agent
|
|
115
|
+
|
|
116
|
+
new(id:,
|
|
117
|
+
agent:,
|
|
118
|
+
depth: depth!(data["depth"], path),
|
|
119
|
+
description: data["description"].to_s.strip,
|
|
120
|
+
fixture: fixture(data["fixture"], path),
|
|
121
|
+
expect: expectations(data["expect"], path),
|
|
122
|
+
path:)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# @param value [Object]
|
|
126
|
+
# @param path [String]
|
|
127
|
+
# @return [Symbol]
|
|
128
|
+
def depth!(value, path)
|
|
129
|
+
depth = value.to_s.strip.downcase.to_sym
|
|
130
|
+
return depth if Plans::Spec::DEPTHS.include?(depth)
|
|
131
|
+
|
|
132
|
+
refuse(path, "depth #{value.inspect} is not one of #{Plans::Spec::DEPTHS.join(", ")}")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
# @return [OpenLanes::Evals::Case::Fixture]
|
|
138
|
+
def fixture(data, path)
|
|
139
|
+
refuse(path, "has no fixture") unless data.is_a?(Hash)
|
|
140
|
+
unknown(path, data, FIXTURE, "fixture.")
|
|
141
|
+
state = Plans.status(data["state"].to_s) or refuse(path, "fixture.state #{data["state"].inspect} is not a status")
|
|
142
|
+
files = files(data.fetch("files", {}), path, "fixture.files")
|
|
143
|
+
refuse(path, "fixture.files has no spec.md") unless files.key?(Plans::Spec::FILENAME)
|
|
144
|
+
|
|
145
|
+
Fixture.new(state: state.key,
|
|
146
|
+
slug: data.fetch("slug", File.basename(path, ".yml")).to_s,
|
|
147
|
+
files:,
|
|
148
|
+
repo: files(data.fetch("repo", {}), path, "fixture.repo"))
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# @return [Hash{String => String}]
|
|
152
|
+
def files(data, path, where)
|
|
153
|
+
refuse(path, "#{where} must map file names to contents") unless data.is_a?(Hash)
|
|
154
|
+
data.to_h { |name, body| [name.to_s, body.to_s] }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# @return [Hash]
|
|
158
|
+
def expectations(data, path)
|
|
159
|
+
refuse(path, "has no expect") unless data.is_a?(Hash)
|
|
160
|
+
unknown(path, data, EXPECT, "expect.")
|
|
161
|
+
if data.key?("state") && !Plans.status(data["state"].to_s)
|
|
162
|
+
refuse(path, "expect.state #{data["state"].inspect} is not a status")
|
|
163
|
+
end
|
|
164
|
+
depth = data.fetch("depth", {})
|
|
165
|
+
refuse(path, "expect.depth must map fast, medium or deep to bounds") unless depth.is_a?(Hash)
|
|
166
|
+
depth.each do |level, bounds|
|
|
167
|
+
depth!(level, path)
|
|
168
|
+
refuse(path, "expect.depth.#{level} must be a mapping") unless bounds.is_a?(Hash)
|
|
169
|
+
unknown(path, bounds, BOUNDS, "expect.depth.#{level}.")
|
|
170
|
+
bounds.slice(*BOUND_KEYS.keys).each do |name, value|
|
|
171
|
+
Array(value.is_a?(Hash) ? [value] : value).each do |bound|
|
|
172
|
+
refuse(path, "expect.depth.#{level}.#{name} must be a mapping") unless bound.is_a?(Hash)
|
|
173
|
+
unknown(path, bound, BOUND_KEYS.fetch(name), "expect.depth.#{level}.#{name}.")
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
data
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# @return [void]
|
|
181
|
+
def unknown(path, data, allowed, prefix)
|
|
182
|
+
extra = data.keys.map(&:to_s) - allowed
|
|
183
|
+
refuse(path, "#{extra.map { |k| "#{prefix}#{k}" }.join(", ")}: unknown, expected one of #{allowed.join(", ")}") unless extra.empty?
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# @return [void] never returns
|
|
187
|
+
def refuse(path, message) = raise(Invalid, "#{path}: #{message}")
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Evals
|
|
5
|
+
# What an agent changed in the repository: which paths, and how many lines.
|
|
6
|
+
#
|
|
7
|
+
# Computed by comparing the fixture's files with a directory, not by asking
|
|
8
|
+
# git, so a recording on disk and a live checkout are measured by the same
|
|
9
|
+
# code. Lines are counted as a multiset difference, added plus removed,
|
|
10
|
+
# which is what a reviewer reads as the size of a diff without needing one.
|
|
11
|
+
#
|
|
12
|
+
# @!attribute [r] paths
|
|
13
|
+
# @return [Array<String>] relative, sorted
|
|
14
|
+
# @!attribute [r] lines
|
|
15
|
+
# @return [Integer] lines added plus lines removed
|
|
16
|
+
Changes = Data.define(:paths, :lines) do
|
|
17
|
+
# @param glob [String]
|
|
18
|
+
# @return [Array<String>] changed paths matching it
|
|
19
|
+
def matching(glob) = paths.select { |p| Changes.match?(glob, p) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class Changes
|
|
23
|
+
# Never part of the diff: git's own bookkeeping and the plans, which the
|
|
24
|
+
# plan-folder checks already cover.
|
|
25
|
+
IGNORED = [".git", OpenLanes::PLANS_DIR].freeze
|
|
26
|
+
|
|
27
|
+
FNMATCH = File::FNM_PATHNAME | File::FNM_EXTGLOB | File::FNM_DOTMATCH
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
# @param before [Hash{String => String}] the fixture's repository
|
|
31
|
+
# @param dir [String] the repository afterwards
|
|
32
|
+
# @return [OpenLanes::Evals::Changes]
|
|
33
|
+
def between(before, dir)
|
|
34
|
+
after = snapshot(dir)
|
|
35
|
+
paths = (before.keys | after.keys).reject { |p| before[p] == after[p] }.sort
|
|
36
|
+
lines = paths.sum { |p| line_delta(before[p].to_s, after[p].to_s) }
|
|
37
|
+
new(paths:, lines:)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @param glob [String]
|
|
41
|
+
# @param path [String]
|
|
42
|
+
# @return [Boolean]
|
|
43
|
+
def match?(glob, path) = File.fnmatch?(glob, path, FNMATCH)
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# @return [Hash{String => String}]
|
|
48
|
+
def snapshot(dir)
|
|
49
|
+
root = File.expand_path(dir)
|
|
50
|
+
Dir.glob("**/*", File::FNM_DOTMATCH, base: root).each_with_object({}) do |rel, out|
|
|
51
|
+
next if IGNORED.include?(rel.split("/").first)
|
|
52
|
+
|
|
53
|
+
full = File.join(root, rel)
|
|
54
|
+
out[rel] = File.read(full, encoding: "UTF-8") if File.file?(full)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @return [Integer]
|
|
59
|
+
def line_delta(old, new)
|
|
60
|
+
a = old.lines.tally
|
|
61
|
+
b = new.lines.tally
|
|
62
|
+
(a.keys | b.keys).sum { |line| (a.fetch(line, 0) - b.fetch(line, 0)).abs }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenLanes
|
|
4
|
+
module Evals
|
|
5
|
+
# One deterministic question about a finished folder.
|
|
6
|
+
#
|
|
7
|
+
# Two families, matching the two things D8 asks of every agent:
|
|
8
|
+
#
|
|
9
|
+
# outcome — state, signed, files, sections, contents, changed paths:
|
|
10
|
+
# did it finish the task?
|
|
11
|
+
# depth — words, counts, diff size, seconds, tokens, bounded per
|
|
12
|
+
# depth: did it work at the level it was asked for? Too
|
|
13
|
+
# shallow for deep fails, and so does overdone for fast.
|
|
14
|
+
#
|
|
15
|
+
# @!attribute [r] kind
|
|
16
|
+
# @return [Symbol] which judge answers it
|
|
17
|
+
# @!attribute [r] name
|
|
18
|
+
# @return [String] how a report names it
|
|
19
|
+
# @!attribute [r] args
|
|
20
|
+
# @return [Hash{Symbol => Object}] what the judge is given
|
|
21
|
+
Check = Data.define(:kind, :name, :args) do
|
|
22
|
+
# @param evidence [OpenLanes::Evals::Evidence]
|
|
23
|
+
# @return [OpenLanes::Evals::Verdict]
|
|
24
|
+
def call(evidence)
|
|
25
|
+
ok, detail = Check::Judges.public_send(kind, evidence, **args)
|
|
26
|
+
Verdict.new(name:, ok:, detail:)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class Check
|
|
31
|
+
class << self
|
|
32
|
+
# @param kase [OpenLanes::Evals::Case]
|
|
33
|
+
# @return [Array<OpenLanes::Evals::Check>]
|
|
34
|
+
def for(kase) = outcome(kase.expect) + depth(kase.bounds, kase.depth)
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# @return [Array<OpenLanes::Evals::Check>]
|
|
39
|
+
def outcome(expect)
|
|
40
|
+
list = []
|
|
41
|
+
list << new(kind: :state, name: "state #{expect["state"]}", args: { expected: expect["state"].to_sym }) if expect["state"]
|
|
42
|
+
if expect["signed"]
|
|
43
|
+
note = expect["signed"] == true ? nil : expect["signed"].to_s
|
|
44
|
+
list << new(kind: :signed, name: "signed Completed#{" (#{note})" if note}", args: { note: })
|
|
45
|
+
end
|
|
46
|
+
Array(expect["files_exist"]).each { |f| list << new(kind: :file_exists, name: "has #{f}", args: { file: f }) }
|
|
47
|
+
Array(expect["files_absent"]).each { |f| list << new(kind: :file_absent, name: "no #{f}", args: { file: f }) }
|
|
48
|
+
expect.fetch("sections", {}).each do |file, headings|
|
|
49
|
+
list << new(kind: :sections, name: "sections in #{file}", args: { file:, headings: Array(headings) })
|
|
50
|
+
end
|
|
51
|
+
%w[contains excludes].each do |kind|
|
|
52
|
+
expect.fetch(kind, {}).each do |file, texts|
|
|
53
|
+
Array(texts).each { |text| list << new(kind: kind.to_sym, name: "#{file} #{kind} #{text}", args: { file:, text: }) }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
Array(expect["changed_paths"]).each { |g| list << new(kind: :changed, name: "changed #{g}", args: { glob: g }) }
|
|
57
|
+
Array(expect["untouched"]).each { |g| list << new(kind: :untouched, name: "untouched #{g}", args: { glob: g }) }
|
|
58
|
+
list
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @return [Array<OpenLanes::Evals::Check>]
|
|
62
|
+
def depth(bounds, level)
|
|
63
|
+
list = []
|
|
64
|
+
%w[words count].each do |kind|
|
|
65
|
+
Array(bounds[kind].is_a?(Hash) ? [bounds[kind]] : bounds[kind]).each do |b|
|
|
66
|
+
where = b["section"] ? "#{b["file"]}##{b["section"]}" : b["file"]
|
|
67
|
+
what = kind == "count" ? "count /#{b["pattern"]}/ in #{where}" : "words in #{where}"
|
|
68
|
+
args = { file: b["file"], section: b["section"], min: b["min"], max: b["max"] }
|
|
69
|
+
args[:pattern] = b["pattern"] if kind == "count"
|
|
70
|
+
list << new(kind: kind.to_sym, name: "#{what} #{range(b)} @#{level}", args:)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
%w[changed_files changed_lines].each do |kind|
|
|
74
|
+
next unless (b = bounds[kind])
|
|
75
|
+
|
|
76
|
+
list << new(kind: kind.to_sym, name: "#{kind.tr("_", " ")} #{range(b)} @#{level}", args: { min: b["min"], max: b["max"] })
|
|
77
|
+
end
|
|
78
|
+
list << new(kind: :seconds, name: "seconds ≤ #{bounds["max_seconds"]} @#{level}", args: { max: bounds["max_seconds"] }) if bounds["max_seconds"]
|
|
79
|
+
list << new(kind: :tokens, name: "tokens ≤ #{bounds["max_tokens"]} @#{level}", args: { max: bounds["max_tokens"] }) if bounds["max_tokens"]
|
|
80
|
+
list
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @return [String] `10..40`, `≥ 10` or `≤ 40`
|
|
84
|
+
def range(bound)
|
|
85
|
+
min, max = bound.values_at("min", "max")
|
|
86
|
+
return "#{min}..#{max}" if min && max
|
|
87
|
+
|
|
88
|
+
min ? "≥ #{min}" : "≤ #{max}"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# The answers. Each takes the evidence and its check's arguments and
|
|
93
|
+
# returns `[ok, detail]`; {Check#call} names the verdict.
|
|
94
|
+
module Judges
|
|
95
|
+
module_function
|
|
96
|
+
|
|
97
|
+
# Where the folder ended, and whether that state's own invariant holds
|
|
98
|
+
# of what is in it. A recording that claims 🔎 without a Research
|
|
99
|
+
# chapter fails here exactly as a rename would be refused.
|
|
100
|
+
#
|
|
101
|
+
# @return [Array(Boolean, String)]
|
|
102
|
+
def state(evidence, expected:)
|
|
103
|
+
actual = evidence.state or return [false, "the run recorded no final state"]
|
|
104
|
+
return [false, "ended #{actual}, expected #{expected}"] unless actual == expected
|
|
105
|
+
|
|
106
|
+
status = Plans.status(expected)
|
|
107
|
+
violation = status.violation(evidence.subject_as(status))
|
|
108
|
+
violation ? [false, violation] : [true, status.to_s]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# @return [Array(Boolean, String)]
|
|
112
|
+
def signed(evidence, note:)
|
|
113
|
+
agent = evidence.agent
|
|
114
|
+
entry = Plans::Ledger.last_for(Plans::Ledger.read(evidence.folder, agent.ledger), agent.name)
|
|
115
|
+
return [false, "#{agent.name} signed none of #{agent.ledger.join(", ")}"] unless entry
|
|
116
|
+
return [false, "last entry says #{entry.status}"] unless entry.completed?
|
|
117
|
+
if note && !entry.note.to_s.downcase.include?(note.downcase)
|
|
118
|
+
return [false, "signed Completed (#{entry.note || "no note"}), expected #{note}"]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
[true, "#{entry.file}:#{entry.line}"]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# @return [Array(Boolean, String)]
|
|
125
|
+
def file_exists(evidence, file:)
|
|
126
|
+
File.exist?(File.join(evidence.folder, file)) ? [true, "present"] : [false, "#{file} is missing"]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# @return [Array(Boolean, String)]
|
|
130
|
+
def file_absent(evidence, file:)
|
|
131
|
+
File.exist?(File.join(evidence.folder, file)) ? [false, "#{file} is still there"] : [true, "absent"]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# @return [Array(Boolean, String)]
|
|
135
|
+
def sections(evidence, file:, headings:)
|
|
136
|
+
body = evidence.text(file) or return [false, "#{file} is missing"]
|
|
137
|
+
missing = headings.reject { |h| body.lines.any? { |l| l.match?(Evidence.heading(h)) } }
|
|
138
|
+
missing.empty? ? [true, "all #{headings.size} present"] : [false, "missing #{missing.map { |h| "## #{h}" }.join(", ")}"]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# @return [Array(Boolean, String)]
|
|
142
|
+
def contains(evidence, file:, text:)
|
|
143
|
+
body = evidence.read(file) or return [false, "#{file} is missing"]
|
|
144
|
+
found?(body, text) ? [true, "found"] : [false, "#{file} does not mention #{text}"]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# @return [Array(Boolean, String)]
|
|
148
|
+
def excludes(evidence, file:, text:)
|
|
149
|
+
body = evidence.read(file) or return [true, "#{file} is absent"]
|
|
150
|
+
found?(body, text) ? [false, "#{file} still says #{text}"] : [true, "not found"]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# @return [Array(Boolean, String)]
|
|
154
|
+
def changed(evidence, glob:)
|
|
155
|
+
changes = evidence.changes or return [false, "no repository was recorded"]
|
|
156
|
+
hits = changes.matching(glob)
|
|
157
|
+
hits.empty? ? [false, "nothing matching #{glob} changed"] : [true, hits.join(", ")]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# @return [Array(Boolean, String)]
|
|
161
|
+
def untouched(evidence, glob:)
|
|
162
|
+
changes = evidence.changes or return [false, "no repository was recorded"]
|
|
163
|
+
hits = changes.matching(glob)
|
|
164
|
+
hits.empty? ? [true, "untouched"] : [false, "changed #{hits.join(", ")}"]
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# @return [Array(Boolean, String)]
|
|
168
|
+
def words(evidence, file:, section: nil, min: nil, max: nil)
|
|
169
|
+
body = evidence.text(file, section) or return [false, missing(file, section)]
|
|
170
|
+
within(body.scan(/[[:alnum:]][[:alnum:]'’_-]*/).size, min, max, "words")
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# @return [Array(Boolean, String)]
|
|
174
|
+
def count(evidence, file:, pattern:, section: nil, min: nil, max: nil)
|
|
175
|
+
body = evidence.text(file, section) or return [false, missing(file, section)]
|
|
176
|
+
within(body.scan(Regexp.new(pattern)).size, min, max, "matches")
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# @return [Array(Boolean, String)]
|
|
180
|
+
def changed_files(evidence, min: nil, max: nil)
|
|
181
|
+
changes = evidence.changes or return [false, "no repository was recorded"]
|
|
182
|
+
within(changes.paths.size, min, max, "files changed")
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# @return [Array(Boolean, String)]
|
|
186
|
+
def changed_lines(evidence, min: nil, max: nil)
|
|
187
|
+
changes = evidence.changes or return [false, "no repository was recorded"]
|
|
188
|
+
within(changes.lines, min, max, "lines changed")
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# @return [Array(Boolean, String)]
|
|
192
|
+
def seconds(evidence, max:)
|
|
193
|
+
spent = evidence.run.seconds or return [false, "not measured"]
|
|
194
|
+
within(spent.round, nil, max, "seconds")
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# @return [Array(Boolean, String)]
|
|
198
|
+
def tokens(evidence, max:)
|
|
199
|
+
spent = evidence.run.tokens or return [false, "not measured"]
|
|
200
|
+
within(spent, nil, max, "tokens")
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# @return [Array(Boolean, String)]
|
|
204
|
+
def within(value, min, max, unit)
|
|
205
|
+
return [false, "#{value} #{unit}, fewer than #{min}"] if min && value < min
|
|
206
|
+
return [false, "#{value} #{unit}, more than #{max}"] if max && value > max
|
|
207
|
+
|
|
208
|
+
[true, "#{value} #{unit}"]
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# `/.../` is a pattern; anything else is a literal.
|
|
212
|
+
#
|
|
213
|
+
# @return [Boolean]
|
|
214
|
+
def found?(body, text)
|
|
215
|
+
text = text.to_s
|
|
216
|
+
text.length > 2 && text.start_with?("/") && text.end_with?("/") ? body.match?(Regexp.new(text[1..-2])) : body.include?(text)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# @return [String]
|
|
220
|
+
def missing(file, section) = section ? "#{file} has no ## #{section}" : "#{file} is missing"
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|