Skip to content

Assessment skill v3: length parity, headline cue, pooled three-run gate - #33

Merged
aharlap merged 7 commits into
mainfrom
assessment-writing-v3
Oct 20, 2026
Merged

aharlap merged 7 commits into
mainfrom
assessment-writing-v3

Conversation

@aharlap

@aharlap aharlap commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Skill v2 fixed false-everywhere distractors, but its masked guessing gate fell back to the no-skill level. Writers attached the case's condition to the key, so the key was the longest option on more than half the items. It was also usually the headline textbook answer, which any solver who knows the subject picks.

Pins. Praxity/praxity-studio#1816 and Praxity/praxity-trace#24 are both merged and pinned here. The Studio pin is the last commit (e50ac09). With both pins, a real course with {shuffle} choices gets a pass or fail verdict instead of "tooling" (see End-to-end check below).

What changed

  • Option shape. Options match in shape and length through detail that describes the option itself, never why it suits a case or the case's condition. The key-check judge flags any option that points at a stem detail, including by paraphrase. No option may restate or contradict a stem condition. No option repeats a content word or phrase from the stem unless every option does. All four worked examples follow this, and none of their options ends with a period.
  • Key length. keyLongestCue flags a bank only when the key is the longest option on more items than chance allows (one-sided binomial, 0.05). Ties count the way Trace counts them.
  • Headline cue. Some stems use the neighbouring case, so the headline answer becomes a distractor. That case must test the same objective, and the item has a one-line check for it. Example 4 is a choking diner who can still answer and breathe, keyed "cough hard to bring the food back up".
  • Solver strength. Solvers and judges must be at least as strong as the writer, and the masked solver is named before the first run. The explanations reviewer may be any model.
  • Hand-in checklist. One (x) and {shuffle} per scored choice, a source count of scored choices, each deliberate poll by ref, and no stem echoes.
  • Seeded runs. Three runs with seeds 1, 2 and 3, fixed before solving. Seeds 2 and 3 feed only the gate.
  • Gate. guessingGate(runs, { scoredChoices, polls }). Each run is { seed, bundleId, courseHash, itemCues }.
    • Before the tails it stops when:
      • the seeds aren't exactly 1, 2 and 3, the bundles aren't distinct, or the runs aren't of one course;
      • the masked solver isn't named the same on every row;
      • the course isn't its banks' disjoint union, item for item and hit for hit;
      • item metadata differs across runs;
      • a result is unscored.
    • It pools masked.right with exact tails at 1/k per item per run.
    • A clear statistical fail returns fail before any other stop.
    • After the tails it stops when:
      • the course count differs from scoredChoices;
      • a row is too small to test;
      • exclusion accounting is incomplete or doesn't reconcile;
      • an exclusion code is anything but multiple-select and the declared polls, matched ref for ref;
      • an excluded ref repeats or is also included.
    • It returns "tooling" when the report has no exclusion accounting or any item's shuffle is unknown, unless an exclusion record already shows a fault in the bank. Accounting that is present but malformed is a stop. On "tooling" the writer finishes pass 1, runs one more pass only to check pass 1's rewrites, and tells the person.
  • Key check.
    • The orchestrating step plants a double-key control, not the writer. If the orchestrating agent also wrote the items, a fresh subagent briefed only with the topic writes it. It uses a single-answer stem and a ref in the course's own pattern, recorded only in the orchestrator's notes.
    • The control is exempt from the rewrite rules, and the judge gets at most one rerun.
    • A pre-ship check confirms no control remains in the course.
    • Distractor notes are scored blind before the judge sees the writer's case.
  • Composer. Composer (Add composer skill and selection fixtures #31) now lists each deliberate poll by ref when it hands items to assessment-writing. It treats a "tooling" bank like a non-English one: it lets the skill check pass 1's rewrites, marks the bank unverified, blocks under Draft, finishes the run and asks the person at step 9.
  • Trace pin. pack.json pins Trace 0.1.1-experimental.53ee25cc6 (source 53ee25cc620fcbc669e0f00508962156f21bdce5, fix: consume choice shuffle and count excluded items per bank praxity-trace#24).
    • Trace prerelease, built by this hosted run. Main CI on that commit: run.
    • Rehosted byte for byte on Rubato's asset-only prerelease v0.1.1-tools.experimental.20261019.5.
    • SHA-256 7596c94314fb587bbe414ed46daa5ed460af9aa009f282d6d17568a4def57aa2 matches:
      • the build log;
      • Trace's asset digest;
      • a local download;
      • Rubato's asset digest and an anonymous download from the Rubato URL.
    • All 130 payload files match the inventory, and the source revision is clean. The licence, notices, skill and report reference match the source.
    • The manifest test now refuses 97e6de3, which lacks the exclusion accounting.
  • Studio pin (last commit). pack.json pins Studio 0.3.0-main.e1930acb (source e1930acba2a684bbca976e4acef5607bc92328ae). It is Studio's first admitted CLI candidate containing Praxity/praxity-studio#1816: candidate run 39361630685, where admit, build (with the CLI proof) and publish succeeded.
    • Rehosted byte for byte on Rubato's asset-only prerelease v0.1.1-tools.experimental.20261019.6.
    • darwin-arm64 8acdb90fc11cb326e72e62ec74b36757d3fec510421c32691d9177d1d87b14a6, darwin-x64 01b821fbccc3758e397e5a9ea4f6fe08d9fbddd9b1823002e5b5ed87245f5981. Each matches:
      • the build job's recorded outputs (publish step EXPECTED_*_SHA256);
      • the candidate's GitHub asset digest and .sha256 file;
      • a local download;
      • Rubato's asset digest and an anonymous download from the Rubato URL.
    • Rubato's verifyStudioArchive passes for both: clean exact source revision, single root, no links, payload inventory, legal files, notices, the private-path scan and prax-format/SKILL.md.

End-to-end check

I built a 16-item course with locale: en: 15 single-answer choices with {shuffle} and one without, as a control. I ran a real praxity inspect --schema 1 with the pinned Studio build, then the pinned Trace, then three seeded prepare/report runs with scripted answers, and the shipped guessingGate on those reports.

  • Inspect reports assessment.shuffle as true on the 15 shuffled choices and false on the control. Trace reads every item's shuffle as known, with no exclusions and a full excludedByReason map.
  • Masked answers fixed on option 1 hit 8, 5 and 5 of 16, so guessingGate returns "pass". Answers that pick every key hit 16 of 16 in each run, so it returns "fail". Neither returns "tooling".

Tests

The shipped gate runs on report-shaped three-run data. Every rule has a mutant: the gate with that rule removed must give a different result. The rules covered:

  • run identity;
  • shape across runs;
  • solver name;
  • union and hit sums;
  • mixed option counts (a fixed 0.25 chance fails where 1/k passes);
  • pooling;
  • applies and status;
  • small banks;
  • power order;
  • declared count;
  • polls by ref (a swapped poll and question pass under a count-only check);
  • each unexpected code;
  • empty banks;
  • each accounting reconciliation;
  • tooling versus stop;
  • shuffle in every run;
  • fail before tooling.

keyLongestCue is tested at 8 and 9 of 18, with tie and threshold mutants. Prose rules are tested by mutants. A behavioural check covers every worked example for stem echoes, periods and Example 4's stated length range.

Local: node --test test/skills.test.mjs test/assessment-writing.test.mjs test/manifest.test.mjs (127 pass), npm run validate (valid). CI runs the rest. With the Studio pin, test/local-install.test.mjs and test/update.test.mjs pass 45 of 45 under WSL, as in #29. On Windows, which Rubato doesn't support, three update tests failed after running for about a minute each.

Known

  • Trace still requires plain and cover answers in every answer.json, so seeds 2 and 3 can't run the masked probe alone. Their plain and cover solvers still run, but any model may answer them, nothing reads them and they need no judge. A Trace change accepting not run for those probes would remove that cost.

Review: 2/12/2 0/4/5; fixes 2. Sol and Opus rounds 1 and 2; fixed the gate pooling and length-parity test.

🤖 Generated with Claude Code

aharlap and others added 6 commits October 19, 2026 22:45
The v2 eval fixed false-everywhere distractors but the masked gate
regressed to the no-skill level: writers attached the case's condition
to the key, so it was the longest option on 29 of 53 items, and the key
was usually the headline textbook answer.

- Options share one sentence shape; distractors carry their own case
  clause. Key-length-rank check: key strictly longest on at most n/k
  items, and keyLongest within longestExpected in Trace's spreads.
- Some stems make the neighbouring case the item, so the headline
  answer is a distractor about one time in k. New Example 4.
- The masked solver is named in advance, at least the writer's model.
- Writer hand-in checklist: one (x) and {shuffle} per scored choice,
  and a source count reconciled with Trace's included count.
- guessingGate takes exactly three seeded runs, pools masked.right with
  exact tails, requires the same items and solver in each run, and stops
  on any excludedCount above 0 or a missing excludedCount
  (Praxity/praxity-trace#24).
- Key check plants a double-key positive control and scores distractor
  notes blind before showing the writer's case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Studio shows a missing (x) the same way as a poll, so an exclusion alone
can't tell them apart. The writer now declares scored choices and
deliberate polls, and guessingGate(runs, { scoredChoices, polls }):

- stops when Trace's included course count differs from the declared
  count;
- stops on any excludedByReason code except multiple-select, and
  no-key-or-poll up to the declared polls;
- stops when excludedByReason is missing (Trace before #24);
- leaves a wholly excluded bank (count 0, no-included-items) out of the
  tails while its codes still count;
- stops after the tails when any item's shuffle is unknown, since the
  position check skipped it; a fail still reports as fail.

Each rule has a mutant test. The Trace pin is unchanged until #24 merges.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Option parity comes from each action's or claim's own detail, never
  the case's condition; no option restates or contradicts the stem.
  Example 4 rewritten; Examples 2 and 3 no longer echo stem words; no
  option ends with a period. Tests check every worked example for stem
  echoes and periods, and Example 4's stated length range.
- keyLongestCue flags a bank only when the key is longest, ties as
  Trace counts them, above chance at one-sided 0.05.
- Headline stems must test the same objective; the key-check judge,
  not the writer, counts headline keys.
- The orchestrating step plants the double-key control under the
  reserved ref prefix 0., exempt from rewrites, with one judge rerun at
  most and a pre-ship check that no control remains.
- Seeds 2 and 3 feed only the gate; their plain and cover answers are
  unread and unjudged.
- guessingGate takes { seed, bundleId, courseHash, itemCues } per run:
  seeds 1, 2, 3, distinct bundles, one course; named solver on every
  row; course equals its banks item for item and hit for hit; full
  item metadata matches across runs. A clear statistical fail returns
  before any other stop. Polls match no-key-or-poll exclusions ref for
  ref. Exclusion accounting must be complete and reconcile. Missing
  accounting or unknown shuffle returns "tooling", and the skill says
  not to spend passes 2 and 3 on it.
- Solver strength covers solvers and judges, not the explanations
  reviewer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Trace #24 (53ee25c) adds per-row excludedByReason, maskedExcludedCount,
coded excluded records and choice shuffle, which the assessment gate
reads. Same route as #30: Trace prerelease built in hosted CI, rehosted
byte for byte on Rubato's asset-only prerelease
v0.1.1-tools.experimental.20261019.5.

SHA-256 7596c94314fb587bbe414ed46daa5ed460af9aa009f282d6d17568a4def57aa2
matches the build log, Trace's asset digest, a local download, and
Rubato's asset digest and an anonymous download. All 130 payload files
match the inventory; licence, notices, skill and report reference match
the source. The manifest test now refuses 97e6de3, which lacks the
accounting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- On "tooling", rewrites from pass 1 get one more pass: inspect, the
  three masked runs, and the key check and blind notes on the changed
  items. No rewrite ships unchecked.
- Option detail describes the option itself, never why it suits a case.
  The key-check judge flags any option that points at a stem detail,
  including by paraphrase; the median example is the negative case.
- guessingGate: excluded refs must be distinct and never also included.
  Exclusion records already in the report decide before a tooling
  limit, and accounting that is present but malformed is a stop, not
  "tooling".
- Example 4 key no longer presumes coughing; back blows are "up to
  five". The numeric example's options are all falls of one length.
- The headline count had no reader and is dropped. The control's ref
  follows the course's own pattern and lives in the orchestrator's
  notes; "the orchestrating step" is defined.
- Seeds 2 and 3's plain and cover solvers are exempt in the solver rule
  and the done list names seed 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
assessment-writing v3's gate matches deliberate polls by ref against
Trace's no-key-or-poll exclusions and can return "tooling". Composer
now lists each poll by ref when it hands over the Items section, and
treats a "tooling" bank like a non-English one: let the skill check
pass 1's rewrites, mark the bank unverified, block under Draft,
continue through steps 7.3 to 9 and ask at step 9.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@aharlap
aharlap force-pushed the assessment-writing-v3 branch from fcf198b to 5169147 Compare October 20, 2026 02:47
Studio's first admitted CLI candidate containing Praxity/praxity-studio#1816
(run 39361630685: admit, build with the CLI proof, and publish succeeded),
rehosted byte for byte on Rubato's asset-only prerelease
v0.1.1-tools.experimental.20261019.6.

darwin-arm64 8acdb90fc11cb326e72e62ec74b36757d3fec510421c32691d9177d1d87b14a6
darwin-x64   01b821fbccc3758e397e5a9ea4f6fe08d9fbddd9b1823002e5b5ed87245f5981

Each matches the build job's recorded outputs, the candidate's asset
digest and .sha256 file, a local download, and Rubato's asset digest
and an anonymous download. verifyStudioArchive passes for both. A real
inspect of a course with {shuffle} choices reports assessment.shuffle,
and the assessment gate then returns pass or fail, not "tooling".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@aharlap
aharlap marked this pull request as ready for review October 20, 2026 12:07
@aharlap
aharlap merged commit 30db686 into main Oct 20, 2026
4 checks passed
@aharlap
aharlap deleted the assessment-writing-v3 branch October 20, 2026 12:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant