From 830b3411fb707bcdda663e3b0b5f82b18b4b9af6 Mon Sep 17 00:00:00 2001 From: Edwin Monk-Fromont <1274422+edmofro@users.noreply.github.com> Date: Thu, 10 Sep 2026 14:03:49 +1200 Subject: [PATCH 1/6] G1: move the virtual-machine provisioning hint into the requirements "Start the virtual machines small, and back them up" was an action in the client's list that restated server figures, which now contradicted the sized compute requirements. It is guidance about those figures, so it belongs under them: requirement hints can now be gated on the answers, and the hint appears only when the servers are virtual machines. It no longer restates the figures it sits beneath, which is what made the two disagree. --- .workhorse/specs/wizard/onboarding.md | 4 +++ crates/pollen-server/src/ruleset/engine.rs | 25 +++++++++++----- crates/pollen-server/src/ruleset/mod.rs | 4 +-- crates/pollen-server/src/ruleset/model.rs | 15 ++++++++-- crates/pollen-server/tests/ruleset.rs | 8 ++--- ruleset.ron | 34 ++++++++++++---------- web/openapi.json | 13 +++++---- web/src/api-types.ts | 2 +- web/src/app.css | 12 ++++++-- web/src/components/Artifact.tsx | 12 +++++--- 10 files changed, 83 insertions(+), 46 deletions(-) diff --git a/.workhorse/specs/wizard/onboarding.md b/.workhorse/specs/wizard/onboarding.md index 9792a1a..b025da2 100644 --- a/.workhorse/specs/wizard/onboarding.md +++ b/.workhorse/specs/wizard/onboarding.md @@ -264,6 +264,10 @@ So the central server appears only when the client hosts it; a facility server a A server's processor, memory and storage scale with the deployment's derived size band, drawn from the recommended per-band figures; its network row is the same at every size. A row may instead be tied to an answer, so the operating system row names the platform the reader chose rather than listing what is available. + +A block can carry hints beneath its rows, and a hint may be tied to an answer the same way. +Guidance about how to provision the figures belongs here rather than among the actions: that virtual machines can start smaller and grow into the figures qualifies the figures themselves, so it sits under them and appears only when the servers are virtual. +Such a hint states the guidance without restating the figures, which would contradict the rows above it. Working out what is needed is the tool's job, so the requirements state it plainly rather than hedging that a larger deployment might need more. The make of a server is a suggestion, never a requirement: the block leads with the specification a server must meet, not a product to buy. For the smallest deployments the block advises hosting with BES or using a mini-server rather than buying a server at all, since dedicated hardware rarely pays off at that scale. diff --git a/crates/pollen-server/src/ruleset/engine.rs b/crates/pollen-server/src/ruleset/engine.rs index 2403445..98656ef 100644 --- a/crates/pollen-server/src/ruleset/engine.rs +++ b/crates/pollen-server/src/ruleset/engine.rs @@ -9,7 +9,9 @@ use serde::{Deserialize, Serialize}; use utoipa::ToSchema; use super::answers::{Answer, Answers}; -use super::model::{Consequence, DerivationKind, QuestionKind, Ruleset, Severity, Spec, SpecRow}; +use super::model::{ + Consequence, DerivationKind, NoteRow, QuestionKind, Ruleset, Severity, Spec, SpecRow, +}; #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct Evaluation { @@ -59,7 +61,7 @@ pub struct TriggeredRequirement { pub class: String, pub summary: Option, pub specs: Vec, - pub note: Option, + pub notes: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] @@ -159,17 +161,24 @@ pub fn evaluate(ruleset: &Ruleset, answers: &Answers) -> Evaluation { specs.extend(present(&ss.specs)); } specs.extend(present(&r.specs)); - // A band-specific note augments the profile's own. - let note = match (r.note.clone(), by_size.and_then(|ss| ss.note.clone())) { - (Some(base), Some(band)) => Some(format!("{base} {band}")), - (base, band) => base.or(band), - }; + // The band-specific hint leads, as the size-varying rows do; the + // profile's own hints follow, minus any whose condition fails. + let mut notes: Vec = Vec::new(); + if let Some(band) = by_size.and_then(|ss| ss.note.clone()) { + notes.push(band); + } + notes.extend( + r.notes + .iter() + .filter(|n: &&NoteRow| n.when.eval(answers)) + .map(|n| n.text.clone()), + ); TriggeredRequirement { id: r.id.clone(), class: r.class.clone(), summary: r.summary.clone(), specs, - note, + notes, } }) .collect(); diff --git a/crates/pollen-server/src/ruleset/mod.rs b/crates/pollen-server/src/ruleset/mod.rs index 9351fc8..4ff287d 100644 --- a/crates/pollen-server/src/ruleset/mod.rs +++ b/crates/pollen-server/src/ruleset/mod.rs @@ -19,8 +19,8 @@ pub use engine::{ }; pub use migrate::{Migration, migrate}; pub use model::{ - Audience, Consequence, ConsequenceType, Cost, Derivation, DerivationKind, Guidance, Opt, - Question, QuestionKind, Requirement, Rule, Ruleset, Section, Severity, SizeSpecs, Spec, + Audience, Consequence, ConsequenceType, Cost, Derivation, DerivationKind, Guidance, NoteRow, + Opt, Question, QuestionKind, Requirement, Rule, Ruleset, Section, Severity, SizeSpecs, Spec, SpecRow, Status, }; pub use resolver::{RULESET_PATH, ResolvedRuleset, RulesetResolver}; diff --git a/crates/pollen-server/src/ruleset/model.rs b/crates/pollen-server/src/ruleset/model.rs index 289aa73..a5b00a8 100644 --- a/crates/pollen-server/src/ruleset/model.rs +++ b/crates/pollen-server/src/ruleset/model.rs @@ -251,9 +251,20 @@ pub struct Requirement { /// for a class that is the same at every size (user devices, mobile, Iti). #[serde(default)] pub by_size: Vec, - /// An optional caveat shown beneath the rows. + /// Hints shown beneath the rows. A hint may be gated on the answers, so + /// guidance that only applies to one way of provisioning (virtual machines, + /// say) sits with the figures it qualifies rather than in a separate list. #[serde(default)] - pub note: Option, + pub notes: Vec, +} + +/// One hint beneath a requirement's rows, optionally gated on the answers. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NoteRow { + pub text: String, + /// Included only when this holds. Defaults to always. + #[serde(default = "Condition::always")] + pub when: Condition, } /// The size-varying spec rows for one size band of a [`Requirement`]. diff --git a/crates/pollen-server/tests/ruleset.rs b/crates/pollen-server/tests/ruleset.rs index 7d04a10..6f97d19 100644 --- a/crates/pollen-server/tests/ruleset.rs +++ b/crates/pollen-server/tests/ruleset.rs @@ -117,9 +117,8 @@ fn demo_config_is_blocking() { "expected {expected} to fire; got {ids:?}" ); } - // Not fired: the client hosts integrations; the servers aren't virtualised. + // Not fired: the client hosts the integrations themselves. assert!(!ids.contains(&"int-hosted")); - assert!(!ids.contains(&"prov-virtualised")); } #[test] @@ -984,9 +983,8 @@ fn the_smallest_band_advises_against_buying_a_server() { .iter() .find(|r| r.id == "req-central") .unwrap() - .note - .clone() - .unwrap_or_default() + .notes + .join(" ") }; let tiny = evaluate( diff --git a/ruleset.ron b/ruleset.ron index 4100991..aabe5ff 100644 --- a/ruleset.ron +++ b/ruleset.ron @@ -578,20 +578,6 @@ detail: "Disk and resources are hard to change on bare metal, so provision ahead: at least 500 GB SSD, 4 cores (prefer 8), and 16 GB RAM (prefer 32). Multiple disks and multiple TB are NOT usually required, though you may want to do RAID for redundancy.", ), ), - ( - id: "prov-virtualised", - source: "topology", - when: Any([ - Equals("onprem_form", "virtualised"), - Equals("onprem_form", "mixed"), - ]), - consequence: ( - status: Advisory, - audience: Client, - title: "Start the virtual machines small, and back them up", - detail: "Resources can scale, so start around 50 GB / 4 cores / 16 GB and grow. VM-level backups are recommended; telling BES is not required but helps disaster planning.", - ), - ), ( id: "iti-note", source: "topology", @@ -1190,6 +1176,15 @@ (label: "Operating system", value: "Linux", when: Equals("platform", "linux")), (label: "Operating system", value: "Windows Server", when: Equals("platform", "windows")), ], + notes: [ + ( + text: "On virtual machines these can scale, so it is fine to start smaller and grow into them. VM-level backups are recommended; telling BES is not required, but it helps disaster planning.", + when: Any([ + Equals("onprem_form", "virtualised"), + Equals("onprem_form", "mixed"), + ]), + ), + ], // Sized to the deployment. The figures track the recommended on-prem // tower tiers; the make is a suggestion, not a requirement. by_size: [ @@ -1246,7 +1241,16 @@ (label: "Operating system", value: "Linux", when: Equals("platform", "linux")), (label: "Operating system", value: "Windows Server", when: Equals("platform", "windows")), ], - note: Some("Keeps working within the facility when the link to Central is down."), + notes: [ + (text: "Keeps working within the facility when the link to Central is down."), + ( + text: "On virtual machines these can scale, so it is fine to start smaller and grow into them. VM-level backups are recommended; telling BES is not required, but it helps disaster planning.", + when: Any([ + Equals("onprem_form", "virtualised"), + Equals("onprem_form", "mixed"), + ]), + ), + ], by_size: [ ( size: "Tiny", diff --git a/web/openapi.json b/web/openapi.json index e0ee350..44b3fc9 100644 --- a/web/openapi.json +++ b/web/openapi.json @@ -795,7 +795,8 @@ "required": [ "id", "class", - "specs" + "specs", + "notes" ], "properties": { "class": { @@ -804,11 +805,11 @@ "id": { "type": "string" }, - "note": { - "type": [ - "string", - "null" - ] + "notes": { + "type": "array", + "items": { + "type": "string" + } }, "specs": { "type": "array", diff --git a/web/src/api-types.ts b/web/src/api-types.ts index 83153fb..1f110fc 100644 --- a/web/src/api-types.ts +++ b/web/src/api-types.ts @@ -352,7 +352,7 @@ export interface components { TriggeredRequirement: { class: string; id: string; - note?: string | null; + notes: string[]; specs: components["schemas"]["Spec"][]; summary?: string | null; }; diff --git a/web/src/app.css b/web/src/app.css index 22dac58..ebc8ab5 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -579,12 +579,18 @@ body { font-weight: 500; line-height: 1.45; } -/* Advisory, so it takes the sky informational wash rather than a severity hue. */ -.req-note { - margin: 0; +/* Advisory, so it takes the sky informational wash rather than a severity hue. + Several hints share one wash so the card ends in a single block. */ +.req-notes { + display: flex; + flex-direction: column; + gap: 7px; padding: 10px 16px; background: var(--sky-tint); border-top: 1px solid #bfe4f8; +} +.req-note { + margin: 0; font-size: 12px; color: var(--navy-deep); line-height: 1.45; diff --git a/web/src/components/Artifact.tsx b/web/src/components/Artifact.tsx index 8dae2eb..514277f 100644 --- a/web/src/components/Artifact.tsx +++ b/web/src/components/Artifact.tsx @@ -204,10 +204,14 @@ export default function Artifact({ view }: { view: AppView }) { ))} - {r.note && ( -

