17 Commits
Author SHA1 Message Date
portal release 955a33609a Match portal v0.5.5
Test / test (push) Successful in 30s
Publish release / publish (push) Successful in 25s
2026-09-30 07:54:29 +02:00
portal release 9b96111916 Match portal v0.5.4
Test / test (push) Successful in 30s
Publish release / publish (push) Successful in 25s
2026-09-29 21:14:02 +02:00
portal release f2fc5097f7 Match portal v0.5.3
Test / test (push) Successful in 29s
Publish release / publish (push) Successful in 25s
2026-09-29 13:33:07 +02:00
portal release f2ca00b309 Match portal v0.5.2
Test / test (push) Successful in 1m17s
Publish release / publish (push) Successful in 26s
2026-09-28 18:11:49 +02:00
portal release ac4af4395a Match portal v0.5.1
Test / test (push) Successful in 1m0s
Publish release / publish (push) Successful in 26s
2026-09-28 17:51:11 +02:00
portal release 1e7245b062 Match portal v0.5.0
Test / test (push) Failing after 34s
Publish release / publish (push) Successful in 28s
2026-09-28 16:50:47 +02:00
portal release a847d0defa Match portal v0.4.0
Test / test (push) Successful in 29s
Publish release / publish (push) Successful in 25s
2026-09-24 21:37:28 +02:00
portal release 57b733e606 Match portal v0.3.47
Test / test (push) Successful in 29s
Publish release / publish (push) Successful in 24s
2026-09-24 05:20:11 +02:00
Bendik Aagaard LynghaugandClaude Fable 5.1 3923d4d0dc needs: stage_words, each state in the site's own words, carried into the simulation model
Test / test (push) Successful in 29s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-24 05:17:11 +02:00
Bendik Aagaard LynghaugandClaude Fable 5.1 00db014eae A card with no button is read, never picked
Test / test (push) Successful in 1m6s
The opener that says what the business does and the look-around card
lead nowhere: they are what the page says before its options, so a
task and a simulation step carry them as `page` context and offer
only the cards that lead somewhere. Five personas per round had been
"choosing" the opener and counted as misroutes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-23 22:36:20 +02:00
portal release d2450fcb75 Match portal v0.3.46
Test / test (push) Successful in 30s
Publish release / publish (push) Successful in 36s
2026-09-23 22:21:14 +02:00
portal release 0841ed561a Match portal v0.3.45
Test / test (push) Successful in 33s
Publish release / publish (push) Successful in 28s
2026-09-23 21:26:51 +02:00
portal release 79f85132c5 Match portal v0.3.44
Test / test (push) Successful in 30s
Publish release / publish (push) Successful in 26s
2026-09-23 21:18:10 +02:00
portal release 58d68c3bff Match portal v0.3.43
Test / test (push) Successful in 29s
Publish release / publish (push) Successful in 25s
2026-09-23 19:48:01 +02:00
portal release 0cf70502fc Match portal v0.3.42
Publish release / publish (push) Successful in 25s
Test / test (push) Successful in 29s
2026-09-23 14:35:49 +02:00
portal release fb66694b85 Match portal v0.3.41
Publish release / publish (push) Successful in 25s
Test / test (push) Successful in 1m6s
2026-09-23 11:55:10 +02:00
Bendik Aagaard LynghaugandClaude Fable 5.1 959e5f6ef0 check: compile and validate the access policy; --access prints the matrix
Test / test (push) Successful in 29s
Every check now compiles the content into portal's Cedar policy and
fails when it does not validate against the schema - a rule the
schema cannot express would grant nothing at runtime. `--access`
prints the matrix: action, resource, who, and the state move where it
applies. Pinned to portal v0.3.40, where the policy lives.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L4jrCgLiKKHAEFuZUJjckH
2026-09-23 11:48:33 +02:00
6 changed files with 352 additions and 13 deletions
+54 -2
View File
@@ -3,9 +3,61 @@
One line per change, grouped by the release that shipped it. Newest
first. Each release names the portal tag it is built against.
## 0.3.40 (2026-09-23) - portal v0.3.40
## 0.5.5 (2026-09-30) - portal v0.5.5
- Matches portal v0.3.40 (released by portal's publish job).
- Matches portal v0.5.5 (released by portal's publish job).
## 0.5.4 (2026-09-29) - portal v0.5.4
- Matches portal v0.5.4 (released by portal's publish job).
## 0.5.3 (2026-09-29) - portal v0.5.3
- Matches portal v0.5.3 (released by portal's publish job).
## 0.5.2 (2026-09-28) - portal v0.5.2
- Matches portal v0.5.2 (released by portal's publish job).
## 0.5.1 (2026-09-28) - portal v0.5.1
- Matches portal v0.5.1 (released by portal's publish job).
## 0.5.0 (2026-09-28) - portal v0.5.0
- Matches portal v0.5.0 (released by portal's publish job).
## 0.4.0 (2026-09-24) - portal v0.4.0
- Matches portal v0.4.0 (released by portal's publish job).
## 0.3.47 (2026-09-24) - portal v0.3.47
- Matches portal v0.3.47 (released by portal's publish job).
## 0.3.46 (2026-09-23) - portal v0.3.46
- Matches portal v0.3.46 (released by portal's publish job).
## 0.3.45 (2026-09-23) - portal v0.3.45
- Matches portal v0.3.45 (released by portal's publish job).
## 0.3.44 (2026-09-23) - portal v0.3.44
- Matches portal v0.3.44 (released by portal's publish job).
## 0.3.43 (2026-09-23) - portal v0.3.43
- Matches portal v0.3.43 (released by portal's publish job).
## 0.3.42 (2026-09-23) - portal v0.3.42
- Matches portal v0.3.42 (released by portal's publish job).
## 0.3.41 (2026-09-23) - portal v0.3.41
- Matches portal v0.3.41 (released by portal's publish job).
## 0.3.39 (2026-09-23) - portal v0.3.39
Generated
+224 -4
View File
@@ -798,7 +798,7 @@ dependencies = [
"linked-hash-map",
"linked_hash_set",
"miette",
"nonempty",
"nonempty 0.12.0",
"ref-cast",
"regex",
"rustc-literal-escaper",
@@ -1060,6 +1060,12 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
[[package]]
name = "crypto-bigint"
version = "0.5.5"
@@ -1161,6 +1167,20 @@ dependencies = [
"syn 3.0.6",
]
[[package]]
name = "dashmap"
version = "6.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c"
dependencies = [
"cfg-if",
"crossbeam-utils",
"hashbrown 0.14.5",
"lock_api",
"once_cell",
"parking_lot_core",
]
[[package]]
name = "data-encoding"
version = "2.11.1"
@@ -1523,6 +1543,16 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "forwarded-header-value"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9"
dependencies = [
"nonempty 0.7.0",
"thiserror 1.0.69",
]
[[package]]
name = "fs_extra"
version = "1.3.0"
@@ -1600,6 +1630,12 @@ version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
[[package]]
name = "futures-timer"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968"
[[package]]
name = "futures-util"
version = "0.3.34"
@@ -1648,9 +1684,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi 5.3.0",
"wasip2",
"wasm-bindgen",
]
[[package]]
@@ -1701,6 +1739,29 @@ dependencies = [
"web-sys",
]
[[package]]
name = "governor"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9efcab3c1958580ff1f25a2a41be1668f7603d849bb63af523b208a3cc1223b8"
dependencies = [
"cfg-if",
"dashmap",
"futures-sink",
"futures-timer",
"futures-util",
"getrandom 0.3.4",
"hashbrown 0.16.1",
"nonzero_ext",
"parking_lot",
"portable-atomic",
"quanta",
"rand 0.9.5",
"smallvec",
"spinning_top",
"web-time",
]
[[package]]
name = "group"
version = "0.13.0"
@@ -1762,6 +1823,23 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash 0.2.0",
]
[[package]]
name = "hashbrown"
version = "0.17.1"
@@ -1993,6 +2071,19 @@ dependencies = [
"webpki-roots",
]
[[package]]
name = "hyper-timeout"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0"
dependencies = [
"hyper 1.11.1",
"hyper-util",
"pin-project-lite",
"tokio",
"tower-service",
]
[[package]]
name = "hyper-util"
version = "0.1.20"
@@ -2196,7 +2287,7 @@ checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0"
[[package]]
name = "iris"
version = "0.3.40"
version = "0.5.5"
dependencies = [
"anyhow",
"async-nats",
@@ -2925,6 +3016,12 @@ dependencies = [
"signatory",
]
[[package]]
name = "nonempty"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7"
[[package]]
name = "nonempty"
version = "0.12.0"
@@ -2934,6 +3031,12 @@ dependencies = [
"serde",
]
[[package]]
name = "nonzero_ext"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21"
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
@@ -3303,12 +3406,13 @@ dependencies = [
[[package]]
name = "portal"
version = "0.3.40"
source = "git+https://project.uhhm.no/uhhm/portal.git?tag=v0.3.40#44d5ef368371b7d0d004d9bb0c89a9db267fcfe6"
version = "0.5.5"
source = "git+https://project.uhhm.no/uhhm/portal.git?tag=v0.5.5#77867090ed443d56f23bd9e617514c56171dba7f"
dependencies = [
"anyhow",
"arc-swap",
"async-nats",
"async-trait",
"aws-sdk-s3",
"axum",
"base64 0.22.1",
@@ -3333,6 +3437,7 @@ dependencies = [
"tower",
"tower-http",
"tower-sessions",
"tower_governor",
"tracing",
"tracing-subscriber",
"url",
@@ -3482,6 +3587,21 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
[[package]]
name = "quanta"
version = "0.12.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7"
dependencies = [
"crossbeam-utils",
"libc",
"once_cell",
"raw-cpuid",
"wasi",
"web-sys",
"winapi",
]
[[package]]
name = "quinn"
version = "0.11.12"
@@ -3666,6 +3786,15 @@ dependencies = [
"rand_core 0.10.1",
]
[[package]]
name = "raw-cpuid"
version = "11.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186"
dependencies = [
"bitflags 2.13.2",
]
[[package]]
name = "reactive_graph"
version = "0.2.14"
@@ -4566,6 +4695,15 @@ version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3"
[[package]]
name = "spinning_top"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300"
dependencies = [
"lock_api",
]
[[package]]
name = "spki"
version = "0.7.3"
@@ -4865,6 +5003,17 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-stream"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b"
dependencies = [
"futures-core",
"pin-project-lite",
"tokio",
]
[[package]]
name = "tokio-tungstenite"
version = "0.29.0"
@@ -4943,6 +5092,35 @@ dependencies = [
"winnow",
]
[[package]]
name = "tonic"
version = "0.14.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef"
dependencies = [
"async-trait",
"axum",
"base64 0.22.1",
"bytes",
"h2 0.4.19",
"http 1.5.0",
"http-body 1.1.0",
"http-body-util",
"hyper 1.11.1",
"hyper-timeout",
"hyper-util",
"percent-encoding",
"pin-project",
"socket2 0.6.5",
"sync_wrapper",
"tokio",
"tokio-stream",
"tower",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower"
version = "0.5.3"
@@ -4951,9 +5129,12 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
dependencies = [
"futures-core",
"futures-util",
"indexmap 2.14.2",
"pin-project-lite",
"slab",
"sync_wrapper",
"tokio",
"tokio-util",
"tower-layer",
"tower-service",
"tracing",
@@ -5065,6 +5246,23 @@ dependencies = [
"tower-sessions-core",
]
[[package]]
name = "tower_governor"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44de9b94d849d3c46e06a883d72d408c2de6403367b39df2b1c9d9e7b6736fe6"
dependencies = [
"axum",
"forwarded-header-value",
"governor",
"http 1.5.0",
"pin-project",
"thiserror 2.0.20",
"tonic",
"tower",
"tracing",
]
[[package]]
name = "tracing"
version = "0.1.44"
@@ -5472,6 +5670,22 @@ dependencies = [
"rustls-pki-types",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.11"
@@ -5481,6 +5695,12 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.62.2"
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "iris"
version = "0.3.40"
version = "0.5.5"
edition = "2021"
description = "The iris over a portal site: nothing from a content repo comes through until it checks out"
license = "MIT"
@@ -10,7 +10,7 @@ repository = "https://project.uhhm.no/uhhm/iris"
# The site's own content types and loaders, so iris reads a repo exactly
# the way the running site does. Pinned to the portal release it
# matches; bumping this tag is what a new iris release is.
portal = { git = "https://project.uhhm.no/uhhm/portal.git", tag = "v0.3.40", features = ["ssr"] }
portal = { git = "https://project.uhhm.no/uhhm/portal.git", tag = "v0.5.5", features = ["ssr"] }
anyhow = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
+21 -1
View File
@@ -23,6 +23,7 @@ pub async fn run(argv: Vec<String>) -> anyhow::Result<()> {
let mut min_accuracy: f64 = 0.0;
let mut skim = false;
let mut sim_out: Option<String> = None;
let mut show_access = false;
while let Some(arg) = args.next() {
match arg.as_str() {
@@ -33,6 +34,7 @@ pub async fn run(argv: Vec<String>) -> anyhow::Result<()> {
"--needs-tasks" => tasks_out = args.next(),
"--skim" => skim = true,
"--needs-sim" => sim_out = args.next(),
"--access" => show_access = true,
"--needs-score" => answers_in = args.next(),
"--min-accuracy" => {
min_accuracy = args.next().and_then(|v| v.parse().ok()).unwrap_or(min_accuracy)
@@ -62,7 +64,7 @@ pub async fn run(argv: Vec<String>) -> anyhow::Result<()> {
(questions, aggregates_map, site, needs_raw)
} else {
eprintln!(
"usage: iris check (--repo <gitea-url> [--branch main] [--subdir questions] | --path <local-dir>) [--needs-tasks <out.jsonl> [--skim]] [--needs-sim <model.json>] [--needs-score <answers.jsonl> [--min-accuracy 0.8]]"
"usage: iris check (--repo <gitea-url> [--branch main] [--subdir questions] | --path <local-dir>) [--needs-tasks <out.jsonl> [--skim]] [--needs-sim <model.json>] [--access] [--needs-score <answers.jsonl> [--min-accuracy 0.8]]"
);
std::process::exit(2);
};
@@ -91,6 +93,24 @@ pub async fn run(argv: Vec<String>) -> anyhow::Result<()> {
questions.len(),
aggregates_map.len()
);
// The access policy the site will compile from this content.
// It must type-check against portal's schema: a rule the
// schema cannot express would grant nothing at runtime.
let policy = portal::access::Policy::from_content(&questions, &aggregates_map);
if let Err(e) = policy.validate() {
eprintln!("FAIL: access policy does not validate: {e}");
std::process::exit(1);
}
println!("OK: access policy, {} rule(s), validates", policy.rules.len());
if show_access {
println!("\n{:<16} {:<44} {:<40} {}", "ACTION", "RESOURCE", "WHO", "WHEN");
let mut rows = policy.rules.clone();
rows.sort_by(|a, b| (&a.resource, &a.action, &a.who).cmp(&(&b.resource, &b.action, &b.who)));
for r in rows {
println!("{:<16} {:<44} {:<40} {}", r.action, r.resource, r.who, r.when);
}
println!();
}
check_needs(
needs_raw.as_deref(),
&questions,
+1 -1
View File
@@ -44,7 +44,7 @@ async fn main() -> anyhow::Result<()> {
Some(flag) if flag.starts_with("--") => check::run(argv).await,
_ => {
eprintln!(
"iris {v} - matches portal v{v}\n\nusage:\n iris check (--repo <gitea-url> [--branch main] [--subdir questions] | --path <dir>) [--needs-tasks <out.jsonl> [--skim]] [--needs-sim <model.json>] [--needs-score <answers.jsonl> [--min-accuracy 0.8]]\n iris replay --path <dir> --nats <url> (--cases <cases.jsonl> | --report-only)",
"iris {v} - matches portal v{v}\n\nusage:\n iris check (--repo <gitea-url> [--branch main] [--subdir questions] | --path <dir>) [--needs-tasks <out.jsonl> [--skim]] [--needs-sim <model.json>] [--access] [--needs-score <answers.jsonl> [--min-accuracy 0.8]]\n iris replay --path <dir> --nats <url> (--cases <cases.jsonl> | --report-only)",
v = env!("CARGO_PKG_VERSION")
);
std::process::exit(2)
+50 -3
View File
@@ -73,6 +73,11 @@ pub struct Need {
/// as a warning, so the gap stays visible without failing CI.
#[serde(default)]
pub planned: bool,
/// Each state of the bucket in plain words, in the site's
/// language, for the scorer: state ids are ASCII, and a Norwegian
/// `ikke_lost` reads as English to a model.
#[serde(default)]
pub stage_words: std::collections::BTreeMap<String, String>,
/// The finished states that are a good outcome for the business -
/// a subset of `done_when`. "lost" is finished; "won" is success.
/// What a simulation or a live bucket report counts toward.
@@ -571,7 +576,8 @@ pub fn sim_model(
json!({
"page": step.page,
"question": page.name,
"options": page.alternatives.iter().filter(|a| !a.disabled).map(|a| json!({
"context": page_context(page),
"options": page.alternatives.iter().filter(|a| !a.disabled && leads_somewhere(a)).map(|a| json!({
"name": a.name,
"description": plain(&a.description),
})).collect::<Vec<_>>(),
@@ -597,6 +603,7 @@ pub fn sim_model(
json!({
"initial": schema.initial,
"transitions": schema.transitions,
"words": need.stage_words,
"desks": desks_over(questions, &need.lands_in).iter().map(|d| json!({
"page": d.page,
"group": d.group,
@@ -646,6 +653,41 @@ fn plain(text: &str) -> String {
text.split_whitespace().collect::<Vec<_>>().join(" ")
}
/// A card a visitor can act on: it leads to a page, records an
/// answer, or moves a record. A card with none of those is read, not
/// picked - the opener that says what the business does, the look
/// around - and is context on the page, never an option.
pub fn leads_somewhere(a: &Alternative) -> bool {
a.action.is_some() || a.record_as.is_some() || a.self_transition.is_some()
}
/// What the page says before its options: every card that leads
/// nowhere, its heading, description and features, as one line each.
pub fn page_context(page: &Question) -> String {
page.alternatives
.iter()
.filter(|a| !a.disabled && !leads_somewhere(a))
.map(|a| {
let mut line = a.name.clone();
let description = plain(&a.description);
if !description.is_empty() {
line.push_str(&format!(": {description}"));
}
for f in &a.features {
let text = plain(&f.description);
match (f.name.trim().is_empty(), text.is_empty()) {
(true, true) => {}
(true, false) => line.push_str(&format!(" {text}")),
(false, true) => line.push_str(&format!(" {}.", f.name.trim())),
(false, false) => line.push_str(&format!(" {}: {text}", f.name.trim())),
}
}
line
})
.collect::<Vec<_>>()
.join("\n")
}
/// Every (persona, page-on-their-path) pair with a real choice on it.
/// `skim` shows the engine only each alternative's heading - what a
/// visitor who never reads the description has to go on.
@@ -660,10 +702,11 @@ pub fn tasks(file: &NeedsFile, questions: &Questions, skim: bool) -> Vec<Task> {
};
for (i, step) in path.iter().enumerate() {
let page = &questions[&step.page];
let open: Vec<&Alternative> = page.alternatives.iter().filter(|a| !a.disabled).collect();
let open: Vec<&Alternative> = page.alternatives.iter().filter(|a| !a.disabled && leads_somewhere(a)).collect();
if open.len() < 2 {
continue;
}
let context = page_context(page);
let mut trail: Vec<String> = path[..=i].iter().map(|s| s.page.clone()).collect();
let expect: Vec<String> = viable(questions, need, page, need.max_steps - i, i == 0, false, &mut trail)
.into_iter()
@@ -684,7 +727,11 @@ pub fn tasks(file: &NeedsFile, questions: &Questions, skim: bool) -> Vec<Task> {
persona: persona.id.clone(),
need: need.id.clone(),
page: step.page.clone(),
state: BTreeMap::from([("body", plain(&persona.text))]),
state: if context.is_empty() {
BTreeMap::from([("body", plain(&persona.text))])
} else {
BTreeMap::from([("body", plain(&persona.text)), ("page", context)])
},
questions: BTreeMap::from([(
"pick",
ChoiceQuestion {