Spike harness for baseline-example anchoring (T0.5, part 1 of #28) - #40
Open
GustavoSena wants to merge 2 commits into
Open
Spike harness for baseline-example anchoring (T0.5, part 1 of #28)#40GustavoSena wants to merge 2 commits into
GustavoSena wants to merge 2 commits into
Conversation
Acceptance criterion 1 of #28: the script and its fixed request set. The numbers still need a funded Galileo key — this makes producing them one command rather than an afternoon. Two arms over 24 requests (6 promptings x single/two-token budgets x maxStrategies 1 and 3): the production prompt, and the same prompt with a worked baseline object appended. The baseline is built from the live request via templateFallback, never a static example — a fixed blob would hand the model stale constants to echo, which is the fabrication bug compose.ts already had to fix once. The arms belong to the experiment, so the spike appends the baseline itself and re-implements the reject-and-re-infer loop in miniature rather than adding a flag to the shipped prompt builder. Beyond the metrics #28 asked for, it measures ECHO FIDELITY: whether the model actually uses the amounts and band tiers we precompute for it. That is the assumption all of Tier 0 rests on and it has never been measured against a live 7B. --dry-run answers from a script keyed per run (malformed-then-good, verbatim parrot, over-budget-then-good, clean first try) so every path the report measures is exercised without a key. Writing it caught two harness bugs: the parrot response was sliced from "schema" rather than the opening brace, so it arrived as a fragment and read as malformed; and a global call counter made a first-attempt success impossible, leaving firstTry and parrotRate permanently zero. The gate is evaluated by the script, not eyeballed by whoever hoped the idea would work. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 26, 2026
Two fixes found by the first live run against Galileo. dotenv: PR #30 moved `import "dotenv/config"` out of config.ts into each CLI entrypoint. The spike is an entrypoint and never imported it, so loadConfig could not see ZG_PRIVATE_KEY. The dry run could not catch this — it never calls loadConfig. Failure diagnostics: the report said 75% fell back and gave nobody a lead. It now prints the failure kind per run and a violation-code histogram per arm, plus one sample rejection text. That is what turned an opaque "100% malformed" into a diagnosis: sub-second empty responses, which is a drained compute ledger rejecting requests, not a model failing to comply. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a reproducible spike harness to measure whether appending a request-specific “baseline” (from templateFallback) improves model compliance versus increasing parroting, per Issue #28. This lives under spike/ and re-implements a minimal reject-and-re-infer loop so the experiment can vary prompt/loop behavior without touching production compose code.
Changes:
- Introduces
spike/baseline-anchoring.tsto run a fixed 24-request suite across two arms (control vs baseline) with metrics and a decision gate. - Adds a
--dry-runmode with canned responses to exercise parsing/validation/parrot detection without a funded key or network. - Reports acceptance/fallback, malformed rate, parroting, echo-fidelity checks, latency, and estimated token costs.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+290
to
+297
| while (attempts < MAX_COMPOSE_ATTEMPTS) { | ||
| const messages = messagesFor(arm, item.req, feedback); | ||
| promptChars = messages.reduce((n, m) => n + m.content.length, 0); | ||
| const raw = await infer(messages); | ||
| attempts += 1; | ||
| latencyMs += raw.latencyMs; | ||
| completionChars = raw.resultText.length; | ||
|
|
Comment on lines
+384
to
+406
| // The gate, evaluated rather than eyeballed — the numbers decide, not the | ||
| // person who hoped the idea would work. | ||
| const badness = (rows_: RunResult[]) => { | ||
| const rs = rows_.filter((r) => r.arm !== undefined); | ||
| const attempts = rs.reduce((s, r) => s + r.attempts, 0); | ||
| const bad = | ||
| rs.reduce((s, r) => s + r.malformedAttempts, 0) + | ||
| rs.filter((r) => !r.accepted).length; | ||
| return attempts === 0 ? 0 : bad / attempts; | ||
| }; | ||
| const c = badness(rows.filter((r) => r.arm === "control")); | ||
| const b = badness(rows.filter((r) => r.arm === "baseline")); | ||
| const drop = c === 0 ? 0 : (c - b) / c; | ||
| const acceptedBaseline = rows.filter((r) => r.arm === "baseline" && r.accepted); | ||
| const parrotRate = | ||
| acceptedBaseline.length === 0 | ||
| ? 0 | ||
| : acceptedBaseline.filter((r) => r.parrot).length / acceptedBaseline.length; | ||
|
|
||
| console.log( | ||
| `\nGATE: malformed+fallback dropped ${Math.round(drop * 100)}% relative (bar: >= 30%), ` + | ||
| `parrot rate ${Math.round(parrotRate * 100)}% (bar: < 33%)`, | ||
| ); |
Comment on lines
+524
to
+528
| const dry = process.argv.includes("--dry-run"); | ||
| const limitArg = process.argv.indexOf("--limit"); | ||
| const limit = limitArg === -1 ? Infinity : Number(process.argv[limitArg + 1]); | ||
|
|
||
| const items = requestSet().slice(0, limit); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #28 — its first acceptance criterion (the script + fixed request set). The other two (the comparison table, the go/no-go) need a funded Galileo key, so #28 stays open; this makes producing the numbers one command instead of an afternoon.
What it does
Two arms over 24 requests (6 promptings × single/two-token budgets ×
maxStrategies1 and 3 — the mix #28 specified):buildComposeMessagesemits itThe baseline is generated from the live request via
templateFallback, never a static example: a fixed blob would hand the model stale constants to echo, which is precisely the chainId/deadline fabricationcompose.tsalready had to fix once.The arms belong to the experiment, not to the shipped prompt, so the spike appends the baseline itself and re-implements the reject-and-re-infer loop in miniature (~20 lines). No flag, no env sniffing, nothing spike-shaped left in
compose.ts.One metric beyond what #28 asked for
Echo fidelity — does the model actually use the pairing amounts and band tiers we precompute, or quietly re-derive them? That is the assumption every Tier 0 change rests on (#24, #25, #26) and it has never been measured against a live 7B. Arguably worth more than the baseline question this issue was opened for. Measured generously on purpose: a model echoing two of three legs is a different animal from one ignoring the block.
Verification, given the real thing can't run yet
--dry-runanswers from a script keyed per run — malformed-then-good, verbatim parrot, over-budget-then-good, clean first try — so every path the report measures is exercised with no key and no network:Parrots are detected in the baseline arm and correctly absent from control (nothing to parrot). These are canned numbers — they say the instrument works, nothing about the model.
Writing the dry run earned its keep by catching two harness bugs that would otherwise have surfaced as findings: the parrot response was sliced from
"schema"rather than the opening brace, so it arrived as a fragment and scored as malformed; and a global call counter made a first-attempt success impossible, pinningfirstTryandparrotRateto zero forever.The gate (≥30% relative drop in malformed+fallback and parrot rate <1/3) is evaluated by the script rather than eyeballed by whoever hoped the idea would work.
Running it
Live, once a funded key exists — up to 96 inferences, so
--limitfirst:Fund the ledger out-of-band (
npm run fund); this script never funds.Notes
stubContext()deliberately — the experiment varies the prompt, so the market and book must not move underneath the arms.inferChatdrops the provider'susagefield; surfacing it would be a production change this spike doesn't need.🤖 Generated with Claude Code