Skip to main content

Deferred work, and what triggers it

Things deliberately not being built yet, each with the observable condition that means it is time.

The rule this file exists to enforce

"Later" is not a plan. A trigger is something you would notice happening.

"When we need it" is a deferral with no owner — nobody is watching for it, so it arrives as a surprise, usually at the worst moment. "At the first release someone outside the project installs" is a trigger, because you will know the day it happens.

Every row below names a condition of the second kind. Where a trigger can be checked by a machine, the check is named, because a rule nobody checks is not a rule.

How to add a row

When something is deferred during design, it goes here immediately, with its trigger, before the conversation moves on. A deferral that is not written down in the moment becomes a thing somebody vaguely remembers.

If you cannot state an observable trigger, that is a signal: either the thing is not actually deferrable, or it is not actually wanted. Both are worth knowing.


Triggered already — these are not deferred

The first deployment is patient data collected for a client's FDA approval process. Several conditions that would ordinarily be future concerns are true on day one, and are recorded here so nobody re-defers them.

ItemTriggerWhy it has fired
A place in the format for identity, authorisation, and auditFirst record writtenA field that does not exist cannot be populated retroactively. Enforcement of versioning rules is separate and not yet triggered
Audit trail, operator identity, append-only recordsFirst regulated use0007
Pseudonymous subject referencesFirst patient dataRetrofitting means re-identifying existing recordings
Software bill of materialsFirst release installed outside the projectThe client is an outside consumer; Ranvier is Software of Unknown Provenance (SOUP) to them
Published known-anomaly listSameIEC 62304 requires a client to monitor their dependencies for known anomalies; without a published list they must reconstruct one
Retained per-release test and benchmark evidenceFirst release a client assessesEvidence not kept is evidence that cannot be shown
Authorisation reference and window on every streamFirst regulated captureFDA's electronic source data guidance requires recording the period an originator was authorised to capture under the protocol
Unique operator identityAny deployment touching protected health informationHIPAA §164.312(a) makes unique user identification required, not addressable
Reproducible buildsFirst release a client validatesA client may need to demonstrate the binary they validated is the binary they ran
In-process node hostingA platform that forbids a second processAndroid forbids executing a file outside the signed application bundle, so a headset cannot host a node as its own program (0017)
Cross-host clock synchronisationA graph spanning two machines0017 makes cross-device placement a supported capability, and nothing works across two clocks without it

Deferred, with triggers

Regulatory and quality

ItemTrigger
Electronic signatures (21 CFR 11 subpart C)The first protocol requiring a record to be signed by a person
Additive-only evolution and permanent field numbers enforcedFirst recording anyone intends to keep — the client application collecting real data, not skeleton test fixtures (0007 rule 2)
Node-package origin and install securityThe first node package installed from outside the project. The runtime executes what a graph names (0013)
Role-based access controlThe first session store written by more than one operator
Formal validation documentation packageThe first client requesting one for a submission
Profiles beyond the first — HIPAA, GCP, EU AI Act, institutional standardsThe first user who needs one Ranvier does not ship. Anyone can write one; the project need not be involved (0010)
Strict mode — a declared profile refusing to start on a failing checkThe first user who asks for enforcement rather than reporting
Anomaly classification and severity policyThe first defect found by someone outside the project
EU AI Act Article 10 data-governance record — training-set origins, collection process, preparation operations, representativenessThe first model trained on Ranvier data that ships inside a regulated device. The graph file and stream origin are already most of the answer
EU AI Act lifetime activity logging, in a retrievable and analysable formatSame
Archival beyond trial closure — integrity maintained against loss, corruption, and alteration (ICH E6(R3))The first study reaching closure

Architecture

