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>
This commit is contained in:
2026-08-17 18:27:03 -07:00
parent 99d165b5e5
commit 516685526c
61 changed files with 13013 additions and 29 deletions
+187
View File
@@ -0,0 +1,187 @@
/**
* 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);
});
});