Speak the scorecard's own band labels, not a second vocabulary

The Trainability and Deal Qualification Scorecard publishes five bands and an
action for each — Decline, Defer, Scope down, Qualified conditional, Build —
and `MOTION_BANDS` published four different ones with different edges. So a
reader could read the scorecard, score a deal against the exact dimensions it
defines, and be told "Strategic" by a band table that document has never heard
of. Two answers to the same question from the same product.

The scorecard wins, on two grounds. Its edges were chosen alongside the
dimension weights they sit on top of, so 78 means something there and 7500 was
a round number here. And every one of its labels is a verb the reader can act
on: "Qualified" describes a deal, "Scope down" says what to do about it, which
is the only reason to band a score rather than show it.

The labels are now duplicated between the JSON a customer reads and the table
the product renders, because a rendered label cannot reach into a seeded row.
That duplication gets a test asserting the whole table verbatim, so
re-authoring one copy alone fails rather than drifts.

`apps/api/test/motion.test.ts` asserted the literal 'Strategic'. It now derives
the band through the shared function, so a band-table change is caught by the
test that owns the decision instead of by a write-path test that does not.
This commit is contained in:
2026-08-19 00:26:21 -07:00
parent 2f32186d22
commit b7d1ffd2d8
3 changed files with 57 additions and 18 deletions
+30 -9
View File
@@ -34,7 +34,7 @@ describe('motionScoreBasisPoints', () => {
it('returns 0 rather than throwing when every weight is zero', () => {
// The naive version divides by the weight total and produces Infinity,
// which `motionBand` would then clamp to "Strategic" — a deal nobody
// which `motionBand` would then clamp to "Build" — a deal nobody
// scored recommended as the best one in the book.
const score = motionScoreBasisPoints([
{ id: 'fit', weight: 0, score: 4 },
@@ -112,20 +112,41 @@ describe('motionBand', () => {
* file and tells a seller to defer a deal that qualified.
*/
it('is inclusive at the lower edge of every band', () => {
assert.equal(motionBand(3500).label, 'Not yet');
assert.equal(motionBand(5500).label, 'Qualified');
assert.equal(motionBand(7500).label, 'Strategic');
assert.equal(motionBand(3000).label, 'Defer');
assert.equal(motionBand(4600).label, 'Scope down');
assert.equal(motionBand(6200).label, 'Qualified, conditional');
assert.equal(motionBand(7800).label, 'Build');
});
it('keeps the basis point below each edge in the band underneath', () => {
assert.equal(motionBand(3499).label, 'Decline or defer');
assert.equal(motionBand(5499).label, 'Not yet');
assert.equal(motionBand(7499).label, 'Qualified');
assert.equal(motionBand(2999).label, 'Decline');
assert.equal(motionBand(4599).label, 'Defer');
assert.equal(motionBand(6199).label, 'Scope down');
assert.equal(motionBand(7799).label, 'Qualified, conditional');
});
it('covers both ends of the scale', () => {
assert.equal(motionBand(0).label, 'Decline or defer');
assert.equal(motionBand(10_000).label, 'Strategic');
assert.equal(motionBand(0).label, 'Decline');
assert.equal(motionBand(10_000).label, 'Build');
});
/**
* The scorecard a customer reads publishes these five bands and an action for
* each. They are duplicated in `MOTION_BANDS` because the product renders a
* label the JSON cannot reach — so this pins the two copies together, and
* fails when somebody re-authors one of them alone.
*/
it('uses the labels and edges the seeded scorecard publishes', () => {
assert.deepEqual(
MOTION_BANDS.map((band) => [band.min, band.max, band.label]),
[
[0, 2999, 'Decline'],
[3000, 4599, 'Defer'],
[4600, 6199, 'Scope down'],
[6200, 7799, 'Qualified, conditional'],
[7800, 10_000, 'Build'],
],
);
});
it('leaves no gap and no overlap across the whole range', () => {