Skip to content

TB empty-gold fairness: pure_refusal-only gate under full catalog - #2831

Draft
Dominic Nguyen (datduyng) wants to merge 29 commits into
mainfrom
domnguyen/tb-negative-fairness
Draft

TB empty-gold fairness: pure_refusal-only gate under full catalog#2831
Dominic Nguyen (datduyng) wants to merge 29 commits into
mainfrom
domnguyen/tb-negative-fairness

Conversation

@datduyng

@datduyng Dominic Nguyen (datduyng) commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Empty-gold TB negatives (expectedActions: []) are scored as zero actions across the full loaded tool catalog. Label-fair Q&A/definition negatives were not zero-action-scorer-fair: models correctly fire chat/help/history, tanking negative pass.

This PR ships a pipeline fairness contract so empty gold is only accepted for hard abstains:

  • Synthesizer + quality-verifier prompts: empty-gold kind is pure_refusal only (leave-alone / don't / never / cancel of the target; no questions, no alternate task).
  • negativeFairness module: path-keyed 1:1 negativeAssessments (no regex path parse); fail-closed on missing/duplicate paths; fairEmptyGold only when kind is in the allowlist (TRANSLATION_BENCH_FAIR_EMPTY_GOLD_KINDS).
  • Partial generation: when complete coverage is optional, emitted caseCount / coverage metadata match the cases actually produced (so validation can succeed).
  • No gold-row hand edits and no empty-gold scorer loosening.

Measured 3-model eval (honest)

Regenerated draft under the new policy (998/1000 rows; 2 slots exhausted data-quality, unrelated to fairness), then evaluated azure/gpt-5.6-{sol,terra,luna}.

Metric Result
Rows / empty-gold negs 998 / 998
Kind mix 100% pure_refusal
Unfair rate (LLM audit) 0%
Overall pass 36.8%
Negative pass (zero fire on empty gold) 13.9%
Neg fire-on-empty 47.9%
Positive pass 59.8%
Pos abstention FNR 3.2%
Tool / param 83.5% / ~71.6%
Eval cost ~$190

Finding: fairness regen fixed the dataset (kind mix + unfair rate), but did not produce a large negative-pass lift. Checkpoint inspection shows models often emit the refused action itself on clear hard-abstains (negation-following failure). Translator/prompt work is out of scope here.

Test plan

  • pnpm run build in ts/packages/benchmarks
  • pnpm run jest-esm --testPathPattern='(negativeFairness|datasetGenerator)\\.spec\\.js' (43/43)
  • Smoke + full regen fairness audit (0% unfair, 100% pure_refusal)
  • 3-model headless eval on regenerated draft; scale-metrics recorded

Dominic Nguyen (datduyng) and others added 17 commits August 7, 2026 22:00
Empty expectedActions is scored as zero actions. Stop minting contrastive
adjacent commands and refuse-then-alternate forms as empty-gold negatives;
format_checker now hard-rejects them as BAD_NEGATIVE.

Allowed negatives: pure refusal, non-action status/howto questions, and
missing-info clarifications. Align synthesizer and quality-verifier prompts
with the zero-action contract; add unit coverage including adversarial holes.
- Drop ACTION_VP / refusal regex classifier (unmaintainable verb lists).
- Require semantic_checker negativeAssessments (kind + fairEmptyGold).
- Code hard-fails unfair assessments as BAD_NEGATIVE; format stays structural.
- Tests cover assessment parse/enforce and mock-LLM semantic gate.
- Single zod schema; derive OpenAI JSON schema via z.toJSONSchema
- Match assessments by order (equal count); drop path-index maps
- Simplify force-reject merge; strip explanatory comments
- Drop dead exports; parse via shared parseWithZod
- Derive OpenAI schema from zod without void/\$schema clutter
- Add test that assessments pair to negatives by order
- Join assessments to negatives by exact genCase path (bijective set);
  reject unknown/duplicate/missing paths as BAD_NEGATIVE instead of
  silent index pairing.
- Parse reviewer decision before assessments so structured reject
  issues survive missing/invalid negativeAssessments.
- Share TRANSLATION_BENCH_NEGATIVE_FAIRNESS_RULE in synthesizer context;
  tighten howto/soft-solicit unfair rules and untrusted payload framing.
- Extend reviewerDecision mocks with path-keyed fair assessments;
  replace order-only path tests with multi-negative path-join coverage.
- Fixed short PATH_MSG/FIX only; use LLM reason as issue message
- Drop rewrite-hint and per-case string assembly
- Keep path 1:1 cover + fairEmptyGold/kind gate
- Honor concurrency with a worker pool and serialized checkpoint commits
- Keep parameters:{} after stripEmpty for required empty-object schemas
- Inject constant string-union fields (e.g. settings id) during gold validate
- Isolate per-slot generation failures so other workers keep committing
- Fair empty gold is pure_refusal/leave-alone only
- Reject definition/status/Q&A and missing_info as empty gold
- Synthesizer + quality-verifier prompts match any-tool abstain test
- Extend unit coverage for definition Q and soft-solicit rejects
Join assessments to genCases via a path→case map built from the same
keys the checker emits, then read dimensions.negativeKind directly.
…params

- Allow incomplete case sets when requireCompleteCoverage is false
- Prompt: nested objects must carry required schema fields (e.g. timeRange)
…ial gen

- Partial-gen branch previously emitted planned caseCount/coverage, so
  validateTranslationBenchBenchmark always threw and the branch was
  unreachable dead code
- Recompute scheduledActionCount, complete, and caseCount from the cases
  actually emitted; happy-path (complete) output is unchanged
- Makes requireCompleteCoverage=false produce a valid draft directly
Derive FAIR_KINDS from TRANSLATION_BENCH_FAIR_EMPTY_GOLD_KINDS so the
allowlist cannot drift from the exported constant.
@datduyng Dominic Nguyen (datduyng) changed the title Reject unfair empty-gold TB negatives via LLM fairness gate TB empty-gold fairness: pure_refusal-only gate under full catalog Aug 8, 2026
@datduyng
Dominic Nguyen (datduyng) requested a balanced review from Copilot August 9, 2026 00:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a fairness contract ensuring empty-gold translation-bench negatives represent only pure refusals.

Changes:

  • Adds path-keyed LLM fairness assessments and enforcement.
  • Updates generation/verifier prompts and gold-action validation.
  • Supports concurrent generation and accurate partial-result metadata.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
README.AUTOGEN.md Refreshes generated documentation metadata.
actionValidation.ts Normalizes gold actions for schema validation.
benchmark.ts Uses normalized gold-action validation.
dataQualityVerifier.ts Enforces semantic negative-fairness assessments.
datasetGenerator.ts Adds concurrency and partial-generation handling.
generationCandidate.ts Preserves required parameter objects during cleanup.
index.ts Exports the fairness module.
negativeFairness.ts Implements fairness parsing and enforcement.
quality-verifier.prompt.yaml Defines full-catalog fairness review requirements.
synthesizer.prompt.yaml Restricts empty-gold negatives to pure refusals.
utteranceDisambiguation.ts Documents separate negative handling.
translationBench.datasetGenerator.spec.ts Updates generation fixtures for fairness assessments.
translationBench.negativeFairness.spec.ts Tests fairness validation and semantic enforcement.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

…s, ambiguous-route guard

Row-by-row 3-model review of the 1k eval showed most all-models "failures"
were dataset/scoring fairness issues, not real misses. Address the
generator-side ones:

- Wire parameterScore specs into every generated case at finalize
  (`parameterScoreSpecsForExpectedActions`). Free-text echo params such as
  `originalRequest` and setting free-text now score `nonempty` instead of
  exact, matching the grader's own classification. Canonical payload hash
  excludes parameterScore, so dataset identity is preserved.
- Add `HARDCODED_NON_EVAL_ACTION_IDS` (`chat.generateResponse`,
  `utility.claudeTask`) as a single source of truth, unioned into the
  packaged exclusion set so they are never targeted.
- Drop cross-schema duplicate action names from targeting: when the same
  bare action name is owned by more than one schema (e.g. `deleteWebFlow`),
  the single gold route is ambiguous, so exclude every sibling.

Tests: parameterScore wiring + canonical-hash stability, non-eval exclusion
set, and ambiguous cross-schema guard. Full benchmarks suite green (138).
…eckpoint, param-spec cleanup

Code-review + Copilot follow-ups on TB generation fairness:

- negativeFairness: require dimensions.negativeKind to exactly equal the
  accepted empty-gold assessment kind. Previously a fair pure_refusal
  assessment was accepted even when negativeKind was missing/numeric/boolean,
  letting unlabeled empty-gold rows through. Now the label must match.
- datasetGenerator: persist the checkpoint row BEFORE mutating casesBySlot /
  usedUtterances in commitAccepted, so an I/O failure can no longer leave an
  uncheckpointed case that the partial-coverage path would return.
- Unify the duplicated param-field-mode union: grader now imports
  TranslationBenchParamFieldMode / TranslationBenchParameterScoreSpec from
  benchmark instead of re-declaring RunnerParamFieldMode; collapse the spec
  derivation to Object.fromEntries.
- benchmark: enforce parameterScore aligns 1:1 with expectedActions in the
  probe payload schema (validateProbePayload).
- Extract ambiguousCrossSchemaActionIds helper; drop the size===0 ternary.

Tests: new integration coverage for generateTranslationBenchBenchmark
(concurrent full run + partial run past a failed slot, asserting checkpoint
contents and coverage); negativeKind-missing rejection; fixtures now label
negatives pure_refusal. Full suite green (141).
- Seed 24 empirically-mined cross-schema collision pairs into
  KNOWN_CONFUSABLE_PAIRS (browser tab routes, code/utility file ops,
  visualStudio/code-debug, desktop/display settings, player queues, etc.)
- Add generic cross-schema near-duplicate detector in
  findTranslationBenchConfusableSiblings: flags equivalent actions in
  different schemas when BOTH action-name token overlap >=0.5 AND
  description overlap >=0.34, so shared generic verbs alone do not
  over-flag unrelated actions
- Add significantTokensFromText helper for description-token overlap
- Extend unit tests with cross-schema newTextFile<->writeFile case
  plus a readFile negative control

Root cause: synthesizer scheduled every catalog action as a gold target
but only compared same-schema siblings for disambiguation, so cross-schema
near-synonyms produced no disambiguation constraint and yielded
ambiguous-gold cases the models unanimously routed elsewhere.
- Move catalog/grader generation into translationBench/policy
- Add action-eligibility hard bans + LLM quality picker (model required)
- Ship eligible-gold-actions.generated.json; fail-closed load/integrity
- Schedule lattice + eval pin allowlist hash; copyAssets requires assets
- Tests for picker, policy, nested llmAsAJudge, schedule allowlist-on
- remove dispatcher.lookup.lookupAndAnswerConversation from packaged
  eligible-gold allowlist and regenerate grader/policy artifacts
- update policyGenerator spec fixture to match
- action-quality picker now records include/exclude reason for every
  catalog action; artifact persists a sorted decisions[] list
- fail-closed integrity: allowlist must equal include=true decisions and
  every scheduled catalog action must have a reasoned decision
- prompt requires a non-empty reason per decision; contentHash unchanged
  (reasons excluded from hash)
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.

3 participants