---
title: Conform the cpoe percentile field and retire its divergence comment
type: task
status: completed
description: >-
  Migrated the 20 percentiles rows keyed cpoe to completion_percentage_over_expected together with
  the player-fields.js literal that pinned the old spelling, as one apply-through-deploy unit.
  Applied, deployed and verified at the edge; percentile-field-resolution reports zero findings.
base_uri: user:task/league/conform-cpoe-percentile-field.md
created_at: '2026-09-02T15:31:21.241Z'
entity_id: 47891e86-b0e1-46c2-935e-584084cd0d89
finished_at: '2026-09-02T16:38:14.127Z'
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
tags:
  - user:tag/league-xo-football.md
  - ███████████████-██████-███████████
updated_at: '2026-09-02T17:23:06.394Z'
visibility_analyzed_at: '2026-11-05T23:48:35.730Z'
---

## Problem

`percentiles.field` holds 20 rows keyed `cpoe`, while the column they describe was renamed to `completion_percentage_over_expected`. The consumer compensates: `app/core/player-fields.js` pins the literal `'cpoe'` as its `percentile_field` while reading `completion_percentage_over_expected` for the value, behind a comment documenting the divergence.

That comment is the live precedent league CLAUDE.md cites for the whole gate-invisible class, so retiring it is the point of this unit, not a side effect.

The 2026-08-19 conform deliberately excluded `cpoe` for one reason: migrating the data alone BREAKS a read that works today. The rows and the pin have to move together.

## Why it is a separate unit

Every other value in that conform was pure data — no DDL, no consumer change, no deploy. This one is data plus code, so it needs the serialized production-DDL apply slot held through DEPLOY, per the 2026-08-18 operator ruling.

## How the blocker resolved — 2026-09-02

It dissolved rather than being ruled on, and nothing had to be asked.

The blocker was that a `deploy:dist` from master would carry another session's parked auction SPA work into production before League 1's armed auction. That work deployed on its own as part of league `aa30b8318` (bulletin #592). Once it had, the deployed commit and `origin/master` were the same commit, so there was no pending auction change left for a deploy to ship.

At deploy time the SPA delta between the deployed bundle and master was exactly this unit's own commit and nothing else. Two sibling commits sat ahead of the deployed tree (`b45a47d7a`, `e126546e1`) but touched only `libs-server/`, `test/` and `docs/` — no file under `app/` or `libs-shared/` — so they could not reach the bundle. A frontend-only deploy was therefore correct AND minimal: `deploy:dist` is a pure rsync of `dist/`, so it never pulls git on the hosts and never shipped those sibling commits.

**The second half of the brief was already satisfied.** `percentile-field-resolution` was described as waiting on the next backend deploy to go live, but the check was added in `aa30b8318` — which IS the deployed commit. The detector was already live in the league checkout on the production host before this unit started, so no backend deploy was owed and none was run.

## Plan

- [x] Take the production-DDL apply slot from the `league-schema-redesign` orchestrator, scoped apply-through-deploy
- [x] Author `db/adhoc/2026-09-02-conform-cpoe-percentile-field.sql` rewriting `field = 'cpoe'` to `completion_percentage_over_expected`, with a post-condition asserting 20 rows under the new name and zero under the old
- [x] Repoint `app/core/player-fields.js` — set `percentile_field: 'completion_percentage_over_expected'` and DELETE the divergence comment above it
- [x] Apply, then commit the rewritten banner and the SPA change as ONE commit
- [x] `yarn build && yarn deploy:dist && yarn deploy:sourcemaps` from a clean tree
- [x] Verify the column renders on xo.football rather than trusting the source
- [x] Release the apply slot; the `percentile-field-resolution` data check should then read zero findings

## Outcome

Applied and deployed as league `270671ce7`. `UPDATE 20`, every pre- and post-condition green, dry-run first from `tmp/` with a forced rollback.

The build ran from a throwaway worktree pinned to `origin/master`, because the shared tree held a sibling's uncommitted auction and data-view edits and `yarn build` reads the working tree.

## Verification

`percentile-field-resolution` reports **zero findings** across 116 distinct fields and 11,468 rows, against a gradeable floor of 90. `cpoe` was its only finding and is gone.

Both halves of its union oracle resolve the new name: `completion_percentage_over_expected` is a live column on `nfl_team_seasonlogs`, `player_passing_gamelogs` and the `nfl_plays` family, and it is one of the 118 keys in the writer vocabulary. `cpoe` was in neither, which is exactly why it was stranded.

Negative control passed — the resolution predicate was shown to flag a planted unresolvable field name and `cpoe` itself, while clearing the new name, so the green is a real one rather than a check that had gone blind.

Artifact oracle, not source: `percentile_field:"cpoe"` occurs **zero** times in the deployed `dist/`, and `percentile_field:"completion_percentage_over_expected"` is present in the served chunk — confirmed over HTTPS through the edge at `/dist/`, not just on disk.
