mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
fix(folio-web): enumerate authored targets and values, declare the llm generator
The two edge-case typing leaves become the typing builtin at their combined weight: that text is deliberately not domain-specific, so naming the field and leaving the text to the policy is the designed path, and it keeps the seeded arm on the corpus while the model writes its own. Total weight is unchanged at 165, so every surviving branch keeps its share and submitTxn stays at 9.70 percent. Claude-Session: https://claude.ai/code/session_01A5KmftdEJ49A9z5mF5ESrX
This commit is contained in:
1 parent
4c5932b4be
commit
b6dabaeeca
1 file changed
+51
-46
@@ -3,14 +3,13 @@ import {
|
|||||||
Tap,
|
Tap,
|
||||||
actions,
|
actions,
|
||||||
always,
|
always,
|
||||||
edgeCaseText,
|
|
||||||
eventually,
|
eventually,
|
||||||
extract,
|
extract,
|
||||||
from,
|
llm,
|
||||||
integers,
|
|
||||||
next,
|
next,
|
||||||
now,
|
now,
|
||||||
taps,
|
taps,
|
||||||
|
typing,
|
||||||
waitOnce,
|
waitOnce,
|
||||||
weighted,
|
weighted,
|
||||||
} from "@sanderling/spec";
|
} from "@sanderling/spec";
|
||||||
@@ -178,8 +177,10 @@ const openAddAccount = actions(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Readable enumeration keeps the demo legible; repeats over a run still
|
// Readable enumeration keeps the demo legible; repeats over a run still
|
||||||
// exercise duplicate-name handling.
|
// exercise duplicate-name handling. One action per name rather than one action
|
||||||
const accountNames = from([
|
// for a drawn name: a draw reads the seeded picker's stream, which the model
|
||||||
|
// policy never enters, so the two would explore different action spaces.
|
||||||
|
const accountNames = [
|
||||||
"Checking",
|
"Checking",
|
||||||
"Savings",
|
"Savings",
|
||||||
"Travel",
|
"Travel",
|
||||||
@@ -188,18 +189,14 @@ const accountNames = from([
|
|||||||
"Investments",
|
"Investments",
|
||||||
"Groceries",
|
"Groceries",
|
||||||
"Petty Cash",
|
"Petty Cash",
|
||||||
]);
|
];
|
||||||
|
|
||||||
function typeAccountNameWith(sampler: { generate(): string }) {
|
const typeAccountName = actions(() => {
|
||||||
return actions(() => {
|
if (!onAddAccountPage.current) return [];
|
||||||
if (!onAddAccountPage.current) return [];
|
const field = accountNameField.current;
|
||||||
const field = accountNameField.current;
|
if (!field) return [];
|
||||||
return field ? [InputText({ into: field, text: sampler.generate() })] : [];
|
return accountNames.map((name) => InputText({ into: field, text: name }));
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const typeAccountName = typeAccountNameWith(accountNames);
|
|
||||||
const typeAccountNameEdge = typeAccountNameWith(edgeCaseText());
|
|
||||||
|
|
||||||
const submitAddAccount = actions(() => {
|
const submitAddAccount = actions(() => {
|
||||||
if (!onAddAccountPage.current) return [];
|
if (!onAddAccountPage.current) return [];
|
||||||
@@ -209,9 +206,7 @@ const submitAddAccount = actions(() => {
|
|||||||
|
|
||||||
const openAccount = actions(() => {
|
const openAccount = actions(() => {
|
||||||
if (!onHomePage.current) return [];
|
if (!onHomePage.current) return [];
|
||||||
const cards = accountCards.current;
|
return accountCards.current.map((card) => Tap({ on: card.element }));
|
||||||
if (cards.length === 0) return [];
|
|
||||||
return [Tap({ on: from(cards).generate().element })];
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const openAddTxn = actions(() => {
|
const openAddTxn = actions(() => {
|
||||||
@@ -220,41 +215,37 @@ const openAddTxn = actions(() => {
|
|||||||
return btn ? [Tap({ on: btn })] : [];
|
return btn ? [Tap({ on: btn })] : [];
|
||||||
});
|
});
|
||||||
|
|
||||||
// Valid happy-path amounts keep the balance properties exercised; the edge
|
// Valid happy-path amounts keep the balance properties exercised; the `typing`
|
||||||
// branch (weighted in actionsRoot) stresses parsing with the adversarial corpus.
|
// branch in actionsRoot stresses parsing with the adversarial corpus. Four
|
||||||
const validAmounts = integers().between(1, 99999);
|
// authored values rather than a range, because a range cannot be enumerated:
|
||||||
|
// the smallest accepted amount, one with cents, an everyday one, and one wide
|
||||||
|
// enough to format with a thousands separator.
|
||||||
|
const validAmounts = ["1", "12.34", "250", "99999"];
|
||||||
|
|
||||||
function typeAmountWith(sampler: { generate(): string }) {
|
const typeAmount = actions(() => {
|
||||||
return actions(() => {
|
if (!onAddTxnPage.current) return [];
|
||||||
if (!onAddTxnPage.current) return [];
|
const field = txnAmountField.current;
|
||||||
const field = txnAmountField.current;
|
if (!field) return [];
|
||||||
return field ? [InputText({ into: field, text: sampler.generate() })] : [];
|
return validAmounts.map((amount) => InputText({ into: field, text: amount }));
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const typeAmount = typeAmountWith({ generate: () => String(validAmounts.generate()) });
|
const notes = ["Coffee", "Paycheck", "Gas", "Refund", "", "Groceries for the week"];
|
||||||
const typeAmountEdge = typeAmountWith(edgeCaseText());
|
|
||||||
|
|
||||||
const noteSampler = from([
|
|
||||||
"Coffee",
|
|
||||||
"Paycheck",
|
|
||||||
"Gas",
|
|
||||||
"Refund",
|
|
||||||
"",
|
|
||||||
"Groceries for the week",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const typeNote = actions(() => {
|
const typeNote = actions(() => {
|
||||||
if (!onAddTxnPage.current) return [];
|
if (!onAddTxnPage.current) return [];
|
||||||
const field = txnNoteField.current;
|
const field = txnNoteField.current;
|
||||||
return field ? [InputText({ into: field, text: noteSampler.generate() })] : [];
|
if (!field) return [];
|
||||||
|
return notes.map((note) => InputText({ into: field, text: note }));
|
||||||
});
|
});
|
||||||
|
|
||||||
const toggleTxnType = actions(() => {
|
const toggleTxnType = actions(() => {
|
||||||
if (!onAddTxnPage.current) return [];
|
if (!onAddTxnPage.current) return [];
|
||||||
const targets = [txnCreditButton.current, txnDebitButton.current].filter(Boolean);
|
const credit = txnCreditButton.current;
|
||||||
if (targets.length === 0) return [];
|
const debit = txnDebitButton.current;
|
||||||
return [Tap({ on: from(targets).generate() })];
|
const options = [];
|
||||||
|
if (credit) options.push(Tap({ on: credit }));
|
||||||
|
if (debit) options.push(Tap({ on: debit }));
|
||||||
|
return options;
|
||||||
});
|
});
|
||||||
|
|
||||||
const submitTxn = actions(() => {
|
const submitTxn = actions(() => {
|
||||||
@@ -274,22 +265,36 @@ const logoutAction = actions(() => {
|
|||||||
return btn ? [Tap({ on: btn })] : [];
|
return btn ? [Tap({ on: btn })] : [];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// `typing` carries the adversarial input the two authored edge-case leaves used
|
||||||
|
// to: it names the field and leaves the value to whichever policy is driving,
|
||||||
|
// so the seeded picker draws from the edge-case corpus and the model writes its
|
||||||
|
// own text. It holds the 8 weight those two leaves shared, so every other
|
||||||
|
// branch keeps the share it had.
|
||||||
export const actionsRoot = weighted(
|
export const actionsRoot = weighted(
|
||||||
[30, loginHelper],
|
[30, loginHelper],
|
||||||
[2, adversarialLogin],
|
[2, adversarialLogin],
|
||||||
[14, openAddAccount],
|
[14, openAddAccount],
|
||||||
[14, typeAccountName],
|
[14, typeAccountName],
|
||||||
[4, typeAccountNameEdge],
|
|
||||||
[14, submitAddAccount],
|
[14, submitAddAccount],
|
||||||
[14, openAccount],
|
[14, openAccount],
|
||||||
[12, openAddTxn],
|
[12, openAddTxn],
|
||||||
[14, typeAmount],
|
[14, typeAmount],
|
||||||
[4, typeAmountEdge],
|
|
||||||
[8, typeNote],
|
[8, typeNote],
|
||||||
[6, toggleTxnType],
|
[6, toggleTxnType],
|
||||||
[16, submitTxn],
|
[16, submitTxn],
|
||||||
[6, goBack],
|
[6, goBack],
|
||||||
[1, logoutAction],
|
[1, logoutAction],
|
||||||
[4, taps],
|
[4, taps],
|
||||||
|
[8, typing],
|
||||||
[2, waitOnce],
|
[2, waitOnce],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// The LLM generator is orthogonal to actionsRoot: with `--generator llm` a model
|
||||||
|
// picks from the SAME weighted candidate set above, reading the screenshot and a
|
||||||
|
// numbered, weight-annotated list; the default `--generator seeded` ignores it.
|
||||||
|
// instructions describe only WHAT the app is, never HOW to test it.
|
||||||
|
export const generator = llm({
|
||||||
|
model: "gpt-5.4-nano",
|
||||||
|
instructions:
|
||||||
|
"Folio is a personal-finance ledger app in the browser. Sign in with the demo credentials shown on the login screen. The home screen lists accounts, each with a balance. You can create accounts, open an account to see its ledger, and add credit or debit transactions; each transaction has an amount and changes that account's balance and the overall total.",
|
||||||
|
});
|
||||||
Reference in new issue
Block a user