Compare commits
@@ -12,7 +12,9 @@ jobs:
|
|||||||
publish:
|
publish:
|
||||||
runs-on: bare
|
runs-on: bare
|
||||||
env:
|
env:
|
||||||
CARGO_HOME: /var/local/cargo
|
# Writable, unlike the shared /var/local/cargo - see test.yml.
|
||||||
|
CARGO_HOME: /var/local/cargo-target/iris-cargo-home
|
||||||
|
RUSTC_WRAPPER: /usr/bin/sccache
|
||||||
RUSTUP_HOME: /var/local/rustup
|
RUSTUP_HOME: /var/local/rustup
|
||||||
PATH: /var/local/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/bin
|
PATH: /var/local/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/bin
|
||||||
SCCACHE_DIR: /var/local/sccache
|
SCCACHE_DIR: /var/local/sccache
|
||||||
|
|||||||
@@ -10,7 +10,12 @@ jobs:
|
|||||||
runs-on: bare
|
runs-on: bare
|
||||||
env:
|
env:
|
||||||
# Same shared toolchain and cache as uhhm/portal's workflows.
|
# Same shared toolchain and cache as uhhm/portal's workflows.
|
||||||
CARGO_HOME: /var/local/cargo
|
# Not the shared /var/local/cargo: the runner may only read it, and
|
||||||
|
# the portal git dependency has to be cloned into CARGO_HOME. This
|
||||||
|
# one lives under the writable target dir; the rustc wrapper the
|
||||||
|
# shared config would have set is given here instead.
|
||||||
|
CARGO_HOME: /var/local/cargo-target/iris-cargo-home
|
||||||
|
RUSTC_WRAPPER: /usr/bin/sccache
|
||||||
RUSTUP_HOME: /var/local/rustup
|
RUSTUP_HOME: /var/local/rustup
|
||||||
PATH: /var/local/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/bin
|
PATH: /var/local/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/bin
|
||||||
SCCACHE_DIR: /var/local/sccache
|
SCCACHE_DIR: /var/local/sccache
|
||||||
|
|||||||
@@ -3,6 +3,86 @@
|
|||||||
One line per change, grouped by the release that shipped it. Newest
|
One line per change, grouped by the release that shipped it. Newest
|
||||||
first. Each release names the portal tag it is built against.
|
first. Each release names the portal tag it is built against.
|
||||||
|
|
||||||
|
## 0.5.8 (2026-09-30) - portal v0.5.8
|
||||||
|
|
||||||
|
- Matches portal v0.5.8 (released by portal's publish job).
|
||||||
|
|
||||||
|
## 0.5.7 (2026-09-30) - portal v0.5.7
|
||||||
|
|
||||||
|
- Matches portal v0.5.7 (released by portal's publish job).
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
- The effort budget counts what a person fills in: a `type: record` field (the case an answer is about, carried on the link) and a `hidden` preset are not asked and no longer count. An objection page whose form carried the application's id used to be one field over.
|
||||||
|
|
||||||
|
## 0.5.6 (2026-09-30) - portal v0.5.6
|
||||||
|
|
||||||
|
- Matches portal v0.5.6 (released by portal's publish job).
|
||||||
|
|
||||||
|
## 0.5.5 (2026-09-30) - portal v0.5.5
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
|
- Matches portal v0.3.39 (released by portal's publish job).
|
||||||
|
|
||||||
|
## 0.1.1 (2026-09-22) - portal v0.3.36
|
||||||
|
|
||||||
|
- `check --repo` fetches needs.yaml itself; the v0.1.0 tag never published (a cache permission on the runner, since fixed).
|
||||||
|
|
||||||
## 0.1.0 (2026-09-22) - portal v0.3.36
|
## 0.1.0 (2026-09-22) - portal v0.3.36
|
||||||
|
|
||||||
- First release: the lint moved out of portal (`question_lint` becomes
|
- First release: the lint moved out of portal (`question_lint` becomes
|
||||||
|
|||||||
Generated
+687
-3
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "iris"
|
name = "iris"
|
||||||
version = "0.1.0"
|
version = "0.5.8"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "The iris over a portal site: nothing from a content repo comes through until it checks out"
|
description = "The iris over a portal site: nothing from a content repo comes through until it checks out"
|
||||||
license = "MIT"
|
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 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
|
# the way the running site does. Pinned to the portal release it
|
||||||
# matches; bumping this tag is what a new iris release is.
|
# matches; bumping this tag is what a new iris release is.
|
||||||
portal = { git = "https://project.uhhm.no/uhhm/portal.git", tag = "v0.3.36", features = ["ssr"] }
|
portal = { git = "https://project.uhhm.no/uhhm/portal.git", tag = "v0.5.8", features = ["ssr"] }
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|||||||
@@ -39,15 +39,19 @@ what sits around those.
|
|||||||
## Matching the site
|
## Matching the site
|
||||||
|
|
||||||
Every type iris reads is portal's own: portal is a library dependency
|
Every type iris reads is portal's own: portal is a library dependency
|
||||||
pinned to a release tag in `Cargo.toml`, and `iris --version` says
|
pinned to a release tag in `Cargo.toml`, and iris carries the same
|
||||||
which. A content repo should run the iris that matches the portal
|
version number as that tag, so `iris v0.3.39` is the lint for portal
|
||||||
release its site runs. Portal's own release tarball ships an `iris`
|
v0.3.39 and nothing else. A content repo pins `IRIS_RELEASE` to the
|
||||||
binary built the same way, so a site that pins nothing extra has one
|
`PORTAL_RELEASE` its site runs. Portal's own release tarball ships the
|
||||||
at `/srv/app/<instance>/current/iris`.
|
matching `iris` binary too, at `/srv/app/<instance>/current/iris`.
|
||||||
|
|
||||||
## Release
|
## Release
|
||||||
|
|
||||||
Move the `Unreleased` lines in `CHANGELOG.md` under a heading for the
|
Iris is released by portal's publish job, never by hand: when a
|
||||||
version, bump `Cargo.toml`, commit, tag `v<version>`, push. CI builds
|
portal tag is pushed, that job re-pins this crate to the tag, sets
|
||||||
the binary and attaches it to the Gitea release as `iris`, with that
|
the version to match, builds and tests it, adds the changelog line,
|
||||||
section as the notes.
|
and pushes the commit and the tag here. This repo's own CI then
|
||||||
|
attaches the binary to the Gitea release as `iris`. A portal change
|
||||||
|
to something the lint reads fails the portal release at that step.
|
||||||
|
Changes to iris itself land on `main` between releases and ship with
|
||||||
|
the next portal tag.
|
||||||
|
|||||||
+24
-3
@@ -23,6 +23,7 @@ pub async fn run(argv: Vec<String>) -> anyhow::Result<()> {
|
|||||||
let mut min_accuracy: f64 = 0.0;
|
let mut min_accuracy: f64 = 0.0;
|
||||||
let mut skim = false;
|
let mut skim = false;
|
||||||
let mut sim_out: Option<String> = None;
|
let mut sim_out: Option<String> = None;
|
||||||
|
let mut show_access = false;
|
||||||
|
|
||||||
while let Some(arg) = args.next() {
|
while let Some(arg) = args.next() {
|
||||||
match arg.as_str() {
|
match arg.as_str() {
|
||||||
@@ -33,6 +34,7 @@ pub async fn run(argv: Vec<String>) -> anyhow::Result<()> {
|
|||||||
"--needs-tasks" => tasks_out = args.next(),
|
"--needs-tasks" => tasks_out = args.next(),
|
||||||
"--skim" => skim = true,
|
"--skim" => skim = true,
|
||||||
"--needs-sim" => sim_out = args.next(),
|
"--needs-sim" => sim_out = args.next(),
|
||||||
|
"--access" => show_access = true,
|
||||||
"--needs-score" => answers_in = args.next(),
|
"--needs-score" => answers_in = args.next(),
|
||||||
"--min-accuracy" => {
|
"--min-accuracy" => {
|
||||||
min_accuracy = args.next().and_then(|v| v.parse().ok()).unwrap_or(min_accuracy)
|
min_accuracy = args.next().and_then(|v| v.parse().ok()).unwrap_or(min_accuracy)
|
||||||
@@ -57,11 +59,12 @@ pub async fn run(argv: Vec<String>) -> anyhow::Result<()> {
|
|||||||
// load_site_from_gitea already validates; a missing file is
|
// load_site_from_gitea already validates; a missing file is
|
||||||
// the default config, same as at portal boot.
|
// the default config, same as at portal boot.
|
||||||
let site = content::load_site_from_gitea(&repo_url, &branch).await?;
|
let site = content::load_site_from_gitea(&repo_url, &branch).await?;
|
||||||
let needs_raw = content::load_needs_from_gitea(&repo_url, &branch).await?;
|
// Optional: a fetch failure means the repo declares no needs.
|
||||||
|
let needs_raw = portal::http::get_text(&format!("{}/needs.yaml?ref={branch}", content::gitea_raw_base(&repo_url)?), portal::http::GITEA_CAP).await.ok();
|
||||||
(questions, aggregates_map, site, needs_raw)
|
(questions, aggregates_map, site, needs_raw)
|
||||||
} else {
|
} else {
|
||||||
eprintln!(
|
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);
|
std::process::exit(2);
|
||||||
};
|
};
|
||||||
@@ -90,6 +93,24 @@ pub async fn run(argv: Vec<String>) -> anyhow::Result<()> {
|
|||||||
questions.len(),
|
questions.len(),
|
||||||
aggregates_map.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(
|
check_needs(
|
||||||
needs_raw.as_deref(),
|
needs_raw.as_deref(),
|
||||||
&questions,
|
&questions,
|
||||||
@@ -108,7 +129,7 @@ pub async fn run(argv: Vec<String>) -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The business-needs pass (see `portal::needs`): structure always,
|
/// The business-needs pass (see `crate::needs`): structure always,
|
||||||
/// then the wording tasks written out and/or an engine's answers
|
/// then the wording tasks written out and/or an engine's answers
|
||||||
/// scored, when asked. A repo without `needs.yaml` skips all of it.
|
/// scored, when asked. A repo without `needs.yaml` skips all of it.
|
||||||
fn check_needs(
|
fn check_needs(
|
||||||
|
|||||||
+1
-1
@@ -82,7 +82,7 @@ fn collect_yaml(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The offline counterpart to `content::load_needs_from_gitea` -
|
/// The offline counterpart to `check`'s needs.yaml fetch -
|
||||||
/// `needs.yaml` sits at the repo root beside `aggregates.yaml`.
|
/// `needs.yaml` sits at the repo root beside `aggregates.yaml`.
|
||||||
pub fn load_needs_from_dir(dir: &str) -> anyhow::Result<Option<String>> {
|
pub fn load_needs_from_dir(dir: &str) -> anyhow::Result<Option<String>> {
|
||||||
let needs_path = std::path::Path::new(dir)
|
let needs_path = std::path::Path::new(dir)
|
||||||
|
|||||||
+3
-5
@@ -38,19 +38,17 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
replay::run(argv).await
|
replay::run(argv).await
|
||||||
}
|
}
|
||||||
Some("--version") | Some("-V") => {
|
Some("--version") | Some("-V") => {
|
||||||
println!("iris {} (portal {})", env!("CARGO_PKG_VERSION"), PORTAL_TAG);
|
println!("iris {v} (portal v{v})", v = env!("CARGO_PKG_VERSION"));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Some(flag) if flag.starts_with("--") => check::run(argv).await,
|
Some(flag) if flag.starts_with("--") => check::run(argv).await,
|
||||||
_ => {
|
_ => {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"iris {} - matches portal {PORTAL_TAG}\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)",
|
||||||
env!("CARGO_PKG_VERSION")
|
v = env!("CARGO_PKG_VERSION")
|
||||||
);
|
);
|
||||||
std::process::exit(2)
|
std::process::exit(2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The portal release this iris was built against (see Cargo.toml).
|
|
||||||
const PORTAL_TAG: &str = "v0.3.36";
|
|
||||||
|
|||||||
+67
-4
@@ -73,6 +73,11 @@ pub struct Need {
|
|||||||
/// as a warning, so the gap stays visible without failing CI.
|
/// as a warning, so the gap stays visible without failing CI.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub planned: bool,
|
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 -
|
/// The finished states that are a good outcome for the business -
|
||||||
/// a subset of `done_when`. "lost" is finished; "won" is success.
|
/// a subset of `done_when`. "lost" is finished; "won" is success.
|
||||||
/// What a simulation or a live bucket report counts toward.
|
/// What a simulation or a live bucket report counts toward.
|
||||||
@@ -152,11 +157,14 @@ fn visible(q: &Question, as_group: Option<&str>) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// What a person has to fill in: required fields, less the ones that
|
||||||
|
/// carry rather than ask - a `record` naming the case an answer is
|
||||||
|
/// about, a `hidden` preset - which cost nobody anything.
|
||||||
fn required_fields(alt: &Alternative) -> usize {
|
fn required_fields(alt: &Alternative) -> usize {
|
||||||
alt.features
|
alt.features
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|f| &f.requirements)
|
.flat_map(|f| &f.requirements)
|
||||||
.filter(|r| !r.optional)
|
.filter(|r| !r.optional && r.kind != "record" && r.kind != "hidden")
|
||||||
.count()
|
.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -571,7 +579,8 @@ pub fn sim_model(
|
|||||||
json!({
|
json!({
|
||||||
"page": step.page,
|
"page": step.page,
|
||||||
"question": page.name,
|
"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,
|
"name": a.name,
|
||||||
"description": plain(&a.description),
|
"description": plain(&a.description),
|
||||||
})).collect::<Vec<_>>(),
|
})).collect::<Vec<_>>(),
|
||||||
@@ -597,6 +606,7 @@ pub fn sim_model(
|
|||||||
json!({
|
json!({
|
||||||
"initial": schema.initial,
|
"initial": schema.initial,
|
||||||
"transitions": schema.transitions,
|
"transitions": schema.transitions,
|
||||||
|
"words": need.stage_words,
|
||||||
"desks": desks_over(questions, &need.lands_in).iter().map(|d| json!({
|
"desks": desks_over(questions, &need.lands_in).iter().map(|d| json!({
|
||||||
"page": d.page,
|
"page": d.page,
|
||||||
"group": d.group,
|
"group": d.group,
|
||||||
@@ -646,6 +656,41 @@ fn plain(text: &str) -> String {
|
|||||||
text.split_whitespace().collect::<Vec<_>>().join(" ")
|
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.
|
/// Every (persona, page-on-their-path) pair with a real choice on it.
|
||||||
/// `skim` shows the engine only each alternative's heading - what a
|
/// `skim` shows the engine only each alternative's heading - what a
|
||||||
/// visitor who never reads the description has to go on.
|
/// visitor who never reads the description has to go on.
|
||||||
@@ -660,10 +705,11 @@ pub fn tasks(file: &NeedsFile, questions: &Questions, skim: bool) -> Vec<Task> {
|
|||||||
};
|
};
|
||||||
for (i, step) in path.iter().enumerate() {
|
for (i, step) in path.iter().enumerate() {
|
||||||
let page = &questions[&step.page];
|
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 {
|
if open.len() < 2 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
let context = page_context(page);
|
||||||
let mut trail: Vec<String> = path[..=i].iter().map(|s| s.page.clone()).collect();
|
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)
|
let expect: Vec<String> = viable(questions, need, page, need.max_steps - i, i == 0, false, &mut trail)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -684,7 +730,11 @@ pub fn tasks(file: &NeedsFile, questions: &Questions, skim: bool) -> Vec<Task> {
|
|||||||
persona: persona.id.clone(),
|
persona: persona.id.clone(),
|
||||||
need: need.id.clone(),
|
need: need.id.clone(),
|
||||||
page: step.page.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([(
|
questions: BTreeMap::from([(
|
||||||
"pick",
|
"pick",
|
||||||
ChoiceQuestion {
|
ChoiceQuestion {
|
||||||
@@ -813,6 +863,19 @@ mod tests {
|
|||||||
let (questions, aggregates) = site(FULL_DESK);
|
let (questions, aggregates) = site(FULL_DESK);
|
||||||
let findings = check(&needs(" max_fields: 1\n"), &questions, &aggregates);
|
let findings = check(&needs(" max_fields: 1\n"), &questions, &aggregates);
|
||||||
assert_eq!(fails(&findings), vec!["the path asks for 2 required field(s), over the budget of 1"]);
|
assert_eq!(fails(&findings), vec!["the path asks for 2 required field(s), over the budget of 1"]);
|
||||||
|
// A field that carries rather than asks is not effort: the
|
||||||
|
// record an answer is about arrives on the link.
|
||||||
|
let (mut questions, aggregates) = site(FULL_DESK);
|
||||||
|
let front = questions.get_mut("/").unwrap();
|
||||||
|
let brief = &mut front.alternatives[0].features[0].requirements;
|
||||||
|
let mut carrier = brief[0].clone();
|
||||||
|
carrier.name = "case".into();
|
||||||
|
carrier.kind = "record".into();
|
||||||
|
carrier.of = Some("projects".into());
|
||||||
|
carrier.optional = false;
|
||||||
|
brief.push(carrier);
|
||||||
|
let findings = check(&needs(" max_fields: 2\n"), &questions, &aggregates);
|
||||||
|
assert!(fails(&findings).is_empty(), "{findings:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user