From 4c5932b4be3fd72f20cee9fc2b4c7c5a2560876a Mon Sep 17 00:00:00 2001 From: PJ Date: Thu, 13 Aug 2026 21:45:05 +0530 Subject: [PATCH] fix(folio): enumerate authored targets and values instead of sampling Sampling inside an authored leaf is refused under the model policy now, because the draw collapses to its first item there. Each sampled leaf offers one action per value instead. Lists are short, three rather than five, because the two form leaves also carry their submit and the seeded picker splits a leaf's probability across the actions it returns. The doubleTaps path that reaches the planted defect is unchanged at 5.88 percent, since no root or defaults weight moved. Claude-Session: https://claude.ai/code/session_01A5KmftdEJ49A9z5mF5ESrX --- examples/folio/sanderling/spec.ts | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/examples/folio/sanderling/spec.ts b/examples/folio/sanderling/spec.ts index 051c97f..58ff05b 100644 --- a/examples/folio/sanderling/spec.ts +++ b/examples/folio/sanderling/spec.ts @@ -4,8 +4,6 @@ import { actions, always, extract, - from, - integers, llm, next, weighted, @@ -133,7 +131,13 @@ const login = actions(() => { return submit ? [Tap({ on: submit })] : []; }); -const accountNames = from(["Checking", "Savings", "Travel", "Emergency Fund", "Investments"]); +// One action per value, never a draw: a draw reads the seeded picker's stream, +// which the model policy never enters, so the two would explore different +// action spaces. Both lists stay short because the leaf offers its values +// alongside the submit that ends the form, so each extra value is one less +// chance per step of submitting. defaultActions types the corpus into the same +// fields, so these are the realistic values, not the exhaustive ones. +const accountNames = ["Checking", "Savings", "Emergency Fund"]; const addAccount = whenRoute(route, ["home", "add-account"], () => { if (route.current === "home") { @@ -143,18 +147,19 @@ const addAccount = whenRoute(route, ["home", "add-account"], () => { const field = accountNameField.current; const submit = addAccountSubmit.current; const opts = []; - if (field) opts.push(InputText({ into: field, text: accountNames.generate() })); + if (field) opts.push(...accountNames.map(name => InputText({ into: field, text: name }))); if (submit) opts.push(Tap({ on: submit })); return opts; }); -const amounts = integers().between(1, 500); +// Amounts stay parseable: submitMovesBalanceByTypedAmount only fires on a +// transaction the app actually accepted, so a value the amount field rejects +// tests nothing. One with cents keeps the sub-dollar arithmetic covered. +const amounts = ["1", "12.34", "500"]; const addTxn = whenRoute(route, ["home", "ledger", "add-transaction"], () => { if (route.current === "home") { - const cards = accountCards.current; - if (cards.length === 0) return []; - return [Tap({ on: from(cards).generate() })]; + return accountCards.current.map(card => Tap({ on: card })); } if (route.current === "ledger") { const btn = addTxnButton.current; @@ -163,7 +168,7 @@ const addTxn = whenRoute(route, ["home", "ledger", "add-transaction"], () => { const field = txnAmountField.current; const submit = txnSubmit.current; const opts = []; - if (field) opts.push(InputText({ into: field, text: String(amounts.generate()) })); + if (field) opts.push(...amounts.map(amount => InputText({ into: field, text: amount }))); if (submit) opts.push(Tap({ on: submit })); return opts; });