@elaraai/e3-ui-components 1.0.12 → 1.0.13

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.
package/dist/index.cjs CHANGED
@@ -7464,6 +7464,11 @@ var faArrowDown = {
7464
7464
  iconName: "arrow-down",
7465
7465
  icon: [384, 512, [8595], "f063", "M169.4 502.6c12.5 12.5 32.8 12.5 45.3 0l160-160c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L224 402.7 224 32c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 370.7-105.4-105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l160 160z"]
7466
7466
  };
7467
+ var faCircleInfo = {
7468
+ prefix: "fas",
7469
+ iconName: "circle-info",
7470
+ icon: [512, 512, ["info-circle"], "f05a", "M256 512a256 256 0 1 0 0-512 256 256 0 1 0 0 512zM224 160a32 32 0 1 1 64 0 32 32 0 1 1 -64 0zm-8 64l48 0c13.3 0 24 10.7 24 24l0 88 8 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-80 0c-13.3 0-24-10.7-24-24s10.7-24 24-24l24 0 0-64-24 0c-13.3 0-24-10.7-24-24s10.7-24 24-24z"]
7471
+ };
7467
7472
  const E3ConfigContext = React.createContext(null);
7468
7473
  function E3Provider({ children: children2, config: config2, queryClient: externalClient }) {
7469
7474
  const client = React.useMemo(
@@ -21879,13 +21884,13 @@ var hashIterableInts = function hashIterableInts2(iterator) {
21879
21884
  }
21880
21885
  return hash2;
21881
21886
  };
21882
- var hashInt = function hashInt2(num) {
21887
+ var hashInt = function hashInt2(num2) {
21883
21888
  var seed = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : DEFAULT_HASH_SEED;
21884
- return seed * K + num | 0;
21889
+ return seed * K + num2 | 0;
21885
21890
  };
21886
- var hashIntAlt = function hashIntAlt2(num) {
21891
+ var hashIntAlt = function hashIntAlt2(num2) {
21887
21892
  var seed = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : DEFAULT_HASH_SEED_ALT;
21888
- return (seed << 5) + seed + num | 0;
21893
+ return (seed << 5) + seed + num2 | 0;
21889
21894
  };
21890
21895
  var combineHashes = function combineHashes2(hash1, hash2) {
21891
21896
  return hash1 * 2097152 + hash2;
@@ -23472,7 +23477,7 @@ var max = function max2(arr2) {
23472
23477
  }
23473
23478
  return max5;
23474
23479
  };
23475
- var mean$1 = function mean(arr2) {
23480
+ var mean = function mean2(arr2) {
23476
23481
  var begin3 = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : 0;
23477
23482
  var end3 = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : arr2.length;
23478
23483
  var total = 0;
@@ -25785,7 +25790,7 @@ var getPreference = function getPreference2(S2, preference) {
25785
25790
  if (preference === "median") {
25786
25791
  p2 = median(S2);
25787
25792
  } else if (preference === "mean") {
25788
- p2 = mean$1(S2);
25793
+ p2 = mean(S2);
25789
25794
  } else if (preference === "min") {
25790
25795
  p2 = min(S2);
25791
25796
  } else if (preference === "max") {
@@ -56758,7 +56763,7 @@ function useBindingValue(binding) {
56758
56763
  return { value, mode, pending, mutate, commit, discard };
56759
56764
  }
56760
56765
  const IDLE = { call: () => {
56761
- }, result: null, status: "idle", pending: false };
56766
+ }, result: null, status: "idle", pending: false, error: null };
56762
56767
  function useFuncCall(name, inputs, output2) {
56763
56768
  const workspace = getReactiveDatasetCache().getConfig().workspace ?? "";
56764
56769
  const ready4 = !!name && !!inputs && workspace !== "";
@@ -56781,15 +56786,22 @@ function useFuncCall(name, inputs, output2) {
56781
56786
  React.useCallback(() => channelKey ? defaultFuncRuntime.getKeyVersion(channelKey) : 0, [channelKey])
56782
56787
  );
56783
56788
  return React.useMemo(() => {
56789
+ var _a2;
56784
56790
  if (!handle) return IDLE;
56785
56791
  let result = null;
56786
56792
  let status = "idle";
56787
56793
  let pending = false;
56794
+ let error3 = null;
56788
56795
  try {
56789
56796
  const read = handle.read();
56790
56797
  result = read.type === "some" ? read.value : null;
56791
56798
  status = handle.status().type;
56792
56799
  pending = handle.pending();
56800
+ const err = handle.error();
56801
+ if (err.type === "some") {
56802
+ const v3 = err.value;
56803
+ error3 = { kind: ((_a2 = v3.kind) == null ? void 0 : _a2.type) ?? "failed", message: v3.message ?? "", stdout: v3.stdout ?? "", stderr: v3.stderr ?? "" };
56804
+ }
56793
56805
  } catch {
56794
56806
  }
56795
56807
  return { call: (...args) => {
@@ -56797,7 +56809,7 @@ function useFuncCall(name, inputs, output2) {
56797
56809
  handle.call(...args);
56798
56810
  } catch {
56799
56811
  }
56800
- }, result, status, pending };
56812
+ }, result, status, pending, error: error3 };
56801
56813
  }, [handle, version2]);
56802
56814
  }
56803
56815
  var propTypes = { exports: {} };
@@ -59041,6 +59053,370 @@ function Circle(_ref2) {
59041
59053
  className: cx("visx-circle", className)
59042
59054
  }, restProps));
59043
59055
  }
59056
+ function fillHelp(entry, vars) {
59057
+ const sub = (s2) => s2.replace(/\{(\w+)\}/g, (m2, k2) => k2 in vars ? vars[k2] : m2);
59058
+ return {
59059
+ term: sub(entry.term),
59060
+ gist: sub(entry.gist),
59061
+ detail: sub(entry.detail),
59062
+ ...entry.jargon !== void 0 ? { jargon: sub(entry.jargon) } : {}
59063
+ };
59064
+ }
59065
+ const HELP = {
59066
+ // ── Guidance mode (the toggle itself) ───────────────────────────────
59067
+ guidance: {
59068
+ term: "Guidance",
59069
+ gist: "Plain-language explainers for everything on this surface.",
59070
+ detail: "While this is on, hover any heading, value or chart label to see what it means — in business terms, with the statistical name underneath. Click to turn it off."
59071
+ },
59072
+ // ── Header ──────────────────────────────────────────────────────────
59073
+ header: {
59074
+ term: "The question",
59075
+ gist: "You’re asking whether {treatment} actually moved {outcome}.",
59076
+ detail: "Pick the lever you changed and the result you cared about, then hit Run — the surface answers whether the change is real and trustworthy.",
59077
+ jargon: "a binary-treatment causal effect"
59078
+ },
59079
+ // ── Set-up rail ─────────────────────────────────────────────────────
59080
+ step_treatment: {
59081
+ term: "What did you change?",
59082
+ gist: "The lever you’re testing — a yes/no flag on each {subject}.",
59083
+ detail: "It splits {subjects} into a “did” group and a “didn’t” group; the whole experiment compares those two.",
59084
+ jargon: "the treatment (binary)"
59085
+ },
59086
+ step_outcome: {
59087
+ term: "What did you want it to improve?",
59088
+ gist: "The result you’re hoping {treatment} moved.",
59089
+ detail: "A number measured on each {subject} (revenue, a duration, a score, …). The effect is the change in this we can credit to the lever.",
59090
+ jargon: "the outcome"
59091
+ },
59092
+ step_confounders: {
59093
+ term: "What else was different?",
59094
+ gist: "Things about the “did” group that could also explain the change.",
59095
+ detail: "If the {subjects} you treated already differed on something that matters — bigger, older, higher-value — that, not the lever, could be moving the result. We level the playing field on each of these so the comparison is fair. Add the ones you suspect.",
59096
+ jargon: "confounders / the backdoor adjustment set"
59097
+ },
59098
+ step_population: {
59099
+ term: "Which {subjects}?",
59100
+ gist: "Narrow the experiment to a slice of the {subjects}.",
59101
+ detail: "Filter to a subset you care about — a region, a period, a category — before measuring, so the answer is about the {subjects} that matter.",
59102
+ jargon: "population / cohort filter"
59103
+ },
59104
+ adv_method: {
59105
+ term: "How to compare",
59106
+ gist: "The maths used to make the like-for-like comparison.",
59107
+ detail: "“Regression” fits a line through the confounders; “re-weighting” up-weights rare look-alikes. They usually agree — if they don’t, treat the result with care.",
59108
+ jargon: "estimator: linear regression vs propensity-score weighting"
59109
+ },
59110
+ adv_estimand: {
59111
+ term: "Answer for",
59112
+ gist: "Who the effect is reported for.",
59113
+ detail: "“All” averages over everyone; “only treated” asks what the lever did for the {subjects} you actually treated — useful when you’d only ever apply it to them.",
59114
+ jargon: "estimand: ATE vs ATT"
59115
+ },
59116
+ confounder_imbalance: {
59117
+ term: "How lopsided this one was",
59118
+ gist: "How different the two groups were on this factor, before adjusting.",
59119
+ detail: "A long bar means the treated and untreated {subjects} started far apart here — exactly the kind of gap the adjustment has to close.",
59120
+ jargon: "standardised mean difference (SMD)"
59121
+ },
59122
+ // ── Tabs ────────────────────────────────────────────────────────────
59123
+ tab_answer: {
59124
+ term: "Answer",
59125
+ gist: "The headline: did {treatment} move {outcome}, and by how much?",
59126
+ detail: "Shows the raw gap, the fair like-for-like estimate, and an honest verdict."
59127
+ },
59128
+ tab_trust: {
59129
+ term: "Can we trust it?",
59130
+ gist: "The stress tests we ran to try to break the answer.",
59131
+ detail: "Each check probes a way the result could be a fluke; green means it held up."
59132
+ },
59133
+ tab_dose: {
59134
+ term: "How much?",
59135
+ gist: "How the result changes as a factor goes up or down.",
59136
+ detail: "Not just yes/no — the shape of the response, so you can find the sweet spot."
59137
+ },
59138
+ // ── Answer tab ──────────────────────────────────────────────────────
59139
+ answer_effect: {
59140
+ term: "Like-for-like effect",
59141
+ gist: "The change in {outcome} we can credit to {treatment}.",
59142
+ detail: "It compares treated vs untreated {subjects} who were otherwise similar — so it strips out “who you targeted” and leaves the lever’s real impact.",
59143
+ jargon: "the backdoor-adjusted average treatment effect"
59144
+ },
59145
+ answer_ci: {
59146
+ term: "95% range",
59147
+ gist: "Where the true effect most likely sits.",
59148
+ detail: "If this range doesn’t cross 0, the effect is real, not noise. The wider it is, the less certain — usually because of fewer {subjects}.",
59149
+ jargon: "95% confidence interval"
59150
+ },
59151
+ answer_flip: {
59152
+ term: "Raw and like-for-like disagree",
59153
+ gist: "The plain average points one way; the fair comparison points the other.",
59154
+ detail: "This happens when the treated group differed sharply on a confounder. Trust the like-for-like number — the raw gap is misleading here.",
59155
+ jargon: "confounding / Simpson’s-paradox sign flip"
59156
+ },
59157
+ answer_cautious: {
59158
+ term: "Treat this as provisional",
59159
+ gist: "We got a number, but a trust check failed.",
59160
+ detail: "The estimate may still be driven by something we didn’t adjust for. See “Can we trust it?” before acting on it.",
59161
+ jargon: "adjustment_insufficient verdict"
59162
+ },
59163
+ forest_plot: {
59164
+ term: "Raw average vs. like-for-like",
59165
+ gist: "The misleading number next to the fair one.",
59166
+ detail: "Each bar is an estimate with its uncertainty range; the dashed line is “no effect”. A bar clear of that line is a real change.",
59167
+ jargon: "a forest plot of naive vs adjusted estimates"
59168
+ },
59169
+ forest_naive: {
59170
+ term: "Raw average",
59171
+ gist: "The plain difference, with no adjusting.",
59172
+ detail: "Just treated-minus-untreated. It mixes in who you targeted, so it can mislead — it’s here only as the “before” picture.",
59173
+ jargon: "the unadjusted mean difference"
59174
+ },
59175
+ forest_adjusted: {
59176
+ term: "Like-for-like",
59177
+ gist: "The fair comparison after levelling the confounders.",
59178
+ detail: "This is the number to act on — what {treatment} did among otherwise-similar {subjects}.",
59179
+ jargon: "the adjusted effect"
59180
+ },
59181
+ balance: {
59182
+ term: "How unbalanced each one was",
59183
+ gist: "How far apart the two groups started on each factor.",
59184
+ detail: "Big bars are the factors the adjustment had to work hardest to correct — and the ones most worth double-checking.",
59185
+ jargon: "pre-adjustment covariate balance (SMD)"
59186
+ },
59187
+ counts: {
59188
+ term: "The {subject} counts",
59189
+ gist: "How many {subjects} went into the comparison.",
59190
+ detail: "“Compared like-for-like” are the ones with a fair match on the other side; “no fair match” were set aside because nobody comparable existed.",
59191
+ jargon: "n total / on-support / off-support"
59192
+ },
59193
+ // ── Refusal zones + overlap ─────────────────────────────────────────
59194
+ overlap_histogram: {
59195
+ term: "Do the two groups overlap?",
59196
+ gist: "Whether treated and untreated {subjects} actually look alike anywhere.",
59197
+ detail: "Each bar is how many {subjects} sit at a given “likelihood of being treated”. Where the two arms both have bars, a fair comparison exists; if they barely meet, there’s nothing to compare.",
59198
+ jargon: "propensity-score overlap / common support"
59199
+ },
59200
+ refusal_positivity: {
59201
+ term: "No like-for-like comparison exists",
59202
+ gist: "The treated and untreated {subjects} are too different to compare.",
59203
+ detail: "For almost every treated {subject} there’s no similar untreated one (or vice-versa), so there’s no fair “other side”. We refuse rather than extrapolate into thin air. Try fewer or different confounders.",
59204
+ jargon: "positivity / overlap violation"
59205
+ },
59206
+ refusal_not_estimable: {
59207
+ term: "Can’t be estimated",
59208
+ gist: "Almost every {subject} is on one side of {treatment}, so there’s no comparison group.",
59209
+ detail: "Measuring an effect needs a decent number of {subjects} who got the lever AND who didn’t. When one side is just a handful, we refuse to guess rather than read too much into a few people.",
59210
+ jargon: "no treatment variation"
59211
+ },
59212
+ // ── Trust tab ───────────────────────────────────────────────────────
59213
+ trust_intro: {
59214
+ term: "Can we trust it?",
59215
+ gist: "Before believing the answer, we tried to break it.",
59216
+ detail: "Each row is a different way the result could be a fluke; colour shows whether it held up (pass) or wobbled (caution)."
59217
+ },
59218
+ check_shuffle: {
59219
+ term: "Shuffle test",
59220
+ gist: "We randomly re-label who got {treatment} and re-measure.",
59221
+ detail: "On fake labels a genuine effect should collapse to nothing. If a “fake” effect still appears, the real one isn’t trustworthy.",
59222
+ jargon: "placebo / permutation refuter"
59223
+ },
59224
+ check_dropsome: {
59225
+ term: "Drop-some test",
59226
+ gist: "We re-run on random slices of the {subjects}.",
59227
+ detail: "A trustworthy effect barely moves when you drop some data; a jumpy one is resting on a few {subjects}.",
59228
+ jargon: "data-subset refuter"
59229
+ },
59230
+ check_decoy: {
59231
+ term: "Decoy factor",
59232
+ gist: "We add a totally random extra factor and re-measure.",
59233
+ detail: "A made-up factor shouldn’t change the answer. If it does, the model is over-reacting to noise.",
59234
+ jargon: "random-common-cause refuter"
59235
+ },
59236
+ check_hidden: {
59237
+ term: "Hidden cause",
59238
+ gist: "How strong an unrecorded cause would have to be to overturn the result.",
59239
+ detail: "Something you didn’t measure could drive both who got {treatment} and {outcome}. “Holds throughout” means only an implausibly strong hidden cause could flip it.",
59240
+ jargon: "unobserved-confounding sensitivity / E-value"
59241
+ },
59242
+ sensitivity: {
59243
+ term: "Effect as a hidden cause gets stronger",
59244
+ gist: "What happens to the effect if an unrecorded cause is dialled up.",
59245
+ detail: "The line is the estimate as a simulated hidden cause grows. The point where it crosses 0 is how much hidden bias it would take to erase the result.",
59246
+ jargon: "sensitivity (tipping-point) curve"
59247
+ },
59248
+ // ── Dose tab ────────────────────────────────────────────────────────
59249
+ dose_curve: {
59250
+ term: "{outcome} gained vs. {feature}",
59251
+ gist: "How {outcome} responds as {feature} goes up.",
59252
+ detail: "The line is the average change in {outcome} at each level of {feature}, with its uncertainty band — the shape tells you where more pays off and where it flattens.",
59253
+ jargon: "accumulated local effects (ALE) dose-response"
59254
+ },
59255
+ dose_here: {
59256
+ term: "You are here",
59257
+ gist: "Where most of your {subjects} currently sit on {feature}.",
59258
+ detail: "The busiest level today — your starting point for deciding whether to push higher or lower.",
59259
+ jargon: "the modal bin"
59260
+ },
59261
+ dose_sweet: {
59262
+ term: "Sweet spot",
59263
+ gist: "Where the gains are solid and start to flatten.",
59264
+ detail: "Past here, pushing {feature} higher buys little extra {outcome} — a sensible target.",
59265
+ jargon: "the diminishing-returns knee"
59266
+ },
59267
+ dose_reco: {
59268
+ term: "Recommended",
59269
+ gist: "The level of {feature} we’d aim for.",
59270
+ detail: "The sweet-spot level and the {outcome} you’d expect there, with its range — and the trade-off of going one step further.",
59271
+ jargon: "the recommended operating point"
59272
+ },
59273
+ dose_marginal: {
59274
+ term: "Extra {outcome} per step",
59275
+ gist: "What each additional step of {feature} buys you.",
59276
+ detail: "Tall early bars then short ones is the classic diminishing-returns shape — spend effort where the bars are tall.",
59277
+ jargon: "marginal / incremental effect per bin"
59278
+ },
59279
+ // ── Verdicts ────────────────────────────────────────────────────────
59280
+ verdict_causal: {
59281
+ term: "Causal",
59282
+ gist: "A real change you can credit to {treatment} — and it held up.",
59283
+ detail: "The like-for-like effect is clear, sizeable, and survived the trust checks. Safe to act on.",
59284
+ jargon: "a robust, identified effect"
59285
+ },
59286
+ verdict_modest: {
59287
+ term: "Modest / unclear",
59288
+ gist: "There may be an effect, but it’s small or fuzzy.",
59289
+ detail: "The fair comparison can’t cleanly separate it from no-effect (the range straddles 0, or it’s tiny). Don’t bet much on it.",
59290
+ jargon: "effect CI spans zero / below materiality"
59291
+ },
59292
+ verdict_adjustment_insufficient: {
59293
+ term: "Not trustworthy yet",
59294
+ gist: "We got a number, but a trust check failed.",
59295
+ detail: "Likely something we didn’t adjust for is still in play. Treat as provisional and look at “Can we trust it?”.",
59296
+ jargon: "placebo / robustness failure"
59297
+ },
59298
+ verdict_non_identifiable_positivity: {
59299
+ term: "No fair comparison",
59300
+ gist: "Treated and untreated {subjects} don’t overlap enough to compare.",
59301
+ detail: "There’s no like-for-like “other side”, so any number would be extrapolation. See the overlap chart.",
59302
+ jargon: "positivity / overlap violation"
59303
+ },
59304
+ verdict_not_estimable: {
59305
+ term: "Can’t be estimated",
59306
+ gist: "Almost everyone is on one side of {treatment} — no comparison group.",
59307
+ detail: "One arm is just a handful of {subjects}, so we won’t guess an effect from it.",
59308
+ jargon: "no treatment variation"
59309
+ },
59310
+ // ── Validate tab ────────────────────────────────────────────────────
59311
+ tab_validate: {
59312
+ term: "Validate",
59313
+ gist: "The real experiment you’d run to prove this — for sure.",
59314
+ detail: "This analysis adjusted for what it could measure. A controlled trial — assigning {treatment} at random — removes any leftover doubt. This tab sizes that trial."
59315
+ },
59316
+ validate_size: {
59317
+ term: "How many to run",
59318
+ gist: "The number of {subjects} the trial needs.",
59319
+ detail: "Enough to spot a change this size if it’s real. A bigger or cleaner effect needs fewer; a small or noisy one needs many more.",
59320
+ jargon: "required sample size at the target power"
59321
+ },
59322
+ validate_split: {
59323
+ term: "How to split them",
59324
+ gist: "How many get {treatment} versus are left alone.",
59325
+ detail: "Assign at random — that’s what makes the two groups comparable with no adjusting. An even split is the most efficient; treating fewer needs a larger total.",
59326
+ jargon: "randomised allocation between arms"
59327
+ },
59328
+ validate_match: {
59329
+ term: "Match the groups on",
59330
+ gist: "Keep both groups balanced on these — they were lopsided last time.",
59331
+ detail: "Give each group the same mix of these factors when you split, so neither one starts ahead. These are the factors that muddied the original comparison most.",
59332
+ jargon: "stratified / blocked randomisation on the high-imbalance covariates"
59333
+ },
59334
+ validate_power: {
59335
+ term: "Chance of detecting it",
59336
+ gist: "How likely the trial is to catch the effect, by size.",
59337
+ detail: "More {subjects} means a better chance of a clear result. The marker shows where today’s data sits — usually well short of a confident answer.",
59338
+ jargon: "statistical power vs sample size"
59339
+ },
59340
+ validate_holdback: {
59341
+ term: "Hold back a control",
59342
+ gist: "Keep a random group untreated to compare against.",
59343
+ detail: "Right now almost everything got {treatment}, so there’s nothing to compare to. Next time, leave a random sample untreated — that group becomes the fair baseline.",
59344
+ jargon: "a randomised control arm"
59345
+ },
59346
+ // ── Journal ─────────────────────────────────────────────────────────
59347
+ journal: {
59348
+ term: "Committed experiments",
59349
+ gist: "A log of the questions you’ve saved.",
59350
+ detail: "Each row is a framing you committed — its verdict and headline effect — so the team can see what’s been tried and what held up.",
59351
+ jargon: "the experiment journal"
59352
+ }
59353
+ };
59354
+ const GuidanceContext = React.createContext({ on: true, vars: {} });
59355
+ function GuidanceProvider({ on: on3, vars, children: children2 }) {
59356
+ return /* @__PURE__ */ jsxRuntime.jsx(GuidanceContext.Provider, { value: { on: on3, vars }, children: children2 });
59357
+ }
59358
+ function HelpCard({ id: id3, extraVars }) {
59359
+ const { vars } = React.useContext(GuidanceContext);
59360
+ const e3 = fillHelp(HELP[id3], { ...vars, ...extraVars });
59361
+ const slot = react.useSlotRecipe({ key: "hoverCard" })();
59362
+ return /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "flex", flexDirection: "column", gap: "1.5", children: [
59363
+ /* @__PURE__ */ jsxRuntime.jsx(react.Box, { css: slot.title, mb: "0", children: e3.term }),
59364
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "body.sm", color: "fg.default", lineHeight: "1.5", children: e3.gist }),
59365
+ /* @__PURE__ */ jsxRuntime.jsx(react.Box, { css: slot.description, lineHeight: "1.5", children: e3.detail }),
59366
+ e3.jargon && /* @__PURE__ */ jsxRuntime.jsxs(react.Text, { textStyle: "caption", color: "fg.subtle", mt: "0.5", pt: "1.5", borderTopWidth: "1px", borderColor: "border.subtle", children: [
59367
+ "Technically: ",
59368
+ e3.jargon
59369
+ ] })
59370
+ ] });
59371
+ }
59372
+ function Hover({ id: id3, extraVars, trigger }) {
59373
+ return /* @__PURE__ */ jsxRuntime.jsxs(react.HoverCard.Root, { openDelay: 150, closeDelay: 80, size: "sm", positioning: { placement: "top" }, children: [
59374
+ /* @__PURE__ */ jsxRuntime.jsx(react.HoverCard.Trigger, { asChild: true, children: trigger }),
59375
+ /* @__PURE__ */ jsxRuntime.jsx(react.Portal, { children: /* @__PURE__ */ jsxRuntime.jsx(react.HoverCard.Positioner, { children: /* @__PURE__ */ jsxRuntime.jsxs(react.HoverCard.Content, { children: [
59376
+ /* @__PURE__ */ jsxRuntime.jsx(react.HoverCard.Arrow, { children: /* @__PURE__ */ jsxRuntime.jsx(react.HoverCard.ArrowTip, {}) }),
59377
+ /* @__PURE__ */ jsxRuntime.jsx(HelpCard, { id: id3, extraVars })
59378
+ ] }) }) })
59379
+ ] });
59380
+ }
59381
+ function Help({ id: id3, extraVars, children: children2, display = "inline", gap }) {
59382
+ const { on: on3 } = React.useContext(GuidanceContext);
59383
+ if (!on3) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: children2 });
59384
+ return /* @__PURE__ */ jsxRuntime.jsx(Hover, { id: id3, extraVars, trigger: (
59385
+ // `tabIndex={0}` makes the term keyboard-focusable — the HoverCard's zag
59386
+ // machine opens on focus, so the glossary is reachable without a pointer.
59387
+ /* @__PURE__ */ jsxRuntime.jsx(
59388
+ react.Box,
59389
+ {
59390
+ as: "span",
59391
+ cursor: "help",
59392
+ tabIndex: 0,
59393
+ display,
59394
+ ...gap ? { alignItems: "center", gap } : {},
59395
+ _focusVisible: { outline: "1px dotted", outlineColor: "brand.fg", outlineOffset: "2px", borderRadius: "2px" },
59396
+ children: children2
59397
+ }
59398
+ )
59399
+ ) });
59400
+ }
59401
+ function GuidanceToggle({ on: on3, onToggle }) {
59402
+ const iconButton = react.useRecipe({ key: "iconButton" });
59403
+ return /* @__PURE__ */ jsxRuntime.jsx(Hover, { id: "guidance", trigger: /* @__PURE__ */ jsxRuntime.jsx(
59404
+ react.Box,
59405
+ {
59406
+ as: "button",
59407
+ css: iconButton({ variant: on3 ? "subtle" : "ghost", size: "sm" }),
59408
+ onClick: onToggle,
59409
+ "aria-pressed": on3,
59410
+ "aria-label": "Toggle guidance",
59411
+ color: on3 ? "brand.fg" : "fg.muted",
59412
+ children: /* @__PURE__ */ jsxRuntime.jsx(FontAwesomeIcon, { icon: faCircleInfo })
59413
+ }
59414
+ ) });
59415
+ }
59416
+ function ChartLabel({ x: x2, y: y2, w: w2, align, size: size3, weight: weight8, color: color2, family, help, children: children2 }) {
59417
+ const span = /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: `${size3}px`, fontFamily: family, fontWeight: weight8, color: color2, lineHeight: 1, whiteSpace: "nowrap" }, children: children2 });
59418
+ return /* @__PURE__ */ jsxRuntime.jsx("foreignObject", { x: x2, y: y2, width: w2, height: size3 + 6, style: { overflow: "visible" }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { textAlign: align, fontSize: `${size3}px`, lineHeight: 1 }, children: help ? /* @__PURE__ */ jsxRuntime.jsx(Help, { id: help, children: span }) : span }) });
59419
+ }
59044
59420
  function svgText(size3, fill, family, weight8) {
59045
59421
  return { fontSize: `${size3}px`, fill, fontFamily: family, ...weight8 !== void 0 ? { fontWeight: weight8 } : {} };
59046
59422
  }
