Files
pig/packages/core/test/motion.test.ts
karti 516685526c Add Motion: the go-to-market operating system on top of the ledger
The ledger answers which contracted capacity is sold, to whom, at what
margin. It says nothing about the motion — the repeatable practice that
turns a customer conversation into a scoped deployment, and turns that
deployment into something the next one reuses.

Motion is deliberately not a parallel entity tree. DEMAND_STAGES already
is the motion, so Motion binds reusable artefacts to the stages of a
demand deal that already exists: an engagement hangs off one deal,
cascade deleted, one per deal by unique constraint.

Nine closed kinds, each declaring which stages it serves, and a starter
library of twelve templates covering all eight open stages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 18:27:03 -07:00

188 lines
7.0 KiB
TypeScript
Raw Permalink 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 "Strategic" — 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(3500).label, 'Not yet');
assert.equal(motionBand(5500).label, 'Qualified');
assert.equal(motionBand(7500).label, 'Strategic');
});
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');
});
it('covers both ends of the scale', () => {
assert.equal(motionBand(0).label, 'Decline or defer');
assert.equal(motionBand(10_000).label, 'Strategic');
});
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);
});
});