answer --seed, and runs/spread.sh: a score comes with its spread

Every score so far was one run of a small visitor. `answer --seed n`
answers the same tasks with other shuffles and sampling, and
runs/spread.sh prints a run's score per seed, so a change can be told
from the visitor's mood.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Bendik Aagaard Lynghaug
2026-09-23 22:46:25 +02:00
co-authored by Claude Fable 5.1
parent ec2f07da77
commit cc50249d7f
3 changed files with 29 additions and 6 deletions
Executable
+19
View File
@@ -0,0 +1,19 @@
#!/bin/sh
# The noise floor: answer one run's tasks with several seeds and print
# each seed's score, so a change can be told from the visitor's mood.
# runs/spread.sh runs/<run> [seeds...] (default seeds: 1 2 3)
set -eu
run=${1:?usage: spread.sh runs/<run> [seeds...]}; shift
seeds=${*:-1 2 3}
IRIS=/var/local/cargo-target/release/iris
T=/var/local/cargo-target/release/portal-trainer
LLM="MODEL=qwen3:8b $(dirname "$0")/../engines/ollama.sh"
cd "$run"
for mode in full skim; do
skim=""; [ "$mode" = skim ] && skim="--skim"
for s in $seeds; do
$T answer --tasks tasks-$mode.jsonl --repo site --llm "$LLM" --seed "$s" > answers-$mode-seed$s.jsonl
printf "%s seed %s: " "$mode" "$s"
$IRIS check --path site/questions --needs-tasks tasks-$mode.jsonl $skim --needs-score answers-$mode-seed$s.jsonl 2>&1 | grep -E "^wording:"
done
done
+9 -5
View File
@@ -65,10 +65,14 @@ fn main() -> anyhow::Result<()> {
.and_then(|i| argv.get(i + 1)) .and_then(|i| argv.get(i + 1))
.cloned() .cloned()
}; };
let tasks = value("--tasks").ok_or_else(|| anyhow!("usage: portal-trainer answer --tasks <tasks.jsonl> [--repo <content dir>] [--llm 'cmd']"))?; let tasks = value("--tasks").ok_or_else(|| anyhow!("usage: portal-trainer answer --tasks <tasks.jsonl> [--repo <content dir>] [--llm 'cmd'] [--seed <n>]"))?;
let cmd = value("--llm").unwrap_or_else(|| "claude -p".to_string()); let cmd = value("--llm").unwrap_or_else(|| "claude -p".to_string());
let repo = value("--repo").map(PathBuf::from); let repo = value("--repo").map(PathBuf::from);
print!("{}", answer_blind(&cmd, &std::fs::read_to_string(&tasks).with_context(|| format!("reading {tasks}"))?, repo.as_deref())?); // --seed: the same tasks answered again with other shuffles and
// sampling, so a score comes with its spread. 0 is the run
// every earlier score used.
let seed: u64 = value("--seed").and_then(|s| s.parse().ok()).unwrap_or(0);
print!("{}", answer_blind(&cmd, &std::fs::read_to_string(&tasks).with_context(|| format!("reading {tasks}"))?, repo.as_deref(), seed)?);
return Ok(()); return Ok(());
} }
let opts = parse_args()?; let opts = parse_args()?;
@@ -167,7 +171,7 @@ fn main() -> anyhow::Result<()> {
args.push("--skim".to_string()); args.push("--skim".to_string());
} }
lint(&opts, &args)?; lint(&opts, &args)?;
let answers = answer_blind(opts.visitor.as_deref().unwrap_or(&opts.llm), &std::fs::read_to_string(&tasks_path)?, Some(&opts.out))?; let answers = answer_blind(opts.visitor.as_deref().unwrap_or(&opts.llm), &std::fs::read_to_string(&tasks_path)?, Some(&opts.out), 0)?;
let answers_path = opts.out.join(if skim { "answers-skim.jsonl" } else { "answers.jsonl" }); let answers_path = opts.out.join(if skim { "answers-skim.jsonl" } else { "answers.jsonl" });
std::fs::write(&answers_path, answers)?; std::fs::write(&answers_path, answers)?;
let mut args = vec!["--needs-score".to_string(), answers_path.display().to_string()]; let mut args = vec!["--needs-score".to_string(), answers_path.display().to_string()];
@@ -447,7 +451,7 @@ fn dump(out: &Path) -> anyhow::Result<String> {
/// card portal renders: button, encouragement, sections with icon and /// card portal renders: button, encouragement, sections with icon and
/// guidance, fields with placeholders. Headings-only tasks (`--skim`, /// guidance, fields with placeholders. Headings-only tasks (`--skim`,
/// where the task's text is the heading itself) stay headings only. /// where the task's text is the heading itself) stay headings only.
pub(crate) fn answer_blind(cmd: &str, tasks: &str, repo: Option<&Path>) -> anyhow::Result<String> { pub(crate) fn answer_blind(cmd: &str, tasks: &str, repo: Option<&Path>, seed_offset: u64) -> anyhow::Result<String> {
let mut answers = String::new(); let mut answers = String::new();
for line in tasks.lines().filter(|l| !l.trim().is_empty()) { for line in tasks.lines().filter(|l| !l.trim().is_empty()) {
let task: serde_json::Value = serde_json::from_str(line)?; let task: serde_json::Value = serde_json::from_str(line)?;
@@ -463,7 +467,7 @@ pub(crate) fn answer_blind(cmd: &str, tasks: &str, repo: Option<&Path>) -> anyho
.collect(); .collect();
let skim = options.iter().all(|(name, text)| text.is_empty() || text == name); let skim = options.iter().all(|(name, text)| text.is_empty() || text == name);
let page = if skim { None } else { repo.and_then(|r| rate::load_page(r, task["page"].as_str().unwrap_or("/"))) }; let page = if skim { None } else { repo.and_then(|r| rate::load_page(r, task["page"].as_str().unwrap_or("/"))) };
let seed = fnv(id); let seed = fnv(id).wrapping_add(seed_offset.wrapping_mul(0x9E37_79B9_7F4A_7C15) as _);
shuffle(&mut options, seed); shuffle(&mut options, seed);
// Options are numbered and the answer is the number: with a // Options are numbered and the answer is the number: with a
// whole card in view, a small model asked to copy a heading // whole card in view, a small model asked to copy a heading
+1 -1
View File
@@ -34,7 +34,7 @@ pub fn run(argv: &[String]) -> anyhow::Result<()> {
let mut summary = serde_json::Map::new(); let mut summary = serde_json::Map::new();
for (mode, tasks, skim) in [("full", &tasks_full, false), ("skim", &tasks_skim, true)] { for (mode, tasks, skim) in [("full", &tasks_full, false), ("skim", &tasks_skim, true)] {
eprintln!("score: the visitor answers {mode}"); eprintln!("score: the visitor answers {mode}");
let answers = crate::answer_blind(&cmd, &std::fs::read_to_string(tasks)?, Some(&site))?; let answers = crate::answer_blind(&cmd, &std::fs::read_to_string(tasks)?, Some(&site), 0)?;
let answers_path = out.join(format!("answers-{mode}.jsonl")); let answers_path = out.join(format!("answers-{mode}.jsonl"));
std::fs::write(&answers_path, &answers)?; std::fs::write(&answers_path, &answers)?;
let (tasks_arg, answers_arg) = (s(tasks), s(&answers_path)); let (tasks_arg, answers_arg) = (s(tasks), s(&answers_path));