Functional Weave
Code in Python

net.device-status

Classify a device as up, degraded or down from its latency summary and loss, p95 and jitter thresholds.

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

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

What it does

Decides whether a monitored device is **up**, **degraded** or **down** from its `net.latency-summary`, and says why, so an alert and a dashboard never disagree about what "degraded" means.

## Rules, in order

For example

  • device_status(sent 4, received 4, lost 0, loss basis points 0%, min us 10,000, avg us 11,500, max us 13,000, p95 us 13,000, jitter us 1,667, degraded loss basis points 10%, down loss basis poin…) → status up, reasons a healthy link is up with no reasons
  • device_status(sent 3, received 0, lost 3, loss basis points 100%, min us —, avg us —, max us —, p95 us —, jitter us —, degraded loss basis points 10%, down loss basis points 50%, degraded p95 u…) → status down, reasons no replies: 3 of 3 probes lost no replies at all is down
  • device_status(sent 3, received 1, lost 2, loss basis points 66.67%, min us 700, avg us 700, max us 700, p95 us 700, jitter us —, degraded loss basis points 10%, down loss basis points 50%, degr…) → status down, reasons loss 66.67% at or above the down threshold 50.00% loss above the down threshold is down

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.

def device_status(summary: LatencySummary, thresholds: StatusThresholds) -> StatusResult
summaryLatencySummary
thresholdsStatusThresholds
returnsStatusResult

The types it declares, generated into your project

@dataclass(frozen=True)
class StatusThresholds:
    """When a device stops counting as healthy."""

    #: loss at or above this is degraded, at least 1
    degraded_loss_basis_points: int
    #: loss at or above this is down, up to 10000
    down_loss_basis_points: int
    #: p95 at or above this many microseconds is degraded
    degraded_p95_us: int
    #: jitter at or above this is degraded; null to ignore jitter
    degraded_jitter_us: Optional[int]

DeviceStatus = Literal["up", "degraded", "down"]

@dataclass(frozen=True)
class StatusResult:
    """The status and every reason for it, in a fixed order."""

    status: DeviceStatus
    #: empty when up
    reasons: List[str]

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

from fune.net.device_status import device_status  # net.device-status@^1
impl/python.py · 55 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.

from typing import List

from .net_latency_summary import LatencySummary  ← from net.latency-summary ^1.0.0 · built alongside by fune
from .net_device_status_types import StatusResult, StatusThresholds


