aidp 0.19.1 → 0.21.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.
@@ -30,6 +30,8 @@ module Aidp
30
30
 
31
31
  def run
32
32
  display_welcome
33
+ # Normalize any legacy or label-based model_family entries before prompting
34
+ normalize_existing_model_families!
33
35
  return @saved if skip_wizard?
34
36
 
35
37
  configure_providers
@@ -150,11 +152,30 @@ module Aidp
150
152
  fallback_choices = available_providers.reject { |_, name| name == provider_choice }
151
153
  fallback_default_names = existing_fallbacks.filter_map { |provider_name| fallback_choices.key(provider_name) }
152
154
 
155
+ prompt.say("\n💡 Use ↑/↓ arrows to navigate, SPACE to select/deselect, ENTER to confirm")
153
156
  fallback_selected = prompt.multi_select("Select fallback providers (used if primary fails):", default: fallback_default_names) do |menu|
154
157
  fallback_choices.each do |display_name, provider_name|
155
158
  menu.choice display_name, provider_name
156
159
  end
157
160
  end
161
+ if ENV["AIDP_FALLBACK_DEBUG"] == "1"
162
+ prompt.say("[debug] raw multi_select fallback_selected=#{fallback_selected.inspect}")
163
+ end
164
+ # Recovery: if multi_select unexpectedly returns empty and there were no existing fallbacks, offer a single-select
165
+ if fallback_selected.empty? && existing_fallbacks.empty? && !fallback_choices.empty?
166
+ if ENV["AIDP_FALLBACK_DEBUG"] == "1"
167
+ prompt.say("[debug] invoking recovery single-select for first fallback")
168
+ end
169
+ if prompt.yes?("No fallback selected. Add one?", default: true)
170
+ recovery_choice = prompt.select("Select a fallback provider:") do |menu|
171
+ fallback_choices.each do |display_name, provider_name|
172
+ menu.choice display_name, provider_name
173
+ end
174
+ menu.choice "Skip", :skip
175
+ end
176
+ fallback_selected = [recovery_choice] unless recovery_choice == :skip
177
+ end
178
+ end
158
179
 
159
180
  # If user selected none but we had existing fallbacks, confirm removal
160
181
  if fallback_selected.empty? && existing_fallbacks.any?
@@ -167,10 +188,81 @@ module Aidp
167
188
  set([:harness, :fallback_providers], cleaned_fallbacks)
168
189
 
169
190
  # Auto-create minimal provider configs for fallbacks if missing