@@ -59096,7 +59472,7 @@ const niceCeil = (v3) => {
59096
59472
  const step3 = n2 <= 1 ? 1 : n2 <= 1.5 ? 1.5 : n2 <= 2 ? 2 : n2 <= 3 ? 3 : n2 <= 4 ? 4 : n2 <= 5 ? 5 : n2 <= 6 ? 6 : n2 <= 8 ? 8 : 10;
59097
59473
  return step3 * mag;
59098
59474
  };
59099
- function ForestPlot({ rows, min: min4, max: max5, unit: unit2, height: height2 }) {
59475
+ function ForestPlot({ rows, min: min4, max: max5, unit: unit2, height: height2, rowHelp }) {
59100
59476
  const t4 = useChartTheme();
59101
59477
  const [ref, w2] = useMeasuredWidth();
59102
59478
  const n2 = rows.length;
@@ -59106,11 +59482,14 @@ function ForestPlot({ rows, min: min4, max: max5, unit: unit2, height: height2 }
59106
59482
  const innerH = hPx - padT - padB;
59107
59483
  const x2 = createLinearScale({ domain: [min4, max5], range: [0, innerW] });
59108
59484
  const rowH = innerH / Math.max(1, n2);
59109
- const zeroX = x2(0);
59110
- const ticks2 = [min4, 0, max5];
59485
+ const zeroX = Math.max(0, Math.min(innerW, x2(0)));
59486
+ const frac = (v3) => max5 > min4 ? (v3 - min4) / (max5 - min4) : 0.5;
59487
+ const anchorAt = (v3) => frac(v3) < 0.06 ? "start" : frac(v3) > 0.94 ? "end" : "middle";
59488
+ const zf = frac(0);
59489
+ const ticks2 = zf > 0.12 && zf < 0.88 ? [min4, 0, max5] : [min4, max5];
59111
59490
  return /* @__PURE__ */ jsxRuntime.jsx(react.Box, { ref, width: "100%", height: `${hPx}px`, children: w2 > 0 && /* @__PURE__ */ jsxRuntime.jsx("svg", { width: w2, height: hPx, style: { display: "block" }, children: /* @__PURE__ */ jsxRuntime.jsxs(Group, { left: padL, top: padT, children: [
59112
59491
  /* @__PURE__ */ jsxRuntime.jsx(Line, { from: { x: zeroX, y: -4 }, to: { x: zeroX, y: innerH }, stroke: t4.muted, strokeWidth: 1, strokeDasharray: "3 2" }),
59113
- /* @__PURE__ */ jsxRuntime.jsx("text", { x: zeroX, y: -6, textAnchor: "middle", style: svgText(t4.labelSize, t4.muted, t4.mono), children: "no effect" }),
59492
+ /* @__PURE__ */ jsxRuntime.jsx("text", { x: zeroX, y: -6, textAnchor: anchorAt(0), style: svgText(t4.labelSize, t4.muted, t4.mono), children: "no effect" }),
59114
59493
  rows.map((r2, i) => {
59115
59494
  const cy = rowH * i + rowH / 2;
59116
59495
  const col = t4.tone(r2.tone);
@@ -59119,20 +59498,20 @@ function ForestPlot({ rows, min: min4, max: max5, unit: unit2, height: height2 }
59119
59498
  /* @__PURE__ */ jsxRuntime.jsx(Line, { from: { x: x2(r2.lo), y: -4 }, to: { x: x2(r2.lo), y: 4 }, stroke: col, strokeWidth: 1.5 }),
59120
59499
  /* @__PURE__ */ jsxRuntime.jsx(Line, { from: { x: x2(r2.hi), y: -4 }, to: { x: x2(r2.hi), y: 4 }, stroke: col, strokeWidth: 1.5 }),
59121
59500
  /* @__PURE__ */ jsxRuntime.jsx(Circle, { cx: x2(r2.est), cy: 0, r: t4.scatterRadius, fill: col, stroke: t4.surface, strokeWidth: 1.8 }),
59122
- /* @__PURE__ */ jsxRuntime.jsx("text", { x: -padL + 2, y: -3, style: svgText(t4.labelSize, t4.ink, t4.body, 600), children: r2.label }),
59501
+ /* @__PURE__ */ jsxRuntime.jsx(ChartLabel, { x: -padL + 2, y: -13, w: padL - 6, align: "left", size: t4.labelSize, weight: 600, color: t4.ink, family: t4.body, ...(rowHelp == null ? void 0 : rowHelp[i]) ? { help: rowHelp[i] } : {}, children: r2.label }),
59123
59502
  r2.note && /* @__PURE__ */ jsxRuntime.jsx("text", { x: -padL + 2, y: 9, style: svgText(9, t4.faint, t4.body), children: r2.note }),
59124
59503
  /* @__PURE__ */ jsxRuntime.jsx("text", { x: innerW + padR - 2, y: 4, textAnchor: "end", style: svgText(t4.titleSize, col, t4.mono, 700), children: signed$1(r2.est) })
59125
59504
  ] }, i);
59126
59505
  }),
59127
59506
  /* @__PURE__ */ jsxRuntime.jsx(Line, { from: { x: 0, y: innerH }, to: { x: innerW, y: innerH }, stroke: t4.rule, strokeWidth: 1 }),
59128
- ticks2.map((tk, i) => /* @__PURE__ */ jsxRuntime.jsxs(Group, { left: x2(tk), children: [
59507
+ ticks2.map((tk, i) => /* @__PURE__ */ jsxRuntime.jsxs(Group, { left: Math.max(0, Math.min(innerW, x2(tk))), children: [
59129
59508
  /* @__PURE__ */ jsxRuntime.jsx(Line, { from: { x: 0, y: innerH }, to: { x: 0, y: innerH + 4 }, stroke: t4.ruleStrong, strokeWidth: 1 }),
59130
- /* @__PURE__ */ jsxRuntime.jsx("text", { x: 0, y: innerH + 14, textAnchor: i === 0 ? "start" : i === ticks2.length - 1 ? "end" : "middle", style: svgText(t4.labelSize, t4.muted, t4.mono), children: signed$1(tk) })
59509
+ /* @__PURE__ */ jsxRuntime.jsx("text", { x: 0, y: innerH + 14, textAnchor: anchorAt(tk), style: svgText(t4.labelSize, t4.muted, t4.mono), children: signed$1(tk) })
59131
59510
  ] }, `t${i}`)),
59132
59511
  unit2 && /* @__PURE__ */ jsxRuntime.jsx("text", { x: innerW / 2, y: innerH + 26, textAnchor: "middle", style: svgText(t4.labelSize, t4.muted, t4.mono), children: unit2 })
59133
59512
  ] }) }) });
59134
59513
  }
59135
- function AreaRange({ lo: lo2, mid, hi, xTicks = [], yTicks = [], zero: zero2, tone: toneName, marks = [], height: height2 }) {
59514
+ function AreaRange({ lo: lo2, mid, hi, xTicks = [], yTicks = [], zero: zero2, tone: toneName, marks = [], height: height2, yFormat = "signed" }) {
59136
59515
  const t4 = useChartTheme();
59137
59516
  const [ref, w2] = useMeasuredWidth();
59138
59517
  const hPx = height2 ?? 100;
@@ -59144,13 +59523,18 @@ function AreaRange({ lo: lo2, mid, hi, xTicks = [], yTicks = [], zero: zero2, to
59144
59523
  const innerW = Math.max(0, w2 - padL - padR);
59145
59524
  const innerH = hPx - padT - padB;
59146
59525
  const xScale = createLinearScale({ domain: [0, Math.max(1, mid.length - 1)], range: [0, innerW] });
59147
- const dataMin = Math.min(...lo2, zero2 ?? Infinity);
59148
- const dataMax = Math.max(...hi, zero2 ?? -Infinity);
59526
+ const fin = (x2, fallback) => typeof x2 === "number" && Number.isFinite(x2) ? x2 : fallback;
59527
+ const data4 = mid.map((m2, i) => {
59528
+ const mm = fin(m2, 0);
59529
+ return { i, lo: fin(lo2[i], mm), hi: fin(hi[i], mm), mid: mm };
59530
+ });
59531
+ const dataMin = Math.min(...data4.map((d2) => d2.lo), zero2 ?? Infinity);
59532
+ const dataMax = Math.max(...data4.map((d2) => d2.hi), zero2 ?? -Infinity);
59149
59533
  const yHiN = niceCeil(Math.max(dataMax, 0));
59150
59534
  const yLoN = dataMin < -0.08 * yHiN ? -niceCeil(-dataMin) : 0;
59151
59535
  const yScale = createLinearScale({ domain: [yLoN, yHiN], range: [innerH, 0] });
59152
- const yLabels = [signed$1(yHiN), signed$1((yHiN + yLoN) / 2), signed$1(yLoN)];
59153
- const data4 = mid.map((m2, i) => ({ i, lo: lo2[i] ?? m2, hi: hi[i] ?? m2, mid: m2 }));
59536
+ const yfmt = (v3) => yFormat === "percent" ? `${Math.round(v3)}%` : signed$1(v3);
59537
+ const yLabels = [yfmt(yHiN), yfmt((yHiN + yLoN) / 2), yfmt(yLoN)];
59154
59538
  return /* @__PURE__ */ jsxRuntime.jsx(react.Box, { ref, width: "100%", height: `${hPx}px`, children: w2 > 0 && /* @__PURE__ */ jsxRuntime.jsx("svg", { width: w2, height: hPx, style: { display: "block" }, children: /* @__PURE__ */ jsxRuntime.jsxs(Group, { left: padL, top: padT, children: [
59155
59539
  /* @__PURE__ */ jsxRuntime.jsx(Line, { from: { x: 0, y: 0 }, to: { x: 0, y: innerH }, stroke: t4.rule, strokeWidth: 1 }),
59156
59540
  yTicks.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
@@ -59163,11 +59547,17 @@ function AreaRange({ lo: lo2, mid, hi, xTicks = [], yTicks = [], zero: zero2, to
59163
59547
  /* @__PURE__ */ jsxRuntime.jsx(LinePath, { data: data4, x: (d2) => xScale(d2.i), y: (d2) => yScale(d2.mid), curve: monotoneX, stroke: col, strokeWidth: t4.lineWidth }),
59164
59548
  marks.map((m2, i) => {
59165
59549
  const mc = t4.tone(m2.tone);
59166
- const dotY = yScale(mid[m2.at] ?? 0);
59550
+ const dotY = yScale(fin(mid[m2.at], 0));
59551
+ const labY = Math.max(9, dotY - 9);
59552
+ const atEnd = m2.at >= mid.length - 2;
59553
+ const atStart = m2.at <= 1;
59554
+ const align = atEnd ? "right" : atStart ? "left" : "center";
59555
+ const labW = m2.label.length * 6 + 6;
59556
+ const labX = atEnd ? -labW : atStart ? 0 : -labW / 2;
59167
59557
  return /* @__PURE__ */ jsxRuntime.jsxs(Group, { left: xScale(m2.at), children: [
59168
59558
  /* @__PURE__ */ jsxRuntime.jsx(Line, { from: { x: 0, y: -2 }, to: { x: 0, y: innerH }, stroke: mc, strokeWidth: 1, strokeDasharray: "2 3" }),
59169
59559
  /* @__PURE__ */ jsxRuntime.jsx(Circle, { cx: 0, cy: dotY, r: t4.dotRadius, fill: mc, stroke: t4.surface, strokeWidth: 1.5 }),
59170
- /* @__PURE__ */ jsxRuntime.jsx("text", { x: 0, y: Math.max(9, dotY - 9), textAnchor: m2.at >= mid.length - 2 ? "end" : m2.at <= 1 ? "start" : "middle", style: svgText(t4.labelSize, mc, t4.body, 500), children: m2.label })
59560
+ /* @__PURE__ */ jsxRuntime.jsx(ChartLabel, { x: labX, y: labY - 11, w: labW, align, size: t4.labelSize, weight: 500, color: mc, family: t4.body, ...m2.help ? { help: m2.help } : {}, children: m2.label })
59171
59561
  ] }, `mk${i}`);
59172
59562
  }),
59173
59563
  /* @__PURE__ */ jsxRuntime.jsx(Line, { from: { x: 0, y: innerH }, to: { x: innerW, y: innerH }, stroke: t4.rule, strokeWidth: 1 }),
@@ -59177,17 +59567,54 @@ function AreaRange({ lo: lo2, mid, hi, xTicks = [], yTicks = [], zero: zero2, to
59177
59567
  })
59178
59568
  ] }) }) });
59179
59569
  }
59570
+ function OverlapHistogram({ treated, control, domain = [0, 1], supportLabel, positivityOk, height: height2 }) {
59571
+ const t4 = useChartTheme();
59572
+ const [ref, w2] = useMeasuredWidth();
59573
+ const hPx = height2 ?? 150;
59574
+ const padL = 8, padR = 8, padT = 16, padB = 26;
59575
+ const innerW = Math.max(0, w2 - padL - padR);
59576
+ const innerH = hPx - padT - padB;
59577
+ const centerY = innerH / 2;
59578
+ const halfH = centerY - 4;
59579
+ const bins = Math.max(treated.length, control.length);
59580
+ const x2 = createLinearScale({ domain, range: [0, innerW] });
59581
+ const span = domain[1] - domain[0];
59582
+ const binW = bins > 0 ? innerW / bins : innerW;
59583
+ const barW = Math.max(1, binW * 0.78);
59584
+ const maxVal = Math.max(1e-9, ...treated, ...control);
59585
+ const tCol = t4.tone("pos");
59586
+ const cCol = t4.tone("muted");
59587
+ const ticks2 = [domain[0], (domain[0] + domain[1]) / 2, domain[1]];
59588
+ const cx2 = (i) => x2(domain[0] + (i + 0.5) / bins * span);
59589
+ return /* @__PURE__ */ jsxRuntime.jsx(react.Box, { ref, width: "100%", height: `${hPx}px`, children: w2 > 0 && /* @__PURE__ */ jsxRuntime.jsx("svg", { width: w2, height: hPx, style: { display: "block" }, children: /* @__PURE__ */ jsxRuntime.jsxs(Group, { left: padL, top: padT, children: [
59590
+ /* @__PURE__ */ jsxRuntime.jsx("text", { x: 0, y: -4, style: svgText(9, t4.faint, t4.body), children: "treated ↑" }),
59591
+ supportLabel && /* @__PURE__ */ jsxRuntime.jsx("text", { x: innerW, y: -4, textAnchor: "end", style: svgText(t4.labelSize, positivityOk ? t4.tone("pos") : t4.tone("warn"), t4.mono, 600), children: supportLabel }),
59592
+ Array.from({ length: bins }, (_2, i) => {
59593
+ const ht = Number(treated[i] ?? 0) / maxVal * halfH;
59594
+ const hc = Number(control[i] ?? 0) / maxVal * halfH;
59595
+ const left = cx2(i) - barW / 2;
59596
+ return /* @__PURE__ */ jsxRuntime.jsxs(Group, { children: [
59597
+ ht > 0 && /* @__PURE__ */ jsxRuntime.jsx("rect", { x: left, y: centerY - ht, width: barW, height: ht, fill: tCol, fillOpacity: 0.85, rx: 0.5 }),
59598
+ hc > 0 && /* @__PURE__ */ jsxRuntime.jsx("rect", { x: left, y: centerY, width: barW, height: hc, fill: cCol, fillOpacity: 0.6, rx: 0.5 })
59599
+ ] }, i);
59600
+ }),
59601
+ /* @__PURE__ */ jsxRuntime.jsx(Line, { from: { x: 0, y: centerY }, to: { x: innerW, y: centerY }, stroke: t4.ruleStrong, strokeWidth: 1 }),
59602
+ /* @__PURE__ */ jsxRuntime.jsx("text", { x: 0, y: innerH + 4, style: svgText(9, t4.faint, t4.body), children: "untreated ↓" }),
59603
+ ticks2.map((tk, i) => /* @__PURE__ */ jsxRuntime.jsx("text", { x: x2(tk), y: innerH + 16, textAnchor: i === 0 ? "start" : i === ticks2.length - 1 ? "end" : "middle", style: svgText(t4.labelSize, t4.muted, t4.mono), children: fmt$2(tk) }, `x${i}`))
59604
+ ] }) }) });
59605
+ }
59180
59606
  const arr = (v3) => v3 ? Array.from(v3, Number) : [];
59607
+ const arrI = (v3) => v3 ? Array.from(v3, Number) : [];
59181
59608
  const getOpt = (o2) => o2 && o2.type === "some" ? o2.value : void 0;
59182
- const fmt$1 = (x2) => Number.isInteger(x2) ? String(x2) : x2.toFixed(1);
59183
- const signed = (x2) => (x2 > 0 ? "+" : "") + fmt$1(x2);
59184
- const clamp01 = (x2) => x2 < 0 ? 0 : x2 > 1 ? 1 : x2;
59185
- const mean2 = (xs) => xs.length ? xs.reduce((a2, b2) => a2 + b2, 0) / xs.length : 0;
59186
- const std = (xs) => {
59187
- if (xs.length < 2) return 0;
59188
- const m2 = mean2(xs);
59189
- return Math.sqrt(xs.reduce((a2, b2) => a2 + (b2 - m2) * (b2 - m2), 0) / xs.length);
59609
+ const fmt$1 = (x2) => {
59610
+ const r2 = Number.isInteger(x2) ? String(x2) : x2.toFixed(1);
59611
+ return r2 === "-0.0" ? "0.0" : r2;
59190
59612
  };
59613
+ const signed = (x2) => {
59614
+ const f2 = fmt$1(x2);
59615
+ return f2 === "0" || f2 === "0.0" || f2.startsWith("-") ? f2 : `+${f2}`;
59616
+ };
59617
+ const clamp01 = (x2) => x2 < 0 ? 0 : x2 > 1 ? 1 : x2;
59191
59618
  function band(absStd) {
59192
59619
  if (absStd >= 0.66) return { level: "large gap", tone: "neg" };
59193
59620
  if (absStd >= 0.33) return { level: "some", tone: "warn" };
@@ -59202,11 +59629,22 @@ const kindOf = (cols, name) => {
59202
59629
  var _a2;
59203
59630
  return (_a2 = cols.find((c2) => c2.name === name)) == null ? void 0 : _a2.kind;
59204
59631
  };
59205
- function populationFieldIds(spec) {
59206
- return (getOpt(spec.population) ?? []).map((p2) => {
59207
- var _a2;
59208
- return (_a2 = p2.value) == null ? void 0 : _a2.fieldId;
59209
- }).filter((id3) => typeof id3 === "string");
59632
+ const VERDICT_TONE = {
59633
+ causal: "pos",
59634
+ modest: "warn",
59635
+ adjustment_insufficient: "warn",
59636
+ non_identifiable_positivity: "neg",
59637
+ not_estimable: "muted"
59638
+ };
59639
+ const VERDICT_LABEL = {
59640
+ causal: "Causal effect",
59641
+ modest: "Modest / unclear",
59642
+ adjustment_insufficient: "Not trustworthy yet",
59643
+ non_identifiable_positivity: "No fair comparison",
59644
+ not_estimable: "Can’t be estimated"
59645
+ };
59646
+ function deriveVerdict(v3) {
59647
+ return { tag: v3.type, tone: VERDICT_TONE[v3.type], label: VERDICT_LABEL[v3.type] };
59210
59648
  }
59211
59649
  function treatmentKind(cols, treatment) {
59212
59650
  switch (kindOf(cols, treatment)) {
@@ -59222,124 +59660,133 @@ function treatmentKind(cols, treatment) {
59222
59660
  return "value";
59223
59661
  }
59224
59662
  }
59225
- function deriveSpec(spec, cols, result, meta3, dataLen) {
59226
- const tKind = kindOf(cols, spec.treatment);
59227
- const outUnit = unitOf(meta3, spec.outcome);
59228
- const balByCol = new Map(((result == null ? void 0 : result.balance) ?? []).map((b2) => [b2.column, b2]));
59229
- const confounders = spec.confounders.map((col) => {
59663
+ function deriveSpec(config2, cols, result, meta3, dataLen) {
59664
+ const tKind = kindOf(cols, config2.treatment);
59665
+ const outUnit = unitOf(meta3, config2.outcome);
59666
+ const balByCol = /* @__PURE__ */ new Map();
59667
+ for (const b2 of (result == null ? void 0 : result.balance) ?? []) {
59668
+ const prev2 = balByCol.get(b2.base_column);
59669
+ if (!prev2 || Math.abs(b2.std_diff) > Math.abs(prev2.std_diff)) balByCol.set(b2.base_column, b2);
59670
+ }
59671
+ const confounders = config2.common_causes.map((col) => {
59230
59672
  const b2 = balByCol.get(col);
59231
- const absStd = b2 ? Math.abs(b2.stdDiff) : 0;
59673
+ const absStd = b2 ? Math.abs(b2.std_diff) : 0;
59232
59674
  const { level, tone } = band(absStd);
59233
- const dir = b2 ? b2.treatedMean < b2.controlMean ? "lower" : "higher" : "";
59234
- const reason = b2 ? `treated batches ran ${dir} on ${col}` : "measured once you Run";
59675
+ const dir = b2 ? b2.treated_mean < b2.control_mean ? "lower" : "higher" : "";
59676
+ const reason = b2 ? `the treated group ran ${dir} on ${col}` : "measured once you Run";
59235
59677
  return { col, reason, imbalance: clamp01(absStd), level, tone };
59236
59678
  });
59237
- const inPlay = /* @__PURE__ */ new Set([spec.treatment, spec.outcome, ...spec.confounders, ...populationFieldIds(spec)]);
59679
+ const inPlay = /* @__PURE__ */ new Set([config2.treatment, config2.outcome, ...config2.common_causes]);
59238
59680
  const suggest = cols.find((c2) => !inPlay.has(c2.name));
59239
59681
  const suggestion = suggest ? `add ${suggest.name}` : "";
59240
- const methodV = getOpt(spec.method);
59682
+ const methodV = getOpt(config2.method);
59241
59683
  const method = (methodV == null ? void 0 : methodV.type) === "propensity_score_weighting" ? "reweighting" : "regression";
59242
- const targetV = getOpt(spec.targetUnits);
59684
+ const targetV = getOpt(config2.estimand);
59243
59685
  const target = targetV && (targetV.type === "att" || targetV.type === "atc") ? "treated" : "all";
59244
- const trim2 = getOpt(spec.trim) !== void 0;
59245
59686
  return {
59246
- treatment: spec.treatment,
59247
- treatmentKind: treatmentKind(cols, spec.treatment),
59248
- outcome: spec.outcome,
59687
+ treatment: config2.treatment,
59688
+ treatmentKind: treatmentKind(cols, config2.treatment),
59689
+ outcome: config2.outcome,
59249
59690
  outcomeKind: outUnit ? `number · ${outUnit}` : "number",
59250
59691
  comparison: tKind === "boolean" ? "yes · vs no" : "high · vs low",
59251
59692
  confounders,
59252
59693
  suggestion,
59253
59694
  method,
59254
59695
  target,
59255
- trim: trim2,
59256
- dataLabel: `${result ? Number(result.nTotal) : dataLen} rows`
59696
+ dataLabel: `${result ? Number(result.n_total) : dataLen} rows`
59257
59697
  };
59258
59698
  }
59259
59699
  function balanceDisplay(b2, categorical) {
59260
- const isProp = categorical.has(b2.column) && b2.treatedMean >= 0 && b2.treatedMean <= 1 && b2.controlMean >= 0 && b2.controlMean <= 1;
59261
- return isProp ? `${Math.round(b2.treatedMean * 100)}% vs ${Math.round(b2.controlMean * 100)}%` : `${b2.treatedMean.toFixed(1)} vs ${b2.controlMean.toFixed(1)}`;
59262
- }
59263
- function deriveAnswer(spec, result, meta3) {
59264
- const ci = getOpt(result.ci);
59265
- const nci = getOpt(result.naiveCi);
59266
- const categorical = new Set(spec.categorical);
59267
- const balance = [...result.balance].sort((a2, b2) => Math.abs(b2.stdDiff) - Math.abs(a2.stdDiff)).map((b2) => ({
59700
+ const isProp = categorical.has(b2.base_column) && b2.treated_mean >= 0 && b2.treated_mean <= 1 && b2.control_mean >= 0 && b2.control_mean <= 1;
59701
+ return isProp ? `${Math.round(b2.treated_mean * 100)}% vs ${Math.round(b2.control_mean * 100)}%` : `${b2.treated_mean.toFixed(1)} vs ${b2.control_mean.toFixed(1)}`;
59702
+ }
59703
+ function deriveAnswer(config2, result, adj, meta3) {
59704
+ const ci = getOpt(adj.ci);
59705
+ const nci = getOpt(result.naive_ci);
59706
+ const categorical = new Set(getOpt(config2.categorical) ?? []);
59707
+ const balance = [...result.balance].sort((a2, b2) => Math.abs(b2.std_diff) - Math.abs(a2.std_diff)).map((b2) => ({
59268
59708
  col: b2.column,
59269
- treated: b2.treatedMean,
59270
- control: b2.controlMean,
59709
+ treated: b2.treated_mean,
59710
+ control: b2.control_mean,
59271
59711
  display: balanceDisplay(b2, categorical),
59272
- frac: clamp01(Math.abs(b2.stdDiff)),
59273
- tone: band(Math.abs(b2.stdDiff)).tone
59712
+ frac: clamp01(Math.abs(b2.std_diff)),
59713
+ tone: band(Math.abs(b2.std_diff)).tone
59274
59714
  }));
59275
- const nDropped = Number(result.nDropped);
59276
- const nTotal = Number(result.nTotal);
59715
+ const lo2 = (ci == null ? void 0 : ci.lower) ?? adj.effect;
59716
+ const hi = (ci == null ? void 0 : ci.upper) ?? adj.effect;
59717
+ const clear = result.verdict.type === "causal" || (ci ? lo2 > 0 || hi < 0 : false);
59718
+ const flip = Math.sign(result.naive) !== Math.sign(adj.effect) && result.naive !== 0;
59719
+ const nDropped = Number(result.n_dropped);
59720
+ const nTotal = Number(result.n_total);
59277
59721
  return {
59278
- treatment: spec.treatment,
59279
- outcome: spec.outcome,
59280
- unit: unitOf(meta3, spec.outcome),
59722
+ treatment: config2.treatment,
59723
+ outcome: config2.outcome,
59724
+ unit: unitOf(meta3, config2.outcome),
59281
59725
  naive: result.naive,
59282
59726
  naiveLo: (nci == null ? void 0 : nci.lower) ?? result.naive,
59283
59727
  naiveHi: (nci == null ? void 0 : nci.upper) ?? result.naive,
59284
- effect: result.effect,
59285
- lo: (ci == null ? void 0 : ci.lower) ?? result.effect,
59286
- hi: (ci == null ? void 0 : ci.upper) ?? result.effect,
59287
- nTotal: result.nTotal,
59288
- nTreated: result.nTreated,
59289
- nControl: result.nControl,
59728
+ effect: adj.effect,
59729
+ lo: lo2,
59730
+ hi,
59731
+ clear,
59732
+ flip,
59733
+ cautious: result.verdict.type === "adjustment_insufficient",
59734
+ nTotal: result.n_total,
59735
+ nTreated: result.n_treated,
59736
+ nControl: result.n_control,
59290
59737
  nCompared: BigInt(Math.max(0, nTotal - nDropped)),
59291
- nDropped: result.nDropped,
59738
+ nDropped: result.n_dropped,
59292
59739
  balance
59293
59740
  };
59294
59741
  }
59295
- function deriveRefuteCheck(c2) {
59296
- const effects = arr(c2.newEffects);
59297
- const m2 = mean2(effects);
59298
- const est = c2.estimatedEffect;
59299
- switch (c2.kind.type) {
59300
- case "placebo": {
59301
- const passed = Math.abs(m2) < Math.max(0.5, Math.abs(est) * 0.15);
59302
- return {
59303
- name: "Shuffle test",
59304
- desc: "Shuffle which rows were treated; a real effect should collapse to zero.",
59305
- value: `→ ${signed(m2)}`,
59306
- passed,
59307
- tip: { type: "some", value: "Randomly re-label which rows were treated. A genuine effect should vanish." }
59308
- };
59309
- }
59310
- case "data_subset": {
59311
- const passed = Math.abs(m2 - est) < Math.max(0.5, Math.abs(est) * 0.2);
59312
- return {
59313
- name: "Drop-some test",
59314
- desc: "Re-estimate on random subsamples; a trustworthy effect stays put.",
59315
- value: `${fmt$1(m2)} ± ${fmt$1(std(effects))}`,
59316
- passed,
59317
- tip: { type: "none", value: null }
59318
- };
59319
- }
59320
- case "random_common_cause": {
59321
- const passed = Math.abs(m2 - est) < Math.max(0.5, Math.abs(est) * 0.2);
59322
- return {
59323
- name: "Decoy cause",
59324
- desc: "Add an irrelevant random factor; the answer should not move.",
59325
- value: `${fmt$1(est)} → ${fmt$1(m2)}`,
59326
- passed,
59327
- tip: { type: "none", value: null }
59328
- };
59329
- }
59330
- default: {
59331
- const strengths = arr(getOpt(c2.strengths));
59332
- const tip = zeroCrossing(strengths, effects);
59333
- const passed = tip === null;
59334
- return {
59335
- name: "Hidden cause",
59336
- desc: "How strong an unrecorded common cause would need to be to overturn the result.",
59337
- value: tip === null ? "holds throughout" : `tips at ${fmt$1(tip)}`,
59338
- passed,
59339
- tip: { type: "some", value: "Something unrecorded could drive both the treatment choice and the outcome." }
59340
- };
59341
- }
59742
+ function deriveRefusal(config2, result) {
59743
+ const nT = Number(result.n_treated), nC = Number(result.n_control), nTot = Number(result.n_total);
59744
+ if (result.verdict.type === "not_estimable") {
59745
+ const reason = result.verdict.value || "The treatment barely varies, so no comparison can be formed.";
59746
+ return {
59747
+ kind: "not_estimable",
59748
+ title: `Can’t estimate the effect of ${config2.treatment}`,
59749
+ body: reason,
59750
+ evidence: [
59751
+ { label: "treated", value: String(nT) },
59752
+ { label: "untreated", value: String(nC) },
59753
+ { label: "rows", value: String(nTot) }
59754
+ ]
59755
+ };
59342
59756
  }
59757
+ if (result.verdict.type === "non_identifiable_positivity") {
59758
+ const pct = Math.round(result.overlap.common_support_frac * 100);
59759
+ return {
59760
+ kind: "positivity",
59761
+ title: "No like-for-like comparison exists",
59762
+ body: `The treated and untreated groups barely overlap on the confounders — only ${pct}% sit in a range where both occur — so there is no fair comparison to adjust toward.`,
59763
+ evidence: [
59764
+ { label: "common support", value: `${pct}%` },
59765
+ { label: "treated", value: String(nT) },
59766
+ { label: "untreated", value: String(nC) }
59767
+ ]
59768
+ };
59769
+ }
59770
+ return {
59771
+ kind: "not_estimable",
59772
+ title: `Can’t estimate the effect of ${config2.treatment}`,
59773
+ body: "The engine could not produce a like-for-like estimate for this configuration.",
59774
+ evidence: [
59775
+ { label: "treated", value: String(nT) },
59776
+ { label: "untreated", value: String(nC) },
59777
+ { label: "rows", value: String(nTot) }
59778
+ ]
59779
+ };
59780
+ }
59781
+ function deriveOverlap(o2) {
59782
+ const pct = Math.round(o2.common_support_frac * 100);
59783
+ return {
59784
+ treated: arr(o2.treated_propensity),
59785
+ control: arr(o2.control_propensity),
59786
+ commonSupportFrac: o2.common_support_frac,
59787
+ positivityOk: o2.positivity_ok,
59788
+ supportLabel: `${pct}% common support`
59789
+ };
59343
59790
  }
59344
59791
  function zeroCrossing(xs, ys) {
59345
59792
  for (let i = 1; i < ys.length && i < xs.length; i++) {
@@ -59351,27 +59798,80 @@ function zeroCrossing(xs, ys) {
59351
59798
  }
59352
59799
  return null;
59353
59800
  }
59354
- function deriveRefute(refute, result) {
59355
- const checks = refute.checks.map(deriveRefuteCheck);
59356
- const uc = refute.checks.find((c2) => c2.kind.type === "unobserved");
59357
- const ci = getOpt(result.ci);
59358
- const half = ci ? Math.abs(ci.upper - ci.lower) / 2 : 0;
59359
- const mid = uc ? arr(uc.newEffects) : [];
59360
- const strengths = uc ? arr(getOpt(uc.strengths)) : [];
59361
- const sensLo = mid.map((m2) => m2 - half);
59362
- const sensHi = mid.map((m2) => m2 + half);
59363
- const xTicks = strengths.length ? [strengths[0], strengths[Math.floor(strengths.length / 2)], strengths[strengths.length - 1]].map((_2, i) => ["none", "weaker", "stronger"][i]) : ["none", "weaker", "stronger"];
59364
- const lo2 = Math.min(0, ...sensLo), hi = Math.max(...sensHi, 0);
59365
- const yTicks = [fmt$1(lo2), fmt$1((lo2 + hi) / 2), fmt$1(hi)];
59366
- return { checks, sensLo, sensMid: mid, sensHi, sensXTicks: xTicks, sensYTicks: yTicks };
59367
- }
59368
- function deriveDose(dose, spec, meta3) {
59369
- var _a2;
59801
+ function deriveRefute(r2, adj) {
59802
+ const checks = [];
59803
+ const est = (adj == null ? void 0 : adj.effect) ?? 0;
59804
+ const placeboEffect = getOpt(r2.placebo_effect);
59805
+ const placeboPasses = getOpt(r2.placebo_passes);
59806
+ if (placeboEffect !== void 0 || placeboPasses !== void 0) {
59807
+ checks.push({
59808
+ name: "Shuffle test",
59809
+ desc: "Shuffle which rows were treated; a real effect should collapse to zero.",
59810
+ value: placeboEffect !== void 0 ? `→ ${signed(placeboEffect)}` : placeboPasses ? "passed" : "failed",
59811
+ passed: placeboPasses ?? (placeboEffect !== void 0 && Math.abs(placeboEffect) < Math.max(0.5, Math.abs(est) * 0.15)),
59812
+ tip: { type: "some", value: "Randomly re-label which rows were treated. A genuine effect should vanish." },
59813
+ help: "check_shuffle"
59814
+ });
59815
+ }
59816
+ const ds = getOpt(r2.data_subset_effect);
59817
+ if (ds !== void 0) {
59818
+ const dstd = getOpt(r2.data_subset_std) ?? 0;
59819
+ checks.push({
59820
+ name: "Drop-some test",
59821
+ desc: "Re-estimate on random subsamples; a trustworthy effect stays put.",
59822
+ value: `${fmt$1(ds)} ± ${fmt$1(dstd)}`,
59823
+ passed: Math.abs(ds - est) < Math.max(0.5, Math.abs(est) * 0.2),
59824
+ tip: { type: "none", value: null },
59825
+ help: "check_dropsome"
59826
+ });
59827
+ }
59828
+ const rcc = getOpt(r2.random_cc_within_ci);
59829
+ if (rcc !== void 0) {
59830
+ checks.push({
59831
+ name: "Decoy cause",
59832
+ desc: "Add an irrelevant random factor; the answer should not move.",
59833
+ value: rcc ? "within range" : "moved",
59834
+ passed: rcc,
59835
+ tip: { type: "none", value: null },
59836
+ help: "check_decoy"
59837
+ });
59838
+ }
59839
+ const sens = getOpt(r2.sensitivity);
59840
+ const evalue = getOpt(r2.robustness_value);
59841
+ if (sens !== void 0 || evalue !== void 0) {
59842
+ const strengths = sens ? arr(sens.strengths) : [];
59843
+ const effects = sens ? arr(sens.effects) : [];
59844
+ const tip = sens ? zeroCrossing(strengths, effects) : null;
59845
+ const value = tip !== null ? `tips at ${fmt$1(tip)}` : evalue !== void 0 ? `E-value ${fmt$1(evalue)}` : "holds throughout";
59846
+ checks.push({
59847
+ name: "Hidden cause",
59848
+ desc: "How strong an unrecorded common cause would need to be to overturn the result.",
59849
+ value,
59850
+ passed: tip === null,
59851
+ tip: { type: "some", value: "Something unrecorded could drive both the treatment choice and the outcome." },
59852
+ help: "check_hidden"
59853
+ });
59854
+ }
59855
+ let sensVM = null;
59856
+ if (sens !== void 0) {
59857
+ const mid = arr(sens.effects);
59858
+ if (mid.length) {
59859
+ const ci = getOpt(adj == null ? void 0 : adj.ci);
59860
+ const half = ci ? Math.abs(ci.upper - ci.lower) / 2 : 0;
59861
+ const lo2 = mid.map((m2) => m2 - half);
59862
+ const hi = mid.map((m2) => m2 + half);
59863
+ const yLo = Math.min(0, ...lo2), yHi = Math.max(...hi, 0);
59864
+ sensVM = { lo: lo2, mid, hi, xTicks: ["none", "weaker", "stronger"], yTicks: [fmt$1(yLo), fmt$1((yLo + yHi) / 2), fmt$1(yHi)] };
59865
+ }
59866
+ }
59867
+ return { checks, sens: sensVM };
59868
+ }
59869
+ function deriveDose(dose, config2, meta3) {
59370
59870
  const grid = arr(dose.grid);
59371
59871
  const mid = arr(dose.effect);
59372
59872
  const lo2 = getOpt(dose.lower) ? arr(getOpt(dose.lower)) : mid.slice();
59373
59873
  const hi = getOpt(dose.upper) ? arr(getOpt(dose.upper)) : mid.slice();
59374
- const sizes = dose.size ? Array.from(dose.size, Number) : [];
59874
+ const sizes = arrI(dose.size);
59375
59875
  const featureUnit = unitOf(meta3, dose.feature);
59376
59876
  const here = sizes.length ? sizes.indexOf(Math.max(...sizes)) : 0;
59377
59877
  const marg = mid.map((m2, i) => i === 0 ? 0 : m2 - mid[i - 1]);
@@ -59391,31 +59891,28 @@ function deriveDose(dose, spec, meta3) {
59391
59891
  let lastStep = margSteps.length - 1;
59392
59892
  while (lastStep > 0 && Math.abs(margSteps[lastStep]) < 0.12 * maxMarg) lastStep--;
59393
59893
  const marginal = margSteps.slice(0, lastStep + 1).map((v3, i) => ({ label: `${fmt$1(grid[i + 1])}`, value: v3, frac: clamp01(Math.abs(v3) / maxMarg) }));
59394
- const segLabels = ((_a2 = getOpt(dose.segments)) == null ? void 0 : _a2.map((s2) => s2.label)) ?? [];
59395
- const segments = segLabels.length ? ["all", ...segLabels] : ["all"];
59396
59894
  const stepFrom = here < sweet ? here : Math.max(0, sweet - 1);
59397
59895
  const gainToSweet = (mid[sweet] ?? 0) - (mid[stepFrom] ?? 0);
59398
59896
  const nextGain = sweet + 1 < mid.length ? mid[sweet + 1] - mid[sweet] : 0;
59399
59897
  const tradeoff = `Going from ${fmt$1(grid[stepFrom] ?? 0)} to ${fmt$1(grid[sweet] ?? 0)} adds ${signed(gainToSweet)}; the next step adds only ${signed(nextGain)}.`;
59898
+ const marks = [];
59899
+ if (sizes.length) marks.push({ at: here, label: "you are here", tone: "muted", help: "dose_here" });
59900
+ marks.push({ at: sweet, label: "sweet spot", tone: "pos", help: "dose_sweet" });
59400
59901
  return {
59401
59902
  feature: dose.feature,
59402
- outcome: spec.outcome,
59903
+ outcome: config2.outcome,
59403
59904
  lo: lo2,
59404
59905
  mid,
59405
59906
  hi,
59406
59907
  xTicks,
59407
59908
  yTicks,
59408
- marks: [
59409
- { at: BigInt(here), label: "you are here", tone: "muted" },
59410
- { at: BigInt(sweet), label: "sweet spot", tone: "pos" }
59411
- ],
59909
+ marks,
59412
59910
  recoLabel: `≈ ${fmt$1(grid[sweet] ?? 0)}${featureUnit ? " " + featureUnit : ""}`,
59413
59911
  recoEffect: mid[sweet] ?? 0,
59414
59912
  recoLo: lo2[sweet] ?? 0,
59415
59913
  recoHi: hi[sweet] ?? 0,
59416
59914
  tradeoff,
59417
- marginal,
59418
- segments
59915
+ marginal
59419
59916
  };
59420
59917
  }
59421
59918
  function relTime(d2, now2) {
@@ -59425,30 +59922,126 @@ function relTime(d2, now2) {
59425
59922
  if (days < 7) return d2.toLocaleDateString(void 0, { weekday: "short" });
59426
59923
  return "last wk";
59427
59924
  }
59925
+ const VERDICT_WORD = {
59926
+ causal: "causal",
59927
+ modest: "modest",
59928
+ adjustment_insufficient: "not robust",
59929
+ non_identifiable_positivity: "no overlap",
59930
+ not_estimable: "n/a"
59931
+ };
59428
59932
  function deriveJournalRow(row, now2) {
59429
- const ci = getOpt(row.ci);
59430
- const clear = ci ? ci.lower > 0 || ci.upper < 0 : true;
59431
- const dirUp = row.effect > 0;
59933
+ const adj = getOpt(row.adjusted);
59432
59934
  return {
59433
- treatment: row.spec.treatment,
59434
- outcome: row.spec.outcome,
59435
- confounders: row.spec.confounders.join(", "),
59436
- effect: signed(row.effect),
59437
- verdict: clear ? dirUp ? "higher" : "lower" : "no clear effect",
59438
- verdictTone: clear ? "pos" : "warn",
59439
- who: row.committedBy,
59440
- when: relTime(row.committedAt, now2)
59441
- };
59442
- }
59443
- function deriveView(spec, cols, result, refute, dose, journal, meta3, dataLen, now2) {
59935
+ treatment: row.config.treatment,
59936
+ outcome: row.config.outcome,
59937
+ confounders: row.config.common_causes.join(", "),
59938
+ effect: adj !== void 0 ? signed(adj) : signed(row.naive),
59939
+ verdict: VERDICT_WORD[row.verdict.type],
59940
+ verdictTone: VERDICT_TONE[row.verdict.type],
59941
+ who: row.committed_by,
59942
+ when: relTime(row.committed_at, now2)
59943
+ };
59944
+ }
59945
+ function deriveView(config2, ranConfig, cols, result, journal, meta3, dataLen, now2) {
59946
+ const adj = result ? getOpt(result.adjusted) : void 0;
59947
+ const refutation = result ? getOpt(result.refutation) : void 0;
59948
+ const doseR = result ? getOpt(result.dose_response) : void 0;
59444
59949
  return {
59445
- spec: deriveSpec(spec, cols, result, meta3, dataLen),
59446
- answer: result ? deriveAnswer(spec, result, meta3) : null,
59447
- refute: refute && result ? deriveRefute(refute, result) : null,
59448
- dose: dose ? deriveDose(dose, spec, meta3) : null,
59950
+ // The set-up rail reflects the LIVE config (the editor); the result deck
59951
+ // reflects RANCONFIG the config that produced `result` so the result
59952
+ // strings never drift ahead of the numbers on a live picker edit.
59953
+ spec: deriveSpec(config2, cols, result, meta3, dataLen),
59954
+ verdict: result ? deriveVerdict(result.verdict) : null,
59955
+ answer: result && adj ? deriveAnswer(ranConfig, result, adj, meta3) : null,
59956
+ refusal: result && !adj ? deriveRefusal(ranConfig, result) : null,
59957
+ overlap: result ? deriveOverlap(result.overlap) : null,
59958
+ refute: refutation ? deriveRefute(refutation, adj) : null,
59959
+ dose: doseR && doseR.effect.length ? deriveDose(doseR, ranConfig, meta3) : null,
59449
59960
  journal: journal ? journal.map((r2) => deriveJournalRow(r2, now2)) : null
59450
59961
  };
59451
59962
  }
59963
+ const DESIGN_HEADLINE = {
59964
+ detect_observed: "Confirm it with a controlled trial",
59965
+ de_bias: "Make it trustworthy with a controlled trial",
59966
+ resolve_vs_null: "Settle it with a bigger trial",
59967
+ restrict_to_overlap: "You can’t compare yet",
59968
+ create_control: "Hold some back next time"
59969
+ };
59970
+ function deriveDesignOption(o2) {
59971
+ return {
59972
+ label: o2.label,
59973
+ nTotal: Number(o2.n_total),
59974
+ nTreated: Number(o2.n_treated),
59975
+ nControl: Number(o2.n_control),
59976
+ treatedShare: o2.treated_share
59977
+ };
59978
+ }
59979
+ function deriveDesign(d2, result, ranConfig, meta3) {
59980
+ const basis = d2.basis.type;
59981
+ const outcome = ranConfig.outcome;
59982
+ const unit2 = unitOf(meta3, outcome);
59983
+ const options2 = d2.options.map(deriveDesignOption);
59984
+ const primary = options2[0] ?? { label: "Even split", nTotal: 0, nTreated: 0, nControl: 0, treatedShare: 0.5 };
59985
+ const balByCol = /* @__PURE__ */ new Map();
59986
+ for (const b2 of (result == null ? void 0 : result.balance) ?? []) {
59987
+ const prev2 = balByCol.get(b2.base_column);
59988
+ if (!prev2 || Math.abs(b2.std_diff) > Math.abs(prev2.std_diff)) balByCol.set(b2.base_column, b2);
59989
+ }
59990
+ const matchOn = d2.match_on.map((col) => {
59991
+ var _a2;
59992
+ const b2 = balByCol.get(col);
59993
+ const absStd = b2 ? Math.abs(b2.std_diff) : 0;
59994
+ const { tone } = band(absStd);
59995
+ return { col, frac: clamp01(absStd), tone, display: getOpt((_a2 = colMeta(meta3, col)) == null ? void 0 : _a2.label) ?? col };
59996
+ });
59997
+ const ns = arrI(d2.power_curve.n);
59998
+ const powers = arr(d2.power_curve.power);
59999
+ const len = Math.min(ns.length, powers.length);
60000
+ const nearest = (target) => {
60001
+ let bi = 0, bd = Infinity;
60002
+ for (let i = 0; i < len; i++) {
60003
+ const dd = Math.abs(ns[i] - target);
60004
+ if (dd < bd) {
60005
+ bd = dd;
60006
+ bi = i;
60007
+ }
60008
+ }
60009
+ return bi;
60010
+ };
60011
+ const xTicks = len ? [String(ns[0]), String(ns[Math.floor((len - 1) / 2)]), String(ns[len - 1])] : [];
60012
+ const marks = [];
60013
+ if (len) marks.push({ at: nearest(primary.nTotal), label: `target ${(d2.target_power * 100).toFixed(0)}%`, tone: "pos", help: "validate_power" });
60014
+ const cp = getOpt(d2.current_power);
60015
+ if (cp !== void 0 && len) {
60016
+ const curN = result ? Number(result.n_total) : 0;
60017
+ if (curN > 0) {
60018
+ const offScale = curN >= (ns[len - 1] ?? 0);
60019
+ marks.push({
60020
+ at: offScale ? len - 1 : nearest(curN),
60021
+ label: offScale ? `you’re here · ${(cp * 100).toFixed(0)}% (off-scale)` : `you’re here ${(cp * 100).toFixed(0)}%`,
60022
+ tone: "muted",
60023
+ help: "validate_power"
60024
+ });
60025
+ }
60026
+ }
60027
+ const faint = d2.outcome_sd > 0 && Math.abs(d2.target_effect) / d2.outcome_sd <= 0.011;
60028
+ return {
60029
+ headline: DESIGN_HEADLINE[basis] ?? "Validate this",
60030
+ rationale: d2.rationale,
60031
+ primary,
60032
+ alternates: options2.slice(1),
60033
+ matchOn,
60034
+ curve: { mid: powers.slice(0, len).map((p2) => p2 * 100), xTicks, marks },
60035
+ targetLabel: `${signed(d2.target_effect)}${unit2 ? ` ${unit2}` : ""}`,
60036
+ targetPctLabel: `${(d2.target_power * 100).toFixed(0)}%`,
60037
+ holdback: basis === "create_control",
60038
+ showOverlap: basis === "restrict_to_overlap",
60039
+ faint
60040
+ };
60041
+ }
60042
+ const SUBJECT_ONE = "record";
60043
+ const SUBJECT_MANY = "records";
60044
+ const AUTORUN_MAX_ROWS = 5e4;
59452
60045
  const TONE_TOKEN = {
59453
60046
  neg: "fg.danger",
59454
60047
  pos: "fg.success",
@@ -59486,13 +60079,73 @@ function useColumns(workspace, source) {
59486
60079
  };
59487
60080
  }, [types == null ? void 0 : types.sourceType]);
59488
60081
  }
59489
- const STR_TYPE = east.variant("String", null);
59490
- function Cap({ children: children2 }) {
59491
- return /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "caption.eyebrow", fontSize: "9px", display: "flex", alignItems: "center", gap: "1.5", mb: "2.5", children: children2 });
60082
+ function rowMatchesAll(row, preds) {
60083
+ return preds.every((p2) => {
60084
+ const { fieldId, op } = p2.value;
60085
+ const v3 = row[fieldId];
60086
+ const ov = op.value;
60087
+ switch (op.type) {
60088
+ case "eq":
60089
+ case "is":
60090
+ return v3 === ov;
60091
+ case "neq":
60092
+ return v3 !== ov;
60093
+ case "lt":
60094
+ return num(v3) != null && num(ov) != null ? num(v3) < num(ov) : true;
60095
+ case "lte":
60096
+ return num(v3) != null && num(ov) != null ? num(v3) <= num(ov) : true;
60097
+ case "gt":
60098
+ return num(v3) != null && num(ov) != null ? num(v3) > num(ov) : true;
60099
+ case "gte":
60100
+ return num(v3) != null && num(ov) != null ? num(v3) >= num(ov) : true;
60101
+ case "in":
60102
+ return ov instanceof Set ? ov.has(v3) : true;
60103
+ case "notIn":
60104
+ return ov instanceof Set ? !ov.has(v3) : true;
60105
+ default:
60106
+ return true;
60107
+ }
60108
+ });
60109
+ }
60110
+ const num = (x2) => typeof x2 === "number" ? x2 : typeof x2 === "bigint" ? Number(x2) : null;
60111
+ function Cap({ children: children2, help }) {
60112
+ return /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "caption.eyebrow", fontSize: "9px", mb: "2.5", children: help ? /* @__PURE__ */ jsxRuntime.jsx(Help, { id: help, children: children2 }) : children2 });
59492
60113
  }
59493
60114
  function Card({ children: children2, mt = "3" }) {
59494
60115
  return /* @__PURE__ */ jsxRuntime.jsx(react.Box, { layerStyle: "card", p: "3.5", borderRadius: "lg", mt, children: children2 });
59495
60116
  }
60117
+ function RunError({ error: error3 }) {
60118
+ const tail = (error3.stderr || error3.stdout || "").trim().split("\n").slice(-12).join("\n");
60119
+ return /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { layerStyle: "banner.stale", display: "flex", flexDirection: "column", gap: "2", mt: "3", children: [
60120
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "inline-flex", alignItems: "flex-start", gap: "2", color: "fg.danger", children: [
60121
+ /* @__PURE__ */ jsxRuntime.jsx(react.Box, { as: "span", mt: "0.5", fontSize: "12px", flexShrink: "0", children: /* @__PURE__ */ jsxRuntime.jsx(FontAwesomeIcon, { icon: faTriangleExclamation }) }),
60122
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Text, { textStyle: "body.sm", color: "fg.default", children: [
60123
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { as: "span", fontWeight: "bold", children: "Could not run the experiment." }),
60124
+ " ",
60125
+ error3.message
60126
+ ] })
60127
+ ] }),
60128
+ tail && /* @__PURE__ */ jsxRuntime.jsx(
60129
+ react.Box,
60130
+ {
60131
+ as: "pre",
60132
+ m: "0",
60133
+ p: "2",
60134
+ maxH: "160px",
60135
+ overflow: "auto",
60136
+ bg: "bg.canvas",
60137
+ borderRadius: "md",
60138
+ borderWidth: "1px",
60139
+ borderColor: "border.subtle",
60140
+ fontFamily: "mono",
60141
+ fontSize: "11px",
60142
+ color: "fg.muted",
60143
+ whiteSpace: "pre-wrap",
60144
+ children: tail
60145
+ }
60146
+ )
60147
+ ] });
60148
+ }
59496
60149
  function ActionButton({ button, variant: variant2, label, onClick, disabled: disabled2, pulse = false }) {
59497
60150
  return /* @__PURE__ */ jsxRuntime.jsx(
59498
60151
  react.Box,
@@ -59508,8 +60161,46 @@ function ActionButton({ button, variant: variant2, label, onClick, disabled: dis
59508
60161
  }
59509
60162
  );
59510
60163
  }
60164
+ function DeckSkeleton({ tab }) {
60165
+ const sk = react.useRecipe({ key: "skeleton" });
60166
+ const bar = (w2, h2) => /* @__PURE__ */ jsxRuntime.jsx(react.Box, { css: sk({ variant: "line" }), width: w2, height: h2 });
60167
+ const block = (h2) => /* @__PURE__ */ jsxRuntime.jsx(react.Box, { css: sk({ variant: "block" }), width: "100%", minHeight: h2 });
60168
+ return /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { p: "4.5", display: "flex", flexDirection: "column", gap: "4", children: [
60169
+ tab === "answer" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
60170
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "flex", gap: "4.5", alignItems: "flex-end", children: [
60171
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "flex", flexDirection: "column", gap: "2", children: [
60172
+ bar("64px", "10px"),
60173
+ bar("120px", "32px")
60174
+ ] }),
60175
+ bar("170px", "24px")
60176
+ ] }),
60177
+ block("120px"),
60178
+ block("120px")
60179
+ ] }),
60180
+ tab === "trust" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
60181
+ bar("70%", "14px"),
60182
+ block("160px")
60183
+ ] }),
60184
+ tab === "dose" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
60185
+ block("240px"),
60186
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "grid", gridTemplateColumns: "1fr 1fr", gap: "3", children: [
60187
+ block("90px"),
60188
+ block("90px")
60189
+ ] })
60190
+ ] }),
60191
+ tab === "validate" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
60192
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "flex", gap: "5", alignItems: "flex-end", children: [
60193
+ bar("120px", "32px"),
60194
+ bar("200px", "10px")
60195
+ ] }),
60196
+ block("110px"),
60197
+ block("150px")
60198
+ ] })
60199
+ ] });
60200
+ }
60201
+ const experimentValueEqual = east.equalFor(internal.Experiment.Component.schema);
59511
60202
  const EastChakraExperiment = React.memo(function EastChakraExperiment2({ value }) {
59512
- var _a2, _b2, _c2;
60203
+ var _a2, _b2, _c2, _d2;
59513
60204
  const v3 = value;
59514
60205
  const button = react.useRecipe({ key: "button" });
59515
60206
  const chip = react.useRecipe({ key: "chip" });
@@ -59519,12 +60210,19 @@ const EastChakraExperiment = React.memo(function EastChakraExperiment2({ value }
59519
60210
  const es = react.useSlotRecipe({ key: "eyebrowRow" })({});
59520
60211
  const workspace = getReactiveDatasetCache().getConfig().workspace ?? "";
59521
60212
  const data4 = useBindingValue(v3.data);
59522
- const specBind = useBindingValue(v3.spec);
60213
+ const configBind = useBindingValue(v3.config);
59523
60214
  const journalBind = useBindingValue(v3.journal.type === "some" ? v3.journal.value : null);
60215
+ const populationBind = useBindingValue(v3.population.type === "some" ? v3.population.value : null);
59524
60216
  const meta3 = getOpt(v3.columnMeta);
59525
60217
  const readonly = getOpt(v3.readonly) ?? false;
59526
60218
  const { columns, rowArrayType } = useColumns(workspace, v3.data.source);
59527
- const spec = specBind.value;
60219
+ const config2 = configBind.value;
60220
+ const [localPop, setLocalPop] = React.useState([]);
60221
+ const population = populationBind.value ?? localPop;
60222
+ const filteredRows = React.useMemo(() => {
60223
+ if (!data4.value) return null;
60224
+ return population.length ? data4.value.filter((r2) => rowMatchesAll(r2, population)) : data4.value;
60225
+ }, [data4.value, population]);
59528
60226
  const fields = React.useMemo(
59529
60227
  () => columns.filter((c2) => c2.kind !== "other").map((c2) => {
59530
60228
  var _a3;
@@ -59536,132 +60234,176 @@ const EastChakraExperiment = React.memo(function EastChakraExperiment2({ value }
59536
60234
  }),
59537
60235
  [columns, meta3]
59538
60236
  );
59539
- const estInputs = React.useMemo(() => rowArrayType ? [rowArrayType, internal.Experiment.Types.Spec] : null, [rowArrayType]);
59540
- const doseInputs = React.useMemo(() => rowArrayType ? [rowArrayType, internal.Experiment.Types.Spec, east.fromEastTypeValue(STR_TYPE)] : null, [rowArrayType]);
59541
- const estimate = useFuncCall(v3.estimate.name, estInputs, internal.Experiment.Types.Result);
59542
- const refute = useFuncCall(v3.refute.type === "some" ? v3.refute.value.name : null, estInputs, internal.Experiment.Types.Refute);
59543
- const dose = useFuncCall(v3.dose.type === "some" ? v3.dose.value.name : null, doseInputs, internal.Experiment.Types.Dose);
59544
- const doseFeature = React.useMemo(() => {
59545
- if (!spec) return "";
59546
- const f2 = columns.find((c2) => c2.kind === "float" && c2.name !== spec.outcome);
59547
- return (f2 == null ? void 0 : f2.name) ?? spec.treatment;
59548
- }, [columns, spec]);
60237
+ const estInputs = React.useMemo(() => rowArrayType ? [rowArrayType, internal.Experiment.Types.Config] : null, [rowArrayType]);
60238
+ const experiment = useFuncCall(v3.experiment.name, estInputs, internal.Experiment.Types.Result);
60239
+ const hasDesign = v3.design.type === "some";
60240
+ const designInputs = React.useMemo(
60241
+ () => rowArrayType ? [rowArrayType, internal.Experiment.Types.Config, internal.Experiment.Types.Result, internal.Experiment.Types.DesignConfig] : null,
60242
+ [rowArrayType]
60243
+ );
60244
+ const design = useFuncCall(v3.design.type === "some" ? v3.design.value.name : null, designInputs, internal.Experiment.Types.Design);
60245
+ const pendingConfigRef = React.useRef(null);
60246
+ const [ranConfig, setRanConfig] = React.useState(null);
59549
60247
  const runAll = React.useCallback(() => {
59550
- if (!data4.value || !spec) return;
59551
- estimate.call(data4.value, spec);
59552
- if (v3.refute.type === "some") refute.call(data4.value, spec);
59553
- if (v3.dose.type === "some") dose.call(data4.value, spec, doseFeature);
59554
- }, [data4.value, spec, estimate, refute, dose, doseFeature, v3.refute.type, v3.dose.type]);
60248
+ if (!filteredRows || !config2) return;
60249
+ pendingConfigRef.current = config2;
60250
+ experiment.call(filteredRows, config2);
60251
+ }, [filteredRows, config2, experiment]);
59555
60252
  const autoRan = React.useRef(false);
59556
60253
  React.useEffect(() => {
59557
60254
  if (autoRan.current) return;
59558
- if (!data4.value || !spec || !rowArrayType) return;
59559
- if (estimate.result !== null || estimate.status === "running") {
60255
+ if (!filteredRows || !config2 || !rowArrayType) return;
60256
+ if (experiment.result !== null || experiment.status === "running") {
59560
60257
  autoRan.current = true;
59561
60258
  return;
59562
60259
  }
59563
- if (estimate.status !== "idle") return;
60260
+ if (experiment.status !== "idle") return;
59564
60261
  autoRan.current = true;
60262
+ if (filteredRows.length > AUTORUN_MAX_ROWS) return;
59565
60263
  runAll();
59566
- }, [data4.value, spec, rowArrayType, estimate.result, estimate.status, runAll]);
60264
+ }, [filteredRows, config2, rowArrayType, experiment.result, experiment.status, runAll]);
60265
+ React.useEffect(() => {
60266
+ if (experiment.result !== null) setRanConfig(pendingConfigRef.current);
60267
+ }, [experiment.result]);
59567
60268
  const [stale, setStale] = React.useState(false);
59568
- const editSpec = React.useCallback((next2) => {
60269
+ const editConfig = React.useCallback((next2) => {
60270
+ if (readonly) return;
60271
+ setStale(true);
60272
+ queueMicrotask(() => configBind.mutate(next2));
60273
+ }, [readonly, configBind]);
60274
+ const editPopulation = React.useCallback((next2) => {
60275
+ if (readonly) return;
59569
60276
  setStale(true);
59570
- queueMicrotask(() => specBind.mutate(next2));
59571
- }, [specBind]);
60277
+ if (v3.population.type === "some") queueMicrotask(() => populationBind.mutate(next2));
60278
+ else setLocalPop(next2);
60279
+ }, [readonly, v3.population.type, populationBind]);
59572
60280
  const onRun = React.useCallback(() => {
59573
60281
  runAll();
59574
60282
  setStale(false);
59575
60283
  }, [runAll]);
59576
60284
  const onCommit = React.useCallback(async () => {
59577
- if (!spec || !estimate.result) return;
59578
- const row = { spec, effect: estimate.result.effect, ci: estimate.result.ci, committedAt: /* @__PURE__ */ new Date(), committedBy: "you" };
60285
+ if (!config2 || !experiment.result) return;
60286
+ const r2 = experiment.result;
60287
+ const row = {
60288
+ config: config2,
60289
+ verdict: r2.verdict,
60290
+ naive: r2.naive,
60291
+ adjusted: r2.adjusted.type === "some" ? east.some(r2.adjusted.value.effect) : east.none,
60292
+ committed_at: /* @__PURE__ */ new Date(),
60293
+ committed_by: "you"
60294
+ };
59579
60295
  journalBind.mutate([row, ...journalBind.value ?? []]);
59580
60296
  try {
59581
60297
  await journalBind.commit();
59582
- await specBind.commit();
60298
+ await configBind.commit();
60299
+ if (v3.population.type === "some") await populationBind.commit();
59583
60300
  setStale(false);
59584
60301
  } catch {
59585
60302
  }
59586
- }, [spec, estimate.result, journalBind, specBind]);
60303
+ }, [config2, experiment.result, journalBind, configBind, populationBind, v3.population.type]);
59587
60304
  const [tab, setTab] = React.useState(((_a2 = getOpt(v3.defaultTab)) == null ? void 0 : _a2.type) ?? "answer");
60305
+ const [guidance, setGuidance] = React.useState(true);
59588
60306
  const now2 = React.useMemo(() => /* @__PURE__ */ new Date(), []);
59589
- const nRows = ((_b2 = data4.value) == null ? void 0 : _b2.length) ?? 0;
60307
+ const nRows = (filteredRows == null ? void 0 : filteredRows.length) ?? 0;
60308
+ const helpVars = React.useMemo(() => {
60309
+ const rc = ranConfig ?? config2;
60310
+ const labelOf = (col) => {
60311
+ var _a3;
60312
+ return col ? getOpt((_a3 = meta3 == null ? void 0 : meta3.get(col)) == null ? void 0 : _a3.label) ?? col : "";
60313
+ };
60314
+ return { treatment: labelOf(rc == null ? void 0 : rc.treatment), outcome: labelOf(rc == null ? void 0 : rc.outcome), subject: SUBJECT_ONE, subjects: SUBJECT_MANY };
60315
+ }, [ranConfig, config2, meta3]);
59590
60316
  const view = React.useMemo(() => {
59591
- if (!spec) return null;
59592
- return deriveView(spec, columns, estimate.result, refute.result, dose.result, journalBind.value, meta3, nRows, now2);
59593
- }, [spec, columns, estimate.result, refute.result, dose.result, journalBind.value, meta3, nRows, now2]);
59594
- if (!spec || !view || !view.answer) {
59595
- const failed = estimate.status === "failed";
59596
- return /* @__PURE__ */ jsxRuntime.jsx(react.Box, { layerStyle: "frame", p: "6", children: /* @__PURE__ */ jsxRuntime.jsx(react.Text, { className: failed ? void 0 : "elara-skeleton", textStyle: "body.sm", color: "fg.muted", children: failed ? "Could not run the experiment." : "Loading experiment…" }) });
59597
- }
59598
- const { spec: vs, answer: a2, refute: vr, dose: vd, journal } = view;
59599
- const clear = a2.lo > 0 || a2.hi < 0;
59600
- const ans = a2;
60317
+ if (!config2) return null;
60318
+ return deriveView(config2, ranConfig ?? config2, columns, experiment.result, journalBind.value, meta3, nRows, now2);
60319
+ }, [config2, ranConfig, columns, experiment.result, journalBind.value, meta3, nRows, now2]);
60320
+ const designConfigValue = React.useMemo(() => ({
60321
+ alpha: east.none,
60322
+ target_power: east.none,
60323
+ materiality: east.none,
60324
+ treated_shares: east.some([0.5, 0.3])
60325
+ }), []);
60326
+ const designSnapRef = React.useRef(null);
60327
+ React.useEffect(() => {
60328
+ var _a3;
60329
+ if (tab !== "validate" || !hasDesign || design.pending) return;
60330
+ if (!filteredRows || !config2 || !experiment.result) return;
60331
+ if (((_a3 = designSnapRef.current) == null ? void 0 : _a3.result) === experiment.result) return;
60332
+ designSnapRef.current = { result: experiment.result, config: ranConfig ?? config2 };
60333
+ design.call(filteredRows, ranConfig ?? config2, experiment.result, designConfigValue);
60334
+ }, [tab, hasDesign, filteredRows, config2, ranConfig, experiment.result, design, designConfigValue]);
60335
+ const vmDesign = React.useMemo(
60336
+ () => design.result && designSnapRef.current ? deriveDesign(design.result, designSnapRef.current.result, designSnapRef.current.config, meta3) : null,
60337
+ [design.result, meta3]
60338
+ );
60339
+ const designFresh = design.result !== null && ((_b2 = designSnapRef.current) == null ? void 0 : _b2.result) === experiment.result && !design.pending;
60340
+ if (!config2 || !view) {
60341
+ const failed2 = experiment.status === "failed";
60342
+ return /* @__PURE__ */ jsxRuntime.jsx(react.Box, { layerStyle: "frame", p: "6", children: failed2 && experiment.error ? /* @__PURE__ */ jsxRuntime.jsx(RunError, { error: experiment.error }) : /* @__PURE__ */ jsxRuntime.jsx(react.Text, { className: failed2 ? void 0 : "elara-skeleton", textStyle: "body.sm", color: "fg.muted", children: failed2 ? "Could not run the experiment." : "Loading experiment…" }) });
60343
+ }
60344
+ const { spec: vs, answer: a2, refusal: ref, overlap: ov, refute: vr, dose: vd, journal, verdict } = view;
59601
60345
  const higherBetter = getOpt((_c2 = meta3 == null ? void 0 : meta3.get(vs.outcome)) == null ? void 0 : _c2.higherIsBetter);