ItemTrigger
A liveness signal on ranvier_link_tNow — this one has already fired. A binding cannot learn that an established connection dropped: Link holds stop, received and undecodable and nothing else, the receive thread's exit is swallowed by let _ = at crates/runtime/node/src/transport.rs, and a dropped socket therefore looks exactly like a quiet publisher — two counters that stopped moving. The Python binding could not port two of its predecessor's behaviour tests because of it (test_a_publisher_that_restarts_does_not_end_the_session and test_reconnect_off_ends_the_iteration_when_the_publisher_goes_away, both present and skipped in bindings/python/tests/test_subscription.py with this reason). Redialling on a stalled counter was considered and rejected: Runtime::connect allocates a fresh source per connection, so a sample lost across a redial of a merely-slow stream is attributed to nothing and counted nowhere, and a silent loss is worse than an absent feature. The shape is an accessor — whether the receive thread is still running — and the decision it needs is what a subscriber then does with the answer, which is where reconnection policy stops being the binding's guess
Reporting why a connection went quietA deployment where a subscriber stops receiving and nobody can say whether the publisher went away or the two disagree about the wire. A length prefix above the ceiling misaligns the stream, so receive closes the connection — correctly, since every byte after it is suspect — and returns Ok(()). It is not counted in undecodable, which is for frames that arrived whole, and it is not reported anywhere else. The hand-written Python client raised FramingError carrying the claimed length, and it was deleted on 2026-08-23 without anything replacing that signal; the binding over the ABI has nothing to raise. Related to the row above and not the same: that one is whether the connection is alive, this one is what killed it
A ranvier_stamp_count / _at / _name, so the stamp family enumerates like the othersThe second binding to transcribe it, or a third stamp kind. Five enumerations carry _count/_at/_name precisely so a binding loops rather than copies, and RANVIER_STAMP_UNDATED / RANVIER_STAMP_INSTRUMENT does not — so bindings/python/src/runtime.rs writes both values out, which is the one transcription in that crate. Two values is small enough that the cost today is a comment; the reason it is a row is that "small enough to type out" is exactly the argument that produced four of six refuse reasons in two TypeScript runtimes. grep -c 'ranvier_stamp' crates/abi/include/ranvier.h
A borrowed view over a sample's values, for a Python or Node consumerA measurement showing the copy dominates. bindings/python copies each sample's payload once, straight into the bytes object Python will hold, and bytes supports the buffer protocol so np.frombuffer costs nothing further. The recorded ecosystem position is that a binding copies rather than exposing native memory, and a borrowed variant needs a lifetime a garbage-collected language cannot state. Deferred with a number as its trigger rather than a feeling: no workload here has been measured yet
A relay, for networks that forbid a direct connectionA deployment where two devices cannot reach each other. Wireless client isolation is the case that produces it — access points forbidding clients from addressing each other, which no arrangement of who-listens-and-who-dials can fix. It usually leaves wireless-to-wired open, so a wired workstation can relay between two devices that cannot see each other. Deferred because the need is predicted rather than observed; an earlier version of this row gave the trigger as the transport exists, which confused a dependency being available with the thing being wanted. When it is built it forwards frames without opening them (0016), so it needs no codec, schema, or clock, and a frame it drops appears as a sequence gap the subscriber counts (Subscription::gaps)
A stream_ref that is unambiguous across sourcesA relay, or a second runtime that both adopts remote streams and serves them onward. A reference is allocated per runtime and means nothing outside it, so a runtime holding adopted streams from two publishers can hold two streams whose references are both 1. serve_one sweeps by name and matches by reference (0040), so in that configuration it can announce the wrong stream for a reference. Nothing builds it today — the relay above is deferred — and it is reachable by hand, by calling Runtime::serve on a runtime that also holds a Link. The relay's record has to answer it, and the answer may be that a relay re-references and says it did
Checking a typed input port against every publisher of the name it is bound toThe first document binding a typed input to a name two node types publish with different schemas. Graph::check_ports builds one entry per stream name with carried.entry(stream).or_insert(...), so a subscriber is compared against whichever publisher the document lists first and a second publisher carrying another schema passes unchecked. A recorder is exempt by design — it names no schema and interprets nothing (0041) — so only a typed consumer is exposed, and none exists in a document yet. It is not a repair but a decision: 0040 §4 keeps a name with several publishers legal, and what a typed port does when one of them speaks a schema it cannot read has not been settled. grep -n 'carried.entry' crates/runtime/graph/src/lib.rs
The observation interface decoding a sample against its own stream's schemaA session where two publishers of one name announce different schemas and somebody watches it in Studio. observe finds the schema with snapshot.streams.iter().find(|stream| stream.name == subscription.name), so the latest sample on a name with several publishers is decoded against whichever stream the snapshot lists first. It is a display rather than a file, and it is visibly wrong when it happens rather than silently wrong afterwards, which is why it is a row here and not a defect. The fix needs LatestSample to carry the stream_ref it already receives on the sample
Blocking publish for non-source streams, so backpressure propagates upstream instead of droppingThe first graph where a derived stream's consumer is slower than its producer and the work is not real-time — offline reprocessing is the archetype. 0009 forbids blocking for acquisition because a source node holds a device buffer where loss cannot be counted; a transform holds no device and could legitimately slow down. The origin field already distinguishes the two, so nothing new is needed to tell which streams may block
The publishing side of an announcement across the C ABI — operator, authorisation, production, and the publishing hostThe first binding that publishes rather than only subscribes, and it is the Unity integration on today's plan. 0048 §4 exports every field an announcement carries on the reading side, because a recorder written over the boundary must be able to record all of them. Nothing exported can declare them: ranvier_stream_spec_t has setters for the schema, the origin and the clock domain only, and ranvier_runtime_create takes no host identity, so a publisher built entirely through the ABI announces no operator, no authorisation, no production record and no host-and-boot pair. That is the asymmetry ranvier_subscription_t::from_subscription exists to test around — a pub fn that is deliberately not extern "C", so it is not in the header and no binding can reach it. The cost of leaving it is not symmetry: a recording made by a binding that could not state who was operating cannot be repaired afterwards. grep -c 'ranvier_stream_spec_set' crates/abi/include/ranvier.h
RANVIER_ERR_EMPTY split into a timeout and a cancellationThe trigger has fired. It was "the first binding that turns a read into a task, promise or future", and bindings/javascript is one: every wait there is a promise. It copes by answering null for both and exposing subscription.closed, which is this side's record of having closed the subscription rather than anything the ABI said — so a caller can separate the two only for a subscription it closed itself, and not for one closed on another thread. .NET fires the same trigger the moment next_timeout is wrapped in a Task. ranvier_subscription_next_timeout answers RANVIER_ERR_EMPTY both when the wait elapsed and when another thread called ranvier_subscription_close, and a binding cannot tell them apart. A caller that reads a cancellation as a timeout retries a subscription that will never deliver again; one that reads a timeout as a cancellation completes a task that should have kept waiting. 0048 §3 gives connect and serve three distinct codes for exactly this reason and did not reach back to fix this one, because the fix is not in the ABI: Queue::pop_wait returns Option<Arc<Sample>> and has no room for the distinction either, so it is a change to ranvier-node that ripples through Subscription::next, next_timeout and serve_one, and it moves RANVIER_ABI_VERSION. Cheap now, expensive once a binding is written around its absence. grep -n 'RANVIER_ERR_EMPTY' crates/abi/src/data.rs
Cancelling a dial that is in flightA binding whose users can close the application while a connect is outstanding and expect it to exit rather than hang. ranvier_runtime_connect bounds each address it tries and bounds name resolution not at all, because no platform here offers a deadline on getaddrinfo (0048 §2). Until this is built, a binding runs the call on a thread it is willing to lose. It needs a cancellation token the ABI does not have, and the review checklist's rule applies to it in advance: a cancelled operation must still complete exactly once, with a distinct status, or the caller's task never finishes and the failure looks like a hang rather than an error
Redialling a publisher that went awayA binding replacing the Python client, which redials by defaultreconnect=True, reconnect_delay=0.5. Runtime::connect_within opens one socket and spawns one receive thread; when that thread ends the link is dead and nothing reopens it. The trigger has effectively fired, and the row is here rather than done because the choice is where it lives: a redial loop in each binding is exactly the per-language client logic 0047 exists to end, so it belongs in ranvier-node and gets exported, and that is a decision about the Link lifecycle rather than an export. grep -c reconnect crates/abi/include/ranvier.h
Gaps::last_break across the C ABIThe first non-opaque struct on the boundary, or a consumer that asks for it. A break is five coupled values, and five independent calls let an update land between two of them, so a caller could assemble a break that never happened. The alternative is a POD struct crossing the boundary, which is a shape 0047 has not committed to and which should be decided deliberately rather than in passing. Nothing asks for it: it is not part of the Python client's Counts, and spec/streams.md §3.7 lists it as SHOULD
A node reading the StopReason it was sentA session report that has to say why a node was stopped. spec/control.md gives Stop.reason three values and the node end decodes it and drops it: Control::stop calls stop.ask(), which takes no reason (crates/runtime/session/src/control.rs:975-983). So STOP_REASON_PEER_FAILED is unreachable from a node in Rust as well as in both TypeScript control planes, and the ABI has no ranvier_joined_stop_reason to export. 0048 §6 exports the enumeration table anyway, so a binding generating constants cannot produce a subset — but a table with no accessor is a table nobody reads. The same holds for Stopping.reason, which Control::stopping chooses between ASKED and COMPLETE and never sets to FAULT, so a node that is leaving because something went wrong in it cannot say so. grep -n 'StoppingReason::' crates/runtime/session/src/control.rs
A publisher observing its own loss rate synchronouslyA node that could act on it — reducing its sampling rate, or switching to a coarser schema. Today loss is counted and visible to a person watching Studio or reading the session record, which is the right default because a node reading a device at 200 Hz has no move to make. A node that does have one is the trigger
Callback delivery, dispatched on one thread per subscriptionThe trigger has fired and the work is owed. It was "the first binding whose language can neither block nor poll idiomatically — JavaScript in both Node and the browser is event-driven and cannot block", and 0047 makes JavaScript one of three languages the C ABI is for. The ABI ships poll and bounded-wait only, which serves .NET and Python better than a callback would and does not serve Node at all. Dispatching on the delivery thread is what makes callbacks hazardous, because one slow callback stalls every other subscriber of that stream; a thread per subscription confines the damage to its own queue, at the cost of a thread. 0047's Alternatives records what the C signature must carry — void *user_data first, an int32_t return, and an unsubscribe that blocks until no invocation is in flight — because each of the three has a specific failure mode in .NET that is not recoverable once a binding is written around its absence
A borrowed view into a sample's values across the C ABI, instead of a copyA measurement showing the copy at the boundary bounds a real workload, as a profile of a named consumer at a named rate rather than an intuition about copies. ranvier_sample_copy_values fills a buffer the caller allocated, which is the shape the row below already committed language bindings to, and 0047 §5 gives the two reasons it is also the right first answer: a .NET caller's array is pinned rather than marshalled, so the copy writes straight into the final destination; and a borrowed const uint8_t * becomes a ReadOnlySpan<byte> that C# cannot stop outliving its handle — ReadOnlySpan<T>(void*, int) validates neither argument and the use-after-free compiles without a warning. The trigger is a number, not an argument, because the argument has been made both ways already
Authentication and encryption between devicesThe first graph that spans two machines. Until then every connection is loopback, where the threat model is whoever already has access to the machine — which is what 0014 relied on when it deferred authentication. The first deployment is single-machine, so this does not bind yet. It must be decided before multi-device work, not during it: HIPAA §164.312(e) covers transmission security, and a lab network is not a trusted channel
A copying delivery path beside the shared oneA measurement showing shared delivery pins enough memory on a constrained device to threaten a session, which the loss policy cannot bound (0019)
Choosing direct-versus-relayed automaticallyManual selection in the graph reported as friction. The first version is told which to use
A hosted relay run as a serviceA client who cannot run a relay inside their own network, and a compliance path for research data leaving the institution. The self-hosted case pays for itself first and is the same component
An embedded Python interpreter in a headset applicationThe first headset deployment needing a node that cannot be rewritten in the host's language. Chaquopy is the candidate; the Unity integration is undocumented
Shared-memory transportThe first cross-process, same-host flow carrying large payloads — raw video between two nodes
Shared-memory endpoints, mapped read-only into consumersA foreign-language consumer of a raw payload, or one raw stream with two or more out-of-process subscribers — a viewer and a processor on one uncompressed video stream is the archetype. Distinct from the row above, which is node-to-node transport: this one is about who may read a payload without it being duplicated for each of them. Language bindings copy at the boundary instead, caller-allocating the destination and having Rust fill it — which the C ABI now does, as 0047 §5 and ranvier_sample_copy_values — what BrainFlow's get_board_data and LSL's pull_chunk with dest_obj both do, neither of them exposing native memory. That keeps Arc<Sample> inside Rust, so 0019's immutability is held by the type system rather than by convention across a boundary no compiler checks. Copying is O(consumers × bandwidth) and a shared mapping is O(bandwidth), so the trigger is fan-out on a stream too large to multiply; at sensor rates it never fires, and serving encoded video rather than raw may keep it from firing at all. A read-only mapping would also enforce 0019 in hardware rather than on trust, which is the part no amount of copying buys. Same-host only — a viewer on another machine needs the network path regardless, so a decimated or preview subscription is the cheaper answer to fan-out and serves both. An interim step needs nothing from this repository: a Python consumer can allocate its destination in multiprocessing.shared_memory and hand that to the binding, putting one copy in a segment every other Python process maps — measured, including that memoryview.toreadonly() makes numpy refuse the write, and that on Windows the segment dies with its creating process while POSIX keeps it until unlinked
Transports beyond a local connection (WebSocket, UDP, LSL)The first node that cannot use the default transport
Additional loss policies beyond the four in 0009The first workload none of the four serves
Native-form passthrough beside a canonical schemaFired, and answered the other way. The Muse integration was the first that could not express its device in the canonical vocabulary. Rather than open the escape hatch, EEG and photoplethysmography became canonical schemas — see 0026, which explains why EEG in particular is the worst case to make an exception for. The general argument may still return for an instrument measuring something with no shared vocabulary; this row does not stand in for it
A canonical hand form, and fingersThe first integration reporting finger joints. body.tensor.v1 fixes 28 joints and stops at the wrist, deliberately: a hand is a separate measurement with its own providers and its own landmark argument — hand-tracking libraries in common use publish 21 landmarks and OpenXR publishes 26, which is the disagreement 0005 warns costs real work to settle. Adding fingers to the body form later costs nothing, because a tensor form names the fields it takes and a new one is a new version alongside the old
Aligning two body tracking spacesThe first session with two body trackers on one subject, or one subject and one headset. BodySample.frame names a tracking space and says two that differ are not comparable; nothing computes the transform between them. It is a rigid alignment from simultaneous observations of shared joints, which is a well-understood estimate and a node rather than a schema change. Deferred because with one body device there is nothing to align to
A chunk form for inertial measurementA second integration whose inertial unit batches. A Muse sends three 52 Hz samples per notification and ImuSample describes an instant, so the package publishes three undated samples per notification. That is honest and it is not what a consumer wants: the samples are 19 ms apart and nothing in the record says so. One device is not enough to shape a schema around, and EegChunk is the pattern to follow when a second one appears
Generating tensor-form packers from the schema and the form togetherThe second tensor form, or the first producer of body.tensor.v1 outside Rust. 0006 says nobody may hand-write a packer, because hand-written packing eventually disagrees with its schema and the failure is a transposed axis producing plausible numbers. The packer is hand-written and is the only one for the only form. The packer belongs to the canonical vocabulary rather than to this repository. The mitigation 0006 names for this case is not held beside it, and that is a gap rather than a decision: a byte-exact conformance corpus written from the form's declaration rather than captured from the code is what would close it, and what exists instead is a test. Read on 2026-08-23, that test reads raw bytes at independently computed offsets (joint.index() * 32 + component * 4), pins the shape against literals rather than against the packer's constants, checks that the left wrist is row 12 and that nothing else is, and asserts an unmeasured joint is NaN in the bytes rather than via unpack. So the layout is checked; what is still absent is a committed corpus, which is what makes the check survive a rewrite of the test rather than only of the code. One implementation cannot disagree with itself; two can, which is what makes the second one the trigger rather than the first.
A tensor-form name in StreamInfoThe second tensor form. 0006 §1 has a stream announce the forms it supports, and StreamInfo has no field for one — a BodySample stream announces its schema and a consumer infers body.tensor.v1 from it, which is right exactly while there is one form per schema. Deferred rather than added because it is a change to the wire contract every implementation speaks, and the JS SDK vendors its own copy: a field added now is a field two repositories regenerate for a distinction nothing yet makes
A reader-side helper that derives per-sample times from a chunkThe second consumer that needs them. EegChunk states its rate and dates index 0, so per-sample times are a two-line calculation every consumer performs — and performs quietly wrongly if it takes the timestamp as the last sample. Deferred rather than written because the right home is the reading side, and there is one