170
- cleaned_fallbacks.each { |fp| ensure_provider_billing_config(fp) }
191
+ cleaned_fallbacks.each do |fp|
192
+ prompt.say("[debug] ensuring billing config for fallback '#{fp}'") if ENV["AIDP_FALLBACK_DEBUG"] == "1"
193
+ ensure_provider_billing_config(fp, force: true)
194
+ end
195
+
196
+ # Offer editing of existing provider configurations (primary + fallbacks)
197
+ # (editable will be recomputed after any additional fallback additions)
198
+ ([provider_choice] + cleaned_fallbacks).uniq.reject { |p| p == "custom" }
199
+
200
+ # Optional: allow adding more fallbacks iteratively
201
+ if prompt.yes?("Add another fallback provider?", default: false)
202
+ loop do
203
+ remaining = available_providers.reject { |_, name| ([provider_choice] + cleaned_fallbacks).include?(name) }
204
+ break if remaining.empty?
205
+ add_choice = prompt.select("Select additional fallback provider:") do |menu|
206
+ remaining.each { |display, name| menu.choice display, name }
207
+ menu.choice "Done", :done
208
+ end
209
+ break if add_choice == :done
210
+ unless cleaned_fallbacks.include?(add_choice)
211
+ cleaned_fallbacks << add_choice
212
+ set([:harness, :fallback_providers], cleaned_fallbacks)
213
+ ensure_provider_billing_config(add_choice, force: true)
214
+ end
215
+ end
216
+ end
217
+ # Recompute editable after additions
218
+ editable = ([provider_choice] + cleaned_fallbacks).uniq.reject { |p| p == "custom" }
219
+ if editable.any? && prompt.yes?("Edit provider configuration details (billing/model family)?", default: false)
220
+ loop do
221
+ # Build dynamic mapping of display names -> internal names for edit menu
222
+ available_map = discover_available_providers # {display_name => internal_name}
223
+ display_name_for = available_map.invert # {internal_name => display_name}
224
+ to_edit = prompt.select("Select a provider to edit or add:") do |menu|
225
+ editable.each do |prov|
226
+ display_label = display_name_for.fetch(prov, prov.capitalize)
227
+ menu.choice display_label, prov
228
+ end
229
+ # Sentinel option: add a new fallback provider that isn't yet in editable list
230
+ remaining = available_map.values - editable
231
+ if remaining.any?
232
+ menu.choice "➕ Add fallback provider…", :add_fallback
233
+ end
234
+ menu.choice "Done", :done
235
+ end
236
+
237
+ case to_edit
238
+ when :done
239
+ break
240
+ when :add_fallback
241
+ # Allow user to pick from remaining providers by display name
242
+ remaining_map = available_map.select { |disp, internal| !editable.include?(internal) && internal != provider_choice }
243
+ add_choice = prompt.select("Select provider to add as fallback:") do |menu|
244
+ remaining_map.each { |disp, internal| menu.choice disp, internal }
245
+ menu.choice "Cancel", :cancel
246
+ end
247
+ next if add_choice == :cancel
248
+ unless cleaned_fallbacks.include?(add_choice)
249
+ cleaned_fallbacks << add_choice
250
+ set([:harness, :fallback_providers], cleaned_fallbacks)
251
+ prompt.say("[debug] ensuring billing config for newly added fallback '#{add_choice}'") if ENV["AIDP_FALLBACK_DEBUG"] == "1"
252
+ ensure_provider_billing_config(add_choice, force: true)
253
+ editable = ([provider_choice] + cleaned_fallbacks).uniq.reject { |p| p == "custom" }
254
+ end
255
+ else
256
+ edit_provider_configuration(to_edit)
257
+ end
258
+ end
259
+ end
171
260
 
172
261
  # Provide informational note (no secret handling stored)
173
262
  show_provider_info_note(provider_choice) unless provider_choice == "custom"
263
+
264
+ # Show summary of configured providers (replaces the earlier inline summary)
265
+ show_provider_summary(provider_choice, cleaned_fallbacks) unless provider_choice == "custom"
174
266
  end
175
267
 
176
268
  # Removed MCP configuration step (MCP now expected to be provider-specific if used)
@@ -262,13 +354,19 @@ module Aidp
262
354
  enabled = prompt.yes?("Enable coverage tracking?", default: existing.fetch(:enabled, false))
263
355
  return set([:work_loop, :coverage], {enabled: false}) unless enabled
264
356
 
265
- tool = prompt.select("Which coverage tool do you use?", default: existing[:tool]) do |menu|
266
- menu.choice "SimpleCov (Ruby)", "simplecov"
267
- menu.choice "NYC/Istanbul (JavaScript)", "nyc"
268
- menu.choice "Coverage.py (Python)", "coverage.py"
269
- menu.choice "go test -cover (Go)", "go-cover"
270
- menu.choice "Jest (JavaScript)", "jest"
271
- menu.choice "Other", "other"
357
+ coverage_tool_choices = [
358
+ ["SimpleCov (Ruby)", "simplecov"],
359
+ ["NYC/Istanbul (JavaScript)", "nyc"],
360
+ ["Coverage.py (Python)", "coverage.py"],
361
+ ["go test -cover (Go)", "go-cover"],
362
+ ["Jest (JavaScript)", "jest"],
363
+ ["Other", "other"]
364
+ ]
365
+ coverage_tool_default = existing[:tool]
366
+ coverage_tool_default_label = coverage_tool_choices.find { |label, value| value == coverage_tool_default }&.first
367
+
368
+ tool = prompt.select("Which coverage tool do you use?", default: coverage_tool_default_label) do |menu|
369
+ coverage_tool_choices.each { |label, value| menu.choice label, value }
272
370
  end
273
371
 
274
372
  run_command = ask_with_default("Coverage run command", existing[:run_command] || detect_coverage_command(tool))
@@ -300,10 +398,16 @@ module Aidp
300
398
  enabled = prompt.yes?("Enable interactive testing tools?", default: existing.fetch(:enabled, false))