59602
- const dirUp = ans.effect > 0;
59603
- const statusWord = higherBetter === void 0 ? dirUp ? "Higher" : "Lower" : dirUp === higherBetter ? "Better" : "Worse";
59604
- const flip = Math.sign(ans.naive) !== Math.sign(ans.effect) && ans.naive !== 0;
59605
- const top = ans.balance[0] ?? { col: "", display: "" };
59606
- const lowerWord = ans.naive < 0 ? "lower" : "higher";
59607
- const population = getOpt(spec.population) ?? [];
59608
60346
  const dataStatus = statusR({ status: "success", size: "sm" });
59609
60347
  const barList = (rows) => /* @__PURE__ */ jsxRuntime.jsx(react.Box, { css: bs.root, children: rows.map((r2, i) => /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { css: bs.row, children: [
59610
60348
  /* @__PURE__ */ jsxRuntime.jsx(react.Text, { css: bs.label, textStyle: "mono.sm", color: "fg.default", truncate: true, children: r2.label }),
59611
60349
  /* @__PURE__ */ jsxRuntime.jsx(react.Box, { css: bs.track, children: /* @__PURE__ */ jsxRuntime.jsx(react.Box, { css: bs.fill, width: `${Math.round(r2.frac * 100)}%`, bg: toneToken(r2.tone) }) }),
59612
60350
  /* @__PURE__ */ jsxRuntime.jsx(react.Text, { css: bs.value, children: r2.value })
59613
60351
  ] }, i)) });
