From 38e0c03fb6bb6c398dde97ebafa21a775deeff10 Mon Sep 17 00:00:00 2001 From: Nils Lehnen <30603423+iderex@users.noreply.github.com> Date: Tue, 25 Aug 2026 05:15:42 +0200 Subject: [PATCH 1/2] Record the language, the toolchain and the binding layer (#11) Entry 2 of #1 was answered on 2026-08-24 with Rust and a foreign function interface per platform, and this record is the reasoning that an answer to a question cannot carry: what the choice costs in build legs, runtimes and a generated binding, what it forecloses, and what would reverse it. The half that is new rather than transcribed is the last section but one. Eleven landed records wrote conditions against a language nobody had chosen, and each is answered here from a program compiled against the toolchain rather than from recollection. Four are met: the two lanes 0009 owns, the connection attempt 0027 and 0069 need bounded separately from a read, the single construction point 0037 requires, and the per-field classification 0071 rests on. Three are not: no source of unpredictable bytes on a stable build, no cryptographic digest, and no promise that a credential's bytes are cleared. Each of the three is left where the record that raised it already put it, and 0041's reversal condition is reached rather than glossed. The failure this prevents is the language being chosen by the first file. Without the record the choice is made by whoever needs to compile something, is then defended by the work resting on it, and those eleven conditions are met or missed by accident. The five absences it measures are the second half: a hole met at a call site is filled with whatever the person at that call site reached for, and naming them together before any call site exists makes them one question for #103 instead of five answers nobody compared. The exact pinned version stays with #14, the layout with #13, the target set with #113 and the dependency rule with #103, and the record says so rather than deciding them in passing. Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com> --- ...age-the-toolchain-and-the-binding-layer.md | 471 ++++++++++++++++++ docs/decisions/README.md | 1 + 2 files changed, 472 insertions(+) create mode 100644 docs/decisions/0011-the-language-the-toolchain-and-the-binding-layer.md diff --git a/docs/decisions/0011-the-language-the-toolchain-and-the-binding-layer.md b/docs/decisions/0011-the-language-the-toolchain-and-the-binding-layer.md new file mode 100644 index 0000000..2aa5ff6 --- /dev/null +++ b/docs/decisions/0011-the-language-the-toolchain-and-the-binding-layer.md @@ -0,0 +1,471 @@ +# 0011. The language, the toolchain, and the binding layer + +Date: 2026-08-24 + +Status: accepted. Supersedes nothing. Superseded by nothing. + +Issue: #11 + +## The decision + +The core is one Rust library, built and tested with the stable `cargo` toolchain +pinned in the tree, reaching each client through a foreign function interface +generated per platform, chosen because the properties the records already +standing depend on are ones a compiler refuses rather than ones a reviewer +remembers. + +## Where the answer came from, and what this record adds + +Entry 2 of #1 was answered on 2026-08-24, and the answer is the first of the four +candidates that entry priced: + + gh issue view 1 --repo Flowfin/core --json comments --jq '.comments[-1].body' | grep -A2 '^Entry 2:' + Entry 2: Rust with a foreign function interface per platform. One implementation + for eleven clients, memory-safe by default - which this issue itself names the + property that costs the most to buy any other way. The binding layer is generated + +That is the choice. What this record adds is the part an answer to a question +cannot carry: what the choice costs in this repository's own terms, what it +forecloses, what would reverse it, and whether the means meets each condition the +records that landed while nobody had chosen a language wrote against it. + +Eleven of those records name this issue: + + git grep -l '#11\b' -- docs/decisions | wc -l + 11 + +## What was measured, and on what + +Every answer below about the language was produced by compiling or running a +program against the toolchain named here, on this host: + + rustc -vV + rustc 1.97.0 (2d8144b78 2026-07-07) + binary: rustc + commit-hash: 2d8144b7880597b6e6d3dfd63a9a9efae3f533d3 + commit-date: 2026-07-07 + host: x86_64-pc-windows-msvc + release: 1.97.0 + LLVM version: 22.1.6 + +A reader reproduces a block below by saving the program in it under the file name +its first line carries and running the command beneath it. The version is stated +because several of the answers are version-dependent, and one of them is a feature +expected to stabilise. + +## The toolchain + +`cargo` is the build tool, the test runner and the dependency resolver, and it +arrives with the compiler rather than being a second thing to install: + + cargo --version + cargo 1.97.0 (c980f4866 2026-06-30) + +Two of the gate's check names in M2 are toolchain components rather than new +tools. `rustfmt` answers #18 and `clippy` answers #17: + + rustup component list --installed + cargo-x86_64-pc-windows-msvc + clippy-x86_64-pc-windows-msvc + rust-docs-x86_64-pc-windows-msvc + rust-std-x86_64-pc-windows-msvc + rustc-x86_64-pc-windows-msvc + rustfmt-x86_64-pc-windows-msvc + +#19 asks for a committed lockfile and a restore that refuses to change it, and the +flag that refuses is the build tool's own: + + cargo build --help | grep -E '^ +--(locked|offline|frozen)\b' + --locked Assert that `Cargo.lock` will remain unchanged + --offline Run without accessing the network + --frozen Equivalent to specifying both --locked and --offline + +The edition is 2024. Every program in this record compiled under `--edition 2024` +on the compiler above. + +Which exact version is pinned, and in which file, is #14 and is not decided here. +A version written into this record would be a second declaration of the same fact, +and the two would disagree at the first upgrade. + +## What the choice costs + +**One build leg per target triple.** The library is compiled for each platform a +client runs on, and the compiler knows this many of them: + + rustc --print target-list | wc -l + 322 + +That number is not the gate's; which triples the gate builds and tests is #113, +and this record says only that the count is a per-target count rather than one. +The suite is the other half: the core's own tests are one run on one host, because +they test the library rather than the binding, while the conformance suite in #76 +is per-client by construction. + +**One runtime a contributor installs, and a second one for one gate leg.** The +toolchain above is the whole requirement for building and testing the core. The +exception is the detector in #117, which is measured below and needs a nightly +compiler. + +**A binding layer that is code.** An interface generated per platform is a +generated artefact crossing a boundary the compiler stops checking, so it is +tested rather than assumed, which is what the answer to entry 2 itself says. Which +generator produces it is not decided here: a generator is a dependency, the rule +that admits a dependency is #103, and choosing one before that rule exists is the +case #103 was opened against. + +**The licence reaches the clients through linking.** Entry 1 of #1 is answered by +the fleet-wide AGPL-3.0-or-later answer, and the repository already publishes +under it: + + gh api repos/Flowfin/core --jq '.license.spdx_id' + AGPL-3.0 + +A client embeds this library, so entry 2's answer is what carries entry 1's answer +to eleven clients rather than stopping at this repository. That follows from the +pair and is not a decision of this record. + +## What the standard library supplies, and what it does not + +This section exists because the difference decides how large the dependency graph +in #103 has to be, and because several records already standing rest on one side +of it or the other. + +**A duration type, unsigned and in nanoseconds.** + + // dur.rs + use std::time::Duration; + fn main() { + println!("size_of::() = {}", std::mem::size_of::()); + println!("Duration::MAX = {:?}", Duration::MAX); + println!("as_nanos(1s) = {}", Duration::from_secs(1).as_nanos()); + } + + rustc --edition 2024 -O -o dur dur.rs && ./dur + size_of::() = 16 + Duration::MAX = 18446744073709551615.999999999s + as_nanos(1s) = 1000000000 + +It cannot hold a negative value, and what refuses one is the type rather than a +convention: + + // durneg.rs + use std::time::Duration; + fn main() { let _ = Duration::new(-1i64, 0); } + + rustc --edition 2024 -o durneg durneg.rs + error[E0308]: mismatched types + --> durneg.rs:2:35 + | + 2 | fn main() { let _ = Duration::new(-1i64, 0); } + | ------------- ^^^^^ expected `u64`, found `i64` + = note: `-1i64` cannot fit into type `u64` + +**A connection attempt bounded separately from a read, against an address that +exists before the connection does.** + + // net.rs + use std::net::{TcpStream, ToSocketAddrs, SocketAddr}; + use std::time::Duration; + fn main() { + let addrs: Vec = "localhost:9".to_socket_addrs().unwrap().collect(); + println!("resolved before any connect: {addrs:?}"); + let f: fn(&SocketAddr, Duration) -> std::io::Result = TcpStream::connect_timeout; + let s: fn(&TcpStream, Option) -> std::io::Result<()> = TcpStream::set_read_timeout; + println!("connect_timeout takes a resolved address: {}", f as usize != 0); + println!("set_read_timeout is a separate bound: {}", s as usize != 0); + } + + rustc --edition 2024 -o net net.rs && ./net + resolved before any connect: [[::1]:9, 127.0.0.1:9] + connect_timeout takes a resolved address: true + set_read_timeout is a separate bound: true + +**A processor count whose floor is in the type.** + + // par.rs + fn main() { println!("{}", std::thread::available_parallelism().unwrap()); } + + rustc --edition 2024 -o par par.rs && ./par + 32 + +The return type is a non-zero integer, so the floor of one that 0009 requires for +the processing lane cannot be lost in the subtraction that sizes it. + +**No source of unpredictable bytes on the stable compiler.** There is one in the +standard library and a stable build cannot reach it: + + // rnd.rs + fn main() { let x: u128 = std::random::random(); println!("{x}"); } + + rustc --edition 2024 -o rnd rnd.rs + error[E0658]: use of unstable library feature `random` + --> rnd.rs:1:27 + = note: see issue #130703 for more information + +**No cryptographic digest.** The standard library offers one hasher, it is sixty +four bits wide, and it is not a digest: + + // dig.rs + use std::hash::{DefaultHasher, Hasher}; + fn main() { let mut h = DefaultHasher::new(); h.write(b"a"); println!("{:016x}", h.finish()); } + + rustc --edition 2024 -o dig dig.rs && ./dig + 407448d2b89b1813 + + // dig2.rs + fn main() { let _ = std::hash::Sha256::new(); } + + rustc --edition 2024 -o dig2 dig2.rs + error[E0433]: cannot find `Sha256` in `hash` + +**No transport security and no HTTP.** The networking module reaches TCP and +stops: + + // tls.rs + fn main() { let _ = std::net::TlsStream::connect("example:443"); } + + rustc --edition 2024 -o tls tls.rs + error[E0433]: cannot find `TlsStream` in `net` + +So certificate validation in #29, the transport in #27 and every request the core +makes rest on something outside the standard library, and what may be taken is +#103's rule. That is the largest single cost of this choice, and it is named here +rather than discovered at the first request. + +## The conditions the standing records put on the means + +Each record below wrote a condition against a language nobody had chosen. Each is +answered here with what was measured, or with the statement that it is not met. + +**0009, the concurrency model.** Two lanes the core owns, completion-based calls, +and no runtime hosted by the client. The lanes are operating-system threads, the +sizing input is the processor count above, and no scheduler outside the standard +library is required, so the model is carried without an executor and without the +hosted runtime that record's third alternative priced. **Met.** + +**0009's reversal condition, the detector in #117.** That record says the +calling-thread guarantee becomes an unproven claim if the detector cannot be run +on the toolchain chosen here. It can be run, and the answer carries two bounds +worth having in one place. It is a nightly compiler flag: + + // tsan.rs + fn main() { println!("x"); } + + rustc --edition 2024 -Zsanitizer=thread -o tsan tsan.rs + error: the option `Z` is only accepted on the nightly compiler + +and it is supported on some targets and not on others: + + for t in x86_64-unknown-linux-gnu aarch64-apple-darwin aarch64-apple-ios aarch64-linux-android x86_64-pc-windows-msvc; do + printf '== %s\n' "$t" + rustup run nightly rustc -Zunstable-options --print target-spec-json --target "$t" \ + | sed -n '/supported-sanitizers/,/]/p' | grep '"thread"' || echo ' no thread sanitizer' + done + == x86_64-unknown-linux-gnu + "thread", + == aarch64-apple-darwin + "thread", + == aarch64-apple-ios + "thread", + == aarch64-linux-android + no thread sanitizer + == x86_64-pc-windows-msvc + no thread sanitizer + +**Met, on a second toolchain and not on every target.** The claims 0009 makes are +about the core's own code rather than about a platform, so a run on a host that +supports the detector verifies them. What stays out of reach is a race that +manifests only on Android or on Windows, and #117 states that bound rather than +reporting a clean run over a set it did not cover. + +**0027 and 0069, a bounded connection attempt and a connection seen before it is +made.** Both are the networking measurement above: the attempt is bounded by one +call and the read by another, and the destination is a resolved address in hand +before anything is dialled. 0069's more serious reversal condition is not reached, +and #70 has something to observe. **Met.** + +**0030, a credential whose bytes can be cleared on a schedule the runtime +guarantees.** The point at which a value is dropped is fixed by the language +rather than by a collector, so the timing half exists. The overwriting half does +not: nothing in the standard library promises to erase the bytes of a string +before its allocation is released, and the compiler is free to have copied them +first. This is a claim rather than a measurement, because the reading that would +prove it is a read of freed memory. **Not met**, so 0030's residual stands exactly +as that record wrote it and its reversal condition is not triggered. + +**0032 and 0036, a source of unpredictable bytes of at least 128 bits.** Measured +above: the standard library's source is unstable on the stable compiler. **Not met +on the toolchain as chosen**, so the seam 0032 already named is what is used, the +client supplies the bytes, and 0036 pays for it a second time on the device +identity. Neither record is superseded, because both wrote this outcome as a case +rather than as a failure. It is not the state 0032's reversal condition describes +either: what that condition refuses is no client on any platform being able to +supply the bytes, and every platform in view has such a source. + +**0037, one construction point for the failure set.** A value of the set cannot be +built outside the module that owns it, and the compiler is what refuses: + + // errset.rs, built as a library + pub struct Failure(Kind); + #[non_exhaustive] + pub enum Kind { NotAuthenticated, Unreachable } + impl Failure { + pub fn map(io: &std::io::Error) -> Failure { + match io.kind() { + std::io::ErrorKind::PermissionDenied => Failure(Kind::NotAuthenticated), + _ => Failure(Kind::Unreachable), + } + } + } + + // caller.rs, built against it + extern crate errset; + use errset::{Failure, Kind}; + fn main() { let _ = Failure(Kind::Unreachable); } + + rustc --edition 2024 --crate-type lib --crate-name errset -o liberrset.rlib errset.rs + rustc --edition 2024 --extern errset=liberrset.rlib -o caller caller.rs + error[E0423]: cannot initialize a tuple struct which contains private fields + --> caller.rs:4:13 + | + 4 | let _ = Failure(Kind::Unreachable); + | ^^^^^^^ + = note: constructor is not visible here due to private fields + +**Met**, and by a refusal rather than by a check over the tree, which is the +stronger of the two routes 0037 names. + +**0041 and 0105, a cryptographic digest without a dependency #103 refuses.** The +standard library has none, measured above. The requirement is therefore met by a +dependency or stated as unmet, and which of the two is #103's to answer rather +than this record's. **Not met by the toolchain alone**, and this is the sentence +0041's reversal condition asked for: that condition is reached, and what it calls +for is a new record about the digest rather than a line added to 0041. + +**0056, the duration type the first position-holding code will reach for.** The +measurement above is the case 0056 predicted: the type is unsigned, its resolution +is nanoseconds, and it does not agree with a tick. 0056 fixes the wire unit against +the server rather than against this type, so the conversion at the boundary is the +one that record already requires, now written against a measured shape rather than +against an unknown one. **Met, in the sense 0056 asked for**, which is that the +runtime's type does not decide the unit. + +**0071, per-field classification that is enforced rather than remembered.** A field +reaches the sink only through a trait it must implement, and a field whose +treatment nobody chose does not compile: + + // cls.rs + pub trait Classified { fn rendered(&self) -> String; } + pub struct ServerId(pub u32); + impl Classified for ServerId { fn rendered(&self) -> String { format!("server {}", self.0) } } + pub struct Password(pub String); // no impl: no treatment was chosen + pub fn emit(field: &dyn Classified) { println!("{}", field.rendered()); } + fn main() { emit(&ServerId(7)); emit(&Password("hunter2".into())); } + + rustc --edition 2024 -o cls cls.rs + error[E0277]: the trait bound `Password: Classified` is not satisfied + --> cls.rs:13:10 + | + 13 | emit(&Password("hunter2".into())); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound + +**Met.** 0071's default, that a field with no treatment is excluded, is a +compilation failure here rather than a convention, so that record is not claiming +a property nothing keeps. + +## What this record does not decide + +The exact toolchain version and where it is pinned. #14. + +The directory layout, the crate names, and the two commands a fresh clone runs. +#13. 0009 expects the type names for its per-kind statements to arrive with one of +those two issues, and #13 is the one with names in it. + +Which target triples the gate builds and tests, and what no run covers. #113. + +What a dependency has to be worth, which licences may appear in the graph, and +what is refused outright. #103. Three of the absences measured above are questions +that record answers and this one does not. + +Which digest function and which width. 0041 and 0105 already say this follows the +toolchain; what follows from the measurement here is that it cannot follow from +the toolchain alone. + +Which generator produces the binding layer. It is a dependency, so it is #103's +rule applied once that rule exists. + +## Why this is written down before the code + +The failure this record is against has a shape, and it is not that somebody picks +the wrong language. It is that the language gets picked by the first file. A +repository with no recorded means acquires one the first time somebody needs to +compile something, the choice is then defended by the work already done in it, and +the conditions above are met or missed by accident rather than checked. + +The second failure is narrower and more expensive. Five of the answers above are +absences: no unpredictable bytes on a stable build, no digest, no transport +security, no HTTP, and no promise about clearing a credential's bytes. Each is a +dependency-shaped hole, and a hole met at a call site is filled with whatever the +person at that call site reached for. Writing them down together, before the first +call site exists, is what makes them one question for #103 instead of five answers +nobody compared. + +## Alternatives, and what each cost + +The four candidates are entry 2 of #1's, and what follows is what each would have +cost against the conditions measured above rather than in general. + +Kotlin Multiplatform. Cheap on Android and on a Java desktop, and it carries a +cryptographic digest and a source of unpredictable bytes in its own standard +library, so two of the absences above would not exist. It costs an extra runtime +on Apple platforms and on a television, it decides the language of every client +rather than leaving that open, and 0030's residual gets worse rather than better, +because a collected runtime fixes neither the timing nor the overwriting of a +credential's bytes. + +C++. Reaches every target with the least ceremony and has the largest supply of +libraries for the five absences. Every memory-safety property then has to be +bought with tooling, review and sanitiser runs that are themselves gate legs +somebody maintains, and 0037's single construction point and 0071's per-field +classification become conventions a reviewer checks rather than refusals a +compiler makes. The property that decided against it is the one #1 itself names as +the most expensive to buy any other way. + +No shared code, a specification plus a conformance suite. It costs eleven +implementations of every condition above and makes the specification the artefact +that has to be perfect, because nothing else is shared. It also empties this +record: there is no means to choose, and 0009's guarantee about the calling thread +becomes eleven promises nobody can verify in one place. + +Rust with each client building the source rather than linking a built library. Not +in entry 2's list, and worth naming because it is the shape somebody proposes +next. It removes the binding layer and replaces it with a build of the core inside +every client's build system, which is eleven build integrations instead of one +generated interface, and it makes the version a client runs a property of that +client's checkout rather than of a released artefact. + +## What would reverse this + +A target the eleven clients need turns out not to be reachable by this compiler at +all, so that a client cannot link the core rather than paying a cost to. The +target list is what says whether this has happened, and it is a comparison against +a named triple rather than a judgement. + +The detector in #117 stops being available on every target that carries it today, +or is measured to be unusable against the core's own suite. 0009's reversal +condition is then reached through this record, the calling-thread guarantee has no +route to verification, and either the means changes or that guarantee is withdrawn +and written as a claim. This is the condition to watch, because it is the one this +record answers with a second toolchain rather than with the pinned one. + +#103's rule, once written, refuses every candidate for transport security, so that +the core cannot make a request at all under the licence and safety conditions that +rule sets. That is not a reason to change the language, and it is written here so +it is not read as one: it would mean the rule and the transport are in conflict and +one of the two records is wrong, which is a decision above both. + +The stable compiler gains a source of unpredictable bytes, which is the one absence +above that is expected to move; the tracking issue in the error text is where that +is decided. When it lands in a pinned stable version, the seam 0032 and 0036 +describe stops being necessary, and both of those records are superseded by one +that takes the source directly rather than this record being edited. diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 12c59aa..002f07f 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -12,6 +12,7 @@ allocated, and why a record is superseded rather than edited are in - [0007. A slow server and a server that is gone](0007-a-slow-server-and-a-server-that-is-gone.md) - [0008. What the core can measure of the speed budget](0008-what-the-core-can-measure-of-the-speed-budget.md) - [0009. The concurrency model, and what the core promises about threads](0009-the-concurrency-model.md) +- [0011. The language, the toolchain, and the binding layer](0011-the-language-the-toolchain-and-the-binding-layer.md) - [0027. The transport's timeouts, its connection limit, and connection reuse](0027-the-transports-timeouts-and-connections.md) - [0028. The address a person typed, and how every path is joined to it](0028-the-address-a-person-typed.md) - [0029. Certificate validation, and the server an operator signed themselves](0029-certificate-validation-and-the-self-signed-server.md) From 955b54f3f607f0ca9ced9399d541b58580ac175c Mon Sep 17 00:00:00 2001 From: Nils Lehnen <30603423+iderex@users.noreply.github.com> Date: Tue, 25 Aug 2026 05:17:28 +0200 Subject: [PATCH 2/2] Record what admits a dependency and what is refused (#103) Both entries this waited on are answered. The repository publishes under AGPL-3.0-or-later, so the licence set can be named as a list rather than as a principle, and 0011 measures which facilities the means does not supply, so the rule is written against the holes it actually has to cover. The record answers the five questions the issue asks and adds a sixth the issue does not. 0041 requires a cryptographic digest, 0105 rests on the same requirement, and the standard library has none, so a landed record already needs a dependency. A rule written without that case would refuse the digest under a clause nobody wrote for it, and the repair would be a supersession of two records rather than a line here. The clause admits a dependency on the authority of the record that stated the requirement, and is bounded to that requirement so a package cannot arrive carrying a second thing. The four outright refusals are the ones the issue names, each tied to the record it would overturn: 0069 for a node with its own network reach, 0040 for one that picks its own storage location, 0009 for one that starts a thread the core does not own, and 0071 with 0100 for one that writes to a log. A fifth is added from a worked case rather than from a principle: 0061 refused a tracing library partly because its spans carry attributes, so a redaction rule would have to reach a second facility. The failure this prevents is a rule with ten exceptions in it. There is no code and therefore no graph, which is the only moment the rule can be written without each clause being argued against work already resting on it. Nothing in this repository refuses a dependency admitted by no clause, and the record says so in its own text rather than leaving a reader to assume a mechanism. Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com> --- ...admits-a-dependency-and-what-is-refused.md | 266 ++++++++++++++++++ docs/decisions/README.md | 1 + 2 files changed, 267 insertions(+) create mode 100644 docs/decisions/0103-what-admits-a-dependency-and-what-is-refused.md diff --git a/docs/decisions/0103-what-admits-a-dependency-and-what-is-refused.md b/docs/decisions/0103-what-admits-a-dependency-and-what-is-refused.md new file mode 100644 index 0000000..9216fae --- /dev/null +++ b/docs/decisions/0103-what-admits-a-dependency-and-what-is-refused.md @@ -0,0 +1,266 @@ +# 0103. What admits a dependency, and what is refused + +Date: 2026-08-24 + +Status: accepted. Supersedes nothing. Superseded by nothing. + +Issue: #103 + +## The decision + +A dependency enters this core only where writing the equivalent here would cost +more than carrying somebody else's release cadence, security response and licence +for as long as the core lives; its licence is one of the set named below; it does +nothing on its own that a record here decided the core would do deliberately; and +it enters with the clause that admitted it and the evidence that would retire it +written beside it. + +## Why the rule is more than the usual one + +Every dependency this core takes is taken eleven times, because a client embeds +the core and embeds its graph. That is #103's own opening sentence, and it is what +makes the ordinary test insufficient: a dependency that saves an afternoon here is +a binary-size negotiation on a television, a store review somebody else sits +through, and a licence obligation in a repository this one never sees. + +Two of the questions the rule needs answered were open until 2026-08-24 and are +answered now. The licence this core publishes under: + + gh api repos/Flowfin/core --jq '.license.spdx_id' + AGPL-3.0 + +and the means, which decides how large a graph the core needs at all. 0011 +measures five absences in the standard library, and each is a dependency-shaped +hole this record is the rule for. + +## What a dependency has to be worth + +The test is not whether it saves work today. It is whether writing the equivalent +here would cost more than carrying it, over the life of the core, where carrying +it means all four of the following at once. + +Its release cadence. Every release is a change the gate has to run against, and a +dependency that releases weekly is a weekly interruption whether or not the core +needed anything in it. + +Its security response. A dependency is a party this core now depends on to answer +an advisory, and the core inherits whatever answer it gives, including silence. +#19 is what notices an advisory; nothing makes somebody else fix one. + +Its licence, in eleven client repositories rather than in this one. + +Its reach. A dependency that pulls its own graph is not one dependency. The count +that matters is the transitive one, and it is read rather than assumed. + +Against that, what writing it here costs: the code, the tests, the review, and the +same security response for the core's own account. Where the thing being replaced +is small, well specified, and already has a test somebody would write anyway, the +answer is usually to write it. Where it is a protocol, a parser of somebody else's +format, or a cryptographic primitive, the answer is usually not, because a wrong +implementation of any of the three is a defect nobody sees until it is exploited. + +## The licence set + +Named explicitly, because a general principle is a thing two readers apply +differently. + +Admitted anywhere in the graph, shipping tree or test tree: `MIT`, `Apache-2.0`, +`BSD-2-Clause`, `BSD-3-Clause`, `ISC`, `Zlib`, `Unlicense`, `CC0-1.0`, `MPL-2.0`, +`LGPL-2.1-or-later`, `LGPL-3.0-or-later`, `GPL-2.0-or-later`, `GPL-3.0-or-later`, +`AGPL-3.0-or-later`. A dual offer that includes one of these is admitted on that +member, which is how the two most common offers in this ecosystem arrive. + +Refused: `GPL-2.0-only`, and every other licence whose terms cannot be satisfied +inside a work distributed under AGPL-3.0-or-later. Refused for a second reason, +which is that they are not free software licences at all whatever their terms say +about source: `SSPL-1.0`, `BUSL-1.1`, `Elastic-2.0`, anything carrying the Commons +Clause, and every licence with a non-commercial or field-of-use restriction. +Refused, finally, and this is the one that arrives most often: a package with no +licence statement at all, which is not permissive by default but reserved by +default. + +Why the set is shaped this way. The core is distributed under +AGPL-3.0-or-later, so the one-way compatibility is the direction that matters: a +permissive or weak-copyleft node can be carried inside this work, and a node whose +own terms forbid the conditions AGPL-3.0-or-later imposes cannot, whichever way +round it is more convenient. `GPL-2.0-only` is named rather than left to the +reader because it is the one widely used free licence in the refused half, and the +`-only` is the whole of the difference. + +What this set does not settle is a node whose licence is stated one way in its +manifest and another way in its own tree. The manifest is what a tool reads and +the tree is what a court reads. Where the two disagree the node is refused until +they agree, because a licence nobody can state is the same problem as none. + +## What is refused outright, whatever it is worth + +Four behaviours, each of which overturns a decision this board took somewhere +else, so that admitting the dependency would move a decision without a record. + +A dependency that reaches the network on its own. 0069 decides every host the core +may contact and #70 is the test that fails when it reaches one nobody configured. +A node with its own client, its own updater, or its own exporter defeats both, and +the defeat is invisible in a diff. + +A dependency that reads or writes the filesystem without being told where. 0040 +puts the storage location in the client's hands, and a node that picks its own +cache directory has taken that decision away from eleven client authors who each +had a reason. + +A dependency that starts a thread the core did not ask for. 0009 says two lanes +and nothing else, no work posted to a shared pool and no timer thread of its own. +A node with a background worker is a third lane the core does not own, cannot size +and cannot stop, and #115's stop call then returns while something is still +running. + +A dependency that writes to a log. 0100 and 0071 decide what leaves the core and +in what shape, and a node writing to a global logger is a second exit for exactly +the values 0071 classifies field by field. + +A fifth ground, which 0061 supplied by refusing a real candidate rather than being +argued from a principle. A dependency that carries its own field-bearing surface +makes a rule in another record reach a second place: a tracing library's spans +carry attributes, so #71's redaction rule would have to cover a second facility +rather than one. The cost has nothing to do with what the dependency does on its +own, and it is the ground least likely to be noticed at the moment of taking one. + +The three refusals with a worked case behind them are all quotable. 0061 refuses a +tracing library, and 0112 refuses a cross-platform media framework on size on +every target: + + git grep -l '#103' -- docs/decisions + docs/decisions/0041-how-a-cache-key-is-built.md + docs/decisions/0061-the-span-facility.md + docs/decisions/0112-where-the-platform-decoder-begins.md + +## A dependency a record already standing requires + +The list above is about what a dependency has to be worth and what is refused, and +neither question is the one 0041 asks. That record requires a cryptographic +digest, 0105 rests on the same requirement, and 0011 measures that the toolchain +offers none. So a landed record already needs a dependency, and a rule written +without that case would refuse the digest under a clause nobody wrote for it. + +The clause. Where a record that has already landed states a requirement the means +cannot meet, a dependency meeting exactly that requirement is admitted on the +record's authority rather than on this one's, provided it is the smallest thing +that meets it, its licence is in the set above, and none of the five outright +refusals applies to it. What this record contributes in that case is the licence +set and the four behaviours, not the judgement of worth, because the worth was +already decided by the record that stated the requirement. + +The bound on that clause. It admits a dependency for the requirement the record +states and nothing else in the same package. A package offering a digest and a +transport is admitted for the digest only if the transport is separable; where it +is not, the transport is a second dependency and is judged on its own terms. + +## The test tree and the shipping tree + +They carry different risk and one rule for both is either too strict for the suite +or too loose for what an operator installs. + +In the shipping tree, everything above applies in full. + +In the test tree, the worth test is relaxed and nothing else is. A test-only +dependency is not distributed, so its size on a television, its release cadence in +eleven client repositories and its behaviour on an operator's machine are not +costs anybody pays. What still applies without relaxation: the licence set, because +a licence obligation attaches to what is distributed and a test fixture can end up +distributed by accident; and the four behaviours, because a node that starts a +thread or reaches the network inside the suite is a node that makes the suite's +verdict depend on something outside the run. A flaky gate is the specific failure, +and it is worse than a slow one because it teaches people that red means nothing. + +What no relaxation reaches: a dependency that is in the test tree today and the +shipping tree tomorrow is judged as a shipping dependency on the day it moves, not +grandfathered by having been there. + +## How one leaves + +A dependency with no stated removal condition is permanent, so one is written when +it enters, beside the clause that admitted it. + +The condition is written so a reader can look at the world and say whether it has +happened, in the same sense a reversal condition in any record here is. Three +shapes cover almost every case. The means grows the facility: 0011 already names +this for the source of unpredictable bytes, where a stable compiler gaining one +retires the dependency and the client seam together. The requirement disappears: a +record is superseded and what it required is no longer required. The cost turns: +the dependency's transitive count, its size on the smallest target, or its +advisory history crosses a number that was written down when it entered. + +"When we no longer need it" is not such a condition and is refused as one. + +## Where the line lives + +Every dependency carries, beside its entry in the manifest, one line naming the +clause of this record that admitted it and one naming what would retire it. The +manifest rather than a separate document, because a separate list is a thing that +drifts from the graph it describes, and the drift is invisible until somebody +audits it. + +#87 produces the bill of materials, which is where the graph is read back, and #19 +is what refuses a graph that does not match the committed lockfile or that carries +a known advisory. Neither reads this record, and nothing in this repository refuses +a dependency admitted by no clause today. The rule is carried by the review and by +the line beside the entry until something reads it. + +## Why this is written down before the code + +There is no code and therefore no graph, which is the only moment this record can +be written honestly. A rule about dependencies written after the first ten exist is +a rule with ten exceptions in it, and each exception is defended by the work +already resting on it. + +The specific failure it prevents is narrower than that and is already visible in +this tree. 0011 measures five absences: no source of unpredictable bytes on a +stable build, no cryptographic digest, no transport security, no HTTP, and no +promise about clearing a credential's bytes. Each is a hole that will be met by +somebody at a call site, and a person at a call site takes the first package that +compiles. Five separate answers nobody compared is the outcome this record exists +to replace with one question asked five times. + +## Alternatives, and what each cost + +No rule, deciding each dependency in its own pull request. The cheapest, and it is +what happens by default. It costs consistency in the direction that matters least +and predictability in the direction that matters most: a contributor cannot tell +before doing the work whether the work will be accepted, so the rule is discovered +at review time, which is the most expensive place to discover anything. + +A number instead of a test, a cap on the transitive count. Checkable by a machine, +which is the strongest argument for anything here. It refuses a small graph of +three excellent nodes and admits a large graph of one bad one, and the number +would be chosen without any graph to choose it against. + +An allow-list of named packages. The most predictable of all and the easiest to +enforce, and it is the one to revisit once a graph exists. Today it would be a +list of nothing, maintained by whoever is asked, and every addition would be this +same argument with no rule to have it against. + +A licence rule only, leaving worth and behaviour to review. It covers the risk that +is hardest to undo, since a licence obligation is not repaired by deleting the +dependency later. It leaves the four behaviours entirely to a reviewer noticing +them, and a background thread inside a package is exactly what a reviewer does not +notice. + +## What would reverse this + +The licence set refuses a dependency that a landed record requires and nothing +admitted by the set can meet. Then the set and the record are in conflict, and +which of the two moves is a decision above both rather than an exception written +into either. + +A dependency admitted under the clause for a standing requirement is measured to +carry more than the requirement, twice. One case is a package judged wrongly; two +is the clause being wider than it reads, and it is replaced by one naming what a +package may contain beside the thing it was taken for. + +Something in this repository begins to read the line beside a manifest entry, for +example under #87 or #19. This record is then superseded by one describing what is +refused rather than what is expected, because a rule nothing refuses is a +suggestion and this record says so of itself. + +The core stops being distributed as something a client links, so that the graph is +no longer taken eleven times. The multiplier is the whole argument for the strength +of this rule, and without it the rule is stricter than its reason. diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 002f07f..bc07766 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -48,6 +48,7 @@ allocated, and why a record is superseded rather than edited are in - [0100. The diagnostics interface, and its relation to measurement spans](0100-the-diagnostics-interface.md) - [0101. What the core trusts, and what it is built to survive](0101-what-the-core-trusts.md) - [0102. The clocks every deadline is measured against](0102-the-clocks-every-deadline-is-measured-against.md) +- [0103. What admits a dependency, and what is refused](0103-what-admits-a-dependency-and-what-is-refused.md) - [0105. An entry this version did not write, and one that was not finished](0105-an-entry-this-version-did-not-write.md) - [0111. Which source is played, and what the handover carries](0111-which-source-is-played-and-the-handover.md) - [0112. Where the platform decoder begins](0112-where-the-platform-decoder-begins.md)