deepstrike 0.2.6__tar.gz → 0.2.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {deepstrike-0.2.6 → deepstrike-0.2.7}/Cargo.lock +5 -5
- {deepstrike-0.2.6 → deepstrike-0.2.7}/Cargo.toml +2 -2
- {deepstrike-0.2.6 → deepstrike-0.2.7}/PKG-INFO +1 -1
- deepstrike-0.2.7/crates/deepstrike-core/src/orchestration/loop_until_done.rs +281 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/orchestration/mod.rs +3 -0
- deepstrike-0.2.7/crates/deepstrike-core/src/orchestration/tournament.rs +347 -0
- deepstrike-0.2.7/crates/deepstrike-core/src/orchestration/workflow.rs +399 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/runtime/event_log.rs +4 -1
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/runtime/kernel.rs +156 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/runtime/mod.rs +3 -3
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/runtime/repair.rs +53 -63
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/runtime/session.rs +9 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/scheduler/mod.rs +1 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/scheduler/state_machine.rs +208 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/scheduler/state_machine_tests.rs +187 -0
- deepstrike-0.2.7/crates/deepstrike-core/src/scheduler/workflow_run.rs +375 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-py/src/lib.rs +283 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/__init__.py +14 -2
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/providers/__init__.py +2 -2
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/providers/anthropic.py +46 -2
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/providers/base.py +15 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/providers/deepseek.py +105 -4
- deepstrike-0.2.7/deepstrike/providers/minimax.py +254 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/providers/openai.py +26 -5
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/providers/replay.py +24 -7
- deepstrike-0.2.7/deepstrike/providers/replay_validator.py +75 -0
- deepstrike-0.2.7/deepstrike/runtime/provider_replay.py +79 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/runner.py +114 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/session_log.py +31 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/session_repair.py +35 -42
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/types/agent.py +141 -0
- deepstrike-0.2.7/deepstrike/workflow.py +54 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/pyproject.toml +1 -1
- deepstrike-0.2.6/deepstrike/providers/minimax.py +0 -33
- deepstrike-0.2.6/deepstrike/runtime/provider_replay.py +0 -41
- {deepstrike-0.2.6 → deepstrike-0.2.7}/README.md +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/Cargo.toml +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/compression.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/config.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/dashboard.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/llm_summarizer.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/manager.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/mod.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/partitions.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/pressure.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/renderer.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/renewal.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/sections.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/skill_catalog.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/snapshot.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/summarizer.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/task_state.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/text.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/context/token_engine.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/governance/audit.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/governance/constraint.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/governance/mod.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/governance/permission.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/governance/pipeline.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/governance/quota.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/governance/rate_limit.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/governance/sandbox.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/governance/tool_decision.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/governance/veto.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/harness/eval_pipeline.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/harness/mod.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/lib.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/memory/curator.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/memory/durable.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/memory/extractor.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/memory/idle_pipeline.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/memory/mod.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/memory/runtime.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/memory/semantic.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/memory/session.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/memory/synthesis.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/memory/trace_analyzer.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/memory/working.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/mm/handle.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/mm/memory.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/mm/mod.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/orchestration/executor.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/orchestration/gen_eval.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/orchestration/planner.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/orchestration/task_graph.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/proc/mod.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/runtime/replay.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/scheduler/milestone.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/scheduler/policy.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/scheduler/rollback.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/scheduler/tcb.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/signals/attention.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/signals/mod.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/signals/queue.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/signals/router.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/syscall/mod.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/types/agent.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/types/capability.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/types/contract.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/types/error.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/types/message.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/types/milestone.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/types/mod.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/types/model.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/types/policy.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/types/result.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/types/signal.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/types/skill.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-core/src/types/task.rs +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/crates/deepstrike-py/Cargo.toml +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/collaboration/__init__.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/collaboration/contract.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/collaboration/handoff.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/collaboration/harness.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/collaboration/modes.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/collaboration/pool.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/governance.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/harness/__init__.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/harness/harness.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/kernel/__init__.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/knowledge/__init__.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/knowledge/source.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/memory/__init__.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/memory/agent.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/memory/protocols.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/memory/working.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/providers/gemini.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/providers/glm.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/providers/kimi.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/providers/ollama.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/providers/qwen.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/providers/stream.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/__init__.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/archive.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/credential_vault.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/execution_plane.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/filtered_plane.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/kernel_event_log.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/kernel_step.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/large_result_spool.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/mcp_proxy_plane.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/os_profile.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/os_snapshot.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/process_sandbox_plane.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/remote_vpc_plane.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/replay_sanitize.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/runtime/sub_agent_orchestrator.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/safety/__init__.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/safety/permissions.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/signals/__init__.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/signals/gateway.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/signals/scheduled.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/signals/types.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/skills/__init__.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/skills/registry.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/skills/watcher.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/tools/__init__.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/tools/builtin/__init__.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/tools/builtin/read_file.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/tools/execution.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/tools/registry.py +0 -0
- {deepstrike-0.2.6 → deepstrike-0.2.7}/deepstrike/types/__init__.py +0 -0
|
@@ -194,7 +194,7 @@ dependencies = [
|
|
|
194
194
|
|
|
195
195
|
[[package]]
|
|
196
196
|
name = "deepstrike-core"
|
|
197
|
-
version = "0.2.
|
|
197
|
+
version = "0.2.7"
|
|
198
198
|
dependencies = [
|
|
199
199
|
"compact_str",
|
|
200
200
|
"pretty_assertions",
|
|
@@ -206,7 +206,7 @@ dependencies = [
|
|
|
206
206
|
|
|
207
207
|
[[package]]
|
|
208
208
|
name = "deepstrike-node"
|
|
209
|
-
version = "0.2.
|
|
209
|
+
version = "0.2.7"
|
|
210
210
|
dependencies = [
|
|
211
211
|
"compact_str",
|
|
212
212
|
"deepstrike-core",
|
|
@@ -218,7 +218,7 @@ dependencies = [
|
|
|
218
218
|
|
|
219
219
|
[[package]]
|
|
220
220
|
name = "deepstrike-py"
|
|
221
|
-
version = "0.2.
|
|
221
|
+
version = "0.2.7"
|
|
222
222
|
dependencies = [
|
|
223
223
|
"compact_str",
|
|
224
224
|
"deepstrike-core",
|
|
@@ -229,7 +229,7 @@ dependencies = [
|
|
|
229
229
|
|
|
230
230
|
[[package]]
|
|
231
231
|
name = "deepstrike-sdk"
|
|
232
|
-
version = "0.2.
|
|
232
|
+
version = "0.2.7"
|
|
233
233
|
dependencies = [
|
|
234
234
|
"async-stream",
|
|
235
235
|
"async-trait",
|
|
@@ -263,7 +263,7 @@ dependencies = [
|
|
|
263
263
|
|
|
264
264
|
[[package]]
|
|
265
265
|
name = "deepstrike-wasm"
|
|
266
|
-
version = "0.2.
|
|
266
|
+
version = "0.2.7"
|
|
267
267
|
dependencies = [
|
|
268
268
|
"compact_str",
|
|
269
269
|
"deepstrike-core",
|
|
@@ -3,13 +3,13 @@ resolver = "2"
|
|
|
3
3
|
members = ["crates/deepstrike-core", "crates/deepstrike-py"]
|
|
4
4
|
|
|
5
5
|
[workspace.package]
|
|
6
|
-
version = "0.2.
|
|
6
|
+
version = "0.2.7"
|
|
7
7
|
edition = "2024"
|
|
8
8
|
license = "MIT"
|
|
9
9
|
repository = "https://github.com/kongusen/deepstrike"
|
|
10
10
|
|
|
11
11
|
[workspace.dependencies]
|
|
12
|
-
deepstrike-core = { path = "crates/deepstrike-core", version = "0.2.
|
|
12
|
+
deepstrike-core = { path = "crates/deepstrike-core", version = "0.2.7" }
|
|
13
13
|
notify = "6"
|
|
14
14
|
serde = { version = "1", features = ["derive"] }
|
|
15
15
|
serde_json = "1"
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
//! Loop-until-done — drive an agent round after round until a stop predicate fires.
|
|
2
|
+
//!
|
|
3
|
+
//! For tasks with an unknown amount of work (keep investigating until no new findings;
|
|
4
|
+
//! keep fixing until no more errors). Mirrors [`super::gen_eval::GenEvalLoop`]: a pure
|
|
5
|
+
//! control state machine emitting **abstract actions**; the SDK spawns a worker per round
|
|
6
|
+
//! and feeds back what that round produced. No I/O, no clock.
|
|
7
|
+
//!
|
|
8
|
+
//! Termination is guaranteed *in-kernel*: a [`StopCondition::MaxRounds`] backstop is always
|
|
9
|
+
//! present (injected with [`DEFAULT_MAX_ROUNDS`] if the caller configured none), so the loop
|
|
10
|
+
//! cannot run forever regardless of SDK behavior. Wall-clock / token backstops are an
|
|
11
|
+
//! orthogonal concern owned by the SDK's [`crate::scheduler::policy::SchedulerBudget`]
|
|
12
|
+
//! wrapped around the loop — this state machine stays zero-clock.
|
|
13
|
+
|
|
14
|
+
/// Default hard round cap injected when the caller configures no [`StopCondition::MaxRounds`].
|
|
15
|
+
pub const DEFAULT_MAX_ROUNDS: u32 = 50;
|
|
16
|
+
|
|
17
|
+
/// A stop predicate. The loop stops as soon as **any** configured condition fires.
|
|
18
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
19
|
+
pub enum StopCondition {
|
|
20
|
+
/// The round produced no new findings (`new_findings == 0`).
|
|
21
|
+
NoNewFindings,
|
|
22
|
+
/// The round reported no errors (`errors == 0`).
|
|
23
|
+
NoErrors,
|
|
24
|
+
/// Hard cap: stop once this many rounds have completed.
|
|
25
|
+
MaxRounds(u32),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// Loop configuration. Always carries a `MaxRounds` backstop after construction.
|
|
29
|
+
#[derive(Debug, Clone)]
|
|
30
|
+
pub struct LoopConfig {
|
|
31
|
+
conditions: Vec<StopCondition>,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
impl LoopConfig {
|
|
35
|
+
/// Build a config. If no [`StopCondition::MaxRounds`] is present, a
|
|
36
|
+
/// [`DEFAULT_MAX_ROUNDS`] backstop is appended so termination is guaranteed.
|
|
37
|
+
pub fn new(conditions: Vec<StopCondition>) -> Self {
|
|
38
|
+
let has_max = conditions
|
|
39
|
+
.iter()
|
|
40
|
+
.any(|c| matches!(c, StopCondition::MaxRounds(_)));
|
|
41
|
+
let mut conditions = conditions;
|
|
42
|
+
if !has_max {
|
|
43
|
+
conditions.push(StopCondition::MaxRounds(DEFAULT_MAX_ROUNDS));
|
|
44
|
+
}
|
|
45
|
+
Self { conditions }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
pub fn conditions(&self) -> &[StopCondition] {
|
|
49
|
+
&self.conditions
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
impl Default for LoopConfig {
|
|
54
|
+
fn default() -> Self {
|
|
55
|
+
Self::new(Vec::new())
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// What the SDK reports after running a round's worker.
|
|
60
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
61
|
+
pub struct RoundReport {
|
|
62
|
+
pub new_findings: u32,
|
|
63
|
+
pub errors: u32,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Why the loop stopped.
|
|
67
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
68
|
+
pub enum StopReason {
|
|
69
|
+
NoNewFindings,
|
|
70
|
+
NoErrors,
|
|
71
|
+
MaxRounds,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// What the SDK should do next.
|
|
75
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
76
|
+
pub enum LoopAction {
|
|
77
|
+
/// Spawn the worker for round `round`, then call [`LoopUntilDone::feed`] with its report.
|
|
78
|
+
Spawn { round: u32 },
|
|
79
|
+
/// The loop converged.
|
|
80
|
+
Done {
|
|
81
|
+
rounds_used: u32,
|
|
82
|
+
reason: StopReason,
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// Loop-until-done control state machine.
|
|
87
|
+
#[derive(Debug)]
|
|
88
|
+
pub struct LoopUntilDone {
|
|
89
|
+
config: LoopConfig,
|
|
90
|
+
/// Round currently in flight (the last one emitted by `start`/`feed`).
|
|
91
|
+
round: u32,
|
|
92
|
+
done: bool,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
impl LoopUntilDone {
|
|
96
|
+
pub fn new(config: LoopConfig) -> Self {
|
|
97
|
+
Self {
|
|
98
|
+
config,
|
|
99
|
+
round: 0,
|
|
100
|
+
done: false,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// Begin the loop — always spawns round 1.
|
|
105
|
+
pub fn start(&mut self) -> LoopAction {
|
|
106
|
+
self.round = 1;
|
|
107
|
+
LoopAction::Spawn { round: 1 }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// Feed the just-completed round's report and get the next action.
|
|
111
|
+
pub fn feed(&mut self, report: RoundReport) -> LoopAction {
|
|
112
|
+
if self.done {
|
|
113
|
+
return LoopAction::Done {
|
|
114
|
+
rounds_used: self.round,
|
|
115
|
+
reason: StopReason::MaxRounds,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if let Some(reason) = self.first_triggered(&report) {
|
|
120
|
+
self.done = true;
|
|
121
|
+
return LoopAction::Done {
|
|
122
|
+
rounds_used: self.round,
|
|
123
|
+
reason,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
self.round += 1;
|
|
128
|
+
LoopAction::Spawn { round: self.round }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// Return the reason for the first stop condition (in configured order) that fires for
|
|
132
|
+
/// the just-completed round (`self.round`).
|
|
133
|
+
fn first_triggered(&self, report: &RoundReport) -> Option<StopReason> {
|
|
134
|
+
for cond in &self.config.conditions {
|
|
135
|
+
let hit = match cond {
|
|
136
|
+
StopCondition::NoNewFindings => report.new_findings == 0,
|
|
137
|
+
StopCondition::NoErrors => report.errors == 0,
|
|
138
|
+
StopCondition::MaxRounds(max) => self.round >= *max,
|
|
139
|
+
};
|
|
140
|
+
if hit {
|
|
141
|
+
return Some(match cond {
|
|
142
|
+
StopCondition::NoNewFindings => StopReason::NoNewFindings,
|
|
143
|
+
StopCondition::NoErrors => StopReason::NoErrors,
|
|
144
|
+
StopCondition::MaxRounds(_) => StopReason::MaxRounds,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
None
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
pub fn is_done(&self) -> bool {
|
|
152
|
+
self.done
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#[cfg(test)]
|
|
157
|
+
mod tests {
|
|
158
|
+
use super::*;
|
|
159
|
+
|
|
160
|
+
fn report(new_findings: u32, errors: u32) -> RoundReport {
|
|
161
|
+
RoundReport {
|
|
162
|
+
new_findings,
|
|
163
|
+
errors,
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#[test]
|
|
168
|
+
fn default_injects_max_rounds_backstop() {
|
|
169
|
+
let cfg = LoopConfig::default();
|
|
170
|
+
assert!(
|
|
171
|
+
cfg.conditions()
|
|
172
|
+
.iter()
|
|
173
|
+
.any(|c| matches!(c, StopCondition::MaxRounds(DEFAULT_MAX_ROUNDS)))
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#[test]
|
|
178
|
+
fn explicit_max_rounds_not_duplicated() {
|
|
179
|
+
let cfg = LoopConfig::new(vec![StopCondition::MaxRounds(5)]);
|
|
180
|
+
let maxes: Vec<_> = cfg
|
|
181
|
+
.conditions()
|
|
182
|
+
.iter()
|
|
183
|
+
.filter(|c| matches!(c, StopCondition::MaxRounds(_)))
|
|
184
|
+
.collect();
|
|
185
|
+
assert_eq!(maxes.len(), 1);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
#[test]
|
|
189
|
+
fn start_spawns_round_one() {
|
|
190
|
+
let mut l = LoopUntilDone::new(LoopConfig::default());
|
|
191
|
+
assert_eq!(l.start(), LoopAction::Spawn { round: 1 });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
#[test]
|
|
195
|
+
fn stops_on_no_new_findings() {
|
|
196
|
+
let mut l = LoopUntilDone::new(LoopConfig::new(vec![StopCondition::NoNewFindings]));
|
|
197
|
+
l.start();
|
|
198
|
+
// round 1 found things → continue
|
|
199
|
+
assert_eq!(l.feed(report(3, 0)), LoopAction::Spawn { round: 2 });
|
|
200
|
+
// round 2 found nothing → stop
|
|
201
|
+
assert_eq!(
|
|
202
|
+
l.feed(report(0, 9)),
|
|
203
|
+
LoopAction::Done {
|
|
204
|
+
rounds_used: 2,
|
|
205
|
+
reason: StopReason::NoNewFindings
|
|
206
|
+
}
|
|
207
|
+
);
|
|
208
|
+
assert!(l.is_done());
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
#[test]
|
|
212
|
+
fn stops_on_no_errors() {
|
|
213
|
+
let mut l = LoopUntilDone::new(LoopConfig::new(vec![StopCondition::NoErrors]));
|
|
214
|
+
l.start();
|
|
215
|
+
assert_eq!(l.feed(report(0, 2)), LoopAction::Spawn { round: 2 });
|
|
216
|
+
assert_eq!(
|
|
217
|
+
l.feed(report(0, 0)),
|
|
218
|
+
LoopAction::Done {
|
|
219
|
+
rounds_used: 2,
|
|
220
|
+
reason: StopReason::NoErrors
|
|
221
|
+
}
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
#[test]
|
|
226
|
+
fn max_rounds_caps_the_loop() {
|
|
227
|
+
let mut l = LoopUntilDone::new(LoopConfig::new(vec![StopCondition::MaxRounds(3)]));
|
|
228
|
+
l.start();
|
|
229
|
+
assert_eq!(l.feed(report(1, 1)), LoopAction::Spawn { round: 2 });
|
|
230
|
+
assert_eq!(l.feed(report(1, 1)), LoopAction::Spawn { round: 3 });
|
|
231
|
+
assert_eq!(
|
|
232
|
+
l.feed(report(1, 1)),
|
|
233
|
+
LoopAction::Done {
|
|
234
|
+
rounds_used: 3,
|
|
235
|
+
reason: StopReason::MaxRounds
|
|
236
|
+
}
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
#[test]
|
|
241
|
+
fn default_backstop_terminates_unbounded_predicate() {
|
|
242
|
+
// NoErrors configured but errors never reach 0 → default MaxRounds(50) must fire.
|
|
243
|
+
let mut l = LoopUntilDone::new(LoopConfig::new(vec![StopCondition::NoErrors]));
|
|
244
|
+
let mut action = l.start();
|
|
245
|
+
let mut last = action;
|
|
246
|
+
for _ in 0..100 {
|
|
247
|
+
match action {
|
|
248
|
+
LoopAction::Spawn { .. } => {
|
|
249
|
+
last = action;
|
|
250
|
+
action = l.feed(report(1, 1));
|
|
251
|
+
}
|
|
252
|
+
LoopAction::Done { .. } => break,
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
let _ = last;
|
|
256
|
+
assert_eq!(
|
|
257
|
+
action,
|
|
258
|
+
LoopAction::Done {
|
|
259
|
+
rounds_used: DEFAULT_MAX_ROUNDS,
|
|
260
|
+
reason: StopReason::MaxRounds
|
|
261
|
+
}
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
#[test]
|
|
266
|
+
fn first_configured_condition_wins() {
|
|
267
|
+
// Both NoNewFindings and NoErrors would fire on (0,0); configured order picks the first.
|
|
268
|
+
let mut l = LoopUntilDone::new(LoopConfig::new(vec![
|
|
269
|
+
StopCondition::NoNewFindings,
|
|
270
|
+
StopCondition::NoErrors,
|
|
271
|
+
]));
|
|
272
|
+
l.start();
|
|
273
|
+
assert_eq!(
|
|
274
|
+
l.feed(report(0, 0)),
|
|
275
|
+
LoopAction::Done {
|
|
276
|
+
rounds_used: 1,
|
|
277
|
+
reason: StopReason::NoNewFindings
|
|
278
|
+
}
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
//! Single-elimination tournament — pairwise comparative judging.
|
|
2
|
+
//!
|
|
3
|
+
//! Pattern mirrors [`super::gen_eval::GenEvalLoop`]: a pure control state machine that
|
|
4
|
+
//! holds the bracket and emits **abstract actions**; the SDK runs a fresh-context judge
|
|
5
|
+
//! agent per match and feeds back winners. No prompt assembly, no I/O, no clock.
|
|
6
|
+
//!
|
|
7
|
+
//! Why a tournament instead of absolute scoring: comparative judgment ("which of these
|
|
8
|
+
//! two is better?") is more reliable than asking one agent to score 1000 items, and only
|
|
9
|
+
//! the current round's match-ups ever enter context — the deterministic loop holds the
|
|
10
|
+
//! whole bracket.
|
|
11
|
+
//!
|
|
12
|
+
//! ```text
|
|
13
|
+
//! entrants ──▶ JudgeRound{round 1, matches} ──▶ SDK runs N parallel pairwise judges
|
|
14
|
+
//! ▲ │
|
|
15
|
+
//! └────────── feed_round(winners) ◀──────┘
|
|
16
|
+
//! (repeat until one survivor) ──▶ Done{winner}
|
|
17
|
+
//! ```
|
|
18
|
+
|
|
19
|
+
use crate::types::error::{DeepStrikeError, Result};
|
|
20
|
+
|
|
21
|
+
/// A participant. The SDK maps the id back to the real item being compared.
|
|
22
|
+
pub type EntrantId = String;
|
|
23
|
+
|
|
24
|
+
/// One pairwise match-up in a round.
|
|
25
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
26
|
+
pub struct Match {
|
|
27
|
+
/// Index of this match within its round (0-based).
|
|
28
|
+
pub id: u32,
|
|
29
|
+
pub left: EntrantId,
|
|
30
|
+
pub right: EntrantId,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// What the SDK should do next.
|
|
34
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
35
|
+
pub enum TournamentAction {
|
|
36
|
+
/// Run one fresh-context judge per match this round (matches are independent — run
|
|
37
|
+
/// them in parallel), then call [`Tournament::feed_round`] with the winners.
|
|
38
|
+
JudgeRound { round: u32, matches: Vec<Match> },
|
|
39
|
+
/// The bracket is resolved.
|
|
40
|
+
Done { winner: EntrantId, rounds_used: u32 },
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Single-elimination bracket control state machine.
|
|
44
|
+
#[derive(Debug)]
|
|
45
|
+
pub struct Tournament {
|
|
46
|
+
/// Entrants advancing into the next round to be played.
|
|
47
|
+
survivors: Vec<EntrantId>,
|
|
48
|
+
/// Matches emitted for the current round, awaiting results.
|
|
49
|
+
pending: Vec<Match>,
|
|
50
|
+
/// Entrant that drew a bye this round (odd survivor count) — auto-advances.
|
|
51
|
+
bye: Option<EntrantId>,
|
|
52
|
+
/// Number of the most recently emitted round (0 before `start`).
|
|
53
|
+
round: u32,
|
|
54
|
+
done: bool,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
impl Tournament {
|
|
58
|
+
/// Build a tournament. Requires at least one entrant.
|
|
59
|
+
pub fn new(entrants: Vec<EntrantId>) -> Result<Self> {
|
|
60
|
+
if entrants.is_empty() {
|
|
61
|
+
return Err(DeepStrikeError::InvalidConfig(
|
|
62
|
+
"tournament requires at least one entrant".into(),
|
|
63
|
+
));
|
|
64
|
+
}
|
|
65
|
+
Ok(Self {
|
|
66
|
+
survivors: entrants,
|
|
67
|
+
pending: Vec::new(),
|
|
68
|
+
bye: None,
|
|
69
|
+
round: 0,
|
|
70
|
+
done: false,
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Begin the tournament. A single entrant wins immediately (zero rounds);
|
|
75
|
+
/// otherwise the first round of match-ups is emitted.
|
|
76
|
+
pub fn start(&mut self) -> TournamentAction {
|
|
77
|
+
self.emit_round_or_done()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Report the winners of the round last emitted by [`TournamentAction::JudgeRound`].
|
|
81
|
+
/// `winners` must align one-to-one with that round's `matches`, and each winner must
|
|
82
|
+
/// be one of the two entrants in its match.
|
|
83
|
+
pub fn feed_round(&mut self, winners: Vec<EntrantId>) -> Result<TournamentAction> {
|
|
84
|
+
if self.done {
|
|
85
|
+
return Err(DeepStrikeError::InvalidConfig(
|
|
86
|
+
"tournament already complete".into(),
|
|
87
|
+
));
|
|
88
|
+
}
|
|
89
|
+
if winners.len() != self.pending.len() {
|
|
90
|
+
return Err(DeepStrikeError::InvalidConfig(format!(
|
|
91
|
+
"expected {} winner(s) for round {}, got {}",
|
|
92
|
+
self.pending.len(),
|
|
93
|
+
self.round,
|
|
94
|
+
winners.len()
|
|
95
|
+
)));
|
|
96
|
+
}
|
|
97
|
+
for (w, m) in winners.iter().zip(&self.pending) {
|
|
98
|
+
if w != &m.left && w != &m.right {
|
|
99
|
+
return Err(DeepStrikeError::InvalidConfig(format!(
|
|
100
|
+
"winner '{w}' is not a participant in match {}",
|
|
101
|
+
m.id
|
|
102
|
+
)));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let mut next = winners;
|
|
107
|
+
if let Some(bye) = self.bye.take() {
|
|
108
|
+
next.push(bye);
|
|
109
|
+
}
|
|
110
|
+
self.survivors = next;
|
|
111
|
+
self.pending.clear();
|
|
112
|
+
Ok(self.emit_round_or_done())
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// Emit the next round of match-ups, or finish if only one survivor remains.
|
|
116
|
+
fn emit_round_or_done(&mut self) -> TournamentAction {
|
|
117
|
+
if self.survivors.len() == 1 {
|
|
118
|
+
self.done = true;
|
|
119
|
+
return TournamentAction::Done {
|
|
120
|
+
winner: self.survivors[0].clone(),
|
|
121
|
+
rounds_used: self.round,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
self.round += 1;
|
|
126
|
+
let mut matches = Vec::with_capacity(self.survivors.len() / 2);
|
|
127
|
+
let mut i = 0;
|
|
128
|
+
while i + 1 < self.survivors.len() {
|
|
129
|
+
matches.push(Match {
|
|
130
|
+
id: (i / 2) as u32,
|
|
131
|
+
left: self.survivors[i].clone(),
|
|
132
|
+
right: self.survivors[i + 1].clone(),
|
|
133
|
+
});
|
|
134
|
+
i += 2;
|
|
135
|
+
}
|
|
136
|
+
// Odd count → the trailing entrant draws a bye and advances untouched.
|
|
137
|
+
self.bye = if self.survivors.len() % 2 == 1 {
|
|
138
|
+
self.survivors.last().cloned()
|
|
139
|
+
} else {
|
|
140
|
+
None
|
|
141
|
+
};
|
|
142
|
+
self.pending = matches.clone();
|
|
143
|
+
TournamentAction::JudgeRound {
|
|
144
|
+
round: self.round,
|
|
145
|
+
matches,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
pub fn is_done(&self) -> bool {
|
|
150
|
+
self.done
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
#[cfg(test)]
|
|
155
|
+
mod tests {
|
|
156
|
+
use super::*;
|
|
157
|
+
|
|
158
|
+
fn ids(xs: &[&str]) -> Vec<EntrantId> {
|
|
159
|
+
xs.iter().map(|s| s.to_string()).collect()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#[test]
|
|
163
|
+
fn empty_entrants_is_error() {
|
|
164
|
+
assert!(Tournament::new(vec![]).is_err());
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#[test]
|
|
168
|
+
fn single_entrant_wins_immediately() {
|
|
169
|
+
let mut t = Tournament::new(ids(&["a"])).unwrap();
|
|
170
|
+
match t.start() {
|
|
171
|
+
TournamentAction::Done {
|
|
172
|
+
winner,
|
|
173
|
+
rounds_used,
|
|
174
|
+
} => {
|
|
175
|
+
assert_eq!(winner, "a");
|
|
176
|
+
assert_eq!(rounds_used, 0);
|
|
177
|
+
}
|
|
178
|
+
_ => panic!("expected immediate Done"),
|
|
179
|
+
}
|
|
180
|
+
assert!(t.is_done());
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
#[test]
|
|
184
|
+
fn two_entrants_one_round() {
|
|
185
|
+
let mut t = Tournament::new(ids(&["a", "b"])).unwrap();
|
|
186
|
+
match t.start() {
|
|
187
|
+
TournamentAction::JudgeRound { round, matches } => {
|
|
188
|
+
assert_eq!(round, 1);
|
|
189
|
+
assert_eq!(matches.len(), 1);
|
|
190
|
+
assert_eq!(
|
|
191
|
+
matches[0],
|
|
192
|
+
Match {
|
|
193
|
+
id: 0,
|
|
194
|
+
left: "a".into(),
|
|
195
|
+
right: "b".into()
|
|
196
|
+
}
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
_ => panic!("expected JudgeRound"),
|
|
200
|
+
}
|
|
201
|
+
match t.feed_round(ids(&["b"])).unwrap() {
|
|
202
|
+
TournamentAction::Done {
|
|
203
|
+
winner,
|
|
204
|
+
rounds_used,
|
|
205
|
+
} => {
|
|
206
|
+
assert_eq!(winner, "b");
|
|
207
|
+
assert_eq!(rounds_used, 1);
|
|
208
|
+
}
|
|
209
|
+
_ => panic!("expected Done"),
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
#[test]
|
|
214
|
+
fn four_entrants_two_rounds() {
|
|
215
|
+
let mut t = Tournament::new(ids(&["a", "b", "c", "d"])).unwrap();
|
|
216
|
+
let r1 = t.start();
|
|
217
|
+
match r1 {
|
|
218
|
+
TournamentAction::JudgeRound { round, matches } => {
|
|
219
|
+
assert_eq!(round, 1);
|
|
220
|
+
assert_eq!(matches.len(), 2);
|
|
221
|
+
}
|
|
222
|
+
_ => panic!(),
|
|
223
|
+
}
|
|
224
|
+
// a beats b, d beats c
|
|
225
|
+
let r2 = t.feed_round(ids(&["a", "d"])).unwrap();
|
|
226
|
+
match r2 {
|
|
227
|
+
TournamentAction::JudgeRound { round, matches } => {
|
|
228
|
+
assert_eq!(round, 2);
|
|
229
|
+
assert_eq!(matches.len(), 1);
|
|
230
|
+
assert_eq!(
|
|
231
|
+
matches[0],
|
|
232
|
+
Match {
|
|
233
|
+
id: 0,
|
|
234
|
+
left: "a".into(),
|
|
235
|
+
right: "d".into()
|
|
236
|
+
}
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
_ => panic!(),
|
|
240
|
+
}
|
|
241
|
+
match t.feed_round(ids(&["d"])).unwrap() {
|
|
242
|
+
TournamentAction::Done {
|
|
243
|
+
winner,
|
|
244
|
+
rounds_used,
|
|
245
|
+
} => {
|
|
246
|
+
assert_eq!(winner, "d");
|
|
247
|
+
assert_eq!(rounds_used, 2);
|
|
248
|
+
}
|
|
249
|
+
_ => panic!(),
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
#[test]
|
|
254
|
+
fn three_entrants_bye_advances() {
|
|
255
|
+
let mut t = Tournament::new(ids(&["a", "b", "c"])).unwrap();
|
|
256
|
+
match t.start() {
|
|
257
|
+
TournamentAction::JudgeRound { round, matches } => {
|
|
258
|
+
assert_eq!(round, 1);
|
|
259
|
+
// only (a,b) plays; c gets a bye
|
|
260
|
+
assert_eq!(matches.len(), 1);
|
|
261
|
+
assert_eq!(
|
|
262
|
+
matches[0],
|
|
263
|
+
Match {
|
|
264
|
+
id: 0,
|
|
265
|
+
left: "a".into(),
|
|
266
|
+
right: "b".into()
|
|
267
|
+
}
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
_ => panic!(),
|
|
271
|
+
}
|
|
272
|
+
// a beats b; survivors = [a, c (bye)]
|
|
273
|
+
match t.feed_round(ids(&["a"])).unwrap() {
|
|
274
|
+
TournamentAction::JudgeRound { round, matches } => {
|
|
275
|
+
assert_eq!(round, 2);
|
|
276
|
+
assert_eq!(
|
|
277
|
+
matches[0],
|
|
278
|
+
Match {
|
|
279
|
+
id: 0,
|
|
280
|
+
left: "a".into(),
|
|
281
|
+
right: "c".into()
|
|
282
|
+
}
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
_ => panic!(),
|
|
286
|
+
}
|
|
287
|
+
match t.feed_round(ids(&["c"])).unwrap() {
|
|
288
|
+
TournamentAction::Done {
|
|
289
|
+
winner,
|
|
290
|
+
rounds_used,
|
|
291
|
+
} => {
|
|
292
|
+
assert_eq!(winner, "c");
|
|
293
|
+
assert_eq!(rounds_used, 2);
|
|
294
|
+
}
|
|
295
|
+
_ => panic!(),
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
#[test]
|
|
300
|
+
fn eight_entrants_three_rounds() {
|
|
301
|
+
let mut t = Tournament::new(ids(&["1", "2", "3", "4", "5", "6", "7", "8"])).unwrap();
|
|
302
|
+
let mut action = t.start();
|
|
303
|
+
let mut last_round = 0;
|
|
304
|
+
loop {
|
|
305
|
+
match action {
|
|
306
|
+
TournamentAction::JudgeRound { round, matches } => {
|
|
307
|
+
last_round = round;
|
|
308
|
+
// winners = the left entrant of each match (deterministic)
|
|
309
|
+
let winners: Vec<EntrantId> = matches.iter().map(|m| m.left.clone()).collect();
|
|
310
|
+
action = t.feed_round(winners).unwrap();
|
|
311
|
+
}
|
|
312
|
+
TournamentAction::Done {
|
|
313
|
+
winner,
|
|
314
|
+
rounds_used,
|
|
315
|
+
} => {
|
|
316
|
+
assert_eq!(winner, "1");
|
|
317
|
+
assert_eq!(rounds_used, 3);
|
|
318
|
+
assert_eq!(last_round, 3);
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
#[test]
|
|
326
|
+
fn wrong_winner_count_is_error() {
|
|
327
|
+
let mut t = Tournament::new(ids(&["a", "b", "c", "d"])).unwrap();
|
|
328
|
+
t.start();
|
|
329
|
+
// round 1 has 2 matches; feeding 1 winner is invalid
|
|
330
|
+
assert!(t.feed_round(ids(&["a"])).is_err());
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
#[test]
|
|
334
|
+
fn winner_not_in_match_is_error() {
|
|
335
|
+
let mut t = Tournament::new(ids(&["a", "b"])).unwrap();
|
|
336
|
+
t.start();
|
|
337
|
+
assert!(t.feed_round(ids(&["zzz"])).is_err());
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
#[test]
|
|
341
|
+
fn feed_after_done_is_error() {
|
|
342
|
+
let mut t = Tournament::new(ids(&["a", "b"])).unwrap();
|
|
343
|
+
t.start();
|
|
344
|
+
t.feed_round(ids(&["a"])).unwrap();
|
|
345
|
+
assert!(t.feed_round(ids(&["a"])).is_err());
|
|
346
|
+
}
|
|
347
|
+
}
|