# Integer arithmetic only, so the reason text is identical in every language.
def _percent(basis_points: int) -> str:
    return "%d.%02d%%" % (basis_points // 100, basis_points % 100)


def _ms(us: int) -> str:
    return "%d.%03d ms" % (us // 1000, us % 1000)


def _whole(value: object) -> bool:
    return isinstance(value, int) and not isinstance(value, bool)


def device_status(summary: LatencySummary, thresholds: StatusThresholds) -> StatusResult:
    """Up, degraded or down, and every reason, from one latency summary.

    Down wins outright: once a device is down, its p95 and jitter are beside
    the point. Otherwise every threshold crossed is listed, loss first, so an
    alert says all that is wrong at once.
    """
    sent, received, lost = summary.sent, summary.received, summary.lost
    if sent < 1 or received < 0 or lost < 0 or received + lost != sent:
        raise ValueError("summary is inconsistent: sent %d, received %d, lost %d" % (sent, received, lost))
    deg_loss = thresholds.degraded_loss_basis_points
    down_loss = thresholds.down_loss_basis_points
    p95_limit = thresholds.degraded_p95_us
    jitter_limit = thresholds.degraded_jitter_us
    if not _whole(down_loss) or down_loss < 1 or down_loss > 10000:
        raise ValueError("downLossBasisPoints must be 1 to 10000, received %r" % (down_loss,))
    if not _whole(deg_loss) or deg_loss < 1 or deg_loss > down_loss:
        raise ValueError("degradedLossBasisPoints must be 1 to downLossBasisPoints (%d), received %r" % (down_loss, deg_loss))
    if not _whole(p95_limit) or p95_limit < 1:
        raise ValueError("degradedP95Us must be at least 1, received %r" % (p95_limit,))
    if jitter_limit is not None and (not _whole(jitter_limit) or jitter_limit < 1):
        raise ValueError("degradedJitterUs must be null or at least 1, received %r" % (jitter_limit,))

    if received == 0:
        return StatusResult(status="down", reasons=["no replies: %d of %d probes lost" % (lost, sent)])
    loss = summary.loss_basis_points
    if loss >= down_loss:
        return StatusResult(status="down", reasons=["loss %s at or above the down threshold %s" % (_percent(loss), _percent(down_loss))])
    reasons: List[str] = []
    if loss >= deg_loss:
        reasons.append("loss %s at or above %s" % (_percent(loss), _percent(deg_loss)))
    if summary.p95_us is not None and summary.p95_us >= p95_limit:
        reasons.append("p95 %s at or above %s" % (_ms(summary.p95_us), _ms(p95_limit)))
    if jitter_limit is not None and summary.jitter_us is not None and summary.jitter_us >= jitter_limit:
        reasons.append("jitter %s at or above %s" % (_ms(summary.jitter_us), _ms(jitter_limit)))
    return StatusResult(status="degraded" if reasons else "up", reasons=reasons)

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the Python 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 net.device-status
Download for Python net.device-status-1.0.0-python.fune · 16,738 bytes sha256 141baff8496e7ba64243c32a75df77d9a3be5ca6c9380f64b7505438b462a3d3

The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./net.device-status-1.0.0-python.fune, or fetch it from a terminal with fune pull net.device-status@1.0.0:python.

The whole function, every language, is one file too: net.device-status-1.0.0.fune, 23,784 bytes, sha256 7d524303a5558a52cdbb5aa7c08d00a4663572dfad2cd19a7e4a2a98f96e7611. 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 net.device-status

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

# fune: after net.device-status

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 net.latency-summary in net.device-status

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 net.device-status --steps.

# fune: step net.device-status 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
a healthy link is up with no reasons sent 4, received 4, lost 0, loss basis points 0%, min us 10,000, avg us 11,500, max us 13,000, p95 us 13,000, jitter us 1,667, degraded loss basis points 10%, down loss basis poin… → status up, reasons
no replies at all is down sent 3, received 0, lost 3, loss basis points 100%, min us —, avg us —, max us —, p95 us —, jitter us —, degraded loss basis points 10%, down loss basis points 50%, degraded p95 u… → status down, reasons no replies: 3 of 3 probes lost
loss above the down threshold is down sent 3, received 1, lost 2, loss basis points 66.67%, min us 700, avg us 700, max us 700, p95 us 700, jitter us —, degraded loss basis points 10%, down loss basis points 50%, degr… → status down, reasons loss 66.67% at or above the down threshold 50.00%
loss exactly at the down threshold is down sent 4, received 2, lost 2, loss basis points 50%, min us 10, avg us 15, max us 20, p95 us 20, jitter us 10, degraded loss basis points 10%, down loss basis points 50%, degraded p… → status down, reasons loss 50.00% at or above the down threshold 50.00%
a quarter lost is degraded sent 4, received 3, lost 1, loss basis points 25%, min us 1,000, avg us 2,000, max us 3,000, p95 us 3,000, jitter us 1,500, degraded loss basis points 10%, down loss basis points … → status degraded, reasons loss 25.00% at or above 10.00%
loss exactly at the degraded threshold is degraded sent 10, received 9, lost 1, loss basis points 10%, min us 1,000, avg us 1,000, max us 1,000, p95 us 1,000, jitter us 0, degraded loss basis points 10%, down loss basis points 50%… → status degraded, reasons loss 10.00% at or above 10.00%
loss just under the degraded threshold is up sent 11, received 10, lost 1, loss basis points 9.09%, min us 1,000, avg us 1,000, max us 1,000, p95 us 1,000, jitter us 0, degraded loss basis points 10%, down loss basis points … → status up, reasons
p95 exactly at the threshold is degraded sent 4, received 4, lost 0, loss basis points 0%, min us 50,000, avg us 70,000, max us 100,000, p95 us 100,000, jitter us 10,000, degraded loss basis points 10%, down loss basis p… → status degraded, reasons p95 100.000 ms at or above 100.000 ms
loss, p95 and jitter reasons are all listed, in that order sent 4, received 3, lost 1, loss basis points 25%, min us 90,000, avg us 120,000, max us 152,300, p95 us 152,300, jitter us 25,000, degraded loss basis points 10%, down loss basis… → status degraded, reasons loss 25.00% at or above 10.00%, p95 152.300 ms at or above 100.000 ms, jitter 25.000 ms at or above 20.000 ms
a null jitter threshold ignores jitter sent 4, received 4, lost 0, loss basis points 0%, min us 1,000, avg us 30,000, max us 60,000, p95 us 60,000, jitter us 50,000, degraded loss basis points 10%, down loss basis poin… → status up, reasons
Show the other 12 tests
CaseArgumentsExpected
a single reply has no jitter to judge sent 1, received 1, lost 0, loss basis points 0%, min us 7, avg us 7, max us 7, p95 us 7, jitter us —, degraded loss basis points 10%, down loss basis points 50%, degraded p95 us … → status up, reasons
a 5 bp loss prints as 0.05% sent 2,000, received 1,999, lost 1, loss basis points 0.05%, min us 100, avg us 100, max us 100, p95 us 100, jitter us 0, degraded loss basis points 0.05%, down loss basis points … → status degraded, reasons loss 0.05% at or above 0.05%
sub-millisecond latency prints with three places sent 2, received 2, lost 0, loss basis points 0%, min us 7, avg us 7, max us 7, p95 us 7, jitter us 0, degraded loss basis points 10%, down loss basis points 50%, degraded p95 us … → status degraded, reasons p95 0.007 ms at or above 0.007 ms
no replies is down even when the down threshold is 100% sent 1, received 0, lost 1, loss basis points 100%, min us —, avg us —, max us —, p95 us —, jitter us —, degraded loss basis points 10%, down loss basis points 100%, degraded p95 … → status down, reasons no replies: 1 of 1 probes lost
received plus lost must equal sent sent 3, received 2, lost 2, loss basis points 66.67%, min us 1, avg us 1, max us 1, p95 us 1, jitter us 0, degraded loss basis points 10%, down loss basis points 50%, degraded p95… → error: summary is inconsistent
a summary of no probes is an error sent 0, received 0, lost 0, loss basis points 0%, min us —, avg us —, max us —, p95 us —, jitter us —, degraded loss basis points 10%, down loss basis points 50%, degraded p95 us … → error: summary is inconsistent
a zero down threshold is an error sent 4, received 4, lost 0, loss basis points 0%, min us 10,000, avg us 11,500, max us 13,000, p95 us 13,000, jitter us 1,667, degraded loss basis points 10%, down loss basis poin… → error: downLossBasisPoints must be 1 to 10000
a down threshold over 100% is an error sent 4, received 4, lost 0, loss basis points 0%, min us 10,000, avg us 11,500, max us 13,000, p95 us 13,000, jitter us 1,667, degraded loss basis points 10%, down loss basis poin… → error: downLossBasisPoints must be 1 to 10000
a degraded threshold above the down threshold is an error sent 4, received 4, lost 0, loss basis points 0%, min us 10,000, avg us 11,500, max us 13,000, p95 us 13,000, jitter us 1,667, degraded loss basis points 60%, down loss basis poin… → error: degradedLossBasisPoints must be 1 to downLossBasisPoints
a zero degraded loss threshold is an error sent 4, received 4, lost 0, loss basis points 0%, min us 10,000, avg us 11,500, max us 13,000, p95 us 13,000, jitter us 1,667, degraded loss basis points 0%, down loss basis point… → error: degradedLossBasisPoints must be 1 to downLossBasisPoints
a zero p95 threshold is an error sent 4, received 4, lost 0, loss basis points 0%, min us 10,000, avg us 11,500, max us 13,000, p95 us 13,000, jitter us 1,667, degraded loss basis points 10%, down loss basis poin… → error: degradedP95Us must be at least 1
a zero jitter threshold is an error sent 4, received 4, lost 0, loss basis points 0%, min us 10,000, avg us 11,500, max us 13,000, p95 us 13,000, jitter us 1,667, degraded loss basis points 10%, down loss basis poin… → error: degradedJitterUs must be null or at least 1

More from the author

1. No replies at all: **down**, `no replies: 3 of 3 probes lost`. 2. Loss at or above `downLossBasisPoints`: **down**, `loss 66.67% at or above the down threshold 50.00%`. Down wins outright; p95 and jitter are not judged. 3. Otherwise each threshold crossed adds a reason, in this order, and any reason makes it **degraded**: - loss at or above `degradedLossBasisPoints`: `loss 25.00% at or above 10.00%` - p95 at or above `degradedP95Us`: `p95 152.300 ms at or above 100.000 ms` - jitter at or above `degradedJitterUs` (skipped when the threshold or the jitter is null): `jitter 25.000 ms at or above 20.000 ms` 4. No reasons: **up**, with an empty list.

Every comparison is "at or above", so a threshold is the first value that counts as bad. Percentages are printed from basis points with two places and times from microseconds as milliseconds with three places, using integer arithmetic only, so the text is identical in every language.

## Errors

- `summary is inconsistent` when `sent` is under 1 or `received + lost` differs from `sent`. - `downLossBasisPoints must be 1 to 10000` - `degradedLossBasisPoints must be 1 to downLossBasisPoints` (a degraded threshold of 0 would make every device degraded) - `degradedP95Us must be at least 1` - `degradedJitterUs must be null or at least 1`

Files

PathBytes
README.md1,556
impl/python.py2,915
impl/rust.rs3,980
impl/typescript.ts2,811
vectors.json8,602