Files
pig/packages/core/test/motion.test.ts
T
karti b7d1ffd2d8 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.
2026-08-19 00:26:21 -07:00

209 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Tests for the motion scoring arithmetic.
*
* A qualification score is not a dashboard ornament: the band it lands in is
* what a seller is told to do about a deal, and the row is append-only
* evidence that the judgement was made. So the cases below pin the
* *decisions* — the empty book, the rounding direction, and above all the band
* boundaries, each of which a plausible-but-wrong implementation gets wrong by
* exactly one basis point and reports no error about.
*/
import { strict as assert } from 'node:assert';
import { describe, it } from 'node:test';
import { DEMAND_STAGES } from '../src/ontology';
import {
MOTION_BANDS,
MOTION_KINDS,
MOTION_KIND_DESCRIPTIONS,
MOTION_KIND_LABELS,
MOTION_KIND_STAGES,
isArtifactStatus,
isMotionKind,
isMotionVisibility,
motionBand,
motionScoreBasisPoints,
} from '../src/motion';
describe('motionScoreBasisPoints', () => {
it('returns 0 for an empty framework rather than NaN', () => {
// A framework with no dimensions renders somewhere. `NaN` renders as "NaN".
const score = motionScoreBasisPoints([]);
assert.equal(score, 0);
assert.ok(!Number.isNaN(score));
});
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 "Build" — a deal nobody
// scored recommended as the best one in the book.
const score = motionScoreBasisPoints([
{ id: 'fit', weight: 0, score: 4 },
{ id: 'budget', weight: 0, score: 4 },
]);
assert.equal(score, 0);
});
it('scores a full house at exactly the maximum', () => {
assert.equal(
motionScoreBasisPoints([
{ id: 'fit', weight: 30, score: 4 },
{ id: 'budget', weight: 70, score: 4 },
]),
10_000,
);
});
it('does not require weights to sum to 100', () => {
// Two frameworks expressing the same judgement must score the same, or the
// number means nothing across templates.
const outOf100 = motionScoreBasisPoints([
{ id: 'fit', weight: 50, score: 4 },
{ id: 'budget', weight: 50, score: 2 },
]);
const outOf6 = motionScoreBasisPoints([
{ id: 'fit', weight: 3, score: 4 },
{ id: 'budget', weight: 3, score: 2 },
]);
assert.equal(outOf100, 7500);
assert.equal(outOf6, 7500);
});
it('rounds half-up rather than truncating', () => {
// 10000 × 5 / (4 × 8) = 1562.5 exactly. Truncation gives 1562, which is
// the whole difference between this and a naive `Math.floor` — small, and
// it moves a score across a band boundary once every few hundred deals.
const score = motionScoreBasisPoints([
{ id: 'fit', weight: 5, score: 1 },
{ id: 'budget', weight: 3, score: 0 },
]);
assert.equal(score, 1563);
});
it('does not let a zero-weight dimension move the total', () => {
// The case that flatters: a dimension somebody scored 4 and weighted out
// of the framework must not drag the score up on its way past.
const withoutIt = motionScoreBasisPoints([{ id: 'fit', weight: 10, score: 2 }]);
const withIt = motionScoreBasisPoints([
{ id: 'fit', weight: 10, score: 2 },
{ id: 'vanity', weight: 0, score: 4 },
]);
assert.equal(withoutIt, 5000);
assert.equal(withIt, withoutIt);
});
it('always returns an integer inside 010000', () => {
for (const weight of [1, 3, 7, 17, 100]) {
for (const score of [0, 1, 2, 3, 4]) {
const basisPoints = motionScoreBasisPoints([
{ id: 'a', weight, score },
{ id: 'b', weight: weight + 1, score: 4 - score },
]);
assert.ok(Number.isInteger(basisPoints), `${weight}/${score} produced a fraction`);
assert.ok(basisPoints >= 0 && basisPoints <= 10_000, `${basisPoints} is out of range`);
}
}
});
});
describe('motionBand', () => {
/**
* The boundaries, one assertion per edge. Inclusive at `min`: an
* implementation using `>` instead of `>=` passes every other test in this
* file and tells a seller to defer a deal that qualified.
*/
it('is inclusive at the lower edge of every band', () => {
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(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');
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', () => {
// Written as a sweep because a hand-edited band table is exactly the kind
// of data where a typo produces a score that belongs to two bands, or none.
for (let basisPoints = 0; basisPoints <= 10_000; basisPoints += 1) {
const matches = MOTION_BANDS.filter(
(band) => basisPoints >= band.min && basisPoints <= band.max,
);
assert.equal(matches.length, 1, `${basisPoints} matched ${matches.length} bands`);
}
});
});
describe('the kind register', () => {
it('labels and describes every kind', () => {
for (const kind of MOTION_KINDS) {
assert.ok(MOTION_KIND_LABELS[kind], `${kind} has no label`);
assert.ok(MOTION_KIND_DESCRIPTIONS[kind], `${kind} has no description`);
}
});
it('maps every kind onto stages that actually exist', () => {
// The stage list is the demand pipeline, not a second vocabulary. A typo
// here would produce a template nothing can ever be filed against.
for (const kind of MOTION_KINDS) {
const stages = MOTION_KIND_STAGES[kind];
assert.ok(stages.length > 0, `${kind} serves no stage`);
for (const stage of stages) {
assert.ok(DEMAND_STAGES.includes(stage), `${kind} claims unknown stage ${stage}`);
}
}
});
it('never files a kind against a closed stage', () => {
// A won or lost deal has left the motion; offering to instantiate into it
// is an invitation to do work against a dead deal.
for (const kind of MOTION_KINDS) {
for (const stage of MOTION_KIND_STAGES[kind]) {
assert.ok(stage !== 'closed_won' && stage !== 'closed_lost', `${kind} serves ${stage}`);
}
}
});
it('gives the playbook the whole live motion', () => {
assert.equal(MOTION_KIND_STAGES.playbook.length, 8);
});
});
describe('type guards', () => {
it('accepts members and rejects near misses', () => {
assert.equal(isMotionKind('playbook'), true);
assert.equal(isMotionKind('Playbook'), false);
assert.equal(isMotionVisibility('shared'), true);
assert.equal(isMotionVisibility('public'), false, 'shared is book-wide, not public');
assert.equal(isArtifactStatus('final'), true);
assert.equal(isArtifactStatus('published'), false);
});
});