Commit Graph
6 Commits
Author SHA1 Message Date
montjeffrey 6a7996aa70 fix(gateguard): make harness graders check content and count only valid trials
- csv-export-delimiter's grader adds a fourth check: the data rows are
  the exported values, with an optional header (the hidden intent says a
  header is fine either way). Reference scores 1; both decoys 0.75.
- graderIsSound grades every decoy-* overlay and is sound only when each
  scores below 1; --check-graders and the pre-billing check print the
  decoy scores through formatGraderScores.
- isValidTrial and completedTrialKeys move to lib.js; coverage.js and
  run-intent.js use them from there.
- run.js resumes with completedTrialKeys, so a provider-error or timed-out
  key is retried, and writes one transcript per attempt
  (<key>__<attemptId>.jsonl, recorded on the row) so a retry never
  overwrites the failed attempt's record.
- armSummary computes trials, passes and means over valid rows and counts
  the failed attempts apart as providerErrors.

Docs: question-effectiveness.md states the decoy rule and that a
csv-export-delimiter pass in the reported runs reflects the dialect only;
its Measures section and the harness README describe retries, per-attempt
transcripts and the hookless-trial stop.
2026-10-11 10:00:47 -04:00
montjeffrey 03a68e04a8 fix(gateguard): harden effectiveness evidence 2026-10-05 16:43:43 -04:00
montjeffrey 175a0800d0 fix(gateguard): enforce evaluation validity guards 2026-10-05 01:43:33 -04:00
montjeffrey 2d7a5313c3 fix(gateguard): do not charge the ungated arm with gate-specific holes
silence-hole and coverage-hole are claims about the gate, and an ungated arm
never fires it, so classify labelled every ungated miss a coverage-hole by
construction. In the 48-trial run that padded the hole counts with five rows
that say nothing about the gate: coverage-hole read 7 when only 2 were the
gate's. Label an ungated miss unaided-miss, which is the baseline rate and is
excluded from the holes list.
2026-10-04 22:40:42 -04:00
montjeffrey acf4980442 fix(gateguard): report intervals and an exact test next to the hole counts
The hidden-intent report printed bare counts. run.js reported Wilson intervals,
a bootstrap paired difference and an exact McNemar test, and that machinery was
lost in the new runner, so four trials rendered as a table that reads like a
result. On this run it is not one: gate 1/2 against off 0/2 gives intervals of
9%-91% and 0%-66%, a paired difference of -50% with a 95% interval spanning
-100% to 0%, and McNemar p = 1.000.

Reuse lib.js's pairedComparison and ai-eval-lib's wilson rather than adding a
second statistics path, and state the shortfall in words whenever a comparison
has fewer than twenty pairs: about 20 trials per arm are needed for 80% power on
a 40-point pass-rate difference at a two-sided 5% level, about 36 for 30 points,
and about 160 for the 15-point threshold the pilot pre-registered.

Mean turns and cost sit in the same table, because the only efficiency figure
this harness has produced points the other way: the gate roughly doubles turns
and cost per trial.
2026-10-04 22:40:42 -04:00
montjeffrey 7afde35e49 feat(gateguard): measure question effectiveness against a hidden user intent
The existing harness cannot measure what it set out to measure. Every trap
hides its deciding fact in the repository, which agents reliably discover, and
trials run one-shot under "do not ask clarifying questions", so a fact-forcing
gate is evaluated with no user to answer it. Its scenarios also declare the
question the gate ought to raise, chosen by reading the gate's own taxonomy, so
the suite passes by construction and can never find a missing question.

Add a second runner that measures effectiveness instead of coverage:

- scenarios-intent/ keeps the deciding fact in intent.json, as a choice between
  plausible conventions the repository cannot settle, so an agent that does not
  ask can only guess and the ungated arm cannot sit at the ceiling.
- user-sim.js plays that user with a separate cheap model: it answers the
  question in front of it, never volunteers, and reports which fact ids it
  disclosed, so asking something and asking the deciding thing are told apart.
  An unparseable judge is a recorded failure, not a silent stonewall.
- session.js runs multi-turn sessions joined by --session-id and --resume, and
  tells every arm the user is reachable, so the comparison is symmetric and the
  gate's contribution is whether it makes the agent ask the deciding question.
- coverage.js classifies each trial from behaviour alone into working, lucky, or
  a follow-through, targeting, silence or coverage hole, and reports the
  questions the gate actually raised rather than asserting declared ones.
- run-intent.js refuses minus-target, which needs targets these scenarios
  deliberately lack, and checks every grader separates the start, the trap and
  the reference before a session is billed.

A scenario counts as carrying no information only when nothing varies across
arms. Trap strength is reported separately: the ungated arm failing is what a
working trap looks like, so it must not make a scenario unusable.
2026-10-04 22:40:42 -04:00