Functional Weave
Code in TypeScript

construction.retention

Retention held and released on a construction valuation: full rate, half at practical completion, none at the end.

1.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 17 tests, run in TypeScript, Python and Rust.

What it does

Retention is the part of each interim payment a client (or main contractor) keeps back as security until the work is complete and any defects are put right. This works out the retention on one valuation.

## Shape

For example

  • retention(£100,000.00, 5%, interim) → full £5,000.00, held £5,000.00, released £0.00 5% on 100,000.00 before practical completion: all 5,000.00 held
  • retention(£100,000.00, 5%, practical-completion) → full £5,000.00, held £2,500.00, released £2,500.00 at practical completion half is released
  • retention(£100,000.00, 5%, final) → full £5,000.00, held £0.00, released £5,000.00 after the defects are made good the rest is released

The function

The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.

export function retention(cumulativeValue: Money, basisPoints: number, stage: RetentionStage): Retention
cumulativeValueMoneygross value certified to date: work done plus materials on site
basisPointsintthe retention percentage, 500 = 5%, 300 = 3%
stageRetentionStageinterim before practical completion, practical-completion until the defects are made good, final after
returnsRetention

The types it declares, generated into your project

export type RetentionStage = "interim" | "practical-completion" | "final";

/** The retention on one valuation, and how much of it has been let go. */
export interface Retention {
  /** the full percentage on the cumulative value */
  readonly full: Money;
  /** what this valuation keeps back */
  readonly held: Money;
  /** full less held: what has been released so far */
  readonly released: Money;
}

Your code names it in one line, in the file that uses it

import { retention } from "#fune/construction.retention@^1";
impl/typescript.ts · 32 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import { roundDiv } from "./math_round_div.ts";  ← from math.round-div ^1.0.0 · built alongside by fune
import { type Money, money } from "./money_amount.ts";  ← from money.amount ^1.0.0 · built alongside by fune
import { type Retention, type RetentionStage } from "./construction_retention_types.ts";

/**
 * Retention on a valuation, the way JCT-style contracts release it.
 *
 * Retention is always worked out afresh on the cumulative value, never
 * accumulated valuation by valuation, so a later valuation corrects an earlier
 * one. At practical completion half the percentage applies (not half of a
 * figure rounded earlier), and after the defects are made good none does.
 */