59614
- const specStaged = specBind.mode === "staged";
60352
+ const specStaged = configBind.mode === "staged";
59615
60353
  const hasJournal = v3.journal.type === "some";
59616
60354
  const canRun = !readonly;
59617
60355
  const canCommit = !readonly && specStaged && hasJournal;
59618
- const runDisabled = !data4.value || estimate.pending;
59619
- const commitDisabled = !estimate.result || estimate.pending || stale;
59620
- return /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { layerStyle: "frame", overflow: "visible", children: [
60356
+ const runDisabled = !data4.value || experiment.pending;
60357
+ const commitDisabled = experiment.status !== "succeeded" || experiment.pending || stale;
60358
+ const failed = experiment.status === "failed" && experiment.error;
60359
+ const showResult = experiment.result !== null && !experiment.pending && !failed;
60360
+ const tabKeys = [...["answer", "trust", "dose"], ...hasDesign ? ["validate"] : []];
60361
+ return /* @__PURE__ */ jsxRuntime.jsx(GuidanceProvider, { on: guidance, vars: helpVars, children: /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { layerStyle: "frame", overflow: "visible", children: [
59621
60362
  /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { layerStyle: "header.bar", display: "flex", alignItems: "center", gap: "3.5", children: [
59622
- /* @__PURE__ */ jsxRuntime.jsxs(react.Text, { textStyle: "title.card", children: [
59623
- "Does ",
59624
- /* @__PURE__ */ jsxRuntime.jsx(react.Text, { as: "span", color: "brand.fg", fontWeight: "bold", children: vs.treatment }),
59625
- " change ",
59626
- /* @__PURE__ */ jsxRuntime.jsx(react.Text, { as: "span", color: "brand.fg", fontWeight: "bold", children: vs.outcome }),
60363
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "title.card", color: "fg.muted", children: /* @__PURE__ */ jsxRuntime.jsxs(Help, { id: "header", children: [
60364
+ "Does ",
60365
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { as: "span", color: "brand.solid", fontWeight: "bold", children: vs.treatment }),
60366
+ " change ",
60367
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { as: "span", color: "brand.solid", fontWeight: "bold", children: vs.outcome }),
59627
60368
  "?"
59628
- ] }),
60369
+ ] }) }),
59629
60370
  /* @__PURE__ */ jsxRuntime.jsx(react.Box, { flex: "1" }),
59630
60371
  /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { as: "span", css: dataStatus.root, children: [
59631
60372
  /* @__PURE__ */ jsxRuntime.jsx(react.Box, { as: "span", css: dataStatus.indicator }),
59632
- /* @__PURE__ */ jsxRuntime.jsx(react.Box, { as: "span", css: dataStatus.label, children: vs.dataLabel })
60373
+ /* @__PURE__ */ jsxRuntime.jsx(react.Box, { as: "span", css: dataStatus.label, children: stale ? `${nRows} rows` : vs.dataLabel })
59633
60374
  ] }),
60375
+ /* @__PURE__ */ jsxRuntime.jsx(GuidanceToggle, { on: guidance, onToggle: () => setGuidance((g2) => !g2) }),
59634
60376
  canRun && /* @__PURE__ */ jsxRuntime.jsx(ActionButton, { button, variant: "solid", label: "Run", onClick: onRun, disabled: runDisabled, pulse: stale }),
59635
60377
  canCommit && /* @__PURE__ */ jsxRuntime.jsx(ActionButton, { button, variant: "ghost", label: "Commit", onClick: onCommit, disabled: commitDisabled })
59636
60378
  ] }),