301
399
  return set([:work_loop, :interactive_testing], {enabled: false}) unless enabled
302
400
 
303
- app_type = prompt.select("What type of application are you testing?", default: existing[:app_type]) do |menu|
304
- menu.choice "Web application", "web"
305
- menu.choice "CLI application", "cli"
306
- menu.choice "Desktop application", "desktop"
401
+ app_type_choices = [
402
+ ["Web application", "web"],
403
+ ["CLI application", "cli"],
404
+ ["Desktop application", "desktop"]
405
+ ]
406
+ app_type_default = existing[:app_type]
407
+ app_type_default_label = app_type_choices.find { |label, value| value == app_type_default }&.first
408
+
409
+ app_type = prompt.select("What type of application are you testing?", default: app_type_default_label) do |menu|
410
+ app_type_choices.each { |label, value| menu.choice label, value }
307
411
  end
308
412
 
309
413
  tools = {}
@@ -382,17 +486,21 @@ module Aidp
382
486
 
383
487
  # Detect VCS
384
488
  detected_vcs = detect_vcs_tool
489
+ vcs_choices = [
490
+ ["git", "git"],
491
+ ["svn", "svn"],
492
+ ["none (no VCS)", "none"]
493
+ ]
494
+ vcs_default = existing[:tool] || detected_vcs || "git"
495
+ vcs_default_label = vcs_choices.find { |label, value| value == vcs_default }&.first
496
+
385
497
  vcs_tool = if detected_vcs
386
- prompt.select("Detected #{detected_vcs}. Use this version control system?", default: existing[:tool] || detected_vcs) do |menu|
387
- menu.choice "git", "git"
388
- menu.choice "svn", "svn"
389
- menu.choice "none (no VCS)", "none"
498
+ prompt.select("Detected #{detected_vcs}. Use this version control system?", default: vcs_default_label) do |menu|
499
+ vcs_choices.each { |label, value| menu.choice label, value }
390
500
  end
391
501
  else
392
- prompt.select("Which version control system do you use?", default: existing[:tool] || "git") do |menu|
393
- menu.choice "git", "git"
394
- menu.choice "svn", "svn"
395
- menu.choice "none (no VCS)", "none"
502
+ prompt.select("Which version control system do you use?", default: vcs_default_label) do |menu|
503
+ vcs_choices.each { |label, value| menu.choice label, value }
396
504
  end
397
505
  end
398
506
 
@@ -400,10 +508,18 @@ module Aidp
400
508
 
401
509
  prompt.say("\n📋 Commit Behavior (applies to copilot/interactive mode only)")
402
510
  prompt.say("Note: Watch mode and fully automatic daemon mode will always commit changes.")
403
- behavior = prompt.select("In copilot mode, should aidp:", default: existing[:behavior] || "nothing") do |menu|
404
- menu.choice "Do nothing (manual git operations)", "nothing"
405
- menu.choice "Stage changes only", "stage"
406
- menu.choice "Stage and commit changes", "commit"
511
+
512
+ # Map value defaults to choice labels for TTY::Prompt validation
513
+ behavior_choices = [
514
+ ["Do nothing (manual git operations)", "nothing"],
515
+ ["Stage changes only", "stage"],
516
+ ["Stage and commit changes", "commit"]
517
+ ]
518
+ behavior_default = existing[:behavior] || "nothing"
519
+ behavior_default_label = behavior_choices.find { |label, value| value == behavior_default }&.first
520
+
521
+ behavior = prompt.select("In copilot mode, should aidp:", default: behavior_default_label) do |menu|
522
+ behavior_choices.each { |label, value| menu.choice label, value }
407
523
  end
408
524
 
409
525
  # Commit message configuration
@@ -437,10 +553,16 @@ module Aidp
437
553
 
438
554
  # Commit message style
439
555
  commit_style = if conventional_commits
