Policy Composer
policyComposer(policyType?) is a fluent, immutable merger for PolicyDefinition shapes. It lives next to the policyRegistry and is the recommended way to express a federation override of a stock policy.
import { policyComposer } from 'tods-competition-factory';
const ctsSeeding = policyComposer('seeding')
.extend(POLICY_SEEDING_USTA_DEFAULT)
.set('policyName', 'CTS SEEDING')
.set('seedingProfile.positioning', 'CLUSTER')
.build();
build() returns { [policyType]: definition } — the shape every engine method expects on its policyDefinitions: param. Hand it in directly.
Why this exists
Every consumer that needs a federation-specific tweak of a stock policy faces the same choice today:
-
Copy the entire policy and edit the changed fields by hand. The two ~25-line
POLICY_SEEDING_DEFAULT/POLICY_SEEDING_ITFblocks inTMX/src/components/drawers/addDraw/submitDrawParams.tsare the canonical example — they differ in three fields and share twenty-two. -
Hand-merge with spreads, accepting that every level of nesting that needs to be modified requires its own spread:
const cts = {[POLICY_TYPE_SEEDING]: {...POLICY_SEEDING_USTA_DEFAULT[POLICY_TYPE_SEEDING],policyName: 'CTS SEEDING',seedingProfile: {...POLICY_SEEDING_USTA_DEFAULT[POLICY_TYPE_SEEDING].seedingProfile,positioning: 'CLUSTER',},},};Adding a fourth modified field at a new depth means re-deriving the spread chain.
The composer collapses both forms to two lines:
policyComposer('seeding')
.extend(POLICY_SEEDING_USTA_DEFAULT)
.set('policyName', 'CTS SEEDING')
.set('seedingProfile.positioning', 'CLUSTER')
.build();
API
policyComposer(policyType?) returns a PolicyComposer with the methods below. Every method that modifies state returns a NEW composer; the original is never mutated.
extend(other)
Deep-merges another policy onto the current accumulator. Accepts either the wrapped form ({[policyType]: ...}) or the raw inner shape — the composer detects which.
policyComposer('seeding').extend(POLICY_SEEDING_USTA_DEFAULT); // wrapped
policyComposer('seeding').extend({ policyName: 'X', ... }); // raw
Arrays REPLACE on merge (matches deepMerge's default). The later extend() wins on conflicting paths.
set(path, value)
Replace the value at a dot-path. Walks intermediate objects, creating them as plain objects (or arrays, when the next segment parses as a non-negative integer) when missing.
.set('policyName', 'CTS SEEDING')
.set('seedingProfile.positioning', 'CLUSTER')
.set('seedingProfile.drawTypes.ROUND_ROBIN.positioning', 'WATERFALL')
.set('seedsCountThresholds.0.seedsCount', 3) // arrays via numeric segment
merge(path, fragment)
Deep-merge a fragment at a dot-path. Equivalent to set(path, deepMerge(get(path), fragment)). Useful when you want to add several keys to a deeper sub-object without re-spreading.
.merge('seedingProfile.drawTypes', {
SINGLE_ELIMINATION: { positioning: 'CLUSTER' },
COMPASS: { positioning: 'CLUSTER' },
})
unset(path)
Remove the key at a dot-path. Array elements are spliced (subsequent indices shift). No-op when the path doesn't exist.
.unset('seedingProfile.positioning')
.unset('seedsCountThresholds.1') // remove the second threshold
get(path)
Read the current value at a dot-path. Returns undefined for missing paths; never throws. Useful for assertions in tests or for "modify based on current" patterns (read, transform, set back).
const composer = policyComposer('seeding').extend(POLICY_SEEDING_USTA_DEFAULT);
composer.get('policyName'); // 'USTA SEEDING'
composer.get('seedingProfile.positioning'); // 'SEPARATE'
from({ name, version? })
Start from a named entry in the policyRegistry. Throws if the entry doesn't exist (a typo in the federation name surfaces immediately rather than silently giving an empty composer).
policyRegistry.register({ policyType: 'seeding', name: 'USTA_DEFAULT', definition: usta });
policyComposer('seeding').from({ name: 'USTA_DEFAULT' }).set('policyName', 'CTS SEEDING').build();
Requires the composer to be scoped to a policyType.
build()
Finalize. Returns { [policyType]: definition } when the composer was constructed with a policyType; returns the raw definition otherwise.
const policyDefinitions = policyComposer('seeding').extend(stock).build();
// -> { seeding: { validSeedPositions: ..., ... } }
engine.getSeedsCount({ policyDefinitions, drawSize: 32 });
register({ name, version? })
Build AND register the result into policyRegistry in a single call. Returns the wrapped value (same shape as build()), handy for inline use.
const ctsSeeding = policyComposer('seeding')
.extend(POLICY_SEEDING_USTA_DEFAULT)
.set('seedingProfile.positioning', 'CLUSTER')
.register({ name: 'CTS_SEEDING', version: '2026' });
// Later, from anywhere:
const looked = policyRegistry.lookup({ policyType: 'seeding', name: 'CTS_SEEDING', version: '2026' });
Worked examples
Federation override of a stock policy
const stock = POLICY_SEEDING_USTA_DEFAULT;
// CTS — change positioning + name
const cts = policyComposer('seeding')
.extend(stock)
.set('policyName', 'CTS SEEDING')
.set('seedingProfile.positioning', 'CLUSTER')
.build();
// CZE-junior — CTS + override one threshold
const czeJunior = policyComposer('seeding')
.extend(cts)
.set('policyName', 'CZE JUNIOR')
.set('seedsCountThresholds.3.seedsCount', 12)
.build();
Compose a fragment then merge into a larger structure
The untyped composer (no policyType) is useful when assembling a fragment that will be merged into a deeper structure later.
const drawTypesFragment = policyComposer()
.set('ROUND_ROBIN.positioning', 'WATERFALL')
.set('ROUND_ROBIN_WITH_PLAYOFF.positioning', 'WATERFALL')
.build();
const full = policyComposer('seeding')
.extend(POLICY_SEEDING_USTA_DEFAULT)
.merge('seedingProfile.drawTypes', drawTypesFragment)
.build();
Semantics
- Immutability. Every modifier returns a new composer. Safe to branch from a common base.
- No mutation of inputs.
extend()does not modify the policy passed in;set()/merge()/unset()do not modify the composer's previous internal state. - Arrays replace by default. Matches
deepMerge. For path-targeted "merge by key" array behavior, do the read-modify-set in the consumer (composer.get(path)to read, thencomposer.set(path, merged)to write). - Dot-path with numeric segments for arrays.
seedsCountThresholds.0.drawSizeindexes into an array. build()re-wraps withpolicyType. A composer scoped toseedingreturns{ seeding: {...} }; a composer with nopolicyTypereturns the raw inner.