59637
60379
  /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "grid", gridTemplateColumns: "304px minmax(0,1fr)", alignItems: "start", children: [
59638
60380
  /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { borderRightWidth: "1px", borderColor: "border.subtle", children: [
59639
- /* @__PURE__ */ jsxRuntime.jsxs(Step, { n: 1, title: "What did you change?", children: [
59640
- /* @__PURE__ */ jsxRuntime.jsx(ColumnPick, { column: vs.treatment, kind: vs.treatmentKind, badge, button, choices: columns.filter((c2) => c2.kind === "boolean" || c2.kind === "integer").map((c2) => c2.name), onPick: (c2) => editSpec({ ...spec, treatment: c2 }) }),
60381
+ /* @__PURE__ */ jsxRuntime.jsxs(Step, { n: 1, title: "What did you change?", help: "step_treatment", children: [
60382
+ /* @__PURE__ */ jsxRuntime.jsx(ColumnPick, { column: vs.treatment, kind: vs.treatmentKind, badge, button, choices: columns.filter((c2) => c2.kind === "boolean" || c2.kind === "integer").map((c2) => c2.name), onPick: (c2) => editConfig({ ...config2, treatment: c2 }) }),
59641
60383
  /* @__PURE__ */ jsxRuntime.jsxs(react.Text, { textStyle: "caption", mt: "1.5", children: [
59642
60384
  "Treated = ",
59643
60385
  /* @__PURE__ */ jsxRuntime.jsx(react.Text, { as: "span", color: "fg.default", fontWeight: "semibold", children: vs.comparison })
59644
60386
  ] })
59645
60387
  ] }),
59646
- /* @__PURE__ */ jsxRuntime.jsx(Step, { n: 2, title: "What did you want it to improve?", children: /* @__PURE__ */ jsxRuntime.jsx(ColumnPick, { column: vs.outcome, kind: vs.outcomeKind, badge, button, choices: columns.filter((c2) => c2.kind === "float" || c2.kind === "integer").map((c2) => c2.name), onPick: (c2) => editSpec({ ...spec, outcome: c2 }) }) }),
59647
- /* @__PURE__ */ jsxRuntime.jsx(Step, { n: 3, title: "What else was different about those batches?", children: /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { layerStyle: "frame.flat", children: [
59648
- vs.confounders.map((c2, i) => /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "grid", gridTemplateColumns: "1fr 78px 16px", gap: "2.5", alignItems: "center", px: "2.5", py: "2.5", borderTopWidth: i ? "1px" : "0", borderColor: "border.subtle", children: [
60388
+ /* @__PURE__ */ jsxRuntime.jsx(Step, { n: 2, title: "What did you want it to improve?", help: "step_outcome", children: /* @__PURE__ */ jsxRuntime.jsx(ColumnPick, { column: vs.outcome, kind: vs.outcomeKind, badge, button, choices: columns.filter((c2) => c2.kind === "float" || c2.kind === "integer").map((c2) => c2.name), onPick: (c2) => editConfig({ ...config2, outcome: c2 }) }) }),
60389
+ /* @__PURE__ */ jsxRuntime.jsx(Step, { n: 3, title: "What else was different?", help: "step_confounders", children: /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { layerStyle: "frame.flat", children: [
60390
+ /* @__PURE__ */ jsxRuntime.jsx(react.Box, { maxH: "216px", overflowY: "auto", children: vs.confounders.map((c2, i) => /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "grid", gridTemplateColumns: "1fr 78px 16px", gap: "2.5", alignItems: "center", px: "2.5", py: "2.5", borderTopWidth: i ? "1px" : "0", borderColor: "border.subtle", children: [
59649
60391
  /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { children: [
59650
60392
  /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "mono.sm", fontWeight: "semibold", color: "fg.default", children: c2.col }),
59651
60393
  /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "caption", lineHeight: "1.35", mt: "px", children: c2.reason })
59652
60394
  ] }),
59653
60395
  /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { children: [
59654
60396
  /* @__PURE__ */ jsxRuntime.jsx(react.Box, { css: bs.track, children: /* @__PURE__ */ jsxRuntime.jsx(react.Box, { css: bs.fill, width: `${Math.round(c2.imbalance * 100)}%`, bg: toneToken(c2.tone) }) }),
59655
- /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "caption.eyebrow", fontSize: "9px", textAlign: "center", mt: "1.5", children: c2.level })
60397
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "caption.eyebrow", fontSize: "9px", textAlign: "center", mt: "1.5", children: /* @__PURE__ */ jsxRuntime.jsx(Help, { id: "confounder_imbalance", children: c2.level }) })
59656
60398
  ] }),
59657
- /* @__PURE__ */ jsxRuntime.jsx(react.Box, { as: "button", css: button({ variant: "ghost", size: "xs" }), px: "0", minW: "16px", display: "inline-flex", alignItems: "center", justifyContent: "center", "aria-label": `Remove ${c2.col}`, onClick: () => editSpec({ ...spec, confounders: spec.confounders.filter((x2) => x2 !== c2.col), categorical: spec.categorical.filter((x2) => x2 !== c2.col) }), children: /* @__PURE__ */ jsxRuntime.jsx(FontAwesomeIcon, { icon: faXmark, style: { fontSize: "11px" } }) })
59658
- ] }, i)),
59659
- vs.suggestion && /* @__PURE__ */ jsxRuntime.jsx(ColumnMenu, { choices: columns.filter((c2) => !(/* @__PURE__ */ new Set([spec.treatment, spec.outcome, ...spec.confounders])).has(c2.name)).map((c2) => c2.name), onPick: (c2) => editSpec({ ...spec, confounders: [...spec.confounders, c2] }), children: /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { as: "button", css: button({ variant: "ghost", size: "sm" }), justifyContent: "flex-start", width: "100%", color: "brand.fg", fontFamily: "mono", display: "inline-flex", alignItems: "center", gap: "2", children: [
60399
+ /* @__PURE__ */ jsxRuntime.jsx(react.Box, { as: "button", css: button({ variant: "ghost", size: "xs" }), px: "0", minW: "16px", display: "inline-flex", alignItems: "center", justifyContent: "center", "aria-label": `Remove ${c2.col}`, onClick: () => editConfig({ ...config2, common_causes: config2.common_causes.filter((x2) => x2 !== c2.col), categorical: config2.categorical.type === "some" ? east.some(config2.categorical.value.filter((x2) => x2 !== c2.col)) : east.none }), children: /* @__PURE__ */ jsxRuntime.jsx(FontAwesomeIcon, { icon: faXmark, style: { fontSize: "11px" } }) })
60400
+ ] }, i)) }),
60401
+ vs.suggestion && /* @__PURE__ */ jsxRuntime.jsx(ColumnMenu, { choices: columns.filter((c2) => !(/* @__PURE__ */ new Set([config2.treatment, config2.outcome, ...config2.common_causes])).has(c2.name)).map((c2) => c2.name), onPick: (c2) => editConfig({ ...config2, common_causes: [...config2.common_causes, c2] }), children: /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { as: "button", css: button({ variant: "ghost", size: "sm" }), justifyContent: "flex-start", width: "100%", color: "brand.fg", fontFamily: "mono", display: "inline-flex", alignItems: "center", gap: "2", children: [
59660
60402
  /* @__PURE__ */ jsxRuntime.jsx(FontAwesomeIcon, { icon: faPlus, style: { fontSize: "9px" } }),
59661
60403
  "add another"
59662
60404
  ] }) })
59663
60405
  ] }) }),
