Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .github/ISSUE_TEMPLATE/bug_report.yml +117 -0
- .github/ISSUE_TEMPLATE/config.yml +11 -0
- .github/ISSUE_TEMPLATE/documentation.yml +100 -0
- .github/ISSUE_TEMPLATE/feature_request.yml +155 -0
- .github/ISSUE_TEMPLATE/performance_issue.yml +150 -0
- .github/ISSUE_TEMPLATE/provider_integration.yml +129 -0
- .github/contribution.md +69 -0
- .github/dependabot.yml +26 -0
- .github/labels.json +349 -0
- .github/release-drafter.yml +132 -0
- .github/scripts/bounty/README.md +155 -0
- .github/scripts/bounty/src/api.ts +110 -0
- .github/scripts/bounty/src/rules.ts +187 -0
- .github/scripts/bounty/src/sync-all-issues.ts +77 -0
- .github/scripts/bounty/src/sync-issue.ts +133 -0
- .github/scripts/bounty/src/sync-pr.ts +82 -0
- .github/scripts/bounty/src/types.ts +82 -0
- .github/scripts/bounty/tests/rules.test.ts +369 -0
- .github/scripts/bounty/tests/sync-all-issues.test.ts +138 -0
- .github/scripts/bounty/tests/sync-issue.test.ts +144 -0
- .github/scripts/bounty/tests/sync-pr.test.ts +241 -0
- .github/workflows/autofix.yml +62 -0
- .github/workflows/bounty.yml +64 -0
- .github/workflows/ci.yml +296 -0
- .github/workflows/labels.yml +39 -0
- .github/workflows/release-drafter.yml +53 -0
- .github/workflows/release.yml +155 -0
- .github/workflows/stale.yml +51 -0
- benchmarks/README.md +320 -0
- benchmarks/cli.ts +394 -0
- benchmarks/command-generator.ts +98 -0
- benchmarks/evals/echo/task.yml +26 -0
- benchmarks/evals/multi_file_patch/multi_file_patch_tasks.csv +5 -0
- benchmarks/evals/multi_file_patch/task.yml +21 -0
- benchmarks/evals/patch_exact_match/patch_exact_match_tasks.csv +7 -0
- benchmarks/evals/patch_exact_match/task.yml +217 -0
- benchmarks/evals/redundant_cd_with_cwd/task.yml +45 -0
- benchmarks/evals/search_over_find/task.yml +58 -0
- benchmarks/evals/sem_search/README.md +33 -0
- benchmarks/evals/sem_search/task.yml +138 -0
- benchmarks/evals/semantic_search_quality/README.md +197 -0
- benchmarks/evals/semantic_search_quality/llm_judge.ts +400 -0
- benchmarks/evals/semantic_search_quality/run_eval.sh +95 -0
- benchmarks/evals/semantic_search_quality/test_context.json +22 -0
- benchmarks/evals/semantic_search_quality/test_queries.ts +232 -0
- benchmarks/evals/suggest/task.yml +47 -0
- benchmarks/model.ts +38 -0
- benchmarks/parse.ts +52 -0
- benchmarks/task-executor.ts +184 -0
- benchmarks/tsconfig.json +47 -0
.github/ISSUE_TEMPLATE/bug_report.yml
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: 🐛 Bug Report
|
| 2 |
+
description: Report a bug or unexpected behavior in Forge
|
| 3 |
+
title: "[Bug]: "
|
| 4 |
+
labels: ["type: bug"]
|
| 5 |
+
body:
|
| 6 |
+
- type: markdown
|
| 7 |
+
attributes:
|
| 8 |
+
value: |
|
| 9 |
+
Thanks for taking the time to report a bug! Please fill out the sections below to help us reproduce and fix the issue.
|
| 10 |
+
|
| 11 |
+
- type: textarea
|
| 12 |
+
id: description
|
| 13 |
+
attributes:
|
| 14 |
+
label: Bug Description
|
| 15 |
+
description: A clear and concise description of what the bug is.
|
| 16 |
+
placeholder: Describe the bug...
|
| 17 |
+
validations:
|
| 18 |
+
required: true
|
| 19 |
+
|
| 20 |
+
- type: textarea
|
| 21 |
+
id: reproduction
|
| 22 |
+
attributes:
|
| 23 |
+
label: Steps to Reproduce
|
| 24 |
+
description: Steps to reproduce the behavior
|
| 25 |
+
placeholder: |
|
| 26 |
+
1. Run command '...'
|
| 27 |
+
2. Enter prompt '...'
|
| 28 |
+
3. See error
|
| 29 |
+
validations:
|
| 30 |
+
required: true
|
| 31 |
+
|
| 32 |
+
- type: textarea
|
| 33 |
+
id: expected
|
| 34 |
+
attributes:
|
| 35 |
+
label: Expected Behavior
|
| 36 |
+
description: What did you expect to happen?
|
| 37 |
+
placeholder: Describe what should happen...
|
| 38 |
+
validations:
|
| 39 |
+
required: true
|
| 40 |
+
|
| 41 |
+
- type: textarea
|
| 42 |
+
id: actual
|
| 43 |
+
attributes:
|
| 44 |
+
label: Actual Behavior
|
| 45 |
+
description: What actually happened?
|
| 46 |
+
placeholder: Describe what actually happened...
|
| 47 |
+
validations:
|
| 48 |
+
required: true
|
| 49 |
+
|
| 50 |
+
- type: input
|
| 51 |
+
id: version
|
| 52 |
+
attributes:
|
| 53 |
+
label: Forge Version
|
| 54 |
+
description: What version of Forge are you running? (`forge --version`)
|
| 55 |
+
placeholder: e.g., 0.1.0
|
| 56 |
+
validations:
|
| 57 |
+
required: true
|
| 58 |
+
|
| 59 |
+
- type: input
|
| 60 |
+
id: os
|
| 61 |
+
attributes:
|
| 62 |
+
label: Operating System & Version
|
| 63 |
+
description: What operating system and version are you using?
|
| 64 |
+
placeholder: e.g., macOS 14.1, Ubuntu 22.04, Windows 11
|
| 65 |
+
validations:
|
| 66 |
+
required: false
|
| 67 |
+
|
| 68 |
+
- type: dropdown
|
| 69 |
+
id: provider
|
| 70 |
+
attributes:
|
| 71 |
+
label: AI Provider
|
| 72 |
+
description: Which AI provider are you using?
|
| 73 |
+
options:
|
| 74 |
+
- OpenAI
|
| 75 |
+
- Anthropic
|
| 76 |
+
- OpenRouter
|
| 77 |
+
- Google Vertex AI
|
| 78 |
+
- Cerebras
|
| 79 |
+
- x-ai
|
| 80 |
+
- z.ai
|
| 81 |
+
- Requesty
|
| 82 |
+
- IO Intelligence
|
| 83 |
+
- Groq
|
| 84 |
+
- Amazon Bedrock
|
| 85 |
+
- OpenAI-compatible
|
| 86 |
+
- Other
|
| 87 |
+
validations:
|
| 88 |
+
required: false
|
| 89 |
+
|
| 90 |
+
- type: input
|
| 91 |
+
id: model
|
| 92 |
+
attributes:
|
| 93 |
+
label: Model
|
| 94 |
+
description: Which model are you using?
|
| 95 |
+
placeholder: e.g., claude-3.7-sonnet, gpt-4, o3-mini-high
|
| 96 |
+
|
| 97 |
+
- type: dropdown
|
| 98 |
+
id: installation
|
| 99 |
+
attributes:
|
| 100 |
+
label: Installation Method
|
| 101 |
+
description: How did you install Forge?
|
| 102 |
+
options:
|
| 103 |
+
- npx forgecode@latest
|
| 104 |
+
- npm install -g forgecode
|
| 105 |
+
- curl -fsSL https://forgecode.dev/cli | sh
|
| 106 |
+
- Built from source
|
| 107 |
+
- Other
|
| 108 |
+
validations:
|
| 109 |
+
required: true
|
| 110 |
+
|
| 111 |
+
- type: textarea
|
| 112 |
+
id: config
|
| 113 |
+
attributes:
|
| 114 |
+
label: Configuration
|
| 115 |
+
description: |
|
| 116 |
+
Please share relevant parts of your `forge.yaml` configuration (remove sensitive data like API keys).
|
| 117 |
+
render: yaml
|
.github/ISSUE_TEMPLATE/config.yml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
blank_issues_enabled: false
|
| 2 |
+
contact_links:
|
| 3 |
+
- name: 💬 Discord Community
|
| 4 |
+
url: https://discord.gg/kRZBPpkgwq
|
| 5 |
+
about: Join our Discord server for questions, discussions, and community support
|
| 6 |
+
- name: 📖 Documentation
|
| 7 |
+
url: https://forgecode.dev/docs/
|
| 8 |
+
about: Check out our comprehensive documentation
|
| 9 |
+
- name: ❓ Discussions
|
| 10 |
+
url: https://github.com/antinomyhq/forge/discussions
|
| 11 |
+
about: Ask questions and discuss ideas with the community
|
.github/ISSUE_TEMPLATE/documentation.yml
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: 📚 Documentation Issue
|
| 2 |
+
description: Report problems with documentation or suggest improvements
|
| 3 |
+
title: "[Docs]: "
|
| 4 |
+
labels: ["type: docs"]
|
| 5 |
+
body:
|
| 6 |
+
- type: markdown
|
| 7 |
+
attributes:
|
| 8 |
+
value: |
|
| 9 |
+
Thanks for helping improve our documentation! Clear docs help everyone.
|
| 10 |
+
|
| 11 |
+
- type: dropdown
|
| 12 |
+
id: issue-type
|
| 13 |
+
attributes:
|
| 14 |
+
label: Documentation Issue Type
|
| 15 |
+
description: What type of documentation issue is this?
|
| 16 |
+
options:
|
| 17 |
+
- Missing documentation
|
| 18 |
+
- Incorrect/outdated information
|
| 19 |
+
- Unclear explanation
|
| 20 |
+
- Broken link
|
| 21 |
+
- Typo/grammar
|
| 22 |
+
- Code example doesn't work
|
| 23 |
+
- Need more examples
|
| 24 |
+
- Need better organization
|
| 25 |
+
- Other
|
| 26 |
+
validations:
|
| 27 |
+
required: true
|
| 28 |
+
|
| 29 |
+
- type: input
|
| 30 |
+
id: location
|
| 31 |
+
attributes:
|
| 32 |
+
label: Documentation Location
|
| 33 |
+
description: Where is the documentation issue? (URL, file path, or section name)
|
| 34 |
+
placeholder: https://github.com/antinomyhq/forge/blob/main/docs/...
|
| 35 |
+
validations:
|
| 36 |
+
required: true
|
| 37 |
+
|
| 38 |
+
- type: textarea
|
| 39 |
+
id: current-content
|
| 40 |
+
attributes:
|
| 41 |
+
label: Current Content
|
| 42 |
+
description: What does the current documentation say? (Quote or describe)
|
| 43 |
+
placeholder: Currently, the documentation says...
|
| 44 |
+
|
| 45 |
+
- type: textarea
|
| 46 |
+
id: issue-description
|
| 47 |
+
attributes:
|
| 48 |
+
label: Issue Description
|
| 49 |
+
description: What is wrong or confusing about the current documentation?
|
| 50 |
+
placeholder: The issue is that...
|
| 51 |
+
validations:
|
| 52 |
+
required: true
|
| 53 |
+
|
| 54 |
+
- type: textarea
|
| 55 |
+
id: suggested-improvement
|
| 56 |
+
attributes:
|
| 57 |
+
label: Suggested Improvement
|
| 58 |
+
description: How would you improve this documentation?
|
| 59 |
+
placeholder: It would be better if...
|
| 60 |
+
validations:
|
| 61 |
+
required: true
|
| 62 |
+
|
| 63 |
+
- type: dropdown
|
| 64 |
+
id: audience
|
| 65 |
+
attributes:
|
| 66 |
+
label: Target Audience
|
| 67 |
+
description: Who would benefit from this documentation improvement?
|
| 68 |
+
options:
|
| 69 |
+
- New users/beginners
|
| 70 |
+
- Experienced users
|
| 71 |
+
- Contributors/developers
|
| 72 |
+
- All users
|
| 73 |
+
validations:
|
| 74 |
+
required: true
|
| 75 |
+
|
| 76 |
+
- type: textarea
|
| 77 |
+
id: examples
|
| 78 |
+
attributes:
|
| 79 |
+
label: Example or Reference
|
| 80 |
+
description: Provide examples or references that could help improve the documentation
|
| 81 |
+
placeholder: |
|
| 82 |
+
For example:
|
| 83 |
+
```yaml
|
| 84 |
+
# forge.yaml example
|
| 85 |
+
model: claude-3.7-sonnet
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
- type: checkboxes
|
| 89 |
+
id: contribution
|
| 90 |
+
attributes:
|
| 91 |
+
label: Contribution
|
| 92 |
+
description: Can you help improve this documentation?
|
| 93 |
+
options:
|
| 94 |
+
- label: I'm willing to submit a PR with the documentation improvements
|
| 95 |
+
|
| 96 |
+
- type: textarea
|
| 97 |
+
id: additional
|
| 98 |
+
attributes:
|
| 99 |
+
label: Additional Context
|
| 100 |
+
description: Any other information that would help improve the documentation
|
.github/ISSUE_TEMPLATE/feature_request.yml
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: ✨ Feature Request
|
| 2 |
+
description: Suggest a new feature or enhancement for Forge
|
| 3 |
+
title: "[Feature]: "
|
| 4 |
+
labels: ["type: feature"]
|
| 5 |
+
body:
|
| 6 |
+
- type: markdown
|
| 7 |
+
attributes:
|
| 8 |
+
value: |
|
| 9 |
+
Thanks for suggesting a new feature!
|
| 10 |
+
|
| 11 |
+
**Before you continue, please ensure you can clearly answer:**
|
| 12 |
+
1. **Why do you NEED this feature?** (Not just "nice to have")
|
| 13 |
+
2. **What is NOT possible right now?** (Current limitations)
|
| 14 |
+
3. **What WILL be possible with this feature?** (Concrete outcomes)
|
| 15 |
+
|
| 16 |
+
Vague or incomplete requests will be closed. Take your time to write a compelling case.
|
| 17 |
+
|
| 18 |
+
- type: textarea
|
| 19 |
+
id: why-needed
|
| 20 |
+
attributes:
|
| 21 |
+
label: "1. Why Do You NEED This Feature?"
|
| 22 |
+
description: |
|
| 23 |
+
**Be specific.** Explain the real-world problem or blocker you're facing.
|
| 24 |
+
Not "it would be cool if..." but "I cannot accomplish X because..."
|
| 25 |
+
Why is this important to your workflow? What is the impact of NOT having this?
|
| 26 |
+
placeholder: |
|
| 27 |
+
I NEED this feature because:
|
| 28 |
+
- Current workflow requires me to...
|
| 29 |
+
- This costs me X hours per week...
|
| 30 |
+
- Without this, I cannot accomplish...
|
| 31 |
+
- The business impact is...
|
| 32 |
+
validations:
|
| 33 |
+
required: true
|
| 34 |
+
|
| 35 |
+
- type: textarea
|
| 36 |
+
id: current-limitation
|
| 37 |
+
attributes:
|
| 38 |
+
label: "2. What Is NOT Possible Right Now?"
|
| 39 |
+
description: |
|
| 40 |
+
**Describe the current limitations in detail.**
|
| 41 |
+
What have you tried? What doesn't work? Why can't you work around it?
|
| 42 |
+
Include specific examples, commands, or workflows that fail.
|
| 43 |
+
placeholder: |
|
| 44 |
+
Currently, I cannot:
|
| 45 |
+
- [Specific action/workflow that fails]
|
| 46 |
+
- [What happens when you try]
|
| 47 |
+
- [Why existing features don't solve this]
|
| 48 |
+
|
| 49 |
+
I've tried:
|
| 50 |
+
- [Workaround 1 and why it doesn't work]
|
| 51 |
+
- [Workaround 2 and why it doesn't work]
|
| 52 |
+
validations:
|
| 53 |
+
required: true
|
| 54 |
+
|
| 55 |
+
- type: textarea
|
| 56 |
+
id: future-possibilities
|
| 57 |
+
attributes:
|
| 58 |
+
label: "3. What WILL Be Possible With This Feature?"
|
| 59 |
+
description: |
|
| 60 |
+
**Describe concrete outcomes and capabilities.**
|
| 61 |
+
What specific tasks/workflows will this enable?
|
| 62 |
+
How will your work change? What new things can you build/do?
|
| 63 |
+
placeholder: |
|
| 64 |
+
With this feature, I will be able to:
|
| 65 |
+
- [Specific new capability 1]
|
| 66 |
+
- [Specific new capability 2]
|
| 67 |
+
- [Measurable improvement: time saved, errors reduced, etc.]
|
| 68 |
+
|
| 69 |
+
Example workflow:
|
| 70 |
+
```
|
| 71 |
+
# Show the exact commands or steps you'll use
|
| 72 |
+
forge new-feature --example
|
| 73 |
+
```
|
| 74 |
+
validations:
|
| 75 |
+
required: true
|
| 76 |
+
|
| 77 |
+
- type: textarea
|
| 78 |
+
id: solution
|
| 79 |
+
attributes:
|
| 80 |
+
label: Proposed Solution (User Experience)
|
| 81 |
+
description: |
|
| 82 |
+
**Focus on the user experience, not implementation details.**
|
| 83 |
+
How would a user interact with this feature? What would they see/do?
|
| 84 |
+
Focus on what the user experiences, not how it would be built.
|
| 85 |
+
placeholder: |
|
| 86 |
+
From a user's perspective, the feature should work like this:
|
| 87 |
+
- When I [action], I would see/get [result]
|
| 88 |
+
- The interaction would be: [step-by-step user flow]
|
| 89 |
+
- Example: "When I run `forge search docs`, I'd see matching results from Google Workspace"
|
| 90 |
+
validations:
|
| 91 |
+
required: true
|
| 92 |
+
|
| 93 |
+
- type: textarea
|
| 94 |
+
id: alternatives
|
| 95 |
+
attributes:
|
| 96 |
+
label: Alternatives Considered
|
| 97 |
+
description: Have you considered any alternative solutions or workarounds?
|
| 98 |
+
placeholder: I've also thought about...
|
| 99 |
+
|
| 100 |
+
- type: dropdown
|
| 101 |
+
id: category
|
| 102 |
+
attributes:
|
| 103 |
+
label: Feature Category
|
| 104 |
+
description: Which area does this feature relate to?
|
| 105 |
+
options:
|
| 106 |
+
- AI/LLM Integration
|
| 107 |
+
- Code Generation/Editing
|
| 108 |
+
- File Operations
|
| 109 |
+
- Configuration
|
| 110 |
+
- CLI/UX
|
| 111 |
+
- Documentation
|
| 112 |
+
- Performance
|
| 113 |
+
- Security
|
| 114 |
+
- MCP Integration
|
| 115 |
+
- Multi-Agent Workflows
|
| 116 |
+
- Shell Plugin
|
| 117 |
+
- Testing
|
| 118 |
+
- Other
|
| 119 |
+
validations:
|
| 120 |
+
required: true
|
| 121 |
+
|
| 122 |
+
- type: dropdown
|
| 123 |
+
id: priority
|
| 124 |
+
attributes:
|
| 125 |
+
label: Priority/Impact
|
| 126 |
+
description: How important is this feature to your workflow?
|
| 127 |
+
options:
|
| 128 |
+
- Critical - Blocking my work
|
| 129 |
+
- High - Would significantly improve my workflow
|
| 130 |
+
- Medium - Would be nice to have
|
| 131 |
+
- Low - Minor improvement
|
| 132 |
+
validations:
|
| 133 |
+
required: true
|
| 134 |
+
|
| 135 |
+
- type: textarea
|
| 136 |
+
id: examples
|
| 137 |
+
attributes:
|
| 138 |
+
label: Examples from Other Tools
|
| 139 |
+
description: Are there similar features in other tools that could serve as inspiration?
|
| 140 |
+
placeholder: Tool X does this by...
|
| 141 |
+
|
| 142 |
+
- type: textarea
|
| 143 |
+
id: additional
|
| 144 |
+
attributes:
|
| 145 |
+
label: Additional Context
|
| 146 |
+
description: Add any other context, screenshots, or examples about the feature request.
|
| 147 |
+
|
| 148 |
+
- type: checkboxes
|
| 149 |
+
id: requirements-check
|
| 150 |
+
attributes:
|
| 151 |
+
label: "Pre-submission Checklist"
|
| 152 |
+
options:
|
| 153 |
+
- label: "I have searched existing issues and confirmed this is not a duplicate"
|
| 154 |
+
required: true
|
| 155 |
+
- label: "I am willing to submit a PR to implement this feature"
|
.github/ISSUE_TEMPLATE/performance_issue.yml
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: ⚡ Performance Issue
|
| 2 |
+
description: Report performance problems, slowness, or resource usage issues
|
| 3 |
+
title: "[Performance]: "
|
| 4 |
+
labels: ["type: performance"]
|
| 5 |
+
body:
|
| 6 |
+
- type: markdown
|
| 7 |
+
attributes:
|
| 8 |
+
value: |
|
| 9 |
+
Thanks for reporting a performance issue! Please provide detailed information to help us investigate.
|
| 10 |
+
|
| 11 |
+
- type: textarea
|
| 12 |
+
id: description
|
| 13 |
+
attributes:
|
| 14 |
+
label: Performance Issue Description
|
| 15 |
+
description: Describe the performance problem you're experiencing
|
| 16 |
+
placeholder: Forge becomes slow when...
|
| 17 |
+
validations:
|
| 18 |
+
required: true
|
| 19 |
+
|
| 20 |
+
- type: dropdown
|
| 21 |
+
id: issue-type
|
| 22 |
+
attributes:
|
| 23 |
+
label: Issue Type
|
| 24 |
+
description: What type of performance issue are you experiencing?
|
| 25 |
+
options:
|
| 26 |
+
- Slow response time
|
| 27 |
+
- High CPU usage
|
| 28 |
+
- High memory usage
|
| 29 |
+
- Network latency
|
| 30 |
+
- File I/O slowness
|
| 31 |
+
- Startup time
|
| 32 |
+
- Tool execution time
|
| 33 |
+
- Other
|
| 34 |
+
validations:
|
| 35 |
+
required: true
|
| 36 |
+
|
| 37 |
+
- type: textarea
|
| 38 |
+
id: reproduction
|
| 39 |
+
attributes:
|
| 40 |
+
label: Steps to Reproduce
|
| 41 |
+
description: What actions lead to the performance issue?
|
| 42 |
+
placeholder: |
|
| 43 |
+
1. Run command '...'
|
| 44 |
+
2. Process large file/directory
|
| 45 |
+
3. Notice slowdown after...
|
| 46 |
+
validations:
|
| 47 |
+
required: true
|
| 48 |
+
|
| 49 |
+
- type: textarea
|
| 50 |
+
id: expected-performance
|
| 51 |
+
attributes:
|
| 52 |
+
label: Expected Performance
|
| 53 |
+
description: What performance did you expect?
|
| 54 |
+
placeholder: I expected the operation to complete in...
|
| 55 |
+
validations:
|
| 56 |
+
required: true
|
| 57 |
+
|
| 58 |
+
- type: textarea
|
| 59 |
+
id: actual-performance
|
| 60 |
+
attributes:
|
| 61 |
+
label: Actual Performance
|
| 62 |
+
description: What performance are you experiencing?
|
| 63 |
+
placeholder: The operation takes... and uses...
|
| 64 |
+
validations:
|
| 65 |
+
required: true
|
| 66 |
+
|
| 67 |
+
- type: textarea
|
| 68 |
+
id: measurements
|
| 69 |
+
attributes:
|
| 70 |
+
label: Performance Measurements
|
| 71 |
+
description: |
|
| 72 |
+
If possible, provide specific measurements (time, memory, CPU usage, etc.)
|
| 73 |
+
You can use tools like `time`, `htop`, Activity Monitor, or Task Manager
|
| 74 |
+
placeholder: |
|
| 75 |
+
- Execution time: 45 seconds
|
| 76 |
+
- Memory usage: 2.5 GB
|
| 77 |
+
- CPU usage: 180%
|
| 78 |
+
render: markdown
|
| 79 |
+
|
| 80 |
+
- type: input
|
| 81 |
+
id: project-size
|
| 82 |
+
attributes:
|
| 83 |
+
label: Project Size
|
| 84 |
+
description: Approximate size of your project (number of files, lines of code, etc.)
|
| 85 |
+
placeholder: e.g., 500 files, ~50k lines of code
|
| 86 |
+
|
| 87 |
+
- type: textarea
|
| 88 |
+
id: environment
|
| 89 |
+
attributes:
|
| 90 |
+
label: Environment Details
|
| 91 |
+
description: |
|
| 92 |
+
Provide details about your environment:
|
| 93 |
+
- Forge version (`forge --version`)
|
| 94 |
+
- Operating system and version
|
| 95 |
+
- CPU, RAM
|
| 96 |
+
- AI provider and model
|
| 97 |
+
- Shell
|
| 98 |
+
placeholder: |
|
| 99 |
+
- Forge: 0.1.0
|
| 100 |
+
- OS: macOS 14.1
|
| 101 |
+
- CPU: M2 Pro
|
| 102 |
+
- RAM: 16GB
|
| 103 |
+
- Provider: Anthropic
|
| 104 |
+
- Model: claude-3.7-sonnet
|
| 105 |
+
- Shell: zsh
|
| 106 |
+
validations:
|
| 107 |
+
required: true
|
| 108 |
+
|
| 109 |
+
- type: textarea
|
| 110 |
+
id: config
|
| 111 |
+
attributes:
|
| 112 |
+
label: Configuration
|
| 113 |
+
description: Share relevant parts of your `forge.yaml` (remove sensitive data)
|
| 114 |
+
render: yaml
|
| 115 |
+
|
| 116 |
+
- type: textarea
|
| 117 |
+
id: profiling
|
| 118 |
+
attributes:
|
| 119 |
+
label: Profiling Data
|
| 120 |
+
description: |
|
| 121 |
+
If you have any profiling data or verbose logs, please share them here.
|
| 122 |
+
Run with `--verbose` flag for detailed output.
|
| 123 |
+
render: shell
|
| 124 |
+
|
| 125 |
+
- type: dropdown
|
| 126 |
+
id: frequency
|
| 127 |
+
attributes:
|
| 128 |
+
label: Frequency
|
| 129 |
+
description: How often does this performance issue occur?
|
| 130 |
+
options:
|
| 131 |
+
- Always
|
| 132 |
+
- Often (>50% of the time)
|
| 133 |
+
- Sometimes (25-50% of the time)
|
| 134 |
+
- Rarely (<25% of the time)
|
| 135 |
+
- Only in specific scenarios
|
| 136 |
+
validations:
|
| 137 |
+
required: true
|
| 138 |
+
|
| 139 |
+
- type: textarea
|
| 140 |
+
id: workarounds
|
| 141 |
+
attributes:
|
| 142 |
+
label: Workarounds
|
| 143 |
+
description: Have you found any workarounds for this issue?
|
| 144 |
+
placeholder: I can work around this by...
|
| 145 |
+
|
| 146 |
+
- type: textarea
|
| 147 |
+
id: additional
|
| 148 |
+
attributes:
|
| 149 |
+
label: Additional Context
|
| 150 |
+
description: Any other relevant information about the performance issue
|
.github/ISSUE_TEMPLATE/provider_integration.yml
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: 🔌 Provider Integration Request
|
| 2 |
+
description: Request support for a new AI provider
|
| 3 |
+
title: "[Provider]: "
|
| 4 |
+
labels: ["type: feature"]
|
| 5 |
+
body:
|
| 6 |
+
- type: markdown
|
| 7 |
+
attributes:
|
| 8 |
+
value: |
|
| 9 |
+
Thanks for suggesting a new AI provider! Please provide detailed information about the provider.
|
| 10 |
+
|
| 11 |
+
- type: input
|
| 12 |
+
id: provider-name
|
| 13 |
+
attributes:
|
| 14 |
+
label: Provider Name
|
| 15 |
+
description: What is the name of the AI provider?
|
| 16 |
+
placeholder: e.g., Cohere, Mistral AI, Together AI
|
| 17 |
+
validations:
|
| 18 |
+
required: true
|
| 19 |
+
|
| 20 |
+
- type: input
|
| 21 |
+
id: website
|
| 22 |
+
attributes:
|
| 23 |
+
label: Provider Website
|
| 24 |
+
description: Link to the provider's website
|
| 25 |
+
placeholder: https://...
|
| 26 |
+
validations:
|
| 27 |
+
required: true
|
| 28 |
+
|
| 29 |
+
- type: input
|
| 30 |
+
id: api-docs
|
| 31 |
+
attributes:
|
| 32 |
+
label: API Documentation
|
| 33 |
+
description: Link to the provider's API documentation
|
| 34 |
+
placeholder: https://docs...
|
| 35 |
+
|
| 36 |
+
- type: dropdown
|
| 37 |
+
id: api-type
|
| 38 |
+
attributes:
|
| 39 |
+
label: API Compatibility
|
| 40 |
+
description: What type of API does this provider offer?
|
| 41 |
+
options:
|
| 42 |
+
- OpenAI-compatible
|
| 43 |
+
- Anthropic-compatible
|
| 44 |
+
- Custom API
|
| 45 |
+
- Other
|
| 46 |
+
validations:
|
| 47 |
+
required: true
|
| 48 |
+
|
| 49 |
+
- type: textarea
|
| 50 |
+
id: models
|
| 51 |
+
attributes:
|
| 52 |
+
label: Available Models
|
| 53 |
+
description: List the models this provider offers that would be useful in Forge
|
| 54 |
+
placeholder: |
|
| 55 |
+
- model-1 (description)
|
| 56 |
+
- model-2 (description)
|
| 57 |
+
validations:
|
| 58 |
+
required: true
|
| 59 |
+
|
| 60 |
+
- type: textarea
|
| 61 |
+
id: features
|
| 62 |
+
attributes:
|
| 63 |
+
label: Key Features
|
| 64 |
+
description: What are the key features or advantages of this provider?
|
| 65 |
+
placeholder: |
|
| 66 |
+
- Fast inference
|
| 67 |
+
- Specialized models
|
| 68 |
+
- Cost-effective
|
| 69 |
+
- Open source models
|
| 70 |
+
validations:
|
| 71 |
+
required: true
|
| 72 |
+
|
| 73 |
+
- type: dropdown
|
| 74 |
+
id: authentication
|
| 75 |
+
attributes:
|
| 76 |
+
label: Authentication Method
|
| 77 |
+
description: How does authentication work for this provider?
|
| 78 |
+
options:
|
| 79 |
+
- API Key
|
| 80 |
+
- OAuth
|
| 81 |
+
- Bearer Token
|
| 82 |
+
- Basic Auth
|
| 83 |
+
- Other
|
| 84 |
+
validations:
|
| 85 |
+
required: true
|
| 86 |
+
|
| 87 |
+
- type: textarea
|
| 88 |
+
id: pricing
|
| 89 |
+
attributes:
|
| 90 |
+
label: Pricing Information
|
| 91 |
+
description: Link to pricing information or describe the pricing model
|
| 92 |
+
placeholder: https://provider.com/pricing or "Free tier available, paid plans start at..."
|
| 93 |
+
|
| 94 |
+
- type: dropdown
|
| 95 |
+
id: priority
|
| 96 |
+
attributes:
|
| 97 |
+
label: Community Interest
|
| 98 |
+
description: How much demand do you think there is for this provider?
|
| 99 |
+
options:
|
| 100 |
+
- High - Many users would benefit
|
| 101 |
+
- Medium - Some users would benefit
|
| 102 |
+
- Low - Niche use case
|
| 103 |
+
validations:
|
| 104 |
+
required: true
|
| 105 |
+
|
| 106 |
+
- type: textarea
|
| 107 |
+
id: use-case
|
| 108 |
+
attributes:
|
| 109 |
+
label: Your Use Case
|
| 110 |
+
description: Why do you want to use this provider with Forge?
|
| 111 |
+
placeholder: I would use this provider because...
|
| 112 |
+
validations:
|
| 113 |
+
required: true
|
| 114 |
+
|
| 115 |
+
- type: checkboxes
|
| 116 |
+
id: contribution
|
| 117 |
+
attributes:
|
| 118 |
+
label: Contribution
|
| 119 |
+
description: Are you willing to help with this integration?
|
| 120 |
+
options:
|
| 121 |
+
- label: I'm willing to submit a PR to implement this integration
|
| 122 |
+
- label: I can help with testing
|
| 123 |
+
- label: I have access to this provider's API for testing
|
| 124 |
+
|
| 125 |
+
- type: textarea
|
| 126 |
+
id: additional
|
| 127 |
+
attributes:
|
| 128 |
+
label: Additional Context
|
| 129 |
+
description: Any other information about this provider that would be helpful
|
.github/contribution.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Code-Forge 🛠️
|
| 2 |
+
|
| 3 |
+
A sophisticated AI-powered coding assistant platform built in Rust, designed to provide intelligent code generation, manipulation, and analysis capabilities through a modular and extensible architecture.
|
| 4 |
+
|
| 5 |
+
## ✨ Features
|
| 6 |
+
|
| 7 |
+
- 🤖 **AI-Powered Code Generation** - Advanced code generation and manipulation using modern AI models
|
| 8 |
+
- 🔍 **Smart Code Analysis** - Language-aware parsing and analysis for multiple programming languages
|
| 9 |
+
- 🛠️ **Extensive Tool System** - Rich set of development tools including file operations, shell commands, and code outline generation
|
| 10 |
+
- 💾 **Persistent Conversations** - Maintain context and history across coding sessions
|
| 11 |
+
- 🔒 **Secure Operations** - Built-in security measures for file system and shell operations
|
| 12 |
+
- 🔌 **Extensible Architecture** - Modular design supporting easy addition of new features and languages
|
| 13 |
+
|
| 14 |
+
## 🚀 Setup
|
| 15 |
+
|
| 16 |
+
### Prerequisites
|
| 17 |
+
|
| 18 |
+
- Rust toolchain (1.75+)
|
| 19 |
+
- SQLite
|
| 20 |
+
- Tree-sitter (for code analysis)
|
| 21 |
+
|
| 22 |
+
### Installation
|
| 23 |
+
|
| 24 |
+
```bash
|
| 25 |
+
# Build the project
|
| 26 |
+
cargo build --release
|
| 27 |
+
|
| 28 |
+
# Run the server
|
| 29 |
+
cargo run --release
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
## 🏗️ Project Structure
|
| 33 |
+
|
| 34 |
+
```
|
| 35 |
+
code-forge/
|
| 36 |
+
├── crates/
|
| 37 |
+
│ ├── forge_main/ # CLI and main application logic
|
| 38 |
+
│ ├── forge_domain/ # Core domain models and interfaces
|
| 39 |
+
│ ├── forge_services/ # HTTP API and database management
|
| 40 |
+
│ ├── forge_tool/ # Tool implementations
|
| 41 |
+
│ └── forge_walker/ # File system operations
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
## 🛠️ Core Components
|
| 45 |
+
|
| 46 |
+
- **Domain Layer** (`forge_domain`) - Core business logic and interfaces
|
| 47 |
+
- **Tool Layer** (`forge_tool`) - Development tools implementation
|
| 48 |
+
- **Server Layer** (`forge_services`) - API endpoints and persistence
|
| 49 |
+
- **Main Application** (`forge_main`) - CLI and application coordination
|
| 50 |
+
|
| 51 |
+
## 🔧 Configuration
|
| 52 |
+
|
| 53 |
+
The application requires several environment variables for proper operation:
|
| 54 |
+
|
| 55 |
+
```bash
|
| 56 |
+
# Required environment variables
|
| 57 |
+
DATABASE_URL="sqlite:path/to/database.db"
|
| 58 |
+
OPENROUTER_API_KEY="your-api-key"
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
## 📚 Documentation
|
| 62 |
+
|
| 63 |
+
Internal documentation:
|
| 64 |
+
- [Onboarding Guide](docs/onboarding.md)
|
| 65 |
+
- [Architecture Overview](docs/architecture.md)
|
| 66 |
+
|
| 67 |
+
## 🔒 Proprietary Software
|
| 68 |
+
|
| 69 |
+
This is proprietary software. All rights reserved.
|
.github/dependabot.yml
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/optimizing-pr-creation-version-updates#setting-up-a-cooldown-period-for-dependency-updates
|
| 2 |
+
|
| 3 |
+
version: 2
|
| 4 |
+
updates:
|
| 5 |
+
- package-ecosystem: cargo
|
| 6 |
+
directory: /
|
| 7 |
+
schedule:
|
| 8 |
+
interval: weekly
|
| 9 |
+
labels: ["type: chore"]
|
| 10 |
+
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference#groups--
|
| 11 |
+
groups:
|
| 12 |
+
major:
|
| 13 |
+
update-types: ["major"]
|
| 14 |
+
minor:
|
| 15 |
+
update-types: ["minor"]
|
| 16 |
+
patch:
|
| 17 |
+
update-types: ["patch"]
|
| 18 |
+
- package-ecosystem: github-actions
|
| 19 |
+
directory: /
|
| 20 |
+
schedule:
|
| 21 |
+
interval: weekly
|
| 22 |
+
labels: ["type: chore"]
|
| 23 |
+
groups:
|
| 24 |
+
actions:
|
| 25 |
+
# Combine all images of last week
|
| 26 |
+
patterns: ["*"]
|
.github/labels.json
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"name": "release: skip changelog",
|
| 4 |
+
"color": "d73a4a",
|
| 5 |
+
"description": "Skip this change from release notes.",
|
| 6 |
+
"aliases": ["skip"]
|
| 7 |
+
},
|
| 8 |
+
{
|
| 9 |
+
"name": "release: breaking",
|
| 10 |
+
"color": "d73a4a",
|
| 11 |
+
"description": "Introduces a breaking change.",
|
| 12 |
+
"aliases": [
|
| 13 |
+
"breaking change",
|
| 14 |
+
"breaking-change",
|
| 15 |
+
"Type: Breaking Change",
|
| 16 |
+
"type: breaking",
|
| 17 |
+
"breaking"
|
| 18 |
+
]
|
| 19 |
+
},
|
| 20 |
+
|
| 21 |
+
{
|
| 22 |
+
"name": "good first issue",
|
| 23 |
+
"color": "7057ff",
|
| 24 |
+
"description": "Good for newcomers.",
|
| 25 |
+
"aliases": [
|
| 26 |
+
"beginner-friendly",
|
| 27 |
+
"beginner",
|
| 28 |
+
"good for beginner",
|
| 29 |
+
"Good for beginners",
|
| 30 |
+
"good-starter-issue",
|
| 31 |
+
"starter-issue",
|
| 32 |
+
"starter",
|
| 33 |
+
"status: good starter issue"
|
| 34 |
+
]
|
| 35 |
+
},
|
| 36 |
+
{
|
| 37 |
+
"name": "help",
|
| 38 |
+
"color": "0e8a16",
|
| 39 |
+
"aliases": [
|
| 40 |
+
"help wanted",
|
| 41 |
+
"Patch Welcome",
|
| 42 |
+
"status: help wanted",
|
| 43 |
+
"Status: PR Welcome",
|
| 44 |
+
"Status: Ready for PR"
|
| 45 |
+
]
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
"name": "state: approved",
|
| 49 |
+
"color": "C3F7F7",
|
| 50 |
+
"description": "Approved to proceed.",
|
| 51 |
+
"aliases": ["approved"]
|
| 52 |
+
},
|
| 53 |
+
{
|
| 54 |
+
"name": "state: blocked",
|
| 55 |
+
"color": "C3F7F7",
|
| 56 |
+
"description": "Something is blocking action.",
|
| 57 |
+
"aliases": ["blocked", "Status: Blocked", "status: blocked", "blocker"]
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
"name": "state: pending",
|
| 61 |
+
"color": "C3F7F7",
|
| 62 |
+
"description": "Action is underway but awaiting other tasks or data before completion.",
|
| 63 |
+
"aliases": [
|
| 64 |
+
"in progress",
|
| 65 |
+
"on hold",
|
| 66 |
+
"pending",
|
| 67 |
+
"Status: In Progress",
|
| 68 |
+
"Status: Review Needed",
|
| 69 |
+
"watchlist"
|
| 70 |
+
]
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
"name": "state: inactive",
|
| 74 |
+
"color": "C3F7F7",
|
| 75 |
+
"description": "No current action needed/possible; issue fixed, out of scope, or superseded.",
|
| 76 |
+
"aliases": [
|
| 77 |
+
"duplicate",
|
| 78 |
+
"invalid",
|
| 79 |
+
"stale",
|
| 80 |
+
"Status: Abandoned",
|
| 81 |
+
"status: duplicate",
|
| 82 |
+
"status: wontfix",
|
| 83 |
+
"wont-fix",
|
| 84 |
+
"wontfix"
|
| 85 |
+
]
|
| 86 |
+
},
|
| 87 |
+
{
|
| 88 |
+
"name": "type: bug",
|
| 89 |
+
"color": "FA1DD4",
|
| 90 |
+
"description": "Something isn't working.",
|
| 91 |
+
"aliases": ["bug", "Type: Bug"]
|
| 92 |
+
},
|
| 93 |
+
{
|
| 94 |
+
"name": "type: chore",
|
| 95 |
+
"color": "FA1DD4",
|
| 96 |
+
"description": "Routine tasks like conversions, reorganization, and maintenance work.",
|
| 97 |
+
"aliases": [
|
| 98 |
+
"chore",
|
| 99 |
+
"dependabot",
|
| 100 |
+
"dependencies",
|
| 101 |
+
"dependency",
|
| 102 |
+
"greenkeeper",
|
| 103 |
+
"legal",
|
| 104 |
+
"maintenance",
|
| 105 |
+
"Type: Maintenance",
|
| 106 |
+
"type: maintenance"
|
| 107 |
+
]
|
| 108 |
+
},
|
| 109 |
+
{
|
| 110 |
+
"name": "type: discussion",
|
| 111 |
+
"color": "FA1DD4",
|
| 112 |
+
"description": "Questions, proposals and info that requires discussion.",
|
| 113 |
+
"aliases": [
|
| 114 |
+
"discussion",
|
| 115 |
+
"feedback",
|
| 116 |
+
"help wanted",
|
| 117 |
+
"idea",
|
| 118 |
+
"Idea",
|
| 119 |
+
"Proposal",
|
| 120 |
+
"proposal",
|
| 121 |
+
"question",
|
| 122 |
+
"type: design",
|
| 123 |
+
"Type: Question",
|
| 124 |
+
"type: question"
|
| 125 |
+
]
|
| 126 |
+
},
|
| 127 |
+
{
|
| 128 |
+
"name": "type: docs",
|
| 129 |
+
"color": "FA1DD4",
|
| 130 |
+
"description": "Related to documentation and information.",
|
| 131 |
+
"aliases": [
|
| 132 |
+
"doc",
|
| 133 |
+
"docs",
|
| 134 |
+
"document",
|
| 135 |
+
"documentation",
|
| 136 |
+
"documents",
|
| 137 |
+
"Type: Documentation",
|
| 138 |
+
"type: documentation",
|
| 139 |
+
"specification"
|
| 140 |
+
]
|
| 141 |
+
},
|
| 142 |
+
{
|
| 143 |
+
"name": "type: feature",
|
| 144 |
+
"color": "FA1DD4",
|
| 145 |
+
"description": "Brand new functionality, features, pages, workflows, endpoints, etc.",
|
| 146 |
+
"aliases": [
|
| 147 |
+
"addition",
|
| 148 |
+
"enhancement",
|
| 149 |
+
"feature",
|
| 150 |
+
"type: enhancement",
|
| 151 |
+
"Type: Feature",
|
| 152 |
+
"performance"
|
| 153 |
+
]
|
| 154 |
+
},
|
| 155 |
+
{
|
| 156 |
+
"name": "type: fix",
|
| 157 |
+
"color": "FA1DD4",
|
| 158 |
+
"description": "Iterations on existing features or infrastructure.",
|
| 159 |
+
"aliases": [
|
| 160 |
+
"fix",
|
| 161 |
+
"improvement",
|
| 162 |
+
"optimization",
|
| 163 |
+
"Type: Fix",
|
| 164 |
+
"bug"
|
| 165 |
+
]
|
| 166 |
+
},
|
| 167 |
+
{
|
| 168 |
+
"name": "type: refactor",
|
| 169 |
+
"color": "FA1DD4",
|
| 170 |
+
"description": "Code refactoring and restructuring.",
|
| 171 |
+
"aliases": [
|
| 172 |
+
"refactor",
|
| 173 |
+
"Type: Refactoring",
|
| 174 |
+
"refactoring"
|
| 175 |
+
]
|
| 176 |
+
},
|
| 177 |
+
{
|
| 178 |
+
"name": "type: security",
|
| 179 |
+
"color": "FA1DD4",
|
| 180 |
+
"description": "Something is vulnerable or not secure.",
|
| 181 |
+
"aliases": ["security"]
|
| 182 |
+
},
|
| 183 |
+
{
|
| 184 |
+
"name": "type: performance",
|
| 185 |
+
"color": "FA1DD4",
|
| 186 |
+
"description": "Improved performance."
|
| 187 |
+
},
|
| 188 |
+
{
|
| 189 |
+
"name": "type: testing",
|
| 190 |
+
"color": "FA1DD4",
|
| 191 |
+
"description": "Related to testing.",
|
| 192 |
+
"aliases": ["test", "testing"]
|
| 193 |
+
},
|
| 194 |
+
{
|
| 195 |
+
"name": "work: chaotic",
|
| 196 |
+
"color": "0288d1",
|
| 197 |
+
"description": "The situation is chaotic, novel practices used.",
|
| 198 |
+
"aliases": ["chaotic"]
|
| 199 |
+
},
|
| 200 |
+
{
|
| 201 |
+
"name": "work: complex",
|
| 202 |
+
"color": "4fc3f7",
|
| 203 |
+
"description": "The situation is complex, emergent practices used.",
|
| 204 |
+
"aliases": ["complex"]
|
| 205 |
+
},
|
| 206 |
+
{
|
| 207 |
+
"name": "work: complicated",
|
| 208 |
+
"color": "81d4fa",
|
| 209 |
+
"description": "The situation is complicated, good practices used.",
|
| 210 |
+
"aliases": ["complicated"]
|
| 211 |
+
},
|
| 212 |
+
{
|
| 213 |
+
"name": "work: obvious",
|
| 214 |
+
"color": "e1f5fe",
|
| 215 |
+
"description": "The situation is obvious, best practices used.",
|
| 216 |
+
"aliases": ["obvious"]
|
| 217 |
+
},
|
| 218 |
+
{
|
| 219 |
+
"name": "ci: lint",
|
| 220 |
+
"color": "F9D0C4",
|
| 221 |
+
"description": "Automatically fix the linters issues and make a commit",
|
| 222 |
+
"aliases": ["ci:lint"]
|
| 223 |
+
},
|
| 224 |
+
{
|
| 225 |
+
"name": "ci: benchmark",
|
| 226 |
+
"color": "F9D0C4",
|
| 227 |
+
"description": "Runs benchmarks",
|
| 228 |
+
"aliases": ["ci:benchmark"]
|
| 229 |
+
},
|
| 230 |
+
{
|
| 231 |
+
"name": "type: provider",
|
| 232 |
+
"color": "0075ca",
|
| 233 |
+
"description": "Updates provider.json configuration.",
|
| 234 |
+
"aliases": ["provider", "update: provider"]
|
| 235 |
+
},
|
| 236 |
+
{
|
| 237 |
+
"name": "ci: test-jit",
|
| 238 |
+
"color": "F9D0C4",
|
| 239 |
+
"description": "Run all integration tests for the JIT optimized engine",
|
| 240 |
+
"aliases": ["ci:test-jit"]
|
| 241 |
+
},
|
| 242 |
+
{
|
| 243 |
+
"name": "os: windows",
|
| 244 |
+
"color": "0078d4",
|
| 245 |
+
"description": "Windows-specific issue or feature.",
|
| 246 |
+
"aliases": ["windows", "win32", "platform: windows"]
|
| 247 |
+
},
|
| 248 |
+
{
|
| 249 |
+
"name": "onlydust-wave",
|
| 250 |
+
"color": "FF6D00",
|
| 251 |
+
"description": "Related to OnlyDust Wave Hackathon"
|
| 252 |
+
},
|
| 253 |
+
{
|
| 254 |
+
"name": "ci: build all targets",
|
| 255 |
+
"color": "F9D0C4",
|
| 256 |
+
"description": "Build release binaries for all target platforms in CI",
|
| 257 |
+
"aliases": ["ci:build-all-targets", "build-all-targets"]
|
| 258 |
+
},
|
| 259 |
+
{
|
| 260 |
+
"name": "bounty",
|
| 261 |
+
"color": "F9C74F",
|
| 262 |
+
"description": "This issue has a bounty attached.",
|
| 263 |
+
"aliases": ["bounty:open"]
|
| 264 |
+
},
|
| 265 |
+
{
|
| 266 |
+
"name": "bounty: $100",
|
| 267 |
+
"color": "F9C74F",
|
| 268 |
+
"description": "This issue has a $100 bounty attached.",
|
| 269 |
+
"aliases": ["bounty:100", "bounty: 💰 $100"]
|
| 270 |
+
},
|
| 271 |
+
{
|
| 272 |
+
"name": "bounty: $200",
|
| 273 |
+
"color": "F9C74F",
|
| 274 |
+
"description": "This issue has a $200 bounty attached.",
|
| 275 |
+
"aliases": ["bounty:200", "bounty: 💰 $200"]
|
| 276 |
+
},
|
| 277 |
+
{
|
| 278 |
+
"name": "bounty: $300",
|
| 279 |
+
"color": "F9C74F",
|
| 280 |
+
"description": "This issue has a $300 bounty attached.",
|
| 281 |
+
"aliases": ["bounty:300", "bounty: 💰 $300"]
|
| 282 |
+
},
|
| 283 |
+
{
|
| 284 |
+
"name": "bounty: $500",
|
| 285 |
+
"color": "F9C74F",
|
| 286 |
+
"description": "This issue has a $500 bounty attached.",
|
| 287 |
+
"aliases": ["bounty:500", "bounty: 💰 $500"]
|
| 288 |
+
},
|
| 289 |
+
{
|
| 290 |
+
"name": "bounty: $800",
|
| 291 |
+
"color": "F9C74F",
|
| 292 |
+
"description": "This issue has a $800 bounty attached.",
|
| 293 |
+
"aliases": ["bounty:800", "bounty: 💰 $800"]
|
| 294 |
+
},
|
| 295 |
+
{
|
| 296 |
+
"name": "bounty: $1300",
|
| 297 |
+
"color": "F9C74F",
|
| 298 |
+
"description": "This issue has a $1300 bounty attached.",
|
| 299 |
+
"aliases": ["bounty:1300", "bounty: 💰 $1300"]
|
| 300 |
+
},
|
| 301 |
+
{
|
| 302 |
+
"name": "bounty: $2100",
|
| 303 |
+
"color": "F9C74F",
|
| 304 |
+
"description": "This issue has a $2100 bounty attached.",
|
| 305 |
+
"aliases": ["bounty:2100", "bounty: 💰 $2100"]
|
| 306 |
+
},
|
| 307 |
+
{
|
| 308 |
+
"name": "bounty: $3400",
|
| 309 |
+
"color": "F9C74F",
|
| 310 |
+
"description": "This issue has a $3400 bounty attached.",
|
| 311 |
+
"aliases": ["bounty:3400", "bounty: 💰 $3400"]
|
| 312 |
+
},
|
| 313 |
+
{
|
| 314 |
+
"name": "bounty: $5500",
|
| 315 |
+
"color": "F9C74F",
|
| 316 |
+
"description": "This issue has a $5500 bounty attached.",
|
| 317 |
+
"aliases": ["bounty:5500", "bounty: 💰 $5500"]
|
| 318 |
+
},
|
| 319 |
+
{
|
| 320 |
+
"name": "bounty: claimed",
|
| 321 |
+
"color": "F9C74F",
|
| 322 |
+
"description": "This bounty has been claimed and is being worked on.",
|
| 323 |
+
"aliases": ["bounty:claimed"]
|
| 324 |
+
},
|
| 325 |
+
{
|
| 326 |
+
"name": "bounty: rewarded",
|
| 327 |
+
"color": "F9C74F",
|
| 328 |
+
"description": "This bounty has been completed and the reward paid out.",
|
| 329 |
+
"aliases": ["bounty:rewarded", "bounty:earned"]
|
| 330 |
+
},
|
| 331 |
+
{
|
| 332 |
+
"name": "severity: low",
|
| 333 |
+
"color": "FFEB3B",
|
| 334 |
+
"description": "Minor impact; does not significantly affect functionality.",
|
| 335 |
+
"aliases": ["severity:low"]
|
| 336 |
+
},
|
| 337 |
+
{
|
| 338 |
+
"name": "severity: high",
|
| 339 |
+
"color": "FF9800",
|
| 340 |
+
"description": "Significant impact; core functionality is impaired.",
|
| 341 |
+
"aliases": ["severity:high"]
|
| 342 |
+
},
|
| 343 |
+
{
|
| 344 |
+
"name": "severity: critical",
|
| 345 |
+
"color": "B71C1C",
|
| 346 |
+
"description": "Severe impact; system is unusable or data is at risk.",
|
| 347 |
+
"aliases": ["severity:critical", "critical"]
|
| 348 |
+
}
|
| 349 |
+
]
|
.github/release-drafter.yml
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name-template: "v$RESOLVED_VERSION"
|
| 2 |
+
tag-template: "v$RESOLVED_VERSION"
|
| 3 |
+
categories:
|
| 4 |
+
- title: "🚀 Features"
|
| 5 |
+
labels:
|
| 6 |
+
- "type: feature"
|
| 7 |
+
|
| 8 |
+
- title: "📝 Documentation"
|
| 9 |
+
labels:
|
| 10 |
+
- "type: docs"
|
| 11 |
+
|
| 12 |
+
- title: "🐛 Bug Fixes"
|
| 13 |
+
labels:
|
| 14 |
+
- "type: bug"
|
| 15 |
+
- "type: fix"
|
| 16 |
+
- "type: provider"
|
| 17 |
+
|
| 18 |
+
- title: "🚀 Performance"
|
| 19 |
+
labels:
|
| 20 |
+
- "type: performance"
|
| 21 |
+
|
| 22 |
+
- title: "🧰 Maintenance"
|
| 23 |
+
labels:
|
| 24 |
+
- "type: chore"
|
| 25 |
+
- "type: testing"
|
| 26 |
+
- "type: refactor"
|
| 27 |
+
- "ratatui"
|
| 28 |
+
|
| 29 |
+
- title: "💥 Breaking Changes"
|
| 30 |
+
labels:
|
| 31 |
+
- "release: breaking"
|
| 32 |
+
|
| 33 |
+
- title: "🔒 Security"
|
| 34 |
+
labels:
|
| 35 |
+
- "type: security"
|
| 36 |
+
|
| 37 |
+
change-template: "- $TITLE @$AUTHOR (#$NUMBER)"
|
| 38 |
+
change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks.
|
| 39 |
+
|
| 40 |
+
version-resolver:
|
| 41 |
+
major:
|
| 42 |
+
labels:
|
| 43 |
+
- "release: breaking"
|
| 44 |
+
minor:
|
| 45 |
+
labels:
|
| 46 |
+
- "type: feature"
|
| 47 |
+
# Add direct commit message pattern matching for features
|
| 48 |
+
commits:
|
| 49 |
+
- "feat:"
|
| 50 |
+
- "feature:"
|
| 51 |
+
patch:
|
| 52 |
+
labels:
|
| 53 |
+
- "type: fix"
|
| 54 |
+
- "type: performance"
|
| 55 |
+
- "type: chore"
|
| 56 |
+
- "type: docs"
|
| 57 |
+
- "type: bug"
|
| 58 |
+
- "type: testing"
|
| 59 |
+
- "type: refactor"
|
| 60 |
+
- "type: provider"
|
| 61 |
+
# Add direct commit message pattern matching for patches
|
| 62 |
+
commits:
|
| 63 |
+
- "fix:"
|
| 64 |
+
- "perf:"
|
| 65 |
+
- "chore:"
|
| 66 |
+
- "docs:"
|
| 67 |
+
- "refactor:"
|
| 68 |
+
default: patch
|
| 69 |
+
|
| 70 |
+
exclude-labels:
|
| 71 |
+
- "release: skip changelog"
|
| 72 |
+
|
| 73 |
+
template: |
|
| 74 |
+
## Changes
|
| 75 |
+
|
| 76 |
+
$CHANGES
|
| 77 |
+
autolabeler:
|
| 78 |
+
- label: "type: chore"
|
| 79 |
+
title:
|
| 80 |
+
- /chore/i
|
| 81 |
+
|
| 82 |
+
- label: "type: refactor"
|
| 83 |
+
title:
|
| 84 |
+
- /refactor/i
|
| 85 |
+
- /refactoring/i
|
| 86 |
+
|
| 87 |
+
- label: "type: fix"
|
| 88 |
+
title:
|
| 89 |
+
- /fix/i
|
| 90 |
+
- /hotfix/i
|
| 91 |
+
- /bugfix/i
|
| 92 |
+
- /patch/i
|
| 93 |
+
|
| 94 |
+
- label: "type: feature"
|
| 95 |
+
title:
|
| 96 |
+
- /feat/i
|
| 97 |
+
- /feature/i
|
| 98 |
+
- /enhancement/i
|
| 99 |
+
- /new/i
|
| 100 |
+
|
| 101 |
+
- label: "type: docs"
|
| 102 |
+
title:
|
| 103 |
+
- /doc/i
|
| 104 |
+
- /docs/i
|
| 105 |
+
|
| 106 |
+
- label: "type: performance"
|
| 107 |
+
title:
|
| 108 |
+
- /perf/i
|
| 109 |
+
|
| 110 |
+
- label: "ci: benchmark"
|
| 111 |
+
title:
|
| 112 |
+
- /perf/i
|
| 113 |
+
files:
|
| 114 |
+
- "/benches/"
|
| 115 |
+
|
| 116 |
+
- label: "type: provider"
|
| 117 |
+
files:
|
| 118 |
+
- "provider.json"
|
| 119 |
+
|
| 120 |
+
- label: "os: windows"
|
| 121 |
+
title:
|
| 122 |
+
- /windows/i
|
| 123 |
+
- /win32/i
|
| 124 |
+
- /winapi/i
|
| 125 |
+
- /powershell/i
|
| 126 |
+
- /scoop/i
|
| 127 |
+
- /chocolatey/i
|
| 128 |
+
- /winget/i
|
| 129 |
+
body:
|
| 130 |
+
- /windows[ -](only|specific|terminal|path|installer|build)/i
|
| 131 |
+
- /multiline paste.*windows/i
|
| 132 |
+
- /windows.*multiline paste/i
|
.github/scripts/bounty/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Bounty Automation
|
| 2 |
+
|
| 3 |
+
Automates the full lifecycle of issue bounties — from label propagation when a PR is opened, through claiming when work begins, to rewarding when a PR is merged.
|
| 4 |
+
|
| 5 |
+
## Flow
|
| 6 |
+
|
| 7 |
+
```
|
| 8 |
+
Issue created
|
| 9 |
+
└── maintainer adds bounty: $N label
|
| 10 |
+
│
|
| 11 |
+
├── parse-sync-generic-bounty.ts → adds generic bounty label
|
| 12 |
+
│
|
| 13 |
+
▼
|
| 14 |
+
Issue assigned to contributor
|
| 15 |
+
└── parse-sync-claimed.ts → adds bounty: claimed
|
| 16 |
+
│
|
| 17 |
+
▼
|
| 18 |
+
Contributor opens PR with "Closes #N" / "Fixes #N" / "Resolves #N"
|
| 19 |
+
└── parse-propagate-label.ts → copies bounty: $N to PR
|
| 20 |
+
→ posts comment on issue: "PR #X opened by @author"
|
| 21 |
+
│
|
| 22 |
+
▼
|
| 23 |
+
PR is merged
|
| 24 |
+
└── parse-mark-rewarded.ts → adds bounty: rewarded to PR
|
| 25 |
+
→ adds bounty: rewarded to linked issue(s)
|
| 26 |
+
→ removes bounty: claimed from linked issue(s)
|
| 27 |
+
│
|
| 28 |
+
▼
|
| 29 |
+
Bounty lifecycle complete
|
| 30 |
+
|
| 31 |
+
Removal path:
|
| 32 |
+
maintainer removes last bounty: $N label
|
| 33 |
+
└── parse-sync-generic-bounty.ts → removes generic bounty label
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
## Labels
|
| 37 |
+
|
| 38 |
+
| Label | Applied to | Set by |
|
| 39 |
+
| -------------------------------- | ---------- | ----------------------------------------------------------- |
|
| 40 |
+
| `bounty: $100` … `bounty: $5500` | Issue | Maintainer (manually) |
|
| 41 |
+
| `bounty` | Issue | `parse-sync-generic-bounty.ts` on value label add/remove |
|
| 42 |
+
| `bounty: claimed` | Issue | `parse-sync-claimed.ts` on assignment |
|
| 43 |
+
| `bounty: rewarded` | Issue + PR | `parse-mark-rewarded.ts` on merge |
|
| 44 |
+
|
| 45 |
+
Bounty values follow the Fibonacci sequence: **$100, $200, $300, $500, $800, $1300, $2100, $3400, $5500**.
|
| 46 |
+
|
| 47 |
+
## Pipeline
|
| 48 |
+
|
| 49 |
+
Each workflow job runs a single Node process (`run.ts`) that executes all three stages in memory — no temp files, no shell piping:
|
| 50 |
+
|
| 51 |
+
```
|
| 52 |
+
parse → ParsedIntent (in memory) → plan → BatchPlan (in memory) → execute
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
**Stage 1 — parse** (`parse-*.ts`): Reads the GitHub Actions event payload from `GITHUB_EVENT_PATH` and returns a `ParsedIntent` object. Pure — makes no API calls.
|
| 56 |
+
|
| 57 |
+
**Stage 2 — plan** (`plan.ts`): Receives the `ParsedIntent`, fetches current labels only for targets not already known from the event payload, diffs desired vs actual state, and returns a `BatchPlan`. Minimises API calls by reusing label data already present in the event.
|
| 58 |
+
|
| 59 |
+
**Stage 3 — execute** (`execute.ts`): Receives the `BatchPlan` and applies all mutations. Label additions per target are sent as a single batched `POST /labels` call. Each removal is a separate `DELETE` (GitHub has no bulk-remove endpoint). Comments are posted last.
|
| 60 |
+
|
| 61 |
+
This design means:
|
| 62 |
+
- The parse stage is trivially unit-testable (pure function, no mocks needed).
|
| 63 |
+
- The plan stage is testable with a minimal mock that only needs `getLabels`.
|
| 64 |
+
- The execute stage is testable with a mock that tracks calls — no HTTP.
|
| 65 |
+
- API calls are minimised: additions are batched per target; label state already in the event payload is never re-fetched.
|
| 66 |
+
|
| 67 |
+
## Scripts
|
| 68 |
+
|
| 69 |
+
The workflow invokes a single orchestrator script (`run.ts`) per job. The parse, plan, and execute modules are called directly in the same Node process, passing objects in memory.
|
| 70 |
+
|
| 71 |
+
### `run.ts`
|
| 72 |
+
|
| 73 |
+
The entrypoint used by `bounty.yml`. Accepts a `--script` flag to select which parse module to run, then calls `plan` and `execute` in sequence — all in-process.
|
| 74 |
+
|
| 75 |
+
```sh
|
| 76 |
+
npx tsx .github/scripts/bounty/run.ts \
|
| 77 |
+
--script <parse-script> \
|
| 78 |
+
--repo <owner/repo> \
|
| 79 |
+
--token <github-token> \
|
| 80 |
+
[--pr <number> | --issue <number>]
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
### `parse-propagate-label.ts`
|
| 84 |
+
|
| 85 |
+
Triggered by: `pull_request` — opened, edited, reopened.
|
| 86 |
+
|
| 87 |
+
1. Parses the PR body for closing keywords (`closes`, `fixes`, `resolves`, case-insensitive).
|
| 88 |
+
2. Returns a `ParsedIntent` with a `labelCopies` field — the plan stage fetches each linked issue's labels and copies any `bounty: $N` ones onto the PR.
|
| 89 |
+
3. Includes a comment mutation per linked issue (the plan stage drops it if the issue has no bounty labels).
|
| 90 |
+
|
| 91 |
+
### `parse-sync-claimed.ts`
|
| 92 |
+
|
| 93 |
+
Triggered by: `issues` — assigned, unassigned.
|
| 94 |
+
|
| 95 |
+
- **assigned**: returns `add: ["bounty: claimed"]` if the issue has a `bounty: $N` label.
|
| 96 |
+
- **unassigned**: returns `remove: ["bounty: claimed"]` only when no assignees remain.
|
| 97 |
+
- Issue labels are already in the event payload and supplied as `knownLabels` — no extra fetch.
|
| 98 |
+
|
| 99 |
+
### `parse-sync-generic-bounty.ts`
|
| 100 |
+
|
| 101 |
+
Triggered by: `issues` — labeled, unlabeled.
|
| 102 |
+
|
| 103 |
+
Keeps the generic `bounty` label in sync with value labels. Inspects `event.label` (the label that just changed) and only acts when it matches `bounty: $`.
|
| 104 |
+
|
| 105 |
+
- **labeled**: returns `add: ["bounty"]`.
|
| 106 |
+
- **unlabeled**: returns `remove: ["bounty"]` only when no value labels remain (guards against mid-tier-swap removal when a maintainer swaps one value label for another).
|
| 107 |
+
- Issue labels from the event are supplied as `knownLabels` — no extra fetch.
|
| 108 |
+
|
| 109 |
+
### `parse-mark-rewarded.ts`
|
| 110 |
+
|
| 111 |
+
Triggered by: `pull_request_target` — closed (merged only).
|
| 112 |
+
|
| 113 |
+
1. Returns empty intent if the PR was not merged or has no `bounty: $N` label.
|
| 114 |
+
2. Returns `add: ["bounty: rewarded"]` for the PR (labels known from event, no fetch).
|
| 115 |
+
3. Parses the PR body for linked issues; returns `add: ["bounty: rewarded"], remove: ["bounty: claimed"]` for each (plan stage fetches their labels).
|
| 116 |
+
|
| 117 |
+
Uses `pull_request_target` so the job has write access to issues and PRs from forks.
|
| 118 |
+
|
| 119 |
+
### `plan.ts`
|
| 120 |
+
|
| 121 |
+
Receives a `ParsedIntent` from the parse stage. Resolves `labelCopies` by fetching source issue labels. Fetches current labels for any target not already in `knownLabels`. Filters out no-op adds and removes. Returns a `BatchPlan`.
|
| 122 |
+
|
| 123 |
+
### `execute.ts`
|
| 124 |
+
|
| 125 |
+
Receives a `BatchPlan` from the plan stage. For each mutation: one batched `POST` for all additions, one `DELETE` per removal, one `POST` per comment.
|
| 126 |
+
|
| 127 |
+
## Shared Module
|
| 128 |
+
|
| 129 |
+
`github-api.ts` defines:
|
| 130 |
+
- Event payload types (`PullRequestEvent`, `IssuesEvent`)
|
| 131 |
+
- Pipeline types (`ParsedIntent`, `BatchPlan`, `TargetMutation`)
|
| 132 |
+
- The `GitHubApi` interface (injectable for testing)
|
| 133 |
+
- `GitHubRestApi` — the production implementation using `node:https`
|
| 134 |
+
|
| 135 |
+
All scripts import types from `github-api.ts` and accept a `GitHubApi` instance in their `run()` / `plan()` / `execute()` function signature, making every step independently mockable.
|
| 136 |
+
|
| 137 |
+
## Tests
|
| 138 |
+
|
| 139 |
+
Unit tests live alongside each script (`*.test.ts`) and use Node's built-in `node:test` runner.
|
| 140 |
+
|
| 141 |
+
```sh
|
| 142 |
+
npm run test:bounty
|
| 143 |
+
```
|
| 144 |
+
|
| 145 |
+
- Parse tests: pure — no mock needed, just call `parse()` with a synthetic event.
|
| 146 |
+
- Plan and execute tests: use a mock `GitHubApi` that tracks calls and returns preset label lists.
|
| 147 |
+
- The CLI entrypoint in each script (yargs parsing + `GITHUB_EVENT_PATH` read) is guarded behind an `import.meta.url` check so it does not execute on import.
|
| 148 |
+
|
| 149 |
+
## Workflow Source
|
| 150 |
+
|
| 151 |
+
`bounty.yml` is auto-generated from Rust source in `crates/forge_ci`. Do not edit it by hand — modify `crates/forge_ci/src/workflows/bounty.rs` and regenerate with:
|
| 152 |
+
|
| 153 |
+
```sh
|
| 154 |
+
cargo test -p forge_ci
|
| 155 |
+
```
|
.github/scripts/bounty/src/api.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GitHub API abstraction for v2 bounty sync.
|
| 2 |
+
// Uses the native `fetch` API (available in Node 18+).
|
| 3 |
+
|
| 4 |
+
import type { Issue, Label, PullRequest } from "./types.js";
|
| 5 |
+
|
| 6 |
+
// ---------------------------------------------------------------------------
|
| 7 |
+
// Interface — injectable for testing
|
| 8 |
+
// ---------------------------------------------------------------------------
|
| 9 |
+
|
| 10 |
+
/// Minimal GitHub REST API surface needed by the bounty sync commands.
|
| 11 |
+
export interface GitHubApi {
|
| 12 |
+
/// Fetch a full issue by number. Throws on non-2xx.
|
| 13 |
+
getIssue(number: number): Promise<Issue>;
|
| 14 |
+
/// Fetch a full pull request by number. Throws on non-2xx.
|
| 15 |
+
getPullRequest(number: number): Promise<PullRequest>;
|
| 16 |
+
/// Fetch all open issues that have any label matching the given prefix.
|
| 17 |
+
/// Handles pagination automatically and excludes pull requests.
|
| 18 |
+
listIssuesWithLabelPrefix(prefix: string): Promise<Issue[]>;
|
| 19 |
+
/// Add one or more labels to an issue or PR. Batched into a single request.
|
| 20 |
+
addLabels(target: number, labels: string[]): Promise<void>;
|
| 21 |
+
/// Remove a single label from an issue or PR.
|
| 22 |
+
removeLabel(target: number, label: string): Promise<void>;
|
| 23 |
+
/// Post a comment on an issue or PR.
|
| 24 |
+
addComment(target: number, body: string): Promise<void>;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
// ---------------------------------------------------------------------------
|
| 28 |
+
// Production implementation
|
| 29 |
+
// ---------------------------------------------------------------------------
|
| 30 |
+
|
| 31 |
+
/// Calls the real GitHub REST API v3 using `fetch`.
|
| 32 |
+
export class GitHubRestApi implements GitHubApi {
|
| 33 |
+
private readonly base: string;
|
| 34 |
+
private readonly headers: Record<string, string>;
|
| 35 |
+
|
| 36 |
+
constructor(
|
| 37 |
+
private readonly owner: string,
|
| 38 |
+
private readonly repo: string,
|
| 39 |
+
token: string
|
| 40 |
+
) {
|
| 41 |
+
this.base = `https://api.github.com/repos/${owner}/${repo}`;
|
| 42 |
+
this.headers = {
|
| 43 |
+
Authorization: `Bearer ${token}`,
|
| 44 |
+
Accept: "application/vnd.github+json",
|
| 45 |
+
"X-GitHub-Api-Version": "2022-11-28",
|
| 46 |
+
"User-Agent": "bounty-bot/v2",
|
| 47 |
+
"Content-Type": "application/json",
|
| 48 |
+
};
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
| 52 |
+
const res = await fetch(`${this.base}${path}`, {
|
| 53 |
+
method,
|
| 54 |
+
headers: this.headers,
|
| 55 |
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
| 56 |
+
});
|
| 57 |
+
|
| 58 |
+
if (!res.ok) {
|
| 59 |
+
const text = await res.text().catch(() => "");
|
| 60 |
+
throw new Error(`GitHub API ${method} ${path} → ${res.status}: ${text}`);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// 204 No Content
|
| 64 |
+
if (res.status === 204) return {} as T;
|
| 65 |
+
|
| 66 |
+
return res.json() as Promise<T>;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
async getIssue(number: number): Promise<Issue> {
|
| 70 |
+
return this.request<Issue>("GET", `/issues/${number}`);
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
async getPullRequest(number: number): Promise<PullRequest> {
|
| 74 |
+
return this.request<PullRequest>("GET", `/pulls/${number}`);
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
async listIssuesWithLabelPrefix(prefix: string): Promise<Issue[]> {
|
| 78 |
+
const results: Issue[] = [];
|
| 79 |
+
let page = 1;
|
| 80 |
+
while (true) {
|
| 81 |
+
const batch = await this.request<Issue[]>(
|
| 82 |
+
"GET",
|
| 83 |
+
`/issues?state=open&per_page=100&page=${page}`
|
| 84 |
+
);
|
| 85 |
+
if (batch.length === 0) break;
|
| 86 |
+
for (const issue of batch) {
|
| 87 |
+
// Exclude pull requests (GitHub returns them in /issues)
|
| 88 |
+
if (issue.pull_request !== undefined) continue;
|
| 89 |
+
if (issue.labels.some((l) => l.name.startsWith(prefix))) {
|
| 90 |
+
results.push(issue);
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
if (batch.length < 100) break;
|
| 94 |
+
page++;
|
| 95 |
+
}
|
| 96 |
+
return results;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
async addLabels(target: number, labels: string[]): Promise<void> {
|
| 100 |
+
await this.request("POST", `/issues/${target}/labels`, { labels });
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
async removeLabel(target: number, label: string): Promise<void> {
|
| 104 |
+
await this.request("DELETE", `/issues/${target}/labels/${encodeURIComponent(label)}`);
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
async addComment(target: number, body: string): Promise<void> {
|
| 108 |
+
await this.request("POST", `/issues/${target}/comments`, { body });
|
| 109 |
+
}
|
| 110 |
+
}
|
.github/scripts/bounty/src/rules.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Rules engine for v2 bounty sync.
|
| 2 |
+
//
|
| 3 |
+
// Both exported functions are pure: they take current state and return the
|
| 4 |
+
// minimal Patch needed to reconcile it with the desired state.
|
| 5 |
+
// No I/O, no side effects — fully unit-testable without mocks.
|
| 6 |
+
|
| 7 |
+
import type { IssueState, PrState, Patch, LabelOp, Issue } from "./types.js";
|
| 8 |
+
|
| 9 |
+
// ---------------------------------------------------------------------------
|
| 10 |
+
// Label name constants
|
| 11 |
+
// ---------------------------------------------------------------------------
|
| 12 |
+
|
| 13 |
+
export const BOUNTY_GENERIC = "bounty";
|
| 14 |
+
export const BOUNTY_CLAIMED = "bounty: claimed";
|
| 15 |
+
export const BOUNTY_REWARDED = "bounty: rewarded";
|
| 16 |
+
|
| 17 |
+
/// Matches bounty value labels in both formats:
|
| 18 |
+
/// - new: "bounty: 💰 $100", "bounty: 💰 $5500"
|
| 19 |
+
/// - legacy: "bounty: $300", "bounty: $500"
|
| 20 |
+
export const VALUE_LABEL_RE = /^bounty: (?:💰 )?\$\d/;
|
| 21 |
+
|
| 22 |
+
// ---------------------------------------------------------------------------
|
| 23 |
+
// Helpers
|
| 24 |
+
// ---------------------------------------------------------------------------
|
| 25 |
+
|
| 26 |
+
/// Returns true when the label name is a bounty value label.
|
| 27 |
+
export function isBountyValue(label: string): boolean {
|
| 28 |
+
return VALUE_LABEL_RE.test(label);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
/// Extract all issue numbers referenced by closing keywords in a PR body.
|
| 32 |
+
/// A new RegExp is created on each call to avoid stale `lastIndex` state.
|
| 33 |
+
export function linkedIssueNumbers(body: string | null): number[] {
|
| 34 |
+
const numbers: number[] = [];
|
| 35 |
+
const text = body ?? "";
|
| 36 |
+
const re = /(?:closes?|fix(?:es)?|resolves?)\s+#(\d+)/gi;
|
| 37 |
+
let m: RegExpExecArray | null;
|
| 38 |
+
while ((m = re.exec(text)) !== null) {
|
| 39 |
+
numbers.push(parseInt(m[1]!, 10));
|
| 40 |
+
}
|
| 41 |
+
return numbers;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
/// Build a LabelOp that only includes labels that actually need to change.
|
| 45 |
+
function diff(
|
| 46 |
+
target: number,
|
| 47 |
+
current: Set<string>,
|
| 48 |
+
desired: Set<string>,
|
| 49 |
+
comment?: string,
|
| 50 |
+
meta?: { title?: string; url?: string }
|
| 51 |
+
): LabelOp | null {
|
| 52 |
+
const add = [...desired].filter((l) => !current.has(l));
|
| 53 |
+
const remove = [...current].filter((l) => !desired.has(l));
|
| 54 |
+
if (add.length === 0 && remove.length === 0 && !comment) return null;
|
| 55 |
+
return { target, title: meta?.title, url: meta?.url, add, remove, comment };
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
// ---------------------------------------------------------------------------
|
| 59 |
+
// Issue rules
|
| 60 |
+
// ---------------------------------------------------------------------------
|
| 61 |
+
|
| 62 |
+
/// Compute the desired label set for an issue given its full current state.
|
| 63 |
+
///
|
| 64 |
+
/// Rules (all applied simultaneously, not sequentially):
|
| 65 |
+
///
|
| 66 |
+
/// 1. **Generic bounty** — `bounty` must be present iff any value label is present.
|
| 67 |
+
/// 2. **Claimed** — `bounty: claimed` must be present iff the issue has a value
|
| 68 |
+
/// label AND at least one assignee. It must be absent otherwise.
|
| 69 |
+
/// 3. **Value labels and `bounty: rewarded`** — these are set by maintainers or
|
| 70 |
+
/// the PR merge job and are left untouched by the issue sync.
|
| 71 |
+
/// 4. All non-bounty labels are preserved as-is.
|
| 72 |
+
export function computeIssuePatch({ issue, currentLabels }: IssueState): Patch {
|
| 73 |
+
const desired = new Set(currentLabels);
|
| 74 |
+
|
| 75 |
+
const hasValueLabel = [...currentLabels].some(isBountyValue);
|
| 76 |
+
const isAssigned = issue.assignees.length > 0;
|
| 77 |
+
|
| 78 |
+
// Rule 1 — generic label mirrors value label presence.
|
| 79 |
+
if (hasValueLabel) {
|
| 80 |
+
desired.add(BOUNTY_GENERIC);
|
| 81 |
+
} else {
|
| 82 |
+
desired.delete(BOUNTY_GENERIC);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
// Rule 2 — claimed mirrors assignment presence (only when bounty exists).
|
| 86 |
+
if (hasValueLabel && isAssigned) {
|
| 87 |
+
desired.add(BOUNTY_CLAIMED);
|
| 88 |
+
} else {
|
| 89 |
+
desired.delete(BOUNTY_CLAIMED);
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
const op = diff(issue.number, currentLabels, desired, undefined, {
|
| 93 |
+
title: issue.title,
|
| 94 |
+
url: issue.html_url,
|
| 95 |
+
});
|
| 96 |
+
return { ops: op ? [op] : [] };
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
// ---------------------------------------------------------------------------
|
| 100 |
+
// PR rules
|
| 101 |
+
// ---------------------------------------------------------------------------
|
| 102 |
+
|
| 103 |
+
/// Compute the desired state for a PR and its linked issues.
|
| 104 |
+
///
|
| 105 |
+
/// Rules:
|
| 106 |
+
///
|
| 107 |
+
/// 1. **Label propagation** — for each linked issue that has a value label,
|
| 108 |
+
/// copy that value label to the PR (if the PR doesn't already have it).
|
| 109 |
+
/// 2. **Rewarded on merge** — if the PR is merged and has a value label:
|
| 110 |
+
/// a. Add `bounty: rewarded` to the PR.
|
| 111 |
+
/// b. Add `bounty: rewarded` to each linked issue.
|
| 112 |
+
/// c. Remove `bounty: claimed` from each linked issue.
|
| 113 |
+
/// 3. **Comment on propagation** — when a value label is first added to the PR
|
| 114 |
+
/// via rule 1, post a comment on the source issue.
|
| 115 |
+
export function computePrPatch({ pr, currentLabels, linkedIssues }: PrState): Patch {
|
| 116 |
+
const ops: LabelOp[] = [];
|
| 117 |
+
|
| 118 |
+
const prDesired = new Set(currentLabels);
|
| 119 |
+
|
| 120 |
+
// Collect value labels to propagate from linked issues.
|
| 121 |
+
const labelsToPropagate: string[] = [];
|
| 122 |
+
for (const issue of linkedIssues) {
|
| 123 |
+
const issueValueLabels = issue.labels.map((l) => l.name).filter(isBountyValue);
|
| 124 |
+
for (const label of issueValueLabels) {
|
| 125 |
+
if (!prDesired.has(label)) {
|
| 126 |
+
labelsToPropagate.push(label);
|
| 127 |
+
prDesired.add(label);
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
const prHasValueLabel = [...prDesired].some(isBountyValue);
|
| 133 |
+
|
| 134 |
+
if (pr.merged && prHasValueLabel) {
|
| 135 |
+
// Rule 2 — rewarded lifecycle.
|
| 136 |
+
prDesired.add(BOUNTY_REWARDED);
|
| 137 |
+
|
| 138 |
+
const prOp = diff(pr.number, currentLabels, prDesired, undefined, {
|
| 139 |
+
title: pr.title,
|
| 140 |
+
url: pr.html_url,
|
| 141 |
+
});
|
| 142 |
+
if (prOp) ops.push(prOp);
|
| 143 |
+
|
| 144 |
+
// Update linked issues — only those that already have a bounty value label.
|
| 145 |
+
for (const issue of linkedIssues) {
|
| 146 |
+
const issueCurrent = new Set(issue.labels.map((l) => l.name));
|
| 147 |
+
const issueHasValue = [...issueCurrent].some(isBountyValue);
|
| 148 |
+
if (!issueHasValue) continue;
|
| 149 |
+
|
| 150 |
+
const issueDesired = new Set(issueCurrent);
|
| 151 |
+
issueDesired.add(BOUNTY_REWARDED);
|
| 152 |
+
issueDesired.delete(BOUNTY_CLAIMED);
|
| 153 |
+
|
| 154 |
+
const op = diff(issue.number, issueCurrent, issueDesired, undefined, {
|
| 155 |
+
title: issue.title,
|
| 156 |
+
url: issue.html_url,
|
| 157 |
+
});
|
| 158 |
+
if (op) ops.push(op);
|
| 159 |
+
}
|
| 160 |
+
} else {
|
| 161 |
+
// Rule 1 — label propagation (pre-merge).
|
| 162 |
+
const prOp = diff(pr.number, currentLabels, prDesired, undefined, {
|
| 163 |
+
title: pr.title,
|
| 164 |
+
url: pr.html_url,
|
| 165 |
+
});
|
| 166 |
+
if (prOp) ops.push(prOp);
|
| 167 |
+
|
| 168 |
+
// Rule 3 — comment on each issue whose value label was newly propagated.
|
| 169 |
+
if (labelsToPropagate.length > 0) {
|
| 170 |
+
for (const issue of linkedIssues) {
|
| 171 |
+
const hadValueLabel = issue.labels.map((l) => l.name).some(isBountyValue);
|
| 172 |
+
if (hadValueLabel) {
|
| 173 |
+
ops.push({
|
| 174 |
+
target: issue.number,
|
| 175 |
+
title: issue.title,
|
| 176 |
+
url: issue.html_url,
|
| 177 |
+
add: [],
|
| 178 |
+
remove: [],
|
| 179 |
+
comment: `PR [#${pr.number}](${pr.html_url}) has been opened for this bounty by @${pr.user.login}.`,
|
| 180 |
+
});
|
| 181 |
+
}
|
| 182 |
+
}
|
| 183 |
+
}
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
return { ops };
|
| 187 |
+
}
|
.github/scripts/bounty/src/sync-all-issues.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env tsx
|
| 2 |
+
// Syncs bounty labels across ALL open issues that carry any "bounty" label.
|
| 3 |
+
//
|
| 4 |
+
// Without --execute: fetches all matching issues, computes the combined patch,
|
| 5 |
+
// and prints a plan showing exactly what would change. No writes are made.
|
| 6 |
+
//
|
| 7 |
+
// With --execute: fetches, computes, and applies the patch for every issue.
|
| 8 |
+
//
|
| 9 |
+
// Usage:
|
| 10 |
+
// tsx sync-all-issues.ts --repo <owner/repo> --token <token> [--execute]
|
| 11 |
+
|
| 12 |
+
import * as url from "url";
|
| 13 |
+
import yargs from "yargs";
|
| 14 |
+
import { hideBin } from "yargs/helpers";
|
| 15 |
+
import { GitHubRestApi, type GitHubApi } from "./api.js";
|
| 16 |
+
import { computeIssuePatch } from "./rules.js";
|
| 17 |
+
import { applyPatch, printPlan, resolveToken } from "./sync-issue.js";
|
| 18 |
+
import type { Patch } from "./types.js";
|
| 19 |
+
|
| 20 |
+
const BOUNTY_LABEL_PREFIX = "bounty";
|
| 21 |
+
|
| 22 |
+
export interface PlanAllIssuesInput {
|
| 23 |
+
api: GitHubApi;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
/// Fetches all open issues with any bounty label and computes the combined patch.
|
| 27 |
+
/// Makes no writes — safe to call at any time.
|
| 28 |
+
export async function planAllIssues({ api }: PlanAllIssuesInput): Promise<Patch> {
|
| 29 |
+
const issues = await api.listIssuesWithLabelPrefix(BOUNTY_LABEL_PREFIX);
|
| 30 |
+
const ops = issues.flatMap((issue) => {
|
| 31 |
+
const currentLabels = new Set(issue.labels.map((l) => l.name));
|
| 32 |
+
return computeIssuePatch({ issue, currentLabels }).ops;
|
| 33 |
+
});
|
| 34 |
+
return { ops };
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
/// Fetches, computes, and applies the label patch for all bounty issues.
|
| 38 |
+
/// Returns the patch that was applied.
|
| 39 |
+
export async function syncAllIssues({ api }: PlanAllIssuesInput): Promise<Patch> {
|
| 40 |
+
const patch = await planAllIssues({ api });
|
| 41 |
+
await applyPatch(patch, api);
|
| 42 |
+
return patch;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// ---------------------------------------------------------------------------
|
| 46 |
+
// CLI entrypoint
|
| 47 |
+
// ---------------------------------------------------------------------------
|
| 48 |
+
|
| 49 |
+
if (process.argv[1] === url.fileURLToPath(import.meta.url)) {
|
| 50 |
+
const argv = await yargs(hideBin(process.argv))
|
| 51 |
+
.option("repo", { type: "string", demandOption: true, description: "owner/repo" })
|
| 52 |
+
.option("token", {
|
| 53 |
+
type: "string",
|
| 54 |
+
description: "GitHub token (falls back to GITHUB_TOKEN env var or `gh auth token`)",
|
| 55 |
+
})
|
| 56 |
+
.option("execute", {
|
| 57 |
+
type: "boolean",
|
| 58 |
+
default: false,
|
| 59 |
+
description: "Apply the patch. Without this flag only the plan is printed.",
|
| 60 |
+
})
|
| 61 |
+
.strict()
|
| 62 |
+
.parseAsync();
|
| 63 |
+
|
| 64 |
+
const [owner, repo] = argv.repo.split("/") as [string, string];
|
| 65 |
+
const token = resolveToken(argv.token);
|
| 66 |
+
const api = new GitHubRestApi(owner, repo, token);
|
| 67 |
+
|
| 68 |
+
if (argv.execute) {
|
| 69 |
+
const patch = await syncAllIssues({ api });
|
| 70 |
+
if (patch.ops.length === 0) {
|
| 71 |
+
console.log("All issues already in sync — no changes needed.");
|
| 72 |
+
}
|
| 73 |
+
} else {
|
| 74 |
+
const patch = await planAllIssues({ api });
|
| 75 |
+
printPlan(patch, "All bounty issues");
|
| 76 |
+
}
|
| 77 |
+
}
|
.github/scripts/bounty/src/sync-issue.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env tsx
|
| 2 |
+
// Syncs all bounty labels on a single issue.
|
| 3 |
+
//
|
| 4 |
+
// Without --execute: fetches current state, computes the patch, and prints
|
| 5 |
+
// a plan showing exactly what would change. No writes are made.
|
| 6 |
+
//
|
| 7 |
+
// With --execute: fetches, computes, and applies the patch.
|
| 8 |
+
//
|
| 9 |
+
// Usage:
|
| 10 |
+
// tsx sync-issue.ts --issue <number> --repo <owner/repo> --token <token> [--execute]
|
| 11 |
+
|
| 12 |
+
import * as url from "url";
|
| 13 |
+
import { execSync } from "child_process";
|
| 14 |
+
import chalk from "chalk";
|
| 15 |
+
import yargs from "yargs";
|
| 16 |
+
import { hideBin } from "yargs/helpers";
|
| 17 |
+
import { GitHubRestApi, type GitHubApi } from "./api.js";
|
| 18 |
+
import { computeIssuePatch } from "./rules.js";
|
| 19 |
+
import type { Patch } from "./types.js";
|
| 20 |
+
|
| 21 |
+
/// Resolves a GitHub token from the provided value, the GITHUB_TOKEN env var,
|
| 22 |
+
/// or by invoking `gh auth token` as a fallback. Exits with an error if none
|
| 23 |
+
/// can be found.
|
| 24 |
+
export function resolveToken(flag: string | undefined): string {
|
| 25 |
+
const token = flag || process.env["GITHUB_TOKEN"] || (() => {
|
| 26 |
+
try {
|
| 27 |
+
return execSync("gh auth token", { encoding: "utf8" }).trim();
|
| 28 |
+
} catch {
|
| 29 |
+
return "";
|
| 30 |
+
}
|
| 31 |
+
})();
|
| 32 |
+
if (!token) {
|
| 33 |
+
console.error(
|
| 34 |
+
"Error: no GitHub token found. Pass --token, set GITHUB_TOKEN, or run `gh auth login`."
|
| 35 |
+
);
|
| 36 |
+
process.exit(1);
|
| 37 |
+
}
|
| 38 |
+
return token;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
export interface PlanIssueInput {
|
| 42 |
+
issueNumber: number;
|
| 43 |
+
api: GitHubApi;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
/// Fetches the current issue state and computes the minimal label patch.
|
| 47 |
+
/// Makes no writes — safe to call at any time.
|
| 48 |
+
export async function planIssue({ issueNumber, api }: PlanIssueInput): Promise<Patch> {
|
| 49 |
+
const issue = await api.getIssue(issueNumber);
|
| 50 |
+
const currentLabels = new Set(issue.labels.map((l) => l.name));
|
| 51 |
+
return computeIssuePatch({ issue, currentLabels });
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/// Fetches, computes, and applies the label patch for a single issue.
|
| 55 |
+
/// Returns the patch that was applied (useful for tests and logging).
|
| 56 |
+
export async function syncIssue({ issueNumber, api }: PlanIssueInput): Promise<Patch> {
|
| 57 |
+
const patch = await planIssue({ issueNumber, api });
|
| 58 |
+
await applyPatch(patch, api);
|
| 59 |
+
return patch;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
/// Applies a Patch against the GitHub API.
|
| 63 |
+
/// Each target gets one batched addLabels call; each removal is individual.
|
| 64 |
+
export async function applyPatch(patch: Patch, api: GitHubApi): Promise<void> {
|
| 65 |
+
for (const op of patch.ops) {
|
| 66 |
+
const ref = chalk.cyan(`#${op.target}`);
|
| 67 |
+
if (op.add.length > 0) {
|
| 68 |
+
await api.addLabels(op.target, op.add);
|
| 69 |
+
console.log(`${chalk.green("✔")} ${ref}: added [${chalk.green(op.add.join(", "))}]`);
|
| 70 |
+
}
|
| 71 |
+
for (const label of op.remove) {
|
| 72 |
+
await api.removeLabel(op.target, label);
|
| 73 |
+
console.log(`${chalk.red("✖")} ${ref}: removed "${chalk.red(label)}"`);
|
| 74 |
+
}
|
| 75 |
+
if (op.comment) {
|
| 76 |
+
await api.addComment(op.target, op.comment);
|
| 77 |
+
console.log(`${chalk.yellow("✉")} ${ref}: posted comment`);
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
/// Prints a human-readable plan to stdout without making any API calls.
|
| 83 |
+
export function printPlan(patch: Patch, subject: string): void {
|
| 84 |
+
if (patch.ops.length === 0) {
|
| 85 |
+
console.log(`${chalk.green("✔")} ${chalk.bold(subject)}: already in sync — no changes needed.`);
|
| 86 |
+
return;
|
| 87 |
+
}
|
| 88 |
+
console.log(`${chalk.yellow("●")} ${chalk.bold(subject)}: plan (${chalk.bold(String(patch.ops.length))} target(s) to update)\n`);
|
| 89 |
+
for (const op of patch.ops) {
|
| 90 |
+
const title = op.title ? chalk.bold(` ${op.title}`) : "";
|
| 91 |
+
const href = op.url ? `\n ${chalk.dim(chalk.blue(op.url))}` : "";
|
| 92 |
+
console.log(` ${chalk.cyan(`#${op.target}`)}${title}${href}`);
|
| 93 |
+
if (op.add.length > 0)
|
| 94 |
+
console.log(` ${chalk.green("+")} add: ${chalk.green(op.add.join(", "))}`);
|
| 95 |
+
if (op.remove.length > 0)
|
| 96 |
+
console.log(` ${chalk.red("-")} remove: ${chalk.red(op.remove.join(", "))}`);
|
| 97 |
+
if (op.comment)
|
| 98 |
+
console.log(` ${chalk.yellow("~")} comment: ${chalk.dim(op.comment.slice(0, 80))}…`);
|
| 99 |
+
}
|
| 100 |
+
console.log(`\n${chalk.dim("Run with --execute to apply.")}`);
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
// ---------------------------------------------------------------------------
|
| 104 |
+
// CLI entrypoint
|
| 105 |
+
// ---------------------------------------------------------------------------
|
| 106 |
+
|
| 107 |
+
if (process.argv[1] === url.fileURLToPath(import.meta.url)) {
|
| 108 |
+
const argv = await yargs(hideBin(process.argv))
|
| 109 |
+
.option("issue", { type: "number", demandOption: true, description: "Issue number" })
|
| 110 |
+
.option("repo", { type: "string", demandOption: true, description: "owner/repo" })
|
| 111 |
+
.option("token", { type: "string", description: "GitHub token (falls back to GITHUB_TOKEN env var or `gh auth token`)" })
|
| 112 |
+
.option("execute", {
|
| 113 |
+
type: "boolean",
|
| 114 |
+
default: false,
|
| 115 |
+
description: "Apply the patch. Without this flag only the plan is printed.",
|
| 116 |
+
})
|
| 117 |
+
.strict()
|
| 118 |
+
.parseAsync();
|
| 119 |
+
|
| 120 |
+
const [owner, repo] = argv.repo.split("/") as [string, string];
|
| 121 |
+
const token = resolveToken(argv.token);
|
| 122 |
+
const api = new GitHubRestApi(owner, repo, token);
|
| 123 |
+
|
| 124 |
+
if (argv.execute) {
|
| 125 |
+
const patch = await syncIssue({ issueNumber: argv.issue, api });
|
| 126 |
+
if (patch.ops.length === 0) {
|
| 127 |
+
console.log(`Issue #${argv.issue}: already in sync, no changes needed.`);
|
| 128 |
+
}
|
| 129 |
+
} else {
|
| 130 |
+
const patch = await planIssue({ issueNumber: argv.issue, api });
|
| 131 |
+
printPlan(patch, `Issue #${argv.issue}`);
|
| 132 |
+
}
|
| 133 |
+
}
|
.github/scripts/bounty/src/sync-pr.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env tsx
|
| 2 |
+
// Syncs bounty labels on a PR (and its linked issues).
|
| 3 |
+
//
|
| 4 |
+
// Without --execute: fetches current state, computes the patch, and prints
|
| 5 |
+
// a plan showing exactly what would change. No writes are made.
|
| 6 |
+
//
|
| 7 |
+
// With --execute: fetches, computes, and applies the patch.
|
| 8 |
+
//
|
| 9 |
+
// Usage:
|
| 10 |
+
// tsx sync-pr.ts --pr <number> --repo <owner/repo> --token <token> [--execute]
|
| 11 |
+
|
| 12 |
+
import * as url from "url";
|
| 13 |
+
import yargs from "yargs";
|
| 14 |
+
import { hideBin } from "yargs/helpers";
|
| 15 |
+
import { GitHubRestApi, type GitHubApi } from "./api.js";
|
| 16 |
+
import { computePrPatch, linkedIssueNumbers } from "./rules.js";
|
| 17 |
+
import { applyPatch, printPlan, resolveToken } from "./sync-issue.js";
|
| 18 |
+
import type { Patch } from "./types.js";
|
| 19 |
+
|
| 20 |
+
export interface PlanPrInput {
|
| 21 |
+
prNumber: number;
|
| 22 |
+
api: GitHubApi;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
/// Fetches the current PR + linked issue state and computes the minimal label patch.
|
| 26 |
+
/// Makes no writes — safe to call at any time.
|
| 27 |
+
export async function planPr({ prNumber, api }: PlanPrInput): Promise<Patch> {
|
| 28 |
+
const pr = await api.getPullRequest(prNumber);
|
| 29 |
+
const currentLabels = new Set(pr.labels.map((l) => l.name));
|
| 30 |
+
const issueNumbers = linkedIssueNumbers(pr.body);
|
| 31 |
+
|
| 32 |
+
const linkedIssues = await Promise.all(
|
| 33 |
+
issueNumbers.map((n) =>
|
| 34 |
+
api.getIssue(n).catch((err) => {
|
| 35 |
+
console.warn(`Could not fetch linked issue #${n}: ${String(err)}, skipping.`);
|
| 36 |
+
return null;
|
| 37 |
+
})
|
| 38 |
+
)
|
| 39 |
+
).then((results) => results.filter((i): i is NonNullable<typeof i> => i !== null));
|
| 40 |
+
|
| 41 |
+
return computePrPatch({ pr, currentLabels, linkedIssues });
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
/// Fetches, computes, and applies the label patch for a PR and its linked issues.
|
| 45 |
+
/// Returns the patch that was applied (useful for tests and logging).
|
| 46 |
+
export async function syncPr({ prNumber, api }: PlanPrInput): Promise<Patch> {
|
| 47 |
+
const patch = await planPr({ prNumber, api });
|
| 48 |
+
await applyPatch(patch, api);
|
| 49 |
+
return patch;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// ---------------------------------------------------------------------------
|
| 53 |
+
// CLI entrypoint
|
| 54 |
+
// ---------------------------------------------------------------------------
|
| 55 |
+
|
| 56 |
+
if (process.argv[1] === url.fileURLToPath(import.meta.url)) {
|
| 57 |
+
const argv = await yargs(hideBin(process.argv))
|
| 58 |
+
.option("pr", { type: "number", demandOption: true, description: "PR number" })
|
| 59 |
+
.option("repo", { type: "string", demandOption: true, description: "owner/repo" })
|
| 60 |
+
.option("token", { type: "string", description: "GitHub token (falls back to GITHUB_TOKEN env var or `gh auth token`)" })
|
| 61 |
+
.option("execute", {
|
| 62 |
+
type: "boolean",
|
| 63 |
+
default: false,
|
| 64 |
+
description: "Apply the patch. Without this flag only the plan is printed.",
|
| 65 |
+
})
|
| 66 |
+
.strict()
|
| 67 |
+
.parseAsync();
|
| 68 |
+
|
| 69 |
+
const [owner, repo] = argv.repo.split("/") as [string, string];
|
| 70 |
+
const token = resolveToken(argv.token);
|
| 71 |
+
const api = new GitHubRestApi(owner, repo, token);
|
| 72 |
+
|
| 73 |
+
if (argv.execute) {
|
| 74 |
+
const patch = await syncPr({ prNumber: argv.pr, api });
|
| 75 |
+
if (patch.ops.length === 0) {
|
| 76 |
+
console.log(`PR #${argv.pr}: already in sync, no changes needed.`);
|
| 77 |
+
}
|
| 78 |
+
} else {
|
| 79 |
+
const patch = await planPr({ prNumber: argv.pr, api });
|
| 80 |
+
printPlan(patch, `PR #${argv.pr}`);
|
| 81 |
+
}
|
| 82 |
+
}
|
.github/scripts/bounty/src/types.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Core domain types for v2 bounty sync.
|
| 2 |
+
//
|
| 3 |
+
// Design: both sync commands work by fetching the complete current state of an
|
| 4 |
+
// issue or PR from GitHub, deriving the *desired* label set from the rules, and
|
| 5 |
+
// computing a minimal patch (add / remove) to reconcile the two.
|
| 6 |
+
|
| 7 |
+
// ---------------------------------------------------------------------------
|
| 8 |
+
// GitHub resource types (as returned by the REST API)
|
| 9 |
+
// ---------------------------------------------------------------------------
|
| 10 |
+
|
| 11 |
+
export interface Label {
|
| 12 |
+
name: string;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export interface User {
|
| 16 |
+
login: string;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
/// Full issue as returned by GET /repos/:owner/:repo/issues/:number
|
| 20 |
+
export interface Issue {
|
| 21 |
+
number: number;
|
| 22 |
+
title: string;
|
| 23 |
+
html_url: string;
|
| 24 |
+
state: "open" | "closed";
|
| 25 |
+
labels: Label[];
|
| 26 |
+
assignees: User[];
|
| 27 |
+
pull_request?: unknown; // present when the issue is actually a PR
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
/// Full pull request as returned by GET /repos/:owner/:repo/pulls/:number
|
| 31 |
+
export interface PullRequest {
|
| 32 |
+
number: number;
|
| 33 |
+
title: string;
|
| 34 |
+
state: "open" | "closed";
|
| 35 |
+
merged: boolean;
|
| 36 |
+
body: string | null;
|
| 37 |
+
labels: Label[];
|
| 38 |
+
user: User;
|
| 39 |
+
html_url: string;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
// ---------------------------------------------------------------------------
|
| 43 |
+
// Derived state used by the rules engine
|
| 44 |
+
// ---------------------------------------------------------------------------
|
| 45 |
+
|
| 46 |
+
/// Everything the rules engine needs to know about an issue.
|
| 47 |
+
export interface IssueState {
|
| 48 |
+
issue: Issue;
|
| 49 |
+
/// Current label names on the issue.
|
| 50 |
+
currentLabels: Set<string>;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
/// Everything the rules engine needs to know about a PR and its linked issues.
|
| 54 |
+
export interface PrState {
|
| 55 |
+
pr: PullRequest;
|
| 56 |
+
/// Current label names on the PR.
|
| 57 |
+
currentLabels: Set<string>;
|
| 58 |
+
/// Full state of each issue linked via "closes/fixes/resolves #N" in the PR body.
|
| 59 |
+
linkedIssues: Issue[];
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// ---------------------------------------------------------------------------
|
| 63 |
+
// Patch types — the minimal diff to apply
|
| 64 |
+
// ---------------------------------------------------------------------------
|
| 65 |
+
|
| 66 |
+
/// A label operation on a single target (issue or PR number).
|
| 67 |
+
export interface LabelOp {
|
| 68 |
+
target: number;
|
| 69 |
+
/// Human-readable title of the issue or PR, for display in plan output.
|
| 70 |
+
title?: string;
|
| 71 |
+
/// URL of the issue or PR, for display in plan output.
|
| 72 |
+
url?: string;
|
| 73 |
+
add: string[];
|
| 74 |
+
remove: string[];
|
| 75 |
+
/// Optional comment to post on the target after label ops.
|
| 76 |
+
comment?: string;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
/// The complete set of operations to apply.
|
| 80 |
+
export interface Patch {
|
| 81 |
+
ops: LabelOp[];
|
| 82 |
+
}
|
.github/scripts/bounty/tests/rules.test.ts
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { describe, it } from "node:test";
|
| 2 |
+
import assert from "node:assert/strict";
|
| 3 |
+
import {
|
| 4 |
+
computeIssuePatch,
|
| 5 |
+
computePrPatch,
|
| 6 |
+
linkedIssueNumbers,
|
| 7 |
+
BOUNTY_GENERIC,
|
| 8 |
+
BOUNTY_CLAIMED,
|
| 9 |
+
BOUNTY_REWARDED,
|
| 10 |
+
} from "../src/rules.js";
|
| 11 |
+
import type { Issue, PullRequest, IssueState, PrState } from "../src/types.js";
|
| 12 |
+
|
| 13 |
+
// ---------------------------------------------------------------------------
|
| 14 |
+
// Fixtures
|
| 15 |
+
// ---------------------------------------------------------------------------
|
| 16 |
+
|
| 17 |
+
function makeIssue(overrides: Partial<Issue> & { number: number }): Issue {
|
| 18 |
+
return {
|
| 19 |
+
title: "Test issue",
|
| 20 |
+
html_url: `https://github.com/owner/repo/issues/${overrides.number}`,
|
| 21 |
+
state: "open",
|
| 22 |
+
labels: [],
|
| 23 |
+
assignees: [],
|
| 24 |
+
...overrides,
|
| 25 |
+
};
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
function makePr(overrides: Partial<PullRequest> & { number: number }): PullRequest {
|
| 29 |
+
return {
|
| 30 |
+
title: "Test PR",
|
| 31 |
+
state: "open",
|
| 32 |
+
merged: false,
|
| 33 |
+
body: null,
|
| 34 |
+
labels: [],
|
| 35 |
+
user: { login: "contributor" },
|
| 36 |
+
html_url: `https://github.com/owner/repo/pull/${overrides.number}`,
|
| 37 |
+
...overrides,
|
| 38 |
+
};
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
function labelNames(...names: string[]): { name: string }[] {
|
| 42 |
+
return names.map((name) => ({ name }));
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
function issueState(issue: Issue): IssueState {
|
| 46 |
+
return { issue, currentLabels: new Set(issue.labels.map((l) => l.name)) };
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
// ---------------------------------------------------------------------------
|
| 50 |
+
// computeIssuePatch — issue rules
|
| 51 |
+
// ---------------------------------------------------------------------------
|
| 52 |
+
|
| 53 |
+
describe("computeIssuePatch", () => {
|
| 54 |
+
describe("no bounty label", () => {
|
| 55 |
+
it("produces no patch when issue has no bounty labels at all", () => {
|
| 56 |
+
const issue = makeIssue({ number: 1, labels: labelNames("type: bug") });
|
| 57 |
+
const patch = computeIssuePatch(issueState(issue));
|
| 58 |
+
assert.deepEqual(patch.ops, []);
|
| 59 |
+
});
|
| 60 |
+
|
| 61 |
+
it("removes generic bounty label when no value label present", () => {
|
| 62 |
+
const issue = makeIssue({ number: 1, labels: labelNames(BOUNTY_GENERIC) });
|
| 63 |
+
const patch = computeIssuePatch(issueState(issue));
|
| 64 |
+
assert.equal(patch.ops.length, 1);
|
| 65 |
+
assert.deepEqual(patch.ops[0]!.remove, [BOUNTY_GENERIC]);
|
| 66 |
+
assert.deepEqual(patch.ops[0]!.add, []);
|
| 67 |
+
});
|
| 68 |
+
|
| 69 |
+
it("removes claimed label when no value label present", () => {
|
| 70 |
+
const issue = makeIssue({
|
| 71 |
+
number: 1,
|
| 72 |
+
labels: labelNames(BOUNTY_CLAIMED),
|
| 73 |
+
assignees: [{ login: "alice" }],
|
| 74 |
+
});
|
| 75 |
+
const patch = computeIssuePatch(issueState(issue));
|
| 76 |
+
assert.equal(patch.ops.length, 1);
|
| 77 |
+
assert.deepEqual(patch.ops[0]!.remove, [BOUNTY_CLAIMED]);
|
| 78 |
+
});
|
| 79 |
+
});
|
| 80 |
+
|
| 81 |
+
describe("has value label, no assignees", () => {
|
| 82 |
+
it("adds generic label when missing", () => {
|
| 83 |
+
const issue = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $500") });
|
| 84 |
+
const patch = computeIssuePatch(issueState(issue));
|
| 85 |
+
assert.equal(patch.ops.length, 1);
|
| 86 |
+
assert.deepEqual(patch.ops[0]!.add, [BOUNTY_GENERIC]);
|
| 87 |
+
assert.deepEqual(patch.ops[0]!.remove, []);
|
| 88 |
+
});
|
| 89 |
+
|
| 90 |
+
it("recognises legacy label format (no emoji) as a value label", () => {
|
| 91 |
+
const issue = makeIssue({ number: 1, labels: labelNames("bounty: $300") });
|
| 92 |
+
const patch = computeIssuePatch(issueState(issue));
|
| 93 |
+
assert.equal(patch.ops.length, 1);
|
| 94 |
+
assert.deepEqual(patch.ops[0]!.add, [BOUNTY_GENERIC]);
|
| 95 |
+
assert.deepEqual(patch.ops[0]!.remove, []);
|
| 96 |
+
});
|
| 97 |
+
|
| 98 |
+
it("produces no patch when generic already present and no assignees", () => {
|
| 99 |
+
const issue = makeIssue({
|
| 100 |
+
number: 1,
|
| 101 |
+
labels: labelNames("bounty: 💰 $500", BOUNTY_GENERIC),
|
| 102 |
+
});
|
| 103 |
+
const patch = computeIssuePatch(issueState(issue));
|
| 104 |
+
assert.deepEqual(patch.ops, []);
|
| 105 |
+
});
|
| 106 |
+
|
| 107 |
+
it("removes claimed when no assignees", () => {
|
| 108 |
+
const issue = makeIssue({
|
| 109 |
+
number: 1,
|
| 110 |
+
labels: labelNames("bounty: 💰 $500", BOUNTY_GENERIC, BOUNTY_CLAIMED),
|
| 111 |
+
assignees: [],
|
| 112 |
+
});
|
| 113 |
+
const patch = computeIssuePatch(issueState(issue));
|
| 114 |
+
assert.equal(patch.ops.length, 1);
|
| 115 |
+
assert.deepEqual(patch.ops[0]!.remove, [BOUNTY_CLAIMED]);
|
| 116 |
+
assert.deepEqual(patch.ops[0]!.add, []);
|
| 117 |
+
});
|
| 118 |
+
});
|
| 119 |
+
|
| 120 |
+
describe("has value label and assignees", () => {
|
| 121 |
+
it("adds both generic and claimed when both missing", () => {
|
| 122 |
+
const issue = makeIssue({
|
| 123 |
+
number: 1,
|
| 124 |
+
labels: labelNames("bounty: 💰 $300"),
|
| 125 |
+
assignees: [{ login: "alice" }],
|
| 126 |
+
});
|
| 127 |
+
const patch = computeIssuePatch(issueState(issue));
|
| 128 |
+
assert.equal(patch.ops.length, 1);
|
| 129 |
+
assert.deepEqual(patch.ops[0]!.add.sort(), [BOUNTY_CLAIMED, BOUNTY_GENERIC].sort());
|
| 130 |
+
assert.deepEqual(patch.ops[0]!.remove, []);
|
| 131 |
+
});
|
| 132 |
+
|
| 133 |
+
it("produces no patch when generic and claimed both already present", () => {
|
| 134 |
+
const issue = makeIssue({
|
| 135 |
+
number: 1,
|
| 136 |
+
labels: labelNames("bounty: 💰 $300", BOUNTY_GENERIC, BOUNTY_CLAIMED),
|
| 137 |
+
assignees: [{ login: "alice" }],
|
| 138 |
+
});
|
| 139 |
+
const patch = computeIssuePatch(issueState(issue));
|
| 140 |
+
assert.deepEqual(patch.ops, []);
|
| 141 |
+
});
|
| 142 |
+
|
| 143 |
+
it("preserves non-bounty labels untouched", () => {
|
| 144 |
+
const issue = makeIssue({
|
| 145 |
+
number: 1,
|
| 146 |
+
labels: labelNames("type: bug", "bounty: 💰 $100", BOUNTY_GENERIC, BOUNTY_CLAIMED),
|
| 147 |
+
assignees: [{ login: "alice" }],
|
| 148 |
+
});
|
| 149 |
+
const patch = computeIssuePatch(issueState(issue));
|
| 150 |
+
assert.deepEqual(patch.ops, []);
|
| 151 |
+
});
|
| 152 |
+
});
|
| 153 |
+
|
| 154 |
+
describe("rewarded state", () => {
|
| 155 |
+
it("leaves rewarded label alone (owned by PR sync)", () => {
|
| 156 |
+
const issue = makeIssue({
|
| 157 |
+
number: 1,
|
| 158 |
+
labels: labelNames("bounty: 💰 $500", BOUNTY_GENERIC, BOUNTY_REWARDED),
|
| 159 |
+
assignees: [],
|
| 160 |
+
});
|
| 161 |
+
const patch = computeIssuePatch(issueState(issue));
|
| 162 |
+
// rewarded is not touched; only generic should be kept since value label present
|
| 163 |
+
assert.deepEqual(patch.ops, []);
|
| 164 |
+
});
|
| 165 |
+
});
|
| 166 |
+
});
|
| 167 |
+
|
| 168 |
+
// ---------------------------------------------------------------------------
|
| 169 |
+
// linkedIssueNumbers
|
| 170 |
+
// ---------------------------------------------------------------------------
|
| 171 |
+
|
| 172 |
+
describe("linkedIssueNumbers", () => {
|
| 173 |
+
it("returns empty array for null body", () => {
|
| 174 |
+
assert.deepEqual(linkedIssueNumbers(null), []);
|
| 175 |
+
});
|
| 176 |
+
|
| 177 |
+
it("parses closes/fixes/resolves keywords", () => {
|
| 178 |
+
const body = "Closes #10\nFixes #20\nResolves #30";
|
| 179 |
+
assert.deepEqual(linkedIssueNumbers(body), [10, 20, 30]);
|
| 180 |
+
});
|
| 181 |
+
|
| 182 |
+
it("is case-insensitive", () => {
|
| 183 |
+
const body = "CLOSES #5 fixes #6 RESOLVES #7";
|
| 184 |
+
assert.deepEqual(linkedIssueNumbers(body), [5, 6, 7]);
|
| 185 |
+
});
|
| 186 |
+
|
| 187 |
+
it("handles plural forms: close/fix/resolve", () => {
|
| 188 |
+
const body = "close #1 fix #2 resolve #3";
|
| 189 |
+
assert.deepEqual(linkedIssueNumbers(body), [1, 2, 3]);
|
| 190 |
+
});
|
| 191 |
+
|
| 192 |
+
it("returns empty when no keywords present", () => {
|
| 193 |
+
assert.deepEqual(linkedIssueNumbers("See issue #99 for context."), []);
|
| 194 |
+
});
|
| 195 |
+
});
|
| 196 |
+
|
| 197 |
+
// ---------------------------------------------------------------------------
|
| 198 |
+
// computePrPatch — PR rules
|
| 199 |
+
// ---------------------------------------------------------------------------
|
| 200 |
+
|
| 201 |
+
describe("computePrPatch", () => {
|
| 202 |
+
describe("label propagation (pre-merge)", () => {
|
| 203 |
+
it("produces no patch when PR body has no linked issues", () => {
|
| 204 |
+
const pr = makePr({ number: 10, body: "No references here." });
|
| 205 |
+
const patch = computePrPatch({ pr, currentLabels: new Set(), linkedIssues: [] });
|
| 206 |
+
assert.deepEqual(patch.ops, []);
|
| 207 |
+
});
|
| 208 |
+
|
| 209 |
+
it("produces no patch when linked issues have no bounty labels", () => {
|
| 210 |
+
const pr = makePr({ number: 10, body: "Closes #1" });
|
| 211 |
+
const issue = makeIssue({ number: 1, labels: labelNames("type: bug") });
|
| 212 |
+
const patch = computePrPatch({ pr, currentLabels: new Set(), linkedIssues: [issue] });
|
| 213 |
+
assert.deepEqual(patch.ops, []);
|
| 214 |
+
});
|
| 215 |
+
|
| 216 |
+
it("copies bounty value label from linked issue to PR", () => {
|
| 217 |
+
const pr = makePr({ number: 10, body: "Closes #1" });
|
| 218 |
+
const issue = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $500") });
|
| 219 |
+
const patch = computePrPatch({ pr, currentLabels: new Set(), linkedIssues: [issue] });
|
| 220 |
+
|
| 221 |
+
const prOp = patch.ops.find((o) => o.target === 10);
|
| 222 |
+
assert.ok(prOp);
|
| 223 |
+
assert.deepEqual(prOp.add, ["bounty: 💰 $500"]);
|
| 224 |
+
assert.deepEqual(prOp.remove, []);
|
| 225 |
+
});
|
| 226 |
+
|
| 227 |
+
it("does not duplicate a value label already on the PR", () => {
|
| 228 |
+
const pr = makePr({ number: 10, body: "Closes #1" });
|
| 229 |
+
const issue = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $500") });
|
| 230 |
+
const patch = computePrPatch({
|
| 231 |
+
pr,
|
| 232 |
+
currentLabels: new Set(["bounty: 💰 $500"]),
|
| 233 |
+
linkedIssues: [issue],
|
| 234 |
+
});
|
| 235 |
+
assert.deepEqual(patch.ops, []);
|
| 236 |
+
});
|
| 237 |
+
|
| 238 |
+
it("posts a comment on the linked issue when label is propagated", () => {
|
| 239 |
+
const pr = makePr({ number: 10, body: "Closes #1" });
|
| 240 |
+
const issue = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $200") });
|
| 241 |
+
const patch = computePrPatch({ pr, currentLabels: new Set(), linkedIssues: [issue] });
|
| 242 |
+
|
| 243 |
+
const issueOp = patch.ops.find((o) => o.target === 1);
|
| 244 |
+
assert.ok(issueOp);
|
| 245 |
+
assert.ok(issueOp.comment?.includes("PR"));
|
| 246 |
+
assert.ok(issueOp.comment?.includes("#10"));
|
| 247 |
+
assert.ok(issueOp.comment?.includes("contributor"));
|
| 248 |
+
});
|
| 249 |
+
|
| 250 |
+
it("does not post a comment when label was already on the PR", () => {
|
| 251 |
+
const pr = makePr({ number: 10, body: "Closes #1" });
|
| 252 |
+
const issue = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $200") });
|
| 253 |
+
const patch = computePrPatch({
|
| 254 |
+
pr,
|
| 255 |
+
currentLabels: new Set(["bounty: 💰 $200"]),
|
| 256 |
+
linkedIssues: [issue],
|
| 257 |
+
});
|
| 258 |
+
const issueOp = patch.ops.find((o) => o.target === 1);
|
| 259 |
+
assert.ok(!issueOp);
|
| 260 |
+
});
|
| 261 |
+
|
| 262 |
+
it("merges value labels from multiple linked issues onto the PR", () => {
|
| 263 |
+
const pr = makePr({ number: 10, body: "Closes #1\nCloses #2" });
|
| 264 |
+
const issue1 = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $100") });
|
| 265 |
+
const issue2 = makeIssue({ number: 2, labels: labelNames("bounty: 💰 $200") });
|
| 266 |
+
const patch = computePrPatch({
|
| 267 |
+
pr,
|
| 268 |
+
currentLabels: new Set(),
|
| 269 |
+
linkedIssues: [issue1, issue2],
|
| 270 |
+
});
|
| 271 |
+
|
| 272 |
+
const prOp = patch.ops.find((o) => o.target === 10);
|
| 273 |
+
assert.ok(prOp);
|
| 274 |
+
assert.deepEqual(prOp.add.sort(), ["bounty: 💰 $100", "bounty: 💰 $200"].sort());
|
| 275 |
+
});
|
| 276 |
+
});
|
| 277 |
+
|
| 278 |
+
describe("rewarded on merge", () => {
|
| 279 |
+
it("produces no patch when PR was closed but not merged", () => {
|
| 280 |
+
const pr = makePr({ number: 10, state: "closed", merged: false, body: "Closes #1" });
|
| 281 |
+
const issue = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $500") });
|
| 282 |
+
const patch = computePrPatch({
|
| 283 |
+
pr,
|
| 284 |
+
currentLabels: new Set(["bounty: 💰 $500"]),
|
| 285 |
+
linkedIssues: [issue],
|
| 286 |
+
});
|
| 287 |
+
const prOp = patch.ops.find((o) => o.target === 10);
|
| 288 |
+
assert.ok(!prOp?.add.includes(BOUNTY_REWARDED));
|
| 289 |
+
});
|
| 290 |
+
|
| 291 |
+
it("adds rewarded to PR on merge", () => {
|
| 292 |
+
const pr = makePr({
|
| 293 |
+
number: 10,
|
| 294 |
+
merged: true,
|
| 295 |
+
body: "Closes #1",
|
| 296 |
+
labels: labelNames("bounty: 💰 $500"),
|
| 297 |
+
});
|
| 298 |
+
const issue = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $500") });
|
| 299 |
+
const patch = computePrPatch({
|
| 300 |
+
pr,
|
| 301 |
+
currentLabels: new Set(["bounty: 💰 $500"]),
|
| 302 |
+
linkedIssues: [issue],
|
| 303 |
+
});
|
| 304 |
+
|
| 305 |
+
const prOp = patch.ops.find((o) => o.target === 10);
|
| 306 |
+
assert.ok(prOp);
|
| 307 |
+
assert.ok(prOp.add.includes(BOUNTY_REWARDED));
|
| 308 |
+
});
|
| 309 |
+
|
| 310 |
+
it("adds rewarded to linked issue on merge", () => {
|
| 311 |
+
const pr = makePr({
|
| 312 |
+
number: 10,
|
| 313 |
+
merged: true,
|
| 314 |
+
body: "Closes #1",
|
| 315 |
+
labels: labelNames("bounty: 💰 $500"),
|
| 316 |
+
});
|
| 317 |
+
const issue = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $500", BOUNTY_CLAIMED) });
|
| 318 |
+
const patch = computePrPatch({
|
| 319 |
+
pr,
|
| 320 |
+
currentLabels: new Set(["bounty: 💰 $500"]),
|
| 321 |
+
linkedIssues: [issue],
|
| 322 |
+
});
|
| 323 |
+
|
| 324 |
+
const issueOp = patch.ops.find((o) => o.target === 1);
|
| 325 |
+
assert.ok(issueOp);
|
| 326 |
+
assert.ok(issueOp.add.includes(BOUNTY_REWARDED));
|
| 327 |
+
assert.ok(issueOp.remove.includes(BOUNTY_CLAIMED));
|
| 328 |
+
});
|
| 329 |
+
|
| 330 |
+
it("does not add rewarded again when already present", () => {
|
| 331 |
+
const pr = makePr({
|
| 332 |
+
number: 10,
|
| 333 |
+
merged: true,
|
| 334 |
+
body: "Closes #1",
|
| 335 |
+
labels: labelNames("bounty: 💰 $500", BOUNTY_REWARDED),
|
| 336 |
+
});
|
| 337 |
+
const issue = makeIssue({
|
| 338 |
+
number: 1,
|
| 339 |
+
labels: labelNames("bounty: 💰 $500", BOUNTY_REWARDED),
|
| 340 |
+
});
|
| 341 |
+
const patch = computePrPatch({
|
| 342 |
+
pr,
|
| 343 |
+
currentLabels: new Set(["bounty: 💰 $500", BOUNTY_REWARDED]),
|
| 344 |
+
linkedIssues: [issue],
|
| 345 |
+
});
|
| 346 |
+
assert.deepEqual(patch.ops, []);
|
| 347 |
+
});
|
| 348 |
+
|
| 349 |
+
it("skips issues with no bounty label when merged PR has a bounty", () => {
|
| 350 |
+
const pr = makePr({
|
| 351 |
+
number: 10,
|
| 352 |
+
merged: true,
|
| 353 |
+
body: "Closes #1\nCloses #2",
|
| 354 |
+
labels: labelNames("bounty: 💰 $500"),
|
| 355 |
+
});
|
| 356 |
+
const issue1 = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $500") });
|
| 357 |
+
const issue2 = makeIssue({ number: 2, labels: labelNames("type: bug") });
|
| 358 |
+
const patch = computePrPatch({
|
| 359 |
+
pr,
|
| 360 |
+
currentLabels: new Set(["bounty: 💰 $500"]),
|
| 361 |
+
linkedIssues: [issue1, issue2],
|
| 362 |
+
});
|
| 363 |
+
|
| 364 |
+
// issue2 has no bounty so its desired state equals current: no op
|
| 365 |
+
const issue2Op = patch.ops.find((o) => o.target === 2);
|
| 366 |
+
assert.ok(!issue2Op);
|
| 367 |
+
});
|
| 368 |
+
});
|
| 369 |
+
});
|
.github/scripts/bounty/tests/sync-all-issues.test.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { describe, it } from "node:test";
|
| 2 |
+
import assert from "node:assert/strict";
|
| 3 |
+
import { planAllIssues, syncAllIssues } from "../src/sync-all-issues.js";
|
| 4 |
+
import type { GitHubApi } from "../src/api.js";
|
| 5 |
+
import type { Issue, PullRequest } from "../src/types.js";
|
| 6 |
+
|
| 7 |
+
// ---------------------------------------------------------------------------
|
| 8 |
+
// Mock API
|
| 9 |
+
// ---------------------------------------------------------------------------
|
| 10 |
+
|
| 11 |
+
function makeMockApi(issues: Issue[]): GitHubApi & {
|
| 12 |
+
added: Map<number, string[][]>;
|
| 13 |
+
removed: Map<number, string[]>;
|
| 14 |
+
} {
|
| 15 |
+
const added = new Map<number, string[][]>();
|
| 16 |
+
const removed = new Map<number, string[]>();
|
| 17 |
+
|
| 18 |
+
return {
|
| 19 |
+
added,
|
| 20 |
+
removed,
|
| 21 |
+
async getIssue(number) {
|
| 22 |
+
const found = issues.find((i) => i.number === number);
|
| 23 |
+
if (!found) throw new Error(`Issue #${number} not found in mock`);
|
| 24 |
+
return found;
|
| 25 |
+
},
|
| 26 |
+
async getPullRequest(): Promise<PullRequest> {
|
| 27 |
+
throw new Error("not used");
|
| 28 |
+
},
|
| 29 |
+
async listIssuesWithLabelPrefix(_prefix) {
|
| 30 |
+
// Return only real issues (not PRs — pull_request field absent)
|
| 31 |
+
return issues.filter((i) => i.pull_request === undefined);
|
| 32 |
+
},
|
| 33 |
+
async addLabels(target, labels) {
|
| 34 |
+
if (!added.has(target)) added.set(target, []);
|
| 35 |
+
added.get(target)!.push(labels);
|
| 36 |
+
},
|
| 37 |
+
async removeLabel(target, label) {
|
| 38 |
+
if (!removed.has(target)) removed.set(target, []);
|
| 39 |
+
removed.get(target)!.push(label);
|
| 40 |
+
},
|
| 41 |
+
async addComment() {},
|
| 42 |
+
};
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
function makeIssue(overrides: Partial<Issue> & { number: number }): Issue {
|
| 46 |
+
return { title: "Test issue", html_url: `https://github.com/owner/repo/issues/${overrides.number}`, state: "open", labels: [], assignees: [], ...overrides };
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
function labelNames(...names: string[]): { name: string }[] {
|
| 50 |
+
return names.map((name) => ({ name }));
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
// ---------------------------------------------------------------------------
|
| 54 |
+
// Tests
|
| 55 |
+
// ---------------------------------------------------------------------------
|
| 56 |
+
|
| 57 |
+
describe("planAllIssues", () => {
|
| 58 |
+
it("returns empty patch when no issues need changes", async () => {
|
| 59 |
+
const issues = [
|
| 60 |
+
makeIssue({ number: 1, labels: labelNames("bounty: $100", "bounty") }),
|
| 61 |
+
makeIssue({ number: 2, labels: labelNames("type: bug") }),
|
| 62 |
+
];
|
| 63 |
+
const api = makeMockApi(issues);
|
| 64 |
+
const patch = await planAllIssues({ api });
|
| 65 |
+
assert.deepEqual(patch.ops, []);
|
| 66 |
+
});
|
| 67 |
+
|
| 68 |
+
it("plans adds for issues missing the generic bounty label", async () => {
|
| 69 |
+
const issues = [
|
| 70 |
+
makeIssue({ number: 1, labels: labelNames("bounty: $100") }),
|
| 71 |
+
makeIssue({ number: 2, labels: labelNames("bounty: $200") }),
|
| 72 |
+
];
|
| 73 |
+
const api = makeMockApi(issues);
|
| 74 |
+
const patch = await planAllIssues({ api });
|
| 75 |
+
assert.equal(patch.ops.length, 2);
|
| 76 |
+
assert.ok(patch.ops.every((op) => op.add.includes("bounty")));
|
| 77 |
+
});
|
| 78 |
+
|
| 79 |
+
it("plans removal of stale claimed label when issue is unassigned", async () => {
|
| 80 |
+
const issues = [
|
| 81 |
+
makeIssue({
|
| 82 |
+
number: 3,
|
| 83 |
+
labels: labelNames("bounty: $500", "bounty", "bounty: claimed"),
|
| 84 |
+
assignees: [],
|
| 85 |
+
}),
|
| 86 |
+
];
|
| 87 |
+
const api = makeMockApi(issues);
|
| 88 |
+
const patch = await planAllIssues({ api });
|
| 89 |
+
assert.equal(patch.ops.length, 1);
|
| 90 |
+
assert.deepEqual(patch.ops[0]!.remove, ["bounty: claimed"]);
|
| 91 |
+
});
|
| 92 |
+
|
| 93 |
+
it("collects ops for multiple issues with different needs", async () => {
|
| 94 |
+
const issues = [
|
| 95 |
+
makeIssue({ number: 1, labels: labelNames("bounty: $100") }), // missing generic
|
| 96 |
+
makeIssue({ number: 2, labels: labelNames("bounty: $200", "bounty") }), // already in sync
|
| 97 |
+
makeIssue({
|
| 98 |
+
number: 3,
|
| 99 |
+
labels: labelNames("bounty: $300", "bounty", "bounty: claimed"),
|
| 100 |
+
assignees: [],
|
| 101 |
+
}), // stale claimed
|
| 102 |
+
];
|
| 103 |
+
const api = makeMockApi(issues);
|
| 104 |
+
const patch = await planAllIssues({ api });
|
| 105 |
+
assert.equal(patch.ops.length, 2);
|
| 106 |
+
const targets = patch.ops.map((op) => op.target).sort();
|
| 107 |
+
assert.deepEqual(targets, [1, 3]);
|
| 108 |
+
});
|
| 109 |
+
});
|
| 110 |
+
|
| 111 |
+
describe("syncAllIssues", () => {
|
| 112 |
+
it("applies adds and removals across all issues", async () => {
|
| 113 |
+
const issues = [
|
| 114 |
+
makeIssue({ number: 1, labels: labelNames("bounty: $100") }),
|
| 115 |
+
makeIssue({
|
| 116 |
+
number: 2,
|
| 117 |
+
labels: labelNames("bounty: $200", "bounty", "bounty: claimed"),
|
| 118 |
+
assignees: [],
|
| 119 |
+
}),
|
| 120 |
+
];
|
| 121 |
+
const api = makeMockApi(issues);
|
| 122 |
+
await syncAllIssues({ api });
|
| 123 |
+
|
| 124 |
+
assert.ok(api.added.get(1)?.[0]?.includes("bounty"));
|
| 125 |
+
assert.deepEqual(api.removed.get(2), ["bounty: claimed"]);
|
| 126 |
+
});
|
| 127 |
+
|
| 128 |
+
it("returns empty patch when all issues are already in sync", async () => {
|
| 129 |
+
const issues = [
|
| 130 |
+
makeIssue({ number: 1, labels: labelNames("bounty: $100", "bounty") }),
|
| 131 |
+
];
|
| 132 |
+
const api = makeMockApi(issues);
|
| 133 |
+
const patch = await syncAllIssues({ api });
|
| 134 |
+
assert.deepEqual(patch.ops, []);
|
| 135 |
+
assert.equal(api.added.size, 0);
|
| 136 |
+
assert.equal(api.removed.size, 0);
|
| 137 |
+
});
|
| 138 |
+
});
|
.github/scripts/bounty/tests/sync-issue.test.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { describe, it } from "node:test";
|
| 2 |
+
import assert from "node:assert/strict";
|
| 3 |
+
import { syncIssue } from "../src/sync-issue.js";
|
| 4 |
+
import type { GitHubApi } from "../src/api.js";
|
| 5 |
+
import type { Issue, PullRequest } from "../src/types.js";
|
| 6 |
+
|
| 7 |
+
// ---------------------------------------------------------------------------
|
| 8 |
+
// Mock API
|
| 9 |
+
// ---------------------------------------------------------------------------
|
| 10 |
+
|
| 11 |
+
function makeMockApi(issue: Issue): GitHubApi & {
|
| 12 |
+
added: string[][];
|
| 13 |
+
removed: string[];
|
| 14 |
+
comments: string[];
|
| 15 |
+
} {
|
| 16 |
+
const added: string[][] = [];
|
| 17 |
+
const removed: string[] = [];
|
| 18 |
+
const comments: string[] = [];
|
| 19 |
+
|
| 20 |
+
return {
|
| 21 |
+
added,
|
| 22 |
+
removed,
|
| 23 |
+
comments,
|
| 24 |
+
async getIssue() {
|
| 25 |
+
return issue;
|
| 26 |
+
},
|
| 27 |
+
async getPullRequest(): Promise<PullRequest> {
|
| 28 |
+
throw new Error("not used");
|
| 29 |
+
},
|
| 30 |
+
async listIssuesWithLabelPrefix(): Promise<Issue[]> {
|
| 31 |
+
throw new Error("not used");
|
| 32 |
+
},
|
| 33 |
+
async addLabels(_target, labels) {
|
| 34 |
+
added.push(labels);
|
| 35 |
+
},
|
| 36 |
+
async removeLabel(_target, label) {
|
| 37 |
+
removed.push(label);
|
| 38 |
+
},
|
| 39 |
+
async addComment(_target, body) {
|
| 40 |
+
comments.push(body);
|
| 41 |
+
},
|
| 42 |
+
};
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
function makeIssue(overrides: Partial<Issue> & { number: number }): Issue {
|
| 46 |
+
return {
|
| 47 |
+
title: "Test issue",
|
| 48 |
+
html_url: `https://github.com/owner/repo/issues/${overrides.number}`,
|
| 49 |
+
state: "open",
|
| 50 |
+
labels: [],
|
| 51 |
+
assignees: [],
|
| 52 |
+
...overrides,
|
| 53 |
+
};
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
function labelNames(...names: string[]): { name: string }[] {
|
| 57 |
+
return names.map((name) => ({ name }));
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// ---------------------------------------------------------------------------
|
| 61 |
+
// Tests
|
| 62 |
+
// ---------------------------------------------------------------------------
|
| 63 |
+
|
| 64 |
+
describe("syncIssue", () => {
|
| 65 |
+
it("returns empty patch and makes no API calls when issue is already in sync", async () => {
|
| 66 |
+
const issue = makeIssue({
|
| 67 |
+
number: 42,
|
| 68 |
+
labels: labelNames("bounty: 💰 $500", "bounty", "bounty: claimed"),
|
| 69 |
+
assignees: [{ login: "alice" }],
|
| 70 |
+
});
|
| 71 |
+
const api = makeMockApi(issue);
|
| 72 |
+
const patch = await syncIssue({ issueNumber: 42, api });
|
| 73 |
+
|
| 74 |
+
assert.deepEqual(patch.ops, []);
|
| 75 |
+
assert.deepEqual(api.added, []);
|
| 76 |
+
assert.deepEqual(api.removed, []);
|
| 77 |
+
});
|
| 78 |
+
|
| 79 |
+
it("adds generic bounty label when value label is present but generic is missing", async () => {
|
| 80 |
+
const issue = makeIssue({
|
| 81 |
+
number: 42,
|
| 82 |
+
labels: labelNames("bounty: 💰 $300"),
|
| 83 |
+
});
|
| 84 |
+
const api = makeMockApi(issue);
|
| 85 |
+
const patch = await syncIssue({ issueNumber: 42, api });
|
| 86 |
+
|
| 87 |
+
assert.equal(patch.ops.length, 1);
|
| 88 |
+
assert.deepEqual(api.added, [["bounty"]]);
|
| 89 |
+
assert.deepEqual(api.removed, []);
|
| 90 |
+
});
|
| 91 |
+
|
| 92 |
+
it("adds both generic and claimed when value label present and issue assigned", async () => {
|
| 93 |
+
const issue = makeIssue({
|
| 94 |
+
number: 42,
|
| 95 |
+
labels: labelNames("bounty: 💰 $800"),
|
| 96 |
+
assignees: [{ login: "bob" }],
|
| 97 |
+
});
|
| 98 |
+
const api = makeMockApi(issue);
|
| 99 |
+
await syncIssue({ issueNumber: 42, api });
|
| 100 |
+
|
| 101 |
+
assert.equal(api.added.length, 1);
|
| 102 |
+
assert.deepEqual(api.added[0]!.sort(), ["bounty", "bounty: claimed"].sort());
|
| 103 |
+
});
|
| 104 |
+
|
| 105 |
+
it("removes claimed when last assignee is removed", async () => {
|
| 106 |
+
const issue = makeIssue({
|
| 107 |
+
number: 42,
|
| 108 |
+
labels: labelNames("bounty: 💰 $500", "bounty", "bounty: claimed"),
|
| 109 |
+
assignees: [],
|
| 110 |
+
});
|
| 111 |
+
const api = makeMockApi(issue);
|
| 112 |
+
await syncIssue({ issueNumber: 42, api });
|
| 113 |
+
|
| 114 |
+
assert.deepEqual(api.removed, ["bounty: claimed"]);
|
| 115 |
+
assert.deepEqual(api.added, []);
|
| 116 |
+
});
|
| 117 |
+
|
| 118 |
+
it("removes both generic and claimed when value label is removed", async () => {
|
| 119 |
+
const issue = makeIssue({
|
| 120 |
+
number: 42,
|
| 121 |
+
labels: labelNames("bounty", "bounty: claimed"),
|
| 122 |
+
assignees: [{ login: "alice" }],
|
| 123 |
+
});
|
| 124 |
+
const api = makeMockApi(issue);
|
| 125 |
+
await syncIssue({ issueNumber: 42, api });
|
| 126 |
+
|
| 127 |
+
assert.deepEqual(api.removed.sort(), ["bounty", "bounty: claimed"].sort());
|
| 128 |
+
assert.deepEqual(api.added, []);
|
| 129 |
+
});
|
| 130 |
+
|
| 131 |
+
it("does a single batched addLabels call for multiple adds", async () => {
|
| 132 |
+
const issue = makeIssue({
|
| 133 |
+
number: 42,
|
| 134 |
+
labels: labelNames("bounty: 💰 $100"),
|
| 135 |
+
assignees: [{ login: "carol" }],
|
| 136 |
+
});
|
| 137 |
+
const api = makeMockApi(issue);
|
| 138 |
+
await syncIssue({ issueNumber: 42, api });
|
| 139 |
+
|
| 140 |
+
// One call, two labels
|
| 141 |
+
assert.equal(api.added.length, 1);
|
| 142 |
+
assert.equal(api.added[0]!.length, 2);
|
| 143 |
+
});
|
| 144 |
+
});
|
.github/scripts/bounty/tests/sync-pr.test.ts
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { describe, it } from "node:test";
|
| 2 |
+
import assert from "node:assert/strict";
|
| 3 |
+
import { syncPr } from "../src/sync-pr.js";
|
| 4 |
+
import type { GitHubApi } from "../src/api.js";
|
| 5 |
+
import type { Issue, PullRequest } from "../src/types.js";
|
| 6 |
+
import { BOUNTY_CLAIMED, BOUNTY_REWARDED } from "../src/rules.js";
|
| 7 |
+
|
| 8 |
+
// ---------------------------------------------------------------------------
|
| 9 |
+
// Mock API
|
| 10 |
+
// ---------------------------------------------------------------------------
|
| 11 |
+
|
| 12 |
+
interface MockApi extends GitHubApi {
|
| 13 |
+
addedByTarget: Map<number, string[][]>;
|
| 14 |
+
removedByTarget: Map<number, string[]>;
|
| 15 |
+
commentsByTarget: Map<number, string[]>;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
function makeMockApi(pr: PullRequest, issues: Issue[]): MockApi {
|
| 19 |
+
const addedByTarget = new Map<number, string[][]>();
|
| 20 |
+
const removedByTarget = new Map<number, string[]>();
|
| 21 |
+
const commentsByTarget = new Map<number, string[]>();
|
| 22 |
+
|
| 23 |
+
function recordAdd(target: number, labels: string[]) {
|
| 24 |
+
if (!addedByTarget.has(target)) addedByTarget.set(target, []);
|
| 25 |
+
addedByTarget.get(target)!.push(labels);
|
| 26 |
+
}
|
| 27 |
+
function recordRemove(target: number, label: string) {
|
| 28 |
+
if (!removedByTarget.has(target)) removedByTarget.set(target, []);
|
| 29 |
+
removedByTarget.get(target)!.push(label);
|
| 30 |
+
}
|
| 31 |
+
function recordComment(target: number, body: string) {
|
| 32 |
+
if (!commentsByTarget.has(target)) commentsByTarget.set(target, []);
|
| 33 |
+
commentsByTarget.get(target)!.push(body);
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
return {
|
| 37 |
+
addedByTarget,
|
| 38 |
+
removedByTarget,
|
| 39 |
+
commentsByTarget,
|
| 40 |
+
async getIssue(number) {
|
| 41 |
+
const found = issues.find((i) => i.number === number);
|
| 42 |
+
if (!found) throw new Error(`Issue #${number} not found in mock`);
|
| 43 |
+
return found;
|
| 44 |
+
},
|
| 45 |
+
async getPullRequest() {
|
| 46 |
+
return pr;
|
| 47 |
+
},
|
| 48 |
+
async listIssuesWithLabelPrefix(): Promise<Issue[]> {
|
| 49 |
+
throw new Error("not used");
|
| 50 |
+
},
|
| 51 |
+
async addLabels(target, labels) {
|
| 52 |
+
recordAdd(target, labels);
|
| 53 |
+
},
|
| 54 |
+
async removeLabel(target, label) {
|
| 55 |
+
recordRemove(target, label);
|
| 56 |
+
},
|
| 57 |
+
async addComment(target, body) {
|
| 58 |
+
recordComment(target, body);
|
| 59 |
+
},
|
| 60 |
+
};
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
function makeIssue(overrides: Partial<Issue> & { number: number }): Issue {
|
| 64 |
+
return { title: "issue", html_url: `https://github.com/owner/repo/issues/${overrides.number}`, state: "open", labels: [], assignees: [], ...overrides };
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
function makePr(overrides: Partial<PullRequest> & { number: number }): PullRequest {
|
| 68 |
+
return {
|
| 69 |
+
title: "Test PR",
|
| 70 |
+
state: "open",
|
| 71 |
+
merged: false,
|
| 72 |
+
body: null,
|
| 73 |
+
labels: [],
|
| 74 |
+
user: { login: "dev" },
|
| 75 |
+
html_url: `https://github.com/owner/repo/pull/${overrides.number}`,
|
| 76 |
+
...overrides,
|
| 77 |
+
};
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
function labelNames(...names: string[]): { name: string }[] {
|
| 81 |
+
return names.map((name) => ({ name }));
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
// ---------------------------------------------------------------------------
|
| 85 |
+
// Tests
|
| 86 |
+
// ---------------------------------------------------------------------------
|
| 87 |
+
|
| 88 |
+
describe("syncPr", () => {
|
| 89 |
+
describe("label propagation", () => {
|
| 90 |
+
it("returns empty patch when PR has no linked issues", async () => {
|
| 91 |
+
const pr = makePr({ number: 10, body: "No links" });
|
| 92 |
+
const api = makeMockApi(pr, []);
|
| 93 |
+
const patch = await syncPr({ prNumber: 10, api });
|
| 94 |
+
assert.deepEqual(patch.ops, []);
|
| 95 |
+
});
|
| 96 |
+
|
| 97 |
+
it("returns empty patch when linked issue has no bounty label", async () => {
|
| 98 |
+
const pr = makePr({ number: 10, body: "Closes #1" });
|
| 99 |
+
const issue = makeIssue({ number: 1, labels: labelNames("type: bug") });
|
| 100 |
+
const api = makeMockApi(pr, [issue]);
|
| 101 |
+
const patch = await syncPr({ prNumber: 10, api });
|
| 102 |
+
assert.deepEqual(patch.ops, []);
|
| 103 |
+
});
|
| 104 |
+
|
| 105 |
+
it("copies bounty label from linked issue to PR", async () => {
|
| 106 |
+
const pr = makePr({ number: 10, body: "Closes #1" });
|
| 107 |
+
const issue = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $500") });
|
| 108 |
+
const api = makeMockApi(pr, [issue]);
|
| 109 |
+
await syncPr({ prNumber: 10, api });
|
| 110 |
+
|
| 111 |
+
assert.ok(api.addedByTarget.get(10)?.[0]?.includes("bounty: 💰 $500"));
|
| 112 |
+
});
|
| 113 |
+
|
| 114 |
+
it("skips label already on the PR", async () => {
|
| 115 |
+
const pr = makePr({
|
| 116 |
+
number: 10,
|
| 117 |
+
body: "Closes #1",
|
| 118 |
+
labels: labelNames("bounty: 💰 $500"),
|
| 119 |
+
});
|
| 120 |
+
const issue = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $500") });
|
| 121 |
+
const api = makeMockApi(pr, [issue]);
|
| 122 |
+
const patch = await syncPr({ prNumber: 10, api });
|
| 123 |
+
assert.deepEqual(patch.ops, []);
|
| 124 |
+
});
|
| 125 |
+
|
| 126 |
+
it("posts comment on linked issue when label is propagated", async () => {
|
| 127 |
+
const pr = makePr({ number: 10, body: "Closes #1" });
|
| 128 |
+
const issue = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $200") });
|
| 129 |
+
const api = makeMockApi(pr, [issue]);
|
| 130 |
+
await syncPr({ prNumber: 10, api });
|
| 131 |
+
|
| 132 |
+
const comments = api.commentsByTarget.get(1);
|
| 133 |
+
assert.ok(comments && comments.length === 1);
|
| 134 |
+
assert.ok(comments[0]!.includes("#10"));
|
| 135 |
+
assert.ok(comments[0]!.includes("dev"));
|
| 136 |
+
});
|
| 137 |
+
|
| 138 |
+
it("does not post comment when label was already on the PR", async () => {
|
| 139 |
+
const pr = makePr({
|
| 140 |
+
number: 10,
|
| 141 |
+
body: "Closes #1",
|
| 142 |
+
labels: labelNames("bounty: 💰 $200"),
|
| 143 |
+
});
|
| 144 |
+
const issue = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $200") });
|
| 145 |
+
const api = makeMockApi(pr, [issue]);
|
| 146 |
+
await syncPr({ prNumber: 10, api });
|
| 147 |
+
|
| 148 |
+
assert.ok(!api.commentsByTarget.has(1));
|
| 149 |
+
});
|
| 150 |
+
|
| 151 |
+
it("fetches multiple linked issues in parallel and merges labels", async () => {
|
| 152 |
+
const pr = makePr({ number: 10, body: "Closes #1\nCloses #2" });
|
| 153 |
+
const issue1 = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $100") });
|
| 154 |
+
const issue2 = makeIssue({ number: 2, labels: labelNames("bounty: 💰 $200") });
|
| 155 |
+
const api = makeMockApi(pr, [issue1, issue2]);
|
| 156 |
+
await syncPr({ prNumber: 10, api });
|
| 157 |
+
|
| 158 |
+
const added = api.addedByTarget.get(10)?.[0] ?? [];
|
| 159 |
+
assert.ok(added.includes("bounty: 💰 $100"));
|
| 160 |
+
assert.ok(added.includes("bounty: 💰 $200"));
|
| 161 |
+
});
|
| 162 |
+
|
| 163 |
+
it("skips a linked issue that fails to fetch without aborting", async () => {
|
| 164 |
+
const pr = makePr({ number: 10, body: "Closes #1\nCloses #999" });
|
| 165 |
+
const issue1 = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $300") });
|
| 166 |
+
// issue #999 will throw (not in mock store)
|
| 167 |
+
const api = makeMockApi(pr, [issue1]);
|
| 168 |
+
const patch = await syncPr({ prNumber: 10, api });
|
| 169 |
+
|
| 170 |
+
// Issue #1 label still propagated
|
| 171 |
+
assert.ok(api.addedByTarget.get(10)?.[0]?.includes("bounty: 💰 $300"));
|
| 172 |
+
});
|
| 173 |
+
});
|
| 174 |
+
|
| 175 |
+
describe("rewarded on merge", () => {
|
| 176 |
+
it("adds rewarded to PR and linked issue on merge", async () => {
|
| 177 |
+
const pr = makePr({
|
| 178 |
+
number: 10,
|
| 179 |
+
merged: true,
|
| 180 |
+
body: "Closes #1",
|
| 181 |
+
labels: labelNames("bounty: 💰 $500"),
|
| 182 |
+
});
|
| 183 |
+
const issue = makeIssue({
|
| 184 |
+
number: 1,
|
| 185 |
+
labels: labelNames("bounty: 💰 $500", BOUNTY_CLAIMED),
|
| 186 |
+
});
|
| 187 |
+
const api = makeMockApi(pr, [issue]);
|
| 188 |
+
await syncPr({ prNumber: 10, api });
|
| 189 |
+
|
| 190 |
+
assert.ok(api.addedByTarget.get(10)?.[0]?.includes(BOUNTY_REWARDED));
|
| 191 |
+
assert.ok(api.addedByTarget.get(1)?.[0]?.includes(BOUNTY_REWARDED));
|
| 192 |
+
assert.ok(api.removedByTarget.get(1)?.includes(BOUNTY_CLAIMED));
|
| 193 |
+
});
|
| 194 |
+
|
| 195 |
+
it("does not add rewarded again when already present on PR and issue", async () => {
|
| 196 |
+
const pr = makePr({
|
| 197 |
+
number: 10,
|
| 198 |
+
merged: true,
|
| 199 |
+
body: "Closes #1",
|
| 200 |
+
labels: labelNames("bounty: 💰 $500", BOUNTY_REWARDED),
|
| 201 |
+
});
|
| 202 |
+
const issue = makeIssue({
|
| 203 |
+
number: 1,
|
| 204 |
+
labels: labelNames("bounty: 💰 $500", BOUNTY_REWARDED),
|
| 205 |
+
});
|
| 206 |
+
const api = makeMockApi(pr, [issue]);
|
| 207 |
+
const patch = await syncPr({ prNumber: 10, api });
|
| 208 |
+
assert.deepEqual(patch.ops, []);
|
| 209 |
+
});
|
| 210 |
+
|
| 211 |
+
it("does not add rewarded when PR was closed but not merged", async () => {
|
| 212 |
+
const pr = makePr({
|
| 213 |
+
number: 10,
|
| 214 |
+
state: "closed",
|
| 215 |
+
merged: false,
|
| 216 |
+
body: "Closes #1",
|
| 217 |
+
labels: labelNames("bounty: 💰 $500"),
|
| 218 |
+
});
|
| 219 |
+
const issue = makeIssue({ number: 1, labels: labelNames("bounty: 💰 $500") });
|
| 220 |
+
const api = makeMockApi(pr, [issue]);
|
| 221 |
+
await syncPr({ prNumber: 10, api });
|
| 222 |
+
|
| 223 |
+
assert.ok(!api.addedByTarget.get(10)?.[0]?.includes(BOUNTY_REWARDED));
|
| 224 |
+
});
|
| 225 |
+
|
| 226 |
+
it("handles merged PR with no linked issues gracefully", async () => {
|
| 227 |
+
const pr = makePr({
|
| 228 |
+
number: 10,
|
| 229 |
+
merged: true,
|
| 230 |
+
body: "No issue links",
|
| 231 |
+
labels: labelNames("bounty: 💰 $500"),
|
| 232 |
+
});
|
| 233 |
+
const api = makeMockApi(pr, []);
|
| 234 |
+
const patch = await syncPr({ prNumber: 10, api });
|
| 235 |
+
|
| 236 |
+
// Only the PR op
|
| 237 |
+
assert.ok(api.addedByTarget.get(10)?.[0]?.includes(BOUNTY_REWARDED));
|
| 238 |
+
assert.equal(patch.ops.length, 1);
|
| 239 |
+
});
|
| 240 |
+
});
|
| 241 |
+
});
|
.github/workflows/autofix.yml
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -------------------------------------------------------------------
|
| 2 |
+
# ------------------------------- WARNING ---------------------------
|
| 3 |
+
# -------------------------------------------------------------------
|
| 4 |
+
#
|
| 5 |
+
# This file was automatically generated by gh-workflows using the
|
| 6 |
+
# gh-workflow-gen bin. You should add and commit this file to your
|
| 7 |
+
# git repository. **DO NOT EDIT THIS FILE BY HAND!** Any manual changes
|
| 8 |
+
# will be lost if the file is regenerated.
|
| 9 |
+
#
|
| 10 |
+
# To make modifications, update your `build.rs` configuration to adjust
|
| 11 |
+
# the workflow description as needed, then regenerate this file to apply
|
| 12 |
+
# those changes.
|
| 13 |
+
#
|
| 14 |
+
# -------------------------------------------------------------------
|
| 15 |
+
# ----------------------------- END WARNING -------------------------
|
| 16 |
+
# -------------------------------------------------------------------
|
| 17 |
+
|
| 18 |
+
name: autofix.ci
|
| 19 |
+
env:
|
| 20 |
+
RUSTFLAGS: '-Dwarnings'
|
| 21 |
+
'on':
|
| 22 |
+
pull_request:
|
| 23 |
+
types:
|
| 24 |
+
- opened
|
| 25 |
+
- synchronize
|
| 26 |
+
- reopened
|
| 27 |
+
branches:
|
| 28 |
+
- main
|
| 29 |
+
push:
|
| 30 |
+
branches:
|
| 31 |
+
- main
|
| 32 |
+
jobs:
|
| 33 |
+
lint:
|
| 34 |
+
name: Lint Fix
|
| 35 |
+
runs-on: ubuntu-latest
|
| 36 |
+
permissions:
|
| 37 |
+
contents: read
|
| 38 |
+
steps:
|
| 39 |
+
- name: Checkout Code
|
| 40 |
+
uses: actions/checkout@v6
|
| 41 |
+
- name: Install SQLite
|
| 42 |
+
run: sudo apt-get install -y libsqlite3-dev
|
| 43 |
+
- name: Setup Protobuf Compiler
|
| 44 |
+
uses: arduino/setup-protoc@v3
|
| 45 |
+
with:
|
| 46 |
+
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
| 47 |
+
- name: Setup Rust Toolchain
|
| 48 |
+
uses: actions-rust-lang/setup-rust-toolchain@v1
|
| 49 |
+
with:
|
| 50 |
+
toolchain: nightly
|
| 51 |
+
components: clippy, rustfmt
|
| 52 |
+
- name: Cargo Fmt
|
| 53 |
+
run: cargo +nightly fmt --all
|
| 54 |
+
- name: Cargo Clippy
|
| 55 |
+
run: cargo +nightly clippy --all-features --workspace --all-targets --fix --allow-dirty -- -D warnings
|
| 56 |
+
- name: Cargo Clippy String Safety
|
| 57 |
+
run: cargo +nightly clippy --all-features --workspace -- -D clippy::string_slice -D clippy::indexing_slicing -D clippy::disallowed_methods
|
| 58 |
+
- name: Autofix
|
| 59 |
+
uses: autofix-ci/action@7a166d7532b277f34e16238930461bf77f9d7ed8
|
| 60 |
+
concurrency:
|
| 61 |
+
group: autofix-${{github.ref}}
|
| 62 |
+
cancel-in-progress: false
|
.github/workflows/bounty.yml
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -------------------------------------------------------------------
|
| 2 |
+
# ------------------------------- WARNING ---------------------------
|
| 3 |
+
# -------------------------------------------------------------------
|
| 4 |
+
#
|
| 5 |
+
# This file was automatically generated by gh-workflows using the
|
| 6 |
+
# gh-workflow-gen bin. You should add and commit this file to your
|
| 7 |
+
# git repository. **DO NOT EDIT THIS FILE BY HAND!** Any manual changes
|
| 8 |
+
# will be lost if the file is regenerated.
|
| 9 |
+
#
|
| 10 |
+
# To make modifications, update your `build.rs` configuration to adjust
|
| 11 |
+
# the workflow description as needed, then regenerate this file to apply
|
| 12 |
+
# those changes.
|
| 13 |
+
#
|
| 14 |
+
# -------------------------------------------------------------------
|
| 15 |
+
# ----------------------------- END WARNING -------------------------
|
| 16 |
+
# -------------------------------------------------------------------
|
| 17 |
+
|
| 18 |
+
name: Bounty Management
|
| 19 |
+
'on':
|
| 20 |
+
issues:
|
| 21 |
+
types:
|
| 22 |
+
- assigned
|
| 23 |
+
- unassigned
|
| 24 |
+
- labeled
|
| 25 |
+
- unlabeled
|
| 26 |
+
pull_request:
|
| 27 |
+
types:
|
| 28 |
+
- opened
|
| 29 |
+
- edited
|
| 30 |
+
- reopened
|
| 31 |
+
pull_request_target:
|
| 32 |
+
types:
|
| 33 |
+
- closed
|
| 34 |
+
schedule:
|
| 35 |
+
- cron: '0 2 * * *'
|
| 36 |
+
permissions:
|
| 37 |
+
issues: write
|
| 38 |
+
pull-requests: write
|
| 39 |
+
jobs:
|
| 40 |
+
sync-all-issues:
|
| 41 |
+
name: Sync all bounty issues
|
| 42 |
+
runs-on: ubuntu-latest
|
| 43 |
+
permissions:
|
| 44 |
+
issues: write
|
| 45 |
+
steps:
|
| 46 |
+
- name: Checkout
|
| 47 |
+
uses: actions/checkout@v6
|
| 48 |
+
- name: Install npm packages
|
| 49 |
+
run: npm install
|
| 50 |
+
- name: Sync all bounty labels
|
| 51 |
+
run: npx tsx .github/scripts/bounty/src/sync-all-issues.ts --repo ${{ github.repository }} --token ${{ secrets.GITHUB_TOKEN }} --execute
|
| 52 |
+
sync-pr:
|
| 53 |
+
name: Sync PR bounty labels
|
| 54 |
+
runs-on: ubuntu-latest
|
| 55 |
+
permissions:
|
| 56 |
+
issues: write
|
| 57 |
+
pull-requests: write
|
| 58 |
+
steps:
|
| 59 |
+
- name: Checkout
|
| 60 |
+
uses: actions/checkout@v6
|
| 61 |
+
- name: Install npm packages
|
| 62 |
+
run: npm install
|
| 63 |
+
- name: Sync bounty labels
|
| 64 |
+
run: npx tsx .github/scripts/bounty/src/sync-pr.ts --pr ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --token ${{ secrets.GITHUB_TOKEN }}
|
.github/workflows/ci.yml
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -------------------------------------------------------------------
|
| 2 |
+
# ------------------------------- WARNING ---------------------------
|
| 3 |
+
# -------------------------------------------------------------------
|
| 4 |
+
#
|
| 5 |
+
# This file was automatically generated by gh-workflows using the
|
| 6 |
+
# gh-workflow-gen bin. You should add and commit this file to your
|
| 7 |
+
# git repository. **DO NOT EDIT THIS FILE BY HAND!** Any manual changes
|
| 8 |
+
# will be lost if the file is regenerated.
|
| 9 |
+
#
|
| 10 |
+
# To make modifications, update your `build.rs` configuration to adjust
|
| 11 |
+
# the workflow description as needed, then regenerate this file to apply
|
| 12 |
+
# those changes.
|
| 13 |
+
#
|
| 14 |
+
# -------------------------------------------------------------------
|
| 15 |
+
# ----------------------------- END WARNING -------------------------
|
| 16 |
+
# -------------------------------------------------------------------
|
| 17 |
+
|
| 18 |
+
name: ci
|
| 19 |
+
env:
|
| 20 |
+
RUSTFLAGS: '-Dwarnings'
|
| 21 |
+
OPENROUTER_API_KEY: ${{secrets.OPENROUTER_API_KEY}}
|
| 22 |
+
'on':
|
| 23 |
+
pull_request:
|
| 24 |
+
types:
|
| 25 |
+
- opened
|
| 26 |
+
- synchronize
|
| 27 |
+
- reopened
|
| 28 |
+
- labeled
|
| 29 |
+
branches:
|
| 30 |
+
- main
|
| 31 |
+
push:
|
| 32 |
+
branches:
|
| 33 |
+
- main
|
| 34 |
+
tags:
|
| 35 |
+
- v*
|
| 36 |
+
jobs:
|
| 37 |
+
build:
|
| 38 |
+
name: Build and Test
|
| 39 |
+
runs-on: ubuntu-latest
|
| 40 |
+
permissions:
|
| 41 |
+
contents: read
|
| 42 |
+
steps:
|
| 43 |
+
- name: Checkout Code
|
| 44 |
+
uses: actions/checkout@v6
|
| 45 |
+
- name: Setup Protobuf Compiler
|
| 46 |
+
uses: arduino/setup-protoc@v3
|
| 47 |
+
with:
|
| 48 |
+
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
| 49 |
+
- name: Setup Rust Toolchain
|
| 50 |
+
uses: actions-rust-lang/setup-rust-toolchain@v1
|
| 51 |
+
with:
|
| 52 |
+
toolchain: stable
|
| 53 |
+
- name: Install cargo-llvm-cov
|
| 54 |
+
run: cargo install cargo-llvm-cov
|
| 55 |
+
- name: Generate coverage
|
| 56 |
+
run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info
|
| 57 |
+
zsh_rprompt_perf:
|
| 58 |
+
name: 'Performance: zsh rprompt'
|
| 59 |
+
runs-on: ubuntu-latest
|
| 60 |
+
permissions:
|
| 61 |
+
contents: read
|
| 62 |
+
steps:
|
| 63 |
+
- name: Checkout Code
|
| 64 |
+
uses: actions/checkout@v6
|
| 65 |
+
- name: Setup Protobuf Compiler
|
| 66 |
+
uses: arduino/setup-protoc@v3
|
| 67 |
+
with:
|
| 68 |
+
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
| 69 |
+
- name: Setup Rust Toolchain
|
| 70 |
+
uses: actions-rust-lang/setup-rust-toolchain@v1
|
| 71 |
+
with:
|
| 72 |
+
toolchain: stable
|
| 73 |
+
- name: Run performance benchmark
|
| 74 |
+
run: './scripts/benchmark.sh --threshold 60 zsh rprompt'
|
| 75 |
+
draft_release:
|
| 76 |
+
needs:
|
| 77 |
+
- build
|
| 78 |
+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
| 79 |
+
name: Draft Release
|
| 80 |
+
runs-on: ubuntu-latest
|
| 81 |
+
permissions:
|
| 82 |
+
contents: write
|
| 83 |
+
pull-requests: write
|
| 84 |
+
outputs:
|
| 85 |
+
crate_release_name: ${{ steps.set_output.outputs.crate_release_name }}
|
| 86 |
+
crate_release_id: ${{ steps.set_output.outputs.crate_release_id }}
|
| 87 |
+
steps:
|
| 88 |
+
- name: Checkout Code
|
| 89 |
+
uses: actions/checkout@v6
|
| 90 |
+
- id: create_release
|
| 91 |
+
name: Draft Release
|
| 92 |
+
uses: release-drafter/release-drafter@v7
|
| 93 |
+
with:
|
| 94 |
+
config-name: release-drafter.yml
|
| 95 |
+
env:
|
| 96 |
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
| 97 |
+
- id: set_output
|
| 98 |
+
name: Export Outputs
|
| 99 |
+
run: echo "crate_release_id=${{ steps.create_release.outputs.id }}" >> $GITHUB_OUTPUT && echo "crate_release_name=${{ steps.create_release.outputs.tag_name }}" >> $GITHUB_OUTPUT
|
| 100 |
+
draft_release_pr:
|
| 101 |
+
if: 'github.event_name == ''pull_request'' && contains(github.event.pull_request.labels.*.name, ''ci: build all targets'')'
|
| 102 |
+
name: Draft Release for PR
|
| 103 |
+
runs-on: ubuntu-latest
|
| 104 |
+
outputs:
|
| 105 |
+
crate_release_name: ${{ steps.set_output.outputs.crate_release_name }}
|
| 106 |
+
crate_release_id: ${{ steps.set_output.outputs.crate_release_id }}
|
| 107 |
+
steps:
|
| 108 |
+
- name: Checkout Code
|
| 109 |
+
uses: actions/checkout@v6
|
| 110 |
+
- id: set_output
|
| 111 |
+
name: Set Release Version
|
| 112 |
+
run: echo "crate_release_name=pr-build-${{ github.event.number }}" >> $GITHUB_OUTPUT && echo "crate_release_id=pr-build-${{ github.event.number }}" >> $GITHUB_OUTPUT
|
| 113 |
+
build_release:
|
| 114 |
+
needs:
|
| 115 |
+
- draft_release
|
| 116 |
+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
| 117 |
+
name: build-release
|
| 118 |
+
runs-on: ${{ matrix.os }}
|
| 119 |
+
permissions:
|
| 120 |
+
contents: write
|
| 121 |
+
pull-requests: write
|
| 122 |
+
strategy:
|
| 123 |
+
matrix:
|
| 124 |
+
include:
|
| 125 |
+
- binary_name: forge-x86_64-unknown-linux-musl
|
| 126 |
+
binary_path: target/x86_64-unknown-linux-musl/release/forge
|
| 127 |
+
cross: 'true'
|
| 128 |
+
os: ubuntu-latest
|
| 129 |
+
target: x86_64-unknown-linux-musl
|
| 130 |
+
- binary_name: forge-aarch64-unknown-linux-musl
|
| 131 |
+
binary_path: target/aarch64-unknown-linux-musl/release/forge
|
| 132 |
+
cross: 'true'
|
| 133 |
+
os: ubuntu-latest
|
| 134 |
+
target: aarch64-unknown-linux-musl
|
| 135 |
+
- binary_name: forge-x86_64-unknown-linux-gnu
|
| 136 |
+
binary_path: target/x86_64-unknown-linux-gnu/release/forge
|
| 137 |
+
cross: 'false'
|
| 138 |
+
os: ubuntu-latest
|
| 139 |
+
target: x86_64-unknown-linux-gnu
|
| 140 |
+
- binary_name: forge-aarch64-unknown-linux-gnu
|
| 141 |
+
binary_path: target/aarch64-unknown-linux-gnu/release/forge
|
| 142 |
+
cross: 'true'
|
| 143 |
+
os: ubuntu-latest
|
| 144 |
+
target: aarch64-unknown-linux-gnu
|
| 145 |
+
- binary_name: forge-x86_64-apple-darwin
|
| 146 |
+
binary_path: target/x86_64-apple-darwin/release/forge
|
| 147 |
+
cross: 'false'
|
| 148 |
+
os: macos-latest
|
| 149 |
+
target: x86_64-apple-darwin
|
| 150 |
+
- binary_name: forge-aarch64-apple-darwin
|
| 151 |
+
binary_path: target/aarch64-apple-darwin/release/forge
|
| 152 |
+
cross: 'false'
|
| 153 |
+
os: macos-latest
|
| 154 |
+
target: aarch64-apple-darwin
|
| 155 |
+
- binary_name: forge-x86_64-pc-windows-msvc.exe
|
| 156 |
+
binary_path: target/x86_64-pc-windows-msvc/release/forge.exe
|
| 157 |
+
cross: 'false'
|
| 158 |
+
os: windows-latest
|
| 159 |
+
target: x86_64-pc-windows-msvc
|
| 160 |
+
- binary_name: forge-aarch64-pc-windows-msvc.exe
|
| 161 |
+
binary_path: target/aarch64-pc-windows-msvc/release/forge.exe
|
| 162 |
+
cross: 'false'
|
| 163 |
+
os: windows-latest
|
| 164 |
+
target: aarch64-pc-windows-msvc
|
| 165 |
+
- binary_name: forge-aarch64-linux-android
|
| 166 |
+
binary_path: target/aarch64-linux-android/release/forge
|
| 167 |
+
cross: 'true'
|
| 168 |
+
os: ubuntu-latest
|
| 169 |
+
target: aarch64-linux-android
|
| 170 |
+
steps:
|
| 171 |
+
- name: Checkout Code
|
| 172 |
+
uses: actions/checkout@v6
|
| 173 |
+
- name: Setup Protobuf Compiler
|
| 174 |
+
if: ${{ matrix.cross == 'false' }}
|
| 175 |
+
uses: arduino/setup-protoc@v3
|
| 176 |
+
with:
|
| 177 |
+
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
| 178 |
+
- name: Setup Cross Toolchain
|
| 179 |
+
if: ${{ matrix.cross == 'false' }}
|
| 180 |
+
uses: taiki-e/setup-cross-toolchain-action@v1
|
| 181 |
+
with:
|
| 182 |
+
target: ${{ matrix.target }}
|
| 183 |
+
- name: Add Rust target
|
| 184 |
+
if: ${{ matrix.cross == 'false' }}
|
| 185 |
+
run: rustup target add ${{ matrix.target }}
|
| 186 |
+
- name: Set Rust Flags
|
| 187 |
+
if: '!(contains(matrix.target, ''-unknown-linux-'') || contains(matrix.target, ''-android''))'
|
| 188 |
+
run: echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV
|
| 189 |
+
- name: Build Binary
|
| 190 |
+
uses: ClementTsang/cargo-action@v0.0.7
|
| 191 |
+
with:
|
| 192 |
+
command: build --release
|
| 193 |
+
args: '--target ${{ matrix.target }}'
|
| 194 |
+
use-cross: ${{ matrix.cross }}
|
| 195 |
+
cross-version: '0.2.5'
|
| 196 |
+
env:
|
| 197 |
+
RUSTFLAGS: ${{ env.RUSTFLAGS }}
|
| 198 |
+
POSTHOG_API_SECRET: ${{secrets.POSTHOG_API_SECRET}}
|
| 199 |
+
APP_VERSION: ${{ needs.draft_release.outputs.crate_release_name }}
|
| 200 |
+
- name: Copy Binary
|
| 201 |
+
run: cp ${{ matrix.binary_path }} ${{ matrix.binary_name }}
|
| 202 |
+
- name: Upload to Release
|
| 203 |
+
uses: xresloader/upload-to-github-release@v1
|
| 204 |
+
with:
|
| 205 |
+
release_id: ${{ needs.draft_release.outputs.crate_release_id }}
|
| 206 |
+
file: ${{ matrix.binary_name }}
|
| 207 |
+
overwrite: 'true'
|
| 208 |
+
build_release_pr:
|
| 209 |
+
needs:
|
| 210 |
+
- draft_release_pr
|
| 211 |
+
if: 'github.event_name == ''pull_request'' && contains(github.event.pull_request.labels.*.name, ''ci: build all targets'')'
|
| 212 |
+
name: build-release
|
| 213 |
+
runs-on: ${{ matrix.os }}
|
| 214 |
+
permissions:
|
| 215 |
+
contents: write
|
| 216 |
+
pull-requests: write
|
| 217 |
+
strategy:
|
| 218 |
+
matrix:
|
| 219 |
+
include:
|
| 220 |
+
- binary_name: forge-x86_64-unknown-linux-musl
|
| 221 |
+
binary_path: target/x86_64-unknown-linux-musl/release/forge
|
| 222 |
+
cross: 'true'
|
| 223 |
+
os: ubuntu-latest
|
| 224 |
+
target: x86_64-unknown-linux-musl
|
| 225 |
+
- binary_name: forge-aarch64-unknown-linux-musl
|
| 226 |
+
binary_path: target/aarch64-unknown-linux-musl/release/forge
|
| 227 |
+
cross: 'true'
|
| 228 |
+
os: ubuntu-latest
|
| 229 |
+
target: aarch64-unknown-linux-musl
|
| 230 |
+
- binary_name: forge-x86_64-unknown-linux-gnu
|
| 231 |
+
binary_path: target/x86_64-unknown-linux-gnu/release/forge
|
| 232 |
+
cross: 'false'
|
| 233 |
+
os: ubuntu-latest
|
| 234 |
+
target: x86_64-unknown-linux-gnu
|
| 235 |
+
- binary_name: forge-aarch64-unknown-linux-gnu
|
| 236 |
+
binary_path: target/aarch64-unknown-linux-gnu/release/forge
|
| 237 |
+
cross: 'true'
|
| 238 |
+
os: ubuntu-latest
|
| 239 |
+
target: aarch64-unknown-linux-gnu
|
| 240 |
+
- binary_name: forge-x86_64-apple-darwin
|
| 241 |
+
binary_path: target/x86_64-apple-darwin/release/forge
|
| 242 |
+
cross: 'false'
|
| 243 |
+
os: macos-latest
|
| 244 |
+
target: x86_64-apple-darwin
|
| 245 |
+
- binary_name: forge-aarch64-apple-darwin
|
| 246 |
+
binary_path: target/aarch64-apple-darwin/release/forge
|
| 247 |
+
cross: 'false'
|
| 248 |
+
os: macos-latest
|
| 249 |
+
target: aarch64-apple-darwin
|
| 250 |
+
- binary_name: forge-x86_64-pc-windows-msvc.exe
|
| 251 |
+
binary_path: target/x86_64-pc-windows-msvc/release/forge.exe
|
| 252 |
+
cross: 'false'
|
| 253 |
+
os: windows-latest
|
| 254 |
+
target: x86_64-pc-windows-msvc
|
| 255 |
+
- binary_name: forge-aarch64-pc-windows-msvc.exe
|
| 256 |
+
binary_path: target/aarch64-pc-windows-msvc/release/forge.exe
|
| 257 |
+
cross: 'false'
|
| 258 |
+
os: windows-latest
|
| 259 |
+
target: aarch64-pc-windows-msvc
|
| 260 |
+
- binary_name: forge-aarch64-linux-android
|
| 261 |
+
binary_path: target/aarch64-linux-android/release/forge
|
| 262 |
+
cross: 'true'
|
| 263 |
+
os: ubuntu-latest
|
| 264 |
+
target: aarch64-linux-android
|
| 265 |
+
steps:
|
| 266 |
+
- name: Checkout Code
|
| 267 |
+
uses: actions/checkout@v6
|
| 268 |
+
- name: Setup Protobuf Compiler
|
| 269 |
+
if: ${{ matrix.cross == 'false' }}
|
| 270 |
+
uses: arduino/setup-protoc@v3
|
| 271 |
+
with:
|
| 272 |
+
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
| 273 |
+
- name: Setup Cross Toolchain
|
| 274 |
+
if: ${{ matrix.cross == 'false' }}
|
| 275 |
+
uses: taiki-e/setup-cross-toolchain-action@v1
|
| 276 |
+
with:
|
| 277 |
+
target: ${{ matrix.target }}
|
| 278 |
+
- name: Add Rust target
|
| 279 |
+
if: ${{ matrix.cross == 'false' }}
|
| 280 |
+
run: rustup target add ${{ matrix.target }}
|
| 281 |
+
- name: Set Rust Flags
|
| 282 |
+
if: '!(contains(matrix.target, ''-unknown-linux-'') || contains(matrix.target, ''-android''))'
|
| 283 |
+
run: echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV
|
| 284 |
+
- name: Build Binary
|
| 285 |
+
uses: ClementTsang/cargo-action@v0.0.7
|
| 286 |
+
with:
|
| 287 |
+
command: build --release
|
| 288 |
+
args: '--target ${{ matrix.target }}'
|
| 289 |
+
use-cross: ${{ matrix.cross }}
|
| 290 |
+
cross-version: '0.2.5'
|
| 291 |
+
env:
|
| 292 |
+
RUSTFLAGS: ${{ env.RUSTFLAGS }}
|
| 293 |
+
POSTHOG_API_SECRET: ${{secrets.POSTHOG_API_SECRET}}
|
| 294 |
+
APP_VERSION: ${{ needs.draft_release_pr.outputs.crate_release_name }}
|
| 295 |
+
concurrency:
|
| 296 |
+
group: ${{ github.workflow }}-${{ github.ref }}
|
.github/workflows/labels.yml
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -------------------------------------------------------------------
|
| 2 |
+
# ------------------------------- WARNING ---------------------------
|
| 3 |
+
# -------------------------------------------------------------------
|
| 4 |
+
#
|
| 5 |
+
# This file was automatically generated by gh-workflows using the
|
| 6 |
+
# gh-workflow-gen bin. You should add and commit this file to your
|
| 7 |
+
# git repository. **DO NOT EDIT THIS FILE BY HAND!** Any manual changes
|
| 8 |
+
# will be lost if the file is regenerated.
|
| 9 |
+
#
|
| 10 |
+
# To make modifications, update your `build.rs` configuration to adjust
|
| 11 |
+
# the workflow description as needed, then regenerate this file to apply
|
| 12 |
+
# those changes.
|
| 13 |
+
#
|
| 14 |
+
# -------------------------------------------------------------------
|
| 15 |
+
# ----------------------------- END WARNING -------------------------
|
| 16 |
+
# -------------------------------------------------------------------
|
| 17 |
+
|
| 18 |
+
name: Github Label Sync
|
| 19 |
+
'on':
|
| 20 |
+
push:
|
| 21 |
+
branches:
|
| 22 |
+
- main
|
| 23 |
+
permissions:
|
| 24 |
+
contents: write
|
| 25 |
+
jobs:
|
| 26 |
+
label-sync:
|
| 27 |
+
name: label-sync
|
| 28 |
+
runs-on: ubuntu-latest
|
| 29 |
+
permissions:
|
| 30 |
+
issues: write
|
| 31 |
+
steps:
|
| 32 |
+
- name: Checkout
|
| 33 |
+
uses: actions/checkout@v6
|
| 34 |
+
- name: Sync labels
|
| 35 |
+
run: |-
|
| 36 |
+
npx github-label-sync \
|
| 37 |
+
--access-token ${{ secrets.GITHUB_TOKEN }} \
|
| 38 |
+
--labels ".github/labels.json" \
|
| 39 |
+
${{ github.repository }}
|
.github/workflows/release-drafter.yml
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -------------------------------------------------------------------
|
| 2 |
+
# ------------------------------- WARNING ---------------------------
|
| 3 |
+
# -------------------------------------------------------------------
|
| 4 |
+
#
|
| 5 |
+
# This file was automatically generated by gh-workflows using the
|
| 6 |
+
# gh-workflow-gen bin. You should add and commit this file to your
|
| 7 |
+
# git repository. **DO NOT EDIT THIS FILE BY HAND!** Any manual changes
|
| 8 |
+
# will be lost if the file is regenerated.
|
| 9 |
+
#
|
| 10 |
+
# To make modifications, update your `build.rs` configuration to adjust
|
| 11 |
+
# the workflow description as needed, then regenerate this file to apply
|
| 12 |
+
# those changes.
|
| 13 |
+
#
|
| 14 |
+
# -------------------------------------------------------------------
|
| 15 |
+
# ----------------------------- END WARNING -------------------------
|
| 16 |
+
# -------------------------------------------------------------------
|
| 17 |
+
|
| 18 |
+
name: Release Drafter
|
| 19 |
+
'on':
|
| 20 |
+
pull_request_target:
|
| 21 |
+
types:
|
| 22 |
+
- opened
|
| 23 |
+
- reopened
|
| 24 |
+
- synchronize
|
| 25 |
+
- labeled
|
| 26 |
+
- unlabeled
|
| 27 |
+
- closed
|
| 28 |
+
branches:
|
| 29 |
+
- main
|
| 30 |
+
push:
|
| 31 |
+
branches:
|
| 32 |
+
- main
|
| 33 |
+
permissions:
|
| 34 |
+
contents: write
|
| 35 |
+
pull-requests: write
|
| 36 |
+
jobs:
|
| 37 |
+
update_release_draft:
|
| 38 |
+
name: update_release_draft
|
| 39 |
+
runs-on: ubuntu-latest
|
| 40 |
+
steps:
|
| 41 |
+
- name: Auto Labeler
|
| 42 |
+
if: github.event_name == 'pull_request_target'
|
| 43 |
+
uses: release-drafter/release-drafter/autolabeler@v7
|
| 44 |
+
with:
|
| 45 |
+
config-name: release-drafter.yml
|
| 46 |
+
env:
|
| 47 |
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
| 48 |
+
- name: Release Drafter
|
| 49 |
+
uses: release-drafter/release-drafter@v7
|
| 50 |
+
with:
|
| 51 |
+
config-name: release-drafter.yml
|
| 52 |
+
env:
|
| 53 |
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
.github/workflows/release.yml
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -------------------------------------------------------------------
|
| 2 |
+
# ------------------------------- WARNING ---------------------------
|
| 3 |
+
# -------------------------------------------------------------------
|
| 4 |
+
#
|
| 5 |
+
# This file was automatically generated by gh-workflows using the
|
| 6 |
+
# gh-workflow-gen bin. You should add and commit this file to your
|
| 7 |
+
# git repository. **DO NOT EDIT THIS FILE BY HAND!** Any manual changes
|
| 8 |
+
# will be lost if the file is regenerated.
|
| 9 |
+
#
|
| 10 |
+
# To make modifications, update your `build.rs` configuration to adjust
|
| 11 |
+
# the workflow description as needed, then regenerate this file to apply
|
| 12 |
+
# those changes.
|
| 13 |
+
#
|
| 14 |
+
# -------------------------------------------------------------------
|
| 15 |
+
# ----------------------------- END WARNING -------------------------
|
| 16 |
+
# -------------------------------------------------------------------
|
| 17 |
+
|
| 18 |
+
name: Multi Channel Release
|
| 19 |
+
'on':
|
| 20 |
+
release:
|
| 21 |
+
types:
|
| 22 |
+
- published
|
| 23 |
+
permissions:
|
| 24 |
+
contents: write
|
| 25 |
+
pull-requests: write
|
| 26 |
+
jobs:
|
| 27 |
+
build_release:
|
| 28 |
+
name: build-release
|
| 29 |
+
runs-on: ${{ matrix.os }}
|
| 30 |
+
permissions:
|
| 31 |
+
contents: write
|
| 32 |
+
pull-requests: write
|
| 33 |
+
strategy:
|
| 34 |
+
matrix:
|
| 35 |
+
include:
|
| 36 |
+
- binary_name: forge-x86_64-unknown-linux-musl
|
| 37 |
+
binary_path: target/x86_64-unknown-linux-musl/release/forge
|
| 38 |
+
cross: 'true'
|
| 39 |
+
os: ubuntu-latest
|
| 40 |
+
target: x86_64-unknown-linux-musl
|
| 41 |
+
- binary_name: forge-aarch64-unknown-linux-musl
|
| 42 |
+
binary_path: target/aarch64-unknown-linux-musl/release/forge
|
| 43 |
+
cross: 'true'
|
| 44 |
+
os: ubuntu-latest
|
| 45 |
+
target: aarch64-unknown-linux-musl
|
| 46 |
+
- binary_name: forge-x86_64-unknown-linux-gnu
|
| 47 |
+
binary_path: target/x86_64-unknown-linux-gnu/release/forge
|
| 48 |
+
cross: 'false'
|
| 49 |
+
os: ubuntu-latest
|
| 50 |
+
target: x86_64-unknown-linux-gnu
|
| 51 |
+
- binary_name: forge-aarch64-unknown-linux-gnu
|
| 52 |
+
binary_path: target/aarch64-unknown-linux-gnu/release/forge
|
| 53 |
+
cross: 'true'
|
| 54 |
+
os: ubuntu-latest
|
| 55 |
+
target: aarch64-unknown-linux-gnu
|
| 56 |
+
- binary_name: forge-x86_64-apple-darwin
|
| 57 |
+
binary_path: target/x86_64-apple-darwin/release/forge
|
| 58 |
+
cross: 'false'
|
| 59 |
+
os: macos-latest
|
| 60 |
+
target: x86_64-apple-darwin
|
| 61 |
+
- binary_name: forge-aarch64-apple-darwin
|
| 62 |
+
binary_path: target/aarch64-apple-darwin/release/forge
|
| 63 |
+
cross: 'false'
|
| 64 |
+
os: macos-latest
|
| 65 |
+
target: aarch64-apple-darwin
|
| 66 |
+
- binary_name: forge-x86_64-pc-windows-msvc.exe
|
| 67 |
+
binary_path: target/x86_64-pc-windows-msvc/release/forge.exe
|
| 68 |
+
cross: 'false'
|
| 69 |
+
os: windows-latest
|
| 70 |
+
target: x86_64-pc-windows-msvc
|
| 71 |
+
- binary_name: forge-aarch64-pc-windows-msvc.exe
|
| 72 |
+
binary_path: target/aarch64-pc-windows-msvc/release/forge.exe
|
| 73 |
+
cross: 'false'
|
| 74 |
+
os: windows-latest
|
| 75 |
+
target: aarch64-pc-windows-msvc
|
| 76 |
+
- binary_name: forge-aarch64-linux-android
|
| 77 |
+
binary_path: target/aarch64-linux-android/release/forge
|
| 78 |
+
cross: 'true'
|
| 79 |
+
os: ubuntu-latest
|
| 80 |
+
target: aarch64-linux-android
|
| 81 |
+
steps:
|
| 82 |
+
- name: Checkout Code
|
| 83 |
+
uses: actions/checkout@v6
|
| 84 |
+
- name: Setup Protobuf Compiler
|
| 85 |
+
if: ${{ matrix.cross == 'false' }}
|
| 86 |
+
uses: arduino/setup-protoc@v3
|
| 87 |
+
with:
|
| 88 |
+
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
| 89 |
+
- name: Setup Cross Toolchain
|
| 90 |
+
if: ${{ matrix.cross == 'false' }}
|
| 91 |
+
uses: taiki-e/setup-cross-toolchain-action@v1
|
| 92 |
+
with:
|
| 93 |
+
target: ${{ matrix.target }}
|
| 94 |
+
- name: Add Rust target
|
| 95 |
+
if: ${{ matrix.cross == 'false' }}
|
| 96 |
+
run: rustup target add ${{ matrix.target }}
|
| 97 |
+
- name: Set Rust Flags
|
| 98 |
+
if: '!(contains(matrix.target, ''-unknown-linux-'') || contains(matrix.target, ''-android''))'
|
| 99 |
+
run: echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV
|
| 100 |
+
- name: Build Binary
|
| 101 |
+
uses: ClementTsang/cargo-action@v0.0.7
|
| 102 |
+
with:
|
| 103 |
+
command: build --release
|
| 104 |
+
args: '--target ${{ matrix.target }}'
|
| 105 |
+
use-cross: ${{ matrix.cross }}
|
| 106 |
+
cross-version: '0.2.5'
|
| 107 |
+
env:
|
| 108 |
+
RUSTFLAGS: ${{ env.RUSTFLAGS }}
|
| 109 |
+
POSTHOG_API_SECRET: ${{secrets.POSTHOG_API_SECRET}}
|
| 110 |
+
APP_VERSION: ${{ github.event.release.tag_name }}
|
| 111 |
+
- name: Copy Binary
|
| 112 |
+
run: cp ${{ matrix.binary_path }} ${{ matrix.binary_name }}
|
| 113 |
+
- name: Upload to Release
|
| 114 |
+
uses: xresloader/upload-to-github-release@v1
|
| 115 |
+
with:
|
| 116 |
+
release_id: ${{ github.event.release.id }}
|
| 117 |
+
file: ${{ matrix.binary_name }}
|
| 118 |
+
overwrite: 'true'
|
| 119 |
+
npm_release:
|
| 120 |
+
needs:
|
| 121 |
+
- build_release
|
| 122 |
+
name: npm_release
|
| 123 |
+
runs-on: ubuntu-latest
|
| 124 |
+
strategy:
|
| 125 |
+
matrix:
|
| 126 |
+
repository:
|
| 127 |
+
- antinomyhq/npm-code-forge
|
| 128 |
+
- antinomyhq/npm-forgecode
|
| 129 |
+
steps:
|
| 130 |
+
- name: Checkout Code
|
| 131 |
+
uses: actions/checkout@v6
|
| 132 |
+
with:
|
| 133 |
+
repository: ${{ matrix.repository }}
|
| 134 |
+
ref: main
|
| 135 |
+
token: ${{ secrets.NPM_ACCESS }}
|
| 136 |
+
- name: Update NPM Package
|
| 137 |
+
run: './update-package.sh ${{ github.event.release.tag_name }}'
|
| 138 |
+
env:
|
| 139 |
+
AUTO_PUSH: 'true'
|
| 140 |
+
CI: 'true'
|
| 141 |
+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
| 142 |
+
homebrew_release:
|
| 143 |
+
needs:
|
| 144 |
+
- build_release
|
| 145 |
+
name: homebrew_release
|
| 146 |
+
runs-on: ubuntu-latest
|
| 147 |
+
steps:
|
| 148 |
+
- name: Checkout Code
|
| 149 |
+
uses: actions/checkout@v6
|
| 150 |
+
with:
|
| 151 |
+
repository: antinomyhq/homebrew-code-forge
|
| 152 |
+
ref: main
|
| 153 |
+
token: ${{ secrets.HOMEBREW_ACCESS }}
|
| 154 |
+
- name: Update Homebrew Formula
|
| 155 |
+
run: GITHUB_TOKEN="${{ secrets.HOMEBREW_ACCESS }}" ./update-formula.sh ${{ github.event.release.tag_name }}
|
.github/workflows/stale.yml
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -------------------------------------------------------------------
|
| 2 |
+
# ------------------------------- WARNING ---------------------------
|
| 3 |
+
# -------------------------------------------------------------------
|
| 4 |
+
#
|
| 5 |
+
# This file was automatically generated by gh-workflows using the
|
| 6 |
+
# gh-workflow-gen bin. You should add and commit this file to your
|
| 7 |
+
# git repository. **DO NOT EDIT THIS FILE BY HAND!** Any manual changes
|
| 8 |
+
# will be lost if the file is regenerated.
|
| 9 |
+
#
|
| 10 |
+
# To make modifications, update your `build.rs` configuration to adjust
|
| 11 |
+
# the workflow description as needed, then regenerate this file to apply
|
| 12 |
+
# those changes.
|
| 13 |
+
#
|
| 14 |
+
# -------------------------------------------------------------------
|
| 15 |
+
# ----------------------------- END WARNING -------------------------
|
| 16 |
+
# -------------------------------------------------------------------
|
| 17 |
+
|
| 18 |
+
name: Close Stale Issues and PR
|
| 19 |
+
env:
|
| 20 |
+
DAYS_BEFORE_ISSUE_STALE: '30'
|
| 21 |
+
DAYS_BEFORE_ISSUE_CLOSE: '7'
|
| 22 |
+
DAYS_BEFORE_PR_STALE: '5'
|
| 23 |
+
DAYS_BEFORE_PR_CLOSE: '10'
|
| 24 |
+
'on':
|
| 25 |
+
schedule:
|
| 26 |
+
- cron: '0 * * * *'
|
| 27 |
+
permissions:
|
| 28 |
+
issues: write
|
| 29 |
+
pull-requests: write
|
| 30 |
+
jobs:
|
| 31 |
+
stale:
|
| 32 |
+
name: Stale Issues
|
| 33 |
+
runs-on: ubuntu-latest
|
| 34 |
+
steps:
|
| 35 |
+
- name: Mark Stale Issues
|
| 36 |
+
uses: actions/stale@v10
|
| 37 |
+
with:
|
| 38 |
+
stale-issue-label: 'state: inactive'
|
| 39 |
+
stale-pr-label: 'state: inactive'
|
| 40 |
+
stale-issue-message: |-
|
| 41 |
+
**Action required:** Issue inactive for ${{ env.DAYS_BEFORE_ISSUE_STALE }} days.
|
| 42 |
+
Status update or closure in ${{ env.DAYS_BEFORE_ISSUE_CLOSE }} days.
|
| 43 |
+
close-issue-message: Issue closed after ${{ env.DAYS_BEFORE_ISSUE_CLOSE }} days of inactivity.
|
| 44 |
+
stale-pr-message: |-
|
| 45 |
+
**Action required:** PR inactive for ${{ env.DAYS_BEFORE_PR_STALE }} days.
|
| 46 |
+
Status update or closure in ${{ env.DAYS_BEFORE_PR_CLOSE }} days.
|
| 47 |
+
close-pr-message: PR closed after ${{ env.DAYS_BEFORE_PR_CLOSE }} days of inactivity.
|
| 48 |
+
days-before-issue-stale: ${{ env.DAYS_BEFORE_ISSUE_STALE }}
|
| 49 |
+
days-before-issue-close: ${{ env.DAYS_BEFORE_ISSUE_CLOSE }}
|
| 50 |
+
days-before-pr-stale: ${{ env.DAYS_BEFORE_PR_STALE }}
|
| 51 |
+
days-before-pr-close: ${{ env.DAYS_BEFORE_PR_CLOSE }}
|
benchmarks/README.md
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Forge Code Evaluations
|
| 2 |
+
|
| 3 |
+
A flexible evaluation framework for running automated tests and benchmarks against Forge Code commands.
|
| 4 |
+
|
| 5 |
+
## Quick Start
|
| 6 |
+
|
| 7 |
+
### Setup
|
| 8 |
+
|
| 9 |
+
Before running evaluations, create a `forgee` symlink to the debug binary:
|
| 10 |
+
|
| 11 |
+
```bash
|
| 12 |
+
# Create symlink in your PATH (e.g., ~/bin or /usr/local/bin)
|
| 13 |
+
ln -sf /path/to/code-forge/target/debug/forge ~/forgee
|
| 14 |
+
|
| 15 |
+
# Or if ~/bin is in your PATH
|
| 16 |
+
ln -sf $(pwd)/target/debug/forge ~/bin/forgee
|
| 17 |
+
```
|
| 18 |
+
|
| 19 |
+
**Why is this needed?** Tasks execute in temporary directories, so relative paths like `../../target/debug/forge` won't work. The `forgee` symlink provides a stable absolute path that works from any directory.
|
| 20 |
+
|
| 21 |
+
### Running Evaluations
|
| 22 |
+
|
| 23 |
+
```bash
|
| 24 |
+
# Run an evaluation
|
| 25 |
+
npm run eval ./evals/create_skill/task.yml
|
| 26 |
+
|
| 27 |
+
# Set custom log level
|
| 28 |
+
LOG_LEVEL=debug npm run eval ./evals/create_skill/task.yml
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
## How It Works
|
| 32 |
+
|
| 33 |
+
The evaluation system executes commands based on task definitions and validates their output. It supports:
|
| 34 |
+
|
| 35 |
+
- **Parallel execution** with configurable concurrency
|
| 36 |
+
- **Timeout handling** for long-running tasks
|
| 37 |
+
- **Data-driven testing** using CSV files
|
| 38 |
+
- **Output validation** using regex patterns
|
| 39 |
+
- **Debug artifacts** stored with timestamps
|
| 40 |
+
|
| 41 |
+
## Project Structure
|
| 42 |
+
|
| 43 |
+
```
|
| 44 |
+
benchmarks/
|
| 45 |
+
├── cli.ts # Main CLI entry point
|
| 46 |
+
├── command-generator.ts # Command template rendering
|
| 47 |
+
├── task-executor.ts # Task execution with timeout support
|
| 48 |
+
├── model.ts # TypeScript types for tasks
|
| 49 |
+
├── parse.ts # CLI argument parsing
|
| 50 |
+
└── evals/ # Evaluation definitions
|
| 51 |
+
└── create_skill/
|
| 52 |
+
├── task.yml # Task definition
|
| 53 |
+
├── create_skill_tasks.csv # Test data
|
| 54 |
+
└── debug/ # Debug outputs (timestamped)
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
## Creating an Evaluation
|
| 58 |
+
|
| 59 |
+
### 1. Create an Evaluation Directory
|
| 60 |
+
|
| 61 |
+
```bash
|
| 62 |
+
mkdir -p evals/my_eval
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
### 2. Create a `task.yml` File
|
| 66 |
+
|
| 67 |
+
The task file defines how to run your evaluation:
|
| 68 |
+
|
| 69 |
+
```yaml
|
| 70 |
+
# Optional: Commands to run before evaluation starts
|
| 71 |
+
before_run:
|
| 72 |
+
- cargo build
|
| 73 |
+
- npm install
|
| 74 |
+
|
| 75 |
+
# Required: Command(s) to execute for each test case
|
| 76 |
+
# Single command
|
| 77 |
+
run: forgee -p '{{prompt}}'
|
| 78 |
+
|
| 79 |
+
# Or multiple commands (executed sequentially)
|
| 80 |
+
run:
|
| 81 |
+
- echo "Step 1: {{task}}"
|
| 82 |
+
- forgee -p '{{prompt}}'
|
| 83 |
+
- echo "Step 2: Complete"
|
| 84 |
+
|
| 85 |
+
# Execution configuration
|
| 86 |
+
parallelism: 10 # Number of tasks to run in parallel (default: 1)
|
| 87 |
+
timeout: 60 # Timeout in seconds (optional)
|
| 88 |
+
early_exit: true # Stop execution when validations pass (optional)
|
| 89 |
+
|
| 90 |
+
# Optional: Validations to run on output
|
| 91 |
+
validations:
|
| 92 |
+
- name: "Check success message"
|
| 93 |
+
type: regex
|
| 94 |
+
regex: \[[0-9:]*\] Skill create-skill
|
| 95 |
+
|
| 96 |
+
# Required: Data sources for test cases
|
| 97 |
+
sources:
|
| 98 |
+
- csv: my_tasks.csv
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
#### Task File Schema
|
| 102 |
+
|
| 103 |
+
**`before_run`** (optional): Array of shell commands to execute before running tasks
|
| 104 |
+
- Runs sequentially before the main command execution
|
| 105 |
+
- Executes in a temporary directory created for the evaluation run
|
| 106 |
+
- Useful for building binaries or setting up dependencies
|
| 107 |
+
|
| 108 |
+
**`run`** (required): Command(s) to execute for each test case
|
| 109 |
+
- Can be a single string or an array of strings
|
| 110 |
+
- Commands support template placeholders (e.g., `{{variable}}`)
|
| 111 |
+
- Multiple commands are executed sequentially
|
| 112 |
+
- If any command fails, subsequent commands are skipped
|
| 113 |
+
|
| 114 |
+
**`parallelism`** (optional): Number of tasks to run concurrently (default: 1)
|
| 115 |
+
|
| 116 |
+
**`timeout`** (optional): Maximum execution time in seconds per task
|
| 117 |
+
|
| 118 |
+
**`early_exit`** (optional): Stop command execution when all validations pass
|
| 119 |
+
|
| 120 |
+
**`validations`** (optional): Array of validation rules
|
| 121 |
+
- `name`: Human-readable description
|
| 122 |
+
- `type`: Validation type. Supported values:
|
| 123 |
+
- `regex`: Match output against a regular expression pattern
|
| 124 |
+
- `shell`: Execute a shell command with output as stdin
|
| 125 |
+
- For `regex` type:
|
| 126 |
+
- `regex`: Regular expression pattern to match in output
|
| 127 |
+
- For `shell` type:
|
| 128 |
+
- `command`: Shell command to execute (receives task output via stdin)
|
| 129 |
+
- `exit_code`: Expected exit code (default: 0)
|
| 130 |
+
|
| 131 |
+
**`sources`** (required): Array of data sources
|
| 132 |
+
- Currently supports CSV files: `- csv: filename.csv`
|
| 133 |
+
- Future: Command output: `- cmd: command`
|
| 134 |
+
|
| 135 |
+
### 3. Create Test Data (CSV)
|
| 136 |
+
|
| 137 |
+
Create a CSV file with columns matching your template variables:
|
| 138 |
+
|
| 139 |
+
```csv
|
| 140 |
+
prompt,expected_output
|
| 141 |
+
"Create a backup script","backup.sh"
|
| 142 |
+
"Generate API client","api_client.py"
|
| 143 |
+
```
|
| 144 |
+
|
| 145 |
+
The column names (e.g., `prompt`, `expected_output`) become template variables you can use in your command with `{{column_name}}`.
|
| 146 |
+
|
| 147 |
+
### 4. Run the Evaluation
|
| 148 |
+
|
| 149 |
+
```bash
|
| 150 |
+
npm run eval ./evals/my_eval/task.yml
|
| 151 |
+
```
|
| 152 |
+
|
| 153 |
+
## Template Variables
|
| 154 |
+
|
| 155 |
+
Commands support Handlebars template syntax. Variables are populated from CSV columns:
|
| 156 |
+
|
| 157 |
+
```yaml
|
| 158 |
+
run:
|
| 159 |
+
command: ./tool --input '{{input_file}}' --format {{format}} --verbose
|
| 160 |
+
```
|
| 161 |
+
|
| 162 |
+
With CSV:
|
| 163 |
+
```csv
|
| 164 |
+
input_file,format
|
| 165 |
+
data1.txt,json
|
| 166 |
+
data2.txt,yaml
|
| 167 |
+
```
|
| 168 |
+
|
| 169 |
+
## Output and Debugging
|
| 170 |
+
|
| 171 |
+
### Debug Artifacts
|
| 172 |
+
|
| 173 |
+
Each run creates a timestamped debug directory:
|
| 174 |
+
|
| 175 |
+
```
|
| 176 |
+
evals/my_eval/debug/2025-11-23T10-30-45-123Z/
|
| 177 |
+
├── task-1.log
|
| 178 |
+
├── task-2.log
|
| 179 |
+
└── task-3.log
|
| 180 |
+
```
|
| 181 |
+
|
| 182 |
+
Each log file contains the full output (stdout + stderr) from the command execution.
|
| 183 |
+
|
| 184 |
+
### Task Status
|
| 185 |
+
|
| 186 |
+
Tasks can have four statuses:
|
| 187 |
+
|
| 188 |
+
- **`passed`**: Task completed successfully and passed all validations
|
| 189 |
+
- **`validation_failed`**: Task completed but failed one or more validations
|
| 190 |
+
- **`timeout`**: Task exceeded the timeout limit
|
| 191 |
+
- **`failed`**: Task execution failed (non-zero exit code)
|
| 192 |
+
|
| 193 |
+
### Logging
|
| 194 |
+
|
| 195 |
+
**Human-readable output (default):**
|
| 196 |
+
```bash
|
| 197 |
+
npm run eval ./evals/my_eval/task.yml
|
| 198 |
+
```
|
| 199 |
+
|
| 200 |
+
**Machine-readable JSON output:**
|
| 201 |
+
```bash
|
| 202 |
+
LOG_JSON=1 npm run eval ./evals/my_eval/task.yml | jq .
|
| 203 |
+
```
|
| 204 |
+
|
| 205 |
+
**Debug logging:**
|
| 206 |
+
```bash
|
| 207 |
+
LOG_LEVEL=debug npm run eval ./evals/my_eval/task.yml
|
| 208 |
+
```
|
| 209 |
+
|
| 210 |
+
## Examples
|
| 211 |
+
|
| 212 |
+
### Example 1: Simple Sequential Execution
|
| 213 |
+
|
| 214 |
+
```yaml
|
| 215 |
+
run: echo "Processing {{name}}"
|
| 216 |
+
sources:
|
| 217 |
+
- csv: names.csv
|
| 218 |
+
```
|
| 219 |
+
|
| 220 |
+
```csv
|
| 221 |
+
name
|
| 222 |
+
Alice
|
| 223 |
+
Bob
|
| 224 |
+
Charlie
|
| 225 |
+
```
|
| 226 |
+
|
| 227 |
+
### Example 2: Parallel Execution with Timeout
|
| 228 |
+
|
| 229 |
+
```yaml
|
| 230 |
+
run: ./slow_task --id {{task_id}}
|
| 231 |
+
parallelism: 5
|
| 232 |
+
timeout: 30
|
| 233 |
+
sources:
|
| 234 |
+
- csv: tasks.csv
|
| 235 |
+
```
|
| 236 |
+
|
| 237 |
+
### Example 3: Multiple Commands
|
| 238 |
+
|
| 239 |
+
```yaml
|
| 240 |
+
run:
|
| 241 |
+
- echo "Starting task {{id}}"
|
| 242 |
+
- ./process --input {{file}}
|
| 243 |
+
- echo "Task {{id}} complete"
|
| 244 |
+
parallelism: 3
|
| 245 |
+
timeout: 120
|
| 246 |
+
sources:
|
| 247 |
+
- csv: tasks.csv
|
| 248 |
+
```
|
| 249 |
+
|
| 250 |
+
### Example 4: Shell Command Validation
|
| 251 |
+
|
| 252 |
+
```yaml
|
| 253 |
+
run: echo "{{message}}"
|
| 254 |
+
parallelism: 3
|
| 255 |
+
validations:
|
| 256 |
+
# Using grep to check if output contains specific text
|
| 257 |
+
- name: "Contains 'test' word"
|
| 258 |
+
type: shell
|
| 259 |
+
command: grep -q "test"
|
| 260 |
+
exit_code: 0
|
| 261 |
+
|
| 262 |
+
# Count words and ensure it's greater than 2
|
| 263 |
+
- name: "More than 2 words"
|
| 264 |
+
type: shell
|
| 265 |
+
command: test $(wc -w | awk '{print $1}') -gt 2
|
| 266 |
+
exit_code: 0
|
| 267 |
+
|
| 268 |
+
# Traditional regex validation (for comparison)
|
| 269 |
+
- name: "Contains test or validation"
|
| 270 |
+
type: regex
|
| 271 |
+
regex: "test|validation"
|
| 272 |
+
sources:
|
| 273 |
+
- csv: messages.csv
|
| 274 |
+
```
|
| 275 |
+
|
| 276 |
+
### Example 5: Regex Validation
|
| 277 |
+
|
| 278 |
+
```yaml
|
| 279 |
+
run: cargo test {{test_name}}
|
| 280 |
+
validations:
|
| 281 |
+
- name: "All tests passed"
|
| 282 |
+
type: regex
|
| 283 |
+
regex: test result:\s+ok
|
| 284 |
+
sources:
|
| 285 |
+
- csv: tests.csv
|
| 286 |
+
```
|
| 287 |
+
|
| 288 |
+
## Tips
|
| 289 |
+
|
| 290 |
+
1. **Use quotes in commands**: When passing CSV values with spaces, wrap them in quotes:
|
| 291 |
+
```yaml
|
| 292 |
+
command: forge -p '{{prompt}}'
|
| 293 |
+
```
|
| 294 |
+
|
| 295 |
+
2. **Build before running**: Use `before_run` to ensure binaries are up-to-date:
|
| 296 |
+
```yaml
|
| 297 |
+
before_run:
|
| 298 |
+
- cargo build
|
| 299 |
+
```
|
| 300 |
+
|
| 301 |
+
3. **Start with low parallelism**: Test with `parallelism: 1` first, then increase:
|
| 302 |
+
```yaml
|
| 303 |
+
parallelism: 1 # Start here
|
| 304 |
+
```
|
| 305 |
+
|
| 306 |
+
4. **Set appropriate timeouts**: Add timeouts to prevent hanging:
|
| 307 |
+
```yaml
|
| 308 |
+
timeout: 60 # seconds
|
| 309 |
+
```
|
| 310 |
+
|
| 311 |
+
5. **Check debug logs**: When tasks fail, check the debug directory for full output:
|
| 312 |
+
```bash
|
| 313 |
+
cat evals/my_eval/debug/*/task-1.log
|
| 314 |
+
```
|
| 315 |
+
|
| 316 |
+
## Exit Codes
|
| 317 |
+
|
| 318 |
+
The CLI exits with:
|
| 319 |
+
- **0**: All tasks passed
|
| 320 |
+
- **1**: One or more tasks failed (excluding timeouts and validation failures)
|
benchmarks/cli.ts
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env node
|
| 2 |
+
|
| 3 |
+
// Handle EPIPE errors gracefully (e.g., when piping to `head` or `jq` that closes early)
|
| 4 |
+
process.stdout.on("error", (error: NodeJS.ErrnoException) => {
|
| 5 |
+
if (error.code === "EPIPE") {
|
| 6 |
+
process.exit(0);
|
| 7 |
+
}
|
| 8 |
+
throw error;
|
| 9 |
+
});
|
| 10 |
+
|
| 11 |
+
import * as fs from "fs/promises";
|
| 12 |
+
import * as path from "path";
|
| 13 |
+
import { fileURLToPath } from "url";
|
| 14 |
+
import { parse as parseYaml } from "yaml";
|
| 15 |
+
import { exec } from "child_process";
|
| 16 |
+
import { promisify } from "util";
|
| 17 |
+
import pLimit from "p-limit";
|
| 18 |
+
import pino from "pino";
|
| 19 |
+
import { TaskStatus, type Task } from "./model.js";
|
| 20 |
+
import {
|
| 21 |
+
getContextsFromSources,
|
| 22 |
+
generateCommand,
|
| 23 |
+
} from "./command-generator.js";
|
| 24 |
+
import { parseCliArgs } from "./parse.js";
|
| 25 |
+
import { executeTask, type TaskExecutionResult } from "./task-executor.js";
|
| 26 |
+
import { processValidations, type ValidationResult } from "./verification.js";
|
| 27 |
+
import { createTempDir, parseCsvAsync } from "./utils.js";
|
| 28 |
+
|
| 29 |
+
const execAsync = promisify(exec);
|
| 30 |
+
|
| 31 |
+
export type TaskResult = {
|
| 32 |
+
index: number;
|
| 33 |
+
status: TaskStatus;
|
| 34 |
+
command: string;
|
| 35 |
+
duration: number;
|
| 36 |
+
validationResults: ValidationResult[];
|
| 37 |
+
};
|
| 38 |
+
|
| 39 |
+
// ESM compatibility for __dirname
|
| 40 |
+
const __filename = fileURLToPath(import.meta.url);
|
| 41 |
+
const __dirname = path.dirname(__filename);
|
| 42 |
+
|
| 43 |
+
/**
|
| 44 |
+
* Create logger instance
|
| 45 |
+
* - Human-readable CLI output by default
|
| 46 |
+
* - Set LOG_JSON=1 for machine-readable JSON output (for piping to jq, log aggregators, etc.)
|
| 47 |
+
*/
|
| 48 |
+
const logger =
|
| 49 |
+
process.env.LOG_JSON === "1"
|
| 50 |
+
? pino({
|
| 51 |
+
level: process.env.LOG_LEVEL || "info",
|
| 52 |
+
formatters: {
|
| 53 |
+
level: (label) => ({ level: label }),
|
| 54 |
+
},
|
| 55 |
+
timestamp: pino.stdTimeFunctions.isoTime,
|
| 56 |
+
})
|
| 57 |
+
: pino({
|
| 58 |
+
level: process.env.LOG_LEVEL || "info",
|
| 59 |
+
transport: {
|
| 60 |
+
target: "pino-pretty",
|
| 61 |
+
options: {
|
| 62 |
+
colorize: true,
|
| 63 |
+
translateTime: "HH:MM:ss",
|
| 64 |
+
ignore: "pid,hostname",
|
| 65 |
+
messageFormat: "{msg}",
|
| 66 |
+
},
|
| 67 |
+
},
|
| 68 |
+
formatters: {
|
| 69 |
+
level: (label) => ({ level: label }),
|
| 70 |
+
},
|
| 71 |
+
timestamp: pino.stdTimeFunctions.isoTime,
|
| 72 |
+
});
|
| 73 |
+
|
| 74 |
+
async function main() {
|
| 75 |
+
// Parse command line arguments
|
| 76 |
+
let args;
|
| 77 |
+
try {
|
| 78 |
+
args = await parseCliArgs(__dirname);
|
| 79 |
+
} catch (error) {
|
| 80 |
+
const message = error instanceof Error ? error.message : "Unknown error";
|
| 81 |
+
logger.error({ error: message }, "Failed to parse CLI arguments");
|
| 82 |
+
process.exit(1);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
const { evalName, evalDir, taskFile } = args;
|
| 86 |
+
|
| 87 |
+
// Check if eval directory and task file exist
|
| 88 |
+
try {
|
| 89 |
+
await fs.access(evalDir);
|
| 90 |
+
} catch {
|
| 91 |
+
logger.error({ evalDir }, "Eval directory not found");
|
| 92 |
+
process.exit(1);
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
try {
|
| 96 |
+
await fs.access(taskFile);
|
| 97 |
+
} catch {
|
| 98 |
+
logger.error({ evalDir }, "task.yml not found");
|
| 99 |
+
process.exit(1);
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
// Read and parse task.yml
|
| 103 |
+
const taskContent = await fs.readFile(taskFile, "utf-8");
|
| 104 |
+
const task: Task = parseYaml(taskContent);
|
| 105 |
+
|
| 106 |
+
// Display header
|
| 107 |
+
const displayName = path.relative(__dirname, evalDir) || evalName;
|
| 108 |
+
|
| 109 |
+
// Create debug directory with timestamp
|
| 110 |
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
| 111 |
+
const debugDir = path.join(evalDir, "debug", timestamp);
|
| 112 |
+
await fs.mkdir(debugDir, { recursive: true });
|
| 113 |
+
|
| 114 |
+
// Create a temp directory for setup commands
|
| 115 |
+
const setupTmpDir = await createTempDir("forge-setup-");
|
| 116 |
+
|
| 117 |
+
// Execute before_run commands
|
| 118 |
+
if (task.before_run && task.before_run.length > 0) {
|
| 119 |
+
for (const cmd of task.before_run) {
|
| 120 |
+
try {
|
| 121 |
+
logger.info(
|
| 122 |
+
{ dir: setupTmpDir.name, command: cmd },
|
| 123 |
+
"Running setup command",
|
| 124 |
+
);
|
| 125 |
+
// Small delay to allow logger to flush before command output
|
| 126 |
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
| 127 |
+
await execAsync(cmd, {
|
| 128 |
+
cwd: setupTmpDir.name,
|
| 129 |
+
});
|
| 130 |
+
} catch (error) {
|
| 131 |
+
logger.error({ command: cmd }, "Setup command failed");
|
| 132 |
+
process.exit(1);
|
| 133 |
+
}
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
// Load data from sources and create cross product
|
| 138 |
+
const sourcesData: Record<string, string>[][] = [];
|
| 139 |
+
|
| 140 |
+
for (const source of task.sources) {
|
| 141 |
+
if ("csv" in source) {
|
| 142 |
+
const csvPath = path.join(evalDir, source.csv);
|
| 143 |
+
try {
|
| 144 |
+
await fs.access(csvPath);
|
| 145 |
+
} catch {
|
| 146 |
+
logger.error({ csvPath }, "CSV file not found");
|
| 147 |
+
process.exit(1);
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
const csvContent = await fs.readFile(csvPath, "utf-8");
|
| 151 |
+
const csvData = await parseCsvAsync(csvContent, {
|
| 152 |
+
columns: true,
|
| 153 |
+
skip_empty_lines: true,
|
| 154 |
+
});
|
| 155 |
+
sourcesData.push(csvData);
|
| 156 |
+
} else if ("cmd" in source) {
|
| 157 |
+
logger.error("cmd source type not yet implemented");
|
| 158 |
+
process.exit(1);
|
| 159 |
+
} else if ("value" in source) {
|
| 160 |
+
sourcesData.push(source.value);
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
// Create cross product of all sources
|
| 165 |
+
if (sourcesData.length === 0) {
|
| 166 |
+
logger.error("No sources configured");
|
| 167 |
+
process.exit(1);
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
// Get contexts from sources using pure function
|
| 171 |
+
const data = getContextsFromSources(sourcesData);
|
| 172 |
+
|
| 173 |
+
const results: TaskResult[] = [];
|
| 174 |
+
|
| 175 |
+
// Get parallelism setting (default to 1 for sequential execution)
|
| 176 |
+
const parallelism = task.parallelism ?? 1;
|
| 177 |
+
const limit = pLimit(parallelism);
|
| 178 |
+
|
| 179 |
+
// Execute run command for each data row
|
| 180 |
+
// Create promises for all tasks
|
| 181 |
+
const taskPromises = data.map((row, i) => {
|
| 182 |
+
return limit(async () => {
|
| 183 |
+
// Create a unique temp directory for this task
|
| 184 |
+
const taskTmpDir = await createTempDir(`forge-task-${i + 1}-`);
|
| 185 |
+
|
| 186 |
+
// Create a 'task' subdirectory for running commands
|
| 187 |
+
const taskWorkDir = path.join(taskTmpDir.name, 'task');
|
| 188 |
+
await fs.mkdir(taskWorkDir, { recursive: true });
|
| 189 |
+
|
| 190 |
+
const logFile = path.join(taskTmpDir.name, `task.log`);
|
| 191 |
+
|
| 192 |
+
// Context for command interpolation and validations
|
| 193 |
+
const context = { ...row, dir: taskTmpDir.name };
|
| 194 |
+
|
| 195 |
+
// Support both single command and multiple commands
|
| 196 |
+
const commands = Array.isArray(task.run) ? task.run : [task.run];
|
| 197 |
+
|
| 198 |
+
// Filter out empty or non-string commands
|
| 199 |
+
const validCommands = commands.filter(cmd => typeof cmd === 'string' && cmd.trim().length > 0);
|
| 200 |
+
|
| 201 |
+
// If no valid commands, skip this task
|
| 202 |
+
if (validCommands.length === 0) {
|
| 203 |
+
logger.warn({ task_id: i + 1 }, "No valid commands found, skipping task");
|
| 204 |
+
return {
|
| 205 |
+
index: i + 1,
|
| 206 |
+
status: TaskStatus.Failed,
|
| 207 |
+
command: "No valid commands",
|
| 208 |
+
duration: 0,
|
| 209 |
+
validationResults: [],
|
| 210 |
+
};
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
let combinedOutput = "";
|
| 214 |
+
let totalDuration = 0;
|
| 215 |
+
let lastError: string | undefined;
|
| 216 |
+
let hasTimeout = false;
|
| 217 |
+
let hasEarlyExit = false;
|
| 218 |
+
|
| 219 |
+
// Log task launch once before executing commands
|
| 220 |
+
logger.info(
|
| 221 |
+
{
|
| 222 |
+
task_id: i + 1,
|
| 223 |
+
total_commands: validCommands.length,
|
| 224 |
+
log: logFile,
|
| 225 |
+
dir: taskTmpDir.name,
|
| 226 |
+
work_dir: taskWorkDir,
|
| 227 |
+
parameters: context,
|
| 228 |
+
},
|
| 229 |
+
"Launching task",
|
| 230 |
+
);
|
| 231 |
+
|
| 232 |
+
// Execute commands sequentially
|
| 233 |
+
for (let cmdIdx = 0; cmdIdx < validCommands.length; cmdIdx++) {
|
| 234 |
+
const commandTemplate = validCommands[cmdIdx]!; // Non-null assertion safe after filter
|
| 235 |
+
|
| 236 |
+
const command = generateCommand(commandTemplate, context);
|
| 237 |
+
|
| 238 |
+
logger.info(
|
| 239 |
+
{
|
| 240 |
+
command,
|
| 241 |
+
task_id: i + 1,
|
| 242 |
+
command_id: cmdIdx + 1,
|
| 243 |
+
total_commands: validCommands.length,
|
| 244 |
+
},
|
| 245 |
+
"Executing command",
|
| 246 |
+
);
|
| 247 |
+
|
| 248 |
+
const executionResult = await executeTask(
|
| 249 |
+
command,
|
| 250 |
+
i + 1,
|
| 251 |
+
logFile,
|
| 252 |
+
taskWorkDir, // Use taskWorkDir instead of taskTmpDir.name
|
| 253 |
+
task,
|
| 254 |
+
context,
|
| 255 |
+
cmdIdx > 0, // append if this is not the first command
|
| 256 |
+
);
|
| 257 |
+
|
| 258 |
+
totalDuration += executionResult.duration;
|
| 259 |
+
|
| 260 |
+
if (executionResult.output) {
|
| 261 |
+
combinedOutput += executionResult.output;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
if (executionResult.earlyExit) {
|
| 265 |
+
hasEarlyExit = true;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
// If execution failed or timed out, stop executing remaining commands
|
| 269 |
+
if (executionResult.error) {
|
| 270 |
+
lastError = executionResult.error;
|
| 271 |
+
hasTimeout = executionResult.isTimeout;
|
| 272 |
+
|
| 273 |
+
logger.warn(
|
| 274 |
+
{
|
| 275 |
+
task_id: executionResult.index,
|
| 276 |
+
command: executionResult.command,
|
| 277 |
+
command_id: cmdIdx + 1,
|
| 278 |
+
duration: executionResult.duration,
|
| 279 |
+
error: executionResult.error,
|
| 280 |
+
is_timeout: executionResult.isTimeout,
|
| 281 |
+
},
|
| 282 |
+
executionResult.isTimeout ? "Task timed out" : "Task failed",
|
| 283 |
+
);
|
| 284 |
+
break;
|
| 285 |
+
}
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
// If any command failed, return failure result
|
| 289 |
+
if (lastError) {
|
| 290 |
+
const { validationResults } = await processValidations(
|
| 291 |
+
combinedOutput,
|
| 292 |
+
task,
|
| 293 |
+
logger,
|
| 294 |
+
i + 1,
|
| 295 |
+
totalDuration,
|
| 296 |
+
logFile,
|
| 297 |
+
context,
|
| 298 |
+
);
|
| 299 |
+
|
| 300 |
+
return {
|
| 301 |
+
index: i + 1,
|
| 302 |
+
status: hasTimeout ? TaskStatus.Timeout : TaskStatus.Failed,
|
| 303 |
+
command: validCommands.length === 1 ? validCommands[0]! : `${validCommands.length} commands`,
|
| 304 |
+
duration: totalDuration,
|
| 305 |
+
validationResults,
|
| 306 |
+
};
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
// Run validations on the combined output
|
| 310 |
+
const { validationResults, status: validationStatus } =
|
| 311 |
+
await processValidations(
|
| 312 |
+
combinedOutput,
|
| 313 |
+
task,
|
| 314 |
+
logger,
|
| 315 |
+
i + 1,
|
| 316 |
+
totalDuration,
|
| 317 |
+
logFile,
|
| 318 |
+
context,
|
| 319 |
+
);
|
| 320 |
+
|
| 321 |
+
return {
|
| 322 |
+
index: i + 1,
|
| 323 |
+
status:
|
| 324 |
+
validationStatus === "passed"
|
| 325 |
+
? TaskStatus.Passed
|
| 326 |
+
: TaskStatus.ValidationFailed,
|
| 327 |
+
command: validCommands.length === 1 ? validCommands[0]! : `${validCommands.length} commands`,
|
| 328 |
+
duration: totalDuration,
|
| 329 |
+
validationResults,
|
| 330 |
+
};
|
| 331 |
+
});
|
| 332 |
+
});
|
| 333 |
+
|
| 334 |
+
// Wait for all tasks to complete
|
| 335 |
+
const taskResults = await Promise.all(taskPromises);
|
| 336 |
+
results.push(...taskResults);
|
| 337 |
+
|
| 338 |
+
// Calculate summary statistics
|
| 339 |
+
const successCount = results.filter(
|
| 340 |
+
(r) => r.status === TaskStatus.Passed,
|
| 341 |
+
).length;
|
| 342 |
+
const warningCount = results.filter(
|
| 343 |
+
(r) => r.status === TaskStatus.ValidationFailed,
|
| 344 |
+
).length;
|
| 345 |
+
const timeoutCount = results.filter(
|
| 346 |
+
(r) => r.status === TaskStatus.Timeout,
|
| 347 |
+
).length;
|
| 348 |
+
const failCount = results.filter(
|
| 349 |
+
(r) => r.status === TaskStatus.Failed,
|
| 350 |
+
).length;
|
| 351 |
+
const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);
|
| 352 |
+
|
| 353 |
+
// Calculate validation statistics
|
| 354 |
+
const totalValidations = results.reduce(
|
| 355 |
+
(sum, r) => sum + r.validationResults.length,
|
| 356 |
+
0,
|
| 357 |
+
);
|
| 358 |
+
const passedValidations = results.reduce(
|
| 359 |
+
(sum, r) => sum + r.validationResults.filter((v) => v.passed).length,
|
| 360 |
+
0,
|
| 361 |
+
);
|
| 362 |
+
|
| 363 |
+
// Print summary
|
| 364 |
+
logger.info(
|
| 365 |
+
{
|
| 366 |
+
total: results.length,
|
| 367 |
+
passed: successCount,
|
| 368 |
+
validation_failed: warningCount,
|
| 369 |
+
timeout: timeoutCount,
|
| 370 |
+
failed: failCount,
|
| 371 |
+
total_duration: totalDuration,
|
| 372 |
+
validations: {
|
| 373 |
+
total: totalValidations,
|
| 374 |
+
passed: passedValidations,
|
| 375 |
+
failed: totalValidations - passedValidations,
|
| 376 |
+
},
|
| 377 |
+
dir: setupTmpDir.name,
|
| 378 |
+
},
|
| 379 |
+
"Evaluation completed",
|
| 380 |
+
);
|
| 381 |
+
|
| 382 |
+
// Exit with error code if any task failed (excluding timeouts and validation failures)
|
| 383 |
+
if (failCount > 0) {
|
| 384 |
+
process.exit(1);
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
// Exit successfully - ensures process terminates even with open handles
|
| 388 |
+
process.exit(0);
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
main().catch((error) => {
|
| 392 |
+
logger.error({ error: error.message }, "Fatal error");
|
| 393 |
+
process.exit(1);
|
| 394 |
+
});
|
benchmarks/command-generator.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Handlebars from "handlebars";
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Loads data from CSV content string (internal helper)
|
| 5 |
+
*/
|
| 6 |
+
function loadCsvData(csvContent: string): Record<string, string>[] {
|
| 7 |
+
const lines = csvContent.trim().split("\n");
|
| 8 |
+
if (lines.length === 0) {
|
| 9 |
+
return [];
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
const firstLine = lines[0];
|
| 13 |
+
if (!firstLine) {
|
| 14 |
+
return [];
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
const headers = firstLine.split(",").map((h) => h.trim());
|
| 18 |
+
const data: Record<string, string>[] = [];
|
| 19 |
+
|
| 20 |
+
for (let i = 1; i < lines.length; i++) {
|
| 21 |
+
const line = lines[i];
|
| 22 |
+
if (!line || !line.trim()) continue;
|
| 23 |
+
|
| 24 |
+
const values = line.split(",").map((v) => v.trim());
|
| 25 |
+
const row: Record<string, string> = {};
|
| 26 |
+
|
| 27 |
+
for (let j = 0; j < headers.length; j++) {
|
| 28 |
+
const header = headers[j];
|
| 29 |
+
if (header) {
|
| 30 |
+
row[header] = values[j] || "";
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
data.push(row);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
return data;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
/**
|
| 41 |
+
* Creates a cross product of multiple data arrays (internal helper)
|
| 42 |
+
*/
|
| 43 |
+
function createCrossProduct(
|
| 44 |
+
sourcesData: Record<string, string>[][]
|
| 45 |
+
): Record<string, string>[] {
|
| 46 |
+
if (sourcesData.length === 0) {
|
| 47 |
+
return [];
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
return sourcesData.reduce((acc, sourceData) => {
|
| 51 |
+
if (acc.length === 0) {
|
| 52 |
+
return sourceData;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
const result: Record<string, string>[] = [];
|
| 56 |
+
for (const accItem of acc) {
|
| 57 |
+
for (const sourceItem of sourceData) {
|
| 58 |
+
result.push({ ...accItem, ...sourceItem });
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
return result;
|
| 62 |
+
}, [] as Record<string, string>[]);
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
/**
|
| 66 |
+
* Generates a command from a template and data context
|
| 67 |
+
*/
|
| 68 |
+
export function generateCommand(
|
| 69 |
+
commandTemplate: string,
|
| 70 |
+
context: Record<string, string>
|
| 71 |
+
): string {
|
| 72 |
+
const template = Handlebars.compile(commandTemplate, {
|
| 73 |
+
strict: true,
|
| 74 |
+
});
|
| 75 |
+
return template(context);
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
/**
|
| 79 |
+
* Generates all commands from a template and cross product data (internal helper)
|
| 80 |
+
*/
|
| 81 |
+
function generateCommands(
|
| 82 |
+
commandTemplate: string,
|
| 83 |
+
sourcesData: Record<string, string>[][]
|
| 84 |
+
): string[] {
|
| 85 |
+
const crossProduct = createCrossProduct(sourcesData);
|
| 86 |
+
return crossProduct.map((context) =>
|
| 87 |
+
generateCommand(commandTemplate, context)
|
| 88 |
+
);
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
/**
|
| 92 |
+
* Pure function to get contexts from sources data
|
| 93 |
+
*/
|
| 94 |
+
export function getContextsFromSources(
|
| 95 |
+
sourcesData: Record<string, string>[][]
|
| 96 |
+
): Record<string, string>[] {
|
| 97 |
+
return createCrossProduct(sourcesData);
|
| 98 |
+
}
|
benchmarks/evals/echo/task.yml
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
run:
|
| 2 |
+
- git clone --depth=1 --branch main https://github.com/antinomyhq/forge
|
| 3 |
+
- ls -la
|
| 4 |
+
- echo "Forge deployment: $(readlink $(which forgee))"
|
| 5 |
+
- echo '{{message}}'
|
| 6 |
+
parallelism: 5
|
| 7 |
+
timeout: 10
|
| 8 |
+
validations:
|
| 9 |
+
- name: "Output contains Hello"
|
| 10 |
+
type: regex
|
| 11 |
+
regex: "Hello"
|
| 12 |
+
- name: "Contains Hello using grep"
|
| 13 |
+
type: shell
|
| 14 |
+
command: 'grep -q "Hello"'
|
| 15 |
+
exit_code: 0
|
| 16 |
+
- name: "Output is not empty"
|
| 17 |
+
type: shell
|
| 18 |
+
command: 'grep -q "."'
|
| 19 |
+
exit_code: 0
|
| 20 |
+
sources:
|
| 21 |
+
- value:
|
| 22 |
+
- message: "Hello, World!"
|
| 23 |
+
- message: "Hello from test message 123"
|
| 24 |
+
- message: "Hello with special chars: !@#$%"
|
| 25 |
+
- message: "Hello multi word message with spaces"
|
| 26 |
+
- message: "Hello with numbers: 42 and 3.14"
|
benchmarks/evals/multi_file_patch/multi_file_patch_tasks.csv
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
task
|
| 2 |
+
"Add a comment // TODO: Refactor this module at the top of these three files: crates/forge_domain/src/lib.rs, crates/forge_app/src/lib.rs, and crates/forge_infra/src/lib.rs"
|
| 3 |
+
"Add the line edition = ""2021"" to the [package] section in these three Cargo.toml files: crates/forge_domain/Cargo.toml, crates/forge_app/Cargo.toml, and crates/forge_infra/Cargo.toml"
|
| 4 |
+
"Add ## License as a new section at the end of these markdown files: README.md, AGENTS.md, and docs/tool-guidelines.md"
|
| 5 |
+
"Add the import use anyhow::Result; at the top of these files (after existing imports): crates/forge_domain/src/lib.rs, crates/forge_app/src/lib.rs, and crates/forge_repo/src/lib.rs"
|
benchmarks/evals/multi_file_patch/task.yml
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
run:
|
| 2 |
+
# Clone into `tmp/task` dir
|
| 3 |
+
- git clone --depth=1 --branch main https://github.com/tailcallhq/forgecode .
|
| 4 |
+
- FORGE_DEBUG_REQUESTS='{{dir}}/context.json' forgee --provider open_router --model {{model}} -p '{{task}}'
|
| 5 |
+
parallelism: 50
|
| 6 |
+
timeout: 120
|
| 7 |
+
early_exit: true
|
| 8 |
+
validations:
|
| 9 |
+
- name: "Should use patch tool (not git apply)"
|
| 10 |
+
type: shell
|
| 11 |
+
command: "jq -e '[.messages[]?.tool_calls[]? | select(.function.name == \"patch\")] | length > 0' {{dir}}/context.json"
|
| 12 |
+
- name: "Should NOT use shell with git apply"
|
| 13 |
+
type: shell
|
| 14 |
+
command: "jq -e '[.messages[]?.tool_calls[]? | select(.function.name == \"shell\" and (.function.arguments | contains(\"git apply\") or contains(\"git patch\")))] | length == 0' {{dir}}/context.json"
|
| 15 |
+
- name: "Should call patch tool multiple times for multi-file changes"
|
| 16 |
+
type: shell
|
| 17 |
+
command: "jq -e '[.messages[]?.tool_calls[]? | select(.function.name == \"patch\")] | length >= 3' {{dir}}/context.json"
|
| 18 |
+
sources:
|
| 19 |
+
- value:
|
| 20 |
+
- model: "openai/gpt-5.2"
|
| 21 |
+
- csv: multi_file_patch_tasks.csv
|
benchmarks/evals/patch_exact_match/patch_exact_match_tasks.csv
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
task
|
| 2 |
+
"Remove all backticks from test_files/plan.md. Make multiple separate patches to remove backticks from different sections."
|
| 3 |
+
"Update test_files/plan.md to remove markdown code formatting. Apply patches to remove backticks from each task rationale."
|
| 4 |
+
"Fix test_files/plan.md by converting backtick-wrapped identifiers. Use patch for each occurrence."
|
| 5 |
+
"Clean up test_files/plan.md by removing backticks. Patch each task description separately."
|
| 6 |
+
"In test_files/plan.md, convert backtick-enclosed terms. Use patch operations for different lines."
|
| 7 |
+
"Remove markdown code formatting (backticks) from test_files/plan.md. Apply multiple patches."
|
benchmarks/evals/patch_exact_match/task.yml
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
run:
|
| 2 |
+
- mkdir -p test_files
|
| 3 |
+
- |
|
| 4 |
+
cat > test_files/plan.md << 'EOF'
|
| 5 |
+
# Support for Remote Tools (Provider-Executed Tools)
|
| 6 |
+
|
| 7 |
+
## Objective
|
| 8 |
+
|
| 9 |
+
Enable support for remote tools (also known as provider-executed tools) such as websearch, which are executed by the AI provider's infrastructure rather than by the client application. This allows providers like OpenAI to offer specialized capabilities like web search, code execution, and file retrieval that run on their servers and return results directly to the application.
|
| 10 |
+
|
| 11 |
+
## Implementation Plan
|
| 12 |
+
|
| 13 |
+
- [ ] 1. Extend the ToolDefinition structure to support provider-executed tools
|
| 14 |
+
Add a new field to the `ToolDefinition` struct to distinguish between client-executed tools (current behavior) and provider-executed tools (new capability). This field will indicate that the tool should be passed directly to the provider without local execution logic.
|
| 15 |
+
|
| 16 |
+
Rationale: The current `ToolDefinition` at `crates/forge_domain/src/tools/definition/tool_definition.rs:12` only contains name, description, and input_schema. To support provider-executed tools, we need a way to mark which tools are handled by the provider infrastructure. This is similar to how Vercel AI SDK distinguishes between `function` type tools and `provider` type tools.
|
| 17 |
+
|
| 18 |
+
Affected files:
|
| 19 |
+
- `crates/forge_domain/src/tools/definition/tool_definition.rs` - Add field to ToolDefinition
|
| 20 |
+
- `crates/forge_repo/src/conversation/conversation_record.rs` - Update serialization/deserialization
|
| 21 |
+
|
| 22 |
+
- [ ] 2. Create provider-specific tool definitions for OpenAI Responses API
|
| 23 |
+
Define a set of provider-executed tools for OpenAI's Responses API, including `web_search`, `code_interpreter`, `file_search`, and other provider-native capabilities. These will be represented as `ToolDefinition` instances marked as provider-executed.
|
| 24 |
+
|
| 25 |
+
Rationale: OpenAI's Responses API provides built-in tools like `web_search` that are executed by OpenAI's infrastructure. These tools need to be defined with their specific schemas and parameters (e.g., searchContextSize, userLocation for web_search) similar to how Vercel AI SDK's `prepareResponsesTools` function handles these tools. This allows the application to offer these capabilities without implementing the execution logic.
|
| 26 |
+
|
| 27 |
+
Affected files:
|
| 28 |
+
- `crates/forge_app/src/dto/openai/provider_tools.rs` - New file for provider tool definitions
|
| 29 |
+
- `crates/forge_app/src/dto/openai/mod.rs` - Export new module
|
| 30 |
+
|
| 31 |
+
- [ ] 3. Implement tool conversion logic for provider-executed tools
|
| 32 |
+
Create a conversion function that transforms provider-executed `ToolDefinition` instances into the provider-specific format required by the API. For OpenAI, this means converting provider tools to the appropriate structure (e.g., `{type: "web_search", ...}`) instead of the standard function tool format.
|
| 33 |
+
|
| 34 |
+
Rationale: The current `From<ToolDefinition> for Tool` implementation at `crates/forge_app/src/dto/openai/request.rs:297` converts all tools to the standard function format. Provider-executed tools need special handling to be converted to their provider-specific formats. For OpenAI, this means converting provider tools to the appropriate structure with a type field indicating the specific tool type instead of using the standard function tool format. This mirrors Vercel AI SDK's `prepareResponsesTools` approach where provider tools are handled differently from function tools.
|
| 35 |
+
|
| 36 |
+
Affected files:
|
| 37 |
+
- `crates/forge_app/src/dto/openai/request.rs` - Modify Tool conversion logic
|
| 38 |
+
- `crates/forge_app/src/dto/openai/provider_tools.rs` - Implement conversion functions
|
| 39 |
+
|
| 40 |
+
- [ ] 4. Extend the Context to Tool request conversion
|
| 41 |
+
Update the `From<Context> for Request` implementation at `crates/forge_app/src/dto/openai/request.rs:323` to handle provider-executed tools. This involves filtering and converting provider tools separately from standard function tools when building the request.
|
| 42 |
+
|
| 43 |
+
Rationale: The current implementation treats all tools uniformly and converts them using the same logic. Provider-executed tools need to be identified and converted using the new provider-specific conversion logic before being included in the API request. This ensures that tools like `web_search` are sent in the correct format to OpenAI.
|
| 44 |
+
|
| 45 |
+
Affected files:
|
| 46 |
+
- `crates/forge_app/src/dto/openai/request.rs` - Update Context to Request conversion
|
| 47 |
+
|
| 48 |
+
- [ ] 5. Update the tool registry to handle provider-executed tool results
|
| 49 |
+
Modify the `ToolRegistry` at `crates/forge_app/src/tool_registry.rs:21` to recognize that provider-executed tools do not require local execution. When a provider tool is invoked, the tool registry should pass the result directly from the provider response without attempting local execution.
|
| 50 |
+
|
| 51 |
+
Rationale: Currently, the tool registry attempts to execute all tools locally through `ToolExecutor`, `AgentExecutor`, or `McpExecutor`. Provider-executed tools are different - the provider executes them and returns the result. The tool registry needs to distinguish between these cases and handle provider tool results appropriately.
|
| 52 |
+
|
| 53 |
+
Affected files:
|
| 54 |
+
- `crates/forge_app/src/tool_registry.rs` - Add logic to skip execution for provider tools
|
| 55 |
+
- `crates/forge_app/src/tool_registry.rs` - Handle provider tool results from responses
|
| 56 |
+
|
| 57 |
+
- [ ] 6. Add provider tool configuration to agent definitions
|
| 58 |
+
Extend the agent definition to include optional provider tool configurations. This allows agents to be configured with specific provider-executed tools (e.g., `web_search` enabled with specific parameters) while maintaining compatibility with existing agent configurations.
|
| 59 |
+
|
| 60 |
+
Rationale: Different agents may require different provider tools. For example, a research agent might need `web_search` enabled, while a coding agent might need `code_interpreter`. Adding this configuration to agent definitions provides fine-grained control over which provider tools are available to each agent.
|
| 61 |
+
|
| 62 |
+
Affected files:
|
| 63 |
+
- `crates/forge_domain/src/agent_definition.rs` - Add provider tools configuration
|
| 64 |
+
- `crates/forge_app/src/dto/tools_overview.rs` - Include provider tools in overview
|
| 65 |
+
|
| 66 |
+
- [ ] 7. Create provider tool factory and registration
|
| 67 |
+
Implement a factory pattern for registering provider tools with the system. This factory will create provider tool definitions for different providers (OpenAI, Anthropic, etc.) and make them available to agents that request them.
|
| 68 |
+
|
| 69 |
+
Rationale: Different providers support different provider-executed tools. A factory pattern allows the system to create the appropriate tool definitions based on the provider being used. This is extensible - new providers can be added by implementing their tool factory without modifying core logic.
|
| 70 |
+
|
| 71 |
+
Affected files:
|
| 72 |
+
- `crates/forge_app/src/provider_tools/factory.rs` - New file for tool factory
|
| 73 |
+
- `crates/forge_app/src/provider_tools/mod.rs` - New module
|
| 74 |
+
- `crates/forge_app/src/dto/tools_overview.rs` - Integrate with factory
|
| 75 |
+
|
| 76 |
+
- [ ] 8. Update the response parsing to handle provider tool results
|
| 77 |
+
Modify the response parsing logic in `crates/forge_app/src/dto/openai/response.rs` to correctly parse and handle tool results from provider-executed tools. This includes recognizing when a tool result comes from a provider tool and ensuring it's properly formatted.
|
| 78 |
+
|
| 79 |
+
Rationale: Provider tool results may have different structures than standard tool results. The response parser needs to handle these differences and convert them to the internal `ToolResult` format consistently. This ensures that tool results are processed correctly regardless of whether they came from a client-executed or provider-executed tool.
|
| 80 |
+
|
| 81 |
+
Affected files:
|
| 82 |
+
- `crates/forge_app/src/dto/openai/response.rs` - Update tool result parsing
|
| 83 |
+
- `crates/forge_app/src/dto/openai/response.rs` - Handle provider-specific result formats
|
| 84 |
+
|
| 85 |
+
- [ ] 9. Add provider tool support to the tool overview API
|
| 86 |
+
Update the `ToolsOverview` structure and related API endpoints to include provider-executed tools alongside system tools, agent tools, and MCP tools. This allows clients to discover which provider tools are available.
|
| 87 |
+
|
| 88 |
+
Rationale: The current `ToolsOverview` at `crates/forge_app/src/dto/tools_overview.rs:9` includes system tools, agents, and MCP tools. Provider tools should be included in this overview so clients can see all available tools, including those executed by providers. This maintains consistency with the existing tool discovery mechanism.
|
| 89 |
+
|
| 90 |
+
Affected files:
|
| 91 |
+
- `crates/forge_app/src/dto/tools_overview.rs` - Add provider tools field
|
| 92 |
+
- `crates/forge_app/src/tool_registry.rs` - Update tools_overview method
|
| 93 |
+
|
| 94 |
+
- [ ] 10. Implement Anthropic provider tools support
|
| 95 |
+
Create provider tool definitions for Anthropic's provider-executed tools (e.g., `computer_20241022`, `textEditor_20241022`, `bash_20241022`, `webSearch_20250305`, `codeExecution_20250522`). This demonstrates the extensibility of the provider tool system to multiple providers.
|
| 96 |
+
|
| 97 |
+
Rationale: Anthropic offers provider-executed tools similar to OpenAI but with different names and capabilities. Implementing support for Anthropic provider tools validates that the architecture is provider-agnostic and extensible. It follows the same pattern established for OpenAI tools.
|
| 98 |
+
|
| 99 |
+
Affected files:
|
| 100 |
+
- `crates/forge_app/src/dto/anthropic/provider_tools.rs` - New file for Anthropic provider tools
|
| 101 |
+
- `crates/forge_app/src/dto/anthropic/request.rs` - Update conversion logic
|
| 102 |
+
- `crates/forge_app/src/provider_tools/factory.rs` - Add Anthropic tool factory
|
| 103 |
+
|
| 104 |
+
- [ ] 11. Add tests for provider tool conversion and execution
|
| 105 |
+
Create comprehensive tests covering provider tool definition, conversion to provider formats, integration with Context to Request conversion, and handling of provider tool results. Tests should verify that provider tools are correctly formatted and that client-executed tools remain unaffected.
|
| 106 |
+
|
| 107 |
+
Rationale: Provider tools introduce new code paths and conversion logic that need thorough testing. Tests should ensure that provider tools are correctly converted to their provider-specific formats, that they don't interfere with standard tool execution, and that the system handles mixed scenarios (both provider and client tools) correctly.
|
| 108 |
+
|
| 109 |
+
Affected files:
|
| 110 |
+
- `crates/forge_app/src/dto/openai/request.rs` - Add conversion tests
|
| 111 |
+
- `crates/forge_app/src/dto/anthropic/request.rs` - Add conversion tests
|
| 112 |
+
- `crates/forge_app/src/tool_registry.rs` - Add execution handling tests
|
| 113 |
+
|
| 114 |
+
- [ ] 12. Update documentation and examples
|
| 115 |
+
Document the provider tool system, including how to define provider tools, how they are converted to provider formats, and how agents can be configured to use them. Include examples of using `web_search` and other provider tools.
|
| 116 |
+
|
| 117 |
+
Rationale: Provider tools are a new concept in the system. Documentation is essential for developers to understand when and how to use provider-executed tools versus client-executed tools. Examples should demonstrate practical usage patterns and configuration options.
|
| 118 |
+
|
| 119 |
+
Affected files:
|
| 120 |
+
- `docs/provider-tools.md` - New documentation file
|
| 121 |
+
- `examples/` - Add example configurations
|
| 122 |
+
|
| 123 |
+
## Verification Criteria
|
| 124 |
+
|
| 125 |
+
- [ ] Provider tool definitions can be created and marked as provider-executed
|
| 126 |
+
- [ ] Provider tools are correctly converted to OpenAI-specific format (for example, using the type field to specify web search instead of function format)
|
| 127 |
+
- [ ] Provider tools are correctly converted to Anthropic-specific format
|
| 128 |
+
- [ ] Context to Request conversion includes provider tools in the correct format
|
| 129 |
+
- [ ] Tool registry recognizes provider tools and skips local execution
|
| 130 |
+
- [ ] Provider tool results from API responses are correctly parsed and handled
|
| 131 |
+
- [ ] Agent definitions can be configured with provider tools
|
| 132 |
+
- [ ] Tool overview API includes provider tools alongside other tool types
|
| 133 |
+
- [ ] All existing tests pass without modification
|
| 134 |
+
- [ ] New tests cover provider tool conversion, execution, and result handling
|
| 135 |
+
- [ ] Documentation describes the provider tool system with examples
|
| 136 |
+
|
| 137 |
+
## Potential Risks and Mitigations
|
| 138 |
+
|
| 139 |
+
1. **Breaking changes to existing tool definitions**
|
| 140 |
+
Risk: Adding a new field to `ToolDefinition` could break existing serialization/deserialization or require migrations.
|
| 141 |
+
|
| 142 |
+
Mitigation: Make the new field optional with a default value of `false` (client-executed). This ensures backward compatibility with existing tool definitions. Run all existing tests to verify no regressions.
|
| 143 |
+
|
| 144 |
+
2. **Provider tool format incompatibility**
|
| 145 |
+
Risk: Different providers may have incompatible tool formats, making it difficult to create a unified abstraction.
|
| 146 |
+
|
| 147 |
+
Mitigation: Use provider-specific conversion modules (e.g., `openai_provider_tools`, `anthropic_provider_tools`) that handle format conversion independently. The common `ToolDefinition` serves as an abstraction, but each provider has its own conversion logic.
|
| 148 |
+
|
| 149 |
+
3. **Tool result handling complexity**
|
| 150 |
+
Risk: Provider tool results may have different structures that are difficult to normalize into a common `ToolResult` format.
|
| 151 |
+
|
| 152 |
+
Mitigation: Create provider-specific result parsers that normalize results to the internal format. Handle unknown or unexpected result formats gracefully with appropriate error messages.
|
| 153 |
+
|
| 154 |
+
4. **Execution model confusion**
|
| 155 |
+
Risk: Developers may be confused about which tools are executed locally versus by the provider, leading to unexpected behavior.
|
| 156 |
+
|
| 157 |
+
Mitigation: Clearly document the distinction in the `ToolDefinition` structure and provide tool overview information that indicates execution location (client vs. provider). Use descriptive naming conventions.
|
| 158 |
+
|
| 159 |
+
5. **Testing complexity**
|
| 160 |
+
Risk: Testing provider tools requires mocking provider responses, which can be complex and brittle.
|
| 161 |
+
|
| 162 |
+
Mitigation: Create integration tests that use real provider APIs for critical paths, supplemented by unit tests with mocked responses. Use snapshot testing to verify correct request formatting.
|
| 163 |
+
|
| 164 |
+
## Alternative Approaches
|
| 165 |
+
|
| 166 |
+
1. **Separate tool type enum instead of boolean flag**
|
| 167 |
+
Instead of adding a boolean field to `ToolDefinition`, create an enum `ToolExecutionType` with variants `Client` and `Provider`. This is more explicit and extensible, allowing future execution types (e.g., `Hybrid`, `Delegated`).
|
| 168 |
+
|
| 169 |
+
Trade-offs: More explicit and type-safe, but adds complexity. The boolean flag is simpler for the current use case and can be extended to an enum later if needed.
|
| 170 |
+
|
| 171 |
+
2. **Provider-specific tool registries**
|
| 172 |
+
Create separate tool registries for each provider (OpenAI, Anthropic, etc.) rather than a unified registry. Each provider would manage its own tools, including provider-executed tools.
|
| 173 |
+
|
| 174 |
+
Trade-offs: Cleaner separation of concerns and easier provider-specific logic, but increases code duplication and makes it harder to share tools across providers.
|
| 175 |
+
|
| 176 |
+
3. **Dynamic tool discovery from provider APIs**
|
| 177 |
+
Instead of hard-coding provider tool definitions, query provider APIs to discover available tools and their schemas dynamically. This would automatically support new provider tools as they are added.
|
| 178 |
+
|
| 179 |
+
Trade-offs: More flexible and future-proof, but introduces runtime dependencies on provider APIs and may be slower. Requires robust error handling for API failures.
|
| 180 |
+
|
| 181 |
+
4. **Tool execution delegation pattern**
|
| 182 |
+
Implement a delegation pattern where tools specify an executor (client, provider, MCP, etc.) through a trait. The tool registry would delegate execution based on the executor specified by the tool.
|
| 183 |
+
|
| 184 |
+
Trade-offs: More flexible and extensible architecture, but adds abstraction layers and complexity. The current approach of checking tool type is simpler for the immediate use case.
|
| 185 |
+
EOF
|
| 186 |
+
- |
|
| 187 |
+
FORGE_DEBUG_REQUESTS='{{dir}}/context.json' forgee --provider open_router --model {{model}} -p '{{task}}'
|
| 188 |
+
parallelism: 2
|
| 189 |
+
timeout: 180
|
| 190 |
+
early_exit: true
|
| 191 |
+
validations:
|
| 192 |
+
- name: "Should use patch tool"
|
| 193 |
+
type: shell
|
| 194 |
+
command: "jq -e '[.messages[]?.tool_calls[]? | select(.function.name == \"patch\")] | length > 0' {{dir}}/context.json"
|
| 195 |
+
|
| 196 |
+
- name: "Should NOT have patch failures due to missing operation field"
|
| 197 |
+
type: shell
|
| 198 |
+
command: "! jq -e '[.messages[]? | select(.role == \"tool\" and .name == \"patch\" and (.content | contains(\"missing field\") and contains(\"operation\")))] | length > 0' {{dir}}/context.json"
|
| 199 |
+
|
| 200 |
+
- name: "Should NOT have patch failures due to text mismatch"
|
| 201 |
+
type: shell
|
| 202 |
+
command: "! jq -e '[.messages[]? | select(.role == \"tool\" and .name == \"patch\" and (.content | contains(\"Could not find match for search text\")))] | length > 0' {{dir}}/context.json"
|
| 203 |
+
|
| 204 |
+
- name: "Patch search text should match file content exactly"
|
| 205 |
+
type: shell
|
| 206 |
+
command: |
|
| 207 |
+
# Extract all patch tool calls and verify search text exists in the file
|
| 208 |
+
# This is a heuristic check - we look for patch calls that should have succeeded
|
| 209 |
+
jq -e '[.messages[]?.tool_calls[]? | select(.function.name == "patch")] | length > 0' {{dir}}/context.json
|
| 210 |
+
|
| 211 |
+
sources:
|
| 212 |
+
- value:
|
| 213 |
+
- model: "anthropic/claude-sonnet-4.5"
|
| 214 |
+
- model: "z-ai/glm-4.6:exacto"
|
| 215 |
+
- model: "minimax/minimax-m2.1"
|
| 216 |
+
|
| 217 |
+
- csv: patch_exact_match_tasks.csv
|
benchmarks/evals/redundant_cd_with_cwd/task.yml
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
run:
|
| 2 |
+
- git clone --depth=1 --branch main https://github.com/tailcallhq/forgecode .
|
| 3 |
+
- mkdir ../test-dir && cd ../test-dir
|
| 4 |
+
- FORGE_DEBUG_REQUESTS='{{dir}}/context.json' forgee --provider open_router --model {{model}} -p '{{task}}'
|
| 5 |
+
parallelism: 5
|
| 6 |
+
timeout: 120
|
| 7 |
+
early_exit: false # We want to see all cases, not just the first success
|
| 8 |
+
|
| 9 |
+
validations:
|
| 10 |
+
# Check if any shell tool call contains "cd" command when cwd is also provided
|
| 11 |
+
- name: "No redundant cd with cwd"
|
| 12 |
+
type: shell
|
| 13 |
+
command: |
|
| 14 |
+
jq -e '
|
| 15 |
+
[.messages[]?.tool_calls[]? |
|
| 16 |
+
select(.function.name == "shell") |
|
| 17 |
+
.function.arguments | fromjson |
|
| 18 |
+
select((.command | contains("cd "))) |
|
| 19 |
+
{command: .command, cwd: .cwd}
|
| 20 |
+
] | length == 0
|
| 21 |
+
' {{dir}}/context.json
|
| 22 |
+
|
| 23 |
+
# Detect specific pattern: "cd <path> && <command>" with cwd set
|
| 24 |
+
- name: "No cd && pattern with cwd"
|
| 25 |
+
type: shell
|
| 26 |
+
command: |
|
| 27 |
+
jq -e '
|
| 28 |
+
[.messages[]?.tool_calls[]? |
|
| 29 |
+
select(.function.name == "shell") |
|
| 30 |
+
.function.arguments | fromjson |
|
| 31 |
+
select((.command | test("cd\\s+[^;]+\\s*&&"))) |
|
| 32 |
+
{command: .command, cwd: .cwd, issue: "redundant_cd_with_cwd"}
|
| 33 |
+
] | length == 0
|
| 34 |
+
' {{dir}}/context.json
|
| 35 |
+
|
| 36 |
+
sources:
|
| 37 |
+
- value:
|
| 38 |
+
- model: "anthropic/claude-sonnet-4.5"
|
| 39 |
+
- model: "z-ai/glm-4.6:exacto"
|
| 40 |
+
- value:
|
| 41 |
+
- task: "clean ~/tailcall"
|
| 42 |
+
- task: "format code in ~/task/crates/forge_domain"
|
| 43 |
+
- task: "install dependencies in ~/task/benchmarks"
|
| 44 |
+
- task: "cd to ~/task/crates and run cargo test"
|
| 45 |
+
- task: "go to ~/task/docs and list files"
|
benchmarks/evals/search_over_find/task.yml
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
run:
|
| 2 |
+
- git clone --depth=1 --branch main https://github.com/tailcallhq/forgecode .
|
| 3 |
+
- FORGE_DEBUG_REQUESTS='{{dir}}/context.json' forgee --provider open_router --model {{model}} -p '{{task}}'
|
| 4 |
+
parallelism: 10
|
| 5 |
+
timeout: 180
|
| 6 |
+
early_exit: true
|
| 7 |
+
|
| 8 |
+
validations:
|
| 9 |
+
# Primary: Model should NOT use shell find command for searching code
|
| 10 |
+
- name: "No shell find command usage"
|
| 11 |
+
type: shell
|
| 12 |
+
command: |
|
| 13 |
+
jq -e '
|
| 14 |
+
[.messages[]?.tool_calls[]? |
|
| 15 |
+
select(.function.name == "shell") |
|
| 16 |
+
.function.arguments | fromjson |
|
| 17 |
+
select(.command | test("\\bfind\\s+"))
|
| 18 |
+
] | length == 0
|
| 19 |
+
' {{dir}}/context.json
|
| 20 |
+
|
| 21 |
+
# Model SHOULD use builtin search tools instead
|
| 22 |
+
- name: "Uses builtin search tools"
|
| 23 |
+
type: shell
|
| 24 |
+
command: |
|
| 25 |
+
jq -e '
|
| 26 |
+
[.messages[]?.tool_calls[]? |
|
| 27 |
+
select(.function.name == "search" or
|
| 28 |
+
.function.name == "fs_search" or
|
| 29 |
+
.function.name == "sem_search")
|
| 30 |
+
] | length > 0
|
| 31 |
+
' {{dir}}/context.json
|
| 32 |
+
|
| 33 |
+
# Detect specific anti-pattern: find with grep pipeline
|
| 34 |
+
- name: "No find + grep anti-pattern"
|
| 35 |
+
type: shell
|
| 36 |
+
command: |
|
| 37 |
+
jq -e '
|
| 38 |
+
[.messages[]?.tool_calls[]? |
|
| 39 |
+
select(.function.name == "shell") |
|
| 40 |
+
.function.arguments | fromjson |
|
| 41 |
+
select(.command | test("find.*\\|.*grep"))
|
| 42 |
+
] | length == 0
|
| 43 |
+
' {{dir}}/context.json
|
| 44 |
+
|
| 45 |
+
sources:
|
| 46 |
+
- value:
|
| 47 |
+
- model: "anthropic/claude-sonnet-4.5"
|
| 48 |
+
- model: "z-ai/glm-4.6:exacto"
|
| 49 |
+
- value:
|
| 50 |
+
# File discovery tasks - should use fs_search, NOT find command
|
| 51 |
+
- task: "I need to merge service and app crates. Tell me what files exist in the crates/ directory."
|
| 52 |
+
- task: "Find all Rust source files in the crates/service directory."
|
| 53 |
+
- task: "List all Rust files in crates/app to understand the structure."
|
| 54 |
+
- task: "Show me all .rs files in the project so I can plan refactoring."
|
| 55 |
+
|
| 56 |
+
# Structure understanding - should use fs_search or sem_search
|
| 57 |
+
- task: "Find all source files under crates/service and crates/app for analysis."
|
| 58 |
+
- task: "List all Rust source files in the workspace for documentation purposes."
|
benchmarks/evals/sem_search/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Semantic Search Tool Selection Evaluation
|
| 2 |
+
|
| 3 |
+
This evaluation validates that the Forge agent correctly identifies and uses the `sem_search` (Codebase Search) tool when presented with conceptual, functionality-based code location queries.
|
| 4 |
+
|
| 5 |
+
## What We're Testing
|
| 6 |
+
|
| 7 |
+
The agent's ability to recognize when a user query requires semantic understanding rather than exact pattern matching. Specifically, we test whether the agent:
|
| 8 |
+
|
| 9 |
+
1. **Correctly invokes `sem_search`** for conceptual queries about code functionality
|
| 10 |
+
2. **Understands the distinction** between semantic search (conceptual) vs regex search (exact patterns)
|
| 11 |
+
3. **Recognizes appropriate use cases** such as:
|
| 12 |
+
- Finding code by its purpose/behavior (e.g., "retry logic with exponential backoff")
|
| 13 |
+
- Locating implementation patterns (e.g., "authentication token validation")
|
| 14 |
+
- Discovering architectural components (e.g., "message transformation between AI providers")
|
| 15 |
+
- Identifying system behaviors (e.g., "rate limiting for API requests")
|
| 16 |
+
|
| 17 |
+
## Test Scenarios
|
| 18 |
+
|
| 19 |
+
The evaluation uses real-world queries that describe:
|
| 20 |
+
- **Message transformation** between different AI provider formats
|
| 21 |
+
- **Retry mechanisms** with exponential backoff patterns
|
| 22 |
+
- **Authentication flows** including token validation and refresh
|
| 23 |
+
- **Error handling** for network failures
|
| 24 |
+
- **Caching strategies** for API responses
|
| 25 |
+
- **Streaming response** handling from LLM APIs
|
| 26 |
+
- **File validation** including upload and size checks
|
| 27 |
+
- **Rate limiting** implementations for API requests
|
| 28 |
+
- **Tool registration** and availability management
|
| 29 |
+
- **Context management** including conversation history truncation
|
| 30 |
+
|
| 31 |
+
## Expected Behavior
|
| 32 |
+
|
| 33 |
+
For all test queries, the agent should invoke the `sem_search` tool (displayed as "Codebase Search" in logs) rather than falling back to regex-based `search` tool, demonstrating understanding that these are conceptual queries requiring semantic code comprehension.
|
benchmarks/evals/sem_search/task.yml
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
run:
|
| 2 |
+
# Clone into `tmp/task` dir
|
| 3 |
+
- git clone --depth=1 --branch main https://github.com/tailcallhq/forgecode .
|
| 4 |
+
- forge workspace init --yes
|
| 5 |
+
- forge workspace sync
|
| 6 |
+
- FORGE_DEBUG_REQUESTS='{{dir}}/context.jsonl' FORGE_SESSION__PROVIDER_ID=open_router FORGE_SESSION__MODEL_ID={{model}} forge -p '{{task}}'
|
| 7 |
+
parallelism: 8
|
| 8 |
+
timeout: 60
|
| 9 |
+
early_exit: true
|
| 10 |
+
validations:
|
| 11 |
+
- name: "Uses codebase search tool"
|
| 12 |
+
type: shell
|
| 13 |
+
command: grep -q "Codebase Search" '{{dir}}/task.log'
|
| 14 |
+
- name: "Does not call task tool before sem_search"
|
| 15 |
+
type: shell
|
| 16 |
+
command: "python3 -c \"\nimport sys\nlog = open('{{dir}}/task.log').read().splitlines()\ntask_line = next((i for i, l in enumerate(log) if '[Agent]' in l), None)\nsearch_line = next((i for i, l in enumerate(log) if 'Codebase Search' in l), None)\nif task_line is None: sys.exit(0)\nif search_line is None: sys.exit(1)\nsys.exit(0 if search_line < task_line else 1)\n\""
|
| 17 |
+
sources:
|
| 18 |
+
- value:
|
| 19 |
+
# - model: "x-ai/grok-code-fast-1"
|
| 20 |
+
- model: "anthropic/claude-sonnet-4.6"
|
| 21 |
+
- value:
|
| 22 |
+
# # Location questions - "where is"
|
| 23 |
+
# - task: "Where is the code that transforms messages between different AI provider formats?"
|
| 24 |
+
# - task: "Where is the error handling code for network failures?"
|
| 25 |
+
# - task: "Where is file upload validation and size checking implemented?"
|
| 26 |
+
# - task: "Where are environment variables parsed and configuration loaded?"
|
| 27 |
+
# - task: "Where is the logic for selecting which AI model to use for a task?"
|
| 28 |
+
|
| 29 |
+
# # Discovery questions - "find"
|
| 30 |
+
# - task: "Find the code that caches API responses to reduce costs"
|
| 31 |
+
# - task: "Find where tool definitions are registered and made available to the AI"
|
| 32 |
+
# - task: "Find the code that manages API keys, tokens, and credentials securely"
|
| 33 |
+
# - task: "Find where Git operations like commit, diff, and status are implemented"
|
| 34 |
+
|
| 35 |
+
# # Understanding questions - "how does"
|
| 36 |
+
# - task: "How does the system handle differences between OpenAI and Anthropic APIs?"
|
| 37 |
+
# - task: "How does the tool execution pipeline process and validate tool calls?"
|
| 38 |
+
# - task: "How are streaming responses from LLMs processed?"
|
| 39 |
+
|
| 40 |
+
# # Need-based questions - "I need to"
|
| 41 |
+
# - task: "I need to find retry logic with exponential backoff for API calls"
|
| 42 |
+
# - task: "I need to understand how streaming responses from LLM APIs work"
|
| 43 |
+
# - task: "I need to see how conversation history is truncated when context is too large"
|
| 44 |
+
# - task: "I need to find examples of rate limiting implementations"
|
| 45 |
+
|
| 46 |
+
# # Show me questions
|
| 47 |
+
# - task: "Show me where authentication tokens are validated and refreshed"
|
| 48 |
+
# - task: "Show me the code that handles rate limiting for API requests"
|
| 49 |
+
# - task: "Show me the code that compacts conversation context when tokens exceed limits"
|
| 50 |
+
# - task: "Show me how the system handles request timeouts and cancellation"
|
| 51 |
+
# - task: "Show me the permission checking and security policy enforcement code"
|
| 52 |
+
|
| 53 |
+
# # Strategy/pattern questions
|
| 54 |
+
# - task: "I want to understand the caching strategy for semantic search embeddings"
|
| 55 |
+
# - task: "What's the strategy for managing concurrent API requests?"
|
| 56 |
+
# - task: "What's the approach for handling API errors and retries?"
|
| 57 |
+
|
| 58 |
+
# # Troubleshooting questions
|
| 59 |
+
# - task: "What could cause API requests to fail?"
|
| 60 |
+
# - task: "How are timeout errors handled in the system?"
|
| 61 |
+
|
| 62 |
+
# # Architecture questions
|
| 63 |
+
# - task: "How do the different AI provider adapters integrate with the main system?"
|
| 64 |
+
# - task: "What's the flow of a user request from input to response?"
|
| 65 |
+
# - task: "How are tools registered and executed?"
|
| 66 |
+
|
| 67 |
+
# # Capability questions
|
| 68 |
+
# - task: "How does the system track token usage?"
|
| 69 |
+
# - task: "What mechanism prevents infinite loops in tool execution?"
|
| 70 |
+
|
| 71 |
+
# Human review tasks - integrated from human-review-sem-search-tasks.json
|
| 72 |
+
- task: "Now that we support piping, how can I use an external editor to type a large command and pipe it to the forge cli?"
|
| 73 |
+
- task: "Is there a type-safe version of commander that can make sure the command is parsed into a type-safe object"
|
| 74 |
+
- task: "Search for ZAI-related model fetching code in the codebase"
|
| 75 |
+
- task: "Look for API endpoints and authentication patterns used for ZAI"
|
| 76 |
+
- task: "Starting a new session always loads the older node version"
|
| 77 |
+
- task: "Examine any existing curl examples or HTTP client implementations"
|
| 78 |
+
- task: "Before sorting the tools we will dedupe all the tools"
|
| 79 |
+
- task: "Identify the current embedding implementation and how it interfaces with OpenRouter"
|
| 80 |
+
- task: "Is there a place in the code base where we are subscribing to events?"
|
| 81 |
+
- task: "There is a bug, where when I paste text the syntax highlighting is lost for a moment until some key is pressed again."
|
| 82 |
+
- task: "Search for any existing validation patterns in the forge_domain crate, particularly looking for validation methods, validate functions, or error types used for domain validation"
|
| 83 |
+
- task: "Show me the temperature type"
|
| 84 |
+
- task: "Search for cancellation mechanisms including cancellation tokens, abort signals, task cancellation, and how the system handles user interrupts or cancellation of in-flight requests across all crates"
|
| 85 |
+
- task: "In the workspace info, we should return the creation time of the workspace too."
|
| 86 |
+
- task: "token cost is being sent but we are not able to show it on the ui. Our cost conversation in some place is getting missed."
|
| 87 |
+
- task: "Look at workflow-related files to understand how model should be obtained from API"
|
| 88 |
+
- task: "Lets use the newtype crate for all the type in domain that are newtypes so that we don't need to implement all this boilerplate"
|
| 89 |
+
- task: "Look for any existing patterns in the codebase for ensuring migrations run exactly once across all tests, such as global mutexes or initialization guards"
|
| 90 |
+
- task: "The clear conversation doesn't happen on CMD+K."
|
| 91 |
+
- task: "Use plimit and prompt.all to run it concurrently"
|
| 92 |
+
- task: "Like infra I need another crate called - test_infra that looks similar and has a ForgeInfra struct that has test implementations of stuff."
|
| 93 |
+
- task: "Infra query nodes should take Query<Embedding> instead of each param as a separate arg."
|
| 94 |
+
- task: "Add health check endpoints for monitoring"
|
| 95 |
+
- task: "Examine conversation ID generation and management in domain layer"
|
| 96 |
+
- task: "Lets recursively derive Dummy from fake, on Node and its children."
|
| 97 |
+
- task: "Add a new infra component called Reranker. It takes a list of nodes and reranks them."
|
| 98 |
+
- task: "Update the implementations of Cache based on the new trait implementation."
|
| 99 |
+
- task: "In skill lets add description also"
|
| 100 |
+
- task: "Investigate signal handling and graceful shutdown mechanisms in /Volumes/Bran/code-forge-workspace/code-forge, particularly looking for SIGINT, SIGTERM handling and how they affect ongoing operations"
|
| 101 |
+
- task: "Lets add a new range.rs under arbitrary/ and implement Arbitrary for all the std Range types eg: Range, RangeFrom etc."
|
| 102 |
+
- task: "Make `-v` optional. It should not be required."
|
| 103 |
+
- task: "in ProviderRegistry I need a few more methods - get_active_provider, set_active_provider and get_all_providers"
|
| 104 |
+
- task: "Lets use derive_setters on Config. Drop the handwritten setters"
|
| 105 |
+
- task: "Lets drop path and env from the `info` command. Its a bit too noisy."
|
| 106 |
+
- task: "The Secret type should print the last 4 chars of the secret in debug."
|
| 107 |
+
- task: "What's the purpose of ModelId?"
|
| 108 |
+
- task: "Where is the Agent struct defined?"
|
| 109 |
+
- task: "**Questions**\n\n1. Switching agent happens via CLI `forge config --set-agent muse` or `forge config -i` opens up an interactive configuration manager (RATATUI)\n2. How do I know the active agent and model? `forge config --get-agent` show in via a ZSH theme\n3. Additional CLI commands to be added\n 1. `forge conversation --set-active <cid>`\n 2. `forge info` Show current info including agent, model etc."
|
| 110 |
+
- task: "Find all places where event names are constructed or used, particularly in relation to agent IDs. Look for patterns like 'forge', 'muse', 'sage' in event names and how they map to agents."
|
| 111 |
+
- task: "Change the structure to be a simple - Vec of Vec of Option of String."
|
| 112 |
+
- task: "Where are the fixtures located?"
|
| 113 |
+
- task: "Move McpToolCache to forge_app dto"
|
| 114 |
+
- task: "based on your understanding of the project, where are the tables created in supbase?"
|
| 115 |
+
- task: "Examine how the truncation functions are called and where the results are used. Identify the call sites and understand the data flow from truncation to final output."
|
| 116 |
+
- task: "Like ShowCommands, I need ShowModels and ShowConversations"
|
| 117 |
+
- task: "Lets implement a function on SummaryMessage that can merge consecutive calls together. If the nth can be merged with nth-1, then we should remove the nth-1 and insert the new value."
|
| 118 |
+
- task: "Lets have upload take a glob pattern for upload"
|
| 119 |
+
- task: "Check this for security issues: eval(user_input)"
|
| 120 |
+
- task: "Update the vector dimension to be 4096"
|
| 121 |
+
- task: "Fix this - 'docker run tailcallhq/forgecode'"
|
| 122 |
+
- task: "Document the adapter registration and discovery mechanism"
|
| 123 |
+
- task: "There are multiple places where we throw - \"Conversation not found\" how do u suggest we consolidate and reuse."
|
| 124 |
+
- task: "Is there a crate for humanizing date time format that we are using?"
|
| 125 |
+
- task: "Lets move the domain compaction transformers into app/compact/transformers. Make sure you move them in bulk."
|
| 126 |
+
- task: "Identify the main entry point (main.rs or equivalent) and understand how the application initializes and starts the server."
|
| 127 |
+
- task: "Lets use chrono-humanize instead of humantime in forge_main everywhere."
|
| 128 |
+
- task: "Look at all the docs in .rs files and remove the args and return info from the docs."
|
| 129 |
+
- task: "Investigate why each approach (1-4) has failed - what are the technical limitations we're hitting with ZLE widgets, terminal control sequences, and shell execution contexts"
|
| 130 |
+
- task: "as per rust best practices, what would you recommend should we call arbit? and how should the file be structured. Show me the best examples - purest form"
|
| 131 |
+
- task: "Explain the flow of authentication in the project. I need to understand it deeply, going from high level to micro details."
|
| 132 |
+
- task: "Research xml.rs in forge_domain and create a plan to add helper functions for common XML operations"
|
| 133 |
+
- task: "Add JWT-based authentication system"
|
| 134 |
+
- task: "How does FinishReason work?"
|
| 135 |
+
- task: "Check what changes were made to api_key.rs conversions and identify why the key field might be getting the wrong value"
|
| 136 |
+
- task: "Trace the flow from skill listing to system prompt generation to identify where the integration is missing or broken"
|
| 137 |
+
- task: "What could cause memory leaks in long-running sessions?"
|
| 138 |
+
- task: "I am not able to make send of these name - DefinitionBatchGenerator DefinitionBatch. I need something much better,"
|
benchmarks/evals/semantic_search_quality/README.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Semantic Search Quality Evaluation
|
| 2 |
+
|
| 3 |
+
This evaluation measures the quality of semantic search queries generated by Forge using an LLM-as-judge approach.
|
| 4 |
+
|
| 5 |
+
## Quick Start
|
| 6 |
+
|
| 7 |
+
### Test Your Queries Manually
|
| 8 |
+
|
| 9 |
+
Before running the full eval, test your queries to get immediate feedback:
|
| 10 |
+
|
| 11 |
+
```bash
|
| 12 |
+
# Interactive mode
|
| 13 |
+
npx tsx benchmarks/evals/semantic_search_quality/test_queries.ts
|
| 14 |
+
|
| 15 |
+
# Non-interactive mode
|
| 16 |
+
npx tsx benchmarks/evals/semantic_search_quality/test_queries.ts \
|
| 17 |
+
"retry mechanism with exponential backoff" \
|
| 18 |
+
"I need to find the implementation code for retry logic"
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
The query tester provides:
|
| 22 |
+
- Detailed scores for embedding and reranking queries
|
| 23 |
+
- Specific strengths and weaknesses
|
| 24 |
+
- Actionable suggestions for improvement
|
| 25 |
+
- Differentiation analysis
|
| 26 |
+
|
| 27 |
+
### Run the Full Evaluation
|
| 28 |
+
|
| 29 |
+
```bash
|
| 30 |
+
# Run the entire semantic search quality eval
|
| 31 |
+
npm run eval semantic_search_quality
|
| 32 |
+
|
| 33 |
+
# Run with specific parallelism
|
| 34 |
+
npm run eval semantic_search_quality -- --parallelism 10
|
| 35 |
+
|
| 36 |
+
# Run with specific model
|
| 37 |
+
npm run eval semantic_search_quality -- --model "anthropic/claude-sonnet-4.5"
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
## Purpose
|
| 41 |
+
|
| 42 |
+
The semantic search feature uses two types of queries:
|
| 43 |
+
1. **Embedding Query (`query`)**: Converted to vector embeddings for semantic similarity search
|
| 44 |
+
2. **Reranking Query (`use_case`)**: Used by the reranker to filter results based on intent
|
| 45 |
+
|
| 46 |
+
This eval verifies that:
|
| 47 |
+
- Queries are well-formed and contain sufficient context
|
| 48 |
+
- The two queries are different and serve distinct purposes
|
| 49 |
+
- Results match the user's intent (implementation vs docs vs tests, etc.)
|
| 50 |
+
- Correct file types are returned based on intent
|
| 51 |
+
- Unwanted file types are avoided
|
| 52 |
+
|
| 53 |
+
## How It Works
|
| 54 |
+
|
| 55 |
+
### 1. Test Execution
|
| 56 |
+
The eval runs various semantic search tasks covering different intents:
|
| 57 |
+
- **Implementation**: Find actual code, not documentation
|
| 58 |
+
- **Flow Understanding**: Understand how systems work
|
| 59 |
+
- **Architecture**: Understand structural design
|
| 60 |
+
- **Tests**: Find test examples and fixtures
|
| 61 |
+
- **Documentation**: Find docs and guides
|
| 62 |
+
- **Debugging**: Investigate issues
|
| 63 |
+
- **Modification**: Find code to modify
|
| 64 |
+
- **Configuration**: Find config files and schemas
|
| 65 |
+
|
| 66 |
+
### 2. LLM Judge Evaluation
|
| 67 |
+
After each task, an LLM judge (Gemini 3 Pro) evaluates:
|
| 68 |
+
|
| 69 |
+
#### Query Quality (40 points)
|
| 70 |
+
- **Embedding Query (15pts)**: Domain terms, technical context, behavior description
|
| 71 |
+
- **Reranking Query (15pts)**: Intent clarity, context about WHY, specificity
|
| 72 |
+
- **Query Differentiation (10pts)**: Queries serve different purposes
|
| 73 |
+
|
| 74 |
+
#### Result Relevance (60 points)
|
| 75 |
+
- **Intent Matching (25pts)**: Results match the stated intent
|
| 76 |
+
- **File Type Accuracy (20pts)**: Correct file types returned
|
| 77 |
+
- **Avoidance Compliance (15pts)**: Unwanted file types avoided
|
| 78 |
+
|
| 79 |
+
#### Pass Criteria
|
| 80 |
+
- Total score >= 70/100
|
| 81 |
+
- No critical issues (e.g., identical queries, severe intent mismatch)
|
| 82 |
+
|
| 83 |
+
### 3. Structured Output
|
| 84 |
+
The judge uses structured output (Zod schema) to provide:
|
| 85 |
+
- Detailed scores for each dimension
|
| 86 |
+
- Specific feedback on what's good/bad
|
| 87 |
+
- List of critical issues
|
| 88 |
+
- Overall pass/fail determination
|
| 89 |
+
|
| 90 |
+
## Environment Variables
|
| 91 |
+
|
| 92 |
+
Required:
|
| 93 |
+
- `GOOGLE_APPLICATION_CREDENTIALS`: Path to Google Cloud service account JSON credentials file
|
| 94 |
+
|
| 95 |
+
Or authenticate using `gcloud auth application-default login`.
|
| 96 |
+
|
| 97 |
+
The LLM judge uses Vertex AI to run Gemini 3 Pro. It supports Google's Application Default Credentials through the `google-auth-library`. The most common authentication method is to set the path to a JSON credentials file in the `GOOGLE_APPLICATION_CREDENTIALS` environment variable.
|
| 98 |
+
|
| 99 |
+
## Test Cases
|
| 100 |
+
|
| 101 |
+
The eval includes diverse test cases:
|
| 102 |
+
|
| 103 |
+
### Implementation Queries
|
| 104 |
+
```yaml
|
| 105 |
+
- task: "Find the retry mechanism with exponential backoff implementation"
|
| 106 |
+
intent: "implementation"
|
| 107 |
+
expected_file_types: "rust,typescript,javascript"
|
| 108 |
+
should_avoid: "markdown,txt,documentation"
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
### Flow Understanding
|
| 112 |
+
```yaml
|
| 113 |
+
- task: "How does the authentication flow work from request to response?"
|
| 114 |
+
intent: "flow_understanding"
|
| 115 |
+
expected_file_types: "rust,typescript"
|
| 116 |
+
should_avoid: "test,markdown"
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
### Documentation
|
| 120 |
+
```yaml
|
| 121 |
+
- task: "Find documentation on how to configure semantic search"
|
| 122 |
+
intent: "documentation"
|
| 123 |
+
expected_file_types: "markdown,txt"
|
| 124 |
+
should_avoid: "rust,typescript,javascript"
|
| 125 |
+
```
|
| 126 |
+
|
| 127 |
+
## Interpreting Results
|
| 128 |
+
|
| 129 |
+
### High Scores (>80)
|
| 130 |
+
Queries are excellent - domain-specific, well-differentiated, and would return highly relevant results.
|
| 131 |
+
|
| 132 |
+
### Medium Scores (60-80)
|
| 133 |
+
Queries are functional but have room for improvement. Check feedback for specific suggestions.
|
| 134 |
+
|
| 135 |
+
### Low Scores (<60)
|
| 136 |
+
Queries have significant issues:
|
| 137 |
+
- Too generic or vague
|
| 138 |
+
- Embedding and reranking queries too similar
|
| 139 |
+
- Intent mismatch
|
| 140 |
+
- Would return wrong file types
|
| 141 |
+
|
| 142 |
+
## Common Issues
|
| 143 |
+
|
| 144 |
+
1. **Identical Queries**: Embedding and reranking queries are the same or too similar
|
| 145 |
+
- **Fix**: Make reranking query focus on intent, not just rephrasing
|
| 146 |
+
|
| 147 |
+
2. **Generic Queries**: Lack domain-specific context
|
| 148 |
+
- **Fix**: Add technical terms, describe behavior, include framework names
|
| 149 |
+
|
| 150 |
+
3. **Intent Mismatch**: Queries would return docs when implementation is needed
|
| 151 |
+
- **Fix**: Be explicit in reranking query about what type of code is needed
|
| 152 |
+
|
| 153 |
+
4. **Poor Differentiation**: Both queries try to do the same thing
|
| 154 |
+
- **Fix**: Embedding = WHAT code does, Reranking = WHY you need it
|
| 155 |
+
|
| 156 |
+
## Architecture
|
| 157 |
+
|
| 158 |
+
```
|
| 159 |
+
task.yml
|
| 160 |
+
├── Runs Forge with semantic search task
|
| 161 |
+
├── Captures debug output (context.json)
|
| 162 |
+
└── Validates with llm_judge.ts
|
| 163 |
+
├── Extracts sem_search calls
|
| 164 |
+
├── Calls Gemini 3 Pro with structured output
|
| 165 |
+
└── Returns evaluation with scores
|
| 166 |
+
```
|
| 167 |
+
|
| 168 |
+
## Extending the Eval
|
| 169 |
+
|
| 170 |
+
To add new test cases:
|
| 171 |
+
|
| 172 |
+
1. Add to `task.yml` under `sources`:
|
| 173 |
+
```yaml
|
| 174 |
+
- task: "Your task description"
|
| 175 |
+
intent: "implementation|flow_understanding|tests|documentation|debugging|modification|architecture|configuration"
|
| 176 |
+
expected_file_types: "comma,separated,extensions"
|
| 177 |
+
should_avoid: "comma,separated,extensions"
|
| 178 |
+
```
|
| 179 |
+
|
| 180 |
+
2. Choose appropriate intent:
|
| 181 |
+
- `implementation`: Actual code implementation
|
| 182 |
+
- `flow_understanding`: Understanding how things work
|
| 183 |
+
- `architecture`: Structural/design understanding
|
| 184 |
+
- `tests`: Test files and fixtures
|
| 185 |
+
- `documentation`: Docs, README, guides
|
| 186 |
+
- `debugging`: Finding bugs/issues
|
| 187 |
+
- `modification`: Code to modify/extend
|
| 188 |
+
- `configuration`: Config files, schemas
|
| 189 |
+
|
| 190 |
+
## Future Improvements
|
| 191 |
+
|
| 192 |
+
- [ ] Add actual result verification (not just query quality)
|
| 193 |
+
- [ ] Measure embedding quality directly
|
| 194 |
+
- [ ] Test reranking effectiveness with real results
|
| 195 |
+
- [ ] Add precision/recall metrics
|
| 196 |
+
- [ ] Compare against human-labeled ground truth
|
| 197 |
+
- [ ] Test edge cases (very short queries, multi-lingual code, etc.)
|
benchmarks/evals/semantic_search_quality/llm_judge.ts
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env tsx
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* LLM-as-Judge for Semantic Search Quality Evaluation
|
| 5 |
+
*
|
| 6 |
+
* This script evaluates the quality of semantic search queries and results
|
| 7 |
+
* using Google's Gemini 3 Pro via Vertex AI as a judge. It verifies:
|
| 8 |
+
* 1. Query quality - Are the embedding and reranking queries well-formed?
|
| 9 |
+
* 2. Result relevance - Do the results match the stated intent?
|
| 10 |
+
* 3. File type accuracy - Are the correct file types returned?
|
| 11 |
+
* 4. Avoidance compliance - Are unwanted file types properly avoided?
|
| 12 |
+
*/
|
| 13 |
+
|
| 14 |
+
import { readFileSync } from "fs";
|
| 15 |
+
import { vertex } from "@ai-sdk/google-vertex";
|
| 16 |
+
import { generateObject } from "ai";
|
| 17 |
+
import { z } from "zod";
|
| 18 |
+
|
| 19 |
+
// Evaluation result schema
|
| 20 |
+
const EvaluationSchema = z.object({
|
| 21 |
+
query_quality: z.object({
|
| 22 |
+
embedding_query_score: z
|
| 23 |
+
.number()
|
| 24 |
+
.min(0)
|
| 25 |
+
.max(15)
|
| 26 |
+
.describe("Score for the embedding query quality (0-15)"),
|
| 27 |
+
embedding_query_feedback: z
|
| 28 |
+
.string()
|
| 29 |
+
.describe("Specific feedback on the embedding query"),
|
| 30 |
+
reranking_query_score: z
|
| 31 |
+
.number()
|
| 32 |
+
.min(0)
|
| 33 |
+
.max(15)
|
| 34 |
+
.describe("Score for the reranking query quality (0-15)"),
|
| 35 |
+
reranking_query_feedback: z
|
| 36 |
+
.string()
|
| 37 |
+
.describe("Specific feedback on the reranking query"),
|
| 38 |
+
construct_keywords_score: z
|
| 39 |
+
.number()
|
| 40 |
+
.min(0)
|
| 41 |
+
.max(10)
|
| 42 |
+
.describe(
|
| 43 |
+
"Score for codebase construct keywords in reranking query (0-10)",
|
| 44 |
+
),
|
| 45 |
+
construct_keywords_feedback: z
|
| 46 |
+
.string()
|
| 47 |
+
.describe(
|
| 48 |
+
"Feedback on use of codebase constructs (struct, trait, impl, interface, class, function, definition, implementation, declaration)",
|
| 49 |
+
),
|
| 50 |
+
queries_differentiated: z
|
| 51 |
+
.boolean()
|
| 52 |
+
.describe(
|
| 53 |
+
"Whether the embedding and reranking queries are sufficiently different",
|
| 54 |
+
),
|
| 55 |
+
}),
|
| 56 |
+
result_relevance: z.object({
|
| 57 |
+
intent_match_score: z
|
| 58 |
+
.number()
|
| 59 |
+
.min(0)
|
| 60 |
+
.max(25)
|
| 61 |
+
.describe("How well results match the stated intent (0-25)"),
|
| 62 |
+
intent_match_feedback: z
|
| 63 |
+
.string()
|
| 64 |
+
.describe("Explanation of intent matching"),
|
| 65 |
+
file_type_accuracy_score: z
|
| 66 |
+
.number()
|
| 67 |
+
.min(0)
|
| 68 |
+
.max(20)
|
| 69 |
+
.describe("Correctness of file types in results (0-20)"),
|
| 70 |
+
file_type_feedback: z.string().describe("Feedback on file type selection"),
|
| 71 |
+
avoidance_compliance_score: z
|
| 72 |
+
.number()
|
| 73 |
+
.min(0)
|
| 74 |
+
.max(15)
|
| 75 |
+
.describe("How well unwanted file types were avoided (0-15)"),
|
| 76 |
+
avoidance_feedback: z.string().describe("Feedback on file type avoidance"),
|
| 77 |
+
}),
|
| 78 |
+
overall: z.object({
|
| 79 |
+
passed: z
|
| 80 |
+
.boolean()
|
| 81 |
+
.describe("Whether the search quality passes the evaluation"),
|
| 82 |
+
total_score: z
|
| 83 |
+
.number()
|
| 84 |
+
.min(0)
|
| 85 |
+
.max(100)
|
| 86 |
+
.describe("Total score (0-100)"),
|
| 87 |
+
summary: z
|
| 88 |
+
.string()
|
| 89 |
+
.describe("Brief summary of the evaluation (2-3 sentences)"),
|
| 90 |
+
critical_issues: z
|
| 91 |
+
.array(z.string())
|
| 92 |
+
.describe("List of critical issues that caused failure"),
|
| 93 |
+
}),
|
| 94 |
+
});
|
| 95 |
+
|
| 96 |
+
type Evaluation = z.infer<typeof EvaluationSchema>;
|
| 97 |
+
|
| 98 |
+
interface ToolCall {
|
| 99 |
+
id: string;
|
| 100 |
+
type: string;
|
| 101 |
+
function: {
|
| 102 |
+
name: string;
|
| 103 |
+
arguments: string;
|
| 104 |
+
};
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
interface Message {
|
| 108 |
+
tool_calls?: ToolCall[];
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
interface ContextData {
|
| 112 |
+
messages: Message[];
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
interface Args {
|
| 116 |
+
context: string;
|
| 117 |
+
intent: string;
|
| 118 |
+
expectedFileTypes: string;
|
| 119 |
+
shouldAvoid: string;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
function parseArgs(): Args {
|
| 123 |
+
const args = process.argv.slice(2);
|
| 124 |
+
const parsed: Record<string, string> = {};
|
| 125 |
+
|
| 126 |
+
for (let i = 0; i < args.length; i += 2) {
|
| 127 |
+
const key = args[i]?.replace(/^--/, "").replace(/-/g, "_");
|
| 128 |
+
const value = args[i + 1];
|
| 129 |
+
if (key && value) {
|
| 130 |
+
parsed[key] = value;
|
| 131 |
+
}
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
if (
|
| 135 |
+
!parsed.context ||
|
| 136 |
+
!parsed.intent ||
|
| 137 |
+
!parsed.expected_file_types ||
|
| 138 |
+
!parsed.should_avoid
|
| 139 |
+
) {
|
| 140 |
+
console.error("Missing required arguments");
|
| 141 |
+
console.error(
|
| 142 |
+
"Usage: llm_judge.ts --context <file> --intent <intent> --expected-file-types <types> --should-avoid <types>",
|
| 143 |
+
);
|
| 144 |
+
process.exit(1);
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
return {
|
| 148 |
+
context: parsed.context,
|
| 149 |
+
intent: parsed.intent,
|
| 150 |
+
expectedFileTypes: parsed.expected_file_types,
|
| 151 |
+
shouldAvoid: parsed.should_avoid,
|
| 152 |
+
};
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
function extractSemanticSearchCalls(contextData: ContextData): Array<{
|
| 156 |
+
query: string;
|
| 157 |
+
use_case: string;
|
| 158 |
+
}> {
|
| 159 |
+
const searches: Array<{ query: string; use_case: string }> = [];
|
| 160 |
+
|
| 161 |
+
for (const message of contextData.messages) {
|
| 162 |
+
if (message.tool_calls) {
|
| 163 |
+
for (const toolCall of message.tool_calls) {
|
| 164 |
+
if (toolCall.function.name === "sem_search") {
|
| 165 |
+
try {
|
| 166 |
+
const args = JSON.parse(toolCall.function.arguments);
|
| 167 |
+
if (args.queries && Array.isArray(args.queries)) {
|
| 168 |
+
for (const q of args.queries) {
|
| 169 |
+
if (q.query && q.use_case) {
|
| 170 |
+
searches.push({
|
| 171 |
+
query: q.query,
|
| 172 |
+
use_case: q.use_case,
|
| 173 |
+
});
|
| 174 |
+
}
|
| 175 |
+
}
|
| 176 |
+
}
|
| 177 |
+
} catch (e) {
|
| 178 |
+
// Skip invalid JSON
|
| 179 |
+
}
|
| 180 |
+
}
|
| 181 |
+
}
|
| 182 |
+
}
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
return searches;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
async function evaluateWithLLM(
|
| 189 |
+
searches: Array<{ query: string; use_case: string }>,
|
| 190 |
+
intent: string,
|
| 191 |
+
expectedFileTypes: string,
|
| 192 |
+
shouldAvoid: string,
|
| 193 |
+
): Promise<Evaluation> {
|
| 194 |
+
const prompt = `You are an expert evaluator of semantic code search quality. Your task is to evaluate the quality of semantic search queries and assess whether they would return relevant results based on the user's intent.
|
| 195 |
+
|
| 196 |
+
## User's Intent
|
| 197 |
+
The user wants to: ${intent}
|
| 198 |
+
|
| 199 |
+
## Expected File Types
|
| 200 |
+
The search should primarily return files of these types: ${expectedFileTypes}
|
| 201 |
+
|
| 202 |
+
## File Types to Avoid
|
| 203 |
+
The search should avoid returning files of these types: ${shouldAvoid}
|
| 204 |
+
|
| 205 |
+
## Semantic Search Queries Generated
|
| 206 |
+
${searches.map((s, i) => `### Query ${i + 1}
|
| 207 |
+
**Embedding Query (for vector search):** ${s.query}
|
| 208 |
+
**Reranking Query (for result filtering):** ${s.use_case}
|
| 209 |
+
`).join("\n")}
|
| 210 |
+
|
| 211 |
+
## Evaluation Criteria
|
| 212 |
+
|
| 213 |
+
### Query Quality (50 points)
|
| 214 |
+
1. **Embedding Query (15 points):**
|
| 215 |
+
- Contains domain-specific terms and technical context
|
| 216 |
+
- Describes behavior and functionality, not meta-descriptions
|
| 217 |
+
- Is complete and descriptive, not overly generic
|
| 218 |
+
- Would generate embeddings that match similar code
|
| 219 |
+
|
| 220 |
+
2. **Reranking Query (15 points):**
|
| 221 |
+
- Expresses the specific intent and use case
|
| 222 |
+
- Provides context about WHY the code is needed
|
| 223 |
+
- Specifies whether implementation/docs/tests are needed
|
| 224 |
+
- Is different from the embedding query (not verbatim)
|
| 225 |
+
|
| 226 |
+
3. **Codebase Construct Keywords (10 points):**
|
| 227 |
+
- **CRITICAL**: When looking for code (not documentation), the reranking query MUST include specific construct keywords
|
| 228 |
+
- These keywords get HIGH WEIGHTAGE in reranking: struct, trait, impl, interface, class, function, definition, implementation, declaration, type
|
| 229 |
+
- Examples of GOOD use_case queries:
|
| 230 |
+
* "I need the struct definition for User authentication"
|
| 231 |
+
* "Show me the trait implementation for DatabaseConnection"
|
| 232 |
+
* "Find the function implementation that handles file patching"
|
| 233 |
+
* "I need the type declarations and interface definitions for the tool registry"
|
| 234 |
+
- Examples of BAD use_case queries (missing construct keywords):
|
| 235 |
+
* "I need code that handles authentication" (missing: struct/trait/impl/function)
|
| 236 |
+
* "Show me the database logic" (missing: trait/impl/function)
|
| 237 |
+
* "Find file patching code" (missing: function/impl/struct)
|
| 238 |
+
- **Scoring**:
|
| 239 |
+
* 10 points: Multiple specific construct keywords (struct, trait, impl, function, etc.)
|
| 240 |
+
* 7-9 points: At least one specific construct keyword
|
| 241 |
+
* 4-6 points: Generic keywords (code, implementation) without specifics
|
| 242 |
+
* 0-3 points: No construct keywords at all
|
| 243 |
+
- **Exception**: Documentation/config intents don't need construct keywords
|
| 244 |
+
|
| 245 |
+
4. **Query Differentiation (10 points):**
|
| 246 |
+
- The two queries serve different purposes
|
| 247 |
+
- Embedding query focuses on WHAT (semantic similarity)
|
| 248 |
+
- Reranking query focuses on INTENT + CONSTRUCTS (contextual relevance + code structure)
|
| 249 |
+
|
| 250 |
+
### Result Relevance (50 points)
|
| 251 |
+
Based on the queries, evaluate whether they would return appropriate results:
|
| 252 |
+
|
| 253 |
+
1. **Intent Matching (25 points):**
|
| 254 |
+
- Would the queries find code matching the user's intent?
|
| 255 |
+
- For "implementation" intent: would it find actual code, not docs?
|
| 256 |
+
- For "documentation" intent: would it find docs, not code?
|
| 257 |
+
- For "tests" intent: would it find test files?
|
| 258 |
+
- For "flow_understanding": would it find the relevant code flow?
|
| 259 |
+
- For "debugging": would it find the problematic areas?
|
| 260 |
+
- For "modification": would it find the right code to modify?
|
| 261 |
+
|
| 262 |
+
2. **File Type Accuracy (20 points):**
|
| 263 |
+
- Would the queries naturally return the expected file types?
|
| 264 |
+
- Are the queries specific enough to filter for the right extensions?
|
| 265 |
+
|
| 266 |
+
3. **Avoidance Compliance (15 points):**
|
| 267 |
+
- Would the queries avoid returning the unwanted file types?
|
| 268 |
+
- Is the intent clear enough to prevent irrelevant results?
|
| 269 |
+
|
| 270 |
+
## Scoring Guidelines
|
| 271 |
+
- **9-10:** Excellent - Professional quality, no significant issues
|
| 272 |
+
- **7-8:** Good - Minor improvements possible but functional
|
| 273 |
+
- **5-6:** Adequate - Some issues but would work in most cases
|
| 274 |
+
- **3-4:** Poor - Significant issues that impact effectiveness
|
| 275 |
+
- **0-2:** Failing - Critical problems, would not work as intended
|
| 276 |
+
|
| 277 |
+
## Pass/Fail Criteria
|
| 278 |
+
- **PASS:** Total score >= 70/100 AND no critical issues
|
| 279 |
+
- **FAIL:** Total score < 70/100 OR has critical issues
|
| 280 |
+
|
| 281 |
+
Critical issues include:
|
| 282 |
+
- Embedding and reranking queries are identical or nearly identical
|
| 283 |
+
- Query is too generic to return relevant results
|
| 284 |
+
- Intent mismatch is severe (e.g., asking for implementation but would return docs)
|
| 285 |
+
- Would definitely return wrong file types for the intent
|
| 286 |
+
|
| 287 |
+
Please evaluate these queries and provide detailed, constructive feedback.`;
|
| 288 |
+
|
| 289 |
+
try {
|
| 290 |
+
// @ts-ignore - Type instantiation depth issue with complex Zod schemas
|
| 291 |
+
const result = await generateObject({
|
| 292 |
+
model: vertex("gemini-3-pro-preview"),
|
| 293 |
+
schema: EvaluationSchema,
|
| 294 |
+
prompt,
|
| 295 |
+
temperature: 0.3, // Lower temperature for more consistent evaluations
|
| 296 |
+
});
|
| 297 |
+
|
| 298 |
+
return result.object as Evaluation;
|
| 299 |
+
} catch (error) {
|
| 300 |
+
console.error("Error calling Gemini API:", error);
|
| 301 |
+
throw error;
|
| 302 |
+
}
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
function formatEvaluation(evaluation: Evaluation): string {
|
| 306 |
+
const lines: string[] = [];
|
| 307 |
+
|
| 308 |
+
lines.push("=== SEMANTIC SEARCH QUALITY EVALUATION ===\n");
|
| 309 |
+
|
| 310 |
+
// Query Quality
|
| 311 |
+
lines.push("## Query Quality");
|
| 312 |
+
lines.push(
|
| 313 |
+
`Embedding Query: ${evaluation.query_quality.embedding_query_score}/15`,
|
| 314 |
+
);
|
| 315 |
+
lines.push(` ${evaluation.query_quality.embedding_query_feedback}`);
|
| 316 |
+
lines.push(
|
| 317 |
+
`Reranking Query: ${evaluation.query_quality.reranking_query_score}/15`,
|
| 318 |
+
);
|
| 319 |
+
lines.push(` ${evaluation.query_quality.reranking_query_feedback}`);
|
| 320 |
+
lines.push(
|
| 321 |
+
`Construct Keywords: ${evaluation.query_quality.construct_keywords_score}/10`,
|
| 322 |
+
);
|
| 323 |
+
lines.push(` ${evaluation.query_quality.construct_keywords_feedback}`);
|
| 324 |
+
lines.push(
|
| 325 |
+
`Queries Differentiated: ${evaluation.query_quality.queries_differentiated ? "✓ Yes" : "✗ No"}`,
|
| 326 |
+
);
|
| 327 |
+
lines.push("");
|
| 328 |
+
|
| 329 |
+
// Result Relevance
|
| 330 |
+
lines.push("## Result Relevance");
|
| 331 |
+
lines.push(
|
| 332 |
+
`Intent Match: ${evaluation.result_relevance.intent_match_score}/25`,
|
| 333 |
+
);
|
| 334 |
+
lines.push(` ${evaluation.result_relevance.intent_match_feedback}`);
|
| 335 |
+
lines.push(
|
| 336 |
+
`File Type Accuracy: ${evaluation.result_relevance.file_type_accuracy_score}/20`,
|
| 337 |
+
);
|
| 338 |
+
lines.push(` ${evaluation.result_relevance.file_type_feedback}`);
|
| 339 |
+
lines.push(
|
| 340 |
+
`Avoidance Compliance: ${evaluation.result_relevance.avoidance_compliance_score}/15`,
|
| 341 |
+
);
|
| 342 |
+
lines.push(` ${evaluation.result_relevance.avoidance_feedback}`);
|
| 343 |
+
lines.push("");
|
| 344 |
+
|
| 345 |
+
// Overall
|
| 346 |
+
lines.push("## Overall Assessment");
|
| 347 |
+
lines.push(`Total Score: ${evaluation.overall.total_score}/100`);
|
| 348 |
+
lines.push(`Status: ${evaluation.overall.passed ? "✓ PASS" : "✗ FAIL"}`);
|
| 349 |
+
lines.push(`Summary: ${evaluation.overall.summary}`);
|
| 350 |
+
|
| 351 |
+
if (evaluation.overall.critical_issues.length > 0) {
|
| 352 |
+
lines.push("\n## Critical Issues:");
|
| 353 |
+
for (const issue of evaluation.overall.critical_issues) {
|
| 354 |
+
lines.push(` - ${issue}`);
|
| 355 |
+
}
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
return lines.join("\n");
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
async function main() {
|
| 362 |
+
const args = parseArgs();
|
| 363 |
+
|
| 364 |
+
// Read context file
|
| 365 |
+
let contextData: ContextData;
|
| 366 |
+
try {
|
| 367 |
+
const contextContent = readFileSync(args.context, "utf-8");
|
| 368 |
+
contextData = JSON.parse(contextContent);
|
| 369 |
+
} catch (error) {
|
| 370 |
+
console.error(`Error reading context file: ${error}`);
|
| 371 |
+
process.exit(1);
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
// Extract semantic search calls
|
| 375 |
+
const searches = extractSemanticSearchCalls(contextData);
|
| 376 |
+
|
| 377 |
+
if (searches.length === 0) {
|
| 378 |
+
console.error("No semantic search calls found in context");
|
| 379 |
+
process.exit(1);
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
// Evaluate with LLM
|
| 383 |
+
const evaluation = await evaluateWithLLM(
|
| 384 |
+
searches,
|
| 385 |
+
args.intent,
|
| 386 |
+
args.expectedFileTypes,
|
| 387 |
+
args.shouldAvoid,
|
| 388 |
+
);
|
| 389 |
+
|
| 390 |
+
// Output results
|
| 391 |
+
console.log(formatEvaluation(evaluation));
|
| 392 |
+
|
| 393 |
+
// Exit with appropriate code
|
| 394 |
+
process.exit(evaluation.overall.passed ? 0 : 1);
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
main().catch((error) => {
|
| 398 |
+
console.error("Unexpected error:", error);
|
| 399 |
+
process.exit(1);
|
| 400 |
+
});
|
benchmarks/evals/semantic_search_quality/run_eval.sh
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
set -e
|
| 4 |
+
|
| 5 |
+
CONTEXT_FILE="$1"
|
| 6 |
+
if [ -z "$CONTEXT_FILE" ]; then
|
| 7 |
+
echo "Usage: $0 <context_file>"
|
| 8 |
+
exit 1
|
| 9 |
+
fi
|
| 10 |
+
|
| 11 |
+
echo "=========================================="
|
| 12 |
+
echo "Running Semantic Search Quality Eval"
|
| 13 |
+
echo "=========================================="
|
| 14 |
+
echo ""
|
| 15 |
+
|
| 16 |
+
# Step 1: Check if sem_search was used
|
| 17 |
+
echo "Step 1: Checking for sem_search tool usage..."
|
| 18 |
+
if cat "$CONTEXT_FILE" | jq -e '[.messages[]?.tool_calls[]? | select(.function.name == "sem_search")] | any' > /dev/null 2>&1; then
|
| 19 |
+
echo "✓ sem_search tool found in context"
|
| 20 |
+
else
|
| 21 |
+
echo "✗ FAILED: sem_search tool not used"
|
| 22 |
+
exit 1
|
| 23 |
+
fi
|
| 24 |
+
|
| 25 |
+
# Step 2: Extract intent and queries
|
| 26 |
+
echo ""
|
| 27 |
+
echo "Step 2: Extracting task intent and queries..."
|
| 28 |
+
INTENT=$(cat "$CONTEXT_FILE" | jq -r '.task.intent // "implementation"')
|
| 29 |
+
echo " Intent: $INTENT"
|
| 30 |
+
|
| 31 |
+
# Extract expected file types and avoid list from task
|
| 32 |
+
EXPECTED_TYPES=$(cat "$CONTEXT_FILE" | jq -r '.task.expected_file_types // [] | join(",")')
|
| 33 |
+
SHOULD_AVOID=$(cat "$CONTEXT_FILE" | jq -r '.task.should_avoid // [] | join(",")')
|
| 34 |
+
|
| 35 |
+
# Extract queries from tool calls
|
| 36 |
+
QUERIES=$(cat "$CONTEXT_FILE" | jq -r '[.messages[]?.tool_calls[]? | select(.function.name == "sem_search") | .function.arguments] | .[0]' 2>/dev/null || echo '{}')
|
| 37 |
+
echo " Queries extracted: $(echo "$QUERIES" | jq -r '.queries | length // 0') query pairs"
|
| 38 |
+
|
| 39 |
+
# Step 3: Check authentication
|
| 40 |
+
echo ""
|
| 41 |
+
echo "Step 3: Checking Google Cloud authentication..."
|
| 42 |
+
if gcloud auth application-default print-access-token > /dev/null 2>&1; then
|
| 43 |
+
echo "✓ Google Cloud authentication available"
|
| 44 |
+
RUN_LLM_JUDGE=true
|
| 45 |
+
elif [ -n "$GOOGLE_APPLICATION_CREDENTIALS" ] && [ -f "$GOOGLE_APPLICATION_CREDENTIALS" ]; then
|
| 46 |
+
echo "✓ Service account credentials found"
|
| 47 |
+
RUN_LLM_JUDGE=true
|
| 48 |
+
else
|
| 49 |
+
echo "⚠ Google Cloud authentication not configured"
|
| 50 |
+
echo " Skipping LLM judge evaluation"
|
| 51 |
+
echo " To enable: run 'gcloud auth application-default login'"
|
| 52 |
+
RUN_LLM_JUDGE=false
|
| 53 |
+
fi
|
| 54 |
+
|
| 55 |
+
# Step 4: Run LLM judge (conditional)
|
| 56 |
+
echo ""
|
| 57 |
+
if [ "$RUN_LLM_JUDGE" = true ]; then
|
| 58 |
+
echo "Step 4: Running LLM judge evaluation..."
|
| 59 |
+
|
| 60 |
+
# Build command with parameters (use empty strings if not set)
|
| 61 |
+
if [ -z "$EXPECTED_TYPES" ]; then
|
| 62 |
+
EXPECTED_TYPES=""
|
| 63 |
+
fi
|
| 64 |
+
|
| 65 |
+
if [ -z "$SHOULD_AVOID" ]; then
|
| 66 |
+
SHOULD_AVOID=""
|
| 67 |
+
fi
|
| 68 |
+
|
| 69 |
+
CMD="npx tsx llm_judge.ts --context \"$CONTEXT_FILE\" --intent \"$INTENT\" --expected-file-types \"$EXPECTED_TYPES\" --should-avoid \"$SHOULD_AVOID\""
|
| 70 |
+
|
| 71 |
+
eval $CMD
|
| 72 |
+
|
| 73 |
+
if [ $? -eq 0 ]; then
|
| 74 |
+
echo ""
|
| 75 |
+
echo "=========================================="
|
| 76 |
+
echo "✓ EVALUATION PASSED"
|
| 77 |
+
echo "=========================================="
|
| 78 |
+
exit 0
|
| 79 |
+
else
|
| 80 |
+
echo ""
|
| 81 |
+
echo "=========================================="
|
| 82 |
+
echo "✗ EVALUATION FAILED"
|
| 83 |
+
echo "=========================================="
|
| 84 |
+
exit 1
|
| 85 |
+
fi
|
| 86 |
+
else
|
| 87 |
+
echo "Step 4: Skipped (authentication required)"
|
| 88 |
+
echo ""
|
| 89 |
+
echo "=========================================="
|
| 90 |
+
echo "✓ VALIDATION PASSED (LLM judge skipped)"
|
| 91 |
+
echo "=========================================="
|
| 92 |
+
echo ""
|
| 93 |
+
echo "Note: Full evaluation requires Google Cloud auth"
|
| 94 |
+
exit 0
|
| 95 |
+
fi
|
benchmarks/evals/semantic_search_quality/test_context.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"messages": [
|
| 3 |
+
{
|
| 4 |
+
"role": "user",
|
| 5 |
+
"content": "Find the retry mechanism with exponential backoff implementation"
|
| 6 |
+
},
|
| 7 |
+
{
|
| 8 |
+
"role": "assistant",
|
| 9 |
+
"content": "I'll search for the retry mechanism implementation.",
|
| 10 |
+
"tool_calls": [
|
| 11 |
+
{
|
| 12 |
+
"id": "call_1",
|
| 13 |
+
"type": "function",
|
| 14 |
+
"function": {
|
| 15 |
+
"name": "sem_search",
|
| 16 |
+
"arguments": "{\"queries\":[{\"query\":\"retry mechanism with exponential backoff for API calls handling network failures\",\"use_case\":\"I need to find the implementation code for retry logic with backoff, not documentation or test files\"}]}"
|
| 17 |
+
}
|
| 18 |
+
}
|
| 19 |
+
]
|
| 20 |
+
}
|
| 21 |
+
]
|
| 22 |
+
}
|
benchmarks/evals/semantic_search_quality/test_queries.ts
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env tsx
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Manual Query Tester for Semantic Search
|
| 5 |
+
*
|
| 6 |
+
* This script helps developers test and improve their semantic search queries
|
| 7 |
+
* before committing them to the codebase using Gemini 3 Pro via Vertex AI.
|
| 8 |
+
*/
|
| 9 |
+
|
| 10 |
+
import { vertex } from "@ai-sdk/google-vertex";
|
| 11 |
+
import { generateObject } from "ai";
|
| 12 |
+
import { z } from "zod";
|
| 13 |
+
import * as readline from "readline";
|
| 14 |
+
|
| 15 |
+
const QueryEvaluationSchema = z.object({
|
| 16 |
+
embedding_query: z.object({
|
| 17 |
+
score: z.number().min(0).max(10),
|
| 18 |
+
strengths: z.array(z.string()),
|
| 19 |
+
weaknesses: z.array(z.string()),
|
| 20 |
+
suggestions: z.array(z.string()),
|
| 21 |
+
}),
|
| 22 |
+
reranking_query: z.object({
|
| 23 |
+
score: z.number().min(0).max(10),
|
| 24 |
+
strengths: z.array(z.string()),
|
| 25 |
+
weaknesses: z.array(z.string()),
|
| 26 |
+
suggestions: z.array(z.string()),
|
| 27 |
+
}),
|
| 28 |
+
differentiation: z.object({
|
| 29 |
+
are_different: z.boolean(),
|
| 30 |
+
explanation: z.string(),
|
| 31 |
+
suggestion: z.string().optional(),
|
| 32 |
+
}),
|
| 33 |
+
overall_recommendation: z.string(),
|
| 34 |
+
});
|
| 35 |
+
|
| 36 |
+
async function evaluateQueries(
|
| 37 |
+
embeddingQuery: string,
|
| 38 |
+
rerankingQuery: string,
|
| 39 |
+
): Promise<z.infer<typeof QueryEvaluationSchema>> {
|
| 40 |
+
const prompt = `You are an expert in semantic code search. Evaluate these two queries:
|
| 41 |
+
|
| 42 |
+
**Embedding Query (for vector search):**
|
| 43 |
+
${embeddingQuery}
|
| 44 |
+
|
| 45 |
+
**Reranking Query (for result filtering):**
|
| 46 |
+
${rerankingQuery}
|
| 47 |
+
|
| 48 |
+
Provide detailed, constructive feedback on:
|
| 49 |
+
1. The quality of each query
|
| 50 |
+
2. Whether they are sufficiently different
|
| 51 |
+
3. How to improve them
|
| 52 |
+
|
| 53 |
+
Scoring (0-10):
|
| 54 |
+
- 9-10: Excellent
|
| 55 |
+
- 7-8: Good
|
| 56 |
+
- 5-6: Adequate
|
| 57 |
+
- 3-4: Poor
|
| 58 |
+
- 0-2: Failing
|
| 59 |
+
|
| 60 |
+
Focus on practical, actionable suggestions.`;
|
| 61 |
+
|
| 62 |
+
// @ts-ignore - Type instantiation depth issue with complex Zod schemas
|
| 63 |
+
const result = await generateObject({
|
| 64 |
+
model: vertex("gemini-3-pro-preview"),
|
| 65 |
+
schema: QueryEvaluationSchema,
|
| 66 |
+
prompt,
|
| 67 |
+
temperature: 0.3,
|
| 68 |
+
});
|
| 69 |
+
return result.object as z.infer<typeof QueryEvaluationSchema>;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
function formatEvaluation(evaluation: z.infer<typeof QueryEvaluationSchema>): string {
|
| 73 |
+
const lines: string[] = [];
|
| 74 |
+
|
| 75 |
+
lines.push("\n╔═══════════════════════════════════════════════════════════╗");
|
| 76 |
+
lines.push("║ SEMANTIC SEARCH QUERY EVALUATION ║");
|
| 77 |
+
lines.push("╚═══════════════════════════════════════════════════════════╝\n");
|
| 78 |
+
|
| 79 |
+
// Embedding Query
|
| 80 |
+
lines.push("📊 EMBEDDING QUERY ANALYSIS");
|
| 81 |
+
lines.push(` Score: ${evaluation.embedding_query.score}/10 ${getScoreEmoji(evaluation.embedding_query.score)}\n`);
|
| 82 |
+
|
| 83 |
+
if (evaluation.embedding_query.strengths.length > 0) {
|
| 84 |
+
lines.push(" ✅ Strengths:");
|
| 85 |
+
for (const strength of evaluation.embedding_query.strengths) {
|
| 86 |
+
lines.push(` • ${strength}`);
|
| 87 |
+
}
|
| 88 |
+
lines.push("");
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
if (evaluation.embedding_query.weaknesses.length > 0) {
|
| 92 |
+
lines.push(" ⚠️ Weaknesses:");
|
| 93 |
+
for (const weakness of evaluation.embedding_query.weaknesses) {
|
| 94 |
+
lines.push(` • ${weakness}`);
|
| 95 |
+
}
|
| 96 |
+
lines.push("");
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
if (evaluation.embedding_query.suggestions.length > 0) {
|
| 100 |
+
lines.push(" 💡 Suggestions:");
|
| 101 |
+
for (const suggestion of evaluation.embedding_query.suggestions) {
|
| 102 |
+
lines.push(` • ${suggestion}`);
|
| 103 |
+
}
|
| 104 |
+
lines.push("");
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
// Reranking Query
|
| 108 |
+
lines.push("📊 RERANKING QUERY ANALYSIS");
|
| 109 |
+
lines.push(` Score: ${evaluation.reranking_query.score}/10 ${getScoreEmoji(evaluation.reranking_query.score)}\n`);
|
| 110 |
+
|
| 111 |
+
if (evaluation.reranking_query.strengths.length > 0) {
|
| 112 |
+
lines.push(" ✅ Strengths:");
|
| 113 |
+
for (const strength of evaluation.reranking_query.strengths) {
|
| 114 |
+
lines.push(` • ${strength}`);
|
| 115 |
+
}
|
| 116 |
+
lines.push("");
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
if (evaluation.reranking_query.weaknesses.length > 0) {
|
| 120 |
+
lines.push(" ⚠️ Weaknesses:");
|
| 121 |
+
for (const weakness of evaluation.reranking_query.weaknesses) {
|
| 122 |
+
lines.push(` • ${weakness}`);
|
| 123 |
+
}
|
| 124 |
+
lines.push("");
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
if (evaluation.reranking_query.suggestions.length > 0) {
|
| 128 |
+
lines.push(" 💡 Suggestions:");
|
| 129 |
+
for (const suggestion of evaluation.reranking_query.suggestions) {
|
| 130 |
+
lines.push(` • ${suggestion}`);
|
| 131 |
+
}
|
| 132 |
+
lines.push("");
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
// Differentiation
|
| 136 |
+
lines.push("🔄 QUERY DIFFERENTIATION");
|
| 137 |
+
lines.push(` ${evaluation.differentiation.are_different ? "✅" : "❌"} Queries are ${evaluation.differentiation.are_different ? "sufficiently different" : "too similar"}`);
|
| 138 |
+
lines.push(` ${evaluation.differentiation.explanation}\n`);
|
| 139 |
+
|
| 140 |
+
if (evaluation.differentiation.suggestion) {
|
| 141 |
+
lines.push(` 💡 ${evaluation.differentiation.suggestion}\n`);
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
// Overall
|
| 145 |
+
lines.push("🎯 OVERALL RECOMMENDATION");
|
| 146 |
+
lines.push(` ${evaluation.overall_recommendation}\n`);
|
| 147 |
+
|
| 148 |
+
return lines.join("\n");
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
function getScoreEmoji(score: number): string {
|
| 152 |
+
if (score >= 9) return "🌟";
|
| 153 |
+
if (score >= 7) return "👍";
|
| 154 |
+
if (score >= 5) return "👌";
|
| 155 |
+
if (score >= 3) return "⚠️";
|
| 156 |
+
return "❌";
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
async function promptUser(question: string): Promise<string> {
|
| 160 |
+
const rl = readline.createInterface({
|
| 161 |
+
input: process.stdin,
|
| 162 |
+
output: process.stdout,
|
| 163 |
+
});
|
| 164 |
+
|
| 165 |
+
return new Promise((resolve) => {
|
| 166 |
+
rl.question(question, (answer) => {
|
| 167 |
+
rl.close();
|
| 168 |
+
resolve(answer.trim());
|
| 169 |
+
});
|
| 170 |
+
});
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
async function main() {
|
| 174 |
+
console.log("\n╔═══════════════════════════════════════════════════════════╗");
|
| 175 |
+
console.log("║ SEMANTIC SEARCH QUERY TESTER ║");
|
| 176 |
+
console.log("║ Test and improve your queries with AI feedback ║");
|
| 177 |
+
console.log("╚═══════════════════════════════════════════════════════════╝\n");
|
| 178 |
+
|
| 179 |
+
// Get queries from user
|
| 180 |
+
const embeddingQuery = await promptUser(
|
| 181 |
+
"Enter your embedding query (for vector search):\n> ",
|
| 182 |
+
);
|
| 183 |
+
|
| 184 |
+
if (!embeddingQuery) {
|
| 185 |
+
console.error("Embedding query is required");
|
| 186 |
+
process.exit(1);
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
console.log("");
|
| 190 |
+
const rerankingQuery = await promptUser(
|
| 191 |
+
"Enter your reranking query (for result filtering):\n> ",
|
| 192 |
+
);
|
| 193 |
+
|
| 194 |
+
if (!rerankingQuery) {
|
| 195 |
+
console.error("Reranking query is required");
|
| 196 |
+
process.exit(1);
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
console.log("\n🔄 Evaluating your queries...\n");
|
| 200 |
+
|
| 201 |
+
try {
|
| 202 |
+
const evaluation = await evaluateQueries(embeddingQuery, rerankingQuery);
|
| 203 |
+
console.log(formatEvaluation(evaluation));
|
| 204 |
+
} catch (error) {
|
| 205 |
+
console.error("Error evaluating queries:", error);
|
| 206 |
+
process.exit(1);
|
| 207 |
+
}
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
// Check if running in interactive mode or with args
|
| 211 |
+
const args = process.argv.slice(2);
|
| 212 |
+
if (args.length >= 2) {
|
| 213 |
+
// Non-interactive mode
|
| 214 |
+
const embeddingQuery = args[0];
|
| 215 |
+
const rerankingQuery = args[1];
|
| 216 |
+
|
| 217 |
+
console.log("\n🔄 Evaluating your queries...\n");
|
| 218 |
+
evaluateQueries(embeddingQuery!, rerankingQuery!)
|
| 219 |
+
.then((evaluation) => {
|
| 220 |
+
console.log(formatEvaluation(evaluation));
|
| 221 |
+
})
|
| 222 |
+
.catch((error) => {
|
| 223 |
+
console.error("Error evaluating queries:", error);
|
| 224 |
+
process.exit(1);
|
| 225 |
+
});
|
| 226 |
+
} else {
|
| 227 |
+
// Interactive mode
|
| 228 |
+
main().catch((error) => {
|
| 229 |
+
console.error("Unexpected error:", error);
|
| 230 |
+
process.exit(1);
|
| 231 |
+
});
|
| 232 |
+
}
|
benchmarks/evals/suggest/task.yml
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
run:
|
| 2 |
+
- mkdir -p test_dir
|
| 3 |
+
- cd test_dir && touch file1.txt file2.py README.md
|
| 4 |
+
- cd test_dir && forgee --provider {{provider}} --model {{model}} suggest "{{prompt}}"
|
| 5 |
+
|
| 6 |
+
parallelism: 2
|
| 7 |
+
timeout: 60
|
| 8 |
+
early_exit: true
|
| 9 |
+
|
| 10 |
+
validations:
|
| 11 |
+
- name: "Should generate output (non-empty)"
|
| 12 |
+
type: shell
|
| 13 |
+
command: "test $(wc -c | awk '{print $1}') -gt 0"
|
| 14 |
+
|
| 15 |
+
- name: "Should NOT contain markdown code blocks"
|
| 16 |
+
type: shell
|
| 17 |
+
command: "! grep -qE '^```'"
|
| 18 |
+
|
| 19 |
+
- name: "Should NOT contain JSON formatting tags"
|
| 20 |
+
type: shell
|
| 21 |
+
command: "! grep -qE '\\{\\s*\"command\"\\s*:'"
|
| 22 |
+
|
| 23 |
+
sources:
|
| 24 |
+
- value:
|
| 25 |
+
- provider: "anthropic"
|
| 26 |
+
model: "claude-sonnet-4-5-20250929"
|
| 27 |
+
- provider: "open_router"
|
| 28 |
+
model: "z-ai/glm-4.7"
|
| 29 |
+
- provider: "open_router"
|
| 30 |
+
model: "minimax/minimax-m2.1"
|
| 31 |
+
|
| 32 |
+
- value:
|
| 33 |
+
- prompt: "list all files"
|
| 34 |
+
- prompt: "find all python files"
|
| 35 |
+
- prompt: "show disk usage"
|
| 36 |
+
- prompt: "git status"
|
| 37 |
+
- prompt: "count lines in all files"
|
| 38 |
+
- prompt: "search for TODO in code"
|
| 39 |
+
- prompt: "check running processes"
|
| 40 |
+
- prompt: "compress this directory"
|
| 41 |
+
- prompt: "show last 20 commits"
|
| 42 |
+
- prompt: "find files larger than 1MB"
|
| 43 |
+
- prompt: "kill process on port 3000"
|
| 44 |
+
- prompt: "show system memory usage"
|
| 45 |
+
- prompt: "download file from url"
|
| 46 |
+
- prompt: "create git branch"
|
| 47 |
+
- prompt: "watch log file changes"
|
benchmarks/model.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export type Task = {
|
| 2 |
+
before_run: Array<string>;
|
| 3 |
+
run: string | Array<string>;
|
| 4 |
+
parallelism?: number;
|
| 5 |
+
timeout?: number;
|
| 6 |
+
early_exit?: boolean;
|
| 7 |
+
validations?: Array<Validation>;
|
| 8 |
+
sources: Array<Source>;
|
| 9 |
+
};
|
| 10 |
+
|
| 11 |
+
export type Validation =
|
| 12 |
+
| {
|
| 13 |
+
name: string;
|
| 14 |
+
type: "regex";
|
| 15 |
+
regex: string;
|
| 16 |
+
}
|
| 17 |
+
| {
|
| 18 |
+
name: string;
|
| 19 |
+
type: "shell";
|
| 20 |
+
command: string;
|
| 21 |
+
exit_code?: number;
|
| 22 |
+
}
|
| 23 |
+
| {
|
| 24 |
+
name: string;
|
| 25 |
+
type: "llm";
|
| 26 |
+
evaluator: string;
|
| 27 |
+
criteria: Record<string, any>;
|
| 28 |
+
};
|
| 29 |
+
|
| 30 |
+
export type Source = { csv: string } | { cmd: string } | { value: Record<string, string>[] };
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
export enum TaskStatus {
|
| 34 |
+
Passed = "passed",
|
| 35 |
+
ValidationFailed = "validation_failed",
|
| 36 |
+
Timeout = "timeout",
|
| 37 |
+
Failed = "failed",
|
| 38 |
+
}
|
benchmarks/parse.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import yargs from "yargs";
|
| 2 |
+
import { hideBin } from "yargs/helpers";
|
| 3 |
+
import path from "path";
|
| 4 |
+
|
| 5 |
+
export type CliArgs = {
|
| 6 |
+
evalName: string;
|
| 7 |
+
evalDir: string;
|
| 8 |
+
taskFile: string;
|
| 9 |
+
};
|
| 10 |
+
|
| 11 |
+
/**
|
| 12 |
+
* Parses command line arguments and resolves paths
|
| 13 |
+
*/
|
| 14 |
+
export async function parseCliArgs(dirname: string): Promise<CliArgs> {
|
| 15 |
+
const argv = await yargs(hideBin(process.argv))
|
| 16 |
+
.usage("Usage: $0 <eval-name> [options]")
|
| 17 |
+
.command("$0 <eval-name>", "Run an evaluation")
|
| 18 |
+
.positional("eval-name", {
|
| 19 |
+
describe: "Name of the evaluation to run",
|
| 20 |
+
type: "string",
|
| 21 |
+
})
|
| 22 |
+
.help()
|
| 23 |
+
.alias("h", "help")
|
| 24 |
+
.parseAsync();
|
| 25 |
+
|
| 26 |
+
const evalName = argv["eval-name"];
|
| 27 |
+
|
| 28 |
+
// Ensure evalName is provided
|
| 29 |
+
if (!evalName) {
|
| 30 |
+
throw new Error("eval-name is required");
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
// Support both directory path and direct task.yml path
|
| 34 |
+
let evalDir: string;
|
| 35 |
+
let taskFile: string;
|
| 36 |
+
|
| 37 |
+
if (evalName.endsWith("task.yml") || evalName.endsWith(".yml") || evalName.endsWith(".yaml")) {
|
| 38 |
+
// Direct path to task file
|
| 39 |
+
taskFile = path.isAbsolute(evalName) ? evalName : path.join(dirname, evalName);
|
| 40 |
+
evalDir = path.dirname(taskFile);
|
| 41 |
+
} else {
|
| 42 |
+
// Directory path (original behavior)
|
| 43 |
+
evalDir = path.join(dirname, evalName);
|
| 44 |
+
taskFile = path.join(evalDir, "task.yml");
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
return {
|
| 48 |
+
evalName,
|
| 49 |
+
evalDir,
|
| 50 |
+
taskFile,
|
| 51 |
+
};
|
| 52 |
+
}
|
benchmarks/task-executor.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as fs from "fs";
|
| 2 |
+
import * as path from "path";
|
| 3 |
+
import { spawn } from "child_process";
|
| 4 |
+
import stripAnsi from "strip-ansi";
|
| 5 |
+
import type { Validation, Task } from "./model.js";
|
| 6 |
+
import { runValidations, allValidationsPassed } from "./verification.js";
|
| 7 |
+
import { formatTimestamp } from "./utils.js";
|
| 8 |
+
|
| 9 |
+
export type TaskExecutionResult = {
|
| 10 |
+
index: number;
|
| 11 |
+
command: string;
|
| 12 |
+
duration: number;
|
| 13 |
+
output?: string;
|
| 14 |
+
error?: string;
|
| 15 |
+
isTimeout: boolean;
|
| 16 |
+
earlyExit?: boolean;
|
| 17 |
+
};
|
| 18 |
+
|
| 19 |
+
/**
|
| 20 |
+
* Executes a single task command and returns the result
|
| 21 |
+
*/
|
| 22 |
+
export async function executeTask(
|
| 23 |
+
command: string,
|
| 24 |
+
index: number,
|
| 25 |
+
logFile: string,
|
| 26 |
+
cwd: string,
|
| 27 |
+
task: Task,
|
| 28 |
+
context?: Record<string, string>,
|
| 29 |
+
append: boolean = false,
|
| 30 |
+
): Promise<TaskExecutionResult> {
|
| 31 |
+
const startTime = Date.now();
|
| 32 |
+
|
| 33 |
+
// Create log stream for this task (append if this is not the first command)
|
| 34 |
+
const logStream = fs.createWriteStream(logFile, {
|
| 35 |
+
flags: append ? "a" : "w",
|
| 36 |
+
});
|
| 37 |
+
|
| 38 |
+
// Write command at the top of the log file
|
| 39 |
+
logStream.write(`Command: ${command}\n`);
|
| 40 |
+
logStream.write(`Started: ${formatTimestamp(new Date())}\n`);
|
| 41 |
+
logStream.write(`${"=".repeat(80)}\n\n`);
|
| 42 |
+
|
| 43 |
+
try {
|
| 44 |
+
// Track timeout state outside the promise
|
| 45 |
+
let timedOut = false;
|
| 46 |
+
let exitedEarly = false;
|
| 47 |
+
|
| 48 |
+
// Execute command and stream output to log file
|
| 49 |
+
const output = await new Promise<string>((resolve, reject) => {
|
| 50 |
+
const child = spawn(command, {
|
| 51 |
+
shell: true,
|
| 52 |
+
cwd: cwd,
|
| 53 |
+
stdio: ["ignore", "pipe", "pipe"],
|
| 54 |
+
});
|
| 55 |
+
|
| 56 |
+
let stdout = "";
|
| 57 |
+
let stderr = "";
|
| 58 |
+
let timeoutId: NodeJS.Timeout | null = null;
|
| 59 |
+
|
| 60 |
+
// Helper function to check validations after each write
|
| 61 |
+
const checkValidations = async () => {
|
| 62 |
+
if (exitedEarly || timedOut) return;
|
| 63 |
+
|
| 64 |
+
if (
|
| 65 |
+
task.early_exit &&
|
| 66 |
+
task.validations &&
|
| 67 |
+
task.validations.length > 0
|
| 68 |
+
) {
|
| 69 |
+
const currentOutput = stdout + stderr;
|
| 70 |
+
if (currentOutput) {
|
| 71 |
+
const results = await runValidations(
|
| 72 |
+
currentOutput,
|
| 73 |
+
task.validations,
|
| 74 |
+
context,
|
| 75 |
+
);
|
| 76 |
+
if (allValidationsPassed(results)) {
|
| 77 |
+
exitedEarly = true;
|
| 78 |
+
if (timeoutId) clearTimeout(timeoutId);
|
| 79 |
+
if (logStream.writable) {
|
| 80 |
+
logStream.write(
|
| 81 |
+
`\n${"=".repeat(80)}\nEarly exit: All validations passed\nKilling process...\n`,
|
| 82 |
+
() => {
|
| 83 |
+
logStream.end();
|
| 84 |
+
},
|
| 85 |
+
);
|
| 86 |
+
}
|
| 87 |
+
child.kill("SIGTERM");
|
| 88 |
+
resolve(currentOutput);
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
}
|
| 92 |
+
};
|
| 93 |
+
|
| 94 |
+
// Set up timeout if configured
|
| 95 |
+
if (task.timeout) {
|
| 96 |
+
timeoutId = setTimeout(() => {
|
| 97 |
+
timedOut = true;
|
| 98 |
+
if (logStream.writable) {
|
| 99 |
+
logStream.write(`\n${"=".repeat(80)}\n`);
|
| 100 |
+
logStream.write(`Timeout: ${task.timeout}s exceeded\n`);
|
| 101 |
+
logStream.write(`Killing process...\n`);
|
| 102 |
+
logStream.end();
|
| 103 |
+
}
|
| 104 |
+
child.kill("SIGKILL");
|
| 105 |
+
// Resolve with captured output so far
|
| 106 |
+
resolve(stdout + stderr);
|
| 107 |
+
}, task.timeout * 1000);
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
// Stream stdout to both log file and capture for validation
|
| 111 |
+
child.stdout?.on("data", (data) => {
|
| 112 |
+
const text = data.toString();
|
| 113 |
+
stdout += text;
|
| 114 |
+
if (logStream.writable) {
|
| 115 |
+
logStream.write(stripAnsi(text));
|
| 116 |
+
}
|
| 117 |
+
checkValidations();
|
| 118 |
+
});
|
| 119 |
+
|
| 120 |
+
// Stream stderr to both log file and capture for validation
|
| 121 |
+
child.stderr?.on("data", (data) => {
|
| 122 |
+
const text = data.toString();
|
| 123 |
+
stderr += text;
|
| 124 |
+
if (logStream.writable) {
|
| 125 |
+
logStream.write(stripAnsi(text));
|
| 126 |
+
}
|
| 127 |
+
checkValidations();
|
| 128 |
+
});
|
| 129 |
+
|
| 130 |
+
child.on("close", (code) => {
|
| 131 |
+
if (timeoutId) clearTimeout(timeoutId);
|
| 132 |
+
|
| 133 |
+
// Don't log or resolve if already timed out or exited early
|
| 134 |
+
if (timedOut || exitedEarly) return;
|
| 135 |
+
|
| 136 |
+
logStream.write(`\n${"=".repeat(80)}\n`);
|
| 137 |
+
logStream.write(`Finished: ${formatTimestamp(new Date())}\n`);
|
| 138 |
+
logStream.write(`Exit Code: ${code}\n`);
|
| 139 |
+
logStream.end();
|
| 140 |
+
|
| 141 |
+
if (code === 0) {
|
| 142 |
+
resolve(stdout + stderr);
|
| 143 |
+
} else {
|
| 144 |
+
reject(new Error(`Command failed with exit code ${code}`));
|
| 145 |
+
}
|
| 146 |
+
});
|
| 147 |
+
|
| 148 |
+
child.on("error", (err) => {
|
| 149 |
+
if (timeoutId) clearTimeout(timeoutId);
|
| 150 |
+
|
| 151 |
+
// Don't log if already timed out or exited early
|
| 152 |
+
if (timedOut || exitedEarly) return;
|
| 153 |
+
|
| 154 |
+
logStream.write(`\nError: ${err.message}\n`);
|
| 155 |
+
logStream.end();
|
| 156 |
+
reject(err);
|
| 157 |
+
});
|
| 158 |
+
});
|
| 159 |
+
|
| 160 |
+
const duration = Date.now() - startTime;
|
| 161 |
+
|
| 162 |
+
return {
|
| 163 |
+
index,
|
| 164 |
+
command,
|
| 165 |
+
duration,
|
| 166 |
+
output,
|
| 167 |
+
isTimeout: timedOut,
|
| 168 |
+
earlyExit: exitedEarly,
|
| 169 |
+
};
|
| 170 |
+
} catch (error) {
|
| 171 |
+
const duration = Date.now() - startTime;
|
| 172 |
+
const errorMessage =
|
| 173 |
+
error instanceof Error ? error.message : "Command failed";
|
| 174 |
+
|
| 175 |
+
return {
|
| 176 |
+
index,
|
| 177 |
+
command,
|
| 178 |
+
duration,
|
| 179 |
+
error: errorMessage,
|
| 180 |
+
isTimeout: false,
|
| 181 |
+
earlyExit: false,
|
| 182 |
+
};
|
| 183 |
+
}
|
| 184 |
+
}
|
benchmarks/tsconfig.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
// Visit https://aka.ms/tsconfig to read more about this file
|
| 3 |
+
"compilerOptions": {
|
| 4 |
+
// File Layout
|
| 5 |
+
"rootDir": ".",
|
| 6 |
+
"outDir": "dist",
|
| 7 |
+
|
| 8 |
+
// Environment Settings
|
| 9 |
+
// See also https://aka.ms/tsconfig/module
|
| 10 |
+
"module": "nodenext",
|
| 11 |
+
"target": "esnext",
|
| 12 |
+
"types": [],
|
| 13 |
+
// For nodejs:
|
| 14 |
+
// "lib": ["esnext"],
|
| 15 |
+
// "types": ["node"],
|
| 16 |
+
// and npm install -D @types/node
|
| 17 |
+
|
| 18 |
+
// Other Outputs
|
| 19 |
+
"sourceMap": true,
|
| 20 |
+
"declaration": true,
|
| 21 |
+
"declarationMap": true,
|
| 22 |
+
|
| 23 |
+
// Stricter Typechecking Options
|
| 24 |
+
"noUncheckedIndexedAccess": true,
|
| 25 |
+
"exactOptionalPropertyTypes": true,
|
| 26 |
+
|
| 27 |
+
// Style Options
|
| 28 |
+
// "noImplicitReturns": true,
|
| 29 |
+
// "noImplicitOverride": true,
|
| 30 |
+
// "noUnusedLocals": true,
|
| 31 |
+
// "noUnusedParameters": true,
|
| 32 |
+
// "noFallthroughCasesInSwitch": true,
|
| 33 |
+
// "noPropertyAccessFromIndexSignature": true,
|
| 34 |
+
|
| 35 |
+
// Recommended Options
|
| 36 |
+
"strict": true,
|
| 37 |
+
"jsx": "react-jsx",
|
| 38 |
+
"verbatimModuleSyntax": true,
|
| 39 |
+
"isolatedModules": true,
|
| 40 |
+
"noUncheckedSideEffectImports": true,
|
| 41 |
+
"moduleDetection": "force",
|
| 42 |
+
"skipLibCheck": true,
|
| 43 |
+
|
| 44 |
+
"esModuleInterop": true,
|
| 45 |
+
"resolveJsonModule": true,
|
| 46 |
+
}
|
| 47 |
+
}
|