sandbox-meter + jcp-gateway: count, and flag, work services did on the tree's behalf #214

Merged
dcharlot merged 14 commits from feat/tree-delegated-energy into main 2026-10-11 05:08:35 -04:00
Owner

Why

The process-tree meter (#198) reads a server's own CPU energy. On this kernel, work a system service does on a process's behalf is billed to that process in fields any same-user process can read:

  • ri_billed_energy and ri_serviced_energy, in nJ;
  • ri_billed_system_time and ri_serviced_system_time, in mach ticks.

The bill is not part of the process's own count. A probe measured:

  • Controls: billed exactly 0 in every control run.
  • Delegating clients: billed 4–45× their own energy for an on-device model client and about 200× for Spotlight. Billing is conserved system-wide.
  • A lower bound: it carries none of the service's ANE or GPU energy, credits land late, and some work is never billed.

Until now that delegated work was invisible, and only the operator's "self-contained" declaration stood against it.

What

  • sandbox-meter TreeMeter.
    • The figure is each member's advance of (own − serviced + billed), summed signed. Work a member did for others is subtracted, work others did for it is added, and work one member does for another is counted once.
    • New gaps:
      • Delegated (any bill moved);
      • Serviced (any serviced count moved);
      • ServicedBeyondSpent (a member's net went negative).
    • Direction of error: delegated, serviced and unsettled-at-start are the gaps that can put a figure over the call's energy as well as under it, for example a late bill for earlier work landing in this span. The docs say so.
    • The stop waits briefly for bills. When a bill moved, the stop re-reads the billed counts until they hold (5 ms polls, bounded). A call with no bill pays nothing.
  • jcp-gateway.
    • A delegating server's call is Partial even when declared self-contained: evidence overrides the declaration. The declaration still gates Whole when no bill arrives.
    • gap_kind has no catch-all, so a new gap kind fails to compile until it is named.
    • The flow-label scan (Phase 0) is metered on this process's own threads.
  • In-process meter (detect_in_process).
    • The defect: a process reading its own count does not fold the reading thread's energy, so a short span read as nothing, or as energy from before it.
    • The fix: each self-read now sleeps (50 µs → 5 ms) until the reading thread's own energy count has moved across a sleep. If it never moves, the span is an estimate, never a short figure graded as a reading.
    • Private flavour: the per-thread count is proc_pidinfo flavour PROC_PIDTHREADCOUNTS, which the SDK exposes only in a private header. The probe is built only where it reads in the expected layout, and a host where it is absent gets estimates only.
  • Exit loses the last slice. A process's exit folds its CPU time but not reliably its energy (measured), so child_reading / Attribution::child_energy_uj are documented as lower bounds.
  • joule-meter meters its own workload in-process.
  • jcp SPEC §5.1 rung 3 says what delegated work a tree figure holds and which gaps can over-count. It also says the in-process rung is Whole only for work that hands nothing to another process, and that a self-read MUST check its fold. The sandbox README says the same.

Evidence

  • Delegation, end to end. A server compiled at test time makes directory-service lookups. The Delegated gap fires, the bill is at least the oracle's, and figure minus bill equals the oracle's own advance to the nanojoule. Pure-CPU calls on the same server read whole and exact. In the gateway, a declared server's delegating calls are sealed HwShunt + Partial with ["delegated"].
  • In-process fold. The short-span test had failed 2 of 140 runs under load. It passed 150/150 at load 70–145 and 160/160 at load 146–305 on the final code. Its oracle folds independently.
  • Review and mutation checks. An independent review found 6 defects, all fixed:
    • the unverified fold;
    • over-count direction undocumented;
    • silent serviced credits;
    • the catch-all;
    • an uncited figure;
    • an in-process doc gap.
      Ten mutation checks each broke their target test.
  • Suites, on a rebase onto main with #211–#213:
    • sandbox-meter + sandbox-proof: 210 passed.
    • jcp-gateway: 42 passed.
    • proof-solvers: 30 passed. z3 decisions still read whole.
    • joule-code-remediate, jouledesk-github and l3-jouleclaw compile.
    • Linux and x86_64-macOS cross-checks are clean.

Not covered

  • The bill is a lower bound. It holds none of the service's ANE or GPU energy and misses unbilled work.
  • Stop wait barely measurable. On the services tested here, the wait moved late bills only within noise.
  • No delegation detection in the in-process meter. The rung documents the limit, because Measurement has no coverage field to carry it.

🤖 Generated with Claude Code

## Why The process-tree meter (#198) reads a server's own CPU energy. On this kernel, work a **system service** does on a process's behalf is billed to that process in fields any same-user process can read: - `ri_billed_energy` and `ri_serviced_energy`, in nJ; - `ri_billed_system_time` and `ri_serviced_system_time`, in mach ticks. The bill is not part of the process's own count. A probe measured: - **Controls:** billed exactly 0 in every control run. - **Delegating clients:** billed 4–45× their own energy for an on-device model client and about 200× for Spotlight. Billing is conserved system-wide. - **A lower bound:** it carries none of the service's ANE or GPU energy, credits land late, and some work is never billed. Until now that delegated work was invisible, and only the operator's "self-contained" declaration stood against it. ## What - **sandbox-meter `TreeMeter`.** - **The figure** is each member's advance of (own − serviced + billed), summed signed. Work a member did for others is subtracted, work others did for it is added, and work one member does for another is counted once. - **New gaps:** - `Delegated` (any bill moved); - `Serviced` (any serviced count moved); - `ServicedBeyondSpent` (a member's net went negative). - **Direction of error:** `delegated`, `serviced` and `unsettled-at-start` are the gaps that can put a figure **over** the call's energy as well as under it, for example a late bill for earlier work landing in this span. The docs say so. - **The stop waits briefly for bills.** When a bill moved, the stop re-reads the billed counts until they hold (5 ms polls, bounded). A call with no bill pays nothing. - **jcp-gateway.** - A delegating server's call is `Partial` **even when declared self-contained**: evidence overrides the declaration. The declaration still gates `Whole` when no bill arrives. - `gap_kind` has no catch-all, so a new gap kind fails to compile until it is named. - The flow-label scan (Phase 0) is metered on this process's own threads. - **In-process meter (`detect_in_process`).** - **The defect:** a process reading its own count does not fold the reading thread's energy, so a short span read as nothing, or as energy from before it. - **The fix:** each self-read now sleeps (50 µs → 5 ms) until the reading thread's own energy count has moved across a sleep. If it never moves, the span is an estimate, never a short figure graded as a reading. - **Private flavour:** the per-thread count is `proc_pidinfo` flavour `PROC_PIDTHREADCOUNTS`, which the SDK exposes only in a **private** header. The probe is built only where it reads in the expected layout, and a host where it is absent gets estimates only. - **Exit loses the last slice.** A process's exit folds its CPU time but not reliably its energy (measured), so `child_reading` / `Attribution::child_energy_uj` are documented as lower bounds. - **`joule-meter`** meters its own workload in-process. - **jcp SPEC §5.1 rung 3** says what delegated work a tree figure holds and which gaps can over-count. It also says the in-process rung is `Whole` only for work that hands nothing to another process, and that a self-read MUST check its fold. The sandbox README says the same. ## Evidence - **Delegation, end to end.** A server compiled at test time makes directory-service lookups. The `Delegated` gap fires, the bill is at least the oracle's, and figure minus bill equals the oracle's own advance to the nanojoule. Pure-CPU calls on the same server read whole and exact. In the gateway, a declared server's delegating calls are sealed `HwShunt` + `Partial` with `["delegated"]`. - **In-process fold.** The short-span test had failed 2 of 140 runs under load. It passed 150/150 at load 70–145 and 160/160 at load 146–305 on the final code. Its oracle folds independently. - **Review and mutation checks.** An independent review found 6 defects, all fixed: - the unverified fold; - over-count direction undocumented; - silent serviced credits; - the catch-all; - an uncited figure; - an in-process doc gap. Ten mutation checks each broke their target test. - **Suites, on a rebase onto main with #211–#213:** - sandbox-meter + sandbox-proof: 210 passed. - jcp-gateway: 42 passed. - proof-solvers: 30 passed. z3 decisions still read whole. - joule-code-remediate, jouledesk-github and l3-jouleclaw compile. - Linux and x86_64-macOS cross-checks are clean. ## Not covered - **The bill is a lower bound.** It holds none of the service's ANE or GPU energy and misses unbilled work. - **Stop wait barely measurable.** On the services tested here, the wait moved late bills only within noise. - **No delegation detection in the in-process meter.** The rung documents the limit, because `Measurement` has no coverage field to carry it. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The kernel's bank ledgers bill CPU work a system service does on a
process's behalf to that process (ri_billed_energy / ri_billed_system_time),
and credit the servicer (ri_serviced_*). Measured on this M5 Max: exactly 0
in every CPU-only control, 4-45x a client's own energy for on-device model
inference, ~200x for a metadata search, ~2.4x for speech. The tree meter
read none of it, so a server that delegated its work read a plausible own
share with nothing to show the rest.

Each member's count is now ri_energy_nj - ri_serviced_energy +
ri_billed_energy, its advances summed signed: work a member did for others
is theirs, work others did for it is the tree's, and service between two
members is counted once, through the payer. A member whose net goes below
zero (a credit for earlier service landing in the span) is counted as it is
and is a gap, TreeGap::ServicedBeyondSpent, never a wrapped u64; a sum not
above zero is no reading. Any billed energy or CPU time in the span is
TreeGap::Delegated { billed_nj, billed_cpu }: the bill is IN the figure and
a lower bound of the delegated work (no GPU or Neural Engine energy, no
unbilled threads, no late credits, nothing billed to another originator).

The billed counts take no part in settling or in confirming a walk: another
process's thread credits them, whatever the member is doing, up to a second
and more late, so waiting on them would stretch every span without making a
figure whole. The serviced counts stay in settling: the member's own thread
moves them. A CPU-only tree's figure is unchanged (its ledgers are 0).

Tests: the arithmetic (billed added, serviced subtracted, intra-tree service
counted once, a negative member net a gap and summed signed, a born member's
whole ledgers, a ledger that went back, billed CPU alone), settling with a
bill landing between two reads, the rusage layout at 216/224/264/272, and a
real server compiled at test time whose directory-service lookups are
billed to it: Delegated fires, its bill is at least the raw-buffer oracle's,
and the figure less the bill is the server's own advance to the nJ; its
pure-CPU control and the awk control read whole and exact.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Two kernel facts the per-process figures relied on without checking, both
measured on this M5 Max (macOS 27.0.1):

1. A process's read of ITSELF does not fold the reading thread's energy
   (its CPU time is current, its energy is folded only when it leaves a
   core). A thread burning 1 ms between two self-reads read the same
   energy at both in 28-29 of 40, and up to 9.4 mJ (a quantum spent mostly
   before the first read) in others. So detect_in_process() read a short
   span as nothing (estimated) or as energy from before it (sealed
   HwShunt). ProcessEnergyProbe now sleeps 1 us before each read: the
   blocked thread leaves its core and is folded. Read so, the same burns
   gave 0.93-1.41 mJ in 80 of 80. Work on the thread that opens and closes
   a span is read whole; other threads add what they spent, so a span in a
   multithreaded process is over its work's energy, never under it. The
   liveness probe still reads raw.

2. A process's exit folds its CPU time into its record but not the energy
   of its threads' last time on a core. Paired 2 ms burns ended by exiting
   vs by blocking (equal CPU time) read 0-1.12 vs 0.81-1.20 mJ per CPU-ms,
   under 0.05 in 11 of 40 exits; 60 ms burns lose within the spread. A
   thread's exit in a live process does fold. tree.rs already grades an
   exited member a gap (TreeGap::Exited), which this confirms; but
   child_reading / Attribution::child_energy_uj called the same record a
   full HwShunt reading. Its docs now grade it Partial, a lower bound, and
   say how a sealer must label it. The value and the rule are unchanged
   (no in-repo consumer seals it; joule-code-remediate is untouched).

Test: a 1 ms in-process span, 20 times, is a Cpu HwShunt reading every time
and within 1 uJ above / a quarter below an oracle that folds and reads raw
around it (it read 98-99%). Removing the fold makes most spans estimates.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The demo's busy span runs on threads of its own process (one per core), so
the machine-wide meter read everything the host did beside it, and on this
fleet (IOReport CPU rail frozen) only estimated it. It now uses
detect_in_process(): where this process's CPU energy count is live, the
figure is the workload's own threads' CPU energy and the receipt seals
that; elsewhere it is detect(), as before, with the machine-wide wording.
The idle span uses the same meter, reading this process asleep (30 uJ over
0.5 s here) against the busy span's 4.66 J. Worker threads exit before the
stop read, which folds their energy (measured: a thread's exit in a live
process folds; the process's own exit does not).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Two changes to how a call's energy is graded and where the monitor's own
energy is read.

1. The tree meter now counts the CPU work a system service was billed to
   the upstream tree for, and lists it as TreeGap::Delegated. gap_kind
   names it `delegated` (and `serviced-beyond-spent` for the signed-net
   gap), so energy_partial_because carries it and the figure, which holds
   the bill, is Partial. That holds EVEN WHEN the operator declared the
   server self-contained: a bill is evidence against the declaration for
   that call, and the bill is a lower bound (no service GPU/ANE energy, no
   unbilled threads, no late credits). The declaration still gates Whole
   where no bill arrived, because billing misses socket/pipe servers,
   other originators and remote APIs. declare_upstream_self_contained and
   the binary's docs say so. gap_kind gains a wildcard (`other-gap`) so a
   gap kind added to the meter by another branch still reaches the wire
   and keeps the figure Partial instead of breaking the build.

2. Phase 0, the flow-label scan sealed as FlowFacts eval_uj/eval_prov, is
   a computation on this thread over this process's memory, so it is now
   metered with PowerMeter::detect_in_process() (scan_meter) instead of the
   machine meter, which stays the calls' fallback. On this fleet the
   machine meter only estimates (IOReport CPU rail frozen); the scan now
   reads HwShunt with its own figure (about 0.4 mJ per scan in a debug
   build). In a multithreaded host the span also counts the other threads'
   work: an over-count, never an under-count, now that the meter folds the
   reading thread before each read. A count that did not advance is still
   the estimator's figure, never a measured zero.

Tests: grading shapes with Delegated (declared and not) and
ServicedBeyondSpent; every gap kind named, none `other-gap`; the scan meter
is the process-energy probe where that count is live and every tainted
call's flow block is HwShunt > 0 (or Estimator where not live); and end to
end, a C server compiled at test time whose calls do directory-service
lookups: declared, its delegating calls are sealed HwShunt Partial with
["delegated"], cost >= the raw-buffer oracle's own - serviced + billed, and
its non-delegating calls before them are Whole and exact to the uJ.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The process-tree part of the per-process rung now says how a platform's
billing of delegated work enters a tree's figure: each member's own energy,
less its serviced energy, plus its billed energy, summed signed (service
between members counted once, through the payer; a negative member net a
gap, never wrapped); that the bill is a lower bound (no GPU/Neural Engine
energy in the service, no unbilled threads, no late credits, nothing billed
to another originator, nothing over a socket or off the host); that any
bill makes the figure Partial, named `delegated`; that billing takes no part
in settling; and that the operator's declaration does not outweigh a bill.

The in-process paragraph gains two measured facts the reference meter now
acts on: a process's read of itself does not fold the reading thread, so the
reading thread SHOULD leave its core just before each read (over the work's
energy, never under it, once it does); and a computation known to run on the
metering thread alone, touching no other process, is not made Partial by a
child the process happens to have (the gateway's flow scan, whose host
always has the upstream server as a child).

The README's meter paragraph says the same in brief, and that an exited
child's own figure is partial (an exit does not fold its last time on a
core).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A 1 µs sleep before a self-read does not always take the reading thread
off its core: when the sleep's timer fires before the thread has blocked,
the thread goes on unswitched and the read misses everything it spent
since its last fold. The review saw 4 of ~44,000 such sleeps with no
context switch at load 85-118 (each read missing the whole burn), and the
in-process test fail 2 of 140 — once by the meter, once by its own oracle,
whose usleep(1) had the same flaw.

The probe now sleeps 50 µs, then 200 µs, 1 ms and 5 ms, until the reading
thread's own energy count (proc_pidinfo PROC_PIDTHREADCOUNTS) moved across
a sleep — it moves only when the kernel folds the thread — and only then
reads. A thread not seen to fold fails the read, so the span comes back
estimated instead of a short figure graded a reading. The process-wide
switch count (getrusage, pti_csw) was measured and rejected: with two
other threads switching it passed 2 of the 12 sleeps (of 25,000) after
which the reading thread had not folded. The probe is built only where
the thread count reads in the layout the code knows.

The test's oracle now folds its own, independent way: a sleep of 1 ms or
more and the process's switch count checked to have moved, failing loudly
if it never does. Docs (process, detect_in_process, SPEC §5.1 rung 3, the
sandbox README, jcp-gateway's scan_meter) no longer say the thread always
folds or that the figure is "never under"; they say the fold is checked
and what a failed check gives.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
gap_kind ended in a catch-all arm that named any kind it did not know
`other-gap`. That kept a new kind Partial, but it dropped the guarantee
that matters on the wire: a meter change adding a kind would compile and
ship a reason that says nothing of what was missing, and the test that
lists the kinds by hand could not notice. With no catch-all, a new
TreeGap variant fails to compile here until it is named. The test stays,
for distinctness.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The tree figure subtracts each member's serviced credit (work it did for
other processes, part of its own count) and raised a gap only when a
member's net went below zero; a positive net read Whole. But a servicing
thread is credited at its next voucher change, not as it works — a
daemon's serviced energy was 14-100% of its own energy over a window in
which its serviced CPU time was 93-99% — so a credit inside a span need
not be that span's service: one for work before the span subtracts energy
the span never held (short), and work in the span credited after the
stop stays in (over). A reading graded Whole could be off either way.

TreeGap::Serviced { serviced_nj, serviced_cpu } is now raised whenever a
counted member's serviced energy or CPU time moved over the span, however
the net came out; its docs, the TreeGap and module docs, SPEC §5.1 and
the sandbox README name it, beside unsettled-at-start, as a gap that can
put the figure OVER the span's energy as well as under. jcp-gateway names
it `serviced` (the compiler now insists every kind has a name), and its
docs say a `serviced` figure may be over. The unit test that asserted a
serviced member reads whole now asserts the gap.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A bill is credited when the servicing thread next changes voucher, so a
late credit for an earlier call's work lands in whichever span is open and
is added to that span's figure — tree.rs said so — yet the TreeGap, module,
gateway, SPEC and README docs called the bill a lower bound and every gap
but unsettled-at-start a shortfall. `delegated` is now described, beside
unsettled-at-start and serviced, as able to put the figure over the span's
energy (pre-span bills) as well as under it (unbilled work, the services'
GPU and Neural Engine energy, credits after the stop).

The stop also waits, when a counted member's billed counts moved over the
span, for them to hold: it re-reads every member's billed counts every
5 ms (BILLED_HOLD_POLL) until two reads agree, at most 20 times
(BILLED_HOLD_POLLS), and the reading takes the last read, so a call's own
credit landing just after its reply falls in its own span. It is separate
from settling (after the stop walk has settled; it reads only the billed
fields). Measured: it costs a delegating call one poll — stop median
9.6 ms against 2.7 ms without it, three directory lookups, 40 calls each —
and a pure-CPU call nothing. Its benefit was small on the services tried:
of 40 directory-scoped metadata searches, 5 had bills land after the reply
(one 0.40 J within 5 ms, the rest 0.03-0.44 mJ from 20 ms to 1.5 s), and
metered, 3 of 30 calls had a bill land after the stop without the wait and
2 of 30 with it. The docs say what it catches and what it cannot.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The delegated-work docs gave "under 1 mJ for a directory-service lookup
that cost it 0.06 mJ of its own", a figure in no evidence file (the meter
sweep has no directory-service case). They now give what was measured on
this branch: 0.83-1.46 mJ billed for three getpwnam lookups against
0.57-1.17 mJ of the client's own per round trip, over 10 round trips of
the tree-energy test and a probe like it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Three intra-doc links added with the stop's wait for the billed counts
pointed public docs (BILLED_HOLD_POLLS, TreeMeter::stop, the tree module)
at private items (`settled`, `hold_bills`), each a new cargo doc warning.
They are plain code spans now, as the module's other mentions of
`settled` are.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
SPEC §5.1 rung 3 graded the per-process count `Whole` "for work that runs
on those threads", and detect_in_process's docs listed what the count
leaves out (children, GPU, Neural Engine) but not work the process's
threads hand to a system service: the service does it on its own
threads, and the kernel bills it to this process beside the count
(ri_billed_energy), never in it — so a span over work that calls a
service read as an HwShunt record with nothing said missing.

Rung 3 now says such a span is not Whole for that work, that the
reference meter does not read the bill on this rung and so does not
detect it, and that an implementation SHOULD meter such work on another
rung or grade it Partial; detect_in_process, jcp-gateway's scan_meter and
the sandbox README say the same, and that a pure computation over the
process's own memory (the flow scan, joule-meter's workload) is
unaffected. Detection is left out: the Measurement record has no coverage
field to carry it, and no caller of this meter delegates.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The fold rule and the bill-hold rule were tested as pure functions, but
nothing failed if the probe's snapshot stopped calling the fold check, or
the tree meter's stop stopped calling the bill wait. The probe now holds
its thread-count reader (production: this thread's own energy count), so
a test gives it one that never moves and sees the read fail after every
fold sleep; and TreeMeter::stop is stop_with over its reads, so a test
sees a bill that landed after the stop walk reach the reading, and no
read or sleep when no bill moved. The integration test's assertion that a
delegating stop took at least one poll is dropped: a stop takes that long
anyway, so it could not fail; the stop times are still logged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
sandbox-meter: the oracle's switch count needs no conversion
All checks were successful
energy meters on real hardware / RAPL and NVML on jetson-hub (pull_request) Successful in 43s
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 2m9s
standards workspaces build + test / aac-rs (pull_request) Successful in 1m34s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 52s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 40s
standards workspaces build + test / map-rs (pull_request) Successful in 30s
standards workspaces build + test / mesh-rs (pull_request) Successful in 29s
standards workspaces build + test / openpay (pull_request) Successful in 1m19s
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 3m1s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 6m24s
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 4s
no-vanity-metrics / WAI public text names no comparison (pull_request) Successful in 4s
no-vanity-metrics / WAI public text names no company or product (pull_request) Successful in 3s
standards workspaces build + test / arl-rs (pull_request) Successful in 1m12s
standards workspaces build + test / eoc-rs (pull_request) Successful in 2m2s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m35s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 1m11s
standards workspaces build + test / joulehook-rs (pull_request) Successful in 1m15s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m22s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m30s
standards workspaces build + test / sense-rs (pull_request) Successful in 1m9s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 3m45s
e838070cdf
clippy flagged the i64::from calls in the process-energy test's switch
count as useless (ru_nvcsw is already a c_long, i64 here): two new
warnings, now gone. The count is summed as the c_long it is.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Transaction-Science/open-standards!214
No description provided.