59664
- /* @__PURE__ */ jsxRuntime.jsx(Step, { n: 4, title: "Which batches?", children: /* @__PURE__ */ jsxRuntime.jsx(FilterRail, { fields, population, onChange: (next2) => editSpec({ ...spec, population: next2.length ? east.some(next2) : east.none }), chip, button }) }),
60406
+ /* @__PURE__ */ jsxRuntime.jsx(Step, { n: 4, title: `Which ${SUBJECT_MANY}?`, help: "step_population", children: /* @__PURE__ */ jsxRuntime.jsx(FilterRail, { fields, population, onChange: editPopulation, chip, button }) }),
59665
60407
  /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { as: "details", borderTopWidth: "1px", borderColor: "border.subtle", children: [
59666
60408
  /* @__PURE__ */ jsxRuntime.jsxs(
59667
60409
  react.Box,
@@ -59694,173 +60436,168 @@ const EastChakraExperiment = React.memo(function EastChakraExperiment2({ value }
59694
60436
  }
59695
60437
  ),
59696
60438
  /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { px: "4.5", pb: "3.5", pt: "0.5", children: [
59697
- /* @__PURE__ */ jsxRuntime.jsx(Segmented, { label: "How to compare", left: "regression", right: "reweighting", active: vs.method === "reweighting" ? "right" : "left", onPick: (s2) => editSpec({ ...spec, method: east.some(s2 === "right" ? east.variant("propensity_score_weighting", { weighting_scheme: east.none }) : east.variant("linear_regression", null)) }) }),
59698
- /* @__PURE__ */ jsxRuntime.jsx(Segmented, { label: "Answer for", left: "all batches", right: "only treated", active: vs.target === "treated" ? "right" : "left", onPick: (s2) => editSpec({ ...spec, targetUnits: east.some(east.variant(s2 === "right" ? "att" : "ate", null)) }) }),
59699
- /* @__PURE__ */ jsxRuntime.jsx(Segmented, { label: "Drop un-matchable", left: "on", right: "off", active: vs.trim ? "left" : "right", onPick: (s2) => editSpec({ ...spec, trim: s2 === "left" ? east.some(east.variant("overlap", null)) : east.none }), last: true })
60439
+ /* @__PURE__ */ jsxRuntime.jsx(Segmented, { label: "How to compare", help: "adv_method", left: "regression", right: "reweighting", active: vs.method === "reweighting" ? "right" : "left", onPick: (s2) => editConfig({ ...config2, method: east.some(s2 === "right" ? east.variant("propensity_score_weighting", { weighting_scheme: east.none }) : east.variant("linear_regression", null)) }) }),
60440
+ /* @__PURE__ */ jsxRuntime.jsx(Segmented, { label: "Answer for", help: "adv_estimand", left: "all", right: "only treated", active: vs.target === "treated" ? "right" : "left", onPick: (s2) => editConfig({ ...config2, estimand: east.some(east.variant(s2 === "right" ? "att" : "ate", null)) }), last: true })
59700
60441
  ] })
59701
60442
  ] })
59702
60443
  ] }),
59703
60444
  /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { minW: "0", children: [
59704
- /* @__PURE__ */ jsxRuntime.jsx(react.Box, { display: "flex", px: "4", borderBottomWidth: "1px", borderColor: "border.subtle", children: ["answer", "trust", "dose"].map((tk) => {
59705
- const on3 = tab === tk;
59706
- return /* @__PURE__ */ jsxRuntime.jsx(
59707
- react.Box,
59708
- {
59709
- as: "button",
59710
- onClick: () => setTab(tk),
59711
- cursor: "pointer",
59712
- fontSize: "xs",
59713
- fontWeight: "semibold",
59714
- px: "3.5",
59715
- py: "3",
59716
- mb: "-1px",
59717
- color: on3 ? "brand.fg" : "fg.muted",
59718
- borderBottomWidth: "2px",
59719
- borderColor: on3 ? "brand.solid" : "transparent",
59720
- _hover: { color: on3 ? "brand.fg" : "fg.default" },
59721
- children: tk === "answer" ? "Answer" : tk === "trust" ? "Can we trust it?" : "How much?"
60445
+ /* @__PURE__ */ jsxRuntime.jsxs(
60446
+ react.Box,
60447
+ {
60448
+ display: "flex",
60449
+ alignItems: "center",
60450
+ px: "4",
60451
+ borderBottomWidth: "1px",
60452
+ borderColor: "border.subtle",
60453
+ role: "tablist",
60454
+ "aria-label": "Result views",
60455
+ onKeyDown: (e3) => {
60456
+ var _a3;
60457
+ const i = tabKeys.indexOf(tab);
60458
+ let j2 = i;
60459
+ if (e3.key === "ArrowRight") j2 = (i + 1) % tabKeys.length;
60460
+ else if (e3.key === "ArrowLeft") j2 = (i - 1 + tabKeys.length) % tabKeys.length;
60461
+ else if (e3.key === "Home") j2 = 0;
60462
+ else if (e3.key === "End") j2 = tabKeys.length - 1;
60463
+ else return;
60464
+ e3.preventDefault();
60465
+ setTab(tabKeys[j2]);
60466
+ const btns = e3.currentTarget.querySelectorAll('[role="tab"]');
60467
+ (_a3 = btns[j2]) == null ? void 0 : _a3.focus();
59722
60468
  },
59723
- tk
59724
- );
59725
- }) }),
59726
- tab === "answer" && /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { p: "4.5", children: [
59727
- /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "flex", alignItems: "center", gap: "4.5", flexWrap: "wrap", children: [
59728
- /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "flex", flexDirection: "column", gap: "0.5", children: [
59729
- /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "mono.sm", color: "fg.muted", children: ans.outcome }),
59730
- /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "flex", alignItems: "baseline", gap: "2.5", children: [
59731
- /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "mono-kpi", fontFamily: "heading", fontSize: "32px", color: clear ? "fg.success" : "fg.warning", children: signed(ans.effect) }),
59732
- /* @__PURE__ */ jsxRuntime.jsxs(react.Text, { textStyle: "mono.sm", color: "fg.muted", children: [
59733
- "95% CI  ",
59734
- signed(ans.lo),
59735
- " ",
59736
- signed(ans.hi)
59737
- ] })
60469
+ children: [
60470
+ tabKeys.map((tk) => {
60471
+ const on3 = tab === tk;
60472
+ const tabHelp = tk === "answer" ? "tab_answer" : tk === "trust" ? "tab_trust" : tk === "dose" ? "tab_dose" : "tab_validate";
60473
+ const tabLabel = tk === "answer" ? "Answer" : tk === "trust" ? "Trust" : tk === "dose" ? "Dose" : "Validate";
60474
+ return /* @__PURE__ */ jsxRuntime.jsx(
60475
+ react.Box,
60476
+ {
60477
+ as: "button",
60478
+ role: "tab",
60479
+ "aria-selected": on3,
60480
+ tabIndex: on3 ? 0 : -1,
60481
+ onClick: () => setTab(tk),
60482
+ cursor: "pointer",
60483
+ fontSize: "xs",
60484
+ fontWeight: "semibold",
60485
+ px: "3.5",
60486
+ py: "3",
60487
+ mb: "-1px",
60488
+ color: on3 ? "brand.fg" : "fg.muted",
60489
+ borderBottomWidth: "2px",
60490
+ borderColor: on3 ? "brand.solid" : "transparent",
60491
+ _hover: { color: on3 ? "brand.fg" : "fg.default" },
60492
+ _focusVisible: { outline: "2px solid", outlineColor: "brand.solid", outlineOffset: "-2px" },
60493
+ children: /* @__PURE__ */ jsxRuntime.jsx(Help, { id: tabHelp, children: tabLabel })
60494
+ },
60495
+ tk
60496
+ );
60497
+ }),
60498
+ experiment.pending && /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { ml: "auto", display: "inline-flex", alignItems: "center", gap: "2", color: "fg.muted", pr: "1", children: [
60499
+ /* @__PURE__ */ jsxRuntime.jsx(react.Spinner, { size: "xs", borderWidth: "1.5px", color: "brand.solid" }),
60500
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "caption.eyebrow", children: "Running…" })
59738
60501
  ] })
59739
- ] }),
59740
- /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { as: "span", css: badge({ variant: clear ? "ok" : "warn", size: "md" }), alignSelf: "flex-end", mb: "1", display: "inline-flex", alignItems: "center", gap: "1", children: [
59741
- clear && /* @__PURE__ */ jsxRuntime.jsx(FontAwesomeIcon, { icon: dirUp ? faArrowUp : faArrowDown, style: { fontSize: "10px" } }),
59742
- clear ? `${statusWord} with ${ans.treatment}` : "No clear effect"
59743
- ] })
59744
- ] }),
59745
- flip && /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { layerStyle: "banner.stale", display: "flex", alignItems: "flex-start", gap: "2", mt: "3", children: [
59746
- /* @__PURE__ */ jsxRuntime.jsx(react.Box, { as: "span", color: "fg.warning", flexShrink: "0", mt: "0.5", fontSize: "12px", children: /* @__PURE__ */ jsxRuntime.jsx(FontAwesomeIcon, { icon: faTriangleExclamation }) }),
60502
+ ]
60503
+ }
60504
+ ),
60505
+ failed ? /* @__PURE__ */ jsxRuntime.jsx(react.Box, { px: "4.5", pt: "4.5", children: /* @__PURE__ */ jsxRuntime.jsx(RunError, { error: experiment.error }) }) : !showResult ? /* @__PURE__ */ jsxRuntime.jsx(DeckSkeleton, { tab }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
60506
+ stale && /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { layerStyle: "banner.stale", display: "flex", alignItems: "center", gap: "2", mx: "4.5", mt: "4.5", children: [
60507
+ /* @__PURE__ */ jsxRuntime.jsx(react.Box, { as: "span", color: "fg.warning", flexShrink: "0", fontSize: "12px", children: /* @__PURE__ */ jsxRuntime.jsx(FontAwesomeIcon, { icon: faTriangleExclamation }) }),
59747
60508
  /* @__PURE__ */ jsxRuntime.jsxs(react.Text, { textStyle: "body.sm", color: "fg.default", children: [
59748
- /* @__PURE__ */ jsxRuntime.jsx(react.Text, { as: "span", fontWeight: "bold", children: "Raw and like-for-like disagree." }),
59749
- " In the plain average, ",
59750
- /* @__PURE__ */ jsxRuntime.jsx(react.Text, { as: "span", fontFamily: "mono", children: ans.treatment }),
59751
- " batches sit ",
59752
- /* @__PURE__ */ jsxRuntime.jsx(react.Text, { as: "span", fontStyle: "italic", children: lowerWord }),
59753
- " on ",
59754
- /* @__PURE__ */ jsxRuntime.jsx(react.Text, { as: "span", fontFamily: "mono", children: ans.outcome }),
59755
- " (",
59756
- signed(ans.naive),
59757
- ") — but they also differ most on ",
59758
- /* @__PURE__ */ jsxRuntime.jsx(react.Text, { as: "span", fontFamily: "mono", children: top.col }),
59759
- " (",
59760
- top.display,
59761
- "). Adjusting for it reverses the result."
60509
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { as: "span", fontWeight: "bold", children: "Showing the previous setup." }),
60510
+ " Hit Run to update these results for your edits."
59762
60511
  ] })
59763
60512
  ] }),
