One note per decision the evidence in ../research/ left open. A note states the question, the condensed evidence with references, the options with their named losses, the decision, and the follow-ups it creates. Status: provisional means it was taken without a human and is reversible (a config default, a feature flag, or docs-only); accepted means it is normative for the documents it names; superseded by NNNN means a later note replaced its decision, and the note stays as the record of why the earlier one was taken.
The catalogue the questions come from is SYNTHESIS.md §8; a closed question carries a Closed by [NNNN] paragraph there.
| id | title | status | summary |
|---|---|---|---|
| 0001 | Sequence field — scope and layer | accepted | Two new L0 DATA keys: a per-producer monotone sequence for ordering and gap detection, and a separate producer identity for bounded deduplication; PerKey stays L2. |
| 0002 | Control and bulk traffic — separation against head-of-line coupling | accepted | Control traffic gets its own connection per peer and bulk traffic a connection per path, so a stalled reader cannot withhold a control frame; no multiplexed control stream. The per-path half is implemented; the per-peer control connection is parked by 0011 §4.3 for want of any frame that needs it. |
| 0003 | Flow-control credit — unit and layer | accepted | L0 carries no application credit — QUIC's windows are the byte credit, the stream budget the message credit; L2 gets an absolute per-subscription message credit, which 0011 §4.3 places on the connection of the path that subscription names rather than on a control connection. |
| 0004 | Durability levels for Stored and Replicated | accepted | Stored(Written|Flushed) names the failure domain it survives and Replicated(n, flushed) counts replicas that reached at least Written, leader included; the two axes are a partial order and n is achieved, not configured. |
| 0005 | The refusal race — closed as documented behaviour | accepted | Race stays documented; a refusal is guaranteed only beyond the peer's stream window or in Req/Rep; no L0 application ACK, L2 Accepted later. |
| 0006 | Guarantee sets, and where the chain ends at an adapter edge | accepted | Guarantees are a set over the §3 dimensions with core as the default and only supersets configurable, declared in HELLO and intersected with failure instead of downgrade; at an adapter edge the chain ends at the foreign transfer point and degradation must be named in configuration. |
| 0007 | The topic namespace — opaque paths, segmented topic filters | accepted | Paths stay exact and opaque; topic filters become .-segmented with * and trailing #; ZeroMQ byte prefix maps to a segment boundary as a named loss. |
| 0008 | Session identity — the proved fingerprint, and nothing else | accepted | Fingerprint is the peer across connections and binds control to bulk; no L0 session, resumption is L2; producer identity absent by default, else raw 32-byte bstr. |
| 0009 | A bounded drain at shutdown | accepted | shutdown stays abortive; a separate drain(Duration) stops admission and waits for already-finished transfers to reach the peer's transport, with a mandatory finite deadline and a local count rather than an acknowledgement; L2's queue drain is a different operation. |
| 0010 | The local transport, per platform | accepted | Inproc, then AF_UNIX, then named pipes; the OS connection is the stream, one per transfer, so no framing or multiplexing layer is invented; no TLS locally, so the kernel proves the peer and IncomingMeta::peer becomes key-or-local-principal; no automatic fallback, and zero-copy by handle passing is named and deferred. |
| 0011 | Server-initiated traffic is answered where the registration arrived | accepted | Traffic a side originates for a peer's registration rides the connection that carried the registration, and no header field selects a connection; a frame naming a path is path-scoped and rides that path's connection, which takes SUBSCRIBE/UNSUBSCRIBE and the reserved credit frame out of the control tier and parks that tier for want of cargo; the residual coupling — an endpoint publishing and subscribing on one path — is named. |
| 0012 | Grouping local connections into a peer | accepted | The first local connection is the peer's control connection and holds everything the runtime keeps per peer; every further one is admitted on a 16-byte server-issued group token plus matching kernel credentials, never the token alone; transfer connections dispatch by path; server-initiated streams ride parked reverse connections bounded by max_parked_reverse, and an exhausted pool is a counted fan-out drop that leaves the subscription alive. SCM_RIGHTS is rejected because Windows AF_UNIX and named pipes cannot offer it. |
| 0013 | Standalone competitor libraries beside weida, not backends inside it | accepted | The repository holds two products: weida, and first-class standalone implementations of the protocols it competes with — weida-zmq first, weida-nng after — sharing a new weida-runtime crate (Exec, reactor ownership, resolver, socket hygiene, name registry) but none of weida's frames, HELLO or Link; no zmq backend inside weida's primitives and no zmq:// scheme; the bridges shrink to forwarders that terminate both sides and state their guarantee set; "first-class" is defined as a checkable list — every zmq_socket(3) type, tcp/ipc/inproc, NULL/PLAIN/CURVE with ZAP, interop against libzmq and zmq.rs in both roles, the zguide's recipes, and a parity table naming every gap. |
| 0014 | Four parallel library workstreams, and bindings that follow each library | accepted | The remaining competitor work runs as four parallel workstreams, each a library in 0013's shape in its own worktree with its own CARGO_TARGET_DIR: the ZeroMQ Python binding; weida-nng on the existing SP codec plus its rebuilt forwarder; the MQTT 5 client weida-mqtt; and weida-amqp plus weida-nats. Phase C is unparked — a binding follows its own library, asyncio first and then a sync surface over the blocking facade — and every binding is built on one shared weida-py-core (error mapping, the Runtime/Context bridge to asyncio, the bytes boundary). MQTT and AMQP servers stay Phase D, because a server there is a broker. |
| 0015 | Peer authorization — no credential in the handshake, a verdict per path | provisional | A weida handshake carries no application credential and does not grow one: the handshake proves a key, and authorization is the acceptor's local decision on (proved peer, the path the stream dispatched to), expressed with the refusals the protocol already has (REJECTED, UNKNOWN_ENDPOINT, Untrusted). Every precedent that carries a handshake credential — ZMTP PLAIN/CURVE with ZAP, MQTT 5's AUTH, NATS' callout, AMQP's SASL — decides "may this peer connect" there and the per-resource question elsewhere, which is what Trust pinning already answers here. What "neither" costs is named: Pub/Sub has no in-band way to present a token, so a token-authorized subscription needs ServerTls::require_client or a companion Req/Rep path; and the bill for reopening it is itemized down to the free HELLO key, DATA key, frame kind and error codes. |
| 0016 | Conflation — keep the newest, discard the rest | provisional | "Keep the newest" needs no key in L0/L1, so none is added: the producer holds the single slot, Publisher::open + write_now makes the discard the transport's rather than the producer's, and subscriber_buffer_bytes sized to about one value is the whole configuration (B-064, B-065). A fan-out coalescer keyed per subscriber is refused rather than deferred, because holding the superseded value is the per-subscriber copy B-064 exists to remove; Coalescing(key) stays reserved for L2 with its preconditions written down, and a key would buy exactly one thing — several keys multiplexed over one subscription. Conflation and PerProducer numbering are reconciled in the order the code already has: numbered before fan-out, so a discarded copy is a gap rather than a renumbering. |
| 0017 | A per-subscription verdict — the silence stays, and it is now defined | provisional | SUBSCRIBE keeps no reply half and weida grows no verdict frame, because no legal v0 frame can answer a SUBSCRIBE and a new frame kind would be fatal to a v0 peer rather than additive (kind 6, PROTOCOL §3.2). What the note adds instead is the definition the silence never had: it means "received and not refused", a subscriber may not infer that anything will ever be published to it, and the three silences v0 conflates are named. The LIMIT_EXCEEDED close stays the only refusal, with the cases where it is legitimate written down. Coherent with weida-zmq's XPubSocket::refuse, which is also connection-fatal: same granularity, different spelling. The missing prerequisite is a local decision point, not a frame. |
| 0018 | What the minimal L2 broker is | provisional | The first broker slice issues Accepted on the producer hop and Processed on the consumer hop, and leaves Stored/Replicated reserved, because reporting Stored for an in-memory buffer is prohibited and the store is Phase 5. The L2 credit frame of 0003 is in the first slice as the frame kind PROTOCOL already reserves - initial credit zero, an absolute idempotent per-subscription delivery limit riding that path's connection - because the roadmap gated Phase D on it. A queue is an ordinary opaque endpoint path, never a topic and never a third namespace, created by the broker's configuration rather than by a declare frame, and a queue drain is explicitly a different operation from Runtime::drain. Section 4.9 enumerates what the broker is not, with the phase or note that owns each. |
| 0019 | How a JVM binding reaches these libraries | provisional | JNI, not the Panama FFI, because Panama is final only from JDK 22 (JEP 454) and that floor sits above two of the three LTS releases a deployment may be pinned to; the stated floor is JDK 17, the MSRV-equivalent of the workspace's rust-version. One shared foundation per language, weida-jvm-core, shipped as a versioned Rust-crate and jar pair, carrying one concept weida-py-core did not need: a generation-tagged handle table with AutoCloseable and a Cleaner backstop, because JNI has no refcount and a raw long is a forgeable pointer. The bridge is a CompletableFuture subclass whose defaultExecutor keeps caller continuations off the reactor's threads, byte[] costs exactly one copy each way, and ZeroMQ gets the first binding because its interop bench needs no third process. |
| 0020 | A cluster is a set of nodes the client may know about | provisional | Awareness is a capability, not a requirement, which is how the owner's "aware" preference and the deferred invariant "a broker cluster appears as one logical broker" both hold: Discovery::Aware is the default and Discovery::Single the opt-out, and a node behaves identically toward either. DNS says who, the protocol says who leads — a portless URL resolves SRV _weida._udp.name and then a headless service's A/AAAA, which Kubernetes documents as "the set of IPs of all of the Pods" and filters by readiness, while leadership is answered on the connection because an election takes seconds and no TTL expresses that. The redirect carries a term and is bounded at two hops, which is precisely what AMQP 1.0 left unspecified. The node id is the proved fingerprint (etcd's rule: an id is unique for all time, so IP addresses make poor ids), so a client pins a set and DNS is never authority. Payload is stream-replicated with only the commit record in the log, which keeps a 33 MB frame out of a WAL and makes Replicated(n, flushed) reportable. |
| 0021 | Consensus is a dependency, not a subsystem | provisional | openraft as an ordinary dependency of weida-broker, not a Raft written here: the owner's premise — no async Rust Raft — turned out to be one word off (openraft is async; what is missing is etcd/raft's I/O-free tick-driven core with async traits), and the part of building one that matters is the invariant harness openraft already runs tick by tick against the paper and the TLA+ spec. No façade crate, because the same encapsulation is free: one crate may name it and no public signature may, which is how quinn is already handled, and which matters because openraft promises incompatible changes before 1.0. Two consequences carry further than the choice: AsyncRuntime's nine types are implemented over Exec so nothing calls tokio::spawn (and if that is not clean, the note is reopened), and RaftStorage's thirteen methods are the requirements list for the Phase 5 store — truncate backwards, purge forwards, byte-limited ranged reads, a snapshot that is a stream — which decides the store's interface before the store is built. Raft rides its own ALPN weida-raft/0, encoded with minicbor rather than a second codec. |
| 0022 | Consensus topology: how many groups | provisional | One group for the cluster, one per replicated queue, payload in neither — RabbitMQ's architecture (Khepri plus a Raft per quorum queue) and JetStream's (a meta group plus per-stream groups), not Kafka's, whose data path is a purpose-built ISR protocol this note writes down as the unbuilt alternative. The cost RabbitMQ measured is mostly the cost of putting bodies in the log, which 0020 §4.5 already refuses, so a per-queue group's entries are tens of bytes; and because weida's transport is QUIC, every group between two nodes shares one connection as independent streams, so adding a queue adds a timer and a log rather than a socket. "Optional data raft" is made coherent without a second protocol: replication factor 1 means no group at all, an owner recorded in the control log, and such a queue can never report Replicated because Replicated(1, …) is not reportable. Two is refused at configuration, per group and per cluster. The availability split is the reason the two kinds exist: with the control group below quorum administration stops and messaging does not, deliberately unlike Kafka's fencing. |
| 0023 | A completion is a cursor, and wire state is not one | provisional | The completion levels are verdicts about whole messages in a system whose first invariant is that payloads stay streams, and PATTERNS.md §1.2 shows the information is already cursor-shaped: a receipt beyond the window cannot resolve until the reader consumed p - w bytes. So Stored(*), Replicated(n, …) and settlement become absolute byte offsets on frame kind 6, which travels on its own uni stream naming the payload stream it reports on (0024 §4.4) — a cursor never shares a stream with payload, and §4.1 stays exactly as it is. The encoding mirrors the credit of 0003: absolute, monotone, idempotent; credit is a cursor the receiver grants, a completion one it reports. Transport progress is deliberately not published: quinn keeps the acknowledged ranges internal, ZeroMQ hides even the connect and offers a separate opt-in monitor instead, and turning PATTERNS.md §1.2's inference into an API would invite applications to rebuild the reliability QUIC already has — no upstream change is requested. Processed loses its end-to-end reading (that is request/reply, 0018 §4.3) and keeps a settlement one: "you may forget up to N", which is what makes redelivery meaningful at all. A cursor reports responsibility and is not a recovery mechanism: within a connection QUIC retransmits, beyond it weida does not resend, and what an interrupted stream means — cancel, reschedule, or open a new stream for the remainder — follows from the pattern (PATTERNS.md §1.11), not from the protocol. Whether cursors are reported is the negotiated acknowledgement level; how often is a local number. |
| 0024 | Three pattern families on one surface, and one back channel | provisional | The "three layers" opening of ARCHITECTURE.md §1 was a real dependency direction sold as a user's mental model, and the document refuted itself two screens later: "L2 is a layer, not a fork". So the surface is two stream kinds and three vocabularies over them — stream, message, broker — chosen freely, overlapping on purpose, because a request is a stream and a completed request is a message. The only significant addition to the ZeroMQ base is the cursor back channel, and it changes no pattern's semantics: a sender states per message which levels it wants, in one of three modes — per_chunk, coalesced { bytes, interval }, fin_only. fin_only makes the classic whole-message ack this system's degenerate case, so a migrating application asks for it and gets what it had. The carrier is a reliable stream, separate from payload, and the separation is forced rather than chosen: a DATA frame runs to FIN, so a responder streaming a reply could not interleave cursors about the request, and a cursor about the reply direction has no carrier once the initiating half is finished. So kind 6 is a uni stream naming the payload stream it reports on (QUIC's own StreamId, which quinn exposes) — which leaves every pattern's payload topology unchanged: a Push transfer stays one uni stream and still gets a reliable verdict, an exchange keeps its reply half for the application, and the broker's confirm is untouched. Datagrams were evaluated and rejected: they needed the same stream id anyway, so their only advantage was per-cursor cost against the one property the mechanism must have. "Always bidi, reply on its own uni" is also rejected — that is the separate-reply choice whose correlation machinery ARCHITECTURE.md §1 deleted. Nothing waits on a cursor, and coalescing stays free because cursors are absolute. Headline: materialization is a per-hop choice — a load balancer satisfies an uploader with its own transport receipt and never holds the video, so streaming is the thing to lead with. |
| 0025 | The website is the document set, rendered | superseded by 0033 | homepage has named weida.doodleshnookie.net since B-068 and nothing was there; the owner's constraints are that the site lives in this tree and that there is no public release. So the site is crates/site, one workspace member and one dependency (pulldown-cmark, the Markdown implementation rustdoc itself uses), which renders README.md and the normative documents and writes no prose of its own — a landing page with its own summary of the guarantees would be the first thing to go stale, which is the drift class every coherence sweep here has found. A document is published or excluded with its reason (the loop's bookkeeping is not documentation, a third party's requirements and other projects' specification text are not ours to serve), and a document that is neither fails a test. A link to an excluded document becomes a link into the repository; a link that resolves to nothing fails the build, which found 58 broken links in the notes on its first run. The pre-release notice is on every page from one constant, the output is files under target/site that work over file:// with no server, no JavaScript and no web font — and nothing deploys it, because the release it would announce does not exist (B-254). |
| 0026 | The guide, and the question it answers | provisional | Two owner statements, one decision: ZeroMQ's zguide is the model for the documentation this project owes — it teaches ways of constructing networked software rather than an API — and the question it is organised around is C8B: how do you scale a software system to eight billion people, this project's C10K, utopian on purpose because a number in the question is what makes a design falsifiable before it is built. The arithmetic lands in an afternoon on measured figures: one producer needs 12.4 hours to hand one 64-byte message to everybody (B-009's 179.4 Kmsg/s), a fan-out node holds 10³-10⁴ subscribers at B-011's per-connection cost and the default 8 MiB budget, so two to four hops reach everybody — which means the hard problem at C8B is neither throughput nor depth but what a hop may claim, and an acknowledgement per recipient converging on one root is 1.1 TB for one message. Every conclusion is a decision already taken for other reasons (0006, 0018, 0023, 0024, no materialization); the question explains them and names the next gap, which is that the fan-out width the whole depth argument rests on has only ever been measured at eight subscribers (B-247). The form is the one weida-zmq's zguide recipes already use and the opposite of a prose guide: docs/GUIDE.md, whose every program is an example in the tree, included as a module by tests/guide.rs and asserted against the chapter's own claim, with a test that counts the claims so prose cannot outrun the code. A race is taught as a race — chapter 1 shows 0005's refusal race by running the same program three times — and no number is an estimate: measured figures name their item, arithmetic says so, and §0.4 names three things nobody has answered. Chapters 0 and 1 are written; the arc is filed slice by slice, in order of what can be asserted rather than in order of the chapter numbers. |
| 0027 | A live connection's guarantee set is the one it configured, so nothing reports it | provisional | GUIDE.md §4 went looking for a way to print what a chain had negotiated and found none: Agreed and ConnCtx::guarantees are pub(crate) while GUARANTEES.md §4 describes the negotiated set as a property of a connection. The answer is no accessor, and it is arithmetic rather than taste: RuntimeConfig::guarantees is what a runtime offers and requires (0006 §4.4), so for a live connection agreed = min(mine, theirs) and agreed ≥ mine — or there would be no connection — therefore agreed = mine. A connection.guarantees() would hand a caller its own configuration back, plus a branch that can never be taken. What a peer requiring more than its peer offers receives is not a weaker connection but none: measured at this commit, core dials core with Ok(()) while the same runtime with delivery: AtLeastOnce or acknowledgement: Accepted is refused with Error::Negotiation. So the observation an application needs is the connection having succeeded, and it already has it. Two findings on the way: HELLO keys 5 and 6 are on the wire (a core set omitted so a v0 HELLO stays byte-identical), which retires a stale "spec ahead of code" caveat in GUARANTEES.md §4; and the refusal's wording is a race — negotiation failed when the dial read the peer's CONNECTION_CLOSE reason, connection lost when the local teardown won — so the test asserts the refusal and not the text, exactly as 0005 established. Revisited if offered and required are ever separated, or if sets become per connection rather than per runtime. |
| 0028 | A trace context is propagated, never minted | provisional | data_header set traceparent on every DATA header of every pattern and minted a fresh root when the caller supplied none, so a 64-byte push paid a fixed 60 bytes of frame — 58 of header plus 2 of the frame's length field — for a trace context nobody asked for, the largest single item in a 135-byte frame, against ZMTP's one to nine bytes of framing and SP's eight. GUIDE.md §0.3's C8B arithmetic is what made it urgent (≈480 GB of an ≈1.08 TB message-to-everybody, 44 %), but the argument that decides it is not the bytes: a minted root is not a safe default, it is a fabricated fact — a hop that received a context and forgot to propagate it emitted a new trace rather than nothing, so a collector saw two unrelated traces instead of one visible gap. The failure mode of "always mint" is a lie that looks like data; the failure mode of "propagate only" is a gap that looks like a gap. So: key 3 is written exactly when the caller supplied a context, weida::new_trace() is public so starting a trace is one line and a deliberate one, OutgoingTransfer::trace() returns Option, and a runtime setting is rejected because it would make the bytes a peer observes depend on a local configuration nobody declared — the class of thing 0006's negotiated dimensions exist for. Measured after: a 64-byte push is 76 B — twelve bytes of framing, half of it the endpoint path, 84 % of the wire payload — at 192.6-195.9 Kmsg/s, against 136 B and 168.9-171.2 Kmsg/s with a caller's context, both variants in one run because this machine's spread is wider than several of the effects it is asked to show. The bench's byte helper no longer hardcodes a traceparent: it builds the header the send path builds, so the number cannot drift from the code the way it just had. |
| 0029 | A report is relayed, a certificate is not | provisional | Three decisions contradicted each other once a queue existed: 0023 §4.4 had Processed "lose its end-to-end reading", 0018 §4.3 said the broker "MUST NOT relay it", and GUARANTEES.md §2 forbids presenting a hop-local acknowledgement as an end-to-end result — so the completion mechanism the cursor stream exists for was unavailable through exactly the topology a persistent queue is. The distinction that resolves it is already in 0023 §4.5: a guarantee is the negotiated acknowledgement dimension, a cursor is a report, and only the first is hop-local. So a queue re-emits its consumer's report toward the producer as the consumer's, while the broker's own certificate stays Accepted; two prohibitions keep that from drifting — a hop may not emit a level it did not receive, and may not re-emit a report that reached it as a re-emission, so a claim travels at most one queue from its observer. Attribution bytes on the record were refused (a record is 16 bytes and the level's own definition already names whose statement it is), as was a broker-specific restatement level (it would make a producer order a private number to learn a standard thing). The note also settles what B-203 would otherwise decide in code: no visibility timeout — packet loss is QUIC's retransmission, a dead consumer is the requeue on subscription loss, a consumer that holds a delivery is bounded by max_unsettled, three failure modes with three answers and no invented duplicates — the names Unsettled/max_unsettled, delivery_attempt as a count on DATA key 12, and retire(deadline) for the queue drain, because 0009 §4.7 forbids presenting it as the L0 drain. |
| 0030 | Which runtimes Phase C reaches, and in which order | provisional | LOOP.md §9 had said "Python first, then Java, then Node" since the first week and nothing justified it; 0019 answered only how a JVM binding would be built. The order is now Python, Node, Erlang, C, Java, measured against 0026's question — how many runtimes can call this, and through what substrate — rather than against popularity. Python is done and is the evidence that a per-language foundation is the right unit: five bindings ride weida-py-core and it never changed to admit one. Node is one row for three runtimes, because N-API is a stable ABI and Bun and Deno implement it, so one prebuilt addon serves all three — each with its own smoke test, the way package.sh proves the wheel rather than trusting maturin. Erlang is the cheapest bridge of any target and is third only because Node's reach is wider: enif_send delivers a completion into a process mailbox, so the loop-wakeup machinery that costs the Python row ~78 µs per operation is simply absent, ResourceArc is a handle table with GC cleanup instead of the generation-tagged one the JVM needs, and BEAM binaries are refcounted so payloads cross near-zero-copy in both directions. A C ABI is fourth — it is weida::blocking with opaque handles, an errno pair in ZeroMQ's shape, catch_unwind at every boundary and cbindgen, but explicitly not ZeroMQ's polymorphic socket — and it waits because its consumers are the targets with no Rust-native path (Go, .NET, C, Zig, Lua), none of them asked for, and because every binding here is written by this project, which is what a C ABI's value normally depends on not being true. Java is last rather than dropped, with the cost that decides it written down: the heaviest release story of any row and nobody on this project reads Java, so a binding it cannot maintain would be worse than one it does not have. |
| 0031 | Transparent redial, and the outbox that makes a send a message | provisional | A dialled address outlives its connection: connect records a slot the runtime redials with a ReconnectPolicy (ZeroMQ's RECONNECT_IVL shape, exponential with jitter by default), re-registers the subscriber's own route and filters on the new connection, refuses a peer that answers with a different key, and reports every transition on a bounded PeerEvent stream — the reaction ZeroMQ never offered. Responsibility moves at the API boundary, not at a size: send(body) is a message the runtime owns from the call, held in a bounded outbox until written to a live connection and never resent after that (at-most-once, ZeroMQ's contract); open is a stream that waits for a live peer and otherwise stays the application's, which abandons it on loss and opens a new one. No session state, no counter across the reconnect, no resend of a stream — 0008 §4.5 and 0023 §6 hold; only "nothing reconnects" is withdrawn. At-least-once at L0 and idempotent request replay are named and left open. |
| 0032 | Where an identity comes from, and how a process is handed one | provisional | An identity becomes a source rather than a value: IdentitySource/TrustSource with a current value and a change signal, a rustls resolver that serves a rotated certificate to the next handshake without re-binding, and IdentityEvent in PeerEvent's shape. Three sources in weida — Ephemeral (the default, today's generate), Static, Files (owner-only bootstrap, mtime reload, which is how Vault Agent, cert-manager, certbot and SPIRE all plug in) — and weida-openbao beside it with the HTTP client the core does not carry: Auth::{Token, AppRole, Handoff}, PkiSign (local key, CSR, pki/sign, renewal under an unchanged key so the fingerprint and 0031's redial survive rotation), PkiAnchor, Kv; PkiIssue refused because it rotates the key. The hand-off is OpenBao's response wrapping delivered through systemd's credentials directory: a single-use, seconds-lived wrapping token the service redeems first, where a second redemption is a defined error and therefore an alarm. Revocation stays unchecked and is said so. |
| 0033 | The website moves out of the tree, and its rules go with it | provisional | The owner decided on 2026-09-26 that weida.doodleshnookie.net is a separate Zola site in its own repository, beside the hub and the zeughaus site, and the premise 0025 was built on had gone with the first publish: 0.1.0-alpha.1 and 0.1.0-alpha.2 of weida, weida-core, weida-protocol, weida-runtime and weida-winpipe, and 0.1.0-alpha.2 of weida-broker, weida-raft and weida-openbao, are on crates.io. The site imports this repository's documents at build time (sync/import.py there), so 0025's rules move rather than lapse: normative text is never rewritten, and the landing page is the only designed page and quotes README.md and cargo metadata; published-or-excluded, with the same exclusion list, and the link rules — excluded documents link into the repository, a dangling link fails — live in the sync script; the pre-release notice is replaced by the release state read from crates.io at build time; the site has self-hosted web fonts and no JavaScript; Zola renders with pulldown-cmark too, so the dialect is still rustdoc's. The named loss is that the site is no longer in cargo test --workspace: the sync script is the gate on the site side, and a broken link is found when the site is next built rather than when the commit is made. crates/site is removed and the workspace has 25 members. |