- -

+ {r.notes.length > 0 && ( +
+ {r.notes.map((n) => ( +

+ +

+ ))} +
)} ))} From 3086d2696faf106a770fec74a7e3dd06faf42bb0 Mon Sep 17 00:00:00 2001 From: Edwin Monk-Fromont <1274422+edmofro@users.noreply.github.com> Date: Thu, 10 Sep 2026 14:09:35 +1200 Subject: [PATCH 2/6] G1: move the bare-metal provisioning hint into the requirements too "Provision the physical servers generously up front" had the same problem as its virtual-machine counterpart: an action restating figures that contradicted the sized rows. Moved to a hint gated on physical servers, keeping the headroom judgement and the RAID and multiple-disk guidance while dropping the figures. A deployment running both kinds now gets both hints, which is what "Both" means. --- .workhorse/specs/wizard/onboarding.md | 3 ++- crates/pollen-server/tests/ruleset.rs | 1 - ruleset.ron | 29 +++++++++++++-------------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/.workhorse/specs/wizard/onboarding.md b/.workhorse/specs/wizard/onboarding.md index b025da2..254d723 100644 --- a/.workhorse/specs/wizard/onboarding.md +++ b/.workhorse/specs/wizard/onboarding.md @@ -266,7 +266,8 @@ A server's processor, memory and storage scale with the deployment's derived siz A row may instead be tied to an answer, so the operating system row names the platform the reader chose rather than listing what is available. A block can carry hints beneath its rows, and a hint may be tied to an answer the same way. -Guidance about how to provision the figures belongs here rather than among the actions: that virtual machines can start smaller and grow into the figures qualifies the figures themselves, so it sits under them and appears only when the servers are virtual. +Guidance about how to provision the figures belongs here rather than among the actions, because it qualifies the figures themselves: that virtual machines can start smaller and grow into them, or that physical hardware should be bought with headroom because it is hard to change later. +Each appears only for the way of provisioning it describes, and a deployment with both kinds gets both. Such a hint states the guidance without restating the figures, which would contradict the rows above it. Working out what is needed is the tool's job, so the requirements state it plainly rather than hedging that a larger deployment might need more. The make of a server is a suggestion, never a requirement: the block leads with the specification a server must meet, not a product to buy. diff --git a/crates/pollen-server/tests/ruleset.rs b/crates/pollen-server/tests/ruleset.rs index 6f97d19..eb26bcf 100644 --- a/crates/pollen-server/tests/ruleset.rs +++ b/crates/pollen-server/tests/ruleset.rs @@ -105,7 +105,6 @@ fn demo_config_is_blocking() { "int-capacity", "region-other", "plat-windows", - "prov-baremetal", "iti-note", "dns-client", "remote-other", diff --git a/ruleset.ron b/ruleset.ron index aabe5ff..98c0968 100644 --- a/ruleset.ron +++ b/ruleset.ron @@ -563,21 +563,6 @@ ), // ── Client-hosted server provisioning ─────────────────────────────── - ( - id: "prov-baremetal", - source: "topology", - when: Any([ - Equals("onprem_form", "baremetal"), - Equals("onprem_form", "mixed"), - ]), - consequence: ( - status: Requirement, - audience: Client, - types: [Operational], - title: "Provision the physical servers generously up front", - detail: "Disk and resources are hard to change on bare metal, so provision ahead: at least 500 GB SSD, 4 cores (prefer 8), and 16 GB RAM (prefer 32). Multiple disks and multiple TB are NOT usually required, though you may want to do RAID for redundancy.", - ), - ), ( id: "iti-note", source: "topology", @@ -1184,6 +1169,13 @@ Equals("onprem_form", "mixed"), ]), ), + ( + text: "Physical hardware is hard to change later, so buy with headroom above these figures rather than matching them exactly. Multiple disks or multiple terabytes are not usually needed, though RAID is worth considering for redundancy.", + when: Any([ + Equals("onprem_form", "baremetal"), + Equals("onprem_form", "mixed"), + ]), + ), ], // Sized to the deployment. The figures track the recommended on-prem // tower tiers; the make is a suggestion, not a requirement. @@ -1250,6 +1242,13 @@ Equals("onprem_form", "mixed"), ]), ), + ( + text: "Physical hardware is hard to change later, so buy with headroom above these figures rather than matching them exactly. Multiple disks or multiple terabytes are not usually needed, though RAID is worth considering for redundancy.", + when: Any([ + Equals("onprem_form", "baremetal"), + Equals("onprem_form", "mixed"), + ]), + ), ], by_size: [ ( From af216d0a003de387ec1304452855d45a47a6428d Mon Sep 17 00:00:00 2001 From: Edwin Monk-Fromont Date: Thu, 10 Sep 2026 14:57:32 +1200 Subject: [PATCH 3/6] G1: remove card-scoped artefacts before merge --- .workhorse/plans/g1/plan.md | 71 ---------------------------- .workhorse/test-cases/g1/overview.md | 34 ------------- 2 files changed, 105 deletions(-) delete mode 100644 .workhorse/plans/g1/plan.md delete mode 100644 .workhorse/test-cases/g1/overview.md diff --git a/.workhorse/plans/g1/plan.md b/.workhorse/plans/g1/plan.md deleted file mode 100644 index d6eb10b..0000000 --- a/.workhorse/plans/g1/plan.md +++ /dev/null @@ -1,71 +0,0 @@ -# G1 · Display compute requirements in form submission results - -## Goal - -The finalised artifact and its PDF should present concrete compute requirements -(processor, memory, storage, network, OS/software) for each server and device -class actually present in the deployment. The numbers are the authoritative -recommended base-level specs from the Tamanu "Compute resource recommendations" -reference; which classes appear is driven by the answers. - -## Design - -Keep the ruleset as data. Add a `requirements` block to the ruleset, evaluated -by the engine exactly like `rules`: each entry has a `when` condition and a -requirement profile (server/device class, a short who-provisions summary, a list -of spec rows, an optional note). The engine emits the union of triggered -requirements; the artifact renders them in a new "Compute requirements" section. - -Requirements surface only for classes someone must act on / provision: - -- **Central server** — when client-hosted (`central == clienthosted`). BES-cloud - Central is provisioned by BES, so it carries no client-facing requirement. -- **Facility server** — when client-hosted facilities are present and not every - site runs an Iti (`hosting_where` allclient/mix AND `iti_use != all`). -- **Tamanu Iti mini-server** — when `iti_use` is some/all. BES-built; only its - network needs stating. -- **User devices (workstations)** — always. The client provides these regardless - of hosting. -- **Mobile devices** — when mobile users are in play (`mobile` m1/m2/m3). - -Numbers are the recommended base level from the reference doc. Per-size scaling -is deliberately out of scope: the doc gives one base tier and says higher tiers -are advised separately by BES, and inventing per-band figures would state costs -nobody has confirmed. The size band already appears in the artifact header. - -## Steps - -- [x] Add `Requirement` + `Spec` to the ruleset model; `requirements` on `Ruleset` (serde default) -- [x] Validate requirement id uniqueness in `Ruleset::validate()` -- [x] Emit `requirements` (`TriggeredRequirement`) from the engine `evaluate()` -- [x] Export the new types from `ruleset/mod.rs` -- [x] Author the `requirements` block in `ruleset.ron` with the reference specs -- [x] Regenerate `web/openapi.json` + `api-types.ts`; add wire type re-exports -- [x] Render a "Compute requirements" section in `Artifact.tsx` (+ CSS) -- [x] Update the WIZ spec: outputs, PDF ordering, engine model note -- [x] Rust engine tests for presence gating; extend `tests/ruleset.rs` -- [x] Create `.workhorse/test-cases/g1/overview.md` -- [x] `just check`, `just test`, frontend typecheck - -## Refinement: size-scaling (after the price-list resource) - -The price list gives authoritative per-band server specs, so compute requirements -now scale with the derived size band instead of showing one baseline tier. - -- Server profiles (Central, Facility) carry size-invariant rows in `specs` - (network, OS) plus per-band rows in `by_size` (processor, memory, storage). The - engine resolves `by_size` against `derived["size"]` and leads with those rows. -- Figures follow the recommended on-prem tower tiers (small 2c/16GB/480GB, - medium 4c/16GB/960GB, large 8c/32GB/2TB); Tiny reuses small figures plus an - advisory to host with BES or use an Iti rather than buy a server. Make is a - suggestion, not a requirement. -- The Iti profile now states its one-model hardware spec (4c/8GB/500GB SSD). -- Only client-provisioned classes appear (what they'd buy to self-host); BES-cloud - servers stay off — confirmed with the user. -- Cost/pricing deferred: user chose "cost tier only" and will return to pricing. - A per-server tier would just restate the deployment size band, so no cost rows - this pass. Wire type and frontend unchanged (size resolved server-side). - -- [x] Model: `by_size`/`SizeSpecs`, validation, engine resolution, ruleset rework -- [x] Tests for size-scaling + Tiny advisory; spec updated to match -- [x] `cargo test`, clippy, frontend build, playwright e2e all green diff --git a/.workhorse/test-cases/g1/overview.md b/.workhorse/test-cases/g1/overview.md deleted file mode 100644 index 377f447..0000000 --- a/.workhorse/test-cases/g1/overview.md +++ /dev/null @@ -1,34 +0,0 @@ -# G1 · Compute requirements in results - -Scenarios verifying the artifact presents compute requirements for the classes a -deployment actually uses, drawn from the ruleset (verifies spec: WIZ, Compute -requirements). - -## Engine: which classes surface - -- [x] Default path (BES-cloud Central, all-client facilities, no mobile) surfaces facility server and user devices, but not Central, Iti, or mobile -- [x] A fully BES-cloud deployment with no mobile surfaces only user devices -- [x] Client-hosted Central surfaces the Central server requirement -- [x] Mobile users surface the mobile device requirement; an unsure mobile count surfaces nothing -- [x] Some sites on Iti keep the facility-server requirement and add the Iti one; every site on Iti drops the facility server and keeps only Iti -- [x] Every requirement profile in the ruleset names a class and has at least one spec row (flat or per-band) - -## Size scaling - -- [x] A client-hosted server's processor/memory/storage scale with the derived size band (Tiny vs Large give different rows) -- [x] Size-varying rows lead; network and OS rows follow -- [x] The smallest band carries an advisory to host with BES or use an Iti rather than buy a server; larger bands do not -- [x] The Iti profile states its one-model hardware spec (4c/8GB/500GB), not size-varying -- [x] A draft not yet sized (bands unanswered) falls back to the lightest band's rows - -## Artifact rendering - -- [x] The finalised web view shows a "Compute requirements" section below the consequence groups, one block per present class, each with its spec rows -- [ ] Each block shows the who-provisions summary and, where authored, the note -- [x] The section is absent from no deployment (user devices always present, so it never renders empty) -- [ ] The PDF export includes the compute requirements with every block expanded - -## Ruleset integrity - -- [x] The bundled ruleset with requirements parses, validates (unique requirement ids), and hashes deterministically -- [x] An older artifact bound to a ruleset without a `requirements` block still loads (serde default) From a44dffdc683735e4fbc16e2eb89c475ac76b8b58 Mon Sep 17 00:00:00 2001 From: Edwin Monk-Fromont <1274422+edmofro@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:08:17 +1200 Subject: [PATCH 4/6] G1: say which way the VM hint departs from the figures "It is fine to start smaller and grow into them" left it unclear whether the figures were the target or the starting point. Say it outright: the figures are a target rather than a day-one commitment, and name storage as the part that can start smaller. Read against the bare-metal hint, the pair now contrasts cleanly: grow into these figures on a VM, buy above them on hardware. --- .workhorse/specs/wizard/onboarding.md | 3 ++- ruleset.ron | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.workhorse/specs/wizard/onboarding.md b/.workhorse/specs/wizard/onboarding.md index 254d723..94981ff 100644 --- a/.workhorse/specs/wizard/onboarding.md +++ b/.workhorse/specs/wizard/onboarding.md @@ -266,7 +266,8 @@ A server's processor, memory and storage scale with the deployment's derived siz A row may instead be tied to an answer, so the operating system row names the platform the reader chose rather than listing what is available. A block can carry hints beneath its rows, and a hint may be tied to an answer the same way. -Guidance about how to provision the figures belongs here rather than among the actions, because it qualifies the figures themselves: that virtual machines can start smaller and grow into them, or that physical hardware should be bought with headroom because it is hard to change later. +Guidance about how to provision the figures belongs here rather than among the actions, because it qualifies the figures themselves: that a virtual machine can be resized, so the figures are a target rather than a day-one commitment, or that physical hardware should be bought with headroom above them because it is hard to change later. +Such a hint says which way it departs from the figures it sits under, since "start smaller" alone would leave a reader unsure whether the figures were the target or the starting point. Each appears only for the way of provisioning it describes, and a deployment with both kinds gets both. Such a hint states the guidance without restating the figures, which would contradict the rows above it. Working out what is needed is the tool's job, so the requirements state it plainly rather than hedging that a larger deployment might need more. diff --git a/ruleset.ron b/ruleset.ron index 98c0968..ddfd71c 100644 --- a/ruleset.ron +++ b/ruleset.ron @@ -1163,7 +1163,7 @@ ], notes: [ ( - text: "On virtual machines these can scale, so it is fine to start smaller and grow into them. VM-level backups are recommended; telling BES is not required, but it helps disaster planning.", + text: "A virtual machine can be resized, so these figures are a target rather than a day-one commitment: storage especially can start smaller and grow. VM-level backups are recommended; telling BES is not required, but it helps disaster planning.", when: Any([ Equals("onprem_form", "virtualised"), Equals("onprem_form", "mixed"), @@ -1236,7 +1236,7 @@ notes: [ (text: "Keeps working within the facility when the link to Central is down."), ( - text: "On virtual machines these can scale, so it is fine to start smaller and grow into them. VM-level backups are recommended; telling BES is not required, but it helps disaster planning.", + text: "A virtual machine can be resized, so these figures are a target rather than a day-one commitment: storage especially can start smaller and grow. VM-level backups are recommended; telling BES is not required, but it helps disaster planning.", when: Any([ Equals("onprem_form", "virtualised"), Equals("onprem_form", "mixed"), From 440ea0fcee3cf6c61c12aa834f8752096bad7d4b Mon Sep 17 00:00:00 2001 From: Edwin Monk-Fromont <1274422+edmofro@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:33:26 +1200 Subject: [PATCH 5/6] G1: tighten the provisioning hints Both hints were wordier than a hint should be, and the virtual-machine one did not say that the figures are the recommendation. Lead with aiming for them, and allow a VM to sit slightly under only where its host has room to grow. The pair now reads as one contrast: aim for these on a VM, buy above them on hardware. --- .workhorse/specs/wizard/onboarding.md | 4 ++-- ruleset.ron | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.workhorse/specs/wizard/onboarding.md b/.workhorse/specs/wizard/onboarding.md index 94981ff..2cf8a2e 100644 --- a/.workhorse/specs/wizard/onboarding.md +++ b/.workhorse/specs/wizard/onboarding.md @@ -266,8 +266,8 @@ A server's processor, memory and storage scale with the deployment's derived siz A row may instead be tied to an answer, so the operating system row names the platform the reader chose rather than listing what is available. A block can carry hints beneath its rows, and a hint may be tied to an answer the same way. -Guidance about how to provision the figures belongs here rather than among the actions, because it qualifies the figures themselves: that a virtual machine can be resized, so the figures are a target rather than a day-one commitment, or that physical hardware should be bought with headroom above them because it is hard to change later. -Such a hint says which way it departs from the figures it sits under, since "start smaller" alone would leave a reader unsure whether the figures were the target or the starting point. +Guidance about how to provision the figures belongs here rather than among the actions, because it qualifies the figures themselves: that a virtual machine may sit slightly under them where its host has room to grow, or that physical hardware should be bought above them because it is hard to change later. +The figures are what to aim for either way, and a hint says which direction it departs in, since "smaller" alone would leave a reader unsure whether the figures were the target or the starting point. Each appears only for the way of provisioning it describes, and a deployment with both kinds gets both. Such a hint states the guidance without restating the figures, which would contradict the rows above it. Working out what is needed is the tool's job, so the requirements state it plainly rather than hedging that a larger deployment might need more. diff --git a/ruleset.ron b/ruleset.ron index ddfd71c..c0e0c4e 100644 --- a/ruleset.ron +++ b/ruleset.ron @@ -1163,14 +1163,14 @@ ], notes: [ ( - text: "A virtual machine can be resized, so these figures are a target rather than a day-one commitment: storage especially can start smaller and grow. VM-level backups are recommended; telling BES is not required, but it helps disaster planning.", + text: "Aim for these figures; a virtual machine may sit slightly under if its host has room to grow. VM-level backups are recommended, and worth telling BES about for disaster planning.", when: Any([ Equals("onprem_form", "virtualised"), Equals("onprem_form", "mixed"), ]), ), ( - text: "Physical hardware is hard to change later, so buy with headroom above these figures rather than matching them exactly. Multiple disks or multiple terabytes are not usually needed, though RAID is worth considering for redundancy.", + text: "Buy above these figures: physical hardware is hard to change later. Multiple disks or terabytes are not usually needed, though RAID helps redundancy.", when: Any([ Equals("onprem_form", "baremetal"), Equals("onprem_form", "mixed"), @@ -1236,14 +1236,14 @@ notes: [ (text: "Keeps working within the facility when the link to Central is down."), ( - text: "A virtual machine can be resized, so these figures are a target rather than a day-one commitment: storage especially can start smaller and grow. VM-level backups are recommended; telling BES is not required, but it helps disaster planning.", + text: "Aim for these figures; a virtual machine may sit slightly under if its host has room to grow. VM-level backups are recommended, and worth telling BES about for disaster planning.", when: Any([ Equals("onprem_form", "virtualised"), Equals("onprem_form", "mixed"), ]), ), ( - text: "Physical hardware is hard to change later, so buy with headroom above these figures rather than matching them exactly. Multiple disks or multiple terabytes are not usually needed, though RAID is worth considering for redundancy.", + text: "Buy above these figures: physical hardware is hard to change later. Multiple disks or terabytes are not usually needed, though RAID helps redundancy.", when: Any([ Equals("onprem_form", "baremetal"), Equals("onprem_form", "mixed"), From 609bd624c6f05e89e045564e8f2a6243935ee171 Mon Sep 17 00:00:00 2001 From: Edwin Monk-Fromont <1274422+edmofro@users.noreply.github.com> Date: Fri, 11 Sep 2026 09:59:16 +1200 Subject: [PATCH 6/6] G1: frame the hardware figures as a minimum "Buy above these figures" read as an instruction without saying why. State what the figures are on hardware, and leave the buying decision to the reader: a minimum, with more being wise to allow for growth. --- .workhorse/specs/wizard/onboarding.md | 4 ++-- ruleset.ron | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.workhorse/specs/wizard/onboarding.md b/.workhorse/specs/wizard/onboarding.md index 2cf8a2e..1ff1fa2 100644 --- a/.workhorse/specs/wizard/onboarding.md +++ b/.workhorse/specs/wizard/onboarding.md @@ -266,8 +266,8 @@ A server's processor, memory and storage scale with the deployment's derived siz A row may instead be tied to an answer, so the operating system row names the platform the reader chose rather than listing what is available. A block can carry hints beneath its rows, and a hint may be tied to an answer the same way. -Guidance about how to provision the figures belongs here rather than among the actions, because it qualifies the figures themselves: that a virtual machine may sit slightly under them where its host has room to grow, or that physical hardware should be bought above them because it is hard to change later. -The figures are what to aim for either way, and a hint says which direction it departs in, since "smaller" alone would leave a reader unsure whether the figures were the target or the starting point. +Guidance about how to provision the figures belongs here rather than among the actions, because it qualifies the figures themselves: that a virtual machine may sit slightly under them where its host has room to grow, or that on physical hardware they are a minimum worth exceeding, since it cannot be resized later. +A hint says which direction it departs in, since "smaller" alone would leave a reader unsure whether the figures were the target or the starting point. Each appears only for the way of provisioning it describes, and a deployment with both kinds gets both. Such a hint states the guidance without restating the figures, which would contradict the rows above it. Working out what is needed is the tool's job, so the requirements state it plainly rather than hedging that a larger deployment might need more. diff --git a/ruleset.ron b/ruleset.ron index c0e0c4e..5746300 100644 --- a/ruleset.ron +++ b/ruleset.ron @@ -1170,7 +1170,7 @@ ]), ), ( - text: "Buy above these figures: physical hardware is hard to change later. Multiple disks or terabytes are not usually needed, though RAID helps redundancy.", + text: "These figures are a minimum; buying more is wise to allow for growth. Multiple disks or terabytes are not usually needed, though RAID helps redundancy.", when: Any([ Equals("onprem_form", "baremetal"), Equals("onprem_form", "mixed"), @@ -1243,7 +1243,7 @@ ]), ), ( - text: "Buy above these figures: physical hardware is hard to change later. Multiple disks or terabytes are not usually needed, though RAID helps redundancy.", + text: "These figures are a minimum; buying more is wise to allow for growth. Multiple disks or terabytes are not usually needed, though RAID helps redundancy.", when: Any([ Equals("onprem_form", "baremetal"), Equals("onprem_form", "mixed"),