59764
- /* @__PURE__ */ jsxRuntime.jsxs(Card, { children: [
59765
- /* @__PURE__ */ jsxRuntime.jsx(Cap, { children: "Raw average vs. like-for-like" }),
59766
- /* @__PURE__ */ jsxRuntime.jsx(
59767
- ForestPlot,
59768
- {
59769
- rows: [
59770
- { label: "Raw average", note: "unadjusted", est: ans.naive, lo: ans.naiveLo, hi: ans.naiveHi, tone: ans.naive < 0 ? "neg" : "pos" },
59771
- { label: "Like-for-like", note: "adjusted", est: ans.effect, lo: ans.lo, hi: ans.hi, tone: clear ? "pos" : "warn" }
59772
- ],
59773
- min: Math.floor(Math.min(ans.naiveLo, ans.lo) - 2),
59774
- max: Math.ceil(Math.max(ans.naiveHi, ans.hi) + 2),
59775
- unit: `change in ${ans.outcome}${ans.unit ? ` (${ans.unit})` : ""}`,
59776
- height: 150
59777
- }
59778
- )
59779
- ] }),
59780
- /* @__PURE__ */ jsxRuntime.jsxs(Card, { children: [
59781
- /* @__PURE__ */ jsxRuntime.jsx(Cap, { children: "How unbalanced each one was — before adjusting" }),
59782
- /* @__PURE__ */ jsxRuntime.jsx(react.Box, { py: "0.5", children: barList(ans.balance.map((b2) => ({ label: b2.col, frac: b2.frac, tone: b2.tone, value: b2.display }))) })
59783
- ] }),
59784
- /* @__PURE__ */ jsxRuntime.jsx(react.Box, { display: "flex", gap: "4", mt: "3.5", children: [[ans.nTotal, "batches"], [ans.nCompared, "compared like-for-like"], [ans.nDropped, "had no fair match"]].map(([n2, label], i) => /* @__PURE__ */ jsxRuntime.jsxs(react.Text, { textStyle: "mono.sm", color: "fg.muted", children: [
59785
- /* @__PURE__ */ jsxRuntime.jsx(react.Text, { as: "span", color: "fg.default", fontWeight: "semibold", children: Number(n2) }),
59786
- " ",
59787
- label
59788
- ] }, i)) })
59789
- ] }),
59790
- tab === "trust" && vr && /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { p: "4.5", children: [
59791
- /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "body.sm", color: "fg.muted", mb: "3.5", children: "Before trusting the answer we tried to break it — colour shows pass / caution." }),
59792
- /* @__PURE__ */ jsxRuntime.jsx(react.Box, { layerStyle: "frame.flat", children: vr.checks.map((c2, i) => {
59793
- const cs = statusR({ status: c2.passed ? "success" : "warning", size: "sm" });
59794
- return /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "grid", gridTemplateColumns: "auto 1fr minmax(96px,auto)", gap: "2.5", alignItems: "start", px: "3.5", py: "2.5", borderTopWidth: i ? "1px" : "0", borderColor: "border.subtle", children: [
59795
- /* @__PURE__ */ jsxRuntime.jsx(react.Box, { as: "span", css: cs.root, mt: "1", children: /* @__PURE__ */ jsxRuntime.jsx(react.Box, { as: "span", css: cs.indicator }) }),
59796
- /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { children: [
59797
- /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "body.sm", fontWeight: "semibold", color: "fg.default", children: c2.name }),
59798
- /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "caption", lineHeight: "1.45", mt: "0.5", children: c2.desc })
59799
- ] }),
59800
- /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "inline-flex", alignItems: "center", justifyContent: "flex-end", gap: "1.5", whiteSpace: "nowrap", color: c2.passed ? "fg.success" : "fg.warning", children: [
59801
- /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "mono.sm", fontWeight: "bold", fontVariantNumeric: "tabular-nums", children: c2.value }),
59802
- /* @__PURE__ */ jsxRuntime.jsx(FontAwesomeIcon, { icon: c2.passed ? faCheck : faTriangleExclamation, style: { fontSize: "11px" } })
59803
- ] })
59804
- ] }, i);
59805
- }) }),
59806
- /* @__PURE__ */ jsxRuntime.jsxs(Card, { children: [
59807
- /* @__PURE__ */ jsxRuntime.jsx(Cap, { children: "Effect as a hidden cause is made stronger" }),
59808
- /* @__PURE__ */ jsxRuntime.jsx(AreaRange, { lo: vr.sensLo, mid: vr.sensMid, hi: vr.sensHi, zero: 0, tone: "brand", xTicks: vr.sensXTicks, yTicks: vr.sensYTicks, height: 132 })
59809
- ] })
59810
- ] }),
59811
- tab === "dose" && vd && /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { p: "4.5", children: [
59812
- vd.segments.length > 1 && /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "flex", alignItems: "center", gap: "2.5", mb: "3.5", children: [
59813
- /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "caption.eyebrow", children: "Response for" }),
59814
- /* @__PURE__ */ jsxRuntime.jsx(SegmentSelect, { options: vd.segments, active: 0, onPick: () => {
59815
- } })
60513
+ tab === "answer" && a2 && /* @__PURE__ */ jsxRuntime.jsx(AnswerNumeric, { a: a2, verdict, higherBetter, badge, barList }),
60514
+ tab === "answer" && !a2 && ref && /* @__PURE__ */ jsxRuntime.jsx(RefusalZone, { refusal: ref, overlap: ov, naiveValue: ((_d2 = experiment.result) == null ? void 0 : _d2.naive) ?? 0, outcome: vs.outcome }),
60515
+ tab === "trust" && vr && /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { p: "4.5", children: [
60516
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "body.sm", color: "fg.muted", mb: "3.5", children: /* @__PURE__ */ jsxRuntime.jsx(Help, { id: "trust_intro", children: "Before trusting the answer we tried to break it — colour shows pass / caution." }) }),
60517
+ /* @__PURE__ */ jsxRuntime.jsx(react.Box, { layerStyle: "frame.flat", children: vr.checks.map((c2, i) => {
60518
+ const cs = statusR({ status: c2.passed ? "success" : "warning", size: "sm" });
60519
+ return /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "grid", gridTemplateColumns: "auto 1fr minmax(96px,auto)", gap: "2.5", alignItems: "start", px: "3.5", py: "2.5", borderTopWidth: i ? "1px" : "0", borderColor: "border.subtle", children: [
60520
+ /* @__PURE__ */ jsxRuntime.jsx(react.Box, { as: "span", css: cs.root, mt: "1", children: /* @__PURE__ */ jsxRuntime.jsx(react.Box, { as: "span", css: cs.indicator }) }),
60521
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { children: [
60522
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "body.sm", fontWeight: "semibold", color: "fg.default", children: /* @__PURE__ */ jsxRuntime.jsx(Help, { id: c2.help, children: c2.name }) }),
60523
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "caption", lineHeight: "1.45", mt: "0.5", children: c2.desc })
60524
+ ] }),
60525
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "inline-flex", alignItems: "center", justifyContent: "flex-end", gap: "1.5", whiteSpace: "nowrap", color: c2.passed ? "fg.success" : "fg.warning", children: [
60526
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "mono.sm", fontWeight: "bold", fontVariantNumeric: "tabular-nums", children: c2.value }),
60527
+ /* @__PURE__ */ jsxRuntime.jsx(FontAwesomeIcon, { icon: c2.passed ? faCheck : faTriangleExclamation, style: { fontSize: "11px" } })
60528
+ ] })
60529
+ ] }, i);
60530
+ }) }),
60531
+ vr.sens && /* @__PURE__ */ jsxRuntime.jsxs(Card, { children: [
60532
+ /* @__PURE__ */ jsxRuntime.jsx(Cap, { help: "sensitivity", children: "Effect as a hidden cause is made stronger" }),
60533
+ /* @__PURE__ */ jsxRuntime.jsx(AreaRange, { lo: vr.sens.lo, mid: vr.sens.mid, hi: vr.sens.hi, zero: 0, tone: "brand", xTicks: vr.sens.xTicks, yTicks: vr.sens.yTicks, height: 132 })
60534
+ ] })
59816
60535
  ] }),
59817
- /* @__PURE__ */ jsxRuntime.jsxs(Card, { mt: "0", children: [
59818
- /* @__PURE__ */ jsxRuntime.jsxs(Cap, { children: [
59819
- vd.outcome,
59820
- " gained vs. ",
59821
- vd.feature
60536
+ tab === "trust" && !vr && /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { p: "4.5", children: [
60537
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "inline-flex", alignItems: "center", gap: "2", mb: "2", color: "fg.muted", children: [
60538
+ /* @__PURE__ */ jsxRuntime.jsx(FontAwesomeIcon, { icon: faTriangleExclamation, style: { fontSize: "13px" } }),
60539
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "body.sm", fontWeight: "semibold", color: "fg.default", children: "Nothing to stress-test" })
59822
60540
  ] }),
59823
- /* @__PURE__ */ jsxRuntime.jsx(AreaRange, { lo: vd.lo, mid: vd.mid, hi: vd.hi, tone: "pos", xTicks: vd.xTicks, yTicks: vd.yTicks, marks: vd.marks.map((m2) => ({ at: Number(m2.at), label: m2.label, tone: m2.tone })), height: 256 })
60541
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Text, { textStyle: "body.sm", color: "fg.muted", lineHeight: "1.5", children: [
60542
+ "There’s no adjusted estimate to try to break — the experiment couldn’t produce one (see the ",
60543
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { as: "span", fontWeight: "semibold", color: "fg.default", children: "Answer" }),
60544
+ " tab for why)."
60545
+ ] })
59824
60546
  ] }),
59825
- /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "grid", gridTemplateColumns: "1fr 1fr", gap: "3", mt: "3", children: [
59826
- /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { layerStyle: "card", p: "3.5", borderRadius: "lg", children: [
59827
- /* @__PURE__ */ jsxRuntime.jsx(Cap, { children: "Recommended" }),
59828
- /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "flex", flexDirection: "column", gap: "0.5", children: [
59829
- /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "caption.eyebrow", children: vd.recoLabel }),
59830
- /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "flex", alignItems: "baseline", gap: "2", children: [
59831
- /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "mono-kpi", fontFamily: "heading", color: "brand.solid", children: signed(vd.recoEffect) }),
59832
- /* @__PURE__ */ jsxRuntime.jsxs(react.Text, { textStyle: "mono.sm", color: "fg.muted", children: [
59833
- vd.outcome,
59834
- " · ",
59835
- signed(vd.recoLo),
59836
- " … ",
59837
- signed(vd.recoHi)
59838
- ] })
59839
- ] })
60547
+ tab === "dose" && vd && /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { p: "4.5", children: [
60548
+ /* @__PURE__ */ jsxRuntime.jsxs(Card, { mt: "0", children: [
60549
+ /* @__PURE__ */ jsxRuntime.jsxs(Cap, { help: "dose_curve", children: [
60550
+ vd.outcome,
60551
+ " gained vs. ",
60552
+ vd.feature
59840
60553
  ] }),
59841
- /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "caption", mt: "2.5", pt: "2.5", borderTopWidth: "1px", borderColor: "border.subtle", children: vd.tradeoff })
60554
+ /* @__PURE__ */ jsxRuntime.jsx(AreaRange, { lo: vd.lo, mid: vd.mid, hi: vd.hi, tone: "pos", xTicks: vd.xTicks, yTicks: vd.yTicks, marks: vd.marks.map((m2) => ({ at: m2.at, label: m2.label, tone: m2.tone, help: m2.help })), height: 256 })
59842
60555
  ] }),
59843
- /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { layerStyle: "card", p: "3.5", borderRadius: "lg", children: [
59844
- /* @__PURE__ */ jsxRuntime.jsxs(Cap, { children: [
59845
- "Extra ",
59846
- vd.outcome,
59847
- " per step"
60556
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "grid", gridTemplateColumns: "1fr 1fr", gap: "3", mt: "3", children: [
60557
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { layerStyle: "card", p: "3.5", borderRadius: "lg", children: [
60558
+ /* @__PURE__ */ jsxRuntime.jsx(Cap, { help: "dose_reco", children: "Recommended" }),
60559
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "flex", flexDirection: "column", gap: "0.5", children: [
60560
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "caption.eyebrow", children: vd.recoLabel }),
60561
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "flex", alignItems: "baseline", gap: "2", children: [
60562
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "mono-kpi", fontFamily: "heading", color: "brand.solid", children: signed(vd.recoEffect) }),
60563
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Text, { textStyle: "mono.sm", color: "fg.muted", children: [
60564
+ vd.outcome,
60565
+ " · ",
60566
+ signed(vd.recoLo),
60567
+ " … ",
60568
+ signed(vd.recoHi)
60569
+ ] })
60570
+ ] })
60571
+ ] }),
60572
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "caption", mt: "2.5", pt: "2.5", borderTopWidth: "1px", borderColor: "border.subtle", children: vd.tradeoff })
59848
60573
  ] }),
59849
- /* @__PURE__ */ jsxRuntime.jsx(react.Box, { py: "0.5", children: barList(vd.marginal.map((m2, i) => ({ label: m2.label, frac: m2.frac, tone: i < 2 ? "brand" : "muted", value: signed(m2.value) }))) })
60574
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { layerStyle: "card", p: "3.5", borderRadius: "lg", children: [
60575
+ /* @__PURE__ */ jsxRuntime.jsxs(Cap, { help: "dose_marginal", children: [
60576
+ "Extra ",
60577
+ vd.outcome,
60578
+ " per step"
60579
+ ] }),
60580
+ /* @__PURE__ */ jsxRuntime.jsx(react.Box, { py: "0.5", maxH: "200px", overflowY: "auto", children: barList(vd.marginal.map((m2, i) => ({ label: m2.label, frac: m2.frac, tone: i < 2 ? "brand" : "muted", value: signed(m2.value) }))) })
60581
+ ] })
59850
60582
  ] })
59851
- ] })
60583
+ ] }),
60584
+ tab === "validate" && (design.status === "failed" && design.error ? /* @__PURE__ */ jsxRuntime.jsx(react.Box, { px: "4.5", pt: "4.5", children: /* @__PURE__ */ jsxRuntime.jsx(RunError, { error: design.error }) }) : designFresh && vmDesign ? /* @__PURE__ */ jsxRuntime.jsx(ValidatePanel, { vm: vmDesign, barList }) : /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { p: "4.5", display: "inline-flex", alignItems: "center", gap: "2", color: "fg.muted", children: [
60585
+ /* @__PURE__ */ jsxRuntime.jsx(react.Spinner, { size: "xs", borderWidth: "1.5px", color: "brand.solid" }),
60586
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "body.sm", children: "Sizing the trial that would confirm this…" })
60587
+ ] }))
59852
60588
  ] })
59853
60589
  ] })
59854
60590
  ] }),
59855
60591
  journal && journal.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
59856
60592
  /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { css: es.root, bg: "bg.canvas", borderTopWidth: "1px", borderColor: "border.subtle", children: [
59857
- /* @__PURE__ */ jsxRuntime.jsx(react.Box, { css: es.lbl, children: "Committed experiments" }),
60593
+ /* @__PURE__ */ jsxRuntime.jsx(react.Box, { css: es.lbl, children: /* @__PURE__ */ jsxRuntime.jsx(Help, { id: "journal", children: "Committed experiments" }) }),
59858
60594
  /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { css: es.meta, children: [
59859
60595
  /* @__PURE__ */ jsxRuntime.jsx(react.Text, { as: "span", color: "fg.default", fontWeight: "semibold", children: journal.length }),
59860
- " on record"
60596
+ " on record",
60597
+ journal.length > 50 ? " · showing newest 50" : ""
59861
60598
  ] })
59862
60599
  ] }),
59863
- journal.map((r2, i) => /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "grid", gridTemplateColumns: "2fr 1fr 1fr 1fr", gap: "3", alignItems: "center", px: "4.5", py: "2.5", borderTopWidth: "1px", borderColor: "border.subtle", children: [
60600
+ journal.slice(0, 50).map((r2, i) => /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "grid", gridTemplateColumns: "2fr 1fr 1fr 1fr", gap: "3", alignItems: "center", px: "4.5", py: "2.5", borderTopWidth: "1px", borderColor: "border.subtle", children: [
59864
60601
  /* @__PURE__ */ jsxRuntime.jsxs(react.Text, { textStyle: "body.sm", children: [
59865
60602
  /* @__PURE__ */ jsxRuntime.jsxs(react.Text, { as: "span", fontWeight: "bold", children: [
59866
60603
  r2.treatment,
@@ -59874,7 +60611,7 @@ const EastChakraExperiment = React.memo(function EastChakraExperiment2({ value }
59874
60611
  ] })
59875
60612
  ] }),
59876
60613
  /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "mono.sm", fontWeight: "semibold", textAlign: "right", fontVariantNumeric: "tabular-nums", color: r2.verdictTone === "pos" ? "fg.success" : "fg.default", children: r2.effect }),
59877
- /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "caption.eyebrow", textAlign: "right", color: r2.verdictTone === "pos" ? "fg.success" : "fg.warning", children: r2.verdict }),
60614
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "caption.eyebrow", textAlign: "right", color: toneToken(r2.verdictTone), children: r2.verdict }),
59878
60615
  /* @__PURE__ */ jsxRuntime.jsxs(react.Text, { textStyle: "mono.sm", textAlign: "right", color: "fg.muted", children: [
59879
60616
  r2.who,
59880
60617
  " · ",
@@ -59882,8 +60619,180 @@ const EastChakraExperiment = React.memo(function EastChakraExperiment2({ value }
59882
60619
  ] })
59883
60620
  ] }, i))
59884
60621
  ] })
