diff --git a/crates/registry-relay/src/main.rs b/crates/registry-relay/src/main.rs index 679ae95ba..829150f59 100644 --- a/crates/registry-relay/src/main.rs +++ b/crates/registry-relay/src/main.rs @@ -4010,12 +4010,19 @@ consultation: .await .expect("test listener binds"); let addr = listener.local_addr().expect("listener has local addr"); - drop(listener); let url = format!("http://{addr}/healthz"); + let peer = tokio::spawn(async move { + let (connection, _) = listener.accept().await.expect("test peer accepts"); + drop(connection); + }); let err = run_healthcheck(&url, Duration::from_millis(200)) .await .expect_err("healthcheck fails"); + tokio::time::timeout(Duration::from_secs(1), peer) + .await + .expect("healthcheck contacts the test peer") + .expect("test peer joins"); assert!( err.to_string().contains("request failed"), "unexpected error: {err}" diff --git a/crates/registryctl/CHANGELOG.md b/crates/registryctl/CHANGELOG.md index 86d7c31b7..4c4e27d19 100644 --- a/crates/registryctl/CHANGELOG.md +++ b/crates/registryctl/CHANGELOG.md @@ -6,6 +6,23 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Changed + +- The maintained project-authoring fixtures now keep Relay source adaptation, + Notary evidence policy, and consumer decisions separate. The + DHIS2 Tracker starter retains its bounded health-evidence contract while + distinguishing positive, negative, unknown, no-match, ambiguity, and source + failure. Snapshot and custom-system examples now model reusable evidence or + explicitly source-owned decisions instead of Notary-owned eligibility. +- Smoke reports now carry the `registryctl.smoke.v1` schema version and are + validated against a committed JSON Schema. + +### Removed + +- Removed the hidden `registryctl init spreadsheet-api` compatibility alias. + Use `registryctl init relay` for the local spreadsheet tutorial or + `registryctl init --from` for Registry Stack project authoring. + ## [0.12.2] - 2026-07-20 - No user-visible Registryctl changes. This release fixes forward from the diff --git a/crates/registryctl/README.md b/crates/registryctl/README.md index ef4abaf3f..5532ad678 100644 --- a/crates/registryctl/README.md +++ b/crates/registryctl/README.md @@ -28,9 +28,10 @@ Interactive report commands print concise human-readable results. Add `--format another program needs a versioned report. Artifact and protocol commands, including authoring schemas, editor metadata, the language server, and logs, retain their native output formats. -The generated project contains a local Registry Relay configuration, sample -XLSX workbook, Compose file, project manifest, local demo credentials, and an -optional Bruno API collection. +The initialization report identifies the project root and supported generated +entry points. Automation must use the versioned JSON report rather than +hard-code the local tutorial's generated directory layout, which remains an +implementation detail. Run `registryctl doctor` before starting a generated stack or after editing config. It calls the product-owned validators and redacts local secret values. Add `--format json` when another program @@ -131,10 +132,10 @@ Registryctl never searches the current working directory for a lock, and rejects a missing, mismatched, oversized, symlinked, or structurally invalid file. -Existing projects do not need the lock for `start`, `stop`, `status`, or other -runtime commands. They keep using the immutable image references already stored -in `registryctl.yaml` and `compose.yaml`. A later `init` or `add` is a generation -operation and requires the lock for that registryctl version. +Existing local tutorial projects do not need the lock for `start`, `stop`, +`status`, or other runtime commands. Those commands use the immutable image +references already written into the project. A later `init` or `add` is a +generation operation and requires the lock for that registryctl version. ## Update checks diff --git a/crates/registryctl/schemas/registryctl.smoke.v1.schema.json b/crates/registryctl/schemas/registryctl.smoke.v1.schema.json new file mode 100644 index 000000000..6fc5f9f99 --- /dev/null +++ b/crates/registryctl/schemas/registryctl.smoke.v1.schema.json @@ -0,0 +1,71 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://id.registrystack.org/schemas/registryctl/registryctl.smoke.v1.schema.json", + "title": "registryctl smoke report v1", + "type": "object", + "required": ["schema_version", "base_url", "passed", "checks"], + "additionalProperties": false, + "properties": { + "schema_version": { + "const": "registryctl.smoke.v1" + }, + "base_url": { + "type": "string", + "minLength": 1 + }, + "passed": { + "type": "boolean" + }, + "checks": { + "type": "array", + "items": { + "$ref": "#/$defs/check" + } + } + }, + "$defs": { + "check": { + "type": "object", + "required": [ + "name", + "method", + "path", + "expected_status", + "actual_status", + "passed", + "error" + ], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "method": { + "type": "string", + "minLength": 1 + }, + "path": { + "type": "string", + "pattern": "^/" + }, + "expected_status": { + "type": "integer", + "minimum": 100, + "maximum": 599 + }, + "actual_status": { + "type": ["integer", "null"], + "minimum": 100, + "maximum": 599 + }, + "passed": { + "type": "boolean" + }, + "error": { + "type": ["string", "null"] + } + } + } + } +} diff --git a/crates/registryctl/src/lib.rs b/crates/registryctl/src/lib.rs index e580fb653..4b2c1b918 100644 --- a/crates/registryctl/src/lib.rs +++ b/crates/registryctl/src/lib.rs @@ -80,6 +80,8 @@ const CONFIG_BUNDLE_SIGNATURE_SCHEMA: &str = "registry.platform.config_bundle_si const CONFIG_TRUST_ANCHOR_SCHEMA: &str = "registry.platform.config_trust_anchor.v1"; const INIT_REPORT_SCHEMA_VERSION: &str = "registryctl.init.v1"; const ADD_NOTARY_REPORT_SCHEMA_VERSION: &str = "registryctl.add_notary.v1"; +pub const SMOKE_REPORT_SCHEMA_V1: &str = + include_str!("../schemas/registryctl.smoke.v1.schema.json"); #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] @@ -3667,13 +3669,22 @@ fn relay_config(credentials: &LocalCredentials) -> String { } #[derive(Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] struct SmokeReport { + schema_version: SmokeReportSchema, base_url: String, passed: bool, checks: Vec, } +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +enum SmokeReportSchema { + #[serde(rename = "registryctl.smoke.v1")] + V1, +} + #[derive(Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] struct SmokeCheck { name: String, method: String, @@ -3795,6 +3806,7 @@ fn run_smoke_checks(base_url: &str, secrets: &LocalEnv) -> SmokeReport { ); SmokeReport { + schema_version: SmokeReportSchema::V1, base_url: base_url.to_string(), passed: checks.iter().all(|check| check.passed), checks, @@ -5416,9 +5428,42 @@ mod tests { assert!(!json.contains("row-secret")); assert!(!json.contains("identity-secret")); assert!(!report.passed); + assert_eq!(parsed.schema_version, SmokeReportSchema::V1); assert_eq!(parsed.checks.len(), 11); } + #[test] + fn smoke_report_rejects_another_schema_version() { + let secrets = LocalEnv { + values: BTreeMap::new(), + }; + let report = run_smoke_checks("http://127.0.0.1:1", &secrets); + let mut document = serde_json::to_value(report).unwrap(); + document["schema_version"] = serde_json::json!("registryctl.smoke.v2"); + + assert!(parse_smoke_report(&document.to_string()).is_err()); + } + + #[test] + fn smoke_report_json_matches_committed_schema() { + let secrets = LocalEnv { + values: BTreeMap::new(), + }; + let report = run_smoke_checks("http://127.0.0.1:1", &secrets); + let document = serde_json::to_value(report).unwrap(); + let schema: JsonValue = serde_json::from_str(SMOKE_REPORT_SCHEMA_V1).unwrap(); + let compiled = jsonschema::JSONSchema::compile(&schema).expect("schema compiles"); + let validation_errors = match compiled.validate(&document) { + Ok(()) => Vec::new(), + Err(errors) => errors.map(|error| error.to_string()).collect::>(), + }; + + assert!( + validation_errors.is_empty(), + "registryctl smoke report must satisfy its schema: {validation_errors:?}" + ); + } + #[test] fn smoke_project_writes_redacted_failure_report() { let temp = TempDir::new().unwrap(); @@ -5435,6 +5480,7 @@ mod tests { for (_, secret) in env.lines().filter_map(|line| line.split_once('=')) { assert!(!report.contains(secret)); } + assert!(report.contains("\"schema_version\": \"registryctl.smoke.v1\"")); assert!(report.contains("\"passed\": false")); } diff --git a/crates/registryctl/src/main.rs b/crates/registryctl/src/main.rs index 495930fe2..687efcfa7 100644 --- a/crates/registryctl/src/main.rs +++ b/crates/registryctl/src/main.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use anyhow::{Context, Result}; -use clap::{Parser, Subcommand, ValueEnum}; +use clap::{error::ErrorKind, CommandFactory, Parser, Subcommand, ValueEnum}; use registryctl::{ AddNotaryReport, AnchorReport, BundleInspectReport, BundleSignOptions, BundleSignReport, @@ -39,8 +39,7 @@ fn main() -> Result<()> { } => { let destination = match (&from, command.as_deref()) { (Some(_), None) => Some(project_dir.as_path()), - (None, Some(InitCommand::Relay { dir, .. })) - | (None, Some(InitCommand::SpreadsheetApi { dir, .. })) => Some(dir.as_path()), + (None, Some(InitCommand::Relay { dir, .. })) => Some(dir.as_path()), _ => None, }; if format == OutputFormat::Json @@ -59,14 +58,20 @@ fn main() -> Result<()> { InitCommand::Relay { dir, sample } => { registryctl::init_spreadsheet_api(&dir, sample, &image_lock)? } - InitCommand::SpreadsheetApi { dir, sample } => { - registryctl::init_spreadsheet_api(&dir, sample, &image_lock)? - } } } - _ => anyhow::bail!( - "init requires exactly one of --from or a legacy product subcommand" - ), + (None, None) => Cli::command() + .error( + ErrorKind::MissingRequiredArgument, + "init requires exactly one of --from or the relay subcommand", + ) + .exit(), + (Some(_), Some(_)) => Cli::command() + .error( + ErrorKind::ArgumentConflict, + "init accepts only one of --from or the relay subcommand", + ) + .exit(), }; match format { OutputFormat::Human => println!("{}", render_init_report(&report)?), @@ -2168,15 +2173,6 @@ enum InitCommand { #[arg(long, value_enum, default_value_t = Sample::Benefits)] sample: Sample, }, - /// Create a local Relay-backed spreadsheet API project. - #[command(name = "spreadsheet-api", hide = true)] - SpreadsheetApi { - /// Directory to create. - dir: PathBuf, - /// Sample project to generate. - #[arg(long, value_enum, default_value_t = Sample::Benefits)] - sample: Sample, - }, } #[derive(Debug, Subcommand)] diff --git a/crates/registryctl/src/project_authoring/fixtures.rs b/crates/registryctl/src/project_authoring/fixtures.rs index 9bcfa67b3..d1d3db28f 100644 --- a/crates/registryctl/src/project_authoring/fixtures.rs +++ b/crates/registryctl/src/project_authoring/fixtures.rs @@ -1737,7 +1737,7 @@ mod fixture_interface_tests { let (_, fixture) = loaded.integrations["health-record"] .fixtures .iter() - .find(|(_, fixture)| fixture.name == "complete-health-match") + .find(|(_, fixture)| fixture.name == "complete-child-health-evidence") .expect("match fixture exists"); let input = offline_fixture_input(fixture).expect("fixture input is valid"); let interactions = diff --git a/crates/registryctl/src/project_authoring/tests.rs b/crates/registryctl/src/project_authoring/tests.rs index 750ec2394..d733cc962 100644 --- a/crates/registryctl/src/project_authoring/tests.rs +++ b/crates/registryctl/src/project_authoring/tests.rs @@ -782,11 +782,14 @@ outputs: &loaded, &json!({ "purpose": "household-support-screening", - "claims": ["household-category", "household-eligible"], + "claims": ["household-category", "source-household-approval-decision"], }), ) .expect("claims from both same-purpose services are valid"); - assert_eq!(claims, ["household-category", "household-eligible"]); + assert_eq!( + claims, + ["household-category", "source-household-approval-decision"] + ); } #[test] diff --git a/crates/registryctl/tests/exit_status.rs b/crates/registryctl/tests/exit_status.rs new file mode 100644 index 000000000..aef3e9084 --- /dev/null +++ b/crates/registryctl/tests/exit_status.rs @@ -0,0 +1,57 @@ +use std::process::Command; + +fn run_registryctl(args: &[&str]) -> std::process::Output { + Command::new(env!("CARGO_BIN_EXE_registryctl")) + .args(args) + .env("REGISTRYCTL_NO_UPDATE_CHECK", "1") + .output() + .expect("registryctl runs") +} + +#[test] +fn success_uses_exit_status_zero() { + let output = run_registryctl(&["--version"]); + + assert_eq!(output.status.code(), Some(0)); +} + +#[test] +fn command_failure_uses_exit_status_one() { + let temporary = tempfile::tempdir().expect("temporary directory"); + let output = Command::new(env!("CARGO_BIN_EXE_registryctl")) + .arg("restart") + .current_dir(temporary.path()) + .env("REGISTRYCTL_NO_UPDATE_CHECK", "1") + .output() + .expect("registryctl runs"); + + assert_eq!(output.status.code(), Some(1)); +} + +#[test] +fn usage_failure_uses_exit_status_two() { + let output = run_registryctl(&["not-a-command"]); + + assert_eq!(output.status.code(), Some(2)); +} + +#[test] +fn removed_spreadsheet_alias_is_a_usage_failure() { + let output = run_registryctl(&["init", "spreadsheet-api", "project"]); + + assert_eq!(output.status.code(), Some(2)); +} + +#[test] +fn missing_init_mode_is_a_usage_failure() { + let output = run_registryctl(&["init"]); + + assert_eq!(output.status.code(), Some(2)); +} + +#[test] +fn conflicting_init_modes_are_a_usage_failure() { + let output = run_registryctl(&["init", "--from", "http", "relay", "project"]); + + assert_eq!(output.status.code(), Some(2)); +} diff --git a/crates/registryctl/tests/fixtures/project-authoring-journeys.yaml b/crates/registryctl/tests/fixtures/project-authoring-journeys.yaml index 1ed14ab04..ace5ca33a 100644 --- a/crates/registryctl/tests/fixtures/project-authoring-journeys.yaml +++ b/crates/registryctl/tests/fixtures/project-authoring-journeys.yaml @@ -20,7 +20,7 @@ workspaces: classification: maintained topology: combined project_dir: crates/registryctl/tests/fixtures/project-authoring/custom-system - focused_fixture_file: eligible.yaml + focused_fixture_file: source-approved.yaml steps: [trace, watch, test, check, build] environment: local check_explain: true @@ -38,7 +38,7 @@ workspaces: - id: dhis2-tracker label: DHIS2 Tracker - summary: A product-neutral script adapter applied to a bounded DHIS2 Tracker read journey. + summary: Bounded DHIS2 Tracker acquisition normalized into reusable health evidence for consumer-owned decisions. source: crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker classification: maintained topology: combined diff --git a/crates/registryctl/tests/fixtures/project-authoring/custom-system/integrations/eligibility/fixtures/no-match.yaml b/crates/registryctl/tests/fixtures/project-authoring/custom-system/integrations/eligibility/fixtures/no-match.yaml index d6cb7475f..bb62583e9 100644 --- a/crates/registryctl/tests/fixtures/project-authoring/custom-system/integrations/eligibility/fixtures/no-match.yaml +++ b/crates/registryctl/tests/fixtures/project-authoring/custom-system/integrations/eligibility/fixtures/no-match.yaml @@ -10,4 +10,7 @@ interactions: expect: outcome: no_match outputs: {} - claims: { household-record-exists: false, household-category: null, household-eligible: false } + claims: + household-record-exists: false + household-category: null + source-household-approval-decision: null diff --git a/crates/registryctl/tests/fixtures/project-authoring/custom-system/integrations/eligibility/fixtures/eligible.yaml b/crates/registryctl/tests/fixtures/project-authoring/custom-system/integrations/eligibility/fixtures/source-approved.yaml similarity index 69% rename from crates/registryctl/tests/fixtures/project-authoring/custom-system/integrations/eligibility/fixtures/eligible.yaml rename to crates/registryctl/tests/fixtures/project-authoring/custom-system/integrations/eligibility/fixtures/source-approved.yaml index 765baa92a..64343f01d 100644 --- a/crates/registryctl/tests/fixtures/project-authoring/custom-system/integrations/eligibility/fixtures/eligible.yaml +++ b/crates/registryctl/tests/fixtures/project-authoring/custom-system/integrations/eligibility/fixtures/source-approved.yaml @@ -1,4 +1,4 @@ -name: eligible-household +name: source-approved-household classification: synthetic input: { household_reference: HH-AB12CD34 } interactions: @@ -12,4 +12,7 @@ interactions: expect: outcome: match outputs: { approved: true, category: PRIORITY } - claims: { household-record-exists: true, household-category: PRIORITY, household-eligible: true } + claims: + household-record-exists: true + household-category: PRIORITY + source-household-approval-decision: true diff --git a/crates/registryctl/tests/fixtures/project-authoring/custom-system/integrations/eligibility/integration.yaml b/crates/registryctl/tests/fixtures/project-authoring/custom-system/integrations/eligibility/integration.yaml index 731c2b8bb..509061824 100644 --- a/crates/registryctl/tests/fixtures/project-authoring/custom-system/integrations/eligibility/integration.yaml +++ b/crates/registryctl/tests/fixtures/project-authoring/custom-system/integrations/eligibility/integration.yaml @@ -38,4 +38,4 @@ outputs: not_applicable: subject_mismatch: rationale: The selected response projection contains no identifier that can be compared with the requested household reference. - request_fixture: eligible-household + request_fixture: source-approved-household diff --git a/crates/registryctl/tests/fixtures/project-authoring/custom-system/registry-stack.yaml b/crates/registryctl/tests/fixtures/project-authoring/custom-system/registry-stack.yaml index a27bad844..21f03bd9b 100644 --- a/crates/registryctl/tests/fixtures/project-authoring/custom-system/registry-stack.yaml +++ b/crates/registryctl/tests/fixtures/project-authoring/custom-system/registry-stack.yaml @@ -28,13 +28,13 @@ services: household-category: output: household.category disclosure: value - household-eligible: + source-household-approval-decision: cel: >- - household.approved != null ? household.matched && household.approved : false + household.matched && household.approved != null ? household.approved : null disclosure: predicate credential_profiles: household-eligibility: format: dc+sd-jwt type: https://credentials.invalid/household-eligibility/v1 validity: 5m - claims: [household-record-exists, household-category, household-eligible] + claims: [household-record-exists, household-category, source-household-approval-decision] diff --git a/crates/registryctl/tests/fixtures/project-authoring/dhis2-script/integrations/health-record/adapter.rhai b/crates/registryctl/tests/fixtures/project-authoring/dhis2-script/integrations/health-record/adapter.rhai index 2c8ae0310..e7db83777 100644 --- a/crates/registryctl/tests/fixtures/project-authoring/dhis2-script/integrations/health-record/adapter.rhai +++ b/crates/registryctl/tests/fixtures/project-authoring/dhis2-script/integrations/health-record/adapter.rhai @@ -17,16 +17,27 @@ fn enrollment_for(enrollments, programme_uid) { () } -fn completed_stage(enrollment, stage_uid) { +fn enrollment_active(enrollment) { if enrollment == () { - return false; + return (); } + enrollment.status == "ACTIVE" +} + +fn completed_stage_evidence(enrollment, stage_uid) { + if enrollment == () { + return (); + } + let stage_exists = false; for event in enrollment.events { - if event.programStage == stage_uid && event.status == "COMPLETED" { - return true; + if event.programStage == stage_uid { + if event.status == "COMPLETED" { + return true; + } + stage_exists = true; } } - false + if stage_exists { false } else { () } } fn consult(ctx) { @@ -34,6 +45,9 @@ fn consult(ctx) { let maternal_program_uid = "DEMO_MATERNAL_PROGRAM_UID"; let tb_program_uid = "DEMO_TB_PROGRAM_UID"; let child_visit_stage_uid = "DEMO_CHILD_VISIT_STAGE_UID"; + let bcg_birth_stage_uid = "DEMO_BCG_BIRTH_STAGE_UID"; + let opv_birth_stage_uid = "DEMO_OPV_BIRTH_STAGE_UID"; + let measles_stage_uid = "DEMO_MEASLES_STAGE_UID"; let first_name_uid = "DEMO_FIRST_NAME_ATTRIBUTE_UID"; let last_name_uid = "DEMO_LAST_NAME_ATTRIBUTE_UID"; let birth_date_uid = "DEMO_BIRTH_DATE_ATTRIBUTE_UID"; @@ -68,11 +82,14 @@ fn consult(ctx) { first_name: attribute_value(entity.attributes, first_name_uid), last_name: attribute_value(entity.attributes, last_name_uid), date_of_birth: attribute_value(entity.attributes, birth_date_uid), - child_program_active: child != () && child.status == "ACTIVE", + child_program_active: enrollment_active(child), programme_code: if child == () { () } else { "DEMO_CHILD_PROGRAM" }, reconciliation_reference: attribute_value(entity.attributes, reconciliation_uid), - maternal_postnatal_active: maternal != () && maternal.status == "ACTIVE", - child_health_visit_recorded: completed_stage(child, child_visit_stage_uid), - tb_program_active: tb != () && tb.status == "ACTIVE" + maternal_postnatal_active: enrollment_active(maternal), + child_health_visit_recorded: completed_stage_evidence(child, child_visit_stage_uid), + tb_program_active: enrollment_active(tb), + bcg_birth_dose_recorded: completed_stage_evidence(child, bcg_birth_stage_uid), + opv_birth_dose_recorded: completed_stage_evidence(child, opv_birth_stage_uid), + measles_dose_recorded: completed_stage_evidence(child, measles_stage_uid) }) } diff --git a/crates/registryctl/tests/fixtures/project-authoring/dhis2-script/integrations/health-record/fixtures/match.yaml b/crates/registryctl/tests/fixtures/project-authoring/dhis2-script/integrations/health-record/fixtures/match.yaml index 1f3cf20ea..fe6107235 100644 --- a/crates/registryctl/tests/fixtures/project-authoring/dhis2-script/integrations/health-record/fixtures/match.yaml +++ b/crates/registryctl/tests/fixtures/project-authoring/dhis2-script/integrations/health-record/fixtures/match.yaml @@ -1,4 +1,4 @@ -name: complete-health-match +name: complete-child-health-evidence classification: synthetic input: { tracked_entity: A0000000001, include_inactive: true } variables: { as_of_date: 2026-01-01 } @@ -21,7 +21,11 @@ interactions: enrollments: - program: DEMO_CHILD_PROGRAM_UID status: ACTIVE - events: [{ programStage: DEMO_CHILD_VISIT_STAGE_UID, status: COMPLETED }] + events: + - { programStage: DEMO_CHILD_VISIT_STAGE_UID, status: COMPLETED } + - { programStage: DEMO_BCG_BIRTH_STAGE_UID, status: COMPLETED } + - { programStage: DEMO_OPV_BIRTH_STAGE_UID, status: COMPLETED } + - { programStage: DEMO_MEASLES_STAGE_UID, status: COMPLETED } - { program: DEMO_MATERNAL_PROGRAM_UID, status: ACTIVE, events: [] } - { program: DEMO_TB_PROGRAM_UID, status: CANCELLED, events: [] } ignored_upstream_field: safe-extra-field @@ -37,6 +41,9 @@ expect: maternal_postnatal_active: true child_health_visit_recorded: true tb_program_active: false + bcg_birth_dose_recorded: true + opv_birth_dose_recorded: true + measles_dose_recorded: true claims: tracked-entity-first-name: Nia tracked-entity-last-name: Example @@ -47,3 +54,6 @@ expect: maternal-postnatal-care-active: true child-health-visit-recorded: true tb-program-active: false + bcg-birth-dose-recorded: true + opv-birth-dose-recorded: true + measles-dose-recorded: true diff --git a/crates/registryctl/tests/fixtures/project-authoring/dhis2-script/integrations/health-record/fixtures/no-enrollment.yaml b/crates/registryctl/tests/fixtures/project-authoring/dhis2-script/integrations/health-record/fixtures/no-enrollment.yaml new file mode 100644 index 000000000..62f8bf449 --- /dev/null +++ b/crates/registryctl/tests/fixtures/project-authoring/dhis2-script/integrations/health-record/fixtures/no-enrollment.yaml @@ -0,0 +1,46 @@ +name: no-child-program-enrollment +classification: synthetic +input: { tracked_entity: A0000000001, include_inactive: true } +variables: { as_of_date: 2026-01-01 } +interactions: + - expect: + method: GET + path: /api/tracker/trackedEntities/A0000000001 + query: + fields: trackedEntity,attributes[attribute,value],enrollments[program,status,events[programStage,status]] + includeDeleted: true + respond: + status: 200 + body: + trackedEntity: A0000000001 + attributes: [] + enrollments: + - { program: DEMO_OTHER_PROGRAM_UID, status: ACTIVE, events: [] } +expect: + outcome: match + outputs: + first_name: null + last_name: null + date_of_birth: null + child_program_active: null + programme_code: null + reconciliation_reference: null + maternal_postnatal_active: null + child_health_visit_recorded: null + tb_program_active: null + bcg_birth_dose_recorded: null + opv_birth_dose_recorded: null + measles_dose_recorded: null + claims: + tracked-entity-first-name: null + tracked-entity-last-name: null + child-program-active: null + child-age-band: null + programme-code: null + reconciliation-reference: redacted + maternal-postnatal-care-active: null + child-health-visit-recorded: null + tb-program-active: null + bcg-birth-dose-recorded: null + opv-birth-dose-recorded: null + measles-dose-recorded: null diff --git a/crates/registryctl/tests/fixtures/project-authoring/dhis2-script/integrations/health-record/fixtures/no-match.yaml b/crates/registryctl/tests/fixtures/project-authoring/dhis2-script/integrations/health-record/fixtures/no-match.yaml index 1fecb6434..ded4cda16 100644 --- a/crates/registryctl/tests/fixtures/project-authoring/dhis2-script/integrations/health-record/fixtures/no-match.yaml +++ b/crates/registryctl/tests/fixtures/project-authoring/dhis2-script/integrations/health-record/fixtures/no-match.yaml @@ -16,10 +16,13 @@ expect: claims: tracked-entity-first-name: null tracked-entity-last-name: null - child-program-active: false + child-program-active: null child-age-band: null programme-code: null reconciliation-reference: redacted - maternal-postnatal-care-active: false - child-health-visit-recorded: false - tb-program-active: false + maternal-postnatal-care-active: null + child-health-visit-recorded: null + tb-program-active: null + bcg-birth-dose-recorded: null + opv-birth-dose-recorded: null + measles-dose-recorded: null diff --git a/crates/registryctl/tests/fixtures/project-authoring/dhis2-script/integrations/health-record/fixtures/partial.yaml b/crates/registryctl/tests/fixtures/project-authoring/dhis2-script/integrations/health-record/fixtures/partial.yaml new file mode 100644 index 000000000..006970494 --- /dev/null +++ b/crates/registryctl/tests/fixtures/project-authoring/dhis2-script/integrations/health-record/fixtures/partial.yaml @@ -0,0 +1,50 @@ +name: partial-child-health-evidence +classification: synthetic +input: { tracked_entity: A0000000001, include_inactive: true } +variables: { as_of_date: 2026-01-01 } +interactions: + - expect: + method: GET + path: /api/tracker/trackedEntities/A0000000001 + query: + fields: trackedEntity,attributes[attribute,value],enrollments[program,status,events[programStage,status]] + includeDeleted: true + respond: + status: 200 + body: + trackedEntity: A0000000001 + attributes: [] + enrollments: + - program: DEMO_CHILD_PROGRAM_UID + status: CANCELLED + events: + - { programStage: DEMO_BCG_BIRTH_STAGE_UID, status: SKIPPED } + - { programStage: DEMO_MEASLES_STAGE_UID, status: COMPLETED } +expect: + outcome: match + outputs: + first_name: null + last_name: null + date_of_birth: null + child_program_active: false + programme_code: DEMO_CHILD_PROGRAM + reconciliation_reference: null + maternal_postnatal_active: null + child_health_visit_recorded: null + tb_program_active: null + bcg_birth_dose_recorded: false + opv_birth_dose_recorded: null + measles_dose_recorded: true + claims: + tracked-entity-first-name: null + tracked-entity-last-name: null + child-program-active: false + child-age-band: null + programme-code: DEMO_CHILD_PROGRAM + reconciliation-reference: redacted + maternal-postnatal-care-active: null + child-health-visit-recorded: null + tb-program-active: null + bcg-birth-dose-recorded: false + opv-birth-dose-recorded: null + measles-dose-recorded: true diff --git a/crates/registryctl/tests/fixtures/project-authoring/dhis2-script/integrations/health-record/integration.yaml b/crates/registryctl/tests/fixtures/project-authoring/dhis2-script/integrations/health-record/integration.yaml index afbe4a642..375eed5bb 100644 --- a/crates/registryctl/tests/fixtures/project-authoring/dhis2-script/integrations/health-record/integration.yaml +++ b/crates/registryctl/tests/fixtures/project-authoring/dhis2-script/integrations/health-record/integration.yaml @@ -29,16 +29,19 @@ outputs: first_name: { type: [string, "null"], maxLength: 80 } last_name: { type: [string, "null"], maxLength: 80 } date_of_birth: { type: [string, "null"], format: date, maxLength: 10 } - child_program_active: { type: boolean } + child_program_active: { type: [boolean, "null"] } programme_code: { type: [string, "null"], maxLength: 32 } reconciliation_reference: { type: [string, "null"], maxLength: 64 } - maternal_postnatal_active: { type: boolean } - child_health_visit_recorded: { type: boolean } - tb_program_active: { type: boolean } + maternal_postnatal_active: { type: [boolean, "null"] } + child_health_visit_recorded: { type: [boolean, "null"] } + tb_program_active: { type: [boolean, "null"] } + bcg_birth_dose_recorded: { type: [boolean, "null"] } + opv_birth_dose_recorded: { type: [boolean, "null"] } + measles_dose_recorded: { type: [boolean, "null"] } not_applicable: ambiguity: rationale: The tracked-entity identifier is resolved by the exact singleton trackedEntities resource endpoint, so a collection cannot be returned. - request_fixture: complete-health-match + request_fixture: complete-child-health-evidence limits: { calls: 1, source_bytes: 64KiB, request_bytes: 4KiB, deadline: 8s } diff --git a/crates/registryctl/tests/fixtures/project-authoring/dhis2-script/registry-stack.yaml b/crates/registryctl/tests/fixtures/project-authoring/dhis2-script/registry-stack.yaml index 22a37bad4..7496cebe9 100644 --- a/crates/registryctl/tests/fixtures/project-authoring/dhis2-script/registry-stack.yaml +++ b/crates/registryctl/tests/fixtures/project-authoring/dhis2-script/registry-stack.yaml @@ -21,12 +21,7 @@ services: claims: tracked-entity-first-name: { output: health.first_name, disclosure: value } tracked-entity-last-name: { output: health.last_name, disclosure: value } - child-program-active: - cel: >- - health.child_program_active != null - ? health.matched && health.child_program_active - : false - disclosure: predicate + child-program-active: { output: health.child_program_active, disclosure: predicate } child-age-band: cel: >- health.matched && health.date_of_birth != null @@ -37,24 +32,12 @@ services: disclosure: value programme-code: { output: health.programme_code, disclosure: value } reconciliation-reference: { output: health.reconciliation_reference, disclosure: redacted } - maternal-postnatal-care-active: - cel: >- - health.maternal_postnatal_active != null - ? health.matched && health.maternal_postnatal_active - : false - disclosure: predicate - child-health-visit-recorded: - cel: >- - health.child_health_visit_recorded != null - ? health.matched && health.child_health_visit_recorded - : false - disclosure: predicate - tb-program-active: - cel: >- - health.tb_program_active != null - ? health.matched && health.tb_program_active - : false - disclosure: predicate + maternal-postnatal-care-active: { output: health.maternal_postnatal_active, disclosure: predicate } + child-health-visit-recorded: { output: health.child_health_visit_recorded, disclosure: predicate } + tb-program-active: { output: health.tb_program_active, disclosure: predicate } + bcg-birth-dose-recorded: { output: health.bcg_birth_dose_recorded, disclosure: predicate } + opv-birth-dose-recorded: { output: health.opv_birth_dose_recorded, disclosure: predicate } + measles-dose-recorded: { output: health.measles_dose_recorded, disclosure: predicate } credential_profiles: health-status: format: dc+sd-jwt @@ -71,3 +54,8 @@ services: type: https://credentials.invalid/programme-participation/v1 validity: 10m claims: [programme-code, reconciliation-reference, child-program-active] + child-health-evidence: + format: dc+sd-jwt + type: https://credentials.invalid/child-health-evidence/v1 + validity: 10m + claims: [child-program-active, bcg-birth-dose-recorded, opv-birth-dose-recorded, measles-dose-recorded] diff --git a/crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker/README.md b/crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker/README.md index f4b8d785e..8f880d548 100644 --- a/crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker/README.md +++ b/crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker/README.md @@ -1,13 +1,14 @@ -# Script source-adapter Registry Stack project +# DHIS2 child-health evidence Registry Stack project This starter demonstrates the product-neutral `script` capability with a synthetic DHIS2 Tracker wire shape. Product and version metadata do not select -the Rhai runtime. +the Rhai runtime. The offline fixtures are the deterministic acceptance path; +a reachable live DHIS2 instance is optional compatibility evidence. ```bash registryctl authoring editor --project-dir . -registryctl test --project-dir . --integration health-record --fixture complete-health-match --trace -registryctl test --project-dir . --integration health-record --fixture complete-health-match --watch +registryctl test --project-dir . --integration health-record --fixture complete-child-health-evidence --trace +registryctl test --project-dir . --integration health-record --fixture complete-child-health-evidence --watch registryctl test --project-dir . registryctl check --project-dir . --environment local --explain registryctl build --project-dir . --environment local @@ -21,6 +22,35 @@ from this `registryctl` build for VS Code and Zed. Edit the reviewed `adapter.rhai`, integration contract, and synthetic fixtures together. Keep source credentials in the environment binding. +The authored layers remain separate: + +1. Relay performs the bounded DHIS2 request and preserves the starter's + declared identity, date, programme, reconciliation, and health-status + outputs. Nullable programme and stage booleans keep `true`, `false`, and + `null` distinct, including BCG, OPV, and measles dose evidence. +2. Notary discloses those outputs as atomic evidence claims. It does not decide + outreach, follow-up priority, eligibility, entitlement, or case action. +3. In this example, a public-health programme is both the evidence consumer and + decision owner. It might first route any `null` evidence to resolution, then + derive `outreach_required` only from known enrollment and dose evidence. + That downstream rule is illustrative and is not part of this Registry Stack + project. + +For a matched tracked entity, a completed DHIS2 programme-stage event maps to +`true`, an existing non-completed stage event maps to `false`, and an absent +enrollment or stage maps to `null`. A 404 is a no-match, not negative health +evidence. An upstream rejection and an echoed-subject mismatch are failures +and produce no claims. Ambiguity is explicitly not applicable because the +adapter uses DHIS2's singleton tracked-entity resource. + +The demo programme and stage UIDs in `adapter.rhai` are project-owned mappings. +Replace and review them against the deployed DHIS2 metadata without changing +the product-neutral Script runtime or broadening source access. + +Record any live compatibility result through the repository root's +`release/conformance/integrations/` evidence flow. Never rewrite the +deterministic offline fixtures to reflect a transient live server result. + The `include_inactive` boolean is a bounded, typed target attribute supplied by the evaluation caller and forwarded through Notary and Relay. It is request context only. It is not an authenticated identity or a substitute for the diff --git a/crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker/integrations/health-record/adapter.rhai b/crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker/integrations/health-record/adapter.rhai index 2c8ae0310..e7db83777 100644 --- a/crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker/integrations/health-record/adapter.rhai +++ b/crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker/integrations/health-record/adapter.rhai @@ -17,16 +17,27 @@ fn enrollment_for(enrollments, programme_uid) { () } -fn completed_stage(enrollment, stage_uid) { +fn enrollment_active(enrollment) { if enrollment == () { - return false; + return (); } + enrollment.status == "ACTIVE" +} + +fn completed_stage_evidence(enrollment, stage_uid) { + if enrollment == () { + return (); + } + let stage_exists = false; for event in enrollment.events { - if event.programStage == stage_uid && event.status == "COMPLETED" { - return true; + if event.programStage == stage_uid { + if event.status == "COMPLETED" { + return true; + } + stage_exists = true; } } - false + if stage_exists { false } else { () } } fn consult(ctx) { @@ -34,6 +45,9 @@ fn consult(ctx) { let maternal_program_uid = "DEMO_MATERNAL_PROGRAM_UID"; let tb_program_uid = "DEMO_TB_PROGRAM_UID"; let child_visit_stage_uid = "DEMO_CHILD_VISIT_STAGE_UID"; + let bcg_birth_stage_uid = "DEMO_BCG_BIRTH_STAGE_UID"; + let opv_birth_stage_uid = "DEMO_OPV_BIRTH_STAGE_UID"; + let measles_stage_uid = "DEMO_MEASLES_STAGE_UID"; let first_name_uid = "DEMO_FIRST_NAME_ATTRIBUTE_UID"; let last_name_uid = "DEMO_LAST_NAME_ATTRIBUTE_UID"; let birth_date_uid = "DEMO_BIRTH_DATE_ATTRIBUTE_UID"; @@ -68,11 +82,14 @@ fn consult(ctx) { first_name: attribute_value(entity.attributes, first_name_uid), last_name: attribute_value(entity.attributes, last_name_uid), date_of_birth: attribute_value(entity.attributes, birth_date_uid), - child_program_active: child != () && child.status == "ACTIVE", + child_program_active: enrollment_active(child), programme_code: if child == () { () } else { "DEMO_CHILD_PROGRAM" }, reconciliation_reference: attribute_value(entity.attributes, reconciliation_uid), - maternal_postnatal_active: maternal != () && maternal.status == "ACTIVE", - child_health_visit_recorded: completed_stage(child, child_visit_stage_uid), - tb_program_active: tb != () && tb.status == "ACTIVE" + maternal_postnatal_active: enrollment_active(maternal), + child_health_visit_recorded: completed_stage_evidence(child, child_visit_stage_uid), + tb_program_active: enrollment_active(tb), + bcg_birth_dose_recorded: completed_stage_evidence(child, bcg_birth_stage_uid), + opv_birth_dose_recorded: completed_stage_evidence(child, opv_birth_stage_uid), + measles_dose_recorded: completed_stage_evidence(child, measles_stage_uid) }) } diff --git a/crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker/integrations/health-record/fixtures/match.yaml b/crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker/integrations/health-record/fixtures/match.yaml index 1f3cf20ea..fe6107235 100644 --- a/crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker/integrations/health-record/fixtures/match.yaml +++ b/crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker/integrations/health-record/fixtures/match.yaml @@ -1,4 +1,4 @@ -name: complete-health-match +name: complete-child-health-evidence classification: synthetic input: { tracked_entity: A0000000001, include_inactive: true } variables: { as_of_date: 2026-01-01 } @@ -21,7 +21,11 @@ interactions: enrollments: - program: DEMO_CHILD_PROGRAM_UID status: ACTIVE - events: [{ programStage: DEMO_CHILD_VISIT_STAGE_UID, status: COMPLETED }] + events: + - { programStage: DEMO_CHILD_VISIT_STAGE_UID, status: COMPLETED } + - { programStage: DEMO_BCG_BIRTH_STAGE_UID, status: COMPLETED } + - { programStage: DEMO_OPV_BIRTH_STAGE_UID, status: COMPLETED } + - { programStage: DEMO_MEASLES_STAGE_UID, status: COMPLETED } - { program: DEMO_MATERNAL_PROGRAM_UID, status: ACTIVE, events: [] } - { program: DEMO_TB_PROGRAM_UID, status: CANCELLED, events: [] } ignored_upstream_field: safe-extra-field @@ -37,6 +41,9 @@ expect: maternal_postnatal_active: true child_health_visit_recorded: true tb_program_active: false + bcg_birth_dose_recorded: true + opv_birth_dose_recorded: true + measles_dose_recorded: true claims: tracked-entity-first-name: Nia tracked-entity-last-name: Example @@ -47,3 +54,6 @@ expect: maternal-postnatal-care-active: true child-health-visit-recorded: true tb-program-active: false + bcg-birth-dose-recorded: true + opv-birth-dose-recorded: true + measles-dose-recorded: true diff --git a/crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker/integrations/health-record/fixtures/no-enrollment.yaml b/crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker/integrations/health-record/fixtures/no-enrollment.yaml new file mode 100644 index 000000000..62f8bf449 --- /dev/null +++ b/crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker/integrations/health-record/fixtures/no-enrollment.yaml @@ -0,0 +1,46 @@ +name: no-child-program-enrollment +classification: synthetic +input: { tracked_entity: A0000000001, include_inactive: true } +variables: { as_of_date: 2026-01-01 } +interactions: + - expect: + method: GET + path: /api/tracker/trackedEntities/A0000000001 + query: + fields: trackedEntity,attributes[attribute,value],enrollments[program,status,events[programStage,status]] + includeDeleted: true + respond: + status: 200 + body: + trackedEntity: A0000000001 + attributes: [] + enrollments: + - { program: DEMO_OTHER_PROGRAM_UID, status: ACTIVE, events: [] } +expect: + outcome: match + outputs: + first_name: null + last_name: null + date_of_birth: null + child_program_active: null + programme_code: null + reconciliation_reference: null + maternal_postnatal_active: null + child_health_visit_recorded: null + tb_program_active: null + bcg_birth_dose_recorded: null + opv_birth_dose_recorded: null + measles_dose_recorded: null + claims: + tracked-entity-first-name: null + tracked-entity-last-name: null + child-program-active: null + child-age-band: null + programme-code: null + reconciliation-reference: redacted + maternal-postnatal-care-active: null + child-health-visit-recorded: null + tb-program-active: null + bcg-birth-dose-recorded: null + opv-birth-dose-recorded: null + measles-dose-recorded: null diff --git a/crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker/integrations/health-record/fixtures/no-match.yaml b/crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker/integrations/health-record/fixtures/no-match.yaml index 1fecb6434..ded4cda16 100644 --- a/crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker/integrations/health-record/fixtures/no-match.yaml +++ b/crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker/integrations/health-record/fixtures/no-match.yaml @@ -16,10 +16,13 @@ expect: claims: tracked-entity-first-name: null tracked-entity-last-name: null - child-program-active: false + child-program-active: null child-age-band: null programme-code: null reconciliation-reference: redacted - maternal-postnatal-care-active: false - child-health-visit-recorded: false - tb-program-active: false + maternal-postnatal-care-active: null + child-health-visit-recorded: null + tb-program-active: null + bcg-birth-dose-recorded: null + opv-birth-dose-recorded: null + measles-dose-recorded: null diff --git a/crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker/integrations/health-record/fixtures/partial.yaml b/crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker/integrations/health-record/fixtures/partial.yaml new file mode 100644 index 000000000..006970494 --- /dev/null +++ b/crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker/integrations/health-record/fixtures/partial.yaml @@ -0,0 +1,50 @@ +name: partial-child-health-evidence +classification: synthetic +input: { tracked_entity: A0000000001, include_inactive: true } +variables: { as_of_date: 2026-01-01 } +interactions: + - expect: + method: GET + path: /api/tracker/trackedEntities/A0000000001 + query: + fields: trackedEntity,attributes[attribute,value],enrollments[program,status,events[programStage,status]] + includeDeleted: true + respond: + status: 200 + body: + trackedEntity: A0000000001 + attributes: [] + enrollments: + - program: DEMO_CHILD_PROGRAM_UID + status: CANCELLED + events: + - { programStage: DEMO_BCG_BIRTH_STAGE_UID, status: SKIPPED } + - { programStage: DEMO_MEASLES_STAGE_UID, status: COMPLETED } +expect: + outcome: match + outputs: + first_name: null + last_name: null + date_of_birth: null + child_program_active: false + programme_code: DEMO_CHILD_PROGRAM + reconciliation_reference: null + maternal_postnatal_active: null + child_health_visit_recorded: null + tb_program_active: null + bcg_birth_dose_recorded: false + opv_birth_dose_recorded: null + measles_dose_recorded: true + claims: + tracked-entity-first-name: null + tracked-entity-last-name: null + child-program-active: false + child-age-band: null + programme-code: DEMO_CHILD_PROGRAM + reconciliation-reference: redacted + maternal-postnatal-care-active: null + child-health-visit-recorded: null + tb-program-active: null + bcg-birth-dose-recorded: false + opv-birth-dose-recorded: null + measles-dose-recorded: true diff --git a/crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker/integrations/health-record/integration.yaml b/crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker/integrations/health-record/integration.yaml index afbe4a642..375eed5bb 100644 --- a/crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker/integrations/health-record/integration.yaml +++ b/crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker/integrations/health-record/integration.yaml @@ -29,16 +29,19 @@ outputs: first_name: { type: [string, "null"], maxLength: 80 } last_name: { type: [string, "null"], maxLength: 80 } date_of_birth: { type: [string, "null"], format: date, maxLength: 10 } - child_program_active: { type: boolean } + child_program_active: { type: [boolean, "null"] } programme_code: { type: [string, "null"], maxLength: 32 } reconciliation_reference: { type: [string, "null"], maxLength: 64 } - maternal_postnatal_active: { type: boolean } - child_health_visit_recorded: { type: boolean } - tb_program_active: { type: boolean } + maternal_postnatal_active: { type: [boolean, "null"] } + child_health_visit_recorded: { type: [boolean, "null"] } + tb_program_active: { type: [boolean, "null"] } + bcg_birth_dose_recorded: { type: [boolean, "null"] } + opv_birth_dose_recorded: { type: [boolean, "null"] } + measles_dose_recorded: { type: [boolean, "null"] } not_applicable: ambiguity: rationale: The tracked-entity identifier is resolved by the exact singleton trackedEntities resource endpoint, so a collection cannot be returned. - request_fixture: complete-health-match + request_fixture: complete-child-health-evidence limits: { calls: 1, source_bytes: 64KiB, request_bytes: 4KiB, deadline: 8s } diff --git a/crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker/registry-stack.yaml b/crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker/registry-stack.yaml index 9dc685bc9..a610b06e9 100644 --- a/crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker/registry-stack.yaml +++ b/crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker/registry-stack.yaml @@ -2,7 +2,7 @@ version: 1 starter: id: dhis2-tracker release: 0.12.2 - content_digest: sha256:406356fab5814d45f8cce64d85840031da0a87288778572e392e218dea0886f7 + content_digest: sha256:0f1f5fb710c5490d92346120d0cd791bac4bbd617033f03b9402ba3174caff19 registry: { id: fictional-health-registry } integrations: health-record: { file: integrations/health-record/integration.yaml } @@ -25,12 +25,7 @@ services: claims: tracked-entity-first-name: { output: health.first_name, disclosure: value } tracked-entity-last-name: { output: health.last_name, disclosure: value } - child-program-active: - cel: >- - health.child_program_active != null - ? health.matched && health.child_program_active - : false - disclosure: predicate + child-program-active: { output: health.child_program_active, disclosure: predicate } child-age-band: cel: >- health.matched && health.date_of_birth != null @@ -41,24 +36,12 @@ services: disclosure: value programme-code: { output: health.programme_code, disclosure: value } reconciliation-reference: { output: health.reconciliation_reference, disclosure: redacted } - maternal-postnatal-care-active: - cel: >- - health.maternal_postnatal_active != null - ? health.matched && health.maternal_postnatal_active - : false - disclosure: predicate - child-health-visit-recorded: - cel: >- - health.child_health_visit_recorded != null - ? health.matched && health.child_health_visit_recorded - : false - disclosure: predicate - tb-program-active: - cel: >- - health.tb_program_active != null - ? health.matched && health.tb_program_active - : false - disclosure: predicate + maternal-postnatal-care-active: { output: health.maternal_postnatal_active, disclosure: predicate } + child-health-visit-recorded: { output: health.child_health_visit_recorded, disclosure: predicate } + tb-program-active: { output: health.tb_program_active, disclosure: predicate } + bcg-birth-dose-recorded: { output: health.bcg_birth_dose_recorded, disclosure: predicate } + opv-birth-dose-recorded: { output: health.opv_birth_dose_recorded, disclosure: predicate } + measles-dose-recorded: { output: health.measles_dose_recorded, disclosure: predicate } credential_profiles: health-status: format: dc+sd-jwt @@ -75,3 +58,8 @@ services: type: https://credentials.invalid/programme-participation/v1 validity: 10m claims: [programme-code, reconciliation-reference, child-program-active] + child-health-evidence: + format: dc+sd-jwt + type: https://credentials.invalid/child-health-evidence/v1 + validity: 10m + claims: [child-program-active, bcg-birth-dose-recorded, opv-birth-dose-recorded, measles-dose-recorded] diff --git a/crates/registryctl/tests/fixtures/project-authoring/snapshot-exact/README.md b/crates/registryctl/tests/fixtures/project-authoring/snapshot-exact/README.md index ce4b7c562..e20d333f6 100644 --- a/crates/registryctl/tests/fixtures/project-authoring/snapshot-exact/README.md +++ b/crates/registryctl/tests/fixtures/project-authoring/snapshot-exact/README.md @@ -18,3 +18,12 @@ from this `registryctl` build for VS Code and Zed. Add a records service only when the project intentionally publishes the entity through Relay's governed records API. + +Relay normalizes the source fields as `registration_status` and +`residency_confirmed`. Notary exposes the reusable +`population-registration-status` and `residency-confirmed` evidence claims. +The evidence consumer, not this project, determines how those claims are used. +The decision owner remains accountable for eligibility, qualification, +prioritization, approval, payment, workflow, and action rules. A no-match keeps +both evidence values unknown rather than silently converting missing evidence +to a negative fact. diff --git a/crates/registryctl/tests/fixtures/project-authoring/snapshot-exact/entities/people.yaml b/crates/registryctl/tests/fixtures/project-authoring/snapshot-exact/entities/people.yaml index 0d049f5f6..652de0085 100644 --- a/crates/registryctl/tests/fixtures/project-authoring/snapshot-exact/entities/people.yaml +++ b/crates/registryctl/tests/fixtures/project-authoring/snapshot-exact/entities/people.yaml @@ -5,11 +5,11 @@ primary_key: person_id schema: type: object additionalProperties: false - required: [person_id, registration_status, eligible, guardian_id] + required: [person_id, registration_status, residency_confirmed, guardian_id] properties: person_id: { type: string, maxLength: 64 } registration_status: { type: [string, "null"], maxLength: 32 } - eligible: { type: [boolean, "null"] } + residency_confirmed: { type: [boolean, "null"] } guardian_id: { type: [string, "null"], maxLength: 64 } materialization: max_records: 1000000 diff --git a/crates/registryctl/tests/fixtures/project-authoring/snapshot-exact/environments/local.yaml b/crates/registryctl/tests/fixtures/project-authoring/snapshot-exact/environments/local.yaml index 09cf1a3dd..ff8f18ee5 100644 --- a/crates/registryctl/tests/fixtures/project-authoring/snapshot-exact/environments/local.yaml +++ b/crates/registryctl/tests/fixtures/project-authoring/snapshot-exact/environments/local.yaml @@ -8,7 +8,7 @@ entities: columns: person_id: subject_key registration_status: status_code - eligible: benefit_eligible + residency_confirmed: residency_flag guardian_id: guardian_key source_revision: population-export-v1 generation: 2026-07-12 diff --git a/crates/registryctl/tests/fixtures/project-authoring/snapshot-exact/integrations/person-snapshot/fixtures/match.yaml b/crates/registryctl/tests/fixtures/project-authoring/snapshot-exact/integrations/person-snapshot/fixtures/match.yaml index f3ae1d58c..bbe7cc7d4 100644 --- a/crates/registryctl/tests/fixtures/project-authoring/snapshot-exact/integrations/person-snapshot/fixtures/match.yaml +++ b/crates/registryctl/tests/fixtures/project-authoring/snapshot-exact/integrations/person-snapshot/fixtures/match.yaml @@ -3,13 +3,13 @@ classification: synthetic input: { person_id: PER-00000001 } interactions: - expect: { method: GET, path: /snapshot } - respond: { status: 200, body: { registration_status: active, eligible: true } } + respond: { status: 200, body: { registration_status: active, residency_confirmed: true } } expect: outcome: match - outputs: { registration_status: active, eligible: true } + outputs: { registration_status: active, residency_confirmed: true } claims: population-record-exists: true - benefits-status: active - benefits-eligible: true + population-registration-status: active + residency-confirmed: true emergency-record-exists: true emergency-status: redacted diff --git a/crates/registryctl/tests/fixtures/project-authoring/snapshot-exact/integrations/person-snapshot/fixtures/no-match.yaml b/crates/registryctl/tests/fixtures/project-authoring/snapshot-exact/integrations/person-snapshot/fixtures/no-match.yaml index 972dad2b4..55a1e18a6 100644 --- a/crates/registryctl/tests/fixtures/project-authoring/snapshot-exact/integrations/person-snapshot/fixtures/no-match.yaml +++ b/crates/registryctl/tests/fixtures/project-authoring/snapshot-exact/integrations/person-snapshot/fixtures/no-match.yaml @@ -9,7 +9,7 @@ expect: outputs: {} claims: population-record-exists: false - benefits-status: null - benefits-eligible: null + population-registration-status: null + residency-confirmed: null emergency-record-exists: false emergency-status: redacted diff --git a/crates/registryctl/tests/fixtures/project-authoring/snapshot-exact/integrations/person-snapshot/integration.yaml b/crates/registryctl/tests/fixtures/project-authoring/snapshot-exact/integrations/person-snapshot/integration.yaml index 4baa2fd06..5dbae4f8e 100644 --- a/crates/registryctl/tests/fixtures/project-authoring/snapshot-exact/integrations/person-snapshot/integration.yaml +++ b/crates/registryctl/tests/fixtures/project-authoring/snapshot-exact/integrations/person-snapshot/integration.yaml @@ -13,7 +13,7 @@ capability: exact: person_id: { input: person_id } freshness: 24h -outputs: [registration_status, eligible] +outputs: [registration_status, residency_confirmed] not_applicable: ambiguity: rationale: The exact snapshot selector is the entity primary key, whose materialized unique-key constraint permits at most one record. diff --git a/crates/registryctl/tests/fixtures/project-authoring/snapshot-exact/registry-stack.yaml b/crates/registryctl/tests/fixtures/project-authoring/snapshot-exact/registry-stack.yaml index 78f6c7741..92efb42a5 100644 --- a/crates/registryctl/tests/fixtures/project-authoring/snapshot-exact/registry-stack.yaml +++ b/crates/registryctl/tests/fixtures/project-authoring/snapshot-exact/registry-stack.yaml @@ -2,7 +2,7 @@ version: 1 starter: id: snapshot release: 0.12.2 - content_digest: sha256:efcf38e246e52ad93ca5d0f716386f8657080afe133a63782d1892f802ee630f + content_digest: sha256:fddfd7203247c69ada8449b456bd4878e3d760061f4b99be23919e63d61237fb registry: { id: fictional-population-registry } integrations: person-snapshot: { file: integrations/person-snapshot/integration.yaml } @@ -22,14 +22,14 @@ services: input: { person_id: request.target.identifiers.population_person_id } claims: population-record-exists: { cel: person.matched, disclosure: predicate } - benefits-status: { output: person.registration_status, disclosure: value } - benefits-eligible: { output: person.eligible, disclosure: predicate } + population-registration-status: { output: person.registration_status, disclosure: value } + residency-confirmed: { output: person.residency_confirmed, disclosure: predicate } credential_profiles: - benefits-status: + population-evidence: format: dc+sd-jwt - type: https://credentials.invalid/benefits-status/v1 + type: https://credentials.invalid/population-evidence/v1 validity: 10m - claims: [population-record-exists, benefits-status, benefits-eligible] + claims: [population-record-exists, population-registration-status, residency-confirmed] emergency-assistance: kind: evidence version: 1 diff --git a/crates/registryctl/tests/fixtures/project-authoring/snapshot-with-records/entities/people.yaml b/crates/registryctl/tests/fixtures/project-authoring/snapshot-with-records/entities/people.yaml index 0d049f5f6..652de0085 100644 --- a/crates/registryctl/tests/fixtures/project-authoring/snapshot-with-records/entities/people.yaml +++ b/crates/registryctl/tests/fixtures/project-authoring/snapshot-with-records/entities/people.yaml @@ -5,11 +5,11 @@ primary_key: person_id schema: type: object additionalProperties: false - required: [person_id, registration_status, eligible, guardian_id] + required: [person_id, registration_status, residency_confirmed, guardian_id] properties: person_id: { type: string, maxLength: 64 } registration_status: { type: [string, "null"], maxLength: 32 } - eligible: { type: [boolean, "null"] } + residency_confirmed: { type: [boolean, "null"] } guardian_id: { type: [string, "null"], maxLength: 64 } materialization: max_records: 1000000 diff --git a/crates/registryctl/tests/fixtures/project-authoring/snapshot-with-records/environments/local.yaml b/crates/registryctl/tests/fixtures/project-authoring/snapshot-with-records/environments/local.yaml index 90885c70f..1fa460442 100644 --- a/crates/registryctl/tests/fixtures/project-authoring/snapshot-with-records/environments/local.yaml +++ b/crates/registryctl/tests/fixtures/project-authoring/snapshot-with-records/environments/local.yaml @@ -8,7 +8,7 @@ entities: columns: person_id: subject_key registration_status: status_code - eligible: benefit_eligible + residency_confirmed: residency_flag guardian_id: guardian_key source_revision: population-export-v1 generation: 2026-07-13 diff --git a/crates/registryctl/tests/fixtures/project-authoring/snapshot-with-records/integrations/person-snapshot/fixtures/match.yaml b/crates/registryctl/tests/fixtures/project-authoring/snapshot-with-records/integrations/person-snapshot/fixtures/match.yaml index f3ae1d58c..bbe7cc7d4 100644 --- a/crates/registryctl/tests/fixtures/project-authoring/snapshot-with-records/integrations/person-snapshot/fixtures/match.yaml +++ b/crates/registryctl/tests/fixtures/project-authoring/snapshot-with-records/integrations/person-snapshot/fixtures/match.yaml @@ -3,13 +3,13 @@ classification: synthetic input: { person_id: PER-00000001 } interactions: - expect: { method: GET, path: /snapshot } - respond: { status: 200, body: { registration_status: active, eligible: true } } + respond: { status: 200, body: { registration_status: active, residency_confirmed: true } } expect: outcome: match - outputs: { registration_status: active, eligible: true } + outputs: { registration_status: active, residency_confirmed: true } claims: population-record-exists: true - benefits-status: active - benefits-eligible: true + population-registration-status: active + residency-confirmed: true emergency-record-exists: true emergency-status: redacted diff --git a/crates/registryctl/tests/fixtures/project-authoring/snapshot-with-records/integrations/person-snapshot/fixtures/no-match.yaml b/crates/registryctl/tests/fixtures/project-authoring/snapshot-with-records/integrations/person-snapshot/fixtures/no-match.yaml index 972dad2b4..55a1e18a6 100644 --- a/crates/registryctl/tests/fixtures/project-authoring/snapshot-with-records/integrations/person-snapshot/fixtures/no-match.yaml +++ b/crates/registryctl/tests/fixtures/project-authoring/snapshot-with-records/integrations/person-snapshot/fixtures/no-match.yaml @@ -9,7 +9,7 @@ expect: outputs: {} claims: population-record-exists: false - benefits-status: null - benefits-eligible: null + population-registration-status: null + residency-confirmed: null emergency-record-exists: false emergency-status: redacted diff --git a/crates/registryctl/tests/fixtures/project-authoring/snapshot-with-records/integrations/person-snapshot/integration.yaml b/crates/registryctl/tests/fixtures/project-authoring/snapshot-with-records/integrations/person-snapshot/integration.yaml index 4baa2fd06..5dbae4f8e 100644 --- a/crates/registryctl/tests/fixtures/project-authoring/snapshot-with-records/integrations/person-snapshot/integration.yaml +++ b/crates/registryctl/tests/fixtures/project-authoring/snapshot-with-records/integrations/person-snapshot/integration.yaml @@ -13,7 +13,7 @@ capability: exact: person_id: { input: person_id } freshness: 24h -outputs: [registration_status, eligible] +outputs: [registration_status, residency_confirmed] not_applicable: ambiguity: rationale: The exact snapshot selector is the entity primary key, whose materialized unique-key constraint permits at most one record. diff --git a/crates/registryctl/tests/fixtures/project-authoring/snapshot-with-records/registry-stack.yaml b/crates/registryctl/tests/fixtures/project-authoring/snapshot-with-records/registry-stack.yaml index 490a48ade..444941552 100644 --- a/crates/registryctl/tests/fixtures/project-authoring/snapshot-with-records/registry-stack.yaml +++ b/crates/registryctl/tests/fixtures/project-authoring/snapshot-with-records/registry-stack.yaml @@ -16,7 +16,7 @@ services: aggregate: people:aggregate evidence_verification: people:evidence_verification purposes: [case-management] - projection: [person_id, registration_status, eligible] + projection: [person_id, registration_status, residency_confirmed] pagination: { default_limit: 50, max_limit: 100 } filters: { person_id: [eq] } required_principal_filters: [person_id] @@ -34,14 +34,14 @@ services: input: { person_id: request.target.identifiers.population_person_id } claims: population-record-exists: { cel: person.matched, disclosure: predicate } - benefits-status: { output: person.registration_status, disclosure: value } - benefits-eligible: { output: person.eligible, disclosure: predicate } + population-registration-status: { output: person.registration_status, disclosure: value } + residency-confirmed: { output: person.residency_confirmed, disclosure: predicate } credential_profiles: - benefits-status: + population-evidence: format: dc+sd-jwt - type: https://credentials.invalid/benefits-status/v1 + type: https://credentials.invalid/population-evidence/v1 validity: 10m - claims: [population-record-exists, benefits-status, benefits-eligible] + claims: [population-record-exists, population-registration-status, residency-confirmed] emergency-assistance: kind: evidence version: 1 diff --git a/crates/registryctl/tests/init_output.rs b/crates/registryctl/tests/init_output.rs index 242d0c568..8701b40a2 100644 --- a/crates/registryctl/tests/init_output.rs +++ b/crates/registryctl/tests/init_output.rs @@ -284,12 +284,6 @@ fn json_init_rejects_non_utf8_destinations_before_all_dispatches() { &["--format", "json"][..], true, ), - ( - "spreadsheet-api", - &["init", "spreadsheet-api"][..], - &["--format", "json"][..], - true, - ), ] { let mut leaf = format!("{name}-").into_bytes(); leaf.push(0xff); diff --git a/crates/registryctl/tests/project_authoring.rs b/crates/registryctl/tests/project_authoring.rs index abdda82d4..13562a0b4 100644 --- a/crates/registryctl/tests/project_authoring.rs +++ b/crates/registryctl/tests/project_authoring.rs @@ -481,7 +481,7 @@ fn project_check_collects_separate_integration_and_fixture_yaml_errors() { "version: [\n", ) .expect("invalid integration writes"); - let fixture_path = project.join("integrations/eligibility/fixtures/eligible.yaml"); + let fixture_path = project.join("integrations/eligibility/fixtures/source-approved.yaml"); let mut fixture = std::fs::read_to_string(&fixture_path).expect("fixture reads"); fixture.push_str("unknown_authoring_field: true\n"); std::fs::write(&fixture_path, fixture).expect("unknown fixture field writes"); @@ -506,7 +506,7 @@ fn project_check_collects_separate_integration_and_fixture_yaml_errors() { let fixture = report .diagnostics .iter() - .find(|diagnostic| diagnostic.file.ends_with("fixtures/eligible.yaml")) + .find(|diagnostic| diagnostic.file.ends_with("fixtures/source-approved.yaml")) .expect("fixture unknown-field diagnostic"); assert_eq!(fixture.code, "registryctl.authoring.yaml.unknown_field"); assert!(fixture.line.is_some()); @@ -521,7 +521,7 @@ fn project_check_collects_separate_integration_and_fixture_yaml_errors() { fn project_check_single_error_report_is_concise_and_typed() { let temporary = tempfile::tempdir().expect("temporary directory"); let project = copy_project("custom-system", temporary.path()); - let fixture = project.join("integrations/eligibility/fixtures/eligible.yaml"); + let fixture = project.join("integrations/eligibility/fixtures/source-approved.yaml"); std::fs::write(&fixture, "name: [\n").expect("invalid fixture writes"); let report = authoring_diagnostics(&project); assert_eq!(report.diagnostics.len(), 1, "{report:#?}"); @@ -540,7 +540,7 @@ fn project_check_cli_renders_the_same_typed_diagnostic_in_human_and_json() { let temporary = tempfile::tempdir().expect("temporary directory"); let project = copy_project("custom-system", temporary.path()); std::fs::write( - project.join("integrations/eligibility/fixtures/eligible.yaml"), + project.join("integrations/eligibility/fixtures/source-approved.yaml"), "name: [\n", ) .expect("invalid fixture writes"); @@ -605,7 +605,7 @@ fn project_check_cli_rejects_an_unselected_environment_symlink_with_typed_output symlink(&target, project.join("environments/zzz.yaml")) .expect("unselected environment symlink creates"); - let fixture_path = project.join("integrations/eligibility/fixtures/eligible.yaml"); + let fixture_path = project.join("integrations/eligibility/fixtures/source-approved.yaml"); let mut fixture = read_yaml(&fixture_path); fixture["expect"]["outputs"]["approved"] = serde_norway::Value::Bool(false); write_yaml(&fixture_path, &fixture); @@ -682,7 +682,7 @@ fn project_check_cli_reports_malformed_root_before_unselected_environment_bounda ) .expect("malformed referenced integration writes"); std::fs::write( - project.join("integrations/eligibility/fixtures/eligible.yaml"), + project.join("integrations/eligibility/fixtures/source-approved.yaml"), format!("{FIXTURE_MARKER}: [\n"), ) .expect("malformed fixture writes"); @@ -1328,6 +1328,148 @@ fn approved_opencrvs_and_dhis2_claim_sets_execute_offline() { } } +#[test] +fn dhis2_health_evidence_journey_preserves_distinct_results() { + let project = golden("dhis2-tracker"); + let report = test_registry_project(&ProjectTestOptions { + project_directory: project.clone(), + environment: None, + live: false, + }) + .expect("DHIS2 health evidence journey passes offline"); + assert_eq!(report.status, "passed"); + + let expected_outputs = [ + "bcg_birth_dose_recorded", + "child_health_visit_recorded", + "child_program_active", + "date_of_birth", + "first_name", + "last_name", + "maternal_postnatal_active", + "measles_dose_recorded", + "opv_birth_dose_recorded", + "programme_code", + "reconciliation_reference", + "tb_program_active", + ] + .map(String::from); + let expected_claims = [ + "bcg-birth-dose-recorded", + "child-age-band", + "child-health-visit-recorded", + "child-program-active", + "maternal-postnatal-care-active", + "measles-dose-recorded", + "opv-birth-dose-recorded", + "programme-code", + "reconciliation-reference", + "tb-program-active", + "tracked-entity-first-name", + "tracked-entity-last-name", + ] + .map(String::from); + + for fixture_name in [ + "complete-child-health-evidence", + "partial-child-health-evidence", + "no-child-program-enrollment", + ] { + let fixture = report + .fixtures + .iter() + .find(|fixture| fixture.fixture == fixture_name) + .unwrap_or_else(|| panic!("missing {fixture_name}")); + assert_eq!(fixture.outcome.as_deref(), Some("match")); + assert_eq!(fixture.outputs, expected_outputs); + assert_eq!(fixture.claims, expected_claims); + assert!(fixture.passed, "{fixture:#?}"); + } + + let no_match = report + .fixtures + .iter() + .find(|fixture| fixture.fixture == "health-no-match") + .expect("no-match fixture report"); + assert_eq!(no_match.outcome.as_deref(), Some("no_match")); + assert!(no_match.outputs.is_empty()); + assert_eq!(no_match.claims, expected_claims); + assert!(no_match.passed, "{no_match:#?}"); + + for (fixture_name, expected_error) in [ + ("health-source-rejected", "source.status_rejected"), + ("health-subject-mismatch", "failure.subject_mismatch"), + ] { + let fixture = report + .fixtures + .iter() + .find(|fixture| fixture.fixture == fixture_name) + .unwrap_or_else(|| panic!("missing {fixture_name}")); + assert_eq!(fixture.expected_error.as_deref(), Some(expected_error)); + assert_eq!(fixture.source_access, Some(true)); + assert!(fixture.outputs.is_empty()); + assert!(fixture.claims.is_empty()); + assert!(fixture.passed, "{fixture:#?}"); + } + + let malformed = report + .fixtures + .iter() + .find(|fixture| fixture.fixture.ends_with("::derived/malformed_decode")) + .expect("derived malformed-source fixture report"); + assert_eq!( + malformed.expected_error.as_deref(), + Some("source.response_malformed") + ); + assert_eq!(malformed.source_access, Some(true)); + assert!(malformed.passed, "{malformed:#?}"); + + let fixtures = project.join("integrations/health-record/fixtures"); + let complete = read_yaml(&fixtures.join("match.yaml")); + for claim in [ + "child-program-active", + "bcg-birth-dose-recorded", + "opv-birth-dose-recorded", + "measles-dose-recorded", + ] { + assert_eq!(complete["expect"]["claims"][claim].as_bool(), Some(true)); + } + + let partial = read_yaml(&fixtures.join("partial.yaml")); + assert_eq!( + partial["expect"]["claims"]["child-program-active"].as_bool(), + Some(false) + ); + assert_eq!( + partial["expect"]["claims"]["bcg-birth-dose-recorded"].as_bool(), + Some(false) + ); + assert!(partial["expect"]["claims"]["opv-birth-dose-recorded"].is_null()); + assert_eq!( + partial["expect"]["claims"]["measles-dose-recorded"].as_bool(), + Some(true) + ); + + for fixture_name in ["no-enrollment.yaml", "no-match.yaml"] { + let fixture = read_yaml(&fixtures.join(fixture_name)); + for claim in [ + "child-program-active", + "bcg-birth-dose-recorded", + "opv-birth-dose-recorded", + "measles-dose-recorded", + ] { + assert!( + fixture["expect"]["claims"][claim].is_null(), + "{fixture_name} must keep {claim} unknown" + ); + } + } + + let authored = read_yaml(&project.join("registry-stack.yaml")); + assert!(!yaml_contains_string(&authored, "eligible")); + assert!(!yaml_contains_string(&authored, "outreach")); +} + #[test] fn successful_negative_fixtures_report_the_closed_denial_assertion() { let report = test_registry_project(&ProjectTestOptions { @@ -1371,8 +1513,8 @@ fn successful_negative_fixtures_report_the_closed_denial_assertion() { let successful = report .fixtures .iter() - .find(|fixture| fixture.fixture == "eligible-household") - .expect("eligible fixture report"); + .find(|fixture| fixture.fixture == "source-approved-household") + .expect("source-approved fixture report"); assert_eq!(successful.expected_error, None); assert_eq!(successful.source_access, None); } @@ -1380,7 +1522,11 @@ fn successful_negative_fixtures_report_the_closed_denial_assertion() { #[test] fn exact_sources_report_reviewable_ambiguity_not_applicable_evidence() { for (project, integration, fixture) in [ - ("dhis2-tracker", "health-record", "complete-health-match"), + ( + "dhis2-tracker", + "health-record", + "complete-child-health-evidence", + ), ("openspp-exact", "individual", "social-registry-match"), ("snapshot-exact", "person-snapshot", "snapshot-match"), ] { @@ -1419,7 +1565,7 @@ fn exact_sources_report_reviewable_ambiguity_not_applicable_evidence() { #[test] fn response_contracts_without_comparable_identifiers_report_subject_mismatch_evidence() { for (project, integration, fixture) in [ - ("custom-system", "eligibility", "eligible-household"), + ("custom-system", "eligibility", "source-approved-household"), ("openspp-exact", "individual", "social-registry-match"), ("snapshot-exact", "person-snapshot", "snapshot-match"), ] { @@ -1571,8 +1717,8 @@ fn subject_mismatch_not_applicable_rejects_comparable_output_contract() { let project = copy_project("snapshot-exact", temporary.path()); replace_in_file( &project.join("integrations/person-snapshot/integration.yaml"), - "outputs: [registration_status, eligible]", - "outputs: [person_id, registration_status, eligible]", + "outputs: [registration_status, residency_confirmed]", + "outputs: [person_id, registration_status, residency_confirmed]", ); let error = test_registry_project(&ProjectTestOptions { project_directory: project, @@ -2905,7 +3051,7 @@ fn pre_freeze_fact_authoring_keys_are_rejected_without_aliases() { let fixture_root = tempfile::tempdir().expect("fixture-key temporary directory"); let fixture = copy_project("custom-system", fixture_root.path()); - let fixture_path = fixture.join("integrations/eligibility/fixtures/eligible.yaml"); + let fixture_path = fixture.join("integrations/eligibility/fixtures/source-approved.yaml"); replace_in_file(&fixture_path, " outputs:", " facts:"); let error = test_registry_project(&ProjectTestOptions { project_directory: fixture, @@ -3007,7 +3153,7 @@ fn authored_unknown_fields_and_traversal_fail_closed() { fn fixture_failure_reports_safe_validation_error_without_input_value() { let temporary = tempfile::tempdir().expect("temporary directory"); let project = copy_project("custom-system", temporary.path()); - let fixture_path = project.join("integrations/eligibility/fixtures/eligible.yaml"); + let fixture_path = project.join("integrations/eligibility/fixtures/source-approved.yaml"); replace_in_file(&fixture_path, "HH-AB12CD34", "invalid-reference"); let error = test_registry_project(&ProjectTestOptions { @@ -3022,7 +3168,7 @@ fn fixture_failure_reports_safe_validation_error_without_input_value() { "{diagnostic}" ); assert!( - diagnostic.contains("integrations/eligibility/fixtures/eligible.yaml"), + diagnostic.contains("integrations/eligibility/fixtures/source-approved.yaml"), "{diagnostic}" ); assert!( @@ -4106,7 +4252,7 @@ fn dci_exact_and_and_full_date_inputs_fail_closed_before_source_access() { let temporary = tempfile::tempdir().expect("temporary directory"); let project = copy_project("custom-system", temporary.path()); extend_exact_selector(&project, "custom-system", 4); - let fixture = project.join("integrations/eligibility/fixtures/eligible.yaml"); + let fixture = project.join("integrations/eligibility/fixtures/source-approved.yaml"); replace_in_file(&fixture, "2017-06-15", "2017-02-31"); let error = test_registry_project(&ProjectTestOptions { project_directory: project, @@ -4119,7 +4265,7 @@ fn dci_exact_and_and_full_date_inputs_fail_closed_before_source_access() { let temporary = tempfile::tempdir().expect("temporary directory"); let project = copy_project("custom-system", temporary.path()); extend_exact_selector(&project, "custom-system", 3); - let fixture = project.join("integrations/eligibility/fixtures/eligible.yaml"); + let fixture = project.join("integrations/eligibility/fixtures/source-approved.yaml"); let mut document = read_yaml(&fixture); document["input"] .as_mapping_mut() @@ -4256,7 +4402,7 @@ fn check_and_build_produce_deterministic_product_inputs() { .pointer("/services/household-eligibility/consultations") .is_some()); assert!(explanation - .pointer("/services/household-eligibility/claims/household-eligible/cel") + .pointer("/services/household-eligibility/claims/source-household-approval-decision/cel",) .and_then(serde_json::Value::as_str) .is_some()); assert!(explanation @@ -4308,7 +4454,7 @@ fn check_and_build_produce_deterministic_product_inputs() { assert_eq!(first_closure, directory_closure(&output)); assert_eq!( closure_digest(&first_closure), - "bb52a3962eeed19d05577e23b6f092c02cb3a0fc18481da469a98517a57df9d5", + "f7202608870d8f7da613ce3b355f6c4c4d99fc7e863c135eb1d61e33f65806db", "project inputs must match the cross-machine golden digest" ); } @@ -4436,7 +4582,7 @@ fn generated_snapshot_contracts_activate_through_notary_at_the_authoring_bound() .evidence .claims .iter() - .find(|claim| claim.id == "benefits-status") + .find(|claim| claim.id == "population-registration-status") .expect("registry-backed snapshot claim"); let ClaimEvidenceMode::RegistryBacked { consultations } = &claim.evidence_mode else { panic!("snapshot claim remains registry-backed"); @@ -5549,7 +5695,7 @@ registry_type: civil-registry record_type: person identifiers: { person_id: person_id } expression_fields: { registration_status: registration_status } -response_fields: { eligible: eligible } +response_fields: { residency_confirmed: residency_confirmed } "#, ) .expect("SP DCI mapping"); @@ -6199,8 +6345,8 @@ fn verified_signed_baseline_classifies_semantic_review_dimensions_independently( let authored = std::fs::read_to_string(&project_file) .expect("project reads") .replace( - "household.approved != null ? household.matched && household.approved : false", - "household.approved != null ? household.matched && household.approved == true : false", + "household.matched && household.approved != null ? household.approved : null", + "household.matched && household.approved != null ? household.approved == true : null", ); std::fs::write(&project_file, authored).expect("claim-only edit writes"); let changed = check_registry_project(&ProjectCheckOptions { @@ -6798,12 +6944,12 @@ fn remove_custom_cel_claim(project: &Path) { .as_mapping_mut() .expect("custom claims") .remove(serde_norway::Value::String( - "household-eligible".to_string(), + "source-household-approval-decision".to_string(), )); service["credential_profiles"]["household-eligibility"]["claims"] .as_sequence_mut() .expect("custom credential claims") - .retain(|claim| claim.as_str() != Some("household-eligible")); + .retain(|claim| claim.as_str() != Some("source-household-approval-decision")); write_yaml(&project_path, &document); for fixture in std::fs::read_dir(project.join("integrations/eligibility/fixtures")) .expect("custom fixture directory") @@ -6817,7 +6963,7 @@ fn remove_custom_cel_claim(project: &Path) { .and_then(serde_norway::Value::as_mapping_mut); if let Some(claims) = claims { claims.remove(serde_norway::Value::String( - "household-eligible".to_string(), + "source-household-approval-decision".to_string(), )); } write_yaml(&path, &document); diff --git a/docs/site/astro.config.mjs b/docs/site/astro.config.mjs index a22b5831b..3df14bd1c 100644 --- a/docs/site/astro.config.mjs +++ b/docs/site/astro.config.mjs @@ -390,6 +390,7 @@ export default defineConfig({ { label: 'RS-TERMS · Terms', slug: 'spec/rs-terms' }, { label: 'RS-ARC-G · Architecture', slug: 'spec/rs-arc-g' }, { label: 'RS-PR-NOTARY · Notary protocol', slug: 'spec/rs-pr-notary' }, + { label: 'RS-PR-REGISTRYCTL · registryctl contract', slug: 'spec/rs-pr-registryctl' }, { label: 'RS-PR-RELAY · Relay protocol', slug: 'spec/rs-pr-relay' }, { label: 'RS-SEC-G · Security model', slug: 'spec/rs-sec-g' }, { label: 'RS-DM-CLAIM · Claim definition model', slug: 'spec/rs-dm-claim' }, diff --git a/docs/site/scripts/generate-project-starters.test.mjs b/docs/site/scripts/generate-project-starters.test.mjs index 6289e310c..dd7c39554 100644 --- a/docs/site/scripts/generate-project-starters.test.mjs +++ b/docs/site/scripts/generate-project-starters.test.mjs @@ -65,7 +65,7 @@ test('derives all advertised starter selections from committed workspaces', asyn { starter: 'dhis2-tracker', integration: 'health-record', - fixture: 'complete-health-match', + fixture: 'complete-child-health-evidence', }, { starter: 'opencrvs-dci', diff --git a/docs/site/src/content/docs/changelog.mdx b/docs/site/src/content/docs/changelog.mdx index 6f1080e9f..f43fe4caf 100644 --- a/docs/site/src/content/docs/changelog.mdx +++ b/docs/site/src/content/docs/changelog.mdx @@ -16,6 +16,12 @@ documents. Per-product release notes live in each product repository; the entries below link to the relevant product pages on this site rather than duplicating release notes. +## 2026-07-23 + +- Made the Relay-to-Notary-to-evidence-consumer responsibility boundary normative for 1.0 project + authoring. Updated Notary guidance and registryctl examples to keep reusable evidence separate + from consumer-owned requirements, decisions, workflow, and action rules. + ## 2026-07-20 Documentation updates for the v0.12.2 beta-16 fix-forward release: diff --git a/docs/site/src/content/docs/explanation/architecture.mdx b/docs/site/src/content/docs/explanation/architecture.mdx index deef031db..f313c909a 100644 --- a/docs/site/src/content/docs/explanation/architecture.mdx +++ b/docs/site/src/content/docs/explanation/architecture.mdx @@ -64,6 +64,45 @@ updated policy documents without touching deployment config. topologies." /> +## Evidence and decision ownership + +The following responsibility flow is normative for 1.0 project authoring: + +1. A source system owns its operational data and any decisions made inside that source. +2. Registry Relay owns source-specific acquisition, source-access policy, and typed normalization. + Relay returns only the declared consultation outcome and typed outputs. It does not assign a + consumer consequence to those outputs. +3. Registry Notary owns atomic, precise, reusable evidence statements plus the authorization and + disclosure policy for evaluating and releasing them. Purpose-bound authorization controls who + can request evidence and what can be disclosed. It does not turn the evidence statement into a + consumer rule. +4. An evidence consumer determines how returned evidence is used. The accountable decision owner + retains responsibility for requirements, eligibility, qualification, prioritization, approval, + routing, payment, workflow, and action policy. + +These are three different policy categories: Relay source-access and adaptation policy, Notary +evidence authorization and disclosure policy, and consumer use, decision, and action policy. +Project authors must keep them separate even when one deployment operates every component. The +caller, evidence consumer, and decision owner can be the same component or separate components. + +An evidence consumer can be a social-protection programme, admissions service, licensing +authority, healthcare workflow, lender, insurer, or credential verifier. The caller is the +technical client that invokes Notary, which may be a portal, intermediary, wallet backend, or +workflow connector acting for the consumer. + +| Context | Caller | Evidence consumer | Decision owner | +| --- | --- | --- | --- | +| Social protection | Case-management connector | Programme workflow | Administering authority | +| Public administration | Procedure portal or intermediary | Online procedure | Competent authority | +| Education | Admissions portal | Admissions workflow | Education institution | +| Healthcare | Clinical or case system | Care or public-health workflow | Provider or health authority | +| Regulated private service | Application platform | Lending or insurance workflow | Lender or insurer | + +Registry Notary can attest a decision already made by an authoritative source system. The claim +identifier and its review documentation must identify that fact as a source-owned decision. +Notary attests what the source decided; it does not recompute the decision or take ownership of +the source's policy. + ## Data and contract flow 1. Registry Platform provides reusable security and operational primitives consumed by runtime services. @@ -78,12 +117,15 @@ updated policy documents without touching deployment config. endpoints through configured auth. 5. Registry Relay executes compiler-pinned consultations. Its product-neutral `http`, `script`, and `snapshot` capabilities adapt reviewed source contracts and return only declared typed - outputs. Registry Notary evaluates registry-backed claims from those Relay outputs, or evaluates - source-free and self-attested claims without Relay. It renders evaluation results as + outputs. Registry Notary evaluates registry-backed evidence claims from those Relay outputs, or + evaluates source-free and self-attested evidence claims without Relay. It renders evaluation + results as `application/vnd.registry-notary.claim-result+json` - or CCCEV-shaped JSON-LD (`application/ld+json; profile="cccev"`), and it materializes eligible - stored evaluations into SD-JWT VC credentials (`application/dc+sd-jwt`). -6. A trusted Registry Notary can call another trusted Registry Notary through + or CCCEV-shaped JSON-LD (`application/ld+json; profile="cccev"`), and it materializes stored + evaluations that pass issuance checks into SD-JWT VC credentials (`application/dc+sd-jwt`). +6. An evidence consumer receives the returned evidence. The decision owner applies any + consumer-specific requirement, decision, workflow, or action rules outside Relay and Notary. +7. A trusted Registry Notary can call another trusted Registry Notary through `POST /federation/v1/evaluations` for signed delegated evaluation. Registry Manifest can publish discovery metadata for that relationship, but local Notary peer policy grants access. For what each project does and does not own, refer to the product overview pages linked in the capabilities table. diff --git a/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx b/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx index ebd40faa7..021d6085b 100644 --- a/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx +++ b/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx @@ -16,15 +16,17 @@ still answering questions about the subjects in it. The short answer is that a caller asks a question and receives a *computed answer*, not the record. This page explains how that works, what each kind of answer does and does not reveal, and where the privacy claim has edges. -You will see one product term used throughout: a **claim**, which is a single pre-modelled -question (one decision or one extracted value), such as "is this person registered?" or "what is -this person's registration date?". -A claim is deliberately narrow: a claim that tried to return a whole record would over-collect and -be hard to authorize, which is exactly the outcome this design avoids. +You will see one product term used throughout: an **evidence claim**, which is one atomic, +precisely defined evidence statement or extracted value, such as "is this person registered?" or +"what is this person's registration date?". +A claim is deliberately narrow and reusable. A claim that returned a whole record would +over-collect, and a claim that embedded one consumer's eligibility or action rules could not be +reused as neutral evidence. ## How Registry Notary controls what leaves the service -Registry Notary is the component that evaluates a claim and decides what the caller receives back. +Registry Notary is the component that evaluates an evidence claim and enforces what the caller +receives back. It controls the answer through three **disclosure modes**: `value`, `predicate`, and `redacted`. There are exactly three. `value` is not always all-or-nothing, though: when a claim returns an object, `value` mode can @@ -49,18 +51,36 @@ inputs. The caller does not supply the evaluated value, and cannot inject one. The evaluation runs as a pipeline: Relay returns a closed typed consultation result when the claim -is registry-backed, a rule computes the configured condition, a disclosure mode shapes what leaves -the service, and a response format carries the result. +is registry-backed, a rule evaluates the configured evidence statement, a disclosure mode shapes +what leaves the service, and a response format carries the result. Because Notary computes the answer from the closed Relay result or permitted self-attestation, it can return that computed answer rather than handing back a source record. The rule that does the computing is one of three kinds. An **exists** rule checks whether the pinned consultation produced one admissible match. An **extract** rule reads a declared typed output. -A **cel** rule derives a value from typed outputs, request variables, or earlier claim results -through a hardened expression. +A **`cel`** rule derives an evidence value from typed outputs, request variables, or earlier claim +results through a hardened expression. Each kind produces an answer about the subject without that answer being a copy of the source row. +## Evidence is not a consumer decision + +Registry Relay source-access and adaptation policy controls how an authoritative source is read +and normalized. Registry Notary evidence policy controls which evidence statement is evaluated, +who can request it, and how much can be disclosed. The evidence consumer determines how the +evidence is used, while the decision owner remains accountable for requirements, decisions, and +actions. + +A Common Expression Language (CEL) rule can derive an evidence predicate, such as whether the +source records an active programme enrollment or a measles dose. It is not a general-purpose +consumer eligibility or decision engine. A public-health programme, for example, can act as both +evidence consumer and decision owner when it combines those evidence claims with its own +thresholds, priorities, and case state to decide whether outreach or follow-up is required. + +Registry Notary can also attest a decision that an authoritative source already owns. The claim +identifier and its review documentation must say that it is a source-owned decision. Notary does +not recompute that decision or present the source's decision policy as Notary policy. + ## The three disclosure modes The disclosure mode fixes how much of the computed answer the caller receives: @@ -87,9 +107,8 @@ A question of whether someone has a registered record is modelled as an `exists` matching record returns `evidence.not_available` (collapsed to a single public reason by default) rather than `false`; absence is surfaced as no-evidence, not as a negative result. Either way the source row never leaves the service. -A question about an eligibility threshold without exposing the figure behind it is modelled as a -`cel` rule whose eligibility boolean is disclosed as `predicate`; the underlying value stays -inside the service. +A source fact such as whether a recorded vaccination dose is present can be modelled as a boolean +evidence predicate without returning the underlying health record. A `redacted` result goes further: it carries neither the source value nor the satisfaction outcome. @@ -99,6 +118,25 @@ The standard result body reports no `satisfied` value, and the CCCEV JSON-LD ren `predicate` still discloses a true-or-false fact; `redacted` does not. +## Preserve negative, unknown, and unavailable evidence + +Claim authors must keep these source meanings distinct: + +- `true` means the reviewed source contract positively establishes the evidence statement. +- `false` means the reviewed source contract positively establishes its negative. +- `null` means a subject matched but the declared source value is unknown, absent, or not recorded. +- `no_match` means the exact selector did not resolve a subject. +- `ambiguous` means the selector resolved more than one admissible subject. +- A source failure means the source could not provide a reliable result. + +A claim rule must not silently coerce `null`, `no_match`, `ambiguous`, or source failure to +`false`. Missing evidence is not a negative fact. An explicitly named existence predicate is the +narrow exception: a reviewed rule can map `matched == false` to `false` only when the claim means +that one admissible match exists. This exception does not make other claims negative, and ambiguity +or source failure never becomes `false`. A public disclosure policy can collapse matching failures +to `evidence.not_available`, but the reviewed source outcome and audit provenance remain distinct +from an explicit negative source value. + ## How disclosure policy is configured per claim The caller does not freely pick from all three modes. diff --git a/docs/site/src/content/docs/explanation/integration-patterns.mdx b/docs/site/src/content/docs/explanation/integration-patterns.mdx index 0a8d46d3b..3c2d10613 100644 --- a/docs/site/src/content/docs/explanation/integration-patterns.mdx +++ b/docs/site/src/content/docs/explanation/integration-patterns.mdx @@ -64,6 +64,33 @@ such as a social-protection management information system do not become raw-reco owns the eligibility decision and reads the registry it owns directly." /> +## Evidence-to-action boundary + +Every combined integration follows the same ownership boundary. Registry Relay owns +source-specific acquisition, source-access policy, and typed normalization. Registry Notary owns +atomic evidence claims plus evidence authorization and disclosure policy. The evidence consumer +determines how the evidence is used, and the decision owner remains accountable for requirements, +decisions, workflow, and action. + +The DHIS2 health-evidence starter makes that boundary visible. DHIS2 remains the system of record. +Relay uses its product-neutral script capability to perform bounded, same-origin acquisition and +normalize the existing health outputs plus `bcg_birth_dose_recorded`, +`opv_birth_dose_recorded`, and `measles_dose_recorded`. Notary exposes those facts as reusable +evidence claims. In this example, a public-health programme is both the evidence consumer and the +decision owner. It can compute `outreach-required` or `follow-up-priority` without moving either +decision into Relay or Notary. + +The reviewed contract keeps `true`, `false`, `null`, no-match, ambiguity, and source failure +distinct. Missing evidence is not a negative fact. Offline synthetic fixtures are the deterministic +acceptance path; a governed live check against a reachable DHIS2 instance is optional compatibility +evidence. + +Registry Notary may attest a decision that DHIS2 or another authoritative source already made only +when the claim identifier and its review documentation identify it as a source-owned decision. +This exception does not make Notary a general-purpose eligibility or workflow engine. + +{/* Evidence: crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker/. */} + ## Domain registry platforms Examples: [OpenCRVS](https://www.opencrvs.org/) (civil registration), [OpenSPP](https://openspp.org/) @@ -176,7 +203,8 @@ source tables, raw SQL, or full records. Wire Registry Stack in alongside a workflow engine when: -- A workflow step needs an authoritative registry output (status, predicate, selected value). +- A workflow step needs an authoritative evidence fact (status, predicate, selected value) as an + input to the workflow engine's own decision. - A workflow step needs an evidence artifact or credential rather than an internal service result. - Generated workflow connectors must not depend on source schema details. diff --git a/docs/site/src/content/docs/explanation/records-stay-home.mdx b/docs/site/src/content/docs/explanation/records-stay-home.mdx index af056a838..9d2b7ddf7 100644 --- a/docs/site/src/content/docs/explanation/records-stay-home.mdx +++ b/docs/site/src/content/docs/explanation/records-stay-home.mdx @@ -14,9 +14,9 @@ standards_referenced: An institution that runs a civil registry, a social-protection database, or a health registry already holds the records it needs. -Registry Stack lets it **answer questions about those records** (*is this person alive? is -this household eligible?*) and return a result another system can trust, while the records -themselves are **read where they already live, never written back, and not copied into a +Registry Stack lets it **answer evidence questions about those records** (*is this person alive? +is this programme enrollment active?*) and return a result another system can trust, while the +records themselves are **read where they already live, never written back, and not copied into a central exchange**. This page explains what that means in practice: what stays inside the institution's boundary, what crosses it, and (equally important) what the design does and does not @@ -24,13 +24,15 @@ guarantee. ## A question goes in, an answer comes out -The combined Relay and Notary mental model is one sentence: **a scoped question crosses into the -institution, Relay reads the record in place, and Notary returns a computed answer.** +The combined Relay and Notary mental model is one sentence: **a scoped evidence question crosses +into the institution, Relay reads and normalizes the source, Notary returns a governed evidence +answer, an evidence consumer uses it, and the decision owner remains accountable for what happens +next.** A Notary caller never sends the value it is asking about and never receives the underlying record as the answer. -It sends the id of a *claim* (a single, pre-modelled question) and only the typed target, -requester, or variable inputs that the claim admits. +It sends the id of an *evidence claim* (an atomic, precisely defined evidence statement) and only +the typed target, requester, or variable inputs that the claim admits. It receives one of a few narrow shapes of answer: a yes/no, a single value, a machine-readable evaluation result, or a credential the subject can carry in a wallet. The source row that the answer was computed from stays behind. @@ -50,10 +52,10 @@ flowchart LR notary -. records .-> audit key -. signs .-> notary end - caller["Caller / verifier"] + caller["Caller / evidence consumer"] holder(["Subject / wallet"]) caller == "request: claim id + subject inputs + scope" ==> notary - notary == "answer: yes/no · value · evaluation result" ==> caller + notary == "evidence: yes/no · value · evaluation result" ==> caller notary == "issued credential" ==> holder caller == "scoped record read" ==> relay relay == "authorized source records" ==> caller @@ -74,6 +76,15 @@ applies disclosure policy, and issues credentials. Notary is the strongest minimization surface. Relay record reads are scoped and audited, not open data. +The policy boundary remains separate across the two products and their consumers. Relay owns +source access and adaptation policy. Notary owns evidence authorization and disclosure policy. +The evidence consumer determines how evidence is used, and the decision owner remains accountable +for requirements, eligibility, qualification, prioritization, approval, routing, payment, +workflow, and action policy. +Purpose-bound authorization can restrict an evidence request without changing which component owns +the consumer decision. The caller that invokes Notary can be the evidence consumer or a technical +intermediary acting for it. + ## What stays home - Source data is read in place: Relay reads sources as batch snapshots or table scans; @@ -101,7 +112,7 @@ governed, audited read bounded by the caller's per-dataset row scope and the dat configured filters and limits. Registry Notary returns the answer a rule computes from typed consultation outputs rather than the source row; keeping that answer narrow is a modelling discipline, because a well-modelled claim -returns one decision or one extracted value. +returns one reusable evidence statement or one extracted value. A Notary answer takes one of a few shapes: - A yes/no: only the true/false satisfaction of the modelled rule. @@ -148,9 +159,13 @@ By default, a record that does not resolve collapses to a single not-available r rather than a `false`, which hides the reason matching failed. An authorized caller can still distinguish a resolved record from a not-available response, so use this pattern for minimization, not for hiding whether a requested subject matched. -To check eligibility without exposing an income figure, derive the decision with an -expression rule and disclose the eligibility boolean as a `predicate`; the income value -stays home. +To report whether a source records a vaccination dose, derive that evidence predicate from the +declared typed source output. A public-health programme, acting as evidence consumer and decision +owner, can combine the result with other evidence and its own case state to decide whether outreach +or follow-up is required. +Registry Notary can attest an eligibility or other decision already made by an authoritative +source only when the claim identifier and its review documentation identify the fact as a +source-owned decision. Notary does not recompute that decision. ## Why the answer is not the record @@ -211,6 +226,15 @@ Security material: - Matching is only as strict as the Relay integration contract: Relay returns `match`, `no_match`, or `ambiguous` under the compiled selector and output rules. Notary does not replace that result with direct source matching or choose between ambiguous records. +- Missing evidence is not a negative fact: A matched source value of `false`, a matched `null` + value, `no_match`, `ambiguous`, and source failure have different reviewed meanings. Claim rules + must not collapse `null`, unavailable evidence, or failures to `false`. An explicitly named + existence predicate can map `matched == false` to `false` when its reviewed meaning is exactly + whether one admissible match exists; ambiguity and failure remain unavailable. +- Notary is not a consumer decision engine: Notary returns governed evidence. The evidence consumer + determines how that evidence is used, and the decision owner remains accountable for requirements, + decisions, workflow, and actions unless Notary is attesting a decision explicitly owned by the + authoritative source. - This is not zero-knowledge: A `predicate` answer is a policy-enforced boolean computed inside the service; SD-JWT selective disclosure is digest omission. Neither is a zero-knowledge proof. diff --git a/docs/site/src/content/docs/reference/glossary.mdx b/docs/site/src/content/docs/reference/glossary.mdx index 9f7b3a6e3..7a7066c8e 100644 --- a/docs/site/src/content/docs/reference/glossary.mdx +++ b/docs/site/src/content/docs/reference/glossary.mdx @@ -45,6 +45,9 @@ Product names are always in English, including on future translated pages.
CCCEV
Core Criterion and Core Evidence Vocabulary. Registry Manifest emits CCCEV-shaped requirement, evidence type, and evidence type list metadata. Registry Notary renders CCCEV-shaped JSON-LD for claim evaluation results. Media type: `application/ld+json; profile="cccev"`.
+
caller
+
The technical client that invokes Registry Relay or Registry Notary. A caller can be the evidence consumer or an intermediary, portal, wallet backend, or workflow connector acting for it.
+
claim evaluation
Registry Notary process that evaluates configured evidence rules over compiler-pinned Registry Relay outputs or permitted self-attestation and returns a structured claim result.
@@ -54,6 +57,9 @@ Product names are always in English, including on future translated pages.
consultation
A named use of one Registry Relay integration by a Registry Notary evidence service. One consultation can supply several Notary claims.
+
consumer decision
+
An eligibility, qualification, prioritization, approval, routing, payment, workflow, or other outcome determined outside Registry Relay and Registry Notary by or for an evidence consumer. The decision owner remains accountable for its rules and consequences.
+
DCAT
Data Catalog Vocabulary. W3C recommendation. Registry Relay and Registry Manifest emit DCAT-shaped JSON-LD catalogs. Spell out on first use per page: "Data Catalog Vocabulary (DCAT)".
@@ -72,6 +78,9 @@ Product names are always in English, including on future translated pages.
deployment
One activated deployment-bundle generation. Replicas of that generation are one logical deployment.
+
decision owner
+
The institution accountable for the requirements, rules, decisions, and actions that use evidence. The decision owner can operate the evidence consumer directly or rely on a separate caller or intermediary.
+
DPI
Digital Public Infrastructure. Shared, interoperable digital systems (identity, payments, data exchange) deployed at population scale to support public service delivery. These projects cover the registry-consultation slice of a DPI deployment.
@@ -99,6 +108,9 @@ Product names are always in English, including on future translated pages.
Evidence Gateway
Governed runtime path for evidence responses. A Relay read or consultation and a Notary claim evaluation pass trusted request and evidence context through configured authorization and disclosure policy before returning or denying a response. Registry-backed Notary claims consume compiler-pinned Relay results.
+
evidence consumer
+
A service or process that uses returned evidence. Examples include a social-protection programme, admissions service, licensing authority, healthcare workflow, lender, insurer, or credential verifier. The evidence consumer, caller, and decision owner can be the same component or separate components.
+
evidence service
A Registry Notary-owned service containing service policy, claims, disclosure, optional Registry Relay consultations, and optional credential profiles.
@@ -111,8 +123,8 @@ Product names are always in English, including on future translated pages.
evidence offering
Metadata entry that describes a verification capability and the access path a client uses to reach it. In the current stack it points a client from Registry Relay metadata to Registry Notary or another evidence service; Relay describes the offering but does not evaluate the claim.
-
evidence claim
-
Registry Stack product term for a verifiable claim, attestation, credential claim, or status assertion that Registry Notary evaluates from Relay outputs or permitted self-attestation.
+
Notary evidence claim
+
An atomic, precise, reusable evidence statement that Registry Notary evaluates from Relay outputs or permitted self-attestation. Notary owns the claim's evidence semantics, authorization, and disclosure policy. A claim does not import an evidence consumer's decision or action rules.
evidence type list
CCCEV list of evidence types that satisfy a requirement. In Registry Manifest, one list is one grouped option; multiple lists on the same requirement are alternatives.
@@ -135,11 +147,11 @@ Product names are always in English, including on future translated pages.
integration
A product-neutral source adaptation authored in a Registry Stack project. An integration declares reviewed metadata and one source capability such as `http`, `script`, or `snapshot`. Product and version labels do not select compiler or runtime behavior.
-
output
-
A typed, minimized scalar returned by Registry Relay on a matching consultation.
+
Relay output
+
A declared typed value returned by Registry Relay after source-specific acquisition and normalization on a matching consultation. A Relay output carries source evidence, not a consumer consequence. `true`, `false`, `null`, `no_match`, `ambiguous`, and source failure retain distinct meanings under the reviewed integration contract.
claim
-
A Registry Notary-owned, purpose-specific value or predicate derived from consultation outcome and outputs.
+
Short form for a Notary evidence claim: a Registry Notary-owned evidence value or predicate derived from a consultation outcome and typed Relay outputs, or from permitted self-attestation. Purpose-bound authorization limits use of the claim without turning the claim into consumer policy.
JSON-LD
JSON-based linked data format. W3C recommendation (JSON-LD 1.1). Used for catalog, policy, and CCCEV render output.
@@ -240,6 +252,9 @@ Product names are always in English, including on future translated pages.
source
The one logical registry data interface available to Registry Relay, when present, inside a project trust domain. An OAuth or JSON Web Key Set endpoint used by a protocol is not another data source.
+
source-owned decision
+
A decision already made and owned by an authoritative source system. Registry Notary may attest this fact only when the claim identifier and its review documentation identify it as source-owned; Notary does not recompute the decision or claim ownership of its policy.
+
`registry-manifest/v1`
Schema version string for portable metadata manifests.
diff --git a/docs/site/src/content/docs/reference/registryctl.mdx b/docs/site/src/content/docs/reference/registryctl.mdx index 649155827..c8eab547c 100644 --- a/docs/site/src/content/docs/reference/registryctl.mdx +++ b/docs/site/src/content/docs/reference/registryctl.mdx @@ -73,6 +73,7 @@ formatting. Artifact and protocol streams retain their native formats, including | Invalid-project `check` | `registryctl.project_diagnostics.v1` | | `authoring editor` | `registryctl.project_editor.v1` | | `doctor` | `registryctl.validation.report.v1` | +| `smoke` | `registryctl.smoke.v1` | | `bundle` or `anchor` | The operation-specific `schema_version` in the report | An initialized starter records its starter ID, Registry Stack release, and @@ -86,7 +87,9 @@ Without `--against`, `check` and `build` label the baseline Their `semantic_changes` entries identify changed `claim`, `integration`, `service_policy`, `operator_security`, and `disclosure` dimensions. The authorized bundle signer reviews that report; Registry Stack does not create a separate reviewer workflow. -Generated files are written under `.registry-stack/build//` only by `build`. +The `registryctl.project_command.v1` build report identifies the complete generated build root in +its `output` field. Automation must use that field instead of constructing a path from registryctl's +private generated directory layout. For invalid authoring, `check` exits nonzero and reports a nonempty typed diagnostic list. The default human output and `--format json` use the same diagnostics, stable codes, normalized @@ -135,6 +138,9 @@ The command copies the five JSON Schema Draft 2020-12 documents embedded in the `registryctl`. It records the binary version and schema SHA-256 values in `.registry-stack-editor/manifest.json`, so the project does not depend on a source checkout, a mutable branch, or network schema retrieval. +For automation, `authoring editor --format json` returns the project root in `project_directory` +and every managed file in `files`. Use those `registryctl.project_editor.v1` fields instead of +inferring additional editor paths. | Authored file | Project-local schema | | --- | --- | @@ -280,7 +286,7 @@ default and accept `--format json` for automation. ## Project setup -The legacy `init relay` command creates a local spreadsheet project. +The canonical `init relay` command creates a local Relay-backed spreadsheet API tutorial project. In registryctl `v0.12.2`, this generation command requires the strict `registryctl-vX.Y.Z-image-lock.json` from the same release beside the running binary. Set `REGISTRYCTL_IMAGE_LOCK` only when an operator has verified the same @@ -297,7 +303,9 @@ not need the lock. Registryctl `v0.8.4` predates the image-lock contract. The human-readable result identifies the generated Bruno collection and gives the validation and start commands. The JSON result contains no progress text, so another program can parse standard -output directly. +output directly. Automation must read the project root from `output` and supported generated entry +points from `artifacts` in the `registryctl.init.v1` report. The remaining generated layout is a +private implementation detail. {/* Evidence: crates/registryctl/src/main.rs and crates/registryctl/tests/init_output.rs. */} @@ -350,7 +358,9 @@ Inspect commands report on a running or generated project. ## Testing -Smoke commands run built-in local checks and write a JSON result file. +Smoke commands run built-in local checks and write a versioned JSON result file. The +`registryctl.smoke.v1` report is saved as `smoke-results.json` under the authored +`local.output_dir`. Smoke-check ordering and human-readable names are not compatibility contracts. | Subcommand | Purpose | | --- | --- | diff --git a/docs/site/src/content/docs/spec/rs-pr-registryctl.mdx b/docs/site/src/content/docs/spec/rs-pr-registryctl.mdx new file mode 100644 index 000000000..ae0268e70 --- /dev/null +++ b/docs/site/src/content/docs/spec/rs-pr-registryctl.mdx @@ -0,0 +1,243 @@ +--- +title: "RS-PR-REGISTRYCTL: registryctl compatibility contract" +description: "The normative 1.0 contract for registryctl project authoring, machine-readable reports, generated-output discovery, and exit statuses." +status: draft +owner: registry-docs +source_repos: + - registry-stack +last_reviewed: "2026-07-20" +doc_type: specification +doc_id: RS-PR-REGISTRYCTL +category: normative +evidence: verified +locale: en +standards_referenced: [] +layer: + - operations +audience: + - integrator + - maintainer + - tooling +--- + +This document defines the Registry Stack 1.0 compatibility contract for `registryctl`: the +authored project files that adopters maintain, the machine-readable reports that automation +consumes, how automation discovers generated outputs, and the process exit statuses. The +implementation meets these requirements, but the document remains `draft`; the compatibility +promise takes effect with Registry Stack `v1.0.0`, not with a pre-1.0 release. + +## Version history + +| Version | Date | Status | Change | +| --- | --- | --- | --- | +| 0.1.0 | 2026-07-20 | draft | Initial registryctl compatibility contract. | + +## 1. Scope and conventions + +The key words in this document are interpreted per [RS-DOC](../rs-doc/) Section 2. +This specification refines the command-line compatibility promise in the +[API stability and versioning reference](../../reference/api-stability/). + +This specification covers: + +- Authored Registry Stack project files rooted at `registry-stack.yaml`. +- Versioned JSON reports intended for automation. +- Report fields that identify supported generated outputs. +- Success, command-failure, and usage-failure exit statuses. +- The credential-issuance boundary enforced by project compilation. + +This specification does not stabilize human-readable output, private generated directory +internals, hidden commands, or implementation-owned worker protocols. + +REQ-PR-REGISTRYCTL-001: A released `registryctl` MUST identify every machine-readable report with +a `schema_version`. Within a major release, a report schema MUST remain backward compatible. + +## 2. Authored project contract + +A Registry Stack project starts at `registry-stack.yaml`. The root document references authored +environment, integration, fixture, and entity files. The strict schemas embedded in `registryctl` +define these files, while project-local editor setup makes the same schemas available to common +editors. The implementation and its path checks are in the +[project loader](https://github.com/registrystack/registry-stack/blob/1a9c33d31f4d4db523da0d36011358200696e16f/crates/registryctl/src/project_authoring/project.rs) +and [project diagnostics](https://github.com/registrystack/registry-stack/blob/1a9c33d31f4d4db523da0d36011358200696e16f/crates/registryctl/src/project_authoring/diagnostics.rs). + +REQ-PR-REGISTRYCTL-002: `registry-stack.yaml` and every authored file it references MUST be treated +as adopter-owned input. `registryctl` MUST NOT overwrite an existing nonempty project destination +during initialization. + +REQ-PR-REGISTRYCTL-003: The project, environment, integration, fixture, and entity schemas MUST +reject unknown fields. A minor or patch release MUST NOT remove a field, change its meaning, or +narrow its accepted values after `v1.0.0`. + +REQ-PR-REGISTRYCTL-004: Authored paths MUST remain inside the project root. Project loading MUST +reject path traversal, symbolic links, and unsupported file types before compilation or source +access. + +REQ-PR-REGISTRYCTL-005: `registryctl init --from` MUST validate a starter and its editor setup in +private staging before publishing the complete project. If validation or publication fails, the +command MUST leave an absent destination absent and a pre-existing empty destination empty. +The [initialization implementation](https://github.com/registrystack/registry-stack/blob/1a9c33d31f4d4db523da0d36011358200696e16f/crates/registryctl/src/project_authoring/commands.rs) +and its staging tests verify this behavior. + +## 3. Generated-output discovery + +`registryctl` generates product inputs only after the authored project and its offline fixtures +pass validation. Automation discovers supported output entry points from versioned reports instead +of constructing paths from a private directory layout. The current compiler publication boundary +is implemented in +[`output.rs`](https://github.com/registrystack/registry-stack/blob/1a9c33d31f4d4db523da0d36011358200696e16f/crates/registryctl/src/project_authoring/output.rs). + +REQ-PR-REGISTRYCTL-006: The `registryctl.init.v1` report MUST identify the project root in `output` +and supported generated entry points in `artifacts`. A consumer MUST use these fields instead of +assuming a generated filename. The `registryctl.project_editor.v1` report MUST identify the project +root in `project_directory` and every managed editor file in `files`. + +REQ-PR-REGISTRYCTL-007: A successful `build --format json` MUST return a +`registryctl.project_command.v1` report whose `output` field identifies the complete build root. +The exact files and directories below that root are implementation details unless another public +contract names them. + +REQ-PR-REGISTRYCTL-008: For identical authored inputs, environment, compiler version, and verified +baseline, `build` MUST produce the same file closure. Publication MUST replace the selected +environment output as one complete unit so product processes do not observe a partial closure. + +REQ-PR-REGISTRYCTL-009: Generated review material and private product inputs MUST remain separate. +The implementation MAY change their directory names, but MUST NOT place secrets or private source +bindings in the reviewable output. + +## 4. Machine-readable reports + +Commands that accept `--format json` emit one JSON document on standard output. Human progress +text does not share that stream. The command dispatch and JSON rendering are defined in the +[registryctl entry point](https://github.com/registrystack/registry-stack/blob/1a9c33d31f4d4db523da0d36011358200696e16f/crates/registryctl/src/main.rs). + +REQ-PR-REGISTRYCTL-010: A command in JSON format MUST write only its report to standard output. +Warnings that are not part of the report MAY use standard error, but MUST NOT change the JSON +document or a successful command's exit status. + +### 4.1 Doctor report + +`doctor --format json` emits `registryctl.validation.report.v1`. Its committed +[JSON Schema](https://github.com/registrystack/registry-stack/blob/1a9c33d31f4d4db523da0d36011358200696e16f/crates/registry-config-report/schemas/registryctl.validation.report.v1.schema.json) +and [canonical fixture](https://github.com/registrystack/registry-stack/blob/1a9c33d31f4d4db523da0d36011358200696e16f/crates/registry-config-report/fixtures/registryctl/registryctl.validation.error.json) +are the field-level contract. + +REQ-PR-REGISTRYCTL-011: A doctor report MUST contain `schema_version`, `project`, `status`, +`products`, and `generated_at`. It MAY contain `cross_product_diagnostics`. Its aggregate and +product statuses MUST use `ok`, `warning`, `error`, or `not_run`. + +REQ-PR-REGISTRYCTL-012: Doctor aggregation MUST redact local secret values from captured product +output. A report containing only `ok` or `warning` product statuses MUST exit successfully. A +report containing `error` or `not_run` MUST be emitted before the command exits with status `1`. +The [doctor tests](https://github.com/registrystack/registry-stack/blob/1a9c33d31f4d4db523da0d36011358200696e16f/crates/registryctl/src/lib.rs) +cover schema validation, redaction, and failure reporting. + +### 4.2 Smoke report + +`registryctl smoke` writes a `registryctl.smoke.v1` document to the configured local output +directory as `smoke-results.json`. The committed schema at +`crates/registryctl/schemas/registryctl.smoke.v1.schema.json` is the field-level contract. + +REQ-PR-REGISTRYCTL-013: A smoke report MUST contain `schema_version`, `base_url`, `passed`, and +`checks`. Each check MUST contain `name`, `method`, `path`, `expected_status`, `actual_status`, +`passed`, and `error`. + +REQ-PR-REGISTRYCTL-014: `registryctl smoke` MUST write the complete report when a check fails, then +exit with status `1`. The report MUST NOT contain local API keys or other values loaded from the +project secrets file. A report in which every check passes MUST set `passed` to `true` and exit +with status `0`. + +REQ-PR-REGISTRYCTL-015: Within the 1.x release line, a report MUST continue to satisfy the committed +schema identified by its `schema_version`. A release MUST NOT remove or rename a field, change a +field's type or meaning, make an optional field required, add a field where the schema closes +additional properties, or reuse a schema version for a different shape. + +## 5. Exit statuses + +Registryctl has three process exit statuses. Tests in `crates/registryctl/tests/exit_status.rs` +pin the status assigned to each class. + +| Status | Meaning | +| --- | --- | +| `0` | The requested command completed successfully. | +| `1` | A parsed command failed during validation, execution, or runtime interaction. | +| `2` | Command-line parsing failed because the command, subcommand, flag, or value was invalid or missing. | + +REQ-PR-REGISTRYCTL-016: Registryctl MUST return exactly `0`, `1`, or `2` according to this table. +A command MUST NOT return `0` when its machine-readable report has a failure status. + +The former hidden `init spreadsheet-api` alias is not part of the command surface. Invoking the +removed alias is a usage failure and returns status `2`. + +## 6. Explicitly unstable surfaces + +The following surfaces remain outside the 1.0 compatibility promise: + +- The generated layout of the local `init relay` Compose tutorial, including `registryctl.yaml`, + `compose.yaml`, and its data, state, secret, and product subdirectories. +- Directory names and intermediate files below a build root returned by + `registryctl.project_command.v1`. +- Editor staging, backup, and transaction artifacts that are not returned by a versioned report. +- Human-readable output text, whitespace, ordering, and terminal formatting. +- Hidden commands, worker processes, and their standard-input or standard-output protocols. +- Smoke check ordering and human-readable check names or error text. + +REQ-PR-REGISTRYCTL-017: Registryctl documentation MUST NOT present an unstable generated path as a +1.0 compatibility guarantee. Automation MUST use a versioned report, a committed schema, or a +separate public product contract to discover an output it consumes. + +## 7. Credential-issuance boundary + +Project authoring can compile evaluation-only Notary services without a credential profile. When +a project declares credential issuance, every selected claim must derive from an exact +compiler-pinned Registry Relay consultation. The compiler and end-to-end tests enforce this in +[`compiler/notary.rs`](https://github.com/registrystack/registry-stack/blob/1a9c33d31f4d4db523da0d36011358200696e16f/crates/registryctl/src/project_authoring/compiler/notary.rs) +and [`project_authoring.rs`](https://github.com/registrystack/registry-stack/blob/1a9c33d31f4d4db523da0d36011358200696e16f/crates/registryctl/tests/project_authoring.rs). + +REQ-PR-REGISTRYCTL-018: Registryctl MUST NOT compile a credential profile or OpenID for Verifiable +Credential Issuance (OID4VCI) configuration that selects a source-free claim. Source-free claims +MAY be used only for evaluation when no credential profile selects them. Credential issuance MUST +remain registry-backed. + +## Conformance + +A registryctl release conforms to this specification when it: + +- Preserves the authored project contract and validates it before compilation + (REQ-PR-REGISTRYCTL-002 through REQ-PR-REGISTRYCTL-005). +- Publishes deterministic complete outputs and exposes supported entry points through versioned + reports without stabilizing private internals (REQ-PR-REGISTRYCTL-006 through + REQ-PR-REGISTRYCTL-009). +- Emits compatible doctor and smoke reports without secret values (REQ-PR-REGISTRYCTL-010 through + REQ-PR-REGISTRYCTL-015). +- Returns the exact process status for success, command failure, and usage failure + (REQ-PR-REGISTRYCTL-016). +- Keeps unstable generated internals outside the compatibility promise + (REQ-PR-REGISTRYCTL-017). +- Refuses credential issuance from source-free claims (REQ-PR-REGISTRYCTL-018). + +## Evidence + +This specification is `verified`: the requirements describe implemented behavior with automated +tests. + +- `crates/registryctl/tests/init_output.rs` pins versioned initialization reports and supported + artifact entry points. +- `crates/registryctl/tests/project_authoring.rs` and inline project-authoring tests pin authored + path checks, staged initialization, deterministic generation, complete publication, and the + registry-backed issuance boundary. +- `crates/registry-config-report/tests/report_contract.rs` validates the doctor schema and its + canonical fixtures. +- Inline registryctl tests validate doctor aggregation, redaction, the smoke schema, failure-report + persistence, and secret absence. +- `crates/registryctl/tests/exit_status.rs` pins process statuses `0`, `1`, and `2`, including the + removed alias. + +## Next + +- [registryctl CLI reference](../../reference/registryctl/) lists the supported commands and flags. +- [API stability and versioning](../../reference/api-stability/) defines the stack-wide 1.0 + compatibility policy. +- [RS-PR-RELAY](../rs-pr-relay/) specifies the Registry Relay protocol compiled project inputs + configure. +- [RS-PR-NOTARY](../rs-pr-notary/) specifies the Registry Notary evaluation and issuance protocol. diff --git a/docs/site/src/content/docs/start/when-to-use.mdx b/docs/site/src/content/docs/start/when-to-use.mdx index b0b1b3330..310eb3202 100644 --- a/docs/site/src/content/docs/start/when-to-use.mdx +++ b/docs/site/src/content/docs/start/when-to-use.mdx @@ -39,8 +39,10 @@ These docs cover the two products you build against directly: - Registry Relay exposes existing source data (a file, extract, database, or legacy system) through protected, scoped, read-only HTTP routes, without giving callers database access. -- Registry Notary evaluates configured claims and returns a status, predicate, selected value, or - signed credential, instead of the full source record. +- Registry Notary evaluates configured evidence claims and returns a status, predicate, selected + value, or signed credential, instead of the full source record. An evidence consumer uses that + evidence, while the decision owner remains accountable for any resulting requirements, + decisions, or actions. {/* SVG diagram. Essential labels are restated in the bullets above and the sentence below. */}
@@ -92,6 +94,11 @@ Registry Stack deliberately leaves these to other systems: - It does not solve foundational identity, deduplication, legal authority, semantic governance, or institutional approval by itself. It makes those boundaries explicit so they can be reviewed before data moves. +- Registry Notary is not an eligibility, prioritization, workflow, or action engine. It evaluates + atomic, reusable evidence statements under evidence authorization and disclosure policy. The + evidence consumer determines how the evidence is used, and the decision owner retains + accountability for its rules and outcomes. Notary can attest a decision already made by an + authoritative source only when the claim identifies the decision as source-owned. - Registry Relay does not mutate source registry data: provisioning, inserts, updates, and write-back are out of scope for its consultation API. - Registry Relay is not an open-data portal: it publishes restricted consultation APIs for diff --git a/docs/site/src/content/docs/tutorials/author-registry-project.mdx b/docs/site/src/content/docs/tutorials/author-registry-project.mdx index 1a9d0a870..f37e86262 100644 --- a/docs/site/src/content/docs/tutorials/author-registry-project.mdx +++ b/docs/site/src/content/docs/tutorials/author-registry-project.mdx @@ -208,6 +208,34 @@ An attribute is caller-supplied context. Mapping an attribute does not authentic turn the value into an identifier. Registryctl rejects any other target path before generating Relay or Notary configuration. +## Apply the evidence-claim design test + +Keep three policy categories separate while you author the project. The integration and +environment define Relay source access and adaptation policy. The evidence service defines Notary +evidence authorization and disclosure policy. The evidence consumer and decision owner keep their +use, decision, workflow, and action policy outside the project. + +Test every proposed claim before adding it: + +1. Does the claim state one atomic evidence fact or value, rather than a consumer decision? +2. Does the claim define what `true`, `false`, `null`, `no_match`, `ambiguous`, and source failure + mean without turning missing evidence into a negative fact? +3. Can another authorized service or procedure reuse the evidence without importing the first + consumer's eligibility, qualification, ranking, workflow, or action rules? + +If any answer is no, narrow or rename the claim and keep the consuming decision downstream. +For example, prefer `programme-enrollment-active` or `measles-dose-recorded` to +`outreach-required` or `benefits-eligible`. A Common Expression Language (CEL) rule may derive a +precise evidence predicate from typed outputs, but it is not a general-purpose eligibility engine. +Across other sectors, prefer evidence such as `qualification-awarded`, +`business-registration-active`, `coverage-active`, `inspection-completed`, or +`professional-credential-valid` to outcomes such as `admission-approved`, `licence-granted`, +`claim-payable`, `inspection-priority`, or `access-permitted`. + +A claim may attest a decision that an authoritative source already made. In that exception, the +claim id and its review evidence must identify it as a source-owned decision, and the integration +must preserve the source's exact meaning. Notary does not recompute the decision. + ## Add another integration An existing project can add another protocol view over the same logical source without starting a @@ -221,7 +249,8 @@ second project or copying generated product configuration. Use a stable integrat `registry-stack.yaml`. 3. Under the consuming evidence service, add a consultation with `integration: benefit-record` and bind every integration input to one typed request path. Add - claims that select or derive from that consultation's declared outputs. + atomic evidence claims that select or derive from that consultation's declared outputs. Keep + consumer decisions outside the evidence service. 4. Add `integrations.benefit-record.source` to `environments/local.yaml` with its non-production origin, credential reference, and generation. Do not put credential values in the project files. diff --git a/docs/site/src/content/docs/tutorials/configure-project-script-adapter.mdx b/docs/site/src/content/docs/tutorials/configure-project-script-adapter.mdx index b2e83e009..dd3966276 100644 --- a/docs/site/src/content/docs/tutorials/configure-project-script-adapter.mdx +++ b/docs/site/src/content/docs/tutorials/configure-project-script-adapter.mdx @@ -29,6 +29,34 @@ evidence only; any reviewed source system can use the same script capability and +## Keep DHIS2 evidence separate from consumer action + +The bundled health-evidence journey follows one responsibility flow: + +1. DHIS2 remains the system of record. +2. Registry Relay performs bounded, same-origin DHIS2 acquisition and preserves the starter's + declared typed health outputs. The child-program and dose outputs use nullable booleans so an + unknown source value does not become a negative fact. +3. Registry Notary exposes those outputs as atomic evidence claims under evidence authorization + and disclosure policy. +4. In this example, a public-health programme is both the evidence consumer and decision owner. It + combines the evidence with its own rules and case state to decide whether outreach, follow-up, + or another action is required. + +Do not add claims such as `outreach-required` or `follow-up-priority` to the evidence service. +Those names describe consumer decisions. A source-owned decision is the exception: a +claim can attest it only when the claim id and its review evidence identify the decision as +source-owned. + +The adapter and fixtures must preserve source meaning. For a matched subject, a completed DHIS2 +programme-stage event maps to `true`, an existing non-completed event maps to `false`, and an +absent enrollment or stage maps to `null`. A `404` maps to `no_match`, not to negative health +evidence. Source rejection and subject mismatch are failures and produce no claims. Ambiguity is +explicitly not applicable to this singleton DHIS2 resource; another adapter must prove ambiguity +or handle it separately. Missing evidence is not a negative fact. + +{/* Evidence: crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker/. */} + ## Declare source authority The public integration declares input schemas, authentication interface, allowed source paths, @@ -194,6 +222,10 @@ registryctl check --project-dir --environment --explain Offline tests are portable and use no real credentials or network. Production activation remains Linux-only while the process controls are code-owned. Fixture success proves the adapter against synthetic observations, not production source interoperability or deployment approval. +Keep the offline fixtures as the deterministic acceptance path. A live DHIS2 compatibility check +is optional and requires a reachable instance, dedicated credentials, reviewed source authority, +and separate evidence recording. Public demo availability does not determine whether the project +passes its offline acceptance checks. ## Troubleshooting diff --git a/docs/site/src/content/docs/tutorials/configure-project-snapshot-materialization.mdx b/docs/site/src/content/docs/tutorials/configure-project-snapshot-materialization.mdx index 7e435c05a..4d3c76cf6 100644 --- a/docs/site/src/content/docs/tutorials/configure-project-snapshot-materialization.mdx +++ b/docs/site/src/content/docs/tutorials/configure-project-snapshot-materialization.mdx @@ -64,11 +64,11 @@ primary_key: person_id schema: type: object additionalProperties: false - required: [person_id, registration_status, eligible] + required: [person_id, registration_status, residency_confirmed] properties: person_id: { type: string, maxLength: 64 } registration_status: { type: [string, "null"], maxLength: 32 } - eligible: { type: [boolean, "null"] } + residency_confirmed: { type: [boolean, "null"] } materialization: max_records: 1000000 max_bytes: 256MiB @@ -100,7 +100,7 @@ services: metadata: people:metadata rows: people:rows purposes: [case-management] - projection: [person_id, registration_status, eligible] + projection: [person_id, registration_status, residency_confirmed] pagination: { default_limit: 50, max_limit: 100 } filters: person_id: [eq] @@ -133,7 +133,7 @@ capability: exact: person_id: { input: person_id } freshness: 24h -outputs: [registration_status, eligible] +outputs: [registration_status, residency_confirmed] ``` The `snapshot` capability applies the complete exact selector to one immutable published handle and probes at @@ -144,6 +144,16 @@ Add an evidence service and consultation in `registry-stack.yaml`, mapping `pers declared request identifier. This Notary-owned service defines its service policy, claims, disclosure, and optional credential profiles. Claims can select only the integration's closed outputs through their authored `output` field. +Name those claims for reusable evidence, such as `population-registration-status` and +`residency-confirmed`, rather than for a consumer decision such as `benefits-eligible`. The +evidence consumer determines how the evidence is used, and the decision owner retains +accountability for eligibility and action rules. + +Preserve the distinction between an explicit `false`, a matched `null`, `no_match`, `ambiguous`, +and an unavailable or stale materialization. Missing evidence is not a negative fact, so a claim +must not turn those non-values or failures into `false`. An explicitly named record-exists claim +can map `matched == false` to `false`; the residency evidence remains `null`, and ambiguity or +source failure remains unavailable. ## Bind the physical source once @@ -159,7 +169,7 @@ entities: columns: person_id: subject_key registration_status: status_code - eligible: benefit_eligible + residency_confirmed: residency_flag source_revision: population-export-v1 generation: 2026-07-12 ``` diff --git a/docs/site/src/data/generated/project-authoring-journeys.json b/docs/site/src/data/generated/project-authoring-journeys.json index a9ec48403..04f100bbc 100644 --- a/docs/site/src/data/generated/project-authoring-journeys.json +++ b/docs/site/src/data/generated/project-authoring-journeys.json @@ -45,10 +45,10 @@ "build" ], "integration": "eligibility", - "fixture": "eligible-household", + "fixture": "source-approved-household", "commands": [ - "registryctl test --project-dir crates/registryctl/tests/fixtures/project-authoring/custom-system --integration eligibility --fixture eligible-household --trace", - "registryctl test --project-dir crates/registryctl/tests/fixtures/project-authoring/custom-system --integration eligibility --fixture eligible-household --watch", + "registryctl test --project-dir crates/registryctl/tests/fixtures/project-authoring/custom-system --integration eligibility --fixture source-approved-household --trace", + "registryctl test --project-dir crates/registryctl/tests/fixtures/project-authoring/custom-system --integration eligibility --fixture source-approved-household --watch", "registryctl test --project-dir crates/registryctl/tests/fixtures/project-authoring/custom-system", "registryctl check --project-dir crates/registryctl/tests/fixtures/project-authoring/custom-system --environment local --explain", "registryctl build --project-dir crates/registryctl/tests/fixtures/project-authoring/custom-system --environment local" @@ -76,7 +76,7 @@ { "id": "dhis2-tracker", "label": "DHIS2 Tracker", - "summary": "A product-neutral script adapter applied to a bounded DHIS2 Tracker read journey.", + "summary": "Bounded DHIS2 Tracker acquisition normalized into reusable health evidence for consumer-owned decisions.", "source": "crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker", "classification": "maintained", "topology": "combined", @@ -92,12 +92,12 @@ "build" ], "integration": "health-record", - "fixture": "complete-health-match", + "fixture": "complete-child-health-evidence", "commands": [ "registryctl init --from dhis2-tracker --project-dir dhis2-project", "registryctl authoring editor --project-dir dhis2-project", - "registryctl test --project-dir dhis2-project --integration health-record --fixture complete-health-match --trace", - "registryctl test --project-dir dhis2-project --integration health-record --fixture complete-health-match --watch", + "registryctl test --project-dir dhis2-project --integration health-record --fixture complete-child-health-evidence --trace", + "registryctl test --project-dir dhis2-project --integration health-record --fixture complete-child-health-evidence --watch", "registryctl test --project-dir dhis2-project", "registryctl check --project-dir dhis2-project --environment local --explain", "registryctl build --project-dir dhis2-project --environment local" diff --git a/docs/site/src/data/generated/project-starters.json b/docs/site/src/data/generated/project-starters.json index 3d778655d..b9730c018 100644 --- a/docs/site/src/data/generated/project-starters.json +++ b/docs/site/src/data/generated/project-starters.json @@ -32,7 +32,7 @@ { "id": "dhis2-tracker", "label": "DHIS2 Tracker", - "summary": "A product-neutral script adapter applied to a bounded DHIS2 Tracker read journey.", + "summary": "Bounded DHIS2 Tracker acquisition normalized into reusable health evidence for consumer-owned decisions.", "source": "crates/registryctl/tests/fixtures/project-authoring/dhis2-tracker", "classification": "maintained", "topology": "combined", @@ -48,12 +48,12 @@ "build" ], "integration": "health-record", - "fixture": "complete-health-match", + "fixture": "complete-child-health-evidence", "commands": [ "registryctl init --from dhis2-tracker --project-dir dhis2-project", "registryctl authoring editor --project-dir dhis2-project", - "registryctl test --project-dir dhis2-project --integration health-record --fixture complete-health-match --trace", - "registryctl test --project-dir dhis2-project --integration health-record --fixture complete-health-match --watch", + "registryctl test --project-dir dhis2-project --integration health-record --fixture complete-child-health-evidence --trace", + "registryctl test --project-dir dhis2-project --integration health-record --fixture complete-child-health-evidence --watch", "registryctl test --project-dir dhis2-project", "registryctl check --project-dir dhis2-project --environment local --explain", "registryctl build --project-dir dhis2-project --environment local" diff --git a/products/notary/README.md b/products/notary/README.md index 243c8a17b..1bb7d8f2c 100644 --- a/products/notary/README.md +++ b/products/notary/README.md @@ -24,6 +24,13 @@ service policy, claim evaluation, disclosure, credential issuance, and its own audit chain. Relay keeps independent authority over source acquisition, normalization, protocol verification, typed outputs, and its audit chain. +The evidence consumer determines how returned evidence is used. The decision +owner remains accountable for requirements, eligibility, qualification, +prioritization, approval, referral, payment, workflow, and action policy. +Notary may attest a decision that an authoritative source already made when +the claim is named and documented as source-owned, but Notary does not +recompute consumer policy. + See [`docs/README.md`](docs/README.md) for product documentation. Use Registry Stack project authoring and `registryctl` to generate deployable Relay and Notary inputs. Do not hand-author source access inside Notary. diff --git a/products/notary/docs/README.md b/products/notary/docs/README.md index 3c8eaa989..05420a4d6 100644 --- a/products/notary/docs/README.md +++ b/products/notary/docs/README.md @@ -43,5 +43,7 @@ every selected claim. It never connects directly to a registry source. Registry source adaptation belongs to Relay and Registry Stack project authoring. Rhai is Relay's reviewed `script` capability and CEL is Notary's -claim policy language. Product and version metadata never selects either -runtime. +evidence-claim policy language. Product and version metadata never selects +either runtime. The evidence consumer determines how returned evidence is +used, and the decision owner remains accountable for requirements, decisions, +workflow, and actions. CEL is not a general-purpose consumer decision engine. diff --git a/products/notary/docs/architecture-overview.md b/products/notary/docs/architecture-overview.md index 03797638c..93710ca87 100644 --- a/products/notary/docs/architecture-overview.md +++ b/products/notary/docs/architecture-overview.md @@ -28,18 +28,21 @@ verifies their full semantic contract before serving. before source access. 4. Relay returns `match`, `no_match`, or `ambiguous`, plus typed minimized outputs only on `match` and closed acquisition provenance. -5. Notary derives direct and CEL claims, applies disclosure, and either returns - the result or issues an allowed credential. -6. Each product writes its own redacted audit record. The evaluation id and +5. Notary derives direct and CEL evidence claims, applies disclosure, and + either returns the result or issues an allowed credential. +6. The evidence consumer determines how the returned evidence is used. The + decision owner applies any requirement, decision, workflow, or action + policy. +7. Each product writes its own redacted audit record. The evaluation id and consultation id support restricted cross-service reconciliation. ```mermaid flowchart LR - Caller[Caller] --> Notary[Registry Notary
policy, claims, disclosure, issuance] + Consumer[Evidence consumer
use, decision input, action] --> Notary[Registry Notary
evidence, disclosure, issuance] Notary --> Relay[Registry Relay
acquisition, normalization, outputs] Relay --> Source[(Registry source)] Relay --> Notary - Notary --> Caller + Notary --> Consumer ``` Notary never treats a Relay failure as `no_match`. Ambiguity, denial, source, @@ -50,14 +53,21 @@ consultation group. Raw Relay errors are not exposed as claim values. `self_attested` evidence performs no Relay or source I/O. Its rules and dependency closure must remain source-free. Delegated authorization is a -separate Notary policy decision. Where a delegated relationship is proved by -Relay, Notary still pins the exact consultation and performs all authorization -checks before invoking it. +separate Notary authorization decision, not a consumer decision. Where a +delegated relationship is proved by Relay, Notary still pins the exact +consultation and performs all authorization checks before invoking it. ## Product boundaries Relay owns source authentication, network policy, HTTP and protocol helpers, Rhai source adaptation, typed outputs, and acquisition provenance. Notary owns -caller authentication, purpose and legal basis, consent policy, claims, CEL, -disclosure, credential profiles, signing, and issuance. Their state and audit -authority remain separate. +caller authentication, purpose and legal basis, consent policy, reusable +evidence claims, CEL evidence derivation, disclosure, credential profiles, +signing, and issuance. The evidence consumer determines how returned evidence is +used, and the decision owner remains accountable for requirements, eligibility, +qualification, prioritization, approval, referral, payment, workflow, and +action policy. Their state and audit authority remain separate. + +Notary may attest a decision that an authoritative source has already made. +That claim is a source-owned decision and must be named and documented as such; +Notary does not recompute it as consumer policy. diff --git a/products/notary/docs/client-sdk-guide.md b/products/notary/docs/client-sdk-guide.md index 21726635d..d68c5749d 100644 --- a/products/notary/docs/client-sdk-guide.md +++ b/products/notary/docs/client-sdk-guide.md @@ -484,6 +484,10 @@ Use the canonical request shape when the high-level helper hides fields your workflow cares about. The Python and Node raw helpers preserve canonical snake_case JSON. +In these examples, `benefits_eligibility` identifies the authorized purpose +for evidence access and audit. It does not define the evidence consumer's +eligibility rule or transfer decision ownership to Notary. + ```python result = client.evaluate_request( { diff --git a/products/notary/docs/notary-capability-matrix.md b/products/notary/docs/notary-capability-matrix.md index b150d0ed5..6a5938705 100644 --- a/products/notary/docs/notary-capability-matrix.md +++ b/products/notary/docs/notary-capability-matrix.md @@ -29,9 +29,9 @@ scenario patterns](notary-scenario-patterns.md). | --- | --- | --- | | Citizen or resident | Share only the proof needed to access a service | Parent applying for child support, farmer applying for a voucher | | Case worker | Make an evidence-backed decision without seeing unnecessary registry data | Benefits officer, enrollment officer | -| Program administrator | Define eligibility policy, evidence requirements, and acceptable issuers | Social protection ministry, agriculture program team | +| Service or procedure owner | Define requirements, decision policy, and acceptable issuers | Social protection ministry, university admissions office, licensing authority | | Registry steward | Protect source registry data while answering authorized evidence questions | Civil registry, farmer registry, health facility registry | -| Auditor or oversight body | Verify decisions and data exchanges were lawful, minimized, and replay-protected | Internal audit, data protection authority | +| Auditor or oversight body | Verify evidence evaluations and data exchanges were lawful, minimized, and replay-protected | Internal audit, data protection authority | | Wallet or client app operator | Help users present proofs or receive credentials | Mobile wallet, service portal, case-management app | ## Systems @@ -40,39 +40,45 @@ scenario patterns](notary-scenario-patterns.md). | --- | --- | | Source registry | Operational system of record. It is not exposed directly to consumers | | Registry Relay | Read-only gateway and metadata publisher for source registry data | -| Registry Notary | Evaluates Relay-backed or source-free claims, signs results, issues credentials only from exact Relay-backed evaluation provenance, enforces evidence policy, and emits audit. Source-free results cannot authorize issuance | +| Registry Notary | Evaluates reusable Relay-backed or source-free evidence claims, signs results, issues credentials only from exact Relay-backed evaluation provenance, enforces evidence policy, and emits audit. Source-free results cannot authorize issuance | | Registry Manifest | Public metadata and discovery artifact for capabilities, profiles, and evidence offerings | | Registry Platform | Shared crypto, HTTP, OIDC, SD-JWT, DID/JWK, replay, and audit primitives | -| Service portal or case system | Starts a service workflow and consumes evidence or decisions | +| Service portal or case system | Acts as an evidence consumer; the operating institution remains the decision owner | | Holder wallet or client app | Stores credentials, presents proofs, and receives issued credentials | | Trust bundle or trust registry | Signed trust metadata; not yet supported. Peer trust today is a static allowlist | | Audit store | Local audit trail for evaluations, issuance, denials, and federation exchanges | ## Scenario matrix +The supported scenarios return evidence. The evidence consumer determines how +the evidence is used, and the decision owner remains accountable for any +eligibility, qualification, entitlement, payment, referral, workflow, or +action decision. A Notary claim may attest a source-owned decision, but Notary +does not recompute that decision as consumer policy. + | # | Scenario | Pattern | Status | Main gap | | --- | --- | --- | --- | --- | | 1 | Civil alive predicate | Relay-backed evaluation | Supported | None for a compiler-pinned Relay consultation | | 2 | Age or date-of-birth evidence | Relay-backed evaluation | Supported | None for a compiler-pinned Relay consultation | -| 3 | Program enrollment active | Relay-backed evaluation | Supported | None for a compiler-pinned Relay consultation | +| 3 | Programme enrollment active | Relay-backed evaluation | Supported | None for a compiler-pinned Relay consultation | | 4 | Health facility service available | Relay-backed evaluation | Supported | None for a compiler-pinned Relay consultation | -| 5 | Agriculture voucher eligibility | Relay-backed evaluation | Supported | None for a compiler-pinned Relay consultation | -| 6 | Livestock movement permit eligibility | Relay-backed evaluation | Supported | None for a compiler-pinned Relay consultation | +| 5 | Farmer registration and landholding evidence | Relay-backed evaluation | Supported | None for a compiler-pinned Relay consultation | +| 6 | Livestock identity and movement-status evidence | Relay-backed evaluation | Supported | None for a compiler-pinned Relay consultation | | 7 | Benefits agency asks Civil Notary for alive predicate | Delegated evaluation | Planned | Inbound federation is source-free in this version, and no outbound Notary client ships | | 8 | Benefits agency asks Social Notary for active beneficiary predicate | Delegated evaluation | Planned | Inbound federation is source-free in this version, and no outbound Notary client ships | | 9 | Health-linked child support across civil, social, and health | Outbound composition | Planned | No outbound Notary client or peer-result composition runtime ships yet; you cannot compose signed peer results across authorities in one flow | | 10 | Municipality verifies residency with a national registry steward | Delegated evaluation | Planned | Inbound federation is source-free in this version, and no outbound client or demo wiring ships | | 11 | Citizen presents civil-status proof to a benefits service | User-presented proof | Planned | No proof profiles or verifier runtime ship yet; you cannot accept this user-presented civil-status proof | | 12 | Farmer presents landholding or farmer-registration proof | User-presented proof | Planned | No proof profiles or status/freshness policy ship yet; you cannot accept a user-presented landholding or farmer-registration proof | -| 13 | Health worker presents professional credential for service eligibility | User-presented proof | Planned | No proof profiles or issuer trust policy ship yet; you cannot accept a presented professional credential for this eligibility check | +| 13 | Health worker presents professional credential for workforce assignment | User-presented proof | Planned | No proof profiles or issuer trust policy ship yet; you cannot accept the presented professional evidence | | 14 | Parent or guardian requests a service for a child or dependent | Delegated self-attestation plus proof | Supported | Evaluation and rendering require the configured Relay-backed relationship proof; delegated credential issuance is intentionally unavailable in 1.0 | | 15 | Household or group representative requests a service | Representation plus proof | Planned | No collective subject model or representative authority policy ships yet; you cannot let a household or group representative request this service | | 16 | Civil Notary issues date-of-birth or alive credential | Credential issuance | Supported | Full EdDSA and ES256 source-tested pre-authorized flow exists; external wallet evidence remains candidate-only | -| 17 | Agriculture Notary issues voucher eligibility credential | Credential issuance | Supported | Full EdDSA and ES256 source-tested pre-authorized flow exists; external wallet evidence remains candidate-only | -| 18 | Shared Eligibility Notary issues combined-support credential | Credential issuance plus composition | Partial | Relay-backed credential issuance exists, but peer-result composition is missing | +| 17 | Agriculture Notary issues farmer-registration or landholding credential | Credential issuance | Supported | Full EdDSA and ES256 source-tested pre-authorized flow exists; external wallet evidence remains candidate-only | +| 18 | Shared evidence service issues combined-support evidence credential | Credential issuance plus composition | Partial | Relay-backed credential issuance exists, but peer-result composition is missing | | 19 | Consuming service helps holder obtain credential from remote Notary | Federated credential issuance | Planned | No holder-binding ceremony, nonce ownership, or relay rules ship yet; you cannot help a holder obtain a credential from a remote Notary through this service | | 20 | Replay and emergency peer/key denial | Governance | Supported | Active-active deployments require the typed Notary-owned PostgreSQL state schema | -| 21 | Auditor verifies minimized decision evidence | Governance | Partial | Signed results and audit exist, checkpoints are planned | +| 21 | Auditor verifies minimized evidence exchange | Governance | Partial | Signed results and audit exist, checkpoints are planned | | 22 | Peer audit checkpoint monitoring | Governance | Planned | No checkpoint publisher, Merkle builder, or peer monitor ships yet; you cannot independently verify peer audit checkpoints | Each Relay authority uses one Notary authority, with Notary-owned PostgreSQL diff --git a/products/notary/docs/notary-scenario-patterns.md b/products/notary/docs/notary-scenario-patterns.md index d1b35a2d3..a727ce5a7 100644 --- a/products/notary/docs/notary-scenario-patterns.md +++ b/products/notary/docs/notary-scenario-patterns.md @@ -4,23 +4,26 @@ ```mermaid sequenceDiagram - participant App as Service application + participant Consumer as Evidence consumer participant Notary as Registry Notary participant Relay as Registry Relay participant Source as Registry source - App->>Notary: Evaluate claims for purpose + Consumer->>Notary: Evaluate evidence claims for purpose Notary->>Notary: Authenticate and authorize Notary->>Relay: Execute pinned consultation Relay->>Source: Governed read Source-->>Relay: Bounded response Relay-->>Notary: Outcome, outputs, provenance Notary->>Notary: Claims, disclosure, issuance policy - Notary-->>App: Minimized result or credential + Notary-->>Consumer: Minimized evidence or credential + Consumer->>Consumer: Use evidence under applicable policy ``` -One consultation can support several direct and CEL claims. Relay returns -typed outputs, while Notary owns claim meaning and disclosure. +One consultation can support several direct and CEL evidence claims. Relay +returns typed outputs, Notary owns evidence meaning and disclosure, and the +evidence consumer determines how the evidence is used. The decision owner +remains accountable for requirements, decisions, workflow, and actions. ## Self-attested Notary-only evaluation @@ -31,11 +34,13 @@ sequenceDiagram Holder->>Notary: Source-free evidence request Notary->>Notary: Validate token and subject binding - Notary->>Notary: Evaluate self-attested CEL policy + Notary->>Notary: Evaluate allowed self-attested evidence claim Notary-->>Holder: Allowed result or credential ``` -This topology performs no Relay or registry-source call. +This topology performs no Relay or registry-source call. The identity token +authorizes subject-bound access; it does not establish consumer eligibility +or another consumer-owned outcome. ## Delegated evaluation diff --git a/products/notary/docs/self-attestation-operator-guide.md b/products/notary/docs/self-attestation-operator-guide.md index a52346be1..81f37dc36 100644 --- a/products/notary/docs/self-attestation-operator-guide.md +++ b/products/notary/docs/self-attestation-operator-guide.md @@ -57,12 +57,18 @@ caller-supplied identity context is rejected before claim evaluation. Use self-attestation when: -- A citizen portal evaluates eligibility from the citizen's own token. +- A citizen portal evaluates allowed evidence claims for the token-bound + subject before the evidence consumer applies its own eligibility or + decision policy. - A wallet flow issues a credential for the token-bound subject from a compiler-pinned Relay-backed evaluation. - The identity provider can provide a stable, reviewed subject-binding claim. - The evidence service accepts token-bound subject access for the configured purpose. +Self-attestation authorizes subject-bound access to configured evidence. The +identity token is not evidence of consumer eligibility, and Registry Notary +does not turn it into a consumer-owned outcome. + Do not use it when: - The token has no trustworthy subject identifier. @@ -327,7 +333,11 @@ Confirm that: - claim and request purposes are stable and auditable; - caller scopes, client ids, audiences, formats, disclosures, and any registry-backed credential profiles are narrowly allow-listed; and -- the evidence service does not present self-attestation as registry-verified evidence. +- the evidence service does not present self-attestation as registry-verified evidence; and +- the evidence consumer determines how evidence is used, and the decision + owner, not Registry Notary, remains accountable for eligibility, + qualification, entitlement, prioritization, approval, referral, payment, + workflow, and action decisions. Use [`source-claim-modeling-guide.md`](source-claim-modeling-guide.md) to review the evidence boundary. diff --git a/products/notary/docs/source-claim-modeling-guide.md b/products/notary/docs/source-claim-modeling-guide.md index 827984293..cbd4cd934 100644 --- a/products/notary/docs/source-claim-modeling-guide.md +++ b/products/notary/docs/source-claim-modeling-guide.md @@ -1,7 +1,7 @@ # Source and claim modeling guide -Use this guide to keep source adaptation in Relay and evidence policy in -Notary. +Use this guide to keep source adaptation in Relay, reusable evidence in +Notary, and consumer decisions in the consuming system. ## Choose the topology @@ -15,6 +15,38 @@ A Registry Stack project has one registry trust domain and one logical source available to Relay. Separate independent registries require separate projects. Do not join them inside one Notary claim. +## Keep evidence separate from consumer decisions + +This evidence-versus-decision boundary is normative for 1.0 project +authoring. Use it for every registry-backed flow: + +```text +Source system + -> Registry Relay: source-specific acquisition and typed normalization + -> Registry Notary: atomic, precise, reusable evidence statements + -> Evidence consumer: use evidence under consumer-owned policy + -> Decision owner: remain accountable for requirements and outcomes +``` + +The caller, evidence consumer, and decision owner can be the same component or +separate components. The caller is the technical client that invokes Notary. +The evidence consumer uses the returned evidence. The decision owner is the +institution accountable for the requirements, rules, decisions, and actions. + +The products and consuming roles have different responsibilities: + +| Role | Responsibility | +| --- | --- | +| Registry Relay | Source access, bounded acquisition, and source-specific adaptation | +| Registry Notary | Evidence meaning, caller authorization, disclosure, and credential issuance | +| Evidence consumer | Evidence use, presentation, routing, and workflow integration | +| Decision owner | Requirements, eligibility, qualification, prioritization, approval, referral, payment, and action | + +Registry Notary may attest a decision already made by an authoritative source. +Name and document that claim as a source-owned decision, such as +`social-registry-assessed-eligible`. Do not present it as a decision computed +by Registry Notary. + ## Model the Relay integration Author an integration with one product-neutral capability: @@ -32,24 +64,51 @@ only the operational overrides the integration needs. Authentication, destinations, private networks, trust roots, and secrets belong to the private environment binding. +Relay outputs describe the source response in a stable typed form. They do not +encode an evidence consumer's decision or action rules. + ## Model the Notary service An evidence service owns purpose, legal basis, consent policy, caller access, consultations, claims, disclosure, and credential profiles. A consultation is one named use of a Relay integration. It may feed several direct or CEL claims. -Use a direct output claim for a single Relay output. Use CEL for -purpose-specific predicates or derived values. CEL is not a source adapter and -cannot perform I/O. Credential profile membership has one authored direction: -the profile lists its claims. +Use a direct output claim for a single Relay output. Use CEL for evidence +predicates or derived evidence values. CEL is not a source adapter, cannot +perform I/O, and is not a general-purpose consumer eligibility or decision +engine. +Credential profile membership has one authored direction: the profile lists +its claims. + +A claim can be evaluated under purpose-bound authorization while retaining +evidence semantics that several services or procedures can reuse. The evidence +consumer determines how the claims are used after Notary returns them, while +the decision owner remains accountable for any resulting outcome. + +## Test each claim design + +Before accepting a claim, confirm that: + +- The claim states evidence, not an entitlement, payment, referral, outreach, + or workflow action. +- Its `true`, `false`, `null`, and unavailable cases have reviewed meanings. +- Another service or procedure can reuse the statement without importing the + first consumer's decision rules. +- A claim that reports an authoritative source's decision is named and + documented as source-owned. ## Preserve failure semantics Treat `no_match` as an explicit consultation outcome. Do not model presence as an author-declared output. Ambiguity and Relay failures abort the consultation group. A credential containing a direct output claim is not issuable on -`no_match`; a reviewed predicate may explicitly turn `matched == false` into -`false` when its profile allows that predicate. +`no_match`. Do not turn missing evidence into a negative fact merely to produce +a boolean. A boolean evidence claim can return `false` from a matched source +outcome whose declared typed outputs establish the negative fact. An explicitly +named existence predicate may also map `matched == false` to `false` when its +reviewed meaning is exactly whether one admissible match exists. This exception +does not make other claims negative, and ambiguity or failure remains +unavailable. ## Review checklist @@ -58,6 +117,7 @@ group. A credential containing a direct output claim is not issuable on - The compiler-produced semantic contract and hash are pinned exactly. - Inputs map only from the closed request grammar. - Outputs are typed, bounded, minimized, and distinct from claims. +- Claims pass the evidence-versus-decision design test. - One consultation is reused for related claims within an evaluation. - No raw Relay error becomes a claim. - Fixtures cover match, no-match, ambiguity, mismatch where applicable, and diff --git a/release/conformance/integrations/README.md b/release/conformance/integrations/README.md index 71584a0bd..5d4d99392 100644 --- a/release/conformance/integrations/README.md +++ b/release/conformance/integrations/README.md @@ -27,8 +27,14 @@ The current starter's synthetic route is not evidence that the real operation is compatible. For DHIS2, the instance owner must attest every metadata UID used by the -authored adapter. The `DEMO_*` values in the starter are examples and cannot -appear in a live evidence project. +authored adapter, including the child programme and BCG, OPV, and measles +programme-stage UIDs. The `DEMO_*` values in the starter are examples and +cannot appear in a live evidence project. + +The DHIS2 starter's offline fixtures are the deterministic acceptance path for +claim semantics and failure behavior. Record live public-demo or operator-owned +compatibility only through this release evidence flow. Public demo uptime, +credentials, and mutable records are not offline acceptance dependencies. Do not simulate either prerequisite. Do not convert fixture output, a dry run, or application-only logs into candidate evidence. diff --git a/release/conformance/integrations/profiles/dhis2-tracker-2.41.9.profile.json b/release/conformance/integrations/profiles/dhis2-tracker-2.41.9.profile.json index 9c73b6f3d..b74e88f7f 100644 --- a/release/conformance/integrations/profiles/dhis2-tracker-2.41.9.profile.json +++ b/release/conformance/integrations/profiles/dhis2-tracker-2.41.9.profile.json @@ -42,6 +42,9 @@ { "env": "DHIS2_MATERNAL_PROGRAM_UID", "classification": "restricted", "purpose": "maternal programme metadata" }, { "env": "DHIS2_TB_PROGRAM_UID", "classification": "restricted", "purpose": "tuberculosis programme metadata" }, { "env": "DHIS2_CHILD_VISIT_STAGE_UID", "classification": "restricted", "purpose": "child visit stage metadata" }, + { "env": "DHIS2_BCG_BIRTH_STAGE_UID", "classification": "restricted", "purpose": "BCG birth-dose stage metadata" }, + { "env": "DHIS2_OPV_BIRTH_STAGE_UID", "classification": "restricted", "purpose": "OPV birth-dose stage metadata" }, + { "env": "DHIS2_MEASLES_STAGE_UID", "classification": "restricted", "purpose": "measles-dose stage metadata" }, { "env": "DHIS2_FIRST_NAME_ATTRIBUTE_UID", "classification": "restricted", "purpose": "first-name attribute metadata" }, { "env": "DHIS2_LAST_NAME_ATTRIBUTE_UID", "classification": "restricted", "purpose": "last-name attribute metadata" }, { "env": "DHIS2_BIRTH_DATE_ATTRIBUTE_UID", "classification": "restricted", "purpose": "birth-date attribute metadata" }, diff --git a/release/scripts/integration-e2-runner.py b/release/scripts/integration-e2-runner.py index f4b0282ad..ca03df52b 100755 --- a/release/scripts/integration-e2-runner.py +++ b/release/scripts/integration-e2-runner.py @@ -149,6 +149,9 @@ "DHIS2_MATERNAL_PROGRAM_UID", "DHIS2_TB_PROGRAM_UID", "DHIS2_CHILD_VISIT_STAGE_UID", + "DHIS2_BCG_BIRTH_STAGE_UID", + "DHIS2_OPV_BIRTH_STAGE_UID", + "DHIS2_MEASLES_STAGE_UID", "DHIS2_FIRST_NAME_ATTRIBUTE_UID", "DHIS2_LAST_NAME_ATTRIBUTE_UID", "DHIS2_BIRTH_DATE_ATTRIBUTE_UID", diff --git a/release/scripts/test_integration_e2_runner.py b/release/scripts/test_integration_e2_runner.py index 77061386a..51e671c85 100644 --- a/release/scripts/test_integration_e2_runner.py +++ b/release/scripts/test_integration_e2_runner.py @@ -302,6 +302,28 @@ def test_dry_run_is_explicitly_not_evidence_and_hides_values(self) -> None: any("compatibility probe" in item for item in plan["prerequisites"]) ) + def test_dhis2_plan_includes_reviewed_child_health_metadata(self) -> None: + profile = self.module.load_profile("dhis2-tracker-2.41.9") + plan = self.module.plan_document(profile) + required = set(plan["required_input_names"]) + self.assertTrue( + { + "DHIS2_CHILD_PROGRAM_UID", + "DHIS2_CHILD_VISIT_STAGE_UID", + "DHIS2_BCG_BIRTH_STAGE_UID", + "DHIS2_OPV_BIRTH_STAGE_UID", + "DHIS2_MEASLES_STAGE_UID", + }.issubset(required) + ) + self.assertTrue( + { + "DHIS2_MATERNAL_PROGRAM_UID", + "DHIS2_TB_PROGRAM_UID", + "DHIS2_FIRST_NAME_ATTRIBUTE_UID", + "DHIS2_BIRTH_DATE_ATTRIBUTE_UID", + }.issubset(required) + ) + def test_candidate_assets_cross_validate_all_release_bindings(self) -> None: candidate = self.make_candidate() metadata = self.candidate_metadata(candidate)