Current State, In-Flight Work, and Roadmap
Snapshot taken 2026-07-16, revised 2026-07-17 after Effort B landed. Reconcile against
git log/git statusbefore acting on anything here — this chapter goes stale fastest.
Repository state at handover
- Branch
main, 15 commits ahead oforigin/main(unpushed, as of this revision) — the design spec/plan for Python NT value-type selection, the NTTable-metadata/timestamp fix, and the full Effort B implementation (7c5bc9bthroughd85473b, latestfix(py): final-review fixes — strict types= key validation, doc accuracy). - Working tree is clean — the three files that were mid-edit at the
2026-07-16 snapshot (
spvirit-codec/src/spvd_encode.rs,spvirit-server/src/simple_store.rs,spvirit-server/src/types.rs) have since been committed as part of Effort A below. There are no uncommitted changes to reconcile.
Effort A — NTTable metadata + store-entry timestamps (committed: 0ac87d5)
Purpose: make static/NTTable PVs archivable by the EPICS Archiver Appliance
(it rejects epoch-0 events and NPEs on structures without a top-level
timeStamp).
spvd_encode.rs:nt_table_descnow includesdescriptor,alarm,timeStampfields;encode_nt_table_fullencodes them (defaulting whenNone); new round-trip testnt_table_wire_format_carries_metadata. Encode order must match descriptor field order — that's the invariant.types.rs: newRecordInstance::stamp_missing_timestamps()fills missing timestamps per payload family (NdArray stampsdata_time_stamptoo; Generic skipped).simple_store.rs: calls it inSimplePvStore::newandinsert, plus a test asserting all record families getseconds_past_epoch > 0on store entry. Purely additive; caller-supplied timestamps are preserved.
Effort B — Python NT value-type selection (landed)
Delivered as a 10-task TDD plan with per-task commits.
Goal (achieved): let Python select any of the twelve NTScalar wire types
instead of collapsing int→I64/float→F64. Architecture: shared
type-string parser + strict coercion layer in spvirit-py/src/convert.rs
(parse_scalar_type, py_to_scalar_typed, py_to_scalar_array_typed,
coerce_scalar_value, coerce_scalar_array_value); keyword-only type=/
types= params across NT classes, factories, and ServerBuilder, and a new
spvirit.scalar() factory backed by PvKind::Typed(Pv<ScalarValue>, TypeCode); store put paths coerce to the record's existing wire type.
Errors: ValueError (unknown type string), OverflowError (out of range),
TypeError (wrong kind) — matches spvirit-py/README.md, the authoritative
user-facing doc.
Status: all 10 tasks are committed (git log 0819a18..d85473b).
spvirit-py/tests/test_value_types.py (324 LOC, ports 16060–16081) now
exists and exercises the full surface: NtScalar/NtScalarArray
type=/.value_type, NtTable/NtNdArray constructors, spvirit.scalar(),
server.pv() on long/unsigned records (no more KeyError), waveform/
aai/aao/pv() with type=, ServerBuilder type=/types= kwargs, and
Store.set_value/set_array_value/put_nt strict coercion. Build:
.\.venv\Scripts\maturin.exe develop; tests:
cargo test -p spvirit-server pv:: and
.\.venv\Scripts\python.exe tests\test_value_types.py.
Known gaps and latent bugs (triage list)
Cross-referenced from the per-crate chapters.
Crate audit + remediation, 2026-08-29 (landed on branch
audit-remediation-2026-08-29, not yet onmain). A workspace-wide adversarial audit — one pass per crate — produced fixes across the high and medium tiers, since committed on that branch. Load-bearing landings: the server monitor writer is now the two-lane flat-combiningConnWriter(spvirit-server/src/conn_writer.rs), which coalesces monitor frames on a latest-per-ioid lane and keeps control replies on a lossless FIFO lane, building self-contained per-subscriber frames (the stored delta serves only as a change detector); client-supplied pipeline windows (nfree) are now clamped toMAX_PIPELINE_WINDOW = 4096at every assignment from client input (serverhandler.rsandmonitor.rs, and thespserverbinary); PythonNotifier.notifydelivers monitor updates in order rather than via fire-and-forget spawns;spvirit-calcuseswrapping_remto avoid ani32::MIN % -1panic; and the IOC layer prunes deadfield_subs. Reconcile againstgit logbefore relying on any of this.
Codec-gaps effort, 2026-08-07 (landed). Three of the protocol/codec entries below are struck through: segmentation reassembly, the heuristic monitor bitset ordering, and silent array truncation. Delivered as a 10-task TDD plan with per-task commits (
241f4f4..ef7d587), the same shape as Efforts A and B. New codec surface:error.rs(DecodeError/DecodeResult),segment.rs(SegmentReassembler),monitor.rs(MonitorUpdate,MonitorLayout),DecodeLimitsonPvdDecoder, andDecodeModeonPvaOpPayload::decode_with_field_desc. Client, server, spserver, the tools and the Python bindings are all on the new APIs. Breaking:PvdDecoderreturnsDecodeResultrather thanOption, and monitor callbacks receive a&MonitorUpdaterather than a bare value. What is not done: segment emission — see roadmap item 6.
Protocol/codec
- No TLS anywhere (top roadmap item in README).
No segmentation reassembly in the codec (consumers roll their own)— fixed by the codec-gaps effort:spvirit-codec/src/segment.rs'sSegmentReassembler, driven by the client transport, the server handler and spserver. Emission is still missing (roadmap item 6).Monitor bitset overrun ordering is heuristic (three variants + scoring)— fixed:decode_monitor_updateis spec-exact (changed bitset, data, overrun bitset) and is what every workspace call site uses. The scoring heuristic survives only behindDecodeMode::Lenient, for out-of-tree packet-capture consumers.- No packet-capture regression corpus; four duplicated size codecs.
Array decode caps silently truncate; string/struct truncation can desync— fixed: over-limit counts areDecodeError::ArrayTooLargeand counts that cannot fit the remaining buffer areDecodeError::CountExceedsBuffer, both raised before allocation. Limits are configurable viaDecodeLimits.
Server
- CANCEL_REQUEST unimplemented (some clients send it).
- PUT to
Genericnot wired intoRecordInstance::apply_put(onlyput_ntworks);NtTable/NtNdArrayPUTs are wired and restamp like any other record. .dbparser: one-statement-per-line only; cannot load longin/longout/mbbi/mbbo/table/ndarray/generic (the repo's only twoTODO(follow-up)markers:types.rs:45,db.rs:546).Pv::alarm_limits()sets only the wire metadata fields, not the fields the alarm engine reads —.alarm_limits()+compute_alarms(true)does not auto-alarm.compute_alarmsdefaults to false.- ADEL parsed but not enforced; beacon change counter only bumps on protocol PUTs.
Client/tools
- Structured puts not fully surfaced (README caveat;
put_encode.rsis more capable than the high-level API exposes). - Epoch disambiguation heuristic in
format.rs(UNIX vs 1990 epoch by closeness to now). - Hardcoded cid=1/ioid=1 in single-shot paths — hazard if ever multiplexed.
- spserver: NtTable/NtNdArray DOL links are no-ops;
demo/docker_compose.ymlis an empty placeholder.
Python
- No
.pyitype stubs. - Stale artifacts present but gitignored (not committed):
spvirit-py/dist/spvirit-0.1.9.tar.gz,spvirit-py/.venv/. - "sync-only for phase 1" file headers are outdated.
The type-collapsing limitation— fixed by Effort B: all twelve NTScalar wire types are now selectable from Python viatype=/types=andspvirit.scalar(), with strict coercion. Remaining Python-side gaps:on_put/scanstill unsupported on array PVs; widenedbyte/short/inthandles (the plainintPvKind) are not range-checked on write — onlyspvirit.scalar(type=...)/store.set_valueenforce strict range checks on those types (see the README's "Widenedinthandles are not range-checked" caveat).
Process
- No clippy/rustfmt CI gate; no MSRV; no CONTRIBUTING.md; crates.io releases are manual with a history of version-numbering slips.
Roadmap (from README + observed direction)
- TLS support in client (and eventually server).
- Structured put payloads surfaced in the high-level client API.
- More complete softIOC behaviours and record processing in the server
(record types in
.db, table/ndarray PUT, CANCEL_REQUEST). Finish the Python value-types work (Effort B)— done; see above.- Quality infrastructure: packet-capture regression corpus, benchmarks,
lint gate in CI,
.pyistubs. - Segmentation emission in the encoder. Reassembly landed in
spvirit-codecwith the codec-gaps effort above, but nothing in the workspace splits an oversized outgoing message into segments — the server sends one large frame regardless of size. Needed before spvirit can serve large NTNDArray images to clients that expect segmented delivery, and it would also let the reassembler be tested end-to-end against our own server instead of only against synthetic buffers.