From 91710602b12160e750745db2a9cacd099c138f38 Mon Sep 17 00:00:00 2001 From: "Christoph Engelbert (noctarius)" Date: Fri, 21 Aug 2026 21:10:07 +0200 Subject: [PATCH 1/4] docs: settle on "datacenter" as one word and gate it "Data center" and "datacenter" are both correct English, which is why the documentation carried six of the first and none of the second. Only one of them can be the house spelling, and the one-word form is picked. check-prose.py gains a one-word-compound rule for the class of compounds that have two accepted spellings. It matches the spaced and the hyphenated form together with a trailing plural "s", keeps a leading capital through the rewrite so a heading and the start of a sentence survive it, and reports the finding as the house decision it is rather than as a misspelling. Co-Authored-By: Claude Opus 5 (1M context) --- docs/architecture/concepts/hyper-converged.md | 2 +- .../storage-performance-and-qos.md | 2 +- docs/important-notes/terminology.md | 8 ++-- scripts/check-prose.py | 40 ++++++++++++++++++- 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/docs/architecture/concepts/hyper-converged.md b/docs/architecture/concepts/hyper-converged.md index 6e434aa3..c8d9ef88 100644 --- a/docs/architecture/concepts/hyper-converged.md +++ b/docs/architecture/concepts/hyper-converged.md @@ -31,7 +31,7 @@ Key characteristics and benefits of hyper-converged storage include: New nodes can be added seamlessly, increasing both compute and storage capacity without complex reconfiguration. Storage capacity and performance grows and shrinks seamlessly with the size of the cluster. This provides a great deal of flexibility for customers with strong and unknown dynamics as well as those with a variety of different sizing - requirements from small edge clusters to large data center clusters. + requirements from small edge clusters to large datacenter clusters. - **De-coupling from Hardware Lifecycle** Individual hardware components or units can be replaced choosing from different vendors without service interruption or degradation. Gradual replacement of hardware is supported. diff --git a/docs/architecture/storage-performance-and-qos.md b/docs/architecture/storage-performance-and-qos.md index a2ced35a..ed39d419 100644 --- a/docs/architecture/storage-performance-and-qos.md +++ b/docs/architecture/storage-performance-and-qos.md @@ -90,7 +90,7 @@ bottlenecks specific to mixed I/O patterns on other protocols (such as iSCSI) an ### Support for ROCEv2 Simplyblock also supports NVMe over RDMA (ROCEv2). RDMA, as a transport layer, offers significant latency and tail -latency advantages over TCP. Today, RDMA can be used in most data center environments because it requires only specific +latency advantages over TCP. Today, RDMA can be used in most datacenter environments because it requires only specific hardware features from NICs, which are available across a broad range of models. It runs over UDP/IP and, as such, does not require any changes to the networking. diff --git a/docs/important-notes/terminology.md b/docs/important-notes/terminology.md index 0f2ec020..cd328c57 100644 --- a/docs/important-notes/terminology.md +++ b/docs/important-notes/terminology.md @@ -18,7 +18,7 @@ fault-tolerant, and high-performance storage system. Unlike traditional single-n distribute data across multiple nodes, ensuring redundancy, load balancing, and resilience against hardware failures. To optimize data availability and efficiency, these clusters can be configured using different architectures, including replication and erasure coding. Storage clusters are commonly used in cloud storage, high-performance computing (HPC), -and enterprise data centers, enabling seamless scalability and improved data accessibility across distributed +and enterprise datacenters, enabling seamless scalability and improved data accessibility across distributed environments. ### Storage Node @@ -67,7 +67,7 @@ infrastructures, providing fast and efficient remote storage access. NVMe/TCP (NVMe over TCP) is a transport protocol that extends NVMe over Fabrics (NVMe-oF) using standard TCP/IP networks to enable high-performance, low-latency access to remote NVMe storage. By leveraging existing Ethernet infrastructure, NVMe/TCP eliminates the need for specialized networking hardware such as RDMA (RoCE or iWARP) or Fibre Channel (FC), -making it a cost-effective and easily deployable solution for cloud, enterprise, and data center storage environments. +making it a cost-effective and easily deployable solution for cloud, enterprise, and datacenter storage environments. It maintains the efficiency of NVMe, providing scalable, high-throughput, and low-latency remote storage access while ensuring broad compatibility with modern network architectures. @@ -309,7 +309,7 @@ software-defined system. Unlike traditional architectures that rely on separate hyper-converged infrastructure (HCI) leverages virtualization and centralized management to streamline operations, improve scalability, and reduce complexity. This approach enhances performance, fault tolerance, and resource efficiency by distributing workloads across multiple nodes, allowing seamless scaling by adding more nodes. HCI is widely -used in cloud environments, virtual desktop infrastructure (VDI), and enterprise data centers for its ease of +used in cloud environments, virtual desktop infrastructure (VDI), and enterprise datacenters for its ease of deployment, automation capabilities, and cost-effectiveness. ### Disaggregated @@ -319,5 +319,5 @@ independent components rather than tightly integrated within the same physical s for example, storage resources are managed independently of compute nodes, allowing for flexible scaling, improved resource utilization, and reduced hardware dependencies. This contrasts with traditional or hyper-converged architectures, where these resources are combined. Disaggregated architectures are widely used in cloud computing, -high-performance computing (HPC), and modern data centers to enhance scalability, cost-efficiency, and operational +high-performance computing (HPC), and modern datacenters to enhance scalability, cost-efficiency, and operational flexibility while optimizing performance for dynamic workloads. diff --git a/scripts/check-prose.py b/scripts/check-prose.py index 5485c450..f46c5186 100755 --- a/scripts/check-prose.py +++ b/scripts/check-prose.py @@ -10,9 +10,12 @@ * A **repeated word**, as in "the volume is is migrated". * An **abbreviation or an introduction without its comma**: American usage writes "e.g.," and "i.e.," and "for example," with the comma. +* A **compound the house writes as one word**, as in "data center". Both + spellings are correct English, so this is a house decision rather than a + misspelling, and the decision is that it is written "datacenter". -All three have exactly one right answer, so all three are errors that "--fix" -resolves. +All of them have exactly one right answer, so all of them are errors that +"--fix" resolves. By default all Markdown files below "docs/" and "snippets/" are scanned. Generated files are skipped, since they have to be corrected at their source. @@ -166,12 +169,31 @@ # An adverb ending in "ly" is never hyphenated to the adjective behind it. ADVERB_HYPHEN_PATTERN = re.compile(r"\b(\w+ly)-(\w+)\b") +# Compounds that both dictionaries accept in two spellings, and that the house +# writes as one word. Neither form is wrong, which is exactly why one of them has +# to be picked: a page that alternates between them reads as two pages. The +# hyphenated spelling is matched as well, so "data-center" is caught next to +# "data center", and a trailing plural "s" is part of the match. A leading +# capital survives the rewrite, so a heading and the start of a sentence keep +# theirs. +ONE_WORD_COMPOUNDS = {"data center": "datacenter"} +ONE_WORD_COMPOUND_PATTERN = re.compile( + r"\b(?:" + + "|".join( + re.escape(compound).replace(r"\ ", r"[\s\-]") + for compound in sorted(ONE_WORD_COMPOUNDS, key=len, reverse=True) + ) + + r")s?\b", + re.IGNORECASE, +) + MISSPELLING_REASON = "Misspelling of '{expected}'" REPEATED_REASON = "The word '{word}' is repeated" COMMA_REASON = "'{phrase}' introduces an example and takes a comma" DOUBLE_SPACE_REASON = "Two spaces between words" COMPOUND_REASON = "'{compound}' describes '{next}' here, so it is hyphenated: '{expected}'" ADVERB_REASON = "An adverb is not hyphenated to its adjective: '{expected}'" +ONE_WORD_REASON = "'{found}' is written as one word: '{expected}'" def scan_file(file_path): @@ -252,6 +274,20 @@ def report(number, column, check, reason, text, length=0, replacement=""): prose.text, len(match.group(0)), expected, ) + for match in ONE_WORD_COMPOUND_PATTERN.finditer(prose.masked): + found = match.group(0) + key = re.sub(r"[\s\-]", " ", found).lower() + # The trailing plural "s" is part of the match and not of the key. + plural = "" if key in ONE_WORD_COMPOUNDS else "s" + expected = ONE_WORD_COMPOUNDS[key.removesuffix(plural)] + plural + if found[0].isupper(): + expected = expected[0].upper() + expected[1:] + report( + prose.number, match.start(), "one-word-compound", + ONE_WORD_REASON.format(found=found, expected=expected), + prose.text, len(found), expected, + ) + for match in COMMA_PATTERN.finditer(prose.masked): phrase = match.group("phrase") report( From 7e3f18c3ed1c45b62ab8b5cf6719dc30ececbe59 Mon Sep 17 00:00:00 2001 From: "Christoph Engelbert (noctarius)" Date: Fri, 21 Aug 2026 21:10:21 +0200 Subject: [PATCH 2/4] docs(failure-domains): name domains with labels instead of integer ids `sn add-node --failure-domain` takes a label (`RACK1`, `AZ2`, `HOST1`) rather than a non-negative integer. The integer stays the internal identity that placement, the distrib cluster map and the expansion planner key off, and the v2 API keeps its integer `failure_domain` field, but it no longer has to be invented or tracked by hand. Two sections are new on the operations page. "Label Syntax" gives the grammar: a letter followed by up to 31 of `[A-Z0-9_-]`, case-insensitive and stored upper-cased. "Labels on Existing Clusters" covers initialization through `cluster update`, which names every id in service `FD` or `HOST`. It is idempotent, so a later rename survives, and it refuses to take a derived name already owned by a different id. An all-digits value is still read as the internal id, so existing scripts, CI bootstraps and the Kubernetes operator keep working unchanged. The node list's Failure Domain column shows the label and falls back to the id for a cluster that has not been initialized. `reference/operator/reference.md` is generated CRD documentation and is untouched: the Kubernetes `failureDomain` field lives in the operator repository and needs its own change to accept labels. Co-Authored-By: Claude Opus 5 (1M context) --- docs/architecture/concepts/failure-domains.md | 17 ++-- .../operations/failure-domains.md | 84 ++++++++++++++----- 2 files changed, 76 insertions(+), 25 deletions(-) diff --git a/docs/architecture/concepts/failure-domains.md b/docs/architecture/concepts/failure-domains.md index 67127475..c3621aef 100644 --- a/docs/architecture/concepts/failure-domains.md +++ b/docs/architecture/concepts/failure-domains.md @@ -9,9 +9,14 @@ distribution unit, or an availability zone. When failure domains are enabled, si journal copies, and failover paths across the domains so that the loss of one entire domain does not interrupt the availability of the cluster. -Failure domains are identified by a non-negative integer chosen by the operator. Simplyblock does not detect the -physical topology itself: every storage node is explicitly tagged with the id of the domain it belongs to when it -is added to the cluster. +Each domain is identified by a label, such as `RACK1`, `AZ2`, or `HOST1`. Simplyblock does not detect the +physical topology itself: every storage node is explicitly tagged with the label of the domain it belongs to when +it is added to the cluster. The domain is created by the first node carrying a given label, and every later node +naming that label joins it. + +Internally, each label maps to a cluster-unique integer id, which is what placement and the data plane key off. +That id is assigned automatically and does not have to be tracked. It surfaces only in low-level logs and in the +`failure_domain` field of the API, which keeps its integer type for compatibility. !!! important Failure-domain support is a deploy-time decision. It is enabled when the storage cluster is created and cannot @@ -22,7 +27,7 @@ is added to the cluster. With failure domains enabled, placement decisions consider the domain tag in four independent dimensions: 1. **Data and parity chunks:** The distributed erasure coding spreads the chunks of each stripe across distinct - failure domains, so that a full domain outage leaves enough chunks to reconstruct all data within the configured + failure domains so that a full domain outage leaves enough chunks to reconstruct all data within the configured erasure coding scheme. 2. **Journal copies:** The copies of the high-availability write journal are balanced across domains with a per-domain cap, so that losing a whole domain always leaves enough journal copies to maintain the journal quorum. @@ -66,7 +71,7 @@ a same-domain secondary path, and its tertiary path is still guaranteed to be cr !!! note Balance is counted in physical hosts, not storage nodes. On multi-socket hosts running two storage nodes, both - nodes count as one host and must carry the same failure-domain id. Dedicated secondary nodes are not counted + nodes count as one host and must carry the same failure-domain label. Dedicated secondary nodes are not counted toward the balance. ## Failure Domains and Erasure Coding Schemes @@ -85,7 +90,7 @@ its loss would break the journal quorum. ## Domain Membership Is Immutable A host's failure domain cannot be changed while the host is part of the cluster. Moving a host between domains -requires removing the node, restoring the domain balance, and re-adding it with the new failure-domain id. This +requires removing the node, restoring the domain balance, and re-adding it with the new failure-domain label. This prevents accidental topology changes that would silently invalidate the placement of existing data. ## Recovery Behavior diff --git a/docs/non-kubernetes/operations/failure-domains.md b/docs/non-kubernetes/operations/failure-domains.md index 57eccf2f..2c18db6a 100644 --- a/docs/non-kubernetes/operations/failure-domains.md +++ b/docs/non-kubernetes/operations/failure-domains.md @@ -18,7 +18,7 @@ failure domains are assigned declaratively through the Simplyblock Operator Failure-domain support is enabled when the storage cluster is created and is immutable afterward: -```bash title="Create a cluster with failure-domain support" +```bash title="Creating a cluster with failure-domain support" {{ cliname }} cluster create --enable-failure-domain ``` @@ -31,36 +31,78 @@ plane. ## Tagging Storage Nodes -On a failure-domain cluster, every storage node must be added with a failure-domain id (a non-negative integer -identifying the rack, cabinet, or availability zone). All nodes in the same physical fault group share the same id. +On a failure-domain cluster, every storage node must be added with a failure-domain label naming the rack, +cabinet, or availability zone it sits in. All nodes in the same physical fault group share the same label. -```bash title="Add storage nodes with failure-domain tags" -# Rack A (domain 0) -{{ cliname }} storage-node add-node --failure-domain 0 +```bash title="Adding storage nodes to two different racks" +{{ cliname }} storage-node add-node \ + --failure-domain RACK1 \ + -# Rack B (domain 1) -{{ cliname }} storage-node add-node --failure-domain 1 +{{ cliname }} storage-node add-node \ + --failure-domain RACK2 \ + ``` +A domain comes into existence with the first node that carries its label, and every later node naming that label +joins it. There is no separate command to declare a domain up front. + The tag is mandatory on failure-domain clusters and must be omitted on clusters without the feature. Both mismatches are rejected with an explanatory error. -All storage nodes on the same physical host must carry the same failure-domain id. On multi-socket hosts with two -storage nodes, both nodes belong to the host's domain. +All storage nodes on the same physical host must carry the same failure-domain label. On multi-socket hosts with +two storage nodes, both nodes belong to the host's domain. + +### Label Syntax + +A label starts with a letter, followed by up to 31 letters, digits, `_`, or `-`. `RACK1`, `AZ2`, `DC-EU-WEST_1`, +and `HOST1` are all valid. Labels are case-insensitive, so `rack1`, `Rack1`, and `RACK1` name the same domain. +They are stored upper-cased. A value that does not match the syntax is rejected before the node is touched. + +Labels should match how the datacenter is actually described, so that a node list reads like the floor plan. + +!!! note + Internally, each label maps to a cluster-unique integer id that placement and the data plane key off. An + all-digits value passed to `--failure-domain` is still read as that internal id rather than as a label, which + keeps existing scripts and automation working unchanged. New deployments should use labels. The assigned domains are shown in the node list once at least one node carries a tag: -```bash title="List storage nodes with their failure domains" +```bash title="Listing the storage nodes with their failure domains" {{ cliname }} storage-node list ``` +The **Failure Domain** column shows the label. The id is shown instead for a cluster that has not been through +[label initialization](#labels-on-existing-clusters), and for a domain created by passing an internal id directly. + +## Labels on Existing Clusters + +Clusters deployed before labels existed identify their domains by internal id only. The label registry is +initialized by the regular cluster update: + +```bash title="Initializing the labels of an existing cluster" +{{ cliname }} cluster update +``` + +Every domain in service is given a derived name (`FD0`, `FD1`, and so on), and every physical label becomes +`HOST1`, `HOST2`, and so on. These names are placeholders. They make the existing topology addressable by name +without guessing at intent, so a domain the datacenter calls `RACK7` should be renamed afterward. + +Initialization is idempotent and safe to repeat. An id that already carries a label is left untouched, so a rename +survives later updates. Where the derived name is already owned by a different id (a domain named `FD3` by hand, +for example), that id is left unnamed and a warning is logged rather than anything being renamed. + +!!! note + Only the names of the existing domains are initialized. The failure-domain feature itself is not enabled on a + cluster created without `--enable-failure-domain`. That still requires a redeployment. + ## Activation Requirements Activating a freshly assembled failure-domain cluster enforces the following rules: | Rule | Enforcement | |-----------------------------------------------|-------------------------------------------------------------------------------------| -| Every node carries a failure-domain id | Hard: activation fails | +| Every node carries a failure-domain label | Hard: activation fails | | A host does not span two domains | Hard: activation fails | | At least two distinct domains exist | Hard: activation fails | | All domains hold an equal number of hosts | Hard: activation fails | @@ -75,9 +117,11 @@ deliberately skips these gates: recovery always takes precedence over topology p Failure-domain clusters require at least four copies of the high-availability journal, even with a single parity chunk. The default of `--ha-jm-count` is 3 for single-parity clusters, so it must be raised explicitly: -```bash title="Add a node with four journal copies" +```bash title="Adding a node with four journal copies" {{ cliname }} storage-node add-node \ - --failure-domain 0 --ha-jm-count 4 + --failure-domain RACK1 \ + --ha-jm-count 4 \ + ``` With three copies and two domains, one domain would hold two copies, and losing that domain would break the @@ -91,7 +135,7 @@ Once the cluster holds data, topology changes are admitted only if the failure d added to any domain. The next host must then go to a different domain. - No domain may drop below two hosts. - Adding another storage node slot on an already-member host (multi-socket systems) is balance-neutral and always - admitted, as long as the host keeps its original domain id. + admitted, as long as the host keeps its original domain label. Violating additions and removals are refused up front, before any data is moved. @@ -100,9 +144,11 @@ Violating additions and removals are refused up front, before any data is moved. Single-node expansion integrates a new node into the cluster by re-homing existing secondary and tertiary failover paths: -```bash title="Expand the cluster by one node" +```bash title="Expanding the cluster by one node" {{ cliname }} storage-node add-node \ - --failure-domain --expansion + --failure-domain \ + --expansion \ + ``` On failure-domain clusters, the expansion planner inserts the newcomer into the existing host rotation at a @@ -123,12 +169,12 @@ primary. If no such node exists, the removal is refused. ## Moving a Host Between Domains -A host's failure domain is immutable. Re-adding a host or one of its node slots with a different domain id is +A host's failure domain is immutable. Re-adding a host or one of its node slots with a different domain label is rejected. To move a host: 1. Remove the node with `{{ cliname }} storage-node remove`. 2. Restore the domain balance if necessary. -3. Re-add the node with the new `--failure-domain` id. +3. Re-add the node with the new `--failure-domain` label. ## Behavior During Outages From 477701ea5bb691d13e2e0642d855cd7600139700 Mon Sep 17 00:00:00 2001 From: "Christoph Engelbert (noctarius)" Date: Fri, 21 Aug 2026 21:18:26 +0200 Subject: [PATCH 3/4] docs: require the comma behind an opening connective "However the volume stays online" reads as a sentence about however many volumes until the verb arrives and the reader has to start over. The comma is what marks the word as a comment on the sentence rather than as part of it, and the documentation already wrote it that way in 37 of 37 "However" and 17 of 17 "Therefore". The eight places that did not are corrected here. check-prose.py gains an introductory-comma rule over two lists. A connective ("However", "Therefore", "Otherwise", "By default") can only join clauses and is reported whatever follows it. A sentence adverb ("Internally", "Typically", "Today", "Now") can also modify the word behind it, so it is reported only where no participle or adjective follows and "Initially developed by Google" stays as it is. Guarded against three shapes that must not take the comma: the phrase continued into a preposition or a conjunction ("Instead of", "Now that", "Together with", "In addition to", "However many"), the adverb modifying the word behind it, and the bold subject of a list item. "Then" and "First" are left out entirely, since they number the steps of a procedure and take no comma there. Co-Authored-By: Claude Opus 5 (1M context) --- .../nvme-namespaces-and-subsystems.md | 2 +- .../cloud-instance-recommendations.md | 2 +- docs/kubernetes/usage/removing.md | 2 +- docs/kubernetes/usage/snapshotting.md | 2 +- .../operations/manual-restarting-nodes.md | 2 +- .../operations/replacing-storage-node.md | 2 +- docs/reference/operator/index.md | 2 +- scripts/check-prose.py | 82 +++++++++++++++++++ snippets/data-migration.md | 2 +- 9 files changed, 90 insertions(+), 8 deletions(-) diff --git a/docs/architecture/concepts/nvme-namespaces-and-subsystems.md b/docs/architecture/concepts/nvme-namespaces-and-subsystems.md index 6898716d..d594abd1 100644 --- a/docs/architecture/concepts/nvme-namespaces-and-subsystems.md +++ b/docs/architecture/concepts/nvme-namespaces-and-subsystems.md @@ -22,7 +22,7 @@ In simplyblock this process is either automated (CSI, OpenStack, or Proxmox) or It’s roughly equivalent to an NVMe controller or logical device that can contain one or more namespaces. -Now subsystems are backed by multiple queue pairs, each of which is backed by a network connection such as a TCP socket. +Now, subsystems are backed by multiple queue pairs, each of which is backed by a network connection such as a TCP socket. More queue pairs require more resources from the cluster but make the volumes faster. Namespaces on the other side are actual block storage regions that hold user data. diff --git a/docs/deployment-preparation/cloud-instance-recommendations.md b/docs/deployment-preparation/cloud-instance-recommendations.md index be860654..1105b475 100644 --- a/docs/deployment-preparation/cloud-instance-recommendations.md +++ b/docs/deployment-preparation/cloud-instance-recommendations.md @@ -14,7 +14,7 @@ Amazon EBS is not recommended for high-performance clusters. !!! important If local NVMe devices are chosen, make sure that the nodes in the cluster are provisioned into a placement group of type - _Spread_! Otherwise there is no guarantee that multiple storage nodes won't be located in the same failure domain. + _Spread_! Otherwise, there is no guarantee that multiple storage nodes won't be located in the same failure domain. Generally, with AWS, there are three considerations when selecting virtual machine types: diff --git a/docs/kubernetes/usage/removing.md b/docs/kubernetes/usage/removing.md index 6d065d10..d3abdd00 100644 --- a/docs/kubernetes/usage/removing.md +++ b/docs/kubernetes/usage/removing.md @@ -31,7 +31,7 @@ When the PVC is deleted, the PersistentVolume state must be checked. It should b kubectl get pv ``` -Now the PV can be deleted: +Now, the PV can be deleted: ```bash title="Delete a PersistentVolume" kubectl delete pv diff --git a/docs/kubernetes/usage/snapshotting.md b/docs/kubernetes/usage/snapshotting.md index 7ed8f420..d2aaaeb5 100644 --- a/docs/kubernetes/usage/snapshotting.md +++ b/docs/kubernetes/usage/snapshotting.md @@ -9,7 +9,7 @@ simplyblock's [copy-on-write](../../important-notes/terminology.md#cow-copy-on-w In simplyblock, a snapshot is comparable to the table of contents in a book, meaning that the snapshot refers to the same data as the original volume. If the volume diverges from the snapshot, the mutated data segment is duplicated, changed, -and stored as a new data block. Now the volume refers to the new block, while the snapshot refers to the old one. +and stored as a new data block. Now, the volume refers to the new block, while the snapshot refers to the old one. A deeper explanation can be found here: diff --git a/docs/non-kubernetes/operations/manual-restarting-nodes.md b/docs/non-kubernetes/operations/manual-restarting-nodes.md index 0904ee6a..b20e637e 100644 --- a/docs/non-kubernetes/operations/manual-restarting-nodes.md +++ b/docs/non-kubernetes/operations/manual-restarting-nodes.md @@ -30,7 +30,7 @@ There are a few reasons to manually restart a storage node: Nodes can only be restarted from `offline` state! It is important to ensure that the cluster is not in `degraded` state and all other nodes are `online` - before shutting down a storage node for maintenance or upgrades! Otherwise loss of availability - I/O interrupt - may occur! + before shutting down a storage node for maintenance or upgrades! Otherwise, loss of availability - I/O interrupt - may occur! Suspending a storage node and then shutting it down: diff --git a/docs/non-kubernetes/operations/replacing-storage-node.md b/docs/non-kubernetes/operations/replacing-storage-node.md index b5dc47a8..17bafa18 100644 --- a/docs/non-kubernetes/operations/replacing-storage-node.md +++ b/docs/non-kubernetes/operations/replacing-storage-node.md @@ -31,7 +31,7 @@ To start a new storage node, follow the storage node installation according to t ## Remove the old Storage Node !!! important - A storage node can only be removed when it hosts no logical volumes or snapshots. Otherwise the removal is + A storage node can only be removed when it hosts no logical volumes or snapshots. Otherwise, the removal is refused, so all volumes have to be migrated off the node first (see [Volume Migration](volume-migration.md)). The node to be removed must be online or suspended, and all other storage nodes must be online. diff --git a/docs/reference/operator/index.md b/docs/reference/operator/index.md index f4f76f33..9ce26a0e 100644 --- a/docs/reference/operator/index.md +++ b/docs/reference/operator/index.md @@ -41,7 +41,7 @@ the cluster's entry from the Secret automatically. ## Storage Nodes -Storage node management uses three separate CRDs with distinct responsibilities. Together they form a three-tier model: +Storage node management uses three separate CRDs with distinct responsibilities. Together, they form a three-tier model: ```plain StorageNodeSet ──► declares which workers to use and how to configure them diff --git a/scripts/check-prose.py b/scripts/check-prose.py index f46c5186..13467724 100755 --- a/scripts/check-prose.py +++ b/scripts/check-prose.py @@ -10,6 +10,9 @@ * A **repeated word**, as in "the volume is is migrated". * An **abbreviation or an introduction without its comma**: American usage writes "e.g.," and "i.e.," and "for example," with the comma. +* An **opening connective or sentence adverb without its comma**: "However,", + "Therefore,", "Internally,". The comma is what marks the word as a comment on + the sentence rather than as part of it. * A **compound the house writes as one word**, as in "data center". Both spellings are correct English, so this is a house decision rather than a misspelling, and the decision is that it is written "datacenter". @@ -144,6 +147,69 @@ re.IGNORECASE, ) +# A connective or a sentence adverb that opens a sentence is followed by a comma: +# "However, the volume stays online", "Internally, each label maps to an id". The +# comma is what marks the word as a comment on the whole sentence rather than as +# part of it, and English readers expect it there. +# +# The words below are split by what they can be mistaken for. A connective can +# only join clauses, so it is reported whatever follows it. A sentence adverb can +# also modify the word behind it, and "Initially developed by Google" takes no +# comma, so those are reported only when no modifiable word follows. +CONNECTIVES = ( + "However", "Therefore", "Moreover", "Furthermore", "Nevertheless", + "Nonetheless", "Consequently", "Otherwise", "Meanwhile", "Instead", + "Additionally", "Conversely", "Alternatively", "Likewise", "Accordingly", + "Hence", "Thus", "Regardless", "Overall", "Together", "In contrast", + "In addition", "As a result", "On the other hand", "For this reason", + "In practice", "In general", "In particular", "In this case", "In fact", + "In summary", "By default", "At the same time", +) +SENTENCE_ADVERBS = ( + "Internally", "Externally", "Typically", "Optionally", "Ideally", + "Generally", "Normally", "Usually", "Occasionally", "Historically", + "Traditionally", "Originally", "Currently", "Previously", "Recently", + "Today", "Initially", "Subsequently", "Afterward", "Afterwards", "Finally", + "Ultimately", "Technically", "Practically", "Logically", "Physically", + "Functionally", "Operationally", "Effectively", "Importantly", "Notably", + "Specifically", "Similarly", "Now", +) + +# Deliberately absent: "Then", "First", "Second" and "Third". They number the +# steps of a procedure ("Then apply the change", "First run the health check"), +# where the sequence is part of the instruction and takes no comma, and the last +# three are ordinary adjectives on top of that. + +# The word behind the phrase that turns it into a preposition or a conjunction, +# where the comma belongs behind the whole phrase and not behind its first word: +# "Instead of", "Now that", "Together with", "In addition to", "However many". +CONTINUATIONS = { + "of", "to", "with", "that", "than", "as", "much", "many", "long", "often", + "far", "large", "small", "enough", +} + +# What marks the word behind a sentence adverb as the word it modifies rather +# than the start of a clause. The suffixes catch a participle and most +# adjectives ("developed", "using", "smaller", "identical"), and the words below +# are the adjectives that carry none of them. Missing one of these only leaves a +# comma unreported, while flagging one would insert a comma that is wrong. +MODIFIER_SUFFIXES = ("ed", "ing", "er", "est", "ive", "able", "ible", "ous", "ic", "al") +MODIFIER_WORDS = { + "separate", "similar", "same", "safe", "free", "open", "full", "close", + "equal", "aware", "specific", "distinct", "unique", "present", "absent", +} + +# A sentence opens at the start of a line, behind a list marker, or behind the +# full stop of the sentence before it. A phrase that already carries a mark, or +# that is wrapped in the asterisks of a bold list subject, is left alone. +INTRODUCTORY_PATTERN = re.compile( + r"(?:^[ \t]*(?:(?:[-*+]|\d+\.)[ \t]+)?|(?<=[.!?])[ \t])" + r"(?P" + + "|".join(sorted(CONNECTIVES + SENTENCE_ADVERBS, key=len, reverse=True)) + + r")" + r"(?![,:;.!?)\]*_`\w-])[ \t]+(?P[A-Za-z][\w'-]*)" +) + # Two spaces between words are a typing artifact. Table columns and the wide # markers of a grid card are lined up on purpose, so those lines are left out. DOUBLE_SPACE_PATTERN = re.compile(r"(?<=[A-Za-z,.;:)\]`])( {2,})(?=[A-Za-z(\[`])") @@ -190,6 +256,7 @@ MISSPELLING_REASON = "Misspelling of '{expected}'" REPEATED_REASON = "The word '{word}' is repeated" COMMA_REASON = "'{phrase}' introduces an example and takes a comma" +INTRODUCTORY_REASON = "'{phrase}' opens the sentence and takes a comma" DOUBLE_SPACE_REASON = "Two spaces between words" COMPOUND_REASON = "'{compound}' describes '{next}' here, so it is hyphenated: '{expected}'" ADVERB_REASON = "An adverb is not hyphenated to its adjective: '{expected}'" @@ -274,6 +341,21 @@ def report(number, column, check, reason, text, length=0, replacement=""): prose.text, len(match.group(0)), expected, ) + for match in INTRODUCTORY_PATTERN.finditer(prose.masked): + phrase = match.group("phrase") + following = match.group("next").lower() + if following in CONTINUATIONS: + continue + if phrase in SENTENCE_ADVERBS and ( + following in MODIFIER_WORDS or following.endswith(MODIFIER_SUFFIXES) + ): + continue + report( + prose.number, match.start("phrase"), "introductory-comma", + INTRODUCTORY_REASON.format(phrase=phrase), + prose.text, len(phrase), phrase + ",", + ) + for match in ONE_WORD_COMPOUND_PATTERN.finditer(prose.masked): found = match.group(0) key = re.sub(r"[\s\-]", " ", found).lower() diff --git a/snippets/data-migration.md b/snippets/data-migration.md index f1fef275..40cdc684 100644 --- a/snippets/data-migration.md +++ b/snippets/data-migration.md @@ -198,7 +198,7 @@ same mount point as the original disk before, _/data/pg_ in this example. All services that require access to the data can be started again. The RAID itself is still in a degraded state, but it provides the same data security as the original device. -Now the second, new device must be added to the RAID setup to start the re-silvering (data synchronization) process. +Now, the second, new device must be added to the RAID setup to start the re-silvering (data synchronization) process. This is again done using `mdadm` tool. ```bash title="Add the new simplyblock block device to RAID-1" From e5691b1821b58b119c033391bec33ceab827787d Mon Sep 17 00:00:00 2001 From: "Christoph Engelbert (noctarius)" Date: Fri, 21 Aug 2026 21:18:28 +0200 Subject: [PATCH 4/4] docs: teach the writing skill the two new prose rules The skill is what a writer reads before a page, so a rule the gate enforces has to be findable there and not only in the checker. Adds the introductory comma with the three shapes that are exempt from it, and the one-word compound with "datacenter" as its first entry. The punctuation section no longer counts itself as three habits. Co-Authored-By: Claude Opus 5 (1M context) --- .claude/skills/documentation-writing/SKILL.md | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/.claude/skills/documentation-writing/SKILL.md b/.claude/skills/documentation-writing/SKILL.md index 8c623e4b..852a6b44 100644 --- a/.claude/skills/documentation-writing/SKILL.md +++ b/.claude/skills/documentation-writing/SKILL.md @@ -208,6 +208,13 @@ so write `NVMe devices`. `behavior`, `labeled`, `enroll`, `artifact`, `program`, `license`, `gray`. Note that `Fibre Channel` is the name of a standard and keeps its spelling. +**A compound with two accepted spellings is written the house way.** `datacenter` +is one word, never "data center" or "data-center". Neither spelling is wrong, +which is exactly why one of them is picked: a page that alternates between them +reads as two pages. The list is `ONE_WORD_COMPOUNDS` in +`scripts/check-prose.py`, and a new pair is added there rather than settled per +page. + **The Oxford comma** goes before the final `and`, `or`, or `nor` of a series of three or more items: "storage nodes, volumes, and snapshots". It belongs to a series and nowhere else. A comma before an `and` that joins two sentences is @@ -216,16 +223,30 @@ nothing to insert. ## Punctuation to avoid -Three habits make a page read as though nobody chose the words. The punctuation -gate reports all of them. The first two are warnings, because what replaces them -depends on the sentence and is a decision for the writer. The list item form and -the placement of a mark have one right answer and are errors that `--fix` +A handful of habits make a page read as though nobody chose the words, and the +gates report all of them. The semicolon and the em dash below are warnings, +because what replaces them depends on the sentence and is a decision for the +writer. Everything else has one right answer and is an error that `--fix` resolves. **A missing comma after an abbreviation.** American usage writes "e.g.," and "i.e.," and "for example," with the comma, since each of them introduces the example that follows. +**A missing comma after an opening connective or sentence adverb.** "However,", +"Therefore,", "Otherwise,", "Internally,", "By default,", "Today,". The comma is +what marks the word as a comment on the whole sentence rather than as part of +it, and without it the reader parses the word as the subject and has to start +over. The full list is in `scripts/check-prose.py`. + +The comma belongs to the word only where the word opens a clause. "Instead of", +"Now that", "Together with" and "In addition to" are prepositions and +conjunctions that carry the comma behind the whole phrase, and "Initially +developed by Google" is an adverb modifying a participle. The check knows all +three shapes and leaves them alone. "Then" and "First" are absent from it +entirely, because they number the steps of a procedure: "Then apply the change" +takes no comma. + **A comma and a full stop go inside the closing quotation mark**, whatever the quoted words are: "docking points," and never "docking points",. A colon and a semicolon stay outside, and a question mark belongs to whichever sentence asks