440
- prompt.select("Conventional commit style:", default: existing[:commit_style] || "default") do |menu|
441
- menu.choice "Default (e.g., 'feat: add user authentication')", "default"
442
- menu.choice "Angular (with scope: 'feat(auth): add login')", "angular"
443
- menu.choice "Emoji (e.g., '✨ feat: add user authentication')", "emoji"
556
+ commit_style_choices = [
557
+ ["Default (e.g., 'feat: add user authentication')", "default"],
558
+ ["Angular (with scope: 'feat(auth): add login')", "angular"],
559
+ ["Emoji (e.g., '✨ feat: add user authentication')", "emoji"]
560
+ ]
561
+ commit_style_default = existing[:commit_style] || "default"
562
+ commit_style_default_label = commit_style_choices.find { |label, value| value == commit_style_default }&.first
563
+
564
+ prompt.select("Conventional commit style:", default: commit_style_default_label) do |menu|
565
+ commit_style_choices.each { |label, value| menu.choice label, value }
444
566
  end
445
567
  else
446
568
  "default"
@@ -476,10 +598,16 @@ module Aidp
476
598
  )
477
599
 
478
600
  if auto_create_pr
479
- pr_strategy = prompt.select("PR creation strategy:", default: existing[:pr_strategy] || "draft") do |menu|
480
- menu.choice "Create as draft PR (safe, allows review before merge)", "draft"
481
- menu.choice "Create as ready PR (immediately reviewable)", "ready"
482
- menu.choice "Create and auto-merge (fully autonomous, requires approval rules)", "auto_merge"
601
+ pr_strategy_choices = [
602
+ ["Create as draft PR (safe, allows review before merge)", "draft"],
603
+ ["Create as ready PR (immediately reviewable)", "ready"],
604
+ ["Create and auto-merge (fully autonomous, requires approval rules)", "auto_merge"]
605
+ ]
606
+ pr_strategy_default = existing[:pr_strategy] || "draft"
607
+ pr_strategy_default_label = pr_strategy_choices.find { |label, value| value == pr_strategy_default }&.first
608
+
609
+ pr_strategy = prompt.select("PR creation strategy:", default: pr_strategy_default_label) do |menu|
610
+ pr_strategy_choices.each { |label, value| menu.choice label, value }
483
611
  end
484
612
 
