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
This commit is contained in:
pj committed 2026-08-13 21:45:05 +05:30
1 parent f307354bc7
commit 4c5932b4be
1 file changed
+14 -9
+14 -9
View File
@@ -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;
});