Tooling and interface

ItemTrigger
Selecting an edge that overlaps anotherEdges fanning out from one node overlap near their source, so the wrong one is easy to hit. Trigger: somebody using the viewer for real work rather than for a demonstration
A latency budget, measured against a defined workloadThe window in latency.rs is a distribution over recent deliveries and slides, so it is the right shape for watching a session and the wrong shape for a published figure. 0008's budget gate is what produces one of those. measurement/ closed the workload half on 2026-08-23 — defined conditions, a stated noise floor, spacing and age at delivery through both bindings — and closed none of the rest: there is no budget file, no comparison against one, and no build failure on regression, which is what G2 asks for. The remaining question is not how to measure but what figure is worth defending and on which machine, and the previous gate failed on exactly that: absolute ceilings compared across machines that differ by more than any regression worth catching
Graph authoring in StudioThe graph layer exists. The first Studio is a viewer only (0022)
A stream announcing only its own schema's descriptorHalf closed on 2026-08-10, and the remaining half is Ranvier's own schemas. A canonical stream announces one message's file plus the transitive closure of its imports, and a Port carries that descriptor rather than the runtime attaching a set of its own choosing. What still hands over FILE_DESCRIPTOR_SET wholesale is a stream announcing a ranvier.* schema: a pointer stream announces Sample, StreamInfo, Frame and Request alongside its two doubles. That is over-broad rather than false, and it is 44,505 bytes. What a per-message closure costs instead, measured on 2026-08-10 against a canonical vocabulary: 4,360 bytes for AudioFrame, 8,086 for EegChunk, 9,981 for GazeSample and 10,953 for VideoFrame, against 74,681 for that whole set. Trigger: a device announcing several streams at once, or the first recording, whichever comes first. python -c "import re;d=open('crates/message/src/generated/descriptor.bin','rb').read();print(len(d),sorted(set(re.findall(rb'ranvier/\w+/v1/\w+\.proto',d))))"
buf lint and buf breaking over the contractNothing checks the .proto files beyond whether protoc accepts them. The earlier project ran both, using buf's FILE category rather than WIRE — wire compatibility accepts a field rename, since the wire carries numbers rather than names, and generated code in every downstream language then stops compiling. Trigger: the second language generating from this contract, which the JS SDK makes imminent
A check that no payload schema declares a timestampThe sample owns time. A canonical payload carrying its own would be a second source of truth that generic tooling — the recorder, Studio, a converter — never decodes far enough to see, so two consumers could disagree about when something happened and neither would error. Holds today by inspection. Trigger: the same as the buf gates, since it wants a compiled descriptor to read
Editing a graph while it runsRestarting a session to change wiring reported as friction. Node-RED and Bonsai both allow it, so this is a real gap rather than a hypothetical one (0020)
A command-line inspector beside the browser viewerSomebody needing observation without a browser. The observation interface serves both
Visual graph editorAfter the base-complete gate in 0008. Above the seam, so unblocked by anything else
Inspection and replay viewerSame
Multi-resolution summaries for scrubbingThe first recording long enough that a viewer stalls on it
Raw-stream buffer sizing and the rate-deficit halt thresholdThe first sustained-rate workload run against real hardware. 0009 states the threshold is not yet known
Tracing, metrics backend, dashboardsThe first regression the single benchmark number failed to catch
stop read by the three spawnable programs that still do not — mocopi, muse, neonFired, and closed on 2026-08-12. All three call control::join, take a Stop from it and wait on that, so a session containing one reports it stopping rather than killed. The row's own check, grep -rLn 'control::listen' ../ranvier-package-*/src/bin/*.rs, now returns only probe.rs binaries — one-shot programs with nothing to stop — plus av's camera.rs, which has no manifest and is not a spawnable node type. The trigger it named never fired. It was a machine with one of those instruments attached, and no such machine has appeared; the work landed anyway, in the general migration onto the control plane. That is worth keeping rather than deleting: a trigger that is bypassed is not a trigger that was wrong, but it does mean the row sat here describing a gap that had closed, and nothing was watching for the close. grep -n 'joined.stop()' ../ranvier-package-{mocopi,muse,pupillabs}/src/bin/*.rs
What a capture node does when it is told to pauseThe first graph that needs an instrument to stop publishing mid-session without ending the session. A recorder pauses by not writing, which is well defined; a capture node could drop the samples, buffer them, or leave a gap the recording can show, and the three produce different files. 0043 §4 recognises the word and refuses it out loud rather than choosing one silently, because the choice reaches every capture package at once. An application above the runtime already writes pause to the recorder and binds it to a key, so the gesture exists and only the recorder answers it
A measurement of what a killed node actually losesA capture node that batches to disk, or one that must tell an instrument it is done. 0043's What it costs records that neither input nor ranvier-processors holds a buffer at exit — both publish per sample — so the benefit measured today is the report's information content and not recovered data. The claim is worth re-running against a node that does hold something, because it is the argument for the deadline mattering at all

Ecosystem

ItemTrigger
The control plane in the Python bindingThe first node type written in Python. bindings/python binds the data plane, the clock and all five enumerations, and not ranvier_join or the Joined / Refused handles. It is a strict addition rather than a regression — the hand-written package had no control plane either, because regen_proto.py compiled ranvier/message/v1 and never the control group — and binding it well needs a Python node lifecycle, which is a design rather than a wrapper. The enumerations it would need are already there and already enumerated, so the missing half is the handles
Package registry integration and in-app installationThe first node package written by somebody outside the project
Contribution quality tiersThe first published package that is not complete for what it covers
Language bindings beyond the firstThe first contributor blocked by the absence of one

Not technical

ItemTrigger
Free and paid packagingA deliberate decision, not a condition. Nothing to build in advance; the architecture already keeps packaging open. One rule is already fixed: no property of the wire protocol may ever be gated (0007). The recording format is CA3's, and its licence is CA3's decision rather than one taken here
Public release and licence choiceA deliberate decision
Splitting the contract into its own repositoryDone and undone. The contract was split out on 2026-08-07 and came back on 2026-08-10, at proto/. The canonical vocabulary went elsewhere instead, which left one contract with one implementation and nothing for a second repository to coordinate (0012)
Splitting a language binding outThe first binding maintained by someone who is not a core contributor

Blocked, needing a decision

Not deferred — these stop the next piece of work and need deciding before it can start.

What to call the boundary between your code and the machinery

The concept is settled: application code depends on one interface and never on a transport, socket, codec, or wire type. That is what lets the machinery be replaced without breaking a node anybody wrote.

The name is not settled. "The seam" and "node handle" were both tried and neither landed — the first is jargon from one book, the second names a thing that already has a name, since your program is a node in the graph. The current text states the rule and avoids coining a noun, which works but leaves prose slightly awkward wherever the boundary needs referring to.

Trigger: the first SDK surface written for a language other than Rust. At that point the name appears in an API a node author reads, and leaving it unnamed stops being free.

A tail figure for sub-100 ns operations is not available on Windows

concepts.md §8 commits to publishing tail latency. The harness cannot supply a real per-call tail for anything below the platform clock tick, and batch timing recovers resolution by averaging the tail away. Higher-resolution timing than std::time::Instant offers would be needed. Nothing here has been measured against a defined workload.


Ideas captured, not yet assessed

Things raised in passing that have not been thought through. A row here is not a commitment and does not have a trigger yet — the next step for each is to decide whether it is deferrable at all.

(empty)