485
613
  {
@@ -614,11 +742,16 @@ module Aidp
614
742
  prompt.say("-" * 40)
615
743
  existing = get([:logging]) || {}
616
744
 
617
- # TODO: Add default back once TTY-Prompt default validation issue is resolved
618
- log_level = prompt.select("Log level:") do |menu|
619
- menu.choice "Debug", "debug"
620
- menu.choice "Info", "info"
621
- menu.choice "Error", "error"
745
+ log_level_choices = [
746
+ ["Debug", "debug"],
747
+ ["Info", "info"],
748
+ ["Error", "error"]
749
+ ]
750
+ log_level_default = existing[:level] || "info"
751
+ log_level_default_label = log_level_choices.find { |label, value| value == log_level_default }&.first
752
+
753
+ log_level = prompt.select("Log level:", default: log_level_default_label) do |menu|
754
+ log_level_choices.each { |label, value| menu.choice label, value }
622
755
  end
623
756
  json = prompt.yes?("Use JSON log format?", default: existing.fetch(:json, false))
624
757
  max_size = ask_with_default("Max log size (MB)", (existing[:max_size_mb] || 10).to_s) { |value| value.to_i }
@@ -897,44 +1030,132 @@ module Aidp
897
1030
  prompt.say("Only the billing model (subscription vs usage_based) is recorded for fallback decisions.")
898
1031
  end
899
1032
 
1033
+ def show_provider_summary(primary, fallbacks)
1034
+ prompt.say("\n📋 Provider Configuration Summary:")
1035
+ providers_config = get([:providers]) || {}
1036
+
1037
+ # Show primary
1038
+ if primary && primary != "custom"
1039
+ primary_cfg = providers_config[primary.to_sym] || {}
1040
+ prompt.say(" ✓ Primary: #{primary} (#{primary_cfg[:type] || "not configured"}, #{primary_cfg[:model_family] || "auto"})")
1041
+ end
1042
+
1043
+ # Show fallbacks
1044
+ if fallbacks && !fallbacks.empty?
1045
+ fallbacks.each do |fallback|
1046
+ fallback_cfg = providers_config[fallback.to_sym] || {}
1047
+ prompt.say(" ✓ Fallback: #{fallback} (#{fallback_cfg[:type] || "not configured"}, #{fallback_cfg[:model_family] || "auto"})")
1048
+ end
1049
+ end
1050
+ end
1051
+
900
1052
  # Ensure a minimal billing configuration exists for a selected provider (no secrets)
901
- def ensure_provider_billing_config(provider_name)
1053
+ def ensure_provider_billing_config(provider_name, force: false)
902
1054
  return if provider_name.nil? || provider_name == "custom"
903
1055
  providers_section = get([:providers]) || {}
904
1056
  existing = providers_section[provider_name.to_sym]
905
1057
 
906
- if existing && existing[:type]
1058
+ if existing && existing[:type] && !force
907
1059
  prompt.say(" • Provider '#{provider_name}' already configured (type: #{existing[:type]})")
908
- # Still ask for model family if not set
909
1060
  unless existing[:model_family]
910
- model_family = ask_model_family(provider_name, existing[:model_family])
1061
+ model_family = ask_model_family(provider_name)
911
1062
  set([:providers, provider_name.to_sym, :model_family], model_family)
912
1063
  end
913
1064
  return
914
1065
  end
915
1066
 
916
- provider_type = ask_provider_billing_type(provider_name)
917
- model_family = ask_model_family(provider_name)
918
- set([:providers, provider_name.to_sym], {type: provider_type, model_family: model_family})
919
- prompt.say(" • Added provider '#{provider_name}' with billing type '#{provider_type}' and model family '#{model_family}' (no secrets stored)")
1067
+ provider_type = ask_provider_billing_type_with_default(provider_name, existing&.dig(:type))
1068
+ model_family = ask_model_family(provider_name, existing&.dig(:model_family) || "auto")
1069
+ merged = (existing || {}).merge(type: provider_type, model_family: model_family)
1070
+ set([:providers, provider_name.to_sym], merged)
1071
+ normalize_existing_model_families!
1072
+ action_word = if existing
1073
+ force ? "reconfigured" : "updated"
1074
+ else
1075
+ "added"
1076
+ end
1077
+ # Enhance messaging with display name when available
1078
+ display_name = discover_available_providers.invert.fetch(provider_name, provider_name)
1079
+ prompt.say(" • #{action_word.capitalize} provider '#{display_name}' (#{provider_name}) with billing type '#{provider_type}' and model family '#{model_family}'")
1080
+ end
1081
+
1082
+ def edit_provider_configuration(provider_name)
1083
+ existing = get([:providers, provider_name.to_sym]) || {}
1084
+ prompt.say("\n🔧 Editing provider '#{provider_name}' (current: type=#{existing[:type] || "unset"}, model_family=#{existing[:model_family] || "unset"})")
1085
+ new_type = ask_provider_billing_type_with_default(provider_name, existing[:type])
1086
+ new_family = ask_model_family(provider_name, existing[:model_family] || "auto")
1087
+ set([:providers, provider_name.to_sym], {type: new_type, model_family: new_family})
1088
+ # Normalize immediately so tests relying on canonical value see 'claude' rather than label
1089
+ normalize_existing_model_families!
1090
+ prompt.ok("Updated '#{provider_name}' → type=#{new_type}, model_family=#{new_family}")
920
1091
  end
921
1092
 
922
1093
  def ask_provider_billing_type(provider_name)
923
- prompt.select("Billing model for #{provider_name}:") do |menu|
924
- menu.choice "Subscription / flat-rate", "subscription"
925
- # e.g. tools that expose an integrated model under a subscription cost
926
- menu.choice "Usage-based / metered (API)", "usage_based"
927
- menu.choice "Passthrough / local (no billing)", "passthrough"
1094
+ ask_provider_billing_type_with_default(provider_name, nil)
1095
+ end
1096
+
1097
+ BILLING_TYPE_CHOICES = [
1098
+ ["Subscription / flat-rate", "subscription"],
1099
+ ["Usage-based / metered (API)", "usage_based"],
1100
+ ["Passthrough / local (no billing)", "passthrough"]
1101
+ ].freeze
1102
+
1103
+ def ask_provider_billing_type_with_default(provider_name, default_value)
1104
+ default_label = BILLING_TYPE_CHOICES.find { |label, value| value == default_value }&.first
1105
+ suffix = default_value ? " (current: #{default_value})" : ""
1106
+ prompt.select("Billing model for #{provider_name}:#{suffix}", default: default_label) do |menu|
1107
+ BILLING_TYPE_CHOICES.each do |label, value|
1108
+ menu.choice(label, value)
1109
+ end
928
1110
  end
929
1111
  end
930
1112
 
1113
+ MODEL_FAMILY_CHOICES = [
1114
+ ["Auto (let provider decide)", "auto"],
1115
+ ["OpenAI o-series (reasoning models)", "openai_o"],
1116
+ ["Anthropic Claude (balanced)", "claude"],
1117
+ ["Mistral (European/open)", "mistral"],
1118
+ ["Local LLM (self-hosted)", "local"]
1119
+ ].freeze
1120
+
931
1121
  def ask_model_family(provider_name, default = "auto")
932
- prompt.select("Preferred model family for #{provider_name}:", default: default) do |menu|
933
- menu.choice "Auto (let provider decide)", "auto"
934
- menu.choice "OpenAI o-series (reasoning models)", "openai_o"
935
- menu.choice "Anthropic Claude (balanced)", "claude"
936
- menu.choice "Mistral (European/open)", "mistral"
937
- menu.choice "Local LLM (self-hosted)", "local"
1122
+ # TTY::Prompt validates defaults against the displayed choice labels, not values.
1123
+ # Map the value default (e.g. "auto") to its corresponding label.
1124
+ default_label = MODEL_FAMILY_CHOICES.find { |label, value| value == default }&.first
1125
+
1126
+ prompt.select("Preferred model family for #{provider_name}:", default: default_label) do |menu|
1127
+ MODEL_FAMILY_CHOICES.each do |label, value|
1128
+ menu.choice(label, value)
1129
+ end
1130
+ end
1131
+ end
1132
+
1133
+ # Canonicalization helpers ------------------------------------------------
1134
+ MODEL_FAMILY_LABEL_TO_VALUE = MODEL_FAMILY_CHOICES.each_with_object({}) do |(label, value), h|
1135
+ h[label] = value
1136
+ end.freeze
1137
+ MODEL_FAMILY_VALUES = MODEL_FAMILY_CHOICES.map { |(_, value)| value }.freeze
1138
+
1139
+ def normalize_model_family(value)
1140
+ return "auto" if value.nil? || value.to_s.strip.empty?
1141
+ # Already a canonical value
1142
+ return value if MODEL_FAMILY_VALUES.include?(value)
1143
+ # Try label -> value
1144
+ mapped = MODEL_FAMILY_LABEL_TO_VALUE[value]
1145
+ return mapped if mapped
1146
+ # Unknown legacy entry -> fallback to auto
1147
+ "auto"
1148
+ end
1149
+
1150
+ def normalize_existing_model_families!
1151
+ providers_cfg = @config[:providers]
1152
+ return unless providers_cfg.is_a?(Hash)
1153
+ providers_cfg.each do |prov_name, prov_cfg|
1154
+ next unless prov_cfg.is_a?(Hash)
1155
+ mf = prov_cfg[:model_family]
1156
+ # Normalize and write back only if different to avoid unnecessary YAML churn
1157
+ normalized = normalize_model_family(mf)
1158
+ prov_cfg[:model_family] = normalized
938
1159
  end
939
1160
  end
940
1161
 
data/lib/aidp/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Aidp
4
- VERSION = "0.19.1"
4
+ VERSION = "0.21.0"
5
5
  end
@@ -209,6 +209,7 @@ module Aidp
209
209
  number: raw["number"],
210
210
  title: raw["title"],
211
211
  body: raw["body"] || "",
212
+ author: raw.dig("author", "login") || raw["author"],
212
213
  comments: Array(raw["comments"]).map { |comment| normalize_comment(comment) },
213
214
  labels: Array(raw["labels"]).map { |label| label.is_a?(Hash) ? label["name"] : label },
214
215
  state: raw["state"],
@@ -223,6 +224,7 @@ module Aidp
223
224
  number: raw["number"],
224
225
  title: raw["title"],
225
226
  body: raw["body"] || "",
227
+ author: raw.dig("user", "login"),
226
228
  comments: Array(raw["comments"]).map { |comment| normalize_comment(comment) },
227
229
  labels: Array(raw["labels"]).map { |label| label["name"] },
228
230
  state: raw["state"],