export function retention(cumulativeValue: Money, basisPoints: number, stage: RetentionStage): Retention {
  if (!Number.isInteger(basisPoints) || basisPoints < 0 || basisPoints > 10000) {
    throw new RangeError(`basisPoints must be a whole number from 0 to 10000, received ${basisPoints}`);
  }
  if (cumulativeValue.minor < 0) {
    throw new RangeError(`cumulativeValue must not be negative, received ${cumulativeValue.minor}`);
  }
  const product = cumulativeValue.minor * basisPoints;
  if (!Number.isSafeInteger(product)) {
    throw new RangeError("the retention calculation exceeds 2^53 - 1");
  }
  const full = roundDiv(product, 10000, "half-up");
  let held: number;
  if (stage === "interim") held = full;
  else if (stage === "practical-completion") held = roundDiv(product, 20000, "half-up");
  else if (stage === "final") held = 0;
  else throw new RangeError(`unknown retention stage "${stage}"`);
  const currency = cumulativeValue.currency;
  return { full: money(full, currency), held: money(held, currency), released: money(full - held, currency) };
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:

fune add construction.retention
Download for TypeScript construction.retention-1.0.0-typescript.fune · 13,541 bytes sha256 875a786bfe00611ac7da10056ed9a86fa432110b016cd54048288289642c1b1d

The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./construction.retention-1.0.0-typescript.fune, or fetch it from a terminal with fune pull construction.retention@1.0.0:typescript.

The whole function, every language, is one file too: construction.retention-1.0.0.fune, 17,797 bytes, sha256 3af86f923a327011e14fe345cb3d8f3ecec00bc9d184e6f288c24ed4a47f2132. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

// fune: before construction.retention

after — your function gets the result and the arguments, and returns the final result.

// fune: after construction.retention

replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.

// fune: replace math.round-div in construction.retention
// fune: replace money.amount in construction.retention

step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show construction.retention --steps.

// fune: step construction.retention after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

CaseArgumentsExpected
5% on 100,000.00 before practical completion: all 5,000.00 held £100,000.00, 5%, interim → full £5,000.00, held £5,000.00, released £0.00
at practical completion half is released £100,000.00, 5%, practical-completion → full £5,000.00, held £2,500.00, released £2,500.00
after the defects are made good the rest is released £100,000.00, 5%, final → full £5,000.00, held £0.00, released £5,000.00
3% on 12,345.67 is 370.37... so 370.37 held £12,345.67, 3%, interim → full £370.37, held £370.37, released £0.00
3% on 12,345.67 at practical completion: 1.5% is 185.185... so 185.19 held £12,345.67, 3%, practical-completion → full £370.37, held £185.19, released £185.18
half the percentage, not half the rounded retention: 5% of 10,000.10 is 500.005 (500.01), 2.5% is 250.0025 (250.00) £10,000.10, 5%, practical-completion → full £500.01, held £250.00, released £250.01
the full retention on the same value rounds half up £10,000.10, 5%, interim → full £500.01, held £500.01, released £0.00
no retention agreed £100,000.00, 0%, interim → full £0.00, held £0.00, released £0.00
nothing certified yet £0.00, 5%, interim → full £0.00, held £0.00, released £0.00
100% retention holds the whole value £123.45, 100%, interim → full £123.45, held £123.45, released £0.00
Show the other 7 tests
CaseArgumentsExpected
the currency is carried through €7,777.00, 3%, practical-completion → full €233.31, held €116.66, released €116.65
a negative value -£0.01, 5%, interim → error: cumulativeValue must not be negative
a percentage above 100 £1.00, 100.01%, interim → error: basisPoints must be a whole number from 0 to 10000
a negative percentage £1.00, -0.01%, interim → error: basisPoints must be a whole number from 0 to 10000
a fractional percentage £1.00, 0.025%, interim → error: basisPoints must be a whole number from 0 to 10000
an unknown stage £1.00, 5%, retained → error: unknown retention stage "retained"
a value too large to multiply exactly £10,000,000,000.00, 100%, interim → error: exceeds 2^53 - 1

More from the author

- **Work it out on the cumulative value, every time.** Valuations are cumulative (everything done to date), and so is retention: pass the gross value certified to date (work done plus materials on site), not this month's increase. A later valuation then corrects an earlier one automatically, and `construction.valuation` takes the previous payments off the net figure. - **The usual release pattern.** Before practical completion the full percentage is held (`interim`). At practical completion half of it is released: the retention becomes **half the percentage on the value** (`practical-completion`), which is how JCT contracts word it. After the end of the rectification (defects) period, when the defects are made good, the rest is released (`final`, nothing held). - `released` is `full - held`, the total released so far, not the release in this valuation.

## Rounding

Each figure is one rounding of an exact product, half up to the minor unit: `full` is value x percentage, `held` at practical completion is value x percentage / 2. Halving the already rounded full retention can differ by a penny: 5% of 10,000.10 is 500.005 (500.01) and 2.5% is 250.0025 (250.00), where half of 500.01 would round to 250.01.

## Edges and limits

Percentages are basis points from 0 to 10000; the common figures are 3% and 5%. Negative values are refused (a valuation below the previous one is still a positive cumulative value). Contracts that cap retention at a fixed sum, or release it in more than two parts, are not modelled: compute the cap in the caller, or call this with the stage that applies. value x percentage must stay within 2^53 - 1.

Source: the JCT Standard Building Contract (2016 edition, section 4, rules on retention: the Retention Percentage, 3% unless stated, halved after practical completion). The contract text is not freely published, so this was not checked against it; the percentage and stages are whatever your contract states.

Files

PathBytes
README.md2,218
impl/python.py1,726
impl/rust.rs2,358
impl/typescript.ts1,650
vectors.json6,213