1 Commits
Author SHA1 Message Date
portal release edf401f773 Match portal v0.3.40
Publish release / publish (push) Successful in 23s
2026-09-23 11:51:22 +02:00
6 changed files with 12 additions and 103 deletions
+2 -26
View File
@@ -3,33 +3,9 @@
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.3.47 (2026-09-24) - portal v0.3.47 ## 0.3.40 (2026-09-23) - portal v0.3.40
- Matches portal v0.3.47 (released by portal's publish job). - Matches portal v0.3.40 (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 ## 0.3.39 (2026-09-23) - portal v0.3.39
Generated
+3 -3
View File
@@ -2196,7 +2196,7 @@ checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0"
[[package]] [[package]]
name = "iris" name = "iris"
version = "0.3.47" version = "0.3.40"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-nats", "async-nats",
@@ -3303,8 +3303,8 @@ dependencies = [
[[package]] [[package]]
name = "portal" name = "portal"
version = "0.3.47" version = "0.3.40"
source = "git+https://project.uhhm.no/uhhm/portal.git?tag=v0.3.47#9ed81944847c20cb6751e221e679449a8b5ebc47" source = "git+https://project.uhhm.no/uhhm/portal.git?tag=v0.3.40#44d5ef368371b7d0d004d9bb0c89a9db267fcfe6"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"arc-swap", "arc-swap",
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "iris" name = "iris"
version = "0.3.47" version = "0.3.40"
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.47", features = ["ssr"] } portal = { git = "https://project.uhhm.no/uhhm/portal.git", tag = "v0.3.40", features = ["ssr"] }
anyhow = "1" anyhow = "1"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
+1 -21
View File
@@ -23,7 +23,6 @@ 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() {
@@ -34,7 +33,6 @@ 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)
@@ -64,7 +62,7 @@ pub async fn run(argv: Vec<String>) -> anyhow::Result<()> {
(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>] [--access] [--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>] [--needs-score <answers.jsonl> [--min-accuracy 0.8]]"
); );
std::process::exit(2); std::process::exit(2);
}; };
@@ -93,24 +91,6 @@ 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,
+1 -1
View File
@@ -44,7 +44,7 @@ async fn main() -> anyhow::Result<()> {
Some(flag) if flag.starts_with("--") => check::run(argv).await, Some(flag) if flag.starts_with("--") => check::run(argv).await,
_ => { _ => {
eprintln!( 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>] [--access] [--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>] [--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") v = env!("CARGO_PKG_VERSION")
); );
std::process::exit(2) std::process::exit(2)
+3 -50
View File
@@ -73,11 +73,6 @@ 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.
@@ -576,8 +571,7 @@ pub fn sim_model(
json!({ json!({
"page": step.page, "page": step.page,
"question": page.name, "question": page.name,
"context": page_context(page), "options": page.alternatives.iter().filter(|a| !a.disabled).map(|a| json!({
"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<_>>(),
@@ -603,7 +597,6 @@ 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,
@@ -653,41 +646,6 @@ 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.
@@ -702,11 +660,10 @@ 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 && leads_somewhere(a)).collect(); let open: Vec<&Alternative> = page.alternatives.iter().filter(|a| !a.disabled).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()
@@ -727,11 +684,7 @@ 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: if context.is_empty() { state: BTreeMap::from([("body", plain(&persona.text))]),
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 {