60622
+ ] }) });
60623
+ }, (prev2, next2) => experimentValueEqual(prev2.value, next2.value) && prev2.storageKey === next2.storageKey);
60624
+ function AnswerNumeric({ a: a2, verdict, higherBetter, badge, barList }) {
60625
+ const dirUp = a2.effect > 0;
60626
+ const statusWord = higherBetter === void 0 ? dirUp ? "Higher" : "Lower" : dirUp === higherBetter ? "Better" : "Worse";
60627
+ const lowerWord = a2.naive < 0 ? "lower" : "higher";
60628
+ const top = a2.balance[0] ?? { col: "", display: "" };
60629
+ const kpiColor = a2.clear && !a2.cautious ? "fg.success" : "fg.warning";
60630
+ const badgeOk = a2.clear && !a2.cautious;
60631
+ const badgeText = a2.cautious ? (verdict == null ? void 0 : verdict.label) ?? "Not trustworthy yet" : a2.clear ? `${statusWord} with ${a2.treatment}` : "No clear effect";
60632
+ return /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { p: "4.5", children: [
60633
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "flex", alignItems: "center", gap: "4.5", flexWrap: "wrap", children: [
60634
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "flex", flexDirection: "column", gap: "0.5", children: [
60635
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "mono.sm", color: "fg.muted", children: a2.outcome }),
60636
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "flex", alignItems: "baseline", gap: "2.5", children: [
60637
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "mono-kpi", fontFamily: "heading", fontSize: "32px", color: kpiColor, children: /* @__PURE__ */ jsxRuntime.jsx(Help, { id: "answer_effect", children: signed(a2.effect) }) }),
60638
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "mono.sm", color: "fg.muted", children: /* @__PURE__ */ jsxRuntime.jsxs(Help, { id: "answer_ci", children: [
60639
+ "95% CI  ",
60640
+ signed(a2.lo),
60641
+ " … ",
60642
+ signed(a2.hi)
60643
+ ] }) })
60644
+ ] })
60645
+ ] }),
60646
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { as: "span", css: badge({ variant: badgeOk ? "ok" : "warn", size: "md" }), alignSelf: "flex-end", mb: "1", display: "inline-flex", alignItems: "center", gap: "1", children: [
60647
+ a2.clear && !a2.cautious && /* @__PURE__ */ jsxRuntime.jsx(FontAwesomeIcon, { icon: dirUp ? faArrowUp : faArrowDown, style: { fontSize: "10px" } }),
60648
+ /* @__PURE__ */ jsxRuntime.jsx(Help, { id: `verdict_${(verdict == null ? void 0 : verdict.tag) ?? "modest"}`, children: badgeText })
60649
+ ] })
60650
+ ] }),
60651
+ a2.cautious && /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { layerStyle: "banner.stale", display: "flex", alignItems: "flex-start", gap: "2", mt: "3", children: [
60652
+ /* @__PURE__ */ jsxRuntime.jsx(react.Box, { as: "span", color: "fg.warning", flexShrink: "0", mt: "0.5", fontSize: "12px", children: /* @__PURE__ */ jsxRuntime.jsx(FontAwesomeIcon, { icon: faTriangleExclamation }) }),
60653
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Text, { textStyle: "body.sm", color: "fg.default", children: [
60654
+ /* @__PURE__ */ jsxRuntime.jsx(Help, { id: "answer_cautious", children: /* @__PURE__ */ jsxRuntime.jsx(react.Text, { as: "span", fontWeight: "bold", children: "Treat this as provisional." }) }),
60655
+ " We adjusted and got a number, but a robustness check failed — the estimate may still be driven by something we didn’t adjust for. See ",
60656
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { as: "span", fontWeight: "semibold", children: "Can we trust it?" })
60657
+ ] })
60658
+ ] }),
60659
+ a2.flip && /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { layerStyle: "banner.stale", display: "flex", alignItems: "flex-start", gap: "2", mt: "3", children: [
60660
+ /* @__PURE__ */ jsxRuntime.jsx(react.Box, { as: "span", color: "fg.warning", flexShrink: "0", mt: "0.5", fontSize: "12px", children: /* @__PURE__ */ jsxRuntime.jsx(FontAwesomeIcon, { icon: faTriangleExclamation }) }),
60661
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Text, { textStyle: "body.sm", color: "fg.default", children: [
60662
+ /* @__PURE__ */ jsxRuntime.jsx(Help, { id: "answer_flip", children: /* @__PURE__ */ jsxRuntime.jsx(react.Text, { as: "span", fontWeight: "bold", children: "Raw and like-for-like disagree." }) }),
60663
+ " In the plain average, the ",
60664
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { as: "span", fontFamily: "mono", children: a2.treatment }),
60665
+ " group sits ",
60666
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { as: "span", fontStyle: "italic", children: lowerWord }),
60667
+ " on ",
60668
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { as: "span", fontFamily: "mono", children: a2.outcome }),
60669
+ " (",
60670
+ signed(a2.naive),
60671
+ ") — but they also differ most on ",
60672
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { as: "span", fontFamily: "mono", children: top.col }),
60673
+ " (",
60674
+ top.display,
60675
+ "). Adjusting for it reverses the result."
60676
+ ] })
60677
+ ] }),
60678
+ /* @__PURE__ */ jsxRuntime.jsxs(Card, { children: [
60679
+ /* @__PURE__ */ jsxRuntime.jsx(Cap, { help: "forest_plot", children: "Raw average vs. like-for-like" }),
60680
+ /* @__PURE__ */ jsxRuntime.jsx(
60681
+ ForestPlot,
60682
+ {
60683
+ rows: [
60684
+ { label: "Raw average", note: "unadjusted", est: a2.naive, lo: a2.naiveLo, hi: a2.naiveHi, tone: a2.naive < 0 ? "neg" : "pos" },
60685
+ { label: "Like-for-like", note: "adjusted", est: a2.effect, lo: a2.lo, hi: a2.hi, tone: a2.clear && !a2.cautious ? "pos" : "warn" }
60686
+ ],
60687
+ min: Math.floor(Math.min(0, a2.naiveLo, a2.lo) - 2),
60688
+ max: Math.ceil(Math.max(0, a2.naiveHi, a2.hi) + 2),
60689
+ unit: `change in ${a2.outcome}${a2.unit ? ` (${a2.unit})` : ""}`,
60690
+ height: 150,
60691
+ rowHelp: ["forest_naive", "forest_adjusted"]
60692
+ }
60693
+ )
60694
+ ] }),
60695
+ /* @__PURE__ */ jsxRuntime.jsxs(Card, { children: [
60696
+ /* @__PURE__ */ jsxRuntime.jsx(Cap, { help: "balance", children: "How unbalanced each one was — before adjusting" }),
60697
+ /* @__PURE__ */ jsxRuntime.jsx(react.Box, { py: "0.5", maxH: "208px", overflowY: "auto", children: barList(a2.balance.map((b2) => ({ label: b2.col, frac: b2.frac, tone: b2.tone, value: b2.display }))) })
60698
+ ] }),
60699
+ /* @__PURE__ */ jsxRuntime.jsx(react.Box, { mt: "3.5", children: /* @__PURE__ */ jsxRuntime.jsx(Help, { id: "counts", display: "inline-flex", gap: "4", children: [[a2.nTotal, SUBJECT_MANY], [a2.nCompared, "compared like-for-like"], [a2.nDropped, "had no fair match"]].map(([n2, label], i) => /* @__PURE__ */ jsxRuntime.jsxs(react.Text, { textStyle: "mono.sm", color: "fg.muted", children: [
60700
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { as: "span", color: "fg.default", fontWeight: "semibold", children: Number(n2) }),
60701
+ " ",
60702
+ label
60703
+ ] }, i)) }) })
59885
60704
  ] });
59886
- });
60705
+ }
60706
+ function RefusalZone({ refusal, overlap, naiveValue, outcome }) {
60707
+ return /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { p: "4.5", children: [
60708
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "inline-flex", alignItems: "center", gap: "2", mb: "2", color: "fg.warning", children: [
60709
+ /* @__PURE__ */ jsxRuntime.jsx(FontAwesomeIcon, { icon: faTriangleExclamation, style: { fontSize: "14px" } }),
60710
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "title.card", color: "fg.default", children: /* @__PURE__ */ jsxRuntime.jsx(Help, { id: refusal.kind === "positivity" ? "refusal_positivity" : "refusal_not_estimable", children: refusal.title }) })
60711
+ ] }),
60712
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "body.sm", color: "fg.muted", mb: "3", lineHeight: "1.5", children: refusal.body }),
60713
+ refusal.kind === "positivity" && overlap && /* @__PURE__ */ jsxRuntime.jsxs(Card, { mt: "1", children: [
60714
+ /* @__PURE__ */ jsxRuntime.jsxs(Cap, { help: "overlap_histogram", children: [
60715
+ "Propensity overlap — ",
60716
+ overlap.supportLabel
60717
+ ] }),
60718
+ /* @__PURE__ */ jsxRuntime.jsx(OverlapHistogram, { treated: overlap.treated, control: overlap.control, supportLabel: overlap.supportLabel, positivityOk: overlap.positivityOk, height: 170 })
60719
+ ] }),
60720
+ /* @__PURE__ */ jsxRuntime.jsx(react.Box, { display: "flex", gap: "4", mt: "3.5", children: refusal.evidence.map((e3, i) => /* @__PURE__ */ jsxRuntime.jsxs(react.Text, { textStyle: "mono.sm", color: "fg.muted", children: [
60721
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { as: "span", color: "fg.default", fontWeight: "semibold", children: e3.value }),
60722
+ " ",
60723
+ e3.label
60724
+ ] }, i)) }),
60725
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Text, { textStyle: "caption", color: "fg.subtle", mt: "3.5", pt: "3", borderTopWidth: "1px", borderColor: "border.subtle", children: [
60726
+ "Raw average difference in ",
60727
+ outcome,
60728
+ " (context only, not an answer): ",
60729
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { as: "span", fontFamily: "mono", color: "fg.muted", children: signed(naiveValue) })
60730
+ ] })
60731
+ ] });
60732
+ }
60733
+ function ValidatePanel({ vm, barList }) {
60734
+ const treatedPct = Math.round(vm.primary.treatedShare * 100);
60735
+ return /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { p: "4.5", children: [
60736
+ /* @__PURE__ */ jsxRuntime.jsx(Cap, { help: "tab_validate", children: vm.headline }),
60737
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "flex", alignItems: "flex-end", gap: "5", flexWrap: "wrap", children: [
60738
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "flex", flexDirection: "column", gap: "0.5", children: [
60739
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "mono.sm", color: "fg.muted", children: /* @__PURE__ */ jsxRuntime.jsx(Help, { id: "validate_size", children: vm.holdback ? "to hold back from" : "to run" }) }),
60740
+ vm.faint ? /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "body.sm", fontWeight: "semibold", color: "fg.warning", maxW: "220px", children: "Effect too faint to size — set a materiality threshold to size a trial." }) : /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "mono-kpi", fontFamily: "heading", fontSize: "32px", color: "brand.solid", children: vm.primary.nTotal.toLocaleString() })
60741
+ ] }),
60742
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { flex: "1", minW: "220px", children: [
60743
+ /* @__PURE__ */ jsxRuntime.jsx(Cap, { help: "validate_split", children: vm.holdback ? "Hold-back split" : "Split" }),
60744
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "flex", h: "10px", borderRadius: "full", overflow: "hidden", borderWidth: "1px", borderColor: "border.subtle", children: [
60745
+ /* @__PURE__ */ jsxRuntime.jsx(react.Box, { bg: "brand.solid", width: `${treatedPct}%` }),
60746
+ /* @__PURE__ */ jsxRuntime.jsx(react.Box, { bg: "bg.emphasized", flex: "1" })
60747
+ ] }),
60748
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "flex", justifyContent: "space-between", mt: "1.5", children: [
60749
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Text, { textStyle: "caption", children: [
60750
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { as: "span", color: "brand.fg", fontWeight: "semibold", children: vm.primary.nTreated.toLocaleString() }),
60751
+ " ",
60752
+ vm.holdback ? "treated" : "get it"
60753
+ ] }),
60754
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Text, { textStyle: "caption", children: [
60755
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { as: "span", color: "fg.default", fontWeight: "semibold", children: vm.primary.nControl.toLocaleString() }),
60756
+ " ",
60757
+ vm.holdback ? "held back" : "left alone"
60758
+ ] })
60759
+ ] })
60760
+ ] })
60761
+ ] }),
60762
+ vm.matchOn.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(Card, { children: [
60763
+ /* @__PURE__ */ jsxRuntime.jsx(Cap, { help: "validate_match", children: "Match both groups on" }),
60764
+ /* @__PURE__ */ jsxRuntime.jsx(react.Box, { py: "0.5", children: barList(vm.matchOn.map((m2) => ({ label: m2.display, frac: m2.frac, tone: m2.tone, value: "" }))) })
60765
+ ] }),
60766
+ vm.curve.mid.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(Card, { children: [
60767
+ /* @__PURE__ */ jsxRuntime.jsx(Cap, { help: "validate_power", children: "Chance of detecting it" }),
60768
+ /* @__PURE__ */ jsxRuntime.jsx(
60769
+ AreaRange,
60770
+ {
60771
+ lo: vm.curve.mid,
60772
+ mid: vm.curve.mid,
60773
+ hi: vm.curve.mid,
60774
+ tone: "brand",
60775
+ xTicks: vm.curve.xTicks,
60776
+ yTicks: ["100", "50", "0"],
60777
+ yFormat: "percent",
60778
+ marks: vm.curve.marks.map((m2) => ({ at: m2.at, label: m2.label, tone: m2.tone, help: m2.help })),
60779
+ height: 170
60780
+ }
60781
+ )
60782
+ ] }),
60783
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "body.sm", color: "fg.default", lineHeight: "1.5", mt: "3.5", children: vm.rationale }),
60784
+ vm.alternates.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(react.Box, { display: "flex", flexDirection: "column", gap: "1.5", mt: "3", pt: "3", borderTopWidth: "1px", borderColor: "border.subtle", children: vm.alternates.map((o2, i) => /* @__PURE__ */ jsxRuntime.jsxs(react.Text, { textStyle: "caption", color: "fg.muted", children: [
60785
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { as: "span", fontWeight: "semibold", color: "fg.default", children: o2.label }),
60786
+ " · ",
60787
+ o2.nTotal.toLocaleString(),
60788
+ " total (",
60789
+ o2.nTreated.toLocaleString(),
60790
+ " / ",
60791
+ o2.nControl.toLocaleString(),
60792
+ ")"
60793
+ ] }, i)) })
60794
+ ] });
60795
+ }
59887
60796
  function FilterRail({ fields, population, onChange: onChange2, chip, button }) {
59888
60797
  const [open, setOpen] = React.useState(null);
59889
60798
  const replaceAt = (i, p2) => onChange2(population.map((f2, j2) => j2 === i ? p2 : f2));
@@ -59934,11 +60843,11 @@ function FilterRail({ fields, population, onChange: onChange2, chip, button }) {
59934
60843
  )
59935
60844
  ] });
59936
60845
  }
59937
- function Step({ n: n2, title, children: children2 }) {
60846
+ function Step({ n: n2, title, help, children: children2 }) {
59938
60847
  return /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { px: "4.5", py: "3", borderBottomWidth: "1px", borderColor: "border.subtle", children: [
59939
- /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "flex", alignItems: "baseline", gap: "2", mb: "2", children: [
59940
- /* @__PURE__ */ jsxRuntime.jsx(react.Box, { as: "span", fontFamily: "mono", fontSize: "9px", fontWeight: "bold", color: "fg.inverse", bg: "brand.solid", w: "16px", h: "16px", borderRadius: "full", display: "inline-flex", alignItems: "center", justifyContent: "center", flex: "0 0 auto", children: n2 }),
59941
- /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "title.row", children: title })
60848
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "flex", alignItems: "baseline", gap: "2.5", mb: "2", children: [
60849
+ /* @__PURE__ */ jsxRuntime.jsx(react.Box, { as: "span", fontFamily: "mono", fontSize: "11px", fontWeight: "bold", color: "brand.fg", flex: "0 0 auto", lineHeight: "1", children: n2 }),
60850
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "title.row", children: help ? /* @__PURE__ */ jsxRuntime.jsx(Help, { id: help, children: title }) : title })
59942
60851
  ] }),
59943
60852
  children2
59944
60853
  ] });
@@ -59956,13 +60865,45 @@ function ColumnPick({ column: column2, kind, choices, onPick, badge, button }) {
59956
60865
  /* @__PURE__ */ jsxRuntime.jsx(react.Box, { as: "span", display: "inline-flex", color: "fg.muted", fontSize: "10px", children: /* @__PURE__ */ jsxRuntime.jsx(FontAwesomeIcon, { icon: faChevronDown }) })
59957
60866
  ] }) });
59958
60867
  }
59959
- function SegmentSelect({ options: options2, active, onPick, fill, size: size3 = "sm" }) {
60868
+ function SegmentSelect({ options: options2, active, onPick, fill, size: size3 = "xs" }) {
59960
60869
  const s2 = react.useSlotRecipe({ key: "segmentGroup" })({ size: size3 });
59961
- return /* @__PURE__ */ jsxRuntime.jsx(react.Box, { css: s2.root, ...fill ? { width: "100%" } : {}, children: options2.map((o2, i) => /* @__PURE__ */ jsxRuntime.jsx(react.Box, { as: "button", css: s2.item, ...fill ? { flex: "1" } : {}, ...active === i ? { "data-state": "checked" } : {}, onClick: () => onPick(i), children: /* @__PURE__ */ jsxRuntime.jsx(react.Box, { as: "span", css: s2.itemText, children: o2 }) }, i)) });
60870
+ return /* @__PURE__ */ jsxRuntime.jsx(
60871
+ react.Box,
60872
+ {
60873
+ css: s2.root,
60874
+ ...fill ? { width: "100%" } : {},
60875
+ role: "radiogroup",
60876
+ onKeyDown: (e3) => {
60877
+ var _a2;
60878
+ const d2 = e3.key === "ArrowRight" || e3.key === "ArrowDown" ? 1 : e3.key === "ArrowLeft" || e3.key === "ArrowUp" ? -1 : 0;
60879
+ if (!d2) return;
60880
+ e3.preventDefault();
60881
+ const next2 = (active + d2 + options2.length) % options2.length;
60882
+ onPick(next2);
60883
+ const btns = e3.currentTarget.querySelectorAll('[role="radio"]');
60884
+ (_a2 = btns[next2]) == null ? void 0 : _a2.focus();
60885
+ },
60886
+ children: options2.map((o2, i) => /* @__PURE__ */ jsxRuntime.jsx(
60887
+ react.Box,
60888
+ {
60889
+ as: "button",
60890
+ role: "radio",
60891
+ "aria-checked": active === i,
60892
+ tabIndex: active === i ? 0 : -1,
60893
+ css: s2.item,
60894
+ ...fill ? { flex: "1" } : {},
60895
+ ...active === i ? { "data-state": "checked" } : {},
60896
+ onClick: () => onPick(i),
60897
+ children: /* @__PURE__ */ jsxRuntime.jsx(react.Box, { as: "span", css: s2.itemText, children: o2 })
60898
+ },
60899
+ i
60900
+ ))
60901
+ }
60902
+ );
59962
60903
  }
59963
- function Segmented({ label, left, right, active, onPick, last: last2 }) {
60904
+ function Segmented({ label, left, right, active, onPick, last: last2, help }) {
59964
60905
  return /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { display: "flex", flexDirection: "column", alignItems: "stretch", gap: "1.5", py: "2", borderBottomWidth: last2 ? "0" : "1px", borderColor: "border.subtle", children: [
59965
- /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "caption.eyebrow", textTransform: "none", letterSpacing: "normal", color: "fg.default", children: label }),
60906
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { textStyle: "caption.eyebrow", textTransform: "none", letterSpacing: "normal", color: "fg.default", children: help ? /* @__PURE__ */ jsxRuntime.jsx(Help, { id: help, children: label }) : label }),
59966
60907
  /* @__PURE__ */ jsxRuntime.jsx(SegmentSelect, { options: [left, right], active: active === "left" ? 0 : 1, onPick: (i) => onPick(i === 0 ? "left" : "right"), fill: true, size: "xs" })
59967
60908
  ] });
59968
60909
  }
@@ -61664,10 +62605,10 @@ function formatPrimitive(type, value) {
61664
62605
  case "Integer":
61665
62606
  return String(value);
61666
62607
  case "Float": {
61667
- const num = value;
61668
- if (Number.isNaN(num)) return "NaN";
61669
- if (!Number.isFinite(num)) return num > 0 ? "Infinity" : "-Infinity";
61670
- return String(num);
62608
+ const num2 = value;
62609
+ if (Number.isNaN(num2)) return "NaN";
62610
+ if (!Number.isFinite(num2)) return num2 > 0 ? "Infinity" : "-Infinity";
62611
+ return String(num2);
61671
62612
  }
61672
62613
  case "String":
61673
62614
  return `"${value}"`;