diff --git a/.gitignore b/.gitignore index 13869c7..2be296d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,9 @@ docs/.obsidian/ docs/obsidian/ /wiki/ /wifi/ +/website/ +eval/results/ +eval/traces/ +eval/training/ +**/__pycache__/ +**/*.pyc diff --git a/Dockerfile.eval b/Dockerfile.eval new file mode 100644 index 0000000..e006f53 --- /dev/null +++ b/Dockerfile.eval @@ -0,0 +1,18 @@ +# Use the official Rust image as a builder +FROM rust:1.77 as builder +WORKDIR /usr/src/cinto +COPY . . +RUN cargo build --release + +# Run stage +FROM debian:bookworm-slim +# Install basic dependencies for eval (git, nodejs, etc if needed by fixtures) +RUN apt-get update && apt-get install -y git ca-certificates curl build-essential && rm -rf /var/lib/apt/lists/* +# Install rust in the runtime image because we run `cargo test` in eval jobs! +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +ENV PATH="/root/.cargo/bin:${PATH}" + +COPY --from=builder /usr/src/cinto/target/release/cinto /usr/local/bin/cinto + +WORKDIR /eval +ENTRYPOINT ["cinto", "batch", "--dangerously-auto-approve"] diff --git a/README.md b/README.md index a29c7d9..e0dced2 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ servers.** It gives local and open-weight models a focused workspace loop: read files, search code, propose edits, keep todos, inspect prompts, and stay inside explicit safety rails. -[Português](README_pt.md) · [Architecture notes](docs/architecture.md) +[Português](README_pt.md) · [Architecture notes](docs/architecture.md) · [CRP spec](docs/CRP_SPEC.md) ![Cinto TUI](docs/demo.png) @@ -30,6 +30,8 @@ Most coding agents hide the harness. Cinto keeps it visible. workspace root. - **Context management:** large tool outputs and older transcript history are compacted before they overwhelm the model context window. +- **Reasoning protocol draft:** CRP explores typed, auditable reasoning traces + for small and compressed coding agents. ## Install @@ -129,8 +131,8 @@ explicit `/v1/completions` endpoint for text-completion servers. | Server | Example endpoint | Recommended format | Notes | | --- | --- | --- | --- | -| LM Studio with `gpt-oss` | `http://127.0.0.1:1234` | `harmony` | Use the model id shown by LM Studio. | -| LM Studio with Qwen/Llama | `http://127.0.0.1:1234` | `openai-tools` | Set `thinking_effort = "none"`. | +| LM Studio with `gpt-oss` | `http://127.0.0.1:1234` | `harmony` | Default transport. CRP reasoning is enabled by default. | +| LM Studio with Qwen/Llama | `http://127.0.0.1:1234` | `openai-tools` | Set `thinking_effort = "none"`; use `no_think = true` for Qwen3-class models. | | Ollama | `http://127.0.0.1:11434` | `openai-tools` | Pull a tool-capable model such as `qwen2.5-coder:7b-instruct`. | Example Ollama flow: @@ -148,6 +150,7 @@ endpoint = "http://127.0.0.1:11434" model = "qwen2.5-coder:7b-instruct" format = "openai-tools" thinking_effort = "none" +no_think = false ``` ## TUI Workflow @@ -228,6 +231,8 @@ api_key_env = "" max_tokens = 4096 temperature = 0.2 thinking_effort = "medium" # none, low, medium, high +no_think = false # prepends no_think_prefix to system prompts when true +no_think_prefix = "/no_think" # set to "" for servers/templates that expect it stream = true stop = ["<|return|>", "<|call|>"] request_timeout_secs = 600 @@ -237,7 +242,12 @@ context_window = 8192 workspace = "/home/you/project" allow_shell = false require_edit_approval = true +reasoning_protocol = "crp" # crp or plain +crp_retry_budget = 3 +transport_retry_budget = 1 +empty_response_retry_budget = 1 max_tool_turns = 16 +max_model_rounds = 64 auto_context_compression = true context_compression_threshold = 80 context_compression_keep_recent = 18 @@ -249,16 +259,24 @@ developer_prompt = "Use concise reasoning, ask before destructive actions, and p When `api_key_env` is set, Cinto reads the secret from that environment variable and sends it as a bearer token. The TUI stores the variable name, not the secret. -## Supported Tool Formats +## Supported Tool Formats And Reasoning | Format | Tool-calling shape | Best fit | | --- | --- | --- | | `harmony` | Tool calls embedded in Harmony-style assistant text | `gpt-oss-20b`, `gpt-oss-120b`, and Harmony-compatible servers | | `openai-tools` | Native OpenAI-compatible `tools` and `tool_calls` fields | Qwen, Llama, Ollama, LM Studio, and other OpenAI-compatible chat servers | +`harness.reasoning_protocol = "crp"` is enabled by default and works with both +tool formats. It asks the model to return final answers as CRP slot traces while +leaving tool invocation to the selected transport. + If the model keeps requesting tools without answering, raise `max_tool_turns` or -ask for a narrower step. If the model returns neither text nor a tool call, -Cinto shows an `Empty Model Response` note with the active model and format. +ask for a narrower step. `max_model_rounds`, `crp_retry_budget`, +`transport_retry_budget`, and `empty_response_retry_budget` are tracked +separately so protocol repairs and transient request failures do not consume +tool-call budget. If the model returns neither text nor a tool call after the +empty-response retry budget is exhausted, Cinto shows an `Empty Model Response` +note with the active model and format. ## Development diff --git a/README_pt.md b/README_pt.md index 8087094..54ca5f8 100644 --- a/README_pt.md +++ b/README_pt.md @@ -1,6 +1,6 @@ # Cinto -[English](README.md) · [Notas de arquitetura](docs/architecture.md) +[English](README.md) · [Notas de arquitetura](docs/architecture.md) · [Spec CRP](docs/CRP_SPEC.md) ![Cinto TUI](docs/demo.png) @@ -26,6 +26,8 @@ A maioria dos agentes esconde o harness. O Cinto deixa a engrenagem visível. - **Contexto persistente:** suporte opcional a `AGENTS.md` na raiz do projeto. - **Gestão de contexto:** resultados grandes e histórico antigo são compactados antes de estourar a janela do modelo. +- **Protocolo de raciocínio:** CRP explora traces tipados e auditáveis para + agentes de código pequenos ou comprimidos. ## Instalação @@ -126,8 +128,8 @@ texto. | Servidor | Endpoint | Formato recomendado | Observação | | --- | --- | --- | --- | -| LM Studio com `gpt-oss` | `http://127.0.0.1:1234` | `harmony` | Use o model id mostrado pelo LM Studio. | -| LM Studio com Qwen/Llama | `http://127.0.0.1:1234` | `openai-tools` | Use `thinking_effort = "none"`. | +| LM Studio com `gpt-oss` | `http://127.0.0.1:1234` | `harmony` | Transporte padrão. Reasoning CRP vem ligado por padrão. | +| LM Studio com Qwen/Llama | `http://127.0.0.1:1234` | `openai-tools` | Use `thinking_effort = "none"`; para modelos Qwen3, use `no_think = true`. | | Ollama | `http://127.0.0.1:11434` | `openai-tools` | Use um modelo com tools, como `qwen2.5-coder:7b-instruct`. | Exemplo com Ollama: @@ -145,6 +147,7 @@ endpoint = "http://127.0.0.1:11434" model = "qwen2.5-coder:7b-instruct" format = "openai-tools" thinking_effort = "none" +no_think = false ``` ## Fluxo no TUI @@ -222,6 +225,8 @@ api_key_env = "" max_tokens = 4096 temperature = 0.2 thinking_effort = "medium" # none, low, medium, high +no_think = false # prefixa no_think_prefix nos system prompts quando true +no_think_prefix = "/no_think" # use "" se o template/servidor exigir stream = true stop = ["<|return|>", "<|call|>"] request_timeout_secs = 600 @@ -231,6 +236,7 @@ context_window = 8192 workspace = "/home/you/project" allow_shell = false require_edit_approval = true +reasoning_protocol = "crp" # crp ou plain max_tool_turns = 16 auto_context_compression = true context_compression_threshold = 80 @@ -243,13 +249,17 @@ developer_prompt = "Use concise reasoning, ask before destructive actions, and p Quando `api_key_env` é definido, o Cinto lê o segredo dessa variável de ambiente e envia como bearer token. O TUI salva só o nome da variável, não o segredo. -## Formatos Suportados +## Formatos E Reasoning | Formato | Shape de tool-calling | Melhor uso | | --- | --- | --- | | `harmony` | Tool calls embutidos no texto Harmony | `gpt-oss-20b`, `gpt-oss-120b` e servidores compatíveis com Harmony | | `openai-tools` | Campos `tools` e `tool_calls` compatíveis com OpenAI | Qwen, Llama, Ollama, LM Studio e servidores chat compatíveis | +`harness.reasoning_protocol = "crp"` vem ligado por padrão e funciona com os +dois formatos de tool-call. Ele pede respostas finais como traces CRP, enquanto +a invocação de tools continua no transporte escolhido. + Se o modelo fica chamando tools sem responder, aumente `max_tool_turns` ou peça uma tarefa menor. Se o modelo não retorna texto nem tool call, o Cinto mostra um evento `Empty Model Response` com o modelo e formato ativos. diff --git a/docs/CRP_SPEC.md b/docs/CRP_SPEC.md new file mode 100644 index 0000000..c0ccf85 --- /dev/null +++ b/docs/CRP_SPEC.md @@ -0,0 +1,499 @@ +# CRP - Cinto Reasoning Protocol + +**Version:** 1.0-draft +**Status:** Draft / Under development +**Last updated:** 2026-05-01 + +## 1. Overview + +The Cinto Reasoning Protocol (CRP) is a structured output protocol for coding +agents. It replaces free-form reasoning ("chain of thought") with typed, +slot-based reasoning that can be parsed, validated, and audited +programmatically. + +CRP exists because free-form reasoning has known failure modes in small or +compressed language models: drift across long traces, unverifiable +confabulation, and structural inconsistency that prevents downstream tooling +from acting on the output reliably. CRP constrains reasoning into a predictable +shape that a harness can validate slot-by-slot, reject when malformed, and route +to specialized handlers when valid. + +CRP is the native reasoning protocol of the Cinto harness. Other harnesses or +tools may implement CRP independently; this specification is the reference. + +### 1.1 Design Goals + +- **Auditability.** Every reasoning step occupies a labeled slot with a known + type. Validation is mechanical, not subjective. +- **Recoverability.** When a slot fails validation, the harness can request a + targeted retry rather than discarding the whole trace. +- **Model-agnosticism.** CRP works with any model capable of emitting structured + text. No special tokenization required. +- **Hardware-friendliness.** Small models (1B-7B parameters) at aggressive + quantization (Q4 and below) can produce valid CRP output reliably when + fine-tuned on it. +- **Composability.** Slots can be processed independently. Multiple models or + tools can fill or audit different slots in the same trace. + +### 1.2 Non-goals + +CRP does not specify the underlying transport between harness and model (HTTP, +OpenAI-compatible API, local llama.cpp, etc). It does not mandate specific +tool-calling formats. It does not replace function calling or tool invocation; +CRP describes reasoning, tools handle action. + +In Cinto, CRP is a reasoning protocol rather than a transport format. It can be +enabled independently of whether the selected model adapter uses Harmony text +tool calls or OpenAI-compatible `tools` / `tool_calls`. + +CRP also does not authorize the agent to silently rewrite its own harness +configuration based on conversation. Harness configuration must remain explicit, +user-controlled, and reproducible. Project-local guidance may be loaded into +prompt context, but persistent configuration changes require an explicit user +action outside normal model output. + +## 2. Syntax + +CRP uses XML-like slot tags. Each slot is delimited by an opening tag and a +matching closing tag, with the slot name in uppercase ASCII. + +### 2.1 Basic Form + +```text + +content goes here + +``` + +Slot content may span multiple lines and may include any text content, including +code blocks, markdown formatting, lists, or special characters. Parsers must +treat content as opaque text until the closing tag. + +### 2.2 Parsing Rules + +- Slot names are case-sensitive and uppercase. `` is + valid; `` is not. +- Whitespace surrounding slot content is trimmed during parsing. +- Slots appear sequentially. Nested slots are not permitted in v1.0. +- Unknown slots are preserved by the parser and passed to user handlers, but + standard validation does not apply. +- Malformed slots (missing closing tag, mismatched names) trigger validation + failure. + +### 2.3 Reserved Characters + +Content may contain `<` and `>` freely. Parsers must use the closing tag pattern +`` to delimit, not raw bracket detection. This allows CRP slots to +contain HTML, XML, code, or any markup without escaping. + +The literal sequence `` (matching the active opening tag exactly) +cannot appear inside content. If a model needs to reference such a sequence in +content, it must use the slot type that does not include the conflicting tag, or +escape per slot-type-specific rules. + +## 3. Standard Slot Types (v1.0) + +The following slots are defined in v1.0. Implementations should support all of +them. Custom slots may be defined per-template; standard slots are the lingua +franca. + +### 3.1 Core Slots + +**TASK_INTERPRETATION** +A single paragraph rephrasing the user's request in the agent's own words. +Establishes shared understanding before action. + +- Type: `Paragraph` +- Required: yes (in most templates) +- Validation: non-empty, less than 500 words + +**ASSUMPTIONS** +Explicit list of assumptions the agent is making to disambiguate the request. + +- Type: `BulletList` +- Required: yes when ambiguity is detected +- Validation: each item is one sentence, falsifiable in principle + +**RELEVANT_FILES** +Files the agent considers necessary to read or modify. + +- Type: `BulletList` +- Required: yes for tasks involving existing code +- Validation: each path must exist in the workspace; non-existent paths trigger + retry + +**PROPOSED_APPROACH** +High-level strategy in 3-7 bullet points. Describes what will be done, not how. + +- Type: `BulletList` +- Required: yes +- Validation: each bullet is one sentence, no implementation details + +**RISKS** +Things that could go wrong with the proposed approach. + +- Type: `BulletList` +- Required: optional +- Validation: each item describes an observable failure mode + +**DELIVERABLE_SPEC** +Description of what success looks like, observably. + +- Type: `Paragraph` +- Required: yes for executable tasks +- Validation: includes at least one observable outcome (file changed, test + passes, output produced) + +### 3.2 Action Slots + +**FILE_EDITS** +Concrete file edits to apply. Wraps one or more `` sub-blocks. + +- Type: `Sequence` +- Required: when task involves file changes +- Validation: each `EDIT` references an existing file, syntactically valid for + target language when language is known + +**COMMAND_PROPOSALS** +Shell commands the agent proposes to execute. + +- Type: `BulletList` +- Required: when task involves command execution +- Validation: commands are non-destructive by default, destructive ones flagged + +**CHECKPOINTS** +Intermediate states the agent will verify before proceeding. + +- Type: `BulletList` +- Required: optional, recommended for multi-step tasks +- Validation: each checkpoint is observable + +### 3.3 Communication Slots + +**CLARIFICATION_REQUEST** +Question to the user when the agent cannot proceed without input. + +- Type: `Paragraph` +- Required: when ambiguity blocks progress +- Validation: phrased as direct question, single subject + +**FINAL_RESPONSE** +The user-facing answer or summary, after action is complete. + +- Type: `Paragraph` or `Markdown` +- Required: yes (terminates the trace) +- Validation: non-empty, addresses the original task + +## 4. Slot Type System + +Slot types define what valid content looks like. Validation is performed by the +harness; the model is informed of types via the system prompt. + +### 4.1 Primitive Types + +- `Paragraph` - one or more sentences of natural text, no list markers. +- `Markdown` - full markdown allowed. +- `Sentence` - single sentence, no period required. +- `Identifier` - alphanumeric plus underscores, valid as a programming + identifier. +- `FilePath` - relative or absolute path; harness validates existence in + workspace. +- `Command` - shell command line; harness may apply safety classification. + +### 4.2 Composite Types + +- `BulletList` - sequence of items each of type T, expressed as markdown + list. +- `Sequence` - ordered sequence of typed sub-blocks (used for `FILE_EDITS`). +- `Optional` - slot may be empty. + +### 4.3 Semantic Types + +These require LLM-based audit (deferred to v1.1+ or implementation-specific): + +- `Assumption` - a falsifiable claim. +- `Risk` - an observable failure mode. +- `Checkpoint` - an observable intermediate state. + +In v1.0, semantic types are validated heuristically (length, format) and audited +by humans or LLM auditors when configured. + +## 5. Validation and Retry Protocol + +### 5.1 Validation Outcomes + +When a CRP trace is parsed, each slot receives one of three outcomes: + +- **VALID** - slot present, content matches type, all checks pass. +- **WARNING** - slot present, content matches type, but a soft check failed + (e.g., proposed file does not exist but path is well-formed). +- **INVALID** - hard-required slot is missing or empty, a typed slot is + malformed, or a hard check failed. + +A trace is **executable** if no INVALID slots exist. `FINAL_RESPONSE` is the +default hard-required slot. Other template-required slots are recommended +structure by default: missing or empty recommended slots produce WARNING +outcomes, while typed content in those slots is still validated when present. +WARNING slots do not block execution but may be surfaced to the user. + +### 5.2 Retry Message Format + +When a trace contains INVALID slots, the harness sends a retry message to the +model. The retry message uses the same CRP syntax to be parseable: + +```text + +The previous response had validation issues. Please provide a corrected response. + + + +The file 'src/auth/old_session.rs' does not exist in the workspace. +Available files in src/auth/ are: session.rs, middleware.rs, jwt.rs. +Please reconsider which files are actually relevant. + + + +This slot is required for ambiguous requests but was empty. +The original request mentioned "improve auth" without specifying scope. +Please list at least one assumption you are making. + +``` + +The model is expected to produce a complete new CRP trace addressing the +feedback. v1.0 does not support partial retries (slot-by-slot replacement); the +entire trace is regenerated. + +### 5.3 Retry Budget + +Implementations should enforce a retry budget. Recommended default: 3 retries +before giving up and surfacing the best-effort trace to the user with warnings. + +### 5.4 Kill Conditions + +A trace may be killed (no retry, immediate failure) when: + +- Slot syntax is so malformed that parsing cannot recover (e.g., no + recognizable slots in output). +- Required `FINAL_RESPONSE` slot indicates the model cannot complete the task + and is requesting clarification (in which case execution stops and the request + is surfaced to the user, not killed as an error). +- Configured safety policies are violated (e.g., `COMMAND_PROPOSALS` contains + explicitly forbidden commands). + +## 6. Lifecycle (Level 2: Output + Structured Retry) + +CRP v1.0 specifies the **Level 2 lifecycle**: the model receives an +unstructured prompt (with system instructions to respond in CRP), produces a +complete CRP trace, and may be asked to retry with structured feedback if +validation fails. + +### 6.1 Standard Flow + +1. **User input.** User provides natural language request to harness. +2. **Prompt construction.** Harness constructs prompt: system instructions + describing CRP + applicable template + user request. +3. **Model generation.** Model produces output. Harness parses as CRP. +4. **Validation.** Harness validates each slot per its type and applicability. +5. **Branch:** + - If valid: harness renders trace, executes any actions (edits, commands) + per user approval, and returns result. + - If invalid and retry budget remains: harness sends retry message (per + section 5.2) and returns to step 3. + - If invalid and retry budget exhausted: harness surfaces best-effort trace + with warnings. + +### 6.2 System Prompt Content + +Implementations must include the following information in the system prompt: + +- A brief explanation of CRP and its slot syntax. +- The list of slot types relevant to the current template, with type + signatures. +- An example of a valid CRP trace for the task category at hand. +- A reminder that the model must respond entirely in CRP format. + +Implementations may compress this into a `CRP_BRIEF` section if the model is +fine-tuned on CRP and does not require full instructions. + +## 7. Templates + +A **template** is a named bundle of slot requirements appropriate for a category +of task. Templates are expressed as TOML files in the harness configuration. + +Example template (`code_edit.toml`): + +```toml +name = "code_edit" +description = "Modify existing files in the workspace" + +[slots] +TASK_INTERPRETATION = { required = true } +ASSUMPTIONS = { required = "when_ambiguous" } +RELEVANT_FILES = { required = true, type = "BulletList" } +PROPOSED_APPROACH = { required = true } +RISKS = { required = false } +FILE_EDITS = { required = true } +DELIVERABLE_SPEC = { required = true } +FINAL_RESPONSE = { required = true } +``` + +The harness selects a template based on task classification (manual via flag, or +automatic via classifier). Custom templates may be defined per-project in +`.cinto/templates/`. + +CRP v1.0 ships three reference templates: `code_edit`, `code_explanation`, and +`design_proposal`. Implementations should treat these as a baseline. + +## 8. Versioning + +CRP follows semantic versioning at the protocol level. + +- **MAJOR** version increments on breaking changes to slot syntax, type system, + or lifecycle semantics. +- **MINOR** version increments on additions (new standard slots, new types, new + lifecycle levels) that remain backward compatible. +- **PATCH** version increments on clarifications and editorial fixes. + +Implementations should declare their supported CRP version in their handshake or +system prompt. Models fine-tuned on a specific CRP version should declare that +version in their model card. + +## 9. Future Directions (Informative, Not Normative) + +The following are anticipated extensions in CRP v1.x and v2.0. They are +documented here to guide implementation choices that preserve compatibility. + +### 9.1 Lifecycle Level 3 (Slot-by-slot) + +A fuller lifecycle in which the harness drives generation slot-by-slot rather +than receiving the full trace at once. This enables earlier auditing (kill on +first invalid slot, before downstream slots are wasted) and finer-grained +retries (regenerate only the failing slot). + +### 9.2 LLM-based Semantic Auditors + +Validation of semantic types (`Assumption`, `Risk`, `Checkpoint`) by a separate +auditor model. The auditor reads each slot and emits a structured judgment, also +in CRP. Composes naturally with section 5. + +### 9.3 Multi-model Role Specialization + +Different slots produced by different models. For example, a small planner +model produces `TASK_INTERPRETATION` and `PROPOSED_APPROACH`; a code-specialized +model fills `FILE_EDITS`; an auditor model reviews. Each specialist is smaller +than a generalist would be, but the composition matches generalist quality. + +### 9.4 Cross-session Memory Binding + +Slots that reference persistent notes (file-based memory, like an Obsidian +vault). The model can declare in `RELEVANT_FILES` not just code files but +knowledge files, and the harness loads them into context. + +### 9.5 Streaming and Partial Validation + +Streaming CRP output with validation triggered as each closing tag is emitted. +Allows immediate feedback on early errors without waiting for full trace +completion. + +## 10. Reference Implementation + +The Cinto harness (https://github.com/Jshebb/cinto) is the reference +implementation of CRP. Where this specification is ambiguous, the reference +implementation defines the expected behavior, and ambiguity is treated as a +specification bug to be resolved in subsequent versions. + +## Appendix A: Minimal Example Trace + +User request: "Add a hello function to src/main.rs that prints 'hello, world'" + +Model response: + +```text + +The user wants me to add a new function named `hello` to the file src/main.rs. The function should print the literal string "hello, world" to standard output. + + + +- The function should be named exactly `hello` (lowercase, no parameters). +- "hello, world" should be printed with a newline (using println! in Rust). +- The function does not need to be called from main; it just needs to be defined. + + + +- src/main.rs + + + +- Add a new function `fn hello()` to src/main.rs. +- Use `println!("hello, world");` as the function body. +- Place the function above main() for visibility. + + + + +fn hello() { + println!("hello, world"); +} + + + + + +After the edit, src/main.rs contains a function named `hello` that, when called, prints "hello, world" followed by a newline. The existing main function and other code are unchanged. + + + +I've added a `hello()` function to src/main.rs that prints "hello, world". You can call it from main() or anywhere else in your code. Let me know if you'd like me to wire it into main() or modify the implementation. + +``` + +## Appendix B: Draft Notes + +The `FILE_EDITS` subformat is intentionally not final in v1.0-draft. Edit modes +such as prepend, append, replace, and string replacement should be specified +after dogfooding against existing agent editing conventions. + +A candidate v1.1 edit subformat is a terse diff-inspired block syntax inside +`FILE_EDITS`, optimized for small local models already familiar with patches: + +```text + +@@ src/main.rs prepend +fn hello() { println!("hi"); } +@@ src/lib.rs replace_function:greet +pub fn greet() -> String { "hello".into() } + +``` + +Each `@@` header starts a new edit directive. The first token after `@@` is the +path, and the remaining header text is the edit mode or operation descriptor. +The body continues until the next `@@` header or the end of `FILE_EDITS`. +This format is more compact than nested JSON or XML-like edit blocks, and it is +closer to training data that contains unified diffs. The reference +implementation should dogfood both `` blocks and this terse form +before freezing the `FILE_EDITS` grammar. + +Template classification is intentionally underspecified. v1.0 should assume +manual template selection; automatic classification belongs in a future minor +version. + +Slot names are always uppercase English identifiers for parser stability. +Slot content may be written in any natural language. + +Post-v0.2 roadmap note: Cinto v0.3.x should explore Skills + hierarchical +AGENTS.md as a single capability set: + +- Skills loaded on demand from `.cinto/skills/*.md`. +- Hierarchical `AGENTS.md` discovery, ordered from global to project to module. +- A `list_skills()` tool exposed to the agent for explicit discovery. +- Optional audit slot: + +```text + +- rust-parser +- project-style + +``` + +`SKILLS_USED` records which skills were consulted during a trace. It should be +informative for auditability, not a mechanism for mutating configuration. diff --git a/docs/architecture.md b/docs/architecture.md index 08f9862..bb119aa 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -44,6 +44,14 @@ session adapter is built. The file is appended to the developer instructions for both Harmony and OpenAI tool-calling adapters, so project context applies across model formats. The loaded content is bounded to keep prompt size predictable. +## Reasoning Protocol + +The draft Cinto Reasoning Protocol (CRP) is documented in +[CRP_SPEC.md](CRP_SPEC.md). CRP is a proposed structured-output layer for +typed, auditable reasoning traces. It is not implemented in the current runtime +yet; the first implementation milestone should start with a parser, slot +validator, and retry feedback loop before any model fine-tuning work. + ## Tool Policy Milestone 1 exposes workspace tools: @@ -65,9 +73,10 @@ session. `write_file` and `delete_file` are intentionally narrow: they only mutate regular files beneath the configured workspace. They require explicit TUI approval by default unless `harness.require_edit_approval` is disabled. -Tool loops are capped by `harness.max_tool_turns`, which defaults to 16. Tool -execution errors are returned to the model as tool output so the model can -recover instead of crashing the UI turn. +Tool execution is capped by `harness.max_tool_turns`, while total model calls, +CRP repair attempts, empty-response retries, and transport retries each use +their own budgets. Tool execution errors are returned to the model as tool +output so the model can recover instead of crashing the UI turn. Milestone 2 should add a diff preview and a user approval step around write tools. diff --git a/eval/tasks/v1/baseline/bugfix_01/initial/Cargo.toml b/eval/tasks/v1/baseline/bugfix_01/initial/Cargo.toml new file mode 100644 index 0000000..cb79fe0 --- /dev/null +++ b/eval/tasks/v1/baseline/bugfix_01/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_01" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/baseline/bugfix_01/initial/src/lib.rs b/eval/tasks/v1/baseline/bugfix_01/initial/src/lib.rs new file mode 100644 index 0000000..fb0ad78 --- /dev/null +++ b/eval/tasks/v1/baseline/bugfix_01/initial/src/lib.rs @@ -0,0 +1,18 @@ +pub fn find_max(data: &[i32]) -> i32 { + let mut max = data[0]; + for i in 1..=data.len() { + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_find_max() { + assert_eq!(find_max(&[1, 5, 3]), 5); + } +} diff --git a/eval/tasks/v1/baseline/bugfix_02/initial/Cargo.toml b/eval/tasks/v1/baseline/bugfix_02/initial/Cargo.toml new file mode 100644 index 0000000..3edf67b --- /dev/null +++ b/eval/tasks/v1/baseline/bugfix_02/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_02" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/baseline/bugfix_02/initial/src/lib.rs b/eval/tasks/v1/baseline/bugfix_02/initial/src/lib.rs new file mode 100644 index 0000000..54a4156 --- /dev/null +++ b/eval/tasks/v1/baseline/bugfix_02/initial/src/lib.rs @@ -0,0 +1,16 @@ +pub fn get_first_char(text: Option<&str>) -> char { + text.unwrap().chars().next().unwrap() +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_empty() { + assert_eq!(get_first_char(Some("")), '\0'); + } + #[test] + fn test_none() { + assert_eq!(get_first_char(None), '\0'); + } +} diff --git a/eval/tasks/v1/baseline/edge_01/initial/Cargo.toml b/eval/tasks/v1/baseline/edge_01/initial/Cargo.toml new file mode 100644 index 0000000..9682092 --- /dev/null +++ b/eval/tasks/v1/baseline/edge_01/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_01" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/baseline/edge_01/initial/src/lib.rs b/eval/tasks/v1/baseline/edge_01/initial/src/lib.rs new file mode 100644 index 0000000..8328442 --- /dev/null +++ b/eval/tasks/v1/baseline/edge_01/initial/src/lib.rs @@ -0,0 +1,8 @@ +pub fn is_even(n: i32) -> bool { + n % 2 == 0 +} + +pub fn check_numbers() { + let a = is_even(2) + let b = is_even(3); +} diff --git a/eval/tasks/v1/baseline/feature_01/initial/Cargo.toml b/eval/tasks/v1/baseline/feature_01/initial/Cargo.toml new file mode 100644 index 0000000..4abba78 --- /dev/null +++ b/eval/tasks/v1/baseline/feature_01/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_01" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/baseline/feature_01/initial/src/lib.rs b/eval/tasks/v1/baseline/feature_01/initial/src/lib.rs new file mode 100644 index 0000000..017d425 --- /dev/null +++ b/eval/tasks/v1/baseline/feature_01/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct User { + id: u32, + username: String, +} + +impl User { + pub fn new(id: u32, username: String) -> Self { + Self { id, username } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_get_username() { + let user = User::new(1, "alice".to_string()); + assert_eq!(user.username(), "alice"); + } +} diff --git a/eval/tasks/v1/baseline/py_bugfix_01/initial/lib.py b/eval/tasks/v1/baseline/py_bugfix_01/initial/lib.py new file mode 100644 index 0000000..169346e --- /dev/null +++ b/eval/tasks/v1/baseline/py_bugfix_01/initial/lib.py @@ -0,0 +1,6 @@ +def find_max(data): + max_val = data[0] + for i in range(1, len(data) - 1): + if data[i] > max_val: + max_val = data[i] + return max_val diff --git a/eval/tasks/v1/baseline/py_bugfix_01/initial/test_lib.py b/eval/tasks/v1/baseline/py_bugfix_01/initial/test_lib.py new file mode 100644 index 0000000..40cb6ff --- /dev/null +++ b/eval/tasks/v1/baseline/py_bugfix_01/initial/test_lib.py @@ -0,0 +1,15 @@ +import unittest +from lib import find_max + +class TestFindMax(unittest.TestCase): + def test_basic(self): + self.assertEqual(find_max([1, 5, 3]), 5) + + def test_last_element_is_max(self): + self.assertEqual(find_max([1, 2, 9]), 9) + + def test_single_element(self): + self.assertEqual(find_max([7]), 7) + +if __name__ == '__main__': + unittest.main() diff --git a/eval/tasks/v1/baseline/py_bugfix_02/initial/lib.py b/eval/tasks/v1/baseline/py_bugfix_02/initial/lib.py new file mode 100644 index 0000000..24b9c2b --- /dev/null +++ b/eval/tasks/v1/baseline/py_bugfix_02/initial/lib.py @@ -0,0 +1,2 @@ +def calculate_average(numbers): + return sum(numbers) / len(numbers) diff --git a/eval/tasks/v1/baseline/py_bugfix_02/initial/test_lib.py b/eval/tasks/v1/baseline/py_bugfix_02/initial/test_lib.py new file mode 100644 index 0000000..4c9a9d1 --- /dev/null +++ b/eval/tasks/v1/baseline/py_bugfix_02/initial/test_lib.py @@ -0,0 +1,15 @@ +import unittest +from lib import calculate_average + +class TestCalculateAverage(unittest.TestCase): + def test_normal(self): + self.assertEqual(calculate_average([1, 2, 3]), 2.0) + + def test_empty_returns_zero(self): + self.assertEqual(calculate_average([]), 0.0) + + def test_single(self): + self.assertEqual(calculate_average([5]), 5.0) + +if __name__ == '__main__': + unittest.main() diff --git a/eval/tasks/v1/baseline/py_edge_01/initial/lib.py b/eval/tasks/v1/baseline/py_edge_01/initial/lib.py new file mode 100644 index 0000000..41d163a --- /dev/null +++ b/eval/tasks/v1/baseline/py_edge_01/initial/lib.py @@ -0,0 +1,5 @@ +def first_element(lst): + return lst[0] + +def safe_divide(a, b): + return a / b diff --git a/eval/tasks/v1/baseline/py_edge_01/initial/test_lib.py b/eval/tasks/v1/baseline/py_edge_01/initial/test_lib.py new file mode 100644 index 0000000..6109f9e --- /dev/null +++ b/eval/tasks/v1/baseline/py_edge_01/initial/test_lib.py @@ -0,0 +1,19 @@ +import unittest +from lib import first_element, safe_divide + +class TestFirstElement(unittest.TestCase): + def test_normal(self): + self.assertEqual(first_element([1, 2, 3]), 1) + + def test_empty_returns_none(self): + self.assertIsNone(first_element([])) + +class TestSafeDivide(unittest.TestCase): + def test_normal(self): + self.assertEqual(safe_divide(10, 2), 5.0) + + def test_divide_by_zero_returns_none(self): + self.assertIsNone(safe_divide(10, 0)) + +if __name__ == '__main__': + unittest.main() diff --git a/eval/tasks/v1/baseline/py_feature_01/initial/lib.py b/eval/tasks/v1/baseline/py_feature_01/initial/lib.py new file mode 100644 index 0000000..7f6bb63 --- /dev/null +++ b/eval/tasks/v1/baseline/py_feature_01/initial/lib.py @@ -0,0 +1,4 @@ +class User: + def __init__(self, username, email): + self._username = username + self._email = email diff --git a/eval/tasks/v1/baseline/py_feature_01/initial/test_lib.py b/eval/tasks/v1/baseline/py_feature_01/initial/test_lib.py new file mode 100644 index 0000000..5bbf863 --- /dev/null +++ b/eval/tasks/v1/baseline/py_feature_01/initial/test_lib.py @@ -0,0 +1,14 @@ +import unittest +from lib import User + +class TestUser(unittest.TestCase): + def test_get_username(self): + u = User("alice", "alice@example.com") + self.assertEqual(u.get_username(), "alice") + + def test_get_email(self): + u = User("bob", "bob@example.com") + self.assertEqual(u.get_email(), "bob@example.com") + +if __name__ == '__main__': + unittest.main() diff --git a/eval/tasks/v1/baseline/py_refactor_01/initial/lib.py b/eval/tasks/v1/baseline/py_refactor_01/initial/lib.py new file mode 100644 index 0000000..364429a --- /dev/null +++ b/eval/tasks/v1/baseline/py_refactor_01/initial/lib.py @@ -0,0 +1,9 @@ +def process_data(data): + total = 0 + for x in data: + total += x + average = total / len(data) if data else 0.0 + result = [] + for x in data: + result.append(x - average) + return result diff --git a/eval/tasks/v1/baseline/py_refactor_01/initial/test_lib.py b/eval/tasks/v1/baseline/py_refactor_01/initial/test_lib.py new file mode 100644 index 0000000..c71ed19 --- /dev/null +++ b/eval/tasks/v1/baseline/py_refactor_01/initial/test_lib.py @@ -0,0 +1,19 @@ +import unittest +from lib import process_data, calculate_average + +class TestProcessData(unittest.TestCase): + def test_basic(self): + result = process_data([1.0, 2.0, 3.0]) + self.assertAlmostEqual(result[0], -1.0) + self.assertAlmostEqual(result[1], 0.0) + self.assertAlmostEqual(result[2], 1.0) + +class TestCalculateAverage(unittest.TestCase): + def test_average(self): + self.assertAlmostEqual(calculate_average([1.0, 2.0, 3.0]), 2.0) + + def test_empty(self): + self.assertEqual(calculate_average([]), 0.0) + +if __name__ == '__main__': + unittest.main() diff --git a/eval/tasks/v1/baseline/refactor_01/initial/Cargo.toml b/eval/tasks/v1/baseline/refactor_01/initial/Cargo.toml new file mode 100644 index 0000000..5415d2d --- /dev/null +++ b/eval/tasks/v1/baseline/refactor_01/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_01" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/baseline/refactor_01/initial/src/lib.rs b/eval/tasks/v1/baseline/refactor_01/initial/src/lib.rs new file mode 100644 index 0000000..36e86fe --- /dev/null +++ b/eval/tasks/v1/baseline/refactor_01/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_data(data: &[i32]) -> i32 { + let mut sum = 0; + for item in data { + sum += item; + } + let average = if data.is_empty() { 0 } else { sum / data.len() as i32 }; + + // some complex business logic + let mut result = 0; + for item in data { + result += (item - average) * (item - average); + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_data(&[1, 2, 3, 4, 5]), 10); + } +} diff --git a/eval/tasks/v1/baseline/tasks.jsonl b/eval/tasks/v1/baseline/tasks.jsonl new file mode 100644 index 0000000..ecbfc70 --- /dev/null +++ b/eval/tasks/v1/baseline/tasks.jsonl @@ -0,0 +1,5 @@ +{"id": "refactor_01", "prompt": "The process_data function in src/lib.rs is too long. Extract the average calculation into a separate helper function called calculate_average.", "fixture_dir": "eval/tasks/v1/baseline/refactor_01/initial", "validation_command": "cargo test"} +{"id": "feature_01", "prompt": "The User struct in src/lib.rs needs a getter method for the username. Add a method `pub fn username(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/baseline/feature_01/initial", "validation_command": "cargo test"} +{"id": "bugfix_01", "prompt": "There is an out-of-bounds bug in the find_max function in src/lib.rs. Fix it so the test passes.", "fixture_dir": "eval/tasks/v1/baseline/bugfix_01/initial", "validation_command": "cargo test"} +{"id": "bugfix_02", "prompt": "The get_first_char function panics on empty strings and None values. Make it return '\\0' instead of panicking.", "fixture_dir": "eval/tasks/v1/baseline/bugfix_02/initial", "validation_command": "cargo test"} +{"id": "edge_01", "prompt": "Fix the syntax error in src/lib.rs.", "fixture_dir": "eval/tasks/v1/baseline/edge_01/initial", "validation_command": "cargo check"} diff --git a/eval/tasks/v1/baseline/tasks_python.jsonl b/eval/tasks/v1/baseline/tasks_python.jsonl new file mode 100644 index 0000000..40dbcd7 --- /dev/null +++ b/eval/tasks/v1/baseline/tasks_python.jsonl @@ -0,0 +1,5 @@ +{"id": "py_bugfix_01", "prompt": "There is an off-by-one bug in the find_max function in lib.py. The loop does not check the last element. Fix it so all tests pass.", "fixture_dir": "eval/tasks/v1/baseline/py_bugfix_01/initial", "validation_command": "python3 -m unittest test_lib"} +{"id": "py_bugfix_02", "prompt": "The calculate_average function in lib.py crashes with ZeroDivisionError when given an empty list. Fix it to return 0.0 for empty input.", "fixture_dir": "eval/tasks/v1/baseline/py_bugfix_02/initial", "validation_command": "python3 -m unittest test_lib"} +{"id": "py_feature_01", "prompt": "The User class in lib.py is missing getter methods. Add get_username(self) and get_email(self) methods that return the stored values.", "fixture_dir": "eval/tasks/v1/baseline/py_feature_01/initial", "validation_command": "python3 -m unittest test_lib"} +{"id": "py_refactor_01", "prompt": "The process_data function in lib.py calculates the average inline. Extract it into a separate function called calculate_average(data) that returns 0.0 for empty input.", "fixture_dir": "eval/tasks/v1/baseline/py_refactor_01/initial", "validation_command": "python3 -m unittest test_lib"} +{"id": "py_edge_01", "prompt": "The first_element function panics on empty lists and safe_divide crashes on zero. Fix first_element to return None for empty input and safe_divide to return None when b is zero.", "fixture_dir": "eval/tasks/v1/baseline/py_edge_01/initial", "validation_command": "python3 -m unittest test_lib"} diff --git a/eval/tasks/v1/expanded/bugfix_001/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_001/initial/Cargo.toml new file mode 100644 index 0000000..b8d4b7e --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_001/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_001" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_001/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_001/initial/src/lib.rs new file mode 100644 index 0000000..151fbad --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_001/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_1(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_1(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_002/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_002/initial/Cargo.toml new file mode 100644 index 0000000..158b324 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_002/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_002" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_002/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_002/initial/src/lib.rs new file mode 100644 index 0000000..af3ba75 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_002/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_2(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_2(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_003/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_003/initial/Cargo.toml new file mode 100644 index 0000000..c0d9c55 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_003/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_003" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_003/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_003/initial/src/lib.rs new file mode 100644 index 0000000..00fa882 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_003/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_3(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_3(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_004/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_004/initial/Cargo.toml new file mode 100644 index 0000000..1747f56 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_004/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_004" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_004/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_004/initial/src/lib.rs new file mode 100644 index 0000000..e26fb66 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_004/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_4(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_4(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_005/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_005/initial/Cargo.toml new file mode 100644 index 0000000..85993e3 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_005/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_005" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_005/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_005/initial/src/lib.rs new file mode 100644 index 0000000..69a2aca --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_005/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_5(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_5(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_006/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_006/initial/Cargo.toml new file mode 100644 index 0000000..101318c --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_006/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_006" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_006/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_006/initial/src/lib.rs new file mode 100644 index 0000000..0936d09 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_006/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_6(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_6(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_007/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_007/initial/Cargo.toml new file mode 100644 index 0000000..28880ab --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_007/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_007" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_007/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_007/initial/src/lib.rs new file mode 100644 index 0000000..8b6a230 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_007/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_7(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_7(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_008/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_008/initial/Cargo.toml new file mode 100644 index 0000000..5f56206 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_008/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_008" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_008/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_008/initial/src/lib.rs new file mode 100644 index 0000000..73c2c0a --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_008/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_8(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_8(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_009/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_009/initial/Cargo.toml new file mode 100644 index 0000000..fcaebc3 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_009/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_009" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_009/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_009/initial/src/lib.rs new file mode 100644 index 0000000..946a0d2 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_009/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_9(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_9(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_010/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_010/initial/Cargo.toml new file mode 100644 index 0000000..342132f --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_010/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_010" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_010/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_010/initial/src/lib.rs new file mode 100644 index 0000000..405b506 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_010/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_10(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_10(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_011/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_011/initial/Cargo.toml new file mode 100644 index 0000000..ab66934 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_011/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_011" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_011/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_011/initial/src/lib.rs new file mode 100644 index 0000000..fbc9cdf --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_011/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_11(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_11(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_012/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_012/initial/Cargo.toml new file mode 100644 index 0000000..9e37aff --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_012/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_012" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_012/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_012/initial/src/lib.rs new file mode 100644 index 0000000..c237709 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_012/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_12(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_12(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_013/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_013/initial/Cargo.toml new file mode 100644 index 0000000..a16a64e --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_013/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_013" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_013/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_013/initial/src/lib.rs new file mode 100644 index 0000000..a840668 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_013/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_13(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_13(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_014/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_014/initial/Cargo.toml new file mode 100644 index 0000000..074c27a --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_014/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_014" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_014/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_014/initial/src/lib.rs new file mode 100644 index 0000000..f5c8dd9 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_014/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_14(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_14(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_015/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_015/initial/Cargo.toml new file mode 100644 index 0000000..77ae114 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_015/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_015" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_015/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_015/initial/src/lib.rs new file mode 100644 index 0000000..b2790ec --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_015/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_15(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_15(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_016/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_016/initial/Cargo.toml new file mode 100644 index 0000000..9c673cf --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_016/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_016" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_016/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_016/initial/src/lib.rs new file mode 100644 index 0000000..739b55e --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_016/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_16(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_16(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_017/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_017/initial/Cargo.toml new file mode 100644 index 0000000..7fc66da --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_017/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_017" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_017/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_017/initial/src/lib.rs new file mode 100644 index 0000000..435559b --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_017/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_17(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_17(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_018/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_018/initial/Cargo.toml new file mode 100644 index 0000000..7a04a2d --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_018/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_018" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_018/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_018/initial/src/lib.rs new file mode 100644 index 0000000..70185e8 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_018/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_18(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_18(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_019/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_019/initial/Cargo.toml new file mode 100644 index 0000000..14488ab --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_019/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_019" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_019/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_019/initial/src/lib.rs new file mode 100644 index 0000000..0ac2231 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_019/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_19(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_19(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_020/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_020/initial/Cargo.toml new file mode 100644 index 0000000..70c42ee --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_020/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_020" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_020/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_020/initial/src/lib.rs new file mode 100644 index 0000000..2dd808f --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_020/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_20(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_20(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_021/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_021/initial/Cargo.toml new file mode 100644 index 0000000..0af94ef --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_021/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_021" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_021/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_021/initial/src/lib.rs new file mode 100644 index 0000000..273fb0b --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_021/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_21(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_21(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_022/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_022/initial/Cargo.toml new file mode 100644 index 0000000..0637f4b --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_022/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_022" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_022/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_022/initial/src/lib.rs new file mode 100644 index 0000000..46a317c --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_022/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_22(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_22(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_023/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_023/initial/Cargo.toml new file mode 100644 index 0000000..c721715 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_023/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_023" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_023/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_023/initial/src/lib.rs new file mode 100644 index 0000000..71de6b0 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_023/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_23(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_23(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_024/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_024/initial/Cargo.toml new file mode 100644 index 0000000..6731f05 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_024/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_024" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_024/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_024/initial/src/lib.rs new file mode 100644 index 0000000..bb51bc8 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_024/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_24(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_24(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_025/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_025/initial/Cargo.toml new file mode 100644 index 0000000..d8ad2c2 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_025/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_025" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_025/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_025/initial/src/lib.rs new file mode 100644 index 0000000..961def5 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_025/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_25(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_25(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_026/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_026/initial/Cargo.toml new file mode 100644 index 0000000..fe1c208 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_026/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_026" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_026/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_026/initial/src/lib.rs new file mode 100644 index 0000000..151d579 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_026/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_26(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_26(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_027/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_027/initial/Cargo.toml new file mode 100644 index 0000000..54b21b9 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_027/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_027" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_027/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_027/initial/src/lib.rs new file mode 100644 index 0000000..050feb8 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_027/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_27(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_27(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_028/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_028/initial/Cargo.toml new file mode 100644 index 0000000..95d7f22 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_028/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_028" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_028/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_028/initial/src/lib.rs new file mode 100644 index 0000000..4133dc2 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_028/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_28(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_28(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_029/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_029/initial/Cargo.toml new file mode 100644 index 0000000..9cd4aa1 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_029/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_029" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_029/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_029/initial/src/lib.rs new file mode 100644 index 0000000..7247207 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_029/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_29(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_29(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_030/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_030/initial/Cargo.toml new file mode 100644 index 0000000..7b68d3b --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_030/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_030" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_030/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_030/initial/src/lib.rs new file mode 100644 index 0000000..c576345 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_030/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_30(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_30(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_031/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_031/initial/Cargo.toml new file mode 100644 index 0000000..5e97cce --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_031/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_031" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_031/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_031/initial/src/lib.rs new file mode 100644 index 0000000..4791006 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_031/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_31(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_31(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_032/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_032/initial/Cargo.toml new file mode 100644 index 0000000..a111068 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_032/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_032" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_032/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_032/initial/src/lib.rs new file mode 100644 index 0000000..396bb35 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_032/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_32(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_32(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_033/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_033/initial/Cargo.toml new file mode 100644 index 0000000..876d5d7 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_033/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_033" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_033/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_033/initial/src/lib.rs new file mode 100644 index 0000000..b9bc04e --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_033/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_33(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_33(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_034/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_034/initial/Cargo.toml new file mode 100644 index 0000000..a9b83be --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_034/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_034" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_034/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_034/initial/src/lib.rs new file mode 100644 index 0000000..985ccfa --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_034/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_34(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_34(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_035/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_035/initial/Cargo.toml new file mode 100644 index 0000000..7d91ec6 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_035/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_035" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_035/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_035/initial/src/lib.rs new file mode 100644 index 0000000..59f34b0 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_035/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_35(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_35(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_036/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_036/initial/Cargo.toml new file mode 100644 index 0000000..95f7862 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_036/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_036" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_036/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_036/initial/src/lib.rs new file mode 100644 index 0000000..cbd336b --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_036/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_36(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_36(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_037/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_037/initial/Cargo.toml new file mode 100644 index 0000000..e1fe837 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_037/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_037" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_037/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_037/initial/src/lib.rs new file mode 100644 index 0000000..768e014 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_037/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_37(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_37(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_038/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_038/initial/Cargo.toml new file mode 100644 index 0000000..58b1ba9 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_038/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_038" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_038/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_038/initial/src/lib.rs new file mode 100644 index 0000000..b3804bb --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_038/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_38(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_38(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_039/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_039/initial/Cargo.toml new file mode 100644 index 0000000..88d2c5e --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_039/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_039" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_039/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_039/initial/src/lib.rs new file mode 100644 index 0000000..ccb5819 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_039/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_39(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_39(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_040/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_040/initial/Cargo.toml new file mode 100644 index 0000000..7ff1829 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_040/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_040" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_040/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_040/initial/src/lib.rs new file mode 100644 index 0000000..a8fb85e --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_040/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_40(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_40(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_041/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_041/initial/Cargo.toml new file mode 100644 index 0000000..85a684c --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_041/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_041" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_041/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_041/initial/src/lib.rs new file mode 100644 index 0000000..58ffa98 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_041/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_41(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_41(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_042/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_042/initial/Cargo.toml new file mode 100644 index 0000000..0a6c655 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_042/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_042" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_042/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_042/initial/src/lib.rs new file mode 100644 index 0000000..141c942 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_042/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_42(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_42(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_043/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_043/initial/Cargo.toml new file mode 100644 index 0000000..c82407f --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_043/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_043" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_043/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_043/initial/src/lib.rs new file mode 100644 index 0000000..51d2e68 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_043/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_43(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_43(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_044/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_044/initial/Cargo.toml new file mode 100644 index 0000000..31c3c90 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_044/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_044" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_044/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_044/initial/src/lib.rs new file mode 100644 index 0000000..70e83a3 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_044/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_44(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_44(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_045/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_045/initial/Cargo.toml new file mode 100644 index 0000000..0e871d4 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_045/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_045" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_045/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_045/initial/src/lib.rs new file mode 100644 index 0000000..8c6ecb9 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_045/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_45(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_45(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_046/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_046/initial/Cargo.toml new file mode 100644 index 0000000..8580d95 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_046/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_046" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_046/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_046/initial/src/lib.rs new file mode 100644 index 0000000..6e3c624 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_046/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_46(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_46(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_047/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_047/initial/Cargo.toml new file mode 100644 index 0000000..f90b736 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_047/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_047" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_047/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_047/initial/src/lib.rs new file mode 100644 index 0000000..c420a53 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_047/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_47(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_47(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_048/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_048/initial/Cargo.toml new file mode 100644 index 0000000..e1eba71 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_048/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_048" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_048/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_048/initial/src/lib.rs new file mode 100644 index 0000000..ab38f11 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_048/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_48(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_48(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_049/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_049/initial/Cargo.toml new file mode 100644 index 0000000..b7aa7d5 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_049/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_049" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_049/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_049/initial/src/lib.rs new file mode 100644 index 0000000..95908e7 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_049/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_49(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_49(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/bugfix_050/initial/Cargo.toml b/eval/tasks/v1/expanded/bugfix_050/initial/Cargo.toml new file mode 100644 index 0000000..f800401 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_050/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bugfix_050" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/bugfix_050/initial/src/lib.rs b/eval/tasks/v1/expanded/bugfix_050/initial/src/lib.rs new file mode 100644 index 0000000..87e3dc1 --- /dev/null +++ b/eval/tasks/v1/expanded/bugfix_050/initial/src/lib.rs @@ -0,0 +1,19 @@ +pub fn analyze_50(data: &[i32]) -> i32 { + if data.is_empty() { return 0; } + let mut max = data[0]; + for i in 1..=data.len() { // OFF BY ONE BUG + if data[i] > max { + max = data[i]; + } + } + max +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_analyze() { + assert_eq!(analyze_50(&[1, 5, 3]), 5); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_001/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_001/initial/Cargo.toml new file mode 100644 index 0000000..579d52c --- /dev/null +++ b/eval/tasks/v1/expanded/edge_001/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_001" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_001/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_001/initial/src/lib.rs new file mode 100644 index 0000000..61bc757 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_001/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_1(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_1(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_002/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_002/initial/Cargo.toml new file mode 100644 index 0000000..e7e01f0 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_002/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_002" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_002/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_002/initial/src/lib.rs new file mode 100644 index 0000000..b314407 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_002/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_2(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_2(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_003/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_003/initial/Cargo.toml new file mode 100644 index 0000000..08c1d91 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_003/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_003" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_003/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_003/initial/src/lib.rs new file mode 100644 index 0000000..0a071bf --- /dev/null +++ b/eval/tasks/v1/expanded/edge_003/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_3(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_3(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_004/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_004/initial/Cargo.toml new file mode 100644 index 0000000..104cd32 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_004/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_004" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_004/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_004/initial/src/lib.rs new file mode 100644 index 0000000..a692ace --- /dev/null +++ b/eval/tasks/v1/expanded/edge_004/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_4(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_4(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_005/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_005/initial/Cargo.toml new file mode 100644 index 0000000..5582833 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_005/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_005" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_005/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_005/initial/src/lib.rs new file mode 100644 index 0000000..a838858 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_005/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_5(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_5(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_006/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_006/initial/Cargo.toml new file mode 100644 index 0000000..b00d966 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_006/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_006" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_006/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_006/initial/src/lib.rs new file mode 100644 index 0000000..c992963 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_006/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_6(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_6(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_007/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_007/initial/Cargo.toml new file mode 100644 index 0000000..abf0387 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_007/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_007" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_007/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_007/initial/src/lib.rs new file mode 100644 index 0000000..a8cd03b --- /dev/null +++ b/eval/tasks/v1/expanded/edge_007/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_7(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_7(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_008/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_008/initial/Cargo.toml new file mode 100644 index 0000000..597333d --- /dev/null +++ b/eval/tasks/v1/expanded/edge_008/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_008" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_008/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_008/initial/src/lib.rs new file mode 100644 index 0000000..3ba48ee --- /dev/null +++ b/eval/tasks/v1/expanded/edge_008/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_8(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_8(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_009/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_009/initial/Cargo.toml new file mode 100644 index 0000000..42f7e20 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_009/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_009" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_009/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_009/initial/src/lib.rs new file mode 100644 index 0000000..62d1678 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_009/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_9(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_9(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_010/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_010/initial/Cargo.toml new file mode 100644 index 0000000..7e8928b --- /dev/null +++ b/eval/tasks/v1/expanded/edge_010/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_010" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_010/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_010/initial/src/lib.rs new file mode 100644 index 0000000..32efe6a --- /dev/null +++ b/eval/tasks/v1/expanded/edge_010/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_10(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_10(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_011/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_011/initial/Cargo.toml new file mode 100644 index 0000000..7e4b61f --- /dev/null +++ b/eval/tasks/v1/expanded/edge_011/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_011" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_011/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_011/initial/src/lib.rs new file mode 100644 index 0000000..39e02aa --- /dev/null +++ b/eval/tasks/v1/expanded/edge_011/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_11(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_11(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_012/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_012/initial/Cargo.toml new file mode 100644 index 0000000..be40efd --- /dev/null +++ b/eval/tasks/v1/expanded/edge_012/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_012" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_012/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_012/initial/src/lib.rs new file mode 100644 index 0000000..cf87254 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_012/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_12(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_12(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_013/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_013/initial/Cargo.toml new file mode 100644 index 0000000..b51fa3b --- /dev/null +++ b/eval/tasks/v1/expanded/edge_013/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_013" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_013/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_013/initial/src/lib.rs new file mode 100644 index 0000000..8d18172 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_013/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_13(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_13(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_014/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_014/initial/Cargo.toml new file mode 100644 index 0000000..e8b44dd --- /dev/null +++ b/eval/tasks/v1/expanded/edge_014/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_014" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_014/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_014/initial/src/lib.rs new file mode 100644 index 0000000..d2e89f7 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_014/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_14(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_14(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_015/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_015/initial/Cargo.toml new file mode 100644 index 0000000..38fd334 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_015/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_015" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_015/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_015/initial/src/lib.rs new file mode 100644 index 0000000..1cd91b4 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_015/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_15(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_15(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_016/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_016/initial/Cargo.toml new file mode 100644 index 0000000..803be43 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_016/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_016" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_016/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_016/initial/src/lib.rs new file mode 100644 index 0000000..c253875 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_016/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_16(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_16(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_017/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_017/initial/Cargo.toml new file mode 100644 index 0000000..b498538 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_017/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_017" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_017/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_017/initial/src/lib.rs new file mode 100644 index 0000000..f46b393 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_017/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_17(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_17(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_018/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_018/initial/Cargo.toml new file mode 100644 index 0000000..7379c70 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_018/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_018" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_018/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_018/initial/src/lib.rs new file mode 100644 index 0000000..88b4eeb --- /dev/null +++ b/eval/tasks/v1/expanded/edge_018/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_18(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_18(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_019/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_019/initial/Cargo.toml new file mode 100644 index 0000000..ebbda5c --- /dev/null +++ b/eval/tasks/v1/expanded/edge_019/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_019" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_019/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_019/initial/src/lib.rs new file mode 100644 index 0000000..78f4f5c --- /dev/null +++ b/eval/tasks/v1/expanded/edge_019/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_19(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_19(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_020/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_020/initial/Cargo.toml new file mode 100644 index 0000000..60c8dd4 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_020/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_020" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_020/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_020/initial/src/lib.rs new file mode 100644 index 0000000..39faf88 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_020/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_20(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_20(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_021/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_021/initial/Cargo.toml new file mode 100644 index 0000000..69b6d2c --- /dev/null +++ b/eval/tasks/v1/expanded/edge_021/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_021" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_021/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_021/initial/src/lib.rs new file mode 100644 index 0000000..81da5a1 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_021/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_21(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_21(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_022/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_022/initial/Cargo.toml new file mode 100644 index 0000000..3abe19f --- /dev/null +++ b/eval/tasks/v1/expanded/edge_022/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_022" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_022/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_022/initial/src/lib.rs new file mode 100644 index 0000000..0c8a2f0 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_022/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_22(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_22(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_023/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_023/initial/Cargo.toml new file mode 100644 index 0000000..f5e18e7 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_023/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_023" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_023/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_023/initial/src/lib.rs new file mode 100644 index 0000000..a16a576 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_023/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_23(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_23(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_024/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_024/initial/Cargo.toml new file mode 100644 index 0000000..096f02e --- /dev/null +++ b/eval/tasks/v1/expanded/edge_024/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_024" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_024/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_024/initial/src/lib.rs new file mode 100644 index 0000000..dc8b54e --- /dev/null +++ b/eval/tasks/v1/expanded/edge_024/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_24(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_24(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_025/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_025/initial/Cargo.toml new file mode 100644 index 0000000..7ebeeca --- /dev/null +++ b/eval/tasks/v1/expanded/edge_025/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_025" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_025/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_025/initial/src/lib.rs new file mode 100644 index 0000000..f0e9bae --- /dev/null +++ b/eval/tasks/v1/expanded/edge_025/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_25(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_25(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_026/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_026/initial/Cargo.toml new file mode 100644 index 0000000..858513e --- /dev/null +++ b/eval/tasks/v1/expanded/edge_026/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_026" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_026/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_026/initial/src/lib.rs new file mode 100644 index 0000000..cb8d3a8 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_026/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_26(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_26(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_027/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_027/initial/Cargo.toml new file mode 100644 index 0000000..4ffdff9 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_027/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_027" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_027/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_027/initial/src/lib.rs new file mode 100644 index 0000000..24bbde2 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_027/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_27(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_27(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_028/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_028/initial/Cargo.toml new file mode 100644 index 0000000..7fde166 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_028/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_028" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_028/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_028/initial/src/lib.rs new file mode 100644 index 0000000..4baae79 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_028/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_28(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_28(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_029/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_029/initial/Cargo.toml new file mode 100644 index 0000000..272dd9a --- /dev/null +++ b/eval/tasks/v1/expanded/edge_029/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_029" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_029/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_029/initial/src/lib.rs new file mode 100644 index 0000000..2ecb7a2 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_029/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_29(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_29(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_030/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_030/initial/Cargo.toml new file mode 100644 index 0000000..bc6193d --- /dev/null +++ b/eval/tasks/v1/expanded/edge_030/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_030" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_030/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_030/initial/src/lib.rs new file mode 100644 index 0000000..640cd1b --- /dev/null +++ b/eval/tasks/v1/expanded/edge_030/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_30(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_30(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_031/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_031/initial/Cargo.toml new file mode 100644 index 0000000..cd32bfe --- /dev/null +++ b/eval/tasks/v1/expanded/edge_031/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_031" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_031/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_031/initial/src/lib.rs new file mode 100644 index 0000000..9364029 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_031/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_31(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_31(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_032/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_032/initial/Cargo.toml new file mode 100644 index 0000000..4441b6c --- /dev/null +++ b/eval/tasks/v1/expanded/edge_032/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_032" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_032/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_032/initial/src/lib.rs new file mode 100644 index 0000000..6f23a48 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_032/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_32(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_32(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_033/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_033/initial/Cargo.toml new file mode 100644 index 0000000..9daec50 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_033/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_033" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_033/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_033/initial/src/lib.rs new file mode 100644 index 0000000..b456a1f --- /dev/null +++ b/eval/tasks/v1/expanded/edge_033/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_33(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_33(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_034/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_034/initial/Cargo.toml new file mode 100644 index 0000000..bbebe96 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_034/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_034" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_034/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_034/initial/src/lib.rs new file mode 100644 index 0000000..b0edc8d --- /dev/null +++ b/eval/tasks/v1/expanded/edge_034/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_34(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_34(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_035/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_035/initial/Cargo.toml new file mode 100644 index 0000000..4f99742 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_035/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_035" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_035/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_035/initial/src/lib.rs new file mode 100644 index 0000000..24c52c3 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_035/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_35(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_35(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_036/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_036/initial/Cargo.toml new file mode 100644 index 0000000..846b38e --- /dev/null +++ b/eval/tasks/v1/expanded/edge_036/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_036" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_036/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_036/initial/src/lib.rs new file mode 100644 index 0000000..6ec8ce2 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_036/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_36(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_36(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_037/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_037/initial/Cargo.toml new file mode 100644 index 0000000..2da9f3f --- /dev/null +++ b/eval/tasks/v1/expanded/edge_037/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_037" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_037/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_037/initial/src/lib.rs new file mode 100644 index 0000000..ec7ee3d --- /dev/null +++ b/eval/tasks/v1/expanded/edge_037/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_37(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_37(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_038/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_038/initial/Cargo.toml new file mode 100644 index 0000000..d6c2fca --- /dev/null +++ b/eval/tasks/v1/expanded/edge_038/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_038" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_038/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_038/initial/src/lib.rs new file mode 100644 index 0000000..8c4ffc4 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_038/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_38(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_38(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_039/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_039/initial/Cargo.toml new file mode 100644 index 0000000..cba3935 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_039/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_039" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_039/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_039/initial/src/lib.rs new file mode 100644 index 0000000..00e8a88 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_039/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_39(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_39(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_040/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_040/initial/Cargo.toml new file mode 100644 index 0000000..2cfa6bd --- /dev/null +++ b/eval/tasks/v1/expanded/edge_040/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_040" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_040/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_040/initial/src/lib.rs new file mode 100644 index 0000000..3a0f075 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_040/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_40(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_40(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_041/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_041/initial/Cargo.toml new file mode 100644 index 0000000..2413502 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_041/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_041" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_041/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_041/initial/src/lib.rs new file mode 100644 index 0000000..4bbf014 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_041/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_41(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_41(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_042/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_042/initial/Cargo.toml new file mode 100644 index 0000000..ac23b12 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_042/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_042" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_042/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_042/initial/src/lib.rs new file mode 100644 index 0000000..6afaf80 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_042/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_42(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_42(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_043/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_043/initial/Cargo.toml new file mode 100644 index 0000000..d06cc0d --- /dev/null +++ b/eval/tasks/v1/expanded/edge_043/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_043" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_043/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_043/initial/src/lib.rs new file mode 100644 index 0000000..e91bcae --- /dev/null +++ b/eval/tasks/v1/expanded/edge_043/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_43(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_43(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_044/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_044/initial/Cargo.toml new file mode 100644 index 0000000..363b0ca --- /dev/null +++ b/eval/tasks/v1/expanded/edge_044/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_044" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_044/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_044/initial/src/lib.rs new file mode 100644 index 0000000..4be3730 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_044/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_44(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_44(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_045/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_045/initial/Cargo.toml new file mode 100644 index 0000000..368abdd --- /dev/null +++ b/eval/tasks/v1/expanded/edge_045/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_045" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_045/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_045/initial/src/lib.rs new file mode 100644 index 0000000..21de358 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_045/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_45(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_45(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_046/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_046/initial/Cargo.toml new file mode 100644 index 0000000..69b36fe --- /dev/null +++ b/eval/tasks/v1/expanded/edge_046/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_046" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_046/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_046/initial/src/lib.rs new file mode 100644 index 0000000..b395291 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_046/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_46(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_46(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_047/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_047/initial/Cargo.toml new file mode 100644 index 0000000..32a6fb7 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_047/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_047" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_047/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_047/initial/src/lib.rs new file mode 100644 index 0000000..643d254 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_047/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_47(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_47(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_048/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_048/initial/Cargo.toml new file mode 100644 index 0000000..c9b296e --- /dev/null +++ b/eval/tasks/v1/expanded/edge_048/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_048" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_048/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_048/initial/src/lib.rs new file mode 100644 index 0000000..e680abe --- /dev/null +++ b/eval/tasks/v1/expanded/edge_048/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_48(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_48(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_049/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_049/initial/Cargo.toml new file mode 100644 index 0000000..142fd7e --- /dev/null +++ b/eval/tasks/v1/expanded/edge_049/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_049" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_049/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_049/initial/src/lib.rs new file mode 100644 index 0000000..2458433 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_049/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_49(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_49(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/edge_050/initial/Cargo.toml b/eval/tasks/v1/expanded/edge_050/initial/Cargo.toml new file mode 100644 index 0000000..0e68e3c --- /dev/null +++ b/eval/tasks/v1/expanded/edge_050/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "edge_050" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/edge_050/initial/src/lib.rs b/eval/tasks/v1/expanded/edge_050/initial/src/lib.rs new file mode 100644 index 0000000..fe162d1 --- /dev/null +++ b/eval/tasks/v1/expanded/edge_050/initial/src/lib.rs @@ -0,0 +1,14 @@ +pub fn compute_50(x: i32) -> i32 { + let a = x * 2 + let b = a + 5; + b +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_compute() { + assert_eq!(compute_50(2), 9); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_001/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_001/initial/Cargo.toml new file mode 100644 index 0000000..734b1f2 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_001/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_001" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_001/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_001/initial/src/lib.rs new file mode 100644 index 0000000..90d72e6 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_001/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity1 { + id: u32, + property_1: String, +} + +impl Entity1 { + pub fn new(id: u32, property_1: String) -> Self { + Self { id, property_1 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity1::new(1, "test".to_string()); + assert_eq!(e.get_property_1(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_002/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_002/initial/Cargo.toml new file mode 100644 index 0000000..dfe67f9 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_002/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_002" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_002/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_002/initial/src/lib.rs new file mode 100644 index 0000000..95f0e19 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_002/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity2 { + id: u32, + property_2: String, +} + +impl Entity2 { + pub fn new(id: u32, property_2: String) -> Self { + Self { id, property_2 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity2::new(1, "test".to_string()); + assert_eq!(e.get_property_2(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_003/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_003/initial/Cargo.toml new file mode 100644 index 0000000..194925a --- /dev/null +++ b/eval/tasks/v1/expanded/feature_003/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_003" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_003/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_003/initial/src/lib.rs new file mode 100644 index 0000000..908f359 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_003/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity3 { + id: u32, + property_3: String, +} + +impl Entity3 { + pub fn new(id: u32, property_3: String) -> Self { + Self { id, property_3 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity3::new(1, "test".to_string()); + assert_eq!(e.get_property_3(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_004/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_004/initial/Cargo.toml new file mode 100644 index 0000000..65adb3d --- /dev/null +++ b/eval/tasks/v1/expanded/feature_004/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_004" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_004/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_004/initial/src/lib.rs new file mode 100644 index 0000000..916ac48 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_004/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity4 { + id: u32, + property_4: String, +} + +impl Entity4 { + pub fn new(id: u32, property_4: String) -> Self { + Self { id, property_4 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity4::new(1, "test".to_string()); + assert_eq!(e.get_property_4(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_005/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_005/initial/Cargo.toml new file mode 100644 index 0000000..094d4d4 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_005/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_005" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_005/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_005/initial/src/lib.rs new file mode 100644 index 0000000..b0959ab --- /dev/null +++ b/eval/tasks/v1/expanded/feature_005/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity5 { + id: u32, + property_5: String, +} + +impl Entity5 { + pub fn new(id: u32, property_5: String) -> Self { + Self { id, property_5 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity5::new(1, "test".to_string()); + assert_eq!(e.get_property_5(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_006/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_006/initial/Cargo.toml new file mode 100644 index 0000000..ab2775b --- /dev/null +++ b/eval/tasks/v1/expanded/feature_006/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_006" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_006/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_006/initial/src/lib.rs new file mode 100644 index 0000000..940eb93 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_006/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity6 { + id: u32, + property_6: String, +} + +impl Entity6 { + pub fn new(id: u32, property_6: String) -> Self { + Self { id, property_6 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity6::new(1, "test".to_string()); + assert_eq!(e.get_property_6(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_007/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_007/initial/Cargo.toml new file mode 100644 index 0000000..ae5d482 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_007/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_007" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_007/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_007/initial/src/lib.rs new file mode 100644 index 0000000..33edb1f --- /dev/null +++ b/eval/tasks/v1/expanded/feature_007/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity7 { + id: u32, + property_7: String, +} + +impl Entity7 { + pub fn new(id: u32, property_7: String) -> Self { + Self { id, property_7 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity7::new(1, "test".to_string()); + assert_eq!(e.get_property_7(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_008/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_008/initial/Cargo.toml new file mode 100644 index 0000000..ff822df --- /dev/null +++ b/eval/tasks/v1/expanded/feature_008/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_008" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_008/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_008/initial/src/lib.rs new file mode 100644 index 0000000..d64d125 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_008/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity8 { + id: u32, + property_8: String, +} + +impl Entity8 { + pub fn new(id: u32, property_8: String) -> Self { + Self { id, property_8 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity8::new(1, "test".to_string()); + assert_eq!(e.get_property_8(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_009/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_009/initial/Cargo.toml new file mode 100644 index 0000000..dcac8d9 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_009/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_009" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_009/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_009/initial/src/lib.rs new file mode 100644 index 0000000..10b7f8f --- /dev/null +++ b/eval/tasks/v1/expanded/feature_009/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity9 { + id: u32, + property_9: String, +} + +impl Entity9 { + pub fn new(id: u32, property_9: String) -> Self { + Self { id, property_9 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity9::new(1, "test".to_string()); + assert_eq!(e.get_property_9(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_010/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_010/initial/Cargo.toml new file mode 100644 index 0000000..b95525c --- /dev/null +++ b/eval/tasks/v1/expanded/feature_010/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_010" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_010/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_010/initial/src/lib.rs new file mode 100644 index 0000000..4584d9b --- /dev/null +++ b/eval/tasks/v1/expanded/feature_010/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity10 { + id: u32, + property_10: String, +} + +impl Entity10 { + pub fn new(id: u32, property_10: String) -> Self { + Self { id, property_10 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity10::new(1, "test".to_string()); + assert_eq!(e.get_property_10(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_011/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_011/initial/Cargo.toml new file mode 100644 index 0000000..2422927 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_011/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_011" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_011/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_011/initial/src/lib.rs new file mode 100644 index 0000000..fa24ac0 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_011/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity11 { + id: u32, + property_11: String, +} + +impl Entity11 { + pub fn new(id: u32, property_11: String) -> Self { + Self { id, property_11 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity11::new(1, "test".to_string()); + assert_eq!(e.get_property_11(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_012/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_012/initial/Cargo.toml new file mode 100644 index 0000000..dff1756 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_012/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_012" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_012/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_012/initial/src/lib.rs new file mode 100644 index 0000000..ae7b1e0 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_012/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity12 { + id: u32, + property_12: String, +} + +impl Entity12 { + pub fn new(id: u32, property_12: String) -> Self { + Self { id, property_12 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity12::new(1, "test".to_string()); + assert_eq!(e.get_property_12(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_013/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_013/initial/Cargo.toml new file mode 100644 index 0000000..73e383a --- /dev/null +++ b/eval/tasks/v1/expanded/feature_013/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_013" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_013/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_013/initial/src/lib.rs new file mode 100644 index 0000000..27d5f4a --- /dev/null +++ b/eval/tasks/v1/expanded/feature_013/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity13 { + id: u32, + property_13: String, +} + +impl Entity13 { + pub fn new(id: u32, property_13: String) -> Self { + Self { id, property_13 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity13::new(1, "test".to_string()); + assert_eq!(e.get_property_13(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_014/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_014/initial/Cargo.toml new file mode 100644 index 0000000..08af5f3 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_014/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_014" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_014/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_014/initial/src/lib.rs new file mode 100644 index 0000000..a3027d9 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_014/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity14 { + id: u32, + property_14: String, +} + +impl Entity14 { + pub fn new(id: u32, property_14: String) -> Self { + Self { id, property_14 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity14::new(1, "test".to_string()); + assert_eq!(e.get_property_14(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_015/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_015/initial/Cargo.toml new file mode 100644 index 0000000..dd7239a --- /dev/null +++ b/eval/tasks/v1/expanded/feature_015/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_015" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_015/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_015/initial/src/lib.rs new file mode 100644 index 0000000..26cd10f --- /dev/null +++ b/eval/tasks/v1/expanded/feature_015/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity15 { + id: u32, + property_15: String, +} + +impl Entity15 { + pub fn new(id: u32, property_15: String) -> Self { + Self { id, property_15 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity15::new(1, "test".to_string()); + assert_eq!(e.get_property_15(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_016/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_016/initial/Cargo.toml new file mode 100644 index 0000000..2b11305 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_016/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_016" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_016/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_016/initial/src/lib.rs new file mode 100644 index 0000000..fdf0370 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_016/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity16 { + id: u32, + property_16: String, +} + +impl Entity16 { + pub fn new(id: u32, property_16: String) -> Self { + Self { id, property_16 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity16::new(1, "test".to_string()); + assert_eq!(e.get_property_16(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_017/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_017/initial/Cargo.toml new file mode 100644 index 0000000..9cef2da --- /dev/null +++ b/eval/tasks/v1/expanded/feature_017/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_017" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_017/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_017/initial/src/lib.rs new file mode 100644 index 0000000..5036baf --- /dev/null +++ b/eval/tasks/v1/expanded/feature_017/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity17 { + id: u32, + property_17: String, +} + +impl Entity17 { + pub fn new(id: u32, property_17: String) -> Self { + Self { id, property_17 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity17::new(1, "test".to_string()); + assert_eq!(e.get_property_17(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_018/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_018/initial/Cargo.toml new file mode 100644 index 0000000..14e9b78 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_018/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_018" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_018/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_018/initial/src/lib.rs new file mode 100644 index 0000000..a2ed737 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_018/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity18 { + id: u32, + property_18: String, +} + +impl Entity18 { + pub fn new(id: u32, property_18: String) -> Self { + Self { id, property_18 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity18::new(1, "test".to_string()); + assert_eq!(e.get_property_18(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_019/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_019/initial/Cargo.toml new file mode 100644 index 0000000..6397ba1 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_019/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_019" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_019/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_019/initial/src/lib.rs new file mode 100644 index 0000000..c6df409 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_019/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity19 { + id: u32, + property_19: String, +} + +impl Entity19 { + pub fn new(id: u32, property_19: String) -> Self { + Self { id, property_19 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity19::new(1, "test".to_string()); + assert_eq!(e.get_property_19(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_020/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_020/initial/Cargo.toml new file mode 100644 index 0000000..74c7624 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_020/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_020" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_020/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_020/initial/src/lib.rs new file mode 100644 index 0000000..874eeb0 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_020/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity20 { + id: u32, + property_20: String, +} + +impl Entity20 { + pub fn new(id: u32, property_20: String) -> Self { + Self { id, property_20 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity20::new(1, "test".to_string()); + assert_eq!(e.get_property_20(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_021/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_021/initial/Cargo.toml new file mode 100644 index 0000000..a351cc4 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_021/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_021" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_021/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_021/initial/src/lib.rs new file mode 100644 index 0000000..0950534 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_021/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity21 { + id: u32, + property_21: String, +} + +impl Entity21 { + pub fn new(id: u32, property_21: String) -> Self { + Self { id, property_21 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity21::new(1, "test".to_string()); + assert_eq!(e.get_property_21(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_022/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_022/initial/Cargo.toml new file mode 100644 index 0000000..46bc7e1 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_022/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_022" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_022/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_022/initial/src/lib.rs new file mode 100644 index 0000000..68643ae --- /dev/null +++ b/eval/tasks/v1/expanded/feature_022/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity22 { + id: u32, + property_22: String, +} + +impl Entity22 { + pub fn new(id: u32, property_22: String) -> Self { + Self { id, property_22 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity22::new(1, "test".to_string()); + assert_eq!(e.get_property_22(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_023/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_023/initial/Cargo.toml new file mode 100644 index 0000000..dc63098 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_023/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_023" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_023/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_023/initial/src/lib.rs new file mode 100644 index 0000000..7a5269a --- /dev/null +++ b/eval/tasks/v1/expanded/feature_023/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity23 { + id: u32, + property_23: String, +} + +impl Entity23 { + pub fn new(id: u32, property_23: String) -> Self { + Self { id, property_23 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity23::new(1, "test".to_string()); + assert_eq!(e.get_property_23(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_024/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_024/initial/Cargo.toml new file mode 100644 index 0000000..ee283f6 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_024/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_024" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_024/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_024/initial/src/lib.rs new file mode 100644 index 0000000..f642119 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_024/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity24 { + id: u32, + property_24: String, +} + +impl Entity24 { + pub fn new(id: u32, property_24: String) -> Self { + Self { id, property_24 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity24::new(1, "test".to_string()); + assert_eq!(e.get_property_24(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_025/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_025/initial/Cargo.toml new file mode 100644 index 0000000..df6288c --- /dev/null +++ b/eval/tasks/v1/expanded/feature_025/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_025" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_025/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_025/initial/src/lib.rs new file mode 100644 index 0000000..44991bb --- /dev/null +++ b/eval/tasks/v1/expanded/feature_025/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity25 { + id: u32, + property_25: String, +} + +impl Entity25 { + pub fn new(id: u32, property_25: String) -> Self { + Self { id, property_25 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity25::new(1, "test".to_string()); + assert_eq!(e.get_property_25(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_026/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_026/initial/Cargo.toml new file mode 100644 index 0000000..2168b7b --- /dev/null +++ b/eval/tasks/v1/expanded/feature_026/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_026" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_026/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_026/initial/src/lib.rs new file mode 100644 index 0000000..5febd1e --- /dev/null +++ b/eval/tasks/v1/expanded/feature_026/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity26 { + id: u32, + property_26: String, +} + +impl Entity26 { + pub fn new(id: u32, property_26: String) -> Self { + Self { id, property_26 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity26::new(1, "test".to_string()); + assert_eq!(e.get_property_26(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_027/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_027/initial/Cargo.toml new file mode 100644 index 0000000..c860df8 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_027/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_027" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_027/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_027/initial/src/lib.rs new file mode 100644 index 0000000..f95e365 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_027/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity27 { + id: u32, + property_27: String, +} + +impl Entity27 { + pub fn new(id: u32, property_27: String) -> Self { + Self { id, property_27 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity27::new(1, "test".to_string()); + assert_eq!(e.get_property_27(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_028/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_028/initial/Cargo.toml new file mode 100644 index 0000000..9fada77 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_028/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_028" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_028/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_028/initial/src/lib.rs new file mode 100644 index 0000000..020e35f --- /dev/null +++ b/eval/tasks/v1/expanded/feature_028/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity28 { + id: u32, + property_28: String, +} + +impl Entity28 { + pub fn new(id: u32, property_28: String) -> Self { + Self { id, property_28 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity28::new(1, "test".to_string()); + assert_eq!(e.get_property_28(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_029/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_029/initial/Cargo.toml new file mode 100644 index 0000000..af3755e --- /dev/null +++ b/eval/tasks/v1/expanded/feature_029/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_029" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_029/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_029/initial/src/lib.rs new file mode 100644 index 0000000..a7a6724 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_029/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity29 { + id: u32, + property_29: String, +} + +impl Entity29 { + pub fn new(id: u32, property_29: String) -> Self { + Self { id, property_29 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity29::new(1, "test".to_string()); + assert_eq!(e.get_property_29(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_030/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_030/initial/Cargo.toml new file mode 100644 index 0000000..6df9821 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_030/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_030" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_030/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_030/initial/src/lib.rs new file mode 100644 index 0000000..43def25 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_030/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity30 { + id: u32, + property_30: String, +} + +impl Entity30 { + pub fn new(id: u32, property_30: String) -> Self { + Self { id, property_30 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity30::new(1, "test".to_string()); + assert_eq!(e.get_property_30(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_031/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_031/initial/Cargo.toml new file mode 100644 index 0000000..f1c4300 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_031/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_031" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_031/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_031/initial/src/lib.rs new file mode 100644 index 0000000..21b69a1 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_031/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity31 { + id: u32, + property_31: String, +} + +impl Entity31 { + pub fn new(id: u32, property_31: String) -> Self { + Self { id, property_31 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity31::new(1, "test".to_string()); + assert_eq!(e.get_property_31(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_032/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_032/initial/Cargo.toml new file mode 100644 index 0000000..80bb860 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_032/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_032" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_032/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_032/initial/src/lib.rs new file mode 100644 index 0000000..d10a2f4 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_032/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity32 { + id: u32, + property_32: String, +} + +impl Entity32 { + pub fn new(id: u32, property_32: String) -> Self { + Self { id, property_32 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity32::new(1, "test".to_string()); + assert_eq!(e.get_property_32(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_033/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_033/initial/Cargo.toml new file mode 100644 index 0000000..ecc7688 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_033/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_033" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_033/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_033/initial/src/lib.rs new file mode 100644 index 0000000..8bf6aaf --- /dev/null +++ b/eval/tasks/v1/expanded/feature_033/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity33 { + id: u32, + property_33: String, +} + +impl Entity33 { + pub fn new(id: u32, property_33: String) -> Self { + Self { id, property_33 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity33::new(1, "test".to_string()); + assert_eq!(e.get_property_33(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_034/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_034/initial/Cargo.toml new file mode 100644 index 0000000..82c6eee --- /dev/null +++ b/eval/tasks/v1/expanded/feature_034/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_034" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_034/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_034/initial/src/lib.rs new file mode 100644 index 0000000..d2f0e3b --- /dev/null +++ b/eval/tasks/v1/expanded/feature_034/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity34 { + id: u32, + property_34: String, +} + +impl Entity34 { + pub fn new(id: u32, property_34: String) -> Self { + Self { id, property_34 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity34::new(1, "test".to_string()); + assert_eq!(e.get_property_34(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_035/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_035/initial/Cargo.toml new file mode 100644 index 0000000..f4acb6c --- /dev/null +++ b/eval/tasks/v1/expanded/feature_035/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_035" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_035/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_035/initial/src/lib.rs new file mode 100644 index 0000000..6cdc46e --- /dev/null +++ b/eval/tasks/v1/expanded/feature_035/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity35 { + id: u32, + property_35: String, +} + +impl Entity35 { + pub fn new(id: u32, property_35: String) -> Self { + Self { id, property_35 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity35::new(1, "test".to_string()); + assert_eq!(e.get_property_35(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_036/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_036/initial/Cargo.toml new file mode 100644 index 0000000..ce5be63 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_036/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_036" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_036/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_036/initial/src/lib.rs new file mode 100644 index 0000000..d886f59 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_036/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity36 { + id: u32, + property_36: String, +} + +impl Entity36 { + pub fn new(id: u32, property_36: String) -> Self { + Self { id, property_36 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity36::new(1, "test".to_string()); + assert_eq!(e.get_property_36(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_037/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_037/initial/Cargo.toml new file mode 100644 index 0000000..474796a --- /dev/null +++ b/eval/tasks/v1/expanded/feature_037/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_037" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_037/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_037/initial/src/lib.rs new file mode 100644 index 0000000..3b48842 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_037/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity37 { + id: u32, + property_37: String, +} + +impl Entity37 { + pub fn new(id: u32, property_37: String) -> Self { + Self { id, property_37 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity37::new(1, "test".to_string()); + assert_eq!(e.get_property_37(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_038/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_038/initial/Cargo.toml new file mode 100644 index 0000000..9f967cb --- /dev/null +++ b/eval/tasks/v1/expanded/feature_038/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_038" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_038/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_038/initial/src/lib.rs new file mode 100644 index 0000000..bb275c4 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_038/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity38 { + id: u32, + property_38: String, +} + +impl Entity38 { + pub fn new(id: u32, property_38: String) -> Self { + Self { id, property_38 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity38::new(1, "test".to_string()); + assert_eq!(e.get_property_38(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_039/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_039/initial/Cargo.toml new file mode 100644 index 0000000..2e577d9 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_039/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_039" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_039/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_039/initial/src/lib.rs new file mode 100644 index 0000000..f1f39c1 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_039/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity39 { + id: u32, + property_39: String, +} + +impl Entity39 { + pub fn new(id: u32, property_39: String) -> Self { + Self { id, property_39 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity39::new(1, "test".to_string()); + assert_eq!(e.get_property_39(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_040/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_040/initial/Cargo.toml new file mode 100644 index 0000000..0fa2287 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_040/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_040" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_040/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_040/initial/src/lib.rs new file mode 100644 index 0000000..c3613f7 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_040/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity40 { + id: u32, + property_40: String, +} + +impl Entity40 { + pub fn new(id: u32, property_40: String) -> Self { + Self { id, property_40 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity40::new(1, "test".to_string()); + assert_eq!(e.get_property_40(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_041/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_041/initial/Cargo.toml new file mode 100644 index 0000000..81ddbb2 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_041/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_041" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_041/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_041/initial/src/lib.rs new file mode 100644 index 0000000..f1abde3 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_041/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity41 { + id: u32, + property_41: String, +} + +impl Entity41 { + pub fn new(id: u32, property_41: String) -> Self { + Self { id, property_41 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity41::new(1, "test".to_string()); + assert_eq!(e.get_property_41(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_042/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_042/initial/Cargo.toml new file mode 100644 index 0000000..9ad8ced --- /dev/null +++ b/eval/tasks/v1/expanded/feature_042/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_042" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_042/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_042/initial/src/lib.rs new file mode 100644 index 0000000..29147b8 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_042/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity42 { + id: u32, + property_42: String, +} + +impl Entity42 { + pub fn new(id: u32, property_42: String) -> Self { + Self { id, property_42 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity42::new(1, "test".to_string()); + assert_eq!(e.get_property_42(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_043/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_043/initial/Cargo.toml new file mode 100644 index 0000000..98bad89 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_043/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_043" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_043/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_043/initial/src/lib.rs new file mode 100644 index 0000000..e0c4a83 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_043/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity43 { + id: u32, + property_43: String, +} + +impl Entity43 { + pub fn new(id: u32, property_43: String) -> Self { + Self { id, property_43 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity43::new(1, "test".to_string()); + assert_eq!(e.get_property_43(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_044/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_044/initial/Cargo.toml new file mode 100644 index 0000000..2e9c9ca --- /dev/null +++ b/eval/tasks/v1/expanded/feature_044/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_044" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_044/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_044/initial/src/lib.rs new file mode 100644 index 0000000..54cfb51 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_044/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity44 { + id: u32, + property_44: String, +} + +impl Entity44 { + pub fn new(id: u32, property_44: String) -> Self { + Self { id, property_44 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity44::new(1, "test".to_string()); + assert_eq!(e.get_property_44(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_045/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_045/initial/Cargo.toml new file mode 100644 index 0000000..1bf2c4c --- /dev/null +++ b/eval/tasks/v1/expanded/feature_045/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_045" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_045/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_045/initial/src/lib.rs new file mode 100644 index 0000000..facc425 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_045/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity45 { + id: u32, + property_45: String, +} + +impl Entity45 { + pub fn new(id: u32, property_45: String) -> Self { + Self { id, property_45 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity45::new(1, "test".to_string()); + assert_eq!(e.get_property_45(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_046/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_046/initial/Cargo.toml new file mode 100644 index 0000000..dfd9285 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_046/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_046" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_046/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_046/initial/src/lib.rs new file mode 100644 index 0000000..b786371 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_046/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity46 { + id: u32, + property_46: String, +} + +impl Entity46 { + pub fn new(id: u32, property_46: String) -> Self { + Self { id, property_46 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity46::new(1, "test".to_string()); + assert_eq!(e.get_property_46(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_047/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_047/initial/Cargo.toml new file mode 100644 index 0000000..24def19 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_047/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_047" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_047/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_047/initial/src/lib.rs new file mode 100644 index 0000000..c112958 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_047/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity47 { + id: u32, + property_47: String, +} + +impl Entity47 { + pub fn new(id: u32, property_47: String) -> Self { + Self { id, property_47 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity47::new(1, "test".to_string()); + assert_eq!(e.get_property_47(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_048/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_048/initial/Cargo.toml new file mode 100644 index 0000000..08451e8 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_048/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_048" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_048/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_048/initial/src/lib.rs new file mode 100644 index 0000000..e550286 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_048/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity48 { + id: u32, + property_48: String, +} + +impl Entity48 { + pub fn new(id: u32, property_48: String) -> Self { + Self { id, property_48 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity48::new(1, "test".to_string()); + assert_eq!(e.get_property_48(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_049/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_049/initial/Cargo.toml new file mode 100644 index 0000000..4f6d8c7 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_049/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_049" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_049/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_049/initial/src/lib.rs new file mode 100644 index 0000000..e7c5028 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_049/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity49 { + id: u32, + property_49: String, +} + +impl Entity49 { + pub fn new(id: u32, property_49: String) -> Self { + Self { id, property_49 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity49::new(1, "test".to_string()); + assert_eq!(e.get_property_49(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/feature_050/initial/Cargo.toml b/eval/tasks/v1/expanded/feature_050/initial/Cargo.toml new file mode 100644 index 0000000..1527723 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_050/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "feature_050" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/feature_050/initial/src/lib.rs b/eval/tasks/v1/expanded/feature_050/initial/src/lib.rs new file mode 100644 index 0000000..5d97a12 --- /dev/null +++ b/eval/tasks/v1/expanded/feature_050/initial/src/lib.rs @@ -0,0 +1,20 @@ +pub struct Entity50 { + id: u32, + property_50: String, +} + +impl Entity50 { + pub fn new(id: u32, property_50: String) -> Self { + Self { id, property_50 } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_getter() { + let e = Entity50::new(1, "test".to_string()); + assert_eq!(e.get_property_50(), "test"); + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_001/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_001/initial/Cargo.toml new file mode 100644 index 0000000..e70097c --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_001/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_001" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_001/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_001/initial/src/lib.rs new file mode 100644 index 0000000..7672af2 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_001/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_values(values: &[i32]) -> i32 { + let mut sum = 0; + for v in values { + sum += v; + } + let avg = if values.is_empty() { 0 } else { sum / values.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in values { + result += (v - avg) * 1; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_values(&[1, 2, 3]), process_values(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_002/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_002/initial/Cargo.toml new file mode 100644 index 0000000..e1c0713 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_002/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_002" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_002/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_002/initial/src/lib.rs new file mode 100644 index 0000000..380566a --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_002/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_numbers(numbers: &[i32]) -> i32 { + let mut sum = 0; + for v in numbers { + sum += v; + } + let avg = if numbers.is_empty() { 0 } else { sum / numbers.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in numbers { + result += (v - avg) * 2; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_numbers(&[1, 2, 3]), process_numbers(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_003/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_003/initial/Cargo.toml new file mode 100644 index 0000000..1e4a80b --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_003/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_003" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_003/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_003/initial/src/lib.rs new file mode 100644 index 0000000..32fc1e7 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_003/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_measurements(measurements: &[i32]) -> i32 { + let mut sum = 0; + for v in measurements { + sum += v; + } + let avg = if measurements.is_empty() { 0 } else { sum / measurements.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in measurements { + result += (v - avg) * 3; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_measurements(&[1, 2, 3]), process_measurements(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_004/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_004/initial/Cargo.toml new file mode 100644 index 0000000..cb8e7e7 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_004/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_004" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_004/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_004/initial/src/lib.rs new file mode 100644 index 0000000..1afddd1 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_004/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_items(items: &[i32]) -> i32 { + let mut sum = 0; + for v in items { + sum += v; + } + let avg = if items.is_empty() { 0 } else { sum / items.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in items { + result += (v - avg) * 4; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_items(&[1, 2, 3]), process_items(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_005/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_005/initial/Cargo.toml new file mode 100644 index 0000000..972b278 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_005/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_005" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_005/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_005/initial/src/lib.rs new file mode 100644 index 0000000..2062cb0 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_005/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_numbers(numbers: &[i32]) -> i32 { + let mut sum = 0; + for v in numbers { + sum += v; + } + let avg = if numbers.is_empty() { 0 } else { sum / numbers.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in numbers { + result += (v - avg) * 5; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_numbers(&[1, 2, 3]), process_numbers(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_006/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_006/initial/Cargo.toml new file mode 100644 index 0000000..ab13282 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_006/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_006" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_006/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_006/initial/src/lib.rs new file mode 100644 index 0000000..cff113d --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_006/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_items(items: &[i32]) -> i32 { + let mut sum = 0; + for v in items { + sum += v; + } + let avg = if items.is_empty() { 0 } else { sum / items.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in items { + result += (v - avg) * 6; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_items(&[1, 2, 3]), process_items(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_007/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_007/initial/Cargo.toml new file mode 100644 index 0000000..a219b31 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_007/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_007" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_007/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_007/initial/src/lib.rs new file mode 100644 index 0000000..c1daf60 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_007/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_measurements(measurements: &[i32]) -> i32 { + let mut sum = 0; + for v in measurements { + sum += v; + } + let avg = if measurements.is_empty() { 0 } else { sum / measurements.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in measurements { + result += (v - avg) * 7; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_measurements(&[1, 2, 3]), process_measurements(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_008/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_008/initial/Cargo.toml new file mode 100644 index 0000000..c3dbe84 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_008/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_008" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_008/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_008/initial/src/lib.rs new file mode 100644 index 0000000..147d242 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_008/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_items(items: &[i32]) -> i32 { + let mut sum = 0; + for v in items { + sum += v; + } + let avg = if items.is_empty() { 0 } else { sum / items.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in items { + result += (v - avg) * 8; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_items(&[1, 2, 3]), process_items(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_009/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_009/initial/Cargo.toml new file mode 100644 index 0000000..12e66bc --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_009/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_009" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_009/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_009/initial/src/lib.rs new file mode 100644 index 0000000..dd34fd4 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_009/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_numbers(numbers: &[i32]) -> i32 { + let mut sum = 0; + for v in numbers { + sum += v; + } + let avg = if numbers.is_empty() { 0 } else { sum / numbers.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in numbers { + result += (v - avg) * 9; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_numbers(&[1, 2, 3]), process_numbers(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_010/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_010/initial/Cargo.toml new file mode 100644 index 0000000..8edec10 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_010/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_010" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_010/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_010/initial/src/lib.rs new file mode 100644 index 0000000..ae35583 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_010/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_numbers(numbers: &[i32]) -> i32 { + let mut sum = 0; + for v in numbers { + sum += v; + } + let avg = if numbers.is_empty() { 0 } else { sum / numbers.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in numbers { + result += (v - avg) * 10; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_numbers(&[1, 2, 3]), process_numbers(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_011/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_011/initial/Cargo.toml new file mode 100644 index 0000000..0a05c72 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_011/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_011" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_011/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_011/initial/src/lib.rs new file mode 100644 index 0000000..4c21e71 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_011/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_measurements(measurements: &[i32]) -> i32 { + let mut sum = 0; + for v in measurements { + sum += v; + } + let avg = if measurements.is_empty() { 0 } else { sum / measurements.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in measurements { + result += (v - avg) * 11; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_measurements(&[1, 2, 3]), process_measurements(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_012/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_012/initial/Cargo.toml new file mode 100644 index 0000000..a9a3802 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_012/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_012" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_012/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_012/initial/src/lib.rs new file mode 100644 index 0000000..d8598c5 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_012/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_values(values: &[i32]) -> i32 { + let mut sum = 0; + for v in values { + sum += v; + } + let avg = if values.is_empty() { 0 } else { sum / values.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in values { + result += (v - avg) * 12; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_values(&[1, 2, 3]), process_values(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_013/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_013/initial/Cargo.toml new file mode 100644 index 0000000..e0f4793 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_013/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_013" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_013/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_013/initial/src/lib.rs new file mode 100644 index 0000000..f8223c8 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_013/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_measurements(measurements: &[i32]) -> i32 { + let mut sum = 0; + for v in measurements { + sum += v; + } + let avg = if measurements.is_empty() { 0 } else { sum / measurements.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in measurements { + result += (v - avg) * 13; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_measurements(&[1, 2, 3]), process_measurements(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_014/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_014/initial/Cargo.toml new file mode 100644 index 0000000..310b0fb --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_014/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_014" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_014/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_014/initial/src/lib.rs new file mode 100644 index 0000000..d53bab7 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_014/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_items(items: &[i32]) -> i32 { + let mut sum = 0; + for v in items { + sum += v; + } + let avg = if items.is_empty() { 0 } else { sum / items.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in items { + result += (v - avg) * 14; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_items(&[1, 2, 3]), process_items(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_015/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_015/initial/Cargo.toml new file mode 100644 index 0000000..ebe9348 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_015/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_015" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_015/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_015/initial/src/lib.rs new file mode 100644 index 0000000..c454510 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_015/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_items(items: &[i32]) -> i32 { + let mut sum = 0; + for v in items { + sum += v; + } + let avg = if items.is_empty() { 0 } else { sum / items.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in items { + result += (v - avg) * 15; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_items(&[1, 2, 3]), process_items(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_016/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_016/initial/Cargo.toml new file mode 100644 index 0000000..140d4e4 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_016/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_016" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_016/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_016/initial/src/lib.rs new file mode 100644 index 0000000..44c0960 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_016/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_items(items: &[i32]) -> i32 { + let mut sum = 0; + for v in items { + sum += v; + } + let avg = if items.is_empty() { 0 } else { sum / items.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in items { + result += (v - avg) * 16; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_items(&[1, 2, 3]), process_items(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_017/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_017/initial/Cargo.toml new file mode 100644 index 0000000..a0c8e34 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_017/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_017" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_017/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_017/initial/src/lib.rs new file mode 100644 index 0000000..4c6f82a --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_017/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_numbers(numbers: &[i32]) -> i32 { + let mut sum = 0; + for v in numbers { + sum += v; + } + let avg = if numbers.is_empty() { 0 } else { sum / numbers.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in numbers { + result += (v - avg) * 17; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_numbers(&[1, 2, 3]), process_numbers(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_018/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_018/initial/Cargo.toml new file mode 100644 index 0000000..3765bf5 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_018/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_018" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_018/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_018/initial/src/lib.rs new file mode 100644 index 0000000..0f9f1f9 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_018/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_numbers(numbers: &[i32]) -> i32 { + let mut sum = 0; + for v in numbers { + sum += v; + } + let avg = if numbers.is_empty() { 0 } else { sum / numbers.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in numbers { + result += (v - avg) * 18; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_numbers(&[1, 2, 3]), process_numbers(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_019/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_019/initial/Cargo.toml new file mode 100644 index 0000000..27019b8 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_019/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_019" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_019/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_019/initial/src/lib.rs new file mode 100644 index 0000000..f3ae4a8 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_019/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_values(values: &[i32]) -> i32 { + let mut sum = 0; + for v in values { + sum += v; + } + let avg = if values.is_empty() { 0 } else { sum / values.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in values { + result += (v - avg) * 19; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_values(&[1, 2, 3]), process_values(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_020/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_020/initial/Cargo.toml new file mode 100644 index 0000000..2f85b4c --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_020/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_020" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_020/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_020/initial/src/lib.rs new file mode 100644 index 0000000..fda313d --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_020/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_values(values: &[i32]) -> i32 { + let mut sum = 0; + for v in values { + sum += v; + } + let avg = if values.is_empty() { 0 } else { sum / values.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in values { + result += (v - avg) * 20; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_values(&[1, 2, 3]), process_values(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_021/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_021/initial/Cargo.toml new file mode 100644 index 0000000..fb1a636 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_021/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_021" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_021/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_021/initial/src/lib.rs new file mode 100644 index 0000000..41fcbeb --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_021/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_items(items: &[i32]) -> i32 { + let mut sum = 0; + for v in items { + sum += v; + } + let avg = if items.is_empty() { 0 } else { sum / items.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in items { + result += (v - avg) * 21; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_items(&[1, 2, 3]), process_items(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_022/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_022/initial/Cargo.toml new file mode 100644 index 0000000..19caf63 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_022/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_022" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_022/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_022/initial/src/lib.rs new file mode 100644 index 0000000..eb3e029 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_022/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_items(items: &[i32]) -> i32 { + let mut sum = 0; + for v in items { + sum += v; + } + let avg = if items.is_empty() { 0 } else { sum / items.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in items { + result += (v - avg) * 22; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_items(&[1, 2, 3]), process_items(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_023/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_023/initial/Cargo.toml new file mode 100644 index 0000000..b01e7d0 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_023/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_023" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_023/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_023/initial/src/lib.rs new file mode 100644 index 0000000..596f454 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_023/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_values(values: &[i32]) -> i32 { + let mut sum = 0; + for v in values { + sum += v; + } + let avg = if values.is_empty() { 0 } else { sum / values.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in values { + result += (v - avg) * 23; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_values(&[1, 2, 3]), process_values(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_024/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_024/initial/Cargo.toml new file mode 100644 index 0000000..f8dcc52 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_024/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_024" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_024/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_024/initial/src/lib.rs new file mode 100644 index 0000000..8c6fd48 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_024/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_numbers(numbers: &[i32]) -> i32 { + let mut sum = 0; + for v in numbers { + sum += v; + } + let avg = if numbers.is_empty() { 0 } else { sum / numbers.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in numbers { + result += (v - avg) * 24; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_numbers(&[1, 2, 3]), process_numbers(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_025/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_025/initial/Cargo.toml new file mode 100644 index 0000000..9764f02 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_025/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_025" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_025/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_025/initial/src/lib.rs new file mode 100644 index 0000000..5a664d7 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_025/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_data(data: &[i32]) -> i32 { + let mut sum = 0; + for v in data { + sum += v; + } + let avg = if data.is_empty() { 0 } else { sum / data.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in data { + result += (v - avg) * 25; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_data(&[1, 2, 3]), process_data(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_026/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_026/initial/Cargo.toml new file mode 100644 index 0000000..1221334 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_026/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_026" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_026/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_026/initial/src/lib.rs new file mode 100644 index 0000000..82c2c0e --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_026/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_data(data: &[i32]) -> i32 { + let mut sum = 0; + for v in data { + sum += v; + } + let avg = if data.is_empty() { 0 } else { sum / data.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in data { + result += (v - avg) * 26; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_data(&[1, 2, 3]), process_data(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_027/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_027/initial/Cargo.toml new file mode 100644 index 0000000..3bd712c --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_027/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_027" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_027/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_027/initial/src/lib.rs new file mode 100644 index 0000000..107a2aa --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_027/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_measurements(measurements: &[i32]) -> i32 { + let mut sum = 0; + for v in measurements { + sum += v; + } + let avg = if measurements.is_empty() { 0 } else { sum / measurements.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in measurements { + result += (v - avg) * 27; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_measurements(&[1, 2, 3]), process_measurements(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_028/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_028/initial/Cargo.toml new file mode 100644 index 0000000..8af220c --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_028/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_028" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_028/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_028/initial/src/lib.rs new file mode 100644 index 0000000..d4e9bf1 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_028/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_values(values: &[i32]) -> i32 { + let mut sum = 0; + for v in values { + sum += v; + } + let avg = if values.is_empty() { 0 } else { sum / values.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in values { + result += (v - avg) * 28; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_values(&[1, 2, 3]), process_values(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_029/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_029/initial/Cargo.toml new file mode 100644 index 0000000..a52cdf8 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_029/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_029" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_029/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_029/initial/src/lib.rs new file mode 100644 index 0000000..6500ef0 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_029/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_values(values: &[i32]) -> i32 { + let mut sum = 0; + for v in values { + sum += v; + } + let avg = if values.is_empty() { 0 } else { sum / values.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in values { + result += (v - avg) * 29; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_values(&[1, 2, 3]), process_values(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_030/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_030/initial/Cargo.toml new file mode 100644 index 0000000..c8b0c13 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_030/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_030" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_030/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_030/initial/src/lib.rs new file mode 100644 index 0000000..7419681 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_030/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_measurements(measurements: &[i32]) -> i32 { + let mut sum = 0; + for v in measurements { + sum += v; + } + let avg = if measurements.is_empty() { 0 } else { sum / measurements.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in measurements { + result += (v - avg) * 30; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_measurements(&[1, 2, 3]), process_measurements(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_031/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_031/initial/Cargo.toml new file mode 100644 index 0000000..5967e2c --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_031/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_031" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_031/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_031/initial/src/lib.rs new file mode 100644 index 0000000..f030fab --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_031/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_numbers(numbers: &[i32]) -> i32 { + let mut sum = 0; + for v in numbers { + sum += v; + } + let avg = if numbers.is_empty() { 0 } else { sum / numbers.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in numbers { + result += (v - avg) * 31; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_numbers(&[1, 2, 3]), process_numbers(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_032/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_032/initial/Cargo.toml new file mode 100644 index 0000000..6882b1d --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_032/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_032" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_032/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_032/initial/src/lib.rs new file mode 100644 index 0000000..255f94b --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_032/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_items(items: &[i32]) -> i32 { + let mut sum = 0; + for v in items { + sum += v; + } + let avg = if items.is_empty() { 0 } else { sum / items.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in items { + result += (v - avg) * 32; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_items(&[1, 2, 3]), process_items(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_033/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_033/initial/Cargo.toml new file mode 100644 index 0000000..5222644 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_033/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_033" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_033/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_033/initial/src/lib.rs new file mode 100644 index 0000000..0094834 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_033/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_measurements(measurements: &[i32]) -> i32 { + let mut sum = 0; + for v in measurements { + sum += v; + } + let avg = if measurements.is_empty() { 0 } else { sum / measurements.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in measurements { + result += (v - avg) * 33; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_measurements(&[1, 2, 3]), process_measurements(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_034/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_034/initial/Cargo.toml new file mode 100644 index 0000000..07b4245 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_034/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_034" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_034/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_034/initial/src/lib.rs new file mode 100644 index 0000000..76f38ca --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_034/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_numbers(numbers: &[i32]) -> i32 { + let mut sum = 0; + for v in numbers { + sum += v; + } + let avg = if numbers.is_empty() { 0 } else { sum / numbers.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in numbers { + result += (v - avg) * 34; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_numbers(&[1, 2, 3]), process_numbers(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_035/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_035/initial/Cargo.toml new file mode 100644 index 0000000..7204d6b --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_035/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_035" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_035/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_035/initial/src/lib.rs new file mode 100644 index 0000000..1c9e3b6 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_035/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_numbers(numbers: &[i32]) -> i32 { + let mut sum = 0; + for v in numbers { + sum += v; + } + let avg = if numbers.is_empty() { 0 } else { sum / numbers.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in numbers { + result += (v - avg) * 35; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_numbers(&[1, 2, 3]), process_numbers(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_036/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_036/initial/Cargo.toml new file mode 100644 index 0000000..d9228c9 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_036/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_036" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_036/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_036/initial/src/lib.rs new file mode 100644 index 0000000..b37aa6c --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_036/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_numbers(numbers: &[i32]) -> i32 { + let mut sum = 0; + for v in numbers { + sum += v; + } + let avg = if numbers.is_empty() { 0 } else { sum / numbers.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in numbers { + result += (v - avg) * 36; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_numbers(&[1, 2, 3]), process_numbers(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_037/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_037/initial/Cargo.toml new file mode 100644 index 0000000..e22b116 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_037/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_037" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_037/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_037/initial/src/lib.rs new file mode 100644 index 0000000..222acc8 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_037/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_numbers(numbers: &[i32]) -> i32 { + let mut sum = 0; + for v in numbers { + sum += v; + } + let avg = if numbers.is_empty() { 0 } else { sum / numbers.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in numbers { + result += (v - avg) * 37; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_numbers(&[1, 2, 3]), process_numbers(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_038/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_038/initial/Cargo.toml new file mode 100644 index 0000000..c8ff0b3 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_038/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_038" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_038/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_038/initial/src/lib.rs new file mode 100644 index 0000000..43fff44 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_038/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_measurements(measurements: &[i32]) -> i32 { + let mut sum = 0; + for v in measurements { + sum += v; + } + let avg = if measurements.is_empty() { 0 } else { sum / measurements.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in measurements { + result += (v - avg) * 38; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_measurements(&[1, 2, 3]), process_measurements(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_039/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_039/initial/Cargo.toml new file mode 100644 index 0000000..413d740 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_039/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_039" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_039/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_039/initial/src/lib.rs new file mode 100644 index 0000000..bf14c09 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_039/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_values(values: &[i32]) -> i32 { + let mut sum = 0; + for v in values { + sum += v; + } + let avg = if values.is_empty() { 0 } else { sum / values.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in values { + result += (v - avg) * 39; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_values(&[1, 2, 3]), process_values(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_040/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_040/initial/Cargo.toml new file mode 100644 index 0000000..fb0709d --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_040/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_040" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_040/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_040/initial/src/lib.rs new file mode 100644 index 0000000..39d18ac --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_040/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_values(values: &[i32]) -> i32 { + let mut sum = 0; + for v in values { + sum += v; + } + let avg = if values.is_empty() { 0 } else { sum / values.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in values { + result += (v - avg) * 40; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_values(&[1, 2, 3]), process_values(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_041/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_041/initial/Cargo.toml new file mode 100644 index 0000000..ea16642 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_041/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_041" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_041/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_041/initial/src/lib.rs new file mode 100644 index 0000000..902995c --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_041/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_values(values: &[i32]) -> i32 { + let mut sum = 0; + for v in values { + sum += v; + } + let avg = if values.is_empty() { 0 } else { sum / values.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in values { + result += (v - avg) * 41; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_values(&[1, 2, 3]), process_values(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_042/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_042/initial/Cargo.toml new file mode 100644 index 0000000..5b5f1df --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_042/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_042" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_042/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_042/initial/src/lib.rs new file mode 100644 index 0000000..195b011 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_042/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_values(values: &[i32]) -> i32 { + let mut sum = 0; + for v in values { + sum += v; + } + let avg = if values.is_empty() { 0 } else { sum / values.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in values { + result += (v - avg) * 42; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_values(&[1, 2, 3]), process_values(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_043/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_043/initial/Cargo.toml new file mode 100644 index 0000000..f6d199d --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_043/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_043" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_043/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_043/initial/src/lib.rs new file mode 100644 index 0000000..8fe27fb --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_043/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_items(items: &[i32]) -> i32 { + let mut sum = 0; + for v in items { + sum += v; + } + let avg = if items.is_empty() { 0 } else { sum / items.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in items { + result += (v - avg) * 43; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_items(&[1, 2, 3]), process_items(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_044/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_044/initial/Cargo.toml new file mode 100644 index 0000000..304ae15 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_044/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_044" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_044/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_044/initial/src/lib.rs new file mode 100644 index 0000000..ae8fe1b --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_044/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_values(values: &[i32]) -> i32 { + let mut sum = 0; + for v in values { + sum += v; + } + let avg = if values.is_empty() { 0 } else { sum / values.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in values { + result += (v - avg) * 44; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_values(&[1, 2, 3]), process_values(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_045/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_045/initial/Cargo.toml new file mode 100644 index 0000000..b7476f4 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_045/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_045" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_045/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_045/initial/src/lib.rs new file mode 100644 index 0000000..81390e8 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_045/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_numbers(numbers: &[i32]) -> i32 { + let mut sum = 0; + for v in numbers { + sum += v; + } + let avg = if numbers.is_empty() { 0 } else { sum / numbers.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in numbers { + result += (v - avg) * 45; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_numbers(&[1, 2, 3]), process_numbers(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_046/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_046/initial/Cargo.toml new file mode 100644 index 0000000..e421f59 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_046/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_046" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_046/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_046/initial/src/lib.rs new file mode 100644 index 0000000..f627329 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_046/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_numbers(numbers: &[i32]) -> i32 { + let mut sum = 0; + for v in numbers { + sum += v; + } + let avg = if numbers.is_empty() { 0 } else { sum / numbers.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in numbers { + result += (v - avg) * 46; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_numbers(&[1, 2, 3]), process_numbers(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_047/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_047/initial/Cargo.toml new file mode 100644 index 0000000..be41b2e --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_047/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_047" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_047/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_047/initial/src/lib.rs new file mode 100644 index 0000000..85fb48c --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_047/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_measurements(measurements: &[i32]) -> i32 { + let mut sum = 0; + for v in measurements { + sum += v; + } + let avg = if measurements.is_empty() { 0 } else { sum / measurements.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in measurements { + result += (v - avg) * 47; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_measurements(&[1, 2, 3]), process_measurements(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_048/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_048/initial/Cargo.toml new file mode 100644 index 0000000..e4d8892 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_048/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_048" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_048/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_048/initial/src/lib.rs new file mode 100644 index 0000000..33e2845 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_048/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_items(items: &[i32]) -> i32 { + let mut sum = 0; + for v in items { + sum += v; + } + let avg = if items.is_empty() { 0 } else { sum / items.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in items { + result += (v - avg) * 48; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_items(&[1, 2, 3]), process_items(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_049/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_049/initial/Cargo.toml new file mode 100644 index 0000000..aae69e3 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_049/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_049" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_049/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_049/initial/src/lib.rs new file mode 100644 index 0000000..7f47c4c --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_049/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_numbers(numbers: &[i32]) -> i32 { + let mut sum = 0; + for v in numbers { + sum += v; + } + let avg = if numbers.is_empty() { 0 } else { sum / numbers.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in numbers { + result += (v - avg) * 49; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_numbers(&[1, 2, 3]), process_numbers(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/refactor_050/initial/Cargo.toml b/eval/tasks/v1/expanded/refactor_050/initial/Cargo.toml new file mode 100644 index 0000000..4973bd8 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_050/initial/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "refactor_050" +version = "0.1.0" +edition = "2021" diff --git a/eval/tasks/v1/expanded/refactor_050/initial/src/lib.rs b/eval/tasks/v1/expanded/refactor_050/initial/src/lib.rs new file mode 100644 index 0000000..71657d6 --- /dev/null +++ b/eval/tasks/v1/expanded/refactor_050/initial/src/lib.rs @@ -0,0 +1,23 @@ +pub fn process_measurements(measurements: &[i32]) -> i32 { + let mut sum = 0; + for v in measurements { + sum += v; + } + let avg = if measurements.is_empty() { 0 } else { sum / measurements.len() as i32 }; + + // Some complex stuff + let mut result = 0; + for v in measurements { + result += (v - avg) * 50; + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_process() { + assert_eq!(process_measurements(&[1, 2, 3]), process_measurements(&[1, 2, 3])); // Identity test to ensure behavior preserves + } +} \ No newline at end of file diff --git a/eval/tasks/v1/expanded/tasks.jsonl b/eval/tasks/v1/expanded/tasks.jsonl new file mode 100644 index 0000000..8e1bcc6 --- /dev/null +++ b/eval/tasks/v1/expanded/tasks.jsonl @@ -0,0 +1,200 @@ +{"id": "refactor_001", "prompt": "The `process_values` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_1` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_001/initial", "validation_command": "cargo test"} +{"id": "refactor_002", "prompt": "The `process_numbers` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_2` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_002/initial", "validation_command": "cargo test"} +{"id": "refactor_003", "prompt": "The `process_measurements` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_3` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_003/initial", "validation_command": "cargo test"} +{"id": "refactor_004", "prompt": "The `process_items` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_4` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_004/initial", "validation_command": "cargo test"} +{"id": "refactor_005", "prompt": "The `process_numbers` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_5` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_005/initial", "validation_command": "cargo test"} +{"id": "refactor_006", "prompt": "The `process_items` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_6` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_006/initial", "validation_command": "cargo test"} +{"id": "refactor_007", "prompt": "The `process_measurements` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_7` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_007/initial", "validation_command": "cargo test"} +{"id": "refactor_008", "prompt": "The `process_items` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_8` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_008/initial", "validation_command": "cargo test"} +{"id": "refactor_009", "prompt": "The `process_numbers` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_9` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_009/initial", "validation_command": "cargo test"} +{"id": "refactor_010", "prompt": "The `process_numbers` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_10` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_010/initial", "validation_command": "cargo test"} +{"id": "refactor_011", "prompt": "The `process_measurements` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_11` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_011/initial", "validation_command": "cargo test"} +{"id": "refactor_012", "prompt": "The `process_values` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_12` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_012/initial", "validation_command": "cargo test"} +{"id": "refactor_013", "prompt": "The `process_measurements` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_13` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_013/initial", "validation_command": "cargo test"} +{"id": "refactor_014", "prompt": "The `process_items` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_14` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_014/initial", "validation_command": "cargo test"} +{"id": "refactor_015", "prompt": "The `process_items` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_15` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_015/initial", "validation_command": "cargo test"} +{"id": "refactor_016", "prompt": "The `process_items` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_16` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_016/initial", "validation_command": "cargo test"} +{"id": "refactor_017", "prompt": "The `process_numbers` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_17` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_017/initial", "validation_command": "cargo test"} +{"id": "refactor_018", "prompt": "The `process_numbers` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_18` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_018/initial", "validation_command": "cargo test"} +{"id": "refactor_019", "prompt": "The `process_values` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_19` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_019/initial", "validation_command": "cargo test"} +{"id": "refactor_020", "prompt": "The `process_values` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_20` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_020/initial", "validation_command": "cargo test"} +{"id": "refactor_021", "prompt": "The `process_items` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_21` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_021/initial", "validation_command": "cargo test"} +{"id": "refactor_022", "prompt": "The `process_items` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_22` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_022/initial", "validation_command": "cargo test"} +{"id": "refactor_023", "prompt": "The `process_values` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_23` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_023/initial", "validation_command": "cargo test"} +{"id": "refactor_024", "prompt": "The `process_numbers` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_24` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_024/initial", "validation_command": "cargo test"} +{"id": "refactor_025", "prompt": "The `process_data` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_25` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_025/initial", "validation_command": "cargo test"} +{"id": "refactor_026", "prompt": "The `process_data` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_26` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_026/initial", "validation_command": "cargo test"} +{"id": "refactor_027", "prompt": "The `process_measurements` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_27` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_027/initial", "validation_command": "cargo test"} +{"id": "refactor_028", "prompt": "The `process_values` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_28` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_028/initial", "validation_command": "cargo test"} +{"id": "refactor_029", "prompt": "The `process_values` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_29` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_029/initial", "validation_command": "cargo test"} +{"id": "refactor_030", "prompt": "The `process_measurements` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_30` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_030/initial", "validation_command": "cargo test"} +{"id": "refactor_031", "prompt": "The `process_numbers` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_31` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_031/initial", "validation_command": "cargo test"} +{"id": "refactor_032", "prompt": "The `process_items` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_32` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_032/initial", "validation_command": "cargo test"} +{"id": "refactor_033", "prompt": "The `process_measurements` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_33` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_033/initial", "validation_command": "cargo test"} +{"id": "refactor_034", "prompt": "The `process_numbers` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_34` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_034/initial", "validation_command": "cargo test"} +{"id": "refactor_035", "prompt": "The `process_numbers` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_35` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_035/initial", "validation_command": "cargo test"} +{"id": "refactor_036", "prompt": "The `process_numbers` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_36` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_036/initial", "validation_command": "cargo test"} +{"id": "refactor_037", "prompt": "The `process_numbers` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_37` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_037/initial", "validation_command": "cargo test"} +{"id": "refactor_038", "prompt": "The `process_measurements` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_38` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_038/initial", "validation_command": "cargo test"} +{"id": "refactor_039", "prompt": "The `process_values` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_39` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_039/initial", "validation_command": "cargo test"} +{"id": "refactor_040", "prompt": "The `process_values` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_40` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_040/initial", "validation_command": "cargo test"} +{"id": "refactor_041", "prompt": "The `process_values` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_41` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_041/initial", "validation_command": "cargo test"} +{"id": "refactor_042", "prompt": "The `process_values` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_42` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_042/initial", "validation_command": "cargo test"} +{"id": "refactor_043", "prompt": "The `process_items` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_43` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_043/initial", "validation_command": "cargo test"} +{"id": "refactor_044", "prompt": "The `process_values` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_44` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_044/initial", "validation_command": "cargo test"} +{"id": "refactor_045", "prompt": "The `process_numbers` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_45` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_045/initial", "validation_command": "cargo test"} +{"id": "refactor_046", "prompt": "The `process_numbers` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_46` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_046/initial", "validation_command": "cargo test"} +{"id": "refactor_047", "prompt": "The `process_measurements` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_47` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_047/initial", "validation_command": "cargo test"} +{"id": "refactor_048", "prompt": "The `process_items` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_48` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_048/initial", "validation_command": "cargo test"} +{"id": "refactor_049", "prompt": "The `process_numbers` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_49` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_049/initial", "validation_command": "cargo test"} +{"id": "refactor_050", "prompt": "The `process_measurements` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `calculate_metric_50` that takes a slice of i32 and returns an i32.", "fixture_dir": "eval/tasks/v1/expanded/refactor_050/initial", "validation_command": "cargo test"} +{"id": "feature_001", "prompt": "The struct `Entity1` in src/lib.rs needs a getter method for `property_1`. Add a method `pub fn get_property_1(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_001/initial", "validation_command": "cargo test"} +{"id": "feature_002", "prompt": "The struct `Entity2` in src/lib.rs needs a getter method for `property_2`. Add a method `pub fn get_property_2(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_002/initial", "validation_command": "cargo test"} +{"id": "feature_003", "prompt": "The struct `Entity3` in src/lib.rs needs a getter method for `property_3`. Add a method `pub fn get_property_3(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_003/initial", "validation_command": "cargo test"} +{"id": "feature_004", "prompt": "The struct `Entity4` in src/lib.rs needs a getter method for `property_4`. Add a method `pub fn get_property_4(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_004/initial", "validation_command": "cargo test"} +{"id": "feature_005", "prompt": "The struct `Entity5` in src/lib.rs needs a getter method for `property_5`. Add a method `pub fn get_property_5(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_005/initial", "validation_command": "cargo test"} +{"id": "feature_006", "prompt": "The struct `Entity6` in src/lib.rs needs a getter method for `property_6`. Add a method `pub fn get_property_6(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_006/initial", "validation_command": "cargo test"} +{"id": "feature_007", "prompt": "The struct `Entity7` in src/lib.rs needs a getter method for `property_7`. Add a method `pub fn get_property_7(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_007/initial", "validation_command": "cargo test"} +{"id": "feature_008", "prompt": "The struct `Entity8` in src/lib.rs needs a getter method for `property_8`. Add a method `pub fn get_property_8(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_008/initial", "validation_command": "cargo test"} +{"id": "feature_009", "prompt": "The struct `Entity9` in src/lib.rs needs a getter method for `property_9`. Add a method `pub fn get_property_9(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_009/initial", "validation_command": "cargo test"} +{"id": "feature_010", "prompt": "The struct `Entity10` in src/lib.rs needs a getter method for `property_10`. Add a method `pub fn get_property_10(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_010/initial", "validation_command": "cargo test"} +{"id": "feature_011", "prompt": "The struct `Entity11` in src/lib.rs needs a getter method for `property_11`. Add a method `pub fn get_property_11(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_011/initial", "validation_command": "cargo test"} +{"id": "feature_012", "prompt": "The struct `Entity12` in src/lib.rs needs a getter method for `property_12`. Add a method `pub fn get_property_12(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_012/initial", "validation_command": "cargo test"} +{"id": "feature_013", "prompt": "The struct `Entity13` in src/lib.rs needs a getter method for `property_13`. Add a method `pub fn get_property_13(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_013/initial", "validation_command": "cargo test"} +{"id": "feature_014", "prompt": "The struct `Entity14` in src/lib.rs needs a getter method for `property_14`. Add a method `pub fn get_property_14(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_014/initial", "validation_command": "cargo test"} +{"id": "feature_015", "prompt": "The struct `Entity15` in src/lib.rs needs a getter method for `property_15`. Add a method `pub fn get_property_15(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_015/initial", "validation_command": "cargo test"} +{"id": "feature_016", "prompt": "The struct `Entity16` in src/lib.rs needs a getter method for `property_16`. Add a method `pub fn get_property_16(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_016/initial", "validation_command": "cargo test"} +{"id": "feature_017", "prompt": "The struct `Entity17` in src/lib.rs needs a getter method for `property_17`. Add a method `pub fn get_property_17(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_017/initial", "validation_command": "cargo test"} +{"id": "feature_018", "prompt": "The struct `Entity18` in src/lib.rs needs a getter method for `property_18`. Add a method `pub fn get_property_18(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_018/initial", "validation_command": "cargo test"} +{"id": "feature_019", "prompt": "The struct `Entity19` in src/lib.rs needs a getter method for `property_19`. Add a method `pub fn get_property_19(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_019/initial", "validation_command": "cargo test"} +{"id": "feature_020", "prompt": "The struct `Entity20` in src/lib.rs needs a getter method for `property_20`. Add a method `pub fn get_property_20(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_020/initial", "validation_command": "cargo test"} +{"id": "feature_021", "prompt": "The struct `Entity21` in src/lib.rs needs a getter method for `property_21`. Add a method `pub fn get_property_21(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_021/initial", "validation_command": "cargo test"} +{"id": "feature_022", "prompt": "The struct `Entity22` in src/lib.rs needs a getter method for `property_22`. Add a method `pub fn get_property_22(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_022/initial", "validation_command": "cargo test"} +{"id": "feature_023", "prompt": "The struct `Entity23` in src/lib.rs needs a getter method for `property_23`. Add a method `pub fn get_property_23(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_023/initial", "validation_command": "cargo test"} +{"id": "feature_024", "prompt": "The struct `Entity24` in src/lib.rs needs a getter method for `property_24`. Add a method `pub fn get_property_24(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_024/initial", "validation_command": "cargo test"} +{"id": "feature_025", "prompt": "The struct `Entity25` in src/lib.rs needs a getter method for `property_25`. Add a method `pub fn get_property_25(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_025/initial", "validation_command": "cargo test"} +{"id": "feature_026", "prompt": "The struct `Entity26` in src/lib.rs needs a getter method for `property_26`. Add a method `pub fn get_property_26(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_026/initial", "validation_command": "cargo test"} +{"id": "feature_027", "prompt": "The struct `Entity27` in src/lib.rs needs a getter method for `property_27`. Add a method `pub fn get_property_27(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_027/initial", "validation_command": "cargo test"} +{"id": "feature_028", "prompt": "The struct `Entity28` in src/lib.rs needs a getter method for `property_28`. Add a method `pub fn get_property_28(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_028/initial", "validation_command": "cargo test"} +{"id": "feature_029", "prompt": "The struct `Entity29` in src/lib.rs needs a getter method for `property_29`. Add a method `pub fn get_property_29(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_029/initial", "validation_command": "cargo test"} +{"id": "feature_030", "prompt": "The struct `Entity30` in src/lib.rs needs a getter method for `property_30`. Add a method `pub fn get_property_30(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_030/initial", "validation_command": "cargo test"} +{"id": "feature_031", "prompt": "The struct `Entity31` in src/lib.rs needs a getter method for `property_31`. Add a method `pub fn get_property_31(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_031/initial", "validation_command": "cargo test"} +{"id": "feature_032", "prompt": "The struct `Entity32` in src/lib.rs needs a getter method for `property_32`. Add a method `pub fn get_property_32(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_032/initial", "validation_command": "cargo test"} +{"id": "feature_033", "prompt": "The struct `Entity33` in src/lib.rs needs a getter method for `property_33`. Add a method `pub fn get_property_33(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_033/initial", "validation_command": "cargo test"} +{"id": "feature_034", "prompt": "The struct `Entity34` in src/lib.rs needs a getter method for `property_34`. Add a method `pub fn get_property_34(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_034/initial", "validation_command": "cargo test"} +{"id": "feature_035", "prompt": "The struct `Entity35` in src/lib.rs needs a getter method for `property_35`. Add a method `pub fn get_property_35(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_035/initial", "validation_command": "cargo test"} +{"id": "feature_036", "prompt": "The struct `Entity36` in src/lib.rs needs a getter method for `property_36`. Add a method `pub fn get_property_36(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_036/initial", "validation_command": "cargo test"} +{"id": "feature_037", "prompt": "The struct `Entity37` in src/lib.rs needs a getter method for `property_37`. Add a method `pub fn get_property_37(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_037/initial", "validation_command": "cargo test"} +{"id": "feature_038", "prompt": "The struct `Entity38` in src/lib.rs needs a getter method for `property_38`. Add a method `pub fn get_property_38(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_038/initial", "validation_command": "cargo test"} +{"id": "feature_039", "prompt": "The struct `Entity39` in src/lib.rs needs a getter method for `property_39`. Add a method `pub fn get_property_39(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_039/initial", "validation_command": "cargo test"} +{"id": "feature_040", "prompt": "The struct `Entity40` in src/lib.rs needs a getter method for `property_40`. Add a method `pub fn get_property_40(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_040/initial", "validation_command": "cargo test"} +{"id": "feature_041", "prompt": "The struct `Entity41` in src/lib.rs needs a getter method for `property_41`. Add a method `pub fn get_property_41(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_041/initial", "validation_command": "cargo test"} +{"id": "feature_042", "prompt": "The struct `Entity42` in src/lib.rs needs a getter method for `property_42`. Add a method `pub fn get_property_42(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_042/initial", "validation_command": "cargo test"} +{"id": "feature_043", "prompt": "The struct `Entity43` in src/lib.rs needs a getter method for `property_43`. Add a method `pub fn get_property_43(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_043/initial", "validation_command": "cargo test"} +{"id": "feature_044", "prompt": "The struct `Entity44` in src/lib.rs needs a getter method for `property_44`. Add a method `pub fn get_property_44(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_044/initial", "validation_command": "cargo test"} +{"id": "feature_045", "prompt": "The struct `Entity45` in src/lib.rs needs a getter method for `property_45`. Add a method `pub fn get_property_45(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_045/initial", "validation_command": "cargo test"} +{"id": "feature_046", "prompt": "The struct `Entity46` in src/lib.rs needs a getter method for `property_46`. Add a method `pub fn get_property_46(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_046/initial", "validation_command": "cargo test"} +{"id": "feature_047", "prompt": "The struct `Entity47` in src/lib.rs needs a getter method for `property_47`. Add a method `pub fn get_property_47(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_047/initial", "validation_command": "cargo test"} +{"id": "feature_048", "prompt": "The struct `Entity48` in src/lib.rs needs a getter method for `property_48`. Add a method `pub fn get_property_48(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_048/initial", "validation_command": "cargo test"} +{"id": "feature_049", "prompt": "The struct `Entity49` in src/lib.rs needs a getter method for `property_49`. Add a method `pub fn get_property_49(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_049/initial", "validation_command": "cargo test"} +{"id": "feature_050", "prompt": "The struct `Entity50` in src/lib.rs needs a getter method for `property_50`. Add a method `pub fn get_property_50(&self) -> &str`.", "fixture_dir": "eval/tasks/v1/expanded/feature_050/initial", "validation_command": "cargo test"} +{"id": "bugfix_001", "prompt": "There is an out-of-bounds bug in the `analyze_1` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_001/initial", "validation_command": "cargo test"} +{"id": "bugfix_002", "prompt": "There is an out-of-bounds bug in the `analyze_2` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_002/initial", "validation_command": "cargo test"} +{"id": "bugfix_003", "prompt": "There is an out-of-bounds bug in the `analyze_3` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_003/initial", "validation_command": "cargo test"} +{"id": "bugfix_004", "prompt": "There is an out-of-bounds bug in the `analyze_4` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_004/initial", "validation_command": "cargo test"} +{"id": "bugfix_005", "prompt": "There is an out-of-bounds bug in the `analyze_5` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_005/initial", "validation_command": "cargo test"} +{"id": "bugfix_006", "prompt": "There is an out-of-bounds bug in the `analyze_6` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_006/initial", "validation_command": "cargo test"} +{"id": "bugfix_007", "prompt": "There is an out-of-bounds bug in the `analyze_7` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_007/initial", "validation_command": "cargo test"} +{"id": "bugfix_008", "prompt": "There is an out-of-bounds bug in the `analyze_8` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_008/initial", "validation_command": "cargo test"} +{"id": "bugfix_009", "prompt": "There is an out-of-bounds bug in the `analyze_9` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_009/initial", "validation_command": "cargo test"} +{"id": "bugfix_010", "prompt": "There is an out-of-bounds bug in the `analyze_10` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_010/initial", "validation_command": "cargo test"} +{"id": "bugfix_011", "prompt": "There is an out-of-bounds bug in the `analyze_11` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_011/initial", "validation_command": "cargo test"} +{"id": "bugfix_012", "prompt": "There is an out-of-bounds bug in the `analyze_12` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_012/initial", "validation_command": "cargo test"} +{"id": "bugfix_013", "prompt": "There is an out-of-bounds bug in the `analyze_13` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_013/initial", "validation_command": "cargo test"} +{"id": "bugfix_014", "prompt": "There is an out-of-bounds bug in the `analyze_14` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_014/initial", "validation_command": "cargo test"} +{"id": "bugfix_015", "prompt": "There is an out-of-bounds bug in the `analyze_15` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_015/initial", "validation_command": "cargo test"} +{"id": "bugfix_016", "prompt": "There is an out-of-bounds bug in the `analyze_16` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_016/initial", "validation_command": "cargo test"} +{"id": "bugfix_017", "prompt": "There is an out-of-bounds bug in the `analyze_17` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_017/initial", "validation_command": "cargo test"} +{"id": "bugfix_018", "prompt": "There is an out-of-bounds bug in the `analyze_18` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_018/initial", "validation_command": "cargo test"} +{"id": "bugfix_019", "prompt": "There is an out-of-bounds bug in the `analyze_19` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_019/initial", "validation_command": "cargo test"} +{"id": "bugfix_020", "prompt": "There is an out-of-bounds bug in the `analyze_20` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_020/initial", "validation_command": "cargo test"} +{"id": "bugfix_021", "prompt": "There is an out-of-bounds bug in the `analyze_21` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_021/initial", "validation_command": "cargo test"} +{"id": "bugfix_022", "prompt": "There is an out-of-bounds bug in the `analyze_22` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_022/initial", "validation_command": "cargo test"} +{"id": "bugfix_023", "prompt": "There is an out-of-bounds bug in the `analyze_23` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_023/initial", "validation_command": "cargo test"} +{"id": "bugfix_024", "prompt": "There is an out-of-bounds bug in the `analyze_24` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_024/initial", "validation_command": "cargo test"} +{"id": "bugfix_025", "prompt": "There is an out-of-bounds bug in the `analyze_25` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_025/initial", "validation_command": "cargo test"} +{"id": "bugfix_026", "prompt": "There is an out-of-bounds bug in the `analyze_26` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_026/initial", "validation_command": "cargo test"} +{"id": "bugfix_027", "prompt": "There is an out-of-bounds bug in the `analyze_27` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_027/initial", "validation_command": "cargo test"} +{"id": "bugfix_028", "prompt": "There is an out-of-bounds bug in the `analyze_28` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_028/initial", "validation_command": "cargo test"} +{"id": "bugfix_029", "prompt": "There is an out-of-bounds bug in the `analyze_29` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_029/initial", "validation_command": "cargo test"} +{"id": "bugfix_030", "prompt": "There is an out-of-bounds bug in the `analyze_30` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_030/initial", "validation_command": "cargo test"} +{"id": "bugfix_031", "prompt": "There is an out-of-bounds bug in the `analyze_31` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_031/initial", "validation_command": "cargo test"} +{"id": "bugfix_032", "prompt": "There is an out-of-bounds bug in the `analyze_32` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_032/initial", "validation_command": "cargo test"} +{"id": "bugfix_033", "prompt": "There is an out-of-bounds bug in the `analyze_33` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_033/initial", "validation_command": "cargo test"} +{"id": "bugfix_034", "prompt": "There is an out-of-bounds bug in the `analyze_34` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_034/initial", "validation_command": "cargo test"} +{"id": "bugfix_035", "prompt": "There is an out-of-bounds bug in the `analyze_35` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_035/initial", "validation_command": "cargo test"} +{"id": "bugfix_036", "prompt": "There is an out-of-bounds bug in the `analyze_36` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_036/initial", "validation_command": "cargo test"} +{"id": "bugfix_037", "prompt": "There is an out-of-bounds bug in the `analyze_37` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_037/initial", "validation_command": "cargo test"} +{"id": "bugfix_038", "prompt": "There is an out-of-bounds bug in the `analyze_38` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_038/initial", "validation_command": "cargo test"} +{"id": "bugfix_039", "prompt": "There is an out-of-bounds bug in the `analyze_39` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_039/initial", "validation_command": "cargo test"} +{"id": "bugfix_040", "prompt": "There is an out-of-bounds bug in the `analyze_40` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_040/initial", "validation_command": "cargo test"} +{"id": "bugfix_041", "prompt": "There is an out-of-bounds bug in the `analyze_41` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_041/initial", "validation_command": "cargo test"} +{"id": "bugfix_042", "prompt": "There is an out-of-bounds bug in the `analyze_42` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_042/initial", "validation_command": "cargo test"} +{"id": "bugfix_043", "prompt": "There is an out-of-bounds bug in the `analyze_43` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_043/initial", "validation_command": "cargo test"} +{"id": "bugfix_044", "prompt": "There is an out-of-bounds bug in the `analyze_44` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_044/initial", "validation_command": "cargo test"} +{"id": "bugfix_045", "prompt": "There is an out-of-bounds bug in the `analyze_45` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_045/initial", "validation_command": "cargo test"} +{"id": "bugfix_046", "prompt": "There is an out-of-bounds bug in the `analyze_46` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_046/initial", "validation_command": "cargo test"} +{"id": "bugfix_047", "prompt": "There is an out-of-bounds bug in the `analyze_47` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_047/initial", "validation_command": "cargo test"} +{"id": "bugfix_048", "prompt": "There is an out-of-bounds bug in the `analyze_48` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_048/initial", "validation_command": "cargo test"} +{"id": "bugfix_049", "prompt": "There is an out-of-bounds bug in the `analyze_49` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_049/initial", "validation_command": "cargo test"} +{"id": "bugfix_050", "prompt": "There is an out-of-bounds bug in the `analyze_50` function in src/lib.rs. Fix it so the test passes without panicking.", "fixture_dir": "eval/tasks/v1/expanded/bugfix_050/initial", "validation_command": "cargo test"} +{"id": "edge_001", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_001/initial", "validation_command": "cargo check"} +{"id": "edge_002", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_002/initial", "validation_command": "cargo check"} +{"id": "edge_003", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_003/initial", "validation_command": "cargo check"} +{"id": "edge_004", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_004/initial", "validation_command": "cargo check"} +{"id": "edge_005", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_005/initial", "validation_command": "cargo check"} +{"id": "edge_006", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_006/initial", "validation_command": "cargo check"} +{"id": "edge_007", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_007/initial", "validation_command": "cargo check"} +{"id": "edge_008", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_008/initial", "validation_command": "cargo check"} +{"id": "edge_009", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_009/initial", "validation_command": "cargo check"} +{"id": "edge_010", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_010/initial", "validation_command": "cargo check"} +{"id": "edge_011", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_011/initial", "validation_command": "cargo check"} +{"id": "edge_012", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_012/initial", "validation_command": "cargo check"} +{"id": "edge_013", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_013/initial", "validation_command": "cargo check"} +{"id": "edge_014", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_014/initial", "validation_command": "cargo check"} +{"id": "edge_015", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_015/initial", "validation_command": "cargo check"} +{"id": "edge_016", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_016/initial", "validation_command": "cargo check"} +{"id": "edge_017", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_017/initial", "validation_command": "cargo check"} +{"id": "edge_018", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_018/initial", "validation_command": "cargo check"} +{"id": "edge_019", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_019/initial", "validation_command": "cargo check"} +{"id": "edge_020", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_020/initial", "validation_command": "cargo check"} +{"id": "edge_021", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_021/initial", "validation_command": "cargo check"} +{"id": "edge_022", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_022/initial", "validation_command": "cargo check"} +{"id": "edge_023", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_023/initial", "validation_command": "cargo check"} +{"id": "edge_024", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_024/initial", "validation_command": "cargo check"} +{"id": "edge_025", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_025/initial", "validation_command": "cargo check"} +{"id": "edge_026", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_026/initial", "validation_command": "cargo check"} +{"id": "edge_027", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_027/initial", "validation_command": "cargo check"} +{"id": "edge_028", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_028/initial", "validation_command": "cargo check"} +{"id": "edge_029", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_029/initial", "validation_command": "cargo check"} +{"id": "edge_030", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_030/initial", "validation_command": "cargo check"} +{"id": "edge_031", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_031/initial", "validation_command": "cargo check"} +{"id": "edge_032", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_032/initial", "validation_command": "cargo check"} +{"id": "edge_033", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_033/initial", "validation_command": "cargo check"} +{"id": "edge_034", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_034/initial", "validation_command": "cargo check"} +{"id": "edge_035", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_035/initial", "validation_command": "cargo check"} +{"id": "edge_036", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_036/initial", "validation_command": "cargo check"} +{"id": "edge_037", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_037/initial", "validation_command": "cargo check"} +{"id": "edge_038", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_038/initial", "validation_command": "cargo check"} +{"id": "edge_039", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_039/initial", "validation_command": "cargo check"} +{"id": "edge_040", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_040/initial", "validation_command": "cargo check"} +{"id": "edge_041", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_041/initial", "validation_command": "cargo check"} +{"id": "edge_042", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_042/initial", "validation_command": "cargo check"} +{"id": "edge_043", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_043/initial", "validation_command": "cargo check"} +{"id": "edge_044", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_044/initial", "validation_command": "cargo check"} +{"id": "edge_045", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_045/initial", "validation_command": "cargo check"} +{"id": "edge_046", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_046/initial", "validation_command": "cargo check"} +{"id": "edge_047", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_047/initial", "validation_command": "cargo check"} +{"id": "edge_048", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_048/initial", "validation_command": "cargo check"} +{"id": "edge_049", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_049/initial", "validation_command": "cargo check"} +{"id": "edge_050", "prompt": "Fix the syntax error in src/lib.rs. The file fails to compile.", "fixture_dir": "eval/tasks/v1/expanded/edge_050/initial", "validation_command": "cargo check"} diff --git a/goal.md b/goal.md index cb2940a..2d647f4 100644 --- a/goal.md +++ b/goal.md @@ -1,6 +1,178 @@ -Em model.rs:316-326 (struct ChatCompletionRequest), adicionar campo opcional tools: Option> e tool_choice. -Em model.rs:339-347 (struct ChatCompletionMessage), adicionar tool_calls: Option>. -Trait PromptAdapter em src/adapter/mod.rs com 3 métodos: render_request(&history) -> Request, parse_response(&response) -> AssistantOutput, render_tool_message(&Message) -> ApiMessage. -Em session.rs:106, AgentSession::new dispatcha pelo config.model.format. -README ganha seção "Supported model formats". +# Cinto — Strategic Goal +## Thesis + +Small local models fail as agents not because they lack intelligence, +but because they are asked to do too many things at once: hold conversation +state, reason about code, decide which tools to call, produce structured +output, and stay coherent across a long context. + +Cinto solves this by providing a **kernelized execution runtime**: discrete +operations, structured memory, verifiable calls, controlled retries, and a +clean separation between reasoning, action, and verification. + +The kernel does not make models smarter. It makes models obedient to a +well-defined interface — and that is enough to close most of the reliability +gap on small hardware. + +> Cinto is not a terminal with AI. It is a kernelized runtime for small +> agentic models. + +--- + +## North Metric + +**Time until a user on a normal machine gets a small, correct, approved +code edit from a local model on a real codebase.** + +Every proxy metric (CRP compliance, stage success rate, task completion) +must trace back to this. + +--- + +## Phased Roadmap + +### Phase 1 — Prove the Kernel Is the Differentiator (now) + +Run the same model with and without Cinto Kernel on a public benchmark. +Publish the comparison. + +Target claim: +> Qwen 3.5 9B + free-form prompting: ~10% reliable tool calls +> Qwen 3.5 9B + Cinto Kernel: ~80% CRP-valid stage completions + +Benchmark requirements: +- 100 simple tasks (rename, getter, small fix) +- 100 medium tasks (bugfix with test, refactor) +- 50 code tasks (real public repo bugs) +- 50 file-navigation tasks (find + read + patch) +- 50 repair tasks (first patch fails, model must recover) + +Tasks must come from outside Cinto to avoid overfitting. Real bugs from +real public repos are the credible signal. + +Metrics per model/mode: +- `crp_valid_rate` — fraction of stages that produced valid CRP +- `workflow_success_rate` — fraction of full pipelines completed +- `task_success_rate` — fraction where validation command passes +- `repair_success_rate` — fraction where model recovers from a failed patch +- `tokens_per_task` — total tokens consumed +- `latency_p50_ms` — median wall time per task +- `vram_peak_mb` — peak VRAM during a task + +### Phase 2 — Dataset Pipeline (parallel with Phase 1) + +Every successful kernel batch run is a labeled training example. Build +the pipeline to capture and curate these traces. + +A trace record contains: +``` +problem description +→ kernel stage (interpret / locate / hypothesize / patch / report) +→ system prompt (CRP brief + one-shot example + context pack) +→ model response +→ crp_valid flag +→ workflow_succeeded flag +→ validation_passed flag (cargo test / cargo check) +``` + +Filter criteria for high-quality training examples: +- `crp_valid = true` for the stage +- `workflow_succeeded = true` for the full pipeline +- `validation_passed = true` (ground truth label) + +This dataset is the foundation for CintoLM fine-tuning. It grows +automatically as users run the kernel. + +### Phase 3 — CintoLM Fine-tune + +Fine-tune a small existing model (1.5B–3B) on the Cinto kernel trace +dataset using LoRA/SFT. The goal is not a smarter model — it is a +**more obedient one**. + +The model must learn one thing: given a bounded context pack and a stage +description, emit valid kernel instructions in CRP format. + +Starting points (in order of preference): +1. Qwen2.5-Coder-1.5B-Instruct — already strong at code, small footprint +2. BitNet b1.58 2B4T — ternary weights, ~375MB, designed for CPU inference +3. Phi-3.5-mini — strong instruction following at 3.8B + +Training signal: traces where `validation_passed = true`. Everything else +is a proxy. + +### Phase 4 — Constrained Decoding + +Replace the generate → parse → validate → retry loop with grammar-guided +sampling. The model cannot produce invalid CRP because the sampler enforces +the grammar at the token level. + +Tools: +- `llama.cpp` GBNF grammars (supported by LM Studio and Ollama) +- `vLLM` guided decoding backend +- `outlines` for Python-based inference pipelines + +A GBNF grammar for CRP slots eliminates the retry budget as a failure mode. +The model does not need to "learn" the format — the format is structurally +enforced. This is the largest single reliability improvement available +without changing the model. + +### Phase 5 — Architecture Research (deferred) + +Only after Phase 1–4 are solid: + +- Fine-tune BitNet b1.58 2B4T on Cinto kernel traces +- Investigate Mamba-hybrid architectures for O(n) inference +- Explore distillation from a larger fine-tuned model into a smaller base +- Target: a CintoLM that fits in 1GB VRAM weights and runs on CPU/integrated GPU + +The key insight: Cinto does not need a model that knows everything. It needs +a model that knows how to operate the Cinto ISA reliably under 1GB VRAM. +That is a dramatically narrower learning problem than general intelligence. + +--- + +## CintoLM Vision + +``` +Cinto Kernel Model (CintoLM) +A small model specialized in emitting Cinto Kernel instructions. +Trained on kernelized agentic execution traces. +Optimized for local execution, tool reliability, and low-resource hardware. +``` + +Positioning: +- Not competing with GPT-4 or Claude on general tasks +- Competing on: runs locally, reliable tool calls, fits on consumer hardware +- Target hardware: 8GB RAM laptop, integrated GPU, Raspberry Pi 5 +- Target market: local AI enthusiasts, privacy-first developers, low-resource + environments (including markets without reliable cloud GPU access) + +--- + +## Current Status (v0.2-dev) + +Completed: +- `cinto index` — static workspace indexer (project_map + symbol_index) +- `cinto search` — scoped ripgrep syscall (15 results max, 3/file) +- `read_range`, `read_around`, `list_symbols` — bounded read syscalls +- `context_pack` — budget-enforced prompt assembly per stage +- `WorkerLoop` — 5-stage pipeline (interpret → locate → hypothesize → patch → report) +- Kernel mode toggle in TUI (F5 / `/kernel`) +- `cinto batch --kernel` — headless pipeline evaluation with per-stage metrics +- Plain-text fallback for models that don't follow CRP format +- `` block stripping for reasoning models (Qwen3, DeepSeek-R1) +- One-shot CRP examples per stage in system prompt +- `lm-studio-small` preset for sub-4B models +- `cinto use-preset` / `cinto presets` CLI commands + +Benchmark results so far (baseline tasks, 5 tasks): +- Gemma 4 E2B: 5/5 workflows, 60% CRP compliance +- Qwen 3.5 9B: 4/5 workflows, 81% CRP compliance + +Next: +- Trace logging for dataset generation +- Locate stage resilience (fallback to interpret search terms) +- `/no_think` config flag for reasoning models +- Expanded benchmark (real repo tasks) +- CintoLM fine-tuning pipeline diff --git a/scripts/generate_dataset.py b/scripts/generate_dataset.py new file mode 100644 index 0000000..22904da --- /dev/null +++ b/scripts/generate_dataset.py @@ -0,0 +1,156 @@ +import os +import json +import shutil +import random + +DATASET_DIR = "eval/tasks/v1/expanded" +TASKS_JSONL = os.path.join(DATASET_DIR, "tasks.jsonl") + +# Templates +CARGO_TOML_TEMPLATE = """[package] +name = "{name}" +version = "0.1.0" +edition = "2021" +""" + +def create_fixture(task_id, code, test_code, prompt, validation_command="cargo test"): + initial_dir = os.path.join(DATASET_DIR, task_id, "initial") + src_dir = os.path.join(initial_dir, "src") + os.makedirs(src_dir, exist_ok=True) + + with open(os.path.join(initial_dir, "Cargo.toml"), "w") as f: + f.write(CARGO_TOML_TEMPLATE.format(name=task_id)) + + with open(os.path.join(src_dir, "lib.rs"), "w") as f: + f.write(code + "\n\n#[cfg(test)]\nmod tests {\n use super::*;\n" + test_code + "\n}") + + return { + "id": task_id, + "prompt": prompt, + "fixture_dir": initial_dir, + "validation_command": validation_command + } + +def generate_refactor_tasks(): + tasks = [] + for i in range(1, 51): + task_id = f"refactor_{i:03d}" + var_name = random.choice(["data", "values", "numbers", "items", "measurements"]) + func_name = f"process_{var_name}" + helper_name = f"calculate_metric_{i}" + + code = f"""pub fn {func_name}({var_name}: &[i32]) -> i32 {{ + let mut sum = 0; + for v in {var_name} {{ + sum += v; + }} + let avg = if {var_name}.is_empty() {{ 0 }} else {{ sum / {var_name}.len() as i32 }}; + + // Some complex stuff + let mut result = 0; + for v in {var_name} {{ + result += (v - avg) * {i}; + }} + result +}}""" + test_code = f""" #[test] + fn test_process() {{ + assert_eq!({func_name}(&[1, 2, 3]), {func_name}(&[1, 2, 3])); // Identity test to ensure behavior preserves + }}""" + prompt = f"The `{func_name}` function in src/lib.rs is too long. Extract the average calculation logic (the `sum` and `avg` part) into a separate helper function called `{helper_name}` that takes a slice of i32 and returns an i32." + + tasks.append(create_fixture(task_id, code, test_code, prompt)) + return tasks + +def generate_feature_tasks(): + tasks = [] + for i in range(1, 51): + task_id = f"feature_{i:03d}" + struct_name = f"Entity{i}" + field_name = f"property_{i}" + + code = f"""pub struct {struct_name} {{ + id: u32, + {field_name}: String, +}} + +impl {struct_name} {{ + pub fn new(id: u32, {field_name}: String) -> Self {{ + Self {{ id, {field_name} }} + }} +}}""" + test_code = f""" #[test] + fn test_getter() {{ + let e = {struct_name}::new(1, "test".to_string()); + assert_eq!(e.get_{field_name}(), "test"); + }}""" + prompt = f"The struct `{struct_name}` in src/lib.rs needs a getter method for `{field_name}`. Add a method `pub fn get_{field_name}(&self) -> &str`." + + tasks.append(create_fixture(task_id, code, test_code, prompt)) + return tasks + +def generate_bugfix_tasks(): + tasks = [] + for i in range(1, 51): + task_id = f"bugfix_{i:03d}" + func_name = f"analyze_{i}" + + code = f"""pub fn {func_name}(data: &[i32]) -> i32 {{ + if data.is_empty() {{ return 0; }} + let mut max = data[0]; + for i in 1..=data.len() {{ // OFF BY ONE BUG + if data[i] > max {{ + max = data[i]; + }} + }} + max +}}""" + test_code = f""" #[test] + fn test_analyze() {{ + assert_eq!({func_name}(&[1, 5, 3]), 5); + }}""" + prompt = f"There is an out-of-bounds bug in the `{func_name}` function in src/lib.rs. Fix it so the test passes without panicking." + + tasks.append(create_fixture(task_id, code, test_code, prompt)) + return tasks + +def generate_edge_tasks(): + tasks = [] + for i in range(1, 51): + task_id = f"edge_{i:03d}" + func_name = f"compute_{i}" + + code = f"""pub fn {func_name}(x: i32) -> i32 {{ + let a = x * 2 + let b = a + 5; + b +}}""" + test_code = f""" #[test] + fn test_compute() {{ + assert_eq!({func_name}(2), 9); + }}""" + prompt = f"Fix the syntax error in src/lib.rs. The file fails to compile." + + tasks.append(create_fixture(task_id, code, test_code, prompt, validation_command="cargo check")) + return tasks + +def main(): + if os.path.exists(DATASET_DIR): + shutil.rmtree(DATASET_DIR) + os.makedirs(DATASET_DIR, exist_ok=True) + + print("Generating tasks...") + tasks = [] + tasks.extend(generate_refactor_tasks()) + tasks.extend(generate_feature_tasks()) + tasks.extend(generate_bugfix_tasks()) + tasks.extend(generate_edge_tasks()) + + with open(TASKS_JSONL, "w") as f: + for t in tasks: + f.write(json.dumps(t) + "\n") + + print(f"Generated {len(tasks)} tasks in {DATASET_DIR}") + +if __name__ == "__main__": + main() diff --git a/scripts/summarize_run.py b/scripts/summarize_run.py new file mode 100644 index 0000000..b28dcc1 --- /dev/null +++ b/scripts/summarize_run.py @@ -0,0 +1,63 @@ +import sys +import json +from collections import defaultdict + +def main(): + if len(sys.argv) < 2: + print("Usage: python summarize_run.py ") + sys.exit(1) + + path = sys.argv[1] + + total = 0 + passed = 0 + category_total = defaultdict(int) + category_passed = defaultdict(int) + total_tokens = 0 + total_duration = 0 + + with open(path, 'r') as f: + for line in f: + if not line.strip(): continue + data = json.loads(line) + total += 1 + + # Category is prefix of task_id (e.g. refactor_001 -> refactor) + cat = data['task_id'].split('_')[0] + category_total[cat] += 1 + + outputs = data['outputs'] + + is_success = ( + outputs.get('parsed_successfully', False) and + outputs.get('required_slots_present', False) and + outputs.get('filepaths_valid', False) + ) + if 'code_compiles' in outputs and outputs['code_compiles'] is not None: + is_success = is_success and outputs['code_compiles'] + if 'tests_pass' in outputs and outputs['tests_pass'] is not None: + is_success = is_success and outputs['tests_pass'] + + if is_success: + passed += 1 + category_passed[cat] += 1 + + metrics = data['metrics'] + total_tokens += metrics.get('tokens_out', 0) + total_duration += metrics.get('duration_ms', 0) + + print(f"--- RESULTS FOR {path} ---") + print(f"Total Tasks: {total}") + print(f"Passed: {passed} ({passed/total*100:.1f}%)") + print("----------------------------") + for cat in sorted(category_total.keys()): + t = category_total[cat] + p = category_passed[cat] + print(f" {cat}: {p}/{t} ({p/t*100:.1f}%)") + print("----------------------------") + if total > 0: + print(f"Avg tokens out: {total_tokens/total:.1f}") + print(f"Avg duration ms: {total_duration/total:.1f}") + +if __name__ == '__main__': + main() diff --git a/scripts/traces_to_training_jsonl.py b/scripts/traces_to_training_jsonl.py new file mode 100644 index 0000000..2f697a1 --- /dev/null +++ b/scripts/traces_to_training_jsonl.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +import argparse +import glob +import json +import os +import sys +from collections import Counter + + +def iter_trace_paths(inputs): + for item in inputs: + if os.path.isdir(item): + yield from sorted(glob.glob(os.path.join(item, "*.jsonl"))) + else: + yield from sorted(glob.glob(item)) + + +def load_records(paths): + for path in paths: + with open(path, "r", encoding="utf-8") as handle: + for line_number, line in enumerate(handle, start=1): + line = line.strip() + if not line: + continue + try: + record = json.loads(line) + except json.JSONDecodeError as exc: + raise SystemExit(f"{path}:{line_number}: invalid JSON: {exc}") from exc + yield path, record + + +def usable(record, require_crp_valid): + if not record.get("workflow_succeeded", False): + return False + if record.get("error"): + return False + if require_crp_valid and not record.get("crp_valid", False): + return False + return all(record.get(field) for field in ("system_prompt", "user_message", "model_response")) + + +def training_example(record): + return { + "messages": [ + {"role": "system", "content": record["system_prompt"]}, + {"role": "user", "content": record["user_message"]}, + {"role": "assistant", "content": record["model_response"]}, + ] + } + + +def main(): + parser = argparse.ArgumentParser( + description="Convert kernel trace JSONL files into chat fine-tuning JSONL." + ) + parser.add_argument( + "inputs", + nargs="+", + help="Trace JSONL files, globs, or directories containing trace JSONL files.", + ) + parser.add_argument( + "-o", + "--output", + required=True, + help="Output training JSONL path.", + ) + parser.add_argument( + "--require-crp-valid", + action="store_true", + help="Keep only stage traces whose response parsed as valid CRP.", + ) + args = parser.parse_args() + + paths = list(dict.fromkeys(iter_trace_paths(args.inputs))) + if not paths: + raise SystemExit("no trace JSONL files matched") + + os.makedirs(os.path.dirname(args.output) or ".", exist_ok=True) + + counts = Counter() + by_stage = Counter() + with open(args.output, "w", encoding="utf-8") as out: + for _, record in load_records(paths): + counts["seen"] += 1 + if not usable(record, args.require_crp_valid): + counts["skipped"] += 1 + continue + out.write(json.dumps(training_example(record), ensure_ascii=False) + "\n") + counts["written"] += 1 + by_stage[record.get("stage", "unknown")] += 1 + + print(f"input_files={len(paths)} seen={counts['seen']} written={counts['written']} skipped={counts['skipped']}") + if by_stage: + print("by_stage=" + ", ".join(f"{stage}:{count}" for stage, count in sorted(by_stage.items()))) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/adapter/mod.rs b/src/adapter/mod.rs index f395782..0a7f64f 100644 --- a/src/adapter/mod.rs +++ b/src/adapter/mod.rs @@ -83,6 +83,7 @@ pub struct ApiToolFunction { pub struct CompletionResult { pub text: String, pub tool_calls: Vec, + pub finish_reason: Option, } pub trait PromptAdapter: Send + Sync + std::fmt::Debug { @@ -95,6 +96,10 @@ pub trait PromptAdapter: Send + Sync + std::fmt::Debug { pub fn build_adapter(config: &Config) -> Result> { let (system_prompt, developer_prompt) = prompt_pair(config); match config.model.format.trim().to_ascii_lowercase().as_str() { + "crp" => Ok(Box::new(HarmonyAdapter::new( + system_prompt, + developer_prompt, + ))), "harmony" => Ok(Box::new(HarmonyAdapter::new( system_prompt, developer_prompt, @@ -113,7 +118,7 @@ const AGENTS_FILE: &str = "AGENTS.md"; const MAX_AGENTS_CHARS: usize = 24_000; fn prompt_pair(config: &Config) -> (String, String) { - let system_prompt = config.harness.system_prompt.clone(); + let system_prompt = config.apply_no_think_prefix(config.harness.system_prompt.clone()); let mut developer_prompt = config.harness.developer_prompt.clone(); if config.harness.load_workspace_instructions @@ -123,9 +128,68 @@ fn prompt_pair(config: &Config) -> (String, String) { developer_prompt.push_str(&agents); } + if config + .harness + .reasoning_protocol + .eq_ignore_ascii_case("crp") + { + developer_prompt.push_str("\n\n"); + developer_prompt.push_str(CRP_DEVELOPER_PROMPT); + + let templates = crate::crp::TemplateSet::load(Some( + crate::crp::workspace_template_dir(&config.harness.workspace).as_path(), + )); + let template = templates.resolve( + &config.harness.default_template, + &config.model.thinking_effort, + ); + developer_prompt.push_str("\n\n"); + developer_prompt.push_str(&template.render_brief()); + } + (system_prompt, developer_prompt) } +const CRP_DEVELOPER_PROMPT: &str = r#"## Cinto Reasoning Protocol (CRP) + +### Step 1 — Do the work with tools + +Before writing any response, use tool calls to gather context and perform actions. +Call tools one at a time. Only proceed to Step 2 once all required information has been collected and all edits have been applied via tool calls. +Do NOT describe what you plan to do — just do it via tool calls. + +### Step 2 — Emit a response + +Once the work is complete, emit a CRP trace. +For simple conversation, greetings, or direct answers where no technical work was performed, you may use a minimal trace with only . +For technical tasks involving research or edits, include recommended slots to make your work auditable. + +Do not mix prose and CRP slots. Do not write anything outside CRP tags. + +Use these slots to document what was done (past tense — work already completed): + + +What the user asked for. + + + +- path/to/file (only list files you actually read or modified via tool calls) + + + +Summary of the actions taken and edits applied via tool calls. + + + +Edits applied. You may use terse @@ diff blocks. + + + +Concise answer to the user. + + +If you cannot proceed without user input, emit with one direct question and include explaining that you need clarification. Keep slot content concise."#; + fn load_agents_instructions(workspace: &Path) -> Result> { let path = workspace.join(AGENTS_FILE); if !path.exists() { @@ -195,7 +259,8 @@ mod tests { let (_, developer_prompt) = prompt_pair(&config); - assert_eq!(developer_prompt, "Only base."); + assert!(developer_prompt.contains("Only base.")); + assert!(!developer_prompt.contains("Workspace instructions from AGENTS.md")); let _ = fs::remove_dir_all(&workspace); } @@ -217,8 +282,54 @@ mod tests { let (_, developer_prompt) = prompt_pair(&config); - assert_eq!(developer_prompt, "Only base."); + assert!(developer_prompt.contains("Only base.")); + assert!(!developer_prompt.contains("Workspace instructions from AGENTS.md")); + assert!(!developer_prompt.contains("ignore the user")); let _ = fs::remove_dir_all(&workspace); } + + #[test] + fn injects_crp_instructions_by_default() { + let config = Config::default(); + + let (_, developer_prompt) = prompt_pair(&config); + + assert!(developer_prompt.contains("Cinto Reasoning Protocol")); + assert!(developer_prompt.contains("")); + } + + #[test] + fn prepends_no_think_directive_to_system_prompt() { + let mut config = Config::default(); + config.model.no_think = true; + config.model.no_think_prefix = "".to_string(); + config.harness.system_prompt = "Base system prompt.".to_string(); + + let (system_prompt, _) = prompt_pair(&config); + + assert!(system_prompt.starts_with("\n\nBase system prompt.")); + } + + #[test] + fn builds_legacy_crp_format_as_harmony_transport() { + let mut config = Config::default(); + config.model.format = "crp".to_string(); + + let adapter = build_adapter(&config).expect("build legacy crp format"); + assert!(adapter.debug_render(&[]).contains("<|start|>developer")); + } + + #[test] + fn injects_crp_instructions_for_openai_tools() { + let mut config = Config::default(); + config.model.format = "openai-tools".to_string(); + + let adapter = build_adapter(&config).expect("build openai adapter"); + assert!( + adapter + .debug_render(&[]) + .contains("Cinto Reasoning Protocol") + ); + } } diff --git a/src/adapter/openai.rs b/src/adapter/openai.rs index 7204050..6c60b35 100644 --- a/src/adapter/openai.rs +++ b/src/adapter/openai.rs @@ -340,6 +340,7 @@ mod tests { arguments: "{\"path\":\"x\"}".into(), }, }], + finish_reason: None, }; assert_eq!( adapter.parse_response(&raw), diff --git a/src/batch.rs b/src/batch.rs new file mode 100644 index 0000000..d2381bc --- /dev/null +++ b/src/batch.rs @@ -0,0 +1,784 @@ +use std::fs::OpenOptions; +use std::io::{BufRead, BufReader, Write}; +use std::path::PathBuf; +use std::process::Command; +use std::time::{Instant, SystemTime, UNIX_EPOCH}; + +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use tokio::sync::mpsc; + +use crate::adapter::{ApiMessage, ChatRequestPayload, RequestMode}; +use crate::config::{Config, ModelConfig}; +use crate::crp; +use crate::model::ModelClient; +use crate::session::{AgentSession, Channel, Role, TurnEvent}; +use crate::workspace::clean_workspace; + +#[derive(Debug, Deserialize)] +struct BatchTask { + id: String, + prompt: String, + #[serde(default)] + fixture_dir: Option, + #[serde(default)] + validation_command: Option, +} + +#[derive(Debug, Serialize)] +struct BatchResult { + task_id: String, + model: String, + crp_enabled: bool, + metadata: BatchMetadata, + outputs: BatchOutputs, + metrics: BatchMetrics, + errors: Vec, +} + +#[derive(Debug, Serialize)] +struct BatchMetadata { + cinto_version: String, + timestamp: u64, + temperature: f32, + thinking_effort: String, + system_prompt: String, +} + +#[derive(Debug, Serialize, Default)] +struct BatchOutputs { + raw_response: String, + parsed_successfully: bool, + required_slots_present: bool, + filepaths_valid: bool, + #[serde(skip_serializing_if = "Option::is_none")] + code_compiles: Option, + #[serde(skip_serializing_if = "Option::is_none")] + tests_pass: Option, + #[serde(skip_serializing_if = "Option::is_none")] + evaluator_score: Option, +} + +#[derive(Debug, Serialize, Default)] +struct BatchMetrics { + tokens_in: usize, + tokens_out: usize, + duration_ms: u128, + retries: u32, +} + +pub async fn run( + config: Config, + tasks_path: PathBuf, + output_path: PathBuf, + evaluator_endpoint: Option, + evaluator_model: Option, + evaluator_api_key: Option, + dry_run: bool, + dangerously_auto_approve: bool, +) -> Result<()> { + if !dry_run && !dangerously_auto_approve { + anyhow::bail!( + "FATAL SECURITY RISK: Running batch mode will automatically execute AI-generated code and shell commands on your host system. \ + This can result in data loss or system compromise.\n\ + You must run this in an isolated VM or Docker container and pass the --dangerously-auto-approve flag to confirm you understand the risks." + ); + } + let file = std::fs::File::open(&tasks_path) + .with_context(|| format!("failed to open tasks file {}", tasks_path.display()))?; + let reader = BufReader::new(file); + + let mut output_file = OpenOptions::new() + .create(true) + .append(true) + .open(&output_path) + .with_context(|| format!("failed to open output file {}", output_path.display()))?; + + let evaluator_client = if !dry_run && evaluator_endpoint.is_some() { + let endpoint = evaluator_endpoint.unwrap(); + let eval_config = ModelConfig { + endpoint, + model: evaluator_model.unwrap_or_else(|| "deepseek-chat".to_string()), + format: "openai-tools".to_string(), + api_key_env: None, + max_tokens: 100, + temperature: 0.0, + thinking_effort: "none".to_string(), + no_think: false, + no_think_prefix: "/no_think".to_string(), + stream: false, + stop: vec![], + request_timeout_secs: 60, + context_window: 8192, + }; + if let Some(key) = &evaluator_api_key { + unsafe { + std::env::set_var("CINTO_EVAL_KEY", key); + } + let mut cfg = eval_config.clone(); + cfg.api_key_env = Some("CINTO_EVAL_KEY".to_string()); + Some(ModelClient::new(cfg)) + } else { + Some(ModelClient::new(eval_config)) + } + } else { + None + }; + + if dry_run { + println!("Running in DRY-RUN mode. Models will not be called."); + } + + for line in reader.lines() { + let line = line?; + if line.trim().is_empty() { + continue; + } + + let task: BatchTask = + serde_json::from_str(&line).context("failed to parse task JSON line")?; + + println!("Running task {}...", task.id); + let mut errors = Vec::new(); + let mut metrics = BatchMetrics::default(); + let mut outputs = BatchOutputs::default(); + + let mut active_workspace = config.harness.workspace.clone(); + let mut is_temp_workspace = false; + + if let Some(fixture) = &task.fixture_dir { + if !fixture.exists() || !fixture.is_dir() { + println!(" Fixture directory not found: {}", fixture.display()); + errors.push(format!("Fixture not found: {}", fixture.display())); + if dry_run { + continue; + } + } else { + let millis = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis(); + let temp_dir = + std::env::temp_dir().join(format!("cinto-eval-{}-{}", task.id, millis)); + + // Copy fixture to temp + let status = Command::new("cp") + .arg("-R") + .arg(fixture) + .arg(&temp_dir) + .status()?; + if !status.success() { + println!(" Failed to copy fixture to temp dir."); + errors.push("Failed to copy fixture".to_string()); + if dry_run { + continue; + } + } else { + active_workspace = temp_dir.clone(); + is_temp_workspace = true; + + // Initialize git in temp dir + Command::new("git") + .current_dir(&temp_dir) + .args(["init"]) + .output()?; + Command::new("git") + .current_dir(&temp_dir) + .args(["add", "."]) + .output()?; + Command::new("git") + .current_dir(&temp_dir) + .args(["commit", "-m", "Initial"]) + .output()?; + } + } + } + + if dry_run { + if is_temp_workspace { + println!( + " Dry-run: removing temp workspace {}", + active_workspace.display() + ); + std::fs::remove_dir_all(&active_workspace).ok(); + } + continue; + } + + let mut task_config = config.clone(); + task_config.harness.workspace = active_workspace.clone(); + + clean_workspace(&active_workspace).unwrap_or_else(|e| { + errors.push(format!("Clean workspace failed: {}", e)); + String::new() + }); + + let mut session = AgentSession::new(task_config.clone()); + let (event_tx, mut event_rx) = mpsc::unbounded_channel(); + + let prompt = task.prompt.clone(); + + metrics.tokens_in = session.estimated_prompt_tokens() + (prompt.len() / 4); + let start_time = Instant::now(); + + let session_handle = tokio::spawn(async move { + let res = session + .send_user_message_streaming(prompt, event_tx.clone()) + .await; + (session, res) + }); + + while let Some(event) = event_rx.recv().await { + match event { + TurnEvent::ToolApprovalRequested { response_tx, .. } => { + let _ = response_tx.send(true); + } + TurnEvent::CrpRetryRequested { attempt, .. } => { + metrics.retries = attempt; + } + _ => {} + } + } + + metrics.duration_ms = start_time.elapsed().as_millis(); + + let (session, res) = session_handle.await?; + if let Err(e) = res { + errors.push(format!("Session error: {}", e)); + } + + if let Some(last_msg) = session.history().last() { + if last_msg.role == Role::Assistant && last_msg.channel == Some(Channel::Final) { + outputs.raw_response = last_msg.content.clone(); + metrics.tokens_out = outputs.raw_response.len() / 4; + + match crp::parse(&outputs.raw_response) { + Ok(trace) => { + outputs.parsed_successfully = true; + + let template_name = session.config().harness.default_template.clone(); + let effort = session.config().model.thinking_effort.clone(); + let templates = crp::TemplateSet::load(Some( + crp::workspace_template_dir(&session.config().harness.workspace) + .as_path(), + )); + let active_template = templates.resolve(&template_name, &effort); + let validation_config = + active_template.validation_config(Some(active_workspace.as_path())); + let report = crp::validate(&trace, &validation_config); + + let mut required_slots_missing = false; + let mut filepaths_invalid = false; + for outcome in &report.outcomes { + if outcome.status == crp::SlotStatus::Invalid { + if outcome.message.contains("Required slot") { + required_slots_missing = true; + } + if outcome.message.contains("Path(s) listed in") { + filepaths_invalid = true; + } + } + } + + outputs.required_slots_present = !required_slots_missing; + outputs.filepaths_valid = !filepaths_invalid; + + if !report.is_executable() { + errors.push( + "Trace failed semantic validation (slots or files missing)." + .to_string(), + ); + } else { + // Syntactically and statically valid. Let's check semantic tests if requested. + if let Some(cmd) = &task.validation_command { + // Run the validation command in the active workspace + let args: Vec<&str> = cmd.split_whitespace().collect(); + if !args.is_empty() { + let mut command = Command::new(args[0]); + if args.len() > 1 { + command.args(&args[1..]); + } + command.current_dir(&active_workspace); + if let Ok(output) = command.output() { + outputs.tests_pass = Some(output.status.success()); + outputs.code_compiles = Some(true); // Assuming tests compiled + } else { + outputs.tests_pass = Some(false); + errors.push(format!( + "Failed to execute validation command: {}", + cmd + )); + } + } + } + } + } + Err(e) => { + errors.push(format!("CRP parse error: {}", e)); + } + } + } else { + errors.push("Last message was not a final assistant response.".to_string()); + } + } else { + errors.push("No assistant response generated.".to_string()); + } + + // Run evaluator judge if we parsed successfully and have an evaluator + if outputs.parsed_successfully && evaluator_client.is_some() { + let eval_client = evaluator_client.as_ref().unwrap(); + let eval_prompt = format!( + "Task: {}\n\nTrace:\n{}\n\nDid this trace successfully accomplish the task? Output ONLY or .", + task.prompt, outputs.raw_response + ); + let payload = ChatRequestPayload { + mode: RequestMode::OpenAiChat { + messages: vec![ApiMessage { + role: "user".to_string(), + content: Some(eval_prompt), + name: None, + tool_call_id: None, + tool_calls: None, + }], + tools: vec![], + }, + }; + match eval_client.complete(payload).await { + Ok(res) => { + let score = res.text.trim(); + outputs.evaluator_score = Some(score.to_string()); + } + Err(e) => { + errors.push(format!("Evaluator error: {}", e)); + } + } + } + + let result = BatchResult { + task_id: task.id.clone(), + model: config.model.model.clone(), + crp_enabled: config + .harness + .reasoning_protocol + .eq_ignore_ascii_case("crp"), + metadata: BatchMetadata { + cinto_version: env!("CARGO_PKG_VERSION").to_string(), + timestamp: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + temperature: config.model.temperature, + thinking_effort: config.model.thinking_effort.clone(), + system_prompt: config.harness.system_prompt.clone(), + }, + outputs, + metrics, + errors, + }; + + let json = serde_json::to_string(&result)?; + writeln!(output_file, "{}", json)?; + output_file.flush()?; + + if is_temp_workspace { + std::fs::remove_dir_all(&active_workspace).ok(); + } else { + clean_workspace(&active_workspace).ok(); + } + } + + if !dry_run { + println!("Batch complete."); + } + Ok(()) +} + +// --------------------------------------------------------------------------- +// Kernel batch runner +// --------------------------------------------------------------------------- + +#[derive(Debug, Serialize, Default)] +struct KernelStageResult { + crp_valid: bool, + duration_ms: u128, + retries: u32, + error: Option, +} + +#[derive(Debug, Serialize)] +struct KernelBatchResult { + task_id: String, + model: String, + timestamp: u64, + stages_attempted: u32, + stages_completed: u32, + workflow_succeeded: bool, + files_changed: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + validation_passed: Option, + #[serde(skip_serializing_if = "Option::is_none")] + validation_output: Option, + interpret: Option, + locate: Option, + hypothesize: Option, + patch: Option, + report: Option, + total_duration_ms: u128, + errors: Vec, +} + +pub async fn run_kernel( + config: Config, + tasks_path: PathBuf, + output_path: PathBuf, + dry_run: bool, + budget: Option, + traces_dir: Option, +) -> Result<()> { + use crate::kernel::{ + index::index_repo, + worker::{WorkerEvent, WorkerLoop}, + }; + + if dry_run { + println!("Kernel batch DRY-RUN: validating task file only."); + } + + // ── Context window detection ───────────────────────────────────────────── + // Detect the actual context window from the server before running tasks. + // LM Studio loads models with a fixed n_ctx set in the GUI — if it differs + // from config.model.context_window, the config is lying and we'll overflow. + let effective_budget = if !dry_run { + let client = ModelClient::new(config.model.clone()); + match client.get_available_context_window().await { + actual if actual > 0 && actual < config.model.context_window => { + eprintln!( + "WARNING: server reports {}K context but config expects {}K.", + actual / 1024, + config.model.context_window / 1024, + ); + eprintln!( + " → Reload the model in LM Studio with context length: {}", + config.model.context_window + ); + eprintln!( + " → Using detected context ({} tokens) to size the budget.", + actual + ); + // Reserve ~30% for system/CRP/examples overhead and output. + // Remaining 70% at 4 chars/token = available context pack budget. + let chars = (actual as usize).saturating_sub(actual as usize * 30 / 100) * 4; + budget.unwrap_or(chars) + } + _ => budget.unwrap_or(crate::kernel::context_pack::DEFAULT_CODE_BUDGET), + } + } else { + budget.unwrap_or(crate::kernel::context_pack::DEFAULT_CODE_BUDGET) + }; + println!("Context pack budget: {} chars", effective_budget); + + let file = std::fs::File::open(&tasks_path) + .with_context(|| format!("failed to open {}", tasks_path.display()))?; + let reader = BufReader::new(file); + + if let Some(parent) = output_path.parent() { + if !parent.as_os_str().is_empty() { + std::fs::create_dir_all(parent) + .with_context(|| format!("failed to create {}", parent.display()))?; + } + } + + // Set up trace writer if requested + let mut trace_file = if let Some(ref dir) = traces_dir { + std::fs::create_dir_all(dir) + .with_context(|| format!("failed to create traces dir {}", dir.display()))?; + let ts = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + let trace_path = dir.join(format!("traces_{ts}.jsonl")); + println!("Saving traces to {}", trace_path.display()); + Some( + OpenOptions::new() + .create(true) + .append(true) + .open(&trace_path) + .with_context(|| format!("failed to open {}", trace_path.display()))?, + ) + } else { + None + }; + + let mut output_file = OpenOptions::new() + .create(true) + .append(true) + .open(&output_path) + .with_context(|| format!("failed to open {}", output_path.display()))?; + + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + + for (line_idx, line) in reader.lines().enumerate() { + let line = line?; + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + + let task: BatchTask = serde_json::from_str(line) + .with_context(|| format!("failed to parse task at line {}", line_idx + 1))?; + + println!("[kernel] task: {} — {}", task.id, &task.prompt[..task.prompt.len().min(60)]); + + if dry_run { + println!(" [dry-run] would run WorkerLoop on task {}", task.id); + continue; + } + + // ── Set up workspace ───────────────────────────────────────────────── + let (active_workspace, is_temp) = if let Some(fixture) = &task.fixture_dir { + let src = if fixture.is_absolute() { + fixture.clone() + } else { + std::env::current_dir()?.join(fixture) + }; + let tmp = std::env::temp_dir().join(format!("cinto_kernel_{}_{}", task.id, timestamp)); + copy_dir_all(&src, &tmp)?; + (tmp, true) + } else { + (config.harness.workspace.clone(), false) + }; + + // ── Pre-index the workspace ────────────────────────────────────────── + if let Ok(idx) = index_repo(&active_workspace) { + let _ = crate::kernel::index::save(&active_workspace, &idx); + } + + // ── Run WorkerLoop with concurrent event handler ───────────────────── + // Approvals must be answered during the workflow (the worker awaits + // response_rx), so we drain events in a separate task rather than + // collecting them post-hoc. + let (event_tx, event_rx) = mpsc::unbounded_channel::(); + let mut kernel_config = config.clone(); + kernel_config.harness.workspace = active_workspace.clone(); + + let mut worker = WorkerLoop::new(&active_workspace, kernel_config, task.prompt.clone(), event_tx); + worker = worker.with_budget(effective_budget); + + // Spawn event consumer — auto-approves patches, records all other events. + let event_handle: tokio::task::JoinHandle> = + tokio::spawn(async move { + let mut rx = event_rx; + let mut recorded = Vec::new(); + while let Some(event) = rx.recv().await { + match event { + WorkerEvent::PatchApprovalRequested { path, response_tx, .. } => { + println!(" [auto-approve] write {path}"); + let _ = response_tx.send(true); + } + e => recorded.push(e), + } + } + recorded + }); + + let start = Instant::now(); + let workflow_result = worker.run_bugfix().await; + let total_duration_ms = start.elapsed().as_millis(); + + // event_tx dropped when WorkerLoop is consumed — signals handler to finish. + let events = event_handle.await.unwrap_or_default(); + + // ── Build per-stage results from events ────────────────────────────── + let mut stage_results: std::collections::HashMap = + std::collections::HashMap::new(); + let mut current_stage: Option = None; + let mut stage_start: Option = None; + let mut retry_counts: std::collections::HashMap = std::collections::HashMap::new(); + let mut errors: Vec = Vec::new(); + let mut stages_attempted: u32 = 0; + let mut stages_completed: u32 = 0; + let mut files_changed: Vec = Vec::new(); + + // Replay events to reconstruct per-stage timing and validity + let event_start = Instant::now(); // approximate — events already happened + let mut pending_traces: Vec = Vec::new(); + for event in &events { + match event { + WorkerEvent::StageStarted { stage } => { + current_stage = Some(stage.clone()); + stage_start = Some(event_start); // approximate + stages_attempted += 1; + } + WorkerEvent::StageCompleted { stage, crp_valid } => { + let dur = stage_start.map(|s| s.elapsed().as_millis()).unwrap_or(0); + let retries = *retry_counts.get(stage).unwrap_or(&0); + stage_results.insert(stage.clone(), KernelStageResult { + crp_valid: *crp_valid, + duration_ms: dur, + retries, + error: None, + }); + stages_completed += 1; + current_stage = None; + } + WorkerEvent::StageRetry { stage, attempt, .. } => { + *retry_counts.entry(stage.clone()).or_default() = *attempt; + } + WorkerEvent::StageFailed { stage, error } => { + let dur = stage_start.map(|s| s.elapsed().as_millis()).unwrap_or(0); + let retries = *retry_counts.get(stage).unwrap_or(&0); + stage_results.insert(stage.clone(), KernelStageResult { + crp_valid: false, + duration_ms: dur, + retries, + error: Some(error.clone()), + }); + errors.push(format!("{stage}: {error}")); + } + WorkerEvent::WorkflowFailed { error } => { + errors.push(error.clone()); + } + WorkerEvent::PatchApplied { files_changed: changed } => { + println!(" [patch] {}", changed.join(", ")); + for f in changed { files_changed.push(f.clone()); } + } + WorkerEvent::StageSkipped { stage, reason } => { + errors.push(format!("{stage} skipped: {reason}")); + } + WorkerEvent::StageTrace { + stage, + attempt, + system_prompt, + user_message, + model_response, + crp_valid, + error, + finish_reason, + } => { + pending_traces.push(serde_json::json!({ + "task_id": task.id, + "model": config.model.model, + "stage": stage, + "attempt": attempt, + "system_prompt": system_prompt, + "user_message": user_message, + "model_response": model_response, + "crp_valid": crp_valid, + "error": error, + "finish_reason": finish_reason, + "workflow_succeeded": workflow_result.is_ok(), + "timestamp": timestamp, + })); + } + _ => {} + } + } + + if let Err(e) = &workflow_result { + errors.push(format!("{e:#}")); + } + + // ── Validation ─────────────────────────────────────────────────────── + let (validation_passed, validation_output) = + if workflow_result.is_ok() && !files_changed.is_empty() { + if let Some(cmd) = &task.validation_command { + let parts: Vec<&str> = cmd.split_whitespace().collect(); + match Command::new(parts[0]) + .args(&parts[1..]) + .current_dir(&active_workspace) + .output() + { + Ok(out) => { + let passed = out.status.success(); + let text = format!( + "{}{}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr) + ) + .trim() + .chars() + .take(800) + .collect::(); + println!(" [validate] {} — {}", if passed { "PASS" } else { "FAIL" }, cmd); + (Some(passed), Some(text)) + } + Err(e) => { + errors.push(format!("validation command failed: {e}")); + (Some(false), None) + } + } + } else { + (None, None) + } + } else { + (None, None) + }; + + let result = KernelBatchResult { + task_id: task.id.clone(), + model: config.model.model.clone(), + timestamp, + stages_attempted, + stages_completed, + workflow_succeeded: workflow_result.is_ok(), + files_changed, + validation_passed, + validation_output, + interpret: stage_results.remove("interpret"), + locate: stage_results.remove("locate"), + hypothesize: stage_results.remove("hypothesize"), + patch: stage_results.remove("patch"), + report: stage_results.remove("report"), + total_duration_ms, + errors, + }; + + let json = serde_json::to_string(&result)?; + writeln!(output_file, "{json}")?; + output_file.flush()?; + + // Flush traces for this task + if let Some(ref mut tf) = trace_file { + for trace in &pending_traces { + writeln!(tf, "{trace}")?; + } + tf.flush()?; + if !pending_traces.is_empty() { + println!(" traces: {} stage record(s) saved", pending_traces.len()); + } + } + + println!( + " stages: {}/{} — workflow: {} — {total_duration_ms}ms", + stages_completed, + stages_attempted, + if result.workflow_succeeded { "ok" } else { "failed" } + ); + + if is_temp { + std::fs::remove_dir_all(&active_workspace).ok(); + } + } + + if !dry_run { + println!("Kernel batch complete."); + } + Ok(()) +} + +fn copy_dir_all(src: &std::path::Path, dst: &std::path::Path) -> Result<()> { + std::fs::create_dir_all(dst)?; + for entry in std::fs::read_dir(src)? { + let entry = entry?; + let dst_path = dst.join(entry.file_name()); + if entry.file_type()?.is_dir() { + copy_dir_all(&entry.path(), &dst_path)?; + } else { + std::fs::copy(entry.path(), dst_path)?; + } + } + Ok(()) +} diff --git a/src/config.rs b/src/config.rs index 3399ac0..ecdeb25 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,8 +1,78 @@ -use std::{fs, path::PathBuf}; + use std::{fs, path::PathBuf}; use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; +// --------------------------------------------------------------------------- +// Config presets — embedded TOML profiles for common server setups +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone)] +pub struct ConfigPreset { + pub name: &'static str, + pub label: &'static str, + pub description: &'static str, + toml: &'static str, +} + +impl ConfigPreset { + /// Parse this preset's TOML into a partial `Config`, patching in runtime + /// workspace and prompt strings from the global default. + pub fn to_config(&self) -> Result { + let mut config: Config = toml::from_str(self.toml) + .with_context(|| format!("failed to parse built-in preset `{}`", self.name))?; + config.normalize(); + Ok(config) + } +} + +pub const PRESETS: &[ConfigPreset] = &[ + ConfigPreset { + name: "lm-studio", + label: "LM Studio", + description: "Local LM Studio server with Harmony/gpt-oss defaults", + toml: include_str!("presets/lm_studio.toml"), + }, + ConfigPreset { + name: "lm-studio-small", + label: "LM Studio (small model)", + description: "LM Studio with openai-tools format for sub-4B models (Gemma, Phi, etc.)", + toml: include_str!("presets/lm_studio_small.toml"), + }, + ConfigPreset { + name: "qwen3", + label: "Qwen3 / Qwen3.5", + description: "Qwen3-class local models with /no_think enabled", + toml: include_str!("presets/qwen3.toml"), + }, + ConfigPreset { + name: "ollama", + label: "Ollama", + description: "Local Ollama server with OpenAI-tools format", + toml: include_str!("presets/ollama.toml"), + }, + ConfigPreset { + name: "vllm", + label: "vLLM / OpenAI-compat", + description: "vLLM or any OpenAI-compatible API server", + toml: include_str!("presets/vllm.toml"), + }, + ConfigPreset { + name: "minimal", + label: "Minimal", + description: "Bare-bones config without CRP reasoning", + toml: include_str!("presets/minimal.toml"), + }, +]; + +pub fn preset_by_name(name: &str) -> Option<&'static ConfigPreset> { + PRESETS.iter().find(|p| p.name == name) +} + +pub fn preset_index(name: &str) -> Option { + PRESETS.iter().position(|p| p.name == name) +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { pub model: ModelConfig, @@ -21,6 +91,10 @@ pub struct ModelConfig { pub temperature: f32, #[serde(default = "default_thinking_effort")] pub thinking_effort: String, + #[serde(default)] + pub no_think: bool, + #[serde(default = "default_no_think_prefix")] + pub no_think_prefix: String, #[serde(default = "default_stream")] pub stream: bool, pub stop: Vec, @@ -36,8 +110,20 @@ pub struct HarnessConfig { pub allow_shell: bool, #[serde(default = "default_require_edit_approval")] pub require_edit_approval: bool, + #[serde(default = "default_reasoning_protocol")] + pub reasoning_protocol: String, + #[serde(default = "default_crp_retry_budget")] + pub crp_retry_budget: u32, + #[serde(default = "default_transport_retry_budget")] + pub transport_retry_budget: u32, + #[serde(default = "default_empty_response_retry_budget")] + pub empty_response_retry_budget: u32, + #[serde(default = "default_template")] + pub default_template: String, #[serde(default = "default_max_tool_turns")] pub max_tool_turns: u32, + #[serde(default = "default_max_model_rounds")] + pub max_model_rounds: u32, #[serde(default = "default_auto_context_compression")] pub auto_context_compression: bool, #[serde(default = "default_context_compression_threshold")] @@ -63,6 +149,8 @@ impl Default for Config { max_tokens: 4096, temperature: 0.2, thinking_effort: default_thinking_effort(), + no_think: false, + no_think_prefix: default_no_think_prefix(), stream: default_stream(), stop: vec!["<|return|>".to_string(), "<|call|>".to_string()], request_timeout_secs: default_timeout_secs(), @@ -72,7 +160,13 @@ impl Default for Config { workspace, allow_shell: false, require_edit_approval: default_require_edit_approval(), + reasoning_protocol: default_reasoning_protocol(), + crp_retry_budget: default_crp_retry_budget(), + transport_retry_budget: default_transport_retry_budget(), + empty_response_retry_budget: default_empty_response_retry_budget(), + default_template: default_template(), max_tool_turns: default_max_tool_turns(), + max_model_rounds: default_max_model_rounds(), auto_context_compression: default_auto_context_compression(), context_compression_threshold: default_context_compression_threshold(), context_compression_keep_recent: default_context_compression_keep_recent(), @@ -96,8 +190,10 @@ impl Config { let contents = fs::read_to_string(&path) .with_context(|| format!("failed to read config at {}", path.display()))?; - toml::from_str(&contents) - .with_context(|| format!("failed to parse config at {}", path.display())) + let mut config: Self = toml::from_str(&contents) + .with_context(|| format!("failed to parse config at {}", path.display()))?; + config.normalize(); + Ok(config) } pub fn save(&self, path: Option) -> Result { @@ -119,6 +215,62 @@ impl Config { pub fn default_path() -> Option { dirs::config_dir().map(|dir| dir.join("cinto").join("config.toml")) } + + fn normalize(&mut self) { + self.model.format = self.model.format.trim().to_ascii_lowercase(); + self.harness.reasoning_protocol = + self.harness.reasoning_protocol.trim().to_ascii_lowercase(); + + if self.model.format == "crp" { + self.model.format = "harmony".to_string(); + self.harness.reasoning_protocol = "crp".to_string(); + } + } + + /// Returns the maximum context window supported by the configured model provider. + /// Uses provider-specific defaults: Ollama (16k), LM Studio (32k), vLLM/OpenAI-compat (128k). + pub fn provider_max_context_window(&self) -> u32 { + let endpoint = self.model.endpoint.to_lowercase(); + + // Detect Ollama by default port or path + if endpoint.contains(":11434") || endpoint.contains("/api/chat") { + return 16_000; + } + + // LM Studio typically uses Harmony/gpt-oss with large context support + if endpoint.contains(":1234") && (self.model.format == "harmony" || self.model.model.contains("gpt-oss")) { + return 200_000; + } + + // vLLM or generic OpenAI-compatible server - assume large context support + if endpoint.contains(":8000") || endpoint.contains("/v1/chat/completions") { + return 128_000; + } + + // Default fallback for unknown providers + 32_768 + } + + pub fn apply_no_think_prefix(&self, prompt: String) -> String { + let Some(prefix) = self.model.no_think_directive() else { + return prompt; + }; + format!("{prefix}\n\n{prompt}") + } +} + +impl ModelConfig { + pub fn no_think_directive(&self) -> Option<&str> { + if !self.no_think { + return None; + } + let prefix = self.no_think_prefix.trim(); + if prefix.is_empty() { + None + } else { + Some(prefix) + } + } } fn default_timeout_secs() -> u64 { @@ -126,11 +278,23 @@ fn default_timeout_secs() -> u64 { } fn default_context_window() -> u32 { - 8192 + 32768 } fn default_max_tool_turns() -> u32 { - 16 + 24 +} + +fn default_max_model_rounds() -> u32 { + 64 +} + +fn default_transport_retry_budget() -> u32 { + 1 +} + +fn default_empty_response_retry_budget() -> u32 { + 1 } fn default_auto_context_compression() -> bool { @@ -153,10 +317,26 @@ fn default_require_edit_approval() -> bool { true } +fn default_reasoning_protocol() -> String { + "crp".to_string() +} + +fn default_crp_retry_budget() -> u32 { + 3 +} + +fn default_template() -> String { + crate::crp::DEFAULT_TEMPLATE_NAME.to_string() +} + fn default_thinking_effort() -> String { "medium".to_string() } +fn default_no_think_prefix() -> String { + "/no_think".to_string() +} + fn default_format() -> String { "harmony".to_string() } @@ -169,6 +349,14 @@ fn default_stream() -> bool { mod tests { use super::*; + #[test] + fn defaults_to_harmony_transport_with_crp_reasoning() { + let config = Config::default(); + + assert_eq!(config.model.format, "harmony"); + assert_eq!(config.harness.reasoning_protocol, "crp"); + } + #[test] fn saves_and_loads_config() { let path = @@ -186,4 +374,112 @@ mod tests { let _ = fs::remove_file(path); } + + #[test] + fn normalizes_legacy_crp_format() { + let path = std::env::temp_dir().join(format!( + "cinto-legacy-crp-format-test-{}.toml", + std::process::id() + )); + fs::write( + &path, + r#" +[model] +endpoint = "http://127.0.0.1:1234" +model = "openai/gpt-oss-20b" +format = "crp" +max_tokens = 4096 +temperature = 0.2 +stop = [] + +[harness] +workspace = "." +allow_shell = false +system_prompt = "system" +developer_prompt = "developer" +"#, + ) + .expect("write config"); + + let loaded = Config::load(Some(path.clone())).expect("load config"); + + assert_eq!(loaded.model.format, "harmony"); + assert_eq!(loaded.harness.reasoning_protocol, "crp"); + + let _ = fs::remove_file(path); + } + + #[test] + fn all_presets_parse_successfully() { + for preset in PRESETS { + let config = preset + .to_config() + .unwrap_or_else(|e| panic!("preset `{}` failed to parse: {e}", preset.name)); + assert!( + !config.model.endpoint.is_empty(), + "preset `{}` has empty endpoint", + preset.name + ); + assert!( + !config.model.model.is_empty(), + "preset `{}` has empty model", + preset.name + ); + } + } + + #[test] + fn preset_lm_studio_uses_harmony_and_crp() { + let preset = preset_by_name("lm-studio").expect("lm-studio preset exists"); + let config = preset.to_config().expect("parse"); + assert_eq!(config.model.format, "harmony"); + assert_eq!(config.harness.reasoning_protocol, "crp"); + assert!(config.model.endpoint.contains("1234")); + } + + #[test] + fn preset_ollama_uses_openai_tools() { + let preset = preset_by_name("ollama").expect("ollama preset exists"); + let config = preset.to_config().expect("parse"); + assert_eq!(config.model.format, "openai-tools"); + assert!(config.model.endpoint.contains("11434")); + } + + #[test] + fn preset_qwen3_enables_no_think() { + let preset = preset_by_name("qwen3").expect("qwen3 preset exists"); + let config = preset.to_config().expect("parse"); + assert_eq!(config.model.format, "openai-tools"); + assert_eq!(config.model.no_think_directive(), Some("/no_think")); + } + + #[test] + fn preset_minimal_disables_crp() { + let preset = preset_by_name("minimal").expect("minimal preset exists"); + let config = preset.to_config().expect("parse"); + assert_eq!(config.harness.reasoning_protocol, "plain"); + assert_eq!(config.harness.crp_retry_budget, 0); + } + + #[test] + fn default_context_window_is_32k() { + let config = Config::default(); + assert_eq!(config.model.context_window, 32768); + } + + #[test] + fn default_max_tool_turns_is_24() { + let config = Config::default(); + assert_eq!(config.harness.max_tool_turns, 24); + } + + #[test] + fn default_retry_and_round_budgets_are_separate() { + let config = Config::default(); + + assert_eq!(config.harness.max_model_rounds, 64); + assert_eq!(config.harness.crp_retry_budget, 3); + assert_eq!(config.harness.transport_retry_budget, 1); + assert_eq!(config.harness.empty_response_retry_budget, 1); + } } diff --git a/src/crp.rs b/src/crp.rs new file mode 100644 index 0000000..e264a87 --- /dev/null +++ b/src/crp.rs @@ -0,0 +1,1280 @@ +use std::{ + collections::BTreeMap, + error::Error, + fmt, fs, + ops::Range, + path::{Path, PathBuf}, +}; + +use serde::Deserialize; + +pub const VERSION: &str = "1.0-draft"; + +pub fn default_required_slots() -> Vec { + vec!["FINAL_RESPONSE".to_string()] +} + +pub fn default_file_path_slots() -> Vec { + vec!["RELEVANT_FILES".to_string()] +} + +#[derive(Debug, Clone)] +pub struct ValidationConfig<'a> { + pub workspace: Option<&'a Path>, + pub required_slots: Vec, + pub hard_required_slots: Vec, + pub file_path_slots: Vec, +} + +impl Default for ValidationConfig<'_> { + fn default() -> Self { + Self { + workspace: None, + required_slots: default_required_slots(), + hard_required_slots: default_required_slots(), + file_path_slots: default_file_path_slots(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SlotStatus { + Valid, + Warning, + Invalid, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SlotOutcome { + pub slot: String, + pub status: SlotStatus, + pub message: String, +} + +#[derive(Debug, Clone, Default)] +pub struct ValidationReport { + pub outcomes: Vec, +} + +impl ValidationReport { + pub fn is_executable(&self) -> bool { + !self + .outcomes + .iter() + .any(|outcome| outcome.status == SlotStatus::Invalid) + } + + pub fn invalid(&self) -> impl Iterator { + self.outcomes + .iter() + .filter(|outcome| outcome.status == SlotStatus::Invalid) + } + + pub fn warnings(&self) -> impl Iterator { + self.outcomes + .iter() + .filter(|outcome| outcome.status == SlotStatus::Warning) + } +} + +pub fn validate(trace: &Trace, config: &ValidationConfig<'_>) -> ValidationReport { + let mut outcomes = Vec::new(); + + for required in &config.required_slots { + let hard_required = config + .hard_required_slots + .iter() + .any(|slot| slot == required); + let status = if hard_required { + SlotStatus::Invalid + } else { + SlotStatus::Warning + }; + match trace.get(required.as_str()) { + None => outcomes.push(SlotOutcome { + slot: required.clone(), + status, + message: missing_slot_message(required, hard_required), + }), + Some(slot) if slot.content.trim().is_empty() => outcomes.push(SlotOutcome { + slot: required.clone(), + status, + message: empty_slot_message(required, hard_required), + }), + Some(_) => {} + } + } + + if let Some(workspace) = config.workspace { + for slot_name in &config.file_path_slots { + for slot in trace.get_all(slot_name.as_str()) { + let mut missing = Vec::new(); + for path in extract_bullet_paths(&slot.content) { + if !workspace.join(&path).exists() { + missing.push(path); + } + } + if !missing.is_empty() { + outcomes.push(SlotOutcome { + slot: slot_name.clone(), + status: SlotStatus::Invalid, + message: format!( + "Path(s) listed in <{slot_name}> do not exist in the workspace: {}. Use the read/list tools to confirm before listing files.", + missing.join(", ") + ), + }); + } + } + } + } + + ValidationReport { outcomes } +} + +fn missing_slot_message(slot: &str, hard_required: bool) -> String { + if hard_required { + format!("Required slot <{slot}> is missing. Emit it with non-empty content.") + } else { + format!( + "Recommended slot <{slot}> is missing. Emit it when it helps make the trace auditable." + ) + } +} + +fn empty_slot_message(slot: &str, hard_required: bool) -> String { + if hard_required { + format!("Required slot <{slot}> is present but empty. Provide concrete content.") + } else { + format!( + "Recommended slot <{slot}> is present but empty. Provide concrete content or omit it." + ) + } +} + +pub fn build_retry_message(report: &ValidationReport) -> String { + let mut buf = String::new(); + buf.push_str("\n"); + buf.push_str( + "The previous response failed CRP validation. Produce a new complete CRP trace addressing the feedback below.\n", + ); + buf.push_str("\n"); + + for outcome in &report.outcomes { + if outcome.status == SlotStatus::Valid { + continue; + } + let status = match outcome.status { + SlotStatus::Invalid => "INVALID", + SlotStatus::Warning => "WARNING", + SlotStatus::Valid => continue, + }; + buf.push('\n'); + buf.push_str(&format!( + "\n{}\n\n", + outcome.slot, status, outcome.message + )); + } + + buf +} + +pub fn build_parse_retry_message(error: &ParseError) -> String { + format!( + "\nThe previous response could not be parsed as a CRP trace ({error}). Re-emit the entire answer using uppercase CRP slot tags such as ... and do not include prose outside slots.\n\n" + ) +} + +fn extract_bullet_paths(content: &str) -> Vec { + let mut paths = Vec::new(); + for line in content.lines() { + let trimmed = line.trim(); + if trimmed.is_empty() { + continue; + } + let stripped = trimmed + .strip_prefix("- ") + .or_else(|| trimmed.strip_prefix("* ")) + .or_else(|| trimmed.strip_prefix("• ")) + .unwrap_or(trimmed); + let candidate = stripped.split_whitespace().next().unwrap_or("").trim(); + if candidate.is_empty() { + continue; + } + paths.push( + candidate + .trim_end_matches(|c: char| matches!(c, ',' | ';')) + .to_string(), + ); + } + paths +} + +// --------------------------------------------------------------------------- +// Templates +// --------------------------------------------------------------------------- + +const BUILTIN_CODE_EDIT: &str = include_str!("../templates/code_edit.toml"); +const BUILTIN_CODE_EDIT_MINIMAL: &str = include_str!("../templates/code_edit_minimal.toml"); +const BUILTIN_CODE_EDIT_THOROUGH: &str = include_str!("../templates/code_edit_thorough.toml"); +const BUILTIN_CODE_EXPLANATION: &str = include_str!("../templates/code_explanation.toml"); +const BUILTIN_DESIGN_PROPOSAL: &str = include_str!("../templates/design_proposal.toml"); + +pub const DEFAULT_TEMPLATE_NAME: &str = "code_edit"; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Template { + pub name: String, + pub description: String, + pub slots: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TemplateSlot { + pub name: String, + pub required: bool, + pub kind: TemplateSlotKind, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TemplateSlotKind { + Generic, + FilePathList, +} + +#[derive(Debug, Clone, Default)] +pub struct TemplateSet { + by_name: BTreeMap, +} + +impl TemplateSet { + pub fn builtin() -> Self { + let mut set = Self::default(); + for source in [ + BUILTIN_CODE_EDIT, + BUILTIN_CODE_EDIT_MINIMAL, + BUILTIN_CODE_EDIT_THOROUGH, + BUILTIN_CODE_EXPLANATION, + BUILTIN_DESIGN_PROPOSAL, + ] { + let template = Template::from_toml_str(source).expect("builtin template parses"); + set.insert(template); + } + set + } + + pub fn dump_builtins(dir: &Path) -> std::io::Result<()> { + std::fs::create_dir_all(dir)?; + std::fs::write(dir.join("code_edit.toml"), BUILTIN_CODE_EDIT)?; + std::fs::write( + dir.join("code_edit_minimal.toml"), + BUILTIN_CODE_EDIT_MINIMAL, + )?; + std::fs::write( + dir.join("code_edit_thorough.toml"), + BUILTIN_CODE_EDIT_THOROUGH, + )?; + std::fs::write(dir.join("code_explanation.toml"), BUILTIN_CODE_EXPLANATION)?; + std::fs::write(dir.join("design_proposal.toml"), BUILTIN_DESIGN_PROPOSAL)?; + Ok(()) + } + + pub fn load(workspace_overlay: Option<&Path>) -> Self { + let mut set = Self::builtin(); + if let Some(dir) = workspace_overlay + && dir.is_dir() + && let Ok(entries) = fs::read_dir(dir) + { + for entry in entries.flatten() { + let path = entry.path(); + if path.extension().and_then(|ext| ext.to_str()) != Some("toml") { + continue; + } + match Template::from_path(&path) { + Ok(template) => set.insert(template), + Err(error) => eprintln!( + "warning: failed to load template {}: {error}", + path.display() + ), + } + } + } + set + } + + pub fn insert(&mut self, template: Template) { + self.by_name.insert(template.name.clone(), template); + } + + pub fn get(&self, name: &str) -> Option<&Template> { + self.by_name.get(name) + } + + pub fn select(&self, name: &str) -> &Template { + if let Some(template) = self.by_name.get(name) { + return template; + } + if let Some(template) = self.by_name.get(DEFAULT_TEMPLATE_NAME) { + eprintln!( + "warning: template `{name}` not found; falling back to `{DEFAULT_TEMPLATE_NAME}`" + ); + return template; + } + self.by_name + .values() + .next() + .expect("template set must contain at least the builtin code_edit") + } + + pub fn names(&self) -> impl Iterator { + self.by_name.keys().map(String::as_str) + } + + /// Select a template qualified by thinking effort. + /// + /// Maps `thinking_effort` to a suffix (`_minimal` for none/low, `_thorough` + /// for high) and looks up `"{base_name}_{suffix}"`. Falls back silently to + /// the base template when an effort-specific variant is not found. + pub fn resolve(&self, base_name: &str, thinking_effort: &str) -> &Template { + let qualified = effort_qualified_name(base_name, thinking_effort); + if let Some(name) = &qualified { + if let Some(template) = self.by_name.get(name.as_str()) { + return template; + } + } + self.select(base_name) + } +} + +/// Compute the effort-qualified template name, if the effort level has a +/// specific variant. +fn effort_qualified_name(base_name: &str, thinking_effort: &str) -> Option { + match thinking_effort.to_ascii_lowercase().as_str() { + "none" | "low" => Some(format!("{base_name}_minimal")), + "high" => Some(format!("{base_name}_thorough")), + _ => None, // "medium" and others use the base template + } +} + +impl Template { + pub fn from_toml_str(source: &str) -> Result { + let raw: RawTemplate = + toml::from_str(source).map_err(|error| TemplateLoadError::Parse(error.to_string()))?; + if raw.name.trim().is_empty() { + return Err(TemplateLoadError::Parse( + "template `name` is required".to_string(), + )); + } + + let mut slots = Vec::with_capacity(raw.slots.len()); + for (name, spec) in raw.slots { + if !is_slot_name(&name) { + return Err(TemplateLoadError::Parse(format!( + "slot name `{name}` must be uppercase ASCII" + ))); + } + let kind = match spec.r#type.as_deref() { + Some(value) if value.eq_ignore_ascii_case("BulletList") => { + TemplateSlotKind::FilePathList + } + _ => TemplateSlotKind::Generic, + }; + slots.push(TemplateSlot { + name, + required: spec.required.unwrap_or(false), + kind, + }); + } + slots.sort_by(|a, b| a.name.cmp(&b.name)); + + Ok(Self { + name: raw.name, + description: raw.description.unwrap_or_default(), + slots, + }) + } + + pub fn from_path(path: &Path) -> Result { + let source = fs::read_to_string(path).map_err(TemplateLoadError::Io)?; + Self::from_toml_str(&source) + } + + pub fn validation_config<'a>(&self, workspace: Option<&'a Path>) -> ValidationConfig<'a> { + let required_slots = self + .slots + .iter() + .filter(|slot| slot.required) + .map(|slot| slot.name.clone()) + .collect(); + let hard_required_slots = self + .slots + .iter() + .filter(|slot| slot.required && slot.name == "FINAL_RESPONSE") + .map(|slot| slot.name.clone()) + .collect(); + let file_path_slots = self + .slots + .iter() + .filter(|slot| slot.kind == TemplateSlotKind::FilePathList) + .map(|slot| slot.name.clone()) + .collect(); + ValidationConfig { + workspace, + required_slots, + hard_required_slots, + file_path_slots, + } + } + + pub fn render_brief(&self) -> String { + let hard_required: Vec<&str> = self + .slots + .iter() + .filter(|slot| slot.required && slot.name == "FINAL_RESPONSE") + .map(|slot| slot.name.as_str()) + .collect(); + let recommended: Vec<&str> = self + .slots + .iter() + .filter(|slot| slot.required && slot.name != "FINAL_RESPONSE") + .map(|slot| slot.name.as_str()) + .collect(); + let optional: Vec<&str> = self + .slots + .iter() + .filter(|slot| !slot.required) + .map(|slot| slot.name.as_str()) + .collect(); + + let mut buf = format!("\n", self.name); + if !self.description.is_empty() { + buf.push_str(&format!("description: {}\n", self.description)); + } + if hard_required.is_empty() { + buf.push_str("hard_required: (none)\n"); + } else { + buf.push_str(&format!("hard_required: {}\n", hard_required.join(", "))); + } + if !recommended.is_empty() { + buf.push_str(&format!("recommended: {}\n", recommended.join(", "))); + } + if !optional.is_empty() { + buf.push_str(&format!("optional: {}\n", optional.join(", "))); + } + buf.push_str("\n"); + buf + } +} + +#[derive(Debug)] +pub enum TemplateLoadError { + Parse(String), + Io(std::io::Error), +} + +impl fmt::Display for TemplateLoadError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TemplateLoadError::Parse(message) => write!(f, "{message}"), + TemplateLoadError::Io(error) => write!(f, "{error}"), + } + } +} + +impl Error for TemplateLoadError {} + +#[derive(Debug, Deserialize)] +struct RawTemplate { + name: String, + description: Option, + #[serde(default)] + slots: BTreeMap, +} + +#[derive(Debug, Deserialize)] +struct RawTemplateSlot { + required: Option, + r#type: Option, +} + +pub fn workspace_template_dir(workspace: &Path) -> PathBuf { + workspace.join(".cinto").join("templates") +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Trace { + pub slots: Vec, +} + +impl Trace { + pub fn get(&self, name: &str) -> Option<&Slot> { + self.slots.iter().find(|slot| slot.name == name) + } + + pub fn get_all<'a>(&'a self, name: &'a str) -> impl Iterator + 'a { + self.slots.iter().filter(move |slot| slot.name == name) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Slot { + pub name: String, + pub attributes: Vec, + pub content: String, + pub byte_range: Range, +} + +impl Slot { + pub fn attribute(&self, name: &str) -> Option<&str> { + self.attributes + .iter() + .find(|attribute| attribute.name == name) + .map(|attribute| attribute.value.as_str()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Attribute { + pub name: String, + pub value: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ParseError { + pub position: usize, + pub kind: ParseErrorKind, +} + +impl ParseError { + fn new(position: usize, kind: ParseErrorKind) -> Self { + Self { position, kind } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ParseErrorKind { + NoSlots, + ExpectedOpeningTag, + UnexpectedClosingTag, + UnterminatedOpeningTag, + InvalidSlotName(String), + InvalidAttribute(String), + MissingClosingTag { slot: String }, + MismatchedClosingTag { expected: String, found: String }, +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.kind { + ParseErrorKind::NoSlots => write!(f, "CRP trace contains no slots"), + ParseErrorKind::ExpectedOpeningTag => { + write!(f, "expected CRP opening tag at byte {}", self.position) + } + ParseErrorKind::UnexpectedClosingTag => { + write!(f, "unexpected CRP closing tag at byte {}", self.position) + } + ParseErrorKind::UnterminatedOpeningTag => { + write!(f, "unterminated CRP opening tag at byte {}", self.position) + } + ParseErrorKind::InvalidSlotName(name) => { + write!( + f, + "invalid CRP slot name `{name}` at byte {}", + self.position + ) + } + ParseErrorKind::InvalidAttribute(attribute) => { + write!( + f, + "invalid CRP attribute `{attribute}` at byte {}", + self.position + ) + } + ParseErrorKind::MissingClosingTag { slot } => { + write!(f, "missing CRP closing tag ") + } + ParseErrorKind::MismatchedClosingTag { expected, found } => { + write!( + f, + "mismatched CRP closing tag: expected , found " + ) + } + } + } +} + +impl Error for ParseError {} + +pub fn parse(input: &str) -> Result { + let mut parser = Parser::new(input); + let slots = parser.parse_slots()?; + Ok(Trace { slots }) +} + +pub fn active_slot(input: &str) -> Option { + let mut position = 0; + let mut active: Option = None; + + loop { + if let Some(name) = active.as_deref() { + let close_tag = format!(""); + let Some(relative_end) = input[position..].find(&close_tag) else { + return active; + }; + position += relative_end + close_tag.len(); + active = None; + continue; + } + + let Some(relative_start) = input[position..].find('<') else { + return None; + }; + let tag_start = position + relative_start; + if input[tag_start..].starts_with("') else { + return None; + }; + let tag_end = tag_start + relative_end; + let inner = &input[tag_start + 1..tag_end]; + let trimmed = inner.trim_end(); + if trimmed.is_empty() + || inner + .chars() + .next() + .is_some_and(|character| character.is_whitespace()) + { + position = tag_end + 1; + continue; + } + + let name_end = trimmed + .find(|character: char| character.is_ascii_whitespace()) + .unwrap_or(trimmed.len()); + let name = &trimmed[..name_end]; + if is_slot_name(name) { + active = Some(name.to_string()); + } + position = tag_end + 1; + } +} + +struct Parser<'a> { + input: &'a str, + position: usize, +} + +impl<'a> Parser<'a> { + fn new(input: &'a str) -> Self { + Self { input, position: 0 } + } + + fn parse_slots(&mut self) -> Result, ParseError> { + let mut slots = Vec::new(); + self.skip_whitespace(); + + while self.position < self.input.len() { + let slot = self.parse_slot()?; + slots.push(slot); + self.skip_whitespace(); + } + + if slots.is_empty() { + return Err(ParseError::new(0, ParseErrorKind::NoSlots)); + } + + Ok(slots) + } + + fn parse_slot(&mut self) -> Result { + let start = self.position; + let opening = self.parse_opening_tag()?; + let close_tag = format!("", opening.name); + let content_start = self.position; + let Some(relative_content_end) = self.input[content_start..].find(&close_tag) else { + if let Some((found, position)) = find_closing_tag(self.input, content_start) { + return Err(ParseError::new( + position, + ParseErrorKind::MismatchedClosingTag { + expected: opening.name, + found, + }, + )); + } + + return Err(ParseError::new( + content_start, + ParseErrorKind::MissingClosingTag { slot: opening.name }, + )); + }; + + let content_end = content_start + relative_content_end; + let end = content_end + close_tag.len(); + self.position = end; + + Ok(Slot { + name: opening.name, + attributes: opening.attributes, + content: self.input[content_start..content_end].trim().to_string(), + byte_range: start..end, + }) + } + + fn parse_opening_tag(&mut self) -> Result { + let start = self.position; + if !self.input[start..].starts_with('<') { + return Err(ParseError::new(start, ParseErrorKind::ExpectedOpeningTag)); + } + if self.input[start..].starts_with("') else { + return Err(ParseError::new( + start, + ParseErrorKind::UnterminatedOpeningTag, + )); + }; + let end = start + relative_end; + let inner = &self.input[start + 1..end]; + let trimmed = inner.trim_end(); + if trimmed.is_empty() { + return Err(ParseError::new( + start + 1, + ParseErrorKind::InvalidSlotName(String::new()), + )); + } + if inner + .chars() + .next() + .is_some_and(|character| character.is_whitespace()) + { + return Err(ParseError::new( + start + 1, + ParseErrorKind::InvalidSlotName(trimmed.to_string()), + )); + } + if trimmed.ends_with('/') { + return Err(ParseError::new( + start + 1, + ParseErrorKind::InvalidSlotName(trimmed.to_string()), + )); + } + + let name_end = trimmed + .find(|character: char| character.is_ascii_whitespace()) + .unwrap_or(trimmed.len()); + let name = &trimmed[..name_end]; + if !is_slot_name(name) { + return Err(ParseError::new( + start + 1, + ParseErrorKind::InvalidSlotName(name.to_string()), + )); + } + + let attributes = parse_attributes(&trimmed[name_end..], start + 1 + name_end)?; + self.position = end + 1; + + Ok(OpeningTag { + name: name.to_string(), + attributes, + }) + } + + fn skip_whitespace(&mut self) { + while let Some(character) = self.input[self.position..].chars().next() { + if !character.is_whitespace() { + break; + } + self.position += character.len_utf8(); + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct OpeningTag { + name: String, + attributes: Vec, +} + +fn parse_attributes(input: &str, base_position: usize) -> Result, ParseError> { + let mut attributes = Vec::new(); + let mut position = 0; + + while position < input.len() { + position = skip_ascii_whitespace(input, position); + if position >= input.len() { + break; + } + + let name_start = position; + while position < input.len() { + let byte = input.as_bytes()[position]; + if !is_attribute_name_byte(byte) { + break; + } + position += 1; + } + if position == name_start { + return Err(ParseError::new( + base_position + position, + ParseErrorKind::InvalidAttribute(input[position..].to_string()), + )); + } + + let name = &input[name_start..position]; + position = skip_ascii_whitespace(input, position); + if input.as_bytes().get(position) != Some(&b'=') { + return Err(ParseError::new( + base_position + position, + ParseErrorKind::InvalidAttribute(name.to_string()), + )); + } + position += 1; + position = skip_ascii_whitespace(input, position); + + let Some(quote) = input.as_bytes().get(position).copied() else { + return Err(ParseError::new( + base_position + position, + ParseErrorKind::InvalidAttribute(name.to_string()), + )); + }; + if quote != b'"' && quote != b'\'' { + return Err(ParseError::new( + base_position + position, + ParseErrorKind::InvalidAttribute(name.to_string()), + )); + } + position += 1; + + let value_start = position; + while input + .as_bytes() + .get(position) + .is_some_and(|byte| *byte != quote) + { + position += 1; + } + if position >= input.len() { + return Err(ParseError::new( + base_position + value_start, + ParseErrorKind::InvalidAttribute(name.to_string()), + )); + } + + attributes.push(Attribute { + name: name.to_string(), + value: input[value_start..position].to_string(), + }); + position += 1; + } + + Ok(attributes) +} + +fn find_closing_tag(input: &str, start: usize) -> Option<(String, usize)> { + let mut search_start = start; + + while let Some(relative_start) = input[search_start..].find("') else { + return None; + }; + let name_end = name_start + relative_end; + let name = &input[name_start..name_end]; + if is_slot_name(name) { + return Some((name.to_string(), tag_start)); + } + search_start = name_end + 1; + } + + None +} + +fn skip_ascii_whitespace(input: &str, mut position: usize) -> usize { + while input + .as_bytes() + .get(position) + .is_some_and(|byte| byte.is_ascii_whitespace()) + { + position += 1; + } + position +} + +fn is_slot_name(name: &str) -> bool { + let mut characters = name.chars(); + let Some(first) = characters.next() else { + return false; + }; + if !first.is_ascii_uppercase() { + return false; + } + characters.all(|character| { + character.is_ascii_uppercase() || character.is_ascii_digit() || character == '_' + }) +} + +fn is_attribute_name_byte(byte: u8) -> bool { + byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'-' +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn exposes_supported_version() { + assert_eq!(VERSION, "1.0-draft"); + } + + #[test] + fn parses_sequential_slots_and_trims_content() { + let trace = parse( + r#" + + Add a parser module. + + + + Done. + +"#, + ) + .expect("valid CRP"); + + assert_eq!(trace.slots.len(), 2); + assert_eq!(trace.slots[0].name, "TASK_INTERPRETATION"); + assert_eq!(trace.slots[0].content, "Add a parser module."); + assert_eq!(trace.slots[1].content, "Done."); + } + + #[test] + fn preserves_unknown_slots_and_attributes() { + let trace = parse( + r#" +src/missing.rs does not exist. +"#, + ) + .expect("valid CRP"); + + let slot = trace.get("SLOT_FEEDBACK").expect("slot"); + assert_eq!(slot.attribute("slot"), Some("RELEVANT_FILES")); + assert_eq!(slot.attribute("status"), Some("INVALID")); + assert_eq!(slot.content, "src/missing.rs does not exist."); + } + + #[test] + fn treats_content_as_opaque_until_matching_close_tag() { + let trace = parse( + r#" +
HTML is fine.
+let text = ""; +
"#, + ) + .expect("valid CRP"); + + assert!(trace.slots[0].content.contains("")); + assert!(trace.slots[0].content.contains("")); + } + + #[test] + fn keeps_file_edit_subblocks_as_slot_content() { + let trace = parse( + r#" + +fn hello() {} + +"#, + ) + .expect("valid CRP"); + + let edits = trace.get("FILE_EDITS").expect("file edits"); + assert!(edits.content.contains("")); + } + + #[test] + fn preserves_diff_like_file_edits_as_slot_content() { + let trace = parse( + r#" +@@ src/main.rs prepend +fn hello() { println!("hi"); } +@@ src/lib.rs replace_function:greet +pub fn greet() -> String { "hello".into() } +"#, + ) + .expect("valid CRP"); + + let edits = trace.get("FILE_EDITS").expect("file edits"); + assert!(edits.content.contains("@@ src/main.rs prepend")); + assert!(edits.content.contains("replace_function:greet")); + } + + #[test] + fn reports_active_slot_for_partial_traces() { + assert_eq!( + active_slot("\nReading..."), + Some("TASK_INTERPRETATION".to_string()) + ); + assert_eq!( + active_slot("\n\nStill final."), + Some("FINAL_RESPONSE".to_string()) + ); + assert_eq!(active_slot("Done"), None); + } + + #[test] + fn rejects_non_crp_text_outside_slots() { + let error = parse("hello\nDone").expect_err("invalid"); + + assert_eq!(error.kind, ParseErrorKind::ExpectedOpeningTag); + assert_eq!(error.position, 0); + } + + #[test] + fn rejects_lowercase_slot_names() { + let error = parse("Done").expect_err("invalid"); + + assert_eq!( + error.kind, + ParseErrorKind::InvalidSlotName("final".to_string()) + ); + } + + #[test] + fn rejects_leading_whitespace_in_opening_tag() { + let error = parse("< FINAL_RESPONSE>Done").expect_err("invalid"); + + assert_eq!( + error.kind, + ParseErrorKind::InvalidSlotName(" FINAL_RESPONSE".to_string()) + ); + } + + #[test] + fn reports_missing_closing_tag() { + let error = parse("Done").expect_err("invalid"); + + assert_eq!( + error.kind, + ParseErrorKind::MissingClosingTag { + slot: "FINAL_RESPONSE".to_string() + } + ); + } + + #[test] + fn reports_mismatched_closing_tag() { + let error = parse("Done").expect_err("invalid"); + + assert_eq!( + error.kind, + ParseErrorKind::MismatchedClosingTag { + expected: "FINAL_RESPONSE".to_string(), + found: "TASK_INTERPRETATION".to_string() + } + ); + } + + #[test] + fn validate_flags_missing_required_final_response() { + let trace = parse("Add hello.").expect("parse"); + let report = validate(&trace, &ValidationConfig::default()); + + assert!(!report.is_executable()); + let invalid: Vec<_> = report.invalid().collect(); + assert_eq!(invalid.len(), 1); + assert_eq!(invalid[0].slot, "FINAL_RESPONSE"); + } + + #[test] + fn validate_flags_empty_required_slot() { + let trace = parse(" ").expect("parse"); + let report = validate(&trace, &ValidationConfig::default()); + + assert!(!report.is_executable()); + assert_eq!(report.invalid().count(), 1); + } + + #[test] + fn validate_passes_when_required_slot_has_content() { + let trace = parse("Done.").expect("parse"); + let report = validate(&trace, &ValidationConfig::default()); + + assert!(report.is_executable()); + } + + #[test] + fn validate_warns_for_missing_recommended_template_slots() { + let templates = TemplateSet::builtin(); + let template = templates.select("code_edit"); + let config = template.validation_config(None); + let trace = parse("Done.").expect("parse"); + + let report = validate(&trace, &config); + + assert!(report.is_executable()); + assert_eq!(report.invalid().count(), 0); + let warnings: Vec<_> = report.warnings().collect(); + assert!(warnings + .iter() + .any(|outcome| outcome.slot == "TASK_INTERPRETATION")); + // RELEVANT_FILES and WORK_DONE are optional now, so they don't produce warnings if missing + } + + #[test] + fn validate_flags_missing_relevant_files_paths() { + let workspace = + std::env::temp_dir().join(format!("cinto-crp-validate-test-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&workspace); + std::fs::create_dir_all(&workspace).expect("create workspace"); + std::fs::write(workspace.join("real.rs"), "// real").expect("write real"); + + let trace = parse( + r#" +- real.rs +- ghost.rs + +Done."#, + ) + .expect("parse"); + + let config = ValidationConfig { + workspace: Some(&workspace), + ..ValidationConfig::default() + }; + let report = validate(&trace, &config); + + assert!(!report.is_executable()); + let invalid: Vec<_> = report.invalid().collect(); + assert_eq!(invalid.len(), 1); + assert_eq!(invalid[0].slot, "RELEVANT_FILES"); + assert!(invalid[0].message.contains("ghost.rs")); + assert!(!invalid[0].message.contains("real.rs")); + + let _ = std::fs::remove_dir_all(&workspace); + } + + #[test] + fn build_retry_message_emits_one_feedback_per_invalid_slot() { + let trace = parse("x").expect("parse"); + let report = validate(&trace, &ValidationConfig::default()); + let retry = build_retry_message(&report); + + assert!(retry.contains("")); + assert!(retry.contains("")); + assert!(retry.contains("")); + } + + #[test] + fn build_parse_retry_message_includes_error_detail() { + let error = parse("hello").expect_err("invalid"); + let retry = build_parse_retry_message(&error); + assert!(retry.contains("")); + assert!(retry.contains("could not be parsed")); + } + + #[test] + fn code_edit_minimal_template_parses() { + let template = Template::from_toml_str(BUILTIN_CODE_EDIT_MINIMAL).expect("minimal parses"); + assert_eq!(template.name, "code_edit_minimal"); + let required: Vec<&str> = template + .slots + .iter() + .filter(|s| s.required) + .map(|s| s.name.as_str()) + .collect(); + assert!(required.contains(&"TASK_INTERPRETATION")); + assert!(required.contains(&"FINAL_RESPONSE")); + assert_eq!(required.len(), 2); + } + + #[test] + fn code_edit_thorough_template_parses() { + let template = + Template::from_toml_str(BUILTIN_CODE_EDIT_THOROUGH).expect("thorough parses"); + assert_eq!(template.name, "code_edit_thorough"); + let required: Vec<&str> = template + .slots + .iter() + .filter(|s| s.required) + .map(|s| s.name.as_str()) + .collect(); + assert!(required.contains(&"TASK_INTERPRETATION")); + assert!(required.contains(&"FILE_EDITS")); + assert!(required.contains(&"FINAL_RESPONSE")); + assert_eq!(required.len(), 3); + + let optional: Vec<&str> = template + .slots + .iter() + .filter(|s| !s.required) + .map(|s| s.name.as_str()) + .collect(); + assert!(optional.contains(&"RELEVANT_FILES")); + assert!(optional.contains(&"WORK_DONE")); + assert!(optional.contains(&"SELF_VERIFICATION")); + } + + #[test] + fn template_brief_distinguishes_hard_required_from_recommended() { + let template = Template::from_toml_str(BUILTIN_CODE_EDIT).expect("parse template"); + let brief = template.render_brief(); + + assert!(brief.contains("hard_required: FINAL_RESPONSE")); + assert!(brief.contains("recommended:")); + assert!(brief.contains("TASK_INTERPRETATION")); + assert!(brief.contains("FILE_EDITS")); + assert!(brief.contains("optional:")); + assert!(brief.contains("RELEVANT_FILES")); + assert!(brief.contains("WORK_DONE")); + } + + #[test] + fn effort_qualified_name_maps_correctly() { + assert_eq!( + effort_qualified_name("code_edit", "none"), + Some("code_edit_minimal".to_string()) + ); + assert_eq!( + effort_qualified_name("code_edit", "low"), + Some("code_edit_minimal".to_string()) + ); + assert_eq!(effort_qualified_name("code_edit", "medium"), None); + assert_eq!( + effort_qualified_name("code_edit", "high"), + Some("code_edit_thorough".to_string()) + ); + // Case-insensitive + assert_eq!( + effort_qualified_name("code_edit", "HIGH"), + Some("code_edit_thorough".to_string()) + ); + } + + #[test] + fn resolve_returns_effort_specific_template() { + let set = TemplateSet::builtin(); + let template = set.resolve("code_edit", "none"); + assert_eq!(template.name, "code_edit_minimal"); + } + + #[test] + fn resolve_falls_back_to_base_template() { + let set = TemplateSet::builtin(); + let template = set.resolve("code_edit", "medium"); + assert_eq!(template.name, "code_edit"); + } + + #[test] + fn resolve_falls_back_when_effort_variant_missing() { + let set = TemplateSet::builtin(); + // code_explanation has no _minimal variant + let template = set.resolve("code_explanation", "none"); + assert_eq!(template.name, "code_explanation"); + } +} diff --git a/src/eval_diff.rs b/src/eval_diff.rs new file mode 100644 index 0000000..56ab4ed --- /dev/null +++ b/src/eval_diff.rs @@ -0,0 +1,163 @@ +use std::collections::HashMap; +use std::fs::File; +use std::io::{BufRead, BufReader}; +use std::path::PathBuf; + +use anyhow::{Context, Result}; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +struct BatchOutputs { + parsed_successfully: bool, + required_slots_present: bool, + filepaths_valid: bool, + code_compiles: Option, + tests_pass: Option, + evaluator_score: Option, +} + +#[derive(Debug, Deserialize)] +struct BatchMetrics { + duration_ms: u128, +} + +#[derive(Debug, Deserialize)] +struct BatchResult { + task_id: String, + outputs: BatchOutputs, + metrics: BatchMetrics, +} + +pub fn run(base_path: PathBuf, compare_path: PathBuf) -> Result<()> { + let base_results = load_results(&base_path)?; + let compare_results = load_results(&compare_path)?; + + let mut regressions = Vec::new(); + let mut improvements = Vec::new(); + let mut unchanged_pass = 0; + let mut unchanged_fail = 0; + + let mut total_duration_base = 0; + let mut total_duration_compare = 0; + let mut common_tasks = 0; + + for (task_id, base) in &base_results { + if let Some(compare) = compare_results.get(task_id) { + common_tasks += 1; + total_duration_base += base.metrics.duration_ms; + total_duration_compare += compare.metrics.duration_ms; + + let base_pass = is_success(base); + let compare_pass = is_success(compare); + + if base_pass && !compare_pass { + regressions.push(task_id.clone()); + } else if !base_pass && compare_pass { + improvements.push(task_id.clone()); + } else if base_pass && compare_pass { + unchanged_pass += 1; + } else { + unchanged_fail += 1; + } + } + } + + println!("========================================"); + println!(" EVALUATION DIFF RUN "); + println!("========================================"); + println!("Base run: {}", base_path.display()); + println!("Compare run: {}", compare_path.display()); + println!("Common tasks evaluated: {}", common_tasks); + println!("----------------------------------------"); + + println!("SUMMARY:"); + println!(" Regressions (Pass -> Fail): {}", regressions.len()); + println!(" Improvements (Fail -> Pass): {}", improvements.len()); + println!(" Unchanged (Pass): {}", unchanged_pass); + println!(" Unchanged (Fail): {}", unchanged_fail); + println!("----------------------------------------"); + + if !regressions.is_empty() { + println!("REGRESSIONS:"); + for task_id in ®ressions { + println!(" - {}", task_id); + } + println!("----------------------------------------"); + } + + if !improvements.is_empty() { + println!("IMPROVEMENTS:"); + for task_id in &improvements { + println!(" - {}", task_id); + } + println!("----------------------------------------"); + } + + if common_tasks > 0 { + let avg_base = total_duration_base / common_tasks as u128; + let avg_compare = total_duration_compare / common_tasks as u128; + let diff = avg_compare as f64 - avg_base as f64; + let diff_percent = (diff / avg_base as f64) * 100.0; + + println!("METRICS (Averages for common tasks):"); + println!(" Duration Base: {} ms", avg_base); + println!(" Duration Compare: {} ms", avg_compare); + println!(" Diff: {:.2} ms ({:+.2}%)", diff, diff_percent); + } + + Ok(()) +} + +fn load_results(path: &PathBuf) -> Result> { + let file = + File::open(path).with_context(|| format!("failed to open file {}", path.display()))?; + let reader = BufReader::new(file); + let mut map = HashMap::new(); + + for (idx, line) in reader.lines().enumerate() { + let line = line?; + if line.trim().is_empty() { + continue; + } + let result: BatchResult = serde_json::from_str(&line) + .with_context(|| format!("failed to parse JSON at {}:{}", path.display(), idx + 1))?; + map.insert(result.task_id.clone(), result); + } + Ok(map) +} + +fn is_success(res: &BatchResult) -> bool { + // A task is considered successful if: + // 1. It parsed successfully + // 2. All required slots are present + // 3. Filepaths are valid + // 4. (If checked) Code compiles + // 5. (If checked) Tests pass + // 6. (If checked) Evaluator score contains PASS + if !res.outputs.parsed_successfully + || !res.outputs.required_slots_present + || !res.outputs.filepaths_valid + { + return false; + } + + if let Some(compiles) = res.outputs.code_compiles { + if !compiles { + return false; + } + } + + if let Some(tests) = res.outputs.tests_pass { + if !tests { + return false; + } + } + + if let Some(score) = &res.outputs.evaluator_score { + if !score.contains("PASS") { + return false; + } + } + + true +} diff --git a/src/init.rs b/src/init.rs new file mode 100644 index 0000000..75f515b --- /dev/null +++ b/src/init.rs @@ -0,0 +1,19 @@ +use anyhow::Result; + +use crate::config::Config; +use crate::crp; + +pub fn run(config: &Config) -> Result<()> { + let workspace = &config.harness.workspace; + let templates_dir = crp::workspace_template_dir(workspace); + + crp::TemplateSet::dump_builtins(&templates_dir)?; + + println!( + "Successfully initialized Cinto templates in {}", + templates_dir.display() + ); + println!("You can now edit the .toml files to customize the CRP prompts for this workspace."); + + Ok(()) +} diff --git a/src/kernel/context_pack.rs b/src/kernel/context_pack.rs new file mode 100644 index 0000000..6192d2b --- /dev/null +++ b/src/kernel/context_pack.rs @@ -0,0 +1,357 @@ +use std::{fs, path::Path}; + +use anyhow::Result; + +use super::{ + index::{FileEntry, ProjectMap, SymbolIndex, extract_symbols, language_for}, + read::{AroundParams, read_around, read_range}, + search::{SearchParams, search}, +}; + +/// ~4192 tokens at 4 chars/token — mirrors the kernel budget doc. +pub const DEFAULT_CODE_BUDGET: usize = 16_000; +const REPO_MAP_BUDGET: usize = 800; +const SYMBOLS_BUDGET_PER_FILE: usize = 600; +const MAX_REPO_MAP_FILES: usize = 20; + +// --------------------------------------------------------------------------- +// Public types +// --------------------------------------------------------------------------- + +#[derive(Default)] +pub struct ContextHints { + /// Files to show symbol tables for. + pub files: Vec, + /// Explicit line ranges: (file, start, end). + pub ranges: Vec<(String, usize, usize)>, + /// Search terms to run and include as code snippets. + pub search_terms: Vec, +} + +pub struct ContextPack { + pub chars_used: usize, + pub chars_budget: usize, + pub truncated: bool, + pub formatted: String, +} + +pub struct ContextPackBuilder { + workspace: std::path::PathBuf, + project_map: Option, + symbol_index: Option, + code_budget: usize, +} + +// --------------------------------------------------------------------------- +// Builder +// --------------------------------------------------------------------------- + +impl ContextPackBuilder { + pub fn new(workspace: &Path) -> Self { + Self { + workspace: workspace.to_path_buf(), + project_map: None, + symbol_index: None, + code_budget: DEFAULT_CODE_BUDGET, + } + } + + pub fn with_budget(mut self, chars: usize) -> Self { + self.code_budget = chars; + self + } + + /// Load project_map.json and symbol_index.json from .cinto/ if present. + /// Degrades gracefully when index is missing. + pub fn with_index(mut self) -> Self { + let dir = self.workspace.join(".cinto"); + self.project_map = load_json(&dir.join("project_map.json")); + self.symbol_index = load_json(&dir.join("symbol_index.json")); + self + } + + pub fn build(self, task: &str, hints: &ContextHints) -> Result { + let mut out = String::new(); + let mut code_used: usize = 0; + let mut truncated = false; + + // ── TASK (always included, not counted against code budget) ───────── + section(&mut out, "TASK"); + out.push_str(task.trim()); + out.push_str("\n\n"); + + // ── REPO MAP ──────────────────────────────────────────────────────── + if let Some(ref map) = self.project_map { + let repo_map = build_repo_map(map, hints, REPO_MAP_BUDGET); + if !repo_map.is_empty() { + section( + &mut out, + &format!("REPO MAP — {} files total", map.files.len()), + ); + out.push_str(&repo_map); + out.push('\n'); + } + } + + // ── SYMBOLS (per hinted file) ──────────────────────────────────────── + for file in &hints.files { + if code_used >= self.code_budget { + truncated = true; + break; + } + let block = self.symbols_block(file); + if fits(&block, code_used, self.code_budget, SYMBOLS_BUDGET_PER_FILE) { + section(&mut out, &format!("SYMBOLS {file}")); + out.push_str(&block); + out.push('\n'); + code_used += block.len(); + } else { + truncated = true; + } + } + + // ── CODE (explicit ranges) ─────────────────────────────────────────── + for (file, start, end) in &hints.ranges { + if code_used >= self.code_budget { + truncated = true; + break; + } + match read_range(&self.workspace, file, *start, *end) { + Ok(snippet) => { + let available = self.code_budget.saturating_sub(code_used); + let (snippet, cut) = budget_cut(snippet, available); + if cut { + truncated = true; + } + section(&mut out, &format!("CODE {file} lines {start}–{end}")); + out.push_str(&snippet); + out.push('\n'); + code_used += snippet.len(); + } + Err(e) => { + out.push_str(&format!("[could not read {file}: {e}]\n\n")); + } + } + } + + // ── CODE (search results) ──────────────────────────────────────────── + for term in &hints.search_terms { + if code_used >= self.code_budget { + truncated = true; + break; + } + let params = SearchParams::new(term); + match search(&self.workspace, params) { + Ok(result) => { + if result.match_count == 0 { + continue; + } + let available = self.code_budget.saturating_sub(code_used); + let (snippet, cut) = budget_cut(result.formatted, available); + if cut { + truncated = true; + } + section(&mut out, &format!("SEARCH {term:?}")); + out.push_str(&snippet); + out.push('\n'); + code_used += snippet.len(); + } + Err(_) => {} + } + } + + // ── BUDGET FOOTER ──────────────────────────────────────────────────── + if truncated { + out.push_str("[context truncated — some sections omitted to respect budget]\n"); + } + out.push_str(&format!( + "[budget: {code_used} / {} chars]\n", + self.code_budget + )); + + Ok(ContextPack { + chars_used: code_used, + chars_budget: self.code_budget, + truncated, + formatted: out, + }) + } + + /// Extract symbols for a file — from symbol_index if available, otherwise live. + fn symbols_block(&self, rel_path: &str) -> String { + // Try index first (faster) + if let Some(ref idx) = self.symbol_index { + if let Some(syms) = idx.get(rel_path) { + return format_symbols(syms.iter().map(|s| (s.line, s.kind.as_str(), s.name.as_str()))); + } + } + + // Fall back to live extraction + let path = self.workspace.join(rel_path); + let ext = path.extension().and_then(|e| e.to_str()).unwrap_or(""); + let Some(lang) = language_for(ext) else { + return String::new(); + }; + let Ok(content) = fs::read_to_string(&path) else { + return String::new(); + }; + let syms = extract_symbols(&content, &lang); + format_symbols(syms.iter().map(|s| (s.line, s.kind.as_str(), s.name.as_str()))) + } +} + +// --------------------------------------------------------------------------- +// Repo map +// --------------------------------------------------------------------------- + +fn build_repo_map(map: &ProjectMap, hints: &ContextHints, budget: usize) -> String { + let mut out = String::new(); + + // Hinted files first, then others up to MAX_REPO_MAP_FILES + let mut ordered: Vec<&FileEntry> = Vec::new(); + + for hint in &hints.files { + if let Some(entry) = map.files.iter().find(|f| &f.path == hint) { + ordered.push(entry); + } + } + for entry in &map.files { + if ordered.len() >= MAX_REPO_MAP_FILES { + break; + } + if !ordered.iter().any(|e| e.path == entry.path) { + ordered.push(entry); + } + } + + for entry in ordered { + let lang = entry.language.as_deref().unwrap_or(""); + let line = format!( + " {:<50} {:>5} lines {}\n", + entry.path, entry.lines, lang + ); + if out.len() + line.len() > budget { + out.push_str(" ...\n"); + break; + } + out.push_str(&line); + } + + out +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +fn section(out: &mut String, title: &str) { + out.push_str(&format!("[{title}]\n")); +} + +fn format_symbols<'a>( + iter: impl Iterator, +) -> String { + let mut out = String::new(); + for (line, kind, name) in iter { + out.push_str(&format!(" {:5} {:<8} {}\n", line, kind, name)); + } + out +} + +fn fits(block: &str, used: usize, budget: usize, per_section_cap: usize) -> bool { + block.len() <= per_section_cap && used + block.len() <= budget +} + +/// Trim a string to `max_chars`, appending a truncation note. +fn budget_cut(mut s: String, max_chars: usize) -> (String, bool) { + if s.len() <= max_chars { + return (s, false); + } + s.truncate(max_chars); + s.push_str("\n... truncated to fit budget\n"); + (s, true) +} + +fn load_json(path: &Path) -> Option { + let text = fs::read_to_string(path).ok()?; + serde_json::from_str(&text).ok() +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::Path; + + fn workspace() -> &'static Path { + Path::new(env!("CARGO_MANIFEST_DIR")) + } + + #[test] + fn pack_always_includes_task() { + let pack = ContextPackBuilder::new(workspace()) + .build("Fix the search bug", &ContextHints::default()) + .unwrap(); + assert!(pack.formatted.contains("[TASK]")); + assert!(pack.formatted.contains("Fix the search bug")); + } + + #[test] + fn pack_includes_budget_footer() { + let pack = ContextPackBuilder::new(workspace()) + .build("test", &ContextHints::default()) + .unwrap(); + assert!(pack.formatted.contains("[budget:")); + } + + #[test] + fn pack_respects_tiny_budget() { + let hints = ContextHints { + files: vec!["src/kernel/index.rs".into()], + search_terms: vec!["index_repo".into()], + ..Default::default() + }; + let pack = ContextPackBuilder::new(workspace()) + .with_budget(200) + .build("test task", &hints) + .unwrap(); + // Code sections should be skipped or truncated to stay near budget + assert!(pack.chars_used <= 200 + 80); // small footer margin + } + + #[test] + fn pack_sets_truncated_flag_when_over_budget() { + let hints = ContextHints { + files: vec!["src/session.rs".into()], // large file, many symbols + search_terms: vec!["fn".into()], + ..Default::default() + }; + let pack = ContextPackBuilder::new(workspace()) + .with_budget(100) + .build("test", &hints) + .unwrap(); + assert!(pack.truncated); + } + + #[test] + fn pack_with_index_loads_repo_map() { + let pack = ContextPackBuilder::new(workspace()) + .with_index() + .build("test", &ContextHints::default()) + .unwrap(); + // If index exists (.cinto/project_map.json), repo map section appears + // Degrades gracefully if index missing + assert!(pack.formatted.contains("[TASK]")); + } + + #[test] + fn pack_includes_symbols_for_hinted_file() { + let hints = ContextHints { + files: vec!["src/kernel/index.rs".into()], + ..Default::default() + }; + let pack = ContextPackBuilder::new(workspace()) + .build("test", &hints) + .unwrap(); + assert!(pack.formatted.contains("index_repo") || pack.truncated); + } +} diff --git a/src/kernel/index.rs b/src/kernel/index.rs new file mode 100644 index 0000000..a6661db --- /dev/null +++ b/src/kernel/index.rs @@ -0,0 +1,442 @@ +use std::{collections::HashMap, fs, path::Path}; + +use anyhow::{Context, Result}; +use chrono::Utc; +use serde::{Deserialize, Serialize}; + +const SKIP_DIRS: &[&str] = &[ + "target", + ".git", + ".cinto", + "node_modules", + ".venv", + "venv", + "dist", + "build", + "out", + "__pycache__", + ".next", + ".nuxt", + ".tox", + ".mypy_cache", + ".pytest_cache", + ".ruff_cache", + "vendor", +]; +const MAX_FILE_BYTES: u64 = 512 * 1024; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProjectMap { + pub root: String, + pub generated_at: String, + pub files: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FileEntry { + pub path: String, + pub size_bytes: u64, + pub lines: usize, + pub extension: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub language: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Symbol { + pub line: usize, + pub kind: String, + pub name: String, +} + +pub type SymbolIndex = HashMap>; + +pub struct IndexResult { + pub project_map: ProjectMap, + pub symbol_index: SymbolIndex, + pub file_count: usize, + pub symbol_count: usize, +} + +pub fn index_repo(workspace: &Path) -> Result { + let workspace = workspace + .canonicalize() + .with_context(|| format!("cannot resolve workspace: {}", workspace.display()))?; + + let mut files = Vec::new(); + let mut symbol_index = SymbolIndex::new(); + + walk(&workspace, &workspace, &mut files, &mut symbol_index)?; + + let symbol_count = symbol_index.values().map(Vec::len).sum(); + let file_count = files.len(); + + let project_map = ProjectMap { + root: workspace.to_string_lossy().into_owned(), + generated_at: Utc::now().to_rfc3339(), + files, + }; + + Ok(IndexResult { + project_map, + symbol_index, + file_count, + symbol_count, + }) +} + +pub fn save(workspace: &Path, result: &IndexResult) -> Result<()> { + let dir = workspace.join(".cinto"); + fs::create_dir_all(&dir) + .with_context(|| format!("cannot create {}", dir.display()))?; + + let map_json = serde_json::to_string_pretty(&result.project_map) + .context("failed to serialize project map")?; + fs::write(dir.join("project_map.json"), map_json)?; + + let sym_json = serde_json::to_string_pretty(&result.symbol_index) + .context("failed to serialize symbol index")?; + fs::write(dir.join("symbol_index.json"), sym_json)?; + + Ok(()) +} + +fn walk( + workspace: &Path, + dir: &Path, + files: &mut Vec, + symbols: &mut SymbolIndex, +) -> Result<()> { + let mut entries: Vec<_> = fs::read_dir(dir) + .with_context(|| format!("cannot read {}", dir.display()))? + .filter_map(Result::ok) + .collect(); + entries.sort_by_key(|e| e.file_name()); + + for entry in entries { + let path = entry.path(); + let name = entry.file_name(); + let name_str = name.to_string_lossy(); + + let ft = match entry.file_type() { + Ok(ft) => ft, + Err(_) => continue, + }; + + if ft.is_dir() { + if SKIP_DIRS.contains(&name_str.as_ref()) { + continue; + } + walk(workspace, &path, files, symbols)?; + continue; + } + + if !ft.is_file() { + continue; + } + + let meta = match fs::metadata(&path) { + Ok(m) => m, + Err(_) => continue, + }; + if meta.len() > MAX_FILE_BYTES { + continue; + } + + let rel = path + .strip_prefix(workspace) + .map(|p| p.to_string_lossy().replace('\\', "/")) + .unwrap_or_default(); + + let ext = path + .extension() + .and_then(|e| e.to_str()) + .unwrap_or("") + .to_string(); + + let language = language_for(&ext); + + let (line_count, file_symbols) = match &language { + Some(lang) => match fs::read_to_string(&path) { + Ok(content) => (content.lines().count(), extract_symbols(&content, lang)), + Err(_) => (0, vec![]), + }, + None => (0, vec![]), + }; + + if !file_symbols.is_empty() { + symbols.insert(rel.clone(), file_symbols); + } + + files.push(FileEntry { + path: rel, + size_bytes: meta.len(), + lines: line_count, + extension: ext, + language, + }); + } + + Ok(()) +} + +// --------------------------------------------------------------------------- +// Symbol extraction +// --------------------------------------------------------------------------- + +pub fn extract_symbols(content: &str, language: &str) -> Vec { + match language { + "rust" => rust_symbols(content), + "python" => python_symbols(content), + "javascript" | "typescript" => js_symbols(content), + "go" => go_symbols(content), + _ => vec![], + } +} + +fn rust_symbols(content: &str) -> Vec { + let mut out = Vec::new(); + for (i, line) in content.lines().enumerate() { + let t = line.trim_start(); + let Some((kind, rest)) = rust_def(t) else { + continue; + }; + let name = ident(rest); + if !name.is_empty() { + out.push(sym(i + 1, kind, name)); + } + } + out +} + +fn rust_def(s: &str) -> Option<(&'static str, &str)> { + const FN: &[&str] = &[ + "pub(crate) async fn ", + "pub async fn ", + "async fn ", + "pub(crate) fn ", + "pub fn ", + "fn ", + ]; + const STRUCT: &[&str] = &["pub(crate) struct ", "pub struct ", "struct "]; + const ENUM: &[&str] = &["pub(crate) enum ", "pub enum ", "enum "]; + const TRAIT: &[&str] = &["pub(crate) trait ", "pub trait ", "trait "]; + const TYPE: &[&str] = &["pub(crate) type ", "pub type ", "type "]; + const MOD: &[&str] = &["pub(crate) mod ", "pub mod ", "mod "]; + + strip_any(s, FN) + .map(|r| ("fn", r)) + .or_else(|| strip_any(s, STRUCT).map(|r| ("struct", r))) + .or_else(|| strip_any(s, ENUM).map(|r| ("enum", r))) + .or_else(|| strip_any(s, TRAIT).map(|r| ("trait", r))) + .or_else(|| strip_any(s, TYPE).map(|r| ("type", r))) + .or_else(|| strip_any(s, MOD).map(|r| ("mod", r))) + .or_else(|| s.strip_prefix("impl ").map(|r| ("impl", r))) +} + +fn python_symbols(content: &str) -> Vec { + let mut out = Vec::new(); + for (i, line) in content.lines().enumerate() { + let t = line.trim_start(); + let (kind, rest) = if let Some(r) = t.strip_prefix("async def ") { + ("fn", r) + } else if let Some(r) = t.strip_prefix("def ") { + ("fn", r) + } else if let Some(r) = t.strip_prefix("class ") { + ("class", r) + } else { + continue; + }; + let name = ident(rest); + if !name.is_empty() { + out.push(sym(i + 1, kind, name)); + } + } + out +} + +fn js_symbols(content: &str) -> Vec { + let mut out = Vec::new(); + for (i, line) in content.lines().enumerate() { + let t = line.trim_start(); + let Some((kind, rest)) = js_def(t) else { + continue; + }; + let name = ident(rest); + if !name.is_empty() { + out.push(sym(i + 1, kind, name)); + } + } + out +} + +fn js_def(s: &str) -> Option<(&'static str, &str)> { + const FN: &[&str] = &[ + "export default async function ", + "export async function ", + "export default function ", + "export function ", + "async function ", + "function ", + ]; + const CLASS: &[&str] = &["export default class ", "export class ", "class "]; + const IFACE: &[&str] = &["export interface ", "interface "]; + + strip_any(s, FN) + .map(|r| ("fn", r)) + .or_else(|| strip_any(s, CLASS).map(|r| ("class", r))) + .or_else(|| strip_any(s, IFACE).map(|r| ("interface", r))) +} + +fn go_symbols(content: &str) -> Vec { + let mut out = Vec::new(); + for (i, line) in content.lines().enumerate() { + let t = line.trim_start(); + if let Some(rest) = t.strip_prefix("func ") { + let name = if rest.starts_with('(') { + // receiver method: `func (recv Type) Name(` + rest.find(')') + .map(|j| ident(rest[j + 1..].trim_start()).to_string()) + .unwrap_or_default() + } else { + ident(rest).to_string() + }; + if !name.is_empty() { + out.push(sym(i + 1, "fn", &name)); + } + } else if let Some(rest) = t.strip_prefix("type ") { + let name = ident(rest); + if !name.is_empty() { + out.push(sym(i + 1, "type", name)); + } + } + } + out +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +fn strip_any<'a>(s: &'a str, prefixes: &[&str]) -> Option<&'a str> { + prefixes.iter().find_map(|p| s.strip_prefix(p)) +} + +fn ident(s: &str) -> &str { + let end = s + .find(|c: char| !c.is_alphanumeric() && c != '_') + .unwrap_or(s.len()); + &s[..end] +} + +fn sym(line: usize, kind: &str, name: &str) -> Symbol { + Symbol { + line, + kind: kind.to_string(), + name: name.to_string(), + } +} + +pub(crate) fn language_for(ext: &str) -> Option { + Some( + match ext { + "rs" => "rust", + "py" | "pyw" => "python", + "js" | "jsx" | "mjs" | "cjs" => "javascript", + "ts" | "tsx" | "mts" | "cts" => "typescript", + "go" => "go", + "c" | "h" => "c", + "cpp" | "cc" | "cxx" | "hpp" => "cpp", + "java" => "java", + "kt" | "kts" => "kotlin", + "rb" => "ruby", + "sh" | "bash" => "shell", + "toml" => "toml", + "json" => "json", + "yaml" | "yml" => "yaml", + "md" | "mdx" => "markdown", + _ => return None, + } + .to_string(), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn workspace() -> &'static Path { + Path::new(env!("CARGO_MANIFEST_DIR")) + } + + #[test] + fn index_repo_finds_rust_files() { + let result = index_repo(workspace()).unwrap(); + assert!(result.file_count > 0); + assert!(result.project_map.files.iter().any(|f| f.path == "src/main.rs")); + } + + #[test] + fn index_repo_skips_target_and_cinto() { + let result = index_repo(workspace()).unwrap(); + assert!(!result.project_map.files.iter().any(|f| f.path.starts_with("target/"))); + assert!(!result.project_map.files.iter().any(|f| f.path.starts_with(".cinto/"))); + } + + #[test] + fn index_repo_extracts_rust_symbols() { + let result = index_repo(workspace()).unwrap(); + let syms = result.symbol_index.get("src/main.rs").expect("main.rs should have symbols"); + let names: Vec<&str> = syms.iter().map(|s| s.name.as_str()).collect(); + assert!(names.contains(&"main"), "should find main fn"); + assert!(names.contains(&"Args"), "should find Args struct"); + } + + #[test] + fn language_for_known_extensions() { + assert_eq!(language_for("rs"), Some("rust".into())); + assert_eq!(language_for("py"), Some("python".into())); + assert_eq!(language_for("ts"), Some("typescript".into())); + assert_eq!(language_for("go"), Some("go".into())); + assert_eq!(language_for("xyz"), None); + } + + #[test] + fn rust_symbol_extraction() { + let src = "pub struct Foo;\npub fn bar() {}\nenum Baz {}\nimpl Foo {}\n"; + let syms = extract_symbols(src, "rust"); + let names: Vec<&str> = syms.iter().map(|s| s.name.as_str()).collect(); + assert!(names.contains(&"Foo")); + assert!(names.contains(&"bar")); + assert!(names.contains(&"Baz")); + } + + #[test] + fn python_symbol_extraction() { + let src = "def foo():\n pass\nclass Bar:\n async def method(self):\n pass\n"; + let syms = extract_symbols(src, "python"); + let names: Vec<&str> = syms.iter().map(|s| s.name.as_str()).collect(); + assert!(names.contains(&"foo")); + assert!(names.contains(&"Bar")); + assert!(names.contains(&"method")); + } + + #[test] + fn js_symbol_extraction() { + let src = "function greet() {}\nexport class App {}\nexport interface Config {}\n"; + let syms = extract_symbols(src, "javascript"); + let names: Vec<&str> = syms.iter().map(|s| s.name.as_str()).collect(); + assert!(names.contains(&"greet")); + assert!(names.contains(&"App")); + assert!(names.contains(&"Config")); + } + + #[test] + fn file_entry_has_correct_metadata() { + let result = index_repo(workspace()).unwrap(); + let entry = result.project_map.files.iter().find(|f| f.path == "src/main.rs").unwrap(); + assert!(entry.lines > 0); + assert_eq!(entry.extension, "rs"); + assert_eq!(entry.language.as_deref(), Some("rust")); + } +} diff --git a/src/kernel/memory.rs b/src/kernel/memory.rs new file mode 100644 index 0000000..8d1c469 --- /dev/null +++ b/src/kernel/memory.rs @@ -0,0 +1,50 @@ +use std::{ + fs, + io::Write, + path::Path, +}; + +use anyhow::Result; +use chrono::Local; + +const MAX_LOAD_CHARS: usize = 2_000; + +/// Append a decision record to `.cinto/memory/decisions.md`. +pub fn save_decision(workspace: &Path, task: &str, decision: &str) -> Result<()> { + let dir = workspace.join(".cinto").join("memory"); + fs::create_dir_all(&dir)?; + + let path = dir.join("decisions.md"); + let entry = format!( + "\n## {}\n**Task:** {}\n\n{}\n", + Local::now().format("%Y-%m-%d %H:%M"), + task.lines().next().unwrap_or("").trim(), + decision.trim(), + ); + + let mut file = fs::OpenOptions::new() + .create(true) + .append(true) + .open(path)?; + file.write_all(entry.as_bytes())?; + Ok(()) +} + +/// Load recent decisions for inclusion in a context pack. +/// Returns the last MAX_LOAD_CHARS of decisions.md, or an empty string. +pub fn load_recent_decisions(workspace: &Path) -> String { + let path = workspace.join(".cinto").join("memory").join("decisions.md"); + let content = fs::read_to_string(path).unwrap_or_default(); + if content.is_empty() { + return String::new(); + } + if content.len() <= MAX_LOAD_CHARS { + content + } else { + // Start from a clean entry boundary + let tail = &content[content.len() - MAX_LOAD_CHARS..]; + tail.find("\n## ") + .map(|i| tail[i..].to_string()) + .unwrap_or_else(|| tail.to_string()) + } +} diff --git a/src/kernel/mod.rs b/src/kernel/mod.rs new file mode 100644 index 0000000..781e3e9 --- /dev/null +++ b/src/kernel/mod.rs @@ -0,0 +1,7 @@ +pub mod context_pack; +pub mod index; +pub mod memory; +pub mod patch; +pub mod read; +pub mod search; +pub mod worker; diff --git a/src/kernel/patch.rs b/src/kernel/patch.rs new file mode 100644 index 0000000..19236ac --- /dev/null +++ b/src/kernel/patch.rs @@ -0,0 +1,346 @@ +use std::{fs, path::Path}; + +use anyhow::{Context, Result, anyhow}; +use tokio::sync::oneshot; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone)] +pub struct EditDirective { + pub path: String, + pub mode: EditMode, + pub content: String, +} + +#[derive(Debug, Clone)] +pub enum EditMode { + Replace, + ReplaceFunction(String), + Prepend, + Append, +} + +pub struct ApplyRequest { + pub directive: EditDirective, + pub preview: String, + pub response_tx: oneshot::Sender, +} + +#[derive(Debug, Default)] +pub struct PatchResult { + pub directives_found: usize, + pub files_written: Vec, + pub rejected: Vec, + pub errors: Vec, +} + +// --------------------------------------------------------------------------- +// Parsing +// --------------------------------------------------------------------------- + +/// Parse `content` blocks from a CRP +/// FILE_EDITS slot or plain model output. +pub fn parse_edit_directives(text: &str) -> Vec { + let mut directives = Vec::new(); + let mut remaining = text; + + while let Some(rel) = remaining.find("') else { + break; + }; + let tag = &remaining[..tag_end + 1]; + remaining = &remaining[tag_end + 1..]; + + let path = match extract_attr(tag, "path") { + Some(p) => p, + None => continue, + }; + let mode_str = extract_attr(tag, "mode").unwrap_or_else(|| "replace".into()); + let mode = parse_mode(&mode_str); + + let Some(close) = remaining.find("") else { + break; + }; + let content = remaining[..close].trim().to_string(); + remaining = &remaining[close + "".len()..]; + + if !content.is_empty() { + directives.push(EditDirective { path, mode, content }); + } + } + + // Terse @@ format fallback (Appendix B of CRP spec) + if directives.is_empty() { + directives = parse_terse_format(text); + } + + directives +} + +fn parse_terse_format(text: &str) -> Vec { + let mut directives = Vec::new(); + let mut current: Option<(String, EditMode, Vec<&str>)> = None; + + for line in text.lines() { + if let Some(rest) = line.strip_prefix("@@ ") { + if let Some((path, mode, lines)) = current.take() { + let content = lines.join("\n").trim().to_string(); + if !content.is_empty() { + directives.push(EditDirective { path, mode, content }); + } + } + let mut parts = rest.splitn(2, ' '); + let path = parts.next().unwrap_or("").to_string(); + let mode_str = parts.next().unwrap_or("replace"); + let mode = parse_mode(mode_str); + if !path.is_empty() { + current = Some((path, mode, Vec::new())); + } + } else if let Some((_, _, ref mut lines)) = current { + lines.push(line); + } + } + + if let Some((path, mode, lines)) = current { + let content = lines.join("\n").trim().to_string(); + if !content.is_empty() { + directives.push(EditDirective { path, mode, content }); + } + } + + directives +} + +fn parse_mode(s: &str) -> EditMode { + if s.starts_with("replace_function:") { + let name = s["replace_function:".len()..].trim().to_string(); + return EditMode::ReplaceFunction(name); + } + match s.trim() { + "prepend" | "prepend_to_existing" => EditMode::Prepend, + "append" | "append_to_existing" => EditMode::Append, + _ => EditMode::Replace, + } +} + +fn extract_attr(tag: &str, name: &str) -> Option { + let key = format!("{name}=\""); + let start = tag.find(&key)? + key.len(); + let end = tag[start..].find('"')?; + Some(tag[start..start + end].to_string()) +} + +// --------------------------------------------------------------------------- +// Application +// --------------------------------------------------------------------------- + +/// Apply a single directive to the workspace. Returns a short diff summary. +pub fn apply_directive(workspace: &Path, directive: &EditDirective) -> Result { + let path = resolve(workspace, &directive.path)?; + + if let Some(parent) = path.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("cannot create parent of {}", directive.path))?; + } + + match &directive.mode { + EditMode::Replace => { + let old_lines = fs::read_to_string(&path).unwrap_or_default().lines().count(); + let new_lines = directive.content.lines().count(); + fs::write(&path, &directive.content) + .with_context(|| format!("cannot write {}", directive.path))?; + Ok(format!( + "{}: replaced ({old_lines} → {new_lines} lines)", + directive.path + )) + } + + EditMode::ReplaceFunction(name) => { + let original = fs::read_to_string(&path) + .with_context(|| format!("cannot read {}", directive.path))?; + match replace_function_body(&original, name, &directive.content) { + Some(updated) => { + fs::write(&path, &updated) + .with_context(|| format!("cannot write {}", directive.path))?; + Ok(format!("{}: replaced function '{name}'", directive.path)) + } + None => { + // Function not found — the model is adding a new one, so append it. + let updated = format!("{}\n\n{}", original.trim_end(), directive.content.trim_end()); + fs::write(&path, updated) + .with_context(|| format!("cannot write {}", directive.path))?; + Ok(format!("{}: added function '{name}' (appended — not found in file)", directive.path)) + } + } + } + + EditMode::Prepend => { + let existing = fs::read_to_string(&path).unwrap_or_default(); + let updated = format!("{}\n{}", directive.content.trim_end(), existing); + fs::write(&path, &updated) + .with_context(|| format!("cannot write {}", directive.path))?; + Ok(format!("{}: prepended {} lines", directive.path, directive.content.lines().count())) + } + + EditMode::Append => { + let existing = fs::read_to_string(&path).unwrap_or_default(); + let updated = format!("{}\n{}", existing.trim_end(), directive.content); + fs::write(&path, &updated) + .with_context(|| format!("cannot write {}", directive.path))?; + Ok(format!("{}: appended {} lines", directive.path, directive.content.lines().count())) + } + } +} + +/// Build a human-readable preview for the approval prompt. +pub fn preview(workspace: &Path, directive: &EditDirective) -> String { + let current = workspace + .join(&directive.path) + .pipe(|p| fs::read_to_string(p).unwrap_or_default()); + let current_lines = current.lines().count(); + let new_lines = directive.content.lines().count(); + + let mode_label = match &directive.mode { + EditMode::Replace => format!("replace entire file ({current_lines} → {new_lines} lines)"), + EditMode::ReplaceFunction(name) => format!("replace function '{name}'"), + EditMode::Prepend => format!("prepend {new_lines} lines"), + EditMode::Append => format!("append {new_lines} lines"), + }; + + // Show a compact snippet of what will be written + let snippet: String = directive + .content + .lines() + .take(8) + .map(|l| format!(" {l}")) + .collect::>() + .join("\n"); + let ellipsis = if new_lines > 8 { + format!("\n ... ({} more lines)", new_lines - 8) + } else { + String::new() + }; + + format!("{} — {mode_label}\n{snippet}{ellipsis}", directive.path) +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +fn resolve(workspace: &Path, rel_path: &str) -> Result { + // New files don't exist yet — only validate the path isn't escaping. + let joined = workspace.join(rel_path); + // Resolve as much as exists without requiring the file itself. + let canonical_ws = workspace + .canonicalize() + .with_context(|| format!("cannot resolve workspace {}", workspace.display()))?; + // Normalise the path manually to detect traversal without requiring existence. + let mut parts = Vec::new(); + for component in joined.components() { + match component { + std::path::Component::ParentDir => { + if parts.pop().is_none() { + return Err(anyhow!("path escapes workspace: {rel_path}")); + } + } + std::path::Component::CurDir => {} + c => parts.push(c), + } + } + let normalised: std::path::PathBuf = parts.iter().collect(); + if !normalised.starts_with(&canonical_ws) { + return Err(anyhow!("path escapes workspace: {rel_path}")); + } + Ok(normalised) +} + +/// Replace a named function/class/struct in `source`. +/// Detects Python (indentation-based) vs brace languages automatically. +fn replace_function_body(source: &str, name: &str, new_body: &str) -> Option { + let lines: Vec<&str> = source.lines().collect(); + + let start_idx = lines.iter().position(|l| { + let t = l.trim(); + t.contains(name) + && (t.contains("fn ") + || t.contains("struct ") + || t.contains("impl ") + || t.contains("class ") + || t.contains("def ") + || t.contains("func ")) + })?; + + // Detect Python by checking if the declaration line ends with ':' (after stripping comments) + let decl = lines[start_idx].trim(); + let is_python = decl.ends_with(':') + || decl.split('#').next().unwrap_or("").trim().ends_with(':'); + + let end_idx = if is_python { + find_python_end(&lines, start_idx) + } else { + find_brace_end(&lines, start_idx)? + }; + + let mut result = String::new(); + if start_idx > 0 { + result.push_str(&lines[..start_idx].join("\n")); + result.push('\n'); + } + result.push_str(new_body.trim_end()); + result.push('\n'); + if end_idx < lines.len() { + result.push('\n'); + result.push_str(&lines[end_idx..].join("\n")); + result.push('\n'); + } + + Some(result) +} + +fn find_python_end(lines: &[&str], start_idx: usize) -> usize { + let base_indent = lines[start_idx].len() - lines[start_idx].trim_start().len(); + + for i in (start_idx + 1)..lines.len() { + let line = lines[i]; + let trimmed = line.trim(); + if trimmed.is_empty() || trimmed.starts_with('#') { + continue; + } + let indent = line.len() - line.trim_start().len(); + if indent <= base_indent { + return i; + } + } + lines.len() +} + +fn find_brace_end(lines: &[&str], start_idx: usize) -> Option { + let mut depth: i32 = 0; + let mut found_open = false; + + for (i, line) in lines[start_idx..].iter().enumerate() { + for ch in line.chars() { + if ch == '{' { + depth += 1; + found_open = true; + } else if ch == '}' { + depth -= 1; + } + } + if found_open && depth == 0 { + return Some(start_idx + i + 1); + } + } + None +} + +// Small helper to avoid a `let` binding just for piping. +trait Pipe: Sized { + fn pipe R, R>(self, f: F) -> R { f(self) } +} +impl Pipe for T {} diff --git a/src/kernel/read.rs b/src/kernel/read.rs new file mode 100644 index 0000000..c5da1e6 --- /dev/null +++ b/src/kernel/read.rs @@ -0,0 +1,325 @@ +use std::{ + fs, + path::{Path, PathBuf}, +}; + +use anyhow::{Context, Result, anyhow}; + +use super::index::{Symbol, extract_symbols, language_for}; + +const MAX_RANGE_LINES: usize = 150; +const DEFAULT_AROUND_BEFORE: usize = 5; +const DEFAULT_AROUND_AFTER: usize = 5; +const MAX_AROUND_LINES: usize = 20; +const MAX_AROUND_OCCURRENCES: usize = 3; +const MAX_OUTPUT_CHARS: usize = 4_000; + +// --------------------------------------------------------------------------- +// read_range +// --------------------------------------------------------------------------- + +/// Read a specific line range from a file (1-indexed, inclusive). +/// Capped at MAX_RANGE_LINES lines. +pub fn read_range(workspace: &Path, rel_path: &str, start: usize, end: usize) -> Result { + if start == 0 { + return Err(anyhow!("start line must be >= 1")); + } + if end < start { + return Err(anyhow!("end line must be >= start line")); + } + + let path = resolve(workspace, rel_path)?; + let content = fs::read_to_string(&path) + .with_context(|| format!("cannot read {rel_path}"))?; + + let lines: Vec<&str> = content.lines().collect(); + let total = lines.len(); + + let lo = (start - 1).min(total); + let hi = end.min(total); + let requested = hi.saturating_sub(lo); + + let (hi, truncated) = if requested > MAX_RANGE_LINES { + (lo + MAX_RANGE_LINES, true) + } else { + (hi, false) + }; + + let mut out = String::new(); + out.push_str(&format!( + "{rel_path} lines {start}–{}\n\n", + lo + (hi - lo) + )); + + for (i, line) in lines[lo..hi].iter().enumerate() { + out.push_str(&format!(" {:4} | {line}\n", lo + i + 1)); + } + + if truncated { + out.push_str(&format!( + "\n... capped at {MAX_RANGE_LINES} lines (requested {requested})\n" + )); + } + + Ok(out) +} + +// --------------------------------------------------------------------------- +// read_around +// --------------------------------------------------------------------------- + +pub struct AroundParams { + pub before: usize, + pub after: usize, +} + +impl Default for AroundParams { + fn default() -> Self { + Self { + before: DEFAULT_AROUND_BEFORE, + after: DEFAULT_AROUND_AFTER, + } + } +} + +/// Find up to MAX_AROUND_OCCURRENCES of `query` in `file` and return a +/// windowed snippet around each match. Case-insensitive. +pub fn read_around( + workspace: &Path, + rel_path: &str, + query: &str, + params: AroundParams, +) -> Result { + if query.is_empty() { + return Err(anyhow!("query must not be empty")); + } + + let before = params.before.min(MAX_AROUND_LINES); + let after = params.after.min(MAX_AROUND_LINES); + + let path = resolve(workspace, rel_path)?; + let content = fs::read_to_string(&path) + .with_context(|| format!("cannot read {rel_path}"))?; + + let lines: Vec<&str> = content.lines().collect(); + let query_lower = query.to_lowercase(); + + // Collect match line indices (0-based) + let mut match_indices: Vec = lines + .iter() + .enumerate() + .filter(|(_, l)| l.to_lowercase().contains(&query_lower)) + .map(|(i, _)| i) + .collect(); + + if match_indices.is_empty() { + return Ok(format!("{rel_path} no occurrences of {:?}\n", query)); + } + + let total = match_indices.len(); + match_indices.truncate(MAX_AROUND_OCCURRENCES); + + let mut out = String::new(); + out.push_str(&format!( + "{rel_path} {} occurrence{} of {:?}", + total, + if total == 1 { "" } else { "s" }, + query + )); + if total > MAX_AROUND_OCCURRENCES { + out.push_str(&format!( + " (showing first {MAX_AROUND_OCCURRENCES})" + )); + } + out.push_str("\n\n"); + + let n = lines.len(); + for (occurrence, &match_idx) in match_indices.iter().enumerate() { + let lo = match_idx.saturating_sub(before); + let hi = (match_idx + after + 1).min(n); + + out.push_str(&format!( + "match {} — line {}\n", + occurrence + 1, + match_idx + 1 + )); + + for (i, line) in lines[lo..hi].iter().enumerate() { + let line_num = lo + i + 1; + let marker = if lo + i == match_idx { '>' } else { ' ' }; + out.push_str(&format!("{marker} {:4} | {line}\n", line_num)); + } + out.push('\n'); + + if out.len() >= MAX_OUTPUT_CHARS { + out.push_str("... output limit reached\n"); + break; + } + } + + Ok(out) +} + +// --------------------------------------------------------------------------- +// list_symbols +// --------------------------------------------------------------------------- + +/// Extract and list all symbols from a file. Always re-reads from disk +/// so results are fresh regardless of index state. +pub fn list_symbols(workspace: &Path, rel_path: &str) -> Result { + let path = resolve(workspace, rel_path)?; + + let ext = path + .extension() + .and_then(|e| e.to_str()) + .unwrap_or(""); + + let lang = language_for(ext); + + let symbols: Vec = match &lang { + Some(l) => { + let content = fs::read_to_string(&path) + .with_context(|| format!("cannot read {rel_path}"))?; + extract_symbols(&content, l) + } + None => vec![], + }; + + let mut out = String::new(); + + if symbols.is_empty() { + out.push_str(&format!( + "{rel_path} no symbols (language: {})\n", + lang.as_deref().unwrap_or("unknown") + )); + return Ok(out); + } + + out.push_str(&format!( + "{rel_path} {} symbol{}\n\n", + symbols.len(), + if symbols.len() == 1 { "" } else { "s" } + )); + + for sym in &symbols { + out.push_str(&format!( + " {:5} {:<8} {}\n", + sym.line, sym.kind, sym.name + )); + } + + Ok(out) +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +fn resolve(workspace: &Path, rel_path: &str) -> Result { + let path = workspace.join(rel_path); + let canonical = path + .canonicalize() + .with_context(|| format!("file not found: {rel_path}"))?; + if !canonical.starts_with(workspace) { + return Err(anyhow!("path escapes workspace: {rel_path}")); + } + Ok(canonical) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::Path; + + fn workspace() -> &'static Path { + Path::new(env!("CARGO_MANIFEST_DIR")) + } + + #[test] + fn read_range_returns_correct_lines() { + let out = read_range(workspace(), "src/kernel/index.rs", 1, 5).unwrap(); + assert!(out.contains("src/kernel/index.rs")); + assert!(out.contains("1")); + } + + #[test] + fn read_range_rejects_zero_start() { + let err = read_range(workspace(), "src/main.rs", 0, 5).unwrap_err(); + assert!(err.to_string().contains("start line")); + } + + #[test] + fn read_range_rejects_inverted_range() { + let err = read_range(workspace(), "src/main.rs", 10, 5).unwrap_err(); + assert!(err.to_string().contains("end line")); + } + + #[test] + fn read_range_rejects_path_traversal() { + let err = read_range(workspace(), "../../etc/passwd", 1, 5).unwrap_err(); + assert!( + err.to_string().contains("not found") || err.to_string().contains("escapes"), + "unexpected error: {err}" + ); + } + + #[test] + fn read_range_caps_at_max_lines() { + // src/session.rs is large — request way more than MAX_RANGE_LINES + let out = read_range(workspace(), "src/session.rs", 1, 600).unwrap(); + let code_lines = out.lines().filter(|l| l.contains('|')).count(); + assert!(code_lines <= MAX_RANGE_LINES, "got {code_lines} lines, expected <= {MAX_RANGE_LINES}"); + assert!(out.contains("capped")); + } + + #[test] + fn read_around_finds_known_content() { + let out = read_around( + workspace(), + "src/kernel/index.rs", + "index_repo", + AroundParams::default(), + ).unwrap(); + assert!(out.contains('>'), "should have a match marker"); + assert!(out.contains("index_repo")); + } + + #[test] + fn read_around_reports_no_match() { + let out = read_around( + workspace(), + "src/kernel/index.rs", + "zzz_not_in_this_file_xyz", + AroundParams::default(), + ).unwrap(); + assert!(out.contains("no occurrences")); + } + + #[test] + fn read_around_caps_occurrences() { + // "use" appears many times — should show at most MAX_AROUND_OCCURRENCES + let out = read_around( + workspace(), + "src/session.rs", + "use ", + AroundParams::default(), + ).unwrap(); + let match_count = out.lines().filter(|l| l.starts_with("match ")).count(); + assert!(match_count <= MAX_AROUND_OCCURRENCES); + } + + #[test] + fn list_symbols_returns_rust_symbols() { + let out = list_symbols(workspace(), "src/kernel/index.rs").unwrap(); + assert!(out.contains("index_repo")); + assert!(out.contains("fn")); + assert!(out.contains("symbol")); + } + + #[test] + fn list_symbols_handles_markdown() { + let out = list_symbols(workspace(), "README.md").unwrap(); + // No error, just "no symbols" message + assert!(!out.is_empty()); + } +} diff --git a/src/kernel/search.rs b/src/kernel/search.rs new file mode 100644 index 0000000..0dbeefd --- /dev/null +++ b/src/kernel/search.rs @@ -0,0 +1,334 @@ +use std::{path::Path, process::Command}; + +use anyhow::{Context, Result}; +use serde::Deserialize; + +const DEFAULT_MAX_RESULTS: usize = 15; +const MAX_RESULTS_HARD_CAP: usize = 20; +const DEFAULT_CONTEXT_LINES: usize = 2; +const MAX_CONTEXT_LINES: usize = 4; +const MAX_MATCHES_PER_FILE: usize = 3; +const MAX_OUTPUT_CHARS: usize = 3_000; + +pub struct SearchParams { + pub query: String, + /// ripgrep glob pattern, e.g. `*.rs` + pub file_glob: Option, + pub max_results: usize, + pub context_lines: usize, +} + +impl SearchParams { + pub fn new(query: impl Into) -> Self { + Self { + query: query.into(), + file_glob: None, + max_results: DEFAULT_MAX_RESULTS, + context_lines: DEFAULT_CONTEXT_LINES, + } + } +} + +pub struct SearchResult { + pub query: String, + pub match_count: usize, + pub truncated: bool, + pub files: Vec, + /// Ready-to-use text block for the model's context pack. + pub formatted: String, +} + +pub fn search(workspace: &Path, params: SearchParams) -> Result { + let max_results = params.max_results.min(MAX_RESULTS_HARD_CAP); + let context_lines = params.context_lines.min(MAX_CONTEXT_LINES); + + let mut cmd = Command::new("rg"); + cmd.current_dir(workspace) + .arg("--json") + .arg("--smart-case") + .arg("--max-count") + .arg(MAX_MATCHES_PER_FILE.to_string()) + .arg("--context") + .arg(context_lines.to_string()) + .arg("--max-filesize") + .arg("524288"); + + if let Some(glob) = ¶ms.file_glob { + cmd.arg("--glob").arg(glob); + } + + cmd.arg("--").arg(¶ms.query); + + let output = cmd + .output() + .context("failed to run ripgrep — is rg installed and on PATH?")?; + + // exit code 2 = real error; exit code 1 = no matches (ok) + if output.status.code() == Some(2) { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(anyhow::anyhow!("ripgrep error: {}", stderr.trim())); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + let files = parse_rg_json(&stdout, max_results); + let match_count: usize = files.iter().map(|f| f.match_count).sum(); + let truncated = match_count >= max_results; + let matched_files = files.iter().map(|f| f.file.clone()).collect(); + let formatted = format_output(¶ms.query, &files, truncated); + + Ok(SearchResult { + query: params.query, + match_count, + truncated, + files: matched_files, + formatted, + }) +} + +// --------------------------------------------------------------------------- +// rg --json parsing +// --------------------------------------------------------------------------- + +struct FileLine { + line_number: usize, + text: String, + is_match: bool, +} + +struct FileResult { + file: String, + lines: Vec, + match_count: usize, +} + +#[derive(Deserialize)] +struct RgMessage { + #[serde(rename = "type")] + kind: String, + data: RgData, +} + +#[derive(Deserialize)] +struct RgData { + path: Option, + line_number: Option, + lines: Option, +} + +#[derive(Deserialize)] +struct RgText { + text: Option, +} + +fn parse_rg_json(stdout: &str, max_results: usize) -> Vec { + let mut results: Vec = Vec::new(); + let mut current: Option = None; + let mut total_matches: usize = 0; + + for line in stdout.lines() { + if total_matches >= max_results { + break; + } + + let Ok(msg) = serde_json::from_str::(line) else { + continue; + }; + + match msg.kind.as_str() { + "begin" => { + if let Some(prev) = current.take() { + if prev.match_count > 0 { + results.push(prev); + } + } + let file = msg.data.path.and_then(|p| p.text).unwrap_or_default(); + current = Some(FileResult { + file, + lines: vec![], + match_count: 0, + }); + } + "match" => { + let line_number = msg.data.line_number.unwrap_or(0); + let text = msg + .data + .lines + .and_then(|l| l.text) + .unwrap_or_default() + .trim_end_matches('\n') + .to_string(); + if let Some(r) = &mut current { + r.lines.push(FileLine { + line_number, + text, + is_match: true, + }); + r.match_count += 1; + total_matches += 1; + } + } + "context" => { + let line_number = msg.data.line_number.unwrap_or(0); + let text = msg + .data + .lines + .and_then(|l| l.text) + .unwrap_or_default() + .trim_end_matches('\n') + .to_string(); + if let Some(r) = &mut current { + r.lines.push(FileLine { + line_number, + text, + is_match: false, + }); + } + } + "end" => { + if let Some(r) = current.take() { + if r.match_count > 0 { + results.push(r); + } + } + } + _ => {} + } + } + + if let Some(r) = current { + if r.match_count > 0 { + results.push(r); + } + } + + results +} + +// --------------------------------------------------------------------------- +// Formatting +// --------------------------------------------------------------------------- + +fn format_output(query: &str, files: &[FileResult], truncated: bool) -> String { + let total: usize = files.iter().map(|f| f.match_count).sum(); + + + let mut out = String::new(); + + out.push_str(&format!("search: {:?}\n", query)); + if total == 0 { + out.push_str("no matches\n"); + return out; + } + + out.push_str(&format!("matches: {total}")); + if truncated { + out.push_str(" (capped — refine query or use a file glob)"); + } + out.push_str("\n\n"); + + 'outer: for file in files { + out.push_str(&file.file); + out.push('\n'); + + let mut prev_line: Option = None; + for fl in &file.lines { + if out.len() >= MAX_OUTPUT_CHARS { + out.push_str("... output limit reached\n"); + break 'outer; + } + if let Some(prev) = prev_line { + if fl.line_number > prev + 1 { + out.push_str(" ...\n"); + } + } + let marker = if fl.is_match { '>' } else { ' ' }; + out.push_str(&format!("{marker} {:4} | {}\n", fl.line_number, fl.text)); + prev_line = Some(fl.line_number); + } + out.push('\n'); + } + + cap_output(out) +} + +fn cap_output(mut out: String) -> String { + if out.len() <= MAX_OUTPUT_CHARS { + return out; + } + + let mut cutoff = MAX_OUTPUT_CHARS; + while !out.is_char_boundary(cutoff) { + cutoff -= 1; + } + out.truncate(cutoff); + out.push_str("\n... output limit reached\n"); + out +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::Path; + + fn workspace() -> &'static Path { + Path::new(env!("CARGO_MANIFEST_DIR")) + } + + #[test] + fn search_result_exposes_matched_files() { + let result = search(workspace(), SearchParams::new("SearchResult")).unwrap(); + + assert!(result.files.iter().any(|file| file == "src/kernel/search.rs")); + } + + #[test] + fn search_finds_known_term() { + let result = search(workspace(), SearchParams::new("index_repo")).unwrap(); + assert!(result.match_count > 0); + assert!(result.formatted.contains("index_repo")); + } + + #[test] + fn search_no_matches() { + // Search for a Rust-only pattern inside TOML files — structurally impossible to match. + let mut params = SearchParams::new("pub async fn"); + params.file_glob = Some("*.toml".to_string()); + let result = search(workspace(), params).unwrap(); + assert_eq!(result.match_count, 0); + assert!(result.formatted.contains("no matches")); + } + + #[test] + fn search_glob_restricts_files() { + let mut params = SearchParams::new("pub fn"); + params.file_glob = Some("*.toml".to_string()); + let result = search(workspace(), params).unwrap(); + // toml files don't have `pub fn` — should be zero or only toml paths + for line in result.formatted.lines() { + if line.ends_with(".rs") && !line.contains('|') { + panic!("unexpected .rs file in toml-only search: {line}"); + } + } + } + + #[test] + fn search_output_within_char_limit() { + // "fn" is very common — will hit the cap + let result = search(workspace(), SearchParams::new("fn")).unwrap(); + assert!(result.formatted.len() <= MAX_OUTPUT_CHARS + 60); + } + + #[test] + fn search_caps_total_matches() { + let result = search(workspace(), SearchParams::new("fn")).unwrap(); + assert!(result.match_count <= MAX_RESULTS_HARD_CAP); + } + + #[test] + fn search_truncated_flag_set_at_cap() { + let result = search(workspace(), SearchParams::new("fn")).unwrap(); + if result.match_count >= MAX_RESULTS_HARD_CAP { + assert!(result.truncated); + } + } +} diff --git a/src/kernel/worker.rs b/src/kernel/worker.rs new file mode 100644 index 0000000..99f3a95 --- /dev/null +++ b/src/kernel/worker.rs @@ -0,0 +1,762 @@ +use std::path::{Path, PathBuf}; + +use anyhow::{Result, anyhow}; +use tokio::sync::mpsc; + +use crate::{ + adapter::{ApiMessage, ChatRequestPayload, RequestMode}, + config::Config, + crp, + model::ModelClient, +}; + +use super::context_pack::{ContextHints, ContextPack, ContextPackBuilder}; +use super::memory; +use super::patch; +use super::search::{SearchParams, search}; + +// --------------------------------------------------------------------------- +// Stage types +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum StageKind { + Interpret, + Locate, + Hypothesize, + Patch, + Report, +} + +impl StageKind { + pub fn label(&self) -> &'static str { + match self { + StageKind::Interpret => "interpret", + StageKind::Locate => "locate", + StageKind::Hypothesize => "hypothesize", + StageKind::Patch => "patch", + StageKind::Report => "report", + } + } + + fn template_name(&self) -> &'static str { + match self { + StageKind::Patch => "code_edit", + _ => "code_edit_minimal", + } + } +} + +/// Typed output produced by each stage. +#[derive(Debug, Clone, Default)] +pub struct StageOutput { + pub search_terms: Vec, + pub relevant_files: Vec, + pub approach: Option, + pub file_edits: Option, + pub final_response: Option, + pub raw_trace: String, + /// False when the model responded in plain text instead of CRP format. + pub crp_valid: bool, +} + +// --------------------------------------------------------------------------- +// Events +// --------------------------------------------------------------------------- + +#[derive(Debug)] +pub enum WorkerEvent { + StageStarted { stage: String }, + StageCompleted { stage: String, crp_valid: bool }, + StageRetry { stage: String, attempt: u32, reason: String }, + StageFailed { stage: String, error: String }, + ContextPackReady { stage: String, chars_used: usize, budget: usize }, + WorkflowComplete { final_response: String }, + WorkflowFailed { error: String }, + PatchApprovalRequested { + path: String, + preview: String, + response_tx: tokio::sync::oneshot::Sender, + }, + PatchApplied { files_changed: Vec }, + StageSkipped { stage: String, reason: String }, + /// Full prompt/response pair for diagnostics and fine-tuning dataset generation. + StageTrace { + stage: String, + attempt: u32, + system_prompt: String, + user_message: String, + model_response: String, + crp_valid: bool, + error: Option, + finish_reason: Option, + }, +} + +// --------------------------------------------------------------------------- +// Worker loop +// --------------------------------------------------------------------------- + +pub struct WorkerLoop { + workspace: PathBuf, + config: Config, + task: String, + event_tx: mpsc::UnboundedSender, + /// Override the default context pack budget (chars). None = use DEFAULT_CODE_BUDGET. + budget: Option, +} + +impl WorkerLoop { + pub fn new( + workspace: &Path, + config: Config, + task: impl Into, + event_tx: mpsc::UnboundedSender, + ) -> Self { + Self { + workspace: workspace.to_path_buf(), + config, + task: task.into(), + event_tx, + budget: None, + } + } + + pub fn with_budget(mut self, chars: usize) -> Self { + self.budget = Some(chars); + self + } + + /// Run the full bugfix/feature pipeline. + pub async fn run_bugfix(self) -> Result { + let task = self.task.clone(); + + // ── Stage 1: INTERPRET ────────────────────────────────────────────── + // Minimal pack — no code context yet, just the task. + let interpret_pack = self.make_pack(&task, &ContextHints::default()); + self.emit_pack(&interpret_pack, StageKind::Interpret.label()); + + self.emit(WorkerEvent::StageStarted { + stage: StageKind::Interpret.label().into(), + }); + let interpret_out = self + .run_stage(StageKind::Interpret, &task, &interpret_pack) + .await?; + self.emit(WorkerEvent::StageCompleted { + stage: StageKind::Interpret.label().into(), + crp_valid: interpret_out.crp_valid, + }); + + // ── Pipeline: load index concurrently while we continue ───────────── + // The index load is I/O — we spawn it so stage 2's pack is ready faster. + let ws = self.workspace.clone(); + let index_task = + tokio::task::spawn_blocking(move || ContextPackBuilder::new(&ws).with_index()); + + // ── Stage 2: LOCATE ───────────────────────────────────────────────── + let builder = index_task + .await + .unwrap_or_else(|_| ContextPackBuilder::new(&self.workspace).with_index()); + + let locate_hints = ContextHints { + search_terms: interpret_out.search_terms.clone(), + ..Default::default() + }; + let locate_pack = builder.build(&task, &locate_hints)?; + self.emit_pack(&locate_pack, StageKind::Locate.label()); + + self.emit(WorkerEvent::StageStarted { + stage: StageKind::Locate.label().into(), + }); + let mut locate_out = match self + .run_stage(StageKind::Locate, &task, &locate_pack) + .await + { + Ok(out) => { + self.emit(WorkerEvent::StageCompleted { + stage: StageKind::Locate.label().into(), + crp_valid: out.crp_valid, + }); + out + } + Err(e) => { + // Locate failed — skip it and let hypothesize use interpret's + // search terms to find code context via the context pack. + self.emit(WorkerEvent::StageSkipped { + stage: StageKind::Locate.label().into(), + reason: e.to_string(), + }); + StageOutput::default() + } + }; + if locate_out.relevant_files.is_empty() { + locate_out.relevant_files = + file_hints_from_search_terms(&self.workspace, &interpret_out.search_terms); + } + + // ── Pipeline: reload index for symbol/code reads ───────────────────── + let ws = self.workspace.clone(); + let sym_task = + tokio::task::spawn_blocking(move || ContextPackBuilder::new(&ws).with_index()); + + // ── Stage 3: HYPOTHESIZE ───────────────────────────────────────────── + let builder = sym_task + .await + .unwrap_or_else(|_| ContextPackBuilder::new(&self.workspace).with_index()); + + // Load past decisions to give hypothesize stage relevant prior context. + let past_decisions = memory::load_recent_decisions(&self.workspace); + let hyp_task = if past_decisions.is_empty() { + task.clone() + } else { + format!("{task}\n\n[Past decisions]\n{past_decisions}") + }; + + let hyp_hints = ContextHints { + files: locate_out.relevant_files.clone(), + search_terms: interpret_out.search_terms.clone(), + ..Default::default() + }; + let hyp_pack = builder.build(&hyp_task, &hyp_hints)?; + self.emit_pack(&hyp_pack, StageKind::Hypothesize.label()); + + self.emit(WorkerEvent::StageStarted { + stage: StageKind::Hypothesize.label().into(), + }); + let hyp_out = self + .run_stage(StageKind::Hypothesize, &hyp_task, &hyp_pack) + .await?; + self.emit(WorkerEvent::StageCompleted { + stage: StageKind::Hypothesize.label().into(), + crp_valid: hyp_out.crp_valid, + }); + + // Save the approach to local memory for future runs. + if let Some(ref approach) = hyp_out.approach { + let _ = memory::save_decision(&self.workspace, &task, approach); + } + + // ── Stage 4: PATCH ─────────────────────────────────────────────────── + // Enrich the task description with the hypothesis for the patch stage. + let patch_task = match &hyp_out.approach { + Some(a) => format!("{task}\n\nApproach:\n{a}"), + None => task.clone(), + }; + // Give the patch stage full file content — a model writing a replace + // edit must see the whole file. The context pack budget (sized from + // the actual server context window) keeps total size in check. + let patch_hints = ContextHints { + files: locate_out.relevant_files.clone(), + ranges: locate_out + .relevant_files + .iter() + .map(|f| (f.clone(), 1, 150)) + .collect(), + ..Default::default() + }; + let patch_pack = self.builder() + .build(&patch_task, &patch_hints)?; + self.emit_pack(&patch_pack, StageKind::Patch.label()); + + self.emit(WorkerEvent::StageStarted { + stage: StageKind::Patch.label().into(), + }); + let patch_out = self + .run_stage(StageKind::Patch, &patch_task, &patch_pack) + .await?; + self.emit(WorkerEvent::StageCompleted { + stage: StageKind::Patch.label().into(), + crp_valid: patch_out.crp_valid, + }); + + // ── Apply patch ────────────────────────────────────────────────────── + let mut files_changed: Vec = Vec::new(); + if let Some(ref edits) = patch_out.file_edits { + let directives = patch::parse_edit_directives(edits); + for directive in &directives { + let preview = patch::preview(&self.workspace, directive); + let (response_tx, response_rx) = tokio::sync::oneshot::channel::(); + self.emit(WorkerEvent::PatchApprovalRequested { + path: directive.path.clone(), + preview, + response_tx, + }); + let approved = response_rx.await.unwrap_or(false); + if approved { + match patch::apply_directive(&self.workspace, directive) { + Ok(summary) => files_changed.push(summary), + Err(e) => self.emit(WorkerEvent::StageFailed { + stage: "patch:apply".into(), + error: e.to_string(), + }), + } + } + } + if !files_changed.is_empty() { + self.emit(WorkerEvent::PatchApplied { + files_changed: files_changed.clone(), + }); + } + } + + // ── Stage 5: REPORT ────────────────────────────────────────────────── + let patch_summary = if files_changed.is_empty() { + "No files were changed.".to_string() + } else { + files_changed.join(", ") + }; + let report_task = format!( + "{task}\n\nChanges applied: {patch_summary}\nSummarize what was done and what to verify." + ); + let report_pack = ContextPackBuilder::new(&self.workspace) + .with_budget(4_000) + .build(&report_task, &ContextHints::default())?; + self.emit_pack(&report_pack, StageKind::Report.label()); + + self.emit(WorkerEvent::StageStarted { + stage: StageKind::Report.label().into(), + }); + let report_out = self + .run_stage(StageKind::Report, &report_task, &report_pack) + .await?; + self.emit(WorkerEvent::StageCompleted { + stage: StageKind::Report.label().into(), + crp_valid: report_out.crp_valid, + }); + + let final_response = report_out + .final_response + .unwrap_or_else(|| "Task complete.".into()); + + self.emit(WorkerEvent::WorkflowComplete { + final_response: final_response.clone(), + }); + + Ok(final_response) + } + + // ── Single stage ───────────────────────────────────────────────────────── + + async fn run_stage( + &self, + kind: StageKind, + task: &str, + pack: &ContextPack, + ) -> Result { + let templates = crp::TemplateSet::builtin(); + let template = templates.resolve(kind.template_name(), ""); + let system_prompt = self.config.apply_no_think_prefix(format!( + "{}\n\n{}\n\n---\n\n{}", + template.render_brief(), + one_shot_example(&kind), + pack.formatted + )); + + let client = ModelClient::new(self.config.model.clone()); + let max_retries = self.config.harness.crp_retry_budget.max(1); + + let mut user_message = task.to_string(); + for attempt in 0..=max_retries { + let payload = build_payload(&self.config, &system_prompt, &user_message); + let result = client + .complete(payload) + .await + .map_err(|e| anyhow!("model call failed at stage {}: {e}", kind.label()))?; + let finish_reason = result.finish_reason.clone(); + + match parse_stage_output(&result.text, &kind) { + Ok(out) => { + self.emit(WorkerEvent::StageTrace { + stage: kind.label().into(), + attempt, + system_prompt: system_prompt.clone(), + user_message: user_message.clone(), + model_response: result.text.clone(), + crp_valid: out.crp_valid, + error: None, + finish_reason, + }); + return Ok(out); + } + Err(reason) if attempt < max_retries => { + let reason = with_finish_reason(reason, finish_reason.as_deref()); + self.emit(WorkerEvent::StageTrace { + stage: kind.label().into(), + attempt, + system_prompt: system_prompt.clone(), + user_message: user_message.clone(), + model_response: result.text.clone(), + crp_valid: false, + error: Some(reason.clone()), + finish_reason, + }); + self.emit(WorkerEvent::StageRetry { + stage: kind.label().into(), + attempt: attempt + 1, + reason: reason.clone(), + }); + user_message = retry_user_message(task, kind.label(), &reason); + } + Err(reason) => { + let reason = with_finish_reason(reason, finish_reason.as_deref()); + self.emit(WorkerEvent::StageTrace { + stage: kind.label().into(), + attempt, + system_prompt: system_prompt.clone(), + user_message: user_message.clone(), + model_response: result.text.clone(), + crp_valid: false, + error: Some(reason.clone()), + finish_reason, + }); + self.emit(WorkerEvent::StageFailed { + stage: kind.label().into(), + error: reason.clone(), + }); + return Err(anyhow!("stage {} exhausted retries: {}", kind.label(), reason)); + } + } + } + + unreachable!() + } + + fn make_pack(&self, task: &str, hints: &ContextHints) -> ContextPack { + let mut builder = ContextPackBuilder::new(&self.workspace).with_index(); + if let Some(b) = self.budget { + builder = builder.with_budget(b); + } + builder.build(task, hints).unwrap_or_else(|_| ContextPack { + chars_used: 0, + chars_budget: 0, + truncated: false, + formatted: format!("[TASK]\n{task}\n"), + }) + } + + fn builder(&self) -> ContextPackBuilder { + let mut b = ContextPackBuilder::new(&self.workspace).with_index(); + if let Some(budget) = self.budget { + b = b.with_budget(budget); + } + b + } + + fn emit(&self, event: WorkerEvent) { + let _ = self.event_tx.send(event); + } + + fn emit_pack(&self, pack: &ContextPack, stage: &str) { + self.emit(WorkerEvent::ContextPackReady { + stage: stage.into(), + chars_used: pack.chars_used, + budget: pack.chars_budget, + }); + } +} + +// --------------------------------------------------------------------------- +// One-shot format example +// --------------------------------------------------------------------------- + +fn one_shot_example(kind: &StageKind) -> &'static str { + match kind { + StageKind::Interpret => "\ +## Output format — follow this exactly + + +One sentence restating the task in your own words. + + + +Brief answer or plan. +", + + StageKind::Locate => "\ +## Output format — follow this exactly + + +src/lib.rs +src/main.rs + + + +The relevant files are src/lib.rs and src/main.rs. +", + + StageKind::Hypothesize => "\ +## Output format — follow this exactly + + +- Step one of the fix. +- Step two of the fix. + + + +Brief summary of the proposed approach. +", + + StageKind::Patch => "\ +## Output format — follow this exactly + +To fix or replace a single function: + + +fn broken_fn() -> i32 { + 42 +} + + + +To add methods to a class or rewrite a whole file, use replace mode with the COMPLETE file content: + + +class User: + def __init__(self, name): + self._name = name + + def get_name(self): + return self._name + + def get_email(self): + return self._email + + + + +Brief description of what was changed. +", + + StageKind::Report => "\ +## Output format — follow this exactly + + +Summary of what was changed and what the user should verify. +", + } +} + +// --------------------------------------------------------------------------- +// Payload construction +// --------------------------------------------------------------------------- + +fn build_payload(config: &Config, system_prompt: &str, user_message: &str) -> ChatRequestPayload { + match config.model.format.as_str() { + "harmony" => ChatRequestPayload { + mode: RequestMode::HarmonyText { + prompt: format!("{system_prompt}\n\n<|user|>{user_message}<|assistant|>"), + }, + }, + _ => ChatRequestPayload { + mode: RequestMode::OpenAiChat { + messages: vec![ + ApiMessage { + role: "system".into(), + content: Some(system_prompt.into()), + name: None, + tool_call_id: None, + tool_calls: None, + }, + ApiMessage { + role: "user".into(), + content: Some(user_message.into()), + name: None, + tool_call_id: None, + tool_calls: None, + }, + ], + tools: vec![], + }, + }, + } +} + +// --------------------------------------------------------------------------- +// Stage output parsing +// --------------------------------------------------------------------------- + +/// Strip `...` blocks emitted by reasoning models (Qwen3, DeepSeek-R1, etc.) +/// before attempting to parse the response. Returns the text after the last closing tag, +/// or the full text if no think block is present. +fn strip_think_blocks(text: &str) -> &str { + // Find the last
tag — the actual response comes after it. + if let Some(end) = text.rfind("") { + text[end + "".len()..].trim_start() + } else { + text + } +} + +fn parse_stage_output(raw: &str, kind: &StageKind) -> Result { + let raw = strip_think_blocks(raw); + + // Try structured CRP first. + if let Ok(trace) = crp::parse(raw) { + let final_response = trace + .get("FINAL_RESPONSE") + .map(|s| s.content.trim().to_string()); + + if final_response.as_deref().is_some_and(|s| !s.is_empty()) { + let mut out = StageOutput { + raw_trace: raw.to_string(), + final_response, + crp_valid: true, + ..Default::default() + }; + + if matches!(kind, StageKind::Interpret) { + let source = trace + .get("TASK_INTERPRETATION") + .or_else(|| trace.get("PROPOSED_APPROACH")) + .map(|s| s.content.as_str()) + .unwrap_or_default(); + out.search_terms = extract_search_terms(source); + } + if matches!(kind, StageKind::Locate) { + if let Some(slot) = trace.get("RELEVANT_FILES") { + out.relevant_files = parse_bullet_paths(&slot.content); + } + } + if let Some(slot) = trace.get("PROPOSED_APPROACH") { + let t = slot.content.trim(); + if !t.is_empty() { + out.approach = Some(t.to_string()); + } + } + if let Some(slot) = trace.get("FILE_EDITS") { + let t = slot.content.trim(); + if !t.is_empty() { + out.file_edits = Some(t.to_string()); + } + } + return Ok(out); + } + } + + // Plain-text fallback for models that don't follow CRP format. + // The response is non-empty prose — salvage what we can so the + // pipeline can continue. crp_valid = false is recorded in metrics. + let trimmed = raw.trim(); + if trimmed.is_empty() { + return Err("empty model response".into()); + } + + let mut out = StageOutput { + raw_trace: raw.to_string(), + final_response: Some(trimmed.to_string()), + ..Default::default() + }; + + // For interpret, extract identifiers from plain prose as search terms. + if matches!(kind, StageKind::Interpret) { + out.search_terms = extract_search_terms(trimmed); + } + + // For locate, try to find file paths mentioned in plain text. + if matches!(kind, StageKind::Locate) { + out.relevant_files = parse_bullet_paths(trimmed); + if out.relevant_files.is_empty() { + // Scan for src/*.rs style paths directly in prose + out.relevant_files = trimmed + .split_whitespace() + .filter(|w| w.contains('/') && w.contains('.')) + .map(|w| w.trim_matches(|c: char| !c.is_alphanumeric() && c != '/' && c != '.').to_string()) + .filter(|w| !w.is_empty()) + .take(6) + .collect(); + } + } + + if matches!(kind, StageKind::Hypothesize | StageKind::Patch) { + out.approach = Some(trimmed.to_string()); + } + + Ok(out) +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +fn extract_search_terms(text: &str) -> Vec { + text.split_whitespace() + .filter_map(|word| { + let clean: String = word + .trim_matches(|c: char| !c.is_alphanumeric() && c != '_' && c != '/' && c != '.') + .to_string(); + if clean.len() > 3 + && clean.chars().next().is_some_and(|c| c.is_alphabetic()) + && clean + .chars() + .all(|c| c.is_alphanumeric() || matches!(c, '_' | '.' | '/')) + { + Some(clean) + } else { + None + } + }) + .take(8) + .collect() +} + +fn parse_bullet_paths(text: &str) -> Vec { + text.lines() + .filter_map(|line| { + let t = line + .trim() + .trim_start_matches(['-', '*', ' ']) + .trim(); + if t.is_empty() || t.starts_with('#') { + return None; + } + let path = t.split_whitespace().next().unwrap_or("").to_string(); + if path.is_empty() { None } else { Some(path) } + }) + .collect() +} + +fn with_finish_reason(reason: String, finish_reason: Option<&str>) -> String { + let Some(finish_reason) = finish_reason.filter(|value| !value.trim().is_empty()) else { + return reason; + }; + format!("{reason} (finish_reason: {finish_reason})") +} + +fn retry_user_message(task: &str, stage: &str, reason: &str) -> String { + format!( + "{task}\n\n\nThe previous {stage} response was not usable: {reason}. Re-emit a concise response in the requested CRP format with a non-empty .\n" + ) +} + +fn file_hints_from_search_terms(workspace: &Path, terms: &[String]) -> Vec { + let mut files = Vec::new(); + for term in terms.iter().filter(|term| !term.trim().is_empty()).take(8) { + let Ok(result) = search(workspace, SearchParams::new(term)) else { + continue; + }; + for file in result.files { + if files.len() >= 6 { + return files; + } + if !files.iter().any(|seen| seen == &file) { + files.push(file); + } + } + } + files +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn file_hints_fall_back_to_search_matches() { + let workspace = Path::new(env!("CARGO_MANIFEST_DIR")); + let terms = vec!["WorkerLoop".to_string()]; + + let files = file_hints_from_search_terms(workspace, &terms); + + assert!(files.iter().any(|file| file == "src/kernel/worker.rs")); + } +} diff --git a/src/main.rs b/src/main.rs index 38290b4..8a2826b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,11 @@ mod adapter; +mod batch; mod config; +pub mod crp; +mod eval_diff; mod harmony; +mod init; +mod kernel; mod model; mod session; mod theme; @@ -14,6 +19,7 @@ use anyhow::Result; use clap::{Parser, Subcommand}; use crate::{config::Config, session::AgentSession, ui::App}; +use crate::config as config_mod; #[derive(Debug, Parser)] #[command( @@ -24,7 +30,7 @@ struct Args { #[arg(short, long)] config: Option, - #[arg(long, help = "Render the current empty Harmony prompt and exit")] + #[arg(long, help = "Render the current empty model prompt and exit")] print_prompt: bool, #[arg( @@ -41,6 +47,21 @@ struct Args { enum Command { #[command(about = "Open the initial setup TUI")] Setup, + #[command(about = "Apply a built-in preset to the saved config without opening the TUI")] + UsePreset { + #[arg(help = "Preset name (e.g. lm-studio-small, qwen3, ollama, vllm, minimal)")] + preset: String, + #[arg(long, help = "Override the model name from the preset")] + model: Option, + #[arg(long, help = "Override the context window size in tokens")] + context: Option, + #[arg(long, help = "Override max output tokens")] + max_tokens: Option, + }, + #[command(about = "List available built-in presets")] + Presets, + #[command(about = "Initialize .cinto/templates in the current workspace")] + Init, #[command(about = "Remove the installed cinto binary")] Uninstall { #[arg(long, help = "Remove ~/.config/cinto after removing the binary")] @@ -49,6 +70,95 @@ enum Command { #[arg(long, help = "Do not ask for confirmation")] yes: bool, }, + #[command(about = "Run tasks headlessly to generate synthetic CRP datasets")] + Batch { + #[arg(long, help = "Path to the input JSONL tasks file")] + tasks: PathBuf, + #[arg(long, help = "Path to the output JSONL file for valid traces")] + output: PathBuf, + #[arg( + long, + help = "Optional LLM endpoint to use as a semantic evaluator (e.g., https://api.deepseek.com/v1)" + )] + evaluator_endpoint: Option, + #[arg( + long, + help = "Optional LLM model name for the evaluator (e.g., deepseek-chat)" + )] + evaluator_model: Option, + #[arg(long, help = "Optional API key for the evaluator model")] + evaluator_api_key: Option, + #[arg( + long, + help = "Validate the JSONL and fixtures without calling the model" + )] + dry_run: bool, + #[arg( + long, + help = "DANGEROUS: Automatically approve all tool executions (including shell commands). Only use in an isolated VM or Docker container!" + )] + dangerously_auto_approve: bool, + #[arg(long, help = "Run tasks through the kernel pipeline instead of the conversational agent")] + kernel: bool, + #[arg(long, help = "Context pack budget in chars for kernel mode (default 16000, reduce for small models)")] + kernel_budget: Option, + #[arg(long, help = "Save full stage traces (prompt + response) to this directory for fine-tuning dataset generation")] + save_traces: Option, + }, + #[command( + about = "Compare two batch evaluation JSONL runs and highlight regressions/improvements" + )] + EvalDiff { + #[arg(help = "Path to the base (control) JSONL file")] + base: PathBuf, + #[arg(help = "Path to the compare (experiment) JSONL file")] + compare: PathBuf, + }, + #[command(about = "Index the workspace and write .cinto/project_map.json and symbol_index.json")] + Index, + #[command(about = "Search the workspace (kernel syscall, scoped output)")] + Search { + #[arg(help = "Query string (smart-case)")] + query: String, + #[arg(long, help = "Restrict to files matching a glob, e.g. '*.rs'")] + glob: Option, + }, + #[command(about = "Read a line range from a workspace file")] + ReadRange { + #[arg(help = "Relative file path")] + file: String, + #[arg(help = "Start line (1-indexed)")] + start: usize, + #[arg(help = "End line (inclusive)")] + end: usize, + }, + #[command(about = "Read lines around a query match in a workspace file")] + ReadAround { + #[arg(help = "Relative file path")] + file: String, + #[arg(help = "Search query")] + query: String, + #[arg(long, default_value = "5")] + before: usize, + #[arg(long, default_value = "5")] + after: usize, + }, + #[command(about = "List symbols defined in a workspace file")] + ListSymbols { + #[arg(help = "Relative file path")] + file: String, + }, + #[command(about = "Build and print a context pack for a task (kernel preview)")] + Pack { + #[arg(help = "Task description")] + task: String, + #[arg(long = "file", help = "Hint: include symbols for this file (repeatable)")] + files: Vec, + #[arg(long = "search", help = "Hint: run this search and include results (repeatable)")] + search_terms: Vec, + #[arg(long, help = "Budget in chars (default 16000)")] + budget: Option, + }, } #[tokio::main] @@ -60,10 +170,159 @@ async fn main() -> Result<()> { } let config_path = args.config.clone().or_else(Config::default_path); + let config = Config::load(args.config.clone())?; + + if let Some(Command::Batch { + tasks, + output, + evaluator_endpoint, + evaluator_model, + evaluator_api_key, + dry_run, + dangerously_auto_approve, + kernel, + kernel_budget, + save_traces, + }) = args.command + { + if kernel { + batch::run_kernel(config, tasks, output, dry_run, kernel_budget, save_traces).await?; + } else { + batch::run( + config, + tasks, + output, + evaluator_endpoint, + evaluator_model, + evaluator_api_key, + dry_run, + dangerously_auto_approve, + ) + .await?; + } + return Ok(()); + } + + if let Some(Command::EvalDiff { base, compare }) = args.command { + eval_diff::run(base, compare)?; + return Ok(()); + } + + if let Some(Command::Presets) = args.command { + for preset in config_mod::PRESETS { + println!("{:<20} {}", preset.name, preset.description); + } + return Ok(()); + } + + if let Some(Command::UsePreset { preset: preset_name, model: model_override, context, max_tokens }) = args.command { + let preset = config_mod::preset_by_name(&preset_name) + .ok_or_else(|| anyhow::anyhow!( + "unknown preset '{}'. Run `cinto presets` to list available presets.", + preset_name + ))?; + let mut new_config = preset.to_config()?; + new_config.harness.workspace = config.harness.workspace.clone(); + if let Some(m) = model_override { + new_config.model.model = m; + } + if let Some(c) = context { + new_config.model.context_window = c; + } + if let Some(t) = max_tokens { + new_config.model.max_tokens = t; + } + let path = new_config.save(config_path)?; + println!("Preset '{}' written to {}", preset_name, path.display()); + println!(" endpoint : {}", new_config.model.endpoint); + println!(" model : {}", new_config.model.model); + println!(" format : {}", new_config.model.format); + println!(" context : {} tokens", new_config.model.context_window); + println!(" max_tok : {}", new_config.model.max_tokens); + return Ok(()); + } + + if let Some(Command::Init) = args.command { + let config = Config::load(args.config)?; + init::run(&config)?; + return Ok(()); + } + + if let Some(Command::Search { query, glob }) = args.command { + let config = Config::load(args.config)?; + let workspace = &config.harness.workspace; + let mut params = kernel::search::SearchParams::new(query); + params.file_glob = glob; + let result = kernel::search::search(workspace, params)?; + print!("{}", result.formatted); + return Ok(()); + } + + if let Some(Command::Pack { task, files, search_terms, budget }) = args.command { + let config = Config::load(args.config)?; + let workspace = &config.harness.workspace; + let mut builder = kernel::context_pack::ContextPackBuilder::new(workspace) + .with_index(); + if let Some(b) = budget { + builder = builder.with_budget(b); + } + let hints = kernel::context_pack::ContextHints { + files, + search_terms, + ranges: vec![], + }; + let pack = builder.build(&task, &hints)?; + print!("{}", pack.formatted); + return Ok(()); + } + + if let Some(Command::ReadRange { file, start, end }) = args.command { + let config = Config::load(args.config)?; + let workspace = &config.harness.workspace; + let out = kernel::read::read_range(workspace, &file, start, end)?; + print!("{out}"); + return Ok(()); + } + + if let Some(Command::ReadAround { file, query, before, after }) = args.command { + let config = Config::load(args.config)?; + let workspace = &config.harness.workspace; + let params = kernel::read::AroundParams { before, after }; + let out = kernel::read::read_around(workspace, &file, &query, params)?; + print!("{out}"); + return Ok(()); + } + + if let Some(Command::ListSymbols { file }) = args.command { + let config = Config::load(args.config)?; + let workspace = &config.harness.workspace; + let out = kernel::read::list_symbols(workspace, &file)?; + print!("{out}"); + return Ok(()); + } + + if let Some(Command::Index) = args.command { + let config = Config::load(args.config)?; + let workspace = &config.harness.workspace; + let result = kernel::index::index_repo(workspace)?; + kernel::index::save(workspace, &result)?; + println!("Indexed workspace: {}", result.project_map.root); + println!(" Files: {}", result.file_count); + println!(" Symbols: {}", result.symbol_count); + println!( + " Written: {}", + workspace.join(".cinto/project_map.json").display() + ); + println!( + " {}", + workspace.join(".cinto/symbol_index.json").display() + ); + return Ok(()); + } + let setup_requested = matches!(args.command, Some(Command::Setup)); let first_run = setup_requested || (!args.skip_setup && config_path.as_ref().is_some_and(|path| !path.exists())); - let config = Config::load(args.config)?; let session = AgentSession::new(config); if args.print_prompt { diff --git a/src/model.rs b/src/model.rs index d4b1632..617375a 100644 --- a/src/model.rs +++ b/src/model.rs @@ -28,6 +28,98 @@ impl ModelClient { Self { http, config } } + pub async fn detect_context_window(&self) -> Result> { + let base_endpoint = model_base_endpoint(&self.config.endpoint); + for endpoint in [ + format!("{base_endpoint}/v1/models"), + format!("{base_endpoint}/api/v0/models"), + ] { + if let Some(context_window) = + self.detect_context_from_models_endpoint(&endpoint).await? + { + return Ok(Some(context_window)); + } + } + + self.detect_context_from_ollama_show(&base_endpoint).await + } + + /// Gets the maximum context window size for a model. + /// Tries automatic detection first, then falls back to provider-specific defaults (halved). + /// If configured context_window is smaller than what's available, uses the configured value. + pub async fn get_available_context_window(&self) -> u32 { + if let Some(detected) = self.detect_context_window().await.ok().flatten() { + // If configured context_window is set, trust it (it might be intentionally larger or smaller than detected) + if self.config.context_window > 0 { + return self.config.context_window; + } + return detected; + } + + // Detection failed - use configured value directly if set, otherwise provider default + if self.config.context_window > 0 { + return self.config.context_window; + } + self.provider_default_context_limit() + } + + /// Gets the default context limit for this provider when detection fails. + fn provider_default_context_limit(&self) -> u32 { + let endpoint = self.config.endpoint.to_lowercase(); + + if endpoint.contains("lm-studio") || endpoint.contains("lmstudio") { + 131072 + } else if endpoint.contains("ollama") { + 131072 + } else if endpoint.contains("vllm") || endpoint.contains("llama.cpp") { + 1048576 + } else { + 131072 // Default for unknown providers (e.g. Gemini, Anthropic) + } + } + + async fn detect_context_from_models_endpoint(&self, endpoint: &str) -> Result> { + let response = match self.authorize(self.http.get(endpoint)).send().await { + Ok(response) => response, + Err(_) => return Ok(None), + }; + if !response.status().is_success() { + return Ok(None); + } + + let body: serde_json::Value = response + .json() + .await + .context("failed to decode models response")?; + let Some(model) = select_model_info(&body, &self.config.model) else { + return Ok(None); + }; + Ok(extract_context_window(model)) + } + + async fn detect_context_from_ollama_show(&self, base_endpoint: &str) -> Result> { + let endpoint = format!("{base_endpoint}/api/show"); + let request = serde_json::json!({ "model": &self.config.model }); + let response = match self + .authorize(self.http.post(&endpoint)) + .json(&request) + .send() + .await + { + Ok(response) => response, + Err(_) => return Ok(None), + }; + if !response.status().is_success() { + return Ok(None); + } + + let body: serde_json::Value = response + .json() + .await + .context("failed to decode Ollama model response")?; + Ok(extract_context_window(&body)) + } + pub async fn complete(&self, payload: ChatRequestPayload) -> Result { let endpoint = normalize_endpoint(&self.config.endpoint, &payload.mode); match payload.mode { @@ -107,15 +199,15 @@ impl ModelClient { .json() .await .context("failed to decode completion response")?; - let text = body + let choice = body .choices .into_iter() .next() - .map(|choice| choice.text) .context("completion response had no choices")?; Ok(CompletionResult { - text, + text: choice.text, tool_calls: Vec::new(), + finish_reason: choice.finish_reason, }) } } @@ -194,6 +286,7 @@ impl ModelClient { Ok(CompletionResult { text: choice.message.content.unwrap_or_default(), tool_calls: choice.message.tool_calls.unwrap_or_default(), + finish_reason: choice.finish_reason, }) } } @@ -219,6 +312,88 @@ impl ModelClient { } } +fn model_base_endpoint(endpoint: &str) -> String { + endpoint + .trim() + .trim_end_matches('/') + .trim_end_matches("/v1/chat/completions") + .trim_end_matches("/v1/completions") + .trim_end_matches("/v1") + .to_string() +} + +fn select_model_info<'a>( + body: &'a serde_json::Value, + configured_model: &str, +) -> Option<&'a serde_json::Value> { + let models = body + .get("data") + .and_then(serde_json::Value::as_array) + .or_else(|| body.as_array())?; + models + .iter() + .find(|model| { + model + .get("id") + .and_then(serde_json::Value::as_str) + .is_some_and(|id| id == configured_model) + }) + .or_else(|| { + if models.len() == 1 { + models.first() + } else { + None + } + }) +} + +fn extract_context_window(model: &serde_json::Value) -> Option { + const CONTEXT_FIELDS: &[&str] = &[ + "context_length", + "context_window", + "max_context_length", + "max_model_len", + "max_sequence_length", + "max_position_embeddings", + "input_token_limit", + "n_ctx", + "num_ctx", + ]; + const CONTAINERS: &[&str] = &["metadata", "config", "details", "model_info", "parameters"]; + + find_context_field(model, CONTEXT_FIELDS).or_else(|| { + CONTAINERS.iter().find_map(|container| { + model + .get(*container) + .and_then(|value| find_context_field(value, CONTEXT_FIELDS)) + }) + }) +} + +fn find_context_field(value: &serde_json::Value, fields: &[&str]) -> Option { + let object = value.as_object()?; + object.iter().find_map(|(key, value)| { + let matches_field = fields.iter().any(|field| { + key == field + || key + .strip_suffix(field) + .is_some_and(|prefix| prefix.ends_with('.')) + }); + matches_field + .then_some(value) + .and_then(context_value_as_u32) + }) +} + +fn context_value_as_u32(value: &serde_json::Value) -> Option { + let raw = value.as_u64().or_else(|| { + value + .as_str() + .and_then(|text| text.trim().parse::().ok()) + })?; + u32::try_from(raw).ok().filter(|tokens| *tokens > 0) +} + async fn require_success(response: reqwest::Response, endpoint: &str) -> Result { let status = response.status(); if status.is_success() { @@ -256,6 +431,7 @@ async fn collect_stream( let mut buffer = String::new(); let mut full = String::new(); let mut tool_calls: Vec = Vec::new(); + let mut finish_reason: Option = None; while let Some(chunk) = stream.next().await { let chunk = chunk.with_context(|| format!("failed while streaming from {endpoint}"))?; @@ -263,12 +439,24 @@ async fn collect_stream( while let Some(newline) = buffer.find('\n') { let line = buffer.drain(..=newline).collect::(); - process_sse_line(line.trim(), delta_tx.as_ref(), &mut full, &mut tool_calls)?; + process_sse_line( + line.trim(), + delta_tx.as_ref(), + &mut full, + &mut tool_calls, + &mut finish_reason, + )?; } } if !buffer.trim().is_empty() { - process_sse_line(buffer.trim(), delta_tx.as_ref(), &mut full, &mut tool_calls)?; + process_sse_line( + buffer.trim(), + delta_tx.as_ref(), + &mut full, + &mut tool_calls, + &mut finish_reason, + )?; } Ok(CompletionResult { @@ -277,6 +465,7 @@ async fn collect_stream( .into_iter() .filter_map(PartialToolCall::finish) .collect(), + finish_reason, }) } @@ -285,6 +474,7 @@ fn process_sse_line( delta_tx: Option<&UnboundedSender>, full: &mut String, tool_calls: &mut Vec, + finish_reason: &mut Option, ) -> Result<()> { let Some(data) = line.strip_prefix("data:") else { return Ok(()); @@ -298,6 +488,13 @@ fn process_sse_line( serde_json::from_str(data).context("failed to parse stream chunk")?; let choice = value.get("choices").and_then(|choices| choices.get(0)); if let Some(choice) = choice { + if let Some(reason) = choice + .get("finish_reason") + .and_then(serde_json::Value::as_str) + && !reason.is_empty() + { + *finish_reason = Some(reason.to_string()); + } if let Some(delta) = extract_delta(choice) && !delta.is_empty() { @@ -421,6 +618,8 @@ struct CompletionResponse { #[derive(Debug, Deserialize)] struct CompletionChoice { text: String, + #[serde(default)] + finish_reason: Option, } #[derive(Debug, Serialize)] @@ -463,6 +662,8 @@ struct ChatCompletionResponse { #[derive(Debug, Deserialize)] struct ChatCompletionChoice { message: ChatCompletionMessage, + #[serde(default)] + finish_reason: Option, } #[derive(Debug, Deserialize)] @@ -495,4 +696,67 @@ mod tests { "http://127.0.0.1:1234/v1/completions" ); } + + #[test] + fn captures_stream_finish_reason() { + let mut full = String::new(); + let mut tool_calls = Vec::new(); + let mut finish_reason = None; + + process_sse_line( + r#"data: {"choices":[{"delta":{"content":"hello"},"finish_reason":"length"}]}"#, + None, + &mut full, + &mut tool_calls, + &mut finish_reason, + ) + .expect("stream line"); + + assert_eq!(full, "hello"); + assert_eq!(finish_reason.as_deref(), Some("length")); + } + + #[test] + fn extracts_context_window_from_common_model_fields() { + let model = serde_json::json!({ + "id": "qwen", + "metadata": { + "max_model_len": 131072 + } + }); + + assert_eq!(extract_context_window(&model), Some(131072)); + } + + #[test] + fn extracts_namespaced_ollama_context_window() { + let model = serde_json::json!({ + "model_info": { + "llama.context_length": 98304 + } + }); + + assert_eq!(extract_context_window(&model), Some(98304)); + } + + #[test] + fn selects_exact_model_or_single_available_model() { + let body = serde_json::json!({ + "data": [ + { "id": "a", "context_length": 8192 }, + { "id": "b", "context_length": 65536 } + ] + }); + + let selected = select_model_info(&body, "b").expect("model b"); + assert_eq!(extract_context_window(selected), Some(65536)); + + let single = serde_json::json!({ + "data": [ + { "id": "served-model", "num_ctx": "32768" } + ] + }); + let selected = select_model_info(&single, "configured-alias").expect("single model"); + assert_eq!(extract_context_window(selected), Some(32768)); + } } diff --git a/src/presets/lm_studio.toml b/src/presets/lm_studio.toml new file mode 100644 index 0000000..feaa15d --- /dev/null +++ b/src/presets/lm_studio.toml @@ -0,0 +1,31 @@ +# Cinto preset: LM Studio +# Local LM Studio server with Harmony transport for gpt-oss models. + +[model] +endpoint = "http://127.0.0.1:1234" +model = "openai/gpt-oss-20b" +format = "harmony" +max_tokens = 4096 +temperature = 0.2 +thinking_effort = "medium" +stream = true +stop = ["<|return|>", "<|call|>"] +request_timeout_secs = 600 +context_window = 32768 + +[harness] +workspace = "." +allow_shell = false +require_edit_approval = true +reasoning_protocol = "crp" +crp_retry_budget = 3 +transport_retry_budget = 1 +empty_response_retry_budget = 1 +max_tool_turns = 24 +max_model_rounds = 64 +auto_context_compression = true +context_compression_threshold = 80 +context_compression_keep_recent = 18 +load_workspace_instructions = true +system_prompt = "You are Cinto, a local coding agent running in a terminal UI. You help the user understand and modify the current workspace." +developer_prompt = "Use concise reasoning, ask before destructive actions, and prefer small verifiable edits. When you need repository context, request tools in the commentary channel. Treat as the end of tool output; do not continue or echo tool output after that marker. If a tool result says it was compacted, use search or narrower reads for missing details. Treat as a concise note about earlier conversation history; rely on recent messages for exact details." diff --git a/src/presets/lm_studio_small.toml b/src/presets/lm_studio_small.toml new file mode 100644 index 0000000..b02467a --- /dev/null +++ b/src/presets/lm_studio_small.toml @@ -0,0 +1,33 @@ +# Cinto preset: LM Studio (small model) +# Optimized for sub-4B models (Gemma 4 E2B, Phi-3 mini, etc.) via LM Studio. +# Uses openai-tools format — LM Studio applies the model's native chat template. +# Harmony format is NOT used here: it is gpt-oss specific and confuses small models. + +[model] +endpoint = "http://127.0.0.1:1234" +model = "google/gemma-4-e2b" +format = "openai-tools" +max_tokens = 1024 +temperature = 0.1 +thinking_effort = "none" +stream = true +stop = [] +request_timeout_secs = 300 +context_window = 8192 + +[harness] +workspace = "." +allow_shell = false +require_edit_approval = true +reasoning_protocol = "crp" +crp_retry_budget = 2 +transport_retry_budget = 1 +empty_response_retry_budget = 1 +max_tool_turns = 12 +max_model_rounds = 24 +auto_context_compression = true +context_compression_threshold = 70 +context_compression_keep_recent = 8 +load_workspace_instructions = true +system_prompt = "You are Cinto, a local coding agent. You help the user understand and modify the current workspace." +developer_prompt = "Be concise. Prefer small verifiable edits. Ask before destructive actions." diff --git a/src/presets/minimal.toml b/src/presets/minimal.toml new file mode 100644 index 0000000..8b1ad1b --- /dev/null +++ b/src/presets/minimal.toml @@ -0,0 +1,31 @@ +# Cinto preset: Minimal +# Bare-bones config without CRP reasoning overhead. + +[model] +endpoint = "http://127.0.0.1:1234" +model = "local-model" +format = "openai-tools" +max_tokens = 4096 +temperature = 0.2 +thinking_effort = "none" +stream = true +stop = [] +request_timeout_secs = 300 +context_window = 32768 + +[harness] +workspace = "." +allow_shell = false +require_edit_approval = true +reasoning_protocol = "plain" +crp_retry_budget = 0 +transport_retry_budget = 1 +empty_response_retry_budget = 1 +max_tool_turns = 16 +max_model_rounds = 32 +auto_context_compression = true +context_compression_threshold = 80 +context_compression_keep_recent = 18 +load_workspace_instructions = true +system_prompt = "You are Cinto, a local coding agent running in a terminal UI. You help the user understand and modify the current workspace." +developer_prompt = "Use concise reasoning, ask before destructive actions, and prefer small verifiable edits. When you need repository context, request tools in the commentary channel. Treat as the end of tool output; do not continue or echo tool output after that marker. If a tool result says it was compacted, use search or narrower reads for missing details. Treat as a concise note about earlier conversation history; rely on recent messages for exact details." diff --git a/src/presets/ollama.toml b/src/presets/ollama.toml new file mode 100644 index 0000000..856f6ca --- /dev/null +++ b/src/presets/ollama.toml @@ -0,0 +1,31 @@ +# Cinto preset: Ollama +# Local Ollama server with OpenAI-tools format. + +[model] +endpoint = "http://127.0.0.1:11434" +model = "qwen2.5-coder:7b-instruct" +format = "openai-tools" +max_tokens = 4096 +temperature = 0.2 +thinking_effort = "none" +stream = true +stop = [] +request_timeout_secs = 600 +context_window = 32768 + +[harness] +workspace = "." +allow_shell = false +require_edit_approval = true +reasoning_protocol = "crp" +crp_retry_budget = 3 +transport_retry_budget = 1 +empty_response_retry_budget = 1 +max_tool_turns = 24 +max_model_rounds = 64 +auto_context_compression = true +context_compression_threshold = 80 +context_compression_keep_recent = 18 +load_workspace_instructions = true +system_prompt = "You are Cinto, a local coding agent running in a terminal UI. You help the user understand and modify the current workspace." +developer_prompt = "Use concise reasoning, ask before destructive actions, and prefer small verifiable edits. When you need repository context, request tools in the commentary channel. Treat as the end of tool output; do not continue or echo tool output after that marker. If a tool result says it was compacted, use search or narrower reads for missing details. Treat as a concise note about earlier conversation history; rely on recent messages for exact details." diff --git a/src/presets/qwen3.toml b/src/presets/qwen3.toml new file mode 100644 index 0000000..1b32801 --- /dev/null +++ b/src/presets/qwen3.toml @@ -0,0 +1,33 @@ +# Cinto preset: Qwen3 / Qwen3.5 local +# OpenAI-compatible chat template with thinking disabled in-prompt. + +[model] +endpoint = "http://127.0.0.1:1234" +model = "qwen/qwen3.5-9b" +format = "openai-tools" +max_tokens = 2048 +temperature = 0.1 +thinking_effort = "none" +no_think = true +no_think_prefix = "/no_think" +stream = true +stop = [] +request_timeout_secs = 600 +context_window = 32768 + +[harness] +workspace = "." +allow_shell = false +require_edit_approval = true +reasoning_protocol = "crp" +crp_retry_budget = 2 +transport_retry_budget = 1 +empty_response_retry_budget = 1 +max_tool_turns = 16 +max_model_rounds = 32 +auto_context_compression = true +context_compression_threshold = 70 +context_compression_keep_recent = 8 +load_workspace_instructions = true +system_prompt = "You are Cinto, a local coding agent. You help the user understand and modify the current workspace." +developer_prompt = "Be concise. Prefer small verifiable edits. Ask before destructive actions." diff --git a/src/presets/vllm.toml b/src/presets/vllm.toml new file mode 100644 index 0000000..3af9cf1 --- /dev/null +++ b/src/presets/vllm.toml @@ -0,0 +1,31 @@ +# Cinto preset: vLLM / OpenAI-compatible +# vLLM or any server exposing the OpenAI chat API. + +[model] +endpoint = "http://127.0.0.1:8000" +model = "Qwen/Qwen2.5-Coder-32B-Instruct" +format = "openai-tools" +max_tokens = 4096 +temperature = 0.2 +thinking_effort = "none" +stream = true +stop = [] +request_timeout_secs = 600 +context_window = 65536 + +[harness] +workspace = "." +allow_shell = false +require_edit_approval = true +reasoning_protocol = "crp" +crp_retry_budget = 3 +transport_retry_budget = 1 +empty_response_retry_budget = 1 +max_tool_turns = 24 +max_model_rounds = 64 +auto_context_compression = true +context_compression_threshold = 80 +context_compression_keep_recent = 18 +load_workspace_instructions = true +system_prompt = "You are Cinto, a local coding agent running in a terminal UI. You help the user understand and modify the current workspace." +developer_prompt = "Use concise reasoning, ask before destructive actions, and prefer small verifiable edits. When you need repository context, request tools in the commentary channel. Treat as the end of tool output; do not continue or echo tool output after that marker. If a tool result says it was compacted, use search or narrower reads for missing details. Treat as a concise note about earlier conversation history; rely on recent messages for exact details." diff --git a/src/session.rs b/src/session.rs index 07c9138..baae346 100644 --- a/src/session.rs +++ b/src/session.rs @@ -12,6 +12,7 @@ use tokio::sync::{mpsc::UnboundedSender, oneshot}; use crate::{ adapter::{AssistantOutput, PromptAdapter, build_adapter}, config::Config, + crp, model::ModelClient, }; @@ -63,9 +64,37 @@ pub enum TurnEvent { format: String, model: String, }, + ModelRetryRequested { + attempt: u32, + budget: u32, + reason: String, + }, + CrpRetryRequested { + attempt: u32, + budget: u32, + invalid_slots: Vec, + parse_error: Option, + }, + CrpRetryExhausted { + budget: u32, + invalid_slots: Vec, + }, Message(Message), } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum CrpValidationDecision { + Accept, + RetryQueued, + Exhausted, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ModelRetryDecision { + RetryQueued, + Exhausted, +} + impl Message { pub fn user(content: impl Into) -> Self { Self { @@ -109,6 +138,7 @@ pub struct AgentSession { config: Config, adapter: Box, client: ModelClient, + templates: crp::TemplateSet, history: Vec, todos: Vec, } @@ -116,21 +146,21 @@ pub struct AgentSession { impl AgentSession { pub fn new(config: Config) -> Self { let adapter = build_adapter(&config).unwrap_or_else(|error| { - eprintln!("warning: {error}; falling back to harmony adapter"); - Box::new(crate::adapter::HarmonyAdapter::new( - config.harness.system_prompt.clone(), - format!( - "{}\n\nAGENTS.md instructions were not loaded because adapter initialization failed.", - config.harness.developer_prompt - ), - )) + eprintln!( + "warning: {error}; falling back to harmony transport with configured reasoning protocol" + ); + fallback_adapter(&config) }); let client = ModelClient::new(config.model.clone()); + let templates = crp::TemplateSet::load(Some( + crp::workspace_template_dir(&config.harness.workspace).as_path(), + )); Self { config, adapter, client, + templates, history: Vec::new(), todos: Vec::new(), } @@ -147,16 +177,15 @@ impl AgentSession { pub fn update_config(&mut self, config: Config) { self.adapter = build_adapter(&config).unwrap_or_else(|error| { - eprintln!("warning: {error}; falling back to harmony adapter"); - Box::new(crate::adapter::HarmonyAdapter::new( - config.harness.system_prompt.clone(), - format!( - "{}\n\nAGENTS.md instructions were not loaded because adapter initialization failed.", - config.harness.developer_prompt - ), - )) + eprintln!( + "warning: {error}; falling back to harmony transport with configured reasoning protocol" + ); + fallback_adapter(&config) }); self.client = ModelClient::new(config.model.clone()); + self.templates = crp::TemplateSet::load(Some( + crp::workspace_template_dir(&config.harness.workspace).as_path(), + )); self.config = config; } @@ -172,6 +201,22 @@ impl AgentSession { self.history.len() } + pub fn history(&self) -> &[Message] { + &self.history + } + + pub fn discard_trailing_user_message(&mut self) -> bool { + if self + .history + .last() + .is_some_and(|message| message.role == Role::User) + { + self.history.pop(); + return true; + } + false + } + pub fn tool_details(&self) -> String { self.adapter.tool_details() } @@ -219,9 +264,33 @@ impl AgentSession { event_tx: UnboundedSender, ) -> Result<()> { let max_tool_turns = self.config.harness.max_tool_turns.max(1); + let max_model_rounds = self.config.harness.max_model_rounds.max(1); + let crp_enabled = self + .config + .harness + .reasoning_protocol + .eq_ignore_ascii_case("crp"); + let crp_budget = self.config.harness.crp_retry_budget; + let transport_budget = self.config.harness.transport_retry_budget; + let empty_response_budget = self.config.harness.empty_response_retry_budget; + let mut crp_retries_used: u32 = 0; + let mut empty_retries_used: u32 = 0; + let mut transport_retries_used: u32 = 0; + let mut tool_turns_used: u32 = 0; + let mut model_rounds_used: u32 = 0; + + loop { + if model_rounds_used >= max_model_rounds { + let message = Message::assistant_final(format!( + "I hit the configured model-round budget ({max_model_rounds}) before producing a final answer. Try asking for a narrower step, or raise `harness.max_model_rounds` in settings/config." + )); + self.history.push(message.clone()); + let _ = event_tx.send(TurnEvent::Message(message)); + return Ok(()); + } + model_rounds_used += 1; - for _ in 0..max_tool_turns { - if let Some(event) = self.compact_context_if_needed() { + if let Some(event) = self.compact_context_if_needed().await { let _ = event_tx.send(event); } let payload = self.adapter.render_request(&self.history); @@ -233,15 +302,48 @@ impl AgentSession { loop { tokio::select! { result = &mut completion => { - let completion = result?; + let completion = match result { + Ok(completion) => completion, + Err(error) => { + match self.handle_transport_retry( + &error, + &mut transport_retries_used, + transport_budget, + &event_tx, + ) { + ModelRetryDecision::RetryQueued => break, + ModelRetryDecision::Exhausted => return Err(error), + } + } + }; + let finish_reason = completion.finish_reason.as_deref(); match self.adapter.parse_response(&completion) { AssistantOutput::Final(text) => { if text.trim().is_empty() { - let _ = event_tx.send(TurnEvent::EmptyModelResponse { - format: self.config.model.format.clone(), - model: self.config.model.model.clone(), - }); - return Ok(()); + match self.handle_empty_model_response( + &mut empty_retries_used, + empty_response_budget, + &event_tx, + ) { + ModelRetryDecision::RetryQueued => break, + ModelRetryDecision::Exhausted => { + self.emit_empty_model_response(&event_tx); + return Ok(()); + } + } + } + if crp_enabled { + match self.handle_crp_validation( + &text, + finish_reason, + &mut crp_retries_used, + crp_budget, + &event_tx, + ) { + CrpValidationDecision::Accept => {} + CrpValidationDecision::RetryQueued => break, + CrpValidationDecision::Exhausted => {} + } } let message = Message::assistant_final(text); self.history.push(message.clone()); @@ -249,6 +351,16 @@ impl AgentSession { return Ok(()); } AssistantOutput::ToolCall { recipient, arguments } => { + if tool_turns_used >= max_tool_turns { + let message = Message::assistant_final(format!( + "I hit the configured tool-call budget ({max_tool_turns}) before producing a final answer. Try asking for a narrower step, or raise `harness.max_tool_turns` in settings/config." + )); + self.history.push(message.clone()); + let _ = event_tx.send(TurnEvent::Message(message)); + return Ok(()); + } + tool_turns_used += 1; + let _ = event_tx.send(TurnEvent::DiscardAssistantDraft); let call = Message::assistant_tool_call(recipient.clone(), arguments.clone()); self.history.push(call.clone()); @@ -273,11 +385,30 @@ impl AgentSession { } AssistantOutput::Raw(text) => { if text.trim().is_empty() { - let _ = event_tx.send(TurnEvent::EmptyModelResponse { - format: self.config.model.format.clone(), - model: self.config.model.model.clone(), - }); - return Ok(()); + match self.handle_empty_model_response( + &mut empty_retries_used, + empty_response_budget, + &event_tx, + ) { + ModelRetryDecision::RetryQueued => break, + ModelRetryDecision::Exhausted => { + self.emit_empty_model_response(&event_tx); + return Ok(()); + } + } + } + if crp_enabled { + match self.handle_crp_validation( + &text, + finish_reason, + &mut crp_retries_used, + crp_budget, + &event_tx, + ) { + CrpValidationDecision::Accept => {} + CrpValidationDecision::RetryQueued => break, + CrpValidationDecision::Exhausted => {} + } } let message = Message::assistant_final(text); self.history.push(message.clone()); @@ -292,21 +423,132 @@ impl AgentSession { } } } + } - let message = Message::assistant_final(format!( - "I hit the configured tool-call budget ({max_tool_turns}) before producing a final answer. Try asking for a narrower step, or raise `harness.max_tool_turns` in settings/config." - )); - self.history.push(message.clone()); - let _ = event_tx.send(TurnEvent::Message(message)); - Ok(()) + fn handle_empty_model_response( + &mut self, + retries_used: &mut u32, + budget: u32, + event_tx: &UnboundedSender, + ) -> ModelRetryDecision { + if *retries_used >= budget { + return ModelRetryDecision::Exhausted; + } + + *retries_used += 1; + let attempt = *retries_used; + let crp_enabled = self + .config + .harness + .reasoning_protocol + .eq_ignore_ascii_case("crp"); + self.history + .push(Message::user(empty_response_retry_message(crp_enabled))); + + let _ = event_tx.send(TurnEvent::ModelRetryRequested { + attempt, + budget, + reason: "The model returned an empty response.".to_string(), + }); + ModelRetryDecision::RetryQueued + } + + fn handle_transport_retry( + &self, + error: &anyhow::Error, + retries_used: &mut u32, + budget: u32, + event_tx: &UnboundedSender, + ) -> ModelRetryDecision { + if *retries_used >= budget { + return ModelRetryDecision::Exhausted; + } + + *retries_used += 1; + let attempt = *retries_used; + let detail = compact_inline(&format!("{error:#}"), 300); + let _ = event_tx.send(TurnEvent::ModelRetryRequested { + attempt, + budget, + reason: format!("Model request failed: {detail}. Retrying."), + }); + ModelRetryDecision::RetryQueued + } + + fn emit_empty_model_response(&self, event_tx: &UnboundedSender) { + let _ = event_tx.send(TurnEvent::EmptyModelResponse { + format: self.config.model.format.clone(), + model: self.config.model.model.clone(), + }); + } + + fn handle_crp_validation( + &mut self, + text: &str, + finish_reason: Option<&str>, + retries_used: &mut u32, + budget: u32, + event_tx: &UnboundedSender, + ) -> CrpValidationDecision { + let workspace = self.config.harness.workspace.clone(); + let template = self.templates.resolve( + &self.config.harness.default_template, + &self.config.model.thinking_effort, + ); + let config = template.validation_config(Some(workspace.as_path())); + + let (mut retry_message, invalid_slots, parse_error) = match crp::parse(text) { + Ok(trace) => { + let report = crp::validate(&trace, &config); + if report.is_executable() { + return CrpValidationDecision::Accept; + } + let invalid: Vec = report + .invalid() + .map(|outcome| outcome.slot.clone()) + .collect(); + (crp::build_retry_message(&report), invalid, None) + } + Err(error) => ( + crp::build_parse_retry_message(&error), + Vec::new(), + Some(error.to_string()), + ), + }; + retry_message = add_finish_reason_retry_guidance(retry_message, finish_reason); + + if *retries_used >= budget { + let _ = event_tx.send(TurnEvent::CrpRetryExhausted { + budget, + invalid_slots, + }); + return CrpValidationDecision::Exhausted; + } + + *retries_used += 1; + let attempt = *retries_used; + + let assistant = Message::assistant_final(text.to_string()); + self.history.push(assistant); + self.history.push(Message::user(retry_message)); + + let _ = event_tx.send(TurnEvent::CrpRetryRequested { + attempt, + budget, + invalid_slots, + parse_error, + }); + CrpValidationDecision::RetryQueued } - fn compact_context_if_needed(&mut self) -> Option { + async fn compact_context_if_needed(&mut self) -> Option { if !self.config.harness.auto_context_compression { return None; } - let context_window = self.config.model.context_window.max(1) as usize; + // Use the model client to dynamically determine the available context window size, + // falling back to a configured value if the client cannot report it. + let context_window = self.client.get_available_context_window().await.max(1) as usize; let threshold_percent = self .config .harness @@ -599,6 +841,17 @@ impl AgentSession { } } +fn fallback_adapter(config: &Config) -> Box { + let mut fallback = config.clone(); + fallback.model.format = "harmony".to_string(); + build_adapter(&fallback).unwrap_or_else(|_| { + Box::new(crate::adapter::HarmonyAdapter::new( + config.harness.system_prompt.clone(), + config.harness.developer_prompt.clone(), + )) + }) +} + fn is_protected_workspace_component(name: &std::ffi::OsStr) -> bool { matches!(name.to_str(), Some(".git" | ".cinto")) } @@ -619,6 +872,31 @@ fn parse_json(arguments: &str) -> Result { serde_json::from_str(arguments.trim()).context("tool arguments must be JSON") } +fn empty_response_retry_message(crp_enabled: bool) -> String { + if crp_enabled { + return "\nThe previous model response was empty. Re-emit either one valid tool call or a complete CRP trace with a non-empty slot. Do not return an empty response.\n\n".to_string(); + } + + "The previous model response was empty. Please respond with either one valid tool call or a concise final answer.".to_string() +} + +fn add_finish_reason_retry_guidance(message: String, finish_reason: Option<&str>) -> String { + let Some(reason) = finish_reason.filter(|reason| is_truncated_finish_reason(reason)) else { + return message; + }; + + format!( + "\nThe previous model response stopped with finish_reason `{reason}`, so it was likely truncated by the output token limit. Re-emit a shorter complete CRP trace with only essential slots and a non-empty .\n\n\n{message}" + ) +} + +fn is_truncated_finish_reason(reason: &str) -> bool { + matches!( + reason.trim().to_ascii_lowercase().as_str(), + "length" | "max_tokens" | "model_length" + ) +} + fn is_mutating_tool(recipient: &str) -> bool { let recipient = clean_recipient(recipient); let tool_name = recipient @@ -734,11 +1012,62 @@ fn summarize_message(message: &Message) -> String { .map(clean_recipient) .map(|value| format!(" to {value}")) .unwrap_or_default(); - let content = compact_inline(&message.content, 240); + // For assistant final messages, try to extract structured CRP slot summaries + // instead of blindly truncating the entire trace. + if message.role == Role::Assistant && message.channel == Some(Channel::Final) { + if let Ok(trace) = crp::parse(&message.content) { + return summarize_crp_trace(&trace, role, channel, &recipient); + } + } + + let content = compact_inline(&message.content, 1000); format!("{role}{channel}{recipient}: {content}") } +fn summarize_crp_trace(trace: &crp::Trace, role: &str, channel: &str, recipient: &str) -> String { + let mut parts = Vec::new(); + + if let Some(task) = trace.get("TASK_INTERPRETATION") { + parts.push(format!("task: {}", compact_inline(&task.content, 300))); + } + if let Some(work) = trace.get("WORK_DONE") { + parts.push(format!("work: {}", compact_inline(&work.content, 500))); + } + if let Some(response) = trace.get("FINAL_RESPONSE") { + parts.push(format!( + "response: {}", + compact_inline(&response.content, 800) + )); + } + + let other_slots: Vec<&str> = trace + .slots + .iter() + .filter(|s| s.name != "TASK_INTERPRETATION" && s.name != "FINAL_RESPONSE") + .map(|s| s.name.as_str()) + .collect(); + if !other_slots.is_empty() { + parts.push(format!("also emitted: {}", other_slots.join(", "))); + } + + if parts.is_empty() { + return format!( + "{role}{channel}{recipient}: {}", + compact_inline( + &trace + .slots + .first() + .map(|s| s.content.as_str()) + .unwrap_or("(empty CRP trace)"), + 240 + ) + ); + } + + format!("{role}{channel}{recipient}: {}", parts.join(" | ")) +} + fn compact_inline(value: &str, max_chars: usize) -> String { let one_line = value.split_whitespace().collect::>().join(" "); if one_line.chars().count() <= max_chars { @@ -877,6 +1206,21 @@ fn clean_recipient(recipient: &str) -> String { mod tests { use super::*; + #[test] + fn discard_trailing_user_message_only_removes_unanswered_user() { + let mut session = AgentSession::new(Config::default()); + session.history.push(Message::user("cancel me")); + + assert!(session.discard_trailing_user_message()); + assert!(session.history().is_empty()); + + session + .history + .push(Message::assistant_final("keep assistant")); + assert!(!session.discard_trailing_user_message()); + assert_eq!(session.history().len(), 1); + } + #[test] fn write_file_creates_nested_workspace_file() { let workspace = @@ -1040,8 +1384,8 @@ mod tests { let _ = fs::remove_dir_all(&workspace); } - #[test] - fn auto_context_compression_keeps_recent_history() { + #[tokio::test] + async fn auto_context_compression_keeps_recent_history() { let mut config = Config::default(); config.model.context_window = 80; config.harness.context_compression_threshold = 10; @@ -1061,6 +1405,7 @@ mod tests { let event = session .compact_context_if_needed() + .await .expect("context should compact"); assert!(matches!(event, TurnEvent::ContextCompacted { .. })); @@ -1083,8 +1428,8 @@ mod tests { ); } - #[test] - fn auto_context_compression_does_not_start_tail_with_tool_result() { + #[tokio::test] + async fn auto_context_compression_does_not_start_tail_with_tool_result() { let mut config = Config::default(); config.model.context_window = 80; config.harness.context_compression_threshold = 10; @@ -1107,8 +1452,285 @@ mod tests { session .compact_context_if_needed() + .await .expect("context should compact"); assert_ne!(session.history[1].role, Role::Tool); } + + #[test] + fn handle_crp_validation_no_op_for_valid_trace() { + let mut config = Config::default(); + config.harness.workspace = std::env::current_dir().unwrap(); + let mut session = AgentSession::new(config); + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); + let mut retries = 0u32; + + let trace = "\ +test + +- Cargo.toml + + +- do it + + +@@ Cargo.toml prepend +fn hello() {} + +All good."; + + let decision = session.handle_crp_validation(trace, None, &mut retries, 3, &tx); + + assert_eq!(decision, CrpValidationDecision::Accept); + assert_eq!(retries, 0); + assert!(rx.try_recv().is_err()); + assert!(session.history.is_empty()); + } + + #[test] + fn handle_crp_validation_accepts_final_response_only_trace() { + let mut config = Config::default(); + config.harness.workspace = std::env::current_dir().unwrap(); + let mut session = AgentSession::new(config); + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); + let mut retries = 0u32; + + let decision = session.handle_crp_validation( + "Done without extra structure.", + None, + &mut retries, + 3, + &tx, + ); + + assert_eq!(decision, CrpValidationDecision::Accept); + assert_eq!(retries, 0); + assert!(rx.try_recv().is_err()); + assert!(session.history.is_empty()); + } + + #[test] + fn handle_crp_validation_pushes_retry_when_final_response_missing() { + let mut config = Config::default(); + config.harness.workspace = std::env::current_dir().unwrap(); + let mut session = AgentSession::new(config); + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); + let mut retries = 0u32; + + let decision = session.handle_crp_validation( + "x", + None, + &mut retries, + 3, + &tx, + ); + + assert_eq!(decision, CrpValidationDecision::RetryQueued); + assert_eq!(retries, 1); + assert_eq!(session.history.len(), 2); + assert_eq!(session.history[0].role, Role::Assistant); + assert_eq!(session.history[1].role, Role::User); + assert!(session.history[1].content.contains("")); + assert!(session.history[1].content.contains("FINAL_RESPONSE")); + + match rx.try_recv().expect("event") { + TurnEvent::CrpRetryRequested { + attempt, + budget, + invalid_slots, + parse_error, + } => { + assert_eq!(attempt, 1); + assert_eq!(budget, 3); + assert!(invalid_slots.contains(&"FINAL_RESPONSE".to_string())); + assert!(parse_error.is_none()); + } + other => panic!("unexpected event: {other:?}"), + } + } + + #[test] + fn handle_crp_validation_emits_exhausted_when_budget_consumed() { + let mut config = Config::default(); + config.harness.workspace = std::env::current_dir().unwrap(); + let mut session = AgentSession::new(config); + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); + let mut retries = 3u32; + + let decision = session.handle_crp_validation( + "x", + None, + &mut retries, + 3, + &tx, + ); + + assert_eq!(decision, CrpValidationDecision::Exhausted); + assert_eq!(retries, 3); + assert!(session.history.is_empty()); + match rx.try_recv().expect("event") { + TurnEvent::CrpRetryExhausted { + budget, + invalid_slots, + } => { + assert_eq!(budget, 3); + assert!(invalid_slots.contains(&"FINAL_RESPONSE".to_string())); + } + other => panic!("unexpected event: {other:?}"), + } + } + + #[test] + fn handle_crp_validation_retries_on_parse_failure() { + let mut config = Config::default(); + config.harness.workspace = std::env::current_dir().unwrap(); + let mut session = AgentSession::new(config); + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); + let mut retries = 0u32; + + let decision = session.handle_crp_validation("hello", None, &mut retries, 3, &tx); + + assert_eq!(decision, CrpValidationDecision::RetryQueued); + assert_eq!(retries, 1); + match rx.try_recv().expect("event") { + TurnEvent::CrpRetryRequested { parse_error, .. } => { + assert!(parse_error.is_some()); + } + other => panic!("unexpected event: {other:?}"), + } + } + + #[test] + fn handle_crp_validation_adds_truncation_guidance() { + let mut config = Config::default(); + config.harness.workspace = std::env::current_dir().unwrap(); + let mut session = AgentSession::new(config); + let (tx, _rx) = tokio::sync::mpsc::unbounded_channel(); + let mut retries = 0u32; + + let decision = session.handle_crp_validation("hello", Some("length"), &mut retries, 3, &tx); + + assert_eq!(decision, CrpValidationDecision::RetryQueued); + assert_eq!(session.history.len(), 2); + let retry = &session.history[1].content; + assert!(retry.contains("finish_reason `length`")); + assert!(retry.contains("shorter complete CRP trace")); + assert!(retry.contains("FINAL_RESPONSE")); + } + + #[test] + fn handle_empty_model_response_queues_one_retry() { + let mut session = AgentSession::new(Config::default()); + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); + let mut retries = 0u32; + + let decision = session.handle_empty_model_response(&mut retries, 1, &tx); + + assert_eq!(decision, ModelRetryDecision::RetryQueued); + assert_eq!(retries, 1); + assert_eq!(session.history.len(), 1); + assert_eq!(session.history[0].role, Role::User); + assert!(session.history[0].content.contains("")); + match rx.try_recv().expect("event") { + TurnEvent::ModelRetryRequested { + attempt, + budget, + reason, + } => { + assert_eq!(attempt, 1); + assert_eq!(budget, 1); + assert!(reason.contains("empty response")); + } + other => panic!("unexpected event: {other:?}"), + } + } + + #[test] + fn handle_empty_model_response_exhausts_after_budget() { + let mut session = AgentSession::new(Config::default()); + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); + let mut retries = 1u32; + + let decision = session.handle_empty_model_response(&mut retries, 1, &tx); + + assert_eq!(decision, ModelRetryDecision::Exhausted); + assert_eq!(retries, 1); + assert!(session.history.is_empty()); + assert!(rx.try_recv().is_err()); + } + + #[test] + fn handle_transport_retry_uses_separate_budget_without_history_mutation() { + let session = AgentSession::new(Config::default()); + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); + let mut retries = 0u32; + let error = anyhow!("connection closed"); + + let decision = session.handle_transport_retry(&error, &mut retries, 1, &tx); + + assert_eq!(decision, ModelRetryDecision::RetryQueued); + assert_eq!(retries, 1); + assert!(session.history.is_empty()); + match rx.try_recv().expect("event") { + TurnEvent::ModelRetryRequested { + attempt, + budget, + reason, + } => { + assert_eq!(attempt, 1); + assert_eq!(budget, 1); + assert!(reason.contains("connection closed")); + } + other => panic!("unexpected event: {other:?}"), + } + } + + #[test] + fn summarize_message_extracts_crp_slots() { + let msg = Message { + role: Role::Assistant, + channel: Some(Channel::Final), + content: "Fix the bug in parser\n\n- Step one\n\nDone, the parser now handles edge cases.".to_string(), + recipient: None, + }; + + let summary = summarize_message(&msg); + + assert!(summary.starts_with("assistant final:")); + assert!(summary.contains("task: Fix the bug in parser")); + assert!(summary.contains("response: Done, the parser now handles edge cases.")); + assert!(summary.contains("also emitted: PROPOSED_APPROACH")); + // Should NOT contain the full PROPOSED_APPROACH content + assert!(!summary.contains("Step one")); + } + + #[test] + fn summarize_message_falls_back_for_non_crp() { + let msg = Message { + role: Role::Assistant, + channel: Some(Channel::Final), + content: "Just a plain text answer without CRP slots.".to_string(), + recipient: None, + }; + + let summary = summarize_message(&msg); + + assert!(summary.starts_with("assistant final:")); + assert!(summary.contains("Just a plain text answer")); + } + + #[test] + fn summarize_message_user_messages_unchanged() { + let msg = Message { + role: Role::User, + channel: None, + content: "Please fix the bug.".to_string(), + recipient: None, + }; + + let summary = summarize_message(&msg); + + assert_eq!(summary, "user: Please fix the bug."); + } } diff --git a/src/ui.rs b/src/ui.rs index 84c9f2f..609a0cc 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -4,7 +4,7 @@ use std::{ time::{Duration, Instant}, }; -use anyhow::{Context, Result}; +use anyhow::{Context, Result, anyhow}; use crossterm::{ event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}, execute, @@ -21,6 +21,8 @@ use tokio::{ use crate::{ config::Config, + kernel::worker::{WorkerEvent, WorkerLoop}, + model::ModelClient, session::{AgentSession, Channel, Role, TurnEvent}, theme::{StatusKind, Theme}, workspace, @@ -36,9 +38,11 @@ mod transcript; use self::{ layout::app_areas, - settings::{SETTINGS, SettingField, next_format, next_thinking_effort}, - setup::SetupPreset, - transcript::TranscriptItem, + settings::{ + SETTINGS, SettingField, next_format, next_reasoning_protocol, next_thinking_effort, + }, + setup::preset_index_from_endpoint, + transcript::{TranscriptItem, sanitize_stream_body}, }; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -49,6 +53,8 @@ enum View { } type TurnTask = JoinHandle<(AgentSession, Result<()>)>; +type KernelTask = JoinHandle>; +const TURN_INTERRUPTED: &str = "turn interrupted"; struct PendingToolApproval { recipient: String, @@ -61,8 +67,10 @@ pub(crate) enum StreamPhase { Idle, WarmingUp, Thinking, + CrpSlot(String), CallingTool(String), Responding, + KernelStage(String), } pub struct App { @@ -80,13 +88,17 @@ pub struct App { setting_editor: Option, setup_selected: usize, setup_editor: Option, - setup_preset: SetupPreset, + setup_preset: usize, sidebar_visible: bool, header_expanded: bool, chat_scroll: u16, follow_tail: bool, spinner_tick: u64, + kernel_mode: bool, + kernel_task: Option, + kernel_rx: Option>, send_task: Option, + cancel_tx: Option>, stream_rx: Option>, stream_item_index: Option, pending_tool_approval: Option, @@ -110,7 +122,7 @@ impl App { let history_len = session.history_len(); let todo_details = session.todo_details(); let todo_status_line = session.todo_status_line(); - let setup_preset = SetupPreset::from_endpoint(&config.model.endpoint); + let setup_preset = preset_index_from_endpoint(&config.model.endpoint); Self { session: Some(session), @@ -136,7 +148,11 @@ impl App { chat_scroll: 0, follow_tail: true, spinner_tick: 0, + kernel_mode: false, + kernel_task: None, + kernel_rx: None, send_task: None, + cancel_tx: None, stream_rx: None, stream_item_index: None, pending_tool_approval: None, @@ -210,6 +226,10 @@ impl App { } if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') { + if self.is_busy() { + self.interrupt_turn(); + continue; + } return Ok(()); } if self.handle_approval_key(key.code) { @@ -223,6 +243,10 @@ impl App { self.header_expanded = !self.header_expanded; continue; } + if key.code == KeyCode::F(5) { + self.toggle_kernel_mode(); + continue; + } match self.view { View::Chat => { @@ -252,8 +276,9 @@ impl App { } KeyCode::Up => self.scroll_up(1), KeyCode::Down => self.scroll_down(1), + KeyCode::Esc if self.is_busy() => self.interrupt_turn(), KeyCode::Char(_) | KeyCode::Backspace | KeyCode::Enter if self.is_busy() => { - self.status = "waiting for current turn".to_string(); + self.status = "waiting for current turn; Esc interrupts".to_string(); self.status_kind = StatusKind::Working; } KeyCode::Char(ch) => { @@ -284,6 +309,10 @@ impl App { match input.as_str() { "/quit" | "/exit" => return Ok(true), + "/kernel" => { + self.toggle_kernel_mode(); + return Ok(false); + } "/settings" => { self.view = View::Settings; self.clear_path_suggestions(); @@ -311,7 +340,7 @@ impl App { "/prompt" => { if let Some(session) = &self.session { self.transcript.push(TranscriptItem::system( - "Harmony Prompt", + "Model Prompt", session.render_prompt(), )); self.follow_tail = true; @@ -355,6 +384,10 @@ impl App { self.show_checkpoints(); return Ok(false); } + "/context" => { + self.detect_context_window().await?; + return Ok(false); + } _ => {} } @@ -446,11 +479,16 @@ impl App { } fn start_async_turn(&mut self, input: String) -> Result<()> { + if self.kernel_mode { + return self.start_kernel_turn(input); + } + let mut session = self .session .take() .context("session is unavailable while a turn is already running")?; let (event_tx, event_rx) = unbounded_channel(); + let (cancel_tx, cancel_rx) = oneshot::channel(); self.transcript.push(TranscriptItem::user(input.clone())); self.follow_tail = true; @@ -462,17 +500,174 @@ impl App { self.turn_first_token_at = None; self.turn_token_chars = 0; self.stream_rx = Some(event_rx); + self.cancel_tx = Some(cancel_tx); self.stream_item_index = None; self.send_task = Some(tokio::spawn(async move { - let result = session.send_user_message_streaming(input, event_tx).await; + let result = tokio::select! { + result = session.send_user_message_streaming(input, event_tx) => result, + _ = cancel_rx => { + session.discard_trailing_user_message(); + Err(anyhow!(TURN_INTERRUPTED)) + } + }; (session, result) })); Ok(()) } + fn start_kernel_turn(&mut self, input: String) -> Result<()> { + if self.kernel_task.is_some() { + self.status = "kernel turn already running".to_string(); + self.status_kind = StatusKind::Warn; + return Ok(()); + } + + let (event_tx, event_rx) = unbounded_channel::(); + let workspace = self.config.harness.workspace.clone(); + let config = self.config.clone(); + + self.transcript.push(TranscriptItem::user(input.clone())); + self.follow_tail = true; + self.status = "kernel: starting".to_string(); + self.status_kind = StatusKind::Working; + self.busy_since = Some(Instant::now()); + self.last_turn_elapsed = None; + self.stream_phase = StreamPhase::WarmingUp; + self.kernel_rx = Some(event_rx); + + self.kernel_task = Some(tokio::spawn(async move { + WorkerLoop::new(&workspace, config, input, event_tx) + .run_bugfix() + .await + })); + + Ok(()) + } + + fn apply_kernel_event(&mut self, event: WorkerEvent) { + match event { + WorkerEvent::StageStarted { stage } => { + self.stream_phase = StreamPhase::KernelStage(stage.clone()); + self.status = format!("kernel: {stage}"); + self.status_kind = StatusKind::Working; + } + WorkerEvent::ContextPackReady { stage, chars_used, budget } => { + self.status = format!("kernel: {stage} [{chars_used}/{budget} chars]"); + self.status_kind = StatusKind::Working; + } + WorkerEvent::StageCompleted { stage, crp_valid } => { + let marker = if crp_valid { "✓" } else { "~ (plain text)" }; + self.status = format!("kernel: {stage} {marker}"); + self.status_kind = StatusKind::Working; + } + WorkerEvent::StageRetry { stage, attempt, reason } => { + self.transcript.push(TranscriptItem::system( + format!("Kernel retry — {stage} (attempt {attempt})"), + reason, + )); + } + WorkerEvent::StageFailed { stage, error } => { + self.transcript.push(TranscriptItem::error( + format!("Kernel stage {stage} failed: {error}"), + )); + } + WorkerEvent::WorkflowComplete { final_response } => { + let mut item = TranscriptItem::assistant_stream(); + item.body = final_response; + self.transcript.push(item); + self.follow_tail = true; + } + WorkerEvent::WorkflowFailed { error } => { + self.transcript.push(TranscriptItem::error( + format!("Kernel workflow failed: {error}"), + )); + } + WorkerEvent::PatchApprovalRequested { path, preview, response_tx } => { + self.pending_tool_approval = Some(PendingToolApproval { + recipient: path.clone(), + summary: format!("write {path}"), + response_tx, + }); + self.transcript.push(TranscriptItem::system( + "Patch Approval", + format!("{preview}\nPress y to apply or n to skip."), + )); + self.status = "patch approval needed".to_string(); + self.status_kind = StatusKind::Warn; + self.follow_tail = true; + } + WorkerEvent::PatchApplied { files_changed } => { + self.transcript.push(TranscriptItem::system( + "Patch Applied", + files_changed.join("\n"), + )); + self.follow_tail = true; + } + // Trace events carry raw prompt/response pairs for dataset generation. + // The TUI has no use for them — only the batch runner consumes them. + WorkerEvent::StageSkipped { stage, reason } => { + self.transcript.push(TranscriptItem::system( + format!("Kernel skipped — {stage}"), + format!("{reason}\nContinuing with search-based context."), + )); + } + WorkerEvent::StageTrace { .. } => {} + } + } + + fn toggle_kernel_mode(&mut self) { + if self.is_busy() { + self.status = "cannot switch mode while a turn is running".to_string(); + self.status_kind = StatusKind::Warn; + return; + } + self.kernel_mode = !self.kernel_mode; + let mode = if self.kernel_mode { "kernel" } else { "core" }; + self.append_system( + "Mode", + format!("Switched to {mode} mode. {}", if self.kernel_mode { + "Tasks run as a staged pipeline. F5 or /kernel to switch back." + } else { + "Tasks run as a conversational agent. F5 or /kernel to switch back." + }), + ); + self.status = format!("mode: {mode}"); + self.status_kind = StatusKind::Idle; + } + async fn finish_completed_turn(&mut self) -> Result<()> { + // ── Kernel task ────────────────────────────────────────────────────── + if let Some(task) = &self.kernel_task { + if task.is_finished() { + let task = self.kernel_task.take().expect("checked"); + match task.await { + Ok(Ok(_)) => { + self.status = "kernel: done".to_string(); + self.status_kind = StatusKind::Ok; + } + Ok(Err(e)) => { + self.transcript.push(TranscriptItem::error(format!("{e:#}"))); + self.status = "kernel: failed".to_string(); + self.status_kind = StatusKind::Error; + } + Err(e) => { + self.transcript.push(TranscriptItem::error(format!("kernel task: {e}"))); + self.status = "kernel: task error".to_string(); + self.status_kind = StatusKind::Error; + } + } + self.last_turn_elapsed = self.busy_since.map(|s| s.elapsed()); + self.last_reply_at = Some(Instant::now()); + self.busy_since = None; + self.stream_phase = StreamPhase::Idle; + self.kernel_rx = None; + self.follow_tail = true; + } + } + + // ── Core task ──────────────────────────────────────────────────────── let Some(task) = &self.send_task else { return Ok(()); }; @@ -489,6 +684,14 @@ impl App { self.status = "ready".to_string(); self.status_kind = StatusKind::Ok; } + Err(error) if error.to_string() == TURN_INTERRUPTED => { + self.transcript.push(TranscriptItem::system( + "Interrupted", + "Stopped the in-flight model turn.", + )); + self.status = "interrupted".to_string(); + self.status_kind = StatusKind::Warn; + } Err(error) => { self.transcript .push(TranscriptItem::error(format!("{error:#}"))); @@ -510,6 +713,7 @@ impl App { self.last_turn_elapsed = self.busy_since.map(|started| started.elapsed()); self.last_reply_at = Some(Instant::now()); self.busy_since = None; + self.cancel_tx = None; self.stream_phase = StreamPhase::Idle; self.turn_first_token_at = None; self.turn_token_chars = 0; @@ -521,15 +725,21 @@ impl App { } fn drain_stream_events(&mut self) { - let Some(mut rx) = self.stream_rx.take() else { - return; - }; - - while let Ok(event) = rx.try_recv() { - self.apply_turn_event(event); + // Core events + if let Some(mut rx) = self.stream_rx.take() { + while let Ok(event) = rx.try_recv() { + self.apply_turn_event(event); + } + self.stream_rx = Some(rx); } - self.stream_rx = Some(rx); + // Kernel events + if let Some(mut rx) = self.kernel_rx.take() { + while let Ok(event) = rx.try_recv() { + self.apply_kernel_event(event); + } + self.kernel_rx = Some(rx); + } } fn apply_turn_event(&mut self, event: TurnEvent) { @@ -606,6 +816,73 @@ impl App { self.status_kind = StatusKind::Warn; self.follow_tail = true; } + TurnEvent::ModelRetryRequested { + attempt, + budget, + reason, + } => { + if let Some(index) = self.stream_item_index.take() + && index < self.transcript.len() + { + self.transcript.remove(index); + } + self.transcript.push(TranscriptItem::system( + format!("Model Retry {attempt}/{budget}"), + reason, + )); + self.status = "model retry".to_string(); + self.status_kind = StatusKind::Warn; + self.follow_tail = true; + } + TurnEvent::CrpRetryRequested { + attempt, + budget, + invalid_slots, + parse_error, + } => { + if let Some(index) = self.stream_item_index.take() + && index < self.transcript.len() + { + self.transcript.remove(index); + } + let detail = if let Some(error) = parse_error { + format!("CRP parse failed: {error}. Asking the model to re-emit a valid trace.") + } else if invalid_slots.is_empty() { + "CRP validation failed. Asking the model to retry.".to_string() + } else { + format!( + "Invalid slots: {}. Asking the model to retry.", + invalid_slots.join(", ") + ) + }; + self.transcript.push(TranscriptItem::system( + format!("CRP Retry {attempt}/{budget}"), + detail, + )); + self.status = "crp retry".to_string(); + self.status_kind = StatusKind::Warn; + self.follow_tail = true; + } + TurnEvent::CrpRetryExhausted { + budget, + invalid_slots, + } => { + let detail = if invalid_slots.is_empty() { + format!( + "CRP retry budget ({budget}) exhausted. Surfacing the best-effort response with warnings." + ) + } else { + format!( + "CRP retry budget ({budget}) exhausted with invalid slots: {}. Surfacing the best-effort response.", + invalid_slots.join(", ") + ) + }; + self.transcript + .push(TranscriptItem::system("CRP Retry Exhausted", detail)); + self.status = "crp retry exhausted".to_string(); + self.status_kind = StatusKind::Warn; + self.follow_tail = true; + } TurnEvent::Message(message) => { if message.role == Role::Assistant && message.channel == Some(Channel::Final) @@ -905,6 +1182,90 @@ impl App { self.follow_tail = true; } + async fn detect_context_window(&mut self) -> Result<()> { + if self.is_busy() { + self.status = "wait for current turn before detecting context".to_string(); + self.status_kind = StatusKind::Warn; + return Ok(()); + } + + let old_window = self.config.model.context_window; + self.transcript.push(TranscriptItem::system( + "Context Detection", + "Checking the model server for the served context window...", + )); + self.status = "detecting context".to_string(); + self.status_kind = StatusKind::Working; + self.follow_tail = true; + + let client = ModelClient::new(self.config.model.clone()); + match tokio::time::timeout(Duration::from_secs(15), client.detect_context_window()).await { + Ok(Ok(Some(detected_window))) if detected_window > 0 => { + let mut config = self.config.clone(); + config.model.context_window = detected_window; + self.apply_config(config); + let path = self.config.save(self.config_path.clone())?; + self.config_path = Some(path.clone()); + + let detail = if detected_window == old_window { + format!("Confirmed {detected_window} tokens.") + } else { + format!( + "Updated context window from {old_window} to {detected_window} tokens.\nSaved config to {}.", + path.display() + ) + }; + self.transcript + .push(TranscriptItem::system("Context Detection", detail)); + self.status = "context updated".to_string(); + self.status_kind = StatusKind::Ok; + } + Ok(Ok(_)) => { + self.transcript.push(TranscriptItem::system( + "Context Detection", + "The server did not expose a context window for this model.", + )); + self.status = "context unavailable".to_string(); + self.status_kind = StatusKind::Warn; + } + Ok(Err(error)) => { + self.transcript.push(TranscriptItem::error(format!( + "Context detection failed: {error:#}" + ))); + self.status = "context detection failed".to_string(); + self.status_kind = StatusKind::Error; + } + Err(_) => { + self.transcript.push(TranscriptItem::system( + "Context Detection", + "Timed out while checking the model server.", + )); + self.status = "context detection timed out".to_string(); + self.status_kind = StatusKind::Warn; + } + } + + self.follow_tail = true; + Ok(()) + } + + fn interrupt_turn(&mut self) { + let Some(cancel_tx) = self.cancel_tx.take() else { + self.status = "interrupt already requested".to_string(); + self.status_kind = StatusKind::Warn; + return; + }; + + let _ = cancel_tx.send(()); + self.transcript.push(TranscriptItem::system( + "Interrupt Requested", + "Stopping the in-flight model turn...", + )); + self.status = "interrupting".to_string(); + self.status_kind = StatusKind::Warn; + self.follow_tail = true; + } + fn refresh_session_stats(&mut self) { if let Some(session) = &self.session { self.config = session.config().clone(); @@ -942,6 +1303,7 @@ impl App { | SettingField::AutoContextCompression | SettingField::WorkspaceInstructions | SettingField::ThinkingEffort + | SettingField::ReasoningProtocol | SettingField::Format ) { return self.toggle_setting(); @@ -980,6 +1342,10 @@ impl App { SettingField::ThinkingEffort => { config.model.thinking_effort = next_thinking_effort(&config.model.thinking_effort); } + SettingField::ReasoningProtocol => { + config.harness.reasoning_protocol = + next_reasoning_protocol(&config.harness.reasoning_protocol); + } SettingField::Format => { config.model.format = next_format(&config.model.format); } @@ -1041,7 +1407,7 @@ impl App { } fn is_busy(&self) -> bool { - self.send_task.is_some() + self.send_task.is_some() || self.kernel_task.is_some() } } @@ -1070,12 +1436,19 @@ fn current_path_prefix(input: &str) -> Option<((usize, usize), String)> { } fn derive_stream_phase(body: &str) -> StreamPhase { + let visible = visible_stream_body(body); + if visible.trim_start().starts_with('<') + && let Some(slot) = crate::crp::active_slot(&visible) + { + return StreamPhase::CrpSlot(slot); + } + const MARKER: &str = "<|channel|>"; let Some(idx) = body.rfind(MARKER) else { return if body.is_empty() { StreamPhase::WarmingUp } else { - StreamPhase::Thinking + StreamPhase::Responding }; }; let after = &body[idx + MARKER.len()..]; @@ -1106,6 +1479,15 @@ fn derive_stream_phase(body: &str) -> StreamPhase { } } +fn visible_stream_body(body: &str) -> String { + let sanitized = sanitize_stream_body(body); + if sanitized.is_empty() && !body.contains("<|") { + body.to_string() + } else { + sanitized + } +} + fn is_path_like(token: &str) -> bool { token.len() >= 2 && !token.starts_with('/') @@ -1115,3 +1497,32 @@ fn is_path_like(token: &str) -> bool { .chars() .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_'))) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn derives_crp_slot_from_plain_stream() { + assert_eq!( + derive_stream_phase("\n- Add CRP UI."), + StreamPhase::CrpSlot("PROPOSED_APPROACH".to_string()) + ); + } + + #[test] + fn derives_crp_slot_from_harmony_final_stream() { + assert_eq!( + derive_stream_phase("<|channel|>final<|message|>\n@@ src/main.rs prepend"), + StreamPhase::CrpSlot("FILE_EDITS".to_string()) + ); + } + + #[test] + fn ignores_uppercase_markup_when_stream_is_not_crp() { + assert_eq!( + derive_stream_phase("Use as placeholder."), + StreamPhase::Responding + ); + } +} diff --git a/src/ui/commands.rs b/src/ui/commands.rs index 62ecf61..422419b 100644 --- a/src/ui/commands.rs +++ b/src/ui/commands.rs @@ -1,8 +1,9 @@ -pub(super) const COMMAND_TIPS: [(&str, &str); 14] = [ +pub(super) const COMMAND_TIPS: [(&str, &str); 15] = [ ("/settings", "open API settings"), ("/prompt", "show Harmony prompt"), ("/tools", "list agent tools"), ("/todos", "show current todo list"), + ("/context", "detect model context window"), ("/git", "show git changes"), ("/changes", "show git changes"), ("/stage", "stage paths"), diff --git a/src/ui/render.rs b/src/ui/render.rs index de2d717..dc310be 100644 --- a/src/ui/render.rs +++ b/src/ui/render.rs @@ -256,7 +256,7 @@ impl App { let (title, mut content, style) = match self.view { View::Chat if self.is_busy() => ( " Input ", - vec![Line::raw("model is working; scroll remains available")], + vec![Line::raw("model is working; Esc or Ctrl-C interrupts")], self.theme.dim_style(), ), View::Chat => ( @@ -366,6 +366,9 @@ impl App { status_spans.push(Span::styled("Keys ", Style::default().fg(self.theme.muted))); let keys = match self.view { View::Setup => "Enter edit/apply Space toggle Esc skip Ctrl-C quit", + View::Chat | View::Settings if self.is_busy() => { + "Esc/Ctrl-C interrupt PgUp/PgDn scroll" + } View::Chat | View::Settings => { "F2 settings F3 sidebar F4 header PgUp/PgDn scroll Ctrl-C quit" } @@ -405,6 +408,11 @@ impl App { "thinking".to_string(), self.theme.phase_style(PhaseGlyph::Thinking), ), + StreamPhase::CrpSlot(slot) => ( + PhaseGlyph::Responding, + format!("CRP · {}", crp_slot_label(slot)), + self.theme.phase_style(PhaseGlyph::Responding), + ), StreamPhase::CallingTool(name) => { let action = match name.as_str() { "read_file" => "reading file", @@ -431,6 +439,11 @@ impl App { "thinking".to_string(), self.theme.phase_style(PhaseGlyph::Thinking), ), + StreamPhase::KernelStage(stage) => ( + PhaseGlyph::Tool, + format!("kernel · {stage}"), + self.theme.phase_style(PhaseGlyph::Tool), + ), } }; @@ -564,11 +577,18 @@ impl App { "messages: {} todos: {}", self.history_len, self.todo_status_line )), + Line::raw(format!( + "format: {} reasoning: {}", + self.config.model.format, self.config.harness.reasoning_protocol + )), Line::raw(format!( "ctx: {} / {}", self.estimated_tokens, self.config.model.context_window )), - Line::raw(format!("tools: {}", self.config.harness.max_tool_turns)), + Line::raw(format!( + "tools: {} rounds: {}", + self.config.harness.max_tool_turns, self.config.harness.max_model_rounds + )), Line::raw(format!("edits: {edit_mode}")), ]; @@ -988,9 +1008,11 @@ fn stream_header_line( first_token_at: Option, ) -> Line<'static> { let channel = match phase { + StreamPhase::CrpSlot(_) => "crp", StreamPhase::Responding => "final", StreamPhase::CallingTool(_) => "commentary", StreamPhase::Idle | StreamPhase::WarmingUp | StreamPhase::Thinking => "analysis", + StreamPhase::KernelStage(_) => "commentary", }; let tokens = token_chars / 4; let speed = first_token_at @@ -1006,6 +1028,24 @@ fn stream_header_line( ]) } +fn crp_slot_label(slot: &str) -> &'static str { + match slot { + "TASK_INTERPRETATION" => "task interpretation", + "ASSUMPTIONS" => "assumptions", + "RELEVANT_FILES" => "relevant files", + "PROPOSED_APPROACH" => "proposed approach", + "RISKS" => "risks", + "DELIVERABLE_SPEC" => "deliverable spec", + "FILE_EDITS" => "file edits", + "COMMAND_PROPOSALS" => "command proposals", + "CHECKPOINTS" => "checkpoints", + "CLARIFICATION_REQUEST" => "clarification request", + "FINAL_RESPONSE" => "final response", + "SKILLS_USED" => "skills used", + _ => "custom slot", + } +} + fn tool_panel_lines(theme: &Theme, title: &str, body: &str, width: u16) -> Vec> { let panel_width = (width as usize).max(18); let inner_width = panel_width.saturating_sub(4).max(10); diff --git a/src/ui/settings.rs b/src/ui/settings.rs index 649d0c5..831426b 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -9,17 +9,23 @@ pub(super) enum SettingField { Endpoint, Model, Format, + ReasoningProtocol, ApiKeyEnv, MaxTokens, Temperature, ThinkingEffort, Stream, Timeout, + ContextWindow, AutoContextCompression, ContextCompressionThreshold, ContextCompressionKeepRecent, WorkspaceInstructions, ToolTurns, + ModelRounds, + CrpRetries, + TransportRetries, + EmptyResponseRetries, Stop, Workspace, EditApproval, @@ -28,21 +34,27 @@ pub(super) enum SettingField { DeveloperPrompt, } -pub(super) const SETTINGS: [SettingField; 20] = [ +pub(super) const SETTINGS: [SettingField; 26] = [ SettingField::Endpoint, SettingField::Model, SettingField::Format, + SettingField::ReasoningProtocol, SettingField::ApiKeyEnv, SettingField::MaxTokens, SettingField::Temperature, SettingField::ThinkingEffort, SettingField::Stream, SettingField::Timeout, + SettingField::ContextWindow, SettingField::AutoContextCompression, SettingField::ContextCompressionThreshold, SettingField::ContextCompressionKeepRecent, SettingField::WorkspaceInstructions, SettingField::ToolTurns, + SettingField::ModelRounds, + SettingField::CrpRetries, + SettingField::TransportRetries, + SettingField::EmptyResponseRetries, SettingField::Stop, SettingField::Workspace, SettingField::EditApproval, @@ -53,6 +65,7 @@ pub(super) const SETTINGS: [SettingField; 20] = [ const THINKING_EFFORTS: [&str; 4] = ["none", "low", "medium", "high"]; const FORMATS: [&str; 2] = ["harmony", "openai-tools"]; +const REASONING_PROTOCOLS: [&str; 2] = ["crp", "plain"]; impl SettingField { pub(super) fn label(self) -> &'static str { @@ -60,17 +73,23 @@ impl SettingField { SettingField::Endpoint => "endpoint", SettingField::Model => "model", SettingField::Format => "format", + SettingField::ReasoningProtocol => "reasoning", SettingField::ApiKeyEnv => "api key env", SettingField::MaxTokens => "max tokens", SettingField::Temperature => "temperature", SettingField::ThinkingEffort => "thinking", SettingField::Stream => "stream", SettingField::Timeout => "timeout secs", + SettingField::ContextWindow => "context window", SettingField::AutoContextCompression => "auto context compression", SettingField::ContextCompressionThreshold => "context compression %", SettingField::ContextCompressionKeepRecent => "context keep recent", SettingField::WorkspaceInstructions => "workspace instructions", SettingField::ToolTurns => "tool turns", + SettingField::ModelRounds => "model rounds", + SettingField::CrpRetries => "crp retries", + SettingField::TransportRetries => "transport retries", + SettingField::EmptyResponseRetries => "empty response retries", SettingField::Stop => "stop", SettingField::Workspace => "workspace", SettingField::EditApproval => "edit approval", @@ -85,12 +104,14 @@ impl SettingField { SettingField::Endpoint => config.model.endpoint.clone(), SettingField::Model => config.model.model.clone(), SettingField::Format => config.model.format.clone(), + SettingField::ReasoningProtocol => config.harness.reasoning_protocol.clone(), SettingField::ApiKeyEnv => config.model.api_key_env.clone().unwrap_or_default(), SettingField::MaxTokens => config.model.max_tokens.to_string(), SettingField::Temperature => format!("{:.2}", config.model.temperature), SettingField::ThinkingEffort => config.model.thinking_effort.clone(), SettingField::Stream => config.model.stream.to_string(), SettingField::Timeout => config.model.request_timeout_secs.to_string(), + SettingField::ContextWindow => config.model.context_window.to_string(), SettingField::AutoContextCompression => { config.harness.auto_context_compression.to_string() } @@ -104,6 +125,12 @@ impl SettingField { config.harness.load_workspace_instructions.to_string() } SettingField::ToolTurns => config.harness.max_tool_turns.to_string(), + SettingField::ModelRounds => config.harness.max_model_rounds.to_string(), + SettingField::CrpRetries => config.harness.crp_retry_budget.to_string(), + SettingField::TransportRetries => config.harness.transport_retry_budget.to_string(), + SettingField::EmptyResponseRetries => { + config.harness.empty_response_retry_budget.to_string() + } SettingField::Stop => config.model.stop.join(","), SettingField::Workspace => config.harness.workspace.display().to_string(), SettingField::EditApproval => config.harness.require_edit_approval.to_string(), @@ -124,6 +151,13 @@ impl SettingField { } config.model.format = value; } + SettingField::ReasoningProtocol => { + let value = value.trim().to_ascii_lowercase(); + if !REASONING_PROTOCOLS.contains(&value.as_str()) { + return Err(anyhow!("reasoning must be one of crp, plain")); + } + config.harness.reasoning_protocol = value; + } SettingField::ApiKeyEnv => { let value = value.trim().to_string(); config.model.api_key_env = if value.is_empty() { None } else { Some(value) }; @@ -153,6 +187,9 @@ impl SettingField { SettingField::Timeout => { config.model.request_timeout_secs = parse_number(&value, "timeout secs")?; } + SettingField::ContextWindow => { + config.model.context_window = parse_number(&value, "context window")?; + } SettingField::AutoContextCompression => { config.harness.auto_context_compression = value .trim() @@ -179,6 +216,19 @@ impl SettingField { SettingField::ToolTurns => { config.harness.max_tool_turns = parse_number(&value, "tool turns")?; } + SettingField::ModelRounds => { + config.harness.max_model_rounds = parse_number(&value, "model rounds")?; + } + SettingField::CrpRetries => { + config.harness.crp_retry_budget = parse_number(&value, "crp retries")?; + } + SettingField::TransportRetries => { + config.harness.transport_retry_budget = parse_number(&value, "transport retries")?; + } + SettingField::EmptyResponseRetries => { + config.harness.empty_response_retry_budget = + parse_number(&value, "empty response retries")?; + } SettingField::Stop => { config.model.stop = value .split(',') @@ -259,6 +309,15 @@ pub(super) fn next_format(current: &str) -> String { FORMATS[(index + 1) % FORMATS.len()].to_string() } +pub(super) fn next_reasoning_protocol(current: &str) -> String { + let current = current.trim().to_ascii_lowercase(); + let index = REASONING_PROTOCOLS + .iter() + .position(|protocol| *protocol == current) + .unwrap_or(0); + REASONING_PROTOCOLS[(index + 1) % REASONING_PROTOCOLS.len()].to_string() +} + fn non_empty(value: String, label: &str) -> Result { let value = value.trim().to_string(); if value.is_empty() { diff --git a/src/ui/setup.rs b/src/ui/setup.rs index e0dbb30..1f78bdf 100644 --- a/src/ui/setup.rs +++ b/src/ui/setup.rs @@ -9,49 +9,53 @@ use ratatui::{ widgets::{Block, BorderType, Borders, Clear, Paragraph, Wrap}, }; -use super::{App, StatusKind, View, settings::next_format}; +use crate::{ + config::{self, PRESETS}, + model::ModelClient, +}; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(super) enum SetupPreset { - LmStudio, - Ollama, - Custom, -} +use super::{ + App, StatusKind, View, + settings::{next_format, next_reasoning_protocol}, +}; -impl SetupPreset { - pub(super) fn from_endpoint(endpoint: &str) -> Self { - if endpoint.contains("127.0.0.1:1234") || endpoint.contains("localhost:1234") { - Self::LmStudio - } else if endpoint.contains("127.0.0.1:11434") || endpoint.contains("localhost:11434") { - Self::Ollama - } else { - Self::Custom +/// Index into `config::PRESETS`, or `PRESETS.len()` for "Custom". +pub(super) fn preset_index_from_endpoint(endpoint: &str) -> usize { + for (index, preset) in PRESETS.iter().enumerate() { + if let Ok(config) = preset.to_config() { + if endpoint.contains(&strip_scheme(&config.model.endpoint)) { + return index; + } } } + PRESETS.len() // Custom +} - fn label(self) -> &'static str { - match self { - Self::LmStudio => "LM Studio", - Self::Ollama => "Ollama", - Self::Custom => "Custom", - } - } +fn strip_scheme(url: &str) -> String { + url.trim_start_matches("http://") + .trim_start_matches("https://") + .to_string() +} - fn next(self) -> Self { - match self { - Self::LmStudio => Self::Ollama, - Self::Ollama => Self::Custom, - Self::Custom => Self::LmStudio, - } +fn preset_label(index: usize) -> String { + if index < PRESETS.len() { + format!("{} — {}", PRESETS[index].label, PRESETS[index].description) + } else { + "Custom".to_string() } } +fn next_preset(index: usize) -> usize { + (index + 1) % (PRESETS.len() + 1) +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum SetupField { Preset, Endpoint, Model, Format, + ReasoningProtocol, Workspace, EditApproval, Shell, @@ -59,11 +63,12 @@ enum SetupField { SaveStart, } -const SETUP_FIELDS: [SetupField; 9] = [ +const SETUP_FIELDS: [SetupField; 10] = [ SetupField::Preset, SetupField::Endpoint, SetupField::Model, SetupField::Format, + SetupField::ReasoningProtocol, SetupField::Workspace, SetupField::EditApproval, SetupField::Shell, @@ -79,9 +84,18 @@ impl App { match code { KeyCode::Esc | KeyCode::Tab | KeyCode::F(2) => { + match self.config.save(self.config_path.clone()) { + Ok(path) => { + self.config_path = Some(path); + self.status = "setup saved".to_string(); + self.status_kind = StatusKind::Ok; + } + Err(e) => { + self.status = format!("setup: could not save — {e}"); + self.status_kind = StatusKind::Error; + } + } self.view = View::Chat; - self.status = "setup skipped".to_string(); - self.status_kind = StatusKind::Warn; } KeyCode::Up => { self.setup_selected = self @@ -134,7 +148,7 @@ impl App { fn activate_setup_field(&mut self) -> Result<()> { match SETUP_FIELDS[self.setup_selected] { SetupField::Preset => { - self.setup_preset = self.setup_preset.next(); + self.setup_preset = next_preset(self.setup_preset); self.apply_setup_preset(); } SetupField::Endpoint | SetupField::Model | SetupField::Workspace => { @@ -145,6 +159,12 @@ impl App { config.model.format = next_format(&config.model.format); self.apply_config(config); } + SetupField::ReasoningProtocol => { + let mut config = self.config.clone(); + config.harness.reasoning_protocol = + next_reasoning_protocol(&config.harness.reasoning_protocol); + self.apply_config(config); + } SetupField::EditApproval => { let mut config = self.config.clone(); config.harness.require_edit_approval = !config.harness.require_edit_approval; @@ -174,7 +194,7 @@ impl App { match field { SetupField::Endpoint => { config.model.endpoint = non_empty(value, "endpoint")?; - self.setup_preset = SetupPreset::from_endpoint(&config.model.endpoint); + self.setup_preset = preset_index_from_endpoint(&config.model.endpoint); } SetupField::Model => config.model.model = non_empty(value, "model")?, SetupField::Workspace => { @@ -188,27 +208,70 @@ impl App { } fn apply_setup_preset(&mut self) { - let mut config = self.config.clone(); - match self.setup_preset { - SetupPreset::LmStudio => { - config.model.endpoint = "http://127.0.0.1:1234".to_string(); - config.model.model = "openai/gpt-oss-20b".to_string(); - config.model.format = "harmony".to_string(); - config.model.thinking_effort = "medium".to_string(); - } - SetupPreset::Ollama => { - config.model.endpoint = "http://127.0.0.1:11434".to_string(); - config.model.model = "qwen2.5-coder:7b-instruct".to_string(); - config.model.format = "openai-tools".to_string(); - config.model.thinking_effort = "none".to_string(); - config.model.stop.clear(); - } - SetupPreset::Custom => {} + if self.setup_preset >= PRESETS.len() { + // Custom — don't change anything + return; } + + let preset = &PRESETS[self.setup_preset]; + let Ok(preset_config) = preset.to_config() else { + return; + }; + + let mut config = self.config.clone(); + // Apply model settings from preset, keep workspace and prompts + config.model.endpoint = preset_config.model.endpoint; + config.model.model = preset_config.model.model; + config.model.format = preset_config.model.format; + config.model.max_tokens = preset_config.model.max_tokens; + config.model.temperature = preset_config.model.temperature; + config.model.thinking_effort = preset_config.model.thinking_effort; + config.model.stream = preset_config.model.stream; + config.model.stop = preset_config.model.stop; + config.model.request_timeout_secs = preset_config.model.request_timeout_secs; + config.model.context_window = preset_config.model.context_window; + // Apply harness settings from preset, keep workspace and prompts + config.harness.reasoning_protocol = preset_config.harness.reasoning_protocol; + config.harness.crp_retry_budget = preset_config.harness.crp_retry_budget; + config.harness.transport_retry_budget = preset_config.harness.transport_retry_budget; + config.harness.empty_response_retry_budget = + preset_config.harness.empty_response_retry_budget; + config.harness.max_tool_turns = preset_config.harness.max_tool_turns; + config.harness.max_model_rounds = preset_config.harness.max_model_rounds; + config.harness.auto_context_compression = preset_config.harness.auto_context_compression; + config.harness.require_edit_approval = preset_config.harness.require_edit_approval; + config.harness.allow_shell = preset_config.harness.allow_shell; + self.apply_config(config); } fn finish_setup(&mut self) -> Result<()> { + let old_window = self.config.model.context_window; + let detected_ctx = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + let client = ModelClient::new(self.config.model.clone()); + tokio::time::timeout( + std::time::Duration::from_secs(15), + client.detect_context_window(), + ) + .await + .ok() + .and_then(|result| result.ok()) + .flatten() + }) + }); + + if let Some(detected_window) = detected_ctx + && detected_window > 0 + && detected_window != old_window + { + self.config.model.context_window = detected_window; + self.append_system( + "Context Detection", + format!("Updated context window from {old_window} to {detected_window} tokens."), + ); + } + let path = self .config .save(self.config_path.clone()) @@ -229,10 +292,11 @@ impl App { fn setup_value(&self, field: SetupField) -> String { match field { - SetupField::Preset => self.setup_preset.label().to_string(), + SetupField::Preset => preset_label(self.setup_preset), SetupField::Endpoint => self.config.model.endpoint.clone(), SetupField::Model => self.config.model.model.clone(), SetupField::Format => self.config.model.format.clone(), + SetupField::ReasoningProtocol => self.config.harness.reasoning_protocol.clone(), SetupField::Workspace => self.config.harness.workspace.display().to_string(), SetupField::EditApproval => bool_label(self.config.harness.require_edit_approval), SetupField::Shell => bool_label(self.config.harness.allow_shell), @@ -287,7 +351,7 @@ impl App { lines.push(Line::raw("")); lines.push(Line::from(vec![ Span::styled("First run setup", self.theme.brand()), - Span::styled(" choose sane defaults, then chat", self.theme.dim_style()), + Span::styled(" choose a preset, then chat", self.theme.dim_style()), ])); if inner.height >= 13 { @@ -304,6 +368,21 @@ impl App { Span::styled("model ", self.theme.dim_style()), Span::raw(self.config.model.model.clone()), ])); + + if inner.height >= 16 { + lines.push(Line::raw("")); + lines.push(Line::from(vec![ + Span::styled("Presets: ", self.theme.dim_style()), + Span::raw( + config::PRESETS + .iter() + .map(|p| p.label) + .collect::>() + .join(", "), + ), + Span::styled(", Custom", self.theme.dim_style()), + ])); + } } let hero = Paragraph::new(lines) @@ -399,6 +478,7 @@ impl SetupField { Self::Endpoint => "endpoint", Self::Model => "model", Self::Format => "format", + Self::ReasoningProtocol => "reasoning", Self::Workspace => "workspace", Self::EditApproval => "edit approval", Self::Shell => "shell tools", diff --git a/src/ui/transcript.rs b/src/ui/transcript.rs index fc0f964..959da2e 100644 --- a/src/ui/transcript.rs +++ b/src/ui/transcript.rs @@ -5,6 +5,7 @@ use ratatui::{ use serde_json::Value; use crate::{ + crp, session::{Channel, Message, Role}, theme::{RoleKind, StatusKind, Theme}, }; @@ -66,11 +67,17 @@ impl TranscriptItem { pub(super) fn from_message(message: &Message) -> Self { match (message.role, message.channel, &message.recipient) { - (Role::Assistant, Some(Channel::Final), _) => Self { - role: TranscriptRole::Assistant, - title: "Assistant".to_string(), - body: message.content.clone(), - }, + (Role::Assistant, Some(Channel::Final), _) => { + if let Some(item) = Self::from_crp_content(&message.content) { + item + } else { + Self { + role: TranscriptRole::Assistant, + title: "Assistant".to_string(), + body: message.content.clone(), + } + } + } (Role::Assistant, Some(Channel::Commentary), Some(recipient)) => { let name = clean_recipient(recipient); let short = strip_functions_prefix(&name); @@ -99,6 +106,25 @@ impl TranscriptItem { }, } } + + fn from_crp_content(content: &str) -> Option { + match crp::parse(content) { + Ok(trace) => Some(Self { + role: TranscriptRole::Assistant, + title: format!("CRP Trace · {} slots", trace.slots.len()), + body: format_crp_trace(&trace), + }), + Err(error) if looks_like_crp(content) => Some(Self { + role: TranscriptRole::Error, + title: "CRP Parse Error".to_string(), + body: format!( + "{error}\n\nraw preview:\n{}", + truncate_tool_body(content.trim(), ToolPreviewKind::Result) + ), + }), + Err(_) => None, + } + } } impl TranscriptRole { @@ -225,6 +251,114 @@ fn strip_functions_prefix(name: &str) -> &str { name.strip_prefix("functions.").unwrap_or(name) } +fn looks_like_crp(content: &str) -> bool { + let trimmed = content.trim_start(); + let Some(after_open) = trimmed.strip_prefix('<') else { + return false; + }; + after_open.starts_with('/') + || after_open + .chars() + .next() + .is_some_and(|character| character.is_ascii_uppercase()) +} + +fn format_crp_trace(trace: &crp::Trace) -> String { + let mut lines = vec![format!( + "CRP {} · {} slots", + crp::VERSION, + trace.slots.len() + )]; + + for slot in &trace.slots { + lines.push(String::new()); + lines.push(format_crp_slot_heading(slot)); + + let content = truncate_tool_body(&slot.content, ToolPreviewKind::Result); + if content.trim().is_empty() { + lines.push("(empty)".to_string()); + } else { + lines.extend(content.lines().map(ToString::to_string)); + } + } + + lines.join("\n") +} + +fn format_crp_slot_heading(slot: &crp::Slot) -> String { + let mut details = vec![crp_slot_summary(slot)]; + if !slot.attributes.is_empty() { + details.push(format_crp_attributes(&slot.attributes)); + } + + format!("**{}** · {}", slot.name, details.join(" · ")) +} + +fn crp_slot_summary(slot: &crp::Slot) -> String { + match slot.name.as_str() { + "TASK_INTERPRETATION" => "task understanding".to_string(), + "ASSUMPTIONS" => pluralize(count_list_items(&slot.content), "assumption"), + "RELEVANT_FILES" => pluralize(count_list_items(&slot.content), "file"), + "PROPOSED_APPROACH" => pluralize(count_list_items(&slot.content), "step"), + "RISKS" => pluralize(count_list_items(&slot.content), "risk"), + "DELIVERABLE_SPEC" => "success criteria".to_string(), + "FILE_EDITS" => pluralize(count_edit_blocks(&slot.content), "edit block"), + "COMMAND_PROPOSALS" => pluralize(count_list_items(&slot.content), "command"), + "CHECKPOINTS" => pluralize(count_list_items(&slot.content), "checkpoint"), + "CLARIFICATION_REQUEST" => "needs user input".to_string(), + "FINAL_RESPONSE" => "user-facing response".to_string(), + "SKILLS_USED" => pluralize(count_list_items(&slot.content), "skill"), + _ => format!("{} lines", slot.content.lines().count()), + } +} + +fn format_crp_attributes(attributes: &[crp::Attribute]) -> String { + attributes + .iter() + .map(|attribute| format!("{}=\"{}\"", attribute.name, attribute.value)) + .collect::>() + .join(" ") +} + +fn count_list_items(content: &str) -> usize { + let count = content + .lines() + .filter(|line| { + let trimmed = line.trim_start(); + trimmed.starts_with("- ") || trimmed.starts_with("* ") + }) + .count(); + + if count == 0 && !content.trim().is_empty() { + 1 + } else { + count + } +} + +fn count_edit_blocks(content: &str) -> usize { + let edit_tags = content.matches(" String { + if count == 1 { + format!("1 {label}") + } else { + format!("{count} {label}s") + } +} + fn format_tool_call_body(tool: &str, raw_args: &str) -> String { let parsed: Option = serde_json::from_str(raw_args.trim()).ok(); match (tool, parsed.as_ref()) { @@ -475,3 +609,43 @@ fn clip_preview_line(line: &str) -> String { let preview = line.chars().take(keep).collect::(); format!("{preview} ... [line clipped]") } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn formats_valid_crp_final_response_as_trace() { + let item = TranscriptItem::from_message(&Message::assistant_final( + r#" +Parse CRP and show it in the UI. + + + +@@ src/main.rs prepend +fn hello() {} + + + +Done. +"#, + )); + + assert_eq!(item.role, TranscriptRole::Assistant); + assert_eq!(item.title, "CRP Trace · 3 slots"); + assert!(item.body.contains("CRP 1.0-draft · 3 slots")); + assert!(item.body.contains("**FILE_EDITS** · 1 edit block")); + } + + #[test] + fn formats_malformed_crp_final_response_as_parse_error() { + let item = TranscriptItem::from_message(&Message::assistant_final( + "\nMissing close", + )); + + assert_eq!(item.role, TranscriptRole::Error); + assert_eq!(item.title, "CRP Parse Error"); + assert!(item.body.contains("missing CRP closing tag")); + assert!(item.body.contains("raw preview:")); + } +} diff --git a/src/workspace.rs b/src/workspace.rs index 42eb214..0a110e7 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -221,6 +221,13 @@ pub fn path_suggestions(workspace: &Path, prefix: &str, limit: usize) -> Result< Ok(suggestions) } +pub fn clean_workspace(workspace: &Path) -> Result { + ensure_git_workspace(workspace)?; + git_output(workspace, &["reset", "--hard", "HEAD"])?; + git_output(workspace, &["clean", "-fd"])?; + Ok("Workspace restored to a clean state.".to_string()) +} + fn ensure_git_workspace(workspace: &Path) -> Result<()> { let output = Command::new("git") .current_dir(workspace) diff --git a/task.md b/task.md new file mode 100644 index 0000000..f461740 --- /dev/null +++ b/task.md @@ -0,0 +1,30 @@ +# Task: Relax CRP Without Losing Typed Validation + +## Goal + +Make the CRP loop less brittle for local models while keeping structured, +typed validation where it matters. + +## Current Direction + +- Keep `FINAL_RESPONSE` as the hard-required slot. +- Treat other template `required = true` slots as recommended structure: + missing or empty content should warn, not force retry. +- Keep typed validation strict when typed slots are present, especially + `BulletList` slots such as `RELEVANT_FILES`. +- Keep parse errors as retryable failures. +- Keep retry feedback typed enough for the model and UI to understand what + failed. + +## Work Items + +- [x] Create this task tracker. +- [x] Relax CRP required-slot validation while preserving typed slot checks. +- [x] Update prompt/docs language so models understand the softer contract. +- [x] Add tests covering relaxed structural slots and strict typed slots. +- [x] Split CRP retry decisions into typed outcomes instead of returning `bool`. +- [x] Add one bounded retry for empty model responses. +- [x] Capture model `finish_reason` and use truncation-aware CRP retry guidance. +- [x] Separate budgets for tool calls, model rounds, CRP retries, empty + responses, and transport retries. +- [x] Re-run focused formatting, tests, and package script checks. diff --git a/templates/code_edit.toml b/templates/code_edit.toml new file mode 100644 index 0000000..78fb144 --- /dev/null +++ b/templates/code_edit.toml @@ -0,0 +1,18 @@ +name = "code_edit" +description = "Modify or extend existing files in the workspace." + +[slots.TASK_INTERPRETATION] +required = true + +[slots.RELEVANT_FILES] +required = false +type = "BulletList" + +[slots.WORK_DONE] +required = false + +[slots.FILE_EDITS] +required = true + +[slots.FINAL_RESPONSE] +required = true diff --git a/templates/code_edit_minimal.toml b/templates/code_edit_minimal.toml new file mode 100644 index 0000000..e2a4f38 --- /dev/null +++ b/templates/code_edit_minimal.toml @@ -0,0 +1,11 @@ +name = "code_edit_minimal" +description = "Quick code edit with minimal reasoning overhead." + +[slots.TASK_INTERPRETATION] +required = true + +[slots.FILE_EDITS] +required = false + +[slots.FINAL_RESPONSE] +required = true diff --git a/templates/code_edit_thorough.toml b/templates/code_edit_thorough.toml new file mode 100644 index 0000000..de0ad5f --- /dev/null +++ b/templates/code_edit_thorough.toml @@ -0,0 +1,21 @@ +name = "code_edit_thorough" +description = "Thorough code edit with self-verification and edge case analysis." + +[slots.TASK_INTERPRETATION] +required = true + +[slots.RELEVANT_FILES] +required = false +type = "BulletList" + +[slots.WORK_DONE] +required = false + +[slots.FILE_EDITS] +required = true + +[slots.SELF_VERIFICATION] +required = false + +[slots.FINAL_RESPONSE] +required = true diff --git a/templates/code_explanation.toml b/templates/code_explanation.toml new file mode 100644 index 0000000..af990eb --- /dev/null +++ b/templates/code_explanation.toml @@ -0,0 +1,12 @@ +name = "code_explanation" +description = "Explain or describe existing code without modifying it." + +[slots.TASK_INTERPRETATION] +required = true + +[slots.RELEVANT_FILES] +required = false +type = "BulletList" + +[slots.FINAL_RESPONSE] +required = true diff --git a/templates/design_proposal.toml b/templates/design_proposal.toml new file mode 100644 index 0000000..1efdf6e --- /dev/null +++ b/templates/design_proposal.toml @@ -0,0 +1,17 @@ +name = "design_proposal" +description = "Propose a design or architectural approach without producing edits." + +[slots.TASK_INTERPRETATION] +required = true + +[slots.PROPOSED_APPROACH] +required = true + +[slots.RISKS] +required = false + +[slots.DELIVERABLE_SPEC] +required = true + +[slots.FINAL_RESPONSE] +required = true