Score what decides advancement, not just delivery#10
Conversation
The headline score averaged four delivery dimensions (answer_relevance, structure, conciseness, questions_asked) and nothing else, so a candidate who communicated well and got the substance wrong scored highly. On one real transcript — fluent, structured, brute-force-only, three hints, no Big-O — the old rubric returned 4.25 (green); the new one returns lean_no with all four delivery dimensions still at 4. Three causes, all structural: - Nothing ever asked about advancement. The schema requested scores and prose; "would this candidate advance?" was never a question the model answered. Now `advancement` is a discrete 4-point forced choice, elicited independently and explicitly NOT derived from the scores — matching how real scorecards co-record a verdict alongside ratings rather than computing one from the other. - The schema forbade scoring substance. `additionalProperties: false` over four fixed keys meant the rich round-specific signal already written into product_sense.md and tech_deep_dive.md could never reach a score. Scored dimensions are now parsed from each overlay's `## Scored dimensions` section, so the rubric stays data: a new round type is a new .md file, no Swift change. - Averaging compressed dimensions destroys discrimination. Anchored 1-5 bands and an explicit anti-leniency instruction replace free-floating numbers. `overallScore` remains, but as a secondary trend line only — it is comparable within a round type, not across. Evidence and its limits are recorded in the DefaultPrompts header. A deep research pass (20 sources, 25 claims adversarially verified, 11 survived) found NO primary rubric for recruiter_screen, system_design, product_sense, or tech_deep_dive — those dimension sets are authored judgment and say so. Notably, whether advancement is conjunctive or compensatory is NOT settled: claims in both directions were refuted, so no weighting formula was added. Also in this change: - process_notes: timestamped extraction of what the interviewer said about the process, next steps, and timeline — captured in every round type, surfaced per company in the Pipeline and above the debrief in a session. Guarded against inventing next steps from a polite sign-off. - TranscriptArtifacts: strips Whisper's non-speech markers at the Store write boundary, where both live-stop and crash recovery funnel through. 11% of segments in a real database were [BLANK_AUDIO], silence variants, chunk-cut brackets, or ">>" speaker markers. Migration v5 applies the identical rules to existing rows. - Re-run debriefs reports live progress. State lives on AppEnvironment because MainWindow's tab `switch` destroys SettingsView — as view @State the progress vanished on a tab switch while the run continued, and returning re-enabled the button, allowing a second concurrent run. - Pipeline session cells navigate to the session. Schema gotcha worth knowing: the Messages API rejects minimum/maximum on integer types with a 400. Mocked tests passed against a schema that would have failed every real debrief; only a live call caught it. Score ranges live in the prompt, and the 1-5 band is enforced in CoachingResult's decoder — a real debrief in the database was stored as all-zeros because nothing rejected it. Migrations v3 (verdict), v4 (process notes), v5 (transcript cleanup), verified against a copy of a real 11-session database. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hitting Stop tears down the in-flight URLSession call, which throws URLError.cancelled into coach()'s catch — where it was treated like any other error: markCoachingFailed. So stopping a re-run flipped a session that still held perfectly good feedback into a failed state, showed an error badge, and dragged it into "Retry pending debriefs". The exact opposite of what Stop should do. The guard is deliberately narrow — a refusal or an HTTP error is a real failure and stays retryable. Covered by three tests: cancel leaves a complete session complete with its debrief intact, cancellation isn't reported to the UI as a per-session failure, and a genuine refusal is still marked failed. Found while reviewing the cancellation path added earlier in this branch; the workflow-backed review could not run (all finder agents hit the session limit), so this is not a substitute for that review. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The dominant one would have shipped a broken app to every existing user.
dimensions(for:) returned [] on any install that had ever launched Debrief.
ensureDefaults() never overwrites, and the old seeded base.md has no
`## Scored dimensions` heading — so parsing yielded nothing, the Anthropic
schema was built with an empty `scores` object, and every debrief stored a
0.0 average (resurrecting the all-zeros bug the new decoder was added to
prevent). The local-LLM path failed every debrief outright. "Re-run debriefs
on current rubric" would have spread it across all history. Every test
seeded into a fresh temp dir, and the author had hand-regenerated their own
prompts — so nothing caught it.
ensureDefaults now upgrades a builtin prompt that predates scored
dimensions, preserving the old file as <name>.md.pre-dimensions.bak. This
deliberately narrows the old "never overwrites" contract: a base.md without
dimensions cannot work at all now, so replacing it beats leaving the app
broken with the user's text intact but unusable. A prompt the user edited
that still declares its dimensions is still untouchable. dimensions(for:)
now throws rather than returning [], so the failure is loud and retryable.
Verified end-to-end against the real pre-PR prompts on a real install.
Also fixed:
- insertSegments can legitimately write zero rows (a recording that is all
[BLANK_AUDIO]), which defeated RecordingCoordinator's orphan guard and
left a session with an empty transcript that still got coached — the LLM
confabulating a debrief for an interview it never saw. It now returns the
count and finalize throws, routing into the existing orphan cleanup.
- bareMarkerSentence ate real speech: "No audio." (an interviewer flagging a
mic problem) and "Pause. Let me think about that." both vanished. Narrowed
to the two markers actually observed bare in real recordings.
- Stop reported "Stopped after 11 of 11" and hid the failure count; progress
is now the authority, not Task.isCancelled.
- Cancellation is keyed on Task.isCancelled rather than the error type, so a
URLError.cancelled with no Stop behind it stays a real, retryable failure
in retryAllPending (which has no Stop button and shares the loop).
- The Pipeline reveal never scrolled: onChange was installed inside a branch
that only exists once rows load, so it never observed a change. task(id:).
- ForEach over process notes keyed on the timestamp, which is not unique.
- A re-coach failure on a complete session was invisible: the status badge
only rendered when overallScore == nil, but the stale feedback row stays.
- processNotes decoded with decodeIfPresent while both clients declare it
required — the one path that could catch a client dropping it was the one
that hid it.
DECLINED, with evidence: the finding that the paren rule eats spoken asides
("we shipped it (about 200ms p99)"). All 10 distinct parenthesized spans in
a real 11-interview database are non-speech annotations — (Laughter),
(door opens), (eerie music), (upbeat music), (laughing), (sniffing) — and 0
are speech. Whisper inherits subtitle convention, where parens mean sound
events. Anchoring on marker words would leak most of them. The counter-
examples were invented, not observed; the rule stays broad and says why.
Known and measured, not fixed: process_notes recall is a model behaviour and
misses on ~2 in 7 runs against a transcript that states the process. The
instruction was rebalanced (it spent three sentences on not inventing notes
and one on capturing them) and moved out of the trailing field list. The
integration test is documented as a recall probe that can flake rather than
weakened into something that cannot fail.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review round: 10 verified findings, 9 fixed, 1 declined with evidenceHigh-effort workflow review (28 agents, independent verifier per finding). The first attempt reported "no findings survived verification" — that was an infrastructure failure, not a clean bill of health: all four finder agents died on a session limit and produced zero candidates. Re-ran after the reset. The one that mattered — would have broken every existing install
Why the original verification missed it: every test seeds into a fresh temp dir (works), and I'd hand-regenerated my own live prompts — masking it on the only machine that could have shown it. I documented this exact gotcha in CLAUDE.md and still walked into it.
Also fixed
Declined, with evidenceThe finding that the paren rule eats spoken asides ( Known and measured, not fixed
114 unit tests, 0 failures (was 79 at branch start). |
Rebalancing the instruction for recall traded away precision: on a transcript with no process at all, the model produced "Interviewer ended on time and invited candidate questions; no next steps, timeline, or follow-up process were stated." — a note whose entire content is that there was nothing to note. That is the empty case with extra words, and there is nothing in it to act on. Replaces the removed three-sentence negative wall (which suppressed recall) with one concrete negative example of the actual observed failure, and says plainly that both mistakes are real so neither gets traded for the other. Verified in both directions against the live API: the no-process transcript now returns [], and the transcript that states three rounds / a Friday ask still captures them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The problem, on the bench
The headline score averaged four delivery dimensions and nothing else. Same transcript (fluent, structured, brute-force-only, three escalating hints, couldn't state a complexity), same model:
The user's own data says the same thing: OneSchema scored their highest marks (3.0, 3.0, 2.0) and is
dead; Cloneable scored their lowest (1.75) and isactive.Three structural causes
advancementis now a discrete 4-point forced choice (strong_no/lean_no/lean_yes/strong_yes), elicited independently and explicitly not derived from the scores.additionalProperties: falseover four fixed keys meant the detailed signal already written intoproduct_sense.md/tech_deep_dive.mdcould never reach a score. Dimensions are now parsed from each overlay's## Scored dimensionssection — a new round type is a new.mdfile, no Swift change.overallScorestays as a secondary trend line, comparable within a round type only.What the research did and didn't support
20 sources, 25 claims adversarially verified, 11 survived. Recorded in the
DefaultPromptsheader.Also included
process_notes— timestamped extraction of what the interviewer said about process/next steps/timeline, in every round type. Surfaced per company in the Pipeline (newest round first) and above the debrief in a session. Guarded against inventing next steps from a polite sign-off.TranscriptArtifacts— strips Whisper non-speech markers at the Store write boundary (the funnel both live-stop and crash recovery pass through). 11% of segments in the real database were[BLANK_AUDIO], silence variants, chunk-cut brackets, or>>speaker markers. Migration v5 applies identical rules to existing rows.AppEnvironmentbecauseMainWindow's tabswitchdestroysSettingsView; as view@Stateprogress vanished on tab switch while the run continued, and returning re-enabled the button (second concurrent run).Bugs found by verifying against reality
Two classes of bug that
swift testcannot see, both caught by real calls:minimum/maximumon integer types (400). Mocked tests passed against a schema that would have failed every debrief.CoachingResult's decoder (rejects rather than clamps, so the session stays retryable).Testing
CoachingIntegrationTests— real API calls; the only check that the built schema is one the API accepts, and that the rubric elicits what it asks forReviewer notes
ClaudeClient.outputSchema(JSON schema),OpenAICompatibleClient.formatAppendix(prose — local servers can't be trusted withjson_schema), and the prompt markdown. They drift independently;scoresbeing[String: Int]means a local model returning wrong keys still decodes, so there's an explicit key-set check.PromptStore.ensureDefaults()only writes files that don't exist — editingDefaultPromptsalone will not update an existing install.Known, not smoothed over
process_notesreturned[]once, early, on a transcript full of stated process. Correct 5/5 times since; not reproducible, and not explained. Possible intermittent."placeholder"once, never again.🤖 Generated with Claude Code