ruby_reactor 0.8.5 → 0.8.6
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.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/.specify/feature.json +1 -1
- data/.specify/memory/constitution.md +17 -14
- data/CHANGELOG.md +58 -0
- data/CLAUDE.md +1 -1
- data/README.md +23 -8
- data/lib/ruby_reactor/adapters/active_job/map_element_rollback_worker.rb +20 -0
- data/lib/ruby_reactor/adapters/active_job/router.rb +32 -4
- data/lib/ruby_reactor/adapters/sidekiq/map_element_rollback_worker.rb +16 -0
- data/lib/ruby_reactor/adapters/sidekiq/router.rb +32 -4
- data/lib/ruby_reactor/context.rb +14 -1
- data/lib/ruby_reactor/dsl/definition_warnings.rb +30 -0
- data/lib/ruby_reactor/dsl/map_builder.rb +27 -7
- data/lib/ruby_reactor/dsl/step_builder.rb +8 -21
- data/lib/ruby_reactor/error/rescuable.rb +6 -1
- data/lib/ruby_reactor/error/rollback_handed_off.rb +27 -0
- data/lib/ruby_reactor/executor/compensation_manager.rb +33 -9
- data/lib/ruby_reactor/executor/result_handler.rb +72 -52
- data/lib/ruby_reactor/executor/retry_manager.rb +4 -3
- data/lib/ruby_reactor/executor/step_executor.rb +3 -1
- data/lib/ruby_reactor/executor.rb +186 -6
- data/lib/ruby_reactor/map/collector.rb +25 -93
- data/lib/ruby_reactor/map/dispatcher.rb +38 -7
- data/lib/ruby_reactor/map/element_executor.rb +14 -10
- data/lib/ruby_reactor/map/element_rollback.rb +158 -0
- data/lib/ruby_reactor/map/helpers.rb +11 -155
- data/lib/ruby_reactor/map/step_rollback.rb +136 -0
- data/lib/ruby_reactor/map/sweeper.rb +66 -8
- data/lib/ruby_reactor/map.rb +17 -0
- data/lib/ruby_reactor/max_retries_exhausted_failure.rb +4 -2
- data/lib/ruby_reactor/reactor.rb +41 -2
- data/lib/ruby_reactor/rspec/active_job_helpers.rb +13 -2
- data/lib/ruby_reactor/rspec/matchers.rb +40 -21
- data/lib/ruby_reactor/rspec/sidekiq_helpers.rb +7 -3
- data/lib/ruby_reactor/rspec/test_subject.rb +13 -3
- data/lib/ruby_reactor/rspec.rb +3 -6
- data/lib/ruby_reactor/step/compose_step.rb +3 -0
- data/lib/ruby_reactor/step/map_step.rb +117 -114
- data/lib/ruby_reactor/storage/adapter.rb +14 -0
- data/lib/ruby_reactor/storage/redis_adapter.rb +18 -2
- data/lib/ruby_reactor/storage/redis_map_rollback.rb +153 -0
- data/lib/ruby_reactor/storage/redis_reactor_scan.rb +1 -1
- data/lib/ruby_reactor/sweeper.rb +3 -1
- data/lib/ruby_reactor/version.rb +1 -1
- data/lib/ruby_reactor/web/api.rb +11 -1
- data/lib/ruby_reactor/web/public/assets/{index-CQbgHtd0.js → index-C6ejszCQ.js} +12 -12
- data/lib/ruby_reactor/web/public/assets/index-DM5darhy.css +1 -0
- data/lib/ruby_reactor/web/public/index.html +2 -2
- data/lib/ruby_reactor/worker.rb +4 -2
- data/specs/007-execution-flow-analysis/evidence/harness.rb +2 -0
- data/specs/007-execution-flow-analysis/evidence/probes/03_map.rb +4 -1
- data/specs/009-distributed-map-undo/checklists/requirements.md +53 -0
- data/specs/009-distributed-map-undo/contracts/api-surface.md +125 -0
- data/specs/009-distributed-map-undo/contracts/rollback-protocol.md +157 -0
- data/specs/009-distributed-map-undo/data-model.md +151 -0
- data/specs/009-distributed-map-undo/plan.md +323 -0
- data/specs/009-distributed-map-undo/pr-description.md +78 -0
- data/specs/009-distributed-map-undo/quickstart.md +154 -0
- data/specs/009-distributed-map-undo/research.md +585 -0
- data/specs/009-distributed-map-undo/spec.md +360 -0
- data/specs/009-distributed-map-undo/tasks.md +1060 -0
- data/specs/future_improvements.md +18 -30
- metadata +21 -3
- data/lib/ruby_reactor/web/public/assets/index-BQvIWPdx.css +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap";@layer properties{@supports ((-webkit-hyphens:none) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial}}}@layer theme{:root,:host{--font-sans:"Inter",system-ui,sans-serif;--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-950:oklch(25.8% .092 26.042);--color-orange-400:oklch(75% .183 55.934);--color-orange-500:oklch(70.5% .213 47.604);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-600:oklch(59.6% .145 163.225);--color-teal-400:oklch(77.7% .152 181.912);--color-teal-500:oklch(70.4% .14 182.503);--color-cyan-400:oklch(78.9% .154 211.53);--color-sky-400:oklch(74.6% .16 232.661);--color-sky-500:oklch(68.5% .169 237.323);--color-sky-800:oklch(44.3% .11 240.79);--color-sky-900:oklch(39.1% .09 240.876);--color-indigo-300:oklch(78.5% .115 274.713);--color-indigo-400:oklch(67.3% .182 276.935);--color-indigo-500:#6366f1;--color-indigo-600:oklch(51.1% .262 276.966);--color-violet-400:oklch(70.2% .183 293.541);--color-violet-500:oklch(60.6% .25 292.717);--color-rose-300:oklch(81% .117 11.638);--color-rose-400:oklch(71.2% .194 13.428);--color-rose-500:oklch(64.5% .246 16.439);--color-slate-50:oklch(98.4% .003 247.858);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-400:oklch(70.4% .04 256.788);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-800:#1e293b;--color-slate-900:#0f172a;--color-slate-950:#020617;--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-tight:1.25;--leading-relaxed:1.625;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite;--blur-sm:8px;--blur-md:12px;--blur-xl:24px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::-moz-placeholder{opacity:1}::placeholder{opacity:1}@supports (not (-webkit-appearance:-apple-pay-button)) or (contain-intrinsic-size:1px){::-moz-placeholder{color:currentColor}::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::-moz-placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.relative{position:relative}.static{position:static}.sticky{position:sticky}.top-0{top:calc(var(--spacing)*0)}.top-1\/2{top:50%}.-left-2{left:calc(var(--spacing)*-2)}.left-0{left:calc(var(--spacing)*0)}.left-3{left:calc(var(--spacing)*3)}.isolate{isolation:isolate}.z-50{z-index:50}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.m-0{margin:calc(var(--spacing)*0)}.-mx-4{margin-inline:calc(var(--spacing)*-4)}.mx-auto{margin-inline:auto}.mt-0\.5{margin-top:calc(var(--spacing)*.5)}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-1\.5{margin-top:calc(var(--spacing)*1.5)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-3{margin-top:calc(var(--spacing)*3)}.mt-4{margin-top:calc(var(--spacing)*4)}.mb-1\.5{margin-bottom:calc(var(--spacing)*1.5)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.ml-3{margin-left:calc(var(--spacing)*3)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.inline-flex{display:inline-flex}.\!h-2{height:calc(var(--spacing)*2)!important}.h-0{height:calc(var(--spacing)*0)}.h-1\.5{height:calc(var(--spacing)*1.5)}.h-2\.5{height:calc(var(--spacing)*2.5)}.h-3{height:calc(var(--spacing)*3)}.h-4{height:calc(var(--spacing)*4)}.h-5{height:calc(var(--spacing)*5)}.h-6{height:calc(var(--spacing)*6)}.h-16{height:calc(var(--spacing)*16)}.h-24{height:calc(var(--spacing)*24)}.h-40{height:calc(var(--spacing)*40)}.h-\[calc\(100vh-8rem\)\]{height:calc(100vh - 8rem)}.h-full{height:100%}.h-px{height:1px}.max-h-48{max-height:calc(var(--spacing)*48)}.max-h-\[300px\]{max-height:300px}.min-h-0{min-height:calc(var(--spacing)*0)}.min-h-\[500px\]{min-height:500px}.min-h-screen{min-height:100vh}.\!w-2{width:calc(var(--spacing)*2)!important}.w-1{width:calc(var(--spacing)*1)}.w-2\.5{width:calc(var(--spacing)*2.5)}.w-3{width:calc(var(--spacing)*3)}.w-4{width:calc(var(--spacing)*4)}.w-5{width:calc(var(--spacing)*5)}.w-6{width:calc(var(--spacing)*6)}.w-10{width:calc(var(--spacing)*10)}.w-28{width:calc(var(--spacing)*28)}.w-full{width:100%}.max-w-7xl{max-width:var(--container-7xl)}.max-w-full{max-width:100%}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:calc(var(--spacing)*0)}.min-w-\[150px\]{min-width:150px}.flex-1{flex:1}.shrink-0{flex-shrink:0}.-translate-y-1\/2{--tw-translate-y: -50% ;translate:var(--tw-translate-x)var(--tw-translate-y)}.scale-105{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x)var(--tw-scale-y)}.animate-pulse{animation:var(--animate-pulse)}.cursor-pointer{cursor:pointer}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing)*1)}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-6{gap:calc(var(--spacing)*6)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1.5)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1.5)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*8)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*8)*calc(1 - var(--tw-space-y-reverse)))}.gap-x-4{-moz-column-gap:calc(var(--spacing)*4);column-gap:calc(var(--spacing)*4)}.gap-y-1{row-gap:calc(var(--spacing)*1)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px*var(--tw-divide-y-reverse));border-bottom-width:calc(1px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-slate-800\/50>:not(:last-child)){border-color:#1e293b80}@supports (color:color-mix(in lab,red,red)){:where(.divide-slate-800\/50>:not(:last-child)){border-color:color-mix(in oklab,var(--color-slate-800)50%,transparent)}}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-br-lg{border-bottom-right-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-amber-400\/30{border-color:#fcbb004d}@supports (color:color-mix(in lab,red,red)){.border-amber-400\/30{border-color:color-mix(in oklab,var(--color-amber-400)30%,transparent)}}.border-amber-500\/20{border-color:#f99c0033}@supports (color:color-mix(in lab,red,red)){.border-amber-500\/20{border-color:color-mix(in oklab,var(--color-amber-500)20%,transparent)}}.border-amber-500\/50{border-color:#f99c0080}@supports (color:color-mix(in lab,red,red)){.border-amber-500\/50{border-color:color-mix(in oklab,var(--color-amber-500)50%,transparent)}}.border-emerald-500\/20{border-color:#00bb7f33}@supports (color:color-mix(in lab,red,red)){.border-emerald-500\/20{border-color:color-mix(in oklab,var(--color-emerald-500)20%,transparent)}}.border-indigo-500{border-color:var(--color-indigo-500)}.border-indigo-500\/20{border-color:#6366f133}@supports (color:color-mix(in lab,red,red)){.border-indigo-500\/20{border-color:color-mix(in oklab,var(--color-indigo-500)20%,transparent)}}.border-indigo-500\/60{border-color:#6366f199}@supports (color:color-mix(in lab,red,red)){.border-indigo-500\/60{border-color:color-mix(in oklab,var(--color-indigo-500)60%,transparent)}}.border-orange-500\/20{border-color:#fe6e0033}@supports (color:color-mix(in lab,red,red)){.border-orange-500\/20{border-color:color-mix(in oklab,var(--color-orange-500)20%,transparent)}}.border-red-400{border-color:var(--color-red-400)}.border-red-500\/10{border-color:#fb2c361a}@supports (color:color-mix(in lab,red,red)){.border-red-500\/10{border-color:color-mix(in oklab,var(--color-red-500)10%,transparent)}}.border-red-500\/20{border-color:#fb2c3633}@supports (color:color-mix(in lab,red,red)){.border-red-500\/20{border-color:color-mix(in oklab,var(--color-red-500)20%,transparent)}}.border-rose-500{border-color:var(--color-rose-500)}.border-rose-500\/10{border-color:#ff23571a}@supports (color:color-mix(in lab,red,red)){.border-rose-500\/10{border-color:color-mix(in oklab,var(--color-rose-500)10%,transparent)}}.border-rose-500\/20{border-color:#ff235733}@supports (color:color-mix(in lab,red,red)){.border-rose-500\/20{border-color:color-mix(in oklab,var(--color-rose-500)20%,transparent)}}.border-rose-500\/60{border-color:#ff235799}@supports (color:color-mix(in lab,red,red)){.border-rose-500\/60{border-color:color-mix(in oklab,var(--color-rose-500)60%,transparent)}}.border-sky-500{border-color:var(--color-sky-500)}.border-sky-500\/20{border-color:#00a5ef33}@supports (color:color-mix(in lab,red,red)){.border-sky-500\/20{border-color:color-mix(in oklab,var(--color-sky-500)20%,transparent)}}.border-sky-800\/40{border-color:#00598666}@supports (color:color-mix(in lab,red,red)){.border-sky-800\/40{border-color:color-mix(in oklab,var(--color-sky-800)40%,transparent)}}.border-slate-400{border-color:var(--color-slate-400)}.border-slate-500{border-color:var(--color-slate-500)}.border-slate-600\/50{border-color:#45556c80}@supports (color:color-mix(in lab,red,red)){.border-slate-600\/50{border-color:color-mix(in oklab,var(--color-slate-600)50%,transparent)}}.border-slate-700{border-color:var(--color-slate-700)}.border-slate-800{border-color:var(--color-slate-800)}.border-slate-800\/50{border-color:#1e293b80}@supports (color:color-mix(in lab,red,red)){.border-slate-800\/50{border-color:color-mix(in oklab,var(--color-slate-800)50%,transparent)}}.border-slate-800\/60{border-color:#1e293b99}@supports (color:color-mix(in lab,red,red)){.border-slate-800\/60{border-color:color-mix(in oklab,var(--color-slate-800)60%,transparent)}}.border-teal-500{border-color:var(--color-teal-500)}.border-teal-500\/20{border-color:#00baa733}@supports (color:color-mix(in lab,red,red)){.border-teal-500\/20{border-color:color-mix(in oklab,var(--color-teal-500)20%,transparent)}}.border-teal-500\/60{border-color:#00baa799}@supports (color:color-mix(in lab,red,red)){.border-teal-500\/60{border-color:color-mix(in oklab,var(--color-teal-500)60%,transparent)}}.border-white\/5{border-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.border-white\/5{border-color:color-mix(in oklab,var(--color-white)5%,transparent)}}.\!bg-slate-500{background-color:var(--color-slate-500)!important}.\!bg-transparent{background-color:#0000!important}.bg-amber-500\/5{background-color:#f99c000d}@supports (color:color-mix(in lab,red,red)){.bg-amber-500\/5{background-color:color-mix(in oklab,var(--color-amber-500)5%,transparent)}}.bg-amber-500\/10{background-color:#f99c001a}@supports (color:color-mix(in lab,red,red)){.bg-amber-500\/10{background-color:color-mix(in oklab,var(--color-amber-500)10%,transparent)}}.bg-black\/30{background-color:#0000004d}@supports (color:color-mix(in lab,red,red)){.bg-black\/30{background-color:color-mix(in oklab,var(--color-black)30%,transparent)}}.bg-emerald-500\/5{background-color:#00bb7f0d}@supports (color:color-mix(in lab,red,red)){.bg-emerald-500\/5{background-color:color-mix(in oklab,var(--color-emerald-500)5%,transparent)}}.bg-emerald-500\/10{background-color:#00bb7f1a}@supports (color:color-mix(in lab,red,red)){.bg-emerald-500\/10{background-color:color-mix(in oklab,var(--color-emerald-500)10%,transparent)}}.bg-emerald-600{background-color:var(--color-emerald-600)}.bg-indigo-500{background-color:var(--color-indigo-500)}.bg-indigo-500\/10{background-color:#6366f11a}@supports (color:color-mix(in lab,red,red)){.bg-indigo-500\/10{background-color:color-mix(in oklab,var(--color-indigo-500)10%,transparent)}}.bg-indigo-600{background-color:var(--color-indigo-600)}.bg-orange-500\/10{background-color:#fe6e001a}@supports (color:color-mix(in lab,red,red)){.bg-orange-500\/10{background-color:color-mix(in oklab,var(--color-orange-500)10%,transparent)}}.bg-red-500\/10{background-color:#fb2c361a}@supports (color:color-mix(in lab,red,red)){.bg-red-500\/10{background-color:color-mix(in oklab,var(--color-red-500)10%,transparent)}}.bg-red-500\/15{background-color:#fb2c3626}@supports (color:color-mix(in lab,red,red)){.bg-red-500\/15{background-color:color-mix(in oklab,var(--color-red-500)15%,transparent)}}.bg-red-950\/20{background-color:#46080933}@supports (color:color-mix(in lab,red,red)){.bg-red-950\/20{background-color:color-mix(in oklab,var(--color-red-950)20%,transparent)}}.bg-rose-500\/5{background-color:#ff23570d}@supports (color:color-mix(in lab,red,red)){.bg-rose-500\/5{background-color:color-mix(in oklab,var(--color-rose-500)5%,transparent)}}.bg-rose-500\/10{background-color:#ff23571a}@supports (color:color-mix(in lab,red,red)){.bg-rose-500\/10{background-color:color-mix(in oklab,var(--color-rose-500)10%,transparent)}}.bg-sky-500\/10{background-color:#00a5ef1a}@supports (color:color-mix(in lab,red,red)){.bg-sky-500\/10{background-color:color-mix(in oklab,var(--color-sky-500)10%,transparent)}}.bg-sky-900\/10{background-color:#024a701a}@supports (color:color-mix(in lab,red,red)){.bg-sky-900\/10{background-color:color-mix(in oklab,var(--color-sky-900)10%,transparent)}}.bg-slate-500\/10{background-color:#62748e1a}@supports (color:color-mix(in lab,red,red)){.bg-slate-500\/10{background-color:color-mix(in oklab,var(--color-slate-500)10%,transparent)}}.bg-slate-700\/50{background-color:#31415880}@supports (color:color-mix(in lab,red,red)){.bg-slate-700\/50{background-color:color-mix(in oklab,var(--color-slate-700)50%,transparent)}}.bg-slate-800{background-color:var(--color-slate-800)}.bg-slate-800\/40{background-color:#1e293b66}@supports (color:color-mix(in lab,red,red)){.bg-slate-800\/40{background-color:color-mix(in oklab,var(--color-slate-800)40%,transparent)}}.bg-slate-800\/60{background-color:#1e293b99}@supports (color:color-mix(in lab,red,red)){.bg-slate-800\/60{background-color:color-mix(in oklab,var(--color-slate-800)60%,transparent)}}.bg-slate-900{background-color:var(--color-slate-900)}.bg-slate-900\/30{background-color:#0f172a4d}@supports (color:color-mix(in lab,red,red)){.bg-slate-900\/30{background-color:color-mix(in oklab,var(--color-slate-900)30%,transparent)}}.bg-slate-900\/40{background-color:#0f172a66}@supports (color:color-mix(in lab,red,red)){.bg-slate-900\/40{background-color:color-mix(in oklab,var(--color-slate-900)40%,transparent)}}.bg-slate-900\/50{background-color:#0f172a80}@supports (color:color-mix(in lab,red,red)){.bg-slate-900\/50{background-color:color-mix(in oklab,var(--color-slate-900)50%,transparent)}}.bg-slate-900\/60{background-color:#0f172a99}@supports (color:color-mix(in lab,red,red)){.bg-slate-900\/60{background-color:color-mix(in oklab,var(--color-slate-900)60%,transparent)}}.bg-slate-900\/80{background-color:#0f172acc}@supports (color:color-mix(in lab,red,red)){.bg-slate-900\/80{background-color:color-mix(in oklab,var(--color-slate-900)80%,transparent)}}.bg-slate-950{background-color:var(--color-slate-950)}.bg-slate-950\/50{background-color:#02061780}@supports (color:color-mix(in lab,red,red)){.bg-slate-950\/50{background-color:color-mix(in oklab,var(--color-slate-950)50%,transparent)}}.bg-teal-500\/10{background-color:#00baa71a}@supports (color:color-mix(in lab,red,red)){.bg-teal-500\/10{background-color:color-mix(in oklab,var(--color-teal-500)10%,transparent)}}.bg-transparent{background-color:#0000}.bg-violet-500\/70{background-color:#8d54ffb3}@supports (color:color-mix(in lab,red,red)){.bg-violet-500\/70{background-color:color-mix(in oklab,var(--color-violet-500)70%,transparent)}}.bg-white\/10{background-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.bg-white\/10{background-color:color-mix(in oklab,var(--color-white)10%,transparent)}}.fill-slate-400{fill:var(--color-slate-400)}.\!p-0{padding:calc(var(--spacing)*0)!important}.p-1{padding:calc(var(--spacing)*1)}.p-1\.5{padding:calc(var(--spacing)*1.5)}.p-2{padding:calc(var(--spacing)*2)}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.p-6{padding:calc(var(--spacing)*6)}.p-8{padding:calc(var(--spacing)*8)}.p-12{padding:calc(var(--spacing)*12)}.px-1{padding-inline:calc(var(--spacing)*1)}.px-1\.5{padding-inline:calc(var(--spacing)*1.5)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-2\.5{padding-inline:calc(var(--spacing)*2.5)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-6{padding-inline:calc(var(--spacing)*6)}.py-0\.5{padding-block:calc(var(--spacing)*.5)}.py-1{padding-block:calc(var(--spacing)*1)}.py-1\.5{padding-block:calc(var(--spacing)*1.5)}.py-2{padding-block:calc(var(--spacing)*2)}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.py-8{padding-block:calc(var(--spacing)*8)}.py-24{padding-block:calc(var(--spacing)*24)}.py-px{padding-block:1px}.pt-3{padding-top:calc(var(--spacing)*3)}.pr-2{padding-right:calc(var(--spacing)*2)}.pr-4{padding-right:calc(var(--spacing)*4)}.pr-8{padding-right:calc(var(--spacing)*8)}.pb-3{padding-bottom:calc(var(--spacing)*3)}.pb-4{padding-bottom:calc(var(--spacing)*4)}.pb-6{padding-bottom:calc(var(--spacing)*6)}.pl-2{padding-left:calc(var(--spacing)*2)}.pl-3{padding-left:calc(var(--spacing)*3)}.pl-9{padding-left:calc(var(--spacing)*9)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.font-sans{font-family:var(--font-sans)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.leading-6{--tw-leading:calc(var(--spacing)*6);line-height:calc(var(--spacing)*6)}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.whitespace-pre{white-space:pre}.whitespace-pre-wrap{white-space:pre-wrap}.text-amber-300{color:var(--color-amber-300)}.text-amber-400{color:var(--color-amber-400)}.text-amber-400\/80{color:#fcbb00cc}@supports (color:color-mix(in lab,red,red)){.text-amber-400\/80{color:color-mix(in oklab,var(--color-amber-400)80%,transparent)}}.text-amber-500{color:var(--color-amber-500)}.text-cyan-400{color:var(--color-cyan-400)}.text-emerald-400{color:var(--color-emerald-400)}.text-indigo-400{color:var(--color-indigo-400)}.text-orange-400{color:var(--color-orange-400)}.text-red-300{color:var(--color-red-300)}.text-red-300\/90{color:#ffa3a3e6}@supports (color:color-mix(in lab,red,red)){.text-red-300\/90{color:color-mix(in oklab,var(--color-red-300)90%,transparent)}}.text-red-400{color:var(--color-red-400)}.text-red-400\/30{color:#ff65684d}@supports (color:color-mix(in lab,red,red)){.text-red-400\/30{color:color-mix(in oklab,var(--color-red-400)30%,transparent)}}.text-red-400\/50{color:#ff656880}@supports (color:color-mix(in lab,red,red)){.text-red-400\/50{color:color-mix(in oklab,var(--color-red-400)50%,transparent)}}.text-red-400\/70{color:#ff6568b3}@supports (color:color-mix(in lab,red,red)){.text-red-400\/70{color:color-mix(in oklab,var(--color-red-400)70%,transparent)}}.text-red-400\/80{color:#ff6568cc}@supports (color:color-mix(in lab,red,red)){.text-red-400\/80{color:color-mix(in oklab,var(--color-red-400)80%,transparent)}}.text-red-500{color:var(--color-red-500)}.text-rose-300{color:var(--color-rose-300)}.text-rose-400{color:var(--color-rose-400)}.text-rose-400\/70{color:#ff667fb3}@supports (color:color-mix(in lab,red,red)){.text-rose-400\/70{color:color-mix(in oklab,var(--color-rose-400)70%,transparent)}}.text-rose-400\/80{color:#ff667fcc}@supports (color:color-mix(in lab,red,red)){.text-rose-400\/80{color:color-mix(in oklab,var(--color-rose-400)80%,transparent)}}.text-sky-400{color:var(--color-sky-400)}.text-slate-50{color:var(--color-slate-50)}.text-slate-200{color:var(--color-slate-200)}.text-slate-300{color:var(--color-slate-300)}.text-slate-400{color:var(--color-slate-400)}.text-slate-500{color:var(--color-slate-500)}.text-slate-600{color:var(--color-slate-600)}.text-teal-400{color:var(--color-teal-400)}.text-violet-400{color:var(--color-violet-400)}.text-white{color:var(--color-white)}.capitalize{text-transform:capitalize}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,)var(--tw-slashed-zero,)var(--tw-numeric-figure,)var(--tw-numeric-spacing,)var(--tw-numeric-fraction,)}.underline{text-decoration-line:underline}.placeholder-slate-700::-moz-placeholder{color:var(--color-slate-700)}.placeholder-slate-700::placeholder{color:var(--color-slate-700)}.opacity-0{opacity:0}.opacity-50{opacity:.5}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.shadow-\[0_0_10px_rgba\(14\,165\,233\,0\.15\)\]{--tw-shadow:0 0 10px var(--tw-shadow-color,#0ea5e926);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_10px_rgba\(20\,184\,166\,0\.15\)\]{--tw-shadow:0 0 10px var(--tw-shadow-color,#14b8a626);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_10px_rgba\(99\,102\,241\,0\.15\)\]{--tw-shadow:0 0 10px var(--tw-shadow-color,#6366f126);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_10px_rgba\(244\,63\,94\,0\.15\)\]{--tw-shadow:0 0 10px var(--tw-shadow-color,#f43f5e26);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_15px_rgba\(244\,63\,94\,0\.2\)\]{--tw-shadow:0 0 15px var(--tw-shadow-color,#f43f5e33);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a),0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-black\/20{--tw-shadow-color:#0003}@supports (color:color-mix(in lab,red,red)){.shadow-black\/20{--tw-shadow-color:color-mix(in oklab,color-mix(in oklab,var(--color-black)20%,transparent)var(--tw-shadow-alpha),transparent)}}.shadow-emerald-500\/20{--tw-shadow-color:#00bb7f33}@supports (color:color-mix(in lab,red,red)){.shadow-emerald-500\/20{--tw-shadow-color:color-mix(in oklab,color-mix(in oklab,var(--color-emerald-500)20%,transparent)var(--tw-shadow-alpha),transparent)}}.shadow-indigo-500\/20{--tw-shadow-color:#6366f133}@supports (color:color-mix(in lab,red,red)){.shadow-indigo-500\/20{--tw-shadow-color:color-mix(in oklab,color-mix(in oklab,var(--color-indigo-500)20%,transparent)var(--tw-shadow-alpha),transparent)}}.ring-indigo-500\/20{--tw-ring-color:#6366f133}@supports (color:color-mix(in lab,red,red)){.ring-indigo-500\/20{--tw-ring-color:color-mix(in oklab,var(--color-indigo-500)20%,transparent)}}.ring-indigo-500\/30{--tw-ring-color:#6366f14d}@supports (color:color-mix(in lab,red,red)){.ring-indigo-500\/30{--tw-ring-color:color-mix(in oklab,var(--color-indigo-500)30%,transparent)}}.ring-rose-500\/30{--tw-ring-color:#ff23574d}@supports (color:color-mix(in lab,red,red)){.ring-rose-500\/30{--tw-ring-color:color-mix(in oklab,var(--color-rose-500)30%,transparent)}}.ring-slate-500\/40{--tw-ring-color:#62748e66}@supports (color:color-mix(in lab,red,red)){.ring-slate-500\/40{--tw-ring-color:color-mix(in oklab,var(--color-slate-500)40%,transparent)}}.ring-teal-500\/30{--tw-ring-color:#00baa74d}@supports (color:color-mix(in lab,red,red)){.ring-teal-500\/30{--tw-ring-color:color-mix(in oklab,var(--color-teal-500)30%,transparent)}}.ring-white\/20{--tw-ring-color:#fff3}@supports (color:color-mix(in lab,red,red)){.ring-white\/20{--tw-ring-color:color-mix(in oklab,var(--color-white)20%,transparent)}}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.backdrop-blur-md{--tw-backdrop-blur:blur(var(--blur-md));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-blur-xl{--tw-backdrop-blur:blur(var(--blur-xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}@media(hover:hover){.group-hover\:h-4:is(:where(.group):hover *){height:calc(var(--spacing)*4)}.group-hover\:bg-indigo-500\/20:is(:where(.group):hover *){background-color:#6366f133}@supports (color:color-mix(in lab,red,red)){.group-hover\:bg-indigo-500\/20:is(:where(.group):hover *){background-color:color-mix(in oklab,var(--color-indigo-500)20%,transparent)}}.group-hover\:text-indigo-300:is(:where(.group):hover *){color:var(--color-indigo-300)}.group-hover\:text-indigo-400:is(:where(.group):hover *){color:var(--color-indigo-400)}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}}.selection\:bg-indigo-500\/30 ::-moz-selection{background-color:#6366f14d}.selection\:bg-indigo-500\/30 ::selection{background-color:#6366f14d}@supports (color:color-mix(in lab,red,red)){.selection\:bg-indigo-500\/30 ::-moz-selection{background-color:color-mix(in oklab,var(--color-indigo-500)30%,transparent)}.selection\:bg-indigo-500\/30 ::selection{background-color:color-mix(in oklab,var(--color-indigo-500)30%,transparent)}}.selection\:bg-indigo-500\/30::-moz-selection{background-color:#6366f14d}.selection\:bg-indigo-500\/30::selection{background-color:#6366f14d}@supports (color:color-mix(in lab,red,red)){.selection\:bg-indigo-500\/30::-moz-selection{background-color:color-mix(in oklab,var(--color-indigo-500)30%,transparent)}.selection\:bg-indigo-500\/30::selection{background-color:color-mix(in oklab,var(--color-indigo-500)30%,transparent)}}.placeholder\:text-slate-600::-moz-placeholder{color:var(--color-slate-600)}.placeholder\:text-slate-600::placeholder{color:var(--color-slate-600)}@media(hover:hover){.hover\:-translate-y-0\.5:hover{--tw-translate-y:calc(var(--spacing)*-.5);translate:var(--tw-translate-x)var(--tw-translate-y)}.hover\:border-slate-600:hover{border-color:var(--color-slate-600)}.hover\:bg-emerald-500:hover{background-color:var(--color-emerald-500)}.hover\:bg-indigo-500:hover{background-color:var(--color-indigo-500)}.hover\:bg-indigo-500\/10:hover{background-color:#6366f11a}@supports (color:color-mix(in lab,red,red)){.hover\:bg-indigo-500\/10:hover{background-color:color-mix(in oklab,var(--color-indigo-500)10%,transparent)}}.hover\:bg-rose-500\/10:hover{background-color:#ff23571a}@supports (color:color-mix(in lab,red,red)){.hover\:bg-rose-500\/10:hover{background-color:color-mix(in oklab,var(--color-rose-500)10%,transparent)}}.hover\:bg-slate-800:hover{background-color:var(--color-slate-800)}.hover\:bg-slate-800\/30:hover{background-color:#1e293b4d}@supports (color:color-mix(in lab,red,red)){.hover\:bg-slate-800\/30:hover{background-color:color-mix(in oklab,var(--color-slate-800)30%,transparent)}}.hover\:bg-slate-800\/40:hover{background-color:#1e293b66}@supports (color:color-mix(in lab,red,red)){.hover\:bg-slate-800\/40:hover{background-color:color-mix(in oklab,var(--color-slate-800)40%,transparent)}}.hover\:bg-white\/5:hover{background-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.hover\:bg-white\/5:hover{background-color:color-mix(in oklab,var(--color-white)5%,transparent)}}.hover\:bg-white\/10:hover{background-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.hover\:bg-white\/10:hover{background-color:color-mix(in oklab,var(--color-white)10%,transparent)}}.hover\:text-indigo-300:hover{color:var(--color-indigo-300)}.hover\:text-indigo-400:hover{color:var(--color-indigo-400)}.hover\:text-red-400:hover{color:var(--color-red-400)}.hover\:text-white:hover{color:var(--color-white)}.hover\:opacity-100:hover{opacity:1}}.focus\:border-emerald-500\/50:focus{border-color:#00bb7f80}@supports (color:color-mix(in lab,red,red)){.focus\:border-emerald-500\/50:focus{border-color:color-mix(in oklab,var(--color-emerald-500)50%,transparent)}}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-emerald-500\/50:focus{--tw-ring-color:#00bb7f80}@supports (color:color-mix(in lab,red,red)){.focus\:ring-emerald-500\/50:focus{--tw-ring-color:color-mix(in oklab,var(--color-emerald-500)50%,transparent)}}.focus\:ring-indigo-500\/50:focus{--tw-ring-color:#6366f180}@supports (color:color-mix(in lab,red,red)){.focus\:ring-indigo-500\/50:focus{--tw-ring-color:color-mix(in oklab,var(--color-indigo-500)50%,transparent)}}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media(min-width:40rem){.sm\:w-64{width:calc(var(--spacing)*64)}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:px-6{padding-inline:calc(var(--spacing)*6)}}@media(min-width:64rem){.lg\:col-span-2{grid-column:span 2/span 2}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:px-8{padding-inline:calc(var(--spacing)*8)}}}body{background-color:var(--color-slate-950);font-family:var(--font-sans);color:var(--color-slate-50);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@keyframes pulse{50%{opacity:.5}}.react-flow{direction:ltr;--xy-edge-stroke-default: #b1b1b7;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #555;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(255, 255, 255, .5);--xy-minimap-background-color-default: #fff;--xy-minimap-mask-background-color-default: rgba(240, 240, 240, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #e2e2e2;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: transparent;--xy-background-pattern-dots-color-default: #91919a;--xy-background-pattern-lines-color-default: #eee;--xy-background-pattern-cross-color-default: #e2e2e2;background-color:var(--xy-background-color, var(--xy-background-color-default));--xy-node-color-default: inherit;--xy-node-border-default: 1px solid #1a192b;--xy-node-background-color-default: #fff;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(0, 0, 0, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #1a192b;--xy-node-border-radius-default: 3px;--xy-handle-background-color-default: #1a192b;--xy-handle-border-color-default: #fff;--xy-selection-background-color-default: rgba(0, 89, 220, .08);--xy-selection-border-default: 1px dotted rgba(0, 89, 220, .8);--xy-controls-button-background-color-default: #fefefe;--xy-controls-button-background-color-hover-default: #f4f4f4;--xy-controls-button-color-default: inherit;--xy-controls-button-color-hover-default: inherit;--xy-controls-button-border-color-default: #eee;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #ffffff;--xy-edge-label-color-default: inherit;--xy-resize-background-color-default: #3367d9}.react-flow.dark{--xy-edge-stroke-default: #3e3e3e;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #727272;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(150, 150, 150, .25);--xy-minimap-background-color-default: #141414;--xy-minimap-mask-background-color-default: rgba(60, 60, 60, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #2b2b2b;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: #141414;--xy-background-pattern-dots-color-default: #777;--xy-background-pattern-lines-color-default: #777;--xy-background-pattern-cross-color-default: #777;--xy-node-color-default: #f8f8f8;--xy-node-border-default: 1px solid #3c3c3c;--xy-node-background-color-default: #1e1e1e;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(255, 255, 255, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #999;--xy-handle-background-color-default: #bebebe;--xy-handle-border-color-default: #1e1e1e;--xy-selection-background-color-default: rgba(200, 200, 220, .08);--xy-selection-border-default: 1px dotted rgba(200, 200, 220, .8);--xy-controls-button-background-color-default: #2b2b2b;--xy-controls-button-background-color-hover-default: #3e3e3e;--xy-controls-button-color-default: #f8f8f8;--xy-controls-button-color-hover-default: #fff;--xy-controls-button-border-color-default: #5b5b5b;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #141414;--xy-edge-label-color-default: #f8f8f8}.react-flow__background{background-color:var(--xy-background-color-props, var(--xy-background-color, var(--xy-background-color-default)));pointer-events:none;z-index:-1}.react-flow__container{position:absolute;width:100%;height:100%;top:0;left:0}.react-flow__pane{z-index:1}.react-flow__pane.draggable{cursor:grab}.react-flow__pane.dragging{cursor:grabbing}.react-flow__pane.selection{cursor:pointer}.react-flow__viewport{transform-origin:0 0;z-index:2;pointer-events:none}.react-flow__renderer{z-index:4}.react-flow__selection{z-index:6}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible{outline:none}.react-flow__edge-path{stroke:var(--xy-edge-stroke, var(--xy-edge-stroke-default));stroke-width:var(--xy-edge-stroke-width, var(--xy-edge-stroke-width-default));fill:none}.react-flow__connection-path{stroke:var(--xy-connectionline-stroke, var(--xy-connectionline-stroke-default));stroke-width:var(--xy-connectionline-stroke-width, var(--xy-connectionline-stroke-width-default));fill:none}.react-flow .react-flow__edges{position:absolute}.react-flow .react-flow__edges svg{overflow:visible;position:absolute;pointer-events:none}.react-flow__edge{pointer-events:visibleStroke}.react-flow__edge.selectable{cursor:pointer}.react-flow__edge.animated path{stroke-dasharray:5;animation:dashdraw .5s linear infinite}.react-flow__edge.animated path.react-flow__edge-interaction{stroke-dasharray:none;animation:none}.react-flow__edge.inactive{pointer-events:none}.react-flow__edge.selected,.react-flow__edge:focus,.react-flow__edge:focus-visible{outline:none}.react-flow__edge.selected .react-flow__edge-path,.react-flow__edge.selectable:focus .react-flow__edge-path,.react-flow__edge.selectable:focus-visible .react-flow__edge-path{stroke:var(--xy-edge-stroke-selected, var(--xy-edge-stroke-selected-default))}.react-flow__edge-textwrapper{pointer-events:all}.react-flow__edge .react-flow__edge-text{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__arrowhead polyline{stroke:var(--xy-edge-stroke, var(--xy-edge-stroke-default))}.react-flow__arrowhead polyline.arrowclosed{fill:var(--xy-edge-stroke, var(--xy-edge-stroke-default))}.react-flow__connection{pointer-events:none}.react-flow__connection .animated{stroke-dasharray:5;animation:dashdraw .5s linear infinite}svg.react-flow__connectionline{z-index:1001;overflow:visible;position:absolute}.react-flow__nodes{pointer-events:none;transform-origin:0 0}.react-flow__node{position:absolute;-webkit-user-select:none;-moz-user-select:none;user-select:none;pointer-events:all;transform-origin:0 0;box-sizing:border-box;cursor:default}.react-flow__node.selectable{cursor:pointer}.react-flow__node.draggable{cursor:grab;pointer-events:all}.react-flow__node.draggable.dragging{cursor:grabbing}.react-flow__nodesselection{z-index:3;transform-origin:left top;pointer-events:none}.react-flow__nodesselection-rect{position:absolute;pointer-events:all;cursor:grab}.react-flow__handle{position:absolute;pointer-events:none;min-width:5px;min-height:5px;width:6px;height:6px;background-color:var(--xy-handle-background-color, var(--xy-handle-background-color-default));border:1px solid var(--xy-handle-border-color, var(--xy-handle-border-color-default));border-radius:100%}.react-flow__handle.connectingfrom{pointer-events:all}.react-flow__handle.connectionindicator{pointer-events:all;cursor:crosshair}.react-flow__handle-bottom{top:auto;left:50%;bottom:0;transform:translate(-50%,50%)}.react-flow__handle-top{top:0;left:50%;transform:translate(-50%,-50%)}.react-flow__handle-left{top:50%;left:0;transform:translate(-50%,-50%)}.react-flow__handle-right{top:50%;right:0;transform:translate(50%,-50%)}.react-flow__edgeupdater{cursor:move;pointer-events:all}.react-flow__pane.selection .react-flow__panel{pointer-events:none}.react-flow__panel{position:absolute;z-index:5;margin:15px}.react-flow__panel.top{top:0}.react-flow__panel.bottom{bottom:0}.react-flow__panel.top.center,.react-flow__panel.bottom.center{left:50%;transform:translate(-15px) translate(-50%)}.react-flow__panel.left{left:0}.react-flow__panel.right{right:0}.react-flow__panel.left.center,.react-flow__panel.right.center{top:50%;transform:translateY(-15px) translateY(-50%)}.react-flow__attribution{font-size:10px;background:var(--xy-attribution-background-color, var(--xy-attribution-background-color-default));padding:2px 3px;margin:0}.react-flow__attribution a{text-decoration:none;color:#999}@keyframes dashdraw{0%{stroke-dashoffset:10}}.react-flow__edgelabel-renderer{position:absolute;width:100%;height:100%;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;left:0;top:0}.react-flow__viewport-portal{position:absolute;width:100%;height:100%;left:0;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__minimap{background:var( --xy-minimap-background-color-props, var(--xy-minimap-background-color, var(--xy-minimap-background-color-default)) )}.react-flow__minimap-svg{display:block}.react-flow__minimap-mask{fill:var( --xy-minimap-mask-background-color-props, var(--xy-minimap-mask-background-color, var(--xy-minimap-mask-background-color-default)) );stroke:var( --xy-minimap-mask-stroke-color-props, var(--xy-minimap-mask-stroke-color, var(--xy-minimap-mask-stroke-color-default)) );stroke-width:var( --xy-minimap-mask-stroke-width-props, var(--xy-minimap-mask-stroke-width, var(--xy-minimap-mask-stroke-width-default)) )}.react-flow__minimap-node{fill:var( --xy-minimap-node-background-color-props, var(--xy-minimap-node-background-color, var(--xy-minimap-node-background-color-default)) );stroke:var( --xy-minimap-node-stroke-color-props, var(--xy-minimap-node-stroke-color, var(--xy-minimap-node-stroke-color-default)) );stroke-width:var( --xy-minimap-node-stroke-width-props, var(--xy-minimap-node-stroke-width, var(--xy-minimap-node-stroke-width-default)) )}.react-flow__background-pattern.dots{fill:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-dots-color-default)) )}.react-flow__background-pattern.lines{stroke:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-lines-color-default)) )}.react-flow__background-pattern.cross{stroke:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-cross-color-default)) )}.react-flow__controls{display:flex;flex-direction:column;box-shadow:var(--xy-controls-box-shadow, var(--xy-controls-box-shadow-default))}.react-flow__controls.horizontal{flex-direction:row}.react-flow__controls-button{display:flex;justify-content:center;align-items:center;height:26px;width:26px;padding:4px;border:none;background:var(--xy-controls-button-background-color, var(--xy-controls-button-background-color-default));border-bottom:1px solid var( --xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)) );color:var( --xy-controls-button-color-props, var(--xy-controls-button-color, var(--xy-controls-button-color-default)) );cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__controls-button svg{width:100%;max-width:12px;max-height:12px;fill:currentColor}.react-flow__edge.updating .react-flow__edge-path{stroke:#777}.react-flow__edge-text{font-size:10px}.react-flow__node.selectable:focus,.react-flow__node.selectable:focus-visible{outline:none}.react-flow__node-input,.react-flow__node-default,.react-flow__node-output,.react-flow__node-group{padding:10px;border-radius:var(--xy-node-border-radius, var(--xy-node-border-radius-default));width:150px;font-size:12px;color:var(--xy-node-color, var(--xy-node-color-default));text-align:center;border:var(--xy-node-border, var(--xy-node-border-default));background-color:var(--xy-node-background-color, var(--xy-node-background-color-default))}.react-flow__node-input.selectable:hover,.react-flow__node-default.selectable:hover,.react-flow__node-output.selectable:hover,.react-flow__node-group.selectable:hover{box-shadow:var(--xy-node-boxshadow-hover, var(--xy-node-boxshadow-hover-default))}.react-flow__node-input.selectable.selected,.react-flow__node-input.selectable:focus,.react-flow__node-input.selectable:focus-visible,.react-flow__node-default.selectable.selected,.react-flow__node-default.selectable:focus,.react-flow__node-default.selectable:focus-visible,.react-flow__node-output.selectable.selected,.react-flow__node-output.selectable:focus,.react-flow__node-output.selectable:focus-visible,.react-flow__node-group.selectable.selected,.react-flow__node-group.selectable:focus,.react-flow__node-group.selectable:focus-visible{box-shadow:var(--xy-node-boxshadow-selected, var(--xy-node-boxshadow-selected-default))}.react-flow__node-group{background-color:var(--xy-node-group-background-color, var(--xy-node-group-background-color-default))}.react-flow__nodesselection-rect,.react-flow__selection{background:var(--xy-selection-background-color, var(--xy-selection-background-color-default));border:var(--xy-selection-border, var(--xy-selection-border-default))}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible,.react-flow__selection:focus,.react-flow__selection:focus-visible{outline:none}.react-flow__controls-button:hover{background:var( --xy-controls-button-background-color-hover-props, var(--xy-controls-button-background-color-hover, var(--xy-controls-button-background-color-hover-default)) );color:var( --xy-controls-button-color-hover-props, var(--xy-controls-button-color-hover, var(--xy-controls-button-color-hover-default)) )}.react-flow__controls-button:disabled{pointer-events:none}.react-flow__controls-button:disabled svg{fill-opacity:.4}.react-flow__controls-button:last-child{border-bottom:none}.react-flow__controls.horizontal .react-flow__controls-button{border-bottom:none;border-right:1px solid var( --xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)) )}.react-flow__controls.horizontal .react-flow__controls-button:last-child{border-right:none}.react-flow__resize-control{position:absolute}.react-flow__resize-control.left,.react-flow__resize-control.right{cursor:ew-resize}.react-flow__resize-control.top,.react-flow__resize-control.bottom{cursor:ns-resize}.react-flow__resize-control.top.left,.react-flow__resize-control.bottom.right{cursor:nwse-resize}.react-flow__resize-control.bottom.left,.react-flow__resize-control.top.right{cursor:nesw-resize}.react-flow__resize-control.handle{width:5px;height:5px;border:1px solid #fff;border-radius:1px;background-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));translate:-50% -50%}.react-flow__resize-control.handle.left{left:0;top:50%}.react-flow__resize-control.handle.right{left:100%;top:50%}.react-flow__resize-control.handle.top{left:50%;top:0}.react-flow__resize-control.handle.bottom{left:50%;top:100%}.react-flow__resize-control.handle.top.left,.react-flow__resize-control.handle.bottom.left{left:0}.react-flow__resize-control.handle.top.right,.react-flow__resize-control.handle.bottom.right{left:100%}.react-flow__resize-control.line{border-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));border-width:0;border-style:solid}.react-flow__resize-control.line.left,.react-flow__resize-control.line.right{width:1px;transform:translate(-50%);top:0;height:100%}.react-flow__resize-control.line.left{left:0;border-left-width:1px}.react-flow__resize-control.line.right{left:100%;border-right-width:1px}.react-flow__resize-control.line.top,.react-flow__resize-control.line.bottom{height:1px;transform:translateY(-50%);left:0;width:100%}.react-flow__resize-control.line.top{top:0;border-top-width:1px}.react-flow__resize-control.line.bottom{border-bottom-width:1px;top:100%}.react-flow__edge-textbg{fill:var(--xy-edge-label-background-color, var(--xy-edge-label-background-color-default))}.react-flow__edge-text{fill:var(--xy-edge-label-color, var(--xy-edge-label-color-default))}pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>ui</title>
|
|
8
|
-
<script type="module" crossorigin src="./assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
8
|
+
<script type="module" crossorigin src="./assets/index-C6ejszCQ.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="./assets/index-DM5darhy.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
data/lib/ruby_reactor/worker.rb
CHANGED
|
@@ -119,9 +119,11 @@ module RubyReactor
|
|
|
119
119
|
context.inline_async_execution = true
|
|
120
120
|
|
|
121
121
|
begin
|
|
122
|
-
# Resume execution from the failed step
|
|
122
|
+
# Resume execution from the failed step — or, for a run whose rollback
|
|
123
|
+
# handed off at a fan-out map, finish that rollback (009 R-04).
|
|
123
124
|
executor = Executor.new(context.reactor_class, {}, context)
|
|
124
|
-
|
|
125
|
+
rolling_back = (data["status"] || data[:status]).to_s == "rolling_back"
|
|
126
|
+
rolling_back ? executor.resume_rollback : executor.resume_execution
|
|
125
127
|
# No explicit save here: resume_execution's ensure block already persists
|
|
126
128
|
# the final root state (`save_context unless skip_context_persist?`), and
|
|
127
129
|
# in the worker the executor's context IS the root, so an extra checkpoint!
|
|
@@ -25,6 +25,8 @@ module Probe
|
|
|
25
25
|
RubyReactor::Adapters::Sidekiq::Worker,
|
|
26
26
|
RubyReactor::Adapters::Sidekiq::MapElementWorker,
|
|
27
27
|
RubyReactor::Adapters::Sidekiq::MapCollectorWorker,
|
|
28
|
+
# 009 R-04: a fan-out map rolls back one element per job.
|
|
29
|
+
RubyReactor::Adapters::Sidekiq::MapElementRollbackWorker,
|
|
28
30
|
RubyReactor::Adapters::Sidekiq::StepWorker
|
|
29
31
|
].freeze
|
|
30
32
|
|
|
@@ -222,10 +222,13 @@ Probe.scenario "S-map-11", "a → map(inline; elem 1 returns Halt) → b",
|
|
|
222
222
|
P::Map11.run({ items: [0, 1, 2] })
|
|
223
223
|
end
|
|
224
224
|
|
|
225
|
+
# 009 R-08: element 0 is now undone by its own rollback job, enqueued once the
|
|
226
|
+
# map settles, so the elements' independent async_step units (queued earlier)
|
|
227
|
+
# run before it. The undone set and the final failure are unchanged.
|
|
225
228
|
Probe.scenario "S-map-12", "a → map(fan_out; element e1 → async_step u, e2; elem 1 fails) → b",
|
|
226
229
|
mode: :worker,
|
|
227
230
|
expected: %w[run:a run:e.e1[0] run:e.e2[0] run:e.e1[1] run:e.e2[1] compensate:e.e2[1]
|
|
228
|
-
undo:e.e1[1]
|
|
231
|
+
undo:e.e1[1] run:e.u[0] run:e.u[1] undo:e.e2[0] undo:e.e1[0] undo:a => failure(m)] do
|
|
229
232
|
result = Probe.run_async(P::Map12, { items: [0, 1] })
|
|
230
233
|
Probe.note("StepWorker jobs left after drain: #{RubyReactor::Adapters::Sidekiq::StepWorker.jobs.size}")
|
|
231
234
|
result
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Specification Quality Checklist: Distributed Map Rollback and Bounded Fan-out
|
|
2
|
+
|
|
3
|
+
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
|
4
|
+
**Created**: 2026-09-30
|
|
5
|
+
**Feature**: [spec.md](../spec.md)
|
|
6
|
+
|
|
7
|
+
## Content Quality
|
|
8
|
+
|
|
9
|
+
- [x] No implementation details (languages, frameworks, APIs)
|
|
10
|
+
- [x] Focused on user value and business needs
|
|
11
|
+
- [x] Written for non-technical stakeholders
|
|
12
|
+
- [x] All mandatory sections completed
|
|
13
|
+
|
|
14
|
+
## Requirement Completeness
|
|
15
|
+
|
|
16
|
+
- [x] No [NEEDS CLARIFICATION] markers remain
|
|
17
|
+
- [x] Requirements are testable and unambiguous
|
|
18
|
+
- [x] Success criteria are measurable
|
|
19
|
+
- [x] Success criteria are technology-agnostic (no implementation details)
|
|
20
|
+
- [x] All acceptance scenarios are defined
|
|
21
|
+
- [x] Edge cases are identified
|
|
22
|
+
- [x] Scope is clearly bounded
|
|
23
|
+
- [x] Dependencies and assumptions identified
|
|
24
|
+
|
|
25
|
+
## Feature Readiness
|
|
26
|
+
|
|
27
|
+
- [x] All functional requirements have clear acceptance criteria
|
|
28
|
+
- [x] User scenarios cover primary flows
|
|
29
|
+
- [x] Feature meets measurable outcomes defined in Success Criteria
|
|
30
|
+
- [x] No implementation details leak into specification
|
|
31
|
+
|
|
32
|
+
## Notes
|
|
33
|
+
|
|
34
|
+
- The product is a developer library, so its stakeholders are developers: DSL names (`fan_out`,
|
|
35
|
+
`batch_size`, `fail_fast`, `atomic`) are the user-facing surface and are kept on purpose.
|
|
36
|
+
The spec names no internal classes, storage keys or job backends.
|
|
37
|
+
- No clarification markers. Defaults taken instead, recorded in Assumptions:
|
|
38
|
+
- `fail_fast` becomes a deprecated alias, not removed (MINOR release);
|
|
39
|
+
- 50 is a fixed library default with no global setting (per-map `batch_size` overrides it);
|
|
40
|
+
- rollback mirrors execution mode (inline maps stay in process, with bounded reads);
|
|
41
|
+
- no ordering guarantee across elements' rollbacks.
|
|
42
|
+
- FR-025/FR-026 (docs, CHANGELOG, demo app) are there because the constitution requires them for
|
|
43
|
+
every feature, not as implementation detail.
|
|
44
|
+
- 2026-09-30, after /speckit-plan: FR-002, FR-006 and FR-008 and SC-001, SC-003 and SC-004
|
|
45
|
+
reworded to match research R-02 (the back-pressure bound per throw), R-07 (the at-least-once
|
|
46
|
+
window for the undo that was cut off) and R-08 (start order). Re-validated: all items still pass.
|
|
47
|
+
- 2026-09-30, after /speckit-analyze. All 27 findings were addressed:
|
|
48
|
+
- spec: US1-AS2, FR-002, FR-003, FR-007, FR-009, FR-018, SC-001, SC-002 and SC-004, the Edge
|
|
49
|
+
Cases and Key Entities were revised, and FR-027 (a single rollback per run) was added;
|
|
50
|
+
- plan, research, data-model, contracts, quickstart and tasks were updated to match;
|
|
51
|
+
- the constitution was amended to 1.3.1 (the worker path).
|
|
52
|
+
|
|
53
|
+
Re-validated: all items still pass, and there are no clarification markers.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Contract: Public API Surface
|
|
2
|
+
|
|
3
|
+
What users of the gem see change. Internal classes are listed only where a custom setup (router,
|
|
4
|
+
worker registration, dashboards) touches them.
|
|
5
|
+
|
|
6
|
+
## Map DSL
|
|
7
|
+
|
|
8
|
+
```ruby
|
|
9
|
+
map :charge_orders, ChargeOrderReactor do
|
|
10
|
+
source result(:orders)
|
|
11
|
+
argument :order, element(:charge_orders)
|
|
12
|
+
|
|
13
|
+
fan_out batch_size: 200 # batch_size optional; 50 when omitted
|
|
14
|
+
atomic # default; `atomic false` returns per-element outcomes
|
|
15
|
+
collect { |results| ... }
|
|
16
|
+
end
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
| Declaration | Behavior | Errors / warnings |
|
|
20
|
+
| --- | --- | --- |
|
|
21
|
+
| `atomic` / `atomic true` | Default. Any element failure fails the map, no new element starts, and every completed element is rolled back. | — |
|
|
22
|
+
| `atomic false` | The map completes with every element's outcome (a `ResultEnumerator` of `Success` / `Failure`). Failed elements roll themselves back; succeeded ones are kept. | — |
|
|
23
|
+
| `fail_fast(x)` | Same as `atomic(x)`. | One line on stderr per call site: `[RubyReactor] DEPRECATION: <file:line> <Reactor> map :<name> declares fail_fast. Use atomic (same meaning). Removal no earlier than the next MAJOR.` |
|
|
24
|
+
| `fail_fast` and `atomic` on one map | — | `RubyReactor::Error::ValidationError` at class definition: "map :<name> declares both fail_fast and atomic; declare only atomic". |
|
|
25
|
+
| `fan_out` (no `batch_size`) | No throw enqueues more than 50 element jobs; the next throw fires when the previous throw's last element finishes. | — |
|
|
26
|
+
| `fan_out batch_size: n` | As today; `n` must be a positive Integer. | `ValidationError` as today. |
|
|
27
|
+
| `batch_size n` on an inline map | No effect, as today. | — |
|
|
28
|
+
|
|
29
|
+
## Rollback behavior (observable)
|
|
30
|
+
|
|
31
|
+
| Situation | Before | After |
|
|
32
|
+
| --- | --- | --- |
|
|
33
|
+
| A fan-out map fails (atomic), or a later step fails, or `Reactor.undo` reaches a completed fan-out map | Every completed element is undone serially, in one process, with all element states loaded at once. | One background job per started element; only completed elements undo anything, the others report `not_needed`. No throw enqueues more than `batch_size`. The run is `rolling_back` until every element has reported, then the steps before the map are undone and the run ends `failed` (or `cancelled` for `undo`). |
|
|
34
|
+
| An inline map is rolled back | In process, all element states loaded at once. | In process, element states read 100 at a time, highest index first. |
|
|
35
|
+
| The final `Failure` | Carries `rollback_failures` with `map_step` / `element_index`. | Identical shape and content. The oracle is the same run with an inline map. |
|
|
36
|
+
| A worker dies mid-element-rollback | Not applicable (inline). | The element resumes after its last recorded undo. The undo that was cut off may run again, so undos must stay idempotent, as the docs already require. |
|
|
37
|
+
|
|
38
|
+
## Statuses
|
|
39
|
+
|
|
40
|
+
| Status | New? | Terminal? | Meaning |
|
|
41
|
+
| --- | --- | --- | --- |
|
|
42
|
+
| `rolling_back` | **new** | no | A rollback handed off at a fan-out map and is waiting for element rollback jobs. |
|
|
43
|
+
|
|
44
|
+
Dashboards and code that filter status strings see this one new value.
|
|
45
|
+
|
|
46
|
+
## `Reactor.undo(id)` / `Reactor#undo` / `Reactor.cancel`
|
|
47
|
+
|
|
48
|
+
| Case | Behavior |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| No fan-out map on the undo path | As today: undone in the caller's process, then `cancel(reason: "Undo triggered")`. |
|
|
51
|
+
| A completed fan-out map on the undo path | The steps after the map are undone in the caller's process. Element rollbacks are dispatched, the run becomes `rolling_back`, and the call returns. A worker finishes the rollback and applies `cancelled`. |
|
|
52
|
+
| The run's `async:` lock is held (a live run or rollback) | Raises `RubyReactor::Lock::AcquisitionError` after waiting up to 5 s. The caller retries. |
|
|
53
|
+
| The run is already `rolling_back` | Raises `RubyReactor::Error::ValidationError`: "rollback already in progress" (FR-027). |
|
|
54
|
+
| `Reactor.cancel(id:, reason:)` on a `rolling_back` run | Raises `RubyReactor::Error::ValidationError`: "rollback in progress; cannot cancel" (FR-027). Cancel on any other status behaves as today. |
|
|
55
|
+
|
|
56
|
+
## Composition
|
|
57
|
+
|
|
58
|
+
A `compose`d child (at any depth) whose steps include a `fan_out` map now works:
|
|
59
|
+
|
|
60
|
+
- the top-level run is resumed when the map settles, and finishes;
|
|
61
|
+
- rollback travels through the top-level run.
|
|
62
|
+
|
|
63
|
+
Nothing to declare.
|
|
64
|
+
|
|
65
|
+
## Background workers and routers
|
|
66
|
+
|
|
67
|
+
Both shipped routers gain two methods, and each adapter gains one worker class:
|
|
68
|
+
|
|
69
|
+
| Adapter | Router methods | Worker class |
|
|
70
|
+
| --- | --- | --- |
|
|
71
|
+
| Sidekiq | `perform_map_element_rollback_async(**args)`, `perform_map_element_rollback_in(delay, **args)` | `RubyReactor::Adapters::Sidekiq::MapElementRollbackWorker` |
|
|
72
|
+
| ActiveJob | `perform_map_element_rollback_async(**args)`, `perform_map_element_rollback_in(delay, **args)` | `RubyReactor::Adapters::ActiveJob::MapElementRollbackWorker` |
|
|
73
|
+
|
|
74
|
+
`perform_map_element_rollback_in` is the requeue a job uses when the element's lock is contended
|
|
75
|
+
(R-07).
|
|
76
|
+
|
|
77
|
+
Args (JSON-safe):
|
|
78
|
+
|
|
79
|
+
- `map_id`, `position`, `element_context_id`, `reactor_class_info`;
|
|
80
|
+
- `parent_reactor_class_name`, `step_name`, `batch_size`;
|
|
81
|
+
- `owner_context_id`, `owner_reactor_class_name`;
|
|
82
|
+
- `attempt`: 0 on dispatch, incremented on each contended-lock requeue.
|
|
83
|
+
|
|
84
|
+
The worker runs on the same queue as `MapElementWorker`.
|
|
85
|
+
|
|
86
|
+
`MapCollectorWorker` keeps its class and arguments. A collector job enqueued before the upgrade is
|
|
87
|
+
handled by the new body: it signals the owner instead of resuming the parent.
|
|
88
|
+
|
|
89
|
+
## Dashboard API
|
|
90
|
+
|
|
91
|
+
`GET /api/reactors/:id`: a hydrated map reference whose map has rollback records gains:
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{ "rollback": { "total": 1000, "settled": 350, "outstanding": 650, "failed": 2 } }
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`outstanding = total - settled`. The records outlive the run by the durability TTL, so the field is
|
|
98
|
+
present after the rollback finishes too; clients decide from the run's status.
|
|
99
|
+
|
|
100
|
+
GUI:
|
|
101
|
+
|
|
102
|
+
- `rolling_back` appears in amber with an undo icon everywhere statuses are shown or filtered: the
|
|
103
|
+
status badge, the "running" status group, and the live and per-class filters.
|
|
104
|
+
- The step inspector shows "Rolling back 350/1000 (650 outstanding, 2 failed)" **only while the
|
|
105
|
+
run is `rolling_back`**.
|
|
106
|
+
|
|
107
|
+
## Structured logs
|
|
108
|
+
|
|
109
|
+
key=value, one line each, through `RubyReactor.configuration.logger`:
|
|
110
|
+
|
|
111
|
+
```text
|
|
112
|
+
event=ruby_reactor.map.rollback.started reactor=<owner class> context_id=<owner id> map_step=<step> total=<n> batch_size=<b>
|
|
113
|
+
event=ruby_reactor.map.rollback.element reactor=<owner class> context_id=<owner id> map_step=<step> index=<i> outcome=<outcome> failures=<count>
|
|
114
|
+
event=ruby_reactor.map.rollback.completed reactor=<owner class> context_id=<owner id> map_step=<step> total=<n> failed=<count>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## RSpec test surface
|
|
118
|
+
|
|
119
|
+
| Addition | Use |
|
|
120
|
+
| --- | --- |
|
|
121
|
+
| `be_rolling_back` matcher | `expect(subject).to be_rolling_back`, for a run handed off at a fan-out map and not drained yet. |
|
|
122
|
+
| `PendingJob#worker_class` on the ActiveJob helper | Alias of `job_class`, so `pending_async_jobs.first.worker_class` works on both backends for step-wise draining. |
|
|
123
|
+
|
|
124
|
+
`drain_async_jobs` drains `MapElementRollbackWorker` like any other adapter worker. There is no API
|
|
125
|
+
change.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Contract: Map Completion and Distributed Rollback Protocol
|
|
2
|
+
|
|
3
|
+
The sequences the implementation must follow, and the invariants the specs pin. The record names
|
|
4
|
+
are defined in [data-model.md](../data-model.md).
|
|
5
|
+
|
|
6
|
+
## Invariants
|
|
7
|
+
|
|
8
|
+
- **I-1: single writer.** A context is written only by the execution that owns it: the top-level
|
|
9
|
+
run's worker holding `async:<owner id>`, or the caller's process during `Reactor#undo`, which
|
|
10
|
+
holds the same lock. The collector and element rollback jobs write only map records and element
|
|
11
|
+
contexts.
|
|
12
|
+
- **I-2: the owner resumes once per signal, and never before its hand-off is saved.** Each settle
|
|
13
|
+
signal (`owner_signalled`, `rollback:signalled`) enqueues the owner's `Worker` at most once. A
|
|
14
|
+
rollback signal also requires `rollback:handed_off`, which the handler sets after saving (R-05).
|
|
15
|
+
Extra resumes from the sweepers are harmless because every construct adopts idempotently on
|
|
16
|
+
re-entry.
|
|
17
|
+
- **I-3: no double dispatch.** A map dispatches its elements once (`map_operations[step]` is set
|
|
18
|
+
before dispatch), and starts its rollback once (`rollback:metadata` is created with `HSETNX`).
|
|
19
|
+
- **I-4: step-level saga order.** Steps before a map are undone only after its rollback has
|
|
20
|
+
settled. Within an element, steps are undone in reverse order.
|
|
21
|
+
- **I-5: resume cursor.** An undo entry leaves the stack only after its undo returns. A hand-off
|
|
22
|
+
leaves the map entry, and every entry below it, on the stack.
|
|
23
|
+
- **I-6: save before release.** Every hand-off (a forward `DispatchResult` or a rollback hand-off)
|
|
24
|
+
persists the context before the owner's `async:` lock is released.
|
|
25
|
+
- **I-7: oracle.** For the same reactor and failure, the final `Failure` and the set of undone
|
|
26
|
+
elements are the same whether the map is inline or fan-out. That includes `rollback_failures`
|
|
27
|
+
entries with symbol values, and each unavailable element reported once.
|
|
28
|
+
- **I-8: no terminal status mid-rollback.** A `rolling_back` run cannot be cancelled or undone
|
|
29
|
+
again (FR-027). Only `resume_rollback` moves it to `failed` / `cancelled`.
|
|
30
|
+
|
|
31
|
+
## S-1: A fan-out map completes (any composition depth)
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
owner worker (holds async:<owner>) element jobs collector
|
|
35
|
+
───────────────────────────────── ──────────── ─────────
|
|
36
|
+
MapStep#run (first time)
|
|
37
|
+
map_operations[step] = map_id
|
|
38
|
+
store owner tree + child snapshot
|
|
39
|
+
initialize map metadata (owner ids, batch_size, atomic)
|
|
40
|
+
Dispatcher: enqueue batch 0 (≤ B)
|
|
41
|
+
return DispatchResult ── executor saves, then releases async: (I-6)
|
|
42
|
+
run, store result slot,
|
|
43
|
+
position kB-1 → next batch,
|
|
44
|
+
counter hits 0 → enqueue collector
|
|
45
|
+
all slots present?
|
|
46
|
+
SET NX owner_signalled
|
|
47
|
+
enqueue Worker(owner)
|
|
48
|
+
Worker(owner) takes async:<owner>
|
|
49
|
+
resume_execution → … → ComposeStep#run → child resume_execution
|
|
50
|
+
MapStep#run (re-entry: map_operations[step] present)
|
|
51
|
+
settled + failed_context_id → return the element's Failure (+ its rollback_failures)
|
|
52
|
+
settled → return Success(collect(ResultEnumerator)); record started
|
|
53
|
+
not settled → return DispatchResult (no dispatch)
|
|
54
|
+
the executor continues: result recorded, undo entry {step, arguments: {}} (R-14)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## S-2: Rollback reaches a completed fan-out map
|
|
58
|
+
|
|
59
|
+
A later step failed, the map itself failed (atomic), or `Reactor#undo` was called.
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
owner (worker, or caller during undo; holds async:<owner>)
|
|
63
|
+
CompensationManager: compensate the failing step, then pop entries newest first …
|
|
64
|
+
MapStep#undo (map_operations[step] present)
|
|
65
|
+
HSETNX rollback:metadata (total = LLEN element_contexts, batch_size, owner ids)
|
|
66
|
+
created → log rollback.started; claim batch 0 (INCRBY offset B); enqueue ≤ B element rollback jobs
|
|
67
|
+
existed → dispatch nothing (I-3)
|
|
68
|
+
settled? (HLEN results == total)
|
|
69
|
+
yes → aggregate (HSCAN failures + started-index check) → return Success / Failure(rollback_failures)
|
|
70
|
+
no → raise RollbackHandedOff
|
|
71
|
+
▲ unwinds without popping (I-5), through Rescuable sites untouched
|
|
72
|
+
composed child executor / child Executor#undo_all: record child `rollback` (incl. its failures so far), re-raise
|
|
73
|
+
top-level executor: record `rollback`, status rolling_back, SAVE (in rescue, lock held),
|
|
74
|
+
SET rollback:handed_off, then: settled && SET NX rollback:signalled → enqueue Worker(owner);
|
|
75
|
+
release
|
|
76
|
+
Reactor#undo: same, and skip `cancel`
|
|
77
|
+
|
|
78
|
+
element rollback job (position p, attempt a)
|
|
79
|
+
load element context (by id from the job args)
|
|
80
|
+
missing → outcome context_unavailable (index null)
|
|
81
|
+
take map_element:<map>:<index> (wait 2 s)
|
|
82
|
+
held and a < lock_snooze_max_attempts → perform_map_element_rollback_in(delay, attempt: a+1); stop (no outcome)
|
|
83
|
+
held and a = max → outcome element_in_flight
|
|
84
|
+
status completed|aborted → rollback_completed_steps, saving the element after each pop
|
|
85
|
+
→ undone | failed(failures)
|
|
86
|
+
otherwise (failed / halted / superseded running) → not_needed
|
|
87
|
+
HSET results[p]; SADD indexes index; log rollback.element
|
|
88
|
+
p ≡ B-1 (mod B) → claim the next batch
|
|
89
|
+
HLEN results == total && EXISTS rollback:handed_off && SET NX rollback:signalled → enqueue Worker(owner)
|
|
90
|
+
|
|
91
|
+
Worker(owner), status rolling_back → Executor#resume_rollback (takes async:<owner>)
|
|
92
|
+
restore rollback_failures from `rollback.failures`
|
|
93
|
+
unless compensated: handle the failing step's compensate again (constructs adopt)
|
|
94
|
+
rollback_completed_steps → MapStep#undo re-entry → settled → aggregate → pop → continue
|
|
95
|
+
finalize: failure → the same Failure as the inline path (I-7) | undo → cancelled
|
|
96
|
+
clear `rollback`, save, release
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
A second fan-out map lower in the undo stack repeats S-2 from inside `resume_rollback`. The
|
|
100
|
+
status stays `rolling_back`.
|
|
101
|
+
|
|
102
|
+
**Aggregation** (`MapStep#undo` re-entry once settled): `HSCAN` failures and re-symbolize them.
|
|
103
|
+
Then report unavailable elements once: named started-but-unseen indexes when `started` is known,
|
|
104
|
+
otherwise one entry per nil-index outcome (DM §5).
|
|
105
|
+
|
|
106
|
+
**Inline test mode** (all jobs run inside dispatch): every outcome is stored before `MapStep#undo`
|
|
107
|
+
checks settled, and `handed_off` never exists, so no job enqueues the owner. `#undo` aggregates
|
|
108
|
+
synchronously and never raises the hand-off.
|
|
109
|
+
|
|
110
|
+
## S-3: The failing step is inside a composed child
|
|
111
|
+
|
|
112
|
+
```text
|
|
113
|
+
root: ComposeStep#run → child executes → child step fails
|
|
114
|
+
child handle_step_failure: compensate the child step, then pop child entries … child MapStep#undo → RollbackHandedOff
|
|
115
|
+
child executor: record child.rollback {trigger: failure, step: <child step>, compensated: true, failure: <child Failure>}, re-raise
|
|
116
|
+
root StepExecutor: passes it through (not Rescuable)
|
|
117
|
+
root executor: root.rollback {trigger: failure, step: <compose step>, compensated: false, failure: <child Failure>}
|
|
118
|
+
status rolling_back, save (the child is embedded), release
|
|
119
|
+
… element rollbacks settle → Worker(root)
|
|
120
|
+
root resume_rollback: compensated false → handle_step_failure(compose step)
|
|
121
|
+
ComposeStep#compensate → child executor undo_all (restores child.rollback failures) → child stack:
|
|
122
|
+
map entry → adopt → pop → the rest of the child → clear child.rollback
|
|
123
|
+
→ then the root's own completed steps → finalize with the saved failure
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The same path runs when a **manual undo from the root** reaches a composed child: root
|
|
127
|
+
`Reactor#undo` → root `undo_all` → `ComposeStep#undo` → child `undo_all` → child `MapStep#undo`
|
|
128
|
+
hands off. Child `undo_all` records its failures and re-raises, and `Reactor#undo` records the root
|
|
129
|
+
`rollback {trigger: undo}`. On resume, child `undo_all` restores its failures, so none are lost.
|
|
130
|
+
|
|
131
|
+
## S-4: Inline map rollback (no hand-off)
|
|
132
|
+
|
|
133
|
+
```text
|
|
134
|
+
MapStep#undo (map_operations[step] absent)
|
|
135
|
+
for each chunk of 100 positions from the tail of element_contexts:
|
|
136
|
+
load contexts → Map::ElementRollback (same class as the job, in process) → collect failures, indexes
|
|
137
|
+
started-index check → Success / Failure(rollback_failures)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## S-5: Recovery
|
|
141
|
+
|
|
142
|
+
| Lost thing | Detected by | Action |
|
|
143
|
+
| --- | --- | --- |
|
|
144
|
+
| Element rollback job (crash before storing the outcome) | `Map::Sweeper` rollback pass: a position `< offset` with no outcome and no live element lock | Re-enqueue that position. The element resumes after its last saved undo. |
|
|
145
|
+
| Batch trigger (the job at `kB-1` died after storing its outcome) | `Map::Sweeper`: offset `< total` and every claimed position settled | Claim and enqueue the next batch. |
|
|
146
|
+
| Owner resume (after `rollback:signalled` was set) | `RubyReactor::Sweeper`: `rolling_back`, no `async:` lock | Re-enqueue the owner. `resume_rollback` adopts the settled map. |
|
|
147
|
+
| Owner resume (after `owner_signalled` was set) | `RubyReactor::Sweeper`: `running`, no `async:` lock (existing) | Re-enqueue the owner. `MapStep#run` adopts. |
|
|
148
|
+
| Collector | `Map::Sweeper` recollect (existing), with the owner's `async:` lock checked instead of the parent's | Re-trigger the collector. `owner_signalled` keeps it to one resume. |
|
|
149
|
+
|
|
150
|
+
## S-6: Legacy in-flight work across the upgrade
|
|
151
|
+
|
|
152
|
+
| Payload | Handling |
|
|
153
|
+
| --- | --- |
|
|
154
|
+
| Element / dispatcher / collector / retry job carrying `fail_fast` | `Map::Helpers.normalize_arguments` maps it to `atomic`. |
|
|
155
|
+
| Map metadata without `owner_context_id` (a map started before the upgrade) | Collector falls back to `parent_context_id` / `parent_reactor_class_name`. This is correct for root-level maps. A map inside a composed child started before the upgrade keeps the old hang; documented in CHANGELOG. |
|
|
156
|
+
| Map metadata without `batch_size` | The rollback uses `DEFAULT_BATCH_SIZE`. |
|
|
157
|
+
| Element index with duplicate ids | Safe: an empty undo stack reports `undone`, and a concurrent duplicate waits on the element lock or reports `element_in_flight`. |
|