diff --git a/PLANS.md b/PLANS.md index a972e82bf..2e17f06eb 100644 --- a/PLANS.md +++ b/PLANS.md @@ -11,6 +11,7 @@ plans tracker: [crates/windows-file-enumeration-sys/PLANS.md](crates/windows-fil [crates/windows-platform-probes/PLANS.md](crates/windows-platform-probes/PLANS.md), [crates/windows-thread-ambient-sys/PLANS.md](crates/windows-thread-ambient-sys/PLANS.md), [crates/windows-threadpool-sys/PLANS.md](crates/windows-threadpool-sys/PLANS.md), +[crates/topology-planner/PLANS.md](crates/topology-planner/PLANS.md), [crates/windows-topology-sys/PLANS.md](crates/windows-topology-sys/PLANS.md), [crates/windows-waitable-queues/PLANS.md](crates/windows-waitable-queues/PLANS.md), and [crates/wtf-string/PLANS.md](crates/wtf-string/PLANS.md). Checklists whose work is finished move to diff --git a/crates/topology-planner/CHECKLIST.md b/crates/topology-planner/CHECKLIST.md index 3273e095f..4ca82654f 100644 --- a/crates/topology-planner/CHECKLIST.md +++ b/crates/topology-planner/CHECKLIST.md @@ -32,7 +32,7 @@ prerequisites rather than on someone else's decision. | Milestone | State | What it is waiting on | |---|---|---| -| M1 the input contract | 4 done, 1 open | `EP-1.5`'s coverage half, which wants a settled model | +| M1 the input contract | 3 done, 2 open | `EP-1.4` and `EP-1.5`'s coverage half, which want a settled model | | M1+ scenario and naming | **partly answered** | the name is settled (EP-D-4); the goal input is deferred for litigation, by direction | | M2+ the plan as a value | parked, **and needs re-cutting** | re-cut against EP-D-4/EP-D-5, then the topology reshape landing | | M3+ the policies | parked | M2+ | @@ -45,23 +45,7 @@ consumers, and **this crate is the consumer**. Answering in the abstract has alr wrong answer this session. Each item below states a query the planner makes, why it makes it, and whether the topology can answer it today -- so the model is designed against a real caller. -- [x] **EP-1.1** -- **The shard-set query.** Which processors may host a domain: online, with - identity carried as `(group, number)` rather than a bare number, with efficiency class and SMT - structure available so a policy can choose one domain per core or per thread and can decide - whether efficiency cores are peers. **Gap already identified:** parked and allocated state is not - available at all, and pinning a domain to a parked processor is a defect a client cannot detect. - Tracked as `SH-16.10`. - **Done:** stated as [EP-D-1](DESIGN-NOTES.md#ep-d-1), with each of its five inputs checked against - the model rather than assumed. Three are answered cleanly; availability is not answered at all; - and the fourth turned up a defect the item had not anticipated. - **`Processor::capacity` is unsafe for reading efficiency class.** It is - `online.then(find owning Core).flatten().unwrap_or(0)`, so `0` means offline, *or* in no core - domain, *or* genuinely class zero -- and the third is every processor on every non-hybrid machine, - so the sentinel collides with the common legitimate value. Worse here than elsewhere, because - Windows orders class `0` as *least* performant: on a hybrid part an unknown processor is - indistinguishable from an efficiency core, so a policy excluding them silently drops a possible - performance core and a policy tiering them mis-tiers it. Neither fails a functional test. Filed - against the owning crate as `SH-16.12`; use `DomainKind::Core { efficiency_class }` meanwhile. +- [x] **EP-1.1** -- The shard-set query requirements and discovered efficiency-class sentinel defect are recorded. -> [completed 2026-09-18 18:50:30 +00:00](COMPLETED-CHECKLIST.md#ep-11) - [x] **EP-1.2** -- **The proximity query, which is the crux.** For an ~~*ordered pair*~~ **unordered pair** of processors, how close are they -- because that is what chooses SPSC versus @@ -86,10 +70,10 @@ whether the topology can answer it today -- so the model is designed against a r - [x] **EP-1.3** -- **The residency query.** Which memory domain each processor belongs to, and -- for a pair spanning two of them -- what it costs to place a shared buffer on one side rather than - the other. **Gap already identified:** `MachineMemoryTopology::distances` exists, is never populated, and Win32 - cannot populate it; the measurement exists in `windows-placement-probe` and reaches nothing. - Tracked as `SH-16.11`. The probe measures this per node pair with a dedicated ring-placement - column precisely because it was found to matter. + the other. **Gap already identified:** [D-20](../windows-topology-sys/DESIGN-NOTES.md#d-20) + removed `MachineMemoryTopology::distances` at the Win32 boundary, so this cost has to enter through + the abstract model via the inward adapter/synthesizer path. That contract is still missing and + remains tracked as `EP-1+.4`. **Done:** stated as [EP-D-3](DESIGN-NOTES.md#ep-d-3). This is where the direction EP-1.2 refused lands -- proximity is the link and symmetric, residency is the hop and is not. The processor-to-node half is answered, with one asymmetry worth preserving: an unknown *cache* @@ -97,17 +81,10 @@ whether the topology can answer it today -- so the model is designed against a r pool must be allocated somewhere and guessing means quietly allocating remote memory for the life of the process. `windows-placement-probe` already refuses on the second while tolerating the first, and that judgement was correct. - **The cost half needs SH-16.11 restated, and it was.** That item read as though someone had - forgotten to populate a field. Two sharper problems replace it: `distances` can never carry - `Measured` provenance **by construction** -- its only inputs are a literal (`Synthetic`) and a file - (capped at `Restored`) -- so populating it would not help; and even populated it is SLIT-shaped, - one symmetric workload-independent scalar, while the question is directional. `D-9` in the - topology crate already deferred the attributed edge list that would answer it, naming *asymmetry* - among what it would absorb, with the trigger being that a scalar "demonstrably mismodels a machine - somebody is tuning for" -- and this planner is that machine-tuner. - **The trigger is approached, not met**, and the gap is a measurement nobody here can take: both - development hosts are single-node, so every directional run prints "VACUOUS ON THIS MACHINE". - Recorded so D-9 is reopened on evidence rather than on argument. + **The cost half is now scoped to the current boundary.** The planner still needs directed + residency-cost input plus measurement context, but those facts no longer live in + `windows-topology-sys`; they are requirements on `topology-model` and the adapter that populates it. + Historical trigger analysis remains in [DESIGN-RATIONALE.md](DESIGN-RATIONALE.md#ep-d-3-rationale-and-history). - [ ] **EP-1.4** -- **What the planner does with an unanswered query**, given the model's bar is that it answers without further measurement. A fact that was not observed cannot be acquired at @@ -180,8 +157,15 @@ model question -- they are this component's own. are graphs of processors and their relations, so "topology" fits all of them and distinguishes none -- and a reader seeing the word twice will eventually take one for the other. Decide whether the observed machine keeps the bare name (qualified only by its crate), gains a qualifier, or is - renamed outright, and what the synthesized arrangement is called. Cheap now; expensive once either - name is public. This one blocks nothing but should not be settled by whoever writes the first type. + renamed outright; what the synthesized arrangement is called; and whether the inward/outward + adapters keep those role names or gain more specific crate/type names. Cheap now; expensive once + any of those names are public. This one blocks nothing but should not be settled by whoever writes + the first type. + +- [ ] **EP-1+.4** -- **Assign measurement ownership for directed residency cost in the four-part + architecture.** [EP-D-3](DESIGN-NOTES.md#ep-d-3) requires directed cross-domain cost input with + measurement context. Record which layer owns collecting, validating, and supplying that measurement + context to `topology-model` through the inward adapter/synthesizer path. ## M2+: the plan as a value diff --git a/crates/topology-planner/COMPLETED-CHECKLIST.md b/crates/topology-planner/COMPLETED-CHECKLIST.md new file mode 100644 index 000000000..c6f61c364 --- /dev/null +++ b/crates/topology-planner/COMPLETED-CHECKLIST.md @@ -0,0 +1,25 @@ +# Completed checklist + +## Moved 2026-09-18 18:50:30 +00:00 -- Archive completed EP-1.1 details + +### EP-1.1 -- The shard-set query requirements and discovered efficiency-class sentinel defect are recorded. *(completed 2026-09-18 18:50:30 +00:00)* + +- [x] **EP-1.1** -- **The shard-set query.** Which processors may host a domain: online, with + identity carried as `(group, number)` rather than a bare number, with efficiency class and SMT + structure available so a policy can choose one domain per core or per thread and can decide + whether efficiency cores are peers. **Gap already identified:** parked and allocated state is not + available at all, and pinning a domain to a parked processor is a defect a client cannot detect. + Tracked as [CHECKLIST-ship-topology-and-queues.md](../../CHECKLIST-ship-topology-and-queues.md) + -> `SH-16.10`. + **Done:** stated as [EP-D-1](DESIGN-NOTES.md#ep-d-1), with each of its five inputs checked against + the model rather than assumed. Three are answered cleanly; availability is not answered at all; + and the fourth turned up a defect the item had not anticipated. + **`Processor::capacity` is unsafe for reading efficiency class.** It is + `online.then(find owning Core).flatten().unwrap_or(0)`, so `0` means offline, *or* in no core + domain, *or* genuinely class zero -- and the third is every processor on every non-hybrid machine, + so the sentinel collides with the common legitimate value. Worse here than elsewhere, because + Windows orders class `0` as *least* performant: on a hybrid part an unknown processor is + indistinguishable from an efficiency core, so a policy excluding them silently drops a possible + performance core and a policy tiering them mis-tiers it. Neither fails a functional test. Filed + against the owning crate as [CHECKLIST-ship-topology-and-queues.md](../../CHECKLIST-ship-topology-and-queues.md) + -> `SH-16.12`; use `DomainKind::Core { efficiency_class }` meanwhile. diff --git a/crates/topology-planner/COMPONENT.md b/crates/topology-planner/COMPONENT.md index 439bac093..65541d650 100644 --- a/crates/topology-planner/COMPONENT.md +++ b/crates/topology-planner/COMPONENT.md @@ -37,7 +37,9 @@ is not yet known, and knowing them is what decides whether that is one trait or | the outward adapter (the realizer) | Windows | `topology-model`, the runtime crates | `topology-model` holds the abstract machine description, **the traits the planner queries**, and -**the plan type**. Everything depends on it; **nothing depends on this crate**. +**the plan type**. Everything depends on it; components that only describe or realize topology +depend on `topology-model` and do not depend on this crate. Callers that need planning policy do +depend on this crate. That is the whole point of the arrangement. If the traits lived here, an adapter whose only job is to describe a machine would have to depend on a planner, and anyone wanting to read a topology would diff --git a/crates/topology-planner/DESIGN-NOTES.md b/crates/topology-planner/DESIGN-NOTES.md index 430306c9e..448444bfc 100644 --- a/crates/topology-planner/DESIGN-NOTES.md +++ b/crates/topology-planner/DESIGN-NOTES.md @@ -1,7 +1,8 @@ # Design notes: the topology planner Current canonical decisions for this component. See [COMPONENT.md](COMPONENT.md) for what the -component is; see [CHECKLIST.md](CHECKLIST.md) for what is planned. +component is; see [CHECKLIST.md](CHECKLIST.md) for what is planned. Historical context and +exploratory analysis live in [DESIGN-RATIONALE.md](DESIGN-RATIONALE.md). `EP-D-1` through `EP-D-3` are **queries** rather than choices: the planner's requirements, stated precisely enough that the topology model could be designed against a real caller instead of against @@ -20,9 +21,9 @@ renamed to match. |---|---| | EP-D-1 | **The shard-set query**: what the planner must know to choose which processors host a domain, and what today's model cannot tell it. | | EP-D-2 | **The proximity query**: how close two processors are, which selects the channel between their domains. Takes an **unordered** pair; the model has no answer today. | -| EP-D-3 | **The residency query**: where a domain's pool lives, and which side of a cross-domain pair should host a shared ring. **Ordered**, and the half the model cannot answer is structurally unanswerable rather than merely unpopulated. | +| EP-D-3 | **The residency query**: where a domain's pool lives, and which side of a cross-domain pair should host a shared ring. **Ordered**, with directed cost entering through the abstract model/adapter path under [D-20](../windows-topology-sys/DESIGN-NOTES.md#d-20). | | EP-D-4 | **The four-part architecture, and the planner's name.** The engineer's position: the planner is **`topology-planner`** (no `windows-` prefix); it takes a **goal** description (shape deferred for litigation), queries an **abstracted idealized** model covering processors, memory, storage, interconnects, distances and bottlenecks, and emits a **JSON-serializable, platform-neutral** plan. Two kinds of **adapter** bracket it: one exposing the planner's traits over the Windows topology objects, one **realizing** a plan as buffers, rings and threads with the user's code inserted at the right steps. Settles `MMT-1.5` (the facts crate keeps its `-sys` name), the "two graphs, one word" ambiguity, and where distance lives -- the attributed interconnect shape D-9 sketched goes in the abstract model, so D-9's deferral in the facts crate stands unreopened. | -| EP-D-5 | **The component layout: `topology-model` is its own crate, and dependencies point one way.** The abstract model and the traits the planner queries live in `topology-model`, which the planner and both adapters depend on; nothing depends on `topology-planner`. Putting the traits in the planner would make a crate whose job is to *describe a machine* depend on one that applies *policy* -- the same defect as `outermost_partitioning_cache`, arriving as a dependency edge instead of an API. Two consequences derived from the same rule rather than decided separately: **the plan type also lives in `topology-model`** (otherwise the realizer depends on the planner), and the inward adapter and the realizer are **separate crates** (their dependency sets barely overlap, and fusing them would make reading a topology pull in the whole runtime). | +| EP-D-5 | **The component layout: `topology-model` is its own crate, and dependencies point one way.** The abstract model and the traits the planner queries live in `topology-model`, which the planner and both adapters depend on; non-planner components do not depend on `topology-planner`. Putting the traits in the planner would make a crate whose job is to *describe a machine* depend on one that applies *policy* -- the same defect as `outermost_partitioning_cache`, arriving as a dependency edge instead of an API. Two consequences derived from the same rule rather than decided separately: **the plan type also lives in `topology-model`** (otherwise the realizer depends on the planner), and the inward adapter and the realizer are **separate crates** (their dependency sets barely overlap, and fusing them would make reading a topology pull in the whole runtime). | ## EP-D-1: the shard-set query @@ -230,109 +231,36 @@ this query is the cause of that defect, not a separate problem. *Recorded by [CHECKLIST.md](CHECKLIST.md) EP-1.3.* -### What the planner is choosing - -Two things, and they are different questions that happen to share a subject: - -- **Where each domain's own pool lives.** A domain allocates node-locally to the processor it is - pinned to. Per-processor, unordered, cheap. -- **Which side of a cross-domain pair hosts their shared ring.** Ordered, because the producer - writes and the consumer reads, so the placement decides which of them pays for the crossing. - -This is where the direction that [EP-D-2](#ep-d-2) deliberately refused lands. Proximity is the -link and is symmetric; residency is the hop and is not. - -### The first half is answered, with one asymmetry worth keeping - -`MachineMemoryTopology::memory_domains()` yields the memory domains with their processor sets, so -processor-to-domain is a lookup. - -Partial coverage exists here as it does for caches -- a processor may be named by no memory domain --- but **the right response is different, and `windows-placement-probe` already got this right**. -Its `places_from_topology` refuses on a missing NUMA node while tolerating a missing cache domain, -and the asymmetry is principled: an unknown cache domain costs an optimisation, whereas an unknown -memory domain has no honest fallback at all, since the pool has to be allocated *somewhere* and -guessing means quietly allocating remote memory for the life of the process. - -So the planner inherits that: an unplaced processor may still host a domain, but not with a -node-local pool, and the difference has to be visible in the plan rather than assumed away. - -### The second half is not merely unpopulated -- it cannot be measured, by construction - -`MachineMemoryTopology::distances` exists, and it is easy to read its permanent `None` as an oversight. It is -not. The field is documented as being for a fed-in description, because "Windows exposes no -user-mode SLIT reader", and that is accurate. - -The sharper problem is what follows from it. `distances` has exactly two input paths: hand -construction, which defaults to `Provenance::Synthetic`, and deserialization, which -`downgraded_to(Provenance::Restored)` caps. `MachineMemoryTopology::discover` hardcodes `None`. So **no path -exists by which `distances` can ever carry `Measured` provenance** -- not because nobody wrote the -code, but because the only sources are a literal and a file, and a file cannot establish that it -describes the machine you are on. - -Under the model's bar -- usable without further measurement -- a planner on a real machine -therefore cannot obtain trustworthy distance for that machine today, and no amount of populating -the existing field would change that. - -### A scalar distance cannot express what this query asks - -Even a populated `Distances` would not answer it. The matrix is SLIT-shaped: one scalar per pair, -with `matrix[i][i]` conventionally `10`. That is a *symmetric, workload-independent* abstraction, -and the residency question is neither. It asks which of two directions is cheaper for a specific -access pattern -- a ring one side writes and the other reads. - -`windows-topology-sys` D-9 already anticipated this precisely, and excluded it deliberately: - -> **HMAT-style attributed relations.** ACPI's Heterogeneous Memory Attribute Table supersedes SLIT, -> giving per-initiator/per-target read and write latency and bandwidth -- four numbers where SLIT -> gives one scalar [...] A general edge list (`{ from, to, read_latency_ns, read_bandwidth_mbps, -> ... }`) would absorb HMAT, **asymmetry**, and multi-hop CXL fabrics; the scalar distance matrix -> this schema keeps will [be revisited when] scalar distance demonstrably mismodels a machine -> somebody is tuning for. - -**This planner is the machine-tuner that deferral names, and asymmetry is exactly the property it -needs.** D-8 makes the revision cheap by keeping the JSON schema outside the semver contract, which -that decision says is "precisely what makes D-9's deferrals safe rather than merely convenient". - -### The trigger is approached, not met, and saying which matters +### The decision -D-9's condition is *demonstrable* mismodelling, and honesty requires separating what is shown from -what is expected. +The planner needs two distinct residency facts: -What is shown: `windows-placement-probe` measures per-hop cost as four numbers per undirected edge --- two directions times two ring placements -- and its code states that "a hop is not symmetric even -though the link is". The apparatus treats direction as real. +- **Processor to memory-domain placement** for node-local pool allocation, with unplaced processors + represented explicitly. +- **Directed cross-domain residency cost** for choosing which side of a pair hosts shared buffers, + with measurement context attached to measured values. -What is **not** shown: any measurement demonstrating that the four numbers differ. Both development -hosts report a single NUMA node, so every such run is vacuous -- the spike says so itself, printing -"VACUOUS ON THIS MACHINE" and "Apparatus works; question unanswered". So the claim "scalar distance -mismodels this machine" is currently unproven on hardware anyone here has. +### Boundary alignment with D-20 -The requirement is real either way, because the planner must choose a side and today has nothing to -choose with. But the *specific* claim that a scalar is insufficient needs a multi-node measurement, -and that measurement should be taken before D-9 is reopened on those grounds rather than after. +[D-20](../windows-topology-sys/DESIGN-NOTES.md#d-20) deleted `MachineMemoryTopology::distances` and +fixed the Win32 boundary for `windows-topology-sys`. Residency-cost data required by the planner +therefore enters through the abstract model path and the adapter/synthesizer that populates it, not +through `windows-topology-sys`. That required input remains explicitly directional and must carry +measurement context with any measured value. -### A measured locality fact must carry what it measured +### Current status -The probe's numbers are nanoseconds for one ring-handoff pattern at one message size. Promoting -them into the topology as "the distance" would bake one workload into a model other consumers share --- and a different consumer, streaming large buffers rather than handing off small messages, would -read them as authoritative and be wrong. +The processor-to-memory-domain half is available from topology memory-domain memberships and still +keeps the established asymmetry: unknown cache placement can degrade, unknown memory placement has +no honest default. -So a measured relation has to name its measurement, not just its value. This is the concrete reason -per-relation provenance has to be more than a trust label: "measured" is not a sufficient -description of a number whose meaning depends on how it was obtained. +The directed-cost half remains an open requirement on `topology-model` plus adapter inputs and stays +tracked as [CHECKLIST.md](CHECKLIST.md) `EP-1+.4`. -### What this asks of the model +### Historical rationale -- Processor-to-memory-domain, with the unplaced case distinguishable rather than defaulted, because - here it has no honest default. -- A **directed** cost between memory domains, which SLIT's scalar cannot express and which D-9 - already sketched as an attributed edge list. -- Provenance rich enough to say *what* a measured number measured, so one consumer's workload does - not become every consumer's constant. -- And, before reopening D-9 on the asymmetry argument: a multi-node measurement showing the - directions actually differ. +Detailed trigger analysis and prior framing are recorded in +[DESIGN-RATIONALE.md](DESIGN-RATIONALE.md#ep-d-3-rationale-and-history). ## EP-D-4: the four-part architecture, and the planner's name @@ -391,21 +319,9 @@ that it "changes the crate's identity from processor topology to system topology about *that crate* and still holds. NVMe belongs to the abstract model, which was never scoped to a processor topology. -### What it opens - -- **Component layout.** How many crates, and where the traits live. If the traits are defined in the - planner, the inward adapter depends on the planner, which points the wrong way for a crate whose - job is to describe a machine. An abstract-model crate that both depend on avoids that, at the cost - of a fourth component. **Not yet decided.** -- **Who measures.** The previous framing had this component measuring with permission. If distance is - a property of the abstract model, measurement plausibly belongs to whatever *populates* that model - -- an adapter -- rather than to the planner. The three-stage split (observe / synthesize / execute) - survives; which component owns the middle stage does not obviously. -- **`MMT-1.3` / `EP-1.4`'s consumer changed.** Both ask what a consumer does with a fact that was not - observed. That consumer is no longer the planner reading `MachineMemoryTopology` directly -- it is - the **inward adapter**, deciding how an absent Windows fact appears in the abstract model. The - decision is still one decision, and it is still to be taken jointly, but it is taken at a boundary - that did not exist when both items were written. +Open follow-up history for this decision is recorded in +[DESIGN-RATIONALE.md](DESIGN-RATIONALE.md#ep-d-4-open-follow-ups); current actionable work is tracked +in [CHECKLIST.md](CHECKLIST.md). ### What survives unchanged diff --git a/crates/topology-planner/DESIGN-RATIONALE.md b/crates/topology-planner/DESIGN-RATIONALE.md new file mode 100644 index 000000000..b81e1f0ce --- /dev/null +++ b/crates/topology-planner/DESIGN-RATIONALE.md @@ -0,0 +1,37 @@ +# Design rationale: topology-planner + +Historical design context and exploratory analysis for this component. Current canonical decisions are +in [DESIGN-NOTES.md](DESIGN-NOTES.md). + +## EP-D-3 rationale and history + +Earlier drafts of EP-D-3 were written against a now-deleted `MachineMemoryTopology::distances` +field and analyzed whether that field could be repurposed. The current contract boundary is owned by +[D-20](../windows-topology-sys/DESIGN-NOTES.md#d-20); this section keeps only the historical reasons +that the old field-centric framing stayed open. + +The first reason was shape: the planner's question is directional -- which side of a spanning pair +hosts the shared buffer -- while a scalar, symmetric distance matrix can at best describe the link +between two domains and not the hop-specific residency choice. + +The second reason was evidence. `windows-placement-probe` already supplied directional measurements, +but every host available when this was written was single-node, so the measured result was vacuous: +it confirmed that the probe carried direction and measurement context correctly, but it did not yet +show a real cross-node asymmetry. + +That left the D-9 reopening trigger approached but unmet. The planner had reached the point of +needing a directional cost, but without a machine whose measured result demonstrated that a scalar +distance mismodels the machine somebody is tuning for, reopening the old topology-field decision +remained deferred rather than claimed as proved. + +## EP-D-4 open follow-ups (historical) + +EP-D-4 intentionally left several follow-ups unresolved: + +- Adapter naming. +- Measurement ownership in the new four-part architecture. +- Consumer behavior for not-observed facts after the boundary split. + +These are tracked as checklist work in [CHECKLIST.md](CHECKLIST.md) as `EP-1.4` (not-observed +behavior), `EP-1+.3` (planner/model/adapter naming), and `EP-1+.4` (measurement ownership), rather +than as canonical decisions. diff --git a/crates/topology-planner/PLANS.md b/crates/topology-planner/PLANS.md new file mode 100644 index 000000000..d515fe976 --- /dev/null +++ b/crates/topology-planner/PLANS.md @@ -0,0 +1,5 @@ +# Plans: topology-planner + +| Path to CHECKLIST.md | Status | Brief description | Design Notes | +|---|---|---|---| +| [CHECKLIST.md](CHECKLIST.md) | in progress | Active checklist maintenance for requirements/planning milestones; code implementation milestones are parked. | [DESIGN-NOTES.md](DESIGN-NOTES.md), [DESIGN-RATIONALE.md](DESIGN-RATIONALE.md) |