Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 0 additions & 71 deletions .workhorse/plans/g1/plan.md

This file was deleted.

6 changes: 6 additions & 0 deletions .workhorse/specs/wizard/onboarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,12 @@ 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, 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.
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.
Expand Down
34 changes: 0 additions & 34 deletions .workhorse/test-cases/g1/overview.md

This file was deleted.

25 changes: 17 additions & 8 deletions crates/pollen-server/src/ruleset/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -59,7 +61,7 @@ pub struct TriggeredRequirement {
pub class: String,
pub summary: Option<String>,
pub specs: Vec<Spec>,
pub note: Option<String>,
pub notes: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
Expand Down Expand Up @@ -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<String> = 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();
Expand Down
4 changes: 2 additions & 2 deletions crates/pollen-server/src/ruleset/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
15 changes: 13 additions & 2 deletions crates/pollen-server/src/ruleset/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SizeSpecs>,
/// 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<String>,
pub notes: Vec<NoteRow>,
}

/// 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`].
Expand Down
9 changes: 3 additions & 6 deletions crates/pollen-server/tests/ruleset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ fn demo_config_is_blocking() {
"int-capacity",
"region-other",
"plat-windows",
"prov-baremetal",
"iti-note",
"dns-client",
"remote-other",
Expand All @@ -117,9 +116,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]
Expand Down Expand Up @@ -984,9 +982,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(
Expand Down
63 changes: 33 additions & 30 deletions ruleset.ron
Original file line number Diff line number Diff line change
Expand Up @@ -563,35 +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: "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",
Expand Down Expand Up @@ -1190,6 +1161,22 @@
(label: "Operating system", value: "Linux", when: Equals("platform", "linux")),
(label: "Operating system", value: "Windows Server", when: Equals("platform", "windows")),
],
notes: [
(
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: "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"),
]),
),
],
// Sized to the deployment. The figures track the recommended on-prem
// tower tiers; the make is a suggestion, not a requirement.
by_size: [
Expand Down Expand Up @@ -1246,7 +1233,23 @@
(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: "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: "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"),
]),
),
],
by_size: [
(
size: "Tiny",
Expand Down
13 changes: 7 additions & 6 deletions web/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -795,7 +795,8 @@
"required": [
"id",
"class",
"specs"
"specs",
"notes"
],
"properties": {
"class": {
Expand All @@ -804,11 +805,11 @@
"id": {
"type": "string"
},
"note": {
"type": [
"string",
"null"
]
"notes": {
"type": "array",
"items": {
"type": "string"
}
},
"specs": {
"type": "array",
Expand Down
2 changes: 1 addition & 1 deletion web/src/api-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ export interface components {
TriggeredRequirement: {
class: string;
id: string;
note?: string | null;
notes: string[];
specs: components["schemas"]["Spec"][];
summary?: string | null;
};
Expand Down
12 changes: 9 additions & 3 deletions web/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
12 changes: 8 additions & 4 deletions web/src/components/Artifact.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -204,10 +204,14 @@ export default function Artifact({ view }: { view: AppView }) {
</div>
))}
</dl>
{r.note && (
<p className="req-note">
<Markup text={r.note} />
</p>
{r.notes.length > 0 && (
<div className="req-notes">
{r.notes.map((n) => (
<p className="req-note" key={n}>
<Markup text={n} />
</p>
))}
</div>
)}
</article>
))}
Expand Down