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 heldretention(£100,000.00, 5%, practical-completion)→ full £5,000.00, held £2,500.00, released £2,500.00 at practical completion half is releasedretention(£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
| cumulativeValue | Money | gross value certified to date: work done plus materials on site |
| basisPoints | int | the retention percentage, 500 = 5%, 300 = 3% |
| stage | RetentionStage | interim before practical completion, practical-completion until the defects are made good, final after |
| returns | Retention |
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";
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
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.
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Path | Bytes |
|---|---|
| README.md | 2,218 |
| impl/python.py | 1,726 |
| impl/rust.rs | 2,358 |
| impl/typescript.ts | 1,650 |
| vectors.json | 6,213 |