ace-sim 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/docs/handbook.md ADDED
@@ -0,0 +1,24 @@
1
+ ---
2
+ doc-type: user
3
+ title: ace-sim Handbook
4
+ purpose: Package-specific docs and workflow catalog
5
+ ace-docs:
6
+ last-updated: 2026-03-22
7
+ last-checked: 2026-03-22
8
+ ---
9
+
10
+ # ace-sim Handbook
11
+
12
+ ## Skills
13
+
14
+ - [`as-sim-run`](../handbook/skills/as-sim-run/SKILL.md)
15
+
16
+ ## Workflows
17
+
18
+ - [`sim/run`](../handbook/workflow-instructions/sim/run.wf.md)
19
+
20
+ ## Related Docs
21
+
22
+ - [Getting Started](getting-started.md)
23
+ - [Usage Reference](usage.md)
24
+ - Command help: `ace-sim --help`
data/docs/usage.md ADDED
@@ -0,0 +1,127 @@
1
+ ---
2
+ doc-type: reference
3
+ title: ace-sim Usage Reference
4
+ purpose: Complete CLI reference for simulation runs and command behavior
5
+ ace-docs:
6
+ last-updated: 2026-03-22
7
+ last-checked: 2026-03-22
8
+ ---
9
+
10
+ # ace-sim Usage Reference
11
+
12
+ `ace-sim` runs provider simulations through configurable steps and presets.
13
+
14
+ ## Command Overview
15
+
16
+ - `ace-sim` — entrypoint
17
+ - `ace-sim run` — run a simulation preset with one or more source files
18
+ - `ace-sim --help` — print command list and examples
19
+
20
+ ## `ace-sim --help`
21
+
22
+ Shows command examples and the `run` subcommand.
23
+
24
+ ## `ace-sim run`
25
+
26
+ ### Syntax
27
+
28
+ ```bash
29
+ ace-sim run [OPTIONS]
30
+ ```
31
+
32
+ ### Purpose
33
+
34
+ Execute a preset-driven simulation with source files, provider list, and optional final synthesis.
35
+
36
+ ### Options
37
+
38
+ | Option | Type | Default / Source | Description |
39
+ |---|---|---|---|
40
+ | `--preset` | string | config `sim.default_preset` or `validate-idea` | Preset name from `.ace/sim/presets/*.yml|yaml` |
41
+ | `--source` | array | preset `source` | One or more source files (repeatable, supports globs) |
42
+ | `--steps` | string | preset steps | Comma-separated step names (override preset step list) |
43
+ | `--provider` | array | preset providers | Provider:model values (`--provider` may repeat) |
44
+ | `--repeat` | integer | sim default repeat (or `1`) | Run each provider this many times |
45
+ | `--synthesis-workflow` | string | preset / config | Workflow/file ref for final synthesis |
46
+ | `--synthesis-provider` | string | preset / config provider | Provider for final suggestions generation |
47
+ | `--dry-run` | flag | preset / false | Prepare and preview without mutating providers |
48
+ | `--writeback` | flag | preset / false | Write final revised source back to source when set |
49
+ | `--quiet`, `-q` | flag | false | Suppress non-essential status output |
50
+ | `--verbose`, `-v` | flag | false | Print extended diagnostics |
51
+ | `--help`, `-h` | flag | false | Show command help |
52
+
53
+ ## Preset configuration model
54
+
55
+ - Presets are resolved by name.
56
+ - Preset loading precedence for files is:
57
+ - gem defaults (`.ace-defaults/sim/presets`)
58
+ - user presets (`~/.ace/sim/presets`)
59
+ - project presets (`.ace/sim/presets`)
60
+
61
+ - File extensions: `.yml` and `.yaml`.
62
+
63
+ If a preset is missing but known, fallback behavior is an empty preset with default steps and the system-level defaults for provider/repeat behavior.
64
+
65
+ ## Step config resolution
66
+
67
+ Each requested step is resolved in this order:
68
+
69
+ 1. `.ace/sim/steps/<step>.md`
70
+ 2. `~/.ace/sim/steps/<step>.md`
71
+ 3. `.ace-defaults/sim/steps/<step>.md`
72
+
73
+ Run fails with `Missing step config` if a required step file is not found.
74
+
75
+ ## Synthesis and precedence
76
+
77
+ - If `--synthesis-provider` is passed, that provider is used for final synthesis.
78
+ - If not passed, synthesis defaults use: preset `synthesis_provider`, then global config `sim.synthesis_provider`.
79
+ - `--synthesis-provider` requires `--synthesis-workflow` to be set.
80
+ - `--dry-run` is a non-mutating preview and cannot be combined with `--writeback`.
81
+
82
+ ## Artifacts
83
+
84
+ Run output lives under `.ace-local/sim/simulations/<run-id>/`.
85
+
86
+ Run root:
87
+
88
+ - `session.yml` — simulation session metadata
89
+ - `synthesis.yml` — final synthesis status and summaries
90
+ - `input.md` — bundled source content used for provider execution
91
+ - `input.bundle.md` — source bundle manifest generated before execution
92
+
93
+ Per chain (`<provider>-<iteration>`):
94
+
95
+ - `NN-step/input.md` — effective input for that step
96
+ - `NN-step/user.bundle.md` — step bundle for LLM prompt
97
+ - `NN-step/user.prompt.md` — resolved prompt file
98
+ - `NN-step/output.md` — provider output for that step
99
+
100
+ Final directory:
101
+
102
+ - `final/input.md` — combined chain outputs
103
+ - `final/user.bundle.md` — final synthesis bundle
104
+ - `final/user.prompt.md` — final synthesis prompt
105
+ - `final/output.sequence.md` — raw LLM output sequence
106
+ - `final/suggestions.report.md` — parsed suggestions block
107
+ - `final/source.original.md` — original source snapshot
108
+ - `final/source.revised.md` — revised source output (if synthesis enabled)
109
+
110
+ ## Behavior notes
111
+
112
+ - Steps run sequentially within each chain — each step's output becomes the next step's input, so later steps build on earlier reasoning.
113
+ - `draft`, `plan`, `work` are common defaults; custom step order is supported via `--steps`.
114
+ - After all chains complete, the synthesis stage gathers feedback from every stage to propose improvements and produce a revised source artifact.
115
+ - Synthesis is optional; enable via preset or explicit `--synthesis-workflow`.
116
+ - `--dry-run` does not perform provider calls.
117
+
118
+ ## Troubleshooting
119
+
120
+ - `Unknown preset`:
121
+ - verify preset exists under one of `.ace-defaults/sim/presets`, `~/.ace/sim/presets`, or `.ace/sim/presets`
122
+ - `Missing step config`:
123
+ - verify step bundles exist in step search path above
124
+ - `--source is required`:
125
+ - provide source files directly or via preset defaults
126
+ - `synthesis_provider requires synthesis_workflow`:
127
+ - include both flags together when overriding synthesis provider
data/exe/ace-sim ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "ace/sim"
6
+
7
+ args = ARGV.empty? ? ["--help"] : ARGV
8
+
9
+ begin
10
+ exit_code = Ace::Sim::CLI.start(args)
11
+ exit(exit_code) if exit_code.is_a?(Integer) && exit_code.nonzero?
12
+ rescue Ace::Support::Cli::Error => e
13
+ warn e.message
14
+ exit(e.exit_code)
15
+ end
@@ -0,0 +1,29 @@
1
+ ---
2
+ name: as-sim-run
3
+ description: Run scenario simulation with provider comparison
4
+ # bundle: wfi://sim/run
5
+ # agent: general-purpose
6
+ user-invocable: true
7
+ allowed-tools:
8
+ - Bash(ace-sim:*)
9
+ - Bash(ace-bundle:*)
10
+ - Read
11
+ - TodoWrite
12
+ argument-hint: "[--preset NAME] [--source PATH]"
13
+ last_modified: 2026-02-28
14
+ source: ace-sim
15
+ integration:
16
+ targets:
17
+ - claude
18
+ - codex
19
+ - gemini
20
+ - opencode
21
+ - pi
22
+ providers: {}
23
+ skill:
24
+ kind: workflow
25
+ execution:
26
+ workflow: wfi://sim/run
27
+ ---
28
+
29
+ Load and run `ace-bundle wfi://sim/run` in the current project, then follow the loaded workflow as the source of truth and execute it end-to-end instead of only summarizing it.
@@ -0,0 +1,155 @@
1
+ ---
2
+ doc-type: workflow
3
+ title: Simulation Run Workflow
4
+ purpose: Run ace-sim simulation with preset, source, and provider configuration
5
+ ace-docs:
6
+ last-updated: 2026-02-28
7
+ last-checked: 2026-03-21
8
+ ---
9
+
10
+ # Simulation Run Workflow
11
+
12
+ ## Goal
13
+
14
+ Run a simulation against a source document using a preset, then review the synthesis results (suggestions report and revised source).
15
+
16
+ ## Arguments
17
+
18
+ - `--preset`: Preset name (optional). Available presets:
19
+ - `validate-task` — steps: plan, work (synthesis: `wfi://task/review`)
20
+ - `validate-idea` — steps: draft, plan, work (synthesis: `wfi://idea/review`)
21
+ - `--source`: Source markdown file path (required unless preset defines a default source)
22
+ - `--provider`: Provider:model override (repeatable). Examples: `google:flash-preview`, `google:pro-preview`
23
+ - `--synthesis-provider`: Provider:model for final synthesis. Example: `claude:haiku`
24
+ - `--dry-run`: Preview what would run without executing
25
+
26
+ ## Instructions
27
+
28
+ ### Step 1: Resolve Source
29
+
30
+ If the source is a task number or multiple files, use `ace-bundle` to merge into a single file:
31
+
32
+ ```bash
33
+ # Single task
34
+ ace-bundle task:148 --output /tmp/sim-source.md
35
+
36
+ # Task spec with usage context (recommended for validate-task)
37
+ ace-sim run --preset validate-task --source "path/to/task.s.md,path/to/ux/usage.md"
38
+
39
+ # Already a single file — use directly
40
+ # --source path/to/spec.md
41
+ ```
42
+
43
+ If `--source` points to an existing file, use it directly.
44
+
45
+ If the source is a task with usage documentation (`ux/usage.md`), include both spec and usage files to give the simulation behavioral acceptance context.
46
+
47
+ ### Step 2: Run Simulation
48
+
49
+ Execute `ace-sim run` with the resolved source and any overrides.
50
+
51
+ ```bash
52
+ # With preset (defaults)
53
+ ace-sim run --preset validate-task --source path/to/source.md
54
+
55
+ # With provider override
56
+ ace-sim run --preset validate-task --source path/to/source.md --provider google:flash-preview
57
+
58
+ # With synthesis provider override
59
+ ace-sim run --preset validate-task --source path/to/source.md --synthesis-provider google:pro-preview
60
+
61
+ # Dry run — preview without executing
62
+ ace-sim run --preset validate-task --source path/to/source.md --dry-run
63
+ ```
64
+
65
+ **Important for Claude Code**: Run with 10-minute timeout (600000ms) and wait for completion inline (not background). Simulations may take several minutes depending on step count and provider.
66
+
67
+ #### Execution Guard (Mandatory)
68
+
69
+ - Completion is defined by **process exit** (success or failure), not by partial output.
70
+ - Do **not** treat temporary silence/no new output as completion.
71
+ - Do **not** run any Step 3+ commands until Step 2 process exit is confirmed.
72
+ - If 10-minute timeout (600000ms) is reached, report timeout and last observed output, then stop dependent steps.
73
+
74
+ Wait for the simulation process to exit. Note the **Run Dir** path from the output.
75
+
76
+ ### Step 3: Review Results
77
+
78
+ After successful completion, read the synthesis output files from the run directory:
79
+
80
+ ```bash
81
+ # Read the suggestions report
82
+ Read: <run-dir>/final/suggestions.report.md
83
+
84
+ # Read the revised source document
85
+ Read: <run-dir>/final/source.revised.md
86
+ ```
87
+
88
+ The run directory structure:
89
+ ```
90
+ <run-dir>/
91
+ chains/ # Individual chain execution outputs
92
+ final/
93
+ suggestions.report.md # Synthesis report with findings
94
+ source.revised.md # Revised source incorporating suggestions
95
+ ```
96
+
97
+ ### Step 4: Apply Validated Changes
98
+
99
+ After reviewing `source.revised.md`, apply the validated refinements back to the
100
+ original source files. The simulation output is only useful if it feeds back into
101
+ the actual specs.
102
+
103
+ **When the source was a single file** — apply diffs directly using the Edit tool,
104
+ comparing `source.revised.md` against the original.
105
+
106
+ **When the source was bundled from multiple files** (e.g. merged task specs):
107
+ - Compare `source.revised.md` section by section against each original file
108
+ - Apply each relevant change to its correct source file using the Edit tool
109
+ - Common changes to propagate: status promotions, spec refinements, new error
110
+ messages, updated success criteria, clarified edge cases
111
+
112
+ **Skip or flag for human decision:**
113
+ - Changes marked `[PENDING DECISION]` in the revised source
114
+ - Structural additions that don't have a clear home in the originals
115
+ - Questions from the suggestions report — surface these to the user instead
116
+
117
+ Do not commit yet — let the commit workflow handle that after review.
118
+
119
+ ### Step 5: Present Summary
120
+
121
+ Summarize the simulation findings to the user:
122
+
123
+ 1. **Run metadata** — preset, providers, step count, run directory
124
+ 2. **Key findings** — top suggestions from the suggestions report
125
+ 3. **Changes applied** — which original files were updated and what changed
126
+ 4. **Pending decisions** — any `[PENDING DECISION]` items needing human input
127
+ 5. **Recommended actions** — next steps (commit, re-run with different providers, etc.)
128
+
129
+ ## Quick Reference
130
+
131
+ ```bash
132
+ # List available presets
133
+ ace-sim run --help
134
+
135
+ # Validate a task spec
136
+ ace-sim run --preset validate-task --source path/to/task-spec.md
137
+
138
+ # Validate an idea
139
+ ace-sim run --preset validate-idea --source path/to/idea.md
140
+
141
+ # Override providers
142
+ ace-sim run --preset validate-task --source spec.md --provider google:flash-preview --provider google:pro-preview
143
+
144
+ # Dry run
145
+ ace-sim run --preset validate-task --source spec.md --dry-run
146
+ ```
147
+
148
+ ## Success Criteria
149
+
150
+ - [ ] Simulation completed without errors
151
+ - [ ] Synthesis report reviewed (`final/suggestions.report.md`)
152
+ - [ ] Revised source reviewed (`final/source.revised.md`)
153
+ - [ ] Validated changes applied back to original source files
154
+ - [ ] Pending decisions surfaced to user
155
+ - [ ] Key findings summarized to user
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Sim
5
+ module CLI
6
+ module Commands
7
+ class Run < Ace::Support::Cli::Command
8
+ include Ace::Support::Cli::Base
9
+
10
+ desc "Run preset simulation"
11
+
12
+ option :preset, type: :string, desc: "Preset name (configured in .ace/sim/presets/*.yml)"
13
+ option :source, type: :array, desc: "Source file(s) - repeatable, supports globs"
14
+ option :steps, type: :string, desc: "Comma-separated step names"
15
+ option :provider, type: :array, desc: "Provider:model (repeatable, e.g. --provider codex:mini)"
16
+ option :repeat, type: :integer, desc: "Repeat count for each provider"
17
+ option :synthesis_workflow, type: :string, desc: "Workflow/file reference for final synthesis (e.g. wfi://task/review, wfi://idea/review)"
18
+ option :synthesis_provider, type: :string, desc: "Provider:model for final suggestions synthesis"
19
+ option :dry_run, type: :boolean, desc: "Enable non-mutating run"
20
+ option :writeback, type: :boolean, desc: "Enable writeback"
21
+ option :quiet, aliases: ["-q"], type: :boolean, default: false, desc: "Suppress non-essential output"
22
+ option :verbose, aliases: ["-v"], type: :boolean, default: false, desc: "Verbose output"
23
+
24
+ def initialize(runner: nil)
25
+ super()
26
+ @runner = runner
27
+ end
28
+
29
+ def call(**options)
30
+ session = build_session(options)
31
+ result = runner.run(session)
32
+
33
+ unless options[:quiet]
34
+ puts "Run ID: #{result[:run_id]}"
35
+ puts "Run Dir: #{result[:run_dir]}"
36
+ puts "Status: #{result[:status]}"
37
+ end
38
+
39
+ return if result[:success]
40
+
41
+ raise Ace::Support::Cli::Error.new(result[:error] || "Simulation failed")
42
+ end
43
+
44
+ private
45
+
46
+ def build_session(options)
47
+ preset_name = pick_value(options[:preset], Ace::Sim.default_preset_name)
48
+ preset_data = Ace::Sim.load_preset(preset_name)
49
+ if preset_data.nil?
50
+ raise Ace::Support::Cli::Error.new("Unknown preset '#{preset_name}'. Known presets: #{Ace::Sim.preset_names.join(", ")}")
51
+ end
52
+
53
+ if !options[:synthesis_provider].to_s.strip.empty? && options[:synthesis_workflow].nil?
54
+ raise Ace::Support::Cli::Error.new("synthesis_provider requires synthesis_workflow")
55
+ end
56
+
57
+ sources = options[:source] || Array(preset_data["source"])
58
+ raise Ace::Support::Cli::Error.new("--source is required") if sources.empty?
59
+
60
+ steps = if options[:steps] && !options[:steps].to_s.strip.empty?
61
+ parse_steps(options[:steps])
62
+ else
63
+ Ace::Sim.normalize_list(preset_data["steps"] || Ace::Sim.get("sim", "default_steps"))
64
+ end
65
+ raise Ace::Support::Cli::Error.new("At least one step is required") if steps.empty?
66
+
67
+ providers = if options[:provider].nil?
68
+ Ace::Sim.normalize_list(preset_data["provider"] || preset_data["providers"] || Ace::Sim.get("sim", "default_providers"))
69
+ else
70
+ Ace::Sim.normalize_list(options[:provider])
71
+ end
72
+ raise Ace::Support::Cli::Error.new("At least one --provider is required") if providers.empty?
73
+
74
+ repeat = pick_value(options[:repeat], preset_data["repeat"], Ace::Sim.get("sim", "default_repeat") || 1)
75
+ repeat = Integer(repeat)
76
+ raise Ace::Support::Cli::Error.new("--repeat must be >= 1") if repeat < 1
77
+
78
+ synthesis_workflow = pick_value(
79
+ options[:synthesis_workflow],
80
+ preset_data["synthesis_workflow"],
81
+ Ace::Sim.get("sim", "synthesis_workflow")
82
+ ).to_s.strip
83
+ synthesis_provider = pick_value(
84
+ options[:synthesis_provider],
85
+ preset_data["synthesis_provider"],
86
+ Ace::Sim.get("sim", "synthesis_provider")
87
+ ).to_s.strip
88
+
89
+ dry_run = pick_value(options[:dry_run], preset_data["dry_run"], false)
90
+ writeback = pick_value(options[:writeback], preset_data["writeback"], Ace::Sim.get("sim", "writeback") || false)
91
+
92
+ step_bundles = resolve_step_bundles(steps)
93
+
94
+ Models::SimulationSession.new(
95
+ preset: preset_name,
96
+ source: sources,
97
+ steps: steps,
98
+ providers: providers,
99
+ repeat: repeat,
100
+ dry_run: dry_run,
101
+ writeback: writeback,
102
+ verbose: options[:verbose],
103
+ step_bundles: step_bundles,
104
+ synthesis_workflow: synthesis_workflow,
105
+ synthesis_provider: synthesis_provider
106
+ )
107
+ rescue ArgumentError => e
108
+ raise Ace::Support::Cli::Error.new(e.message)
109
+ rescue Ace::Sim::ValidationError => e
110
+ raise Ace::Support::Cli::Error.new(e.message)
111
+ end
112
+
113
+ def parse_steps(raw_steps)
114
+ raw_steps.to_s.split(",").map(&:strip).reject(&:empty?)
115
+ end
116
+
117
+ def resolve_step_bundles(steps)
118
+ steps.each_with_object({}) do |step, configs|
119
+ config_path = Ace::Sim.step_bundle_path(step)
120
+ if config_path.nil?
121
+ raise Ace::Support::Cli::Error.new("Missing step config for '#{step}' in .ace/sim/steps or defaults")
122
+ end
123
+
124
+ configs[step] = config_path
125
+ end
126
+ end
127
+
128
+ def pick_value(*values)
129
+ values.find { |value| !value.nil? }
130
+ end
131
+
132
+ def runner
133
+ @runner ||= Organisms::SimulationRunner.new
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/support/cli"
4
+ require "ace/core"
5
+ require_relative "cli/commands/run"
6
+
7
+ module Ace
8
+ module Sim
9
+ module CLI
10
+ extend Ace::Support::Cli::RegistryDsl
11
+
12
+ PROGRAM_NAME = "ace-sim"
13
+
14
+ REGISTERED_COMMANDS = [
15
+ ["run", "Run a preset simulation"]
16
+ ].freeze
17
+
18
+ HELP_EXAMPLES = [
19
+ "ace-sim run --preset validate-idea --source path/to/source.md --provider codex:mini --dry-run",
20
+ "ace-sim run --preset validate-idea --source path/to/source.md --provider codex:mini --provider google:gflash --repeat 2",
21
+ "ace-sim run --preset validate-idea --source path/to/source.md --provider glite --synthesis-workflow wfi://task/review --synthesis-provider claude:haiku"
22
+ ].freeze
23
+
24
+ def self.start(args)
25
+ Ace::Support::Cli::Runner.new(self).call(args: args)
26
+ end
27
+
28
+ register "run", Commands::Run
29
+
30
+ version_cmd = Ace::Support::Cli::VersionCommand.build(
31
+ gem_name: "ace-sim",
32
+ version: Ace::Sim::VERSION
33
+ )
34
+ register "version", version_cmd
35
+ register "--version", version_cmd
36
+
37
+ help_cmd = Ace::Support::Cli::HelpCommand.build(
38
+ program_name: PROGRAM_NAME,
39
+ version: Ace::Sim::VERSION,
40
+ commands: REGISTERED_COMMANDS,
41
+ examples: HELP_EXAMPLES
42
+ )
43
+ register "help", help_cmd
44
+ register "--help", help_cmd
45
+ register "-h", help_cmd
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Sim
5
+ module Models
6
+ class SimulationSession
7
+ attr_reader :preset, :source, :steps, :providers, :repeat, :dry_run, :writeback, :run_id, :verbose,
8
+ :step_bundles, :synthesis_workflow, :synthesis_provider
9
+
10
+ def initialize(preset:, source:, steps:, providers:, repeat:, dry_run:, writeback:, verbose: false,
11
+ run_id: nil, step_bundles: {}, synthesis_workflow: nil, synthesis_provider: nil)
12
+ @preset = preset.to_s.strip
13
+ @source = Array(source).map(&:to_s).map(&:strip).reject(&:empty?)
14
+ @steps = Ace::Sim.normalize_list(steps)
15
+ @providers = Ace::Sim.normalize_list(providers)
16
+ @repeat = Integer(repeat)
17
+ @dry_run = !!dry_run
18
+ @writeback = !!writeback
19
+ @verbose = !!verbose
20
+ @step_bundles = stringify_step_bundles(step_bundles)
21
+ @synthesis_workflow = synthesis_workflow.to_s.strip
22
+ @synthesis_provider = synthesis_provider.to_s.strip
23
+ @run_id = run_id || Ace::Sim.next_run_id
24
+
25
+ validate!
26
+ end
27
+
28
+ def dry_run?
29
+ dry_run
30
+ end
31
+
32
+ def regenerate_run_id!
33
+ @run_id = Ace::Sim.next_run_id
34
+ end
35
+
36
+ def bundle_path_for(step)
37
+ step_bundles[step.to_s]
38
+ end
39
+
40
+ def synthesis_enabled?
41
+ !synthesis_workflow.empty?
42
+ end
43
+
44
+ def to_h
45
+ {
46
+ "run_id" => run_id,
47
+ "preset" => preset,
48
+ "source" => source,
49
+ "steps" => steps,
50
+ "providers" => providers,
51
+ "repeat" => repeat,
52
+ "dry_run" => dry_run,
53
+ "writeback" => writeback,
54
+ "synthesis_workflow" => synthesis_workflow,
55
+ "synthesis_provider" => synthesis_provider
56
+ }
57
+ end
58
+
59
+ private
60
+
61
+ def validate!
62
+ raise Ace::Sim::ValidationError, "source cannot be empty" if source.empty?
63
+ raise Ace::Sim::ValidationError, "steps cannot be empty" if steps.empty?
64
+ raise Ace::Sim::ValidationError, "providers cannot be empty" if providers.empty?
65
+ raise Ace::Sim::ValidationError, "repeat must be >= 1" if repeat < 1
66
+ raise Ace::Sim::ValidationError, "writeback cannot be enabled with --dry-run" if dry_run && writeback
67
+ if !synthesis_provider.empty? && synthesis_workflow.empty?
68
+ raise Ace::Sim::ValidationError, "synthesis_provider requires synthesis_workflow"
69
+ end
70
+
71
+ missing_bundles = steps.reject { |step| step_bundles.key?(step) && !step_bundles[step].to_s.strip.empty? }
72
+ return if missing_bundles.empty?
73
+
74
+ raise Ace::Sim::ValidationError, "Missing step configs for: #{missing_bundles.join(", ")}"
75
+ end
76
+
77
+ def stringify_step_bundles(raw)
78
+ raw.to_h.each_with_object({}) do |(step, path), acc|
79
+ acc[step.to_s] = path.to_s
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end