Functional Weave
Code in Rust

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.

pub fn retention(cumulative_value: &Money, basis_points: i64, stage: &str) -> Retention
cumulative_valueMoneygross value certified to date: work done plus materials on site
basis_pointsintthe 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

// RetentionStage is a string in Rust, one of: "interim", "practical-completion", "final".
// Parameters take it as &str and results hold it as String.

/// The retention on one valuation, and how much of it has been let go.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Retention {
    /// the full percentage on the cumulative value
    pub full: Money,
    /// what this valuation keeps back
    pub held: Money,
    /// full less held: what has been released so far
    pub released: Money,
}

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

fune!(construction.retention@^1);  // then call retention(…)
impl/rust.rs · 62 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.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::math_round_div::round_div;  ← from math.round-div ^1.0.0 · built alongside by fune
use super::money_amount::{money, money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune

const MAX_SAFE: i128 = 9_007_199_254_740_991;

/// 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.
///
/// # Panics
/// Panics on a percentage outside 0..=10000, a negative value, an unknown
/// stage, or a product beyond 2^53 - 1.
pub fn retention(cumulative_value: &Money, basis_points: i64, stage: &str) -> Retention {
    if !(0..=10000).contains(&basis_points) {
        panic!("basisPoints must be a whole number from 0 to 10000, received {}", basis_points);
    }
    if cumulative_value.minor < 0 {
        panic!("cumulativeValue must not be negative, received {}", cumulative_value.minor);
    }
    let product = cumulative_value.minor as i128 * basis_points as i128;
    // i64 could go further, but TypeScript cannot, and all three must agree.
    if product > MAX_SAFE {
        panic!("the retention calculation exceeds 2^53 - 1");
    }
    let product = product as i64;
    let full = round_div(product, 10000, "half-up");
    let held = match stage {
        "interim" => full,
        "practical-completion" => round_div(product, 20000, "half-up"),
        "final" => 0,
        other => panic!("unknown retention stage \"{}\"", other),
    };
    let currency = &cumulative_value.currency;
    Retention {
        full: money(full, currency),
        held: money(held, currency),
        released: money(full - held, currency),
    }
}

pub fn retention_to_value(r: &Retention) -> Value {
    Value::obj(vec![
        ("full", money_to_value(&r.full)),
        ("held", money_to_value(&r.held)),
        ("released", money_to_value(&r.released)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    if let Value::Float(f) = &args[1] {
        panic!("basisPoints must be a whole number from 0 to 10000, received {}", f);
    }
    retention_to_value(&retention(
        &money_from_value(&args[0]),
        args[1].as_i64(),
        args[2].as_str(),
    ))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the Rust 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. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:

fune add construction.retention
Download for Rust construction.retention-1.0.0-rust.fune · 14,277 bytes sha256 ab4783fe413178b422ac942047ef1b046d16f009ba188dba26cbbdf0a4568a77

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

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