Skip to content

feat: voice-drift check can baseline on style-guide voice exemplars (#2179 slice 3)#2247

Merged
atomantic merged 4 commits into
mainfrom
claim/issue-2179
Jul 7, 2026
Merged

feat: voice-drift check can baseline on style-guide voice exemplars (#2179 slice 3)#2247
atomantic merged 4 commits into
mainfrom
claim/issue-2179

Conversation

@atomantic

Copy link
Copy Markdown
Owner

Summary

Third slice of #2179 (CWQE Phase 14) — the voice-fingerprint baseline hook. Slices 1 (#2232, exemplar fields + prompt injection) and the prose-craft/continuity slices already shipped; this wires the exemplars into the deterministic style.voice-drift check.

The check flags an issue whose prose fingerprint sits > σ·threshold from a baseline. That baseline was always the drafted-issue mean, which has a blind spot: if every issue drifted the same way, the mean drifts with them and nothing flags. This adds a Drift baseline setting so drift can be measured against the voice you chose — the style guide's voice exemplars.

What shipped

  • computeExemplarBaseline(voiceExemplars, opts) (server/lib/editorial/voiceFingerprint.js) — combines the exemplar passages into one fingerprint; returns null (→ drafted fallback) when the combined text is under ~40 words (too thin to center against).
  • computeVoiceDrift gains baselineModedrafted (default, unchanged), exemplars (center on the chosen-voice profile), blended (midpoint). In exemplar/blended mode the σ stays the drafted spread — only the center shifts — so an issue is judged against the chosen voice while still tolerating the series' natural per-metric variance. Reports baselineMode + exemplarBaselineUsed; falls back to drafted (and says so) when no usable exemplars exist. series[key].center is added; describeDrift labels the baseline honestly ("vs the style guide's chosen voice").
  • style.voice-drift check (server/lib/editorial/checks/proseStyle.js) — declares series.styleGuide as a source (so an exemplar edit re-stales the finding, like its LLM siblings), adds a baselineMode config field (preprocessed to coerce any bad value to drafted), and passes ctx.series.styleGuide.voiceExemplars through.
  • Matrix view (server/services/pipeline/voiceFingerprint.js + client/src/pages/PipelineVoiceFingerprint.jsx) — resolves the same config so the highlighted outliers agree with the editor's findings, and renders the chosen-voice baseline as its own footer row + tooltip.

Partial ship

This is a slice, not the whole issue — the AI-assisted voice-discovery flow and Writers Room parity remain. So this PR uses Refs #2179 (not Closes), matching how slice 1 (#2232) shipped, and the claim markers are released so the remainder stays claimable.

Refs #2179

Test plan

  • server/lib/editorial/voiceFingerprint.test.js — 43 tests: computeExemplarBaseline (combine / null / word-floor / wells), the three baseline modes + fallback + coercion in computeVoiceDrift, baseline-aware describeDrift, and registry wiring (source declared, config parses, exemplar baseline surfaces drift the drafted mean hides).
  • server/services/pipeline/voiceFingerprint.test.js — service threads baselineMode + exemplars from the style guide; drafted default; exemplar re-center; thin-exemplar fallback.
  • client/src/pages/PipelineVoiceFingerprint.test.jsx — chosen-voice baseline row + copy render when exemplars mode is active.
  • Full server/lib/editorial/ suite green (907 tests). DB-backed suites are unrun here (no portos_test provisioned — they fail identically on clean main).

…2179)

Add a 'Drift baseline' mode to the style.voice-drift editorial check so an
issue's prose fingerprint can be measured against the CHOSEN voice (the style
guide's voice exemplars) instead of only the drafted-issue mean — closing the
blind spot where a whole series that drifted the same way never flags.

- computeExemplarBaseline() combines the voice exemplars into one fingerprint
  (>= 40 words to be a stable center, else null -> fall back to drafted).
- computeVoiceDrift() gains baselineMode (drafted|exemplars|blended); the center
  shifts to the exemplar profile (or midpoint) while sigma stays the drafted
  spread, and reports the effective mode + exemplarBaselineUsed.
- style.voice-drift declares series.styleGuide as a source (exemplar edits
  re-stale the finding) and a baselineMode config field; describeDrift labels
  the baseline honestly.
- The Voice Fingerprint matrix view resolves the same config and renders the
  chosen-voice baseline as its own footer row.

Third slice of #2179 (voice-fingerprint baseline hook).

Refs #2179
atomantic added 3 commits July 6, 2026 21:43
…≈0 boundary (#2179)

Review-gate findings on PR #2247:
- getVoiceFingerprint reports baselineMode/exemplarBaselineUsed even on a
  gatedOff run (< minIssues) where the baseline never ran and series is {}. The
  matrix view now gates usesChosenVoice on !gatedOff so the intro copy + footer
  row fall back instead of promising a baseline that wasn't applied.
- Document the σ≈0 boundary at the metric skip: a metric where every drafted
  issue shares the same value but sits far from the chosen-voice center wants a
  SERIES-level finding (different shape than the per-issue outliers), filed as
  #2248. The common same-direction-drift case (σ>0) is unaffected.

Refs #2179
…d label (#2179)

Codex review-gate findings on PR #2247:
- getVoiceFingerprint resolved only the GLOBAL style.voice-drift config, so a
  per-series editorialCheckConfig override (baselineMode/threshold/wells) drove
  the editor's findings but NOT the matrix view — they disagreed. resolveVoiceDriftConfig
  now takes the series' editorialCheckConfig and overlays the override on the
  global stored config, mirroring applySeriesCheckConfig's merge.
- The Voice Fingerprint intro copy described blended mode as 'the chosen voice
  (its voice exemplars)', but blended centers on the MIDPOINT of the drafted
  mean and the chosen voice. Intro copy + footer row now label blended distinctly.

Refs #2179
…2179)

Codex round-2 finding: resolveVoiceDriftConfig merged the RAW global config with
the series override and let resolveCheckConfig collapse the whole blob to bare
schema defaults if the merge was invalid — while the finding-emitting
applySeriesCheckConfig keeps the resolved global on an invalid override. A bad
per-series value (e.g. sigmaThreshold beyond the schema max) would make the
matrix use the default 1.5 while findings used the global threshold, re-opening
the matrix/finding disagreement. Now resolves the global first, then overlays
the override on it, keeping the resolved global on parse failure — byte-for-byte
the applySeriesCheckConfig contract.

Refs #2179
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