From 13055d7ee18f9d29edc641d0bd0e57c3d034dfa0 Mon Sep 17 00:00:00 2001 From: Hare Date: Wed, 7 Oct 2026 00:08:01 +0900 Subject: [PATCH 1/9] feat: execute durable standalone Jobs through Host-owned Internal Workers (T-708) --- Cargo.lock | 15 + Cargo.toml | 3 + crates/job/Cargo.toml | 13 + crates/job/src/lib.rs | 336 +++++ crates/job/tests/contract.rs | 308 ++++ crates/manifest/src/builtin_profile.rs | 7 + crates/manifest/src/profile.rs | 7 + crates/standalone/Cargo.toml | 2 + crates/standalone/src/host.rs | 43 + crates/standalone/src/job_store.rs | 1483 ++++++++++++++++++++ crates/standalone/src/jobs.rs | 393 ++++++ crates/standalone/src/jobs/tests.rs | 126 ++ crates/standalone/src/lib.rs | 2 + crates/standalone/src/store.rs | 4 + crates/standalone/tests/jobs.rs | 478 +++++++ crates/worker/Cargo.toml | 1 + crates/worker/src/internal_worker.rs | 3 + crates/worker/src/job.rs | 323 +++++ crates/worker/src/job/result.rs | 179 +++ crates/worker/src/job/tests.rs | 666 +++++++++ crates/worker/src/lib.rs | 1 + crates/worker/src/prompt/catalog.rs | 3 + crates/workspace-server/Cargo.toml | 1 + crates/workspace-server/src/backend_job.rs | 539 +++++-- docs/design/standalone-agent-host.md | 3 +- docs/design/standalone-jobs.md | 152 ++ docs/report/2026-10-06-t708-validation.md | 134 ++ resources/profiles/job.dcdl | 27 + resources/prompts/catalog.dcdl | 2 + resources/prompts/internal/job_system.md | 7 + 30 files changed, 5108 insertions(+), 153 deletions(-) create mode 100644 crates/job/Cargo.toml create mode 100644 crates/job/src/lib.rs create mode 100644 crates/job/tests/contract.rs create mode 100644 crates/standalone/src/job_store.rs create mode 100644 crates/standalone/src/jobs.rs create mode 100644 crates/standalone/src/jobs/tests.rs create mode 100644 crates/standalone/tests/jobs.rs create mode 100644 crates/worker/src/job.rs create mode 100644 crates/worker/src/job/result.rs create mode 100644 crates/worker/src/job/tests.rs create mode 100644 docs/design/standalone-jobs.md create mode 100644 docs/report/2026-10-06-t708-validation.md create mode 100644 resources/profiles/job.dcdl create mode 100644 resources/prompts/internal/job_system.md diff --git a/Cargo.lock b/Cargo.lock index c0879f5e5..37b4fe8b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2339,6 +2339,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "job" +version = "0.1.0" +dependencies = [ + "manifest", + "serde", + "serde_json", + "sha2 0.11.0", + "thiserror 2.0.18", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -4625,8 +4636,10 @@ dependencies = [ "client", "fs4", "futures", + "job", "manifest", "protocol", + "rusqlite", "serde", "serde_json", "session-store", @@ -6432,6 +6445,7 @@ dependencies = [ "futures-util", "http", "include_dir", + "job", "jsonschema", "libc", "manifest", @@ -6615,6 +6629,7 @@ dependencies = [ "config-source", "flow", "futures", + "job", "manifest", "memory", "merge-request", diff --git a/Cargo.toml b/Cargo.toml index b04334dea..a43ed2096 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "crates/api-macros-impl", "crates/session-store", "crates/standalone", + "crates/job", "crates/secrets", "crates/manifest", "crates/mcp", @@ -43,6 +44,7 @@ default-members = [ "crates/api-macros-impl", "crates/session-store", "crates/standalone", + "crates/job", "crates/secrets", "crates/manifest", "crates/mcp", @@ -103,6 +105,7 @@ session-metrics = { path = "crates/session-metrics" } session-analytics = { path = "crates/session-analytics" } session-store = { path = "crates/session-store" } standalone = { path = "crates/standalone" } +job = { path = "crates/job" } runtime-api = { path = "crates/runtime-api" } server-api = { path = "crates/server-api" } secrets = { path = "crates/secrets" } diff --git a/crates/job/Cargo.toml b/crates/job/Cargo.toml new file mode 100644 index 000000000..934910479 --- /dev/null +++ b/crates/job/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "job" +version = "0.1.0" +edition.workspace = true +license.workspace = true +publish = false + +[dependencies] +manifest.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +sha2.workspace = true +thiserror.workspace = true diff --git a/crates/job/src/lib.rs b/crates/job/src/lib.rs new file mode 100644 index 000000000..8426b4b2c --- /dev/null +++ b/crates/job/src/lib.rs @@ -0,0 +1,336 @@ +//! Transport-neutral immutable Job intent and structured-result contract. +//! +//! Hosts own execution, authority, persistence and lifecycle state. This crate +//! deliberately contains no runner, state machine, Runtime or domain grants. + +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use sha2::{Digest, Sha256}; + +pub const DEFAULT_MAX_CONCURRENT_JOBS: u16 = 8; +pub const ABSOLUTE_MAX_CONCURRENT_JOBS: u16 = 64; +pub const DEFAULT_JOB_TIMEOUT_SECONDS: u32 = 120; +pub const MAX_JOB_TIMEOUT_SECONDS: u32 = 600; +pub const DEFAULT_MAX_RESULT_BYTES: u32 = 16 * 1024; +pub const ABSOLUTE_MAX_RESULT_BYTES: u32 = 64 * 1024; +pub const DEFAULT_MAX_ATTEMPTS: u8 = 2; +pub const ABSOLUTE_MAX_ATTEMPTS: u8 = 3; +pub const MAX_JOB_INPUT_BYTES: usize = 128 * 1024; +pub const MAX_JOB_INSTRUCTION_BYTES: usize = 16 * 1024; +pub const MAX_JOB_PURPOSE_BYTES: usize = 120; +pub const MAX_JOB_REFERENCE_BYTES: usize = 512; +pub const MAX_JOB_FAILURE_BYTES: usize = 1024; +pub const MAX_JOB_DELIVERY_BYTES: usize = 16 * 1024; + +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +pub enum JobError { + #[error("{0}")] + InvalidInput(String), +} + +pub type Result = std::result::Result; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct JobLimits { + #[serde(default = "default_max_concurrent_jobs")] + pub max_concurrent_jobs: u16, + #[serde(default = "default_job_timeout_seconds")] + pub timeout_seconds: u32, + #[serde(default = "default_max_result_bytes")] + pub max_result_bytes: u32, + #[serde(default = "default_max_attempts")] + pub max_attempts: u8, +} + +impl Default for JobLimits { + fn default() -> Self { + Self { + max_concurrent_jobs: DEFAULT_MAX_CONCURRENT_JOBS, + timeout_seconds: DEFAULT_JOB_TIMEOUT_SECONDS, + max_result_bytes: DEFAULT_MAX_RESULT_BYTES, + max_attempts: DEFAULT_MAX_ATTEMPTS, + } + } +} + +impl JobLimits { + pub fn validate(&self) -> Result<()> { + if self.max_concurrent_jobs == 0 || self.max_concurrent_jobs > ABSOLUTE_MAX_CONCURRENT_JOBS + { + return Err(JobError::InvalidInput(format!( + "Job max_concurrent_jobs must be in 1..={ABSOLUTE_MAX_CONCURRENT_JOBS}" + ))); + } + if self.timeout_seconds == 0 || self.timeout_seconds > MAX_JOB_TIMEOUT_SECONDS { + return Err(JobError::InvalidInput(format!( + "Job timeout_seconds must be in 1..={MAX_JOB_TIMEOUT_SECONDS}" + ))); + } + if self.max_result_bytes == 0 || self.max_result_bytes > ABSOLUTE_MAX_RESULT_BYTES { + return Err(JobError::InvalidInput(format!( + "Job max_result_bytes must be in 1..={ABSOLUTE_MAX_RESULT_BYTES}" + ))); + } + if self.max_attempts == 0 || self.max_attempts > ABSOLUTE_MAX_ATTEMPTS { + return Err(JobError::InvalidInput(format!( + "Job max_attempts must be in 1..={ABSOLUTE_MAX_ATTEMPTS}" + ))); + } + Ok(()) + } +} + +const fn default_max_concurrent_jobs() -> u16 { + DEFAULT_MAX_CONCURRENT_JOBS +} +const fn default_job_timeout_seconds() -> u32 { + DEFAULT_JOB_TIMEOUT_SECONDS +} +const fn default_max_result_bytes() -> u32 { + DEFAULT_MAX_RESULT_BYTES +} +const fn default_max_attempts() -> u8 { + DEFAULT_MAX_ATTEMPTS +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct JobRequest { + /// Stable caller-selected idempotency key within the host's namespace. + pub job_id: String, + pub purpose: String, + /// Revision fenced by the structured result. + pub input_revision: String, + pub input_ref: String, + /// Bounded immutable snapshot; must not contain credentials. + pub input: Value, + pub instruction: String, + /// Manifest registry selector, never raw source or a filesystem path. + pub profile: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub serialization_key: Option, + #[serde(default)] + pub limits: JobLimits, +} + +impl JobRequest { + pub fn validate(&self) -> Result<()> { + self.validate_fields()?; + self.validate_input()?; + self.limits.validate() + } + + /// Request metadata validation, separable so adapters can retain their + /// domain-validation ordering before validating input and limits. + pub fn validate_fields(&self) -> Result<()> { + validate_identifier("job_id", &self.job_id, 256)?; + validate_identifier("purpose", &self.purpose, MAX_JOB_PURPOSE_BYTES)?; + validate_identifier("input_revision", &self.input_revision, 256)?; + validate_bounded_text("input_ref", &self.input_ref, MAX_JOB_REFERENCE_BYTES)?; + validate_bounded_text("instruction", &self.instruction, MAX_JOB_INSTRUCTION_BYTES)?; + self.profile_selector()?; + if let Some(key) = &self.serialization_key { + validate_identifier("serialization_key", key, MAX_JOB_REFERENCE_BYTES)?; + } + Ok(()) + } + + pub fn validate_input(&self) -> Result<()> { + let input = serde_json::to_vec(&self.input) + .map_err(|error| JobError::InvalidInput(format!("serialize Job input: {error}")))?; + if input.len() > MAX_JOB_INPUT_BYTES { + return Err(JobError::InvalidInput(format!( + "Job input exceeds {MAX_JOB_INPUT_BYTES} bytes" + ))); + } + Ok(()) + } + + /// Syntax only; registry existence and resolution are host authority. + pub fn profile_selector(&self) -> Result { + profile_selector(&self.profile) + } + + pub fn fingerprint(&self) -> Result { + self.validate()?; + fingerprint(self) + } + + pub fn worker_input(&self, attempt_id: &str) -> Result { + worker_input(self, attempt_id) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct JobResultSubmission { + pub job_id: String, + pub attempt_id: String, + pub input_revision: String, + pub result: Value, +} + +/// Client-facing lifecycle vocabulary, not a runner or transition policy. +/// A variant's presence does not imply every host supports that operation. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum JobState { + Pending, + Completed, + Failed, + Unknown, + Cancelled, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum JobAttemptState { + Reserved, + Dispatching, + Dispatched, + Completed, + Failed, + Unknown, + Cancelled, +} + +/// Transport-neutral read snapshot. Job status/result and the corresponding +/// attempt identity/status remain distinct; host identities, delivery and +/// resource cleanup are deliberately not part of the model outcome. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct JobOutcome { + pub job_id: String, + pub input_revision: String, + pub attempt_id: String, + pub attempt: u8, + pub state: JobState, + pub attempt_state: JobAttemptState, + pub result: Option, + pub failure_category: Option, + pub failure_detail: Option, +} + +pub fn profile_selector(profile: &str) -> Result { + use manifest::ProfileSelector; + validate_identifier("profile", profile, 256)?; + let selector = ProfileSelector::parse_cli(profile); + let valid = match &selector { + ProfileSelector::Default => true, + ProfileSelector::Path { .. } => false, + ProfileSelector::Named { source, name } => { + !name.is_empty() + && !name.starts_with('.') + && !name.contains(['/', '\\', ':']) + && name + .chars() + .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.')) + && ![".toml", ".json", ".dcdl", ".nix"] + .iter() + .any(|suffix| name.ends_with(suffix)) + && (source.is_some() || name != "inherit") + } + }; + if !valid { + return Err(JobError::InvalidInput( + "Job profile must be a manifest registry selector, not raw source or a path".into(), + )); + } + Ok(selector) +} + +/// Hash the full serialized host intent, not just its neutral projection. +/// Callers must validate the complete intent first. JSON field ordering and +/// omission rules are intentionally preserved for durable replay compatibility. +pub fn fingerprint(intent: &T) -> Result { + let encoded = serde_json::to_vec(intent) + .map_err(|error| JobError::InvalidInput(format!("serialize Job intent: {error}")))?; + Ok(sha256(&encoded)) +} + +pub fn worker_input(request: &JobRequest, attempt_id: &str) -> Result { + worker_input_with_label(request, attempt_id, "Job") +} + +/// Adapter-specific capability label; only the host, never the model, selects it. +/// Keeps an existing host's prompt envelope byte-for-byte compatible. +pub fn worker_input_with_label( + request: &JobRequest, + attempt_id: &str, + label: &str, +) -> Result { + let input = serde_json::to_string(&request.input) + .map_err(|error| JobError::InvalidInput(format!("serialize Job input: {error}")))?; + Ok(format!( + "{}\n\n{label} envelope (immutable):\njob_id: {}\nattempt_id: {}\ninput_revision: {}\ninput_ref: {}\ninput_json: {}\n\nReturn success only through the structured {label} result capability. Final prose and Worker Idle/Stopped state are not result authority.", + request.instruction, + request.job_id, + attempt_id, + request.input_revision, + request.input_ref, + input + )) +} + +pub fn attempt_id(job_id: &str, attempt: u8) -> String { + format!("{job_id}:attempt:{attempt}") +} + +/// Bound the encoded JSON before hashing. Returns (digest, exact JSON). +pub fn result_digest(result: &Value, max_bytes: u32) -> Result<(String, String)> { + let encoded = serde_json::to_string(result) + .map_err(|error| JobError::InvalidInput(format!("serialize Job result: {error}")))?; + if encoded.len() > max_bytes as usize { + return Err(JobError::InvalidInput(format!( + "Job result exceeds {max_bytes} bytes" + ))); + } + Ok((sha256(encoded.as_bytes()), encoded)) +} + +pub fn bounded_failure_detail(detail: &str) -> String { + if detail.len() <= MAX_JOB_FAILURE_BYTES { + return detail.to_string(); + } + let mut end = MAX_JOB_FAILURE_BYTES; + while !detail.is_char_boundary(end) { + end -= 1; + } + detail[..end].to_string() +} + +pub fn validate_identifier(field: &str, value: &str, max_bytes: usize) -> Result<()> { + validate_bounded_text(field, value, max_bytes)?; + if value.chars().any(char::is_control) { + return Err(JobError::InvalidInput(format!( + "Job {field} must not contain control characters" + ))); + } + Ok(()) +} + +pub fn validate_bounded_text(field: &str, value: &str, max_bytes: usize) -> Result<()> { + if value.is_empty() || value.trim() != value { + return Err(JobError::InvalidInput(format!( + "Job {field} must be non-empty and trimmed" + ))); + } + if value.len() > max_bytes { + return Err(JobError::InvalidInput(format!( + "Job {field} exceeds {max_bytes} bytes" + ))); + } + Ok(()) +} + +pub fn sha256(bytes: &[u8]) -> String { + let digest = Sha256::digest(bytes); + let mut output = String::with_capacity(7 + digest.len() * 2); + output.push_str("sha256:"); + for byte in digest { + use std::fmt::Write as _; + let _ = write!(output, "{byte:02x}"); + } + output +} diff --git a/crates/job/tests/contract.rs b/crates/job/tests/contract.rs new file mode 100644 index 000000000..fdfd328ff --- /dev/null +++ b/crates/job/tests/contract.rs @@ -0,0 +1,308 @@ +use job::*; +use serde_json::json; + +fn request() -> JobRequest { + JobRequest { + job_id: "check:r7".into(), + purpose: "snapshot_check".into(), + input_revision: "7".into(), + input_ref: "snapshot://7".into(), + input: json!({"title": "Check me"}), + instruction: "Check this immutable snapshot.".into(), + profile: "project:custom-check".into(), + serialization_key: None, + limits: JobLimits::default(), + } +} + +#[test] +fn shared_lifecycle_enums_round_trip_all_host_states() { + for (state, name) in [ + (JobState::Pending, "pending"), + (JobState::Completed, "completed"), + (JobState::Failed, "failed"), + (JobState::Unknown, "unknown"), + (JobState::Cancelled, "cancelled"), + ] { + assert_eq!(serde_json::to_value(state).unwrap(), json!(name)); + assert_eq!( + serde_json::from_value::(json!(name)).unwrap(), + state + ); + } + for (state, name) in [ + (JobAttemptState::Reserved, "reserved"), + (JobAttemptState::Dispatching, "dispatching"), + (JobAttemptState::Dispatched, "dispatched"), + (JobAttemptState::Completed, "completed"), + (JobAttemptState::Failed, "failed"), + (JobAttemptState::Unknown, "unknown"), + (JobAttemptState::Cancelled, "cancelled"), + ] { + assert_eq!(serde_json::to_value(state).unwrap(), json!(name)); + assert_eq!( + serde_json::from_value::(json!(name)).unwrap(), + state + ); + } + assert!(serde_json::from_value::(json!("running")).is_err()); + assert!(serde_json::from_value::(json!("idle")).is_err()); +} + +#[test] +fn outcome_is_a_neutral_read_snapshot_with_optional_result_and_failure() { + let mut outcome = JobOutcome { + job_id: "job-1".into(), + input_revision: "7".into(), + attempt_id: "job-1:attempt:2".into(), + attempt: 2, + state: JobState::Pending, + attempt_state: JobAttemptState::Dispatching, + result: None, + failure_category: None, + failure_detail: None, + }; + let encoded = serde_json::to_value(&outcome).unwrap(); + assert_eq!( + encoded, + json!({ + "job_id": "job-1", "input_revision": "7", "attempt_id": "job-1:attempt:2", + "attempt": 2, "state": "pending", "attempt_state": "dispatching", + "result": null, "failure_category": null, "failure_detail": null + }) + ); + assert_eq!( + serde_json::from_value::(encoded.clone()).unwrap(), + outcome + ); + let mut with_host_identity = encoded; + with_host_identity["workspace_id"] = json!("workspace-a"); + assert!(serde_json::from_value::(with_host_identity).is_err()); + for (state, attempt_state, result, category, detail) in [ + ( + JobState::Completed, + JobAttemptState::Completed, + Some(json!({"ok": true})), + None, + None, + ), + ( + JobState::Failed, + JobAttemptState::Failed, + None, + Some("execution"), + Some("known failure"), + ), + ( + JobState::Unknown, + JobAttemptState::Unknown, + None, + Some("interrupted"), + Some("requires reconciliation"), + ), + ( + JobState::Cancelled, + JobAttemptState::Cancelled, + None, + Some("cancelled"), + Some("caller cancelled"), + ), + ] { + outcome.state = state; + outcome.attempt_state = attempt_state; + outcome.result = result; + outcome.failure_category = category.map(str::to_owned); + outcome.failure_detail = detail.map(str::to_owned); + assert_eq!( + serde_json::from_str::(&serde_json::to_string(&outcome).unwrap()).unwrap(), + outcome + ); + } +} + +#[test] +fn neutral_request_and_result_have_strict_round_trip_contracts() { + let request = request(); + let encoded = serde_json::to_value(&request).unwrap(); + assert_eq!(encoded.as_object().unwrap().len(), 8); + assert!(encoded.get("serialization_key").is_none()); + assert_eq!( + serde_json::from_value::(encoded.clone()).unwrap(), + request + ); + let mut with_grant = encoded.clone(); + with_grant["grants"] = json!({}); + assert!(serde_json::from_value::(with_grant).is_err()); + let mut with_provenance = encoded; + with_provenance["source_worker"] = json!({}); + assert!(serde_json::from_value::(with_provenance).is_err()); + + let submission = JobResultSubmission { + job_id: request.job_id, + attempt_id: "check:r7:attempt:1".into(), + input_revision: request.input_revision, + result: json!({"ok": true}), + }; + let mut encoded = serde_json::to_value(&submission).unwrap(); + assert_eq!( + serde_json::from_value::(encoded.clone()).unwrap(), + submission + ); + encoded["authority"] = json!("profile"); + assert!(serde_json::from_value::(encoded).is_err()); + assert_eq!( + serde_json::from_str::("{}").unwrap(), + JobLimits::default() + ); + assert!(serde_json::from_str::(r#"{"turns":999}"#).is_err()); +} + +#[test] +fn fingerprint_covers_every_immutable_field_and_preserves_replay() { + let request = request(); + let digest = request.fingerprint().unwrap(); + let decoded: JobRequest = + serde_json::from_str(&serde_json::to_string(&request).unwrap()).unwrap(); + assert_eq!(digest, decoded.fingerprint().unwrap()); + assert_eq!(digest, fingerprint(&request).unwrap()); + for mutate in [ + |r: &mut JobRequest| r.job_id.push('x'), + |r: &mut JobRequest| r.purpose.push('x'), + |r: &mut JobRequest| r.input_revision.push('x'), + |r: &mut JobRequest| r.input_ref.push('x'), + |r: &mut JobRequest| r.input = json!({"title": "changed"}), + |r: &mut JobRequest| r.instruction.push('x'), + |r: &mut JobRequest| r.profile = "builtin:backend-job".into(), + |r: &mut JobRequest| r.serialization_key = Some("resource:7".into()), + |r: &mut JobRequest| r.limits.max_attempts = 3, + ] { + let mut changed = request.clone(); + mutate(&mut changed); + assert_ne!(digest, changed.fingerprint().unwrap()); + } +} + +#[test] +fn registry_profiles_are_not_authority_or_a_fixed_allowlist() { + for profile in [ + "default", + "custom", + "project:custom", + "user:custom", + "builtin:future-job", + ] { + profile_selector(profile).unwrap(); + } + for profile in [ + "", + " inherit", + "inherit", + "path:custom", + "./a.toml", + "/tmp/a", + "project:../a", + "project:a.toml", + "builtin:", + "unknown:a", + "project:a:b", + "C:\\custom", + "{\"worker\":{}}", + ] { + assert!(profile_selector(profile).is_err(), "accepted {profile}"); + } +} + +#[test] +fn validation_enforces_utf8_bytes_and_metadata_bounds() { + let mut r = request(); + r.job_id = "x".repeat(256); + r.input_revision = "x".repeat(256); + r.purpose = "x".repeat(MAX_JOB_PURPOSE_BYTES); + r.input_ref = "x".repeat(MAX_JOB_REFERENCE_BYTES); + r.instruction = "x".repeat(MAX_JOB_INSTRUCTION_BYTES); + r.serialization_key = Some("x".repeat(MAX_JOB_REFERENCE_BYTES)); + r.input = json!("x".repeat(MAX_JOB_INPUT_BYTES - 2)); + r.validate().unwrap(); + for mutate in [ + |r: &mut JobRequest| r.job_id.push('x'), + |r: &mut JobRequest| r.input_revision.push('x'), + |r: &mut JobRequest| r.purpose.push('x'), + |r: &mut JobRequest| r.input_ref.push('x'), + |r: &mut JobRequest| r.instruction.push('x'), + |r: &mut JobRequest| r.serialization_key.as_mut().unwrap().push('x'), + |r: &mut JobRequest| r.input = json!("x".repeat(MAX_JOB_INPUT_BYTES - 1)), + ] { + let mut changed = r.clone(); + mutate(&mut changed); + assert!(changed.validate().is_err()); + assert!(changed.fingerprint().is_err()); + } + assert!(validate_identifier("id", "a\nb", 256).is_err()); + assert!(validate_bounded_text("text", " untrimmed", 256).is_err()); + assert!(validate_bounded_text("text", "", 256).is_err()); + assert!(validate_bounded_text("text", "界", 2).is_err()); + validate_bounded_text("text", "a\nb", 256).unwrap(); +} + +#[test] +fn all_limit_dimensions_reject_zero_and_above_absolute_bounds() { + let maximum = JobLimits { + max_concurrent_jobs: ABSOLUTE_MAX_CONCURRENT_JOBS, + timeout_seconds: MAX_JOB_TIMEOUT_SECONDS, + max_result_bytes: ABSOLUTE_MAX_RESULT_BYTES, + max_attempts: ABSOLUTE_MAX_ATTEMPTS, + }; + maximum.validate().unwrap(); + for mutate in [ + |l: &mut JobLimits| l.max_concurrent_jobs = 0, + |l: &mut JobLimits| l.max_concurrent_jobs += 1, + |l: &mut JobLimits| l.timeout_seconds = 0, + |l: &mut JobLimits| l.timeout_seconds += 1, + |l: &mut JobLimits| l.max_result_bytes = 0, + |l: &mut JobLimits| l.max_result_bytes += 1, + |l: &mut JobLimits| l.max_attempts = 0, + |l: &mut JobLimits| l.max_attempts += 1, + ] { + let mut changed = maximum.clone(); + mutate(&mut changed); + assert!(changed.validate().is_err()); + } +} + +#[test] +fn result_digest_and_failure_detail_bound_exact_utf8_encoding() { + let result = json!({"ok": true}); + let (digest, encoded) = result_digest(&result, 11).unwrap(); + assert_eq!(encoded, r#"{"ok":true}"#); + assert_eq!(digest, sha256(encoded.as_bytes())); + assert!(result_digest(&result, 10).is_err()); + let unicode = json!("界"); + assert!(result_digest(&unicode, 4).is_err()); + result_digest(&unicode, 5).unwrap(); + let detail = "界".repeat(MAX_JOB_FAILURE_BYTES); + let bounded = bounded_failure_detail(&detail); + assert_eq!(bounded.len(), MAX_JOB_FAILURE_BYTES - 1); + assert!(detail.starts_with(&bounded)); + assert_eq!(bounded_failure_detail("known failure"), "known failure"); +} + +#[test] +fn worker_input_is_revision_and_attempt_bound_not_final_prose_authority() { + let request = request(); + let attempt = attempt_id(&request.job_id, 2); + assert_eq!(attempt, "check:r7:attempt:2"); + let input = request.worker_input(&attempt).unwrap(); + assert!(input.starts_with(&request.instruction)); + for field in [ + "Job envelope (immutable):", + "job_id: check:r7", + "attempt_id: check:r7:attempt:2", + "input_revision: 7", + "input_ref: snapshot://7", + r#"input_json: {"title":"Check me"}"#, + "structured Job result capability", + "Final prose and Worker Idle/Stopped state are not result authority.", + ] { + assert!(input.contains(field), "missing {field}"); + } +} diff --git a/crates/manifest/src/builtin_profile.rs b/crates/manifest/src/builtin_profile.rs index e19b44956..4f44ca57a 100644 --- a/crates/manifest/src/builtin_profile.rs +++ b/crates/manifest/src/builtin_profile.rs @@ -94,6 +94,13 @@ pub const BUILTIN_PROFILE_RESOURCES: &[BuiltinProfileResource] = &[ description: "Workspace orchestration and Worker control.", imports: BASE_IMPORT, }, + BuiltinProfileResource { + selector: Some("builtin:job"), + path: "profiles/job.dcdl", + source: include_str!("../../../resources/profiles/job.dcdl"), + description: "Result-only caller-selected Job policy.", + imports: NO_IMPORTS, + }, BuiltinProfileResource { selector: Some("builtin:backend-job"), path: "profiles/backend-job.dcdl", diff --git a/crates/manifest/src/profile.rs b/crates/manifest/src/profile.rs index aba2a5a9f..4fc3ad04e 100644 --- a/crates/manifest/src/profile.rs +++ b/crates/manifest/src/profile.rs @@ -1257,6 +1257,13 @@ mod tests { ) .unwrap(); assert_eq!(resolved.manifest.worker.name, "role-worker"); + if entry.name == "job" { + assert!(resolved.manifest.compaction.is_none()); + assert!(resolved.manifest.scope.allow.is_empty()); + assert!(resolved.manifest.delegation_scope.allow.is_empty()); + assert_eq!(resolved.manifest.engine.instruction, "internal.job_system"); + continue; + } let compaction = resolved.manifest.compaction.as_ref().unwrap(); assert_eq!(compaction.worker_max_turns, Some(100), "{}", entry.name); assert_eq!( diff --git a/crates/standalone/Cargo.toml b/crates/standalone/Cargo.toml index 8c5adc9df..a8b078496 100644 --- a/crates/standalone/Cargo.toml +++ b/crates/standalone/Cargo.toml @@ -10,6 +10,8 @@ agen.workspace = true client.workspace = true fs4.workspace = true manifest.workspace = true +job.workspace = true +rusqlite.workspace = true protocol.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/standalone/src/host.rs b/crates/standalone/src/host.rs index c7e6e754d..17c77ac35 100644 --- a/crates/standalone/src/host.rs +++ b/crates/standalone/src/host.rs @@ -43,6 +43,7 @@ pub struct StandaloneHost { worker_store: FsWorkerStore, record: StandaloneWorkerRecord, lease: Option, + jobs: crate::jobs::StandaloneJobs, } #[derive(Debug, Clone, PartialEq, Eq, Error)] @@ -77,6 +78,8 @@ pub enum StandaloneStartupError { #[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] pub enum StandaloneShutdownError { + #[error("standalone Job execution cleanup or terminal persistence could not be confirmed")] + JobCleanup, #[error("the standalone Worker did not stop before the shutdown deadline")] DeadlineExceeded, #[error("the standalone Worker shutdown confirmation was lost")] @@ -110,6 +113,16 @@ impl StandaloneHost { .allocate(&launch.cwd, StaleLeasePolicy::Reject) .map_err(classify_store_startup_error)?; let worker_id = allocation.worker_id(); + let jobs = match crate::jobs::StandaloneJobs::open( + &store.jobs_path(worker_id), + launch.cwd.clone(), + ) { + Ok(jobs) => jobs, + Err(_) => { + let _ = store.abandon_allocation(allocation); + return Err(StandaloneStartupError::StateStore); + } + }; // WorkerId is the stable identity. The current Worker store remains // name-keyed, so keep its derived storage key separate from the @@ -181,6 +194,7 @@ impl StandaloneHost { worker_store, record, allocation.into_lease(), + jobs, )) } @@ -216,6 +230,11 @@ impl StandaloneHost { let lease = store .acquire_lease(worker_id, StaleLeasePolicy::Recover) .map_err(classify_store_startup_error)?; + let jobs = crate::jobs::StandaloneJobs::open( + &store.jobs_path(worker_id), + record.cwd.canonical_path.clone(), + ) + .map_err(|_| StandaloneStartupError::StateStore)?; let (backing_store, worker_store) = backing_store(&store, worker_id)?; let storage_key = record.storage_key.clone(); let mut manifest = record.manifest.clone(); @@ -270,6 +289,7 @@ impl StandaloneHost { worker_store, record, lease, + jobs, )) } @@ -279,6 +299,7 @@ impl StandaloneHost { worker_store: FsWorkerStore, record: StandaloneWorkerRecord, lease: StandaloneWorkerLease, + jobs: crate::jobs::StandaloneJobs, ) -> Self { Self { handle: started.handle, @@ -288,9 +309,16 @@ impl StandaloneHost { worker_store, record, lease: Some(lease), + jobs, } } + /// Explicit Host execution capability for local Features, separate from the + /// interactive Worker protocol and its model-created SubWorkers. + pub fn jobs(&self) -> crate::jobs::StandaloneJobs { + self.jobs.clone() + } + #[must_use] pub fn worker_id(&self) -> WorkerId { self.record.worker_id @@ -318,6 +346,7 @@ impl StandaloneHost { } pub async fn shutdown(mut self) -> Result<(), StandaloneShutdownError> { + let jobs_result = self.jobs.shutdown().await; let command = protocol::WorkerCommandEnvelope::new(u64::MAX); let _ = self.handle.send(Method::Shutdown { command }).await; let Some(shutdown) = self.shutdown.take() else { @@ -335,6 +364,10 @@ impl StandaloneHost { return Err(StandaloneShutdownError::DeadlineExceeded); } } + if jobs_result.is_err() { + self.retain_lease(); + return Err(StandaloneShutdownError::JobCleanup); + } let active = match active_pointer(&self.worker_store, &self.record.storage_key) { Ok(active) => active, Err(_) => { @@ -370,6 +403,16 @@ impl StandaloneHost { } } +impl Drop for StandaloneHost { + fn drop(&mut self) { + self.jobs.close(); + // A dropped Host cannot synchronously confirm either controller or Job + // cleanup. Keep its lease until explicit shutdown or process recovery, + // preventing another Host from reopening a still-live execution store. + self.retain_lease(); + } +} + async fn run_protocol_session( handle: worker::WorkerHandle, streams: WorkerProtocolSessionStreams, diff --git a/crates/standalone/src/job_store.rs b/crates/standalone/src/job_store.rs new file mode 100644 index 000000000..a28979f00 --- /dev/null +++ b/crates/standalone/src/job_store.rs @@ -0,0 +1,1483 @@ +//! Host-owned durable Jobs. Opening performs recovery and therefore requires the +//! StandaloneHost's Worker lease; this is not a user-facing database constructor. +//! +//! A result is committed here before the service releases its Internal Worker. +//! Worker termination, final prose, and consumer acknowledgement are not success +//! authority. The service owns the connection mutex and live-resource cleanup. + +use std::path::Path; +use std::time::Duration; + +use job::{JobRequest, JobResultSubmission}; +use rusqlite::{Connection, OptionalExtension, TransactionBehavior, params}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use thiserror::Error; + +const SCHEMA_VERSION: i64 = 1; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum JobState { + Pending, + Completed, + Failed, + Unknown, + Cancelled, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum JobAttemptState { + Reserved, + Dispatched, + Completed, + Failed, + Unknown, + Cancelled, +} + +impl JobAttemptState { + pub fn terminal(self) -> bool { + !matches!(self, Self::Reserved | Self::Dispatched) + } + + fn as_str(self) -> &'static str { + match self { + Self::Reserved => "reserved", + Self::Dispatched => "dispatched", + Self::Completed => "completed", + Self::Failed => "failed", + Self::Unknown => "unknown", + Self::Cancelled => "cancelled", + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct JobSnapshot { + pub request: JobRequest, + pub state: JobState, + pub attempt: JobAttempt, + pub result: Option, + pub acknowledged: bool, +} + +impl JobSnapshot { + /// Shared Feature-facing read contract; no Workspace or execution identity leaks. + pub fn outcome(&self) -> job::JobOutcome { + job::JobOutcome { + job_id: self.request.job_id.clone(), + input_revision: self.request.input_revision.clone(), + attempt_id: self.attempt.attempt_id.clone(), + attempt: self.attempt.number, + state: match self.state { + JobState::Pending => job::JobState::Pending, + JobState::Completed => job::JobState::Completed, + JobState::Failed => job::JobState::Failed, + JobState::Unknown => job::JobState::Unknown, + JobState::Cancelled => job::JobState::Cancelled, + }, + attempt_state: match self.attempt.state { + JobAttemptState::Reserved => job::JobAttemptState::Reserved, + JobAttemptState::Dispatched => job::JobAttemptState::Dispatched, + JobAttemptState::Completed => job::JobAttemptState::Completed, + JobAttemptState::Failed => job::JobAttemptState::Failed, + JobAttemptState::Unknown => job::JobAttemptState::Unknown, + JobAttemptState::Cancelled => job::JobAttemptState::Cancelled, + }, + result: self.result.clone(), + failure_category: self.attempt.failure.as_ref().map(|_| { + match self.attempt.state { + JobAttemptState::Cancelled => "cancelled", + JobAttemptState::Unknown => "unknown_outcome", + _ => "execution_failed", + } + .to_string() + }), + failure_detail: self.attempt.failure.clone(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct JobAttempt { + pub attempt_id: String, + pub number: u8, + pub state: JobAttemptState, + pub failure: Option, +} + +#[derive(Debug, Error)] +pub enum JobStoreError { + #[error("Job database: {0}")] + Sqlite(#[from] rusqlite::Error), + #[error("Job JSON: {0}")] + Json(#[from] serde_json::Error), + #[error(transparent)] + Job(#[from] job::JobError), + #[error("Job directory: {0}")] + Io(#[from] std::io::Error), + #[error("unsupported Job schema version {0}")] + UnsupportedSchema(i64), + #[error("corrupt Job database: {0}")] + Corrupt(String), + #[error("unknown Job `{0}`")] + NotFound(String), + #[error("Job id `{0}` was reused with different immutable intent")] + IntentConflict(String), + #[error("serialization key `{0}` is reserved by another Job")] + SerializationConflict(String), + #[error("Job concurrent execution limit reached")] + ConcurrentLimit, + #[error("result or lifecycle operation is not bound to the current Job attempt")] + AttemptMismatch, + #[error("result input revision does not match the immutable request")] + RevisionMismatch, + #[error("accepted Job result cannot be changed")] + ResultConflict, + #[error("invalid Job transition: {0}")] + InvalidTransition(&'static str), +} + +pub(crate) struct JobStore { + conn: Connection, +} + +impl JobStore { + /// The caller must hold the Host's Worker lease for this connection's entire + /// lifetime. Recovery must never run beside a live service for this Worker. + pub(crate) fn open(path: &Path) -> Result { + if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) { + std::fs::create_dir_all(parent)?; + } + let mut conn = Connection::open(path)?; + conn.busy_timeout(Duration::from_secs(5))?; + conn.pragma_update(None, "foreign_keys", "ON")?; + conn.pragma_update(None, "journal_mode", "WAL")?; + conn.pragma_update(None, "synchronous", "FULL")?; + let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?; + let version: i64 = tx.pragma_query_value(None, "user_version", |row| row.get(0))?; + match version { + 0 => { + tx.execute_batch(SCHEMA)?; + tx.pragma_update(None, "user_version", SCHEMA_VERSION)?; + } + SCHEMA_VERSION => {} + other => return Err(JobStoreError::UnsupportedSchema(other)), + } + // An accepted result is the only evidence of success. Reserved attempts + // were never started and remain pending; cancelled attempts never resume. + tx.execute_batch( + "UPDATE job_attempts SET state = 'completed', failure = NULL + WHERE state = 'dispatched' AND result_json IS NOT NULL; + UPDATE job_attempts SET state = 'unknown', + failure = 'Host interrupted before an accepted result' + WHERE state = 'dispatched' AND result_json IS NULL; + UPDATE job_intents SET state = ( + SELECT state FROM job_attempts + WHERE job_id = job_intents.job_id AND number = current_attempt) + WHERE state = 'pending' AND EXISTS ( + SELECT 1 FROM job_attempts WHERE job_id = job_intents.job_id + AND number = current_attempt AND state IN ('completed', 'unknown'));", + )?; + tx.commit()?; + Ok(Self { conn }) + } + + pub(crate) fn reserve(&mut self, request: JobRequest) -> Result { + request.validate()?; + let fingerprint = request.fingerprint()?; + let request_json = serde_json::to_string(&request)?; + let tx = self + .conn + .transaction_with_behavior(TransactionBehavior::Immediate)?; + if let Some(snapshot) = read_snapshot(&tx, &request.job_id)? { + let stored: String = tx.query_row( + "SELECT fingerprint FROM job_intents WHERE job_id = ?1", + [&request.job_id], + |row| row.get(0), + )?; + if stored != fingerprint || snapshot.request != request { + return Err(JobStoreError::IntentConflict(request.job_id)); + } + tx.commit()?; + return Ok(snapshot); + } + check_serialization(&tx, request.serialization_key.as_deref(), &request.job_id)?; + tx.execute( + "INSERT INTO job_intents + (job_id, request_json, fingerprint, serialization_key, max_concurrent, + state, current_attempt, acknowledged) + VALUES (?1, ?2, ?3, ?4, ?5, 'pending', 1, 0)", + params![ + request.job_id, + request_json, + fingerprint, + request.serialization_key, + request.limits.max_concurrent_jobs + ], + )?; + insert_attempt(&tx, &request, 1)?; + let snapshot = require_snapshot(&tx, &request.job_id)?; + tx.commit()?; + Ok(snapshot) + } + + /// Only definitive failure can create a new immutable attempt. Unknown + /// outcomes require caller reconciliation, never an automatic replay. + pub(crate) fn retry(&mut self, job_id: &str) -> Result { + let tx = self + .conn + .transaction_with_behavior(TransactionBehavior::Immediate)?; + let previous = require_snapshot(&tx, job_id)?; + if previous.state != JobState::Failed || previous.attempt.state != JobAttemptState::Failed { + return Err(JobStoreError::InvalidTransition( + "retry requires known failure", + )); + } + if previous.attempt.number >= previous.request.limits.max_attempts { + return Err(JobStoreError::InvalidTransition("attempt limit reached")); + } + check_serialization(&tx, previous.request.serialization_key.as_deref(), job_id)?; + let next = previous.attempt.number + 1; + insert_attempt(&tx, &previous.request, next)?; + tx.execute( + "UPDATE job_intents SET state = 'pending', current_attempt = ?2 WHERE job_id = ?1", + params![job_id, next], + )?; + let snapshot = require_snapshot(&tx, job_id)?; + tx.commit()?; + Ok(snapshot) + } + + pub(crate) fn get(&self, job_id: &str) -> Result { + require_snapshot(&self.conn, job_id) + } + + /// Only unstarted current attempts are eligible for service startup/resume. + pub(crate) fn pending(&self) -> Result, JobStoreError> { + let mut statement = self.conn.prepare( + "SELECT i.job_id FROM job_intents i JOIN job_attempts a + ON a.job_id = i.job_id AND a.number = i.current_attempt + WHERE i.state = 'pending' AND a.state = 'reserved' ORDER BY i.rowid", + )?; + let ids = statement + .query_map([], |row| row.get::<_, String>(0))? + .collect::>>()?; + ids.into_iter() + .map(|id| require_snapshot(&self.conn, &id)) + .collect() + } + + /// A dispatch is a claim, not an idempotent instruction to spawn again. + /// Repeating it after the claim is rejected, so one attempt has one Worker. + pub(crate) fn dispatch( + &mut self, + job_id: &str, + attempt_id: &str, + max_concurrent: u16, + ) -> Result { + let tx = self + .conn + .transaction_with_behavior(TransactionBehavior::Immediate)?; + let snapshot = require_snapshot(&tx, job_id)?; + check_attempt(&snapshot, attempt_id)?; + if snapshot.state != JobState::Pending + || snapshot.attempt.state != JobAttemptState::Reserved + { + return Err(JobStoreError::InvalidTransition( + "dispatch requires a reserved pending attempt", + )); + } + check_serialization(&tx, snapshot.request.serialization_key.as_deref(), job_id)?; + let (active, active_limit): (i64, Option) = tx.query_row( + "SELECT COUNT(*), MIN(i.max_concurrent) FROM job_attempts a JOIN job_intents i + ON i.job_id = a.job_id AND i.current_attempt = a.number + WHERE a.state = 'dispatched'", + [], + |row| Ok((row.get(0)?, row.get(1)?)), + )?; + // Respect the Host ceiling and every currently executing request's + // ceiling, not just a new request with a more permissive limit. + let limit = max_concurrent + .min(snapshot.request.limits.max_concurrent_jobs) + .min(active_limit.unwrap_or(u16::MAX)); + if active >= i64::from(limit) { + return Err(JobStoreError::ConcurrentLimit); + } + tx.execute( + "UPDATE job_attempts SET state = 'dispatched' WHERE job_id = ?1 AND attempt_id = ?2", + params![job_id, attempt_id], + )?; + let snapshot = require_snapshot(&tx, job_id)?; + tx.commit()?; + Ok(snapshot) + } + + pub(crate) fn accept( + &mut self, + bound: &JobResultSubmission, + ) -> Result { + let tx = self + .conn + .transaction_with_behavior(TransactionBehavior::Immediate)?; + let snapshot = require_snapshot(&tx, &bound.job_id)?; + check_attempt(&snapshot, &bound.attempt_id)?; + if snapshot.request.input_revision != bound.input_revision { + return Err(JobStoreError::RevisionMismatch); + } + let (digest, encoded) = + job::result_digest(&bound.result, snapshot.request.limits.max_result_bytes)?; + if snapshot.state == JobState::Completed + && snapshot.attempt.state == JobAttemptState::Completed + { + let (stored_digest, stored_json): (String, String) = tx.query_row( + "SELECT result_digest, result_json FROM job_attempts WHERE attempt_id = ?1", + [&bound.attempt_id], + |row| Ok((row.get(0)?, row.get(1)?)), + )?; + if digest != stored_digest || encoded != stored_json { + return Err(JobStoreError::ResultConflict); + } + tx.commit()?; + return Ok(snapshot); + } + if snapshot.state != JobState::Pending + || snapshot.attempt.state != JobAttemptState::Dispatched + { + return Err(JobStoreError::InvalidTransition( + "result requires the active dispatched attempt", + )); + } + tx.execute( + "UPDATE job_attempts SET result_json = ?3, result_digest = ?4, + state = 'completed', failure = NULL WHERE job_id = ?1 AND attempt_id = ?2", + params![bound.job_id, bound.attempt_id, encoded, digest], + )?; + tx.execute( + "UPDATE job_intents SET state = 'completed' WHERE job_id = ?1", + [&bound.job_id], + )?; + let snapshot = require_snapshot(&tx, &bound.job_id)?; + tx.commit()?; + Ok(snapshot) + } + + pub(crate) fn finish( + &mut self, + job_id: &str, + attempt_id: &str, + state: JobAttemptState, + detail: Option<&str>, + ) -> Result { + if !state.terminal() { + return Err(JobStoreError::InvalidTransition( + "finish requires a terminal state", + )); + } + let tx = self + .conn + .transaction_with_behavior(TransactionBehavior::Immediate)?; + let snapshot = require_snapshot(&tx, job_id)?; + check_attempt(&snapshot, attempt_id)?; + // Cleanup/failure signals racing a committed result cannot erase success. + if snapshot.state == JobState::Completed || snapshot.attempt.state == state { + tx.commit()?; + return Ok(snapshot); + } + if state == JobAttemptState::Completed { + return Err(JobStoreError::InvalidTransition( + "success requires an accepted result", + )); + } + if snapshot.attempt.state.terminal() { + return Err(JobStoreError::InvalidTransition( + "terminal attempt cannot be reclassified", + )); + } + set_terminal(&tx, &snapshot, state, detail)?; + let snapshot = require_snapshot(&tx, job_id)?; + tx.commit()?; + Ok(snapshot) + } + + /// Cancellation is durable. Cancelling unknown intent must not erase the + /// attempt's uncertain execution evidence or release its serialization fence. + /// A previously accepted result wins and remains success. + pub(crate) fn cancel(&mut self, job_id: &str) -> Result { + let tx = self + .conn + .transaction_with_behavior(TransactionBehavior::Immediate)?; + let snapshot = require_snapshot(&tx, job_id)?; + if snapshot.state == JobState::Unknown { + tx.execute( + "UPDATE job_intents SET state = 'cancelled' WHERE job_id = ?1", + [job_id], + )?; + } else if !matches!(snapshot.state, JobState::Completed | JobState::Cancelled) { + set_terminal( + &tx, + &snapshot, + JobAttemptState::Cancelled, + Some("cancelled"), + )?; + } + let snapshot = require_snapshot(&tx, job_id)?; + tx.commit()?; + Ok(snapshot) + } + + pub(crate) fn ack(&mut self, job_id: &str) -> Result { + let tx = self + .conn + .transaction_with_behavior(TransactionBehavior::Immediate)?; + let snapshot = require_snapshot(&tx, job_id)?; + if snapshot.state != JobState::Completed { + return Err(JobStoreError::InvalidTransition( + "acknowledgement requires a completed Job", + )); + } + tx.execute( + "UPDATE job_intents SET acknowledged = 1 WHERE job_id = ?1", + [job_id], + )?; + let snapshot = require_snapshot(&tx, job_id)?; + tx.commit()?; + Ok(snapshot) + } +} + +fn check_attempt(snapshot: &JobSnapshot, attempt_id: &str) -> Result<(), JobStoreError> { + if snapshot.attempt.attempt_id != attempt_id { + return Err(JobStoreError::AttemptMismatch); + } + Ok(()) +} + +fn check_serialization( + conn: &Connection, + key: Option<&str>, + job_id: &str, +) -> Result<(), JobStoreError> { + if let Some(key) = key { + let conflicting: bool = conn.query_row( + "SELECT EXISTS(SELECT 1 FROM job_intents i WHERE serialization_key = ?1 + AND job_id <> ?2 AND (state IN ('pending', 'unknown') OR EXISTS + (SELECT 1 FROM job_attempts a WHERE a.job_id = i.job_id AND a.state = 'unknown')))", + params![key, job_id], + |row| row.get(0), + )?; + if conflicting { + return Err(JobStoreError::SerializationConflict(key.to_owned())); + } + } + Ok(()) +} + +fn insert_attempt( + conn: &Connection, + request: &JobRequest, + number: u8, +) -> Result<(), JobStoreError> { + conn.execute( + "INSERT INTO job_attempts (job_id, number, attempt_id, input_revision, state) + VALUES (?1, ?2, ?3, ?4, 'reserved')", + params![ + request.job_id, + number, + job::attempt_id(&request.job_id, number), + request.input_revision + ], + )?; + Ok(()) +} + +fn set_terminal( + conn: &Connection, + snapshot: &JobSnapshot, + state: JobAttemptState, + detail: Option<&str>, +) -> Result<(), JobStoreError> { + let detail = detail.map(job::bounded_failure_detail); + conn.execute( + "UPDATE job_attempts SET state = ?3, failure = ?4 WHERE job_id = ?1 AND attempt_id = ?2", + params![ + snapshot.request.job_id, + snapshot.attempt.attempt_id, + state.as_str(), + detail + ], + )?; + conn.execute( + "UPDATE job_intents SET state = ?2 WHERE job_id = ?1", + params![snapshot.request.job_id, state.as_str()], + )?; + Ok(()) +} + +fn require_snapshot(conn: &Connection, job_id: &str) -> Result { + read_snapshot(conn, job_id)?.ok_or_else(|| JobStoreError::NotFound(job_id.to_owned())) +} + +fn read_snapshot(conn: &Connection, job_id: &str) -> Result, JobStoreError> { + let row = conn + .query_row( + "SELECT i.request_json, i.state, i.acknowledged, a.attempt_id, a.number, + a.state, a.failure, a.result_json FROM job_intents i JOIN job_attempts a + ON a.job_id = i.job_id AND a.number = i.current_attempt WHERE i.job_id = ?1", + [job_id], + |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, bool>(2)?, + row.get::<_, String>(3)?, + row.get::<_, u8>(4)?, + row.get::<_, String>(5)?, + row.get::<_, Option>(6)?, + row.get::<_, Option>(7)?, + )) + }, + ) + .optional()?; + row.map( + |(request, state, acknowledged, attempt_id, number, attempt_state, failure, result)| { + let state = match state.as_str() { + "pending" => JobState::Pending, + "completed" => JobState::Completed, + "failed" => JobState::Failed, + "unknown" => JobState::Unknown, + "cancelled" => JobState::Cancelled, + _ => { + return Err(JobStoreError::Corrupt(format!( + "unknown intent state `{state}`" + ))); + } + }; + let attempt_state = match attempt_state.as_str() { + "reserved" => JobAttemptState::Reserved, + "dispatched" => JobAttemptState::Dispatched, + "completed" => JobAttemptState::Completed, + "failed" => JobAttemptState::Failed, + "unknown" => JobAttemptState::Unknown, + "cancelled" => JobAttemptState::Cancelled, + _ => { + return Err(JobStoreError::Corrupt(format!( + "unknown attempt state `{attempt_state}`" + ))); + } + }; + Ok(JobSnapshot { + request: serde_json::from_str(&request)?, + state, + acknowledged, + attempt: JobAttempt { + attempt_id, + number, + state: attempt_state, + failure, + }, + result: result.map(|json| serde_json::from_str(&json)).transpose()?, + }) + }, + ) + .transpose() +} + +const SCHEMA: &str = " +CREATE TABLE job_intents ( + job_id TEXT PRIMARY KEY NOT NULL, + request_json TEXT NOT NULL, + fingerprint TEXT NOT NULL, + serialization_key TEXT, + max_concurrent INTEGER NOT NULL CHECK (max_concurrent BETWEEN 1 AND 65535), + state TEXT NOT NULL CHECK (state IN ('pending', 'completed', 'failed', 'unknown', 'cancelled')), + current_attempt INTEGER NOT NULL CHECK (current_attempt BETWEEN 1 AND 255), + acknowledged INTEGER NOT NULL DEFAULT 0 CHECK (acknowledged IN (0, 1)), + CHECK (acknowledged = 0 OR state = 'completed'), + FOREIGN KEY (job_id, current_attempt) REFERENCES job_attempts(job_id, number) + DEFERRABLE INITIALLY DEFERRED +); +CREATE TABLE job_attempts ( + job_id TEXT NOT NULL REFERENCES job_intents(job_id), + number INTEGER NOT NULL CHECK (number BETWEEN 1 AND 255), + attempt_id TEXT NOT NULL UNIQUE, + input_revision TEXT NOT NULL, + state TEXT NOT NULL CHECK (state IN ('reserved', 'dispatched', 'completed', 'failed', 'unknown', 'cancelled')), + result_json TEXT, + result_digest TEXT, + failure TEXT, + PRIMARY KEY (job_id, number), + CHECK ((result_json IS NULL) = (result_digest IS NULL)), + CHECK (state <> 'completed' OR result_json IS NOT NULL) +); +CREATE UNIQUE INDEX job_serialization_singleflight ON job_intents(serialization_key) + WHERE serialization_key IS NOT NULL AND state IN ('pending', 'unknown'); +CREATE INDEX job_attempt_state ON job_attempts(state); +CREATE TRIGGER job_intent_immutable BEFORE UPDATE OF + job_id, request_json, fingerprint, serialization_key, max_concurrent ON job_intents +BEGIN SELECT RAISE(ABORT, 'immutable Job intent'); END; +CREATE TRIGGER job_attempt_immutable BEFORE UPDATE OF + job_id, number, attempt_id, input_revision ON job_attempts +BEGIN SELECT RAISE(ABORT, 'immutable Job attempt identity'); END; +CREATE TRIGGER job_result_immutable BEFORE UPDATE OF result_json, result_digest ON job_attempts +WHEN OLD.result_json IS NOT NULL AND + (NEW.result_json IS NOT OLD.result_json OR NEW.result_digest IS NOT OLD.result_digest) +BEGIN SELECT RAISE(ABORT, 'immutable accepted Job result'); END; +"; + +#[cfg(test)] +mod tests { + use super::*; + use job::JobLimits; + use serde_json::json; + use std::sync::{Arc, Barrier}; + use tempfile::TempDir; + + fn request(id: &str) -> JobRequest { + JobRequest { + job_id: id.into(), + purpose: "fixture".into(), + input_revision: "revision-1".into(), + input_ref: "fixture:input".into(), + input: json!({"immutable": [1, 2, 3]}), + instruction: "Produce a structured result".into(), + profile: "builtin:default".into(), + serialization_key: None, + limits: JobLimits::default(), + } + } + + fn fixture() -> (TempDir, JobStore) { + let dir = tempfile::tempdir().unwrap(); + let store = JobStore::open(&dir.path().join("jobs.sqlite3")).unwrap(); + (dir, store) + } + + fn submission(snapshot: &JobSnapshot, result: Value) -> JobResultSubmission { + JobResultSubmission { + job_id: snapshot.request.job_id.clone(), + attempt_id: snapshot.attempt.attempt_id.clone(), + input_revision: snapshot.request.input_revision.clone(), + result, + } + } + + fn dispatched(store: &mut JobStore, request: JobRequest) -> JobSnapshot { + let reserved = store.reserve(request).unwrap(); + store + .dispatch(&reserved.request.job_id, &reserved.attempt.attempt_id, 8) + .unwrap() + } + + #[test] + fn database_configuration_schema_and_identity_constraints() { + let (_dir, mut store) = fixture(); + let version: i64 = store + .conn + .pragma_query_value(None, "user_version", |r| r.get(0)) + .unwrap(); + let foreign_keys: i64 = store + .conn + .pragma_query_value(None, "foreign_keys", |r| r.get(0)) + .unwrap(); + let synchronous: i64 = store + .conn + .pragma_query_value(None, "synchronous", |r| r.get(0)) + .unwrap(); + let journal: String = store + .conn + .pragma_query_value(None, "journal_mode", |r| r.get(0)) + .unwrap(); + assert_eq!((version, foreign_keys, synchronous), (SCHEMA_VERSION, 1, 2)); + assert_eq!(journal, "wal"); + store.reserve(request("identity")).unwrap(); + for sql in [ + "UPDATE job_intents SET request_json = '{}' WHERE job_id = 'identity'", + "UPDATE job_intents SET fingerprint = 'changed' WHERE job_id = 'identity'", + "UPDATE job_attempts SET input_revision = 'changed' WHERE job_id = 'identity'", + "UPDATE job_attempts SET attempt_id = 'changed' WHERE job_id = 'identity'", + "UPDATE job_intents SET current_attempt = 2 WHERE job_id = 'identity'", + "INSERT INTO job_attempts (job_id, number, attempt_id, input_revision, state) + VALUES ('absent', 1, 'absent:attempt:1', 'r', 'reserved')", + ] { + assert!( + store.conn.execute_batch(sql).is_err(), + "constraint accepted {sql}" + ); + } + let mut check = store.conn.prepare("PRAGMA foreign_key_check").unwrap(); + assert!(!check.exists([]).unwrap()); + } + + #[test] + fn schema_version_is_rejected_without_recovery_or_upgrade() { + let (dir, mut store) = fixture(); + dispatched(&mut store, request("running")); + store.conn.pragma_update(None, "user_version", 42).unwrap(); + drop(store); + let path = dir.path().join("jobs.sqlite3"); + assert!(matches!( + JobStore::open(&path), + Err(JobStoreError::UnsupportedSchema(42)) + )); + let conn = Connection::open(path).unwrap(); + let state: String = conn + .query_row("SELECT state FROM job_attempts", [], |r| r.get(0)) + .unwrap(); + assert_eq!(state, "dispatched"); + assert_eq!( + conn.pragma_query_value(None, "user_version", |r| r.get::<_, i64>(0)) + .unwrap(), + 42 + ); + } + + #[test] + fn exact_replay_is_stable_and_every_intent_change_conflicts() { + let (dir, mut store) = fixture(); + assert!(matches!( + store.get("absent"), + Err(JobStoreError::NotFound(_)) + )); + let original = request("replay"); + let snapshot = store.reserve(original.clone()).unwrap(); + assert_eq!(snapshot.state, JobState::Pending); + assert_eq!(snapshot.attempt.number, 1); + assert_eq!(snapshot.attempt.attempt_id, job::attempt_id("replay", 1)); + assert_eq!(snapshot.attempt.state, JobAttemptState::Reserved); + assert_eq!(snapshot.result, None); + assert!(!snapshot.acknowledged); + assert_eq!(store.reserve(original.clone()).unwrap(), snapshot); + let mut variants = Vec::new(); + let mut changed = original.clone(); + changed.input_revision = "revision-2".into(); + variants.push(changed); + let mut changed = original.clone(); + changed.input = json!({"changed": true}); + variants.push(changed); + let mut changed = original.clone(); + changed.input_ref = "other:input".into(); + variants.push(changed); + let mut changed = original.clone(); + changed.profile = "builtin:coder".into(); + variants.push(changed); + let mut changed = original.clone(); + changed.instruction = "Different instruction".into(); + variants.push(changed); + let mut changed = original.clone(); + changed.purpose = "other_purpose".into(); + variants.push(changed); + let mut changed = original.clone(); + changed.serialization_key = Some("key".into()); + variants.push(changed); + let mut changed = original.clone(); + changed.limits.max_attempts = 3; + variants.push(changed); + for variant in variants { + assert!(matches!( + store.reserve(variant), + Err(JobStoreError::IntentConflict(_)) + )); + } + drop(store); + let mut reopened = JobStore::open(&dir.path().join("jobs.sqlite3")).unwrap(); + assert_eq!(reopened.reserve(original).unwrap(), snapshot); + assert_eq!(reopened.pending().unwrap(), vec![snapshot]); + let count: i64 = reopened + .conn + .query_row("SELECT COUNT(*) FROM job_attempts", [], |r| r.get(0)) + .unwrap(); + assert_eq!(count, 1); + } + + #[test] + fn invalid_requests_never_create_intent_or_attempt() { + let (_dir, mut store) = fixture(); + let mut invalid = request("invalid"); + invalid.limits.max_attempts = 0; + assert!(matches!(store.reserve(invalid), Err(JobStoreError::Job(_)))); + let mut invalid = request("invalid"); + invalid.profile = "/tmp/raw-profile.toml".into(); + assert!(matches!(store.reserve(invalid), Err(JobStoreError::Job(_)))); + let mut invalid = request("invalid"); + invalid.input = json!("x".repeat(job::MAX_JOB_INPUT_BYTES)); + assert!(matches!(store.reserve(invalid), Err(JobStoreError::Job(_)))); + assert!(store.pending().unwrap().is_empty()); + assert!(matches!( + store.get("invalid"), + Err(JobStoreError::NotFound(_)) + )); + } + + #[test] + fn retry_is_explicit_bounded_and_preserves_prior_attempt() { + let (dir, mut store) = fixture(); + let mut original = request("retry"); + original.limits.max_attempts = 2; + let first = dispatched(&mut store, original.clone()); + assert!(store.retry("retry").is_err()); + let failed = store + .finish( + "retry", + &first.attempt.attempt_id, + JobAttemptState::Failed, + Some("timeout"), + ) + .unwrap(); + assert_eq!(store.reserve(original.clone()).unwrap(), failed); + let second = store.retry("retry").unwrap(); + assert_eq!(second.request, original); + assert_eq!(second.attempt.number, 2); + assert_eq!(second.attempt.state, JobAttemptState::Reserved); + assert_eq!(second.attempt.failure, None); + assert_eq!(second.result, None); + assert!(matches!( + store.accept(&submission(&first, json!({}))), + Err(JobStoreError::AttemptMismatch) + )); + assert!(matches!( + store.finish( + "retry", + &first.attempt.attempt_id, + JobAttemptState::Failed, + None + ), + Err(JobStoreError::AttemptMismatch) + )); + assert!(matches!( + store.dispatch("retry", &first.attempt.attempt_id, 8), + Err(JobStoreError::AttemptMismatch) + )); + let prior: (String, String, String) = store.conn.query_row( + "SELECT state, failure, input_revision FROM job_attempts WHERE job_id = 'retry' AND number = 1", + [], |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)), + ).unwrap(); + assert_eq!( + prior, + ("failed".into(), "timeout".into(), "revision-1".into()) + ); + store + .finish( + "retry", + &second.attempt.attempt_id, + JobAttemptState::Failed, + None, + ) + .unwrap(); + assert!(matches!( + store.retry("retry"), + Err(JobStoreError::InvalidTransition("attempt limit reached")) + )); + drop(store); + let reopened = JobStore::open(&dir.path().join("jobs.sqlite3")).unwrap(); + assert_eq!(reopened.get("retry").unwrap().attempt.number, 2); + } + + #[test] + fn result_fences_size_and_replay_are_enforced_transactionally() { + let (_dir, mut store) = fixture(); + let mut req = request("result"); + req.limits.max_result_bytes = 32; + let reserved = store.reserve(req).unwrap(); + let valid = submission(&reserved, json!({"ok": true})); + assert!(store.accept(&valid).is_err()); + let active = store + .dispatch("result", &reserved.attempt.attempt_id, 8) + .unwrap(); + assert!(store.pending().unwrap().is_empty()); + let mut invalid = valid.clone(); + invalid.attempt_id = "wrong-attempt".into(); + assert!(matches!( + store.accept(&invalid), + Err(JobStoreError::AttemptMismatch) + )); + let mut invalid = valid.clone(); + invalid.input_revision = "revision-2".into(); + assert!(matches!( + store.accept(&invalid), + Err(JobStoreError::RevisionMismatch) + )); + let mut invalid = valid.clone(); + invalid.result = json!("x".repeat(32)); + assert!(matches!(store.accept(&invalid), Err(JobStoreError::Job(_)))); + assert_eq!(store.get("result").unwrap(), active); + let completed = store.accept(&valid).unwrap(); + assert_eq!(completed.state, JobState::Completed); + assert_eq!(completed.attempt.state, JobAttemptState::Completed); + assert_eq!(completed.result, Some(valid.result.clone())); + assert_eq!(store.accept(&valid).unwrap(), completed); + let (digest, json): (String, String) = store + .conn + .query_row( + "SELECT result_digest, result_json FROM job_attempts WHERE job_id = 'result'", + [], + |r| Ok((r.get(0)?, r.get(1)?)), + ) + .unwrap(); + assert_eq!( + (digest, json), + job::result_digest(&valid.result, 32).unwrap() + ); + let mut changed = valid.clone(); + changed.result = json!({"ok": false}); + assert!(matches!( + store.accept(&changed), + Err(JobStoreError::ResultConflict) + )); + assert!(store.conn.execute("UPDATE job_attempts SET result_json = 'null', result_digest = 'changed' WHERE job_id = 'result'", []).is_err()); + assert_eq!(store.get("result").unwrap(), completed); + } + + #[test] + fn exact_byte_boundary_and_json_null_are_accepted() { + let (_dir, mut store) = fixture(); + let mut req = request("boundary"); + req.limits.max_result_bytes = 4; + let active = dispatched(&mut store, req); + let completed = store.accept(&submission(&active, Value::Null)).unwrap(); + assert_eq!(completed.result, Some(Value::Null)); + let mut req = request("utf8"); + req.limits.max_result_bytes = 4; + let active = dispatched(&mut store, req); + assert!(store.accept(&submission(&active, json!("猫"))).is_err()); + assert_eq!(store.get("utf8").unwrap().state, JobState::Pending); + } + + #[test] + fn only_structured_result_is_success_and_cleanup_cannot_erase_it() { + let (_dir, mut store) = fixture(); + let active = dispatched(&mut store, request("success")); + for state in [ + JobAttemptState::Completed, + JobAttemptState::Reserved, + JobAttemptState::Dispatched, + ] { + assert!( + store + .finish( + "success", + &active.attempt.attempt_id, + state, + Some("final prose") + ) + .is_err() + ); + } + assert!(store.ack("success").is_err()); + let completed = store + .accept(&submission(&active, json!({"value": 1}))) + .unwrap(); + for state in [ + JobAttemptState::Failed, + JobAttemptState::Unknown, + JobAttemptState::Cancelled, + JobAttemptState::Completed, + ] { + assert_eq!( + store + .finish( + "success", + &active.attempt.attempt_id, + state, + Some("cleanup error") + ) + .unwrap(), + completed + ); + } + assert_eq!(store.cancel("success").unwrap(), completed); + assert!(store.retry("success").is_err()); + } + + #[test] + fn consumer_acknowledgement_is_independent_durable_and_idempotent() { + let (dir, mut store) = fixture(); + let active = dispatched(&mut store, request("ack")); + let accepted = store + .accept(&submission(&active, json!({"result": "retained"}))) + .unwrap(); + assert!(!accepted.acknowledged); + drop(store); + let mut reopened = JobStore::open(&dir.path().join("jobs.sqlite3")).unwrap(); + assert_eq!(reopened.get("ack").unwrap(), accepted.clone()); + assert!(reopened.pending().unwrap().is_empty()); + assert!(reopened.retry("ack").is_err()); + let acknowledged = reopened.ack("ack").unwrap(); + assert!(acknowledged.acknowledged); + assert_eq!(acknowledged.result, accepted.result); + assert_eq!(reopened.ack("ack").unwrap(), acknowledged); + assert_eq!( + reopened + .accept(&submission(&active, json!({"result": "retained"}))) + .unwrap(), + acknowledged + ); + drop(reopened); + let mut reopened = JobStore::open(&dir.path().join("jobs.sqlite3")).unwrap(); + assert_eq!(reopened.reserve(request("ack")).unwrap(), acknowledged); + } + + #[test] + fn reopen_keeps_reserved_and_marks_unaccepted_dispatch_unknown() { + let (dir, mut store) = fixture(); + let pending = store.reserve(request("pending")).unwrap(); + let active = dispatched(&mut store, request("interrupted")); + let failed = store.reserve(request("failed")).unwrap(); + store + .finish( + "failed", + &failed.attempt.attempt_id, + JobAttemptState::Failed, + Some("known failure"), + ) + .unwrap(); + drop(store); + let mut reopened = JobStore::open(&dir.path().join("jobs.sqlite3")).unwrap(); + assert_eq!(reopened.pending().unwrap(), vec![pending.clone()]); + assert_eq!(reopened.get("pending").unwrap(), pending); + let unknown = reopened.get("interrupted").unwrap(); + assert_eq!(unknown.state, JobState::Unknown); + assert_eq!(unknown.attempt.state, JobAttemptState::Unknown); + assert_eq!(unknown.result, None); + assert!(unknown.attempt.failure.is_some()); + assert_eq!(reopened.reserve(request("interrupted")).unwrap(), unknown); + assert!(reopened.retry("interrupted").is_err()); + assert!( + reopened + .accept(&submission(&active, json!({"late": true}))) + .is_err() + ); + assert!( + reopened + .dispatch("interrupted", &active.attempt.attempt_id, 8) + .is_err() + ); + assert_eq!(reopened.get("failed").unwrap().state, JobState::Failed); + drop(reopened); + let reopened = JobStore::open(&dir.path().join("jobs.sqlite3")).unwrap(); + assert_eq!(reopened.get("interrupted").unwrap(), unknown); + } + + #[test] + fn recovery_retains_accepted_result_in_dispatched_fixture() { + let (dir, mut store) = fixture(); + let active = dispatched(&mut store, request("accepted-fixture")); + let bound = submission(&active, json!({"accepted": true})); + // Explicit recovery fixture, not a possible half-commit of accept(): + // accept() updates both states and the result in one transaction. + let (digest, encoded) = job::result_digest(&bound.result, 1024).unwrap(); + store.conn.execute( + "UPDATE job_attempts SET result_json = ?1, result_digest = ?2 WHERE attempt_id = ?3", + params![encoded, digest, bound.attempt_id], + ).unwrap(); + drop(store); + let mut reopened = JobStore::open(&dir.path().join("jobs.sqlite3")).unwrap(); + let retained = reopened.get("accepted-fixture").unwrap(); + assert_eq!(retained.state, JobState::Completed); + assert_eq!(retained.attempt.state, JobAttemptState::Completed); + assert_eq!(retained.result, Some(bound.result.clone())); + assert_eq!(reopened.accept(&bound).unwrap(), retained); + assert!(reopened.pending().unwrap().is_empty()); + } + + #[test] + fn cancellation_never_resumes_and_rejects_late_results() { + let (dir, mut store) = fixture(); + for (id, start) in [("reserved-cancel", false), ("dispatched-cancel", true)] { + let snapshot = if start { + dispatched(&mut store, request(id)) + } else { + store.reserve(request(id)).unwrap() + }; + let cancelled = store.cancel(id).unwrap(); + assert_eq!(cancelled.state, JobState::Cancelled); + assert_eq!(cancelled.attempt.state, JobAttemptState::Cancelled); + assert_eq!(store.cancel(id).unwrap(), cancelled); + assert_eq!(store.reserve(request(id)).unwrap(), cancelled); + assert!(store.retry(id).is_err()); + assert!(store.dispatch(id, &snapshot.attempt.attempt_id, 8).is_err()); + assert!(store.accept(&submission(&snapshot, json!({}))).is_err()); + assert!( + store + .finish( + id, + &snapshot.attempt.attempt_id, + JobAttemptState::Failed, + None + ) + .is_err() + ); + assert!(store.ack(id).is_err()); + } + drop(store); + let mut reopened = JobStore::open(&dir.path().join("jobs.sqlite3")).unwrap(); + assert!(reopened.pending().unwrap().is_empty()); + for id in ["reserved-cancel", "dispatched-cancel"] { + assert_eq!( + reopened.reserve(request(id)).unwrap().state, + JobState::Cancelled + ); + } + } + + #[test] + fn serialization_fences_pending_unknown_and_conflicting_retry() { + let (dir, mut store) = fixture(); + let mut first = request("first"); + first.serialization_key = Some("resource".into()); + let mut second = request("second"); + second.serialization_key = Some("resource".into()); + let reserved = store.reserve(first.clone()).unwrap(); + assert_eq!(store.reserve(first.clone()).unwrap(), reserved); + assert!(matches!( + store.reserve(second.clone()), + Err(JobStoreError::SerializationConflict(_)) + )); + store + .dispatch("first", &reserved.attempt.attempt_id, 8) + .unwrap(); + drop(store); + let mut reopened = JobStore::open(&dir.path().join("jobs.sqlite3")).unwrap(); + assert!(matches!( + reopened.reserve(second.clone()), + Err(JobStoreError::SerializationConflict(_)) + )); + let cancelled_unknown = reopened.cancel("first").unwrap(); + assert_eq!(cancelled_unknown.state, JobState::Cancelled); + assert_eq!(cancelled_unknown.attempt.state, JobAttemptState::Unknown); + assert!(matches!( + reopened.reserve(second.clone()), + Err(JobStoreError::SerializationConflict(_)) + )); + assert!(reopened.retry("first").is_err()); + // Another independent resource is not fenced by this uncertain attempt. + second.serialization_key = Some("independent-resource".into()); + let second_snapshot = reopened.reserve(second.clone()).unwrap(); + reopened + .finish( + "second", + &second_snapshot.attempt.attempt_id, + JobAttemptState::Failed, + None, + ) + .unwrap(); + let mut third = request("third"); + third.serialization_key = Some("independent-resource".into()); + reopened.reserve(third).unwrap(); + assert!(matches!( + reopened.retry("second"), + Err(JobStoreError::SerializationConflict(_)) + )); + assert_eq!(reopened.get("second").unwrap().attempt.number, 1); + reopened.cancel("third").unwrap(); + assert_eq!(reopened.retry("second").unwrap().attempt.number, 2); + } + + #[test] + fn dispatch_enforces_host_and_all_active_request_limits_without_double_start() { + let (_dir, mut store) = fixture(); + let mut strict = request("strict"); + strict.limits.max_concurrent_jobs = 1; + let strict = store.reserve(strict).unwrap(); + let other = store.reserve(request("other")).unwrap(); + assert!(matches!( + store.dispatch("strict", &strict.attempt.attempt_id, 0), + Err(JobStoreError::ConcurrentLimit) + )); + store + .dispatch("strict", &strict.attempt.attempt_id, 8) + .unwrap(); + assert!(matches!( + store.dispatch("other", &other.attempt.attempt_id, 8), + Err(JobStoreError::ConcurrentLimit) + )); + assert!( + store + .dispatch("strict", &strict.attempt.attempt_id, 8) + .is_err() + ); + store + .finish( + "strict", + &strict.attempt.attempt_id, + JobAttemptState::Failed, + None, + ) + .unwrap(); + store + .dispatch("other", &other.attempt.attempt_id, 8) + .unwrap(); + let strict = store.retry("strict").unwrap(); + assert!(matches!( + store.dispatch("strict", &strict.attempt.attempt_id, 8), + Err(JobStoreError::ConcurrentLimit) + )); + let host_bound = store.reserve(request("host-bound")).unwrap(); + assert!(matches!( + store.dispatch("host-bound", &host_bound.attempt.attempt_id, 1), + Err(JobStoreError::ConcurrentLimit) + )); + assert_eq!( + store.get("host-bound").unwrap().attempt.state, + JobAttemptState::Reserved + ); + store.accept(&submission(&other, json!({}))).unwrap(); + store + .dispatch("host-bound", &host_bound.attempt.attempt_id, 1) + .unwrap(); + } + + #[test] + fn competing_transactions_cannot_exceed_host_dispatch_limit() { + let (dir, mut one) = fixture(); + // Open all fixture connections before dispatch: production has one + // Host-leased connection and never invokes recovery beside live work. + let mut two = JobStore::open(&dir.path().join("jobs.sqlite3")).unwrap(); + let first = one.reserve(request("one")).unwrap(); + let second = two.reserve(request("two")).unwrap(); + let barrier = Arc::new(Barrier::new(2)); + let threads = [(one, first), (two, second)] + .into_iter() + .map(|(mut store, snapshot)| { + let barrier = barrier.clone(); + std::thread::spawn(move || { + barrier.wait(); + store.dispatch(&snapshot.request.job_id, &snapshot.attempt.attempt_id, 1) + }) + }) + .collect::>(); + let results = threads + .into_iter() + .map(|thread| thread.join().unwrap()) + .collect::>(); + assert_eq!(results.iter().filter(|r| r.is_ok()).count(), 1); + assert_eq!( + results + .iter() + .filter(|r| matches!(r, Err(JobStoreError::ConcurrentLimit))) + .count(), + 1 + ); + let conn = Connection::open(dir.path().join("jobs.sqlite3")).unwrap(); + let count: i64 = conn + .query_row( + "SELECT COUNT(*) FROM job_attempts WHERE state = 'dispatched'", + [], + |r| r.get(0), + ) + .unwrap(); + assert_eq!(count, 1); + } + + #[test] + fn competing_reservations_enforce_singleflight_without_partial_intent() { + let (dir, one) = fixture(); + let two = JobStore::open(&dir.path().join("jobs.sqlite3")).unwrap(); + let barrier = Arc::new(Barrier::new(2)); + let threads = [(one, "one"), (two, "two")] + .into_iter() + .map(|(mut store, id)| { + let barrier = barrier.clone(); + std::thread::spawn(move || { + let mut req = request(id); + req.serialization_key = Some("singleflight".into()); + barrier.wait(); + store.reserve(req) + }) + }) + .collect::>(); + let results = threads + .into_iter() + .map(|thread| thread.join().unwrap()) + .collect::>(); + assert_eq!(results.iter().filter(|r| r.is_ok()).count(), 1); + assert_eq!( + results + .iter() + .filter(|r| matches!(r, Err(JobStoreError::SerializationConflict(_)))) + .count(), + 1 + ); + let reopened = JobStore::open(&dir.path().join("jobs.sqlite3")).unwrap(); + assert_eq!(reopened.pending().unwrap().len(), 1); + let count: i64 = reopened + .conn + .query_row("SELECT COUNT(*) FROM job_attempts", [], |r| r.get(0)) + .unwrap(); + assert_eq!(count, 1); + } + + #[test] + fn failure_detail_is_utf8_bounded_and_terminal_classification_is_stable() { + let (_dir, mut store) = fixture(); + let reserved = store.reserve(request("failure")).unwrap(); + let failed = store + .finish( + "failure", + &reserved.attempt.attempt_id, + JobAttemptState::Failed, + Some(&"猫".repeat(1024)), + ) + .unwrap(); + let detail = failed.attempt.failure.as_ref().unwrap(); + assert!(detail.len() <= job::MAX_JOB_FAILURE_BYTES); + assert!(detail.ends_with('猫')); + assert_eq!( + store + .finish( + "failure", + &reserved.attempt.attempt_id, + JobAttemptState::Failed, + Some("different detail") + ) + .unwrap(), + failed + ); + assert!( + store + .finish( + "failure", + &reserved.attempt.attempt_id, + JobAttemptState::Unknown, + None + ) + .is_err() + ); + assert!( + store + .finish( + "failure", + &reserved.attempt.attempt_id, + JobAttemptState::Completed, + None + ) + .is_err() + ); + assert!(store.accept(&submission(&reserved, json!({}))).is_err()); + assert!(store.ack("failure").is_err()); + assert_eq!(store.cancel("failure").unwrap().state, JobState::Cancelled); + assert!(store.retry("failure").is_err()); + } + + #[test] + fn injected_sql_failures_roll_back_reserve_result_and_retry() { + let (_dir, mut store) = fixture(); + store + .conn + .execute_batch( + "CREATE TRIGGER fixture_abort_attempt BEFORE INSERT ON job_attempts + BEGIN SELECT RAISE(ABORT, 'fixture insertion failure'); END;", + ) + .unwrap(); + assert!(matches!( + store.reserve(request("atomic")), + Err(JobStoreError::Sqlite(_)) + )); + assert!(store.get("atomic").is_err()); + store + .conn + .execute_batch("DROP TRIGGER fixture_abort_attempt") + .unwrap(); + let active = dispatched(&mut store, request("atomic")); + store + .conn + .execute_batch( + "CREATE TRIGGER fixture_abort_intent BEFORE UPDATE OF state ON job_intents + BEGIN SELECT RAISE(ABORT, 'fixture intent failure'); END;", + ) + .unwrap(); + let bound = submission(&active, json!({"atomic": true})); + assert!(matches!( + store.accept(&bound), + Err(JobStoreError::Sqlite(_)) + )); + assert_eq!(store.get("atomic").unwrap(), active); + assert!(matches!( + store.cancel("atomic"), + Err(JobStoreError::Sqlite(_)) + )); + assert_eq!(store.get("atomic").unwrap(), active); + store + .conn + .execute_batch("DROP TRIGGER fixture_abort_intent") + .unwrap(); + let failed = store + .finish( + "atomic", + &active.attempt.attempt_id, + JobAttemptState::Failed, + Some("known failure"), + ) + .unwrap(); + store + .conn + .execute_batch( + "CREATE TRIGGER fixture_abort_intent BEFORE UPDATE OF state ON job_intents + BEGIN SELECT RAISE(ABORT, 'fixture intent failure'); END;", + ) + .unwrap(); + assert!(matches!( + store.retry("atomic"), + Err(JobStoreError::Sqlite(_)) + )); + assert_eq!(store.get("atomic").unwrap(), failed); + let count: i64 = store + .conn + .query_row("SELECT COUNT(*) FROM job_attempts", [], |r| r.get(0)) + .unwrap(); + assert_eq!(count, 1); + store + .conn + .execute_batch("DROP TRIGGER fixture_abort_intent") + .unwrap(); + let next = store.retry("atomic").unwrap(); + store + .dispatch("atomic", &next.attempt.attempt_id, 8) + .unwrap(); + assert_eq!( + store + .accept(&submission(&next, bound.result)) + .unwrap() + .state, + JobState::Completed + ); + } + + #[test] + fn unknown_job_operations_do_not_create_records() { + let (_dir, mut store) = fixture(); + assert!(matches!( + store.retry("missing"), + Err(JobStoreError::NotFound(_)) + )); + assert!(matches!( + store.dispatch("missing", "attempt", 8), + Err(JobStoreError::NotFound(_)) + )); + assert!(matches!( + store.finish("missing", "attempt", JobAttemptState::Failed, None), + Err(JobStoreError::NotFound(_)) + )); + assert!(matches!( + store.cancel("missing"), + Err(JobStoreError::NotFound(_)) + )); + assert!(matches!( + store.ack("missing"), + Err(JobStoreError::NotFound(_)) + )); + let absent = JobResultSubmission { + job_id: "missing".into(), + attempt_id: "attempt".into(), + input_revision: "r".into(), + result: json!({}), + }; + assert!(matches!( + store.accept(&absent), + Err(JobStoreError::NotFound(_)) + )); + assert!(store.pending().unwrap().is_empty()); + } +} diff --git a/crates/standalone/src/jobs.rs b/crates/standalone/src/jobs.rs new file mode 100644 index 000000000..95a198efe --- /dev/null +++ b/crates/standalone/src/jobs.rs @@ -0,0 +1,393 @@ +//! Host-provided local Jobs. No Runtime registry, Workspace client or dialog-session state. +#[cfg(test)] +mod tests; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use agen::llm_client::LlmClient; +use job::{JobRequest, JobResultSubmission}; +use tokio::sync::watch; + +use crate::job_store::JobStore; +pub use crate::job_store::{JobAttempt, JobAttemptState, JobSnapshot, JobState, JobStoreError}; + +#[derive(Debug, thiserror::Error)] +pub enum JobServiceError { + #[error(transparent)] + Store(#[from] JobStoreError), + #[error("standalone Job execution requirements are unavailable: {0}")] + Configuration(String), + #[error("the standalone Job Host is shutting down")] + Closed, + #[error("standalone Job execution capacity is exhausted")] + Capacity, + #[error("standalone Job cleanup failed: {0}")] + Cleanup(String), +} + +struct Execution { + cancel: watch::Sender, + complete: watch::Receiver>>, + attempt_id: String, + resource_key: Option, + max_concurrent: u16, + task: tokio::task::JoinHandle>, +} +struct Executions { + closed: bool, + jobs: HashMap, +} + +/// Cloneable Feature-facing access to a Host-owned service. Cloning this handle +/// neither extends the Host's authority nor permits opening/recovering its store. +#[derive(Clone)] +pub struct StandaloneJobs { + store: Arc>>, + executions: Arc>, + cwd: PathBuf, + capacity: u16, +} + +impl StandaloneJobs { + pub(crate) fn open(path: &Path, cwd: PathBuf) -> Result { + Ok(Self { + store: Arc::new(Mutex::new(Some(JobStore::open(path)?))), + executions: Arc::new(Mutex::new(Executions { + closed: false, + jobs: HashMap::new(), + })), + cwd, + capacity: job::DEFAULT_MAX_CONCURRENT_JOBS, + }) + } + + fn with_store( + &self, + operation: impl FnOnce(&mut JobStore) -> Result, + ) -> Result { + let mut slot = self.store.lock().expect("Job store poisoned"); + Ok(operation(slot.as_mut().ok_or(JobServiceError::Closed)?)?) + } + + /// Persist intent without starting a model. Exact resend returns the same + /// current attempt, including after acknowledgement or cancellation. + pub fn request(&self, request: JobRequest) -> Result { + let executions = self.executions.lock().expect("Job executions poisoned"); + if executions.closed { + return Err(JobServiceError::Closed); + } + Ok(self.with_store(|store| store.reserve(request))?) + } + + pub fn get(&self, job_id: &str) -> Result { + Ok(self.with_store(|store| store.get(job_id))?) + } + + /// Pending attempts survive shutdown. The consumer chooses when to resume + /// them; opening a Host never implicitly replays unknown side effects. + pub fn pending(&self) -> Result, JobServiceError> { + Ok(self.with_store(|store| store.pending())?) + } + + /// Explicit new attempt, bounded by immutable intent limits. Unknown + /// outcomes cannot be retried without domain reconciliation/new intent. + pub fn retry(&self, job_id: &str) -> Result { + let executions = self.executions.lock().expect("Job executions poisoned"); + if executions.closed { + return Err(JobServiceError::Closed); + } + if executions + .jobs + .get(job_id) + .is_some_and(|run| !run.task.is_finished()) + { + return Err(JobServiceError::Capacity); + } + Ok(self.with_store(|store| store.retry(job_id))?) + } + + /// Successful model processing and consumer postprocessing have independent + /// lifetimes. A failed delivery simply leaves this acknowledgement pending. + pub fn acknowledge(&self, job_id: &str) -> Result { + let executions = self.executions.lock().expect("Job executions poisoned"); + if executions.closed { + return Err(JobServiceError::Closed); + } + Ok(self.with_store(|store| store.ack(job_id))?) + } + + pub fn cancel(&self, job_id: &str) -> Result { + let executions = self.executions.lock().expect("Job executions poisoned"); + if executions.closed { + return Err(JobServiceError::Closed); + } + let snapshot = self.with_store(|store| store.cancel(job_id))?; + if let Some(execution) = executions.jobs.get(job_id) { + execution.cancel.send_replace(true); + } + Ok(snapshot) + } + + pub async fn start(&self, job_id: &str) -> Result<(), JobServiceError> { + self.start_with_optional_model_client(job_id, None).await + } + + /// Injectable model transport for embedders and scripted execution tests. + /// The requested registry Profile still resolves/validates normally; the + /// transport grants no filesystem, Feature or result-submission authority. + pub async fn start_with_model_client( + &self, + job_id: &str, + client: C, + ) -> Result<(), JobServiceError> + where + C: LlmClient + 'static, + { + self.start_with_optional_model_client(job_id, Some(Box::new(client))) + .await + } + + async fn start_with_optional_model_client( + &self, + job_id: &str, + client: Option>, + ) -> Result<(), JobServiceError> { + let snapshot = self.get(job_id)?; + let prepared = worker::job::prepare_job(&snapshot.request, &self.cwd, client) + .map_err(|error| JobServiceError::Configuration(error.to_string()))?; + let mut executions = self.executions.lock().expect("Job executions poisoned"); + if executions.closed { + return Err(JobServiceError::Closed); + } + executions.jobs.retain(|_, run| { + !(run.task.is_finished() && matches!(run.complete.borrow().as_ref(), Some(Ok(())))) + }); + // Count live resources, not only result states: acceptance can precede + // the Internal Worker's cleanup and must not free a concurrency slot. + let live = || { + executions + .jobs + .values() + .filter(|run| !run.task.is_finished()) + }; + let count = live().count(); + let active_limit = live() + .map(|run| run.max_concurrent) + .min() + .unwrap_or(self.capacity); + if count >= usize::from(self.capacity.min(active_limit)) + || count >= usize::from(snapshot.request.limits.max_concurrent_jobs) + || executions + .jobs + .get(job_id) + .is_some_and(|run| !run.task.is_finished()) + { + return Err(JobServiceError::Capacity); + } + let resource_key = snapshot.request.serialization_key.clone(); + if let Some(key) = &resource_key { + if executions + .jobs + .values() + .any(|run| !run.task.is_finished() && run.resource_key.as_ref() == Some(key)) + { + return Err(JobServiceError::Capacity); + } + } + // Retain errors from finished tasks until a caller observes them rather + // than silently swallowing store/cleanup failures. + if let Some(previous) = executions.jobs.get(job_id) { + if !previous.task.is_finished() { + return Err(JobServiceError::Capacity); + } + } + let snapshot = self.with_store(|store| { + store.dispatch(job_id, &snapshot.attempt.attempt_id, self.capacity) + })?; + let store = self.store.clone(); + let id = job_id.to_string(); + let attempt_id = snapshot.attempt.attempt_id.clone(); + let timeout = Duration::from_secs(u64::from(snapshot.request.limits.timeout_seconds)); + let sink = Arc::new(BoundResultSink { + store: store.clone(), + job_id: id.clone(), + attempt_id: attempt_id.clone(), + input_revision: snapshot.request.input_revision.clone(), + deadline: tokio::time::Instant::now() + timeout, + }); + let (cancel, mut cancellation) = watch::channel(false); + let (complete_tx, complete) = watch::channel(None); + let execution_attempt = attempt_id.clone(); + let task = tokio::spawn(async move { + let outcome: Result<(), JobServiceError> = async { + let (sender_tx, mut sender_rx) = tokio::sync::oneshot::channel(); + let run = prepared.run(attempt_id.clone(), sink, move |sender| { let _ = sender_tx.send(sender); }); + tokio::pin!(run); + let reason = tokio::select! { + result = &mut run => { + let (state, detail) = match result { + Ok(_) => (JobAttemptState::Failed, "Worker finished without an accepted structured result".to_string()), + Err(error @ worker::job::JobExecutionError::ResultUnconfirmed(_)) => (JobAttemptState::Unknown, error.to_string()), + Err(error) => (JobAttemptState::Failed, error.to_string()), + }; + finish_if_pending(&store, &id, &attempt_id, state, &detail)?; + return Ok(()); + } + _ = tokio::time::sleep(timeout) => "timeout", + _ = cancellation.wait_for(|cancelled| *cancelled) => "Host cancellation/shutdown", + }; + // Fence timeout/shutdown before cancelling so a late tool call can + // never turn an interrupted unaccepted execution into success. + let fence_result = finish_if_pending(&store, &id, &attempt_id, JobAttemptState::Unknown, reason); + // Cancellation is routed through Engine; keep the future alive to + // allow normal Feature cleanup. Hard stop is a fenced unknown + // outcome, never an automatic retry. Result acceptance wins races. + if let Ok(sender) = sender_rx.try_recv() { + let _ = sender.try_send(()); + } else if let Ok(Ok(sender)) = tokio::time::timeout(Duration::from_secs(1), &mut sender_rx).await { + let _ = sender.try_send(()); + } + let cleanup = tokio::time::timeout(Duration::from_secs(2), &mut run).await; + fence_result?; + if cleanup.is_err() { return Err(JobServiceError::Cleanup("Internal Worker cancellation deadline exceeded; outcome fenced unknown".into())); } + Ok(()) + }.await; + complete_tx.send_replace(Some( + outcome.as_ref().map(|_| ()).map_err(ToString::to_string), + )); + outcome + }); + executions.jobs.insert( + job_id.to_string(), + Execution { + cancel, + complete, + task, + attempt_id: execution_attempt, + resource_key, + max_concurrent: snapshot.request.limits.max_concurrent_jobs, + }, + ); + Ok(()) + } + + /// Observe execution/cleanup, not success: read the durable result separately. + pub async fn wait(&self, job_id: &str) -> Result { + let execution = self + .executions + .lock() + .expect("Job executions poisoned") + .jobs + .get(job_id) + .map(|run| (run.attempt_id.clone(), run.complete.clone())); + if let Some((attempt, mut completion)) = execution { + let result = completion + .wait_for(Option::is_some) + .await + .map_err(|error| JobServiceError::Cleanup(error.to_string()))? + .clone() + .expect("completion"); + result.map_err(JobServiceError::Cleanup)?; + let task = { + let mut executions = self.executions.lock().expect("Job executions poisoned"); + if executions + .jobs + .get(job_id) + .is_some_and(|run| run.attempt_id == attempt) + { + executions.jobs.remove(job_id) + } else { + None + } + }; + if let Some(execution) = task { + execution + .task + .await + .map_err(|error| JobServiceError::Cleanup(error.to_string()))??; + } + } + self.get(job_id) + } + + pub(crate) fn close(&self) { + let mut executions = self.executions.lock().expect("Job executions poisoned"); + executions.closed = true; + for execution in executions.jobs.values() { + execution.cancel.send_replace(true); + } + } + + pub(crate) async fn shutdown(&self) -> Result<(), JobServiceError> { + self.close(); + let tasks = { + let mut executions = self.executions.lock().expect("Job executions poisoned"); + std::mem::take(&mut executions.jobs) + }; + let mut error = None; + for execution in tasks.into_values() { + match execution.task.await { + Ok(Ok(())) => {} + Ok(Err(failure)) => { + error = Some(failure); + } + Err(failure) => { + error = Some(JobServiceError::Cleanup(failure.to_string())); + } + } + } + // Close SQLite before the Host releases its lease. Cloned handles may + // outlive the Host but cannot mutate or retain the live connection. + self.store.lock().expect("Job store poisoned").take(); + error.map_or(Ok(()), Err) + } +} + +fn finish_if_pending( + store: &Mutex>, + id: &str, + attempt: &str, + state: JobAttemptState, + detail: &str, +) -> Result<(), JobServiceError> { + let mut slot = store.lock().expect("Job store poisoned"); + let store = slot.as_mut().ok_or(JobServiceError::Closed)?; + let snapshot = store.get(id)?; + if snapshot.state == JobState::Pending { + store.finish(id, attempt, state, Some(detail))?; + } + Ok(()) +} + +struct BoundResultSink { + store: Arc>>, + job_id: String, + attempt_id: String, + input_revision: String, + deadline: tokio::time::Instant, +} +impl worker::job::JobResultSink for BoundResultSink { + fn submit(&self, submission: JobResultSubmission) -> Result<(), String> { + if submission.job_id != self.job_id + || submission.attempt_id != self.attempt_id + || submission.input_revision != self.input_revision + { + return Err("Job result capability binding mismatch".into()); + } + let mut slot = self + .store + .lock() + .map_err(|_| "Job store poisoned".to_string())?; + let store = slot.as_mut().ok_or_else(|| "Job Host closed".to_string())?; + let snapshot = store.get(&self.job_id).map_err(|error| error.to_string())?; + if tokio::time::Instant::now() >= self.deadline && snapshot.state != JobState::Completed { + return Err("Job result deadline expired".into()); + } + store + .accept(&submission) + .map(|_| ()) + .map_err(|error| error.to_string()) + } +} diff --git a/crates/standalone/src/jobs/tests.rs b/crates/standalone/src/jobs/tests.rs new file mode 100644 index 000000000..825cd360c --- /dev/null +++ b/crates/standalone/src/jobs/tests.rs @@ -0,0 +1,126 @@ +use super::*; +use worker::job::JobResultSink; + +fn request(id: &str) -> JobRequest { + JobRequest { + job_id: id.into(), + purpose: "test".into(), + input_revision: "r1".into(), + input_ref: "fixture:input".into(), + input: serde_json::json!({"data":42}), + instruction: "Return a structured result".into(), + profile: "builtin:job".into(), + serialization_key: None, + limits: job::JobLimits::default(), + } +} +fn sink(jobs: &StandaloneJobs, id: &str, attempt: &str) -> BoundResultSink { + BoundResultSink { + store: jobs.store.clone(), + job_id: id.into(), + attempt_id: attempt.into(), + input_revision: "r1".into(), + deadline: tokio::time::Instant::now() + Duration::from_secs(10), + } +} +fn result(id: &str, attempt: &str) -> JobResultSubmission { + JobResultSubmission { + job_id: id.into(), + attempt_id: attempt.into(), + input_revision: "r1".into(), + result: serde_json::json!({"valid":true}), + } +} + +#[tokio::test] +async fn host_result_capability_cannot_cross_job_attempt_revision_or_deadline() { + let temp = tempfile::tempdir().unwrap(); + let jobs = + StandaloneJobs::open(&temp.path().join("jobs.sqlite3"), temp.path().to_path_buf()).unwrap(); + let first = jobs.request(request("first")).unwrap(); + let second = jobs.request(request("second")).unwrap(); + for snapshot in [&first, &second] { + jobs.with_store(|store| { + store.dispatch(&snapshot.request.job_id, &snapshot.attempt.attempt_id, 8) + }) + .unwrap(); + } + let bound = sink(&jobs, "first", &first.attempt.attempt_id); + assert!( + bound + .submit(result("second", &second.attempt.attempt_id)) + .is_err() + ); + assert!(bound.submit(result("first", "first:attempt:2")).is_err()); + let mut stale = result("first", &first.attempt.attempt_id); + stale.input_revision = "r0".into(); + assert!(bound.submit(stale).is_err()); + let mut expired = sink(&jobs, "first", &first.attempt.attempt_id); + expired.deadline = tokio::time::Instant::now() - Duration::from_secs(1); + assert!( + expired + .submit(result("first", &first.attempt.attempt_id)) + .is_err() + ); + assert!(jobs.get("first").unwrap().result.is_none()); + bound + .submit(result("first", &first.attempt.attempt_id)) + .unwrap(); + // Accepted exact replay is still allowed after the deadline, never a new result. + expired + .submit(result("first", &first.attempt.attempt_id)) + .unwrap(); + let mut changed = result("first", &first.attempt.attempt_id); + changed.result = serde_json::json!({"valid":false}); + assert!(expired.submit(changed).is_err()); + assert_eq!(jobs.get("second").unwrap().state, JobState::Pending); + jobs.shutdown().await.unwrap(); + assert!( + bound + .submit(result("first", &first.attempt.attempt_id)) + .is_err() + ); +} + +#[tokio::test] +async fn cleanup_cannot_reclassify_success_or_durable_cancellation_and_stale_handles_are_closed() { + let temp = tempfile::tempdir().unwrap(); + let jobs = + StandaloneJobs::open(&temp.path().join("jobs.sqlite3"), temp.path().to_path_buf()).unwrap(); + let cancelled = jobs.request(request("cancelled")).unwrap(); + jobs.cancel("cancelled").unwrap(); + finish_if_pending( + &jobs.store, + "cancelled", + &cancelled.attempt.attempt_id, + JobAttemptState::Unknown, + "cleanup", + ) + .unwrap(); + assert_eq!(jobs.get("cancelled").unwrap().state, JobState::Cancelled); + let accepted = jobs.request(request("accepted")).unwrap(); + jobs.with_store(|store| store.dispatch("accepted", &accepted.attempt.attempt_id, 8)) + .unwrap(); + sink(&jobs, "accepted", &accepted.attempt.attempt_id) + .submit(result("accepted", &accepted.attempt.attempt_id)) + .unwrap(); + finish_if_pending( + &jobs.store, + "accepted", + &accepted.attempt.attempt_id, + JobAttemptState::Failed, + "model failed later", + ) + .unwrap(); + assert_eq!(jobs.get("accepted").unwrap().state, JobState::Completed); + jobs.shutdown().await.unwrap(); + assert!(jobs.store.lock().unwrap().is_none()); + for operation in [ + jobs.cancel("cancelled"), + jobs.acknowledge("accepted"), + jobs.retry("cancelled"), + jobs.request(request("new")), + ] { + assert!(matches!(operation, Err(JobServiceError::Closed))); + } +} diff --git a/crates/standalone/src/lib.rs b/crates/standalone/src/lib.rs index 100537a96..43da6c0e7 100644 --- a/crates/standalone/src/lib.rs +++ b/crates/standalone/src/lib.rs @@ -5,6 +5,8 @@ //! Server, HTTP, WebSocket, subprocess Worker, or alternative execution path. pub mod host; +mod job_store; +pub mod jobs; pub mod launch; pub mod store; diff --git a/crates/standalone/src/store.rs b/crates/standalone/src/store.rs index f55dfefd0..870da83f4 100644 --- a/crates/standalone/src/store.rs +++ b/crates/standalone/src/store.rs @@ -352,6 +352,10 @@ impl StandaloneWorkerStore { } #[must_use] + pub(crate) fn jobs_path(&self, id: WorkerId) -> PathBuf { + self.worker_dir(id).join("jobs.sqlite3") + } + pub(crate) fn runtime_dir(&self, id: WorkerId) -> PathBuf { self.worker_dir(id).join("runtime") } diff --git a/crates/standalone/tests/jobs.rs b/crates/standalone/tests/jobs.rs new file mode 100644 index 000000000..2c4d71688 --- /dev/null +++ b/crates/standalone/tests/jobs.rs @@ -0,0 +1,478 @@ +use std::collections::VecDeque; +use std::pin::Pin; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use agen::llm_client::{ + LlmClient, + error::ClientError, + event::{Event as LlmEvent, StopReason}, + types::Request, +}; +use async_trait::async_trait; +use futures::{Stream, stream}; +use job::{JobLimits, JobRequest}; +use standalone::jobs::{JobServiceError, JobState}; +use standalone::{StandaloneHost, StandaloneLaunchConfig}; + +#[derive(Clone)] +struct ScriptedClient { + responses: Arc>>>, + requests: Arc>>, + started: Arc, + stall: bool, + fail: bool, +} +impl ScriptedClient { + fn new(responses: Vec>) -> Self { + Self { + responses: Arc::new(Mutex::new(responses.into())), + requests: Arc::new(Mutex::new(Vec::new())), + started: Arc::new(tokio::sync::Notify::new()), + stall: false, + fail: false, + } + } + fn stall() -> Self { + Self { + stall: true, + ..Self::new(Vec::new()) + } + } + fn requests(&self) -> Vec { + self.requests.lock().unwrap().clone() + } +} +#[async_trait] +impl LlmClient for ScriptedClient { + async fn stream( + &self, + request: Request, + ) -> Result> + Send>>, ClientError> + { + self.requests.lock().unwrap().push(request); + self.started.notify_one(); + if self.fail { + return Err(ClientError::Config("scripted model failure".into())); + } + if self.stall { + return Ok(Box::pin(stream::pending())); + } + let events = self + .responses + .lock() + .unwrap() + .pop_front() + .expect("scripted response"); + Ok(Box::pin(stream::iter(events.into_iter().map(Ok)))) + } + fn clone_boxed(&self) -> Box { + Box::new(self.clone()) + } +} +fn prose() -> Vec { + vec![ + LlmEvent::text_block_start(0), + LlmEvent::text_delta(0, "done"), + LlmEvent::text_block_stop(0, Some(StopReason::EndTurn)), + ] +} +fn success() -> ScriptedClient { + ScriptedClient::new(vec![ + vec![ + LlmEvent::tool_use_start(0, "result-1", "SubmitJobResult"), + LlmEvent::tool_input_delta(0, r#"{"result":{"valid":true}}"#), + LlmEvent::tool_use_stop(0), + ], + prose(), + ]) +} +fn request(id: &str) -> JobRequest { + JobRequest { + job_id: id.into(), + purpose: "generic_test".into(), + input_revision: "r1".into(), + input_ref: "test:immutable".into(), + input: serde_json::json!({"test_input":42}), + instruction: "Return a structured validity result".into(), + profile: "builtin:job".into(), + serialization_key: None, + limits: JobLimits::default(), + } +} +async fn host(temp: &tempfile::TempDir) -> (StandaloneHost, ScriptedClient) { + let launch = StandaloneLaunchConfig::new( + temp.path(), + temp.path().join("state"), + manifest::ProfileSelector::Default, + "job-host", + ) + .resolve() + .unwrap(); + let client = ScriptedClient::new(Vec::new()); + let host = StandaloneHost::start_with_model_client(launch, client.clone()) + .await + .unwrap(); + (host, client) +} +async fn wait_started(client: &ScriptedClient) { + tokio::time::timeout(Duration::from_secs(10), client.started.notified()) + .await + .unwrap(); +} + +#[tokio::test] +async fn host_job_runs_without_dialog_and_result_survives_restore_and_ack() { + let temp = tempfile::tempdir().unwrap(); + let (host, dialog) = host(&temp).await; + let id = host.worker_id(); + let jobs = host.jobs(); + let intent = request("success"); + let reserved = jobs.request(intent.clone()).unwrap(); + assert_eq!(jobs.request(intent.clone()).unwrap(), reserved); + let model = success(); + jobs.start_with_model_client("success", model.clone()) + .await + .unwrap(); + let completed = jobs.wait("success").await.unwrap(); + assert_eq!(completed.state, JobState::Completed); + assert_eq!(completed.result, Some(serde_json::json!({"valid":true}))); + assert!(!completed.acknowledged); + let neutral = completed.outcome(); + assert_eq!(neutral.state, job::JobState::Completed); + assert_eq!(neutral.result, completed.result); + assert_eq!(neutral.attempt_state, job::JobAttemptState::Completed); + assert!( + dialog.requests().is_empty(), + "Job never runs on the interactive model/session" + ); + assert_eq!( + model.requests()[0] + .tools + .iter() + .map(|tool| tool.name.as_str()) + .collect::>(), + ["SubmitJobResult"] + ); + assert_eq!(jobs.request(intent.clone()).unwrap(), completed); + assert!( + jobs.start_with_model_client("success", success()) + .await + .is_err() + ); + assert!(jobs.retry("success").is_err()); + let db = temp + .path() + .join("state") + .join(id.to_string()) + .join("jobs.sqlite3"); + assert!(db.exists()); + host.shutdown().await.unwrap(); + assert!(matches!( + jobs.request(request("after-shutdown")), + Err(JobServiceError::Closed) + )); + assert!(matches!( + jobs.cancel("success"), + Err(JobServiceError::Closed) + )); + assert!(matches!( + jobs.acknowledge("success"), + Err(JobServiceError::Closed) + )); + assert!(matches!( + jobs.start_with_model_client("success", success()).await, + Err(JobServiceError::Closed) + )); + let restored = StandaloneHost::restore_with_model_client( + temp.path().join("state"), + id, + ScriptedClient::new(Vec::new()), + ) + .await + .unwrap(); + let restored_jobs = restored.jobs(); + assert_eq!(restored_jobs.get("success").unwrap(), completed); + assert!(restored_jobs.acknowledge("success").unwrap().acknowledged); + restored.shutdown().await.unwrap(); +} + +#[tokio::test] +async fn live_resource_limits_cancellation_and_wait_are_host_owned() { + let temp = tempfile::tempdir().unwrap(); + let (host, _) = host(&temp).await; + let jobs = host.jobs(); + let mut first = request("first"); + first.limits.max_concurrent_jobs = 1; + jobs.request(first).unwrap(); + jobs.request(request("second")).unwrap(); + let stalled = ScriptedClient::stall(); + jobs.start_with_model_client("first", stalled.clone()) + .await + .unwrap(); + wait_started(&stalled).await; + // Waiting must not remove the Host-owned task/cancellation capability. + let waiting_jobs = jobs.clone(); + let waiter = tokio::spawn(async move { waiting_jobs.wait("first").await }); + assert!(matches!( + jobs.start_with_model_client("second", success()).await, + Err(JobServiceError::Capacity) + )); + jobs.cancel("first").unwrap(); + let cancelled = tokio::time::timeout(Duration::from_secs(10), waiter) + .await + .unwrap() + .unwrap() + .unwrap(); + assert_eq!(cancelled.state, JobState::Cancelled); + assert!(jobs.retry("first").is_err()); + jobs.start_with_model_client("second", success()) + .await + .unwrap(); + assert_eq!( + jobs.wait("second").await.unwrap().state, + JobState::Completed + ); + host.shutdown().await.unwrap(); +} + +#[tokio::test] +async fn prose_failure_requires_explicit_bounded_new_attempt_and_exact_replay_does_not_retry() { + let temp = tempfile::tempdir().unwrap(); + let (host, _) = host(&temp).await; + let jobs = host.jobs(); + let intent = request("prose"); + jobs.request(intent.clone()).unwrap(); + jobs.start_with_model_client("prose", ScriptedClient::new(vec![prose()])) + .await + .unwrap(); + let failed = jobs.wait("prose").await.unwrap(); + assert_eq!(failed.state, JobState::Failed); + assert!(failed.result.is_none()); + assert_eq!(jobs.request(intent).unwrap(), failed); + let retry = jobs.retry("prose").unwrap(); + assert_eq!(retry.attempt.number, 2); + assert_ne!(retry.attempt.attempt_id, failed.attempt.attempt_id); + jobs.start_with_model_client("prose", ScriptedClient::new(vec![prose()])) + .await + .unwrap(); + assert_eq!(jobs.wait("prose").await.unwrap().state, JobState::Failed); + assert!(jobs.retry("prose").is_err()); + host.shutdown().await.unwrap(); +} + +#[tokio::test] +async fn timeout_is_unknown_not_prose_success_or_automatic_retry() { + let temp = tempfile::tempdir().unwrap(); + let (host, _) = host(&temp).await; + let jobs = host.jobs(); + let mut intent = request("timeout"); + intent.limits.timeout_seconds = 1; + jobs.request(intent).unwrap(); + let stalled = ScriptedClient::stall(); + jobs.start_with_model_client("timeout", stalled.clone()) + .await + .unwrap(); + wait_started(&stalled).await; + let outcome = tokio::time::timeout(Duration::from_secs(10), jobs.wait("timeout")) + .await + .unwrap() + .unwrap(); + assert_eq!(outcome.state, JobState::Unknown); + assert!(outcome.result.is_none()); + assert!(jobs.retry("timeout").is_err()); + host.shutdown().await.unwrap(); +} + +#[tokio::test] +async fn shutdown_keeps_reserved_results_and_fences_running_attempts_on_reopen() { + let temp = tempfile::tempdir().unwrap(); + let (host, _) = host(&temp).await; + let id = host.worker_id(); + let jobs = host.jobs(); + jobs.request(request("reserved")).unwrap(); + jobs.request(request("cancelled")).unwrap(); + jobs.cancel("cancelled").unwrap(); + jobs.request(request("running")).unwrap(); + let stalled = ScriptedClient::stall(); + jobs.start_with_model_client("running", stalled.clone()) + .await + .unwrap(); + wait_started(&stalled).await; + host.shutdown().await.unwrap(); + assert!(matches!(jobs.get("running"), Err(JobServiceError::Closed))); + let restored = StandaloneHost::restore_with_model_client( + temp.path().join("state"), + id, + ScriptedClient::new(Vec::new()), + ) + .await + .unwrap(); + let jobs = restored.jobs(); + assert_eq!( + jobs.pending() + .unwrap() + .iter() + .map(|snapshot| snapshot.request.job_id.as_str()) + .collect::>(), + ["reserved"] + ); + assert_eq!(jobs.get("cancelled").unwrap().state, JobState::Cancelled); + assert_eq!(jobs.get("running").unwrap().state, JobState::Unknown); + assert!(jobs.retry("running").is_err()); + jobs.start_with_model_client("reserved", success()) + .await + .unwrap(); + assert_eq!( + jobs.wait("reserved").await.unwrap().state, + JobState::Completed + ); + restored.shutdown().await.unwrap(); +} + +#[tokio::test] +async fn profile_requirements_are_rejected_without_model_execution_or_implicit_fallback() { + let temp = tempfile::tempdir().unwrap(); + let (host, _) = host(&temp).await; + let jobs = host.jobs(); + for profile in [ + "builtin:coder", + "builtin:backend-job", + "builtin:does-not-exist", + ] { + let mut intent = request(profile); + intent.profile = profile.into(); + jobs.request(intent).unwrap(); + let model = success(); + assert!(matches!( + jobs.start_with_model_client(profile, model.clone()).await, + Err(JobServiceError::Configuration(_)) + )); + assert!(model.requests().is_empty()); + assert_eq!(jobs.get(profile).unwrap().state, JobState::Pending); + } + host.shutdown().await.unwrap(); +} + +// Only this test harness starts a subprocess: production Jobs remain in-process. +// The child exits deliberately without destructors to prove actual process-loss +// recovery, rather than treating a hand-edited SQLite fixture as that evidence. +#[tokio::test] +#[ignore = "entry point for process_loss_recovers_durable_jobs_without_replaying_unknown"] +async fn process_job_fixture() { + let root = std::path::PathBuf::from( + std::env::var_os("YOI_JOB_PROCESS_FIXTURE_DIR").expect("fixture root"), + ); + let launch = StandaloneLaunchConfig::new( + &root, + root.join("state"), + manifest::ProfileSelector::Default, + "job-process-fixture", + ) + .resolve() + .unwrap(); + let host = StandaloneHost::start_with_model_client(launch, ScriptedClient::new(Vec::new())) + .await + .unwrap(); + std::fs::write(root.join("worker-id"), host.worker_id().to_string()).unwrap(); + let jobs = host.jobs(); + jobs.request(request("reserved")).unwrap(); + jobs.request(request("cancelled")).unwrap(); + jobs.cancel("cancelled").unwrap(); + jobs.request(request("accepted")).unwrap(); + jobs.start_with_model_client("accepted", success()) + .await + .unwrap(); + assert_eq!( + jobs.wait("accepted").await.unwrap().state, + JobState::Completed + ); + jobs.request(request("interrupted")).unwrap(); + let stalled = ScriptedClient::stall(); + jobs.start_with_model_client("interrupted", stalled.clone()) + .await + .unwrap(); + wait_started(&stalled).await; + // Test-owned process only, never the operating Yoi Worker or dogfood daemon. + std::process::exit(23); +} + +#[tokio::test] +async fn process_loss_recovers_durable_jobs_without_replaying_unknown() { + let temp = tempfile::tempdir().unwrap(); + let output = std::process::Command::new(std::env::current_exe().unwrap()) + .args(["--exact", "process_job_fixture", "--ignored", "--nocapture"]) + .env("YOI_JOB_PROCESS_FIXTURE_DIR", temp.path()) + .output() + .unwrap(); + assert_eq!( + output.status.code(), + Some(23), + "fixture failed: {}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let worker_id = std::fs::read_to_string(temp.path().join("worker-id")) + .unwrap() + .parse() + .unwrap(); + let host = StandaloneHost::restore_with_model_client( + temp.path().join("state"), + worker_id, + ScriptedClient::new(Vec::new()), + ) + .await + .unwrap(); + let jobs = host.jobs(); + assert_eq!(jobs.get("interrupted").unwrap().state, JobState::Unknown); + assert!(jobs.retry("interrupted").is_err()); + let completed = jobs.get("accepted").unwrap(); + assert_eq!(completed.state, JobState::Completed); + assert_eq!(completed.result, Some(serde_json::json!({"valid":true}))); + assert!(!completed.acknowledged); + assert_eq!(jobs.get("cancelled").unwrap().state, JobState::Cancelled); + assert_eq!(jobs.pending().unwrap().len(), 1); + jobs.start_with_model_client("reserved", success()) + .await + .unwrap(); + assert_eq!( + jobs.wait("reserved").await.unwrap().state, + JobState::Completed + ); + host.shutdown().await.unwrap(); +} + +#[tokio::test] +async fn model_failure_is_durable_known_failure_with_explicit_successful_retry() { + let temp = tempfile::tempdir().unwrap(); + let (host, _) = host(&temp).await; + let jobs = host.jobs(); + jobs.request(request("model-failure")).unwrap(); + let client = ScriptedClient { + fail: true, + ..ScriptedClient::new(Vec::new()) + }; + jobs.start_with_model_client("model-failure", client) + .await + .unwrap(); + let failure = jobs.wait("model-failure").await.unwrap(); + assert_eq!(failure.state, JobState::Failed); + assert!(failure.result.is_none()); + assert!( + failure + .attempt + .failure + .unwrap() + .contains("scripted model failure") + ); + let next = jobs.retry("model-failure").unwrap(); + assert_eq!(next.attempt.number, 2); + jobs.start_with_model_client("model-failure", success()) + .await + .unwrap(); + assert_eq!( + jobs.wait("model-failure").await.unwrap().state, + JobState::Completed + ); + host.shutdown().await.unwrap(); +} diff --git a/crates/worker/Cargo.toml b/crates/worker/Cargo.toml index 597cf460c..2fd9691aa 100644 --- a/crates/worker/Cargo.toml +++ b/crates/worker/Cargo.toml @@ -16,6 +16,7 @@ agen = { workspace = true, features = ["codex"] } session-store = { workspace = true } secrets = { workspace = true } manifest = { workspace = true } +job = { workspace = true } mcp = { workspace = true } protocol = { workspace = true, features = ["json-schema"] } client = { workspace = true } diff --git a/crates/worker/src/internal_worker.rs b/crates/worker/src/internal_worker.rs index ce6bff263..852b4d583 100644 --- a/crates/worker/src/internal_worker.rs +++ b/crates/worker/src/internal_worker.rs @@ -205,6 +205,9 @@ where .copied() .collect::>() .join(", "); + worker + .stop_feature_runtime("internal Worker feature installation failed") + .await; return Err(InternalWorkerError { source: WorkerError::FeatureInstall(format!( "internal Worker feature installation failed: {diagnostics}; missing tools: {missing}" diff --git a/crates/worker/src/job.rs b/crates/worker/src/job.rs new file mode 100644 index 000000000..afae9a29f --- /dev/null +++ b/crates/worker/src/job.rs @@ -0,0 +1,323 @@ +//! Host-facing, result-only Jobs executed by the private Internal Worker substrate. +//! +//! The host owns persistence, concurrency, timeout, retries and result acceptance. Normal +//! timeout/shutdown routes through the cancellation sender and awaits `run`, including Worker +//! resource cleanup. An emergency drop/abort does not confirm asynchronous cleanup; the Host +//! must retain its lease and reconcile any unknown outcome before allowing another execution. +//! The result-only Feature owns no background tasks, but an in-flight synchronous Host sink +//! cannot be preempted by dropping its tool future and must itself be bounded. + +mod result; +pub use result::{JobResultFeature, JobResultSink, SUBMIT_JOB_RESULT_TOOL}; + +use std::borrow::Cow; +use std::path::Path; +use std::sync::Arc; + +use agen::llm_client::LlmClient; +use manifest::{ProfileDiscovery, ProfileResolveOptions, ProfileResolver, Scope, WorkerManifest}; + +use crate::feature::FeatureRegistryBuilder; +use crate::internal_worker::{ + InternalWorkerAuthority, InternalWorkerIdentity, InternalWorkerSpec, + run_internal_worker_with_cancel_sender, +}; +use crate::{ + PromptCatalog, PromptCatalogSource, SystemPromptContext, SystemPromptTemplate, + WorkerFilesystemAuthority, WorkerPrompt, WorkerRunResult, WorkerWorkspaceContext, +}; + +/// Host adapter ceilings, applied even if the selected Profile leaves budgets unbounded. +pub const MAX_JOB_TURNS: u32 = 32; +pub const MAX_JOB_TOKENS: u32 = 8192; + +#[derive(Debug, thiserror::Error)] +pub enum JobExecutionError { + #[error(transparent)] + Request(#[from] ::job::JobError), + #[error("Job Profile resolution failed: {0}")] + Profile(#[from] manifest::ProfileError), + #[error("result-only Job cannot satisfy Profile requirements: {0}")] + UnsupportedProfile(String), + #[error("Job instruction resolution failed: {0}")] + Instruction(#[from] crate::SystemPromptError), + #[error("Job Prompt catalog resolution failed: {0}")] + Catalog(#[from] crate::CatalogError), + #[error("Job model configuration failed: {0}")] + Model(#[from] crate::ProviderError), + #[error("Job execution failed: {0}")] + Worker(String), + /// A sink error can follow durable acceptance. Hosts must reconcile their store and must + /// not classify this as a known retryable failure unless they can prove non-acceptance. + #[error( + "Host Job result acceptance is unconfirmed; reconcile the Host store before retrying: {0}" + )] + ResultUnconfirmed(String), + #[error("Job execution was cancelled without an accepted result")] + Cancelled, + #[error("Job ended without an accepted SubmitJobResult; final prose is not success")] + MissingResult, +} + +/// Confirms only a synchronous, successful Host acceptance. Lifecycle/prose cannot create this. +#[derive(Debug)] +pub struct InternalJobResult { + pub submission: ::job::JobResultSubmission, + pub usage: Option, +} + +/// Resolved policy/model/prompt, with no running resources and no inherited authority. +pub struct PreparedInternalJob { + request: ::job::JobRequest, + manifest: WorkerManifest, + client: Box, + system_prompt: String, +} + +/// Resolve exactly the requested registry Profile using the existing user settings registry. +/// `cwd` is a resolution base only; it never becomes filesystem or Workspace authority. +/// An injected client is useful for scripted executions and does not bypass policy validation. +pub fn prepare_job( + request: &::job::JobRequest, + cwd: &Path, + client: Option>, +) -> Result { + let registry = ProfileDiscovery::user_settings().discover()?; + prepare_job_from_registry(request, cwd, client, ®istry) +} + +fn prepare_job_from_registry( + request: &::job::JobRequest, + cwd: &Path, + client: Option>, + registry: &manifest::ProfileRegistry, +) -> Result { + request.validate()?; + let resolved = ProfileResolver::new() + .with_workspace_base(cwd) + .resolve_from_registry( + &request.profile_selector()?, + registry, + ProfileResolveOptions::with_worker_name("internal-job"), + )?; + let mut manifest = resolved.manifest; + validate_result_only_profile(&manifest)?; + let scope = Scope::empty(); + let prompts = PromptCatalog::builtins_only()?; + let template = SystemPromptTemplate::parse( + &manifest.engine.instruction, + PromptCatalogSource::builtins_only(), + )?; + let mut system_prompt = template.render(&SystemPromptContext { + now: chrono::Utc::now(), + cwd: Cow::Borrowed("(no filesystem)"), + language: &manifest.engine.language, + scope: &scope, + tool_names: vec![SUBMIT_JOB_RESULT_TOOL.into()], + feature_instructions: &[], + agents_md: None, + resident_summary: None, + prompts: &prompts, + })?; + system_prompt.push_str("\n\n"); + system_prompt.push_str(&prompts.render(WorkerPrompt::JobSystem, minijinja::context! {})?); + // Host budgets attenuate Profile output/turn policy, never grant another capability. + manifest.engine.max_turns = std::num::NonZeroU32::new( + manifest + .engine + .max_turns + .map(|n| n.get()) + .unwrap_or(MAX_JOB_TURNS) + .min(MAX_JOB_TURNS), + ); + manifest.engine.max_tokens = Some( + manifest + .engine + .max_tokens + .unwrap_or(MAX_JOB_TOKENS) + .min(MAX_JOB_TOKENS), + ); + let model_config = manifest::model_catalog::resolve_model_manifest(&manifest.model) + .map_err(crate::ProviderError::from)?; + let client = match client { + Some(client) => client, + None => crate::model_client::build_client_from_config(&model_config)?, + }; + Ok(PreparedInternalJob { + request: request.clone(), + manifest, + client, + system_prompt, + }) +} + +impl PreparedInternalJob { + /// Run one immutable attempt. The sink must perform fenced, durable acceptance before + /// returning Ok. Timeout is Host-owned: cancel and then await this future for cleanup. + /// A result already accepted by the sink remains success even if the model subsequently + /// fails, reaches its turn limit or is cancelled; callers never rerun an accepted result. + pub async fn run( + self, + attempt_id: String, + submit: Arc, + on_cancel_sender: F, + ) -> Result + where + F: FnOnce(tokio::sync::mpsc::Sender<()>), + { + let feature = JobResultFeature::new(&self.request, attempt_id.clone(), submit)?; + let accepted = feature.accepted(); + let engine_policy = self.manifest.engine.clone(); + let spec = InternalWorkerSpec { + identity: InternalWorkerIdentity { + kind: "job", + run_id: uuid::Uuid::now_v7(), + }, + max_turns: engine_policy.max_turns.map(|n| n.get()), + engine_configurator: Some(Box::new(move |engine| { + crate::apply_worker_manifest(engine, &engine_policy); + })), + manifest: self.manifest, + client: self.client, + system_prompt: self.system_prompt, + input: self.request.worker_input(&attempt_id)?, + cache_key: None, + features: FeatureRegistryBuilder::new().with_module(feature.clone()), + required_tools: &[SUBMIT_JOB_RESULT_TOOL], + authority: InternalWorkerAuthority { + workspace: WorkerWorkspaceContext::no_workspace(), + filesystem: WorkerFilesystemAuthority::None, + scope: Scope::empty(), + workdir_session: None, + }, + }; + let outcome = run_internal_worker_with_cancel_sender(spec, on_cancel_sender).await; + let usage = match &outcome { + Ok(run) => run.usage.clone(), + Err(error) => error.usage.clone(), + }; + if let Some(submission) = accepted.lock().expect("Job result state poisoned").clone() { + return Ok(InternalJobResult { submission, usage }); + } + if let Some(error) = feature.unconfirmed() { + return Err(JobExecutionError::ResultUnconfirmed(error)); + } + match outcome { + Err(error) => Err(JobExecutionError::Worker(error.source.to_string())), + Ok(run) + if matches!( + run.lifecycle, + WorkerRunResult::Cancelled | WorkerRunResult::RolledBack + ) => + { + Err(JobExecutionError::Cancelled) + } + Ok(_) => Err(JobExecutionError::MissingResult), + } + } +} + +/// Validate requirements, not Profile names. Nothing configured as active is silently stripped. +fn validate_result_only_profile(manifest: &WorkerManifest) -> Result<(), JobExecutionError> { + let manifest::FeatureConfig { + task, + memory, + subjektiv, + web, + image, + sub_worker, + flow, + worker, + workspace_worker_discovery, + objective, + manage_workdir, + workdir_catalog, + workspace_config, + ticket, + merge_request, + orchestration, + } = &manifest.feature; + let mut unavailable = Vec::new(); + for (name, enabled) in [ + ("feature.task", task.enabled), + ( + "feature.memory", + memory.profile.enabled + || memory.profile.staging_tools + || memory.workspace_settings.is_some(), + ), + ( + "feature.subjektiv", + subjektiv.profile.enabled + || subjektiv.profile.consolidation_tools + || subjektiv.workspace_settings.is_some(), + ), + ("feature.web", web.enabled), + ("feature.image", image.enabled), + ("feature.sub_worker", sub_worker.enabled), + ("feature.flow", flow.enabled), + ("feature.worker", worker.enabled), + ( + "feature.workspace_worker_discovery", + workspace_worker_discovery.enabled, + ), + ("feature.objective", objective.enabled), + ("feature.manage_workdir", manage_workdir.enabled), + ("feature.workspace_config", workspace_config.enabled), + ( + "feature.ticket", + ticket.enabled || ticket.authoring || ticket.thread || ticket.intake || ticket.workflow, + ), + ( + "feature.merge_request", + merge_request.show + || merge_request.open + || merge_request.review + || merge_request.readiness_check + || merge_request.complete, + ), + ("feature.orchestration", orchestration.enabled), + // The default WIP-only catalog is inert in Tools mode, not a tool requirement. + ( + "feature.workdir_catalog", + workdir_catalog.enabled && manifest.worker.mode == manifest::WorkerMode::Wip, + ), + ( + "worker.mode=wip", + manifest.worker.mode == manifest::WorkerMode::Wip, + ), + ("scope", !manifest.scope.allow.is_empty()), + ( + "delegation_scope", + !manifest.delegation_scope.allow.is_empty(), + ), + ("mcp", !manifest.mcp.stdio_servers.is_empty()), + ("skills", manifest.skills.is_some()), + ("compaction", manifest.compaction.is_some()), + ( + "web", + manifest + .web + .as_ref() + .is_some_and(|config| config.enabled == Some(true)), + ), + ( + "session.record_event_trace", + manifest.session.record_event_trace, + ), + ] { + if enabled { + unavailable.push(name); + } + } + if unavailable.is_empty() { + Ok(()) + } else { + Err(JobExecutionError::UnsupportedProfile( + unavailable.join(", "), + )) + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/worker/src/job/result.rs b/crates/worker/src/job/result.rs new file mode 100644 index 000000000..edb10712b --- /dev/null +++ b/crates/worker/src/job/result.rs @@ -0,0 +1,179 @@ +use std::collections::HashSet; +use std::sync::{Arc, Mutex}; + +use agen::tool::{Tool, ToolDefinition, ToolError, ToolExecutionContext, ToolMeta, ToolOutput}; +use async_trait::async_trait; +use schemars::JsonSchema; +use serde::Deserialize; +use serde_json::Value; + +use super::JobExecutionError; +use crate::feature::{ + FeatureDescriptor, FeatureInstallContext, FeatureInstallError, FeatureModule, ToolContribution, + ToolDeclaration, +}; + +pub const SUBMIT_JOB_RESULT_TOOL: &str = "SubmitJobResult"; +const DESCRIPTION: &str = "Submit this Host Job's bounded structured result. Job, attempt and input revision are immutable Host bindings. Only result is accepted as input; final prose is not success."; + +/// Host acceptance authority. Implementations must validate the immutable fences and commit +/// acceptance durably before returning Ok. Err may mean an unknown outcome; identical retries +/// must be idempotent. This synchronous callback must be bounded and must not wait for async work. +pub trait JobResultSink: Send + Sync { + fn submit(&self, submission: ::job::JobResultSubmission) -> Result<(), String>; +} + +#[derive(Deserialize, JsonSchema)] +#[serde(deny_unknown_fields)] +struct SubmitJobResultInput { + result: Value, +} + +#[derive(Default)] +struct SubmissionState { + sealed: Option, + unconfirmed: Option, +} + +/// Single-purpose FeatureModule; Profile names never install it or confer result authority. +#[derive(Clone)] +pub struct JobResultFeature { + job_id: String, + attempt_id: String, + input_revision: String, + max_result_bytes: u32, + sink: Arc, + state: Arc>, + accepted: Arc>>, + cancelled: Arc>>, +} + +impl JobResultFeature { + pub fn new( + request: &::job::JobRequest, + attempt_id: String, + sink: Arc, + ) -> Result { + request.validate()?; + ::job::validate_identifier("attempt_id", &attempt_id, 512)?; + Ok(Self { + job_id: request.job_id.clone(), + attempt_id, + input_revision: request.input_revision.clone(), + max_result_bytes: request.limits.max_result_bytes, + sink, + state: Arc::default(), + accepted: Arc::default(), + cancelled: Arc::default(), + }) + } + + pub(super) fn accepted(&self) -> Arc>> { + self.accepted.clone() + } + + pub(super) fn unconfirmed(&self) -> Option { + self.state + .lock() + .expect("Job submission state poisoned") + .unconfirmed + .clone() + } + + fn definition(&self) -> ToolDefinition { + let feature = self.clone(); + Arc::new(move || { + ( + ToolMeta::new(SUBMIT_JOB_RESULT_TOOL) + .description(DESCRIPTION) + .input_schema( + serde_json::to_value(schemars::schema_for!(SubmitJobResultInput)) + .expect("Job result schema serializes"), + ), + Arc::new(feature.clone()) as Arc, + ) + }) + } +} + +#[async_trait] +impl Tool for JobResultFeature { + async fn execute( + &self, + input_json: &str, + context: ToolExecutionContext, + ) -> Result { + let input: SubmitJobResultInput = serde_json::from_str(input_json) + .map_err(|error| ToolError::InvalidArgument(error.to_string()))?; + ::job::result_digest(&input.result, self.max_result_bytes) + .map_err(|error| ToolError::InvalidArgument(error.to_string()))?; + let mut state = self.state.lock().expect("Job submission state poisoned"); + if self + .cancelled + .lock() + .expect("Job cancellation state poisoned") + .contains(&context.execution_id()) + { + return Err(ToolError::Cancelled(ToolOutput { + summary: "Job result submission cancelled".into(), + content: None, + attachments: vec![], + })); + } + if let Some(sealed) = &state.sealed { + if sealed != &input.result { + return Err(ToolError::InvalidArgument( + "Job result is sealed; retry only the identical result".into(), + )); + } + } else { + // Seal before dispatch: a sink error can follow durable Host acceptance. + state.sealed = Some(input.result.clone()); + } + let submission = ::job::JobResultSubmission { + job_id: self.job_id.clone(), + attempt_id: self.attempt_id.clone(), + input_revision: self.input_revision.clone(), + result: input.result, + }; + let mut accepted = self.accepted.lock().expect("Job result state poisoned"); + if accepted.is_none() { + if let Err(error) = self.sink.submit(submission.clone()) { + let error = ::job::bounded_failure_detail(&error); + state.unconfirmed = Some(error.clone()); + return Err(ToolError::ExecutionFailed(format!( + "Host Job result acceptance was not confirmed; retry only the identical result: {error}" + ))); + } + state.unconfirmed = None; + *accepted = Some(submission); + } + Ok(ToolOutput { + summary: "Host Job result accepted".into(), + content: None, + attachments: vec![], + }) + } + + async fn cancel_execution(&self, context: &ToolExecutionContext) -> Result<(), ToolError> { + self.cancelled + .lock() + .expect("Job cancellation state poisoned") + .insert(context.execution_id()); + Ok(()) + } +} + +impl FeatureModule for JobResultFeature { + fn descriptor(&self) -> FeatureDescriptor { + FeatureDescriptor::builtin("host-job-result", "Host Job result") + .with_description("Result-only immutable Host Job submission capability.") + .with_tool(ToolDeclaration::new(SUBMIT_JOB_RESULT_TOOL, DESCRIPTION)) + } + fn install(&self, context: &mut FeatureInstallContext<'_>) -> Result<(), FeatureInstallError> { + context.tools().register(ToolContribution::new( + SUBMIT_JOB_RESULT_TOOL, + self.definition(), + )) + } +} diff --git a/crates/worker/src/job/tests.rs b/crates/worker/src/job/tests.rs new file mode 100644 index 000000000..2135e1b80 --- /dev/null +++ b/crates/worker/src/job/tests.rs @@ -0,0 +1,666 @@ +//! Scripted in-process model executions, not provider/process tests. +use super::*; +use agen::llm_client::event::{Event, ResponseStatus, StatusEvent}; +use agen::llm_client::{ClientError, Request}; +use agen::tool::{Tool, ToolExecutionContext}; +use async_trait::async_trait; +use futures::Stream; +use serde_json::{Value, json}; +use std::collections::VecDeque; +use std::pin::Pin; +use std::sync::Mutex; + +#[derive(Clone)] +struct ScriptedClient { + scripts: Arc>>>, + requests: Arc>>, + pending: bool, + entered: Arc, + fail: bool, +} +impl ScriptedClient { + fn new(scripts: Vec>) -> Self { + Self { + scripts: Arc::new(Mutex::new(scripts.into())), + requests: Arc::default(), + pending: false, + entered: Arc::default(), + fail: false, + } + } +} +#[async_trait] +impl LlmClient for ScriptedClient { + fn clone_boxed(&self) -> Box { + Box::new(self.clone()) + } + async fn stream( + &self, + request: Request, + ) -> Result> + Send>>, ClientError> { + self.requests.lock().unwrap().push(request); + if self.fail && self.scripts.lock().unwrap().is_empty() { + return Err(ClientError::Config("scripted failure".into())); + } + if self.pending && self.scripts.lock().unwrap().is_empty() { + self.entered.notify_one(); + return Ok(Box::pin(futures::stream::pending())); + } + let events = self + .scripts + .lock() + .unwrap() + .pop_front() + .expect("unexpected extra model turn"); + Ok(Box::pin(futures::stream::iter(events.into_iter().map(Ok)))) + } +} +#[derive(Default)] +struct RecordingSink(Mutex>); +impl JobResultSink for RecordingSink { + fn submit(&self, result: ::job::JobResultSubmission) -> Result<(), String> { + self.0.lock().unwrap().push(result); + Ok(()) + } +} +fn prose() -> Vec { + vec![ + Event::text_block_start(0), + Event::text_delta(0, "success"), + Event::text_block_stop(0, None), + Event::Status(StatusEvent { + status: ResponseStatus::Completed, + }), + ] +} +fn call(name: &str, input: Value) -> Vec { + vec![ + Event::tool_use_start(0, "call", name), + Event::tool_input_delta(0, input.to_string()), + Event::tool_use_stop(0), + Event::Status(StatusEvent { + status: ResponseStatus::Completed, + }), + ] +} +fn request(profile: &str) -> ::job::JobRequest { + ::job::JobRequest { + job_id: "job-a".into(), + purpose: "scripted".into(), + input_revision: "rev-1".into(), + input_ref: "snapshot:one".into(), + input: json!({"text":"input"}), + instruction: "Summarize the input".into(), + profile: profile.into(), + serialization_key: None, + limits: Default::default(), + } +} +fn artifact(instruction: &str, tokens: u32, turns: u32) -> Value { + json!({"model":{"scheme":"anthropic", "model_id":"scripted-model", "auth":{"kind":"none"}}, + "engine":{"instruction":instruction, "language":"English", "max_tokens":tokens, "max_turns":turns}}) +} +fn registry(root: &Path, a: Value, b: Value) -> manifest::ProfileRegistry { + std::fs::write(root.join("a.json"), a.to_string()).unwrap(); + std::fs::write(root.join("b.json"), b.to_string()).unwrap(); + let settings = root.join("profiles.toml"); + std::fs::write( + &settings, + "default = 'user:a'\n[profile]\na = 'a.json'\nb = 'b.json'\n", + ) + .unwrap(); + ProfileDiscovery::with_sources(Some(settings), None) + .discover() + .unwrap() +} +fn prepared( + root: &Path, + request: &::job::JobRequest, + client: ScriptedClient, +) -> PreparedInternalJob { + let registry = registry( + root, + artifact("default", 512, 3), + artifact("internal.flow_verifier_system", 100000, 1000), + ); + prepare_job_from_registry(request, root, Some(Box::new(client)), ®istry).unwrap() +} + +#[tokio::test] +async fn executes_selected_profiles_with_only_result_authority_and_bounded_policy() { + let root = tempfile::tempdir().unwrap(); + let registry = registry( + root.path(), + artifact("default", 512, 3), + artifact("internal.flow_verifier_system", 100000, 1000), + ); + let contract = PromptCatalog::builtins_only() + .unwrap() + .render(WorkerPrompt::JobSystem, minijinja::context! {}) + .unwrap(); + let mut prompts = vec![]; + for (profile, tokens) in [ + ("user:a", 512), + ("user:b", MAX_JOB_TOKENS), + ("default", 512), + ] { + let client = ScriptedClient::new(vec![ + call(SUBMIT_JOB_RESULT_TOOL, json!({"result":{"ok":true}})), + prose(), + ]); + let sink = Arc::new(RecordingSink::default()); + let job = prepare_job_from_registry( + &request(profile), + root.path(), + Some(Box::new(client.clone())), + ®istry, + ) + .unwrap(); + assert_eq!( + job.manifest.engine.max_turns.unwrap().get(), + if profile == "user:b" { + MAX_JOB_TURNS + } else { + 3 + } + ); + let (tx, rx) = tokio::sync::oneshot::channel(); + let result = job + .run("attempt-7".into(), sink.clone(), move |sender| { + let _ = tx.send(sender); + }) + .await + .unwrap(); + assert!( + rx.await.unwrap().is_closed(), + "Worker must be released before returning acceptance" + ); + assert_eq!( + result.submission, + ::job::JobResultSubmission { + job_id: "job-a".into(), + attempt_id: "attempt-7".into(), + input_revision: "rev-1".into(), + result: json!({"ok":true}) + } + ); + assert_eq!(sink.0.lock().unwrap().len(), 1); + let requests = client.requests.lock().unwrap(); + assert_eq!(requests.len(), 2); + for request in requests.iter() { + assert_eq!( + request + .tools + .iter() + .map(|tool| tool.name.as_str()) + .collect::>(), + [SUBMIT_JOB_RESULT_TOOL] + ); + assert_eq!(request.config.max_tokens, Some(tokens)); + assert_eq!( + request.tools[0].input_schema["properties"] + .as_object() + .unwrap() + .keys() + .collect::>(), + ["result"] + ); + assert!(request.system_prompt.as_ref().unwrap().ends_with(&contract)); + } + prompts.push(requests[0].system_prompt.clone()); + } + assert_ne!(prompts[0], prompts[1]); + assert_eq!(prompts[0], prompts[2]); +} + +#[tokio::test] +async fn selected_builtin_job_executes_with_catalog_contract_and_only_result_tool() { + let root = tempfile::tempdir().unwrap(); + let registry = ProfileDiscovery::with_sources(None, None) + .discover() + .unwrap(); + let request = request("builtin:job"); + let client = ScriptedClient::new(vec![ + call(SUBMIT_JOB_RESULT_TOOL, json!({"result":{"ok":true}})), + prose(), + ]); + let sink = Arc::new(RecordingSink::default()); + let job = prepare_job_from_registry( + &request, + root.path(), + Some(Box::new(client.clone())), + ®istry, + ) + .unwrap(); + assert_eq!( + job.manifest.engine.instruction, + WorkerPrompt::JobSystem.key() + ); + assert_eq!( + job.manifest.model.ref_.as_deref(), + Some("codex-oauth/gpt-5.6-luna") + ); + assert_eq!(job.manifest.engine.max_turns.unwrap().get(), MAX_JOB_TURNS); + assert!(job.manifest.scope.allow.is_empty()); + assert!(job.manifest.delegation_scope.allow.is_empty()); + assert!(WorkerPrompt::ALL.contains(&WorkerPrompt::JobSystem)); + let contract = PromptCatalog::builtins_only() + .unwrap() + .render(WorkerPrompt::JobSystem, minijinja::context! {}) + .unwrap(); + assert!(job.system_prompt.ends_with(&contract)); + let (tx, rx) = tokio::sync::oneshot::channel(); + let result = job + .run("builtin-attempt".into(), sink.clone(), move |sender| { + let _ = tx.send(sender); + }) + .await + .unwrap(); + assert!(rx.await.unwrap().is_closed()); + assert_eq!(result.submission.job_id, request.job_id); + assert_eq!(result.submission.attempt_id, "builtin-attempt"); + assert_eq!(result.submission.input_revision, request.input_revision); + assert_eq!(result.submission.result, json!({"ok":true})); + assert_eq!(sink.0.lock().unwrap().len(), 1); + let requests = client.requests.lock().unwrap(); + assert_eq!(requests.len(), 2); + for request in requests.iter() { + assert_eq!( + request + .tools + .iter() + .map(|tool| tool.name.as_str()) + .collect::>(), + [SUBMIT_JOB_RESULT_TOOL] + ); + assert_eq!(request.config.max_tokens, Some(MAX_JOB_TOKENS)); + assert!(request.system_prompt.as_ref().unwrap().ends_with(&contract)); + } +} + +#[tokio::test] +async fn final_prose_and_ungranted_tool_calls_are_not_success() { + let root = tempfile::tempdir().unwrap(); + for scripts in [ + vec![prose()], + vec![ + call("Bash", json!({"command":"touch should-not-exist"})), + prose(), + ], + vec![ + call("SubmitBackendJobResult", json!({"result":{"ok":true}})), + prose(), + ], + vec![ + call( + SUBMIT_JOB_RESULT_TOOL, + json!({"job_id":"forged","result":{"ok":true}}), + ), + prose(), + ], + ] { + let sink = Arc::new(RecordingSink::default()); + let outcome = prepared( + root.path(), + &request("user:a"), + ScriptedClient::new(scripts), + ) + .run("attempt".into(), sink.clone(), |_| {}) + .await; + assert!( + matches!(outcome, Err(JobExecutionError::MissingResult)), + "{outcome:?}" + ); + assert!(sink.0.lock().unwrap().is_empty()); + assert!(!root.path().join("should-not-exist").exists()); + } +} + +#[tokio::test] +async fn result_size_and_identity_are_fenced_before_host_submission() { + let root = tempfile::tempdir().unwrap(); + let mut request = request("user:a"); + request.limits.max_result_bytes = 5; + let sink = Arc::new(RecordingSink::default()); + let job = prepared( + root.path(), + &request, + ScriptedClient::new(vec![ + call(SUBMIT_JOB_RESULT_TOOL, json!({"result":"too big"})), + prose(), + ]), + ); + assert!(matches!( + job.run("attempt".into(), sink.clone(), |_| {}).await, + Err(JobExecutionError::MissingResult) + )); + assert!(sink.0.lock().unwrap().is_empty()); +} + +#[tokio::test] +async fn cancellation_and_errors_return_without_acceptance() { + let root = tempfile::tempdir().unwrap(); + let mut client = ScriptedClient::new(vec![]); + client.pending = true; + let entered = client.entered.clone(); + let job = prepared(root.path(), &request("user:a"), client); + let sink = Arc::new(RecordingSink::default()); + let (tx, rx) = tokio::sync::oneshot::channel(); + let run = tokio::spawn(async move { + job.run("attempt".into(), sink, move |sender| { + let _ = tx.send(sender); + }) + .await + }); + let sender = rx.await.unwrap(); + entered.notified().await; + sender.send(()).await.unwrap(); + assert!(matches!( + tokio::time::timeout(std::time::Duration::from_secs(2), run) + .await + .unwrap() + .unwrap(), + Err(JobExecutionError::Cancelled) + )); + assert!( + sender.is_closed(), + "normal Worker release must close cancellation receiver" + ); + let mut client = ScriptedClient::new(vec![]); + client.fail = true; + let (tx, rx) = tokio::sync::oneshot::channel(); + let outcome = prepared(root.path(), &request("user:a"), client) + .run( + "attempt".into(), + Arc::new(RecordingSink::default()), + move |sender| { + let _ = tx.send(sender); + }, + ) + .await; + assert!(matches!(outcome, Err(JobExecutionError::Worker(_)))); + assert!( + rx.await.unwrap().is_closed(), + "error must release the Worker" + ); +} + +#[tokio::test] +async fn accepted_result_survives_later_provider_error_and_cancellation() { + let root = tempfile::tempdir().unwrap(); + for cancel in [false, true] { + let mut client = + ScriptedClient::new(vec![call(SUBMIT_JOB_RESULT_TOOL, json!({"result":true}))]); + client.pending = cancel; + client.fail = !cancel; + let entered = client.entered.clone(); + let sink = Arc::new(RecordingSink::default()); + let job = prepared(root.path(), &request("user:a"), client); + let (tx, rx) = tokio::sync::oneshot::channel(); + let recording = sink.clone(); + let run = tokio::spawn(async move { + job.run("attempt".into(), recording, move |sender| { + let _ = tx.send(sender); + }) + .await + }); + let sender = rx.await.unwrap(); + if cancel { + entered.notified().await; + assert_eq!( + sink.0.lock().unwrap().len(), + 1, + "acceptance must precede cancellation" + ); + sender.send(()).await.unwrap(); + } + let result = tokio::time::timeout(std::time::Duration::from_secs(2), run) + .await + .unwrap() + .unwrap() + .unwrap(); + assert_eq!(result.submission.result, json!(true)); + assert_eq!(sink.0.lock().unwrap().len(), 1); + assert!(sender.is_closed()); + } +} + +#[tokio::test] +async fn turn_limit_is_bounded_and_accepted_result_survives_later_failure() { + let root = tempfile::tempdir().unwrap(); + let registry = registry( + root.path(), + artifact("default", 512, 1), + artifact("default", 512, 1), + ); + for (tool, expected) in [("UnknownTool", false), (SUBMIT_JOB_RESULT_TOOL, true)] { + let client = ScriptedClient::new(vec![call(tool, json!({"result":true}))]); + let prepared = prepare_job_from_registry( + &request("user:a"), + root.path(), + Some(Box::new(client.clone())), + ®istry, + ) + .unwrap(); + let outcome = prepared + .run("attempt".into(), Arc::new(RecordingSink::default()), |_| {}) + .await; + assert_eq!(outcome.is_ok(), expected); + assert_eq!(client.requests.lock().unwrap().len(), 1); + } +} + +#[test] +fn profiles_with_unavailable_requirements_are_rejected_without_name_allowlist_or_fallback() { + let root = tempfile::tempdir().unwrap(); + let base = artifact("default", 512, 3); + for (key, value, needle) in [ + ("feature", json!({"task":{"enabled":true}}), "feature.task"), + ( + "feature", + json!({"workspace_config":{"enabled":true}}), + "feature.workspace_config", + ), + ( + "feature", + json!({"subjektiv":{"consolidation_tools":true}}), + "feature.subjektiv", + ), + ( + "feature", + json!({"memory":{"staging_tools":true}}), + "feature.memory", + ), + ("scope", json!("workspace_read"), "scope"), + ( + "delegation_scope", + json!("workspace_write"), + "delegation_scope", + ), + ( + "session", + json!({"record_event_trace":true}), + "session.record_event_trace", + ), + ("web", json!({"enabled":true}), "web"), + ( + "compaction", + json!({"kind":"tokens","threshold":10000}), + "compaction", + ), + ("worker", json!({"mode":"wip"}), "worker.mode=wip"), + ("skills", json!({"directories":["skills"]}), "skills"), + ( + "mcp", + json!({"stdio_server":[{"name":"test","command":"fake"}]}), + "mcp", + ), + ] { + let mut profile = base.clone(); + profile[key] = value; + let registry = registry(root.path(), profile, base.clone()); + let error = prepare_job_from_registry( + &request("user:a"), + root.path(), + Some(Box::new(ScriptedClient::new(vec![]))), + ®istry, + ) + .err() + .unwrap(); + assert!( + matches!(error, JobExecutionError::UnsupportedProfile(_)), + "{error}" + ); + assert!(error.to_string().contains(needle), "{error}"); + } + for name in [ + "web", + "image", + "sub_worker", + "flow", + "worker", + "workspace_worker_discovery", + "objective", + "manage_workdir", + "ticket", + "orchestration", + ] { + let mut profile = base.clone(); + profile["feature"] = json!({name:{"enabled":true}}); + let registry = registry(root.path(), profile, base.clone()); + assert!(matches!( + prepare_job_from_registry( + &request("user:a"), + root.path(), + Some(Box::new(ScriptedClient::new(vec![]))), + ®istry + ), + Err(JobExecutionError::UnsupportedProfile(_)) + )); + } + let registry = registry(root.path(), base.clone(), base); + for selector in ["user:missing", "project:a", "./a.json", "inherit"] { + assert!( + prepare_job_from_registry( + &request(selector), + root.path(), + Some(Box::new(ScriptedClient::new(vec![]))), + ®istry + ) + .is_err() + ); + } + let builtin = request("builtin:backend-job"); + assert!(matches!( + prepare_job_from_registry( + &builtin, + root.path(), + Some(Box::new(ScriptedClient::new(vec![]))), + ®istry + ), + Err(JobExecutionError::UnsupportedProfile(_)) + )); +} + +#[test] +fn unknown_prompt_and_model_are_not_substituted_even_with_injected_client() { + let root = tempfile::tempdir().unwrap(); + let mut unknown_model = artifact("default", 512, 3); + unknown_model["model"] = json!({"ref":"not-a-provider/not-a-model"}); + let registry = registry( + root.path(), + artifact("internal.missing", 512, 3), + unknown_model, + ); + for (profile, prompt_error) in [("user:a", true), ("user:b", false)] { + let error = prepare_job_from_registry( + &request(profile), + root.path(), + Some(Box::new(ScriptedClient::new(vec![]))), + ®istry, + ) + .err() + .unwrap(); + if prompt_error { + assert!( + matches!(error, JobExecutionError::Instruction(_)), + "{error}" + ); + } else { + assert!(matches!(error, JobExecutionError::Model(_)), "{error}"); + } + } +} + +struct UnknownSink(Mutex>); +impl JobResultSink for UnknownSink { + fn submit(&self, value: ::job::JobResultSubmission) -> Result<(), String> { + self.0.lock().unwrap().push(value); + Err("unknown".into()) + } +} + +#[tokio::test] +async fn unconfirmed_sink_acceptance_is_not_classified_as_missing_result_or_known_failure() { + let root = tempfile::tempdir().unwrap(); + let sink = Arc::new(UnknownSink(Mutex::new(vec![]))); + let job = prepared( + root.path(), + &request("user:a"), + ScriptedClient::new(vec![ + call(SUBMIT_JOB_RESULT_TOOL, json!({"result":true})), + prose(), + ]), + ); + assert!(matches!( + job.run("attempt".into(), sink.clone(), |_| {}).await, + Err(JobExecutionError::ResultUnconfirmed(_)) + )); + assert_eq!(sink.0.lock().unwrap().len(), 1); +} + +#[tokio::test] +async fn feature_registration_rejects_forged_fences_seals_unknown_outcomes_and_honors_cancel() { + let sink = Arc::new(UnknownSink(Mutex::new(vec![]))); + let feature = + JobResultFeature::new(&request("user:a"), "attempt".into(), sink.clone()).unwrap(); + let mut pending = vec![]; + let mut hooks = crate::HookRegistryBuilder::new(); + let report = FeatureRegistryBuilder::new() + .with_module(feature.clone()) + .install_into_pending(&mut pending, &mut hooks); + assert_eq!(report.installed_tool_names(), [SUBMIT_JOB_RESULT_TOOL]); + let (_, tool) = pending.remove(0)(); + assert!( + tool.execute( + r#"{"attempt_id":"forged","result":true}"#, + ToolExecutionContext::direct() + ) + .await + .is_err() + ); + assert!(sink.0.lock().unwrap().is_empty()); + assert!( + tool.execute(r#"{"result":true}"#, ToolExecutionContext::direct()) + .await + .is_err() + ); + assert!( + tool.execute(r#"{"result":false}"#, ToolExecutionContext::direct()) + .await + .is_err() + ); + assert!( + tool.execute(r#"{"result":true}"#, ToolExecutionContext::direct()) + .await + .is_err() + ); + assert_eq!(sink.0.lock().unwrap().len(), 2); + let context = ToolExecutionContext::new("cancelled", "batch", 0); + feature.cancel_execution(&context).await.unwrap(); + assert!(matches!( + tool.execute(r#"{"result":true}"#, context).await, + Err(agen::tool::ToolError::Cancelled(_)) + )); + assert_eq!(sink.0.lock().unwrap().len(), 2); +} diff --git a/crates/worker/src/lib.rs b/crates/worker/src/lib.rs index 1dbc8a526..07daef4e2 100644 --- a/crates/worker/src/lib.rs +++ b/crates/worker/src/lib.rs @@ -12,6 +12,7 @@ pub mod fs_view; pub mod hook; pub(crate) mod in_flight; pub mod ipc; +pub mod job; pub mod model_client; mod paste_artifact_tool; pub mod prompt; diff --git a/crates/worker/src/prompt/catalog.rs b/crates/worker/src/prompt/catalog.rs index 9b3e30459..1b72b8415 100644 --- a/crates/worker/src/prompt/catalog.rs +++ b/crates/worker/src/prompt/catalog.rs @@ -97,6 +97,7 @@ pub enum WorkerPrompt { SubjektivMemoryConsolidationSystem, SubjektivMemorySurfaceSystem, FlowVerifierSystem, + JobSystem, BackendJobSystem, NotifyWrapper, InterruptToolResultSummary, @@ -121,6 +122,7 @@ impl WorkerPrompt { } Self::SubjektivMemorySurfaceSystem => "internal.subjektiv_memory_surface_system", Self::FlowVerifierSystem => "internal.flow_verifier_system", + Self::JobSystem => "internal.job_system", Self::BackendJobSystem => "internal.backend_job_system", Self::NotifyWrapper => "internal.notify_wrapper", Self::InterruptToolResultSummary => "internal.interrupt_tool_result_summary", @@ -144,6 +146,7 @@ impl WorkerPrompt { WorkerPrompt::SubjektivMemoryConsolidationSystem, WorkerPrompt::SubjektivMemorySurfaceSystem, WorkerPrompt::FlowVerifierSystem, + WorkerPrompt::JobSystem, WorkerPrompt::BackendJobSystem, WorkerPrompt::NotifyWrapper, WorkerPrompt::InterruptToolResultSummary, diff --git a/crates/workspace-server/Cargo.toml b/crates/workspace-server/Cargo.toml index 521b7f4ac..d89f9e161 100644 --- a/crates/workspace-server/Cargo.toml +++ b/crates/workspace-server/Cargo.toml @@ -18,6 +18,7 @@ futures.workspace = true config-source.workspace = true flow = { path = "../flow" } manifest.workspace = true +job.workspace = true protocol = { workspace = true } runtime-api.workspace = true server-api.workspace = true diff --git a/crates/workspace-server/src/backend_job.rs b/crates/workspace-server/src/backend_job.rs index f5b54d7a4..f7c6fc827 100644 --- a/crates/workspace-server/src/backend_job.rs +++ b/crates/workspace-server/src/backend_job.rs @@ -7,26 +7,28 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; -use sha2::{Digest, Sha256}; use worker_runtime::identity::RuntimeWorkerRef; use crate::{Error, Result}; pub const BACKEND_JOB_RUNTIME_ID: &str = crate::hosts::EMBEDDED_RUNTIME_ID; -pub const DEFAULT_MAX_CONCURRENT_JOBS: u16 = 8; -pub const ABSOLUTE_MAX_CONCURRENT_JOBS: u16 = 64; -pub const DEFAULT_JOB_TIMEOUT_SECONDS: u32 = 120; -pub const MAX_JOB_TIMEOUT_SECONDS: u32 = 600; -pub const DEFAULT_MAX_RESULT_BYTES: u32 = 16 * 1024; -pub const ABSOLUTE_MAX_RESULT_BYTES: u32 = 64 * 1024; -pub const DEFAULT_MAX_ATTEMPTS: u8 = 2; -pub const ABSOLUTE_MAX_ATTEMPTS: u8 = 3; -pub const MAX_JOB_INPUT_BYTES: usize = 128 * 1024; -pub const MAX_JOB_INSTRUCTION_BYTES: usize = 16 * 1024; -pub const MAX_JOB_PURPOSE_BYTES: usize = 120; -pub const MAX_JOB_REFERENCE_BYTES: usize = 512; -pub const MAX_JOB_FAILURE_BYTES: usize = 1024; -pub const MAX_JOB_DELIVERY_BYTES: usize = 16 * 1024; +pub use job::{ + ABSOLUTE_MAX_ATTEMPTS, ABSOLUTE_MAX_CONCURRENT_JOBS, ABSOLUTE_MAX_RESULT_BYTES, + DEFAULT_JOB_TIMEOUT_SECONDS, DEFAULT_MAX_ATTEMPTS, DEFAULT_MAX_CONCURRENT_JOBS, + DEFAULT_MAX_RESULT_BYTES, JobResultSubmission as BackendJobResultSubmission, + MAX_JOB_DELIVERY_BYTES, MAX_JOB_FAILURE_BYTES, MAX_JOB_INPUT_BYTES, MAX_JOB_INSTRUCTION_BYTES, + MAX_JOB_PURPOSE_BYTES, MAX_JOB_REFERENCE_BYTES, MAX_JOB_TIMEOUT_SECONDS, attempt_id, + bounded_failure_detail, +}; + +// Preserve the Backend InvalidInput surface and its exact historical messages. +fn backend_error(error: job::JobError) -> Error { + match error { + job::JobError::InvalidInput(detail) => { + Error::InvalidInput(detail.replacen("Job ", "Backend Job ", 1)) + } + } +} #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] @@ -37,6 +39,17 @@ pub enum BackendJobState { Unknown, } +impl From for job::JobState { + fn from(state: BackendJobState) -> Self { + match state { + BackendJobState::Pending => Self::Pending, + BackendJobState::Completed => Self::Completed, + BackendJobState::Failed => Self::Failed, + BackendJobState::Unknown => Self::Unknown, + } + } +} + impl BackendJobState { pub(crate) fn as_str(self) -> &'static str { match self { @@ -69,6 +82,19 @@ pub enum BackendJobAttemptState { Unknown, } +impl From for job::JobAttemptState { + fn from(state: BackendJobAttemptState) -> Self { + match state { + BackendJobAttemptState::Reserved => Self::Reserved, + BackendJobAttemptState::Dispatching => Self::Dispatching, + BackendJobAttemptState::Dispatched => Self::Dispatched, + BackendJobAttemptState::Completed => Self::Completed, + BackendJobAttemptState::Failed => Self::Failed, + BackendJobAttemptState::Unknown => Self::Unknown, + } + } +} + impl BackendJobAttemptState { pub(crate) fn as_str(self) -> &'static str { match self { @@ -161,28 +187,29 @@ impl Default for BackendJobLimits { impl BackendJobLimits { pub fn validate(&self) -> Result<()> { - if self.max_concurrent_jobs == 0 || self.max_concurrent_jobs > ABSOLUTE_MAX_CONCURRENT_JOBS - { - return Err(Error::InvalidInput(format!( - "Backend Job max_concurrent_jobs must be in 1..={ABSOLUTE_MAX_CONCURRENT_JOBS}" - ))); + job::JobLimits::from(self).validate().map_err(backend_error) + } +} + +impl From<&BackendJobLimits> for job::JobLimits { + fn from(limits: &BackendJobLimits) -> Self { + Self { + max_concurrent_jobs: limits.max_concurrent_jobs, + timeout_seconds: limits.timeout_seconds, + max_result_bytes: limits.max_result_bytes, + max_attempts: limits.max_attempts, } - if self.timeout_seconds == 0 || self.timeout_seconds > MAX_JOB_TIMEOUT_SECONDS { - return Err(Error::InvalidInput(format!( - "Backend Job timeout_seconds must be in 1..={MAX_JOB_TIMEOUT_SECONDS}" - ))); + } +} + +impl From for BackendJobLimits { + fn from(limits: job::JobLimits) -> Self { + Self { + max_concurrent_jobs: limits.max_concurrent_jobs, + timeout_seconds: limits.timeout_seconds, + max_result_bytes: limits.max_result_bytes, + max_attempts: limits.max_attempts, } - if self.max_result_bytes == 0 || self.max_result_bytes > ABSOLUTE_MAX_RESULT_BYTES { - return Err(Error::InvalidInput(format!( - "Backend Job max_result_bytes must be in 1..={ABSOLUTE_MAX_RESULT_BYTES}" - ))); - } - if self.max_attempts == 0 || self.max_attempts > ABSOLUTE_MAX_ATTEMPTS { - return Err(Error::InvalidInput(format!( - "Backend Job max_attempts must be in 1..={ABSOLUTE_MAX_ATTEMPTS}" - ))); - } - Ok(()) } } @@ -261,17 +288,29 @@ pub struct BackendJobRequest { } impl BackendJobRequest { - pub fn validate(&self) -> Result<()> { - validate_identifier("job_id", &self.job_id, 256)?; - validate_identifier("purpose", &self.purpose, MAX_JOB_PURPOSE_BYTES)?; - validate_identifier("input_revision", &self.input_revision, 256)?; - validate_bounded_text("input_ref", &self.input_ref, MAX_JOB_REFERENCE_BYTES)?; - validate_bounded_text("instruction", &self.instruction, MAX_JOB_INSTRUCTION_BYTES)?; - validate_identifier("profile", &self.profile, 256)?; - self.profile_selector()?; - if let Some(key) = &self.serialization_key { - validate_identifier("serialization_key", key, MAX_JOB_REFERENCE_BYTES)?; + /// Adapt neutral intent without granting domain access or writer/notification + /// authority. Validation remains the Backend submission boundary's job; + /// the caller-selected profile is retained exactly, with no fallback. + pub fn from_job_request(request: job::JobRequest) -> Self { + Self { + job_id: request.job_id, + purpose: request.purpose, + input_revision: request.input_revision, + input_ref: request.input_ref, + input: request.input, + instruction: request.instruction, + profile: request.profile, + grants: BackendJobGrants::default(), + serialization_key: request.serialization_key, + source_worker: None, + notification_target: None, + limits: request.limits.into(), } + } + + pub fn validate(&self) -> Result<()> { + let request = self.to_job_request(); + request.validate_fields().map_err(backend_error)?; if let Some(grant) = &self.grants.subjektiv_consolidation { validate_identifier("grant subject_id", &grant.subject_id, 256)?; if grant.candidate_ids.len() > 256 { @@ -297,44 +336,30 @@ impl BackendJobRequest { )); } } - let input = serde_json::to_vec(&self.input).map_err(|error| { - Error::InvalidInput(format!("serialize Backend Job input: {error}")) - })?; - if input.len() > MAX_JOB_INPUT_BYTES { - return Err(Error::InvalidInput(format!( - "Backend Job input exceeds {MAX_JOB_INPUT_BYTES} bytes" - ))); - } + request.validate_input().map_err(backend_error)?; self.limits.validate() } + /// Transport-neutral projection only. Domain grants, writer authority and + /// derived resource locks remain Backend-owned; never fingerprint this + /// projection as a substitute for the full Backend intent. + pub fn to_job_request(&self) -> job::JobRequest { + job::JobRequest { + job_id: self.job_id.clone(), + purpose: self.purpose.clone(), + input_revision: self.input_revision.clone(), + input_ref: self.input_ref.clone(), + input: self.input.clone(), + instruction: self.instruction.clone(), + profile: self.profile.clone(), + serialization_key: self.serialization_key.clone(), + limits: (&self.limits).into(), + } + } + /// Validate selector syntax only. Registry existence and resolution are dispatch authority. pub fn profile_selector(&self) -> Result { - use manifest::ProfileSelector; - validate_identifier("profile", &self.profile, 256)?; - let selector = ProfileSelector::parse_cli(&self.profile); - let valid = match &selector { - ProfileSelector::Default => true, - ProfileSelector::Path { .. } => false, - ProfileSelector::Named { source, name } => { - !name.is_empty() - && !name.starts_with('.') - && !name.contains(['/', '\\', ':']) - && name - .chars() - .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.')) - && ![".toml", ".json", ".dcdl", ".nix"] - .iter() - .any(|suffix| name.ends_with(suffix)) - && (source.is_some() || name != "inherit") - } - }; - if !valid { - return Err(Error::InvalidInput( - "Backend Job profile must be a manifest registry selector, not raw source or a path".into(), - )); - } - Ok(selector) + job::profile_selector(&self.profile).map_err(backend_error) } /// Effective single-flight key. Typed grants cannot bypass their subject fence. @@ -348,20 +373,12 @@ impl BackendJobRequest { pub fn fingerprint(&self) -> Result { self.validate()?; - let encoded = serde_json::to_vec(self).map_err(|error| { - Error::InvalidInput(format!("serialize Backend Job intent: {error}")) - })?; - Ok(sha256(&encoded)) + job::fingerprint(self).map_err(backend_error) } pub fn worker_input(&self, attempt_id: &str) -> Result { - let input = serde_json::to_string(&self.input).map_err(|error| { - Error::InvalidInput(format!("serialize Backend Job input: {error}")) - })?; - Ok(format!( - "{}\n\nBackend Job envelope (immutable):\njob_id: {}\nattempt_id: {}\ninput_revision: {}\ninput_ref: {}\ninput_json: {}\n\nReturn success only through the structured Backend Job result capability. Final prose and Worker Idle/Stopped state are not result authority.", - self.instruction, self.job_id, attempt_id, self.input_revision, self.input_ref, input - )) + job::worker_input_with_label(&self.to_job_request(), attempt_id, "Backend Job") + .map_err(backend_error) } } @@ -451,13 +468,10 @@ pub struct BackendJobReservation { pub resource_reused: bool, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(deny_unknown_fields)] -pub struct BackendJobResultSubmission { - pub job_id: String, - pub attempt_id: String, - pub input_revision: String, - pub result: Value, +impl BackendJobReservation { + pub fn outcome(&self) -> job::JobOutcome { + job_outcome(&self.job, &self.attempt) + } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] @@ -467,6 +481,28 @@ pub struct BackendJobResultAcceptance { pub replayed: bool, } +impl BackendJobResultAcceptance { + pub fn outcome(&self) -> job::JobOutcome { + job_outcome(&self.job, &self.attempt) + } +} + +// Project the durable Job outcome, not Worker lifecycle, cleanup or delivery. +// The paired attempt supplies its own identity, number and execution state. +fn job_outcome(job: &BackendJobRecord, attempt: &BackendJobAttemptRecord) -> job::JobOutcome { + job::JobOutcome { + job_id: job.request.job_id.clone(), + input_revision: job.request.input_revision.clone(), + attempt_id: attempt.attempt_id.clone(), + attempt: attempt.attempt, + state: job.state.into(), + attempt_state: attempt.state.into(), + result: job.result.clone(), + failure_category: job.failure_category.clone(), + failure_detail: job.failure_detail.clone(), + } +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct BackendJobWorkerBinding { pub job_id: String, @@ -489,10 +525,6 @@ pub struct BackendJobDeliveryRecord { pub delivered_at: Option, } -pub fn attempt_id(job_id: &str, attempt: u8) -> String { - format!("{job_id}:attempt:{attempt}") -} - pub fn allocation_key(attempt_id: &str) -> String { format!("backend-job:{attempt_id}") } @@ -515,64 +547,15 @@ pub fn delivery_id(job_id: &str, attempt_id: &str) -> String { } fn tracked_request_id(kind: &str, identity: &[u8]) -> String { - format!("backend-job-{kind}:{}", sha256(identity)) + format!("backend-job-{kind}:{}", job::sha256(identity)) } pub fn result_digest(result: &Value, max_bytes: u32) -> Result<(String, String)> { - let encoded = serde_json::to_string(result) - .map_err(|error| Error::InvalidInput(format!("serialize Backend Job result: {error}")))?; - if encoded.len() > max_bytes as usize { - return Err(Error::InvalidInput(format!( - "Backend Job result exceeds {max_bytes} bytes" - ))); - } - Ok((sha256(encoded.as_bytes()), encoded)) -} - -pub(crate) fn bounded_failure_detail(detail: &str) -> String { - if detail.len() <= MAX_JOB_FAILURE_BYTES { - return detail.to_string(); - } - let mut end = MAX_JOB_FAILURE_BYTES; - while !detail.is_char_boundary(end) { - end -= 1; - } - detail[..end].to_string() + job::result_digest(result, max_bytes).map_err(backend_error) } fn validate_identifier(field: &str, value: &str, max_bytes: usize) -> Result<()> { - validate_bounded_text(field, value, max_bytes)?; - if value.chars().any(char::is_control) { - return Err(Error::InvalidInput(format!( - "Backend Job {field} must not contain control characters" - ))); - } - Ok(()) -} - -fn validate_bounded_text(field: &str, value: &str, max_bytes: usize) -> Result<()> { - if value.is_empty() || value.trim() != value { - return Err(Error::InvalidInput(format!( - "Backend Job {field} must be non-empty and trimmed" - ))); - } - if value.len() > max_bytes { - return Err(Error::InvalidInput(format!( - "Backend Job {field} exceeds {max_bytes} bytes" - ))); - } - Ok(()) -} - -fn sha256(bytes: &[u8]) -> String { - let digest = Sha256::digest(bytes); - let mut output = String::with_capacity(7 + digest.len() * 2); - output.push_str("sha256:"); - for byte in digest { - use std::fmt::Write as _; - let _ = write!(output, "{byte:02x}"); - } - output + job::validate_identifier(field, value, max_bytes).map_err(backend_error) } #[cfg(test)] @@ -596,6 +579,258 @@ mod tests { } } + fn reservation() -> BackendJobReservation { + serde_json::from_value(serde_json::json!({ + "job": { + "workspace_id": "workspace-a", + "request": request(), + "intent_fingerprint": request().fingerprint().unwrap(), + "state": "pending", + "current_attempt": 2, + "created_at": "created", + "updated_at": "updated" + }, + "attempt": { + "workspace_id": "workspace-a", + "job_id": "ticket-check:T-1:r7", + "attempt_id": "ticket-check:T-1:r7:attempt:2", + "attempt": 2, + "input_revision": "7", + "state": "reserved", + "worker": {"runtime_id": "runtime-a", "worker_id": "worker-a"}, + "runtime_run_id": "run-a", + "deadline_at": "deadline", + "created_at": "created", + "updated_at": "updated" + }, + "replayed": false, + "resource_reused": false + })) + .unwrap() + } + + #[test] + fn backend_state_projections_preserve_existing_wire_vocabulary() { + for state in [ + BackendJobState::Pending, + BackendJobState::Completed, + BackendJobState::Failed, + BackendJobState::Unknown, + ] { + let shared = job::JobState::from(state); + assert_eq!( + serde_json::to_value(state).unwrap(), + serde_json::to_value(shared).unwrap() + ); + } + for state in [ + BackendJobAttemptState::Reserved, + BackendJobAttemptState::Dispatching, + BackendJobAttemptState::Dispatched, + BackendJobAttemptState::Completed, + BackendJobAttemptState::Failed, + BackendJobAttemptState::Unknown, + ] { + let shared = job::JobAttemptState::from(state); + assert_eq!( + serde_json::to_value(state).unwrap(), + serde_json::to_value(shared).unwrap() + ); + } + // The shared read vocabulary does not add Backend cancellation support. + assert!(BackendJobState::parse("cancelled").is_err()); + assert!(BackendJobAttemptState::parse("cancelled").is_err()); + assert!(serde_json::from_str::("\"cancelled\"").is_err()); + assert!(serde_json::from_str::("\"cancelled\"").is_err()); + } + + #[test] + fn neutral_request_round_trip_does_not_infer_authority_or_replace_profile() { + for profile in ["builtin:backend-job", "builtin:job", "project:custom-job"] { + for key in [None, Some("resource:7".to_string())] { + let mut neutral = request().to_job_request(); + neutral.profile = profile.into(); + neutral.serialization_key = key; + neutral.limits = job::JobLimits { + max_concurrent_jobs: 3, + timeout_seconds: 17, + max_result_bytes: 777, + max_attempts: 1, + }; + let backend = BackendJobRequest::from_job_request(neutral.clone()); + backend.validate().unwrap(); + assert_eq!(backend.to_job_request(), neutral); + assert!(backend.grants.is_empty()); + assert!(backend.source_worker.is_none()); + assert!(backend.notification_target.is_none()); + let encoded = serde_json::to_value(&backend).unwrap(); + for field in ["grants", "source_worker", "notification_target"] { + assert!(encoded.get(field).is_none()); + } + assert_eq!(encoded["profile"], profile); + } + } + // Conversion is not validation and must not normalize an invalid request. + let mut neutral = request().to_job_request(); + neutral.limits.timeout_seconds = 0; + neutral.profile = "inherit".into(); + let backend = BackendJobRequest::from_job_request(neutral.clone()); + assert_eq!(backend.to_job_request(), neutral); + assert!(backend.validate().is_err()); + } + + #[test] + fn reservation_and_acceptance_outcomes_expose_durable_job_and_attempt_not_host_lifecycle() { + let mut reservation = reservation(); + let encoded_before = serde_json::to_value(&reservation).unwrap(); + assert_eq!( + reservation.outcome(), + job::JobOutcome { + job_id: reservation.job.request.job_id.clone(), + input_revision: "7".into(), + attempt_id: "ticket-check:T-1:r7:attempt:2".into(), + attempt: 2, + state: job::JobState::Pending, + attempt_state: job::JobAttemptState::Reserved, + result: None, + failure_category: None, + failure_detail: None, + } + ); + assert_eq!(serde_json::to_value(&reservation).unwrap(), encoded_before); + for (state, attempt_state) in [ + ( + BackendJobState::Pending, + BackendJobAttemptState::Dispatching, + ), + (BackendJobState::Pending, BackendJobAttemptState::Dispatched), + (BackendJobState::Failed, BackendJobAttemptState::Failed), + (BackendJobState::Unknown, BackendJobAttemptState::Unknown), + ] { + reservation.job.state = state; + reservation.attempt.state = attempt_state; + reservation.job.failure_category = Some("job-category".into()); + reservation.job.failure_detail = Some("job-detail".into()); + reservation.attempt.failure_category = Some("attempt-category".into()); + let outcome = reservation.outcome(); + assert_eq!(outcome.state, state.into()); + assert_eq!(outcome.attempt_state, attempt_state.into()); + assert_eq!(outcome.failure_category.as_deref(), Some("job-category")); + assert_eq!(outcome.failure_detail.as_deref(), Some("job-detail")); + assert!(outcome.result.is_none()); + let accepted = BackendJobResultAcceptance { + job: reservation.job.clone(), + attempt: reservation.attempt.clone(), + replayed: true, + }; + assert_eq!(accepted.outcome(), outcome); + } + reservation.job.state = BackendJobState::Completed; + reservation.attempt.state = BackendJobAttemptState::Completed; + reservation.job.result = Some(serde_json::json!({"ok": true})); + reservation.job.failure_category = None; + reservation.job.failure_detail = None; + reservation.attempt.worker_cleanup_state = Some(BackendJobWorkerCleanupState::Failed); + reservation.attempt.worker_cleanup_failure_detail = + Some("cleanup is not model failure".into()); + reservation.replayed = true; + reservation.resource_reused = true; + let acceptance = BackendJobResultAcceptance { + job: reservation.job.clone(), + attempt: reservation.attempt.clone(), + replayed: false, + }; + let outcome = acceptance.outcome(); + assert_eq!(outcome, reservation.outcome()); + assert_eq!(outcome.state, job::JobState::Completed); + assert_eq!(outcome.result, Some(serde_json::json!({"ok": true}))); + assert!(outcome.failure_category.is_none()); + assert!(outcome.failure_detail.is_none()); + let encoded = serde_json::to_value(outcome).unwrap(); + assert_eq!(encoded.as_object().unwrap().len(), 9); + for field in [ + "workspace_id", + "worker", + "runtime_run_id", + "request", + "replayed", + "resource_reused", + "worker_cleanup_state", + ] { + assert!(encoded.get(field).is_none()); + } + } + + #[test] + fn extraction_preserves_backend_wire_fingerprint_and_worker_envelope() { + let request = request(); + let encoded = serde_json::to_string(&request).unwrap(); + assert_eq!( + encoded, + r#"{"job_id":"ticket-check:T-1:r7","purpose":"ticket_item_check","input_revision":"7","input_ref":"ticket://T-1/revisions/7","input":{"title":"Check me"},"instruction":"Check this immutable Ticket snapshot.","profile":"builtin:backend-job","source_worker":{"runtime_id":"runtime-a","worker_id":"worker-a"},"notification_target":{"runtime_id":"runtime-a","worker_id":"worker-a"},"limits":{"max_concurrent_jobs":8,"timeout_seconds":120,"max_result_bytes":16384,"max_attempts":2}}"# + ); + assert_eq!( + request.fingerprint().unwrap(), + "sha256:920187fa9efa111a97c4d6869d134943e45b52edb2861cad19a0a173233c8f60" + ); + let neutral = request.to_job_request(); + neutral.validate().unwrap(); + assert_ne!( + request.fingerprint().unwrap(), + neutral.fingerprint().unwrap() + ); + assert_eq!( + serde_json::to_value(&request.limits).unwrap(), + serde_json::to_value(&neutral.limits).unwrap() + ); + assert_eq!( + request.worker_input("attempt-1").unwrap(), + "Check this immutable Ticket snapshot.\n\nBackend Job envelope (immutable):\njob_id: ticket-check:T-1:r7\nattempt_id: attempt-1\ninput_revision: 7\ninput_ref: ticket://T-1/revisions/7\ninput_json: {\"title\":\"Check me\"}\n\nReturn success only through the structured Backend Job result capability. Final prose and Worker Idle/Stopped state are not result authority." + ); + } + + #[test] + fn shared_result_contract_preserves_backend_wire_and_errors() { + let submission = BackendJobResultSubmission { + job_id: "job-1".into(), + attempt_id: "job-1:attempt:1".into(), + input_revision: "7".into(), + result: serde_json::json!({"ok": true}), + }; + let encoded = serde_json::to_string(&submission).unwrap(); + assert_eq!( + encoded, + r#"{"job_id":"job-1","attempt_id":"job-1:attempt:1","input_revision":"7","result":{"ok":true}}"# + ); + let neutral: job::JobResultSubmission = serde_json::from_str(&encoded).unwrap(); + assert_eq!(neutral, submission); + assert_eq!( + result_digest(&submission.result, 128).unwrap(), + job::result_digest(&submission.result, 128).unwrap() + ); + assert!( + matches!(result_digest(&submission.result, 4), Err(Error::InvalidInput(detail)) if detail == "Backend Job result exceeds 4 bytes") + ); + let mut request = request(); + request.job_id = "".into(); + assert!( + matches!(request.validate(), Err(Error::InvalidInput(detail)) if detail == "Backend Job job_id must be non-empty and trimmed") + ); + request.job_id = "job-1".into(); + request.limits.timeout_seconds = 0; + assert!( + matches!(request.validate(), Err(Error::InvalidInput(detail)) if detail == "Backend Job timeout_seconds must be in 1..=600") + ); + // Preserve domain-before-input/limits error ordering as well. + request.grants.subjektiv_consolidation = Some(SubjektivConsolidationGrant { + subject_id: "".into(), + candidate_ids: vec![], + }); + assert!( + matches!(request.validate(), Err(Error::InvalidInput(detail)) if detail == "Backend Job grant subject_id must be non-empty and trimmed") + ); + } + #[test] fn fingerprint_covers_revision_profile_limits_and_notification_provenance() { let request = request(); diff --git a/docs/design/standalone-agent-host.md b/docs/design/standalone-agent-host.md index e8ac94f20..984014456 100644 --- a/docs/design/standalone-agent-host.md +++ b/docs/design/standalone-agent-host.md @@ -26,7 +26,8 @@ worker - Controller transport は `InProcess` に固定する。通常起動で Worker subprocess、HTTP/WS server、Unix socket を作らない。 - model provider は通常の resolved Manifest から構築する。埋め込み host と deterministic test は `start_with_model_client` で同じ bootstrap に process-owned client を注入できる。 - feature plan/install は既存 `WorkerController` が行う。Task や optional direct SubWorker を standalone 側で再実装しない。 -- `shutdown()` は既存 `Method::Shutdown` を送り、controller が active run、SubWorker registry、Workdir session、MachineScope allocation を順に片付けた後の confirmation を待つ。 +- `shutdown()` は Host-owned Job service の scheduling を閉じ、Internal Worker の取消・cleanup とローカル SQLite connection の close を確認してから、既存 `Method::Shutdown` を送る。controller が active run、SubWorker registry、Workdir session、MachineScope allocation を順に片付けた後の confirmation を待つ。cleanup を確認できない drop/error では lease を保持する。 +- `jobs()` は Feature へ明示的に渡す汎用 Job 実行 capability。対話 Session・モデル作成 SubWorker とは別で、Profile/result authority と永続化の契約は [Standalone Jobs](standalone-jobs.md) に記録する。 - startup error は category のみを公開し、credential、prompt 本文、session metadata、内部 path を error text に含めない。 ## CLI / TUI routing diff --git a/docs/design/standalone-jobs.md b/docs/design/standalone-jobs.md new file mode 100644 index 000000000..abbe6ce3e --- /dev/null +++ b/docs/design/standalone-jobs.md @@ -0,0 +1,152 @@ +# Host-owned standalone Jobs (T-708) + +## Implementation design recorded before code changes + +Base: `3c424fd17d1a5e8bb5542de2d0ad88f55338e0ea` (current origin/develop; T-704 integrated). T-709's subjektiv consumer is not part of this change. + +The reusable `job` crate owns the transport-neutral immutable request (registry Profile selector, input snapshot/reference/revision, caller Job ID, instruction, serialization key and bounded limits), result envelope/digest and validation. Backend retains its existing SQLite state machine, Runtime binding, domain grants, authorization, unknown-outcome and cleanup implementation; its adapter uses the shared request validation and result contract, not a second runner. + +Standalone owns a separate adapter, not a WorkspaceClient impersonation. `StandaloneHost` owns a Job service and its connection lifecycle. The concrete local placement is `//jobs.sqlite3` (using the existing Worker directory layout; alongside, not inside, interactive sessions). The `standalone` crate owns the versioned local schema: durable intents, immutable attempts, execution state, accepted result/digest, cancellation and consumer acknowledgement. A single Host connection is protected by the existing Worker lease. SQLite transactions serialize reserve, dispatch, result acceptance and terminal transitions. No subjektiv tables or fake Workspace/Runtime registry are created. + +Local adapter states follow the existing intent/attempt/result distinctions. Exact immutable request replay returns the same Job, never starts another attempt. Explicit retry creates a bounded new attempt only for known failure; unknown outcomes require caller reconciliation and never auto-replay. Unstarted pending attempts remain pending on shutdown/reopen. Interrupted dispatch/running attempts without an accepted result become unknown; accepted results survive restart and remain available until consumer acknowledgement. Cancellation is durable and never auto-resumed. Consumer acknowledgement/delivery failure is separate from model success and never reruns the model. + +Execution uses the existing private Internal Worker substrate with a new Host-facing bounded Job entry point. It gets a distinct ephemeral session, explicitly resolved selected Profile/system prompt/model, no ambient parent tools, filesystem, Workspace or Feature connections, and only a Job/attempt/revision-bound structured result submission capability. Profiles requiring unavailable tools/features are explicitly rejected rather than silently stripped. Result authority is independent of Profile name; final prose, Idle and Worker termination are never success evidence. This first generic adapter grants no domain operations; future consumers must provide explicit domain capabilities rather than gaining them through a profile tag. + +The Host limits concurrent active resources independently of caller limits, enforces timeout/turn/output limits, routes cancellation into the Internal Worker lifecycle and waits for resource cleanup. Accepted results are committed before resource release; required domain postprocessing must finish before submission (or be independently durable in its consumer). No daemon, pool, subprocess, HTTP/WS listener or Server dependency is introduced. + +Validation will distinguish scripted in-process Internal Worker executions and SQLite reopen/recovery fixtures from any real-provider/process tests. It will cover alternative Profiles, immutable retry, invalid/stale results, resource limits, failure/cancellation/timeout, shutdown and restart. Backend focused regression tests must still prove its existing fences and cleanup. + +## API and ownership + +`job::{JobRequest, JobLimits, JobResultSubmission, JobOutcome}` are the common +Feature-facing data contract. `JobOutcome` distinguishes intent state from attempt +state and omits Workspace/Runtime identities. These enums are read vocabulary, +not another transition engine. Backend converts neutral requests with +`BackendJobRequest::from_job_request` and projects reservation/acceptance with +`.outcome()`; Backend domain grants, provenance, delivery and cleanup remain on +its own adapter and retain their existing durable/wire format and fingerprints. + +Standalone consumers receive `StandaloneHost::jobs()` explicitly, not a fake +Workspace client and not a tool on the interactive Worker. They can persist +`request`, read `get`/`pending`, explicitly `start` or `retry`, `cancel`, and +`acknowledge`. `.wait()` observes execution cleanup; `.outcome()` on the returned +snapshot is the common read view. `Completed` with its accepted structured result +is success authority; merely returning from `wait` is not. The injected-client +entry point resolves/validates the same selected Profile and model configuration; +it replaces only the model transport for embedders/tests. + +A typical generic consumer supplies its own stable Job ID/revision/reference, +immutable JSON input, instruction and registry Profile: + +```rust,ignore +let jobs = host.jobs(); +let snapshot = jobs.request(request)?; +if snapshot.state == standalone::jobs::JobState::Pending { + jobs.start(&snapshot.request.job_id).await?; +} +let outcome = jobs.wait(&snapshot.request.job_id).await?.outcome(); +// Validate/apply the consumer's domain result here. A failed notification or +// delivery leaves the successful result unacknowledged; do not retry the model. +if outcome.state == job::JobState::Completed { + deliver_idempotently(&outcome)?; + jobs.acknowledge(&outcome.job_id)?; +} +``` + +The Host retains tasks/cancellation even while a consumer waits; dropping a wait +future does not detach cancellation ownership or release a concurrency slot. +Active resources are bounded to eight by the Host and by the minimum of all live +requests' ceilings, including after result acceptance and before cleanup. +Serialization keys likewise fence live resources, not just pending result state. +Busy `start` returns a typed capacity error and leaves the reserved attempt +unchanged; callers decide when to start/resume pending work. There is no pool, +background daemon, implicit retry or independent Feature-owned Internal Worker. + +## Profile and capability policy + +The caller must select an existing built-in or user registry Profile. Path/raw +selectors and `inherit` are not accepted. Registry existence, selected instruction +and model configuration are checked before dispatch. The ordinary user settings +registry and Prompt catalog are used; repository-local recipes are not discovered +by the standalone Host, and default never silently maps to a different Profile. + +`builtin:job` is an **optional**, compatible result-only policy. It is not a +runner-selected default or authority token. User registry Profiles with different +instructions/models/bounds execute the same way, as demonstrated by the scripted +Worker tests. This adapter deliberately rejects active unsupported Feature, +filesystem/delegation scope, MCP, skills, compaction and durable event-trace +requirements. Existing `builtin:backend-job` has such requirements and is rejected +by this standalone adapter, while its Backend behavior is unchanged. A future +consumer requiring domain operations must arrange explicit Host capabilities; +choosing `builtin:subjektiv-memory-consolidation` alone grants nothing. + +Every run uses the existing private Internal Worker substrate, a distinct identity +and ephemeral Session. Only `SubmitJobResult` is installed by a FeatureModule. +Its model input is `{result: ...}`; Job/attempt/input-revision bindings come from +the Host capability, not model arguments. The sink checks those bindings, the +current persisted attempt, deadline, byte limit and result digest. Exact result +replay is idempotent; changed/foreign/stale results are rejected. Submission is +sealed before acceptance, so uncertain responses permit only identical retries. +An ambiguous sink failure is reconciled against SQLite; absent accepted evidence +it is `Unknown`, not a known retryable model failure. No domain logic is embedded +in this sink. Required consumer/domain postprocessing must complete before +submission or have its own durable ownership outside the ephemeral Worker. + +Selected instructions are retained. The Host execution contract is rendered from +`resources/prompts/internal/job_system.md` before the run's initial history is +created, not injected into a previous/transient turn. Turn/output policy is +attenuated to at most 32 turns and 8192 output tokens per model request, preserving +tighter selected Profile bounds. Structured results retain the shared 16 KiB +default / 64 KiB absolute byte limits, not a larger standalone exception. + +## Persistence and termination + +Local schema version **1** is owned only by `standalone::job_store`: `job_intents` +contains immutable serialized intent/fingerprint, serialization key, current +attempt and acknowledgement; `job_attempts` contains immutable identity/revision, +execution state, failure and accepted result/digest. Foreign keys, immutable +identity/result triggers, transactions, `WAL` and `synchronous=FULL` protect the +boundary. No Server schema or subjektiv/Memory schema is copied or opened. +The Host opens and recovers the store only under its existing top-level Worker +lease; clones cannot open or recover it themselves. + +- **Exact request replay:** same immutable intent returns its current snapshot. + Changing any intent field under the same ID is a conflict, not a new attempt. +- **Known failure:** only explicit `retry` creates a new immutable attempt, up to + the intent's bounded maximum. Prior attempts/results remain in SQLite. +- **Timeout/shutdown before acceptance:** fence `Unknown` before requesting Engine + cancellation; late result submissions cannot create success. Await normal + cleanup; retain the Host lease and report an error if confirmation is lost. +- **Normal Host shutdown:** seal scheduling/mutations, cancel active runs, await + resource cleanup, close SQLite, then stop the interactive controller and release + its lease. Reserved/unstarted requests remain resumable. Old cloned handles + return `Closed`, rather than mutating a store reopened by another Host. +- **Unexpected Host drop:** request cancellation and retain the lease because + synchronous drop cannot attest cleanup. No daemon continues after process exit. +- **Process loss/reopen:** reserved attempts stay pending; unaccepted dispatched + attempts become unknown; accepted results/acknowledgements remain intact. + SQLite result evidence, never final prose or an ephemeral Session, decides success. +- **Cancellation:** durable and never automatically resumed. Cancelling already + unknown intent does not erase its attempt's uncertain execution evidence or + release its serialization fence. `retry` still refuses it. There is no generic + operation that declares uncertain domain side effects unexecuted; callers must + reconcile domain evidence rather than bypassing the fence with an implicit replay. +- **After acceptance:** model error, cancellation or consumer delivery failure + cannot erase success or rerun the model. Acknowledgement is independently durable + and idempotent. Execution resources are reclaimed independently of result lifetime. + +Emergency cancellation has a bounded grace (one second for cancellation setup, +two seconds for normal cleanup). Expiry is **not** cleanup confirmation: it returns +an error, keeps unknown evidence/accepted results, and the Host retains its lease. +The result-only Feature owns no async/background tasks or domain connections; +the dropped engine stream/tool pumps request abort. Synchronous SQLite submission +is bounded by its five-second busy timeout and cannot be preempted mid-transaction. +No resource or side effect is claimed rolled back merely because a future dropped. + +## Validation evidence + +See [T-708 validation ledger](../report/2026-10-06-t708-validation.md). It separates +scripted in-process Worker/Host runs, direct SQLite/reopen/rollback fixtures and +an actual test-owned child-process loss/Host restore test from live-provider or +live dogfood validation. T-709's subjektiv consumer is intentionally not migrated +here; Backend T-704 Profile selection and T-690 cleanup remain regression-tested. diff --git a/docs/report/2026-10-06-t708-validation.md b/docs/report/2026-10-06-t708-validation.md new file mode 100644 index 000000000..8cae5eac1 --- /dev/null +++ b/docs/report/2026-10-06-t708-validation.md @@ -0,0 +1,134 @@ +# T-708 implementation and validation ledger + +Base: clean detached assigned checkout `3c424fd17d1a5e8bb5542de2d0ad88f55338e0ea`, +also the inspected `origin/develop`. T-704 was authoritative `done` with that +integrated source. Created `work/T-708-standalone-jobs` without overwriting an +existing branch or discarding changes. T-709's subjektiv consumer is separate. +The concrete SQLite placement/schema ownership was recorded in +[the design](../design/standalone-jobs.md) before implementation; it uses the +existing `/` directory layout, not a new `workers` subdirectory. + +Three scoped implementation children supplied the common contract/Backend +projection, Worker execution adapter, and local SQLite store. Parent inspected +committed sessions, exact source changes and concrete test results, then integrated +Host/service ownership, live-resource ceilings, deadline fencing, independent +acknowledgement, shared outcome projection, process-loss tests and documentation. +Independent published-MR review is separate from these implementation children. + +## Implemented boundaries + +- `job`: neutral immutable request/limits/result envelope, fingerprint, strict + registry selectors, bounded digests and read-only intent/attempt/outcome DTOs. + No HTTP, Workspace, Runtime identity, domain grants, runner or workflow DSL. +- Backend reuses validation/result helpers and neutral projections while retaining + its exact request wire/fingerprint/envelope, domain grants, persistent states, + Runtime authorization, unknown fences, resource serialization and cleanup. +- `worker::job`: selected registry Profile/instruction/model, explicit requirement + rejection, bounded policy, distinct ephemeral Internal Worker, result-only + FeatureModule and Host-fenced structured acceptance. No parent filesystem, + Workspace, Feature or domain connections; no Profile-name authorization. + Ambiguous sink acceptance is Unknown unless the Host's durable result proves success. +- Standalone Host owns schema-1 SQLite intent/attempt/result/ack state and a bounded + execution service. It opens and recovers under its Worker lease; shutdown cancels + and awaits executions, closes SQLite, then releases the lease after controller + confirmation. Old cloned handles are closed. Drop cannot attest cleanup and + retains the lease. Busy/unknown/cancelled/terminal work is not blindly replayed. +- Exact immutable request resend differs from explicit bounded new-attempt retry. + Results commit before resource release, survive model/notification failure, and + are independently acknowledged by the consumer. Waiting does not relinquish + Host ownership of cancellation/concurrency. Cancelling unknown intent retains + its uncertain attempt evidence and serialization fence. +- Optional `builtin:job` is a caller-selected compatible policy, not a fixed runner + default. Different user registry Profiles are demonstrated in Worker fixtures. + Existing `builtin:backend-job` retains Backend behavior; standalone rejects its + unavailable requirements rather than silently stripping or replacing them. +- No production Server/Runtime process, HTTP/WS listener, Worker registry, + subprocess Worker, daemon/pool or subjektiv-specific runner is added. + +## Validation results + +Development began with narrow contract/module targets before full changed-crate +validation. Final root check covers the normal default-members TUI/Server closure. + +| Command | Concrete result | +| --- | --- | +| `cargo test -p job` | 9 contract tests passed; no unit/doc tests | +| `cargo test -p standalone --lib job_store` | 19 fixtures passed: transactional replay/conflict, all limits, result fences, retry/old-attempt preservation, cancellation, concurrency races, SQL rollback, schema rejection and reopen recovery | +| `cargo test -p standalone --lib jobs::tests` | 2 passed: Host capability binding/deadline/replay and success/cancellation/closed-handle lifetime | +| `cargo test -p standalone --test jobs process_loss_recovers_durable_jobs_without_replaying_unknown` | Actual test-owned process exit and Host stale-lease/SQLite recovery passed | +| Final `cargo test -p standalone` | 25 library + 10 existing Host integration + 8 Job integration passed; 1 child-process fixture entry point marked ignored in parent harness, explicitly invoked with `--ignored` by the passing process-loss test | +| `cargo test -p manifest` | All 185 tests passed, including new optional builtin Profile catalogue resolution; no resolver/runtime/schema implementation changed | +| `cargo test -p worker` | All 826 library + 110 integration tests passed, including 11 new Job tests and 8 existing Internal Worker substrate tests | +| `cargo test -p yoi-workspace-server --lib backend_job` | All 38 passed: neutral adapter/wire/fingerprint, T-704 Profiles/no fallback, structured fences, unknown/recovery, resource serialization and T-690 cleanup | +| `cargo test -p yoi-workspace-server --bins` | All 11 binary tests passed | +| `cargo test -p worker-runtime --lib backend_job` | All 3 Job binding/dispatch regressions passed | +| `cargo test -p server-api --features typescript` | All 77 passed, including checked-in generated/OpenAPI/TypeScript freshness; existing ts-rs serde-attribute warnings | +| Root `cargo check` | Passed after integration and again after final local adapter/tests changes | +| `cargo fmt --all -- --check` / `git diff --check HEAD` | Passed | +| `cargo test -p yoi-workspace-server` | **Failed: 643 passed, 1 failed** in library; see retained anomaly below. Binary tests were therefore run separately. This is not a green full Server suite. | +| `cargo test -p yoi-workspace-server -- --test-threads=4` | **Failed: 643 passed, 1 failed**, same unchanged checker assertion; not claimed passed | + +Profile fixtures use two different existing user registry entries with distinct +instructions and budgets, plus optional `builtin:job`. They prove only +`SubmitJobResult` is exposed, selected instructions/policy are retained, scopes and +Backend/domain tools are absent, malformed/oversized/forged results are rejected, +uncertain sink submission permits only identical replay, final prose is not +success, provider failure/cancellation/turn limits do not manufacture success, +and accepted results survive later provider failure. + +Host Job integration additionally proves explicit known-model-failure retry, +request replay, live concurrency while a consumer waits, cancellation, timeout, +shutdown, pending resume, consumer acknowledgement and unsupported Profile +rejection without model calls. Direct-store fixtures prove persisted facts and +rollback/reopen behavior, not live model or process behavior. + +### Actual process verification versus fixtures + +`process_loss_recovers_durable_jobs_without_replaying_unknown` starts **only a +test-owned copy of the integration test executable**, using scripted model clients. +The child owns a real StandaloneHost/SQLite connection/Worker lease, persists a +reserved request, a cancelled request and an accepted result, then exits with code +23 while a separate Internal Worker is executing. It deliberately skips destructors +and graceful shutdown. The parent verifies that exit and restores the Host through +the real stale-process lease path: accepted result retained/unacknowledged, +cancelled work never resumed, reserved work explicitly resumable and interrupted +work Unknown/no automatic retry. The helper's ignored annotation is an entry-point +selector, not skipped semantic coverage. + +These are real Rust test/compiler processes plus scripted in-process model +executions and SQLite fixtures. They are **not** live-provider LLM calls, a live +standalone CLI/TUI session, a Backend/Runtime deployment or dogfood environment +update. No operating Yoi Worker or live dogfood process was stopped or updated. +Production Jobs do not create the subprocess used solely by that crash test. + +## Retained failures and evidence limits + +Initial integrated compilation exposed `get` snapshot/Option disagreement between +concurrently authored store/service; signatures were aligned and the subsequent +store/Host/full standalone runs passed. A private tool-type import during child +integration was corrected before focused/full Worker validation. No passing +result is attributed to those initial failing builds. + +Both full Server runs and one isolated run of +`server::tests::ticket_checker_suppresses_stale_and_clean_results_and_diagnoses_invalid_output` +failed the unchanged post-stale-result `assert!(execution.take_inputs().is_empty())` +at `server.rs:44748`. All 38 Job-filter tests pass, including unchanged Backend +envelope/fingerprint compatibility and late-result/cleanup fences. The same +checker assertion/race was independently reproduced on the earlier original base +in the [T-702 ledger](2026-10-06-t702-stale-checker-validation.md), and explicitly +retained as unfixed in the [T-704 ledger](2026-10-06-t704-validation.md). +No new isolated build of this Ticket's exact base was run, and its root cause is +not claimed proven or fixed here. The checker implementation/test was neither +changed nor skipped to manufacture a green suite. This validation anomaly remains +visible for Reviewer/Orchestrator attention. + +No whole-workspace tests, E2E, Nix/Docker builds or live-provider validation were +claimed. Backend generated contracts were regression checked despite no wire +change. Result-only Jobs reject unsupported Feature/authority requirements; +subjektiv consumer capabilities/postprocessing are T-709 work, not inferred grants. + +Publication must use only the clean committed Ticket work branch by normal +non-force push, verified with exact provider source ref. No target/tags/unrelated +refs, replacement MR, integration branch, merge or Ticket completion is performed +by this Coder. Final immutable source/MR/review identities belong in the MR thread +and approved Ticket handoff rather than being guessed in this ledger. diff --git a/resources/profiles/job.dcdl b/resources/profiles/job.dcdl new file mode 100644 index 000000000..87683ac27 --- /dev/null +++ b/resources/profiles/job.dcdl @@ -0,0 +1,27 @@ +slug = "job"; +description = "Result-only Job policy; the Host grants attempt/result authority independently."; +model = { ref = "codex-oauth/gpt-5.6-luna"; }; +engine = { + instruction = "internal.job_system"; + reasoning = "medium"; + max_tokens = 8192; + max_turns = 32; +}; +session = { record_event_trace = false; }; +feature = { + task = { enabled = false; }; + memory = { enabled = false; staging_tools = false; }; + subjektiv = { enabled = false; consolidation_tools = false; extraction = { enabled = false; }; }; + web = { enabled = false; }; + image = { enabled = false; }; + sub_worker = { enabled = false; }; + flow = { enabled = false; }; + worker = { enabled = false; direct_spawn = false; }; + workspace_worker_discovery = { enabled = false; }; + objective = { enabled = false; }; + manage_workdir = { enabled = false; }; + workspace_config = { enabled = false; }; + ticket = { enabled = false; authoring = false; thread = false; intake = false; workflow = false; }; + merge_request = { show = false; open = false; review = false; readiness_check = false; complete = false; }; + orchestration = { enabled = false; }; +}; diff --git a/resources/prompts/catalog.dcdl b/resources/prompts/catalog.dcdl index e1039ba32..991e702e9 100644 --- a/resources/prompts/catalog.dcdl +++ b/resources/prompts/catalog.dcdl @@ -21,6 +21,7 @@ roleReviewer = import "./role/reviewer.md"; internalCompactSystem = import "./internal/compact_system.md"; internalFlowVerifierSystem = import "./internal/flow_verifier_system.md"; internalBackendJobSystem = import "./internal/backend_job_system.md"; +internalJobSystem = import "./internal/job_system.md"; internalTicketItemChecker = import "./internal/ticket_item_checker.md"; internalMemoryConsolidationSystem = import "./internal/memory_consolidation_system.md"; internalSubjektivMemoryConsolidationSystem = import "./internal/subjektiv_memory_consolidation_system.md"; @@ -62,6 +63,7 @@ in compact_system = internalCompactSystem.content; flow_verifier_system = internalFlowVerifierSystem.content; backend_job_system = internalBackendJobSystem.content; + job_system = internalJobSystem.content; ticket_item_checker = internalTicketItemChecker.content; memory_consolidation_system = internalMemoryConsolidationSystem.content; subjektiv_memory_consolidation_system = internalSubjektivMemoryConsolidationSystem.content; diff --git a/resources/prompts/internal/job_system.md b/resources/prompts/internal/job_system.md new file mode 100644 index 000000000..2b254ef7c --- /dev/null +++ b/resources/prompts/internal/job_system.md @@ -0,0 +1,7 @@ +You execute one bounded Host-owned Job against its immutable input envelope. + +The caller selects the Profile. The Host, not the Profile name or task prose, binds Job/attempt/input identity and grants tools. Use only the capabilities provided for this execution. Do not infer Workspace, filesystem, parent Worker, or domain permissions from the input. + +Submit a structured result through the Host-provided Job result tool. Complete any required postprocessing before submitting, unless its durable ownership is explicitly established independently of this Worker. Final prose, Idle, and Worker termination are not success authority. Do not claim a submission succeeded if the tool rejects it. + +On a definitive validation rejection, correct the result. If acceptance is uncertain, retry only the exact submitted result; never change a result already accepted. Never ask to run another attempt or access another Job's capabilities. From 58462d584462f780080f8f8e72b0a8b9a4e3c4be Mon Sep 17 00:00:00 2001 From: Hare Date: Wed, 7 Oct 2026 00:14:51 +0900 Subject: [PATCH 2/9] feat: register Workspace Profiles as Decodal values (T-710) Project the evaluated Workspace Profile value into a revision-bound JSON archive without source-path registration or Decodal re-evaluation. Preserve partial-value omissions and explicit overrides, validate before save, and reject legacy registrations with a documented explicit migration. Fix array-element schema completion and provide fingerprint-bound authoring shapes independently of materialization defaults. Carry metadata through REST, regenerate TypeScript/OpenAPI/WASM artifacts, and exercise save/projection/Runtime consumption and persisted Manifest restore boundaries. Validation: root cargo check; cargo test -p config-source; cargo test -p worker-runtime; cargo test -p server-api --features typescript; focused profile_settings/value_profiles/archive tests; 39 web config-source/profile API tests; deno task check; cargo fmt --all -- --check; git diff --check HEAD. Full workspace-server test: 641 passed, one pre-existing ticket_checker_suppresses_stale_and_clean_results_and_diagnoses_invalid_output assertion failure. The same filter fails at base 3c424fd17d1a5e8bb5542de2d0ad88f55338e0ea in a separately archived source tree and isolated target directory. Remaining server crate tests pass with only that filter skipped (641 lib + 11 bin); unrelated checker code is unchanged. --- crates/config-source/src/lib.rs | 688 +++++++++++++++++- crates/server-api/src/lib.rs | 4 + crates/worker-runtime/src/profile_archive.rs | 274 ++++++- crates/workspace-server/src/config_source.rs | 6 +- .../workspace-server/src/profile_settings.rs | 393 ++++------ crates/workspace-server/src/server.rs | 2 + .../src/server/tests/subjektiv_jobs_tests.rs | 2 +- .../src/server/tests/value_profiles_tests.rs | 362 +++++++++ docs/design/profiles-manifests-prompts.md | 30 +- openapi/server-api.json | 9 +- .../config-schema/profile-authoring.dcdl | 72 ++ resources/config-schema/profile.dcdl | 11 + web/workspace/deno.json | 2 +- .../src/lib/generated/companion-api.ts | 2 +- .../src/lib/generated/legacy-server-api.ts | 4 + .../src/lib/generated/repository-api.ts | 2 +- .../src/lib/generated/runtime-api.ts | 2 +- web/workspace/src/lib/generated/ticket-api.ts | 2 +- .../src/lib/generated/worker-launch-api.ts | 2 +- .../src/lib/workspace/config-source/api.ts | 8 +- .../generated/config_source_wasm_bg.wasm | Bin 1265992 -> 1283030 bytes .../types/ConfigSchemaContribution.ts | 2 +- web/workspace/test/config-source/api.test.ts | 38 + .../test/config-source/wasm-parity.test.ts | 110 +++ 24 files changed, 1708 insertions(+), 319 deletions(-) create mode 100644 crates/workspace-server/src/server/tests/value_profiles_tests.rs create mode 100644 resources/config-schema/profile-authoring.dcdl create mode 100644 resources/config-schema/profile.dcdl diff --git a/crates/config-source/src/lib.rs b/crates/config-source/src/lib.rs index a5df85dce..2d6604d82 100644 --- a/crates/config-source/src/lib.rs +++ b/crates/config-source/src/lib.rs @@ -487,6 +487,9 @@ pub struct ConfigSchemaContribution { pub namespace: String, pub version: String, pub source: String, + // Optional editor-only schema; never used to validate or materialize config. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub authoring_source: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub projection_validator: Option, pub source_digest: String, @@ -529,16 +532,31 @@ impl ConfigSchemaContribution { version, source_digest: digest_bytes(source.as_bytes()), source, + authoring_source: None, projection_validator: None, }) } + pub fn with_authoring_source(mut self, source: impl Into) -> Self { + self.authoring_source = Some(source.into()); + self + } + pub fn with_projection_validator(mut self, validator: ConfigProjectionValidator) -> Self { self.projection_validator = Some(validator); self } fn validate(&self) -> Result<(), ConfigTreeError> { + if self + .authoring_source + .as_ref() + .is_some_and(|source| source.trim().is_empty()) + { + return Err(ConfigTreeError::InvalidSchemaContribution( + "authoring schema source must not be empty".to_string(), + )); + } let expected = digest_bytes(self.source.as_bytes()); if self.source_digest != expected { return Err(ConfigTreeError::InvalidSchemaContribution(format!( @@ -591,27 +609,41 @@ impl WorkspaceConfigSchemaBundle { .collect::>() .join(" & ") }; - let fingerprint = digest_bytes( - serde_json::to_vec(&( - CONFIG_SOURCE_CONTRACT_VERSION, - DECODAL_VERSION, - contributions - .iter() - .map(|contribution| { - ( - contribution.provider_id.as_str(), - contribution.namespace.as_str(), - contribution.version.as_str(), - contribution.source_digest.as_str(), - contribution.projection_validator.as_ref(), - ) - }) - .collect::>(), - digest_bytes(source.as_bytes()), - )) - .expect("schema bundle fingerprint input serializes") - .as_slice(), - ); + let mut fingerprint_input = serde_json::to_vec(&( + CONFIG_SOURCE_CONTRACT_VERSION, + DECODAL_VERSION, + contributions + .iter() + .map(|contribution| { + ( + contribution.provider_id.as_str(), + contribution.namespace.as_str(), + contribution.version.as_str(), + contribution.source_digest.as_str(), + contribution.projection_validator.as_ref(), + ) + }) + .collect::>(), + digest_bytes(source.as_bytes()), + )) + .expect("schema bundle fingerprint input serializes"); + let authoring_sources = contributions + .iter() + .filter_map(|contribution| { + contribution + .authoring_source + .as_deref() + .map(|source| (contribution.provider_id.as_str(), source)) + }) + .collect::>(); + // Preserve legacy fingerprints when no editor-only metadata is present. + if !authoring_sources.is_empty() { + fingerprint_input.extend( + serde_json::to_vec(&("authoring_sources", authoring_sources)) + .expect("authoring schema fingerprint input serializes"), + ); + } + let fingerprint = digest_bytes(&fingerprint_input); Ok(Self { contributions, source, @@ -619,6 +651,25 @@ impl WorkspaceConfigSchemaBundle { }) } + fn authoring_source(&self) -> String { + if self.contributions.is_empty() { + return self.source.clone(); + } + self.contributions + .iter() + .map(|contribution| { + format!( + "({})", + contribution + .authoring_source + .as_deref() + .unwrap_or(&contribution.source) + ) + }) + .collect::>() + .join(" & ") + } + pub fn empty() -> Self { Self::compose(Vec::new()).expect("empty schema bundle is valid") } @@ -751,26 +802,32 @@ pub struct EvaluationResult { pub projection_digest: String, } +#[derive(Debug, Clone, PartialEq, Eq)] +enum ConfigCompletionPathSegment { + Field(String), + Index(usize), +} + #[derive(Debug, Clone, PartialEq, Eq)] struct ConfigFieldCompletionContext { - schema_path: Vec, + schema_path: Vec, from: usize, } #[derive(Debug)] enum ConfigCompletionContainer { Object { - schema_path: Vec, + schema_path: Vec, pending_path: Vec, last_identifier_from: Option, trailing_dot: bool, reading_value: bool, }, Array { - schema_path: Vec, + schema_path: Vec, }, Other { - schema_path: Vec, + schema_path: Vec, }, } @@ -855,9 +912,18 @@ fn config_field_completion_context( }); } SyntaxTokenKind::LBracket => { - let schema_path = pending_container_path(&containers); + let mut schema_path = pending_container_path(&containers); + schema_path.push(ConfigCompletionPathSegment::Index(0)); containers.push(ConfigCompletionContainer::Array { schema_path }); } + SyntaxTokenKind::Comma => { + if let Some(ConfigCompletionContainer::Array { schema_path }) = + containers.last_mut() + && let Some(ConfigCompletionPathSegment::Index(index)) = schema_path.last_mut() + { + *index += 1; + } + } SyntaxTokenKind::RBracket => { pop_container(&mut containers, |container| { matches!(container, ConfigCompletionContainer::Array { .. }) @@ -943,7 +1009,12 @@ fn config_field_completion_context( return None; }; let mut schema_path = schema_path.clone(); - schema_path.extend(pending_path.iter().cloned()); + schema_path.extend( + pending_path + .iter() + .cloned() + .map(ConfigCompletionPathSegment::Field), + ); Some(ConfigFieldCompletionContext { schema_path, from: if *trailing_dot { @@ -954,14 +1025,25 @@ fn config_field_completion_context( }) } -fn pending_container_path(containers: &[ConfigCompletionContainer]) -> Vec { +fn pending_container_path( + containers: &[ConfigCompletionContainer], +) -> Vec { match containers.last() { Some(ConfigCompletionContainer::Object { schema_path, pending_path, reading_value: true, .. - }) => schema_path.iter().chain(pending_path).cloned().collect(), + }) => schema_path + .iter() + .cloned() + .chain( + pending_path + .iter() + .cloned() + .map(ConfigCompletionPathSegment::Field), + ) + .collect(), Some(ConfigCompletionContainer::Array { schema_path }) | Some(ConfigCompletionContainer::Other { schema_path }) => schema_path.clone(), _ => Vec::new(), @@ -1201,12 +1283,37 @@ impl SnapshotEnvironment { if self.schema_bundle.is_some() && let Some(context) = config_field_completion_context(source, utf8_byte_offset) { - let mut member_source = format!("{WORKSPACE_CONFIG_SCHEMA_GLOBAL}."); - member_source.push_str(&context.schema_path.join(".")); - if !context.schema_path.is_empty() && context.from == utf8_byte_offset { - member_source.push('.'); + let (schema_path, prefix) = if context.from == utf8_byte_offset { + (context.schema_path.as_slice(), "") + } else { + let Some((ConfigCompletionPathSegment::Field(prefix), schema_path)) = + context.schema_path.split_last() + else { + return self.complete(entrypoint, source, utf8_byte_offset, explicit); + }; + (schema_path, prefix.as_str()) + }; + let has_array = schema_path + .iter() + .any(|segment| matches!(segment, ConfigCompletionPathSegment::Index(_))); + let mut member_source = WORKSPACE_CONFIG_SCHEMA_GLOBAL.to_owned(); + // Ordinary object paths retain the language service's member traversal, + // but both branches install the composed authoring schema. + if !has_array { + for segment in schema_path { + if let ConfigCompletionPathSegment::Field(field) = segment { + member_source.push('.'); + member_source.push_str(field); + } + } } - let mut completion = LanguageService::new(self).complete( + member_source.push('.'); + member_source.push_str(prefix); + let completion_environment = ConfigFieldCompletionEnvironment { + environment: self, + schema_path: if has_array { schema_path } else { &[] }, + }; + let mut completion = LanguageService::new(completion_environment).complete( entrypoint.as_str(), &member_source, member_source.len(), @@ -1270,6 +1377,91 @@ impl HostEnvironment for &SnapshotEnvironment { } } +// Decodal has no indexed member syntax. Resolve array ranges only in this +// completion environment, then reuse the language service's member behavior. +struct ConfigFieldCompletionEnvironment<'a> { + environment: &'a SnapshotEnvironment, + schema_path: &'a [ConfigCompletionPathSegment], +} + +impl HostEnvironment for ConfigFieldCompletionEnvironment<'_> { + type Loader = SnapshotImportLoader; + + fn create_loader(&self) -> Self::Loader { + self.environment.create_loader() + } + + fn configure_engine(&self, engine: &mut Engine) -> decodal::Result<()> { + use decodal::runtime::{ConcreteValue, ObjectField, ObjectValue, RuntimeValue}; + + let Some(bundle) = &self.environment.schema_bundle else { + return Ok(()); + }; + let module = engine.add_root_source( + WORKSPACE_CONFIG_SCHEMA_SOURCE, + WORKSPACE_CONFIG_SCHEMA_SOURCE, + &bundle.authoring_source(), + )?; + let mut schema = Some(engine.eval_module(module)?); + for segment in self.schema_path { + let Some(value) = schema else { + break; + }; + schema = match segment { + ConfigCompletionPathSegment::Field(field) => engine + .value_fields(&value)? + .unwrap_or_default() + .into_iter() + .find(|(name, _)| name == field) + .map(|(_, value)| value), + ConfigCompletionPathSegment::Index(index) => { + let element = match value { + RuntimeValue::Concrete(ConcreteValue::Array(elements)) => { + elements.get(*index).copied() + } + RuntimeValue::Abstract(range) => { + range.constraints.iter().find_map(|entry| { + if let decodal::Constraint::ArrayItems(element) = entry.constraint { + Some(element) + } else { + None + } + }) + } + _ => None, + }; + if let Some(element) = element { + // value_fields is the public API for forcing lazy fields. + // Wrap the element thunk without materializing its schema. + let object = RuntimeValue::Concrete(ConcreteValue::Object(ObjectValue { + fields: vec![ObjectField { + name: "element".into(), + value: element, + span: Span::default(), + }], + rest: None, + })); + engine + .value_fields(&object)? + .and_then(|fields| fields.into_iter().next().map(|(_, value)| value)) + } else { + None + } + } + }; + } + if let Some(schema) = schema { + engine.bind_global_runtime(WORKSPACE_CONFIG_SCHEMA_GLOBAL, schema); + } else { + engine.bind_global( + WORKSPACE_CONFIG_SCHEMA_GLOBAL, + Value::object([] as [(&str, Value); 0]), + )?; + } + Ok(()) + } +} + #[derive(Debug, Clone)] pub struct SnapshotImportLoader { snapshot: ConfigTreeSnapshot, @@ -1890,7 +2082,7 @@ mod tests { assert_eq!( config_field_completion_context(root, root_cursor), Some(ConfigFieldCompletionContext { - schema_path: vec!["pro".into()], + schema_path: vec![ConfigCompletionPathSegment::Field("pro".into())], from: root_cursor - 3, }) ); @@ -1899,7 +2091,10 @@ mod tests { assert_eq!( config_field_completion_context(nested, nested_cursor), Some(ConfigFieldCompletionContext { - schema_path: vec!["profile".into(), "def".into()], + schema_path: vec![ + ConfigCompletionPathSegment::Field("profile".into()), + ConfigCompletionPathSegment::Field("def".into()), + ], from: nested_cursor - 3, }) ); @@ -1908,7 +2103,12 @@ mod tests { assert_eq!( config_field_completion_context(array, array_cursor), Some(ConfigFieldCompletionContext { - schema_path: vec!["profile".into(), "entries".into(), "sel".into()], + schema_path: vec![ + ConfigCompletionPathSegment::Field("profile".into()), + ConfigCompletionPathSegment::Field("entries".into()), + ConfigCompletionPathSegment::Index(0), + ConfigCompletionPathSegment::Field("sel".into()), + ], from: array_cursor - 3, }) ); @@ -1991,6 +2191,420 @@ mod tests { ); } + fn profile_entries_completion_schema() -> WorkspaceConfigSchemaBundle { + WorkspaceConfigSchemaBundle::compose([ConfigSchemaContribution::new( + "builtin:profile", + "profile", + "1", + "{ profile = { default_profile = String; entries = [...{ selector = String; profile = { slug = String; worker = { mode = String; }; }; }]; }; }", + ) + .unwrap()]) + .unwrap() + } + + #[test] + fn completion_projects_array_element_and_nested_profile_fields() { + let snapshot = ConfigTreeSnapshot::from_entries(1, [entry("main.dcdl", "{}")]).unwrap(); + let environment = SnapshotEnvironment::new(snapshot) + .with_schema_bundle(profile_entries_completion_schema()); + for (marked_source, expected, prefix_len) in [ + ( + "{ profile.entries = [{ sel| }] } as WorkspaceConfigSchema", + "selector", + 3, + ), + ( + "{ profile = { entries = [{ pro| }] } } as WorkspaceConfigSchema", + "profile", + 3, + ), + ( + "{ profile.entries = [{ | }] } as WorkspaceConfigSchema", + "selector", + 0, + ), + ( + "{ profile.entries = [{ | }] } as WorkspaceConfigSchema", + "profile", + 0, + ), + ( + "{ profile.entries = [{ profile = { slu| } }] } as WorkspaceConfigSchema", + "slug", + 3, + ), + ( + "{ profile.entries = [{ profile = { | } }] } as WorkspaceConfigSchema", + "worker", + 0, + ), + ( + "{ profile.entries = [{ profile.worker.mo| }] } as WorkspaceConfigSchema", + "mode", + 2, + ), + ( + "{ profile.entries = [{ profile.worker.| }] } as WorkspaceConfigSchema", + "mode", + 0, + ), + ( + "{ profile.entries = [{ profile = { worker = { mo| } } }] } as WorkspaceConfigSchema", + "mode", + 2, + ), + ( + "{ profile.entries = [{}, { sel| }] } as WorkspaceConfigSchema", + "selector", + 3, + ), + ( + "{ profile.entries = [{ selector = \"日本語\"; profile = { slu|g } }] } as WorkspaceConfigSchema", + "slug", + 3, + ), + ( + "{ profile.entries = [{ pro|file }] } as WorkspaceConfigSchema", + "profile", + 3, + ), + ( + "{ profile.entries = [({ profile = { worker = { mo| } } })] } as WorkspaceConfigSchema", + "mode", + 2, + ), + ] { + let cursor = marked_source.find('|').unwrap(); + let source = marked_source.replace('|', ""); + let completion = environment + .complete_config(&path("main.dcdl"), &source, cursor, true) + .unwrap() + .unwrap_or_else(|| panic!("missing completion for {marked_source}")); + assert_eq!(completion.from, cursor - prefix_len, "{marked_source}"); + assert!( + completion.items.iter().any(|item| item.label == expected), + "{marked_source}: {completion:?}" + ); + } + } + + #[test] + fn completion_tracks_fixed_array_positions_and_nested_array_ranges() { + let snapshot = ConfigTreeSnapshot::from_entries(1, [entry("main.dcdl", "{}")]).unwrap(); + let schema = WorkspaceConfigSchemaBundle::compose([ConfigSchemaContribution::new( + "builtin:profile", + "profile", + "1", + "{ profile = { entries = [{ selector = String; }, { profile = { slug = String; worker = { mode = String; }; }; }]; matrix = [...[...{ selector = String; }]]; }; }", + ) + .unwrap()]) + .unwrap(); + let environment = SnapshotEnvironment::new(snapshot).with_schema_bundle(schema); + for (marked_source, expected, unexpected) in [ + ( + "{ profile.entries = [{ | }] } as WorkspaceConfigSchema", + "selector", + "profile", + ), + ( + "{ profile.entries = [{}, { | }] } as WorkspaceConfigSchema", + "profile", + "selector", + ), + ( + "{ profile.entries = [{}, { profile.worker.mo| }] } as WorkspaceConfigSchema", + "mode", + "selector", + ), + ( + "{ profile.matrix = [[{}, { sel| }]] } as WorkspaceConfigSchema", + "selector", + "profile", + ), + ( + "{ profile.matrix = [[], [{ sel| }]] } as WorkspaceConfigSchema", + "selector", + "profile", + ), + ] { + let cursor = marked_source.find('|').unwrap(); + let source = marked_source.replace('|', ""); + let completion = environment + .complete_config(&path("main.dcdl"), &source, cursor, true) + .unwrap() + .unwrap_or_else(|| panic!("missing completion for {marked_source}")); + assert!( + completion.items.iter().any(|item| item.label == expected), + "{marked_source}: {completion:?}" + ); + assert!( + !completion.items.iter().any(|item| item.label == unexpected), + "{marked_source}: {completion:?}" + ); + } + for marked_source in [ + "{ profile.entries = [{}, {}, { | }] } as WorkspaceConfigSchema", + "{ profile.entries = [{ missing = { | } }] } as WorkspaceConfigSchema", + ] { + let cursor = marked_source.find('|').unwrap(); + let source = marked_source.replace('|', ""); + let completion = environment + .complete_config(&path("main.dcdl"), &source, cursor, true) + .unwrap(); + assert!( + completion.is_none_or(|completion| !completion + .items + .iter() + .any(|item| matches!(item.label.as_str(), "selector" | "profile"))), + "{marked_source}" + ); + } + } + + #[test] + fn completion_preserves_general_schema_member_behavior() { + let snapshot = ConfigTreeSnapshot::from_entries(1, [entry("main.dcdl", "{}")]).unwrap(); + let environment = SnapshotEnvironment::new(snapshot) + .with_schema_bundle(profile_entries_completion_schema()); + for source in [ + "WorkspaceConfigSchema.pro", + "WorkspaceConfigSchema.profile.", + "WorkspaceConfigSchema.profile.entries.", + "WorkspaceConfigSchema.profile.default_pro", + "let schema = WorkspaceConfigSchema; in schema.profile.def", + ] { + assert_eq!( + environment + .complete_config(&path("main.dcdl"), source, source.len(), true) + .unwrap(), + environment + .complete(&path("main.dcdl"), source, source.len(), true) + .unwrap(), + "{source}" + ); + } + } + + #[test] + fn completion_array_schema_retains_nested_profile_diagnostics() { + let valid = "{ profile = { default_profile = \"default\"; entries = [{ selector = \"default\"; profile = { slug = \"default\"; worker = { mode = \"interactive\"; }; }; }]; }; } as WorkspaceConfigSchema"; + let snapshot = ConfigTreeSnapshot::from_entries(7, [entry("main.dcdl", valid)]).unwrap(); + let environment = SnapshotEnvironment::new(snapshot) + .with_schema_bundle(profile_entries_completion_schema()); + assert!(environment.analyze(&path("main.dcdl"), None).is_empty()); + for (source, field) in [ + ( + valid.replace("selector = \"default\"", "selector = 42"), + "selector", + ), + (valid.replace("slug = \"default\"", "slug = 42"), "slug"), + (valid.replace("mode = \"interactive\"", "mode = 42"), "mode"), + ( + valid.replace("mode = \"interactive\"", "typo = \"interactive\""), + "typo", + ), + ] { + let diagnostics = environment.analyze(&path("main.dcdl"), Some(&source)); + assert!( + diagnostics.iter().any(|diagnostic| { + diagnostic.kind == "constraint_violation" + && diagnostic.path == path("main.dcdl") + && diagnostic.revision == 7 + && diagnostic.span.end_byte > diagnostic.span.start_byte + && (diagnostic.message.contains(field) + || diagnostic + .labels + .iter() + .any(|label| label.message.contains(field))) + }), + "{field}: {diagnostics:#?}" + ); + } + } + + #[test] + fn completion_authoring_schema_preserves_omitted_profile_fields_in_evaluation() { + let schema = WorkspaceConfigSchemaBundle::compose([ConfigSchemaContribution::new( + "builtin:profile", + "profile", + "1", + "{ profile = { defaults = {...Unknown}; entries = [...{ selector = String; profile = {...Unknown}; }]; }; }", + ) + .unwrap() + .with_authoring_source( + "{ profile = { defaults = { slug = String; worker = { mode = String; }; }; entries = [...{ selector = String; profile = { slug = String; worker = { mode = String; }; }; }]; }; }", + )]) + .unwrap(); + let source = "{ profile = { defaults = {}; entries = [{ selector = \"default\"; profile = {}; }]; }; } as WorkspaceConfigSchema"; + let snapshot = ConfigTreeSnapshot::from_entries(1, [entry("main.dcdl", source)]).unwrap(); + let environment = SnapshotEnvironment::new(snapshot).with_schema_bundle(schema.clone()); + assert!(environment.analyze(&path("main.dcdl"), None).is_empty()); + let contract = ToolchainContract::with_schema_bundle(1, vec![path("main.dcdl")], 1, schema); + let evaluated = environment.evaluate_contract(&contract).unwrap(); + assert_eq!( + evaluated.projections[0].data_json, + serde_json::json!({"profile": {"defaults": {}, "entries": [{"selector": "default", "profile": {}}]}}) + ); + // Both ordinary object paths and array-element paths use editor metadata. + for (marked_source, expected) in [ + ( + "{ profile.defaults = { slu| } } as WorkspaceConfigSchema", + "slug", + ), + ( + "{ profile.defaults.worker = { mo| } } as WorkspaceConfigSchema", + "mode", + ), + ( + "{ profile.entries = [{ pro| }] } as WorkspaceConfigSchema", + "profile", + ), + ( + "{ profile.entries = [{ profile = { slu| } }] } as WorkspaceConfigSchema", + "slug", + ), + ( + "{ profile.entries = [{ profile.worker = { mo| } }] } as WorkspaceConfigSchema", + "mode", + ), + ( + "{ profile.entries = [{ profile = { | } }] } as WorkspaceConfigSchema", + "worker", + ), + ] { + let cursor = marked_source.find('|').unwrap(); + let source = marked_source.replace('|', ""); + let completion = environment + .complete_config(&path("main.dcdl"), &source, cursor, true) + .unwrap() + .unwrap_or_else(|| panic!("missing completion for {marked_source}")); + assert!( + completion.items.iter().any(|item| item.label == expected), + "{marked_source}: {completion:?}" + ); + } + // Completion does not install its schema into later analysis/evaluation. + assert!(environment.analyze(&path("main.dcdl"), None).is_empty()); + assert_eq!(environment.evaluate_contract(&contract).unwrap(), evaluated); + } + + #[test] + fn completion_authoring_sources_compose_with_validation_source_fallbacks() { + let schema = WorkspaceConfigSchemaBundle::compose([ + ConfigSchemaContribution::new( + "builtin:editor", + "editor", + "1", + "{ editor = {...Unknown}; }", + ) + .unwrap() + .with_authoring_source( + "{ editor = { theme = String; settings = { enabled = Bool; }; }; }", + ), + ConfigSchemaContribution::new( + "builtin:web", + "web", + "1", + "{ web = { enabled = Bool; }; }", + ) + .unwrap(), + ]) + .unwrap(); + let snapshot = ConfigTreeSnapshot::from_entries(1, [entry("main.dcdl", "{}")]).unwrap(); + let environment = SnapshotEnvironment::new(snapshot).with_schema_bundle(schema); + for (marked_source, expected) in [ + ("{ ed| } as WorkspaceConfigSchema", "editor"), + ("{ editor = { th| } } as WorkspaceConfigSchema", "theme"), + ( + "{ editor.settings = { en| } } as WorkspaceConfigSchema", + "enabled", + ), + ("{ web = { en| } } as WorkspaceConfigSchema", "enabled"), + ] { + let cursor = marked_source.find('|').unwrap(); + let source = marked_source.replace('|', ""); + let completion = environment + .complete_config(&path("main.dcdl"), &source, cursor, true) + .unwrap() + .unwrap(); + assert!( + completion.items.iter().any(|item| item.label == expected), + "{marked_source}: {completion:?}" + ); + } + } + + #[test] + fn authoring_schema_metadata_is_optional_validated_and_fingerprinted() { + let contribution = ConfigSchemaContribution::new( + "builtin:editor", + "editor", + "1", + "{ editor = {...Unknown}; }", + ) + .unwrap(); + let serialized = serde_json::to_value(&contribution).unwrap(); + assert!(serialized.get("authoring_source").is_none()); + assert_eq!( + serde_json::from_value::(serialized).unwrap(), + contribution + ); + let legacy = WorkspaceConfigSchemaBundle::compose([contribution.clone()]).unwrap(); + let legacy_input = serde_json::to_vec(&( + CONFIG_SOURCE_CONTRACT_VERSION, + DECODAL_VERSION, + vec![( + contribution.provider_id.as_str(), + contribution.namespace.as_str(), + contribution.version.as_str(), + contribution.source_digest.as_str(), + contribution.projection_validator.as_ref(), + )], + digest_bytes(legacy.source.as_bytes()), + )) + .unwrap(); + assert_eq!(legacy.fingerprint, digest_bytes(&legacy_input)); + legacy.validate().unwrap(); + + let authoring = contribution + .clone() + .with_authoring_source("{ editor = { theme = String; }; }"); + assert_eq!(authoring.source_digest, contribution.source_digest); + let serialized = serde_json::to_value(&authoring).unwrap(); + assert_eq!( + serialized["authoring_source"], + "{ editor = { theme = String; }; }" + ); + assert_eq!( + serde_json::from_value::(serialized).unwrap(), + authoring + ); + let authored = WorkspaceConfigSchemaBundle::compose([authoring]).unwrap(); + authored.validate().unwrap(); + assert_eq!(authored.source, legacy.source); + assert_ne!(authored.fingerprint, legacy.fingerprint); + assert_ne!( + ToolchainContract::with_schema_bundle(1, vec![path("main.dcdl")], 1, authored.clone()) + .fingerprint, + ToolchainContract::with_schema_bundle(1, vec![path("main.dcdl")], 1, legacy) + .fingerprint, + ); + let changed = WorkspaceConfigSchemaBundle::compose([contribution + .clone() + .with_authoring_source("{ editor = { theme = String; mode = String; }; }")]) + .unwrap(); + assert_ne!(changed.fingerprint, authored.fingerprint); + let mut tampered = authored; + tampered.contributions[0].authoring_source = + changed.contributions[0].authoring_source.clone(); + assert!(tampered.validate().is_err()); + for source in ["", " \n\t "] { + assert!( + matches!(WorkspaceConfigSchemaBundle::compose([contribution.clone().with_authoring_source(source)]), Err(ConfigTreeError::InvalidSchemaContribution(message)) if message.contains("authoring")) + ); + } + } + #[test] fn relative_imports_and_completion_share_the_snapshot_namespace() { let snapshot = ConfigTreeSnapshot::from_entries( diff --git a/crates/server-api/src/lib.rs b/crates/server-api/src/lib.rs index 0062c3bc5..65c717d8b 100644 --- a/crates/server-api/src/lib.rs +++ b/crates/server-api/src/lib.rs @@ -4496,6 +4496,10 @@ pub struct ConfigSchemaContribution { #[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "typescript", ts(optional))] pub projection_validator: Option, + /// Completion-only shape for partial values; never materialized as defaults. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(optional))] + pub authoring_source: Option, pub source_digest: String, } diff --git a/crates/worker-runtime/src/profile_archive.rs b/crates/worker-runtime/src/profile_archive.rs index 6c056b878..a38ff72dd 100644 --- a/crates/worker-runtime/src/profile_archive.rs +++ b/crates/worker-runtime/src/profile_archive.rs @@ -125,6 +125,35 @@ pub struct ProfileSourceArchiveInput { impl ProfileSourceArchive { pub fn build(input: ProfileSourceArchiveInput) -> Result { + Self::build_with_source_format(input, "decodal", "text/x-decodal") + } + + /// Archive an already evaluated Profile value without Decodal evaluation or conversion. + pub fn build_evaluated_profile( + id: String, + selector: String, + value: serde_json::Value, + ) -> Result { + let path = "profiles/evaluated.json".to_string(); + let source = serde_json::to_string(&value) + .map_err(|err| ProfileArchiveError::Json(err.to_string()))?; + Self::build_with_source_format( + ProfileSourceArchiveInput { + id, + entrypoints: BTreeMap::from([(selector, path.clone())]), + imports: BTreeMap::new(), + sources: BTreeMap::from([(path, source)]), + }, + "json", + "application/json", + ) + } + + fn build_with_source_format( + input: ProfileSourceArchiveInput, + kind: &str, + content_type: &str, + ) -> Result { if input.sources.len() > MAX_SOURCES { return Err(ProfileArchiveError::LimitExceeded("source count")); } @@ -145,8 +174,8 @@ impl ProfileSourceArchive { source_meta.push(ProfileSourceArchiveSource { path: path.clone(), source_key: path.clone(), - kind: "decodal".to_string(), - content_type: default_decodal_content_type(), + kind: kind.to_string(), + content_type: content_type.to_string(), digest: sha256_hex(source.as_bytes()), size_bytes: size, }); @@ -288,7 +317,17 @@ impl VerifiedProfileSourceArchive { actual: source.source_key.clone(), }); } - if source.content_type != default_decodal_content_type() { + let expected_content_type = match source.kind.as_str() { + "decodal" => "text/x-decodal", + "json" => "application/json", + _ => { + return Err(ProfileArchiveError::UnsupportedSource { + path: source.path.clone(), + kind: source.kind.clone(), + }); + } + }; + if source.content_type != expected_content_type { return Err(ProfileArchiveError::UnsupportedSource { path: source.path.clone(), kind: source.content_type.clone(), @@ -297,12 +336,6 @@ impl VerifiedProfileSourceArchive { if !manifest_paths.insert(source.path.clone()) { return Err(ProfileArchiveError::DuplicateSource(source.path.clone())); } - if source.kind != "decodal" { - return Err(ProfileArchiveError::UnsupportedSource { - path: source.path.clone(), - kind: source.kind.clone(), - }); - } let content = entries .get(&source.path) .ok_or_else(|| ProfileArchiveError::MissingSource(source.path.clone()))?; @@ -373,30 +406,50 @@ impl VerifiedProfileSourceArchive { .entrypoints .get(selector) .ok_or_else(|| ProfileArchiveError::MissingEntrypoint(selector.to_string()))?; - let mut engine = Engine::new(ArchiveSourceLoader { archive: self }); let source = self .sources .get(path) .ok_or_else(|| ProfileArchiveError::MissingSource(path.clone()))?; - let module = engine.add_root_source(path, path, source).map_err(|err| { - ProfileArchiveError::Decodal { - path: path.clone(), - message: format!("{err:?}"), + let metadata = self + .manifest + .sources + .iter() + .find(|source| &source.path == path) + .ok_or_else(|| ProfileArchiveError::MissingSource(path.clone()))?; + let json = match metadata.kind.as_str() { + "json" => serde_json::from_str(source) + .map_err(|err| ProfileArchiveError::Json(err.to_string()))?, + "decodal" => { + let mut engine = Engine::new(ArchiveSourceLoader { archive: self }); + let module = engine.add_root_source(path, path, source).map_err(|err| { + ProfileArchiveError::Decodal { + path: path.clone(), + message: format!("{err:?}"), + } + })?; + let value = + engine + .eval_module(module) + .map_err(|err| ProfileArchiveError::Decodal { + path: path.clone(), + message: format!("{err:?}"), + })?; + let data = + engine + .materialize(&value) + .map_err(|err| ProfileArchiveError::Decodal { + path: path.clone(), + message: format!("{err:?}"), + })?; + decodal_data_to_json(&data) } - })?; - let value = engine - .eval_module(module) - .map_err(|err| ProfileArchiveError::Decodal { - path: path.clone(), - message: format!("{err:?}"), - })?; - let data = engine - .materialize(&value) - .map_err(|err| ProfileArchiveError::Decodal { - path: path.clone(), - message: format!("{err:?}"), - })?; - let json = decodal_data_to_json(&data); + _ => { + return Err(ProfileArchiveError::UnsupportedSource { + path: path.clone(), + kind: metadata.kind.clone(), + }); + } + }; let resolved = resolve_profile_artifact_value( json, ProfileSource::Archive { @@ -674,6 +727,171 @@ mod tests { .unwrap() } + fn evaluated_profile() -> serde_json::Value { + serde_json::json!({ + "slug": "evaluated", + "description": "Evaluated profile", + "scope": "workspace_read", + "worker": { "mode": "wip" }, + "feature": { + "task": { "enabled": true }, + "memory": { "enabled": true }, + "sub_worker": { "enabled": true } + } + }) + } + + fn evaluated_archive() -> ProfileSourceArchive { + ProfileSourceArchive::build_evaluated_profile( + "evaluated-archive".to_string(), + "project:evaluated".to_string(), + evaluated_profile(), + ) + .unwrap() + } + + #[test] + fn evaluated_json_preserves_explicit_worker_mode_and_features() { + let archive = evaluated_archive(); + let verified = archive.verify().unwrap(); + let source = &verified.manifest().sources[0]; + assert_eq!(source.kind, "json"); + assert_eq!(source.content_type, "application/json"); + assert_eq!(verified.manifest().imports.len(), 0); + assert_eq!( + serde_json::from_str::(&verified.sources[&source.path]).unwrap(), + evaluated_profile() + ); + let root = tempfile::tempdir().unwrap(); + let manifest = verified + .resolve_profile("project:evaluated", root.path(), "evaluated-worker") + .unwrap(); + let json = serde_json::to_value(&manifest).unwrap(); + assert_eq!(manifest.worker.name, "evaluated-worker"); + assert_eq!(json["worker"]["mode"], "wip"); + assert_eq!(json["feature"]["task"]["enabled"], true); + assert_eq!(json["feature"]["memory"]["profile"]["enabled"], true); + assert_eq!(json["feature"]["sub_worker"]["enabled"], true); + } + + // Recompute the outer digest so verification reaches the source-level checks. + fn repack_archive( + archive: &mut ProfileSourceArchive, + manifest: &ProfileSourceArchiveManifest, + sources: &BTreeMap, + ) { + let mut content = Vec::new(); + { + let mut builder = Builder::new(&mut content); + append_bytes( + &mut builder, + MANIFEST_PATH, + &serde_json::to_vec(manifest).unwrap(), + ) + .unwrap(); + for (path, source) in sources { + append_bytes(&mut builder, path, source.as_bytes()).unwrap(); + } + builder.finish().unwrap(); + } + archive.reference.digest = sha256_hex(&content); + archive.reference.size_bytes = content.len() as u64; + archive.content = content; + } + + #[test] + fn evaluated_json_reuses_source_size_limit() { + assert!(matches!( + ProfileSourceArchive::build_evaluated_profile( + "oversized".to_string(), + "default".to_string(), + serde_json::Value::String("x".repeat(MAX_SOURCE_BYTES as usize)), + ), + Err(ProfileArchiveError::LimitExceeded("source bytes")) + )); + } + + #[test] + fn evaluated_json_rejects_archive_and_source_tampering() { + let mut archive = evaluated_archive(); + archive.content[0] ^= 1; + assert!(matches!( + archive.verify(), + Err(ProfileArchiveError::ArchiveDigestMismatch { .. }) + )); + + let mut archive = evaluated_archive(); + let mut verified = archive.verify().unwrap(); + let path = verified.manifest.sources[0].path.clone(); + verified + .sources + .get_mut(&path) + .unwrap() + .replace_range(0..1, "["); + repack_archive(&mut archive, &verified.manifest, &verified.sources); + assert!(matches!( + archive.verify(), + Err(ProfileArchiveError::SourceDigestMismatch { path: actual, .. }) if actual == path + )); + } + + #[test] + fn evaluated_json_requires_matching_source_kind_and_content_type() { + for (kind, content_type) in [ + ("json", "text/x-decodal"), + ("decodal", "application/json"), + ("unknown", "application/json"), + ] { + let mut archive = evaluated_archive(); + let mut verified = archive.verify().unwrap(); + verified.manifest.sources[0].kind = kind.to_string(); + verified.manifest.sources[0].content_type = content_type.to_string(); + repack_archive(&mut archive, &verified.manifest, &verified.sources); + assert!(matches!( + archive.verify(), + Err(ProfileArchiveError::UnsupportedSource { .. }) + )); + } + } + + #[test] + fn evaluated_json_does_not_fallback_to_filesystem_or_default() { + let root = tempfile::tempdir().unwrap(); + let profiles = root.path().join("profiles"); + std::fs::create_dir(&profiles).unwrap(); + let local_profile = profiles.join("evaluated.json"); + std::fs::write( + &local_profile, + serde_json::to_vec(&evaluated_profile()).unwrap(), + ) + .unwrap(); + let verified = evaluated_archive().verify().unwrap(); + for selector in [ + "default", + "profiles/evaluated.json", + local_profile.to_str().unwrap(), + ] { + assert!(matches!( + verified.resolve_profile(selector, root.path(), "evaluated-worker"), + Err(ProfileArchiveError::MissingEntrypoint(actual)) if actual == selector + )); + } + + // A valid local file must not rescue an invalid archived Profile value either. + let invalid = ProfileSourceArchive::build_evaluated_profile( + "invalid".to_string(), + "project:evaluated".to_string(), + serde_json::json!({ "not_a_profile_field": true }), + ) + .unwrap() + .verify() + .unwrap(); + assert!(matches!( + invalid.resolve_profile("project:evaluated", root.path(), "evaluated-worker"), + Err(ProfileArchiveError::Profile { .. }) + )); + } + #[test] fn build_and_verify_archive() { let archive = sample_archive(); diff --git a/crates/workspace-server/src/config_source.rs b/crates/workspace-server/src/config_source.rs index 2a127bd9c..19d566416 100644 --- a/crates/workspace-server/src/config_source.rs +++ b/crates/workspace-server/src/config_source.rs @@ -140,7 +140,11 @@ pub(crate) fn validate_workspace_config_candidate_projections( }; crate::prompt_settings::validate_evaluated_prompt_catalog(&candidate.evaluation)?; if has_provider("builtin:profile") { - crate::profile_settings::project_profiles_from_workspace_config(workspace_id, &state)?; + crate::profile_settings::project_profiles_from_evaluation( + workspace_id, + &state, + &candidate.evaluation, + )?; } if has_provider("builtin:runtime") { crate::runtime_settings::project_runtime_from_workspace_config(workspace_id, &state)?; diff --git a/crates/workspace-server/src/profile_settings.rs b/crates/workspace-server/src/profile_settings.rs index 5c38a099b..2d08b2e31 100644 --- a/crates/workspace-server/src/profile_settings.rs +++ b/crates/workspace-server/src/profile_settings.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; -use std::path::{Component, Path, PathBuf}; +use std::path::Path; -use config_source::{ConfigContentType, ConfigSchemaContribution, VirtualPath}; +use config_source::ConfigSchemaContribution; use manifest::{ProfileSource, builtin_profile_catalog_snapshot, resolve_profile_artifact_value}; use serde::Deserialize; use server_api::{ @@ -21,24 +21,19 @@ use crate::config_source::{ use crate::store::WorkspaceRecord; use crate::{Error, Result}; -const PROFILE_SCHEMA_SOURCE: &str = r#"{ - profile = { - default_profile = String default "builtin:companion"; - entries = [...{ - selector = String; - source = String; - label = String default ""; - description = String default ""; - }] default []; - }; -}"#; +const PROFILE_SCHEMA_SOURCE: &str = include_str!("../../../resources/config-schema/profile.dcdl"); #[derive(Debug, Default)] pub struct ProfileConfigSchemaProvider; impl WorkspaceConfigSchemaProvider for ProfileConfigSchemaProvider { fn contribution(&self) -> Result { - ConfigSchemaContribution::new("builtin:profile", "profile", "1", PROFILE_SCHEMA_SOURCE) + ConfigSchemaContribution::new("builtin:profile", "profile", "2", PROFILE_SCHEMA_SOURCE) + .map(|schema| { + schema.with_authoring_source(include_str!( + "../../../resources/config-schema/profile-authoring.dcdl" + )) + }) .map_err(|error| Error::Config(error.to_string())) } } @@ -59,7 +54,7 @@ struct VirtualProfileSection { #[serde(deny_unknown_fields)] struct VirtualProfileEntry { selector: String, - source: String, + profile: serde_json::Value, label: String, description: String, } @@ -68,7 +63,6 @@ struct VirtualProfileEntry { pub struct ProfileConfigProjection { pub settings: ProfileSettingsResponse, entries: BTreeMap, - sources: BTreeMap, } pub fn project_profiles_from_workspace_config( @@ -96,23 +90,42 @@ pub fn project_profiles_from_workspace_config( "Profile projection digest mismatch for Workspace {workspace_id}" ))); } + project_profiles_from_evaluation(workspace_id, state, &evaluation) +} + +pub(crate) fn project_profiles_from_evaluation( + workspace_id: &str, + state: &WorkspaceConfigState, + evaluation: &config_source::EvaluationResult, +) -> Result { + if evaluation.projection_digest != state.projection_digest { + return Err(Error::Config( + "Profile evaluation does not match Workspace config state".into(), + )); + } let projected = evaluation.projections.first().ok_or_else(|| { Error::RegistryInconsistency("Workspace config has no active projection".to_string()) })?; + if projected + .data_json + .pointer("/profile/entries") + .and_then(serde_json::Value::as_array) + .is_some_and(|entries| entries.iter().any(|entry| entry.get("source").is_some())) + { + return Err(profile_validation_error( + "profile_source_registration_removed", + "profile.entries.source is no longer supported: replace source = \"profiles/name.dcdl\" with profile = import \"./profiles/name.dcdl\" (relative to the containing config file). Saved config is not rewritten.", + )); + } let config: VirtualProfileConfig = serde_json::from_value(projected.data_json.clone()) .map_err(|error| Error::RegistryInconsistency(error.to_string()))?; let mut profiles = builtin_profile_summaries(Some(&config.profile.default_profile)); let mut entries = BTreeMap::new(); - let sources = state - .snapshot - .entries - .iter() - .filter(|(_, entry)| entry.content_type == ConfigContentType::Decodal) - .map(|(path, entry)| (path.as_str().to_string(), entry.content.clone())) - .collect::>(); let mut source_summaries = Vec::new(); for entry in config.profile.entries { - if !entry.selector.starts_with("project:") { + if !entry.selector.starts_with("project:") + || entry.selector.trim_start_matches("project:").is_empty() + { return Err(profile_validation_error( "profile_selector_invalid", "Workspace Profile selectors must use project:*", @@ -124,61 +137,43 @@ pub fn project_profiles_from_workspace_config( "Workspace Profile selectors must be unique", )); } - let source_path = VirtualPath::parse(&entry.source).map_err(|error| { - profile_validation_error("profile_source_path_invalid", &error.to_string()) - })?; - let source_entry = state.snapshot.get(&source_path).ok_or_else(|| { - profile_validation_error( - "profile_source_missing", - &format!( - "Profile source {:?} is missing from the active config revision", - entry.source - ), - ) - })?; - if source_entry.content_type != ConfigContentType::Decodal { - return Err(profile_validation_error( - "profile_source_type_invalid", - "Profile sources must use Decodal content", - )); - } - let archive_source = entry.source.clone(); let label = if entry.label.is_empty() { entry.selector.trim_start_matches("project:").to_string() } else { entry.label.clone() }; resolve_profile_artifact_value( - evaluate_profile_source(&state.snapshot, &source_path)?, + entry.profile.clone(), ProfileSource::Archive { archive_id: format!("workspace-config-r{}", state.snapshot.revision), - source: archive_source.clone(), + source: entry.selector.clone(), }, Path::new("/"), "workspace-config-validation", ) - .map_err(|error| profile_validation_error("profile_source_invalid", &error.to_string()))?; + .map_err(|error| profile_validation_error("profile_value_invalid", &error.to_string()))?; profiles.push(WorkspaceProfileSummary { profile_id: entry.selector.clone(), selector: entry.selector.clone(), label, source_kind: "project".to_string(), - profile_source_id: Some(entry.source.clone()), + profile_source_id: Some(entry.selector.clone()), description: (!entry.description.is_empty()).then(|| entry.description.clone()), editable: false, is_default: config.profile.default_profile == entry.selector, diagnostics: Vec::new(), }); + let value_bytes = serde_json::to_vec(&entry.profile).expect("Profile value serializes"); source_summaries.push(WorkspaceProfileSourceSummary { - profile_source_id: entry.source.clone(), - display_path: entry.source.clone(), - kind: "virtual_config".to_string(), - content_type: "decodal".to_string(), - content_digest: source_entry.content_digest.clone(), + profile_source_id: entry.selector.clone(), + display_path: format!("profile.entries[{}].profile", source_summaries.len()), + kind: "evaluated_config".to_string(), + content_type: "application/json".to_string(), + content_digest: config_source::digest_bytes(&value_bytes), provenance: WorkspaceProfileSourceProvenance::ProjectProfileSourceTree, editable: false, revision: state.snapshot.revision.to_string(), - size_bytes: source_entry.content.len() as u64, + size_bytes: value_bytes.len() as u64, diagnostics: Vec::new(), }); entries.insert(entry.selector.clone(), entry); @@ -198,46 +193,16 @@ pub fn project_profiles_from_workspace_config( registry_revision: format!("config:{}", state.snapshot.revision), config_revision: Some(state.snapshot.revision), tree_digest: Some(state.snapshot.digest.clone()), - projection_digest: Some(evaluation.projection_digest), + projection_digest: Some(evaluation.projection_digest.clone()), default_profile: Some(config.profile.default_profile), profiles, sources: source_summaries, diagnostics: Vec::new(), }, entries, - sources, }) } -fn evaluate_profile_source( - snapshot: &config_source::ConfigTreeSnapshot, - source_path: &VirtualPath, -) -> Result { - let contract = config_source::ToolchainContract::with_schema_bundle( - config_source::DEFAULT_SCHEMA_VERSION, - vec![source_path.clone()], - config_source::DEFAULT_IMPORT_POLICY_VERSION, - config_source::WorkspaceConfigSchemaBundle::empty(), - ); - let evaluation = config_source::SnapshotEnvironment::new(snapshot.clone()) - .evaluate_contract(&contract) - .map_err(|diagnostics| { - profile_validation_error( - "profile_source_invalid", - &serde_json::to_string(&diagnostics) - .unwrap_or_else(|_| "Profile source evaluation failed".to_string()), - ) - })?; - evaluation - .projections - .into_iter() - .next() - .map(|projection| projection.data_json) - .ok_or_else(|| { - profile_validation_error("profile_source_invalid", "Profile source has no projection") - }) -} - pub fn selector_for_workspace_candidate( projection: &ProfileConfigProjection, profile: &str, @@ -420,6 +385,16 @@ pub fn build_virtual_profile_config_bundle_with_prompt_projection( selector: &str, prompt_projection: &worker::WorkspacePromptProjection, ) -> Result> { + if projection.settings.workspace_id != workspace_id + || projection.settings.config_revision != Some(state.snapshot.revision) + || projection.settings.tree_digest.as_deref() != Some(state.snapshot.digest.as_str()) + || projection.settings.projection_digest.as_deref() + != Some(state.projection_digest.as_str()) + { + return Err(Error::Config( + "Profile projection does not match Workspace config state".into(), + )); + } validate_prompt_projection_matches_state(workspace_id, state, prompt_projection)?; let prompt_catalog = prompt_projection.catalog().clone(); let profile_selector = @@ -429,12 +404,7 @@ pub fn build_virtual_profile_config_bundle_with_prompt_projection( )) })?; let archive = match projection.entries.get(selector) { - Some(entry) => Some(build_virtual_profile_archive( - selector, - entry, - &projection.sources, - state, - )?), + Some(entry) => Some(build_virtual_profile_archive(selector, entry, state)?), None => Some(builtin_profile_source_archive(&profile_selector).map_err(Error::Store)?), }; let bundle_id = virtual_profile_bundle_id( @@ -492,19 +462,14 @@ pub(crate) fn resolve_profile_manifest_from_config_bundle( fn build_virtual_profile_archive( selector: &str, entry: &VirtualProfileEntry, - all_sources: &BTreeMap, state: &WorkspaceConfigState, ) -> Result { - let mut closure = BTreeMap::new(); - let mut imports = BTreeMap::new(); - collect_profile_import_closure(&entry.source, all_sources, &mut closure, &mut imports)?; - ProfileSourceArchive::build(ProfileSourceArchiveInput { - id: format!("workspace-config-profile-r{}", state.snapshot.revision), - entrypoints: BTreeMap::from([(selector.to_string(), entry.source.clone())]), - imports, - sources: closure, - }) - .map_err(|error| profile_validation_error("profile_source_archive_invalid", &error.to_string())) + ProfileSourceArchive::build_evaluated_profile( + format!("workspace-config-profile-r{}", state.snapshot.revision), + selector.to_string(), + entry.profile.clone(), + ) + .map_err(|error| profile_validation_error("profile_value_archive_invalid", &error.to_string())) } pub fn workspace_metadata_settings( @@ -576,124 +541,6 @@ fn profile_validation_error(code: impl Into, message: impl Into) } } -fn collect_profile_import_closure( - current_path: &str, - all_sources: &BTreeMap, - closure_sources: &mut BTreeMap, - imports: &mut BTreeMap, -) -> Result<()> { - if closure_sources.contains_key(current_path) { - return Ok(()); - } - let content = all_sources.get(current_path).ok_or_else(|| { - profile_validation_error( - "profile_source_import_missing", - &format!("Profile source import closure is missing {current_path}"), - ) - })?; - closure_sources.insert(current_path.to_string(), content.clone()); - for specifier in collect_decodal_import_specifiers(content) { - let target = resolve_profile_source_import(current_path, &specifier)?; - if !all_sources.contains_key(&target) { - return Err(profile_validation_error( - "profile_source_import_missing", - &format!( - "Profile source import {specifier:?} from {current_path} resolves to missing {target}" - ), - )); - } - imports.insert(format!("{current_path}\0{specifier}"), target.clone()); - collect_profile_import_closure(&target, all_sources, closure_sources, imports)?; - } - Ok(()) -} - -fn resolve_profile_source_import(current_path: &str, specifier: &str) -> Result { - if specifier.is_empty() || specifier.contains("://") || Path::new(specifier).is_absolute() { - return Err(profile_validation_error( - "profile_source_import_invalid", - "Profile source import must be a virtual relative path", - )); - } - let raw = specifier - .strip_prefix("project:") - .or_else(|| specifier.strip_prefix("workspace:")) - .unwrap_or(specifier); - if specifier.contains(':') && raw == specifier { - return Err(profile_validation_error( - "profile_source_import_invalid", - "Unsupported profile source import namespace", - )); - } - let base = if raw.starts_with("profiles/") || raw.starts_with("./profiles/") { - PathBuf::from(raw) - } else { - Path::new(current_path) - .parent() - .map(Path::to_path_buf) - .unwrap_or_default() - .join(raw) - }; - let normalized = normalize_virtual_profile_source_path(&base.to_string_lossy())?; - Ok(normalized) -} - -fn normalize_virtual_profile_source_path(path: &str) -> Result { - let mut normalized = PathBuf::new(); - for component in Path::new(path).components() { - match component { - Component::CurDir => {} - Component::Normal(value) => normalized.push(value), - Component::ParentDir => { - if !normalized.pop() { - return Err(profile_validation_error( - "profile_source_import_invalid", - "Profile source import escapes the virtual tree", - )); - } - } - Component::RootDir | Component::Prefix(_) => { - return Err(profile_validation_error( - "profile_source_import_invalid", - "Profile source import must be relative", - )); - } - } - } - Ok(normalized.to_string_lossy().replace('\\', "/")) -} - -fn collect_decodal_import_specifiers(content: &str) -> Vec { - let mut specifiers = Vec::new(); - for line in content.lines() { - let trimmed = line.trim_start(); - if trimmed.starts_with("//") || trimmed.starts_with('#') { - continue; - } - let Some(index) = trimmed.find("import") else { - continue; - }; - let after = trimmed[index + "import".len()..].trim_start(); - let Some(first) = after.chars().next() else { - continue; - }; - if first == '"' || first == '\'' { - if let Some(end) = after[1..].find(first) { - specifiers.push(after[1..1 + end].to_string()); - } - } else { - let ident: String = after - .chars() - .take_while(|ch| !ch.is_whitespace() && *ch != ';' && *ch != ',' && *ch != '{') - .collect(); - if !ident.is_empty() { - specifiers.push(ident); - } - } - } - specifiers -} - pub fn selector_for_builtin_candidate( id: &str, ) -> Option { @@ -711,6 +558,7 @@ pub fn selector_for_builtin_candidate( #[cfg(test)] mod tests { use super::*; + use config_source::{ConfigContentType, VirtualPath}; #[test] fn workspace_metadata_projects_server_database_record_without_filesystem_diagnostics() { @@ -779,7 +627,7 @@ mod tests { config_source::ConfigEntry::new( VirtualPath::parse("main.dcdl").unwrap(), ConfigContentType::Decodal, - r#"{ profile = { default_profile = "project:alpha"; entries = [{ selector = "project:alpha"; source = "profiles/alpha.dcdl"; label = "Alpha"; }]; }; }"#, + r#"{ profile = { default_profile = "project:alpha"; entries = [{ selector = "project:alpha"; profile = import "./profiles/alpha.dcdl"; label = "Alpha"; }]; }; }"#, ) .unwrap(), config_source::ConfigEntry::new( @@ -819,18 +667,18 @@ mod tests { .entrypoints .get("project:alpha") .map(String::as_str), - Some("profiles/alpha.dcdl") + Some("profiles/evaluated.json") ); assert_eq!(archive.reference.source_graph.source_count, 1); } #[test] - fn virtual_config_projection_preserves_import_closure() { + fn virtual_config_projection_materializes_import_closure() { let state = virtual_state(vec![ config_source::ConfigEntry::new( VirtualPath::parse("main.dcdl").unwrap(), ConfigContentType::Decodal, - r#"{ profile = { entries = [{ selector = "project:alpha"; source = "profiles/alpha.dcdl"; }]; }; }"#, + r#"{ profile = { entries = [{ selector = "project:alpha"; profile = import "./profiles/alpha.dcdl"; }]; }; }"#, ) .unwrap(), config_source::ConfigEntry::new( @@ -857,8 +705,8 @@ mod tests { .unwrap() .unwrap(); let archive = bundle.profile_source_archive.unwrap(); - assert_eq!(archive.reference.source_graph.source_count, 2); - assert_eq!(archive.reference.source_graph.import_count, 1); + assert_eq!(archive.reference.source_graph.source_count, 1); + assert_eq!(archive.reference.source_graph.import_count, 0); } #[test] @@ -955,8 +803,8 @@ mod tests { VirtualPath::parse("main.dcdl").unwrap(), ConfigContentType::Decodal, r#"{ profile = { entries = [ - { selector = "project:alpha"; source = "profiles/alpha.dcdl"; }, - { selector = "project:beta"; source = "profiles/beta.dcdl"; }, + { selector = "project:alpha"; profile = import "./profiles/alpha.dcdl"; }, + { selector = "project:beta"; profile = import "./profiles/beta.dcdl"; }, ]; }; }"#, ) .unwrap(), @@ -1002,20 +850,83 @@ mod tests { } #[test] - fn virtual_config_projection_rejects_missing_profile_source() { - let state = virtual_state(vec![ - config_source::ConfigEntry::new( - VirtualPath::parse("main.dcdl").unwrap(), - ConfigContentType::Decodal, - r#"{ profile = { entries = [{ selector = "project:alpha"; source = "profiles/missing.dcdl"; }]; }; }"#, - ) - .unwrap(), - ]); - let error = project_profiles_from_workspace_config("workspace-test", &state).unwrap_err(); - assert!( - error - .to_string() - .contains("missing from the active config revision") + fn value_profile_authoring_uses_current_provider_schema_without_inserting_defaults() { + let bundle = + config_source::WorkspaceConfigSchemaBundle::compose([ProfileConfigSchemaProvider + .contribution() + .unwrap()]) + .unwrap(); + let snapshot = config_source::ConfigTreeSnapshot::from_entries(1, [config_source::ConfigEntry::new( + VirtualPath::parse("main.dcdl").unwrap(), ConfigContentType::Decodal, + r#"{ profile = { entries = [{ selector = "project:alpha"; profile = {}; }]; }; } as WorkspaceConfigSchema"#, + ).unwrap()]).unwrap(); + let environment = config_source::SnapshotEnvironment::new(snapshot.clone()) + .with_schema_bundle(bundle.clone()); + for (body, token, label) in [ + ( + "{ profile = { entries = [{ sel }] } } as WorkspaceConfigSchema", + "sel", + "selector", + ), + ( + "{ profile = { entries = [{ pro }] } } as WorkspaceConfigSchema", + "pro", + "profile", + ), + ( + "{ profile = { entries = [{ profile = { wor } }] } } as WorkspaceConfigSchema", + "wor", + "worker", + ), + ( + "{ profile = { entries = [{ profile = { worker = { mo } } }] } } as WorkspaceConfigSchema", + "mo", + "mode", + ), + ( + "{ profile = { entries = [{ profile = { feature = { ti } } }] } } as WorkspaceConfigSchema", + "ti", + "ticket", + ), + ] { + let offset = body.rfind(token).unwrap() + token.len(); + let completion = environment + .complete_config( + &VirtualPath::parse("main.dcdl").unwrap(), + body, + offset, + true, + ) + .unwrap() + .unwrap(); + assert!( + completion.items.iter().any(|item| item.label == label), + "{label}: {completion:?}" + ); + } + for invalid in [ + r#"{ profile = { entries = [{ selector = 42; profile = {}; }]; }; } as WorkspaceConfigSchema"#, + r#"{ profile = { entries = [{ selector = "project:alpha"; profile = 42; }]; }; } as WorkspaceConfigSchema"#, + ] { + assert!( + !environment + .analyze(&VirtualPath::parse("main.dcdl").unwrap(), Some(invalid)) + .is_empty() + ); + } + let evaluation = environment + .evaluate_contract(&config_source::ToolchainContract::with_schema_bundle( + config_source::DEFAULT_SCHEMA_VERSION, + vec![VirtualPath::parse("main.dcdl").unwrap()], + config_source::DEFAULT_IMPORT_POLICY_VERSION, + bundle, + )) + .unwrap(); + assert_eq!( + evaluation.projections[0] + .data_json + .pointer("/profile/entries/0/profile"), + Some(&serde_json::json!({})) ); } } diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index 84fb046e1..ac8759821 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -12320,6 +12320,7 @@ fn config_schema_bundle_to_api( namespace: contribution.namespace, version: contribution.version, source: contribution.source, + authoring_source: contribution.authoring_source, projection_validator: contribution .projection_validator .map(config_projection_validator_to_api), @@ -33466,6 +33467,7 @@ impl IntoResponse for ApiError { #[cfg(test)] mod tests { mod subjektiv_jobs_tests; + mod value_profiles_tests; mod worker_operations_tests; include!("server_workspace_config_tests.rs"); use super::*; diff --git a/crates/workspace-server/src/server/tests/subjektiv_jobs_tests.rs b/crates/workspace-server/src/server/tests/subjektiv_jobs_tests.rs index 78c191a4d..6905bfe58 100644 --- a/crates/workspace-server/src/server/tests/subjektiv_jobs_tests.rs +++ b/crates/workspace-server/src/server/tests/subjektiv_jobs_tests.rs @@ -454,7 +454,7 @@ async fn backend_job_project_recipe_is_resolved_and_delivered_without_system_ove entrypoints: state.contract.entrypoints.iter().map(|p| p.as_str().into()).collect(), changes: vec![server_api::ConfigTreeChange::Update { path: entrypoint.as_str().into(), expected_digest: root.content_digest.clone(), - content: "{ profile = { entries = [{ selector = \"project:job-recipe\"; source = \"profiles/job-recipe.dcdl\"; }]; }; }".into(), + content: "{ profile = { entries = [{ selector = \"project:job-recipe\"; profile = import \"./profiles/job-recipe.dcdl\"; }]; }; }".into(), }, server_api::ConfigTreeChange::Create { path: "profiles/job-recipe.dcdl".into(), content_type: server_api::ConfigContentType::Decodal, content: "{ slug = \"job-recipe\"; scope = \"workspace_read\"; model = { id = \"fixture-model\"; }; engine = { instruction = \"role.default\"; }; feature = { task = { enabled = true; }; }; }".into(), diff --git a/crates/workspace-server/src/server/tests/value_profiles_tests.rs b/crates/workspace-server/src/server/tests/value_profiles_tests.rs new file mode 100644 index 000000000..984f8f5b4 --- /dev/null +++ b/crates/workspace-server/src/server/tests/value_profiles_tests.rs @@ -0,0 +1,362 @@ +use super::*; +use crate::profile_settings::{ + build_virtual_profile_config_bundle, project_profiles_from_workspace_config, +}; + +const PROFILE: &str = r#"{ + slug = "alpha"; + worker = { mode = "wip"; }; + model = { ref = "codex-oauth/gpt-5.6-sol"; }; + feature = { task = { enabled = true; }; ticket = { enabled = false; }; }; +}"#; + +fn request( + api: &WorkspaceApi, + source: &str, + extra: Vec, +) -> ConfigCommitRequest { + let state = api + .config_store + .load_workspace_config(TEST_WORKSPACE_ID) + .unwrap() + .unwrap(); + let main = state + .snapshot + .get(&config_source::VirtualPath::parse("main.dcdl").unwrap()) + .unwrap(); + let mut changes = vec![config_source::ConfigTreeChange::Update { + path: main.path.clone(), + expected_digest: main.content_digest.clone(), + content: source.into(), + }]; + changes.extend(extra); + ConfigCommitRequest { + base_revision: state.snapshot.revision, + base_digest: state.snapshot.digest, + entrypoints: state.contract.entrypoints, + changes, + } +} + +fn source(value: &str) -> String { + format!( + r#"{{ profile = {{ default_profile = "project:alpha"; entries = [{{ selector = "project:alpha"; label = "Alpha"; description = "Value recipe"; profile = {value}; }}]; }}; }}"# + ) +} + +#[tokio::test] +async fn value_profiles_save_project_and_runtime_consume_the_same_revision() { + let dir = tempfile::tempdir().unwrap(); + let api = test_api(dir.path()).await; + let forms = [ + PROFILE.to_string(), + r#"import "./recipes/alpha.dcdl""#.into(), + r#"(import "./recipes/base.dcdl") // { worker = { mode = "wip"; }; feature.task.enabled = true; }"#.into(), + ]; + let mut manifests = Vec::new(); + let mut saved_manifest = None; + for (index, form) in forms.iter().enumerate() { + let extra = if index == 0 { + vec![ + config_source::ConfigTreeChange::Create { + path: config_source::VirtualPath::parse("recipes/alpha.dcdl").unwrap(), + content_type: config_source::ConfigContentType::Decodal, + content: PROFILE.into(), + }, + config_source::ConfigTreeChange::Create { + path: config_source::VirtualPath::parse("recipes/base.dcdl").unwrap(), + content_type: config_source::ConfigContentType::Decodal, + content: PROFILE + .replace("wip", "tools") + .replace("enabled = true", "enabled = false"), + }, + ] + } else { + vec![] + }; + let state = commit_workspace_config_tree( + &api, + TEST_WORKSPACE_ID, + &request(&api, &source(form), extra), + ) + .unwrap(); + let loaded = api + .config_store + .load_workspace_config(TEST_WORKSPACE_ID) + .unwrap() + .unwrap(); + assert_eq!(loaded, state); + // The Browser receives the same fingerprint-bound authoring metadata through REST. + let wire = workspace_config_state_to_api(loaded.clone()); + let decoded: config_source::WorkspaceConfigSchemaBundle = + serde_json::from_value(serde_json::to_value(wire.contract.schema_bundle).unwrap()) + .unwrap(); + decoded.validate().unwrap(); + assert_eq!(decoded, loaded.contract.schema_bundle); + assert!( + decoded + .contributions + .iter() + .find(|entry| entry.provider_id == "builtin:profile") + .unwrap() + .authoring_source + .is_some() + ); + let projection = + project_profiles_from_workspace_config(TEST_WORKSPACE_ID, &loaded).unwrap(); + assert_eq!( + projection.settings.default_profile.as_deref(), + Some("project:alpha") + ); + assert_eq!( + projection.settings.config_revision, + Some(state.snapshot.revision) + ); + let selected = projection + .settings + .profiles + .iter() + .find(|entry| entry.selector == "project:alpha") + .unwrap(); + assert_eq!(selected.label, "Alpha"); + assert_eq!(selected.description.as_deref(), Some("Value recipe")); + assert!(selected.is_default); + assert_eq!(selected.profile_source_id.as_deref(), Some("project:alpha")); + let bundle = build_virtual_profile_config_bundle( + &projection, + &state, + TEST_WORKSPACE_ID, + "2026-01-01T00:00:00Z", + "project:alpha", + ) + .unwrap() + .unwrap(); + assert_eq!(bundle.metadata.digest, bundle.computed_digest()); + assert_eq!( + bundle.metadata.revision, + state.snapshot.revision.to_string() + ); + let archive = bundle.profile_source_archive.as_ref().unwrap(); + let verified = archive.verify().unwrap(); + // This is the Runtime's archive consumer. It needs neither the Workspace tree nor imports. + let resolved = verified + .resolve_profile( + "project:alpha", + Path::new("/runtime-worker"), + "value-worker", + ) + .unwrap(); + assert_eq!(resolved.worker.mode, manifest::WorkerMode::Wip); + assert!(resolved.feature.task.enabled); + assert!(!resolved.feature.ticket.enabled); + assert!(!resolved.feature.ticket.authoring); + assert!(!resolved.feature.sub_worker.enabled); + assert_eq!(archive.reference.source_graph.import_count, 0); + let evaluation = config_source::SnapshotEnvironment::new(state.snapshot.clone()) + .evaluate_contract(&state.contract) + .unwrap(); + let value = evaluation.projections[0] + .data_json + .pointer("/profile/entries/0/profile") + .unwrap(); + assert!(value.is_object()); + assert!( + value.get("engine").is_none(), + "omission must remain omission in the Profile value" + ); + let mut snapshot = serde_json::to_value(&resolved).unwrap(); + snapshot.as_object_mut().unwrap().remove("profile"); // revision-specific provenance only + manifests.push(snapshot); + if index == 0 { + saved_manifest = + Some(manifest::write_persisted_worker_manifest_snapshot(&resolved).unwrap()); + } + if index == 2 { + let mut other_state = state.clone(); + other_state.snapshot.revision += 1; + assert!( + build_virtual_profile_config_bundle( + &projection, + &other_state, + TEST_WORKSPACE_ID, + "created", + "project:alpha" + ) + .unwrap_err() + .to_string() + .contains("does not match") + ); + } + } + assert_eq!(manifests[0], manifests[1]); + assert_eq!(manifests[1], manifests[2]); + // Saving a new recipe is not an operation on existing Workers or their persisted Manifest. + let state = commit_workspace_config_tree( + &api, + TEST_WORKSPACE_ID, + &request( + &api, + &source( + &PROFILE + .replace("wip", "tools") + .replace("enabled = true", "enabled = false"), + ), + vec![], + ), + ) + .unwrap(); + let projection = project_profiles_from_workspace_config(TEST_WORKSPACE_ID, &state).unwrap(); + let bundle = build_virtual_profile_config_bundle( + &projection, + &state, + TEST_WORKSPACE_ID, + "created", + "project:alpha", + ) + .unwrap() + .unwrap(); + let fresh = crate::profile_settings::resolve_profile_manifest_from_config_bundle( + &bundle, + "project:alpha", + ) + .unwrap(); + assert_eq!(fresh.worker.mode, manifest::WorkerMode::Tools); + assert!(!fresh.feature.task.enabled); + let restored = + manifest::read_persisted_worker_manifest_snapshot(saved_manifest.unwrap()).unwrap(); + assert_eq!(restored.worker.mode, manifest::WorkerMode::Wip); + assert!(restored.feature.task.enabled); +} + +#[tokio::test] +async fn value_profiles_invalid_saves_preserve_the_active_config() { + let dir = tempfile::tempdir().unwrap(); + let api = test_api(dir.path()).await; + let before = commit_workspace_config_tree( + &api, + TEST_WORKSPACE_ID, + &request(&api, &source(PROFILE), vec![]), + ) + .unwrap(); + let invalid = [ + (source("42"), ""), + (source(r#"{ worker = { mode = "not-a-mode"; }; }"#), "profile_value_invalid"), + (source(r#"{ worker = { name = "runtime-only"; }; }"#), "profile_value_invalid"), + (source(r#"import "./missing.dcdl""#), ""), + (source(r#"import "../outside.dcdl""#), ""), + (r#"{ profile = { entries = [{ selector = "project:alpha"; profile = {}; }, { selector = "project:alpha"; profile = {}; }]; }; }"#.into(), "profile_selector_duplicate"), + (r#"{ profile = { default_profile = "project:unknown"; }; }"#.into(), "unknown_default_profile"), + (r#"{ profile = { entries = [{ selector = "project:"; profile = {}; }]; }; }"#.into(), "profile_selector_invalid"), + (r#"{ profile = { entries = [{ selector = "project:alpha"; source = "profiles/alpha.dcdl"; }]; }; }"#.into(), ""), + ]; + for (source, code) in invalid { + let error = + commit_workspace_config_tree(&api, TEST_WORKSPACE_ID, &request(&api, &source, vec![])) + .unwrap_err(); + assert!(error.to_string().contains(code), "{error}"); + assert_eq!( + api.config_store + .load_workspace_config(TEST_WORKSPACE_ID) + .unwrap() + .unwrap(), + before + ); + } +} + +#[tokio::test] +async fn value_profiles_legacy_source_config_requires_explicit_lossless_migration() { + let dir = tempfile::tempdir().unwrap(); + let api = test_api(dir.path()).await; + let mut contributions = api.config_schema_registry.compose().unwrap().contributions; + let profile_schema = contributions + .iter_mut() + .find(|entry| entry.provider_id == "builtin:profile") + .unwrap(); + *profile_schema = config_source::ConfigSchemaContribution::new("builtin:profile", "profile", "1", r#"{ + profile = { default_profile = String default "builtin:companion"; + entries = [...{ selector = String; source = String; label = String default ""; description = String default ""; }] default []; + }; + }"#).unwrap(); + let old_schema = config_source::WorkspaceConfigSchemaBundle::compose(contributions).unwrap(); + let old_source = r#"{ profile = { default_profile = "project:alpha"; entries = [{ selector = "project:alpha"; label = "Alpha"; source = "recipes/alpha.dcdl"; }]; }; }"#; + let candidate = api + .config_store + .evaluate_workspace_config_candidate_with_schema( + TEST_WORKSPACE_ID, + &request( + &api, + old_source, + vec![config_source::ConfigTreeChange::Create { + path: config_source::VirtualPath::parse("recipes/alpha.dcdl").unwrap(), + content_type: config_source::ConfigContentType::Decodal, + content: PROFILE.into(), + }], + ), + old_schema, + ) + .unwrap(); + // Fixture simulates a previously persisted v1 schema/revision. + let legacy = api + .config_store + .commit_evaluated_workspace_config(TEST_WORKSPACE_ID, &candidate) + .unwrap(); + let error = project_profiles_from_workspace_config(TEST_WORKSPACE_ID, &legacy).unwrap_err(); + assert!( + error + .to_string() + .contains("profile_source_registration_removed") + ); + assert!(error.to_string().contains("profile = import")); + assert_eq!( + api.config_store + .load_workspace_config(TEST_WORKSPACE_ID) + .unwrap() + .unwrap(), + legacy + ); + assert!( + commit_workspace_config_tree(&api, TEST_WORKSPACE_ID, &request(&api, old_source, vec![])) + .is_err() + ); + assert_eq!( + api.config_store + .load_workspace_config(TEST_WORKSPACE_ID) + .unwrap() + .unwrap(), + legacy + ); + let migrated_source = old_source.replace( + r#"source = "recipes/alpha.dcdl""#, + r#"profile = import "./recipes/alpha.dcdl""#, + ); + let migrated = commit_workspace_config_tree( + &api, + TEST_WORKSPACE_ID, + &request(&api, &migrated_source, vec![]), + ) + .unwrap(); + let projection = project_profiles_from_workspace_config(TEST_WORKSPACE_ID, &migrated).unwrap(); + assert_eq!( + projection.settings.default_profile.as_deref(), + Some("project:alpha") + ); + assert_eq!( + migrated + .snapshot + .get(&config_source::VirtualPath::parse("recipes/alpha.dcdl").unwrap()), + legacy + .snapshot + .get(&config_source::VirtualPath::parse("recipes/alpha.dcdl").unwrap()) + ); + assert_eq!( + projection + .settings + .profiles + .iter() + .find(|entry| entry.selector == "project:alpha") + .unwrap() + .label, + "Alpha" + ); +} diff --git a/docs/design/profiles-manifests-prompts.md b/docs/design/profiles-manifests-prompts.md index 4e98f622a..8d5192f39 100644 --- a/docs/design/profiles-manifests-prompts.md +++ b/docs/design/profiles-manifests-prompts.md @@ -17,7 +17,33 @@ A Profile should not contain runtime-bound fields: Those fields depend on one run, one parent, or one machine. Putting them in a reusable Profile makes reuse unsafe. -Yoi Profiles are data artifacts resolved from builtin profile definitions, `profiles.toml`, Decodal source archives, or explicit JSON/TOML artifacts. Decodal is the authoring syntax used by the Workspace profile editor and Backend Runtime archive path; JSON/TOML artifacts are the low-level resolver interchange format. Runtime launch should not depend on executable profile scripts. +Yoi Profiles are data artifacts resolved from builtin profile definitions, `profiles.toml`, Workspace Config values, or explicit JSON/TOML artifacts. Decodal is the Workspace authoring syntax; JSON/TOML artifacts are the low-level resolver interchange format. Runtime launch does not depend on executable profile scripts. + +### Workspace registration and migration + +Register the Profile **value**, not a source pathname: + +```dcdl +{ + profile = { + default_profile = "project:my-companion"; + entries = [{ + selector = "project:my-companion"; + label = "My Companion"; + description = "My reusable Worker recipe"; + profile = import "./profiles/my-companion.dcdl"; + }]; + }; +} as WorkspaceConfigSchema +``` + +The imported file can contain `{ worker = { mode = "wip"; }; model = { ref = "codex-oauth/gpt-5.6-sol"; }; feature = { task = { enabled = true; }; }; }`. That record can also be written directly at `entries[0].profile`. Composition uses ordinary Decodal expressions, for example `(import "./profiles/base.dcdl") // { worker = { mode = "wip"; }; }` for an explicit patch, or `&` for compatible refinement. No dedicated Profile file or `profiles/` directory is required. Imports are relative to the **containing virtual config file**, not a host filesystem path; missing imports and virtual-tree escapes are rejected. + +`selector` must be a nonempty `project:*` selector, unique in the Workspace. `label` and `description` are optional display metadata. An omitted `default_profile` selects `builtin:companion`; an explicitly unknown default is an error, not a fallback. A project Profile is a partial recipe resolved against `WorkerManifestConfig::resolution_defaults()`, **not** implicitly inherited from a builtin role. Explicit values remain explicit and omitted Profile fields remain absent in the transported value. Builtin-value import helpers are not part of this registration contract. + +The former `entries = [{ selector = "project:name"; source = "profiles/name.dcdl"; }]` registration is no longer accepted. Migrate it explicitly to `profile = import "./profiles/name.dcdl";`, preserving the selector, label, description and default. For registrations in a nested config file, adjust the relative import to the original virtual-tree entry (for example `../profiles/name.dcdl`). Do not rename or recreate the Profile file merely for registration. Previously saved source-based revisions remain readable as config data and are not rewritten, deleted, or silently substituted with a builtin. Their Profile projection/launch reports `profile_source_registration_removed` with the replacement form; saves under the new schema reject the old field. Existing Workers continue using their saved Manifest while the author migrates and saves the config. + +The schema contribution separates its materialized registration schema from an optional, fingerprint-bound `authoring_source`. The latter supplies editor field suggestions inside array elements and partial Profile records; it is never evaluated as defaults or substituted for the validation schema. Decodal structural diagnostics cover registration/value shape, and the shared Backend save boundary checks full Profile/Manifest semantics (including mode, forbidden runtime fields, duplicate selectors and default references). UI and WIP saves use that same boundary; invalid candidates leave the active config revision untouched. ## Manifests @@ -27,7 +53,7 @@ Source/partial layers may omit fields. Resolved manifests should be explicit eno `--manifest ` exists as an explicit low-level escape hatch. Normal fresh startup selects a `builtin:*` or `project:*` Profile from the Backend-managed Workspace Config revision rather than applying an ambient manifest cascade. -Project Profiles are evaluated from the revisioned Virtual Config's Decodal source/import closure. The Backend packages that closure into a digest-bound Profile source archive, delivers it with the resolved launch bundle, and the Worker persists the resulting Manifest for restore. Files below the Workdir are not implicit Profile override layers. +Project Profiles are evaluated as part of the revisioned Virtual Config, including its imports and value composition. The Backend packages the **evaluated selected value** into a digest-bound JSON entry in the existing Profile archive transport, and binds the launch bundle to the Workspace identity, revision, tree/projection digests and schema/toolchain fingerprints. Runtime verifies the archive and resolves that exact value without re-evaluating Decodal, scanning imports or reading Workspace files. Builtin Profiles retain their embedded Decodal archive path. Saving config changes only future fresh launches; the Worker persists the resulting resolved Manifest and restore uses that saved snapshot rather than selecting the current recipe again. Files below the Workdir are not implicit Profile override layers. ## Local stdio MCP server declarations diff --git a/openapi/server-api.json b/openapi/server-api.json index c217d3c0f..9eb8e17d3 100644 --- a/openapi/server-api.json +++ b/openapi/server-api.json @@ -1235,6 +1235,13 @@ "ConfigSchemaContribution": { "additionalProperties": false, "properties": { + "authoring_source": { + "description": "Completion-only shape for partial values; never materialized as defaults.", + "type": [ + "string", + "null" + ] + }, "namespace": { "type": "string" }, @@ -15474,7 +15481,7 @@ "info": { "title": "Yoi Server API", "version": "0.1.0", - "x-yoi-source-digest": "sha256:fd06f382c6d33a5c3cc98ef9e264545923c53fa95a9be555a85680f98c6065da" + "x-yoi-source-digest": "sha256:a6a7147c9a2f465a5da044e9ba21947cb3d86eacbfe3905696483917e794d5cf" }, "openapi": "3.1.0", "paths": { diff --git a/resources/config-schema/profile-authoring.dcdl b/resources/config-schema/profile-authoring.dcdl new file mode 100644 index 000000000..62317038e --- /dev/null +++ b/resources/config-schema/profile-authoring.dcdl @@ -0,0 +1,72 @@ +# Completion-only shape of partial Profile values. This is never materialized or +# used as defaults: omitted fields remain omitted until Manifest resolution. +let + Flag = { enabled = Bool; }; + Model = { + ref = String; scheme = String; base_url = String; model_id = String; + auth = { kind = String; ref = String; file = String; }; + capability = Unknown; context_window = Int; max_context_window = Int; + }; + Extraction = { + enabled = Bool; model = Model; reasoning = String; + threshold = Int; worker_max_turns = Int; + }; + Scope = { intent = String; deny_write = [...String]; symlink_policy = String; }; + Profile = { + slug = String; + description = String; + worker = { mode = String; }; + model = Model; + engine = { + instruction = String; language = String; reasoning = Unknown; + max_tokens = Int; max_turns = Int; temperature = Unknown; top_p = Unknown; + top_k = Int; stop_sequences = [...String]; tool_call_dispatch = String; + tool_output = { default_max_bytes = Int; per_tool = { ...Int }; }; + file_upload = { max_bytes = Int; }; + }; + scope = Scope; + delegation_scope = Scope; + session = { + record_event_trace = Bool; + }; + permissions = { default_action = String; rule = [...{ tool = String; pattern = String; action = String; }]; }; + feature = { + task = Flag; web = Flag; image = Flag; sub_worker = Flag; flow = Flag; + worker = { enabled = Bool; direct_spawn = Bool; }; + workspace_worker_discovery = Flag; objective = Flag; manage_workdir = Flag; + workdir_catalog = Flag; workspace_config = Flag; orchestration = Flag; + ticket = { enabled = Bool; authoring = Bool; thread = Bool; intake = Bool; workflow = Bool; }; + merge_request = { show = Bool; open = Bool; review = Bool; readiness_check = Bool; complete = Bool; }; + memory = { + enabled = Bool; staging_tools = Bool; + resident = { inject_summary = Bool; }; + extraction = Extraction; + consolidation = { request_enabled = Bool; }; + }; + subjektiv = { enabled = Bool; consolidation_tools = Bool; extraction = Extraction; }; + }; + mcp = { stdio_server = [...{ + name = String; command = String; args = [...String]; + cwd = { kind = String; path = String; }; + env = { inherit = [...String]; set = { ...Unknown }; }; + }]; }; + compaction = { + kind = String; threshold = Unknown; request_threshold = Unknown; + prune_enabled = Bool; worker_context_max_tokens = Unknown; + worker_max_turns = Int; model = Model; + }; + web = { + enabled = Bool; allow_private_addresses = Bool; + search = { enabled = Bool; provider = String; api_key_secret = String; timeout_secs = Int; base_url = String; country = String; search_lang = String; ui_lang = String; safesearch = String; }; + fetch = { enabled = Bool; timeout_secs = Int; redirect_limit = Int; max_response_bytes = Int; max_output_bytes = Int; allow_private_addresses = Bool; }; + }; + skills = { directories = [...String]; }; + }; +in { + profile = { + default_profile = String; + entries = [...{ + selector = String; profile = Profile; label = String; description = String; + }]; + }; +} diff --git a/resources/config-schema/profile.dcdl b/resources/config-schema/profile.dcdl new file mode 100644 index 000000000..aff4ccadc --- /dev/null +++ b/resources/config-schema/profile.dcdl @@ -0,0 +1,11 @@ +{ + profile = { + default_profile = String default "builtin:companion"; + entries = [...{ + selector = String; + profile = { ...Unknown }; + label = String default ""; + description = String default ""; + }] default []; + }; +} diff --git a/web/workspace/deno.json b/web/workspace/deno.json index 9479ef44f..e17a7fb8b 100644 --- a/web/workspace/deno.json +++ b/web/workspace/deno.json @@ -6,7 +6,7 @@ "dev": "deno run -A npm:vite@8.3.2 dev", "dev:backend": "cd ../.. && cargo run -p yoi-workspace-server --bin yoi-server -- serve --listen 127.0.0.1:8787", "check": "deno run -A npm:@sveltejs/kit@3.0.0 sync && deno run -A npm:svelte-check@4.7.6 --tsconfig ./tsconfig.json", - "test": "deno test --allow-read=src,test,tests,../../docs --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/repository-api.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts tests/tooltip-contract.test.ts tests/bevel-contract.test.ts tests/bevel-line-contract.test.ts tests/typography-contract.test.ts src/lib/workspace/auth/model.test.ts src/lib/workspace/protocol-validator.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/api/ticket-browser.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/composer-invocation.test.ts src/lib/workspace/console/file-completions.test.ts test/console/ansi.test.ts src/lib/workspace/console/history.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/turn-navigation.test.ts src/lib/workspace/console/run-status.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts tests/workdir-permissions.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/spa-shell.test.ts test/objective-pages.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/route-ownership.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-workdir-meta.test.ts src/lib/workspace/sidebar/worker-launch.test.ts tests/runtime-workers.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/config-source/api.test.ts tests/memory-api.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/model.test.ts test/repository-access/structure.test.ts test/repository-ssh-connection-ui.test.ts", + "test": "deno test --allow-read=src,test,tests,../../docs,../../resources/config-schema --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/repository-api.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts tests/tooltip-contract.test.ts tests/bevel-contract.test.ts tests/bevel-line-contract.test.ts tests/typography-contract.test.ts src/lib/workspace/auth/model.test.ts src/lib/workspace/protocol-validator.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/api/ticket-browser.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/composer-invocation.test.ts src/lib/workspace/console/file-completions.test.ts test/console/ansi.test.ts src/lib/workspace/console/history.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/turn-navigation.test.ts src/lib/workspace/console/run-status.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts tests/workdir-permissions.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/spa-shell.test.ts test/objective-pages.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/route-ownership.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-workdir-meta.test.ts src/lib/workspace/sidebar/worker-launch.test.ts tests/runtime-workers.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/config-source/api.test.ts tests/memory-api.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/model.test.ts test/repository-access/structure.test.ts test/repository-ssh-connection-ui.test.ts", "test:component": "deno run -A npm:vitest@5.0.3 run --config vitest.config.ts", "build": "deno run -A npm:vite@8.3.2 build", "preview": "deno run -A npm:vite@8.3.2 preview", diff --git a/web/workspace/src/lib/generated/companion-api.ts b/web/workspace/src/lib/generated/companion-api.ts index 8a530c53d..f85678a77 100644 --- a/web/workspace/src/lib/generated/companion-api.ts +++ b/web/workspace/src/lib/generated/companion-api.ts @@ -1,6 +1,6 @@ // @generated by yoi-companion-openapi-typescript@1. Do not edit. // Input: openapi/server-api.json -// Input contract: 0.1.0 (sha256:fd06f382c6d33a5c3cc98ef9e264545923c53fa95a9be555a85680f98c6065da) +// Input contract: 0.1.0 (sha256:a6a7147c9a2f465a5da044e9ba21947cb3d86eacbfe3905696483917e794d5cf) // Options: companion-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output // Output: web/workspace/src/lib/generated/companion-api.ts // Regenerate/check: cargo run -q -p server-api --example generate_companion_api_types -- --check diff --git a/web/workspace/src/lib/generated/legacy-server-api.ts b/web/workspace/src/lib/generated/legacy-server-api.ts index 7a34b2e60..aeb99cdaf 100644 --- a/web/workspace/src/lib/generated/legacy-server-api.ts +++ b/web/workspace/src/lib/generated/legacy-server-api.ts @@ -237,6 +237,10 @@ export type ConfigSchemaContribution = { version: string; source: string; projection_validator?: ConfigProjectionValidator; + /** + * Completion-only shape for partial values; never materialized as defaults. + */ + authoring_source?: string; source_digest: string; }; diff --git a/web/workspace/src/lib/generated/repository-api.ts b/web/workspace/src/lib/generated/repository-api.ts index 5b72d2098..2ac059cfb 100644 --- a/web/workspace/src/lib/generated/repository-api.ts +++ b/web/workspace/src/lib/generated/repository-api.ts @@ -1,6 +1,6 @@ // @generated by yoi-repository-openapi-typescript@1. Do not edit. // Input: openapi/server-api.json -// Input contract: 0.1.0 (sha256:fd06f382c6d33a5c3cc98ef9e264545923c53fa95a9be555a85680f98c6065da) +// Input contract: 0.1.0 (sha256:a6a7147c9a2f465a5da044e9ba21947cb3d86eacbfe3905696483917e794d5cf) // Options: repository-operation-schema-closure,openapi-3.1,format:uint64=safe-number,strict-safe-integers,sorted-output // Output: web/workspace/src/lib/generated/repository-api.ts // Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check diff --git a/web/workspace/src/lib/generated/runtime-api.ts b/web/workspace/src/lib/generated/runtime-api.ts index 53eeae412..9197abbc9 100644 --- a/web/workspace/src/lib/generated/runtime-api.ts +++ b/web/workspace/src/lib/generated/runtime-api.ts @@ -1,6 +1,6 @@ // @generated by yoi-runtime-openapi-typescript@1. Do not edit. // Input: openapi/server-api.json -// Input contract: 0.1.0 (sha256:fd06f382c6d33a5c3cc98ef9e264545923c53fa95a9be555a85680f98c6065da) +// Input contract: 0.1.0 (sha256:a6a7147c9a2f465a5da044e9ba21947cb3d86eacbfe3905696483917e794d5cf) // Options: runtime-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output // Output: web/workspace/src/lib/generated/runtime-api.ts // Regenerate/check: cargo run -q -p server-api --example generate_runtime_api_types -- --check diff --git a/web/workspace/src/lib/generated/ticket-api.ts b/web/workspace/src/lib/generated/ticket-api.ts index 657aa7d1b..530b8fbe0 100644 --- a/web/workspace/src/lib/generated/ticket-api.ts +++ b/web/workspace/src/lib/generated/ticket-api.ts @@ -1,6 +1,6 @@ // @generated by yoi-ticket-browser-openapi-typescript@1. Do not edit. // Input: openapi/server-api.json -// Input contract: 0.1.0 (sha256:fd06f382c6d33a5c3cc98ef9e264545923c53fa95a9be555a85680f98c6065da) +// Input contract: 0.1.0 (sha256:a6a7147c9a2f465a5da044e9ba21947cb3d86eacbfe3905696483917e794d5cf) // Options: ticket-objective-merge-request-browser-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output // Output: web/workspace/src/lib/generated/ticket-api.ts // Regenerate/check: cargo run -q -p server-api --example generate_ticket_api_types -- --check diff --git a/web/workspace/src/lib/generated/worker-launch-api.ts b/web/workspace/src/lib/generated/worker-launch-api.ts index f56aba174..2ec93ec9e 100644 --- a/web/workspace/src/lib/generated/worker-launch-api.ts +++ b/web/workspace/src/lib/generated/worker-launch-api.ts @@ -1,6 +1,6 @@ // @generated by yoi-worker-openapi-typescript@1. Do not edit. // Input: openapi/server-api.json -// Input contract: 0.1.0 (sha256:fd06f382c6d33a5c3cc98ef9e264545923c53fa95a9be555a85680f98c6065da) +// Input contract: 0.1.0 (sha256:a6a7147c9a2f465a5da044e9ba21947cb3d86eacbfe3905696483917e794d5cf) // Options: worker-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output // Output: web/workspace/src/lib/generated/worker-launch-api.ts // Regenerate/check: cargo run -q -p server-api --example generate_worker_launch_api_types -- --check diff --git a/web/workspace/src/lib/workspace/config-source/api.ts b/web/workspace/src/lib/workspace/config-source/api.ts index c277ccc5d..2afb76407 100644 --- a/web/workspace/src/lib/workspace/config-source/api.ts +++ b/web/workspace/src/lib/workspace/config-source/api.ts @@ -187,7 +187,7 @@ function parseContribution(value: unknown): ConfigSchemaContribution { const item = record( value, ["provider_id", "namespace", "version", "source", "source_digest"], - ["projection_validator"], + ["projection_validator", "authoring_source"], ); const projection = item.projection_validator === undefined ? undefined @@ -197,6 +197,12 @@ function parseContribution(value: unknown): ConfigSchemaContribution { namespace: boundedString(item.namespace), version: boundedString(item.version), source: boundedString(item.source, MAX_SCHEMA_SOURCE_BYTES), + ...(item.authoring_source === undefined ? {} : { + authoring_source: boundedString( + item.authoring_source, + MAX_SCHEMA_SOURCE_BYTES, + ), + }), ...(projection === undefined ? {} : { projection_validator: projection }), source_digest: boundedString(item.source_digest, MAX_DIGEST_BYTES), }; diff --git a/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm_bg.wasm b/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm_bg.wasm index 4662c4ac37451a58907c03aa11860e19860922fa..8fc0b42457573bdb91793220a34f0aaabd88b0b1 100644 GIT binary patch delta 299942 zcmce9349dA68Cn`9y7ZM*^q<;2(Y^Z33ou`78sCAxfBl+ltV!bh=8bgZa_p-)W86Z zh}@S52nJn1jzqacZV*8ZK~N(ghYt-1%J;9H+1){X-_!5+-X}k|r+Rw&sH(22uCDHE zncZpsg3il^l~VTk_83o;s+#Ojy$&_t&;pKB&8=|7VJsG>1pwDtEXw zO*M{`YLww-io%!!|IrWq?{T}in;A8evKTkICN-<;a4MATKwXR}oH-N+cPI@1yE%7o zCw>&Cm!k}KyA=g3u1UHfOm7vpBh8(b#vC`PPSv5RD!z3$K!YfT+ysYK*5$%?4$XnY zP4%KK4^p(~Mtz(&#E*kHncLAw@i_3ggId6Au3!`Z9O#HcQ5niQ@fml~x7=eq;4hQt z#NmV!uLqwPHmu*^;f~G5 zlGKc*<1~N2C;JR~qL1y(8(MNK@A1KXhCDvtvEh!FwZ#694STfD@LrEU_QbFU`#JtK zAuYAzA}!(Ze#nZJdRI#@OLa2~P0*5i^?IuBBZD7$xZmJjkMJ)>wDioq-t=C*`i&UgZ`j~r{rdMB-ly-Ne!a{B?;A#1{cwpp$@X#oVg33!-uC)! zS&t9zGiXzInbyz5?HJ-v_>TeT#j|eDC4k`@TcI!@ix`I?vv)^MH4h?<;MRXN$T{oueMo z4tREIA9!bYzIK1+Ug*tN3cTag_nhZF8??_*;vnAQGv`9@KKDk?>*@;ML1npiPb zP&uf4=%Ms|E#g^$yiI_XX&-q$&?tAg_8!uW+9J<~cvo|bZ@G85w;=2t@6Gof)}}Zo zJ3sWAb${*M?%uB!D=+yrXzSIZ+H&u1?Q_|?BidJ1?~c2-x{F+sJu7|N@aB-!tIs_% zT*tf%y=UB~JujOBbpGV|T3g^ToROUOs_Wi)NtH-r1-pSe+^%ZrtcaFE%wbymryUDe} z^*#Gh`z-9e<|+2>a;W@?P;A_5G}VtB%%Q^Ud{AbH~&j>bF|Nd&%>y_jA`W*J*Y^`^bCQbIQBH z^^xm5JFBhr{^B|5-RfHEI>$=1kMS;i*1O&Hq3a_1S-b4{-n-59f$IW0t*!C?<~iZr z?ONqJ#ZG9ey)UX;+$Em*>UaE{w$b~m=SS~m*GgA0JEN^c$#1+nTm`P5*}t@P-j~$l zzF*Xn>L~43^;4C)^_}{r`kl7QJ4!w6-Q!yBI?GOJ8@!{{v+gy%ePON0_px@sm#^*7 z4DF(Ok?XAUqVlaWT{+FaR_A%6WBW!u;r>P2iXOk|-pXDC2hDe$P*=Nu(uy$N%j!|} zg6oo6q|SF9R#&R$wawl}NiHu+X*ulk5_3$>3uN8NMO z{hlwJ>(trWZtn-~cir#vLib|N9zMsr)fZl?9reBG{v0TcqWWteA@-8C16+H_GeM5v zl*?bC35HdZuFbFZWDv&vwso&vH)lp7n&^ zRzK0c)XYNTd{?w35Po~S>(qtbd7AL9QKz`CDHGLSwVmFH>IvT{?{?p1Ja6}%@Ga2_ z-LLqjcz?6LejThf-uNuU)AZnf8%}h?@iw~?q%*hzQVBkv}?Jg_a>lF^!b9>7DXL@FN zCi|xNO1x9O3q3QPZ}}!cIj(h0ato{2C8)j6El!;3TjnW1rOUk^fKz|h_DO#I067~x z`<%ae-^MreRN$Q&_7-@jyZ3nCa&6bHcn@kL-36#-xp#xMM4je)%eO~;3yQkHyVq*w zs`m@;balV?9kBlg>Ll04?(M!j?=`F8->i{L_s#Tu&rf-NN2k8@j`e=(T<=`sKE&6! zXZz;(68IJ7EnMAZk8je)Mx4(fy!V$^QeP0lnUukv`6D%aEevqYV3t};fQ ztg>jkLsk{O>LX)F);BHYe)wN)+tns9&1m^ZhOw*48!X@GbwgP#)-z+|4MWQ<@FXPp z0?urHQqlc>K&53Jq{ZY9XaonCiM?Hvn%LWhM*s&{6j3(pZHr|ZJG0BVk0=4|-(j4~?#8|< zXkG0|#&#R4t5>biGr$5Yn-AC7zy^GH0*m$)#C5i`(>7%2HPYD( z+u1VU3b=J;xPO6>ReN&U#R6(znbp8JtAU-hV+~xZ-C3nVy30SyxU0@UHq9ui^JSvs zOlM2osb~9V81r%-Q>m+Z0t#i;Jy{tf;l6-H9EK8>Skw>(}CAJ;v_(ZPG6Pr0sEN^>R#7MfLh77T6wxb>y)u8KXK${bKvbF@C7XIix@i_ zWI!NvZ5iVNwP7{XSg&DY)HtYNF=bBt3 zW*yqB8KLj}W;e63#*fVo+o+rcD(}DfUOv)m)NNiK9q!aTgx?{}FQ@zkt&Qhe*hj1; zDAZaZHz{?zMeCYz9FnAxIE*6Il2QJj&GH3W4XfB&iQD}nE(?li6m6n*D`;1+!YO@8#wN_=>z6qVvjhxmg0B>pCps|HR zLXP};3y*%i!JJ1_fvyhp@596b_4UMdsGO=SXk8t39Bo}z-9{3}<3D0tYkh-7@ifV) zAz#3y@3r1~5>?~)z|eIpfdbdFa^#lUAi{UIbXIpmG!bzNnzd=q_->DJY(za{W!rA* zm-HeDv`uMOPyLPnYPwUqwvY%q8=6Q$EXoUW+qD9AzHiqNs)J@q`}Wy@hql+jo(1iz z*RtxeMz0^^x&Bowp@NUMjQ+y_Hq!uHXv5oKHa{;XBHLkX=}^Fq7|-20L@gu{;Kg*h zcx$MfDGh0w%E5nSeJck)JktndyNug!8<9Z zZi~&a@*P*EZI?Rk|LyyFopR!R&h1nc7@5$C1i?q0IuLtQD^E-Y{;~5yl#_^idUPi4 zd8~6DzB%9dt@_swW^Q~2&%`6Bu`4!=*w}6~c4ucq=klR0RZ?j{B<*ODR`Nl>HuKLY zNbTB(p^YbR@5YJ>w%+~$XOoQQ@4SgkDk!*fCo45=Homz@--pEive(GJyAld7ySqdB zu^D)gqvy|*FJNLULx%sF;lHPQGnyv)GJ12(izf%fwNJk~3-CH8CQzkpt_IzKU^aJ_ zgU+&G{g|{5jJfx$jY|BVj7PddbuKq%bwA*PFhE&1X=P*dz0JWGpWa(7;h38=qbKNw zATVY&&YaLa#i)8;0d&cs`v~9H?i-RB)gLtO3YglMTpkP8uNtrQs2W<}3Apht)5ep; zo8ZeL;w{V&V_%Q&_^uV`NQ$wu=XCa}G5G$fAkgId8&pBZq!5B_bdu;|YSv@)h^&1k zsf-Kvr$UU6e~5m|KTyub#9#!M9+$J)pJuQW>JVB|8FlTQYAk)LaoHC#u&3>^4T(v(Z{Ga#PzFh7CHc6PWvreV?(dr*NG9SWaFJ^3K24{cN1WE zbMGz{iJ4)CWCargtPZOk^ionYn51V(yR&|uA*psED8zgL$o-Xls;e{*onvaB>QhOL z5Wc;@cXD58Y{e9Ed6HsLRoS=iSw3@xamR!6L5ST0QjNriXto3&lEi>{5XZ^iB1WCK zL;)pD+rk=<=Pw6z?x7*|Yy+YOAxB8Al_O%VpixNk&_XI%hyDi(#`mkm{!*#!?!Tdh z+0*luop?BICz9!i2F}M!!q^PA8;^E7@#4ev`HF~9eSnFdO2Ey90aZg%>@BnOxc=>E zNpcuDAbHfrZSu$q)h($_GAp{TQlk)rN0tg|+e z3_lG9lkxTNy2h8ozC?fDe|%(Iq%|5|i_a`5czAf8RJq58*D*4l>==JP=*fGNqINg& zjlQzL*!Sf7Hmf^{tnMUfb*Dp?xdZ8@)jjvArWWxe3md1N$^!9{M!ZLOSTkaAl*KH` z^xs&>xuXj?-N<_8_UIt2#hmd>FqUJid8SIY_>>`W4IbLy@RP1cNFX4O`A0%s1QIbO zH7cJ?3}dO0NP|w4gRVdY0u{_b*MGK~N=qy$Lf(A#UPy!=p3QD7)vKQG%*}EnQRNx3 zAv_eCP2wpao5KSvP*PYBe|Try`dkk-vta&nn^?-+&x6ER+xcwF9b2e};9Ye;xK21^LUyh`5uaIIVbqmzaUwD5 z%4j=`u3U|C5`)OnFWTWVg~m?NzmeGh`7ZFm8lc@8oBUvf9bOy{pj4546M3z-q&E@H|Enk^e(7JbK64$49QWzlqns3@Y?3KGvJNR?LiA( zM9t?H(A*xna4dV%_;q1#RN3QQs`ATs>rusv0^~5LxhMpn%OX;~&n&8uPV*hSvXX~r z%y?RZ>4NpbnngFD#EC`0(&$YBF^}4SlkCO)9jMOb`-DpZmG-<&p+KX{n{Z;eEpU0sPi_Uk7C$d;fMIq3HcEa`a^l z$Tx}h%f=+6@$xeAn=D!e($CqujA8w}Z#g06+Vbicwl7dl*V;mSSp6wz`T=8=cenvh zgz0(#QXJk445dw?lOCtgSTY9zdA$m52R)V-JPvwPS@E-MzPv$$xcNhW0=fYmpay)P zs&Vg!uUoJAM`?J<_^2VgDG6R57HNl!?jPmI?xY#hKT0J^`~jWDhL1uR;*iBAsaW<9Eu+#`wLqJk9uiB?-wZD@U?J1<$Xl#!{!PiYg6`CFA)G z1Qo1aeUQ~&j5nr3%cVQWEcEFgP?j{;ojN1?(5HV$S>%r>S`z?Lzgg2mT}N5u25Gal zCkQlqZ8deHRjOd`+M0}QpRm3xn`@L?Uom4=d#tHyc7e}~!%C^9oS)Sm*jzcw=(@gf z&d$x2K4Z~$!3xwW)643`v6uK>za$}J)%vFFm@#5Y&?r;bs`O?psL+BDX0aF3ir721 zunya7Of7ttzvwd3i`sc#bOj*;brnAU0Ywkl!Tm+A^4B~CeLs=XV&tc(7~SMgTXmkW zF)A4p65uNUThd?J2#f77WZhgl3`sG~4o{q5+_0g#lLVWQw7ZOvyJ2F=Iggw8;{43?^Aj+dy2iDyZXkSOrc&o?A0k1#^iW;=9y|1MbgZKf zCnIC>;VPD3^k6pm4;5@ayn?6B`V0(}t#3mDUm6Trm%huGd7z9j@9V{2`+{!AGDw9F zIz9li`HSNZWt=6SlwQXqj@z_F>yVti{hPEdi?_xMG&fDzd3G2~^Nt+`!6w;Z5bSk3 z{L)tA!*4$Gqd{`CKqusHl6LkRCqmj*l5%_CyTe3yx_m-9ggH&1U5ZJ`6!6C}Q^0mQ zc{g{Vj?zA3*Y4fM)88L2Yfb|QD_zBK(T&#$3906+@>vtag*`WXvCG)HgT%x6Gbz=mAw-4chG3hMjSp908|==( zvu90j&8Jr*Od2(RysgV<4Bp~yX&GHbuVU6Nkay7zqt|EbFpziB4j=x^*z{vnuQcRL zpx6Fcl*)j4FdG5zO@ z(6`1fPeI@MN@}J;n8Rc-CBC93}-GL;mE@47ktDRwX+V&kLBy6E;#>{J{!q8aY@?suLAS)rRTj+7L5_G_% zK@kUPN5)tM%tuNMjYPkKpy0q~v0SkdKFJ!m6ob$g2bG1* zIF`Mde}PD6Y!FFNP<5u|)lg}NG%}g7Fyk9mi*$v-N)qoW44!GRS7Fbv64BYo+Oq`_ z;be8$jkJ8B{_5BlDW+8nID5;-wp9!`yAW_}CanBeTye2?)NRPrOZ&eOGu^DQdX%6P zK*!xIzvcUG8bu)^bt(tHfdLoUk|lg3@l#DXLAWqi?}Q6?42mrGu<6)>5M4D^fnAOa z)>tRbj)*lrcCYWU8yreZ@81^*B(M{l)_8EPkasjYSO0c>Y(XQPEk^)botMs*FYGV| zyWI|Bup900+V!GW61p&6JfFn&uzbr+_OP)u&9esODQRvT44U7C$X#lM$ky+SY|pp(=6CgLV4A#QQP z6k-rUoCv3|a-HP_g?k6$ftx5K?$&wRL>IBUOJ9t21z3=nJvC4&%6^Gt%O|2F5rhRw zgBWDSN4`&C<1n|x@HF-!yDTcEv+^RP46Eb;M!lHHqE|Y`d0F%aw(Zl*t<3n4j+_% zhd5G+EkTE03ot~nh%MNb0CXY1rXm>^l$oCeW#&&o_5m{Ig%}O=ScozsT4iy9Rh1T;sPY%to`BbU z?+KWW2#78ZRnBX&{vKlb%KmfWv6?IcqGfDN))p=8tjWfLr@Pl;MX94PHdBQtXyD>H zf<&#_ES((^ZE7>Dd__@hRu2-FJQgG{UFPc8C7pwZgs)DtP$eXU_AoKRZ}r*jk`Z7|FpLdS$HoZK^0G04 zo6L0^BVfDoI!17lxo%?ww+=>F)Ids|Ee+Uia-$(Gi)1AlvWYjjDzk|ZAF{{ts z5ic}m>w&iBO;~v(_cdXpZ!Bnf6HKE8VsjH#4atQjEIsoO%nT4C5W>DlTSz0`VHgy@ zOB%)^t0_b|qDz?JU)`z{qIslrM7$a-H8G_rjI%Y+m@$QfL9MXEi&qy3e=Rk;hWwJe z3?y73mMt!ZLU+q80I7;i#*cJt#y%s3Q>Hn3epxhc&hD)w{ni<97ANHDr%z)7CW4Z* z7n-PN<@&FPkD9ZaQ1%Qy!7?h6)`In8U^()=j{|E#48s*UPA){@=+EZ)U_B|3#q^fg z!^q>JuqCSqlziQil|^#7C2*K8R$(cKIaH$+BOkUH*@|6YmnV0GXnvzL!&bXk+FHu0 zFI%HrzUXiZWcc{V{#)20MjY$Zi=9!96>qj>RlM)Gg7C#)_zT4CU09Hh@rdJX*DrFfgnGbNIE|Ec@`C@usRt|c7RbO^rg%f0ZF|6t#Wf|;C zj_HjLAqY7*BIiLVHG4eBY6NYyn`NxZiK9taUK0x*WYttEruo6X2OgB{TVhrbdWcn* zL`WxhVY>es!zLs#U9X2EUsviU`FeamHUQ%G*M97ES(A%udb2;(6j|CIlDrZ^FEYts zK_|CK6B3(~SX*L(iDCAGgv6o;Be^ zR@(hJ_H2UHN+1EkMO=8ARS{2wSwLhx&uYfrCFTa4V(>F~_Y=LFg?H4KozI)~b$r20 zKeN)dVG~AQyNJ?x=;SEs@N-DeS4z;$7m&b-OVCZDWb(l%EMrEAS}#gyk(pE-Eupz) z5`0NQ)6Jy(7zs@=leCv*ofBV%5+5bLdl`5d73n*c_2up@Z++_UFNth@6m^Hnf&w<65ywu!oap#;cQc zjT3dJ#Or!_3VS|Qmv<^8UbK~=Z?hMRl`0b;1s zK>Ywlsw=v~|BJY7274@8LE#McDC4hqB6Vi5cHF&B_9OD#99D%;JbNxsJWZmw1Hklo zY(%q_e}OW@6a!_zY9b5!Uu(=j~Y<* zi9(`z%zSoNv}ecXvq)^5(-*Ry90a|xNYb_Jdu)#Ts;owQ@*b<}9w+%kTz-$;z{ZKd zVlcx}ar0u9#l91L7qk1^CgCHWEQWE6;fljc*d({r>QhTu`|x4(K(DNyr;boGK87L} zkO(xVPz(pwZYKZHFumxshHl5%Dd{hB+@7Py zex68L$101#1+23NFW^hRDn2b>RU{AqaIOHFph%Qj!CH5oZ>t&J%s1>X2tCFQqvhZ2 zEurOK?C{xnV#W%rh89G2tYBYpbGQSKr20*_#Bt*prJtt5-3t65i{i8mGadPn4LV*R_8CDT#Lr zbdp3%*R5=Jw6M6mmGz4Pk$#`E7M!MqQ!ag-O&mE8!i{qE^D|=e)Tv`~6a%*SK{^|c z*59VAT?*>Mn#FV_)In?$eB53v++ z`772M3&fibv722Mvr|~cZ=Z@W$IMwKH@QqN)0&uj()2O`w&(;eQyftBX;Q7!ILQ$qY>OJ->@{6miH2a`_m7f(Dc{gdZFRK^*Q4NE7xvvn+7}qy9Mx7 zZKG%SFY(@Kg|X+$4>M$*YURz0RkmNmDw})~E|t?KnAi96*;uB?iRPx26vuKEY>Vk> z(*Klul7;F_wy`?~oo9zH#WvLun~xDMx;p=hvnyrItTK;g}HM?y!9=t?ge7~xA0z!k6iqg zbs{H7tMAz4i!FK!t-`ZA(mVMF_>km> ziEr$E1sLtBn01+zrEOUF|1iT_hGf!+WJaPtU+lRo z_cHGJ6)QA&M?d@(o0x~h_rFR%;DcAt-~uuE3aipl;-(@vB)+wg0rz@MR9-F*e7fDPpsrs_ihO@9TCL zyz-JA2A}@6$@V??^k+N#<0kRWZ>&FY8}^J7Fske4iVPF7%@ysh=1Pm-*)dGIz%_O^ z_~5B)>{fQK6blr2QibG{7Qz8)in^(Qu<&^QTykF7VVYNVnC6uop0`<)V^C$|BTX64 zWaSVYC4KjD!?=-@VxUqL5(z^&Z<_(PzKWeAeQVUX1&1(w>t?Z&b7|Yc1oFZJx_;Es z1hS2q|JA66D|`~V!)OjOq4CyVT^2=9iA8mAl073L_sp# z)Pff=&&G@8PF@nWj1tRYsb?Y=gP`*cIoN^#ajh4|FQt_<1wT0S18YP>AAi_lX8%89W{**ILt794>PC3W0`W}>r+wYTR9>U9 zWE|K54x0d1WJodGrwooG;Gjn;pMz0fOXYhp-o0tO26h&%rSUvu=9Q6%`M3-rCUUY2 zZ>1ouT8@unvHwJeL};as^Hr@74<3(s)V30%b_! zrI5z+Q6e>7imCDDR4_l=Uy;9nf!tMzcW3!xX(djP9ybPffAnu^fIoyuaxEYy$+JOx zu|O;h@>)m^2YF^&sksua(_BE-E^BvO9&#}EGy|y=FoJi7T_FT^IOy@13mB6|SMv}? zvOx3+MRA%T=7x9*?YC3GnNIo!Tl(32u9L7I*@?FlWrLYlMqJL~ZGgoVRU{UlsUopB zrwYFSDP2AD72sLm-eXI^zVaW+*4T4iKTDSKp& z)EYD+iLTA zEeM&;Kvug%1~EtBTZ6Y`^X!Kgp(WdG*NUTFGD+=Ffne3RVco{kw1*huP*g@0Vw|JO+W#H zzRYCXO*})~e-qb`_3%xSCH_<(h<9gi;#GmaG7We;n)1*)+CNjCQK9ix1HSt{vj%cu zTrV`FI<^T7sg7+zgX`i<>Y-q@v!WacQaDW402$<~)1c;F- zHZF&0) zPl``mO?l;13RZyS>N3v_GT8B0t~YCn9ZCO2cS08Ju58(d(}p-3ATn=8=gx@kH$#V= z5ue`7?_(20#pYnfGvfBDy!srd6A^^cGqP({BMYNTJcWt?<%(D`Pkohzcs(p%B*AdNJms_kxV^x8Y7UQKYruH+#n+z6IoN zlNbldnXw zx^QfE9TB&7<(E;ms2gVWy|;6plt^?S(+fn>jX97kstkwKQ)$Itjt&2jJNX^13Al)%j0pDNwZXdQ?nDAczRP6fyCg)6Tv0NFq`17HgiZ$F^mB_S z+wQsxdToOE-Tbib-Fz^hnRm--_TJ6=%4W1^GmqQ@eR)QFau2xhjPQ5ox1;oo9|$ zDTuh5HB+`Y?73*O%>A+>R3_1!4p??z%oD}j`?<~*iSO>`_n@`r5Ad5%eE0*rYjg&* z6^Xs%1hYo*MlW8u)Zsml9ohQ6z3@xE3~y^Me#MT4 zoK~zbocZve2t33qv69LC`15SHc%h%Hen~&x1;u~s#~(qmco0-xiP+Jf4?&R@4`VDP z;)#cOW6kUv0(QmtCwT?re(^Bot1Mbp)P9z$;?@CFyT~0t`H^)4cp8pih<%ShjO-TG za(P8S?Q{8U;oS&c2{14y$4+sRgfc$8c;TnCFFNjBjB+;JQZ@}wdX`=o%^;ruzJZ7d z@qxO#lQWD8xbA_(XZ{j|5aEq0fGux4z<12`e4x_uRpa1Qu5QF(QFn=T5f-7qgQXb9LF zAI={NHrp+Vhe|A~!}!z495oDvT8UUYjEC{==EwPTK!+da!&77QLX>+c-Qui=hD*+x zJzSE0_i&J=MBMNMZx=QhjBJ>z=mJnbbAFT-o6L+eNF;G+$KR@u;#AUzR6*zcD2S_i=euwgV$$fa52csmKii9WmE$H3t zPxAYby#FMBIF2zJp;w?=mIW%UiZ$~@v!^f<<(HXa*;72(w`-Br&bar)wx=LD?X_1G zN1o)+R3mt0(jh@_6jg^ z&_#Z*>>#{oWWR9T-37i93P}*?A@nrvjuE#$0~>9H82C(dW;YglpW#7UgP3`8I#oqq z?tYf{MiY@|Wmon;%P#@i`5doNBZgjm8OWs9)DK6g1)&ts^YS)8BA15+@rRN0dmeIm zg)p9vw%S8`(M0 zZW6CZ?L9n+_suwmWn?vFL@))%+k%|x?rPSI?`1vLCh>Chp)X; zsf+QV_n%*gPLr|LULg8U=FQnzF>f-KBtMBhf;TSxlY*524r&B&7Rr%}lP5ok#R3xX zr^!=L_M~Vrh2IgrOdecpb1@i~0lJm~mj=kIC%(FoCxqQERCZts4EO0}!O0a&;)s*R z#s>cHP(CCNe>rf=5rY>@k#9O-HHL#ju~|sL$+#lJ4cQOAq>@PBomcg-Fd>`)7e&$t zE`p1+^n2iw8pq%&gvEe2c(Fn{sU0$;a4cOm8E%N9f$)!_WIlrhigYXkyAzH=teWBo zrlRU8bnzM~#LI5;Bo#fzdJ_klQ9$nAWcWXYMp6T*4Hd-3s&wTXe)I7Q zV-SdQ`nSn_fU7*nsIg1jZ1f@zxc;71iBJ?n#5?*w z4)_Z=#ICO}B^~1CcX_?=tDM4PRd^JM%>I6OY=*|Mia_``l13q)e-B-3g2M)&fzSUf zbsM9g=y3lrix43nWJ_M&2s+KK>dSD(E&=I8CAS)PG*o~@eI@crA)RK+gW*~xgeW9^ zt}Sy53MPuvi+P`ZV4rGqkSdK*P#um^ihd0wfVaSVfYtyi6@xYa^2VTTfas8miQ6&_ zDK=IOq&4KYsU^l_0iRgDga;~PXp+_Zg_4iaSR#!cEC%FcoYS<$T00BV|dJ~~9nhfq@~BKad8>VnFuDPtL-A^|fjkZ6rP0nWXEKL%vv z21>{9ka8U#(&RGx=J4+nqdwwKwE;ci3dYcfPARZNY*&7UeF5#Bf~kq^pC zYI|h9iC+^AsLnR?~<_6sXZyojqN3cY~AV|S* zFu_y`Qk>(wB8EdD0Krm7P&^qMHVFgCRH3VI5&}uJv3N&abUq7pX@O)g5MonHA;C1p zLx=LXzzRF~cpMl=7Ujl!GBbgHA8|YUEdzl;FKXk%!MOP}dwyNRD`v#B0_MPVS|MjG zFV|FR1!x%zN}nb*5^`)>VQgH0j>RN_p%1-~L_JFqFRkT)Fodj>HIl2NjAhE7Wiro{ zNTD<{Z=uW^CxuU_jF~r6=8ZP<(#Zi<36g%va)DpMTb@TbSmAki-i~NqC8q_~-s@lINgc<#_=tv7Dw)0uiAwDM==XU)S@hi1S8O ziRfQs)Mhr zu(bpX+k^WTAONhz9MjJLs3uquuPD|Pa;;&s4)RGO$o0>aV0A^6{BEzL_D=Q-hDmBxu?Q`w`PA&7*9xNQLoSjgv9+(h9_wyn6`|+0bKZK z;+VWLQMuzD7#*+18A9J zK6^V11Msz@EqXNZb6yLUAz9r1FENO0-V(LQk=>%pCgC&103_yMvf&hrH1w`RHZJOJ zN2HKwMIniKU1y8oVe!2cByv69J59cq*T?BTpQ#!Kn6_JQ5*g;;!=uQ5m<>Z# zgSktx69R0ZG#zkAR|iJRH*sVFZ>bG9ZYn236LG3ED!&QQjDv|Gjk}mJp+Q?fYhh@L z$-i`Aj$|a*#nRXWy2g@;kPPk_O_;|EX*erlluLv&0+%?-EB=;GGHk}$d=3DN2mOqD|3&_)IwHV(*u^%1RYthE3w>4*lTz;iJ? z1$$4lgrNn57y+}~buKdy+S@}lCqFgzr;W4V!MW}4Ijx6?9* zWB{%mNAQ@uDqN~6EX$&t9>*8-H)JFceg*6ar<}7+l6g`;L!S~JXq{lO5_yRw^0Gzv zOP>CyB-3AE$ra;XtP!a;aIeX@6(!?RJ#JEh@)gWrnAy2KD z@Lm>29uFPl)$jbHc7T?Fc7S1JSsppGJ?(^H0Bu5+2liJXcD$z80j*;Ct9uB|OWETU zl@4Q}i};|gcxr3d?vl^(RTKfX}fC4{cg?Gu9CF&f8 zOZe(nyrNf%oqW`ARAe3E-I}A3gn)zAg;;0P0s;GS0Vq1t;es_e7@H{89+3{vOZ3?B z&kOVIhj{&p#EbO4S-fb=KNoFynAZ1rxyPUs@)(XaTOG+hB)~oljCt z8ee4l1F+05il&8?kI6AGiHO&^pi* zX?KK|j~$&FaFpxJJ(tckEs4DUHLuCB1#tK{ws6=ak#s`37@M5nZOOxGCR@xT>7>lO z+f3d$$>H;jd~*^z?nv8xXS%*eeh>F{iD1QiGLk99*lsI{r2fE%G58~2K80_v6?}@r zOCKqJn)jejpE^TT6p6z>!uN*ru4kp6eBfE$s@f!6DT~{b{4yb{;#aC1TKd{sEv$7x*1WhFyRYb&{BM0fyEjQFsCS(k0^i3)qVm->R>W2qtyttiurc$>bmak<|){LzBZ=l~(%mmv|>Smaa2$5aCX9s4ZGn zK)L#H*$|wA@^PeBoV~=yN6*8(bea1IZR3CC)z}8H4M+3D$zP#ai-h|Mf31SlgK0QD zSPF~2*by^W7KJ$17+HFS_hmJUc{#}VxwP{0o6lQl{X);C?mUD281UP`B7bqD*>8NS zt32-3Nx?TO+@vt@*J{{RFSM!~H~AKeprTY|#gPt*^0W`>r=^q}7M_L}etDuY8DY~K zK9Am`M?uv4q7d33_$0xyEbUSv+&}s)e3}%|yy3IW4!xm%k&3-c%sy@_G8*Wkw?#9y zY(Yk*ei8X*i7A69r-FVmmUCvaEV3__LA|%NrGJW6^>Sw8vK8!$Tvvc5p#6jFD704` z^(&8JuRGEyS((GI#dx8#Qm;JrAZfFOQ=AKa_sQS6w3}EUnx`lO|{B`Jztej_f*ujOcjzb%pI#}>h5mst-kZn-SFr7F!(JU>;LB`YWut)O3;awo+r z)TyMj6-!omJwh#`+=0&2t)ftr!y9Fk`|a;F(JWn|eO&8(QM&m)kfHpDZSYcMl|1&5 zc(bhXe7sD{aqGP5)0Ss2Pn=_R>KEWxbWhpuK00}HG z{tzHBse)1;JN{cMD1GCf_pYcs^T(ao;)+TeYb1q~UZ|uDr^^!_hVe_W$4diBDEiH^ zFXZlRqjZf;DI-hnW{ij3y_zy`cM zENk8B-9&4{xzr6xH%Ve=l*E&6P>N8|Jyn(Z(N@3{g^|fsmEJ17F8^dHd8t?_h=Gz>qODbO5bP;)taMeQ|LsxHCOPrk3JDwTPX7&D~GfM zhOpV(QlTTn=UXd5(Lz@|{Mcks?iQu4cv@F7@M40lTw`B|39Xd3kko9g%mRz;Kxy-% zY`#~Pr#c?GMHycD3pchpX@aG8i{lKyiKObdvXdfX{fNMjSB)3K&cXdAKekn>@s+aiTB1!mOx7aN zx1Dm>ie-Zf*}P@S613ggE5}*&_)%f32O!NAeYB(t=?BVzC(k-gEFQR3sZ&2Dug$l* zVmoXs%j?{h!{u@7NYSmztDJ7bcO|C74yXu&Akw{)qC&xoK3$Z%(9uX2rESG&v9n<= z7#;fefGx|_rGGt5lU36S+`a8Dr_)AqHin=-}hA4mV>p{hmQl-8^wYOtI}vPVnpHO_xW`0i4MvERiD zcPV{ZO0@=cV|J$u&ehrrBaU&D9Y*(lwGZ#7X`<5IO7ELd#ys}#Lc99KlV7~&ZkMR) zxQ8VnnQk81h5{5W=1IivFft*&UHWdy#FevR&D~0{Zmfh8oTB1%4@w-QO!E&f>0V^Y zD*(TyOx!^$O5LM0N&`ldq5QRgdtea9hs)yAsd|=U_&co`|3Vd*?Bj z^>#QC2b@8=uopQ9o>uL^|f-a*`18cF12aKr7@}lJ6|w7(;y*2e3igRXPx93s+h` z9eXI1+L=5?#xDAH=ns9{XYbp#7$CQS|4`WnJ(NcwxB@*DGBW%1ROm9JH+w4QDc1OY zrDJJCaUyW50q>fSjVqxMadott^5my!++`>>ZXJC&lc6!K5Wze2!pX&z8I zu(wi@YMDhIS|-QLq9^-cy0C4LEq#?&c-S;9#=_{Z><`oYSQ*Q(ER>V649kYu5VJ=t zE9WYV9kY&wWRR4P8zi_SQ@SQ8C>3j_#qvUbOo`zkS7cW|Wguh4qS?brStNHqtTcut zmjAHQ&f}sA_lm=VmDcicT_o!7(W6w z^&zn>Pxc~b6tFR9q@vMrEX4;grL!P)xtgK|Wf#QjBbC|c7)sXaX?Bdx?8H0PCb1NH z^55xMWwGZO7>kEQw`Y~r|39!X`MJNu2AUrFyix`o`Ol+I{O^n&J*h0dd_j)Bd-(s) z=->T&snhINA{J#xfE~o$LRG{m} z{|i$AJ*zByW90D1jZvDpNmqXNyVx;C$?lm9*>=n!BZeS35dlfJ0A%EbGW}b~`PD2p zn2Hj+6gYmeq(r7jN;v;P8j5{#hTyM<1CpK{A?eSeUhh=k2|Om)VQ0FS`3g%5dvuPd zHfV^n4lp;MDp2Scc7IOF>xOW5A3P0|2L+O0CVstCu%ced6j(kOK1?rVYI%I^{~a5F z9&~(AFacjG3K=M<4?90Nx%DDk71%by4F@DuQvHj$7jqDx()q9;>8=abT2A z%|Nw=;1q}H8|x&wqyuhQEjgZPXo)C^pdUQao{3`m%8;Ivc{plHM>!GeBYFLdR)$0)YYEF#3^&eo!TZzkp&M z_}-O(gY+XzeClJ)6@u#eB|6D?I@B&FhjU=EZ(V{_; z+##m3-DuGyBsFE-q>Mj03KTf8bixSH2JVfO+mxYARF-&oDklrur@M3YIT)v9bNcjm;sEZFgsnDq zCf+Jxxkrx+wvm%3bF7R)WL%m-MXZeTQD7ySYK2VvIX85>i6er|4fL%P(C7x4v73W4 zFM~o!Q8LTzLpMccNiusuW`r2hwFwCMX`<+<1FbC=I+F?n){Mi%c6y^23}AyJ28o>4 zlyYH{)D%yWg$u>43)6AmThJTr9n$!C|L$;O1GEpjwBF)2>{|s&gYc3a1HN#eG&%c- zuB8bT#4dQ$-A{&;B^HhlR2IPG@ZTigv=|z~-dP0eUD|iIs=*h92>$a7#el8) z1GdMcmo7zltv7XzK0Ls4TRFn!^D%lhAB*FU*ffadA_gP+n5RS^DF*`9RZS`6!5agv zT83M6zvOvaC_$APeWsVEeX|c)pShY##Y*xaOkE${Z`7D(a6gCU3sxp z_L$Mg$jiIxd9hW-82OlYDe1*l>X^}^@dX=05(F7|P8w^Lqx?#E#^-^XS~VIwdh}@O z4Q+{0KoGIO1e45{DdPO=P#9%RsEV1$ znJ8P7ZAtbc+iRAz{Cf;jgYqxqOJk2kJ7Lra*#lz)-<7P zaJi}PF+rtK(SSdPPx7$4JOaFUNN$;97Yafcwc-sXXb4+sTR5%Dbg%rgE62i+#a}u@ zo`J-!*?dF@ajOVaVWEV9i13AOBCSsi;T(m05mhzbwj^TcOck=9qnj&D&E<)U22>Z6 zssx;d=^#4-y+}5J><#Jt@b#5LJIm@YiEGjvhSHE5r8ER$Rs3R(OT56xGg6_UkU$s< z#nq1LC5nz!8Y;)=15z|=RVXx)b~p$DsgaF+I{c_1*$pKSMjxVGpnU4EG)Q2RVH+Pt zTfk}d1(uy3;f`e3A<&p=Qt@Z#sRZI0Ww$~UJ!fmeBds5(nd@619|dJXl2r+QoMxNHK)%FWs{VzndK z=AU8ry58HWXHE+W9n_dGf-DZ z&k!3)mY~^5#2^QP%#^$pXRWiRmY;Cr&nW(xClH|`=eGR%FL__GQver))BH-rL)M^N z43qBZ4kp4#`UBHj^XhMLDsUM_xasw?=ATsXM}9S}TszCU(NTGFmI!YyPs~|sF>d&u z%&ZvgGr+kv+PhG7DlKC=Lm#tY88$KM54eBF!d=E z{J+tsQk#yT7$PjdN&8crDP1@J54vM%*04#HxXPva;7~v^0xL6^LU|+b2Uw^hrdiT} zX90lb3?P9`kx4JILmmhxJk$g;WtnFX0JV%}-vTZte=uooI8?m;ucWn;)5aObXXfe< z>3?Va1>R54TabKCpiYt91y!67U4JFS*Ix;`JN#tGZC`-VpQfS(7-|?A`ajH_3!GI| z+5gWzd+(VuR}Rb!3^2eshud%y6cxn_Y|8ruy8D(HmA=_vCT3>6<^e?|Lq$UuDh3t? zDk>;7D5!(tr6R>dLq)}+BqK$owAB26e{1b?<_rvozWzV2@^SWAYp=a7&wAE#ThDq{ znN`57pLDUBq5@V{P0h#(F-ohJjlr-$QVN*V zGAUr~YMB%;sb#1cC2ARphE>a?fVHb-Qoy8^p>33?Wm3STmZ5R9t7RoR2ebA?tAGuZ z+L5Vo;NAm0th%EDR_2oB?)y&luz~_snXzsC|5w0b{p;X8R=|Y9Nce&T3ssAGmFarB zKcHeEYgNe=F1a91E_ca!adN3ka;K9myu>BB{L&^DyCgn~Ho4Fxacs271vZ(VNitf? zYPB7aGG;A`@&$%V$j@Hp(PSWZ*!oO2Id#{|J2A}zhuLcP`q(j8Wfk9(YVa3|zoh@o zI`LrJ8xE>Eze^6bfOZ&O!e~^!SXQkxk8y^zt{laZE^`ZpG0enXQSGBLsePD~+tt3Z z&egvClhQJ^Hm#az>r$kZqWbg*Cs2;aYeAP_r-K5F%-Td5z%&oPF`nnNYjEr^7Z{~2 zkabMpToC~)$k4%d1r8OL9$*Yj|2QeLs#D7>BZ1<>iepY9rLj4ALQejcy4@<3t5lN^ z?)K)Con;%3r_6MY8@nP_oT7qrvz4~H1>f_-YvKXNLqV~zN^Xc;1?SD%!NiJg#_^y5 zPTd98^wkn#D0wNxqeZ9N4#HE=FAdn zA9T-m+{WtSF}EpL#4^@~XIDGLu=aJzY^VTcli!)!_?>eb5mva`2j?~-wh-Bvt?a;T z@^Q8vJ8>H~ILlu4z}&YhZnIlvlizc5Uzvq^=jOi3f^o@m8=2U|ZOV(eVm2}uh}m>0 z=E7|9xkj?ZZR(2I;x_%r6}QP0bH#14 zW&zO68<+ZB3wFE8jvOX7@m0Gi+5<*a;5TI%+uR=Wn>2JFMIq5D zRC#1+&I>@=*jbAmTG(0>Iag-;wh{9ATVh^tuAO&{s6a0Y+eT1dw`~MR+ugPi+LM+4 zVTJFgt3!na>;l=YF21GERUI-)|IO!614@%P2m7C~4zSLbI+e20 zMoTPE{i=f}W!BO@Z9P%pY&?kIsj{-Fjll z`3|%tYD7BAQEzvGTaaLb+HHNI-*Zi#-Sx#yRXTC^Xe)9|B^UB&1DYe-Xc)qX?h)~3 zV{xxK(|t(tG=YkHLUk}4jofWDR}s1UH~@MoPs}DK^B^@@U6_7xAP@dN@P*pG#wK2 zeCPQ<*QU^_L_53vnytppNFSyMx2-)yza$a@c$?Wpc9eSkB&deG3uJl`F2a<0AUG)0B4`PB_&#CaFjipd#}~XJiMg1*hD$TCIx z74?1{VWG>JC99=d&1x~-Z}Jrb52!R%TjOGskX%t~zI>BEq%b+c(-^`2zfaDX1At{P zBC!QpD*nOZ7~*eX2fmHpwim)4i*g*d6p6K%i%r$sa-TirMLLq zYb%{`(RL4*uD`|qX5DBF;ytI;x)o4qCN1&%<%?bxA4=`o6Ris|3@3|@Ha-T6G6QWG zZ?&U>f)!}rpQGEXZ1f<+tr%3&j^t@2?Wn4lD}AVo+3Qxn&xodCru4PG6kWlyU=9R= zq;!04DbIf9irf6UiIpU!uVth}WKse){Kq}IxkrKV2~MYns4*Z^(tF$yp4liHR22K9Lq@R&B&#G)4=IE ztb*Xaf3nSs+7=)+_n{gVfRmaxk(0j3oUxSiRnM90m-_ui>Wbh@+6kBS=|$G_5PIcQ zhQS@A#ld8nzb*B1$6tO!i3EtmH?uU2z&EWljllO7?G`1*ZS9HfENHO*OUwL>`TjD0 zq_`d7Kjbpb^hN8gH!GI;d+&YY&lyNPnFQmAR#A7fiX5k@D0U0pITf-tWfW{7*PVlR zz3%Y8JP6x@t%vO4R+J(I6z!$z2v!wClCy&B{^t5S{IPv1Az|Wt6}o+dzZ?95gjWIB zx8LE9@%A^v@AMn2Gw%cU7dx%u^Szdx=0eK|*|Doy1t{pK&~Q$=rxQb@DN9-2FJFzA~c4MBnp&PNRK zN6H0!Tr2>ZujAe260ODP(Tqib$dFf>Pn)zh>z97dJX{O{Dz>6+Ly($`xgO`gc`hW5 z13}z8uEkO)C3XHJyFU|U5PEp(OsIB$*W z%(u$O)%k91ThdwEGvd4?-JH5zMJl69>LFA%Jri@L>x6P3**1qJV^gvY-teMeWY!X= zZ-NRgLcj8R3{nNKgo8En0ba(gMn&qd$JNj!wJy!0zw&!Gm-s@0tx^h0{Us~Vf|$Z+ zCKm5j&J1pw-#V(*KR%gUH{VRV3nsJ9+cD@MMP1w)=Y(Ux(!f3Cde&CFSiR`# z_C!Ur$}SBS8Y`^vZvU6~@IQLDKj@Qa1VqELx)u7Wqy>pv4kcL5MA^~Gm3AwIhGWwd zGQVIMr=vxB9F)E&G&kJs_cWhc>5n+67@#bAXDM&T@HtkYXsZ&-m&FSoL8FT$QdVlO z708-^FUuJ5zR8Ic*?MG@T7-8nv^HAh%L%jAY+UJQj;W+9VFhf|uNu%7xke|Ckkn-; zXl~&P#0-5tEPES= zpvuv|JE)>aYoWbMO4AFAiD;PS-5h?8pPTq1xv?gZltTYZv6OXtkA7d8W@Z1TG))ut zm!|K&(X`&>yrhl1J@AVJpUYt`VUp|FVy*;S;t9$)Bdak>kK{r9|^$;30 z>(B7?Vl(|de?Igk+veBl78BV8lHv;E+BjBFY0hZlu=FDH^ERJL-ppF}_=kP`q|4tL z%;?pA9WRbp?GNp7qvIh-Q6|emwu5=_!kMf6L(AJhQ7(rt+aB?!o8#~I>jztKgD^>m zBigkWYu5#=R!=|0pfPIX!58IO;K45!t#RKg&QpD&pxmx*zalMoI;TnHzFJ$nkq;deCnkv{t~{FPgrPM_u|- z$(1@0@1_52W!BGTJ$hMb?tjo9cnT&4q;b(L7?o9cxM~muMt!A!PHU^yeo^IN5?2mC z(>l9BhS@s;Wz`l|mRJtecL;mp7_LoeIq5%}V;{nWGE`H%*r_S2c9b?%t){}xn~FE+ z>8*BjTvM*bOyyC)c$eM(gkHw|hMn!c$51&)3B^B)S}!e1(cU25`#tOrLpk~C!yGAD z`*U-_!~VG39KksgxrZ=uy_XxJ=LRpmIJ8Kx`C|P^=KNoCNb*$^{l@Q6Ze<8wedRa)uXxq{xBhqXeIXxn5mSbzUsPB}Vt!`(3D26c z8Sr5n$0NEF@PVnVWuzHMCUQzkb_7H;gELCZ_;}SNlac$p;;IBl0p)zrcLXYAD9dGE zV3{N4enR314m^4UL{iiXR9{Yb5S>?6pHwu9UO;X~dRKCsUG}I-LM|e7(4a!p0Kue% zoM^SEGh&rUw$(wCqSXis7%^neahjoZmw|U$jZ`W_h8Vw?XL2d@1h5lEd)ZnPnOejI z4$(}(UPoj$N2}-h90v9zEO3$*MlOEXMeI549(W~b&>*##fo`-*zlcSw17P}Px(rDk zDGN(K-cyLf?oP18X$B|(#?K<)4h1;;JW<9+`|AXIn!}R`DkFKq9ZvzYmFBjygMMZm z#FDt$LABW$1;fm$-}&7je~iCYI zwI>3kL&Kn=IZQyU$$t;qfsIk=N!B1|$@hYu<`cj7vv>{K90_5|rzxBE5raS}_@f8J3cu~m#i5M>k!zpU2AgS2D2Jm2zEDp)DO+4@mXat!s$_?W& zT#`d=G|X`h(b+P@YMI(fOf?C2j6e&e#*T%$@}h@AH%phSsUHHBOrIr6&Q}BSf<1DiZdy4xmnL4Hzt_Il>J$UXR*a3BR^quJXBMsi z8EaW}uWg5D&`f>YA2M2V2szdqsaE{a<6kzew}1!Kz`S&v&19S3J?{6^u(AFMO6q(8 zt7SsqLW8NfYO~kpYZ?bt@>#CRBD3sQ0K~?#Wr+!0QVt@OX8xI#nS7y`FPwL9V{V4{ zjat~a{b~AbP&dVxw9{|*fjC5MH!W32t6xx(sev$7{NAKZ2@nDOsus~+mQtUzY^M7X z$wDz=N9oV-Y+gsHSPKNcog(D`>m0J06d}8Cupe8mxxi{$6xF;7ZQzay8Y{D>@WO(T zeSI59lra`n(R#I6t+Cim!&>Ljs*jM>y-c!T;jed}@B-YA?|({GMQhi=tg_KI9`5M- z#^?E^c*1}ja@f?~QWr&d?Sd^jp0h3@k4@~lktzO>%yg4N(1lB)>3xzP%`NFwc^jSf z0H{l`%R2N&)d#a4#N4Qd1?3LrO{?Kx-Xxncreujk&AewD|q0)<_CZa|M3WSD15)Q;e2sLQS zD4LxsP1tUgW=D0x^n9U-s%Y^Wt8JxK5&o75Xn@w9TMl-)U>aa`s55p6m8Ty<5|w?z zs?^nkIDyZx9GnF?H7MKAMugF-^dh9S5sC2xbz(^6={CNRyq=B6owoyjzhU=s!*2Vr zW>dpnY=7-y*p$|=U1<&5l`b82an#3uB*RAZJ(*$sev5hf56J%I=Dli8CO2&GHy>6` zJfw`L7zN|UB+sJIjv(@M?zeL=Ar-3H4O@gNb!Os=ewq2g)BcdXJHU816)?-H;N4De z?fpRB-+_F|8wE_8V}Er5^1(ou(gL|FEs(p?9YLOV$o#*?w80+$(_#pmzlC9#Q(D7zr8R6Fy=n5!hK%wa|P~Gx5?kk@3%JjgZg%0!pWEk$E5Ke?c8NT=LKpr^()B!CX*?V#ZL(!SujJx4 z%vb;9kMtfgSNzEz(0K<6MEtL%9WWE*($D-q+5BGHp`Nf?KeaWoC(C64+yC1qMt)b7EwdX^J zehBSv?b!UNt$z7|bN*47_;+H>r&F}gI<$AC)%-i=jIDm}9k9M5=%9<{cP|sc(GfVK z_QuZmXQ92Vw&h(6H*U|BR(o6j@qrQTn$0_tw{5Q$n%`i5?V|Y@&s}LXzv0zAZ$4h- zOFKf_5}EZc7n)yXf9<0Ac$K@-YJSzrd)|DEYs)(}Z&&%MzZaTcXn*aZ`FQlMw3=V| z_dRbu9{pV%o42FiQbhH2yF>MO^scm;U-!XL{hp4^Th!b7c474E?XO*oK1MxPTFtM2 zd(TH7ue7#Kn%{OoVWDrZzjoieE3M`?T(IX0JqG>*9h(>M58QNVq4`JbuU#}B1K*Wa z^N$#uLv~!~or@K5>kseT`q+6^#nFz&j9A&}l+47fyV7cX#(R4X`&ew7)p6NRdKaoQ zZ)RceGwiQj41R#4a;~&(e&z>;{ck%N{f6S`+xB4euC$tO`v9Ylm;GZMo43n;U94#) zqZRhoE=C_OdskY`ub8&yqmP?k-%0aZ-YKlRb@tcpn|Gzv{JMAceD33Q_jJeRS@x*| zig-V3cX%JK`h~WPTA#J&cpoF)#*U3!#G83Z0r9r|NWXS5_89S8X|=xXM|%$XxcNVJ z()^mDShvLf+I{n`w3=VCb{!an**4SUW7{4_yph?&=>cE=+?D_cP4!qTA2OhDrE(k2$-TcQLaHZ9Or5|Aa zW86RggDOL14k2F_%( zAU1Gz3ip`ey3%TX!N2w#?(wvnj?LR?U$U)$_H*p7U5q}Sc2`=>&)K%;&BwsMwqx@S z_?H)fZ-4Eg`2z5584LW&_Z;|f^9wp@e$AVO(J!&TcHg`!t>%}!+1coK)@kl65B5T* z*>`L({1e)a(gk0MXd1tCr7&BP^~F7$3|ABDMKjxCUF~K+-XuD4K+!01Bv-S%bGP=j z`efqmx#G6gJB8giYQuq`&RmuXMkghtjgQ zxze(?xzeTfwvTv|3ny^9%f+d7cDZo2E$ni&c^$X8AT^sk$|UD@0*CjrIyO10TVo`W^19zo0a96tHz~jju^C5RU zOOSDn*sl3XPw&B%2;G^qQQ(G z6qK1$YJ*`Pyo%dC1X>MpXtnVoNBGO5HTKsoEc%odTDj7Kt}ERUx*yfrvKIY4URy9# zOJRQbA%KAL%3*?NL(!L@ZCAbo+TpdTE}I!eUjkS9;{b#Lfm!h(fPhFNAgqXu`Q_1q z*qFc5IxE@ccBO?0uJp$N2zXCfOjz|HfH2enp)Iy`;$C5Y?P57lTFb$e77$$Nj{^{p zvMdlD{t!Swv==69d(BB>EDZZ=7eKIz0K0s)jDX-ue;j~NkUFm1Ea9s$%$27%g=Sg* zpq^hH$#EF(78J&^JBfJjC)yxM3)EX?6v*#7`)e1VwsJR@IomP< zwJZH`0CfT1u;G1RAjJ4KFWx^bk7mUCr=3!s7~foJ0l}62H~@j|VT%dtKE$FM>@Z<* zY;wb&EjGDz3Is}v^0?9hf-C)T0>Xw54}=-99>^iN-I3oI2(Gk%;7WfSfKZ@3n?3{( zVv%f1(cf&{?)=SSk<690G{KhsH~;~sX1g3-`Vc@E=9a^T*cwzGwZ+z;PL~6vwH#b& z0l}62H~^tC2ErR30tiE5?hqS=%A<9$QK(ZOP+CCPV#`=eu)jVIKq#ccZgZ! zw4%S+me@AbDG*{7=}HR-uJp$V2 z%u{>hj~`FXir5JZtvPlA>o7Ik`X%STR20vRD=wsT#XFsw4{M#WQ#03sGv^yV0WB1S6uVvihuO;wrGpFVOTq9dTvf-o zG4G8I$}%0TUIhpE0;aDl1?2dBgXn$A-@_`~HyGN%Pjp(*RdLJiJXb6yBs7;Ttxeo5 zJ9)0$FeVtdiw*=arMs_V2iU?-9T;zxCZny^g=ZI=$e6q3hCjoWR{L8o*>MkhH|-Y` zng=?zZ#U7G7u^Nt?ap1$3h8*v*fQ$Cyq(+ychiA_9Q{aX2eM-q#&kFt&5G%8r|t_@ zHpV;DmQmZY-raEreK&0vfd6P|+r8W=gxN*Mp|^JDCmTcGl~xDd+R0B=9T?vky4G=o z(494|(~)Q{lrNkGCcb?f)`;V!?7kDoguVU|RoaoaTm5g{ZB82-G@clrcP(=<9d_8* z-yZMiy92jwt1>7=9Eb{Ui~Ie`u%~Ys1^fNwX4BXp*Xymnh#`)mclN+BH?I|T`%Tk9 z0TX4t$=QsLYxC$a}}dfiTxAGy-R^`*2q<6w7Ke&kB4 z`8hN8ym{7$nxFnsN%Og}YwV&_%S-I9U9=nz+?7_#OD@^-mMdJ#7nQc$djj^66V!Cpw&{vHMMhqz8;WmE7 zE+>jb7)(G5K(Umo z)$~bQ{3LuO=kAHQMN|U&Vk=j>sla9%7N#-6-5|xaQH=1Afcwfw%Hv1z2yb1jbND(C zZo@21BV7z>rg(%)O48ewm?NWL_#yjKxkA2WdEx_*<(ii)5egKgqdr^aq5&xW2u=iw zkE2*CwFDs{=#t{JEQ^9GkFv2S7_SY*qS&}zj^FK}SZq`q#h4?UjpxF^Dz;J~Xa%8f z01QDZQs&S*(~W(Ii;~FI+MpG>cAB^aDa9?IJ)$I;)f0jtUak4tgkZ$DfkZ|il0r?i zfEYi68MFek3?mDYIGgXf0po!Rw!*bAin8Km1}oK74=&67$@IZ>dBun2Wg^$p(ZR!W zJ@h)kt)Z4K9zIpEO1cpbo;%B_cT_RxbTS_$GFe4guTyzNIPR64hP=MzO`CQ?UvrWbhBphg<> z!^Ls2c~dgIdgmIV#L*f4962R=&znk8forQW4aBtoYMC_gU3wAhk^nD|De4CB&B3T6A+kxny*EsRI+-J;oI*7a;e{v>fGZ9! zM@Vmhw7tBbM2s>U(;{6G;bn({IlQ_`KkS&!&+yV~R`V>mG^OJ>F)CzUJin$XpM%TW zqC_4o%s_*htrr>!Tn!Ff4eDWvR3CM_LA_5XR>de$KZ4?9^rsPk*Jc_DEqOp_ZsAC205;c zAH8TQwTw28*HqOt?B{5PD#;MoJdw!lo7vCY{<$Fg>Cux|WUP-mhC2EOiz5H*OcMV_ z2*4DGnIgFN0B9}{czK53L|-A~Tp@luK~jF08GS*HYAmYlo7vYKvaqVhu(ZZPz@93) z0pj3g>d_x&(MH1l7O%L$g^rA-kEf5T>0X@zWl~cpPv~KD@KHhkJWJ3{C*G(jXi^rC z)d?IT8T7&avSr*%R)e&ujO{a^$R&pmCde{1mE{Av4&pc@)-NVNaI=AX_(gbWpkvtv zkOni%t6vR5v+mF!{ABtiBIgLo41sW2V1}K)s7p(FSt=n6r*EK6foU?A-NdQ)nj>F0%dg?Y&H`?PO|-#os0S6V zDA-WpB}WH?@{0PeAgrvyc4tN>ehdqU^ykLfXS@I zfH~k@nj-KF0>J`sktA041Ve_VB>U!t8MrPzOLGOK7QW`(JL1CsWQ!erbSb+cs zL2ddB7x&%jd)71?u7@$~H&1F?g1MzTKPg=2F`+zF}i~1#~x%h!S z6?3UP(^K=|RhjJ3AdOpj1gdObd1+=1c-9l^OE+n%_Ary|qyf!T#}>R8M*t2OLl5~} zZqZDYVwj!FgVCJBZ6{;`!falVoVrpGzkRX^GFg!$UR!80VTeq(>wGuKlcT5rW6^LP zq?*AB;EwcXV`r{Qz&?aZpo)Tc5#UX;>Hb-C%vr2V<;r!DVx?FyoVc8+wyjsnO*y`B zLeVYD(=)j)*YfjCF;Wo+4LIrA1j({6C&{uPF0!l!Hi_tI79gNKfR51=DMnP>A!-aE zMTP=GNFwT|;{ICFTdCMrcJ)-+n0@Ld%&B;x#3o&e8Cf>2&|>RsK;sC;DX`I1mbZ(2 z>=*GMN>m|~1CN`F(pXKr?XcAM&SD7ifv{THL zS)HQ|7rA$fO7LICNrg z5V5d-FfquMSuRp(ZkQPK$OHHko%YgqR%qqci7mpi}(vRA_CYRSp9(+zL1#+V0 z*%dO);uNSw4G@A6JkC~!dZ2!GWLdn(2{g&WtLsF(Tsa z9i4Sr3(#DY+Xux!jIXrL+`iNirLz60E|ux8h1Xxb>91>yGCib9{Bu%}>)kc8uVakR z9_6+SPe2Ne*Lcm;in4}0tYKz7rnY2knhK-*cPu+qj4f8z z-i5T9i~|VMeynMU1LcyIqr!A0rId?65D+Z07lB;G5T~P9oC8_ydD#9e6JY>XOw_eR z=McBAC8}VewR~7rQ=oXDG{MEoGei@0uxFagyPpdBm8TdyI)b_8)4>Qc>C?eJJ?sq? z2$=}OPG@4EafvUB-7tqFTUY?_bfT6V?qlxDK}Ys^fmPqE z2{Adh_GY1nF*mOQtIk9NXu^v-9y4 zU3Za5PJwtOr+kMZQA#U?&jAYE24j0@rg~_mgo&&P#Ve#_izdhBQ@Su353D{MnK8vQ zfqyBJ{VXS3NCo>RM1qp`(VR%>B@82x+CRZl`9v-gri;MAcY&H~*@`ZAx zE7d#CLMp`}go1cw>mn4#uIWacSMBmrs+Nj{jEqIdn05vTr0;5m&cl|mvP0T-8(m~S z`g@iM>74Kx$nrJ`|4dRNU>6~}b z1-$G9iP4H_5JKgk`dZ=yg%wiO=W`80`D`7LGAWTcU{Um50%D*!B17m??IzJ?m1|@v zq-t-fAkEU#3@{%e`zo)Ih~7C!g5fMv!BjCsrRZ2ZAkHD0kub=YbgOI&X0zI2-K8z=SeF^Zo7^DqojzrKbMzPBcM>|0{2&EYFdQqY_Hs5kM8aN0QC5sg zyb@UK%pVmM*z!kWB`Qq4)4Ur#bwDAd@F6k?oz~5lD4~V#Dd6*i?WuA!d?} z+u6VR+THZSB9}r;T>mPFK@`$B#v7|)Fc0BTu)IoMh8P|_ABZq0CXh?ZP@JK%T7e$7 zPueKg1qC|8tq=*=qTUh)k)pXOI;=Rje8q%SIZ>}ec|@b+SrTrM<`pq9hd3+E8x*k` zc*fI5Vysr=lalOY38cz=g(OW$X=C{C7~84$^PJINBK8bk`@Yb0zCG2}-Dg z2nERIQJL28&eg(OI@aoStaTy_r>mr{yzB8G5XqW<5sCy2@=o*UN z2}t?iRl?GGJ(A-sXZ6jCQ)`GpXnUTr$OmTuIIL}mre{%)@;)fpJ0n|P5d|#_VJRxz zTQt{#LYJdJ>;(f18luL&Bm@bSc2>0DP81V?1x?BI5+wTQ26s?e-&4Aen}Hp^wgA95 zB#iDMLW($(V^>T$Q@X~3FRS}4A8m+4thbbO=!DBJIt z4XvR|TG)!`t$R+;B&7Hv?GtoXSHi(x)vl}^^(``aw4|T(t)T5xbJi($5N9zurG?tN z{mQ2?xOH<61Inla>Yhaztuye_{>Hv|Y$AVom(TPFQ1YCiPB&XM%^v^Nf~7 zYJki%vJThj(FD!qA@irBLP9z$_Va#*p>n@yJr4|(c^hH6zwLnjq(nW$M5{1#I`(4K z1y-e7o!Z`F0+Nm|2+X;<;zL{+qO zex9E)k}`wLX%=Os zQoDrdxE-7%9SN+dOn!iD905t&(X_MY0wz(es^}e=bMFo4ACiT{c(zG`Uixc;qr1!^ zBbEsTf@Arx1(28^wv&XYkR1SujrxOH=8TY0l&I=OA!bNqPCaU{va@CiuY`D5h+HxZ zVquq!fGMgGso-ouO=h6^@>hes{J}`Lmwh!DkX%%5URzq-mm6B1_-ZheSc?DQ(f58v z%-v-&s<5YrL|hZ$DZqrQbB*TkuLZ}{y?YBW$dkI5q$b*osW-_DAq&4294B8Q^d%&o z*-JW>2y}G`!R0rDA|9)dpIeeLY9&a(ctuut98qKUqRJt1yl zw+aPn6tbLB>E*W81FRUx&l)Br+fB6}%x!u<{Cd!vi+kvhjn$7_Zf^W~kol~(61*1i zH&i)~JNc}&vforo0>2_VSSl(zE0aExe27P(WRj9l+p@Ei??q=wP_nv%GR(MJy`}^0 zTlZPeQY8k?2bsh9Q!xcDeJ0d_c;DVpHOb_7_m6by>|E;V-c|koD}{+KjdC`1MB>4F zjzH|M@g;ci#RzmqiE|}ylT(2eS}JchC61^;+ysTDWFXZ6lh9R+1Ekj;0>gg!Wcqqr z-~d0I@C@Y;s%Rnb6s($&7|OwlqfZI;u5b_?0QhE{5)9;;iG4x$nP&)4dDLUf&3=s_5ne z0MNOcS^JG(?@BcD$U?r-l%E<5?TT6hR6!=#qb91-OgJ_8I;i@KQ-ksOxAEDa1GBYQ zwY^ZOrGp($E0YGDKBP+swNB7qf$AQ7kiNjK3J1 z%IrVJUy#h?q9?RXgVGJEW(v?3070ju zZgsKH^EI(L8DxTuk`qDcGy+zPj##O`G&-!I1naNw1XNoCG3n zXB+?~Rfh#7ueNBmRfDy`CNo1N*$svFfJ#->$VKWJu#VOh=ib_p_-a})hkDvP^dFup zJb)3qK$sP$1=T~V4?7bAj`Dc~!=%J2FIy&zrfc?!gQH&ToXmT^9Sp5=&=DAD zO?%9I^zUy6Cv~|FtS4k>`o=&6rqO)kj9_^FT4i!cV7mRS=V}$=xh;aFFq!)wU@9k)BJ;B@lba-zd8`Vv=Ms4kQCpyI@>Eyi z1ot>HzF6Wa2wx`JWJXO5n)186{Rax7ODlrX8!8TPHAHXzxlOlJ9AME=HDK)FA|b8` z7ftEsmaA8_2cyC>2vpwNQes?x0K-5S_UGiS$DiEJ}MRA9pP2PCrZ++y;>x zLh0_L)0=o+l=oEhxfZ}q~W8xFQh?qw@YFJEXLIy1Pr zt8SL64SnDD*zN=fea-am1f#m?HYvAV4e`WW@}rp~VlycBO*8Y=Y=HaG z_RUY;Qnr60!Pn?FDc!Ru+ctj`vBY*Btk&Jh?76e?DTYV`L;Um@zmZanT;go{emCgF z{s@~1wdT<827T(dluh4?J3Ar^w>_OgjkW0?=E{It5{T%QE|@A+WGl^r@50sJG^@WG zG*86uDg8saAlX+LE-IBEKvk-e%}>ZD1X`9&%i@@FPj|BwUfWD7KavBtGzXj=G>w1= z2;Ir5<^T@g`q8@>_(UC8ohh*-ii}M(X?SM(*}=e+uF-}*)#{F{bB5F&qXw-j4}67s zW+t`!*1%X`A`s+}-fW)>ziQ3I?*$FLx&BM61@%+xool1TD5813yh81TSlBuf5O^*b zCPx=ydF3YBR*^AFZ%>sWXqMcFrLP-dd)?hS(2Y-myuFAUi<4e!2Z8I(a+02%niFK} zGvy-^usBgpb`-Jh@yE*7xmrOl$#=lcLC=0zD>*_v?=M)9#hvT!I&D~rTnu%2l&-Aw z!3cBNIYCo7S9mi%{L{_}`Y?fyofGsQTABV~6%Z-2rqfKarVJ=r8;#p65h^tuRwIa; zrtjN%Kjix?t*%1IS6*KBr&5H3K-=9_p;!8RKP$5%2%)P@V{cmSUU71f^{ugsQX8zr zMG3;jZVvW>RPEyd2c&(?xb+Uy-HgecdGY(f(C%UyqeOwY$u`o(yGcaIVKF;kY8JZ0PR^soz|79bU;(l!W$Y^GfZEKK5rgAf(zCUa{p7>w zx6ctsDk3EmbcEAl7lhLseJ%{V+I;uiK$mSUJ~uc#zg3hFA4#aAy*Sg$C8%vkMoOEA zCAYn%pV#fn_;jRig;&V7s3QD05h}YO;plCJ7{uIAS2KB0cPsbuW0(_wD^z+ti+`~n z{Tj{?^`t^w1iLi9fYfHP+!fHJ1-GkUpF%;En{#8a9B4#OH9b3aRh{n8R~72KvlN{a zZ42OxUD&)3JG5I3+m}hQLU3mQ)7%}LXvOvJz&+Es?Wtu)Qhx0I$C`liw}L*G!EX6Wlm(~q<# zn7}p395;P8o9iwKDwwMqFA08`zrqJCBzAxmVCKPvHCFtwpIdAWLuPSJCMYAEvdA-u zkAt&>Q^~3*>jWDQeDa{n(dPS$;)`{<-blJQ$@F|Z>E6W*y1urwz?%Eb_8$jduWRoh zO?Xk3?zFbd3fjB|^e*X*+Srb%sGok@jJqtjphi@Nb0`EcEQj_#c3E&$ADac4i7LcS zwXxTdW~m_IuvEfFnNefY5IablEa`Q2-R zMp4M*_~?oEE1#nLp*IALwUm#ZdcV@ixb$Fw0mhg95jU7v<4rikKQ#+C7C~Tvs#_xJ zugwj50g&ozf?nuh5)ndOQK)aZOZeT^1Fs1thi;%>@A_mJZ!>IcO-M|roRCOAX=>*O zmofC~=Le$)PYc10Wh+4C@>Syu5>#osF1R!)^$MRTPJ3 zzH&n_uyRp(22l$$+fzU9MU7&~zuXXvYI@Il5hkLU@<3sW zLJ&wWyhM6h*t*Zpg8jXQHm=_sA+sL_F?VK%z*FXjKM#J29^14q7>2xYvu6WHHwYj>f^^F^Y{rvRfn3+c;CPg(kro-Ud%mFu{ zsLXHu(oMkNdEnN>sOZtRo-#7(^Nt@U!ZFyADaVeaf8stKbeg zFtRoHj)7-KmxI`0*GyEtfl*hg$6MEcT!6z8%6?X;4 zGVtf`3I--0c>X7649J;puME1Rki2E{oNo@j8``wZeBtD@FcY~^{&7hUkHt)GYZS|=Q&jHJR>5b;cD>;h&HM4YO&@Xxa^R17s49dMvTj0#~ z*jh?O7kQ;q&pwDUF%`s9A0tUh?hg)XoTNom>Ob9tU2_);4^V{nX>t#?4R8i3`(wk zzV$cv(f=cD|C>=n!l-*`?=#9O>3Aar;8b?9Ln+T4dtIwuH=4ay2N%$xU#<@J22wAr z4hARRe!dm!bCuWKw!|V)Q=1DwnQiudAgCX_POv$L7h4OU2?eDc8nFoVDRa*QoL~EK zXf`|$e6#v(!3LIW5$m`$!77Mv<%7Ws-St8?EH%fCj0J-6-h)BI7~336vqM)Li79mA zWdm04&c8vQz9Av|PsV&pB!K*XJ|2F7l#@`+NJZ$03*K}}4V7GsXYAULA~dMt@#_P=f8GTtzM=3TAb))J%PtzKFdb9ll4e) zwm)P1r-N}_R3?Lzpe-{zs?4XJ#xlFwocA<@;ax{&9)3C)-SaNg2}OveQG_X+yx3`% z8SMpA_e?Mc&awEJ;K0v`@7UG`xJQ%gU;5opyPo2r-6OZ0*`Xwjx>u5(_bd}VhA+_- zX6g5O{-kd*`#&3;)bJa|>Ufr}VNF_Z?6%gMo()ENwEpzQ;MCxHwzJICdxQPV{Oh|^ z;AA12*nC@>MFE$c^IUK+gM0kBpigr9Yi8SXOzwQs^n5V0N-X28zR9wD@;q}xP&LRr z`FyZ%YT9G;B1m6vy1o!h=sU}+uBnPLf%QDjzR{7FW+_0p_l{B2U)Mfl8z>J?&u*e2G{k*Yin1PYs_^4 zHlF*;6@O;fVf(Q0{$bb&4SVWVH|)!|GVE{uF}QTc?t>eg-DtqU&zZ3=1)umxtDO6h*4ALU{WYjd&D0_ZS%oA1iVXOBbN63^19-gg z*Wi$0*JCr^(;40qFgKdEmxFX_ftoC%$qg?D{b=j0m*F?F%+S9D*YSAhZ^7aFvq+P6 zzk@BSJ;&cYn(L9fY5zEL%HNULXPM=H4~`hvp%Yl5X47xyPU2+}Q{;;4L)0^`z_so* zpMM2O@?LZ8D;$?tX`Xr|7~P)>!qFm*!*=A=B%?=M(ixKyr(z3_maH_xUkx6k{5!7( zi}-rsw%{|~D)ZL1U@)o7Yrz3MApmzO%Ww_gj!ecm--jg!UJ61Fuv6dYeY zTJg_5D_5?Q%JhPoICQXM^H_!@L)zvUh_Yziy{7t~!K}P$g+Zt2+D2Cbx~M^J z(i`_+60mSUlaG-jZnPn;Nk>+rEpZ|N!vuoI^)6&;#|z58mG{wm{q)@mAnO3QRKMiY z80DAfr;0|8@XJD9iFHb>;g1q}7T;`8Vr~3J&tkDnO019H=vgebMTw2^8$FA~^4pZ! z9KX`DSne$)w#IMtEEbz~4~gyZ8$FA~X6ns5@f$sh#b%VgnXQC!_RllCS;Z8Ixk@NU z1D{ixSfGS*^k!aZVzCm+(VK;(iKR*?M{kytCRQl15#MbzY zp2cF*gm&BGH+mL}&D5KB;y3T)^{P;AwqDKPkA|mbvD{oGX2oyxEEZd!#GLqzp2cE| zm6#X5(X&`=sS*qAn@_tR#d0h3YKeX2o^iQE)TTGf<2QO1i>)esv*BLz^?wJWEM3z$ zkerh(Q8W7{(PovyRAWu8SnQUpGAsTaOtiFO%e|)N?VvZbbI99>p{vY)za32fTFD1? z)!S2mkPcA>)g2=CMax0+A?^&Y6w@3XE9OM)D_zCxS0!|S9vt-!6#EHt{X4;twbTo# zIUEBZlYg&y;~mK0o2KF2;DDwlWGIYV&XnQjqbU+0;*ZI^7YuEd@p!$fAq&Z6+!RYj zU2s*i`Er>M={76h4Vo=6W!uKK^UAxB<$KMj{~#gXYmWF2@lsTA3@JBoThV_j>DquUwFfyEiiYEXC5dtsA8Yuc&*GK!><=gJL3iJ{nnLqs}=sCa@%wf_w z4o5Tx_0Gn=#`~H1^LxR!%5Y;yR+pR8yzro_ei9yE-&z)xNg?v1wP!VfQ@PkRU9vFsO}7liA)Rx>dSdw360 zLpDHD6&?l5UaSf)>hDbWI8)h(K>y6{UpUZ`UpSDWGW zVMKpVuMa0zxh|&EMe}rhczU_RmZqOf9MCUmmdRvSp)EjwkUp{X+J>;ZClfv!?`~LH z3N@GoALt&QNO_a(5%%ozXnEGlq^j%V&&;xcWp%A5_6VyA-&Ru3c!i50l`Cj4ocznKh_)Q*dy~BaY3-7d6 z%ZjG(O|rZ`;Ww<=m%Z3#dVXe~a6}*Vg}s;?LeA$xL;^ep>?Umx4WP)XKH=}(Cj`wS zJ-2UonfGrqxL3?%*|tzNJTHFP-+Vb29#c&G zE*IWWqbbAPj(sxiXU*y#VoSQ#tQZjXJ<>cFFEEU^L;S@!%ME~XvGcd}x(D0WJ+yyw zX`1D-u{6E@y4Jn}!6)x>GjULO?GX+~IH^`zY4?`sl6f5~ol%;m(sxQL-F}@JH8}ic zc(uA{emXe(BLJS>95x<0_rb)-MB<H2p7Q=Is?8XwpN% zamh=|&8LQh+vEOr>DXUtzO(d8YQCj3P0c?qOEbRNm;-p|>ZigVF?_U5cu5iUdY*iui z2v>aV8mBj7lvMbX3IHBje*xS9eH}nbZ#LgP9N}b9>&1tM{oM-b<5tMCw0-H3;T#^h zqr!n*Sp^}hplfuYaCKN=89jTHT}EF#y0DDQ504H9XayV{-o>{k9uxK*ROlHUu6G?~ z0WI<39q;BOq2;vez2KPehH<+ZigB;;g@ZDjfbf(Jbr@?0^sP8yy?LE#nbjF`^Lr4zy__UBqqB zW<_Zlv{_o3UTjRur^C-k3cK&q;S6E*VV?<4iRstvp9$xA^^|uKB4>mTS1g7!rKW!& z9Ar-aV%WvZ`CQml-;p!eJo>qCP%+Wx^CgMXN)szf6O~j|{N}&f6UP-Z-)zqu%$e$v z;#ZX>HkT$w|5r(|^Gg$tmnJe_EEcolxsI8x{8BiqPmxYl3*+Vd1-c{)6Z`2eg>7E) znJZ2Gm%}Ly&s@1Ck?jUu%x4lq{Bslwf>*Ci4I9mpFNY(%+2)xqhp%|Am|ve1-mb4+ zyeio}bfvEPN_cU{tkwf3hbP*~Db~wZRm;B`ep7`unXr4>en*48)R9E`I&;|9kX2l* zO*~tF{WS=@GPZs_e1cZnP7XIH|JqZ+&wJi0t*`xe*ka#LKMi^K`t!n+`N65-GTsdT zX83|iR8IkK^$8`)y)uV*X5?jIPqXM-;h9~|4>}H29bGix^w8UT0Z!U1oM2*aqHXEN z5%yeyg-{uB1NCF4Z|lrO-wwZ{9;>20wh)r3eS+?I{@|&?_1#dOS!SOz9Kcqd5q_#( z)s>XkZcAjQhCju7;=ZY2&z`p7O8L8XGCfzuO*WeA&kP&7&B1+mhtAnK+<0bqNwMU# z@8DWPPkGa0(dzGno;+0DK@bE6lxe9d_(hsOoE83{iyOB>^AIr)zrV@f4G%0#ZKK)r zLKwELI6L$TS-s4Y-wXdx_b*&ZVjxZMPH=d_)b|J(EA##AIbqW%TD9{B9Q~Yx-381)rjtG{2jw`3)7VM<7{pet4V8o_Ikx#Pe3S zUNj>-$@AuzKVKLQ^cI+^i^Ba=L{4H$F}=F=sEfkKyh2y{&J26kqthU}ry}$bw&?g# ziL8FyOpBQB%?zJSs*R05wr!Z&#bI~zvsvMS66EYL@kcTca{bFt z4yo?M^OJpB{ma8;Njy$|c}2Kyx7Xz{RbkK6vXz)=NWX60z9Re#uP0s^9_np2mt7ft zB6)sbp12Z*NEMox*4$6R)gEOw&p~0~}WTkS;QpToX12 z!XGp9^>Cnh_8J!QA~W&Yu(6l$l#SA}a!?)^5$J)VfWy`4*M?&u6sxWce~xd`H2WFS}6Nz_**dG_dgAd-nJ|VAL8xaH^3HOF<-nPJdHxFkK7O*mZVhe z!tg{C$Nw_JmWF@zt~3qH!qMJVbJQ~A=hsc&JJ6d~oBi$x zf5f}{?y&FPxPuSoo8EVZe!PU%sdtAy_nzze*6b$K|^>%Q^# zD4fmF<|o*Mxfp1JFT&Q({wlmeE?;FM63x!#%V#tCAHPC?SZ&6)hC`E^UpA+;hF=J; z40_?QHhV=lvU;oEOM4&IdD!?X!kONcSFf}^X4YCX0M!K~_0oR6)b_eyB?e<0ke)xSU7+w&eZ`#lhT zp9#PHf$&_MQHQSyuL1EMS`)r*r6o(l)6>dv-hYjVWeTi@L8?-}CNq0Y60}};Fuap* ze)3Q_Vn2w1+yKDS8LJ8wDy6osETqCrqm%r)kv$DpQ&iZ0JOqL)GN(Np4pK!A1I0z= z;fGoBi_DgX!yjN}`uZc`3y1#B+IsMB;=}+4HuZGoAA8ChE?>0tI1e69LFpL`rO=ET++k7I8b(OIWJH8#IFqrijn-+T&drX$ao7k?if znw(i~vQNMg{<*Z!p~s#GbIB)MGWjS6P1lC?CbK@QQ`!C2hvN$`>HOvE!_g^CkPJG4 zQg^dyJwoz)^VjuY*n0EzC&Q7N-6zA}ib5cw^fv$VRM=O9-w(!{O;3gW&HYaSiPh$r zr^4}YqTYW1%qnb&?UD`Qd&R_>r^8!7^)sFcpI06iNp&xPOW3B%yr1;?L@d_&SoHS_(LkI{vb&n!Y2+b~FfsL-fI$!teIAqJ}-tFJc5^doNlRaPW)aP2 zGro(s5=Q%T`Yd{W0o&%`Dc$rcR$}Z)!Cx#eslSAKb(`IuA4Fv>(bWsgk$(w?HYmTn zs(n<|x&^Hl|0O)REdSc){Gp7*JD0I+vK3~ zCUS_d7@qAZu89G559?`^O9uRKM`y~dLdRAtk+*6*#1^88F+CD-Ds-__P@Tumy(EJm zCpMl#)YHb86~SOeSx!_@X5EKIwZuC>_;hNr4=AMVu_~Es2ou{ph67=E@7q}R0Thtq zft*b*7G-~ebUo=Z8&Nhiul_ykegHweT!&@&4HY;8)n5IPM4%lOQ81msBEu@dSl=0} zZw`4SY#MB1wgNOPY%vP>^3Eb>L3<8`r*TXuFJEdl!ow-l7#!EzdR2Q{0c_W@?w2}F%X8t&is$+P} zR%6bd{LQmbgfkXHLh#qj*@;xVH{3K&%Osgjg(XYN2!l7X2pN$p6g#rQ=XhQ;9K6U> zj`U7q-;0<+<%&(49M3C-D$OKaRN63VmRYQvkY`!A8ZF}08{#dO4fA*l65}M1OCO_O zN4W^)HiQ8{CNeq7TZN8Y;ORF7RD>0}qwABgVJ9R27m$Yi56Kq{MJMCCXs%K^F=|Z@ zWtnQnm1dXYYnqBKzp_nk%c+)pv}$R5_y`AX$$5N1HQtEC@Ue;ER6#})8C2NF13pMt zZ^iB)#Dp~nQiOA?bT&RW2tLg5@I!NW!OR|xGYL*PP$XXmwPUPh(v*{rmX>6Rcsaar zo14Dn7!GgZ91Ph>8DdY>MC;Hto+LWmtOKe1k=f}pI-1K{mP{@M;1WlqAF#VyiCiEo z6;rZ)a`idjuQ3B#4cpgX=UHDn8_C7C0Q?sP{o?GFTqz1V#z03}M zvI`Q+DUbc>Zb=PgV-rXZ7Bl&~aU#e2+JxFG$_2CNVrOk_R{w#x9Z7TYRPcpvpPVhX zI9egxvVr+=%WVrk?XBKJzU*bfd8L9Ih>$5J{Dj8XkHz)_aQ`tBDT|7$Qf0b z)j~$IGD?U6w1f=AsX`^%u9)p{R+%u-bY`VG(?gvR=$Kb0djdI`V7159CNRLQxp~z5o?`U0FCFMUaoSkZB={P58NT%y0$h zo{2-OoqnL832)S&ru3spNpluI>@4G}1fJw^J#3%mXtw6}uFmI|rtQ~4XFdj<^{LRA zQAP`Q^Q9$qpngfag=T3V9F11&=4-7Gt(cvLWB0BxDuAIm0LU2;G*SxPA%+g z>EwmsT9&qN;PFmtC(y|th%HnGrHLOBj;4s<*fqFG-?Q6bX*kEd2`0hG7U=g<7;^{# zw_K2<>fFPOeUj|kQJly#J)ib8{&^2BW5J| zA;E)UfppV5Tr7`Eko_AN?jrdG>eMG2dpsVgRi6D6(Kn=V!`Vd|kqZq@<^0EqZh z+y!t0}Rfn>HKc2D=aqo&wOp7^!M3Rbahixb_I%StM=eMPnp9n~jH+CF7P!hN6UEERG4$7u4&bqXTMGy1m4r=oKf)bpxivK zFspvV(FbytlrUfE4}P?~B!?Jw?mQPQr_7D_nqO&B;8M6InD(u*7Wi{DP1Mm!@aBl8A3C?#Sp{ z@|Mjm*&4>nwJHI+K=>qoxsx7@L4bq&l)*9C&Dl|KjscOEhA|ttE;=w;(k|X9RDZk} zD}lu{y}7n=D#pJW`tEIiVcP{ zR>wsg_B$k%m;;uak%>T=Wx4ca1qj>U&7&Xmc!)=9Bu+cZqp7NZmTEm=3Dgy z2X-^<^@{HKKM?AZ?q`SVk;;xJdbaDNMl|brgraxxujA=7BRRg?sQva>M`i>xK6*>; zZNr@&u4~FPc<2QY7R#Wr(SnU=&~O8Ym`d6|G?^RD`4Ge?DG4B8W2UUYY5g==(`d-T z6}zXayeyhqsHuqpkJtztjsrKPe~GD+Fj`3`zQ(JcoDG~zFNl(AX-QInb*T#XNqB93 zAiv+CuYGxLar}1>Ki81ei|0m))3b=}oNPayl9>2xFet~PnVkBTY=%d=CEEigP$tR> zV`^^ai8{lX(T~GsCsZjUpf#rST3bc;AY~s9DemDjqQo-k_*xlro75s1YwbBKfZI z1T_(jXS~pel4uf4)S%*R%>VtV?wK7F{l?$__mh{~Q`OaVKK0!76qGVf!Ku|$8U@2m zqeVC1+=I+*xEkg{aY{P&4E}fIbtbQ4+psL;bs?`~=lJ0Y@){xX5pV8`m1mqxc%oF^ zJGABY&K*KLENvkPZps3%kvar?(ZeYixCN2|(7PhYmmq8T9*}s@*HRF6&)LfqAH5tN zeT44Y$-zW$iH>+sDJ2AF{ef9T1!sbF)Rw4Zrbvl zW)|yn&!?DeAoi#EVP_YI#n<%=&ktAd^XhQvypaSX4yc&bw*>=Ms*M_Dua(h~=myLR z!gL(GlaJ~VyQhcgk|LlwHZJ!E=UqzNR$W!-N{*rm5Rn+Icsi3pLmB>5(yXLTkXF*H zvs`*BX@EeCX*n%!NxWKF;iB8vqMehDSqN|oI|fS4l5^vL_O9EvTC-KjB1iya)l2^o$hcCRgRZs zF~!+t^(G(+c8RH}RiaLkFv+L=t`+Wk!S=no)l=4fNq|o(@TJpwexW8$pOsmnJQSDf7og z-FD5zU0)0)lUh+x(qN8CjG4W@7hdXitNmOr#vqOC+=7bfITU!%bn~k!LGA-l+ zTi_fxmNHtFEHE%XDBdZ3@}<7<&w<(}x{M%hs*lor`BN^cP1RRB+fi4due0O&@~6@@ zK!`Y=4$!aU>9}YIo^L9=BVd+_B_cpH?6aQV)C;#gt%>5I2??>Mg#)wk`>qQEKhrPB z$Jm$9m%Pci8zs`wjS?LBk8hO7xEm#aCJ0cfjhJSRsuRMKHWh`KM-#!jVW1J)punUX z(XqO_BxzNV?6}6HldhmZ3d2M}TWy+RmxaS%K8?zbP-$Fr6!}W2m_yOi^UW}zPSTh3 zTWD3~>B11SvD$0Utm?}Y*i=#(u;>H@L(C-192L?d7lxjUv=qh4B6vxB1yLt=1rhfW z&CaJ4Z8y512r7m=Ke?HLrA~y>1vLM|e;)Ed2o@xEd1#JfA09oM6948+YO)w|s%F>a zo`|m>N=G5DIaB~_Q@14FLRRVz#!NiinW!@%(9D{%CsCp%{8E+8EYwTT&2v3Y4PuY| zGUc}hI8lh2yVR&|(6H&ndP%-~=1M|%Rj>t(g{Y#$EgEq{XWd!g$hsow)cM!f;Pa4- zOfgw$v-{JrWJ-vM{34OzyCapra$x6j=qi)%Pkk&)aRB2eCUp>wU_`opCP^rF^_X=D z*L4MLp~@>3M9{fDl|l^>ND`2jY3(AAbT^1~QA;Z2K-Pg|E?WhX?#->AXR#~@zcgT1 z;Lizsp=hNniYBOmUJ_@mAQ)?it~p`U(<#K3Q<^qnV1n1t6*)wN#p#l;YxQ7;$orDY z$5J#JSe|P(q*2huSuerrxssd(yGL!fOL3&SkJ~6;hSQz8qvp%6;tcqYs1;#5h=gK1 zbYfCzp)(gE1;Bp!nk0o0j-f}le#n@B$tY7>3Z?1@7&gc(Oz+ZM%cX}L;52QzM=1%= zIxYxEPwqk^p8!K)mf z4O;@*9~QDQJ4SFVr6E0I=L?}W2-FJE<$Dnr0cc;p7i*YXubhs-! zL>HB`(4DgN(fg4ZziD91R3d>jla0YKU&i!unEA*v0%%0abUcYJo;s z@GbzAOH*|WRKV8&n91o{V`!svpG640Tiq_=E;}piUV=v#)+yz#83X!vY2J9{^PObC z&4QkiC+;3{Ap{GX7QlCEyYe-sygyek@iqIS}z#9%E%z1 zwbFnzc8C?aw3T#4tf-aP(byqY)Qa726!TGv#5&ziOh!enkj@6LwdxFXjAT?K5#7$I zjEbmI>+CLy?H{uiwTwq4D#A`}%!BBv$Kvn$cO>H1r zw;xN|kib@DL-M|ovR#Njtq?+P~wB`&=TRRe99FWsTN$ui& z>ni-Eq(Rv?^IBwhP{&2CCNCf>VoP1tTC%caRdJ>;&FNUm%ezk{XR&0F^aQ4e72+^9 zPKt^;fkw%(nt-qSxhjd4zr;OZsQFnkhv~XX&0kh)bd{Pv5TLM$yJAXr5zK&lU4{$V z%JD!7)Dqm0Vky|p;R=K*Lc&D)pyhue@d-Imz|T#o*h{BGI`-oKZ5?~Hirr}xH?3mF z6!(8m$6k~7@*0SA>|a%}Lo0RH*hIhpNfM+7WTD?K#U3i@4h?(cp@NK9rLq2svRC@! zvQiiC6wITMQW|%Rs%$(RBTY6Kon?ZuN4#`JWe;4TuaVY1GaRkGFRi^_(b`KQ^%99h zO44kz z7>|R7R1W+c(_D*Bv{Q$(Uo`;P0l>Pl9iVfN4hZ0@6gDKG!=Y&!DB(Dr`R5_!fR(Gop+(CvJF%}sX)=ME+5Y0LfeyO`a=(&5Z9Au!U z5{`m~?{fa!UAIcUG&3(K&G0|u8?55P6G=`e$3JygnV;e!Roazdp~_|4&!~)A%bSwk zJi#Z>?%@eeW$UMO;IaJQqMABv)vUO)Racwu*W(lLP3C(INYK=zg=hId{)MC)V+O_; zeh8J0rXrC{vT7v+YQ!kocia$AhtS|e?DTXYVJMS)9JDe-R|n!^dvsbxq=Y6G$Bz&z zqCmhCBZZp+th#(Z6tfE}i=Zr$&ywNUj8UdQfimzdY6pA+%F%YE8`R)Ps_H{(a7(wz zrugLRP0`(`sg!Ff#)!0(kBoo4d6cxVv=Y=1QE#wxOGGwgJ0_*uEo zEbyEqA8rL~5LqrH#OwZOFT!)0bd39uVYd*`!YakK!W1~3d$zUKXyrzcwd$US$(=~% zVlvUmZJ+nZ9tEShHM2)3+_v0x#E#<^kWL>)Me0<=Cx%B+7~`0f5evVqY*?Xatf!>} z{G&t^HVB{okbS1Jv1B>cO8QuBb1C|ZBiwJwmh0hD_uk9%iY@QS^2?qVEQjmqI;77F zpT^FyR*4($W&7U*18>83GuA4=mT7;XN9I;9O?Nz=IY zdnHY`QWb7bOPGk3O#nrg9GwO?$hz%9UZ+udf`()Khd_Q3o_VZUf}*wY&E65A*(FZ|&vp+2t>8TO{PN z!NBBLZF9~3I9~7E?6=|=JgeCs>0Gx_IAS}LkASXWz;%0hZ-2>Fa@}^&YznNj@ za}+6U*N*fR0l<2r{QmZVe!lVC0sJQWT0g%}@&aT3-Ot|zkLYla-@_i7_dDYjeSF?m zoPj6u{+&F|DEI^88jAMOg8%NwP5X1R0a}v7L>1ky0ZeAt+eLDHvq%&1x&N@gzlut0 z2KXPAhw{EP1N}YAsU`OCf$)=b`NDyI&w8zpgeZ1O6gprFb$Z=EfB&e{ErkBXXDfX9 zu7@HI`I86v6N5Ol#6CF44{`hU2Ky7dd)v1j?4K@I`QNtqdwQ#EU9107`f@?5PppOZ zn_K-Bw~i3#ctW+JOz2;S_&0Qm*6~Ks9v1R?Jy+gg7Z3Ftw3!UO_gKHd)8}Fc91eJPoIjf8Qsey#`RtPM z{`V;THyO|6?BSZtwq!$LCx5NAfs)M>cA1M}KPc`{;7k+8zCQ-kSDj zzvp+;F`PNO_=oejZ6c+oP4st*eyakPH6--Uqx7Aje~DMRV>L{Sy9QA7NY{>l=>+-W zRzu5M&cQEMp(O#H9?m9+v%ob+q(>3~(>vY!Y53+BD-wmy5Z?uert+fZNFfL_iOie( zz-bF(n{>EfKK0y|g03fGKFgkdm!I95md`;6>#@&2b^&``4YCaYz%m`H31JsP%=}xw za`9F0m__XRbN$AC&ZIwp_t3?H+ynA$g+Wmpg{JUK+kBos1eFi655`3I^tpu>MImem z)y67EveF{s#Z->lx@sg!`FYg}UM>c?murgHbmw>)nP^q+Be^IB`2o3`9HrJsaMlEUv&a^++x9(aDC-e?*qAtW@ZZ_GZIht5i-|NFjA%xwGLz%4<{}NACsF z+3-bW)hmk%#88-8c&cP{Up5?xZq@OJCw5`kK+e1Jj+8>fo*hVSU&ETgb>d^!v8;}y zGPQ(Nmy+rnXE+#CtCD;dCEa)Vbg{ErQy4S$H$h{Q*7LL77S1;Q7DjeS-y}4%5hSQ8 zU39COG&x;kt`6?^_6q>j+T4fs_`CdWCAkNep+}1{N5)u|BVM@IrL9pC=3b6a-7}T$!B_lO-Rr0!>fX@r*1f9FlZ8C6 zQtn|mU~(TPO6JQL)@?=I z8y=%f0&IzAa&W*sy3vkGxE%#{FBF9nOvljHYG{p>p#e43L)+dB?U9b5g`>tMPp8OB z4o#oJFR;LgJTCb}cpH4G{Um*%aH{hangi8H4}hK5KJ8sbk^j<+smaFaKIz_a?kTyFtu7 zPCgN~Qo9s%t-c?bx}YW8ka1;@PvbwF7n(~CO||IKy4-6u60n^{{2O|aih8m6mZ{_a z>s|mqZZb3q`i*$$dS0w4<$g{tIQF3fgD58?EgoMSom*TZt@SY&f-||BvBkvTkQBbv z9UdFO$G7*F>C&u?9=ECfmcC)yKo{RS32~*Hf8lSk+Ha^86p)s>POFi7UH(HQ?mI|E zpzD@jiaY0Cv^5r}Z|i})eVKo$Ic880Ee?a+1Wcf6sfG#EeE^W$W6{qb+?7B}9pJGh z1`mLWw>P?5m8QnK*MWuTjq6pc8PIwqr;d|qU(mlPz&!>wDmI9uox7oQn=)*p-3*9O z>fA0|uR;zCiB)sMr~q}%#sgs33|bsbhV8;}1!wz%36|EJz3jm3$nu_}F0LTRt%0i{ zs{~GXF;XB(Z6UZT9T_HaXV>IDbCWK+o7)X&j72bY&4>#ypW6Tjv8nCF0V32zx_^La zb%!04+MASXSqCGkRwefuATD5cyHu*bDF8uh>KLgpaLLi+GL4a5LIl73tp;%yb@Hvi zV|Zc)ei6y(4asDRZVHf+Gc+;A)?eZe+&hz-$Mv(}Rrjt+FoFUhr=nl(DW6;v0BSvz zd%iZlYcO{~jczVX#TzZ<7@I(vCUuJmvgcjm7uq%$mD&iTq7`v$jK^|B?5-9BGgl(_ zp%dkD(TD}&;3aoAV?1E#mg4ccks$X)WCYK1@vr&JY z2BcQ3UVVH(9n_c+UoSkEyFJ1@Zd0r)0n-`r!gO3XNF2Rzkbq+&oCIXOAjA{{;Izuh zK^rP72b&)Ki&hZRu2xXY;6@!3-Uh*`gL`HrLIdy<5+_CwCY%e|i8p(phTSDsrwrs~ z6DNf-jyAE^(0<8N7+biVeW^dPn;bPkt^pwMN;>XT#@=(OKceIg@{Dp$xNx?#mC*fa z8TPL7(z#pR)}0uyC==og727-T>p(SXG52@p<<5D|narQs`f$@k3f_aNcwU!SztIZuaiq7$7L=z?tOXa1m)yJ@6ct8o94qbq-%Cq?nr|4yT3Lh&KWd35H2`*p!=?4aUh~rQ70@ zJ>Yy!$!IKbZA@0Uf^8Fy&Rwnp4^_W4iq>SKY?TfvWgu5950FslLZt`HP)Tbiy}Xu- z?zwHk{%|M+-wWw=wPD=B?nuQs2i-mPt_Uz3USqgA69^UsMN7lzz#O0#1fE_2dVYWl zA$=(EQ!(t@7cTdEH9J%p`H6Odft4=}!_5^Lzpc3frRGXI;R=7~5oFeBCH1b0TxR%{ z#=yWsFoUX?i-(CQCaUm)v&Hw8Gl-P;4Le{4WONGPMCcQVez=Tiv7=BC_z3~y=h;WE z@Q1g`6al8&e_U!jzjBf1G)|YJSQkwt-)KkWZnb?DqI}8ODGU7}T+MdcLcb5GD;N4> z@eqhhf=nHaq8pktVJacYLdLFN=ns>|r?2(%!(GpVj-CgY>!9-*aaA|#;%-j2{(wz) z6P4h%b+g%KuJk+g)9#?|77T;h9ZZtW0I>6FwDa5$hT21~^mE&k$KeLSZf?+b&>#i^ zKGpnZoMIrt5DIZv#l~jrtylWJxo?(3{sSs7nZ$@r9ZaMn)O^V<*d()q>0P^kW(E|| zxiRtTvnlT>t-C`w0$xe-yVU!V)o8CJ*@4wc5m&7vK0<3>#n~b%7?zaJXM01WHy#78 z-3eQC%EbWaQ6CgK66yjXkXwB81QFm9DntP71)orEfa7#Z`htRGOd#^0>0?JaKy(pD zBy+SRxzO?}r0QF#C=`la1y}xURJ0b}iAtw1H{#{LO+GN+#ENbLM+swZS};H^5D6Bu zgN=E34q5>bETdEoPXoKMMuJp<-|#)5 z(Whh28-C<2tYH?QyyPXzis00jB_NP=de@M}p<15#pEloxVpffJpJVo74fIeOAUB-iJWZG%aP z!e|h$d~~_lyU>aMA=!my{vT`)zsjHN`F7b={=WXY4_6?O+Gm#d-EF_C{h&A+&XU=l zik%ceY=2UcF;d~}9#OEX3Lo}e3NQLDgr6n_1?6z;dk z5BiT`P7%=6=P~zg`7IMYjnc)70=0%p=Xo zs#z1bd>yy*x*SL=tecHOBpq%$Gn`YJ8=|gD2dhs_xHkGU6@BX7ca7ew zn2GC{kl0+wg2AtMCRN#G%h_sG+in0nsx!=s9y6(q zS)B^Q#F+rRrqsLd&`Hf+?r&mq)H#tWO_&oG!Sn^fKQENVpvstq-Q(V?@aEkh#_z! zq#_a2?@$lVj9Ybd8?nsft;va?W6qe#y9LEg9q@(y$K*M4EdX=ZnFEL}#lQs40E0wA zAz^-&933~Y&m@7EVpU_gKga<@xGlOMEXWv+UD7*SPhT*ptC^wyoG(P8KOK#+aj_ zzi4&f5)u_kCM2WFL`}5U;5D^+V$2*8kQA)t##feKJ%Q!?^uh>9nC0@x+(ZdzATSp; zB%O!dy}=*g^|3>4z_gyR-@gHKrLn)f!SCI8>p2W+1KXbq@-UTnAxOaffpVqICV6`_ z=Qo?$EQTpnHkolaDoebYMACDHs=LnzB%C44gL!mWE0yKUWiT0ZJ51J2NX$dCqKgV6 zeHS&xB73%~woF)ETgjQ)xWu7c9Wn!TIdvbzKmIV|!Rg|$n7}h_Cm@=iUijzo&t&*c z`DZHpYx!q7{Gj|(!d?8C-|?9V-$?9KifRj@?-*RUp?~rP2uTg8O7}Avep>#S3STe( zOuH&w0bLjxm-WN9%0EpB6d>>f}r2%yOH|y%~b82{EF2Up!6O~;)ibp-*1;DC$HW=JeN)e<^ z=fMUz#l0@1?H4!sJqdK){bqk$E|>4AjmBooiAeOcf4;@1{Eps1L{I+lVdOG5x@baMGrx zD_$-2Gs8+haI4>Q{OE01-54DYj?gjOvRr6q7PH)Q2CmMJo|K=vluV_e8d3)s+i979 z><+|ynI2}uLuK;4H;sQID&fddvO{mwg+8P?Jtis9u3YB#XyxFzgYfYB82z#jnb@hZ z`c7-;$nD>)cbnh19m8}?$@s|47I$J4G7*B7P3$fm!D1R zxzKkkg|mz8r?>fqGVhbW-5*ef?RaN~kBION3)gD~uEmAe86R0(B%qz4=MKbogkeyK zg#1A9P4OXu{6P0>6u@()>}ojn$~Vw`phX2uFkjI-2#zP%Ljv2kL;j4)78t=GiB~Q# zf{xOG2U829+>EH%O}Ys5uac=V(t0)7+wTCj`fKpt&>!{1xueJ# z!p>YB^k|f{8X@A?$AE2VN8jl;dA;oS@AP}^l|`);+n{$WbS_W&j>RiSV&|KvbYdRRMh+8|R~!%(zC` zS%{hG5gd$7?i|~j^F8vNn_+3HW|Re0+@j>pfXDE$$?bU*<%bK?EzjFT9)t3D9?aet z9!;VmiTp6JHHmydkzx{gOdtbz3>622<{_`ZIEj2W!ABxLMCgLR8Dg&z`R+m{iF~WP zo)h`LT6~U}IOufTotwBA(&SC7Ipc4f;}s({fv#IF1Db3+RZva{|47N{+)awID4{2F z^Wb<__9}Ut*>!j#0h0}ykPzsK%<3pvj8RFWnDauu(UtF}M4qVP!W7+==$G%VL~>%j zD|*&2F#yjKAhU12tFqxFP+SA-KoZPR>?7k=s(`g9+6``%`OPko*)Cojzzxt<6a9&l z>@d>~Wgy$t>2wWa8Oc!!z4N6v)IfUdkng+$aCw$ie&<=PU@_mr6>4@3;uJfv?k`g9a4KG^ zom$V@64eiC0hc zTee!vS-8~1=tv!$Fh-Qc0cQy~FPYCycGoNL!(FeyvO2dn3PAJy+~S3MpjCg^_T9su z#KN(-L;^pG8L|Zp4wj>;P>lst&?nq z6%%}oNDCI=01+?$bO4L@q)i>@|ACP|eV{)e%OwGe@4^HAKkZ)}=#TZt-uz&{mB%3m z`)9Khe?His>0NF2I>i5Za=x+e9^&7?%PS70=g->JhqB`D+b<9G74N;q_< zDF0q^A3e%n#^a=;{Z`!`Avg$dZgck@05gglzWivw_cQzFqx~OnDe(@+0NHEoxySfh z^LXeO=VAK6G5#!4NBzkExwmFvo4*f%NJ&JbFbiWOk(MzZM=X#9? z<_*`(7pA}_IyEoWgwO1dWBs|5zUx?)5I@u7{C;-easKAz-=`esd|b(wqw4q{<2gI- zv*>Kk#!$X7L_3#LrkxlGZ6PGVHTBu?{sj5BOR%t|Ju?IBVKe=vP74U_-kL~^OQ`(Z zd+m8M@x)zVSI+cj5K#X0*?z%JndK&6{w!3rpZfOMfqplx*Lq}@znp%adV;^7caVMd z1pj&_clL=6ViupsS{-DMJIOz#={+P$SiB+z5Oz)rC^rr~|K&+c-U7S#Z2y9&r5RMW z>&gDp)YJPEfB5F;G{8kVwsB=8{yh4LWs`*T7j$w58xrB~|FHu0Rtg6^#TPE@G{7!C z#qZZ}W(_=8HoTtTGHHT!KY5CO0F4bk)jzXpSQ-2DseUI0^ZKd&`BCc!N|TiPll}c^ z{zu*`?f3r#kqWQsZO`N|z{q!ff8kI4nkIS%n#Y)*fKQES5~%Z)_To?dY^C^=GyGg! z)SV_m+u(LGGmT^Z$!7phE8A~8!=G6BV$B=by56v?6jK8Z4@8>7$VdE@_Azt(nz;Dp z)peultE&5gGv%q>zT!+aZZma3VLECB(3w_s;+6J8&t~{>2^ikEUHq8i5(nG4=lCP~ z05*((J)$^H$Q&FnNXVX>c9#J@caFcH{m0Awhm%D5-@F}QUuu8b?zar1Wh4PMe$^G1 zQc!kWcq!%U`bS`ypR@IM`%}1<;*h&J@bV`+=WhQeRP)8%Ac&>yLw?J-3X-qh0|;`Z zH`Xa$g6!K(y2YUHy*NKFwcFn77kC_guYW9$JMQy)+kf7RC&g0x$-RDSzju>R z>O{mXM7%XZMmo%j$4Im26Ys-)+-gdwLC5UI)1O5}-<$BtK{!!RM z+yC+)=P&8#fZuT%*Ce?8`RS8RXtxIe49)UA*EbNgP-Sxb?W=M$rbXaCUq7p98W4dxr10hmBBa&8=Nn?$G;{W7-v(k`qJDOv{qx z!D)TO!4a8yl{RvwB5YmH2o~yjN=~-aIaq6OnAos}$MxwkO!* zf3(4qn7Ze;Pk0j0VP%eaiUR^%O7@h$9gjDk@>>RK4P4Au=Q98m;C@PZ>B}-7ZeT)i zn!VMIe%imhUMZc@QP!IFKR)fh;W119^DH>}ZTpXBq3M^|<`-S+j6ZUCVP*S0e`GCm zlZj5{WAoRk{9+w}yAa2@7%%y}|J&|&-%cAX;aVQ$i%jYuKhHi7^sclYJns)p-u<4< zzrc~5O7+WL0H!OcDKGj5AESG2B78x7!}yvZ{je9U@lRAE zsn`4uD~)m0Qzi+;BcP?kq^604Wp71?;LWSyIOwmyxJ_C1;=NXTHy6 z%xKjIa#nXb5lFa}9iTn+9aEG}#zj=K8A6Wg}39NXu9)fCW6*X$ye5uYD^5)TI> z=FZL+4vl(EzSh2$H$8UkgvPZYst!YL%w=LHz|nwcC;9}@v_Z?PATh9qo#;Q*M^P7Q zq*RiOLVck}jbXtwq1O6eY9#*Xx7G;Nl?z^tW&u^YP(QAKOayaRBWglpvug{T2*^{T z*;CIF3g%3|I=7Y3cVHusPch1Gx}&O;2GgX8j2a&aIP@y0D^ zu^diMmIJC`V&adpdG~RUC$tNk6m{rGxoRU6iMZ_&yN>NwPU@WtbEhJ>;i*VmB2HD6 zFv$8HC9>tzMkNMpR3c7Ql_(jVZml$vEvKrA7(7d=OT?*-N)SxCs*X6dQHe1dm55Uu z(yffw$l_F0Nu1iKlC3u?5vMjPF%dDQJZD+C*DAG93DkB~MM_~gy-^|jdaDY#)1_al z=!uQXl#?6PgJPk&o^rCI4Drgt=Ql1?PIeTcjKY^z4Xm8ps08N}677bn{kHZFg#k4tHcyHezX#uehoVJXxcv+0xCDb^6NZ3-Q&n?nUBPoA_FPY|XM$u3a0cLnn)0{W)5qyo1D;r?E^|qjJO1!63U5tin z;%6Pm1{ej6m*gj=pAz6}od93##u$U?`EJd{fY?gK+GbjG5pC-Eq1aWRW;X)d&5RBp9k#nH<5v# zZvWn2q0HXXw)8g`wX5yJfAeRQKi%T*{vq+F_R7DL8-Ka$I^^_~?RTu>G%GUNCvW+i zbKl;kBqy|jKd?Le!+(QQwM&Qm)A!=T$yZ`Y)8S+nrGmrB&#w0yw{nM*PXSC_fP_fg zba}^<0~U)8C#U3e+Hp8J7ObcKKHbe;_qKm($2sKz?{GGJk)897|45&2l*L>J4}aJH zQpbJ||CfJ|{4F2&m%j^KRh^tO?35$CX-68SS5l6?dzw3zY;v zzSO?__aUANtKr_bapvFK~*$vXls~i7Rk#nTT#5`Q!F0x0Qx7 z_0+TRs#%eA=-GJH47W?onhMF}B*@9P<*wH_Ir`^5lQ|iu>q&%&!bK>&exbdN$_JFE zzC7d|(ZINhtB3sMM_*r$@|;!HWBkW{?`S=o$l>18lj`-D`LW;JwcJm2I$DpP*+n1w zgM#wx+b1c;;NSY#@6$2S4WGD)9`cF5)a!eB1{t-ya*6{c@7eG!&0Z2P`q!uaEqu<& z^`-Wp&;6Y`OSHnw;B+dw?RshZU7!2h&Wm*2@^AlQJ@5Xve{^Q6L@stb{jF!*pp69RLI}*8^r*kn=sNiPK$0_*y@hb!2^-dH;biR4h!=~e88VgXB*jdJm>d6r$ zY|k?NayO!W>@oD~}_x z<_zyB`{%4V2+;gqjk&Q@;|}7RaDK84%5O$`mk}q67zBlSA|gen^y|eS1yOWRr0zB( zHE2g0@q+8ay6IvOm^5aCi}O^6pYO(kAg9T^+a0A>-%o0n*wN4Gubtc`JZlyW(m3~W5=7dmPIhcC7ww*PH%CeiE=#QneE{=B zg^>)5c<+84pRZwETp-uF&~T$&=oj$J+_=s{Cv#E%s7{TbT!pI>y=^7KKHJHRZzvDe z9jvLfJ#uD5scLux0*EW92PUKQ3Z0m(rWstNpv3{IB6hKyt8N3`mC1l9ji)8+q$p|H zH(>9LtmaM`Pq5gmZpDf7N?B=0Fo$Jzv1YvFu75&Gab>vUlqWYUaesymO;vfmbONH; zGFc&{vgnuOh2251GdqGv`tVmwyuQWGV^w|}HAraE7xVJ(-o_Xh_*I6&}8MemI z0?}zB&K}1}5IRt`08|MaV}L6L4#rGaD{E<1xD%BHeuJ{>BnzpKyYPgZ0&PzO0Nj+( zj->MTq0Y#Q96CaKZr61-lh`gpyO^H+qb$lX`;-IEa_o|-QDGA*6uk9RSHrbKl_g|AAn*}-khgVRuL#el$lcofcs9S~wr ztV=Kg21KE}D0XfpOetuD8llhIb~U}hM+bK`y~b2Nbdrth2U~(VIaU(YsfLuFs^r_7 zyPB~;!9;=@1K}!C zZb8tiNXQjX;1Y*o9`LC^Z8qQ8U5S;_>4UIt3eK*9J-*RQZV52GYqI?a?5WOG4|udv zSOmyjC(_ft(P+kR*OSu3M3#seHLV3t z60H=qUvx9M76*Ve6ww4C*vF{Bh1~7jfm&+qnC@o6q1B~9&YHHOEYpH?%^<`in!~Ss zFOZe9PPzU{OKvhb*O~y*pXhFya~^chi!oRV%j8!}J}@IWqu4x>v@Q zjK&5*Ms4e8br%7yYCEcjDU2%@4jixweuTdPR(4jop319a<&}M)2il8r37ourgm3MY zVlv_#oVqcYFEHQQ)lP$%%I7QKhq|){AIylSD&r!li)6jz_A^`yoZM6S9y~_0ITuPY zD~_tBHC2jI0z-b~^TJPZoQZK%9zO#77r|^2;SmN_KZ_UF7U6@RUW38pD(0{IYgQPt(5?k3i#*BTvU9ysb7|&0ig@ zIwYzx8Z2Pv zpc(C-UHo`Wcf0WMnl88sx$e(rGVK+;Ou_4F+k2U9Z~*_PmzgvO?>qSx!Elb{wiTXK zwK{&T;wd2@x5ggYYSs}Rh>qR_llz($X?jlTYjzA8(MGbZ8tub<&8Y3!UVmsYm-Cp`YJQ4?%4@CW4I-(v zKQzponH;iljI@?YjI@*Ck2VgG)_&|rvtx4eFN6p_PlWe9!!BKd4{f;id2%lH!u2nd zal=<~?B7P4BN{gTuzlan%spx3kj~>wzF}iL*S_sIvst=z&c7Ao5>2%Dhw#^zt>7oB zJYl*0-7W3>IG71pYJd7Y)61?(HZ)M*4c{};YCT<*^+}Yq&DQ2v?-%ygt<6O}qq>{o zy03S2Yb&g=dw$Oh;d`o7H667n6<1W)#uS=v1X4xC-FbH*UUrRc(zFL|W6lN=U)#o9 zi6_Fz+nTckJD+cBZW%P^S9or@aO*+~l~g$Q#)lK(A}?Ic6IZt&|{V zp<;R=IyfnJPEl`fYM;OKmiIrJe@)3%vUUg4L?xf@U}p0;en-=d$9X%N4SUljHteVZ za5#5ICBnyaRS7;&t*I4s!aKc4W>kVzr;X$}`Sf{3GYKia{KCm-WVn*{sh!LTU0k2m zq9RF_^V|2@*&LJd{$uZ(Y=$Q1O|@@LHho&q%1A`eKX=m%G>;j($&(HvKuxYIks;lI z9kDBGu+kpCs~H;vc>?L;5@T=M)x16KHZU5i2PkreJdhY6*JI2I+Pt}k@VIVC=5<<3 zkpaOralUs3 z_2|qVWNH{)=lDts_yuv4d+E%(uB;YdP5Y#_KZ)(wZzMRpu(nV~y! zMao3HrDK*AqkwAgN^>suP=8Tsr!Ql=*;%hmMZ2=pHtb<0uwc9FVFvZPp+a=raKpvb z=_{|X7w=)79^=-rcH?yf`LmA5_3=7#{(eU%UB#L9(mlUn5%1sAoaUWt$M0nhrl(B8 z=}vmmuQ{H45XdPeu~TWXmhWW_I-J?qUDF>DGaJJq%jeFfpIx5;4g1ZMGeLOR zZj@`lB{W|@%au%)OC~9aw#YwG0bR1;@lvC3Axl_CM%Es`x9LAwr`aAMxOgN(icvwD zp2DC1B&Or&$c4`WV68kP2o_+ebB5iScIfiwR)ouS2RI_AlXIL7m^mOYKGbnSJQw8%hPX&;Dl0=m73t z4hniGC=R+W=|bfrLMt^Q4K?}^>F8KSd-eY2Z4XUo z3sc}ihgqC251b?ZNT1qW4gk2<*mDjrM*`z-9bnGHJxW>@qkH^lpIZf`uR&CorbfTm z`Gvqn>PWO5H5wH`uL zTl8|vL6c6o81aZgoPljT)a=(u*RU4j6RIZwRHq$^#^+hP@-WlQ-g>CX0+*FbP4I7w z6@rn<&x1}m_>edwH$G9lgf%}fTiSkyLA0}W{9$JHNT|wq+Q*@8Krt&6m7AA^D8`p> z>aYnW0EtVA44-?2KjV>cnjH*8p z?7rT?aVZSIo@69P1M~LT_ZjT(rkNq7IlqB%LO$jZ$;a$FOgkw2%J+HUl~og6*nz59 zeP&BKe5L4Y&O#A=q8+P9K?uVln1OR&hV088gR)f*jxSTFsWUyrOoDN7r|p>>*)a;a zN{gJ%*70ZB5GKxPnzQo9l@F|16iKfu*6TwQ?fB^}wZA^xwDe><3$7(EcNkCr>cD<* zxEWe^6FRkVbHYtx| zcIZuiS%a43@tPb@Dg|5$SQJj*9bkWZgt>TcZsg*^QI(E9It3^_UrH4A_r{>Z$LM`> zWq^_5g3o%>9UXcAU`P$n-4fiUwOP7r$z5inGdNGWBe6tscJeh2=g8Gu0gqR8N05Go ztm>(PfKPH}4{{fR6)z!^tCP=0wk&e*gB-dxnE{StxG->@qlTW;eQdU|cKDHI zN{KzOb2M-+SBU%PQ8_zflkK~m!Il7cIz}q>*;j_j7R^0}({J2gF!?6?TZ$r0ZKA`d z$=ys8W!~0usIDvV0Mwu>s6t3TP)D?%c_DC&W*ayqUB}UWI)?@<`pBEESp;NJqp>h` z+iCU%F|_bpQdZ2WgI}I58bIR_eB)|#;EB?=EPK^lh^A{e>dmHShbX{~ zz%jZTgyB0Vsa_p70d955Q=^##1Ag2c(u?ni&VOKC%XRlm^i&d!iy@c5W!9pXd+&P4P zZSIacWyfy=e;6SLCa$snLs+Q>72Ek3Y@&fZ_82n}3+Zjgn0-lQeq<&?Z1(t(8J>kB zLt?0fzMcOg^V2TqJql$r^5_BX@5N3bj!!+irdY~xFxy6EI1YiM>^ki{dCi^v(wGy1Gj3JnOPflAwO6& zmfPVUOi*f;`R7xNq%o>M1?FDsDqm`AoMF!P*TG=m&T#b%)4Da16JdJ!h?5bH!AS1_ zbCKH7?GEe(uomSDYd|eR-oB!No+G5rnH!lhAxzbF4WU{e*uU^Rd((e4N>f*}3C5q}iqR= zN~qK`I8r*Ynje>1rCVO&0|(Ml8zJ*zU^^cVyGB3npsXV8-!11$_y(!aVVc1^&OC;z zoV2QuK^g8_bBe27iM2(euUp{>5dsq_?FXDB%f1)W8p zMk;cv1A=hfMz`A0?OM9skI9UB$%rCVf$OD2aojFEd+Ap7gS)1~WE&B4)GbLF=z#(| zXQnxF*cW%e$&VuoUM+lEiRE_!kGhdZ<8>1T7x9cH+vaAOgMo=-XQ514YHy!qZq}hb zcj!WjGpq|NP+pMz;}gudu2)=+15hz+*W-;uc(uoApWmKf_6%N3Bc_IzUv9gehy=8z z{ge~U_MZ1*`~4@GBQxHo?Zuy%gG_Qxtu38p9vQOY&W8ZExv|&aIeM<4NbG}uuUs-@k3XcLk3IYX)@qTx?gBHn6N8Y3ND2Z-(x-O9h2{|FA@!WS z>O#}n^r_o=!=PjAzyb0Z%7`L_1pPaP&nH<-^`QO z)lnCjKk2#M#pVLCpSsu_#bb+0+~cZC%*mu07og5wYQqKQDWEBLDMVzcJ^E6!Ka$Ui zON|bfetN0ATH1SEW=?Xm=hz~5R1SW?WA?87nfcX_=(3yd3K^X$Q0#Jg&acOs;WeROf(e-@g%C$Sbzx zCiBjrk2{o*hln9OfDw`%7(t4$d>J9=x+*wX<)LM+@+yXSCEdnx*K-l&RW`j8IKkZn zwfM12Uwi4z=7-6ve%=1b&8BNo*Dx(Zq+V(VE;GNVXD7LP?BH#Hi6@qsg*q4NqI5VQ z4G1+Md{dWu-=20GE;n&%iGBDsvu)>l(Nn8QTywzo;qqVGuD6?K2;lSP?E-t7xx>u# zuCgcJVTKKM+m0KC$E8sJiqDn$7ep_gQy8U6#gE@%PTuS)+;ZiFk}Y^C2u$t*j|Ol> z(@~;|k4Zzs0XLx3#l`Z3JIxf&e!9Xu`hR2#y6-MC?W?w+!yhuc$rg0`@4sdXvTJ^C z_HO^|Znk{)PnII)J7rIUSiqWmX8`CSd(=H<2ktVu^&T_Wt6i-PnD55PwP)>X_nO`O z=p#G&J~P5wZl~RcPU>TO;eBRj9(&AAHrcoDLvy>_9({lL%?P{Velv*If4`qu{n#Gx zfO)W7P2UIE39Idr2Tfy~)qd}Tn1adsY=!wD`m$*&>Bft8kGsr(on8bLi`l5xqpr{Z z@P{{E@H=NeTgNl9pUuC79PDRTEgvo}OFX zUjG}@rZ@@AWug8;YHkUi=Rw7q%3lOG;E@T>c|&{YQL~lD{=IjVxtaZY)MI9Vk{3R1 ze#0aDgL#!l-zUs`Uf%Kq8%^|e?UQIgSK3}rp=DqA(TXw)?(Dt`s?&2nvWGoo{xI$u zG7*O&ou)2F2l=sfXxSAZ`61_j*(5Eyq-el1&bNm@ZMp~gm`^XVrKipA-m>lpX^s#bXq^!TlI724=-`oK#$DYo9kiso!uJ%+40!tv`d^xz_&dd2FjI z?Qfr_z{fWIf|-_F@{v921#^VVeUHCj2G+0E2VKKAdE}PZFJ3VHhDP`w*H-qV32#)q zQwPwgh=+^WPB#+!*mI^}kABhgq625WXr4SE;%km!AA`h>v5XT;R2FW{lbP31bIq&f!x@)DgQD>#wI(y@ z;QzZZ!rk<;=dOW-EwYcTfnxmJe*ZPI)Vrkp{nzmJWs)~r>ukOUtz{=HZJ)E&eCe^m zrN5XSeeLhc3S4caT`@`Ucme zed|p}syTky+wS#O_5@)-{_05ez`r?CJ@;=WBT{|wTW+gw`44Dp2W9@lKh2+GTI(1S z+x|9$bESRfZSzA%PxJI5_o_Yk9rJw0SL5F`Z^mD_)wYBGWiAUAirgo2@3*h~7bsNk z9Umih;QjU36JE6w-UD5(w5PrYqqfri>pk=RYMQ;>`)22IB}?qj-Z$s5xe9*WK)dY+ zAn=dvaUYmsmsek@(&u9|S^J8;_5;?=742>R{s9E|td#xY1Jj&5Gi7^jFny6z$8P|M zeQalJU`{@^Pj5gp;c-g3x83nh6vT;+0IWnG15G zQwh1(YTE&5al9Mjd9G=;eN2#7uT}5u6!~auCuqDdQOnFC!63xW+oK` zlX*-D0>x^%CkPJkyaVmpQ9<60sR{mv-~X%$T6i?p23Jt<=Gx#g9^b1AUiS`c?^7QP z^}Ms}E)BsCz1!^V4Z&a*?4^dFu+ztmL?<|0AW1tS(JK}M#JIT3Sps@mA{UqIe?<-? zdif3HI1>Gp9J_6&U|Pe*AKGP|g2%et7L({V5xT(p({pFF&&>rrQ`|P1=PrT^ykeT$ z!QEZZqK!mHRxo#vX!r=tL%rWtFV3%Nf2M2D&Fc%b!%oNg--TJ+1^(9~hgmFBn|sQp zx&?pB+?ZNg>|*oXgYNC=?m-uTpuI6UdIJ_x%$Lsc#`&QAC`4+VY@+jZ_pjR-c z^sl;P=Tb`12i9ZcOGfSzLPA?d3a-d{h?RmY7)DIQ7^|d|;VN(BR5DtyZSoj~HYKSe zPlxgNgu%~UjK(hVoJ`8EJDgLU0r?6)S+ufLDKZHY74kYo<&CB9rqN(3+*bB8o!1c8 zF47aJWY`ikCBd3sM5iuhmc`#g1SyuuZQ?dKMFSQ(T*L1Vxl@ z6u|IAroCrfWm{0bzTAA!r_TWJ$Iyp&STK(=wrCv?r=qL zLJ8HC(`6xlpAf?OYB1~YMAwW_ZLxf>G7<+VdC$^r(Ntz{H7Q91SMrR!PSqh!a~9>{ zi)&a#AC<(cAxDg4IG1Pafe&*Kt{};mKA?3S9leVtoCqtt)23Bq- zWLV@dD01xI|B>0P@7J4F!&+k??oB)O%3whD2@*=KjgqV4S1EDh)a;`TNyrIPrf} zr_rdl{G~eMl}ds&hU00Ccxxa&Y0vEyyualQoT{26Zr+?X$vJd&lNMB2u^(4Am%x7v zZ?8${X!BmaX{5LA275@{Z^#%M&i$WqiMh%c7p~wD)p6x=GKPn%c#vT^;@Fx|1JAXO=@Sg}eC3~6+kRNz zU}vwZ5|PJV*g5>frR+WZg8wwleKetkX$vpsmwF#AzaJv}rF}LZ9E8K$@Iuhk3%9gb ziG!Qmi|e19`}wb4{^OP!vOSFVa$8F|qc`7ELizIw!6kg&vlwh%H*cp2iT=Q#AHFxw z?qB2*7v&5jC#{@w$;r&yX{$s(+t5Gg?#;6g6>%wQPZx`jk=}&;M!OB)$S@E9kA!#sGAS>56-8m$pfIe^X!QOT)TFFYxlnB z@#g_SFW!B@W1M=4XFitcZ1`qk#&k#LIGr~#v0kq*D7f?dUwvhF8yK84L4%gQ6(z{zm4dykHK^%PAwl2||#34h2-oVFcLxV9c zuOE5885+n%Z~f3$VHFNkI?$-~rj(R#A!Gl;AFo1iUnckIHXcnx6Nl06|Dk z7}l^}oTT$uc;3D{G3ZgQtPfT};B7?Mk#z`J!=w5+HvZL03;n#f_Bn88lGien7)Q-GK*CotwIcH_Ji#MtcUGWcL@HL!gK)J7sIN@ ziDv-8?y?KVr!O>i#xB7V-f!%d-$%Ur)GqpdFkTj=m%q>F3+&j*!RbVkylZl>KZ{tm zYtS-Qb;$|~o7NDW6YDC6lN_g+CB`}{j-R_bDdh#{pW;x6wr$toCu2T&5T)MM&=6R+ zaGes%S0LCr`=?Het$m~{=6YNU!fDmq*%{k&x8Tg47sJSFUpR{|zu@2jS+HIEUAqOF zdEU?L-*yiMb)OIE;6=PclH!`Y+jA)xC0)TuBG^X<`7gGoGYo=X4b+ci^zok44P zm=!uSd=FZ>+U~bUu+xMEPd^l^J)G4#%uk1)r*cv1CDRxwqJhj%98 z)v9KtP3{R<`k6grPq)=>*fZE&_O6fi3^r?s4H|%M3zKEX?iFl3a`AJCa|&5DI~XgU zY?B!qpPq54JnIT!-0&cx^OI+-5Z6FlgLmwby@Fkn=Va`^_F`O1t-p8B-+Rvv*&EvK z=$|yd+*s(enQmpAtphO8oVej0w|6kI^Err(bR8BQ5!`{`ZF{40dC$JEcd#z=KT-hN zhxQ5P$3OSlH#iyqzIWeXYHByVb$T3Nf^My?q?KCXzLgDKx0F0nNfAl4D%DZa?tfq~ z%{$Nj_P}6|+BaS3UYJ4-6LdQ$*xdUcyURhG!#&TQdQdQBNc38x_-m%Nn1ol&Oy8wm z+*k&mWuY9$^Ch{j@co0(8NFuTJvg|LO6(!QRiwro8r)A^?;jeR?furyJ`5i3Jp0ST z(8RoM{b^9DFYL%^!S~AXer8PzexW#l4TlG}>G{Cn!7}XHGmZ$RdY#Tn<39{xI%O&z z8N7UNW~N8`Uw#1Ltoc-y51cKrkL~`$;Hs|_<_}KQF74+@OO|cLj3B@D1Gw(t+{%v;Z zEKJ@X+nKY1Zgo$5tc$@VP`~}Ly=+!+%&<3;MO-R$JVWQviEP)Hd%32_!NGiTA{oHy zM>5AX=DXx>vV%?tb|_sw4-``;?`$xKwo^7-tSqCfRG5@&TQ*#(tOjM_NFCW&qfp@) z7b<+?oA6Arh7PUZ@)Y@O00JqjzO@CMg{m*iOHR~38~E8e`_TzO@19(C3$cmaOrhT4 z0yUcEI)b4m2K@kr$tMQGyRUcCkWzJ-xH|e^FFrBo9mHe#(Ej?wV4G6sV$^G1JGaGL8V|&nS9QyB=@7D3m z*+Cz&7DaFFn;q<1AJ02im!#`5cGQW8H?P|XCx7#xe|mDTeKua24~^YpR?ui)J2@D} zXSJtPe}*BmYOZ!aCD<(q#OSUBd;gujvG<-5Ts%0QJ%aMf54H>iE{O;pH?tvXrYpW& zI6Mna5uZ*Z4aVX2JGqGK)>DISiuxTC(vhEhyowv_3#SHK>@Ct*vjcG^B9Fz$Q8ua~ zQcsox(E zwq1cBsJpe3;h8tn8+ZE>_Wm#V1;N&k(@QRZzuTRV+V8!^;f~=2;8H|H z3>RmJoPe&2FXyEAN$?1FK4nE>3eY3%Q;;{gx9#qi1UeRb&L!-K z1bj8k`MMME5p1q4cEZI&8erC+kU6^7EipY@YrO>wd5zt2K`?NH;vP1lC8e!;7u*nE zcD_?2=1^ekIqU%9b`NNMp`4ndgdJB#eAkA%{WJkiyP+ zpNN@Kf50J4W;-XCC`E0NjYu7=L%r=TZy=teYrJFLG*Zo*M#6cYd?!2UvY=J7my>pA zdOp{TUp3(VuKLM)O{UXqn1;c(Byvd{U zRe`SQ_|;XxeLRl1I+(%Z>8pbe?|DDMtg5?sf_yA~ng)BlgSa{;hw+8_Vf`<$6Md#^Jy zr2MDU_I)}$x;EVjO7zlPVYj6LR!~2j z65suF7)_gU-DYO^K}s&086KM|*;Psk+?JC1%4folQt;3-;jP7$YTOlXGl4O0^gB%8 z{XVvtbi7d^dB!1KWpVN4Nc}@;a*JcA7puKEI?ZMO`47?Q`leZMz)@M86>epZarIhKlvID3 z6+X*)O!u9QFmu2DrzA7trL)8JU|Bz8s()=xcyRg*|98i^;b-OJ9dr43zpgPa-2d1; zE(dZY@4aKAGcpkWg%bC6C=oeY;shhk0PPl6KTr(&_2cuxIY+&MHfuFB3#|teiN+7@ zF4gqrL#okR=Y{i{NO+ob!2x2fpa}*wWuyG_ap<;Tq;6#7a%h6=^KqhbH@N_JUXFO7%mho?xD z;Vhntpm7RGtmr%KUL{`GQMwd zxMx*omXs8GGo}(kA>qtYYDcZ$@+IM%s+D0_TD>woZb|rDd1HQ_q(v2TQn1%O0X z1I@s*k}p3H*sLj5&O4q+HXaGI{ShLh?yalFt+A3=>G0jiMX(}0H};z#KL z;(tHPb2OFBY)UqB7}1X|1FV@EHf60PWRF+q9gxU9h@7r_J6xDM9g#_Qs3Z}#9Wf4+CA3He#tnJZRqobxv-}3no>*FhhOa>ZG*PD z28u6JR!*_E3zfqBbkrNVVg|vIgO(sIwK-TEr-ESAC+S5He=z=H zCe##%d>0!`k*Z5|M#I)C*{{Rzg>Oty_^l|J&HGl!%7wxY5!=l)(l$Zi*Th`%8U5sY z;no)pgJLGCpP}##V=s~nJ7*|-6GTw_7J7KF{U+n|oZTa_x+!S>-hA*_~C1mcrqGX`G49M0>f+PlVjqR+g zb)x1y`m_(jN8td)A4t9-x?r1p2-5)Yi0e;5uMAPjzZ(rQVgOx9;L1==*%Us6|j>W#M%%OAhK@ssc>!K*rb8g6>p zOsTV$w?OKA995>T9XnKPiGEMasP}kSdl>5>18m?b4HMxT$k#w`#k7=`UC>)TO_zQe z?oo3snci#sVXlbe5Q4XQ=+khW;>vuoASwmj7rv}0pQp>U!o8mQw6aL>_ltynzew7^ z`oG?ip0gudC$n1PdL3Cu>@sm}ey_{U?-y}@zli$HE|+}vEB(v-{hG6Wzex7)7m5C6 z|Jr>1EB&ka`!(14{USNPU!?YL_U~i*ci56!JAb9ixxe37z27fV|M!bD_{}cQ`?dZx z{Qa66{eF?gzg@(jz-jUByAYkhV7lvz@DqZ{S%AbIka>?nE>oI|C3b# z;hf(01B-?~iXXtJQR4jpp8r-Fffw&LnVT=h*$_t=(XBfB$MAu4zfqleFx;a~8QL4w zYv(R4)7Kp|l}3O9fRV!*~70^0t~RTTr~( zcpFS_^oF02qiu=v$u{Y=!6OC1fUyCRd%5LIfl>0a<(y^|=2;luOkcCBh3fDc`iKB676l7C;9kt~Y7g|jt+aa()eWzr z>N5&RJE}f!*72t!&XrdFM3Ia7_Q4PV6sdelSTG!wwHzUmtNeB6*IEmn} zZ>!@7W;v~n(~74xb)1KJx~#7GU_@PKjtRY&p5(<_z!e#~Cu|!t2QZbT^B3njm>cN3 zbDbu|xO~ng_g^5SW_vUK1hW)2ZsI3yQqfP&5+B@#V-5gw^V5S}Iq>3i4k@p=&_awY z3zL@oUO0Mk^N1uo5xPeNJ(4BtK0k1eTOiCoCBC-w(e<3RC&EBZRzQXP6lY83hp_dr zSN4`#;!ju-juLCs=DEdZN#eTN}wgsd8baBPF&m&-2NrnlE~V*TlU zK3s8RYTF<$8B?}D#)6WMqh(;oF;$iE=SNWn|IR z$YdZWshgDt0U>`6z= z1oRZ2tJx}A0?7TuvC<^C$2@)6;7E?{w=^8mFXlNz#RmsxPo@HhxO4@2cS+Y`z@PxKF4$(mYaQq|Rx#2Kk_q%)pl5z{U1o!JJ3!KaB`H$&W3!E#2 zk}YcDG|j9=$?A)mIMcy5e{AB+68_=LW=E`5CSNop1xEpQV;2CIyjecnY`Y?F>$qr-(zz3c5vP$cU4E{DUusHIV*^0A`@W& zSff9L^tOpvIr`|6og7{d{nj~3kL~O{!tWEmb1;L{{Z4ScN-vosjK9NM*gf0iR<hN0UeVz$)ghZ7BtkiTjs?$v(&~QfLcHr|-Ktw}+*AP=?3#KTmPKV*g%uD(85b z9(}5F98cw^I?e2F7ig<(dVzMkJB@f%)ZGCm&(L>wch(T(wQmpSE|RxyP?4dw5!Vx_LTyy7N(voHn3ll8Iw}=r7N3 z`ku1`OcV*pR2ODnmas7~1;Jgc%n|%PE4sqr1p!TXfp)h9Ela!@1Dye4{mU1xYT~Cf zgcJSTnNEE?Prr7i)6$+-Q{OR1)#EXIc|(RB!9&ER5`u zAwr;Z3!KwHU*6ZrZ}8!kGGSA(?xq9Cq-^hK<|hD3xaBO+o&7rPZ09(;Bu6jr>(o0} zN*UEl(T6s_awyAbf&X8X1^}Vv^!!`eL&s636kLDLb{;#>0uXeE{GRm*=kFOk^K9pz zEM?Pkocsj7w&5CkmCtNMgQ=<~z<|b3LJdKF4WT z&(zU%kf{Sop(xm|q#(1Km}DGxt~0U4lhOp%b^d$Fm)f|?rRYuaXDH&hC*$9q%kDwD zJEg$Q92*5^lhvZOP+J1PQ`**|2du9+-?`YDr)-}lF0kO$z%wIn_qxe9C~-K(eu zGkTdkphg0Y=(8?%syYU`>0)Q5yhU+&#OqdWJuwPl@N|9+upN4c)Sk*i}xwN(rTiN$Z*q<=pW-C|OcO8{cr1^Nfi;7sP;B4x5n`6&UepD*D`` z*EkKb5=|#$oOi&OqnpZabz+ahXuIj%!qSg?-)>)2Q?zPtf zs&PSd9hga}zVtdr3`Cy0&RJG}1yTaTiir_`G$hJuc~~9ARCKJqXP`4QB=@y0`7+!{ zHyq@2%r)0{I)s~u#*uPyYPj;*E!PinPVmngm4Ic7r3L z2lAy9U#Cj_^G5b1IQm;}N{cVjwf4`>{H*4J9_( zz~~G5Z$q5?^nVMA_isIKn9~@7mdl78;Y|>clWa60a-uu+x*^VIv>d4;^Ylk_p#xq9n?c|JnR-Ae0>dynB zH|7EF8#{IO-A;o>FPKPF0TCz~{1mk*Z!V?<;{|=&qkG+Lu#!P{JAVV+-FmlkjbH*j zhC3p_u5qtHcgv9FvG+TB`RcwA&iJFuSCA-&eQX*{et_ysX25y^J4a*apgSaVJJs+ICm=3D9G$ z?haH^p$_geGf2 zP2c-6R(56BRgbk|HA_)uB+gENs=A7@A6z$MW-b(AflKZzzO)lrh3J!!wV+BCAR$85 z(p1zoD<(#KW2qj0wfwYa@H3ae`%V|_P*=V5L2#r>^YJ_r%*aH3mt7K5`oM!u8;~Yp z`Rbz{a$07hx_*GlBu;z@g*{Y$@!D5ZFw_9?+q8a66@ZWXjuQ-dDM?THu+Bm?|m$8U*m0`vTFn*^KW1r{4b#?sQCo zoL{K+Rf3#K)jl7L2pIZ(g}y2vC#k%5;$-J}YmweP*?9uX)`uT)uIB0cN8m%eryqIL z`8!E;3YY17`u-`-Ho!B?i7xROQ=NQ)O|L!XtR+uRGdu}E_LBhW_*IWPj|baMClr`> ztha&02ben~qlGsbUV@)-7%F%CQCm3|@p@Kes?exoOY@u- zM;SE3D-pLy5=x_ce%OScM1FTLEXv{-K}cW*H+oR3`A$8NKH1{in;S4AC4K5R-)VRS z(gUMUQb^pu0I-aH4K)NH=N%5dP=%(mgMF$H{U=d8PfCVF+<2JSg&C2{6{Jl7O(1%O zj?Z_*BCPTA;9j*~?&+Jm2uD9CeQzCj^=19U3(n^BwL4k)+412EoQP$8r^hXXR4UhB zUz*uizrN79&gTUQ|5cP@%lyKpyy%>c+aNu5k<+Lqszab1X<`i%^6>o;ou|tdITxO= zs&obTj9kFdr7C@=EX+jlECVwNrXgkvl0*{Bs4&c^a~C@so6Qqy7Zgy!`uYv9O7f%Y zAmf)x6z-?R&wA+yeF0fd_T}xR|Mz`qtsi>XdF~e-0u4hHd02<^rOTYNqCbiE*Bu~M z-b%r)p4Vl|V0-P1eeM_0MvxS*M68-iwrboqXw({(*H?1cHo_!5hMy~3GbCB#TZfeG#c^gD~i zNqSnZ{n8P;jc|q2A@05ke@VP?nmr<7q-3eFUpZYVweBk@&uRLrubdlohu7ggtka8M zcOHXda=|L+dXm>xIj57Vz&mMi@Bv9R?+O#_0c8DuT!H9mvG z84?D1%mP1M0YZXY*mvJ|E<+SQW@ONMr@tT5NEp)YDGwdyidGq8)H<#<54LM>j5!0w;y7(!N9f* zgd5O)@VCV9KyWJHr}UWR#1%P{MBwX4kP*hPkmN6}qQD|~Yp55`Sb~HA$l4ke$|-IO z1`D1Vh07x@X%amW@`~rY%4Z>ufpLlVqOjO1IppO08GJ-V-(eDE&w*@0ou&%K(&gJA zy^_hy^e3}NG2F5LYBrNo_4|_vQDK7S2-+l72fIr63UDgPpz*c_BJ<-ofUmJwmySov zBsOi^L9sN!Jq6M-&&bV)7tVh-J~4v@uC*N)E=UvPFA~(73fxvFW?+C* zz)3Je21kt%XX@~gV^^09oLGJ2x^@${CX^kYhGNIuAI6!eA;DZnNGHalyF-3t?bvwtoas;STN5?`Pp;jLMw z8?SLrxlCFUo`ScQU*-?>$JmYjC!9u8P4d&2n9%bvWu<`0HoxRz+TnNT46#h|iZDy? zW-Zk#);Mh&`7cbhRqFE_rYF(2oVq9bWjJ{Z02{Mg3UJ3pTPw+z@i7?sg@3Fe-qNLV zT(IBp??cI&wM1X@mUDr%R4;$aX{q?egA5IL70dMAx16G5m+(b?P@ovqgJf{x{Pv^` z&azAaGlv~cu6e^Beb(E~@;s@j!Y^ueu<1o+fO-~~wvMuC;(~Xeg3I;vcbsmB2ku{8|LfIe7I;Gl=M3N z**b98a-H$6Q}$ctG?l!WN}RjWoJL|+{}f)tB>7W$*{h3vm*>^ z@_mN&)Xjeg!@6vfbM~(fDB-E&*9TOV7*Lfl`VK@AUP*+FDn3Mn^_XxlVGxQ$R&K)0 zi12kxmUu&{QrB5Wa4}-COs&kAIEbm_`d58*g!atL3-`YdZON5Mr;O(lqw#JY68=Vz z2>eR5Ue#CPZ$$Uon2Q2PWWOKXw+SFEW_BZnhNwgNR|PFXb&Ux$#+B8FgqL;V8A*?lefVK>2p+@OC!o zF5PmwQ;54sE;7OAVg1MtVdN#8-ta^K6F*yQ8*7P5D_4l7RdweU5m~qa)6(zGKa~7q{uALynrY?a*h=XO*tuD?WF2(1uh~*`(gK)1bAqG_11dv>$DNF8NOF{R!BrtD(_x z(CD+R^fe-wF+%jl`l@fRK&_P3FZEw-_{PZUOLseCS`!F|$V0yHM9u{mJ&P&=7D4Le zov{KjQvCh;@^1}jLVmxJYDIM4SXM)jg{A@ZZVS7hI3!B{31r$Ev0RsY=hP|w0K;72 zS~Zmzq1G1@q3V4f*ds)#dl-Gt(@k;m=YX;lPJHW*hYwG)VlxT{A z-9kz(x)&E;hKp%Rm_IxhG$qKZ6b^aw?#)aV5lm;fF8GhL)tag8J09BOnv@dbR4$mpY}Re6?~r#@twv0dHmmyk#~hR*i>4T zXf3f?(V6<5ea=qtr|K}V#-D0bE=4;=BK}k}#3$;A{SZoT>ksxjbMc;f{{dmYm&U*T zUfAq2<{9V9u=<6MZ-n znTVUu=*ETV%!V}6iyzY5&J6tcbazlvh1HOlh<_+FFT*_a@mcOW>BFBlr_IfF2dD3m2sMZF9og<;+HW6q z3+iuy=OU*=cpQZw&m^rtHbT+u`sS$HQNWcj5hMUukcE>SQmU8nr8MwK)a_Jfi(!P= z=pKW}TF{%|j4(;s)o?qARkG+?f%T^eE8U7N#WF|_xi~u~OqIE0lK5;W1E)FAHZeZ;W<&ybIz?nn4w{nR46LvgyGgJ{KpOml<6qf2}x zAF$Vn&x@B~QD!g&VHTl%2~EU!@gU1N2L&w;;EfUFwSI3t*c#};-|eZan{Sp{{BTlG zBz{S!PbSp8P!z>z`O&tJB5ZADV9)TRcCR!@#&DoO(oZ7nd3w_aDj3`PGuZN&h?&Tj zU3*DfTu}}zP$ux7bQgM0m+7j6H_hhzD$>2g1*ji3f^g7Ffo!4i(b_CTn26MQ_t$;l7L< z`I}!IlSdok#lZ*X#0NFxsZ@b?Pa!_j2-Ql0z#&^!ELdg5I?xZ6?B?#I$wjNzX9(qC zzzo894xP!Crl|ncN#pQ|DGKVR7um-Zi>|48SA{r4!90c%H3!RZG=#AMqd_Qfk!Xl0 zUzT}CYMG&9n2mIAysWbcWx#m&K&H_DC0-TGweVeruRysL=MKWq(QnjnJBinPwo*s{ z4ptucDyZo;%L6K-_0A}uguOC~B-lr&?M{rJUDM65B9o0(poJq9z}VHb+RCM0iOB99S4Z~J)*6M1arRJp~8JT_>^gX1ILj>XTW zw80p;>ioQkexAb6l}rddUxj44?w8}%;d1zEj@z_(*}WD%fY~4vi6lz+c^%2K{ZGpD zd_Jiwx@y7|GV&f&M|z+XXcJrM;~aO6@s`4c5%E1Y@(ZRGF&Lhx?LHC|Lqufl2}8t~ zzPg^lFm%@rIQLU`3Y+9|wEF!3F8!Q93Bh#sWv)i&hEGB2waslfycv55#(R zQ$6=uhI2}NS8N*>)^}Hu+}^+y+s6G3+;gpE@ze9%=E0%`kDwiBb_ACMRgKp?Cmf*3 zehzvilS~PHd3(3DJv?0xY2uEpy@}(=IR!+H5|IELtYCDw9@fM4YOpI{!MhAZn9tsf zoqBs4?go_gPIDXBODF01XPU>$Te-FE9eee+9n9mpv)v+l&m=vrty@Il%R9Px_UK1Z zJ#*)CJGSfOUZ^TQgxo_~#Aux!)M*bAR|iH~eXk*ZStyx9>OqPqwe_*X2(+sP;X- zv84C>(MzfaH2ZT#`S5Ry^5H*tl<)irNUm=F7x!OBgtqtp!K2I5AGH1dy@v6Lm$eSUkSarV= zrl|FjzV9mc*|bM+$}l^A{M9aYmHzKju5rsMZRO(F%4JuY#zCD58{B~k#&08xwv(@O z#WZgDb*`9geR7@Ksn$dxXL0I^)E31D_%6|j`sjgfr=Ie{*Fivwj0dQ8sNapL;^(rd8$Pm+~`-QI)&&AQ(G zJjv?+;d+?0rTU^9Tw{@!;PQ{S!QDvi0AEhuHg5Wo7^TO^KWv$P7_}HA${1V zyF{{dNLTrk%YOBZZh7U$7vJJOZNHbUznzSzvi{$x_r3U*zq`F)M(d*6U2)QJ&QP~Y zhH!f=;?ztW>NagfHdn=} zer+Tbd=ahs#dEhMzmVq7yB)Ul&${?_w;5j7lX601x6X$+(GcFaWT(2J-@M(O#h0`0 zaP#!=J6(wkG2@?Zmq=m|(z8PC4R>=U-r=fWjONf0Mnj%InhtlkxA;}J_|<`=3J36N{np@;z51UG&X`k}`88^YfoO?ojAeQJ zZdd&6{Ws&`8-MVB?$r%w8Ot`ou=GVE-1>E%?lllE zx8BJ^W*20QD8(ehYGZxzv>r7AiOchP-UxTviGKL2(envHb-(CrPT`l=lpv|ejJ1-H z@9jxuFkR~c-f$DnN{=4t&P|t}!<6<$xp~-64jAR$9^GxussOe%e!*R%-27s4;c<#M zWwXf@haHB4OX~U@+;+ZPTQrV+xpt*=TvU}kqqZzs%${g8NB^T#;)~q~w~d6`#3J@3 z=8Gh6to$jFKj~{m1LFRpAAG=V>l_LjHaZaZ^#|P61>YG@@uFgifM$}>OYJJkbZISv zy-=#*!ul)5VXV z^dUOe72kgyXrOr%*d)DUyYB zmj~U#QznbgLS#wECJ-T14)*ci9IV{ne(mDgwe&%^Ve@jrC(B&g-nV!=^xpA3LEHR@ zK7eT}`TQ!?Up(k`3~zCfCs@(smC12w$#}%c2UIXF z7lYF(^CJ@Hh^X&!bVZ|Ie*N!f3E(L6Ym&tL{*Pu!CIy!^=^`SQKnqo7JGD;Sd%Ejw zp?aOn#3FnZNn65-OOSH$a%?2DzsM9=DUwCK@G*BT?i8+`=8E$LJ}XcT@l3j zg7P-MYi{I~>-}5$xan>mN|a1D<)5D}x()iP>F$?ViGm9f1wVhB>!4i6o^ZPL^$50+qE@NXpK^<`s@g_&9i+c{9Ps>Wy?^k(>-~e( zdVd5nt7PvhRX!_w|KFHdrFx%K>b!TRJJhOWT=oeH0)p@R^IIS1_RgyECYXAo+dbzN z8ZUp#^nmBwE#jB1Mzjo5CZRXy>APoF=3PCb0YKDCkZcR*yGOG{cFZ@$o#)+uRMM%wPnU%+xI4s+3!+$Y=}rs<@o&Uy1C(~hq7U1}MjsF$)} zKj}sHlB}s<838~Do>tkbiyL=QT&WA66fXm&P8t5)~nDRUh-ex{gPXfX`G^nxK!*e^pO5`UEO>M zt6QOuU&3J-ug_n?A+FGKmbjlK7mGVmsOj&QR=XZsyzFi?*7Gv+OgVU-09etm;kql* zCoXd*^VJ8-%$@t=GWT8C9T7Yeu3zqc!lJxb=AIKN^SwJ``NdhPUG6SP7J9$joso7w znCW_5yuv*%@pID(cXs0E=vUlp%+J|+|0{0OL~d-Qdt3%85hldV#Vg$_{37ucE8Tgi z-8b|#vz?~C25~M}LSnO>Sm8EvR|Sba5DT&iwY5r$>E^54t1y_yJD8a8>W5dkQyW)I zUE#Z=7tJ*WEt+fSXQ0Q@${4x#eZ&1r<^sQv7c*J&-*9`{lcwq)-f$a9b!+|V^51kP z+H0rk#c#R=wZRF+p3fI}iJZQH4LzsaRNWtXnG+HF>l5QN8kL3o&6C4a5;f0gQ& zmbgvzovYo2$p!4V#w_5ZHPsfd=e60*^p|Vg=NO}Y%dkT(dfP14U2nS|liB~BYULkz z#~?9%*BTwSp?>oCTDO#wW9od%@T5 zc-KAIo?`2l-*x|PE!W4ccOOGz_}%rWrA^UIHsF+Q%_gwe_4>38=-y4ygEqKVq29K3 zgL|_W*Prsfd$skBe&&7mtnfDI2e3fq$HO0i z3gW8q@0;Dm>EpLk{cQcnW_OvMxCupy=M(Rr_ur40!@-&z=Y3B*Ie9xjHgnSIOZOeU zX)7LyDmP5wHn%m8fG*MI(OZ|fxgoQ2Zr|p9!K_@p-JihW*s;Mxx;$6q>jm50DZD!C z6Sslyd%`G_2zsB;CDFq^F>HlS^;93d?GvL(cv8GX-biuu(B3W-;A0E57L0x$Z}b_;fmjH~Fa6y8!O9Z7P7~V+4KglcV%?VE8+!xKAV|Kr|nL1BHe$35d1F;DfeBe@Dh z#Uvu4VJGt+$;yJp*ez%6yFNA8Me$csO{g+{*C^bU8Cg1u9 zWwOR``wcy`e7{=+JK3-w617-~T8u8bq4bh9_+*OdfBges3OnKtA8;?TFecpdJxjVk z-}!@kqqQLZ)eoR($;0#TL34QG2i*fz&ds8qO!J@rWL~;IyN{;N_*_nA{JEdqW9Ds6vQOw^Vz$4j)*mbEe@i*gm2;Q#Z0z=d*oYf-SrRs?4*YF@ytQCwK`` zwFJeMsv!uh=(dn7Vzm~@t>hy_x9MkXRoL6`5M&rW4}o)suUB{%@DP5Z>$FhWbrzlj zqh~+p))h{IIlaMZoP=P4lOPIThLdnXP_?b(B#aHJ7BxOLsr1Wh* zFM<72P_$PFO>w$!I1mzFCdf*l%q)Pz%!ijKett+DfyHodNL^f5=>;tisUh`MdZ z)|)OI23`y9FI(2zS5XFrhJ)kfY3jtxFQo#X5{f>lopg0#(+a7F0}62zeNuiWBDG4j z(BQ%rKcEMut32y~zCT?xvG;zeXQiu-jpx-GE`hXcFdoqh4R{4Yks2fTR)MmobTC7; zs`Zo~x!jB1!kcU?%;!C&yJe`}xnIHcfi^YmNn*O}?f+CinW2ud4}7ZM$x!t=ejy$5 zxld8^+f26=eM0g9&eCb~n+p#Ut&~A|JQh~n?Y29f;CejPn*>ADXJF10)@TcFZ7O4B zH3^ZP;Hnh=bcD}j)nB-(zCFs(=}JADK4T?Sjfuag)IrOdrC-QYh%95+%N`gt&XH$Hi9>o%mg;3ar%%jQ=OYvm=$WpUpOjrc zn2zbpqHHz2_D=?phyE3R&hQ|(D!3c2kE+IeHzum?|K)cbYpC;psSnjq&9DStSVOHx zA#!9*b)zk@PziUM8?RYg-6hdQ^){VWHv_IW+v6KOuZ}8_%i`KdR0I)9f5Qt%tf= zdo9%9Byq6%RMjb7(Sj9E<{av!dg_OY)TDZ^_4L#&y&J$g!P_qe-X!)dh4!x3bl0O) zy&mRu%}4+VyJ;ahIz4i4d6z!da0NaKfe?KY~jHBt9# zqZ*J5ZlfBd`7a)7qq>>tsrQTNY50rj$@|6hG&pQ}>bI5Y(O0!qOVT$F2Zh+9o3&FX zsXd3v0^ULXaHZYQPF-FtV^cfT|M)k*r-X^+E380Riv|@A76t;|I$n`ta3FA2W_6i;~`waYekM>NB1ZUaIwaadR_L_|T~Hcl3TDn*S#ZmL+ZU_psjVL<`JsYBwKSeL?M z$D4#0gMgxmx9`G%XhXb=*8{O*iuxp&;e<=)5>b+RVA653NYu>rkl{-Q5Rdt;F2u=m zl2_cm7$Xnv^NX;g1-CyNlN~Sj2H{!9!W?yde<$3_i#X6jFyI*?2OFC9 zc2l~75r!bxJmvULQJ!QtGInwc{g>EVC3xZxN(!?g^GPcbWJP&?LZ)DzP@;@pj4DkP z#^%xs7t0gV>|+V?7O^5!X*f{`olcGno$;#4OcRP)X6C-9v$cRZQM+{FuVL@eb zWyG{ABPKcGh=C1OYA}WZ=Tw^kLtU^{Wx$lF959=AaIwFkelcRYj@JwU*D`&~h-gNn zCc-fugE%|fTLJ$#3TWrZCH5s~07Q;IHgizrNF`YTQft12xCQ-`^Q6mWx*07hJTl#$ z#q3ivQZX5lIc3tk+#;rlh5%Aqm1F`)fSD$sF8d?kj<3}){a2I+W!GMhQ6q{R%m6tw}%PB!>3DA&#D ztfwqLehdQwUnrIIEV$8RYhVRky}fkQ;WY*Lq+KuX^yqKt)0ds#PC z$4b-hbyKGsFN9JS=e~l91jjVMGcbl>r>PR zNi-b9yxe+MkLiwaZJE#QH<b-kiJ!jbml=L0qz=()Bv-%;SB$sq zJ!sHx#iU+}u&x3m1O!cgNndgr=;}*)=xM4ji$0_oR(y%E*Pe5l8Uo@M>!TW9;a4r~ zjgXp+rwiH`M05*^ zU*UYQAou48cq{W@`nS7}daKrO;WK}RI!pje&NC$&N839#>l4pZ*HFuhGu5rle?dj2Xvf^7 zbsi?-LPQp3|63{UEQT#RG5YWLKl&;qto(cWsk19(Kj-4OZhJq4U-VM_{@FgIozUDc zJo9GfsF8~86!65MURk<6p`Plk?Q_+Yd~nUV>ZsNtdN>%evx{wUwlI%1l^CPUCmx%C zwuAFC4A4*W-h1{vSC?;8b&IpT5SJP;P?Os&hUKf^nM63q=cdAA7Z{5zhbxmLXCR2D z%~g;e<@6%$HW>qy!bgZEH(24%;ZU&_6oOtv^*7KpU$HXs`Hbn zL$NOS0;_g2+v3_4cD#ICWY4tnIpc-+mCc7?Wr8&SYI%VgG9Id9yV5kfa=LUk0KPfN zK#w4iXXJeW*6P434=1`@tP%@Ha-$ClugNr8Gtnq0KZ-8gf&O3sCAI*33Jb2l92MEO z2tdyAVQZzYH)gTO76eoR(1Y6X9hXbuJ7&7b9&KJm{13;c-r0aHTcS^31>BDWQb$yY z96=C-cWQHAg_9`7^wejj#s0_z-q7^2dTx4=ZF9i#DK}x`? zjVoT)H$-3!Z|9>&u9GDs3k>f)e{42P&_-qKvh5}=aqPQX$thwUY z3(+6ha%3318Bhtkp~%jWZDf!H@%~PGviU>@N4!nT36<`dsQ2`dc`>w=SIpR;UWBIa z+rW&?^x@`wS`aFbIbcR`h5#-i3~scGSkEaV5JMXSYMC4;Bql?E0#8D6;ptR96`ByG z8z+|KXVRS_J1pHajB3%CDQuAFCqTt)I??hQEwvM+O@DhHjJuz7_W9~`l1t8q(^jru zKVO}ZHRaoKpWj|+cLcH)T%bpJHb>0_ZGIP{}#(>IDNUk z^J&9?2qulV%L6JOyu@6wD=$#(I&dM#by+EW#tYLI<;<-3gW&85S8mo`$>6FIj8=w* zT)NeTs`)Vq_F?J@@GZ5o4Dk0hujXR-l55G{oh5EwT7pZQMks-6aELgHn z>ggA$zrbAHcM-hYa-Davx|n3>#p==GVPFAFePr@TO0rN3GnkYB$zbYChYU%YDx?bRV(x0z-}av69trfx zwAkJmf(V52A(0Iij-U9`NZl$z(hIBr_ZD(lVrxO9fLhVF|4FyZZnld-M633vZ-~H; z;!Of?4FS3(d|WVkP{c4>SujN<;J|m=Wbg}O90`{s%;p8{Kw*Z?CZ+|}qB+ZQ*1!Y! zXQ&)Oxwkl=hPD?}hY_%`Aw8lpWkRDMX#1W61Zpag>?t9%qxmj*E`=gv%#z@>LLf|n zG?yDU&5WtQs@g9ij_=}ooz6F%w$E}NEwr2y;4mX>UDMCmpd zR_Zjeo5@KYL*~H1{s+@^tY&5pebXhXdFU%9PjBz9n(MMllq-*}c~s*kmia@J5X_gT zW93y?Ue(sc{Z$QpNq@h1Mk=p?6iH7#)~G*mLaGYeC=7Y`EW;9nzQHphjbARB;1Rrc&+QsBUWTlbjrOcFZ%xEWeuOv#|^mTXUn2)*@jO zX*}mmHl0Chcg(4Z)<$jutz9)ocezP*q2k+aQm>$`cH$7EGCTEUlI+s=3{lq@ch7*x z6jT$$_TUhB&ZWA?&FT*N_R7ubO5?sRU{C@f$DrK-Lm6s=V?_lnZL^bn2+Xy_jl5@f z4wOk+qrJUU(#iLWUz0`h6Q{NdB>k7knkPT`YL=vYHB-`>?d=(omY5>bO~?bF@f68> z@jj_(lKkYe@sjcx$`=rQ?d{Q$?lwh6B#I1|^h=XflE^w}`Cm!M0cmWHq#v3hyX7b3 zLWQL7o2*@ygj}Gp9g;_5+azUVnuc7D&oh^CS%cmS)Kh|FfAUk2#X`dGpzH`N?NfOxigxNzz>A zN76@3k+Jd<&^%hwNhWKA{N$_QChZ(3krc+^9VDYniJ|;r!HRDoy~pI-#4ox$m;{-^ zwIn#wy^3TXN@?P_*382tB)d(<1^lA?xg?bDN5V3lPO{Cs?&ZHemELN+! zKt~dGdGT?i>&<)Ic*!VQk*qTrh5Vv{rX+k2BVh!2B(Ip)!mu>l8RB!847gP00$eMc zVsAQEi_k~5x#V~>aFTD<;$s%cvw%_7pBC51=3%U>Bqe_%X$(ia8zklZX35{cS4PDg z1dO|Z9O83=ut=0ql2AcUqhuOQ6r(JoH7_@=?|)hFVwOkVa?hGuORl(No=`Qs1(g5;NQ4%RMZ`a_BUmqT z3rSpFL&XZQn+KjCh(0ak%W!3G&jeKj{pD&k-E+YoSU17#s9sUXJP7}7l`R)2dsCuS zs8_;vR zVp{YQ{r6kdNi8Z6fJLJF=p%et-zO%EaGcM$mQJt`Dmp@Mx>em-YXqWpv-f0OknqrP zJ@6muBvgQ&`G=B#yIUpMuWJuQM7&=Q9;)iqns1z06{MXIklPzOfH&vsNkdgjK3X}{ zl>T8TqVD~=&uz#qC+que7jIMNgN$a~t~w+X$ok)or1x+IvabIGd(RpZL~@We z&~szKLByaNbO#9A1A6M6s=hvShjQybK=gDGDT1$M3Cbp=L>~nAp+oLa_3RI-6pkIS zu;BHA)OvTvb5LqNRt6+^0qrw6Tit=&XFq6>wd35(Ov41v{J;l>-DO;)V##c zJB!sO|7ZM?67`=@TDcHK`rLcfwETFlSiL8NUW&wOcMHUFO0fj?W)dl0_`=;JL|)E) zYLgY$-`oe@@~UofzdFWxHGbaxYO$R*la`jnb4Ee#B!1uXfI8XATGCTw0+1*0fzk6^ zJ6a9OSkjZL87-=AV^nu*vmP-9Y3#Q6>@n&%%i0|OLpYqEXstf;yGt$_ZwuqbC?d?ZpYE>W?O<9=t1-Sdu8%wy0c5dIJ6d$UMwk=oKJxMEd>YRJ!!NetW7RCHG8K5_voSF(r|=7e1yWRQHz0)C(qp zrw4)P!$9N@0#jy8Q!}ioy7zQ;-Bdl;Pag7<=l$e8l47w2NcV$v3NK^Ezmg8u%u)9i z9rnETu;*QeJ(oOO{rgF&XR&a=nJCZbxBIOEH?Q%t4^T}aJI$z$nZnyDb3<}O{j#eh zH_cSxhxs#Q7f7xID@6(>7ouNwdLmbEeq0q@In{+(maazUAhHh%@P}PelL#;yU}tmD z8tW`s2Kl!#6Mj>`X6>bv;D{Kd`s{CELF0Hn@sP$9y7DD`?Gx$&0P&tDRJ&sT4LYcSF@K&8piaO;8H z#1t$uhO3{Pp%#nVTZ0iqUvOd(-|a0)%I)p#Pobmg*9|6NiznPQPqF)6ihuJIG*>+Yl$tYF)g^gtuDa-qiqB{m zr-Bm3NVYs2yj@a3TWCU(R%DPgCBZ3$aX1KP{u@y`mB>N})dHx(fxZ~JFMjH*Hzu{CJRs!NMSw3Fz0#gPWap6LRf?j;- zLiRjBsaIUxZI#DY$5HU(an~Ysdfi``b8UW`5gv;7S*$wR6dt}ry=fO44qT-**hX=n ze!MqU48o-|Fo%{}zJNv+R(Xpk;74$j=ZLo|^;|vZ6`u;ySLFl23Uka7BFpdz1gpD# zYZ;nK({$l-)jyYQo*=@FYzdjV>_J>ZPF=1Rz@id~I4u@Vm;_=n+uC5H@z-8cjx6EY*VVE1h6$RQd{5krt0SV%M|>!YJe#y9<%Puc}jYk@TiHm(WJ}{UAbxc(3@P{)AV0ftFzKk z!nJYBIcv4b^=Jt|&E+QahA0yyXDi8CW)gWP%S__4TxRTqYYpi2C5dTkRJ)uJWGN^x zpB7EtKVFsvYQzVV_|?E&WVO+Bc~arA!5d#*T7Lq zn4g5W^bA&n^I#F;G5#msW{u|Q)o-)JqxIGAa70JP-+f0t5aOf`*Z?DXi+*W?Y6BnZ ziw$6zq^WpeM^uJ-E5n^kRg%-qdjQf2Y1 zT9I2V>mhyRHq{*LV$?Q+LRFUkDN$6<{6wuU`i==kEe#9`AP?ODS{ao*XjxzRGnuDn ze5!7$Qe3Y5!;4p`LZ7%pwM+(*x?zXvT;n@om>Nt$K8tdQ7Vl8qicL1umO2^AFve`L z;l*&)TUo-N1@ddS{26^We|E^PS@KIlx0)!jo0hcRGTHlKxsVKUsQXr+ti3<}dIe}fyw+}Tc=n6;EtzF{ z@wbpAW%|&!YGG@zQy=>fjb=l{@JA$ENcMn*)pO(9zf;-bFX-Dn%4Q|;&E*(MZv(tq&r}K>2(Lx0JX!&g+Z<{ur6e9mHjZD)d{}-u#>L=AYvOyJymav zcm0{nS^C1C)$=5gLtMx^b5C7k76$r5bc72y0>21;ewjw>E{VuBE7<8;_ahdbo_+TVt+Q_jmU9^t#C6?qMAvX-fu+~^XYs$at6sE zJJOY;YcSHqJ~&0+6^yjx@!4Rc&rt^-JC#*I=5%Z8fcz#By+Pgvd1*rKAx-t*l|c^e z*NsDwzI=9LC~`~aL3kDV;`E5P3Q13koM%0#`=>>E^7K?%ykk&ehHM`lDsn}oeruSG({2uOkg_BBe5cn^G*5ou?S*r{{FkxLS>PC>?^ z+S{j&t$jq`t=X&%^{3%TTeeh=6X_^=(6&Jl;mhj_oX9!ttKCn4p6x3F`DxxkwvE6` zyLDi<*@bmMI)Yx^^V>L0sSmQW;RyqKo|PNZlzr za&*0sm^BzOfl_^8JBB@R04J|YvLnaVNqq((QCW|EBRg`gHBvW6;T!jjU9# zT4QM4RB1ZUuY1hx=SS?KI!KlrQAVi#31A6TnNrWXrFDQ_OpZpn4=|!~qXovX$VA0O zh!#WXCAGw4l7vc2X^IKAm#P>_2wN5rwR=0y6A*S>ku=k){{D8$=1yq4g=uu7p;%3+PXZL*2kLG3u^JqQu&RW zT2LW7rDkMOn+?J**nvB}4K{89cjGs}YvWB9l{7;V>`p1Ctu?>mZmr1eR^CHF56uq& z>LDMt2ND*#&<}cg4sfefZ^?;>K~Q?_$S^U+V(lzFWPP}{ADhVAHVzjGQLjXjZ2|AS zVDuxsv^L-Zi=x_*_9WSLIMAiKYn_M)2M7Aect2TICvvWDU&R6=BtU*|01vCMwWN>H#Je3Pz3Q@vG63RvJu%7gQ{hiL*dG#iCqK>k<)TFPCNC4074$l&6MK7;t$8VrH!?5njYF9(#C%qIl|lJ4I(G_Z^6WW@omk# z$jScO&=KBVmlrw7e+#_&#kb4zBAsp8|1mFeLi#E^Z$#I}yEFt8wRvRu3QmKN{cwzc z4Sbr6-H-r{l9d9Ddj8vy+Sw^+ZV0=ePa9=6j`Ynm02uOvE@BscUE|0d4a+tJfTxT| z4mN*6-oJMlYy_>Nue2SDh?KoONi78Uplt9DS_Z?Nf}GHptuK8vGgm!S<%4OlNbXT3 zZy#Rg^OZ$xFsLr+b54eIa^*E ze(`j9%#=qt*xqb;G%dlku|&}T2@Dty`U++F@YX+Dp?Bm*?#-OceHlxeZ3Kv8^{oYw z!W^P$fXL=05!smGzdv6PIU9?|v?h@ywSZ1C`+_6;>8`+p`H{L^wum+wYPq&oMvLAI zJMns@QSc*YrpYM~S%j=#wm+gC2zVd+LM^Mkf(2N z7FiB4-MM+>e(_LaP-O#Elc+Mx4gF#BhR~!>Q4MpMeoK#eqp2&pMO*s5ew9jMUgjKJ_u?qT9)3}eegwn zC4JjN9ThXH5!JHx>AQ}K^agoebre`%na*z+=|*x@%g9;G*NZJ9Cz0%J8M%Yx=2np& zB#T?Iid%G~HQ1uoBU(r9vF{1#%%dZn(Hl_dX|$ltBJtA?(Lf^+19Uo~fHEeAkshv5J zO&#+_NNGGsS{*p;rTX{|k=tjvF@TVwhKGx2>Q^I#jxoUWy%VKzk zisC@@Ey(G#H4ZPa)C*4@`vOG1ezykPSzN_OU>FgzEBb=k+8un(TGi@gth)v3_LD&* zyrs%dc4C)TRT1E5@R(#uAOrjfvzT9HI0e)Gg1YDwC)W4{HYk`H5WJ)bKTBKurDIpB$){j3tX07tPR}#&ahSI{7$jWu z$3bw>KR5aUPq~K`8cqxZVFp#a1KZn#qm9w1t#_yMV_E!|75!3;@8Q$|uQj6ww$wrO zTMy?NEa@v;XDT+zAFk8Pp6pjgU8lY}t*29AR+lkUR=KA-t*x2rR8C9nExAWQkq zoUXD#DPWi|_WApKY5V*l%p2tisCyFj0(JwzBxrkMe~7uXwO1&HlvV>dX&8nDlpyy7 z&8Cyi>;cndNpR$^$Q@myd|Ze~|&T4*_$?w9UUX{xw6GA@Rm zX-}&d?qHx(h)ZrqxJkh{lcwdq7=+QpNt6~5X9Ej)9e|!a)fLE`Gy4mZiqw-ob+Ys? zddbFB^--@+?P$ojjij@+7|QbIBW4+YR%DWl($Cp?h=kdCqH#TOY%}&-RSJ7gy9p#W zpXCHBEb;CAocBvtX4^StN$_m4P?3)-zy`?=N`MEE8KO*E&=bi4Pb1j#SQ`iL!45YJ zyL7Gb#jNPCwL!R`gIs~)hBhyd_|hmljBN*t7jj2fY7uTpQeeO`zF0lDfAUL!u(2G{ z4WR_7O<;RRH5?UgRGLl2pf9e)q1eWQu#G8Av1y?{o_0aBoB;%*HVJ05^oqrP=VU?# z!^5QUF2Dc`uUg>Jl3*a@wtzW0)P>v7<3JH;4lGyNK*4mAQvtaJ1^_pKM7UH7sEp}# zfI2HpUXej$h)#d>9u3m~5K|2b;#EK)7`b!ovRpq71=p*V2hCw!2V+5wUsZW5p7a(E zyTD<7goed4R$cYvvYhw@UJk)V%4!+YOixrj;eZM;;1V>g5Yvz0FF=aM z*MN)M^em(iF~-}Zv3J#?PyMY*Ep8~T3vSd?XQ0bF*bY5- z9thVatLD8;e2QRQ?2!9O_2g$>Qtde(MDpfj(+ixg)Y@Ekq4PMjulp}_u7@U7&Ai3= z5|`n(IxCaEUgFdXfa<*QPUmAQx&12VHM{eSSMibd?lxSE1SRaQgi|p^d$4P^eSXt^p2Wnlav0~9cJZBqsShKx6eI}IrOCZFIg2=;GX^@( zqD~BGLGrRePQR*AO=48Hdjnb;=scMrirTyDaXJ1_j^PtMl*hy>YIJXED9^65Z3=2B zamP-^M z#}dV>{8q3On;pC{j$?3h=+c0DqsGAvMqcj(^~waNS-1dXBQgusrU^I@6+1+g-Ir6l zU!FM=0?&-&K2=4G!bhD5?_jcvN-}bFEi~k1v^eiUv8lVI`t2pBVMp#h3&x*uu(<^I zaJC`m$U}A!nGQT6zLgPvCuUwQQvF|cZp36Zd(vrCqmV^Gw4kVVaE_=gL!ItdS7xI& zU|kUZ+I9-|56}Q1ll2DN{DJOcUx4*Au|{ac>JHCLgLgWzG2{7xbOPu@8-?8epmVDs zc(K>0qdxGUvlorpQR>{pgcnZcyvKXTY&&t1G4&}lT?R?of9&0i&e$r#U=tJPqn%PvukhL=rbbQfGFslMk#q(q&0qUTeDNcvaKfM9uVWPByMS=JR zUcRJ2xt#o3xsGa7z2QzoH;wK+aH}Z?tquNlI6Avo-89^3(VZdtPo+LEsQb=*49yPytjACE_vG5)PA{u;>2~C7;?FAlM1PO``OQglum>0MX9j+b zTV%&qcshI&eh%P{Mc(CB;S%&EEQ}m(Jc6=n`(%USR`Vw~C;b0wr$mq4DQeINDC-U+ z)d(OGNLV(Hb6!$QW;-%j@EAljn639lIg7YyEYh2nd!pV+YVGz=1NF*ir|y+cqz~e? zfbHFHkxobNK{TH&nQ5q0Kg&#G$F0sxW5>OhnO^>cN{j*Q-k`o3<0NWq0O<#y4-x}d zv?pzM<>O8llZ`#$@5#vL2x_PmqtJC{M}m4J}t67O)eb^q-4F7L^4Kw_8$|R zCM7AD6i9ZKDp>gXBvj> z@RJ&OKRVH&8I-Dc+F1vDJyAKA0?nxT_hpwP8%=g*_&XoR77B!nhqgf~{-y2H(^H*B zs46zq`AaJ-_3%_@kU=%Z_(~m~3XE;6t5ox88rAfl=8OZB+BVIp3x@vJY0ef{4Sew; zhOR=L`%h;cI%7|FXjOjIbZ4Ev3`l9$%jn$bD|vzoNuUOx*}Xlb#7-8hO^!ui(=<)hI{%gg~eU z+316>Kp#6^ee{acyuc`EC{+VXVWtk2fDq}4*-jJ8ezM&hsLSm!Z>YU3A0c!^$4bJq>g0ABE$Y^?@u8B~PP2V>Uvt6#D^MP656*k}LOl=mB8|mZp z(HBer4Sh7<`2?k&f6dWOa-PrIhp$0yg=}kqR%^9b;Lw=lfdx)W>#^j_1!$7BPHkEU z8g#v?{f5&T{&f^f6#;Vhdjk^CQOaHHS$Y2@5j{tB&t2e5Z;GtHx zF2-Kc?X9o=x!9@E00pV(IQH8Rov`{(7CVKy1Hd@00A|7HtyjMak z;jV9DD%R_Z#o6^Cjj3ELm?_Efcr5`5G|m#221pGU$n%V z?i@YcW0znCOpS||=x=W<*mOAtvdi`nAdEW_J$$sxSVSiPOYPfZwbJu;~j3_-qb0k^@XilWEj$PYGjCw%0 z0P#tfViifL5&qyiMhvQnCD5>8a|g!Rg+ANWA}N;F`+cSr3saWYfTG(HmWD5VBHMK! zpMy6P>>OjbG$;jkA8Sw^uZz{F>&4Q6SjUgbV{{tVHG3a)eG!NT1M&P=R=frZWt{-y zXpq}Mv@=K!nr_!*q~S>J_Td%oTX-V>y#9cQHJ~T-+5R2~ z*LfF=YSx|~4X8PCVvgciP7Uw@Laa>`6r zqJug3T7aF1sOa61f+ku}hftM#6vL>SAPr!H#~R7hxIx!RRgr{T&7w7|zC=UC5ER9^p-kXWE-K=(f?$w7Sd;=HvNv?;ZVZNIBs~tIP4*l=+y?|KvxF zh>B5MigjHJ-3Z2Nah~AGmJ}WV)mbidb zDmF!maDj+1$j~`IAqa;a8HzROK>=AL@glle3iXqb8OO2_rv%HxL>1IQQ#i>y%?Of3 zNRjyCV5G@EVFBQ6#uh+bh`It%ULp(20DSgD|n3DVcQP(WL0JG3Oris!|srb5Mt9KogUi^DdWF08KI2RyWE#cFZL`qf7z zs6Qt9)E@+)ELw&4u*(AOYcEt{aPU!diGIGk)9uaJ-a8s zfsAQL_m_*ils)p_1s_&yI$i_D(G^pP(>%%gUVu+)5jDi{5T|pmQ6S&O=X2+5^1*+V z>jg_1yOEgSRj#vb)X}(A;eD{`n<0i#NB_m>P+>>^P3Z9Z4^{Xu|BS{2qd2zlVjcVg zHY_foc1X-zF8#24L zls=kT{(j8X z90a(76`ha?!X4!-n0#0IqruLHq^dUtkMiGaSu=G2>+aEcK?5V#Z1f^!ySV4*Z-k6}q*qfyxui!`%~DOJa!hIGufmK3v8BJg#V@+}XbgqS+jfdLa zHJ`bl_nUq|vo7DvpKQ+r=RB(C{MCm&9TgK8+0! zIvqbbeeck*gd3FWc0{sb@U8BsVhgM~{WwhF7@Er7q1)^oY%99>N^mJ2h;lZTT3EX% zfoDH55AwW@NS`v$-Oc+2B*JFG;JN0Boc?1b&|V4XEBgr`b|*NFj{&nq?&w8 zYtZBsA45*k=JEdvi@yx}?KP{NiwntNUBbaWug_0oaW->GYyj@H`b{cD_T z+p0)D@`1Uykmp2JPX&hUpZH0^*Nq~!V^7U$GI3rtxlk_2SKnQMfyvP z#TVPO#v=b~=dU#u_kHb5tERDNvK?Y1Ph)Y>b}dF4x*hVuDXPUc@CB$;gTHY)G<*fJ zi&nk@S1=AC#LeKEsoSHeWE4`Iif^=(yx9(?A*DEdb~xQ3k9c&4(^yM!{2Cr*N^yV@ zu>sV|9T3bcSNnH3b($`RbukF0JIww&WDRkbE~ReQHWAbi0Tx-am#8-Xa&Bx3Hwy%L zvOx^TH2t#@Z(}EFvu#NE^ui=_Rp9aoe$=_Evumj@|K-dKjt}`osb4+zt<&gaYQz&h z5>nKKVH-@H!iX9+Xj*lq^Q0O3976@AN(0f!?f%?d!r z5y)crj4?216vnSXZxJw>q#oK0?y7tU8j#zw?#P};eGm5>_R#Q=YO{!h)%hfLA;L^F@;5lt;S(CezsXD3DI;Qedsa;Ib z>{gfjg(;fs>eNF!kfNoqPK{xTApvyi38q4+)H6)c*Iz$-p_ED51?%LrYN_efQsvcB zvzVd*v3_q(wbX0XN-nIHT2w8yWJhV{=a*M|SXnLgPPNqgOr<~nQMJ^XY9&9d_UyB2 z&%W5PqBNtL&D9=mtCsqvTI$kNu{VjfgMq{%o`@xx83VDCT@AE}w0Q)jx7-A~u4a7bp-O< zaA1oP2t!F0hRexBHX^(?1A8~*T zjRZtMU=4U;wjK5&UGGUhu~Fv3x@oSZxzj&2FQbeOCy{C$8)8~VjSpG}hMlsARP$ht zMHm?{cnp*{Xl`(y(R64-h;(U&g+Qq=_X7n6Zj_u0q-*R51OdiEhnzooNES%9#ZZap zS4KF%lh7|6910yKv*sfq2Xaaia*XZ@ih>$pPv23&I8>_rZWuvVst0yE*Vn9sfhEyz z9P(KcNLKx{+c^QMo;rJ+xwL0qxyRY;iAKQ@8T+m((df23(M28nH}tI)s{3A``YFll z_B!=IlBfswIkA%0-t}=AME3yNBK8)N?q}pkatFEmSAPM2wrIQB_=^*7td(;VIUIZAd9HIkOi>LFVw7K3JrBZO z_NbbE&^f1J85nuA5{AIoAy`G^jL;;Qr(pm;Q#B4j)%23;amcx#2sSTh3VKD1g2%L} zqKN{2phc}ZQ9WOMe8>^7rupg+EV&P>&WD}B=+euFow)V2T6x%6j?|;SI^Cmd5JaDR zDvSuVm4sUVtCI-B>laq0(XZ7XzdCDCbj@#0Nf<>Tg@BYhbOa1BW})p7kZPONvqzjS zaJlPuXbdZpfj^ulEbE_29cA*VrUG`sPdz4}G<)hbF~iuq8?_^a|6!b6>gmR`T~hKN zmgr`k@J}u7>fv#@23EMcSA*`4xtrDikHJ1^qm7c~D&H^0wJ?WqWEo4Y1c(s}YpsB= zhdKu>&)`GEx<07Z_yyx4edkA;%2myPphZ!afEe1v)PnIZ~IkxEKqe9f)P&Hg(6WOJ!6KRH5?2;^dU62nRx485XB= zXkQMC))>qUVR5E)l3q{*Y}E;P29f+r0{*-lvDx}jl|%%?N%W2g`fR)(5phuC-$lfC z_M-u{-Vt~6+E<94(4&qO;uI+RR||0|Mx>b(EnzABH!0fU*+{9MX{V2pD{@2X8!7tR zip!C6Z0lFGGAf=%`>v`X z>NK46uCLhV>)-?AARGQI(#?z&j^%Edq$bu7H^!+?vOT5{Yc}8>cp89&R1F|*Eke1% zn&Rxz=}-!|Ea||WkqUyt0dtlfu!t5%SngaFp`~?35q-8^RgtAELZx0t5eQO2@TF_n zm{H40??d4Tv^p7O{>3t2z*$YI47-t0WY2M`0iB;&s5ugMW@R|js2whtsvbXt=v z#V*Mgz2T0k@b}^*oFqz=s1am>k`>S=8WvzLt9iXd-GnJn#7wV1ajJlRwc9{7WmPo4 zvmrk;3}B7m6!jY(TtdyJb_kpaY6vPKbbE5ws~V?^cJ@Y}n()1-Wo=TIoenVbl$zg5 z*44IcK2jqd^J9*oLC>_laD?CTYcr#8LbF~z5*0pj#%=3})%NI+vI?+mM}^eA1){ig zxF2VSlj=(S83q8(^DxPE(I$jnuP<$j6aiQR;6h&XSvSzCYH69~9a08c5Cfa@pA5m{ z$)I^EQXEKvwfa2{iPgj*Gr|?@EE@SgsZIUyY;Hkql7pVO3$>)J3oD+M4X@%99s60` z1=tlrWQ=u;S2DYUwedgb4p0BfixSLpDW{sDrW6R;kmS}Cu01kKomW@fSj~_lD`m)m z8}&(DQHFT?=hYKKdAjl1`r^8*H(>F%Ty-iGpElIDkhNevP>Mk286XSZ+@?%%1JMOb z;<^UnZmj(;8i3z3XN!ZT7)x*Dc1riiFew@ zgj={u3l2!JdGkU}Im?Y{l4WcRJ8qSdad8sJgPw7;VU-#h7v}@(BtMOd->pj#hXMhp zyf_qrR2&M_z=#xDFb8rPIw3>j!zcm9fmBVxxC0R^Fq~dc30qqNW`SPU#)$zCbu8w-Y)pVL@$Mrv1M@q!6~H+u=# ztQu|wF3}gmz9*-~>Z^rK#Bytix~r+UK1i9(JImG9rlNmsBfrt&9rwYQ{r4f5mdHLe zqb0!4H)=&QF&8LvNGpvp|9zFXNNsB_+Scdzj`v#*`5dSPz~`Vt#n8z|7pRskFfEVg zsM}hIMx|9waCG4UAb^~v$XKp(1oSa|n1pefU5LiPoM2{1&@p1U>z&SLo)(y=g>?QU zWeDj<$bE%Ab56=dj68odj!nYprKyk-Ek)>Z<2sijeN(R{`D!Th)j|p*D9VsnlyJlG z+~*)=XR~~%7nk!z>Ak3KZz<|w^hdV@^@Z|)<$&g^;8ya3inZ~#so|y3*l)?VY^mnO*T62GHeRXk(V7!kl zC8A|bfDmjt%WY}||k zD_^%RKAf}e?%^Zw9bB(VE3xC21YrJwRb+g~C}zaB@xX<;(1oGrDqTnoZYSb34=~?* zSs%T-L``WY>ed{&qALH8DgR+R(JnTb`NliW9fNy*OdEFSk&I4M4cmi$dvS&8(Oxue zu%I9mvnw?{qHUfhS18-8yOYIe73(*f}AiGVuVUbHFP?WZOY z9qop(z@?o=xODLx8A`6{AX->}!oT+yu0H~K{3)G8KfwIQJAt~f4ktHv79Fg79a+{$ z)j2x{DnOGiSH)fNvGMBCu0RhH)T(rCofQ%}Xzq=TX%ed}hy){D(I2kK=bF$VcVz#9tPGGkdC$c#5FbC3B zNb7rummvzj?W633dfpY00>C|^VF9M2!~hf?U>JCz3Rl#P<0;0BF_>+4eJTM&g@)V< zcPte9)fEjL^8|t$chz!L)Kj!7UFXO4+Q`a@-`ush(?5C!Aml+N*SBjy5D!`=Rk^7G zW%XP9qK6+*WR>{}H--0}e#?A-4?Rs03dGdfp5oN*q!R#Hz_M{nQyiPlD^BIL^e8w& zpj2Kb=BfUtYA)~}=JKMsd}J;MP8Eowo2+*l@K0zWHA1iEs70rXzM&%zV}1^V)%f*U z1uD^7P^@xuZ&0oC)tugw zSC5||TA_|5XNay1xeBlhqC0^~fwi>^2Ld8R-Q5>`aqy;fMr2s>7AhoQNE35bfoc0 zA#5}pY1GA+ia0$Pv||QPN>xL0ppDbB2S>=*P0D2j`yntjA~IDz^|A5bjIRL3*LIs`e5^u>5CSAF_|uU)PN z^%Vn3mO;7{cX#1pSnxy4Xyi^>p310PjEsak!^=?D_7hhT7(BQnrww4kPyNJm+>ztY z7AMM0@YiyS-Sz7LeIRmJd$!nafB}R2y61{H_NSKGf3CQ&dM?B@`_2`AtCrbV%{vdQ zu%Y97s>u1GBZ>Ia&KKiJ)PHuqxTxNa2@t+gv~Hw4mirqs@&Srf?+e5Xt1`Lo0+IJ; zdq$P_H?fmqgWvxq#$p|fxkxYwz$+JtX3YKUB9Km-m3uM3$!0bGVlgZ@{s}DSC92aU zqL54r$oz@?)tt3zc$Fxq^BqZgFqVvSq)8M=nJjp{_q$3wMura1CJeZVOUh&tUNj0+ z(@0?bzhA96xk*=p`ALZ{>|?|ikYN6`_+ra7qIZ7TIs?)Pm{M_6#;Y#Zibe=>a_O}o zMaHYUP3oy@MVIE|LmC6Hc-RX+HC0zZ_s=YM-M=+HfFs!+b@*D*td2n$=HVlgsvdT} zPMlnKiFYcu1ptOqxp~p2axYQiuY&+-iTZe{D6n^#Ypq?nc#NlXr_1fVPSkgnz=+j| zPf@h?0CBm!;C(f8nTV^W28icsf3px1TDP-B^D?@V>vdTV}hH@gcV^R~QVmEq5W4ANrW4nYDYA z=>jHoa2J(KmDZyAc}!v(A$&9qq%HSnX6Ps@G>LW}UW8j68 zF(#?PbSE!HvV=)3##qJViA(|(As7skI9|pJ@ABN6NY-c4j=0hrwMEF}wtW2taq6XI z!z_#fAX9o2#t{D#m_Z6n11$~8S`tF)ul_m50@YT(tYyIbdG^zX4(sE5fQ?3e8B(_c4fq?J_)l~z< z3HGiDYWP6WxZS@fg2x*?{u!4%n5^JZ5JjA05Ruq^5Kime>GwD)qU_BBMWZ_7pMXrJ zBWAYEK(~zdEAbPZscH=pZ={9xx>9(_YI|mG&aVgQ%{g$e_#U9?!dnEP*fY0?3P6QR zZv{b9o}6_nXbnJ+4z~lQRVK&WE{6H7&y~Cj9NFi|)9w=WY`GEA7RKp)-HiU=d$1n? zbM6sm)tNURk4oHaxESa=m3K$g-g`tZV=A!EWSsJM(I;r+I%@3SfoCgK^h0cl4SyGB z0e&Rz74!eM{kZENlq$owRGi-uVPo{cf~l}Ei9kn~<@f>5Qc+_Pu&-cF*)OW_K49#{ z>bm>H`S@1yKGDo7Q(xZ)ZgY>yzF#!X8((JD617slj=(C)0?6ojKSZ*7)R6l{^SVFHSoC`@h|fl`>k5e*h4Cw0h|Qac$vf2BYI@x7?4+(FZ+++M@V zyndtCXP1il{_#hBYRjzLx}8(t2|Eosl|ucczpiJ}?y7p0mx{BSAj~6NbP-o#P;{-1 zk3e$<@<=^Tc{CAtBo-J7N@A=!u?!-Nwd#g4(fa(|Pa~XKfW-+H$(&>HWBb@ZS~jbp z>;*v;i`@XE7WLSoX8vL>2s^qAz{<$ppeOdZdF~9wkX~wMnP};}7o>3+U?P|_weR}e zMt*?D)iy<5TZI~#oH50FA_q2w_vL6QT2%}R>N{5F;8w~yp4~cEK^nO@0t~!-U z-Tkom0L%82;etVr_6!%zvWS6wYTp8!gs|=U++Y^t-_26p9uY;>EOiYo_N=h_;riSr z)_ZEsBcgd2%_1C&s%zej7Gwcnp;5m-0x8A|s{RO3<RpwFYw zom8q3kBZAs<@QI#Rk-vVDVmd9xOJqc!-pe*!w;+Skx&jG%+W|qDA%GUr(AV2m+Nt< zrO5{kGr}XVl>&s)rHkv6t`h@6q95bbWyUG)+!B_7XG9mh( z$OLDS=!mACHVLrjuN`WdsDMP_6ABE3n;4|eoMYS;r}BVBQ+e%Dd7YT2 zZhu}ckY~(gg}MBoFUh=3UN8kPYnkdgMRYsKM2Vy?!VKJ+AeW0wW&&1z-W{Vq zP_T{}nEj@jHAOU&sV97O>6_~7DOhQ5s>4%6JaqyCkUzv5(8C9!V>P`BtdRgpoa%OY=Tv?MFiM}y~Qq!S?7QyW|LEIoQ%SF1@E$2 z-7Wqg?bWf=q~gR`q%~N{Ky02FbAT-OtIwxG0avL`oQ7Rlsh*eyA|8ft%cfyFR;Vr0 zM7JC*@zrjOs^~=#JN>PylPscZs5e5j?#eG#0C3{iL!3E59-BW%C+hXI9?+1_tw@!i za;ogDT}+1FXGj`b#1a-1s>-prbh20kK5Z;x&xvY<1uzF;Xza zpfnuj&FZn2#Rc}-FVxnTMg1CkHSyV!M`Lvwl*on$d!$_Sf;REoa&c#@Dnzo)fNvyy z?n+>k91Yo`^VN=WK&Kh1$Ba~KS}S{|NHm@T8B#0XNND^isljVw5apAKaTt$LzK+Tu zsOd8kBBx4ocm={KL&$3E1Rt0QiOpX1`b<bv6~1> zHt2`wD{8T7q2l4Mmf`HU&7{0)V z!rcG@ApMl}m3d~tF>-RlC`LlT=weXP+7ud)C%P}WVxH&?-|b}1YoI_dzl~lOBkc8G zB+FkHYyHT-Hz`gD)EUEIB{<(=U;^>Yb+{6k?P5GvFVQ^Zo+XgpmaEQ7Au=XZ$ktjb zH1j)k<5F=_-A6OnCO-OZCL;N7L*QvKlQDm*Whk z2bKZR&xPBc$Zw=i@(AG^kK^kn)Q)AMUCGakY+yCU_G)ZJ>%~J+yoDnL{`##mmWz_M zKWRTFzEWUbfw@Byk`dY*ha711=NxbcCazilr1;F`7?Oi(<#K?2s3ew)SSe#41l@4W05a1-KJhod;38ZrnS9*4=}Zvj9G-pMd>wm!`hT2iUUa z0ecm25i}mh8tYeJ?;mkh>@CrSMm-QFQECJ@wrFB&&Q>Gd5=~1h zGDHO_qITX0gN8FW!o3v$_NWvbVH|gWTE@d4cibZ&pmkedTFYfiXtZ>8FG|HU#l7QI z+ynTaiqtnM%r{himz?~1tW?WZSufrNV8f};ey_}2=s zAeG6^Z;P}2f$cD%S2gZ}R^^5Fu%fmnm%k^1cI#t}y6qmZx~My$JrP1!1KOc4=uU>5 z0!-eID@C8bq=nA=K=iC;vOVPk(JN)L{rv|iK5Q`PprId%mf`0R-4>j~S~dSe(V%cy z*t4Go&cztO<-zYxNtIZ&FsD9{dhjFBJ^Xwsv+qaXDwe4mKN79O>5|WXgx)Pv%RUk_ z^Lb@aMJ)k1Ge-&uXO0|5IxH?^;hO3WH^-7%dG3-$esyjax4{%ube+=<{ zxf;A0gvCxZZZ$YM98|Vi+=yD@Ys3j+1_P@DLzrV%&s_s!m3Jnq+t!E#1El+b!L@l- zgr@kQ5WZ6{uK_MZfb^AXMB@~`MI>}}a19va&8pv8F$9;l){48EzDa9mYyljjhaXt~ z`J>1}XYhwC01%y}F8c&<41okb0nNHuz5fY#gw5*UC*tzp0#I{mfx6^V(G`&-pZXMf zg9YmSPeISWq5g$b>zy_ffLZQj7PO-}1zTe*WGY{r8a7BZVh5~*s5@yv2c%5~iG*i{6bq}sT;Evh7@vbnwvrS!Ms=t6_0Iq+<4Hb{3r zxE*ED>ltrFAE!V`e-wtH`lI;~Sl9C?9Mc&JP~CmHFf}6>qh|J9bQYq+D;^z5KceZr zU54{a;dkqXOZgB)4L-v6;GfkDBo7~Ik0i^Tn0{u(@;mZqR@#{M$0e_${|sDkw(V)S z;Jus3#t8Ghsp;21S!&NjR2hs#_&LB?PZ=3>v$j!Ps8($dCv==g&o9C_o@g|TtkjVw z0r#~tw2dhTo~8VZ9kyvGixkY}4yz`giI<|opGMIi=~Xk4cmGa)^_jTYYU;^3VOodJ zW=b^OPe%Y=Yx_n&VCbNYqTzoi{QT(5!t+qr9v@O`H;P2D`2xwUKYik}YH!v1LiDRK z(x1TM_9PH9aej?j^93ryNtgSd$W5J5NsNfDfs@pSSg!g4kjE4#^&~mf3TW&~A}z8@ z+PUTsQrHsc0FqoamnVQH==^EZfdqFKps9Hb2#75WJQ=tPQbj0V@Xyy5Yl=^&GN zOriO(e}={C2~@C~P?Db25izU|w1^MEHI6QG-eM0h{F7df+I92ryFx**dl!K1bvDj!~p1?30Mh>sU2EuC+CrjZ?4g=o9)Fg6nS>R<|&paGc z#uhJ4Z*g7ycvKI)Cx>u4ZVS+PhOP1R(QH5`%^A{-g#tokAeLiZOyl~n*9IHQK9;Ro z-k4R_im3POoQ6FaA~Q#ia=KBRHU^qbWzc+GPa79I){jN|_d{q3ZJ7^P%9)VesFdTM z8hui?7QuNu3xEV*--?I*%=$l@1rG1AlfY^IPm_Qc(|VYyW+B2^;M@WHCSYTeo(4Ga zAWSd9HKaR8$ezIYz*xJfKD&pIg}@0wXWnww)pHig*z+u1@M`uLVP8)Vl(Ks;6oJ7H z1ZgGo9>Ak$9KZus8d%S6-k=20243epF{myu99$d*nLw2qk?7Tgt~T>uo!YM^$gbYJ z*vx{x&GB(U5tv^CdPWi+bCRg2tpd2g$4kK z0Uh9SRzf?VfpZvyBYzM=v~?h-wmyQMv^aJsjLA}AFHpR{Hjlu~88HyrjHg4MN~6eB zq)tE+tYHVepfn8LA)XIx`r}}g2vmaFA;Mr%t*O)zzi~i!??41VGu@#5ZLGg)5cQl1 zsPF;#FjQ47LV#f&DvbpT63mO+!ZiyJ)yxiae4z;;{?qDhB5o-9m@d)lL#F|Nbud&7 zS$m>@4o8bV?h)5!>Zh3@Q}bKM0zI&NHM+#F+W_j+t^(6| zshf$RXz?kXjS1a!v%Bd4&W4;aEyijL5RaQxLgg6XBmis(-Jo*`ps?-ESORRWIX|G5 zDCzMQU&780cD-MUVni9zbWtqfGnlOipJk{3HT6pv+Ce<>rMSiZ!cw=7%1s0yJuXk4 z^_95As`0pohI2HAB^r)x77d7ot+8n|435$}m>OdKnX18NXg&T@fjc*gMELO(d-D8d zSnuI%s%@L0OJ1vf+bo(Va2{0`AA?E+Q^!+v@i`bCqF+!&K`=wTw}63Jt8UyPn$}wj zF13{p@-VP39K|d*UHgnJ;+)3py)XJMdD#leOsQkQV3O_BJSFfMhN5t*D1-!l+;&== zsH?VW8S0kpqJH*!D1fnse1G;uk9;ex7Q0`Db`Dg909PHhhR>`rleosZ4Q$PDRDJuy~PJH3wLyR?w5$1ZWEws8kK z@%A^ssa|vdL&P>(udIf=gDtm~V*5{b=?xqDPBibPH!RPo)RSkz5R6&RNM(V# zih6lhr}9WZEmXIB2iBUKAF4fQNoL zIOz@RysC)?V`AkO06wYc58_P8GoAsYum{079#S{`AUZWT@)Rh8ENxvtft3ZEN>j?= z&<4W(ZrKlFfR0wDM8Uwz%y}J|_qfRl0 zZ_uuqnj*(Nz<|%%<6t)KV6d^C88AKQaS))OQeAye^s9{nHlX`Hgo~+U67PP+Pr_E}?VTcPhtJOhSgiGXCaV<`Jy!BThE&Ju5y6sm11IZ(zSF=CCQ_9P2 zkW?Cm3aU7AhqUr}@U@NMVyeSenw=c?rSJX&%uTlaUHC2WEanQnQ44UvGI#$FgUty` zz~q=!p6moj?w#!P?jO)Z;)JFBe~3I@Lq|ndUQatJ+65N@dP6R3~C z+K?|jDq1y$WqN?zyq}io9uW`oHHgJCEGnLove?@CR7Y7-d*5gvvN^B^C8cQOyjYn} z4VB&DtC!tNHvg}AmUIPOkL7OmAx8fH#Ipp&Ab`M$81y2ktq}{caW(Y*)M6Rus6bZa zjS8XynLAvc`bk!uBbmi>BwJdtpgD!sI#d|G@HB#c+zZ@Knt+=PLhKD-&d4DG1FF~` z5z4y9TC&g@pzHgE|5laTE42r*^T%zXx9S5N=vfwyfGThqGj#+FOtJ-d6M8*5kkTQ&4tayTp*$95S24S6Q}tE+p$)pQ&U>wkUoH)GtkZ5LB1}?LZS4Urv_A3$Ul&rWc>e z3CjdJ*eWbfXg~Y63ST@5#+HXo1jyHt$98Ot3;AS!6gmqoZzLJjeCil3U-d{>#u6z{ zQIwD2F{ok2U1EjCXY`%l)caw1ajP9To1z0K?Eqr9nFA1VqF>}Nc(V1*g3Avv-Z$~}@J zn_17OxjC}Vz-c)N@RW?s7K+XyTWXpD)fxuNQsz&cEKn;>UBOrQpJMcC^OVx6I-L98tRZG_33>*%(v zb!4Ziwt*$8nhVuXU>iT7x(c9YM;Pxz- zmFYYLXG`T>%mW#Ge0sT;8H)a8x73+Z_Mz)DiqpY`wecRFX#Lj~CKPxF$Eu->TYnpmL zDkoS!sY`0eMgUj3R|_oT$cEUEpd>(fjH@BZqP$!~p3;n?iv|{e4d)gHp}{RoeGw=a z{Ir8>yV-!UrYtU*#sGb$bs;@7Y{rzLGbDMg(+xG{g#~6`=-3E)r9I1y z6^{O_*432Pp5?8(16ZS`hw)tUzuxLXwjV*n^sJHjg}j1HTrP$d5nnh<+-Y8o2h^}y zGTv<(M$22=cT~-{S3DC5qVtrvr#rwFZ>c4_)0nYAp47&S5K=HODFTJj&fd>UCmJ(i zL!k}i4LQykm{r}AC$A{*j2TV%`%G)bAM${iD^*^;Yyk&~9{F-HS(4xK<%v*_w5}~L z0~0o?w(N=2$F*f=`eB9Z$VTFG02f3W@eZnLS4YM#`I=eTUKU^i+g8kPsBPev&kJ{ghER|!S8IWKra%($E-sM$IpmSL zva$6q)wZszQ-~1;355|hW&&K0X5G#jOprHUuxgU2kXmg zOOG1_+k~Y0BnyIY`$1s0v{Rjy+4za907l#Z5s1xU%?04I5=52onnE60icuoJfY3$+ zIc|)t*LNkngHM-Elxl&I+ufvE>PJD7(EWAyJp!U#<1+LeSePcb93M!<{TLodHMy}( z(!u=p^u=qL76&N%(SN0S;Uqj&Y#6?HS|f`9K*g%{-MO`|LZq$1ae>3109(-$LFior zD+qg!2jDJmn!SUx;Diq2}<00}2PQMjX}Gz|0>;LsLP?ifJeUC615;!sejBX{T4E3L^^>ocru ze3Dz0?K4HO{LK;F5ln)HFI3k0<0yb2yyf_ucXrW8t(LM)usxnt1mPTEBSeDKWSEK7 zDg~3^CJMxAAr{>YxFQq^gI)k?(I&qp1H=JSQsqu9CBi~}0&lZO3c!JQFo(Tx__0ns z@D*d|J4~jxWC3t;Dg4THvl!1;T|F_kVSutLb+k}k&ZA|hZNSkoR}}(XmaDQtkaWA$ zvIep-gAEvQjj;_d_t5M|zoiHuiv93Mp}gY%vW}hqg*wps|Ih9C?!QpS_o+H+#^o6R z0hh%Au@9?9<8nA5wn)f&UCaD~6VQfpn@!CcoX|S)-PK6$R2B)COwIZ3l;-^DgeKbG zMgU3B?zb7ejby35MS3j^E&`C< zsh%j3=fi02+agI<*V4uy2`bfdP2`0&7E`I!V~`evGPc+hb)+#Ek?Msi{#3|nDu;qw zDQzlmuqu+@HkFQLeV~3S#@HQH-I~d^tcW567KReh^lVG?!88o#)=71N?#56RZ8=GFMi!>Zh39*gVfv_5% zhRC0OF&gH=HVZJBE+(kogwaI%O;AkS0MeUtg}C9NLseFDN$cg4o6F|4)_z;54Lb^q zzLR2A)wj89Xnm`mZ4L-o3FVdv__lPfdJj1?>KO8vY?yCOQh?n@K`LSo>B!CQh7$#MUiPJ^t>`S3NGu&IxFl)U+0$P0H1W=JHbuiQ_2M+?KMiSxT%0 zyNW-Fwe;|&x}&c7Qsk*{E3CD7s#z=0rj_dAR+2`ai&|m2Vf6V;E7>MxeUgXP6}FbW zPM{Hp6|-std=MdHs#7)~AS?~>JAHu~-Wq%44K=g1>;&p)du!R;TBt-DFy9MR(>8La zlSvDxcv~!VoNQhqH)KBeE%l&xThxZ|jcqj{HmR*4#Lhoajz;d*6XmVBRZ&>90iwG6 zB-u%hd7raKpdNCMj8-q6B%79wA4Q|APQXziPt~t8N|*usYx3slykeaf_420byqNpy zgbJ!3pgIXgr@@v$7fRa=R8^K)dsR(!iM>)){W3nIDs@deIrhw{HPO18uqmC%5C}#MRxP& z^K$&G=1-D8uj2>i8rwU_>)`QpdPhJg?Yi77IC(Li;3SZa082B};f`_&7W^}vP+;rR zon^;*Uibn?1uT#YSzL*MVxJE|57fDxWkHXPB&gwS5sU+Rfl&m>s<}jx00cGoX#{#w zb5MTp!U%a?omZIIS;hegKk6*I7ab1T1X^%v%GLsKQokTb(s8@_M78W9XPV$jgw4Q1 z+q%du)^t_Bt30DNpt4Q6z}A3APn;Eo(oM@FHg=Pz zGh|Y%I|NwE)!E(UBN2KfYYTGtMQ`aYdxsyT8wxz3aWt@uNA+WxRzRjg|7dmP$(nO{ z^JIDM8Eb*1VEH{}F7|Y5cPTDLcD%}D97$*VZ%U~1Pm%Mmw})LN`<4FaA%!Tq2cZ$+ zegZ>0*gMv77A(7bCc1JKNKu%JXbm7S1IC@EUY_v?WrB?C>BEGQ+#{v{n}QL+=vy&J z;vpCGtf`1G*V05JKu2Rv8B!f|ou`G?LPox+K?Pb%suvT+$qDMo z!S*9Ni(RkX$PHIZW^}1$Ra%?DcgGE?o_mn$d9jPlg>ohQ08`Ox?=A1wVXrM5Rcm_ttXk|aBOUh8pW3BvFOHP*;0*2SO2k#9kCY@l zNMeBS=-fx1Rqf?x_%h7Z)1*n&%k9OBd{`sqqp98|b;_AQqMB&IhtHI~fF(AZsS#WI zz6P;f-M2bod#11E9e?a=c*pi<$zIi>+@ylUfkB<3o;XXkvMZFTJWGzCFKPiXG^j^A zx-j6>A!9pl5|jPw76=1yT!w~{f(+{H|u%ywvhwS!FNMW4S}JJ;Io=;x4b$Hx>?xo555qV4r$8mhQ(f zWD$y*tbGp=%V!4TcwU=c1BH?^Go&5awwP8#;!Q(EySw0f2gg>{0^hd}cQ43IJ zN8eH3p9?Sp%jfgtd3p2R1+)SrqwSf#3&1;a@OknkK}@BgE!M*n_4RomQRhE>zFbr1 zi`0;4KGsd)N%igdT1S$1ft(}2XrQ((J_{F1MSDnBMK6Rvs9cp?Cm`&Iply`xWvQ z=(6?T1MZRS24UdW_9rnoAAo=dX){F~y+ZbbQncTdxFBlQRZ^)Fuaakb_x4M+I{QjM zH2tWbT5z?jYp?%Rt+`q@YM;`g*qWxBHyVV_9FQO&F!W%Or9qq>A|J{wsl5SVuZG+a z&adMYfRC%Y{#WW&ASZLj{HD&iM&92Vx)s>r4gjWzBNtAc2}#`wYPt#Y#Sqk?}1L)p+tt>8}#5>!QC1xA#0$4}% zuFZAwR1~`NI(dQ(eaximWO1gDBK;OR}g8etPOxUXd`U#akK(-3Mi<0Qi z&uZFjGGARa0O)3`8akl53MKQc6$2pjJmxz+ug~~S-m%}Aa6P`Wo!^P1zVp@fvPF0^ zRas$Z7UT`G3|ML04YG5shjV!14Sl1aS2Oc{H^9N>(0nW9kHr7uM z!Vo+}^?^tY!I^{PKf~jgl^&Ix!Dz--)oF0%=+~(>XF~_eE_q|LzK+_yh`Wd5nxCq4 z+h7?8uVi(nVC8DS?K0k#@Xpbu-=QH4pC6*K ztTwzH_#i`@g563ljkJn}8?NGZqoO%_yDVnB34MOd)qri0VtW9D-MHLD6-{o8@?*r) zKDJVI*$~;bo~NRTXaJd^q6zubZFfMIQLdH`0e@7kei$OP+%!g8M$QBLjy8YcKH1*-KDqurxyL#-rpB5FfZP$U`T^PJFJo$)GgMw| z{Mt=S4LW0hNN^)2eEV{>Vko3Km1@^edA>a*qDo3ZraYr2jFFYrm&zI@OYB7f)uT*S zcuDJs+W$!UA$(*rQuf4PwW8lwtLK$!57jp#)n6LgeR<&+CrC3_Flfs10K`_7`}afUj)!xf^jcuKZlXq$=_g1IxuY0GQfp)x8B+s({Z_-r#?O@8@31a0I0_?H7=~Ui(=phSb0Bz$!>UuGJgj`$Cb68OUy#p8oo z?Upi^=U3rt0?#cVO%v|l)Dk4g!xIssut~vki83Uc%AY9Pg~l5)SPgkSn8(95p=2nd zybKGGiUVs%7av_jFm{ZPjmCfmPB(@wBSlbN+CA(Ze*8xgQhz_{@H~I`0GTji%4+4) zHhM(aG#3e_tzZxv-m_e@VbJ%liNe{DdaX_<`W1o^m7vKy*^8);=Z6Dr1zi^I~7BQu3LKE{+6AIfcd>Lg5((|cN zHzt}m7bh(S)Y`_u_(04x^~gL0zl7ZnPrdI7n zPE#??U>RqSfWr`nvbWD=d`94_HCF3 zS`cd1tO%UhX_Mrct(IUyu=Hq?h1;+T?=mQKaT{@=(2fb-N;iwA6d^9%l1Z{_gEs(r zjEy`YRKOS4VniT&NKqH6TF=N%Ht{* zOMX->2~?O_6Odo#t3Y>|N#L}Mq&jDsT%2kG5NyUHBAU!3&`@Ub`*ahAz6@34VpY3b zjs!ZKQVzg=SiN5^FC=TxaE81UZ0zGRugdo< zFeE?Bkr(IB1Kw)o>j4vREi$nIP7t{0;<>UVTRLK{CSn)Om3Q{V^75ivKwyix59;_8 zb`&QuLh$JcLSX`kmd&E{m$5JSlsdn3bPKo`g7!+@Gfze>i5j&)CG3v3(dvow<%Ryh zys+a}(S@>pmJI@VrxFW+A9ksdg_6-2A6MfRI%-I%IL557ju$Abb#3sJG z7^H-(;0I&w)Tz3Dwf{}oIYT(a4byRnEb;H#u|l=FBvMBWTOx;Jyn{>SX}I)Vss*Lf zm&(pLn_x zE?+KxfO!0jx8z3a#bmrfK4{?$h|zk+C;o;u&gM^(=pT_tEfO8u_8@tPKaHb*DE|t1 z6`QRWPS`ERCvRIJZ>jYJi0s&;S7o}nYd!4$uS zVD_tI{z*KQ75E?sU<;k8G56F5=UUN85bgow(px6j!XM+okI^kT!cslnmJQW6Z_B!w zRTZfE@5nl*J)>W`msUVrQb9V*CF(_|<}fDhz1SLl8irP5ov0(OX0K;HF!7!0AIMi9 zyd%42ellOh-jxD8bnAEJ6+zQ2HQ`;^sG$}TRCQ~!wqgxZ>VrPaP^;gS_ovFA^B&~z zkS)9?iwtcO0&O$pJ$Z}$aE_AiLm9DEmAo(8mM*3p8SRy{YQ}vlon%-IbLg+%U7wk2 zr*gNYa-q-h>tnF-2FNdLu%$;e9Dr^P1d4I+hry%~x=|m4p@MF1-C;AVBB6>>I;*~NN0+0_@%9m)5o}I?(ku+8ZQh)RV*(7bHpREmA ziM+K+17FDyKB-SX0F}E{9sJJ^e9V+?XIsx4EX&kgFqRe)cwt4XWm+dwpze=IM8toz+h zW%JrQ10*q_o5uNu0eU0y(AJ-owGN{3oYfFKZB>0&%S7V+U`(=uIf(mirkQdtyl6T| zLEgC4vOygZ9)8UNbD%2F3Ab%@Vnl6NCll&ls{xAsP$#dE_tOT7;xpJ$tym+Q=8?76 zBy{HJ{EmVc7BnVbja@5)E#`RUC?=W&nI1n3Q_!H}y!_T$S-&@%LlJ^U$)?+rB4$BF z1?frw<9k94@)H;X007cc*!nzpnOsBs6Zsf0+AE*Ps{liCWrv2E%A)xRs2iRk z=EJvoz_S)xBs)4nUH2&fz8UA57-wiRklt#v5K806r^Y zcqgxu4eIlYSyjXP42HL*yBimH$gE!nu>Xg;WIcrF-k_blK?V!Vpylz9BQ%iIs2MbM z`3BjDSa;|KStnyYp5GvwHsFwgsMM-QTmuP;i8OIZA<@?xWW4|#2Gl1X$wsQ-XELX1 z!RD(|KRXuUJ^PugcltA7F5O~VgIO93rNAug@BagsrSV3FS+oJO1)m=aX8V0E!%Z}b z$1=6t{aku$3>9tRCOR6n@5skpxw`*z*{iC-wbdrPTR05z5c8>&S&Y|XSYyAj5r!JthGL8ZEObdfYRSM$(2YBu-h)6nE1l-{Wec|=E*A{tp%l}Dr z>a8vEc}PUA*eV;;K;-~NUaf!)qtxiF+5rE}t#UwQ-w4d;I}c;6Kckv$gFWgMSGNqN+Y7RiP-fB+71)E+~b@G9)d#vnowds2~EBiZb zl{pZR*~a~#M|3VOji#qY6zzeu*?o%a8o`)e9y`^ZA7ld^0}@egfF1zb(D)o+$;@F5 zfqnq&!UrG)a4m3V5Quv{KcIT=hGer`4c#pZv==H)ndb*;#6pAItb5`<$WlU4v@S%9 z@tsQUmI>`8j`l=Ndl+VnHi@9%_wR<-r(DTBP!s93k?#<5fxI{75&iZ+`Q)wF`}fFw z!9!+SWBayG-OtXr-vUM2`v3=7OW9QaZSM7t`&nx&y z#>0wz*w$Wt#eTi5H|@u^_Oh^V^Ht%G@~l+Gtv~8n`npkWo_g^|SvQqm`6D*C_tL+A z#0EF_f8O8>!1Lz~u1yW}2Iq62Hg0gQx<`J(2LGQ_r;h$4%dry652QD^SF7IO8xH6V z9{3rO?ol|P5g2ondh=&lW~MF=XDdQkT&^zrMb;0Ry{#VjMb^_X3uwdz0i4GF*wZy| zDvjRLH6Yh^=T+_L5G}~k;DCBrDC4j`GmAvxFjz`K3}7S15e8)Vt0F4@pv-MaEQwX` zgRqUOo)Q|!E5MR)^;!r+z~~hgR<94M7EmfrfRqEJ0dXF~R3wKSL`Q?G(y*q1$~!E3 zw|jyRLF;cJ3?^+FP6LtyLEt4dDq25MZKuXao<-j`gd*Sses@eOT@$2wHsv zX2Uzx&|10O)djzU;@qk3|2-29z5KfzgIPG~k2DdZG-ULq0s>=JXt*fiMy8Av! zx;vfjPV!WxlXNx+OBO?bJjfFEu&98bVMN6wDj=gcBGq8j2tk8H9&lKKMuLDEH5isC z2}Wg{s1b44At)ef(2PdK*NptXzjL3eu1*@9`Oo{NKdI+=?sK2JoO{l>=bm%!xkt4Y z;3CN6@dFt*uF7hS;f#$lStmv@KI^*d%X*Vp(I2ge$t*sF_i94Id@jm5&wLPzYRm`r z;+fA~S=V1By(hA6SN<{2T(;-jtm=KSVetOwy8FxP?l;xl|AqUJzMMwpHNs42j?Tx= z{=RKWwaoYC2g|%Y?`98||C(Kc`Nuc9|5N>Te8EYAFjkW^_gw{dZuRx8O>R`Yqu^S| zGstR^jA4_TT+Mi5lRK$;&qt%@+OCIs(P3E5GEC2|`D5D=^(DEzN{Zjs?Cu2Xnv1R~ zHPkbexkaaKd+#o~50UBFBoot5eWunHH?g?>lTcbBQykjj6afA0E$&3V`bvuv4+`5{ zM-hz5qIML?#wWG98Y6mttLr{lIM+Z9P8Bv50M0p-lC};+qY0N8tiu>lb^OdhB;hiH zS>nf9U9T|~vm$fYs+9_J5@Sh;v7|UJN{l6yg~`BJXd@d@Hb!l}lOH(uq0%5X z7{WAg;33EspVHaD9ap90r#F5*a94;!96ZV;9700MbEDjZ!pf?U($en49BycLRUu^v zb9l1dEiJ54<_r?eNgeLfWPZA1_{`%w-FvC->P|Pg+K4~Jn>ro5Bc3o?%p?* zf+nyU^h`pPy$r|*Mv`ihig(8cbQ$+Jr^|B>+p+WOc-$D{9*1$MNi|4W5OSSVqwXL@ zTsp>$U7)d*1BkByC5HG(V1{&as;f#fa4xbf1o`7`n9f6<>1^$RDJ1u=*4@wR9Zdh{ zy8HEY_rI;XpEPzb|G9PdH;#3)tK~mdmp*Eo`#)u0Z(~WzH)B|hI+F>s(?rcm4D8!~ zyqj44{;ctEoTRN=#@jf-Y-UuG+C%tE@;VCPGlN-2&3r8>rok^$8YIQAa~3Hk`RcK~ zN*Fav3Gt~VckBLFnrq0#_&;E7x}a(S(q=X}tM~;=QohME<+8D!Q_99fZYjHW!$fvW za0qS_Tyqk_io?#eWgPRzTx5=n8aJ1%0aZDBFMvtP!L`iT0Qla9e!=j)O;XM-k(y^< z#$0N!HYo>GL<%3wa!6xwpYc$vzgqby(nCya!?GEAAvTllbIa2CO`gqsV4_<#gv}^s zMwBtx#Mlh#mtfs>liaK-fj_IW2d;wU;+$ZpXN4b@=iFwTc#UkeTYj{ zezzb~kvog?j4SbRSO_Zogc7UnC;zoxx?3nI)s!iP;XJzO{ob^8c9p%aq^ zR<*SYOuE*B^JgTKY-Vv=ODVRZ9m(*k_zM(PMeXe~-2BQ`kRqBM?GkQqSY`2B>2zM( zxA4@@^`(d&^YN>&hR{8(GFsbx`h%FXp|OTzl|npG4BHK|D3=;DFf$9(dLsH?B0QS+ zzI4Offa9o05{oxs0gyZK9r8{rIT>t`wH2dLn=G;7vX-MZjQG^uGL0;{bOP%C>($z%477Bp&Q+h`0Quc~mRAx$yU3?w6)8e*1y0BiXXVsv9r*c;GU|9&ts5>}Ig-I?=HGVN{n-1+OOvxy?1 zCe9|is?H_=D+Rys&L(4+!6F%LZkXlz<^wC+ih>~-NO_P!Gq`${lXMJoM*5`9B{+@O z;mtQh{Oc_D@|fF&TCA!ur&dF(N%xFP$*Z&7l>RO2tL9gw)*1ZzSrKdJa*`A;n&Ub; zhd1!W82Gw2UOUGfoCXvR&2@G<$pD63q80sluDc5>&==p}bUMi?^W0Pm@Pes$DK5Jo zpXUy0{BaI*4FanjhobY~pPT2jeXQ{ix2Rjt^!x2vW@o@j&7X;l>cT_Z;cv5}3kr&~ zp}RQ4DVToyVgvYpJNAD2K~+io2|IBi!@&=c#N^aMPHuq#t|ilqR7wxJ)$WH>v_^Mp z?vLB%yIEz9L+}OYlV}x>eD05L&_lHPf%u*C-NF0$d`7I%_5J~Oj1d&4Y!tyB6!5#q zcvDsUgZb_)Eg-#?X#W#TA0OX(sOuVS3Q0p=F zeVdy~Xu0OMGm;P55)!TFy7=3NyE5wP9f!La726>;S{gB$WQr>2edPeGDy{umSv*Ve z?!kPkJzKUjTw+$uBC;ddvqe6gvoO?Nk*e|*IE`>x>xbg6bN)14cC9k(oRcfpby2jP z2#6c@tFIn@JZ(s#+{)u4YX<5PjLSC88>55WX#-eJybTuRS({id>TUQN-WNW0KYRK` z!wpL8B9Y0o-M{Mv_g2lXy0u^vr}Y*}pxRiTefY(!8jt6WW*)z%iD#T=;{S{Yvu%cq zKCEdCX2Jw@%)JSy(X|4)2oW?2rXu3j^}i;1QdJjZb`VkZPhs%^PBghgc7`AElH z77zh|%aN4P0(T`YK}I2Q2|g;xmWtHU3Y3rUM7n2Q>O9ifgu7VmU`l2tfpUSEHY+u9 z1`sg?W@}eIPqQ=2)ha$8&@<_m_~eUy>IDoYQvomo{Z}drZ0}NkSw@zfC^7#AJ_sfd z=#G>H#icD$+wq=|u=N|fsxe0~txU--l+Q~JK;t_l!URmO1s`)ONKgmhQDa8iw@_c3 zixYt<1b`OiE+Q3J)Xj$Yk_Aq?Rc~70j+`lWw6|BwPBC&hE$tvEye2;DTf)*uM9lxU z#PcGiB!rsCDSE~xE}DjlKTW68@ra4Ye<#1AsStM`;eJU?x1QoAvn}G^PI0~Cu4)eD zCd6!NY+gn&`Hx049Q@q4_>ItJ$H40EQZX#syJ-=?H%nN_&mZY}dROo&O!dqrKrije zC;)DGWRQ62QEmliZT#ja z_Z02>!O?C#PnR6yet|vmtqa|!WG8xYA(G!K@oC4p{keSXST{A$X^EwdaFnl)=-J;s z*3GM|`5dKYNFd0)GS)l3r7^$8w$F{`X2g}#=+8Wo4l8m|v9;yqTJqMk zq~S(Xm7tIUzgHc^ZchWG1Q@(n%tzFnX|&^6);BA! z;b8~h58m))4%Qgv$717J3{@D!32$ey=;xy{U3Du4rOX zEojbf!V~tdww(s&G!qZu-O(Cl5mu6A75B=b-E?(XHYf{ok+Q5!vS=bDOAaAZrEF6c zut`}q`7C6J;*XrfT>fbMuUty7G)=|QPMfWo9%~NB1&=C*8khLVliZv{-0%#>JUCvu zkwwQh<0&V*DT<1awP9g58=K|aLBmPGyI)EeB@L@e!JBp~Z79nmnu1Be3ZL*BN{8dL zFLH+sSL8jz6uIUf6nW<&cO1v9{&~?qTveQOiaYgx#E#?asc`Ns@w8KiZ|=bF_l3Yi ze|migJb0>mzcwhEQ@Xs=ZJ%NTa@kR*xpP>M{OWXfe*BfwnOHX0-QTVItB!u@8kJ6r z?<3Y=m&+^odfH4Ixz^2k9bbWyu(9;s`T-NRjUDd6Vn!6R}F1vH2(2 z!C{U(bsIS1+?(AwqgOOY>XV2Svi)0lD@587U-D-6Z^WiJ;w|oo%){CE#HH@AQEWw| zPt}d{BDXE^qi=C%rmu|;ID_fw+W4e1+?X+VwByl@M>`E?@hXW{j^4qw@c_B<{_wB( z3um~w#A|-?3^)4?)ngNgmNj!p#CHUAmZ$MdI#6s=Ic&sd?hbxmtUj*!tsdj!&U7E( zWX|uL3GTfdzjUS>%b4cRa#O|+GySjlT{5Q8W4q&%&T?ZE5ac~)xv^bOFt;6Hfz@lX zc0s{G;+xNMZ)#l+NNk%wt~;Brd#jt2S)YyD{;Sv-ANE#L61~2>z4NWuHJ*(d&UQ!8 zxP@oCgEMzM7hiU^>qgqR;cT#BZTz#dQPggZfBHoXfHTiQKHn0bc8;4p93gG3_M(n9 zSG@*iPDeN_*mCQuX`MkObWt>>e#9>>eI>*YFG<4_;`IyM0mIe!6tMQrz=Pai?Q_G^ z!l+t}Uq)bJeFD11;o*!63y-&%Uj8+fC5OG;9gOjL*R?{RBoX zLUh0b%`SCO^8cdnZc zhFmVmz2n#9gCC%FDB6>LVqw)H8DPRi93l>u3zG2 z4u^ZIUfvh?cE8TF0k+4brL24Jj89nV4j+bR-5KA!)SZ(4RJ>~`^8TmdG0RX0eL0@B z3na}>D)XWUlQGAr{=|n z|27zhw)k)Fca!Q1iO>14J0$moES8@50!IGIrj7Sp;I0^^*yAp8U(9b(VOOiLj*&WZ z1!1Ns_`T*8@x}Pg54a1!rrwKPDZc9i?)@Md_m^hY7vcvlcD?bAOI#yk`<;tf61^1v z@?v*4IDNxEyQ}Hm7T}%KIlG*zx%uo+2nckhv4A2%FB8@wu0{ zb6=y()(^WeuhG97KkQBzeng9XHI`UtFfOT7Fe?!D^NA3wqox_8Fi zm%77;kl;UG>fZ7i-5dQ;H@4}a8<7Vaf~%V1LqF=ur|!@rEutyWcJA4`(2Wo$t)1r4 z3=2zTh<56-JISDQayTxlZz5eo=$1swxkqRE^^ZbhcgN3t6#Be7ZoJGLG>T1HQP$4- zqHq|fHa(np874d}81bz3@kGH*WWD+V}6)4Qv$7^nu^h*mUL6BC^slf{bUwBDK?Picc+ zPzbo9l~(z405 z27WQ)8@N}yzd&;Q%gfzSXJ3szM~7VzRBRn`^x0lwI$gm|_xffaDe7|7P_ET*d6N7? zB?q8sGlM0<4K$f%y_3-vFrg`<39>kD-V<(f9GSM?`Pu|J_eKh zZ0wd}6x|sgvfO=4hN$8V=~QL`u*^hxDG}4rpszTOhMsAE(SFwS5U**{QKP06&6er1 z>L@OGkcqSQqUJlDEI$CbcOB2>h|Im<<0ea=|8ck9oY#}PG>r@SAA1c3J`v3DA=wh4 zZ{_Ea@}92=eJh^UV_f`%8`t>Q9f0ehJK~w2a2>~N<{`|YI-zvRukwjqKbM@Kkiwb= ztBYTfkg_2w{p6yTc7maTGRl*cpKzC^zYsTl(hN}Ne9|3*E29A8cU&v%%DVXOPr^~_ zw>@`PeB>S=mTa?p&Caf&gW`L9&8 zhpQ(Mz4rR;0q(*Kk(BHh-_WE~a8;slC6Tb=H7i`X`B#miC(y7f-ttyAF3&D(lI*01 zaiO#o#f#Iq#N?EvQ5I>1feii-&-|2AWW!@Vg@}4feC4OGdEOL1_9;g7;p?t+onu!0 zgm&qqw@XmktBMriMv%7dj`!y^Gtbdil9wIwSAwSN;?G^lx@}dw<4V`n{<7%;TFQw+ z1;_)!maE*SXA81GbV+b)#{Egryo_e)XsI79a>V=KIF4?Mc4ig9M@q~(el*<)w#70&;PWW zJluggoVp6O=|G)XpJra#_i%^73^#n*O*nKhu42u)ncdM5D?a|S?zlX(!znCA+u4~Ey7{wiCWE#2DTOvz zc1q#IYY}f>FT<*9-J1xORt7=rf7dS< zgfGW$TaDQtx4hM+FaPUm);R0pxu3JAi#~@WJ_Is>d@YLb5!QxTj7(eNou6|{C83>p zotxA7O(dUbyg)qKtouFuP{(c8xf7<}*_^{1sA<(kmMklz*CJjI1r=G*)|sE;aj}aA zanXz|!M*YOWAJKyd|mARoE#P3ni)KO9@I$AXD_Gipav}e<=W}+>BL#T=p) zc|7F?7xwBkQ!xT1k)tSuF80q$VRDK#Ul+gg23Kmn9H#7F$E$7t^jqSe+yFNLo|(_P zcT{%j)G0U^!gK1F;N|+KFpWYzxT;W;qDGZc?O*&<3#Y9<+Di_X^DpfwtUfi_hrw@( zMZ4?Oo#mKW&C#EHkd$pteM5@w_fPRz|LTr{Sl0ZjJLt`{!gBIeUA4iGC;VyAY*cg7 zytX&5Cci$sO0@cQ#A#@H?BeYLF}<5kGgcn|;#e zn6iJ!4^2lIwu|FZqaXXnK7wTG?Zf_YmF^$+_sEfJ^!ThEjq9!FF8FS|;$}DDq}>~N z^>==JnQQfU#Xa^&nH%)@3IDiN_gDLS%6vqR*Xq&Ae4ah;{QK%s|)i4{?8L-|97_{fY^rku0v8~O%GDFD|#=yrvorRL7mYK&{_ga3?Ie!=~q?W$%?K+GKA9E5r97hU&AY<(!DD9!?NXlrtO z^%vb6=>{sip4@w3Z}GGTGH;xeir@U_qTAer?CphOyyG@gl7YL8m_(#NVTW2;^yMw1QP6IS-o+gQH(YSO#nL)N*^&bvD6CmYPB zrteK=;lN-6!hMSvy}_%+Vl5#~5>AZ|z15A4`)+3-b_wGG%j?nW)p*hE?$Gk8kqIUy zD#Ly9JsyAUb~onGS0L6zx4HM~At|D!bxev-D`*{<{CM2(Wj7_Xxf~P&MFf{*N>6l_ zOzH7SUv~SI5PaCT=T|3XZ9`_-X9ImAC@GsTC%)&)&h=iwq_?Yvl9#`*nV?(Ib3Qfx z{#Q{z|D_@R-B;Z~@nLtkMTINA1-P|`>AE}I)Ugk3Kf;@7kXWz(DfhHC%y_)m9zT7D zq0K9IxNzYbEo5_A)Mld%x|eXY)-PzIhpGvje;VDBBtE4?NwS>I9c}Z8pwi+ykv`VN z*WBrz;c~%UL)3&f-{rndj6eZ~Fwda-=JxAmlZh-IeqY@kNiDyy*qgj+V5HK=4XGF z_baTdHW>V9y*stxV+`JR;=Zq=l35r3)7RaC%$oN2nLFH=vX(+#(tI0@tp#96W_XbO zmiXDPyNgPe9fXd2-mvC&6a%W!8bW#y|WfD({!$#trVO(v2GHq!lYQ)YVq3 zuotxAn;YDO3L=W^QcTBQg%rZ!5Z}JSf|c70SIO66m86@PJ>3XA+NzoDm-^YDpJ(*5 zRzHvF=MjE_-xZ5V=4~hfqTlLglYU;*&t3Yd?9lI1{M6-sS#n zJbFa;d-Nln5hM9L_ragzJHCY{@vq{M8_~e7i`Q**6K4HTxe`cwcA1QKjo(UxXs{~+ zyC_5&{8jwwMz>$ft&B6Q2f_vY-f)lc*bm=h+KV6EQyN(We zMljDjA|)f?ot7h!F!D?8g~B$**WT++*=rTB@elX9CF!2!95a0%pWuviMC!D1Lr*F1 z6Yjn*{?PsIh*5fZH$VP;V-IiRZTGta(DMHMeix;$ju&jg40K=owoTCceeuUPQR98_ zS2v-jeRkv1o7gOnULGI$9e3nlQLU<~U`74wchHaCA8+}N8#B_kBiBV5YEEX%HrKlK0#=x5{qcmSBays>k$yE)x( zzme{}TU^gz6Rfv+Yr?O#xal3_fxT)AWY(9RRoVRyTG=N*==N9Z#DKF==8&{#>yG>5 zRS&wZe!h$jjdrS%PfBX92j`H!Y$td6WAU#abc@S$NrVjR;(|Muv>ChA|FO|(UsAmE zyKd^JXE}K#+EI{JC=ttFiql)&xI?c0SFqSc>vW+kVd5(ynLCw`s32ExAN*M>D?_qG zn)d}v9eBbD_>3+|0Xsw)*D4_?V3(-m&+)Na-3dB6rD~+T?l+LgKuh)U`~ESW`8`C8 zAI2|p1Zn!ZIQX7>bLRF!?7q+P<%M|N_uMGfV3xAU-kL%0+QtcKo~R9mEhr(xPFA+h+J4ZF8eQ?Kf>Rq_}jO>o0qx(C8(@ zZeQi=QTY4YP!xPD{@pezygGjSkF2O`egwAO7k~3dqLTO*KXTLChRRKG?~mQIwxN?^ z{H7nfbEmF+fdMK+_dU-g&&Z>U-l_Z5N>7;59ugS19Y1yxCa-*v^ycU~UC>rWJC*My zJte$p4+&(R`V-gRi!VT;&w{yomjG!>{Hd+)`w3GyXQ%zd9a-4^O_&>DOX5HL#GRwa zJ$3Ph;&ZmUx6Hl!b&3L)7QlH8`OE48S`R7?b@kehxgf-dpSnMEBuibjHFZpNaT|9% z>}DZ-B=e!>#`vs<*=6wQ_}+)zd0Hor|CuXIpk2u{?9Js`GmD6k7bHNz$=7du$Inpn zpgp+e5jQ&Xr-o~<=^K^D{V>|>lZgKDw`xbgjeHdevZ}h zx{d$m=dJ-)?&}^MB698bsQccCUzx~-(r=k!%1P|XJ!WFpHy$&w>*>ebBnvz{cPM~= zkP%=tV;uUpJDL^E^2gmo+oUZo4qHr#$D1g_+k3H_rOFteT`h=Un>~`)G zZd7tQ=nlR!1tSHAcfnQ^Q8QZ)^HbHFUub^bB~OFe~_Bu|7team?Y9@cU+|pkK77XmkYQ`|vMx!<+qtLYD%YQrPG5#`f=MPF zp+S*qDj`)JKl5+TVN4$ef8(BCxZaV=*U?}JDfDXfH)!x*<70o}4$i-FANN0tKlTgv zElv)Z_e*zw>(^xf#DfWWmW6ued3So|(T4bx8`w?%zUSR-M78OE5pRe?UqJQvr`+Bz zF3YTFh;M(<9a7P1NYOO^v?-mLUdih)Ib{DCeJO5mE-z!?D2sS|xj@QY{;8b{>fC^f zE&J@L5SCG*muB+if+gi~d^rw@A$82W)F|!$yxN ztFqx#fC7+Yrl(TnrYLn16*MUl5lR!Lhb8JMYu$8o_=P1xD@Eh=qA7YiTbGcJ@)tw> zKfN{G9;CUy$Z}kuHm2hjzR}cIvDAyKsN53m?k|MR?AwDiBH(c^lV_^bVb9SstH8tw zifc?%Y%Rw-ux6mr#Y(FO`g>(emU0+?FE1LtJd=;N*UIOl>*`0lq_^wHMnp0qjEXK9ax5^Igt z*QM+rkd(DY&iBd_;(5OYbKw~z+t|v{pDt`o*AMke3N1g-c!D?9=1qm|(r2+`JixY|)Jpv}>5PCNBbsQb0%J|!tM;xn@`7%d^ z{G51^tjn2j;u6p$dZ2QGJX%o=ED0yFA@}n*-5ACak`_I*=AE(L3hc)?9Xqac9}${t|)|4F|al=={E^+s!!P*nUCXe=Vr6U}{`GrJ3phfK*-UAHBKps(8A~rc#*<>MV z6%;8`g1v-+EN>}ro>?YbN`|a@iIzu~-ieV=_tW{k%6Q1SPx+Mf7JiAb~ z3QJ6|TABkAqYl2{!4B<-{&*93ZO05)2;s0yM;SH`AneZxfbjJ;06|lNgy%e=08{`N zW1_9_UKsVlC?j_1Mwt(SB~jyrOKsCn+6%bSPi2np zDe+kGpR+9tE?~w=zCV_7v!TO^$(RlO2GnF?*9bwyl){ElrPweHNwHBJk`nCm6~TkS z`XQOR_Q_-+YpZ$GWDODR^YK8Ns|;Ej`#Fe{)v0ZZAj%5kj=fGGgyQ^2w(zyzH6hj@ z0$!Loy^exUfJ9S9l~6Az6zxbYDUAgP5RXs=uA-iIMl-Ypm-~XKLF?wGYyb1UnaI|9 zYA=VPv=L!=BDp%-x(lQ0|7EhdhJ0gFb8+-&9VOIsR79qWqcImA1)=1Qzv!aY(Lmqg zn+*S&cx?VPrxMY-)3qJ4S$CC1kaHiQf!Qy*R78}~^p=aVOJ}4YhGRG(NZV^$^iGFh zhxXWIk0!ei&x`j3`h2ODX}Wmr;IFp8JBd0i9JNIzBic6IRngiBIL!$qw87WmYq#%ZT@3qgOX5VE@}gz7%j8Zet=Cqk-;VvC{wns|1G!maEpwE44*fabC~u793_ z2qF`LJtLwWcOfHu1!VzzPlM^V{7zQ)I_R6Q5Tu%{t%;p*Rm~&p4i4K;>g zPO?dJ@Hh0glRc~$B|)w^bkY>FA`pgw`Wo2|UV5N0VGYgS!Piz~S=;+NJaa~VW^q^h zn#QP9wmIv)N~ZZ^e1OqTztw*-3@i*)SHO&O5t}TNIfPSah^U1)=BE@Pzp#_A4*i)E zynqEq4n9V8q$~mtM9IKrO6I&d^nh_tsalc)fBCA?eWfTir1~T;S$(>XR4dq!E*tdp@MThML9Uj3czW69mbyk(YOVDAhEC{wHCmZf>=c9swNfbR z3-drfUCTEs&qlniyjpR7R#kDCGv+XQSx-+~4oQdpsW;3Xp(Vt?=9CuKX6hPSt0|L! zg5hn_G~ci^y3Cd;9_)y|VNXr!40|Nl^@J&l2a?VT;Y^;!Bk6dAg^URcTIf_oF(_0I z{#Z;Uolp%V1r`>xJV!4)BNbw6;8nwI+gwjrG{h$k5s-vfHF9J+Z zMer9k%uAL6I=C=|4acw;8pNVZWVT%yJw` zIPi#Y#X44-2d*QWb-LtWD=CI&MA(0{`J{1wpb<)N7OiL!jWeMdU(qtMQsu`Id&72V zqe?bv;S>RqpMvqZ5#gv{zObH;a!j>c3dK?j#kDLe71S+UEDKUQ$&N$U7;Hg$u=rxs zp+$)Pa2YWFGG-?M85^90PZv3RTT=6XbR5T^bB6{W-(c~oL!>B z@R23aeoQjdN*-e|i|M7!pT0*t`PGbxE@y;ULsnlfF}Q{nthJ?Id8md8b2U$NTgXf7 zYp>goRE-h{ec)n|r9l+jWqJoMZI!~>_Hs=*X^2pLDd$-IGKSV0xKv%GXLyNvaf$4z z6T)J9fbSAiqdvji7Heb>IZ{i*7Oeq%Nk~j}C7tczLP_d{LY~@hDuhoiVI|d4Z9Sj) zLIK*CNZ3#~rkutLo=H?hL{tdQ8|1zM%AH4DEQq_gAX=8D8S0gK4f#q3gxS(qkXrMl zR647eMe}_K45g_wH&q@}4!qzbtwv@+b^){17#)!lgkuve&T)DY+6MrSVw981RN`|) zE%=axV_E74L7chv^2uElMUP&YMtB1T{>26axpdfVRJ20;T1-Pi897%8o4Bq>mr~(0 z@T}Z`k_-CJN7q}>Ep#zadycRR@Y*Q-Ao0lj+ANwTChADYHA4*8VVF^`@ORzuvADv znD(re+X2D;;d9%#AuKPAFM|Vvy%r%vgwtWrRX%zqti^?i@9jp z#MCl7vj+&MqE~dK-rdP{m#)pTGwT(nM1WPWD5&OkJ$s0u1`T#Aqz7FT$J0Enu;7`+ zbFv}*#A7#4XM<<(0V#&vEJ!R%w?8U}p}jt1Y?-?o(OcL&nD+o)oAIVOP%1Ktu%Pa@ zlb`4$3Q$E$Rw$+f&&&19)p|m@dZP5*8Bm#1|N21+W}8(NkN5P`W|DP(TzkLCTb<6 z_!@D|YA;WuDN+fyNy*yAsa6%%e6=)iuq_&ZKTWA7g>6%^=gV=2BciAd<*=Wx&F<}JlSf5`E(LylRqW=4m-$ArSIZladfNk&?s_BnVThvmR>0 zAQ+W_p-Lmn(icw_p}S^Zo>ZcI-{=g?KINw>16EN-Jf`f3N0x-2zXL68HdvFkm1+u+ zEzaH6(qSc zT}WZpq^__yB@+lSyF^ySTI`|<39ySIsR{e*5OG3$o zY|ardsCa?b*orWT=*arUr-4I0sThtHRv`$$J1Z2WUMZ~)P|sb)V9pGVvmW?xTxxrE z<{VykfqvwlTMcbQpIS->t84A0gLIKNN^O12Q1(^-9Q4!HvMw%2kpY*33=HAHn%Z(G z)+xPTpXofsdcSg=J}FA=eSJGe>gioLyy2-ugmC(`)h^gu`=Z)O2*Y zg^IH{Eki~rL}75rqyfZyDPBP^33>g<`TF?dh5}%(z3|$F5+`$%nAMrAy7(NWV~G_- zse^WB!gjesms0Vqmp4z3Kl*sOxsnE)RJBkt5ae#^9uQMfvtMd+{_he>@s+a7hF&_1 zUi?n@W=gcl1hM@>)B(+I6csZKw0dy+kWzTw;VNh{DPUua=e^ z;s7;P9vXoNV9G0pTEc6sGYg;!&q(;Z(F@+j!)?;#G<29H7?9Sqs;y!K;nFe-ywy62 zCpr}&0{rbU-r}Vt@rb_LpDBgQ1BgN$s2Aj*pTqs~M7eixXi{*6ippj$?7Eh3- zun#G+?gkb2p{%_r&nUA}N18)u*KjhL^4 zYiMC}nIlqcs*7ea=uKoWcLVambQOJ+fv5fwxTV~uug&?E|(_zVL?m~PE?3&S{X&2OG&bgPUx8KnhFlOby78$rU4R7+`pU@&yZ zDhoWIlQG+C2>z$sQ@GPYfvo3X#^L^Tyc6PPbPHk@I=PqL~85GcngbVhR`!!t-2ae&wTZyuCBKEh}`mJ0xvJT7cPN z^v!%==VnQEV6Cvhd@p#*`NR>v1WXi74EoF^<#sa&jOP{)fMThF!f2Sjl3!Usp8_)x zcsL$SgBSyAFkKo*F!LDj5&i+9@pzc%E*FmHQf`r4lPPg_Hle`#2|**QBzlWFjHM8d13k}acP zr&dv&GpZZPtFt09X@G2{E;DPwtOXM>5Ad;)g1Q?ZZEW0OK|~Ejw-n*iRWJany?8N1 zIm|bu&SE$n;ffh^lEzq7Fii@5m?p1>5H(M7B%w`|t8`_itgO@xbL9$MnJbs;%49i^ zMs6rdY7UlLKrmy)7lg$l(1M=N<&(gP+r?x;;%#7YAUBBuGHO8{L;4s#5~YHJtI(EG zZLSwP3Jf6mb|R+>jxc$B2DKC-*6yHOdA2aWAaW^i!l)(9@kx+NL1878YnD%fVhWND zlytC9f)y2zXDx~TQhEy+}_65$+wM;){L9Ysr+;VmU1-AD<6 ztYbVHgSBcw@grKb4+>BLNs3}gFi5q=dWP~>@gz{iDA9^hd$J2op+TTehU#czn!2pp z3)q&aVNHqLtT#aWdpY08ejNAW@-6XGT3(TDW9e6vlEp6Z1gErC7jk5 znZ#dW&!NpJ^@c2Glv5`ot8sTa%(I+R1}mr$uMFh;__X=0rHZWp zn9m05M&V?2vw$sV(aFLfgP>5;Vexv$R*Wd-U{XKNNLrN4T9wj)NxhIgtf)`{F~BE{ z-@Z~j=rbtht7!P};AA8EmT!W04wSb`SFID`BZE)kc54*Hk6~z2cnyQI^q&(dtWh)P zOE)_~EA+PL`m|_8lrkYYSP8@mO&T~svU^*E5~`Naw z+jx?wZp{-jK%tWtZ&<4<6f8xXmao_AN|m>#=hSp{PYE`B`bajMz#WRXU^5z)ezn66 z_CoUxi`nNiNw!By}9}a5W@;bzu6sg_7R|7c2p~CA@&`yw&=-re@eMgU% zT`bFFUabEm>eU?ayj?4^Gdt{tH8?U4!4Ftp?PP6_41nE^S8@%Eau=1FRTYaA9efKi zml~LAn?V2G!S;Z=a4)P8)g*Kq;A{Ykig%@?YP~VqS@DtO48N|(`c8TU_ zf2`>c;F-#6D1i4Jgnb6826Et>VhZx0?nim&N4h%nh6)u8eYUDI+zszD5yM&); zWS0a#Xp}lng|TcvLiKkVU11c{I=H23cW35g2BgV*1W3|z`yC)S#J6~@fmD&q%FN)% z4#>W#ilj6nU4A{``pL5jj(P$YzL~6AMaFun_HYnQR|kLwi8vRll`FCy@1aLJ)ela3 zU{n-HBjJaw4Y1Tf{v+@ScE)8B0TFrK3y3KRhzS_dlEA!xI6~4O(pw@QQqdzxMUN#F zKV=OuihPEySMmxReKvJL5;|NH3OPLiomc4pLbysnyKAGg3%3!aypm<%L-glE(uh;0 zzeSufvcFWx^HP^^!3uY=e|dp_x6D7C&xP()&hvQylIACdn_2lsnW`|QtE%cMOAo+} zoc6dZEF&$#cbL=7iH=}ckmOW)j<;SAj@Uo_>5QBxmZ52{B+3o*B|uG~5hoIYG><`4 zA-_nrWhn#?Ahm3qD&^f&UUCUSTZp1DG|IwmheP%8JuE|@bkdfFxgHD$spCw&u~g2| zJj!#Bbuavb$zEJULNWu7@2A&2tb!I=Qp_!teM}|D2h%_ylc9`f!*uwpY8pY37&MKn zP2hc_t+RP$8fV}V-N;8KcGOu$Rw%dE^D~DsEnyp}gj1=EE}MleSSpb2k|Rt$+ROxC z6PH@!c}jGTr6qJ?!YfmurFl9joAe5)S=vhD%wBwjXv&u$Z8(Foj=CoVJ9X!i3n z2;!;4+c00zOitoJ)=RB^VyVvLnnaf)H?UB7V!$0c^LEs6ffcB%fFLXC$h-T3>&s{P z1o`^%CAc%pdsk4wu;y#{dwmUm`NuWXi;DXYG2*Yyo2t@_t2WNy6DiE0H4&x);*rG6 zQcDst*z8kH(ag3OF+kV=mV0Cnj}&MFOu7Pcv^t~p1}W{ORMyyLf;#FmmZjiUd%Mbf zS&%o417s^;U2TZ7R^xWX2BVw-_O5v?YO2m)w;Uy_x5-KqeY@Jkv&n>9O-3YLGNIB} z8%nOo!G_aC*8et4>vbj58m`En*zvfr8Huo49`#^ld`-7RS=%v6*{lv>S39o;w!pHv z6fjkbsbh~c8&=Kgtd~1#vljAR6$d2|JZ2^+>L-)$({eS7C;E-(G@v%ohCMV%xV2kX z(s!weJnz&qxmUpa%|O3GpQqVwB>23NeD#v$k_>~9Qzbr?$o?-u27hY^yA?j4c0Gg1 zyta74OpJQj%$#a|LPLY?XOYS5VP<)dp!ye11r5w{&%OfL^v7z>Nr!>ZA*7A*`~E#M z2Te>y6CmSJjh;2?DCj;o8nrs@)Aai&4eie?5QXdJNIJ-6ep!L}Z0rDojPep0h9E#T z>AeP4urh2QQyn%SZvY+}aQGt?!lwe|7f)*M1qq6pmL_>5T5!-Ox(kqa1w(gy>%*DO%H=GGlhLw*n05q?gOP%`%ng2ML&&OA)sn)zV_3ti zX#QnoAYttWFtX+wNhMdBh|`zGJOiQvo0>!n^yND=B?HLF4)XULIVhx8NjH5 zkjw~5pvZ9nzVuE~okou~qL5@|CZ{5xq*32(%L+}->EKm^2!g5y%_2`v3yHk|B`6V? z3_I4^yeMumK*(>)weQH2P6iuf(NlUaTJr148<`q71xb6_w`sS_@;QN3^>a033}@shAG39B*q8_>jSnmw;M;wlhzpur<5I3z?x`bjuV{gBe3 zy%|k&bxsGDhebZp2hk1u8{{;!S14|%YuH(bgc>r>X0bDgP1A`r65k`loYLgzb)E&ZFNw*%8E*4ea zVZTg%ogSMS!OKmLWUF#hy`$4(%;ZVEG=125Nw2ThJ32j;eO*TuEpG#XmYk3DAh=H@je%9!Zz6Q-p6`Irtw>$xIguC`mism>7e^ zdrMKD66V{=!k$r(H=-ym39H7~LYniXV1K&`f#YEA5K73m3DqZO9P+uwZ3fTehtTb5VggRKeBEn$}> zOs}r@w7omiHz&(Vf!m06fp=Y7-$3BIH6&^SdGzt?H>RW5BzF@)0X6xjC7N!FH+#|B;tpi6I~(NtS-pqyS_l zNYB&sk^t}+Ym)!S1D>Yp09{5ZG}fRP2COaNyCb0xjzsYu{^jh+xdjkQRx6wAiMEp? z%+?&7Q_*dT6EI%;_-U#whj?Ae!q77OADLYM=DzxBmLunt6q0F4X(ZSer68FscD8Oy zPfbs;LCnWVz!j2AslmKV#*2?N0T$^ig-^G_B``hBNM(qhpsV62Sfm6Z$MD)>C1^8R z3&9fvsjLND$VDSBrr*0#_Dxpb5+DBJi+mnMQV^ZJdS>H=(ZD^Gi)f-KI(KqUi5Vej zc?wNq^gVUDoOD`W0X~s8Mha0k;BNPL#3GWOOKl}INylHG*4EWvC=y?tZXSKI87}z3 z>xvR8tQ&LeCJmw7Fl{4@RBsHOt(;pmd4P&`HH=S(2gXZYbv>01P1p>Xog8_gJWQ6s z02qKAW0We57xZ&@CNRBg5ik;VqNoNmeLBU_k$x`YwWHP&&*rHWjwq$JyNeVmc zt{+P@TE#Oc1;f8*1EY-~U;|j27vVx7qqA*g$pNPn{~PFPhzIlM{v()BDF|>{Mzf<* zfGbEHUEsM>YVpvOTcSYw(OaTjb{M+ZwUEwDEgp6P6YyGiK0T^@0A<#NQR4^0v=a6k zOpt;)W4ZPpnwYIv>ZqZqIZNyxn%H28y+af8mN;o>VxuMY3{5OpV*Aj}%bnMX7 zX3IKeXkyV4F$h#UsXnz>;)tP%t(Mq4G_lPRn}#O3CDYHLdi;cIg~~t>1%^x|#kYst z34k3!wb^J(%IpVGH$hFZ7ikTfi`x9lrXeqzl9#RVUAdsUqJ3Ic#E2oeMkKk4{$()a zWstmV_Akc_c{wI|+2mi29rAK)@)D(sn%y47w`GHoG=gQ05_*Q@?n!dz{mV&1UQSA0 zHu#slLtgeKFLVB7|B#pc$;+&N34YZBZ&dO&It+ z=n_F;lp?11PqA9@Mjn%HsnW;NQh&$ZBXkra!~qheA_3c~trq(m8v2K@LGXAB{)3T? zNe(AD3j%bJT34Ot_KF3hS&fW1gKC9=|0n(4;WO0S7o))3V%dwanTnWMLo^5 zvf=O+b_zD7A$Nd1h3ikKv=oNOT^J zV2a5Ob*TE9W`rn0b2I(RMMPzyE2(m@nO6%G5h-9ISHo*rvScaO@;p#E&N8`LCQ4x+ z7VgkDx3*VIc;A{jx|NZ|jnH&njQ54K9 zPG+B0H0s0%;Uf$#jxN7Eys$@KM$e>{wq?_)H1mGCG%8F>xKHtIJfkt_seFSi)tZ+H zG@iQ9(7gL(=2HV^51=zhf;k?TV-1)I_&lE%JmvijJV~g5^a>Bsc0elN37%Om9x<;I zMoPm@7^w=-%O%Jp#)n{p^SDPP4SJYT^eABO1)!?LWU`X-o1`RoNm5cqFDa!5B_&EX zNr{k!NJ@h+*HA1e(%}C++_P1`XJv$VE>!M4A?Ucj(DI0OB%N4p1eG;XO$A+e5Km=kG_Yz@{7{^=$l<6Z%6}02R{4IDW-|vl zVawy#h_t37;ZG2@=ng2fL@ghLSK6vV-|IsbG0<>@Tzra^C&Vj5CFG(Vz68FrcUk{V zS#n7Oe9o3=S`s~st^I9z5BqS(whLnf53=&IKcj^aNs*DEbHGXOFbM~D76L$X%<0* z*X*v|4C!sfkl_DCcR_%CySqt3qw2?= zhuYX9%nBwDH@F{+9HO_XLNVKo>6H0^}| zw06|q2v8x(QvyBVgW(Xsdda)jM1WC)2mm19nRSfJbm zYdb@)lSG-4*c?XEB+3Y@Hi(gO@G8|_Vlf;oy@|J2j8rSC4$C8PWJfDA9x)1~P6tJx zx|!(Bg%$Kd_>)s(7*6n7DfSe|*TE}(`}q*{7eNIfQ55Sk5s$=A0&us^LcheR8{sFG zvm>H7NB$qCPui&~eSC=pLn*`mELs;FOM57?C%XKKfx_%mA-cR!Lfzz*OXwK zW4w}PEZN7ZlRb1M*w7pj)26qSg7a~Mre7McG);l!pmbM&*|E~0b@36vNQWb5aU%Dl zdF9qNG%nKK@f8_*mqb5bwFQM)C3s#Ih9@YUT1q2))nlmI^rza&*=*rpY3vwn?NE!# z@+1zn69k2ZnC*d^cE#yZ>oj~RG+}&5a<6Ut9HYWp0L3T|igqesD=1D6IrWTaqUdw^ zmYaSy6NQscF2Y1_{y;DAM@QP7&;YC?jrKIEf5g9|sIUoW+z-y>Gw9qMC) zx|mWI+XYRQL_`8;el>j`b4BmT?I=xYX;>PIYA$ee6gkcinX=#qp-2h~|2l*mGg zGTLb}0~-wxhKyBTk0kYE1=ksUQ|0EX=5T?JT(kC@0x+`-jU9#fi?hZYflGa4$Eixo@6u&{$=d~kC! zL;-DdApSxrOv6z>fv-%_&mqxEqQ`4s%m79zmsf>hMurAxU7De0Em~or#PdtEr9?%W_GZ!@aQtg6F4PMd= z;f99ODvyra_breN#dL)|=JmzSbOMYU8o~zfY^Y4Wt=$K04+{uZxR-S_prs+9Z73RujOLwMuI|8SJ>E@_n#*A zWP@~r9~oRc1lt}%3Jv6{&u+m)F^81>+2{0G5bYs;f-h^|4!lo0{<(-<^;wWd%35nRT3tHLb*k!F?H+al(#;TP`Qa^{4`+ZVoV?M&Cw#b-hDL?xMYZ?k+A&UcgiRs( z-RG4W-`W*S2CwP1?`X=1yIO)BU{i=bzi~(jc2Z3t`VSqpLAle3VGqMZqnM%})TmuV zENGsi_GUY(Smy^z^ElYF?a%YFCECFyc=;Vge^y9W@JezQ=&qk~*^LK7LG7=oTJ$}M z2OXx_3|u?<`867_{x-tk`?Hcc8|scR!Qttz#lIO7yf6J+d}eo`W0B(SU_6(N-NBc- z36YK}hH98MdJ}YQz_81qG#~54inCx??^BQqv(@+W zmSh-2p3!B`POuWMrzD4Y8IYsu?`*Cke>U&6nZ5+#bVUctI4N>@tO_?Po8lM~XOYp^ z^plBRa9?5oMn-?pB@8~%5N{YCOybPlC&vd<(%WOU7QC_NU)jGZ$|4w#XNfcudn6~@ z-5kFo3}&SN5?>PrlQJ(|6W<#K(<<;J3ud%dp51(Md%A3&-jxBb*9FgPN@w_+#d^p?Wi))*Z~7idt=TVyDq> z#(6T?PpN4X{JJGxJ1KYrxcJPZ;EPSS+tkdloN=kIxjnvla!?*YqjBPD46{~Hv}aTN z)5$>>jrh&vpr_~hZ$Frlk~xDH7vsa_JXE3hgL1rJ*dKKCY)}Rkvz7tQtPDg>vJ8v* zgDIJv-;O`hAIzI&rzunq%?3vP^pg6Uz;8uNituNP{-!_Z&8+xNT$qAlhoeTP1czsK zY}&YFN^n71;OFeWCeHo?n4Gi`H%<#C@H(D8Ef~}EU$lm~DEM`J(zM{dLpE%B5C$fR zxZDuwA@~m3DIH_WsN|tErt++9SYaBd?u+C`WBJDE!674d>2xsdk19}VIUhV4e`b1c z>{Q@~7Y7z*55Mg%)ZlRmC_`s11|A+@I}QqZGCN<0$ImbTUo$`G$gFrVKG{ERnGdu! zzZmbFAIzDqO_}wusX=QHNEer~@J9{ADMtdNwJ*l+Jv7i+BJ=hGNE=>=KeAtNV*2~> zzwH<3oZE8`3nnz}GQ1P0u0AYyX3|<3>pN9$jPxLwGzum<-&f0~OhiG~99{({Ugf`z z&zc$ZHYW`fbOvSy-|YGW-}uRuOG`O#&Qd8Fcl-gtX(J6WHHnFJ!Ke6Y{Q48>7*wiEo zW;FdOkTR%kC~jjiZ%%OU*aXG5?~CF(v@w_)wEvHW?WECbnNJNHOaK`iySKdo41??4 z@`j*qDA)VR8-fbsc=9}W|N8i&^MVWEfxn*@98=tI(;yFw4uJ>0P~(Bi4q;4wAAkCg z;Mjer?7?BFY|mY_Q5~MjuK&sqD%oP}!b4J(WE?gvtg7(5P(Z zopn^EhheCUg8sKuc2e(`v_9OI)I>xL@s7iTyIz+{Pa6I3YYg|CBZ8wjGwQM_r4iy>*(L6&sx^yZHI ziWe;@I;Od^(HD@5-!}s`4ks)hi5Qizx2Il=-+yHAWoTr~QNaf)#;ll82l*7t!N>s} z!|w}v^Dlhs-tX)2wp(DFtyFY42fXQi!`JPT z!;T9kv>^Z@o=}^x80qY|;Ls6Bb4a%-TQe0b`P;_@ixUwTaU{|jmUEV798Yu-gy?r1 zAIuE?i_NGAKbhbgtSN$D<>DV7kHq?L{G;Q8shKZj>@hB%5Nzb2rzcJbwsMfuEhh$J z@{&*>l zhAr{R(}GD&Es>g+T@c36#l^Al_S1q~{M2bd`-tV=00C^hohvm!vMq7?bf&2mMR#sM zC)N^=KRuY|Pym@P5h`2dkIz3nI3e8<-+g-UYZh3yyeT-jjg?iSG;vanaj4gxHw7~? z%NyfqZw@{Lv!jgB#BW&zZtX9y(MT)=Y9L)@n;0f@=4v-oDtmARn&rA ziY>HFT;yd37{X$F^qIlCY0_8E45mz0KwK=8>7~5E?&J4GZ_RNcfxH#XQ8{d<-oKw2 z^rZh9cb^r^IAS?tfkUuNxH#dJvC*0J$A@4i2>z6G*POJhJ2*uXxDVT?l!1*XL7)@q zLt5fbofW(p;OsanXlI@Fr?Xghwde$x>;l%{t5B+6si`9&C9XkXQO?3#c=H1sa~}Y z6>XxM-Md%lW&uCTOljpcyGB3#tM&Rb2FU`R_S9?PbT#8fKh)dCz`qUr+x8-LNHI>+ zveBJ_T1yoGHj@p||7fGq8&N?pTi)ELQkSPoHna@>g<8gt#8EWGkdwenBU&_#Xp>(H zyf#i`h6CY(rO^Xb5N0ZesO%%zqQ+%`HI#1*h^=)%%zD(AI~0iVj&lN{fYxv!$u)Cd zR5;jIFf*y3H5$Z)V0GA>RM(K>d{N1+)r~p8*f* zv_4W5BXBG;?X~2+m(o;@q~qswB$Y{NuN!SdH>&L>r6%!dyz=1F392zw8#WSFJmZYSNR5|awhnHKcfKPydfG3oQ7yi0c4C%V zJ(vp)-{euRDL&`C;G&u9e)*u(n>RIckWgA)?R1o<;g_+2nez@w4TZm?6a-&!#XW@ahDBuEuw5as?OU%5v9!Vif&!9Xhycl1? z5Gadrh+LTs32gn;xTng!B7RNRUyHx`u3!?USOq*VRR_Man<)jmN3leD&m*+z$M3m&Sam4qABZ6apVN$66Sm z4&5#dJK`-DHI1(*nrtG}>iENq0u94994rSalpjH9qSUErH5YIa#Yn-66$F_HgQoaC zA?#zJsb)%ejm}Q#3@0ieH7*%Km(Gxo!|hyPRs}92-ev0#xHIguBIa#6cs8*e&Vc#@{))5A<@&VdoA5;Iygou%2Ck-e!c_bswd!bUAXss)^0qREj zLyc&B+I$oFz`kz>lX8Aw8<&=vqC5jTOdB|HX=oeV-fh6U9)wmK5=5^M>9bI6LqoL< zwynOQ+6LzVpy6Renh+Nni-ds%_LA4lKlDNXuhVluV-I)>vdk!0UDT+vN|FMrsZ9cV z#o*4lL`#rur))DZ#s|LgAE z<7}$_`2W4nnfn;_K7*NIaApjK-0z9lB;E7r{;58nnlhCzgzlF^Dfe4pO@)x$6_P`_ zQ&f_Ql2j^{QYgB(<@sL${B`~LTvdA!cLyw`ob*IMs&-Ibk{u_~gGu$CpK z97ZvTmv2H*sXQvJ>r5V_mCs}Fj+syxH7F=y00epIcd1h`C5Aba&KA{RhTEpjQWR@w z8oLM^2gRq@VcxbJN5J8_a`%D-A>ljkn2k&9dicoTewD{^5o`bIZ<(kbRw z(4#=A6TFyEB@tl`p73g-8wTC31Vi%t^~iG+o;4x)D7=}%y^5F*(&{709;gn8n*rsl zX`m!2nYgKfbU7U+b2sWn4}4S={Zqh{l`cnGD|ILX1C2IO3-e_*iFt{XzsI4JGoi*f z^R$MXs2g&YE@CbPUJ0j4@kv9@b{lZE+owQut5)|V+0vK9Y8Lm}JFLb)mL&z69nOkK zV9C02!r9$;X@aFxC{=iPm8#E%NBHcWRFqp!;cU0M-PfDl774dex8g#|i|;Gz!lEy7 zn@pB`pNxILaMs1rJ!f$#g;9*p9?NKx!vzl!c+$md6)8{oMh4k}m-WyIAD5*!kEl|^ zS;E-qE@SKWm^EW(g;V*^yX3w$WKtoa;HRPZwJ(i_*SFnYw{x32Re&ve`}CQli4zGGFQwNDGjV{HlPv>0NXO^(V_T+U8(xLR4>2dph; z!sIYlexiHuu`xEs5>T-!Jg(FcFrE{w;Pddo)Em@M~g-sUzSDNl~_uHk1l%+>! z<>Pdjt(+q$ZkOpfg8wN$C*7GWB>C(!=4^KP7R1earMoS3M#cTYPJR#Js;pJz=>r$u zcq|f;I~Kkz7~;&>O34dQiKW z(*KEs8P3#mi&Fi9Q!Q&_|cnVl7AZz)2 z+rkx_5xMh{UEzzj$EC9hbkYlyVfmzMELj!gLx4`OH=}HV96HH^+2P$37nIa8OIwCz+h;_AuEUVZe?ZP$85}569Ek=v-TI{WmCo;26 zy#A$Z!j)v~N^*OJ3i5PmCB`+KBe@)C%8W(cz=kV&A+gKyMqVW;04H!c{WDUjv?=;< z{f%##$xQrftP(a7*&11FTK-Ob#e8^kv`g1|DutC#Jk|TFA1<>u0)uWwK zC7I4%6l*$FA``rRWDTWvUZj$q@8#LlPVBR8`gEjlLtYY9Vv=_R52Syo>l9KiQa5_m z7WA%$<^*@*2WN^ucS_7`l2VE@K9fX?FD&NsE?$iF4!(Wb}6l*Al54McXv*2&iaL9IkQOD z;FfVEC+TiyG}T()eJ`?-Gc;}xd5r08D${NKoE{wEdJ4)n#hNl@=S+P=6il(!ET_QO zm3W@1wiA)rlfM}Uol?SX%|ckP^OMZ5!)*d>W!|sbHVxMOcLoWs(}Bn;1PffP1Gf^& zAk>@5{AZf^f)2(W;#cod(Px*^23Z(Js$;3<6PC#M8Ze2F4-~SD$+@EZB>f#l-lZdy zSZfmdyd-Hx3G$HyR2^p_F(ygU=~WQG!luj55?CHgE8Bx|kxx=!h66Is*U4bbsa(js ztE(CE)-a=ILuoVk)LkzrcX+CVUt~L7D4fq89F!b{gN=-7u#p_HmZAwx<6W`4a7kLM zRVnWIUQ$wO>OERv1)lZLYYqPR#aeZFFrjKhB6d5*gaT4PKPw(nkgHLo8`O!AIg{ZF zv4R^SLaA3)+7p*C^q!P{1)PqI5abrOt`xWS+~06Ay`ij}6xy+ckg~+74i6^e*8E)R zQA%DMo!!mFZe6pPrMGeA2c`|m%~)2kWom&<1*VTEDfH=*LMr?3g6piSKtih!X1S;= z9M+#@l-1!_{}MP)2N=geT}g{F4^{&xlbZD`UByPw68wCQ8(|2s54 zJCsWs>`A}xrtSZdBXp}IjI-7I4M-RNPbyw`2WE1)iaW)G=5{DB5IAd(t_O*m z<%~6Fd}Zv>r?e3&{#UfVUVyHVmxAwh++ub+1!=YtUY;HRiNITK(u?hSNR;P*ScI1g zA$p|E)#9><4MBy?Q~49oGy3vtD% z0e;<;b(YO>EV9JSZ36wUCd%F@Q?$gv7geNhL#SZ-B-Oyo9Kl6P6@^s2PpS~HMn*~f zkt97fVw}J*Eg`WJRW?kQ`iJRKlS_QlMC@T;7rxZkpe=<|$R+o(F;&R& zf(dFvM65SHEJl2rXHk zrX^%lW)>b_$RtBg{dA0ZZo-&u>Fo|?27yi@fJmAX7!h27DX0$vQAV-B0dm;osDha2 zq?1LtvfU_R6YG8nF@Z^1fEJVr!XjNWebMqWL9nYO<-uN;SEa&NE(e+ammCy1lZA41 zH$fH#xe{!dj@m|u<%_0LyNz@WIg3Pph#&X%kI01dY;Mx&C1f(J#X#B(jCBgf4e*G7 z=K72*z{>T?g9mq8pC*L$<0~Pqh_Wpe3y1e)^}L%kg;DOBg6yef=U0!_29dI*2Zv`+ z0kqxt(^tF=ruQdqg~Gm+#k~@0HMTD&uV6(W6zYevXg~UiK7N~)SxD>6VCN&&!r05J zA3zkpKsF26rj>6&WrUemw8zz8eNR(%9NE{af*i@aS$xeltUp($cNFE24{JcVvcfBx zY~*e_1TRG_Imz*3)2ZjHc_Pr5dMibTo-a$@a=nrzkZipg zFKZ$BdGHw#Nu9{7r#F*5?D^^iVntO7DKA$(y4#7fH3ZKpr*z{e?Fl+4yFGc5UihxP zzOjj>t^PPS$xV=vO?QUv-424e3B@M9X*Z-!jtk+TrRUQI8g{E{7%} zaz7AjY3s6e%#XfiiQk59%aIczG?yG_hImR9Pcc8RF3xl1N8hT%Pl4+v?D?55ev*BD zm{Q`Lr;G?+(ip)Lk6}t$V@HN^obA8kNng8^h@=3$MsK*21C!#C0@8EgJXVOu@AWT7rLu+DSmbDOrNc3P#4p_3_WD#i-5nZP5q}@BZkP%C}JXxpHo=-~i*emsFtK!b|Womy9vGnO1hfm<0=v}ootM)@LEB!d&@8h;Xb)~5*=8)f&4>MHq> z7dl82)rF<(gP3w&g{n8nFAPrI%CnYcxX$c6x90TrKSf!0cC4}KLXcH6X%RID#$8*K z!+V?g+M*5t5~_A0-3C>58D)ra5rIg9rI1z=hwm{@0 zGNnLG24NI3MkSFXG$J;Hh#|mMZIbQ~D3N3c!c5^1!3#5~bxy*W40LWJMYEvGM&V>x zU=>a>WedXNoq(=HLjN14p+NKkyjNLFKm0rrXOVLJMEX)lSR0<@B+eAQoF)qKYW9Og zsSVD7@KQDPOAC#?F{{}pMXl?cP`|lHjdP96{yhd^J4UKnc!NzY(Q+_WO&exaj<|Iz zBOo==Ru?3}GK0#iC|?bWSEgQJOOqjw%*QITB}I)dJ=6tRO&~(4#B#SQPpd=UJx9*i z8F`U_@GX2TM{USgW{lrqQ8uB^%A$;%!s2H#XhWAl3(FW(4ubtNgGPiy8CT0Vw7}!g zuyCk*z64PYXR^q4Bu(l_3Wxb-n#iQwl#TH(<2p=f{J&z#)c;>|<(Yli|Kv({4P5s- zRI{EA#<&eJUARr87CmaXJW3yQI*+ny9^=sn?%5#|Z+g(>>$)Opmaj~c%49O+vaf!- z;Ve8UvB->4k9M%0JI*n}VZ&&rmmS}u%^U~c;x}*f8(LT+(@1l`X~CpK_wv&Dx_x{Ssh9n1CiH)Z zupvE#bmZlmeoEww#>cT*8axH8rM}uC^=%P3Nz2Ca$O?fmed0Eh2z*+c5`ltKitc>K zBsGKCSQTl=kWd)T(I%wlapI2-4t=hSBKpdT86kCa;1Z^$tq z@TiVRsVJ_Ls)f|R=|XD0BzL_Csg+L`Qn`0eQAwIv0+)d0rJVvCVs+iHzpx z+VW)p5z1T2Pg`MeEag216v-IwbDWPQt_)vXdZB~aiDXLpOdRv1zs|0~Bvi&lZgWa9 zctpTm_0ab#Wf~+5h=LSCx5ic=H}>USlS(34a7IbQI41p-QQa*#&KgU8H_7Bcr$Me( zP?Y)w7n7+~YHU1C)$uaP^Y# z0VtW`S@NT2L)cd!F(^8@22S)mw```8DAOoTDad??A3Yz!xok?I#7Zz?I=dwKn!CgY z$>)HKNbz3u9=CYKm*JkT)^Bc0w89?R7qeev9`b=ka~>Jt{3wuK+Sl!H5 z)%z2y^OLrXT4z`rNeeBY>#H2IuFNX<7s|R=E;iIJdI0pE0^Q& z70bQClO4+f;i$(;tt-qE(GNCZvt7##}Isy#1$Z$W%Dz|$QCd|QR znN6{L?`#f8wvVd8&smLY@X$xH9C?Yq9+2QZvB2xZ)XbLWtR-Aki#%`bLAiCZwcRz< zRX0tcvW}`Jr&y~ztDaM-Mr~DJt(t0OwA3l#8=U#c;T$<_lGMnM&Mx4r#g3&%*g>q_ z6v=$Tw1%tc)2u>O_XX>wydx}_6!MicI>y-ACA+%RD3*LtPm)(_rd6e`KEKX8A-IzR zkWl$7n=ItX$Bx~wiCumEf_14rmrX-2vu!)gYE_obBsZP%>JnpyFR5I?^U^5?*J9}` zrk0bZTSev()pNRj14a^r*C!fHGfw82IO-_pyshI1H0nJ-wC)x|IJm;=wzyl54ZqLVLj zW8N*IuH}k|rW`LK7Ve}o;gO4&6FyTxDYA6#mLO}+WeKGO3)S?QR_(JEV*~ZqOwRD{ zP!(RXYF!WuZacxbHMY~O6T&MD?9q{s1@42ys)|a<_qql&I(pUjC95jcE0R_)TuCO} zvK|osG`So`mJNss&cf+x{!7-CX*yPT-&(H{W?A>~%D}_3tg+^Jl|GxB%SY9m+18C` zu$5l#I)kmUd}ZbmU9M7B&f#k7cvUjT$|3w=bF549)%0};lH9Y@CtQ}Zm!BeQ#}(u| zRFb?I)76nVR)H5JpA(4{%AclY<0f;hs_AapX)V?W)qSq@P}&I1B|0qjnX2} z5ZT^{_h3ApAqiWGS>js_iY7DJhxe3iD<4e`>`BrQ053ikX<%Unlr8xbfntuLH z9};KJfCEyD)rc(W#C)q}#?<4u(yNEKQuP*CH=8q~Ll#*7HZ9-sh@SeYpBGv?0(ji` zGX2gU>gSiOY9uA)6)UgV1|Exv^;7N@o^sc*X!P#nUj$B`A4Kj`O_o^So*BiJi>#Rv z#czwOc7)w@v6a(+u*(gYbZ%~F+zlf%U%YC-Vymfme`&E!;nu}Eg+DB|YST6{mT;SP zm#V+S+JDxFN2|rJT3O=#omX}2iA$|;Wp~aP%*zml%n-4UayoaZb(Q&!8nx7F7jbu< zM9OE;nN>%gb>cWR{Kh;9g(nn6(NE1W641hA zA643buM*|Qrr!yyo_c9Pf+2=H|FBvZ(=45!SxcKN&O)k<;vnl zEK7XS<8c9an=bmt|Z_?b(2bWYxn3OVny%HR{Z)J4m!TXVVT=Up6& zU#HT0uL0{NwQ;$1lX*ggS6J832JT&9J=5(u`bLq{=s06iH61DPD-+Ln-+(bO-Pt2u zg}zmnNG~o^DY|~m5XlygoxE;U)%}J^nIX3IeckGC?l(GcIWSA$mc*fnGUm}&ATG<< zJXs9eCI#qw4o`jiy45-4Qyg&~NTi-@RcCtt}0O zg>Ft8x|6_wEH$QK>52ZJC?1>c&ht*%7b zZ8hos{}eM@bZ5qVl9-Vu+8G8ZIXY9_zlLYQ)~Q$5SQojmJTGJ1S+d4GdOcQmqD@HF z)T*^s`@puj>iAl#Zm>s2n%ceI8gxm9e#*ovOA3|q2y$eZPqezF>*fxDGl0PBFFPX8 zB33+t1M7HOY_>{VZ!PfRF?*<8>#dP3+~Fxcjreo!2I1)dkB>V0M89a>2H#GqD-D-! zu$mz*OWv3@JMbwFad1otK0-@@@7-YSH0P`7ZzA-5tv+~5%f3y-^u5x`HdlGarESwL z)x5X3RQiKUdEfPX*mW?^HO+CA*{<@vOZ(3Ca7fkIWVOrowK}<7CmUmurms}rO6e z&OY%GcVAtWX;v9q#tNS;i?k`Jsp_#L`{*=o`|)^Y1dyt9p^ zbo#qic0CD>a7hkN3&%sk*pj_0ZQqtiXRAH$>X3eW*UIx=;xqYRNBN$#@fPdqs%Kp2 zmDR&3N^N1xpRL~5VmX!NNwPd6tUsys=VZFa<~-_Xy=Qf}^ka#X1rnMmSJS9Z>xp1` zbyULG@DH?Sq!i2f+pJ38W5A1^;puq53-GAOf8{(q9jA>u!{3K-+N3jRZ@DyUQ&5+` zq;`gl@nG4c{ zFquccAuN}-zq!@^zLhOP2m*|}-(zLy7kg5-SxqbK-RVfc+jlx}Ue|3_&nn*p2|$0; z!tpq3cW}}NR^sg#bVpku5u5(2EN=>bJqv!ofCA>D<=^2 znbZl-YU;kyvr-o<)6_xB&Px6B9ZlU5c}J6TVx>o%=#ctEH7)A$^dxPZ+F24@I<>PT z$rjf;$E`Og!1ac8Jl(Zf`X}vdB|mYSK~Lgl7(a2FX~%IhC&F*sYSdsHP2eYPvvLSF z8~KUb3=*4Rew&RF*%p6f%m2V;EkAKzrN3h{ji0#9w#Nx)=i?Dy&@&~V1^$3aC7@~k zfHq5BxB2rrOx%p~yO}K6nCZ{PILT|NKdW;_<7Uz5^^vj^O&EjKWPajl+#_k+$4}g5 zjzqS|AK4ZukL~{QSRm0a^+*4w0O*5OK8%yol+EitE9&1&F#5vlDv)2b(8)s zC*I##Y(nrp*Ou*8PLA&I0=mPiDaFKmpWe&awp0DJo%!H&CK%+HF-`x_L5WWCZ2cTa z&x!?UnR)u@F5Kvl;`YI_*^~URBtW`aH)hv8_KnNiS4ktTu#8_U7OdN0WoG$bUXMM_ z40W&Czk>~$DQW73k6BLBJz=@taz&DKj^(V@s*66hI%d5FrLxYEuuQx*)zpuzsvW3Q zMi|Mm^j=yQtLw8_6KzFO8>^w&>X(nL0p>DQ{E4*yCA5<#vtCmV?X=oeTgI^Nu!I^8 z)DQOKo-E1(`RSe0tk=}moxK0BLtXW$)e2?Ur!0Z&P-{MA3co`g`xLQzhibixWwB-I zhF!d$wnN>!%i4|Us?V%ND8bz+0Elv%hb}{$PGKx&fTn_4BuqPilE5woCCa2 zFOVU{asZ8WR_B-Ds`BS7Gb~dbKDVk>-@#I0IC!_Sm1R60J3(Z8loJ~Rn;3_NtNx!` zZK_HR#NNu+y2*=$xniG_R&xUi3vwY+x!}^HFXV7~l z)#3}XHdmE=LFtTCAAiBy?_cUwqea2kG?*gwoUc~)msZ2fKa#BE^ep$`Im2owJf^yT zX=MpB6EQ3ZYkK4+nmp)B>q&D`G-(ghBXgCy>?>=|<ek)QO9#-WNc%T~lr6 zy^1(4S#AM3Zw*>2OVNe0@+{3lyQ%E?Q_j~`=6U`gQ(`3T{j4EQ_J^p(er^3+#SP%1 z@@Z1zzp*keJ>*MnK`iV+BP9!G4SdGn(~`@|GmVMUKi0zN!Eab+{fEdR|0}X`nJaV- zpB`C)``n!yCu=&^LfI*ml8%>{FFR+Heq$V-7R7J=!lL-AxPnW?JYE`7_IFm5e^dM9 zklHt8)JDtVg8WqQMt13`wWVp%R&j$NZbMslZQ{*5=7!Ch$aj2e5Ai@c3ihF7sff7| z16gaNzf7?#bnUh=GKrmbI7aV&8qSa7&vf_$<7M38$^dZ8=?m6v+6e3H}A|q_OA8WgfLT(OP7!{BXS-72>AZ0(91D)p0*7Mn9A^(r_USJw z=-ocK9$21(4o%vqbLDX`B`kw2%%}P6#3Hq7zm*$eJh!Cqmp;GF9Uy;J`}bQp^?ZY> zJHKJrs^d;^WQkPfIgFQosB?a@x<)4N@;rN?`7A0BJUwqpFKb9rdYNIU-!LTTqnt?e z87BCh9rRP?$0-rQej;wd@NB>r!el??M?d9Vzu|emvuFH<hCLaJ7EuYoSAoqpwa$yEbP5W6mwjv>6CiZp!=l`IdS1;dgeG5 z2qm$D4_UcQXZ;9ENvUwPoHGbPcSjU+GD{K8T#uTpsma#o>b*leJ+?^fVZ^&x-uzr0 zJ7isNDs|N_R;#RKP-3A-&8~9}x>3jK`2cINIwc?ef~Yq}HT>0TmOmv; zHfqf<3v+zTh@mszu*L*sIK128qfEU>c;r`Wbzsmc)$y>^v;S3@r!&V8)_hLOS zKcO??O=pqfMjd9|dWL%cu$2?iF69nz8Tr^@tFZQrWJ1`3eE@Gk@g_lTi#gO5s_PLX z${DKUi1nYwd%o?G?>Y?(k*Hkv?y0qX-)_+7*7Av7;voq~kFerCLv{Gg%C>dLoTL+K zlBVAAo0XTZQ^2Xm@+shmuefs)F!wjBz?>1?`kR$%hSstq;(sGx{kZ54zgw+>=69;r zF&47FQ+FM+2BLg^%(^77{0&wAI350Xs{3*4dNIyDZrvzq;t5`1Mm_I@RRf_wq+4rM zXXD>`v1$BbYDct^w94&2 zUCO8wALXL;sV;whLD!`ta*fNGb-Ko~P>y+>qfCi|F}_~@DpN?|>slwR#*x)};WyuK zUPExIZW~;mb=&w-T%}9jk}iBNO`HKplNPR`!^n;sM~g8WH|~F#&Y@nsQ`)`|V<>JM zu?So~w%I_N?C3*8QY@r7~(;nBM+-e<#ZZS%S)sZs;tnRp1D_PPgX=38&XLs<3+-m+ ziitPcuO}^^yy+Vgwm7eK-l+T*EGm_qIFQ64BC4M?dt@) zP7Pa%Kb^U90r@7WY?7d-%hgg%+YFplAXh6AQF@9))3n#+Z|CJ zNw&X2`ELs6&u6PhiY<@N&vBIxT;*7bgcdDGwJV$1MX^D=LVi%ohxvhCVY7rM)9WRq z)!^6;8)ae6Qpyu5Z1yHhx*(%6Dbzb1s!f``f`{0DNwd3}t5utHTb^aVCEdQy{5<+` zI>-FWi2u#-hZe+^?1dJF?Q2rR zH%BT?sUcx|a*EyoU?Qvv^Xx7->77UNUWhizw|57!wbNv$9!CaTUxb$+;pNq_uL%rI zi{4+y9uUkpgH}UTu5Y&y2bb5k=LTVuKN{M#8f$l42xbRTH_$lS-aJDP3$Wnwr*-S3k(jbo=xpsmSLW1 zYS+egUQ_#elt43z>5u4z&FrdXx_&J`sHa%!R13RMjcslZ7q8V?*abN5*ut)lF?x3k z{JQ2`^+iiNL)`q_(sNTMiFcW~3#~e|(lOrH%C3p)2V2<R;hbYUyjUKtJLyV zc6(ebs$VfzjlHB|vdV64KS}2?wYB{KEhMWAiusWm*VfLeAd6Y7kIC1BK2oo?r75pd zhuhk1GiVEPQ4CJX<|9CD&$Sx`<{Vaco@+M=y~QaX_YOY~gX%+zYRl6s99~Wz zpP^p4&%P_v?L5oK!230*NvJoTr|REtUsY{3W}-0WqgFtTq9&uRK2J@&-~Ki2o%XKz zgZ66t19t!BM=@K_Q#)8jCDd%xil}u_(@~qDRzbZGRpKL_Ru9^BLnjC~1@lzwCH|Qm zRG$a!fmYGI1MlkFzh}vvw+!feZ~xni)mIPN`OUM=cSFlZO~v&&sFEr#_02I$DmtT9 zM!oKQ)#xF6Xp7TB8(?T}l0ktaAnqhQ;P$(U`xL3kW9-6|Cjy316{;FL#%|catY;au zFx`u78tS=ZS`ytcrd|_MZ*8w?JZ#sjvAd&XWJ8xfi9_O&WWPr8vedTs?G~!XL!qW> z>NLBbI+zi9Ip|D&LXCPDmY=B>KWx|QvE7m4a*jN)E>_LyhBIV{XJF)<5I5j#X53Jb z^@O-V=A3bZC3`Yn0Jh=m9irMlVvjeMtF4c~^?$00kJ=prZM&!|AGOanA5dc+wR@Rc z)UikH+bRtkj>o40!fv`y9(%;jQhn~XE2?pi+0UP~&hpe{5xY{Pbfgz$7W*%8LpI}d z+>pZ(l^Ci>_|<^?dBs+p?WVZPT*LV@ZlDl`aS+>W19!M7B`T-eQ^T=+@E65Ny^g+GBkrpO)|YN9qkeG&m=Av;+qMQ8#7DYILLnA92RXb z*zRuzhCLmf`na8Hs-8part0}2c5d|wmsmz7s19m@dY~z2dx@of9b&J_YZ!~cV_J3q z^q*&LthPOApW`qu(tpxuh13)@1D?4#`W8NOOZ4*R1=C8kf6D&koV}M?#*g4Hkan46 z)BtrrOK>je2rdQJU1q89V|F9;(Gzy=zs_^RiU-ynb5VFe@jdqzm)u@l6z)^hPrL9U z>u{R;^G`E3R@M;v9J}Lb*6r10L+l!+p{^KeX9u=FukIRZ7gjiQIlMiNa}?>HRaTRS z+G|zHNIN&TxOZ`%;*x>a_wC-F__G=iUJW!zFzOhHonl7x(Em@WDon8fB#MqYjR?$2Jew4L=-i+_>?z zCA~`e-d_@aG#*Z@BODO7`1_8^_R`{YRQIb|7tmBEjj=P;A7kt~MUCk-r6;TNXj#IW zV^UzPCqlJVULq5BoCM33F}Cjr;V7ySWy->OMCs)T3kVE_u0F*YH_)56Y-j z7Ui|kug2Qb%)pUF(I>{+#es7w{L3;zpdM%mE&_aF$hZ|0gI?ePz!!(qEmQ4A4f1ca zj6%>JTm)_cw}bxRe()&Z`!dFa8>4qmu=CRzWZ!HVH9>Q5KKK{-H@FM*0Run;jJR1X zn`+mrwgCMS@EUj>ya`$sTSmKLbzrJJDbVu{_0$V?&DO*2aEGFzl5o$GfyH+g_cx4E z>|~rM3g6bZZ|~xw67&nOOQrXD4?g`gBf9GadwYV~JIj7JwNK9;;q$}2i%Znn$3o#2 zR}Ylo@8Xhy7xwO3G_W%P7<#aw2#rJ_ZKzXk%eW9+46f*Hse>=sHN)7;pU?%v#K50t z93G?ACK?je{Ks%VFus~+BA z=LAN5rT#P5F6{Us0nG$2fmxt^&w<7Liw5@XPp>l@vlrSgru=N0!WYV|(5o?5)muA)lk+nLQ4 zL%gD>PqFTddK3*T!n_2Cpdf*~3gpiVV5!2o@bm)ayzi++3+#>&5wXPo z=im$QCD;Sr16#rS;4APo_y&9gwu2pDFZcv}3%&!p!1uuKcOT{-z>mQ1cRyzQMvR}( z{0zkN0pRz15c47M3-EjX6|+Qq7#soO_c!48`#a`8z@NbH_b6uZdkh?x-d;SP0DjLW zG5-Zl0l#Mh_7Tr8ry;VF_zm)%!I$m)h?GbL>@q+_P$@>YQ2k-sm_r~F_`?>~l(0oW zsR|_QEL6YWY|J^J8u0tAZc>usITr$>srFnG)$h3$<}ks1b)BuF^k^@W~40D=V0mg+z@jk&=~kVH^D5Po1!)Y5^-}>zuy*^TY^@=@3%E( z@!JNqE!ai;#<^(xp4(wQ53~n<&mAy}=krlJ0*Sa2s^71Jxijbj{C+RMEPlJ9UI@f* zH`LaC&lh2FF}MWyJzt7hJYR--IoM^WZl$5Rns=_pd;|D5xKYy#<0e#pxHn_|5BM+e zhb!H%gnKJ$5s*aPhI%_FKrb!42l`@g2e=dHF7vkD#o-=3RpZ5Woep z4SIpzpbsd-UIHpX-xu@)_kb%we>G~cU0vtnL5vT9hruI2(()*(NY9Uf2p9xXKq?pv z9tYBXoa@hJ zo-gj6!}vUy45k2aHWl>+u+>nnJR7Q}^DztaY%mAR1>$ZVs-$H;SO6A+m%%IGTCfO6 z9E(wxfLFm%Abwv%l|Qe?U4}UdmV*`LX%XXfEZzVs!78vCc-{FL^lQO7upY>t)S1-H z2Fx45o8Ya}Xqzx^25*CRz`MZnw*~!sU@Lgv>}VL<(0D2R0KKI2L+}yU4m^)L(0>d* z0XqSscIofY-LKlM?AD)RmI!x&&%kcrjm)2;{{nmo_JFT|8=rdRxlq-}-?{r9cYjCQ z_Ybrm{(<)6nY2heVf)V{#%VwO1MTO3pdI)J+CfdDHxC zCXH08GIah_Z+CyvBA{4JvdKD*O%$_`&7f|cA+E=7` z%+e~La^AOVMm+Z&aC<)J2s#1z^P+PwcLrU+1)wYNXkLGFA$HQ=bVK#xxCs5l;1X~t zxD0pGUaTsSMmRz_*#zu4R& zP``X&Usr$5tDf^)u`F`otQFh;i1XX9xm}?8eP~xt?SZyfpeB51H|{)#?>eW~6?b=F zd1nm8B|TVW=vzGC_M(2wT6^^D)30y;fvl2=>&2!xzYC{51uEeqdt{Tl(e{er_M+as zi~AFXXjq|KhNmgoQ*T`K5vbK4*-!s#WfaPwfCN{9ZQmI1IiPqTLcFq!KS8*(JPgdX z&kpZK1I(>U)%5N5@}$335Ug(kn7s0JETRc2W0u`f?b>0lh}=U|UPTEL_Q&j1>Hzcu zfvla}2kr+R?E&;cdk{PX9tK`GkDz}PJO&~_{yYX8gn2M{9I&KZVhlm!5k2mF5kh2Rwh{(Tl&Q!82;mC-#-ecfG(Id(ay7@h5hj zDieHeC!(JO<|%8ZU9bI1XuB8nA5hHFgPz*=>RHl*yv*`>nT>vqc%gdNQn>GHew`ao zW5(N;s8u`d9{-vLdA`rf0`xtK2Nw10txG1J`-Rw-FdY_Kud8?&yWTxZw4GN6sv;&uc|AG?m z2p9%R!7LC3Z-KAD5m12y=7T1n>uh!2S9aaTYsrSkJ5p2YfXCRqlMK{E0u9B?*{D*jVN@AT z>&NJgQ6=K$F>@Q#B+MO9#eLV9`bHJYxC66H!le#R#r~EVpf>F`}R%j_crNM zwZFC7s$0IbdlY=?Oa3nOpSj7O@EJb7t|TGt*2#a!zM`+okX|uVRsFxSJGS~94_^1M z5$7@@O( z`SyGJ)4LA5<+k2^Z|{Xy`Sa>-KXmE3Z*lu><0s6L0!j4G;&We27U)#iTr{7 zPjD0*1M=queH`-%a1#6lP63bR^_NC4HhVzz`ouucsA~kVNC1f-$wy2^&uvyC6{JO{ z{$STKt5rZ>K*Ul&8R#p5O27)LL&;3)Z1i4&r6Ghcd)1vOdQb&a1zEszn~gpPR0Gw4 z{COG3#asi_1hqgIcr-6_dDt-+T*Zagvf8bUz5vt#bwNGg<-8C*+uue5a1Lk)Jg#Vj zUi>u%O`_}f+aH;!P0=?KDDy8nPp$dcPRbm#AFh|h6Qenl76NtfR~9Z89kMfW5C)L} zR*IXx;-)3^RssnjU$r`5+o`QFb0W=XWp6jJ1>93S~=@FdVCQfdE{BMB-k(D!`=ng%YL(#qSjgW2< zl!cJpw?_~5wtpi8U2``>{*S;dxT4zz$Y}?tvK(dn7s@RH3E-U6ThSH?T*qv{a2N4+ z9Jdq4w?VmGP!^kd?694Yc|@GF5hpz$7Ylq&)*iOgDlh#5CqHWG^$zHF3Unmu)M1M1 zF3ddzK7Z|x&=Bs%vX{VheE$)oOla=*Zrl`npyR(FdN9l!&S9~8s_ko1+FfU3*zAt(5tN0a-v1r0-7ZdaLIyz|EGTo5)tf-Z{pGoM9O@GSpNl(j zu|-_;mnt6u^+|zFhI;f5YUnAj%MRKQ%h(7= zBL!~82KF2%X<8enkAgN@;L>j^8hHC%JhLG5`jVnC(8dZR&Dm<|pLEHK27958gEC&A zeXD8f>_mE^pMv1oD2@od=TnC+w+L@Vqg{n(^5)>tn;NhP8 znDLYPnNQ$AciyECpB2Qz&rok3h1H+K@_B(y*ra22V&%z0@U>94n#oY62vpulyFhg~ zW>-p`iunbBFR1&E5!5s+rwgRVYmmA9NiXd)uzXSAX8zzY#(;58LDf|`6Us{hUwG#n zC%jo$&K8sra=oosXVg&q>%7mw$y`C1v+4u%_A3k{u5<&Lhl}|ForW4cdfq;8fSwkQ z8io^HMhhS>6u8N(b%HM9Wz4S#$^y^o4OPrMB_e1#%Z2RDwUa;Qq&DHkBlG$-Qt#Gxk6Bu1U2ZSaMwtjG!Wi< z9r7Cj2`@`6I!SxmE7Zn9T?u8CK&Ps)STW$(x@hkhOGH^J){i+P0k*0FP-8xp=!??p}Z;3RJH9dQax%6)cYk}Z$a54 zaH(mhD22_K-xl~X+wGKHvGS;~csr??@*U{!3S569#NQUo?+JYVHl4C_LXRxAjKN?u zCIsLhPTU__6aE;R zPry!2>m7OqPvf7?j~fzZXlR$pK#`^D`=?l10fevhs06o_(& zi;psAkUuFL!I!FgQmA&$9=ygQ7dn3|M(vrRMkR%M6nq2O%gJ8!-vV;7XDW$$TPNx} zos);unB>qS8Q=RuQ&}mY&VjYlqc^35swC9;!57qz==X!4B!hDa>wqO``q?cN^<{;S z)o$vCmN5mq0H%TI;6?Bfm<{HDd0;+R2wnk;!IBSE{fyB0q0`;G_+j+Hj8JQ{$^lhUS~UseYA0=c&GxLY1PgR|-uDR0$&fdj*hyJ`p5Edxt^~2kNByXldxv zg~pl6eXo#Bs_iHu!^y~(We1+A%UIIn08$JH^tR{Z5UT`)O;|{H=R$z~$RwO71l3wFhdv!btR~Hg-=t`1Qwo ztKs;EHPOPX(5X=1;J4BG4MIuT_D5;9KC+Mn69}IkQk^=7nw(q7SHKo}zuCqtc1^j_ z8$!(ll|dCyRn6%fy1q^}TCeVM&{qT1sh>SuoJn89nkyUJZv8|{x`bK=s?_j#sENLo zcu>) z;k=}^M*|Z%#3|Z@PSGx}bB0svt(Ug%;|S*`8zT@Jd_QEo;d~>6#6d^G-@RryhqYl^ zv$?l?V>nOj6iULHx2umaoM*J*&BiNdEH<1OVo2(AU%OuEUl`63ZQ3_Hq4uT`hO=Iq zZd>r?{s$Hs&Ie*j9`sOFwC*Q64d*Lu9Jin(tN8;@aiZrJZC-kPTF0t)%{83CpGF7V z7WyP)o{C<2Z|J>1Y8^_qE~uyS9tho<)EUNE`($+J1EHr=>`QH<6=(z6f_9g3JTi2? zX+9h6`FN-((6tf%++$EwB7Yb)8lz!#vYcHL?DR<~ZCmEHsjBp3D7)g7wh;y`!DUz4 zYSCAA{pf$63{41{&7+5hg_Z{+SKG#=;AT()9s^^*bnq%z3$}u9z(H^fRJz7C3P20s zfNQ`*U^?1Gw`nKR)Ab;LULOaaof%c#SI3IY61s&0M0uJb`YK;jsvAdx42YZ3K zWlX3=m9AKOFWsu zr`{wMW+JTpys8t-e9xQ`Jsiy3Y(}PweMe**(FCoE=hE+LyZUsxTGNPLP!m%!&!-0z zZARCIMyqBm@+H-&?P!l;nGiIr^JtDUBt%Apv7$+o<6E5!=S6LMF|ANEb!yw$t=lwj znp~jTC1uv)=H!h@nYq|WB{d?)#0eeS*yEz1V|zhs=-6Hs zjqcc1Yn^m#TeVI)w$DZ9c5FXut?t-PYOU_rMxPJ|x?`Ix+E~Z7Khj_qx2EbG`l)#iA| z_QT0&q4_uu2Qlc4EHLuKk-CUtQy)rYexm-a@>=a!7 hHO#X{4CWvh!PAW6QNDB&-_tzJqSvDhyJzN_{|6!GlraDR delta 282766 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

OLw!(0^K$OUcQhCMkxo)lA^F6IbJwNm8N4CW;e zpBAPWnEGs{XlGGjt}tQ4WWpbXE6Cz#8+;S6>~EZA4m{@~%$V2+huM!@M;4YEcMsH| z3LT7fd|BB-`9VOVhIsyL_M9^9h{4v=Wb0`<6I8a5k1yK1z;;rGZRJDV>Wh?KmaUUb z>Dwiv%~8tKC~OYIgnT;8GH?lG!p)SU4cYciOjn~94(mIj%~$zF8E&D~beW3PyT3j* z8qE6|i247EfdnXgFi*S;I8l~Y4kxgm8BVloD7pe-j&CT2V>`rMBU-k3vm9ZnQ5YJT z#+aRZBjPy_?5~mOBX7{yYs7n6@tdo6r_qg#L|hdt69{%Rv$jI3Ec6aN(MT-Pw5?RV ziAaov`!SomAZ7=(Z6Xp1phUyxW96$jh=98~ymUD$s9^)Sy283LItZMNXt6lb0w$4| zOni(3?rVy(P`GD87S4&|}vcK$;P#_d;gJ`48H z{sNVcDil29!S#=f7C=gXMA%KSW+4pT6y==H5QW)Q^OJS-Rx=QSh1+RWGm*+x%+17$ z@b|enU+{@`!}7%~G2@gO0X8)C&5lW>KBHr0dYRLUX~1Z-!hqq@Sm9eX&pYo9)o3p2 zaRiw#^#i$qXniSD(6Kv21}$#^>V9|VKnsuw(Azg`C7NmpD&iB6C}1zo=jpYe z@PeH3qA?`O{2#u#L#)!@H)p5zT!}4>IsJPkjQ~ z?^F7@jd&a#xudNZhs(jX;#(LDE^miLy*c!-DHdqTa{#+II~Zj_!W^X%$i7z0fewDl zwIY|1hZU6Bhg71*HoN%gv1~5|uywlW``w2Yg@dCyc^fx;AZ8!mn0Nr|i5Mo!H zpqs7})eP$icqMbwYI^=Uk(;;)5>E9x8-_v7i4ypsRo96$GqNYy02&DQ0N zby24ZH0~()w1G|?1wvef9_t9y9lMNGAFR#YNu8R@>eMwxa-^!hF){^uJjPhb$`+fu zf+Ga@UbT{&EK(HCa=<`^_nYmr$)+<_*>-BFW&V3T10{^}%JqfD#LqG7nqQ=PH6OX#D{qGdWSKde9hUQk0Iyp$GR zy@?0(bvJPY$4@Np4veshe(x@}BR1MU6x9RV-fF7dLp;!86@+`4=3!hczVs|- zR5y!PhcniPkP$TJhBFT6tYvsve~GSL5&LK_mX<>Yit){LU_kNH5zC69fC64hHU;GX zfP$qQP&xvJA^!{I041!9&2+H_fLgDNZEl}sRugUEtEaMn-bt%^hnmSRDi^zdb$BQ#4}|e`-%r#3X-%JH;KT!;3z(B>YS*02i6>a7F^aInB6J z%mv%g`7ROvcQqr<2L)3(^lq`8nf}{*iB|&vJ2=+^0|+_*N=EGG^^VS17ln|ff!VkoMm&tHY}b9FP09-U5ETt;8;7XyMIDc_ zf*Rb9kyt?kSBqr*uyw6^SQQWOF)HQdUcFzW87rX4xAYe%`r-Yem%j81N?#)~sqq8i zoy1|wuxql-1-K}%6yP1dhz>mNe8LU}7;E@7H%`!Ml$Tce}K#(ZN~ z`IoZrL4izVU1P51_EMnl;zO%6a|b6kIylWnG3N9Skxu5M@`eg=Qk7=-Yi{Kvz$GH= zDH9cQ;z!6(Zb`}?>R+5}!fz{os9l_dM;X%ClbSgtgmi$DY)WFT=42ixmDuGNC$Hfo zwwq>7TaKiPKFj;eT*b*WeyV#-X^+}2WMb5)$3x=UyNZTt7zOMsh3~LJ%<+F<6nJ|> z*~3Z?wOey|@2FP)o}+pOR^sU;;_iEZy{?}RwWUqxzS7gMfIfO*C8NkhM8>u%Ur(SiXZfJX@BvN zEgfRcl6w=uSr!fuqrnTs3=~HJ?@tdDj2ZGD73%;^&pisdu{hNEF_3h?yE`5SPACc0 zdqNC#f>Bxc6hsJHLkFJ{sk%H1=@6_kmXuhd?|oKO#~byY6*nYpU5rP0W+6&gSauyU zVA=fYv!a7#mr%ryPSU<-MQ67qhoao)K;59pjo1`VKPPTL7uP>0O8&?1sQLoPrV?uP zg19AT0#m2#(qg+=1zqfQ?F1eOdszjetQ`1Rt$IOpgqT-T@FuA#oFUQbzFNyoo)iYk~n)#I#2i9Q@1rB_VgT#LF5=3jTpB^G= zDAOqLM-~yMM8IJh@6jP5h*CvEM5c>{`1HgOQQZo|jT=1qbBJi~S;Di#SjK!=)J!P4 z%pe(YpRk-<rj<#7Pj^m=wxJ>5bzSIo3Q7XX1Xpy}yxXy|< z%mP(s?*egCEd)crY$5Q+06xzcdyx?@v#9OcjtaU)1YARa_Quh+0tnVNP*kC)f6MrZ za4~k3AAbx%2v#^N|A5kTu0|;%a~;{H3$nCSZkz?qa#v3v1wIP9+Vc^c8y#pb#W2k6=L5zAul;ZN!D?&v> zw-4}*A11y8TG%&Cu=C&e;i8U3BO{IsmAK=Q-H`0}#~$51Tx4mJCyfwU`eYB4Jmaf{ z4%|3G)b*?cT*S!vRdGHW2#0=7839r56k0h#R2XHcjctInDo_xjgzg!sHum9>&;*rG z%~9fRfTW?LKrpVO1EWM;CZ5D-k;D&2L+WyjI*u0Yd0kAJ28-9}qeT^TdFg1yhU~U3 zt}z${MN%nv2wK-V^*7XZj5yAAPPyYyA+8<9i&D0Cs`Z9wbrpLjm$}In`*PH;%K9At zf1(%OC(#TYK1hHo|77pfu~@8E7RaXMLcun!(2JHgL&3KdZ0Yp2Sc5(8o-A5Lvs*6A zNT~Bg~5!e0ih`!)YK6^)WsAlmetHa!fY4azW ztPH9>Mbtpiu2TS|R?|~c#661X1hz|8(nj0_m{?{^%X~LWAaMh97tU)O&dcXK@=aB< zoo`+4wJxH$Opd!DT(CO^g~gYl52gwyAN3cS2Jo_qewZd&HL{{3u%W?R+*;ukmRp%Y zhq=tYV^JgXlBj9Ha_T%?)REyQ{OaoER5%^WXgL*67nz-`M<}fVfWt}8|B8}sLMz`9 zaU7Q5Ia8{Lqt4YK0Y!9qTZq;gfZ>6TVx-Lw34OT$fGPNPlv2DDT>4G!qpFEvrUNt` zgHf(VeFpy^jkh>!wUx?bwi(!HCDd>xxD*)8^_qzt zwv2|%6s@d4pf1xgmfO%+Pqu>ea=0s*<*31a({S(0LZI9=74drgpR%e74Vs5{egd$F zGiTu7!9ofCK{}WUxsQTtW@)$t>%$6Bi7Fnz8cYS59m;=K#OaXDbeSdI1d`u3OFRG~ zzHZ_9|%o6qJ&N)oH?4+0H zh_tw)fY~~v%rLjeN31Oz-!OlU=l~_^sX5}QKt&KCUBwy%CGvC-BWoxC=U+_2-UE=D zMf=|icdS0moGXGg--g&P*D)HF2rT--YvU39LY=?OthTBQ{NCZYBC`tfjRHhnCWZ@~ zBU>}1&4bkIDD{|!%4g7`d04JHX&Wy7UDy~_7)BjL0lX-k^8qY(QoH%0sghQqsho>Wac+0;OId7lL%nCVG@ zg92Zws8SIu4<3W%Nfir5l=D2uTDw~GLLB+DH9+LksoNTnP+i3_jz$#YaKxy}S}_{7 zMl{Yl#Qs-WO>DiInm;u&+K;zjB=pa3{k%rxHRRKh0VD8Avh@m#HEJ=L0CL3AJo^vv z2~$?=C>NZuI<3W!9HocX0>;CJWUUBP=aYurICuC)HY9VagNJ!LB7V;=x~3H%Izns$ zP5r?gL$T{bLJsB_EIjk-*s{y#m#g2|2*+H4w5@&zh?!4a*NNKETP-e__O27vZySG7 zp&e#pfh8=OU3nd0Me>}3j#Eelawnb!-bJhDcVKCR5)qfZk9%wytnM)NwsoXNX06zi zRIX6bR?G)5T;tsbqSpTg1=fo^CJTD37o)Ijepn9{2Wx%)r=W>TLVtZKZgOh7>BcWW z_wNo3`$D+&_$y7B^>%?2ihEYXyQhFfhUO~trRe;Rs5)t*XkW=R`GJk1L)bKV@y0L< zFc4_E{Z}H#y9&WbK%{PfUW;0Gh)GRfhz%j zv_;ftuwk-w3W$A*2t=Z2*g6|cEm@vdwp=vJP|%x6L%PRR2g3uzb%7Ly zO*jy=7uXpy4Xq=4%)@(JV>9VEEDJ~;Gox8sh8ozEPgB7lqXHZumTd@CSSJ7mXn<6T zqxQOa(Wn$h?RBGmr0md$2#df$d#f-#VHt2SL{mX~tDqG6ug(+0V_wl3s~n0nsA&1d zirVYue&cyCM%mpdOv<&|QG3S$SAfPtv|e=r8>07C^#*XrEU@H&5i*K!? ztDwC;-acAH&|Zv29i4;r7693(XBAO>E-@xZ4-{WvA$;)wacH9APdj z9aLF<^P7?Dc)hXLDBMg2e=n;2hr(0GMHXI%!ur-ID*avrv#l0@z5m|Cca`3%`-8YK zZUi`Bkdqb<{2nC5wEqWGhVx`D{~*FA!2p-_2M28eaZG4wj)N$Rcl`uy=>~e{Cs8+u z$cil-0)y!e2U=|5Fak_QDUrR~53`$}fV4NzfuBU};07SzTnETl0Bk1qqs(yaN_-FW zO@&9r^*$aAhkp&D>vhZr75*#|0F{p&73pwWjQNE%ZPefwk*$p6fG_!|2waja=5Gd)EC| zTfFjn*8e}P{geNvwSW3wtsQf5`Gn{z#?OWb4W4PZfS4M1QgjjDa#mw=^cWe$^f^v)?ol&?G`x@4-o3WFNU0vEs?YFfr%SbzYb z$fmyQeKlNLdF9^ltJ>p3e-(9GsrUH!9ot(JOLHEmSg0vGg|mQ4!g+nedB6=|g?|1O z@=$nS|AxNqByEl_%T>%Bd-*pY{$l#*H}P6M%TkyPsO&yzSmH|2th&V*m>&jlz!*L8 zJD3DSlKNeAk$ifMC54B0^cVW^cafhl<*mHvyctzgl4D31MpQv z=Xo4Lvrda0OqsrT2AbWH&`o~|c9sv#yCA-I=8pw=(ZpPhizRNhf=saiA4`D@^Yj=z zQI?Jia0f-px10gV%gW1u9EfgmS=^X73QzLPv$$B56!YE5iLIsW3vWfUn)rdtiBi=~ zER(hHu2+*I-ArhW&}o8Crc#L}8r~u zTpshwB$}ejt_YKTSeH`MCqvDv%TH*fOLn*t5Q`|-Ls@`0$wCN~0WsvUEaoy&{_7&}i9` zqYAE#mW|!J0OTQQJsT}+vXLU<6TdZdaI>|$ET$#H@o_ehQtJlZ3( zwJlWQk$H)S#{!?vgN+QdDmDwJfrb0iBU@9GSQ+5@L3G;nSBgXYSgg#g8ID67#io@k zR6sS**h1IEaj6iF6oUSvv@TY1Wa9m?k|Ptxcx5^`-dbKc2Wxt>SJrA+DLRBg4mO_# z^ouWZ@s`E*Q0yBxHteJ2$(C3k%6Bundk-Ya#1! z?ju5GAa|sYqkzbzlpogHakaID%)hU#9dtrUrrp#1GUtl5HO?omsrTL0szT=9tHL>M z|4>ykbZ?Gd_Nj8wlKL}osDp~~!)L>vEEA&R!Ady&U(hvivR?K~r5uZgKmrlKczD29 z^9=AC_<^O>E;YL}B2Kb5$J{tsL%(>5Hpj{8@nGwDSzsA*K%e4qybLB)0nvJtGONf0 zcwF|-hN`k!>T+u_gAqy1Os-V(qqD+m>M`CH4aX<{}8<&k0IVdo8o1I$h{ZQ1jQxD9FILrhp0n>%*U#DIYHii zGxnnAs(aD4dS;of4r0o~7SC>;Uk`6f46?`mCXW)mu#At_(dV#dSKaBFDDx}2150hY zIA(#~PQ+e%ojytgEXBEkiSiZLNB2pR)jKgp0hfLQ^r1?w*qwcNth}CVf1)HWz3j|r z|3CJgK2MUJ*)0i6oZXUgl4X$<$7)@&tdG$;nJjZFx>pr~J!^&px_3>Atcyj_I|Wet zb$UJpFBH>zDRMGIW9?Jr>o=WLf`_?U&}q#H^Pz0Pfu1kGUAV`~P{7Jv9+A5axeR$w z_%oG_K##I*9TsOH3k0m1c%WL|dMr}{!p2WyNUg^4j-oMXGCOZNA`;nMj#d-EJjF0i zS3Zviw<^n8LP#>aJdh@DOMdIGb+CT29F%wwE#;l#!5X?LUEWvS-dbf?el~OLV|9qV zZpvs&x~#^uGkO(%vy3jL%S?<*wG5dOwCDkAAOZ66MR8ci<-Ou=+?gR;vueFKL)Jtb zrL`F{DIJ=0tapqQLd2=Di$oj0X2{;jwkF+*I~A!)f2bPpP6@qMP1b{v|N1(V#* zR+ra6Y(1~KWEERfKys+WP664xzOuze#1&>3G;Cem_`|l?={R%3d=Dc8-{yU6;Ioky z2V`cO63&XTv#^%~F&uXfKRM;b(3nTyN~H&foAU&86snKCK2Qk}7ogFpzx*4gSz0=|P6z6CH?T-rD& zyYncI3`*O80bYn4k^&btV8BHgFo0yhuqzoiZ!&v9!H0qN2W4}`V3f3$4X8^E`8*$? zr!WEHH=LxeYe=RZ57&?llwh92J1V|2T6waiYaUPy99m-=&|7(cBgM3mFFUC;Pu>FMR^x`U zm<>~anAvO&mq@d@BiG2=;t+MyvOmO1f%OL5h`L-ORrulm@Dhx)8|#2f`^SDfsa2#SLstqWAkoNC%!t}16H%S~87 z4uX0Fpo?H8A^v4%GQEXNL9|n5FOw}>;~vOCgvfy{fIZ=&+(NSU^+F4f&CtHqYAG9p z#oti7(mgF@2MC^Lw*)R(N?)|Z`dLQDT7pbjN;O*n8-GCETcN%WsDCT@b7e@>yR}*Y z!&=K7kq=Ih{a|bx)K)@^+b9&VyN!h+CbgC0kQ;0#AM;gsRVtMpO>Zaj<=8I(OhB&2 z1J7O@Lxx5wAJI$gWp(!+6advRv%Q?xrRZ;U zPx~M1UjDzTyV^~)9e@&$IKP4Zl=08-Lj3&3$oKeV{BTHR{}Hd(F2^UNtHaUF{c~{l z9shjHKdbQLKSDLHllQ}YV$5~enaX{lj(b4}Pd=sU9k3l|QL7GeI+(6K9Z+BkUE5JM zE3jRuz(_#657`GeMOU`Np&YPq1%(eJ%^{I-i3YKcNEH{+Gu>a}&cq%DeF|Wsh*QRt zu(JgxVOH;Np0$|>_*rpGjR^9_4L^!NG;lC#X+h^Y!b#vL$?IinoYagbFsWc22@9ea zQG?8|;#0BalP!QBzh2I>{InT{f%2)*Nw(15r9Pcxr$j(2J?xmry%ukeYD}}P+fGb62S9!hXGAsGm zKjVq6GSjaf<9t_U+?3bG%&wrkw$P=n^5#xk0N7!TF@7NgIQ7ldxL5+5O;*Om@{IqX z1Wmd@E&*`gKTzHn8hw*op=C^j-#o&`1kXJ-#A5f%<2 zxECBd60NsbD}$LSpJi%ui+sX@!~RnG@D|mL(pzL=4O>(Rb)A1G3|5t@nb|4au~ikH z=0YjA%G&^NAHP**RcDo;!kmme<6Jl%hL;e;Fnl^&@iJO^D~4(*{c$UH&oSzCn|zv4 z+ify8hIkXk`hnaS??o;2V5LyWZL+#mAe|n#T~-a_KCq#5ySyR%?o56+=0Cjq+#UG( z`CKXvy@nodJ59XlDm zMo6ZzIdnWHr*cx+X+r_1n;&sfoh?y?lb>+1Di@C9E^WZBrITba{(t+OYHe?e#}YLOZM_NzvpBd?uRnQq_>fb z_fP1j%)@o_eNH##$IP#q`#G7zNtlQ0=D5j7W^odN8QomWN!1U<$FAX|Y8Z`%SX#PlY&49KjYO5=E`TfsWfq1k97cV6$)sjKgC4{}R)7*WmJps7 zEFUop;4_*hrkQ%tJj({E>yQB}&qclD%khAdSlKN#j%bLeANh3pHdcq}wt*}Ao&pwc4Xenu^^xtWL&@dLb%1h%fh$HF9Y!EPwqE3p z!+0U@9+{M}S`~sSTm?nuBFesjD@2h8?vXv>7kv)24I(7SKYZtc&Ev*<BAZ1c%`y9qXW@^ z)(M1N%{Xw615nQY3K%X%O>kUh#O#3DktsYdzG|Rxlcy*_m6uZ4N=iR$!BcxJs+I8U;rSN zO5R9S9+N5htV@*pScCvnSCrLUOH$hRF)*t~X(%qnEF}R2e{Al#L<=92gX=>A3a$JD zz$cl=h3#=rvIG>ylaF9pu<-%%l?YS@G9d!J{c(A1!Iw;Q_*vfRHXW>>i8jCIEFWhf zMgeclX!9^I<{8fNT7pt1Z_#LTD0&2kFqDF25{sLn%}JakI7>4X4HRw8 zX9sfiB4n?W{Df?bFS_vwnVrl+o)}wh5TissZaDqm>n1!QuSKD4Pe4EnVd{bBWVX9_ zv?3+52FYg0>V8&qW>xm)@Ypp#mPk(xlDVF}D2eYmM3-NZ3AAPqgtc2}_n^uW&d9d{ zPeS5xMLRQ|jA$qR%63jZiFVF&JKk_R&7YF>JeyhI?14ah;8U^?80Gj=vU%0tV}e2j z;ZpVoY+SO1nm;Y`QV-jt9mol9X3a`s#9>ck1r^h_r)3=ua*7$imh|u;sMyYIHwiyl zs`89%?_$1t;sJM68t@Fj%|e>`jBMjxjzbYacOQ5LwQQvZ&&qZn&YpM{+juKYdlm~) zRb6$`bFzzN+sM@h*&?nh>Si(PJbUP2D92`h)ijYp7_yN^?<*;2apb1 zp4zxkp4!;?xNXd~s~`fQgkbS0+)8A_-VCViU};sDI;^5FQKjyngn_eoE?i0<0J8Yt8h8+Rv5ak3Y1XupIZe+{O`B=Rv`%3653FRjb1{DLuH$^ zu^NWlcA-YpkFcGw3S|EHP^D}9bEuqbc@GT@`BG?WW0%X#v=UC2S^kQ=3A%Yp1{$D2 zp8$kDF+#Xp!09_fIm2W}R@FZ>9I~>K&=8-w1c5*M$6;cmCj?iV?n)_ z(2Zkd=YMn_TR2wsw46LF=P}F4!)d~)pPk7<6c`6NO$l`zCvV}<#6u^@G@TIJ^;NkJ zP$cdxnWvYyB9q!FDtc9}w;#ftIwGZyc1IS~%PLniz0y-0Nfq&`b~Z9acfTP&3AbMq z`C1YJ<0}(_-Lqux*4WaHn?P}aU}KKlvH>*L%-44Mc`}&PViL1t zYBY!OMhJ3hH%q3w*mjKiBAY|5_j^Z`di5O{a3kKm1E&4U-jR*7*zN<$b;vZ7TNYE> zr6oLDh(>{`E?suQ3E7w;b5a&7m6uC_5is=3HR1>C3*9_LCc3w(x9HI+vKP3*jZ@Iv zQaU+B)&`xFG*z~2txQan`T=jnfuD3T=sV2W6Et(l2apQ94r4%wIKb0$5u6E%d!RI! zr_yUv<@2b?GflQg=2!M2vMH|CD}cg&b0!R2r^)V+>%2crHrD@&p##%o^;SPH;iA+z ztZ7lRq?lj-Un-c+mGN)NxVSnW48^G;j7?GwZ$|I%7(`KpKL|pGx$De-S6}FMaOoBFlGZvVtp`eh zeg=?4CbS@I_>Mc~^eueG#GlTzOf1Y)IT8#%hO?R~&$Nkp{}l6h{2IDVyJyIh8nB%Y zmq#Z1-x!Ed3HAxbo)C|*jW0Ev6IGSE;gshrZUbiy+9&Qp70!eX6X0%cTG{9>q<%AH zo9MISnAmYns_jlF(7YI8cD{kpG0Rz?R%4q&>@jPDVYpxi-kyb_>kycWsp$Y>^XTpL zc24|9630?GQAa82aAi680W#V3GuNRkSFmCntUHb_*L)+gqh*g`7Q;L6+IKB87Z2;Z zquI>mrFUg=4%50g7XRG;1lK23iLfF^qevUlK5`J_;*^H?k@e1S? zm74{dgYcxns2bY=5W*rh*}Ui2zm!f6fQT`6i&;u25BS5Wt!W~e3g3~j9y`v~QSwca z)!b$lZv+B>9ngY-DqxWX7$Ji}E6eA965bl>S2m6>fN45l9S>MH%2HnDAbD(f2qQ~dT z<-iDi-;;IqvM73Dt_+2fXDcPO*D93-GK(w=P#u{(Rr%G?k*`939?>Yhx){Puy8{<0 zy~Y?aveG{*KLzTDdm!y23mIBCEqGA#;+Lx8q=w z>?q=fB5uCk4I5WQG*}kO`V@M;yg4{Z3^se~_(U;vzT1YG$f#|e!o|wC;$X6SKUWiV z^m8>(H~d=}IXv@kIo^s$Tj54)^;L;1&2`-Cz!%sHvXmPPnCq;=%;<&Y98+-bm-0GX=6tDyq?f;x&0}|fMT1WxAA8e&qii1z3ad^F zdu^laj6wK*quTlBH$vb8yY#Oh94MxTz5+6yNTILfFOb>)Ym?liy%l1_{zdx_qRHc1rL1Y0d9h0fCV#Yw zMxD$aJ??tWKxP{9zK#_dZQCZRQJZZtrBVe?ZBvGvuWplx^@?KIW%YUN3n!a+RZ{A( zVm87&hqi2!%_~)u^^I&?z%xmhfP zo=%}U+vVVJ`T5(SsDM?(c9~@fF`_|=oZl`V)qjtne&0f;v4zHcD;pM!Ve=C8AqQ*U(Z(I}8fRIwmr4)H1V1p7+k}e?e&Id>v1rzJ zayFZf#*`y&Zw2CBK|g*cYq9yLLh^8Sg`Jv$0<5<{dl7Of%H0VPbqn>_dF6P-Tsa;F zk4Mav<6-Ak9uLDB4-~(0JOmGQ4A74SmpDKj5S%q0N!EB^=vi27j|nWeC}o$tAKP)r zE?6^_(UM)@Mvl_XUGio3XiGMfc?gSo(D$8;WwvO(W8A(h~I2EtUgSP=6 zW;XPX@KW9Z8Kic50EvF3xA(}wY`Oy@iT?h5l(ARVj%P82B99|Stmj^=qB8DB0w0(Q z5_lS&+A9;UUOIK3OsmK3EB7mlN-gg|m{0{dq#V51UbZh@fHC1q`{WqlFZX`g*Rm<> zzaOCQD9zjt#WH+p_XBEM162hB1vLu1ewwKSIS>OralT z^`P=shRQEJ7y}%In`9Mi-RZ9w+_OJ|I4GvTL3}C>?>s0|UA$x{=X+V*j|E{Z4zpqf zCeXBlm4I0uOx^uuGPxd%VE+SzWJ3tcR1gM?3?T#bR9R(sTi@-4DY2~s=f$L%=Xil^ zv1nKgH~tIQ{Y%)FFCGT7Z8tb0;^FSY@_aad_YsUT;=LS^HN#oK zqw?PH{g|VWyA;#BqcS&~wdbg;9lnqGMcx#?@BNER%2Bp%%&B5PVDc8t;iFt;bPERv zI(0U-38z3a+@~wv_FeHdr5=-0VLGw=n9Q(`0<$Z@CLD${M3h-kMqt-(3?S?r}w=VXQoDFRxpXt&H0FeHrvIs$)?!{Inqgu%y5po@90x_7WJyU-6tf#3xmtLsibp zAaoe0lVvRvnU6)N4-5*P?xh>f1Mb27?L4$Jr8MfiA_wQ5hr-8RYTuogr`#K?y|?KC z_KAHTeX-)ceZ>8chHF3G0hmr5fFru~Ht zXjhhYS#7{(m$3outotrw1KRgrURD!vtObufUmofB0!%K-s3>TcFk&E z=IK5*SHDO1-GUYPf$qB(mjtJ;5K}YB>8t(-Ph%AP;YA$YQ%(xerjt9C4`GE`Y|)tz zEtxo<7Z;A8>IyKnz>zIjY*+%g$1!>89$PpLQK8iqj)Pvq+%N(=$c6+NE?=@YGNT>(idfe^1K5ag4 zU!??vOp~cLPx-EA2*GE<9(DVgT~Q976agJtMf;91bf^>S>xBKbE8uHQi(`GYqJhbw z*fFS@;mfE16q?sJ9^?C}*B1s9_FPziVyo9Tz+-po5M68d`k?T;hVO+6PytXAPtAm{ z2cC=-zPr)(Pr^4eT%Zg`SoqScvqDU}rZOs)J_Z|~N#F7aurb2tV{|bamlz9dU?ypO zEnh-D2W9mLmefT~tAVM68Izhyo?@orc#1)-dSJQ_YL!y`zG~smRj{LZBQ623fm5B8 z^2h=k;a4M7N?ukesrqSw-xmo_Hv4@X0qvCEmxw`*j`L-Q_f(TO-zoQ83slsr;%gGV ze<GDNcCtS}2lnLK&_=oaKF3Gy#GHv54%D))z zJ0H$}F##eY0B!~xc6Zk&`0fqgXD6zuP=LdJJU9`M!@i%J=)0+NxTiR$#_lOo-`MWh ziI&-VBr3lu9I>lwo8-GG=l`TST9M>?zZ|qE2=hb`L9!XJGiYJ`D-g>SpykbE1zJ8y z_C1Hi+9JiL=mKDxNci(eim#@3Lj*xkoZ`#GNUcoq-H5k5sR|N}Nmb)BFV$C07i>)R zrRFH$gjEBPb9g2CAgfb+6HGJ^A|j$@e;UJ+iVVP$idnY+mr{*}D^u!ESO8*K_fAvr zWN?~-CsWcGp3Jh~Nif6rTzJoJrkNQEZmi7kbt%sb8P$BRFpzjH5C#%VLbS^C)$q&; zlMoMQD#&m;^XibHX%LWME*I88Z_Eq&Ud7<#*SLD&7i;()MRSL1_-cncafp(#d~DhJ za+ZP_KVEBeE3xQxcvRDfi8O$(4H|B6!fuZX7!wR~PQ z`gSc}IT5n976xN4?X2a?P{e~ed|g!v$jC6m&%lIaZNLnBAj+t5ZC`WL^H^=4MR7pg ztKbGJodrV(=!@~v3_OpZIM&yWgb){M147t~y-qeDM6m@Ss@K8pwD0eVxStqtzcu3C zTeqTo$B6r(5%;Sj?*FXoYZ|VfS^N(VHr2L0r1&M6u=)|mK_Cq zD8gDUA1DP!B5|z|)B_Y;Og%(fn~*66cIn}Fu@Ged7hZ^xDTdJ%6hm~T2Ob>01aCy% zq=r7GEi)Rb{o1mj+ONGD`UYz5Z>|0M^)|`&*o5ZBj0trgZDP^wZMu_ZtClc%L`4hgUgwKSxtQ}W9(`)E8mp%*jc#!!DhY( z!xbK)Z<;CG-Y_3zS22yq_dODRRz`p1`C1}K%Q%?3c;Zm|KE&`NkI z*K6r(CHIU1c*`+I!=4AH)KG3K-=9fJKf^KBZ2b(p>981XoIc%s zx~r?JtE#K2Ph*=AKBsP9?eBg-+JD>T&Nt&=@)k_S=7-ZdI0`jT3+{xn1TW-bza_W(?!lqMZOP49Up9Eq zS?7LBZsm(*g9in{0<7NPp*O32hF5nF`gTzlfC^veLuM>nRPi{un5474WaNRgX@U>^ z#5%`N`QVQRe;xgO%^t*AZIQ}g(~O`ocDcrVc8_4vC!hUcc~?-H)v9f)ezuLZm0gt- zk-)y}+jEzzGE8C~&tmxLcjT*gt)AE;*gp-l#_tta0NDlXJmUEHiM@h_Y+?L!uRsB0 z7w#Qw)g-#cUgk{Dc z9aAvOAd(H1hxZ9S(&z2Z35h%uWAPa zx0?C_1`W+jU${p(^oSKzo*}LMYEximl}~XktF-Cv?gN5ux5q)jSTw}P92D%aod$f9 zjqN2bB(o?UxT1`>HFIZ_d-0&4Jw5NTPeAIhrT&DmP-oa>>7Abl8f3ur#iN5QaiN}j zbkIBYt;Q(D3`6B((@2VPGkgdB+Tfh{N#HYb!A1AlmYHlJ=uAI08@Rw6b#Ty=x#1%B zUk3-H$J$=+a!SQ3G3n@tFI05)OuU?j&hT-Ls|dEuEWOD6@!(*a&W&$suUPJBjqx(7 zhE_fAw)|8uCq3IO`c$xnCZ7N4U=dHdeJ1z=8yST|f^(#jU3dt~_p5I7p~1FX_B}M% zD%1%8g^^K*pQnWCI`h!r!_CVtrdBnSiXS-C#E2IT4aRM2o&cKK?C)Y)Iiq%h%Y&{l zzDF%e%1--iFsbwy32{0iJYHKd7yebAlj>uA{A>Rr73F4HGA~9Y1j8R`FpV$O`7_Md0?rzW-YZcU6lPidYe`0XtegE!J+=l@=7xP|6)zv{A|#}N@Xt(lCch; z1Zlmkgtzg~;8L{Lk1tr4LK_jks+Mm}d!KF{z_lXu7?GMqpU|}rn9keY8II7zUrcT2i6jc z&Vy^s&z==JbFp6@aKmGeWiha3}NBAa*gbia+(08-skfhl-crS z;#9#5PS(m+DKe70dDj*V3qFq05Zo8|et-G!;8Ugf7^unht_8=xD=<>y6i&InFrr#9 z@lA;rD2ot~Ec3Wm7P-V!Wm%#um}4o+bx9VHAhMLt@_blXq%I)Kojwa0V!TwKf)BW( zb-|nXh@d;`@$60iw)%LshsBP7sHm~i@_pWW$QNhPpcJDl@noaCH8nxNKLkQh3MKc$>myaI4 zi}U}wd85|7@}6Cs^ZDQ;xeq}NJS${gM0G4k&Bp|X!*|_sY;e5$%CS)IB~|w~>i%5C zXZY}O!MF(zWyJi$?$zwrnha!V05Q%Re7xedON~LWJ5M@YWK5g6TRo2t) z!Q+C%N4?|EEmO@of+%TA9|TzJcKbr`%k<57+yuL)A9DMBG5AOauJ9SjdQ2EI1xtr_ zxGTRH96Ifyv?1=4)F@noMjeR4UxbOgGnhBLRHa~}=4P%)i$QI+2e2*O=J;ULcru7# zwL@)`F74M@r1ifWwQKhD0co^`F?i<~E)wxRHf3 ze5Selcqit!;WIA$Qt%Zbzt8>>2=~1E;g=8<7rTdf8V-xE+mH-#{AvZ;V8Njk)bCl^ ztT@w zdUJkc{189V(7Z66t{0!qDe7kGs2Q0KSG$L1GC25oo)GM?#lz2d27%sNBgZ-S6dGRU-HK49j7B=NS;ds;`kK{V!J zOC#Y1g6n%EF(+03evB5Jit&d#=|6+%@WzY&6FPXiTm7HG9r!C>a$2zIQ5qOgLUbNI z4Hk`Hil+t7aCzpdHImTjOPVIQ4}LAU4vX2}e$B+8v%bz09CSbWda&cxSJf9lTK-%7 z;v}3Ni7=P^w+P1NL>GJ`_&8lW;v2zs!y)a$zidvH=e-}&KL3qitIUlVxBnM|-G-rW zH@aiL8GIsrw)^QfgMKd0e3LnIy?g7MAj+lggWn2ta7f30F}Kz(J>b8BvxYe!?GN7$ z7KIyD-Xkjp*58M@3vYFEC%2DHKk4o+f!}N0U*`m~a#v-E*M9f_BmIy3(r*t0XADyr zr>$LEzeZJ@p(;kYU!4&&Ha^3apHV+I>&)O}kmeI-2J=8AE(MpJjoNpCJ92i=m-#L6 zCT9n|-p2!-#%g!t?BFM$@sVc*=hKtFo)uhzbNU(I3H~en)@5hgn)%7IgWZLD?rfWb zx1EiK`5d?99JULGsdcPsMfSQljqC-*;+$Jgu?!Kbd;0lvg8hegT)pR-nPj{}GJ82=&$-Z8i|aAU z9er-_Y4zr|a{>H~Ztb~&PKalcoibc+at5QP&I`6^Tz@&tn~pbi+4F{j>4 z`N8%bI6K7IP{FsUT;lc0TV2Nm!AI&471OxDe6YUL&AcG!q;~@sFkKhByDxz4S?u1q zAlQ}5&U1quxy<6y;MEEBE}k27xaD(EmM(Tr`S%_3f*p^WHxF;+_ISZuE^E;pVBT0S z2lsf+Ksxbt=eF$j9?xkiCngFAW5M0F3dyYT%ZY240)-4cw&N-zVAKcww+7A+%n)Fc=eW z@_ryGc8zK)R%$yvNq#}eVI}zt)%LNAz>XiepIsE}Q#$?nd*s-tQ*Pt=x<_-ZxZCl0 zJji4mz8jx5&mY7~lta8ZSaMx;hGQ*5OMIKJ!ZYtaL%dRHNa@&}#>;&gryx=Ze36@d zF?f5fyZK_s=eh3ji`kS}=hBx1XPTPsd+AhWI&jOx^|JC$$7KUfcg-cisE=3MZ&E*c zAmiK8H71L^$W&fbtQHvnuT#v#*Gc&y4zd1HW0AMHG}vb9FuIEh|5WCAhuEkZ6~>r8 z&yEGVEdgBIQvz{ND4#jMb_o<>iGvc*MRtjTdAomnu z;0f^o{)ob|de77X)hFz&-vA~G!X}U#-`qTy&SbuAL=3hohg$dBySo!FL;Zh=d-O6> z-j8>|-WWWI)An2A3V<-^zUcyaT2(m*ei@u-@$z~a_$Iy$U&y36kaWDUbt#FH;r(C} zC%&-qamg1jbV99$>W*y`gy=QP==?jlIq5KWn=5+l)pqGEC;C3 zW}yfM(1}xycmIn}1b1B#9Mu1cT-g!yVLxsU=UTu$dmvONicJ(}p25z#$yc(JJ~2NS z!Gi4$yOP~cTinlZr(YS2PCw%&T^W4fJziaLWw6&rHlD5^QT^J@p-*M~2}zM9mDSTe zTX%f1pi?KRaNR}3q`1&US3v|ej{}V76{w)7?cctN73KXh{Nbu#{4Q^1%KHp*d;%4a zRqI+!-*aUIgk4uowpB=dTM zCh~9HAFmE32rMlNh9LHV1;OZ1#>PnaD-!|(=WpH73xfS6LLuymX_eG``+{IvJq?3M zy6v{kkIQwR0FEfsbyhQQ_iKVFtnMdY6O2jFZbG+|YRxAPA*>vtP)!N*IxC2}>Y5=$ z?WLme-w*nBxI60|NTlDjW+=cBaM~0wWBDAZt$q zvqQq5G7OvDYO@&*hkETtjFoM65Ev2$WVM|J1}lOKQl^xd{@J)u-519Nw#2_6ta;oh z*Ej-_LpN=g23iplokJ@RI4Zu7r%Hx2$`v1hh5D(Y-%b^d*IvSrRzaajJ0SkGd-6tx zWU=eM3CaPa_q!=LrnE->*^-5F7(#?kRX;^(bkyNn4b5mU?Q1mX`DXv5-B5ed>|-m9 zFATkm$}f-BzMid4#<=8)Z0h!&SYWC}I_MhuT@rovGlk(4twzaYYaY&%+G7OtS+5X6 z73F7ua;4*5<|G(oZQHK=L3@g78pB3nRns``(Lc?kX)KQJh?^0ym$)l#4z~YvxpQQx zDuSzZ)e|Otcp;T@0)(n}o#iJ3%5oMry>xvGgD-xh9Nf=Z*k9A$-kM@7lVVBN2;XCs zJh(78aN>mzGTZ7BkC{|_S?%K$$)o$oEkQB&eHJly(sj8V-Q+uR4E7BHJ07=hOv-rVBuTofD%F4*NpT?%gM;$Yi}1GbcQvqZd4HNSG_Ee>|&^SG*ny5&!Hr%Q-Z#k~%Ve>xZP? zgoH|}d*;U|_pbhN@T0c38nqH?Mi~)3?ABn+2sYsuUuFk0n-|*8oO5fS<9m@rJKfq_ zg9i7hCBcXK9#y!Z|0&$vkCz17ZZTjo6-Eg*_ry&t7Qc`YU-rEF=aS%~d-$-C-C{00 zv*9B|hVeu4%am;fi<ohU zX&`s9fuEwHO`e>4>ZfQ`AA0&8=#Bz+p#jGAqVl-O2E{tPNOnS2agR6O^rcAVNo~)$ z>y`!=P5o|i!cJ47@y$pgN;QWzEcEoJ1iB1gY92~(mf*g;EcozjWm7=KGaSu$Kz2`BNS&7w7TGi2BZ>3!`2dGG~CkLf^7;gUIYU0vQ1IWgfJs$G4q_QNGklu5cX`H z;OZZqgvJqPdVs}wZ3S0P|0Ag*<7a)U+xD(tVrD~~`^;Uz_U`)IgM;$lU&46EUEe#iat4w$4t_m4Y*r@1_NXN^9w;b+0mTD1nr?V$Sv zh4*^wXHqL(_98sYpu6ekFb@Ph`gt&Qf?uC$uV)@LAA;uaAr|6t1Zk-yrrd>k?{fF) zyMp7}Y7cU958oB^=Bv^FKia!b)c#A9^$TR0&tJRT_{N8p2L}!TuXr~L*ZuCuyR9|f zxjXoeIvo_d)ID`K+L=Mua}P9akvr_3V9W%`JRY5o0Lv<7CJE;NUYm_aJfor0{pgIF(uJe_94#Iox z-r(!yBeFz6%aVr|ljGCRPUZ|N|Ap?%74+&Qw|qr#ZsAT1Uebz%8oP2UZm<`$;)MHx z@p8)5&K2vjU_Jjn+r*}=Hhm|1s=D!F2+Z6|_*tvF@9J))?q=(5fS>T`=H?{xBCTqV z>Suv|9@Gz~jDu?WSTGI5x=VY`AUcMYtUwM&jC8e z+>hB68#(u*cqIzU1Ht$mUr^QrsGb-mLtCMy(jyw|nt@#$p?p~FmOT(`GwLFS7c#=6 zGZrn$wV%7{cY$23KlsbwlZt=dya&w}BBYA^RCu2nsyg`5V8Z0v?t{(G$-gNpb65+&*bwg4Ty9#fr;yy! zs{ej8n4129d+Jg2`^(&$kJ?wGm8iVWa=WiY5&a}i;;cwdEq!!la8-JHD!14-ndzD7 zFy`WwS>zFyqxw^b7Gk|0>R`RKYQe9A$s?;Nxf51s$pK;JK&t_0aJFE!4F%9QYH?d z2t6Z1P7%jwcDL z8?Vi4d2AFiwy=kB^r~R&&Ub3s2Ju6>P?u1CzV08@j~YP2Lhi%AXeBgkwhDTa3btCQ z`4+^_=#o^hNcYd_C#hgL_e`S9YVJ@f?1_2pJ1^SgRy zS?}^SIG=jIPt81;yuHFFuSt@hYp_C@73Jq7+ppclzcY5{H@{+OJh zLcD?t!;5Qk(7Oq;Hyc_#uqODxNRKq$*Ln!8+MY7B+VQENznJ8};yFPRn=kd%lc4kz ztJoRt`ltB%S+4tOn~xuRnn|$Co$$1#p1b1dVDhGiRz3c7u=S>^X4^Bt(Occ{97C0l zpZEh8uStyOC~1*?5;{1@Jwy1tXM*wD+^~-HrufIYNUuIeH@E30A%}z9R}jYS|12%L z#c^O-ha0yRQ@264)GHlXgexpO?^In zh%6@ROsK2WnqGM`4TaEy&jx=RnMmYnRBG?C4DKHLL$D)UN!1eTI-bMU`wVx=bHT9^ z=T|=$6voq#WL5W@+>*34-{B|3S)ob!;nLo9=&$xyR9Dz}UWrJf2hPiwk=U7aXc1Tw z<%45I#M-owvi!YExcT2~-*Tj9`15`%+~Ry|MStv%!8RXy+ZxUau(Dn~;O5c#=6y1m zQu-uxyy+<^H98~=mJV))+vMP8$)&iBPy17F@RlnjsLX$y;du$Y@P_~-qI-?GLY$u& zes}4ue+uekD|_4XH5}dZ&jE~muIKO1ki^@~AYm!FZ- zYKQASxAM!Os&@TA_79`$#(HzPB1Ie%E@g)aohLJnSk zAs9ba?Lq&1m}TPTkjk>UR{q0N|6(BrkNr#VXPWo%4K*G5)`sAh35!Yqk*D{ z928VFXNc%gdKm|xfuA#bTjTR}NoN1XpTROw_u+pFx+{TRa%{QGb7%Z5_((h~3hz*h zing42+q(09K)=OG0gLMv(d-vp=9OTF`Z>38f3Mr+mEfmX%)IhSa7XJk+L*-X3I>~S zu6Mi|9GrgGg|7ua!%Fzs*Mold`fF%kU&y)k*Md8J+P5?Bjg=3xuHCKXr0pR)<{R23q`6J{rYFK_Q~#bPgh>Im^bBe0O2lu5}Uz#!+^fu*e`EBS4 zpZe6uW=Nqy^BZq-!l~*mRczt07#86@jck@U6gpGNs&4POB62o4g-X-nG&26gCMifK zot|ML6r?Wx$SjUnBJ7G@Wa1+a;A6SC{?w@H6w8?@=gg232B`kjQ!M|Y zW@oezrH+IVr_$_=ax|Kf%2o>bOt~4&0H5HeI8;JZF#*+y)zp+KwAyq0Cx8`?SD@5Z z@2}j7!W)Bm4!_P6Xm~8;@W4QB-=2s)9E{`-Kp`8SGmlaeN=Y%fP&Y)xCYC+EZKMzx zkYk|~Ah25pQq@PJwdptou-<^--e^%34rFFP`Y>*9jKRY@lj{O!yN##pSZZ@3Qm^wmgNBjTWq^QGoy} zDQhhc?J1{3jgzQoYW5JaCoACB)P)u4YP?v_SM&*%krI1acuu190!9y5JP<|c;pA`x z12BYRwz#yg%UaliIiBNBApsRukkE~WbyiQYKyUmv_KMQQAW9WG_B#b-aeB{)p93r= zLOKd(fLrlhrTw*GuGmo}d>uQ&@8-3Y01oOvtzs&vhcV{4x6q|a1OjvEE9l5?fKBY+ zO6WqJibqmZl_Wi0J6>VMkuwUNQM=$i60*R2sMCyVis}lvEV5vUuceuR`a0j&Oo`iw zEtpC}&iI~Y3Kv7TdJ&HEbwCLa0ev&4A`^(kU}M*2VW=763(3~nl4r`6EYv~el|q_^ zX-%4R)Lx2L+$@5MsW~ksQ242AB6JqVM!|v95U4?&9RCgs+nGIaRKQS*>CjY2)B3bw zIDAhuQaUbHB5f!|Gs%!vFVj#5E}|sGG4a+4qaTfd;I*nS5g0R39{MwrmL;MzwJ5{JTf63czv_R~Ku_&&Lpr)7Gl zO64W9kb+iZwq1OXRdVzhiy~lXACz3A?cfXJBxQxoZ9rn!W`(ztVFFrcN}FMV2oU8( z8n|>9GGGliQBg+}O1)gjk;1-@!v8CD5;Y>Mo>~NcocSL2&Wd{_4$I&za~fz9H_Vz@ zDCM&tpDn*dC}h;MUk?&{wU{xZYf0F_GR21OG4N@SGb8RY%I*mwu-Pfd znS$jA-T67C5z>CcFtfa_kg{3IbWZVJr?!j*2+Sr!a|3@d8&A!pQo z+mcBNoyCl1UPki^nWAk>zN`9QCY!6PZ)ogl>gsCdD3aDaV=|o@kDj$BgP7azlv7%} zSi#JVe)!j;%C9-sgf2lo_G~4nBAsJhjr)ir%I{2o7H6~?vIfqal45e~O&A){ix#~r z!s$fz*lmySKixcPR_CoOzB=L_IBiNrxRCJv84#@{o8B{2c-A}092Y8{<5xx zGg7Qs8QQ|USHozJQ4R^H94l6(a~$YWuO*CcN7gs^s&_2TGct#n~WTupdKyXuX8|vCj5^B zR~j0o_ZT@M6IVLN`7`yg63U$;ZpHg_PDYc%_056_`@WTIdZ6*TDr6{@P1?y~Hj7Q4+OA;I?h?MJXu^j-ggDw&owTHIE5}JoYa9KMe)QqzjqegV zZIYi-zRF^cjY58AN%i;G`kxeMY-XxjK1(}I!6g335Nn+Gl5U5zc-NF}7K2k->&oOE zj-Kc~EYyB601_T^bQxk6UlZICJSx!(cFredj-3krW)w0Cya6A8EGDwF&Jdabl0X+G ze8y({XLXh38V~U`vT9UnT)SC~rBKt*T1hj8ZKW0k7AQ4x|EWNm;Zc=bc1}nBi31`k zY!Pl5ln3hs9`7KOpj%Yl$~+Yp8)w8lmnVsMg>Ia5N$Ax=!PQf1O<-lK zEUJZ41Hui+h?6*SU}k3Aaw1HX1ZR_mim9186ede4tXfd#Z!oDkUjXmfZHDi`UX*w2 zr$C8}Wuzz20}C3Sk-%tM3<^w{Kn>G^Ntv7v8QwCZM8Aa-G_1nIvi*x?C1NYUM&v3% z3PmlLL^6CQsE<0VW7Efw9%+@4z|0u-BCP_SZm4G&Vg@g35voYKAwjPi(pbBSBTJa= z7G>}(b0R+~S{;mtj`K7MthH~-R0w7L?r&dn_rS&$*%I5rMFO?^ZJ@kr8(<_cD(r+FR9)srtVx^DfFHuPP;DyOBHTqq`vE!w6!`(N z?)z4j>W1WW>YJ3AELBo-JXsYIyI1NdaIu+jBVm`J=UzSUkUaZNC^m-a0QTxpKJKu z4^9iGRnP5qEr9K|0CtH9sGi&cVmA#mFb)AwD@`B(>J7>fW+;k{z9D!u#)BEDOb`o_ zxz$Jna;3l$1GtjcqRg?R1eRIpRR%1@1Je@TKtI!%K+_U7TEb)luM$wRWcsFN*Q2r_ zBkB}y$FjB_YZ*}5^f-PSp)uyrOksax|2~-q(jYgDemHGvA+MM*<3VP0Lr+HDaqTMU@If3;Hn4a%2AUS7<}l&hGXQ5DH} zn9N&S#XzRqgxL_t#2}wufzjcE^(^}y=pZrG_QO*Fj_srxLmG$llrsXH1OxZZeVbJH zxH_pFa|Kbc78c|@#MS6QF%7qvnJ#TnI?5fWH3Pz&uz~Q4@eie~8z->QR4rtBEd`+Q zpDkcz8m(xSI9N&|BsOnbz`a-%$pUDMrKhCY%z4#j$xqv%ypY_ZB$TmyFrB3kL&FIS zr=(1g$yKUPQ)f2Nfn00~P3DLxGzDxFn$VB*kUo<#rq66psZ&%L{Gh_C(b^IRMhz@0 zok9~uU2_Na6eyguJSAJ(N7U(J(rJ|fd}4l~K$^9HyOS!p5S>&Jz@V=}l8h(tOah~N ziAuoJJ`Ja)2S9M6dRp5K)b9GmBfJ3+pb^f>1jL ztpHJ^8QKuW+s*i7ak8n)%1KeH-SxvQqv$<^JWwQ0A3~c{(1(z)4TTO}4lV@1D9c}p zzYJYvBc$1VbGLAO$)e7&=>W%VM%M^1NHjcm6zTwYKp2it3L+?^1&1qSXf&)08cr@9 zcrY+A{2VBKB_1H`c*AK?of4YjN`eArWh_^|HZfbW)b`rcoF%r^Ce~SES8ZawC3e;( zHdtbJZDQUMdukIKXOz^^akZ&UmbF-$*lda8YZF^6v9C6<)eS;c4I8U4?qk?x*zy!{1=+aGznoa}a$@qb)xVrj^KwG+ zl1(>N+gJ0lFL~L_OFRgo@ilM9CvTg4(PGWZV)C-lzZ_Tda$NEfH#~i`r{-l(^0L9d z>`q>md{4WRxAp#QXHC)0!>KbA@q9QFtWY6lp^+Q|aTy>)Kr(&CNyaA_;!CgEh=roh7gp z1Tk6PP*30)GYfhm7~AHaEr@KHXpb*{iW-+HC5vL-mO~Y>#ZXINp$UZw63(eUr*zmV zTiN3)uZdRl6pN^W3>Y;@F_Mk1MTSB(Kucv7otX~f+)mIWuPN%Zt;VQt#-1g#N+>XL zQJ1D_Jbj;54q?_)f!=6T;xh>;1r*Rf$yy8!$@*I7`&!3BZvA7|mG-2Hb<2z0@pMk6 zqow0!sf*p^EZOQTca=V7dD??YCTd5ik#g5$n>*P~V<}&e+N+g?()42VfbpIs9v6BG zm&5h?^Z{U0U3ei8+O+hqNExYbqr+8CWZZHlt?D=+hEs{UPmNDMJ(|^{lJV22nQhs0 zDh-p6W{a*)41S8T?$>*SJ?Vw+#XZ8&D7?qbNZ zQqKM8@}}-8mdJbZvCA@$bPg9$(y*$c7E<^8A%4s%!St{>*iEp%-DJ zM$b#Bt&(1_!zY)r!>3obv8pMXC(}KtNa>*FZ{6<5 zsbW;6<#vAV1vk1%x)IXv&rc6MA=h#FCx(_5nqeNI`?^|qATtCbQ;sNnRV`30Y4>AU5E+Pht z3Cy5mD3usWZEr}W=w*H0g|NO*V)2Y(q4uK@H_BE{=k_x~`|6h;2MYY->zm%B2Y$Fhtt@WZ!5#0Ekg0(0pSaYrVSy0 zb+^0HTFajF*7AdNUy3??x$bgzAw2d{+Y`Jak1a{UJD;;^*g+AxpCB1Sz@0G`;v7)$ zga!;zsC|ohcl6RsZz&n%I-$PrHJY`mUbnE#17Mf{fF0b=fcthJn8aDbeJ$2D($s5R zr-f=5>`J-o!raY9T0&vAq}9j@$oIrXIs<%$Fy7utrD`LscmA{f3)vcsXGSC)rVp?~ z<}Exki0VbLvUa`5;)Xp^P;H?2et$}4X`g*w}8Lk zP@w@D9>HN~=5GPb(0L)6`sAd5;9#gjm)%j>w$4x3To*K}d)~B3&!HJM)U+BQn4W7> z29{EQ*kt6JY~)W&dR#N|)iiIY$3r7;RIf7fO@8Fzt|}vMRIj$jHu7z5;Q6LeC8KOZ z{i_*g-f5hN?d^MybGf&Sih5gqgn<&@XH-yxJ;u(a6qP(_k2Av}IHS`bqXmOLCivP+ zQ>9|9jWsuB)HtlDM_fCbL{amkOkSGUFnPw~zH~~vsLlpQ;u#Tt{fWIgi zgZR}S-Ir{Wq0D3{>x|b`QnpZ4WBEacp{lls#!79#9nQ*X;N;{*7=@Fv{ibY)wzjlB z=?(TbO(NTIrb$GnA)_!#x5TAr26aScC1wfer?;rQWeX>i{-T_lRw&GdFWN8d2nXBG zK)K?LgrcoH3{cOcq5|kUDJqmVWa|IzeKts&MCaG1<5WR^IkX?!S=eZh?esGS@;jyS@#*Zhp!SiD2Te2k ztW0JdxR9IfbtNrmg<7P8&jPpbx~8av(y_OYf`oCyS?A1O*k;>oZqes@Blb$AbZ!9- z5&G*LiK6=u=bU;48y6|qH0xV*Q0G%~@0Q{UKiISmLvGiSX$Tv$1 zZ7p@cC0dh;T{Jjc-WYXgUq(cgn@$sNxR>5Hg+_{!B=;6tv%FXBZGfW5gQAWv)W!WX zqE7V;|J=#eF>d;cBYnpLG3BNsml-x`HW32O+* zJW7*65U}zsOc|=dQ+#(#DNZsDN!f@RS_Vnk-4~EpKNYpC{2)X0vAxm}u!WsvUWAV~ zz#8$bkUan{okiO@?e=>e8T!h$x6vj6Uws$U1r1XcC$m_4!Zw3g66mvSL}Ut5@B@NW zv4LU)mN1od1C^DeG4~p6|H^B#^-6EN#ae?W5PMt%KE6o4ZBJEPuFYdLAOBjtc*^bd z>2OO9COGlaVRy;gQ}gjnazh=;Z*Hjh_y=-uBXx4ciy=j;wZo33MLJ$X{7eq5S2Qw( z2$OPes39Bvz{{5SZ7!;!QRTYB^~qhRyZ&?<`&h!D=+xfU&xB+8V{Xl zoZ^@2xYd#yU#0Tb<&xKRl zu`Os)&pR-$9RX^uaj$(Y++U7WA3ZEQ+KzMJAPRT$VPQ0pjT|Tl^Q+IF=fGV*i!-<5?8aOMW93XCmWJ^?vF=>_u-Ot^U?UeLfsZ;H=#~AKtGxWtfKNg+!*EfXi)Q1Q)yLF>FVj|soka;z)DYjKF<`FGr724`%dVKt$>v}57d6fS15n|oZ?lUa6$TXG=G``U5gq|AADy66kxhj%nbqH?%9VB=i?t8YT{^e=zUA`H2? zUkH0MH{9uN`2w-f7Q0`4A^b>Y;Etui7sHd~OQ-l7jSed_AAWA(@!@!0yT^_XM>oEV zeuvcqr`lJJ4{zUj)$RABNLY*hjgv=s2yaFHRU|7$L&2X}d$TPmhyaA!T&w(z#&G?Y z!ktH~m1&mr;uS!o)A7g zam8~69{N{oi1i@waQ=(}l5}>E8Wv*xQseP=NaOe}%U~(2gpF zU3`7xiQy+l7(!~r5k9i~lj~C_g}1&hl79|2wdKww1fGWP<-x2Or&Y=i z&RKqVGUeA1JQhiik!w8fPBOrvVD@%;ILiI#8)2i{^^`FDSB6rb9vaHVjeM&at$NNa zp9z7m5uM^*I3*mBS$Dhp=PBVvPMv<~E8(}wlVPwj83s>fGLY$Kr`8PFGpB~Pl_$f> z%_oD6woQiIdrXGS#`xo-u1mvd3^UN#uY!J1#XY_n_SI6wQ@fsg+{i29Y8{}haM{i8 zHv?DQTr&eV-u%Cvf#)r(nSt{;=~^@Jk(wDeG*mVN&%3E=2I^s$8AwI{?hO3+sB5wF z8E$lb`0a3U&1C)fsK?%8AlH<_y;#M^4}|A;5$`<%FT#TK4^c$KKF(p!xoh%Gk^9p? z7>)EKuxW?{-jgb%-O*b&i6*?fQ$ak!mpH!Hh~{QWC>bev>tuHGkI^fUWL1mB~L&-xg3U)mKP;DBYr~t zJFIL)QW@f&(92dKDYYEjM9d3*rgc5jKwtx4e`R-9F6guTWfM) z9{iJ}hP3dy*+k^K2N;ZX^n3K@=J z4nsVNe?2GML7Z;>oN&AFHEopeGQ1X^KYS|Zt~v*dc`R+$^tJBFbHZnFT`qk$T+yS0 z3A~6e-YvCvvSkfRaE&|S-0)Zs`rdQHWjHN=>%4HU4D0!dbHk;2JRg-1=yrbiIf&Dk z3&Q@sE1IanM2xAK^htz}Nr%r7>QNpPsR`+@tB~OY`Y&7%?y72jb%Dt^k5JH!of|f} zCoVuZSm?&h1!|AFJ?DnwMiNQ@h)hf?rT`-7L=+o^CinHZ(El7UZLXXfZt($WmVH$E zCWL9NpY;g0{H%d)=M(*5y*C%dodkEczswD{%{Ebfi+N$+mJ)zff|#*z)6KquU05nB zMKO$GX*qsg_~mqqdtzQVc2`We_AYcs*!6c8LR~sxcpKuL8HEvO_;S&xLQ{5nhW|*+ z-6nBUb+?WzM5p6EaA7#H5lMx}l-cQ#d-k%XE!@0;raE`Vg<;3Gmn;G!6eJf1wy!B_ELd3D*;d-MUKH-1ZgQW!DEu?@bdQU} z1KW^Hq@zLMif7aYcg@A&l+5e(Zso<{X%ip8iVDCrVC#i+P@7Z5;2b(?%Zy&Sy7de3@mdR?JUH^#Nn+jQANEc9#pM?EX4SAZj%P7t4kd zp*A4^pS0OcQNyHk_tdoZ%o-+T`lZ)tGL;_44|+J)S|S61-!ro4 zo6U?A4?DnBb;e2XY*dNl{T74CyO-ik&AC$t58jMW}o!|={UoLd%TNDbUV$`Q(=J-CHDz#>Z>lI>i z5>ebYtgq0>9H51fz9PxuJ-UT%m8R8ns@JzK(XDRMsoo}(tBe8CrnKr>YU69(wqC#6 z=mstiM>bxr$A*x{t1ibB0!CENv8x%~woxBMQq(FHe6lqP@ zLcjw6$+8?x{+&j~&>b@$CC6q!z@T+AggDH1=#WZ>ZW6}YE=1BHIQ7lY{9s?|ph~G9 zs#HdZkd%5Lbu@r5ICv0&4V%GXAA^Gp!~UDXf%(4)ax`P)Ylp`omdITMP4$)GR??QU zug+!$UqIv9?A(=zD=)a8UKvi1-sh<+!~Q)?hYST8kZ(KJ*$@$3=a|Vnj_Tlh)_ks{ z`0+ZHI^m>3C$nhqy2d|5?u4t1fBDW;p~4<6z6x!5a@JFQJQwV*QsKz`p0D6= zq6%QB63y>e9!cLJ8f&^Cxm>VPMq_+(XEY{Bwgt2~isVj_s%5b& z>UH~H9mb`H>(Y%Sc5}R+kyiK0G7sazGM_Ef`_~PqGaH!Qg*?ymuvJS!YeH?|01PkZ zpg#9hW*(qQ6y0iG0-Sme93Q77BSygG7ld0K1Sz(t|DxA&=fPS_CWaoR>%+_8aP^9I zbA^=G4^=IF9ErqSv)-#x)Rt!~K*Lw>Zd?$aTC#OuU0O7Z8rNx~F1*BQ%6VVWguqZy z)_i&5IdfqL+qG37Ydvu^XgrM8)JwP5NKRJyidxT_DYagPMLWC#tLAbC<=)}uUlUH< z`git0L7v9u!Ng4q$-qKdl_>B2c};lAcK1JfkF=WSH{nQ}mZdo0;Hi0Q)XRw)SPD($KTo&Dg%i-bVO6Y!i4qoq+`4-t(sOMnbRNkCnEu{+EohkK_oNUj4exyU$%#S91 zTXaEe4|6M{4-K&QsVj5K%s~9yVqNA_K-2~`m!MCs8hvbAouFE*q#j#XW|i$q#cgbaLPDf>Zf%8MjoT78(mX;7@? zgaVfLnv>c8m=3c}4VpdIbz|isp)N!nH-->!M~n%FQ?*H6=)mWAh9+{Yp2b`A^T-nZdXD$dK0K zCWXn6W@jN6pU0MjrWc<1xRz?xVg4RjM;O4;SLm599Jd^(5CtU4JKHvxTca#SF{Q!; z#9&&Q2GqhH+A5<8-$V*n`JBG2fo+HzaO;oj8Q5XkK;(hiHpt7QrVWZL(PnLs@B`0K zLcYmw)`o_fHndqAIQ`?hX+|W=3<;Wx+Eq4Nh`j8wW&!wy1AsLb13aP(^VkEt$QNe_ zlYOVksfb&XG9@#d5oT4xy+!En;oe%oTgE>PRt2~nO=D(8jBCgX|PgOEKmYg;}l zaMBGw4?1ifut^N4ah@2$zjYSlTfqa#5!roSFil6OnlAT=aTy-G2{}Tcd?psR&b)D`RS_by2&j zN(KzTSq;K0ET=;}?aNu(7lSRLaBP0Y8F$|)tzoG(YBIpU!)gjKWQt*dL5un%N|;!Q zWaI33zQ#Tn*^81>NvUUobUCMqSJChoYsB3Qm zdIWKav1Qfj(r7DofEFaihJl77kGc}fP;n-XAP$02UO5Dk7H^bM}b9~IQOHc_PxLuGwTJrv zm{;%Y+k&XDS<7TcQXdeLKF{}$`Du^=`-NM=9h=P7OIWj+#Wry?B?bk=rfCV-h1ga} zg4mnY;}KiaKSAuFFBoF$4T!B#DZeqgNOFSLTC6^w=AA~00Y7!(IsFJXyh z38Q^HktiIq_iBksV+hBK`o zl~|#QSh5}su`Iy9RZO4O94vNXrtt{)MsLQz?gyhgQg~tkDMp$!F_@OU=h*PBjCLiB z^@UB9Gd*D`ff(2lO7ek85Y@#YmKEOf3XoITO7+3BGMS9~KCXNKLdd?zb}*@M_3?LJ zTAtz-OA_2&5$SYhJdH3OoUv2~F{d~7T)Hwv_d+R88xSNK{is<*CJf6#&#hb(c3D^{ z!gR!0sr2L;i}{nWx~$k@%SGD7y-NNQUoEuZV0-F}8pM9bZ_99N$9Ty_Xr3?fp-k~; z$4{)4jqi%i)-Rd0}(?orBt*z zZD7z%LI%e0rSeOJ9vlEYhVRrAqD2H#fZ73vM+WXrZJSE%oD!fhW_gt7DJfGGvm#+{ z8*y+H8UW^&#()O%sWJ~I<_A`VJZeLU`jm+!m~|Lnd~a!;!~U@OHlfTw8*MUs~jX&&Rnvh~54*oq8izUfSz zqnRojLZ&_NECHR?7~bJN#&A+9HI*@Kjqj>FXvQKqTPy@$Lg18f(=;xyZ9DiU%~A!0>y+Ae1pXWsc%? zoy6ZFi{r6Y7AA)z$@h;EBb&IG8plJJhFp#+$^%0zlq~^%QYgKw)@g!Ig&EOji8&o* zMK`Dl6>degP;F9G(=Oi2bpI&OwowAPwM0|5Jm0Kp5Y;MU0!-E}i4a}{leVjFW=fs$ z3w1F&X9f=}qW=kT$x!2z)I8`P@qW06YGvmUb@g~gei9c0E2jy3{U7 z-}Jd&VFAunT4u$ht5BlW5Bn}4<%|o!q|W`@B|Tda- z@@ygfOZAqkEn2WNfQTUGksFi^7;jDrd%tOHurK3j&wP#c*o zRi>3}3Y9uUX9>|^bXMGD##D2tCKUo)5)JlCw!qNJ^f1X%zzmbDm9r+fWVMiL99NWq z%VeQC2sVz({_Lhi*RCgJbMvX5tafKQUe_7YkoHrX9P|GT+Emha?(1{yOriIsAnGcm{J zu{gbv=8#uMKqIJx5~ReJ~O0UDP8np`E}jY=2nmFyAvxB z@@bb?OFX{L7{H}ZTU&719dJE_I%1(j z7+EZgB>VUHT1HX}+Y{n`(}hvyx&=cw5^74e63Ty^UsCeKBkpLoZAx;}Su6lbJ|t8~ z3!D@sy|)ms9%&p4W2t^R^PK8)MFgr{&{Z}n22?>UWwF?$P)`NiO!Iq%X+q610Z~+| z%ZR9O-X)4)v``6dC!-Wf?4Y8)us2*F+b#q@iJ$)`>VszO+H>-z_CfotaHty08;`=1 znYLW|ZL1%B#NSk=v8qgDWqMVHSiJs=eZEjsF7%cPHB=Qs)FFSrekLbndVQJ1G^I1X zPPQfHoo9{HQ48jcnQU>722ui0v8WYNqtTrq+CrOp2F*y7vvZh*;5*I_pCWtlwv%bA zP+V+x1KrTrEC9q!1rO>4UHlS-2cgFUxwFQ9eV1m9qRoJm{%UZU&}{0`*OE4kLLq*A zm61_Su$8Z^zKpB{iBp#;)8dPwPr;6741(nlaIKKyn?tTKj4L#VZ1aZ*To^%L29h07 z^`h7(5TJ@gX!V`P-oOk5^O(-*qZCWC8`vx%*JdaC=6T@+U4RB1AM2=l-AU$^Dwb?a zQhqV1qH<47WIs~g+AL0xqp(C20#;6(nmx$K>0yO-Y8;`NL^ESi3j2z%4-*T1396jZ zh4A!tJYkeC*!Q?F75Ml}rW3n$^D;~TGLuO{AoVLuPTw|#z){K&&&d0X|X~< zkzACFf_uVV_lb>%w-uwhoAUX281#f-v4Qb@LbVmY*cBL|~mX;mBL z!q(ue6|vUeFE?>7)lZ*VrR`t{EMy<6vzD+CY7Jvaq7>Wd7#k^>!&s<7!9wwE($NIM zXuEITPpCY34+|^sLH0Z*{q&N!^72OT(^aU*z$dHoKA<9ils+ELykSqcXN)GyyRqHO zCTvgn5C$Hya~L&Fp9$!ClS=mBBeuv-CrSnucVkrZ!9@W)H@z_mwd_5@KUT zU=!eY`Yy%OItyeW;GkGT#Z`i*%AJznJ;WbuPW@l7`CmY0QOwGp+vXQxXAOz`|0ia% zzz>hvRUrndFdK@uDQ2@rIE2}q31(}lXNH@ivK9r>paA+_kMkB4U>Ho+S6Ej0Wr<}H z`o&cyTc%Gmq0BF>*@L9~s}t+s5$$$Nl)>e4Famum-Atg0GxMTU*bYU!LblOxC%0TO z@%HE?7BdAg7P414d6vI*!QH9;R0=H%y8V7m%jG%I^yUz%;|3t+lbWY#c8;3wk2$R=C-q7hA!Mq0}-3YP!KeM zpWZ<~(?pDUrXcon->KBatWok@oRbzZkMZFp8n=>|QD5*V--i;pt zMu^Bo)iWWm-~vwA;|5wEu?CYA$7(Z*D`)OO#G9i?{dQvQ?NhQArr)Eqm`k3Ljiyk; z+h`&lHb;bG6D)e7QGC0Ev?M5#nT;?9#IqZeLG(Ns0T%MW=5*OMvxJo;iQYK#y=^l^ zO#bh-nMl@1Q_=*;*nu`qk4jyVUQHRm^FC(R%+3DvR4zZz0!ztQn)JobdPsVGOed6U+R(G835^D-07>#s(axo z0tyVeCsu{ul0VPEkB6tr5$v~*hkJAQ?YJkx-Rd7W7Xjc#cgz#vo}=GvEVRY*Z+iga zUdA;Z^mkm{-A~}}-MkqY<|wCWgoIHsgbF*k{JTg~|J>^|#?%eetT_h9~t5 zUo9k$iu+fGv#cj=PzOG1J!z}$Nr$_CbvTNCJhM9Nmrs_$kNUNu+dds=2_kp})88j| zhA+5no(%Vu;r!R1#AobrclDFus9p@H*_s8a?19-xcPCw-9{1}f!;hzDFD?8Impq_- z(C@>3E?@tBxLrRUM%o@l`}~|~1DXBuIpg3rSS3h){6I$i-ryeleYg$B0V8$9RSx<& zuVO;G!O--s34e^=)uU^|wOkfG6|VA66Wz^EGg=$m<4=b_D<|Fd4C81?lU(cCuyqG( z$$ZIS(P%k7)lW%D?#sl>NrT=3MJ4+dvF7v1Iqv0W@uNF!ZTPvuMl8<7UqLuc53v_! zMrq(d;w7fsb0_^V97zNWIz<#p^2th$+l5~Br=xCs-F)3mc{be7YsphxkOCj5q+j$(r{Fy(5N2fQs+x}nyLDV1;U;0DX)edCAJ70(+ zBEyxg>$&h2YXZ_`O%o=$x1S4}I3L!R&`}3!+{ks|{(Ti7rlb52w2-T(q|VDk#Slu> z=svL)XVCAh!|8a?J-;qIjusv8N8G0e4Mcm^fGAZ3BKRTziiW}v4A60208o@shm@z@ zH%R!BK(ODv{Ks(ouwdNEef&?@d9QSb{V5!`NAks0ROd%*by*$DN7mX$=t6&HT{2V* zO0hhnxUtW>yZ#h*DZmLV7wcTIi#~MtvSci&Xj|fUu6A$zDLl{uoCx>vay$6>@U-+f z?xE+yx#_v?bL(+~-QeC>AAWW-w9=N<=4dsl3TD0s9;@8df5z2iuKW3)@klwxt^RZP zvEuVrYkxhATFgjd8QaHsDZ5)@d`Hy0zg_M2cp>a7SIMys$@e(cL9?v%LfF-8-;0?o z;W=)>3*k4K&M_>p&h-5y+%*`GdTi({Qg`xS=-P(MHiU;*zsIn&;=A$r4fOju_vD6f zr}RTE{A<`7JuKI-ytoWoZQHa=BiWj1dB`&D_Wx@*w)Mq#$W@Na<-YpY@UZkHOIQ6h zJTx6{DoMLtUkV@3P_T0&WakC9>qfl0Uv`IV3=1DX>a1lbYFvAm^PUWwFH1BE2FaCP z3IDkHC$4@uyiT9k^KapYse9mW;pnY4Q)krHUG-ge@881hRQ$ESSqsO$VlDj8E8+O` zI(PUhL{@&#ef^d2FT;NLe3yPL9Hru8UbC-%>a{TH^b%(lH39gDg*21qL3hq;;eVuW zcWcO38shew;8^~b-2VTb*~j+3%o>OjGvO>;r2v< zQC2)z!CGd&Q(WY$LK0IdCtRdfR?jT9xz4|bBei00{dWS;yx@-ed-!RsX+QgWIH6?D zU@#~l%rO@!aJeJ_#p-DUIy<95RfA{&#kRt126n{Erp5gxYoWCNa^Hgc6{i|RTZtvn z?!_j2jt}RRFURw8UwR{)K4!jRc0F4Yvnz?aO)i`y~b6K7t7ThjW7F`^?Qi~{kj@7WTdCiVse!q5kHEGqRLUt4Zay3)r)TtUh?K! zw1~R1NHbaapCFoD?mGX0Px@?k@;}0ZpBfNNTe6P?hBVd|RTl_I})VoX%9aCHf zipCGqN$-axYZ&i`+P^JeTDnZ+qiE3%KheT0z}>HM!f^seAMmqPI&fs zO%$~3n1Om>Fe8q82O7y`a-5Oi?AE>$?w(%Y_D{F>xOd+Tzj*-bfUVrqq~{4QHd}^W zigg+V))6%6$@w_MWhe{YRH$GoCjMM|Z`n*P7him>tA97#wavGKH4vl%Z$I&FxF?@< z@6zu7)t51sN<{vuFJH-*VJ2jil#9Q2?WOm(?+mAwO}FpkKXQduxM6x`UJBN;to6*b zX4-ejY`os>mua7vJ*KtEP0qCse5}=CV3a`#OzRD%+|+sC`p&^`V!2iWIzC_w7LuR; zz;4<0E>Qnd+4hCy@1&1$Tjko%-QF)wLsG;I_X?#m?0d#@bia}xzmchxEoH0P`ds@5 z;Fi@l;WTJUdK(e;U+fzw~c?;*&n-bMDLe_78Vg%{RGUrI~MP*=l!JzWp<$r6x{jFXdHk z*%?LIuRAtUv+R6wMGh~ro9W+bKPD|(J6uj?FSD{tka(l7rjW5aJC`e7uM%0TOydoz z4_!mb(j;@HtUiuj%3G{fs&ZF0wjXJnEr-I2&`G_NV{?bOx0ch$J@HEBTd@hln;YG+ zP3^~u-agpWzU2<)Ajl*>h(&=55~41L$SV^!8KbvlmruVSLmAwJ=JpeJn?Dy7l|QdO zE4_F|!YFv?r5$dlsUnc_*ivR@Q2M~jJl5Ra2@?IOx&5thZH3GxEq%GAy?aYlN4+#> zG8j}AZF$_mLcVb`jgJnx?OLs-JzLug-3bj%6VShwhJL%X{gYjrd3$2K6Q1v$Z*3n> zQ<~e_<4#3|DWo`Ij-efx4`KXZc$Pb=t$o-19#dmbjW9uR7G*rcfoyXRSL(R?ecsIz z6l==e;mlFnrUq-$O6`yDk#qMqBzL!m3!Ya9_Exic)FN)A9EP?@vKOevzThbbOVbay&JU zOQ%nYQWMfqn!gOeP_VO+gDg{gVmeAS_qyh=eRS#X>E>~pfSVBjytZzI?HCF7;EM|Y zlh}idOPWdsf3mD_mr3N78LW%#xI2A>n^pr=UhtNo4*~n+}^JG z-mq`)Tw-tMEgAOhoL}18HKhma_2>kAe3LuYOvnlmW2j-N`( z0;Swgow80J+fep^BEAN79}ARH`00os>EJR8k($ z$5vK zJXGGl`osH%-fyTp`1fC;-Rml8v};Y}!7^y}^EnF+HhU3sdVD;{#at3L#qHIDd#iaX z2KN@h8(krIA?6@xAA7snM~^m&mobVrR$xNXhkV5wpKu@VYX5NQLSzei%vxqYP$=E0 z&*Af#=21zNVY*-up~z!3RnoJ=rPKUK4G^vMU-tP)-=;QwWE0M1^ZNkcfehKnUf6notyw77!g&RP>6X zfP#V>AaszT2q-EjDkv%-RZ&n-zxOx0XIZ%S@3+tMW`1SbnKNh3oH-@_PitE;!FImi zi}!2YHl9b(XGiOE@hYy4849l(E?1P1(eXOdYP~RCb&P)lk9=_KhP|oJ#;eNh*{Pgj z#AkN5*>3;7{$FN_tw3!9duXBdCaBS_*YuhMwVYsZq8iG#$Mz(uHc78>UiY$kn&Pe- zHj?Kv3C7oq=^X7@qN^pTDX!i6l_b@I;82okNl-CawIS%0%#?e#ej-`bAj74}stVeU zWOW4Xl`3i_!R=MmT?8wts(}RI)tET!*6Gz$m6T~aEm;ec={pba$v@3yXUelBytfeP zy)aE@R#WMunO#jKCGTeWQ67i>h!wMFsb0#XuX|kujjZ$2^ww&sRb|-%v3q#Oo*wjZHAndtGm=PV3Cj zb>xx3!)Co`Wmw<}m?m|i`*cQ%svmo*j3+WC-v2=}%Tr(am|mQs;-xcFFkO(Y>9`vi z_x=?1xU1MVu!idDa;?{kYO1BTu6)wPt6=iVi9EEp!mN4uZM=Ab&H0qx{s^L_d*3hG zEVlsDWzAU*3lpc3lTOd8rDEHLW?44iGRgmEj*CKb)Zf%nXX5-B^tirG`dF%p?OinU zT6I$cc^4bo|IGZANjFrNHD?YcUPto0;Okgh-Rk;>!V3R?gE2&%EG*prs7p^u`1{K5DUk;-x4qf<9J@w`_fc$A=3jHD7XbB>mr1l$Yd0X6=S;JcAitR z*i6JZ=DX#V_sQb_jyQw!Vld+5K#tRya|De~1&r@L={FXdJbw#VFNCH)6EGfL5R(32 zp!AQP2rIjw_jc1ycmte^+Sx zZx3XB;%Z3Nb^TedoUFIiRdwo3XU=eACR3Rf1Z^q)`1=p(*m|mt>!`0yJw_7Gi6zVi z&P6kpO@B=!z3(Qq(sN}0R^C@(F!6qfzX?j#Q6w*9kfhH_-j$Xj-}=W;~MHRVdEGAS&?b3W8F zy-bMbc&O*4pr??$FNM0aJ0wwYFdos4hkAB~Dsw}lO$$|4h5MW6$U%0BY&8=W(`GA| zZ!-ogdUHc{ci{_xn1|l=J0vETOn=YgS}M)EV7^>SG2)oSQ=i{{eQu?bcU*6n#f0s; z@;>C6>pH{2)z=IaEpOrRC%GuSiL1IxQPLT_M^~Xg>*6b{oG_(iB5mJ>tr>AdmuZIg zZyu82!j_(4)aCR9{n+oa7`^`NBh+rSD21!%u>}1-Keoj>&7|S%b8tH z{ye3_`C_=|b$Kt$d_>d~^y3#!TB9!g#!;n{(A}6IEz^q{tLk+QZD8JftyZQP<6Z=`3h zuFzDal;1CQ`sXlKJooChP1PXR0{v7|)gt~gJb_$fhaV28`kSVzYW&JYPL0fNn8WI< zY0cPX%m8QV5zSPSnzN(jKCDarcanU9LjGfuXJ(k3C5L${@vbzDzqy&(=w7^D7d2On zt1|7gy!`8V@6z>hBn)SruV#2>W<8x{a<{j+>P*!;4B>b{0W$Gg<@M|Dq?qWtmY5tL}}^yAws`4><%DPGE< z3T;}dE+l=drAkmHXP&VWZyDo#y``#A)6{?u3tV3V9yOMD{tBFGsZzNoSh1Cga@j{& zF$uhP&f@bh@3(V&^;)YIVSKUs!M3bzf1}@Ms|pCJwNt&^ht}$0?Kls7qvy3#gG4>m zPTehGxAv;JgpagWRhcWuq}Q{dvvrqQgj)4Bdq+lbF}u8LM3vqocUO6e%exqZnHu9< z=fsr{Ude4BGbs8GEJmtpN7+06vD5Fl8X4m0@lKS#+y2Ff=I^$qlN{pY;#aTba>f}m zy71P@OGmN>IJBoG~LUI945BhH3}Z=iWl?7*wAqygv4C65A7WUs-`#J*bw5nh;b^ zih3>U{h}rYy$7$)dByeWVW~$=&+LkU5%v{HT$x6Bz9Z`d#ZzTE&Lj4w{W(W_Kl^4g z&A?H{`GfG>{+9bfRQckcU!5L|oMbMxDUG$tiZwc4 zcfoDi{JiY!3X(F4k+`$0dl-q`!b-5%kk$vK%X&;F)jn^H+i_iXIntJ(?agq<_H8g> zaZp+nOtCDO;&hH5 zGcJ8kXNI?hdQE3ln*~hoVrL%4FwDO@-X4{t!@HwRTq=NzTMP%Owrxd3WAdEN}dP%w!5k$|5YHm z2bb@Yb^RVHkzu4;53cYR>PLI1jszQesAB{#_2enwg?e*O2Fiu{q(88G8Le$E$<3G5 zi<=7i%igMlyY}n*a#`!sU-VIFXm$Ik8oJi4>Q2{Q-@k9=mddplu6|C8u{`XwxSzV; zbyok~PqpVxRr~(DG) z?=PRAkM>s{{+rH){;F0w0~J|I;4^c{#4`_F8Mt-OjXs#9#XB`m!f=0L)GZ8 z^8Xd#4)L8Irrro+e?OP2s{5mak%wFIct4j8vC8*juDaWmX4d}XIzy@!*zPiHNVtDP zf{7j;N!OaJUmB?@%hL?Lk*W{5hmTUNxCh^Elxpue8D2An+s^V)i72L)pXeE*R6m@D zN2w<6zudZ9K1)n1blrTGkxuJd^O>5Q)=To)_|v|V`P^hJE0;jrbUvpe3)E^!vY~); z>uJ4aEJs5W?f;kCSAGoXuw{)^btOviSSN~6kB&8k{5h6OrPI2~I8`ssiXX? z=6w|2G1m8tQ|*XxU{Hlxdi^-A!d7eVcr~70zD|NvSz4lB^1PD#XG%`?J$d|>eQNB{ zbjL`~Ha&0x2ihuq>|9uaerp2LFS@#X+KxVw;3?K$PEZZp+b`(oiK>ykj;Bfd|Hq(W z;K@SWd!o82Ww9&>CCK_&V9`i)CXyzy=Z^^e%tY0|vstXNOco~9$NI@cRWDYi?=kz9 zmA&F(rp1y*%;V#Io=4QuHu~pMyRmO(rvOw0%u1~)6; zK_!fp`k)fd(tc2hVA3VZ;V4N|2K1&qDoLmMR5x3<^H}9Ey-FyqH{0n)d}=__C3MQO zt`L?ZjBM|*A_*(B*C%}H=jfyke!WHqeY`~FH|>e;!G9NFMZ$*(D-ez)j3Jyv7)SUF zp_E5C)7Ppxb|3OBi+&cnlz*{<-n>>7=x$q7jV9L6j{em7gi&}W5=t$c+D=BVg#HYn z1Gp9XHcvZH#ZQz8VFam*N4`tYDwEWd8`Ed z@{{UG7E*yT9kr)aH4pQyo~-o-`ffX2x)%{DceC0h4d1wM@srMW#M7;UN&?+V zlsM@~i6B3xd6GDr24g03+aRd0`9(9G47r#t4k}gX!9k@eMfsJx$hI0~1zpi{L&?dZ zIt2w4&N}CVN(~B{<Uzvpwa?}8b={`& zUGCy}zO~!A6wviItNQwtU8+hAE(pxe=}C=m(3|LryVXsq=?&&)y7z7d`>i`vV>U;+ zqSH_kN?&sHfxSG|eUJs&FbXEXWRRaza0{bd)0@!=>b|cw)R68VKPO^qzZ%p{4QuIs zpR2Oon*%;MrX@c20OUJc<`@^f9kdV8J0Q!?$-ARo__W#eD`cdM&Kfl)D|bj%M#_kckya=2bn4(m?hNVl|L63S-6~y;zm@*K@OgIA zcd@dYy6b(L?b~(5J?dfo_8xZ47kktqJ?;aw(6v%~K2*=Ur$p!#AFARugYxo+j~ zs@GhP=}I4|My>*V%SURT>Oec(g4c=FQm5`yUqv-&(4c%Ye=OZ}znbfrn9{Ie!#i__ z=jM&eO?fn>j?O#46JPBQD7!*1r5TCzy$96o8M%x^Qe^E%uVtK%ts1uM+#xw*GP8oo z#$rzisX!5WE%b&XGJ|oR#+DosXPowYth&^=7jxARb9|VbPp%bN#aHmLTI_O9ecM<0 zpvrRBzn4ZE4C7!bEQH0d64t9Klc>Pg4cGwy$9 z>BMhUwU!IB{3nWx+?4Fxf~=ug`Ifa5TQu97kuo?hFDEM_7r7K$l;u720X=O}jBmuZ z>R@>JPbzq<2XQ{c$)cX_e@MmYh@Vu=>a?Fln^_Sc$3y8bDg!%U&oGbnZm}D*>YAUQ zM`7mYbnqw9X2L9P__NyPKYLH(vPP5#q2l5%0bmz*E+=%?OEOFlp0!fP8E-JH?H2aN)ln8B#(73 zVMC~;|2m;=(i!pg#HWg5%Y=JReZ55=i?>^skyXN&J9Xo9JE1obgX7?<$V=cgcpchh z7i8sU6y)VI1}#N*s`m!+o3M-r2#%7~vz4TJ%cXh~^{&(Ee1jF}y~kzb4;?))BWFz3 z=(o_F(e`a*`8f@cvdAhc?VD5j1gI81j)=O$UuJ=WG)~%tOofxsn_VN zv#Ql??_u&m39N;6Pz5B~dMJerun{(aWGBAOum!flHrNg_p9$9&&Z^j?-H1-fdyqeX z4`HvRH`;bh-RYcaSNIhMIZ;S{U&A+W6pq11un+dbx9}YthlB7bdoq=nbKX`=CqWTM7FD{pau(cAde}cm9Qb7u*f^ z7@uX`OBkB(edzatjQpYbK8P;)W)Kbrsnig{Oh`ozw{(*)?O46y7uD$IY>X)wWmFo5 zJREXh1mr@xNnlxd$RlAC&%3B%>&`}ufjJ=2=MomfJeUs>{VBr!dM`Dq^Bkf?d!BF+ zyZ|qPM0<&FG3>Ln_lTWTDDhrLUkY!)n;`L)5lYRM!wPr{-iDPh5LSVteTVQ}SPg4H z(!NJ1KW8lUp_jl~Sa-dz&{~h76gI#{*aQhAmc6kVc?)cXZ6H6f%K*I{eFwY`J8$sq zLf;L0-~;#&oOFAUKZ1R*-_^yk4j?!+{TNwl`UxC_Pr*s}8S>|F2o8h%SZ*k}=*##` zwNNd;z$S%$2}j^7a89&eBYy)&;TU`i{xY@qE4y;x{}t~9@%}I0$$#*j`Ul@n|LrRb zR_yeDJA=M6|KL0O559B%;5%=8jF+-YZ)~Lt|B&$KfAIbC559~4%O{7Y?3TY%nKx4r z>34E@_bZ{(HGV_B1ef7=_ye3>a|QV-{0V=-CelhDmW|pBTi|c(tTSEL7hUMm7Yp48 zVIaR0kc8prl4%5CnSif;z!w?tl_iV{`1<3Mz8Z}l1LdH6!1u3!uR_3AksHJhAbp%TPFWw#!=L)Fs_ zE849K<1yTxHzKP~UfxLJ1`nqM>^WJvV)Ab;VIsEd%<vqIis&jR~36f*67LwqaVu7Vfs6|8ipQ3kv6KXuT-=fx{H_U zn>_ZNweMTt;ZXMfoWuRafbNvjKrivw)A(O+*M_InM5%#ZiI6i&FJ<>FY=Q1{8J6

bGhmjt21)EjV&iT*Vvp^TIsu#Y{$6Dufpfp-tkqeYEN|Y zKb&V)x4$f0jmybGN(t51NS4@^zUr~wL-s)ltc7(TKk0SC`Wy6j(ZzP0w55a_U?XgT z&5)Z{kdl!y()WFeeU~e8{A)C8@m}4vrailFm&M#afjc1!MnVx3!%MIneDFSe0!QE! z`~fj9b7p~RP!C$ZtW#>)1Nx>c@mP(ZJ@kdUAqO6VIj{(pLJ7PNpTiH}rec+#K6HkD zkoB70T+6OgZ3~;>?0(r(Tfynrt1VqA)vj4Vn#gJ19d2E;!LH|H@G5boz8{i$C-N?! zYg|1@*Z9#K%)0|!qrwhu8Wh*D7Y;~U=D!QlfG~obn+EjOgtC7-5z5K7XTaZ=P)Z*V z&<7EgLBEes;%6<>6VmKzoH=4;`OC;yJL&5iRFv{&Pe|awxe(ioQC=q`8zlc-|OLZ?U{)` zAf}M5)bK~-6L1nv>5BF2DRH5l-|5sp>DTMo)eBD(D`QMS-UC^qhh&V*vd*AOLDB|i zk%ttFN4mC0&SA^S9osiAH>*d%2>(Im?5*?ox{MiJkT*i+6=Ti1)CFu#k^i+W#qs~+ z8C6>kyUDIp_%kt`l7B(I2*1K_a0#5vzl{7l`~g=$eohHj(f@?M;BWCU4mmz&{BvQG z@y|`@jB#OMRvjxGLj;t8$Pg!QcUn;p4Kco2_3i4eD&>(=$zL|T0&+$0fC7EAhn-Mp zBRN%;u|F2w*}IjHvsDjng8I+^oOBJ5C0!$E?Cajp{=^m41i7i8kKJt7NX#fG z$Y*>%N!M;BJ-ZpM=7M&$VEwMJv0WzihEP@Anvp{EaoS!wwT)q+S}SrjpIh+Pfj)3W9}fBf|4G}&L2&#(@*1< zRT`@!zD|O_B1fCpQL($k+eExxJe>uz5%HWNyi?EMY%0z!IBpS4(!^nT*}2@v6Sv8^ zE3R&WKj-46tBM& zvkJGxMp9=k&VMGxV2N=T-n#{Tpt+qEbr0gbf+R?bef1aOnWni9=wWH$A zl3X6Z^PnKf;`EFbcIBuH^udBZ1vg2!#d?txZ6w7IJeh(%*1~QSyWm$GvQ}qhVSGrC zPF1U_EYJ_mp#vP07^lo^awzVH1^-T*+LB(JjXq2;Tbh((T7vB4C)p0imm~PoTy04W zN1*2l#+F!+pFM&%?=De}=_`4-MheEAH00q7>1k6hJN{Al@&&(t%*c^uhl=0q@zMAS z1b>mITS-s+9j|H7F?hxb#+x8_EWM@TH9Kw`p7DaT!cDPPgUSAlaf0B_ba87tBDUlY zl9|Svi02W(w1WPkwH+OMS-fV09>w#RAZcqR=420{T~}YhZR%Tyt4J`3ll_;6+(MEF zrpl9WP8Ljrc-bo}uR6{tIHn4I=TMxd#hEXy|2U2(1Sz>y)HKBDg7n(7*b{#e&m7w` zFg_`mtxJ@TopZpN(&h;P(&CC>X-Qx#(}w*Oh9_ z#aApy9piLLTlUC)@tWM{;h8U()cU@*j19Ajwgoo+DLe}V|Mq2Z>uQ%cP12`vJR=xq zV%AtL;Js5P;WjO+aV-@5IhSikx!c8SM%ZWZJSUjMx>q}n>*vuI2_{A2h>Y>{j>)80 zVUBe%tBxY}N9|Nny764LolO#+y8vqf17f z(-OgS@ntxd3uY6m$~KJ7%V&^S{sd7>%dEitmLO4T>Lnd0^=FR|fnyBaaKE_gN%ODARVv7s8lN1P+IgowNrZ+HVFS*Mf~jVW?9mU5 z$<54qh-;xtcC$n?#cU$VX2Db~DL;=`jPBw$N6Qv`TLrzP6Nl02nK;aWx((xY!L(ca z7>2d;;xwst;CNp!&IEsx&%!%Ssn~o$W+>(Mpf?GOU0S*tSxS>ue85{g`;pjOP>NgCLijb1C6t zb6xRinE#TqRTsN{`Ok1Wv$fTFWEXo=%IBC4!C@XzE1plXc_!Hxn7)J~zJ9mZWnAu( z1-?OD?Ve#RzQHKBUfbp54H=%5*L}+e$Q7^l*`{+x)qrL37 z@Y<(Bvib@6G@OwQTuNSNJyNH$fp*F4XM5UgJm9f5z$VxXTVNaPfSs@l_P__Q7xuvc z`1pW+tDoJ$zA?_W1HQld*)3gh=RzuS9{GY)MEAVSUZxuluq*1Y0d@n|C0~;P_A+-| zI8$<`0V0sgK&0=&9bCYtMu+%fL>38*s7A-WbQ{GoR$c}RrzP%oki)*}1MyOv z)|&BE-)21i-KhPe&wcyZ@gyS8*yvkvm;JZxo_pN)R*qdJLH!b?%#Hte_P=ydkKQrW zZgjIJq-_;)sBWW+t$(z#VhJlj98`vQ-QaP1Q0+uSXSXFGCqor>P0=&kt(Z-6!^^4~ zZVp1J-Eq}I5>!V{kp#NkbUWR1nU~gLT=#O~eF5QgOJBisJ2lGp@;tk@U3gG@ zi!JZ#M%ZO}eMb1h^1d&EyVD}e`%OYG0>GJlflUH+}-bbHt9ua(&Ds5&%j8l>yD*Vzx2@zP^9Pw~aAx96AT34N;t zw1U>qrkCR3dOObd(Kb8X<bp+>% zy%Vw*Iztz|V!z!;-GUgJZC8C}zuhdZ8)m1Z?#Ml$r*3h;?p~``h^sepA8~QO?c(zq zF=OO-yH&q*z#d(WccWt7EGLG(;X31_YUu0pG5eSA5OF}mZN5i8v3G^LH|{XcKDjS$ z^i4WszcNJKiBoK6SFZ|1s8{9E&2^oHpFH01LwYk5Gz_9j`5g69q62S~>8;vBx+DAr`VeKoC{KMKABbs6D zPa~RPZT24$!3=AQL<|gT%Z=V; zD>Ey0NrY>SUOq2&9WN3MogZ7Hsk|i>(J8FtTRP1qQCvlI07oe1D{(O{QZIIdl_kD) J^JA;H{vQ|SKpp@9 diff --git a/web/workspace/src/lib/workspace/config-source/generated/types/ConfigSchemaContribution.ts b/web/workspace/src/lib/workspace/config-source/generated/types/ConfigSchemaContribution.ts index 69d5a595d..957a1a06a 100644 --- a/web/workspace/src/lib/workspace/config-source/generated/types/ConfigSchemaContribution.ts +++ b/web/workspace/src/lib/workspace/config-source/generated/types/ConfigSchemaContribution.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { ConfigProjectionValidator } from "./ConfigProjectionValidator"; -export type ConfigSchemaContribution = { provider_id: string, namespace: string, version: string, source: string, projection_validator?: ConfigProjectionValidator | null, source_digest: string, }; +export type ConfigSchemaContribution = { provider_id: string, namespace: string, version: string, source: string, authoring_source?: string | null, projection_validator?: ConfigProjectionValidator | null, source_digest: string, }; diff --git a/web/workspace/test/config-source/api.test.ts b/web/workspace/test/config-source/api.test.ts index d0269c215..6bded5f07 100644 --- a/web/workspace/test/config-source/api.test.ts +++ b/web/workspace/test/config-source/api.test.ts @@ -244,3 +244,41 @@ Deno.test("config source API surfaces bounded failed evaluation details", async } assert(message.includes("structured diagnostics")); }); + +Deno.test("config source API carries bounded optional authoring schemas without changing validation source", () => { + const body = withSchema("validation"); + const authoringSource = "shape\n".repeat(6000); + const contribution = { + ...body.contract.schema_bundle.contributions[0], + authoring_source: authoringSource, + }; + const withAuthoring = { + ...body, + contract: { + ...body.contract, + schema_bundle: { + ...body.contract.schema_bundle, + contributions: [contribution], + }, + }, + }; + const parsed = parseWorkspaceConfigTreeResponse(withAuthoring); + assertEquals( + parsed.contract.schema_bundle.contributions[0].authoring_source, + authoringSource, + ); + assertEquals(parsed.contract.schema_bundle.source, "validation"); + for (const invalid of [null, 42, {}, "é".repeat(4 * 1024 * 1024) + "x"]) { + assertThrows(() => + parseWorkspaceConfigTreeResponse({ + ...withAuthoring, + contract: { + ...body.contract, + schema_bundle: { + ...body.contract.schema_bundle, + contributions: [{ ...contribution, authoring_source: invalid }], + }, + }, + }), ConfigSourceApiError); + } +}); diff --git a/web/workspace/test/config-source/wasm-parity.test.ts b/web/workspace/test/config-source/wasm-parity.test.ts index 23449ea63..637299db7 100644 --- a/web/workspace/test/config-source/wasm-parity.test.ts +++ b/web/workspace/test/config-source/wasm-parity.test.ts @@ -381,3 +381,113 @@ Deno.test("generated WASM completes asserted WorkspaceConfigSchema keys", () => assertEquals(result.from, 2); assertEquals(result.items.some((item) => item.label === "profile"), true); }); + +Deno.test("generated WASM authors value-based Profiles using the Backend schema without materializing authoring fields", async () => { + const source = await Deno.readTextFile( + new URL( + "../../../../resources/config-schema/profile.dcdl", + import.meta.url, + ), + ); + const authoring_source = await Deno.readTextFile( + new URL( + "../../../../resources/config-schema/profile-authoring.dcdl", + import.meta.url, + ), + ); + const schema = compose_schema_bundle([{ + provider_id: "builtin:profile", + namespace: "profile", + version: "2", + source, + source_digest: await digestText(source), + authoring_source, + }]) as WorkspaceConfigSchemaBundle; + const content = + '{ profile = { entries = [{ selector = "project:alpha"; profile = {}; }]; }; } as WorkspaceConfigSchema'; + const tree: ConfigTreeSnapshot = { + revision: 8, + digest: "sha256:value-profile-tree", + entries: { + "main.dcdl": { + path: "main.dcdl", + content_type: "decodal", + content, + content_digest: await digestText(content), + }, + }, + }; + const valueContract = { + ...contract, + entrypoints: ["main.dcdl"], + schema_bundle: schema, + fingerprint: await toolchainFingerprint(["main.dcdl"], schema), + }; + set_snapshot(tree); + set_schema_bundle(schema); + const evaluated = evaluate_snapshot(tree, valueContract) as { + projections: Array< + { data_json: { profile: { entries: Array<{ profile: unknown }> } } } + >; + }; + assertEquals( + evaluated.projections[0].data_json.profile.entries[0].profile, + {}, + ); + for ( + const [body, token, label] of [ + [ + "{ profile = { entries = [{ sel }] } } as WorkspaceConfigSchema", + "sel", + "selector", + ], + [ + "{ profile = { entries = [{ pro }] } } as WorkspaceConfigSchema", + "pro", + "profile", + ], + [ + "{ profile = { entries = [{ profile = { wor } }] } } as WorkspaceConfigSchema", + "wor", + "worker", + ], + [ + "{ profile = { entries = [{ profile = { worker = { mo } } }] } } as WorkspaceConfigSchema", + "mo", + "mode", + ], + [ + "{ profile = { entries = [{ profile = { feature = { ta } } }] } } as WorkspaceConfigSchema", + "ta", + "task", + ], + ] + ) { + const cursor = body.lastIndexOf(token) + token.length; + const result = complete_current("main.dcdl", body, cursor, true) as { + from: number; + items: Array<{ label: string }>; + }; + assertEquals( + result.items.some((item) => item.label === label), + true, + label, + ); + assertEquals(result.from, cursor - token.length); + } + const invalidContent = content.replace("profile = {};", "profile = 42;"); + const invalid = { + ...tree, + entries: { + "main.dcdl": { ...tree.entries["main.dcdl"], content: invalidContent }, + }, + }; + const diagnostics = analyze_snapshot( + invalid, + "main.dcdl", + undefined, + ) as Array< + unknown + >; + assertEquals(diagnostics.length > 0, true); +}); From ed3bcf35bd2c039909a7c9b85a12905beed8ce9d Mon Sep 17 00:00:00 2001 From: Hare Date: Wed, 7 Oct 2026 00:31:33 +0900 Subject: [PATCH 3/9] fix(worker): remove Wip prefix from gateway tool names (T-712) Expose Discover, Inspect, and Call in WIP mode and update schema descriptions and current documentation. Preserve execution, argument schemas, internal types, and ordinary Tools mode. Add a registered-tool discovery/inspection/call regression. Validation: cargo test -p worker --lib wip::tests:: (43 passed); cargo test -p worker (827 unit + 110 integration passed); root cargo check; cargo fmt --all -- --check; git diff --check HEAD. --- crates/worker/src/wip.rs | 78 ++++++++++++++++++++++++--- docs/design/workdir-wip-projection.md | 2 +- docs/design/worker-wip-mode.md | 14 ++--- 3 files changed, 78 insertions(+), 16 deletions(-) diff --git a/crates/worker/src/wip.rs b/crates/worker/src/wip.rs index bbce6e9cd..c2043edca 100644 --- a/crates/worker/src/wip.rs +++ b/crates/worker/src/wip.rs @@ -2426,7 +2426,7 @@ fn one() -> u32 { #[derive(Debug, Deserialize, schemars::JsonSchema)] struct WipInspectInput { - /// Opaque interface reference returned by WipDiscover. + /// Opaque interface reference returned by Discover. interface: String, /// Explicitly supersede the cached descriptor observation. #[serde(default)] @@ -2435,11 +2435,11 @@ struct WipInspectInput { #[derive(Debug, Deserialize, schemars::JsonSchema)] struct WipCallInput { - /// Exact object path returned by WipDiscover. + /// Exact object path returned by Discover. path: String, /// Exact interface reference selected from that object. interface: String, - /// Exact operation name from WipInspect. + /// Exact operation name from Inspect. operation: String, /// Original operation arguments. Compatibility operations accept the original tool object here. arguments: Json, @@ -2521,7 +2521,7 @@ fn wip_tool_definitions(runtime: Arc) -> Vec { Arc::new(move || { let schema = schemars::schema_for!(WipDiscoverInput); ( - ToolMeta::new("WipDiscover") + ToolMeta::new("Discover") .description("Explore the authorized WIP Worldspace and stateful Known Space. Start at `/`; use reset after reconnect or authority changes.") .input_schema(serde_json::to_value(schema).expect("WIP discover schema serializes")), Arc::new(WipDiscoverTool { @@ -2532,8 +2532,8 @@ fn wip_tool_definitions(runtime: Arc) -> Vec { Arc::new(move || { let schema = schemars::schema_for!(WipInspectInput); ( - ToolMeta::new("WipInspect") - .description("Fetch and validate one descriptor returned by WipDiscover. Inspect before calling an operation.") + ToolMeta::new("Inspect") + .description("Fetch and validate one descriptor returned by Discover. Inspect before calling an operation.") .input_schema(serde_json::to_value(schema).expect("WIP inspect schema serializes")), Arc::new(WipInspectTool { runtime: Arc::clone(&inspect_runtime), @@ -2543,7 +2543,7 @@ fn wip_tool_definitions(runtime: Arc) -> Vec { Arc::new(move || { let schema = schemars::schema_for!(WipCallInput); ( - ToolMeta::new("WipCall") + ToolMeta::new("Call") .description("Invoke an operation only from fresh object/interface observations. Stale validators and invalid original JSON Schema inputs fail closed; unknown write outcomes must not be retried automatically.") .input_schema(serde_json::to_value(schema).expect("WIP call schema serializes")), Arc::new(WipCallTool { @@ -3093,12 +3093,74 @@ mod tests { .into_iter() .map(|definition| definition.name) .collect::>(), - ["WipCall", "WipDiscover", "WipInspect"] + ["Call", "Discover", "Inspect"] ); let metrics = runtime.metrics(); assert!(metrics.ordinary_schema_bytes > metrics.wip_schema_bytes); } + #[tokio::test] + async fn renamed_gateway_tools_discover_inspect_and_call_through_registered_surface() { + let calls = Arc::new(AtomicUsize::new(0)); + let mut engine = Engine::<_, Mutable, ()>::new_annotated(DummyClient); + engine.register_tool(echo_definition("Echo".into(), Arc::clone(&calls))); + install_wip_mode(&mut engine, None, "worker-a".into()).unwrap(); + let tools = engine.tool_server_handle(); + tools.flush_pending(); + + let discovered = transport_output_json( + tools + .call_tool( + "Discover", + r#"{"path":"/","depth":2}"#, + ToolExecutionContext::direct(), + ) + .await + .unwrap(), + ); + let object = discovered["known_space"] + .as_array() + .unwrap() + .iter() + .find(|item| item["path"] == "/tools/Echo") + .unwrap(); + assert_eq!(object["state"], "fresh"); + let interface = object["object"]["interfaces"][0].as_str().unwrap(); + let inspected = transport_output_json( + tools + .call_tool( + "Inspect", + &json!({"interface": interface}).to_string(), + ToolExecutionContext::direct(), + ) + .await + .unwrap(), + ); + assert_eq!(inspected["interface"], interface); + assert_eq!(inspected["state"], "fresh"); + assert_eq!(inspected["descriptor"]["operations"][0]["name"], "call"); + + let arguments = json!({"message": "hello"}); + let output = tools + .call_tool( + "Call", + &json!({ + "path": object["path"], + "interface": interface, + "operation": "call", + "arguments": arguments, + }) + .to_string(), + ToolExecutionContext::direct(), + ) + .await + .unwrap(); + assert_eq!(output.summary, arguments.to_string()); + assert!(output.content.is_none()); + assert!(output.attachments.is_empty()); + assert_eq!(calls.load(Ordering::SeqCst), 1); + } + #[test] fn mount_collision_and_native_replacement_are_explicit() { struct Native; diff --git a/docs/design/workdir-wip-projection.md b/docs/design/workdir-wip-projection.md index bace39405..53a904cdf 100644 --- a/docs/design/workdir-wip-projection.md +++ b/docs/design/workdir-wip-projection.md @@ -165,7 +165,7 @@ Ticket/Objective/Merge Request routes retain their T-698 contracts. ## First-Workspace example -1. `WipDiscover(path="/", depth=1)` finds `repositories`, `workdirs` and +1. `Discover(path="/", depth=1)` finds `repositories`, `workdirs` and `workdir-attachments` when permitted. 2. Inspect the returned Repository collection Interface and call `list` with a bounded limit. An empty registered/visible catalog explicitly returns an empty diff --git a/docs/design/worker-wip-mode.md b/docs/design/worker-wip-mode.md index f77be0daf..4798fb19e 100644 --- a/docs/design/worker-wip-mode.md +++ b/docs/design/worker-wip-mode.md @@ -30,9 +30,9 @@ The implementation uses the crates.io `wip-client`, `wip-protocol`, and `wip-htt A WIP Worker exposes exactly these generic LLM tools instead of exposing every enabled ordinary tool twice: -- `WipDiscover`: observe an object path and bounded descendants, beginning at `/`. -- `WipInspect`: fetch the descriptor for an opaque interface reference returned by discovery. -- `WipCall`: call an operation using fresh object and interface observations. +- `Discover`: observe an object path and bounded descendants, beginning at `/`. +- `Inspect`: fetch the descriptor for an opaque interface reference returned by discovery. +- `Call`: call an operation using fresh object and interface observations. The compatibility projection mounts enabled ordinary tools at: @@ -46,7 +46,7 @@ Each object has one opaque interface reference: yoi.tool//v1 ``` -Its single operation is `call`. For this compatibility route, `WipCall.arguments` is the original ordinary tool argument object. Native projections instead receive a JSON object keyed by the descriptor's declared parameter names. Native input is decoded with the published `wip-http` descriptor-bound codec, so named and composite types are resolved, an integral JSON number remains a WIP `Number` when declared as such, and WIP `Bytes` use the codec's canonical RFC 4648 base64 JSON representation. Tool argument IDs remain JSON values; the adapter never infers a domain object or Worldspace route from them. +Its single operation is `call`. For this compatibility route, `Call.arguments` is the original ordinary tool argument object. Native projections instead receive a JSON object keyed by the descriptor's declared parameter names. Native input is decoded with the published `wip-http` descriptor-bound codec, so named and composite types are resolved, an integral JSON number remains a WIP `Number` when declared as such, and WIP `Bytes` use the codec's canonical RFC 4648 base64 JSON representation. Tool argument IDs remain JSON values; the adapter never infers a domain object or Worldspace route from them. The interface descriptor uses WIP `Json` for compatibility input and includes the exact original JSON Schema in descriptor documentation. Before execution, the Host validates the JSON value with that original schema and then delegates to the original async `Tool`. Constraints are therefore neither approximated nor silently dropped. A schema that cannot be compiled prevents WIP startup instead of creating a weaker projection. @@ -65,7 +65,7 @@ Object resolution and operations are separate contracts. One mounted `WipProject - A native projection replaces a compatibility projection either by the original same-route semantic capability or by an explicit registry capability claim owned by a mounted native route. - Conflicting native claims fail startup. When native is selected, the claimed compatibility tool is hidden rather than exposed through a second entry. Normal Tool mode is unaffected because it does not install the WIP registry. -`WipInspect` renders the complete effective descriptor, including contributed operations, descriptor-local named declarations, and recursive record, list, enum, and union shapes with required flags and declaration documentation. The tests `host_namespace_object_resolution_and_operation_contributions_are_independent`, `operation_contributions_reject_interface_and_target_conflicts`, and `native_projection_preserves_descriptor_and_decodes_typed_arguments_end_to_end` demonstrate root allocation, unique resolution, contribution merging, collision rejection, current call-time authorization, complete descriptor inspection, and descriptor-typed invocation. +`Inspect` renders the complete effective descriptor, including contributed operations, descriptor-local named declarations, and recursive record, list, enum, and union shapes with required flags and declaration documentation. The tests `host_namespace_object_resolution_and_operation_contributions_are_independent`, `operation_contributions_reject_interface_and_target_conflicts`, and `native_projection_preserves_descriptor_and_decodes_typed_arguments_end_to_end` demonstrate root allocation, unique resolution, contribution merging, collision rejection, current call-time authorization, complete descriptor inspection, and descriptor-typed invocation. The built-in native route migration is: @@ -103,13 +103,13 @@ The Worldspace contains only the operations registered from enabled Features and Filesystem scope, provider Workdir capability checks, Backend Workspace authority, Feature enablement, and input-specific restrictions stay in the original tool implementation and are checked again at invocation. `ask` permissions remain denied fail-closed because the runtime has no approval protocol. The gateway does not mint authority or expose authentication material. -Pre/post Engine history still records one bounded `WipCall` result, while the compatibility result retains the original `ToolOutput`, including attachments and normal Engine output pruning. WIP call audit records retain the route, operation, request identity, and terminal classification. +Pre/post Engine history still records one bounded `Call` result, while the compatibility result retains the original `ToolOutput`, including attachments and normal Engine output pruning. WIP call audit records retain the route, operation, request identity, and terminal classification. ## Client lifetime, restoration, and compaction The stateful `wip-client::Client` is owned by one Worker WIP runtime. Sessions are keyed by the canonical endpoint and an opaque security-context identity derived by the Host from Workspace, Worker, and session identity. Authentication credentials are not included in that identity and are never emitted to descriptors, tool output, or history. -Known Space and interface observations are therefore not shared across Workers, endpoints, or security contexts. A restored Worker creates a new runtime/client and must explore again. `WipDiscover(reset = true)` drops all observations before reconnect/authority-change exploration; `refresh = true` explicitly supersedes one cached observation. A normal compaction keeps the live runtime but the three gateway schemas are supplied again on every LLM request, including the instruction to begin discovery at `/`; the compacted transcript is not treated as cache authority. +Known Space and interface observations are therefore not shared across Workers, endpoints, or security contexts. A restored Worker creates a new runtime/client and must explore again. `Discover(reset = true)` drops all observations before reconnect/authority-change exploration; `refresh = true` explicitly supersedes one cached observation. A normal compaction keeps the live runtime but the three gateway schemas are supplied again on every LLM request, including the instruction to begin discovery at `/`; the compacted transcript is not treated as cache authority. Saved Session/history entries remain append-only evidence and are never rewritten or replayed as calls. In particular, historical `/features//...` text stays displayable but is not a current route alias. Passing such a legacy path to discovery or call returns `NotFound` with guidance to rediscover from `/`; the Host does not translate it, resolve it under another namespace, or expose old and new routes in parallel. From 3558e4aae160bf894b756f8060bb9d91c7d801a4 Mon Sep 17 00:00:00 2001 From: Hare Date: Wed, 7 Oct 2026 00:36:16 +0900 Subject: [PATCH 4/9] fix: diagnose nested partial Profile values in the editor (T-710) Resolve MR sequence 3 profile_nested_editor_diagnostics_missing with a separate, non-materializing authoring constraint pass. Preserve omission and the persisted evaluation schema, and map nested/imported failures to supplied virtual-source spans. Exercise actual Backend schema in native and regenerated WASM tests for empty/partial values, worker and feature types, unknown fields, imports and composition. Validation: root cargo check; config-source 38; config-source-wasm native/doc; unfiltered yoi-workspace-server 642 lib + 11 bin; worker-runtime 340 lib + 17 per binary; server-api --features typescript 77; focused Deno 39; deno task check; wasm32 release build, wasm-bindgen 0.2.117 + Binaryen 132 -Oz regeneration; cargo fmt/diff checks. Earlier checker failure did not recur: Reviewer independently passed full source suite and exact original baseline checker filter, recorded in MR sequence 3. --- crates/config-source/src/lib.rs | 195 ++++++++++++++++-- docs/design/profiles-manifests-prompts.md | 2 +- .../2026-10-06-review-finding-severity.md | 12 ++ .../config-schema/profile-authoring.dcdl | 5 +- .../generated/config_source_wasm_bg.wasm | Bin 1283030 -> 1289088 bytes .../test/config-source/wasm-parity.test.ts | 103 ++++++++- 6 files changed, 291 insertions(+), 26 deletions(-) create mode 100644 docs/report/2026-10-06-review-finding-severity.md diff --git a/crates/config-source/src/lib.rs b/crates/config-source/src/lib.rs index 2d6604d82..8f1239d3d 100644 --- a/crates/config-source/src/lib.rs +++ b/crates/config-source/src/lib.rs @@ -487,7 +487,8 @@ pub struct ConfigSchemaContribution { pub namespace: String, pub version: String, pub source: String, - // Optional editor-only schema; never used to validate or materialize config. + // Optional editor shape for completion and non-materializing constraint analysis; + // never used to materialize or validate the persisted config projection. #[serde(default, skip_serializing_if = "Option::is_none")] pub authoring_source: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -1250,17 +1251,83 @@ impl SnapshotEnvironment { "configured entrypoint is missing", )]; }; - let service = LanguageService::new(self); - service - .analyze( - entrypoint.as_str(), - entrypoint.as_str(), - source_override.unwrap_or(&entry.content), - ) + let source = source_override.unwrap_or(&entry.content); + let diagnostics: Vec<_> = LanguageService::new(self) + .analyze(entrypoint.as_str(), entrypoint.as_str(), source) .diagnostics .iter() .map(|diagnostic| project_diagnostic(&self.snapshot, entrypoint.clone(), diagnostic)) - .collect() + .collect(); + if !diagnostics.is_empty() + || !self.schema_bundle.as_ref().is_some_and(|bundle| { + bundle + .contributions + .iter() + .any(|entry| entry.authoring_source.is_some()) + }) + { + return diagnostics; + } + + // Check authoring constraints separately, without materializing the shape. + // Partial records may omit any recipe field: authoring metadata must neither + // make those fields required nor supply defaults to the saved projection. + let environment = ConfigAuthoringEnvironment { + environment: self, + schema_path: &[], + }; + let mut engine = match environment.create_engine() { + Ok(engine) => engine, + Err(diagnostic) => { + return vec![project_diagnostic( + &self.snapshot, + entrypoint.clone(), + &diagnostic, + )]; + } + }; + let result = engine + .add_root_source(entrypoint.as_str(), entrypoint.as_str(), source) + .and_then(|module| engine.eval_module(module)); + match result { + Ok(_) => Vec::new(), + Err(mut diagnostic) => { + // Constraint failures often point at the schema declaration. Anchor + // the editor diagnostic at the narrowest supplied value instead; + // label spans have no file field, so retain only that source's labels. + if !engine + .source_name(diagnostic.span.source) + .is_some_and(|name| { + VirtualPath::parse(name) + .is_ok_and(|path| self.snapshot.get(&path).is_some()) + }) + && let Some(span) = diagnostic + .labels + .iter() + .map(|label| label.span) + .filter(|span| { + engine.source_name(span.source).is_some_and(|name| { + VirtualPath::parse(name) + .is_ok_and(|path| self.snapshot.get(&path).is_some()) + }) + }) + .min_by_key(|span| span.end.saturating_sub(span.start)) + { + diagnostic.span = span; + } + diagnostic + .labels + .retain(|label| label.span.source == diagnostic.span.source); + let mut projected = project_engine_diagnostic( + &engine, + &self.snapshot, + entrypoint.clone(), + &diagnostic, + ); + projected.kind = diagnostic_kind(diagnostic.kind).to_string(); + vec![projected] + } + } } pub fn complete( @@ -1309,7 +1376,7 @@ impl SnapshotEnvironment { } member_source.push('.'); member_source.push_str(prefix); - let completion_environment = ConfigFieldCompletionEnvironment { + let completion_environment = ConfigAuthoringEnvironment { environment: self, schema_path: if has_array { schema_path } else { &[] }, }; @@ -1379,12 +1446,12 @@ impl HostEnvironment for &SnapshotEnvironment { // Decodal has no indexed member syntax. Resolve array ranges only in this // completion environment, then reuse the language service's member behavior. -struct ConfigFieldCompletionEnvironment<'a> { +struct ConfigAuthoringEnvironment<'a> { environment: &'a SnapshotEnvironment, schema_path: &'a [ConfigCompletionPathSegment], } -impl HostEnvironment for ConfigFieldCompletionEnvironment<'_> { +impl HostEnvironment for ConfigAuthoringEnvironment<'_> { type Loader = SnapshotImportLoader; fn create_loader(&self) -> Self::Loader { @@ -2488,6 +2555,110 @@ mod tests { assert_eq!(environment.evaluate_contract(&contract).unwrap(), evaluated); } + #[test] + fn actual_profile_schema_diagnoses_nested_values_without_materializing_omissions() { + let schema = WorkspaceConfigSchemaBundle::compose([ConfigSchemaContribution::new( + "builtin:profile", + "profile", + "2", + include_str!("../../../resources/config-schema/profile.dcdl"), + ) + .unwrap() + .with_authoring_source(include_str!( + "../../../resources/config-schema/profile-authoring.dcdl" + ))]) + .unwrap(); + let source = "{ profile = { entries = [{ selector = \"project:alpha\"; profile = {}; }]; }; } as WorkspaceConfigSchema"; + let snapshot = ConfigTreeSnapshot::from_entries( + 8, + [ + entry("main.dcdl", source), + entry("recipe.dcdl", "{ worker = { mode = 42; }; }"), + ], + ) + .unwrap(); + let environment = SnapshotEnvironment::new(snapshot).with_schema_bundle(schema.clone()); + let contract = ToolchainContract::with_schema_bundle(1, vec![path("main.dcdl")], 1, schema); + let before = environment.evaluate_contract(&contract).unwrap(); + for value in [ + "{}", + "{ worker = {}; }", + "{ worker = { mode = \"wip\"; }; }", + "{ feature = { task = {}; }; }", + ] { + let source = source.replace("profile = {};", &format!("profile = {value};")); + assert!( + environment + .analyze(&path("main.dcdl"), Some(&source)) + .is_empty(), + "{source}" + ); + } + for (value, field, expected_path) in [ + ("{ worker = 42; }", "worker", "main.dcdl"), + ("{ worker = { mode = 42; }; }", "mode", "main.dcdl"), + ( + "{ feature = { task = { enabled = 42; }; }; }", + "enabled", + "main.dcdl", + ), + ("{ worker = { typo = \"wip\"; }; }", "typo", "main.dcdl"), + ("import \"./recipe.dcdl\"", "mode", "recipe.dcdl"), + ( + "(import \"./recipe.dcdl\") // { worker = { mode = 42; }; }", + "mode", + "main.dcdl", + ), + ] { + let source = source.replace("profile = {};", &format!("profile = {value};")); + let diagnostics = environment.analyze(&path("main.dcdl"), Some(&source)); + let diagnostic = diagnostics + .first() + .unwrap_or_else(|| panic!("missing {field} diagnostic for {source}")); + assert!( + matches!( + diagnostic.kind.as_str(), + "constraint_violation" | "type_mismatch" + ), + "{diagnostic:?}" + ); + assert_eq!(diagnostic.path, path(expected_path), "{diagnostic:?}"); + assert_eq!(diagnostic.revision, 8); + let diagnostic_source = if expected_path == "main.dcdl" { + &source + } else { + "{ worker = { mode = 42; }; }" + }; + assert!( + diagnostic.span.end_byte > diagnostic.span.start_byte + && diagnostic.span.end_byte <= diagnostic_source.len() as u32, + "{diagnostic:?}" + ); + assert!( + diagnostic.message.contains(field) + || diagnostic + .labels + .iter() + .any(|label| label.message.contains(field)), + "{diagnostic:?}" + ); + assert!( + diagnostic + .labels + .iter() + .all(|label| label.span.end_byte <= diagnostic_source.len() as u32) + ); + } + // Analysis never changes the evaluation schema or transports authoring fields. + assert_eq!(environment.evaluate_contract(&contract).unwrap(), before); + assert_eq!( + before.projections[0] + .data_json + .pointer("/profile/entries/0/profile"), + Some(&serde_json::json!({})) + ); + } + #[test] fn completion_authoring_sources_compose_with_validation_source_fallbacks() { let schema = WorkspaceConfigSchemaBundle::compose([ diff --git a/docs/design/profiles-manifests-prompts.md b/docs/design/profiles-manifests-prompts.md index 8d5192f39..c75a33fe1 100644 --- a/docs/design/profiles-manifests-prompts.md +++ b/docs/design/profiles-manifests-prompts.md @@ -43,7 +43,7 @@ The imported file can contain `{ worker = { mode = "wip"; }; model = { ref = "co The former `entries = [{ selector = "project:name"; source = "profiles/name.dcdl"; }]` registration is no longer accepted. Migrate it explicitly to `profile = import "./profiles/name.dcdl";`, preserving the selector, label, description and default. For registrations in a nested config file, adjust the relative import to the original virtual-tree entry (for example `../profiles/name.dcdl`). Do not rename or recreate the Profile file merely for registration. Previously saved source-based revisions remain readable as config data and are not rewritten, deleted, or silently substituted with a builtin. Their Profile projection/launch reports `profile_source_registration_removed` with the replacement form; saves under the new schema reject the old field. Existing Workers continue using their saved Manifest while the author migrates and saves the config. -The schema contribution separates its materialized registration schema from an optional, fingerprint-bound `authoring_source`. The latter supplies editor field suggestions inside array elements and partial Profile records; it is never evaluated as defaults or substituted for the validation schema. Decodal structural diagnostics cover registration/value shape, and the shared Backend save boundary checks full Profile/Manifest semantics (including mode, forbidden runtime fields, duplicate selectors and default references). UI and WIP saves use that same boundary; invalid candidates leave the active config revision untouched. +The schema contribution separates its materialized registration schema from an optional, fingerprint-bound `authoring_source`. The latter supplies editor field suggestions inside array elements and partial Profile records, plus a separate constraint-only analysis pass for supplied nested values (including imported recipes); that pass never materializes the shape, makes omitted fields required, or supplies defaults to the saved projection. Editor diagnostics point to the supplied value's virtual file/span. The authoring shape is never substituted for the persisted evaluation schema. Decodal structural diagnostics cover registration and nested Profile field types; the shared Backend save boundary additionally checks full Profile/Manifest semantics (including mode values, forbidden runtime fields, duplicate selectors and default references). UI and WIP saves use that same boundary; invalid candidates leave the active config revision untouched. ## Manifests diff --git a/docs/report/2026-10-06-review-finding-severity.md b/docs/report/2026-10-06-review-finding-severity.md new file mode 100644 index 000000000..72277abf6 --- /dev/null +++ b/docs/report/2026-10-06-review-finding-severity.md @@ -0,0 +1,12 @@ +# Review finding severity の入力契約 + +T-710 の独立レビュー中、Reviewer が `ReviewMergeRequest` に finding severity `medium` を渡し、HTTP 422 で拒否された。Backend の許可値は `blocker` / `major` / `minor` / `note` で、最初の呼び出しでは verdict が記録されなかった。 + +親は committed session と MR thread を確認して未記録であることを確かめ、Reviewer の新しい turn で同じ capability・同じ MR/source に対して `major` を使った訂正を依頼した。訂正は成功し、MR sequence 3 に `request_changes` が記録された。別の保存経路や Ticket comment で verdict を代替しなかった。 + +## 改善案 + +- finding severity は公開 tool schema でも Backend と同じ enum として示し、受理可能な語彙を入力前に確認できるようにする。 +- capability を使う操作の検証エラーでは、イベントが未記録であることと、訂正した入力を再送できるかを明確に返す。Reviewer が「一回の呼び出し」と「一回の有効な verdict 記録」を混同し、未記録のまま止まる障壁を減らせる。 + +これは observed rejection と訂正成功に基づくフィードバックであり、Backend の review authority を迂回する提案ではない。 diff --git a/resources/config-schema/profile-authoring.dcdl b/resources/config-schema/profile-authoring.dcdl index 62317038e..da6fe1853 100644 --- a/resources/config-schema/profile-authoring.dcdl +++ b/resources/config-schema/profile-authoring.dcdl @@ -1,5 +1,6 @@ -# Completion-only shape of partial Profile values. This is never materialized or -# used as defaults: omitted fields remain omitted until Manifest resolution. +# Editor-only shape of partial Profile values, used for completion and constraint +# analysis without materialization. Never used as defaults: omitted fields remain +# omitted until Manifest resolution. let Flag = { enabled = Bool; }; Model = { diff --git a/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm_bg.wasm b/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm_bg.wasm index 8fc0b42457573bdb91793220a34f0aaabd88b0b1..f6cf6e8cf87e1475bb84c2482d3c5fbc5c7846d3 100644 GIT binary patch delta 194571 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 =?f^NQzqXFd-H96VH4V-I#kl<3ZcTix2RU3 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 Bd-DJQ diff --git a/web/workspace/test/config-source/wasm-parity.test.ts b/web/workspace/test/config-source/wasm-parity.test.ts index 637299db7..cd03c15d5 100644 --- a/web/workspace/test/config-source/wasm-parity.test.ts +++ b/web/workspace/test/config-source/wasm-parity.test.ts @@ -475,19 +475,100 @@ Deno.test("generated WASM authors value-based Profiles using the Backend schema ); assertEquals(result.from, cursor - token.length); } - const invalidContent = content.replace("profile = {};", "profile = 42;"); - const invalid = { + for ( + const value of [ + "{}", + "{ worker = {}; }", + '{ worker = { mode = "wip"; }; }', + "{ feature = { task = {}; }; }", + ] + ) { + const partialContent = content.replace( + "profile = {};", + `profile = ${value};`, + ); + assertEquals( + analyze_snapshot(tree, "main.dcdl", partialContent), + [], + value, + ); + } + const recipe = "{ worker = { mode = 42; }; }"; + const analysisTree = { ...tree, entries: { - "main.dcdl": { ...tree.entries["main.dcdl"], content: invalidContent }, + ...tree.entries, + "recipe.dcdl": { + path: "recipe.dcdl", + content_type: "decodal", + content: recipe, + content_digest: await digestText(recipe), + }, }, }; - const diagnostics = analyze_snapshot( - invalid, - "main.dcdl", - undefined, - ) as Array< - unknown - >; - assertEquals(diagnostics.length > 0, true); + for ( + const [value, field, expectedPath] of [ + ["42", "profile", "main.dcdl"], + ["{ worker = 42; }", "worker", "main.dcdl"], + ["{ worker = { mode = 42; }; }", "mode", "main.dcdl"], + ["{ feature = { task = { enabled = 42; }; }; }", "enabled", "main.dcdl"], + ['{ worker = { typo = "wip"; }; }', "typo", "main.dcdl"], + ['import "./recipe.dcdl"', "mode", "recipe.dcdl"], + [ + '(import "./recipe.dcdl") // { worker = { mode = 42; }; }', + "mode", + "main.dcdl", + ], + ] + ) { + const invalidContent = content.replace( + "profile = {};", + `profile = ${value};`, + ); + const diagnostics = analyze_snapshot( + analysisTree, + "main.dcdl", + invalidContent, + ) as Array<{ + path: string; + revision: number; + kind: string; + span: { start_byte: number; end_byte: number }; + message: string; + labels: Array< + { span: { start_byte: number; end_byte: number }; message: string } + >; + }>; + assertEquals(diagnostics.length > 0, true, value); + const diagnostic = diagnostics[0]; + assertEquals(diagnostic.path, expectedPath); + assertEquals(diagnostic.revision, tree.revision); + assertEquals( + ["constraint_violation", "type_mismatch"].includes(diagnostic.kind), + true, + ); + assertEquals( + diagnostic.message.includes(field) || + diagnostic.labels.some((label) => label.message.includes(field)), + true, + ); + // New authoring failures must be anchored in the supplied value, not the + // much longer schema source, including relative imported recipes. + if (value !== "42") { + const length = new TextEncoder().encode( + expectedPath === "main.dcdl" ? invalidContent : recipe, + ).length; + assertEquals( + diagnostic.span.end_byte > diagnostic.span.start_byte && + diagnostic.span.end_byte <= length, + true, + ); + assertEquals( + diagnostic.labels.every((label) => label.span.end_byte <= length), + true, + ); + } + } + // Neither completion nor diagnostic shape checking changes the saved value. + assertEquals(evaluate_snapshot(tree, valueContract), evaluated); }); From 24ae6288a5bc70b8c56037bcf87602c524128b16 Mon Sep 17 00:00:00 2001 From: Hare Date: Wed, 7 Oct 2026 01:52:06 +0900 Subject: [PATCH 5/9] feat(subjektiv): share domain and add explicit standalone Subject hosts --- Cargo.lock | 35 + Cargo.toml | 6 + crates/feature-storage/Cargo.toml | 14 + crates/feature-storage/README.md | 28 + crates/feature-storage/src/lib.rs | 1433 ++++ crates/manifest/src/builtin_profile.rs | 20 + crates/manifest/src/profile.rs | 23 +- crates/protocol/src/lib.rs | 26 + crates/session-store/src/fs_store.rs | 2 +- crates/session-store/src/history.rs | 2 + crates/session-store/src/public_index.rs | 170 +- crates/session-store/src/public_snapshot.rs | 3 +- crates/standalone/Cargo.toml | 6 + crates/standalone/src/host.rs | 229 +- crates/standalone/src/job_store.rs | 80 +- crates/standalone/src/jobs.rs | 123 +- crates/standalone/src/jobs/tests.rs | 345 + crates/standalone/src/launch.rs | 32 + crates/standalone/src/lib.rs | 2 + crates/standalone/src/store.rs | 34 + crates/standalone/src/subjektiv.rs | 1115 +++ crates/standalone/src/subjektiv/tests.rs | 130 + crates/standalone/src/subjektiv_sessions.rs | 1696 +++++ crates/standalone/tests/subjektiv.rs | 1430 ++++ crates/subjektiv/Cargo.toml | 21 + crates/subjektiv/README.md | 76 + crates/subjektiv/src/api.rs | 1316 ++++ crates/subjektiv/src/api/context.rs | 155 + crates/subjektiv/src/api/operation.rs | 255 + crates/subjektiv/src/api/tests.rs | 343 + crates/subjektiv/src/job.rs | 153 + crates/subjektiv/src/job/tests.rs | 183 + crates/subjektiv/src/lib.rs | 6182 ++++++++++++++++ crates/tui/src/backend_worker_picker.rs | 1 + crates/tui/src/console/mod.rs | 17 +- crates/tui/src/lib.rs | 5 +- crates/tui/src/standalone_spawn.rs | 144 +- crates/worker/src/bootstrap.rs | 48 + crates/worker/src/controller.rs | 35 +- crates/worker/src/feature/builtin/memory.rs | 290 +- .../src/feature/builtin/memory_lifecycle.rs | 68 +- .../feature/builtin/memory_staging_output.rs | 55 +- .../builtin/memory_surface_lifecycle.rs | 76 +- .../src/feature/builtin/subjektiv_memory.rs | 98 +- .../src/feature/builtin/subjektiv_session.rs | 55 +- crates/worker/src/job.rs | 65 +- crates/worker/src/job/result.rs | 86 +- crates/worker/src/job/tests.rs | 459 ++ crates/worker/src/lib.rs | 1 + crates/worker/src/session_capture.rs | 4 +- crates/worker/src/subjektiv.rs | 700 ++ crates/worker/src/worker.rs | 224 +- crates/workspace-server/Cargo.toml | 2 + .../workspace-server/src/feature_storage.rs | 1387 +--- crates/workspace-server/src/server.rs | 1764 +---- .../src/server/subjektiv_job.rs | 100 +- crates/workspace-server/src/subjektiv.rs | 6189 +---------------- crates/yoi/Cargo.toml | 1 + crates/yoi/src/cli_connection.rs | 5 +- crates/yoi/src/main.rs | 219 +- crates/yoi/src/subject_cli.rs | 184 + docs/design/standalone-subjektiv.md | 41 + docs/standalone.md | 52 + .../standalone-subjektiv-consolidation.dcdl | 8 + resources/profiles/standalone-subjektiv.dcdl | 7 + 65 files changed, 18273 insertions(+), 9785 deletions(-) create mode 100644 crates/feature-storage/Cargo.toml create mode 100644 crates/feature-storage/README.md create mode 100644 crates/feature-storage/src/lib.rs create mode 100644 crates/standalone/src/subjektiv.rs create mode 100644 crates/standalone/src/subjektiv/tests.rs create mode 100644 crates/standalone/src/subjektiv_sessions.rs create mode 100644 crates/standalone/tests/subjektiv.rs create mode 100644 crates/subjektiv/Cargo.toml create mode 100644 crates/subjektiv/README.md create mode 100644 crates/subjektiv/src/api.rs create mode 100644 crates/subjektiv/src/api/context.rs create mode 100644 crates/subjektiv/src/api/operation.rs create mode 100644 crates/subjektiv/src/api/tests.rs create mode 100644 crates/subjektiv/src/job.rs create mode 100644 crates/subjektiv/src/job/tests.rs create mode 100644 crates/subjektiv/src/lib.rs create mode 100644 crates/worker/src/subjektiv.rs create mode 100644 crates/yoi/src/subject_cli.rs create mode 100644 docs/design/standalone-subjektiv.md create mode 100644 docs/standalone.md create mode 100644 resources/profiles/standalone-subjektiv-consolidation.dcdl create mode 100644 resources/profiles/standalone-subjektiv.dcdl diff --git a/Cargo.lock b/Cargo.lock index 37b4fe8b2..b23475786 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1396,6 +1396,16 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "feature-storage" +version = "0.1.0" +dependencies = [ + "rusqlite", + "tempfile", + "thiserror 2.0.18", + "uuid", +] + [[package]] name = "ff" version = "0.13.1" @@ -4634,15 +4644,20 @@ dependencies = [ "agen", "async-trait", "client", + "feature-storage", "fs4", "futures", "job", + "libc", "manifest", + "memory", "protocol", "rusqlite", "serde", "serde_json", + "server-api", "session-store", + "subjektiv", "tempfile", "thiserror 2.0.18", "tokio", @@ -4719,6 +4734,23 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "subjektiv" +version = "0.1.0" +dependencies = [ + "chrono", + "feature-storage", + "manifest", + "memory", + "rusqlite", + "serde", + "serde_json", + "server-api", + "tempfile", + "thiserror 2.0.18", + "uuid", +] + [[package]] name = "subtle" version = "2.6.1" @@ -6583,6 +6615,7 @@ dependencies = [ "server-api", "session-analytics", "session-store", + "standalone", "tempfile", "ticket", "tokio", @@ -6627,6 +6660,7 @@ dependencies = [ "axum", "chrono", "config-source", + "feature-storage", "flow", "futures", "job", @@ -6646,6 +6680,7 @@ dependencies = [ "session-store", "sha2 0.11.0", "ssh-key", + "subjektiv", "tempfile", "thiserror 2.0.18", "ticket", diff --git a/Cargo.toml b/Cargo.toml index a43ed2096..a573ccb12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,8 @@ members = [ "crates/workdir", "crates/tui", "crates/memory", + "crates/feature-storage", + "crates/subjektiv", "crates/ticket", "crates/merge-request", "crates/project-record", @@ -66,6 +68,8 @@ default-members = [ "crates/workdir", "crates/tui", "crates/memory", + "crates/feature-storage", + "crates/subjektiv", "crates/ticket", "crates/merge-request", "crates/project-record", @@ -93,6 +97,8 @@ manifest = { path = "crates/manifest" } mcp = { path = "crates/mcp" } lint-common = { path = "crates/lint-common" } memory = { path = "crates/memory" } +feature-storage = { path = "crates/feature-storage" } +subjektiv = { path = "crates/subjektiv" } merge-request = { path = "crates/merge-request" } ticket = { path = "crates/ticket" } project-record = { path = "crates/project-record" } diff --git a/crates/feature-storage/Cargo.toml b/crates/feature-storage/Cargo.toml new file mode 100644 index 000000000..2702517ea --- /dev/null +++ b/crates/feature-storage/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "feature-storage" +version = "0.1.0" +edition.workspace = true +license.workspace = true +publish = false + +[dependencies] +rusqlite.workspace = true +thiserror.workspace = true +uuid = { workspace = true, features = ["v7"] } + +[dev-dependencies] +tempfile.workspace = true diff --git a/crates/feature-storage/README.md b/crates/feature-storage/README.md new file mode 100644 index 000000000..9defd6a75 --- /dev/null +++ b/crates/feature-storage/README.md @@ -0,0 +1,28 @@ +# feature-storage + +Neutral Host-owned SQLite lifecycle manager for trusted Features. Owns placement, +WAL/connection settings, transaction serialization, migrations, snapshots, +restore/delete fencing, and shutdown. Features receive a managed +`FeatureDatabase` after registration. Worker/model input never selects a database +path or receives SQL access. + +Use `FeatureStorage::new(trusted_root)`, `register(FeatureRegistration)`, and +`scope(host_issued_scope_id).open(®istration)`. `ScopedFeatureStorage` and +`FeatureDatabase::scope_id()` are neutral storage isolation APIs: a local Host's +persisted random scope does not create a Workspace record or imply Backend +membership. Existing placement `//features/.sqlite` and +snapshot `workspace=` keys remain unchanged for physical compatibility. + +`WorkspaceFeatureStorage`, `workspace()`, `workspace_id()`, and +`for_server_database()` are Backend compatibility aliases/helpers. They do not +introduce a Server dependency. The Host is responsible for state-root privacy, +symlink/path validation, and external execution leases before constructing this +trusted manager. + +`configure_connection(&rusqlite::Connection)` is reusable by other Host stores +(such as JobStore). It sets the five-second busy timeout, foreign keys ON, WAL, +and synchronous FULL before migrations/transactions. The Feature manager uses +this same helper; WAL lock-contention retries stay within the busy timeout. + +Dependencies: `rusqlite` (existing workspace backup support), `thiserror`, `uuid`; +tests use `tempfile`. diff --git a/crates/feature-storage/src/lib.rs b/crates/feature-storage/src/lib.rs new file mode 100644 index 000000000..877a20dce --- /dev/null +++ b/crates/feature-storage/src/lib.rs @@ -0,0 +1,1433 @@ +//! Host-owned SQLite storage for trusted Features. +//! +//! The Host owns database placement, connection configuration, migrations, and +//! operational lifecycle. A Feature owns its schema and only receives a scoped +//! [`FeatureDatabase`] after trusted registration; Worker/model inputs never +//! select a path or receive a SQL surface. + +use std::collections::{HashMap, HashSet}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex, Weak}; +use std::time::Duration; + +use rusqlite::backup::Backup; +use rusqlite::{Connection, ErrorCode, OpenFlags, Transaction, TransactionBehavior, params}; +use uuid::Uuid; + +const BUSY_TIMEOUT: Duration = Duration::from_secs(5); +const MIGRATION_TABLE: &str = "__yoi_feature_schema_migrations"; +const SNAPSHOT_FORMAT: &str = "yoi-feature-storage-v1"; +const SNAPSHOT_MANIFEST: &str = "manifest.txt"; +const MAX_IDENTIFIER_BYTES: usize = 128; + +pub type Result = std::result::Result; +pub type FeatureMigrationApply = for<'connection> fn(&Transaction<'connection>) -> Result<()>; + +#[derive(Debug, thiserror::Error)] +pub enum FeatureStorageError { + #[error("invalid {kind} storage identifier `{value}`")] + InvalidIdentifier { kind: &'static str, value: String }, + #[error("Feature `{0}` is not registered with this Host storage manager")] + FeatureNotRegistered(String), + #[error("Feature `{0}` is already registered with this Host storage manager")] + FeatureAlreadyRegistered(String), + #[error("invalid migration plan for Feature `{feature}`: {detail}")] + InvalidMigrationPlan { feature: String, detail: String }, + #[error("Feature registration belongs to a different Host storage manager")] + ForeignRegistration, + #[error("Feature storage is shutting down")] + ShuttingDown, + #[error("Feature storage for scope `{0}` is undergoing maintenance")] + ScopeMaintenance(String), + #[error("Feature database is closed")] + DatabaseClosed, + #[error( + "Feature `{feature}` database schema version {actual} is newer than supported version {supported}" + )] + UnsupportedSchema { + feature: String, + actual: u32, + supported: u32, + }, + #[error("Feature `{feature}` migration history is incompatible: {detail}")] + MigrationHistory { feature: String, detail: String }, + #[error("Feature `{feature}` migration {version} (`{name}`) failed: {detail}")] + MigrationFailed { + feature: String, + version: u32, + name: String, + detail: String, + }, + #[error("Feature storage snapshot is invalid: {0}")] + InvalidSnapshot(String), + #[error("Feature storage destination already exists: {0}")] + DestinationExists(String), + #[error("Feature storage operation failed: {0}")] + Operation(String), + #[error("feature storage I/O error: {0}")] + Io(#[from] std::io::Error), + #[error("feature storage SQLite error: {0}")] + Sqlite(#[from] rusqlite::Error), +} + +impl FeatureStorageError { + /// Lets a Feature reject a domain operation while retaining transaction rollback. + pub fn operation(message: impl Into) -> Self { + Self::Operation(message.into()) + } +} + +#[derive(Clone, Copy)] +pub struct FeatureMigration { + version: u32, + name: &'static str, + apply: FeatureMigrationApply, +} + +impl FeatureMigration { + pub const fn new(version: u32, name: &'static str, apply: FeatureMigrationApply) -> Self { + Self { + version, + name, + apply, + } + } + + pub fn version(&self) -> u32 { + self.version + } + + pub fn name(&self) -> &'static str { + self.name + } +} + +#[derive(Clone, Copy)] +pub struct FeatureRegistration { + feature_id: &'static str, + migrations: &'static [FeatureMigration], +} + +impl FeatureRegistration { + pub const fn new(feature_id: &'static str, migrations: &'static [FeatureMigration]) -> Self { + Self { + feature_id, + migrations, + } + } + + pub fn feature_id(&self) -> &'static str { + self.feature_id + } +} + +#[derive(Clone, Debug)] +pub struct RegisteredFeature { + manager_id: Uuid, + feature_id: String, +} + +impl RegisteredFeature { + pub fn feature_id(&self) -> &str { + &self.feature_id + } +} + +#[derive(Clone)] +pub struct FeatureStorage { + inner: Arc, +} + +struct FeatureStorageInner { + id: Uuid, + root: PathBuf, + state: Mutex, +} + +struct FeatureStorageState { + running: bool, + registrations: HashMap, + connections: HashMap<(String, String), Weak>, + maintenance: HashSet, + deletion_fences: HashSet, +} + +impl FeatureStorage { + /// Creates a lazy manager. No directories or databases are created until a + /// registered Feature is opened, restored, or explicitly backed up. + pub fn new(root: impl Into) -> Self { + Self { + inner: Arc::new(FeatureStorageInner { + id: Uuid::now_v7(), + root: root.into(), + state: Mutex::new(FeatureStorageState { + running: true, + registrations: HashMap::new(), + connections: HashMap::new(), + maintenance: HashSet::new(), + deletion_fences: HashSet::new(), + }), + }), + } + } + + pub fn for_server_database(database_path: impl AsRef) -> Result { + let database_path = database_path.as_ref(); + let parent = database_path.parent().ok_or_else(|| { + FeatureStorageError::Operation(format!( + "Server database path `{}` has no parent", + database_path.display() + )) + })?; + Ok(Self::new(parent.join("feature-storage"))) + } + + /// Binds trusted storage to a Host-issued opaque scope. This is a storage + /// isolation key, not a Workspace record or model authorization credential. + pub fn scope(&self, scope_id: &str) -> Result { + Ok(ScopedFeatureStorage { + manager: self.clone(), + scope_id: validate_identifier("Storage scope", scope_id)?.to_string(), + }) + } + + /// Backend compatibility alias; does not create or require a Workspace. + pub fn workspace(&self, scope_id: &str) -> Result { + self.scope(scope_id) + } + + pub fn register(&self, registration: FeatureRegistration) -> Result { + let feature_id = validate_identifier("Feature", registration.feature_id)?.to_string(); + validate_migrations(&feature_id, registration.migrations)?; + let mut state = self.lock_state()?; + ensure_running(&state)?; + if state.registrations.contains_key(&feature_id) { + return Err(FeatureStorageError::FeatureAlreadyRegistered(feature_id)); + } + state + .registrations + .insert(feature_id.clone(), registration.migrations); + Ok(RegisteredFeature { + manager_id: self.inner.id, + feature_id, + }) + } + + /// Closes every managed connection after any operation currently holding a + /// connection lock completes. New opens and operations then fail. + pub fn shutdown(&self) -> Result<()> { + let connections = { + let mut state = self.lock_state()?; + if !state.running { + return Ok(()); + } + state.running = false; + let connections = state + .connections + .values() + .filter_map(Weak::upgrade) + .collect::>(); + for connection in &connections { + connection.fence(); + } + state.connections.clear(); + connections + }; + for connection in connections { + connection.close()?; + } + Ok(()) + } + + fn open(&self, scope_id: &str, registration: &RegisteredFeature) -> Result { + if registration.manager_id != self.inner.id { + return Err(FeatureStorageError::ForeignRegistration); + } + let key = (scope_id.to_string(), registration.feature_id.clone()); + let mut state = self.lock_state()?; + ensure_running(&state)?; + if state.maintenance.contains(scope_id) || state.deletion_fences.contains(scope_id) { + return Err(FeatureStorageError::ScopeMaintenance(scope_id.to_string())); + } + let migrations = state + .registrations + .get(®istration.feature_id) + .copied() + .ok_or_else(|| { + FeatureStorageError::FeatureNotRegistered(registration.feature_id.clone()) + })?; + if let Some(connection) = state.connections.get(&key).and_then(Weak::upgrade) { + return Ok(FeatureDatabase { inner: connection }); + } + + let feature_dir = self.feature_dir(scope_id); + fs::create_dir_all(&feature_dir)?; + let path = feature_dir.join(format!("{}.sqlite", registration.feature_id)); + let connection = Connection::open_with_flags( + path, + OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE, + )?; + configure_connection(&connection)?; + migrate(&connection, ®istration.feature_id, migrations)?; + let connection = Arc::new(FeatureDatabaseInner { + scope_id: scope_id.to_string(), + feature_id: registration.feature_id.clone(), + accepting_operations: AtomicBool::new(true), + connection: Mutex::new(Some(connection)), + }); + state.connections.insert(key, Arc::downgrade(&connection)); + Ok(FeatureDatabase { inner: connection }) + } + + fn backup_scope(&self, scope_id: &str, destination: &Path) -> Result<()> { + // Keep manager lifecycle and deletion/restore mutually exclusive for the + // duration. Feature transactions use SQLite's online backup snapshot rules. + let state = self.lock_state()?; + ensure_running(&state)?; + if state.maintenance.contains(scope_id) || state.deletion_fences.contains(scope_id) { + return Err(FeatureStorageError::ScopeMaintenance(scope_id.to_string())); + } + if destination.exists() { + return Err(FeatureStorageError::DestinationExists( + destination.display().to_string(), + )); + } + let feature_ids = list_feature_databases(&self.feature_dir(scope_id))?; + let parent = destination.parent().ok_or_else(|| { + FeatureStorageError::Operation(format!( + "snapshot destination `{}` has no parent", + destination.display() + )) + })?; + fs::create_dir_all(parent)?; + let temporary = temporary_sibling(destination, "backup")?; + let result = (|| { + let temporary_features = temporary.join("features"); + fs::create_dir_all(&temporary_features)?; + for feature_id in &feature_ids { + backup_database( + &self + .feature_dir(scope_id) + .join(format!("{feature_id}.sqlite")), + &temporary_features.join(format!("{feature_id}.sqlite")), + )?; + } + fs::write( + temporary.join(SNAPSHOT_MANIFEST), + snapshot_manifest(scope_id, &feature_ids), + )?; + fs::rename(&temporary, destination)?; + Ok(()) + })(); + drop(state); + if result.is_err() { + let _ = fs::remove_dir_all(&temporary); + } + result + } + + fn restore_scope(&self, scope_id: &str, snapshot: &Path) -> Result<()> { + let feature_ids = read_snapshot_manifest(snapshot, scope_id)?; + self.begin_maintenance(scope_id, true)?; + let scope_dir = self.scope_dir(scope_id); + let temporary = self + .inner + .root + .join(format!(".restore-{scope_id}-{}", Uuid::now_v7())); + let result = (|| { + if scope_dir.exists() { + return Err(FeatureStorageError::DestinationExists( + scope_dir.display().to_string(), + )); + } + let temporary_features = temporary.join("features"); + fs::create_dir_all(&temporary_features)?; + for feature_id in &feature_ids { + backup_database( + &snapshot + .join("features") + .join(format!("{feature_id}.sqlite")), + &temporary_features.join(format!("{feature_id}.sqlite")), + )?; + } + fs::create_dir_all(&self.inner.root)?; + fs::rename(&temporary, &scope_dir)?; + Ok(()) + })(); + if result.is_err() { + let _ = fs::remove_dir_all(&temporary); + } + self.end_maintenance(scope_id)?; + result + } + + fn delete_scope(&self, scope_id: &str) -> Result<()> { + let connections = { + let mut state = self.lock_state()?; + ensure_running(&state)?; + if state.maintenance.contains(scope_id) { + return Err(FeatureStorageError::ScopeMaintenance(scope_id.to_string())); + } + // The fence intentionally survives physical removal. Workspace deletion + // is finalized in the Server DB immediately afterwards; keeping this + // admission fence prevents a cached API/background task from recreating + // Feature files in that gap. A retry is idempotently admitted. + state.deletion_fences.insert(scope_id.to_string()); + let connections = state + .connections + .iter() + .filter(|((candidate, _), _)| candidate == scope_id) + .filter_map(|(_, connection)| connection.upgrade()) + .collect::>(); + for connection in &connections { + connection.fence(); + } + state + .connections + .retain(|(candidate, _), _| candidate != scope_id); + connections + }; + for connection in connections { + connection.close()?; + } + match fs::remove_dir_all(self.scope_dir(scope_id)) { + Ok(()) => Ok(()), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(error) => Err(error.into()), + } + } + + fn begin_maintenance( + &self, + scope_id: &str, + require_no_connections: bool, + ) -> Result>> { + let mut state = self.lock_state()?; + ensure_running(&state)?; + if state.deletion_fences.contains(scope_id) + || !state.maintenance.insert(scope_id.to_string()) + { + return Err(FeatureStorageError::ScopeMaintenance(scope_id.to_string())); + } + let connections = state + .connections + .iter() + .filter(|((candidate, _), _)| candidate == scope_id) + .filter_map(|(_, connection)| connection.upgrade()) + .collect::>(); + if require_no_connections && !connections.is_empty() { + state.maintenance.remove(scope_id); + return Err(FeatureStorageError::Operation(format!( + "cannot restore storage scope `{scope_id}` while Feature databases are open" + ))); + } + state + .connections + .retain(|(candidate, _), _| candidate != scope_id); + Ok(connections) + } + + fn end_maintenance(&self, scope_id: &str) -> Result<()> { + let mut state = self.lock_state()?; + state.maintenance.remove(scope_id); + Ok(()) + } + + fn scope_dir(&self, scope_id: &str) -> PathBuf { + self.inner.root.join(scope_id) + } + + fn feature_dir(&self, scope_id: &str) -> PathBuf { + self.scope_dir(scope_id).join("features") + } + + fn lock_state(&self) -> Result> { + self.inner.state.lock().map_err(|_| { + FeatureStorageError::Operation("Feature storage state lock was poisoned".to_string()) + }) + } +} + +impl Drop for FeatureStorageInner { + fn drop(&mut self) { + if let Ok(mut state) = self.state.lock() { + state.running = false; + for connection in state.connections.values().filter_map(Weak::upgrade) { + let _ = connection.close(); + } + state.connections.clear(); + } + } +} + +#[derive(Clone)] +pub struct ScopedFeatureStorage { + manager: FeatureStorage, + scope_id: String, +} + +/// Compatibility name for Backend callers. New Hosts use [`ScopedFeatureStorage`]. +pub type WorkspaceFeatureStorage = ScopedFeatureStorage; + +impl ScopedFeatureStorage { + pub fn scope_id(&self) -> &str { + &self.scope_id + } + + /// Backend compatibility alias for the storage-scope identifier. + pub fn workspace_id(&self) -> &str { + &self.scope_id + } + + pub fn register(&self, registration: FeatureRegistration) -> Result { + self.manager.register(registration) + } + + pub fn open(&self, registration: &RegisteredFeature) -> Result { + self.manager.open(&self.scope_id, registration) + } + + /// Creates a consistent online SQLite snapshot of every Feature database, + /// including data for Features that are currently disabled/unregistered. + pub fn backup(&self, destination: impl AsRef) -> Result<()> { + self.manager + .backup_scope(&self.scope_id, destination.as_ref()) + } + + /// Restores a snapshot only into an absent scope storage directory and + /// only while no Feature database for this scope is open. + pub fn restore(&self, snapshot: impl AsRef) -> Result<()> { + self.manager + .restore_scope(&self.scope_id, snapshot.as_ref()) + } + + /// Closes this scope's Feature connections and removes only its scoped + /// storage tree. This is the operation used by Workspace deletion. + pub fn delete(&self) -> Result<()> { + self.manager.delete_scope(&self.scope_id) + } + + pub fn shutdown(&self) -> Result<()> { + self.manager.shutdown() + } +} + +#[derive(Clone)] +pub struct FeatureDatabase { + inner: Arc, +} + +struct FeatureDatabaseInner { + scope_id: String, + feature_id: String, + accepting_operations: AtomicBool, + connection: Mutex>, +} + +impl FeatureDatabase { + pub fn scope_id(&self) -> &str { + &self.inner.scope_id + } + + /// Backend compatibility alias for the storage-scope identifier. + pub fn workspace_id(&self) -> &str { + &self.inner.scope_id + } + + pub fn feature_id(&self) -> &str { + &self.inner.feature_id + } + + /// Executes a Feature-owned read/query while serializing use of the managed + /// connection. The connection never leaves trusted Host Rust code. + pub fn with_connection( + &self, + operation: impl FnOnce(&Connection) -> Result, + ) -> Result { + self.try_with_connection(operation) + } + + /// Variant of [`Self::with_connection`] that lets a typed Feature repository + /// retain its own domain errors without encoding them as storage strings. + pub fn try_with_connection( + &self, + operation: impl FnOnce(&Connection) -> std::result::Result, + ) -> std::result::Result + where + E: From, + { + self.inner.ensure_accepting().map_err(E::from)?; + let connection = self + .inner + .connection + .lock() + .map_err(|_| { + FeatureStorageError::Operation("Feature database lock was poisoned".to_string()) + }) + .map_err(E::from)?; + self.inner.ensure_accepting().map_err(E::from)?; + let connection = connection + .as_ref() + .ok_or(FeatureStorageError::DatabaseClosed) + .map_err(E::from)?; + operation(connection) + } + + /// Executes a Feature-owned multi-table operation atomically. Returning an + /// error rolls the transaction back; successful return commits it. + pub fn transaction( + &self, + operation: impl FnOnce(&Transaction<'_>) -> Result, + ) -> Result { + self.try_transaction(operation) + } + + /// Variant of [`Self::transaction`] that preserves a Feature repository's + /// typed domain errors while still rolling every failure back atomically. + pub fn try_transaction( + &self, + operation: impl FnOnce(&Transaction<'_>) -> std::result::Result, + ) -> std::result::Result + where + E: From, + { + self.inner.ensure_accepting().map_err(E::from)?; + let mut connection = self + .inner + .connection + .lock() + .map_err(|_| { + FeatureStorageError::Operation("Feature database lock was poisoned".to_string()) + }) + .map_err(E::from)?; + self.inner.ensure_accepting().map_err(E::from)?; + let connection = connection + .as_mut() + .ok_or(FeatureStorageError::DatabaseClosed) + .map_err(E::from)?; + let transaction = connection + .transaction_with_behavior(TransactionBehavior::Immediate) + .map_err(FeatureStorageError::from) + .map_err(E::from)?; + let value = operation(&transaction)?; + transaction + .commit() + .map_err(FeatureStorageError::from) + .map_err(E::from)?; + Ok(value) + } + + pub fn schema_version(&self) -> Result { + self.with_connection(|connection| { + connection + .query_row( + &format!("SELECT COALESCE(MAX(version), 0) FROM {MIGRATION_TABLE}"), + [], + |row| row.get(0), + ) + .map_err(Into::into) + }) + } +} + +impl FeatureDatabaseInner { + fn ensure_accepting(&self) -> Result<()> { + if self.accepting_operations.load(Ordering::Acquire) { + Ok(()) + } else { + Err(FeatureStorageError::DatabaseClosed) + } + } + + fn fence(&self) { + self.accepting_operations.store(false, Ordering::Release); + } + + fn close(&self) -> Result<()> { + self.fence(); + let mut connection = self.connection.lock().map_err(|_| { + FeatureStorageError::Operation("Feature database lock was poisoned".to_string()) + })?; + connection.take(); + Ok(()) + } +} + +fn validate_identifier<'a>(kind: &'static str, value: &'a str) -> Result<&'a str> { + let valid = !value.is_empty() + && value.len() <= MAX_IDENTIFIER_BYTES + && value.bytes().enumerate().all(|(index, byte)| match byte { + b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' => true, + b'-' | b'_' => index > 0, + _ => false, + }); + if !valid { + return Err(FeatureStorageError::InvalidIdentifier { + kind, + value: value.to_string(), + }); + } + Ok(value) +} + +fn validate_migrations(feature: &str, migrations: &[FeatureMigration]) -> Result<()> { + for (index, migration) in migrations.iter().enumerate() { + let expected = (index + 1) as u32; + if migration.version != expected { + return Err(FeatureStorageError::InvalidMigrationPlan { + feature: feature.to_string(), + detail: format!( + "versions must be contiguous from 1; expected {expected}, found {}", + migration.version + ), + }); + } + if migration.name.trim().is_empty() { + return Err(FeatureStorageError::InvalidMigrationPlan { + feature: feature.to_string(), + detail: format!("migration {expected} has an empty name"), + }); + } + } + Ok(()) +} + +fn ensure_running(state: &FeatureStorageState) -> Result<()> { + if state.running { + Ok(()) + } else { + Err(FeatureStorageError::ShuttingDown) + } +} + +/// Applies the common durable Host SQLite connection policy: five-second busy +/// timeout, foreign keys, WAL, and FULL synchronous commits. Call immediately +/// after opening a file-backed database, before transactions or migrations. +/// WAL initialization retries lock contention within the same bounded timeout. +/// This does not grant path authority or select a database schema. +pub fn configure_connection(connection: &Connection) -> Result<()> { + connection.busy_timeout(BUSY_TIMEOUT)?; + connection.execute_batch( + r#" +PRAGMA foreign_keys = ON; +PRAGMA synchronous = FULL; +PRAGMA busy_timeout = 5000; +"#, + )?; + // Changing journal mode takes a database-wide lock and, unlike ordinary + // statements, may report SQLITE_BUSY immediately while another opener is + // making the same transition. Bound retries by the same common timeout. + let attempts = (BUSY_TIMEOUT.as_millis() / 10) as usize; + for attempt in 0..=attempts { + match connection.query_row("PRAGMA journal_mode = WAL", [], |row| { + row.get::<_, String>(0) + }) { + Ok(mode) if mode.eq_ignore_ascii_case("wal") => return Ok(()), + Ok(mode) => { + return Err(FeatureStorageError::Operation(format!( + "SQLite refused WAL journal mode and selected `{mode}`" + ))); + } + Err(error) if sqlite_is_busy(&error) && attempt < attempts => { + std::thread::sleep(Duration::from_millis(10)); + } + Err(error) => return Err(error.into()), + } + } + unreachable!("bounded WAL retry loop always returns") +} + +fn sqlite_is_busy(error: &rusqlite::Error) -> bool { + matches!( + error, + rusqlite::Error::SqliteFailure(error, _) + if matches!(error.code, ErrorCode::DatabaseBusy | ErrorCode::DatabaseLocked) + ) +} + +fn migrate( + connection: &Connection, + feature_id: &str, + migrations: &[FeatureMigration], +) -> Result<()> { + let transaction = Transaction::new_unchecked(connection, TransactionBehavior::Immediate)?; + transaction.execute_batch(&format!( + r#" +CREATE TABLE IF NOT EXISTS {MIGRATION_TABLE} ( + version INTEGER PRIMARY KEY CHECK (version > 0), + name TEXT NOT NULL, + applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); +"# + ))?; + let mut statement = transaction.prepare(&format!( + "SELECT version, name FROM {MIGRATION_TABLE} ORDER BY version" + ))?; + let applied = statement + .query_map([], |row| { + Ok((row.get::<_, u32>(0)?, row.get::<_, String>(1)?)) + })? + .collect::, _>>()?; + drop(statement); + + let supported = migrations.last().map_or(0, |migration| migration.version); + if let Some((actual, _)) = applied.last() + && *actual > supported + { + return Err(FeatureStorageError::UnsupportedSchema { + feature: feature_id.to_string(), + actual: *actual, + supported, + }); + } + for (index, (version, name)) in applied.iter().enumerate() { + let Some(expected) = migrations.get(index) else { + return Err(FeatureStorageError::UnsupportedSchema { + feature: feature_id.to_string(), + actual: *version, + supported, + }); + }; + if *version != expected.version || name != expected.name { + return Err(FeatureStorageError::MigrationHistory { + feature: feature_id.to_string(), + detail: format!( + "stored migration {version} (`{name}`) does not match expected {} (`{}`)", + expected.version, expected.name + ), + }); + } + } + + for migration in migrations.iter().skip(applied.len()) { + if let Err(error) = (migration.apply)(&transaction) { + return Err(FeatureStorageError::MigrationFailed { + feature: feature_id.to_string(), + version: migration.version, + name: migration.name.to_string(), + detail: error.to_string(), + }); + } + transaction.execute( + &format!("INSERT INTO {MIGRATION_TABLE} (version, name) VALUES (?1, ?2)"), + params![migration.version, migration.name], + )?; + } + transaction.commit()?; + Ok(()) +} + +fn backup_database(source: &Path, destination: &Path) -> Result<()> { + if !source.is_file() { + return Err(FeatureStorageError::InvalidSnapshot(format!( + "Feature database `{}` is missing", + source.display() + ))); + } + let source = Connection::open_with_flags(source, OpenFlags::SQLITE_OPEN_READ_ONLY)?; + source.busy_timeout(BUSY_TIMEOUT)?; + verify_database(&source)?; + let mut destination = Connection::open(destination)?; + Backup::new(&source, &mut destination)?.run_to_completion( + 128, + Duration::from_millis(2), + None, + )?; + verify_database(&destination)?; + Ok(()) +} + +fn verify_database(connection: &Connection) -> Result<()> { + let integrity: String = connection.query_row("PRAGMA integrity_check", [], |row| row.get(0))?; + if integrity != "ok" { + return Err(FeatureStorageError::InvalidSnapshot(format!( + "SQLite integrity_check failed: {integrity}" + ))); + } + let mut foreign_keys = connection.prepare("PRAGMA foreign_key_check")?; + if foreign_keys.query([])?.next()?.is_some() { + return Err(FeatureStorageError::InvalidSnapshot( + "SQLite foreign_key_check reported a violation".to_string(), + )); + } + Ok(()) +} + +fn list_feature_databases(feature_dir: &Path) -> Result> { + if !feature_dir.exists() { + return Ok(Vec::new()); + } + let mut feature_ids = Vec::new(); + for entry in fs::read_dir(feature_dir)? { + let entry = entry?; + let file_type = entry.file_type()?; + if !file_type.is_file() { + continue; + } + let path = entry.path(); + if path.extension().and_then(|value| value.to_str()) != Some("sqlite") { + continue; + } + let feature_id = path + .file_stem() + .and_then(|value| value.to_str()) + .ok_or_else(|| { + FeatureStorageError::InvalidSnapshot(format!( + "Feature database filename `{}` is not UTF-8", + path.display() + )) + })?; + validate_identifier("Feature", feature_id)?; + feature_ids.push(feature_id.to_string()); + } + feature_ids.sort(); + Ok(feature_ids) +} + +fn snapshot_manifest(scope_id: &str, feature_ids: &[String]) -> String { + let mut manifest = format!("format={SNAPSHOT_FORMAT}\nworkspace={scope_id}\n"); + for feature_id in feature_ids { + manifest.push_str("feature="); + manifest.push_str(feature_id); + manifest.push('\n'); + } + manifest +} + +fn read_snapshot_manifest(snapshot: &Path, expected_workspace: &str) -> Result> { + let manifest = fs::read_to_string(snapshot.join(SNAPSHOT_MANIFEST)).map_err(|error| { + FeatureStorageError::InvalidSnapshot(format!("cannot read manifest: {error}")) + })?; + let mut format = None; + let mut workspace = None; + let mut feature_ids = Vec::new(); + for line in manifest.lines() { + if let Some(value) = line.strip_prefix("format=") { + format = Some(value); + } else if let Some(value) = line.strip_prefix("workspace=") { + workspace = Some(value); + } else if let Some(value) = line.strip_prefix("feature=") { + validate_identifier("Feature", value)?; + feature_ids.push(value.to_string()); + } else { + return Err(FeatureStorageError::InvalidSnapshot(format!( + "unknown manifest entry `{line}`" + ))); + } + } + if format != Some(SNAPSHOT_FORMAT) { + return Err(FeatureStorageError::InvalidSnapshot( + "unsupported snapshot format".to_string(), + )); + } + if workspace != Some(expected_workspace) { + return Err(FeatureStorageError::InvalidSnapshot(format!( + "snapshot storage scope `{}` does not match `{expected_workspace}`", + workspace.unwrap_or("") + ))); + } + feature_ids.sort(); + if feature_ids.windows(2).any(|ids| ids[0] == ids[1]) { + return Err(FeatureStorageError::InvalidSnapshot( + "duplicate Feature entry".to_string(), + )); + } + for feature_id in &feature_ids { + let database = snapshot + .join("features") + .join(format!("{feature_id}.sqlite")); + if !database.is_file() { + return Err(FeatureStorageError::InvalidSnapshot(format!( + "Feature database `{}` is missing", + database.display() + ))); + } + } + Ok(feature_ids) +} + +fn temporary_sibling(destination: &Path, operation: &str) -> Result { + let parent = destination.parent().ok_or_else(|| { + FeatureStorageError::Operation(format!( + "destination `{}` has no parent", + destination.display() + )) + })?; + let name = destination + .file_name() + .and_then(|value| value.to_str()) + .ok_or_else(|| { + FeatureStorageError::Operation(format!( + "destination `{}` has no UTF-8 filename", + destination.display() + )) + })?; + Ok(parent.join(format!(".{name}.{operation}-{}", Uuid::now_v7()))) +} + +#[cfg(test)] +mod tests { + use std::sync::{Arc, Barrier}; + use std::thread; + + use rusqlite::OptionalExtension; + + use super::*; + + #[derive(Debug)] + enum TestDomainError { + Storage, + Rejected, + } + + impl From for TestDomainError { + fn from(_error: FeatureStorageError) -> Self { + Self::Storage + } + } + + fn create_items(transaction: &Transaction<'_>) -> Result<()> { + transaction.execute_batch( + "CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT NOT NULL UNIQUE);", + )?; + Ok(()) + } + + fn add_audit(transaction: &Transaction<'_>) -> Result<()> { + transaction.execute_batch( + "CREATE TABLE audit (id INTEGER PRIMARY KEY, item_id INTEGER NOT NULL, note TEXT NOT NULL);", + )?; + Ok(()) + } + + const MIGRATIONS: &[FeatureMigration] = &[ + FeatureMigration::new(1, "create items", create_items), + FeatureMigration::new(2, "add audit", add_audit), + ]; + + fn failing_migration(transaction: &Transaction<'_>) -> Result<()> { + transaction.execute_batch("CREATE TABLE partial (id INTEGER PRIMARY KEY);")?; + Err(FeatureStorageError::operation( + "intentional migration failure", + )) + } + + const FAILING_MIGRATIONS: &[FeatureMigration] = &[ + FeatureMigration::new(1, "create items", create_items), + FeatureMigration::new(2, "fail", failing_migration), + ]; + + fn registration(feature_id: &'static str) -> FeatureRegistration { + FeatureRegistration::new(feature_id, MIGRATIONS) + } + + #[test] + fn reusable_connection_policy_and_manager_require_durable_commits() { + let temp = tempfile::tempdir().unwrap(); + let verify = |connection: &Connection| { + let integer = |pragma: &str| { + connection + .query_row(pragma, [], |row| row.get::<_, u32>(0)) + .unwrap() + }; + assert_eq!(integer("PRAGMA busy_timeout"), 5_000); + assert_eq!(integer("PRAGMA foreign_keys"), 1); + assert_eq!(integer("PRAGMA synchronous"), 2); // FULL + assert_eq!( + connection + .query_row("PRAGMA journal_mode", [], |row| row.get::<_, String>(0)) + .unwrap(), + "wal" + ); + }; + let connection = Connection::open(temp.path().join("job.sqlite")).unwrap(); + configure_connection(&connection).unwrap(); + verify(&connection); + let manager = FeatureStorage::new(temp.path().join("features")); + let registration = manager.register(registration("test-feature")).unwrap(); + let database = manager + .scope("random-local-scope") + .unwrap() + .open(®istration) + .unwrap(); + database + .with_connection(|connection| { + verify(connection); + Ok(()) + }) + .unwrap(); + } + + fn insert(database: &FeatureDatabase, value: &str) -> Result<()> { + database.transaction(|transaction| { + transaction.execute("INSERT INTO items (value) VALUES (?1)", [value])?; + Ok(()) + }) + } + + fn values(database: &FeatureDatabase) -> Result> { + database.with_connection(|connection| { + let mut statement = connection.prepare("SELECT value FROM items ORDER BY id")?; + statement + .query_map([], |row| row.get(0))? + .collect::, _>>() + .map_err(Into::into) + }) + } + + #[test] + fn open_migrate_transaction_rollback_and_reopen_persist() { + let temp = tempfile::tempdir().unwrap(); + let manager = FeatureStorage::new(temp.path().join("storage")); + let workspace = manager.workspace("workspace-a").unwrap(); + let feature = workspace.register(registration("subject-memory")).unwrap(); + let database = workspace.open(&feature).unwrap(); + assert_eq!(database.schema_version().unwrap(), 2); + insert(&database, "committed").unwrap(); + let error = database + .transaction::<()>(|transaction| { + transaction.execute("INSERT INTO items (value) VALUES ('rolled-back')", [])?; + Err(FeatureStorageError::operation("reject candidate")) + }) + .unwrap_err(); + assert!(error.to_string().contains("reject candidate")); + assert_eq!(values(&database).unwrap(), ["committed"]); + + manager.shutdown().unwrap(); + assert!(matches!( + values(&database).unwrap_err(), + FeatureStorageError::DatabaseClosed + )); + + let reopened = FeatureStorage::new(temp.path().join("storage")); + let workspace = reopened.workspace("workspace-a").unwrap(); + let feature = workspace.register(registration("subject-memory")).unwrap(); + let database = workspace.open(&feature).unwrap(); + assert_eq!(values(&database).unwrap(), ["committed"]); + assert_eq!(database.schema_version().unwrap(), 2); + } + + #[test] + fn typed_domain_error_rolls_back_transaction() { + let temp = tempfile::tempdir().unwrap(); + let manager = FeatureStorage::new(temp.path().join("storage")); + let workspace = manager.workspace("workspace-a").unwrap(); + let feature = workspace.register(registration("feature-a")).unwrap(); + let database = workspace.open(&feature).unwrap(); + + let result = database.try_transaction::<(), TestDomainError>(|transaction| { + transaction + .execute("INSERT INTO items (value) VALUES ('must-roll-back')", []) + .map_err(FeatureStorageError::from)?; + Err(TestDomainError::Rejected) + }); + assert!(matches!(result, Err(TestDomainError::Rejected))); + assert!(values(&database).unwrap().is_empty()); + } + + #[test] + fn workspace_and_feature_databases_are_isolated() { + let temp = tempfile::tempdir().unwrap(); + let manager = FeatureStorage::new(temp.path().join("storage")); + let feature_a = manager.register(registration("feature-a")).unwrap(); + let feature_b = manager.register(registration("feature-b")).unwrap(); + let workspace_a = manager.workspace("workspace-a").unwrap(); + let workspace_b = manager.workspace("workspace-b").unwrap(); + let a_a = workspace_a.open(&feature_a).unwrap(); + let a_b = workspace_a.open(&feature_b).unwrap(); + let b_a = workspace_b.open(&feature_a).unwrap(); + insert(&a_a, "workspace-a/feature-a").unwrap(); + insert(&a_b, "workspace-a/feature-b").unwrap(); + insert(&b_a, "workspace-b/feature-a").unwrap(); + assert_eq!(values(&a_a).unwrap(), ["workspace-a/feature-a"]); + assert_eq!(values(&a_b).unwrap(), ["workspace-a/feature-b"]); + assert_eq!(values(&b_a).unwrap(), ["workspace-b/feature-a"]); + assert_eq!(a_a.schema_version().unwrap(), 2); + assert_eq!(a_b.schema_version().unwrap(), 2); + assert_eq!(b_a.schema_version().unwrap(), 2); + } + + #[test] + fn concurrent_open_migrates_once_and_reuses_connection() { + let temp = tempfile::tempdir().unwrap(); + let manager = FeatureStorage::new(temp.path().join("storage")); + let workspace = manager.workspace("workspace-a").unwrap(); + let feature = workspace.register(registration("feature-a")).unwrap(); + let barrier = Arc::new(Barrier::new(3)); + let threads = (0..2) + .map(|_| { + let workspace = workspace.clone(); + let feature = feature.clone(); + let barrier = barrier.clone(); + thread::spawn(move || { + barrier.wait(); + workspace.open(&feature).unwrap() + }) + }) + .collect::>(); + barrier.wait(); + let mut databases = Vec::new(); + for thread in threads { + databases.push(thread.join().unwrap()); + } + assert!(Arc::ptr_eq(&databases[0].inner, &databases[1].inner)); + assert_eq!(databases[0].schema_version().unwrap(), 2); + } + + #[test] + fn independent_managers_serialize_migration_and_writes() { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("storage"); + let first_manager = FeatureStorage::new(&root); + let second_manager = FeatureStorage::new(&root); + let barrier = Arc::new(Barrier::new(3)); + let open = |manager: FeatureStorage, barrier: Arc| { + thread::spawn(move || { + let workspace = manager.workspace("workspace-a").unwrap(); + let feature = workspace.register(registration("feature-a")).unwrap(); + barrier.wait(); + let database = workspace.open(&feature).unwrap(); + (manager, database) + }) + }; + let first = open(first_manager, barrier.clone()); + let second = open(second_manager, barrier.clone()); + barrier.wait(); + let (first_manager, first_database) = first.join().unwrap(); + let (second_manager, second_database) = second.join().unwrap(); + assert_eq!(first_database.schema_version().unwrap(), 2); + assert_eq!(second_database.schema_version().unwrap(), 2); + + let writers = (0..16) + .map(|index| { + let database = if index % 2 == 0 { + first_database.clone() + } else { + second_database.clone() + }; + thread::spawn(move || insert(&database, &format!("manager-value-{index}"))) + }) + .collect::>(); + for writer in writers { + writer.join().unwrap().unwrap(); + } + assert_eq!(values(&first_database).unwrap().len(), 16); + drop((first_manager, second_manager)); + } + + #[test] + fn concurrent_writes_are_serialized_without_lost_updates() { + let temp = tempfile::tempdir().unwrap(); + let manager = FeatureStorage::new(temp.path().join("storage")); + let workspace = manager.workspace("workspace-a").unwrap(); + let feature = workspace.register(registration("feature-a")).unwrap(); + let database = workspace.open(&feature).unwrap(); + let threads = (0..8) + .map(|index| { + let database = database.clone(); + thread::spawn(move || insert(&database, &format!("value-{index}"))) + }) + .collect::>(); + for thread in threads { + thread.join().unwrap().unwrap(); + } + assert_eq!(values(&database).unwrap().len(), 8); + } + + #[test] + fn failed_migration_rolls_back_schema_and_version() { + let temp = tempfile::tempdir().unwrap(); + let manager = FeatureStorage::new(temp.path().join("storage")); + let workspace = manager.workspace("workspace-a").unwrap(); + let feature = workspace + .register(FeatureRegistration::new("broken", FAILING_MIGRATIONS)) + .unwrap(); + let error = workspace.open(&feature).err().unwrap(); + assert!(matches!(error, FeatureStorageError::MigrationFailed { .. })); + let path = temp + .path() + .join("storage/workspace-a/features/broken.sqlite"); + let connection = Connection::open(path).unwrap(); + let migration_table_exists: bool = connection + .query_row( + "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?1", + [MIGRATION_TABLE], + |_| Ok(true), + ) + .optional() + .unwrap() + .unwrap_or(false); + assert!(!migration_table_exists); + let partial_exists: bool = connection + .query_row( + "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'partial'", + [], + |_| Ok(true), + ) + .optional() + .unwrap() + .unwrap_or(false); + assert!(!partial_exists); + } + + #[test] + fn rejects_invalid_identifiers_and_newer_schema() { + let temp = tempfile::tempdir().unwrap(); + let manager = FeatureStorage::new(temp.path().join("storage")); + assert!(manager.workspace("../escape").is_err()); + assert!( + manager + .register(FeatureRegistration::new("bad/name", MIGRATIONS)) + .is_err() + ); + + let workspace = manager.workspace("workspace-a").unwrap(); + let feature = workspace.register(registration("feature-a")).unwrap(); + let database = workspace.open(&feature).unwrap(); + database + .with_connection(|connection| { + connection.execute( + &format!("INSERT INTO {MIGRATION_TABLE} (version, name) VALUES (99, 'future')"), + [], + )?; + Ok(()) + }) + .unwrap(); + drop(database); + manager.shutdown().unwrap(); + + let manager = FeatureStorage::new(temp.path().join("storage")); + let workspace = manager.workspace("workspace-a").unwrap(); + let feature = workspace.register(registration("feature-a")).unwrap(); + assert!(matches!( + workspace.open(&feature).err().unwrap(), + FeatureStorageError::UnsupportedSchema { actual: 99, .. } + )); + } + + #[test] + fn registration_is_lazy_and_disable_preserves_data() { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("storage"); + let manager = FeatureStorage::new(&root); + let workspace = manager.workspace("workspace-a").unwrap(); + let feature = workspace.register(registration("feature-a")).unwrap(); + assert!(!root.exists(), "registration must not create storage"); + let database = workspace.open(&feature).unwrap(); + insert(&database, "retained").unwrap(); + manager.shutdown().unwrap(); + + let disabled = FeatureStorage::new(&root); + assert!(root.join("workspace-a/features/feature-a.sqlite").is_file()); + disabled.shutdown().unwrap(); + + let enabled = FeatureStorage::new(&root); + let workspace = enabled.workspace("workspace-a").unwrap(); + let feature = workspace.register(registration("feature-a")).unwrap(); + assert_eq!( + values(&workspace.open(&feature).unwrap()).unwrap(), + ["retained"] + ); + } + + #[test] + fn backup_restore_is_consistent_and_includes_disabled_features() { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("storage"); + let snapshot = temp.path().join("snapshot"); + let manager = FeatureStorage::new(&root); + let workspace = manager.workspace("workspace-a").unwrap(); + let feature = workspace.register(registration("feature-a")).unwrap(); + let database = workspace.open(&feature).unwrap(); + insert(&database, "snapshot-value").unwrap(); + workspace.backup(&snapshot).unwrap(); + insert(&database, "after-snapshot").unwrap(); + workspace.delete().unwrap(); + assert!(matches!( + workspace.open(&feature).err().unwrap(), + FeatureStorageError::ScopeMaintenance(_) + )); + drop(database); + drop(workspace); + drop(manager); + + let manager = FeatureStorage::new(&root); + let workspace = manager.workspace("workspace-a").unwrap(); + workspace.restore(&snapshot).unwrap(); + let feature = workspace.register(registration("feature-a")).unwrap(); + assert_eq!( + values(&workspace.open(&feature).unwrap()).unwrap(), + ["snapshot-value"] + ); + } + + #[test] + fn workspace_delete_closes_handles_and_is_target_limited() { + let temp = tempfile::tempdir().unwrap(); + let manager = FeatureStorage::new(temp.path().join("storage")); + let feature = manager.register(registration("feature-a")).unwrap(); + let workspace_a = manager.workspace("workspace-a").unwrap(); + let workspace_b = manager.workspace("workspace-b").unwrap(); + let database_a = workspace_a.open(&feature).unwrap(); + let database_b = workspace_b.open(&feature).unwrap(); + insert(&database_a, "a").unwrap(); + insert(&database_b, "b").unwrap(); + workspace_a.delete().unwrap(); + assert!(matches!( + values(&database_a).unwrap_err(), + FeatureStorageError::DatabaseClosed + )); + assert_eq!(values(&database_b).unwrap(), ["b"]); + assert!(!temp.path().join("storage/workspace-a").exists()); + assert!(temp.path().join("storage/workspace-b").exists()); + } + + #[test] + fn shutdown_waits_for_operation_and_rejects_later_use() { + let temp = tempfile::tempdir().unwrap(); + let manager = FeatureStorage::new(temp.path().join("storage")); + let workspace = manager.workspace("workspace-a").unwrap(); + let feature = workspace.register(registration("feature-a")).unwrap(); + let database = workspace.open(&feature).unwrap(); + let entered = Arc::new(Barrier::new(2)); + let release = Arc::new(Barrier::new(2)); + let operation = { + let database = database.clone(); + let entered = entered.clone(); + let release = release.clone(); + thread::spawn(move || { + database.with_connection(|_| { + entered.wait(); + release.wait(); + Ok(()) + }) + }) + }; + entered.wait(); + let shutdown = { + let manager = manager.clone(); + thread::spawn(move || manager.shutdown()) + }; + while database.inner.accepting_operations.load(Ordering::Acquire) { + thread::yield_now(); + } + assert!(!shutdown.is_finished()); + assert!(matches!( + values(&database).unwrap_err(), + FeatureStorageError::DatabaseClosed + )); + release.wait(); + operation.join().unwrap().unwrap(); + shutdown.join().unwrap().unwrap(); + assert!(matches!( + values(&database).unwrap_err(), + FeatureStorageError::DatabaseClosed + )); + assert!(matches!( + workspace.open(&feature).err().unwrap(), + FeatureStorageError::ShuttingDown + )); + } +} diff --git a/crates/manifest/src/builtin_profile.rs b/crates/manifest/src/builtin_profile.rs index 4f44ca57a..17d63848b 100644 --- a/crates/manifest/src/builtin_profile.rs +++ b/crates/manifest/src/builtin_profile.rs @@ -59,6 +59,26 @@ pub const BUILTIN_PROFILE_RESOURCES: &[BuiltinProfileResource] = &[ description: "Standalone Yoi coding profile.", imports: DEFAULT_IMPORT, }, + BuiltinProfileResource { + selector: Some("builtin:standalone-subjektiv"), + path: "profiles/standalone-subjektiv.dcdl", + source: include_str!("../../../resources/profiles/standalone-subjektiv.dcdl"), + description: "Standalone with an explicitly connected local Subject.", + imports: &[BuiltinProfileImport { + specifier: "./standalone.dcdl", + resolved_path: "profiles/standalone.dcdl", + }], + }, + BuiltinProfileResource { + selector: Some("builtin:standalone-subjektiv-consolidation"), + path: "profiles/standalone-subjektiv-consolidation.dcdl", + source: include_str!("../../../resources/profiles/standalone-subjektiv-consolidation.dcdl"), + description: "Delegated local Subject Job policy.", + imports: &[BuiltinProfileImport { + specifier: "./job.dcdl", + resolved_path: "profiles/job.dcdl", + }], + }, BuiltinProfileResource { selector: Some("builtin:coder"), path: "profiles/coder.dcdl", diff --git a/crates/manifest/src/profile.rs b/crates/manifest/src/profile.rs index 4fc3ad04e..cf4c711ad 100644 --- a/crates/manifest/src/profile.rs +++ b/crates/manifest/src/profile.rs @@ -189,7 +189,9 @@ pub fn validate_profile_execution_target( if feature.memory.profile.enabled || feature.memory.profile.staging_tools { requirements.insert(WorkspaceAuthorityRequirement::Memory); } - if feature.subjektiv.profile.enabled { + // Subject policy is Host-neutral. Only an already bound Backend snapshot + // requires Workspace authority; a local Host supplies its own live capability. + if feature.subjektiv.workspace_settings.is_some() { requirements.insert(WorkspaceAuthorityRequirement::Subjektiv); } if feature.merge_request.show @@ -1257,11 +1259,21 @@ mod tests { ) .unwrap(); assert_eq!(resolved.manifest.worker.name, "role-worker"); - if entry.name == "job" { + if matches!( + entry.name.as_str(), + "job" | "standalone-subjektiv-consolidation" + ) { assert!(resolved.manifest.compaction.is_none()); assert!(resolved.manifest.scope.allow.is_empty()); assert!(resolved.manifest.delegation_scope.allow.is_empty()); - assert_eq!(resolved.manifest.engine.instruction, "internal.job_system"); + assert_eq!( + resolved.manifest.engine.instruction, + if entry.name == "job" { + "internal.job_system" + } else { + "internal.subjektiv_memory_consolidation_system" + } + ); continue; } let compaction = resolved.manifest.compaction.as_ref().unwrap(); @@ -1669,7 +1681,10 @@ mod tests { panic!("unexpected error: {error}"); }; assert_eq!(target, ProfileExecutionTarget::Standalone); - assert!(requirements.contains(&WorkspaceAuthorityRequirement::Subjektiv)); + assert!( + !requirements.contains(&WorkspaceAuthorityRequirement::Subjektiv), + "subject policy alone is no longer Backend authority" + ); assert!(!requirements.contains(&WorkspaceAuthorityRequirement::Memory)); assert!(requirements.contains(&WorkspaceAuthorityRequirement::MergeRequest)); assert!(requirements.contains(&WorkspaceAuthorityRequirement::Ticket)); diff --git a/crates/protocol/src/lib.rs b/crates/protocol/src/lib.rs index f7dcd9160..2c42b5161 100644 --- a/crates/protocol/src/lib.rs +++ b/crates/protocol/src/lib.rs @@ -50,6 +50,9 @@ pub enum AuthenticatedInputSource { /// Assigned whenever a serialized tracked method crosses an untrusted /// protocol boundary. Receivers must handle it exactly like public input. UntrustedWire, + /// Stamped only by the operator-owned standalone in-process transport. + /// This is not a Backend Account or authority accepted from wire fields. + LocalOperator, Account { account_id: String, }, @@ -86,6 +89,7 @@ impl AuthenticatedInputSource { pub fn namespace(&self) -> String { match self { Self::UntrustedWire => "untrusted-wire".into(), + Self::LocalOperator => "local-operator".into(), Self::Account { account_id } => format!("account:{account_id}"), Self::Worker { runtime_id, @@ -2175,6 +2179,28 @@ mod tests { ); } + #[test] + fn local_operator_source_cannot_be_granted_by_serialized_input() { + let method = Method::SubmitTracked { + input: vec![Segment::text("forged local operator")], + submission_request_id: "local-forged".into(), + source: AuthenticatedInputSource::LocalOperator, + }; + let decoded: Method = + serde_json::from_str(&serde_json::to_string(&method).unwrap()).unwrap(); + assert!(matches!( + decoded, + Method::SubmitTracked { + source: AuthenticatedInputSource::UntrustedWire, + .. + } + )); + assert_ne!( + AuthenticatedInputSource::LocalOperator.namespace(), + AuthenticatedInputSource::UntrustedWire.namespace() + ); + } + #[test] fn notify_rejects_typed_intent_instead_of_silently_discarding_it() { for method in ["notify", "notify_tracked"] { diff --git a/crates/session-store/src/fs_store.rs b/crates/session-store/src/fs_store.rs index e9ca34761..12ab5bcdb 100644 --- a/crates/session-store/src/fs_store.rs +++ b/crates/session-store/src/fs_store.rs @@ -211,7 +211,7 @@ impl FsStore { } } - fn parse_jsonl( + pub(crate) fn parse_jsonl( content: &[u8], session_id: SessionId, segment_id: SegmentId, diff --git a/crates/session-store/src/history.rs b/crates/session-store/src/history.rs index 4ed2bd2bb..d93bead53 100644 --- a/crates/session-store/src/history.rs +++ b/crates/session-store/src/history.rs @@ -40,6 +40,8 @@ pub struct LoggedWorkerSubject { #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum LoggedSessionHistoryOrigin { + /// Human input verified by the standalone Host, without a Backend Account. + LocalHumanInput, HumanInput { account_id: String, }, diff --git a/crates/session-store/src/public_index.rs b/crates/session-store/src/public_index.rs index 1e1406d16..1a8392c7e 100644 --- a/crates/session-store/src/public_index.rs +++ b/crates/session-store/src/public_index.rs @@ -208,6 +208,172 @@ pub fn read_session_public_index( let segment_ids = store .list_segments_read_only(session_id) .map_err(map_store_error)?; + project_public_index_records(session_id, segment_ids, limits, |segment_id, remaining| { + store + .read_all_read_only_bounded(session_id, segment_id, remaining) + .map_err(map_store_error) + }) +} + +/// Strict committed public projection for the standalone FsStore layout. +/// Uses the same lineage, commit filtering and entry projection as retained +/// WorkerSessionStore observation. No writes, migrations, trace/artifact reads, +/// access-time updates or directory creation occur here. +pub fn read_fs_session_public_index( + sessions_root: &Path, + session_id: SessionId, + limits: SessionPublicIndexLimits, +) -> Result { + read_fs_session_evidence_index(sessions_root, session_id, limits).map(|value| value.index) +} + +/// Producer-compatible numeric hints for stable public evidence references. +/// Both committed annotated-history and filtered public-snapshot capture are +/// valid producers. No hidden history content is exposed by this metadata. +pub struct FsSessionEvidenceIndex { + pub index: SessionPublicIndex, + pub ranges: HashMap<(String, String), Vec<[u64; 2]>>, + pub has_open_run: bool, +} +pub fn read_fs_session_evidence_index( + sessions_root: &Path, + session_id: SessionId, + limits: SessionPublicIndexLimits, +) -> Result { + if limits.max_bytes == 0 || limits.max_segments == 0 || limits.max_entries == 0 { + return Err(SessionPublicIndexReadError::ResourceLimit); + } + let session_dir = sessions_root.join(session_id.to_string()); + // IDs are typed and every storage component is observationally validated; + // a known Session ID never authorizes following arbitrary local symlinks. + let mut prefix = std::path::PathBuf::new(); + for part in session_dir.components() { + prefix.push(part); + let metadata = + std::fs::symlink_metadata(&prefix).map_err(|_| SessionPublicIndexReadError::Missing)?; + if metadata.file_type().is_symlink() { + return Err(SessionPublicIndexReadError::Storage); + } + } + let mut segment_ids = Vec::new(); + for entry in + std::fs::read_dir(&session_dir).map_err(|_| SessionPublicIndexReadError::Missing)? + { + let entry = entry.map_err(|_| SessionPublicIndexReadError::Storage)?; + let path = entry.path(); + if path.extension().and_then(|v| v.to_str()) != Some("jsonl") { + continue; + } + let Some(id) = path + .file_stem() + .and_then(|v| v.to_str()) + .and_then(|v| v.parse::().ok()) + else { + continue; + }; + if !entry + .file_type() + .map_err(|_| SessionPublicIndexReadError::Storage)? + .is_file() + { + return Err(SessionPublicIndexReadError::Storage); + } + segment_ids.push(id); + if segment_ids.len() > limits.max_segments { + return Err(SessionPublicIndexReadError::ResourceLimit); + } + } + segment_ids.sort(); + if segment_ids.is_empty() { + return Err(SessionPublicIndexReadError::Missing); + } + let mut ranges = HashMap::<(String, String), Vec<[u64; 2]>>::new(); + let mut has_open_run = false; + let index = + project_public_index_records(session_id, segment_ids, limits, |segment_id, remaining| { + let path = session_dir.join(format!("{segment_id}.jsonl")); + let bytes = crate::read_without_atime_bounded(&path, remaining) + .map_err(|_| SessionPublicIndexReadError::Storage)?; + if bytes.len() as u64 > remaining { + return Err(SessionPublicIndexReadError::ResourceLimit); + } + let entries = crate::FsStore::parse_jsonl(&bytes, session_id, segment_id) + .map_err(map_store_error)?; + let mut open = false; + for entry in &entries { + match entry { + LogEntry::Invoke { .. } | LogEntry::RunResumed { .. } => open = true, + LogEntry::RunCompleted { .. } + | LogEntry::RunYielded { .. } + | LogEntry::RunCancelled { .. } + | LogEntry::RunErrored { .. } + | LogEntry::PausedTurnAbandoned { .. } => open = false, + _ => {} + } + } + has_open_run |= open; + let committed = committed_conversation_records(&entries); + let state = crate::segment_log::collect_state(&committed); + for (position, entry) in state.annotated_history.iter().enumerate() { + ranges + .entry(( + segment_id.to_string(), + format!("E{}", entry.metadata.entry_id.0), + )) + .or_default() + .push([position as u64, position as u64]); + } + let snapshot = crate::public_snapshot::project_session_snapshot_for_segment( + session_id, + Some(segment_id), + &committed, + ); + let mut position = 0_u64; + for entry in snapshot.entries { + if matches!( + entry.data, + SessionSnapshotEntryData::UserInput { .. } + | SessionSnapshotEntryData::Message { .. } + | SessionSnapshotEntryData::ToolCall { .. } + | SessionSnapshotEntryData::ToolResult { .. } + ) { + ranges + .entry((segment_id.to_string(), format!("E{}", entry.entry_id))) + .or_default() + .push([position, position]); + position += 1; + } + } + Ok((entries, bytes.len() as u64)) + })?; + // Intersect with the strict public projection, never turn knowledge of a + // hidden/reasoning history identity into a public evidence reference. + let public = index + .segments + .iter() + .flat_map(|seg| { + seg.entries + .iter() + .map(|entry| (seg.segment_id.clone(), entry.entry_ref.clone())) + }) + .collect::>(); + ranges.retain(|key, _| public.contains(key)); + Ok(FsSessionEvidenceIndex { + index, + ranges, + has_open_run, + }) +} + +fn project_public_index_records( + session_id: SessionId, + segment_ids: Vec, + limits: SessionPublicIndexLimits, + mut read_segment: impl FnMut( + SegmentId, + u64, + ) -> Result<(Vec, u64), SessionPublicIndexReadError>, +) -> Result { if segment_ids.len() > limits.max_segments { return Err(SessionPublicIndexReadError::ResourceLimit); } @@ -224,9 +390,7 @@ pub fn read_session_public_index( .max_bytes .checked_sub(scanned_bytes) .ok_or(SessionPublicIndexReadError::ResourceLimit)?; - let (entries, bytes) = store - .read_all_read_only_bounded(session_id, segment_id, remaining) - .map_err(map_store_error)?; + let (entries, bytes) = read_segment(segment_id, remaining)?; scanned_bytes = scanned_bytes .checked_add(bytes) .ok_or(SessionPublicIndexReadError::ResourceLimit)?; diff --git a/crates/session-store/src/public_snapshot.rs b/crates/session-store/src/public_snapshot.rs index 3f7f8e53c..6c305793a 100644 --- a/crates/session-store/src/public_snapshot.rs +++ b/crates/session-store/src/public_snapshot.rs @@ -1654,7 +1654,8 @@ fn legacy_entry_id( fn provenance(origin: &LoggedSessionHistoryOrigin) -> SessionEntryProvenance { match origin { - LoggedSessionHistoryOrigin::HumanInput { .. } => SessionEntryProvenance::HumanInput, + LoggedSessionHistoryOrigin::HumanInput { .. } + | LoggedSessionHistoryOrigin::LocalHumanInput => SessionEntryProvenance::HumanInput, LoggedSessionHistoryOrigin::WorkerInput { .. } => SessionEntryProvenance::WorkerInput, LoggedSessionHistoryOrigin::FlowInstruction { .. } => { SessionEntryProvenance::FlowInstruction diff --git a/crates/standalone/Cargo.toml b/crates/standalone/Cargo.toml index a8b078496..bf05d8375 100644 --- a/crates/standalone/Cargo.toml +++ b/crates/standalone/Cargo.toml @@ -9,6 +9,12 @@ license.workspace = true agen.workspace = true client.workspace = true fs4.workspace = true +feature-storage.workspace = true +subjektiv.workspace = true +memory.workspace = true +server-api.workspace = true +libc.workspace = true +async-trait.workspace = true manifest.workspace = true job.workspace = true rusqlite.workspace = true diff --git a/crates/standalone/src/host.rs b/crates/standalone/src/host.rs index 17c77ac35..09cd555e5 100644 --- a/crates/standalone/src/host.rs +++ b/crates/standalone/src/host.rs @@ -44,12 +44,15 @@ pub struct StandaloneHost { record: StandaloneWorkerRecord, lease: Option, jobs: crate::jobs::StandaloneJobs, + subject: Option, } #[derive(Debug, Clone, PartialEq, Eq, Error)] pub enum StandaloneStartupError { #[error("the standalone state store could not be opened or validated")] StateStore, + #[error("local Subject connection rejected: {0}")] + Subject(String), #[error("the standalone Worker is already active")] WorkerActive, #[error("the standalone Worker lease cannot be observed safely; recovery is rejected")] @@ -107,16 +110,53 @@ impl StandaloneHost { launch: ResolvedStandaloneLaunch, model_client: Option>, ) -> Result { + if launch.subject_id.is_some() + && (!launch.profile.manifest.feature.subjektiv.profile.enabled + || launch + .profile + .manifest + .feature + .subjektiv + .profile + .consolidation_tools + || launch + .profile + .manifest + .feature + .subjektiv + .workspace_settings + .is_some()) + { + return Err(StandaloneStartupError::Subject( + "incompatible local Subject policy".into(), + )); + } let store = StandaloneWorkerStore::open(&launch.state_dir).map_err(classify_store_startup_error)?; let allocation = store .allocate(&launch.cwd, StaleLeasePolicy::Reject) .map_err(classify_store_startup_error)?; let worker_id = allocation.worker_id(); - let jobs = match crate::jobs::StandaloneJobs::open( - &store.jobs_path(worker_id), - launch.cwd.clone(), - ) { + let mut subject = match launch + .subject_id + .as_deref() + .map(|id| { + crate::subjektiv::StandaloneSubjects::open_existing(&launch.state_dir)? + .connect(id, worker_id) + }) + .transpose() + { + Ok(subject) => subject, + Err(error) => { + let _ = store.abandon_allocation(allocation); + return Err(StandaloneStartupError::Subject(error.to_string())); + } + }; + let jobs_path = subject + .as_ref() + .map(|s| s.jobs_path.clone()) + .unwrap_or_else(|| store.jobs_path(worker_id)); + let jobs = match crate::jobs::StandaloneJobs::open(&jobs_path, launch.cwd.clone()) { Ok(jobs) => jobs, Err(_) => { let _ = store.abandon_allocation(allocation); @@ -124,6 +164,12 @@ impl StandaloneHost { } }; + if let Some(client) = &model_client { + jobs.bind_model_client(client.clone_boxed()); + } + if let Some(subject) = &subject { + subject.host.bind_jobs(jobs.clone())?; + } // WorkerId is the stable identity. The current Worker store remains // name-keyed, so keep its derived storage key separate from the // user-facing profile name. @@ -156,9 +202,15 @@ impl StandaloneHost { }, WorkerControllerTransport::InProcess, ); + if let Some(subject) = &subject { + bootstrap = bootstrap.with_subjektiv_host(subject.host.clone()); + } if let Some(model_client) = model_client { bootstrap = bootstrap.with_model_client(model_client); } + if let Some(subject) = &mut subject { + subject.arm(); + } let started = match bootstrap.start().await { Ok(started) => started, Err(error) => { @@ -174,12 +226,13 @@ impl StandaloneHost { return Err(error); } }; - let record = match store.commit_created( + let record = match store.commit_created_connected( &allocation, manifest, storage_key, active.session_id, active.segment_id, + subject.as_ref().map(|s| s.binding.clone()), ) { Ok(record) => record, Err(_) => { @@ -195,6 +248,7 @@ impl StandaloneHost { record, allocation.into_lease(), jobs, + subject, )) } @@ -230,11 +284,50 @@ impl StandaloneHost { let lease = store .acquire_lease(worker_id, StaleLeasePolicy::Recover) .map_err(classify_store_startup_error)?; - let jobs = crate::jobs::StandaloneJobs::open( - &store.jobs_path(worker_id), - record.cwd.canonical_path.clone(), - ) - .map_err(|_| StandaloneStartupError::StateStore)?; + let mut subject = record + .subject + .as_ref() + .map(|binding| { + if !record.manifest.feature.subjektiv.profile.enabled + || record + .manifest + .feature + .subjektiv + .profile + .consolidation_tools + || record + .manifest + .feature + .subjektiv + .workspace_settings + .is_some() + { + return Err(crate::subjektiv::SubjectError::InvalidScope( + "persisted Subject policy changed".into(), + )); + } + let catalog = crate::subjektiv::StandaloneSubjects::open_existing(store.root())?; + if catalog.scope_id() != binding.scope_id { + return Err(crate::subjektiv::SubjectError::InvalidScope( + "persisted local storage scope changed".into(), + )); + } + catalog.connect(&binding.subject_id, worker_id) + }) + .transpose() + .map_err(|e| StandaloneStartupError::Subject(e.to_string()))?; + let jobs_path = subject + .as_ref() + .map(|s| s.jobs_path.clone()) + .unwrap_or_else(|| store.jobs_path(worker_id)); + let jobs = crate::jobs::StandaloneJobs::open(&jobs_path, record.cwd.canonical_path.clone()) + .map_err(|_| StandaloneStartupError::StateStore)?; + if let Some(client) = &model_client { + jobs.bind_model_client(client.clone_boxed()); + } + if let Some(subject) = &subject { + subject.host.bind_jobs(jobs.clone())?; + } let (backing_store, worker_store) = backing_store(&store, worker_id)?; let storage_key = record.storage_key.clone(); let mut manifest = record.manifest.clone(); @@ -259,6 +352,9 @@ impl StandaloneHost { }, WorkerControllerTransport::InProcess, ); + if let Some(subject) = &subject { + bootstrap = bootstrap.with_subjektiv_host(subject.host.clone()); + } if let Some(model_client) = model_client { bootstrap = bootstrap.with_model_client(model_client); } @@ -266,6 +362,9 @@ impl StandaloneHost { .prepare_restored(&storage_key) .await .map_err(classify_startup_error)?; + if let Some(subject) = &mut subject { + subject.arm(); + } let started = prepared.start().await.map_err(classify_startup_error)?; let active = match active_pointer(&worker_store, &storage_key) { Ok(active) => active, @@ -290,6 +389,7 @@ impl StandaloneHost { record, lease, jobs, + subject, )) } @@ -300,7 +400,11 @@ impl StandaloneHost { record: StandaloneWorkerRecord, lease: StandaloneWorkerLease, jobs: crate::jobs::StandaloneJobs, + subject: Option, ) -> Self { + if let Some(connection) = &subject { + connection.host.recover_jobs(); + } Self { handle: started.handle, shutdown: Some(started.shutdown), @@ -310,6 +414,7 @@ impl StandaloneHost { record, lease: Some(lease), jobs, + subject, } } @@ -388,6 +493,12 @@ impl StandaloneHost { self.retain_lease(); return Err(StandaloneShutdownError::StateStore); } + if let Some(subject) = &mut self.subject { + if subject.close().is_err() { + self.retain_lease(); + return Err(StandaloneShutdownError::StateStore); + } + } if let Some(lease) = self.lease.take() { lease .release() @@ -397,6 +508,9 @@ impl StandaloneHost { } fn retain_lease(&mut self) { + if let Some(subject) = &mut self.subject { + subject.retain(); + } if let Some(lease) = self.lease.take() { lease.retain(); } @@ -413,6 +527,51 @@ impl Drop for StandaloneHost { } } +// The in-process channel is owned by the local operator's Host, not an +// arbitrary socket or Backend account. decode_method discards wire provenance; +// only this boundary can stamp accepted local human input. +fn authorize_local_input(method: protocol::Method) -> protocol::Method { + use protocol::{AuthenticatedInputSource, Method}; + match method { + Method::Submit { + submission_request_id, + input, + } + | Method::SubmitTracked { + submission_request_id, + input, + .. + } => Method::SubmitTracked { + submission_request_id, + input, + source: AuthenticatedInputSource::LocalOperator, + }, + Method::SubmitIfIdle { + submission_request_id, + input, + .. + } => Method::SubmitIfIdle { + submission_request_id, + input, + source: AuthenticatedInputSource::LocalOperator, + }, + Method::Notify { + notification_request_id, + message, + } + | Method::NotifyTracked { + notification_request_id, + message, + .. + } => Method::NotifyTracked { + notification_request_id, + message, + source: AuthenticatedInputSource::LocalOperator, + }, + method => method, + } +} + async fn run_protocol_session( handle: worker::WorkerHandle, streams: WorkerProtocolSessionStreams, @@ -438,6 +597,7 @@ async fn run_protocol_session( let Ok(method) = decode_method(&message) else { return; }; + let method = authorize_local_input(method); if let Some(event) = dispatch_worker_protocol_method(&handle, method).await && !send_protocol_event(&peer, event).await { @@ -578,6 +738,9 @@ fn classify_startup_error(error: WorkerBootstrapError) -> StandaloneStartupError WorkerBootstrapError::Worker(WorkerError::Provider(_)) => { StandaloneStartupError::ModelProvider } + WorkerBootstrapError::Worker(WorkerError::SubjektivSessionAttribution { + message, .. + }) => StandaloneStartupError::Subject(message), WorkerBootstrapError::Worker(_) => StandaloneStartupError::WorkerConfiguration, WorkerBootstrapError::Controller { source, .. } if source.kind() == std::io::ErrorKind::Other => @@ -587,3 +750,49 @@ fn classify_startup_error(error: WorkerBootstrapError) -> StandaloneStartupError WorkerBootstrapError::Controller { .. } => StandaloneStartupError::Controller, } } + +#[cfg(test)] +mod local_input_tests { + use super::*; + use protocol::{AuthenticatedInputSource, Method}; + + #[test] + fn local_transport_stamps_operator_without_backend_account() { + let source = AuthenticatedInputSource::Account { + account_id: "forged-account".into(), + }; + for method in [ + Method::submit_text("plain", "hello"), + Method::SubmitTracked { + submission_request_id: "tracked".into(), + input: vec![], + source: source.clone(), + }, + Method::SubmitIfIdle { + submission_request_id: "idle".into(), + input: vec![], + source: source.clone(), + }, + Method::Notify { + notification_request_id: "plain-notify".into(), + message: "hello".into(), + }, + Method::NotifyTracked { + notification_request_id: "tracked-notify".into(), + message: "hello".into(), + source, + }, + ] { + let decoded: Method = + serde_json::from_str(&serde_json::to_string(&method).unwrap()).unwrap(); + match authorize_local_input(decoded) { + Method::SubmitTracked { source, .. } + | Method::SubmitIfIdle { source, .. } + | Method::NotifyTracked { source, .. } => { + assert_eq!(source, AuthenticatedInputSource::LocalOperator) + } + other => panic!("unexpected local input {other:?}"), + } + } + } +} diff --git a/crates/standalone/src/job_store.rs b/crates/standalone/src/job_store.rs index a28979f00..c0dd3da43 100644 --- a/crates/standalone/src/job_store.rs +++ b/crates/standalone/src/job_store.rs @@ -6,7 +6,6 @@ //! authority. The service owns the connection mutex and live-resource cleanup. use std::path::Path; -use std::time::Duration; use job::{JobRequest, JobResultSubmission}; use rusqlite::{Connection, OptionalExtension, TransactionBehavior, params}; @@ -14,7 +13,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use thiserror::Error; -const SCHEMA_VERSION: i64 = 1; +const SCHEMA_VERSION: i64 = 2; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -57,6 +56,8 @@ impl JobAttemptState { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct JobSnapshot { pub request: JobRequest, + /// Durable Host-issued domain delegation, not model input or Profile identity. + pub domain_grant: Option, pub state: JobState, pub attempt: JobAttempt, pub result: Option, @@ -112,6 +113,8 @@ pub struct JobAttempt { pub enum JobStoreError { #[error("Job database: {0}")] Sqlite(#[from] rusqlite::Error), + #[error(transparent)] + Storage(#[from] feature_storage::FeatureStorageError), #[error("Job JSON: {0}")] Json(#[from] serde_json::Error), #[error(transparent)] @@ -151,16 +154,30 @@ impl JobStore { if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) { std::fs::create_dir_all(parent)?; } + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + std::fs::OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(false) + .mode(0o600) + .custom_flags(libc::O_NOFOLLOW) + .open(path)?; + } let mut conn = Connection::open(path)?; - conn.busy_timeout(Duration::from_secs(5))?; - conn.pragma_update(None, "foreign_keys", "ON")?; - conn.pragma_update(None, "journal_mode", "WAL")?; - conn.pragma_update(None, "synchronous", "FULL")?; + feature_storage::configure_connection(&conn)?; let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?; let version: i64 = tx.pragma_query_value(None, "user_version", |row| row.get(0))?; match version { 0 => { tx.execute_batch(SCHEMA)?; + tx.execute_batch(GRANT_SCHEMA)?; + tx.pragma_update(None, "user_version", SCHEMA_VERSION)?; + } + 1 => { + tx.execute_batch(GRANT_SCHEMA)?; tx.pragma_update(None, "user_version", SCHEMA_VERSION)?; } SCHEMA_VERSION => {} @@ -186,7 +203,17 @@ impl JobStore { } pub(crate) fn reserve(&mut self, request: JobRequest) -> Result { + self.reserve_granted(request, None) + } + pub(crate) fn reserve_granted( + &mut self, + request: JobRequest, + grant: Option, + ) -> Result { request.validate()?; + if let Some(value) = &grant { + job::result_digest(value, job::ABSOLUTE_MAX_RESULT_BYTES)?; + } let fingerprint = request.fingerprint()?; let request_json = serde_json::to_string(&request)?; let tx = self @@ -198,7 +225,10 @@ impl JobStore { [&request.job_id], |row| row.get(0), )?; - if stored != fingerprint || snapshot.request != request { + if stored != fingerprint + || snapshot.request != request + || snapshot.domain_grant != grant + { return Err(JobStoreError::IntentConflict(request.job_id)); } tx.commit()?; @@ -219,6 +249,12 @@ impl JobStore { ], )?; insert_attempt(&tx, &request, 1)?; + if let Some(grant) = grant { + tx.execute( + "INSERT INTO job_domain_grants(job_id, grant_json) VALUES (?1, ?2)", + params![request.job_id, serde_json::to_string(&grant)?], + )?; + } let snapshot = require_snapshot(&tx, &request.job_id)?; tx.commit()?; Ok(snapshot) @@ -255,6 +291,16 @@ impl JobStore { require_snapshot(&self.conn, job_id) } + pub(crate) fn unacknowledged(&self) -> Result, JobStoreError> { + let mut statement = self.conn.prepare("SELECT job_id FROM job_intents WHERE state = 'completed' AND acknowledged = 0 ORDER BY rowid")?; + let ids = statement + .query_map([], |row| row.get::<_, String>(0))? + .collect::>>()?; + ids.into_iter() + .map(|id| require_snapshot(&self.conn, &id)) + .collect() + } + /// Only unstarted current attempts are eligible for service startup/resume. pub(crate) fn pending(&self) -> Result, JobStoreError> { let mut statement = self.conn.prepare( @@ -570,6 +616,15 @@ fn read_snapshot(conn: &Connection, job_id: &str) -> Result, }; Ok(JobSnapshot { request: serde_json::from_str(&request)?, + domain_grant: conn + .query_row( + "SELECT grant_json FROM job_domain_grants WHERE job_id = ?1", + [job_id], + |row| row.get::<_, String>(0), + ) + .optional()? + .map(|s| serde_json::from_str(&s)) + .transpose()?, state, acknowledged, attempt: JobAttempt { @@ -585,6 +640,17 @@ fn read_snapshot(conn: &Connection, job_id: &str) -> Result, .transpose() } +const GRANT_SCHEMA: &str = " +CREATE TABLE job_domain_grants ( + job_id TEXT PRIMARY KEY NOT NULL REFERENCES job_intents(job_id), + grant_json TEXT NOT NULL +); +CREATE TRIGGER job_grant_immutable BEFORE UPDATE ON job_domain_grants +BEGIN SELECT RAISE(ABORT, 'immutable Job domain grant'); END; +CREATE TRIGGER job_grant_retained BEFORE DELETE ON job_domain_grants +BEGIN SELECT RAISE(ABORT, 'retained Job domain grant'); END; +"; + const SCHEMA: &str = " CREATE TABLE job_intents ( job_id TEXT PRIMARY KEY NOT NULL, diff --git a/crates/standalone/src/jobs.rs b/crates/standalone/src/jobs.rs index 95a198efe..0753e1937 100644 --- a/crates/standalone/src/jobs.rs +++ b/crates/standalone/src/jobs.rs @@ -27,6 +27,17 @@ pub enum JobServiceError { Cleanup(String), } +pub(crate) trait JobAttemptFence: Send + Sync { + fn ensure_live(&self) -> Result<(), String>; +} +pub(crate) trait JobDomainProvider: Send + Sync { + fn grant( + &self, + snapshot: &JobSnapshot, + fence: Arc, + ) -> Result; +} + struct Execution { cancel: watch::Sender, complete: watch::Receiver>>, @@ -48,6 +59,8 @@ pub struct StandaloneJobs { executions: Arc>, cwd: PathBuf, capacity: u16, + domain: Arc>>>, + model_client: Arc>>>, } impl StandaloneJobs { @@ -60,9 +73,43 @@ impl StandaloneJobs { })), cwd, capacity: job::DEFAULT_MAX_CONCURRENT_JOBS, + domain: Arc::default(), + model_client: Arc::default(), }) } + pub(crate) fn bind_model_client(&self, client: Box) { + *self + .model_client + .lock() + .expect("Job model transport poisoned") = Some(client); + } + pub(crate) fn bind_domain( + &self, + provider: Arc, + ) -> Result<(), JobServiceError> { + let mut domain = self.domain.lock().expect("Job domain poisoned"); + if domain.is_some() { + return Err(JobServiceError::Configuration( + "Job domain already bound".into(), + )); + } + *domain = Some(provider); + Ok(()) + } + + pub(crate) fn request_granted( + &self, + request: JobRequest, + grant: serde_json::Value, + ) -> Result { + let executions = self.executions.lock().expect("Job executions poisoned"); + if executions.closed { + return Err(JobServiceError::Closed); + } + self.with_store(|store| store.reserve_granted(request, Some(grant))) + } + fn with_store( &self, operation: impl FnOnce(&mut JobStore) -> Result, @@ -91,6 +138,10 @@ impl StandaloneJobs { Ok(self.with_store(|store| store.pending())?) } + pub fn unacknowledged_results(&self) -> Result, JobServiceError> { + self.with_store(|store| store.unacknowledged()) + } + /// Explicit new attempt, bounded by immutable intent limits. Unknown /// outcomes cannot be retried without domain reconciliation/new intent. pub fn retry(&self, job_id: &str) -> Result { @@ -154,9 +205,40 @@ impl StandaloneJobs { job_id: &str, client: Option>, ) -> Result<(), JobServiceError> { + let client = client.or_else(|| { + self.model_client + .lock() + .expect("Job model transport poisoned") + .as_ref() + .map(|c| c.clone_boxed()) + }); let snapshot = self.get(job_id)?; - let prepared = worker::job::prepare_job(&snapshot.request, &self.cwd, client) - .map_err(|error| JobServiceError::Configuration(error.to_string()))?; + let timeout = Duration::from_secs(u64::from(snapshot.request.limits.timeout_seconds)); + let deadline = tokio::time::Instant::now() + timeout; + let fence: Arc = Arc::new(BoundAttemptFence { + store: self.store.clone(), + job_id: job_id.into(), + attempt_id: snapshot.attempt.attempt_id.clone(), + input_revision: snapshot.request.input_revision.clone(), + deadline, + }); + let grant = match ( + &snapshot.domain_grant, + self.domain.lock().expect("Job domain poisoned").as_ref(), + ) { + (Some(_), Some(provider)) => provider + .grant(&snapshot, fence) + .map_err(JobServiceError::Configuration)?, + (Some(_), None) => { + return Err(JobServiceError::Configuration( + "persisted Job requires an unavailable explicit domain capability".into(), + )); + } + (None, _) => worker::job::JobFeatureGrant::default(), + }; + let prepared = + worker::job::prepare_job_with_grant(&snapshot.request, &self.cwd, client, grant) + .map_err(|error| JobServiceError::Configuration(error.to_string()))?; let mut executions = self.executions.lock().expect("Job executions poisoned"); if executions.closed { return Err(JobServiceError::Closed); @@ -209,13 +291,12 @@ impl StandaloneJobs { let store = self.store.clone(); let id = job_id.to_string(); let attempt_id = snapshot.attempt.attempt_id.clone(); - let timeout = Duration::from_secs(u64::from(snapshot.request.limits.timeout_seconds)); let sink = Arc::new(BoundResultSink { store: store.clone(), job_id: id.clone(), attempt_id: attempt_id.clone(), input_revision: snapshot.request.input_revision.clone(), - deadline: tokio::time::Instant::now() + timeout, + deadline, }); let (cancel, mut cancellation) = watch::channel(false); let (complete_tx, complete) = watch::channel(None); @@ -341,6 +422,11 @@ impl StandaloneJobs { // Close SQLite before the Host releases its lease. Cloned handles may // outlive the Host but cannot mutate or retain the live connection. self.store.lock().expect("Job store poisoned").take(); + self.domain.lock().expect("Job domain poisoned").take(); + self.model_client + .lock() + .expect("Job model transport poisoned") + .take(); error.map_or(Ok(()), Err) } } @@ -391,3 +477,32 @@ impl worker::job::JobResultSink for BoundResultSink { .map_err(|error| error.to_string()) } } + +struct BoundAttemptFence { + store: Arc>>, + job_id: String, + attempt_id: String, + input_revision: String, + deadline: tokio::time::Instant, +} +impl JobAttemptFence for BoundAttemptFence { + fn ensure_live(&self) -> Result<(), String> { + if tokio::time::Instant::now() >= self.deadline { + return Err("Job domain deadline expired".into()); + } + let slot = self.store.lock().map_err(|_| "Job store poisoned")?; + let snapshot = slot + .as_ref() + .ok_or("Job Host closed")? + .get(&self.job_id) + .map_err(|e| e.to_string())?; + if snapshot.state != JobState::Pending + || snapshot.attempt.state != JobAttemptState::Dispatched + || snapshot.attempt.attempt_id != self.attempt_id + || snapshot.request.input_revision != self.input_revision + { + return Err("Job domain grant is not bound to the current live attempt".into()); + } + Ok(()) + } +} diff --git a/crates/standalone/src/jobs/tests.rs b/crates/standalone/src/jobs/tests.rs index 825cd360c..213afa6cf 100644 --- a/crates/standalone/src/jobs/tests.rs +++ b/crates/standalone/src/jobs/tests.rs @@ -1,4 +1,10 @@ use super::*; +use agen::llm_client::{ClientError, Request, event::Event}; +use async_trait::async_trait; +use futures::Stream; +use serde_json::{Value, json}; +use std::collections::VecDeque; +use std::pin::Pin; use worker::job::JobResultSink; fn request(id: &str) -> JobRequest { @@ -32,6 +38,345 @@ fn result(id: &str, attempt: &str) -> JobResultSubmission { } } +#[derive(Clone, Default)] +struct DomainTestClient { + scripts: Arc>>>, + requests: Arc>>, + entered: Arc, + stall: bool, + failure_release: Option>, +} +impl DomainTestClient { + fn success() -> Self { + Self { + scripts: Arc::new(Mutex::new(VecDeque::from([ + vec![ + Event::tool_use_start(0, "result-1", "SubmitJobResult"), + Event::tool_input_delta(0, r#"{"result":{"valid":true}}"#), + Event::tool_use_stop(0), + ], + vec![ + Event::text_block_start(0), + Event::text_delta(0, "done"), + Event::text_block_stop(0, None), + ], + ]))), + ..Default::default() + } + } +} +#[async_trait] +impl LlmClient for DomainTestClient { + fn clone_boxed(&self) -> Box { + Box::new(self.clone()) + } + async fn stream( + &self, + request: Request, + ) -> Result> + Send>>, ClientError> { + self.requests.lock().unwrap().push(request); + self.entered.notify_one(); + if let Some(release) = &self.failure_release { + release.notified().await; + return Err(ClientError::Config( + "known scripted provider failure".into(), + )); + } + if self.stall { + return Ok(Box::pin(futures::stream::pending())); + } + let events = self + .scripts + .lock() + .unwrap() + .pop_front() + .expect("unexpected model turn"); + Ok(Box::pin(futures::stream::iter(events.into_iter().map(Ok)))) + } +} + +type CapturedDomainAttempt = (JobSnapshot, Arc); +#[derive(Default)] +struct RecordingDomainProvider(Mutex>); +fn domain_grant(id: &str) -> Value { + json!({"consumer":"test-domain", "job_id":id, "input_revision":"r1"}) +} +fn domain_request(id: &str) -> JobRequest { + let mut intent = request(id); + intent.profile = "builtin:standalone-subjektiv-consolidation".into(); + intent +} +impl JobDomainProvider for RecordingDomainProvider { + fn grant( + &self, + snapshot: &JobSnapshot, + fence: Arc, + ) -> Result { + if snapshot.domain_grant != Some(domain_grant(&snapshot.request.job_id)) { + return Err("foreign or mismatched immutable domain grant".into()); + } + self.0.lock().unwrap().push((snapshot.clone(), fence)); + Ok(worker::job::JobFeatureGrant { + satisfied_requirements: vec!["feature.subjektiv"], + ..Default::default() + }) + } +} +async fn await_model(client: &DomainTestClient) { + tokio::time::timeout(Duration::from_secs(2), client.entered.notified()) + .await + .unwrap(); +} +async fn await_job(jobs: &StandaloneJobs, id: &str) -> JobSnapshot { + tokio::time::timeout(Duration::from_secs(3), jobs.wait(id)) + .await + .unwrap() + .unwrap() +} + +#[tokio::test] +async fn pending_domain_grants_survive_service_reopen_and_remain_immutable() { + let temp = tempfile::tempdir().unwrap(); + let path = temp.path().join("jobs.sqlite3"); + let jobs = StandaloneJobs::open(&path, temp.path().to_path_buf()).unwrap(); + let intent = domain_request("granted"); + let grant = domain_grant("granted"); + let reserved = jobs.request_granted(intent.clone(), grant.clone()).unwrap(); + let root = jobs.request(request("root")).unwrap(); + assert_eq!(reserved.domain_grant, Some(grant.clone())); + assert!(root.domain_grant.is_none()); + assert_eq!( + jobs.request_granted(intent.clone(), grant.clone()).unwrap(), + reserved + ); + assert!(matches!( + jobs.request(intent.clone()), + Err(JobServiceError::Store(JobStoreError::IntentConflict(_))) + )); + assert!(matches!( + jobs.request_granted(intent.clone(), domain_grant("foreign")), + Err(JobServiceError::Store(JobStoreError::IntentConflict(_))) + )); + assert!(matches!( + jobs.request_granted(request("root"), grant.clone()), + Err(JobServiceError::Store(JobStoreError::IntentConflict(_))) + )); + assert_eq!(jobs.pending().unwrap().len(), 2); + jobs.shutdown().await.unwrap(); + + let reopened = StandaloneJobs::open(&path, temp.path().to_path_buf()).unwrap(); + let pending = reopened.pending().unwrap(); + assert_eq!(pending.len(), 2); + assert!(pending.contains(&reserved)); + assert!(pending.contains(&root)); + assert_eq!(reopened.get("granted").unwrap(), reserved); + assert_eq!( + reopened + .request_granted(intent.clone(), grant.clone()) + .unwrap(), + reserved + ); + assert!(matches!( + reopened.request_granted(intent, json!({"different":true})), + Err(JobServiceError::Store(JobStoreError::IntentConflict(_))) + )); + let provider = Arc::new(RecordingDomainProvider::default()); + reopened.bind_domain(provider.clone()).unwrap(); + reopened + .start_with_model_client("granted", DomainTestClient::success()) + .await + .unwrap(); + let completed = await_job(&reopened, "granted").await; + assert_eq!(completed.state, JobState::Completed); + assert_eq!(completed.domain_grant, Some(grant)); + assert_eq!(completed.result, Some(json!({"valid":true}))); + let captured = provider.0.lock().unwrap()[0].0.clone(); + assert_eq!( + captured, reserved, + "provider receives the persisted immutable intent and grant" + ); + reopened.shutdown().await.unwrap(); +} + +#[tokio::test] +async fn persisted_grant_requires_provider_and_missing_grant_cannot_be_derived_from_profile() { + let temp = tempfile::tempdir().unwrap(); + let jobs = + StandaloneJobs::open(&temp.path().join("jobs.sqlite3"), temp.path().to_path_buf()).unwrap(); + let granted = jobs + .request_granted(domain_request("granted"), domain_grant("granted")) + .unwrap(); + let client = DomainTestClient::success(); + let error = jobs + .start_with_model_client("granted", client.clone()) + .await + .unwrap_err(); + assert!(matches!(error, JobServiceError::Configuration(_))); + assert!( + error + .to_string() + .contains("unavailable explicit domain capability"), + "{error}" + ); + assert_eq!(jobs.get("granted").unwrap(), granted); + assert!(client.requests.lock().unwrap().is_empty()); + + let provider = Arc::new(RecordingDomainProvider::default()); + jobs.bind_domain(provider.clone()).unwrap(); + assert!(matches!( + jobs.bind_domain(provider.clone()), + Err(JobServiceError::Configuration(_)) + )); + let ungranted = jobs.request(domain_request("ungranted")).unwrap(); + let error = jobs + .start_with_model_client("ungranted", client.clone()) + .await + .unwrap_err(); + assert!(matches!(error, JobServiceError::Configuration(_))); + assert!(error.to_string().contains("feature.subjektiv"), "{error}"); + assert_eq!(jobs.get("ungranted").unwrap(), ungranted); + assert!( + provider.0.lock().unwrap().is_empty(), + "bound provider is not a profile-derived grant" + ); + assert!(client.requests.lock().unwrap().is_empty()); + jobs.shutdown().await.unwrap(); +} + +#[tokio::test] +async fn foreign_job_revision_and_consumer_grants_are_refused_before_model_dispatch() { + let temp = tempfile::tempdir().unwrap(); + let jobs = + StandaloneJobs::open(&temp.path().join("jobs.sqlite3"), temp.path().to_path_buf()).unwrap(); + let provider = Arc::new(RecordingDomainProvider::default()); + jobs.bind_domain(provider.clone()).unwrap(); + for (id, grant) in [ + ("foreign-job", domain_grant("another-job")), + ( + "foreign-revision", + json!({"consumer":"test-domain","job_id":"foreign-revision","input_revision":"r0"}), + ), + ( + "foreign-consumer", + json!({"consumer":"another-domain","job_id":"foreign-consumer","input_revision":"r1"}), + ), + ] { + let reserved = jobs.request_granted(domain_request(id), grant).unwrap(); + let client = DomainTestClient::success(); + let error = jobs + .start_with_model_client(id, client.clone()) + .await + .unwrap_err(); + assert!(matches!(error, JobServiceError::Configuration(_))); + assert!( + error.to_string().contains("foreign or mismatched"), + "{error}" + ); + assert_eq!(jobs.get(id).unwrap(), reserved); + assert!(client.requests.lock().unwrap().is_empty()); + } + assert!(provider.0.lock().unwrap().is_empty()); + jobs.shutdown().await.unwrap(); +} + +#[tokio::test] +async fn domain_fence_is_live_only_for_current_dispatched_attempt_and_revoked_on_cancel() { + let temp = tempfile::tempdir().unwrap(); + let jobs = + StandaloneJobs::open(&temp.path().join("jobs.sqlite3"), temp.path().to_path_buf()).unwrap(); + let provider = Arc::new(RecordingDomainProvider::default()); + jobs.bind_domain(provider.clone()).unwrap(); + let reserved = jobs + .request_granted(domain_request("fenced"), domain_grant("fenced")) + .unwrap(); + let failure_release = Arc::new(tokio::sync::Notify::new()); + let first = DomainTestClient { + failure_release: Some(failure_release.clone()), + ..Default::default() + }; + jobs.start_with_model_client("fenced", first.clone()) + .await + .unwrap(); + await_model(&first).await; + let old_fence = provider.0.lock().unwrap()[0].1.clone(); + old_fence.ensure_live().unwrap(); + failure_release.notify_one(); + let failed = await_job(&jobs, "fenced").await; + assert_eq!(failed.state, JobState::Failed); + assert!(failed.result.is_none()); + assert!(old_fence.ensure_live().is_err()); + // Only known failure permits a retry; cancellation remains terminal. + let retried = jobs.retry("fenced").unwrap(); + assert_ne!(retried.attempt.attempt_id, reserved.attempt.attempt_id); + assert_eq!(retried.domain_grant, reserved.domain_grant); + assert!(old_fence.ensure_live().is_err()); + let second = DomainTestClient { + stall: true, + ..Default::default() + }; + jobs.start_with_model_client("fenced", second.clone()) + .await + .unwrap(); + await_model(&second).await; + let current_fence = provider.0.lock().unwrap()[1].1.clone(); + current_fence.ensure_live().unwrap(); + assert!( + old_fence.ensure_live().is_err(), + "a new attempt must not revive a stale capability" + ); + let cancelled = jobs.cancel("fenced").unwrap(); + assert_eq!(cancelled.state, JobState::Cancelled); + assert!( + current_fence.ensure_live().is_err(), + "durable cancellation revokes current domain operations immediately" + ); + assert!(old_fence.ensure_live().is_err()); + let cleaned = await_job(&jobs, "fenced").await; + assert_eq!(cleaned.state, JobState::Cancelled); + assert!(cleaned.result.is_none()); + assert!( + jobs.retry("fenced").is_err(), + "cancelled intent is not a known retryable failure" + ); + jobs.shutdown().await.unwrap(); + assert!(old_fence.ensure_live().is_err()); + assert!(current_fence.ensure_live().is_err()); +} + +#[tokio::test] +async fn bound_domain_does_not_change_root_generic_result_only_execution() { + let temp = tempfile::tempdir().unwrap(); + let jobs = + StandaloneJobs::open(&temp.path().join("jobs.sqlite3"), temp.path().to_path_buf()).unwrap(); + let provider = Arc::new(RecordingDomainProvider::default()); + jobs.bind_domain(provider.clone()).unwrap(); + let root = jobs.request(request("root")).unwrap(); + assert!(root.domain_grant.is_none()); + let client = DomainTestClient::success(); + jobs.start_with_model_client("root", client.clone()) + .await + .unwrap(); + let completed = await_job(&jobs, "root").await; + assert_eq!(completed.state, JobState::Completed); + assert_eq!(completed.result, Some(json!({"valid":true}))); + assert!(completed.domain_grant.is_none()); + assert!(provider.0.lock().unwrap().is_empty()); + let requests = client.requests.lock().unwrap(); + assert_eq!(requests.len(), 2); + for sent in requests.iter() { + assert_eq!( + sent.tools + .iter() + .map(|tool| tool.name.as_str()) + .collect::>(), + ["SubmitJobResult"] + ); + assert_eq!(sent.config.max_tokens, Some(worker::job::MAX_JOB_TOKENS)); + } + drop(requests); + jobs.shutdown().await.unwrap(); +} + #[tokio::test] async fn host_result_capability_cannot_cross_job_attempt_revision_or_deadline() { let temp = tempfile::tempdir().unwrap(); diff --git a/crates/standalone/src/launch.rs b/crates/standalone/src/launch.rs index e65b69bf4..2220a7c30 100644 --- a/crates/standalone/src/launch.rs +++ b/crates/standalone/src/launch.rs @@ -15,6 +15,7 @@ pub struct StandaloneLaunchConfig { pub state_dir: PathBuf, pub profile: ProfileSelector, pub worker_name: String, + pub subject_id: Option, } pub struct ResolvedStandaloneLaunch { @@ -22,6 +23,7 @@ pub struct ResolvedStandaloneLaunch { pub state_dir: PathBuf, pub profile: ResolvedProfile, pub prompt_catalog: PromptCatalogSource, + pub subject_id: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] @@ -32,6 +34,10 @@ pub enum StandaloneLaunchError { PathProfileUnsupported, #[error("the standalone profile could not be resolved")] ProfileResolutionFailed, + #[error( + "local Subject selection requires an enabled, non-consolidator subjektiv Profile and canonical Subject id" + )] + InvalidSubjectPolicy, } impl StandaloneLaunchConfig { @@ -46,9 +52,15 @@ impl StandaloneLaunchConfig { state_dir: state_dir.into(), profile, worker_name: worker_name.into(), + subject_id: None, } } + pub fn with_subject(mut self, subject_id: String) -> Self { + self.subject_id = Some(subject_id); + self + } + /// Resolve only built-in/XDG profile authority and bind standalone scope /// to the canonical process cwd. Repository-local profile discovery is /// deliberately not part of this path. @@ -74,12 +86,32 @@ impl StandaloneLaunchConfig { .map_err(|_| StandaloneLaunchError::ProfileResolutionFailed)?; validate_profile_execution_target(&profile.manifest, ProfileExecutionTarget::Standalone) .map_err(|_| StandaloneLaunchError::ProfileResolutionFailed)?; + if let Some(id) = &self.subject_id { + if !crate::subjektiv::valid_subject_id(id) + || !profile.manifest.feature.subjektiv.profile.enabled + || profile + .manifest + .feature + .subjektiv + .profile + .consolidation_tools + || profile + .manifest + .feature + .subjektiv + .workspace_settings + .is_some() + { + return Err(StandaloneLaunchError::InvalidSubjectPolicy); + } + } Ok(ResolvedStandaloneLaunch { cwd, state_dir: self.state_dir, profile, prompt_catalog: PromptCatalogSource::builtins_only(), + subject_id: self.subject_id, }) } } diff --git a/crates/standalone/src/lib.rs b/crates/standalone/src/lib.rs index 43da6c0e7..2d8c6fa5d 100644 --- a/crates/standalone/src/lib.rs +++ b/crates/standalone/src/lib.rs @@ -9,6 +9,8 @@ mod job_store; pub mod jobs; pub mod launch; pub mod store; +pub mod subjektiv; +mod subjektiv_sessions; pub use host::{StandaloneHost, StandaloneShutdownError, StandaloneStartupError}; pub use launch::{ResolvedStandaloneLaunch, StandaloneLaunchConfig, StandaloneLaunchError}; diff --git a/crates/standalone/src/store.rs b/crates/standalone/src/store.rs index 870da83f4..6d119a6af 100644 --- a/crates/standalone/src/store.rs +++ b/crates/standalone/src/store.rs @@ -87,6 +87,9 @@ pub struct StandaloneWorkerRecord { pub storage_key: String, pub cwd: StandaloneCwdIdentity, pub manifest: WorkerManifest, + /// Original explicit local Subject/storage binding; never inferred from policy. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub subject: Option, pub active_session_id: SessionId, #[serde(default, skip_serializing_if = "Option::is_none")] pub active_segment_id: Option, @@ -127,6 +130,16 @@ impl StandaloneWorkerStore { Ok(Self { root }) } + /// Observational constructor: never creates a missing root or repairs state. + pub fn open_existing(root: impl Into) -> Result { + let root = root.into(); + let metadata = fs::symlink_metadata(&root).map_err(StandaloneStoreError::Io)?; + if !metadata.is_dir() || metadata.file_type().is_symlink() { + return Err(StandaloneStoreError::NotDirectory); + } + Ok(Self { root }) + } + #[must_use] pub fn root(&self) -> &Path { &self.root @@ -158,6 +171,25 @@ impl StandaloneWorkerStore { storage_key: String, active_session_id: SessionId, active_segment_id: Option, + ) -> Result { + self.commit_created_connected( + allocation, + manifest, + storage_key, + active_session_id, + active_segment_id, + None, + ) + } + + pub(crate) fn commit_created_connected( + &self, + allocation: &StandaloneWorkerAllocation, + manifest: WorkerManifest, + storage_key: String, + active_session_id: SessionId, + active_segment_id: Option, + subject: Option, ) -> Result { let now = now_unix_ms()?; let record = StandaloneWorkerRecord { @@ -168,6 +200,7 @@ impl StandaloneWorkerStore { storage_key, cwd: allocation.cwd.clone(), manifest, + subject, active_session_id, active_segment_id, status: StandaloneWorkerStatus::Active, @@ -793,6 +826,7 @@ permission = "write" inode: None, }, manifest, + subject: None, active_session_id: "01a05782-d5dd-78f1-b9cd-ce37535bdb9e".parse().unwrap(), active_segment_id: None, status: StandaloneWorkerStatus::Stopped, diff --git a/crates/standalone/src/subjektiv.rs b/crates/standalone/src/subjektiv.rs new file mode 100644 index 000000000..132ad588a --- /dev/null +++ b/crates/standalone/src/subjektiv.rs @@ -0,0 +1,1115 @@ +//! Operator-managed local Subject storage and explicit Host capabilities. +//! Profile policy, identifiers and repository files are never execution grants. + +use std::fs::{self, File, OpenOptions}; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::sync::{ + Arc, Mutex, + atomic::{AtomicBool, Ordering}, +}; + +use feature_storage::FeatureStorage; +use fs4::fs_std::FileExt; +use serde::{Deserialize, Serialize}; +use server_api::*; +use subjektiv::{SubjectRecord, SubjectRole, SubjectSessionAttribution, SubjektivStore}; +use worker::subjektiv::{ + SubjektivHost, SubjektivHostContext, SubjektivHostError, SubjektivHostSettings, +}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct StandaloneSubjectBinding { + pub scope_id: String, + pub subject_id: String, +} + +#[derive(Debug, thiserror::Error)] +pub enum SubjectError { + #[error("local Subject storage: {0}")] + Io(#[from] std::io::Error), + #[error(transparent)] + Domain(#[from] subjektiv::SubjektivError), + #[error(transparent)] + Storage(#[from] feature_storage::FeatureStorageError), + #[error("local Subject storage metadata: {0}")] + Json(#[from] serde_json::Error), + #[error("local Subject storage permission or identity is invalid: {0}")] + InvalidScope(String), + #[error("Subject `{0}` is already active; its kernel lease cannot be replaced")] + Active(String), +} + +/// Explicit operator create/list surface, not a model-accessible database handle. +pub struct StandaloneSubjects { + state_root: PathBuf, + root: PathBuf, + scope_id: String, + memory_language: String, + manager: FeatureStorage, + store: SubjektivStore, +} + +impl StandaloneSubjects { + pub fn open(state_dir: impl AsRef) -> Result { + Self::open_inner(state_dir.as_ref(), true) + } + + pub(crate) fn open_existing(state_dir: &Path) -> Result { + Self::open_inner(state_dir, false) + } + + fn open_inner(state_dir: &Path, create: bool) -> Result { + if create { + private_dir(state_dir, true)?; + } + validate_directory(state_dir, true)?; + let state_root = fs::canonicalize(state_dir)?; + let root = state_root.join("subjektiv"); + if create { + private_dir(&root, true)?; + } + validate_directory(&root, true)?; + let init_lock = private_file(&root.join("scope.lock"), create)?; + init_lock.lock_exclusive()?; + let scope_path = root.join("scope.json"); + let scope_id: String = match fs::symlink_metadata(&scope_path) { + Ok(_) => { + validate_file(&scope_path)?; + serde_json::from_slice(&fs::read(&scope_path)?)? + } + Err(e) if create && e.kind() == std::io::ErrorKind::NotFound => { + let id = uuid::Uuid::now_v7().to_string(); + let mut file = private_file(&scope_path, true)?; + file.write_all(&serde_json::to_vec(&id)?)?; + file.sync_all()?; + File::open(&root)?.sync_all()?; + id + } + Err(e) => return Err(e.into()), + }; + if uuid::Uuid::parse_str(&scope_id).is_err() { + return Err(SubjectError::InvalidScope( + "storage scope id is invalid".into(), + )); + } + let language_path = root.join("language.json"); + let memory_language: String = match fs::symlink_metadata(&language_path) { + Ok(_) => { + validate_file(&language_path)?; + serde_json::from_slice(&fs::read(&language_path)?)? + } + Err(error) if create && error.kind() == std::io::ErrorKind::NotFound => { + let language = "English".to_owned(); + let mut file = private_file(&language_path, true)?; + file.write_all(&serde_json::to_vec(&language)?)?; + file.sync_all()?; + File::open(&root)?.sync_all()?; + language + } + Err(error) => return Err(error.into()), + }; + if !manifest::is_normalized_workspace_memory_language(&memory_language) { + return Err(SubjectError::InvalidScope( + "invalid local Memory language setting".into(), + )); + } + let db_root = root.join("features"); + if create { + private_dir(&db_root, true)?; + } + validate_directory(&db_root, true)?; + // Validate every existing node before SQLite opens it. Private parents + // prevent another user from replacing a checked node during the open. + validate_tree(&db_root)?; + let manager = FeatureStorage::new(&db_root); + let scope = manager.scope(&scope_id)?; + let registration = SubjektivStore::register(&scope)?; + let store = SubjektivStore::open(&scope, ®istration)?; + protect_tree(&db_root)?; + Ok(Self { + state_root, + root, + scope_id, + memory_language, + manager, + store, + }) + } + + pub fn create( + &self, + role: &str, + behavior_md: Option<&str>, + ) -> Result { + Ok(self.store.create_subject_with_behavior( + SubjectRole::new(role)?, + behavior_md.unwrap_or_default().to_owned(), + )?) + } + + pub fn list(&self) -> Result, SubjectError> { + let mut items = Vec::new(); + let mut after: Option<(String, String)> = None; + loop { + let page = self.store.list_subjects_after( + 100, + after + .as_ref() + .map(|(created, id)| (created.as_str(), id.as_str())), + )?; + if page.has_more { + after = page + .items + .last() + .map(|item| (item.created_at.clone(), item.id.clone())); + } + items.extend(page.items); + if !page.has_more { + return Ok(items); + } + } + } + + pub(crate) fn connect( + self, + subject_id: &str, + worker_id: protocol::WorkerId, + ) -> Result { + let subject = self.store.subject(subject_id)?.ok_or_else(|| { + SubjectError::InvalidScope( + "selected Subject does not exist in this storage scope".into(), + ) + })?; + if subject.state != subjektiv::SubjectState::Active { + return Err(SubjectError::InvalidScope( + "selected Subject is retired".into(), + )); + } + // Canonical common-issued UUID is required before using it as a path. + if !valid_subject_id(subject_id) || subject.id != subject_id { + return Err(SubjectError::InvalidScope("invalid Subject id".into())); + } + let leases = self.root.join("leases"); + private_dir(&leases, true)?; + let lease = private_file(&leases.join(format!("{subject_id}.lock")), true)?; + if !lease.try_lock_exclusive()? { + return Err(SubjectError::Active(subject_id.into())); + } + let jobs_root = self.root.join("jobs"); + private_dir(&jobs_root, true)?; + let jobs_dir = jobs_root.join(subject_id); + private_dir(&jobs_dir, true)?; + validate_tree(&jobs_dir)?; + let binding = StandaloneSubjectBinding { + scope_id: self.scope_id.clone(), + subject_id: subject_id.to_string(), + }; + let host = Arc::new(LocalSubjectHost { + language: self.memory_language.clone(), + catalog: self, + binding: binding.clone(), + worker_id, + worker_key: format!("standalone-{worker_id}"), + live: AtomicBool::new(true), + jobs: Mutex::new(None), + }); + Ok(SubjectConnection { + host, + binding, + lease: Some(lease), + jobs_path: jobs_dir.join("jobs.sqlite3"), + armed: false, + }) + } +} + +pub(crate) struct SubjectConnection { + pub host: Arc, + pub binding: StandaloneSubjectBinding, + lease: Option, + pub jobs_path: PathBuf, + armed: bool, +} +impl SubjectConnection { + pub fn arm(&mut self) { + self.armed = true; + } + pub fn retain(&mut self) { + if let Some(file) = self.lease.take() { + std::mem::forget(file); + } + } + pub fn close(&mut self) -> Result<(), SubjectError> { + self.host.jobs.lock().expect("Subject Jobs poisoned").take(); + self.host.live.store(false, Ordering::Release); + self.host.catalog.manager.shutdown()?; + if let Some(file) = &self.lease { + file.unlock()?; + } + self.lease.take(); + self.armed = false; + Ok(()) + } +} + +impl Drop for SubjectConnection { + fn drop(&mut self) { + if self.armed { + self.retain(); + } + } +} + +pub(crate) struct LocalSubjectHost { + catalog: StandaloneSubjects, + binding: StandaloneSubjectBinding, + worker_id: protocol::WorkerId, + worker_key: String, + language: String, + live: AtomicBool, + jobs: Mutex>, +} + +impl LocalSubjectHost { + fn ensure_live(&self) -> Result<(), SubjektivHostError> { + if !self.live.load(Ordering::Acquire) { + return Err(SubjektivHostError::Unavailable( + "local Subject Host is closed".into(), + )); + } + Ok(()) + } + fn body(&self, context: &SubjektivHostContext) -> Result<(), SubjektivHostError> { + self.ensure_live()?; + if context.worker_id != self.worker_key && context.worker_id != self.worker_id.to_string() { + return Err(SubjektivHostError::InvalidArgument( + "Worker is not bound to this Subject capability".into(), + )); + } + if context + .session_id + .parse::() + .is_err() + { + return Err(SubjektivHostError::InvalidArgument( + "invalid committed Session identity".into(), + )); + } + Ok(()) + } + fn attribution( + &self, + context: &SubjektivHostContext, + create: bool, + ) -> Result { + self.body(context)?; + let store = &self.catalog.store; + if let Some(record) = store + .session_attribution(&context.session_id) + .map_err(domain_error)? + { + if record.subject_id != self.binding.subject_id + || record.runtime_id != self.binding.scope_id + || record.worker_id != self.worker_id.to_string() + { + return Err(SubjektivHostError::InvalidArgument( + "Session does not belong to this local Subject/Worker scope".into(), + )); + } + return Ok(record); + } + if !create { + return Err(SubjektivHostError::InvalidArgument( + "Session attribution is unavailable".into(), + )); + } + // The capability is bound before controller startup. The Worker supplies + // the Session it owns; models cannot call this Host API or select one. + let record = SubjectSessionAttribution::new( + &self.binding.subject_id, + &self.binding.scope_id, + self.worker_id.to_string(), + &context.session_id, + ) + .map_err(domain_error)?; + store + .record_session_attribution(record) + .map_err(domain_error) + } +} + +fn domain_error(error: subjektiv::SubjektivError) -> SubjektivHostError { + operation_error(subjektiv::api::subjektiv_store_error(error)) +} +fn operation_error(error: subjektiv::api::OperationError) -> SubjektivHostError { + use subjektiv::api::OperationError::*; + match error { + Conflict(message) => SubjektivHostError::Conflict { + code: if message.contains("stale_cursor") { + "stale_cursor" + } else if message.contains("candidate_decision_conflict") { + "candidate_decision_conflict" + } else { + "revision_conflict" + } + .into(), + message, + }, + Store(message) => SubjektivHostError::Unavailable(message), + other => SubjektivHostError::InvalidArgument(other.to_string()), + } +} + +fn private_dir(path: &Path, strict: bool) -> Result<(), SubjectError> { + if !path.exists() { + #[cfg(unix)] + { + use std::os::unix::fs::DirBuilderExt; + let mut builder = fs::DirBuilder::new(); + builder.recursive(true).mode(0o700); + match builder.create(path) { + Ok(()) => {} + Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {} + Err(e) => return Err(e.into()), + } + } + #[cfg(not(unix))] + fs::create_dir_all(path)?; + } + validate_directory(path, strict) +} +fn validate_directory(path: &Path, strict: bool) -> Result<(), SubjectError> { + reject_symlink_components(path)?; + let meta = fs::symlink_metadata(path)?; + if !meta.is_dir() { + return Err(SubjectError::InvalidScope(format!( + "{} is not a directory", + path.display() + ))); + } + validate_owner_mode(path, &meta, if strict { 0o077 } else { 0o022 }) +} +fn validate_file(path: &Path) -> Result<(), SubjectError> { + let meta = fs::symlink_metadata(path)?; + if !meta.is_file() { + return Err(SubjectError::InvalidScope(format!( + "{} is not a regular private file", + path.display() + ))); + } + validate_owner_mode(path, &meta, 0o077) +} +fn validate_owner_mode(path: &Path, meta: &fs::Metadata, mask: u32) -> Result<(), SubjectError> { + #[cfg(unix)] + { + use std::os::unix::fs::MetadataExt; + if meta.uid() != unsafe { libc::geteuid() } || meta.mode() & mask != 0 { + return Err(SubjectError::InvalidScope(format!( + "{} must be owned by the operator and private/non-writable by others", + path.display() + ))); + } + } + #[cfg(not(unix))] + { + let _ = (path, meta, mask); + return Err(SubjectError::InvalidScope( + "private local Subject storage is not supported on this platform".into(), + )); + } + Ok(()) +} +fn reject_symlink_components(path: &Path) -> Result<(), SubjectError> { + let mut part = PathBuf::new(); + for component in path.components() { + part.push(component); + if fs::symlink_metadata(&part)?.file_type().is_symlink() { + return Err(SubjectError::InvalidScope( + "symlinked Subject storage is not authority".into(), + )); + } + } + Ok(()) +} +fn private_file(path: &Path, create: bool) -> Result { + reject_symlink_components( + path.parent() + .ok_or_else(|| SubjectError::InvalidScope("missing parent".into()))?, + )?; + let mut options = OpenOptions::new(); + options + .read(true) + .write(true) + .create(create) + .truncate(false); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + options.mode(0o600).custom_flags(libc::O_NOFOLLOW); + } + let file = options.open(path)?; + validate_file(path)?; + Ok(file) +} +fn validate_tree(root: &Path) -> Result<(), SubjectError> { + for entry in fs::read_dir(root)? { + let entry = entry?; + let meta = fs::symlink_metadata(entry.path())?; + if meta.is_dir() { + validate_directory(&entry.path(), true)?; + validate_tree(&entry.path())?; + } else { + validate_file(&entry.path())?; + } + } + Ok(()) +} +fn protect_tree(root: &Path) -> Result<(), SubjectError> { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + for entry in fs::read_dir(root)? { + let entry = entry?; + let meta = fs::symlink_metadata(entry.path())?; + if meta.file_type().is_symlink() { + return Err(SubjectError::InvalidScope("symlinked database".into())); + } + fs::set_permissions( + entry.path(), + fs::Permissions::from_mode(if meta.is_dir() { 0o700 } else { 0o600 }), + )?; + if meta.is_dir() { + protect_tree(&entry.path())?; + } + } + } + Ok(()) +} + +impl StandaloneSubjects { + pub(crate) fn scope_id(&self) -> &str { + &self.scope_id + } +} +impl LocalSubjectHost { + pub(crate) fn bind_jobs( + self: &Arc, + jobs: crate::jobs::StandaloneJobs, + ) -> Result<(), crate::StandaloneStartupError> { + jobs.bind_domain(Arc::new(LocalDomainProvider(Arc::downgrade(self)))) + .map_err(|e| crate::StandaloneStartupError::Subject(e.to_string()))?; + *self.jobs.lock().expect("Subject Job connection poisoned") = Some(jobs); + Ok(()) + } + fn jobs(&self) -> Result { + self.ensure_live()?; + self.jobs + .lock() + .expect("Subject Job connection poisoned") + .clone() + .ok_or_else(|| SubjektivHostError::Unavailable("Subject Job Host unavailable".into())) + } + pub(crate) fn recover_jobs(&self) { + let Ok(jobs) = self.jobs() else { + return; + }; + if let Ok(results) = jobs.unacknowledged_results() { + for result in results { + if result + .domain_grant + .as_ref() + .and_then(|v| serde_json::from_value::(v.clone()).ok()) + .is_some_and(|g| g.binding == self.binding) + { + // Acceptance already verified the immutable attempt/receipts. + // Delivery acknowledgement never replays the successful model. + let _ = jobs.acknowledge(&result.request.job_id); + } + } + } + if let Ok(pending) = jobs.pending() { + for job in pending { + if job + .domain_grant + .as_ref() + .and_then(|v| serde_json::from_value::(v.clone()).ok()) + .is_some_and(|g| g.binding == self.binding) + { + schedule_job(jobs.clone(), job.request.job_id); + } + } + } + } + fn validate_evidence( + &self, + session: &str, + evidence: &mut [memory::extract::StagingEvidence], + sources: &mut [memory::schema::SourceEvidenceRef], + ) -> Result<(), SubjektivHostError> { + if evidence.is_empty() + || sources.is_empty() + || evidence.len() > subjektiv::MAX_STAGING_ANCHORS + || sources.len() > subjektiv::MAX_STAGING_ANCHORS + { + return Err(SubjektivHostError::InvalidArgument( + "staging requires bounded committed public evidence".into(), + )); + } + let index = crate::subjektiv_sessions::authorized_index( + &self.catalog.state_root, + &self.binding.scope_id, + &self.binding.subject_id, + &self.catalog.store, + session, + )?; + let metadata = session_store::public_index::read_fs_session_evidence_index( + &self + .catalog + .state_root + .join(self.worker_id.to_string()) + .join("sessions"), + session + .parse() + .map_err(|_| SubjektivHostError::InvalidArgument("invalid Session id".into()))?, + session_store::SessionPublicIndexLimits { + max_bytes: 8 * 1024 * 1024, + max_entries: 20_000, + max_segments: 64, + }, + ) + .map_err(|e| { + SubjektivHostError::Unavailable(format!("committed evidence metadata unavailable: {e}")) + })?; + if metadata.index.generation != index.generation { + return Err(SubjektivHostError::Unavailable("committed Session changed while resolving evidence; retry from the committed capture".into())); + } + for anchor in evidence { + let source = sources + .iter_mut() + .find(|source| source.evidence_id.as_deref() == Some(&anchor.id)) + .ok_or_else(|| { + SubjektivHostError::InvalidArgument("evidence pointer is missing".into()) + })?; + if source.session_id.as_deref().is_some_and(|id| id != session) { + return Err(SubjektivHostError::InvalidArgument( + "foreign Session evidence".into(), + )); + } + let entry = index.segments.iter().filter(|seg|source.segment_id.as_deref() == Some(seg.segment_id.as_str())) + .flat_map(|seg|&seg.entries).find(|entry|entry.entry_ref == anchor.id) + .ok_or_else(|| if metadata.has_open_run { + SubjektivHostError::Conflict { code:"pending_commit".into(),message:"explicit evidence remains inside an open logical Run; retry after the append-only Run commit".into() } + } else { SubjektivHostError::InvalidArgument("evidence is not an accessible committed public entry".into()) })?; + if source.entry_range != anchor.entry_range + || anchor.entry_range.is_none_or(|[a, b]| a > b) + { + return Err(SubjektivHostError::InvalidArgument( + "invalid committed evidence range".into(), + )); + } + if !metadata + .ranges + .get(&(entry.segment_id.clone(), anchor.id.clone())) + .is_some_and(|ranges| { + anchor + .entry_range + .is_some_and(|range| ranges.contains(&range)) + }) + { + return Err(SubjektivHostError::InvalidArgument("evidence numeric range does not match either common committed producer projection".into())); + } + let kind = match entry.provenance { + protocol::SessionEntryProvenance::HumanInput => { + memory::schema::EvidenceOriginKind::HumanInput + } + protocol::SessionEntryProvenance::WorkerInput => { + memory::schema::EvidenceOriginKind::WorkerInput + } + protocol::SessionEntryProvenance::FlowInstruction => { + memory::schema::EvidenceOriginKind::FlowInstruction + } + protocol::SessionEntryProvenance::BackendInstruction => { + memory::schema::EvidenceOriginKind::BackendInstruction + } + protocol::SessionEntryProvenance::ModelOutput => { + memory::schema::EvidenceOriginKind::ModelOutput + } + protocol::SessionEntryProvenance::ToolOutput => { + memory::schema::EvidenceOriginKind::ToolOutput + } + protocol::SessionEntryProvenance::DerivedSummary => { + memory::schema::EvidenceOriginKind::DerivedSummary + } + protocol::SessionEntryProvenance::LegacyUnknown => { + memory::schema::EvidenceOriginKind::LegacyUnknown + } + }; + let origin = memory::schema::EvidenceOrigin { + kind, + account_id: None, + workspace_id: None, + // Local storage scope is not a Runtime/control-plane identity. + runtime_id: None, + worker_id: Some(self.worker_id.to_string()), + flow_selector: None, + flow_definition_id: None, + flow_definition_revision: None, + }; + anchor.origin = Some(origin.clone()); + source.origin = Some(origin); + anchor.excerpt = Some(entry.compact_text.clone()); + anchor.summary = Some(entry.compact_text.clone()); + source.session_id = Some(session.into()); + source.summary = Some(entry.compact_text.clone()); + } + if sources + .iter() + .any(|source| source.session_id.as_deref() != Some(session)) + { + return Err(SubjektivHostError::InvalidArgument( + "unresolved evidence source".into(), + )); + } + Ok(()) + } +} +impl SubjektivHost for LocalSubjectHost { + fn settings(&self) -> SubjektivHostSettings { + SubjektivHostSettings { + language: self.language.clone(), + } + } + fn memory( + &self, + context: &SubjektivHostContext, + mut operation: SubjektivMemoryBackendOperation, + ) -> Result { + self.body(context)?; + let attribution = + if let SubjektivMemoryBackendOperation::StageExplicit(input) = &mut operation { + if input.session_id != context.session_id { + return Err(SubjektivHostError::InvalidArgument( + "explicit staging must use the connected committed Session".into(), + )); + } + let attribution = self.attribution(context, false)?; + self.validate_evidence( + &context.session_id, + &mut input.evidence, + &mut input.source_refs, + )?; + Some(attribution) + } else { + None + }; + let authority = subjektiv::api::HostOperationContext::validated_body( + &self.catalog.store, + &self.binding.subject_id, + attribution, + ) + .map_err(operation_error)?; + subjektiv::api::execute(&self.catalog.store, &authority, operation).map_err(operation_error) + } + fn session( + &self, + context: &SubjektivHostContext, + operation: SubjektivSessionBackendOperation, + ) -> Result { + self.body(context)?; + crate::subjektiv_sessions::execute( + &self.catalog.state_root, + &self.binding.scope_id, + &self.binding.subject_id, + &self.catalog.store, + operation, + ) + } + fn stage_candidate( + &self, + context: &SubjektivHostContext, + mut operation: memory::backend::MemoryStageCandidateOperation, + ) -> Result { + let attribution = self.attribution(context, false)?; + self.validate_evidence( + &context.session_id, + &mut operation.evidence, + &mut operation.source_refs, + )?; + if operation.candidate.kind == memory::extract::CandidateKind::Preference + && operation.evidence.iter().any(|e| { + e.origin + .as_ref() + .is_none_or(|o| o.kind != memory::schema::EvidenceOriginKind::HumanInput) + }) + { + return Err(SubjektivHostError::InvalidArgument( + "preference requires HumanInput evidence".into(), + )); + } + let record = memory::extract::StagingRecord::from_candidate( + uuid::Uuid::now_v7().to_string(), + operation.extract_run_id, + operation.source, + operation.candidate, + operation.evidence, + operation.source_refs, + ); + let (staged, _) = self + .catalog + .store + .stage_candidate_with_attribution( + subjektiv::SubjectStagingRecord::attach(&self.binding.subject_id, record), + attribution, + ) + .map_err(domain_error)?; + Ok(SubjektivStageCandidateResponse { + staging_id: staged.id, + }) + } + fn record_session( + &self, + context: &SubjektivHostContext, + create_if_missing: bool, + ) -> Result { + let attribution = self.attribution(context, create_if_missing)?; + Ok(SubjektivRecordSessionResponse { + subject_id: attribution.subject_id, + session_id: attribution.session_id, + }) + } + fn request_consolidation( + &self, + context: &SubjektivHostContext, + operation: memory::backend::MemoryConsolidateStagingOperation, + ) -> Result { + self.body(context)?; + let jobs = self.jobs()?; + if let Some(pending) = jobs + .pending() + .map_err(|e| SubjektivHostError::Unavailable(e.to_string()))? + .into_iter() + .find(|job| { + job.domain_grant + .as_ref() + .and_then(|v| serde_json::from_value::(v.clone()).ok()) + .is_some_and(|g| g.binding == self.binding) + }) + { + schedule_job(jobs, pending.request.job_id); + let (count, bytes) = self + .catalog + .store + .pending_staging_backlog(&self.binding.subject_id) + .map_err(domain_error)?; + return Ok(memory::backend::MemoryConsolidationOutput { status:"resumed_pending".into(),summary:"Resumed the existing immutable reserved Subject Job; unknown attempts are never replayed".into(),candidate_count:count,total_bytes:bytes }); + } + let (count, bytes) = self + .catalog + .store + .pending_staging_backlog(&self.binding.subject_id) + .map_err(domain_error)?; + let surface = self + .catalog + .store + .resident_surface(&self.binding.subject_id) + .map_err(domain_error)? + .availability; + if !subjektiv::job::consolidation_required(count, bytes, operation.force, surface) { + return Ok(memory::backend::MemoryConsolidationOutput { + status: "skipped_below_threshold".into(), + summary: "Subject backlog/surface is below the common consolidation threshold" + .into(), + candidate_count: count, + total_bytes: bytes, + }); + } + let ids = subjektiv::job::bounded_candidate_batch( + self.catalog + .store + .pending_staging_candidates(&self.binding.subject_id, 100) + .map_err(domain_error)? + .into_iter() + .map(|c| c.id), + job::DEFAULT_MAX_RESULT_BYTES, + ) + .map_err(operation_error)?; + let subject = self + .catalog + .store + .subject(&self.binding.subject_id) + .map_err(domain_error)? + .ok_or_else(|| SubjektivHostError::Unavailable("Subject missing".into()))?; + let grant = LocalConsolidationGrant { + binding: self.binding.clone(), + candidate_ids: ids, + }; + let request=job::JobRequest { + job_id:format!("subjektiv-consolidation:{}",uuid::Uuid::now_v7()),purpose:"subjektiv_consolidation".into(), + input_revision:subject.store_revision.to_string(),input_ref:format!("subjektiv://{}/consolidation",self.binding.subject_id), + input:serde_json::json!({"subject_id":self.binding.subject_id,"candidate_ids":grant.candidate_ids}), + instruction:"Resolve ONLY candidate_ids with MemoryStagingList/Read, recall and MemoryApplyCandidate. Already resolved IDs are durable receipts; do not repeat them. Leave new candidates for a later Job. When the entire immutable batch has a disposition, call SubmitJobResult with result: {subject_id, candidate_ids}. The Host awaits clean-context surface generation and verifies actual receipts/outcome before accepting success; never supply surface claims.".into(), + profile:"builtin:standalone-subjektiv-consolidation".into(),serialization_key:Some(format!("subjektiv:{}",self.binding.subject_id)), + limits:job::JobLimits { timeout_seconds:600,..Default::default() }, + }; + let jobs = self.jobs()?; + let snapshot = jobs + .request_granted( + request, + serde_json::to_value(grant) + .map_err(|e| SubjektivHostError::Unavailable(e.to_string()))?, + ) + .map_err(|e| SubjektivHostError::Unavailable(e.to_string()))?; + schedule_job(jobs, snapshot.request.job_id); + Ok(memory::backend::MemoryConsolidationOutput { status:"started".into(),summary:"Requested a durable generic standalone consolidation Job; processing stops on Host shutdown".into(),candidate_count:count,total_bytes:bytes }) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +struct LocalConsolidationGrant { + binding: StandaloneSubjectBinding, + candidate_ids: Vec, +} +struct LocalDomainProvider(std::sync::Weak); +impl crate::jobs::JobDomainProvider for LocalDomainProvider { + fn grant( + &self, + snapshot: &crate::jobs::JobSnapshot, + fence: Arc, + ) -> Result { + let host = self.0.upgrade().ok_or("Subject Host unavailable")?; + host.ensure_live().map_err(|e| e.to_string())?; + let grant: LocalConsolidationGrant = serde_json::from_value( + snapshot + .domain_grant + .clone() + .ok_or("missing explicit Subject delegation")?, + ) + .map_err(|e| e.to_string())?; + if grant.binding != host.binding + || snapshot.request.purpose != "subjektiv_consolidation" + || snapshot.request.input + != serde_json::json!({"subject_id":host.binding.subject_id,"candidate_ids":grant.candidate_ids}) + { + return Err("persisted Job grant/input does not match local Subject capability".into()); + } + let adapter = Arc::new(LocalConsolidationHost { + host, + grant, + job_id: snapshot.request.job_id.clone(), + attempt_id: snapshot.attempt.attempt_id.clone(), + fence, + }); + let connection = worker::subjektiv::SubjektivHostConnection::new( + adapter.clone(), + SubjektivHostContext { + worker_id: format!("job:{}", snapshot.attempt.attempt_id), + session_id: format!("job:{}", snapshot.attempt.attempt_id), + }, + ) + .map_err(|e| e.to_string())?; + let feature = worker::feature::builtin::memory::SubjektivConsolidationToolsFeature::new( + connection.clone(), + ) + .map_err(|e| e.to_string())?; + Ok(worker::job::JobFeatureGrant { + features: worker::feature::FeatureRegistryBuilder::new().with_module(feature), + satisfied_requirements: vec!["feature.subjektiv"], + finalizer: Some(Arc::new(LocalConsolidationFinalizer { + adapter, + connection, + })), + }) + } +} +struct LocalConsolidationHost { + host: Arc, + grant: LocalConsolidationGrant, + job_id: String, + attempt_id: String, + fence: Arc, +} +impl LocalConsolidationHost { + fn live(&self) -> Result<(), SubjektivHostError> { + self.host.ensure_live()?; + self.fence + .ensure_live() + .map_err(SubjektivHostError::Unavailable) + } + fn denied(&self) -> Result { + Err(SubjektivHostError::InvalidArgument( + "consolidation Job has no Subject body/extraction authority".into(), + )) + } +} +impl SubjektivHost for LocalConsolidationHost { + fn settings(&self) -> SubjektivHostSettings { + self.host.settings() + } + fn consolidation_granted(&self) -> bool { + true + } + fn memory( + &self, + _context: &SubjektivHostContext, + operation: SubjektivMemoryBackendOperation, + ) -> Result { + self.live()?; + let authority = subjektiv::api::HostOperationContext::validated_consolidation( + &self.host.catalog.store, + &self.grant.binding.subject_id, + Some(self.grant.candidate_ids.clone()), + Some(subjektiv::api::JobAttemptBinding { + job_id: self.job_id.clone(), + attempt_id: self.attempt_id.clone(), + }), + ) + .map_err(operation_error)?; + subjektiv::api::execute(&self.host.catalog.store, &authority, operation) + .map_err(operation_error) + } + fn session( + &self, + _context: &SubjektivHostContext, + _operation: SubjektivSessionBackendOperation, + ) -> Result { + self.denied() + } + fn stage_candidate( + &self, + _: &SubjektivHostContext, + _: memory::backend::MemoryStageCandidateOperation, + ) -> Result { + self.denied() + } + fn record_session( + &self, + _: &SubjektivHostContext, + _: bool, + ) -> Result { + self.denied() + } + fn request_consolidation( + &self, + _: &SubjektivHostContext, + _: memory::backend::MemoryConsolidateStagingOperation, + ) -> Result { + self.denied() + } +} +struct LocalConsolidationFinalizer { + adapter: Arc, + connection: worker::subjektiv::SubjektivHostConnection, +} +#[async_trait::async_trait] +impl worker::job::JobResultFinalizer for LocalConsolidationFinalizer { + fn validate_profile(&self, manifest: &manifest::WorkerManifest) -> Result<(), String> { + let config = &manifest.feature.subjektiv; + if !config.profile.enabled + || !config.profile.consolidation_tools + || config.profile.extraction.enabled + || config.workspace_settings.is_some() + { + return Err("local consolidation requires enabled decision policy without extraction or Backend settings".into()); + } + Ok(()) + } + async fn finalize( + &self, + result: serde_json::Value, + manifest: manifest::WorkerManifest, + client: Box, + cancellation: worker::feature::background::BackgroundTaskCancellation, + ) -> Result { + use agen::tool::ToolError; + self.adapter.live().map_err(ToolError::from)?; + if result + != serde_json::json!({"subject_id":self.adapter.grant.binding.subject_id,"candidate_ids":self.adapter.grant.candidate_ids}) + { + return Err(ToolError::InvalidArgument("result must claim exactly the bound Subject/batch, without model surface/disposition assertions".into())); + } + for id in &self.adapter.grant.candidate_ids { + if self + .adapter + .host + .catalog + .store + .staging_resolution(&self.adapter.grant.binding.subject_id, id) + .map_err(|e| ToolError::ExecutionFailed(e.to_string()))? + .is_none() + { + return Err(ToolError::InvalidArgument(format!( + "candidate {id} is unresolved" + ))); + } + } + let prompts = worker::PromptCatalog::builtins_only() + .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?; + let feature = worker::subjektiv::SubjektivSurfaceLifecycleFeature::for_host( + self.connection.clone(), + manifest, + client, + Arc::new(prompts.into()), + worker::WorkerWorkspaceContext::no_workspace(), + ) + .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?; + let surface = feature + .complete_for_job(cancellation, None) + .await + .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?; + self.adapter.live().map_err(ToolError::from)?; + let mut value = result; + value + .as_object_mut() + .expect("checked object") + .insert("surface".into(), surface); + subjektiv::job::validate_result( + &self.adapter.host.catalog.store, + &self.adapter.grant.binding.subject_id, + &self.adapter.grant.candidate_ids, + &self.adapter.job_id, + &self.adapter.attempt_id, + &value, + ) + .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?; + subjektiv::job::with_dispositions( + &self.adapter.host.catalog.store, + &self.adapter.grant.binding.subject_id, + &self.adapter.grant.candidate_ids, + &value, + ) + .map_err(|e| ToolError::ExecutionFailed(e.to_string())) + } +} + +fn schedule_job(jobs: crate::jobs::StandaloneJobs, id: String) { + // Scheduling/consumer acknowledgement has no Internal Worker ownership. + // The generic service fences and retains every execution until cleanup. + tokio::spawn(async move { + if let Err(error) = jobs.start(&id).await { + eprintln!("standalone Subject Job {id} remains pending: {error}"); + return; + } + match jobs.wait(&id).await { + Ok(result) if result.state == crate::jobs::JobState::Completed => { + let _ = jobs.acknowledge(&id); + } + Ok(_) => {} + Err(error) => eprintln!( + "standalone Subject Job {id} cleanup/acknowledgement is unconfirmed: {error}" + ), + } + }); +} + +#[cfg(test)] +mod tests; + +pub(crate) fn valid_subject_id(value: &str) -> bool { + value + .strip_prefix("subject-") + .and_then(|id| uuid::Uuid::parse_str(id).ok()) + .is_some_and(|id| format!("subject-{id}") == value) +} diff --git a/crates/standalone/src/subjektiv/tests.rs b/crates/standalone/src/subjektiv/tests.rs new file mode 100644 index 000000000..e04a894b3 --- /dev/null +++ b/crates/standalone/src/subjektiv/tests.rs @@ -0,0 +1,130 @@ +use super::*; + +#[test] +fn catalog_reopen_preserves_random_scope_subject_and_behavior() { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("state"); + let catalog = StandaloneSubjects::open(&root).unwrap(); + let subject = catalog + .create( + "continuing-owner", + Some("Keep user behavior separate from Memory."), + ) + .unwrap(); + let scope = catalog.scope_id().to_owned(); + assert!(uuid::Uuid::parse_str(&scope).is_ok()); + assert!(uuid::Uuid::parse_str(subject.id.strip_prefix("subject-").unwrap()).is_ok()); + assert_ne!(scope, subject.id); + drop(catalog); + let reopened = StandaloneSubjects::open_existing(&root).unwrap(); + assert_eq!(reopened.scope_id(), scope); + assert_eq!(reopened.list().unwrap(), vec![subject]); +} + +#[test] +fn implicit_open_does_not_create_subject_storage() { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("missing"); + assert!(StandaloneSubjects::open_existing(&root).is_err()); + assert!(!root.exists()); + assert!(StandaloneSubjects::open_existing(temp.path()).is_err()); + assert!(!temp.path().join("subjektiv").exists()); +} + +#[test] +fn subject_kernel_lease_is_not_worker_or_profile_scoped() { + let temp = tempfile::tempdir().unwrap(); + let catalog = StandaloneSubjects::open(temp.path().join("state")).unwrap(); + let a = catalog.create("a", None).unwrap(); + let b = catalog.create("b", None).unwrap(); + let mut first = catalog + .connect(&a.id, protocol::WorkerId::now_v7()) + .unwrap(); + assert!(matches!( + StandaloneSubjects::open_existing(&temp.path().join("state")) + .unwrap() + .connect(&a.id, protocol::WorkerId::now_v7()), + Err(SubjectError::Active(_)) + )); + let mut second = StandaloneSubjects::open_existing(&temp.path().join("state")) + .unwrap() + .connect(&b.id, protocol::WorkerId::now_v7()) + .unwrap(); + first.close().unwrap(); + let mut replacement = StandaloneSubjects::open_existing(&temp.path().join("state")) + .unwrap() + .connect(&a.id, protocol::WorkerId::now_v7()) + .unwrap(); + replacement.close().unwrap(); + second.close().unwrap(); +} + +#[test] +fn closed_capabilities_and_foreign_worker_context_are_rejected() { + let temp = tempfile::tempdir().unwrap(); + let catalog = StandaloneSubjects::open(temp.path().join("state")).unwrap(); + let subject = catalog.create("owner", None).unwrap(); + let id = protocol::WorkerId::now_v7(); + let mut connection = catalog.connect(&subject.id, id).unwrap(); + let port = connection.host.clone(); + let foreign = SubjektivHostContext { + worker_id: protocol::WorkerId::now_v7().to_string(), + session_id: session_store::SessionId::now_v7().to_string(), + }; + let read = SubjektivMemoryBackendOperation::ResidentContext( + memory::backend::MemoryResidentSummaryOperation {}, + ); + assert!(port.memory(&foreign, read.clone()).is_err()); + let owner = SubjektivHostContext { + worker_id: format!("standalone-{id}"), + session_id: session_store::SessionId::now_v7().to_string(), + }; + assert!(port.memory(&owner, read.clone()).is_ok()); + assert!( + port.record_session(&owner, false).is_err(), + "unattributed Session is not read authority" + ); + connection.close().unwrap(); + assert!(port.memory(&owner, read).is_err()); +} + +#[cfg(unix)] +#[test] +fn scope_rejects_symlinks_and_non_private_roots() { + use std::os::unix::fs::{PermissionsExt, symlink}; + let temp = tempfile::tempdir().unwrap(); + let alias = temp.path().join("alias"); + let real = temp.path().join("real"); + fs::create_dir(&real).unwrap(); + fs::set_permissions(&real, fs::Permissions::from_mode(0o700)).unwrap(); + symlink(&real, &alias).unwrap(); + assert!(StandaloneSubjects::open(&alias).is_err()); + let public = temp.path().join("public"); + fs::create_dir(&public).unwrap(); + fs::set_permissions(&public, fs::Permissions::from_mode(0o755)).unwrap(); + assert!(StandaloneSubjects::open(&public).is_err()); + let catalog = StandaloneSubjects::open(&real).unwrap(); + drop(catalog); + let scope = real.join("subjektiv/scope.json"); + let original = fs::read(&scope).unwrap(); + fs::remove_file(&scope).unwrap(); + let foreign = temp.path().join("foreign.json"); + fs::write(&foreign, &original).unwrap(); + symlink(&foreign, &scope).unwrap(); + assert!(StandaloneSubjects::open_existing(&real).is_err()); + assert_eq!(fs::read(&foreign).unwrap(), original); +} + +#[test] +fn different_local_roots_do_not_authorize_shared_subject_ids() { + let temp = tempfile::tempdir().unwrap(); + let first = StandaloneSubjects::open(temp.path().join("first")).unwrap(); + let subject = first.create("owner", None).unwrap(); + let other = StandaloneSubjects::open(temp.path().join("other")).unwrap(); + assert_ne!(first.scope_id(), other.scope_id()); + assert!( + other + .connect(&subject.id, protocol::WorkerId::now_v7()) + .is_err() + ); +} diff --git a/crates/standalone/src/subjektiv_sessions.rs b/crates/standalone/src/subjektiv_sessions.rs new file mode 100644 index 000000000..0c510aa57 --- /dev/null +++ b/crates/standalone/src/subjektiv_sessions.rs @@ -0,0 +1,1696 @@ +//! Subject-scoped observation of standalone Sessions. This module owns no +//! Session body or projection: it uses the shared strict committed public index +//! reader for the FsStore layout, never a raw snapshot projector. + +use std::path::{Path, PathBuf}; + +use protocol::WorkerId; +use serde::{Deserialize, Serialize}; +use server_api::*; +use session_store::{ + SessionId, SessionPublicIndex, SessionPublicIndexEntry, SessionPublicIndexEntryKind, + SessionPublicIndexLimits, SessionPublicIndexLineage, SessionPublicIndexOriginKind, + SessionPublicIndexReadError, SessionPublicIndexToolPart, +}; +use subjektiv::{SubjectSessionAttribution, SubjektivStore}; +use worker::subjektiv::SubjektivHostError; + +use crate::store::{StandaloneStoreError, StandaloneWorkerStore}; + +/// Adapter to the *shared* strict committed FsStore public projection. It must +/// be observational, enforce all supplied limits, and ignore unterminated tails, +/// system records, reasoning, and output without a persisted commit boundary. +pub(crate) type PublicIndexReader = fn( + &Path, + SessionId, + SessionPublicIndexLimits, +) -> Result; + +const MAX_SCANNED_SESSIONS: usize = 100; +const MAX_SCAN_BYTES: u64 = 8 * 1024 * 1024; +const MAX_SCAN_ENTRIES: usize = 20_000; +const MAX_SCAN_SEGMENTS: usize = 64; +const ITEM_BUDGET: usize = 28 * 1024; +const MAX_CURSOR_BYTES: usize = 24 * 1024; +const MAX_LINES: usize = 200; +const CURSOR_PREFIX: &str = "standalone-session-v1:"; +const END_OF_TIME: &str = "9999-12-31T23:59:59.999999999Z"; + +type Failure = SubjektivSessionErrorResponse; +type LocalResult = Result; + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +struct Cursor { + scope_id: String, + subject_id: String, + operation: String, + filters: serde_json::Value, + snapshot_at: String, + after: Option<(String, String)>, + /// Search resumes this Session before advancing the attribution keyset. + current: Option, + byte_offset: usize, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +struct Position { + session_id: String, + worker_id: String, + generation: String, + entry_offset: usize, +} + +struct Backend<'a> { + state_root: &'a Path, + scope_id: &'a str, + subject_id: &'a str, + store: &'a SubjektivStore, + reader: PublicIndexReader, +} + +struct Authorized { + attribution: SubjectSessionAttribution, + sessions_path: PathBuf, +} + +/// All scope arguments are the parent's trusted local binding, not values taken +/// from tool input. The projection is exclusively the shared FsStore reader. +pub(crate) fn execute( + state_root: &Path, + scope_id: &str, + subject_id: &str, + store: &SubjektivStore, + operation: SubjektivSessionBackendOperation, +) -> Result { + execute_with_reader( + state_root, + scope_id, + subject_id, + store, + operation, + session_store::public_index::read_fs_session_public_index, + ) +} + +fn execute_with_reader( + state_root: &Path, + scope_id: &str, + subject_id: &str, + store: &SubjektivStore, + operation: SubjektivSessionBackendOperation, + read_index: PublicIndexReader, +) -> Result { + let backend = Backend { + state_root, + scope_id, + subject_id, + store, + reader: read_index, + }; + let result = backend.check_scope().and_then(|()| match operation { + SubjektivSessionBackendOperation::List(input) => backend.list(input), + SubjektivSessionBackendOperation::Search(input) => backend.search(input), + SubjektivSessionBackendOperation::Read(input) => backend.read(input), + }); + Ok(match result { + Ok(result) => { + let response = SubjektivSessionBackendResponse::Ok { result }; + if fits(&response) { + response + } else { + error_response(failure( + SubjektivSessionDiagnosticCode::ResourceLimit, + "Session response exceeds the serialized output budget", + )) + } + } + Err(error) => error_response(error), + }) +} + +/// Evidence validation uses exactly the same attribution and Worker checks as +/// model reads. The parent can validate entry refs/ranges against this index; +/// no staging candidate is needed for a Session to be discoverable. +pub(crate) fn authorized_index( + state_root: &Path, + scope_id: &str, + subject_id: &str, + store: &SubjektivStore, + session_id: &str, +) -> Result { + authorized_index_with_reader( + state_root, + scope_id, + subject_id, + store, + session_id, + session_store::public_index::read_fs_session_public_index, + ) +} + +fn authorized_index_with_reader( + state_root: &Path, + scope_id: &str, + subject_id: &str, + store: &SubjektivStore, + session_id: &str, + read_index: PublicIndexReader, +) -> Result { + let backend = Backend { + state_root, + scope_id, + subject_id, + store, + reader: read_index, + }; + (|| { + backend.check_scope()?; + required_selector(session_id)?; + let attribution = backend.attribution(session_id)?; + let source = backend.authorize(attribution)?; + backend.index(&source, limits()) + })() + .map_err(|error: Failure| SubjektivHostError::Conflict { + code: serde_json::to_value(error.code) + .ok() + .and_then(|v| v.as_str().map(str::to_owned)) + .unwrap_or_else(|| "session_unavailable".into()), + message: error.message, + }) +} + +impl Backend<'_> { + fn check_scope(&self) -> LocalResult<()> { + if self.store.scope_id() != self.scope_id { + return Err(denied()); + } + if self + .store + .subject(self.subject_id) + .map_err(|_| subject_unavailable())? + .is_none() + { + return Err(subject_unavailable()); + } + Ok(()) + } + + fn attribution(&self, session_id: &str) -> LocalResult { + self.store + .session_attribution(session_id) + .map_err(|_| subject_unavailable())? + .filter(|a| a.subject_id == self.subject_id && a.runtime_id == self.scope_id) + .ok_or_else(denied) + } + + fn authorize(&self, attribution: SubjectSessionAttribution) -> LocalResult { + if attribution.subject_id != self.subject_id || attribution.runtime_id != self.scope_id { + return Err(denied()); + } + let id: WorkerId = attribution.worker_id.parse().map_err(|_| denied())?; + if !self.state_root.is_dir() { + return Err(failure( + SubjektivSessionDiagnosticCode::RetentionMissing, + "Attributed standalone Worker state is missing", + )); + } + let workers = StandaloneWorkerStore::open_existing(self.state_root).map_err(|_| { + failure( + SubjektivSessionDiagnosticCode::RuntimeUnavailable, + "Standalone Worker storage is inaccessible", + ) + })?; + let record = workers.load(id).map_err(|error| match error { + StandaloneStoreError::WorkerNotFound(_) => failure( + SubjektivSessionDiagnosticCode::RetentionMissing, + "Attributed standalone Worker record is missing", + ), + _ => failure( + SubjektivSessionDiagnosticCode::CorruptLog, + "Attributed standalone Worker record is unavailable or incomplete", + ), + })?; + if record.worker_id != id + || record.worker_id.to_string() != attribution.worker_id + || !record.subject.as_ref().is_some_and(|binding| { + binding.scope_id == self.scope_id && binding.subject_id == self.subject_id + }) + { + return Err(denied()); + } + Ok(Authorized { + attribution, + sessions_path: workers.sessions_dir(id), + }) + } + + fn index( + &self, + source: &Authorized, + limits: SessionPublicIndexLimits, + ) -> LocalResult { + let id: SessionId = source.attribution.session_id.parse().map_err(|_| { + failure( + SubjektivSessionDiagnosticCode::CorruptLog, + "Attributed Session identity is invalid", + ) + })?; + let index = (self.reader)(&source.sessions_path, id, limits).map_err(index_failure)?; + if index.session_id != source.attribution.session_id { + return Err(failure( + SubjektivSessionDiagnosticCode::CorruptLog, + "Committed public index does not match the attributed Session", + )); + } + if index.scanned_bytes > limits.max_bytes + || index.scanned_entries > limits.max_entries + || index.entry_count > limits.max_entries + || index.segments.len() > limits.max_segments + { + return Err(failure( + SubjektivSessionDiagnosticCode::ResourceLimit, + "Committed public index exceeds the scan budget", + )); + } + Ok(index) + } + + fn cursor( + &self, + operation: &str, + filters: serde_json::Value, + token: Option<&str>, + ) -> LocalResult { + if let Some(token) = token { + let cursor = decode_cursor(token)?; + if cursor.scope_id != self.scope_id + || cursor.subject_id != self.subject_id + || cursor.operation != operation + || cursor.filters != filters + || cursor.snapshot_at.is_empty() + || cursor.snapshot_at.len() > 128 + { + return Err(stale()); + } + Ok(cursor) + } else { + Ok(Cursor { + scope_id: self.scope_id.into(), + subject_id: self.subject_id.into(), + operation: operation.into(), + filters, + snapshot_at: END_OF_TIME.into(), + after: None, + current: None, + byte_offset: 0, + }) + } + } + + fn page( + &self, + cursor: &mut Cursor, + selector: Option<&str>, + ) -> LocalResult { + let page = self + .store + .subject_session_attribution_page( + self.subject_id, + selector, + &cursor.snapshot_at, + cursor + .after + .as_ref() + .map(|(at, id)| (at.as_str(), id.as_str())), + 1, + ) + .map_err(|_| subject_unavailable())?; + // Freeze discovery at the newest attribution on the first page, without + // needing a second clock or reading the complete Subject history. + if cursor.snapshot_at == END_OF_TIME { + if let Some(first) = page.items.first() { + cursor.snapshot_at = first.attributed_at.clone(); + } + } + Ok(page) + } + + fn list( + &self, + input: SubjektivSessionListRequest, + ) -> LocalResult { + let limit = limit(input.limit)?; + let selector = optional_selector(input.session_id.as_deref())?; + if let Some(id) = selector { + self.authorize(self.attribution(id)?)?; + } + let mut cursor = self.cursor( + "list", + serde_json::json!({"session_id": selector, "storage": input.storage}), + input.cursor.as_deref(), + )?; + if cursor.current.is_some() || cursor.byte_offset != 0 { + return Err(stale()); + } + let mut items = Vec::new(); + let mut has_more = false; + for _ in 0..MAX_SCANNED_SESSIONS { + let page = self.page(&mut cursor, selector)?; + has_more = page.has_more && selector.is_none(); + let Some(attribution) = page.items.into_iter().next() else { + break; + }; + let after = ( + attribution.attributed_at.clone(), + attribution.session_id.clone(), + ); + if attribution.runtime_id != self.scope_id { + cursor.after = Some(after); + if !has_more { + break; + } + continue; + } + // Discovery observes attribution and Worker authority only. Like + // Backend discovery, body availability remains unchecked until a + // bounded search/read; listing never scans every Session body. + let availability = self.authorize(attribution.clone()); + if availability + .as_ref() + .err() + .is_some_and(|e| e.code == SubjektivSessionDiagnosticCode::NotFoundOrNotAuthorized) + { + if selector.is_some() { + return Err(denied()); + } + cursor.after = Some(after); + if !has_more { + break; + } + continue; + } + // Standalone FsStore has no archive catalog. Archived filtering must + // not manufacture an archive or fall back to an unrelated root. + if input.storage == SubjektivSessionStorageFilter::Archived { + cursor.after = Some(after); + if !has_more { + break; + } + continue; + } + let item = SubjektivSessionListItem { + session_id: attribution.session_id, + attributed_at: attribution.attributed_at, + storage: SubjektivSessionStorage::Retained, + availability: if availability.is_ok() { + SubjektivSessionAvailability::Unchecked + } else { + SubjektivSessionAvailability::Unavailable + }, + reason: availability.err().map(|e| e.message), + }; + items.push(item); + if json_len(&items) > ITEM_BUDGET { + items.pop(); + if items.is_empty() { + return Err(resource_limit()); + } + has_more = true; + break; + } + cursor.after = Some(after); + if items.len() >= limit || !has_more { + break; + } + } + let next_cursor = has_more.then(|| encode_cursor(&cursor)).transpose()?; + Ok(SubjektivSessionBackendResult::List( + SubjektivSessionListResponse { + items, + next_cursor, + has_more, + }, + )) + } + + fn search( + &self, + input: SubjektivSessionSearchRequest, + ) -> LocalResult { + let limit = limit(input.limit)?; + let selector = optional_selector(input.session_id.as_deref())?; + let tool_name = optional_selector(input.tool_name.as_deref())?; + let query = input.query.as_deref().unwrap_or("").trim(); + if query.len() > 4096 || (query.is_empty() && selector.is_none()) { + return Err(failure( + SubjektivSessionDiagnosticCode::InvalidInput, + "Supply a bounded query, or select a Session to list its public entries", + )); + } + // Explicit foreign selectors are denied even if the query has no hits. + if let Some(id) = selector { + self.authorize(self.attribution(id)?)?; + } + let mut cursor = self.cursor( + "search", + serde_json::json!({"session_id": selector, "query": query, + "kind": input.kind, "tool_name": tool_name, "tool_part": input.tool_part}), + input.cursor.as_deref(), + )?; + if cursor.byte_offset != 0 { + return Err(stale()); + } + let mut items = Vec::new(); + let mut issues = Vec::new(); + let mut has_more = false; + let mut scan_limits = limits(); + 'sessions: for _ in 0..MAX_SCANNED_SESSIONS { + let page = self.page(&mut cursor, selector)?; + let more_attributions = page.has_more && selector.is_none(); + let Some(attribution) = page.items.into_iter().next() else { + if cursor.current.is_some() { + return Err(stale()); + } + break; + }; + let after = ( + attribution.attributed_at.clone(), + attribution.session_id.clone(), + ); + let source = self.authorize(attribution.clone()); + let index = source.and_then(|source| self.index(&source, scan_limits)); + let index = match index { + Ok(index) => index, + Err(error) => { + if cursor.current.is_some() { + return Err(error); + } + if error.code == SubjektivSessionDiagnosticCode::ResourceLimit + && (scan_limits.max_bytes < MAX_SCAN_BYTES + || scan_limits.max_entries < MAX_SCAN_ENTRIES + || scan_limits.max_segments < MAX_SCAN_SEGMENTS) + { + // This Session may fit a fresh call. Do not consume its + // attribution just because earlier Sessions used budget. + has_more = true; + break; + } + let exhausted_scan = matches!( + error.code, + SubjektivSessionDiagnosticCode::ResourceLimit + | SubjektivSessionDiagnosticCode::CorruptLog + | SubjektivSessionDiagnosticCode::RuntimeUnavailable + ); + if error.code != SubjektivSessionDiagnosticCode::NotFoundOrNotAuthorized { + issues.push(SubjektivSessionIssue { + session_id: attribution.session_id.clone(), + code: error.code, + message: error.message, + }); + } else if selector.is_some() { + return Err(denied()); + } + cursor.after = Some(after); + has_more = more_attributions; + if exhausted_scan || issues.len() >= 20 || !has_more { + break; + } + continue; + } + }; + scan_limits.max_bytes = scan_limits.max_bytes.saturating_sub(index.scanned_bytes); + scan_limits.max_entries = scan_limits + .max_entries + .saturating_sub(index.scanned_entries.max(index.entry_count)); + scan_limits.max_segments = scan_limits + .max_segments + .saturating_sub(index.segments.len()); + let mut offset = 0; + if let Some(position) = &cursor.current { + if position.session_id != attribution.session_id + || position.worker_id != attribution.worker_id + || position.generation != index.generation + || position.entry_offset > index.entry_count + { + return Err(stale()); + } + offset = position.entry_offset; + } + let total = index + .segments + .iter() + .map(|s| s.entries.len()) + .sum::(); + let mut entry_offset = offset; + for (segment, entry) in index + .segments + .iter() + .flat_map(|s| s.entries.iter().map(move |e| (s, e))) + .skip(offset) + { + if entry_matches(entry, query, input.kind, tool_name, input.tool_part) { + let (snippet, snippet_truncated) = snippet(&entry.full_text, query); + items.push(SubjektivSessionSearchItem { + session_id: index.session_id.clone(), + segment_id: segment.segment_id.clone(), + entry_ref: entry.entry_ref.clone(), + kind: entry_kind(entry.kind), + origin: entry.provenance.clone(), + tool_name: entry.tool_name.clone(), + tool_part: entry.tool_part.map(tool_part), + snippet, + snippet_truncated, + lineage: lineage(&segment.lineage), + }); + if json_len(&items) > ITEM_BUDGET { + items.pop(); + if items.is_empty() { + return Err(resource_limit()); + } + cursor.current = Some(Position { + session_id: attribution.session_id.clone(), + worker_id: attribution.worker_id.clone(), + generation: index.generation.clone(), + entry_offset, + }); + has_more = true; + break 'sessions; + } + } + entry_offset += 1; + if items.len() >= limit && entry_offset < total { + cursor.current = Some(Position { + session_id: attribution.session_id.clone(), + worker_id: attribution.worker_id.clone(), + generation: index.generation.clone(), + entry_offset, + }); + has_more = true; + break 'sessions; + } + } + cursor.current = None; + cursor.after = Some(after); + has_more = more_attributions; + if items.len() >= limit || !has_more { + break; + } + if scan_limits.max_bytes == 0 + || scan_limits.max_entries == 0 + || scan_limits.max_segments == 0 + { + break; + } + } + let next_cursor = has_more.then(|| encode_cursor(&cursor)).transpose()?; + let coverage = if issues.is_empty() { + SubjektivSessionCoverage::Complete + } else { + SubjektivSessionCoverage::Partial + }; + Ok(SubjektivSessionBackendResult::Search( + SubjektivSessionSearchResponse { + items, + issues, + coverage, + next_cursor, + has_more, + }, + )) + } + + fn read( + &self, + input: SubjektivSessionReadRequest, + ) -> LocalResult { + required_selector(&input.session_id)?; + required_selector(&input.segment_id)?; + required_selector(&input.entry_ref)?; + let mut cursor = self.cursor( + "read", + serde_json::json!({"session_id": input.session_id, + "segment_id": input.segment_id, "entry_ref": input.entry_ref, "mode": input.mode}), + input.cursor.as_deref(), + )?; + if cursor.after.is_some() { + return Err(stale()); + } + let attribution = self.attribution(&input.session_id)?; + let source = self.authorize(attribution)?; + let index = self.index(&source, limits())?; + if let Some(position) = &cursor.current { + if position.session_id != input.session_id + || position.worker_id != source.attribution.worker_id + || position.generation != index.generation + || position.entry_offset != 0 + { + return Err(stale()); + } + } else if input.cursor.is_some() { + return Err(stale()); + } + let segment = index + .segments + .iter() + .find(|s| s.segment_id == input.segment_id) + .ok_or_else(denied)?; + let entry = segment + .entries + .iter() + .find(|e| e.entry_ref == input.entry_ref) + .ok_or_else(denied)?; + let text = match input.mode { + SubjektivSessionReadMode::Compact => &entry.compact_text, + SubjektivSessionReadMode::Full => &entry.full_text, + }; + if cursor.byte_offset > text.len() || !text.is_char_boundary(cursor.byte_offset) { + return Err(stale()); + } + cursor.current = Some(Position { + session_id: input.session_id.clone(), + worker_id: source.attribution.worker_id, + generation: index.generation.clone(), + entry_offset: 0, + }); + let start = cursor.byte_offset; + let mut max_bytes = SUBJEKTIV_SESSION_MAX_READ_CONTENT_BYTES; + loop { + let end = chunk_end(text, start, max_bytes, MAX_LINES); + let has_more = end < text.len(); + cursor.byte_offset = end; + let next_cursor = has_more.then(|| encode_cursor(&cursor)).transpose()?; + let result = SubjektivSessionBackendResult::Read(SubjektivSessionReadResponse { + session_id: input.session_id.clone(), + segment_id: input.segment_id.clone(), + entry_ref: input.entry_ref.clone(), + kind: entry_kind(entry.kind), + origin: entry.provenance.clone(), + lineage: lineage(&segment.lineage), + mode: input.mode, + content: text[start..end].into(), + truncated: has_more + || (input.mode == SubjektivSessionReadMode::Compact + && entry.compact_text_truncated), + has_more, + next_cursor, + }); + if fits(&SubjektivSessionBackendResponse::Ok { + result: result.clone(), + }) { + return Ok(result); + } + if max_bytes <= 4 { + return Err(resource_limit()); + } + max_bytes = (max_bytes / 2).max(4); + } + } +} + +fn limits() -> SessionPublicIndexLimits { + SessionPublicIndexLimits { + max_bytes: MAX_SCAN_BYTES, + max_segments: MAX_SCAN_SEGMENTS, + max_entries: MAX_SCAN_ENTRIES, + } +} + +fn failure(code: SubjektivSessionDiagnosticCode, message: &str) -> Failure { + Failure { + code, + message: message.into(), + } +} +fn denied() -> Failure { + failure( + SubjektivSessionDiagnosticCode::NotFoundOrNotAuthorized, + "Session entry was not found or is not authorized", + ) +} +fn stale() -> Failure { + failure( + SubjektivSessionDiagnosticCode::StaleCursor, + "Session cursor is invalid or no longer matches its scope, filters, or committed generation", + ) +} +fn subject_unavailable() -> Failure { + failure( + SubjektivSessionDiagnosticCode::SubjectUnavailable, + "Bound Subject storage is unavailable", + ) +} +fn resource_limit() -> Failure { + failure( + SubjektivSessionDiagnosticCode::ResourceLimit, + "Session operation exceeds its bounded resource budget", + ) +} +fn error_response(error: Failure) -> SubjektivSessionBackendResponse { + SubjektivSessionBackendResponse::Error { error } +} +fn index_failure(error: SessionPublicIndexReadError) -> Failure { + let code = match error { + SessionPublicIndexReadError::MigrationRequired => { + SubjektivSessionDiagnosticCode::MigrationRequired + } + SessionPublicIndexReadError::Missing => SubjektivSessionDiagnosticCode::RetentionMissing, + SessionPublicIndexReadError::Corrupt => SubjektivSessionDiagnosticCode::CorruptLog, + SessionPublicIndexReadError::Storage => SubjektivSessionDiagnosticCode::RuntimeUnavailable, + SessionPublicIndexReadError::ResourceLimit => SubjektivSessionDiagnosticCode::ResourceLimit, + SessionPublicIndexReadError::StaleCursor => SubjektivSessionDiagnosticCode::StaleCursor, + }; + failure(code, &error.to_string()) +} +fn limit(value: Option) -> LocalResult { + let value = value.unwrap_or(20); + if (1..=100).contains(&value) { + Ok(value) + } else { + Err(failure( + SubjektivSessionDiagnosticCode::InvalidInput, + "limit must be between 1 and 100", + )) + } +} +fn required_selector(value: &str) -> LocalResult<()> { + if value.is_empty() + || value.len() > 256 + || value.trim() != value + || value.chars().any(char::is_control) + { + Err(failure( + SubjektivSessionDiagnosticCode::InvalidInput, + "Session selectors must be nonempty bounded identifiers", + )) + } else { + Ok(()) + } +} +fn optional_selector(value: Option<&str>) -> LocalResult> { + value.map(required_selector).transpose()?; + Ok(value) +} +fn json_len(value: &impl Serialize) -> usize { + serde_json::to_vec(value).map_or(usize::MAX, |v| v.len()) +} +fn fits(value: &SubjektivSessionBackendResponse) -> bool { + json_len(value) <= SUBJEKTIV_SESSION_MAX_TOOL_CONTENT_BYTES +} + +// Hex is only an opaque transport encoding, not an authorization credential. +// Every decoded identity is rechecked against durable Host authority on use. +fn encode_cursor(cursor: &Cursor) -> LocalResult { + let bytes = serde_json::to_vec(cursor).map_err(|_| resource_limit())?; + if CURSOR_PREFIX.len() + bytes.len() * 2 > MAX_CURSOR_BYTES { + return Err(resource_limit()); + } + let mut token = String::with_capacity(CURSOR_PREFIX.len() + bytes.len() * 2); + token.push_str(CURSOR_PREFIX); + const HEX: &[u8] = b"0123456789abcdef"; + for byte in bytes { + token.push(HEX[(byte >> 4) as usize] as char); + token.push(HEX[(byte & 15) as usize] as char); + } + Ok(token) +} +fn decode_cursor(token: &str) -> LocalResult { + if token.len() > MAX_CURSOR_BYTES { + return Err(stale()); + } + let hex = token.strip_prefix(CURSOR_PREFIX).ok_or_else(stale)?; + if hex.len() % 2 != 0 { + return Err(stale()); + } + fn nibble(c: u8) -> Option { + match c { + b'0'..=b'9' => Some(c - b'0'), + b'a'..=b'f' => Some(c - b'a' + 10), + _ => None, + } + } + let bytes = hex + .as_bytes() + .chunks_exact(2) + .map(|pair| Some(nibble(pair[0])? * 16 + nibble(pair[1])?)) + .collect::>>() + .ok_or_else(stale)?; + serde_json::from_slice(&bytes).map_err(|_| stale()) +} +fn entry_kind(kind: SessionPublicIndexEntryKind) -> SubjektivSessionEntryKind { + match kind { + SessionPublicIndexEntryKind::User => SubjektivSessionEntryKind::User, + SessionPublicIndexEntryKind::Assistant => SubjektivSessionEntryKind::Assistant, + SessionPublicIndexEntryKind::Tool => SubjektivSessionEntryKind::Tool, + } +} +fn tool_part(part: SessionPublicIndexToolPart) -> SubjektivSessionToolPart { + match part { + SessionPublicIndexToolPart::Input => SubjektivSessionToolPart::Input, + SessionPublicIndexToolPart::Output => SubjektivSessionToolPart::Output, + } +} +fn lineage(value: &SessionPublicIndexLineage) -> SubjektivSessionLineage { + SubjektivSessionLineage { + kind: match value.origin_kind { + SessionPublicIndexOriginKind::Root => SubjektivSessionLineageKind::Root, + SessionPublicIndexOriginKind::Fork => SubjektivSessionLineageKind::Fork, + SessionPublicIndexOriginKind::Compact => SubjektivSessionLineageKind::Compact, + }, + parent_segment_id: value.parent_segment_id.clone(), + at_turn_index: value.parent_turn_index, + } +} +fn entry_matches( + entry: &SessionPublicIndexEntry, + query: &str, + kind: Option, + name: Option<&str>, + part: SubjektivSessionToolPart, +) -> bool { + kind.is_none_or(|k| k == entry_kind(entry.kind)) + && name.is_none_or(|name| entry.tool_name.as_deref() == Some(name)) + && (entry.tool_part.is_none() + || part == SubjektivSessionToolPart::Both + || entry.tool_part.map(tool_part) == Some(part)) + && (query.is_empty() + || entry + .full_text + .to_lowercase() + .contains(&query.to_lowercase())) +} +fn snippet(text: &str, query: &str) -> (String, bool) { + // Search matching is case-insensitive; snippet framing uses exact matches + // only to avoid byte offsets into case-folded Unicode text. + let mut start = if query.is_empty() { + 0 + } else { + text.find(query).unwrap_or(0).saturating_sub(128) + }; + while !text.is_char_boundary(start) { + start -= 1; + } + let end = chunk_end(text, start, 512, 8); + (text[start..end].into(), start > 0 || end < text.len()) +} +fn chunk_end(text: &str, start: usize, max_bytes: usize, max_lines: usize) -> usize { + let mut end = start.saturating_add(max_bytes).min(text.len()); + while !text.is_char_boundary(end) { + end -= 1; + } + if let Some((offset, _)) = text[start..end].match_indices('\n').nth(max_lines - 1) { + end = start + offset + 1; + } + end +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::store::StaleLeasePolicy; + use crate::subjektiv::StandaloneSubjectBinding; + use feature_storage::FeatureStorage; + use session_store::SessionPublicIndexSegment; + + // Test-only committed public index fixtures exercise backend authorization + // and paging independently of storage layout. The production callback must + // be the shared strict reader (whose run-boundary tests live in session-store). + fn fixture_reader( + root: &Path, + id: SessionId, + bounds: SessionPublicIndexLimits, + ) -> Result { + let bytes = std::fs::read(root.join(format!("{id}.fixture"))).map_err(|e| { + if e.kind() == std::io::ErrorKind::NotFound { + SessionPublicIndexReadError::Missing + } else { + SessionPublicIndexReadError::Storage + } + })?; + let index: SessionPublicIndex = + serde_json::from_slice(&bytes).map_err(|_| SessionPublicIndexReadError::Corrupt)?; + if index.scanned_bytes > bounds.max_bytes + || index.scanned_entries > bounds.max_entries + || index.segments.len() > bounds.max_segments + { + return Err(SessionPublicIndexReadError::ResourceLimit); + } + Ok(index) + } + + struct Fixture { + temp: tempfile::TempDir, + store: SubjektivStore, + _manager: FeatureStorage, + subject: String, + worker: WorkerId, + session: SessionId, + segment: String, + index_path: PathBuf, + } + + impl Fixture { + fn new(texts: &[&str]) -> Self { + let temp = tempfile::tempdir().unwrap(); + let manager = FeatureStorage::new(temp.path().join("features")); + let scope = manager.workspace("local-scope-a").unwrap(); + let registration = SubjektivStore::register(&scope).unwrap(); + let store = SubjektivStore::open(&scope, ®istration).unwrap(); + let subject = store + .create_subject(subjektiv::SubjectRole::new("standalone_subject").unwrap()) + .unwrap() + .id; + let workers = StandaloneWorkerStore::open(temp.path().join("workers")).unwrap(); + let allocation = workers + .allocate(temp.path(), StaleLeasePolicy::Reject) + .unwrap(); + let session = SessionId::now_v7(); + let segment = SessionId::now_v7().to_string(); + let manifest = manifest::WorkerManifest::from_toml( + r#" +[worker] +name = "session-test" +[model] +scheme = "anthropic" +model_id = "claude-sonnet-4-20250514" +[engine] +[scope] +allow = [] +"#, + ) + .unwrap(); + workers + .commit_created_connected( + &allocation, + manifest, + "session-test".into(), + session, + None, + Some(StandaloneSubjectBinding { + scope_id: "local-scope-a".into(), + subject_id: subject.clone(), + }), + ) + .unwrap(); + let worker = allocation.worker_id(); + store + .record_session_attribution( + SubjectSessionAttribution::new( + &subject, + "local-scope-a", + worker.to_string(), + session.to_string(), + ) + .unwrap(), + ) + .unwrap(); + let index_path = workers + .sessions_dir(worker) + .join(format!("{session}.fixture")); + let index = SessionPublicIndex { + session_id: session.to_string(), + generation: "generation-1".into(), + scanned_bytes: 100, + scanned_entries: texts.len(), + entry_count: texts.len(), + segments: vec![SessionPublicIndexSegment { + segment_id: segment.clone(), + lineage: SessionPublicIndexLineage { + origin_kind: SessionPublicIndexOriginKind::Root, + parent_segment_id: None, + parent_turn_index: None, + }, + entries: texts + .iter() + .enumerate() + .map(|(i, text)| SessionPublicIndexEntry { + segment_id: segment.clone(), + entry_ref: format!("E{i}"), + kind: SessionPublicIndexEntryKind::User, + provenance: protocol::SessionEntryProvenance::HumanInput, + timestamp: 1, + tool_part: None, + tool_name: None, + compact_text: text.chars().take(100).collect(), + compact_text_truncated: text.len() > 100, + full_text: (*text).into(), + }) + .collect(), + }], + }; + std::fs::write(&index_path, serde_json::to_vec(&index).unwrap()).unwrap(); + Self { + temp, + store, + _manager: manager, + subject, + worker, + session, + segment, + index_path, + } + } + fn execute( + &self, + operation: SubjektivSessionBackendOperation, + ) -> SubjektivSessionBackendResponse { + execute_with_reader( + &self.temp.path().join("workers"), + "local-scope-a", + &self.subject, + &self.store, + operation, + fixture_reader, + ) + .unwrap() + } + fn list(&self, cursor: Option, limit: usize) -> SubjektivSessionBackendResponse { + self.execute(SubjektivSessionBackendOperation::List( + SubjektivSessionListRequest { + session_id: None, + storage: SubjektivSessionStorageFilter::All, + cursor, + limit: Some(limit), + }, + )) + } + fn search(&self, cursor: Option, limit: usize) -> SubjektivSessionBackendResponse { + self.execute(SubjektivSessionBackendOperation::Search( + SubjektivSessionSearchRequest { + query: None, + session_id: Some(self.session.to_string()), + kind: None, + tool_name: None, + tool_part: SubjektivSessionToolPart::Both, + cursor, + limit: Some(limit), + }, + )) + } + fn read(&self, cursor: Option) -> SubjektivSessionBackendResponse { + self.execute(SubjektivSessionBackendOperation::Read( + SubjektivSessionReadRequest { + session_id: self.session.to_string(), + segment_id: self.segment.clone(), + entry_ref: "E0".into(), + mode: SubjektivSessionReadMode::Full, + cursor, + }, + )) + } + } + + fn expect_error( + response: SubjektivSessionBackendResponse, + code: SubjektivSessionDiagnosticCode, + ) { + match response { + SubjektivSessionBackendResponse::Error { error } => assert_eq!(error.code, code), + _ => panic!("expected {code:?}, got {response:?}"), + } + } + + #[test] + fn discovers_sessions_without_staging_candidates_and_empty_subjects() { + let f = Fixture::new(&["public experience"]); + match f.list(None, 20) { + SubjektivSessionBackendResponse::Ok { + result: SubjektivSessionBackendResult::List(page), + } => { + assert_eq!(page.items.len(), 1); + assert_eq!(page.items[0].session_id, f.session.to_string()); + assert_eq!( + page.items[0].availability, + SubjektivSessionAvailability::Unchecked + ); + assert!(!page.has_more); + } + response => panic!("{response:?}"), + } + let other = f + .store + .create_subject(subjektiv::SubjectRole::new("other_subject").unwrap()) + .unwrap(); + let response = execute_with_reader( + &f.temp.path().join("workers"), + "local-scope-a", + &other.id, + &f.store, + SubjektivSessionBackendOperation::List(SubjektivSessionListRequest { + session_id: None, + storage: SubjektivSessionStorageFilter::All, + limit: None, + cursor: None, + }), + fixture_reader, + ) + .unwrap(); + match response { + SubjektivSessionBackendResponse::Ok { + result: SubjektivSessionBackendResult::List(page), + } => { + assert!(page.items.is_empty()); + assert!(!page.has_more); + } + response => panic!("{response:?}"), + } + } + + #[test] + fn foreign_subject_and_storage_scope_reads_are_denied_even_for_known_ids() { + let f = Fixture::new(&["secret from first subject"]); + let other = f + .store + .create_subject(subjektiv::SubjectRole::new("other_subject").unwrap()) + .unwrap(); + let operation = SubjektivSessionBackendOperation::Read(SubjektivSessionReadRequest { + session_id: f.session.to_string(), + segment_id: f.segment.clone(), + entry_ref: "E0".into(), + mode: SubjektivSessionReadMode::Full, + cursor: None, + }); + for (scope, subject) in [ + ("local-scope-a", other.id.as_str()), + ("foreign-scope", f.subject.as_str()), + ] { + expect_error( + execute_with_reader( + &f.temp.path().join("workers"), + scope, + subject, + &f.store, + operation.clone(), + fixture_reader, + ) + .unwrap(), + SubjektivSessionDiagnosticCode::NotFoundOrNotAuthorized, + ); + } + let backend = Backend { + state_root: &f.temp.path().join("workers"), + scope_id: "local-scope-a", + subject_id: &f.subject, + store: &f.store, + reader: fixture_reader, + }; + let mut attribution = f + .store + .session_attribution(&f.session.to_string()) + .unwrap() + .unwrap(); + attribution.worker_id = WorkerId::now_v7().to_string(); + assert_eq!( + backend.authorize(attribution).err().unwrap().code, + SubjektivSessionDiagnosticCode::RetentionMissing + ); + } + + #[test] + fn mismatched_worker_subject_binding_is_denied_without_reading_body() { + let f = Fixture::new(&["must not leak"]); + let record_path = f + .temp + .path() + .join("workers") + .join(f.worker.to_string()) + .join("record.json"); + let mut record: serde_json::Value = + serde_json::from_slice(&std::fs::read(&record_path).unwrap()).unwrap(); + record["subject"]["subject_id"] = serde_json::json!("foreign-subject"); + std::fs::write(record_path, serde_json::to_vec(&record).unwrap()).unwrap(); + // Corrupt the body as well: authorization must still win over parsing. + std::fs::write(&f.index_path, b"bad index").unwrap(); + expect_error( + f.read(None), + SubjektivSessionDiagnosticCode::NotFoundOrNotAuthorized, + ); + expect_error( + f.search(None, 20), + SubjektivSessionDiagnosticCode::NotFoundOrNotAuthorized, + ); + expect_error( + f.execute(SubjektivSessionBackendOperation::List( + SubjektivSessionListRequest { + session_id: Some(f.session.to_string()), + storage: SubjektivSessionStorageFilter::All, + limit: None, + cursor: None, + }, + )), + SubjektivSessionDiagnosticCode::NotFoundOrNotAuthorized, + ); + } + + #[test] + fn search_continuations_have_no_duplicate_or_missing_refs_and_bind_filters() { + let f = Fixture::new(&["first", "second", "third"]); + let mut cursor = None; + let mut refs = Vec::new(); + loop { + let response = f.search(cursor, 1); + assert!(fits(&response)); + match response { + SubjektivSessionBackendResponse::Ok { + result: SubjektivSessionBackendResult::Search(page), + } => { + refs.extend(page.items.into_iter().map(|item| item.entry_ref)); + if !page.has_more { + assert!(page.next_cursor.is_none()); + break; + } + cursor = page.next_cursor; + } + response => panic!("{response:?}"), + } + } + assert_eq!(refs, ["E0", "E1", "E2"]); + let token = match f.search(None, 1) { + SubjektivSessionBackendResponse::Ok { + result: SubjektivSessionBackendResult::Search(page), + } => page.next_cursor.unwrap(), + response => panic!("{response:?}"), + }; + let mut decoded = decode_cursor(&token).unwrap(); + decoded.scope_id = "foreign-scope".into(); + expect_error( + f.search(Some(encode_cursor(&decoded).unwrap()), 1), + SubjektivSessionDiagnosticCode::StaleCursor, + ); + expect_error( + f.execute(SubjektivSessionBackendOperation::Search( + SubjektivSessionSearchRequest { + query: Some("changed query".into()), + session_id: Some(f.session.to_string()), + kind: None, + tool_name: None, + tool_part: SubjektivSessionToolPart::Both, + cursor: Some(token), + limit: Some(1), + }, + )), + SubjektivSessionDiagnosticCode::StaleCursor, + ); + } + + #[test] + fn read_continuations_bound_utf8_lines_and_serialized_json_bytes() { + let text = format!("{}{}", "\0\"\\🦀".repeat(6000), "line\n".repeat(700)); + let f = Fixture::new(&[&text]); + let mut cursor = None; + let mut reconstructed = String::new(); + loop { + let response = f.read(cursor); + assert!(fits(&response)); + match response { + SubjektivSessionBackendResponse::Ok { + result: SubjektivSessionBackendResult::Read(page), + } => { + assert!(page.content.len() <= SUBJEKTIV_SESSION_MAX_READ_CONTENT_BYTES); + assert!(page.content.matches('\n').count() <= MAX_LINES); + assert!(!page.content.is_empty()); + reconstructed.push_str(&page.content); + if !page.has_more { + assert!(page.next_cursor.is_none()); + break; + } + assert!(page.truncated); + cursor = page.next_cursor; + } + response => panic!("{response:?}"), + } + } + assert_eq!(reconstructed, text); + } + + #[test] + fn read_and_search_cursor_generation_changes_are_explicit() { + let text = "long".repeat(6000); + let f = Fixture::new(&[&text, "second"]); + let read_cursor = match f.read(None) { + SubjektivSessionBackendResponse::Ok { + result: SubjektivSessionBackendResult::Read(page), + } => page.next_cursor.unwrap(), + response => panic!("{response:?}"), + }; + let search_cursor = match f.search(None, 1) { + SubjektivSessionBackendResponse::Ok { + result: SubjektivSessionBackendResult::Search(page), + } => page.next_cursor.unwrap(), + response => panic!("{response:?}"), + }; + let mut index: SessionPublicIndex = + serde_json::from_slice(&std::fs::read(&f.index_path).unwrap()).unwrap(); + index.generation = "new-generation".into(); + std::fs::write(&f.index_path, serde_json::to_vec(&index).unwrap()).unwrap(); + expect_error( + f.read(Some(read_cursor)), + SubjektivSessionDiagnosticCode::StaleCursor, + ); + expect_error( + f.search(Some(search_cursor), 1), + SubjektivSessionDiagnosticCode::StaleCursor, + ); + } + + #[test] + fn missing_and_corrupt_sessions_have_explicit_diagnostics_and_partial_search() { + let f = Fixture::new(&["public"]); + std::fs::remove_file(&f.index_path).unwrap(); + expect_error( + f.read(None), + SubjektivSessionDiagnosticCode::RetentionMissing, + ); + match f.search(None, 20) { + SubjektivSessionBackendResponse::Ok { + result: SubjektivSessionBackendResult::Search(page), + } => { + assert!(page.items.is_empty()); + assert_eq!(page.coverage, SubjektivSessionCoverage::Partial); + assert_eq!( + page.issues[0].code, + SubjektivSessionDiagnosticCode::RetentionMissing + ); + } + response => panic!("{response:?}"), + } + std::fs::write(&f.index_path, b"corrupt").unwrap(); + expect_error(f.read(None), SubjektivSessionDiagnosticCode::CorruptLog); + expect_error( + f.execute(SubjektivSessionBackendOperation::Read( + SubjektivSessionReadRequest { + session_id: "known-but-absent".into(), + segment_id: f.segment.clone(), + entry_ref: "E0".into(), + mode: SubjektivSessionReadMode::Full, + cursor: None, + }, + )), + SubjektivSessionDiagnosticCode::NotFoundOrNotAuthorized, + ); + } + + fn worker_tree(root: &Path) -> Vec<(PathBuf, Vec, std::time::SystemTime)> { + fn collect( + root: &Path, + path: &Path, + output: &mut Vec<(PathBuf, Vec, std::time::SystemTime)>, + ) { + let metadata = std::fs::metadata(path).unwrap(); + if metadata.is_dir() { + output.push(( + path.strip_prefix(root).unwrap().into(), + Vec::new(), + metadata.modified().unwrap(), + )); + for entry in std::fs::read_dir(path).unwrap() { + collect(root, &entry.unwrap().path(), output); + } + } else { + output.push(( + path.strip_prefix(root).unwrap().into(), + std::fs::read(path).unwrap(), + metadata.modified().unwrap(), + )); + } + } + let mut output = Vec::new(); + collect(root, root, &mut output); + output.sort_by(|a, b| a.0.cmp(&b.0)); + output + } + + #[test] + fn discovery_keyset_pagination_is_bounded_and_excludes_later_attributions() { + let f = Fixture::new(&["public"]); + let mut expected = std::collections::BTreeSet::from([f.session.to_string()]); + for _ in 0..137 { + let session = SessionId::now_v7().to_string(); + let mut attribution = SubjectSessionAttribution::new( + &f.subject, + "local-scope-a", + f.worker.to_string(), + &session, + ) + .unwrap(); + attribution.attributed_at = "2020-01-01T00:00:00.000Z".into(); + f.store.record_session_attribution(attribution).unwrap(); + expected.insert(session); + } + let before = worker_tree(&f.temp.path().join("workers")); + let mut cursor = None; + let mut found = Vec::new(); + loop { + let response = f.list(cursor, 100); + assert!(fits(&response)); + match response { + SubjektivSessionBackendResponse::Ok { + result: SubjektivSessionBackendResult::List(page), + } => { + assert!(page.items.len() <= 100); + found.extend(page.items.into_iter().map(|item| item.session_id)); + if !page.has_more { + assert!(page.next_cursor.is_none()); + break; + } + if cursor_is_first(&found) { + let mut later = SubjectSessionAttribution::new( + &f.subject, + "local-scope-a", + f.worker.to_string(), + SessionId::now_v7().to_string(), + ) + .unwrap(); + later.attributed_at = "2099-01-01T00:00:00.000Z".into(); + f.store.record_session_attribution(later).unwrap(); + } + cursor = page.next_cursor; + } + response => panic!("{response:?}"), + } + } + assert_eq!(found.len(), expected.len()); + assert_eq!( + found.into_iter().collect::>(), + expected + ); + assert_eq!(before, worker_tree(&f.temp.path().join("workers"))); + + fn cursor_is_first(found: &[String]) -> bool { + found.len() <= 100 + } + } + + #[test] + fn missing_state_root_observation_never_creates_a_directory() { + let f = Fixture::new(&["public"]); + let absent = f.temp.path().join("absent-state-root"); + expect_error( + execute( + &absent, + "local-scope-a", + &f.subject, + &f.store, + SubjektivSessionBackendOperation::Read(SubjektivSessionReadRequest { + session_id: f.session.to_string(), + segment_id: f.segment.clone(), + entry_ref: "E0".into(), + mode: SubjektivSessionReadMode::Full, + cursor: None, + }), + ) + .unwrap(), + SubjektivSessionDiagnosticCode::RetentionMissing, + ); + assert!(!absent.exists()); + } + + #[test] + fn real_fs_projection_hides_system_reasoning_and_unfinished_or_partial_records() { + use session_store::{ + LogEntry, LoggedContentPart, LoggedHistoryEntry, LoggedItem, LoggedRole, + LoggedSessionHistoryEntryId, LoggedSessionHistoryMetadata, LoggedSessionHistoryOrigin, + Store, + }; + use std::io::Write; + + let f = Fixture::new(&[]); + let history = |id: &str, item: LoggedItem| LoggedHistoryEntry { + item, + metadata: LoggedSessionHistoryMetadata { + entry_id: LoggedSessionHistoryEntryId(id.into()), + origin: LoggedSessionHistoryOrigin::BackendInstruction { operation_id: None }, + derivation: None, + }, + }; + let message = |id: &str, role: LoggedRole, text: &str| { + history( + id, + LoggedItem::Message { + role, + content: vec![LoggedContentPart::Text { text: text.into() }], + }, + ) + }; + let sessions = f.index_path.parent().unwrap(); + let fs_store = session_store::FsStore::new(sessions).unwrap(); + let segment: SessionId = f.segment.parse().unwrap(); + fs_store + .create_segment( + f.session, + segment, + &[ + LogEntry::AnnotatedSegmentStart { + ts: 1, + session_id: f.session, + system_prompt: Some("hidden-system-prompt".into()), + config: Default::default(), + forked_from: None, + compacted_from: None, + history: vec![ + message("public", LoggedRole::User, "visible committed input"), + message("system", LoggedRole::System, "hidden-system-message"), + history( + "reasoning", + LoggedItem::Reasoning { + text: "hidden-reasoning".into(), + summary: vec!["hidden-reasoning-summary".into()], + encrypted_content: None, + signature: None, + }, + ), + ], + }, + LogEntry::Invoke { + ts: 2, + trigger: protocol::InvokeKind::UserSend, + }, + LogEntry::AnnotatedAssistantItem { + ts: 3, + entry: message( + "unfinished", + LoggedRole::Assistant, + "hidden-unfinished-output", + ), + }, + ], + ) + .unwrap(); + let path = sessions + .join(f.session.to_string()) + .join(format!("{segment}.jsonl")); + let mut log = std::fs::OpenOptions::new() + .append(true) + .open(&path) + .unwrap(); + // A valid JSON record without its final newline is not committed. + log.write_all( + &serde_json::to_vec(&LogEntry::RunCompleted { + ts: 4, + interrupted: false, + result: agen::EngineResult::Finished, + active_run_turn_count: None, + }) + .unwrap(), + ) + .unwrap(); + drop(log); + let operation = SubjektivSessionBackendOperation::Search(SubjektivSessionSearchRequest { + query: None, + session_id: Some(f.session.to_string()), + kind: None, + tool_name: None, + tool_part: SubjektivSessionToolPart::Both, + limit: None, + cursor: None, + }); + let before = worker_tree(&f.temp.path().join("workers")); + let response = execute( + &f.temp.path().join("workers"), + "local-scope-a", + &f.subject, + &f.store, + operation.clone(), + ) + .unwrap(); + let json = serde_json::to_string(&response).unwrap(); + assert!(json.contains("visible committed input")); + assert!(!json.contains("hidden-")); + match response { + SubjektivSessionBackendResponse::Ok { + result: SubjektivSessionBackendResult::Search(page), + } => { + assert_eq!(page.items.len(), 1); + assert!(page.issues.is_empty()); + } + response => panic!("{response:?}"), + } + let read = execute( + &f.temp.path().join("workers"), + "local-scope-a", + &f.subject, + &f.store, + SubjektivSessionBackendOperation::Read(SubjektivSessionReadRequest { + session_id: f.session.to_string(), + segment_id: f.segment.clone(), + entry_ref: "Epublic".into(), + mode: SubjektivSessionReadMode::Full, + cursor: None, + }), + ) + .unwrap(); + assert!(matches!( + read, + SubjektivSessionBackendResponse::Ok { + result: SubjektivSessionBackendResult::Read(_) + } + )); + assert!(fits(&f.list(None, 100))); + assert_eq!(before, worker_tree(&f.temp.path().join("workers"))); + + // Commit the completion: the same shared projection now exposes only + // the public assistant output, never hidden seed content. + let mut log = std::fs::OpenOptions::new().append(true).open(path).unwrap(); + log.write_all(b"\n").unwrap(); + drop(log); + let response = execute( + &f.temp.path().join("workers"), + "local-scope-a", + &f.subject, + &f.store, + operation, + ) + .unwrap(); + let json = serde_json::to_string(&response).unwrap(); + assert!(json.contains("hidden-unfinished-output")); + assert!(!json.contains("hidden-system")); + assert!(!json.contains("hidden-reasoning")); + } + + #[test] + fn evidence_validation_uses_same_subject_authority() { + let f = Fixture::new(&["evidence"]); + let index = authorized_index_with_reader( + &f.temp.path().join("workers"), + "local-scope-a", + &f.subject, + &f.store, + &f.session.to_string(), + fixture_reader, + ) + .unwrap(); + assert_eq!(index.segments[0].entries[0].entry_ref, "E0"); + assert!( + authorized_index_with_reader( + &f.temp.path().join("workers"), + "local-scope-a", + "foreign-subject", + &f.store, + &f.session.to_string(), + fixture_reader + ) + .is_err() + ); + } +} diff --git a/crates/standalone/tests/subjektiv.rs b/crates/standalone/tests/subjektiv.rs new file mode 100644 index 000000000..b7464bc43 --- /dev/null +++ b/crates/standalone/tests/subjektiv.rs @@ -0,0 +1,1430 @@ +//! T-709: real standalone Hosts and test-owned processes, with scripted models. +//! These tests do not create a Workspace or contact a model provider. + +use std::collections::{BTreeMap, VecDeque}; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use std::pin::Pin; +use std::process::{Child, Command, Stdio}; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; + +use agen::llm_client::{ + LlmClient, + error::ClientError, + event::{Event as LlmEvent, StopReason}, + types::{Item, Request}, +}; +use async_trait::async_trait; +use feature_storage::FeatureStorage; +use futures::{Stream, stream}; +use manifest::{ProfileRegistrySource, ProfileSelector}; +use protocol::{Event, Method}; +use serde_json::{Value, json}; +use standalone::subjektiv::StandaloneSubjects; +use standalone::{ + ResolvedStandaloneLaunch, StandaloneHost, StandaloneLaunchConfig, StandaloneLaunchError, + StandaloneStartupError, StandaloneWorkerStore, +}; +use subjektiv::{SubjectStagingRecord, SubjektivStore}; + +#[derive(Clone)] +struct ScriptedClient { + responses: Arc>>>, + requests: Arc>>, + consolidate: bool, + extract: bool, + commit_guard: Option<(PathBuf, String)>, + fail_after_apply: bool, + job_ids: Arc>>, + job_started: Arc, +} + +impl ScriptedClient { + fn new(responses: Vec>) -> Self { + Self { + responses: Arc::new(Mutex::new(responses.into())), + requests: Arc::new(Mutex::new(Vec::new())), + consolidate: false, + extract: false, + commit_guard: None, + fail_after_apply: false, + job_ids: Arc::new(Mutex::new(Vec::new())), + job_started: Arc::new(tokio::sync::Notify::new()), + } + } + + fn consolidating(responses: Vec>, fail_after_apply: bool) -> Self { + Self { + consolidate: true, + fail_after_apply, + ..Self::new(responses) + } + } + + async fn first_job(&self) -> String { + tokio::time::timeout(Duration::from_secs(30), self.job_started.notified()) + .await + .expect("lifecycle must auto-start a generic Job"); + self.job_ids.lock().unwrap()[0].clone() + } + + fn job_response(&self, request: &Request) -> Result, ClientError> { + assert!( + request + .system_prompt + .as_deref() + .unwrap() + .contains("# Memory staging consolidater"), + "generic Job must render the instruction of its selected consolidation Profile" + ); + for forbidden in [ + "Read", + "Bash", + "SubWorkerSpawn", + "SubjektivMemoryRemember", + "SubjektivSessionRead", + ] { + assert!( + !request.tools.iter().any(|tool| tool.name == forbidden), + "Job received {forbidden}" + ); + } + let envelope = request + .items + .iter() + .filter_map(Item::as_text) + .find(|text| text.contains("input_json: ")) + .expect("immutable Job envelope"); + let input: Value = serde_json::from_str( + envelope + .lines() + .find_map(|line| line.strip_prefix("input_json: ")) + .unwrap(), + ) + .unwrap(); + let id = envelope + .lines() + .find_map(|line| line.strip_prefix("job_id: ")) + .unwrap() + .to_owned(); + let mut ids = self.job_ids.lock().unwrap(); + if !ids.contains(&id) { + ids.push(id); + self.job_started.notify_one(); + } + drop(ids); + let result = |call: &str| { + request.items.iter().find_map(|item| match item { + Item::ToolResult { + call_id, + content, + is_error, + summary, + .. + } if call_id == call => { + assert!(!is_error, "{call}: {summary}; {content:?}"); + Some(content.as_deref().expect("structured domain result")) + } + _ => None, + }) + }; + if let Some(Item::ToolResult { + is_error, + summary, + content, + .. + }) = request.items.iter().find( + |item| matches!(item, Item::ToolResult { call_id, .. } if call_id == "job-result"), + ) { + assert!( + !is_error, + "structured result rejected: {summary}; {content:?}" + ); + assert_eq!(summary, "Host Job result accepted"); + // SubmitJobResult intentionally returns summary-only. Durable + // structured acceptance is asserted against host.jobs().wait(). + return Ok(prose()); + } + if let Some(applied) = result("apply") { + let response: server_api::SubjektivMemoryBackendResponse = + serde_json::from_str(applied).unwrap(); + let server_api::SubjektivMemoryBackendResponse::CandidateDecided(decision) = response + else { + panic!("candidate apply rejected: {applied}"); + }; + assert_eq!( + decision.action, + server_api::SubjektivMemoryCandidateResolutionAction::Applied + ); + assert_eq!(decision.affected_memory.len(), 1); + if self.fail_after_apply { + return Err(ClientError::Config( + "simulated model transport loss after durable candidate apply".into(), + )); + } + return Ok(tool( + "job-result", + "SubmitJobResult", + json!({"result":input}), + )); + } + if let Some(candidate) = result("candidate") { + let response: server_api::SubjektivMemoryBackendResponse = + serde_json::from_str(candidate).unwrap(); + let server_api::SubjektivMemoryBackendResponse::Candidate(candidate) = response else { + panic!("candidate read rejected: {candidate}"); + }; + assert_eq!(input["candidate_ids"], json!([candidate.candidate_id])); + assert!( + !candidate.evidence.is_empty(), + "Job must receive committed provenance" + ); + return Ok(tool( + "apply", + "MemoryApplyCandidate", + json!({ + "request_id":format!("apply:{}", candidate.candidate_id), + "candidate_id":candidate.candidate_id, "reason":"Adopt the committed lesson", + "decision":{"kind":"apply", "target":{"kind":"create"}, "memory":{ + "kind":candidate.kind, "state":"active", "claim":candidate.claim, + "body_md":candidate.claim, "why_useful":candidate.why_useful, + "change_reason":"Adopt committed standalone evidence" + }} + }), + )); + } + if let Some(list) = result("candidates") { + let response: server_api::SubjektivMemoryBackendResponse = + serde_json::from_str(list).unwrap(); + let server_api::SubjektivMemoryBackendResponse::Candidates(list) = response else { + panic!("candidate list rejected: {list}"); + }; + assert_eq!( + input["candidate_ids"], + json!( + list.items + .iter() + .map(|c| &c.candidate_id) + .collect::>() + ) + ); + if let Some(candidate) = list.items.first() { + return Ok(tool( + "candidate", + "MemoryStagingRead", + json!({"candidate_id":candidate.candidate_id}), + )); + } + return Ok(tool( + "job-result", + "SubmitJobResult", + json!({"result":input}), + )); + } + Ok(tool("candidates", "MemoryStagingList", json!({}))) + } + + fn surface_response(&self, request: &Request) -> Vec { + assert_eq!( + request + .tools + .iter() + .map(|t| t.name.as_str()) + .collect::>(), + ["SubmitMemorySurface"] + ); + assert!( + !request.items.iter().any( + |item| matches!(item, Item::ToolCall { name, .. } if name == "MemoryApplyCandidate") + ), + "surface editing must use a clean context, not consolidation history" + ); + if let Some(Item::ToolResult { + is_error, + summary, + content, + .. + }) = request + .items + .iter() + .find(|item| matches!(item, Item::ToolResult { call_id, .. } if call_id == "surface")) + { + assert!(!is_error, "surface rejected: {summary}; {content:?}"); + assert_eq!(summary, "Accepted 1 grounded Memory surface point(s)."); + return prose(); + } + let input: Value = request + .items + .iter() + .filter_map(Item::as_text) + .filter_map(|text| serde_json::from_str::(text).ok()) + .find(|value| value.get("materials").is_some()) + .expect("bounded confirmed surface materials"); + let materials = input["materials"].as_array().unwrap(); + assert_eq!( + materials.len(), + 1, + "only the confirmed lesson belongs in this surface" + ); + let material = &materials[0]; + tool( + "surface", + "SubmitMemorySurface", + json!({"points":[{ + "body_md":format!("- {}", material["body_md"].as_str().unwrap()), + "memory_refs":[{"memory_id":material["memory_id"], "revision":material["revision"]}] + }]}), + ) + } + + fn extract_response(&self, request: &Request) -> Vec { + assert!(self.extract, "unexpected extract request"); + for forbidden in [ + "Bash", + "SubWorkerSpawn", + "MemoryApplyCandidate", + "SubmitJobResult", + "SubjektivMemoryRemember", + ] { + assert!( + !request.tools.iter().any(|tool| tool.name == forbidden), + "extract received {forbidden}" + ); + } + let result = |id: &str| { + request.items.iter().find_map(|item| match item { + Item::ToolResult { + call_id, + is_error, + summary, + .. + } if call_id == id => { + assert!(!is_error, "extract {id}: {summary}"); + Some(summary) + } + _ => None, + }) + }; + if result("extract-finish").is_some() { + return prose(); + } + if result("extract-stage").is_some() { + return tool( + "extract-finish", + "FinishMemoryExtraction", + json!({"staged_count":1}), + ); + } + let input = request + .items + .iter() + .filter_map(Item::as_text) + .find(|text| text.contains("# Initial session entry index")) + .expect("committed extract view"); + let entry = input + .lines() + .filter(|line| line.contains("A useful lesson is worth retaining across Workers.")) + .find_map(|line| { + line.strip_prefix("- [").and_then(|line| { + let (entry, rest) = line.split_once(' ')?; + (rest.starts_with("user ") || rest.starts_with("user]")).then_some(entry) + }) + }) + .expect("user input reference"); + tool( + "extract-stage", + "StageMemoryCandidate", + json!({ + "kind":"lesson", "claim":"Extract a committed lesson through the normal lifecycle", + "why_useful":"Normal extraction must retain Host-validated provenance", "entry_refs":[entry] + }), + ) + } + + fn requests(&self) -> Vec { + self.requests.lock().unwrap().clone() + } +} + +#[async_trait] +impl LlmClient for ScriptedClient { + async fn stream( + &self, + request: Request, + ) -> Result> + Send>>, ClientError> + { + // Lifecycle-triggered consolidation uses a separate Internal Worker. It + // must not consume the dialog script or silently become a live request. + let job = request + .tools + .iter() + .any(|tool| tool.name == "SubmitJobResult"); + self.requests.lock().unwrap().push(request.clone()); + if let Some((state, subject)) = &self.commit_guard { + if request + .items + .iter() + .any(|item| matches!(item,Item::ToolResult {call_id,..} if call_id=="remember")) + { + inspect_store(state, |store| { + assert!( + store + .pending_staging_candidates(subject, 100) + .unwrap() + .is_empty(), + "the open Run must not yet publish an explicit candidate" + ) + }); + } + } + let response = if job { + if !self.consolidate { + return Err(ClientError::Config("unscripted background Job".into())); + } + self.job_response(&request)? + } else if request + .tools + .iter() + .any(|tool| tool.name == "StageMemoryCandidate") + { + self.extract_response(&request) + } else if request + .tools + .iter() + .any(|tool| tool.name == "SubmitMemorySurface") + { + assert!(self.consolidate, "unexpected surface model invocation"); + self.surface_response(&request) + } else { + self.responses + .lock() + .unwrap() + .pop_front() + .expect("unexpected model request: dialog script exhausted") + }; + Ok(Box::pin(stream::iter(response.into_iter().map(Ok)))) + } + + fn clone_boxed(&self) -> Box { + Box::new(self.clone()) + } +} + +fn prose() -> Vec { + vec![ + LlmEvent::text_block_start(0), + LlmEvent::text_delta(0, "scripted answer"), + LlmEvent::text_block_stop(0, Some(StopReason::EndTurn)), + ] +} + +fn tool(id: &str, name: &str, input: Value) -> Vec { + vec![ + LlmEvent::tool_use_start(0, id, name), + LlmEvent::tool_input_delta(0, input.to_string()), + LlmEvent::tool_use_stop(0), + ] +} + +fn launch(cwd: &Path, state: &Path, subject: &str, name: &str) -> ResolvedStandaloneLaunch { + StandaloneLaunchConfig::new( + cwd, + state, + ProfileSelector::source_named(ProfileRegistrySource::Builtin, "standalone-subjektiv"), + name, + ) + .with_subject(subject.to_owned()) + .resolve() + .expect("resolve selected Subject launch") +} + +async fn run(host: &StandaloneHost, text: &str) -> BTreeMap { + let mut client = host.connect(); + client + .send(&Method::submit_text( + protocol::new_submission_request_id(), + text, + )) + .await + .expect("submit dialog input"); + let mut event_trace = Vec::new(); + let outcome = tokio::time::timeout(Duration::from_secs(30), async { + let mut results = BTreeMap::new(); + let mut answered = false; + loop { + let event = client + .next_event() + .await + .expect("protocol stream") + .expect("event"); + if event_trace.len() < 100 { + event_trace.push(format!("{event:?}")); + } + match event { + Event::ToolResult { + id, + output, + summary, + is_error, + .. + } => { + assert!(!is_error, "{id}: {summary}; {output:?}"); + let value = serde_json::from_str(&output.expect("structured tool output")) + .expect("tool output JSON"); + results.insert(id, value); + } + Event::Error { code, message } => { + panic!("dialog {text:?} failed: {code:?}: {message}") + } + Event::TextDelta { text } if text.contains("scripted answer") => answered = true, + Event::RunEnd { .. } => { + assert!( + answered, + "a failed/interrupted Run is not successful execution" + ); + return results; + } + _ => {} + } + } + }) + .await; + outcome.unwrap_or_else(|error| { + panic!("dialog {text:?} did not complete: {error}; events: {event_trace:#?}") + }) +} + +// The common typed store is only an assertion oracle here. Mutations under +// test always go through operator admin or the Host/model capability. +fn inspect_store(state: &Path, inspect: impl FnOnce(&SubjektivStore) -> T) -> T { + let root = state.join("subjektiv"); + let scope_id: String = + serde_json::from_slice(&std::fs::read(root.join("scope.json")).unwrap()).unwrap(); + let manager = FeatureStorage::new(root.join("features")); + let scope = manager.scope(&scope_id).unwrap(); + let registration = SubjektivStore::register(&scope).unwrap(); + let store = SubjektivStore::open(&scope, ®istration).unwrap(); + let result = inspect(&store); + drop(store); + manager.shutdown().unwrap(); + result +} + +#[test] +fn operator_subject_catalog_survives_close_and_reopen() { + let temp = tempfile::tempdir().unwrap(); + let state = temp.path().join("state"); + let catalog = StandaloneSubjects::open(&state).unwrap(); + assert!(catalog.list().unwrap().is_empty()); + let first = catalog + .create("coding", Some("Keep precise evidence.")) + .unwrap(); + let second = catalog.create("coding", None).unwrap(); + assert_ne!( + first.id, second.id, + "role/Profile/cwd must not derive Subject identity" + ); + uuid::Uuid::parse_str( + first + .id + .strip_prefix("subject-") + .expect("common Subject issuer"), + ) + .unwrap(); + uuid::Uuid::parse_str( + second + .id + .strip_prefix("subject-") + .expect("common Subject issuer"), + ) + .unwrap(); + drop(catalog); + let reopened = StandaloneSubjects::open(&state).unwrap(); + let items = reopened.list().unwrap(); + assert_eq!(items.len(), 2); + assert_eq!(items.iter().find(|s| s.id == first.id).unwrap(), &first); + assert_eq!(items.iter().find(|s| s.id == second.id).unwrap(), &second); +} + +#[tokio::test] +async fn enabled_profile_without_subject_is_inert_and_disabled_selection_is_rejected() { + let temp = tempfile::tempdir().unwrap(); + for profile in ["standalone-subjektiv", "standalone"] { + let state = temp.path().join(profile); + let launch = StandaloneLaunchConfig::new( + temp.path(), + &state, + ProfileSelector::source_named(ProfileRegistrySource::Builtin, profile), + "unattached", + ) + .resolve() + .unwrap(); + let model = ScriptedClient::new(vec![prose()]); + let host = StandaloneHost::start_with_model_client(launch, model.clone()) + .await + .unwrap(); + assert!(host.record().subject.is_none()); + run( + &host, + "An unattached policy must not manufacture a Subject.", + ) + .await; + let request = &model.requests()[0]; + assert!(request.tools.iter().any(|t| t.name == "Read")); + assert!( + !request + .tools + .iter() + .any(|t| t.name.starts_with("Subjektiv")) + ); + assert!(host.jobs().pending().unwrap().is_empty()); + host.shutdown().await.unwrap(); + assert!( + !state.join("subjektiv").exists(), + "policy alone must have no Subject storage side effect" + ); + } + let state = temp.path().join("rejected"); + let selected = uuid::Uuid::now_v7().to_string(); + let error = StandaloneLaunchConfig::new( + temp.path(), + &state, + ProfileSelector::source_named(ProfileRegistrySource::Builtin, "standalone"), + "disabled", + ) + .with_subject(selected) + .resolve() + .err() + .expect("disabled selection rejected"); + assert_eq!(error, StandaloneLaunchError::InvalidSubjectPolicy); + assert!(!state.exists(), "admission rejection precedes persistence"); +} + +#[tokio::test] +async fn explicit_remember_is_committed_staging_and_sessions_follow_subject_across_workers() { + let temp = tempfile::tempdir().unwrap(); + let state = temp.path().join("state"); + let catalog = StandaloneSubjects::open(&state).unwrap(); + let subject = catalog + .create("coding", Some("Keep committed references.")) + .unwrap(); + let foreign = catalog.create("other", None).unwrap(); + drop(catalog); + let claim = "Use immutable committed references for test evidence"; + let mut model = ScriptedClient::new(vec![ + tool( + "remember", + "SubjektivMemoryRemember", + json!({ + "kind":"lesson", "claim":claim, "why_useful":"Avoid uncommitted evidence" + }), + ), + tool("query", "SubjektivMemoryQuery", json!({"query":claim})), + prose(), + ]); + model.commit_guard = Some((state.clone(), subject.id.clone())); + let host = StandaloneHost::start_with_model_client( + launch(temp.path(), &state, &subject.id, "first-body"), + model.clone(), + ) + .await + .unwrap(); + let binding = host.record().subject.clone().unwrap(); + let worker_id = host.worker_id(); + let session_id = host.record().active_session_id; + assert_eq!(binding.subject_id, subject.id); + assert!( + host.record() + .manifest + .feature + .subjektiv + .workspace_settings + .is_none() + ); + let results = run( + &host, + "Remember this lesson using committed model-origin evidence.", + ) + .await; + assert_eq!(results["remember"]["status"], "pending_commit"); + assert_eq!( + results["query"]["items"], + json!([]), + "remember never writes confirmed Memory" + ); + let request = &model.requests()[0]; + for name in [ + "SubjektivMemoryRemember", + "SubjektivMemoryQuery", + "SubjektivSessionSearch", + "SubjektivSessionRead", + ] { + assert!( + request.tools.iter().any(|t| t.name == name), + "missing {name}" + ); + } + assert!( + !request + .tools + .iter() + .any(|t| t.name == "MemoryApplyCandidate"), + "body must not receive consolidation authority" + ); + host.shutdown().await.unwrap(); + + let staged: SubjectStagingRecord = inspect_store(&state, |store| { + assert!(store.list_memories(&subject.id).unwrap().is_empty()); + let candidates = store.pending_staging_candidates(&subject.id, 100).unwrap(); + assert_eq!( + candidates.len(), + 1, + "committed remember must become staging, not disappear" + ); + let attribution = store + .session_attribution(&session_id.to_string()) + .unwrap() + .unwrap(); + assert_eq!(attribution.subject_id, subject.id); + assert_eq!(attribution.runtime_id, binding.scope_id); + assert_eq!(attribution.worker_id, worker_id.to_string()); + candidates.into_iter().next().unwrap() + }); + assert_eq!(staged.claim, claim); + assert_eq!(staged.subject_id, subject.id); + assert!(!staged.evidence.is_empty()); + assert_eq!(staged.evidence.len(), staged.source_refs.len()); + let workers = StandaloneWorkerStore::open(&state).unwrap(); + let index = session_store::public_index::read_fs_session_public_index( + &workers.sessions_dir(worker_id), + session_id, + Default::default(), + ) + .unwrap(); + for evidence in &staged.evidence { + let source = staged + .source_refs + .iter() + .find(|s| s.evidence_id.as_deref() == Some(&evidence.id)) + .unwrap(); + assert_eq!( + source.session_id.as_deref(), + Some(session_id.to_string().as_str()) + ); + let entry = index + .segments + .iter() + .filter(|segment| source.segment_id.as_deref() == Some(segment.segment_id.as_str())) + .flat_map(|segment| &segment.entries) + .find(|entry| entry.entry_ref == evidence.id) + .expect("exact committed public reference"); + assert!(entry.compact_text.contains(claim)); + let [start, end] = evidence.entry_range.expect("committed range"); + assert!( + start <= end, + "committed source ranges use inclusive endpoints" + ); + assert_eq!(source.entry_range, evidence.entry_range); + let origin = evidence.origin.as_ref().unwrap(); + assert_eq!(origin.kind, memory::schema::EvidenceOriginKind::ModelOutput); + assert!( + origin.runtime_id.is_none(), + "local scope is not a Runtime identity" + ); + assert_eq!( + origin.worker_id.as_deref(), + Some(worker_id.to_string().as_str()) + ); + assert!(origin.workspace_id.is_none()); + assert!(origin.account_id.is_none()); + assert_eq!(source.origin.as_ref(), Some(origin)); + } + let source = &staged.source_refs[0]; + let read_input = json!({ + "session_id":session_id.to_string(), "segment_id":source.segment_id, + "entry_ref":source.evidence_id, + }); + let restored_model = ScriptedClient::new(vec![ + tool("recall", "SubjektivSessionRead", read_input.clone()), + tool("query-restored", "SubjektivMemoryQuery", json!({})), + prose(), + ]); + let restored = + StandaloneHost::restore_with_model_client(state.clone(), worker_id, restored_model.clone()) + .await + .unwrap(); + assert_eq!(restored.record().subject.as_ref(), Some(&binding)); + assert_eq!(restored.record().active_session_id, session_id); + let results = run(&restored, "Resume and recall the committed evidence.").await; + assert_eq!(results["recall"]["status"], "ok"); + assert!(results["recall"].to_string().contains(claim)); + assert_eq!(results["query-restored"]["items"], json!([])); + assert!(format!("{:?}", restored_model.requests()[0].items).contains("Remember this lesson")); + restored.shutdown().await.unwrap(); + + let second_model = ScriptedClient::new(vec![ + tool("prior-session", "SubjektivSessionRead", read_input.clone()), + tool("sessions", "SubjektivSessionList", json!({})), + prose(), + ]); + let second = StandaloneHost::start_with_model_client( + launch(temp.path(), &state, &subject.id, "second-body"), + second_model, + ) + .await + .unwrap(); + assert_ne!(second.worker_id(), worker_id); + assert_eq!(second.record().subject.as_ref(), Some(&binding)); + let second_session = second.record().active_session_id; + let results = run( + &second, + "Recall from another Worker, without a new Memory candidate.", + ) + .await; + assert_eq!(results["prior-session"]["status"], "ok"); + assert!(results["prior-session"].to_string().contains(claim)); + let sessions = results["sessions"].to_string(); + assert!(sessions.contains(&session_id.to_string())); + assert!( + sessions.contains(&second_session.to_string()), + "Sessions with no candidates must be attributed too" + ); + second.shutdown().await.unwrap(); + inspect_store(&state, |store| { + assert_eq!( + serde_json::to_value(store.pending_staging_candidates(&subject.id, 100).unwrap()) + .unwrap(), + json!([staged]) + ); + assert_eq!( + store + .session_attribution(&second_session.to_string()) + .unwrap() + .unwrap() + .subject_id, + subject.id + ); + }); + + let other = StandaloneHost::start_with_model_client( + launch(temp.path(), &state, &foreign.id, "foreign-body"), + ScriptedClient::new(vec![ + tool("foreign", "SubjektivSessionRead", read_input), + prose(), + ]), + ) + .await + .unwrap(); + let results = run( + &other, + "A known foreign Session reference is not authority.", + ) + .await; + assert_eq!(results["foreign"]["status"], "error"); + assert!(!results["foreign"].to_string().contains(claim)); + other.shutdown().await.unwrap(); +} + +#[tokio::test] +async fn restore_keeps_saved_subject_and_rejects_replaced_storage_scope() { + let temp = tempfile::tempdir().unwrap(); + let state = temp.path().join("state"); + let catalog = StandaloneSubjects::open(&state).unwrap(); + let subject = catalog.create("original", None).unwrap(); + let other = catalog.create("not-a-resume-selection", None).unwrap(); + drop(catalog); + let host = StandaloneHost::start_with_model_client( + launch(temp.path(), &state, &subject.id, "saved-binding"), + ScriptedClient::new(Vec::new()), + ) + .await + .unwrap(); + let id = host.worker_id(); + let binding = host.record().subject.clone().unwrap(); + host.shutdown().await.unwrap(); + + // Replace only the operator-managed storage root with a genuine, separately + // issued scope. The saved Worker/cwd/session remains otherwise untouched. + let foreign_state = temp.path().join("foreign-state"); + let catalog = StandaloneSubjects::open(&foreign_state).unwrap(); + catalog.create("foreign", None).unwrap(); + drop(catalog); + let original_root = temp.path().join("saved-subjektiv"); + std::fs::rename(state.join("subjektiv"), &original_root).unwrap(); + std::fs::rename(foreign_state.join("subjektiv"), state.join("subjektiv")).unwrap(); + let error = StandaloneHost::restore_with_model_client( + state.clone(), + id, + ScriptedClient::new(Vec::new()), + ) + .await + .err() + .expect("foreign scope rejected"); + assert!( + matches!(error, StandaloneStartupError::Subject(ref message) if message.contains("storage scope changed")), + "{error:?}" + ); + assert_eq!( + StandaloneWorkerStore::open(&state) + .unwrap() + .load(id) + .unwrap() + .subject + .as_ref(), + Some(&binding) + ); + std::fs::rename(state.join("subjektiv"), foreign_state.join("subjektiv")).unwrap(); + std::fs::rename(&original_root, state.join("subjektiv")).unwrap(); + let restored = + StandaloneHost::restore_with_model_client(state, id, ScriptedClient::new(Vec::new())) + .await + .unwrap(); + assert_eq!(restored.record().subject.as_ref(), Some(&binding)); + assert_ne!( + restored.record().subject.as_ref().unwrap().subject_id, + other.id + ); + restored.shutdown().await.unwrap(); +} + +// A failing test never leaves its test-owned process running. This guard can +// only address the Child returned by spawn, never a parent or dogfood Worker. +struct OwnedChild(Child); +impl Drop for OwnedChild { + fn drop(&mut self) { + if self.0.try_wait().ok().flatten().is_none() { + let _ = self.0.kill(); + } + let _ = self.0.wait(); + } +} + +fn child_command(mode: &str, cwd: &Path, state: &Path, subject: &str, ready: &Path) -> Command { + let mut command = Command::new(std::env::current_exe().unwrap()); + command + .args(["--exact", "subject_process_child", "--nocapture"]) + .env("YOI_SUBJEKTIV_TEST_CHILD_MODE", mode) + .env("YOI_SUBJEKTIV_TEST_CWD", cwd) + .env("YOI_SUBJEKTIV_TEST_STATE", state) + .env("YOI_SUBJEKTIV_TEST_SUBJECT", subject) + .env("YOI_SUBJEKTIV_TEST_READY", ready) + .stdin(Stdio::piped()); + command +} + +fn wait_child(child: &mut OwnedChild) -> std::process::ExitStatus { + let deadline = Instant::now() + Duration::from_secs(30); + loop { + if let Some(status) = child.0.try_wait().unwrap() { + return status; + } + assert!(Instant::now() < deadline, "test-owned child did not exit"); + std::thread::sleep(Duration::from_millis(10)); + } +} + +#[tokio::test] +async fn subject_process_child() { + // The ordinary harness also discovers this subprocess entry point. Only a + // parent semantic test supplies the private fixture paths and mode. + let Ok(mode) = std::env::var("YOI_SUBJEKTIV_TEST_CHILD_MODE") else { + return; + }; + let cwd = PathBuf::from(std::env::var_os("YOI_SUBJEKTIV_TEST_CWD").unwrap()); + let state = PathBuf::from(std::env::var_os("YOI_SUBJEKTIV_TEST_STATE").unwrap()); + let subject = std::env::var("YOI_SUBJEKTIV_TEST_SUBJECT").unwrap(); + let result = StandaloneHost::start_with_model_client( + launch(&cwd, &state, &subject, "test-process"), + ScriptedClient::new(Vec::new()), + ) + .await; + match mode.as_str() { + "contender" => { + let error = result + .err() + .expect("another process owns the Subject kernel lease"); + assert!( + matches!(error, StandaloneStartupError::Subject(ref message) if message.contains("already active")), + "{error:?}" + ); + } + "abnormal-owner" => { + let host = result.expect("child owns Subject"); + let ready = PathBuf::from(std::env::var_os("YOI_SUBJEKTIV_TEST_READY").unwrap()); + let mut file = std::fs::File::create(&ready).unwrap(); + file.write_all(host.worker_id().to_string().as_bytes()) + .unwrap(); + file.sync_all().unwrap(); + let mut byte = [0]; + std::io::stdin().read_exact(&mut byte).unwrap(); + assert_eq!(byte, [b'x']); + // Deliberately omit Host shutdown and all Rust destructors. The + // parent requests this exit only after proving cross-process lock. + std::process::exit(86); + } + other => panic!("unknown child mode {other}"), + } +} + +#[tokio::test] +async fn kernel_subject_lease_excludes_other_processes_and_recovers_after_abnormal_exit() { + let temp = tempfile::tempdir().unwrap(); + let state = temp.path().join("state"); + let parent_cwd = temp.path().join("parent-project"); + let child_cwd = temp.path().join("child-project"); + std::fs::create_dir(&parent_cwd).unwrap(); + std::fs::create_dir(&child_cwd).unwrap(); + let catalog = StandaloneSubjects::open(&state).unwrap(); + let subject = catalog.create("singleton", None).unwrap(); + let independent = catalog.create("independent", None).unwrap(); + drop(catalog); + let ready = temp.path().join("ready"); + let parent = StandaloneHost::start_with_model_client( + launch(&parent_cwd, &state, &subject.id, "parent-owner"), + ScriptedClient::new(Vec::new()), + ) + .await + .unwrap(); + let mut contender = OwnedChild( + child_command("contender", &child_cwd, &state, &subject.id, &ready) + .spawn() + .unwrap(), + ); + assert!( + wait_child(&mut contender).success(), + "a different cwd must not evade the Subject lock" + ); + let independent_host = StandaloneHost::start_with_model_client( + launch(&child_cwd, &state, &independent.id, "independent-owner"), + ScriptedClient::new(Vec::new()), + ) + .await + .unwrap(); + independent_host.shutdown().await.unwrap(); + parent.shutdown().await.unwrap(); + + let mut owner = OwnedChild( + child_command("abnormal-owner", &child_cwd, &state, &subject.id, &ready) + .spawn() + .unwrap(), + ); + let deadline = Instant::now() + Duration::from_secs(30); + let id = loop { + if ready.exists() { + let text = std::fs::read_to_string(&ready).unwrap(); + if let Ok(id) = text.parse::() { + break id; + } + } + assert!( + owner.0.try_wait().unwrap().is_none(), + "owner exited before ready" + ); + assert!(Instant::now() < deadline, "owner did not become ready"); + std::thread::sleep(Duration::from_millis(10)); + }; + let mut contender = OwnedChild( + child_command("contender", &parent_cwd, &state, &subject.id, &ready) + .spawn() + .unwrap(), + ); + assert!(wait_child(&mut contender).success()); + owner.0.stdin.as_mut().unwrap().write_all(b"x").unwrap(); + assert_eq!( + wait_child(&mut owner).code(), + Some(86), + "exercise process death, not graceful shutdown" + ); + let recovered = + StandaloneHost::restore_with_model_client(state, id, ScriptedClient::new(Vec::new())) + .await + .unwrap(); + assert_eq!(recovered.worker_id(), id); + assert_eq!( + recovered.record().subject.as_ref().unwrap().subject_id, + subject.id + ); + recovered.shutdown().await.unwrap(); +} + +fn remember_script(claim: &str) -> Vec> { + vec![ + tool( + "remember", + "SubjektivMemoryRemember", + json!({ + "kind":"lesson", "claim":claim, "why_useful":"Retain standalone evidence" + }), + ), + prose(), + ] +} + +#[tokio::test] +async fn automatic_generic_consolidation_applies_candidate_publishes_surface_and_follows_subject() { + let temp = tempfile::tempdir().unwrap(); + let state = temp.path().join("state"); + let catalog = StandaloneSubjects::open(&state).unwrap(); + let subject = catalog.create("consolidation", None).unwrap(); + drop(catalog); + let claim = "Validate the immutable result before reporting success"; + let model = ScriptedClient::consolidating(remember_script(claim), false); + let host = StandaloneHost::start_with_model_client( + launch(temp.path(), &state, &subject.id, "consolidation-body"), + model.clone(), + ) + .await + .unwrap(); + let worker_id = host.worker_id(); + let results = run( + &host, + "Remember this lesson and let the Host consolidate its committed candidate.", + ) + .await; + assert_eq!(results["remember"]["status"], "pending_commit"); + let job_id = model.first_job().await; + let completed = tokio::time::timeout(Duration::from_secs(30), host.jobs().wait(&job_id)) + .await + .expect("Job cleanup completes") + .unwrap(); + assert_eq!( + completed.state, + standalone::jobs::JobState::Completed, + "{completed:?}" + ); + assert_eq!( + completed.request.profile, + "builtin:standalone-subjektiv-consolidation" + ); + assert_eq!(completed.request.purpose, "subjektiv_consolidation"); + assert!( + completed.domain_grant.is_some(), + "policy is not a domain capability" + ); + let ids = completed.request.input["candidate_ids"].as_array().unwrap(); + assert_eq!(ids.len(), 1, "Job must snapshot the committed candidate"); + let accepted = completed + .result + .clone() + .expect("structured result is durable success authority"); + assert_eq!(accepted["subject_id"], subject.id); + assert_eq!(accepted["candidate_ids"], json!(ids)); + assert_eq!( + accepted["candidate_dispositions"], + json!([{"candidate_id":ids[0], "action":"applied"}]) + ); + assert_eq!(accepted["surface"]["availability"], "ready"); + assert!(accepted["surface"]["generation_id"].is_string()); + assert!(accepted["surface"]["snapshot_id"].is_string()); + // The Subject consumer may already have acknowledged successful cleanup. + // Acknowledgement timing is not success authority; the immutable accepted + // result above remains durable regardless of that scheduling race. + let requests = model.requests(); + assert!(requests.iter().any(|r| { + r.tools + .iter() + .any(|tool| tool.name == "MemoryApplyCandidate") + })); + assert!(requests.iter().any(|r| { + r.tools + .iter() + .any(|tool| tool.name == "SubmitMemorySurface") + })); + assert!( + state + .join("subjektiv/jobs") + .join(&subject.id) + .join("jobs.sqlite3") + .exists() + ); + assert!( + !state + .join(worker_id.to_string()) + .join("jobs.sqlite3") + .exists(), + "selected Subject owns the shared generic Job ledger" + ); + host.shutdown().await.unwrap(); + let memory = inspect_store(&state, |store| { + assert!( + store + .pending_staging_candidates(&subject.id, 100) + .unwrap() + .is_empty() + ); + let memory = store.list_memories(&subject.id).unwrap().pop().unwrap(); + assert_eq!(memory.claim, claim); + assert_eq!(memory.revision, 1); + assert_eq!(memory.source_candidate_ids, vec![ids[0].as_str().unwrap()]); + let surface = store.resident_surface(&subject.id).unwrap(); + assert_eq!(surface.availability, subjektiv::SurfaceAvailability::Ready); + let snapshot = surface.snapshot.unwrap(); + assert_eq!( + snapshot.id, + accepted["surface"]["snapshot_id"].as_str().unwrap() + ); + assert!(snapshot.body_md.contains(claim)); + assert_eq!( + snapshot.memory_refs, + vec![subjektiv::MemoryRevisionRef { + memory_id: memory.id.clone(), + revision: 1 + }] + ); + assert_eq!( + snapshot.built_from_store_revision, + accepted["surface"]["store_revision"].as_u64().unwrap() + ); + memory + }); + let next_model = ScriptedClient::new(vec![ + tool("confirmed", "SubjektivMemoryQuery", json!({"query":claim})), + prose(), + ]); + let next = StandaloneHost::start_with_model_client( + launch(temp.path(), &state, &subject.id, "next-body"), + next_model.clone(), + ) + .await + .unwrap(); + assert_ne!(next.worker_id(), worker_id); + let retained = next.jobs().get(&job_id).unwrap(); + assert_eq!(retained.request, completed.request); + assert_eq!(retained.state, completed.state); + assert_eq!( + retained.result, completed.result, + "result follows the Subject, not the old Worker" + ); + let results = run(&next, "Recall confirmed Memory in the next Worker.").await; + assert_eq!(results["confirmed"]["items"].as_array().unwrap().len(), 1); + assert!(results["confirmed"].to_string().contains(&memory.id)); + assert!( + next_model.requests()[0] + .system_prompt + .as_deref() + .unwrap() + .contains(claim), + "fresh resident surface must be injected at new Worker creation" + ); + assert_eq!( + next.jobs().acknowledge(&job_id).unwrap().result, + Some(accepted) + ); + next.shutdown().await.unwrap(); +} + +#[tokio::test] +async fn transport_loss_after_candidate_apply_preserves_memory_and_later_job_only_rebuilds_surface() +{ + let temp = tempfile::tempdir().unwrap(); + let state = temp.path().join("state"); + let catalog = StandaloneSubjects::open(&state).unwrap(); + let subject = catalog.create("partial-apply", None).unwrap(); + drop(catalog); + let claim = "Candidate receipts survive transport failure"; + let losing = ScriptedClient::consolidating(remember_script(claim), true); + let host = StandaloneHost::start_with_model_client( + launch(temp.path(), &state, &subject.id, "losing-body"), + losing.clone(), + ) + .await + .unwrap(); + run( + &host, + "Stage a lesson whose Job loses its transport after applying it.", + ) + .await; + let first_id = losing.first_job().await; + let failed = tokio::time::timeout(Duration::from_secs(30), host.jobs().wait(&first_id)) + .await + .unwrap() + .unwrap(); + assert_eq!( + failed.state, + standalone::jobs::JobState::Failed, + "{failed:?}" + ); + assert!( + failed.result.is_none(), + "partial apply is not an accepted Job result" + ); + host.shutdown().await.unwrap(); + let original = inspect_store(&state, |store| { + assert!( + store + .pending_staging_candidates(&subject.id, 100) + .unwrap() + .is_empty() + ); + let memory = store.list_memories(&subject.id).unwrap(); + assert_eq!(memory.len(), 1); + assert_eq!(memory[0].claim, claim); + assert_eq!(memory[0].revision, 1); + assert_ne!( + store.resident_surface(&subject.id).unwrap().availability, + subjektiv::SurfaceAvailability::Ready + ); + serde_json::to_value(&memory[0]).unwrap() + }); + let rebuilding = ScriptedClient::consolidating(vec![prose()], false); + let next = StandaloneHost::start_with_model_client( + launch(temp.path(), &state, &subject.id, "rebuild-body"), + rebuilding.clone(), + ) + .await + .unwrap(); + assert_eq!(next.jobs().get(&first_id).unwrap(), failed); + run( + &next, + "A later explicit Host interaction rebuilds the missing surface.", + ) + .await; + let second_id = rebuilding.first_job().await; + assert_ne!( + first_id, second_id, + "do not silently replay the failed immutable Job" + ); + let completed = tokio::time::timeout(Duration::from_secs(30), next.jobs().wait(&second_id)) + .await + .unwrap() + .unwrap(); + assert_eq!( + completed.state, + standalone::jobs::JobState::Completed, + "{completed:?}" + ); + assert_eq!( + completed.request.input["candidate_ids"], + json!([]), + "resolved candidates cannot be applied again" + ); + assert_eq!( + completed.result.as_ref().unwrap()["surface"]["availability"], + "ready" + ); + assert!( + !rebuilding + .requests() + .iter() + .any(|request| request.items.iter().any( + |item| matches!(item, Item::ToolCall { name, .. } if name == "MemoryApplyCandidate") + )) + ); + assert_eq!( + next.jobs().get(&first_id).unwrap(), + failed, + "later success does not rewrite the original failed outcome" + ); + next.shutdown().await.unwrap(); + inspect_store(&state, |store| { + let memories = store.list_memories(&subject.id).unwrap(); + assert_eq!(memories.len(), 1); + assert_eq!( + serde_json::to_value(&memories[0]).unwrap(), + original, + "confirmed Memory is immutable across surface repair" + ); + assert_eq!( + store.resident_surface(&subject.id).unwrap().availability, + subjektiv::SurfaceAvailability::Ready + ); + }); +} + +#[tokio::test] +async fn normal_extract_stages_committed_sources_and_consolidates_through_generic_job() { + let temp = tempfile::tempdir().unwrap(); + let state = temp.path().join("state"); + let catalog = StandaloneSubjects::open(&state).unwrap(); + let subject = catalog.create("extract", None).unwrap(); + drop(catalog); + let mut model = ScriptedClient::consolidating(vec![prose()], false); + model.extract = true; + let mut selected = launch(temp.path(), &state, &subject.id, "extract-body"); + selected + .profile + .manifest + .feature + .subjektiv + .profile + .extraction + .threshold = Some(1); + // An explicit model override selects a different provider by contract. + // Keep this mechanism test entirely on the injected scripted transport. + selected + .profile + .manifest + .feature + .subjektiv + .profile + .extraction + .model = None; + let host = StandaloneHost::start_with_model_client(selected, model.clone()) + .await + .unwrap(); + let session = host.record().active_session_id; + run(&host, "A useful lesson is worth retaining across Workers.").await; + let job_id = model.first_job().await; + let finished = tokio::time::timeout(Duration::from_secs(30), host.jobs().wait(&job_id)) + .await + .unwrap() + .unwrap(); + assert_eq!( + finished.state, + standalone::jobs::JobState::Completed, + "{finished:?}" + ); + assert_eq!( + finished.result.as_ref().unwrap()["candidate_dispositions"][0]["action"], + "applied" + ); + let worker = host.worker_id(); + host.shutdown().await.unwrap(); + inspect_store(&state, |store| { + let memory = store.list_memories(&subject.id).unwrap().pop().unwrap(); + assert_eq!( + memory.claim, + "Extract a committed lesson through the normal lifecycle" + ); + let candidate = store + .staging_candidate(&subject.id, &memory.source_candidate_ids[0]) + .unwrap() + .unwrap(); + assert_eq!( + candidate.source_refs[0].session_id, + Some(session.to_string()) + ); + assert!( + candidate + .evidence + .iter() + .all(|e| e.origin.as_ref().unwrap().kind + == memory::schema::EvidenceOriginKind::HumanInput), + "{candidate:#?}" + ); + assert_eq!( + store.resident_surface(&subject.id).unwrap().availability, + subjektiv::SurfaceAvailability::Ready + ); + }); + assert!( + model + .requests() + .iter() + .any(|r| r.tools.iter().any(|t| t.name == "StageMemoryCandidate")), + "extract must use the injected scripted transport" + ); + let log_root = state + .join(worker.to_string()) + .join("sessions") + .join(session.to_string()); + let text = std::fs::read_dir(log_root) + .unwrap() + .map(|entry| std::fs::read_to_string(entry.unwrap().path()).unwrap()) + .collect::(); + assert!( + text.contains("subjektiv.extract.v1"), + "normal extraction must durably advance the existing append-only pointer" + ); +} diff --git a/crates/subjektiv/Cargo.toml b/crates/subjektiv/Cargo.toml new file mode 100644 index 000000000..1b4fd76a8 --- /dev/null +++ b/crates/subjektiv/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "subjektiv" +version = "0.1.0" +edition.workspace = true +license.workspace = true +publish = false + +[dependencies] +feature-storage = { path = "../feature-storage" } +chrono = { version = "0.4", default-features = false, features = ["clock"] } +memory.workspace = true +manifest.workspace = true +rusqlite.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +server-api.workspace = true +thiserror.workspace = true +uuid = { workspace = true, features = ["v7"] } + +[dev-dependencies] +tempfile.workspace = true diff --git a/crates/subjektiv/README.md b/crates/subjektiv/README.md new file mode 100644 index 000000000..8f5394522 --- /dev/null +++ b/crates/subjektiv/README.md @@ -0,0 +1,76 @@ +# subjektiv + +Shared Subject-scoped Memory domain and model-facing operations. This crate has +no Workspace Server dependency. `server-api` is the existing transport-neutral +request/response contract crate, not an authentication or Server implementation. + +## Storage scope + +The trusted Host creates `feature_storage::FeatureStorage`, selects +`FeatureStorage::scope(scope_id)`, registers `REGISTRATION` (or uses +`SubjektivStore::register`), and opens `SubjektivStore::open`. A trusted managed +`FeatureDatabase` can also be opened with `SubjektivStore::from_database`. + +`scope_id` identifies the physical storage sharing boundary. Backend uses its +existing Workspace ID. Standalone uses a persisted random local storage-scope +identity under its operator-managed state directory, **not a fake Workspace**. +Subject IDs are independently issued random domain identities. An ID is a lookup +key, never an authorization credential. + +Existing SQL schema and migrations are retained. In particular, +`store_scope.workspace_id` still stores the physical storage scope, under its +historical column name. Existing Backend placement and snapshot manifest keys +are unchanged. Optional historical `EvidenceOrigin.workspace_id` values continue +to be checked against the physical scope; local provenance need not invent a +Workspace value. `workspace_id()` is a Backend compatibility alias for +`scope_id()`. + +## Host operation boundary + +`api::execute(store, &HostOperationContext, operation)` is the common dispatcher +used by Backend and local Hosts. It owns query/cursor rules, bounded body and +nested provenance projections, immutable revision reads, proposal validation, +explicit staging/receipt rules, resident context, candidate decisions and +surface generation/publication/failure projections. + +The Host context is not serializable or model input. Trusted Rust Hosts construct +it only after validating their external execution authority: + +- `HostOperationContext::validated_body` binds one Subject and optionally an + existing immutable `SubjectSessionAttribution`. Explicit staging requires the + request Session to match that attribution. +- `HostOperationContext::validated_consolidation` binds one Subject, an optional + immutable candidate batch, and an optional `JobAttemptBinding`. Attenuated Jobs + must supply `Some(batch)`, including an empty batch; `None` is legacy non-Job + Host consolidation authority. Body-only operations are unavailable to + consolidation contexts, and candidate reads/decisions cannot escape the batch. + +These constructors validate store/domain bindings, not external authentication. +Backend retains authenticated Runtime/Worker identity, current singleton and +live Job attempt checks, candidate grant checks, and public Session validation. +Local Hosts retain exclusive Subject execution leases, generic Job admission, +Worker/Session ownership, and committed public Session evidence validation. +Before explicit staging, the Host resolves evidence using its committed public +Session projection; the common API does not inspect Session files, hidden +reasoning, system prompts, or arbitrary paths. Lower-level repository/projection +functions are trusted Rust APIs, not model capabilities. + +## Common consolidation Job rules + +`job::bounded_candidate_batch(ids, max_result_bytes)` selects the exact T-704 +ordered-prefix budget (100 candidates maximum, 2,048-byte envelope, twice each +JSON-escaped ID size plus 128 bytes per disposition). +`job::consolidation_required(count, bytes, force, availability)` owns the shared +trigger thresholds and empty-ready/surface-retry rules. + +`job::validate_result(store, subject_id, batch, job_id, attempt_id, value)` checks +the strict result shape, exact admitted Subject/batch, durable dispositions, and +the persisted terminal surface outcome from that attempt. After validation, +`job::with_dispositions(store, subject_id, batch, value)` adds the deterministic +compact disposition projection. Hosts retain Job result binding/admission; these +helpers never run or replay a Job. The store's `prepare_job_surface_generation`, +`require_job_surface_generation`, and `validate_job_surface_outcome` are public +trusted-domain methods for both Backend and local Hosts. + +Dependencies: `feature-storage`, `chrono`, `memory`, `manifest`, `rusqlite`, +`serde`, `serde_json`, `server-api`, `thiserror`, `uuid`; tests use `tempfile`. diff --git a/crates/subjektiv/src/api.rs b/crates/subjektiv/src/api.rs new file mode 100644 index 000000000..cd6dc64b5 --- /dev/null +++ b/crates/subjektiv/src/api.rs @@ -0,0 +1,1316 @@ +//! Shared model-facing Memory operations and bounded projections. +//! Hosts retain authentication, execution leases, Job admission and committed +//! Session evidence validation; this module never derives ambient authority. + +mod context; +mod operation; +pub use context::{HostOperationContext, JobAttemptBinding}; +pub use operation::execute; + +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; + +pub const SUBJEKTIV_QUERY_DEFAULT_LIMIT: usize = 20; +pub const SUBJEKTIV_QUERY_MAX_LIMIT: usize = 100; +pub const SUBJEKTIV_BODY_DEFAULT_LINES: usize = 200; +pub const SUBJEKTIV_BODY_MAX_LINES: usize = 1_000; +pub const SUBJEKTIV_BODY_MAX_BYTES: usize = 16 * 1024; +pub const SUBJEKTIV_CANDIDATE_ANCHOR_LIMIT: usize = crate::MAX_STAGING_ANCHORS; +pub const SUBJEKTIV_EVIDENCE_ANCHOR_PAGE_SIZE: usize = 2; +pub const SUBJEKTIV_ANCHOR_TEXT_MAX_BYTES: usize = 64; +pub const SUBJEKTIV_NESTED_EVIDENCE_MAX_BYTES: usize = 40 * 1024; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +struct SubjektivQueryCursor { + subject_id: String, + store_revision: u64, + query: Option, + kinds: Vec, + states: Vec, + offset: usize, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +struct SubjektivRevisionCursor { + subject_id: String, + memory_id: String, + max_revision: u64, + offset: usize, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +struct SubjektivEvidenceCursor { + subject_id: String, + memory_id: String, + revision: u64, + /// Candidate/derivation reference offset within the immutable revision. + offset: usize, + /// Nested offsets keep compatible historical candidates with more anchors + /// resumable without admitting new over-limit records. + #[serde(default)] + evidence_offset: usize, + #[serde(default)] + source_ref_offset: usize, +} + +pub fn resident_summary_output( + resident: crate::ResidentSurface, +) -> memory::backend::MemoryResidentSummaryOutput { + let (availability, content) = match resident.availability { + crate::SurfaceAvailability::Ready => { + let snapshot = resident.snapshot.expect("ready surface has a snapshot"); + ( + memory::backend::MemoryResidentSummaryAvailability::Ready, + (!snapshot.body_md.is_empty()).then_some(snapshot.body_md), + ) + } + crate::SurfaceAvailability::Ungenerated => ( + memory::backend::MemoryResidentSummaryAvailability::Ungenerated, + None, + ), + crate::SurfaceAvailability::Stale => ( + memory::backend::MemoryResidentSummaryAvailability::Stale, + None, + ), + crate::SurfaceAvailability::Failed => ( + memory::backend::MemoryResidentSummaryAvailability::Failed, + None, + ), + }; + memory::backend::MemoryResidentSummaryOutput { + availability, + content, + } +} + +pub fn subjektiv_surface_prepare_response( + generation: crate::SurfaceGeneration, +) -> server_api::SubjektivSurfacePrepareResponse { + server_api::SubjektivSurfacePrepareResponse { + generation_id: generation.id, + current_snapshot_id: None, + store_revision: generation.store_revision, + active_memory_count: generation.active_memory_count, + materials: generation + .materials + .into_iter() + .map(|material| server_api::SubjektivSurfaceMaterial { + memory_id: material.memory_id, + revision: material.revision, + kind: material.kind, + body_md: material.body_md, + why_useful: material.why_useful, + staleness: material.staleness, + }) + .collect(), + body_token_budget: crate::SURFACE_BODY_TOKEN_BUDGET, + input_token_budget: crate::SURFACE_INPUT_TOKEN_BUDGET, + per_kind_limit: crate::SURFACE_PER_KIND_LIMIT, + total_material_limit: crate::SURFACE_TOTAL_MATERIAL_LIMIT, + } +} + +pub fn subjektiv_candidate_summary( + candidate: crate::SubjectStagingRecord, +) -> server_api::SubjektivMemoryCandidateSummary { + server_api::SubjektivMemoryCandidateSummary { + candidate_id: candidate.id, + kind: candidate.kind, + claim: candidate.claim, + revision_proposal: candidate.revision_proposal.map(subjektiv_proposal_metadata), + created_at: candidate.created_at, + } +} + +pub fn subjektiv_candidate( + candidate: crate::SubjectStagingRecord, +) -> server_api::SubjektivMemoryCandidate { + server_api::SubjektivMemoryCandidate { + candidate_id: candidate.id, + kind: candidate.kind, + claim: candidate.claim, + why_useful: candidate.why_useful, + staleness: candidate.staleness, + source: candidate.source, + evidence: candidate.evidence, + source_refs: candidate.source_refs, + revision_proposal: candidate.revision_proposal.map(subjektiv_proposal_metadata), + created_at: candidate.created_at, + } +} + +pub fn subjektiv_proposal_metadata( + proposal: crate::RevisionProposal, +) -> server_api::SubjektivMemoryRevisionProposalMetadata { + server_api::SubjektivMemoryRevisionProposalMetadata { + memory_id: proposal.memory_id, + expected_revision: proposal.expected_revision, + intent: match proposal.intent { + crate::RevisionProposalIntent::Revise => { + server_api::SubjektivMemoryRevisionIntent::Revise + } + crate::RevisionProposalIntent::Resolve => { + server_api::SubjektivMemoryRevisionIntent::Resolve + } + crate::RevisionProposalIntent::Retract => { + server_api::SubjektivMemoryRevisionIntent::Retract + } + crate::RevisionProposalIntent::Reopen => { + server_api::SubjektivMemoryRevisionIntent::Reopen + } + }, + change_reason: proposal.change_reason, + } +} + +pub fn subjektiv_candidate_decision( + input: server_api::SubjektivMemoryCandidateDecisionRequest, +) -> OperationResult { + let decision = match input.decision { + server_api::SubjektivMemoryCandidateDecision::Apply { target, memory } => { + let target = match target { + server_api::SubjektivMemoryApplyTarget::Create => { + crate::MemoryRevisionTarget::Create + } + server_api::SubjektivMemoryApplyTarget::Revise { + memory_id, + expected_revision, + } => crate::MemoryRevisionTarget::Revise { + memory_id, + expected_revision, + }, + }; + crate::CandidateDecision::Apply { + target, + draft: crate::MemoryDraft { + kind: memory.kind, + state: domain_memory_state(memory.state), + claim: memory.claim, + body_md: memory.body_md, + why_useful: memory.why_useful, + staleness: memory.staleness, + source_candidate_ids: Vec::new(), + derived_from: memory + .derived_from + .into_iter() + .map(|reference| crate::MemoryRevisionRef { + memory_id: reference.memory_id, + revision: reference.revision, + }) + .collect(), + change_reason: memory.change_reason, + }, + } + } + server_api::SubjektivMemoryCandidateDecision::Close { + action, + affected_memory, + } => crate::CandidateDecision::Close { + action: match action { + server_api::SubjektivMemoryCandidateCloseAction::Discarded => { + crate::StagingResolutionAction::Discarded + } + server_api::SubjektivMemoryCandidateCloseAction::Invalid => { + crate::StagingResolutionAction::Invalid + } + server_api::SubjektivMemoryCandidateCloseAction::Duplicate => { + crate::StagingResolutionAction::Duplicate + } + server_api::SubjektivMemoryCandidateCloseAction::AlreadyCovered => { + crate::StagingResolutionAction::AlreadyCovered + } + }, + affected_memory: affected_memory + .into_iter() + .map(|reference| crate::MemoryRevisionRef { + memory_id: reference.memory_id, + revision: reference.revision, + }) + .collect(), + }, + }; + Ok(crate::CandidateDecisionRequest { + request_id: input.request_id, + candidate_id: input.candidate_id, + reason: input.reason, + decision, + }) +} + +pub fn subjektiv_candidate_decision_response( + receipt: crate::CandidateDecisionReceipt, +) -> server_api::SubjektivMemoryCandidateDecisionResponse { + let operation = match receipt.operation { + Some(crate::MemoryDecisionOperation::Create) => { + server_api::SubjektivMemoryAffectedOperation::Create + } + Some(crate::MemoryDecisionOperation::Revise) => { + server_api::SubjektivMemoryAffectedOperation::Revise + } + Some(crate::MemoryDecisionOperation::Reference) | None => { + server_api::SubjektivMemoryAffectedOperation::Reference + } + }; + server_api::SubjektivMemoryCandidateDecisionResponse { + request_id: receipt.request_id, + candidate_id: receipt.candidate_id, + action: match receipt.resolution.action { + crate::StagingResolutionAction::Applied => { + server_api::SubjektivMemoryCandidateResolutionAction::Applied + } + crate::StagingResolutionAction::Discarded => { + server_api::SubjektivMemoryCandidateResolutionAction::Discarded + } + crate::StagingResolutionAction::Invalid => { + server_api::SubjektivMemoryCandidateResolutionAction::Invalid + } + crate::StagingResolutionAction::Duplicate => { + server_api::SubjektivMemoryCandidateResolutionAction::Duplicate + } + crate::StagingResolutionAction::AlreadyCovered => { + server_api::SubjektivMemoryCandidateResolutionAction::AlreadyCovered + } + }, + reason: receipt.resolution.reason, + affected_memory: receipt + .resolution + .affected_memory + .into_iter() + .map(|reference| server_api::SubjektivMemoryAffectedRef { + memory_id: reference.memory_id, + revision: reference.revision, + operation, + }) + .collect(), + memory: receipt + .memory + .map(|memory| server_api::SubjektivMemoryRevisionRef { + memory_id: memory.id, + revision: memory.revision, + }), + store_revision: receipt.store_revision, + surface_dirty: receipt.surface_dirty, + } +} + +pub fn domain_memory_state(state: server_api::SubjektivMemoryState) -> crate::MemoryState { + match state { + server_api::SubjektivMemoryState::Active => crate::MemoryState::Active, + server_api::SubjektivMemoryState::Resolved => crate::MemoryState::Resolved, + server_api::SubjektivMemoryState::Retracted => crate::MemoryState::Retracted, + } +} + +pub fn subjektiv_memory_query( + store: &crate::SubjektivStore, + subject_id: &str, + input: server_api::SubjektivMemoryQueryRequest, +) -> OperationResult { + let limit = bounded_subjektiv_limit(input.limit, SUBJEKTIV_QUERY_DEFAULT_LIMIT)?; + let query = input + .query + .map(|value| value.trim().to_lowercase()) + .filter(|value| !value.is_empty()); + let kinds = canonical_subjektiv_kinds(input.kinds)?; + let states = canonical_subjektiv_states(input.states)?; + let subject = store + .subject(subject_id) + .map_err(subjektiv_store_error)? + .ok_or_else(|| OperationError::SubjectNotFound(subject_id.to_string()))?; + let mut offset = 0; + if let Some(cursor) = input.cursor { + let cursor: SubjektivQueryCursor = decode_subjektiv_cursor("query", &cursor)?; + if cursor.subject_id != subject_id + || cursor.query != query + || cursor.kinds != kinds + || cursor.states != states + { + return Err(OperationError::InvalidInput( + "subjektiv query cursor does not match subject or filters".into(), + ) + .into()); + } + if cursor.store_revision != subject.store_revision { + return Err(OperationError::Conflict( + "stale_cursor: subjektiv query cursor is stale because Memory changed; restart the query".into(), + ) + .into()); + } + offset = cursor.offset; + } + + let kind_filter = kinds.iter().cloned().collect::>(); + let state_filter = states.iter().cloned().collect::>(); + let records = store + .list_memories(subject_id) + .map_err(subjektiv_store_error)? + .into_iter() + .filter(|record| { + kind_filter.contains(record.kind.as_str()) + && state_filter.contains(subjektiv_memory_state_name(record.state)) + && query.as_ref().is_none_or(|query| { + record.claim.to_lowercase().contains(query) + || record.body_md.to_lowercase().contains(query) + }) + }) + .collect::>(); + if offset > records.len() { + return Err(OperationError::InvalidInput( + "subjektiv query cursor offset is invalid".into(), + ) + .into()); + } + let end = offset.saturating_add(limit).min(records.len()); + let has_more = end < records.len(); + let items = records[offset..end] + .iter() + .map(|record| server_api::SubjektivMemoryQueryItem { + id: record.id.clone(), + revision: record.revision, + kind: record.kind.clone(), + state: api_memory_state(record.state), + claim: record.claim.clone(), + excerpt: bounded_memory_excerpt(&record.body_md, 240), + updated_at: record.updated_at.clone(), + }) + .collect(); + let next_cursor = has_more + .then(|| { + encode_subjektiv_cursor( + "query", + &SubjektivQueryCursor { + subject_id: subject_id.to_string(), + store_revision: subject.store_revision, + query, + kinds, + states, + offset: end, + }, + ) + }) + .transpose()?; + Ok(server_api::SubjektivMemoryQueryResponse { + items, + next_cursor, + has_more, + }) +} + +pub fn subjektiv_memory_read( + store: &crate::SubjektivStore, + subject_id: &str, + input: server_api::SubjektivMemoryReadRequest, +) -> OperationResult { + let current = store + .scoped_memory(subject_id, &input.memory_id) + .map_err(subjektiv_store_error)?; + let record = match input.revision { + Some(0) => { + return Err( + OperationError::InvalidInput("revision must be a positive integer".into()).into(), + ); + } + Some(revision) => store + .scoped_memory_revision(subject_id, &input.memory_id, revision) + .map_err(subjektiv_store_error)? + .ok_or_else(|| { + OperationError::InvalidInput(format!( + "memory_revision_not_found: Memory `{}` has no revision {revision}", + input.memory_id + )) + })?, + None => current.clone(), + }; + let body_limit = input.limit.unwrap_or(SUBJEKTIV_BODY_DEFAULT_LINES); + if body_limit == 0 || body_limit > SUBJEKTIV_BODY_MAX_LINES { + return Err(OperationError::InvalidInput(format!( + "body line limit must be within 1..={SUBJEKTIV_BODY_MAX_LINES}" + )) + .into()); + } + let body_offset = input.offset.unwrap_or(0); + let body_byte_offset = input.byte_offset.unwrap_or(0); + if (body_offset > 0 || body_byte_offset > 0) && input.revision.is_none() { + return Err(OperationError::InvalidInput( + "body continuation requires the exact revision returned by the first read".into(), + ) + .into()); + } + let lines = memory_body_lines(&record.body_md); + if body_offset > lines.len() { + return Err(OperationError::InvalidInput( + "body line offset exceeds document length".into(), + ) + .into()); + } + if body_offset == lines.len() && body_byte_offset > 0 { + return Err(OperationError::InvalidInput( + "body byte offset exceeds document length".into(), + ) + .into()); + } + if let Some(line) = lines.get(body_offset) + && (body_byte_offset > line.len() || !line.is_char_boundary(body_byte_offset)) + { + return Err(OperationError::InvalidInput( + "body byte offset must be a UTF-8 boundary within the selected line".into(), + ) + .into()); + } + let (body_md, body_next) = bounded_memory_body_page( + &lines, + body_offset, + body_byte_offset, + body_limit, + SUBJEKTIV_BODY_MAX_BYTES, + ); + let body_truncated = body_next.is_some(); + let (body_next_offset, body_next_byte_offset) = body_next + .map(|(line, byte)| (Some(line), Some(byte))) + .unwrap_or((None, None)); + + let (evidence_offset, nested_evidence_offset, nested_source_ref_offset) = + if let Some(cursor) = input.evidence_cursor { + let cursor: SubjektivEvidenceCursor = decode_subjektiv_cursor("evidence", &cursor)?; + if cursor.subject_id != subject_id + || cursor.memory_id != record.id + || cursor.revision != record.revision + { + return Err(OperationError::InvalidInput( + "subjektiv evidence cursor does not match subject, Memory, or revision".into(), + ) + .into()); + } + ( + cursor.offset, + cursor.evidence_offset, + cursor.source_ref_offset, + ) + } else { + (0, 0, 0) + }; + let total_evidence = record + .source_candidate_ids + .len() + .saturating_add(record.derived_from.len()); + if evidence_offset > total_evidence { + return Err(OperationError::InvalidInput( + "subjektiv evidence cursor offset is invalid".into(), + ) + .into()); + } + + let mut source_candidate_ids = Vec::new(); + let mut source_candidates = Vec::new(); + let mut derived_from = Vec::new(); + let mut next_evidence_offset = evidence_offset; + let mut next_nested_evidence_offset = 0; + let mut next_nested_source_ref_offset = 0; + if let Some(candidate_id) = record.source_candidate_ids.get(evidence_offset) { + let candidate = store + .staging_candidate(subject_id, candidate_id) + .map_err(subjektiv_store_error)? + .ok_or_else(|| { + OperationError::Store(format!( + "subjektiv candidate provenance `{candidate_id}` is missing" + )) + })?; + let evidence_total = candidate.evidence.len(); + let source_refs_total = candidate.source_refs.len(); + if nested_evidence_offset > evidence_total || nested_source_ref_offset > source_refs_total { + return Err(OperationError::InvalidInput( + "subjektiv nested evidence cursor offset is invalid".into(), + ) + .into()); + } + let evidence_end = nested_evidence_offset + .saturating_add(SUBJEKTIV_EVIDENCE_ANCHOR_PAGE_SIZE) + .min(evidence_total); + let source_ref_end = nested_source_ref_offset + .saturating_add(SUBJEKTIV_EVIDENCE_ANCHOR_PAGE_SIZE) + .min(source_refs_total); + source_candidate_ids.push(candidate_id.clone()); + source_candidates.push(server_api::SubjektivMemoryEvidenceCandidate { + candidate_id: candidate.id, + evidence: candidate + .evidence + .into_iter() + .skip(nested_evidence_offset) + .take(evidence_end - nested_evidence_offset) + .map(bounded_subjektiv_staging_evidence) + .collect(), + evidence_total, + evidence_truncated: nested_evidence_offset > 0 || evidence_end < evidence_total, + source_refs: candidate + .source_refs + .into_iter() + .skip(nested_source_ref_offset) + .take(source_ref_end - nested_source_ref_offset) + .map(bounded_subjektiv_source_ref) + .collect(), + source_refs_total, + source_refs_truncated: nested_source_ref_offset > 0 + || source_ref_end < source_refs_total, + }); + if evidence_end < evidence_total || source_ref_end < source_refs_total { + next_nested_evidence_offset = evidence_end; + next_nested_source_ref_offset = source_ref_end; + } else { + next_evidence_offset += 1; + } + } else if evidence_offset < total_evidence { + if nested_evidence_offset != 0 || nested_source_ref_offset != 0 { + return Err(OperationError::InvalidInput( + "subjektiv derivation cursor cannot contain nested offsets".into(), + ) + .into()); + } + let derived_index = evidence_offset - record.source_candidate_ids.len(); + let reference = &record.derived_from[derived_index]; + derived_from.push(server_api::SubjektivMemoryRevisionRef { + memory_id: reference.memory_id.clone(), + revision: reference.revision, + }); + next_evidence_offset += 1; + } else if nested_evidence_offset != 0 || nested_source_ref_offset != 0 { + return Err(OperationError::InvalidInput( + "subjektiv evidence cursor points beyond the final reference".into(), + ) + .into()); + } + bound_subjektiv_nested_evidence(&source_candidates)?; + let evidence_has_more = next_evidence_offset < total_evidence + || next_nested_evidence_offset != 0 + || next_nested_source_ref_offset != 0; + let evidence_next_cursor = evidence_has_more + .then(|| { + encode_subjektiv_cursor( + "evidence", + &SubjektivEvidenceCursor { + subject_id: subject_id.to_string(), + memory_id: record.id.clone(), + revision: record.revision, + offset: next_evidence_offset, + evidence_offset: next_nested_evidence_offset, + source_ref_offset: next_nested_source_ref_offset, + }, + ) + }) + .transpose()?; + + let mut response = server_api::SubjektivMemoryReadResponse { + memory_id: record.id, + revision: record.revision, + current_revision: current.revision, + kind: record.kind, + state: api_memory_state(record.state), + claim: record.claim, + body_md, + why_useful: record.why_useful, + staleness: record.staleness, + change_reason: record.change_reason, + created_at: record.created_at, + updated_at: record.updated_at, + body_offset, + body_byte_offset, + body_next_offset, + body_next_byte_offset, + body_truncated, + source_candidate_ids, + source_candidates, + derived_from, + evidence_next_cursor, + evidence_has_more, + }; + fit_subjektiv_read_body_to_output_budget( + &mut response, + &lines, + body_offset, + body_byte_offset, + body_limit, + )?; + Ok(response) +} + +pub fn subjektiv_memory_list_revisions( + store: &crate::SubjektivStore, + subject_id: &str, + input: server_api::SubjektivMemoryListRevisionsRequest, +) -> OperationResult { + let limit = bounded_subjektiv_limit(input.limit, SUBJEKTIV_QUERY_DEFAULT_LIMIT)?; + let current = store + .scoped_memory(subject_id, &input.memory_id) + .map_err(subjektiv_store_error)?; + let (max_revision, offset) = if let Some(cursor) = input.cursor { + let cursor: SubjektivRevisionCursor = decode_subjektiv_cursor("revisions", &cursor)?; + if cursor.subject_id != subject_id || cursor.memory_id != input.memory_id { + return Err(OperationError::InvalidInput( + "subjektiv revision cursor does not match subject or Memory".into(), + ) + .into()); + } + (cursor.max_revision, cursor.offset) + } else { + (current.revision, 0) + }; + let records = store + .list_memory_revisions(subject_id, &input.memory_id) + .map_err(subjektiv_store_error)? + .into_iter() + .filter(|record| record.revision <= max_revision) + .collect::>(); + if offset > records.len() { + return Err(OperationError::InvalidInput( + "subjektiv revision cursor offset is invalid".into(), + ) + .into()); + } + let end = offset.saturating_add(limit).min(records.len()); + let has_more = end < records.len(); + let items = records[offset..end] + .iter() + .map(|record| server_api::SubjektivMemoryRevisionItem { + revision: record.revision, + kind: record.kind.clone(), + state: api_memory_state(record.state), + claim: record.claim.clone(), + change_reason: record.change_reason.clone(), + updated_at: record.updated_at.clone(), + }) + .collect(); + let next_cursor = has_more + .then(|| { + encode_subjektiv_cursor( + "revisions", + &SubjektivRevisionCursor { + subject_id: subject_id.to_string(), + memory_id: input.memory_id.clone(), + max_revision, + offset: end, + }, + ) + }) + .transpose()?; + Ok(server_api::SubjektivMemoryListRevisionsResponse { + memory_id: input.memory_id, + current_revision: current.revision, + items, + next_cursor, + has_more, + }) +} + +pub fn subjektiv_memory_validate_proposal( + store: &crate::SubjektivStore, + subject_id: &str, + input: server_api::SubjektivMemoryValidateProposalRequest, +) -> OperationResult { + if input.expected_revision == 0 { + return Err( + OperationError::InvalidInput("expected_revision must be positive".into()).into(), + ); + } + let current = store + .scoped_memory(subject_id, &input.memory_id) + .map_err(subjektiv_store_error)?; + if current.revision != input.expected_revision { + return Err(OperationError::Conflict(format!( + "revision_conflict: Memory `{}` expected {}, current {}", + input.memory_id, input.expected_revision, current.revision + )) + .into()); + } + validate_subjektiv_proposal_transition(current.state, input.intent)?; + Ok(server_api::SubjektivMemoryProposalValidationResponse { + memory_id: input.memory_id, + current_revision: current.revision, + kind: current.kind, + state: api_memory_state(current.state), + intent: input.intent, + }) +} + +pub fn validate_subjektiv_receipt_id(receipt_id: &str) -> OperationResult<()> { + let receipt_id = receipt_id.trim(); + if receipt_id.is_empty() || receipt_id.len() > 512 || receipt_id.chars().any(char::is_control) { + return Err( + OperationError::InvalidInput("explicit Memory receipt_id is invalid".into()).into(), + ); + } + Ok(()) +} + +pub fn subjektiv_memory_stage_explicit( + store: &crate::SubjektivStore, + mut input: server_api::SubjektivMemoryStageExplicitRequest, + attribution: crate::SubjectSessionAttribution, +) -> OperationResult { + validate_subjektiv_receipt_id(&input.receipt_id)?; + let receipt_id = input.receipt_id.trim(); + if input.evidence.is_empty() || input.source_refs.is_empty() { + return Err(OperationError::InvalidInput( + "explicit Memory staging requires host-resolved committed evidence".into(), + ) + .into()); + } + if input.evidence.len() > SUBJEKTIV_CANDIDATE_ANCHOR_LIMIT + || input.source_refs.len() > SUBJEKTIV_CANDIDATE_ANCHOR_LIMIT + { + return Err(OperationError::InvalidInput(format!( + "explicit Memory evidence/source_refs are limited to {SUBJEKTIV_CANDIDATE_ANCHOR_LIMIT} items each" + )) + .into()); + } + if matches!(input.kind, memory::extract::CandidateKind::Preference) + && input.evidence.iter().any(|evidence| { + !matches!( + evidence.origin.as_ref().map(|origin| &origin.kind), + Some(memory::schema::EvidenceOriginKind::HumanInput) + ) + }) + { + return Err(OperationError::InvalidInput( + "preference candidates require exclusively HumanInput evidence".into(), + ) + .into()); + } + let mut range_start = u64::MAX; + let mut range_end = 0; + let mut segment_id = None; + for source_ref in &mut input.source_refs { + if source_ref + .session_id + .as_deref() + .is_some_and(|value| value != attribution.session_id) + { + return Err(OperationError::InvalidInput( + "explicit candidate source belongs to a different Session".into(), + ) + .into()); + } + source_ref.session_id = Some(attribution.session_id.clone()); + let source_segment = source_ref.segment_id.as_deref().ok_or_else(|| { + OperationError::InvalidInput("explicit candidate source is missing segment_id".into()) + })?; + if segment_id + .as_deref() + .is_some_and(|value| value != source_segment) + { + return Err(OperationError::InvalidInput( + "explicit candidate evidence spans multiple Session segments".into(), + ) + .into()); + } + segment_id = Some(source_segment.to_string()); + let range = source_ref.entry_range.ok_or_else(|| { + OperationError::InvalidInput("explicit candidate source is missing entry_range".into()) + })?; + range_start = range_start.min(range[0]); + range_end = range_end.max(range[1]); + } + let source = memory::schema::SourceRef { + segment_id: segment_id.ok_or_else(|| { + OperationError::InvalidInput("explicit candidate source is missing segment_id".into()) + })?, + range: [range_start, range_end], + }; + let proposal = input + .proposal + .as_ref() + .map(|proposal| { + crate::RevisionProposal::new( + domain_proposal_intent(proposal.intent), + proposal.memory_id.clone(), + proposal.expected_revision, + proposal.change_reason.clone(), + ) + }) + .transpose() + .map_err(subjektiv_store_error)?; + if let Some(proposal) = &proposal { + let current = store + .scoped_memory(&attribution.subject_id, &proposal.memory_id) + .map_err(subjektiv_store_error)?; + if current.kind != input.kind { + return Err(OperationError::InvalidInput( + "revision proposal kind must be derived from its target Memory".into(), + ) + .into()); + } + } + let candidate = memory::extract::ExtractedCandidate { + kind: input.kind, + claim: input.claim, + why_useful: input.why_useful, + staleness: input.staleness, + evidence_ids: input.evidence.iter().map(|item| item.id.clone()).collect(), + }; + let staging = memory::extract::StagingRecord::from_candidate( + receipt_id, + format!("explicit:{receipt_id}"), + source, + candidate, + input.evidence, + input.source_refs, + ); + let mut staging = crate::SubjectStagingRecord::attach(&attribution.subject_id, staging); + if let Some(proposal) = proposal.clone() { + staging + .attach_revision_proposal(proposal) + .map_err(subjektiv_store_error)?; + } + let (staged, _) = store + .stage_candidate_with_attribution(staging, attribution) + .map_err(subjektiv_store_error)?; + Ok(server_api::SubjektivMemoryStageExplicitResponse { + candidate_id: staged.id, + receipt_id: receipt_id.to_string(), + status: server_api::SubjektivMemoryReceiptStatus::Staged, + target: proposal + .as_ref() + .map(|proposal| server_api::SubjektivMemoryProposalTarget { + memory_id: proposal.memory_id.clone(), + expected_revision: proposal.expected_revision, + }), + intent: proposal + .as_ref() + .map(|proposal| api_proposal_intent(proposal.intent)), + }) +} + +pub fn validate_subjektiv_proposal_transition( + state: crate::MemoryState, + intent: server_api::SubjektivMemoryRevisionIntent, +) -> OperationResult<()> { + use crate::MemoryState::{Active, Resolved}; + use server_api::SubjektivMemoryRevisionIntent::{Reopen, Resolve, Retract, Revise}; + let valid = match intent { + Revise => matches!(state, Active | Resolved), + Resolve => state == Active, + Retract => matches!(state, Active | Resolved), + Reopen => state == Resolved, + }; + if valid { + Ok(()) + } else { + Err(OperationError::InvalidInput(format!( + "invalid_state_transition: {state:?} cannot accept {intent:?}" + )) + .into()) + } +} + +pub fn canonical_subjektiv_kinds( + kinds: Option>, +) -> OperationResult> { + let kinds = kinds.unwrap_or_else(|| { + use memory::extract::CandidateKind::*; + vec![ + Preference, + WorkingAssumption, + Constraint, + Decision, + OpenQuestion, + Lesson, + ] + }); + if kinds.is_empty() { + return Err(OperationError::InvalidInput("kinds must not be an empty array".into()).into()); + } + let mut names = kinds + .iter() + .map(|kind| kind.as_str().to_string()) + .collect::>(); + names.sort(); + names.dedup(); + Ok(names) +} + +pub fn canonical_subjektiv_states( + states: Option>, +) -> OperationResult> { + let states = states.unwrap_or_else(|| vec![server_api::SubjektivMemoryState::Active]); + if states.is_empty() { + return Err( + OperationError::InvalidInput("states must not be an empty array".into()).into(), + ); + } + let mut names = states + .into_iter() + .map(|state| match state { + server_api::SubjektivMemoryState::Active => "active".to_string(), + server_api::SubjektivMemoryState::Resolved => "resolved".to_string(), + server_api::SubjektivMemoryState::Retracted => "retracted".to_string(), + }) + .collect::>(); + names.sort(); + names.dedup(); + Ok(names) +} + +pub fn bounded_subjektiv_limit(limit: Option, default: usize) -> OperationResult { + let limit = limit.unwrap_or(default); + if limit == 0 || limit > SUBJEKTIV_QUERY_MAX_LIMIT { + return Err(OperationError::InvalidInput(format!( + "limit must be within 1..={SUBJEKTIV_QUERY_MAX_LIMIT}" + )) + .into()); + } + Ok(limit) +} + +pub fn api_memory_state(state: crate::MemoryState) -> server_api::SubjektivMemoryState { + match state { + crate::MemoryState::Active => server_api::SubjektivMemoryState::Active, + crate::MemoryState::Resolved => server_api::SubjektivMemoryState::Resolved, + crate::MemoryState::Retracted => server_api::SubjektivMemoryState::Retracted, + } +} + +pub fn subjektiv_memory_state_name(state: crate::MemoryState) -> &'static str { + match state { + crate::MemoryState::Active => "active", + crate::MemoryState::Resolved => "resolved", + crate::MemoryState::Retracted => "retracted", + } +} + +pub fn domain_proposal_intent( + intent: server_api::SubjektivMemoryRevisionIntent, +) -> crate::RevisionProposalIntent { + match intent { + server_api::SubjektivMemoryRevisionIntent::Revise => crate::RevisionProposalIntent::Revise, + server_api::SubjektivMemoryRevisionIntent::Resolve => { + crate::RevisionProposalIntent::Resolve + } + server_api::SubjektivMemoryRevisionIntent::Retract => { + crate::RevisionProposalIntent::Retract + } + server_api::SubjektivMemoryRevisionIntent::Reopen => crate::RevisionProposalIntent::Reopen, + } +} + +pub fn api_proposal_intent( + intent: crate::RevisionProposalIntent, +) -> server_api::SubjektivMemoryRevisionIntent { + match intent { + crate::RevisionProposalIntent::Revise => server_api::SubjektivMemoryRevisionIntent::Revise, + crate::RevisionProposalIntent::Resolve => { + server_api::SubjektivMemoryRevisionIntent::Resolve + } + crate::RevisionProposalIntent::Retract => { + server_api::SubjektivMemoryRevisionIntent::Retract + } + crate::RevisionProposalIntent::Reopen => server_api::SubjektivMemoryRevisionIntent::Reopen, + } +} + +pub fn fit_subjektiv_read_body_to_output_budget( + response: &mut server_api::SubjektivMemoryReadResponse, + lines: &[&str], + line_offset: usize, + byte_offset: usize, + line_limit: usize, +) -> OperationResult<()> { + let serialized_len = |response: &server_api::SubjektivMemoryReadResponse| { + serde_json::to_vec_pretty(response) + .map(|value| value.len()) + .map_err(|error| { + OperationError::Store(format!("encode subjektiv Memory read: {error}")) + }) + }; + if serialized_len(response)? <= server_api::SUBJEKTIV_MEMORY_READ_MAX_TOOL_CONTENT_BYTES { + return Ok(()); + } + + let initial_body_len = response.body_md.len(); + let mut lower = 0usize; + let mut upper = initial_body_len; + while lower < upper { + let candidate_limit = lower + (upper - lower).div_ceil(2); + set_subjektiv_read_body_page( + response, + lines, + line_offset, + byte_offset, + line_limit, + candidate_limit, + ); + if serialized_len(response)? <= server_api::SUBJEKTIV_MEMORY_READ_MAX_TOOL_CONTENT_BYTES { + lower = candidate_limit; + } else { + upper = candidate_limit - 1; + } + } + set_subjektiv_read_body_page(response, lines, line_offset, byte_offset, line_limit, lower); + let final_len = serialized_len(response)?; + if final_len > server_api::SUBJEKTIV_MEMORY_READ_MAX_TOOL_CONTENT_BYTES { + return Err(OperationError::Store(format!( + "subjektiv Memory metadata/provenance requires {final_len} bytes and exceeds the {} byte read-output budget", + server_api::SUBJEKTIV_MEMORY_READ_MAX_TOOL_CONTENT_BYTES + )) + .into()); + } + if initial_body_len > 0 && response.body_md.is_empty() { + return Err(OperationError::Store( + "subjektiv Memory metadata/provenance leaves no room for resumable body content".into(), + ) + .into()); + } + Ok(()) +} + +pub fn set_subjektiv_read_body_page( + response: &mut server_api::SubjektivMemoryReadResponse, + lines: &[&str], + line_offset: usize, + byte_offset: usize, + line_limit: usize, + byte_limit: usize, +) { + let (body_md, body_next) = + bounded_memory_body_page(lines, line_offset, byte_offset, line_limit, byte_limit); + response.body_md = body_md; + response.body_truncated = body_next.is_some(); + (response.body_next_offset, response.body_next_byte_offset) = body_next + .map(|(line, byte)| (Some(line), Some(byte))) + .unwrap_or((None, None)); +} + +pub fn bounded_memory_body_page( + lines: &[&str], + line_offset: usize, + byte_offset: usize, + line_limit: usize, + byte_limit: usize, +) -> (String, Option<(usize, usize)>) { + let line_end = line_offset.saturating_add(line_limit).min(lines.len()); + let mut line_index = line_offset; + let mut within_line = byte_offset; + let mut body = String::new(); + while line_index < line_end { + let line = lines[line_index]; + let remainder = &line[within_line..]; + let available = byte_limit.saturating_sub(body.len()); + if remainder.len() <= available { + body.push_str(remainder); + line_index += 1; + within_line = 0; + continue; + } + let mut take = available.min(remainder.len()); + while take > 0 && !remainder.is_char_boundary(take) { + take -= 1; + } + if take == 0 { + return (body, Some((line_index, within_line))); + } + body.push_str(&remainder[..take]); + within_line += take; + return (body, Some((line_index, within_line))); + } + if line_index < lines.len() { + (body, Some((line_index, 0))) + } else { + (body, None) + } +} + +pub fn memory_body_lines(body: &str) -> Vec<&str> { + if body.is_empty() { + Vec::new() + } else { + body.split_inclusive('\n').collect() + } +} + +pub fn bound_subjektiv_nested_evidence( + candidates: &[server_api::SubjektivMemoryEvidenceCandidate], +) -> OperationResult<()> { + let serialized = serde_json::to_vec(candidates).map_err(|error| { + OperationError::Store(format!("encode subjektiv evidence page: {error}")) + })?; + if serialized.len() <= SUBJEKTIV_NESTED_EVIDENCE_MAX_BYTES { + Ok(()) + } else { + Err(OperationError::Store( + "subjektiv evidence page exceeds its enforced response byte budget".into(), + ) + .into()) + } +} + +pub fn bounded_subjektiv_staging_evidence( + mut evidence: memory::extract::StagingEvidence, +) -> memory::extract::StagingEvidence { + evidence.kind = memory::schema::EvidenceKind::new(bounded_utf8_bytes( + evidence.kind.as_str().to_string(), + SUBJEKTIV_ANCHOR_TEXT_MAX_BYTES, + )); + evidence.origin = evidence.origin.map(bounded_subjektiv_evidence_origin); + evidence.excerpt = evidence + .excerpt + .map(|value| bounded_utf8_bytes(value, SUBJEKTIV_ANCHOR_TEXT_MAX_BYTES)); + evidence.summary = evidence + .summary + .map(|value| bounded_utf8_bytes(value, SUBJEKTIV_ANCHOR_TEXT_MAX_BYTES)); + evidence +} + +pub fn bounded_subjektiv_source_ref( + mut source: memory::schema::SourceEvidenceRef, +) -> memory::schema::SourceEvidenceRef { + source.origin = source.origin.map(bounded_subjektiv_evidence_origin); + source.evidence_kind = source.evidence_kind.map(|kind| { + memory::schema::EvidenceKind::new(bounded_utf8_bytes( + kind.as_str().to_string(), + SUBJEKTIV_ANCHOR_TEXT_MAX_BYTES, + )) + }); + source.label = source + .label + .map(|value| bounded_utf8_bytes(value, SUBJEKTIV_ANCHOR_TEXT_MAX_BYTES)); + source.summary = source + .summary + .map(|value| bounded_utf8_bytes(value, SUBJEKTIV_ANCHOR_TEXT_MAX_BYTES)); + source +} + +pub fn bounded_subjektiv_evidence_origin( + mut origin: memory::schema::EvidenceOrigin, +) -> memory::schema::EvidenceOrigin { + for field in [ + &mut origin.account_id, + &mut origin.workspace_id, + &mut origin.runtime_id, + &mut origin.worker_id, + &mut origin.flow_selector, + &mut origin.flow_definition_id, + ] { + if let Some(value) = field.take() { + *field = Some(bounded_utf8_bytes(value, SUBJEKTIV_ANCHOR_TEXT_MAX_BYTES)); + } + } + origin +} + +pub fn bounded_utf8_bytes(value: String, max_bytes: usize) -> String { + let value = value + .chars() + .map(|character| { + if character.is_control() || matches!(character, '"' | '\\') { + ' ' + } else { + character + } + }) + .collect::(); + if value.len() <= max_bytes { + return value; + } + let marker = "…"; + let mut end = max_bytes.saturating_sub(marker.len()); + while end > 0 && !value.is_char_boundary(end) { + end -= 1; + } + format!("{}{marker}", &value[..end]) +} + +pub fn bounded_memory_excerpt(body: &str, max_chars: usize) -> String { + let normalized = body.split_whitespace().collect::>().join(" "); + let mut chars = normalized.chars(); + let excerpt = chars.by_ref().take(max_chars).collect::(); + if chars.next().is_some() { + format!("{excerpt}…") + } else { + excerpt + } +} + +pub fn encode_subjektiv_cursor(kind: &str, value: &T) -> OperationResult { + let raw = serde_json::to_vec(value) + .map_err(|error| OperationError::Store(format!("encode subjektiv cursor: {error}")))?; + let encoded = raw + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::(); + Ok(format!("subjektiv.{kind}.{encoded}")) +} + +pub fn decode_subjektiv_cursor( + kind: &str, + cursor: &str, +) -> OperationResult { + let prefix = format!("subjektiv.{kind}."); + let encoded = cursor.strip_prefix(&prefix).ok_or_else(|| { + OperationError::InvalidInput(format!("invalid subjektiv {kind} cursor type")) + })?; + if encoded.is_empty() + || encoded.len() > 16_384 + || encoded.len() % 2 != 0 + || !encoded.is_ascii() + || !encoded.bytes().all(|byte| byte.is_ascii_hexdigit()) + { + return Err( + OperationError::InvalidInput(format!("invalid subjektiv {kind} cursor")).into(), + ); + } + let mut raw = Vec::with_capacity(encoded.len() / 2); + for index in (0..encoded.len()).step_by(2) { + raw.push( + u8::from_str_radix(&encoded[index..index + 2], 16).map_err(|_| { + OperationError::InvalidInput(format!("invalid subjektiv {kind} cursor encoding")) + })?, + ); + } + serde_json::from_slice(&raw).map_err(|_| { + OperationError::InvalidInput(format!("invalid subjektiv {kind} cursor payload")).into() + }) +} + +/// Transport-independent error classification. Backend maps these to its existing +/// HTTP errors; local Hosts preserve the same conflict/permission distinctions. +#[derive(Debug, thiserror::Error)] +pub enum OperationError { + #[error("{0}")] + InvalidInput(String), + #[error("{0}")] + Conflict(String), + #[error("{0}")] + PermissionDenied(String), + #[error("subject not found: {0}")] + SubjectNotFound(String), + #[error("{0}")] + Store(String), +} + +pub type OperationResult = std::result::Result; + +pub fn subjektiv_store_error(error: crate::SubjektivError) -> OperationError { + match error { + crate::SubjektivError::SubjectScopeMismatch { .. } => { + OperationError::PermissionDenied(format!("subject_scope_mismatch: {error}")) + } + crate::SubjektivError::RevisionConflict { .. } + | crate::SubjektivError::SubjectBehaviorConflict { .. } + | crate::SubjektivError::SurfaceGenerationConflict(_) => { + OperationError::Conflict(format!("revision_conflict: {error}")) + } + crate::SubjektivError::CandidateResolved(_) + | crate::SubjektivError::DecisionRequestConflict(_) => { + OperationError::Conflict(format!("candidate_decision_conflict: {error}")) + } + crate::SubjektivError::SubjectNotFound(id) => OperationError::SubjectNotFound(id), + crate::SubjektivError::MemoryNotFound(id) => { + OperationError::InvalidInput(format!("memory_not_found: Memory `{id}` was not found")) + } + crate::SubjektivError::Storage(_) => OperationError::Store(error.to_string()), + _ => OperationError::InvalidInput(error.to_string()), + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/subjektiv/src/api/context.rs b/crates/subjektiv/src/api/context.rs new file mode 100644 index 000000000..6e1a95633 --- /dev/null +++ b/crates/subjektiv/src/api/context.rs @@ -0,0 +1,155 @@ +//! Trusted Host operation context, never deserialized from model input. +use super::{OperationError, OperationResult, subjektiv_store_error}; +use crate::{SubjectSessionAttribution, SubjektivStore}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct JobAttemptBinding { + pub job_id: String, + pub attempt_id: String, +} + +#[derive(Debug, Clone)] +enum Authority { + Body { + attribution: Option, + }, + Consolidation { + candidate_ids: Option>, + job: Option, + }, +} + +/// A capability issued by trusted Rust Host code after authentication/lease, +/// delegation, and committed Session evidence checks. Not a request DTO. +/// Constructors check domain binding, not external identity or execution leases. +/// Backend must retain its authenticated Worker and active Job grant checks; +/// standalone must hold the selected Subject lease and attenuate Job batches. +#[derive(Debug, Clone)] +pub struct HostOperationContext { + scope_id: String, + subject_id: String, + authority: Authority, +} + +impl HostOperationContext { + /// The Host has validated body execution authority. Attribution, when supplied, + /// must already be immutable history in this store. Before StageExplicit the + /// Host must resolve/validate all evidence against committed public Session + /// entries; neither this constructor nor the operation reads Session files. + pub fn validated_body( + store: &SubjektivStore, + subject_id: &str, + attribution: Option, + ) -> OperationResult { + Self::require_subject(store, subject_id)?; + if let Some(record) = &attribution { + if record.subject_id != subject_id + || store + .session_attribution(&record.session_id) + .map_err(subjektiv_store_error)? + .as_ref() + != Some(record) + { + return Err(OperationError::PermissionDenied( + "Session attribution does not match the Host subject".into(), + )); + } + } + Ok(Self { + scope_id: store.scope_id().into(), + subject_id: subject_id.into(), + authority: Authority::Body { attribution }, + }) + } + + /// The Host has validated consolidation authority and (for a Job) its active + /// attempt. `Some(candidate_ids)` is the immutable admitted batch; no candidate + /// outside it is accessible. `None` retains Backend's non-Job consolidation + /// authority, and must not be issued to an attenuated Job. + pub fn validated_consolidation( + store: &SubjektivStore, + subject_id: &str, + candidate_ids: Option>, + job: Option, + ) -> OperationResult { + Self::require_subject(store, subject_id)?; + if let Some(ids) = &candidate_ids { + for id in ids { + if store + .staging_candidate(subject_id, id) + .map_err(subjektiv_store_error)? + .is_none() + { + return Err(OperationError::InvalidInput(format!( + "candidate_not_found: {id}" + ))); + } + } + } + if job.as_ref().is_some_and(|binding| { + binding.job_id.trim().is_empty() || binding.attempt_id.trim().is_empty() + }) { + return Err(OperationError::InvalidInput( + "Job attempt binding must not be empty".into(), + )); + } + Ok(Self { + scope_id: store.scope_id().into(), + subject_id: subject_id.into(), + authority: Authority::Consolidation { candidate_ids, job }, + }) + } + + fn require_subject(store: &SubjektivStore, id: &str) -> OperationResult<()> { + store + .subject(id) + .map_err(subjektiv_store_error)? + .ok_or_else(|| OperationError::SubjectNotFound(id.into()))?; + Ok(()) + } + + pub fn subject_id(&self) -> &str { + &self.subject_id + } + + pub(super) fn require_store(&self, store: &SubjektivStore) -> OperationResult<()> { + if self.scope_id != store.scope_id() { + return Err(OperationError::PermissionDenied( + "Host operation context belongs to another storage scope".into(), + )); + } + Ok(()) + } + + pub(super) fn body(&self) -> OperationResult> { + match &self.authority { + Authority::Body { attribution } => Ok(attribution.as_ref()), + _ => Err(OperationError::PermissionDenied( + "operation requires Subject body authority".into(), + )), + } + } + + pub(super) fn consolidation( + &self, + ) -> OperationResult<(Option<&[String]>, Option<&JobAttemptBinding>)> { + match &self.authority { + Authority::Consolidation { candidate_ids, job } => { + Ok((candidate_ids.as_deref(), job.as_ref())) + } + _ => Err(OperationError::PermissionDenied( + "operation requires consolidation authority".into(), + )), + } + } + + pub(super) fn candidate(&self, id: &str) -> OperationResult<()> { + let (ids, _) = self.consolidation()?; + if ids.is_some_and(|ids| !ids.iter().any(|allowed| allowed == id)) { + return Err(OperationError::PermissionDenied( + "candidate is outside the admitted Job batch".into(), + )); + } + Ok(()) + } +} diff --git a/crates/subjektiv/src/api/operation.rs b/crates/subjektiv/src/api/operation.rs new file mode 100644 index 000000000..253408ad7 --- /dev/null +++ b/crates/subjektiv/src/api/operation.rs @@ -0,0 +1,255 @@ +use super::*; +use server_api::{ + SubjektivMemoryBackendOperation as Op, SubjektivMemoryBackendResponse as Response, +}; + +/// Executes the same domain operations and bounded projections for every Host. +/// The caller authenticates and validates evidence before issuing `context`. +pub fn execute( + store: &crate::SubjektivStore, + context: &HostOperationContext, + operation: Op, +) -> OperationResult { + context.require_store(store)?; + let subject_id = context.subject_id(); + Ok(match operation { + Op::ResidentSummary(_) => { + context.body()?; + Response::ResidentSummary(resident_summary_output( + store + .resident_surface(subject_id) + .map_err(subjektiv_store_error)?, + )) + } + Op::ResidentContext(_) => { + context.body()?; + let resident = store + .resident_context(subject_id) + .map_err(subjektiv_store_error)?; + Response::ResidentContext(server_api::SubjektivResidentContextOutput { + behavior_md: resident.subject.behavior_md, + behavior_revision: resident.subject.behavior_revision, + memory_surface: resident_summary_output(resident.surface), + }) + } + Op::Query(input) => Response::Query(subjektiv_memory_query(store, subject_id, input)?), + Op::Read(input) => Response::Read(subjektiv_memory_read(store, subject_id, input)?), + Op::ListRevisions(input) => { + Response::ListRevisions(subjektiv_memory_list_revisions(store, subject_id, input)?) + } + Op::ValidateProposal(input) => { + context.body()?; + Response::ProposalValidated(subjektiv_memory_validate_proposal( + store, subject_id, input, + )?) + } + Op::ReceiptStatus(input) => { + context.body()?; + validate_subjektiv_receipt_id(&input.receipt_id)?; + let candidate = store + .staging_candidate(subject_id, &input.receipt_id) + .map_err(subjektiv_store_error)?; + Response::ReceiptStatus(server_api::SubjektivMemoryReceiptStatusResponse { + receipt_id: input.receipt_id, + status: if candidate.is_some() { + server_api::SubjektivMemoryReceiptStatus::Staged + } else { + server_api::SubjektivMemoryReceiptStatus::Missing + }, + candidate_id: candidate.map(|record| record.id), + }) + } + Op::StageExplicit(input) => { + let attribution = context.body()?.ok_or_else(|| { + OperationError::PermissionDenied( + "explicit staging requires validated Session attribution".into(), + ) + })?; + if input.session_id != attribution.session_id { + return Err(OperationError::PermissionDenied( + "explicit staging Session does not match Host attribution".into(), + )); + } + Response::Staged(subjektiv_memory_stage_explicit( + store, + input, + attribution.clone(), + )?) + } + Op::ListCandidates(input) => { + let (ids, _) = context.consolidation()?; + let limit = bounded_subjektiv_limit(input.limit, SUBJEKTIV_QUERY_DEFAULT_LIMIT)?; + let mut candidates = if let Some(ids) = ids { + let mut pending = Vec::new(); + for id in ids { + if store + .staging_resolution(subject_id, id) + .map_err(subjektiv_store_error)? + .is_none() + { + if let Some(record) = store + .staging_candidate(subject_id, id) + .map_err(subjektiv_store_error)? + { + pending.push(record); + } + } + } + pending + } else { + store + .pending_staging_candidates(subject_id, limit.saturating_add(1)) + .map_err(subjektiv_store_error)? + }; + let has_more = candidates.len() > limit; + candidates.truncate(limit); + Response::Candidates(server_api::SubjektivMemoryCandidateListResponse { + items: candidates + .into_iter() + .map(subjektiv_candidate_summary) + .collect(), + has_more, + }) + } + Op::ReadCandidate(input) => { + context.candidate(&input.candidate_id)?; + let candidate = store + .staging_candidate(subject_id, &input.candidate_id) + .map_err(subjektiv_store_error)? + .ok_or_else(|| { + OperationError::InvalidInput(format!( + "candidate_not_found: pending candidate `{}` was not found", + input.candidate_id + )) + })?; + if store + .staging_resolution(subject_id, &input.candidate_id) + .map_err(subjektiv_store_error)? + .is_some() + { + return Err(OperationError::Conflict(format!( + "candidate_resolved: candidate `{}` is already resolved", + input.candidate_id + ))); + } + Response::Candidate(subjektiv_candidate(candidate)) + } + Op::DecideCandidate(input) => { + context.candidate(&input.candidate_id)?; + let receipt = store + .decide_candidate(subject_id, subjektiv_candidate_decision(input)?) + .map_err(subjektiv_store_error)?; + Response::CandidateDecided(subjektiv_candidate_decision_response(receipt)) + } + Op::PrepareSurface(_) => { + let (ids, job) = context.consolidation()?; + if let Some(ids) = ids { + for id in ids { + if store + .staging_resolution(subject_id, id) + .map_err(subjektiv_store_error)? + .is_none() + { + return Err(OperationError::InvalidInput(format!( + "surface generation requires a durable disposition for batch candidate `{id}`" + ))); + } + } + } + let generation = store + .prepare_job_surface_generation( + subject_id, + job.map(|b| (b.job_id.as_str(), b.attempt_id.as_str())), + ) + .map_err(subjektiv_store_error)?; + let resident = store + .resident_surface(subject_id) + .map_err(subjektiv_store_error)?; + let mut response = subjektiv_surface_prepare_response(generation); + if resident.availability == crate::SurfaceAvailability::Ready { + response.current_snapshot_id = resident + .snapshot + .filter(|s| s.built_from_store_revision == response.store_revision) + .map(|s| s.id); + } + Response::SurfacePrepared(response) + } + Op::PublishSurface(input) => { + let (_, job) = context.consolidation()?; + if let Some(job) = job { + store + .require_job_surface_generation( + subject_id, + &input.generation_id, + &job.job_id, + &job.attempt_id, + ) + .map_err(subjektiv_store_error)?; + } + let points = input + .points + .into_iter() + .map(|point| crate::SurfacePoint { + body_md: point.body_md, + memory_refs: point + .memory_refs + .into_iter() + .map(|r| crate::MemoryRevisionRef { + memory_id: r.memory_id, + revision: r.revision, + }) + .collect(), + }) + .collect(); + let snapshot = store + .publish_surface_generation(subject_id, &input.generation_id, points) + .map_err(subjektiv_store_error)?; + Response::SurfacePublished(server_api::SubjektivSurfacePublishResponse { + snapshot_id: snapshot.id, + built_from_store_revision: snapshot.built_from_store_revision, + empty: snapshot.body_md.is_empty(), + }) + } + Op::FailSurface(input) => { + let (_, job) = context.consolidation()?; + if let Some(job) = job { + store + .require_job_surface_generation( + subject_id, + &input.generation_id, + &job.job_id, + &job.attempt_id, + ) + .map_err(subjektiv_store_error)?; + } + let revision = store + .fail_surface_generation(subject_id, &input.generation_id, &input.reason_code) + .map_err(subjektiv_store_error)?; + let status = if let Some(job) = job { + store + .validate_job_surface_outcome( + subject_id, + &input.generation_id, + revision, + "failed", + None, + Some(&input.reason_code), + &job.job_id, + &job.attempt_id, + ) + .map_err(|e| { + OperationError::Conflict(format!( + "surface failure was not recorded for this attempt: {e}" + )) + })?; + "failed_confirmed" + } else { + "failed" + }; + Response::SurfaceFailed(server_api::SubjektivSurfaceFailureResponse { + store_revision: revision, + status: status.into(), + }) + } + }) +} diff --git a/crates/subjektiv/src/api/tests.rs b/crates/subjektiv/src/api/tests.rs new file mode 100644 index 000000000..eab46e930 --- /dev/null +++ b/crates/subjektiv/src/api/tests.rs @@ -0,0 +1,343 @@ +use super::*; +use crate::{MemoryDraft, SubjectRole, SubjectSessionAttribution, SubjektivStore}; +use memory::extract::CandidateKind; +use server_api::{ + SubjektivMemoryBackendOperation as Op, SubjektivMemoryBackendResponse as Response, +}; + +fn open(root: &std::path::Path, scope: &str) -> (feature_storage::FeatureStorage, SubjektivStore) { + let manager = feature_storage::FeatureStorage::new(root); + let scope = manager.scope(scope).unwrap(); + let registration = SubjektivStore::register(&scope).unwrap(); + let store = SubjektivStore::open(&scope, ®istration).unwrap(); + (manager, store) +} + +fn query(cursor: Option) -> Op { + Op::Query(server_api::SubjektivMemoryQueryRequest { + query: None, + kinds: None, + states: None, + limit: Some(1), + cursor, + }) +} + +fn read(id: &str, revision: Option, offset: Option, byte_offset: Option) -> Op { + Op::Read(server_api::SubjektivMemoryReadRequest { + memory_id: id.into(), + revision, + offset, + byte_offset, + limit: Some(1), + evidence_cursor: None, + }) +} + +#[test] +fn neutral_scope_reopen_and_context_attenuation() { + let temp = tempfile::tempdir().unwrap(); + let random_scope = uuid::Uuid::now_v7().to_string(); + let (manager, store) = open(temp.path(), &random_scope); + assert_eq!(store.scope_id(), random_scope); + let subject = store + .create_subject(SubjectRole::new("companion").unwrap()) + .unwrap(); + let body = HostOperationContext::validated_body(&store, &subject.id, None).unwrap(); + let consolidation = + HostOperationContext::validated_consolidation(&store, &subject.id, Some(vec![]), None) + .unwrap(); + assert!( + execute( + &store, + &body, + Op::ListCandidates(server_api::SubjektivMemoryCandidateListRequest { limit: None }) + ) + .is_err() + ); + assert!( + execute( + &store, + &consolidation, + Op::ResidentSummary(memory::backend::MemoryResidentSummaryOperation {}) + ) + .is_err() + ); + let attribution = + SubjectSessionAttribution::new(&subject.id, "local-host", "worker", "session").unwrap(); + assert!( + HostOperationContext::validated_body(&store, &subject.id, Some(attribution.clone())) + .is_err() + ); + store + .record_session_attribution(attribution.clone()) + .unwrap(); + HostOperationContext::validated_body(&store, &subject.id, Some(attribution)).unwrap(); + let (_, foreign) = open(&temp.path().join("foreign"), "foreign-scope"); + assert!(matches!( + execute(&foreign, &body, query(None)), + Err(OperationError::PermissionDenied(_)) + )); + manager.shutdown().unwrap(); + drop(store); + let (_manager, reopened) = open(temp.path(), &random_scope); + assert_eq!(reopened.subject(&subject.id).unwrap().unwrap(), subject); + assert!(reopened.session_attribution("session").unwrap().is_some()); +} + +#[test] +fn shared_query_cursor_and_utf8_fixed_revision_projection() { + let temp = tempfile::tempdir().unwrap(); + let (_manager, store) = open(temp.path(), "local-random-scope"); + let subject = store + .create_subject(SubjectRole::new("companion").unwrap()) + .unwrap(); + let context = HostOperationContext::validated_body(&store, &subject.id, None).unwrap(); + let original = "日本語\\\"".repeat(10_000); + let first = store + .create_memory( + &subject.id, + MemoryDraft::active( + CandidateKind::Lesson, + "first", + &original, + "reason", + "initial", + ), + ) + .unwrap(); + store + .create_memory( + &subject.id, + MemoryDraft::active( + CandidateKind::Decision, + "second", + "second body", + "reason", + "initial", + ), + ) + .unwrap(); + let Response::Query(page) = execute(&store, &context, query(None)).unwrap() else { + panic!() + }; + assert!(page.has_more); + assert_eq!(page.items.len(), 1); + let cursor = page.next_cursor.unwrap(); + store + .create_memory( + &subject.id, + MemoryDraft::active( + CandidateKind::Constraint, + "third", + "third body", + "reason", + "initial", + ), + ) + .unwrap(); + assert!(matches!( + execute(&store, &context, query(Some(cursor))), + Err(OperationError::Conflict(_)) + )); + assert!(execute(&store, &context, read(&first.id, None, Some(1), None)).is_err()); + let mut offset = None; + let mut byte_offset = None; + let mut reconstructed = String::new(); + loop { + let Response::Read(page) = execute( + &store, + &context, + read(&first.id, Some(1), offset, byte_offset), + ) + .unwrap() else { + panic!() + }; + assert!( + serde_json::to_vec_pretty(&page).unwrap().len() + <= server_api::SUBJEKTIV_MEMORY_READ_MAX_TOOL_CONTENT_BYTES + ); + reconstructed.push_str(&page.body_md); + if !page.body_truncated { + break; + } + assert!(!page.body_md.is_empty()); + offset = page.body_next_offset; + byte_offset = page.body_next_byte_offset; + } + assert_eq!(reconstructed, original); +} + +#[test] +fn shared_surface_empty_snapshot_is_ready_not_missing() { + let temp = tempfile::tempdir().unwrap(); + let (_manager, store) = open(temp.path(), "local-random-scope"); + let subject = store + .create_subject(SubjectRole::new("companion").unwrap()) + .unwrap(); + let consolidation = HostOperationContext::validated_consolidation( + &store, + &subject.id, + Some(vec![]), + Some(JobAttemptBinding { + job_id: "job".into(), + attempt_id: "attempt".into(), + }), + ) + .unwrap(); + let prepared: server_api::SubjektivSurfacePrepareRequest = serde_json::from_str("{}").unwrap(); + let Response::SurfacePrepared(prepared) = + execute(&store, &consolidation, Op::PrepareSurface(prepared)).unwrap() + else { + panic!() + }; + let Response::SurfacePublished(published) = execute( + &store, + &consolidation, + Op::PublishSurface(server_api::SubjektivSurfacePublishRequest { + generation_id: prepared.generation_id, + points: vec![], + }), + ) + .unwrap() else { + panic!() + }; + assert!(published.empty); + let body = HostOperationContext::validated_body(&store, &subject.id, None).unwrap(); + let Response::ResidentSummary(resident) = execute( + &store, + &body, + Op::ResidentSummary(memory::backend::MemoryResidentSummaryOperation {}), + ) + .unwrap() else { + panic!() + }; + assert_eq!( + resident.availability, + memory::backend::MemoryResidentSummaryAvailability::Ready + ); + assert_eq!(resident.content, None); +} + +#[test] +fn explicit_receipts_candidate_batch_and_decision_retries_share_logic() { + let temp = tempfile::tempdir().unwrap(); + let (_manager, store) = open(temp.path(), "random-local-scope"); + let subject = store + .create_subject(SubjectRole::new("companion").unwrap()) + .unwrap(); + let attribution = + SubjectSessionAttribution::new(&subject.id, "host", "worker", "session").unwrap(); + store + .record_session_attribution(attribution.clone()) + .unwrap(); + let context = + HostOperationContext::validated_body(&store, &subject.id, Some(attribution)).unwrap(); + // Host-resolved public Session evidence with no invented Workspace identity. + let input = server_api::SubjektivMemoryStageExplicitRequest { + receipt_id: "receipt-1".into(), + session_id: "session".into(), + kind: CandidateKind::Lesson, + claim: "Preserve correction history".into(), + why_useful: "Allows auditing".into(), + staleness: None, + proposal: None, + evidence: vec![memory::extract::StagingEvidence { + id: "e1".into(), + entry_range: Some([1, 1]), + kind: memory::schema::EvidenceKind::new("model_output"), + origin: None, + excerpt: Some("Use immutable revisions".into()), + summary: None, + }], + source_refs: vec![memory::schema::SourceEvidenceRef { + session_id: Some("session".into()), + segment_id: Some("segment".into()), + entry_range: Some([1, 1]), + evidence_id: Some("e1".into()), + ..Default::default() + }], + }; + let mut foreign_session = input.clone(); + foreign_session.session_id = "foreign".into(); + assert!(matches!( + execute(&store, &context, Op::StageExplicit(foreign_session)), + Err(OperationError::PermissionDenied(_)) + )); + execute(&store, &context, Op::StageExplicit(input.clone())).unwrap(); + execute(&store, &context, Op::StageExplicit(input)).unwrap(); + let Response::ReceiptStatus(receipt) = execute( + &store, + &context, + Op::ReceiptStatus(server_api::SubjektivMemoryReceiptStatusRequest { + receipt_id: "receipt-1".into(), + }), + ) + .unwrap() else { + panic!() + }; + assert_eq!( + receipt.status, + server_api::SubjektivMemoryReceiptStatus::Staged + ); + let candidate_id = receipt.candidate_id.unwrap(); + let no_batch = + HostOperationContext::validated_consolidation(&store, &subject.id, Some(vec![]), None) + .unwrap(); + assert!(matches!( + execute( + &store, + &no_batch, + Op::ReadCandidate(server_api::SubjektivMemoryCandidateReadRequest { + candidate_id: candidate_id.clone() + }) + ), + Err(OperationError::PermissionDenied(_)) + )); + let batch = HostOperationContext::validated_consolidation( + &store, + &subject.id, + Some(vec![candidate_id.clone()]), + None, + ) + .unwrap(); + assert!( + execute( + &store, + &batch, + Op::PrepareSurface(serde_json::from_str("{}").unwrap()) + ) + .is_err() + ); + let decision = server_api::SubjektivMemoryCandidateDecisionRequest { + request_id: "decision-1".into(), + candidate_id, + reason: "adopt lesson".into(), + decision: server_api::SubjektivMemoryCandidateDecision::Apply { + target: server_api::SubjektivMemoryApplyTarget::Create, + memory: server_api::SubjektivMemoryDraft { + kind: CandidateKind::Lesson, + state: server_api::SubjektivMemoryState::Active, + claim: "Preserve correction history".into(), + body_md: "Use immutable revisions".into(), + why_useful: "Allows auditing".into(), + staleness: None, + derived_from: vec![], + change_reason: "adopt".into(), + }, + }, + }; + let first = execute(&store, &batch, Op::DecideCandidate(decision.clone())).unwrap(); + let retry = execute(&store, &batch, Op::DecideCandidate(decision)).unwrap(); + assert_eq!( + serde_json::to_value(first).unwrap(), + serde_json::to_value(retry).unwrap() + ); + assert_eq!(store.list_memories(&subject.id).unwrap().len(), 1); + execute( + &store, + &batch, + Op::PrepareSurface(serde_json::from_str("{}").unwrap()), + ) + .unwrap(); +} diff --git a/crates/subjektiv/src/job.rs b/crates/subjektiv/src/job.rs new file mode 100644 index 000000000..6b65b48ab --- /dev/null +++ b/crates/subjektiv/src/job.rs @@ -0,0 +1,153 @@ +//! Common consolidation Job policy and verified result projections. +//! +//! Hosts authenticate/admit Jobs and bind their immutable batch and attempt. +//! These helpers do not dispatch, replay, or grant Job execution authority. +use serde::Deserialize; + +use crate::api::{OperationError, OperationResult}; +use crate::{SubjektivStore, SurfaceAvailability}; + +pub const CANDIDATE_BATCH_LIMIT: usize = 100; +pub const CONSOLIDATION_THRESHOLD_FILES: usize = 5; +pub const CONSOLIDATION_THRESHOLD_BYTES: u64 = 50_000; + +/// Selects an ordered prefix using the T-704 complete-result budget. Reserve +/// 2,048 bytes for the Subject/Host surface envelope, then twice the JSON-escaped +/// ID size plus 128 bytes per candidate for its compact verified disposition. +pub fn bounded_candidate_batch( + ids: impl IntoIterator, + max_result_bytes: u32, +) -> OperationResult> { + let mut estimated_bytes = 2_048_usize; + let mut selected = Vec::new(); + for id in ids.into_iter().take(CANDIDATE_BATCH_LIMIT) { + let id_bytes = serde_json::to_vec(&id) + .map_err(|e| OperationError::InvalidInput(e.to_string()))? + .len(); + let additional = 2 * id_bytes + 128; + if estimated_bytes + additional > max_result_bytes as usize { + break; + } + estimated_bytes += additional; + selected.push(id); + } + Ok(selected) +} + +/// Empty ready Subjects need no work, even when forced. Missing/stale/failed +/// surfaces bypass candidate thresholds; confirmed Memory is never discarded. +pub fn consolidation_required( + candidate_count: usize, + total_bytes: u64, + force: bool, + surface_availability: SurfaceAvailability, +) -> bool { + let surface_needs_generation = surface_availability != SurfaceAvailability::Ready; + if candidate_count == 0 { + return surface_needs_generation; + } + force + || surface_needs_generation + || candidate_count >= CONSOLIDATION_THRESHOLD_FILES + || total_bytes >= CONSOLIDATION_THRESHOLD_BYTES +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ConsolidationResult { + subject_id: String, + candidate_ids: Vec, + surface: SurfaceResult, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct SurfaceResult { + availability: String, + generation_id: String, + store_revision: u64, + #[serde(default)] + snapshot_id: Option, + #[serde(default)] + reason_code: Option, +} + +/// Verifies the exact Subject/batch, every immutable disposition, and the actual +/// terminal surface outcome begun by this Job attempt. Model claims alone never +/// establish completion. Same admission semantics as the Backend T-704 adapter. +pub fn validate_result( + store: &SubjektivStore, + subject_id: &str, + batch: &[String], + job_id: &str, + attempt_id: &str, + value: &serde_json::Value, +) -> OperationResult<()> { + let result: ConsolidationResult = serde_json::from_value(value.clone()) + .map_err(|e| OperationError::InvalidInput(format!("invalid consolidation result: {e}")))?; + if result.subject_id != subject_id || result.candidate_ids != batch { + return Err(OperationError::InvalidInput( + "consolidation result does not match the immutable Subject/batch grant".into(), + )); + } + for candidate_id in batch { + if store + .staging_resolution(subject_id, candidate_id) + .map_err(|e| OperationError::Store(e.to_string()))? + .is_none() + { + return Err(OperationError::InvalidInput(format!( + "consolidation candidate `{candidate_id}` is unresolved" + ))); + } + } + store + .validate_job_surface_outcome( + subject_id, + &result.surface.generation_id, + result.surface.store_revision, + &result.surface.availability, + result.surface.snapshot_id.as_deref(), + result.surface.reason_code.as_deref(), + job_id, + attempt_id, + ) + .map_err(|e| { + OperationError::InvalidInput(format!("consolidation surface result mismatch: {e}")) + }) +} + +/// Enriches a previously verified result with compact durable dispositions. Full +/// reasons, affected revisions and provenance remain in the receipt store. This +/// deterministic projection neither applies candidates nor creates a new ledger. +pub fn with_dispositions( + store: &SubjektivStore, + subject_id: &str, + batch: &[String], + value: &serde_json::Value, +) -> OperationResult { + let mut dispositions = Vec::new(); + for id in batch { + let resolution = store + .staging_resolution(subject_id, id) + .map_err(|e| OperationError::Store(e.to_string()))? + .ok_or_else(|| { + OperationError::InvalidInput(format!("candidate `{id}` has no durable disposition")) + })?; + dispositions.push(serde_json::json!({"candidate_id": id, "action": resolution.action})); + } + let mut enriched = value.clone(); + enriched + .as_object_mut() + .ok_or_else(|| { + OperationError::InvalidInput("consolidation result must be an object".into()) + })? + .insert( + "candidate_dispositions".into(), + serde_json::Value::Array(dispositions), + ); + Ok(enriched) +} + +#[cfg(test)] +mod tests; diff --git a/crates/subjektiv/src/job/tests.rs b/crates/subjektiv/src/job/tests.rs new file mode 100644 index 000000000..a2e447686 --- /dev/null +++ b/crates/subjektiv/src/job/tests.rs @@ -0,0 +1,183 @@ +use super::*; +use crate::{ + CandidateDecision, CandidateDecisionRequest, StagingResolutionAction, SubjectRole, + SubjectStagingRecord, +}; +use memory::extract::{CandidateKind, ExtractedCandidate, StagingRecord}; +use memory::schema::SourceRef; + +#[test] +fn escaped_ids_use_exact_t704_complete_result_budget() { + const MAX_RESULT_BYTES: u32 = 16 * 1024; + let ids: Vec<_> = (0..100) + .map(|i| format!("{i:03}{}", "\\\"".repeat(126))) + .collect(); + let batch = bounded_candidate_batch(ids.clone(), MAX_RESULT_BYTES).unwrap(); + assert!(!batch.is_empty()); + assert!(batch.len() < ids.len()); + assert_eq!(batch, ids[..batch.len()]); + let dispositions: Vec<_> = batch + .iter() + .map(|id| serde_json::json!({"candidate_id": id, "action": "already_covered"})) + .collect(); + let result = serde_json::json!({ + "subject_id": "\\\"".repeat(128), "candidate_ids": batch, "candidate_dispositions": dispositions, + "surface": { "availability": "failed", "generation_id": "g".repeat(100), "store_revision": u64::MAX, "reason_code": "\\\"".repeat(128) } + }); + assert!(serde_json::to_vec(&result).unwrap().len() <= MAX_RESULT_BYTES as usize); + assert!( + bounded_candidate_batch(["one".into()], 2048) + .unwrap() + .is_empty() + ); + assert_eq!( + bounded_candidate_batch((0..200).map(|i| i.to_string()), u32::MAX) + .unwrap() + .len(), + CANDIDATE_BATCH_LIMIT + ); +} + +#[test] +fn trigger_policy_preserves_empty_ready_and_surface_retry_rules() { + for availability in [ + SurfaceAvailability::Ungenerated, + SurfaceAvailability::Stale, + SurfaceAvailability::Failed, + ] { + assert!(consolidation_required(0, 0, false, availability)); + assert!(consolidation_required(1, 1, false, availability)); + } + assert!(!consolidation_required( + 0, + 0, + true, + SurfaceAvailability::Ready + )); + assert!(!consolidation_required( + 4, + 49_999, + false, + SurfaceAvailability::Ready + )); + assert!(consolidation_required( + 5, + 0, + false, + SurfaceAvailability::Ready + )); + assert!(consolidation_required( + 1, + 50_000, + false, + SurfaceAvailability::Ready + )); + assert!(consolidation_required( + 1, + 1, + true, + SurfaceAvailability::Ready + )); +} + +#[test] +fn result_checks_durable_dispositions_and_exact_job_surface_outcome() { + let temp = tempfile::tempdir().unwrap(); + let manager = feature_storage::FeatureStorage::new(temp.path()); + let scope = manager.scope("local-random-scope").unwrap(); + let registration = SubjektivStore::register(&scope).unwrap(); + let store = SubjektivStore::open(&scope, ®istration).unwrap(); + let subject = store + .create_subject(SubjectRole::new("companion").unwrap()) + .unwrap(); + let candidate = SubjectStagingRecord::attach( + &subject.id, + StagingRecord::from_candidate( + "candidate", + "dedup", + SourceRef { + segment_id: "segment".into(), + range: [1, 1], + }, + ExtractedCandidate { + kind: CandidateKind::Lesson, + claim: "claim".into(), + why_useful: "reason".into(), + staleness: None, + evidence_ids: vec![], + }, + vec![], + vec![], + ), + ); + store.stage_candidate(candidate).unwrap(); + let batch = vec!["candidate".to_string()]; + let generation = store + .prepare_job_surface_generation(&subject.id, Some(("job", "attempt"))) + .unwrap(); + let snapshot = store + .publish_surface_generation(&subject.id, &generation.id, vec![]) + .unwrap(); + let ready = serde_json::json!({"subject_id": subject.id, "candidate_ids": batch, + "surface": {"availability": "ready", "generation_id": generation.id, "store_revision": snapshot.built_from_store_revision, "snapshot_id": snapshot.id}}); + assert!( + validate_result(&store, &subject.id, &batch, "job", "attempt", &ready) + .unwrap_err() + .to_string() + .contains("unresolved") + ); + assert!(with_dispositions(&store, &subject.id, &batch, &ready).is_err()); + store + .decide_candidate( + &subject.id, + CandidateDecisionRequest { + request_id: "decision".into(), + candidate_id: "candidate".into(), + reason: "invalid".into(), + decision: CandidateDecision::Close { + action: StagingResolutionAction::Invalid, + affected_memory: vec![], + }, + }, + ) + .unwrap(); + validate_result(&store, &subject.id, &batch, "job", "attempt", &ready).unwrap(); + let enriched = with_dispositions(&store, &subject.id, &batch, &ready).unwrap(); + assert_eq!( + enriched["candidate_dispositions"], + serde_json::json!([{"candidate_id": "candidate", "action": "invalid"}]) + ); + assert_eq!( + enriched, + with_dispositions(&store, &subject.id, &batch, &ready).unwrap() + ); + assert!(validate_result(&store, &subject.id, &batch, "job", "other-attempt", &ready).is_err()); + assert!(validate_result(&store, &subject.id, &[], "job", "attempt", &ready).is_err()); + // A current ready snapshot survives a redundant editor failure. Advance + // Memory first so this attempt genuinely needs a new surface and records failure. + store + .create_memory( + &subject.id, + crate::MemoryDraft::active( + CandidateKind::Lesson, + "new lesson", + "new body", + "reason", + "change", + ), + ) + .unwrap(); + let failure = store + .prepare_job_surface_generation(&subject.id, Some(("job", "attempt"))) + .unwrap(); + let revision = store + .fail_surface_generation(&subject.id, &failure.id, "model_failure") + .unwrap(); + let failed = serde_json::json!({"subject_id": subject.id, "candidate_ids": batch, + "surface": {"availability": "failed", "generation_id": failure.id, "store_revision": revision, "reason_code": "model_failure"}}); + validate_result(&store, &subject.id, &batch, "job", "attempt", &failed).unwrap(); + assert!(validate_result(&store, &subject.id, &batch, "job", "attempt", &ready).is_err()); + let mut false_reason = failed; + false_reason["surface"]["reason_code"] = serde_json::json!("invented"); + assert!(validate_result(&store, &subject.id, &batch, "job", "attempt", &false_reason).is_err()); +} diff --git a/crates/subjektiv/src/lib.rs b/crates/subjektiv/src/lib.rs new file mode 100644 index 000000000..9865a770d --- /dev/null +++ b/crates/subjektiv/src/lib.rs @@ -0,0 +1,6182 @@ +//! Subject-scoped, revisioned Memory storage for the `subjektiv` Feature. +//! +//! This module owns subjektiv's domain schema and typed repository. Physical +//! SQLite placement, connection configuration, migration execution, backup, +//! restore, and teardown remain owned by [`feature_storage`]. A store is +//! already bound to one Host storage scope; a subject identifier is therefore a lookup +//! key, not an authorization credential. + +pub mod api; +pub mod job; + +use std::collections::HashSet; + +use chrono::{SecondsFormat, Utc}; +use memory::extract::{CandidateKind, STAGING_SCHEMA_VERSION, StagingEvidence, StagingRecord}; +use memory::schema::{SourceEvidenceRef, SourceRef}; +use rusqlite::{Connection, OptionalExtension, Transaction, params}; +use serde::{Deserialize, Deserializer, Serialize}; +use uuid::Uuid; + +use feature_storage::{ + FeatureDatabase, FeatureMigration, FeatureRegistration, FeatureStorageError, RegisteredFeature, + ScopedFeatureStorage, +}; + +pub const SUBJEKTIV_SCHEMA_VERSION: u32 = 1; +pub const SUBJEKTIV_FEATURE_ID: &str = "subjektiv"; +pub const SUBJECT_WORKER_SINGLETON_PREFIX: &str = "subjektiv:"; +pub const MAX_STAGING_ANCHORS: usize = 10; +/// Maximum UTF-8 size of the user-managed Subject behavior document. +pub const MAX_SUBJECT_BEHAVIOR_BYTES: usize = server_api::SUBJEKTIV_MAX_BEHAVIOR_BYTES; +/// Deterministic surface-generation policy. Input estimates use the repository's +/// provider-independent UTF-8 byte estimate (`ceil(bytes / 4)`). The material +/// payload is capped below the whole-input budget to reserve room for the fixed +/// prompt and JSON framing. +pub const SURFACE_BODY_TOKEN_BUDGET: usize = 1_024; +pub const SURFACE_INPUT_TOKEN_BUDGET: usize = 12_000; +pub const SURFACE_MATERIAL_TOKEN_BUDGET: usize = 10_000; +pub const SURFACE_PER_KIND_LIMIT: usize = 8; +pub const SURFACE_TOTAL_MATERIAL_LIMIT: usize = 24; + +pub type Result = std::result::Result; + +#[derive(Debug, thiserror::Error)] +pub enum SubjektivError { + #[error(transparent)] + Storage(#[from] FeatureStorageError), + #[error("invalid subjektiv record: {0}")] + InvalidRecord(String), + #[error("subject `{0}` was not found in this Workspace")] + SubjectNotFound(String), + #[error("subject `{0}` is retired")] + SubjectRetired(String), + #[error("the selected Worker Profile does not enable subjektiv")] + ProfileDisabled, + #[error("staging candidate `{0}` was not found for this subject")] + CandidateNotFound(String), + #[error("staging candidate `{0}` already exists with different content")] + CandidateConflict(String), + #[error("staging candidate `{0}` is already resolved")] + CandidateResolved(String), + #[error("candidate decision request `{0}` was already used with different input")] + DecisionRequestConflict(String), + #[error( + "session `{session_id}` is already attributed to subject `{existing_subject_id}` and cannot be attributed to `{requested_subject_id}`" + )] + SessionSubjectConflict { + session_id: String, + existing_subject_id: String, + requested_subject_id: String, + }, + #[error("session `{0}` already has different historical attribution")] + SessionAttributionConflict(String), + #[error("memory `{0}` was not found for this subject")] + MemoryNotFound(String), + #[error("memory `{memory_id}` revision conflict: expected {expected}, current {actual}")] + RevisionConflict { + memory_id: String, + expected: u64, + actual: u64, + }, + #[error( + "subject `{subject_id}` behavior revision conflict: expected {expected}, current {actual}" + )] + SubjectBehaviorConflict { + subject_id: String, + expected: u64, + actual: u64, + }, + #[error("surface generation conflict: {0}")] + SurfaceGenerationConflict(String), + #[error("memory `{memory_id}` cannot transition from {from:?} to {to:?}")] + InvalidStateTransition { + memory_id: String, + from: MemoryState, + to: MemoryState, + }, + #[error("reference `{reference}` does not belong to subject `{subject_id}`")] + SubjectScopeMismatch { + subject_id: String, + reference: String, + }, + #[error("subjektiv JSON error: {0}")] + Json(#[from] serde_json::Error), +} + +impl From for SubjektivError { + fn from(error: rusqlite::Error) -> Self { + Self::Storage(FeatureStorageError::from(error)) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[serde(transparent)] +pub struct SubjectRole(String); + +impl SubjectRole { + pub fn new(value: impl Into) -> Result { + let value = value.into(); + validate_label("subject role", &value)?; + Ok(Self(value)) + } + + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl<'de> Deserialize<'de> for SubjectRole { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let value = String::deserialize(deserializer)?; + SubjectRole::new(value).map_err(serde::de::Error::custom) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum SubjectState { + Active, + Retired, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SubjectRecord { + pub schema_version: u32, + pub id: String, + pub role: SubjectRole, + /// User-managed, host-injected behavior document. This is not Memory and + /// Memory lifecycle operations must preserve it byte-for-byte. + #[serde(default)] + pub behavior_md: String, + /// Monotonic CAS generation for behavior updates, independent of Memory's + /// `store_revision`. + #[serde(default)] + pub behavior_revision: u64, + pub state: SubjectState, + /// Monotonic generation incremented exactly once for every committed Memory + /// creation or revision for this subject. + pub store_revision: u64, + pub created_at: String, + pub updated_at: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SubjectPage { + pub items: Vec, + pub has_more: bool, +} + +/// Backend-authorized launch material for one active subject. The singleton key +/// is derived by subjektiv and then acquired through Yoi's generic singleton path. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AuthorizedSubjectWorkerAttachment { + pub singleton_key: String, +} + +pub fn subject_worker_singleton_key(subject_id: &str) -> Result { + validate_label("subject id", subject_id)?; + if subject_id.trim() != subject_id { + return Err(SubjektivError::InvalidRecord( + "subject id must not contain surrounding whitespace".to_string(), + )); + } + Ok(format!("{SUBJECT_WORKER_SINGLETON_PREFIX}{subject_id}")) +} + +/// Immutable host-recorded history that attributes one durable Session to the +/// subject and Worker that produced it. This is archival provenance only: it is +/// deliberately not a current-Worker link or execution ownership record. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SubjectSessionAttribution { + pub schema_version: u32, + pub subject_id: String, + pub runtime_id: String, + pub worker_id: String, + pub session_id: String, + pub attributed_at: String, +} + +impl SubjectSessionAttribution { + pub fn new( + subject_id: impl Into, + runtime_id: impl Into, + worker_id: impl Into, + session_id: impl Into, + ) -> Result { + let record = Self { + schema_version: SUBJEKTIV_SCHEMA_VERSION, + subject_id: subject_id.into(), + runtime_id: runtime_id.into(), + worker_id: worker_id.into(), + session_id: session_id.into(), + attributed_at: now(), + }; + validate_session_attribution(&record)?; + Ok(record) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SubjectSessionAttributionPage { + pub items: Vec, + pub has_more: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RevisionProposalIntent { + Revise, + Resolve, + Retract, + Reopen, +} + +/// Host-owned metadata describing how a staged candidate is expected to change +/// one existing Memory. It is validated against the current scoped Memory when +/// the candidate is staged; it is not model-authored and does not itself apply +/// the revision. +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct RevisionProposal { + pub intent: RevisionProposalIntent, + pub memory_id: String, + pub expected_revision: u64, + pub change_reason: String, +} + +impl RevisionProposal { + pub fn new( + intent: RevisionProposalIntent, + memory_id: impl Into, + expected_revision: u64, + change_reason: impl Into, + ) -> Result { + let proposal = Self { + intent, + memory_id: memory_id.into(), + expected_revision, + change_reason: change_reason.into(), + }; + validate_revision_proposal_metadata(&proposal)?; + Ok(proposal) + } +} + +impl<'de> Deserialize<'de> for RevisionProposal { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(deny_unknown_fields)] + struct WireProposal { + intent: RevisionProposalIntent, + memory_id: String, + expected_revision: u64, + change_reason: String, + } + + let proposal = WireProposal::deserialize(deserializer)?; + RevisionProposal::new( + proposal.intent, + proposal.memory_id, + proposal.expected_revision, + proposal.change_reason, + ) + .map_err(serde::de::Error::custom) + } +} + +/// The existing extraction schema with only host-owned subject scope, +/// persistence time, and optional revision proposal metadata added. Model input +/// still contains none of those fields. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SubjectStagingRecord { + pub schema_version: u32, + pub id: String, + pub subject_id: String, + pub extract_run_id: String, + pub source: SourceRef, + pub kind: CandidateKind, + pub claim: String, + pub why_useful: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub staleness: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub evidence: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub source_refs: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub revision_proposal: Option, + pub created_at: String, +} + +impl SubjectStagingRecord { + /// Attaches host-resolved subject scope and time to an existing v2 staging + /// record without changing its taxonomy, evidence, or source anchors. + pub fn attach(subject_id: impl Into, record: StagingRecord) -> Self { + Self::attach_at(subject_id, record, now()) + } + + fn attach_at(subject_id: impl Into, record: StagingRecord, created_at: String) -> Self { + Self { + schema_version: record.schema_version, + id: record.id, + subject_id: subject_id.into(), + extract_run_id: record.extract_run_id, + source: record.source, + kind: record.kind, + claim: record.claim, + why_useful: record.why_useful, + staleness: record.staleness, + evidence: record.evidence, + source_refs: record.source_refs, + revision_proposal: None, + created_at, + } + } + + /// Attaches host-owned revision proposal metadata to this candidate. + pub fn with_revision_proposal(mut self, proposal: RevisionProposal) -> Result { + validate_revision_proposal_metadata(&proposal)?; + self.revision_proposal = Some(proposal); + Ok(self) + } + + /// Mutably attaches host-owned revision proposal metadata to this candidate. + pub fn attach_revision_proposal(&mut self, proposal: RevisionProposal) -> Result<()> { + validate_revision_proposal_metadata(&proposal)?; + self.revision_proposal = Some(proposal); + Ok(()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum MemoryState { + /// Current and eligible for ordinary recall. This does not assert truth and + /// grants no authority. + Active, + /// No longer current because its question, condition, or applicability has + /// ended. A later revision may reopen it when circumstances change. + Resolved, + /// Invalidated and no longer eligible for recall. Retraction is terminal; + /// corrected follow-up experience is a separate Memory linked by derivation. + Retracted, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct MemoryRevisionRef { + pub memory_id: String, + pub revision: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct MemoryRecord { + pub schema_version: u32, + pub id: String, + pub subject_id: String, + pub revision: u64, + pub kind: CandidateKind, + pub state: MemoryState, + pub claim: String, + pub body_md: String, + pub why_useful: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub staleness: Option, + #[serde(default)] + pub source_candidate_ids: Vec, + #[serde(default)] + pub derived_from: Vec, + pub change_reason: String, + pub created_at: String, + pub updated_at: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct MemoryDraft { + pub kind: CandidateKind, + pub state: MemoryState, + pub claim: String, + pub body_md: String, + pub why_useful: String, + pub staleness: Option, + pub source_candidate_ids: Vec, + pub derived_from: Vec, + pub change_reason: String, +} + +impl MemoryDraft { + pub fn active( + kind: CandidateKind, + claim: impl Into, + body_md: impl Into, + why_useful: impl Into, + change_reason: impl Into, + ) -> Self { + Self { + kind, + state: MemoryState::Active, + claim: claim.into(), + body_md: body_md.into(), + why_useful: why_useful.into(), + staleness: None, + source_candidate_ids: Vec::new(), + derived_from: Vec::new(), + change_reason: change_reason.into(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum StagingResolutionAction { + Applied, + Discarded, + Invalid, + Duplicate, + AlreadyCovered, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct StagingResolution { + pub schema_version: u32, + pub id: String, + pub subject_id: String, + pub candidate_id: String, + pub action: StagingResolutionAction, + pub reason: String, + #[serde(default)] + pub affected_memory: Vec, + /// Immutable copy of the candidate as it was staged. The database also + /// retains the exact serialized bytes independently of this projection. + pub candidate: SubjectStagingRecord, + pub resolved_at: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SurfaceSnapshot { + pub schema_version: u32, + pub id: String, + pub subject_id: String, + pub body_md: String, + #[serde(default)] + pub memory_refs: Vec, + pub built_from_store_revision: u64, + pub created_at: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SurfaceMaterial { + pub memory_id: String, + pub revision: u64, + pub kind: CandidateKind, + pub body_md: String, + pub why_useful: String, + pub staleness: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SurfaceGeneration { + pub id: String, + pub subject_id: String, + pub store_revision: u64, + pub active_memory_count: usize, + pub materials: Vec, + pub created_at: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SurfacePoint { + pub body_md: String, + pub memory_refs: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SurfaceAvailability { + Ungenerated, + Stale, + Failed, + Ready, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResidentSurface { + pub availability: SurfaceAvailability, + pub snapshot: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SubjectResidentContext { + pub subject: SubjectRecord, + pub surface: ResidentSurface, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum MemoryRevisionTarget { + Create, + Revise { + memory_id: String, + expected_revision: u64, + }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum MemoryDecisionOperation { + Create, + Revise, + Reference, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum CandidateDecision { + Apply { + target: MemoryRevisionTarget, + draft: MemoryDraft, + }, + Close { + action: StagingResolutionAction, + #[serde(default)] + affected_memory: Vec, + }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct CandidateDecisionRequest { + pub request_id: String, + pub candidate_id: String, + pub reason: String, + pub decision: CandidateDecision, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct CandidateDecisionReceipt { + pub request_id: String, + pub candidate_id: String, + pub resolution: StagingResolution, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub memory: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub operation: Option, + pub store_revision: u64, + pub surface_dirty: bool, +} + +fn create_schema(transaction: &Transaction<'_>) -> feature_storage::Result<()> { + transaction.execute_batch( + r#" +CREATE TABLE store_scope ( + singleton INTEGER PRIMARY KEY CHECK (singleton = 1), + workspace_id TEXT NOT NULL UNIQUE +); + +CREATE TABLE subjects ( + subject_id TEXT PRIMARY KEY, + role TEXT NOT NULL, + state TEXT NOT NULL CHECK (state IN ('active', 'retired')), + store_revision INTEGER NOT NULL CHECK (store_revision >= 0), + record_json TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +CREATE TABLE staging_records ( + subject_id TEXT NOT NULL, + candidate_id TEXT NOT NULL, + kind TEXT NOT NULL CHECK ( + kind IN ( + 'preference', 'working_assumption', 'constraint', + 'decision', 'open_question', 'lesson' + ) + ), + record_json TEXT NOT NULL, + created_at TEXT NOT NULL, + PRIMARY KEY (subject_id, candidate_id), + FOREIGN KEY (subject_id) REFERENCES subjects(subject_id) ON DELETE RESTRICT +); + +CREATE TABLE memory_records ( + subject_id TEXT NOT NULL, + memory_id TEXT NOT NULL, + current_revision INTEGER NOT NULL CHECK (current_revision > 0), + kind TEXT NOT NULL CHECK ( + kind IN ( + 'preference', 'working_assumption', 'constraint', + 'decision', 'open_question', 'lesson' + ) + ), + state TEXT NOT NULL CHECK (state IN ('active', 'resolved', 'retracted')), + record_json TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + PRIMARY KEY (subject_id, memory_id), + FOREIGN KEY (subject_id) REFERENCES subjects(subject_id) ON DELETE RESTRICT, + FOREIGN KEY (subject_id, memory_id, current_revision) + REFERENCES memory_revisions(subject_id, memory_id, revision) + DEFERRABLE INITIALLY DEFERRED +); + +CREATE TABLE memory_revisions ( + subject_id TEXT NOT NULL, + memory_id TEXT NOT NULL, + revision INTEGER NOT NULL CHECK (revision > 0), + kind TEXT NOT NULL CHECK ( + kind IN ( + 'preference', 'working_assumption', 'constraint', + 'decision', 'open_question', 'lesson' + ) + ), + state TEXT NOT NULL CHECK (state IN ('active', 'resolved', 'retracted')), + record_json TEXT NOT NULL, + created_at TEXT NOT NULL, + PRIMARY KEY (subject_id, memory_id, revision), + FOREIGN KEY (subject_id, memory_id) + REFERENCES memory_records(subject_id, memory_id) + ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED +); + +CREATE TABLE memory_revision_candidates ( + subject_id TEXT NOT NULL, + memory_id TEXT NOT NULL, + revision INTEGER NOT NULL, + candidate_id TEXT NOT NULL, + PRIMARY KEY (subject_id, memory_id, revision, candidate_id), + FOREIGN KEY (subject_id, memory_id, revision) + REFERENCES memory_revisions(subject_id, memory_id, revision) ON DELETE RESTRICT, + FOREIGN KEY (subject_id, candidate_id) + REFERENCES staging_records(subject_id, candidate_id) ON DELETE RESTRICT +); + +CREATE TABLE memory_revision_derivations ( + subject_id TEXT NOT NULL, + memory_id TEXT NOT NULL, + revision INTEGER NOT NULL, + source_memory_id TEXT NOT NULL, + source_revision INTEGER NOT NULL, + PRIMARY KEY ( + subject_id, memory_id, revision, source_memory_id, source_revision + ), + FOREIGN KEY (subject_id, memory_id, revision) + REFERENCES memory_revisions(subject_id, memory_id, revision) ON DELETE RESTRICT, + FOREIGN KEY (subject_id, source_memory_id, source_revision) + REFERENCES memory_revisions(subject_id, memory_id, revision) ON DELETE RESTRICT +); + +CREATE TABLE staging_resolutions ( + subject_id TEXT NOT NULL, + candidate_id TEXT NOT NULL, + resolution_id TEXT NOT NULL, + action TEXT NOT NULL CHECK ( + action IN ('applied', 'discarded', 'invalid', 'duplicate', 'already_covered') + ), + reason TEXT NOT NULL, + affected_refs_json TEXT NOT NULL, + staging_raw_json TEXT NOT NULL, + resolution_json TEXT NOT NULL, + resolved_at TEXT NOT NULL, + PRIMARY KEY (subject_id, candidate_id), + UNIQUE (resolution_id), + FOREIGN KEY (subject_id, candidate_id) + REFERENCES staging_records(subject_id, candidate_id) ON DELETE RESTRICT +); + +CREATE TABLE staging_resolution_targets ( + subject_id TEXT NOT NULL, + candidate_id TEXT NOT NULL, + memory_id TEXT NOT NULL, + revision INTEGER NOT NULL, + PRIMARY KEY (subject_id, candidate_id, memory_id, revision), + FOREIGN KEY (subject_id, candidate_id) + REFERENCES staging_resolutions(subject_id, candidate_id) ON DELETE RESTRICT, + FOREIGN KEY (subject_id, memory_id, revision) + REFERENCES memory_revisions(subject_id, memory_id, revision) ON DELETE RESTRICT +); + +CREATE TABLE surface_snapshots ( + subject_id TEXT NOT NULL, + snapshot_id TEXT NOT NULL, + built_from_store_revision INTEGER NOT NULL CHECK (built_from_store_revision >= 0), + snapshot_json TEXT NOT NULL, + created_at TEXT NOT NULL, + PRIMARY KEY (subject_id, snapshot_id), + FOREIGN KEY (subject_id) REFERENCES subjects(subject_id) ON DELETE RESTRICT +); + +CREATE TABLE surface_snapshot_refs ( + subject_id TEXT NOT NULL, + snapshot_id TEXT NOT NULL, + memory_id TEXT NOT NULL, + revision INTEGER NOT NULL, + PRIMARY KEY (subject_id, snapshot_id, memory_id), + FOREIGN KEY (subject_id, snapshot_id) + REFERENCES surface_snapshots(subject_id, snapshot_id) ON DELETE RESTRICT, + FOREIGN KEY (subject_id, memory_id, revision) + REFERENCES memory_revisions(subject_id, memory_id, revision) ON DELETE RESTRICT +); + +CREATE TABLE memory_revision_seals ( + subject_id TEXT NOT NULL, + memory_id TEXT NOT NULL, + revision INTEGER NOT NULL, + PRIMARY KEY (subject_id, memory_id, revision), + FOREIGN KEY (subject_id, memory_id, revision) + REFERENCES memory_revisions(subject_id, memory_id, revision) ON DELETE RESTRICT +); + +CREATE TABLE staging_resolution_seals ( + subject_id TEXT NOT NULL, + candidate_id TEXT NOT NULL, + PRIMARY KEY (subject_id, candidate_id), + FOREIGN KEY (subject_id, candidate_id) + REFERENCES staging_resolutions(subject_id, candidate_id) ON DELETE RESTRICT +); + +CREATE TABLE surface_snapshot_seals ( + subject_id TEXT NOT NULL, + snapshot_id TEXT NOT NULL, + PRIMARY KEY (subject_id, snapshot_id), + FOREIGN KEY (subject_id, snapshot_id) + REFERENCES surface_snapshots(subject_id, snapshot_id) ON DELETE RESTRICT +); + +CREATE TRIGGER memory_revision_candidates_no_late_insert +BEFORE INSERT ON memory_revision_candidates +WHEN EXISTS ( + SELECT 1 FROM memory_revision_seals + WHERE subject_id = NEW.subject_id + AND memory_id = NEW.memory_id + AND revision = NEW.revision +) BEGIN + SELECT RAISE(ABORT, 'subjektiv revision evidence is sealed'); +END; +CREATE TRIGGER memory_revision_derivations_no_late_insert +BEFORE INSERT ON memory_revision_derivations +WHEN EXISTS ( + SELECT 1 FROM memory_revision_seals + WHERE subject_id = NEW.subject_id + AND memory_id = NEW.memory_id + AND revision = NEW.revision +) BEGIN + SELECT RAISE(ABORT, 'subjektiv derivations are sealed'); +END; +CREATE TRIGGER staging_resolution_targets_no_late_insert +BEFORE INSERT ON staging_resolution_targets +WHEN EXISTS ( + SELECT 1 FROM staging_resolution_seals + WHERE subject_id = NEW.subject_id + AND candidate_id = NEW.candidate_id +) BEGIN + SELECT RAISE(ABORT, 'subjektiv resolution targets are sealed'); +END; +CREATE TRIGGER surface_snapshot_refs_no_late_insert +BEFORE INSERT ON surface_snapshot_refs +WHEN EXISTS ( + SELECT 1 FROM surface_snapshot_seals + WHERE subject_id = NEW.subject_id + AND snapshot_id = NEW.snapshot_id +) BEGIN + SELECT RAISE(ABORT, 'subjektiv surface references are sealed'); +END; + +CREATE TRIGGER memory_revision_seals_no_update +BEFORE UPDATE ON memory_revision_seals BEGIN + SELECT RAISE(ABORT, 'subjektiv revision seals are immutable'); +END; +CREATE TRIGGER memory_revision_seals_no_delete +BEFORE DELETE ON memory_revision_seals BEGIN + SELECT RAISE(ABORT, 'subjektiv revision seals are retained'); +END; +CREATE TRIGGER staging_resolution_seals_no_update +BEFORE UPDATE ON staging_resolution_seals BEGIN + SELECT RAISE(ABORT, 'subjektiv resolution seals are immutable'); +END; +CREATE TRIGGER staging_resolution_seals_no_delete +BEFORE DELETE ON staging_resolution_seals BEGIN + SELECT RAISE(ABORT, 'subjektiv resolution seals are retained'); +END; +CREATE TRIGGER surface_snapshot_seals_no_update +BEFORE UPDATE ON surface_snapshot_seals BEGIN + SELECT RAISE(ABORT, 'subjektiv snapshot seals are immutable'); +END; +CREATE TRIGGER surface_snapshot_seals_no_delete +BEFORE DELETE ON surface_snapshot_seals BEGIN + SELECT RAISE(ABORT, 'subjektiv snapshot seals are retained'); +END; + +CREATE TRIGGER memory_revisions_no_update +BEFORE UPDATE ON memory_revisions BEGIN + SELECT RAISE(ABORT, 'subjektiv memory revisions are immutable'); +END; +CREATE TRIGGER memory_revisions_no_delete +BEFORE DELETE ON memory_revisions BEGIN + SELECT RAISE(ABORT, 'subjektiv memory revisions are retained'); +END; +CREATE TRIGGER memory_revision_candidates_no_update +BEFORE UPDATE ON memory_revision_candidates BEGIN + SELECT RAISE(ABORT, 'subjektiv revision evidence is immutable'); +END; +CREATE TRIGGER memory_revision_candidates_no_delete +BEFORE DELETE ON memory_revision_candidates BEGIN + SELECT RAISE(ABORT, 'subjektiv revision evidence is retained'); +END; +CREATE TRIGGER memory_revision_derivations_no_update +BEFORE UPDATE ON memory_revision_derivations BEGIN + SELECT RAISE(ABORT, 'subjektiv derivations are immutable'); +END; +CREATE TRIGGER memory_revision_derivations_no_delete +BEFORE DELETE ON memory_revision_derivations BEGIN + SELECT RAISE(ABORT, 'subjektiv derivations are retained'); +END; +CREATE TRIGGER staging_records_no_update +BEFORE UPDATE ON staging_records BEGIN + SELECT RAISE(ABORT, 'subjektiv staging records are immutable'); +END; +CREATE TRIGGER staging_records_no_delete +BEFORE DELETE ON staging_records BEGIN + SELECT RAISE(ABORT, 'subjektiv staging records are retained'); +END; +CREATE TRIGGER staging_resolutions_no_update +BEFORE UPDATE ON staging_resolutions BEGIN + SELECT RAISE(ABORT, 'subjektiv staging resolutions are immutable'); +END; +CREATE TRIGGER staging_resolutions_no_delete +BEFORE DELETE ON staging_resolutions BEGIN + SELECT RAISE(ABORT, 'subjektiv staging resolutions are retained'); +END; +CREATE TRIGGER staging_resolution_targets_no_update +BEFORE UPDATE ON staging_resolution_targets BEGIN + SELECT RAISE(ABORT, 'subjektiv resolution targets are immutable'); +END; +CREATE TRIGGER staging_resolution_targets_no_delete +BEFORE DELETE ON staging_resolution_targets BEGIN + SELECT RAISE(ABORT, 'subjektiv resolution targets are retained'); +END; +CREATE TRIGGER surface_snapshots_no_update +BEFORE UPDATE ON surface_snapshots BEGIN + SELECT RAISE(ABORT, 'subjektiv surface snapshots are immutable'); +END; +CREATE TRIGGER surface_snapshots_no_delete +BEFORE DELETE ON surface_snapshots BEGIN + SELECT RAISE(ABORT, 'subjektiv surface snapshots are retained'); +END; +CREATE TRIGGER surface_snapshot_refs_no_update +BEFORE UPDATE ON surface_snapshot_refs BEGIN + SELECT RAISE(ABORT, 'subjektiv surface references are immutable'); +END; +CREATE TRIGGER surface_snapshot_refs_no_delete +BEFORE DELETE ON surface_snapshot_refs BEGIN + SELECT RAISE(ABORT, 'subjektiv surface references are retained'); +END; +"#, + )?; + Ok(()) +} + +fn add_subject_session_attribution(transaction: &Transaction<'_>) -> feature_storage::Result<()> { + transaction.execute_batch( + r#" +CREATE TABLE subject_session_attributions ( + session_id TEXT PRIMARY KEY, + subject_id TEXT NOT NULL, + runtime_id TEXT NOT NULL, + worker_id TEXT NOT NULL, + record_json TEXT NOT NULL, + attributed_at TEXT NOT NULL, + FOREIGN KEY (subject_id) REFERENCES subjects(subject_id) ON DELETE RESTRICT +); + +CREATE INDEX subject_session_attributions_by_subject +ON subject_session_attributions(subject_id, attributed_at, session_id); + +CREATE TRIGGER subject_session_attributions_no_update +BEFORE UPDATE ON subject_session_attributions BEGIN + SELECT RAISE(ABORT, 'subjektiv session attribution is immutable'); +END; +CREATE TRIGGER subject_session_attributions_no_delete +BEFORE DELETE ON subject_session_attributions BEGIN + SELECT RAISE(ABORT, 'subjektiv session attribution is retained'); +END; +"#, + )?; + Ok(()) +} + +fn add_candidate_decision_receipts(transaction: &Transaction<'_>) -> feature_storage::Result<()> { + transaction.execute_batch( + r#" +CREATE TABLE candidate_decision_receipts ( + subject_id TEXT NOT NULL, + request_id TEXT NOT NULL, + candidate_id TEXT NOT NULL, + request_json TEXT NOT NULL, + receipt_json TEXT NOT NULL, + created_at TEXT NOT NULL, + PRIMARY KEY (subject_id, request_id), + UNIQUE (subject_id, candidate_id), + FOREIGN KEY (subject_id, candidate_id) + REFERENCES staging_resolutions(subject_id, candidate_id) ON DELETE RESTRICT +); + +CREATE TRIGGER candidate_decision_receipts_no_update +BEFORE UPDATE ON candidate_decision_receipts BEGIN + SELECT RAISE(ABORT, 'subjektiv candidate decision receipts are immutable'); +END; +CREATE TRIGGER candidate_decision_receipts_no_delete +BEFORE DELETE ON candidate_decision_receipts BEGIN + SELECT RAISE(ABORT, 'subjektiv candidate decision receipts are retained'); +END; +"#, + )?; + Ok(()) +} + +fn add_surface_generation_state(transaction: &Transaction<'_>) -> feature_storage::Result<()> { + transaction.execute_batch( + r#" +CREATE TABLE surface_generation_state ( + subject_id TEXT PRIMARY KEY, + store_revision INTEGER NOT NULL CHECK (store_revision >= 0), + status TEXT NOT NULL CHECK (status IN ('dirty', 'failed', 'ready')), + snapshot_id TEXT, + reason_code TEXT, + updated_at TEXT NOT NULL, + FOREIGN KEY (subject_id) REFERENCES subjects(subject_id) ON DELETE RESTRICT, + FOREIGN KEY (subject_id, snapshot_id) + REFERENCES surface_snapshots(subject_id, snapshot_id) ON DELETE RESTRICT +); + +INSERT INTO surface_generation_state ( + subject_id, store_revision, status, snapshot_id, reason_code, updated_at +) +SELECT subjects.subject_id, + subjects.store_revision, + 'dirty', + NULL, + 'legacy_snapshot_requires_regeneration', + subjects.updated_at +FROM subjects +WHERE EXISTS ( + SELECT 1 FROM surface_snapshots any_snapshot + WHERE any_snapshot.subject_id = subjects.subject_id +); + +CREATE TABLE surface_generation_runs ( + subject_id TEXT NOT NULL, + generation_id TEXT NOT NULL, + store_revision INTEGER NOT NULL CHECK (store_revision >= 0), + active_memory_count INTEGER NOT NULL CHECK (active_memory_count >= 0), + materials_json TEXT NOT NULL, + created_at TEXT NOT NULL, + PRIMARY KEY (subject_id, generation_id), + FOREIGN KEY (subject_id) REFERENCES subjects(subject_id) ON DELETE RESTRICT +); + +CREATE INDEX surface_generation_runs_by_revision +ON surface_generation_runs(subject_id, store_revision, created_at, generation_id); + +CREATE TRIGGER surface_generation_runs_no_update +BEFORE UPDATE ON surface_generation_runs BEGIN + SELECT RAISE(ABORT, 'subjektiv surface generation inputs are immutable'); +END; +CREATE TRIGGER surface_generation_runs_no_delete +BEFORE DELETE ON surface_generation_runs BEGIN + SELECT RAISE(ABORT, 'subjektiv surface generation inputs are retained'); +END; +"#, + )?; + Ok(()) +} + +fn add_subject_behavior(transaction: &Transaction<'_>) -> feature_storage::Result<()> { + transaction.execute_batch( + r#" +ALTER TABLE subjects ADD COLUMN behavior_md TEXT NOT NULL DEFAULT ''; +ALTER TABLE subjects ADD COLUMN behavior_revision INTEGER NOT NULL DEFAULT 0 + CHECK (behavior_revision >= 0); +"#, + )?; + Ok(()) +} + +fn add_surface_job_provenance(transaction: &Transaction<'_>) -> feature_storage::Result<()> { + transaction.execute_batch("ALTER TABLE surface_generation_runs ADD COLUMN job_id TEXT; ALTER TABLE surface_generation_runs ADD COLUMN attempt_id TEXT;")?; + Ok(()) +} + +static MIGRATIONS: &[FeatureMigration] = &[ + FeatureMigration::new(1, "create subjektiv subject memory store", create_schema), + FeatureMigration::new( + 2, + "add immutable subject session attribution", + add_subject_session_attribution, + ), + FeatureMigration::new( + 3, + "add idempotent atomic candidate decision receipts", + add_candidate_decision_receipts, + ), + FeatureMigration::new( + 4, + "add bounded surface generation state and runs", + add_surface_generation_state, + ), + FeatureMigration::new(5, "add user-managed subject behavior", add_subject_behavior), + FeatureMigration::new( + 6, + "bind surface generation provenance to Job attempts", + add_surface_job_provenance, + ), +]; + +pub const REGISTRATION: FeatureRegistration = + FeatureRegistration::new(SUBJEKTIV_FEATURE_ID, MIGRATIONS); + +#[derive(Clone)] +pub struct SubjektivStore { + database: FeatureDatabase, +} + +impl SubjektivStore { + /// Registers the trusted schema with the neutral Host storage manager. + /// One registration can be reused across all scopes served by that manager. + pub fn register(storage: &ScopedFeatureStorage) -> Result { + storage.register(REGISTRATION).map_err(Into::into) + } + + pub fn open(storage: &ScopedFeatureStorage, registration: &RegisteredFeature) -> Result { + if registration.feature_id() != SUBJEKTIV_FEATURE_ID { + return Err(SubjektivError::InvalidRecord(format!( + "registration is for Feature `{}` rather than `{SUBJEKTIV_FEATURE_ID}`", + registration.feature_id() + ))); + } + Self::from_database(storage.open(registration)?) + } + + /// Opens a trusted managed database. `store_scope.workspace_id` retains its + /// historical SQL name: it stores the physical Host storage-scope identity. + /// Backend uses its Workspace ID; local Hosts use a persisted random scope, + /// without creating a Workspace. Subject IDs are separate domain identities. + pub fn from_database(database: FeatureDatabase) -> Result { + if database.feature_id() != SUBJEKTIV_FEATURE_ID { + return Err(SubjektivError::InvalidRecord( + "not a subjektiv database".into(), + )); + } + let workspace_id = database.scope_id().to_string(); + database.try_transaction::<_, SubjektivError>(|transaction| { + let stored = transaction + .query_row( + "SELECT workspace_id FROM store_scope WHERE singleton = 1", + [], + |row| row.get::<_, String>(0), + ) + .optional()?; + match stored { + Some(stored) if stored != workspace_id => { + Err(SubjektivError::SubjectScopeMismatch { + subject_id: "".to_string(), + reference: format!( + "database is bound to Workspace `{stored}`, not `{workspace_id}`" + ), + }) + } + Some(_) => Ok(()), + None => { + transaction.execute( + "INSERT INTO store_scope (singleton, workspace_id) VALUES (1, ?1)", + [&workspace_id], + )?; + Ok(()) + } + } + })?; + Ok(Self { database }) + } + + pub fn scope_id(&self) -> &str { + self.database.scope_id() + } + + /// Backend compatibility alias; see [`Self::from_database`] for scope semantics. + pub fn workspace_id(&self) -> &str { + self.scope_id() + } + + /// Issues a random host-owned subject id. It is never derived from Worker, + /// Profile, Session, or Memory content. + pub fn create_subject(&self, role: SubjectRole) -> Result { + self.create_subject_with_behavior(role, String::new()) + } + + /// Creates a Subject with its optional user-managed behavior document. + pub fn create_subject_with_behavior( + &self, + role: SubjectRole, + behavior_md: String, + ) -> Result { + validate_subject_behavior(&behavior_md)?; + let timestamp = now(); + let record = SubjectRecord { + schema_version: SUBJEKTIV_SCHEMA_VERSION, + id: issued_id("subject"), + role, + behavior_md, + behavior_revision: 0, + state: SubjectState::Active, + store_revision: 0, + created_at: timestamp.clone(), + updated_at: timestamp, + }; + let raw = serde_json::to_string(&record)?; + self.database.try_transaction(|transaction| { + transaction.execute( + "INSERT INTO subjects ( + subject_id, role, behavior_md, behavior_revision, state, + store_revision, record_json, created_at, updated_at + ) VALUES (?1, ?2, ?3, 0, 'active', 0, ?4, ?5, ?6)", + params![ + record.id, + record.role.as_str(), + record.behavior_md, + raw, + record.created_at, + record.updated_at + ], + )?; + Ok(record) + }) + } + + pub fn subject(&self, subject_id: &str) -> Result> { + self.database.try_with_connection(|connection| { + let raw = connection + .query_row( + "SELECT record_json FROM subjects WHERE subject_id = ?1", + [subject_id], + |row| row.get::<_, String>(0), + ) + .optional()?; + raw.map(|raw| parse_subject(&raw)).transpose() + }) + } + + /// Replaces or clears the user-managed behavior document using its own CAS + /// generation. Memory revisions do not participate in this generation. + pub fn update_subject_behavior( + &self, + subject_id: &str, + expected_behavior_revision: u64, + behavior_md: String, + ) -> Result { + validate_subject_behavior(&behavior_md)?; + self.database.try_transaction(|transaction| { + let mut subject = require_subject(transaction, subject_id)?; + if subject.behavior_revision != expected_behavior_revision { + return Err(SubjektivError::SubjectBehaviorConflict { + subject_id: subject_id.to_string(), + expected: expected_behavior_revision, + actual: subject.behavior_revision, + }); + } + if subject.behavior_md == behavior_md { + return Ok(subject); + } + subject.behavior_md = behavior_md; + subject.behavior_revision = + subject.behavior_revision.checked_add(1).ok_or_else(|| { + SubjektivError::InvalidRecord("subject behavior revision overflow".to_string()) + })?; + subject.updated_at = now(); + let raw = serde_json::to_string(&subject)?; + transaction.execute( + "UPDATE subjects + SET behavior_md = ?2, behavior_revision = ?3, + record_json = ?4, updated_at = ?5 + WHERE subject_id = ?1", + params![ + subject.id, + subject.behavior_md, + to_i64(subject.behavior_revision)?, + raw, + subject.updated_at + ], + )?; + Ok(subject) + }) + } + + /// Validates an explicit product launch connection against this Workspace's + /// subject store and the already-resolved Profile policy. Subject knowledge + /// alone is insufficient: only an active local record produces host launch material. + pub fn authorize_worker_attachment( + &self, + subject_id: &str, + profile: &manifest::WorkerManifest, + ) -> Result { + if !profile.feature.subjektiv.profile.enabled { + return Err(SubjektivError::ProfileDisabled); + } + let subject = self + .subject(subject_id)? + .ok_or_else(|| SubjektivError::SubjectNotFound(subject_id.to_string()))?; + if subject.state != SubjectState::Active { + return Err(SubjektivError::SubjectRetired(subject_id.to_string())); + } + Ok(AuthorizedSubjectWorkerAttachment { + singleton_key: subject_worker_singleton_key(&subject.id)?, + }) + } + + /// Lists the first deterministic bounded page of subjects. Ordering uses + /// immutable creation time with the host-issued subject id as a total-order + /// tie breaker. + pub fn list_subjects(&self, limit: usize) -> Result { + self.list_subjects_after(limit, None) + } + + /// Lists a deterministic bounded keyset page after an exact `(created_at, + /// subject_id)` cursor. The cursor fields are wrapped by the public API in + /// an opaque Workspace-bound token. + pub fn list_subjects_after( + &self, + limit: usize, + after: Option<(&str, &str)>, + ) -> Result { + if !(1..=100).contains(&limit) { + return Err(SubjektivError::InvalidRecord( + "subject list limit must be between 1 and 100".to_string(), + )); + } + let query_limit = limit.saturating_add(1); + self.database.try_with_connection(|connection| { + let sql = if after.is_some() { + "SELECT record_json FROM subjects + WHERE created_at > ?1 OR (created_at = ?1 AND subject_id > ?2) + ORDER BY created_at ASC, subject_id ASC + LIMIT ?3" + } else { + "SELECT record_json FROM subjects + ORDER BY created_at ASC, subject_id ASC + LIMIT ?1" + }; + let mut statement = connection.prepare(sql)?; + let mut items = Vec::new(); + if let Some((created_at, subject_id)) = after { + let rows = statement.query_map( + params![created_at, subject_id, to_i64(query_limit as u64)?], + |row| row.get::<_, String>(0), + )?; + for row in rows { + items.push(parse_subject(&row?)?); + } + } else { + let rows = statement + .query_map([to_i64(query_limit as u64)?], |row| row.get::<_, String>(0))?; + for row in rows { + items.push(parse_subject(&row?)?); + } + } + let has_more = items.len() > limit; + if has_more { + items.truncate(limit); + } + Ok(SubjectPage { items, has_more }) + }) + } + + /// Subject retirement is distinct from stopping any current Worker. It does + /// not edit or delete Memory and cannot be reversed by this baseline schema. + pub fn retire_subject(&self, subject_id: &str) -> Result { + self.database.try_transaction(|transaction| { + let mut subject = require_subject(transaction, subject_id)?; + if subject.state == SubjectState::Retired { + return Ok(subject); + } + subject.state = SubjectState::Retired; + subject.updated_at = now(); + let raw = serde_json::to_string(&subject)?; + transaction.execute( + "UPDATE subjects + SET state = 'retired', record_json = ?2, updated_at = ?3 + WHERE subject_id = ?1", + params![subject_id, raw, subject.updated_at], + )?; + Ok(subject) + }) + } + + pub fn stage_candidate(&self, record: SubjectStagingRecord) -> Result { + validate_staging_record(self.workspace_id(), &record)?; + validate_staging_admission(&record)?; + let raw = serde_json::to_string(&record)?; + self.database.try_transaction(|transaction| { + require_active_subject(transaction, &record.subject_id)?; + write_staging_candidate(transaction, self.workspace_id(), record, raw) + }) + } + + /// Atomically verifies immutable Host attribution already completed at the + /// Session lifecycle boundary and stages its extracted candidate. Candidate + /// staging never creates or repairs Session attribution. + pub fn stage_candidate_with_attribution( + &self, + mut record: SubjectStagingRecord, + attribution: SubjectSessionAttribution, + ) -> Result<(SubjectStagingRecord, SubjectSessionAttribution)> { + validate_session_attribution(&attribution)?; + if record.subject_id != attribution.subject_id { + return Err(SubjektivError::SubjectScopeMismatch { + subject_id: record.subject_id, + reference: format!( + "session {} is attributed to subject {}", + attribution.session_id, attribution.subject_id + ), + }); + } + attach_session_to_candidate(&mut record, &attribution.session_id)?; + validate_staging_record(self.workspace_id(), &record)?; + validate_staging_admission(&record)?; + let candidate_raw = serde_json::to_string(&record)?; + + self.database.try_transaction(|transaction| { + require_active_subject(transaction, &record.subject_id)?; + let existing_raw = transaction + .query_row( + "SELECT record_json FROM subject_session_attributions WHERE session_id = ?1", + [&attribution.session_id], + |row| row.get::<_, String>(0), + ) + .optional()? + .ok_or_else(|| { + SubjektivError::InvalidRecord(format!( + "Session {} has not completed Host attribution", + attribution.session_id + )) + })?; + let existing = parse_session_attribution(&existing_raw)?; + if existing.subject_id != attribution.subject_id { + return Err(SubjektivError::SessionSubjectConflict { + session_id: attribution.session_id, + existing_subject_id: existing.subject_id, + requested_subject_id: attribution.subject_id, + }); + } + if !equivalent_session_attribution(&existing, &attribution) { + return Err(SubjektivError::SessionAttributionConflict( + attribution.session_id, + )); + } + let candidate = + write_staging_candidate(transaction, self.workspace_id(), record, candidate_raw)?; + Ok((candidate, existing)) + }) + } + + pub fn record_session_attribution( + &self, + attribution: SubjectSessionAttribution, + ) -> Result { + validate_session_attribution(&attribution)?; + let raw = serde_json::to_string(&attribution)?; + self.database.try_transaction(|transaction| { + require_active_subject(transaction, &attribution.subject_id)?; + write_session_attribution(transaction, attribution, raw) + }) + } + + pub fn session_attribution( + &self, + session_id: &str, + ) -> Result> { + validate_label("session id", session_id)?; + self.database.try_with_connection(|connection| { + let raw = connection + .query_row( + "SELECT record_json FROM subject_session_attributions + WHERE session_id = ?1", + [session_id], + |row| row.get::<_, String>(0), + ) + .optional()?; + raw.map(|raw| parse_session_attribution(&raw)).transpose() + }) + } + + pub fn subject_session_attributions( + &self, + subject_id: &str, + ) -> Result> { + validate_label("subject id", subject_id)?; + self.database.try_with_connection(|connection| { + let mut statement = connection.prepare( + "SELECT record_json FROM subject_session_attributions + WHERE subject_id = ?1 + ORDER BY attributed_at ASC, session_id ASC", + )?; + let rows = statement.query_map([subject_id], |row| row.get::<_, String>(0))?; + let mut records = Vec::new(); + for row in rows { + records.push(parse_session_attribution(&row?)?); + } + Ok(records) + }) + } + + /// Reads one bounded immutable attribution page without materializing the + /// subject's complete history. `snapshot_at` fixes the initial discovery + /// boundary; later Host attributions require a fresh traversal. + pub fn subject_session_attribution_page( + &self, + subject_id: &str, + session_id: Option<&str>, + snapshot_at: &str, + after: Option<(&str, &str)>, + limit: usize, + ) -> Result { + validate_label("subject id", subject_id)?; + validate_label("attribution snapshot", snapshot_at)?; + if let Some(session_id) = session_id { + validate_label("session id", session_id)?; + } + if let Some((attributed_at, session_id)) = after { + validate_label("attribution cursor timestamp", attributed_at)?; + validate_label("attribution cursor session id", session_id)?; + } + if !(1..=100).contains(&limit) { + return Err(SubjektivError::InvalidRecord( + "subject Session attribution page limit must be between 1 and 100".to_string(), + )); + } + let query_limit = limit.saturating_add(1); + self.database.try_with_connection(|connection| { + let raw = match (session_id, after) { + (Some(session_id), _) => { + let mut statement = connection.prepare( + "SELECT record_json FROM subject_session_attributions + WHERE subject_id = ?1 AND session_id = ?2 AND attributed_at <= ?3 + ORDER BY attributed_at DESC, session_id ASC + LIMIT ?4", + )?; + let rows = statement.query_map( + params![subject_id, session_id, snapshot_at, query_limit as i64], + |row| row.get::<_, String>(0), + )?; + rows.collect::, _>>()? + } + (None, Some((after_at, after_session_id))) => { + let mut statement = connection.prepare( + "SELECT record_json FROM subject_session_attributions + WHERE subject_id = ?1 AND attributed_at <= ?2 + AND (attributed_at < ?3 OR (attributed_at = ?3 AND session_id > ?4)) + ORDER BY attributed_at DESC, session_id ASC + LIMIT ?5", + )?; + let rows = statement.query_map( + params![ + subject_id, + snapshot_at, + after_at, + after_session_id, + query_limit as i64 + ], + |row| row.get::<_, String>(0), + )?; + rows.collect::, _>>()? + } + (None, None) => { + let mut statement = connection.prepare( + "SELECT record_json FROM subject_session_attributions + WHERE subject_id = ?1 AND attributed_at <= ?2 + ORDER BY attributed_at DESC, session_id ASC + LIMIT ?3", + )?; + let rows = statement.query_map( + params![subject_id, snapshot_at, query_limit as i64], + |row| row.get::<_, String>(0), + )?; + rows.collect::, _>>()? + } + }; + let mut items = raw + .into_iter() + .map(|raw| parse_session_attribution(&raw)) + .collect::>>()?; + let has_more = items.len() > limit; + if has_more { + items.truncate(limit); + } + Ok(SubjectSessionAttributionPage { items, has_more }) + }) + } + + pub fn staging_candidate( + &self, + subject_id: &str, + candidate_id: &str, + ) -> Result> { + self.database.try_with_connection(|connection| { + let raw = connection + .query_row( + "SELECT record_json FROM staging_records + WHERE subject_id = ?1 AND candidate_id = ?2", + params![subject_id, candidate_id], + |row| row.get::<_, String>(0), + ) + .optional()?; + raw.map(|raw| parse_staging(self.workspace_id(), &raw)) + .transpose() + }) + } + + pub fn pending_staging_candidates( + &self, + subject_id: &str, + limit: usize, + ) -> Result> { + self.database.try_with_connection(|connection| { + require_subject_in_connection(connection, subject_id)?; + let mut statement = connection.prepare( + "SELECT s.record_json + FROM staging_records s + LEFT JOIN staging_resolutions r + ON r.subject_id = s.subject_id AND r.candidate_id = s.candidate_id + WHERE s.subject_id = ?1 AND r.candidate_id IS NULL + ORDER BY s.created_at ASC, s.candidate_id ASC + LIMIT ?2", + )?; + let rows = statement.query_map(params![subject_id, to_i64(limit as u64)?], |row| { + row.get::<_, String>(0) + })?; + let mut records = Vec::new(); + for row in rows { + records.push(parse_staging(self.workspace_id(), &row?)?); + } + Ok(records) + }) + } + + pub fn pending_staging_backlog(&self, subject_id: &str) -> Result<(usize, u64)> { + self.database.try_with_connection(|connection| { + require_subject_in_connection(connection, subject_id)?; + let (count, bytes) = connection.query_row( + "SELECT COUNT(*), COALESCE(SUM(LENGTH(s.record_json)), 0) + FROM staging_records s + LEFT JOIN staging_resolutions r + ON r.subject_id = s.subject_id AND r.candidate_id = s.candidate_id + WHERE s.subject_id = ?1 AND r.candidate_id IS NULL", + [subject_id], + |row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)), + )?; + Ok(( + usize::try_from(count).map_err(|_| { + SubjektivError::InvalidRecord("pending candidate count exceeds usize".into()) + })?, + u64::try_from(bytes).map_err(|_| { + SubjektivError::InvalidRecord("pending candidate bytes are negative".into()) + })?, + )) + }) + } + + pub fn create_memory(&self, subject_id: &str, draft: MemoryDraft) -> Result { + validate_direct_memory_draft(&draft)?; + if draft.state != MemoryState::Active { + return Err(SubjektivError::InvalidRecord( + "a new Memory must start in active state".to_string(), + )); + } + let memory_id = issued_id("memory"); + self.database.try_transaction(|transaction| { + write_memory_revision(transaction, subject_id, &memory_id, None, draft) + }) + } + + pub fn revise_memory( + &self, + subject_id: &str, + memory_id: &str, + expected_revision: u64, + draft: MemoryDraft, + ) -> Result { + validate_direct_memory_draft(&draft)?; + self.database.try_transaction(|transaction| { + write_memory_revision( + transaction, + subject_id, + memory_id, + Some(expected_revision), + draft, + ) + }) + } + + /// Atomically writes a Memory revision and one candidate's immutable + /// resolution. Any conflict or invalid reference rolls both changes back. + pub fn apply_candidate( + &self, + subject_id: &str, + candidate_id: &str, + target: MemoryRevisionTarget, + draft: MemoryDraft, + reason: impl Into, + ) -> Result<(MemoryRecord, StagingResolution)> { + let candidate_ids = vec![candidate_id.to_string()]; + let (memory, mut resolutions) = + self.apply_candidates(subject_id, &candidate_ids, target, draft, reason)?; + Ok((memory, resolutions.remove(0))) + } + + /// Atomically applies one or more candidates to exactly one new Memory + /// revision. The repository derives `source_candidate_ids` from this list so + /// every applied resolution has a reciprocal immutable provenance edge. + pub fn apply_candidates( + &self, + subject_id: &str, + candidate_ids: &[String], + target: MemoryRevisionTarget, + draft: MemoryDraft, + reason: impl Into, + ) -> Result<(MemoryRecord, Vec)> { + if candidate_ids.is_empty() { + return Err(SubjektivError::InvalidRecord( + "candidate application requires at least one candidate".into(), + )); + } + reject_duplicates("applied candidate", candidate_ids)?; + for candidate_id in candidate_ids { + validate_label("candidate id", candidate_id)?; + } + let reason = reason.into(); + validate_nonempty("resolution reason", &reason)?; + self.database.try_transaction(|transaction| { + apply_candidates_in_transaction( + transaction, + subject_id, + candidate_ids, + &target, + draft, + &reason, + ) + }) + } + + pub fn decide_candidate( + &self, + subject_id: &str, + request: CandidateDecisionRequest, + ) -> Result { + validate_label("candidate decision request id", &request.request_id)?; + validate_label("candidate id", &request.candidate_id)?; + validate_nonempty("resolution reason", &request.reason)?; + let request_raw = serde_json::to_string(&request)?; + self.database.try_transaction(|transaction| { + if let Some((stored_request, stored_receipt)) = transaction + .query_row( + "SELECT request_json, receipt_json + FROM candidate_decision_receipts + WHERE subject_id = ?1 AND request_id = ?2", + params![subject_id, request.request_id], + |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)), + ) + .optional()? + { + if stored_request == request_raw { + return serde_json::from_str(&stored_receipt).map_err(Into::into); + } + return Err(SubjektivError::DecisionRequestConflict( + request.request_id.clone(), + )); + } + if transaction + .query_row( + "SELECT 1 FROM candidate_decision_receipts + WHERE subject_id = ?1 AND candidate_id = ?2", + params![subject_id, request.candidate_id], + |_| Ok(()), + ) + .optional()? + .is_some() + { + return Err(SubjektivError::CandidateResolved( + request.candidate_id.clone(), + )); + } + + let (memory, operation, resolution) = match request.decision.clone() { + CandidateDecision::Apply { target, draft } => { + let operation = match &target { + MemoryRevisionTarget::Create => MemoryDecisionOperation::Create, + MemoryRevisionTarget::Revise { .. } => MemoryDecisionOperation::Revise, + }; + let candidate_ids = [request.candidate_id.clone()]; + let (memory, mut resolutions) = apply_candidates_in_transaction( + transaction, + subject_id, + &candidate_ids, + &target, + draft, + &request.reason, + )?; + (Some(memory), Some(operation), resolutions.remove(0)) + } + CandidateDecision::Close { + action, + affected_memory, + } => { + if action == StagingResolutionAction::Applied { + return Err(SubjektivError::InvalidRecord( + "applied candidates require an apply decision".into(), + )); + } + let resolution = insert_resolution( + transaction, + subject_id, + &request.candidate_id, + action, + &request.reason, + &affected_memory, + )?; + let operation = + (!affected_memory.is_empty()).then_some(MemoryDecisionOperation::Reference); + (None, operation, resolution) + } + }; + let store_revision = require_subject(transaction, subject_id)?.store_revision; + let receipt = CandidateDecisionReceipt { + request_id: request.request_id.clone(), + candidate_id: request.candidate_id.clone(), + surface_dirty: memory.is_some(), + memory, + operation, + resolution, + store_revision, + }; + let receipt_raw = serde_json::to_string(&receipt)?; + transaction.execute( + "INSERT INTO candidate_decision_receipts ( + subject_id, request_id, candidate_id, request_json, + receipt_json, created_at + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", + params![ + subject_id, + request.request_id, + request.candidate_id, + request_raw, + receipt_raw, + now() + ], + )?; + Ok(receipt) + }) + } + + pub fn resolve_candidate( + &self, + subject_id: &str, + candidate_id: &str, + action: StagingResolutionAction, + reason: impl Into, + affected_memory: Vec, + ) -> Result { + let reason = reason.into(); + validate_nonempty("resolution reason", &reason)?; + if action == StagingResolutionAction::Applied { + return Err(SubjektivError::InvalidRecord( + "applied candidates must use apply_candidate(s) so Memory and provenance commit atomically" + .to_string(), + )); + } + self.database.try_transaction(|transaction| { + insert_resolution( + transaction, + subject_id, + candidate_id, + action, + &reason, + &affected_memory, + ) + }) + } + + pub fn staging_resolution( + &self, + subject_id: &str, + candidate_id: &str, + ) -> Result> { + self.database.try_with_connection(|connection| { + let raw = connection + .query_row( + "SELECT resolution_json FROM staging_resolutions + WHERE subject_id = ?1 AND candidate_id = ?2", + params![subject_id, candidate_id], + |row| row.get::<_, String>(0), + ) + .optional()?; + raw.map(|raw| parse_resolution(&raw)).transpose() + }) + } + + pub fn memory(&self, subject_id: &str, memory_id: &str) -> Result> { + self.database.try_with_connection(|connection| { + let raw = connection + .query_row( + "SELECT record_json FROM memory_records + WHERE subject_id = ?1 AND memory_id = ?2", + params![subject_id, memory_id], + |row| row.get::<_, String>(0), + ) + .optional()?; + raw.map(|raw| parse_memory(&raw)).transpose() + }) + } + + pub fn memory_revision( + &self, + subject_id: &str, + memory_id: &str, + revision: u64, + ) -> Result> { + self.database.try_with_connection(|connection| { + let raw = connection + .query_row( + "SELECT record_json FROM memory_revisions + WHERE subject_id = ?1 AND memory_id = ?2 AND revision = ?3", + params![subject_id, memory_id, to_i64(revision)?], + |row| row.get::<_, String>(0), + ) + .optional()?; + raw.map(|raw| parse_memory(&raw)).transpose() + }) + } + + /// Reads one current Memory while distinguishing an absent target from a + /// Memory id owned by another subject. + pub fn scoped_memory(&self, subject_id: &str, memory_id: &str) -> Result { + validate_label("subject id", subject_id)?; + validate_label("Memory id", memory_id)?; + self.database.try_with_connection(|connection| { + require_subject_in_connection(connection, subject_id)?; + scoped_memory_in_connection(connection, subject_id, memory_id) + }) + } + + /// Reads one immutable revision after verifying that the Memory belongs to + /// the requested subject. A missing revision of a valid scoped Memory is + /// returned as `None`. + pub fn scoped_memory_revision( + &self, + subject_id: &str, + memory_id: &str, + revision: u64, + ) -> Result> { + validate_label("subject id", subject_id)?; + validate_label("Memory id", memory_id)?; + if revision == 0 { + return Err(SubjektivError::InvalidRecord( + "Memory revision must be greater than zero".into(), + )); + } + self.database.try_with_connection(|connection| { + require_subject_in_connection(connection, subject_id)?; + scoped_memory_in_connection(connection, subject_id, memory_id)?; + let raw = connection + .query_row( + "SELECT record_json FROM memory_revisions + WHERE subject_id = ?1 AND memory_id = ?2 AND revision = ?3", + params![subject_id, memory_id, to_i64(revision)?], + |row| row.get::<_, String>(0), + ) + .optional()?; + raw.map(|raw| parse_memory(&raw)).transpose() + }) + } + + /// Lists current Memory records for one existing subject in deterministic + /// most-recently-updated order. + pub fn list_memories(&self, subject_id: &str) -> Result> { + validate_label("subject id", subject_id)?; + self.database.try_with_connection(|connection| { + require_subject_in_connection(connection, subject_id)?; + let mut statement = connection.prepare( + "SELECT record_json FROM memory_records + WHERE subject_id = ?1 + ORDER BY updated_at DESC, memory_id ASC", + )?; + let rows = statement.query_map([subject_id], |row| row.get::<_, String>(0))?; + let mut records = Vec::new(); + for row in rows { + records.push(parse_memory(&row?)?); + } + Ok(records) + }) + } + + /// Lists every immutable revision of one scoped Memory, newest first. + pub fn list_memory_revisions( + &self, + subject_id: &str, + memory_id: &str, + ) -> Result> { + validate_label("subject id", subject_id)?; + validate_label("Memory id", memory_id)?; + self.database.try_with_connection(|connection| { + require_subject_in_connection(connection, subject_id)?; + scoped_memory_in_connection(connection, subject_id, memory_id)?; + let mut statement = connection.prepare( + "SELECT record_json FROM memory_revisions + WHERE subject_id = ?1 AND memory_id = ?2 + ORDER BY revision DESC", + )?; + let rows = statement.query_map(params![subject_id, memory_id], |row| { + row.get::<_, String>(0) + })?; + let mut records = Vec::new(); + for row in rows { + records.push(parse_memory(&row?)?); + } + Ok(records) + }) + } + + /// Captures one deterministic, bounded generation input from only current + /// active Memory revisions. Kind order is fixed and selection is round-robin + /// across kinds; within each kind, updated_at desc then memory id asc. + pub fn prepare_surface_generation(&self, subject_id: &str) -> Result { + self.prepare_job_surface_generation(subject_id, None) + } + + pub fn prepare_job_surface_generation( + &self, + subject_id: &str, + job_attempt: Option<(&str, &str)>, + ) -> Result { + self.database.try_transaction(|transaction| { + let subject = require_active_subject(transaction, subject_id)?; + let (active_memory_count, materials) = + select_surface_materials(transaction, subject_id)?; + let generation = SurfaceGeneration { + id: issued_id("surface-generation"), + subject_id: subject_id.to_string(), + store_revision: subject.store_revision, + active_memory_count, + materials, + created_at: now(), + }; + transaction.execute( + "INSERT INTO surface_generation_runs ( + subject_id, generation_id, store_revision, active_memory_count, + materials_json, created_at, job_id, attempt_id + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", + params![ + subject_id, + generation.id, + to_i64(generation.store_revision)?, + to_i64(generation.active_memory_count as u64)?, + serde_json::to_string(&generation.materials)?, + generation.created_at, + job_attempt.map(|(job_id, _)| job_id), + job_attempt.map(|(_, attempt_id)| attempt_id), + ], + )?; + Ok(generation) + }) + } + + /// Validate a Job's structured surface claim against the existing generation + /// and publication/failure records. This does not create another receipt or + /// roll back Memory. The generation must have begun inside this attempt. + #[allow(clippy::too_many_arguments)] + pub fn validate_job_surface_outcome( + &self, + subject_id: &str, + generation_id: &str, + store_revision: u64, + availability: &str, + snapshot_id: Option<&str>, + reason_code: Option<&str>, + job_id: &str, + attempt_id: &str, + ) -> Result<()> { + self.database.try_with_connection(|connection| { + let subject = require_subject_in_connection(connection, subject_id)?; + let generation_revision = require_job_generation(connection, subject_id, generation_id, job_id, attempt_id)?; + if generation_revision != store_revision || subject.store_revision != store_revision { + return Err(SubjektivError::InvalidRecord("surface generation is stale".into())); + } + let state = connection.query_row( + "SELECT store_revision, status, snapshot_id, reason_code FROM surface_generation_state WHERE subject_id = ?1", + [subject_id], + |row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?, row.get::<_, Option>(2)?, row.get::<_, Option>(3)?)), + ).optional()?.ok_or_else(|| SubjektivError::InvalidRecord("surface generation has no terminal outcome".into()))?; + if state.0 != to_i64(store_revision)? || state.1 != availability + || !matches!(availability, "ready" | "failed") + || state.2.as_deref() != snapshot_id || state.3.as_deref() != reason_code { + return Err(SubjektivError::InvalidRecord("surface claim differs from the persisted outcome".into())); + } + Ok(()) + }) + } + + pub fn require_job_surface_generation( + &self, + subject_id: &str, + generation_id: &str, + job_id: &str, + attempt_id: &str, + ) -> Result<()> { + self.database.try_with_connection(|connection| { + require_job_generation(connection, subject_id, generation_id, job_id, attempt_id)?; + Ok(()) + }) + } + + /// Validates structured editor output against the exact material snapshot and + /// atomically publishes it only while the subject store generation is current. + pub fn publish_surface_generation( + &self, + subject_id: &str, + generation_id: &str, + points: Vec, + ) -> Result { + validate_label("surface generation id", generation_id)?; + self.database.try_transaction(|transaction| { + let subject = require_active_subject(transaction, subject_id)?; + let (store_revision, active_memory_count, materials_json) = transaction + .query_row( + "SELECT store_revision, active_memory_count, materials_json + FROM surface_generation_runs + WHERE subject_id = ?1 AND generation_id = ?2", + params![subject_id, generation_id], + |row| { + Ok(( + row.get::<_, i64>(0)?, + row.get::<_, i64>(1)?, + row.get::<_, String>(2)?, + )) + }, + ) + .optional()? + .ok_or_else(|| { + SubjektivError::SurfaceGenerationConflict(format!( + "unknown generation `{generation_id}` for subject `{subject_id}`" + )) + })?; + let store_revision = u64::try_from(store_revision).map_err(|_| { + SubjektivError::InvalidRecord("surface generation revision is negative".into()) + })?; + if subject.store_revision != store_revision { + return Err(SubjektivError::SurfaceGenerationConflict(format!( + "generation {store_revision} is stale; current store revision is {}", + subject.store_revision + ))); + } + let materials: Vec = serde_json::from_str(&materials_json)?; + if active_memory_count > 0 && materials.is_empty() { + return Err(SubjektivError::InvalidRecord( + "active Memory exists but no record fits the surface input budget".into(), + )); + } + let (body_md, memory_refs) = validate_surface_points(&materials, &points)?; + if let Some(existing) = + ready_surface_snapshot_for_revision(transaction, subject_id, store_revision)? + { + if existing.body_md == body_md && existing.memory_refs == memory_refs { + return Ok(existing); + } + return Err(SubjektivError::SurfaceGenerationConflict(format!( + "store revision {store_revision} already has a different published surface" + ))); + } + insert_surface_snapshot( + transaction, + subject_id, + body_md, + memory_refs, + store_revision, + ) + }) + } + + /// Records a bounded generator failure without changing confirmed Memory. A + /// late failure cannot replace a ready surface or dirty a newer generation. + pub fn fail_surface_generation( + &self, + subject_id: &str, + generation_id: &str, + reason_code: &str, + ) -> Result { + validate_label("surface generation id", generation_id)?; + validate_label("surface failure reason", reason_code)?; + self.database.try_transaction(|transaction| { + let subject = require_active_subject(transaction, subject_id)?; + let generation_revision = transaction + .query_row( + "SELECT store_revision FROM surface_generation_runs + WHERE subject_id = ?1 AND generation_id = ?2", + params![subject_id, generation_id], + |row| row.get::<_, i64>(0), + ) + .optional()? + .ok_or_else(|| { + SubjektivError::SurfaceGenerationConflict(format!( + "unknown generation `{generation_id}` for subject `{subject_id}`" + )) + })?; + let generation_revision = u64::try_from(generation_revision).map_err(|_| { + SubjektivError::InvalidRecord("surface generation revision is negative".into()) + })?; + if generation_revision == subject.store_revision { + let ready = transaction + .query_row( + "SELECT status FROM surface_generation_state WHERE subject_id = ?1", + [subject_id], + |row| row.get::<_, String>(0), + ) + .optional()? + .is_some_and(|status| status == "ready"); + if !ready { + transaction.execute( + "INSERT INTO surface_generation_state ( + subject_id, store_revision, status, snapshot_id, reason_code, updated_at + ) VALUES (?1, ?2, 'failed', NULL, ?3, ?4) + ON CONFLICT(subject_id) DO UPDATE SET + store_revision = excluded.store_revision, + status = 'failed', snapshot_id = NULL, + reason_code = excluded.reason_code, + updated_at = excluded.updated_at", + params![subject_id, to_i64(generation_revision)?, reason_code, now()], + )?; + } + } + Ok(subject.store_revision) + }) + } + + /// Returns only a current ready snapshot for injection. Other states are + /// explicit so callers never report missing/stale/failed generation as empty + /// confirmed Memory. + pub fn resident_surface(&self, subject_id: &str) -> Result { + self.database.try_with_connection(|connection| { + let subject = require_subject_in_connection(connection, subject_id)?; + resident_surface_in_connection(connection, &subject) + }) + } + + /// Atomically reads user-managed behavior and the independently-fresh + /// generated Memory surface from one SQLite connection snapshot. + pub fn resident_context(&self, subject_id: &str) -> Result { + self.database.try_transaction(|transaction| { + let subject = require_subject(transaction, subject_id)?; + let surface = resident_surface_in_connection(transaction, &subject)?; + Ok(SubjectResidentContext { subject, surface }) + }) + } + + pub fn create_surface_snapshot( + &self, + subject_id: &str, + body_md: impl Into, + memory_refs: Vec, + built_from_store_revision: u64, + ) -> Result { + reject_duplicate_refs("surface snapshot", &memory_refs)?; + reject_duplicate_memory_ids("surface snapshot", &memory_refs)?; + let body_md = body_md.into(); + self.database.try_transaction(|transaction| { + let subject = require_active_subject(transaction, subject_id)?; + if subject.store_revision != built_from_store_revision { + return Err(SubjektivError::InvalidRecord(format!( + "surface snapshot generation {built_from_store_revision} does not match current subject store revision {}", + subject.store_revision + ))); + } + validate_memory_refs(transaction, subject_id, &memory_refs)?; + if estimated_tokens(&body_md) > SURFACE_BODY_TOKEN_BUDGET { + return Err(SubjektivError::InvalidRecord(format!( + "surface snapshot body exceeds {SURFACE_BODY_TOKEN_BUDGET} token estimate" + ))); + } + if let Some(existing) = surface_snapshot_for_revision( + transaction, + subject_id, + built_from_store_revision, + )? { + if existing.body_md == body_md && existing.memory_refs == memory_refs { + return Ok(existing); + } + return Err(SubjektivError::SurfaceGenerationConflict(format!( + "store revision {built_from_store_revision} already has a different published surface" + ))); + } + insert_surface_snapshot( + transaction, + subject_id, + body_md, + memory_refs, + built_from_store_revision, + ) + }) + } + + pub fn surface_snapshot( + &self, + subject_id: &str, + snapshot_id: &str, + ) -> Result> { + self.database.try_with_connection(|connection| { + let raw = connection + .query_row( + "SELECT snapshot_json FROM surface_snapshots + WHERE subject_id = ?1 AND snapshot_id = ?2", + params![subject_id, snapshot_id], + |row| row.get::<_, String>(0), + ) + .optional()?; + raw.map(|raw| parse_surface_snapshot(&raw)).transpose() + }) + } +} + +fn select_surface_materials( + transaction: &Transaction<'_>, + subject_id: &str, +) -> Result<(usize, Vec)> { + let mut statement = transaction.prepare( + "SELECT record_json FROM memory_records + WHERE subject_id = ?1 AND state = 'active' + ORDER BY updated_at DESC, memory_id ASC", + )?; + let rows = statement.query_map([subject_id], |row| row.get::<_, String>(0))?; + let mut buckets: [Vec; 6] = std::array::from_fn(|_| Vec::new()); + let mut active_memory_count = 0usize; + for row in rows { + let memory = parse_memory(&row?)?; + active_memory_count = active_memory_count.saturating_add(1); + let bucket = surface_kind_index(&memory.kind); + if buckets[bucket].len() < SURFACE_PER_KIND_LIMIT { + buckets[bucket].push(SurfaceMaterial { + memory_id: memory.id, + revision: memory.revision, + kind: memory.kind, + body_md: memory.body_md, + why_useful: memory.why_useful, + staleness: memory.staleness, + }); + } + } + + let populated_kind_count = buckets.iter().filter(|bucket| !bucket.is_empty()).count(); + let per_kind_token_budget = SURFACE_MATERIAL_TOKEN_BUDGET / populated_kind_count.max(1); + let mut ordered = Vec::new(); + for offset in 0..SURFACE_PER_KIND_LIMIT { + for (bucket_index, bucket) in buckets.iter().enumerate() { + if let Some(material) = bucket.get(offset) { + ordered.push((bucket_index, material.clone())); + } + } + } + + let mut accepted = vec![false; ordered.len()]; + let mut selected_count = 0usize; + let mut deferred = Vec::new(); + for (index, (bucket_index, material)) in ordered.iter().enumerate() { + if selected_count == SURFACE_TOTAL_MATERIAL_LIMIT { + break; + } + let kind_materials = ordered + .iter() + .zip(&accepted) + .filter(|((selected_bucket, _), accepted)| { + **accepted && selected_bucket == bucket_index + }) + .map(|((_, material), _)| material) + .chain(std::iter::once(material)); + let all_materials = ordered + .iter() + .zip(&accepted) + .filter(|(_, accepted)| **accepted) + .map(|((_, material), _)| material) + .chain(std::iter::once(material)); + if serialized_material_tokens(kind_materials)? <= per_kind_token_budget + && serialized_material_tokens(all_materials)? <= SURFACE_MATERIAL_TOKEN_BUDGET + { + accepted[index] = true; + selected_count += 1; + } else { + deferred.push(index); + } + } + + // Category shares are fairness reservations, not hard ceilings. Reclaim + // unused shares in the same canonical order after every populated category + // had its first opportunity. + for index in deferred { + if selected_count == SURFACE_TOTAL_MATERIAL_LIMIT { + break; + } + let material = &ordered[index].1; + let all_materials = ordered + .iter() + .zip(&accepted) + .filter(|(_, accepted)| **accepted) + .map(|((_, material), _)| material) + .chain(std::iter::once(material)); + if serialized_material_tokens(all_materials)? <= SURFACE_MATERIAL_TOKEN_BUDGET { + accepted[index] = true; + selected_count += 1; + } + } + + let selected = ordered + .into_iter() + .zip(accepted) + .filter_map(|((_, material), accepted)| accepted.then_some(material)) + .collect(); + Ok((active_memory_count, selected)) +} + +fn serialized_material_tokens<'a>( + materials: impl IntoIterator, +) -> Result { + let materials = materials.into_iter().collect::>(); + Ok(estimated_tokens_for_bytes( + serde_json::to_vec(&materials)?.len(), + )) +} + +fn surface_kind_index(kind: &CandidateKind) -> usize { + match kind { + CandidateKind::Preference => 0, + CandidateKind::Constraint => 1, + CandidateKind::Decision => 2, + CandidateKind::WorkingAssumption => 3, + CandidateKind::OpenQuestion => 4, + CandidateKind::Lesson => 5, + } +} + +fn validate_surface_points( + materials: &[SurfaceMaterial], + points: &[SurfacePoint], +) -> Result<(String, Vec)> { + if materials.is_empty() { + if points.is_empty() { + return Ok((String::new(), Vec::new())); + } + return Err(SubjektivError::InvalidRecord( + "an empty material set requires an empty surface".into(), + )); + } + if points.is_empty() { + return Err(SubjektivError::InvalidRecord( + "non-empty surface materials require at least one grounded point".into(), + )); + } + if points.len() > SURFACE_TOTAL_MATERIAL_LIMIT { + return Err(SubjektivError::InvalidRecord(format!( + "surface output exceeds {SURFACE_TOTAL_MATERIAL_LIMIT} points" + ))); + } + let allowed = materials + .iter() + .map(|material| (material.memory_id.as_str(), material.revision)) + .collect::>(); + let mut bodies = Vec::with_capacity(points.len()); + let mut seen = HashSet::new(); + let mut refs = Vec::new(); + for point in points { + let body = point.body_md.trim(); + validate_nonempty("surface point body", body)?; + if point.memory_refs.is_empty() { + return Err(SubjektivError::InvalidRecord( + "every surface point requires at least one Memory reference".into(), + )); + } + reject_duplicate_refs("surface point", &point.memory_refs)?; + for reference in &point.memory_refs { + if !allowed.contains(&(reference.memory_id.as_str(), reference.revision)) { + return Err(SubjektivError::SubjectScopeMismatch { + subject_id: "surface generation materials".into(), + reference: format!("{}@{}", reference.memory_id, reference.revision), + }); + } + if seen.insert((reference.memory_id.clone(), reference.revision)) { + refs.push(reference.clone()); + } + } + bodies.push(body.to_string()); + } + let body_md = bodies.join("\n\n"); + if estimated_tokens(&body_md) > SURFACE_BODY_TOKEN_BUDGET { + return Err(SubjektivError::InvalidRecord(format!( + "surface body exceeds {SURFACE_BODY_TOKEN_BUDGET} token estimate" + ))); + } + Ok((body_md, refs)) +} + +fn ready_surface_snapshot_for_revision( + transaction: &Transaction<'_>, + subject_id: &str, + store_revision: u64, +) -> Result> { + let raw = transaction + .query_row( + "SELECT snapshots.snapshot_json + FROM surface_generation_state state + JOIN surface_snapshots snapshots + ON snapshots.subject_id = state.subject_id + AND snapshots.snapshot_id = state.snapshot_id + WHERE state.subject_id = ?1 + AND state.store_revision = ?2 + AND state.status = 'ready'", + params![subject_id, to_i64(store_revision)?], + |row| row.get::<_, String>(0), + ) + .optional()?; + raw.map(|raw| parse_surface_snapshot(&raw)).transpose() +} + +fn surface_snapshot_for_revision( + transaction: &Transaction<'_>, + subject_id: &str, + store_revision: u64, +) -> Result> { + let raw = transaction + .query_row( + "SELECT snapshot_json FROM surface_snapshots + WHERE subject_id = ?1 AND built_from_store_revision = ?2 + ORDER BY created_at ASC, snapshot_id ASC LIMIT 1", + params![subject_id, to_i64(store_revision)?], + |row| row.get::<_, String>(0), + ) + .optional()?; + raw.map(|raw| parse_surface_snapshot(&raw)).transpose() +} + +fn insert_surface_snapshot( + transaction: &Transaction<'_>, + subject_id: &str, + body_md: String, + memory_refs: Vec, + built_from_store_revision: u64, +) -> Result { + validate_memory_refs(transaction, subject_id, &memory_refs)?; + let snapshot = SurfaceSnapshot { + schema_version: SUBJEKTIV_SCHEMA_VERSION, + id: issued_id("surface"), + subject_id: subject_id.to_string(), + body_md, + memory_refs, + built_from_store_revision, + created_at: now(), + }; + let raw = serde_json::to_string(&snapshot)?; + transaction.execute( + "INSERT INTO surface_snapshots ( + subject_id, snapshot_id, built_from_store_revision, snapshot_json, created_at + ) VALUES (?1, ?2, ?3, ?4, ?5)", + params![ + subject_id, + snapshot.id, + to_i64(snapshot.built_from_store_revision)?, + raw, + snapshot.created_at + ], + )?; + for reference in &snapshot.memory_refs { + transaction.execute( + "INSERT INTO surface_snapshot_refs ( + subject_id, snapshot_id, memory_id, revision + ) VALUES (?1, ?2, ?3, ?4)", + params![ + subject_id, + snapshot.id, + reference.memory_id, + to_i64(reference.revision)? + ], + )?; + } + transaction.execute( + "INSERT INTO surface_snapshot_seals (subject_id, snapshot_id) VALUES (?1, ?2)", + params![subject_id, snapshot.id], + )?; + transaction.execute( + "INSERT INTO surface_generation_state ( + subject_id, store_revision, status, snapshot_id, reason_code, updated_at + ) VALUES (?1, ?2, 'ready', ?3, NULL, ?4) + ON CONFLICT(subject_id) DO UPDATE SET + store_revision = excluded.store_revision, + status = 'ready', snapshot_id = excluded.snapshot_id, + reason_code = NULL, updated_at = excluded.updated_at", + params![ + subject_id, + to_i64(built_from_store_revision)?, + snapshot.id, + snapshot.created_at + ], + )?; + Ok(snapshot) +} + +fn estimated_tokens(value: &str) -> usize { + estimated_tokens_for_bytes(value.len()) +} + +fn estimated_tokens_for_bytes(bytes: usize) -> usize { + bytes.saturating_add(3) / 4 +} + +fn apply_candidates_in_transaction( + transaction: &Transaction<'_>, + subject_id: &str, + candidate_ids: &[String], + target: &MemoryRevisionTarget, + mut draft: MemoryDraft, + reason: &str, +) -> Result<(MemoryRecord, Vec)> { + let mut candidates = Vec::with_capacity(candidate_ids.len()); + for candidate_id in candidate_ids { + let raw = require_unresolved_candidate(transaction, subject_id, candidate_id)?; + candidates.push(parse_staging_without_workspace(&raw)?); + } + for candidate in &candidates { + validate_candidate_application(transaction, subject_id, candidate, target, &draft)?; + } + draft.source_candidate_ids = candidate_ids.to_vec(); + let new_memory_id = issued_id("memory"); + let memory = match target { + MemoryRevisionTarget::Create => { + if draft.state != MemoryState::Active { + return Err(SubjektivError::InvalidRecord( + "a new Memory must start in active state".into(), + )); + } + write_memory_revision(transaction, subject_id, &new_memory_id, None, draft)? + } + MemoryRevisionTarget::Revise { + memory_id, + expected_revision, + } => write_memory_revision( + transaction, + subject_id, + memory_id, + Some(*expected_revision), + draft, + )?, + }; + let reference = MemoryRevisionRef { + memory_id: memory.id.clone(), + revision: memory.revision, + }; + let resolutions = candidate_ids + .iter() + .map(|candidate_id| { + insert_resolution( + transaction, + subject_id, + candidate_id, + StagingResolutionAction::Applied, + reason, + std::slice::from_ref(&reference), + ) + }) + .collect::>>()?; + Ok((memory, resolutions)) +} + +fn write_memory_revision( + transaction: &Transaction<'_>, + subject_id: &str, + memory_id: &str, + expected_revision: Option, + draft: MemoryDraft, +) -> Result { + validate_memory_draft(&draft)?; + let mut subject = require_active_subject(transaction, subject_id)?; + let current_raw = transaction + .query_row( + "SELECT record_json FROM memory_records + WHERE subject_id = ?1 AND memory_id = ?2", + params![subject_id, memory_id], + |row| row.get::<_, String>(0), + ) + .optional()?; + let current = current_raw.map(|raw| parse_memory(&raw)).transpose()?; + + let (revision, created_at) = match (current.as_ref(), expected_revision) { + (None, None) => (1, now()), + (None, Some(_)) => return Err(SubjektivError::MemoryNotFound(memory_id.to_string())), + (Some(_), None) => { + return Err(SubjektivError::InvalidRecord(format!( + "memory `{memory_id}` already exists" + ))); + } + (Some(current), Some(expected)) if current.revision != expected => { + return Err(SubjektivError::RevisionConflict { + memory_id: memory_id.to_string(), + expected, + actual: current.revision, + }); + } + (Some(current), Some(_)) => { + validate_state_transition(memory_id, current.state, draft.state)?; + (current.revision + 1, current.created_at.clone()) + } + }; + + reject_duplicates("source candidate", &draft.source_candidate_ids)?; + reject_duplicate_refs("derived Memory", &draft.derived_from)?; + validate_candidate_refs(transaction, subject_id, &draft.source_candidate_ids)?; + validate_memory_refs(transaction, subject_id, &draft.derived_from)?; + + let timestamp = now(); + let record = MemoryRecord { + schema_version: SUBJEKTIV_SCHEMA_VERSION, + id: memory_id.to_string(), + subject_id: subject_id.to_string(), + revision, + kind: draft.kind, + state: draft.state, + claim: draft.claim, + body_md: draft.body_md, + why_useful: draft.why_useful, + staleness: draft.staleness, + source_candidate_ids: draft.source_candidate_ids, + derived_from: draft.derived_from, + change_reason: draft.change_reason, + created_at, + updated_at: timestamp, + }; + let raw = serde_json::to_string(&record)?; + + if current.is_none() { + transaction.execute( + "INSERT INTO memory_records ( + subject_id, memory_id, current_revision, kind, state, + record_json, created_at, updated_at + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", + params![ + subject_id, + memory_id, + to_i64(revision)?, + candidate_kind_name(&record.kind), + memory_state_name(record.state), + raw, + record.created_at, + record.updated_at + ], + )?; + } + transaction.execute( + "INSERT INTO memory_revisions ( + subject_id, memory_id, revision, kind, state, record_json, created_at + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", + params![ + subject_id, + memory_id, + to_i64(revision)?, + candidate_kind_name(&record.kind), + memory_state_name(record.state), + raw, + record.updated_at + ], + )?; + if current.is_some() { + transaction.execute( + "UPDATE memory_records + SET current_revision = ?3, kind = ?4, state = ?5, + record_json = ?6, updated_at = ?7 + WHERE subject_id = ?1 AND memory_id = ?2", + params![ + subject_id, + memory_id, + to_i64(revision)?, + candidate_kind_name(&record.kind), + memory_state_name(record.state), + raw, + record.updated_at + ], + )?; + } + for candidate_id in &record.source_candidate_ids { + transaction.execute( + "INSERT INTO memory_revision_candidates ( + subject_id, memory_id, revision, candidate_id + ) VALUES (?1, ?2, ?3, ?4)", + params![subject_id, memory_id, to_i64(revision)?, candidate_id], + )?; + } + for source in &record.derived_from { + transaction.execute( + "INSERT INTO memory_revision_derivations ( + subject_id, memory_id, revision, source_memory_id, source_revision + ) VALUES (?1, ?2, ?3, ?4, ?5)", + params![ + subject_id, + memory_id, + to_i64(revision)?, + source.memory_id, + to_i64(source.revision)? + ], + )?; + } + transaction.execute( + "INSERT INTO memory_revision_seals (subject_id, memory_id, revision) + VALUES (?1, ?2, ?3)", + params![subject_id, memory_id, to_i64(revision)?], + )?; + + subject.store_revision = subject + .store_revision + .checked_add(1) + .ok_or_else(|| SubjektivError::InvalidRecord("subject store revision overflow".into()))?; + subject.updated_at = record.updated_at.clone(); + let subject_raw = serde_json::to_string(&subject)?; + transaction.execute( + "UPDATE subjects + SET store_revision = ?2, record_json = ?3, updated_at = ?4 + WHERE subject_id = ?1", + params![ + subject_id, + to_i64(subject.store_revision)?, + subject_raw, + subject.updated_at + ], + )?; + transaction.execute( + "INSERT INTO surface_generation_state ( + subject_id, store_revision, status, snapshot_id, reason_code, updated_at + ) VALUES (?1, ?2, 'dirty', NULL, NULL, ?3) + ON CONFLICT(subject_id) DO UPDATE SET + store_revision = excluded.store_revision, + status = 'dirty', snapshot_id = NULL, reason_code = NULL, + updated_at = excluded.updated_at", + params![ + subject_id, + to_i64(subject.store_revision)?, + subject.updated_at + ], + )?; + Ok(record) +} + +fn write_staging_candidate( + transaction: &Transaction<'_>, + workspace_id: &str, + record: SubjectStagingRecord, + raw: String, +) -> Result { + let existing = transaction + .query_row( + "SELECT record_json FROM staging_records + WHERE subject_id = ?1 AND candidate_id = ?2", + params![record.subject_id, record.id], + |row| row.get::<_, String>(0), + ) + .optional()?; + if let Some(existing) = existing { + if equivalent_staging_payload(&existing, &raw)? { + return parse_staging(workspace_id, &existing); + } + return Err(SubjektivError::CandidateConflict(record.id.clone())); + } + validate_staged_revision_proposal(transaction, &record)?; + transaction.execute( + "INSERT INTO staging_records ( + subject_id, candidate_id, kind, record_json, created_at + ) VALUES (?1, ?2, ?3, ?4, ?5)", + params![ + record.subject_id, + record.id, + candidate_kind_name(&record.kind), + raw, + record.created_at + ], + )?; + Ok(record) +} + +fn write_session_attribution( + transaction: &Transaction<'_>, + record: SubjectSessionAttribution, + raw: String, +) -> Result { + let existing = transaction + .query_row( + "SELECT record_json FROM subject_session_attributions WHERE session_id = ?1", + [&record.session_id], + |row| row.get::<_, String>(0), + ) + .optional()?; + if let Some(existing) = existing { + let existing = parse_session_attribution(&existing)?; + if existing.subject_id != record.subject_id { + return Err(SubjektivError::SessionSubjectConflict { + session_id: record.session_id, + existing_subject_id: existing.subject_id, + requested_subject_id: record.subject_id, + }); + } + if equivalent_session_attribution(&existing, &record) { + return Ok(existing); + } + return Err(SubjektivError::SessionAttributionConflict( + record.session_id, + )); + } + transaction.execute( + "INSERT INTO subject_session_attributions ( + session_id, subject_id, runtime_id, worker_id, record_json, attributed_at + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", + params![ + record.session_id, + record.subject_id, + record.runtime_id, + record.worker_id, + raw, + record.attributed_at + ], + )?; + Ok(record) +} + +fn insert_resolution( + transaction: &Transaction<'_>, + subject_id: &str, + candidate_id: &str, + action: StagingResolutionAction, + reason: &str, + affected_memory: &[MemoryRevisionRef], +) -> Result { + require_active_subject(transaction, subject_id)?; + reject_duplicate_refs("staging resolution", affected_memory)?; + validate_memory_refs(transaction, subject_id, affected_memory)?; + if action == StagingResolutionAction::Applied { + if affected_memory.is_empty() { + return Err(SubjektivError::InvalidRecord( + "an applied candidate requires an affected Memory revision".into(), + )); + } + for reference in affected_memory { + let reciprocal = transaction + .query_row( + "SELECT 1 FROM memory_revision_candidates + WHERE subject_id = ?1 AND memory_id = ?2 + AND revision = ?3 AND candidate_id = ?4", + params![ + subject_id, + reference.memory_id, + to_i64(reference.revision)?, + candidate_id + ], + |_| Ok(()), + ) + .optional()? + .is_some(); + if !reciprocal { + return Err(SubjektivError::InvalidRecord(format!( + "applied candidate `{candidate_id}` has no reciprocal provenance edge to `{}@{}`", + reference.memory_id, reference.revision + ))); + } + } + } + let candidate_raw = require_unresolved_candidate(transaction, subject_id, candidate_id)?; + let candidate = parse_staging_without_workspace(&candidate_raw)?; + if candidate.subject_id != subject_id { + return Err(SubjektivError::SubjectScopeMismatch { + subject_id: subject_id.to_string(), + reference: candidate_id.to_string(), + }); + } + let resolution = StagingResolution { + schema_version: SUBJEKTIV_SCHEMA_VERSION, + id: issued_id("resolution"), + subject_id: subject_id.to_string(), + candidate_id: candidate_id.to_string(), + action, + reason: reason.to_string(), + affected_memory: affected_memory.to_vec(), + candidate, + resolved_at: now(), + }; + let affected_raw = serde_json::to_string(affected_memory)?; + let resolution_raw = serde_json::to_string(&resolution)?; + transaction.execute( + "INSERT INTO staging_resolutions ( + subject_id, candidate_id, resolution_id, action, reason, + affected_refs_json, staging_raw_json, resolution_json, resolved_at + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", + params![ + subject_id, + candidate_id, + resolution.id, + resolution_action_name(action), + reason, + affected_raw, + candidate_raw, + resolution_raw, + resolution.resolved_at + ], + )?; + for reference in affected_memory { + transaction.execute( + "INSERT INTO staging_resolution_targets ( + subject_id, candidate_id, memory_id, revision + ) VALUES (?1, ?2, ?3, ?4)", + params![ + subject_id, + candidate_id, + reference.memory_id, + to_i64(reference.revision)? + ], + )?; + } + transaction.execute( + "INSERT INTO staging_resolution_seals (subject_id, candidate_id) + VALUES (?1, ?2)", + params![subject_id, candidate_id], + )?; + Ok(resolution) +} + +fn require_unresolved_candidate( + transaction: &Transaction<'_>, + subject_id: &str, + candidate_id: &str, +) -> Result { + let raw = transaction + .query_row( + "SELECT record_json FROM staging_records + WHERE subject_id = ?1 AND candidate_id = ?2", + params![subject_id, candidate_id], + |row| row.get::<_, String>(0), + ) + .optional()?; + let raw = match raw { + Some(raw) => raw, + None => { + let foreign = transaction + .query_row( + "SELECT subject_id FROM staging_records WHERE candidate_id = ?1 LIMIT 1", + [candidate_id], + |row| row.get::<_, String>(0), + ) + .optional()?; + if foreign.is_some() { + return Err(SubjektivError::SubjectScopeMismatch { + subject_id: subject_id.to_string(), + reference: candidate_id.to_string(), + }); + } + return Err(SubjektivError::CandidateNotFound(candidate_id.to_string())); + } + }; + let resolved = transaction + .query_row( + "SELECT 1 FROM staging_resolutions + WHERE subject_id = ?1 AND candidate_id = ?2", + params![subject_id, candidate_id], + |_| Ok(()), + ) + .optional()? + .is_some(); + if resolved { + return Err(SubjektivError::CandidateResolved(candidate_id.to_string())); + } + Ok(raw) +} + +fn require_subject(transaction: &Transaction<'_>, subject_id: &str) -> Result { + require_subject_in_connection(transaction, subject_id) +} + +fn require_subject_in_connection( + connection: &rusqlite::Connection, + subject_id: &str, +) -> Result { + let raw = connection + .query_row( + "SELECT record_json FROM subjects WHERE subject_id = ?1", + [subject_id], + |row| row.get::<_, String>(0), + ) + .optional()? + .ok_or_else(|| SubjektivError::SubjectNotFound(subject_id.to_string()))?; + parse_subject(&raw) +} + +fn require_active_subject( + transaction: &Transaction<'_>, + subject_id: &str, +) -> Result { + let subject = require_subject(transaction, subject_id)?; + if subject.state == SubjectState::Retired { + return Err(SubjektivError::SubjectRetired(subject_id.to_string())); + } + Ok(subject) +} + +fn validate_candidate_refs( + transaction: &Transaction<'_>, + subject_id: &str, + candidate_ids: &[String], +) -> Result<()> { + for candidate_id in candidate_ids { + let exists = transaction + .query_row( + "SELECT 1 FROM staging_records + WHERE subject_id = ?1 AND candidate_id = ?2", + params![subject_id, candidate_id], + |_| Ok(()), + ) + .optional()? + .is_some(); + if !exists { + return Err(SubjektivError::SubjectScopeMismatch { + subject_id: subject_id.to_string(), + reference: candidate_id.clone(), + }); + } + } + Ok(()) +} + +fn validate_memory_refs( + transaction: &Transaction<'_>, + subject_id: &str, + references: &[MemoryRevisionRef], +) -> Result<()> { + for reference in references { + if reference.revision == 0 { + return Err(SubjektivError::InvalidRecord(format!( + "Memory `{}` revision must be greater than zero", + reference.memory_id + ))); + } + let exists = transaction + .query_row( + "SELECT 1 FROM memory_revisions + WHERE subject_id = ?1 AND memory_id = ?2 AND revision = ?3", + params![subject_id, reference.memory_id, to_i64(reference.revision)?], + |_| Ok(()), + ) + .optional()? + .is_some(); + if !exists { + return Err(SubjektivError::SubjectScopeMismatch { + subject_id: subject_id.to_string(), + reference: format!("{}@{}", reference.memory_id, reference.revision), + }); + } + } + Ok(()) +} + +fn validate_session_attribution(record: &SubjectSessionAttribution) -> Result<()> { + validate_schema_version(record.schema_version, "subject Session attribution")?; + validate_label("subject id", &record.subject_id)?; + validate_label("runtime id", &record.runtime_id)?; + validate_label("worker id", &record.worker_id)?; + validate_label("session id", &record.session_id)?; + validate_label("Session attribution timestamp", &record.attributed_at)?; + chrono::DateTime::parse_from_rfc3339(&record.attributed_at).map_err(|error| { + SubjektivError::InvalidRecord(format!( + "Session attribution timestamp is not RFC 3339: {error}" + )) + })?; + Ok(()) +} + +fn attach_session_to_candidate(record: &mut SubjectStagingRecord, session_id: &str) -> Result<()> { + if record.source_refs.is_empty() { + record.source_refs.push(SourceEvidenceRef { + session_id: Some(session_id.to_string()), + segment_id: Some(record.source.segment_id.clone()), + entry_range: Some(record.source.range), + ..SourceEvidenceRef::default() + }); + return Ok(()); + } + for source in &mut record.source_refs { + match source.session_id.as_deref() { + Some(existing) if existing != session_id => { + return Err(SubjektivError::SubjectScopeMismatch { + subject_id: record.subject_id.clone(), + reference: format!( + "candidate source Session `{existing}` does not match attributed Session `{session_id}`" + ), + }); + } + Some(_) => {} + None => source.session_id = Some(session_id.to_string()), + } + } + Ok(()) +} + +fn validate_staging_record(workspace_id: &str, record: &SubjectStagingRecord) -> Result<()> { + if record.schema_version != STAGING_SCHEMA_VERSION { + return Err(SubjektivError::InvalidRecord(format!( + "staging schema version {} is unsupported", + record.schema_version + ))); + } + validate_label("candidate id", &record.id)?; + validate_label("subject id", &record.subject_id)?; + validate_label("extract run id", &record.extract_run_id)?; + validate_label("source segment id", &record.source.segment_id)?; + validate_nonempty("candidate claim", &record.claim)?; + validate_nonempty("candidate why_useful", &record.why_useful)?; + validate_range("candidate source", record.source.range)?; + if matches!(record.kind, CandidateKind::Preference) + && record.evidence.iter().any(|evidence| { + !matches!( + evidence.origin.as_ref().map(|origin| &origin.kind), + Some(memory::schema::EvidenceOriginKind::HumanInput) + ) + }) + { + return Err(SubjektivError::InvalidRecord( + "preference candidates require exclusively HumanInput evidence".to_string(), + )); + } + + let mut evidence_ids = HashSet::new(); + for evidence in &record.evidence { + validate_label("evidence id", &evidence.id)?; + if !evidence_ids.insert(evidence.id.as_str()) { + return Err(SubjektivError::InvalidRecord(format!( + "duplicate evidence id `{}`", + evidence.id + ))); + } + validate_nonempty("evidence kind", evidence.kind.as_str())?; + if let Some(range) = evidence.entry_range { + validate_range("evidence entry", range)?; + } + } + for source in &record.source_refs { + if let Some(session_id) = source.session_id.as_deref() { + validate_label("source session id", session_id)?; + } + if let Some(segment_id) = source.segment_id.as_deref() { + validate_label("source segment id", segment_id)?; + } + if let Some(range) = source.entry_range { + validate_range("source evidence entry", range)?; + } + if let Some(evidence_id) = source.evidence_id.as_deref() { + validate_label("source evidence id", evidence_id)?; + if !evidence_ids.contains(evidence_id) { + return Err(SubjektivError::InvalidRecord(format!( + "source evidence id `{evidence_id}` is not present in evidence" + ))); + } + } + if let Some(kind) = source.evidence_kind.as_ref() { + validate_nonempty("source evidence kind", kind.as_str())?; + } + } + for origin in record + .evidence + .iter() + .filter_map(|evidence| evidence.origin.as_ref()) + .chain( + record + .source_refs + .iter() + .filter_map(|source| source.origin.as_ref()), + ) + { + if let Some(origin_workspace) = origin.workspace_id.as_deref() + && origin_workspace != workspace_id + { + return Err(SubjektivError::SubjectScopeMismatch { + subject_id: record.subject_id.clone(), + reference: format!("evidence Workspace {origin_workspace}"), + }); + } + } + if let Some(proposal) = &record.revision_proposal { + validate_revision_proposal_metadata(proposal)?; + } + Ok(()) +} + +/// Applies the current write-admission contract separately from structural +/// validation. Persisted rows from older compatible implementations remain +/// readable even when a later release tightens admission limits. +fn validate_staging_admission(record: &SubjectStagingRecord) -> Result<()> { + if record.evidence.len() > MAX_STAGING_ANCHORS || record.source_refs.len() > MAX_STAGING_ANCHORS + { + return Err(SubjektivError::InvalidRecord(format!( + "staging evidence/source_refs are limited to {MAX_STAGING_ANCHORS} items each" + ))); + } + Ok(()) +} + +fn validate_revision_proposal_metadata(proposal: &RevisionProposal) -> Result<()> { + validate_label("revision proposal Memory id", &proposal.memory_id)?; + if proposal.expected_revision == 0 { + return Err(SubjektivError::InvalidRecord( + "revision proposal expected_revision must be greater than zero".into(), + )); + } + validate_nonempty("revision proposal change_reason", &proposal.change_reason) +} + +fn validate_candidate_application( + transaction: &Transaction<'_>, + subject_id: &str, + candidate: &SubjectStagingRecord, + target: &MemoryRevisionTarget, + draft: &MemoryDraft, +) -> Result<()> { + let Some(proposal) = &candidate.revision_proposal else { + return Ok(()); + }; + let MemoryRevisionTarget::Revise { + memory_id, + expected_revision, + } = target + else { + return Err(SubjektivError::InvalidRecord( + "a revision proposal cannot create a different Memory".into(), + )); + }; + if memory_id != &proposal.memory_id || expected_revision != &proposal.expected_revision { + return Err(SubjektivError::InvalidRecord( + "candidate application target must exactly match revision proposal metadata".into(), + )); + } + let current = scoped_memory_in_connection(transaction, subject_id, memory_id)?; + if current.revision != *expected_revision { + return Err(SubjektivError::RevisionConflict { + memory_id: memory_id.clone(), + expected: *expected_revision, + actual: current.revision, + }); + } + let expected_state = match proposal.intent { + RevisionProposalIntent::Revise => current.state, + RevisionProposalIntent::Resolve => MemoryState::Resolved, + RevisionProposalIntent::Retract => MemoryState::Retracted, + RevisionProposalIntent::Reopen => MemoryState::Active, + }; + let intent_allowed = match proposal.intent { + RevisionProposalIntent::Revise => { + matches!(current.state, MemoryState::Active | MemoryState::Resolved) + } + RevisionProposalIntent::Resolve => current.state == MemoryState::Active, + RevisionProposalIntent::Retract => { + matches!(current.state, MemoryState::Active | MemoryState::Resolved) + } + RevisionProposalIntent::Reopen => current.state == MemoryState::Resolved, + }; + if !intent_allowed || draft.state != expected_state { + return Err(SubjektivError::InvalidStateTransition { + memory_id: memory_id.clone(), + from: current.state, + to: draft.state, + }); + } + if draft.kind != current.kind || draft.kind != candidate.kind { + return Err(SubjektivError::InvalidRecord( + "revision proposal application must preserve the target Memory kind".into(), + )); + } + if draft.change_reason != proposal.change_reason { + return Err(SubjektivError::InvalidRecord( + "revision proposal application must preserve change_reason".into(), + )); + } + Ok(()) +} + +fn validate_staged_revision_proposal( + transaction: &Transaction<'_>, + record: &SubjectStagingRecord, +) -> Result<()> { + let Some(proposal) = &record.revision_proposal else { + return Ok(()); + }; + let current = + scoped_memory_in_connection(transaction, &record.subject_id, &proposal.memory_id)?; + if current.revision != proposal.expected_revision { + return Err(SubjektivError::RevisionConflict { + memory_id: proposal.memory_id.clone(), + expected: proposal.expected_revision, + actual: current.revision, + }); + } + let to = match proposal.intent { + RevisionProposalIntent::Revise => current.state, + RevisionProposalIntent::Resolve => MemoryState::Resolved, + RevisionProposalIntent::Retract => MemoryState::Retracted, + RevisionProposalIntent::Reopen => MemoryState::Active, + }; + let allowed = match proposal.intent { + RevisionProposalIntent::Revise => { + matches!(current.state, MemoryState::Active | MemoryState::Resolved) + } + RevisionProposalIntent::Resolve => current.state == MemoryState::Active, + RevisionProposalIntent::Retract => { + matches!(current.state, MemoryState::Active | MemoryState::Resolved) + } + RevisionProposalIntent::Reopen => current.state == MemoryState::Resolved, + }; + if allowed { + Ok(()) + } else { + Err(SubjektivError::InvalidStateTransition { + memory_id: proposal.memory_id.clone(), + from: current.state, + to, + }) + } +} + +fn scoped_memory_in_connection( + connection: &rusqlite::Connection, + subject_id: &str, + memory_id: &str, +) -> Result { + let raw = connection + .query_row( + "SELECT record_json FROM memory_records + WHERE subject_id = ?1 AND memory_id = ?2", + params![subject_id, memory_id], + |row| row.get::<_, String>(0), + ) + .optional()?; + if let Some(raw) = raw { + return parse_memory(&raw); + } + let foreign_owner = connection + .query_row( + "SELECT subject_id FROM memory_records WHERE memory_id = ?1 LIMIT 1", + [memory_id], + |row| row.get::<_, String>(0), + ) + .optional()?; + if foreign_owner.is_some() { + Err(SubjektivError::SubjectScopeMismatch { + subject_id: subject_id.to_string(), + reference: memory_id.to_string(), + }) + } else { + Err(SubjektivError::MemoryNotFound(memory_id.to_string())) + } +} + +fn validate_memory_draft(draft: &MemoryDraft) -> Result<()> { + validate_nonempty("Memory claim", &draft.claim)?; + validate_nonempty("Memory why_useful", &draft.why_useful)?; + validate_nonempty("Memory change_reason", &draft.change_reason)?; + for candidate_id in &draft.source_candidate_ids { + validate_label("source candidate id", candidate_id)?; + } + for reference in &draft.derived_from { + validate_label("derived Memory id", &reference.memory_id)?; + } + Ok(()) +} + +fn validate_direct_memory_draft(draft: &MemoryDraft) -> Result<()> { + if draft.source_candidate_ids.is_empty() { + Ok(()) + } else { + Err(SubjektivError::InvalidRecord( + "candidate provenance must be committed through apply_candidate(s)".into(), + )) + } +} + +fn validate_state_transition(memory_id: &str, from: MemoryState, to: MemoryState) -> Result<()> { + let allowed = from != MemoryState::Retracted; + if allowed { + Ok(()) + } else { + Err(SubjektivError::InvalidStateTransition { + memory_id: memory_id.to_string(), + from, + to, + }) + } +} + +fn parse_subject(raw: &str) -> Result { + let record: SubjectRecord = serde_json::from_str(raw)?; + validate_schema_version(record.schema_version, "subject")?; + Ok(record) +} + +fn equivalent_session_attribution( + left: &SubjectSessionAttribution, + right: &SubjectSessionAttribution, +) -> bool { + left.schema_version == right.schema_version + && left.subject_id == right.subject_id + && left.runtime_id == right.runtime_id + && left.worker_id == right.worker_id + && left.session_id == right.session_id +} + +fn equivalent_staging_payload(left: &str, right: &str) -> Result { + let mut left = serde_json::from_str::(left)?; + let mut right = serde_json::from_str::(right)?; + let left_object = left.as_object_mut().ok_or_else(|| { + SubjektivError::InvalidRecord("stored staging record is not a JSON object".into()) + })?; + let right_object = right.as_object_mut().ok_or_else(|| { + SubjektivError::InvalidRecord("new staging record is not a JSON object".into()) + })?; + // A retry may reconstruct the host envelope after an ambiguous response. + // Identity and extracted content must match, while the first persisted + // host-issued staging timestamp remains authoritative. + left_object.remove("created_at"); + right_object.remove("created_at"); + Ok(left == right) +} + +fn parse_session_attribution(raw: &str) -> Result { + let record: SubjectSessionAttribution = serde_json::from_str(raw)?; + validate_session_attribution(&record)?; + Ok(record) +} + +fn parse_staging(workspace_id: &str, raw: &str) -> Result { + let record = parse_staging_without_workspace(raw)?; + validate_staging_record(workspace_id, &record)?; + Ok(record) +} + +fn parse_staging_without_workspace(raw: &str) -> Result { + let record: SubjectStagingRecord = serde_json::from_str(raw)?; + if record.schema_version != STAGING_SCHEMA_VERSION { + return Err(SubjektivError::InvalidRecord(format!( + "staging schema version {} is unsupported", + record.schema_version + ))); + } + Ok(record) +} + +fn parse_memory(raw: &str) -> Result { + let record: MemoryRecord = serde_json::from_str(raw)?; + validate_schema_version(record.schema_version, "Memory")?; + if record.revision == 0 { + return Err(SubjektivError::InvalidRecord( + "Memory revision must be greater than zero".to_string(), + )); + } + Ok(record) +} + +fn parse_resolution(raw: &str) -> Result { + let record: StagingResolution = serde_json::from_str(raw)?; + validate_schema_version(record.schema_version, "staging resolution")?; + Ok(record) +} + +fn parse_surface_snapshot(raw: &str) -> Result { + let record: SurfaceSnapshot = serde_json::from_str(raw)?; + validate_schema_version(record.schema_version, "surface snapshot")?; + Ok(record) +} + +fn validate_schema_version(version: u32, kind: &str) -> Result<()> { + if version == SUBJEKTIV_SCHEMA_VERSION { + Ok(()) + } else { + Err(SubjektivError::InvalidRecord(format!( + "{kind} schema version {version} is unsupported" + ))) + } +} + +fn validate_range(kind: &str, range: [u64; 2]) -> Result<()> { + if range[0] <= range[1] { + Ok(()) + } else { + Err(SubjektivError::InvalidRecord(format!( + "{kind} range is reversed" + ))) + } +} + +fn resident_surface_in_connection( + connection: &Connection, + subject: &SubjectRecord, +) -> Result { + let state = connection + .query_row( + "SELECT store_revision, status, snapshot_id + FROM surface_generation_state WHERE subject_id = ?1", + [&subject.id], + |row| { + Ok(( + row.get::<_, i64>(0)?, + row.get::<_, String>(1)?, + row.get::<_, Option>(2)?, + )) + }, + ) + .optional()?; + let Some((revision, status, snapshot_id)) = state else { + return Ok(ResidentSurface { + availability: SurfaceAvailability::Ungenerated, + snapshot: None, + }); + }; + let revision = u64::try_from(revision) + .map_err(|_| SubjektivError::InvalidRecord("surface state revision is negative".into()))?; + if revision != subject.store_revision || status == "dirty" { + return Ok(ResidentSurface { + availability: SurfaceAvailability::Stale, + snapshot: None, + }); + } + if status == "failed" { + return Ok(ResidentSurface { + availability: SurfaceAvailability::Failed, + snapshot: None, + }); + } + let snapshot_id = snapshot_id.ok_or_else(|| { + SubjektivError::InvalidRecord("ready surface state has no snapshot id".into()) + })?; + let raw = connection.query_row( + "SELECT snapshot_json FROM surface_snapshots + WHERE subject_id = ?1 AND snapshot_id = ?2", + params![subject.id, snapshot_id], + |row| row.get::<_, String>(0), + )?; + let snapshot = parse_surface_snapshot(&raw)?; + if snapshot.built_from_store_revision != subject.store_revision { + return Err(SubjektivError::InvalidRecord( + "ready surface snapshot generation does not match subject".into(), + )); + } + Ok(ResidentSurface { + availability: SurfaceAvailability::Ready, + snapshot: Some(snapshot), + }) +} + +pub fn validate_subject_behavior(value: &str) -> Result<()> { + if value.len() > MAX_SUBJECT_BEHAVIOR_BYTES { + return Err(SubjektivError::InvalidRecord(format!( + "subject behavior must be at most {MAX_SUBJECT_BEHAVIOR_BYTES} bytes" + ))); + } + if !value.is_empty() && value.trim().is_empty() { + return Err(SubjektivError::InvalidRecord( + "subject behavior must be empty or contain non-whitespace text".to_string(), + )); + } + if value + .chars() + .any(|character| character.is_control() && !matches!(character, '\n' | '\r' | '\t')) + { + return Err(SubjektivError::InvalidRecord( + "subject behavior contains an unsupported control character".to_string(), + )); + } + Ok(()) +} + +fn validate_label(kind: &str, value: &str) -> Result<()> { + validate_nonempty(kind, value)?; + if value.len() > 256 || value.chars().any(char::is_control) { + return Err(SubjektivError::InvalidRecord(format!( + "{kind} must be at most 256 bytes and contain no control characters" + ))); + } + Ok(()) +} + +fn validate_nonempty(kind: &str, value: &str) -> Result<()> { + if value.trim().is_empty() { + Err(SubjektivError::InvalidRecord(format!( + "{kind} must not be empty" + ))) + } else { + Ok(()) + } +} + +fn reject_duplicates(kind: &str, values: &[String]) -> Result<()> { + let mut seen = HashSet::new(); + if let Some(duplicate) = values.iter().find(|value| !seen.insert(value.as_str())) { + return Err(SubjektivError::InvalidRecord(format!( + "duplicate {kind} `{duplicate}`" + ))); + } + Ok(()) +} + +fn reject_duplicate_refs(kind: &str, values: &[MemoryRevisionRef]) -> Result<()> { + let mut seen = HashSet::new(); + if let Some(duplicate) = values + .iter() + .find(|value| !seen.insert((value.memory_id.as_str(), value.revision))) + { + return Err(SubjektivError::InvalidRecord(format!( + "duplicate {kind} `{}@{}`", + duplicate.memory_id, duplicate.revision + ))); + } + Ok(()) +} + +fn reject_duplicate_memory_ids(kind: &str, values: &[MemoryRevisionRef]) -> Result<()> { + let mut seen = HashSet::new(); + if let Some(duplicate) = values + .iter() + .find(|value| !seen.insert(value.memory_id.as_str())) + { + return Err(SubjektivError::InvalidRecord(format!( + "duplicate {kind} Memory `{}`", + duplicate.memory_id + ))); + } + Ok(()) +} + +fn require_job_generation( + connection: &Connection, + subject_id: &str, + generation_id: &str, + job_id: &str, + attempt_id: &str, +) -> Result { + let revision: Option = connection.query_row( + "SELECT store_revision FROM surface_generation_runs WHERE subject_id = ?1 AND generation_id = ?2 AND job_id = ?3 AND attempt_id = ?4", + params![subject_id, generation_id, job_id, attempt_id], |row| row.get(0), + ).optional()?; + revision + .and_then(|value| u64::try_from(value).ok()) + .ok_or_else(|| { + SubjektivError::InvalidRecord( + "surface generation is not owned by this Job attempt".into(), + ) + }) +} + +fn to_i64(value: u64) -> Result { + i64::try_from(value).map_err(|_| { + SubjektivError::InvalidRecord(format!("revision value {value} exceeds SQLite range")) + }) +} + +fn issued_id(kind: &str) -> String { + format!("{kind}-{}", Uuid::now_v7()) +} + +fn now() -> String { + Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true) +} + +fn candidate_kind_name(kind: &CandidateKind) -> &'static str { + kind.as_str() +} + +fn memory_state_name(state: MemoryState) -> &'static str { + match state { + MemoryState::Active => "active", + MemoryState::Resolved => "resolved", + MemoryState::Retracted => "retracted", + } +} + +fn resolution_action_name(action: StagingResolutionAction) -> &'static str { + match action { + StagingResolutionAction::Applied => "applied", + StagingResolutionAction::Discarded => "discarded", + StagingResolutionAction::Invalid => "invalid", + StagingResolutionAction::Duplicate => "duplicate", + StagingResolutionAction::AlreadyCovered => "already_covered", + } +} + +#[cfg(test)] +mod tests { + use std::sync::{Arc, Barrier}; + use std::thread; + use std::time::Duration; + + use memory::extract::{ExtractedCandidate, StagingEvidence}; + use memory::schema::{ + EvidenceKind, EvidenceOrigin, EvidenceOriginKind, SourceEvidenceRef, SourceRef, + }; + + use super::*; + use feature_storage::FeatureStorage; + + static V1_MIGRATIONS: &[FeatureMigration] = &[FeatureMigration::new( + 1, + "create subjektiv subject memory store", + create_schema, + )]; + static V3_MIGRATIONS: &[FeatureMigration] = &[ + FeatureMigration::new(1, "create subjektiv subject memory store", create_schema), + FeatureMigration::new( + 2, + "add immutable subject session attribution", + add_subject_session_attribution, + ), + FeatureMigration::new( + 3, + "add idempotent atomic candidate decision receipts", + add_candidate_decision_receipts, + ), + ]; + + fn role() -> SubjectRole { + SubjectRole::new("workspace_companion").unwrap() + } + + fn candidate(subject_id: &str, candidate_id: &str, workspace_id: &str) -> SubjectStagingRecord { + let origin = EvidenceOrigin { + kind: EvidenceOriginKind::HumanInput, + account_id: Some("account-1".into()), + workspace_id: Some(workspace_id.into()), + runtime_id: None, + worker_id: None, + flow_selector: None, + flow_definition_id: None, + flow_definition_revision: None, + }; + let record = StagingRecord::from_candidate( + candidate_id, + "extract-run-1", + SourceRef { + segment_id: "segment-1".into(), + range: [10, 12], + }, + ExtractedCandidate { + kind: CandidateKind::Constraint, + claim: "Do not treat Memory text as authority".into(), + why_useful: "Keeps experience separate from authorization".into(), + staleness: Some("Revisit if the authority model changes".into()), + evidence_ids: vec!["evidence-1".into()], + }, + vec![StagingEvidence { + id: "evidence-1".into(), + kind: EvidenceKind::new(EvidenceKind::MESSAGE), + entry_range: Some([10, 12]), + origin: Some(origin.clone()), + excerpt: Some("Memory is not authority".into()), + summary: Some("A bounded source summary".into()), + }], + vec![SourceEvidenceRef { + session_id: Some("session-1".into()), + segment_id: Some("segment-1".into()), + entry_range: Some([10, 12]), + evidence_id: Some("evidence-1".into()), + origin: Some(origin), + evidence_kind: Some(EvidenceKind::new(EvidenceKind::MESSAGE)), + label: Some("design discussion".into()), + summary: Some("Authority boundary".into()), + }], + ); + SubjectStagingRecord::attach_at(subject_id, record, "2026-09-28T10:00:00.000Z".into()) + } + + fn attribution( + subject_id: &str, + runtime_id: &str, + worker_id: &str, + session_id: &str, + attributed_at: &str, + ) -> SubjectSessionAttribution { + let mut record = + SubjectSessionAttribution::new(subject_id, runtime_id, worker_id, session_id).unwrap(); + record.attributed_at = attributed_at.to_string(); + record + } + + fn draft(claim: &str, reason: &str) -> MemoryDraft { + MemoryDraft::active( + CandidateKind::Constraint, + claim, + "Context, applicability, and consequences.", + "It preserves the subject's learned boundary.", + reason, + ) + } + + fn proposal( + intent: RevisionProposalIntent, + memory_id: &str, + expected_revision: u64, + change_reason: &str, + ) -> RevisionProposal { + RevisionProposal::new(intent, memory_id, expected_revision, change_reason).unwrap() + } + + fn open_store( + root: &std::path::Path, + workspace_id: &str, + ) -> (FeatureStorage, ScopedFeatureStorage, SubjektivStore) { + let manager = FeatureStorage::new(root); + let workspace = manager.workspace(workspace_id).unwrap(); + let registration = SubjektivStore::register(&workspace).unwrap(); + let store = SubjektivStore::open(&workspace, ®istration).unwrap(); + (manager, workspace, store) + } + + #[test] + fn subject_listing_is_deterministic_bounded_and_read_only() { + let temp = tempfile::tempdir().unwrap(); + let (_manager, _workspace, store) = open_store(temp.path(), "workspace-a"); + let mut expected = vec![ + store.create_subject(role()).unwrap(), + store.create_subject(role()).unwrap(), + store.create_subject(role()).unwrap(), + ]; + expected.sort_by(|left, right| { + left.created_at + .cmp(&right.created_at) + .then_with(|| left.id.cmp(&right.id)) + }); + + let page = store.list_subjects(2).unwrap(); + assert_eq!(page.items, expected[..2]); + assert!(page.has_more); + let after = page.items.last().unwrap(); + let next = store + .list_subjects_after(2, Some((&after.created_at, &after.id))) + .unwrap(); + assert_eq!(next.items, expected[2..]); + assert!(!next.has_more); + assert!(store.list_subjects(0).is_err()); + assert!(store.list_subjects(101).is_err()); + + assert!(store.subject("missing-subject").unwrap().is_none()); + let complete = store.list_subjects(3).unwrap(); + assert_eq!(complete.items, expected); + assert!(!complete.has_more); + } + + #[test] + fn subject_behavior_is_bounded_revisioned_and_independent_from_memory_revision() { + let temp = tempfile::tempdir().unwrap(); + let (_manager, _workspace, store) = open_store(temp.path(), "workspace-a"); + let subject = store + .create_subject_with_behavior(role(), "Prefer explicit evidence.".to_string()) + .unwrap(); + assert_eq!(subject.behavior_revision, 0); + assert_eq!(subject.store_revision, 0); + + let updated = store + .update_subject_behavior( + &subject.id, + 0, + "Prefer explicit evidence.\nAsk when uncertain.".to_string(), + ) + .unwrap(); + assert_eq!(updated.behavior_revision, 1); + assert_eq!(updated.store_revision, 0); + + store + .create_memory( + &subject.id, + draft( + "A confirmed observation", + "exercise Memory lifecycle isolation", + ), + ) + .unwrap(); + let after_memory = store.subject(&subject.id).unwrap().unwrap(); + assert_eq!(after_memory.behavior_md, updated.behavior_md); + assert_eq!(after_memory.behavior_revision, 1); + assert_eq!(after_memory.store_revision, 1); + + let (_reopened_manager, _reopened_workspace, reopened) = + open_store(temp.path(), "workspace-a"); + let persisted = reopened.subject(&subject.id).unwrap().unwrap(); + assert_eq!(persisted.behavior_md, updated.behavior_md); + assert_eq!(persisted.behavior_revision, 1); + assert!(matches!( + store.update_subject_behavior(&subject.id, 0, String::new()), + Err(SubjektivError::SubjectBehaviorConflict { + expected: 0, + actual: 1, + .. + }) + )); + + let unchanged = store + .update_subject_behavior(&subject.id, 1, updated.behavior_md.clone()) + .unwrap(); + assert_eq!(unchanged.behavior_revision, 1); + let cleared = store + .update_subject_behavior(&subject.id, 1, String::new()) + .unwrap(); + assert_eq!(cleared.behavior_revision, 2); + assert!(cleared.behavior_md.is_empty()); + assert!(validate_subject_behavior(&"x".repeat(MAX_SUBJECT_BEHAVIOR_BYTES + 1)).is_err()); + assert!(validate_subject_behavior(" ").is_err()); + } + + #[test] + fn attributed_candidates_preserve_revision_and_surface_reference_continuity() { + let temp = tempfile::tempdir().unwrap(); + let (_manager, _workspace, store) = open_store(temp.path(), "workspace-a"); + let subject = store.create_subject(role()).unwrap(); + let session = attribution( + &subject.id, + "runtime-1", + "worker-1", + "session-1", + "2026-09-28T09:00:00.000Z", + ); + + store.record_session_attribution(session.clone()).unwrap(); + let (first_candidate, stored_session) = store + .stage_candidate_with_attribution( + candidate(&subject.id, "candidate-1", "workspace-a"), + session.clone(), + ) + .unwrap(); + assert_eq!(stored_session, session); + assert_eq!( + first_candidate.source_refs[0].session_id.as_deref(), + Some("session-1") + ); + + let create_request = CandidateDecisionRequest { + request_id: "decision-create-1".into(), + candidate_id: first_candidate.id.clone(), + reason: "adopt the attributed candidate".into(), + decision: CandidateDecision::Apply { + target: MemoryRevisionTarget::Create, + draft: draft( + "Keep the first confirmed rule", + "initial attributed decision", + ), + }, + }; + let created = store + .decide_candidate(&subject.id, create_request.clone()) + .unwrap(); + let first_revision = created.memory.unwrap(); + assert_eq!(first_revision.revision, 1); + assert_eq!( + first_revision.source_candidate_ids, + [first_candidate.id.clone()] + ); + assert_eq!(created.resolution.affected_memory.len(), 1); + assert_eq!(created.resolution.affected_memory[0].revision, 1); + + // Exact decision retries return the original receipt rather than creating + // a second revision or resolution. + let replayed = store.decide_candidate(&subject.id, create_request).unwrap(); + assert_eq!(replayed.store_revision, created.store_revision); + assert_eq!(replayed.memory.as_ref().unwrap().revision, 1); + + let (second_candidate, _) = store + .stage_candidate_with_attribution( + candidate(&subject.id, "candidate-2", "workspace-a"), + session, + ) + .unwrap(); + let revised = store + .decide_candidate( + &subject.id, + CandidateDecisionRequest { + request_id: "decision-revise-2".into(), + candidate_id: second_candidate.id.clone(), + reason: "apply the attributed correction".into(), + decision: CandidateDecision::Apply { + target: MemoryRevisionTarget::Revise { + memory_id: first_revision.id.clone(), + expected_revision: 1, + }, + draft: draft("Keep the corrected confirmed rule", "attributed correction"), + }, + }, + ) + .unwrap(); + let second_revision = revised.memory.unwrap(); + assert_eq!(second_revision.revision, 2); + assert_eq!(second_revision.source_candidate_ids, [second_candidate.id]); + + let historical = store + .scoped_memory_revision(&subject.id, &first_revision.id, 1) + .unwrap() + .unwrap(); + assert_eq!(historical.source_candidate_ids, [first_candidate.id]); + assert_eq!( + store + .session_attribution("session-1") + .unwrap() + .unwrap() + .subject_id, + subject.id + ); + + let generation = store.prepare_surface_generation(&subject.id).unwrap(); + assert_eq!(generation.materials.len(), 1); + assert_eq!(generation.materials[0].memory_id, second_revision.id); + assert_eq!(generation.materials[0].revision, 2); + let expected_ref = MemoryRevisionRef { + memory_id: second_revision.id.clone(), + revision: second_revision.revision, + }; + let published = store + .publish_surface_generation( + &subject.id, + &generation.id, + vec![SurfacePoint { + body_md: "- Keep the corrected confirmed rule".into(), + memory_refs: vec![expected_ref.clone()], + }], + ) + .unwrap(); + assert_eq!(published.memory_refs, [expected_ref.clone()]); + assert_eq!(published.built_from_store_revision, revised.store_revision); + + let resident = store.resident_surface(&subject.id).unwrap(); + assert_eq!(resident.availability, SurfaceAvailability::Ready); + assert_eq!(resident.snapshot.unwrap().memory_refs, [expected_ref]); + } + + #[test] + fn versioned_json_roundtrips_and_rejects_invalid_shapes() { + let staging = candidate("subject-1", "candidate-1", "workspace-a"); + let raw = serde_json::to_string_pretty(&staging).unwrap(); + let decoded: SubjectStagingRecord = serde_json::from_str(&raw).unwrap(); + assert_eq!(decoded.schema_version, STAGING_SCHEMA_VERSION); + assert_eq!(decoded.subject_id, "subject-1"); + assert_eq!( + decoded.source_refs[0].session_id.as_deref(), + Some("session-1") + ); + assert!(decoded.revision_proposal.is_none()); + assert!(!raw.contains("revision_proposal")); + assert!(raw.contains("human_input")); + + let proposed = staging + .clone() + .with_revision_proposal(proposal( + RevisionProposalIntent::Revise, + "memory-1", + 3, + "Incorporate the new bounded evidence", + )) + .unwrap(); + let proposed_raw = serde_json::to_string(&proposed).unwrap(); + let proposed_decoded: SubjectStagingRecord = serde_json::from_str(&proposed_raw).unwrap(); + assert_eq!( + proposed_decoded.revision_proposal, + Some(proposal( + RevisionProposalIntent::Revise, + "memory-1", + 3, + "Incorporate the new bounded evidence", + )) + ); + assert!( + RevisionProposal::new( + RevisionProposalIntent::Revise, + "memory-1", + 0, + "invalid zero revision" + ) + .is_err() + ); + assert!( + RevisionProposal::new(RevisionProposalIntent::Revise, "memory-1", 1, " ").is_err() + ); + assert!( + serde_json::from_value::(serde_json::json!({ + "intent": "revise", + "memory_id": "memory-1", + "expected_revision": 0, + "change_reason": "invalid" + })) + .is_err() + ); + + let mut value = serde_json::to_value(&staging).unwrap(); + value + .as_object_mut() + .unwrap() + .insert("invented".into(), serde_json::json!(true)); + assert!(serde_json::from_value::(value).is_err()); + + let memory = MemoryRecord { + schema_version: SUBJEKTIV_SCHEMA_VERSION, + id: "memory-1".into(), + subject_id: "subject-1".into(), + revision: 1, + kind: CandidateKind::Constraint, + state: MemoryState::Active, + claim: "Memory is not authority".into(), + body_md: "Applies to every tool call.".into(), + why_useful: "Avoids privilege confusion".into(), + staleness: None, + source_candidate_ids: vec!["candidate-1".into()], + derived_from: vec![], + change_reason: "Accepted the staged constraint".into(), + created_at: "2026-09-28T10:00:00.000Z".into(), + updated_at: "2026-09-28T10:00:00.000Z".into(), + }; + let raw = serde_json::to_string(&memory).unwrap(); + let decoded: MemoryRecord = serde_json::from_str(&raw).unwrap(); + assert_eq!(decoded.revision, 1); + assert!(serde_json::from_str::(&raw.replace("active", "unknown")).is_err()); + } + + #[test] + fn v1_store_migrates_before_attributed_staging() { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("storage"); + let subject_id = { + let manager = FeatureStorage::new(&root); + let workspace = manager.workspace("workspace-a").unwrap(); + let registration = workspace + .register(FeatureRegistration::new( + SUBJEKTIV_FEATURE_ID, + V1_MIGRATIONS, + )) + .unwrap(); + let store = SubjektivStore::open(&workspace, ®istration).unwrap(); + let timestamp = now(); + let subject_id = issued_id("subject"); + let mut raw = serde_json::json!({ + "schema_version": SUBJEKTIV_SCHEMA_VERSION, + "id": subject_id, + "role": role(), + "state": SubjectState::Active, + "store_revision": 0, + "created_at": timestamp.clone(), + "updated_at": timestamp.clone(), + }); + let raw = serde_json::to_string(&raw.take()).unwrap(); + store + .database + .try_transaction(|transaction| -> Result<()> { + transaction.execute( + "INSERT INTO subjects ( + subject_id, role, state, store_revision, + record_json, created_at, updated_at + ) VALUES (?1, ?2, 'active', 0, ?3, ?4, ?5)", + params![subject_id, role().as_str(), raw, timestamp, timestamp], + )?; + Ok(()) + }) + .unwrap(); + subject_id + }; + + let (_manager, _workspace, store) = open_store(&root, "workspace-a"); + let migrated_subject = store.subject(&subject_id).unwrap().unwrap(); + assert!(migrated_subject.behavior_md.is_empty()); + assert_eq!(migrated_subject.behavior_revision, 0); + let lifecycle_attribution = attribution( + &subject_id, + "runtime-1", + "worker-1", + "session-1", + "2026-09-28T10:00:00.000Z", + ); + store + .record_session_attribution(lifecycle_attribution.clone()) + .unwrap(); + let (staged, stored_attribution) = store + .stage_candidate_with_attribution( + candidate(&subject_id, "candidate-after-migration", "workspace-a"), + lifecycle_attribution, + ) + .unwrap(); + + assert_eq!(staged.subject_id, subject_id); + assert_eq!(stored_attribution.session_id, "session-1"); + assert_eq!( + store.session_attribution("session-1").unwrap().unwrap(), + stored_attribution + ); + } + + #[test] + fn legacy_surface_snapshot_migrates_as_stale_until_regenerated() { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("storage"); + let (subject_id, snapshot_id) = { + let manager = FeatureStorage::new(&root); + let workspace = manager.workspace("workspace-a").unwrap(); + let registration = workspace + .register(FeatureRegistration::new( + SUBJEKTIV_FEATURE_ID, + V3_MIGRATIONS, + )) + .unwrap(); + let store = SubjektivStore::open(&workspace, ®istration).unwrap(); + let subject_id = issued_id("subject"); + let timestamp = now(); + let subject_json = serde_json::to_string(&serde_json::json!({ + "schema_version": SUBJEKTIV_SCHEMA_VERSION, + "id": subject_id, + "role": role(), + "state": SubjectState::Active, + "store_revision": 0, + "created_at": timestamp, + "updated_at": timestamp, + })) + .unwrap(); + let snapshot = SurfaceSnapshot { + schema_version: SUBJEKTIV_SCHEMA_VERSION, + id: "legacy-surface".into(), + subject_id: subject_id.clone(), + body_md: "Legacy summary without generation-policy evidence.".into(), + memory_refs: Vec::new(), + built_from_store_revision: 0, + created_at: now(), + }; + store + .database + .try_transaction(|transaction| -> Result<()> { + transaction.execute( + "INSERT INTO subjects ( + subject_id, role, state, store_revision, + record_json, created_at, updated_at + ) VALUES (?1, ?2, 'active', 0, ?3, ?4, ?5)", + params![ + subject_id, + role().as_str(), + subject_json, + timestamp, + timestamp + ], + )?; + transaction.execute( + "INSERT INTO surface_snapshots ( + subject_id, snapshot_id, built_from_store_revision, + snapshot_json, created_at + ) VALUES (?1, ?2, ?3, ?4, ?5)", + params![ + subject_id, + snapshot.id, + to_i64(snapshot.built_from_store_revision)?, + serde_json::to_string(&snapshot)?, + snapshot.created_at + ], + )?; + transaction.execute( + "INSERT INTO surface_snapshot_seals (subject_id, snapshot_id) + VALUES (?1, ?2)", + params![subject_id, snapshot.id], + )?; + Ok(()) + }) + .unwrap(); + (subject_id, snapshot.id) + }; + + let (_manager, _workspace, store) = open_store(&root, "workspace-a"); + let resident = store.resident_surface(&subject_id).unwrap(); + assert_eq!(resident.availability, SurfaceAvailability::Stale); + assert!(resident.snapshot.is_none()); + assert!( + store + .surface_snapshot(&subject_id, &snapshot_id) + .unwrap() + .is_some(), + "migration retains the legacy snapshot as history without injecting it" + ); + + let generation = store.prepare_surface_generation(&subject_id).unwrap(); + let regenerated = store + .publish_surface_generation(&subject_id, &generation.id, Vec::new()) + .unwrap(); + assert_ne!(regenerated.id, snapshot_id); + let ready = store.resident_surface(&subject_id).unwrap(); + assert_eq!(ready.availability, SurfaceAvailability::Ready); + assert_eq!(ready.snapshot.unwrap().id, regenerated.id); + assert!( + store + .surface_snapshot(&subject_id, &snapshot_id) + .unwrap() + .is_some(), + "regeneration appends instead of mutating legacy history" + ); + let retried = store + .publish_surface_generation(&subject_id, &generation.id, Vec::new()) + .unwrap(); + assert_eq!(retried.id, regenerated.id); + } + + #[test] + fn attributed_staging_requires_lifecycle_record_and_retry_is_idempotent() { + let temp = tempfile::tempdir().unwrap(); + let (_manager, _workspace, store) = open_store(&temp.path().join("storage"), "workspace-a"); + let subject = store.create_subject(role()).unwrap(); + let first_attribution = attribution( + &subject.id, + "runtime-1", + "worker-1", + "session-1", + "2026-09-28T10:00:00.000Z", + ); + assert!(matches!( + store.stage_candidate_with_attribution( + candidate(&subject.id, "candidate-unattributed", "workspace-a"), + first_attribution.clone(), + ), + Err(SubjektivError::InvalidRecord(message)) + if message.contains("has not completed Host attribution") + )); + assert!(store.session_attribution("session-1").unwrap().is_none()); + assert!( + store + .staging_candidate(&subject.id, "candidate-unattributed") + .unwrap() + .is_none() + ); + store + .record_session_attribution(first_attribution.clone()) + .unwrap(); + let (first_candidate, first_attribution) = store + .stage_candidate_with_attribution( + candidate(&subject.id, "candidate-1", "workspace-a"), + first_attribution, + ) + .unwrap(); + + let mut retried_candidate = first_candidate.clone(); + retried_candidate.created_at = "2026-09-28T11:00:00.000Z".into(); + let retried_attribution = attribution( + &subject.id, + "runtime-1", + "worker-1", + "session-1", + "2026-09-28T11:00:00.000Z", + ); + let (retried_candidate, retried_attribution) = store + .stage_candidate_with_attribution(retried_candidate, retried_attribution) + .unwrap(); + assert_eq!(retried_candidate.created_at, first_candidate.created_at); + assert_eq!(retried_attribution, first_attribution); + + let mut conflicting_candidate = candidate(&subject.id, "candidate-1", "workspace-a"); + conflicting_candidate.claim = "different content".into(); + conflicting_candidate.source_refs[0].session_id = Some("session-2".into()); + let conflicting_attribution = attribution( + &subject.id, + "runtime-1", + "worker-1", + "session-2", + "2026-09-28T12:00:00.000Z", + ); + store + .record_session_attribution(conflicting_attribution.clone()) + .unwrap(); + let result = + store.stage_candidate_with_attribution(conflicting_candidate, conflicting_attribution); + assert!(matches!( + result, + Err(SubjektivError::CandidateConflict(candidate_id)) + if candidate_id == "candidate-1" + )); + assert!(store.session_attribution("session-2").unwrap().is_some()); + } + + #[test] + fn attribution_pages_are_bounded_descending_and_snapshot_fixed() { + let temp = tempfile::tempdir().unwrap(); + let (_manager, _workspace, store) = open_store(&temp.path().join("storage"), "workspace-a"); + let subject = store.create_subject(role()).unwrap(); + for (session, at) in [ + ("session-1", "2026-09-28T10:00:00.000Z"), + ("session-2", "2026-09-28T11:00:00.000Z"), + ("session-3", "2026-09-28T11:00:00.000Z"), + ] { + store + .record_session_attribution(attribution( + &subject.id, + "runtime-1", + "worker-1", + session, + at, + )) + .unwrap(); + } + + let first = store + .subject_session_attribution_page( + &subject.id, + None, + "2026-09-28T11:30:00.000Z", + None, + 2, + ) + .unwrap(); + assert_eq!( + first + .items + .iter() + .map(|item| item.session_id.as_str()) + .collect::>(), + vec!["session-2", "session-3"] + ); + assert!(first.has_more); + + store + .record_session_attribution(attribution( + &subject.id, + "runtime-2", + "worker-2", + "session-later", + "2026-09-28T12:00:00.000Z", + )) + .unwrap(); + let last = first.items.last().unwrap(); + let second = store + .subject_session_attribution_page( + &subject.id, + None, + "2026-09-28T11:30:00.000Z", + Some((&last.attributed_at, &last.session_id)), + 2, + ) + .unwrap(); + assert_eq!(second.items.len(), 1); + assert_eq!(second.items[0].session_id, "session-1"); + assert!(!second.has_more); + assert!( + second + .items + .iter() + .all(|item| item.session_id != "session-later") + ); + + let exact = store + .subject_session_attribution_page( + &subject.id, + Some("session-1"), + "2026-09-28T11:30:00.000Z", + None, + 20, + ) + .unwrap(); + assert_eq!(exact.items.len(), 1); + assert!(!exact.has_more); + } + + #[test] + fn worker_replacement_preserves_subject_history_and_rejects_cross_subject_session() { + let temp = tempfile::tempdir().unwrap(); + let (_manager, _workspace, store) = open_store(&temp.path().join("storage"), "workspace-a"); + let subject = store.create_subject(role()).unwrap(); + let other_subject = store.create_subject(role()).unwrap(); + for lifecycle_attribution in [ + attribution( + &subject.id, + "runtime-1", + "worker-1", + "session-1", + "2026-09-28T10:00:00.000Z", + ), + attribution( + &subject.id, + "runtime-2", + "worker-2", + "session-2", + "2026-09-28T11:00:00.000Z", + ), + ] { + store + .record_session_attribution(lifecycle_attribution) + .unwrap(); + } + + store + .stage_candidate_with_attribution( + candidate(&subject.id, "candidate-1", "workspace-a"), + attribution( + &subject.id, + "runtime-1", + "worker-1", + "session-1", + "2026-09-28T10:00:00.000Z", + ), + ) + .unwrap(); + let mut replacement_candidate = candidate(&subject.id, "candidate-2", "workspace-a"); + replacement_candidate.source_refs[0].session_id = None; + let (replacement_candidate, _) = store + .stage_candidate_with_attribution( + replacement_candidate, + attribution( + &subject.id, + "runtime-2", + "worker-2", + "session-2", + "2026-09-28T11:00:00.000Z", + ), + ) + .unwrap(); + assert_eq!( + replacement_candidate.source_refs[0].session_id.as_deref(), + Some("session-2") + ); + + let history = store.subject_session_attributions(&subject.id).unwrap(); + assert_eq!(history.len(), 2); + assert_eq!(history[0].worker_id, "worker-1"); + assert_eq!(history[1].worker_id, "worker-2"); + assert!(history.iter().all(|entry| entry.subject_id == subject.id)); + + let update = store + .database + .try_with_connection::<_, SubjektivError>(|connection| { + connection.execute( + "UPDATE subject_session_attributions SET worker_id = 'worker-other' + WHERE session_id = 'session-1'", + [], + )?; + Ok(()) + }); + assert!(update.is_err()); + let delete = store + .database + .try_with_connection::<_, SubjektivError>(|connection| { + connection.execute( + "DELETE FROM subject_session_attributions WHERE session_id = 'session-1'", + [], + )?; + Ok(()) + }); + assert!(delete.is_err()); + + let mut foreign_candidate = + candidate(&other_subject.id, "candidate-foreign", "workspace-a"); + foreign_candidate.source_refs[0].session_id = Some("session-1".into()); + let result = store.stage_candidate_with_attribution( + foreign_candidate, + attribution( + &other_subject.id, + "runtime-1", + "worker-1", + "session-1", + "2026-09-28T12:00:00.000Z", + ), + ); + assert!(matches!( + result, + Err(SubjektivError::SessionSubjectConflict { + session_id, + existing_subject_id, + requested_subject_id, + }) if session_id == "session-1" + && existing_subject_id == subject.id + && requested_subject_id == other_subject.id + )); + assert!( + store + .staging_candidate(&other_subject.id, "candidate-foreign") + .unwrap() + .is_none() + ); + } + + #[test] + fn malformed_nested_provenance_is_rejected_without_staging() { + let temp = tempfile::tempdir().unwrap(); + let (_manager, _workspace, store) = open_store(&temp.path().join("storage"), "workspace-a"); + let subject = store.create_subject(role()).unwrap(); + + let mut non_human_preference = + candidate(&subject.id, "candidate-preference", "workspace-a"); + non_human_preference.kind = CandidateKind::Preference; + non_human_preference.evidence[0] + .origin + .as_mut() + .unwrap() + .kind = EvidenceOriginKind::ModelOutput; + assert!(matches!( + store.stage_candidate(non_human_preference), + Err(SubjektivError::InvalidRecord(message)) + if message.contains("HumanInput") + )); + + let mut reversed = candidate(&subject.id, "candidate-reversed", "workspace-a"); + reversed.evidence[0].entry_range = Some([12, 10]); + assert!(matches!( + store.stage_candidate(reversed), + Err(SubjektivError::InvalidRecord(_)) + )); + + let mut duplicate = candidate(&subject.id, "candidate-duplicate", "workspace-a"); + duplicate.evidence.push(duplicate.evidence[0].clone()); + assert!(matches!( + store.stage_candidate(duplicate), + Err(SubjektivError::InvalidRecord(_)) + )); + + let mut dangling = candidate(&subject.id, "candidate-dangling", "workspace-a"); + dangling.source_refs[0].evidence_id = Some("missing-evidence".into()); + assert!(matches!( + store.stage_candidate(dangling), + Err(SubjektivError::InvalidRecord(_)) + )); + assert!( + store + .staging_candidate(&subject.id, "candidate-dangling") + .unwrap() + .is_none() + ); + } + + #[test] + fn revision_proposal_roundtrips_through_staging_and_resolution() { + let temp = tempfile::tempdir().unwrap(); + let (_manager, _workspace, store) = open_store(&temp.path().join("storage"), "workspace-a"); + let subject = store.create_subject(role()).unwrap(); + let memory = store + .create_memory(&subject.id, draft("Existing memory", "initial observation")) + .unwrap(); + let proposal = proposal( + RevisionProposalIntent::Revise, + &memory.id, + 1, + "Refine the wording from new evidence", + ); + let staged = store + .stage_candidate( + candidate(&subject.id, "candidate-proposed", "workspace-a") + .with_revision_proposal(proposal.clone()) + .unwrap(), + ) + .unwrap(); + assert_eq!(staged.revision_proposal, Some(proposal.clone())); + assert_eq!( + store + .staging_candidate(&subject.id, &staged.id) + .unwrap() + .unwrap() + .revision_proposal, + Some(proposal.clone()) + ); + + let resolution = store + .resolve_candidate( + &subject.id, + &staged.id, + StagingResolutionAction::Discarded, + "The current wording remains preferable", + vec![], + ) + .unwrap(); + assert_eq!( + resolution.candidate.revision_proposal, + Some(proposal.clone()) + ); + assert_eq!( + store + .staging_resolution(&subject.id, &staged.id) + .unwrap() + .unwrap() + .candidate + .revision_proposal, + Some(proposal) + ); + } + + #[test] + fn atomic_candidate_decision_is_idempotent_and_preserves_proposal_metadata() { + let temp = tempfile::tempdir().unwrap(); + let (_manager, _workspace, store) = open_store(&temp.path().join("storage"), "workspace-a"); + let subject = store.create_subject(role()).unwrap(); + let memory = store + .create_memory(&subject.id, draft("Original", "initial observation")) + .unwrap(); + let change_reason = "The remembered condition has ended"; + let staged = store + .stage_candidate( + candidate(&subject.id, "candidate-resolve-atomic", "workspace-a") + .with_revision_proposal(proposal( + RevisionProposalIntent::Resolve, + &memory.id, + memory.revision, + change_reason, + )) + .unwrap(), + ) + .unwrap(); + let mut revised = draft("Resolved", change_reason); + revised.state = MemoryState::Resolved; + let request = CandidateDecisionRequest { + request_id: "decision-resolve-1".into(), + candidate_id: staged.id.clone(), + reason: "Accepted exact resolution proposal".into(), + decision: CandidateDecision::Apply { + target: MemoryRevisionTarget::Revise { + memory_id: memory.id.clone(), + expected_revision: memory.revision, + }, + draft: revised, + }, + }; + + let first = store + .decide_candidate(&subject.id, request.clone()) + .unwrap(); + let retried = store.decide_candidate(&subject.id, request).unwrap(); + + assert_eq!(first.memory.as_ref().unwrap().revision, 2); + assert_eq!(retried.memory.as_ref().unwrap().revision, 2); + assert_eq!(first.resolution.id, retried.resolution.id); + assert_eq!(first.store_revision, 2); + assert!(first.surface_dirty); + assert_eq!( + first + .resolution + .candidate + .revision_proposal + .unwrap() + .change_reason, + change_reason + ); + assert_eq!( + store + .list_memory_revisions(&subject.id, &memory.id) + .unwrap() + .len(), + 2 + ); + } + + #[test] + fn candidate_decision_revalidates_stale_proposals_and_intent_transitions() { + let temp = tempfile::tempdir().unwrap(); + let (_manager, _workspace, store) = open_store(&temp.path().join("storage"), "workspace-a"); + let subject = store.create_subject(role()).unwrap(); + let memory = store + .create_memory(&subject.id, draft("Lifecycle", "initial")) + .unwrap(); + let stale = store + .stage_candidate( + candidate(&subject.id, "candidate-stale-apply", "workspace-a") + .with_revision_proposal(proposal( + RevisionProposalIntent::Revise, + &memory.id, + 1, + "stale correction", + )) + .unwrap(), + ) + .unwrap(); + store + .revise_memory(&subject.id, &memory.id, 1, draft("Advanced", "advance")) + .unwrap(); + let stale_result = store.decide_candidate( + &subject.id, + CandidateDecisionRequest { + request_id: "decision-stale".into(), + candidate_id: stale.id.clone(), + reason: "must not auto-rebase".into(), + decision: CandidateDecision::Apply { + target: MemoryRevisionTarget::Revise { + memory_id: memory.id.clone(), + expected_revision: 1, + }, + draft: draft("Stale", "stale correction"), + }, + }, + ); + assert!(matches!( + stale_result, + Err(SubjektivError::RevisionConflict { + expected: 1, + actual: 2, + .. + }) + )); + assert!( + store + .staging_resolution(&subject.id, &stale.id) + .unwrap() + .is_none() + ); + + let apply_intent = |store: &SubjektivStore, + candidate_id: &str, + intent: RevisionProposalIntent, + expected_revision: u64, + state: MemoryState, + reason: &str| { + let staged = store + .stage_candidate( + candidate(&subject.id, candidate_id, "workspace-a") + .with_revision_proposal(proposal( + intent, + &memory.id, + expected_revision, + reason, + )) + .unwrap(), + ) + .unwrap(); + let mut next = draft(candidate_id, reason); + next.state = state; + store + .decide_candidate( + &subject.id, + CandidateDecisionRequest { + request_id: format!("decision-{candidate_id}"), + candidate_id: staged.id, + reason: format!("apply {candidate_id}"), + decision: CandidateDecision::Apply { + target: MemoryRevisionTarget::Revise { + memory_id: memory.id.clone(), + expected_revision, + }, + draft: next, + }, + }, + ) + .unwrap() + .memory + .unwrap() + }; + let resolved = apply_intent( + &store, + "resolve", + RevisionProposalIntent::Resolve, + 2, + MemoryState::Resolved, + "resolve reason", + ); + let reopened = apply_intent( + &store, + "reopen", + RevisionProposalIntent::Reopen, + resolved.revision, + MemoryState::Active, + "reopen reason", + ); + let retracted = apply_intent( + &store, + "retract", + RevisionProposalIntent::Retract, + reopened.revision, + MemoryState::Retracted, + "retract reason", + ); + assert_eq!(retracted.state, MemoryState::Retracted); + assert!(matches!( + store.stage_candidate( + candidate(&subject.id, "post-retract", "workspace-a") + .with_revision_proposal(proposal( + RevisionProposalIntent::Reopen, + &memory.id, + retracted.revision, + "cannot revive", + )) + .unwrap() + ), + Err(SubjektivError::InvalidStateTransition { .. }) + )); + } + + #[test] + fn candidate_decisions_reject_conflicts_and_roll_back_partial_writes() { + let temp = tempfile::tempdir().unwrap(); + let (_manager, _workspace, store) = open_store(&temp.path().join("storage"), "workspace-a"); + let subject = store.create_subject(role()).unwrap(); + let other = store.create_subject(role()).unwrap(); + let staged = store + .stage_candidate(candidate(&subject.id, "candidate-rollback", "workspace-a")) + .unwrap(); + let foreign = store + .stage_candidate(candidate(&other.id, "candidate-foreign", "workspace-a")) + .unwrap(); + let mut invalid = draft("Would be written", "must roll back"); + invalid.derived_from.push(MemoryRevisionRef { + memory_id: "missing-memory".into(), + revision: 1, + }); + let failed = store.decide_candidate( + &subject.id, + CandidateDecisionRequest { + request_id: "decision-rollback".into(), + candidate_id: staged.id.clone(), + reason: "invalid derivation".into(), + decision: CandidateDecision::Apply { + target: MemoryRevisionTarget::Create, + draft: invalid, + }, + }, + ); + assert!(matches!( + failed, + Err(SubjektivError::SubjectScopeMismatch { .. }) + )); + assert!( + store + .staging_resolution(&subject.id, &staged.id) + .unwrap() + .is_none() + ); + assert!(store.list_memories(&subject.id).unwrap().is_empty()); + + let cross_subject = store.decide_candidate( + &subject.id, + CandidateDecisionRequest { + request_id: "decision-foreign".into(), + candidate_id: foreign.id, + reason: "must remain scoped".into(), + decision: CandidateDecision::Close { + action: StagingResolutionAction::Invalid, + affected_memory: vec![], + }, + }, + ); + assert!(matches!( + cross_subject, + Err(SubjektivError::SubjectScopeMismatch { .. }) + )); + + let closed = store + .decide_candidate( + &subject.id, + CandidateDecisionRequest { + request_id: "decision-close".into(), + candidate_id: staged.id.clone(), + reason: "already represented elsewhere".into(), + decision: CandidateDecision::Close { + action: StagingResolutionAction::AlreadyCovered, + affected_memory: vec![], + }, + }, + ) + .unwrap(); + assert!(!closed.surface_dirty); + assert!(closed.memory.is_none()); + for (suffix, action) in [ + ("discarded", StagingResolutionAction::Discarded), + ("invalid", StagingResolutionAction::Invalid), + ("duplicate", StagingResolutionAction::Duplicate), + ] { + let candidate_id = format!("candidate-{suffix}"); + store + .stage_candidate(candidate(&subject.id, &candidate_id, "workspace-a")) + .unwrap(); + let receipt = store + .decide_candidate( + &subject.id, + CandidateDecisionRequest { + request_id: format!("decision-{suffix}"), + candidate_id, + reason: format!("specific {suffix} reason"), + decision: CandidateDecision::Close { + action, + affected_memory: vec![], + }, + }, + ) + .unwrap(); + assert_eq!(receipt.resolution.action, action); + assert!(!receipt.surface_dirty); + } + let conflicting_retry = store.decide_candidate( + &subject.id, + CandidateDecisionRequest { + request_id: "decision-close".into(), + candidate_id: staged.id, + reason: "different disposition".into(), + decision: CandidateDecision::Close { + action: StagingResolutionAction::Discarded, + affected_memory: vec![], + }, + }, + ); + assert!(matches!( + conflicting_retry, + Err(SubjektivError::DecisionRequestConflict(_)) + )); + } + + #[test] + fn revision_proposals_enforce_scope_revision_and_state_transitions() { + let temp = tempfile::tempdir().unwrap(); + let (_manager, _workspace, store) = open_store(&temp.path().join("storage"), "workspace-a"); + let subject = store.create_subject(role()).unwrap(); + let other_subject = store.create_subject(role()).unwrap(); + let memory = store + .create_memory( + &subject.id, + draft("Transition target", "initial observation"), + ) + .unwrap(); + + let foreign = candidate(&other_subject.id, "candidate-foreign-target", "workspace-a") + .with_revision_proposal(proposal( + RevisionProposalIntent::Revise, + &memory.id, + 1, + "Must remain subject scoped", + )) + .unwrap(); + assert!(matches!( + store.stage_candidate(foreign), + Err(SubjektivError::SubjectScopeMismatch { + subject_id, + reference, + }) if subject_id == other_subject.id && reference == memory.id + )); + + let stale = candidate(&subject.id, "candidate-stale", "workspace-a") + .with_revision_proposal(proposal( + RevisionProposalIntent::Resolve, + &memory.id, + 2, + "Resolve the active memory", + )) + .unwrap(); + assert!(matches!( + store.stage_candidate(stale), + Err(SubjektivError::RevisionConflict { + memory_id, + expected: 2, + actual: 1, + }) if memory_id == memory.id + )); + + let resolve_proposal = candidate(&subject.id, "candidate-resolve", "workspace-a") + .with_revision_proposal(proposal( + RevisionProposalIntent::Resolve, + &memory.id, + 1, + "The active condition has ended", + )) + .unwrap(); + assert!(store.stage_candidate(resolve_proposal).is_ok()); + let invalid_reopen = candidate(&subject.id, "candidate-invalid-reopen", "workspace-a") + .with_revision_proposal(proposal( + RevisionProposalIntent::Reopen, + &memory.id, + 1, + "An active Memory cannot be reopened", + )) + .unwrap(); + assert!(matches!( + store.stage_candidate(invalid_reopen), + Err(SubjektivError::InvalidStateTransition { + from: MemoryState::Active, + to: MemoryState::Active, + .. + }) + )); + + let mut resolve = draft("Resolved target", "the condition ended"); + resolve.state = MemoryState::Resolved; + let resolved = store + .revise_memory(&subject.id, &memory.id, 1, resolve) + .unwrap(); + let mut attributed_stale = + candidate(&subject.id, "candidate-attributed-stale", "workspace-a"); + attributed_stale.source_refs[0].session_id = None; + attributed_stale + .attach_revision_proposal(proposal( + RevisionProposalIntent::Revise, + &memory.id, + 1, + "This attribution must roll back with the stale proposal", + )) + .unwrap(); + let stale_attribution = attribution( + &subject.id, + "runtime-proposal", + "worker-proposal", + "session-proposal-stale", + "2026-09-28T12:00:00.000Z", + ); + store + .record_session_attribution(stale_attribution.clone()) + .unwrap(); + assert!(matches!( + store.stage_candidate_with_attribution(attributed_stale, stale_attribution,), + Err(SubjektivError::RevisionConflict { + expected: 1, + actual: 2, + .. + }) + )); + assert!( + store + .session_attribution("session-proposal-stale") + .unwrap() + .is_some() + ); + let reopen = candidate(&subject.id, "candidate-reopen", "workspace-a") + .with_revision_proposal(proposal( + RevisionProposalIntent::Reopen, + &memory.id, + resolved.revision, + "The condition applies again", + )) + .unwrap(); + assert_eq!( + store + .stage_candidate(reopen) + .unwrap() + .revision_proposal + .unwrap() + .intent, + RevisionProposalIntent::Reopen + ); + let retract_proposal = candidate(&subject.id, "candidate-retract", "workspace-a") + .with_revision_proposal(proposal( + RevisionProposalIntent::Retract, + &memory.id, + resolved.revision, + "The supporting evidence was invalidated", + )) + .unwrap(); + assert!(store.stage_candidate(retract_proposal).is_ok()); + + let mut retract = draft("Retracted target", "the evidence was invalidated"); + retract.state = MemoryState::Retracted; + let retracted = store + .revise_memory(&subject.id, &memory.id, resolved.revision, retract) + .unwrap(); + let terminal = candidate(&subject.id, "candidate-after-retract", "workspace-a") + .with_revision_proposal(proposal( + RevisionProposalIntent::Revise, + &memory.id, + retracted.revision, + "Retraction must remain terminal", + )) + .unwrap(); + assert!(matches!( + store.stage_candidate(terminal), + Err(SubjektivError::InvalidStateTransition { + memory_id, + from: MemoryState::Retracted, + to: MemoryState::Retracted, + }) if memory_id == memory.id + )); + } + + #[test] + fn exact_proposal_retry_remains_idempotent_after_target_advances() { + let temp = tempfile::tempdir().unwrap(); + let (_manager, _workspace, store) = open_store(&temp.path().join("storage"), "workspace-a"); + let subject = store.create_subject(role()).unwrap(); + let memory = store + .create_memory(&subject.id, draft("Retry target", "initial observation")) + .unwrap(); + let staged = store + .stage_candidate( + candidate(&subject.id, "candidate-retry-proposal", "workspace-a") + .with_revision_proposal(proposal( + RevisionProposalIntent::Revise, + &memory.id, + 1, + "Refine the current revision", + )) + .unwrap(), + ) + .unwrap(); + store + .revise_memory( + &subject.id, + &memory.id, + 1, + draft("Advanced target", "another accepted refinement"), + ) + .unwrap(); + + let mut retry = staged.clone(); + retry.created_at = "2026-09-28T11:00:00.000Z".into(); + let retried = store.stage_candidate(retry).unwrap(); + assert_eq!(retried.created_at, staged.created_at); + assert_eq!(retried.revision_proposal, staged.revision_proposal); + } + + #[test] + fn bounded_memory_reads_are_scoped_ordered_and_immutable() { + let temp = tempfile::tempdir().unwrap(); + let (_manager, _workspace, store) = open_store(&temp.path().join("storage"), "workspace-a"); + let subject = store.create_subject(role()).unwrap(); + let other_subject = store.create_subject(role()).unwrap(); + assert!(matches!( + store.list_memories("subject-missing"), + Err(SubjektivError::SubjectNotFound(subject_id)) if subject_id == "subject-missing" + )); + + let mut first_draft = draft("First memory", "first reason"); + first_draft.body_md = "Immutable first body".into(); + let first = store.create_memory(&subject.id, first_draft).unwrap(); + thread::sleep(Duration::from_millis(2)); + let second = store + .create_memory(&subject.id, draft("Second memory", "second reason")) + .unwrap(); + let memories = store.list_memories(&subject.id).unwrap(); + assert_eq!( + memories.iter().map(|record| &record.id).collect::>(), + vec![&second.id, &first.id] + ); + + thread::sleep(Duration::from_millis(2)); + let mut revision = draft("First memory revised", "second revision reason"); + revision.body_md = "New second body".into(); + let revised = store + .revise_memory(&subject.id, &first.id, 1, revision) + .unwrap(); + let memories = store.list_memories(&subject.id).unwrap(); + assert_eq!(memories[0].id, first.id); + assert_eq!(memories[0].revision, 2); + + let history = store.list_memory_revisions(&subject.id, &first.id).unwrap(); + assert_eq!( + history + .iter() + .map(|record| record.revision) + .collect::>(), + vec![2, 1] + ); + assert_eq!(history[0].body_md, "New second body"); + assert_eq!(history[0].change_reason, "second revision reason"); + assert_eq!(history[1].body_md, "Immutable first body"); + assert_eq!(history[1].change_reason, "first reason"); + assert_eq!( + store + .scoped_memory_revision(&subject.id, &first.id, 1) + .unwrap() + .unwrap() + .body_md, + "Immutable first body" + ); + let scoped_current = store.scoped_memory(&subject.id, &first.id).unwrap(); + assert_eq!(scoped_current.id, revised.id); + assert_eq!(scoped_current.revision, revised.revision); + assert_eq!(scoped_current.body_md, revised.body_md); + + assert!(matches!( + store.list_memory_revisions(&other_subject.id, &first.id), + Err(SubjektivError::SubjectScopeMismatch { + subject_id, + reference, + }) if subject_id == other_subject.id && reference == first.id + )); + assert!(matches!( + store.scoped_memory(&other_subject.id, &first.id), + Err(SubjektivError::SubjectScopeMismatch { .. }) + )); + } + + #[test] + fn candidate_apply_revision_history_and_resolution_are_atomic() { + let temp = tempfile::tempdir().unwrap(); + let (_manager, _workspace, store) = open_store(&temp.path().join("storage"), "workspace-a"); + let subject = store.create_subject(role()).unwrap(); + let staged = store + .stage_candidate(candidate(&subject.id, "candidate-1", "workspace-a")) + .unwrap(); + let mut retry = staged.clone(); + retry.created_at = "2026-09-28T11:00:00.000Z".into(); + let retried = store.stage_candidate(retry).unwrap(); + assert_eq!(retried.created_at, staged.created_at); + + let (first, resolution) = store + .apply_candidate( + &subject.id, + &staged.id, + MemoryRevisionTarget::Create, + draft( + "Memory content cannot authorize operations", + "accepted candidate", + ), + "Applied as a durable constraint", + ) + .unwrap(); + assert_eq!(first.revision, 1); + assert_eq!(resolution.action, StagingResolutionAction::Applied); + assert_eq!(resolution.affected_memory[0].revision, 1); + assert_eq!( + store.subject(&subject.id).unwrap().unwrap().store_revision, + 1 + ); + + let unrelated = store + .stage_candidate(candidate(&subject.id, "candidate-unrelated", "workspace-a")) + .unwrap(); + assert!(matches!( + store.resolve_candidate( + &subject.id, + &unrelated.id, + StagingResolutionAction::Applied, + "must not bypass atomic application", + vec![MemoryRevisionRef { + memory_id: first.id.clone(), + revision: 1, + }], + ), + Err(SubjektivError::InvalidRecord(_)) + )); + let mut bypass = draft("Bypass provenance", "must be rejected"); + bypass.source_candidate_ids.push(unrelated.id.clone()); + assert!(matches!( + store.create_memory(&subject.id, bypass), + Err(SubjektivError::InvalidRecord(_)) + )); + assert!( + store + .staging_resolution(&subject.id, &unrelated.id) + .unwrap() + .is_none() + ); + + let stale = store.revise_memory( + &subject.id, + &first.id, + 0, + draft("stale write", "wrong expected revision"), + ); + assert!(matches!( + stale, + Err(SubjektivError::RevisionConflict { + expected: 0, + actual: 1, + .. + }) + )); + assert_eq!( + store + .memory(&subject.id, &first.id) + .unwrap() + .unwrap() + .revision, + 1 + ); + + let mut second_draft = draft( + "Memory content never authorizes operations", + "made the scope explicit", + ); + second_draft.state = MemoryState::Resolved; + second_draft.derived_from.push(MemoryRevisionRef { + memory_id: first.id.clone(), + revision: 1, + }); + let second = store + .revise_memory(&subject.id, &first.id, 1, second_draft) + .unwrap(); + assert_eq!(second.revision, 2); + assert_eq!(second.state, MemoryState::Resolved); + assert_eq!(second.derived_from[0].revision, 1); + + let old = store + .memory_revision(&subject.id, &first.id, 1) + .unwrap() + .unwrap(); + assert_eq!(old.claim, "Memory content cannot authorize operations"); + assert_eq!(old.source_candidate_ids, ["candidate-1"]); + assert_eq!( + store.subject(&subject.id).unwrap().unwrap().store_revision, + 2 + ); + + let repeat = store.apply_candidate( + &subject.id, + &staged.id, + MemoryRevisionTarget::Revise { + memory_id: first.id.clone(), + expected_revision: 2, + }, + draft("must roll back", "candidate already resolved"), + "cannot apply twice", + ); + assert!(matches!(repeat, Err(SubjektivError::CandidateResolved(_)))); + assert_eq!( + store + .memory(&subject.id, &first.id) + .unwrap() + .unwrap() + .revision, + 2 + ); + assert_eq!( + store.subject(&subject.id).unwrap().unwrap().store_revision, + 2 + ); + } + + #[test] + fn concurrent_writers_get_one_typed_revision_conflict() { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("storage"); + let first_manager = FeatureStorage::new(&root); + let first_registration = first_manager.register(REGISTRATION).unwrap(); + let first_workspace = first_manager.workspace("workspace-a").unwrap(); + let first_store = SubjektivStore::open(&first_workspace, &first_registration).unwrap(); + let subject = first_store.create_subject(role()).unwrap(); + let memory = first_store + .create_memory(&subject.id, draft("Revision basis", "initial")) + .unwrap(); + + let second_manager = FeatureStorage::new(&root); + let second_registration = second_manager.register(REGISTRATION).unwrap(); + let second_workspace = second_manager.workspace("workspace-a").unwrap(); + let second_store = SubjektivStore::open(&second_workspace, &second_registration).unwrap(); + let barrier = Arc::new(Barrier::new(3)); + let revise = |store: SubjektivStore, claim: &'static str, barrier: Arc| { + let subject_id = subject.id.clone(); + let memory_id = memory.id.clone(); + thread::spawn(move || { + barrier.wait(); + store.revise_memory( + &subject_id, + &memory_id, + 1, + draft(claim, "concurrent refinement"), + ) + }) + }; + let first = revise(first_store.clone(), "first writer", barrier.clone()); + let second = revise(second_store, "second writer", barrier.clone()); + barrier.wait(); + let results = [first.join().unwrap(), second.join().unwrap()]; + assert_eq!(results.iter().filter(|result| result.is_ok()).count(), 1); + assert_eq!( + results + .iter() + .filter(|result| matches!(result, Err(SubjektivError::RevisionConflict { .. }))) + .count(), + 1 + ); + assert_eq!( + first_store + .subject(&subject.id) + .unwrap() + .unwrap() + .store_revision, + 2 + ); + } + + #[test] + fn scope_and_fixed_revision_references_are_enforced() { + let temp = tempfile::tempdir().unwrap(); + let manager = FeatureStorage::new(temp.path().join("storage")); + let registration = manager.register(REGISTRATION).unwrap(); + let workspace_a = manager.workspace("workspace-a").unwrap(); + let workspace_b = manager.workspace("workspace-b").unwrap(); + let store_a = SubjektivStore::open(&workspace_a, ®istration).unwrap(); + let store_b = SubjektivStore::open(&workspace_b, ®istration).unwrap(); + let subject_a = store_a.create_subject(role()).unwrap(); + let other_subject_a = store_a.create_subject(role()).unwrap(); + let subject_b = store_b.create_subject(role()).unwrap(); + let memory_a = store_a + .create_memory( + &subject_a.id, + draft("Workspace A experience", "direct host record"), + ) + .unwrap(); + + assert!(matches!( + store_b.create_memory(&subject_a.id, draft("foreign subject", "must be rejected")), + Err(SubjektivError::SubjectNotFound(_)) + )); + let mut foreign_derived = draft("Workspace B experience", "invalid derivation"); + foreign_derived.derived_from.push(MemoryRevisionRef { + memory_id: memory_a.id.clone(), + revision: 1, + }); + assert!(matches!( + store_b.create_memory(&subject_b.id, foreign_derived), + Err(SubjektivError::SubjectScopeMismatch { .. }) + )); + assert_eq!( + store_b + .subject(&subject_b.id) + .unwrap() + .unwrap() + .store_revision, + 0 + ); + + let mut other_subject_derived = draft("Same Workspace, other subject", "invalid scope"); + other_subject_derived.derived_from.push(MemoryRevisionRef { + memory_id: memory_a.id.clone(), + revision: 1, + }); + assert!(matches!( + store_a.create_memory(&other_subject_a.id, other_subject_derived), + Err(SubjektivError::SubjectScopeMismatch { .. }) + )); + let mut wrong_revision = draft("Missing fixed revision", "invalid revision"); + wrong_revision.derived_from.push(MemoryRevisionRef { + memory_id: memory_a.id.clone(), + revision: 2, + }); + assert!(matches!( + store_a.create_memory(&subject_a.id, wrong_revision), + Err(SubjektivError::SubjectScopeMismatch { .. }) + )); + assert_eq!( + store_a + .subject(&other_subject_a.id) + .unwrap() + .unwrap() + .store_revision, + 0 + ); + + let foreign_origin = candidate(&subject_a.id, "candidate-foreign", "workspace-b"); + assert!(matches!( + store_a.stage_candidate(foreign_origin), + Err(SubjektivError::SubjectScopeMismatch { .. }) + )); + } + + #[test] + fn memory_states_snapshots_and_retirement_have_explicit_boundaries() { + let temp = tempfile::tempdir().unwrap(); + let (_manager, _workspace, store) = open_store(&temp.path().join("storage"), "workspace-a"); + let subject = store.create_subject(role()).unwrap(); + let first = store + .create_memory(&subject.id, draft("A constraint", "initial observation")) + .unwrap(); + let snapshot = store + .create_surface_snapshot( + &subject.id, + "Bounded resident summary", + vec![MemoryRevisionRef { + memory_id: first.id.clone(), + revision: 1, + }], + 1, + ) + .unwrap(); + assert_eq!( + store + .surface_snapshot(&subject.id, &snapshot.id) + .unwrap() + .unwrap() + .memory_refs[0] + .revision, + 1 + ); + assert!( + store + .create_surface_snapshot(&subject.id, "stale", vec![], 0) + .is_err() + ); + + let mut retract = draft("Retracted constraint", "evidence invalidated it"); + retract.state = MemoryState::Retracted; + let retracted = store + .revise_memory(&subject.id, &first.id, 1, retract) + .unwrap(); + assert_eq!(retracted.state, MemoryState::Retracted); + assert!(matches!( + store.revise_memory( + &subject.id, + &first.id, + 2, + draft("cannot reactivate", "retraction is terminal") + ), + Err(SubjektivError::InvalidStateTransition { .. }) + )); + let mut retract_again = draft("still retracted", "no post-retraction revisions"); + retract_again.state = MemoryState::Retracted; + assert!(matches!( + store.revise_memory(&subject.id, &first.id, 2, retract_again), + Err(SubjektivError::InvalidStateTransition { .. }) + )); + + let retired = store.retire_subject(&subject.id).unwrap(); + assert_eq!(retired.state, SubjectState::Retired); + assert!(matches!( + store.create_memory(&subject.id, draft("after retirement", "invalid")), + Err(SubjektivError::SubjectRetired(_)) + )); + assert!(store.memory(&subject.id, &first.id).unwrap().is_some()); + } + + #[test] + fn history_rows_reject_update_and_delete() { + let temp = tempfile::tempdir().unwrap(); + let (_manager, _workspace, store) = open_store(&temp.path().join("storage"), "workspace-a"); + let subject = store.create_subject(role()).unwrap(); + let staged = store + .stage_candidate(candidate(&subject.id, "candidate-1", "workspace-a")) + .unwrap(); + let (memory, _) = store + .apply_candidate( + &subject.id, + &staged.id, + MemoryRevisionTarget::Create, + draft("Immutable history", "accept"), + "applied", + ) + .unwrap(); + let extra_candidate = store + .stage_candidate(candidate(&subject.id, "candidate-extra", "workspace-a")) + .unwrap(); + let other_memory = store + .create_memory(&subject.id, draft("Other memory", "snapshot seal target")) + .unwrap(); + let snapshot = store + .create_surface_snapshot( + &subject.id, + "sealed snapshot", + vec![MemoryRevisionRef { + memory_id: memory.id.clone(), + revision: 1, + }], + 2, + ) + .unwrap(); + + let update = store + .database + .try_with_connection::<_, SubjektivError>(|connection| { + connection.execute( + "UPDATE memory_revisions SET record_json = '{}' \ + WHERE subject_id = ?1 AND memory_id = ?2 AND revision = 1", + params![subject.id, memory.id], + )?; + Ok(()) + }); + assert!(update.is_err()); + let delete = store + .database + .try_with_connection::<_, SubjektivError>(|connection| { + connection.execute( + "DELETE FROM staging_records WHERE subject_id = ?1 AND candidate_id = ?2", + params![subject.id, staged.id], + )?; + Ok(()) + }); + assert!(delete.is_err()); + + let late_evidence = store + .database + .try_with_connection::<_, SubjektivError>(|connection| { + connection.execute( + "INSERT INTO memory_revision_candidates ( + subject_id, memory_id, revision, candidate_id + ) VALUES (?1, ?2, 1, ?3)", + params![subject.id, memory.id, extra_candidate.id], + )?; + Ok(()) + }); + assert!(late_evidence.is_err()); + let late_derivation = + store + .database + .try_with_connection::<_, SubjektivError>(|connection| { + connection.execute( + "INSERT INTO memory_revision_derivations ( + subject_id, memory_id, revision, source_memory_id, source_revision + ) VALUES (?1, ?2, 1, ?3, 1)", + params![subject.id, memory.id, other_memory.id], + )?; + Ok(()) + }); + assert!(late_derivation.is_err()); + let late_resolution_target = + store + .database + .try_with_connection::<_, SubjektivError>(|connection| { + connection.execute( + "INSERT INTO staging_resolution_targets ( + subject_id, candidate_id, memory_id, revision + ) VALUES (?1, ?2, ?3, 1)", + params![subject.id, staged.id, other_memory.id], + )?; + Ok(()) + }); + assert!(late_resolution_target.is_err()); + let late_snapshot_ref = + store + .database + .try_with_connection::<_, SubjektivError>(|connection| { + connection.execute( + "INSERT INTO surface_snapshot_refs ( + subject_id, snapshot_id, memory_id, revision + ) VALUES (?1, ?2, ?3, 1)", + params![subject.id, snapshot.id, other_memory.id], + )?; + Ok(()) + }); + assert!(late_snapshot_ref.is_err()); + + assert!( + store + .memory_revision(&subject.id, &memory.id, 1) + .unwrap() + .is_some() + ); + assert!( + store + .staging_candidate(&subject.id, &staged.id) + .unwrap() + .is_some() + ); + } + + #[test] + fn surface_material_selection_is_scoped_active_bounded_and_deterministic() { + let temp = tempfile::tempdir().unwrap(); + let (_manager, _workspace, store) = open_store(temp.path(), "workspace-a"); + let subject = store.create_subject(role()).unwrap(); + let other = store.create_subject(role()).unwrap(); + store + .create_memory(&other.id, draft("foreign", "foreign subject")) + .unwrap(); + + let kinds = [ + CandidateKind::Preference, + CandidateKind::Constraint, + CandidateKind::Decision, + CandidateKind::WorkingAssumption, + CandidateKind::OpenQuestion, + CandidateKind::Lesson, + ]; + let mut first_id = None; + for kind in &kinds { + for index in 0..9 { + let mut value = draft(&format!("{kind:?}-{index}"), "selection fixture"); + value.kind = kind.clone(); + let memory = store.create_memory(&subject.id, value).unwrap(); + first_id.get_or_insert(memory.id); + } + } + let resolved_id = first_id.unwrap(); + let current = store.memory(&subject.id, &resolved_id).unwrap().unwrap(); + let mut resolved = draft("resolved", "no longer active"); + resolved.kind = current.kind; + resolved.state = MemoryState::Resolved; + store + .revise_memory(&subject.id, &resolved_id, current.revision, resolved) + .unwrap(); + + let first = store.prepare_surface_generation(&subject.id).unwrap(); + let second = store.prepare_surface_generation(&subject.id).unwrap(); + assert_eq!(first.materials, second.materials); + assert_eq!(first.active_memory_count, 53); + assert_eq!(first.materials.len(), SURFACE_TOTAL_MATERIAL_LIMIT); + assert!( + first + .materials + .iter() + .all(|item| item.memory_id != resolved_id) + ); + assert!(first.materials.iter().all(|item| item.body_md != "foreign")); + for kind in &kinds { + assert!( + first + .materials + .iter() + .filter(|item| &item.kind == kind) + .count() + <= SURFACE_PER_KIND_LIMIT + ); + } + let serialized = serde_json::to_vec(&first.materials).unwrap(); + assert!(estimated_tokens_for_bytes(serialized.len()) <= SURFACE_MATERIAL_TOKEN_BUDGET); + } + + #[test] + fn oversized_kind_cannot_starve_other_surface_categories() { + let temp = tempfile::tempdir().unwrap(); + let (_manager, _workspace, store) = open_store(temp.path(), "workspace-a"); + let subject = store.create_subject(role()).unwrap(); + + let mut oversized = draft("oversized preference", "category fairness fixture"); + oversized.kind = CandidateKind::Preference; + oversized.body_md = "x".repeat((SURFACE_MATERIAL_TOKEN_BUDGET - 100) * 4); + store.create_memory(&subject.id, oversized).unwrap(); + + for kind in [ + CandidateKind::Constraint, + CandidateKind::Decision, + CandidateKind::WorkingAssumption, + CandidateKind::OpenQuestion, + CandidateKind::Lesson, + ] { + let mut value = draft(&format!("{kind:?}"), "category fairness fixture"); + value.kind = kind; + store.create_memory(&subject.id, value).unwrap(); + } + + let generation = store.prepare_surface_generation(&subject.id).unwrap(); + for kind in [ + CandidateKind::Constraint, + CandidateKind::Decision, + CandidateKind::WorkingAssumption, + CandidateKind::OpenQuestion, + CandidateKind::Lesson, + ] { + assert!( + generation.materials.iter().any(|item| item.kind == kind), + "one large category must not exclude {kind:?}" + ); + } + assert!( + estimated_tokens_for_bytes(serde_json::to_vec(&generation.materials).unwrap().len()) + <= SURFACE_MATERIAL_TOKEN_BUDGET + ); + } + + #[test] + fn surface_material_selection_reclaims_unused_category_budget() { + let temp = tempfile::tempdir().unwrap(); + let (_manager, _workspace, store) = open_store(temp.path(), "workspace-a"); + let subject = store.create_subject(role()).unwrap(); + + let mut small = draft("small preference", "soft category budget fixture"); + small.kind = CandidateKind::Preference; + store.create_memory(&subject.id, small).unwrap(); + let mut large = draft("large constraint", "soft category budget fixture"); + large.kind = CandidateKind::Constraint; + large.body_md = "x".repeat(22_000); + store.create_memory(&subject.id, large).unwrap(); + + let generation = store.prepare_surface_generation(&subject.id).unwrap(); + assert_eq!(generation.materials.len(), 2); + assert_eq!(generation.materials[0].kind, CandidateKind::Preference); + assert_eq!(generation.materials[1].kind, CandidateKind::Constraint); + assert!( + estimated_tokens_for_bytes(serde_json::to_vec(&generation.materials).unwrap().len()) + <= SURFACE_MATERIAL_TOKEN_BUDGET + ); + } + + #[test] + fn surface_states_validation_races_and_idempotency_fail_closed() { + let temp = tempfile::tempdir().unwrap(); + let (_manager, _workspace, store) = open_store(temp.path(), "workspace-a"); + let subject = store.create_subject(role()).unwrap(); + assert_eq!( + store.resident_surface(&subject.id).unwrap().availability, + SurfaceAvailability::Ungenerated + ); + + let empty_generation = store.prepare_surface_generation(&subject.id).unwrap(); + let empty = store + .publish_surface_generation(&subject.id, &empty_generation.id, Vec::new()) + .unwrap(); + assert!(empty.body_md.is_empty()); + assert_eq!( + store.resident_surface(&subject.id).unwrap().availability, + SurfaceAvailability::Ready + ); + + let memory = store + .create_memory(&subject.id, draft("Current constraint", "confirmed")) + .unwrap(); + assert_eq!( + store.resident_surface(&subject.id).unwrap().availability, + SurfaceAvailability::Stale + ); + let stale_generation = store.prepare_surface_generation(&subject.id).unwrap(); + let invalid = store.publish_surface_generation( + &subject.id, + &stale_generation.id, + vec![SurfacePoint { + body_md: "Invented".into(), + memory_refs: vec![MemoryRevisionRef { + memory_id: "foreign-memory".into(), + revision: 1, + }], + }], + ); + assert!(matches!( + invalid, + Err(SubjektivError::SubjectScopeMismatch { .. }) + )); + store + .fail_surface_generation(&subject.id, &stale_generation.id, "invalid_output") + .unwrap(); + assert_eq!( + store.resident_surface(&subject.id).unwrap().availability, + SurfaceAvailability::Failed + ); + assert_eq!( + store + .memory(&subject.id, &memory.id) + .unwrap() + .unwrap() + .revision, + 1, + "surface failure must not roll back confirmed Memory" + ); + + let raced = store.prepare_surface_generation(&subject.id).unwrap(); + store + .revise_memory( + &subject.id, + &memory.id, + 1, + draft("Corrected constraint", "correction"), + ) + .unwrap(); + assert!(matches!( + store.publish_surface_generation( + &subject.id, + &raced.id, + vec![SurfacePoint { + body_md: "Old wording".into(), + memory_refs: vec![MemoryRevisionRef { + memory_id: memory.id.clone(), + revision: 1, + }], + }], + ), + Err(SubjektivError::SurfaceGenerationConflict(_)) + )); + assert_eq!( + store.resident_surface(&subject.id).unwrap().availability, + SurfaceAvailability::Stale + ); + + let current = store.prepare_surface_generation(&subject.id).unwrap(); + let current_ref = MemoryRevisionRef { + memory_id: memory.id.clone(), + revision: 2, + }; + let points = vec![SurfacePoint { + body_md: "Keep the corrected constraint, including its condition.".into(), + memory_refs: vec![current_ref.clone()], + }]; + let published = store + .publish_surface_generation(&subject.id, ¤t.id, points.clone()) + .unwrap(); + let retry = store + .publish_surface_generation(&subject.id, ¤t.id, points) + .unwrap(); + assert_eq!(published.id, retry.id); + let resident = store.resident_surface(&subject.id).unwrap(); + assert_eq!(resident.availability, SurfaceAvailability::Ready); + assert_eq!(resident.snapshot.unwrap().memory_refs, vec![current_ref]); + } + + #[test] + fn active_memory_that_cannot_fit_is_a_failure_not_a_normal_empty_surface() { + let temp = tempfile::tempdir().unwrap(); + let (_manager, _workspace, store) = open_store(temp.path(), "workspace-a"); + let subject = store.create_subject(role()).unwrap(); + let mut oversized = draft("oversized", "fixture"); + oversized.body_md = "x".repeat((SURFACE_MATERIAL_TOKEN_BUDGET + 1) * 4); + store.create_memory(&subject.id, oversized).unwrap(); + + let generation = store.prepare_surface_generation(&subject.id).unwrap(); + assert_eq!(generation.active_memory_count, 1); + assert!(generation.materials.is_empty()); + assert!( + store + .publish_surface_generation(&subject.id, &generation.id, Vec::new()) + .is_err() + ); + store + .fail_surface_generation(&subject.id, &generation.id, "input_budget_exhausted") + .unwrap(); + assert_eq!( + store.resident_surface(&subject.id).unwrap().availability, + SurfaceAvailability::Failed + ); + } + + #[test] + fn concurrent_surface_generations_cannot_overwrite_one_revision() { + let temp = tempfile::tempdir().unwrap(); + let (_manager, _workspace, store) = open_store(temp.path(), "workspace-a"); + let subject = store.create_subject(role()).unwrap(); + let memory = store + .create_memory(&subject.id, draft("Concurrent constraint", "fixture")) + .unwrap(); + let first = store.prepare_surface_generation(&subject.id).unwrap(); + let second = store.prepare_surface_generation(&subject.id).unwrap(); + let barrier = Arc::new(Barrier::new(2)); + let mut joins = Vec::new(); + for (generation, body) in [(first, "first"), (second, "second")] { + let store = store.clone(); + let subject_id = subject.id.clone(); + let memory_id = memory.id.clone(); + let barrier = Arc::clone(&barrier); + joins.push(thread::spawn(move || { + barrier.wait(); + store.publish_surface_generation( + &subject_id, + &generation.id, + vec![SurfacePoint { + body_md: body.into(), + memory_refs: vec![MemoryRevisionRef { + memory_id, + revision: 1, + }], + }], + ) + })); + } + let results = joins + .into_iter() + .map(|join| join.join().unwrap()) + .collect::>(); + assert_eq!(results.iter().filter(|result| result.is_ok()).count(), 1); + assert_eq!( + results + .iter() + .filter(|result| matches!( + result, + Err(SubjektivError::SurfaceGenerationConflict(_)) + )) + .count(), + 1 + ); + assert_eq!( + store.resident_surface(&subject.id).unwrap().availability, + SurfaceAvailability::Ready + ); + } + + #[test] + fn reopen_and_backup_restore_preserve_exact_history() { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("storage"); + let snapshot_path = temp.path().join("snapshot"); + let (manager, _workspace, store) = open_store(&root, "workspace-a"); + let subject = store.create_subject(role()).unwrap(); + let candidate = store + .stage_candidate(candidate(&subject.id, "candidate-1", "workspace-a")) + .unwrap(); + let (memory, _) = store + .apply_candidate( + &subject.id, + &candidate.id, + MemoryRevisionTarget::Create, + draft("Persist this revision", "accepted before restart"), + "applied", + ) + .unwrap(); + drop(store); + manager.shutdown().unwrap(); + drop(manager); + + let (manager, workspace, reopened) = open_store(&root, "workspace-a"); + assert_eq!( + reopened + .memory_revision(&subject.id, &memory.id, 1) + .unwrap() + .unwrap() + .claim, + "Persist this revision" + ); + assert_eq!( + reopened + .staging_resolution(&subject.id, &candidate.id) + .unwrap() + .unwrap() + .candidate + .source_refs[0] + .session_id + .as_deref(), + Some("session-1") + ); + let surface = reopened + .create_surface_snapshot( + &subject.id, + "Persisted resident surface", + vec![MemoryRevisionRef { + memory_id: memory.id.clone(), + revision: 1, + }], + 1, + ) + .unwrap(); + workspace.backup(&snapshot_path).unwrap(); + let mut post_backup = draft("Post-backup revision", "not in snapshot"); + post_backup.derived_from.push(MemoryRevisionRef { + memory_id: memory.id.clone(), + revision: 1, + }); + let second = reopened + .revise_memory(&subject.id, &memory.id, 1, post_backup) + .unwrap(); + assert_eq!(second.revision, 2); + workspace.delete().unwrap(); + drop(reopened); + drop(workspace); + drop(manager); + + let manager = FeatureStorage::new(&root); + let workspace = manager.workspace("workspace-a").unwrap(); + workspace.restore(&snapshot_path).unwrap(); + let registration = SubjektivStore::register(&workspace).unwrap(); + let restored = SubjektivStore::open(&workspace, ®istration).unwrap(); + assert_eq!( + restored + .subject(&subject.id) + .unwrap() + .unwrap() + .store_revision, + 1 + ); + assert_eq!( + restored + .memory(&subject.id, &memory.id) + .unwrap() + .unwrap() + .claim, + "Persist this revision" + ); + assert!( + restored + .memory_revision(&subject.id, &memory.id, 2) + .unwrap() + .is_none() + ); + assert!( + restored + .staging_resolution(&subject.id, &candidate.id) + .unwrap() + .is_some() + ); + assert_eq!( + restored + .surface_snapshot(&subject.id, &surface.id) + .unwrap() + .unwrap() + .memory_refs, + vec![MemoryRevisionRef { + memory_id: memory.id, + revision: 1, + }] + ); + } +} diff --git a/crates/tui/src/backend_worker_picker.rs b/crates/tui/src/backend_worker_picker.rs index 6821d1c19..bd8f176fa 100644 --- a/crates/tui/src/backend_worker_picker.rs +++ b/crates/tui/src/backend_worker_picker.rs @@ -957,6 +957,7 @@ mod tests { language: "English".into(), }), subjektiv_attached: false, + backend_job: None, }) .unwrap(); runtime.stop_worker(&worker.worker_ref, None).unwrap(); diff --git a/crates/tui/src/console/mod.rs b/crates/tui/src/console/mod.rs index 03083575d..04a762904 100644 --- a/crates/tui/src/console/mod.rs +++ b/crates/tui/src/console/mod.rs @@ -457,20 +457,21 @@ pub(crate) async fn run_standalone( state_dir: PathBuf, worker_name: Option, profile: Option, + subject_id: Option, ) -> Result<(), Box> { let worker_name = worker_name.unwrap_or_else(|| "local".to_string()); let profile = profile.map_or(manifest::ProfileSelector::Default, |profile| { manifest::ProfileSelector::parse_cli(&profile) }); let history_root = workspace_root.clone(); - let launch = StandaloneLaunchConfig { - state_dir, - cwd: workspace_root, - profile, - worker_name: worker_name.clone(), - } - .resolve() - .map_err(|error| { + let launch = + StandaloneLaunchConfig::new(workspace_root, state_dir, profile, worker_name.clone()); + let launch = if let Some(subject_id) = subject_id { + launch.with_subject(subject_id) + } else { + launch + }; + let launch = launch.resolve().map_err(|error| { io::Error::new( io::ErrorKind::InvalidInput, format!("Standalone launch configuration failed: {error}"), diff --git a/crates/tui/src/lib.rs b/crates/tui/src/lib.rs index 359063b6e..d72dffc1d 100644 --- a/crates/tui/src/lib.rs +++ b/crates/tui/src/lib.rs @@ -65,6 +65,7 @@ pub enum LaunchMode { Spawn { worker_name: Option, profile: Option, + subject_id: Option, }, /// Restore one client-owned standalone Worker. The current cwd is the default scope; /// `include_all` opts into all standalone Workers under the same client data root. @@ -202,7 +203,8 @@ async fn launch_mode( LaunchMode::Spawn { worker_name, profile, - } => match standalone_spawn::select(&workspace_root, worker_name, profile) { + subject_id, + } => match standalone_spawn::select(&workspace_root, worker_name, profile, subject_id) { Ok(Some(selection)) => match target.spawn_worker() { Ok(spawn) => { console::run_standalone( @@ -210,6 +212,7 @@ async fn launch_mode( spawn.state_dir, Some(selection.worker_name), Some(selection.profile), + selection.subject_id, ) .await } diff --git a/crates/tui/src/standalone_spawn.rs b/crates/tui/src/standalone_spawn.rs index 722d1a1aa..091bbc598 100644 --- a/crates/tui/src/standalone_spawn.rs +++ b/crates/tui/src/standalone_spawn.rs @@ -12,13 +12,14 @@ use thiserror::Error; use crate::inline_terminal::{InlineTerminal, with_inline_terminal}; -const VIEWPORT_HEIGHT: u16 = 6; +const VIEWPORT_HEIGHT: u16 = 7; const FALLBACK_WORKER_NAME: &str = "worker"; #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct StandaloneSpawnSelection { pub worker_name: String, pub profile: String, + pub subject_id: Option, } #[derive(Debug, Error)] @@ -55,6 +56,9 @@ enum SpawnAction { struct SpawnForm { worker_name: String, cursor: usize, + subject_id: String, + subject_cursor: usize, + editing_subject: bool, profile_choices: Vec, selected_profile: usize, status: Option<(String, StatusKind)>, @@ -75,12 +79,26 @@ impl SpawnForm { Self { worker_name, cursor, + subject_id: String::new(), + subject_cursor: 0, + editing_subject: false, profile_choices, selected_profile, status: None, } } + fn with_subject(mut self, subject_id: Option) -> Self { + self.subject_id = subject_id.unwrap_or_default(); + self.subject_cursor = self.subject_id.chars().count(); + self + } + + fn subject_selection(&self) -> Option { + let id = self.subject_id.trim(); + (!id.is_empty()).then(|| id.to_owned()) + } + fn selected_profile(&self) -> &ProfileChoice { &self.profile_choices[self.selected_profile] } @@ -98,6 +116,47 @@ impl SpawnForm { } self.status = None; + if key.code == KeyCode::F(2) { + self.editing_subject = !self.editing_subject; + return SpawnAction::None; + } + if self.editing_subject { + match key.code { + KeyCode::Left => self.subject_cursor = self.subject_cursor.saturating_sub(1), + KeyCode::Right => { + self.subject_cursor = + (self.subject_cursor + 1).min(self.subject_id.chars().count()) + } + KeyCode::Home => self.subject_cursor = 0, + KeyCode::End => self.subject_cursor = self.subject_id.chars().count(), + KeyCode::Backspace => { + if self.subject_cursor > 0 { + self.subject_id + .remove(byte_index(&self.subject_id, self.subject_cursor - 1)); + self.subject_cursor -= 1; + } + } + KeyCode::Delete => { + if self.subject_cursor < self.subject_id.chars().count() { + self.subject_id + .remove(byte_index(&self.subject_id, self.subject_cursor)); + } + } + KeyCode::Char(ch) if is_safe_worker_char(ch) => { + self.subject_id + .insert(byte_index(&self.subject_id, self.subject_cursor), ch); + self.subject_cursor += 1; + } + KeyCode::Char(_) => {} + // Submit/cancel and profile cycling remain available while editing the Subject. + _ => return self.apply_form_key(key), + } + return SpawnAction::None; + } + self.apply_form_key(key) + } + + fn apply_form_key(&mut self, key: KeyEvent) -> SpawnAction { match key.code { KeyCode::Esc => SpawnAction::Cancel, KeyCode::Enter => { @@ -167,12 +226,14 @@ pub(crate) fn select( workspace_root: &Path, worker_name: Option, profile: Option, + subject_id: Option, ) -> Result, StandaloneSpawnError> { let default_worker_name = default_worker_name(workspace_root); if let Some(profile) = profile { return Ok(Some(StandaloneSpawnSelection { worker_name: worker_name.unwrap_or(default_worker_name), profile, + subject_id, })); } @@ -185,7 +246,7 @@ pub(crate) fn select( with_inline_terminal(VIEWPORT_HEIGHT, |terminal| { run_picker( terminal, - SpawnForm::new(worker_name, default_worker_name, choices), + SpawnForm::new(worker_name, default_worker_name, choices).with_subject(subject_id), ) }) } @@ -214,6 +275,7 @@ fn run_picker( let selection = StandaloneSpawnSelection { worker_name: form.worker_name.trim().to_owned(), profile: form.selected_profile().selector.clone(), + subject_id: form.subject_selection(), }; form.status = Some(("starting worker...".to_owned(), StatusKind::Progress)); terminal.draw(|frame| draw_form(frame, &form))?; @@ -261,6 +323,7 @@ fn draw_form(frame: &mut ratatui::Frame<'_>, form: &SpawnForm) { Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), + Constraint::Length(1), Constraint::Min(0), ]) .split(frame.area()); @@ -306,12 +369,32 @@ fn draw_form(frame: &mut ratatui::Frame<'_>, form: &SpawnForm) { chunks[2], ); + frame.render_widget( + Paragraph::new(Line::from(vec![ + Span::raw(" "), + Span::styled("subject: ", Style::default().fg(Color::DarkGray)), + Span::styled( + if form.subject_id.is_empty() { + "(none)" + } else { + &form.subject_id + }, + Style::default().fg(Color::Cyan), + ), + Span::styled( + " (F2 edit name/Subject ID)", + Style::default().fg(Color::DarkGray), + ), + ])), + chunks[3], + ); + frame.render_widget( Paragraph::new(Line::from(Span::styled( " enter spawn · left/right edit · esc cancel", Style::default().fg(Color::DarkGray), ))), - chunks[3], + chunks[4], ); let (message, color) = form @@ -331,16 +414,20 @@ fn draw_form(frame: &mut ratatui::Frame<'_>, form: &SpawnForm) { Span::raw(" "), Span::styled(message, Style::default().fg(color)), ])), - chunks[4], + chunks[5], ); - let prefix_width = " name: ".chars().count() as u16; - let x = chunks[1] + let (row, prefix, cursor) = if form.editing_subject { + (3, " subject: ", form.subject_cursor) + } else { + (1, " name: ", form.cursor) + }; + let x = chunks[row] .x - .saturating_add(prefix_width) - .saturating_add(form.cursor as u16) - .min(chunks[1].right().saturating_sub(1)); - frame.set_cursor_position((x, chunks[1].y)); + .saturating_add(prefix.chars().count() as u16) + .saturating_add(cursor as u16) + .min(chunks[row].right().saturating_sub(1)); + frame.set_cursor_position((x, chunks[row].y)); } fn default_worker_name(workspace_root: &Path) -> String { @@ -394,9 +481,43 @@ mod tests { fn default_form_preserves_old_spawn_layout_defaults() { let form = SpawnForm::new(None, "yoi".to_owned(), choices()); assert_eq!(form.worker_name, "yoi"); + assert_eq!(form.subject_selection(), None); assert_eq!(form.selected_profile().selector, "builtin:standalone"); } + #[test] + fn optional_subject_field_edits_independently_and_can_be_cleared() { + let mut form = SpawnForm::new(None, "worker".to_owned(), choices()); + form.apply_key(KeyEvent::new(KeyCode::F(2), KeyModifiers::NONE)); + for ch in "subject-1".chars() { + form.apply_key(KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE)); + } + assert_eq!(form.subject_selection().as_deref(), Some("subject-1")); + assert_eq!(form.worker_name, "worker"); + form.apply_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE)); + assert_eq!(form.selected_profile().selector, "builtin:coder"); + assert_eq!( + form.apply_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)), + SpawnAction::Submit + ); + for _ in 0..9 { + form.apply_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE)); + } + assert_eq!(form.subject_selection(), None); + form.apply_key(KeyEvent::new(KeyCode::F(2), KeyModifiers::NONE)); + form.apply_key(KeyEvent::new(KeyCode::Char('2'), KeyModifiers::NONE)); + assert_eq!(form.worker_name, "worker2"); + assert_eq!(form.subject_selection(), None); + } + + #[test] + fn explicit_subject_prefills_the_optional_field() { + let form = SpawnForm::new(None, "worker".to_owned(), choices()) + .with_subject(Some("subject-id".to_owned())); + assert_eq!(form.subject_selection().as_deref(), Some("subject-id")); + assert_eq!(form.subject_cursor, 10); + } + #[test] fn tab_and_arrows_cycle_profiles() { let mut form = SpawnForm::new(None, "yoi".to_owned(), choices()); @@ -453,6 +574,7 @@ mod tests { assert!(rendered.contains("spawn worker")); assert!(rendered.contains("name: yoi")); + assert!(rendered.contains("subject: (none)")); assert!(rendered.contains("profile: builtin:standalone (default) — Standalone")); assert!(rendered.contains("enter spawn · left/right edit · esc cancel")); } @@ -504,10 +626,12 @@ mod tests { Path::new("/home/hare/Project/yoi"), None, Some("builtin:coder".to_owned()), + Some("subject-id".to_owned()), ) .unwrap() .unwrap(); assert_eq!(selection.worker_name, "yoi"); assert_eq!(selection.profile, "builtin:coder"); + assert_eq!(selection.subject_id.as_deref(), Some("subject-id")); } } diff --git a/crates/worker/src/bootstrap.rs b/crates/worker/src/bootstrap.rs index 587ed11c9..d027aa8c9 100644 --- a/crates/worker/src/bootstrap.rs +++ b/crates/worker/src/bootstrap.rs @@ -52,6 +52,7 @@ pub struct WorkerBootstrap { transport: WorkerControllerTransport, model_client: Option>, workdir_session: Option, + subjektiv_host: Option>, } /// A constructed Worker whose host-owned live bindings can still be installed @@ -106,6 +107,7 @@ where transport, model_client: None, workdir_session: None, + subjektiv_host: None, } } @@ -127,6 +129,16 @@ where self } + /// Bind an explicit, process-owned subject capability. Profile policy alone + /// is inert; the connection is never persisted into the Worker manifest. + pub fn with_subjektiv_host( + mut self, + host: std::sync::Arc, + ) -> Self { + self.subjektiv_host = Some(host); + self + } + /// Construct the Worker without exposing a controller handle. Runtime hosts /// use this seam to bind Workdir, observation, Flow, and other live services /// before [`PreparedWorker::start`] performs Feature installation. @@ -147,6 +159,24 @@ where if let Some(workdir_session) = self.workdir_session { worker.bind_single_workdir_session(Some(workdir_session)); } + let explicit_subjektiv_host = self.subjektiv_host.is_some(); + worker.bind_subjektiv_host(self.subjektiv_host); + if explicit_subjektiv_host + && worker.manifest().feature.subjektiv.profile.enabled + && !worker + .manifest() + .feature + .subjektiv + .profile + .consolidation_tools + { + worker + .finalize_subjektiv_session_attribution( + true, + crate::SubjektivSessionAttributionLifecycle::NewSession, + ) + .map_err(WorkerBootstrapError::Worker)?; + } Ok(PreparedWorker::new(worker, self.layout, self.transport)) } @@ -170,6 +200,24 @@ where if let Some(workdir_session) = self.workdir_session { worker.bind_single_workdir_session(Some(workdir_session)); } + let explicit_subjektiv_host = self.subjektiv_host.is_some(); + worker.bind_subjektiv_host(self.subjektiv_host); + if explicit_subjektiv_host + && worker.manifest().feature.subjektiv.profile.enabled + && !worker + .manifest() + .feature + .subjektiv + .profile + .consolidation_tools + { + worker + .finalize_subjektiv_session_attribution( + true, + crate::SubjektivSessionAttributionLifecycle::RestoredSession, + ) + .map_err(WorkerBootstrapError::Worker)?; + } Ok(PreparedWorker::new(worker, self.layout, self.transport)) } diff --git a/crates/worker/src/controller.rs b/crates/worker/src/controller.rs index 509c2a85e..38e36e541 100644 --- a/crates/worker/src/controller.rs +++ b/crates/worker/src/controller.rs @@ -1377,6 +1377,7 @@ where None => durable_parent_notifications, }; let backend_job = worker.backend_job().cloned(); + let subjektiv_host = worker.subjektiv_host_connection()?; // Bounded Jobs complete their lifecycle synchronously, never by post-run background work. let lifecycle_enabled = worker.manifest_lifecycle_features_enabled() && backend_job.is_none(); let prompts = worker.prompts().clone(); @@ -1443,8 +1444,8 @@ where workspace_client.clone(), ); if binding.subjektiv_consolidation { - let surface = crate::feature::builtin::memory_surface_lifecycle::SubjektivSurfaceLifecycleFeature::for_job( - workspace_client, spawner_manifest.clone(), worker.llm_client_handle(), + let surface = crate::feature::builtin::memory_surface_lifecycle::SubjektivSurfaceLifecycleFeature::for_host( + subjektiv_host.clone().ok_or_else(|| std::io::Error::other("Backend Job requires subjektiv Host capability"))?, spawner_manifest.clone(), worker.llm_client_handle(), prompts.clone(), spawner_workspace_context.clone(), )?; result_feature = result_feature.with_surface(surface); @@ -1453,7 +1454,9 @@ where } let memory_profile = &worker.manifest().feature.memory.profile; let subjektiv_profile = &worker.manifest().feature.subjektiv.profile; - validate_memory_lifecycle_targets(memory_profile, subjektiv_profile)?; + if subjektiv_host.is_some() { + validate_memory_lifecycle_targets(memory_profile, subjektiv_profile)?; + } let memory_install_plan = crate::feature::builtin::memory::MemoryFeatureInstallPlan::prepare( worker.manifest(), worker.workspace_client_handle(), @@ -1476,7 +1479,7 @@ where let subjektiv_consolidation_plan = crate::feature::builtin::memory::SubjektivConsolidationFeatureInstallPlan::prepare( worker.manifest(), - worker.workspace_client_handle(), + subjektiv_host.clone(), worker.prompts().load_full(), )?; if let Some(plan) = subjektiv_consolidation_plan { @@ -1485,22 +1488,20 @@ where } feature_registry.add_module(plan.module); } - let ordinary_subjektiv_features_enabled = - crate::feature::builtin::memory::ordinary_subjektiv_features_enabled(worker.manifest()); + let ordinary_subjektiv_features_enabled = subjektiv_host.is_some() + && crate::feature::builtin::memory::ordinary_subjektiv_features_enabled(worker.manifest()); if let Some(resident_summary_source) = crate::feature::builtin::memory::ordinary_subjektiv_resident_summary_source( worker.manifest(), - worker.workspace_client_handle(), + subjektiv_host.clone(), worker.prompts(), )? { - let workspace_id = worker - .workspace_client() - .workspace_id() - .expect("validated subject resident source has Workspace identity") - .to_string(); + let workspace_id = subjektiv_host + .as_ref() + .and_then(|host| host.host.workspace_id()); if let Some(refresh_feature) = - crate::feature::builtin::memory::SubjektivResidentRestoreRefreshFeature::for_host( + crate::feature::builtin::memory::SubjektivResidentRestoreRefreshFeature::for_connection( lifecycle_enabled, Arc::clone(&resident_summary_source), worker.prompts(), @@ -1516,7 +1517,7 @@ where crate::feature::builtin::subjektiv_memory::SubjektivMemoryFeature::from_resolved_config( &worker.manifest().feature.subjektiv, worker.committed_session_capture_handle(), - worker.workspace_client_handle(), + subjektiv_host.clone(), )? { feature_registry.add_module(subjektiv_memory); @@ -1525,7 +1526,7 @@ where && let Some(subjektiv_sessions) = crate::feature::builtin::subjektiv_session::SubjektivSessionFeature::from_resolved_config( &worker.manifest().feature.subjektiv, - worker.workspace_client_handle(), + subjektiv_host.clone(), )? { feature_registry.add_module(subjektiv_sessions); @@ -1548,7 +1549,7 @@ where } if backend_job.is_none() && let Some(surface_lifecycle) = crate::feature::builtin::memory_surface_lifecycle::SubjektivSurfaceLifecycleFeature::from_manifest( lifecycle_enabled, - worker.workspace_client_handle(), + subjektiv_host.clone(), spawner_manifest.clone(), worker.llm_client_handle(), prompts.clone(), @@ -1563,7 +1564,7 @@ where worker.manifest().feature.subjektiv.clone(), worker.committed_session_capture_handle(), worker.session_extension_handle(), - worker.workspace_client_handle(), + subjektiv_host.clone(), spawner_manifest.clone(), worker.llm_client_handle(), prompts.clone(), diff --git a/crates/worker/src/feature/builtin/memory.rs b/crates/worker/src/feature/builtin/memory.rs index d25581cf5..96fe698cd 100644 --- a/crates/worker/src/feature/builtin/memory.rs +++ b/crates/worker/src/feature/builtin/memory.rs @@ -4,8 +4,10 @@ //! authority. In that case model-visible Memory tools must go through the //! workspace backend instead of resolving `.yoi/memory` from a Worker workdir. +use crate::subjektiv::{SubjektivHostConnection, SubjektivHostError}; use std::collections::HashMap; use std::sync::{Arc, Mutex}; +#[cfg(test)] use std::time::Duration; use agen::tool::{Tool, ToolDefinition, ToolError, ToolExecutionContext, ToolMeta, ToolOutput}; @@ -158,6 +160,7 @@ impl dyn WorkspaceClient + '_ { } } +#[cfg(test)] const RESIDENT_SUMMARY_REQUEST_TIMEOUT: Duration = Duration::from_secs(5); async fn execute_subjektiv_memory_operation( @@ -327,9 +330,7 @@ pub fn workspace_http_memory_tools(client: Arc) -> Vec, -) -> Vec { +fn subjektiv_consolidation_tools(host: SubjektivHostConnection) -> Vec { use SubjectConsolidationOperation as Operation; [ ( @@ -371,14 +372,14 @@ pub fn workspace_http_subjektiv_consolidation_tools( ] .into_iter() .map(|(name, description, schema, operation)| { - let client = Arc::clone(&client); + let host = host.clone(); Arc::new(move || { ( ToolMeta::new(name) .description(description) .input_schema(schema.clone()), Arc::new(SubjectConsolidationTool { - client: Arc::clone(&client), + host: host.clone(), operation, }) as Arc, ) @@ -398,7 +399,7 @@ enum SubjectConsolidationOperation { } struct SubjectConsolidationTool { - client: Arc, + host: SubjektivHostConnection, operation: SubjectConsolidationOperation, } @@ -434,59 +435,17 @@ impl Tool for SubjectConsolidationTool { )?) } }; - let workspace_id = self.client.workspace_id().ok_or_else(|| { - ToolError::ExecutionFailed( - "subjektiv consolidation requires Workspace authority".into(), - ) - })?; - let response = self - .client - .execute(WorkspaceRequest::json( - WorkspaceRequestMethod::Post, - format!("/api/w/{workspace_id}/subjektiv/memory"), - serde_json::to_string(&server_api::SubjektivMemoryBackendRequest { operation }) - .map_err(|error| ToolError::ExecutionFailed(error.to_string()))?, - )) - .map_err(|error| ToolError::ExecutionFailed(error.to_string()))?; - if !response.is_success() { - let parsed = - serde_json::from_str::(&response.body).ok(); - if let Some((code, message)) = parsed.as_ref().and_then(|error| { - error - .diagnostics - .iter() - .find(|diagnostic| { - matches!( - diagnostic.code.as_str(), - "revision_conflict" - | "candidate_decision_conflict" - | "subject_scope_mismatch" - ) - }) - .map(|diagnostic| (diagnostic.code.clone(), error.message.clone())) - }) { + let response = match self.host.memory(operation) { + Ok(response) => response, + Err(SubjektivHostError::Conflict { code, message }) => { return Ok(ToolOutput { summary: format!("Candidate decision requires reread: {code}."), - content: Some( - serde_json::json!({ - "status": "error", - "error": { "code": code, "message": message } - }) - .to_string(), - ), + content: Some(serde_json::json!({"status":"error", "error":{"code":code,"message":message}}).to_string()), attachments: Vec::new(), }); } - let detail = parsed.map(|error| error.message).unwrap_or(response.body); - return Err(if matches!(response.status, 400 | 404 | 409 | 422) { - ToolError::InvalidArgument(detail) - } else { - ToolError::ExecutionFailed(detail) - }); - } - let response: server_api::SubjektivMemoryBackendResponse = - serde_json::from_str(&response.body) - .map_err(|error| ToolError::ExecutionFailed(error.to_string()))?; + Err(error) => return Err(error.into()), + }; let summary = match &response { server_api::SubjektivMemoryBackendResponse::Query(value) => { format!("Found {} subject Memory item(s).", value.items.len()) @@ -686,7 +645,7 @@ struct SubjectBehaviorRefreshState { } struct WorkspaceSubjektivResidentSummarySource { - client: Arc, + host: SubjektivHostConnection, prompts: Arc>, behavior_state: Mutex, } @@ -730,16 +689,12 @@ impl SystemPromptContributionSource for WorkspaceResidentSummarySource { impl WorkspaceSubjektivResidentSummarySource { async fn fetch( &self, - ) -> Result { - match execute_subjektiv_memory_operation_with( - self.client.as_ref(), + ) -> Result { + match self.host.memory( server_api::SubjektivMemoryBackendOperation::ResidentContext(Default::default()), - |client, request| { - client.execute_with_timeout(request, RESIDENT_SUMMARY_REQUEST_TIMEOUT) - }, )? { server_api::SubjektivMemoryBackendResponse::ResidentContext(output) => Ok(output), - other => Err(WorkspaceMemoryBackendError::InvalidResponse(format!( + other => Err(SubjektivHostError::Unavailable(format!( "unexpected subject resident context response: {other:?}" ))), } @@ -862,10 +817,11 @@ const SUBJEKTIV_BEHAVIOR_REWRITE_HOOK: &str = "invalidate-subject-behavior-befor pub(crate) struct SubjektivResidentRestoreRefreshFeature { source: Arc, prompts: Arc>, - workspace_id: String, + workspace_id: Option, } impl SubjektivResidentRestoreRefreshFeature { + #[cfg(test)] pub(crate) fn for_host( lifecycle_enabled: bool, source: Arc, @@ -875,6 +831,20 @@ impl SubjektivResidentRestoreRefreshFeature { lifecycle_enabled.then(|| Self::new(source, prompts, workspace_id)) } + pub(crate) fn for_connection( + lifecycle_enabled: bool, + source: Arc, + prompts: Arc>, + workspace_id: Option, + ) -> Option { + lifecycle_enabled.then_some(Self { + source, + prompts, + workspace_id, + }) + } + + #[cfg(test)] pub(crate) fn new( source: Arc, prompts: Arc>, @@ -883,7 +853,7 @@ impl SubjektivResidentRestoreRefreshFeature { Self { source, prompts, - workspace_id: workspace_id.into(), + workspace_id: Some(workspace_id.into()), } } } @@ -981,7 +951,7 @@ impl Hook for SubjektivBehaviorRefreshHook { struct SubjektivResidentRestoreRefreshHook { source: Arc, prompts: Arc>, - workspace_id: String, + workspace_id: Option, } #[async_trait] @@ -1000,7 +970,7 @@ impl Hook for SubjektivResidentRestoreRefreshHook { context.system_items().append_resident_summary_refresh( body, session_store::PromptRenderProvenance { - workspace_id: Some(self.workspace_id.clone()), + workspace_id: self.workspace_id.clone(), config_revision: projection.config_revision, source_digest: projection.source_digest.clone(), projection_digest: projection.catalog_digest.clone(), @@ -1114,51 +1084,25 @@ pub(crate) fn is_builtin_subjektiv_consolidation_profile( } pub(crate) fn ordinary_subjektiv_features_enabled(manifest: &manifest::WorkerManifest) -> bool { - manifest.feature.subjektiv.execution_enabled() + manifest.feature.subjektiv.profile.enabled && !manifest.feature.subjektiv.profile.consolidation_tools } pub(crate) fn ordinary_subjektiv_resident_summary_source( manifest: &manifest::WorkerManifest, - client: Arc, + host: Option, prompts: Arc>, ) -> std::io::Result>> { - if !ordinary_subjektiv_features_enabled(manifest) { + if !ordinary_subjektiv_features_enabled(manifest) || host.is_none() { return Ok(None); } manifest .feature .subjektiv .validate_execution() - .map_err(|message| std::io::Error::new(std::io::ErrorKind::InvalidInput, message))?; - if !client.is_available() { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "subjektiv resident Memory requires Backend Workspace API authority", - )); - } - let workspace_id = client.workspace_id().ok_or_else(|| { - std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "subjektiv resident Memory requires Backend Workspace API authority", - ) - })?; - let settings = manifest - .feature - .subjektiv - .workspace_settings() - .expect("validated enabled subjektiv config has Workspace settings"); - if settings.workspace_id != workspace_id { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - format!( - "subjektiv Workspace settings belong to {} instead of {}", - settings.workspace_id, workspace_id - ), - )); - } + .map_err(std::io::Error::other)?; Ok(Some(Arc::new(WorkspaceSubjektivResidentSummarySource { - client, + host: host.expect("checked explicit connection"), prompts, behavior_state: Mutex::new(SubjectBehaviorRefreshState::default()), }))) @@ -1172,57 +1116,51 @@ pub(crate) struct SubjektivConsolidationFeatureInstallPlan { impl SubjektivConsolidationFeatureInstallPlan { pub(crate) fn prepare( manifest: &manifest::WorkerManifest, - client: Arc, + host: Option, prompts: Arc, ) -> std::io::Result> { - if !manifest.feature.subjektiv.profile.consolidation_tools { + let config = &manifest.feature.subjektiv; + if !config.profile.enabled || !config.profile.consolidation_tools || host.is_none() { return Ok(None); } - let config = &manifest.feature.subjektiv; - config - .validate_execution() - .map_err(|message| std::io::Error::new(std::io::ErrorKind::InvalidInput, message))?; - if !config.execution_enabled() || config.profile.extraction.enabled { + config.validate_execution().map_err(std::io::Error::other)?; + let host = host.expect("checked explicit connection"); + if config.profile.extraction.enabled || !host.host.consolidation_granted() { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, - "subjektiv consolidation profile requires trusted subject settings without automatic extraction", - )); - } - let workspace_id = client.workspace_id().ok_or_else(|| { - std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "subjektiv consolidation tools require Backend Workspace API authority", - ) - })?; - let settings = config - .workspace_settings() - .expect("validated enabled subjektiv config has Workspace settings"); - if settings.workspace_id != workspace_id { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - format!( - "subjektiv settings belong to {} instead of {}", - settings.workspace_id, workspace_id - ), + "subjektiv consolidation requires explicit Host grant without automatic extraction", )); } let system_prompt_override = prompts - .subjektiv_memory_consolidation_system(&settings.language) + .subjektiv_memory_consolidation_system(&host.host.settings().language) .map_err(|error| std::io::Error::other(error.to_string()))?; Ok(Some(Self { - module: SubjektivConsolidationToolsFeature { - tools: workspace_http_subjektiv_consolidation_tools(client), - }, + module: SubjektivConsolidationToolsFeature::new(host)?, system_prompt_override, })) } } #[derive(Clone)] -pub(crate) struct SubjektivConsolidationToolsFeature { +pub struct SubjektivConsolidationToolsFeature { tools: Vec, } +impl SubjektivConsolidationToolsFeature { + /// Reusable installation for a generic Job with an explicit attenuated Host. + pub fn new(host: SubjektivHostConnection) -> std::io::Result { + if !host.host.consolidation_granted() { + return Err(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "Host did not grant consolidation", + )); + } + Ok(Self { + tools: subjektiv_consolidation_tools(host), + }) + } +} + impl FeatureModule for SubjektivConsolidationToolsFeature { fn descriptor(&self) -> FeatureDescriptor { let mut descriptor = FeatureDescriptor::builtin( @@ -1293,6 +1231,29 @@ mod tests { use agen::tool::ToolDefinition; use std::sync::Mutex; + fn backend_resident_source( + manifest: &manifest::WorkerManifest, + client: Arc, + prompts: Arc>, + ) -> std::io::Result>> { + let host = crate::subjektiv::BackendSubjektivHost::from_resolved_config( + &manifest.feature.subjektiv, + client, + )?; + let host = host + .map(|host| { + SubjektivHostConnection::new( + host, + crate::subjektiv::SubjektivHostContext { + worker_id: manifest.worker.name.clone(), + session_id: "session-1".into(), + }, + ) + }) + .transpose()?; + ordinary_subjektiv_resident_summary_source(manifest, host, prompts) + } + fn test_prompts() -> Arc> { Arc::new(ArcSwap::from(PromptCatalog::builtins_only().unwrap())) } @@ -1601,7 +1562,7 @@ mod tests { let plan = SubjektivConsolidationFeatureInstallPlan::prepare( &manifest, - test_client(), + Some(crate::subjektiv::test_connection(test_client())), crate::prompt::catalog::PromptCatalog::builtins_only().unwrap(), ) .unwrap() @@ -1696,13 +1657,9 @@ permission = "write" memory::backend::MemoryResidentSummaryAvailability::Ready, Some("current subject surface"), )); - let ready = ordinary_subjektiv_resident_summary_source( - &manifest, - ready_client.clone(), - test_prompts(), - ) - .unwrap() - .unwrap(); + let ready = backend_resident_source(&manifest, ready_client.clone(), test_prompts()) + .unwrap() + .unwrap(); let loaded = ready.load().await; let SystemPromptContribution::Ready(loaded) = loaded else { panic!("subject resident context must be present"); @@ -1722,13 +1679,9 @@ permission = "write" memory::backend::MemoryResidentSummaryAvailability::Ready, None, )); - let ready_empty = ordinary_subjektiv_resident_summary_source( - &manifest, - ready_empty_client, - test_prompts(), - ) - .unwrap() - .unwrap(); + let ready_empty = backend_resident_source(&manifest, ready_empty_client, test_prompts()) + .unwrap() + .unwrap(); let SystemPromptContribution::Ready(ready_empty) = ready_empty.load().await else { panic!("empty surface must retain Subject behavior"); }; @@ -1740,7 +1693,7 @@ permission = "write" memory::backend::MemoryResidentSummaryAvailability::Stale, memory::backend::MemoryResidentSummaryAvailability::Failed, ] { - let source = ordinary_subjektiv_resident_summary_source( + let source = backend_resident_source( &manifest, Arc::new(SubjectResidentClient::new( availability, @@ -1765,10 +1718,9 @@ permission = "write" memory::backend::MemoryResidentSummaryAvailability::Ungenerated, None, )); - let source = - ordinary_subjektiv_resident_summary_source(&manifest, client.clone(), test_prompts()) - .unwrap() - .unwrap(); + let source = backend_resident_source(&manifest, client.clone(), test_prompts()) + .unwrap() + .unwrap(); let initial = source.load().await; assert!(matches!(initial, SystemPromptContribution::Ready(_))); @@ -1838,10 +1790,9 @@ permission = "write" async fn subject_resident_source_fails_closed_without_trusted_subject_scope() { let manifest = subject_manifest("workspace"); let client = Arc::new(SubjectResidentClient::denied()); - let source = - ordinary_subjektiv_resident_summary_source(&manifest, client.clone(), test_prompts()) - .unwrap() - .unwrap(); + let source = backend_resident_source(&manifest, client.clone(), test_prompts()) + .unwrap() + .unwrap(); let SystemPromptContribution::Ready(failure) = source.load().await else { panic!("fetch failure must be explicit in resident context"); @@ -1874,7 +1825,7 @@ permission = "write" ) .unwrap(); assert!( - ordinary_subjektiv_resident_summary_source(&disabled, test_client(), test_prompts()) + backend_resident_source(&disabled, test_client(), test_prompts()) .unwrap() .is_none() ); @@ -1886,30 +1837,22 @@ permission = "write" Some("policy-only surface must not load"), )); assert!( - ordinary_subjektiv_resident_summary_source( - &policy_only, - unattached_client.clone(), - test_prompts() - ) - .unwrap() - .is_none() + backend_resident_source(&policy_only, unattached_client.clone(), test_prompts()) + .unwrap() + .is_none() ); assert!(unattached_client.paths.lock().unwrap().is_empty()); let foreign = subject_manifest("other-workspace"); - assert!( - ordinary_subjektiv_resident_summary_source(&foreign, test_client(), test_prompts()) - .is_err() - ); + assert!(backend_resident_source(&foreign, test_client(), test_prompts()).is_err()); } #[test] fn subject_restore_refresh_is_suppressed_for_internal_hosts() { let manifest = subject_manifest("workspace"); - let source = - ordinary_subjektiv_resident_summary_source(&manifest, test_client(), test_prompts()) - .unwrap() - .unwrap(); + let source = backend_resident_source(&manifest, test_client(), test_prompts()) + .unwrap() + .unwrap(); let prompts = Arc::new(ArcSwap::from( crate::prompt::catalog::PromptCatalog::builtins_only().unwrap(), )); @@ -1923,10 +1866,9 @@ permission = "write" #[test] fn subject_restore_refresh_feature_declares_only_restore_lifecycle() { let manifest = subject_manifest("workspace"); - let source = - ordinary_subjektiv_resident_summary_source(&manifest, test_client(), test_prompts()) - .unwrap() - .unwrap(); + let source = backend_resident_source(&manifest, test_client(), test_prompts()) + .unwrap() + .unwrap(); let feature = SubjektivResidentRestoreRefreshFeature::new( source, Arc::new(ArcSwap::from( @@ -2034,7 +1976,7 @@ permission = "write" (403, "subject_scope_mismatch"), ] { let tool = SubjectConsolidationTool { - client: repository_error_client(status, code), + host: crate::subjektiv::test_connection(repository_error_client(status, code)), operation: SubjectConsolidationOperation::DecideCandidate, }; let output = tool @@ -2051,7 +1993,7 @@ permission = "write" #[test] fn subjektiv_consolidation_has_atomic_decision_without_host_scope_fields() { - let tools = workspace_http_subjektiv_consolidation_tools(test_client()); + let tools = subjektiv_consolidation_tools(crate::subjektiv::test_connection(test_client())); let names = tool_names(tools.clone()); assert_eq!( names, diff --git a/crates/worker/src/feature/builtin/memory_lifecycle.rs b/crates/worker/src/feature/builtin/memory_lifecycle.rs index 5635bfe35..13d68eba9 100644 --- a/crates/worker/src/feature/builtin/memory_lifecycle.rs +++ b/crates/worker/src/feature/builtin/memory_lifecycle.rs @@ -101,6 +101,7 @@ struct MemoryLifecycleTask { capture: CommittedSessionCaptureHandle, extensions: SessionExtensionHandle, workspace_client: Arc, + subjektiv_host: Option, manifest: WorkerManifest, client: Box, prompts: Arc>, @@ -175,6 +176,7 @@ impl MemoryLifecycleFeature { capture, extensions, workspace_client, + subjektiv_host: None, manifest, client, prompts, @@ -192,32 +194,34 @@ impl SubjektivLifecycleFeature { config: manifest::ResolvedSubjektivFeatureConfig, capture: CommittedSessionCaptureHandle, extensions: SessionExtensionHandle, - workspace_client: Arc, + host: Option, manifest: WorkerManifest, client: Box, prompts: Arc>, workspace_context: WorkerWorkspaceContext, event_tx: Option>, ) -> std::io::Result> { - if !lifecycle_enabled || !config.execution_enabled() || !config.profile.extraction.enabled { + if !lifecycle_enabled + || !config.profile.enabled + || host.is_none() + || !config.profile.extraction.enabled + { return Ok(None); } config .validate_execution() .map_err(|message| std::io::Error::new(std::io::ErrorKind::InvalidInput, message))?; - if !workspace_client.is_available() || workspace_client.workspace_id().is_none() { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "subjektiv extraction requires Backend Workspace API authority", - )); - } Ok(Some(Self { task: MemoryLifecycleTask { config: LifecycleConfig::from_subjektiv(config), target: ExtractionTarget::Subjektiv, capture, extensions, - workspace_client, + workspace_client: crate::unavailable_workspace_client( + None, + "subjektiv Host has no legacy Memory authority", + ), + subjektiv_host: host, manifest, client, prompts, @@ -477,10 +481,11 @@ impl MemoryLifecycleTask { ), ExtractionTarget::Subjektiv => MemoryStagingOutputState::new_subjektiv( view.clone(), - Arc::clone(&self.workspace_client), + self.subjektiv_host + .clone() + .expect("subjektiv task has Host connection"), source, audit.run_id.to_string(), - capture.session_id.clone(), ), }; let client = if let Some(model) = self.config.extraction.model.as_ref() { @@ -499,6 +504,11 @@ impl MemoryLifecycleTask { .config .workspace_settings() .map(|snapshot| snapshot.language) + .or_else(|| { + self.subjektiv_host + .as_ref() + .map(|host| host.host.settings().language) + }) else { self.record_preparation_failure( &audit, @@ -737,14 +747,25 @@ impl MemoryLifecycleTask { .with_workspace_settings(self.config.workspace_settings.as_ref()); let operation = memory::backend::MemoryConsolidateStagingOperation { force: false }; let request = if subjektiv { - self.workspace_client - .request_subjektiv_memory_staging_consolidation(operation) - .await + let host = self + .subjektiv_host + .as_ref() + .expect("subjektiv task has Host connection"); + host.host + .request_consolidation(&host.context, operation) + .map_err(|error| { + super::memory::WorkspaceMemoryBackendError::Backend(error.to_string()) + }) } else { self.workspace_client .request_memory_staging_consolidation(operation) .await }; + let audit = if subjektiv { + audit.without_legacy_backend() + } else { + audit + }; match request { Ok(output) => { tracing::debug!( @@ -1319,6 +1340,7 @@ permission = "write" capture: capture_handle, extensions, workspace_client, + subjektiv_host: None, manifest: test_manifest(), client, prompts: Arc::new(ArcSwap::from(PromptCatalog::builtins_only().unwrap())), @@ -1379,7 +1401,7 @@ permission = "write" manifest::ResolvedSubjektivFeatureConfig::default(), capture, extensions, - crate::worker::marker_workspace_client(None, "disabled"), + None, test_manifest(), Box::new(ScriptClient::new(Vec::new())), Arc::new(ArcSwap::from(PromptCatalog::builtins_only().unwrap())), @@ -1407,7 +1429,7 @@ permission = "write" config, capture, extensions, - crate::worker::marker_workspace_client(None, "extraction-disabled"), + None, test_manifest(), Box::new(ScriptClient::new(Vec::new())), Arc::new(ArcSwap::from(PromptCatalog::builtins_only().unwrap())), @@ -1444,7 +1466,7 @@ permission = "write" config, capture, extensions, - crate::worker::marker_workspace_client(None, "child-disabled"), + None, test_manifest(), Box::new(ScriptClient::new(Vec::new())), Arc::new(ArcSwap::from(PromptCatalog::builtins_only().unwrap())), @@ -1593,6 +1615,9 @@ permission = "write" workspace_client.clone(), ); task.target = ExtractionTarget::Subjektiv; + task.subjektiv_host = Some(crate::subjektiv::test_connection( + task.workspace_client.clone(), + )); task.config.consolidation_request_enabled = false; run_background_task(task).await; @@ -1647,6 +1672,9 @@ permission = "write" workspace_client.clone(), ); task.target = ExtractionTarget::Subjektiv; + task.subjektiv_host = Some(crate::subjektiv::test_connection( + task.workspace_client.clone(), + )); task.config.consolidation_request_enabled = false; run_background_task(task).await; @@ -1689,6 +1717,9 @@ permission = "write" workspace_client.clone(), ); task.target = ExtractionTarget::Subjektiv; + task.subjektiv_host = Some(crate::subjektiv::test_connection( + task.workspace_client.clone(), + )); task.config.consolidation_request_enabled = false; run_background_task(task).await; @@ -1806,6 +1837,9 @@ permission = "write" workspace_client.clone(), ); task.target = ExtractionTarget::Subjektiv; + task.subjektiv_host = Some(crate::subjektiv::test_connection( + task.workspace_client.clone(), + )); task.config.consolidation_request_enabled = false; let registry = start_background_task(task); tokio::time::timeout(Duration::from_secs(5), async { diff --git a/crates/worker/src/feature/builtin/memory_staging_output.rs b/crates/worker/src/feature/builtin/memory_staging_output.rs index e51939a3a..3896063b0 100644 --- a/crates/worker/src/feature/builtin/memory_staging_output.rs +++ b/crates/worker/src/feature/builtin/memory_staging_output.rs @@ -41,7 +41,9 @@ pub(crate) struct MemoryStagingOutputState { #[derive(Clone)] enum MemoryStagingDestination { WorkspaceMemory, - Subjektiv { session_id: String }, + Subjektiv { + host: crate::subjektiv::SubjektivHostConnection, + }, } impl MemoryStagingOutputState { @@ -64,17 +66,19 @@ impl MemoryStagingOutputState { pub(crate) fn new_subjektiv( view: SessionCapture, - workspace_client: Arc, + host: crate::subjektiv::SubjektivHostConnection, source: SourceRef, extract_run_id: String, - session_id: String, ) -> Self { Self { view: Arc::new(view), - workspace_client, + workspace_client: crate::unavailable_workspace_client( + None, + "subjektiv Host has no legacy Memory authority", + ), source, extract_run_id, - destination: MemoryStagingDestination::Subjektiv { session_id }, + destination: MemoryStagingDestination::Subjektiv { host }, staged: Arc::new(Mutex::new(Vec::new())), finished: Arc::new(Mutex::new(None)), } @@ -263,35 +267,11 @@ impl Tool for StageMemoryCandidateTool { } } } - MemoryStagingDestination::Subjektiv { session_id } => { - let response = self - .state - .workspace_client - .execute_server_operation( - crate::worker::WorkspaceServerOperation::SubjektivStageCandidate( - server_api::SubjektivStageCandidateRequest { - session_id: session_id.clone(), - operation, - }, - ), - ) - .map_err(|error| { - ToolError::ExecutionFailed(format!( - "write subjektiv staging failed: {error}" - )) - })?; - if !response.is_success() { - return Err(ToolError::ExecutionFailed(format!( - "write subjektiv staging returned HTTP {}: {}", - response.status, response.body - ))); - } - let output: server_api::SubjektivStageCandidateResponse = - serde_json::from_str(&response.body).map_err(|error| { - ToolError::ExecutionFailed(format!( - "decode subjektiv staging response: {error}" - )) - })?; + MemoryStagingDestination::Subjektiv { host } => { + let output = host + .host + .stage_candidate(&host.context, operation) + .map_err(ToolError::from)?; vec![output.staging_id] } }; @@ -607,13 +587,16 @@ mod tests { let client = Arc::new(RecordingSubjektivClient::default()); let state = MemoryStagingOutputState::new_subjektiv( SessionCapture::new("segment-1", vec![Item::user_message("durable decision")]), - client.clone(), + { + let mut host = crate::subjektiv::test_connection(client.clone()); + host.context.session_id = "session-committed".into(); + host + }, SourceRef { segment_id: "segment-1".to_string(), range: [0, 0], }, "run-1".to_string(), - "session-committed".to_string(), ); let tool = StageMemoryCandidateTool { state: state.clone(), diff --git a/crates/worker/src/feature/builtin/memory_surface_lifecycle.rs b/crates/worker/src/feature/builtin/memory_surface_lifecycle.rs index 657d9b96e..6f012aedc 100644 --- a/crates/worker/src/feature/builtin/memory_surface_lifecycle.rs +++ b/crates/worker/src/feature/builtin/memory_surface_lifecycle.rs @@ -14,9 +14,7 @@ use crate::feature::background::{ BackgroundTaskCancellation, BackgroundTaskContext, BackgroundTaskSpec, BackgroundTaskTrigger, FeatureBackgroundTask, }; -use crate::feature::builtin::memory::{ - WorkspaceMemoryBackendError, is_builtin_subjektiv_consolidation_profile, -}; +use crate::feature::builtin::memory::is_builtin_subjektiv_consolidation_profile; use crate::feature::builtin::memory_surface_output::{ MemorySurfaceOutputFeature, MemorySurfaceOutputState, submit_llm_tool_definition, }; @@ -29,7 +27,10 @@ use crate::internal_worker::{ InternalWorkerAuthority, InternalWorkerIdentity, InternalWorkerSpec, run_internal_worker_with_cancel_sender, }; -use crate::worker::{WorkerFilesystemAuthority, WorkerWorkspaceContext, WorkspaceClient}; +use crate::subjektiv::{SubjektivHostConnection, SubjektivHostError}; +#[cfg(test)] +use crate::worker::WorkspaceClient; +use crate::worker::{WorkerFilesystemAuthority, WorkerWorkspaceContext}; use manifest::WorkerManifest; const TASK_NAME: &str = "subjektiv-memory-surface"; @@ -39,13 +40,13 @@ const EDITOR_MAX_TURNS: u32 = 3; const EDITOR_QUESTION: &str = "この主体が次の作業を始める際、毎回思い出しておくべきことは何か。継続的な制約、判断の前提、未解決事項、再発を避けたい教訓を、指定予算内でまとめる。"; #[derive(Clone)] -pub(crate) struct SubjektivSurfaceLifecycleFeature { +pub struct SubjektivSurfaceLifecycleFeature { task: SubjektivSurfaceLifecycleTask, } #[derive(Clone)] struct SubjektivSurfaceLifecycleTask { - workspace_client: Arc, + host: SubjektivHostConnection, manifest: WorkerManifest, client: Box, prompts: Arc>, @@ -94,18 +95,20 @@ impl LlmClient for InputBudgetLlmClient { impl SubjektivSurfaceLifecycleFeature { pub(crate) fn from_manifest( lifecycle_enabled: bool, - workspace_client: Arc, + host: Option, manifest: WorkerManifest, client: Box, prompts: Arc>, workspace_context: WorkerWorkspaceContext, ) -> std::io::Result> { - let dedicated = is_builtin_subjektiv_consolidation_profile(&manifest); - if !lifecycle_enabled || !dedicated { + if !lifecycle_enabled + || !is_builtin_subjektiv_consolidation_profile(&manifest) + || host.is_none() + { return Ok(None); } - Self::for_job( - workspace_client, + Self::for_host( + host.expect("checked connection"), manifest, client, prompts, @@ -114,8 +117,9 @@ impl SubjektivSurfaceLifecycleFeature { .map(Some) } - pub(crate) fn for_job( - workspace_client: Arc, + /// Install the reusable surface Feature for an explicitly granted Host. + pub fn for_host( + host: SubjektivHostConnection, manifest: WorkerManifest, client: Box, prompts: Arc>, @@ -125,22 +129,16 @@ impl SubjektivSurfaceLifecycleFeature { .feature .subjektiv .validate_execution() - .map_err(|message| std::io::Error::new(std::io::ErrorKind::InvalidInput, message))?; - if !manifest.feature.subjektiv.execution_enabled() { + .map_err(std::io::Error::other)?; + if !manifest.feature.subjektiv.profile.enabled || !host.host.consolidation_granted() { return Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "subjektiv surface generation requires trusted subject attachment", - )); - } - if !workspace_client.is_available() || workspace_client.workspace_id().is_none() { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "subjektiv surface generation requires Backend Workspace API authority", + std::io::ErrorKind::PermissionDenied, + "surface generation requires explicit Host consolidation grant", )); } Ok(Self { task: SubjektivSurfaceLifecycleTask { - workspace_client, + host, manifest, client, prompts, @@ -150,7 +148,7 @@ impl SubjektivSurfaceLifecycleFeature { } /// The Job tool awaits this clean-context lifecycle before submitting its result. - pub(crate) async fn complete_for_job( + pub async fn complete_for_job( &self, cancellation: BackgroundTaskCancellation, reusable_surface: Option<&serde_json::Value>, @@ -234,7 +232,7 @@ impl SubjektivSurfaceLifecycleTask { )); } let generation = self - .workspace_client + .host .prepare_subjektiv_memory_surface() .await .map_err(surface_hook_error)?; @@ -299,7 +297,7 @@ impl SubjektivSurfaceLifecycleTask { )); } match self - .workspace_client + .host .publish_subjektiv_memory_surface(server_api::SubjektivSurfacePublishRequest { generation_id: generation.generation_id.clone(), points, @@ -312,9 +310,8 @@ impl SubjektivSurfaceLifecycleTask { "store_revision": output.built_from_store_revision, "snapshot_id": output.snapshot_id, })); } - Err(WorkspaceMemoryBackendError::Http { status, .. }) - if status == reqwest::StatusCode::CONFLICT - && attempt + 1 < MAX_GENERATION_ATTEMPTS => + Err(SubjektivHostError::Conflict { .. }) + if attempt + 1 < MAX_GENERATION_ATTEMPTS => { continue; } @@ -334,18 +331,7 @@ impl SubjektivSurfaceLifecycleTask { generation: &server_api::SubjektivSurfacePrepareResponse, cancellation: BackgroundTaskCancellation, ) -> Result, SurfaceEditorFailure> { - let language = self - .manifest - .feature - .subjektiv - .workspace_settings() - .map(|settings| settings.language) - .ok_or_else(|| { - SurfaceEditorFailure::editor(HookError::new( - HookErrorCategory::Internal, - "surface editor requires bound subjektiv Workspace settings", - )) - })?; + let language = self.host.host.settings().language; let system_prompt = self .prompts .load_full() @@ -424,7 +410,7 @@ impl SubjektivSurfaceLifecycleTask { job_confirmation_required: bool, ) -> Result { let failure = self - .workspace_client + .host .fail_subjektiv_memory_surface(server_api::SubjektivSurfaceFailureRequest { generation_id: generation.generation_id.clone(), reason_code: reason_code.to_string(), @@ -586,7 +572,7 @@ fn estimated_editor_request_tokens(request: &Request) -> Result HookError { +fn surface_hook_error(error: SubjektivHostError) -> HookError { HookError::new(HookErrorCategory::Internal, error.to_string()) } @@ -889,7 +875,7 @@ permission = "write" calls: Arc, ) -> SubjektivSurfaceLifecycleTask { SubjektivSurfaceLifecycleTask { - workspace_client, + host: crate::subjektiv::test_connection(workspace_client), manifest: test_manifest(), client: Box::new(SurfaceEditorClient { calls }), prompts: Arc::new(ArcSwap::from(PromptCatalog::builtins_only().unwrap())), diff --git a/crates/worker/src/feature/builtin/subjektiv_memory.rs b/crates/worker/src/feature/builtin/subjektiv_memory.rs index 73331e4d3..a0b752a01 100644 --- a/crates/worker/src/feature/builtin/subjektiv_memory.rs +++ b/crates/worker/src/feature/builtin/subjektiv_memory.rs @@ -13,7 +13,7 @@ use memory::extract::CandidateKind; use schemars::JsonSchema; use serde::Deserialize; use server_api::{ - SubjektivMemoryBackendOperation, SubjektivMemoryBackendRequest, SubjektivMemoryBackendResponse, + SubjektivMemoryBackendOperation, SubjektivMemoryBackendResponse, SubjektivMemoryListRevisionsRequest, SubjektivMemoryQueryRequest, SubjektivMemoryReadRequest, SubjektivMemoryReceiptStatus, SubjektivMemoryReceiptStatusRequest, SubjektivMemoryRevisionIntent, SubjektivMemoryRevisionProposal, @@ -33,7 +33,11 @@ use crate::hook::{ PreRequestContext, RunCommitted, RunCommittedContext, }; use crate::session_capture::{SessionCapture, SessionEntryEvidence}; -use crate::worker::{WorkspaceClient, WorkspaceServerOperation}; +use crate::subjektiv::SubjektivHostConnection; +#[cfg(test)] +use crate::worker::WorkspaceClient; +#[cfg(test)] +use server_api::SubjektivMemoryBackendRequest; const QUERY_TOOL: &str = "SubjektivMemoryQuery"; const READ_TOOL: &str = "SubjektivMemoryRead"; @@ -58,7 +62,7 @@ pub(crate) struct SubjektivMemoryFeature { #[derive(Clone)] struct SubjektivMemoryState { - client: Arc, + host: SubjektivHostConnection, capture: CommittedSessionCaptureHandle, } @@ -76,22 +80,17 @@ impl SubjektivMemoryFeature { pub(crate) fn from_resolved_config( config: &manifest::ResolvedSubjektivFeatureConfig, capture: CommittedSessionCaptureHandle, - client: Arc, + host: Option, ) -> std::io::Result> { - if !config.execution_enabled() { + if !config.profile.enabled || host.is_none() { return Ok(None); } config .validate_execution() .map_err(|message| std::io::Error::new(std::io::ErrorKind::InvalidInput, message))?; - if !client.is_available() || client.workspace_id().is_none() { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "subjektiv Memory tools require Backend Workspace API authority", - )); - } + let host = host.expect("checked explicit connection"); Ok(Some(Self { - state: SubjektivMemoryState { client, capture }, + state: SubjektivMemoryState { host, capture }, })) } } @@ -483,48 +482,7 @@ impl SubjektivMemoryState { &self, operation: SubjektivMemoryBackendOperation, ) -> Result { - let response = self - .client - .execute_server_operation(WorkspaceServerOperation::SubjektivMemory( - SubjektivMemoryBackendRequest { operation }, - )) - .map_err(|error| ToolError::ExecutionFailed(error.to_string()))?; - if !response.is_success() { - let parsed = - serde_json::from_str::(&response.body).ok(); - if let Some((code, message)) = parsed.as_ref().and_then(|error| { - error - .diagnostics - .iter() - .find(|diagnostic| { - matches!( - diagnostic.code.as_str(), - "revision_conflict" | "stale_cursor" - ) - }) - .map(|diagnostic| (diagnostic.code.as_str(), error.message.as_str())) - }) { - return Err(ToolError::StructuredConflict { - code: code.to_string(), - message: message.to_string(), - }); - } - let detail = parsed - .map(|error| error.message) - .unwrap_or_else(|| response.body.clone()); - let message = format!( - "subjektiv Memory backend returned HTTP {}: {detail}", - response.status - ); - return if matches!(response.status, 400 | 404 | 409 | 422) { - Err(ToolError::InvalidArgument(message)) - } else { - Err(ToolError::ExecutionFailed(message)) - }; - } - serde_json::from_str(&response.body).map_err(|error| { - ToolError::ExecutionFailed(format!("decode subjektiv response: {error}")) - }) + self.host.memory(operation).map_err(Into::into) } fn backend_receipt_status( @@ -628,7 +586,7 @@ struct PendingRetryHook { #[async_trait] impl Hook for PendingRetryHook { async fn call(&self, _input: &PreRequestContext) -> Result { - replay_pending_committed_memory(&self.state)?; + replay_pending_committed_memory_with_policy(&self.state, true)?; Ok(HookPreRequestAction::Continue) } } @@ -649,6 +607,12 @@ impl Hook for PendingRewriteHook { } fn replay_pending_committed_memory(state: &SubjektivMemoryState) -> Result<(), HookError> { + replay_pending_committed_memory_with_policy(state, false) +} +fn replay_pending_committed_memory_with_policy( + state: &SubjektivMemoryState, + allow_pending: bool, +) -> Result<(), HookError> { let capture = state .capture .capture() @@ -691,7 +655,11 @@ fn replay_pending_committed_memory(state: &SubjektivMemoryState) -> Result<(), H vec![call.evidence], item.proposal, ) { - failures.push(format!("{receipt_id}: {error}")); + if !allow_pending + || !matches!(&error,ToolError::StructuredConflict {code,..} if code=="pending_commit") + { + failures.push(format!("{receipt_id}: {error}")); + } } } if failures.is_empty() { @@ -1109,7 +1077,7 @@ mod tests { }); SubjektivMemoryFeature { state: SubjektivMemoryState { - client: Arc::new(TestWorkspaceClient), + host: crate::subjektiv::test_connection(Arc::new(TestWorkspaceClient)), capture, }, } @@ -1245,7 +1213,7 @@ mod tests { "\\\u{0000}".repeat(server_api::SUBJEKTIV_MEMORY_READ_MAX_TOOL_CONTENT_BYTES); let tool = SubjektivReadTool { state: SubjektivMemoryState { - client: Arc::new(ReadWorkspaceClient(response)), + host: crate::subjektiv::test_connection(Arc::new(ReadWorkspaceClient(response))), capture: feature().state.capture, }, operation: ReadOperation::Read, @@ -1265,7 +1233,9 @@ mod tests { let oversized_tool = SubjektivReadTool { state: SubjektivMemoryState { - client: Arc::new(ReadWorkspaceClient(oversized_response)), + host: crate::subjektiv::test_connection(Arc::new(ReadWorkspaceClient( + oversized_response, + ))), capture: feature().state.capture, }, operation: ReadOperation::Read, @@ -1285,7 +1255,9 @@ mod tests { for code in ["revision_conflict", "stale_cursor"] { let tool = SubjektivReadTool { state: SubjektivMemoryState { - client: Arc::new(ConflictWorkspaceClient(code)), + host: crate::subjektiv::test_connection(Arc::new(ConflictWorkspaceClient( + code, + ))), capture: feature().state.capture, }, operation: ReadOperation::Query, @@ -1386,7 +1358,7 @@ mod tests { }); PendingCommitHook { state: SubjektivMemoryState { - client: client.clone(), + host: crate::subjektiv::test_connection(client.clone()), capture, }, } @@ -1439,7 +1411,7 @@ mod tests { }); let hook = PendingCommitHook { state: SubjektivMemoryState { - client: client.clone(), + host: crate::subjektiv::test_connection(client.clone()), capture, }, }; @@ -1515,7 +1487,7 @@ mod tests { }) }); let state = SubjektivMemoryState { - client: client.clone(), + host: crate::subjektiv::test_connection(client.clone()), capture, }; assert_eq!( diff --git a/crates/worker/src/feature/builtin/subjektiv_session.rs b/crates/worker/src/feature/builtin/subjektiv_session.rs index b7ee46256..3d257d680 100644 --- a/crates/worker/src/feature/builtin/subjektiv_session.rs +++ b/crates/worker/src/feature/builtin/subjektiv_session.rs @@ -11,16 +11,20 @@ use async_trait::async_trait; use schemars::JsonSchema; use server_api::{ SUBJEKTIV_SESSION_MAX_TOOL_CONTENT_BYTES, SubjektivSessionBackendOperation, - SubjektivSessionBackendRequest, SubjektivSessionBackendResponse, SubjektivSessionBackendResult, - SubjektivSessionDiagnosticCode, SubjektivSessionErrorResponse, SubjektivSessionListRequest, - SubjektivSessionReadRequest, SubjektivSessionSearchRequest, + SubjektivSessionBackendResponse, SubjektivSessionBackendResult, SubjektivSessionDiagnosticCode, + SubjektivSessionErrorResponse, SubjektivSessionListRequest, SubjektivSessionReadRequest, + SubjektivSessionSearchRequest, }; use crate::feature::{ FeatureDescriptor, FeatureInstallContext, FeatureInstallError, FeatureModule, ToolContribution, ToolDeclaration, }; -use crate::worker::{WorkspaceClient, WorkspaceServerOperation}; +use crate::subjektiv::SubjektivHostConnection; +#[cfg(test)] +use crate::worker::WorkspaceClient; +#[cfg(test)] +use server_api::SubjektivSessionBackendRequest; const LIST_TOOL: &str = "SubjektivSessionList"; const SEARCH_TOOL: &str = "SubjektivSessionSearch"; @@ -37,28 +41,23 @@ pub(crate) struct SubjektivSessionFeature { #[derive(Clone)] struct SubjektivSessionState { - client: Arc, + host: SubjektivHostConnection, } impl SubjektivSessionFeature { pub(crate) fn from_resolved_config( config: &manifest::ResolvedSubjektivFeatureConfig, - client: Arc, + host: Option, ) -> std::io::Result> { - if !config.execution_enabled() { + if !config.profile.enabled || host.is_none() { return Ok(None); } config .validate_execution() .map_err(|message| std::io::Error::new(std::io::ErrorKind::InvalidInput, message))?; - if !client.is_available() || client.workspace_id().is_none() { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "subjektiv Session tools require Backend Workspace API authority", - )); - } + let host = host.expect("checked explicit connection"); Ok(Some(Self { - state: SubjektivSessionState { client }, + state: SubjektivSessionState { host }, })) } } @@ -154,25 +153,7 @@ impl SubjektivSessionState { &self, operation: SubjektivSessionBackendOperation, ) -> Result { - let response = self - .client - .execute_server_operation(WorkspaceServerOperation::SubjektivSession( - SubjektivSessionBackendRequest { operation }, - )) - .map_err(|error| ToolError::ExecutionFailed(error.to_string()))?; - - if let Ok(response) = - serde_json::from_str::(&response.body) - { - return Ok(response); - } - let detail = serde_json::from_str::(&response.body) - .map(|error| error.message) - .unwrap_or(response.body); - Err(ToolError::ExecutionFailed(format!( - "subjektiv Session backend returned HTTP {}: {detail}", - response.status - ))) + self.host.session(operation).map_err(Into::into) } } @@ -293,7 +274,9 @@ mod tests { } fn state(client: Arc) -> SubjektivSessionState { - SubjektivSessionState { client } + SubjektivSessionState { + host: crate::subjektiv::test_connection(client), + } } fn feature(client: Arc) -> SubjektivSessionFeature { @@ -363,7 +346,9 @@ mod tests { let config = manifest::ResolvedSubjektivFeatureConfig::default(); let result = SubjektivSessionFeature::from_resolved_config( &config, - Arc::new(SessionToolClient::new(list_response())), + Some(crate::subjektiv::test_connection(Arc::new( + SessionToolClient::new(list_response()), + ))), ) .unwrap(); assert!(result.is_none()); diff --git a/crates/worker/src/job.rs b/crates/worker/src/job.rs index afae9a29f..1788f54dc 100644 --- a/crates/worker/src/job.rs +++ b/crates/worker/src/job.rs @@ -8,7 +8,7 @@ //! cannot be preempted by dropping its tool future and must itself be bounded. mod result; -pub use result::{JobResultFeature, JobResultSink, SUBMIT_JOB_RESULT_TOOL}; +pub use result::{JobResultFeature, JobResultFinalizer, JobResultSink, SUBMIT_JOB_RESULT_TOOL}; use std::borrow::Cow; use std::path::Path; @@ -72,6 +72,28 @@ pub struct PreparedInternalJob { manifest: WorkerManifest, client: Box, system_prompt: String, + features: FeatureRegistryBuilder, + finalizer: Option>, +} + +/// Explicit Host-supplied domain modules and the Profile requirements they satisfy. +/// Profile names never issue this grant. The Host must bind all module operations to +/// the immutable request/attempt and fence them on cancellation, timeout and shutdown. +#[derive(Default)] +pub struct JobFeatureGrant { + pub features: FeatureRegistryBuilder, + pub satisfied_requirements: Vec<&'static str>, + pub finalizer: Option>, +} + +pub fn prepare_job_with_grant( + request: &::job::JobRequest, + cwd: &Path, + client: Option>, + grant: JobFeatureGrant, +) -> Result { + let registry = ProfileDiscovery::user_settings().discover()?; + prepare_job_from_registry_with_grant(request, cwd, client, ®istry, grant) } /// Resolve exactly the requested registry Profile using the existing user settings registry. @@ -91,6 +113,16 @@ fn prepare_job_from_registry( cwd: &Path, client: Option>, registry: &manifest::ProfileRegistry, +) -> Result { + prepare_job_from_registry_with_grant(request, cwd, client, registry, JobFeatureGrant::default()) +} + +fn prepare_job_from_registry_with_grant( + request: &::job::JobRequest, + cwd: &Path, + client: Option>, + registry: &manifest::ProfileRegistry, + grant: JobFeatureGrant, ) -> Result { request.validate()?; let resolved = ProfileResolver::new() @@ -101,7 +133,16 @@ fn prepare_job_from_registry( ProfileResolveOptions::with_worker_name("internal-job"), )?; let mut manifest = resolved.manifest; - validate_result_only_profile(&manifest)?; + validate_profile_with_grant(&manifest, &grant.satisfied_requirements)?; + if let Some(finalizer) = &grant.finalizer { + finalizer + .validate_profile(&manifest) + .map_err(JobExecutionError::UnsupportedProfile)?; + } + let mut tool_names = vec![SUBMIT_JOB_RESULT_TOOL.to_string()]; + for descriptor in grant.features.descriptors() { + tool_names.extend(descriptor.tools.iter().map(|tool| tool.name.clone())); + } let scope = Scope::empty(); let prompts = PromptCatalog::builtins_only()?; let template = SystemPromptTemplate::parse( @@ -113,7 +154,7 @@ fn prepare_job_from_registry( cwd: Cow::Borrowed("(no filesystem)"), language: &manifest.engine.language, scope: &scope, - tool_names: vec![SUBMIT_JOB_RESULT_TOOL.into()], + tool_names, feature_instructions: &[], agents_md: None, resident_summary: None, @@ -148,6 +189,8 @@ fn prepare_job_from_registry( manifest, client, system_prompt, + features: grant.features, + finalizer: grant.finalizer, }) } @@ -165,7 +208,11 @@ impl PreparedInternalJob { where F: FnOnce(tokio::sync::mpsc::Sender<()>), { - let feature = JobResultFeature::new(&self.request, attempt_id.clone(), submit)?; + let mut feature = JobResultFeature::new(&self.request, attempt_id.clone(), submit)?; + if let Some(finalizer) = self.finalizer { + feature = + feature.with_finalizer(finalizer, self.manifest.clone(), self.client.clone_boxed()); + } let accepted = feature.accepted(); let engine_policy = self.manifest.engine.clone(); let spec = InternalWorkerSpec { @@ -182,7 +229,7 @@ impl PreparedInternalJob { system_prompt: self.system_prompt, input: self.request.worker_input(&attempt_id)?, cache_key: None, - features: FeatureRegistryBuilder::new().with_module(feature.clone()), + features: self.features.with_module(feature.clone()), required_tools: &[SUBMIT_JOB_RESULT_TOOL], authority: InternalWorkerAuthority { workspace: WorkerWorkspaceContext::no_workspace(), @@ -218,7 +265,11 @@ impl PreparedInternalJob { } /// Validate requirements, not Profile names. Nothing configured as active is silently stripped. -fn validate_result_only_profile(manifest: &WorkerManifest) -> Result<(), JobExecutionError> { + +fn validate_profile_with_grant( + manifest: &WorkerManifest, + satisfied: &[&str], +) -> Result<(), JobExecutionError> { let manifest::FeatureConfig { task, memory, @@ -306,7 +357,7 @@ fn validate_result_only_profile(manifest: &WorkerManifest) -> Result<(), JobExec manifest.session.record_event_trace, ), ] { - if enabled { + if enabled && !satisfied.contains(&name) { unavailable.push(name); } } diff --git a/crates/worker/src/job/result.rs b/crates/worker/src/job/result.rs index edb10712b..ee3fedb30 100644 --- a/crates/worker/src/job/result.rs +++ b/crates/worker/src/job/result.rs @@ -23,6 +23,29 @@ pub trait JobResultSink: Send + Sync { fn submit(&self, submission: ::job::JobResultSubmission) -> Result<(), String>; } +/// Consumer-owned asynchronous completion before durable result acceptance. +/// The generic runner owns no domain algorithms. This runs in the result tool, +/// never an orphaned background task, with the selected policy/model and a +/// cancellation token that must propagate into any child resource cleanup. +#[async_trait] +pub trait JobResultFinalizer: Send + Sync { + fn validate_profile(&self, manifest: &manifest::WorkerManifest) -> Result<(), String>; + async fn finalize( + &self, + result: Value, + manifest: manifest::WorkerManifest, + client: Box, + cancellation: crate::feature::background::BackgroundTaskCancellation, + ) -> Result; +} + +#[derive(Clone)] +struct FinalizerContext { + finalizer: Arc, + manifest: manifest::WorkerManifest, + client: Box, +} + #[derive(Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] struct SubmitJobResultInput { @@ -32,6 +55,7 @@ struct SubmitJobResultInput { #[derive(Default)] struct SubmissionState { sealed: Option, + prepared: Option, unconfirmed: Option, } @@ -46,6 +70,9 @@ pub struct JobResultFeature { state: Arc>, accepted: Arc>>, cancelled: Arc>>, + serial: Arc>, + cancellation: crate::feature::background::BackgroundTaskCancellation, + finalizer: Option, } impl JobResultFeature { @@ -65,9 +92,26 @@ impl JobResultFeature { state: Arc::default(), accepted: Arc::default(), cancelled: Arc::default(), + serial: Arc::default(), + cancellation: Default::default(), + finalizer: None, }) } + pub fn with_finalizer( + mut self, + finalizer: Arc, + manifest: manifest::WorkerManifest, + client: Box, + ) -> Self { + self.finalizer = Some(FinalizerContext { + finalizer, + manifest, + client, + }); + self + } + pub(super) fn accepted(&self) -> Arc>> { self.accepted.clone() } @@ -107,6 +151,44 @@ impl Tool for JobResultFeature { .map_err(|error| ToolError::InvalidArgument(error.to_string()))?; ::job::result_digest(&input.result, self.max_result_bytes) .map_err(|error| ToolError::InvalidArgument(error.to_string()))?; + let _serial = self.serial.lock().await; + if self.cancellation.is_cancelled() { + return Err(ToolError::Cancelled(ToolOutput { + summary: "Job result submission cancelled".into(), + content: None, + attachments: vec![], + })); + } + let cached = { + let state = self.state.lock().expect("Job submission state poisoned"); + if state + .sealed + .as_ref() + .is_some_and(|sealed| sealed != &input.result) + { + return Err(ToolError::InvalidArgument( + "Job result is sealed; retry only the identical result".into(), + )); + } + state.prepared.clone() + }; + let prepared = if let Some(value) = cached { + value + } else if let Some(finalizer) = &self.finalizer { + finalizer + .finalizer + .finalize( + input.result.clone(), + finalizer.manifest.clone(), + finalizer.client.clone_boxed(), + self.cancellation.clone(), + ) + .await? + } else { + input.result.clone() + }; + ::job::result_digest(&prepared, self.max_result_bytes) + .map_err(|e| ToolError::InvalidArgument(e.to_string()))?; let mut state = self.state.lock().expect("Job submission state poisoned"); if self .cancelled @@ -129,12 +211,13 @@ impl Tool for JobResultFeature { } else { // Seal before dispatch: a sink error can follow durable Host acceptance. state.sealed = Some(input.result.clone()); + state.prepared = Some(prepared.clone()); } let submission = ::job::JobResultSubmission { job_id: self.job_id.clone(), attempt_id: self.attempt_id.clone(), input_revision: self.input_revision.clone(), - result: input.result, + result: prepared, }; let mut accepted = self.accepted.lock().expect("Job result state poisoned"); if accepted.is_none() { @@ -156,6 +239,7 @@ impl Tool for JobResultFeature { } async fn cancel_execution(&self, context: &ToolExecutionContext) -> Result<(), ToolError> { + self.cancellation.cancel(); self.cancelled .lock() .expect("Job cancellation state poisoned") diff --git a/crates/worker/src/job/tests.rs b/crates/worker/src/job/tests.rs index 2135e1b80..3c0c66ec6 100644 --- a/crates/worker/src/job/tests.rs +++ b/crates/worker/src/job/tests.rs @@ -1,14 +1,21 @@ //! Scripted in-process model executions, not provider/process tests. use super::*; +use crate::feature::background::BackgroundTaskCancellation; +use crate::feature::{ + FeatureDescriptor, FeatureInstallContext, FeatureInstallError, FeatureModule, ToolContribution, + ToolDeclaration, +}; use agen::llm_client::event::{Event, ResponseStatus, StatusEvent}; use agen::llm_client::{ClientError, Request}; use agen::tool::{Tool, ToolExecutionContext}; +use agen::tool::{ToolError, ToolOutput}; use async_trait::async_trait; use futures::Stream; use serde_json::{Value, json}; use std::collections::VecDeque; use std::pin::Pin; use std::sync::Mutex; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; #[derive(Clone)] struct ScriptedClient { @@ -592,6 +599,458 @@ fn unknown_prompt_and_model_are_not_substituted_even_with_injected_client() { } } +// A domain capability is issued by the Host, never by the selected Profile. +#[derive(Clone, Default)] +struct GrantedTool(Arc); +#[async_trait] +impl Tool for GrantedTool { + async fn execute(&self, _: &str, _: ToolExecutionContext) -> Result { + self.0.fetch_add(1, Ordering::SeqCst); + Ok(ToolOutput { + summary: "granted".into(), + content: None, + attachments: vec![], + }) + } +} +impl FeatureModule for GrantedTool { + fn descriptor(&self) -> FeatureDescriptor { + FeatureDescriptor::builtin("test-job-domain", "Test domain").with_tool( + ToolDeclaration::new("GrantedDomainTool", "Explicit Host capability"), + ) + } + fn install(&self, context: &mut FeatureInstallContext<'_>) -> Result<(), FeatureInstallError> { + let tool = self.clone(); + context.tools().register(ToolContribution::new( + "GrantedDomainTool", + Arc::new(move || { + ( + agen::tool::ToolMeta::new("GrantedDomainTool"), + Arc::new(tool.clone()) as Arc, + ) + }), + )) + } +} + +#[tokio::test] +async fn domain_grant_is_explicit_not_profile_derived_and_preserves_selected_policy() { + let root = tempfile::tempdir().unwrap(); + let mut profile = artifact("internal.flow_verifier_system", 100000, 1000); + profile["feature"] = json!({"task":{"enabled":true}}); + let registry = registry(root.path(), profile.clone(), profile); + let marker = GrantedTool::default(); + for selector in ["user:a", "user:b", "default"] { + assert!(matches!( + prepare_job_from_registry( + &request(selector), + root.path(), + Some(Box::new(ScriptedClient::new(vec![]))), + ®istry, + ), + Err(JobExecutionError::UnsupportedProfile(_)) + )); + let client = ScriptedClient::new(vec![ + call("GrantedDomainTool", json!({})), + call(SUBMIT_JOB_RESULT_TOOL, json!({"result":true})), + prose(), + ]); + let job = prepare_job_from_registry_with_grant( + &request(selector), + root.path(), + Some(Box::new(client.clone())), + ®istry, + JobFeatureGrant { + features: FeatureRegistryBuilder::new().with_module(marker.clone()), + satisfied_requirements: vec!["feature.task"], + finalizer: None, + }, + ) + .unwrap(); + assert_eq!( + job.manifest.model.model_id.as_deref(), + Some("scripted-model") + ); + assert_eq!( + job.manifest.engine.instruction, + "internal.flow_verifier_system" + ); + assert_eq!(job.manifest.engine.max_turns.unwrap().get(), MAX_JOB_TURNS); + assert_eq!(job.manifest.engine.max_tokens, Some(MAX_JOB_TOKENS)); + assert!(job.manifest.scope.allow.is_empty()); + assert!(job.manifest.delegation_scope.allow.is_empty()); + job.run("attempt".into(), Arc::new(RecordingSink::default()), |_| {}) + .await + .unwrap(); + for sent in client.requests.lock().unwrap().iter() { + let mut names = sent + .tools + .iter() + .map(|tool| tool.name.as_str()) + .collect::>(); + names.sort(); + assert_eq!(names, ["GrantedDomainTool", SUBMIT_JOB_RESULT_TOOL]); + assert_eq!(sent.config.max_tokens, Some(MAX_JOB_TOKENS)); + } + } + assert_eq!(marker.0.load(Ordering::SeqCst), 3); +} + +#[test] +fn domain_grant_does_not_satisfy_unrelated_profile_requirements() { + let root = tempfile::tempdir().unwrap(); + for (key, value, needle) in [ + ( + "feature", + json!({"task":{"enabled":true},"web":{"enabled":true}}), + "feature.web", + ), + ("scope", json!("workspace_read"), "scope"), + ( + "delegation_scope", + json!("workspace_write"), + "delegation_scope", + ), + ] { + let mut profile = artifact("default", 512, 3); + profile[key] = value; + let registry = registry(root.path(), profile.clone(), profile); + let error = prepare_job_from_registry_with_grant( + &request("user:a"), + root.path(), + Some(Box::new(ScriptedClient::new(vec![]))), + ®istry, + JobFeatureGrant { + features: FeatureRegistryBuilder::new().with_module(GrantedTool::default()), + satisfied_requirements: vec!["feature.task"], + finalizer: None, + }, + ) + .err() + .unwrap(); + assert!( + matches!(error, JobExecutionError::UnsupportedProfile(_)), + "{error}" + ); + assert!(error.to_string().contains(needle), "{error}"); + } +} + +#[derive(Default)] +struct ControlledFinalizer { + calls: AtomicUsize, + active: Arc, + manifests: Mutex>, + entered: tokio::sync::Notify, + release: tokio::sync::Notify, + cancellation_seen: tokio::sync::Notify, + cleanup: tokio::sync::Notify, + cancelled: AtomicBool, + gated: bool, + // Reusing a captured request with the supplied client proves transport identity. + probe: Option>>>, + reject_profile: bool, +} +struct FinalizerResource(Arc); +impl Drop for FinalizerResource { + fn drop(&mut self) { + self.0.fetch_sub(1, Ordering::SeqCst); + } +} +#[async_trait] +impl JobResultFinalizer for ControlledFinalizer { + fn validate_profile(&self, _: &manifest::WorkerManifest) -> Result<(), String> { + if self.reject_profile { + Err("test finalizer refuses selected policy".into()) + } else { + Ok(()) + } + } + async fn finalize( + &self, + result: Value, + manifest: manifest::WorkerManifest, + client: Box, + cancellation: BackgroundTaskCancellation, + ) -> Result { + self.calls.fetch_add(1, Ordering::SeqCst); + self.active.fetch_add(1, Ordering::SeqCst); + let _resource = FinalizerResource(self.active.clone()); + self.manifests.lock().unwrap().push(manifest); + if let Some(requests) = &self.probe { + let request = requests.lock().unwrap()[0].clone(); + let events = client + .stream(request) + .await + .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?; + let _: Vec<_> = futures::StreamExt::collect(events).await; + } + self.entered.notify_one(); + if self.gated { + tokio::select! { + _ = self.release.notified() => {}, + _ = cancellation.cancelled() => { + self.cancelled.store(true, Ordering::SeqCst); + self.cancellation_seen.notify_one(); + self.cleanup.notified().await; + return Err(ToolError::Cancelled(ToolOutput { + summary: "finalizer cleaned up".into(), content: None, attachments: vec![], + })); + } + } + } + Ok(json!({"prepared":result,"completion":self.calls.load(Ordering::SeqCst)})) + } +} +async fn signalled(notify: &tokio::sync::Notify) { + tokio::time::timeout(std::time::Duration::from_secs(2), notify.notified()) + .await + .unwrap(); +} + +#[tokio::test] +async fn result_finalization_is_awaited_with_selected_model_policy_and_no_orphan_resource() { + let root = tempfile::tempdir().unwrap(); + let registry = registry( + root.path(), + artifact("default", 777, 4), + artifact("default", 512, 3), + ); + let client = ScriptedClient::new(vec![ + call(SUBMIT_JOB_RESULT_TOOL, json!({"result":{"draft":true}})), + prose(), + prose(), + ]); + let finalizer = Arc::new(ControlledFinalizer { + gated: true, + probe: Some(client.requests.clone()), + ..Default::default() + }); + let job = prepare_job_from_registry_with_grant( + &request("user:a"), + root.path(), + Some(Box::new(client.clone())), + ®istry, + JobFeatureGrant { + finalizer: Some(finalizer.clone()), + ..Default::default() + }, + ) + .unwrap(); + let expected = serde_json::to_value(&job.manifest).unwrap(); + let sink = Arc::new(RecordingSink::default()); + let recording = sink.clone(); + let (tx, rx) = tokio::sync::oneshot::channel(); + let run = tokio::spawn(async move { + job.run("attempt".into(), recording, move |sender| { + let _ = tx.send(sender); + }) + .await + }); + let sender = rx.await.unwrap(); + signalled(&finalizer.entered).await; + assert!(!run.is_finished()); + assert!( + sink.0.lock().unwrap().is_empty(), + "sink cannot accept an unfinished result" + ); + assert_eq!(finalizer.active.load(Ordering::SeqCst), 1); + assert_eq!( + serde_json::to_value(&finalizer.manifests.lock().unwrap()[0]).unwrap(), + expected + ); + assert_eq!( + client.requests.lock().unwrap().len(), + 2, + "finalizer uses the injected selected transport" + ); + finalizer.release.notify_one(); + let accepted = tokio::time::timeout(std::time::Duration::from_secs(2), run) + .await + .unwrap() + .unwrap() + .unwrap(); + assert_eq!( + accepted.submission.result, + json!({"prepared":{"draft":true},"completion":1}) + ); + assert_eq!(sink.0.lock().unwrap().as_slice(), &[accepted.submission]); + assert_eq!(finalizer.active.load(Ordering::SeqCst), 0); + assert_eq!(finalizer.calls.load(Ordering::SeqCst), 1); + assert!(sender.is_closed()); +} + +#[tokio::test] +async fn cancellation_reaches_finalizer_and_runner_waits_for_cleanup_without_acceptance() { + let root = tempfile::tempdir().unwrap(); + let registry = registry( + root.path(), + artifact("default", 512, 3), + artifact("default", 512, 3), + ); + let finalizer = Arc::new(ControlledFinalizer { + gated: true, + ..Default::default() + }); + let job = prepare_job_from_registry_with_grant( + &request("user:a"), + root.path(), + Some(Box::new(ScriptedClient::new(vec![ + call(SUBMIT_JOB_RESULT_TOOL, json!({"result":true})), + prose(), + ]))), + ®istry, + JobFeatureGrant { + finalizer: Some(finalizer.clone()), + ..Default::default() + }, + ) + .unwrap(); + let sink = Arc::new(RecordingSink::default()); + let recording = sink.clone(); + let (tx, rx) = tokio::sync::oneshot::channel(); + let run = tokio::spawn(async move { + job.run("attempt".into(), recording, move |sender| { + let _ = tx.send(sender); + }) + .await + }); + let sender = rx.await.unwrap(); + signalled(&finalizer.entered).await; + assert!(sink.0.lock().unwrap().is_empty()); + sender.send(()).await.unwrap(); + signalled(&finalizer.cancellation_seen).await; + assert!(finalizer.cancelled.load(Ordering::SeqCst)); + assert!( + !run.is_finished(), + "cancellation must await consumer resource cleanup" + ); + assert_eq!(finalizer.active.load(Ordering::SeqCst), 1); + assert!(sink.0.lock().unwrap().is_empty()); + finalizer.cleanup.notify_one(); + let outcome = tokio::time::timeout(std::time::Duration::from_secs(2), run) + .await + .unwrap() + .unwrap(); + assert!( + matches!(outcome, Err(JobExecutionError::Cancelled)), + "{outcome:?}" + ); + assert!(sink.0.lock().unwrap().is_empty()); + assert_eq!(finalizer.active.load(Ordering::SeqCst), 0); + assert!(sender.is_closed()); +} + +#[test] +fn finalizer_can_refuse_selected_profile_before_execution() { + let root = tempfile::tempdir().unwrap(); + let registry = registry( + root.path(), + artifact("default", 512, 3), + artifact("default", 512, 3), + ); + let finalizer = Arc::new(ControlledFinalizer { + reject_profile: true, + ..Default::default() + }); + let client = ScriptedClient::new(vec![]); + let error = prepare_job_from_registry_with_grant( + &request("user:a"), + root.path(), + Some(Box::new(client.clone())), + ®istry, + JobFeatureGrant { + finalizer: Some(finalizer.clone()), + ..Default::default() + }, + ) + .err() + .unwrap(); + assert!(matches!(error, JobExecutionError::UnsupportedProfile(_))); + assert!( + error + .to_string() + .contains("test finalizer refuses selected policy") + ); + assert!(client.requests.lock().unwrap().is_empty()); + assert_eq!(finalizer.calls.load(Ordering::SeqCst), 0); +} + +#[derive(Default)] +struct UncertainThenAcceptedSink(Mutex>); +impl JobResultSink for UncertainThenAcceptedSink { + fn submit(&self, submission: ::job::JobResultSubmission) -> Result<(), String> { + let mut submissions = self.0.lock().unwrap(); + submissions.push(submission); + if submissions.len() == 1 { + Err("durable acceptance is uncertain".into()) + } else { + Ok(()) + } + } +} + +#[tokio::test] +async fn exact_uncertain_sink_retry_reuses_prepared_outcome_without_rerunning_finalizer() { + let root = tempfile::tempdir().unwrap(); + let job = prepared(root.path(), &request("user:a"), ScriptedClient::new(vec![])); + let finalizer = Arc::new(ControlledFinalizer::default()); + let sink = Arc::new(UncertainThenAcceptedSink::default()); + let feature = JobResultFeature::new(&request("user:a"), "attempt".into(), sink.clone()) + .unwrap() + .with_finalizer(finalizer.clone(), job.manifest, job.client); + assert!( + feature + .execute( + r#"{"result":{"draft":true}}"#, + ToolExecutionContext::direct() + ) + .await + .is_err() + ); + assert!(feature.unconfirmed().is_some()); + assert!(feature.accepted().lock().unwrap().is_none()); + assert_eq!(finalizer.calls.load(Ordering::SeqCst), 1); + assert!( + feature + .execute( + r#"{"result":{"draft":false}}"#, + ToolExecutionContext::direct() + ) + .await + .is_err() + ); + assert_eq!(sink.0.lock().unwrap().len(), 1); + feature + .execute( + r#"{"result":{"draft":true}}"#, + ToolExecutionContext::direct(), + ) + .await + .unwrap(); + feature + .execute( + r#"{"result":{"draft":true}}"#, + ToolExecutionContext::direct(), + ) + .await + .unwrap(); + assert!(feature.unconfirmed().is_none()); + let submissions = sink.0.lock().unwrap(); + assert_eq!(submissions.len(), 2); + assert_eq!(submissions[0], submissions[1]); + assert_eq!( + submissions[0].result, + json!({"prepared":{"draft":true},"completion":1}) + ); + assert_eq!( + feature.accepted().lock().unwrap().as_ref(), + Some(&submissions[1]) + ); + assert_eq!(finalizer.calls.load(Ordering::SeqCst), 1); + assert_eq!(finalizer.active.load(Ordering::SeqCst), 0); +} + struct UnknownSink(Mutex>); impl JobResultSink for UnknownSink { fn submit(&self, value: ::job::JobResultSubmission) -> Result<(), String> { diff --git a/crates/worker/src/lib.rs b/crates/worker/src/lib.rs index 07daef4e2..069938d1f 100644 --- a/crates/worker/src/lib.rs +++ b/crates/worker/src/lib.rs @@ -25,6 +25,7 @@ pub mod shared_state; mod shutdown_after_idle; pub mod skill; pub mod spawn; +pub mod subjektiv; pub mod wip; mod internal_worker; diff --git a/crates/worker/src/session_capture.rs b/crates/worker/src/session_capture.rs index ad7a25adf..570c2b7c7 100644 --- a/crates/worker/src/session_capture.rs +++ b/crates/worker/src/session_capture.rs @@ -712,7 +712,9 @@ impl SessionCapture { fn public_provenance(origin: &WorkerHistoryProvenance) -> SessionEntryProvenance { match origin { - WorkerHistoryProvenance::HumanInput { .. } => SessionEntryProvenance::HumanInput, + WorkerHistoryProvenance::HumanInput { .. } | WorkerHistoryProvenance::LocalHumanInput => { + SessionEntryProvenance::HumanInput + } WorkerHistoryProvenance::WorkerInput { .. } => SessionEntryProvenance::WorkerInput, WorkerHistoryProvenance::FlowInstruction { .. } => SessionEntryProvenance::FlowInstruction, WorkerHistoryProvenance::BackendInstruction { .. } => { diff --git a/crates/worker/src/subjektiv.rs b/crates/worker/src/subjektiv.rs new file mode 100644 index 000000000..3a3c31d5a --- /dev/null +++ b/crates/worker/src/subjektiv.rs @@ -0,0 +1,700 @@ +//! Explicit Host capability for subject-scoped Memory and committed Sessions. +//! +//! A manifest is policy, not a connection. Hosts bind Subject/storage authority +//! in their implementation; the Worker supplies its own Worker/Session context. +//! Neither Subject identity nor filesystem/Workspace authority is model input. + +pub use crate::feature::builtin::memory::SubjektivConsolidationToolsFeature; +pub use crate::feature::builtin::memory_surface_lifecycle::SubjektivSurfaceLifecycleFeature; + +use std::sync::Arc; +use std::time::Duration; + +use agen::tool::ToolError; +use memory::backend::{ + MemoryConsolidateStagingOperation, MemoryConsolidationOutput, MemoryStageCandidateOperation, +}; +use server_api::*; + +use crate::{WorkspaceClient, WorkspaceRequest, WorkspaceRequestMethod, WorkspaceServerOperation}; + +/// Host-issued execution context, never deserialized from model arguments. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SubjektivHostContext { + pub worker_id: String, + pub session_id: String, +} + +/// Explicit local configuration. This is not a trusted Workspace snapshot and +/// is not persisted in the reusable Worker manifest. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SubjektivHostSettings { + pub language: String, +} + +#[derive(Debug, thiserror::Error)] +pub enum SubjektivHostError { + #[error("{message}")] + Conflict { code: String, message: String }, + #[error("{0}")] + InvalidArgument(String), + #[error("{0}")] + Unavailable(String), +} + +impl From for ToolError { + fn from(error: SubjektivHostError) -> Self { + match error { + SubjektivHostError::Conflict { code, message } => { + Self::StructuredConflict { code, message } + } + SubjektivHostError::InvalidArgument(message) => Self::InvalidArgument(message), + SubjektivHostError::Unavailable(message) => Self::ExecutionFailed(message), + } + } +} + +/// Object-safe capability implemented by Backend and standalone Hosts. +/// Implementations must enforce their bound Subject, Worker/Session authority, +/// committed evidence and (for Jobs) immutable batch attenuation. Knowing an +/// identifier is not a grant. No method grants ambient filesystem access. +pub trait SubjektivHost: Send + Sync + 'static { + fn settings(&self) -> SubjektivHostSettings; + /// Separate explicit grant for decision/surface execution; Profile policy + /// alone must never authorize consolidation tools. + fn consolidation_granted(&self) -> bool { + false + } + /// Optional real Backend identity, used only for prompt provenance. + fn workspace_id(&self) -> Option { + None + } + fn memory( + &self, + context: &SubjektivHostContext, + operation: SubjektivMemoryBackendOperation, + ) -> Result; + fn session( + &self, + context: &SubjektivHostContext, + operation: SubjektivSessionBackendOperation, + ) -> Result; + fn stage_candidate( + &self, + context: &SubjektivHostContext, + operation: MemoryStageCandidateOperation, + ) -> Result; + fn record_session( + &self, + context: &SubjektivHostContext, + create_if_missing: bool, + ) -> Result; + fn request_consolidation( + &self, + context: &SubjektivHostContext, + operation: MemoryConsolidateStagingOperation, + ) -> Result; +} + +/// Backend adapter. Existing authenticated Workspace operations and snapshot +/// validation remain the authority; no local Workspace is synthesized. +pub struct BackendSubjektivHost { + client: Arc, + settings: SubjektivHostSettings, + workspace_id: String, + consolidation_granted: bool, +} + +impl BackendSubjektivHost { + pub fn from_resolved_config( + config: &manifest::ResolvedSubjektivFeatureConfig, + client: Arc, + ) -> std::io::Result>> { + config.validate_execution().map_err(std::io::Error::other)?; + if !config.execution_enabled() { + return Ok(None); + } + let settings = config + .workspace_settings() + .expect("execution requires trusted settings"); + if !client.is_available() || client.workspace_id() != Some(settings.workspace_id.as_str()) { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "subjektiv requires available Backend authority matching trusted Workspace settings", + )); + } + Ok(Some(Arc::new(Self { + client, + workspace_id: settings.workspace_id, + settings: SubjektivHostSettings { + language: settings.language, + }, + consolidation_granted: config.profile.consolidation_tools, + }))) + } + + fn execute( + &self, + operation: WorkspaceServerOperation, + ) -> Result { + let response = self + .client + .execute_server_operation(operation) + .map_err(|error| SubjektivHostError::Unavailable(error.to_string()))?; + decode(response) + } +} + +fn decode( + response: crate::WorkspaceResponse, +) -> Result { + if !response.is_success() { + let parsed = serde_json::from_str::(&response.body).ok(); + let message = parsed + .as_ref() + .map(|error| error.message.clone()) + .unwrap_or(response.body); + if let Some(code) = parsed.as_ref().and_then(|error| { + error + .diagnostics + .iter() + .find(|diagnostic| { + matches!( + diagnostic.code.as_str(), + "revision_conflict" + | "stale_cursor" + | "candidate_decision_conflict" + | "subject_scope_mismatch" + ) + }) + .map(|diagnostic| diagnostic.code.clone()) + }) { + return Err(SubjektivHostError::Conflict { code, message }); + } + return Err(if response.status == 409 { + SubjektivHostError::Conflict { + code: "conflict".into(), + message, + } + } else if matches!(response.status, 400 | 404 | 422) { + SubjektivHostError::InvalidArgument(message) + } else { + SubjektivHostError::Unavailable(message) + }); + } + serde_json::from_str(&response.body).map_err(|error| { + SubjektivHostError::Unavailable(format!("decode subjektiv response: {error}")) + }) +} + +impl SubjektivHost for BackendSubjektivHost { + fn settings(&self) -> SubjektivHostSettings { + SubjektivHostSettings { + language: self.settings.language.clone(), + } + } + fn workspace_id(&self) -> Option { + Some(self.workspace_id.clone()) + } + fn consolidation_granted(&self) -> bool { + self.consolidation_granted + } + fn memory( + &self, + _context: &SubjektivHostContext, + operation: SubjektivMemoryBackendOperation, + ) -> Result { + // Preserve the bounded resident-context request timeout. + if matches!( + operation, + SubjektivMemoryBackendOperation::ResidentContext(_) + ) { + let request = WorkspaceRequest::json( + WorkspaceRequestMethod::Post, + format!("/api/w/{}/subjektiv/memory", self.workspace_id), + serde_json::to_string(&SubjektivMemoryBackendRequest { operation }) + .map_err(|error| SubjektivHostError::Unavailable(error.to_string()))?, + ); + return decode( + self.client + .execute_with_timeout(request, Duration::from_secs(5)) + .map_err(|error| SubjektivHostError::Unavailable(error.to_string()))?, + ); + } + self.execute(WorkspaceServerOperation::SubjektivMemory( + SubjektivMemoryBackendRequest { operation }, + )) + } + fn session( + &self, + _context: &SubjektivHostContext, + operation: SubjektivSessionBackendOperation, + ) -> Result { + let response = self + .client + .execute_server_operation(WorkspaceServerOperation::SubjektivSession( + SubjektivSessionBackendRequest { operation }, + )) + .map_err(|error| SubjektivHostError::Unavailable(error.to_string()))?; + // Session domain diagnostics are typed even on non-2xx responses. + if let Ok(value) = serde_json::from_str(&response.body) { + return Ok(value); + } + decode(response) + } + fn stage_candidate( + &self, + context: &SubjektivHostContext, + operation: MemoryStageCandidateOperation, + ) -> Result { + self.execute(WorkspaceServerOperation::SubjektivStageCandidate( + SubjektivStageCandidateRequest { + session_id: context.session_id.clone(), + operation, + }, + )) + } + fn record_session( + &self, + context: &SubjektivHostContext, + create_if_missing: bool, + ) -> Result { + self.execute(WorkspaceServerOperation::SubjektivRecordSession( + SubjektivRecordSessionRequest { + session_id: context.session_id.clone(), + create_if_missing, + }, + )) + } + fn request_consolidation( + &self, + _context: &SubjektivHostContext, + operation: MemoryConsolidateStagingOperation, + ) -> Result { + let response = self + .client + .execute(WorkspaceRequest::json( + WorkspaceRequestMethod::Post, + format!("/api/w/{}/subjektiv/consolidation", self.workspace_id), + serde_json::to_string(&MemoryConsolidateStagingRequest { + force: operation.force, + }) + .map_err(|error| SubjektivHostError::Unavailable(error.to_string()))?, + )) + .map_err(|error| SubjektivHostError::Unavailable(error.to_string()))?; + let response: MemoryConsolidationResponse = decode(response)?; + Ok(MemoryConsolidationOutput { + status: response.status, + summary: response.summary, + candidate_count: response.candidate_count, + total_bytes: response.total_bytes, + }) + } +} + +/// Live connection with Worker-supplied authority. Not serializable and never +/// inherited by an ordinary SubWorker. +#[derive(Clone)] +pub struct SubjektivHostConnection { + pub host: Arc, + pub context: SubjektivHostContext, +} + +impl SubjektivHostConnection { + pub fn new( + host: Arc, + context: SubjektivHostContext, + ) -> std::io::Result { + let language = host.settings().language; + if context.worker_id.trim().is_empty() + || context.session_id.trim().is_empty() + || context.worker_id.chars().any(char::is_control) + || context.session_id.chars().any(char::is_control) + || !manifest::is_normalized_workspace_memory_language(&language) + { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "invalid subjektiv Host context/settings", + )); + } + Ok(Self { host, context }) + } + pub fn memory( + &self, + operation: SubjektivMemoryBackendOperation, + ) -> Result { + if let SubjektivMemoryBackendOperation::StageExplicit(input) = &operation + && input.session_id != self.context.session_id + { + return Err(SubjektivHostError::InvalidArgument( + "explicit staging targets a foreign Session".into(), + )); + } + if matches!( + operation, + SubjektivMemoryBackendOperation::DecideCandidate(_) + | SubjektivMemoryBackendOperation::PrepareSurface(_) + | SubjektivMemoryBackendOperation::PublishSurface(_) + | SubjektivMemoryBackendOperation::FailSurface(_) + ) && !self.host.consolidation_granted() + { + return Err(SubjektivHostError::InvalidArgument( + "Host did not grant consolidation".into(), + )); + } + self.host.memory(&self.context, operation) + } + pub fn session( + &self, + operation: SubjektivSessionBackendOperation, + ) -> Result { + self.host.session(&self.context, operation) + } + pub async fn prepare_subjektiv_memory_surface( + &self, + ) -> Result { + match self.memory(SubjektivMemoryBackendOperation::PrepareSurface( + SubjektivSurfacePrepareRequest {}, + ))? { + SubjektivMemoryBackendResponse::SurfacePrepared(value) => Ok(value), + _ => Err(SubjektivHostError::Unavailable( + "unexpected surface preparation response".into(), + )), + } + } + pub async fn publish_subjektiv_memory_surface( + &self, + input: SubjektivSurfacePublishRequest, + ) -> Result { + match self.memory(SubjektivMemoryBackendOperation::PublishSurface(input))? { + SubjektivMemoryBackendResponse::SurfacePublished(value) => Ok(value), + _ => Err(SubjektivHostError::Unavailable( + "unexpected surface publication response".into(), + )), + } + } + pub async fn fail_subjektiv_memory_surface( + &self, + input: SubjektivSurfaceFailureRequest, + ) -> Result { + match self.memory(SubjektivMemoryBackendOperation::FailSurface(input))? { + SubjektivMemoryBackendResponse::SurfaceFailed(value) => Ok(value), + _ => Err(SubjektivHostError::Unavailable( + "unexpected surface failure response".into(), + )), + } + } +} + +#[cfg(test)] +pub(crate) fn test_connection(client: Arc) -> SubjektivHostConnection { + let mut config = manifest::ResolvedSubjektivFeatureConfig::default(); + config.profile.enabled = true; + config.profile.consolidation_tools = true; + config + .bind_workspace_settings(manifest::WorkspaceMemorySettingsSnapshot { + workspace_id: client.workspace_id().unwrap().to_string(), + settings_revision: 1, + language: "English".into(), + }) + .unwrap(); + SubjektivHostConnection::new( + BackendSubjektivHost::from_resolved_config(&config, client) + .unwrap() + .unwrap(), + SubjektivHostContext { + worker_id: "worker-test".into(), + session_id: "session-1".into(), + }, + ) + .unwrap() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + Method, WorkerBootstrap, WorkerBootstrapLayout, WorkerControllerTransport, + WorkerFilesystemAuthority, WorkerManifest, WorkerWorkspaceContext, + }; + use agen::llm_client::event::{Event as LlmEvent, ResponseStatus, StatusEvent}; + use agen::llm_client::{ClientError, LlmClient, Request}; + use async_trait::async_trait; + use futures_util::Stream; + use session_store::{CombinedStore, FsStore, FsWorkerStore, WorkerMetadataStore}; + use std::pin::Pin; + use std::sync::Mutex; + + #[derive(Default)] + struct LocalHost { + contexts: Mutex>, + attributed: Mutex>, + } + impl SubjektivHost for LocalHost { + fn settings(&self) -> SubjektivHostSettings { + SubjektivHostSettings { + language: "English".into(), + } + } + fn memory( + &self, + context: &SubjektivHostContext, + operation: SubjektivMemoryBackendOperation, + ) -> Result { + self.contexts.lock().unwrap().push(context.clone()); + match operation { + SubjektivMemoryBackendOperation::ResidentContext(_) => { + Ok(SubjektivMemoryBackendResponse::ResidentContext( + SubjektivResidentContextOutput { + behavior_md: "Explicit local subject behavior.".into(), + behavior_revision: 1, + memory_surface: memory::backend::MemoryResidentSummaryOutput { + availability: + memory::backend::MemoryResidentSummaryAvailability::Ungenerated, + content: None, + }, + }, + )) + } + _ => Err(SubjektivHostError::Unavailable( + "not granted by test Host".into(), + )), + } + } + fn session( + &self, + _: &SubjektivHostContext, + _: SubjektivSessionBackendOperation, + ) -> Result { + Err(SubjektivHostError::Unavailable("not used".into())) + } + fn stage_candidate( + &self, + _: &SubjektivHostContext, + _: MemoryStageCandidateOperation, + ) -> Result { + Err(SubjektivHostError::Unavailable("not used".into())) + } + fn record_session( + &self, + context: &SubjektivHostContext, + create_if_missing: bool, + ) -> Result { + self.attributed + .lock() + .unwrap() + .push((context.clone(), create_if_missing)); + Ok(SubjektivRecordSessionResponse { + subject_id: "test-owned-local-subject".into(), + session_id: context.session_id.clone(), + }) + } + fn request_consolidation( + &self, + _: &SubjektivHostContext, + _: MemoryConsolidateStagingOperation, + ) -> Result { + Err(SubjektivHostError::Unavailable("no candidates".into())) + } + } + + #[derive(Clone)] + struct ScriptedClient(tokio::sync::mpsc::UnboundedSender); + #[async_trait] + impl LlmClient for ScriptedClient { + fn clone_boxed(&self) -> Box { + Box::new(self.clone()) + } + async fn stream( + &self, + request: Request, + ) -> Result> + Send>>, ClientError> + { + self.0.send(request).unwrap(); + Ok(Box::pin(futures_util::stream::iter(vec![ + Ok(LlmEvent::text_block_start(0)), + Ok(LlmEvent::text_delta(0, "Done.")), + Ok(LlmEvent::text_block_stop(0, None)), + Ok(LlmEvent::Status(StatusEvent { + status: ResponseStatus::Completed, + })), + ]))) + } + } + + async fn launch_case(enabled: bool, attach: bool) { + let temp = tempfile::tempdir().unwrap(); + let _runtime_sandbox = + crate::runtime::worker_allocation::test_util::RuntimeDirSandbox::new(temp.path()); + let name = format!("subjektiv-host-{}", uuid::Uuid::now_v7()); + let mut manifest = WorkerManifest::from_toml(&format!( + r#" +[worker] +name = "{name}" +[model] +scheme = "anthropic" +model_id = "scripted-model" +[engine] +[scope] +allow = [] +[feature.task] +enabled = true +"# + )) + .unwrap(); + manifest.feature.subjektiv.profile.enabled = enabled; + manifest.feature.subjektiv.profile.extraction.enabled = true; + manifest.feature.subjektiv.profile.extraction.threshold = Some(u64::MAX); + assert!(manifest.feature.subjektiv.workspace_settings.is_none()); + let store = CombinedStore::new( + FsStore::new(temp.path().join("sessions")).unwrap(), + FsWorkerStore::new(temp.path().join("workers")).unwrap(), + ); + let host = Arc::new(LocalHost::default()); + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); + let bootstrap = WorkerBootstrap::new( + manifest, + store.clone(), + crate::PromptCatalogSource::builtins_only(), + WorkerWorkspaceContext::local_filesystem(None), + WorkerFilesystemAuthority::None, + WorkerBootstrapLayout::Direct { + runtime_base: temp.path().join("runtime"), + bash_output_dir: temp.path().join("bash-output"), + }, + WorkerControllerTransport::InProcess, + ) + .with_model_client(ScriptedClient(tx)); + let bootstrap = if attach { + bootstrap.with_subjektiv_host(host.clone()) + } else { + bootstrap + }; + let prepared = bootstrap.prepare().await.unwrap(); + let expected = SubjektivHostContext { + worker_id: name.clone(), + session_id: prepared.worker().session_id().to_string(), + }; + assert!( + prepared + .worker() + .manifest() + .feature + .subjektiv + .workspace_settings + .is_none() + ); + assert!(prepared.worker().workspace_id().is_none()); + assert!( + prepared + .worker() + .workspace_client() + .workspace_id() + .is_none() + ); + let controller = prepared.start().await.unwrap(); + controller + .handle + .send(Method::submit_text( + protocol::new_submission_request_id(), + "Hello", + )) + .await + .unwrap(); + let request = tokio::time::timeout(Duration::from_secs(5), rx.recv()) + .await + .unwrap() + .unwrap(); + let tools = request + .tools + .iter() + .map(|tool| tool.name.as_str()) + .collect::>(); + assert!( + tools.contains(&"TaskCreate"), + "ordinary Features must survive" + ); + for name in [ + "SubjektivMemoryQuery", + "SubjektivMemoryRead", + "SubjektivMemoryRemember", + "SubjektivSessionList", + "SubjektivSessionRead", + ] { + assert_eq!(tools.contains(&name), enabled && attach, "{name}"); + } + assert!(!tools.contains(&"MemoryApplyCandidate")); + let rendered = format!( + "{}\n{}", + request.system_prompt.as_deref().unwrap_or_default(), + request + .items + .iter() + .filter_map(|item| item.as_text()) + .collect::>() + .join("\n") + ); + assert_eq!( + rendered.contains("Explicit local subject behavior."), + enabled && attach + ); + if enabled && attach { + assert_eq!( + *host.attributed.lock().unwrap(), + vec![(expected.clone(), true)] + ); + assert!( + host.contexts + .lock() + .unwrap() + .iter() + .all(|context| context == &expected) + ); + let metadata = store.read_by_name(&name).unwrap().unwrap(); + assert!(matches!( + metadata.subjektiv_session_attribution, + Some(session_store::SubjektivSessionAttributionState::Confirmed { .. }) + )); + } else { + assert!(host.contexts.lock().unwrap().is_empty()); + assert!(host.attributed.lock().unwrap().is_empty()); + } + controller + .handle + .send(Method::Shutdown { + command: protocol::WorkerCommandEnvelope::new(1), + }) + .await + .unwrap(); + tokio::time::timeout(Duration::from_secs(5), controller.shutdown) + .await + .unwrap() + .unwrap(); + controller.controller_task.await.unwrap(); + } + + #[tokio::test] + async fn explicit_local_host_installs_ordinary_features_without_workspace_snapshot() { + launch_case(true, true).await; + } + #[tokio::test] + async fn enabled_policy_without_host_is_inert() { + launch_case(true, false).await; + } + #[tokio::test] + async fn disabled_policy_with_host_has_no_subjektiv_side_effects() { + launch_case(false, true).await; + } + #[test] + fn consolidation_installation_requires_separate_explicit_host_grant() { + let connection = SubjektivHostConnection::new( + Arc::new(LocalHost::default()), + SubjektivHostContext { + worker_id: "test-worker".into(), + session_id: "test-session".into(), + }, + ) + .unwrap(); + assert!(SubjektivConsolidationToolsFeature::new(connection).is_err()); + } +} diff --git a/crates/worker/src/worker.rs b/crates/worker/src/worker.rs index e3dbde07d..a4bae5f46 100644 --- a/crates/worker/src/worker.rs +++ b/crates/worker/src/worker.rs @@ -357,6 +357,9 @@ pub(crate) fn authenticated_input_provenance( ) -> WorkerHistoryProvenance { match source { protocol::AuthenticatedInputSource::UntrustedWire => WorkerHistoryProvenance::LegacyUnknown, + protocol::AuthenticatedInputSource::LocalOperator => { + WorkerHistoryProvenance::LocalHumanInput + } protocol::AuthenticatedInputSource::Account { account_id } => { WorkerHistoryProvenance::HumanInput { account_id: account_id.clone(), @@ -2752,6 +2755,7 @@ pub struct Worker { inject_resident_summary: bool, /// Deferred resident prompt source installed by an enabled Feature. feature_resident_summary_source: Option>, + subjektiv_host: Option>, /// One-shot generic Feature lifecycle notification required after restore. restore_feature_lifecycle_pending: bool, /// Complete system prompt replacement installed by an enabled Feature. @@ -3014,6 +3018,7 @@ impl Worker { prompts, inject_resident_summary: true, feature_resident_summary_source: None, + subjektiv_host: None, restore_feature_lifecycle_pending: false, feature_system_prompt_override: None, user_segments: Vec::new(), @@ -3233,6 +3238,49 @@ impl Worker { self.workspace_context.client_handle() } + /// Bind a live capability before Feature installation. This is deliberately + /// separate from Workspace authority and is not inherited by SubWorkers. + pub fn bind_subjektiv_host(&mut self, host: Option>) { + self.subjektiv_host = host; + } + + pub fn subjektiv_host_connection( + &self, + ) -> std::io::Result> { + use crate::subjektiv::{ + BackendSubjektivHost, SubjektivHostConnection, SubjektivHostContext, + }; + let config = &self.manifest.feature.subjektiv; + config.validate_execution().map_err(std::io::Error::other)?; + if !config.profile.enabled { + return Ok(None); + } + let host = if let Some(host) = &self.subjektiv_host { + if config.workspace_settings.is_some() + || self.workspace_id().is_some() + || self.workspace_client().workspace_id().is_some() + { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "explicit local subjektiv Host cannot replace trusted Backend Workspace authority", + )); + } + Some(host.clone()) + } else { + BackendSubjektivHost::from_resolved_config(config, self.workspace_client_handle())? + }; + host.map(|host| { + SubjektivHostConnection::new( + host, + SubjektivHostContext { + worker_id: self.manifest.worker.name.clone(), + session_id: self.session_id().to_string(), + }, + ) + }) + .transpose() + } + /// Bind the host-owned Worker-session observation projection. The provider /// is responsible for workspace authorization and per-capture revalidation. pub fn bind_worker_observation_provider( @@ -7324,6 +7372,7 @@ where prompts: common.prompts, inject_resident_summary: true, feature_resident_summary_source: None, + subjektiv_host: None, restore_feature_lifecycle_pending: false, feature_system_prompt_override: None, user_segments: Vec::new(), @@ -7433,64 +7482,62 @@ where create_if_missing, initial_diagnostic.to_string(), ))?; - let response = match self.workspace_client().execute_server_operation( - WorkspaceServerOperation::SubjektivRecordSession( - server_api::SubjektivRecordSessionRequest { - session_id: session_id.to_string(), - create_if_missing, - }, - ), - ) { - Ok(response) => response, - Err(error) => { - let message = bounded_attribution_diagnostic(format!( - "Session attribution transport outcome is unknown: {error}" - )); - self.persist_subjektiv_session_attribution( - incomplete_subjektiv_attribution_state( - session_id, - create_if_missing, - message.clone(), - ), - )?; - return Err(WorkerError::SubjektivSessionAttribution { - state: incomplete_subjektiv_attribution_label(create_if_missing), - message, - }); - } - }; - if !response.is_success() { - let message = bounded_attribution_diagnostic(format!( - "Session attribution was rejected with HTTP {}: {}", - response.status, response.body - )); - let state = if create_if_missing { - SubjektivSessionAttributionState::Failed { - session_id, - diagnostic: message.clone(), - } - } else { - SubjektivSessionAttributionState::LegacyUnknown { - session_id, - diagnostic: message.clone(), - } - }; - self.persist_subjektiv_session_attribution(state)?; - return Err(WorkerError::SubjektivSessionAttribution { - state: if create_if_missing { - "failed" - } else { - "legacy_unknown" - }, - message, - }); - } - let output: server_api::SubjektivRecordSessionResponse = - match serde_json::from_str(&response.body) { + let output = if self.subjektiv_host.is_some() { + let connection = self + .subjektiv_host_connection() + .map_err(|error| WorkerError::SubjektivSessionAttribution { + state: "failed", + message: error.to_string(), + })? + .ok_or_else(|| WorkerError::SubjektivSessionAttribution { + state: "failed", + message: "Session attribution requires enabled explicit Host capability".into(), + })?; + match connection + .host + .record_session(&connection.context, create_if_missing) + { Ok(output) => output, + Err(error) => { + let message = bounded_attribution_diagnostic(error.to_string()); + let rejected = + !matches!(error, crate::subjektiv::SubjektivHostError::Unavailable(_)); + let state = if create_if_missing && rejected { + SubjektivSessionAttributionState::Failed { + session_id, + diagnostic: message.clone(), + } + } else { + incomplete_subjektiv_attribution_state( + session_id, + create_if_missing, + message.clone(), + ) + }; + self.persist_subjektiv_session_attribution(state)?; + return Err(WorkerError::SubjektivSessionAttribution { + state: if create_if_missing && rejected { + "failed" + } else { + incomplete_subjektiv_attribution_label(create_if_missing) + }, + message, + }); + } + } + } else { + let response = match self.workspace_client().execute_server_operation( + WorkspaceServerOperation::SubjektivRecordSession( + server_api::SubjektivRecordSessionRequest { + session_id: session_id.to_string(), + create_if_missing, + }, + ), + ) { + Ok(response) => response, Err(error) => { let message = bounded_attribution_diagnostic(format!( - "Session attribution response outcome is unknown: {error}" + "Session attribution transport outcome is unknown: {error}" )); self.persist_subjektiv_session_attribution( incomplete_subjektiv_attribution_state( @@ -7505,6 +7552,54 @@ where }); } }; + if !response.is_success() { + let message = bounded_attribution_diagnostic(format!( + "Session attribution was rejected with HTTP {}: {}", + response.status, response.body + )); + let state = if create_if_missing { + SubjektivSessionAttributionState::Failed { + session_id, + diagnostic: message.clone(), + } + } else { + SubjektivSessionAttributionState::LegacyUnknown { + session_id, + diagnostic: message.clone(), + } + }; + self.persist_subjektiv_session_attribution(state)?; + return Err(WorkerError::SubjektivSessionAttribution { + state: if create_if_missing { + "failed" + } else { + "legacy_unknown" + }, + message, + }); + } + let output: server_api::SubjektivRecordSessionResponse = + match serde_json::from_str(&response.body) { + Ok(output) => output, + Err(error) => { + let message = bounded_attribution_diagnostic(format!( + "Session attribution response outcome is unknown: {error}" + )); + self.persist_subjektiv_session_attribution( + incomplete_subjektiv_attribution_state( + session_id, + create_if_missing, + message.clone(), + ), + )?; + return Err(WorkerError::SubjektivSessionAttribution { + state: incomplete_subjektiv_attribution_label(create_if_missing), + message, + }); + } + }; + output + }; if output.session_id != session_id.to_string() || output.subject_id.trim().is_empty() || output.subject_id.chars().any(char::is_control) @@ -7616,6 +7711,7 @@ where prompts: common.prompts, inject_resident_summary: true, feature_resident_summary_source: None, + subjektiv_host: None, restore_feature_lifecycle_pending: false, feature_system_prompt_override: None, user_segments: Vec::new(), @@ -7744,6 +7840,7 @@ where prompts: common.prompts, inject_resident_summary: true, feature_resident_summary_source: None, + subjektiv_host: None, restore_feature_lifecycle_pending: false, feature_system_prompt_override: None, user_segments: Vec::new(), @@ -8130,6 +8227,7 @@ where prompts: common.prompts, inject_resident_summary: true, feature_resident_summary_source: None, + subjektiv_host: None, restore_feature_lifecycle_pending: true, feature_system_prompt_override: None, user_segments: state.user_segments, @@ -9852,7 +9950,7 @@ mod build_summary_prompt_tests { }); let source = crate::feature::builtin::memory::ordinary_subjektiv_resident_summary_source( manifest, - client, + Some(crate::subjektiv::test_connection(client)), Arc::new(ArcSwap::from(PromptCatalog::builtins_only().unwrap())), ) .unwrap() @@ -14731,12 +14829,14 @@ permission = "write" let resident_source = || { crate::feature::builtin::memory::ordinary_subjektiv_resident_summary_source( &manifest, - Arc::new(RestoreSubjectResidentClient { - availability: memory::backend::MemoryResidentSummaryAvailability::Ready, - content: Some("literal /prepare()".into()), - load_count: Arc::default(), - behavior_revision: Some(behavior_revision.clone()), - }), + Some(crate::subjektiv::test_connection(Arc::new( + RestoreSubjectResidentClient { + availability: memory::backend::MemoryResidentSummaryAvailability::Ready, + content: Some("literal /prepare()".into()), + load_count: Arc::default(), + behavior_revision: Some(behavior_revision.clone()), + }, + ))), Arc::new(ArcSwap::from(PromptCatalog::builtins_only().unwrap())), ) .unwrap() diff --git a/crates/workspace-server/Cargo.toml b/crates/workspace-server/Cargo.toml index d89f9e161..25e7fc27b 100644 --- a/crates/workspace-server/Cargo.toml +++ b/crates/workspace-server/Cargo.toml @@ -16,6 +16,8 @@ axum = { workspace = true, features = ["ws"] } chrono = { version = "0.4", default-features = false, features = ["clock"] } futures.workspace = true config-source.workspace = true +feature-storage.workspace = true +subjektiv.workspace = true flow = { path = "../flow" } manifest.workspace = true job.workspace = true diff --git a/crates/workspace-server/src/feature_storage.rs b/crates/workspace-server/src/feature_storage.rs index d523233fd..b4386fd07 100644 --- a/crates/workspace-server/src/feature_storage.rs +++ b/crates/workspace-server/src/feature_storage.rs @@ -1,1385 +1,2 @@ -//! Server-owned SQLite storage for trusted Workspace Features. -//! -//! The server owns database placement, connection configuration, migrations, and -//! operational lifecycle. A Feature owns its schema and only receives a scoped -//! [`FeatureDatabase`] after trusted registration; Worker/model inputs never -//! select a path or receive a SQL surface. - -use std::collections::{HashMap, HashSet}; -use std::fs; -use std::path::{Path, PathBuf}; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex, Weak}; -use std::time::Duration; - -use rusqlite::backup::Backup; -use rusqlite::{Connection, ErrorCode, OpenFlags, Transaction, TransactionBehavior, params}; -use uuid::Uuid; - -const BUSY_TIMEOUT: Duration = Duration::from_secs(5); -const MIGRATION_TABLE: &str = "__yoi_feature_schema_migrations"; -const SNAPSHOT_FORMAT: &str = "yoi-feature-storage-v1"; -const SNAPSHOT_MANIFEST: &str = "manifest.txt"; -const MAX_IDENTIFIER_BYTES: usize = 128; - -pub type Result = std::result::Result; -pub type FeatureMigrationApply = for<'connection> fn(&Transaction<'connection>) -> Result<()>; - -#[derive(Debug, thiserror::Error)] -pub enum FeatureStorageError { - #[error("invalid {kind} storage identifier `{value}`")] - InvalidIdentifier { kind: &'static str, value: String }, - #[error("Feature `{0}` is not registered with this Server storage manager")] - FeatureNotRegistered(String), - #[error("Feature `{0}` is already registered with this Server storage manager")] - FeatureAlreadyRegistered(String), - #[error("invalid migration plan for Feature `{feature}`: {detail}")] - InvalidMigrationPlan { feature: String, detail: String }, - #[error("Feature registration belongs to a different Server storage manager")] - ForeignRegistration, - #[error("Feature storage is shutting down")] - ShuttingDown, - #[error("Feature storage for Workspace `{0}` is undergoing maintenance")] - WorkspaceMaintenance(String), - #[error("Feature database is closed")] - DatabaseClosed, - #[error( - "Feature `{feature}` database schema version {actual} is newer than supported version {supported}" - )] - UnsupportedSchema { - feature: String, - actual: u32, - supported: u32, - }, - #[error("Feature `{feature}` migration history is incompatible: {detail}")] - MigrationHistory { feature: String, detail: String }, - #[error("Feature `{feature}` migration {version} (`{name}`) failed: {detail}")] - MigrationFailed { - feature: String, - version: u32, - name: String, - detail: String, - }, - #[error("Feature storage snapshot is invalid: {0}")] - InvalidSnapshot(String), - #[error("Feature storage destination already exists: {0}")] - DestinationExists(String), - #[error("Feature storage operation failed: {0}")] - Operation(String), - #[error("feature storage I/O error: {0}")] - Io(#[from] std::io::Error), - #[error("feature storage SQLite error: {0}")] - Sqlite(#[from] rusqlite::Error), -} - -impl FeatureStorageError { - /// Lets a Feature reject a domain operation while retaining transaction rollback. - pub fn operation(message: impl Into) -> Self { - Self::Operation(message.into()) - } -} - -#[derive(Clone, Copy)] -pub struct FeatureMigration { - version: u32, - name: &'static str, - apply: FeatureMigrationApply, -} - -impl FeatureMigration { - pub const fn new(version: u32, name: &'static str, apply: FeatureMigrationApply) -> Self { - Self { - version, - name, - apply, - } - } - - pub fn version(&self) -> u32 { - self.version - } - - pub fn name(&self) -> &'static str { - self.name - } -} - -#[derive(Clone, Copy)] -pub struct FeatureRegistration { - feature_id: &'static str, - migrations: &'static [FeatureMigration], -} - -impl FeatureRegistration { - pub const fn new(feature_id: &'static str, migrations: &'static [FeatureMigration]) -> Self { - Self { - feature_id, - migrations, - } - } - - pub fn feature_id(&self) -> &'static str { - self.feature_id - } -} - -#[derive(Clone, Debug)] -pub struct RegisteredFeature { - manager_id: Uuid, - feature_id: String, -} - -impl RegisteredFeature { - pub fn feature_id(&self) -> &str { - &self.feature_id - } -} - -#[derive(Clone)] -pub struct FeatureStorage { - inner: Arc, -} - -struct FeatureStorageInner { - id: Uuid, - root: PathBuf, - state: Mutex, -} - -struct FeatureStorageState { - running: bool, - registrations: HashMap, - connections: HashMap<(String, String), Weak>, - maintenance: HashSet, - deletion_fences: HashSet, -} - -impl FeatureStorage { - /// Creates a lazy manager. No directories or databases are created until a - /// registered Feature is opened, restored, or explicitly backed up. - pub fn new(root: impl Into) -> Self { - Self { - inner: Arc::new(FeatureStorageInner { - id: Uuid::now_v7(), - root: root.into(), - state: Mutex::new(FeatureStorageState { - running: true, - registrations: HashMap::new(), - connections: HashMap::new(), - maintenance: HashSet::new(), - deletion_fences: HashSet::new(), - }), - }), - } - } - - pub fn for_server_database(database_path: impl AsRef) -> Result { - let database_path = database_path.as_ref(); - let parent = database_path.parent().ok_or_else(|| { - FeatureStorageError::Operation(format!( - "Server database path `{}` has no parent", - database_path.display() - )) - })?; - Ok(Self::new(parent.join("feature-storage"))) - } - - pub fn workspace(&self, workspace_id: &str) -> Result { - Ok(WorkspaceFeatureStorage { - manager: self.clone(), - workspace_id: validate_identifier("Workspace", workspace_id)?.to_string(), - }) - } - - pub fn register(&self, registration: FeatureRegistration) -> Result { - let feature_id = validate_identifier("Feature", registration.feature_id)?.to_string(); - validate_migrations(&feature_id, registration.migrations)?; - let mut state = self.lock_state()?; - ensure_running(&state)?; - if state.registrations.contains_key(&feature_id) { - return Err(FeatureStorageError::FeatureAlreadyRegistered(feature_id)); - } - state - .registrations - .insert(feature_id.clone(), registration.migrations); - Ok(RegisteredFeature { - manager_id: self.inner.id, - feature_id, - }) - } - - /// Closes every managed connection after any operation currently holding a - /// connection lock completes. New opens and operations then fail. - pub fn shutdown(&self) -> Result<()> { - let connections = { - let mut state = self.lock_state()?; - if !state.running { - return Ok(()); - } - state.running = false; - let connections = state - .connections - .values() - .filter_map(Weak::upgrade) - .collect::>(); - for connection in &connections { - connection.fence(); - } - state.connections.clear(); - connections - }; - for connection in connections { - connection.close()?; - } - Ok(()) - } - - fn open( - &self, - workspace_id: &str, - registration: &RegisteredFeature, - ) -> Result { - if registration.manager_id != self.inner.id { - return Err(FeatureStorageError::ForeignRegistration); - } - let key = (workspace_id.to_string(), registration.feature_id.clone()); - let mut state = self.lock_state()?; - ensure_running(&state)?; - if state.maintenance.contains(workspace_id) || state.deletion_fences.contains(workspace_id) - { - return Err(FeatureStorageError::WorkspaceMaintenance( - workspace_id.to_string(), - )); - } - let migrations = state - .registrations - .get(®istration.feature_id) - .copied() - .ok_or_else(|| { - FeatureStorageError::FeatureNotRegistered(registration.feature_id.clone()) - })?; - if let Some(connection) = state.connections.get(&key).and_then(Weak::upgrade) { - return Ok(FeatureDatabase { inner: connection }); - } - - let feature_dir = self.feature_dir(workspace_id); - fs::create_dir_all(&feature_dir)?; - let path = feature_dir.join(format!("{}.sqlite", registration.feature_id)); - let connection = Connection::open_with_flags( - path, - OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE, - )?; - configure_connection(&connection)?; - migrate(&connection, ®istration.feature_id, migrations)?; - let connection = Arc::new(FeatureDatabaseInner { - workspace_id: workspace_id.to_string(), - feature_id: registration.feature_id.clone(), - accepting_operations: AtomicBool::new(true), - connection: Mutex::new(Some(connection)), - }); - state.connections.insert(key, Arc::downgrade(&connection)); - Ok(FeatureDatabase { inner: connection }) - } - - fn backup_workspace(&self, workspace_id: &str, destination: &Path) -> Result<()> { - // Keep manager lifecycle and deletion/restore mutually exclusive for the - // duration. Feature transactions use SQLite's online backup snapshot rules. - let state = self.lock_state()?; - ensure_running(&state)?; - if state.maintenance.contains(workspace_id) || state.deletion_fences.contains(workspace_id) - { - return Err(FeatureStorageError::WorkspaceMaintenance( - workspace_id.to_string(), - )); - } - if destination.exists() { - return Err(FeatureStorageError::DestinationExists( - destination.display().to_string(), - )); - } - let feature_ids = list_feature_databases(&self.feature_dir(workspace_id))?; - let parent = destination.parent().ok_or_else(|| { - FeatureStorageError::Operation(format!( - "snapshot destination `{}` has no parent", - destination.display() - )) - })?; - fs::create_dir_all(parent)?; - let temporary = temporary_sibling(destination, "backup")?; - let result = (|| { - let temporary_features = temporary.join("features"); - fs::create_dir_all(&temporary_features)?; - for feature_id in &feature_ids { - backup_database( - &self - .feature_dir(workspace_id) - .join(format!("{feature_id}.sqlite")), - &temporary_features.join(format!("{feature_id}.sqlite")), - )?; - } - fs::write( - temporary.join(SNAPSHOT_MANIFEST), - snapshot_manifest(workspace_id, &feature_ids), - )?; - fs::rename(&temporary, destination)?; - Ok(()) - })(); - drop(state); - if result.is_err() { - let _ = fs::remove_dir_all(&temporary); - } - result - } - - fn restore_workspace(&self, workspace_id: &str, snapshot: &Path) -> Result<()> { - let feature_ids = read_snapshot_manifest(snapshot, workspace_id)?; - self.begin_maintenance(workspace_id, true)?; - let workspace_dir = self.workspace_dir(workspace_id); - let temporary = self - .inner - .root - .join(format!(".restore-{workspace_id}-{}", Uuid::now_v7())); - let result = (|| { - if workspace_dir.exists() { - return Err(FeatureStorageError::DestinationExists( - workspace_dir.display().to_string(), - )); - } - let temporary_features = temporary.join("features"); - fs::create_dir_all(&temporary_features)?; - for feature_id in &feature_ids { - backup_database( - &snapshot - .join("features") - .join(format!("{feature_id}.sqlite")), - &temporary_features.join(format!("{feature_id}.sqlite")), - )?; - } - fs::create_dir_all(&self.inner.root)?; - fs::rename(&temporary, &workspace_dir)?; - Ok(()) - })(); - if result.is_err() { - let _ = fs::remove_dir_all(&temporary); - } - self.end_maintenance(workspace_id)?; - result - } - - fn delete_workspace(&self, workspace_id: &str) -> Result<()> { - let connections = { - let mut state = self.lock_state()?; - ensure_running(&state)?; - if state.maintenance.contains(workspace_id) { - return Err(FeatureStorageError::WorkspaceMaintenance( - workspace_id.to_string(), - )); - } - // The fence intentionally survives physical removal. Workspace deletion - // is finalized in the Server DB immediately afterwards; keeping this - // admission fence prevents a cached API/background task from recreating - // Feature files in that gap. A retry is idempotently admitted. - state.deletion_fences.insert(workspace_id.to_string()); - let connections = state - .connections - .iter() - .filter(|((candidate, _), _)| candidate == workspace_id) - .filter_map(|(_, connection)| connection.upgrade()) - .collect::>(); - for connection in &connections { - connection.fence(); - } - state - .connections - .retain(|(candidate, _), _| candidate != workspace_id); - connections - }; - for connection in connections { - connection.close()?; - } - match fs::remove_dir_all(self.workspace_dir(workspace_id)) { - Ok(()) => Ok(()), - Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()), - Err(error) => Err(error.into()), - } - } - - fn begin_maintenance( - &self, - workspace_id: &str, - require_no_connections: bool, - ) -> Result>> { - let mut state = self.lock_state()?; - ensure_running(&state)?; - if state.deletion_fences.contains(workspace_id) - || !state.maintenance.insert(workspace_id.to_string()) - { - return Err(FeatureStorageError::WorkspaceMaintenance( - workspace_id.to_string(), - )); - } - let connections = state - .connections - .iter() - .filter(|((candidate, _), _)| candidate == workspace_id) - .filter_map(|(_, connection)| connection.upgrade()) - .collect::>(); - if require_no_connections && !connections.is_empty() { - state.maintenance.remove(workspace_id); - return Err(FeatureStorageError::Operation(format!( - "cannot restore Workspace `{workspace_id}` while Feature databases are open" - ))); - } - state - .connections - .retain(|(candidate, _), _| candidate != workspace_id); - Ok(connections) - } - - fn end_maintenance(&self, workspace_id: &str) -> Result<()> { - let mut state = self.lock_state()?; - state.maintenance.remove(workspace_id); - Ok(()) - } - - fn workspace_dir(&self, workspace_id: &str) -> PathBuf { - self.inner.root.join(workspace_id) - } - - fn feature_dir(&self, workspace_id: &str) -> PathBuf { - self.workspace_dir(workspace_id).join("features") - } - - fn lock_state(&self) -> Result> { - self.inner.state.lock().map_err(|_| { - FeatureStorageError::Operation("Feature storage state lock was poisoned".to_string()) - }) - } -} - -impl Drop for FeatureStorageInner { - fn drop(&mut self) { - if let Ok(mut state) = self.state.lock() { - state.running = false; - for connection in state.connections.values().filter_map(Weak::upgrade) { - let _ = connection.close(); - } - state.connections.clear(); - } - } -} - -#[derive(Clone)] -pub struct WorkspaceFeatureStorage { - manager: FeatureStorage, - workspace_id: String, -} - -impl WorkspaceFeatureStorage { - pub fn workspace_id(&self) -> &str { - &self.workspace_id - } - - pub fn register(&self, registration: FeatureRegistration) -> Result { - self.manager.register(registration) - } - - pub fn open(&self, registration: &RegisteredFeature) -> Result { - self.manager.open(&self.workspace_id, registration) - } - - /// Creates a consistent online SQLite snapshot of every Feature database, - /// including data for Features that are currently disabled/unregistered. - pub fn backup(&self, destination: impl AsRef) -> Result<()> { - self.manager - .backup_workspace(&self.workspace_id, destination.as_ref()) - } - - /// Restores a snapshot only into an absent Workspace storage directory and - /// only while no Feature database for this Workspace is open. - pub fn restore(&self, snapshot: impl AsRef) -> Result<()> { - self.manager - .restore_workspace(&self.workspace_id, snapshot.as_ref()) - } - - /// Closes this Workspace's Feature connections and removes only its scoped - /// storage tree. This is the operation used by Workspace deletion. - pub fn delete(&self) -> Result<()> { - self.manager.delete_workspace(&self.workspace_id) - } - - pub fn shutdown(&self) -> Result<()> { - self.manager.shutdown() - } -} - -#[derive(Clone)] -pub struct FeatureDatabase { - inner: Arc, -} - -struct FeatureDatabaseInner { - workspace_id: String, - feature_id: String, - accepting_operations: AtomicBool, - connection: Mutex>, -} - -impl FeatureDatabase { - pub fn workspace_id(&self) -> &str { - &self.inner.workspace_id - } - - pub fn feature_id(&self) -> &str { - &self.inner.feature_id - } - - /// Executes a Feature-owned read/query while serializing use of the managed - /// connection. The connection never leaves trusted Server Rust code. - pub fn with_connection( - &self, - operation: impl FnOnce(&Connection) -> Result, - ) -> Result { - self.try_with_connection(operation) - } - - /// Variant of [`Self::with_connection`] that lets a typed Feature repository - /// retain its own domain errors without encoding them as storage strings. - pub fn try_with_connection( - &self, - operation: impl FnOnce(&Connection) -> std::result::Result, - ) -> std::result::Result - where - E: From, - { - self.inner.ensure_accepting().map_err(E::from)?; - let connection = self - .inner - .connection - .lock() - .map_err(|_| { - FeatureStorageError::Operation("Feature database lock was poisoned".to_string()) - }) - .map_err(E::from)?; - self.inner.ensure_accepting().map_err(E::from)?; - let connection = connection - .as_ref() - .ok_or(FeatureStorageError::DatabaseClosed) - .map_err(E::from)?; - operation(connection) - } - - /// Executes a Feature-owned multi-table operation atomically. Returning an - /// error rolls the transaction back; successful return commits it. - pub fn transaction( - &self, - operation: impl FnOnce(&Transaction<'_>) -> Result, - ) -> Result { - self.try_transaction(operation) - } - - /// Variant of [`Self::transaction`] that preserves a Feature repository's - /// typed domain errors while still rolling every failure back atomically. - pub fn try_transaction( - &self, - operation: impl FnOnce(&Transaction<'_>) -> std::result::Result, - ) -> std::result::Result - where - E: From, - { - self.inner.ensure_accepting().map_err(E::from)?; - let mut connection = self - .inner - .connection - .lock() - .map_err(|_| { - FeatureStorageError::Operation("Feature database lock was poisoned".to_string()) - }) - .map_err(E::from)?; - self.inner.ensure_accepting().map_err(E::from)?; - let connection = connection - .as_mut() - .ok_or(FeatureStorageError::DatabaseClosed) - .map_err(E::from)?; - let transaction = connection - .transaction_with_behavior(TransactionBehavior::Immediate) - .map_err(FeatureStorageError::from) - .map_err(E::from)?; - let value = operation(&transaction)?; - transaction - .commit() - .map_err(FeatureStorageError::from) - .map_err(E::from)?; - Ok(value) - } - - pub fn schema_version(&self) -> Result { - self.with_connection(|connection| { - connection - .query_row( - &format!("SELECT COALESCE(MAX(version), 0) FROM {MIGRATION_TABLE}"), - [], - |row| row.get(0), - ) - .map_err(Into::into) - }) - } -} - -impl FeatureDatabaseInner { - fn ensure_accepting(&self) -> Result<()> { - if self.accepting_operations.load(Ordering::Acquire) { - Ok(()) - } else { - Err(FeatureStorageError::DatabaseClosed) - } - } - - fn fence(&self) { - self.accepting_operations.store(false, Ordering::Release); - } - - fn close(&self) -> Result<()> { - self.fence(); - let mut connection = self.connection.lock().map_err(|_| { - FeatureStorageError::Operation("Feature database lock was poisoned".to_string()) - })?; - connection.take(); - Ok(()) - } -} - -fn validate_identifier<'a>(kind: &'static str, value: &'a str) -> Result<&'a str> { - let valid = !value.is_empty() - && value.len() <= MAX_IDENTIFIER_BYTES - && value.bytes().enumerate().all(|(index, byte)| match byte { - b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' => true, - b'-' | b'_' => index > 0, - _ => false, - }); - if !valid { - return Err(FeatureStorageError::InvalidIdentifier { - kind, - value: value.to_string(), - }); - } - Ok(value) -} - -fn validate_migrations(feature: &str, migrations: &[FeatureMigration]) -> Result<()> { - for (index, migration) in migrations.iter().enumerate() { - let expected = (index + 1) as u32; - if migration.version != expected { - return Err(FeatureStorageError::InvalidMigrationPlan { - feature: feature.to_string(), - detail: format!( - "versions must be contiguous from 1; expected {expected}, found {}", - migration.version - ), - }); - } - if migration.name.trim().is_empty() { - return Err(FeatureStorageError::InvalidMigrationPlan { - feature: feature.to_string(), - detail: format!("migration {expected} has an empty name"), - }); - } - } - Ok(()) -} - -fn ensure_running(state: &FeatureStorageState) -> Result<()> { - if state.running { - Ok(()) - } else { - Err(FeatureStorageError::ShuttingDown) - } -} - -fn configure_connection(connection: &Connection) -> Result<()> { - connection.busy_timeout(BUSY_TIMEOUT)?; - connection.execute_batch( - r#" -PRAGMA foreign_keys = ON; -PRAGMA synchronous = NORMAL; -PRAGMA busy_timeout = 5000; -"#, - )?; - // Changing journal mode takes a database-wide lock and, unlike ordinary - // statements, may report SQLITE_BUSY immediately while another opener is - // making the same transition. Bound retries by the same common timeout. - let attempts = (BUSY_TIMEOUT.as_millis() / 10) as usize; - for attempt in 0..=attempts { - match connection.query_row("PRAGMA journal_mode = WAL", [], |row| { - row.get::<_, String>(0) - }) { - Ok(mode) if mode.eq_ignore_ascii_case("wal") => return Ok(()), - Ok(mode) => { - return Err(FeatureStorageError::Operation(format!( - "SQLite refused WAL journal mode and selected `{mode}`" - ))); - } - Err(error) if sqlite_is_busy(&error) && attempt < attempts => { - std::thread::sleep(Duration::from_millis(10)); - } - Err(error) => return Err(error.into()), - } - } - unreachable!("bounded WAL retry loop always returns") -} - -fn sqlite_is_busy(error: &rusqlite::Error) -> bool { - matches!( - error, - rusqlite::Error::SqliteFailure(error, _) - if matches!(error.code, ErrorCode::DatabaseBusy | ErrorCode::DatabaseLocked) - ) -} - -fn migrate( - connection: &Connection, - feature_id: &str, - migrations: &[FeatureMigration], -) -> Result<()> { - let transaction = Transaction::new_unchecked(connection, TransactionBehavior::Immediate)?; - transaction.execute_batch(&format!( - r#" -CREATE TABLE IF NOT EXISTS {MIGRATION_TABLE} ( - version INTEGER PRIMARY KEY CHECK (version > 0), - name TEXT NOT NULL, - applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP -); -"# - ))?; - let mut statement = transaction.prepare(&format!( - "SELECT version, name FROM {MIGRATION_TABLE} ORDER BY version" - ))?; - let applied = statement - .query_map([], |row| { - Ok((row.get::<_, u32>(0)?, row.get::<_, String>(1)?)) - })? - .collect::, _>>()?; - drop(statement); - - let supported = migrations.last().map_or(0, |migration| migration.version); - if let Some((actual, _)) = applied.last() - && *actual > supported - { - return Err(FeatureStorageError::UnsupportedSchema { - feature: feature_id.to_string(), - actual: *actual, - supported, - }); - } - for (index, (version, name)) in applied.iter().enumerate() { - let Some(expected) = migrations.get(index) else { - return Err(FeatureStorageError::UnsupportedSchema { - feature: feature_id.to_string(), - actual: *version, - supported, - }); - }; - if *version != expected.version || name != expected.name { - return Err(FeatureStorageError::MigrationHistory { - feature: feature_id.to_string(), - detail: format!( - "stored migration {version} (`{name}`) does not match expected {} (`{}`)", - expected.version, expected.name - ), - }); - } - } - - for migration in migrations.iter().skip(applied.len()) { - if let Err(error) = (migration.apply)(&transaction) { - return Err(FeatureStorageError::MigrationFailed { - feature: feature_id.to_string(), - version: migration.version, - name: migration.name.to_string(), - detail: error.to_string(), - }); - } - transaction.execute( - &format!("INSERT INTO {MIGRATION_TABLE} (version, name) VALUES (?1, ?2)"), - params![migration.version, migration.name], - )?; - } - transaction.commit()?; - Ok(()) -} - -fn backup_database(source: &Path, destination: &Path) -> Result<()> { - if !source.is_file() { - return Err(FeatureStorageError::InvalidSnapshot(format!( - "Feature database `{}` is missing", - source.display() - ))); - } - let source = Connection::open_with_flags(source, OpenFlags::SQLITE_OPEN_READ_ONLY)?; - source.busy_timeout(BUSY_TIMEOUT)?; - verify_database(&source)?; - let mut destination = Connection::open(destination)?; - Backup::new(&source, &mut destination)?.run_to_completion( - 128, - Duration::from_millis(2), - None, - )?; - verify_database(&destination)?; - Ok(()) -} - -fn verify_database(connection: &Connection) -> Result<()> { - let integrity: String = connection.query_row("PRAGMA integrity_check", [], |row| row.get(0))?; - if integrity != "ok" { - return Err(FeatureStorageError::InvalidSnapshot(format!( - "SQLite integrity_check failed: {integrity}" - ))); - } - let mut foreign_keys = connection.prepare("PRAGMA foreign_key_check")?; - if foreign_keys.query([])?.next()?.is_some() { - return Err(FeatureStorageError::InvalidSnapshot( - "SQLite foreign_key_check reported a violation".to_string(), - )); - } - Ok(()) -} - -fn list_feature_databases(feature_dir: &Path) -> Result> { - if !feature_dir.exists() { - return Ok(Vec::new()); - } - let mut feature_ids = Vec::new(); - for entry in fs::read_dir(feature_dir)? { - let entry = entry?; - let file_type = entry.file_type()?; - if !file_type.is_file() { - continue; - } - let path = entry.path(); - if path.extension().and_then(|value| value.to_str()) != Some("sqlite") { - continue; - } - let feature_id = path - .file_stem() - .and_then(|value| value.to_str()) - .ok_or_else(|| { - FeatureStorageError::InvalidSnapshot(format!( - "Feature database filename `{}` is not UTF-8", - path.display() - )) - })?; - validate_identifier("Feature", feature_id)?; - feature_ids.push(feature_id.to_string()); - } - feature_ids.sort(); - Ok(feature_ids) -} - -fn snapshot_manifest(workspace_id: &str, feature_ids: &[String]) -> String { - let mut manifest = format!("format={SNAPSHOT_FORMAT}\nworkspace={workspace_id}\n"); - for feature_id in feature_ids { - manifest.push_str("feature="); - manifest.push_str(feature_id); - manifest.push('\n'); - } - manifest -} - -fn read_snapshot_manifest(snapshot: &Path, expected_workspace: &str) -> Result> { - let manifest = fs::read_to_string(snapshot.join(SNAPSHOT_MANIFEST)).map_err(|error| { - FeatureStorageError::InvalidSnapshot(format!("cannot read manifest: {error}")) - })?; - let mut format = None; - let mut workspace = None; - let mut feature_ids = Vec::new(); - for line in manifest.lines() { - if let Some(value) = line.strip_prefix("format=") { - format = Some(value); - } else if let Some(value) = line.strip_prefix("workspace=") { - workspace = Some(value); - } else if let Some(value) = line.strip_prefix("feature=") { - validate_identifier("Feature", value)?; - feature_ids.push(value.to_string()); - } else { - return Err(FeatureStorageError::InvalidSnapshot(format!( - "unknown manifest entry `{line}`" - ))); - } - } - if format != Some(SNAPSHOT_FORMAT) { - return Err(FeatureStorageError::InvalidSnapshot( - "unsupported snapshot format".to_string(), - )); - } - if workspace != Some(expected_workspace) { - return Err(FeatureStorageError::InvalidSnapshot(format!( - "snapshot Workspace `{}` does not match `{expected_workspace}`", - workspace.unwrap_or("") - ))); - } - feature_ids.sort(); - if feature_ids.windows(2).any(|ids| ids[0] == ids[1]) { - return Err(FeatureStorageError::InvalidSnapshot( - "duplicate Feature entry".to_string(), - )); - } - for feature_id in &feature_ids { - let database = snapshot - .join("features") - .join(format!("{feature_id}.sqlite")); - if !database.is_file() { - return Err(FeatureStorageError::InvalidSnapshot(format!( - "Feature database `{}` is missing", - database.display() - ))); - } - } - Ok(feature_ids) -} - -fn temporary_sibling(destination: &Path, operation: &str) -> Result { - let parent = destination.parent().ok_or_else(|| { - FeatureStorageError::Operation(format!( - "destination `{}` has no parent", - destination.display() - )) - })?; - let name = destination - .file_name() - .and_then(|value| value.to_str()) - .ok_or_else(|| { - FeatureStorageError::Operation(format!( - "destination `{}` has no UTF-8 filename", - destination.display() - )) - })?; - Ok(parent.join(format!(".{name}.{operation}-{}", Uuid::now_v7()))) -} - -#[cfg(test)] -mod tests { - use std::sync::{Arc, Barrier}; - use std::thread; - - use rusqlite::OptionalExtension; - - use super::*; - - #[derive(Debug)] - enum TestDomainError { - Storage, - Rejected, - } - - impl From for TestDomainError { - fn from(_error: FeatureStorageError) -> Self { - Self::Storage - } - } - - fn create_items(transaction: &Transaction<'_>) -> Result<()> { - transaction.execute_batch( - "CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT NOT NULL UNIQUE);", - )?; - Ok(()) - } - - fn add_audit(transaction: &Transaction<'_>) -> Result<()> { - transaction.execute_batch( - "CREATE TABLE audit (id INTEGER PRIMARY KEY, item_id INTEGER NOT NULL, note TEXT NOT NULL);", - )?; - Ok(()) - } - - const MIGRATIONS: &[FeatureMigration] = &[ - FeatureMigration::new(1, "create items", create_items), - FeatureMigration::new(2, "add audit", add_audit), - ]; - - fn failing_migration(transaction: &Transaction<'_>) -> Result<()> { - transaction.execute_batch("CREATE TABLE partial (id INTEGER PRIMARY KEY);")?; - Err(FeatureStorageError::operation( - "intentional migration failure", - )) - } - - const FAILING_MIGRATIONS: &[FeatureMigration] = &[ - FeatureMigration::new(1, "create items", create_items), - FeatureMigration::new(2, "fail", failing_migration), - ]; - - fn registration(feature_id: &'static str) -> FeatureRegistration { - FeatureRegistration::new(feature_id, MIGRATIONS) - } - - fn insert(database: &FeatureDatabase, value: &str) -> Result<()> { - database.transaction(|transaction| { - transaction.execute("INSERT INTO items (value) VALUES (?1)", [value])?; - Ok(()) - }) - } - - fn values(database: &FeatureDatabase) -> Result> { - database.with_connection(|connection| { - let mut statement = connection.prepare("SELECT value FROM items ORDER BY id")?; - statement - .query_map([], |row| row.get(0))? - .collect::, _>>() - .map_err(Into::into) - }) - } - - #[test] - fn open_migrate_transaction_rollback_and_reopen_persist() { - let temp = tempfile::tempdir().unwrap(); - let manager = FeatureStorage::new(temp.path().join("storage")); - let workspace = manager.workspace("workspace-a").unwrap(); - let feature = workspace.register(registration("subject-memory")).unwrap(); - let database = workspace.open(&feature).unwrap(); - assert_eq!(database.schema_version().unwrap(), 2); - insert(&database, "committed").unwrap(); - let error = database - .transaction::<()>(|transaction| { - transaction.execute("INSERT INTO items (value) VALUES ('rolled-back')", [])?; - Err(FeatureStorageError::operation("reject candidate")) - }) - .unwrap_err(); - assert!(error.to_string().contains("reject candidate")); - assert_eq!(values(&database).unwrap(), ["committed"]); - - manager.shutdown().unwrap(); - assert!(matches!( - values(&database).unwrap_err(), - FeatureStorageError::DatabaseClosed - )); - - let reopened = FeatureStorage::new(temp.path().join("storage")); - let workspace = reopened.workspace("workspace-a").unwrap(); - let feature = workspace.register(registration("subject-memory")).unwrap(); - let database = workspace.open(&feature).unwrap(); - assert_eq!(values(&database).unwrap(), ["committed"]); - assert_eq!(database.schema_version().unwrap(), 2); - } - - #[test] - fn typed_domain_error_rolls_back_transaction() { - let temp = tempfile::tempdir().unwrap(); - let manager = FeatureStorage::new(temp.path().join("storage")); - let workspace = manager.workspace("workspace-a").unwrap(); - let feature = workspace.register(registration("feature-a")).unwrap(); - let database = workspace.open(&feature).unwrap(); - - let result = database.try_transaction::<(), TestDomainError>(|transaction| { - transaction - .execute("INSERT INTO items (value) VALUES ('must-roll-back')", []) - .map_err(FeatureStorageError::from)?; - Err(TestDomainError::Rejected) - }); - assert!(matches!(result, Err(TestDomainError::Rejected))); - assert!(values(&database).unwrap().is_empty()); - } - - #[test] - fn workspace_and_feature_databases_are_isolated() { - let temp = tempfile::tempdir().unwrap(); - let manager = FeatureStorage::new(temp.path().join("storage")); - let feature_a = manager.register(registration("feature-a")).unwrap(); - let feature_b = manager.register(registration("feature-b")).unwrap(); - let workspace_a = manager.workspace("workspace-a").unwrap(); - let workspace_b = manager.workspace("workspace-b").unwrap(); - let a_a = workspace_a.open(&feature_a).unwrap(); - let a_b = workspace_a.open(&feature_b).unwrap(); - let b_a = workspace_b.open(&feature_a).unwrap(); - insert(&a_a, "workspace-a/feature-a").unwrap(); - insert(&a_b, "workspace-a/feature-b").unwrap(); - insert(&b_a, "workspace-b/feature-a").unwrap(); - assert_eq!(values(&a_a).unwrap(), ["workspace-a/feature-a"]); - assert_eq!(values(&a_b).unwrap(), ["workspace-a/feature-b"]); - assert_eq!(values(&b_a).unwrap(), ["workspace-b/feature-a"]); - assert_eq!(a_a.schema_version().unwrap(), 2); - assert_eq!(a_b.schema_version().unwrap(), 2); - assert_eq!(b_a.schema_version().unwrap(), 2); - } - - #[test] - fn concurrent_open_migrates_once_and_reuses_connection() { - let temp = tempfile::tempdir().unwrap(); - let manager = FeatureStorage::new(temp.path().join("storage")); - let workspace = manager.workspace("workspace-a").unwrap(); - let feature = workspace.register(registration("feature-a")).unwrap(); - let barrier = Arc::new(Barrier::new(3)); - let threads = (0..2) - .map(|_| { - let workspace = workspace.clone(); - let feature = feature.clone(); - let barrier = barrier.clone(); - thread::spawn(move || { - barrier.wait(); - workspace.open(&feature).unwrap() - }) - }) - .collect::>(); - barrier.wait(); - let mut databases = Vec::new(); - for thread in threads { - databases.push(thread.join().unwrap()); - } - assert!(Arc::ptr_eq(&databases[0].inner, &databases[1].inner)); - assert_eq!(databases[0].schema_version().unwrap(), 2); - } - - #[test] - fn independent_managers_serialize_migration_and_writes() { - let temp = tempfile::tempdir().unwrap(); - let root = temp.path().join("storage"); - let first_manager = FeatureStorage::new(&root); - let second_manager = FeatureStorage::new(&root); - let barrier = Arc::new(Barrier::new(3)); - let open = |manager: FeatureStorage, barrier: Arc| { - thread::spawn(move || { - let workspace = manager.workspace("workspace-a").unwrap(); - let feature = workspace.register(registration("feature-a")).unwrap(); - barrier.wait(); - let database = workspace.open(&feature).unwrap(); - (manager, database) - }) - }; - let first = open(first_manager, barrier.clone()); - let second = open(second_manager, barrier.clone()); - barrier.wait(); - let (first_manager, first_database) = first.join().unwrap(); - let (second_manager, second_database) = second.join().unwrap(); - assert_eq!(first_database.schema_version().unwrap(), 2); - assert_eq!(second_database.schema_version().unwrap(), 2); - - let writers = (0..16) - .map(|index| { - let database = if index % 2 == 0 { - first_database.clone() - } else { - second_database.clone() - }; - thread::spawn(move || insert(&database, &format!("manager-value-{index}"))) - }) - .collect::>(); - for writer in writers { - writer.join().unwrap().unwrap(); - } - assert_eq!(values(&first_database).unwrap().len(), 16); - drop((first_manager, second_manager)); - } - - #[test] - fn concurrent_writes_are_serialized_without_lost_updates() { - let temp = tempfile::tempdir().unwrap(); - let manager = FeatureStorage::new(temp.path().join("storage")); - let workspace = manager.workspace("workspace-a").unwrap(); - let feature = workspace.register(registration("feature-a")).unwrap(); - let database = workspace.open(&feature).unwrap(); - let threads = (0..8) - .map(|index| { - let database = database.clone(); - thread::spawn(move || insert(&database, &format!("value-{index}"))) - }) - .collect::>(); - for thread in threads { - thread.join().unwrap().unwrap(); - } - assert_eq!(values(&database).unwrap().len(), 8); - } - - #[test] - fn failed_migration_rolls_back_schema_and_version() { - let temp = tempfile::tempdir().unwrap(); - let manager = FeatureStorage::new(temp.path().join("storage")); - let workspace = manager.workspace("workspace-a").unwrap(); - let feature = workspace - .register(FeatureRegistration::new("broken", FAILING_MIGRATIONS)) - .unwrap(); - let error = workspace.open(&feature).err().unwrap(); - assert!(matches!(error, FeatureStorageError::MigrationFailed { .. })); - let path = temp - .path() - .join("storage/workspace-a/features/broken.sqlite"); - let connection = Connection::open(path).unwrap(); - let migration_table_exists: bool = connection - .query_row( - "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?1", - [MIGRATION_TABLE], - |_| Ok(true), - ) - .optional() - .unwrap() - .unwrap_or(false); - assert!(!migration_table_exists); - let partial_exists: bool = connection - .query_row( - "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'partial'", - [], - |_| Ok(true), - ) - .optional() - .unwrap() - .unwrap_or(false); - assert!(!partial_exists); - } - - #[test] - fn rejects_invalid_identifiers_and_newer_schema() { - let temp = tempfile::tempdir().unwrap(); - let manager = FeatureStorage::new(temp.path().join("storage")); - assert!(manager.workspace("../escape").is_err()); - assert!( - manager - .register(FeatureRegistration::new("bad/name", MIGRATIONS)) - .is_err() - ); - - let workspace = manager.workspace("workspace-a").unwrap(); - let feature = workspace.register(registration("feature-a")).unwrap(); - let database = workspace.open(&feature).unwrap(); - database - .with_connection(|connection| { - connection.execute( - &format!("INSERT INTO {MIGRATION_TABLE} (version, name) VALUES (99, 'future')"), - [], - )?; - Ok(()) - }) - .unwrap(); - drop(database); - manager.shutdown().unwrap(); - - let manager = FeatureStorage::new(temp.path().join("storage")); - let workspace = manager.workspace("workspace-a").unwrap(); - let feature = workspace.register(registration("feature-a")).unwrap(); - assert!(matches!( - workspace.open(&feature).err().unwrap(), - FeatureStorageError::UnsupportedSchema { actual: 99, .. } - )); - } - - #[test] - fn registration_is_lazy_and_disable_preserves_data() { - let temp = tempfile::tempdir().unwrap(); - let root = temp.path().join("storage"); - let manager = FeatureStorage::new(&root); - let workspace = manager.workspace("workspace-a").unwrap(); - let feature = workspace.register(registration("feature-a")).unwrap(); - assert!(!root.exists(), "registration must not create storage"); - let database = workspace.open(&feature).unwrap(); - insert(&database, "retained").unwrap(); - manager.shutdown().unwrap(); - - let disabled = FeatureStorage::new(&root); - assert!(root.join("workspace-a/features/feature-a.sqlite").is_file()); - disabled.shutdown().unwrap(); - - let enabled = FeatureStorage::new(&root); - let workspace = enabled.workspace("workspace-a").unwrap(); - let feature = workspace.register(registration("feature-a")).unwrap(); - assert_eq!( - values(&workspace.open(&feature).unwrap()).unwrap(), - ["retained"] - ); - } - - #[test] - fn backup_restore_is_consistent_and_includes_disabled_features() { - let temp = tempfile::tempdir().unwrap(); - let root = temp.path().join("storage"); - let snapshot = temp.path().join("snapshot"); - let manager = FeatureStorage::new(&root); - let workspace = manager.workspace("workspace-a").unwrap(); - let feature = workspace.register(registration("feature-a")).unwrap(); - let database = workspace.open(&feature).unwrap(); - insert(&database, "snapshot-value").unwrap(); - workspace.backup(&snapshot).unwrap(); - insert(&database, "after-snapshot").unwrap(); - workspace.delete().unwrap(); - assert!(matches!( - workspace.open(&feature).err().unwrap(), - FeatureStorageError::WorkspaceMaintenance(_) - )); - drop(database); - drop(workspace); - drop(manager); - - let manager = FeatureStorage::new(&root); - let workspace = manager.workspace("workspace-a").unwrap(); - workspace.restore(&snapshot).unwrap(); - let feature = workspace.register(registration("feature-a")).unwrap(); - assert_eq!( - values(&workspace.open(&feature).unwrap()).unwrap(), - ["snapshot-value"] - ); - } - - #[test] - fn workspace_delete_closes_handles_and_is_target_limited() { - let temp = tempfile::tempdir().unwrap(); - let manager = FeatureStorage::new(temp.path().join("storage")); - let feature = manager.register(registration("feature-a")).unwrap(); - let workspace_a = manager.workspace("workspace-a").unwrap(); - let workspace_b = manager.workspace("workspace-b").unwrap(); - let database_a = workspace_a.open(&feature).unwrap(); - let database_b = workspace_b.open(&feature).unwrap(); - insert(&database_a, "a").unwrap(); - insert(&database_b, "b").unwrap(); - workspace_a.delete().unwrap(); - assert!(matches!( - values(&database_a).unwrap_err(), - FeatureStorageError::DatabaseClosed - )); - assert_eq!(values(&database_b).unwrap(), ["b"]); - assert!(!temp.path().join("storage/workspace-a").exists()); - assert!(temp.path().join("storage/workspace-b").exists()); - } - - #[test] - fn shutdown_waits_for_operation_and_rejects_later_use() { - let temp = tempfile::tempdir().unwrap(); - let manager = FeatureStorage::new(temp.path().join("storage")); - let workspace = manager.workspace("workspace-a").unwrap(); - let feature = workspace.register(registration("feature-a")).unwrap(); - let database = workspace.open(&feature).unwrap(); - let entered = Arc::new(Barrier::new(2)); - let release = Arc::new(Barrier::new(2)); - let operation = { - let database = database.clone(); - let entered = entered.clone(); - let release = release.clone(); - thread::spawn(move || { - database.with_connection(|_| { - entered.wait(); - release.wait(); - Ok(()) - }) - }) - }; - entered.wait(); - let shutdown = { - let manager = manager.clone(); - thread::spawn(move || manager.shutdown()) - }; - while database.inner.accepting_operations.load(Ordering::Acquire) { - thread::yield_now(); - } - assert!(!shutdown.is_finished()); - assert!(matches!( - values(&database).unwrap_err(), - FeatureStorageError::DatabaseClosed - )); - release.wait(); - operation.join().unwrap().unwrap(); - shutdown.join().unwrap().unwrap(); - assert!(matches!( - values(&database).unwrap_err(), - FeatureStorageError::DatabaseClosed - )); - assert!(matches!( - workspace.open(&feature).err().unwrap(), - FeatureStorageError::ShuttingDown - )); - } -} +//! Backend compatibility facade for the neutral Host storage manager. +pub use feature_storage::*; diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index 84fb046e1..7b13b898a 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -1,3 +1,4 @@ +use subjektiv::api::*; mod backend_job_profile; mod subjektiv_job; mod worker_operations; @@ -18110,36 +18111,6 @@ async fn scoped_memory_backend_operation( Ok(Json(response)) } -fn resident_summary_output( - resident: crate::subjektiv::ResidentSurface, -) -> memory::backend::MemoryResidentSummaryOutput { - let (availability, content) = match resident.availability { - crate::subjektiv::SurfaceAvailability::Ready => { - let snapshot = resident.snapshot.expect("ready surface has a snapshot"); - ( - memory::backend::MemoryResidentSummaryAvailability::Ready, - (!snapshot.body_md.is_empty()).then_some(snapshot.body_md), - ) - } - crate::subjektiv::SurfaceAvailability::Ungenerated => ( - memory::backend::MemoryResidentSummaryAvailability::Ungenerated, - None, - ), - crate::subjektiv::SurfaceAvailability::Stale => ( - memory::backend::MemoryResidentSummaryAvailability::Stale, - None, - ), - crate::subjektiv::SurfaceAvailability::Failed => ( - memory::backend::MemoryResidentSummaryAvailability::Failed, - None, - ), - }; - memory::backend::MemoryResidentSummaryOutput { - availability, - content, - } -} - const SUBJEKTIV_SINGLETON_PREFIX: &str = crate::subjektiv::SUBJECT_WORKER_SINGLETON_PREFIX; const SUBJEKTIV_CONSOLIDATION_SINGLETON_PREFIX: &str = crate::hosts::SUBJEKTIV_CONSOLIDATION_SINGLETON_PREFIX; @@ -18993,7 +18964,7 @@ fn subjektiv_session_list( Err(error) => { return subjektiv_session_error( server_api::SubjektivSessionDiagnosticCode::SubjectUnavailable, - bounded_subjektiv_session_message(&error.error.to_string()), + bounded_subjektiv_session_message(&error.to_string()), ); } } @@ -19197,7 +19168,7 @@ fn subjektiv_session_read( Err(error) => { return subjektiv_session_error( server_api::SubjektivSessionDiagnosticCode::SubjectUnavailable, - error.error.to_string(), + error.to_string(), ); } } @@ -19650,7 +19621,7 @@ fn subjektiv_session_search( Err(error) => { return subjektiv_session_error( server_api::SubjektivSessionDiagnosticCode::SubjectUnavailable, - error.error.to_string(), + error.to_string(), ); } } @@ -20014,52 +19985,6 @@ fn truncate_subjektiv_session_snippet(value: &str) -> (String, bool) { } } -const SUBJEKTIV_QUERY_DEFAULT_LIMIT: usize = 20; -const SUBJEKTIV_QUERY_MAX_LIMIT: usize = 100; -const SUBJEKTIV_BODY_DEFAULT_LINES: usize = 200; -const SUBJEKTIV_BODY_MAX_LINES: usize = 1_000; -const SUBJEKTIV_BODY_MAX_BYTES: usize = 16 * 1024; -const SUBJEKTIV_CANDIDATE_ANCHOR_LIMIT: usize = crate::subjektiv::MAX_STAGING_ANCHORS; -const SUBJEKTIV_EVIDENCE_ANCHOR_PAGE_SIZE: usize = 2; -const SUBJEKTIV_ANCHOR_TEXT_MAX_BYTES: usize = 64; -const SUBJEKTIV_NESTED_EVIDENCE_MAX_BYTES: usize = 40 * 1024; - -#[derive(Debug, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -struct SubjektivQueryCursor { - subject_id: String, - store_revision: u64, - query: Option, - kinds: Vec, - states: Vec, - offset: usize, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -struct SubjektivRevisionCursor { - subject_id: String, - memory_id: String, - max_revision: u64, - offset: usize, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -struct SubjektivEvidenceCursor { - subject_id: String, - memory_id: String, - revision: u64, - /// Candidate/derivation reference offset within the immutable revision. - offset: usize, - /// Nested offsets keep compatible historical candidates with more anchors - /// resumable without admitting new over-limit records. - #[serde(default)] - evidence_offset: usize, - #[serde(default)] - source_ref_offset: usize, -} - async fn scoped_subjektiv_memory_backend( State(api): State, AxumPath(path): AxumPath, @@ -20070,1520 +19995,111 @@ async fn scoped_subjektiv_memory_backend( let (subject_id, source_worker, authority) = subjektiv_subject_scope(&api, &path.workspace_id, &context)?; let store = open_subjektiv_store(&api)?; - let response = match request.operation { - server_api::SubjektivMemoryBackendOperation::ResidentSummary(_) => { - require_subjektiv_worker_authority( - authority, - SubjektivWorkerAuthority::Subject, - "resident surface read", - )?; - let resident = store - .resident_surface(&subject_id) - .map_err(subjektiv_store_error)?; - server_api::SubjektivMemoryBackendResponse::ResidentSummary(resident_summary_output( - resident, - )) + use server_api::SubjektivMemoryBackendOperation as Op; + // Authentication, live Worker/Job authority and committed Session provenance + // remain Backend-owned. Only the domain operation/projection is shared. + let (expected, label) = match &request.operation { + Op::ResidentSummary(_) => ( + Some(SubjektivWorkerAuthority::Subject), + "resident surface read", + ), + Op::ResidentContext(_) => ( + Some(SubjektivWorkerAuthority::Subject), + "resident subject context read", + ), + Op::ValidateProposal(_) => ( + Some(SubjektivWorkerAuthority::Subject), + "revision proposal validation", + ), + Op::ReceiptStatus(_) => ( + Some(SubjektivWorkerAuthority::Subject), + "explicit candidate receipt lookup", + ), + Op::StageExplicit(_) => ( + Some(SubjektivWorkerAuthority::Subject), + "explicit candidate staging", + ), + Op::ListCandidates(_) => ( + Some(SubjektivWorkerAuthority::Consolidation), + "candidate listing", + ), + Op::ReadCandidate(_) => ( + Some(SubjektivWorkerAuthority::Consolidation), + "candidate read", + ), + Op::DecideCandidate(_) => ( + Some(SubjektivWorkerAuthority::Consolidation), + "candidate decision", + ), + Op::PrepareSurface(_) => ( + Some(SubjektivWorkerAuthority::Consolidation), + "surface generation preparation", + ), + Op::PublishSurface(_) => ( + Some(SubjektivWorkerAuthority::Consolidation), + "surface publication", + ), + Op::FailSurface(_) => ( + Some(SubjektivWorkerAuthority::Consolidation), + "surface generation failure", + ), + Op::Query(_) | Op::Read(_) | Op::ListRevisions(_) => (None, "recall"), + }; + if let Some(expected) = expected { + require_subjektiv_worker_authority(authority, expected, label)?; + } + let host_context = match authority { + SubjektivWorkerAuthority::Subject => { + let attribution = if let Op::StageExplicit(input) = &request.operation { + Some(require_existing_subjektiv_session_attribution( + &api, + &path.workspace_id, + &context, + &input.session_id, + )?) + } else { + None + }; + HostOperationContext::validated_body(&store, &subject_id, attribution)? } - server_api::SubjektivMemoryBackendOperation::ResidentContext(_) => { - require_subjektiv_worker_authority( - authority, - SubjektivWorkerAuthority::Subject, - "resident subject context read", - )?; - let resident = store - .resident_context(&subject_id) - .map_err(subjektiv_store_error)?; - server_api::SubjektivMemoryBackendResponse::ResidentContext( - server_api::SubjektivResidentContextOutput { - behavior_md: resident.subject.behavior_md, - behavior_revision: resident.subject.behavior_revision, - memory_surface: resident_summary_output(resident.surface), - }, - ) - } - server_api::SubjektivMemoryBackendOperation::Query(input) => { - server_api::SubjektivMemoryBackendResponse::Query(subjektiv_memory_query( + SubjektivWorkerAuthority::Consolidation => { + match &request.operation { + Op::ReadCandidate(input) => subjektiv_job::require_candidate_grant( + &api, + &path.workspace_id, + &source_worker, + &input.candidate_id, + )?, + Op::DecideCandidate(input) => subjektiv_job::require_candidate_grant( + &api, + &path.workspace_id, + &source_worker, + &input.candidate_id, + )?, + _ => {} + } + let grant = subjektiv_job::active_grant(&api, &path.workspace_id, &source_worker)?; + let job = api + .store + .worker_registry_projection(&path.workspace_id, &source_worker)? + .and_then(|projection| projection.job) + .map(|binding| JobAttemptBinding { + job_id: binding.job_id, + attempt_id: binding.attempt_id, + }); + HostOperationContext::validated_consolidation( &store, &subject_id, - input, - )?) - } - server_api::SubjektivMemoryBackendOperation::Read(input) => { - server_api::SubjektivMemoryBackendResponse::Read(subjektiv_memory_read( - &store, - &subject_id, - input, - )?) - } - server_api::SubjektivMemoryBackendOperation::ListRevisions(input) => { - server_api::SubjektivMemoryBackendResponse::ListRevisions( - subjektiv_memory_list_revisions(&store, &subject_id, input)?, - ) - } - server_api::SubjektivMemoryBackendOperation::ValidateProposal(input) => { - require_subjektiv_worker_authority( - authority, - SubjektivWorkerAuthority::Subject, - "revision proposal validation", - )?; - server_api::SubjektivMemoryBackendResponse::ProposalValidated( - subjektiv_memory_validate_proposal(&store, &subject_id, input)?, - ) - } - server_api::SubjektivMemoryBackendOperation::ReceiptStatus(input) => { - require_subjektiv_worker_authority( - authority, - SubjektivWorkerAuthority::Subject, - "explicit candidate receipt lookup", - )?; - validate_subjektiv_receipt_id(&input.receipt_id)?; - let candidate = store - .staging_candidate(&subject_id, &input.receipt_id) - .map_err(subjektiv_store_error)?; - server_api::SubjektivMemoryBackendResponse::ReceiptStatus( - server_api::SubjektivMemoryReceiptStatusResponse { - receipt_id: input.receipt_id, - status: if candidate.is_some() { - server_api::SubjektivMemoryReceiptStatus::Staged - } else { - server_api::SubjektivMemoryReceiptStatus::Missing - }, - candidate_id: candidate.map(|record| record.id), - }, - ) - } - server_api::SubjektivMemoryBackendOperation::StageExplicit(input) => { - require_subjektiv_worker_authority( - authority, - SubjektivWorkerAuthority::Subject, - "explicit candidate staging", - )?; - // Re-derive the same subject and immutable Session attribution for the - // write. The request never carries subject, Worker, Runtime, origin, - // or arbitrary source JSON from the model. - let attribution = require_existing_subjektiv_session_attribution( - &api, - &path.workspace_id, - &context, - &input.session_id, - )?; - server_api::SubjektivMemoryBackendResponse::Staged(subjektiv_memory_stage_explicit( - &store, - input, - attribution, - )?) - } - server_api::SubjektivMemoryBackendOperation::ListCandidates(input) => { - require_subjektiv_worker_authority( - authority, - SubjektivWorkerAuthority::Consolidation, - "candidate listing", - )?; - let limit = bounded_subjektiv_limit(input.limit, SUBJEKTIV_QUERY_DEFAULT_LIMIT)?; - let mut candidates = if let Some(grant) = - subjektiv_job::active_grant(&api, &path.workspace_id, &source_worker)? - { - let mut pending = Vec::new(); - for id in &grant.candidate_ids { - if store - .staging_resolution(&subject_id, id) - .map_err(subjektiv_store_error)? - .is_none() - { - if let Some(candidate) = store - .staging_candidate(&subject_id, id) - .map_err(subjektiv_store_error)? - { - pending.push(candidate); - } - } - } - pending - } else { - store - .pending_staging_candidates(&subject_id, limit.saturating_add(1)) - .map_err(subjektiv_store_error)? - }; - let has_more = candidates.len() > limit; - candidates.truncate(limit); - server_api::SubjektivMemoryBackendResponse::Candidates( - server_api::SubjektivMemoryCandidateListResponse { - items: candidates - .into_iter() - .map(subjektiv_candidate_summary) - .collect(), - has_more, - }, - ) - } - server_api::SubjektivMemoryBackendOperation::ReadCandidate(input) => { - require_subjektiv_worker_authority( - authority, - SubjektivWorkerAuthority::Consolidation, - "candidate read", - )?; - subjektiv_job::require_candidate_grant( - &api, - &path.workspace_id, - &source_worker, - &input.candidate_id, - )?; - let candidate = store - .staging_candidate(&subject_id, &input.candidate_id) - .map_err(subjektiv_store_error)? - .ok_or_else(|| { - Error::InvalidInput(format!( - "candidate_not_found: pending candidate `{}` was not found", - input.candidate_id - )) - })?; - if store - .staging_resolution(&subject_id, &input.candidate_id) - .map_err(subjektiv_store_error)? - .is_some() - { - return Err(Error::RepositoryConflict(format!( - "candidate_resolved: candidate `{}` is already resolved", - input.candidate_id - )) - .into()); - } - server_api::SubjektivMemoryBackendResponse::Candidate(subjektiv_candidate(candidate)) - } - server_api::SubjektivMemoryBackendOperation::DecideCandidate(input) => { - require_subjektiv_worker_authority( - authority, - SubjektivWorkerAuthority::Consolidation, - "candidate decision", - )?; - subjektiv_job::require_candidate_grant( - &api, - &path.workspace_id, - &source_worker, - &input.candidate_id, - )?; - let receipt = store - .decide_candidate(&subject_id, subjektiv_candidate_decision(input)?) - .map_err(subjektiv_store_error)?; - server_api::SubjektivMemoryBackendResponse::CandidateDecided( - subjektiv_candidate_decision_response(receipt), - ) - } - server_api::SubjektivMemoryBackendOperation::PrepareSurface(_) => { - require_subjektiv_worker_authority( - authority, - SubjektivWorkerAuthority::Consolidation, - "surface generation preparation", - )?; - if let Some(grant) = - subjektiv_job::active_grant(&api, &path.workspace_id, &source_worker)? - { - for id in &grant.candidate_ids { - if store - .staging_resolution(&subject_id, id) - .map_err(subjektiv_store_error)? - .is_none() - { - return Err(Error::InvalidInput(format!("surface generation requires a durable disposition for batch candidate `{id}`")).into()); - } - } - } - let binding = api - .store - .worker_registry_projection(&path.workspace_id, &source_worker)? - .and_then(|p| p.job); - let generation = store - .prepare_job_surface_generation( - &subject_id, - binding - .as_ref() - .map(|b| (b.job_id.as_str(), b.attempt_id.as_str())), - ) - .map_err(subjektiv_store_error)?; - let resident = store - .resident_surface(&subject_id) - .map_err(subjektiv_store_error)?; - let mut response = subjektiv_surface_prepare_response(generation); - if resident.availability == crate::subjektiv::SurfaceAvailability::Ready { - response.current_snapshot_id = resident - .snapshot - .filter(|s| s.built_from_store_revision == response.store_revision) - .map(|s| s.id); - } - server_api::SubjektivMemoryBackendResponse::SurfacePrepared(response) - } - server_api::SubjektivMemoryBackendOperation::PublishSurface(input) => { - require_subjektiv_worker_authority( - authority, - SubjektivWorkerAuthority::Consolidation, - "surface publication", - )?; - if let Some(binding) = api - .store - .worker_registry_projection(&path.workspace_id, &source_worker)? - .and_then(|p| p.job) - { - store - .require_job_surface_generation( - &subject_id, - &input.generation_id, - &binding.job_id, - &binding.attempt_id, - ) - .map_err(subjektiv_store_error)?; - } - let points = input - .points - .into_iter() - .map(|point| crate::subjektiv::SurfacePoint { - body_md: point.body_md, - memory_refs: point - .memory_refs - .into_iter() - .map(|reference| crate::subjektiv::MemoryRevisionRef { - memory_id: reference.memory_id, - revision: reference.revision, - }) - .collect(), - }) - .collect(); - let snapshot = store - .publish_surface_generation(&subject_id, &input.generation_id, points) - .map_err(subjektiv_store_error)?; - server_api::SubjektivMemoryBackendResponse::SurfacePublished( - server_api::SubjektivSurfacePublishResponse { - snapshot_id: snapshot.id, - built_from_store_revision: snapshot.built_from_store_revision, - empty: snapshot.body_md.is_empty(), - }, - ) - } - server_api::SubjektivMemoryBackendOperation::FailSurface(input) => { - require_subjektiv_worker_authority( - authority, - SubjektivWorkerAuthority::Consolidation, - "surface generation failure", - )?; - if let Some(binding) = api - .store - .worker_registry_projection(&path.workspace_id, &source_worker)? - .and_then(|p| p.job) - { - store - .require_job_surface_generation( - &subject_id, - &input.generation_id, - &binding.job_id, - &binding.attempt_id, - ) - .map_err(subjektiv_store_error)?; - } - let revision = store - .fail_surface_generation(&subject_id, &input.generation_id, &input.reason_code) - .map_err(subjektiv_store_error)?; - let status = if let Some(binding) = api - .store - .worker_registry_projection(&path.workspace_id, &source_worker)? - .and_then(|p| p.job) - { - store - .validate_job_surface_outcome( - &subject_id, - &input.generation_id, - revision, - "failed", - None, - Some(&input.reason_code), - &binding.job_id, - &binding.attempt_id, - ) - .map_err(|e| { - Error::RepositoryConflict(format!( - "surface failure was not recorded for this attempt: {e}" - )) - })?; - "failed_confirmed" - } else { - "failed" - }; - server_api::SubjektivMemoryBackendResponse::SurfaceFailed( - server_api::SubjektivSurfaceFailureResponse { - store_revision: revision, - status: status.into(), - }, - ) + grant.map(|g| g.candidate_ids), + job, + )? } }; - Ok(Json(response)) -} - -fn subjektiv_surface_prepare_response( - generation: crate::subjektiv::SurfaceGeneration, -) -> server_api::SubjektivSurfacePrepareResponse { - server_api::SubjektivSurfacePrepareResponse { - generation_id: generation.id, - current_snapshot_id: None, - store_revision: generation.store_revision, - active_memory_count: generation.active_memory_count, - materials: generation - .materials - .into_iter() - .map(|material| server_api::SubjektivSurfaceMaterial { - memory_id: material.memory_id, - revision: material.revision, - kind: material.kind, - body_md: material.body_md, - why_useful: material.why_useful, - staleness: material.staleness, - }) - .collect(), - body_token_budget: crate::subjektiv::SURFACE_BODY_TOKEN_BUDGET, - input_token_budget: crate::subjektiv::SURFACE_INPUT_TOKEN_BUDGET, - per_kind_limit: crate::subjektiv::SURFACE_PER_KIND_LIMIT, - total_material_limit: crate::subjektiv::SURFACE_TOTAL_MATERIAL_LIMIT, - } -} - -fn subjektiv_candidate_summary( - candidate: crate::subjektiv::SubjectStagingRecord, -) -> server_api::SubjektivMemoryCandidateSummary { - server_api::SubjektivMemoryCandidateSummary { - candidate_id: candidate.id, - kind: candidate.kind, - claim: candidate.claim, - revision_proposal: candidate.revision_proposal.map(subjektiv_proposal_metadata), - created_at: candidate.created_at, - } -} - -fn subjektiv_candidate( - candidate: crate::subjektiv::SubjectStagingRecord, -) -> server_api::SubjektivMemoryCandidate { - server_api::SubjektivMemoryCandidate { - candidate_id: candidate.id, - kind: candidate.kind, - claim: candidate.claim, - why_useful: candidate.why_useful, - staleness: candidate.staleness, - source: candidate.source, - evidence: candidate.evidence, - source_refs: candidate.source_refs, - revision_proposal: candidate.revision_proposal.map(subjektiv_proposal_metadata), - created_at: candidate.created_at, - } -} - -fn subjektiv_proposal_metadata( - proposal: crate::subjektiv::RevisionProposal, -) -> server_api::SubjektivMemoryRevisionProposalMetadata { - server_api::SubjektivMemoryRevisionProposalMetadata { - memory_id: proposal.memory_id, - expected_revision: proposal.expected_revision, - intent: match proposal.intent { - crate::subjektiv::RevisionProposalIntent::Revise => { - server_api::SubjektivMemoryRevisionIntent::Revise - } - crate::subjektiv::RevisionProposalIntent::Resolve => { - server_api::SubjektivMemoryRevisionIntent::Resolve - } - crate::subjektiv::RevisionProposalIntent::Retract => { - server_api::SubjektivMemoryRevisionIntent::Retract - } - crate::subjektiv::RevisionProposalIntent::Reopen => { - server_api::SubjektivMemoryRevisionIntent::Reopen - } - }, - change_reason: proposal.change_reason, - } -} - -fn subjektiv_candidate_decision( - input: server_api::SubjektivMemoryCandidateDecisionRequest, -) -> ApiResult { - let decision = match input.decision { - server_api::SubjektivMemoryCandidateDecision::Apply { target, memory } => { - let target = match target { - server_api::SubjektivMemoryApplyTarget::Create => { - crate::subjektiv::MemoryRevisionTarget::Create - } - server_api::SubjektivMemoryApplyTarget::Revise { - memory_id, - expected_revision, - } => crate::subjektiv::MemoryRevisionTarget::Revise { - memory_id, - expected_revision, - }, - }; - crate::subjektiv::CandidateDecision::Apply { - target, - draft: crate::subjektiv::MemoryDraft { - kind: memory.kind, - state: domain_memory_state(memory.state), - claim: memory.claim, - body_md: memory.body_md, - why_useful: memory.why_useful, - staleness: memory.staleness, - source_candidate_ids: Vec::new(), - derived_from: memory - .derived_from - .into_iter() - .map(|reference| crate::subjektiv::MemoryRevisionRef { - memory_id: reference.memory_id, - revision: reference.revision, - }) - .collect(), - change_reason: memory.change_reason, - }, - } - } - server_api::SubjektivMemoryCandidateDecision::Close { - action, - affected_memory, - } => crate::subjektiv::CandidateDecision::Close { - action: match action { - server_api::SubjektivMemoryCandidateCloseAction::Discarded => { - crate::subjektiv::StagingResolutionAction::Discarded - } - server_api::SubjektivMemoryCandidateCloseAction::Invalid => { - crate::subjektiv::StagingResolutionAction::Invalid - } - server_api::SubjektivMemoryCandidateCloseAction::Duplicate => { - crate::subjektiv::StagingResolutionAction::Duplicate - } - server_api::SubjektivMemoryCandidateCloseAction::AlreadyCovered => { - crate::subjektiv::StagingResolutionAction::AlreadyCovered - } - }, - affected_memory: affected_memory - .into_iter() - .map(|reference| crate::subjektiv::MemoryRevisionRef { - memory_id: reference.memory_id, - revision: reference.revision, - }) - .collect(), - }, - }; - Ok(crate::subjektiv::CandidateDecisionRequest { - request_id: input.request_id, - candidate_id: input.candidate_id, - reason: input.reason, - decision, - }) -} - -fn subjektiv_candidate_decision_response( - receipt: crate::subjektiv::CandidateDecisionReceipt, -) -> server_api::SubjektivMemoryCandidateDecisionResponse { - let operation = match receipt.operation { - Some(crate::subjektiv::MemoryDecisionOperation::Create) => { - server_api::SubjektivMemoryAffectedOperation::Create - } - Some(crate::subjektiv::MemoryDecisionOperation::Revise) => { - server_api::SubjektivMemoryAffectedOperation::Revise - } - Some(crate::subjektiv::MemoryDecisionOperation::Reference) | None => { - server_api::SubjektivMemoryAffectedOperation::Reference - } - }; - server_api::SubjektivMemoryCandidateDecisionResponse { - request_id: receipt.request_id, - candidate_id: receipt.candidate_id, - action: match receipt.resolution.action { - crate::subjektiv::StagingResolutionAction::Applied => { - server_api::SubjektivMemoryCandidateResolutionAction::Applied - } - crate::subjektiv::StagingResolutionAction::Discarded => { - server_api::SubjektivMemoryCandidateResolutionAction::Discarded - } - crate::subjektiv::StagingResolutionAction::Invalid => { - server_api::SubjektivMemoryCandidateResolutionAction::Invalid - } - crate::subjektiv::StagingResolutionAction::Duplicate => { - server_api::SubjektivMemoryCandidateResolutionAction::Duplicate - } - crate::subjektiv::StagingResolutionAction::AlreadyCovered => { - server_api::SubjektivMemoryCandidateResolutionAction::AlreadyCovered - } - }, - reason: receipt.resolution.reason, - affected_memory: receipt - .resolution - .affected_memory - .into_iter() - .map(|reference| server_api::SubjektivMemoryAffectedRef { - memory_id: reference.memory_id, - revision: reference.revision, - operation, - }) - .collect(), - memory: receipt - .memory - .map(|memory| server_api::SubjektivMemoryRevisionRef { - memory_id: memory.id, - revision: memory.revision, - }), - store_revision: receipt.store_revision, - surface_dirty: receipt.surface_dirty, - } -} - -fn domain_memory_state(state: server_api::SubjektivMemoryState) -> crate::subjektiv::MemoryState { - match state { - server_api::SubjektivMemoryState::Active => crate::subjektiv::MemoryState::Active, - server_api::SubjektivMemoryState::Resolved => crate::subjektiv::MemoryState::Resolved, - server_api::SubjektivMemoryState::Retracted => crate::subjektiv::MemoryState::Retracted, - } -} - -fn subjektiv_memory_query( - store: &crate::subjektiv::SubjektivStore, - subject_id: &str, - input: server_api::SubjektivMemoryQueryRequest, -) -> ApiResult { - let limit = bounded_subjektiv_limit(input.limit, SUBJEKTIV_QUERY_DEFAULT_LIMIT)?; - let query = input - .query - .map(|value| value.trim().to_lowercase()) - .filter(|value| !value.is_empty()); - let kinds = canonical_subjektiv_kinds(input.kinds)?; - let states = canonical_subjektiv_states(input.states)?; - let subject = store - .subject(subject_id) - .map_err(subjektiv_store_error)? - .ok_or_else(|| Error::SubjektivSubjectNotFound(subject_id.to_string()))?; - let mut offset = 0; - if let Some(cursor) = input.cursor { - let cursor: SubjektivQueryCursor = decode_subjektiv_cursor("query", &cursor)?; - if cursor.subject_id != subject_id - || cursor.query != query - || cursor.kinds != kinds - || cursor.states != states - { - return Err(Error::InvalidInput( - "subjektiv query cursor does not match subject or filters".into(), - ) - .into()); - } - if cursor.store_revision != subject.store_revision { - return Err(Error::RepositoryConflict( - "stale_cursor: subjektiv query cursor is stale because Memory changed; restart the query".into(), - ) - .into()); - } - offset = cursor.offset; - } - - let kind_filter = kinds.iter().cloned().collect::>(); - let state_filter = states.iter().cloned().collect::>(); - let records = store - .list_memories(subject_id) - .map_err(subjektiv_store_error)? - .into_iter() - .filter(|record| { - kind_filter.contains(record.kind.as_str()) - && state_filter.contains(subjektiv_memory_state_name(record.state)) - && query.as_ref().is_none_or(|query| { - record.claim.to_lowercase().contains(query) - || record.body_md.to_lowercase().contains(query) - }) - }) - .collect::>(); - if offset > records.len() { - return Err(Error::InvalidInput("subjektiv query cursor offset is invalid".into()).into()); - } - let end = offset.saturating_add(limit).min(records.len()); - let has_more = end < records.len(); - let items = records[offset..end] - .iter() - .map(|record| server_api::SubjektivMemoryQueryItem { - id: record.id.clone(), - revision: record.revision, - kind: record.kind.clone(), - state: api_memory_state(record.state), - claim: record.claim.clone(), - excerpt: bounded_memory_excerpt(&record.body_md, 240), - updated_at: record.updated_at.clone(), - }) - .collect(); - let next_cursor = has_more - .then(|| { - encode_subjektiv_cursor( - "query", - &SubjektivQueryCursor { - subject_id: subject_id.to_string(), - store_revision: subject.store_revision, - query, - kinds, - states, - offset: end, - }, - ) - }) - .transpose()?; - Ok(server_api::SubjektivMemoryQueryResponse { - items, - next_cursor, - has_more, - }) -} - -fn subjektiv_memory_read( - store: &crate::subjektiv::SubjektivStore, - subject_id: &str, - input: server_api::SubjektivMemoryReadRequest, -) -> ApiResult { - let current = store - .scoped_memory(subject_id, &input.memory_id) - .map_err(subjektiv_store_error)?; - let record = match input.revision { - Some(0) => { - return Err(Error::InvalidInput("revision must be a positive integer".into()).into()); - } - Some(revision) => store - .scoped_memory_revision(subject_id, &input.memory_id, revision) - .map_err(subjektiv_store_error)? - .ok_or_else(|| { - Error::InvalidInput(format!( - "memory_revision_not_found: Memory `{}` has no revision {revision}", - input.memory_id - )) - })?, - None => current.clone(), - }; - let body_limit = input.limit.unwrap_or(SUBJEKTIV_BODY_DEFAULT_LINES); - if body_limit == 0 || body_limit > SUBJEKTIV_BODY_MAX_LINES { - return Err(Error::InvalidInput(format!( - "body line limit must be within 1..={SUBJEKTIV_BODY_MAX_LINES}" - )) - .into()); - } - let body_offset = input.offset.unwrap_or(0); - let body_byte_offset = input.byte_offset.unwrap_or(0); - if (body_offset > 0 || body_byte_offset > 0) && input.revision.is_none() { - return Err(Error::InvalidInput( - "body continuation requires the exact revision returned by the first read".into(), - ) - .into()); - } - let lines = memory_body_lines(&record.body_md); - if body_offset > lines.len() { - return Err(Error::InvalidInput("body line offset exceeds document length".into()).into()); - } - if body_offset == lines.len() && body_byte_offset > 0 { - return Err(Error::InvalidInput("body byte offset exceeds document length".into()).into()); - } - if let Some(line) = lines.get(body_offset) - && (body_byte_offset > line.len() || !line.is_char_boundary(body_byte_offset)) - { - return Err(Error::InvalidInput( - "body byte offset must be a UTF-8 boundary within the selected line".into(), - ) - .into()); - } - let (body_md, body_next) = bounded_memory_body_page( - &lines, - body_offset, - body_byte_offset, - body_limit, - SUBJEKTIV_BODY_MAX_BYTES, - ); - let body_truncated = body_next.is_some(); - let (body_next_offset, body_next_byte_offset) = body_next - .map(|(line, byte)| (Some(line), Some(byte))) - .unwrap_or((None, None)); - - let (evidence_offset, nested_evidence_offset, nested_source_ref_offset) = - if let Some(cursor) = input.evidence_cursor { - let cursor: SubjektivEvidenceCursor = decode_subjektiv_cursor("evidence", &cursor)?; - if cursor.subject_id != subject_id - || cursor.memory_id != record.id - || cursor.revision != record.revision - { - return Err(Error::InvalidInput( - "subjektiv evidence cursor does not match subject, Memory, or revision".into(), - ) - .into()); - } - ( - cursor.offset, - cursor.evidence_offset, - cursor.source_ref_offset, - ) - } else { - (0, 0, 0) - }; - let total_evidence = record - .source_candidate_ids - .len() - .saturating_add(record.derived_from.len()); - if evidence_offset > total_evidence { - return Err( - Error::InvalidInput("subjektiv evidence cursor offset is invalid".into()).into(), - ); - } - - let mut source_candidate_ids = Vec::new(); - let mut source_candidates = Vec::new(); - let mut derived_from = Vec::new(); - let mut next_evidence_offset = evidence_offset; - let mut next_nested_evidence_offset = 0; - let mut next_nested_source_ref_offset = 0; - if let Some(candidate_id) = record.source_candidate_ids.get(evidence_offset) { - let candidate = store - .staging_candidate(subject_id, candidate_id) - .map_err(subjektiv_store_error)? - .ok_or_else(|| { - Error::Store(format!( - "subjektiv candidate provenance `{candidate_id}` is missing" - )) - })?; - let evidence_total = candidate.evidence.len(); - let source_refs_total = candidate.source_refs.len(); - if nested_evidence_offset > evidence_total || nested_source_ref_offset > source_refs_total { - return Err(Error::InvalidInput( - "subjektiv nested evidence cursor offset is invalid".into(), - ) - .into()); - } - let evidence_end = nested_evidence_offset - .saturating_add(SUBJEKTIV_EVIDENCE_ANCHOR_PAGE_SIZE) - .min(evidence_total); - let source_ref_end = nested_source_ref_offset - .saturating_add(SUBJEKTIV_EVIDENCE_ANCHOR_PAGE_SIZE) - .min(source_refs_total); - source_candidate_ids.push(candidate_id.clone()); - source_candidates.push(server_api::SubjektivMemoryEvidenceCandidate { - candidate_id: candidate.id, - evidence: candidate - .evidence - .into_iter() - .skip(nested_evidence_offset) - .take(evidence_end - nested_evidence_offset) - .map(bounded_subjektiv_staging_evidence) - .collect(), - evidence_total, - evidence_truncated: nested_evidence_offset > 0 || evidence_end < evidence_total, - source_refs: candidate - .source_refs - .into_iter() - .skip(nested_source_ref_offset) - .take(source_ref_end - nested_source_ref_offset) - .map(bounded_subjektiv_source_ref) - .collect(), - source_refs_total, - source_refs_truncated: nested_source_ref_offset > 0 - || source_ref_end < source_refs_total, - }); - if evidence_end < evidence_total || source_ref_end < source_refs_total { - next_nested_evidence_offset = evidence_end; - next_nested_source_ref_offset = source_ref_end; - } else { - next_evidence_offset += 1; - } - } else if evidence_offset < total_evidence { - if nested_evidence_offset != 0 || nested_source_ref_offset != 0 { - return Err(Error::InvalidInput( - "subjektiv derivation cursor cannot contain nested offsets".into(), - ) - .into()); - } - let derived_index = evidence_offset - record.source_candidate_ids.len(); - let reference = &record.derived_from[derived_index]; - derived_from.push(server_api::SubjektivMemoryRevisionRef { - memory_id: reference.memory_id.clone(), - revision: reference.revision, - }); - next_evidence_offset += 1; - } else if nested_evidence_offset != 0 || nested_source_ref_offset != 0 { - return Err(Error::InvalidInput( - "subjektiv evidence cursor points beyond the final reference".into(), - ) - .into()); - } - bound_subjektiv_nested_evidence(&source_candidates)?; - let evidence_has_more = next_evidence_offset < total_evidence - || next_nested_evidence_offset != 0 - || next_nested_source_ref_offset != 0; - let evidence_next_cursor = evidence_has_more - .then(|| { - encode_subjektiv_cursor( - "evidence", - &SubjektivEvidenceCursor { - subject_id: subject_id.to_string(), - memory_id: record.id.clone(), - revision: record.revision, - offset: next_evidence_offset, - evidence_offset: next_nested_evidence_offset, - source_ref_offset: next_nested_source_ref_offset, - }, - ) - }) - .transpose()?; - - let mut response = server_api::SubjektivMemoryReadResponse { - memory_id: record.id, - revision: record.revision, - current_revision: current.revision, - kind: record.kind, - state: api_memory_state(record.state), - claim: record.claim, - body_md, - why_useful: record.why_useful, - staleness: record.staleness, - change_reason: record.change_reason, - created_at: record.created_at, - updated_at: record.updated_at, - body_offset, - body_byte_offset, - body_next_offset, - body_next_byte_offset, - body_truncated, - source_candidate_ids, - source_candidates, - derived_from, - evidence_next_cursor, - evidence_has_more, - }; - fit_subjektiv_read_body_to_output_budget( - &mut response, - &lines, - body_offset, - body_byte_offset, - body_limit, - )?; - Ok(response) -} - -fn subjektiv_memory_list_revisions( - store: &crate::subjektiv::SubjektivStore, - subject_id: &str, - input: server_api::SubjektivMemoryListRevisionsRequest, -) -> ApiResult { - let limit = bounded_subjektiv_limit(input.limit, SUBJEKTIV_QUERY_DEFAULT_LIMIT)?; - let current = store - .scoped_memory(subject_id, &input.memory_id) - .map_err(subjektiv_store_error)?; - let (max_revision, offset) = if let Some(cursor) = input.cursor { - let cursor: SubjektivRevisionCursor = decode_subjektiv_cursor("revisions", &cursor)?; - if cursor.subject_id != subject_id || cursor.memory_id != input.memory_id { - return Err(Error::InvalidInput( - "subjektiv revision cursor does not match subject or Memory".into(), - ) - .into()); - } - (cursor.max_revision, cursor.offset) - } else { - (current.revision, 0) - }; - let records = store - .list_memory_revisions(subject_id, &input.memory_id) - .map_err(subjektiv_store_error)? - .into_iter() - .filter(|record| record.revision <= max_revision) - .collect::>(); - if offset > records.len() { - return Err( - Error::InvalidInput("subjektiv revision cursor offset is invalid".into()).into(), - ); - } - let end = offset.saturating_add(limit).min(records.len()); - let has_more = end < records.len(); - let items = records[offset..end] - .iter() - .map(|record| server_api::SubjektivMemoryRevisionItem { - revision: record.revision, - kind: record.kind.clone(), - state: api_memory_state(record.state), - claim: record.claim.clone(), - change_reason: record.change_reason.clone(), - updated_at: record.updated_at.clone(), - }) - .collect(); - let next_cursor = has_more - .then(|| { - encode_subjektiv_cursor( - "revisions", - &SubjektivRevisionCursor { - subject_id: subject_id.to_string(), - memory_id: input.memory_id.clone(), - max_revision, - offset: end, - }, - ) - }) - .transpose()?; - Ok(server_api::SubjektivMemoryListRevisionsResponse { - memory_id: input.memory_id, - current_revision: current.revision, - items, - next_cursor, - has_more, - }) -} - -fn subjektiv_memory_validate_proposal( - store: &crate::subjektiv::SubjektivStore, - subject_id: &str, - input: server_api::SubjektivMemoryValidateProposalRequest, -) -> ApiResult { - if input.expected_revision == 0 { - return Err(Error::InvalidInput("expected_revision must be positive".into()).into()); - } - let current = store - .scoped_memory(subject_id, &input.memory_id) - .map_err(subjektiv_store_error)?; - if current.revision != input.expected_revision { - return Err(Error::RepositoryConflict(format!( - "revision_conflict: Memory `{}` expected {}, current {}", - input.memory_id, input.expected_revision, current.revision - )) - .into()); - } - validate_subjektiv_proposal_transition(current.state, input.intent)?; - Ok(server_api::SubjektivMemoryProposalValidationResponse { - memory_id: input.memory_id, - current_revision: current.revision, - kind: current.kind, - state: api_memory_state(current.state), - intent: input.intent, - }) -} - -fn validate_subjektiv_receipt_id(receipt_id: &str) -> ApiResult<()> { - let receipt_id = receipt_id.trim(); - if receipt_id.is_empty() || receipt_id.len() > 512 || receipt_id.chars().any(char::is_control) { - return Err(Error::InvalidInput("explicit Memory receipt_id is invalid".into()).into()); - } - Ok(()) -} - -fn subjektiv_memory_stage_explicit( - store: &crate::subjektiv::SubjektivStore, - mut input: server_api::SubjektivMemoryStageExplicitRequest, - attribution: crate::subjektiv::SubjectSessionAttribution, -) -> ApiResult { - validate_subjektiv_receipt_id(&input.receipt_id)?; - let receipt_id = input.receipt_id.trim(); - if input.evidence.is_empty() || input.source_refs.is_empty() { - return Err(Error::InvalidInput( - "explicit Memory staging requires host-resolved committed evidence".into(), - ) - .into()); - } - if input.evidence.len() > SUBJEKTIV_CANDIDATE_ANCHOR_LIMIT - || input.source_refs.len() > SUBJEKTIV_CANDIDATE_ANCHOR_LIMIT - { - return Err(Error::InvalidInput(format!( - "explicit Memory evidence/source_refs are limited to {SUBJEKTIV_CANDIDATE_ANCHOR_LIMIT} items each" - )) - .into()); - } - if matches!(input.kind, memory::extract::CandidateKind::Preference) - && input.evidence.iter().any(|evidence| { - !matches!( - evidence.origin.as_ref().map(|origin| &origin.kind), - Some(memory::schema::EvidenceOriginKind::HumanInput) - ) - }) - { - return Err(Error::InvalidInput( - "preference candidates require exclusively HumanInput evidence".into(), - ) - .into()); - } - let mut range_start = u64::MAX; - let mut range_end = 0; - let mut segment_id = None; - for source_ref in &mut input.source_refs { - if source_ref - .session_id - .as_deref() - .is_some_and(|value| value != attribution.session_id) - { - return Err(Error::InvalidInput( - "explicit candidate source belongs to a different Session".into(), - ) - .into()); - } - source_ref.session_id = Some(attribution.session_id.clone()); - let source_segment = source_ref.segment_id.as_deref().ok_or_else(|| { - Error::InvalidInput("explicit candidate source is missing segment_id".into()) - })?; - if segment_id - .as_deref() - .is_some_and(|value| value != source_segment) - { - return Err(Error::InvalidInput( - "explicit candidate evidence spans multiple Session segments".into(), - ) - .into()); - } - segment_id = Some(source_segment.to_string()); - let range = source_ref.entry_range.ok_or_else(|| { - Error::InvalidInput("explicit candidate source is missing entry_range".into()) - })?; - range_start = range_start.min(range[0]); - range_end = range_end.max(range[1]); - } - let source = memory::schema::SourceRef { - segment_id: segment_id.ok_or_else(|| { - Error::InvalidInput("explicit candidate source is missing segment_id".into()) - })?, - range: [range_start, range_end], - }; - let proposal = input - .proposal - .as_ref() - .map(|proposal| { - crate::subjektiv::RevisionProposal::new( - domain_proposal_intent(proposal.intent), - proposal.memory_id.clone(), - proposal.expected_revision, - proposal.change_reason.clone(), - ) - }) - .transpose() - .map_err(subjektiv_store_error)?; - if let Some(proposal) = &proposal { - let current = store - .scoped_memory(&attribution.subject_id, &proposal.memory_id) - .map_err(subjektiv_store_error)?; - if current.kind != input.kind { - return Err(Error::InvalidInput( - "revision proposal kind must be derived from its target Memory".into(), - ) - .into()); - } - } - let candidate = memory::extract::ExtractedCandidate { - kind: input.kind, - claim: input.claim, - why_useful: input.why_useful, - staleness: input.staleness, - evidence_ids: input.evidence.iter().map(|item| item.id.clone()).collect(), - }; - let staging = memory::extract::StagingRecord::from_candidate( - receipt_id, - format!("explicit:{receipt_id}"), - source, - candidate, - input.evidence, - input.source_refs, - ); - let mut staging = - crate::subjektiv::SubjectStagingRecord::attach(&attribution.subject_id, staging); - if let Some(proposal) = proposal.clone() { - staging - .attach_revision_proposal(proposal) - .map_err(subjektiv_store_error)?; - } - let (staged, _) = store - .stage_candidate_with_attribution(staging, attribution) - .map_err(subjektiv_store_error)?; - Ok(server_api::SubjektivMemoryStageExplicitResponse { - candidate_id: staged.id, - receipt_id: receipt_id.to_string(), - status: server_api::SubjektivMemoryReceiptStatus::Staged, - target: proposal - .as_ref() - .map(|proposal| server_api::SubjektivMemoryProposalTarget { - memory_id: proposal.memory_id.clone(), - expected_revision: proposal.expected_revision, - }), - intent: proposal - .as_ref() - .map(|proposal| api_proposal_intent(proposal.intent)), - }) -} - -fn validate_subjektiv_proposal_transition( - state: crate::subjektiv::MemoryState, - intent: server_api::SubjektivMemoryRevisionIntent, -) -> ApiResult<()> { - use crate::subjektiv::MemoryState::{Active, Resolved}; - use server_api::SubjektivMemoryRevisionIntent::{Reopen, Resolve, Retract, Revise}; - let valid = match intent { - Revise => matches!(state, Active | Resolved), - Resolve => state == Active, - Retract => matches!(state, Active | Resolved), - Reopen => state == Resolved, - }; - if valid { - Ok(()) - } else { - Err(Error::InvalidInput(format!( - "invalid_state_transition: {state:?} cannot accept {intent:?}" - )) - .into()) - } -} - -fn canonical_subjektiv_kinds( - kinds: Option>, -) -> ApiResult> { - let kinds = kinds.unwrap_or_else(|| { - use memory::extract::CandidateKind::*; - vec![ - Preference, - WorkingAssumption, - Constraint, - Decision, - OpenQuestion, - Lesson, - ] - }); - if kinds.is_empty() { - return Err(Error::InvalidInput("kinds must not be an empty array".into()).into()); - } - let mut names = kinds - .iter() - .map(|kind| kind.as_str().to_string()) - .collect::>(); - names.sort(); - names.dedup(); - Ok(names) -} - -fn canonical_subjektiv_states( - states: Option>, -) -> ApiResult> { - let states = states.unwrap_or_else(|| vec![server_api::SubjektivMemoryState::Active]); - if states.is_empty() { - return Err(Error::InvalidInput("states must not be an empty array".into()).into()); - } - let mut names = states - .into_iter() - .map(|state| match state { - server_api::SubjektivMemoryState::Active => "active".to_string(), - server_api::SubjektivMemoryState::Resolved => "resolved".to_string(), - server_api::SubjektivMemoryState::Retracted => "retracted".to_string(), - }) - .collect::>(); - names.sort(); - names.dedup(); - Ok(names) -} - -fn bounded_subjektiv_limit(limit: Option, default: usize) -> ApiResult { - let limit = limit.unwrap_or(default); - if limit == 0 || limit > SUBJEKTIV_QUERY_MAX_LIMIT { - return Err(Error::InvalidInput(format!( - "limit must be within 1..={SUBJEKTIV_QUERY_MAX_LIMIT}" - )) - .into()); - } - Ok(limit) -} - -fn api_memory_state(state: crate::subjektiv::MemoryState) -> server_api::SubjektivMemoryState { - match state { - crate::subjektiv::MemoryState::Active => server_api::SubjektivMemoryState::Active, - crate::subjektiv::MemoryState::Resolved => server_api::SubjektivMemoryState::Resolved, - crate::subjektiv::MemoryState::Retracted => server_api::SubjektivMemoryState::Retracted, - } -} - -fn subjektiv_memory_state_name(state: crate::subjektiv::MemoryState) -> &'static str { - match state { - crate::subjektiv::MemoryState::Active => "active", - crate::subjektiv::MemoryState::Resolved => "resolved", - crate::subjektiv::MemoryState::Retracted => "retracted", - } -} - -fn domain_proposal_intent( - intent: server_api::SubjektivMemoryRevisionIntent, -) -> crate::subjektiv::RevisionProposalIntent { - match intent { - server_api::SubjektivMemoryRevisionIntent::Revise => { - crate::subjektiv::RevisionProposalIntent::Revise - } - server_api::SubjektivMemoryRevisionIntent::Resolve => { - crate::subjektiv::RevisionProposalIntent::Resolve - } - server_api::SubjektivMemoryRevisionIntent::Retract => { - crate::subjektiv::RevisionProposalIntent::Retract - } - server_api::SubjektivMemoryRevisionIntent::Reopen => { - crate::subjektiv::RevisionProposalIntent::Reopen - } - } -} - -fn api_proposal_intent( - intent: crate::subjektiv::RevisionProposalIntent, -) -> server_api::SubjektivMemoryRevisionIntent { - match intent { - crate::subjektiv::RevisionProposalIntent::Revise => { - server_api::SubjektivMemoryRevisionIntent::Revise - } - crate::subjektiv::RevisionProposalIntent::Resolve => { - server_api::SubjektivMemoryRevisionIntent::Resolve - } - crate::subjektiv::RevisionProposalIntent::Retract => { - server_api::SubjektivMemoryRevisionIntent::Retract - } - crate::subjektiv::RevisionProposalIntent::Reopen => { - server_api::SubjektivMemoryRevisionIntent::Reopen - } - } -} - -fn fit_subjektiv_read_body_to_output_budget( - response: &mut server_api::SubjektivMemoryReadResponse, - lines: &[&str], - line_offset: usize, - byte_offset: usize, - line_limit: usize, -) -> ApiResult<()> { - let serialized_len = |response: &server_api::SubjektivMemoryReadResponse| { - serde_json::to_vec_pretty(response) - .map(|value| value.len()) - .map_err(|error| Error::Store(format!("encode subjektiv Memory read: {error}"))) - }; - if serialized_len(response)? <= server_api::SUBJEKTIV_MEMORY_READ_MAX_TOOL_CONTENT_BYTES { - return Ok(()); - } - - let initial_body_len = response.body_md.len(); - let mut lower = 0usize; - let mut upper = initial_body_len; - while lower < upper { - let candidate_limit = lower + (upper - lower).div_ceil(2); - set_subjektiv_read_body_page( - response, - lines, - line_offset, - byte_offset, - line_limit, - candidate_limit, - ); - if serialized_len(response)? <= server_api::SUBJEKTIV_MEMORY_READ_MAX_TOOL_CONTENT_BYTES { - lower = candidate_limit; - } else { - upper = candidate_limit - 1; - } - } - set_subjektiv_read_body_page(response, lines, line_offset, byte_offset, line_limit, lower); - let final_len = serialized_len(response)?; - if final_len > server_api::SUBJEKTIV_MEMORY_READ_MAX_TOOL_CONTENT_BYTES { - return Err(Error::Store(format!( - "subjektiv Memory metadata/provenance requires {final_len} bytes and exceeds the {} byte read-output budget", - server_api::SUBJEKTIV_MEMORY_READ_MAX_TOOL_CONTENT_BYTES - )) - .into()); - } - if initial_body_len > 0 && response.body_md.is_empty() { - return Err(Error::Store( - "subjektiv Memory metadata/provenance leaves no room for resumable body content".into(), - ) - .into()); - } - Ok(()) -} - -fn set_subjektiv_read_body_page( - response: &mut server_api::SubjektivMemoryReadResponse, - lines: &[&str], - line_offset: usize, - byte_offset: usize, - line_limit: usize, - byte_limit: usize, -) { - let (body_md, body_next) = - bounded_memory_body_page(lines, line_offset, byte_offset, line_limit, byte_limit); - response.body_md = body_md; - response.body_truncated = body_next.is_some(); - (response.body_next_offset, response.body_next_byte_offset) = body_next - .map(|(line, byte)| (Some(line), Some(byte))) - .unwrap_or((None, None)); -} - -fn bounded_memory_body_page( - lines: &[&str], - line_offset: usize, - byte_offset: usize, - line_limit: usize, - byte_limit: usize, -) -> (String, Option<(usize, usize)>) { - let line_end = line_offset.saturating_add(line_limit).min(lines.len()); - let mut line_index = line_offset; - let mut within_line = byte_offset; - let mut body = String::new(); - while line_index < line_end { - let line = lines[line_index]; - let remainder = &line[within_line..]; - let available = byte_limit.saturating_sub(body.len()); - if remainder.len() <= available { - body.push_str(remainder); - line_index += 1; - within_line = 0; - continue; - } - let mut take = available.min(remainder.len()); - while take > 0 && !remainder.is_char_boundary(take) { - take -= 1; - } - if take == 0 { - return (body, Some((line_index, within_line))); - } - body.push_str(&remainder[..take]); - within_line += take; - return (body, Some((line_index, within_line))); - } - if line_index < lines.len() { - (body, Some((line_index, 0))) - } else { - (body, None) - } -} - -fn memory_body_lines(body: &str) -> Vec<&str> { - if body.is_empty() { - Vec::new() - } else { - body.split_inclusive('\n').collect() - } -} - -fn bound_subjektiv_nested_evidence( - candidates: &[server_api::SubjektivMemoryEvidenceCandidate], -) -> ApiResult<()> { - let serialized = serde_json::to_vec(candidates) - .map_err(|error| Error::Store(format!("encode subjektiv evidence page: {error}")))?; - if serialized.len() <= SUBJEKTIV_NESTED_EVIDENCE_MAX_BYTES { - Ok(()) - } else { - Err(Error::Store( - "subjektiv evidence page exceeds its enforced response byte budget".into(), - ) - .into()) - } -} - -fn bounded_subjektiv_staging_evidence( - mut evidence: memory::extract::StagingEvidence, -) -> memory::extract::StagingEvidence { - evidence.kind = memory::schema::EvidenceKind::new(bounded_utf8_bytes( - evidence.kind.as_str().to_string(), - SUBJEKTIV_ANCHOR_TEXT_MAX_BYTES, - )); - evidence.origin = evidence.origin.map(bounded_subjektiv_evidence_origin); - evidence.excerpt = evidence - .excerpt - .map(|value| bounded_utf8_bytes(value, SUBJEKTIV_ANCHOR_TEXT_MAX_BYTES)); - evidence.summary = evidence - .summary - .map(|value| bounded_utf8_bytes(value, SUBJEKTIV_ANCHOR_TEXT_MAX_BYTES)); - evidence -} - -fn bounded_subjektiv_source_ref( - mut source: memory::schema::SourceEvidenceRef, -) -> memory::schema::SourceEvidenceRef { - source.origin = source.origin.map(bounded_subjektiv_evidence_origin); - source.evidence_kind = source.evidence_kind.map(|kind| { - memory::schema::EvidenceKind::new(bounded_utf8_bytes( - kind.as_str().to_string(), - SUBJEKTIV_ANCHOR_TEXT_MAX_BYTES, - )) - }); - source.label = source - .label - .map(|value| bounded_utf8_bytes(value, SUBJEKTIV_ANCHOR_TEXT_MAX_BYTES)); - source.summary = source - .summary - .map(|value| bounded_utf8_bytes(value, SUBJEKTIV_ANCHOR_TEXT_MAX_BYTES)); - source -} - -fn bounded_subjektiv_evidence_origin( - mut origin: memory::schema::EvidenceOrigin, -) -> memory::schema::EvidenceOrigin { - for field in [ - &mut origin.account_id, - &mut origin.workspace_id, - &mut origin.runtime_id, - &mut origin.worker_id, - &mut origin.flow_selector, - &mut origin.flow_definition_id, - ] { - if let Some(value) = field.take() { - *field = Some(bounded_utf8_bytes(value, SUBJEKTIV_ANCHOR_TEXT_MAX_BYTES)); - } - } - origin -} - -fn bounded_utf8_bytes(value: String, max_bytes: usize) -> String { - let value = value - .chars() - .map(|character| { - if character.is_control() || matches!(character, '"' | '\\') { - ' ' - } else { - character - } - }) - .collect::(); - if value.len() <= max_bytes { - return value; - } - let marker = "…"; - let mut end = max_bytes.saturating_sub(marker.len()); - while end > 0 && !value.is_char_boundary(end) { - end -= 1; - } - format!("{}{marker}", &value[..end]) -} - -fn bounded_memory_excerpt(body: &str, max_chars: usize) -> String { - let normalized = body.split_whitespace().collect::>().join(" "); - let mut chars = normalized.chars(); - let excerpt = chars.by_ref().take(max_chars).collect::(); - if chars.next().is_some() { - format!("{excerpt}…") - } else { - excerpt - } -} - -fn encode_subjektiv_cursor(kind: &str, value: &T) -> ApiResult { - let raw = serde_json::to_vec(value) - .map_err(|error| Error::Store(format!("encode subjektiv cursor: {error}")))?; - let encoded = raw - .iter() - .map(|byte| format!("{byte:02x}")) - .collect::(); - Ok(format!("subjektiv.{kind}.{encoded}")) -} - -fn decode_subjektiv_cursor( - kind: &str, - cursor: &str, -) -> ApiResult { - let prefix = format!("subjektiv.{kind}."); - let encoded = cursor - .strip_prefix(&prefix) - .ok_or_else(|| Error::InvalidInput(format!("invalid subjektiv {kind} cursor type")))?; - if encoded.is_empty() - || encoded.len() > 16_384 - || encoded.len() % 2 != 0 - || !encoded.is_ascii() - || !encoded.bytes().all(|byte| byte.is_ascii_hexdigit()) - { - return Err(Error::InvalidInput(format!("invalid subjektiv {kind} cursor")).into()); - } - let mut raw = Vec::with_capacity(encoded.len() / 2); - for index in (0..encoded.len()).step_by(2) { - raw.push( - u8::from_str_radix(&encoded[index..index + 2], 16).map_err(|_| { - Error::InvalidInput(format!("invalid subjektiv {kind} cursor encoding")) - })?, - ); - } - serde_json::from_slice(&raw) - .map_err(|_| Error::InvalidInput(format!("invalid subjektiv {kind} cursor payload")).into()) + Ok(Json(subjektiv::api::execute( + &store, + &host_context, + request.operation, + )?)) } fn subjektiv_store_error(error: crate::subjektiv::SubjektivError) -> Error { @@ -21632,24 +20148,18 @@ async fn scoped_subjektiv_memory_consolidation( )) } -const SUBJECT_CONSOLIDATION_THRESHOLD_FILES: usize = 5; -const SUBJECT_CONSOLIDATION_THRESHOLD_BYTES: u64 = 50_000; - fn subject_consolidation_required( candidate_count: usize, total_bytes: u64, force: bool, surface_availability: crate::subjektiv::SurfaceAvailability, ) -> bool { - let surface_needs_generation = - surface_availability != crate::subjektiv::SurfaceAvailability::Ready; - if candidate_count == 0 { - return surface_needs_generation; - } - force - || surface_needs_generation - || candidate_count >= SUBJECT_CONSOLIDATION_THRESHOLD_FILES - || total_bytes >= SUBJECT_CONSOLIDATION_THRESHOLD_BYTES + subjektiv::job::consolidation_required( + candidate_count, + total_bytes, + force, + surface_availability, + ) } #[cfg(test)] @@ -21680,7 +20190,7 @@ mod subject_surface_consolidation_policy_tests { SurfaceAvailability::Ready )); assert!(subject_consolidation_required( - SUBJECT_CONSOLIDATION_THRESHOLD_FILES, + subjektiv::job::CONSOLIDATION_THRESHOLD_FILES, 1, false, SurfaceAvailability::Ready @@ -41716,7 +40226,7 @@ mod tests { }, ) .unwrap_err(); - let revision_api = ApiError::from(revision_conflict.error).into_repository_api_error(); + let revision_api = ApiError::from(revision_conflict).into_repository_api_error(); assert_eq!(revision_api.diagnostics[0].code, "revision_conflict"); let default_page = subjektiv_memory_query( @@ -41793,8 +40303,11 @@ mod tests { }, ) .unwrap_err(); - assert!(matches!(&stale.error, Error::RepositoryConflict(_))); - let stale_api = ApiError::from(stale.error).into_repository_api_error(); + assert!(matches!( + &stale, + subjektiv::api::OperationError::Conflict(_) + )); + let stale_api = ApiError::from(stale).into_repository_api_error(); assert_eq!(stale_api.diagnostics[0].code, "stale_cursor"); let initial_read = subjektiv_memory_read( @@ -41839,7 +40352,10 @@ mod tests { }, ) .unwrap_err(); - assert!(matches!(unbound_continuation.error, Error::InvalidInput(_))); + assert!(matches!( + unbound_continuation, + subjektiv::api::OperationError::InvalidInput(_) + )); let read = subjektiv_memory_read( &store, @@ -42219,12 +40735,18 @@ mod tests { legacy_evidence, legacy_refs, ); - let legacy_candidate = store - .stage_legacy_candidate_for_test(crate::subjektiv::SubjectStagingRecord::attach( - &subject.id, - legacy_candidate, - )) - .unwrap(); + let legacy_candidate = + crate::subjektiv::SubjectStagingRecord::attach(&subject.id, legacy_candidate); + // Seed an old compatible row directly: production admission intentionally + // rejects this historical over-limit shape. No shared bypass API is exposed. + api.feature_storage.open(&api.subjektiv_registration).unwrap() + .transaction(|transaction| { + transaction.execute( + "INSERT INTO staging_records (subject_id, candidate_id, kind, record_json, created_at) VALUES (?1, ?2, ?3, ?4, ?5)", + rusqlite::params![legacy_candidate.subject_id, legacy_candidate.id, legacy_candidate.kind.as_str(), serde_json::to_string(&legacy_candidate).unwrap(), legacy_candidate.created_at], + )?; + Ok(()) + }).unwrap(); let (legacy_memory, _) = store .apply_candidate( &subject.id, @@ -60470,3 +58992,23 @@ Objective body. .unwrap(); } } + +impl From for Error { + fn from(error: subjektiv::api::OperationError) -> Self { + use subjektiv::api::OperationError as Common; + let error = match error { + Common::InvalidInput(message) => Error::InvalidInput(message), + Common::Conflict(message) => Error::RepositoryConflict(message), + Common::PermissionDenied(message) => Error::WorkspacePermissionDenied(message), + Common::SubjectNotFound(id) => Error::SubjektivSubjectNotFound(id), + Common::Store(message) => Error::Store(message), + }; + error + } +} + +impl From for ApiError { + fn from(error: subjektiv::api::OperationError) -> Self { + Error::from(error).into() + } +} diff --git a/crates/workspace-server/src/server/subjektiv_job.rs b/crates/workspace-server/src/server/subjektiv_job.rs index 66317f7ca..f2a93075a 100644 --- a/crates/workspace-server/src/server/subjektiv_job.rs +++ b/crates/workspace-server/src/server/subjektiv_job.rs @@ -151,23 +151,7 @@ fn bounded_candidate_batch( ids: impl IntoIterator, max_result_bytes: u32, ) -> Result> { - // Reserve the bounded subject and Host surface envelope. Each candidate ID - // appears twice (input claims and verified outcome); account for JSON escaping, - // not raw UTF-8 bytes. The fixed per-item allowance covers action and framing. - let mut estimated_bytes = 2_048_usize; - let mut selected = Vec::new(); - for id in ids.into_iter().take(CANDIDATE_BATCH_LIMIT) { - let id_bytes = serde_json::to_vec(&id) - .map_err(|e| Error::InvalidInput(e.to_string()))? - .len(); - let additional = 2 * id_bytes + 128; - if estimated_bytes + additional > max_result_bytes as usize { - break; - } - estimated_bytes += additional; - selected.push(id); - } - Ok(selected) + subjektiv::job::bounded_candidate_batch(ids, max_result_bytes).map_err(Error::from) } pub(super) fn dispatch( @@ -234,25 +218,6 @@ pub(super) fn dispatch( }) } -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -struct ConsolidationResult { - subject_id: String, - candidate_ids: Vec, - surface: SurfaceResult, -} -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -struct SurfaceResult { - availability: String, - generation_id: String, - store_revision: u64, - #[serde(default)] - snapshot_id: Option, - #[serde(default)] - reason_code: Option, -} - pub(super) fn validate_result( api: &WorkspaceApi, request: &BackendJobRequest, @@ -262,39 +227,18 @@ pub(super) fn validate_result( let Some(grant) = &request.grants.subjektiv_consolidation else { return Ok(()); }; - let result: ConsolidationResult = serde_json::from_value(value.clone()) - .map_err(|e| Error::InvalidInput(format!("invalid consolidation result: {e}")))?; - if result.subject_id != grant.subject_id || result.candidate_ids != grant.candidate_ids { - return Err(Error::InvalidInput( - "consolidation result does not match the immutable Subject/batch grant".into(), - )); - } let store = crate::subjektiv::SubjektivStore::open(&api.feature_storage, &api.subjektiv_registration) .map_err(|e| Error::Store(e.to_string()))?; - for candidate_id in &grant.candidate_ids { - if store - .staging_resolution(&grant.subject_id, candidate_id) - .map_err(|e| Error::Store(e.to_string()))? - .is_none() - { - return Err(Error::InvalidInput(format!( - "consolidation candidate `{candidate_id}` is unresolved" - ))); - } - } - store - .validate_job_surface_outcome( - &grant.subject_id, - &result.surface.generation_id, - result.surface.store_revision, - &result.surface.availability, - result.surface.snapshot_id.as_deref(), - result.surface.reason_code.as_deref(), - &request.job_id, - &attempt.attempt_id, - ) - .map_err(|e| Error::InvalidInput(format!("consolidation surface result mismatch: {e}"))) + subjektiv::job::validate_result( + &store, + &grant.subject_id, + &grant.candidate_ids, + &request.job_id, + &attempt.attempt_id, + value, + ) + .map_err(Error::from) } /// Persist actual candidate dispositions alongside the separate surface outcome. @@ -313,28 +257,8 @@ pub(super) fn with_dispositions( let store = crate::subjektiv::SubjektivStore::open(&api.feature_storage, &api.subjektiv_registration) .map_err(|e| Error::Store(e.to_string()))?; - let mut dispositions = Vec::new(); - for id in &grant.candidate_ids { - let resolution = store - .staging_resolution(&grant.subject_id, id) - .map_err(|e| Error::Store(e.to_string()))? - .ok_or_else(|| { - Error::InvalidInput(format!("candidate `{id}` has no durable disposition")) - })?; - // Full reasons, affected revisions and source edges remain in the existing - // immutable receipt store. Duplicating them makes a valid bounded batch's - // completion unbounded; this result is its verified compact projection. - dispositions.push(serde_json::json!({ "candidate_id": id, "action": resolution.action })); - } - let mut enriched = value.clone(); - enriched - .as_object_mut() - .ok_or_else(|| Error::InvalidInput("consolidation result must be an object".into()))? - .insert( - "candidate_dispositions".into(), - serde_json::Value::Array(dispositions), - ); - Ok(enriched) + subjektiv::job::with_dispositions(&store, &grant.subject_id, &grant.candidate_ids, value) + .map_err(Error::from) } #[cfg(test)] diff --git a/crates/workspace-server/src/subjektiv.rs b/crates/workspace-server/src/subjektiv.rs index 83d4cfe63..5335ffbf3 100644 --- a/crates/workspace-server/src/subjektiv.rs +++ b/crates/workspace-server/src/subjektiv.rs @@ -1,6187 +1,2 @@ -//! Subject-scoped, revisioned Memory storage for the `subjektiv` Feature. -//! -//! This module owns subjektiv's domain schema and typed repository. Physical -//! SQLite placement, connection configuration, migration execution, backup, -//! restore, and teardown remain owned by [`crate::feature_storage`]. A store is -//! already bound to one Workspace; a subject identifier is therefore a lookup -//! key, not an authorization credential. - -use std::collections::HashSet; - -use chrono::{SecondsFormat, Utc}; -use memory::extract::{CandidateKind, STAGING_SCHEMA_VERSION, StagingEvidence, StagingRecord}; -use memory::schema::{SourceEvidenceRef, SourceRef}; -use rusqlite::{Connection, OptionalExtension, Transaction, params}; -use serde::{Deserialize, Deserializer, Serialize}; -use uuid::Uuid; - -use crate::feature_storage::{ - FeatureDatabase, FeatureMigration, FeatureRegistration, FeatureStorageError, RegisteredFeature, - WorkspaceFeatureStorage, -}; - -pub const SUBJEKTIV_SCHEMA_VERSION: u32 = 1; -pub const SUBJEKTIV_FEATURE_ID: &str = "subjektiv"; -pub const SUBJECT_WORKER_SINGLETON_PREFIX: &str = "subjektiv:"; -pub const MAX_STAGING_ANCHORS: usize = 10; -/// Maximum UTF-8 size of the user-managed Subject behavior document. -pub const MAX_SUBJECT_BEHAVIOR_BYTES: usize = server_api::SUBJEKTIV_MAX_BEHAVIOR_BYTES; -/// Deterministic surface-generation policy. Input estimates use the repository's -/// provider-independent UTF-8 byte estimate (`ceil(bytes / 4)`). The material -/// payload is capped below the whole-input budget to reserve room for the fixed -/// prompt and JSON framing. -pub const SURFACE_BODY_TOKEN_BUDGET: usize = 1_024; -pub const SURFACE_INPUT_TOKEN_BUDGET: usize = 12_000; -pub const SURFACE_MATERIAL_TOKEN_BUDGET: usize = 10_000; -pub const SURFACE_PER_KIND_LIMIT: usize = 8; -pub const SURFACE_TOTAL_MATERIAL_LIMIT: usize = 24; - -pub type Result = std::result::Result; - -#[derive(Debug, thiserror::Error)] -pub enum SubjektivError { - #[error(transparent)] - Storage(#[from] FeatureStorageError), - #[error("invalid subjektiv record: {0}")] - InvalidRecord(String), - #[error("subject `{0}` was not found in this Workspace")] - SubjectNotFound(String), - #[error("subject `{0}` is retired")] - SubjectRetired(String), - #[error("the selected Worker Profile does not enable subjektiv")] - ProfileDisabled, - #[error("staging candidate `{0}` was not found for this subject")] - CandidateNotFound(String), - #[error("staging candidate `{0}` already exists with different content")] - CandidateConflict(String), - #[error("staging candidate `{0}` is already resolved")] - CandidateResolved(String), - #[error("candidate decision request `{0}` was already used with different input")] - DecisionRequestConflict(String), - #[error( - "session `{session_id}` is already attributed to subject `{existing_subject_id}` and cannot be attributed to `{requested_subject_id}`" - )] - SessionSubjectConflict { - session_id: String, - existing_subject_id: String, - requested_subject_id: String, - }, - #[error("session `{0}` already has different historical attribution")] - SessionAttributionConflict(String), - #[error("memory `{0}` was not found for this subject")] - MemoryNotFound(String), - #[error("memory `{memory_id}` revision conflict: expected {expected}, current {actual}")] - RevisionConflict { - memory_id: String, - expected: u64, - actual: u64, - }, - #[error( - "subject `{subject_id}` behavior revision conflict: expected {expected}, current {actual}" - )] - SubjectBehaviorConflict { - subject_id: String, - expected: u64, - actual: u64, - }, - #[error("surface generation conflict: {0}")] - SurfaceGenerationConflict(String), - #[error("memory `{memory_id}` cannot transition from {from:?} to {to:?}")] - InvalidStateTransition { - memory_id: String, - from: MemoryState, - to: MemoryState, - }, - #[error("reference `{reference}` does not belong to subject `{subject_id}`")] - SubjectScopeMismatch { - subject_id: String, - reference: String, - }, - #[error("subjektiv JSON error: {0}")] - Json(#[from] serde_json::Error), -} - -impl From for SubjektivError { - fn from(error: rusqlite::Error) -> Self { - Self::Storage(FeatureStorageError::from(error)) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] -#[serde(transparent)] -pub struct SubjectRole(String); - -impl SubjectRole { - pub fn new(value: impl Into) -> Result { - let value = value.into(); - validate_label("subject role", &value)?; - Ok(Self(value)) - } - - pub fn as_str(&self) -> &str { - &self.0 - } -} - -impl<'de> Deserialize<'de> for SubjectRole { - fn deserialize(deserializer: D) -> std::result::Result - where - D: Deserializer<'de>, - { - let value = String::deserialize(deserializer)?; - SubjectRole::new(value).map_err(serde::de::Error::custom) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum SubjectState { - Active, - Retired, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct SubjectRecord { - pub schema_version: u32, - pub id: String, - pub role: SubjectRole, - /// User-managed, host-injected behavior document. This is not Memory and - /// Memory lifecycle operations must preserve it byte-for-byte. - #[serde(default)] - pub behavior_md: String, - /// Monotonic CAS generation for behavior updates, independent of Memory's - /// `store_revision`. - #[serde(default)] - pub behavior_revision: u64, - pub state: SubjectState, - /// Monotonic generation incremented exactly once for every committed Memory - /// creation or revision for this subject. - pub store_revision: u64, - pub created_at: String, - pub updated_at: String, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SubjectPage { - pub items: Vec, - pub has_more: bool, -} - -/// Backend-authorized launch material for one active subject. The singleton key -/// is derived by subjektiv and then acquired through Yoi's generic singleton path. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct AuthorizedSubjectWorkerAttachment { - pub singleton_key: String, -} - -pub fn subject_worker_singleton_key(subject_id: &str) -> Result { - validate_label("subject id", subject_id)?; - if subject_id.trim() != subject_id { - return Err(SubjektivError::InvalidRecord( - "subject id must not contain surrounding whitespace".to_string(), - )); - } - Ok(format!("{SUBJECT_WORKER_SINGLETON_PREFIX}{subject_id}")) -} - -/// Immutable host-recorded history that attributes one durable Session to the -/// subject and Worker that produced it. This is archival provenance only: it is -/// deliberately not a current-Worker link or execution ownership record. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct SubjectSessionAttribution { - pub schema_version: u32, - pub subject_id: String, - pub runtime_id: String, - pub worker_id: String, - pub session_id: String, - pub attributed_at: String, -} - -impl SubjectSessionAttribution { - pub fn new( - subject_id: impl Into, - runtime_id: impl Into, - worker_id: impl Into, - session_id: impl Into, - ) -> Result { - let record = Self { - schema_version: SUBJEKTIV_SCHEMA_VERSION, - subject_id: subject_id.into(), - runtime_id: runtime_id.into(), - worker_id: worker_id.into(), - session_id: session_id.into(), - attributed_at: now(), - }; - validate_session_attribution(&record)?; - Ok(record) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SubjectSessionAttributionPage { - pub items: Vec, - pub has_more: bool, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum RevisionProposalIntent { - Revise, - Resolve, - Retract, - Reopen, -} - -/// Host-owned metadata describing how a staged candidate is expected to change -/// one existing Memory. It is validated against the current scoped Memory when -/// the candidate is staged; it is not model-authored and does not itself apply -/// the revision. -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] -#[serde(deny_unknown_fields)] -pub struct RevisionProposal { - pub intent: RevisionProposalIntent, - pub memory_id: String, - pub expected_revision: u64, - pub change_reason: String, -} - -impl RevisionProposal { - pub fn new( - intent: RevisionProposalIntent, - memory_id: impl Into, - expected_revision: u64, - change_reason: impl Into, - ) -> Result { - let proposal = Self { - intent, - memory_id: memory_id.into(), - expected_revision, - change_reason: change_reason.into(), - }; - validate_revision_proposal_metadata(&proposal)?; - Ok(proposal) - } -} - -impl<'de> Deserialize<'de> for RevisionProposal { - fn deserialize(deserializer: D) -> std::result::Result - where - D: Deserializer<'de>, - { - #[derive(Deserialize)] - #[serde(deny_unknown_fields)] - struct WireProposal { - intent: RevisionProposalIntent, - memory_id: String, - expected_revision: u64, - change_reason: String, - } - - let proposal = WireProposal::deserialize(deserializer)?; - RevisionProposal::new( - proposal.intent, - proposal.memory_id, - proposal.expected_revision, - proposal.change_reason, - ) - .map_err(serde::de::Error::custom) - } -} - -/// The existing extraction schema with only host-owned subject scope, -/// persistence time, and optional revision proposal metadata added. Model input -/// still contains none of those fields. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct SubjectStagingRecord { - pub schema_version: u32, - pub id: String, - pub subject_id: String, - pub extract_run_id: String, - pub source: SourceRef, - pub kind: CandidateKind, - pub claim: String, - pub why_useful: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub staleness: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub evidence: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub source_refs: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub revision_proposal: Option, - pub created_at: String, -} - -impl SubjectStagingRecord { - /// Attaches host-resolved subject scope and time to an existing v2 staging - /// record without changing its taxonomy, evidence, or source anchors. - pub fn attach(subject_id: impl Into, record: StagingRecord) -> Self { - Self::attach_at(subject_id, record, now()) - } - - fn attach_at(subject_id: impl Into, record: StagingRecord, created_at: String) -> Self { - Self { - schema_version: record.schema_version, - id: record.id, - subject_id: subject_id.into(), - extract_run_id: record.extract_run_id, - source: record.source, - kind: record.kind, - claim: record.claim, - why_useful: record.why_useful, - staleness: record.staleness, - evidence: record.evidence, - source_refs: record.source_refs, - revision_proposal: None, - created_at, - } - } - - /// Attaches host-owned revision proposal metadata to this candidate. - pub fn with_revision_proposal(mut self, proposal: RevisionProposal) -> Result { - validate_revision_proposal_metadata(&proposal)?; - self.revision_proposal = Some(proposal); - Ok(self) - } - - /// Mutably attaches host-owned revision proposal metadata to this candidate. - pub fn attach_revision_proposal(&mut self, proposal: RevisionProposal) -> Result<()> { - validate_revision_proposal_metadata(&proposal)?; - self.revision_proposal = Some(proposal); - Ok(()) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum MemoryState { - /// Current and eligible for ordinary recall. This does not assert truth and - /// grants no authority. - Active, - /// No longer current because its question, condition, or applicability has - /// ended. A later revision may reopen it when circumstances change. - Resolved, - /// Invalidated and no longer eligible for recall. Retraction is terminal; - /// corrected follow-up experience is a separate Memory linked by derivation. - Retracted, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct MemoryRevisionRef { - pub memory_id: String, - pub revision: u64, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct MemoryRecord { - pub schema_version: u32, - pub id: String, - pub subject_id: String, - pub revision: u64, - pub kind: CandidateKind, - pub state: MemoryState, - pub claim: String, - pub body_md: String, - pub why_useful: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub staleness: Option, - #[serde(default)] - pub source_candidate_ids: Vec, - #[serde(default)] - pub derived_from: Vec, - pub change_reason: String, - pub created_at: String, - pub updated_at: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct MemoryDraft { - pub kind: CandidateKind, - pub state: MemoryState, - pub claim: String, - pub body_md: String, - pub why_useful: String, - pub staleness: Option, - pub source_candidate_ids: Vec, - pub derived_from: Vec, - pub change_reason: String, -} - -impl MemoryDraft { - pub fn active( - kind: CandidateKind, - claim: impl Into, - body_md: impl Into, - why_useful: impl Into, - change_reason: impl Into, - ) -> Self { - Self { - kind, - state: MemoryState::Active, - claim: claim.into(), - body_md: body_md.into(), - why_useful: why_useful.into(), - staleness: None, - source_candidate_ids: Vec::new(), - derived_from: Vec::new(), - change_reason: change_reason.into(), - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum StagingResolutionAction { - Applied, - Discarded, - Invalid, - Duplicate, - AlreadyCovered, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct StagingResolution { - pub schema_version: u32, - pub id: String, - pub subject_id: String, - pub candidate_id: String, - pub action: StagingResolutionAction, - pub reason: String, - #[serde(default)] - pub affected_memory: Vec, - /// Immutable copy of the candidate as it was staged. The database also - /// retains the exact serialized bytes independently of this projection. - pub candidate: SubjectStagingRecord, - pub resolved_at: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct SurfaceSnapshot { - pub schema_version: u32, - pub id: String, - pub subject_id: String, - pub body_md: String, - #[serde(default)] - pub memory_refs: Vec, - pub built_from_store_revision: u64, - pub created_at: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct SurfaceMaterial { - pub memory_id: String, - pub revision: u64, - pub kind: CandidateKind, - pub body_md: String, - pub why_useful: String, - pub staleness: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct SurfaceGeneration { - pub id: String, - pub subject_id: String, - pub store_revision: u64, - pub active_memory_count: usize, - pub materials: Vec, - pub created_at: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct SurfacePoint { - pub body_md: String, - pub memory_refs: Vec, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SurfaceAvailability { - Ungenerated, - Stale, - Failed, - Ready, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ResidentSurface { - pub availability: SurfaceAvailability, - pub snapshot: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SubjectResidentContext { - pub subject: SubjectRecord, - pub surface: ResidentSurface, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum MemoryRevisionTarget { - Create, - Revise { - memory_id: String, - expected_revision: u64, - }, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum MemoryDecisionOperation { - Create, - Revise, - Reference, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum CandidateDecision { - Apply { - target: MemoryRevisionTarget, - draft: MemoryDraft, - }, - Close { - action: StagingResolutionAction, - #[serde(default)] - affected_memory: Vec, - }, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct CandidateDecisionRequest { - pub request_id: String, - pub candidate_id: String, - pub reason: String, - pub decision: CandidateDecision, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct CandidateDecisionReceipt { - pub request_id: String, - pub candidate_id: String, - pub resolution: StagingResolution, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub memory: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub operation: Option, - pub store_revision: u64, - pub surface_dirty: bool, -} - -fn create_schema(transaction: &Transaction<'_>) -> crate::feature_storage::Result<()> { - transaction.execute_batch( - r#" -CREATE TABLE store_scope ( - singleton INTEGER PRIMARY KEY CHECK (singleton = 1), - workspace_id TEXT NOT NULL UNIQUE -); - -CREATE TABLE subjects ( - subject_id TEXT PRIMARY KEY, - role TEXT NOT NULL, - state TEXT NOT NULL CHECK (state IN ('active', 'retired')), - store_revision INTEGER NOT NULL CHECK (store_revision >= 0), - record_json TEXT NOT NULL, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL -); - -CREATE TABLE staging_records ( - subject_id TEXT NOT NULL, - candidate_id TEXT NOT NULL, - kind TEXT NOT NULL CHECK ( - kind IN ( - 'preference', 'working_assumption', 'constraint', - 'decision', 'open_question', 'lesson' - ) - ), - record_json TEXT NOT NULL, - created_at TEXT NOT NULL, - PRIMARY KEY (subject_id, candidate_id), - FOREIGN KEY (subject_id) REFERENCES subjects(subject_id) ON DELETE RESTRICT -); - -CREATE TABLE memory_records ( - subject_id TEXT NOT NULL, - memory_id TEXT NOT NULL, - current_revision INTEGER NOT NULL CHECK (current_revision > 0), - kind TEXT NOT NULL CHECK ( - kind IN ( - 'preference', 'working_assumption', 'constraint', - 'decision', 'open_question', 'lesson' - ) - ), - state TEXT NOT NULL CHECK (state IN ('active', 'resolved', 'retracted')), - record_json TEXT NOT NULL, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - PRIMARY KEY (subject_id, memory_id), - FOREIGN KEY (subject_id) REFERENCES subjects(subject_id) ON DELETE RESTRICT, - FOREIGN KEY (subject_id, memory_id, current_revision) - REFERENCES memory_revisions(subject_id, memory_id, revision) - DEFERRABLE INITIALLY DEFERRED -); - -CREATE TABLE memory_revisions ( - subject_id TEXT NOT NULL, - memory_id TEXT NOT NULL, - revision INTEGER NOT NULL CHECK (revision > 0), - kind TEXT NOT NULL CHECK ( - kind IN ( - 'preference', 'working_assumption', 'constraint', - 'decision', 'open_question', 'lesson' - ) - ), - state TEXT NOT NULL CHECK (state IN ('active', 'resolved', 'retracted')), - record_json TEXT NOT NULL, - created_at TEXT NOT NULL, - PRIMARY KEY (subject_id, memory_id, revision), - FOREIGN KEY (subject_id, memory_id) - REFERENCES memory_records(subject_id, memory_id) - ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED -); - -CREATE TABLE memory_revision_candidates ( - subject_id TEXT NOT NULL, - memory_id TEXT NOT NULL, - revision INTEGER NOT NULL, - candidate_id TEXT NOT NULL, - PRIMARY KEY (subject_id, memory_id, revision, candidate_id), - FOREIGN KEY (subject_id, memory_id, revision) - REFERENCES memory_revisions(subject_id, memory_id, revision) ON DELETE RESTRICT, - FOREIGN KEY (subject_id, candidate_id) - REFERENCES staging_records(subject_id, candidate_id) ON DELETE RESTRICT -); - -CREATE TABLE memory_revision_derivations ( - subject_id TEXT NOT NULL, - memory_id TEXT NOT NULL, - revision INTEGER NOT NULL, - source_memory_id TEXT NOT NULL, - source_revision INTEGER NOT NULL, - PRIMARY KEY ( - subject_id, memory_id, revision, source_memory_id, source_revision - ), - FOREIGN KEY (subject_id, memory_id, revision) - REFERENCES memory_revisions(subject_id, memory_id, revision) ON DELETE RESTRICT, - FOREIGN KEY (subject_id, source_memory_id, source_revision) - REFERENCES memory_revisions(subject_id, memory_id, revision) ON DELETE RESTRICT -); - -CREATE TABLE staging_resolutions ( - subject_id TEXT NOT NULL, - candidate_id TEXT NOT NULL, - resolution_id TEXT NOT NULL, - action TEXT NOT NULL CHECK ( - action IN ('applied', 'discarded', 'invalid', 'duplicate', 'already_covered') - ), - reason TEXT NOT NULL, - affected_refs_json TEXT NOT NULL, - staging_raw_json TEXT NOT NULL, - resolution_json TEXT NOT NULL, - resolved_at TEXT NOT NULL, - PRIMARY KEY (subject_id, candidate_id), - UNIQUE (resolution_id), - FOREIGN KEY (subject_id, candidate_id) - REFERENCES staging_records(subject_id, candidate_id) ON DELETE RESTRICT -); - -CREATE TABLE staging_resolution_targets ( - subject_id TEXT NOT NULL, - candidate_id TEXT NOT NULL, - memory_id TEXT NOT NULL, - revision INTEGER NOT NULL, - PRIMARY KEY (subject_id, candidate_id, memory_id, revision), - FOREIGN KEY (subject_id, candidate_id) - REFERENCES staging_resolutions(subject_id, candidate_id) ON DELETE RESTRICT, - FOREIGN KEY (subject_id, memory_id, revision) - REFERENCES memory_revisions(subject_id, memory_id, revision) ON DELETE RESTRICT -); - -CREATE TABLE surface_snapshots ( - subject_id TEXT NOT NULL, - snapshot_id TEXT NOT NULL, - built_from_store_revision INTEGER NOT NULL CHECK (built_from_store_revision >= 0), - snapshot_json TEXT NOT NULL, - created_at TEXT NOT NULL, - PRIMARY KEY (subject_id, snapshot_id), - FOREIGN KEY (subject_id) REFERENCES subjects(subject_id) ON DELETE RESTRICT -); - -CREATE TABLE surface_snapshot_refs ( - subject_id TEXT NOT NULL, - snapshot_id TEXT NOT NULL, - memory_id TEXT NOT NULL, - revision INTEGER NOT NULL, - PRIMARY KEY (subject_id, snapshot_id, memory_id), - FOREIGN KEY (subject_id, snapshot_id) - REFERENCES surface_snapshots(subject_id, snapshot_id) ON DELETE RESTRICT, - FOREIGN KEY (subject_id, memory_id, revision) - REFERENCES memory_revisions(subject_id, memory_id, revision) ON DELETE RESTRICT -); - -CREATE TABLE memory_revision_seals ( - subject_id TEXT NOT NULL, - memory_id TEXT NOT NULL, - revision INTEGER NOT NULL, - PRIMARY KEY (subject_id, memory_id, revision), - FOREIGN KEY (subject_id, memory_id, revision) - REFERENCES memory_revisions(subject_id, memory_id, revision) ON DELETE RESTRICT -); - -CREATE TABLE staging_resolution_seals ( - subject_id TEXT NOT NULL, - candidate_id TEXT NOT NULL, - PRIMARY KEY (subject_id, candidate_id), - FOREIGN KEY (subject_id, candidate_id) - REFERENCES staging_resolutions(subject_id, candidate_id) ON DELETE RESTRICT -); - -CREATE TABLE surface_snapshot_seals ( - subject_id TEXT NOT NULL, - snapshot_id TEXT NOT NULL, - PRIMARY KEY (subject_id, snapshot_id), - FOREIGN KEY (subject_id, snapshot_id) - REFERENCES surface_snapshots(subject_id, snapshot_id) ON DELETE RESTRICT -); - -CREATE TRIGGER memory_revision_candidates_no_late_insert -BEFORE INSERT ON memory_revision_candidates -WHEN EXISTS ( - SELECT 1 FROM memory_revision_seals - WHERE subject_id = NEW.subject_id - AND memory_id = NEW.memory_id - AND revision = NEW.revision -) BEGIN - SELECT RAISE(ABORT, 'subjektiv revision evidence is sealed'); -END; -CREATE TRIGGER memory_revision_derivations_no_late_insert -BEFORE INSERT ON memory_revision_derivations -WHEN EXISTS ( - SELECT 1 FROM memory_revision_seals - WHERE subject_id = NEW.subject_id - AND memory_id = NEW.memory_id - AND revision = NEW.revision -) BEGIN - SELECT RAISE(ABORT, 'subjektiv derivations are sealed'); -END; -CREATE TRIGGER staging_resolution_targets_no_late_insert -BEFORE INSERT ON staging_resolution_targets -WHEN EXISTS ( - SELECT 1 FROM staging_resolution_seals - WHERE subject_id = NEW.subject_id - AND candidate_id = NEW.candidate_id -) BEGIN - SELECT RAISE(ABORT, 'subjektiv resolution targets are sealed'); -END; -CREATE TRIGGER surface_snapshot_refs_no_late_insert -BEFORE INSERT ON surface_snapshot_refs -WHEN EXISTS ( - SELECT 1 FROM surface_snapshot_seals - WHERE subject_id = NEW.subject_id - AND snapshot_id = NEW.snapshot_id -) BEGIN - SELECT RAISE(ABORT, 'subjektiv surface references are sealed'); -END; - -CREATE TRIGGER memory_revision_seals_no_update -BEFORE UPDATE ON memory_revision_seals BEGIN - SELECT RAISE(ABORT, 'subjektiv revision seals are immutable'); -END; -CREATE TRIGGER memory_revision_seals_no_delete -BEFORE DELETE ON memory_revision_seals BEGIN - SELECT RAISE(ABORT, 'subjektiv revision seals are retained'); -END; -CREATE TRIGGER staging_resolution_seals_no_update -BEFORE UPDATE ON staging_resolution_seals BEGIN - SELECT RAISE(ABORT, 'subjektiv resolution seals are immutable'); -END; -CREATE TRIGGER staging_resolution_seals_no_delete -BEFORE DELETE ON staging_resolution_seals BEGIN - SELECT RAISE(ABORT, 'subjektiv resolution seals are retained'); -END; -CREATE TRIGGER surface_snapshot_seals_no_update -BEFORE UPDATE ON surface_snapshot_seals BEGIN - SELECT RAISE(ABORT, 'subjektiv snapshot seals are immutable'); -END; -CREATE TRIGGER surface_snapshot_seals_no_delete -BEFORE DELETE ON surface_snapshot_seals BEGIN - SELECT RAISE(ABORT, 'subjektiv snapshot seals are retained'); -END; - -CREATE TRIGGER memory_revisions_no_update -BEFORE UPDATE ON memory_revisions BEGIN - SELECT RAISE(ABORT, 'subjektiv memory revisions are immutable'); -END; -CREATE TRIGGER memory_revisions_no_delete -BEFORE DELETE ON memory_revisions BEGIN - SELECT RAISE(ABORT, 'subjektiv memory revisions are retained'); -END; -CREATE TRIGGER memory_revision_candidates_no_update -BEFORE UPDATE ON memory_revision_candidates BEGIN - SELECT RAISE(ABORT, 'subjektiv revision evidence is immutable'); -END; -CREATE TRIGGER memory_revision_candidates_no_delete -BEFORE DELETE ON memory_revision_candidates BEGIN - SELECT RAISE(ABORT, 'subjektiv revision evidence is retained'); -END; -CREATE TRIGGER memory_revision_derivations_no_update -BEFORE UPDATE ON memory_revision_derivations BEGIN - SELECT RAISE(ABORT, 'subjektiv derivations are immutable'); -END; -CREATE TRIGGER memory_revision_derivations_no_delete -BEFORE DELETE ON memory_revision_derivations BEGIN - SELECT RAISE(ABORT, 'subjektiv derivations are retained'); -END; -CREATE TRIGGER staging_records_no_update -BEFORE UPDATE ON staging_records BEGIN - SELECT RAISE(ABORT, 'subjektiv staging records are immutable'); -END; -CREATE TRIGGER staging_records_no_delete -BEFORE DELETE ON staging_records BEGIN - SELECT RAISE(ABORT, 'subjektiv staging records are retained'); -END; -CREATE TRIGGER staging_resolutions_no_update -BEFORE UPDATE ON staging_resolutions BEGIN - SELECT RAISE(ABORT, 'subjektiv staging resolutions are immutable'); -END; -CREATE TRIGGER staging_resolutions_no_delete -BEFORE DELETE ON staging_resolutions BEGIN - SELECT RAISE(ABORT, 'subjektiv staging resolutions are retained'); -END; -CREATE TRIGGER staging_resolution_targets_no_update -BEFORE UPDATE ON staging_resolution_targets BEGIN - SELECT RAISE(ABORT, 'subjektiv resolution targets are immutable'); -END; -CREATE TRIGGER staging_resolution_targets_no_delete -BEFORE DELETE ON staging_resolution_targets BEGIN - SELECT RAISE(ABORT, 'subjektiv resolution targets are retained'); -END; -CREATE TRIGGER surface_snapshots_no_update -BEFORE UPDATE ON surface_snapshots BEGIN - SELECT RAISE(ABORT, 'subjektiv surface snapshots are immutable'); -END; -CREATE TRIGGER surface_snapshots_no_delete -BEFORE DELETE ON surface_snapshots BEGIN - SELECT RAISE(ABORT, 'subjektiv surface snapshots are retained'); -END; -CREATE TRIGGER surface_snapshot_refs_no_update -BEFORE UPDATE ON surface_snapshot_refs BEGIN - SELECT RAISE(ABORT, 'subjektiv surface references are immutable'); -END; -CREATE TRIGGER surface_snapshot_refs_no_delete -BEFORE DELETE ON surface_snapshot_refs BEGIN - SELECT RAISE(ABORT, 'subjektiv surface references are retained'); -END; -"#, - )?; - Ok(()) -} - -fn add_subject_session_attribution( - transaction: &Transaction<'_>, -) -> crate::feature_storage::Result<()> { - transaction.execute_batch( - r#" -CREATE TABLE subject_session_attributions ( - session_id TEXT PRIMARY KEY, - subject_id TEXT NOT NULL, - runtime_id TEXT NOT NULL, - worker_id TEXT NOT NULL, - record_json TEXT NOT NULL, - attributed_at TEXT NOT NULL, - FOREIGN KEY (subject_id) REFERENCES subjects(subject_id) ON DELETE RESTRICT -); - -CREATE INDEX subject_session_attributions_by_subject -ON subject_session_attributions(subject_id, attributed_at, session_id); - -CREATE TRIGGER subject_session_attributions_no_update -BEFORE UPDATE ON subject_session_attributions BEGIN - SELECT RAISE(ABORT, 'subjektiv session attribution is immutable'); -END; -CREATE TRIGGER subject_session_attributions_no_delete -BEFORE DELETE ON subject_session_attributions BEGIN - SELECT RAISE(ABORT, 'subjektiv session attribution is retained'); -END; -"#, - )?; - Ok(()) -} - -fn add_candidate_decision_receipts( - transaction: &Transaction<'_>, -) -> crate::feature_storage::Result<()> { - transaction.execute_batch( - r#" -CREATE TABLE candidate_decision_receipts ( - subject_id TEXT NOT NULL, - request_id TEXT NOT NULL, - candidate_id TEXT NOT NULL, - request_json TEXT NOT NULL, - receipt_json TEXT NOT NULL, - created_at TEXT NOT NULL, - PRIMARY KEY (subject_id, request_id), - UNIQUE (subject_id, candidate_id), - FOREIGN KEY (subject_id, candidate_id) - REFERENCES staging_resolutions(subject_id, candidate_id) ON DELETE RESTRICT -); - -CREATE TRIGGER candidate_decision_receipts_no_update -BEFORE UPDATE ON candidate_decision_receipts BEGIN - SELECT RAISE(ABORT, 'subjektiv candidate decision receipts are immutable'); -END; -CREATE TRIGGER candidate_decision_receipts_no_delete -BEFORE DELETE ON candidate_decision_receipts BEGIN - SELECT RAISE(ABORT, 'subjektiv candidate decision receipts are retained'); -END; -"#, - )?; - Ok(()) -} - -fn add_surface_generation_state( - transaction: &Transaction<'_>, -) -> crate::feature_storage::Result<()> { - transaction.execute_batch( - r#" -CREATE TABLE surface_generation_state ( - subject_id TEXT PRIMARY KEY, - store_revision INTEGER NOT NULL CHECK (store_revision >= 0), - status TEXT NOT NULL CHECK (status IN ('dirty', 'failed', 'ready')), - snapshot_id TEXT, - reason_code TEXT, - updated_at TEXT NOT NULL, - FOREIGN KEY (subject_id) REFERENCES subjects(subject_id) ON DELETE RESTRICT, - FOREIGN KEY (subject_id, snapshot_id) - REFERENCES surface_snapshots(subject_id, snapshot_id) ON DELETE RESTRICT -); - -INSERT INTO surface_generation_state ( - subject_id, store_revision, status, snapshot_id, reason_code, updated_at -) -SELECT subjects.subject_id, - subjects.store_revision, - 'dirty', - NULL, - 'legacy_snapshot_requires_regeneration', - subjects.updated_at -FROM subjects -WHERE EXISTS ( - SELECT 1 FROM surface_snapshots any_snapshot - WHERE any_snapshot.subject_id = subjects.subject_id -); - -CREATE TABLE surface_generation_runs ( - subject_id TEXT NOT NULL, - generation_id TEXT NOT NULL, - store_revision INTEGER NOT NULL CHECK (store_revision >= 0), - active_memory_count INTEGER NOT NULL CHECK (active_memory_count >= 0), - materials_json TEXT NOT NULL, - created_at TEXT NOT NULL, - PRIMARY KEY (subject_id, generation_id), - FOREIGN KEY (subject_id) REFERENCES subjects(subject_id) ON DELETE RESTRICT -); - -CREATE INDEX surface_generation_runs_by_revision -ON surface_generation_runs(subject_id, store_revision, created_at, generation_id); - -CREATE TRIGGER surface_generation_runs_no_update -BEFORE UPDATE ON surface_generation_runs BEGIN - SELECT RAISE(ABORT, 'subjektiv surface generation inputs are immutable'); -END; -CREATE TRIGGER surface_generation_runs_no_delete -BEFORE DELETE ON surface_generation_runs BEGIN - SELECT RAISE(ABORT, 'subjektiv surface generation inputs are retained'); -END; -"#, - )?; - Ok(()) -} - -fn add_subject_behavior(transaction: &Transaction<'_>) -> crate::feature_storage::Result<()> { - transaction.execute_batch( - r#" -ALTER TABLE subjects ADD COLUMN behavior_md TEXT NOT NULL DEFAULT ''; -ALTER TABLE subjects ADD COLUMN behavior_revision INTEGER NOT NULL DEFAULT 0 - CHECK (behavior_revision >= 0); -"#, - )?; - Ok(()) -} - -fn add_surface_job_provenance(transaction: &Transaction<'_>) -> crate::feature_storage::Result<()> { - transaction.execute_batch("ALTER TABLE surface_generation_runs ADD COLUMN job_id TEXT; ALTER TABLE surface_generation_runs ADD COLUMN attempt_id TEXT;")?; - Ok(()) -} - -static MIGRATIONS: &[FeatureMigration] = &[ - FeatureMigration::new(1, "create subjektiv subject memory store", create_schema), - FeatureMigration::new( - 2, - "add immutable subject session attribution", - add_subject_session_attribution, - ), - FeatureMigration::new( - 3, - "add idempotent atomic candidate decision receipts", - add_candidate_decision_receipts, - ), - FeatureMigration::new( - 4, - "add bounded surface generation state and runs", - add_surface_generation_state, - ), - FeatureMigration::new(5, "add user-managed subject behavior", add_subject_behavior), - FeatureMigration::new( - 6, - "bind surface generation provenance to Job attempts", - add_surface_job_provenance, - ), -]; - -pub const REGISTRATION: FeatureRegistration = - FeatureRegistration::new(SUBJEKTIV_FEATURE_ID, MIGRATIONS); - -#[derive(Clone)] -pub struct SubjektivStore { - database: FeatureDatabase, -} - -impl SubjektivStore { - /// Registers the trusted subjektiv schema with the Server storage manager. - /// Registration is intentionally separate so one registration can be reused - /// by every Workspace served by that manager. - pub fn register(storage: &WorkspaceFeatureStorage) -> Result { - storage.register(REGISTRATION).map_err(Into::into) - } - - pub fn open( - storage: &WorkspaceFeatureStorage, - registration: &RegisteredFeature, - ) -> Result { - if registration.feature_id() != SUBJEKTIV_FEATURE_ID { - return Err(SubjektivError::InvalidRecord(format!( - "registration is for Feature `{}` rather than `{SUBJEKTIV_FEATURE_ID}`", - registration.feature_id() - ))); - } - let database = storage.open(registration)?; - let workspace_id = database.workspace_id().to_string(); - database.try_transaction::<_, SubjektivError>(|transaction| { - let stored = transaction - .query_row( - "SELECT workspace_id FROM store_scope WHERE singleton = 1", - [], - |row| row.get::<_, String>(0), - ) - .optional()?; - match stored { - Some(stored) if stored != workspace_id => { - Err(SubjektivError::SubjectScopeMismatch { - subject_id: "".to_string(), - reference: format!( - "database is bound to Workspace `{stored}`, not `{workspace_id}`" - ), - }) - } - Some(_) => Ok(()), - None => { - transaction.execute( - "INSERT INTO store_scope (singleton, workspace_id) VALUES (1, ?1)", - [&workspace_id], - )?; - Ok(()) - } - } - })?; - Ok(Self { database }) - } - - pub fn workspace_id(&self) -> &str { - self.database.workspace_id() - } - - /// Issues a random host-owned subject id. It is never derived from Worker, - /// Profile, Session, or Memory content. - pub fn create_subject(&self, role: SubjectRole) -> Result { - self.create_subject_with_behavior(role, String::new()) - } - - /// Creates a Subject with its optional user-managed behavior document. - pub fn create_subject_with_behavior( - &self, - role: SubjectRole, - behavior_md: String, - ) -> Result { - validate_subject_behavior(&behavior_md)?; - let timestamp = now(); - let record = SubjectRecord { - schema_version: SUBJEKTIV_SCHEMA_VERSION, - id: issued_id("subject"), - role, - behavior_md, - behavior_revision: 0, - state: SubjectState::Active, - store_revision: 0, - created_at: timestamp.clone(), - updated_at: timestamp, - }; - let raw = serde_json::to_string(&record)?; - self.database.try_transaction(|transaction| { - transaction.execute( - "INSERT INTO subjects ( - subject_id, role, behavior_md, behavior_revision, state, - store_revision, record_json, created_at, updated_at - ) VALUES (?1, ?2, ?3, 0, 'active', 0, ?4, ?5, ?6)", - params![ - record.id, - record.role.as_str(), - record.behavior_md, - raw, - record.created_at, - record.updated_at - ], - )?; - Ok(record) - }) - } - - pub fn subject(&self, subject_id: &str) -> Result> { - self.database.try_with_connection(|connection| { - let raw = connection - .query_row( - "SELECT record_json FROM subjects WHERE subject_id = ?1", - [subject_id], - |row| row.get::<_, String>(0), - ) - .optional()?; - raw.map(|raw| parse_subject(&raw)).transpose() - }) - } - - /// Replaces or clears the user-managed behavior document using its own CAS - /// generation. Memory revisions do not participate in this generation. - pub fn update_subject_behavior( - &self, - subject_id: &str, - expected_behavior_revision: u64, - behavior_md: String, - ) -> Result { - validate_subject_behavior(&behavior_md)?; - self.database.try_transaction(|transaction| { - let mut subject = require_subject(transaction, subject_id)?; - if subject.behavior_revision != expected_behavior_revision { - return Err(SubjektivError::SubjectBehaviorConflict { - subject_id: subject_id.to_string(), - expected: expected_behavior_revision, - actual: subject.behavior_revision, - }); - } - if subject.behavior_md == behavior_md { - return Ok(subject); - } - subject.behavior_md = behavior_md; - subject.behavior_revision = - subject.behavior_revision.checked_add(1).ok_or_else(|| { - SubjektivError::InvalidRecord("subject behavior revision overflow".to_string()) - })?; - subject.updated_at = now(); - let raw = serde_json::to_string(&subject)?; - transaction.execute( - "UPDATE subjects - SET behavior_md = ?2, behavior_revision = ?3, - record_json = ?4, updated_at = ?5 - WHERE subject_id = ?1", - params![ - subject.id, - subject.behavior_md, - to_i64(subject.behavior_revision)?, - raw, - subject.updated_at - ], - )?; - Ok(subject) - }) - } - - /// Validates an explicit product launch connection against this Workspace's - /// subject store and the already-resolved Profile policy. Subject knowledge - /// alone is insufficient: only an active local record produces host launch material. - pub fn authorize_worker_attachment( - &self, - subject_id: &str, - profile: &manifest::WorkerManifest, - ) -> Result { - if !profile.feature.subjektiv.profile.enabled { - return Err(SubjektivError::ProfileDisabled); - } - let subject = self - .subject(subject_id)? - .ok_or_else(|| SubjektivError::SubjectNotFound(subject_id.to_string()))?; - if subject.state != SubjectState::Active { - return Err(SubjektivError::SubjectRetired(subject_id.to_string())); - } - Ok(AuthorizedSubjectWorkerAttachment { - singleton_key: subject_worker_singleton_key(&subject.id)?, - }) - } - - /// Lists the first deterministic bounded page of subjects. Ordering uses - /// immutable creation time with the host-issued subject id as a total-order - /// tie breaker. - pub fn list_subjects(&self, limit: usize) -> Result { - self.list_subjects_after(limit, None) - } - - /// Lists a deterministic bounded keyset page after an exact `(created_at, - /// subject_id)` cursor. The cursor fields are wrapped by the public API in - /// an opaque Workspace-bound token. - pub fn list_subjects_after( - &self, - limit: usize, - after: Option<(&str, &str)>, - ) -> Result { - if !(1..=100).contains(&limit) { - return Err(SubjektivError::InvalidRecord( - "subject list limit must be between 1 and 100".to_string(), - )); - } - let query_limit = limit.saturating_add(1); - self.database.try_with_connection(|connection| { - let sql = if after.is_some() { - "SELECT record_json FROM subjects - WHERE created_at > ?1 OR (created_at = ?1 AND subject_id > ?2) - ORDER BY created_at ASC, subject_id ASC - LIMIT ?3" - } else { - "SELECT record_json FROM subjects - ORDER BY created_at ASC, subject_id ASC - LIMIT ?1" - }; - let mut statement = connection.prepare(sql)?; - let mut items = Vec::new(); - if let Some((created_at, subject_id)) = after { - let rows = statement.query_map( - params![created_at, subject_id, to_i64(query_limit as u64)?], - |row| row.get::<_, String>(0), - )?; - for row in rows { - items.push(parse_subject(&row?)?); - } - } else { - let rows = statement - .query_map([to_i64(query_limit as u64)?], |row| row.get::<_, String>(0))?; - for row in rows { - items.push(parse_subject(&row?)?); - } - } - let has_more = items.len() > limit; - if has_more { - items.truncate(limit); - } - Ok(SubjectPage { items, has_more }) - }) - } - - /// Subject retirement is distinct from stopping any current Worker. It does - /// not edit or delete Memory and cannot be reversed by this baseline schema. - pub fn retire_subject(&self, subject_id: &str) -> Result { - self.database.try_transaction(|transaction| { - let mut subject = require_subject(transaction, subject_id)?; - if subject.state == SubjectState::Retired { - return Ok(subject); - } - subject.state = SubjectState::Retired; - subject.updated_at = now(); - let raw = serde_json::to_string(&subject)?; - transaction.execute( - "UPDATE subjects - SET state = 'retired', record_json = ?2, updated_at = ?3 - WHERE subject_id = ?1", - params![subject_id, raw, subject.updated_at], - )?; - Ok(subject) - }) - } - - pub fn stage_candidate(&self, record: SubjectStagingRecord) -> Result { - validate_staging_record(self.workspace_id(), &record)?; - validate_staging_admission(&record)?; - let raw = serde_json::to_string(&record)?; - self.database.try_transaction(|transaction| { - require_active_subject(transaction, &record.subject_id)?; - write_staging_candidate(transaction, self.workspace_id(), record, raw) - }) - } - - /// Atomically verifies immutable Host attribution already completed at the - /// Session lifecycle boundary and stages its extracted candidate. Candidate - /// staging never creates or repairs Session attribution. - pub fn stage_candidate_with_attribution( - &self, - mut record: SubjectStagingRecord, - attribution: SubjectSessionAttribution, - ) -> Result<(SubjectStagingRecord, SubjectSessionAttribution)> { - validate_session_attribution(&attribution)?; - if record.subject_id != attribution.subject_id { - return Err(SubjektivError::SubjectScopeMismatch { - subject_id: record.subject_id, - reference: format!( - "session {} is attributed to subject {}", - attribution.session_id, attribution.subject_id - ), - }); - } - attach_session_to_candidate(&mut record, &attribution.session_id)?; - validate_staging_record(self.workspace_id(), &record)?; - validate_staging_admission(&record)?; - let candidate_raw = serde_json::to_string(&record)?; - - self.database.try_transaction(|transaction| { - require_active_subject(transaction, &record.subject_id)?; - let existing_raw = transaction - .query_row( - "SELECT record_json FROM subject_session_attributions WHERE session_id = ?1", - [&attribution.session_id], - |row| row.get::<_, String>(0), - ) - .optional()? - .ok_or_else(|| { - SubjektivError::InvalidRecord(format!( - "Session {} has not completed Host attribution", - attribution.session_id - )) - })?; - let existing = parse_session_attribution(&existing_raw)?; - if existing.subject_id != attribution.subject_id { - return Err(SubjektivError::SessionSubjectConflict { - session_id: attribution.session_id, - existing_subject_id: existing.subject_id, - requested_subject_id: attribution.subject_id, - }); - } - if !equivalent_session_attribution(&existing, &attribution) { - return Err(SubjektivError::SessionAttributionConflict( - attribution.session_id, - )); - } - let candidate = - write_staging_candidate(transaction, self.workspace_id(), record, candidate_raw)?; - Ok((candidate, existing)) - }) - } - - /// Inserts a structurally valid record using the pre-admission-limit path so - /// upgrade tests can prove that compatible historical rows stay readable. - #[cfg(test)] - pub(crate) fn stage_legacy_candidate_for_test( - &self, - record: SubjectStagingRecord, - ) -> Result { - validate_staging_record(self.workspace_id(), &record)?; - let raw = serde_json::to_string(&record)?; - self.database.try_transaction(|transaction| { - require_active_subject(transaction, &record.subject_id)?; - write_staging_candidate(transaction, self.workspace_id(), record, raw) - }) - } - - pub fn record_session_attribution( - &self, - attribution: SubjectSessionAttribution, - ) -> Result { - validate_session_attribution(&attribution)?; - let raw = serde_json::to_string(&attribution)?; - self.database.try_transaction(|transaction| { - require_active_subject(transaction, &attribution.subject_id)?; - write_session_attribution(transaction, attribution, raw) - }) - } - - pub fn session_attribution( - &self, - session_id: &str, - ) -> Result> { - validate_label("session id", session_id)?; - self.database.try_with_connection(|connection| { - let raw = connection - .query_row( - "SELECT record_json FROM subject_session_attributions - WHERE session_id = ?1", - [session_id], - |row| row.get::<_, String>(0), - ) - .optional()?; - raw.map(|raw| parse_session_attribution(&raw)).transpose() - }) - } - - pub fn subject_session_attributions( - &self, - subject_id: &str, - ) -> Result> { - validate_label("subject id", subject_id)?; - self.database.try_with_connection(|connection| { - let mut statement = connection.prepare( - "SELECT record_json FROM subject_session_attributions - WHERE subject_id = ?1 - ORDER BY attributed_at ASC, session_id ASC", - )?; - let rows = statement.query_map([subject_id], |row| row.get::<_, String>(0))?; - let mut records = Vec::new(); - for row in rows { - records.push(parse_session_attribution(&row?)?); - } - Ok(records) - }) - } - - /// Reads one bounded immutable attribution page without materializing the - /// subject's complete history. `snapshot_at` fixes the initial discovery - /// boundary; later Host attributions require a fresh traversal. - pub fn subject_session_attribution_page( - &self, - subject_id: &str, - session_id: Option<&str>, - snapshot_at: &str, - after: Option<(&str, &str)>, - limit: usize, - ) -> Result { - validate_label("subject id", subject_id)?; - validate_label("attribution snapshot", snapshot_at)?; - if let Some(session_id) = session_id { - validate_label("session id", session_id)?; - } - if let Some((attributed_at, session_id)) = after { - validate_label("attribution cursor timestamp", attributed_at)?; - validate_label("attribution cursor session id", session_id)?; - } - if !(1..=100).contains(&limit) { - return Err(SubjektivError::InvalidRecord( - "subject Session attribution page limit must be between 1 and 100".to_string(), - )); - } - let query_limit = limit.saturating_add(1); - self.database.try_with_connection(|connection| { - let raw = match (session_id, after) { - (Some(session_id), _) => { - let mut statement = connection.prepare( - "SELECT record_json FROM subject_session_attributions - WHERE subject_id = ?1 AND session_id = ?2 AND attributed_at <= ?3 - ORDER BY attributed_at DESC, session_id ASC - LIMIT ?4", - )?; - let rows = statement.query_map( - params![subject_id, session_id, snapshot_at, query_limit as i64], - |row| row.get::<_, String>(0), - )?; - rows.collect::, _>>()? - } - (None, Some((after_at, after_session_id))) => { - let mut statement = connection.prepare( - "SELECT record_json FROM subject_session_attributions - WHERE subject_id = ?1 AND attributed_at <= ?2 - AND (attributed_at < ?3 OR (attributed_at = ?3 AND session_id > ?4)) - ORDER BY attributed_at DESC, session_id ASC - LIMIT ?5", - )?; - let rows = statement.query_map( - params![ - subject_id, - snapshot_at, - after_at, - after_session_id, - query_limit as i64 - ], - |row| row.get::<_, String>(0), - )?; - rows.collect::, _>>()? - } - (None, None) => { - let mut statement = connection.prepare( - "SELECT record_json FROM subject_session_attributions - WHERE subject_id = ?1 AND attributed_at <= ?2 - ORDER BY attributed_at DESC, session_id ASC - LIMIT ?3", - )?; - let rows = statement.query_map( - params![subject_id, snapshot_at, query_limit as i64], - |row| row.get::<_, String>(0), - )?; - rows.collect::, _>>()? - } - }; - let mut items = raw - .into_iter() - .map(|raw| parse_session_attribution(&raw)) - .collect::>>()?; - let has_more = items.len() > limit; - if has_more { - items.truncate(limit); - } - Ok(SubjectSessionAttributionPage { items, has_more }) - }) - } - - pub fn staging_candidate( - &self, - subject_id: &str, - candidate_id: &str, - ) -> Result> { - self.database.try_with_connection(|connection| { - let raw = connection - .query_row( - "SELECT record_json FROM staging_records - WHERE subject_id = ?1 AND candidate_id = ?2", - params![subject_id, candidate_id], - |row| row.get::<_, String>(0), - ) - .optional()?; - raw.map(|raw| parse_staging(self.workspace_id(), &raw)) - .transpose() - }) - } - - pub fn pending_staging_candidates( - &self, - subject_id: &str, - limit: usize, - ) -> Result> { - self.database.try_with_connection(|connection| { - require_subject_in_connection(connection, subject_id)?; - let mut statement = connection.prepare( - "SELECT s.record_json - FROM staging_records s - LEFT JOIN staging_resolutions r - ON r.subject_id = s.subject_id AND r.candidate_id = s.candidate_id - WHERE s.subject_id = ?1 AND r.candidate_id IS NULL - ORDER BY s.created_at ASC, s.candidate_id ASC - LIMIT ?2", - )?; - let rows = statement.query_map(params![subject_id, to_i64(limit as u64)?], |row| { - row.get::<_, String>(0) - })?; - let mut records = Vec::new(); - for row in rows { - records.push(parse_staging(self.workspace_id(), &row?)?); - } - Ok(records) - }) - } - - pub fn pending_staging_backlog(&self, subject_id: &str) -> Result<(usize, u64)> { - self.database.try_with_connection(|connection| { - require_subject_in_connection(connection, subject_id)?; - let (count, bytes) = connection.query_row( - "SELECT COUNT(*), COALESCE(SUM(LENGTH(s.record_json)), 0) - FROM staging_records s - LEFT JOIN staging_resolutions r - ON r.subject_id = s.subject_id AND r.candidate_id = s.candidate_id - WHERE s.subject_id = ?1 AND r.candidate_id IS NULL", - [subject_id], - |row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)), - )?; - Ok(( - usize::try_from(count).map_err(|_| { - SubjektivError::InvalidRecord("pending candidate count exceeds usize".into()) - })?, - u64::try_from(bytes).map_err(|_| { - SubjektivError::InvalidRecord("pending candidate bytes are negative".into()) - })?, - )) - }) - } - - pub fn create_memory(&self, subject_id: &str, draft: MemoryDraft) -> Result { - validate_direct_memory_draft(&draft)?; - if draft.state != MemoryState::Active { - return Err(SubjektivError::InvalidRecord( - "a new Memory must start in active state".to_string(), - )); - } - let memory_id = issued_id("memory"); - self.database.try_transaction(|transaction| { - write_memory_revision(transaction, subject_id, &memory_id, None, draft) - }) - } - - pub fn revise_memory( - &self, - subject_id: &str, - memory_id: &str, - expected_revision: u64, - draft: MemoryDraft, - ) -> Result { - validate_direct_memory_draft(&draft)?; - self.database.try_transaction(|transaction| { - write_memory_revision( - transaction, - subject_id, - memory_id, - Some(expected_revision), - draft, - ) - }) - } - - /// Atomically writes a Memory revision and one candidate's immutable - /// resolution. Any conflict or invalid reference rolls both changes back. - pub fn apply_candidate( - &self, - subject_id: &str, - candidate_id: &str, - target: MemoryRevisionTarget, - draft: MemoryDraft, - reason: impl Into, - ) -> Result<(MemoryRecord, StagingResolution)> { - let candidate_ids = vec![candidate_id.to_string()]; - let (memory, mut resolutions) = - self.apply_candidates(subject_id, &candidate_ids, target, draft, reason)?; - Ok((memory, resolutions.remove(0))) - } - - /// Atomically applies one or more candidates to exactly one new Memory - /// revision. The repository derives `source_candidate_ids` from this list so - /// every applied resolution has a reciprocal immutable provenance edge. - pub fn apply_candidates( - &self, - subject_id: &str, - candidate_ids: &[String], - target: MemoryRevisionTarget, - draft: MemoryDraft, - reason: impl Into, - ) -> Result<(MemoryRecord, Vec)> { - if candidate_ids.is_empty() { - return Err(SubjektivError::InvalidRecord( - "candidate application requires at least one candidate".into(), - )); - } - reject_duplicates("applied candidate", candidate_ids)?; - for candidate_id in candidate_ids { - validate_label("candidate id", candidate_id)?; - } - let reason = reason.into(); - validate_nonempty("resolution reason", &reason)?; - self.database.try_transaction(|transaction| { - apply_candidates_in_transaction( - transaction, - subject_id, - candidate_ids, - &target, - draft, - &reason, - ) - }) - } - - pub fn decide_candidate( - &self, - subject_id: &str, - request: CandidateDecisionRequest, - ) -> Result { - validate_label("candidate decision request id", &request.request_id)?; - validate_label("candidate id", &request.candidate_id)?; - validate_nonempty("resolution reason", &request.reason)?; - let request_raw = serde_json::to_string(&request)?; - self.database.try_transaction(|transaction| { - if let Some((stored_request, stored_receipt)) = transaction - .query_row( - "SELECT request_json, receipt_json - FROM candidate_decision_receipts - WHERE subject_id = ?1 AND request_id = ?2", - params![subject_id, request.request_id], - |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)), - ) - .optional()? - { - if stored_request == request_raw { - return serde_json::from_str(&stored_receipt).map_err(Into::into); - } - return Err(SubjektivError::DecisionRequestConflict( - request.request_id.clone(), - )); - } - if transaction - .query_row( - "SELECT 1 FROM candidate_decision_receipts - WHERE subject_id = ?1 AND candidate_id = ?2", - params![subject_id, request.candidate_id], - |_| Ok(()), - ) - .optional()? - .is_some() - { - return Err(SubjektivError::CandidateResolved( - request.candidate_id.clone(), - )); - } - - let (memory, operation, resolution) = match request.decision.clone() { - CandidateDecision::Apply { target, draft } => { - let operation = match &target { - MemoryRevisionTarget::Create => MemoryDecisionOperation::Create, - MemoryRevisionTarget::Revise { .. } => MemoryDecisionOperation::Revise, - }; - let candidate_ids = [request.candidate_id.clone()]; - let (memory, mut resolutions) = apply_candidates_in_transaction( - transaction, - subject_id, - &candidate_ids, - &target, - draft, - &request.reason, - )?; - (Some(memory), Some(operation), resolutions.remove(0)) - } - CandidateDecision::Close { - action, - affected_memory, - } => { - if action == StagingResolutionAction::Applied { - return Err(SubjektivError::InvalidRecord( - "applied candidates require an apply decision".into(), - )); - } - let resolution = insert_resolution( - transaction, - subject_id, - &request.candidate_id, - action, - &request.reason, - &affected_memory, - )?; - let operation = - (!affected_memory.is_empty()).then_some(MemoryDecisionOperation::Reference); - (None, operation, resolution) - } - }; - let store_revision = require_subject(transaction, subject_id)?.store_revision; - let receipt = CandidateDecisionReceipt { - request_id: request.request_id.clone(), - candidate_id: request.candidate_id.clone(), - surface_dirty: memory.is_some(), - memory, - operation, - resolution, - store_revision, - }; - let receipt_raw = serde_json::to_string(&receipt)?; - transaction.execute( - "INSERT INTO candidate_decision_receipts ( - subject_id, request_id, candidate_id, request_json, - receipt_json, created_at - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", - params![ - subject_id, - request.request_id, - request.candidate_id, - request_raw, - receipt_raw, - now() - ], - )?; - Ok(receipt) - }) - } - - pub fn resolve_candidate( - &self, - subject_id: &str, - candidate_id: &str, - action: StagingResolutionAction, - reason: impl Into, - affected_memory: Vec, - ) -> Result { - let reason = reason.into(); - validate_nonempty("resolution reason", &reason)?; - if action == StagingResolutionAction::Applied { - return Err(SubjektivError::InvalidRecord( - "applied candidates must use apply_candidate(s) so Memory and provenance commit atomically" - .to_string(), - )); - } - self.database.try_transaction(|transaction| { - insert_resolution( - transaction, - subject_id, - candidate_id, - action, - &reason, - &affected_memory, - ) - }) - } - - pub fn staging_resolution( - &self, - subject_id: &str, - candidate_id: &str, - ) -> Result> { - self.database.try_with_connection(|connection| { - let raw = connection - .query_row( - "SELECT resolution_json FROM staging_resolutions - WHERE subject_id = ?1 AND candidate_id = ?2", - params![subject_id, candidate_id], - |row| row.get::<_, String>(0), - ) - .optional()?; - raw.map(|raw| parse_resolution(&raw)).transpose() - }) - } - - pub fn memory(&self, subject_id: &str, memory_id: &str) -> Result> { - self.database.try_with_connection(|connection| { - let raw = connection - .query_row( - "SELECT record_json FROM memory_records - WHERE subject_id = ?1 AND memory_id = ?2", - params![subject_id, memory_id], - |row| row.get::<_, String>(0), - ) - .optional()?; - raw.map(|raw| parse_memory(&raw)).transpose() - }) - } - - pub fn memory_revision( - &self, - subject_id: &str, - memory_id: &str, - revision: u64, - ) -> Result> { - self.database.try_with_connection(|connection| { - let raw = connection - .query_row( - "SELECT record_json FROM memory_revisions - WHERE subject_id = ?1 AND memory_id = ?2 AND revision = ?3", - params![subject_id, memory_id, to_i64(revision)?], - |row| row.get::<_, String>(0), - ) - .optional()?; - raw.map(|raw| parse_memory(&raw)).transpose() - }) - } - - /// Reads one current Memory while distinguishing an absent target from a - /// Memory id owned by another subject. - pub fn scoped_memory(&self, subject_id: &str, memory_id: &str) -> Result { - validate_label("subject id", subject_id)?; - validate_label("Memory id", memory_id)?; - self.database.try_with_connection(|connection| { - require_subject_in_connection(connection, subject_id)?; - scoped_memory_in_connection(connection, subject_id, memory_id) - }) - } - - /// Reads one immutable revision after verifying that the Memory belongs to - /// the requested subject. A missing revision of a valid scoped Memory is - /// returned as `None`. - pub fn scoped_memory_revision( - &self, - subject_id: &str, - memory_id: &str, - revision: u64, - ) -> Result> { - validate_label("subject id", subject_id)?; - validate_label("Memory id", memory_id)?; - if revision == 0 { - return Err(SubjektivError::InvalidRecord( - "Memory revision must be greater than zero".into(), - )); - } - self.database.try_with_connection(|connection| { - require_subject_in_connection(connection, subject_id)?; - scoped_memory_in_connection(connection, subject_id, memory_id)?; - let raw = connection - .query_row( - "SELECT record_json FROM memory_revisions - WHERE subject_id = ?1 AND memory_id = ?2 AND revision = ?3", - params![subject_id, memory_id, to_i64(revision)?], - |row| row.get::<_, String>(0), - ) - .optional()?; - raw.map(|raw| parse_memory(&raw)).transpose() - }) - } - - /// Lists current Memory records for one existing subject in deterministic - /// most-recently-updated order. - pub fn list_memories(&self, subject_id: &str) -> Result> { - validate_label("subject id", subject_id)?; - self.database.try_with_connection(|connection| { - require_subject_in_connection(connection, subject_id)?; - let mut statement = connection.prepare( - "SELECT record_json FROM memory_records - WHERE subject_id = ?1 - ORDER BY updated_at DESC, memory_id ASC", - )?; - let rows = statement.query_map([subject_id], |row| row.get::<_, String>(0))?; - let mut records = Vec::new(); - for row in rows { - records.push(parse_memory(&row?)?); - } - Ok(records) - }) - } - - /// Lists every immutable revision of one scoped Memory, newest first. - pub fn list_memory_revisions( - &self, - subject_id: &str, - memory_id: &str, - ) -> Result> { - validate_label("subject id", subject_id)?; - validate_label("Memory id", memory_id)?; - self.database.try_with_connection(|connection| { - require_subject_in_connection(connection, subject_id)?; - scoped_memory_in_connection(connection, subject_id, memory_id)?; - let mut statement = connection.prepare( - "SELECT record_json FROM memory_revisions - WHERE subject_id = ?1 AND memory_id = ?2 - ORDER BY revision DESC", - )?; - let rows = statement.query_map(params![subject_id, memory_id], |row| { - row.get::<_, String>(0) - })?; - let mut records = Vec::new(); - for row in rows { - records.push(parse_memory(&row?)?); - } - Ok(records) - }) - } - - /// Captures one deterministic, bounded generation input from only current - /// active Memory revisions. Kind order is fixed and selection is round-robin - /// across kinds; within each kind, updated_at desc then memory id asc. - pub fn prepare_surface_generation(&self, subject_id: &str) -> Result { - self.prepare_job_surface_generation(subject_id, None) - } - - pub(crate) fn prepare_job_surface_generation( - &self, - subject_id: &str, - job_attempt: Option<(&str, &str)>, - ) -> Result { - self.database.try_transaction(|transaction| { - let subject = require_active_subject(transaction, subject_id)?; - let (active_memory_count, materials) = - select_surface_materials(transaction, subject_id)?; - let generation = SurfaceGeneration { - id: issued_id("surface-generation"), - subject_id: subject_id.to_string(), - store_revision: subject.store_revision, - active_memory_count, - materials, - created_at: now(), - }; - transaction.execute( - "INSERT INTO surface_generation_runs ( - subject_id, generation_id, store_revision, active_memory_count, - materials_json, created_at, job_id, attempt_id - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", - params![ - subject_id, - generation.id, - to_i64(generation.store_revision)?, - to_i64(generation.active_memory_count as u64)?, - serde_json::to_string(&generation.materials)?, - generation.created_at, - job_attempt.map(|(job_id, _)| job_id), - job_attempt.map(|(_, attempt_id)| attempt_id), - ], - )?; - Ok(generation) - }) - } - - /// Validate a Job's structured surface claim against the existing generation - /// and publication/failure records. This does not create another receipt or - /// roll back Memory. The generation must have begun inside this attempt. - #[allow(clippy::too_many_arguments)] - pub(crate) fn validate_job_surface_outcome( - &self, - subject_id: &str, - generation_id: &str, - store_revision: u64, - availability: &str, - snapshot_id: Option<&str>, - reason_code: Option<&str>, - job_id: &str, - attempt_id: &str, - ) -> Result<()> { - self.database.try_with_connection(|connection| { - let subject = require_subject_in_connection(connection, subject_id)?; - let generation_revision = require_job_generation(connection, subject_id, generation_id, job_id, attempt_id)?; - if generation_revision != store_revision || subject.store_revision != store_revision { - return Err(SubjektivError::InvalidRecord("surface generation is stale".into())); - } - let state = connection.query_row( - "SELECT store_revision, status, snapshot_id, reason_code FROM surface_generation_state WHERE subject_id = ?1", - [subject_id], - |row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?, row.get::<_, Option>(2)?, row.get::<_, Option>(3)?)), - ).optional()?.ok_or_else(|| SubjektivError::InvalidRecord("surface generation has no terminal outcome".into()))?; - if state.0 != to_i64(store_revision)? || state.1 != availability - || !matches!(availability, "ready" | "failed") - || state.2.as_deref() != snapshot_id || state.3.as_deref() != reason_code { - return Err(SubjektivError::InvalidRecord("surface claim differs from the persisted outcome".into())); - } - Ok(()) - }) - } - - pub(crate) fn require_job_surface_generation( - &self, - subject_id: &str, - generation_id: &str, - job_id: &str, - attempt_id: &str, - ) -> Result<()> { - self.database.try_with_connection(|connection| { - require_job_generation(connection, subject_id, generation_id, job_id, attempt_id)?; - Ok(()) - }) - } - - /// Validates structured editor output against the exact material snapshot and - /// atomically publishes it only while the subject store generation is current. - pub fn publish_surface_generation( - &self, - subject_id: &str, - generation_id: &str, - points: Vec, - ) -> Result { - validate_label("surface generation id", generation_id)?; - self.database.try_transaction(|transaction| { - let subject = require_active_subject(transaction, subject_id)?; - let (store_revision, active_memory_count, materials_json) = transaction - .query_row( - "SELECT store_revision, active_memory_count, materials_json - FROM surface_generation_runs - WHERE subject_id = ?1 AND generation_id = ?2", - params![subject_id, generation_id], - |row| { - Ok(( - row.get::<_, i64>(0)?, - row.get::<_, i64>(1)?, - row.get::<_, String>(2)?, - )) - }, - ) - .optional()? - .ok_or_else(|| { - SubjektivError::SurfaceGenerationConflict(format!( - "unknown generation `{generation_id}` for subject `{subject_id}`" - )) - })?; - let store_revision = u64::try_from(store_revision).map_err(|_| { - SubjektivError::InvalidRecord("surface generation revision is negative".into()) - })?; - if subject.store_revision != store_revision { - return Err(SubjektivError::SurfaceGenerationConflict(format!( - "generation {store_revision} is stale; current store revision is {}", - subject.store_revision - ))); - } - let materials: Vec = serde_json::from_str(&materials_json)?; - if active_memory_count > 0 && materials.is_empty() { - return Err(SubjektivError::InvalidRecord( - "active Memory exists but no record fits the surface input budget".into(), - )); - } - let (body_md, memory_refs) = validate_surface_points(&materials, &points)?; - if let Some(existing) = - ready_surface_snapshot_for_revision(transaction, subject_id, store_revision)? - { - if existing.body_md == body_md && existing.memory_refs == memory_refs { - return Ok(existing); - } - return Err(SubjektivError::SurfaceGenerationConflict(format!( - "store revision {store_revision} already has a different published surface" - ))); - } - insert_surface_snapshot( - transaction, - subject_id, - body_md, - memory_refs, - store_revision, - ) - }) - } - - /// Records a bounded generator failure without changing confirmed Memory. A - /// late failure cannot replace a ready surface or dirty a newer generation. - pub fn fail_surface_generation( - &self, - subject_id: &str, - generation_id: &str, - reason_code: &str, - ) -> Result { - validate_label("surface generation id", generation_id)?; - validate_label("surface failure reason", reason_code)?; - self.database.try_transaction(|transaction| { - let subject = require_active_subject(transaction, subject_id)?; - let generation_revision = transaction - .query_row( - "SELECT store_revision FROM surface_generation_runs - WHERE subject_id = ?1 AND generation_id = ?2", - params![subject_id, generation_id], - |row| row.get::<_, i64>(0), - ) - .optional()? - .ok_or_else(|| { - SubjektivError::SurfaceGenerationConflict(format!( - "unknown generation `{generation_id}` for subject `{subject_id}`" - )) - })?; - let generation_revision = u64::try_from(generation_revision).map_err(|_| { - SubjektivError::InvalidRecord("surface generation revision is negative".into()) - })?; - if generation_revision == subject.store_revision { - let ready = transaction - .query_row( - "SELECT status FROM surface_generation_state WHERE subject_id = ?1", - [subject_id], - |row| row.get::<_, String>(0), - ) - .optional()? - .is_some_and(|status| status == "ready"); - if !ready { - transaction.execute( - "INSERT INTO surface_generation_state ( - subject_id, store_revision, status, snapshot_id, reason_code, updated_at - ) VALUES (?1, ?2, 'failed', NULL, ?3, ?4) - ON CONFLICT(subject_id) DO UPDATE SET - store_revision = excluded.store_revision, - status = 'failed', snapshot_id = NULL, - reason_code = excluded.reason_code, - updated_at = excluded.updated_at", - params![subject_id, to_i64(generation_revision)?, reason_code, now()], - )?; - } - } - Ok(subject.store_revision) - }) - } - - /// Returns only a current ready snapshot for injection. Other states are - /// explicit so callers never report missing/stale/failed generation as empty - /// confirmed Memory. - pub fn resident_surface(&self, subject_id: &str) -> Result { - self.database.try_with_connection(|connection| { - let subject = require_subject_in_connection(connection, subject_id)?; - resident_surface_in_connection(connection, &subject) - }) - } - - /// Atomically reads user-managed behavior and the independently-fresh - /// generated Memory surface from one SQLite connection snapshot. - pub fn resident_context(&self, subject_id: &str) -> Result { - self.database.try_transaction(|transaction| { - let subject = require_subject(transaction, subject_id)?; - let surface = resident_surface_in_connection(transaction, &subject)?; - Ok(SubjectResidentContext { subject, surface }) - }) - } - - pub fn create_surface_snapshot( - &self, - subject_id: &str, - body_md: impl Into, - memory_refs: Vec, - built_from_store_revision: u64, - ) -> Result { - reject_duplicate_refs("surface snapshot", &memory_refs)?; - reject_duplicate_memory_ids("surface snapshot", &memory_refs)?; - let body_md = body_md.into(); - self.database.try_transaction(|transaction| { - let subject = require_active_subject(transaction, subject_id)?; - if subject.store_revision != built_from_store_revision { - return Err(SubjektivError::InvalidRecord(format!( - "surface snapshot generation {built_from_store_revision} does not match current subject store revision {}", - subject.store_revision - ))); - } - validate_memory_refs(transaction, subject_id, &memory_refs)?; - if estimated_tokens(&body_md) > SURFACE_BODY_TOKEN_BUDGET { - return Err(SubjektivError::InvalidRecord(format!( - "surface snapshot body exceeds {SURFACE_BODY_TOKEN_BUDGET} token estimate" - ))); - } - if let Some(existing) = surface_snapshot_for_revision( - transaction, - subject_id, - built_from_store_revision, - )? { - if existing.body_md == body_md && existing.memory_refs == memory_refs { - return Ok(existing); - } - return Err(SubjektivError::SurfaceGenerationConflict(format!( - "store revision {built_from_store_revision} already has a different published surface" - ))); - } - insert_surface_snapshot( - transaction, - subject_id, - body_md, - memory_refs, - built_from_store_revision, - ) - }) - } - - pub fn surface_snapshot( - &self, - subject_id: &str, - snapshot_id: &str, - ) -> Result> { - self.database.try_with_connection(|connection| { - let raw = connection - .query_row( - "SELECT snapshot_json FROM surface_snapshots - WHERE subject_id = ?1 AND snapshot_id = ?2", - params![subject_id, snapshot_id], - |row| row.get::<_, String>(0), - ) - .optional()?; - raw.map(|raw| parse_surface_snapshot(&raw)).transpose() - }) - } -} - -fn select_surface_materials( - transaction: &Transaction<'_>, - subject_id: &str, -) -> Result<(usize, Vec)> { - let mut statement = transaction.prepare( - "SELECT record_json FROM memory_records - WHERE subject_id = ?1 AND state = 'active' - ORDER BY updated_at DESC, memory_id ASC", - )?; - let rows = statement.query_map([subject_id], |row| row.get::<_, String>(0))?; - let mut buckets: [Vec; 6] = std::array::from_fn(|_| Vec::new()); - let mut active_memory_count = 0usize; - for row in rows { - let memory = parse_memory(&row?)?; - active_memory_count = active_memory_count.saturating_add(1); - let bucket = surface_kind_index(&memory.kind); - if buckets[bucket].len() < SURFACE_PER_KIND_LIMIT { - buckets[bucket].push(SurfaceMaterial { - memory_id: memory.id, - revision: memory.revision, - kind: memory.kind, - body_md: memory.body_md, - why_useful: memory.why_useful, - staleness: memory.staleness, - }); - } - } - - let populated_kind_count = buckets.iter().filter(|bucket| !bucket.is_empty()).count(); - let per_kind_token_budget = SURFACE_MATERIAL_TOKEN_BUDGET / populated_kind_count.max(1); - let mut ordered = Vec::new(); - for offset in 0..SURFACE_PER_KIND_LIMIT { - for (bucket_index, bucket) in buckets.iter().enumerate() { - if let Some(material) = bucket.get(offset) { - ordered.push((bucket_index, material.clone())); - } - } - } - - let mut accepted = vec![false; ordered.len()]; - let mut selected_count = 0usize; - let mut deferred = Vec::new(); - for (index, (bucket_index, material)) in ordered.iter().enumerate() { - if selected_count == SURFACE_TOTAL_MATERIAL_LIMIT { - break; - } - let kind_materials = ordered - .iter() - .zip(&accepted) - .filter(|((selected_bucket, _), accepted)| { - **accepted && selected_bucket == bucket_index - }) - .map(|((_, material), _)| material) - .chain(std::iter::once(material)); - let all_materials = ordered - .iter() - .zip(&accepted) - .filter(|(_, accepted)| **accepted) - .map(|((_, material), _)| material) - .chain(std::iter::once(material)); - if serialized_material_tokens(kind_materials)? <= per_kind_token_budget - && serialized_material_tokens(all_materials)? <= SURFACE_MATERIAL_TOKEN_BUDGET - { - accepted[index] = true; - selected_count += 1; - } else { - deferred.push(index); - } - } - - // Category shares are fairness reservations, not hard ceilings. Reclaim - // unused shares in the same canonical order after every populated category - // had its first opportunity. - for index in deferred { - if selected_count == SURFACE_TOTAL_MATERIAL_LIMIT { - break; - } - let material = &ordered[index].1; - let all_materials = ordered - .iter() - .zip(&accepted) - .filter(|(_, accepted)| **accepted) - .map(|((_, material), _)| material) - .chain(std::iter::once(material)); - if serialized_material_tokens(all_materials)? <= SURFACE_MATERIAL_TOKEN_BUDGET { - accepted[index] = true; - selected_count += 1; - } - } - - let selected = ordered - .into_iter() - .zip(accepted) - .filter_map(|((_, material), accepted)| accepted.then_some(material)) - .collect(); - Ok((active_memory_count, selected)) -} - -fn serialized_material_tokens<'a>( - materials: impl IntoIterator, -) -> Result { - let materials = materials.into_iter().collect::>(); - Ok(estimated_tokens_for_bytes( - serde_json::to_vec(&materials)?.len(), - )) -} - -fn surface_kind_index(kind: &CandidateKind) -> usize { - match kind { - CandidateKind::Preference => 0, - CandidateKind::Constraint => 1, - CandidateKind::Decision => 2, - CandidateKind::WorkingAssumption => 3, - CandidateKind::OpenQuestion => 4, - CandidateKind::Lesson => 5, - } -} - -fn validate_surface_points( - materials: &[SurfaceMaterial], - points: &[SurfacePoint], -) -> Result<(String, Vec)> { - if materials.is_empty() { - if points.is_empty() { - return Ok((String::new(), Vec::new())); - } - return Err(SubjektivError::InvalidRecord( - "an empty material set requires an empty surface".into(), - )); - } - if points.is_empty() { - return Err(SubjektivError::InvalidRecord( - "non-empty surface materials require at least one grounded point".into(), - )); - } - if points.len() > SURFACE_TOTAL_MATERIAL_LIMIT { - return Err(SubjektivError::InvalidRecord(format!( - "surface output exceeds {SURFACE_TOTAL_MATERIAL_LIMIT} points" - ))); - } - let allowed = materials - .iter() - .map(|material| (material.memory_id.as_str(), material.revision)) - .collect::>(); - let mut bodies = Vec::with_capacity(points.len()); - let mut seen = HashSet::new(); - let mut refs = Vec::new(); - for point in points { - let body = point.body_md.trim(); - validate_nonempty("surface point body", body)?; - if point.memory_refs.is_empty() { - return Err(SubjektivError::InvalidRecord( - "every surface point requires at least one Memory reference".into(), - )); - } - reject_duplicate_refs("surface point", &point.memory_refs)?; - for reference in &point.memory_refs { - if !allowed.contains(&(reference.memory_id.as_str(), reference.revision)) { - return Err(SubjektivError::SubjectScopeMismatch { - subject_id: "surface generation materials".into(), - reference: format!("{}@{}", reference.memory_id, reference.revision), - }); - } - if seen.insert((reference.memory_id.clone(), reference.revision)) { - refs.push(reference.clone()); - } - } - bodies.push(body.to_string()); - } - let body_md = bodies.join("\n\n"); - if estimated_tokens(&body_md) > SURFACE_BODY_TOKEN_BUDGET { - return Err(SubjektivError::InvalidRecord(format!( - "surface body exceeds {SURFACE_BODY_TOKEN_BUDGET} token estimate" - ))); - } - Ok((body_md, refs)) -} - -fn ready_surface_snapshot_for_revision( - transaction: &Transaction<'_>, - subject_id: &str, - store_revision: u64, -) -> Result> { - let raw = transaction - .query_row( - "SELECT snapshots.snapshot_json - FROM surface_generation_state state - JOIN surface_snapshots snapshots - ON snapshots.subject_id = state.subject_id - AND snapshots.snapshot_id = state.snapshot_id - WHERE state.subject_id = ?1 - AND state.store_revision = ?2 - AND state.status = 'ready'", - params![subject_id, to_i64(store_revision)?], - |row| row.get::<_, String>(0), - ) - .optional()?; - raw.map(|raw| parse_surface_snapshot(&raw)).transpose() -} - -fn surface_snapshot_for_revision( - transaction: &Transaction<'_>, - subject_id: &str, - store_revision: u64, -) -> Result> { - let raw = transaction - .query_row( - "SELECT snapshot_json FROM surface_snapshots - WHERE subject_id = ?1 AND built_from_store_revision = ?2 - ORDER BY created_at ASC, snapshot_id ASC LIMIT 1", - params![subject_id, to_i64(store_revision)?], - |row| row.get::<_, String>(0), - ) - .optional()?; - raw.map(|raw| parse_surface_snapshot(&raw)).transpose() -} - -fn insert_surface_snapshot( - transaction: &Transaction<'_>, - subject_id: &str, - body_md: String, - memory_refs: Vec, - built_from_store_revision: u64, -) -> Result { - validate_memory_refs(transaction, subject_id, &memory_refs)?; - let snapshot = SurfaceSnapshot { - schema_version: SUBJEKTIV_SCHEMA_VERSION, - id: issued_id("surface"), - subject_id: subject_id.to_string(), - body_md, - memory_refs, - built_from_store_revision, - created_at: now(), - }; - let raw = serde_json::to_string(&snapshot)?; - transaction.execute( - "INSERT INTO surface_snapshots ( - subject_id, snapshot_id, built_from_store_revision, snapshot_json, created_at - ) VALUES (?1, ?2, ?3, ?4, ?5)", - params![ - subject_id, - snapshot.id, - to_i64(snapshot.built_from_store_revision)?, - raw, - snapshot.created_at - ], - )?; - for reference in &snapshot.memory_refs { - transaction.execute( - "INSERT INTO surface_snapshot_refs ( - subject_id, snapshot_id, memory_id, revision - ) VALUES (?1, ?2, ?3, ?4)", - params![ - subject_id, - snapshot.id, - reference.memory_id, - to_i64(reference.revision)? - ], - )?; - } - transaction.execute( - "INSERT INTO surface_snapshot_seals (subject_id, snapshot_id) VALUES (?1, ?2)", - params![subject_id, snapshot.id], - )?; - transaction.execute( - "INSERT INTO surface_generation_state ( - subject_id, store_revision, status, snapshot_id, reason_code, updated_at - ) VALUES (?1, ?2, 'ready', ?3, NULL, ?4) - ON CONFLICT(subject_id) DO UPDATE SET - store_revision = excluded.store_revision, - status = 'ready', snapshot_id = excluded.snapshot_id, - reason_code = NULL, updated_at = excluded.updated_at", - params![ - subject_id, - to_i64(built_from_store_revision)?, - snapshot.id, - snapshot.created_at - ], - )?; - Ok(snapshot) -} - -fn estimated_tokens(value: &str) -> usize { - estimated_tokens_for_bytes(value.len()) -} - -fn estimated_tokens_for_bytes(bytes: usize) -> usize { - bytes.saturating_add(3) / 4 -} - -fn apply_candidates_in_transaction( - transaction: &Transaction<'_>, - subject_id: &str, - candidate_ids: &[String], - target: &MemoryRevisionTarget, - mut draft: MemoryDraft, - reason: &str, -) -> Result<(MemoryRecord, Vec)> { - let mut candidates = Vec::with_capacity(candidate_ids.len()); - for candidate_id in candidate_ids { - let raw = require_unresolved_candidate(transaction, subject_id, candidate_id)?; - candidates.push(parse_staging_without_workspace(&raw)?); - } - for candidate in &candidates { - validate_candidate_application(transaction, subject_id, candidate, target, &draft)?; - } - draft.source_candidate_ids = candidate_ids.to_vec(); - let new_memory_id = issued_id("memory"); - let memory = match target { - MemoryRevisionTarget::Create => { - if draft.state != MemoryState::Active { - return Err(SubjektivError::InvalidRecord( - "a new Memory must start in active state".into(), - )); - } - write_memory_revision(transaction, subject_id, &new_memory_id, None, draft)? - } - MemoryRevisionTarget::Revise { - memory_id, - expected_revision, - } => write_memory_revision( - transaction, - subject_id, - memory_id, - Some(*expected_revision), - draft, - )?, - }; - let reference = MemoryRevisionRef { - memory_id: memory.id.clone(), - revision: memory.revision, - }; - let resolutions = candidate_ids - .iter() - .map(|candidate_id| { - insert_resolution( - transaction, - subject_id, - candidate_id, - StagingResolutionAction::Applied, - reason, - std::slice::from_ref(&reference), - ) - }) - .collect::>>()?; - Ok((memory, resolutions)) -} - -fn write_memory_revision( - transaction: &Transaction<'_>, - subject_id: &str, - memory_id: &str, - expected_revision: Option, - draft: MemoryDraft, -) -> Result { - validate_memory_draft(&draft)?; - let mut subject = require_active_subject(transaction, subject_id)?; - let current_raw = transaction - .query_row( - "SELECT record_json FROM memory_records - WHERE subject_id = ?1 AND memory_id = ?2", - params![subject_id, memory_id], - |row| row.get::<_, String>(0), - ) - .optional()?; - let current = current_raw.map(|raw| parse_memory(&raw)).transpose()?; - - let (revision, created_at) = match (current.as_ref(), expected_revision) { - (None, None) => (1, now()), - (None, Some(_)) => return Err(SubjektivError::MemoryNotFound(memory_id.to_string())), - (Some(_), None) => { - return Err(SubjektivError::InvalidRecord(format!( - "memory `{memory_id}` already exists" - ))); - } - (Some(current), Some(expected)) if current.revision != expected => { - return Err(SubjektivError::RevisionConflict { - memory_id: memory_id.to_string(), - expected, - actual: current.revision, - }); - } - (Some(current), Some(_)) => { - validate_state_transition(memory_id, current.state, draft.state)?; - (current.revision + 1, current.created_at.clone()) - } - }; - - reject_duplicates("source candidate", &draft.source_candidate_ids)?; - reject_duplicate_refs("derived Memory", &draft.derived_from)?; - validate_candidate_refs(transaction, subject_id, &draft.source_candidate_ids)?; - validate_memory_refs(transaction, subject_id, &draft.derived_from)?; - - let timestamp = now(); - let record = MemoryRecord { - schema_version: SUBJEKTIV_SCHEMA_VERSION, - id: memory_id.to_string(), - subject_id: subject_id.to_string(), - revision, - kind: draft.kind, - state: draft.state, - claim: draft.claim, - body_md: draft.body_md, - why_useful: draft.why_useful, - staleness: draft.staleness, - source_candidate_ids: draft.source_candidate_ids, - derived_from: draft.derived_from, - change_reason: draft.change_reason, - created_at, - updated_at: timestamp, - }; - let raw = serde_json::to_string(&record)?; - - if current.is_none() { - transaction.execute( - "INSERT INTO memory_records ( - subject_id, memory_id, current_revision, kind, state, - record_json, created_at, updated_at - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", - params![ - subject_id, - memory_id, - to_i64(revision)?, - candidate_kind_name(&record.kind), - memory_state_name(record.state), - raw, - record.created_at, - record.updated_at - ], - )?; - } - transaction.execute( - "INSERT INTO memory_revisions ( - subject_id, memory_id, revision, kind, state, record_json, created_at - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", - params![ - subject_id, - memory_id, - to_i64(revision)?, - candidate_kind_name(&record.kind), - memory_state_name(record.state), - raw, - record.updated_at - ], - )?; - if current.is_some() { - transaction.execute( - "UPDATE memory_records - SET current_revision = ?3, kind = ?4, state = ?5, - record_json = ?6, updated_at = ?7 - WHERE subject_id = ?1 AND memory_id = ?2", - params![ - subject_id, - memory_id, - to_i64(revision)?, - candidate_kind_name(&record.kind), - memory_state_name(record.state), - raw, - record.updated_at - ], - )?; - } - for candidate_id in &record.source_candidate_ids { - transaction.execute( - "INSERT INTO memory_revision_candidates ( - subject_id, memory_id, revision, candidate_id - ) VALUES (?1, ?2, ?3, ?4)", - params![subject_id, memory_id, to_i64(revision)?, candidate_id], - )?; - } - for source in &record.derived_from { - transaction.execute( - "INSERT INTO memory_revision_derivations ( - subject_id, memory_id, revision, source_memory_id, source_revision - ) VALUES (?1, ?2, ?3, ?4, ?5)", - params![ - subject_id, - memory_id, - to_i64(revision)?, - source.memory_id, - to_i64(source.revision)? - ], - )?; - } - transaction.execute( - "INSERT INTO memory_revision_seals (subject_id, memory_id, revision) - VALUES (?1, ?2, ?3)", - params![subject_id, memory_id, to_i64(revision)?], - )?; - - subject.store_revision = subject - .store_revision - .checked_add(1) - .ok_or_else(|| SubjektivError::InvalidRecord("subject store revision overflow".into()))?; - subject.updated_at = record.updated_at.clone(); - let subject_raw = serde_json::to_string(&subject)?; - transaction.execute( - "UPDATE subjects - SET store_revision = ?2, record_json = ?3, updated_at = ?4 - WHERE subject_id = ?1", - params![ - subject_id, - to_i64(subject.store_revision)?, - subject_raw, - subject.updated_at - ], - )?; - transaction.execute( - "INSERT INTO surface_generation_state ( - subject_id, store_revision, status, snapshot_id, reason_code, updated_at - ) VALUES (?1, ?2, 'dirty', NULL, NULL, ?3) - ON CONFLICT(subject_id) DO UPDATE SET - store_revision = excluded.store_revision, - status = 'dirty', snapshot_id = NULL, reason_code = NULL, - updated_at = excluded.updated_at", - params![ - subject_id, - to_i64(subject.store_revision)?, - subject.updated_at - ], - )?; - Ok(record) -} - -fn write_staging_candidate( - transaction: &Transaction<'_>, - workspace_id: &str, - record: SubjectStagingRecord, - raw: String, -) -> Result { - let existing = transaction - .query_row( - "SELECT record_json FROM staging_records - WHERE subject_id = ?1 AND candidate_id = ?2", - params![record.subject_id, record.id], - |row| row.get::<_, String>(0), - ) - .optional()?; - if let Some(existing) = existing { - if equivalent_staging_payload(&existing, &raw)? { - return parse_staging(workspace_id, &existing); - } - return Err(SubjektivError::CandidateConflict(record.id.clone())); - } - validate_staged_revision_proposal(transaction, &record)?; - transaction.execute( - "INSERT INTO staging_records ( - subject_id, candidate_id, kind, record_json, created_at - ) VALUES (?1, ?2, ?3, ?4, ?5)", - params![ - record.subject_id, - record.id, - candidate_kind_name(&record.kind), - raw, - record.created_at - ], - )?; - Ok(record) -} - -fn write_session_attribution( - transaction: &Transaction<'_>, - record: SubjectSessionAttribution, - raw: String, -) -> Result { - let existing = transaction - .query_row( - "SELECT record_json FROM subject_session_attributions WHERE session_id = ?1", - [&record.session_id], - |row| row.get::<_, String>(0), - ) - .optional()?; - if let Some(existing) = existing { - let existing = parse_session_attribution(&existing)?; - if existing.subject_id != record.subject_id { - return Err(SubjektivError::SessionSubjectConflict { - session_id: record.session_id, - existing_subject_id: existing.subject_id, - requested_subject_id: record.subject_id, - }); - } - if equivalent_session_attribution(&existing, &record) { - return Ok(existing); - } - return Err(SubjektivError::SessionAttributionConflict( - record.session_id, - )); - } - transaction.execute( - "INSERT INTO subject_session_attributions ( - session_id, subject_id, runtime_id, worker_id, record_json, attributed_at - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", - params![ - record.session_id, - record.subject_id, - record.runtime_id, - record.worker_id, - raw, - record.attributed_at - ], - )?; - Ok(record) -} - -fn insert_resolution( - transaction: &Transaction<'_>, - subject_id: &str, - candidate_id: &str, - action: StagingResolutionAction, - reason: &str, - affected_memory: &[MemoryRevisionRef], -) -> Result { - require_active_subject(transaction, subject_id)?; - reject_duplicate_refs("staging resolution", affected_memory)?; - validate_memory_refs(transaction, subject_id, affected_memory)?; - if action == StagingResolutionAction::Applied { - if affected_memory.is_empty() { - return Err(SubjektivError::InvalidRecord( - "an applied candidate requires an affected Memory revision".into(), - )); - } - for reference in affected_memory { - let reciprocal = transaction - .query_row( - "SELECT 1 FROM memory_revision_candidates - WHERE subject_id = ?1 AND memory_id = ?2 - AND revision = ?3 AND candidate_id = ?4", - params![ - subject_id, - reference.memory_id, - to_i64(reference.revision)?, - candidate_id - ], - |_| Ok(()), - ) - .optional()? - .is_some(); - if !reciprocal { - return Err(SubjektivError::InvalidRecord(format!( - "applied candidate `{candidate_id}` has no reciprocal provenance edge to `{}@{}`", - reference.memory_id, reference.revision - ))); - } - } - } - let candidate_raw = require_unresolved_candidate(transaction, subject_id, candidate_id)?; - let candidate = parse_staging_without_workspace(&candidate_raw)?; - if candidate.subject_id != subject_id { - return Err(SubjektivError::SubjectScopeMismatch { - subject_id: subject_id.to_string(), - reference: candidate_id.to_string(), - }); - } - let resolution = StagingResolution { - schema_version: SUBJEKTIV_SCHEMA_VERSION, - id: issued_id("resolution"), - subject_id: subject_id.to_string(), - candidate_id: candidate_id.to_string(), - action, - reason: reason.to_string(), - affected_memory: affected_memory.to_vec(), - candidate, - resolved_at: now(), - }; - let affected_raw = serde_json::to_string(affected_memory)?; - let resolution_raw = serde_json::to_string(&resolution)?; - transaction.execute( - "INSERT INTO staging_resolutions ( - subject_id, candidate_id, resolution_id, action, reason, - affected_refs_json, staging_raw_json, resolution_json, resolved_at - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", - params![ - subject_id, - candidate_id, - resolution.id, - resolution_action_name(action), - reason, - affected_raw, - candidate_raw, - resolution_raw, - resolution.resolved_at - ], - )?; - for reference in affected_memory { - transaction.execute( - "INSERT INTO staging_resolution_targets ( - subject_id, candidate_id, memory_id, revision - ) VALUES (?1, ?2, ?3, ?4)", - params![ - subject_id, - candidate_id, - reference.memory_id, - to_i64(reference.revision)? - ], - )?; - } - transaction.execute( - "INSERT INTO staging_resolution_seals (subject_id, candidate_id) - VALUES (?1, ?2)", - params![subject_id, candidate_id], - )?; - Ok(resolution) -} - -fn require_unresolved_candidate( - transaction: &Transaction<'_>, - subject_id: &str, - candidate_id: &str, -) -> Result { - let raw = transaction - .query_row( - "SELECT record_json FROM staging_records - WHERE subject_id = ?1 AND candidate_id = ?2", - params![subject_id, candidate_id], - |row| row.get::<_, String>(0), - ) - .optional()?; - let raw = match raw { - Some(raw) => raw, - None => { - let foreign = transaction - .query_row( - "SELECT subject_id FROM staging_records WHERE candidate_id = ?1 LIMIT 1", - [candidate_id], - |row| row.get::<_, String>(0), - ) - .optional()?; - if foreign.is_some() { - return Err(SubjektivError::SubjectScopeMismatch { - subject_id: subject_id.to_string(), - reference: candidate_id.to_string(), - }); - } - return Err(SubjektivError::CandidateNotFound(candidate_id.to_string())); - } - }; - let resolved = transaction - .query_row( - "SELECT 1 FROM staging_resolutions - WHERE subject_id = ?1 AND candidate_id = ?2", - params![subject_id, candidate_id], - |_| Ok(()), - ) - .optional()? - .is_some(); - if resolved { - return Err(SubjektivError::CandidateResolved(candidate_id.to_string())); - } - Ok(raw) -} - -fn require_subject(transaction: &Transaction<'_>, subject_id: &str) -> Result { - require_subject_in_connection(transaction, subject_id) -} - -fn require_subject_in_connection( - connection: &rusqlite::Connection, - subject_id: &str, -) -> Result { - let raw = connection - .query_row( - "SELECT record_json FROM subjects WHERE subject_id = ?1", - [subject_id], - |row| row.get::<_, String>(0), - ) - .optional()? - .ok_or_else(|| SubjektivError::SubjectNotFound(subject_id.to_string()))?; - parse_subject(&raw) -} - -fn require_active_subject( - transaction: &Transaction<'_>, - subject_id: &str, -) -> Result { - let subject = require_subject(transaction, subject_id)?; - if subject.state == SubjectState::Retired { - return Err(SubjektivError::SubjectRetired(subject_id.to_string())); - } - Ok(subject) -} - -fn validate_candidate_refs( - transaction: &Transaction<'_>, - subject_id: &str, - candidate_ids: &[String], -) -> Result<()> { - for candidate_id in candidate_ids { - let exists = transaction - .query_row( - "SELECT 1 FROM staging_records - WHERE subject_id = ?1 AND candidate_id = ?2", - params![subject_id, candidate_id], - |_| Ok(()), - ) - .optional()? - .is_some(); - if !exists { - return Err(SubjektivError::SubjectScopeMismatch { - subject_id: subject_id.to_string(), - reference: candidate_id.clone(), - }); - } - } - Ok(()) -} - -fn validate_memory_refs( - transaction: &Transaction<'_>, - subject_id: &str, - references: &[MemoryRevisionRef], -) -> Result<()> { - for reference in references { - if reference.revision == 0 { - return Err(SubjektivError::InvalidRecord(format!( - "Memory `{}` revision must be greater than zero", - reference.memory_id - ))); - } - let exists = transaction - .query_row( - "SELECT 1 FROM memory_revisions - WHERE subject_id = ?1 AND memory_id = ?2 AND revision = ?3", - params![subject_id, reference.memory_id, to_i64(reference.revision)?], - |_| Ok(()), - ) - .optional()? - .is_some(); - if !exists { - return Err(SubjektivError::SubjectScopeMismatch { - subject_id: subject_id.to_string(), - reference: format!("{}@{}", reference.memory_id, reference.revision), - }); - } - } - Ok(()) -} - -fn validate_session_attribution(record: &SubjectSessionAttribution) -> Result<()> { - validate_schema_version(record.schema_version, "subject Session attribution")?; - validate_label("subject id", &record.subject_id)?; - validate_label("runtime id", &record.runtime_id)?; - validate_label("worker id", &record.worker_id)?; - validate_label("session id", &record.session_id)?; - validate_label("Session attribution timestamp", &record.attributed_at)?; - chrono::DateTime::parse_from_rfc3339(&record.attributed_at).map_err(|error| { - SubjektivError::InvalidRecord(format!( - "Session attribution timestamp is not RFC 3339: {error}" - )) - })?; - Ok(()) -} - -fn attach_session_to_candidate(record: &mut SubjectStagingRecord, session_id: &str) -> Result<()> { - if record.source_refs.is_empty() { - record.source_refs.push(SourceEvidenceRef { - session_id: Some(session_id.to_string()), - segment_id: Some(record.source.segment_id.clone()), - entry_range: Some(record.source.range), - ..SourceEvidenceRef::default() - }); - return Ok(()); - } - for source in &mut record.source_refs { - match source.session_id.as_deref() { - Some(existing) if existing != session_id => { - return Err(SubjektivError::SubjectScopeMismatch { - subject_id: record.subject_id.clone(), - reference: format!( - "candidate source Session `{existing}` does not match attributed Session `{session_id}`" - ), - }); - } - Some(_) => {} - None => source.session_id = Some(session_id.to_string()), - } - } - Ok(()) -} - -fn validate_staging_record(workspace_id: &str, record: &SubjectStagingRecord) -> Result<()> { - if record.schema_version != STAGING_SCHEMA_VERSION { - return Err(SubjektivError::InvalidRecord(format!( - "staging schema version {} is unsupported", - record.schema_version - ))); - } - validate_label("candidate id", &record.id)?; - validate_label("subject id", &record.subject_id)?; - validate_label("extract run id", &record.extract_run_id)?; - validate_label("source segment id", &record.source.segment_id)?; - validate_nonempty("candidate claim", &record.claim)?; - validate_nonempty("candidate why_useful", &record.why_useful)?; - validate_range("candidate source", record.source.range)?; - if matches!(record.kind, CandidateKind::Preference) - && record.evidence.iter().any(|evidence| { - !matches!( - evidence.origin.as_ref().map(|origin| &origin.kind), - Some(memory::schema::EvidenceOriginKind::HumanInput) - ) - }) - { - return Err(SubjektivError::InvalidRecord( - "preference candidates require exclusively HumanInput evidence".to_string(), - )); - } - - let mut evidence_ids = HashSet::new(); - for evidence in &record.evidence { - validate_label("evidence id", &evidence.id)?; - if !evidence_ids.insert(evidence.id.as_str()) { - return Err(SubjektivError::InvalidRecord(format!( - "duplicate evidence id `{}`", - evidence.id - ))); - } - validate_nonempty("evidence kind", evidence.kind.as_str())?; - if let Some(range) = evidence.entry_range { - validate_range("evidence entry", range)?; - } - } - for source in &record.source_refs { - if let Some(session_id) = source.session_id.as_deref() { - validate_label("source session id", session_id)?; - } - if let Some(segment_id) = source.segment_id.as_deref() { - validate_label("source segment id", segment_id)?; - } - if let Some(range) = source.entry_range { - validate_range("source evidence entry", range)?; - } - if let Some(evidence_id) = source.evidence_id.as_deref() { - validate_label("source evidence id", evidence_id)?; - if !evidence_ids.contains(evidence_id) { - return Err(SubjektivError::InvalidRecord(format!( - "source evidence id `{evidence_id}` is not present in evidence" - ))); - } - } - if let Some(kind) = source.evidence_kind.as_ref() { - validate_nonempty("source evidence kind", kind.as_str())?; - } - } - for origin in record - .evidence - .iter() - .filter_map(|evidence| evidence.origin.as_ref()) - .chain( - record - .source_refs - .iter() - .filter_map(|source| source.origin.as_ref()), - ) - { - if let Some(origin_workspace) = origin.workspace_id.as_deref() - && origin_workspace != workspace_id - { - return Err(SubjektivError::SubjectScopeMismatch { - subject_id: record.subject_id.clone(), - reference: format!("evidence Workspace {origin_workspace}"), - }); - } - } - if let Some(proposal) = &record.revision_proposal { - validate_revision_proposal_metadata(proposal)?; - } - Ok(()) -} - -/// Applies the current write-admission contract separately from structural -/// validation. Persisted rows from older compatible implementations remain -/// readable even when a later release tightens admission limits. -fn validate_staging_admission(record: &SubjectStagingRecord) -> Result<()> { - if record.evidence.len() > MAX_STAGING_ANCHORS || record.source_refs.len() > MAX_STAGING_ANCHORS - { - return Err(SubjektivError::InvalidRecord(format!( - "staging evidence/source_refs are limited to {MAX_STAGING_ANCHORS} items each" - ))); - } - Ok(()) -} - -fn validate_revision_proposal_metadata(proposal: &RevisionProposal) -> Result<()> { - validate_label("revision proposal Memory id", &proposal.memory_id)?; - if proposal.expected_revision == 0 { - return Err(SubjektivError::InvalidRecord( - "revision proposal expected_revision must be greater than zero".into(), - )); - } - validate_nonempty("revision proposal change_reason", &proposal.change_reason) -} - -fn validate_candidate_application( - transaction: &Transaction<'_>, - subject_id: &str, - candidate: &SubjectStagingRecord, - target: &MemoryRevisionTarget, - draft: &MemoryDraft, -) -> Result<()> { - let Some(proposal) = &candidate.revision_proposal else { - return Ok(()); - }; - let MemoryRevisionTarget::Revise { - memory_id, - expected_revision, - } = target - else { - return Err(SubjektivError::InvalidRecord( - "a revision proposal cannot create a different Memory".into(), - )); - }; - if memory_id != &proposal.memory_id || expected_revision != &proposal.expected_revision { - return Err(SubjektivError::InvalidRecord( - "candidate application target must exactly match revision proposal metadata".into(), - )); - } - let current = scoped_memory_in_connection(transaction, subject_id, memory_id)?; - if current.revision != *expected_revision { - return Err(SubjektivError::RevisionConflict { - memory_id: memory_id.clone(), - expected: *expected_revision, - actual: current.revision, - }); - } - let expected_state = match proposal.intent { - RevisionProposalIntent::Revise => current.state, - RevisionProposalIntent::Resolve => MemoryState::Resolved, - RevisionProposalIntent::Retract => MemoryState::Retracted, - RevisionProposalIntent::Reopen => MemoryState::Active, - }; - let intent_allowed = match proposal.intent { - RevisionProposalIntent::Revise => { - matches!(current.state, MemoryState::Active | MemoryState::Resolved) - } - RevisionProposalIntent::Resolve => current.state == MemoryState::Active, - RevisionProposalIntent::Retract => { - matches!(current.state, MemoryState::Active | MemoryState::Resolved) - } - RevisionProposalIntent::Reopen => current.state == MemoryState::Resolved, - }; - if !intent_allowed || draft.state != expected_state { - return Err(SubjektivError::InvalidStateTransition { - memory_id: memory_id.clone(), - from: current.state, - to: draft.state, - }); - } - if draft.kind != current.kind || draft.kind != candidate.kind { - return Err(SubjektivError::InvalidRecord( - "revision proposal application must preserve the target Memory kind".into(), - )); - } - if draft.change_reason != proposal.change_reason { - return Err(SubjektivError::InvalidRecord( - "revision proposal application must preserve change_reason".into(), - )); - } - Ok(()) -} - -fn validate_staged_revision_proposal( - transaction: &Transaction<'_>, - record: &SubjectStagingRecord, -) -> Result<()> { - let Some(proposal) = &record.revision_proposal else { - return Ok(()); - }; - let current = - scoped_memory_in_connection(transaction, &record.subject_id, &proposal.memory_id)?; - if current.revision != proposal.expected_revision { - return Err(SubjektivError::RevisionConflict { - memory_id: proposal.memory_id.clone(), - expected: proposal.expected_revision, - actual: current.revision, - }); - } - let to = match proposal.intent { - RevisionProposalIntent::Revise => current.state, - RevisionProposalIntent::Resolve => MemoryState::Resolved, - RevisionProposalIntent::Retract => MemoryState::Retracted, - RevisionProposalIntent::Reopen => MemoryState::Active, - }; - let allowed = match proposal.intent { - RevisionProposalIntent::Revise => { - matches!(current.state, MemoryState::Active | MemoryState::Resolved) - } - RevisionProposalIntent::Resolve => current.state == MemoryState::Active, - RevisionProposalIntent::Retract => { - matches!(current.state, MemoryState::Active | MemoryState::Resolved) - } - RevisionProposalIntent::Reopen => current.state == MemoryState::Resolved, - }; - if allowed { - Ok(()) - } else { - Err(SubjektivError::InvalidStateTransition { - memory_id: proposal.memory_id.clone(), - from: current.state, - to, - }) - } -} - -fn scoped_memory_in_connection( - connection: &rusqlite::Connection, - subject_id: &str, - memory_id: &str, -) -> Result { - let raw = connection - .query_row( - "SELECT record_json FROM memory_records - WHERE subject_id = ?1 AND memory_id = ?2", - params![subject_id, memory_id], - |row| row.get::<_, String>(0), - ) - .optional()?; - if let Some(raw) = raw { - return parse_memory(&raw); - } - let foreign_owner = connection - .query_row( - "SELECT subject_id FROM memory_records WHERE memory_id = ?1 LIMIT 1", - [memory_id], - |row| row.get::<_, String>(0), - ) - .optional()?; - if foreign_owner.is_some() { - Err(SubjektivError::SubjectScopeMismatch { - subject_id: subject_id.to_string(), - reference: memory_id.to_string(), - }) - } else { - Err(SubjektivError::MemoryNotFound(memory_id.to_string())) - } -} - -fn validate_memory_draft(draft: &MemoryDraft) -> Result<()> { - validate_nonempty("Memory claim", &draft.claim)?; - validate_nonempty("Memory why_useful", &draft.why_useful)?; - validate_nonempty("Memory change_reason", &draft.change_reason)?; - for candidate_id in &draft.source_candidate_ids { - validate_label("source candidate id", candidate_id)?; - } - for reference in &draft.derived_from { - validate_label("derived Memory id", &reference.memory_id)?; - } - Ok(()) -} - -fn validate_direct_memory_draft(draft: &MemoryDraft) -> Result<()> { - if draft.source_candidate_ids.is_empty() { - Ok(()) - } else { - Err(SubjektivError::InvalidRecord( - "candidate provenance must be committed through apply_candidate(s)".into(), - )) - } -} - -fn validate_state_transition(memory_id: &str, from: MemoryState, to: MemoryState) -> Result<()> { - let allowed = from != MemoryState::Retracted; - if allowed { - Ok(()) - } else { - Err(SubjektivError::InvalidStateTransition { - memory_id: memory_id.to_string(), - from, - to, - }) - } -} - -fn parse_subject(raw: &str) -> Result { - let record: SubjectRecord = serde_json::from_str(raw)?; - validate_schema_version(record.schema_version, "subject")?; - Ok(record) -} - -fn equivalent_session_attribution( - left: &SubjectSessionAttribution, - right: &SubjectSessionAttribution, -) -> bool { - left.schema_version == right.schema_version - && left.subject_id == right.subject_id - && left.runtime_id == right.runtime_id - && left.worker_id == right.worker_id - && left.session_id == right.session_id -} - -fn equivalent_staging_payload(left: &str, right: &str) -> Result { - let mut left = serde_json::from_str::(left)?; - let mut right = serde_json::from_str::(right)?; - let left_object = left.as_object_mut().ok_or_else(|| { - SubjektivError::InvalidRecord("stored staging record is not a JSON object".into()) - })?; - let right_object = right.as_object_mut().ok_or_else(|| { - SubjektivError::InvalidRecord("new staging record is not a JSON object".into()) - })?; - // A retry may reconstruct the host envelope after an ambiguous response. - // Identity and extracted content must match, while the first persisted - // host-issued staging timestamp remains authoritative. - left_object.remove("created_at"); - right_object.remove("created_at"); - Ok(left == right) -} - -fn parse_session_attribution(raw: &str) -> Result { - let record: SubjectSessionAttribution = serde_json::from_str(raw)?; - validate_session_attribution(&record)?; - Ok(record) -} - -fn parse_staging(workspace_id: &str, raw: &str) -> Result { - let record = parse_staging_without_workspace(raw)?; - validate_staging_record(workspace_id, &record)?; - Ok(record) -} - -fn parse_staging_without_workspace(raw: &str) -> Result { - let record: SubjectStagingRecord = serde_json::from_str(raw)?; - if record.schema_version != STAGING_SCHEMA_VERSION { - return Err(SubjektivError::InvalidRecord(format!( - "staging schema version {} is unsupported", - record.schema_version - ))); - } - Ok(record) -} - -fn parse_memory(raw: &str) -> Result { - let record: MemoryRecord = serde_json::from_str(raw)?; - validate_schema_version(record.schema_version, "Memory")?; - if record.revision == 0 { - return Err(SubjektivError::InvalidRecord( - "Memory revision must be greater than zero".to_string(), - )); - } - Ok(record) -} - -fn parse_resolution(raw: &str) -> Result { - let record: StagingResolution = serde_json::from_str(raw)?; - validate_schema_version(record.schema_version, "staging resolution")?; - Ok(record) -} - -fn parse_surface_snapshot(raw: &str) -> Result { - let record: SurfaceSnapshot = serde_json::from_str(raw)?; - validate_schema_version(record.schema_version, "surface snapshot")?; - Ok(record) -} - -fn validate_schema_version(version: u32, kind: &str) -> Result<()> { - if version == SUBJEKTIV_SCHEMA_VERSION { - Ok(()) - } else { - Err(SubjektivError::InvalidRecord(format!( - "{kind} schema version {version} is unsupported" - ))) - } -} - -fn validate_range(kind: &str, range: [u64; 2]) -> Result<()> { - if range[0] <= range[1] { - Ok(()) - } else { - Err(SubjektivError::InvalidRecord(format!( - "{kind} range is reversed" - ))) - } -} - -fn resident_surface_in_connection( - connection: &Connection, - subject: &SubjectRecord, -) -> Result { - let state = connection - .query_row( - "SELECT store_revision, status, snapshot_id - FROM surface_generation_state WHERE subject_id = ?1", - [&subject.id], - |row| { - Ok(( - row.get::<_, i64>(0)?, - row.get::<_, String>(1)?, - row.get::<_, Option>(2)?, - )) - }, - ) - .optional()?; - let Some((revision, status, snapshot_id)) = state else { - return Ok(ResidentSurface { - availability: SurfaceAvailability::Ungenerated, - snapshot: None, - }); - }; - let revision = u64::try_from(revision) - .map_err(|_| SubjektivError::InvalidRecord("surface state revision is negative".into()))?; - if revision != subject.store_revision || status == "dirty" { - return Ok(ResidentSurface { - availability: SurfaceAvailability::Stale, - snapshot: None, - }); - } - if status == "failed" { - return Ok(ResidentSurface { - availability: SurfaceAvailability::Failed, - snapshot: None, - }); - } - let snapshot_id = snapshot_id.ok_or_else(|| { - SubjektivError::InvalidRecord("ready surface state has no snapshot id".into()) - })?; - let raw = connection.query_row( - "SELECT snapshot_json FROM surface_snapshots - WHERE subject_id = ?1 AND snapshot_id = ?2", - params![subject.id, snapshot_id], - |row| row.get::<_, String>(0), - )?; - let snapshot = parse_surface_snapshot(&raw)?; - if snapshot.built_from_store_revision != subject.store_revision { - return Err(SubjektivError::InvalidRecord( - "ready surface snapshot generation does not match subject".into(), - )); - } - Ok(ResidentSurface { - availability: SurfaceAvailability::Ready, - snapshot: Some(snapshot), - }) -} - -pub fn validate_subject_behavior(value: &str) -> Result<()> { - if value.len() > MAX_SUBJECT_BEHAVIOR_BYTES { - return Err(SubjektivError::InvalidRecord(format!( - "subject behavior must be at most {MAX_SUBJECT_BEHAVIOR_BYTES} bytes" - ))); - } - if !value.is_empty() && value.trim().is_empty() { - return Err(SubjektivError::InvalidRecord( - "subject behavior must be empty or contain non-whitespace text".to_string(), - )); - } - if value - .chars() - .any(|character| character.is_control() && !matches!(character, '\n' | '\r' | '\t')) - { - return Err(SubjektivError::InvalidRecord( - "subject behavior contains an unsupported control character".to_string(), - )); - } - Ok(()) -} - -fn validate_label(kind: &str, value: &str) -> Result<()> { - validate_nonempty(kind, value)?; - if value.len() > 256 || value.chars().any(char::is_control) { - return Err(SubjektivError::InvalidRecord(format!( - "{kind} must be at most 256 bytes and contain no control characters" - ))); - } - Ok(()) -} - -fn validate_nonempty(kind: &str, value: &str) -> Result<()> { - if value.trim().is_empty() { - Err(SubjektivError::InvalidRecord(format!( - "{kind} must not be empty" - ))) - } else { - Ok(()) - } -} - -fn reject_duplicates(kind: &str, values: &[String]) -> Result<()> { - let mut seen = HashSet::new(); - if let Some(duplicate) = values.iter().find(|value| !seen.insert(value.as_str())) { - return Err(SubjektivError::InvalidRecord(format!( - "duplicate {kind} `{duplicate}`" - ))); - } - Ok(()) -} - -fn reject_duplicate_refs(kind: &str, values: &[MemoryRevisionRef]) -> Result<()> { - let mut seen = HashSet::new(); - if let Some(duplicate) = values - .iter() - .find(|value| !seen.insert((value.memory_id.as_str(), value.revision))) - { - return Err(SubjektivError::InvalidRecord(format!( - "duplicate {kind} `{}@{}`", - duplicate.memory_id, duplicate.revision - ))); - } - Ok(()) -} - -fn reject_duplicate_memory_ids(kind: &str, values: &[MemoryRevisionRef]) -> Result<()> { - let mut seen = HashSet::new(); - if let Some(duplicate) = values - .iter() - .find(|value| !seen.insert(value.memory_id.as_str())) - { - return Err(SubjektivError::InvalidRecord(format!( - "duplicate {kind} Memory `{}`", - duplicate.memory_id - ))); - } - Ok(()) -} - -fn require_job_generation( - connection: &Connection, - subject_id: &str, - generation_id: &str, - job_id: &str, - attempt_id: &str, -) -> Result { - let revision: Option = connection.query_row( - "SELECT store_revision FROM surface_generation_runs WHERE subject_id = ?1 AND generation_id = ?2 AND job_id = ?3 AND attempt_id = ?4", - params![subject_id, generation_id, job_id, attempt_id], |row| row.get(0), - ).optional()?; - revision - .and_then(|value| u64::try_from(value).ok()) - .ok_or_else(|| { - SubjektivError::InvalidRecord( - "surface generation is not owned by this Job attempt".into(), - ) - }) -} - -fn to_i64(value: u64) -> Result { - i64::try_from(value).map_err(|_| { - SubjektivError::InvalidRecord(format!("revision value {value} exceeds SQLite range")) - }) -} - -fn issued_id(kind: &str) -> String { - format!("{kind}-{}", Uuid::now_v7()) -} - -fn now() -> String { - Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true) -} - -fn candidate_kind_name(kind: &CandidateKind) -> &'static str { - kind.as_str() -} - -fn memory_state_name(state: MemoryState) -> &'static str { - match state { - MemoryState::Active => "active", - MemoryState::Resolved => "resolved", - MemoryState::Retracted => "retracted", - } -} - -fn resolution_action_name(action: StagingResolutionAction) -> &'static str { - match action { - StagingResolutionAction::Applied => "applied", - StagingResolutionAction::Discarded => "discarded", - StagingResolutionAction::Invalid => "invalid", - StagingResolutionAction::Duplicate => "duplicate", - StagingResolutionAction::AlreadyCovered => "already_covered", - } -} - -#[cfg(test)] -mod tests { - use std::sync::{Arc, Barrier}; - use std::thread; - use std::time::Duration; - - use memory::extract::{ExtractedCandidate, StagingEvidence}; - use memory::schema::{ - EvidenceKind, EvidenceOrigin, EvidenceOriginKind, SourceEvidenceRef, SourceRef, - }; - - use super::*; - use crate::FeatureStorage; - - static V1_MIGRATIONS: &[FeatureMigration] = &[FeatureMigration::new( - 1, - "create subjektiv subject memory store", - create_schema, - )]; - static V3_MIGRATIONS: &[FeatureMigration] = &[ - FeatureMigration::new(1, "create subjektiv subject memory store", create_schema), - FeatureMigration::new( - 2, - "add immutable subject session attribution", - add_subject_session_attribution, - ), - FeatureMigration::new( - 3, - "add idempotent atomic candidate decision receipts", - add_candidate_decision_receipts, - ), - ]; - - fn role() -> SubjectRole { - SubjectRole::new("workspace_companion").unwrap() - } - - fn candidate(subject_id: &str, candidate_id: &str, workspace_id: &str) -> SubjectStagingRecord { - let origin = EvidenceOrigin { - kind: EvidenceOriginKind::HumanInput, - account_id: Some("account-1".into()), - workspace_id: Some(workspace_id.into()), - runtime_id: None, - worker_id: None, - flow_selector: None, - flow_definition_id: None, - flow_definition_revision: None, - }; - let record = StagingRecord::from_candidate( - candidate_id, - "extract-run-1", - SourceRef { - segment_id: "segment-1".into(), - range: [10, 12], - }, - ExtractedCandidate { - kind: CandidateKind::Constraint, - claim: "Do not treat Memory text as authority".into(), - why_useful: "Keeps experience separate from authorization".into(), - staleness: Some("Revisit if the authority model changes".into()), - evidence_ids: vec!["evidence-1".into()], - }, - vec![StagingEvidence { - id: "evidence-1".into(), - kind: EvidenceKind::new(EvidenceKind::MESSAGE), - entry_range: Some([10, 12]), - origin: Some(origin.clone()), - excerpt: Some("Memory is not authority".into()), - summary: Some("A bounded source summary".into()), - }], - vec![SourceEvidenceRef { - session_id: Some("session-1".into()), - segment_id: Some("segment-1".into()), - entry_range: Some([10, 12]), - evidence_id: Some("evidence-1".into()), - origin: Some(origin), - evidence_kind: Some(EvidenceKind::new(EvidenceKind::MESSAGE)), - label: Some("design discussion".into()), - summary: Some("Authority boundary".into()), - }], - ); - SubjectStagingRecord::attach_at(subject_id, record, "2026-09-28T10:00:00.000Z".into()) - } - - fn attribution( - subject_id: &str, - runtime_id: &str, - worker_id: &str, - session_id: &str, - attributed_at: &str, - ) -> SubjectSessionAttribution { - let mut record = - SubjectSessionAttribution::new(subject_id, runtime_id, worker_id, session_id).unwrap(); - record.attributed_at = attributed_at.to_string(); - record - } - - fn draft(claim: &str, reason: &str) -> MemoryDraft { - MemoryDraft::active( - CandidateKind::Constraint, - claim, - "Context, applicability, and consequences.", - "It preserves the subject's learned boundary.", - reason, - ) - } - - fn proposal( - intent: RevisionProposalIntent, - memory_id: &str, - expected_revision: u64, - change_reason: &str, - ) -> RevisionProposal { - RevisionProposal::new(intent, memory_id, expected_revision, change_reason).unwrap() - } - - fn open_store( - root: &std::path::Path, - workspace_id: &str, - ) -> (FeatureStorage, WorkspaceFeatureStorage, SubjektivStore) { - let manager = FeatureStorage::new(root); - let workspace = manager.workspace(workspace_id).unwrap(); - let registration = SubjektivStore::register(&workspace).unwrap(); - let store = SubjektivStore::open(&workspace, ®istration).unwrap(); - (manager, workspace, store) - } - - #[test] - fn subject_listing_is_deterministic_bounded_and_read_only() { - let temp = tempfile::tempdir().unwrap(); - let (_manager, _workspace, store) = open_store(temp.path(), "workspace-a"); - let mut expected = vec![ - store.create_subject(role()).unwrap(), - store.create_subject(role()).unwrap(), - store.create_subject(role()).unwrap(), - ]; - expected.sort_by(|left, right| { - left.created_at - .cmp(&right.created_at) - .then_with(|| left.id.cmp(&right.id)) - }); - - let page = store.list_subjects(2).unwrap(); - assert_eq!(page.items, expected[..2]); - assert!(page.has_more); - let after = page.items.last().unwrap(); - let next = store - .list_subjects_after(2, Some((&after.created_at, &after.id))) - .unwrap(); - assert_eq!(next.items, expected[2..]); - assert!(!next.has_more); - assert!(store.list_subjects(0).is_err()); - assert!(store.list_subjects(101).is_err()); - - assert!(store.subject("missing-subject").unwrap().is_none()); - let complete = store.list_subjects(3).unwrap(); - assert_eq!(complete.items, expected); - assert!(!complete.has_more); - } - - #[test] - fn subject_behavior_is_bounded_revisioned_and_independent_from_memory_revision() { - let temp = tempfile::tempdir().unwrap(); - let (_manager, _workspace, store) = open_store(temp.path(), "workspace-a"); - let subject = store - .create_subject_with_behavior(role(), "Prefer explicit evidence.".to_string()) - .unwrap(); - assert_eq!(subject.behavior_revision, 0); - assert_eq!(subject.store_revision, 0); - - let updated = store - .update_subject_behavior( - &subject.id, - 0, - "Prefer explicit evidence.\nAsk when uncertain.".to_string(), - ) - .unwrap(); - assert_eq!(updated.behavior_revision, 1); - assert_eq!(updated.store_revision, 0); - - store - .create_memory( - &subject.id, - draft( - "A confirmed observation", - "exercise Memory lifecycle isolation", - ), - ) - .unwrap(); - let after_memory = store.subject(&subject.id).unwrap().unwrap(); - assert_eq!(after_memory.behavior_md, updated.behavior_md); - assert_eq!(after_memory.behavior_revision, 1); - assert_eq!(after_memory.store_revision, 1); - - let (_reopened_manager, _reopened_workspace, reopened) = - open_store(temp.path(), "workspace-a"); - let persisted = reopened.subject(&subject.id).unwrap().unwrap(); - assert_eq!(persisted.behavior_md, updated.behavior_md); - assert_eq!(persisted.behavior_revision, 1); - assert!(matches!( - store.update_subject_behavior(&subject.id, 0, String::new()), - Err(SubjektivError::SubjectBehaviorConflict { - expected: 0, - actual: 1, - .. - }) - )); - - let unchanged = store - .update_subject_behavior(&subject.id, 1, updated.behavior_md.clone()) - .unwrap(); - assert_eq!(unchanged.behavior_revision, 1); - let cleared = store - .update_subject_behavior(&subject.id, 1, String::new()) - .unwrap(); - assert_eq!(cleared.behavior_revision, 2); - assert!(cleared.behavior_md.is_empty()); - assert!(validate_subject_behavior(&"x".repeat(MAX_SUBJECT_BEHAVIOR_BYTES + 1)).is_err()); - assert!(validate_subject_behavior(" ").is_err()); - } - - #[test] - fn attributed_candidates_preserve_revision_and_surface_reference_continuity() { - let temp = tempfile::tempdir().unwrap(); - let (_manager, _workspace, store) = open_store(temp.path(), "workspace-a"); - let subject = store.create_subject(role()).unwrap(); - let session = attribution( - &subject.id, - "runtime-1", - "worker-1", - "session-1", - "2026-09-28T09:00:00.000Z", - ); - - store.record_session_attribution(session.clone()).unwrap(); - let (first_candidate, stored_session) = store - .stage_candidate_with_attribution( - candidate(&subject.id, "candidate-1", "workspace-a"), - session.clone(), - ) - .unwrap(); - assert_eq!(stored_session, session); - assert_eq!( - first_candidate.source_refs[0].session_id.as_deref(), - Some("session-1") - ); - - let create_request = CandidateDecisionRequest { - request_id: "decision-create-1".into(), - candidate_id: first_candidate.id.clone(), - reason: "adopt the attributed candidate".into(), - decision: CandidateDecision::Apply { - target: MemoryRevisionTarget::Create, - draft: draft( - "Keep the first confirmed rule", - "initial attributed decision", - ), - }, - }; - let created = store - .decide_candidate(&subject.id, create_request.clone()) - .unwrap(); - let first_revision = created.memory.unwrap(); - assert_eq!(first_revision.revision, 1); - assert_eq!( - first_revision.source_candidate_ids, - [first_candidate.id.clone()] - ); - assert_eq!(created.resolution.affected_memory.len(), 1); - assert_eq!(created.resolution.affected_memory[0].revision, 1); - - // Exact decision retries return the original receipt rather than creating - // a second revision or resolution. - let replayed = store.decide_candidate(&subject.id, create_request).unwrap(); - assert_eq!(replayed.store_revision, created.store_revision); - assert_eq!(replayed.memory.as_ref().unwrap().revision, 1); - - let (second_candidate, _) = store - .stage_candidate_with_attribution( - candidate(&subject.id, "candidate-2", "workspace-a"), - session, - ) - .unwrap(); - let revised = store - .decide_candidate( - &subject.id, - CandidateDecisionRequest { - request_id: "decision-revise-2".into(), - candidate_id: second_candidate.id.clone(), - reason: "apply the attributed correction".into(), - decision: CandidateDecision::Apply { - target: MemoryRevisionTarget::Revise { - memory_id: first_revision.id.clone(), - expected_revision: 1, - }, - draft: draft("Keep the corrected confirmed rule", "attributed correction"), - }, - }, - ) - .unwrap(); - let second_revision = revised.memory.unwrap(); - assert_eq!(second_revision.revision, 2); - assert_eq!(second_revision.source_candidate_ids, [second_candidate.id]); - - let historical = store - .scoped_memory_revision(&subject.id, &first_revision.id, 1) - .unwrap() - .unwrap(); - assert_eq!(historical.source_candidate_ids, [first_candidate.id]); - assert_eq!( - store - .session_attribution("session-1") - .unwrap() - .unwrap() - .subject_id, - subject.id - ); - - let generation = store.prepare_surface_generation(&subject.id).unwrap(); - assert_eq!(generation.materials.len(), 1); - assert_eq!(generation.materials[0].memory_id, second_revision.id); - assert_eq!(generation.materials[0].revision, 2); - let expected_ref = MemoryRevisionRef { - memory_id: second_revision.id.clone(), - revision: second_revision.revision, - }; - let published = store - .publish_surface_generation( - &subject.id, - &generation.id, - vec![SurfacePoint { - body_md: "- Keep the corrected confirmed rule".into(), - memory_refs: vec![expected_ref.clone()], - }], - ) - .unwrap(); - assert_eq!(published.memory_refs, [expected_ref.clone()]); - assert_eq!(published.built_from_store_revision, revised.store_revision); - - let resident = store.resident_surface(&subject.id).unwrap(); - assert_eq!(resident.availability, SurfaceAvailability::Ready); - assert_eq!(resident.snapshot.unwrap().memory_refs, [expected_ref]); - } - - #[test] - fn versioned_json_roundtrips_and_rejects_invalid_shapes() { - let staging = candidate("subject-1", "candidate-1", "workspace-a"); - let raw = serde_json::to_string_pretty(&staging).unwrap(); - let decoded: SubjectStagingRecord = serde_json::from_str(&raw).unwrap(); - assert_eq!(decoded.schema_version, STAGING_SCHEMA_VERSION); - assert_eq!(decoded.subject_id, "subject-1"); - assert_eq!( - decoded.source_refs[0].session_id.as_deref(), - Some("session-1") - ); - assert!(decoded.revision_proposal.is_none()); - assert!(!raw.contains("revision_proposal")); - assert!(raw.contains("human_input")); - - let proposed = staging - .clone() - .with_revision_proposal(proposal( - RevisionProposalIntent::Revise, - "memory-1", - 3, - "Incorporate the new bounded evidence", - )) - .unwrap(); - let proposed_raw = serde_json::to_string(&proposed).unwrap(); - let proposed_decoded: SubjectStagingRecord = serde_json::from_str(&proposed_raw).unwrap(); - assert_eq!( - proposed_decoded.revision_proposal, - Some(proposal( - RevisionProposalIntent::Revise, - "memory-1", - 3, - "Incorporate the new bounded evidence", - )) - ); - assert!( - RevisionProposal::new( - RevisionProposalIntent::Revise, - "memory-1", - 0, - "invalid zero revision" - ) - .is_err() - ); - assert!( - RevisionProposal::new(RevisionProposalIntent::Revise, "memory-1", 1, " ").is_err() - ); - assert!( - serde_json::from_value::(serde_json::json!({ - "intent": "revise", - "memory_id": "memory-1", - "expected_revision": 0, - "change_reason": "invalid" - })) - .is_err() - ); - - let mut value = serde_json::to_value(&staging).unwrap(); - value - .as_object_mut() - .unwrap() - .insert("invented".into(), serde_json::json!(true)); - assert!(serde_json::from_value::(value).is_err()); - - let memory = MemoryRecord { - schema_version: SUBJEKTIV_SCHEMA_VERSION, - id: "memory-1".into(), - subject_id: "subject-1".into(), - revision: 1, - kind: CandidateKind::Constraint, - state: MemoryState::Active, - claim: "Memory is not authority".into(), - body_md: "Applies to every tool call.".into(), - why_useful: "Avoids privilege confusion".into(), - staleness: None, - source_candidate_ids: vec!["candidate-1".into()], - derived_from: vec![], - change_reason: "Accepted the staged constraint".into(), - created_at: "2026-09-28T10:00:00.000Z".into(), - updated_at: "2026-09-28T10:00:00.000Z".into(), - }; - let raw = serde_json::to_string(&memory).unwrap(); - let decoded: MemoryRecord = serde_json::from_str(&raw).unwrap(); - assert_eq!(decoded.revision, 1); - assert!(serde_json::from_str::(&raw.replace("active", "unknown")).is_err()); - } - - #[test] - fn v1_store_migrates_before_attributed_staging() { - let temp = tempfile::tempdir().unwrap(); - let root = temp.path().join("storage"); - let subject_id = { - let manager = FeatureStorage::new(&root); - let workspace = manager.workspace("workspace-a").unwrap(); - let registration = workspace - .register(FeatureRegistration::new( - SUBJEKTIV_FEATURE_ID, - V1_MIGRATIONS, - )) - .unwrap(); - let store = SubjektivStore::open(&workspace, ®istration).unwrap(); - let timestamp = now(); - let subject_id = issued_id("subject"); - let mut raw = serde_json::json!({ - "schema_version": SUBJEKTIV_SCHEMA_VERSION, - "id": subject_id, - "role": role(), - "state": SubjectState::Active, - "store_revision": 0, - "created_at": timestamp.clone(), - "updated_at": timestamp.clone(), - }); - let raw = serde_json::to_string(&raw.take()).unwrap(); - store - .database - .try_transaction(|transaction| -> Result<()> { - transaction.execute( - "INSERT INTO subjects ( - subject_id, role, state, store_revision, - record_json, created_at, updated_at - ) VALUES (?1, ?2, 'active', 0, ?3, ?4, ?5)", - params![subject_id, role().as_str(), raw, timestamp, timestamp], - )?; - Ok(()) - }) - .unwrap(); - subject_id - }; - - let (_manager, _workspace, store) = open_store(&root, "workspace-a"); - let migrated_subject = store.subject(&subject_id).unwrap().unwrap(); - assert!(migrated_subject.behavior_md.is_empty()); - assert_eq!(migrated_subject.behavior_revision, 0); - let lifecycle_attribution = attribution( - &subject_id, - "runtime-1", - "worker-1", - "session-1", - "2026-09-28T10:00:00.000Z", - ); - store - .record_session_attribution(lifecycle_attribution.clone()) - .unwrap(); - let (staged, stored_attribution) = store - .stage_candidate_with_attribution( - candidate(&subject_id, "candidate-after-migration", "workspace-a"), - lifecycle_attribution, - ) - .unwrap(); - - assert_eq!(staged.subject_id, subject_id); - assert_eq!(stored_attribution.session_id, "session-1"); - assert_eq!( - store.session_attribution("session-1").unwrap().unwrap(), - stored_attribution - ); - } - - #[test] - fn legacy_surface_snapshot_migrates_as_stale_until_regenerated() { - let temp = tempfile::tempdir().unwrap(); - let root = temp.path().join("storage"); - let (subject_id, snapshot_id) = { - let manager = FeatureStorage::new(&root); - let workspace = manager.workspace("workspace-a").unwrap(); - let registration = workspace - .register(FeatureRegistration::new( - SUBJEKTIV_FEATURE_ID, - V3_MIGRATIONS, - )) - .unwrap(); - let store = SubjektivStore::open(&workspace, ®istration).unwrap(); - let subject_id = issued_id("subject"); - let timestamp = now(); - let subject_json = serde_json::to_string(&serde_json::json!({ - "schema_version": SUBJEKTIV_SCHEMA_VERSION, - "id": subject_id, - "role": role(), - "state": SubjectState::Active, - "store_revision": 0, - "created_at": timestamp, - "updated_at": timestamp, - })) - .unwrap(); - let snapshot = SurfaceSnapshot { - schema_version: SUBJEKTIV_SCHEMA_VERSION, - id: "legacy-surface".into(), - subject_id: subject_id.clone(), - body_md: "Legacy summary without generation-policy evidence.".into(), - memory_refs: Vec::new(), - built_from_store_revision: 0, - created_at: now(), - }; - store - .database - .try_transaction(|transaction| -> Result<()> { - transaction.execute( - "INSERT INTO subjects ( - subject_id, role, state, store_revision, - record_json, created_at, updated_at - ) VALUES (?1, ?2, 'active', 0, ?3, ?4, ?5)", - params![ - subject_id, - role().as_str(), - subject_json, - timestamp, - timestamp - ], - )?; - transaction.execute( - "INSERT INTO surface_snapshots ( - subject_id, snapshot_id, built_from_store_revision, - snapshot_json, created_at - ) VALUES (?1, ?2, ?3, ?4, ?5)", - params![ - subject_id, - snapshot.id, - to_i64(snapshot.built_from_store_revision)?, - serde_json::to_string(&snapshot)?, - snapshot.created_at - ], - )?; - transaction.execute( - "INSERT INTO surface_snapshot_seals (subject_id, snapshot_id) - VALUES (?1, ?2)", - params![subject_id, snapshot.id], - )?; - Ok(()) - }) - .unwrap(); - (subject_id, snapshot.id) - }; - - let (_manager, _workspace, store) = open_store(&root, "workspace-a"); - let resident = store.resident_surface(&subject_id).unwrap(); - assert_eq!(resident.availability, SurfaceAvailability::Stale); - assert!(resident.snapshot.is_none()); - assert!( - store - .surface_snapshot(&subject_id, &snapshot_id) - .unwrap() - .is_some(), - "migration retains the legacy snapshot as history without injecting it" - ); - - let generation = store.prepare_surface_generation(&subject_id).unwrap(); - let regenerated = store - .publish_surface_generation(&subject_id, &generation.id, Vec::new()) - .unwrap(); - assert_ne!(regenerated.id, snapshot_id); - let ready = store.resident_surface(&subject_id).unwrap(); - assert_eq!(ready.availability, SurfaceAvailability::Ready); - assert_eq!(ready.snapshot.unwrap().id, regenerated.id); - assert!( - store - .surface_snapshot(&subject_id, &snapshot_id) - .unwrap() - .is_some(), - "regeneration appends instead of mutating legacy history" - ); - let retried = store - .publish_surface_generation(&subject_id, &generation.id, Vec::new()) - .unwrap(); - assert_eq!(retried.id, regenerated.id); - } - - #[test] - fn attributed_staging_requires_lifecycle_record_and_retry_is_idempotent() { - let temp = tempfile::tempdir().unwrap(); - let (_manager, _workspace, store) = open_store(&temp.path().join("storage"), "workspace-a"); - let subject = store.create_subject(role()).unwrap(); - let first_attribution = attribution( - &subject.id, - "runtime-1", - "worker-1", - "session-1", - "2026-09-28T10:00:00.000Z", - ); - assert!(matches!( - store.stage_candidate_with_attribution( - candidate(&subject.id, "candidate-unattributed", "workspace-a"), - first_attribution.clone(), - ), - Err(SubjektivError::InvalidRecord(message)) - if message.contains("has not completed Host attribution") - )); - assert!(store.session_attribution("session-1").unwrap().is_none()); - assert!( - store - .staging_candidate(&subject.id, "candidate-unattributed") - .unwrap() - .is_none() - ); - store - .record_session_attribution(first_attribution.clone()) - .unwrap(); - let (first_candidate, first_attribution) = store - .stage_candidate_with_attribution( - candidate(&subject.id, "candidate-1", "workspace-a"), - first_attribution, - ) - .unwrap(); - - let mut retried_candidate = first_candidate.clone(); - retried_candidate.created_at = "2026-09-28T11:00:00.000Z".into(); - let retried_attribution = attribution( - &subject.id, - "runtime-1", - "worker-1", - "session-1", - "2026-09-28T11:00:00.000Z", - ); - let (retried_candidate, retried_attribution) = store - .stage_candidate_with_attribution(retried_candidate, retried_attribution) - .unwrap(); - assert_eq!(retried_candidate.created_at, first_candidate.created_at); - assert_eq!(retried_attribution, first_attribution); - - let mut conflicting_candidate = candidate(&subject.id, "candidate-1", "workspace-a"); - conflicting_candidate.claim = "different content".into(); - conflicting_candidate.source_refs[0].session_id = Some("session-2".into()); - let conflicting_attribution = attribution( - &subject.id, - "runtime-1", - "worker-1", - "session-2", - "2026-09-28T12:00:00.000Z", - ); - store - .record_session_attribution(conflicting_attribution.clone()) - .unwrap(); - let result = - store.stage_candidate_with_attribution(conflicting_candidate, conflicting_attribution); - assert!(matches!( - result, - Err(SubjektivError::CandidateConflict(candidate_id)) - if candidate_id == "candidate-1" - )); - assert!(store.session_attribution("session-2").unwrap().is_some()); - } - - #[test] - fn attribution_pages_are_bounded_descending_and_snapshot_fixed() { - let temp = tempfile::tempdir().unwrap(); - let (_manager, _workspace, store) = open_store(&temp.path().join("storage"), "workspace-a"); - let subject = store.create_subject(role()).unwrap(); - for (session, at) in [ - ("session-1", "2026-09-28T10:00:00.000Z"), - ("session-2", "2026-09-28T11:00:00.000Z"), - ("session-3", "2026-09-28T11:00:00.000Z"), - ] { - store - .record_session_attribution(attribution( - &subject.id, - "runtime-1", - "worker-1", - session, - at, - )) - .unwrap(); - } - - let first = store - .subject_session_attribution_page( - &subject.id, - None, - "2026-09-28T11:30:00.000Z", - None, - 2, - ) - .unwrap(); - assert_eq!( - first - .items - .iter() - .map(|item| item.session_id.as_str()) - .collect::>(), - vec!["session-2", "session-3"] - ); - assert!(first.has_more); - - store - .record_session_attribution(attribution( - &subject.id, - "runtime-2", - "worker-2", - "session-later", - "2026-09-28T12:00:00.000Z", - )) - .unwrap(); - let last = first.items.last().unwrap(); - let second = store - .subject_session_attribution_page( - &subject.id, - None, - "2026-09-28T11:30:00.000Z", - Some((&last.attributed_at, &last.session_id)), - 2, - ) - .unwrap(); - assert_eq!(second.items.len(), 1); - assert_eq!(second.items[0].session_id, "session-1"); - assert!(!second.has_more); - assert!( - second - .items - .iter() - .all(|item| item.session_id != "session-later") - ); - - let exact = store - .subject_session_attribution_page( - &subject.id, - Some("session-1"), - "2026-09-28T11:30:00.000Z", - None, - 20, - ) - .unwrap(); - assert_eq!(exact.items.len(), 1); - assert!(!exact.has_more); - } - - #[test] - fn worker_replacement_preserves_subject_history_and_rejects_cross_subject_session() { - let temp = tempfile::tempdir().unwrap(); - let (_manager, _workspace, store) = open_store(&temp.path().join("storage"), "workspace-a"); - let subject = store.create_subject(role()).unwrap(); - let other_subject = store.create_subject(role()).unwrap(); - for lifecycle_attribution in [ - attribution( - &subject.id, - "runtime-1", - "worker-1", - "session-1", - "2026-09-28T10:00:00.000Z", - ), - attribution( - &subject.id, - "runtime-2", - "worker-2", - "session-2", - "2026-09-28T11:00:00.000Z", - ), - ] { - store - .record_session_attribution(lifecycle_attribution) - .unwrap(); - } - - store - .stage_candidate_with_attribution( - candidate(&subject.id, "candidate-1", "workspace-a"), - attribution( - &subject.id, - "runtime-1", - "worker-1", - "session-1", - "2026-09-28T10:00:00.000Z", - ), - ) - .unwrap(); - let mut replacement_candidate = candidate(&subject.id, "candidate-2", "workspace-a"); - replacement_candidate.source_refs[0].session_id = None; - let (replacement_candidate, _) = store - .stage_candidate_with_attribution( - replacement_candidate, - attribution( - &subject.id, - "runtime-2", - "worker-2", - "session-2", - "2026-09-28T11:00:00.000Z", - ), - ) - .unwrap(); - assert_eq!( - replacement_candidate.source_refs[0].session_id.as_deref(), - Some("session-2") - ); - - let history = store.subject_session_attributions(&subject.id).unwrap(); - assert_eq!(history.len(), 2); - assert_eq!(history[0].worker_id, "worker-1"); - assert_eq!(history[1].worker_id, "worker-2"); - assert!(history.iter().all(|entry| entry.subject_id == subject.id)); - - let update = store - .database - .try_with_connection::<_, SubjektivError>(|connection| { - connection.execute( - "UPDATE subject_session_attributions SET worker_id = 'worker-other' - WHERE session_id = 'session-1'", - [], - )?; - Ok(()) - }); - assert!(update.is_err()); - let delete = store - .database - .try_with_connection::<_, SubjektivError>(|connection| { - connection.execute( - "DELETE FROM subject_session_attributions WHERE session_id = 'session-1'", - [], - )?; - Ok(()) - }); - assert!(delete.is_err()); - - let mut foreign_candidate = - candidate(&other_subject.id, "candidate-foreign", "workspace-a"); - foreign_candidate.source_refs[0].session_id = Some("session-1".into()); - let result = store.stage_candidate_with_attribution( - foreign_candidate, - attribution( - &other_subject.id, - "runtime-1", - "worker-1", - "session-1", - "2026-09-28T12:00:00.000Z", - ), - ); - assert!(matches!( - result, - Err(SubjektivError::SessionSubjectConflict { - session_id, - existing_subject_id, - requested_subject_id, - }) if session_id == "session-1" - && existing_subject_id == subject.id - && requested_subject_id == other_subject.id - )); - assert!( - store - .staging_candidate(&other_subject.id, "candidate-foreign") - .unwrap() - .is_none() - ); - } - - #[test] - fn malformed_nested_provenance_is_rejected_without_staging() { - let temp = tempfile::tempdir().unwrap(); - let (_manager, _workspace, store) = open_store(&temp.path().join("storage"), "workspace-a"); - let subject = store.create_subject(role()).unwrap(); - - let mut non_human_preference = - candidate(&subject.id, "candidate-preference", "workspace-a"); - non_human_preference.kind = CandidateKind::Preference; - non_human_preference.evidence[0] - .origin - .as_mut() - .unwrap() - .kind = EvidenceOriginKind::ModelOutput; - assert!(matches!( - store.stage_candidate(non_human_preference), - Err(SubjektivError::InvalidRecord(message)) - if message.contains("HumanInput") - )); - - let mut reversed = candidate(&subject.id, "candidate-reversed", "workspace-a"); - reversed.evidence[0].entry_range = Some([12, 10]); - assert!(matches!( - store.stage_candidate(reversed), - Err(SubjektivError::InvalidRecord(_)) - )); - - let mut duplicate = candidate(&subject.id, "candidate-duplicate", "workspace-a"); - duplicate.evidence.push(duplicate.evidence[0].clone()); - assert!(matches!( - store.stage_candidate(duplicate), - Err(SubjektivError::InvalidRecord(_)) - )); - - let mut dangling = candidate(&subject.id, "candidate-dangling", "workspace-a"); - dangling.source_refs[0].evidence_id = Some("missing-evidence".into()); - assert!(matches!( - store.stage_candidate(dangling), - Err(SubjektivError::InvalidRecord(_)) - )); - assert!( - store - .staging_candidate(&subject.id, "candidate-dangling") - .unwrap() - .is_none() - ); - } - - #[test] - fn revision_proposal_roundtrips_through_staging_and_resolution() { - let temp = tempfile::tempdir().unwrap(); - let (_manager, _workspace, store) = open_store(&temp.path().join("storage"), "workspace-a"); - let subject = store.create_subject(role()).unwrap(); - let memory = store - .create_memory(&subject.id, draft("Existing memory", "initial observation")) - .unwrap(); - let proposal = proposal( - RevisionProposalIntent::Revise, - &memory.id, - 1, - "Refine the wording from new evidence", - ); - let staged = store - .stage_candidate( - candidate(&subject.id, "candidate-proposed", "workspace-a") - .with_revision_proposal(proposal.clone()) - .unwrap(), - ) - .unwrap(); - assert_eq!(staged.revision_proposal, Some(proposal.clone())); - assert_eq!( - store - .staging_candidate(&subject.id, &staged.id) - .unwrap() - .unwrap() - .revision_proposal, - Some(proposal.clone()) - ); - - let resolution = store - .resolve_candidate( - &subject.id, - &staged.id, - StagingResolutionAction::Discarded, - "The current wording remains preferable", - vec![], - ) - .unwrap(); - assert_eq!( - resolution.candidate.revision_proposal, - Some(proposal.clone()) - ); - assert_eq!( - store - .staging_resolution(&subject.id, &staged.id) - .unwrap() - .unwrap() - .candidate - .revision_proposal, - Some(proposal) - ); - } - - #[test] - fn atomic_candidate_decision_is_idempotent_and_preserves_proposal_metadata() { - let temp = tempfile::tempdir().unwrap(); - let (_manager, _workspace, store) = open_store(&temp.path().join("storage"), "workspace-a"); - let subject = store.create_subject(role()).unwrap(); - let memory = store - .create_memory(&subject.id, draft("Original", "initial observation")) - .unwrap(); - let change_reason = "The remembered condition has ended"; - let staged = store - .stage_candidate( - candidate(&subject.id, "candidate-resolve-atomic", "workspace-a") - .with_revision_proposal(proposal( - RevisionProposalIntent::Resolve, - &memory.id, - memory.revision, - change_reason, - )) - .unwrap(), - ) - .unwrap(); - let mut revised = draft("Resolved", change_reason); - revised.state = MemoryState::Resolved; - let request = CandidateDecisionRequest { - request_id: "decision-resolve-1".into(), - candidate_id: staged.id.clone(), - reason: "Accepted exact resolution proposal".into(), - decision: CandidateDecision::Apply { - target: MemoryRevisionTarget::Revise { - memory_id: memory.id.clone(), - expected_revision: memory.revision, - }, - draft: revised, - }, - }; - - let first = store - .decide_candidate(&subject.id, request.clone()) - .unwrap(); - let retried = store.decide_candidate(&subject.id, request).unwrap(); - - assert_eq!(first.memory.as_ref().unwrap().revision, 2); - assert_eq!(retried.memory.as_ref().unwrap().revision, 2); - assert_eq!(first.resolution.id, retried.resolution.id); - assert_eq!(first.store_revision, 2); - assert!(first.surface_dirty); - assert_eq!( - first - .resolution - .candidate - .revision_proposal - .unwrap() - .change_reason, - change_reason - ); - assert_eq!( - store - .list_memory_revisions(&subject.id, &memory.id) - .unwrap() - .len(), - 2 - ); - } - - #[test] - fn candidate_decision_revalidates_stale_proposals_and_intent_transitions() { - let temp = tempfile::tempdir().unwrap(); - let (_manager, _workspace, store) = open_store(&temp.path().join("storage"), "workspace-a"); - let subject = store.create_subject(role()).unwrap(); - let memory = store - .create_memory(&subject.id, draft("Lifecycle", "initial")) - .unwrap(); - let stale = store - .stage_candidate( - candidate(&subject.id, "candidate-stale-apply", "workspace-a") - .with_revision_proposal(proposal( - RevisionProposalIntent::Revise, - &memory.id, - 1, - "stale correction", - )) - .unwrap(), - ) - .unwrap(); - store - .revise_memory(&subject.id, &memory.id, 1, draft("Advanced", "advance")) - .unwrap(); - let stale_result = store.decide_candidate( - &subject.id, - CandidateDecisionRequest { - request_id: "decision-stale".into(), - candidate_id: stale.id.clone(), - reason: "must not auto-rebase".into(), - decision: CandidateDecision::Apply { - target: MemoryRevisionTarget::Revise { - memory_id: memory.id.clone(), - expected_revision: 1, - }, - draft: draft("Stale", "stale correction"), - }, - }, - ); - assert!(matches!( - stale_result, - Err(SubjektivError::RevisionConflict { - expected: 1, - actual: 2, - .. - }) - )); - assert!( - store - .staging_resolution(&subject.id, &stale.id) - .unwrap() - .is_none() - ); - - let apply_intent = |store: &SubjektivStore, - candidate_id: &str, - intent: RevisionProposalIntent, - expected_revision: u64, - state: MemoryState, - reason: &str| { - let staged = store - .stage_candidate( - candidate(&subject.id, candidate_id, "workspace-a") - .with_revision_proposal(proposal( - intent, - &memory.id, - expected_revision, - reason, - )) - .unwrap(), - ) - .unwrap(); - let mut next = draft(candidate_id, reason); - next.state = state; - store - .decide_candidate( - &subject.id, - CandidateDecisionRequest { - request_id: format!("decision-{candidate_id}"), - candidate_id: staged.id, - reason: format!("apply {candidate_id}"), - decision: CandidateDecision::Apply { - target: MemoryRevisionTarget::Revise { - memory_id: memory.id.clone(), - expected_revision, - }, - draft: next, - }, - }, - ) - .unwrap() - .memory - .unwrap() - }; - let resolved = apply_intent( - &store, - "resolve", - RevisionProposalIntent::Resolve, - 2, - MemoryState::Resolved, - "resolve reason", - ); - let reopened = apply_intent( - &store, - "reopen", - RevisionProposalIntent::Reopen, - resolved.revision, - MemoryState::Active, - "reopen reason", - ); - let retracted = apply_intent( - &store, - "retract", - RevisionProposalIntent::Retract, - reopened.revision, - MemoryState::Retracted, - "retract reason", - ); - assert_eq!(retracted.state, MemoryState::Retracted); - assert!(matches!( - store.stage_candidate( - candidate(&subject.id, "post-retract", "workspace-a") - .with_revision_proposal(proposal( - RevisionProposalIntent::Reopen, - &memory.id, - retracted.revision, - "cannot revive", - )) - .unwrap() - ), - Err(SubjektivError::InvalidStateTransition { .. }) - )); - } - - #[test] - fn candidate_decisions_reject_conflicts_and_roll_back_partial_writes() { - let temp = tempfile::tempdir().unwrap(); - let (_manager, _workspace, store) = open_store(&temp.path().join("storage"), "workspace-a"); - let subject = store.create_subject(role()).unwrap(); - let other = store.create_subject(role()).unwrap(); - let staged = store - .stage_candidate(candidate(&subject.id, "candidate-rollback", "workspace-a")) - .unwrap(); - let foreign = store - .stage_candidate(candidate(&other.id, "candidate-foreign", "workspace-a")) - .unwrap(); - let mut invalid = draft("Would be written", "must roll back"); - invalid.derived_from.push(MemoryRevisionRef { - memory_id: "missing-memory".into(), - revision: 1, - }); - let failed = store.decide_candidate( - &subject.id, - CandidateDecisionRequest { - request_id: "decision-rollback".into(), - candidate_id: staged.id.clone(), - reason: "invalid derivation".into(), - decision: CandidateDecision::Apply { - target: MemoryRevisionTarget::Create, - draft: invalid, - }, - }, - ); - assert!(matches!( - failed, - Err(SubjektivError::SubjectScopeMismatch { .. }) - )); - assert!( - store - .staging_resolution(&subject.id, &staged.id) - .unwrap() - .is_none() - ); - assert!(store.list_memories(&subject.id).unwrap().is_empty()); - - let cross_subject = store.decide_candidate( - &subject.id, - CandidateDecisionRequest { - request_id: "decision-foreign".into(), - candidate_id: foreign.id, - reason: "must remain scoped".into(), - decision: CandidateDecision::Close { - action: StagingResolutionAction::Invalid, - affected_memory: vec![], - }, - }, - ); - assert!(matches!( - cross_subject, - Err(SubjektivError::SubjectScopeMismatch { .. }) - )); - - let closed = store - .decide_candidate( - &subject.id, - CandidateDecisionRequest { - request_id: "decision-close".into(), - candidate_id: staged.id.clone(), - reason: "already represented elsewhere".into(), - decision: CandidateDecision::Close { - action: StagingResolutionAction::AlreadyCovered, - affected_memory: vec![], - }, - }, - ) - .unwrap(); - assert!(!closed.surface_dirty); - assert!(closed.memory.is_none()); - for (suffix, action) in [ - ("discarded", StagingResolutionAction::Discarded), - ("invalid", StagingResolutionAction::Invalid), - ("duplicate", StagingResolutionAction::Duplicate), - ] { - let candidate_id = format!("candidate-{suffix}"); - store - .stage_candidate(candidate(&subject.id, &candidate_id, "workspace-a")) - .unwrap(); - let receipt = store - .decide_candidate( - &subject.id, - CandidateDecisionRequest { - request_id: format!("decision-{suffix}"), - candidate_id, - reason: format!("specific {suffix} reason"), - decision: CandidateDecision::Close { - action, - affected_memory: vec![], - }, - }, - ) - .unwrap(); - assert_eq!(receipt.resolution.action, action); - assert!(!receipt.surface_dirty); - } - let conflicting_retry = store.decide_candidate( - &subject.id, - CandidateDecisionRequest { - request_id: "decision-close".into(), - candidate_id: staged.id, - reason: "different disposition".into(), - decision: CandidateDecision::Close { - action: StagingResolutionAction::Discarded, - affected_memory: vec![], - }, - }, - ); - assert!(matches!( - conflicting_retry, - Err(SubjektivError::DecisionRequestConflict(_)) - )); - } - - #[test] - fn revision_proposals_enforce_scope_revision_and_state_transitions() { - let temp = tempfile::tempdir().unwrap(); - let (_manager, _workspace, store) = open_store(&temp.path().join("storage"), "workspace-a"); - let subject = store.create_subject(role()).unwrap(); - let other_subject = store.create_subject(role()).unwrap(); - let memory = store - .create_memory( - &subject.id, - draft("Transition target", "initial observation"), - ) - .unwrap(); - - let foreign = candidate(&other_subject.id, "candidate-foreign-target", "workspace-a") - .with_revision_proposal(proposal( - RevisionProposalIntent::Revise, - &memory.id, - 1, - "Must remain subject scoped", - )) - .unwrap(); - assert!(matches!( - store.stage_candidate(foreign), - Err(SubjektivError::SubjectScopeMismatch { - subject_id, - reference, - }) if subject_id == other_subject.id && reference == memory.id - )); - - let stale = candidate(&subject.id, "candidate-stale", "workspace-a") - .with_revision_proposal(proposal( - RevisionProposalIntent::Resolve, - &memory.id, - 2, - "Resolve the active memory", - )) - .unwrap(); - assert!(matches!( - store.stage_candidate(stale), - Err(SubjektivError::RevisionConflict { - memory_id, - expected: 2, - actual: 1, - }) if memory_id == memory.id - )); - - let resolve_proposal = candidate(&subject.id, "candidate-resolve", "workspace-a") - .with_revision_proposal(proposal( - RevisionProposalIntent::Resolve, - &memory.id, - 1, - "The active condition has ended", - )) - .unwrap(); - assert!(store.stage_candidate(resolve_proposal).is_ok()); - let invalid_reopen = candidate(&subject.id, "candidate-invalid-reopen", "workspace-a") - .with_revision_proposal(proposal( - RevisionProposalIntent::Reopen, - &memory.id, - 1, - "An active Memory cannot be reopened", - )) - .unwrap(); - assert!(matches!( - store.stage_candidate(invalid_reopen), - Err(SubjektivError::InvalidStateTransition { - from: MemoryState::Active, - to: MemoryState::Active, - .. - }) - )); - - let mut resolve = draft("Resolved target", "the condition ended"); - resolve.state = MemoryState::Resolved; - let resolved = store - .revise_memory(&subject.id, &memory.id, 1, resolve) - .unwrap(); - let mut attributed_stale = - candidate(&subject.id, "candidate-attributed-stale", "workspace-a"); - attributed_stale.source_refs[0].session_id = None; - attributed_stale - .attach_revision_proposal(proposal( - RevisionProposalIntent::Revise, - &memory.id, - 1, - "This attribution must roll back with the stale proposal", - )) - .unwrap(); - let stale_attribution = attribution( - &subject.id, - "runtime-proposal", - "worker-proposal", - "session-proposal-stale", - "2026-09-28T12:00:00.000Z", - ); - store - .record_session_attribution(stale_attribution.clone()) - .unwrap(); - assert!(matches!( - store.stage_candidate_with_attribution(attributed_stale, stale_attribution,), - Err(SubjektivError::RevisionConflict { - expected: 1, - actual: 2, - .. - }) - )); - assert!( - store - .session_attribution("session-proposal-stale") - .unwrap() - .is_some() - ); - let reopen = candidate(&subject.id, "candidate-reopen", "workspace-a") - .with_revision_proposal(proposal( - RevisionProposalIntent::Reopen, - &memory.id, - resolved.revision, - "The condition applies again", - )) - .unwrap(); - assert_eq!( - store - .stage_candidate(reopen) - .unwrap() - .revision_proposal - .unwrap() - .intent, - RevisionProposalIntent::Reopen - ); - let retract_proposal = candidate(&subject.id, "candidate-retract", "workspace-a") - .with_revision_proposal(proposal( - RevisionProposalIntent::Retract, - &memory.id, - resolved.revision, - "The supporting evidence was invalidated", - )) - .unwrap(); - assert!(store.stage_candidate(retract_proposal).is_ok()); - - let mut retract = draft("Retracted target", "the evidence was invalidated"); - retract.state = MemoryState::Retracted; - let retracted = store - .revise_memory(&subject.id, &memory.id, resolved.revision, retract) - .unwrap(); - let terminal = candidate(&subject.id, "candidate-after-retract", "workspace-a") - .with_revision_proposal(proposal( - RevisionProposalIntent::Revise, - &memory.id, - retracted.revision, - "Retraction must remain terminal", - )) - .unwrap(); - assert!(matches!( - store.stage_candidate(terminal), - Err(SubjektivError::InvalidStateTransition { - memory_id, - from: MemoryState::Retracted, - to: MemoryState::Retracted, - }) if memory_id == memory.id - )); - } - - #[test] - fn exact_proposal_retry_remains_idempotent_after_target_advances() { - let temp = tempfile::tempdir().unwrap(); - let (_manager, _workspace, store) = open_store(&temp.path().join("storage"), "workspace-a"); - let subject = store.create_subject(role()).unwrap(); - let memory = store - .create_memory(&subject.id, draft("Retry target", "initial observation")) - .unwrap(); - let staged = store - .stage_candidate( - candidate(&subject.id, "candidate-retry-proposal", "workspace-a") - .with_revision_proposal(proposal( - RevisionProposalIntent::Revise, - &memory.id, - 1, - "Refine the current revision", - )) - .unwrap(), - ) - .unwrap(); - store - .revise_memory( - &subject.id, - &memory.id, - 1, - draft("Advanced target", "another accepted refinement"), - ) - .unwrap(); - - let mut retry = staged.clone(); - retry.created_at = "2026-09-28T11:00:00.000Z".into(); - let retried = store.stage_candidate(retry).unwrap(); - assert_eq!(retried.created_at, staged.created_at); - assert_eq!(retried.revision_proposal, staged.revision_proposal); - } - - #[test] - fn bounded_memory_reads_are_scoped_ordered_and_immutable() { - let temp = tempfile::tempdir().unwrap(); - let (_manager, _workspace, store) = open_store(&temp.path().join("storage"), "workspace-a"); - let subject = store.create_subject(role()).unwrap(); - let other_subject = store.create_subject(role()).unwrap(); - assert!(matches!( - store.list_memories("subject-missing"), - Err(SubjektivError::SubjectNotFound(subject_id)) if subject_id == "subject-missing" - )); - - let mut first_draft = draft("First memory", "first reason"); - first_draft.body_md = "Immutable first body".into(); - let first = store.create_memory(&subject.id, first_draft).unwrap(); - thread::sleep(Duration::from_millis(2)); - let second = store - .create_memory(&subject.id, draft("Second memory", "second reason")) - .unwrap(); - let memories = store.list_memories(&subject.id).unwrap(); - assert_eq!( - memories.iter().map(|record| &record.id).collect::>(), - vec![&second.id, &first.id] - ); - - thread::sleep(Duration::from_millis(2)); - let mut revision = draft("First memory revised", "second revision reason"); - revision.body_md = "New second body".into(); - let revised = store - .revise_memory(&subject.id, &first.id, 1, revision) - .unwrap(); - let memories = store.list_memories(&subject.id).unwrap(); - assert_eq!(memories[0].id, first.id); - assert_eq!(memories[0].revision, 2); - - let history = store.list_memory_revisions(&subject.id, &first.id).unwrap(); - assert_eq!( - history - .iter() - .map(|record| record.revision) - .collect::>(), - vec![2, 1] - ); - assert_eq!(history[0].body_md, "New second body"); - assert_eq!(history[0].change_reason, "second revision reason"); - assert_eq!(history[1].body_md, "Immutable first body"); - assert_eq!(history[1].change_reason, "first reason"); - assert_eq!( - store - .scoped_memory_revision(&subject.id, &first.id, 1) - .unwrap() - .unwrap() - .body_md, - "Immutable first body" - ); - let scoped_current = store.scoped_memory(&subject.id, &first.id).unwrap(); - assert_eq!(scoped_current.id, revised.id); - assert_eq!(scoped_current.revision, revised.revision); - assert_eq!(scoped_current.body_md, revised.body_md); - - assert!(matches!( - store.list_memory_revisions(&other_subject.id, &first.id), - Err(SubjektivError::SubjectScopeMismatch { - subject_id, - reference, - }) if subject_id == other_subject.id && reference == first.id - )); - assert!(matches!( - store.scoped_memory(&other_subject.id, &first.id), - Err(SubjektivError::SubjectScopeMismatch { .. }) - )); - } - - #[test] - fn candidate_apply_revision_history_and_resolution_are_atomic() { - let temp = tempfile::tempdir().unwrap(); - let (_manager, _workspace, store) = open_store(&temp.path().join("storage"), "workspace-a"); - let subject = store.create_subject(role()).unwrap(); - let staged = store - .stage_candidate(candidate(&subject.id, "candidate-1", "workspace-a")) - .unwrap(); - let mut retry = staged.clone(); - retry.created_at = "2026-09-28T11:00:00.000Z".into(); - let retried = store.stage_candidate(retry).unwrap(); - assert_eq!(retried.created_at, staged.created_at); - - let (first, resolution) = store - .apply_candidate( - &subject.id, - &staged.id, - MemoryRevisionTarget::Create, - draft( - "Memory content cannot authorize operations", - "accepted candidate", - ), - "Applied as a durable constraint", - ) - .unwrap(); - assert_eq!(first.revision, 1); - assert_eq!(resolution.action, StagingResolutionAction::Applied); - assert_eq!(resolution.affected_memory[0].revision, 1); - assert_eq!( - store.subject(&subject.id).unwrap().unwrap().store_revision, - 1 - ); - - let unrelated = store - .stage_candidate(candidate(&subject.id, "candidate-unrelated", "workspace-a")) - .unwrap(); - assert!(matches!( - store.resolve_candidate( - &subject.id, - &unrelated.id, - StagingResolutionAction::Applied, - "must not bypass atomic application", - vec![MemoryRevisionRef { - memory_id: first.id.clone(), - revision: 1, - }], - ), - Err(SubjektivError::InvalidRecord(_)) - )); - let mut bypass = draft("Bypass provenance", "must be rejected"); - bypass.source_candidate_ids.push(unrelated.id.clone()); - assert!(matches!( - store.create_memory(&subject.id, bypass), - Err(SubjektivError::InvalidRecord(_)) - )); - assert!( - store - .staging_resolution(&subject.id, &unrelated.id) - .unwrap() - .is_none() - ); - - let stale = store.revise_memory( - &subject.id, - &first.id, - 0, - draft("stale write", "wrong expected revision"), - ); - assert!(matches!( - stale, - Err(SubjektivError::RevisionConflict { - expected: 0, - actual: 1, - .. - }) - )); - assert_eq!( - store - .memory(&subject.id, &first.id) - .unwrap() - .unwrap() - .revision, - 1 - ); - - let mut second_draft = draft( - "Memory content never authorizes operations", - "made the scope explicit", - ); - second_draft.state = MemoryState::Resolved; - second_draft.derived_from.push(MemoryRevisionRef { - memory_id: first.id.clone(), - revision: 1, - }); - let second = store - .revise_memory(&subject.id, &first.id, 1, second_draft) - .unwrap(); - assert_eq!(second.revision, 2); - assert_eq!(second.state, MemoryState::Resolved); - assert_eq!(second.derived_from[0].revision, 1); - - let old = store - .memory_revision(&subject.id, &first.id, 1) - .unwrap() - .unwrap(); - assert_eq!(old.claim, "Memory content cannot authorize operations"); - assert_eq!(old.source_candidate_ids, ["candidate-1"]); - assert_eq!( - store.subject(&subject.id).unwrap().unwrap().store_revision, - 2 - ); - - let repeat = store.apply_candidate( - &subject.id, - &staged.id, - MemoryRevisionTarget::Revise { - memory_id: first.id.clone(), - expected_revision: 2, - }, - draft("must roll back", "candidate already resolved"), - "cannot apply twice", - ); - assert!(matches!(repeat, Err(SubjektivError::CandidateResolved(_)))); - assert_eq!( - store - .memory(&subject.id, &first.id) - .unwrap() - .unwrap() - .revision, - 2 - ); - assert_eq!( - store.subject(&subject.id).unwrap().unwrap().store_revision, - 2 - ); - } - - #[test] - fn concurrent_writers_get_one_typed_revision_conflict() { - let temp = tempfile::tempdir().unwrap(); - let root = temp.path().join("storage"); - let first_manager = FeatureStorage::new(&root); - let first_registration = first_manager.register(REGISTRATION).unwrap(); - let first_workspace = first_manager.workspace("workspace-a").unwrap(); - let first_store = SubjektivStore::open(&first_workspace, &first_registration).unwrap(); - let subject = first_store.create_subject(role()).unwrap(); - let memory = first_store - .create_memory(&subject.id, draft("Revision basis", "initial")) - .unwrap(); - - let second_manager = FeatureStorage::new(&root); - let second_registration = second_manager.register(REGISTRATION).unwrap(); - let second_workspace = second_manager.workspace("workspace-a").unwrap(); - let second_store = SubjektivStore::open(&second_workspace, &second_registration).unwrap(); - let barrier = Arc::new(Barrier::new(3)); - let revise = |store: SubjektivStore, claim: &'static str, barrier: Arc| { - let subject_id = subject.id.clone(); - let memory_id = memory.id.clone(); - thread::spawn(move || { - barrier.wait(); - store.revise_memory( - &subject_id, - &memory_id, - 1, - draft(claim, "concurrent refinement"), - ) - }) - }; - let first = revise(first_store.clone(), "first writer", barrier.clone()); - let second = revise(second_store, "second writer", barrier.clone()); - barrier.wait(); - let results = [first.join().unwrap(), second.join().unwrap()]; - assert_eq!(results.iter().filter(|result| result.is_ok()).count(), 1); - assert_eq!( - results - .iter() - .filter(|result| matches!(result, Err(SubjektivError::RevisionConflict { .. }))) - .count(), - 1 - ); - assert_eq!( - first_store - .subject(&subject.id) - .unwrap() - .unwrap() - .store_revision, - 2 - ); - } - - #[test] - fn scope_and_fixed_revision_references_are_enforced() { - let temp = tempfile::tempdir().unwrap(); - let manager = FeatureStorage::new(temp.path().join("storage")); - let registration = manager.register(REGISTRATION).unwrap(); - let workspace_a = manager.workspace("workspace-a").unwrap(); - let workspace_b = manager.workspace("workspace-b").unwrap(); - let store_a = SubjektivStore::open(&workspace_a, ®istration).unwrap(); - let store_b = SubjektivStore::open(&workspace_b, ®istration).unwrap(); - let subject_a = store_a.create_subject(role()).unwrap(); - let other_subject_a = store_a.create_subject(role()).unwrap(); - let subject_b = store_b.create_subject(role()).unwrap(); - let memory_a = store_a - .create_memory( - &subject_a.id, - draft("Workspace A experience", "direct host record"), - ) - .unwrap(); - - assert!(matches!( - store_b.create_memory(&subject_a.id, draft("foreign subject", "must be rejected")), - Err(SubjektivError::SubjectNotFound(_)) - )); - let mut foreign_derived = draft("Workspace B experience", "invalid derivation"); - foreign_derived.derived_from.push(MemoryRevisionRef { - memory_id: memory_a.id.clone(), - revision: 1, - }); - assert!(matches!( - store_b.create_memory(&subject_b.id, foreign_derived), - Err(SubjektivError::SubjectScopeMismatch { .. }) - )); - assert_eq!( - store_b - .subject(&subject_b.id) - .unwrap() - .unwrap() - .store_revision, - 0 - ); - - let mut other_subject_derived = draft("Same Workspace, other subject", "invalid scope"); - other_subject_derived.derived_from.push(MemoryRevisionRef { - memory_id: memory_a.id.clone(), - revision: 1, - }); - assert!(matches!( - store_a.create_memory(&other_subject_a.id, other_subject_derived), - Err(SubjektivError::SubjectScopeMismatch { .. }) - )); - let mut wrong_revision = draft("Missing fixed revision", "invalid revision"); - wrong_revision.derived_from.push(MemoryRevisionRef { - memory_id: memory_a.id.clone(), - revision: 2, - }); - assert!(matches!( - store_a.create_memory(&subject_a.id, wrong_revision), - Err(SubjektivError::SubjectScopeMismatch { .. }) - )); - assert_eq!( - store_a - .subject(&other_subject_a.id) - .unwrap() - .unwrap() - .store_revision, - 0 - ); - - let foreign_origin = candidate(&subject_a.id, "candidate-foreign", "workspace-b"); - assert!(matches!( - store_a.stage_candidate(foreign_origin), - Err(SubjektivError::SubjectScopeMismatch { .. }) - )); - } - - #[test] - fn memory_states_snapshots_and_retirement_have_explicit_boundaries() { - let temp = tempfile::tempdir().unwrap(); - let (_manager, _workspace, store) = open_store(&temp.path().join("storage"), "workspace-a"); - let subject = store.create_subject(role()).unwrap(); - let first = store - .create_memory(&subject.id, draft("A constraint", "initial observation")) - .unwrap(); - let snapshot = store - .create_surface_snapshot( - &subject.id, - "Bounded resident summary", - vec![MemoryRevisionRef { - memory_id: first.id.clone(), - revision: 1, - }], - 1, - ) - .unwrap(); - assert_eq!( - store - .surface_snapshot(&subject.id, &snapshot.id) - .unwrap() - .unwrap() - .memory_refs[0] - .revision, - 1 - ); - assert!( - store - .create_surface_snapshot(&subject.id, "stale", vec![], 0) - .is_err() - ); - - let mut retract = draft("Retracted constraint", "evidence invalidated it"); - retract.state = MemoryState::Retracted; - let retracted = store - .revise_memory(&subject.id, &first.id, 1, retract) - .unwrap(); - assert_eq!(retracted.state, MemoryState::Retracted); - assert!(matches!( - store.revise_memory( - &subject.id, - &first.id, - 2, - draft("cannot reactivate", "retraction is terminal") - ), - Err(SubjektivError::InvalidStateTransition { .. }) - )); - let mut retract_again = draft("still retracted", "no post-retraction revisions"); - retract_again.state = MemoryState::Retracted; - assert!(matches!( - store.revise_memory(&subject.id, &first.id, 2, retract_again), - Err(SubjektivError::InvalidStateTransition { .. }) - )); - - let retired = store.retire_subject(&subject.id).unwrap(); - assert_eq!(retired.state, SubjectState::Retired); - assert!(matches!( - store.create_memory(&subject.id, draft("after retirement", "invalid")), - Err(SubjektivError::SubjectRetired(_)) - )); - assert!(store.memory(&subject.id, &first.id).unwrap().is_some()); - } - - #[test] - fn history_rows_reject_update_and_delete() { - let temp = tempfile::tempdir().unwrap(); - let (_manager, _workspace, store) = open_store(&temp.path().join("storage"), "workspace-a"); - let subject = store.create_subject(role()).unwrap(); - let staged = store - .stage_candidate(candidate(&subject.id, "candidate-1", "workspace-a")) - .unwrap(); - let (memory, _) = store - .apply_candidate( - &subject.id, - &staged.id, - MemoryRevisionTarget::Create, - draft("Immutable history", "accept"), - "applied", - ) - .unwrap(); - let extra_candidate = store - .stage_candidate(candidate(&subject.id, "candidate-extra", "workspace-a")) - .unwrap(); - let other_memory = store - .create_memory(&subject.id, draft("Other memory", "snapshot seal target")) - .unwrap(); - let snapshot = store - .create_surface_snapshot( - &subject.id, - "sealed snapshot", - vec![MemoryRevisionRef { - memory_id: memory.id.clone(), - revision: 1, - }], - 2, - ) - .unwrap(); - - let update = store - .database - .try_with_connection::<_, SubjektivError>(|connection| { - connection.execute( - "UPDATE memory_revisions SET record_json = '{}' \ - WHERE subject_id = ?1 AND memory_id = ?2 AND revision = 1", - params![subject.id, memory.id], - )?; - Ok(()) - }); - assert!(update.is_err()); - let delete = store - .database - .try_with_connection::<_, SubjektivError>(|connection| { - connection.execute( - "DELETE FROM staging_records WHERE subject_id = ?1 AND candidate_id = ?2", - params![subject.id, staged.id], - )?; - Ok(()) - }); - assert!(delete.is_err()); - - let late_evidence = store - .database - .try_with_connection::<_, SubjektivError>(|connection| { - connection.execute( - "INSERT INTO memory_revision_candidates ( - subject_id, memory_id, revision, candidate_id - ) VALUES (?1, ?2, 1, ?3)", - params![subject.id, memory.id, extra_candidate.id], - )?; - Ok(()) - }); - assert!(late_evidence.is_err()); - let late_derivation = - store - .database - .try_with_connection::<_, SubjektivError>(|connection| { - connection.execute( - "INSERT INTO memory_revision_derivations ( - subject_id, memory_id, revision, source_memory_id, source_revision - ) VALUES (?1, ?2, 1, ?3, 1)", - params![subject.id, memory.id, other_memory.id], - )?; - Ok(()) - }); - assert!(late_derivation.is_err()); - let late_resolution_target = - store - .database - .try_with_connection::<_, SubjektivError>(|connection| { - connection.execute( - "INSERT INTO staging_resolution_targets ( - subject_id, candidate_id, memory_id, revision - ) VALUES (?1, ?2, ?3, 1)", - params![subject.id, staged.id, other_memory.id], - )?; - Ok(()) - }); - assert!(late_resolution_target.is_err()); - let late_snapshot_ref = - store - .database - .try_with_connection::<_, SubjektivError>(|connection| { - connection.execute( - "INSERT INTO surface_snapshot_refs ( - subject_id, snapshot_id, memory_id, revision - ) VALUES (?1, ?2, ?3, 1)", - params![subject.id, snapshot.id, other_memory.id], - )?; - Ok(()) - }); - assert!(late_snapshot_ref.is_err()); - - assert!( - store - .memory_revision(&subject.id, &memory.id, 1) - .unwrap() - .is_some() - ); - assert!( - store - .staging_candidate(&subject.id, &staged.id) - .unwrap() - .is_some() - ); - } - - #[test] - fn surface_material_selection_is_scoped_active_bounded_and_deterministic() { - let temp = tempfile::tempdir().unwrap(); - let (_manager, _workspace, store) = open_store(temp.path(), "workspace-a"); - let subject = store.create_subject(role()).unwrap(); - let other = store.create_subject(role()).unwrap(); - store - .create_memory(&other.id, draft("foreign", "foreign subject")) - .unwrap(); - - let kinds = [ - CandidateKind::Preference, - CandidateKind::Constraint, - CandidateKind::Decision, - CandidateKind::WorkingAssumption, - CandidateKind::OpenQuestion, - CandidateKind::Lesson, - ]; - let mut first_id = None; - for kind in &kinds { - for index in 0..9 { - let mut value = draft(&format!("{kind:?}-{index}"), "selection fixture"); - value.kind = kind.clone(); - let memory = store.create_memory(&subject.id, value).unwrap(); - first_id.get_or_insert(memory.id); - } - } - let resolved_id = first_id.unwrap(); - let current = store.memory(&subject.id, &resolved_id).unwrap().unwrap(); - let mut resolved = draft("resolved", "no longer active"); - resolved.kind = current.kind; - resolved.state = MemoryState::Resolved; - store - .revise_memory(&subject.id, &resolved_id, current.revision, resolved) - .unwrap(); - - let first = store.prepare_surface_generation(&subject.id).unwrap(); - let second = store.prepare_surface_generation(&subject.id).unwrap(); - assert_eq!(first.materials, second.materials); - assert_eq!(first.active_memory_count, 53); - assert_eq!(first.materials.len(), SURFACE_TOTAL_MATERIAL_LIMIT); - assert!( - first - .materials - .iter() - .all(|item| item.memory_id != resolved_id) - ); - assert!(first.materials.iter().all(|item| item.body_md != "foreign")); - for kind in &kinds { - assert!( - first - .materials - .iter() - .filter(|item| &item.kind == kind) - .count() - <= SURFACE_PER_KIND_LIMIT - ); - } - let serialized = serde_json::to_vec(&first.materials).unwrap(); - assert!(estimated_tokens_for_bytes(serialized.len()) <= SURFACE_MATERIAL_TOKEN_BUDGET); - } - - #[test] - fn oversized_kind_cannot_starve_other_surface_categories() { - let temp = tempfile::tempdir().unwrap(); - let (_manager, _workspace, store) = open_store(temp.path(), "workspace-a"); - let subject = store.create_subject(role()).unwrap(); - - let mut oversized = draft("oversized preference", "category fairness fixture"); - oversized.kind = CandidateKind::Preference; - oversized.body_md = "x".repeat((SURFACE_MATERIAL_TOKEN_BUDGET - 100) * 4); - store.create_memory(&subject.id, oversized).unwrap(); - - for kind in [ - CandidateKind::Constraint, - CandidateKind::Decision, - CandidateKind::WorkingAssumption, - CandidateKind::OpenQuestion, - CandidateKind::Lesson, - ] { - let mut value = draft(&format!("{kind:?}"), "category fairness fixture"); - value.kind = kind; - store.create_memory(&subject.id, value).unwrap(); - } - - let generation = store.prepare_surface_generation(&subject.id).unwrap(); - for kind in [ - CandidateKind::Constraint, - CandidateKind::Decision, - CandidateKind::WorkingAssumption, - CandidateKind::OpenQuestion, - CandidateKind::Lesson, - ] { - assert!( - generation.materials.iter().any(|item| item.kind == kind), - "one large category must not exclude {kind:?}" - ); - } - assert!( - estimated_tokens_for_bytes(serde_json::to_vec(&generation.materials).unwrap().len()) - <= SURFACE_MATERIAL_TOKEN_BUDGET - ); - } - - #[test] - fn surface_material_selection_reclaims_unused_category_budget() { - let temp = tempfile::tempdir().unwrap(); - let (_manager, _workspace, store) = open_store(temp.path(), "workspace-a"); - let subject = store.create_subject(role()).unwrap(); - - let mut small = draft("small preference", "soft category budget fixture"); - small.kind = CandidateKind::Preference; - store.create_memory(&subject.id, small).unwrap(); - let mut large = draft("large constraint", "soft category budget fixture"); - large.kind = CandidateKind::Constraint; - large.body_md = "x".repeat(22_000); - store.create_memory(&subject.id, large).unwrap(); - - let generation = store.prepare_surface_generation(&subject.id).unwrap(); - assert_eq!(generation.materials.len(), 2); - assert_eq!(generation.materials[0].kind, CandidateKind::Preference); - assert_eq!(generation.materials[1].kind, CandidateKind::Constraint); - assert!( - estimated_tokens_for_bytes(serde_json::to_vec(&generation.materials).unwrap().len()) - <= SURFACE_MATERIAL_TOKEN_BUDGET - ); - } - - #[test] - fn surface_states_validation_races_and_idempotency_fail_closed() { - let temp = tempfile::tempdir().unwrap(); - let (_manager, _workspace, store) = open_store(temp.path(), "workspace-a"); - let subject = store.create_subject(role()).unwrap(); - assert_eq!( - store.resident_surface(&subject.id).unwrap().availability, - SurfaceAvailability::Ungenerated - ); - - let empty_generation = store.prepare_surface_generation(&subject.id).unwrap(); - let empty = store - .publish_surface_generation(&subject.id, &empty_generation.id, Vec::new()) - .unwrap(); - assert!(empty.body_md.is_empty()); - assert_eq!( - store.resident_surface(&subject.id).unwrap().availability, - SurfaceAvailability::Ready - ); - - let memory = store - .create_memory(&subject.id, draft("Current constraint", "confirmed")) - .unwrap(); - assert_eq!( - store.resident_surface(&subject.id).unwrap().availability, - SurfaceAvailability::Stale - ); - let stale_generation = store.prepare_surface_generation(&subject.id).unwrap(); - let invalid = store.publish_surface_generation( - &subject.id, - &stale_generation.id, - vec![SurfacePoint { - body_md: "Invented".into(), - memory_refs: vec![MemoryRevisionRef { - memory_id: "foreign-memory".into(), - revision: 1, - }], - }], - ); - assert!(matches!( - invalid, - Err(SubjektivError::SubjectScopeMismatch { .. }) - )); - store - .fail_surface_generation(&subject.id, &stale_generation.id, "invalid_output") - .unwrap(); - assert_eq!( - store.resident_surface(&subject.id).unwrap().availability, - SurfaceAvailability::Failed - ); - assert_eq!( - store - .memory(&subject.id, &memory.id) - .unwrap() - .unwrap() - .revision, - 1, - "surface failure must not roll back confirmed Memory" - ); - - let raced = store.prepare_surface_generation(&subject.id).unwrap(); - store - .revise_memory( - &subject.id, - &memory.id, - 1, - draft("Corrected constraint", "correction"), - ) - .unwrap(); - assert!(matches!( - store.publish_surface_generation( - &subject.id, - &raced.id, - vec![SurfacePoint { - body_md: "Old wording".into(), - memory_refs: vec![MemoryRevisionRef { - memory_id: memory.id.clone(), - revision: 1, - }], - }], - ), - Err(SubjektivError::SurfaceGenerationConflict(_)) - )); - assert_eq!( - store.resident_surface(&subject.id).unwrap().availability, - SurfaceAvailability::Stale - ); - - let current = store.prepare_surface_generation(&subject.id).unwrap(); - let current_ref = MemoryRevisionRef { - memory_id: memory.id.clone(), - revision: 2, - }; - let points = vec![SurfacePoint { - body_md: "Keep the corrected constraint, including its condition.".into(), - memory_refs: vec![current_ref.clone()], - }]; - let published = store - .publish_surface_generation(&subject.id, ¤t.id, points.clone()) - .unwrap(); - let retry = store - .publish_surface_generation(&subject.id, ¤t.id, points) - .unwrap(); - assert_eq!(published.id, retry.id); - let resident = store.resident_surface(&subject.id).unwrap(); - assert_eq!(resident.availability, SurfaceAvailability::Ready); - assert_eq!(resident.snapshot.unwrap().memory_refs, vec![current_ref]); - } - - #[test] - fn active_memory_that_cannot_fit_is_a_failure_not_a_normal_empty_surface() { - let temp = tempfile::tempdir().unwrap(); - let (_manager, _workspace, store) = open_store(temp.path(), "workspace-a"); - let subject = store.create_subject(role()).unwrap(); - let mut oversized = draft("oversized", "fixture"); - oversized.body_md = "x".repeat((SURFACE_MATERIAL_TOKEN_BUDGET + 1) * 4); - store.create_memory(&subject.id, oversized).unwrap(); - - let generation = store.prepare_surface_generation(&subject.id).unwrap(); - assert_eq!(generation.active_memory_count, 1); - assert!(generation.materials.is_empty()); - assert!( - store - .publish_surface_generation(&subject.id, &generation.id, Vec::new()) - .is_err() - ); - store - .fail_surface_generation(&subject.id, &generation.id, "input_budget_exhausted") - .unwrap(); - assert_eq!( - store.resident_surface(&subject.id).unwrap().availability, - SurfaceAvailability::Failed - ); - } - - #[test] - fn concurrent_surface_generations_cannot_overwrite_one_revision() { - let temp = tempfile::tempdir().unwrap(); - let (_manager, _workspace, store) = open_store(temp.path(), "workspace-a"); - let subject = store.create_subject(role()).unwrap(); - let memory = store - .create_memory(&subject.id, draft("Concurrent constraint", "fixture")) - .unwrap(); - let first = store.prepare_surface_generation(&subject.id).unwrap(); - let second = store.prepare_surface_generation(&subject.id).unwrap(); - let barrier = Arc::new(Barrier::new(2)); - let mut joins = Vec::new(); - for (generation, body) in [(first, "first"), (second, "second")] { - let store = store.clone(); - let subject_id = subject.id.clone(); - let memory_id = memory.id.clone(); - let barrier = Arc::clone(&barrier); - joins.push(thread::spawn(move || { - barrier.wait(); - store.publish_surface_generation( - &subject_id, - &generation.id, - vec![SurfacePoint { - body_md: body.into(), - memory_refs: vec![MemoryRevisionRef { - memory_id, - revision: 1, - }], - }], - ) - })); - } - let results = joins - .into_iter() - .map(|join| join.join().unwrap()) - .collect::>(); - assert_eq!(results.iter().filter(|result| result.is_ok()).count(), 1); - assert_eq!( - results - .iter() - .filter(|result| matches!( - result, - Err(SubjektivError::SurfaceGenerationConflict(_)) - )) - .count(), - 1 - ); - assert_eq!( - store.resident_surface(&subject.id).unwrap().availability, - SurfaceAvailability::Ready - ); - } - - #[test] - fn reopen_and_backup_restore_preserve_exact_history() { - let temp = tempfile::tempdir().unwrap(); - let root = temp.path().join("storage"); - let snapshot_path = temp.path().join("snapshot"); - let (manager, _workspace, store) = open_store(&root, "workspace-a"); - let subject = store.create_subject(role()).unwrap(); - let candidate = store - .stage_candidate(candidate(&subject.id, "candidate-1", "workspace-a")) - .unwrap(); - let (memory, _) = store - .apply_candidate( - &subject.id, - &candidate.id, - MemoryRevisionTarget::Create, - draft("Persist this revision", "accepted before restart"), - "applied", - ) - .unwrap(); - drop(store); - manager.shutdown().unwrap(); - drop(manager); - - let (manager, workspace, reopened) = open_store(&root, "workspace-a"); - assert_eq!( - reopened - .memory_revision(&subject.id, &memory.id, 1) - .unwrap() - .unwrap() - .claim, - "Persist this revision" - ); - assert_eq!( - reopened - .staging_resolution(&subject.id, &candidate.id) - .unwrap() - .unwrap() - .candidate - .source_refs[0] - .session_id - .as_deref(), - Some("session-1") - ); - let surface = reopened - .create_surface_snapshot( - &subject.id, - "Persisted resident surface", - vec![MemoryRevisionRef { - memory_id: memory.id.clone(), - revision: 1, - }], - 1, - ) - .unwrap(); - workspace.backup(&snapshot_path).unwrap(); - let mut post_backup = draft("Post-backup revision", "not in snapshot"); - post_backup.derived_from.push(MemoryRevisionRef { - memory_id: memory.id.clone(), - revision: 1, - }); - let second = reopened - .revise_memory(&subject.id, &memory.id, 1, post_backup) - .unwrap(); - assert_eq!(second.revision, 2); - workspace.delete().unwrap(); - drop(reopened); - drop(workspace); - drop(manager); - - let manager = FeatureStorage::new(&root); - let workspace = manager.workspace("workspace-a").unwrap(); - workspace.restore(&snapshot_path).unwrap(); - let registration = SubjektivStore::register(&workspace).unwrap(); - let restored = SubjektivStore::open(&workspace, ®istration).unwrap(); - assert_eq!( - restored - .subject(&subject.id) - .unwrap() - .unwrap() - .store_revision, - 1 - ); - assert_eq!( - restored - .memory(&subject.id, &memory.id) - .unwrap() - .unwrap() - .claim, - "Persist this revision" - ); - assert!( - restored - .memory_revision(&subject.id, &memory.id, 2) - .unwrap() - .is_none() - ); - assert!( - restored - .staging_resolution(&subject.id, &candidate.id) - .unwrap() - .is_some() - ); - assert_eq!( - restored - .surface_snapshot(&subject.id, &surface.id) - .unwrap() - .unwrap() - .memory_refs, - vec![MemoryRevisionRef { - memory_id: memory.id, - revision: 1, - }] - ); - } -} +//! Backend compatibility facade for the shared subjektiv domain. +pub use subjektiv::*; diff --git a/crates/yoi/Cargo.toml b/crates/yoi/Cargo.toml index 86dbdf81a..b951864da 100644 --- a/crates/yoi/Cargo.toml +++ b/crates/yoi/Cargo.toml @@ -20,6 +20,7 @@ server-api = { workspace = true } workdir = { workspace = true } tokio-tungstenite = { workspace = true } session-store = { workspace = true } +standalone = { workspace = true } session-analytics = { workspace = true } ticket = { workspace = true } tui = { workspace = true } diff --git a/crates/yoi/src/cli_connection.rs b/crates/yoi/src/cli_connection.rs index 0958ea16a..618b8cf39 100644 --- a/crates/yoi/src/cli_connection.rs +++ b/crates/yoi/src/cli_connection.rs @@ -27,6 +27,7 @@ pub(crate) enum CliCommand { Plugin, Mcp, Session, + Subject, Login, } @@ -46,6 +47,7 @@ impl CliCommand { CliCommand::Plugin => "yoi plugin", CliCommand::Mcp => "yoi mcp", CliCommand::Session => "yoi session", + CliCommand::Subject => "yoi subject", CliCommand::Login => "yoi login", } } @@ -65,7 +67,8 @@ impl CliCommand { | CliCommand::WorkerCleanup | CliCommand::Plugin | CliCommand::Mcp - | CliCommand::Session => CliConnectionRequirement::LocalOnly, + | CliCommand::Session + | CliCommand::Subject => CliConnectionRequirement::LocalOnly, } } } diff --git a/crates/yoi/src/main.rs b/crates/yoi/src/main.rs index 1e72559f0..d7aa0967b 100644 --- a/crates/yoi/src/main.rs +++ b/crates/yoi/src/main.rs @@ -3,6 +3,7 @@ mod mcp_cli; mod objective_cli; mod plugin_cli; mod session_cli; +mod subject_cli; mod ticket_cli; mod worker_cleanup_cli; mod workspace_bootstrap; @@ -41,6 +42,10 @@ enum Mode { target: client::ResolvedTarget, }, Session(session_cli::SessionCli), + Subject { + cli: subject_cli::SubjectCli, + target: Box, + }, WorkerCleanup(worker_cleanup_cli::WorkerCleanupCli), Ticket { cli: ticket_cli::TicketCli, @@ -171,6 +176,26 @@ async fn run(mode: Mode) -> ExitCode { } } } + Mode::Subject { cli, target } => { + let result = if cli == subject_cli::SubjectCli::Help { + Ok(subject_cli::HELP.to_owned()) + } else { + match target.spawn_worker() { + Ok(spawn) => subject_cli::run(cli, &spawn.state_dir), + Err(error) => Err(Box::new(error) as Box), + } + }; + match result { + Ok(output) => { + print!("{output}"); + ExitCode::SUCCESS + } + Err(error) => { + eprintln!("yoi subject: {error}"); + ExitCode::FAILURE + } + } + } Mode::Session(cli) => match session_cli::run(cli) { Ok(output) => { print!("{}", output.stdout); @@ -406,6 +431,7 @@ fn parse_args_slice_with_connection_resolver( LaunchMode::Spawn { worker_name: None, profile: None, + subject_id: None, } }; return Ok(Mode::Tui { @@ -454,6 +480,21 @@ fn parse_args_slice_with_connection_resolver( .map_err(|error| ParseError(error.to_string()))?; return Ok(Mode::Objective { cli, target }); } + "subject" => { + if target_selection.backend_url.is_some() || target_selection.workspace_id.is_some() { + return Err(ParseError( + "yoi subject is local-only and does not accept Backend selectors".to_owned(), + )); + } + if !target_selection.explicit_local { + return Err(ParseError( + "yoi subject requires explicit --local selection".to_owned(), + )); + } + let cli = subject_cli::parse(&args[1..])?; + let target = resolve_local_cli_connection(connection_resolver, CliCommand::Subject)?; + return Ok(Mode::Subject { cli, target }); + } "session" => { let _target = resolve_local_cli_connection(connection_resolver, CliCommand::Session)?; let session_cli = session_cli::parse_session_args(&args[1..]) @@ -828,6 +869,19 @@ fn required_option_value<'a>( Ok(value) } +fn set_subject_selector(selector: &mut Option, value: &str) -> Result<(), ParseError> { + if selector.is_some() { + return Err(ParseError( + "--subject must not be provided more than once".to_owned(), + )); + } + if value.trim().is_empty() || value.starts_with('-') { + return Err(ParseError("--subject requires a nonempty ID".to_owned())); + } + *selector = Some(value.to_owned()); + Ok(()) +} + fn parse_console_options( args: &[String], target_selection: &TargetSelection, @@ -837,6 +891,7 @@ fn parse_console_options( let mut worker_name = None; let mut session = None; let mut profile = None; + let mut subject_id = None; let mut socket_override = None; let mut runtime_id = None; let mut worker_id = None; @@ -884,6 +939,15 @@ fn parse_console_options( socket_override = Some(PathBuf::from(value)); i += 2; } + "--subject" => { + let value = required_option_value(args, i, "--subject")?; + set_subject_selector(&mut subject_id, value)?; + i += 2; + } + arg if arg.starts_with("--subject=") => { + set_subject_selector(&mut subject_id, &arg["--subject=".len()..])?; + i += 1; + } "--profile" => { let value = args .get(i + 1) @@ -989,6 +1053,24 @@ fn parse_console_options( } } + if subject_id.is_some() { + if session.is_some() + || socket_override.is_some() + || runtime_id.is_some() + || worker_id.is_some() + { + return Err(ParseError( + "--subject can only be used for fresh local spawn, not attach or restore" + .to_owned(), + )); + } + if target_selection.backend_url.is_some() || target_selection.workspace_id.is_some() { + return Err(ParseError( + "--subject is local-only and does not accept Backend selectors".to_owned(), + )); + } + } + if worker_id.is_some() && runtime_id.is_none() { return Err(ParseError( "--worker-id requires --runtime-id for Runtime API attach".to_string(), @@ -1018,12 +1100,29 @@ fn parse_console_options( return Err(ParseError("--socket requires --worker".to_string())); } - let target = resolve_tui_target( - connection_resolver, - CliCommand::DefaultTui, - target_selection, - &workspace_root, - )?; + let target = if subject_id.is_some() { + // A local Subject selector must never trigger Backend Workspace discovery. + let target = resolve_connection_aware_cli_connection( + connection_resolver, + CliCommand::DefaultTui, + target_selection.explicit_local, + None, + None, + )?; + if target.kind() != TargetKind::Standalone { + return Err(ParseError( + "--subject requires a local target; use --local".to_owned(), + )); + } + target + } else { + resolve_tui_target( + connection_resolver, + CliCommand::DefaultTui, + target_selection, + &workspace_root, + )? + }; if target.kind() == TargetKind::Standalone { if runtime_id.is_some() || worker_id.is_some() { @@ -1081,6 +1180,7 @@ fn parse_console_options( LaunchMode::Spawn { worker_name, profile, + subject_id, } } else { if worker_name.is_some() @@ -1912,6 +2012,7 @@ Connection-aware commands: Console options: --workspace Standalone cwd or client display scope (defaults to cwd) --profile Select the Standalone Profile recipe + --subject Select an existing local Subject for fresh spawn only --runtime-id Backend Runtime id --worker-id Backend Worker id; requires --runtime-id @@ -1926,6 +2027,7 @@ Host commands: plugin Author/check/pack explicit Plugin packages mcp Inspect configured MCP servers session Inspect/prune Standalone session logs + --local subject Explicitly create/list local Subjects (create , list) Standalone binaries: yoi-server Workspace Backend server/admin CLI @@ -2618,11 +2720,115 @@ backend = "shared" LaunchMode::Spawn { worker_name: Some(ref name), profile: None, + subject_id: None, } if name == "my-local-worker" )); assert!(target.spawn_worker().unwrap().state_dir.is_absolute()); } + #[test] + fn subject_selection_is_carried_only_to_local_fresh_spawn() { + let resolver = DefaultBackendCliConnectionResolver { + backend_url: "http://default-backend.example", + }; + for selector in [ + vec!["--subject", "subject-id"], + vec!["--subject=subject-id"], + ] { + let args = [ + vec!["--local", "--profile", "builtin:standalone-subjektiv"], + selector, + ] + .concat() + .into_iter() + .map(String::from) + .collect::>(); + let Mode::Tui { + target, + mode: LaunchMode::Spawn { subject_id, .. }, + .. + } = parse_args_slice_with_connection_resolver(&args, &resolver).unwrap() + else { + panic!("expected local spawn"); + }; + assert_eq!(target.kind(), TargetKind::Standalone); + assert_eq!(subject_id.as_deref(), Some("subject-id")); + } + for input in [ + vec!["--subject", "subject-id"], + vec![ + "--backend", + "http://backend.example", + "--subject", + "subject-id", + ], + vec!["--local", "--workspace-id", "ws", "--subject=id"], + vec!["--local", "--subject", ""], + vec!["--local", "--subject", " "], + vec!["--local", "--subject="], + vec!["--local", "--subject=id", "--subject=other"], + vec!["--local", "--subject=id", "--subject", "other"], + vec!["--local", "--subject=id", "--session", "session"], + vec![ + "--local", + "--subject=id", + "--socket", + "socket", + "--worker", + "worker", + ], + vec!["--local", "--subject=id", "--runtime-id", "runtime"], + vec!["--local", "--subject=id", "--worker-id", "worker"], + vec!["--local", "resume", "--subject=id"], + vec!["--local", "resume", "--subject", "id"], + ] { + let args = input + .iter() + .map(|arg| (*arg).to_owned()) + .collect::>(); + assert!( + parse_args_slice_with_connection_resolver(&args, &resolver).is_err(), + "{input:?}" + ); + } + } + + #[test] + fn subject_commands_require_explicit_local_target() { + let resolver = DefaultBackendCliConnectionResolver { + backend_url: "http://default-backend.example", + }; + for command in [vec!["create", "assistant"], vec!["list"], vec!["--help"]] { + let args = [vec!["--local", "subject"], command] + .concat() + .into_iter() + .map(String::from) + .collect::>(); + let Mode::Subject { target, .. } = + parse_args_slice_with_connection_resolver(&args, &resolver).unwrap() + else { + panic!("expected Subject mode"); + }; + assert_eq!(target.kind(), TargetKind::Standalone); + assert!(target.spawn_worker().unwrap().state_dir.is_absolute()); + } + for input in [ + vec!["subject", "list"], + vec!["--backend", "http://backend.example", "subject", "list"], + vec!["--local", "--workspace-id=ws", "subject", "list"], + vec!["--local", "subject", "list", "--backend=url"], + ] { + let args = input + .iter() + .map(|arg| (*arg).to_owned()) + .collect::>(); + assert!( + parse_args_slice_with_connection_resolver(&args, &resolver).is_err(), + "{input:?}" + ); + } + } + #[test] fn parser_rejects_removed_top_level_resume_flags() { for (args, expected) in [ @@ -3346,6 +3552,7 @@ backend = "shared" LaunchMode::Spawn { worker_name, profile, + subject_id: None, }, workspace_root, .. diff --git a/crates/yoi/src/subject_cli.rs b/crates/yoi/src/subject_cli.rs new file mode 100644 index 000000000..a6c18cb8d --- /dev/null +++ b/crates/yoi/src/subject_cli.rs @@ -0,0 +1,184 @@ +use std::path::Path; + +use standalone::subjektiv::StandaloneSubjects; + +use super::ParseError; + +pub(crate) const HELP: &str = "Local Subjects (operator-managed standalone state): + yoi --local subject create [--behavior-md ] + yoi --local subject list + yoi --local --subject [--profile builtin:standalone-subjektiv] + +Create prints a Subject record as JSON; list prints a JSON array. +Selection is explicit and only applies to fresh local launches. +Resume restores the original Subject binding and does not accept --subject. +"; + +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum SubjectCli { + Help, + Create { + role: String, + behavior_md: Option, + }, + List, +} + +pub(crate) fn parse(args: &[String]) -> Result { + match args.first().map(String::as_str) { + None => Ok(SubjectCli::Help), + Some("--help" | "-h") if args.len() == 1 => Ok(SubjectCli::Help), + Some("list") if args.len() == 1 => Ok(SubjectCli::List), + Some("create") => { + let role = args + .get(1) + .filter(|role| !role.starts_with('-') && !role.trim().is_empty()) + .ok_or_else(|| ParseError("subject create requires a nonempty ROLE".to_owned()))?; + let mut behavior_md = None; + let mut i = 2; + while i < args.len() { + if behavior_md.is_some() { + return Err(ParseError( + "subject create accepts --behavior-md only once".to_owned(), + )); + } + match args[i].as_str() { + "--behavior-md" => { + let text = args + .get(i + 1) + .filter(|text| !text.starts_with("--")) + .ok_or_else(|| ParseError("--behavior-md requires TEXT".to_owned()))?; + behavior_md = Some(text.clone()); + i += 2; + } + arg if arg.starts_with("--behavior-md=") => { + behavior_md = Some(arg.trim_start_matches("--behavior-md=").to_owned()); + i += 1; + } + arg => { + return Err(ParseError(format!( + "unknown subject create argument `{arg}`" + ))); + } + } + } + Ok(SubjectCli::Create { + role: role.clone(), + behavior_md, + }) + } + Some(arg) => Err(ParseError(format!( + "invalid subject command or arguments `{arg}`; use `yoi --local subject --help`" + ))), + } +} + +pub(crate) fn run(cli: SubjectCli, state_dir: &Path) -> Result> { + if cli == SubjectCli::Help { + return Ok(HELP.to_owned()); + } + let subjects = StandaloneSubjects::open(state_dir)?; + let json = match cli { + SubjectCli::Create { role, behavior_md } => { + serde_json::to_string_pretty(&subjects.create(&role, behavior_md.as_deref())?)? + } + SubjectCli::List => serde_json::to_string_pretty(&subjects.list()?)?, + SubjectCli::Help => unreachable!(), + }; + Ok(format!("{json}\n")) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn args(args: &[&str]) -> Vec { + args.iter().map(|arg| (*arg).to_owned()).collect() + } + + #[test] + fn parse_explicit_create_and_list() { + assert_eq!( + parse(&args(&[ + "create", + "assistant", + "--behavior-md", + "Remember decisions." + ])) + .unwrap(), + SubjectCli::Create { + role: "assistant".to_owned(), + behavior_md: Some("Remember decisions.".to_owned()) + } + ); + assert_eq!(parse(&args(&["list"])).unwrap(), SubjectCli::List); + } + + #[test] + fn reject_missing_empty_duplicate_and_unknown_arguments() { + for input in [ + vec!["create"], + vec!["create", " "], + vec!["create", "role", "--behavior-md"], + vec!["create", "role", "--behavior-md=x", "--behavior-md=y"], + vec!["list", "--subject=id"], + vec!["create", "role", "--backend=url"], + ] { + assert!(parse(&args(&input)).is_err(), "{input:?}"); + } + } + + #[test] + fn create_and_list_share_only_the_selected_state_root() { + let root = tempfile::tempdir().unwrap(); + let other = tempfile::tempdir().unwrap(); + // New state roots are created privately; temporary parent directories + // need not meet the state-root permission contract. + let state_dir = root.path().join("state"); + let other_state_dir = other.path().join("state"); + let created: serde_json::Value = serde_json::from_str( + &run( + SubjectCli::Create { + role: "assistant".to_owned(), + behavior_md: Some("Be precise.".to_owned()), + }, + &state_dir, + ) + .unwrap(), + ) + .unwrap(); + let listed: serde_json::Value = + serde_json::from_str(&run(SubjectCli::List, &state_dir).unwrap()).unwrap(); + assert_eq!(listed, serde_json::json!([created])); + let elsewhere: serde_json::Value = + serde_json::from_str(&run(SubjectCli::List, &other_state_dir).unwrap()).unwrap(); + assert_eq!(elsewhere, serde_json::json!([])); + } + + #[cfg(unix)] + #[test] + fn nonprivate_existing_root_requires_explicit_operator_permission_change() { + use std::os::unix::fs::PermissionsExt; + + let root = tempfile::tempdir().unwrap(); + std::fs::set_permissions(root.path(), std::fs::Permissions::from_mode(0o755)).unwrap(); + assert!(run(SubjectCli::List, root.path()).is_err()); + assert_eq!( + std::fs::metadata(root.path()).unwrap().permissions().mode() & 0o777, + 0o755 + ); + assert!(!root.path().join("subjektiv").exists()); + std::fs::set_permissions(root.path(), std::fs::Permissions::from_mode(0o700)).unwrap(); + let listed: serde_json::Value = + serde_json::from_str(&run(SubjectCli::List, root.path()).unwrap()).unwrap(); + assert_eq!(listed, serde_json::json!([])); + } + + #[test] + fn help_does_not_open_state() { + let root = tempfile::tempdir().unwrap(); + let absent = root.path().join("absent"); + assert_eq!(run(SubjectCli::Help, &absent).unwrap(), HELP); + assert!(!absent.exists()); + } +} diff --git a/docs/design/standalone-subjektiv.md b/docs/design/standalone-subjektiv.md new file mode 100644 index 000000000..88d36325e --- /dev/null +++ b/docs/design/standalone-subjektiv.md @@ -0,0 +1,41 @@ +# Standalone subjektiv (T-709) + +## Implementation design, recorded before implementation + +Base: `13055d7ee18f9d29edc641d0bd0e57c3d034dfa0`, including T-704 and T-708. No Backend database, fake Workspace, local fallback, new daemon, or subjektiv-specific Internal Worker runner is introduced. + +The operator-managed standalone state directory (existing CLI/XDG standalone state setting, not cwd or a repository file) is the local sharing boundary. `/subjektiv/` is a private directory. A persisted random storage-scope identity is issued here; Subject IDs use the common domain UUID issuer, never cwd/Profile/Worker name. The common Feature SQLite manager opens the subjektiv database under this root; it owns connection options, migration execution, close/reopen and transaction locking. The common subjektiv crate owns the existing schema/migrations/domain/CAS/receipt/surface rules. Existing Backend layout and physical schema remain unchanged. Job domain state is not embedded into Memory tables or Session text. + +Subjects are explicitly created/listed in that state scope and selected at normal launch; enabled policy alone does not create or attach one. Each selected Subject has a separate OS advisory lock file under `/subjektiv/leases/`. The Host holds the exclusive lock for body execution and all delegated Jobs until confirmed shutdown. A second process cannot acquire the same lock. Process death releases the kernel lock, not a PID/Profile guess; uncertain in-process cleanup retains ownership. A separate Subject can run concurrently, with SQLite serializing common database transactions. Symlinks and non-private state nodes must be rejected at this boundary. Knowledge of an ID/path is not a model capability. + +The selected Subject and local storage-scope identity are persisted in the standalone Worker record. Restore verifies them and reacquires the original Subject lock; resume never silently changes Subjects. Subject-free and disabled-policy launches retain the old path with no subjektiv persistence side effects. Ordinary SubWorkers/Reviewers receive no independent Subject/extraction connection. + +The Feature receives an explicit Host-scoped subjektiv capability, separate from WorkspaceClient and Profile policy. Backend adapts its existing authenticated API operations; standalone binds one local store, Subject, Worker and committed Session authority. Session attribution is established by the Host, including Sessions without candidates. Reads use the existing committed public Session projection and standalone Session store, not a copy in the Memory database. No system prompt, hidden reasoning, uncommitted history or arbitrary filesystem/foreign-Subject access is granted. + +Consolidation uses the existing generic standalone Jobs service and common request/result contracts. For a selected Subject the Host owns `/subjektiv/jobs//jobs.sqlite3` under that Subject lease, allowing durable intents/unknown outcomes/results to follow it across Workers. Subject-free Hosts retain the T-708 Worker-local location. Subjektiv selects the consolidation Profile. Job capabilities are explicitly attenuated to the immutable candidate batch, recall/decision/surface operations and public Session references; they never confer the Subject body's execution lease or ambient parent filesystem/Workspace tools. Common receipt/CAS rules handle partial apply and response loss; unknown execution is never automatically replayed. Surface failure leaves confirmed Memory intact. The generic runner owns cancellation, timeout, result binding, resource cleanup and shutdown; processing stops with the standalone process and pending durable work is recovered only on a subsequent explicit Host connection. + +Validation must distinguish scripted model/Host and real test-owned process/lock runs from live-provider model quality. Root `cargo check`, changed-crate tests and narrow Backend/Feature/Job regression tests are required, plus format/diff checks. Concrete CLI/UI usage and recovery are documented in [standalone.md](../standalone.md). + +## Implemented boundaries and validation + +- `feature-storage` owns the shared SQLite connection policy/Feature manager; `subjektiv` owns the unchanged domain schema/migrations, Host-neutral API dispatch, and T-704 consolidation request/result/surface validation. Server modules remain thin adapters; Worker/standalone do not depend on Workspace Server. +- `worker::subjektiv` exposes a nonserializable live Host capability plus a separate attenuated consolidation capability. Backend creation still requires the actual authenticated client and bound snapshot. Local creation is explicit Host injection; unavailable Backend never falls back locally. +- The local catalog keeps private scope/language settings and common Feature storage under `subjektiv/`, separately from generic Subject Job ledgers. The common attribution table's existing `runtime_id` lookup key is the local storage namespace for local rows, not a running Runtime identity; public local evidence leaves `runtime_id`, Workspace, and Account fields absent. Worker IDs, Subject binding, and operator-managed scope must all match before Session content is read. +- The shared strict FsStore public index is also used for committed evidence admission. Stable references are intersected with visible committed entries; numeric hints accept the two existing capture producers. Open-Run evidence is deferred only before a request, never exposed by Session tools or recorded before commit. Persisted local human provenance is distinct from Backend Account provenance and maps to the existing public `HumanInput` class. +- Standalone integration tests use real Hosts/SQLite, scripted extraction/consolidation/surface models, and test-owned second processes. Nine cases cover inert/disabled policy, reopen, saved-binding restore, cross-Worker recall/source discovery, exact explicit commit staging, normal extract/pointer → generic Job → CAS apply → resident surface, same-Subject cross-process exclusion/abnormal-exit recovery, and transport loss after apply followed by surface-only repair without duplicate Memory revision. +- Generic Job tests additionally cover immutable grant persistence, missing/foreign/revision/consumer grants, per-attempt/deadline revocation, cancellation, timeout, cleanup failure, and stale capabilities. Session adapter tests cover no-candidate discovery, foreign Subject/scope/Worker denial, bounded UTF-8 and JSON output, filter/generation-bound cursors, missing/corrupt storage, and hidden/uncommitted/partial-tail exclusion without observation writes. + +Verified commands: + +```sh +cargo test -p standalone --test subjektiv -- --test-threads=1 +cargo test -p protocol -p feature-storage -p subjektiv -p manifest -p session-store -p standalone -p worker -p tui -p yoi -p yoi-workspace-server +cargo test -p worker-runtime --lib protocol_ +cargo check +cargo check -p protocol --features typescript,json-schema +cargo check -p tui --features e2e-test +cargo fmt --all -- --check +git diff --check HEAD +``` + +The changed-crate run passed, including 836 Worker unit tests, 603 Workspace Server unit tests (authorization/singleton/T-704 regression), 48 standalone unit tests, and nine standalone Subject integration tests. Existing ignored tests remain ignored. TypeScript generation check reports existing unsupported serde-attribute warnings but succeeds. These commands prove mechanism/authorization/storage contracts, not live-model quality or full interactive terminal E2E behavior. diff --git a/docs/standalone.md b/docs/standalone.md new file mode 100644 index 000000000..ff36f4fac --- /dev/null +++ b/docs/standalone.md @@ -0,0 +1,52 @@ +# Standalone local Subjects + +Standalone runs one client-owned Worker in the current process, without a Backend or Workspace. Local Subjects are explicitly created and selected; enabling a subjektiv Profile alone neither creates nor attaches a Subject. + +## Create and list + +```sh +yoi --local subject create assistant --behavior-md 'Keep durable decisions and preferences.' +yoi --local subject list +``` + +`create ` prints the created Subject record as JSON. `--behavior-md ` is optional inline text, not a file path. `list` prints a JSON array of records in the selected standalone state scope. Both commands require explicit `--local` and reject Backend/Workspace selectors. + +The state directory is obtained from the same local connection target as normal standalone launch. Its base follows the existing `YOI_DATA_DIR`, `YOI_HOME`, `XDG_DATA_HOME`, or `HOME` resolution, with `client/standalone/workers` appended. Subjects live under `/subjektiv/`; cwd, repository files, Worker names, and Profiles are not Subject identity or sharing boundaries. Use the returned common-domain Subject ID (`subject-`), not the role, for selection. + +The user-managed state root must be operator-owned and private (mode **0700**). A missing root is created with mode 0700; an existing root is validated, not automatically chmodded. An older nonprivate root is rejected. The operator can explicitly run `chmod 700 ` on their own state root before retrying; do not substitute a repository directory or assume the CLI repairs permissions. Symlink and nonprivate Subject storage nodes are also rejected. + +## Select for a new Worker + +```sh +yoi --local --subject --profile builtin:standalone-subjektiv +``` + +`--subject=` is also accepted. The selector must be nonempty and may only be supplied once. It applies only to fresh local spawn: Backend selection, Runtime attach, socket/session attach, and resume combinations are rejected. A selected Subject requires a compatible subjektiv policy; launch validation rejects incompatible policy before Worker startup side effects. + +The interactive local spawn form has an optional `subject` field, initially `(none)`. Press **F2** to switch between editing the Worker name and Subject ID; **Tab/Up/Down** continue to cycle Profiles. Leave the Subject field empty for the existing Subject-free launch path. The form does not create Subjects or open Subject storage just to display the field. The optional built-in `builtin:standalone-subjektiv` Profile enables local Subject behavior; its consolidation Profile is `builtin:standalone-subjektiv-consolidation` and does not require Backend resources. + +## Resume + +```sh +yoi --local resume +``` + +Resume restores the original persisted Subject/storage-scope binding and reacquires its execution lease. It does not accept a new `--subject` selector or silently change Subjects. The Host holds an exclusive per-Subject kernel lease for body execution and delegated Jobs until confirmed shutdown; one Subject cannot be executed concurrently by two processes. Process death releases the kernel lease, while uncertain in-process cleanup retains ownership. Separate Subjects may run concurrently in the same state scope. + +## Subject Jobs and recovery + +A selected Subject uses `/subjektiv/jobs//jobs.sqlite3`, shared across Worker IDs for that Subject and protected by its exclusive execution lease. Subject-free Hosts retain the existing Worker-local Job location. There is no separate Subject runner or background daemon. + +On explicit selected-Subject Host startup, reserved Jobs resume automatically. An interrupted Job whose execution outcome is unknown is **never automatically replayed**. A successful result that has not yet been acknowledged is delivered from durable storage without retrying the model. Recovery is driven by an explicit Host startup, not an automatic process restart or independent scheduler. + +## Memory and historical Sessions + +The normal body Profile has recall, revision reads, explicit candidate tools, and bounded historical Session exploration. It cannot directly apply Memory. Explicit Remember/Revise without `entry_refs` returns a pending-commit receipt; the Host stages its exact tool-call evidence only after the logical Run is append-only committed. Pre-request retry may defer an open Run, but commit/rewrite failures are not silently treated as success. A candidate remains unconfirmed until the restricted consolidation Job applies or rejects it using the shared CAS/receipt rules. + +Normal extraction uses the existing committed-capture lifecycle, restricted extraction tools, and append-only `subjektiv.extract.v1` pointer. The selected Profile owns extraction thresholds/model policy. An explicit extraction model override chooses that provider, not an injected dialog transport. Local Memory language defaults to English and is stored in operator-managed `subjektiv/language.json`, never loaded from repository policy files. + +The Host stamps human input on its owned in-process client channel as persisted `local_human_input`, projected as the existing `HumanInput` provenance. It neither invents a Backend account nor trusts serialized source claims. Ordinary SubWorker/Job input retains its own nonhuman origin. Local evidence has no Workspace/account/Runtime identity; local storage-scope identity and Worker binding authorize discovery separately. Historical Sessions are not copied into SQLite. Reads/search exclude system prompts, hidden reasoning, incomplete logical Runs, partial JSONL tails, and foreign bindings; cursor continuations revalidate scope/Subject/Worker attribution and committed generation. + +## Validation boundary + +Host injection tests pass a clone of the injected model transport into delegated Jobs, so scripted body and Job calls use the same test-owned transport. These are local contract tests, not live-provider validation or evidence of model/consolidation quality. Generic Job finalizer and Host integration proofs are validated separately from the CLI/parser and spawn-form tests. diff --git a/resources/profiles/standalone-subjektiv-consolidation.dcdl b/resources/profiles/standalone-subjektiv-consolidation.dcdl new file mode 100644 index 000000000..f451ae08a --- /dev/null +++ b/resources/profiles/standalone-subjektiv-consolidation.dcdl @@ -0,0 +1,8 @@ +import "./job.dcdl" // { + slug = "standalone-subjektiv-consolidation"; + description = "Subject consolidation policy; local Host grants a bounded batch, not body authority."; + engine = { instruction = "internal.subjektiv_memory_consolidation_system"; }; + feature = { + subjektiv = { enabled = true; consolidation_tools = true; extraction = { enabled = false; }; }; + }; +} diff --git a/resources/profiles/standalone-subjektiv.dcdl b/resources/profiles/standalone-subjektiv.dcdl new file mode 100644 index 000000000..b03c625f5 --- /dev/null +++ b/resources/profiles/standalone-subjektiv.dcdl @@ -0,0 +1,7 @@ +import "./standalone.dcdl" // { + slug = "standalone-subjektiv"; + description = "Standalone coding with an explicitly selected local Subject."; + feature = { + subjektiv = { enabled = true; extraction = { enabled = true; }; }; + }; +} From 61f0469986b634544f01698211877e1f344ef2e7 Mon Sep 17 00:00:00 2001 From: Hare Date: Wed, 7 Oct 2026 02:25:16 +0900 Subject: [PATCH 6/9] fix: preserve string and table Profile scope authoring (T-710) Resolve MR sequence 5 profile_authoring_rejects_valid_string_scope_intents. Decodal 0.4 has no union constraint: retain an opaque scope range with non-materialized table completion hints so string intents remain valid. Keep shared Backend semantic validation authoritative for scope forms/types/unknown keys without changing persisted values or weakening worker/feature editor diagnostics. Prove actual-provider native/WASM scalar/table, inline/import/patch, nested intent/deny_write/symlink_policy completion and evaluation invariance. Add real save/projection/Runtime scope equivalence and invalid-scope save non-loss coverage. Regenerated WASM artifact is byte-identical; the changed shape is transported via the fingerprint-bound Backend schema metadata. Validation: root cargo check; config-source39; value_profiles4; unfiltered server643+11; worker-runtime340+17+17; server-api --features typescript77; wasm32 release/wasm-bindgen0.2.117/Binaryen132 -Oz; Deno39; deno task check0/0; fmt/diff checks. Logs target/t710-scope-{check,server,server-api,runtime}.log. No target/ref integration changes. --- crates/config-source/src/lib.rs | 87 ++++++++++++++++++- .../src/server/tests/value_profiles_tests.rs | 81 +++++++++++++++++ docs/design/profiles-manifests-prompts.md | 2 + .../config-schema/profile-authoring.dcdl | 6 +- .../test/config-source/wasm-parity.test.ts | 87 +++++++++++++++++++ 5 files changed, 258 insertions(+), 5 deletions(-) diff --git a/crates/config-source/src/lib.rs b/crates/config-source/src/lib.rs index 8f1239d3d..d5c1f7eb5 100644 --- a/crates/config-source/src/lib.rs +++ b/crates/config-source/src/lib.rs @@ -2555,9 +2555,8 @@ mod tests { assert_eq!(environment.evaluate_contract(&contract).unwrap(), evaluated); } - #[test] - fn actual_profile_schema_diagnoses_nested_values_without_materializing_omissions() { - let schema = WorkspaceConfigSchemaBundle::compose([ConfigSchemaContribution::new( + fn actual_profile_schema() -> WorkspaceConfigSchemaBundle { + WorkspaceConfigSchemaBundle::compose([ConfigSchemaContribution::new( "builtin:profile", "profile", "2", @@ -2567,7 +2566,12 @@ mod tests { .with_authoring_source(include_str!( "../../../resources/config-schema/profile-authoring.dcdl" ))]) - .unwrap(); + .unwrap() + } + + #[test] + fn actual_profile_schema_diagnoses_nested_values_without_materializing_omissions() { + let schema = actual_profile_schema(); let source = "{ profile = { entries = [{ selector = \"project:alpha\"; profile = {}; }]; }; } as WorkspaceConfigSchema"; let snapshot = ConfigTreeSnapshot::from_entries( 8, @@ -2659,6 +2663,81 @@ mod tests { ); } + #[test] + fn actual_profile_schema_preserves_scalar_and_table_scope_forms_and_completion() { + for (recipe, expected) in [ + ( + r#"{ scope = "workspace_read"; delegation_scope = "workspace_write"; }"#, + serde_json::json!({"scope": "workspace_read", "delegation_scope": "workspace_write"}), + ), + ( + r#"{ scope = "workspace_write"; delegation_scope = "workspace_read"; }"#, + serde_json::json!({"scope": "workspace_write", "delegation_scope": "workspace_read"}), + ), + ( + r#"{ scope = { intent = "workspace_read"; }; delegation_scope = { intent = "workspace_write"; deny_write = ["private"]; symlink_policy = "resolved"; }; }"#, + serde_json::json!({"scope": {"intent": "workspace_read"}, "delegation_scope": {"intent": "workspace_write", "deny_write": ["private"], "symlink_policy": "resolved"}}), + ), + ] { + for form in [ + recipe.to_owned(), + r#"import "./scoped.dcdl""#.into(), + format!(r#"(import "./scoped.dcdl") // {{ description = "patched"; }}"#), + ] { + let source = format!( + r#"{{ profile.entries = [{{ selector = "project:scoped"; profile = {form}; }}]; }} as WorkspaceConfigSchema"# + ); + let snapshot = ConfigTreeSnapshot::from_entries( + 9, + [entry("main.dcdl", &source), entry("scoped.dcdl", recipe)], + ) + .unwrap(); + let schema = actual_profile_schema(); + let environment = + SnapshotEnvironment::new(snapshot).with_schema_bundle(schema.clone()); + let contract = + ToolchainContract::with_schema_bundle(1, vec![path("main.dcdl")], 1, schema); + let before = environment.evaluate_contract(&contract).unwrap(); + assert!( + environment.analyze(&path("main.dcdl"), None).is_empty(), + "{source}" + ); + let mut expected = expected.clone(); + if form.contains("patched") { + expected["description"] = serde_json::json!("patched"); + } + assert_eq!( + before.projections[0] + .data_json + .pointer("/profile/entries/0/profile"), + Some(&expected) + ); + for field in ["scope", "delegation_scope"] { + for (prefix, label) in [ + ("int", "intent"), + ("deny", "deny_write"), + ("sym", "symlink_policy"), + ] { + let marked = format!( + "{{ profile.entries = [{{ profile.{field} = {{ {prefix}| }}; }}]; }} as WorkspaceConfigSchema" + ); + let cursor = marked.find('|').unwrap(); + let source = marked.replace('|', ""); + let result = environment + .complete_config(&path("main.dcdl"), &source, cursor, true) + .unwrap() + .unwrap(); + assert!( + result.items.iter().any(|item| item.label == label), + "{source}: {result:?}" + ); + } + } + assert_eq!(environment.evaluate_contract(&contract).unwrap(), before); + } + } + } + #[test] fn completion_authoring_sources_compose_with_validation_source_fallbacks() { let schema = WorkspaceConfigSchemaBundle::compose([ diff --git a/crates/workspace-server/src/server/tests/value_profiles_tests.rs b/crates/workspace-server/src/server/tests/value_profiles_tests.rs index 984f8f5b4..72c72f6ad 100644 --- a/crates/workspace-server/src/server/tests/value_profiles_tests.rs +++ b/crates/workspace-server/src/server/tests/value_profiles_tests.rs @@ -228,6 +228,81 @@ async fn value_profiles_save_project_and_runtime_consume_the_same_revision() { assert!(restored.feature.task.enabled); } +#[tokio::test] +async fn value_profiles_scalar_and_table_scopes_analyze_save_and_resolve_equally() { + let dir = tempfile::tempdir().unwrap(); + let api = test_api(dir.path()).await; + let scalar = format!( + r#"{PROFILE} // {{ scope = "workspace_read"; delegation_scope = "workspace_write"; }}"# + ); + let table = format!( + r#"{PROFILE} // {{ scope = {{ intent = "workspace_read"; }}; delegation_scope = {{ intent = "workspace_write"; }}; }}"# + ); + let mut scopes = Vec::new(); + for (index, form) in [scalar.clone(), table, r#"import "./scoped.dcdl""#.into()] + .iter() + .enumerate() + { + let extra = if index == 0 { + vec![config_source::ConfigTreeChange::Create { + path: config_source::VirtualPath::parse("scoped.dcdl").unwrap(), + content_type: config_source::ConfigContentType::Decodal, + content: scalar.clone(), + }] + } else { + vec![] + }; + let authored = format!("{} as WorkspaceConfigSchema", source(form)); + let state = + commit_workspace_config_tree(&api, TEST_WORKSPACE_ID, &request(&api, &authored, extra)) + .unwrap(); + let environment = config_source::SnapshotEnvironment::new(state.snapshot.clone()) + .with_schema_bundle(state.contract.schema_bundle.clone()); + assert!( + environment + .analyze( + &config_source::VirtualPath::parse("main.dcdl").unwrap(), + None + ) + .is_empty(), + "{authored}" + ); + let projection = project_profiles_from_workspace_config(TEST_WORKSPACE_ID, &state).unwrap(); + let bundle = build_virtual_profile_config_bundle( + &projection, + &state, + TEST_WORKSPACE_ID, + "created", + "project:alpha", + ) + .unwrap() + .unwrap(); + let resolved = bundle + .profile_source_archive + .as_ref() + .unwrap() + .verify() + .unwrap() + .resolve_profile( + "project:alpha", + Path::new("/runtime-worker"), + "scoped-worker", + ) + .unwrap(); + assert_eq!(resolved.scope.allow.len(), 1); + assert_eq!(resolved.delegation_scope.allow.len(), 1); + let scope = serde_json::to_value(&resolved.scope).unwrap(); + let delegation = serde_json::to_value(&resolved.delegation_scope).unwrap(); + assert_eq!(scope["allow"][0]["target"], "/runtime-worker"); + assert_eq!(scope["allow"][0]["permission"], "read"); + assert_eq!(delegation["allow"][0]["target"], "/runtime-worker"); + assert_eq!(delegation["allow"][0]["permission"], "write"); + scopes.push((scope, delegation)); + } + assert_eq!(scopes[0], scopes[1]); + assert_eq!(scopes[1], scopes[2]); +} + #[tokio::test] async fn value_profiles_invalid_saves_preserve_the_active_config() { let dir = tempfile::tempdir().unwrap(); @@ -242,6 +317,12 @@ async fn value_profiles_invalid_saves_preserve_the_active_config() { (source("42"), ""), (source(r#"{ worker = { mode = "not-a-mode"; }; }"#), "profile_value_invalid"), (source(r#"{ worker = { name = "runtime-only"; }; }"#), "profile_value_invalid"), + (source(r#"{ scope = 42; }"#), "profile_value_invalid"), + (source(r#"{ scope = "not-an-intent"; }"#), "profile_value_invalid"), + (source(r#"{ delegation_scope = { intent = 42; }; }"#), "profile_value_invalid"), + (source(r#"{ scope = { intent = "workspace_read"; unknown = true; }; }"#), "profile_value_invalid"), + (source(r#"{ scope = { intent = "workspace_read"; deny_write = 42; }; }"#), "profile_value_invalid"), + (source(r#"{ delegation_scope = { intent = "workspace_write"; symlink_policy = 42; }; }"#), "profile_value_invalid"), (source(r#"import "./missing.dcdl""#), ""), (source(r#"import "../outside.dcdl""#), ""), (r#"{ profile = { entries = [{ selector = "project:alpha"; profile = {}; }, { selector = "project:alpha"; profile = {}; }]; }; }"#.into(), "profile_selector_duplicate"), diff --git a/docs/design/profiles-manifests-prompts.md b/docs/design/profiles-manifests-prompts.md index c75a33fe1..f89c33192 100644 --- a/docs/design/profiles-manifests-prompts.md +++ b/docs/design/profiles-manifests-prompts.md @@ -45,6 +45,8 @@ The former `entries = [{ selector = "project:name"; source = "profiles/name.dcdl The schema contribution separates its materialized registration schema from an optional, fingerprint-bound `authoring_source`. The latter supplies editor field suggestions inside array elements and partial Profile records, plus a separate constraint-only analysis pass for supplied nested values (including imported recipes); that pass never materializes the shape, makes omitted fields required, or supplies defaults to the saved projection. Editor diagnostics point to the supplied value's virtual file/span. The authoring shape is never substituted for the persisted evaluation schema. Decodal structural diagnostics cover registration and nested Profile field types; the shared Backend save boundary additionally checks full Profile/Manifest semantics (including mode values, forbidden runtime fields, duplicate selectors and default references). UI and WIP saves use that same boundary; invalid candidates leave the active config revision untouched. +`scope` and `delegation_scope` retain both supported forms: a string such as `"workspace_read"` / `"workspace_write"`, or a table such as `{ intent = "workspace_read"; deny_write = ["private"]; symlink_policy = "resolved"; }`. Decodal 0.4 has no union range, so these fields are opaque to the editor constraint pass; a non-materialized table hint supplies nested completion without rejecting the string form or rewriting it. The shared Backend save validator checks scope forms, intent values and table fields (including their types and unknown keys). Other structurally typed fields such as `worker.mode` and `feature.task.enabled` retain editor type diagnostics. Completion hints never become Profile defaults or saved values. + ## Manifests A resolved Manifest is the concrete contract used to create or restore a Worker. It carries defaults, resolved paths, permissions, scope, prompt references, provider/model decisions, and runtime identity. diff --git a/resources/config-schema/profile-authoring.dcdl b/resources/config-schema/profile-authoring.dcdl index da6fe1853..ffd1b5e83 100644 --- a/resources/config-schema/profile-authoring.dcdl +++ b/resources/config-schema/profile-authoring.dcdl @@ -12,7 +12,11 @@ let enabled = Bool; model = Model; reasoning = String; threshold = Int; worker_max_turns = Int; }; - Scope = { intent = String; deny_write = [...String]; symlink_policy = String; }; + # Profile scope accepts either a string intent or a table. Decodal 0.4 has + # no union range; keep this field opaque to constraint analysis and expose + # the table only as a completion hint. This shape is never materialized; + # actual scope forms, intent values and table fields are checked on save. + Scope = Unknown default { intent = String; deny_write = [...String]; symlink_policy = String; }; Profile = { slug = String; description = String; diff --git a/web/workspace/test/config-source/wasm-parity.test.ts b/web/workspace/test/config-source/wasm-parity.test.ts index cd03c15d5..be59ab095 100644 --- a/web/workspace/test/config-source/wasm-parity.test.ts +++ b/web/workspace/test/config-source/wasm-parity.test.ts @@ -569,6 +569,93 @@ Deno.test("generated WASM authors value-based Profiles using the Backend schema ); } } + for (const field of ["scope", "delegation_scope"]) { + for ( + const [prefix, label] of [["int", "intent"], ["deny", "deny_write"], [ + "sym", + "symlink_policy", + ]] + ) { + const body = + `{ profile.entries = [{ profile.${field} = { ${prefix} } }]; } as WorkspaceConfigSchema`; + const cursor = body.lastIndexOf(prefix) + prefix.length; + const result = complete_current("main.dcdl", body, cursor, true) as { + items: Array<{ label: string }>; + }; + assertEquals( + result.items.some((item) => item.label === label), + true, + `${field}.${label}`, + ); + } + } + for ( + const [recipe, expected] of [ + ['{ scope = "workspace_read"; delegation_scope = "workspace_write"; }', { + scope: "workspace_read", + delegation_scope: "workspace_write", + }], + ['{ scope = "workspace_write"; delegation_scope = "workspace_read"; }', { + scope: "workspace_write", + delegation_scope: "workspace_read", + }], + [ + '{ scope = { intent = "workspace_read"; }; delegation_scope = { intent = "workspace_write"; deny_write = ["private"]; symlink_policy = "resolved"; }; }', + { + scope: { intent: "workspace_read" }, + delegation_scope: { + intent: "workspace_write", + deny_write: ["private"], + symlink_policy: "resolved", + }, + }, + ], + ] as const + ) { + for ( + const form of [ + recipe, + 'import "./scoped.dcdl"', + '(import "./scoped.dcdl") // { description = "patched"; }', + ] + ) { + const scopedContent = content.replace( + "profile = {};", + `profile = ${form};`, + ); + const scopedTree = { + ...tree, + entries: { + "main.dcdl": { + ...tree.entries["main.dcdl"], + content: scopedContent, + content_digest: await digestText(scopedContent), + }, + "scoped.dcdl": { + path: "scoped.dcdl", + content_type: "decodal", + content: recipe, + content_digest: await digestText(recipe), + }, + }, + }; + assertEquals( + analyze_snapshot(scopedTree, "main.dcdl", undefined), + [], + form, + ); + const result = evaluate_snapshot( + scopedTree, + valueContract, + ) as typeof evaluated; + assertEquals( + result.projections[0].data_json.profile.entries[0].profile, + form.includes("patched") + ? { ...expected, description: "patched" } + : expected, + ); + } + } // Neither completion nor diagnostic shape checking changes the saved value. assertEquals(evaluate_snapshot(tree, valueContract), evaluated); }); From e24fbabe54ca01f5c89557e2abf9ef16dcadb818 Mon Sep 17 00:00:00 2001 From: Hare Date: Wed, 7 Oct 2026 02:51:29 +0900 Subject: [PATCH 7/9] fix: align Profile authoring with compaction and reasoning contracts (T-710) Resolve MR seq9 supported-compaction-fields and numeric-extraction-reasoning findings. Cover all canonical CompactionConfigPartial settings, token aliases and ratio helpers; accept string effort and signed integer extraction budgets without materializing editor defaults. Audit the remaining Profile field groups against their real serde contracts. Share an actual-provider value corpus across native, generated WASM, and shared save/projection/verified Runtime archive consumption. Prove inline/import/patch exact values and effective Manifest settings; gate canonical compaction field coverage against serialization of the actual partial type. Keep prior scope union and worker/feature diagnostics fixes intact. Validation: root cargo check; config-source40; unfiltered server644+11; Runtime340+17+17; API typescript77; Deno39; deno task check0/0; wasm32 release regeneration byte-identical with wasm-bindgen0.2.117/Binaryen132; cargo fmt/diff checks. Logs target/t710-contract-{check,config-source,server,api,runtime}.log. Reviewer seq9 independently confirmed seq3/5/7 resolution and correct main/relative-cwd routing. No target integration/ref mutations. --- crates/config-source/src/lib.rs | 104 ++++++++++++++ .../src/server/tests/value_profiles_tests.rs | 135 ++++++++++++++++++ docs/design/profiles-manifests-prompts.md | 2 + .../profile-authoring-values.json | 116 +++++++++++++++ .../config-schema/profile-authoring.dcdl | 17 ++- .../test/config-source/wasm-parity.test.ts | 95 ++++++++++++ 6 files changed, 466 insertions(+), 3 deletions(-) create mode 100644 resources/config-schema/profile-authoring-values.json diff --git a/crates/config-source/src/lib.rs b/crates/config-source/src/lib.rs index d5c1f7eb5..71264ef60 100644 --- a/crates/config-source/src/lib.rs +++ b/crates/config-source/src/lib.rs @@ -2738,6 +2738,110 @@ mod tests { } } + fn fixture_decodal(value: &serde_json::Value) -> String { + match value { + serde_json::Value::Object(fields) => format!( + "{{ {} }}", + fields + .iter() + .map(|(key, value)| format!("{key} = {};", fixture_decodal(value))) + .collect::>() + .join(" ") + ), + serde_json::Value::Array(values) => format!( + "[{}]", + values + .iter() + .map(fixture_decodal) + .collect::>() + .join(", ") + ), + serde_json::Value::Null => panic!("fixture omission must not be represented as null"), + value => value.to_string(), + } + } + + #[test] + fn actual_profile_authoring_value_corpus_preserves_supported_forms() { + let cases: Vec = serde_json::from_str(include_str!( + "../../../resources/config-schema/profile-authoring-values.json" + )) + .unwrap(); + for case in cases { + let recipe = fixture_decodal(&case["profile"]); + for (form, patched) in [ + (recipe.clone(), false), + (r#"import "./recipe.dcdl""#.into(), false), + ( + r#"(import "./recipe.dcdl") // { description = "patched"; }"#.into(), + true, + ), + ] { + let source = format!( + r#"{{ profile.entries = [{{ selector = "project:corpus"; profile = {form}; }}]; }} as WorkspaceConfigSchema"# + ); + let snapshot = ConfigTreeSnapshot::from_entries( + 10, + [entry("main.dcdl", &source), entry("recipe.dcdl", &recipe)], + ) + .unwrap(); + let schema = actual_profile_schema(); + let environment = + SnapshotEnvironment::new(snapshot).with_schema_bundle(schema.clone()); + let contract = + ToolchainContract::with_schema_bundle(1, vec![path("main.dcdl")], 1, schema); + let before = environment.evaluate_contract(&contract).unwrap(); + let diagnostics = environment.analyze(&path("main.dcdl"), None); + assert!( + diagnostics.is_empty(), + "{}: {form}: {diagnostics:?}", + case["name"] + ); + let mut expected = case["profile"].clone(); + if patched { + expected["description"] = serde_json::json!("patched"); + } + assert_eq!( + before.projections[0] + .data_json + .pointer("/profile/entries/0/profile"), + Some(&expected), + "{}", + case["name"] + ); + assert_eq!(environment.evaluate_contract(&contract).unwrap(), before); + } + } + let source = "{} as WorkspaceConfigSchema"; + let environment = SnapshotEnvironment::new( + ConfigTreeSnapshot::from_entries(10, [entry("main.dcdl", source)]).unwrap(), + ) + .with_schema_bundle(actual_profile_schema()); + for (profile_path, prefix, label) in [ + ("compaction", "reta", "retained_tokens"), + ("compaction", "prune_min", "prune_min_savings"), + ("compaction", "req", "request"), + ("compaction", "wor", "worker"), + ("compaction", "compact_ret", "compact_retained_tokens"), + ("feature.memory.extraction", "reas", "reasoning"), + ("feature.subjektiv.extraction", "reas", "reasoning"), + ] { + let marked = format!( + "{{ profile.entries = [{{ profile.{profile_path} = {{ {prefix}| }}; }}]; }} as WorkspaceConfigSchema" + ); + let cursor = marked.find('|').unwrap(); + let source = marked.replace('|', ""); + let result = environment + .complete_config(&path("main.dcdl"), &source, cursor, true) + .unwrap() + .unwrap(); + assert!( + result.items.iter().any(|item| item.label == label), + "{source}: {result:?}" + ); + } + } + #[test] fn completion_authoring_sources_compose_with_validation_source_fallbacks() { let schema = WorkspaceConfigSchemaBundle::compose([ diff --git a/crates/workspace-server/src/server/tests/value_profiles_tests.rs b/crates/workspace-server/src/server/tests/value_profiles_tests.rs index 72c72f6ad..b375fdeb9 100644 --- a/crates/workspace-server/src/server/tests/value_profiles_tests.rs +++ b/crates/workspace-server/src/server/tests/value_profiles_tests.rs @@ -303,6 +303,141 @@ async fn value_profiles_scalar_and_table_scopes_analyze_save_and_resolve_equally assert_eq!(scopes[1], scopes[2]); } +fn profile_fixture_decodal(value: &serde_json::Value) -> String { + match value { + serde_json::Value::Object(fields) => format!( + "{{ {} }}", + fields + .iter() + .map(|(key, value)| format!("{key} = {};", profile_fixture_decodal(value))) + .collect::>() + .join(" ") + ), + serde_json::Value::Array(values) => format!( + "[{}]", + values + .iter() + .map(profile_fixture_decodal) + .collect::>() + .join(", ") + ), + serde_json::Value::Null => panic!("fixture omission must not be represented as null"), + value => value.to_string(), + } +} + +#[tokio::test] +async fn value_profiles_authoring_corpus_preserves_effective_runtime_settings() { + let cases: Vec = serde_json::from_str(include_str!( + "../../../../../resources/config-schema/profile-authoring-values.json" + )) + .unwrap(); + // The canonical token fixture must exercise every serializable setting, + // so adding a field to CompactionConfigPartial cannot silently outgrow the + // editor-shape coverage. Aliases/ratio helpers have separate corpus cases. + let default = serde_json::to_value(manifest::CompactionConfigPartial::default()).unwrap(); + let canonical = cases + .iter() + .find(|case| case["name"] == "canonical_compaction_tokens") + .unwrap(); + assert_eq!( + default + .as_object() + .unwrap() + .keys() + .collect::>(), + canonical["profile"]["compaction"] + .as_object() + .unwrap() + .keys() + .filter(|name| name.as_str() != "kind") + .collect() + ); + for case in cases { + let dir = tempfile::tempdir().unwrap(); + let api = test_api(dir.path()).await; + let recipe = profile_fixture_decodal(&case["profile"]); + for (index, (form, patched)) in [ + (recipe.clone(), false), + (r#"import "./recipe.dcdl""#.into(), false), + ( + r#"(import "./recipe.dcdl") // { description = "patched"; }"#.into(), + true, + ), + ] + .iter() + .enumerate() + { + let extra = if index == 0 { + vec![config_source::ConfigTreeChange::Create { + path: config_source::VirtualPath::parse("recipe.dcdl").unwrap(), + content_type: config_source::ConfigContentType::Decodal, + content: recipe.clone(), + }] + } else { + vec![] + }; + let authored = format!("{} as WorkspaceConfigSchema", source(form)); + let state = commit_workspace_config_tree( + &api, + TEST_WORKSPACE_ID, + &request(&api, &authored, extra), + ) + .unwrap(); + let environment = config_source::SnapshotEnvironment::new(state.snapshot.clone()) + .with_schema_bundle(state.contract.schema_bundle.clone()); + let diagnostics = environment.analyze( + &config_source::VirtualPath::parse("main.dcdl").unwrap(), + None, + ); + assert!(diagnostics.is_empty(), "{}: {diagnostics:?}", case["name"]); + let mut expected = case["profile"].clone(); + if *patched { + expected["description"] = serde_json::json!("patched"); + } + let evaluated = environment.evaluate_contract(&state.contract).unwrap(); + assert_eq!( + evaluated.projections[0] + .data_json + .pointer("/profile/entries/0/profile"), + Some(&expected) + ); + let projection = + project_profiles_from_workspace_config(TEST_WORKSPACE_ID, &state).unwrap(); + let bundle = build_virtual_profile_config_bundle( + &projection, + &state, + TEST_WORKSPACE_ID, + "created", + "project:alpha", + ) + .unwrap() + .unwrap(); + let resolved = bundle + .profile_source_archive + .as_ref() + .unwrap() + .verify() + .unwrap() + .resolve_profile( + "project:alpha", + Path::new("/runtime-worker"), + "corpus-worker", + ) + .unwrap(); + let manifest = serde_json::to_value(resolved).unwrap(); + for (pointer, value) in case["manifest_expect"].as_object().unwrap() { + assert_eq!( + manifest.pointer(pointer), + Some(value), + "{}: {pointer}", + case["name"] + ); + } + } + } +} + #[tokio::test] async fn value_profiles_invalid_saves_preserve_the_active_config() { let dir = tempfile::tempdir().unwrap(); diff --git a/docs/design/profiles-manifests-prompts.md b/docs/design/profiles-manifests-prompts.md index f89c33192..f791eb084 100644 --- a/docs/design/profiles-manifests-prompts.md +++ b/docs/design/profiles-manifests-prompts.md @@ -47,6 +47,8 @@ The schema contribution separates its materialized registration schema from an o `scope` and `delegation_scope` retain both supported forms: a string such as `"workspace_read"` / `"workspace_write"`, or a table such as `{ intent = "workspace_read"; deny_write = ["private"]; symlink_policy = "resolved"; }`. Decodal 0.4 has no union range, so these fields are opaque to the editor constraint pass; a non-materialized table hint supplies nested completion without rejecting the string form or rewriting it. The shared Backend save validator checks scope forms, intent values and table fields (including their types and unknown keys). Other structurally typed fields such as `worker.mode` and `feature.task.enabled` retain editor type diagnostics. Completion hints never become Profile defaults or saved values. +Extraction `reasoning` in both `feature.memory` and `feature.subjektiv`, like `engine.reasoning`, accepts either a string effort or a signed integer budget. These union values are also opaque to editor constraints and semantically checked on save. Compaction authoring covers the complete token settings and their supported `compact_*` aliases, plus the ratio helper's `request` / `worker` aliases; values are not renamed or defaulted during Workspace evaluation. The shared test corpus `resources/config-schema/profile-authoring-values.json` proves exact values in native/WASM analysis and effective settings through save/projection/Runtime consumption. Its canonical compaction case must cover every serialized `CompactionConfigPartial` field, making future contract drift visible in tests. + ## Manifests A resolved Manifest is the concrete contract used to create or restore a Worker. It carries defaults, resolved paths, permissions, scope, prompt references, provider/model decisions, and runtime identity. diff --git a/resources/config-schema/profile-authoring-values.json b/resources/config-schema/profile-authoring-values.json new file mode 100644 index 000000000..4038f72f4 --- /dev/null +++ b/resources/config-schema/profile-authoring-values.json @@ -0,0 +1,116 @@ +[ + { + "name": "canonical_compaction_tokens", + "profile": { + "compaction": { + "kind": "tokens", + "prune_enabled": false, + "prune_protected_tokens": 3000, + "prune_min_savings": 1000, + "threshold": 80000, + "request_threshold": 90000, + "retained_tokens": 12000, + "overview_target_tokens": 1000, + "overview_warning_tokens": 2000, + "overview_deadline_tokens": 3000, + "worker_context_max_tokens": 20000, + "finish_warning_remaining_tokens": 1000, + "final_reserve_tokens": 2000, + "worker_max_turns": 5, + "summary_target_tokens": 1000, + "summary_max_tokens": 2000, + "auto_read_budget_tokens": 500, + "result_context_max_tokens": 4000, + "model": { "ref": "codex-oauth/gpt-5.6-sol" } + } + }, + "manifest_expect": { + "/compaction/prune_enabled": false, + "/compaction/prune_protected_tokens": 3000, + "/compaction/prune_min_savings": 1000, + "/compaction/threshold": 80000, + "/compaction/request_threshold": 90000, + "/compaction/retained_tokens": 12000, + "/compaction/overview_target_tokens": 1000, + "/compaction/overview_warning_tokens": 2000, + "/compaction/overview_deadline_tokens": 3000, + "/compaction/worker_context_max_tokens": 20000, + "/compaction/finish_warning_remaining_tokens": 1000, + "/compaction/final_reserve_tokens": 2000, + "/compaction/worker_max_turns": 5, + "/compaction/summary_target_tokens": 1000, + "/compaction/summary_max_tokens": 2000, + "/compaction/auto_read_budget_tokens": 500, + "/compaction/result_context_max_tokens": 4000, + "/compaction/model/ref": "codex-oauth/gpt-5.6-sol" + } + }, + { + "name": "compaction_token_aliases_without_kind", + "profile": { + "compaction": { + "compact_threshold": 80000, + "compact_request_threshold": 90000, + "compact_retained_tokens": 12000, + "compact_worker_max_input_tokens": 20000, + "compact_worker_max_turns": 5, + "compact_auto_read_budget": 500 + } + }, + "manifest_expect": { + "/compaction/threshold": 80000, + "/compaction/request_threshold": 90000, + "/compaction/retained_tokens": 12000, + "/compaction/worker_context_max_tokens": 20000, + "/compaction/worker_max_turns": 5, + "/compaction/auto_read_budget_tokens": 500 + } + }, + { + "name": "compaction_ratio_aliases", + "profile": { + "model": { "context_window": 100000 }, + "compaction": { + "kind": "ratio", + "threshold": 0.9, + "request": 0.8, + "worker": 0.2 + } + }, + "manifest_expect": { + "/compaction/threshold": 90000, + "/compaction/request_threshold": 80000, + "/compaction/worker_context_max_tokens": 20000 + } + }, + { + "name": "numeric_extraction_reasoning", + "profile": { + "engine": { "reasoning": -1 }, + "feature": { + "memory": { "extraction": { "enabled": true, "reasoning": 4096 } }, + "subjektiv": { "extraction": { "enabled": true, "reasoning": 4096 } } + } + }, + "manifest_expect": { + "/engine/reasoning": -1, + "/feature/memory/profile/extraction/reasoning": 4096, + "/feature/subjektiv/profile/extraction/reasoning": 4096 + } + }, + { + "name": "string_extraction_reasoning", + "profile": { + "feature": { + "memory": { "extraction": { "enabled": true, "reasoning": "high" } }, + "subjektiv": { + "extraction": { "enabled": true, "reasoning": "medium" } + } + } + }, + "manifest_expect": { + "/feature/memory/profile/extraction/reasoning": "high", + "/feature/subjektiv/profile/extraction/reasoning": "medium" + } + } +] diff --git a/resources/config-schema/profile-authoring.dcdl b/resources/config-schema/profile-authoring.dcdl index ffd1b5e83..d2cb97ce3 100644 --- a/resources/config-schema/profile-authoring.dcdl +++ b/resources/config-schema/profile-authoring.dcdl @@ -9,7 +9,8 @@ let capability = Unknown; context_window = Int; max_context_window = Int; }; Extraction = { - enabled = Bool; model = Model; reasoning = String; + # ReasoningControl accepts string effort or signed integer budget. + enabled = Bool; model = Model; reasoning = Unknown; threshold = Int; worker_max_turns = Int; }; # Profile scope accepts either a string intent or a table. Decodal 0.4 has @@ -56,9 +57,19 @@ let env = { inherit = [...String]; set = { ...Unknown }; }; }]; }; compaction = { + # Token settings (CompactionConfigPartial) and ratio helper fields. + # Threshold/context values accept integer tokens or numeric ratios. kind = String; threshold = Unknown; request_threshold = Unknown; - prune_enabled = Bool; worker_context_max_tokens = Unknown; - worker_max_turns = Int; model = Model; + worker_context_max_tokens = Unknown; request = Unknown; worker = Unknown; + prune_enabled = Bool; prune_protected_tokens = Int; prune_min_savings = Int; + retained_tokens = Int; + overview_target_tokens = Int; overview_warning_tokens = Int; overview_deadline_tokens = Int; + finish_warning_remaining_tokens = Int; final_reserve_tokens = Int; + worker_max_turns = Int; summary_target_tokens = Int; summary_max_tokens = Int; + auto_read_budget_tokens = Int; result_context_max_tokens = Int; model = Model; + # Supported token aliases remain authorable without rewriting recipes. + compact_threshold = Int; compact_request_threshold = Int; compact_retained_tokens = Int; + compact_worker_max_input_tokens = Int; compact_worker_max_turns = Int; compact_auto_read_budget = Int; }; web = { enabled = Bool; allow_private_addresses = Bool; diff --git a/web/workspace/test/config-source/wasm-parity.test.ts b/web/workspace/test/config-source/wasm-parity.test.ts index be59ab095..905303f1d 100644 --- a/web/workspace/test/config-source/wasm-parity.test.ts +++ b/web/workspace/test/config-source/wasm-parity.test.ts @@ -382,6 +382,22 @@ Deno.test("generated WASM completes asserted WorkspaceConfigSchema keys", () => assertEquals(result.items.some((item) => item.label === "profile"), true); }); +function fixtureDecodal(value: unknown): string { + if (Array.isArray(value)) return `[${value.map(fixtureDecodal).join(", ")}]`; + if (value !== null && typeof value === "object") { + return `{ ${ + Object.entries(value).map(([key, value]) => + `${key} = ${fixtureDecodal(value)};` + ).join(" ") + } }`; + } + if ( + typeof value === "string" || typeof value === "number" || + typeof value === "boolean" + ) return JSON.stringify(value); + throw new Error("fixture omission must not be represented as null/undefined"); +} + Deno.test("generated WASM authors value-based Profiles using the Backend schema without materializing authoring fields", async () => { const source = await Deno.readTextFile( new URL( @@ -656,6 +672,85 @@ Deno.test("generated WASM authors value-based Profiles using the Backend schema ); } } + const cases = JSON.parse( + await Deno.readTextFile( + new URL( + "../../../../resources/config-schema/profile-authoring-values.json", + import.meta.url, + ), + ), + ) as Array<{ name: string; profile: Record }>; + for (const fixture of cases) { + const recipe = fixtureDecodal(fixture.profile); + for ( + const [form, patched] of [[recipe, false], [ + 'import "./recipe.dcdl"', + false, + ], [ + '(import "./recipe.dcdl") // { description = "patched"; }', + true, + ]] as const + ) { + const fixtureContent = content.replace( + "profile = {};", + `profile = ${form};`, + ); + const fixtureTree = { + ...tree, + entries: { + "main.dcdl": { + ...tree.entries["main.dcdl"], + content: fixtureContent, + content_digest: await digestText(fixtureContent), + }, + "recipe.dcdl": { + path: "recipe.dcdl", + content_type: "decodal", + content: recipe, + content_digest: await digestText(recipe), + }, + }, + }; + assertEquals( + analyze_snapshot(fixtureTree, "main.dcdl", undefined), + [], + `${fixture.name}: ${form}`, + ); + const result = evaluate_snapshot( + fixtureTree, + valueContract, + ) as typeof evaluated; + assertEquals( + result.projections[0].data_json.profile.entries[0].profile, + patched + ? { ...fixture.profile, description: "patched" } + : fixture.profile, + ); + } + } + for ( + const [path, prefix, label] of [ + ["compaction", "reta", "retained_tokens"], + ["compaction", "prune_min", "prune_min_savings"], + ["compaction", "req", "request"], + ["compaction", "wor", "worker"], + ["compaction", "compact_ret", "compact_retained_tokens"], + ["feature.memory.extraction", "reas", "reasoning"], + ["feature.subjektiv.extraction", "reas", "reasoning"], + ] + ) { + const body = + `{ profile.entries = [{ profile.${path} = { ${prefix} } }]; } as WorkspaceConfigSchema`; + const cursor = body.lastIndexOf(prefix) + prefix.length; + const result = complete_current("main.dcdl", body, cursor, true) as { + items: Array<{ label: string }>; + }; + assertEquals( + result.items.some((item) => item.label === label), + true, + `${path}.${label}`, + ); + } // Neither completion nor diagnostic shape checking changes the saved value. assertEquals(evaluate_snapshot(tree, valueContract), evaluated); }); From 4343be86c113ad30fe476f6cdde7066e1e6ab55c Mon Sep 17 00:00:00 2001 From: Hare Date: Wed, 7 Oct 2026 03:35:59 +0900 Subject: [PATCH 8/9] feat(config-source): add confined read-only builtin Decodal imports Share the explicit embedded Profile catalog with native and WASM evaluators. Preserve ordinary Decodal values, isolate builtin relative imports, reject reserved Workspace mutations, and capture the exact loaded builtin closure. Guard lazy import cycles and dependency depth before evaluation recurses. Validation: config-source 48 tests, builtin-source 2 tests, manifest 185 tests; root cargo check, formatting and diff checks pass. Existing contract version/fingerprint stay unchanged; archive provenance binds builtin content instead of invalidating stored Workspace metadata. --- Cargo.lock | 9 + Cargo.toml | 3 + crates/builtin-source/Cargo.toml | 9 + crates/builtin-source/src/lib.rs | 214 ++++++++++++ crates/builtin-source/tests/catalog.rs | 41 +++ crates/config-source/Cargo.toml | 1 + crates/config-source/src/lib.rs | 449 ++++++++++++++++++++++++- crates/manifest/Cargo.toml | 1 + crates/manifest/src/builtin_profile.rs | 205 +---------- 9 files changed, 721 insertions(+), 211 deletions(-) create mode 100644 crates/builtin-source/Cargo.toml create mode 100644 crates/builtin-source/src/lib.rs create mode 100644 crates/builtin-source/tests/catalog.rs diff --git a/Cargo.lock b/Cargo.lock index b23475786..8aa84f3b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -543,6 +543,13 @@ dependencies = [ "serde", ] +[[package]] +name = "builtin-source" +version = "0.1.0" +dependencies = [ + "sha2 0.11.0", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -766,6 +773,7 @@ dependencies = [ name = "config-source" version = "0.1.0" dependencies = [ + "builtin-source", "decodal", "decodal-language-service", "decodal-language-tools", @@ -2598,6 +2606,7 @@ version = "0.1.0" dependencies = [ "agen", "arc-swap", + "builtin-source", "decodal", "protocol", "secrets", diff --git a/Cargo.toml b/Cargo.toml index a573ccb12..07b6674a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ members = [ "crates/tools", "crates/fs-operation", "crates/flow", + "crates/builtin-source", "crates/config-source", "crates/config-source-wasm", "crates/workdir", @@ -63,6 +64,7 @@ default-members = [ "crates/tools", "crates/fs-operation", "crates/flow", + "crates/builtin-source", "crates/config-source", "crates/config-source-wasm", "crates/workdir", @@ -116,6 +118,7 @@ runtime-api = { path = "crates/runtime-api" } server-api = { path = "crates/server-api" } secrets = { path = "crates/secrets" } tools = { path = "crates/tools" } +builtin-source = { path = "crates/builtin-source" } config-source = { path = "crates/config-source" } fs-operation = { path = "crates/fs-operation" } workdir = { path = "crates/workdir" } diff --git a/crates/builtin-source/Cargo.toml b/crates/builtin-source/Cargo.toml new file mode 100644 index 000000000..be1dcf665 --- /dev/null +++ b/crates/builtin-source/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "builtin-source" +version = "0.1.0" +edition.workspace = true +license.workspace = true +publish = false + +[dependencies] +sha2.workspace = true diff --git a/crates/builtin-source/src/lib.rs b/crates/builtin-source/src/lib.rs new file mode 100644 index 000000000..e539484ab --- /dev/null +++ b/crates/builtin-source/src/lib.rs @@ -0,0 +1,214 @@ +//! Explicit compile-time public Decodal source catalog, shared by native and WASM hosts. +use sha2::{Digest, Sha256}; +use std::collections::BTreeMap; + +pub const BUILTIN_PROFILE_CATALOG_ID: &str = "builtin-profiles-v2"; +pub const BUILTIN_DEFAULT_PROFILE: &str = "builtin:default"; +pub const BUILTIN_STANDALONE_PROFILE: &str = "builtin:standalone"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BuiltinProfileImport { + pub specifier: &'static str, + pub resolved_path: &'static str, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BuiltinProfileResource { + pub selector: Option<&'static str>, + pub path: &'static str, + pub source: &'static str, + pub description: &'static str, + pub imports: &'static [BuiltinProfileImport], +} + +const BASE_PATH: &str = "profiles/base.dcdl"; +pub const DEFAULT_PATH: &str = "profiles/default.dcdl"; +const BASE_IMPORT: &[BuiltinProfileImport] = &[BuiltinProfileImport { + specifier: "./base.dcdl", + resolved_path: BASE_PATH, +}]; +const DEFAULT_IMPORT: &[BuiltinProfileImport] = &[BuiltinProfileImport { + specifier: "./default.dcdl", + resolved_path: DEFAULT_PATH, +}]; +const NO_IMPORTS: &[BuiltinProfileImport] = &[]; + +pub const BUILTIN_PROFILE_RESOURCES: &[BuiltinProfileResource] = &[ + BuiltinProfileResource { + selector: None, + path: BASE_PATH, + source: include_str!("../../../resources/profiles/base.dcdl"), + description: "Shared built-in Profile defaults.", + imports: NO_IMPORTS, + }, + BuiltinProfileResource { + selector: Some(BUILTIN_DEFAULT_PROFILE), + path: DEFAULT_PATH, + source: include_str!("../../../resources/profiles/default.dcdl"), + description: "Default Yoi coding profile.", + imports: BASE_IMPORT, + }, + BuiltinProfileResource { + selector: Some(BUILTIN_STANDALONE_PROFILE), + path: "profiles/standalone.dcdl", + source: include_str!("../../../resources/profiles/standalone.dcdl"), + description: "Standalone Yoi coding profile.", + imports: DEFAULT_IMPORT, + }, + BuiltinProfileResource { + selector: Some("builtin:standalone-subjektiv"), + path: "profiles/standalone-subjektiv.dcdl", + source: include_str!("../../../resources/profiles/standalone-subjektiv.dcdl"), + description: "Standalone with an explicitly connected local Subject.", + imports: &[BuiltinProfileImport { + specifier: "./standalone.dcdl", + resolved_path: "profiles/standalone.dcdl", + }], + }, + BuiltinProfileResource { + selector: Some("builtin:standalone-subjektiv-consolidation"), + path: "profiles/standalone-subjektiv-consolidation.dcdl", + source: include_str!("../../../resources/profiles/standalone-subjektiv-consolidation.dcdl"), + description: "Delegated local Subject Job policy.", + imports: &[BuiltinProfileImport { + specifier: "./job.dcdl", + resolved_path: "profiles/job.dcdl", + }], + }, + BuiltinProfileResource { + selector: Some("builtin:coder"), + path: "profiles/coder.dcdl", + source: include_str!("../../../resources/profiles/coder.dcdl"), + description: "Ticket implementation with direct Reviewer SubWorkers.", + imports: BASE_IMPORT, + }, + BuiltinProfileResource { + selector: Some("builtin:companion"), + path: "profiles/companion.dcdl", + source: include_str!("../../../resources/profiles/companion.dcdl"), + description: "General assistance with Workspace tools.", + imports: BASE_IMPORT, + }, + BuiltinProfileResource { + selector: Some("builtin:intake"), + path: "profiles/intake.dcdl", + source: include_str!("../../../resources/profiles/intake.dcdl"), + description: "Read-only intake and planning.", + imports: BASE_IMPORT, + }, + BuiltinProfileResource { + selector: Some("builtin:reviewer"), + path: "profiles/reviewer.dcdl", + source: include_str!("../../../resources/profiles/reviewer.dcdl"), + description: "Independent review of a published Merge Request source.", + imports: BASE_IMPORT, + }, + BuiltinProfileResource { + selector: Some("builtin:orchestrator"), + path: "profiles/orchestrator.dcdl", + source: include_str!("../../../resources/profiles/orchestrator.dcdl"), + description: "Workspace orchestration and Worker control.", + imports: BASE_IMPORT, + }, + BuiltinProfileResource { + selector: Some("builtin:job"), + path: "profiles/job.dcdl", + source: include_str!("../../../resources/profiles/job.dcdl"), + description: "Result-only caller-selected Job policy.", + imports: NO_IMPORTS, + }, + BuiltinProfileResource { + selector: Some("builtin:backend-job"), + path: "profiles/backend-job.dcdl", + source: include_str!("../../../resources/profiles/backend-job.dcdl"), + description: "Backend-owned bounded Job Worker.", + imports: BASE_IMPORT, + }, + BuiltinProfileResource { + selector: Some("builtin:memory-consolidation"), + path: "profiles/memory-consolidation.dcdl", + source: include_str!("../../../resources/profiles/memory-consolidation.dcdl"), + description: "Internal Memory consolidation service.", + imports: BASE_IMPORT, + }, + BuiltinProfileResource { + selector: Some("builtin:subjektiv-memory-consolidation"), + path: "profiles/subjektiv-memory-consolidation.dcdl", + source: include_str!("../../../resources/profiles/subjektiv-memory-consolidation.dcdl"), + description: "Delegated subject Memory consolidation service.", + imports: BASE_IMPORT, + }, +]; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BuiltinProfileCatalogSnapshot { + pub id: &'static str, + pub sources: BTreeMap, + pub entrypoints: BTreeMap, + pub imports: BTreeMap, +} + +impl BuiltinProfileCatalogSnapshot { + pub fn digest(&self) -> String { + let mut hasher = Sha256::new(); + hasher.update(self.id.as_bytes()); + for (path, source) in &self.sources { + hasher.update((path.len() as u64).to_le_bytes()); + hasher.update(path.as_bytes()); + hasher.update((source.len() as u64).to_le_bytes()); + hasher.update(source.as_bytes()); + } + for (selector, path) in &self.entrypoints { + hasher.update((selector.len() as u64).to_le_bytes()); + hasher.update(selector.as_bytes()); + hasher.update((path.len() as u64).to_le_bytes()); + hasher.update(path.as_bytes()); + } + for (request, resolved_path) in &self.imports { + hasher.update((request.len() as u64).to_le_bytes()); + hasher.update(request.as_bytes()); + hasher.update((resolved_path.len() as u64).to_le_bytes()); + hasher.update(resolved_path.as_bytes()); + } + format!( + "sha256:{}", + hasher + .finalize() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::() + ) + } +} + +pub fn builtin_profile_catalog_snapshot() -> BuiltinProfileCatalogSnapshot { + let mut sources = BTreeMap::new(); + let mut entrypoints = BTreeMap::new(); + let mut imports = BTreeMap::new(); + + for resource in BUILTIN_PROFILE_RESOURCES { + sources.insert(resource.path.to_owned(), resource.source.to_owned()); + for import in resource.imports { + imports.insert( + format!("{}\0{}", resource.path, import.specifier), + import.resolved_path.to_owned(), + ); + } + if let Some(selector) = resource.selector { + entrypoints.insert(selector.to_owned(), resource.path.to_owned()); + } + } + + BuiltinProfileCatalogSnapshot { + id: BUILTIN_PROFILE_CATALOG_ID, + sources, + entrypoints, + imports, + } +} + +pub fn builtin_profile_entrypoints() -> impl Iterator { + BUILTIN_PROFILE_RESOURCES + .iter() + .filter(|resource| resource.selector.is_some()) +} diff --git a/crates/builtin-source/tests/catalog.rs b/crates/builtin-source/tests/catalog.rs new file mode 100644 index 000000000..55ead2cb9 --- /dev/null +++ b/crates/builtin-source/tests/catalog.rs @@ -0,0 +1,41 @@ +use builtin_source::{BUILTIN_PROFILE_RESOURCES, builtin_profile_catalog_snapshot}; +use std::collections::BTreeSet; + +#[test] +fn public_catalog_is_closed_and_explicit() { + let catalog = builtin_profile_catalog_snapshot(); + assert_eq!(catalog.sources.len(), BUILTIN_PROFILE_RESOURCES.len()); + assert!(catalog.digest().starts_with("sha256:")); + for resource in BUILTIN_PROFILE_RESOURCES { + assert!(resource.path.starts_with("profiles/")); + assert!(resource.path.ends_with(".dcdl")); + assert_eq!(catalog.sources[resource.path], resource.source); + for import in resource.imports { + assert!(catalog.sources.contains_key(import.resolved_path)); + assert!( + resource + .source + .contains(&format!("import {:?}", import.specifier)) + ); + } + } + assert!(!catalog.sources.contains_key("prompts/default.md")); +} + +#[test] +fn catalog_import_metadata_has_no_cycles() { + fn visit(path: &str, visiting: &mut BTreeSet) { + assert!(visiting.insert(path.to_string()), "catalog cycle at {path}"); + let resource = BUILTIN_PROFILE_RESOURCES + .iter() + .find(|resource| resource.path == path) + .unwrap(); + for import in resource.imports { + visit(import.resolved_path, visiting); + } + visiting.remove(path); + } + for resource in BUILTIN_PROFILE_RESOURCES { + visit(resource.path, &mut BTreeSet::new()); + } +} diff --git a/crates/config-source/Cargo.toml b/crates/config-source/Cargo.toml index b9ee3ecf7..61fe280e5 100644 --- a/crates/config-source/Cargo.toml +++ b/crates/config-source/Cargo.toml @@ -6,6 +6,7 @@ license.workspace = true publish = false [dependencies] +builtin-source.workspace = true decodal.workspace = true decodal-language-service.workspace = true decodal-language-tools.workspace = true diff --git a/crates/config-source/src/lib.rs b/crates/config-source/src/lib.rs index 71264ef60..0e377d0a9 100644 --- a/crates/config-source/src/lib.rs +++ b/crates/config-source/src/lib.rs @@ -1,5 +1,8 @@ use std::collections::{BTreeMap, BTreeSet}; use std::fmt; +use std::sync::{Arc, Mutex}; + +use builtin_source::BUILTIN_PROFILE_RESOURCES; use decodal::{ Data, Diagnostic, DiagnosticKind, Engine, HostEnvironment, ImportCandidate, ImportLoader, @@ -22,6 +25,7 @@ pub const MAX_CHANGE_COUNT: usize = 256; pub const MAX_ENTRY_BYTES: usize = 256 * 1024; pub const MAX_TOTAL_BYTES: usize = 4 * 1024 * 1024; pub const MAX_PATH_BYTES: usize = 512; +pub const MAX_IMPORT_DEPTH: usize = 32; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, ts_rs::TS)] pub struct VirtualPath(String); @@ -249,6 +253,7 @@ impl ConfigEntry { content_type: ConfigContentType, content: impl Into, ) -> Result { + validate_workspace_path(&path)?; let content = content.into(); if content.len() > MAX_ENTRY_BYTES { return Err(ConfigTreeError::LimitExceeded("entry bytes")); @@ -282,6 +287,10 @@ impl ConfigTreeSnapshot { let mut ordered = BTreeMap::new(); let mut total = 0usize; for entry in entries { + validate_workspace_path(&entry.path)?; + if entry.content.len() > MAX_ENTRY_BYTES { + return Err(ConfigTreeError::LimitExceeded("entry bytes")); + } total = total .checked_add(entry.content.len()) .ok_or(ConfigTreeError::LimitExceeded("total bytes"))?; @@ -363,6 +372,7 @@ impl ConfigTreeSnapshot { let mut touched = BTreeSet::new(); for change in changes { for path in change.paths() { + validate_workspace_path(path)?; if !touched.insert(path.clone()) { return Err(ConfigTreeError::PathChangedMoreThanOnce(path.clone())); } @@ -1086,6 +1096,26 @@ impl SnapshotEnvironment { pub fn evaluate_contract( &self, contract: &ToolchainContract, + ) -> Result> { + self.evaluate_with_builtin_sources(contract, Arc::default()) + } + + /// Capture only builtin sources actually loaded by evaluation, including transitive imports. + /// These immutable bytes are provenance for the evaluated value, not Runtime inputs. + pub fn builtin_import_sources( + &self, + contract: &ToolchainContract, + ) -> Result, Vec> { + let sources = Arc::default(); + self.evaluate_with_builtin_sources(contract, Arc::clone(&sources))?; + let captured = sources.lock().expect("builtin source capture lock").clone(); + Ok(captured) + } + + fn evaluate_with_builtin_sources( + &self, + contract: &ToolchainContract, + sources: Arc>>, ) -> Result> { if let Err(error) = contract.validate() { return Err(vec![self.config_error( @@ -1144,9 +1174,9 @@ impl SnapshotEnvironment { "configured entrypoint is not Decodal source", )]); } - let mut engine = decodal::Engine::new(SnapshotImportLoader { - snapshot: self.snapshot.clone(), - }); + let mut loader = SnapshotImportLoader::new(self.snapshot.clone()); + loader.builtin_sources = sources; + let mut engine = decodal::Engine::new(loader); let schema_module = engine .add_root_source( WORKSPACE_CONFIG_SCHEMA_SOURCE, @@ -1424,9 +1454,7 @@ impl HostEnvironment for &SnapshotEnvironment { type Loader = SnapshotImportLoader; fn create_loader(&self) -> Self::Loader { - SnapshotImportLoader { - snapshot: self.snapshot.clone(), - } + SnapshotImportLoader::new(self.snapshot.clone()) } fn configure_engine(&self, engine: &mut Engine) -> decodal::Result<()> { @@ -1532,9 +1560,18 @@ impl HostEnvironment for ConfigAuthoringEnvironment<'_> { #[derive(Debug, Clone)] pub struct SnapshotImportLoader { snapshot: ConfigTreeSnapshot, + builtin_sources: Arc>>, + import_edges: BTreeMap>, } impl SnapshotImportLoader { + fn new(snapshot: ConfigTreeSnapshot) -> Self { + Self { + snapshot, + builtin_sources: Arc::default(), + import_edges: BTreeMap::new(), + } + } pub fn resolve( &self, current_key: Option<&str>, @@ -1555,6 +1592,70 @@ impl ImportLoader for SnapshotImportLoader { specifier: &str, ) -> decodal::Result { let path = self.resolve(current_key, specifier).map_err(import_error)?; + // Guard the import graph before returning a module to Decodal. In particular, + // cycles hidden inside lazy object fields must not recurse during materialization. + let current = current_key + .expect("resolve requires a source context") + .split("@sha256:") + .next() + .unwrap(); + if self + .import_edges + .entry(current.to_string()) + .or_default() + .insert(path.to_string()) + { + let mut depths = BTreeMap::new(); + let validation = self.import_edges.keys().try_for_each(|root| { + validate_import_graph(&self.import_edges, root, &mut BTreeSet::new(), &mut depths) + .map(|_| ()) + }); + if let Err(diagnostic) = validation { + // A language service can keep using its loader after a diagnostic. + // Only successful edges may be cached as already validated. + self.import_edges + .get_mut(current) + .unwrap() + .remove(path.as_str()); + return Err(diagnostic); + } + } + if is_builtin_path(&path) { + let public_path = path.as_str().strip_prefix("$builtin/").unwrap_or(""); + let resource = BUILTIN_PROFILE_RESOURCES + .iter() + .find(|resource| resource.path == public_path) + .ok_or_else(|| { + Diagnostic::new( + DiagnosticKind::Import, + Span::default(), + format!("unknown or non-public read-only builtin source: {path}"), + ) + })?; + let mut captured = self + .builtin_sources + .lock() + .expect("builtin source capture lock"); + captured.insert(path.to_string(), resource.source.to_string()); + if captured.len() + self.snapshot.entries.len() > MAX_ENTRY_COUNT { + return Err(import_error(ConfigTreeError::LimitExceeded("entry count"))); + } + let bytes: usize = captured.values().map(String::len).sum::() + + self + .snapshot + .entries + .values() + .map(|entry| entry.content.len()) + .sum::(); + if resource.source.len() > MAX_ENTRY_BYTES || bytes > MAX_TOTAL_BYTES { + return Err(import_error(ConfigTreeError::LimitExceeded("total bytes"))); + } + return Ok(LoadedImport::source( + format!("{path}@{}", digest_bytes(resource.source.as_bytes())), + path.as_str(), + resource.source, + )); + } let entry = self.snapshot.get(&path).ok_or_else(|| { Diagnostic::new( DiagnosticKind::Import, @@ -1598,11 +1699,52 @@ impl ImportLoader for SnapshotImportLoader { .map_err(import_error)?; Ok(import_completions(&self.snapshot, current.as_ref(), prefix) .into_iter() - .map(|specifier| ImportCandidate::new(specifier).with_detail("virtual config source")) + .map(|specifier| { + let builtin = specifier.starts_with("$builtin/") + || current.as_ref().is_some_and(|p| is_builtin_path(p)); + ImportCandidate::new(specifier).with_detail(if builtin { + "read-only builtin Decodal source" + } else { + "virtual config source" + }) + }) .collect()) } } +fn validate_import_graph( + edges: &BTreeMap>, + current: &str, + visiting: &mut BTreeSet, + depths: &mut BTreeMap, +) -> decodal::Result { + if let Some(depth) = depths.get(current) { + return Ok(*depth); + } + if !visiting.insert(current.to_string()) { + return Err(Diagnostic::new( + DiagnosticKind::Cycle, + Span::default(), + format!("virtual config import cycle at {current}"), + )); + } + if visiting.len() > MAX_IMPORT_DEPTH + 1 { + return Err(import_error(ConfigTreeError::LimitExceeded("import depth"))); + } + let mut depth = 0; + if let Some(children) = edges.get(current) { + for child in children { + depth = depth.max(validate_import_graph(edges, child, visiting, depths)? + 1); + } + } + if depth > MAX_IMPORT_DEPTH { + return Err(import_error(ConfigTreeError::LimitExceeded("import depth"))); + } + visiting.remove(current); + depths.insert(current.to_string(), depth); + Ok(depth) +} + fn snapshot_import_cache_key(entry: &ConfigEntry) -> String { // The source id remains the virtual path for diagnostics and relative-import // resolution. The cache identity also includes immutable source content so @@ -1610,6 +1752,19 @@ fn snapshot_import_cache_key(entry: &ConfigEntry) -> String { format!("{}@{}", entry.path, entry.content_digest) } +fn is_builtin_path(path: &VirtualPath) -> bool { + path.as_str() == "$builtin" || path.as_str().starts_with("$builtin/") +} + +fn validate_workspace_path(path: &VirtualPath) -> Result<(), ConfigTreeError> { + if is_builtin_path(path) { + return Err(ConfigTreeError::InvalidPath(format!( + "{path}: builtin namespace is read-only" + ))); + } + Ok(()) +} + pub fn resolve_import( current: &VirtualPath, specifier: &str, @@ -1623,6 +1778,15 @@ pub fn resolve_import( { return Err(ConfigTreeError::InvalidImport(specifier.into())); } + let current_builtin = is_builtin_path(current); + let explicit_builtin = specifier == "$builtin" || specifier.starts_with("$builtin/"); + if current_builtin + && !explicit_builtin + && !specifier.starts_with("./") + && !specifier.starts_with("../") + { + return Err(ConfigTreeError::InvalidImport(specifier.into())); + } let mut components = if specifier.starts_with("./") || specifier.starts_with("../") { current.parent_components() } else { @@ -1632,6 +1796,9 @@ pub fn resolve_import( match component { "" | "." => {} ".." => { + if (current_builtin || explicit_builtin) && components.len() <= 1 { + return Err(ConfigTreeError::ImportEscape(specifier.into())); + } components .pop() .ok_or_else(|| ConfigTreeError::ImportEscape(specifier.into()))?; @@ -1648,6 +1815,26 @@ pub fn import_completions( prefix: &str, ) -> Vec { let mut candidates = BTreeSet::new(); + for resource in BUILTIN_PROFILE_RESOURCES { + let specifier = format!("$builtin/{}", resource.path); + if specifier.starts_with(prefix) { + candidates.insert(specifier); + } + if let Some(current) = current.filter(|path| is_builtin_path(path)) { + let parent = current.parent_components().join("/"); + if let Some(relative) = + format!("$builtin/{}", resource.path).strip_prefix(&format!("{parent}/")) + { + let relative = format!("./{relative}"); + if relative.starts_with(prefix) { + candidates.insert(relative); + } + } + } + } + if current.is_some_and(is_builtin_path) { + return candidates.into_iter().collect(); + } for path in snapshot.entries.keys() { if current == Some(path) { continue; @@ -3368,6 +3555,254 @@ mod tests { assert!(diagnostics[0].message.contains("skills/broken/SKILL.md")); } + #[test] + fn builtin_imports_are_ordinary_values_with_confined_transitive_sources() { + let snapshot = ConfigTreeSnapshot::from_entries(1, [ + entry("main.dcdl", r#"import "$builtin/profiles/companion.dcdl" // { worker = { mode = "wip"; }; feature = { workspace_config = { enabled = true; }; }; }"#), + entry("profiles/base.dcdl", "{ slug = \"workspace-shadow\"; }"), + ]).unwrap(); + let environment = SnapshotEnvironment::new(snapshot); + let contract = ToolchainContract::new(1, vec![path("main.dcdl")], 1); + let result = environment.evaluate_contract(&contract).unwrap(); + let value = &result.projections[0].data_json; + assert_eq!(value["slug"], "companion"); + assert_eq!(value["worker"]["mode"], "wip"); + assert_eq!(value["feature"]["workspace_config"]["enabled"], true); + assert_eq!(value["feature"]["ticket"]["enabled"], true); + let closure = environment.builtin_import_sources(&contract).unwrap(); + assert_eq!( + closure.keys().cloned().collect::>(), + vec![ + "$builtin/profiles/base.dcdl", + "$builtin/profiles/companion.dcdl" + ] + ); + assert_eq!( + closure["$builtin/profiles/base.dcdl"], + BUILTIN_PROFILE_RESOURCES[0].source + ); + + // Intersections conflict on explicit builtin values, exactly like any other import. + let snapshot = ConfigTreeSnapshot::from_entries( + 1, + [entry( + "main.dcdl", + r#"import "$builtin/profiles/companion.dcdl" & { slug = "different"; }"#, + )], + ) + .unwrap(); + assert!( + SnapshotEnvironment::new(snapshot) + .evaluate_contract(&contract) + .is_err() + ); + } + + #[test] + fn builtin_namespace_rejects_unknown_sources_escape_and_host_paths() { + let contract = ToolchainContract::new(1, vec![path("main.dcdl")], 1); + for specifier in [ + "$builtin/profiles/private.dcdl", + "$builtin/prompts/default.md", + "$builtin/../profiles/base.dcdl", + "$builtin/profiles/../../profiles/base.dcdl", + "/etc/passwd", + "file:///etc/passwd", + "https://example.com/main.dcdl", + "builtin:companion", + ] { + let source = format!("import {specifier:?}"); + let snapshot = ConfigTreeSnapshot::from_entries( + 1, + [ + entry("main.dcdl", &source), + entry("profiles/base.dcdl", "{ spoof = true; }"), + ], + ) + .unwrap(); + let diagnostics = SnapshotEnvironment::new(snapshot) + .evaluate_contract(&contract) + .unwrap_err(); + assert_eq!( + diagnostics[0].kind, "import", + "{specifier}: {diagnostics:?}" + ); + assert!(!diagnostics[0].message.is_empty()); + } + let current = path("$builtin/profiles/companion.dcdl"); + assert_eq!( + resolve_import(¤t, "./base.dcdl").unwrap(), + path("$builtin/profiles/base.dcdl") + ); + assert!(resolve_import(¤t, "../../main.dcdl").is_err()); + assert!(resolve_import(¤t, "main.dcdl").is_err()); + assert!(resolve_import(¤t, "/main.dcdl").is_err()); + } + + #[test] + fn builtin_namespace_is_not_a_mutable_workspace_tree() { + let builtin = path("$builtin/profiles/companion.dcdl"); + assert!( + ConfigEntry::new(builtin.clone(), ConfigContentType::Decodal, "{}") + .unwrap_err() + .to_string() + .contains("read-only") + ); + for change in [ + ConfigTreeChange::Create { + path: builtin.clone(), + content_type: ConfigContentType::Decodal, + content: "{}".into(), + }, + ConfigTreeChange::Delete { + path: builtin.clone(), + expected_digest: "digest".into(), + }, + ConfigTreeChange::Rename { + from: path("main.dcdl"), + to: builtin, + expected_digest: "digest".into(), + }, + ] { + assert!( + ConfigTreeSnapshot::empty() + .apply(&[change]) + .unwrap_err() + .to_string() + .contains("read-only") + ); + } + let environment = SnapshotEnvironment::new(ConfigTreeSnapshot::empty()); + let completions = import_completions( + environment.snapshot(), + Some(&path("main.dcdl")), + "$builtin/profiles/", + ); + assert_eq!(completions.len(), BUILTIN_PROFILE_RESOURCES.len()); + assert!(environment.snapshot().list_prefix(None).is_empty()); + } + + #[test] + fn builtin_values_do_not_bypass_cycle_or_path_limits() { + let snapshot = ConfigTreeSnapshot::from_entries(1, [ + entry("main.dcdl", r#"{ builtin = import "$builtin/profiles/companion.dcdl"; cycle = import "./cycle.dcdl"; }"#), + entry("cycle.dcdl", r#"import "./main.dcdl""#), + ]).unwrap(); + let contract = ToolchainContract::new(1, vec![path("main.dcdl")], 1); + let diagnostics = SnapshotEnvironment::new(snapshot) + .evaluate_contract(&contract) + .unwrap_err(); + assert_eq!(diagnostics[0].kind, "cycle"); + let mut loader = SnapshotImportLoader::new(ConfigTreeSnapshot::empty()); + for index in 0..=MAX_IMPORT_DEPTH { + loader.import_edges.insert( + format!("{index}.dcdl"), + BTreeSet::from([format!("{}.dcdl", index + 1)]), + ); + } + let diagnostic = validate_import_graph( + &loader.import_edges, + "0.dcdl", + &mut BTreeSet::new(), + &mut BTreeMap::new(), + ) + .unwrap_err(); + assert!(diagnostic.message.contains("import depth")); + let oversized = format!("$builtin/profiles/{}.dcdl", "x".repeat(MAX_PATH_BYTES)); + assert!(matches!( + resolve_import(&path("main.dcdl"), &oversized), + Err(ConfigTreeError::LimitExceeded("path bytes")) + )); + } + + #[test] + fn rejected_import_graph_edges_stay_rejected_after_diagnostics() { + let mut loader = SnapshotImportLoader::new( + ConfigTreeSnapshot::from_entries(1, [entry("a.dcdl", "{}"), entry("b.dcdl", "{}")]) + .unwrap(), + ); + loader.load(Some("a.dcdl"), "./b.dcdl").unwrap(); + for _ in 0..2 { + let diagnostic = loader.load(Some("b.dcdl"), "./a.dcdl").unwrap_err(); + assert_eq!(diagnostic.kind, DiagnosticKind::Cycle); + } + } + + #[test] + fn import_graph_shared_dependencies_are_memoized_without_losing_depth() { + let mut edges = BTreeMap::new(); + for index in 0..=MAX_IMPORT_DEPTH { + edges.insert( + format!("{index}"), + BTreeSet::from([format!("{}", index + 1), format!("{}", index + 2)]), + ); + } + let mut depths = BTreeMap::new(); + assert!(validate_import_graph(&edges, "0", &mut BTreeSet::new(), &mut depths).is_err()); + edges.remove(&MAX_IMPORT_DEPTH.to_string()); + let mut depths = BTreeMap::new(); + assert_eq!( + validate_import_graph(&edges, "0", &mut BTreeSet::new(), &mut depths).unwrap(), + MAX_IMPORT_DEPTH + ); + assert_eq!(depths.len(), MAX_IMPORT_DEPTH + 2); + } + + #[test] + fn snapshot_import_depth_is_checked_before_evaluation_recurses() { + let mut entries = Vec::new(); + for index in 0..=MAX_IMPORT_DEPTH + 1 { + let source = if index == MAX_IMPORT_DEPTH + 1 { + "{}".to_string() + } else { + format!("import \"./{}.dcdl\"", index + 1) + }; + entries.push(entry(&format!("{index}.dcdl"), &source)); + } + let diagnostics = + SnapshotEnvironment::new(ConfigTreeSnapshot::from_entries(1, entries).unwrap()) + .evaluate_contract(&ToolchainContract::new(1, vec![path("0.dcdl")], 1)) + .unwrap_err(); + assert!( + diagnostics + .iter() + .any(|diagnostic| diagnostic.message.contains("import depth")) + ); + } + + #[test] + fn builtin_dependencies_share_snapshot_count_and_byte_budgets() { + let contract = ToolchainContract::new(1, vec![path("main.dcdl")], 1); + let mut entries = vec![entry( + "main.dcdl", + r#"import "$builtin/profiles/companion.dcdl""#, + )]; + for i in 1..MAX_ENTRY_COUNT { + entries.push(text_entry(&format!("unused/{i}.txt"), "")); + } + let environment = + SnapshotEnvironment::new(ConfigTreeSnapshot::from_entries(1, entries).unwrap()); + let diagnostics = environment.evaluate_contract(&contract).unwrap_err(); + assert!(diagnostics[0].message.contains("entry count")); + let mut entries = vec![entry( + "main.dcdl", + r#"import "$builtin/profiles/companion.dcdl""#, + )]; + let source_len = entries[0].content.len(); + for i in 0..16 { + let bytes = if i == 15 { + MAX_ENTRY_BYTES - source_len + } else { + MAX_ENTRY_BYTES + }; + entries.push(text_entry(&format!("unused/{i}.txt"), &"a".repeat(bytes))); + } + let environment = + SnapshotEnvironment::new(ConfigTreeSnapshot::from_entries(1, entries).unwrap()); + let diagnostics = environment.evaluate_contract(&contract).unwrap_err(); + assert!(diagnostics[0].message.contains("total bytes")); + } + #[test] fn host_environment_evaluation_uses_only_snapshot_imports() { let snapshot = ConfigTreeSnapshot::from_entries( diff --git a/crates/manifest/Cargo.toml b/crates/manifest/Cargo.toml index 6f6fe2275..abdc916a2 100644 --- a/crates/manifest/Cargo.toml +++ b/crates/manifest/Cargo.toml @@ -6,6 +6,7 @@ license.workspace = true rust-version.workspace = true [dependencies] +builtin-source.workspace = true arc-swap = "1" agen = { workspace = true } decodal.workspace = true diff --git a/crates/manifest/src/builtin_profile.rs b/crates/manifest/src/builtin_profile.rs index 17d63848b..c5bf88b53 100644 --- a/crates/manifest/src/builtin_profile.rs +++ b/crates/manifest/src/builtin_profile.rs @@ -2,213 +2,10 @@ use std::collections::BTreeMap; use decodal::{Data, Engine, ImportLoader, LoadedImport}; use serde_json::{Map, Number, Value}; -use sha2::{Digest, Sha256}; use crate::profile::ProfileError; -pub const BUILTIN_PROFILE_CATALOG_ID: &str = "builtin-profiles-v2"; -pub const BUILTIN_DEFAULT_PROFILE: &str = "builtin:default"; -pub const BUILTIN_STANDALONE_PROFILE: &str = "builtin:standalone"; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct BuiltinProfileImport { - pub specifier: &'static str, - pub resolved_path: &'static str, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct BuiltinProfileResource { - pub selector: Option<&'static str>, - pub path: &'static str, - pub source: &'static str, - pub description: &'static str, - pub imports: &'static [BuiltinProfileImport], -} - -const BASE_PATH: &str = "profiles/base.dcdl"; -const DEFAULT_PATH: &str = "profiles/default.dcdl"; -const BASE_IMPORT: &[BuiltinProfileImport] = &[BuiltinProfileImport { - specifier: "./base.dcdl", - resolved_path: BASE_PATH, -}]; -const DEFAULT_IMPORT: &[BuiltinProfileImport] = &[BuiltinProfileImport { - specifier: "./default.dcdl", - resolved_path: DEFAULT_PATH, -}]; -const NO_IMPORTS: &[BuiltinProfileImport] = &[]; - -pub const BUILTIN_PROFILE_RESOURCES: &[BuiltinProfileResource] = &[ - BuiltinProfileResource { - selector: None, - path: BASE_PATH, - source: include_str!("../../../resources/profiles/base.dcdl"), - description: "Shared built-in Profile defaults.", - imports: NO_IMPORTS, - }, - BuiltinProfileResource { - selector: Some(BUILTIN_DEFAULT_PROFILE), - path: DEFAULT_PATH, - source: include_str!("../../../resources/profiles/default.dcdl"), - description: "Default Yoi coding profile.", - imports: BASE_IMPORT, - }, - BuiltinProfileResource { - selector: Some(BUILTIN_STANDALONE_PROFILE), - path: "profiles/standalone.dcdl", - source: include_str!("../../../resources/profiles/standalone.dcdl"), - description: "Standalone Yoi coding profile.", - imports: DEFAULT_IMPORT, - }, - BuiltinProfileResource { - selector: Some("builtin:standalone-subjektiv"), - path: "profiles/standalone-subjektiv.dcdl", - source: include_str!("../../../resources/profiles/standalone-subjektiv.dcdl"), - description: "Standalone with an explicitly connected local Subject.", - imports: &[BuiltinProfileImport { - specifier: "./standalone.dcdl", - resolved_path: "profiles/standalone.dcdl", - }], - }, - BuiltinProfileResource { - selector: Some("builtin:standalone-subjektiv-consolidation"), - path: "profiles/standalone-subjektiv-consolidation.dcdl", - source: include_str!("../../../resources/profiles/standalone-subjektiv-consolidation.dcdl"), - description: "Delegated local Subject Job policy.", - imports: &[BuiltinProfileImport { - specifier: "./job.dcdl", - resolved_path: "profiles/job.dcdl", - }], - }, - BuiltinProfileResource { - selector: Some("builtin:coder"), - path: "profiles/coder.dcdl", - source: include_str!("../../../resources/profiles/coder.dcdl"), - description: "Ticket implementation with direct Reviewer SubWorkers.", - imports: BASE_IMPORT, - }, - BuiltinProfileResource { - selector: Some("builtin:companion"), - path: "profiles/companion.dcdl", - source: include_str!("../../../resources/profiles/companion.dcdl"), - description: "General assistance with Workspace tools.", - imports: BASE_IMPORT, - }, - BuiltinProfileResource { - selector: Some("builtin:intake"), - path: "profiles/intake.dcdl", - source: include_str!("../../../resources/profiles/intake.dcdl"), - description: "Read-only intake and planning.", - imports: BASE_IMPORT, - }, - BuiltinProfileResource { - selector: Some("builtin:reviewer"), - path: "profiles/reviewer.dcdl", - source: include_str!("../../../resources/profiles/reviewer.dcdl"), - description: "Independent review of a published Merge Request source.", - imports: BASE_IMPORT, - }, - BuiltinProfileResource { - selector: Some("builtin:orchestrator"), - path: "profiles/orchestrator.dcdl", - source: include_str!("../../../resources/profiles/orchestrator.dcdl"), - description: "Workspace orchestration and Worker control.", - imports: BASE_IMPORT, - }, - BuiltinProfileResource { - selector: Some("builtin:job"), - path: "profiles/job.dcdl", - source: include_str!("../../../resources/profiles/job.dcdl"), - description: "Result-only caller-selected Job policy.", - imports: NO_IMPORTS, - }, - BuiltinProfileResource { - selector: Some("builtin:backend-job"), - path: "profiles/backend-job.dcdl", - source: include_str!("../../../resources/profiles/backend-job.dcdl"), - description: "Backend-owned bounded Job Worker.", - imports: BASE_IMPORT, - }, - BuiltinProfileResource { - selector: Some("builtin:memory-consolidation"), - path: "profiles/memory-consolidation.dcdl", - source: include_str!("../../../resources/profiles/memory-consolidation.dcdl"), - description: "Internal Memory consolidation service.", - imports: BASE_IMPORT, - }, - BuiltinProfileResource { - selector: Some("builtin:subjektiv-memory-consolidation"), - path: "profiles/subjektiv-memory-consolidation.dcdl", - source: include_str!("../../../resources/profiles/subjektiv-memory-consolidation.dcdl"), - description: "Delegated subject Memory consolidation service.", - imports: BASE_IMPORT, - }, -]; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct BuiltinProfileCatalogSnapshot { - pub id: &'static str, - pub sources: BTreeMap, - pub entrypoints: BTreeMap, - pub imports: BTreeMap, -} - -impl BuiltinProfileCatalogSnapshot { - pub fn digest(&self) -> String { - let mut hasher = Sha256::new(); - hasher.update(self.id.as_bytes()); - for (path, source) in &self.sources { - hasher.update((path.len() as u64).to_le_bytes()); - hasher.update(path.as_bytes()); - hasher.update((source.len() as u64).to_le_bytes()); - hasher.update(source.as_bytes()); - } - for (selector, path) in &self.entrypoints { - hasher.update((selector.len() as u64).to_le_bytes()); - hasher.update(selector.as_bytes()); - hasher.update((path.len() as u64).to_le_bytes()); - hasher.update(path.as_bytes()); - } - for (request, resolved_path) in &self.imports { - hasher.update((request.len() as u64).to_le_bytes()); - hasher.update(request.as_bytes()); - hasher.update((resolved_path.len() as u64).to_le_bytes()); - hasher.update(resolved_path.as_bytes()); - } - format!("sha256:{:x}", hasher.finalize()) - } -} - -pub fn builtin_profile_catalog_snapshot() -> BuiltinProfileCatalogSnapshot { - let mut sources = BTreeMap::new(); - let mut entrypoints = BTreeMap::new(); - let mut imports = BTreeMap::new(); - - for resource in BUILTIN_PROFILE_RESOURCES { - sources.insert(resource.path.to_owned(), resource.source.to_owned()); - for import in resource.imports { - imports.insert( - format!("{}\0{}", resource.path, import.specifier), - import.resolved_path.to_owned(), - ); - } - if let Some(selector) = resource.selector { - entrypoints.insert(selector.to_owned(), resource.path.to_owned()); - } - } - - BuiltinProfileCatalogSnapshot { - id: BUILTIN_PROFILE_CATALOG_ID, - sources, - entrypoints, - imports, - } -} - -pub fn builtin_profile_entrypoints() -> impl Iterator { - BUILTIN_PROFILE_RESOURCES - .iter() - .filter(|resource| resource.selector.is_some()) -} +pub use builtin_source::*; pub(crate) fn resolve_builtin_profile_artifact( selector: &str, From f086cf3375c12f6f24739b716b704640c8f12630 Mon Sep 17 00:00:00 2001 From: Hare Date: Wed, 7 Oct 2026 03:38:49 +0900 Subject: [PATCH 9/9] feat(profiles): seal builtin import provenance and expose read-only editor imports Retain the evaluated JSON launch entrypoint, seal observed builtin source bytes and digests as verified provenance, and fail on missing/corrupt dependencies without Runtime builtin reevaluation. Cover real HTTP commits and launch resolution, including worker mode and Workspace Config feature composition. Expose read-only builtin import completions/diagnostics in the editor and rebuild wasm32 release assets with wasm-bindgen 0.2.117. Document public paths, ordinary composition, limits, resave semantics, and the lazy-cycle incident. Validation: worker-runtime 344 library + 34 integration tests; workspace-server 609 library + 11 binary tests plus final builtin HTTP/launch tests (2); archive tests (16); native WASM tests (2); real-WASM/web tests (37), editor component tests (10); root cargo check, cargo fmt --all -- --check and git diff --check HEAD pass. --- crates/config-source-wasm/src/lib.rs | 155 ++++++++-- crates/worker-runtime/src/profile_archive.rs | 208 ++++++++++++- .../workspace-server/src/profile_settings.rs | 12 +- .../src/server/tests/value_profiles_tests.rs | 281 ++++++++++++++++++ docs/design/profiles-manifests-prompts.md | 21 +- docs/report/T-711-import-cycle.md | 66 ++++ .../ConfigSourceEditor.browser.spec.ts | 45 +++ .../config-source/ConfigSourceEditor.svelte | 2 +- .../generated/config_source_wasm_bg.wasm | Bin 1289088 -> 2019423 bytes web/workspace/test/config-source/api.test.ts | 56 ++++ .../test/config-source/toolchain.test.ts | 25 ++ .../test/config-source/wasm-parity.test.ts | 144 +++++++++ 12 files changed, 986 insertions(+), 29 deletions(-) create mode 100644 docs/report/T-711-import-cycle.md diff --git a/crates/config-source-wasm/src/lib.rs b/crates/config-source-wasm/src/lib.rs index 5752b64a0..2196469f4 100644 --- a/crates/config-source-wasm/src/lib.rs +++ b/crates/config-source-wasm/src/lib.rs @@ -98,31 +98,47 @@ pub fn complete_current( let snapshot = session .as_ref() .ok_or_else(|| JsValue::from_str("config source snapshot is not initialized"))?; - let utf8_byte_offset = utf16_to_utf8_offset(&source, utf16_offset)?; - let result = session_environment(snapshot.clone()) - .complete_config(&entrypoint, &source, utf8_byte_offset, explicit) - .map_err(|error| JsValue::from_str(&format!("{error:?}")))?; - let result = result - .map(|result| { - Ok::(WasmCompletionResult { - from: utf8_to_utf16_offset(&source, result.from)?, - items: result - .items - .into_iter() - .map(|item| WasmCompletionItem { - label: item.label, - kind: format!("{:?}", item.kind).to_lowercase(), - detail: item.detail, - priority: item.priority, - }) - .collect(), - }) - }) - .transpose()?; - encode(result) + encode(complete_snapshot( + snapshot, + &entrypoint, + &source, + utf16_offset, + explicit, + )?) }) } +fn complete_snapshot( + snapshot: &ConfigTreeSnapshot, + entrypoint: &VirtualPath, + source: &str, + utf16_offset: usize, + explicit: bool, +) -> Result, JsValue> { + let utf8_byte_offset = utf16_to_utf8_offset(source, utf16_offset)?; + let result = session_environment(snapshot.clone()) + .complete_config(entrypoint, source, utf8_byte_offset, explicit) + .map_err(|error| JsValue::from_str(&format!("{error:?}")))?; + let result = result + .map(|result| { + Ok::(WasmCompletionResult { + from: utf8_to_utf16_offset(source, result.from)?, + items: result + .items + .into_iter() + .map(|item| WasmCompletionItem { + label: item.label, + kind: format!("{:?}", item.kind).to_lowercase(), + detail: item.detail, + priority: item.priority, + }) + .collect(), + }) + }) + .transpose()?; + Ok(result) +} + #[wasm_bindgen] pub fn evaluate_snapshot(snapshot: JsValue, contract: JsValue) -> Result { let snapshot: ConfigTreeSnapshot = decode(snapshot)?; @@ -207,3 +223,98 @@ fn js_error(error: impl std::fmt::Display) -> JsValue { #[allow(dead_code)] fn _assert_serializable(_: EvaluationResult) {} + +#[cfg(test)] +mod tests { + use super::*; + use config_source::{ConfigContentType, ConfigEntry}; + + fn snapshot() -> ConfigTreeSnapshot { + ConfigTreeSnapshot::from_entries( + 7, + [ + ConfigEntry::new( + VirtualPath::parse("main.dcdl").unwrap(), + ConfigContentType::Decodal, + "{}", + ) + .unwrap(), + // A same-suffix workspace file must not be an unknown builtin fallback. + ConfigEntry::new( + VirtualPath::parse("profiles/missing.dcdl").unwrap(), + ConfigContentType::Decodal, + "{}", + ) + .unwrap(), + ], + ) + .unwrap() + } + + #[test] + fn wasm_completion_boundary_preserves_read_only_builtin_detail() { + let snapshot = snapshot(); + let source = "let 名 = 1; import \"$builtin/profiles/comp\""; + let cursor = source.encode_utf16().count() - 1; + let result = complete_snapshot( + &snapshot, + &VirtualPath::parse("main.dcdl").unwrap(), + source, + cursor, + true, + ) + .unwrap() + .unwrap(); + assert_eq!(result.from, "let 名 = 1; import \"".encode_utf16().count()); + let item = result + .items + .iter() + .find(|item| item.label == "$builtin/profiles/companion.dcdl") + .unwrap(); + assert_eq!(item.kind, "file"); + assert_eq!( + item.detail.as_deref(), + Some("read-only builtin Decodal source") + ); + assert_eq!(snapshot.entries.len(), 2); + assert!( + snapshot + .entries + .keys() + .all(|path| !path.as_str().starts_with("$builtin/")) + ); + } + + #[test] + fn wasm_analysis_boundary_reports_unknown_builtin_without_workspace_fallback() { + let snapshot = snapshot(); + let path = VirtualPath::parse("main.dcdl").unwrap(); + let diagnostics = session_environment(snapshot.clone()) + .analyze(&path, Some("import \"$builtin/profiles/missing.dcdl\"")); + assert!(!diagnostics.is_empty()); + let diagnostic = diagnostics + .iter() + .find(|diagnostic| { + diagnostic + .message + .contains("unknown or non-public read-only builtin source") + }) + .unwrap(); + assert_eq!(diagnostic.path, path); + assert_eq!(diagnostic.revision, 7); + assert_eq!(diagnostic.tree_digest, snapshot.digest); + assert!( + diagnostic + .message + .contains("$builtin/profiles/missing.dcdl") + ); + assert!( + session_environment(snapshot) + .analyze( + &VirtualPath::parse("main.dcdl").unwrap(), + Some("import \"./profiles/missing.dcdl\"") + ) + .is_empty() + ); + } +} diff --git a/crates/worker-runtime/src/profile_archive.rs b/crates/worker-runtime/src/profile_archive.rs index a38ff72dd..225b03a2d 100644 --- a/crates/worker-runtime/src/profile_archive.rs +++ b/crates/worker-runtime/src/profile_archive.rs @@ -134,15 +134,31 @@ impl ProfileSourceArchive { selector: String, value: serde_json::Value, ) -> Result { + Self::build_evaluated_profile_with_builtin_sources(id, selector, value, BTreeMap::new()) + } + + /// Seal the observed builtin import closure as provenance alongside the evaluated JSON. + /// Builtin sources are verified, but never evaluated to resolve this entrypoint. + pub fn build_evaluated_profile_with_builtin_sources( + id: String, + selector: String, + value: serde_json::Value, + sources: BTreeMap, + ) -> Result { + for key in sources.keys() { + validate_builtin_source_key(key)?; + } let path = "profiles/evaluated.json".to_string(); let source = serde_json::to_string(&value) .map_err(|err| ProfileArchiveError::Json(err.to_string()))?; + let mut sources = sources; + sources.insert(path.clone(), source); Self::build_with_source_format( ProfileSourceArchiveInput { id, - entrypoints: BTreeMap::from([(selector, path.clone())]), + entrypoints: BTreeMap::from([(selector, path)]), imports: BTreeMap::new(), - sources: BTreeMap::from([(path, source)]), + sources, }, "json", "application/json", @@ -171,6 +187,12 @@ impl ProfileSourceArchive { if total_source_bytes > MAX_TOTAL_BYTES { return Err(ProfileArchiveError::LimitExceeded("total source bytes")); } + let (kind, content_type) = if path.starts_with("$builtin/") { + validate_builtin_source_key(path)?; + ("decodal", "text/x-decodal") + } else { + (kind, content_type) + }; source_meta.push(ProfileSourceArchiveSource { path: path.clone(), source_key: path.clone(), @@ -310,7 +332,18 @@ impl VerifiedProfileSourceArchive { let mut manifest_paths = BTreeSet::new(); for source in &manifest.sources { validate_archive_path(&source.path)?; - if !source.source_key.is_empty() && source.source_key != source.path { + if source.path.starts_with("$builtin/") { + validate_builtin_source_key(&source.path)?; + if source.kind != "decodal" { + return Err(ProfileArchiveError::UnsupportedSource { + path: source.path.clone(), + kind: source.kind.clone(), + }); + } + } + if (source.path.starts_with("$builtin/") || !source.source_key.is_empty()) + && source.source_key != source.path + { return Err(ProfileArchiveError::ImportMapMismatch { specifier: source.path.clone(), expected: source.path.clone(), @@ -662,6 +695,26 @@ fn append_bytes( .map_err(|err| ProfileArchiveError::Io(err.to_string())) } +fn validate_builtin_source_key(key: &str) -> Result<(), ProfileArchiveError> { + validate_archive_path(key)?; + let Some(relative) = key.strip_prefix("$builtin/profiles/") else { + return Err(ProfileArchiveError::UnsafePath(key.to_string())); + }; + if !relative.ends_with(".dcdl") + || relative.split('/').any(|part| { + part.is_empty() + || part == "." + || part == ".." + || !part + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || b"-_.".contains(&byte)) + }) + { + return Err(ProfileArchiveError::UnsafePath(key.to_string())); + } + Ok(()) +} + fn validate_archive_path(path: &str) -> Result<(), ProfileArchiveError> { if path.is_empty() || path == MANIFEST_PATH { return if path == MANIFEST_PATH { @@ -799,6 +852,155 @@ mod tests { archive.content = content; } + fn builtin_provenance_archive() -> ProfileSourceArchive { + ProfileSourceArchive::build_evaluated_profile_with_builtin_sources( + "builtin-provenance".into(), + "project:evaluated".into(), + evaluated_profile(), + BTreeMap::from([ + ( + "$builtin/profiles/companion.dcdl".into(), + "import \"./base.dcdl\"".into(), + ), + // Intentionally invalid Decodal: provenance must not be reevaluated. + ( + "$builtin/profiles/base.dcdl".into(), + "not valid Decodal !".into(), + ), + ]), + ) + .unwrap() + } + + #[test] + fn evaluated_json_seals_builtin_closure_without_runtime_evaluation() { + let archive = builtin_provenance_archive(); + let verified = archive.verify().unwrap(); + assert_eq!( + archive, + builtin_provenance_archive(), + "archive must be deterministic" + ); + assert_eq!(archive.reference.source_graph.source_count, 3); + assert!(verified.manifest.imports.is_empty()); + assert_eq!( + verified.manifest.entrypoints["project:evaluated"], + "profiles/evaluated.json" + ); + for source in &verified.manifest.sources { + assert_eq!(source.source_key, source.path); + assert_eq!( + source.digest, + sha256_hex(verified.sources[&source.path].as_bytes()) + ); + if source.path.starts_with("$builtin/") { + assert_eq!(source.kind, "decodal"); + assert_eq!(source.content_type, "text/x-decodal"); + } else { + assert_eq!(source.kind, "json"); + } + } + let root = tempfile::tempdir().unwrap(); + let resolved = verified + .resolve_profile("project:evaluated", root.path(), "sealed-worker") + .unwrap(); + assert_eq!(resolved.worker.mode, manifest::WorkerMode::Wip); + assert!(resolved.feature.task.enabled); + assert!(matches!( + verified.resolve_profile("builtin:companion", root.path(), "sealed-worker"), + Err(ProfileArchiveError::MissingEntrypoint(_)) + )); + } + + #[test] + fn evaluated_json_rejects_missing_or_corrupt_builtin_dependency() { + for missing in [true, false] { + let mut archive = builtin_provenance_archive(); + let mut verified = archive.verify().unwrap(); + let path = "$builtin/profiles/base.dcdl"; + if missing { + verified.sources.remove(path); + } else { + verified + .sources + .insert(path.into(), "corrupt dependency".into()); + } + repack_archive(&mut archive, &verified.manifest, &verified.sources); + match archive.verify().unwrap_err() { + ProfileArchiveError::MissingSource(actual) if missing => assert_eq!(actual, path), + ProfileArchiveError::SourceDigestMismatch { path: actual, .. } if !missing => { + assert_eq!(actual, path) + } + other => panic!("unexpected error: {other}"), + } + } + } + + #[test] + fn evaluated_json_builtin_provenance_rejects_unsafe_keys_and_reuses_limits() { + for key in [ + "profiles/companion.dcdl", + "builtin:profiles/companion.dcdl", + "$builtin/prompts/companion.dcdl", + "$builtin/profiles/../escape.dcdl", + "$builtin/profiles/./base.dcdl", + "$builtin/profiles//base.dcdl", + "$builtin/profiles/base\\other.dcdl", + "$builtin/profiles/base\0.dcdl", + "/$builtin/profiles/base.dcdl", + "$builtin/profiles/base.json", + "profiles/evaluated.json", + ] { + assert!( + matches!( + ProfileSourceArchive::build_evaluated_profile_with_builtin_sources( + "unsafe".into(), + "project:evaluated".into(), + evaluated_profile(), + BTreeMap::from([(key.into(), "{}".into())]), + ), + Err(ProfileArchiveError::UnsafePath(_)) + ), + "{key:?}" + ); + } + assert!(matches!( + ProfileSourceArchive::build_evaluated_profile_with_builtin_sources( + "oversized".into(), + "project:evaluated".into(), + evaluated_profile(), + BTreeMap::from([( + "$builtin/profiles/base.dcdl".into(), + "x".repeat(MAX_SOURCE_BYTES as usize + 1) + )]), + ), + Err(ProfileArchiveError::LimitExceeded("source bytes")) + )); + } + + #[test] + fn evaluated_json_builtin_provenance_requires_canonical_key_and_source_format() { + for key in ["", "profiles/base.dcdl", "$builtin/profiles/other.dcdl"] { + let mut archive = builtin_provenance_archive(); + let mut verified = archive.verify().unwrap(); + verified.manifest.sources[0].source_key = key.into(); + repack_archive(&mut archive, &verified.manifest, &verified.sources); + assert!(matches!( + archive.verify(), + Err(ProfileArchiveError::ImportMapMismatch { .. }) + )); + } + let mut archive = builtin_provenance_archive(); + let mut verified = archive.verify().unwrap(); + verified.manifest.sources[0].kind = "json".into(); + verified.manifest.sources[0].content_type = "application/json".into(); + repack_archive(&mut archive, &verified.manifest, &verified.sources); + assert!(matches!( + archive.verify(), + Err(ProfileArchiveError::UnsupportedSource { .. }) + )); + } + #[test] fn evaluated_json_reuses_source_size_limit() { assert!(matches!( diff --git a/crates/workspace-server/src/profile_settings.rs b/crates/workspace-server/src/profile_settings.rs index 2d08b2e31..d88cb9bb6 100644 --- a/crates/workspace-server/src/profile_settings.rs +++ b/crates/workspace-server/src/profile_settings.rs @@ -464,10 +464,20 @@ fn build_virtual_profile_archive( entry: &VirtualProfileEntry, state: &WorkspaceConfigState, ) -> Result { - ProfileSourceArchive::build_evaluated_profile( + let builtin_sources = config_source::SnapshotEnvironment::new(state.snapshot.clone()) + .builtin_import_sources(&state.contract) + .map_err(|diagnostics| { + profile_validation_error( + "profile_builtin_snapshot_invalid", + &serde_json::to_string(&diagnostics) + .unwrap_or_else(|_| "builtin import snapshot failed".to_string()), + ) + })?; + ProfileSourceArchive::build_evaluated_profile_with_builtin_sources( format!("workspace-config-profile-r{}", state.snapshot.revision), selector.to_string(), entry.profile.clone(), + builtin_sources, ) .map_err(|error| profile_validation_error("profile_value_archive_invalid", &error.to_string())) } diff --git a/crates/workspace-server/src/server/tests/value_profiles_tests.rs b/crates/workspace-server/src/server/tests/value_profiles_tests.rs index b375fdeb9..8a4a7d307 100644 --- a/crates/workspace-server/src/server/tests/value_profiles_tests.rs +++ b/crates/workspace-server/src/server/tests/value_profiles_tests.rs @@ -44,6 +44,287 @@ fn source(value: &str) -> String { ) } +#[tokio::test] +async fn value_profiles_builtin_http_config_tree_is_read_only_and_never_falls_back() { + let dir = tempfile::tempdir().unwrap(); + let api = test_api(dir.path()).await; + let token = seed_test_api_token(api.store.as_ref(), TEST_WORKSPACE_ID); + let app = build_router(api.clone()); + let tree = format!("/api/w/{TEST_WORKSPACE_ID}/config/source-tree"); + let commit = format!("{tree}/commit"); + let initial = get_json_authenticated(app.clone(), &tree, &token).await; + let saved = request_json_authenticated( + app.clone(), + "POST", + &commit, + Some( + serde_json::to_value(request( + &api, + &source(r#"import "$builtin/profiles/companion.dcdl""#), + vec![config_source::ConfigTreeChange::Create { + // An ordinary Workspace file at the matching suffix must not rescue + // an unknown builtin import. + path: config_source::VirtualPath::parse("profiles/private.dcdl").unwrap(), + content_type: config_source::ConfigContentType::Decodal, + content: PROFILE.into(), + }], + )) + .unwrap(), + ), + &token, + StatusCode::CREATED, + ) + .await; + assert_eq!( + saved["snapshot"]["revision"].as_u64().unwrap(), + initial["snapshot"]["revision"].as_u64().unwrap() + 1 + ); + let listed = get_json_authenticated(app.clone(), &tree, &token).await; + assert_eq!(listed, saved); + let entries = listed["snapshot"]["entries"].as_object().unwrap(); + assert!(entries.contains_key("profiles/private.dcdl")); + assert!( + entries + .keys() + .all(|key| key != "$builtin" && !key.starts_with("$builtin/")) + ); + let shadow = get_json_authenticated( + app.clone(), + &format!("{tree}/entries/profiles%2Fprivate.dcdl"), + &token, + ) + .await; + assert_eq!(shadow, entries["profiles/private.dcdl"]); + assert!( + shadow["content"] + .as_str() + .unwrap() + .contains("slug = \"alpha\"") + ); + let active = api + .config_store + .load_workspace_config(TEST_WORKSPACE_ID) + .unwrap() + .unwrap(); + let main_digest = entries["main.dcdl"]["content_digest"].as_str().unwrap(); + let base = json!({ + "base_revision": listed["snapshot"]["revision"], + "base_digest": listed["snapshot"]["digest"], + "entrypoints": listed["contract"]["entrypoints"], + }); + let reserved = "$builtin/profiles/companion.dcdl"; + for change in [ + json!({ "kind": "create", "path": reserved, "content_type": "decodal", "content": PROFILE }), + json!({ "kind": "create", "path": "$builtin", "content_type": "text", "content": "shadow" }), + json!({ "kind": "update", "path": reserved, "expected_digest": "not-a-workspace-entry", "content": PROFILE }), + json!({ "kind": "delete", "path": reserved, "expected_digest": "not-a-workspace-entry" }), + json!({ "kind": "rename", "from": "profiles/private.dcdl", "to": reserved, "expected_digest": shadow["content_digest"] }), + json!({ "kind": "rename", "from": reserved, "to": "profiles/copy.dcdl", "expected_digest": "not-a-workspace-entry" }), + ] { + let mut body = base.clone(); + body["changes"] = json!([change]); + let error = request_json_authenticated( + app.clone(), + "POST", + &commit, + Some(body), + &token, + StatusCode::BAD_REQUEST, + ) + .await; + assert!( + error["message"] + .as_str() + .unwrap() + .contains("builtin namespace is read-only"), + "{error}" + ); + assert_eq!( + get_json_authenticated(app.clone(), &tree, &token).await, + listed + ); + } + let mut body = base; + body["changes"] = json!([{ + "kind": "update", "path": "main.dcdl", "expected_digest": main_digest, + "content": source(r#"import "$builtin/profiles/private.dcdl""#), + }]); + let error = request_json_authenticated( + app.clone(), + "POST", + &commit, + Some(body), + &token, + StatusCode::BAD_REQUEST, + ) + .await; + let message = error["message"].as_str().unwrap(); + assert!( + message.contains( + "unknown or non-public read-only builtin source: $builtin/profiles/private.dcdl" + ), + "{error}" + ); + assert!(message.contains("\"kind\":\"import\""), "{error}"); + assert_eq!(get_json_authenticated(app, &tree, &token).await, listed); + assert_eq!( + api.config_store + .load_workspace_config(TEST_WORKSPACE_ID) + .unwrap() + .unwrap(), + active + ); +} + +#[tokio::test] +async fn value_profiles_builtin_companion_composition_seals_observed_sources() { + let dir = tempfile::tempdir().unwrap(); + let api = test_api(dir.path()).await; + let recipe = r#"(import "$builtin/profiles/companion.dcdl") // { + worker = { mode = "wip"; }; + feature = { + task = { enabled = true; }; + ticket = { enabled = false; }; + workspace_config = { enabled = true; }; + }; + }"#; + let state = commit_workspace_config_tree( + &api, + TEST_WORKSPACE_ID, + &request( + &api, + &source(r#"import "./recipes/custom.dcdl""#), + vec![ + config_source::ConfigTreeChange::Create { + path: config_source::VirtualPath::parse("recipes/custom.dcdl").unwrap(), + content_type: config_source::ConfigContentType::Decodal, + content: recipe.into(), + }, + // Builtin relative imports must not be shadowed by Workspace files. + config_source::ConfigTreeChange::Create { + path: config_source::VirtualPath::parse("profiles/base.dcdl").unwrap(), + content_type: config_source::ConfigContentType::Decodal, + content: "{ slug = \"workspace-shadow\"; }".into(), + }, + ], + ), + ) + .unwrap(); + let projection = project_profiles_from_workspace_config(TEST_WORKSPACE_ID, &state).unwrap(); + let bundle = build_virtual_profile_config_bundle( + &projection, + &state, + TEST_WORKSPACE_ID, + "created", + "project:alpha", + ) + .unwrap() + .unwrap(); + assert_eq!(bundle.metadata.digest, bundle.computed_digest()); + let archive = bundle.profile_source_archive.as_ref().unwrap(); + let verified = archive.verify().unwrap(); + let environment = config_source::SnapshotEnvironment::new(state.snapshot.clone()); + let builtin_sources = environment.builtin_import_sources(&state.contract).unwrap(); + assert_eq!( + builtin_sources + .keys() + .map(String::as_str) + .collect::>(), + vec![ + "$builtin/profiles/base.dcdl", + "$builtin/profiles/companion.dcdl", + ] + ); + assert_eq!( + archive.reference.source_graph.source_count, + builtin_sources.len() + 1 + ); + assert_eq!(archive.reference.source_graph.import_count, 0); + assert_eq!( + verified.manifest().entrypoints["project:alpha"], + "profiles/evaluated.json" + ); + for (key, bytes) in &builtin_sources { + let source = verified + .manifest() + .sources + .iter() + .find(|source| &source.source_key == key) + .unwrap(); + assert_eq!(source.path, *key); + assert_eq!(source.kind, "decodal"); + assert_eq!( + source.digest, + worker_runtime::profile_archive::sha256_hex(bytes.as_bytes()) + ); + assert_eq!(source.size_bytes, bytes.len() as u64); + } + assert!( + verified + .manifest() + .sources + .iter() + .any(|source| source.path == "profiles/evaluated.json" && source.kind == "json") + ); + // Resolve using the Runtime archive consumer, without access to the config tree. + let resolved = verified + .resolve_profile( + "project:alpha", + Path::new("/runtime-worker"), + "builtin-value-worker", + ) + .unwrap(); + assert_eq!(resolved.worker.mode, manifest::WorkerMode::Wip); + assert!(resolved.feature.task.enabled); + assert!(!resolved.feature.ticket.enabled); + assert!(resolved.feature.workspace_config.enabled); + let evaluation = environment.evaluate_contract(&state.contract).unwrap(); + assert_eq!( + resolved.model.ref_.as_deref(), + evaluation.projections[0] + .data_json + .pointer("/profile/entries/0/profile/model/ref") + .and_then(serde_json::Value::as_str) + ); + let persisted = manifest::write_persisted_worker_manifest_snapshot(&resolved).unwrap(); + let old_bundle_digest = bundle.metadata.digest.clone(); + + let updated = commit_workspace_config_tree( + &api, + TEST_WORKSPACE_ID, + &request(&api, &source(PROFILE), vec![]), + ) + .unwrap(); + let projection = project_profiles_from_workspace_config(TEST_WORKSPACE_ID, &updated).unwrap(); + let updated_bundle = build_virtual_profile_config_bundle( + &projection, + &updated, + TEST_WORKSPACE_ID, + "created", + "project:alpha", + ) + .unwrap() + .unwrap(); + assert_ne!(old_bundle_digest, updated_bundle.metadata.digest); + assert_eq!( + updated_bundle + .profile_source_archive + .as_ref() + .unwrap() + .reference + .source_graph + .source_count, + 1 + ); + // Editing config changes future snapshots, not a sealed bundle or persisted Worker. + assert_eq!(bundle.metadata.digest, old_bundle_digest); + assert_eq!( + serde_json::to_value(manifest::read_persisted_worker_manifest_snapshot(persisted).unwrap()) + .unwrap(), + serde_json::to_value(resolved).unwrap() + ); +} + #[tokio::test] async fn value_profiles_save_project_and_runtime_consume_the_same_revision() { let dir = tempfile::tempdir().unwrap(); diff --git a/docs/design/profiles-manifests-prompts.md b/docs/design/profiles-manifests-prompts.md index f791eb084..7c221df12 100644 --- a/docs/design/profiles-manifests-prompts.md +++ b/docs/design/profiles-manifests-prompts.md @@ -39,7 +39,24 @@ Register the Profile **value**, not a source pathname: The imported file can contain `{ worker = { mode = "wip"; }; model = { ref = "codex-oauth/gpt-5.6-sol"; }; feature = { task = { enabled = true; }; }; }`. That record can also be written directly at `entries[0].profile`. Composition uses ordinary Decodal expressions, for example `(import "./profiles/base.dcdl") // { worker = { mode = "wip"; }; }` for an explicit patch, or `&` for compatible refinement. No dedicated Profile file or `profiles/` directory is required. Imports are relative to the **containing virtual config file**, not a host filesystem path; missing imports and virtual-tree escapes are rejected. -`selector` must be a nonempty `project:*` selector, unique in the Workspace. `label` and `description` are optional display metadata. An omitted `default_profile` selects `builtin:companion`; an explicitly unknown default is an error, not a fallback. A project Profile is a partial recipe resolved against `WorkerManifestConfig::resolution_defaults()`, **not** implicitly inherited from a builtin role. Explicit values remain explicit and omitted Profile fields remain absent in the transported value. Builtin-value import helpers are not part of this registration contract. +`selector` must be a nonempty `project:*` selector, unique in the Workspace. `label` and `description` are optional display metadata. An omitted `default_profile` selects `builtin:companion`; an explicitly unknown default is an error, not a fallback. A project Profile is a partial recipe resolved against `WorkerManifestConfig::resolution_defaults()`, **not** implicitly inherited from a builtin role. Explicit values remain explicit and omitted Profile fields remain absent in the transported value. To inherit a builtin recipe, import its public value explicitly; there is no implicit builtin inheritance. + +### Public builtin imports + +Workspace Decodal can import embedded Profile values through the read-only `$builtin/` namespace: + +```dcdl +(import "$builtin/profiles/companion.dcdl") // { + worker = { mode = "wip"; }; + feature = { task = { enabled = true; }; ticket = { enabled = false; }; }; +} +``` + +Use this expression anywhere a Profile value is accepted, including `profile.entries[*].profile` or an imported Workspace recipe. `$builtin/profiles/companion.dcdl` is a source path, not the launch selector `builtin:companion`. Public paths under `$builtin/profiles/` are `base.dcdl`, `default.dcdl`, `standalone.dcdl`, `standalone-subjektiv.dcdl`, `standalone-subjektiv-consolidation.dcdl`, `coder.dcdl`, `companion.dcdl`, `intake.dcdl`, `reviewer.dcdl`, `orchestrator.dcdl`, `job.dcdl`, `backend-job.dcdl`, `memory-consolidation.dcdl`, and `subjektiv-memory-consolidation.dcdl`. This is an explicit embedded catalog, not arbitrary resource or filesystem access. Unknown paths, namespace escapes, host paths and network URLs are errors. + +Relative imports **inside a builtin source** resolve against its builtin virtual directory: companion's `./base.dcdl` means `$builtin/profiles/base.dcdl`, never the Workspace's `profiles/base.dcdl`. Relative imports in Workspace files continue to use their containing Workspace virtual directory. Builtin values obey ordinary Decodal composition: `//` applies an explicit patch and `&` refines compatible values (conflicting explicit values remain errors). Importing a builtin does not add a special merge or fallback mechanism. + +Builtin dependencies share the Workspace snapshot budgets: at most 256 entries across Workspace entries and distinct observed builtin sources, 256 KiB per source, and 4 MiB of total source bytes. Unused Workspace entries still count; repeated imports of the same builtin count once. The observed import graph must be acyclic and at most 32 edges deep, **including the synthetic evaluation wrapper's import of the entrypoint**. These guards apply before the loader returns sources to Decodal, including imports in lazy object fields; builtins do not bypass them. Launch archives retain their separate transport limits (64 sources, 256 KiB per source, 1 MiB total source bytes). The former `entries = [{ selector = "project:name"; source = "profiles/name.dcdl"; }]` registration is no longer accepted. Migrate it explicitly to `profile = import "./profiles/name.dcdl";`, preserving the selector, label, description and default. For registrations in a nested config file, adjust the relative import to the original virtual-tree entry (for example `../profiles/name.dcdl`). Do not rename or recreate the Profile file merely for registration. Previously saved source-based revisions remain readable as config data and are not rewritten, deleted, or silently substituted with a builtin. Their Profile projection/launch reports `profile_source_registration_removed` with the replacement form; saves under the new schema reject the old field. Existing Workers continue using their saved Manifest while the author migrates and saves the config. @@ -57,7 +74,7 @@ Source/partial layers may omit fields. Resolved manifests should be explicit eno `--manifest ` exists as an explicit low-level escape hatch. Normal fresh startup selects a `builtin:*` or `project:*` Profile from the Backend-managed Workspace Config revision rather than applying an ambient manifest cascade. -Project Profiles are evaluated as part of the revisioned Virtual Config, including its imports and value composition. The Backend packages the **evaluated selected value** into a digest-bound JSON entry in the existing Profile archive transport, and binds the launch bundle to the Workspace identity, revision, tree/projection digests and schema/toolchain fingerprints. Runtime verifies the archive and resolves that exact value without re-evaluating Decodal, scanning imports or reading Workspace files. Builtin Profiles retain their embedded Decodal archive path. Saving config changes only future fresh launches; the Worker persists the resulting resolved Manifest and restore uses that saved snapshot rather than selecting the current recipe again. Files below the Workdir are not implicit Profile override layers. +Project Profiles are evaluated as part of the revisioned Virtual Config, including its imports and value composition. The Backend packages the **evaluated selected value** into a digest-bound JSON entry in the existing Profile archive transport, and binds the launch bundle to the Workspace identity, revision, tree/projection digests and schema/toolchain fingerprints. Runtime verifies the archive and resolves that exact value without re-evaluating Decodal, scanning imports or reading Workspace files. When Workspace evaluation imports builtins, the archive additionally seals the exact observed transitive builtin closure for the Workspace contract: canonical `$builtin/profiles/...` source keys, original Decodal bytes, content types, sizes and source digests, all bound by the outer archive and launch bundle digests. These are provenance dependencies, not runtime entrypoints or runtime import instructions. Missing or corrupt provenance fails verification even though the entrypoint is JSON; no local file or current embedded builtin can rescue it. Builtin Profiles selected directly retain their embedded Decodal archive path. Changing an embedded builtin so that it changes the evaluated Workspace value requires an **explicit config save/re-evaluation before a fresh launch**. The Backend compares current evaluation with the saved projection digest and rejects a mismatch rather than silently adopting the new value. T-711 retains the existing version-2 toolchain contract/fingerprint tuple so existing stored contracts remain readable; builtin provenance is bound by the archive and bundle digests, not a new catalog fingerprint field. Neither a builtin update nor a config save rewrites a sealed archive or silently rebases an existing Worker. Saving config changes only future fresh launches; the Worker persists the resulting resolved Manifest and restore uses that saved snapshot rather than selecting the current recipe again. Files below the Workdir are not implicit Profile override layers. ## Local stdio MCP server declarations diff --git a/docs/report/T-711-import-cycle.md b/docs/report/T-711-import-cycle.md new file mode 100644 index 000000000..37dbaf422 --- /dev/null +++ b/docs/report/T-711-import-cycle.md @@ -0,0 +1,66 @@ +# T-711: nested lazy-object import cycle + +Date: 2026-10-06 +Status: fixed at the snapshot import-loader boundary; regression coverage retained. + +## Dogfooding problem + +During T-711 builtin-import validation, a nested import cycle in a lazy object caused a stack overflow instead of a recoverable configuration diagnostic. A configuration author could therefore terminate evaluation while writing an invalid recipe, rather than receive an error and keep the active revision. This report records the implementation-session failure and the guard added by the parent config-source implementation; it is not a recommendation to rerun the crashing version inside a live Yoi Worker. + +## Minimal reproducer + +Build a `ConfigTreeSnapshot` containing these two Decodal entries: + +`main.dcdl`: + +```dcdl +{ + builtin = import "$builtin/profiles/companion.dcdl"; + cycle = import "./cycle.dcdl"; +} +``` + +`cycle.dcdl`: + +```dcdl +import "./main.dcdl" +``` + +Evaluate the snapshot with `SnapshotEnvironment::evaluate_contract` and a `ToolchainContract` whose entrypoint is `main.dcdl`. This fixture intentionally uses the plain evaluation contract rather than `WorkspaceConfigSchema`: it isolates import/materialization behavior, not Profile registration semantics. + +Before the loader guard, evaluating the root object could defer its `cycle` field until materialization. Importing the root again through `cycle.dcdl` then recursed through that lazy value instead of returning a structured cycle failure. The observed failure was a process stack overflow. Increasing the stack would only postpone the failure and would not provide a safe configuration boundary. + +## Fix + +`SnapshotImportLoader::load` now records canonical source-to-source edges and validates the observed graph **before returning a source to Decodal**. Content-digest cache suffixes are stripped for graph identity, so the same virtual source cannot evade cycle detection by appearing under a cache key. + +- A back edge returns a `cycle` diagnostic before lazy materialization can recurse. +- An import chain longer than 32 edges returns an import-depth limit diagnostic. The synthetic evaluation wrapper's entrypoint import counts toward that limit. +- Memoized dependency depths keep shared-subgraph validation bounded without losing the longest path. +- Rejected edges are removed from the successful-edge cache. A reused language-service loader must reject the same bad edge again, not treat it as already validated. +- Workspace and builtin imports use the same graph guard and shared source count/byte budgets. Embedded sources are not an exception. + +The fix belongs in `crates/config-source/src/lib.rs`; it does not require runtime Decodal evaluation. Project launch archives still transport the evaluated JSON entrypoint with builtin source provenance, and Runtime resolves only that evaluated value. + +## Regression evidence + +The following tests in `crates/config-source/src/lib.rs` cover the incident and adjacent guard behavior: + +- `builtin_values_do_not_bypass_cycle_or_path_limits`: the two-entry lazy-object fixture above returns `kind = "cycle"`; builtin imports do not bypass depth/path bounds. +- `rejected_import_graph_edges_stay_rejected_after_diagnostics`: repeated attempts to add the rejected back edge remain errors when the loader is reused. +- `import_graph_shared_dependencies_are_memoized_without_losing_depth`: shared dependencies preserve longest-path validation and the allowed depth boundary. +- `snapshot_import_depth_is_checked_before_evaluation_recurses`: a real source chain fails with an import-depth diagnostic before evaluation recurses beyond the guard. +- `builtin_dependencies_share_snapshot_count_and_byte_budgets`: builtin dependencies cannot exceed the aggregate entry-count or byte budget by living outside the authored tree. + +Run individual regressions with the normal narrow target, for example: + +```sh +cargo test -p config-source --lib builtin_values_do_not_bypass_cycle_or_path_limits +cargo test -p config-source --lib snapshot_import_depth_is_checked_before_evaluation_recurses +``` + +The server follow-up test `value_profiles_builtin_http_config_tree_is_read_only_and_never_falls_back` separately exercises authenticated HTTP commits: reserved builtin mutations and unknown imports are rejected without changing the active revision, even when a Workspace file exists at the unknown builtin's suffix. Editor import completions remain covered by the WASM editor tests; no server completion endpoint was introduced. + +## Operational lesson + +An evaluator's module cache is not sufficient evidence that lazy-value cycles are safe. Keep cycle and depth validation at the source-loader boundary and retain the lazy-object reproducer whenever import identity, caching, or materialization changes. Config errors should be recoverable diagnostics, never an implicit request to replace a running Worker's persisted Manifest or restart the live dogfooding process. diff --git a/web/workspace/src/lib/workspace/config-source/ConfigSourceEditor.browser.spec.ts b/web/workspace/src/lib/workspace/config-source/ConfigSourceEditor.browser.spec.ts index 6355eaa4d..4e28bd4f9 100644 --- a/web/workspace/src/lib/workspace/config-source/ConfigSourceEditor.browser.spec.ts +++ b/web/workspace/src/lib/workspace/config-source/ConfigSourceEditor.browser.spec.ts @@ -216,6 +216,51 @@ test("unmount during schema setup closes the toolchain", async () => { expect(toolchain.close).toHaveBeenCalledOnce(); }); +test("builtin diagnostics show read-only paths without adding editable workspace files", async () => { + toolchain.analyze.mockResolvedValue([{ + path: "$builtin/profiles/companion.dcdl", + revision: 7, + tree_digest: "sha256:tree", + kind: "constraint_violation", + span: { start_byte: 1, end_byte: 4 }, + message: "Imported value does not match the requested schema", + labels: [], + notes: [], + }]); + const view = renderEditor(); + await expectReady(view); + await waitFor(() => { + const diagnostic = view.container.querySelector(".config-source-diagnostics"); + expect(diagnostic?.textContent).toContain("$builtin/profiles/companion.dcdl (read-only builtin source)"); + expect(diagnostic?.textContent).toContain("bytes 1–4"); + }); + const paths = view.getByRole("navigation", { name: "Virtual configuration paths" }); + expect(paths.textContent).not.toContain("$builtin/"); + expect(paths.querySelectorAll("button")).toHaveLength(1); + expect(view.container.querySelector('.cm-content[contenteditable="true"]')).not.toBeNull(); +}); + +test("unknown builtin diagnostics retain the workspace importer path", async () => { + toolchain.analyze.mockResolvedValue([{ + path: "main.dcdl", + revision: 7, + tree_digest: "sha256:tree", + kind: "import", + span: { start_byte: 8, end_byte: 40 }, + message: "unknown or non-public read-only builtin source: $builtin/profiles/missing.dcdl", + labels: [], + notes: [], + }]); + const view = renderEditor(); + await expectReady(view); + await waitFor(() => { + const diagnostic = view.container.querySelector(".config-source-diagnostics"); + expect(diagnostic?.textContent).toContain("main.dcdl · bytes 8–40"); + expect(diagnostic?.textContent).toContain("unknown or non-public read-only builtin source"); + }); + expect(view.getByRole("navigation", { name: "Virtual configuration paths" }).textContent).not.toContain("$builtin/"); +}); + test("unmount during fetch does not start a toolchain", async () => { let finishFetch!: (response: Response) => void; fetcher.mockReturnValueOnce( diff --git a/web/workspace/src/lib/workspace/config-source/ConfigSourceEditor.svelte b/web/workspace/src/lib/workspace/config-source/ConfigSourceEditor.svelte index 78f9edb44..b1064abcd 100644 --- a/web/workspace/src/lib/workspace/config-source/ConfigSourceEditor.svelte +++ b/web/workspace/src/lib/workspace/config-source/ConfigSourceEditor.svelte @@ -377,7 +377,7 @@

  • {diagnostic.kind} {diagnostic.message} - bytes {diagnostic.span.start_byte}–{diagnostic.span.end_byte} + {diagnostic.path}{diagnostic.path.startsWith("$builtin/") ? " (read-only builtin source)" : ""} · bytes {diagnostic.span.start_byte}–{diagnostic.span.end_byte}
  • {/each} diff --git a/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm_bg.wasm b/web/workspace/src/lib/workspace/config-source/generated/config_source_wasm_bg.wasm index f6cf6e8cf87e1475bb84c2482d3c5fbc5c7846d3..b6468fe9338ea34edee11fb63ad4f757bc2ff5f8 100644 GIT binary patch literal 2019423 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

    9h&d+dVR@1?cMwA`j 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 z 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

    a#GZ>D??<2%h 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 z�a{r$*~;-K}l~eA}UHGGNu8V#?H1#ltrxbP zx|{|`NnO-WRFPmnsCjGx4N8bRW5DsC^#Rb$>%Yza2iN^?ff_pFP`u-nirZ;zXQz_ahO)-&e=& zvos)?Vl@E))W+>#aMihX+%+1T$s{*c0^7;$??mf3pKPgL^vu5UNUA%amMrq1NO6#a zF{#hgP_8@3-?J~?fp0!t*3R+YTvvClCa64zZCw{e3k(=WUzb8_Su|BK9+Bj6f}8mm z2~z+c&dVBwfGH@7`RTD{n=Wf>*3>6>3;LQOhHygW;<^^-1Xvh4kLLm1)ll6uH}{K! ze5CV4x_ox!x?K6Ixhm~eJgpU!6Q zg^?b)fMqFWi||43Xl{%&$zcjg)pcn+E)sSYDYhW0T&-nYaxOqxz%V+>LMs8DSZ!^S ze4Vh{axy6{QmFHe`o!+bC0Q_hQg#xgkdDg$lz^HPW(P|)#Xdg30^y$`8RV>}zj`~V z*Zz%CB%A2y{5^dc+Iywv7CI-%w#EZ3)0AgJ^a<*&&|X&uYCh)}givv$Fo1kkJeDDH z$oj+t)BsWVK#`RoN7%y zkr-a)h|B79SmkuT*!NWVBERQ1RpR^Bnkv+;KwF07YfjY?AJ_m{l-O1#z$`CrHX)%? z_#x?d!rO(Gc?(PpM)F0-CG~$`B~KuM4QGYT55dk_Tmx4^PYhvT;bGaTQ6%}I>$)3I*1f~NS<;VLWia;^jZ6pz3lW?C}#YqwKsMaO${Jk#OW#@HY;ydqn zRL*vOTq^^spsU6r2a@h64cTp2EXfO+Bf54377Y^7h9@foeD$^Y9+iMv+TwT&(g&*$ zv|!ucR5X}W1d&D>Dp^nepwuvsgr54y;~??|8x|webl=7jVjvK*n=qYHHPcoLrVCSs z4Z?tO^fRCyTqgX*td=`g(%#wApcher=#VI}mre=WIlUrsk{P_fK`9UgdO>IC1)OFY zheZuj785oWpK4nu?zUw@L1k?hg!S&iKo-%l`ES2UPR7Kwg32hom740R^O_`9GW(j= zg7&lF=yM`=QVyxxI2!7Aq>y3Au@9l34}Qv}V^&vxGh?^!SvH0U!26E) zAeFBl&vBG#03q0^)9r7NUp-8}UE=#v%NQd0O(GS!5~xD_5fn!nJ9LiG5-nzPqaVi{ z{eeN=Pl^Ua1(OXwxKp!cjf50~P2u0PwSBgTr-<;kv9=E$s)y=?reJ9mRzO=V@Q^xj zF&DTnx(5=%fUvX2!anykIzlTuXlQ?imdee-?3mfeN#`bSZUd$j6MWEa+&MWQzE^)< zb2DzxGLxpbX;)N5Y_IuTsBq1_yrKo_>wn1|GG9Rvz7#R@3)u;r#@fsWFpH0NsT>VPDW16v zV05568Oxfw#Doa0Gs%`_f^KMePUA4x;KJ?`y=3sFh=sJ9!LMiso(Zc9U0}qo6yVK9 z!5mX;b)nA08dArkpqTsV0k&C_)wudiN0ST-Utdtc{S>ova5&I9l5zJE{f<)tKNmA- zOE$WNy_;r4Rz)H?;>@W4d9w(l$(tx|NtkmYx!J_{g(l6QgdM0m z9J9MIw<-@mP`^zsV7+?C?U-er<&bYITy#`3r;7x?r>QJN`H>d@0%lZBg>lZOAKtwQ zb&|O;vUJjnf?B&*3{YK+0x#Acrb(+-bUP(BleY5Xi*Za1S~W;=TGl6%9=QB6A{9Tq z7OA*p4N`H-TBM>)oF)|{vP4#s3KPrKq#{w!prp;5#b zj-gam3qkfm*klX0Xm{WgdGzS z81q0C$B8&qIJYEsb|Jg)R2xbmO;ubqY6ZnvY^@O=;g)UM5CdepIr z4WE2?5l}$~FM=-NLFKE^k8%5p9uq zhog{OHHICiy52=4rCOTMq+HLGw3MYsh_PYH;mT)dn=YvD*39-_JDy<&n-~79=8Yx#Xp^8hWr_eRcVBq&a`iVgva+)#Vd^QP zor)eM+fZ`?=_0+yo4P+VQw*n*u8Jb@wKi#0zhlioE*7fkt1vWErfz535 zU$=Ql&m8sEj7>y<@#F%V85w?4nJ%j-aBO9g&{IKWF$_^SFPZ=P`Wjv$*}5c6R8%7K zG`Y~Yi1Lm&nAAmhz(FgObhv}_!Hf)zVCMUtOeiI&ijG&hUD*6Zth*`$sp2*TJ6;n{q?X+q2J> zGV*c>w5t^%DO7O2e!MN{Z?eYZo3hNO8X{+>-f~_2RyP9fVZ%XlQtshvj`9FpcFZkz z8z9R3sjz|m=$BDqvqKb>b^+hzpbxQS$xc!kS!lqZF6*Ms1&f_K@L80}2xbx5lQ{mo z=b^0rPYNxn(xZO^A;3`XRuaF1lyeW%cPqn(Nf^Cvq>e@7$6lDmQ@At8YSqQ#AFy-X z(dy1T&~}DSCj)+Kmx~yD+A&1Dgf0YHxTZ11XFp-K!|*)_*4zenvca9O^=)t`-!ksx z#M7J?zO()^j}RRk`HBw^ALikN)5)D2Rbxx_15Mq(DkBLfIc#TTEg5`I21};My_Hfs zlBAjZnlpuv!c}plV5X0l^6J3}2D0f)rGir`AXXV+sCiJMq)lkcU~g{kQUpc?D@%j7fH*JE894nESYZ|9t{BZNMTTKxwWtP{*cKFAe#1qxq@6<6e}AN@cI zF6l<;D?B1)u@Ch=hEazlX!Y?fjB4Z&#Cl{b;-)?x^hw6B@~{&X@6#2rwQUegZT{?i zO*l_wS`TVUZ!5scyzxwB%hcYO1uqKkAh4-(=UHe>IG)BM@GY$-`wH=M%oXS&Gy8MQ zat_)4H4cxZvoH)_{=C!SMlxnVeIs{A#}zCbQ`8I(i}o}+A%k{n_9fZ###$JopeGbYMV1uEo9mQP+PB#=SAVfx0a*A4Hg{u^00+4EdC!q>$D z>o9qzzp*)*TORyr8!!J1Qp_l<6CR6#=5jL#5Z~PhYTr&;tZgJ zzDDCvwUTdC5iNpiK79ND6XqrZEER>zN)}toNIDX~4UV~~SP_J_Xk$eT zSRM;~`C4x0fq6gS3u|K9DtsX_BElES|5V`%siuSNYk1^%E@PM6uEqW)R;Ra;@tNvsD*Gn0Cr-L#usX-a0CiP`a8>VLQB}5zkpVJU^ z>O(Ehurlh@U}e-P@4594aw~P7alO-;|CPlYfs{9*OresWa1Imrz9^zhO-<`$O3NZ& z=fCL~Q;1`mph?`Rl_nZ);^i4t>O+Z}_?r?{it|SqS{Au`b_fk2pCGgDiK(++jQITa zw>=bErq8z~6cZ-Uu`-J3{J8EZtYcFibE=ZVxfpm%iJe-RG4J0sF6tTS*f=p&sABzI z)Z8*JaeyC=Swi=0oh7eVRU-Q=Rk*BzOUNO3>}=hD`LTP9a_g?Du%O>jSYW*11!3=g zli)H6v+H~86+~I*7L<>^IE614(RZ|l$S-=KXr#|Lv7BFA0(0fCC~^RnTGC-r=#Cd ze&T&><8h|zx1TWi&J+u(vzEd#J^Z!TIxG|Lud`j*iLbp@x5en#rJwgVKY70|-ZMG* zQT-A%?U7GwW6{rV34?gx0B|0Ijk)oQrlQUn835|YdftazTn8FJh!^IL@EK(iOHRJg zqJqWnvdz(C44B?x#-zy`a0v$~bj9&6SgWG`+Oy9-+b8ZcP2VVaJf`KnlK*n4I>O*c zl`F6!lCn$fGNj|0T?pjDO>&UrbRI{9C-tK}rX6Xl8ANgc=rMw&R<04iXT!5V-flHgmeEgc5)e=qnvU=~ z^hG~?XJd__7wWUZ7UhLtIPHHY_#_>2)0I$!wO!E|D_Zj@9q;N3vNPHGq=&5F#J4wy zn$Q(g8-x!sZ^YdMr{e}1UHP*psUr$+p-!<}^8X6t6+&qgtP^xrx`aj%bdgTVKT;G- zRJh-`#i7B2ETXnFeOh__Oi$#=ZgIYT#t=}I(9dvzla9FTWjV*)ZvF1U^2eU85t!jO zo@^1P*K^TryqYwQ_Vxhaxew&xu`GeC+^iK#QEt{JFc=W&>gr_8FFPBr_<;X4%{cNr zmJW(+?FWH#_MaiD)2)x+t-Hr|@+MMuy#U|7Zj0QRaFiY3g6zUbpcXKx*v&7^;hR{E6YoH86pki!5vY0 zB$w_M2u}PaIlZJ>Vh8Z2KuuFe` zDFq5aM5|s>&NEbca1?uWMxrdYVwwWTALhPUOxJc;!Vbo;NV%enzCg!RPoqUym9sv2 zB=yXV(r4s-lqD)+5KKk3fLheIV}>#lc^dh7H?B$LL8s)CzQ|Rn+DvhBEAHKFzF>=g^&UkP-G?-6{_M_x+Vjag;xJSVX>Ct%XTFU7g_nb z62(P#E0?6Ue#kg{cwoddvF=KR#HRplY9duvA{zQyNYW^FW%LzDZpsCjh+K^xg;=h> zJPJSxle)MVF7PBq;ueoD`V(X#u9Vv{Z zeLT$0-t&5Ief9nK{5q34dUWC2By=`6TR#28*TPfO{M%LDf6wd3XK>^1UcHfnq^JBj z-cQ`~dfoF-RA+?UB`C1FDq7wetkATC0DX+*p4=Nv8|i+T6ppqp)4>KJUJ&ALELpmp z*-4re^2)ED+`uEJ61Ej@no=KlXH!sFJOoli8Lg;c6F3t49S#RcX(9S_mbfi#3Wd}F zdxQft%8vEJx=_!T6vgj*HtCxEU6h*5>Z%p6_;u6n%5TK z8hB}2ks&5PNmgenIRwdAh;H>*Z;|VH$V_<3H_11G_YRI6XWWIUTy@6&Gmq|nHoIy6 zJ8wL4^w#4i4(IP!d)qna*Ois@ny}udH5iGjn$6X39VNwXgo= z7ys~Ao_)s+H|;;%)N<&i@fnvXE+U1tAYI9-Gyadeh+jqdYSYyR>)lt4 z?1M$2yC)iu2lzZCZR!`0r0SBEWIM!U)9t_+Vy!X@!9$Se%~N^2hpFHdDF>8_a)$k8 zG#PE73nzu3o|+m8(Ll-p(E$i>8`_U;^UA~5ln|$ceBd{DsA4c`8dOuazNOQ2i>7>x zoYv$uIms6&EplQj1j_oDj1{TwH;EH4cnTVxUmVE=9}rNNvmDkBo;T>B)ZIeL^N9hb zOy?K3C_?0Bu{G^EKySy{|Z=rkQ?cWz*6*QvNl z7AkWdt21&aifmqeA3YxEf- zl7?CjH3-M_q)=qy#broWyAR_Wb}M8@z#DyaDB#4X>A_jzBrK)qh(LTm9E;<)aAKZN zC=Sn@mg7TnfHn|R{i6))yk%im5dG%?KNih&b;ZN03Zx-yZoip=of~xCX{UwH6agYK377A;1uf%CnfeE z3$Yc$I0dtrNpR|N0ll`4H9v+;Z-DbJQGSt0k6Thk2RQ;lHKKk0$nApW&I*g89 zWb_$fPBj`odV85z&8mQTJ~+f1USRwMj-!9JImA16iod0Ucjs)BJU^U|9^YmJv%E!$ zq-IU6=#Y>}TiS&OMkfX_n-_+NW|ks=ChSPm8q5Fl3uN)pizd!XT$GHfy{xA!G6$pu zHfLH>Iz_=o3I&cAVNw1NqpbIm@DohhNA%+v{@5P%bAzH2V$T#2w^)8||0+d9kPVfn zw8ve*e6nwJR-A_|aLFfU(}caE0OmiD%OG)t#Zzf0dzkRB=bn?jRUU-^Q?}<|0>LdO z$84=lV1_~Pg*=!-ZiS0;a4~5st!1_4JQKKFg!<6&NlBh5h9wU zg8lF;Z`TdMC3FV%#U0&BN7YHoAl(suY#T$~s;*_irDUIyx&ZSL_TPLyk1_Dz)*Dkl zAg?L+&dVx9Qq=^8pXA-MsM9{|2}9QnLb}|!BT0*PGgfh)W*i$j%_@8*X4Sy00fcc% z=tX%Ja*hBL-TTx0AzxL9ocWug4nZguyCDV$DxF|SAB~puF-p)6s+)J+qpv_X0EscD zY)FDY_t4iQgsG~sAqYExuArg=50r@M9(}gZE!6^vXFLIjK@aCvBR}3&_1Qj5fpZU0 zv(8;!!9II6ukC6m1UYa1#**>Q)MPLLJ1_`KE&!Q zD;WU4M1-W15cVPUgnPLs$x$DVz}v#T$v=-r7L=J*K+UC|nmy1IYat#8F>y@L9E%Sh z5L1si?Z0t{c^n1CI!u&W{pwze+-jiJ1Um-n>j_poWeB3eEZe%sJz`C;8|ttVKJnS& zEZRM>V_`G>q|{;K@X2H()VnpMn}Q#L@3;8-1L3h;%NNhvYbB^H`o_mOnf7crWJ3 zc{puNeQ!TXH7;Bc%HA+%4I z`gzA@#=W9Lj40D(Y%!W*^v81WD9_23!{T>EURt=Q$$m~&BkKW31DI*w^A0bztK1_o zxuwzS^Mp}~>4RewNwdF(HD_t$70NF)3zG+y@Yp86mGG*gpA}1hD<06&1(*;uDX3_~ zqCkt-{R3**M6tp8sm{CaLvhnfEKGiI$C1dT?&v~6$p zIugoV*>c<$%{}wYfH(?D#_v#odHj&jw83E$Dejup&N+>?@_CsV{nVP%;hv1Xve|%W zjZY=~Y;R!#9gwwz$^%DPl6s!*(SlFE9EB`ChAtA$W)WWeg|)Td5eU@;yMRT8)JZqx@@r84Ef#reoMEFztVMA~ z6kiFAu)*i^nYM_?=&~H>5`CGN>|&7`-`dlgXNt{X#k4uv?ew%!?RwKTWtDttqus&O z#b1`O>VEI$eBR6>doo+pO{FG7k}9ruL)u|wRSTxZ=ri5)cJlm9%^qj7bua#!HvFVc z+i|kp2x(9A#b3YF$3&wm(deVNu$McTduE>%fH-U1dlU4j*q{9ac5Gg5TucIR*e^R6 z#CZjad}Ldc0vtLh5`yb$6EqYU^qK#^X~)}D-ShmtsH$#&_IpbZBLkwZs)IZe)xnC7 zFmUFnb?bpNBkTyA>s6ei^(OkoPW5m;?q9BN`f)-o$_#dNqeC)hu7CuwgZl48_CFa6 zJsu1ez62TIxOWpmME&@ucfnP>1KHKRs-+j5_;v$Tu)BgAw?lzGmhQmT1;WT5*-$facJ-EB` zw=4VcTyX#Xd*8mu*sAR9K#bI5mci^onq#I2+DFgp*D!qyO(zg8$dZ!xH9tU@7hSp_ zHb^X+<~nB{%KETcR+a2CwAra^>w@;@N7ca5{Dwt$_pp4TC!zS*=zkfv-po}F5oN6y5ck?rj<=eg6bGt_)bbb?r z#~!4UZ@Tw}{lyKyOmnO0ZiU9zU zTULbv>V9lc6o)!myZ6P~tvUOVMZ?b_rm&=7>dJp&Hv{X_4S z8?Okq{wT8R=1yj=SC`W3I3efaBcN9JAWV0_@{4c2aUB)R3a``nQH}ptE}wp}w+d?^ zb^hHXbABl6Bk}vVvL%&&DPgiQ9aG{hEQ>qd$1qim)c!hEok+$D&@Bndw6ZZIKt`+c zF;t7mM=-s&V6P4X27lORBbH(}iA&;Q3B5(HRRA{talDjdyOz^>R(B(z{0;=1{JCuO zf4}m(xKX-02l{yJT`}$(9XI`Bq1u&W%o@JG>Rz@3af7C)719*QPVSP_eShvq$||o{ zmC}7~Jh;fM1eNGlI8T)thHkxKNuX3_nF>@$;eMW$G2#$mV@X6mDypI&UM@m83hMZ% z5>blNf0Q~40!t|!`*@&PRVlE_Z?m22+rCTWCr{sZSbxmgmI`) zJA6-;^T0w8KCv3WxAKU1x;0_kyZiSXy)YC0AXe(`ua17V?AyHQAVK(KzvzdkGRY8v z4dgtorg)(9_350sDC@nBdy)V5Md!$Pc5it(Nzj`5H=aBps3AfI_hSJ|m0WfCzt}0A zcUXkm#y*j9GXnHW>h0f=5)oc}EG<7ACvnVSV_6r=(j;2FA)N-CEIJKwx-hMh7DTI4 z>P6WhXl+TBAX%HVifP$6iKSGqo;X>Expxw1TY$UMNoW~tNjjB_#>9nA=i=yK-yv0$ zG%60zaoXFaApvK)?N!}rnm0`!TQJAcyNF^*R1VxlbjLc62IguT4+yZ0@ohNz7ryI; zS1tq5teaGJ7IRBlw0x^H)4HSar&rIkH4zXFTfR-swd&DCNT7K(c$jL`^&p0)S`?zV zR8wun7J;%>w!%O#<2np@s?}^e)pQ;5pdwj(WxnIQ;3Q_y?ZolINz8MaZ=E=t1U5Jc zglLk$87EE9rWHt7bYMXSk{DLwbQex4CKJ$;Dqo`uYjl+9b{9gKwe|>sD)(q6p}}R% zJX7v*pYR`fUW@tZZ0*(jCx3E67_zaNPb($)maJyq{>n;>)r@{(Y3hH%}|rc z-tnzn&4&kU&8_ox7p5O!xacFc7tqIPMj1MfOgO9GjxBCh$Zb`6GL(SzXFu>@W<9a8 zu$#By!=Zf;VB~eq$X*A3@%f+!U3NvHS`F*})ckgotGNH932jAfg*NwK2I1Y&AV_;S zkWD5#IR=w580C;R2{M7Tn3y(>m1UCgCNoL)p7l*6ZDq!0Bx^C6gl(0Xmz3D`EgyO? zSxV-NG~;e^hRPn){DUto8oNm1H_*8m2)O!WlQG%O`ADnPVdF^dn*kjBO{HimXHaN@ ziI}BtVw#`1y=fJhQfu!xt467ffe!#ozBs47f(Omb$zTqNGCLo#S=l?vo(njT6MtVi zPQhe3r%a~;M_a+105WGl!YtDhEM8>8IG(kljRf5ale=4($*cVpur(*p=FtOz00yR*enKj?jj(Ah7b* zf{=-Ki?TbDL3UO@^81rPF>YF!r2=%xd8jw_%NkOWl!e5kn8 z5uuW<3y7Sa;FK(0lH-o)KyZ0JVRO$eqKmR!1VVt-|LDI|PLZs#x^aone0|Z7SHYgs zcZu7S?|bLbcJm~>Wu3oKOI+u;Vz||*uV-deUZrPL`ZI+0g9K>sD#J>pH(Ia=ONZPU zGqXF(5iPx8o*5>X80M9E{qQfKG1Xn;RX@I7I&J<6iUXE4F(?PaDte z&N7T3|1gsUBu^A-$gV76I1@EUH7jFR6q=f`A{}80CQ8TmB~oy0`uxo>xTfIJ?iT zdFI2CYWuoJcc5WqZF5%Sx7fl0!V0K_&|JmC2o8?t;kHpSOM=%u08}MV(bRD+?i>J| z;~5VN`H(KXma1@80#;I|2l+-Ft9`)c_{8j?ZmA? z=&)YK?jN+BC)kf3npb8UQ0yv(?TXe>tzFS5#Vaa00L+2%rM^6rqO%;oM1DCWin?M+ zHY44-F3HuZxWAoYe)0Qm{dSj>gKkc8OoS4&GhYjK6Z{CAkk&$z1?BktZ1hJgMp3Cb zs5w1{Qmq6&b1!AL@tHSS#|@hxF*+xD#i`gL_{IQu#fQv=tey8s^+BCfRk*adE5?E9 z#Ehr`CgWDZfMTT(*8WSZ2^6nd8mvdu>|p!qk+Wm2#QqPY)jqN%qSui2(6mauaWYgy z*~K!ON`xW;RV1txs`Of{|2E-BZw1Eq*0F?3z4|AbKF z3Gbi^ELOmSmjwHP$+W)bMpph<-TI;;{i$UV9%>Wh*o2fAqNfmx?y;l+!cVXpP6#0r7YS>xx2J;$ zOW)hMZr{TVNe}RBQ5L|wYLp(7CjbUPFj!dJ1a8a*_rPv#mmXj*0hT1MkSTBA;1>Mj ztHAr&(O|s?ehf(Kz9Nz%u-vm_Q8B{llRuD5Q?e}u-tBJ z)dI3QN)8m~U4(sHu0*fWSRtCLvtl!?XKDuBAVK?WCv4t{Xz_~N-vewOqj*(F+bT?1 zN#TTaQT@zx^~gO13!pm1Dr~}JT;Y68y0kD6CYcC0z>mErz?nN+oXJ_MpiKQKrFi4X zw5{s;;40okD-}b2?wy08yTXGxmSb9!-q2a(XGK_HIU?C)nTv|9(pmuGQCDt>F7otqtg=f$tiR8zK; zJ#+YC8AWtz1SgW1F?!KCiBUBAOg_v>_yNa~L-k@KVzefsAsVyH0bxbXM%Cs=QXy@7 z`@q}E@^gO`C2e^M!B^U!rZkuhOz$G? zsgwp-@CJ{xKTm1yeaiQhlm@wK+jz9?9cih@pN=%;`ssnCH#Tc+oDbn-e)fk?2oKD| z3!SZ0s3r!wT)f(;?{QhK*OaTC3NM2*`lXYeP6hf^G_`UvID+z%&#u+zun12ESpoI8 zLzM96`#n_MUI!w$LVpouOuBc9jX-t{cMQwLGa;UEj2t3SR&GpT=n9MiLsw%Q7^(PR ztt?N@!|m_Y=nXpe<0Bo09&Zm%THemm{D!@{_yb?^OY&%voSc=v+{0N}7{bn>(AEUc z*2VOYeeJUxY1yws@lAm5h`8N@N{jH44Fbsz?3s2SE^#MaBXcxNFr<*Rlib0-#%?>CSrd@8OJJ=V5}j)-G6GJNf~Wqm>i{u4Q7 zggt}CP?SqC)c+}t%4qfhj)Nn>EFD<%uc~7OGw7rxd+2R-|2n$gyY3py=8p`nt}q?P z&_Ia9*(yGBB5XbfIXcD16x(5qk`{#^qGmd@(`pyxLSFW7?q!BHBagnWe)gTu^yxx8 z4`9UExvCyrC(xTU5|IF@pJDSVFo%PNXXwwPlTfCnlSYmNV)As>94GLAw+oPy2`T=` zeHPGIiEw&~1yp z2(^S45zIlaopDbbNbuwITiZVTY0wmUF#1JDh~OBa30W#00=YnaGY>uz^mMsUmn1D- z@7@eL4oa-c{M`J(sghBS7R8s=TK{BTl?(J)QR?gcX5bxJxrE`PhrltzB0fFb0mb5! zcFSr5?RX4}pNQz8sMc9S`enTdY&7%|VV)C^TGhMZ;6wtg`BFM4K1(`2hO)Jrq;Eek z9+G|`?tClKq13W8;qvscZ%-pdcb{t!Gio}n4Ga1f_7%zoT=fzc#I ziSYrI5jcY0S2{{a;go*P^OF%aasM{)lYtomf|%~ikTo-{v$gDu*00ni10#_1M3~*A zH!h)0Z?MhrX4)p9sKgdw)~{}Xb@jut*^loM5O&D~o2})>0}K*2LUY-DL@gdtZqQ4o zG69Cgs54L!5Gs?(*^{ArFk!07JR%m*oX=9g}n0tlRU(+zsU9(hz}~JSoxyRoDDqGcM1l z{9*#&XGRfzxaFjatq)18ed7a_DxZYolmj=s_dKb^L{ztbgAgD`SF~h7OQ7wc@+Yq89A#Uq^!lB#b+lNj>#P2~I z&D=x}F{}eN7i4EM<4`6Xa4fjQQ%r6l3iEG>G;Se~E0X0FA{utKgsV?)#lNKl=d`pT zDtzTF5dhikA$u;XX@eUMWpJ;J@U3-IvOP*Ap&bm>S}smY@TjTY7C6APv&#R3>S2Js zaW5%>UHHiezoQ(0H3^T5sHE66zv@nXz}~9sT?_gMY9x~?&59?4K2MueF*$4-7i1N% z3Zo{2FSz-#e*cF~2)1VBS3ylwrZrOnJv9r~(L)eV1vROy*92S-8#Stf?PAH5H`I2YWP~x-}MZKhDlNqXL5{0BzK-8Re$W6wvP2 zJ!pXse5OQFy4P_LiTUENTqf5rMv353qtqVi1JZO)#~dO77wrY}j7Bd!(2??RBajAz z8US0a*Y7|o!Re6KgVoHA8fiUa<}+fJIe8JeMOx8fh0ATWR5wPa6If`9gP>_yh_QNP zW}8#HS1FI%eXHA5V>8k2CcPf4Htmp-2AB#0NhTF!;Reos@pe7eMe>G3TGA6)+;hl5 zq(_@`qUh|`0#yPwryjx|3vBcCP+KbJpz&s3XSmtvU zq`TrpU}gOqrloOD@^996|VlXw)CgIhedeJ7KH*`{?BI1{n{D2&@H=RALQc946#Vkj^6lzKp1m z(=|y`f)yo&$OT}<~MeWA1QRm-T8ulZmc7*KkJMP1F|LXGIKTGOHQY zp=4?VN@YexUZy&yRAy8{?bf<2YG6inFbQd*yU6y=Bpnqa?VZWIkm#jagD3_8rUOdy z`Nh1tN30~;fts{4tt1-Z%B&_ybEhkBuYg>DdF-hsSdd6o0+U@YjXuFFi0?r&hQNwJ zvetFpgr`#JE5ui8LMp{AcQQR`&jeNY_XX*fTM;I7!YMC~2n2c~pz~GqPQVzn(>b3TZQ6A4NH2n!JYao%G zll{FUI`tCf#KAQS6%Vxe-efBzLHMI4T^VaxEFT^*C8a|uE~P>dd?awfy);z8JfsSi zM6^)_OA-pSVHxWq?}h`gL|(Keub;Sy-&Pc|!3$;UM~^f)GPy>vl`3AoLS0Ld zxm4sDmtk9pGH=yhGf20wty_lgTdoC}pUSRQ%@JoZz%)~f&Q@&TH5YhYEz(P`pOtY3 zeMK;0EEe1j0|b=qEV7A9z~c#|{f!BX>-1i9 z6R7E%yK#{Byze4@_P)#Vz8)q*WQUDPhp9q=E?{Y&9D*vDmO}g!4Y>qTZaVt58gT;* z7U~V0I)=Tt^p z(CFyaD6Kv=y<7cs6_DDtw|Vrom*rjVu<1D@!1maTv)9hg0tX$e>M#G!gYr$0#RoKN z(OucL=r9*3&a`!tTM9yhj0Ys8yjF+m`j(qNuA)#T7iCr0a#Gv=q^MYTibDD%JIU&g z{0Fxu=&3!Q$WnWww!M@)HLG83>u6vUsc2X~6s-eU79@0h3^~*}xPTg)6Q1ghPW>?w zvHSFcnnUy%HP@egw}(>bOfuas9mgz8VoTV`B=AuD1vWE9FXNX^%hbnA>ztimqpe5y zq*v=ON@J7#{-{c(<8_WQI+9}&t8xL1f?NhBvUej`F)E#tT{151H_51U*0zyg1=qk0 zMDALo6Dy6~N9oEnkdj8NSi2}ILyaL@1S1xGEe6gfyTZ-8S8sxG<4^k=gp>9u{f6bg zAPXT$jPQZqelQ#TF!$VUuhdO;G3ZVM>oy_4gvLY z=L7D137iG)+&BxoAo73{8#G{p1|SpsjC^!n-~-?TKJ^l?5BSuE zeZUJs1Mb`412$*?G~nl70y4p$-;fEuAT;2S4H~dP1E2wa@DiW_f3QIVUJx2^=Y~vh zg9bnYKJpSc3w&hbEbxNRfRAj*12$*?G~f#_0UGdy4I1!*(12$)>;yMx05sr^m%v%z zj*YXx3qk{qZSVmbGyod#rI!F7@TCnt;02)pcW>|ke{*TTXZpn2Ds@|26H%{JL@UL# z=C-I6R(EnioW#OXs4a*4jlfh95-YBbK9MdtBb2{9X?=A(ewm8&lv8=Jo5Xi2cEp+& za}x{ao&`D9J6;{X9wv6wQgNl+cKIULT!pzJ=_*AzY@;&ap{gt9C{{VpMTEN})&mZM zw2c$2KH&P<#=#>Jse^quVAPcajN%jFE|hO{2gXJUQ?;)q0D4yPuE3x2g;pM(DON-z zyH=7T!beG43a++q+}&GV7DkIz_bx#T$Hf6*K8xi#ukvf_`%8ss&Lb?Af|}OZuJZ94 zFH*sbp%FKlgvUteMFPJbR?0u@O4Jq}$xE1e8(hd{R z7K&bugA%Vh9p;Lcr7@u; zi0>J0z=hm{6>NgLjbBT|deB3^I)?*ETd&L1g#bu^$|E zlUo^xM`vVIY!M4;Hcchmqu z84?oE^eomiwoxk~D6|oRLJyAVG4?RZV3yK0AWAUv`~RPP&OP_7kE8$vmQduq=VPC} zpZ)A-Kl}OG&yHLePJJ;@#tYJJ@s&ysln#SW3O-?{xC=W828oaAmFuH0=`j`xmnzKr zc@hj&NyUrc(5+5AvztN61)I zU!dH-WbVDb{)OYmd!!a*Ae7sSyk5)|Fi)~05-xyX9jvLAN!37V5eR^5xGXvLPHf{d zQA9Rfa@Q$BKH{j0_HwuwtcG+tf>UU57XMk#LrS_yW%OzWTUnSBLI$0|9=3y>@{M!{ zdnm91Av(vPGuR7-Pen)?Y$seC2_b_9A%g{kkcl5ufDkfrlHbBPC)8aUj(?=D5UJ#4 z2K$S{5b7dQqog~svT>A(7LHP>XQ*nbT#zIe69xMKOgL%%?_c)_xTp(|x+}7RrT3&Z z0RNh(oU_^E2lkaM-yL~rgW!}nnxlhmReCG1Z62^O>-z~{lMhNcVPFFjO&qud8DGp4 z#ws!uG^A*`HRlKn9*;Q1gj^IeC2OBfz_2o!-S|qp~SLP@3ifsRv6))i%NPWeG4xy|nVd|klIYua0Y{`S#7Fu7B@Yn2a9haE1upIi*HgruzTv-+Fs zll)9Rwn5sEJKn&9&H6!p{NS@gmO|yGo9D<;1(A&;n|#h^Z8U5IWPXKnV*DChKvLK| zFOFcG!?$=9dtgq@;xjl+UL4kgF*DeCaqj*-BER#kd2zB%avMsMs7*;=kmJBQG!}Vr z9*j?$yf{b~Tl3=ZDftKWsgiGtP1YZakD9zVjI};;dnqptAE`9CR#LON>$INo;@tlG z1{`TW1 zQ|2N+^IGAP3k>gWHRQBKk*bECgKqV_3fMVpnX`?Qtp_ZU?=&UD=_^@|GUl}RP@#6o zrh8=U#bi03{xCDFO_uXZkt~O4JNdund2!0fi<5oPd2u`)ZXNh=i)YM>Bl1dlamwWr z!+fZDALl25Jccc4wZa$&F$MmV%UC)0ltoz?wptYtuwNzYqYl)^Z3VLlHu;pdc}oN(vB)bpQ#qP&oieC_20%)0i$b5HWrUBBT#xxMD>_v1|Zac@?YW zpaD!nBvkdZT=(nkud1(mh0acD-ZR&PCU#T^z`_7Z7lLgVz_J05^|u3{4l)v^5EBt} zugH*d1t8gOmAqMsz_j_h=i3ZWFG4Ot@M|OK-!2lg=>&BJOl((wm0GD9&z3S=}oo01d{b*#}-! z8s;yv4B5uYdf_nQMUgs5uJ2sQ|Eg*@`0wsERMOq--060hB1H>B5 zM_QvjKp8B_r@Zq(Ips7qbR&(8s3pLNJg^?V(?C|l$iTP)Q#;Rssola<%Dde~77}7= ztHoCas-pQ8Wtqry5o;R8EkGvY4Aq|qf^P! z@Io}%Y`Wq#6z%7T&(nkdzzdNKI6XuQ=z4uqI(G1_xz3whuV-3n*HIz$tCgH3*QdpB zGltViTv-!urDkAQyTrBFbhU1~jt{cMPMyJv$?d(aMOdGp-t!6fKPbMptQB|iim6La zbdx@L1>dZ(FsH&c?@TBzYsRUCc`>(Ht@ELjPv#2Ea0<}m{mF{Fs^OK&5(MWMVN71nJsTyMRZIbS z3VlWW=>~lbzIlAq$QGU?Cd`aQ9kyD+K*-zJTmI!?zM*ONSm>pRBy6wA_GZt~0R@pl zRLoKtE}4<6xcnfW%A&s;BNBHT$VB{%yz1{(-unVJ!~apAi~4=oZB}c}qgrG34LB9D zoKCn}=Rlo_M8VR?M;HoUM{l}49?VF`tV!zWWm7?)QIuuaQ=!*dPd&?Iol4WoGYaeJ zIx8rH(HEX`=SawV>7lI<5}W;iNGg zPOh)tB3mjHD#~Ld(W4T6$)*1)N*+0~wyW$n|I3?;^s6%jCaWS_I=nY~-F=7S&=Yf<4Lu>F(%s;)Z^(!wS9V?fP;5e+b3_Ogot*V!SF(|gI0i?GME~w9o04x*Xo#2paR{8 zZbI5g)=LAVlMgt7ApokErW+t|2aQa0>M+UHHzlhrL*9aDpU%82bOw}^^mQxg)wV11 z?cD9SA_LB9! z1Q{v`hOkwg`Dz*gVi{pLOhomCxKg>Q5hR6s2si0daEPb|$ALsZ)_4vR;e%0p@C42_ z?v_*<=K>$Fjx>jfAkfPaiw*GlDGr^+02oklyh4I&whXu+fl8uAoLUB zz=#HZ)I}TzcL;IY192J*>DPfjgE2~ng)Alf83Im9Nch8`ai-%~)Lh_iN8k@MLaeK* zd=)~ZDIEYR>&fD=A<*6O?+^2_mOu`)&zWsr;gA!HgSxD6^Z3~c=3p;yUIR62n4vsK z&v^j7Nb6t@qjpwlVixetu<;C{9!(N?XWc+gC$9~8(Ti1wA8L_0D|k1H+wqw)I#fv^ z>=WpzEF%>0JH0>Ce|*=c_p`oTa`5=oyng)QhqK9@=W7-pLdj#@)63?2V$sddx zLWm1rqEbo>?jL!dP;no@)T~9AZf4VhrsOtM3mFI$T>>Fj&?SFmB90NClcY&J2s<4H zNW+k%oYjjYnTcx8hRyU{=lH)q2ek+!H)>*90%1X5BJk4_)6z`jq!T4)6lX>Z7t6_> zxSuuiAI_vc@f62}e>G0(dq1B53#$M6r2eu@>i7Lap41$&*Vu?SXT)@68)fO(2I+^` z)p@cpV@pGo-aL_0Xlp~nJ2k!-f?Xa`&b)yUBfg@m5f3JwByopIC{WlB(z#OUQ(=MQFyNvJKdcg=L%eN=-2v6?E%r80z#74Ag^R<y#GYoNUw!GQ9m5o9ImMlg&cg26|`VgEow??f;-Or$jGI97=ml7X)yR0M1} zg29Cpc!0pX7{P#;;TQqrbal`{R4ll{Q(u@y1<{%m5e&mXmfA6vED^U645w6;A{cm1 zb&IW}2nM}M@g6MzZ3F`<8xnhpU@&xXh7({JbsQ6g?9F<~Nrz1Y!;(a0BvwR(#e{=nyBr*pw-2Ur4*sR~fj~_h3skRZYTXDpE%tFn?`M;`=M_Goz8ec7Bgqez< zryP?gwp_(W6?uWorUyv?=zNTy4fCXNd1GyYMJ`#YbE;^^|!xZmmE;__yxP``1|a#q96yF0v-JJ1W%Hr>O8XH1T5y&24342FHDseyvifn*(09b{3jGrKkN)Py5l+{z6Z& zR1wyxKgdm~r~q!r>FgQ z-9S&<%0}v`8ppO+bMy^GO(7kWoicVYa)CzKI!=#9B0j>6tJ-sF=SbbCy}#V8eJj2g z1?Ymc!=H6)?}CnTNVN-Es+K?0LC*6onB?0gr5;v<(ki>HulWK60{aKz>Bo}Jsc@jq zB$7)*(Jv?G5$qxdpA2l}fCzR;@cRj5-*!L>LH&u}eJ>BBJJ?BQ{h_%`Iz%Q7sfSz) z7MVG_Cch%%XK!z|pZTcpa$ZIpwhF-sQoJwTdyd?@0u_R*ec3<0GDDpb5$WJbU2PVZ zDrNz#ictM%A*YRg#_gcwBiVDdLT zaW^%3ZG?_<^^z$uAbRxDh>smbaO~tAT6TA8*$q2_SvJL9EFnS58E;?C*-eC)Ye-d* z(eq*~U><&s$`jvBq`)ca3TMM16NusH+G;FuMNu8rNLc}0_7(9XTCkzSfjpv$$>&xT z#pA(#>1Ax402k6kdphKHk9#JZF(MjGvl&N^b5(50W6nA$6e7ClYns;AfFYViK^DEz zrgT?ZBQQTpib@384LtWFJcvFZRo&0({^Wz()V*H4=g8zwCGVWEqtQPHIX;UnYwFU! zOEtm4@}-`@U98Gj0t}tt_g;-nlbQW)1^fsv^MBUBktL039dgGU=8(Hle=r#ISPVl) zk2n^T^%c_uTmw|cNv3oV%2zOZ^H18g!pgt!^wUodIrcn)4mTu?zyo>a^+r>aK=Et}ojj8k8QMmnxvzVc6sl!Lq(drSCUdIwQV6-~?V8~_o}1 zk2H_yi60Sj(>#L1`cb>o%Nie#8V<`HOpXXs@o2fL;L$5QRCp<$OV^c41UTXu-z(6g zd6v#`KFnL;#$}97q4H&c%Kbj8xo3_5uZ!>;`kxEoo5kS$ zlEYi#*&KIi(1pMOLI4B3h=+s)>tw;x-Q%^wj9Z;;;$N1|D!IUXvAd0BJu|rZ43A@fF3%Mxr_OYkv0BnjGNE%1u^?l=J&32fj4pzj$L`HjU5RrxI? z2PMbsYZgL-rkKWAS%1U-%!+jrFu(f9v9%t-=L5Wo{a^o88|4e>m23T?==G#l!ed1= zn=fazV@jI_T}_b|Qms}D^4<(l-;QgyRH%_+-bk5erTlgv@LNg(BZNQQKrg?_<}pU6 z|3AtnUP5ck#I8hJl_5Ln5_6N|0E>m$WI-aQ|PTic*gRQ11S_ zRd9#d;mA~@0L~CXSkE1}F5Z4RFwP1@43mIphiMK`43pMLdD^!&OOP^Ku7YGlrVF?! zpBXoUW3}SPf?&bS7loex-qfnr0zHy#wxFkY2J}1w9M6oQ|KSW6a=3!lx#iy(<|BDh zu?%6KW%=sP(&Au!oDtSaHkV1Hdr9xY2qh)!xa4YXQ864|=Q)={V{3zcHpq)o{vug- z(32qyf*rHH*&}bR9h6*Wo7u@X*N(raS*=ExEhzNqkAL+3K|Iv9juosIm{r8u6`9T- zTU;d#h&xR_%pQup8NTu7A<_Z~#-RfkiL_i!etvs8#aW-q{5Scfr8u0qtUr&f3(&P_ zO1Cqo>l?~1$O^_AIyG8>t|@axrW20UIAJ1D+Ix1PF95M;EZ3CND>43Xi^2IM0(mJI zc$FN;Sto3k6qKQi6=AculbscGypi1{KcJma#P9U}O#ks+E_WyBg&FjwxtJ{|kAD>g ztbO^5~?WJ7%K*Qhkg%}>d%V#U&EGr`K%`Ul@bN4Pn zAi$i;s8<6DvNKU7_@SXC^$#nRW`ADasnYa}PEyh&C}ApfRE5seM?G&VSW<77+*^$> zOO8zPN9x;(xJK=oylw35)DTYn)LqGgF1#Q;(mG?zkP-)n@yR@$a5;m>ACKm&=j9FH zpnmB8eVDN|$jq47{svj-3CrS?!}LR{PR!#+yAFa!LZXLbl~OvNZ&Fh z!hl$UR&3H5z?&8KWuXdeNm4*G(BMkmH3yau;2?AHK}fNY07RcQQGI%t#jr2_<#j}Y z(Vt^lEC`835yr!;9v%~)nwBxvWmRZLH$O+~yMJ=<0zi~Vo3>hooUfVBt{2UC#PK%t zimp8I2M>oFq67}=kit0V)4D4;nJi=`yfQ3grMSuEAdH{=Re5NaEM#T<;E6|gu!pOJ zooar$5v5hVSrGf3XpB{WZWYo~v+vfQ!$&PW6vG~atsXN%n4OzbwNGYoB_z=MQ$Ax2 z6lu`o7Nz2O|2&W*S5npg;rcCe6zz<&)ID&F*%cX^(4XKrDHesDujQoeZ6>SZ|;`L{OWftWAB-NfPxz9)JzYee5C+-adfS@Dy~fvp{Y&&cMP#w zp)e%#!rmF)bsR+Y>NL%Xd#2_>10s+{GMbtroq;?XOdOfNn&wn|#=?NP62b=o8#uTPyjd|WIo_e;w*#Lvq(pt+8{7PUEixmX&G zO|>xr4nRE?bo=$+%6xZhI@a^@Ams^@L>H}DIV4zz2TL;eMc3@AidTpas#o1IxqUxP zncQW@pt?9R({?zPaYrJ#Y9uqN)~L<0h0ij5CMK@MnLg43z#O`dimz(=C=(s>8#ZKl z5QzT5kc&V%j6U*q-bZdEtqz+$D%wYV1hxhEzt}zwPVJ+seuh4RW41pbm+)VNfHJHK z0nK)SlkD&`r{3(lJxr;8>NLSy6}UeNg3`!_=}4rkNctMIQU}fze5jPE&#ym0D*MTw zkAzmR(7C5rKbCf0_~NZ;bNHvQJ`GMMI1>(+EI_JDGJ*$4M5Qq(+6^!!-r``Dvr&NM zLL)t@$It=ThZ-fYK;C)h{dBZU`H?j}`+~Zasl3T2XL|>s_z=wtS@W0p2-#~>eoe#- zW(y+2tfxKky1ra46pOd&LV;b?T9aF#u9>!)yOke8}BK52@IQfnadezla}lIS_}a;o9KG3oX+~qedY~o=}`{FQF~^$3=_>IuP9)#T0;0nIB7R zUSTo(H|Kc?+-+$J8jaZjD(LkFOwaP0xQdk`9_~yMEoL4yzMCJ2On`9*U_oQRXIWPg zyLCQH5rt)6dbTg$k7J72U2hk>0llajvU>YEzn_vdYWdz_z9iMQ%~B>qD9giRIQ%hH zhCrjsYq5|RPE9%@H0j?h3fd(q{$6idk&IDqX#1W_sT=4o2^hcF-MrXM%x|J!q>qb> z7ay=l!*r8KLqjjLRHG44fW4q8y3hnH+KU~oRC;ZtwGI>~n=1s3XKDsP>waQ-S00{xB-L-6# z*2C*(#pDY+Tvb;v)PuuXYhZ9vFAJGo(CEQ2Q)fx;u<{@c6Vg3-0p-r02{ z`hn4YenT_B37Y^2wR2haq-9n{CJ_WYT}EyIIk|@k(qfn(Cz+NlIbwDcC7%FN8${U{ za$KFd>t`O~@e6r+Hy9oqz=bR+k@h(#7mH;$*L8^V#PA*dtoF>|9b><)CcG{8QAJ~>*gu`Onz$O zeLeK{bQJ5^!)q_d<}_kB)S_T5X6jvk z4-VL~xnE2Emm^IvoQ>0!SZA=#(g{()a2+>qhW_PpynYy!=PT=Y?Lg3Rymp>i+<*3t z*S}W`dpSXMKh>)ZUMHPSga=U$*UrPxcF>A+@yyQqzt^5w$Fjt_)oI^4T01?7?){xQ zTK^%=&foir&(016o!QxW`n5YdJDr%%&d$TH@!8qw$=7G+-^tqf`(N?d*@2)lJ3CLm zc4ud&6Z6^GdH6LxJ3Br3dzhV{926_w`&}H2f2cSXIa0PyOfa$Ag@WzpAX)tZb3h1*eNJOvf`DPdiNNp5W_ zqXqXatvVw2C!chnL$seawAyV2(`q&FMhrSmqD%UM$yP=SBLJ(cz!HA3V7eT!L6H!% zXiBk{#V6Y0P5Yk@8Ni*pc#)=0PaN>ZBY;8o8{rEbc}YK|&*wqDz^sE{o0f z0kyJOf4r&m{~HubHx87So|HNLJy|p5nqT7EN^-v*%3cJX*CJF3J3#f{GTG#ltnVfi zZ67wWwI7|W--B8A=tG2wzr23n9$@_7MBb~%NM}da)m<{=UR>V&#%m5ySGE57*u>JO zg+t$Yf@EDKTQk;b>;$Uv-Zx%zzRd*@SEbIFI+`_r38NnNAMn)#UZFZHI2W zVYa5z-|=UUm)Fb&yYOJ>O+2W$AF#8hCJI4?-u0h*sSFkQHcF%%+8+hch`^d6J!6l9_*=)(~Ed;J6H9zes}W# zY<@VaAJOe!V0*x^g-KA3!qgz0)t#H9`^`TlXMPm2cnGf7duW!+9Z8z*ruLkd8aLHBjj-eP=BI0G4a3F>0 zjzgqAD(MguwBy9Jz@OOytB4awQoC&o(fXnBYuNDz)e)#S7zA4RnkU)#8EGvB&vCiztRuE1hPYA$x?>2t1^!-S_LvZ$72 z7{)t6`|_y7;>@VPKJ_Q19bi`~8A~W*GqM&lj&aZj5Z_AVK2ddPb7w(b?owIrDixv# zMzcuuL8=-xN;@2{yDY+%iCoK!HVO5?*EoI#|v6O|+ap8@YGCkH( z*8PB`44)Y<=!*QXOdHtQZ#o*c{FmpkexfaWdr6c?eJf(DLI@Qoi!3g$YEuQy!rzN36thn_yY~AXo{c77V1>TS zFybwA>B$?ghc(~t)ZF0KYDL)&$7Y2_=;PXjvz`z{CO-=42T5Y`5w>6Cu0hHB0 zo9-0tfw0JOBJY$|EjRvO&O-#~$u;WR|0ubE26uNL!C$6DQib?U>!=V`>{c>6OLx4* zNBX)+TknsbulTjzq$2l;QG{UB%5aE3v?rD=QMsG<+CV5N|7JSg>t zyuz3DXWoDZ@k42CFS(k${Q|-ibmfs?i2ijmM4(b`z-X^;3tH;$>qojH7CLt4QcOE@ zwOixdRt+x$P5q)&o@z^!y;X>N^ zoK9ZB$=utUrSANIB5?wpW&K0axxMoW-9oN%^t>qniuzdvmY@2$lL%dGnXN6BAXa%F zt*7yd`KNOIeTs+_{JGQ~-PUXqjjH5R zo@~OzWuQc}il&}~66=q_kh<^{-Xqol5TZ`Kxt4-=BhtV&qTTCe0|Jp~RXIWH2wdPz zzkb=V`XJ5M?3rcw{OG!4^~?0A^dodZf%fWS^>xqVP#thD6&^L@T{p~Ds^u52(xVCl z-_7sEE zdf^Aus_<$i&_GS-alL^O(cZuiN9~O}b-8KKCEXj=qPD=L>K+jii8+?0@Fl$lg%Q#+ zPInmlC?ymDj6&3p%hwO}=C33k{3p?uKbZnowW5~1vWyHDBZ5@Sj5KDVAaKFWXf_ll z>-M*$R zqFXmm#1!PJnV}6I&OFY82|AxLk1uHf+CWucRhsa_skG7cR} zODD4Vf~OJFCqg{iRkb@rmn(|!-HD{Z&jx!Y_)!|+Zf^a_EeA?P%v^P(e&UZ$rjd7Q zeT^1qThd@@ptvViWcLhn0<{UFqCaEQATu}QB7$KHvziFSRW?A&D6Ormj-G0_vI4GK zZI@^2)9H8A;7t(;4T)Y;8g@oIwu8D9*mbGcHR?xw<8~XXfKuD?beg+Q`CSbOEImKWaJ2X?h4!ubMeZhbxHJE3JslX=u?P^ZL17 z5r1V^kmEqc(ay@(5C&G)x-Vg@te>lcGV^+m*`T&Dh#QJ_w8Ux`i{@o;c2Nx{Nv!p< zqEPOjax=^7zxurozjr_#itH}ykG>TX?XVT&99Aim73LecSZi@DOk6#s?ND1SZ7rsUx|4h zbct%rk1zv1%lf59nhms?O}zs}MGMf2_=09v;i44$QN`4kIVf@fW-EV!2hNUQ<4J7d z8B88pZTcTwX9ie~V2L@k%f#aP<+y#>=@f}yK*`Vmr-6z7AX&BjG1@f6KpUN1nPU9h ziao-E50u2OHvizC>UY-jP{HN2cs|ZpURJzZJ-@8@A%1@PO?vPzxwh@k5g= zXsn=w8J95<#O={P2Gs(+#O3fqUsW5+8)i9>4-Id|{q|s6vk0 zl7NUFwXzMW$KLE4+XO_@HUSZtWl{nncuOY%5iP_BsF_*`h$2hSp6=?eM!knw3&;Yf zI@m}Y1rb5f+5B7rg);%HKZsgbkZO&h4Z>c)nM)~8P(?}iuOd&-5)`XEL1QgN3T>w^ zBGpKt(_^u77M=Q$Oxj4*O%rr5Lhfn(uvKU-UKlcXG0n?^Xd!1x6LfXj>L;Sll@?Ix@yrOVo6HPo zI{kLU2SIoElizoN?G z;6hAo2d(okSIX$13r2If!D;9g&x9#ayw46^#It8Z(V5_#sop4-Ul`<-H3YNz)^ExWghFxU+UL%rC(VZdl_8Q#&^mSu-8mu48_t zD9`AkaRMiZ^MRe#$+;53R#GT3O~?-t2O${D4S`9nJz$~S?kFdl7*E^bq8LR09-qCnx`xZ)Z^Mc(xofoVn%?tQlylDC@vw@zi z#Ng?VG;n&}R?x|fjtN3yjY&;7LIQ+Y8N5rS*j`+`)am;}u|X^j$af0eoSZJ zIw3ihIF_t}xhHIEx7q-d_nq<{EcVk_6=|%PMndFZMrJQp_@cwJ(d<0iZ!B$tS<$9jt0N{wYJlC`8RdLbEkOG~`y)^iw|7_x!&V-VxA8Genz8 zM7a^lYEx0m|y9eV6J(Yn~1`38ti}^6Jrku3}9b+!D+?o5~%qwRt zq21S~hvA~RJu(5fg zVmH?RrhSu=Uo%B_6bE$~>nRs2;wM@(0ZH7&>*D-*oQ9(QY13J|p1~J!fqHA0Shy>| ziqovKWs^Oogg>#)@&mIGd!`w<&?DLS?T0m;#z2rjbUwF@f5 zGGe%bg52T)2{0}umeEY)0{Wsn6%7`tYIzxzbD-_b1iNY#Wd$I0t9~NSAA_3Bv@H0| zOd@q9`xh8t0N$H28YwDK59y|cQeP_~_Ya$V1;UlkX5_gxjJv9c!CI&Y=Iw_pK&hI+ zk@`QW8qXBf3`0AIZw&&At{Ma9P&lwnK4dCo2UjyN}0OxA75!!wVNp^B0 zNLIjRM#?y6IXlf`Es_d=t@1Bz`n~VVHT{~3zL@EE)^ATG7)`&(w0Ex4@2m}V=tR@6 z)52M&UsLB97}ys({hDfp&vTo8-7m!*7Z~BG*i6%})7^8PerIj5gH}zyPCI9veodWc zL+i7eeoaMZCd@Bw`u*V`l6%bK&n2&2)eU@6eA`uessIR>ubqczd^gL{;n1@ue%;)?p8=zxT9JjW^sqoK0Hak}8X{Q8K-;nCHpejS~-FVCusmoxVKAjwJ+ zMm;DeH;pa|6`-=dpmP&v^6pB7mp&lJH&<2jMIi^nTZb1VF_bk`VGc$8Z9tV4<+B5$ z(CD)PgMm(M2)g{)0z(f7Nyc$T*yTw~2HM+0Etj@1JtK;q2`W%_&QM`)V8S66$3cU@ zU`0bw=@@gB!DBeh%YzAPDM+2;bCqml9BC~yVPoIafJH$9J`ysq?iw;l6%wqNv5{c} zypO(D*}p{&TG$o2(A!IdQ(3?ANG;>b$_sWmNe5okKG}xL40Xb}mYU;8Ixt1tDv{?R z$%V$$`so3qx=_QgvOQ}!iUR-10vu^9Z|RJs0?(IkN{4oxjKG|R{I~u}?v@kx_98M5 zficN9Y>{Ih2fv|7^#PFWPH%9OLeG0Pl=c&Bvirlg1vr=Vt^IKf|5L9yGP$4grderm zvU1h)rjb#-tQ|2{Ac)+j3C#Iv4+f6AbeKJcbM2$x*4rDFJo~7ZU&c|^5$(!wSb&EX?M=tfYthNHen2gk;jafVR9##nK5rZwI1?pX6imO}aX zW4!qVX52B=^7Gjw5@WWcF=MoehmwlnQDdWyMvW~goNs$ECyvngns%so5O?b~jaoMh zcUz3wW@jMDHl|rGq>6!3`y6%YC@p(6w7(1U4@enuEE{lec$#p@p;0~@=wmk#xLVbp z<0nGR@%k2#9rRr?QHnS%fC*ZvfYF8srrf=~VXhcyN7`n6_L)qDy*rDt2F;5&K7 z6nwlbp1yyweQ@qm@V57+cG@ZU=|M4yO^#2bEgw1YMv?|#Z(#v5M^EyYR!6=)UwOHD z^*@E(;V(lPRN(q-$TebKPd@}&*$%}UIdZBpHXV{?USWlK76GgmrXbynuPS1nzKI_~ zHufLLw$)?XANih0rpa4bAO+OW0LV&W3saoK49nL3R>Jjtw1H&EL7Psufx}r4=WwWN z9I>aE52c;MhO@0Eb(VX6nsIn!i@o# zz1DJj|cO-XVq&?BF4gvAyqu(`Xv`>~Os*`xE$7bdF5E60o?{hL{wkPrE%Q`D-ow zX4vtpHSe$7W9e>9MkvXEk?f7Ouc~c@03+4>z_eb9kE7<1gX1<+8y{mI0GI41`Edph zGaMN?6{$B%CqpTy$KopW?GcI`B*AieadBA7q{poRjqkkvX7!p3S~4?D-m<1J%iX+j0`UicUC$O?1MMY`Qy4 zr3hCBY4Y#-7gf0%V{8ptI&TZ_8IoHpuy4B`HR8{Mc2$TkD{*4bxV93c~ct}BEh`Wl&e&n|mlu6+x$Kg(d5C`T)-vIXFde6LLsu??G*1ik<@6y1}xKfZ`%swp}j8?5bKyk_wJ{0zHx@WA8s4E8xXs5Z6CsKmhy70=%{uMbx>_$YmTL|2i7Kku)@x&TAZPi*qHl#;r>bJ$4Mc9 z_HkXm?fuM9<%H}+y-|tI$AHtqYE=CbmRUdA+0W4?1Cl=gFS-`zqWYgvg1nQ(>h5A%&<7 z$lw5&)qsi&NHw75@X6rXsOD}*7#x{gYJ_A*j(BUh8%ROWM= z=>Ot$o8q4PLnU^k3+4$?F@^zYDyYcPDGPZT=Vx`*JX|q$SOw@})mnyF5?La6WZ6SC zc%u_;jkjwZ^JAt{wI?RTt9#Mkpu=a;~bzJq;MZ18~029Py~-wg>Cj&D^H zok6-fkW3NI1!eYe@xmF(>7@)9p)@F}r1wu&bJZ(VmO=WC7-X%B=G}aDxxQC5VPfY2 z8n+(Il#2s4DZd^}n%P(7R2hRr&~wFl20CZ7U&k2i*5tI>2syR5;NA{gbQo@LUKuG`HTbmCE z8F{wHcAeB_0-3*>ts97kUJ;3y>EGDb)(zQXmiA-<9`HwX|9eEA>RbNnB(@&BnWClSBE<(`|JzMXsuU_eY#@BWkDq z^=rwRqU0|tII{Ups|^49+|!w|)a1C9UNl?TRgGz$XT_nM7H8XEKb^>$FupRc_}Q)4XSGZi;;ZWO zj!dB~&3AeA$uW#mUOdA8kuZjt{XMG%{82Wz2k-r@NDQA)zglZEHxozcZx679^f!7T|$YV1rl>-53H@#Dqh zqxd1f{}NMQymblorj=R+`3-57Att5!rEI8QwJ9oMi#CiD~9z8Yxe8^VM1=Mrfm z%6Ki_JDU6^B%?(LXsA9y6Q8E*^{jpy{fUmeTRnSD{gLKDg=NgK4zlDZ8po`hnXl=; z#Eg`&=GW3ul$xLI!q?0rI;jPEe!F-T$3~Q=i$No*FC~BuUZc%XsE-kTcBYq(JUi1% zhZmjcrQ^-c^b&5S6&$atluhI<%k;oE4>dc}r;ai^(@O`Ko#`dCe}+a=Y+3j1LiC#> zdF9SQLeRpI{CR-SCj?hObrD;}T3sAu7-PA%B$d~wx#zY`p%GKHUyiosqEZT2Q=qPNHUoMwC>=sMvz`%?&pj2q`L}o z8s==vD@q-nNL-^$*;D|($d&&~KP2la`C&Sb`7y_KdhkAX*d%}A4R@Y|2}#n8s)ZPe7{x{0~syB&vb#D>B$zqv495l(mqO8|J_==aAe2|(=eS>Q49x@og`Yt@d zy368t=%&}+a^m*8Pae--LzX9GjvjUnen#5gD9$6p`I!H_SE3m`i?a-?_~dmg z<~P2MDxdzs=l|+2{^&QKe$5RxA3EMtd*jWsHIjvuW}g7Vg7d%JQ4Iz?Kddb+?sODF|)UID|eS!*W`|(opPb}>8cjZHNMDS zfAgX2`(hD8m8-^I{bGrI&V-2-Kr*wWQms#p6j4RAL2CdLs-7S1OpjuZrXL~9yA<+# zY%Mma`cW&-XAQY~C)-N>G8zwzpb@%pwZ~S_zMf7%OSeY=5HGg*McZ|6{B%9|JGwYx zSY-q5@)p3%j1T|uvKd9BPMV44r(lQ+aUH5DYh%u>!!AYN#USWuy*$2vM7 z<{J~dHa$JdJ^kNIJDgU8x`n$;B0;tOK+)(oFNnQ%AzERv5+m%bceXlO_@@r@L>zH{ZAa);~U}R%CCG z_0h|QB2K@)b3Y9oQ4Yac_)T*cA4Pi9PUq967+~Byq%CVmUO&i#p$k5Mq3M}+?tin@ zzb|n{U3|)n+M9jw{zGD~&5Zi3W|Ck-V4nO&a~m`2|7dPwM*VVgOUbwUx;Q9O`y!KO zT)Z%&xcl0jQGffy*Jl(grA^1NKh1)c8Ghww)Wx1r?@&^1XMwt#El74U)OUNWE9;D_ zr`TuGie>nB?#|I0l2hqo?Q6^7!HPGs5fk#abr3t6%#{v1Dv<7EZ61{eQ9tdZnl5oY zl=3-6a;!+VF=F||!ikAPU!o6)d*h1W;cRt^Vk_aST6x!W{g77uf_Dh&6+su&gc$L? zIAxZrNwxe>itL7JiNDqQYi^ir`=?xNJ5-(E8#h36YJdo%Xl?~VmeQ%>5wZyc>5uO{q1%U(#^&69F&IQ~3sf9PHg~yxq$4#x^gu9kgRXN@zz=1Hd`j*BZ31bkW{;I~NfKbaRB& z!cDqQCmJr?qx1&JO9+7E$1>zIHkU!|exfMnOyb;teeK0T#Kj6j83>kg==-NLJ;>98 zROt9|cDL{)BCYo5aDnL&c|Nay`Ay5u=k^KR)=&KI`1y?Rn?E&v{;^{`z5QOcwH&)f zm&b4C^8bE$Tp#E9&DZ<-fj=O)?thKPfBXC+_22x-i+%H{n=ilS7(k%FvU8CNMz50T zZ(0ggh98B|c3J7$pL_cGGwjs^;OFkIxTCXDbofzb?*SF`pwnLcD7usCRJSONsaV|_ zkee2JVx)K}>h~T;d~*L01NE-H<#mv+@Le2K(`Y&X1T^vxrCBKFf;>##0wfsGp0+fc zVK_ZqZ=%vpe;)hmZht<+UAI5L)b{6b_oq0eKOd2OwCT_NfAibj{(OY|d|z^Z)UYNu zXi#cySu4@$ry1yhc$!VS*(KT2pQkUOIu=uDLZH2zJi&F z1B(6VGFIFnIK6`vhY_jURge9buVgec*An=S*88+Bdr`Ax_>%08UVAvcLjJI$!L7f| zIXeeU>;3wP*D~U6U5PemT|S9U>yXIN8MEYYjJI0v*KaEV1rWfo<^MD&E(~)++W!hN zFooJis9=H-l6!_rQ{A)A5z^uUwOT*Yq-G;Lj{*-kW=qZX#4E(jw8aljS0Jat^jM@` zFOkI8zvnP->fECKK<)b5^whh>qr;pyA?S-K*_ zoKyAQrCgY}1+K^{Zpn;!n4et-RI{uuJFAt+o5+u?KjJ~SN9^yS^Gdevs7-lJ<*@-f zO((#Rh@OgP!Qge&AmFd_L|ZGloTv$byKPG!CN1!sQy#$2wzuE|;mZQTm+-UgitI)F z(3xwY#2tZrP=u9hG~3Yf#MR35JlfiD7|ZzzV`0utFQ_)A_7Rt22(a?%{OJYqaP$?z z(yxe^I6|S=e!5eK5MNmBl-7RHbZ2#;y%86tNR|s7$#R}dUFQi6=T#eg4%c@i%LXJR zM1XTvRzq3$KY@`F=QU7BN;3QEgrr@=4O_YBy9E8G-*vPT<-UxlG4++y}s zIBOK^R=CADmc(wcpL!NqS(v)oq3f`W5z>ln>d=BNR%{0psj-fb)I&VCmvZ1LtrEzG zd9tQ>+bs;ol9ixb!-#+{<`M6;TV^DtMM#v^Fz-$;qKrmuNWpo44GpJ#{q#8_7(<)O zI%*1mN z@St54VFSBzwI~z=v9V*N^?k=g>-&y<*7qIbYym|mGy{sIH#VL-YsdnE=NtYRpb}OV z^0|W07#dk)BMmZ0E7Qk9yY)ogry z{T7s2I;Tz^*MT)r&JB-VMad&a)^?Ts=6`u}k$yEL$BLU(hxcZ$yYH|$opYjeo)zML z%uO}&M24Q2uJ@vUiW?z3b7Z;H(DgVI{Mb z3dX_XUbPZt1bL^k=SukCN>14>`|@<6t2Lh+k1e zyXJjLGs|UfHU|Ef3yNrG(Njdb#!yQ~zZ=o6UO+2M#ReXN*JYg=W5`izhzP%I7(V5}&N1jmaCVX*LQI0PpFVauS1-|79C{?lk_Z7lp< z&(hXB^tqi5jI&fF^$#`%g48^l<9dPx5VT5A)4u!;dBRJe;&1vQttR4i@}t3WcrP3i zQ|Q&?E}2N(b;|%+O+@>Y;D?5qtpHk02sTh@dbqHfSQt=;758(dzUp~f!C1Y9<^sCt zs3Fo;&n5J31(=rw0p^$$+xT*s5w&1y2IpEl6mDear%>(qau{h9!p$Zq;cidJGb*VO zM~1WsAkAn^c?4G^R$A{(?j94joi{-ioRsB=ii$@YN9teIZUQyCC87d~(J-b(fQZwG zs6fI}I;*1M0TO6Rpe)MlJ8)V-tS3`g3{_|*S59YJ-*SIMZnt%+iY?V6d-fDr^5i`! zvskSKpc{DH#$U`MF<=+O6<-w)Vr!6;bBV+jMW46A9e3cD8Ny&P&Oi+Cnuk?ESjVUD zqroTHP{1D6rOrMACII5VN>zq}M(nrd1ad0=RcbH6TiugQspNsKLV}7VQ08sSf(^mz z7iv;TK-Tt}_a=={^K~*?s{c&$9SX?@J=g?-MG`5@m$YpI*abMl?LZoCA=@-E7|EF* zb|ga;lY4<~v)CW!n`qZ^d(_DYEb!`5*Q9~}W2r&KLGU4akI$17bicH1y5%cWEFEt} zD`0>?m%kutQnBm@BTjc~3s*PA_hxmNJAujZ>l#&9?Uvwn11g0N5d+Cc{)b`Ok7zutUPX7}0Q9h?n3L7b`Hd5fx!DbHJ<}anCzA z9;sg!j>*v~=BXn-kk>bJfTNao(NA}bbVQ1K3l0)(WLX7SYcUee$UB~4JLf7pcp%eQ za@JjR8r){yG3FqSrMMAy)WHAZX$L-V{nzZiHCS3u&Y?HCI;!62JRsVM16IoV38{Qc zz(?NjP*(rr$$wxq{GCCphVw*!7@w**?vM5cHxsxxscS*mt7GsQ9Q zmbdlT>EcBTehj4{Pq=&}W!DSVa59INJH^M2<5N>u39 zY9-mB>6RJ8jao_jUXbS{;WK9uDLiMX?x2MtRqV4+A`Pw95NPC_!t=FauK*iA2sd9M zY^r4xkZ~okn&EsY`w?i)Aq@DtR!!H_um56R#c3+oNilYf zxB@TQ-r@m-N|0g4k2NjGSP;c>%21&{iU2)RZn_N^3t$unOV5t$nc(J$fo2T^I``AN zBe^2qr^taTS-^zNCcu)V1d$9WjintXpO$VmRweE;W)|d=zp>@t%w)6v>}zRoY4VPB z6;Ycz!Dpa~{zL3N%~r05s>_q$U}n)qT1mK+(BewFd8Fz=K&fwgP>W8X+JmJfE~tyVK&B%Z?J17 zJ+mQu{T@WhhAet>S~OLl*#RZUYa|~z1x`cQNRFa*8O;mX$2&yN6F3rLWUk|Fm*cRM zNUoP?1-jbl`D()OY0|4YwU_6UfGw_KPKsamI8$2UrB)XOxI*Pl?1xjJ8_nCf<811V z>~G_0xA5x-z=cMv&1ugf*Q*NVT_u>86$TtYM>S!rj^qsMODg{BL%r_Y8V~9df?aonR5PW33iZU`5qqlus*-0Qu)<yrqa-co|W>H zw>p;94Gm!X9&`vNaZhWkUTowQh+OS)*4B7>No4dFGo)MIByi*FIxO&Jj6~QVN^yhH z)|r7uh&FCeXR>mRx*tLdbG~X$+I{dk8M5Uq9%*JE2ANZiIy|5`Wjb<%rRjkA)MO5S z6J)YE%rZ>}rnr0x*akVEa8@2cDNFl=aSl>+6|m{d}#H*!mesf$FVM#TBITGr_y9Z}}HFc9$QYG8HR zJ$gZiK%~WsUB1wSrcds)cv`Bt10kq=su{WBAI-y0^3D7rJ975rJ0&1p%wj8r$&C*5 zbI=k-eUec2lW-F^%_;B7_@}JdP)juwdom9nU6#u%!GZ3isB(l0>cf>ep+#bRh%<+| z$|uhQCc+uq*^3Jfm_zLIV(4a{bzjB)PU)*nBEWDR1F0Fas2oEhU~u(?EhVt*H^>RowT7Mq zT;N}ckTvx9I|%+dYevNWvrR%M!2v2snv>Zdofgl`;h7q{ztJp#xkyfeXO^CaDJ zDZ(707a>ia<7j9soFwlKMVfd%Y1?PHCkyea)hukON5q%zrQpG4uATW8q`l_I+ESU> zmBe(FOeVI9#L5-v&^9I?RLGLkbx6x2&Wxk9oVo?M53j$@E|7dX@)`)HV4&ZR__hNx z!VVZ23nF7AcL-(>ElViG@`PN<-42d{0wp!+0Gt+JV_qJ}_J%PkmBon0##$|Qmmk7% z|6^13ah5UzBtUlnsPuGCRnHr4HrTa=V06^G)@9$6#~IUxcHHw?sRj7CJn}byz6b#n zgT#W0p` z+)J39}sw$gAEqfRV*->LZrG~rB(*jC52%^)R%OprnCi^@?>TpCE&k3Q_sQPC= zuCZ@8K&VK+{#p8@v?uB0ZOJ3;l}xLv}=wNN~F>4mFcH!PuifSX@RKl@v{r7AG4Ao?N0LKG^@B zZ28|xb@Qz3s&1x0X1;7Ac%E)0pkK^s#V$D!CTaP3h-j{uWv?2yk|_(4V}k&zgKo4w zAwaO=W8jCkDG;97$-+4WD6iL$h*f|g7F#6eAr>+%l59W6zBW_#7Sya^#rcU^jL;uY z7xYSJuHn_A1*uYS3T3&{15c>2sh)=b{BoV0!-V4>Cg5Sr2O1~KBL%pa=sP?<-u3Ua zP4#W)AF=`RW0%@ggT_ODLUi=?Gq=WfRi`N(6~#6I!=X30ecRTu86aGlpAavVF_os7 z#xf?UodWqNf*BpO>T)Uf39U7y%CnVn(68zOgfrJ=n~X~JJEefhEi#NI3>zR3up%HF za;iE1P8#l=U_b_g8zS**#^TY)e;6uU2UA)DqkJEX9dT$J$Sj`O^qyq zgdGVa04W3Aar;VGw?Oz#HMUtl(bWB+K|X3cWj)zH@CS-iSy6;TGMgyQDCK>duU^!h za^3w$yccbkdBM5$N@UH6aNR!(HsXBh7l_xJQBLTohARw|A`Aoh(=2CBK)Nspre$#up}&C ziwEfV5EyvsJ`(uqWqlI=2Q;0c5Vdk)7SUELpP!@T%+2{Z3nk~_bYAZz*rB{jY35rA z`^qz^2u0;hW~PXFIAVr^SVUEVANZ<#zF0^VNsE+lyGlbnV9%-qRHnr{1<|O`*$~)P z@NCY{kwB1AWD7Xy!gg$g#lL%Yj z{4KPTfhE8@Gu`MN-<{jWg|SqRH0td^I4&-u%OZ6wFaL?`8m-ZAya#C{ z{xOVBkcAi}?@LMkr>ghRY~{OI{@DqD?<4+4zN~pBs#}PyU&xoW-tGh_3AfiyfbF$< zR(dBu*)#@*C)du%3sslZs&)djVf)?*FkvS^+sG+90TdI6UERt4w%?reP{=NuAY4d2{wG?D7I}-i7aKBmH}eNxQgRQ@1Xs8Gf#^ zbUgmn{2)fDWyh~pD=(D0V=Sp0ixMD0z7oWhS&C0&eBoz8aGs2eu^C}hH+vS4y`vjT z%uM(afnP`%ZLwz0LKKtuKGyQnG^A#T@uz+*ApD@?gNKAqTzxENyWn9EQp#y{N3&O> ze)rqi$*{M6+x!7n^|#g^Zyr<@<5&2>mV=2?xvmr&i3^9Z3`^oNCvI3w#Cn)(l9|oS zY~_FfVzD;)a2c_TbP^AkpM?>4s>!aC?9l` zSngRDEHj?)V8vobbFqyRToQxWEJ9fJEH4%ZX_Ok%B18!mAzmgnlchxn`jb4Z4V7$8 ziS*>L2yH33Wf8(sEUH~tgd(s}i%=0bYZf6TC1cL>15EZ=Ve^FEJ9faBhO|L z5;|GShn4|wP+0*jd|rTrC7SAmZC*YcEi3d|wr@~aXw>*mYNpp7uS>ZIW)Eu%Vr5nN zZX@m#xu}(C#+yy9`Pu1Bb|!rmDYBL7ym7xFw~h*|@af8t$xmoGqn}CSph1>hMoUY$ z@l^_6Lad~kGQvgErN&YOMBs@DL5dkwg}uHdpl2iX%mZJ8hBXXSgEY_(aY18^tTGSF zcjFL~1g`YWa-tq_H}4`bqQe_aP#5Wv5Nb+Avu2&>-S>{19c(**|!LEh@o zSFz?L>uy)`s*m?rtrk_Uc=pw;dp+{?iS)HUk+5XE0liaFMPHK%J{npX zzz-<%Rbs(#TF`}g#JaY18tKtj_bmF8bh_H7!n1|k0nFq=w*x~54wGQsp{OZ%et#Q0 zk2mc!!Sh)dLbQ_3|FL|QxHvJwYz?8WX6G0}zu#igLI}P5TiB;8)T9{tgPj<9`BMmM zmx%MT3b~b}EALAuhCX&pof<$dwvMGr`x~O!oYh=os(>CfE0-O#JNdBFbJ}xcJ?Pn#(P-nl5Z+v>ea@cMfAzvWdNF(`ki*j z=FcLgexr@4m(i_>VW2+HI7*@BAZl{2!uDVWLbHX{!ZG!c-c=kTNNF+by&0}RM})n* zL9Bo{PUa0tcY0(Vz23d9K&E%gP+1HP8*Z8yCp-Gi1Sqs2DVWSK5iVc26QyFI@mBrX zpx6<~K$MA!fF~qui4TellYXUSAVnxzNU~HUTlb)o{$r?Y4+z3F=|7~jkp4saNJ!*{ z|9i|8c{2oGMn(~PXe1!ZL42NmCe!o7JG#OYT;9x`0ECE598&yg(>kyRP6cJRtNzUAX`y zxaos)Rj3c0JRlcD9uVN|(Ql*Ufe`L&7m^G8F{)ZJ?CZEzwXsZzs5;Rj-?Y#}j#VUK}t(f^c+J>1UH) ziWB5Fi|Z$k;OSlgCg<=HoOQAlH90~~n;}HN9m7)zu3jCZe>|UZG#OO0Ndl0Zn9&w} zO8?TP`Y=5-Rkt*eQhk8G3erdvl2UzuX&7<^rZ@T7`RoY+k8>Q$In|G@-w47rFo~{^=*Hd8evQoQW0$_?vxu0gtBB* z!;=`5fAhv8j&;6>&=SI=@3{t_|B$?eKMw z%F7r|sue66a&YY|a4e8IXk-G#u&DM}*VH8m4|g?PN<+^%3795L+?Ek&0GQ!Ve75|@%C@Vf-8}B#a5Kp- zj0wbM877g1564J-2^lu-#L&W^F-7}^03jt@NU)DPSxC?PAKx``SmyBblxi>-bpc1f z2l0#mu*-4755UH~^MLSkCnW00Am2j4(gP^^fc;#duM1T~m0!-C zbQXu6XFeFpVd+_YGn=~u$%L!ljaO$UMbVwI`rdqwx=lTJpm|W=_w*y6M;2x9gn_5N z@3SXce0gbw#Oj=djK5c8Pv(s1$7sG=X|9c(-C7y$1j4mEW-azC1s~VV`mk)m?{55V zPvk-UvSal@CbisU!CeRSb;s(L=~0ktEf)%VO^(&qJr5oi*wHS=cin&*Gk!7CijblM zbXaXZkk7`4ZhVm+;@KkT{{g*coralQ2ke%5XMLtF8jWw+6RE%y-|kneRnVMjdo@Ae z2g+Ad4yB5fXHJ6ta*sA{N&^bYWQ6?qlIC#`s2|gaWbTG!%{v$$u<6Z`IwL8_pHZ$K z1!0P~0AY_pK>B^eXCpE4xe)z+{%SxTLFl_@80EC#9d@4fs}}XTD8>)y8yn@lds7oe zkF|ky(B80hb=cmxQnz~G&yl?F!&?A28B#S?v~OWMtVvKMVcji zc09x~yqD|Clb+W2A_?!M5G89xJG?R z?4;t_V#yVm4u=HRJrYG6k|J9dQAsj%aVn?YP2vY|m^@G~f(7 zD>f>38#7wQ=z$_UoRLNJ$Vx!)kdMD4m864&Nq)yD6MVD`mG|ohKJqXlpu}uq9Ktv* zC~;;;(0sgp(k0|YO}5IL8~c){lSA-0ro!P&R2`c$>Jgkb9*vk&zGD0QS6LX*ZMnQ! z5+qIyBf1S-p7}p%Q(vwyqFZxG-)9RWBGskBh#t&|lssb?(XGV#Heo~yCFc@`NkSsU z>BES~IXQjyVML0!eWoy?Gb0cxjS8M2j0hT5l#>IAWO(wFJ_`7RoIzKNy;ldmYwB`cFoqScUYM4JC{; z;2^6AE2UScP*(FPY;o#ik+Nw9jve-Z$gfLzqlM;D>;w0&`51pPg)JqwkiLBE zCVe!&&@TwVYa{4#PmfI9hoCAYopkWR0GLt~hxx0{;#Nqqw7y6srCuT8%rOG`&x{lz z;-UiWN=}?daEQk9Hh6)iQt$!{yBoZ~5aEa)MPP$M5@eRL9$n17S2fxckZSJxmPG<4PrPbpD|5oG#^Xay%kfG{*@q7;~uqZB0XQ9(p0 zw3Om^XG08xuh8^nHy}_-Bx|A+61A!^axFQcGEc!Qsd1=AlP`VKL@DIEfQuKhHh>w$ ze=NUgP>xZ8iW}HHSf!o0F>_=OrNgCW(1ek)-t@{^?4B&LGpf+@wADn%RdYBGRR0 zHbq(Z)mM|b&88Ze?Fm7al zWm_gV#*Z+>qj)fu#{^>!crjR@`M!UjbE@k0y{!ghut|4Ied?UE&))z3_ul{h-+QfE z{qFd#TKlExj%xLnrsIEB?WlHqFNZ0NztW>RDXbh0XtfRM#x$-$r#(%M7C@#`yojo6tVNWMK_rkP@l985OdrR8}jx+?)BR^4Y zuU=BrzYF-d>?S`nSvGVWBHZM8Ovbz&AU}e+UN8D~p329+K^nl2A4~v7D07qKBl*9t za*)~PW6X#GSGUAbVw`oB@qr>oxJbi}PjqM_6&vHZTp>QI%a*L2l0%vQHO!)!Iq zFry||>eUFs<>P92<8)>G`2qS^KEJhEy<@tX4k@Gd^~$qM@0g46y~B5)b0ix~h{_P? zAVPt#G%qq|F<2V^pe(ioiUfy{uwx{(iHTqJRoS`tAAX4Fi$=lAC^&^5%iLXppzmUc zM6vVm2jK~rg$05Ea!tANm~rcIgj^T5-Xec?jrls|WWGGlIh8Lj3w~q(1bAtR0S^c5 zJ$)gx3w<~*7f;ewH+2yYZ%>S6#m`1v(Kuh(A1#!CY@$Mx%X46q^~;a04|-V%1gcV1 zw53_iDr5)IlZvHf6krd3G_=qHNGL5D7D!-XmT3}3nUfN`65CcnoCa&wnRl9&;O0WF zyD!U`W%<5rdAgj8&GziJyY9T2)igYE=tQ-AWO(J?voHrw@sJq4Cn};3s z%a!@&q-2+m1`p(bi?|Rk_Dn8Bj_l(?%+jT^adb>JA2!egF64w{kOh+I=YMb4~>R1LG@VFzx3-wFAm3dr?g)V zeU&DHI2nPx2@z|Pyjq44T^5-mk!kV@>i_%VLT!s#gveB&1QQv^KoBeI$bl1=XVgcq z6*tw1zZa=6Ly`q;NmvTA<<~h`!B6j|@%z@~!sZ7AMFR%Yzv!o|ezV{{=jxSE0coVm zn^|^=6Pc8GZ7A;C}B zHn%PT-k-&J@P2|qF*bWeL5T?$_5Tbk%&H-4&TP1* zuWYj6F4fm7>O?l&<(rt==z*|G+-%vD26vg7mQosA&k-^W?n0?k^WTPtX_#_2gR#z; z3H0tp_hTwLj!cxHuft+hA0PX>tc_kg&Z#b()B|=g#t?0%WXlI<)a`^0Vq+67rtnHH zKw(qX*I)T`aTILwOM&6C%*^b-!f>dMjN>{HwtHIOHjPK-cyNFzRBBTVp#Y)wPyGhe zT+%+XT~*IXlZzUg$@v`ASj?HjQ%-_VI(AP`jZE^=ttLpBJ_K~4;X63ZBq7N7 z9w-~>84(g@7=mmyG*|Krq7dk-0d}-{x7iw`g&yHc5Dc%nt`7vg?Q(>8ZtAjx9eFi$ z&hY&ZzWEL`-~;GN!?4=Wc)chTT!b>lS4gK0a--bBRfKFcmE-Ia1RSU;ciQ|Ux|H8L z&3z*K+GY$6Ccty5$8RKVin(HzRR}H&lo&l-4l5lXvqb8tvjNZM+v$hT+L?n#Q4HKk zbXV?^>N9qUM&eFhYngWcAjSO-o3QELVgPdseup`9D5;uteB)rf~}IQllh zRH;-)ltXiS+D`ON=>&W^Dg$UUi9cwCP6V58Cz4|LxodHNcvXZ3wNx~i??p~8H~~Jy zDiN2kLm%Ac<7hda=05G&GiWFcj!=n#u^N9Yy1G9rwt0_K7Vpb#-s4pk?|a+4hnJ7{ zF=kt|kCrpu5Bxq)@ToNp_>#f`@xd-CakD0x5sT>;y=JzBM4(PbUydka;g#1lS+-@7 zdZ?qqihGsa4->0&>~OlfoC|k{xUdB~7TUw>Y|Kw>^GqlAb);VRGB^c4bR#R%dO>sr z2ok@M!bn#hbhADFNeh{KSP{+`4JL&>EJHiSJ+^aLmAFUIrvbga7YGQ1jE}koeB)ZF z$6XuBNb{jlh$C*&EzaS{%h!v<%DllUuVd2g)`_Ffc0ToxhqQ-|^_K5LaHf%m2i{Ek z{1oFY+RN!$!%QKUy7lumHCfirvZJg^YXKRuaDax^IDVlc>5^Ti905^|j_Rh-_o}?9 zViGHbUf6b8l1f=D4ms>5JGEP+xQ+fkx(jg1~xv=@yeN3pra5SZrsp- z8A`zX_85U^rwTUN_&ASC7+gtV@1qiq?+UA3R)3&mzOsCi*-mQVh=(&Czoe8Ay_{aG zb}uAdb1!yxFZQ-CfPH4uL+KU@BJh+qj%?J}S@n3vXoY33`ROL>)*s}QNg^O%C0m>% zr=1|8Dp-fj);(qc^?<9^7@gBvubB$lnfKC1Bt8(y)mHYsA2DlMoQU2E+8~g1!g*-O zk%+$ZxU7Hf(~mrjoi(NeoE745SbW9xWXUW$v3De?I0Uf>@z=nELa|WzB{WQ95y*|R zdjJL{nDuS_7x@x#F$|O|^_SyJm6~h}DLdQ?_pC3$plLExI;n*QN9B-AIL_;odWC@y*#Y;`1A!JqNgq3>ri2I z#X3w90fHl00t21i=l~d}zvYy!Zp&*oWMj+{QZxyQp!)1n@8j zF&WsfsG{@8vux*~D0a!d?N)B3(PrN??<`#aXWn^QR65wH3U+D}<3Y`_hrnI2@rR)O zQ-fl(VB4cANFD4P$8Wm!*{@-l&=@pl23?Zu2!wqUhl3Qv5Yw~yIZm(Rv7AN;6Tn$k z)o`*Ra@EwAJkHh@)|lKwjua;Ii?H#r&oc7Q)-Vy^}wW|@B|BpL@IW?4+O@4<~n z=XCdAXc;s$_Z(~QXCqGmp!LM@@x)n^&F-`qf<+)hON|lIR3zCH2XM_4!K^U1M~R0a z1>y+Sh6YTn0+YpQ&ZS_EU?CbXar~de|GANf!)e zF`EisWr}J#2312lFoa8#+^KQlwud*brcRD@OA#McBt6$`-0T_-q`Io{dcW7}<+#75 zCmQ29GzUQv zfHScycM;_-Iy(M%5olAx2bI=FWc;?ZL=A$3tyJ~_w@dr{Qe`=-DGGj>)*{HQ{{_w zgy)vzr1#%x=r5slQHUQW)3a;&i5o}#_SezF($JSvk51HILq81oFpsc`trbPwzT%35 zMa2V@Gn)=9?vw z(RYMjM8^E%X|d;yYUx%zN~-?L`Hp)GI((Gf3KWBDh{VBVl+SfF!ObJZ6D{dW0^_D( z0OfXHUy`NpC4nWSmE=nj=Uh;4@+ARN8B-o#lEJ($iF!|cNtkh+yGKmsI(Pbye|jb4 z$i|sPr97Q%!skyk=NT)slzL)`VHSg$C9vk*<>`(z=PDDf;I3?SUaTWdgmpP7w{U57 zUi{3a&9KX4QSrRa%lr%7$i`4F>by!>lEiccOCpQY-X~8xaI&ED0?s%og=r7m#)YVh z;{UkqbY3XaI-Qr6l~Z+I5{=C|FS)7BIxm(+r}MInqSbi`iOWVh$*Ma8ofoSxbY9H8 z1bEVU30*_Q;D?1FbY3iDbzZW~b2=}1XfXtzuAtcIycoePbYA!`2_-^g1Ev1Doz6?A ztuEwAH_2d|;{(QJ0=Na&85naquVP;3C8yQEBp9dayuN-gAVWleH99X0djc8LlV zIxhxG)hM%iUguSY&MU|aI-#@!T%|q_wb8sOw>_1RVqvBgo(*8nzBDQ9NMW4{df#;gn-lHA}#~{ z4e@`}-!L}g4KAz=wDcb90OwP(BaQz{&^Vmdg5pI?IGrP#gaEL$qbo(C(ETArc1?1G z7*wRlrhpWZgo!X9JUC$dTSO*TI1139oHa9-DyVgj{$c7c1st=J!06<3zeynyQ$sZg zq@P4YhWeByCy6{JQd;1K4#fFF7U#c?X^XCeH$-|Q$@T|M)Wmh%V_v3+G3@# zNIuf%JW@X3L~_QJ&8)cwIX4y)T!{w;8%Mr9h<{M9tp1|zCtM$o4V@n!Z+{T-X!$^KQPpd(E>b6Okf3>uS`7<5tQAuD$REHyl&f9}3lMu#XL3^f14lk{raSIwZiGqIe z4S0axJZ>RYxsl*(A(lGep$EjW37%`8nqAt%^iB0hN%@HT_G9^BtC{(hZc$d!|b2%^*5!z(c3t(b0wbZjkkI z;lAV4^9m3C*cqVrpi52I=vcK95ca7$-r^V^9=i(-G3bje9op2=fyI_0sf)<4+Ifia z0$;4?(ReTibBdZi1?a0G*=BVBlL=14jhQqH9W;W42XnPy9A9jYqY8M?;&A1WehRF8 zG+F#&4dpOrc{snf6rHxi8J+ahz za2;!$%e6?gT&|h$Dy~kg;!59V50Pe(-biyOyQc8oM<80@Xf6g?SKwYoFa4~e)@7c(2W+IyKb}tt-!d^ zY9Yam77D|UaHAdSIlFWdq6NIlk~fr1ZnR5L>LprUV4uT{cA->qqaD`g+>9{rxr`8z zr0`b`f~5fSEPP#8^Qh=}<3@|da?Xubotkd6c7jO(+^6*T`D>vyn(Hz9S@brq+K0Ej z;OWvmn}aCZHg^bT5d@sD&Nj4ZOtQH#(G?!SjB&+@gcGT`gwi5T4Uc1nOWrBFRsH8- zZjf0E$BM9OFv1=$qQMJ@oI3=q*qV0F2OXBUSN0p|22kO)KG$BCg^c&FM=ujOWB!&6$ z=~@VqXbU8%3(P9^G++gt^xdvzL|m86yVN%CLWRu4Wk^{`F{Ec#z}XeR%=oW)1$?1j z3>z=J3Qoq&tqE^LEUNb-aIw)ni~aKy29+EqwNtHhQz+azdp&~*~w;o^LC zx&owhB)U9QIYdOsvB{c%^FMuz9xc`1b_@MOX`JOMjn|T;ajpIer!v<0EgBUG33}oQ zG6YgFGz%S#+t*}N92T%Rz7YeMr#RyB3eU01!|*^Hu?gaGHQu?T;{`0H9Epbm;~N#| zzWp4W8QE>&4=K00b1hpdgwK3~$D&5}%cc#buq*kTi;jz1x54IKF%LMFbGm+?6XA(} zd1L(m*WxDBYSDb!d!s7|C>V>dL#maYi%pEWn}5aP-3u1!k>F-}WT=8RWC@ zLt(MNIBWjZ$>*;oecP5$Iy=y^oH3NbsIvDAow!XWLZ6<*iJO!nI&r7065Ba(M`jYa zxgux_vNlTu&MoK{gFlGe38oE6^46I+ahvty$(*>iM-5Kgse-S;iCcEQfL($Ppa%;Z z*+Tcf8b|H);gdURFO-uJ)7D{f=9N$N^A#h3IDRCl`jfd|t~snjsuDvNHyHnsGgdW2 zthjWao={*FxdxZlkCup`mKwi}6Sm_YS~KdGGF_DndF(c`D#PhjzCRvqQ7a0BQ)`8i z#JM`ii_cRRlt3o*)A~8%DK)b^YlfxjF+3wbKqbej4ZBV%u|=z@*f;g%0KHMuWPFZY zibiy9h%T+nwPh>Mk5<&bDKV&BL*FzMaKWCU^@zr@X`^<9tc%UH zu4y=Y1}UkPMv&#zD!tpHgGsI14OXa)q^D??41a2ML}|{>DPme@5&eHu9l41$@Dca5 z@TvNcdBs!u)CAEdnHFwno4-Jzw8a+(Adz5Rtxu}r3aY>wxKK6{?aei>Q3)00r~4eT z!$jO&!>cfAOjbtFmrBsg6&?f@xjKgx6kl(SzAe5{jZ`XTZ}DM=gg&asl8+y?9yDL7 zDOKd8W1t3K_og3y2uXkZ<;YB-D;_q04eb=2z1=`+Z0fqE zoe`*K^W{9P|-+U$VcQ+K=>kgPmEJ%ai{k@FJUA#WtDZoFRfW6Jd;O8`D=q;my1{Q zdLifZ>hoe|u>7@(jej^9uP7igdGLuoBZ1ez=gZbfym1^zCRBhKKfcrchV}{dC6nbq z;~AIm%c}$u;0q5Ifq{FM%IT{Ij(}|ZS~Am*KX}f9RF~ z|5=1{4v&uCg3hs&E{GHKhBg56w7R6bDKPU)m=A5dkPl2$~nI+9i_luFw6r8#Z8elhhE zSp=+wa=>K~XdZ?v0&nA5jsp}M6rmOb1u>bnEDBFjuPBkTkS@y%mNxO*LZ!GU@o8Pu zOk&zUuJ+#R_(~Zavuvsww_Too*Nye@+inV`a&Sw8SucsQdKD)r`ro`xT}O;gacgS{ z0Ehuhni*p>v!NQ#ZxST7cTE*Vz1|grVYHZ~drh6`qq08dmii^}nf_b!MAkjJN9|V2 z!I@Dl3#2|D6Jc5)5WlMbIA&XpunmkGx3){o&vwBBE+%xj!$Y{9Z*5{b$Def_O zXQRlZqbJw}6c`zoMw!aF{e-%m;uR2k?N##b%+w)B;S!Mrv>bDH5L1lRC!3kDs)?Hx znR&(dw?h@<6fJIs2;pCu^JT=}1mFWK0O`0OR2^+XKBF0J%CMQ&7PBwYceIGG7$<^f zB~UDuavjfq^KwgXf z^jc%RGd~?jKhROz#3Y#F59!M~o~V zXw>jA(F?I>m|h5nH=VmL3}R6*=BzyiE-k(o+&G_?&5*7{m<<|>DjnA7g`PCo1yQa` zWsOpXc6GXq*n44VQm-v^x(M*Dg1N{eCS2rNcIp9m#gwKSga^4BXmg3(kdaC(kW-W~ zY6nloNm?De_fxn4e)n5*0sQXS1@JAt0RF6xbK1ZiKnLeOJ3wFgNP?4%JD+7~$f3k2 z^_6$~^=Fb#4QtLXU|0L~r!qOsX(Y(+v%m1@lj#xuXqJMo_I1#)REqy`vG(gn(xX{$ zP~Q`aEfwKG2)+A?%cqU&5v@m~5z@S31WIOG5t0&WrwCICG7zCFvHBxYC9CkdO1_-B z9`+UUp$7mQeZWt`t^?)N6z$Wf(P-rK80Sr+bphPcMyDMnMoJQ#js#fpkbs1}v?nwX zPW0*!#1e{rH7XY_SIBOLOU(E;QQYz5Dg@P!k-iYZPql}r1~Q3OUcyHBH~LcOYOfxp zl;dDAQpnR-q&SZl;<$m#wfW$2fDHEP>eTdgX-~N(HO&x=e=9Y8Ep?#6YDFjj6biE`S?f{BX+SDPY3@H69bie9~JMS zkIL+AE|xL+FJ47-PjCF&fTT^9Eu?od1$8CGlwqv5>S7%0WELXwn_bJhYzDVq2{)GZ ze)2%3HUNw`F<=?g4?h6&YzoPLlQjDkTTc~E4Pe(13B|cj3E;H9^q8CQe+<0QJ?@8- z+{@~e{zfs7K$cw?i>ho6beI&>*k)%)+z_2dz29;lje4K1Z=d3$`K~?cHq`s3U+6+d zB1a;a0rZ3mmj&{KAi`-R=y57Y+>P!tiH$A}1078pD-i;BBqpX^144@b_8*4IU(FLil^!CUZxtO;1_ zmUW;5^mKdlbrrPs5x6K=pvY?Ax)N_OqXdo>Fn;U0sAEC|!^O|cfwpJ{Dv=WL=*bdx z>4QHwIW7cWe#UH|95246*^s3n*}glQW`iu@qyQtDN@*0)mf>W5f=~sFn15+F2|SJ% zB6^_6QBDaHaG9m@R5X`>*n%sZ4N;nQB{!ik8hww4U6*vswC%Z|*){I3?P}_&0Y7dliv#&nh<@;qjlJr=F-95OhHeuWpPC z`p^UzCf4#>^%nmjeeTYr6B8=kjcI@Ux7ZGdm4zjM~z3D@HTN`#lOl2|c*^-gM0&6%iC>OWrCq^y!ey{72^KNTwY zhq(ndqlWRHodl-(xC@@vmQC=l6SdoR z#&0+egr~kU0q8ru$j2XDrfU>hL`pt>-~xQeTr5#UAHVty?HKTUi!rdJ&O5Oig|D3P zC}dQy%_!X2wmb+;;lF1Ca>Fq@b8)!-_nNioz!zTop^Tff@09!+-+KXKhrcLKMyG{C zJ=Hum?S*5xJw3i+?2jX{_h|jl4}B;b|M(zu?{kNNg4W1CpWDE1?t<1Ppw&#I<1dq) z;C+KgTg2+wN@X-#K8wG??~m-I_{W8J-12A@$)6_P`>_YrkA6v)P4rWF{Q(~=rw3z) z2lZ_}{=;*5&z>j0Goedt1FS~SH@rw%2s8;|M|McoF{g;y&v-9Wrd?j_>mwAK* z-UVbBb`4?^Z$22}8<={68jIH2o-h~3Z?PwQ?(g4N<2S0iud%9ogrCo?>Z4ZW(sy0M zSaF338GoqcF_Z}759_SHzywe+04NdCOBW2^RC#=Y?<~$2d=rkPZd&z+9X^_%5E};BNZaAyd47oU_MdE5F~**EBq+i(=PlHVm6~!851Yd>6&xG{1v; z>`j+ml#YdgHdjj|T9Vy+q0aE1enfVbf3Ms_n){6#$dnj{2KgS_g$%C(TBx-^ThImZ z4>RQQ8OpkPB2loc=~qq?-x7MKkc1M4?PB~}yT)%?1*Bqsc4)d>o6J#qc=?1WkSO`3 zlmkoDZ!y`07+ikAO;TIVK!$qE^aXY|P6m-vwgnhl_hmk9GEcUzBj{)DwQ82Ic;#v^ zjjyt<@G8y0aX_2u;w3)hBB$d@0j4K6%nFRD4!V*TB_nL`6-Iz52?)3Vtl@$Mn-&mG z@1{V~WOHnEM9t}&v zZ}*F(6bQE6#t`dJ=ToO&I+3$rhi*>JjL{^kCtMPbj^TZmP4J;-0-?=wSjb=&<4=w? z^?}PpBrqgtf-hO@ zA<`(R8kT&}=x46GVgqzf93HLjzU!px0=JM?Ms}W)Zt*ZSG!AB4sD!(l4QAWTFGi ziij@d4MfTrD@n#{ZWi{70y<86LtS`;u&psoLS4_wf}y4)xQ{uj$%Wj&&UnXH@DTWJ z;$UmBkNB%@1|YdbUQCr33Q*_q1jwoC58{{vr}!z~(`q=zHy?!EZ=-k{fq_~N00-Ow zIzNxGgX{o-f#&?iYB&R8Qzc-0ng7F{lqTgV5TgT{hd4rWhbsPi^($_Sfd=wRi+7eH z<5J*ps}3NB^$wB0O})bg6U%|&IV224f<$Jcct_j0t?FrTiU9r|+m0pygm28j7eF)8 zGC&SFD`ou8V#@?aMSW}ivcT|EzhI4iFS}8PeH2e@^M>s(8tei|8lZ;pD_Q2|{VP{~ zr(di!zRYO`QDH8$yQb_Wug<3=z+|F3r+A3+Bp$MUlFgPM%#&wR&`rxL-vh~Flf?I?EPw$@++U`+5!w%$Sd zAk?b*52so~@5T}F6tdn%`=-+{w@lY7o-l6O0&){g2?1v1Y6^Zu)J-dV6Q|Xw#lWDuJAnG-GH~}Bw-xzC>4bf!W zj7F8IU0#Z=(1noT)G!6~eo!@XoO(zWC9dCnrn$ty6ThhxmzN1O)R#j&d{Z)pb##^W z4M%apy$^DBEstLoED(>WZ1K&l-J4?ZO^_x&C>I|?U1?Q~1YOD4;=-N!y|4S_tp1Mr zF)riZLnN%f=!3sgzwPzygEzgF2OISVxI~FGt)aI!6J?eVWCxm77h#OFeS%K+XErp* z@&i;)rN7s<^6EkO=zlwOw^7&tB{ToTxk@UbvNU_CG8LB{L2Qr(ppg!N5mYgY982au z1eWa+j)+5o<`48~4iXwgVhe!o)o)7{T#BL$mS5%6D$-8Pe>9!H`)+xW`3HCL9iO=4 zXw|NX^NHhzj+s%AL9IXk0C@X}(bM^gBZo$JgwmN^7hRV!qxn`{UqF7KlbA@(;ZzHG zmZ8`5>O{%Q!>QvzA{tp_A_ijhf+c^d%$4%czlf;=6WvL8wA+t%Nw3jaFt>N`ZN8hG z16H9~1`tnNw<$HV>A*xZv(+%NQrRpKo%W?q4`S#khLIN+M{vu(%||P#mW-Z4=Yti3 z%>k8gL@r{jy$Thygb&h<@+lRYbdcwHb!)`N*WCgSFjQ@AxXh!?JKa&Pm#!D4F_#Fg(eLw!=bLg*J4Z79S3Q)Czr2%$_xSzPWlE*bG{?|1s>?Pb9C|m}C}h4^Rw3a<#(fba7Yew8 zI=BYQuy#DhK$&h>ud&+s&A51kau?n@n9nXR8$p3#Vr^nV!EBIs0QOfdBp5rMc&C~* z1}Z8{dzqb=uEN~X8u{%^Ezfr1vjha(O>BkaFi@xp3s-cf^Yv?F4|>=-HzJ;={t4Sg zqQC^-;=48-viO~B)e?O|IAbr~pbx?FhKpYtoZbYb18bXHsBxGAO9On=LMRh2p@U`D zj8K2NN!Vg^J^ ztP-Yinw|~CaDFJ%cEwQW;gEtnaW0Obko8HK*K{}LZeb{bHL~6`6pbX)3`LUyMCr1U zg*L>#YV!9on9HV|gJPK?ph z!{*=Mm(2p(W!bX?S&@_0+oT)%mQ_4JO*Qj&NzLUqahQo7Uz0=@8ca7$2U&e)H(xXa9f`!;bFDV>^`bp*}GMcm!K z?L=y(M$-brG^k&Snn@u{B+`u7jQ0G2cl0SD>B)2^Tu7ja`N4;+o&A-jC~vA9#RYr~ znhTc{8=XUpxvKGO>O3XMjL$PZ=+*y|GFU$n5$HGw&(2rQpm3K&VK;M^?6vNaI1At| zY2k8H;p8>hpLtEPSPYnSvc(1!UdaPmhCyQ?*TpTsK>T0!d0O=5?66o&h0heCJGenR0A+`?uQwuKW4 zm$yKnVwapUTOErCrEKU7!?~FaUwoo8xdqlH;S;=}*64Xu4rPa-7*oJWC;?I665_*sN`#r&9sID{VJ3{N}(%#fu zH28WNf5*h%opT_7>5KDKu0YgpGe3hyW7n=@wS8?W5DteOd($Dckvl!AtdH4FX*?I-)NPM`!0^?e-zYix=W7qr0yN{^B z*O|=Q54N|k7To@PdkYWD?a#Eg{JuAS7mSD69yI8%46;}yfSKPP_I&gD8YYahsy$*h{-Sgz;!Yda=>6vhfJ3LrJIhCGIvJ zVU^Iyc>WQbgfwLSQP7@_Er#O7%UB~}F`-l1Nz$dm?K2Z@o2^>hUYxz--AA4cG6h3) zy)~gL{G7RcQ+u0m`8Q%Y)sHS@?fFQ`|zQ z#BE-4$6Qdq4aLOWOWZ2fB)SSgPjLc!on-JrD8Z$_ptwPGQx0`V^9U@5lP{$}af9ma z5h!j@-8}-u@hEYmQ9US*M_@VCr5-%7A9=km2zd?Y@#8Q0Bq1+eL32b}Q5>}z38mha zPvliGg(=1pa%DwwyVBr6%6s0!A(2N}P0ZHJm{S}Ek<{?AP_xwUdg7tXi2oxP?G2Dx z2?3aOT|+l0xy+W}O7*kZUso5NjchR$h}$S0$fv7U$xm(+FPZuYdcX>9U6tL!vk=rQ zZxsAmz5F~rT6r!x!Z}~s|6f2-hahmX+gSyX++aQFCDx>e)0O&`qwC0YIf5|2uG9}k zf~%GK50GzifhT;c{aMssfwsQWTj_Ybyo>UM_Uci!9>*=qtYmQyxwh%Yd-fRUR4G3}{R|&lODf<{FrdVE#Sq@!bd(n?^8=olJf)ekTH8s+9R7G)y0Lvu(Yo z;=>s>%^A^Sq3I4&%a+uz<4At%L1vvZhr!6)t1j+OBbp=bG`W}kwo>P(x<4V8Ki0ZK-DG&^Z>rVy5V!Wsxpn)#dYxol8H zgug#kQ>nUR-%;I}v@`~jKw#%|Lyb;~DgLAmbK4%}5@)*6E>t~O*y6C8on49D2bCL{ zxOrKh45!HLPI7szd`=|X+Ent4Pjd0txIE#^?iXMqOJ{d*0j4INB0%dkPLZcS$z^+_ zASxJ;Av#GKkyLF?GIXsd@Q7HA5uvFFR)Z|V;&LMiZafec2b%)%f}w<-J47X%!Pj*r z3!}}-v?{0?v#n?~kZd8_cP!i^-A(9L?stJuKEXeR73dU#XbC15opX4M(#e_jYb)x*x#ywdTwq-%>xxM z1-;szRY?WI0>kYH|DA0!P&B*WM!YD$s8N{+ z1ZhS97p89x4|SRb5ZEz9NbGOwjC>$l!6+KBjT5q)D%gBJ^pDDM9?;!brc)tU5Y(N;*$r%Iy+p!UR(iD ziT9C~=lILmYuEwjF&uKB+jP=%So*4HT_&%J{D)w;)xyg7UdXucRmBAndZwI7XbyCD zo5dn$L0a0UMw2q%0jCHPA_!~$6gFNB=557{Gk3A1RLH zv^X8#J;6~D)9thKiVN)9Zql>o`YsJU+y)JDMS$fLz)0%8N&+Mpnk8~Lr8%Ac#ndTl z#CEKXX;@!^F_`Jb%IJ^(Uk(Jj_aibft7d>7?ZL($55ByEIJefSNcv~oM-1O z<~0-}am&ibicvofKyXot=|g$%t_#nFTWcCa$4-_JD-G59h8lOgRW8mAE646V{sLwK zHW=#V;0s+0|BiU5OB64WMRm0SM2{=C?;owaCd3Qau|bE2AWVN?RgNf=nU$AloPf!5Mw{>gG- zr1Sl7+P!)mMI_X1?fDQN=@P_|O?I!^o88}!G{8-TV?jme<_is8h9jXHr3EKoM8OH% z0QbUosMO*+vlaJI9(5)8N<~~cB|A@hA)9@TuFDD=l4DY9Y&)&jky|YTipP>1Htsui zd9|6b^IZVDr*r{EDL}y?q*|g2=0c;H;xL`aX!BIyH+8zr4`)xBFIbm4%hP9|?2fh` zPtYwPUZzZ~y-s87gi*YNx`(;|VLi(YeC`_(&9R=Xsyoi1(i?DJQhkT^11X43CF}+( zYIo93IzOxPhhLsf>Sfzm2$L-^y7EaTQ`H)Cc8aX z4MKX<2@|>IB|?XR#9d9MDmJ&7@t>)PsN6)m@qHwwPqrXbIgDt?q`&6Otf&n*L4UiY z%3LHPnOpUmUIb8VUw=J#Pf)B12=^t+q>n3JJTD0?S=|$jktUfal=4d)^Gph z?T_Y4246A0pM!G~CGPNtr(Gnq;BDZ?nSj^nJPuQ4WH~d%e3RC}Egth=3Wz%Mj&$@% zIA`g?8)hqK=knRPERSJM@UwIIEL}cJms@iD&85qq^u!LhU!!$OT(n>`@@KZcd6jYyNA`FiDe-TQxqh*j4i@HhoK4Hp8L)&9kDDt zz{C`>ENq5NEDKzY@Dw=NW*V>2P)95a5-%cF1Fb%BoGpqz+M=?UqwWT+P#fifK|eIU z7*)2$Jyzrv_sQQVwqvldt(hCP^o}9XBJS&*(j=?cw%N1WzJ}kny$klT0O)WO)~8Q} zTnc?k602-xqgk|5bFWiu)9kf;R0yUaG#jRA;m}_csExR>x7Gzlh;2fAk@M;&Wnf@G z`GyDDDUza_V1lDZ^e4q|mdt0=9xI>wnzY;Kv8#LnI$C|Bnz>NK6cboKK$~f}RdP}k zN#n88Xt*Ck*xCMnB!owp55AMc&_*PrnE%fp5CnS(pq+X<#Rxk69WlF>=t1tq&^S9vfW+#Cl-FwE{)5a6nCp>(q`SUDA=Ezt&EfB_BJAB0}n& zMm7Mj`Bdq&8Q&h=r&BRb^l#=pynSzCUZ`~5tkY%?sRvxOlPqU&d(BAB!u`Q3vtv4- zZG0@#M=M<^-tk7}8!wuX3=dMV3KC}J*e7CJ-@(uc`R;OAfN!ZbvCSPAl zGV6yqTNNwMBJJ)|H&gWD{0*2YTGQl0$t`F%TW4&UiFRucC11+TeeV1qcJue1QLz=FD#dn`OVQ1ww>nTQna8?$8YraMT( zH|6}B-uz8}{-$&A^P8plH*Iurw6i?_W;B1ZGJmr=f3r4!vp#>bWBz75f3tJ`=A8MP zbLVe%&EK3ie{=r)%?0x}yXSA7Hh=T<`I|lSH+$!AzHR>IZ_VF4qq|{AOd>Rq!Kwx- zBGsWef>SmrM;Q&Z6RaIXc)u*hF}+8|N+0Q2l%@+-gqSZ*1vY8@fB2&6!^ZE=KKbiM znwn?kR;G>I^U9x`rMlk=R5xfYL{!SZiT;LQM?#}SbfE^Z_vimLlG{J(7b_{1g9h*> zSxK`ig&D9}L|%wYNlR@FDzn;t_10E@KBU=dsuas&>OEj+>-b9 zTCJ`ZSz-L6wh&kPc0~Jxe%*4u7=XsFS&DrI;gmy0t({+FrUlk{4;bD=FrslG%hrA?(SAOr!O zO8t3S@-QVlA~kb{fp=W3kfJVG{~QQ7VHhe|*VTke27u>H5TGl9=UHNm%M??Vo<&rd z^mb&4IehLmiI5emvx$-J?0@l;7DIW1$uBZzRc6K`e;5tAw5_vmDS_LEMZJKKW z-cGs2YaP4=-hUqONQlh#2Ru6CDVBM_BYVwAB%_xK5@Liuvqy6)ey$W;c)mM)Z+M(y zlmV2T38b^hAycp$S#wNPe!;)T%TOnzc=M!nf-!8Um2sf+Tsu{T&sT7iRzd6C@s4WS zr+8R5JFA}Kl#4LxR-l=KJ}qn!kq#hCuOb^`^moA_@|)CwKr?{`K-oxSa~%jewTS*G z5C6M)SmJrC70Ykxup$#*tTy8kC{N>(WZotf0843ZTs#1NUYeqUXawez7Me31=YY*y zpqe8c=U!#)s~Gc@KlqwBldnye$0=D0y(n5;4^#3~&i+am;V zFjcTgm4{e5Wnx@6&9_tKnQ~+N#bFRK^!hZ93yO+uX$E~Gq>WuFx)It~`^&Sc2zjji z3G*xEqzAbFF{ax}fZKPW`R-H+@Y)anohb7HU>Xwyz(t=_#z6I5e6!LFamatXNZPyP zllr+H9(e9K z{@-TYi8!2OZc2;*ll$m*WO`hbH{XAEwR9)!42G2CVW2K6l+G0TyNiX~cX2yEfNrAX z)B4bj_uqZzbcw)Mst1@5S1kcsgbPfybk}rvEA|*}#?#S}wO6qV@;keAI=XARa_e-p zS~;@wRd?xmN7|Aj)vMe!Wpai`#;@YrBj>#e$D9!_Zk-M(hHF3fOZ-Yv)C$1h=yIqA zt;51GrQ>7>&|;>bP@W5pd>SywwE-90fKbS*Rm=^Yh%f~$-vy+A>zPAKK#1CK4Iuc5 zx==9r8mGZiAsgLAg+fm&9Dijtj0g~MY!K#v84TG&5+X(qQZGmJLSM!r299XI*}#Qq zM##FcD;ztrFf+}0Os^e2>`C456%Jn!>!U3PN{+?|&XsGRIRG?hb1y$jRW*=oPgOtm z#wH&{T7_q+Dg^d#99886rd_x7^mBeaVZ?}ekjAoL=_~R;XoqebGT{6se!vU+1$+-; z7{wGErb}}1uDA!y1;rKCI;1|VlyLTP?yNtT9}1RTk(38KnZ$5%zL(vT3#@2)V*J(0 zMxI@oD~+*~Apu7(&T(?3_G-B1R%s;5TgUwZ@f+NSPE1-hyvK#F_*!BfhILyRF{mi42HA))#N(V?h8hIV~xg#nB zUeK{h921xEWT&078(?gqZ#%k9?PxnSJ*VpykiR%VemxhAU?spW5oZv(g3_;HM$pXH4Yd0bw2r~9G(hL30is{*(vAqjR6Q*X44$73 z(<$SF7~2eu`AC^=i$l|#Mm#q{h`G5Qs>Ckk%5mk&s|R_Zw+4R3IBQ6gw*On@>l;Ph z{3-i+-fM5>uS0K^*rak;F2V+FP3Jx>6HoMC5d=gTz8xve}OT7a>%y@EcJQ>bCIVYa1&OPC*FuoekJ;Ccjvk;T`rvJGvZkgid z(X0P5nl;mD{e=dC$is~bCNO=ndjwwLi}D!^tZ-Z*@>RVo%KFO`G?6seCj(PP+-&3l z#(^S73a&r-sOvD24|k6q%YB6WubV|G^{IG-Oh0?{$#{et{p`{E;}L3!qCT$AX+HPv zqtQ-ao7f5bKaU@0`)924@t@)`4y5OALiYKArw-Xqi`#i*e_K46M>ekMU}7HGBz6c8 z;U$)jD1BMB#z#b9;hTh>4Bw7-4T}Dg=V2x3a8g9Qv_6k7vn%#6h1PfESyp7}kBPdS z|MU+c{=i*i4gc&pgd!E@TBFI7`9=oXzh?REP0RHnHK@GUJi)R~myQ^CS}omYW1$~xlD`EvnP^D8OLGf z;b&~B8`*Dv=`fZGy2U&r!J*$2<9pUY0wg`8ki{Qeq$6NreE-O>0=6yC5MWX;O2qpi@A+{2 zpP>+j2Q=G?zN}Wy3feE*%?}bcnu;2^wqVvu7$-Hem$yqI_v8l9_N4jwCaw z5(`2nne;;}(O0LHD)Ap8nT~uByIm4r`XQ6d6>@cT)l_&;g1GkM- z*$DejDpfY+o+4;tl{`#~SXXo45`Sy*}nw_0*XXo45`SxU5+HcZ+dT)0a zk#QQ`sVXg-y!jeOk*p8OEH8%3N?Y|Ei3OFDM1Vn#=y}9Xt_laE6U^>q`>a@K6{Wmr53IqXyH_wZ5v2IMGe*tr;D1UXoohh zNWT$4FD;(KZyu?C;_j04X0=9CAZyZ9J|*wu6trLND>HSZgTy%Gc>Hsr4BLyM0Z29! zIm|V}oj)p*uE!_Wq)TYCsEz|$Oe9ZYO1nhI#-CtF1(jt}zp{lNnXt{OWe4&9<%4NT zyVlL6F4aL+J0)}#Rsym-2V0&&X_54(?hWLW_8iWLnEpV&IQJW>>BZ3owQ$K$xs=ZH zvRj4k*Qx16p(xVNdkQtZuHn%1qVQGA+bDc@@Y6)TNNHv|!`!>Nb#F*n)oD$~&H(k+ z)194)cR{JEM)bw01J|dv(78rK<5wd@PtHMW!1SD)>m=Wwy=1y;lXh1na#!_^jhs2w zX?IbitKCJ(t9JL?YS-kP1?}#72d6sXss-^(w7aNvYrsAyX?N*_tBX-^4Wia{`87&- z7L>j@Gwu6Rpxr$?d)+_6UeC~m&84+d{B@bVzktfu-&1P>^{e_A=nBnVq`RMvoT$i{Kb%B$IS-@ z7qdi*oLrR0KEi2K82g))y4S$g#8FmkkW0qA8WGucB3hg2nAO~)V_q-HjNPo4vMh3r ziDw~0j3y-A;v#^sWs$7J%sG72_+GiIMG%KcCo!_k9JFoF?mj%j7e9US@p*jl3^i)~ z`YrH9Ig5X6OMKBS(aq;T*x(CKgM=^L$H7Rcy7!R}G7^tD65X$aN|BdPSu&tAR5A~` zR424>P2w1?^78Eb=5%*C8$U_6%;AVv!X-RBfO(7^iz~=dV;aFy@n~ZfN`%8~JerFx zB3LROEe1=)qs3sUc+`d+h4G=dAziLdKwX0Iv$saABP)+MI zlu_N2#wUGUzG&OVCnaCBQ{$7ce>b6}_@ikAT<|gjBg{@l)TLl}()OMcC+@`l;Yl7& z+$z(Mk!Ci*Fmifrc+!^sXkjYYDm>{a@<-b;JV~~G+6XZzV|M_lP ziv9Ds=CjjL?Dr;$O)BO0=LVF959(%${qu7k#0w?2@F3orVt+uN;6&AD9>m*H?Dtz5 zuI6+e#M>hT+i$Ain?|u2OqCXZ&mM+H4qgHW@_MaxUGekFDHWq8D z(?Fc!HoQo&+d@u~MT-4z`}s;7ALOYcFx~(hTBvDX*Dqq2SGz-Wdi%31$=aNn!_atA zBU`Wu>f}vN@@P3gI&jpucaLzHwxKOOxG|J#Jgf-rZz5OXAxxKt0#D;K-Vy*TNvKSu zPLET0+lM;d=r*$6er^(0(sy%mj_MqQg>Y8`%Mdy45OWBqSEcgu0vwa3+sXiFg^el4k z(z(~OToU;B>b@)IK(gPF!J31Crl4r8y+ zr^}|NL>o)ERQM})we`$vPmL=zhrC7#QkgKvDK$!dbbcV*j{=MQ%94@!``pRcBreU-t{brk;04c5^skiDo})uwKf_lsa@mf;O3-X zP)2?YWg$JymBT#gR{?8xii{&1ij=E{&UvIogv5R9SAUuKHu`L_U5zxGilJSuOg>=- z`h>Jd&m%~Y&!Wl0P`@{-Q2pBEOHBZP_YrP8r$*N8nvN3Uz&4{X7g@*iCZP{GBbv#; zGY;?-H9V`hn?v6C$s_BA&1z~Q>sIuhUzv}rLz&$~)(ulX(dl>VH!|9WYgf0-L$_VA zz)p#*ONS#zqS8kZU65jTwSjdbqi%v?y9bb&Khk=ths%R?CO5P|{>L#U(k#RK;q_lx7tc zY4Vk&cU4#Y-ZQ8^PClJgZ!dOtmG2dyZ89{1b3H0$RGo9M*x-Un?Lv30Fj-XR9wexm zFeNRRm?SmcjjCM-*>H$@c3T-$YitmB>KX_x8ZV+>^9YaKf3G7H2&Xfmd>W_36s3N# zLrhHPU=G`bEo?+2!7|3BbBTcHO*juVt9ELQ%rK<@S61O8TXETulJ1{>Fv7Y4Ph0$X zVH>URAdPciPFpX!S(Q?WH_B>j0Q^Q<_qEJt5=yZTo}JHU=ksaj-8W@E-`AzCNN0t& zWHcDeT&{W7lysD@&Zp2~RNd+hWRRy3?nWj{)Bq?cSTLTf_-qQoob}UHL&b8T#Ikxr zVtQfWwXndMj*?#ok&tL}rz^)y9tX+uFusA`E%DuG?mG^jk1yxG(^Ml@1}RO^MFH8+ zVpFoE^{`Smsohw6q#5U_MNQTQeS$@;M&$T(C-D#|Zr38enh86EnqGR8y_~e1YjD)Y zwohO`Q#f*TO@j;7$d!UF{H?3yO{dup0Nn&y=$zZr?9H8oGc>2!D`y~*)p43V8N862 z)%kFmy&1%_k{z)*&3@3FW*_Gn&{#?w$9VxT7U^*M?oB$)-Z#x@_U#*Y9UnKR*|(43 za%mOe$gX4%K1@H@%07q#&Ue(G;*yR<_`DDPPW{$jY9GA*7kRKzzn@Dw%|6gTt2dJq zKjFv@G_A&I_Fa>VG5AuI502gJmK(K5IwG@v53Nx5$J@p}-y|e#Lhs<6@jSp^{&v{t z4MrtzXRfy|nU=}o6vC0)*sXkk{YT?}B=JKWI?nI%{U zEIb41$7#f*s^cHt$<(Ck!o4Tz)O4oLVL9S5;C$AAz{ zGCv>@m+4^(?SS-~0qL6|SR^TeU4C@@!JtwuVZN7MJ(|rJEFN9aq^afXpLTdHc}@5D z+U_yL!L2@X&QV>8xTCR$m}m7t9=s0q(R;mloPWlz8zUW_=H36Jad5^wDuOuY6Q(v~9J&&rBTPx-#xR!E%U5E)?vwbRn4G*M97tjRBnw~LH8w;;69^u4 zcv%;?tq`mRlf__b@Rq}3aipKjV9R85Hd1(^4@}`nuzUldqMcC$yUN@n-plmoi5wIx z{DdHqz;GT1+5v1wjHf1EV%!a0Irr#BmR*`fyz2$<>U`f;&x0s5nyyN zNa6A5gKRFqomUi3#jHW=|)g(OBD@O9WfdU%b>jpOYDO z83U9tR`%qAONmCqw(prDTFm$&~Al(G@rcgoz0^$iB#b4v>U|jxaUi(R9r|c<5+kUar=pQXDii{>z=lZ~E1iMRJO~ zai=R|+ak=@@tbx^+lkHAr_#*Afre1eqP0Fc;qw*=tQYqwm{;IIE3jgeD9migek>8r zoNtL+2(wfca}|OPQ=6u^rx86mujJzZz9C1zLbZ>lGy$xw$Jn)OBids?+TnnnzZhO6 zbVf`J2Nxn1_s-R}6_@%%dU#n@aTy)Vr|meql?F#;9j4+BDx&UN=WT2( zG+V#ee4(C;6gH{w?}fF5_qC<*PY$ijeeCLKmkYt(gkl+A;MufGy=3sB4zSWpM|qNx zY}2}ij*|Wss6-s~jNiWQ1Yt9{X8aJtmje$O&)8+bjI*oo7=iV<{-y&45NunF9afOS2rZs)O`Kb_}x0TgK&STu66d@)y+CDmEN+F4S}AVE%MI znF=C+Hp3h4PK9u$@fA5~qZSpgxe(q*zCj*H_po7s19*LQuXz(FGxQ$F;P(IX-M04Z zHyfE)oodg1eH>ewo8A6RvBc|#$tuj2woQiQHkppDIn^By5Q3HV9B0tlOrzjL0pQJ8kIQ9 zu71^P?>WgDmsgcO(>A@oX_Lm_h4aV~DLN#@vq ztCW_3s*p26z>wO(i%H9~8ul~eLHR7GApeYi;ycUs)U@KTZ0c@%0+F2#?xGXKj=wyZ zS2j_eqBy_i{+E*x><$Bn;2qriUp^ghT<2X^BQTj%OC&lYYA6XObN@6fLPJ3302v-R zbYg-fly?JmzX7+T9vj|$Cr7POtpE43oJ*6vYtWGF6p6pYR&?aRiOVzkQTM&6PW(MX z?n}k_#UodLPGd@N77sL>g%fFN$r}(G8j+AMIqY$Stm5S1ARI;^@p{g^NsJ7#IU;Ka zeK4+UaUuN5c@Q-NTOGW{g());4>3uM(leNDHx8a1AjHEat8%gi2N~I+!BDU|sLv%s z?=~vr^RhnbHR6sPhdcBsai>ZwuP^j2&5p@>bKL>tIkNe5%;2YIAxhTo;FEEDy{~v@ zKH2GR`Hppql9z8-zgSbK$S~iUI*_nb%uz@^>J3f7}ldZ8`)0^3s8oTK=?G}fb zKaM~7`UHwE4&y`jnW z?4&Kesgw2(&raGjP34oHWYWI%m(gT9X+Qi8Vu>GP@7|6jev-q0PQwzX!=6rMi8=PC zb;#hrCj`aj0HEzy;;-hwrzKVSXm*3wfefTWL$*BdXSA57D@I9y@Pl2L#IJE<->SOYgiUd2<6 zCU*R)~-S} zE31TsX$4JyWMi+2qoJ#j1}c*{?pX6&bNVi|U9Wse{-L=? zFG-Ke^oW;;Vx)0H9eK=#Jk^*DbIA=;bT}kS$KptCF}yk*`n{zmT1&L_g=#kjLcw^VOYe86U{f;b?O#YKTMoQk_#YELV!Vifp(n^i%6h(zC#b z?*J~<8n{_`%*lvWj04ikdG&6fU)Gcsf}iy)XHdZ$SJ{MA6k7ZGpn?gr^`ekD%u8v zN)9zE_8917v7pN@T61M5wB!9<$H|IZ$SX#z6$>oq+@Gt=>=(4E`C-h?b{!MucFe45t*lD5t%$pZA>UDj=3nK;U$c zCQS{iOuq*&+{4_!@|6X~mm!+C{!*;YM?at2J9fGG|qQn;zo!Ksx7+S=OK zE@o8?w&_=(&VH5bI}BqIhZ44zWAm(*kD-4!wNG0#k51h_EkJ>X3F`X<2xyPoD(u|I zZBDH)tF~v~{b)ZQHY7Kf z>2Ay&RqUQpU?aUjE3L(pk?T1rrb!s6TGYTfLJ5%GH!M8ZQ zux#9@OE>CN{pDTt!qZDfcQo&=?yA444c%@Us7LZhy?XsdE#4>+it+yu)U}{5Q!RY; zvbN)%kEs_s#};j0(Y1Y5l(`|D#Yz8J)8pS?O%PtwmAaNvbDp4d`^D)5H-3Fza6520 ze8gOuD6rN5eVPqQvyL&vFuL(oY78s5a zawhI@A)d=>4PinN4EaE2B&!Q|U)hp+pTE!$E2ic!epF(Ch&9v46aHX?XsYLkE)`RD zf`5nwUQgHF&C}ImcwcZZn+c+FFRRtc1cknTV?$c4Uy@FBPED>R57+wIqvSi$?SIX^ zd5zTkFR_XhzTaUhJ7B8%A+^Rac(?JK1A~Enx!U3W%Z21Dz(xxE$qrkZ{iU+-id`@B zeinIz$!ecXSL&ZaLi_fjU-nP;hxdTBB82@M%U_A&WA(Y!4s<5EkZ}_6ItBnB!J;?@ z8(fAVyTX)>e;2hl4_2$ya81USi*N_^{X1mY{F*DkD4&)TQl=V5FS5xF+4YpsU-=D} zAQ1s5)#?lP#A$#52d6Bo&_l4c4)Q?m8lspIenrd#li-to{^2Bm1tPKFfH_S%JA&h) znC?n)${@*xf#5R%%jZGX$ zf+FP=Vyg#8RA?1d_+iAe@VNC^3jd8~(dpEb{u=R&4&#gEP5qhw^dJ(NQ-Ho58h>yt zA1Yjv&1cvR`wnb%+Zod=l<@>Y04V?>{AQ)7J7WgqUB~(-cx%v3L zI$e_XaDD|0w%_fZE*}M-=zk2L=DX!ug%;L5&TtHpi82tH81E3#kKqTmM0K>iPHC)c zu6BqPLT2-~ez|=X@VDF9aA8^+d|UagRV+2LgF`yw!4%h2R!WR+Nhi?wVjj*k z%_76LhEl^DR?g14!w%AEXWe_W#hsmX66CY9?i)ty9(y(Z>1Su%Q#kAXo&I?Ft;z7y zpEvwe#d^QU%Q7p&^1yD#_DO(R{ZUSkLg`t5mTQ<1cusUr{oCI2{0qdW!2#^giuyC{ z&oaE(;?_OVFQOb~!nR!eoRuqs+WYbmDq&~bpQV>^8^j%O>aXw;vYxe;gE4Oe6&>Fv zcGxI@6~ddfF*5q)8Yp2E8a#{fKXx18BaF7=`UEBjQb%S@C4aKSx2f1`@W6o|3-xUM z0~S-X%Z?Prm}Jk%;?EavtNOUR!KU-~d-v$@mUQucrj3pZ#(=6g2Gd>HI*0AZQ}9!6 zE15fsU?=oTl=;fzW!M|w>hRS|b@8K7Gq+5YT|?t5w*^b*w#J<*eCnt4_VxSnl=^|_ z%<5-A!I50Ch|oHxiLSec-R?K6cD#qx?l)|9+{0$~;pV&Yve*g~QD55gi8I zQc`}dmu!{@+ln&~Voo}8DE;egqru~swuE~1`>~#4fyjM_9Ox6wfng2j0~F0w(MjrI ze~IS1vSA}eObi*F*-*a=SHA3pYk9QCOu?=<1lL;E9XPCf2yMYRV}yZwVF+ZGiKcLr zBGL}tm8L_{I0q)7N1MQeDV3#3VA6&b!?|w6*{{4r$D6p-aN|&|j#vr|#6x_ThQ(0B ze1--*+GVjSheYg-mU%*S!4se-oK0yOLsWtjoTjBF4N^-lS8J{(UdZtybA%-5I*j*d z)!!YSXUJb>xrwoDzid7NI@KWN>ZIYBbv{{(SSE}>%0$tEr#6G3bO5Iepms8WsY*Tl z1ZQ&#N{G>X(^hF)B~2o*_ZxYdB0V7tV~tTeRMLpiAR}hpef@l(dPfNw+o3o~cuCP` zOZBI5aU7-qv@r8bms1Sdl9y}SkzR^(5TOH&!Kyw8JG_rTlFY&Sy9OdL`@PZW_`A3) z?JKW6nC-)w9Cr<1B!{9URUm6GZ?Yrgr{q@I??T!0U``VwHNv=7=0D<5@i-4VgO$St)G^WcpU{zNC^ak6{vY4 zNNB{@gl&iWx|yIfz@9{fux#$B@!MT@)C9L*|EeRhkCU<>HcRz0@OS`5gyi_~-j#d2j?*RQxb&za7y@2A!8V=IN{OQv zn+0w-1-mYF+Rjn^D1t9*8iknKj7A&YL4CD7M|tx zcX86^Z3N)a1TF7S?a{9f-z3{G10a8L06=a)H4gx)A((9AeW7DIxNHr{>Man_ehbM` zKq8U=C9lu9rGCjx1RW0B_b@F2mIr3~0a$)I%!GT*Cd}Yg1rfX~V^b9{N#am2@mCW} zHY*xfL>S(CfbkR}B@}20H__-O+>qMswf$l>#UFjFQ-U92Or33VL2PAB^k<@x#zG!q zA8KPjzmsVdYI7po|NfLzDo)&DBfo44!vJTsuRN?;4PEmD^}SxaR=aJur`tP05{7e{ zYhrL|PV9miS~y*#+HtU~xU74gJQE*52flAa^95lZ43Ytz(;<@&GGUl!9O^NQJ$Nl5 zJlTU81hcg?dvMP1ufd+mI=Tnza>X9(JcfCSG?mTHxeVxoMt<_}6O0hC2NMe23o{3b zO($5p2g`1_tiffs1sCjrY$ME9ku^hAMD&Z+2r&hH`?K$~U*Zldh&ylsi_gaf2zpK| zcz}7`fHzToOfiz7dZ*X~>k1$4;H$_e2IxeGuH$Q`n~bBNNk-n+P)&?ra)F^OA zv9CI5SOd;f+S z43{3S8ObY}g^py}m z0(K5@h@dFJ7acJe2@&23kWlAfy@&y7e~Kmn1Fba3>vX_Hh#uW&Yz<2)zbQnMqVV8M zjYk11O@y2_vIg^;vJ4Y#OOvWE{DzYwVtASWO%^v0Bd3xD{~I@Tymr%sd~1$2>P#l&LcvYZ#amBExbUxHLSp=+E2p23TXdfZnZ8^IwSKB5ee^Ua))*Ka=|x1bNM$**rhE)c%|pS|~w zvg|DDeD{y@t4^J&KAlvO4jm@Djy+PX-II3ROwvNurFTt$L?RO$WJQ+i4s(}(WTmrQ z3B;Hg+jNI?u!DhVG-9h!TVdz~$t8GM5)`%agPDL4qM`rS=lgx$ zea@-ss!ouAUP&5i@3Z&2-yhHWywCgme#P28Jqr2IPven2PPV!}LOx3&wN*hO10laQ z3Yie{AD=>gqY?5M+Ok(?|9uuA8*XA<3i%D$vftb;+LJ8nWupnVS{G%sStYn3JWb$C zklG}ul+rpW@k01%a8nFFCq6>*WN`JjZZp_R zzlIr>@K4v2+p>c$1ku|W40b86t>mZy&r^e8M2e{cOoe9x9a1z{ zBQ7*qfd4YY*{20@tH~2QV{n=i)S-wqZ%l5Gim^#){~I*SWcLy_I)6TQO8jqsEVTFU z;&x|t5xpS`7F-fJ{fva>Tpef88Lkj~0DBJzB?!_ZBrjWS9H(#a)c?wfn096_l|bjD ziJJ3h22nTJnvf~a$u53DzC+R_S&JlSyUCuEwZ|EtoSty0B&Z|QKDg_hlka#zrUz)q zTk_u%jFA3jh={B@J2N!q=-(|*H_g;^mb@B@2|~WDS%8WbFpe4s;f+R~G!zJSOe1?! zzurQS0J19+ouGsA2>|h>K}5WmZ7?SXkwpEXz8}Z7LXMz)E~$$aPl3uDYM{p2Mj!hU zbn^?ji;Voc0wQvLa+1J77=Bn;#1uG({s(PW8Y&2ChAy&*XRYv&oUN43Fp5BCkeZd! zUqHN2(zFxb0l-~S&gX(zbCQQ*rNRq}#SEsS%tsaTnVDj1}7PJVBT090&$q(cqYv64Ef z85U)agCPuNW>C<^nRfsN;M$8`UR0ycL(EO+cd7xs1ba#QhI^}SF5Zd-n3>^rZ#CFD zLd^UgbQIY|gm_}TMtx!J3$_o|J;y6V^eQS2Qg{cQz8?oTu%5-IMX{Sp->GusK?PY8 zz25<{ftN3g^~v8IqK+@TVt7mrvRKQ!ZnZE9+eu1IMLdfXq)bmDsjTlBzsaT?#@#4~ z!MAyaIj59qA|`|$Kw>Y_3`0^c1jtT!v>xb{hO-kJQ&6DujDS8-4 zh&LmhHO|Qp&(4((KOWmQ__$>Z6!OP2dLC6H$T3MXQB8xaR&qu_m}&y78~VnX@}3oY z2<=rtiq9G;&RY1>*^3QM9bp}#{&x3Jc2s4kGPr1CV*_R`SkHA|#K0;MtFBrw2zqS7 zREk>wQli@IMbta-KTv6rSPmCeZ$)KUg;HeYFOsqabP!elE4_GjFGJdSz z?wVHXs7vvVHGML7vfIksiH;Dx8JIV1?o`FbcEISm=~f5hCYuSib)s@ns|kO;{R(Vx zx%Q1pcOY(|3tP1kc6gJhpMQ`8*?RGRFo z$vKuJV5P;R&E!sIW@Rg6D$)_9G-I5TiYBH0#2C@yS4#b9Qvf6-X)cu~QhT$utCaea zE^I>asgXIf>n&+-Zn6{=T0>_sMy(s|p$i5s4maqQCuqrn05f1^i%?6RD zN*`R*w?gWa{!l4$T39>EJrro~%qj@ykI7t3sKQIPimcT?PKMhu%j5&i#71@;#`OyD zyA<8`7$Lhlq#V{|2<*5lh(haTOJwY075oqn*oa@P=R30<&sV}@jpxef2Y=DC4qu3E9XR;q!=at3O^huIQBxq0WYRzk;dII6s1ufnZ0V#-B5ewCH#f z>@z!RX6pf6V0q95{gLH>uggiOLGwg~B1>)4xPUlx}Jsh$N>1E?T3%&q`jZUo>Q z0MnfWV6$?o0UWHJU_Qlcf@Op!17J1kea`Kg3cFpkXPHLG>RC0Uqqic)e5k&CjRWS$ zkYs?V+Rx&hLHI1*rGz=(G`#!mPF~7I=XkMqluKz-vmEZ1@r>LjE4p%m&ii%!*x!Bt zGJdyi;Q`*G>mT~jUApC~ck0Slx9fV!dBxjwU2&14x@8}|P1i3zu8NN6malHrm9Gx# z%K1FE=z5pGI+ThW)b-c==s9=%Q1&-tsPy5+0ee0_d)L{}J$TXp?27dfn3TxoC7^`0eu&F&`1Px~0}4OLKpx9J_pR@0+$1RKvD^_h!4RcN%MJNKp$`LoKvy0=%vDi0AJvujkLbC0%=)8b#y#Hgb|+EMRZz)xK9$Ii z<+6~9>8Lz8awYO^4jsu6YeRm~SytlGL-|?kDfWmLjeqvFW!r$6-9@4gF1sZwN?}PJ zGKtLW&Xt3rDiU^D{aF}30wgRW6xFC-|Lkx6!R$+w*izw3vNlV5@Njk|3>J{VqcU^g znQ~!Ys$eu6Nkp@ACKL`c=jIsw0RP!RYn zDFNiRp2JbK%)te)hnjB9$DWwwi^Ot|#r8|YmyYM0l4)pyW-~4ri%S|TK%jKO(~eM~ zV^k1(1ZV^2Cl!meKywR*ez}Ej2lyE&wP5@}b2RT_D^V{aOYo<4#qdi2)_rA^Rbn0$ zH_Gxr6P*SkK(WYzGnAlPgxHLuhn+wW%hf3<%UILWiqqI2Rh(CaJlv!YufcRXxjlr+ zT+AcBN0P6sm03ud0o56zE{HuP>Y^a1Q9yLtqVD~sbZ=vrz#%TNKJ6-ILX!7 zIoY9Kdhw_u#h;yOvYpu*cwjF!=>bqI&}j`J)HX^XWgwA|RzOluQGI|2EIq+yl9$c8 zdJl-mie{|p;Z&Cv!i_|dLmHh%PDVI&FxC%Hb6;bCl6#_61m5YP%IlDthXF(Cg+xSE zC_gtWBw zqMO>Vl>b|;yuY6pXGzZD8u@iHI>2wT`$(G5orYXm2?@>2&_36@xIAd>9XIEu&yk{4 zy3M2AzLVam6Us|s4$vG`-K{8iMxCv4zM&SXs`Nw0b4Z~>GkfvBU{l!t+WPEkaZqEr z`BLfm+(mkx9yh&J7)bQ7>8&hQczQ}@3Pr}HFtys<%A$;Vq<>X?lH22BQ5@Y(eXM23 z$31tsr%pAUfd2BBYEg2|GD`8xAmf4f z24-_rSrPE5MrUto7p>S^r7`brV;M~T0R|tn)2+Wa9qfR@cQhUBaCw0&Dx%$W(rLyV zYph$JF&*r%8HYg$+!+PshIcj>f*meDKbt{MmVzB1?kUoNT~R%8B8+Ds(gB};sgIQZ z3?66(Wgu(I{pit%wt7jHgYe9g&@8XX9uQQB15~Ihhez}vvWBp&{VHFnb8>Ek+Yw*k zSOcMmiV8hK-w=;s68v~95_{~sM;sq)xb?Tr zYrkJ%^(2KVI=z&TtjUW74dLbgmIyc(2LY#{Sg&kNq9 zJa~zAGpM|Z!`;G8?X9<@KrbnVBaJ5el4h}qqnZ?qC@;N&+(O%wul+j+@|yMBcG!i9 zq2iTnwGD#>u@TIr|I+@?PRy1e%z}SPx-{@&x{GYsCw5Ur=;~nMETQ|8&irglHLKn)a}ZrKnl9lr9Os1H`Q-n@)X7gYpCcz%wRz)C#%3ja>m9# zos}Zq1g2eVnN`UO6I4e-=Og!MtyI~-NIJ+UoPjOauuiaHQAXS2s$eed2|>H94W$EM z*T-9t2XZ<@MgvKic!wypH4)lcEAH8M#iZz;_!syh_`|rX`@_fD9Kl)l*;IzV1l98q zPkO&tGDo5m{(>T$C{Wg$uHKp5@x)6s0m*-YXD=1n)~#!4rVUpxZtvI=|1GO!TM#Rz zVbFeR#?e$KZ{}ye?q)#g=nKQC?|tikRc8Q`=pv!30uo?a4Yx7W@)IB?!K?$M1hG;O zD+Mv^H!x9*gu9IZ_EaNrQyT&+QYN0fg{PX5r)oh<-*GMF?2&dL*I|@9@c`VdkS zETd1ZJCw_-LD*$$M2hqR&3S~p70Ei%F^KkkE~yu-c~rwqVkP_-3xI16R!<0S2KrXS zblV7ZR3=a4`4QYT7X$=%s2Kh+GoV*?v)~TrmZA}Q)E#f(JlEONPDja?B!2v9eg!`r zZA-^qG$LsU+X$ZWMI)H9a>6tXgMV)7E=Dx>KQivH_@gGMjvtS=O36yoYGaK~di+BO zNDbe6yY7zz;TV=WFnn}#0Qh*$Ud$qPShoiI7*dq3k1_&5rhXLpq%-_km_i{SD*v3Aqn73fX?aE50ybjN(?VjaDU*0{W_|eBJU4XcPjCik zVD*FH8tsbc6|>k9XnQAT8gn9oCqNy`!tpoEOEKN`eme1$j3 zp<{_+sDs+aQI-1GQ05T^>yX$6u^P}AZ|j02QCfWt$_dMq2WH6`Bo$g9M>G2he-B4| z)&vSm+3)#;J_vuJAjqMS9%x27`*5q6i6AQW&KA@SqOPqO;9DpKp|x#;7{j0HEY1Nm zW=6(jNtkLaAAT1232%Kt3n4iQd;|u}A+p`vv8w_5-13=+M)S*0W;ZV1@VbMC4j)}P zkYCS9^FmrD%oDEmO9E(`e=IYjG2c}3PRXYNTuo%ue>zr|*YbVUy_(2!n55Y_##!^N?0J!AkN3*jfG&+>NXz^8RdcGz zaIS{a^?T$qcHpUKimXFZ4UqMThUfSqfBlVGHb909)6Uf(YN8B%I~a#tfoLAggoT=} z-F1xyny^LrSW>>4=fJ%T*UaGT2K1hxm=ya_=6PTs`PF+vdjKFKr5z4PKwuEV&KRkt zlJcs8xz*i1s4I6uv{`v!dJ=jSYg6wvn^K?|JFDp3v3WO$Si38d-Lz<)H9)oJzrLJ zuC?CLV@@_NZ&4q6=4A7IixWypL8oe^o`zejy!T4(oaPxHW(Z6D$vw@sVdzz@OnX@=Ltauhadj;;uRH zOd;x08p0x{)6UQwu0HI{eglae5H|!fFYJZdJDFm|1FTfGp;*T(iY$}KQg9{oQ^mt$ z2h`W}Xq%VH&Bu-=5%w2_$K~}-th0g-Y(iwmi!qA?Gy;SPP#c8){wwCoEYE}?ETIt` z1!yW#&J+9J<&d6t70rk8ncL0=>;yMDF^vsNF1uu)c-6k;3ts`r>Dpui^D}%Uzo5KV z&y`GHZ~64^XJz(|!2xuClP!jv75|R?0A1xZ-&sEJ){CqDuj17R8mkHwtrJ3Kn@TdOxdpjR_{J@2(s2C5HCUE}2^vQpu7Sh7YmZ8mG z6^;J)H!gqhwx9dZqj&t&J3b~(;=;`TJ&wnv;Z^1TTL#d-D=U_VuRo~IIya8G`lf(@ zt}ac#Km5A)@jOq@pP&|PYW<&QM{dn?-k(F{0m_nN>1Mtq`SIOe+Mw zDe{00o@u`(!C1Rr{01Z?h?B$NuTtJQ$xIkK_9`rR94UB+H@{HJXC?Yso29DhK5fQ1a z`9b_{_XT-hTogJ~E`(7_@5R1Y$zx4q1y=Aw$VRFh#HE%nV(__e*1KGCPka_PpT*6w zD)^g)o4@xgZsMYRGx83H?W>o1cApxcMdNb?gr_QE10`W+mH%gf;zvvJZb< z`I^jQBFakami7t#oA+$pHG95Hh0}hLC zp8Stj*LQI(UNeH!vned+)Q|KhX;T^^9fXGaTLlA-+NCs`s0wlDwQj~~eEE&0fUmmc zo~CbNR7kBMep!KFIC^PsN`fAWC*F;ESbFj#@iEMJPuF|(7oU_7vcB!fv#+?C>yamv zfUSP~-TZuQz4=3QniR!H8;a>>8{R@=yT=g)*Q_C&bYgYKjBr(8-h+GafFakmezyY^|w{!QtP7gO_BrJn&?KJ8FUoqJ+2t z%k%l{M?xV~>~Z%cw`9#cF^U&nJu2(`8U<4xzx5)J>B&uGD5-DLA;|P%mSgk4hGMU@ zKTH!qkSgjp>LzBfQ0cuG+#dZA>;5plk^L#9FMI{mtMFWeFHqVDN`hmWG1bjH~*)PVWcg`p19 z8)em}(;S+JcYxX}?_8W+MhDm+YE__ozM-RpDTEM8KUmGaz8dT!Twrdwe_%9Nz81xx z9Ly!}g-%i7F7v1csW?=7;Evv_;lqs*jt=K2d@ji7K*11zJr#1Y6%Ykob7p<8rV%5h zvNlkWREV^}Tu#)7*`&tg+)j8%uXR$mvIG5h@Z&E_HNH+jviOn7XFQ$6_Fcx-EhB$%-(VcuEYTy`xo zH)eI4wc!bgKF>%|X7OBIo-a$pE6pk59UBm&frX)~Z4_G?(;LJl=cvJHvp+(CR?jKZ z9hQy*Kal0k=NC)7JNRxXleTdOW!^wakdOpX8~$-ISGX0V8wt1nW6mG~$^LU3?k6u9 zrv_9hcenn_BP+r}I!y2B>jGv%dbOsPqW)iZFWYO!{*2U$(rV0sp8SqsCKC9Wl#5mo z&n>IO?L2GE_%J3MN^ZxQR{baV%IHwbTyVm(gG24T11kw+d6A}_b;K*^l*FlUoN3k1 z!_jDaWC?B6i@0gZnIscdA#t6-1y_`ZiuVdZ@Ib1RxC)G8<%H3Ym|MvN90yT}nO<-IL~)VEFJ zagZvzuTLxyaSxgL**W;VJA5QNz)Ck|R@7e!$35!MAo zYq6&`%%xNv@5XwUsWbp5&zTPf-Da$EmzEA_u0Vb5z#X!0^Eup0`<&WPsa_^F{ zTlKT0#A6$|d2Dw^?KG)b{N`g9fU(k9SaB3^9Tk5t4Nu=x>3%)@dOdlRgP z)*S26%=0Sfi=9Ac41*Dn2ZO`Ra6oQX=&>JV?uMT}C7Z2}=uQF5^#ec0-D5mEkoIP| zHlRK9NCDOjV4ePHmVa=JP*4H@CY!ow0N%{TOPsga5BGvgKMZ!nJ6yK??jpV06YpXR z6sJ+vZ^1o2?6oo{xTUfd=n))1D)PL`R~`~lvkqud<_&E*3hM_j$t6x-QqbhpJ?BS| z*Fv@y+it^a3k4UwPEPhpWzPhU{TYh-Zn!T$^}sdggT-Xj4{O-@VqOX$pgyY-D#8;1 zf?r3hyW{37fqEsI#7+-odLZ<8R+BU7Z+!&M16=UqVd`FedpxW%G=5VVv{yM@X}x4x zIS4voD8S1hqn2NFNtNE-HP> zY&W0&=b7#1(?NyUG+FqSa9`mh*oEXesZE)ir>q?Pd7&bph~D9nAUr^d5u(Q%XRGA3Y?Zv@Qp(Qpjm~UC z*G|Z16aJbAY9Zt>LiS)$T`VSl)+%Ph6;-L<6V{ooX&8W2Lj(h5{V0=W|}hO$1zz9kTF{s&ZTo~a9xzW{^zYSYW%z4JwCGw@>9BjM<)DF~N! zS(FFPu_@yh+}b=~k?Hg*nFjZ8c9ngQOEk)~V0*jxNAHC^;t-}jfAlEN*irj_iG6iv zRxc__2%gDxD+C(JV|%%zf{`&k>eSZiN7UtJrm90|+DxrvgJDOF;FlTIv`K!d;@#POdRPd~dT8B@}RUmQXiN!>m@e6?LIX{=2-dDv`pTHsi zeI8JIz*Zb>%JKkmH0+&|q<~v*)EO*xb5`hkXImC1)%>jrPz!xS$ZGah=!oGIe=z)Q zxCXMTXt^o53cuJqM`cZ#9_}kAgFr+V-Bmz@>3w46&}6VnX*sGg$cCUlJc#%0Qo&)z zV$rpa=IkNi0qx2!grukqC9_7`g})|X7K5w3R>mFWVZjv|35Gd$0uVSAnnv$q#szd$ z)Hh2(GN{5xNRD$QOyLFmV9o8;GkFR6LNuPrUqmnY`a*uesz4w-8ihl4#Hx7+!~cc$ z3_PLhVyPQGN$}YwMlx$a2sGI-@KP}#lmh~Oz{4I;sLAkRsBMYn7q|e*v!WOD&|4Vt z5#6f~9la;IwtSQdJLjvj?v^_WBb&PSxt6BnU!xUp4B)u|$X#X$%G%1Xvi2bymKS=* z08Frm_6w`h#<>Dl%wgWkt@EYOu&#~^5W~4jz|la7Jle%&=?dgY5wVUqb8n%{Pec+O zyzO2NJ>U(K^MzmF{{1`{{%7=A_qOwybaX&#f{P?9@f5GDR*bB2me3SU=@}mT{ioJ5 z&}Z;KeqlENJk;$nbApJ%Iu_X0;V*!eCaVS1fjNcGb4dv+{C0GsO6$jfrH*eqA405% z&uPA$iKP_Z_a{Q#q*1ydsxQ**N>dkHX$9`CSFc1ocSV`X+!=GxQW7uorq8<}W zk>)fl=w_l4LIa-~bxdK9P!ieFhW5m4uTHt}RN&^oNVhX5% z>rzjzqoaZPNFOuH$w0IFwO3S3QhCi{39-_CfHs-`a_J?@`YYV(Y)4^-bPtpv7;P*# z$P8WvF;nhyh?!5O_2Sh2&Nj}40|sJKM{*N4lv4{faBblU>tMO@0*O|v4C%3@m+Y@y zYiTGiewxq`(AHkqJ!g^TH``EB#KN>4A)PtH8z)?07X~LN;&*z#(f`#$w7w%&@T_RQ zJP$o)x^YhaQy2q)25LPW`l#9tp|h4AB~G?gnZu>H5bEJ?`U2KqKp8}P@i0oDgpb#e zTT^n{`tIhg$|(a^Sl?E%`?%SG#wT~s&=lxG!%ZNB)_Au*qSA00440Dbc8?BChK#mm z;}MT5XldM$pr(UfNE|G{phu7ik)8O#_EXYaf!d#srjkGeoR|rGf*@d2(4`xc1Ph!Us`z4+;AYL zUlp0(sL1?)Ts$s9QmyrC8OVjqMfw{R=?`!ua}n6w^=s)3a3^yS1thQA-Od1~GKzrS zU-&A7EX;oXedx*6zmKl@efOH*53TwA*!VZI#A-Xv@490k$wjw{rw>e?IvGtoy=(H+ zrpb7EL{CYf0GK0o%Jm-9)0Uq`WPzVP&S=t8n11NEEu0lcCyKDKx)m{>CXZj5JV0K~ z{x7){l#}?&Vo#mz%x*n)v32gl8SBg#L@H;9+qpdyY!P}BrG=_YO zhU|4+A3XL!_Kx~iUxrUe$b*O;JVP;(X`t|s2`C+aLI}uq9ABpzWyFpGfT3Usv#rQD zU(yBbgF?%7zj5dbu10h=aQKn!o%fSy1B=}rvyP%vvcDp6kfUhLCk4FBr1#ZUv>xlC-Hw^*Y4D3U#-}6ZeA^hYo_j8Q}s9`O1gobeo(fc>}wXKrR&sJsTA=5 z_28N{rO4cR^=o)>$~AD?k7s;g)tL>+MCjHqV@xBS-fdTOAlJ&2HzzX3Q1@jGI`J); z6m&KY2T7^lus3FrgZVi5umu%XQv(Yy4G(AO*UF?1c_XVu44s>bTOeu=gBv!{VUg-{kYo2pd@+5zpgTtsIOCbQW; zO_9ATs8iY!Vfp?E@%`e%DNAVredzRYECveX<@fFRm0s(}$G_BG1T*-@D_{cwh=-z( z+E4j2dC2S`qls5+3&Sfeuq+^eP>5v!vn1;(f`YhDNoxu+!>;#15WzEu4!R<5o+uEP(P%&qG@IuGB#pN)G2gQ6aRj~ny|EExBQ!s^j<;iugr z;Gt2b9u-)A&3-(YlK}>BixuJiwaf=AQvmZHNDVgF=p%c_sy+fML3aYYrm*@rU6?*f zE@l{lRs%d~6os&UNMi;e*95|GkG3-T(_NP@5DKzwpv~;ZlHox*V41$?K}K=0c98o+ zr-Dg(gu&ES&Y)UBgn1qed5td93NoD2U{=Ykpc5A%KHxF@8+tgc4~UfsCUujWLptGB zgt^&0pK>yxvRty+^f;R@vRpAGB4(`-2+YtRA(Rng#uelGFQRtTr%)Sp z&RFWS0X$~J!{c&j%cv0p5Z`<8a7!NSsw?8;t^rdkM}~LE6UcgICRofZ`!d&M!<?2js)oLC_E;nA^{s2F%04ZEb)Qq=?>bVE8{zVTOx1xUw4B;juZ>B~~(- zo28U2>Z5ra+llZl->2b4-y(*c?jNW z(ZDJTtqBza6icb$$q~`-rO-iTCDsB1hLY`S!HZ5d&-5$O?P|-`M;rsNo08Cafdh)c z@YEseex+oPB>QAE0HGvDW*qG^XAX zVTBlIh<4PVeGPd}83jsdfnf)8PY1qQ@S{9K>4x`gGZO`4sAl$#X07kzgYt@c-_;P` zzKu2H&64b36&Atq)^G5JMy;|O>4*w(hKk-E&Ars}aa1W%T@aKDlYGMGO35XSXZl>l z*j!YHbyRG$d+GzPJJ`eVHlvW{e7=6et+zjvy>@Rkj~-v|lARKrQ}br<8V-Xc=KQ*J zu|&d@F`7BV?pYxVoo#wn8YSHZtX(A7r>nvPN~wF6+C2)Mkr{v|Jho`T;IuD~VM3*X zdNqUgc7`p9T@2NCmiDxn19{R4E-DdsG7goZ2~b|?mb&ZBpg-BMH=-kIuQ8#W42 zm`Wt&))k5IMy+e5PjAsiTZR0w3WRKo10yc)7A)(f`r$*3S2=6*3M!s{R{!G7C z6na?dK_%{(EY zy`iY!Y0ZA_;1*VoIuK?=^*092qO`M|OZUPGr{%AYSh+ey7}3-8o->ZL$0{xp0T0)7 zDZ|lIv#4p#sIO2=?Je3#&|8fa)qJ6%B|C?h?kLjJl}>8T#F`fzYbC@($E|vzD1R}8 z>*4TSxde&@v&doyZeauxh_HZ(Z=oJ`k?<&D<4~>eDKB8!KpNN1dmn-zz9!mbiE6GX z2g9M=#(}!Fc{|%gi$bz zdW+Zyy&?U)4TQ9UUSChP4F9&Ujde?m4XA3N@G_VhFC<=F=i^Wm>vdI(;!2u8z`eXG@LMQf&4qT6xf)kM zyodefqIk0>yBUh~({Re8l0lM)lwvx-QG+-)uR`tZQzi(zSW2ji`$K1DA$^Lc<48 z4C{J}4`CYArKB;|?-1rm`)g9o(tEjMAsfS{j`wF3NXibja|s@Hx+|4{BdfpgM`<9d z_i4)~$=3j3U;V&qqTySBqx(JONKrrRip1B!A5^L>L62Ucy$0uo`^}j=vwF>@Cj~c8l$h2x%{DkLbLiPzH1JXDrq20ipNnRK zVqltbE`b#*kpvpCP~0^%#EEev$e{^*2D8!x?>2l0mjH<=I1N6FiI{4>wCY}opExkV@VS@$f)UvBQ;3_$t(|tf+Us#W9 zmB^GI1A0UIF(C0%mZWXggj&f?yKxo>+9y;4IDdQ)v0LMVuyW;y8S71HZdU9HZWZ-` z{jXuqk94W3M2#cP2xO3YW})G+<7xyL#(w<*ifM3eTd*Xhp4LrnBSik&2D zql!&Q-|)5D_$7McjbEbvf*~V%Pey6E*qgPX71){{xeiY9=}BKIpTUOR1%?|m`m(t= z7H+Cb-P9!lU@nqQS_s9gu)1tnXll~)5s59Gx=l-)bcv&W1dhB4POeYxMd(cPTz=`B zUJ2jn=WW!^*4w4Zz4~=*w%mB4DUHWb(MuEXLkJXV1%nNuM^ZM>Oi9^b=B;f~%u=PK zY>J#koAFsVNmUMy54dgQ&&ee7J51m>U18pff#zt{O@$Z|3l>ij z4hi7g4(35rtePP)fZR7w>xPz9C$>Yg?8PS?tDGr%p<_La_Yi<(i4Sk%OjH!_P`94- ztt5vmKxky=K98|}`RbU$$&vWv@3y5^%>~w2JlTA%J z)+K%KWaU-dp&FI?70M3tZ4Ip3IaLF0RIh}Qg;cNubsKkSap80%^;C(9%vMdy5;#gs zi9s@UTAtwXYB`8>>1f+RH>N1KaNQ_kK_C#_$fQeJliPwsj>G7+k|+9wM|CQOKx|UfG-k=nDyilr-g*`y*EESWbuduP1EhoOB#ygFJkH`<&>~5I6z;JByo~qg~Ltx^MZIxofa6^4JsD|iWiIqHoF1|C5LL-`utOKw%U9U~bBDG3cy3=~yl8U#{k6KmJl)wNTtjjOV* zg6{8#IPvcvn&Ol$uOp{rk>YftmrilI(MzW|-RLFzq`|yyn<8PX z2&P6HV&ic}|JamLiqnniwOF~)OIuDhddXVdpwWL)DNZY>MQASMj`B4#^O!GHW;XD>?URXqfYup6!-})A0|*Nuv7%4rE1+3!6uXf@(*-I>I}5cXz^1F z-KJ>paJqZId`fZ`Q#^W)^5ubYv=qwD#5Os)Z5@Vxxr<; zO_6uXCyvg3RV16mF*aB`iUk{CRTr`w-o6ej`}KqY~zAb!#eVr<58a{@Qi{+bQiQ^&PNxWkc3Oc?GEhaTQbAGoe;5Iw zf92kKmn3*#h5o`*kGl&o8~v-|1&{4g(?jUyt4FgRo_?i(5+s&=di7X$)vH##`Z2va z{9!(GJ5(14R{y6f>RsBQ+O^N=>DB*n{J^;d6b$7Of>HFo0fh!HT@Qd^GL-~P`US<2 z0aaeeegPw1%&sUn=i7WLUu%prfc|QF&1S?gq1YUlNdZz|<(2({gg~aW#-=9ni-Ewc zn>HsLX(#PfWO$pY4*)^LJX9|)VdCr%D!))Ov8^WS0xBQSffx&I55ib3EvHDOep>b7 z))Gv4j%UV$<VQmRdpPoSf)j(YSV9rI#c!x6sk7!Vvr6h1NLxFV2EKA zL`W_+cIsMCS`$7Ps^pMhmxo|HLY_uR$_oS%+4TIBpDcd!n-yCU9@^w`$FI+gV<8(075x$So4(doQOhc;t-JT`Cj9PO$G`=1m<2a1)|r&mFT-lkSEMP2Ks>~tAE6B(~rrR)Gsl1 z1`TCd63vhS&2Q0;pEO_lhqH+SM*oODsmmgn(^ZwLs^YXG$}q31%4qVm2|yguyWV1E zfG;uwQoP`HtxuR0gobRa?xmoB(I&0j{kEGn5IG`s-HF2^}Tc@pUD$MlL(4Hk>0+hSS<`qS%WKt!H1? zDJQ~4Q1(PA-`env8?`!fNh0G|KQEE77!cnfJ^IF90=EqsP1^A3x2#L+a9GaLhI`R~ z8q$V$MHo|AJs%%r0gDyHOB=qCH5kNjTN)7RaE%J=4D}Wq zuLKIjc=-IP9cPq~8wq|=Vm$k)m}slR#kz$$oI61rQJ07abH9qCsaUo9Uz>6@HM={` ztnWoWW6!x72a*aZ7Q!3lMSE<@yK6&I^)nljs=s-bRQ>;yR6$TdbqJOBMQIhjW(xHw zNn{f~L^X940=Xb+g zu}`Ny4)vQx+1JDmouo>&eUd7vus8dg<9wEEgJU61SQq%qOhLlbS*zAkva6QRfX=t; z_hbEFUp*(kw-tGOX0h9$wlQkW@+}#LG|Sq6v60VcRP*o!L{mhV6^sK>7&hls^TNIe zs^G{1ZX%jONn_@St=c*wNznITC2yiB=8~OnE=5yB?kq8!NO#!d3q@1hAE`Jc(U2=` z2`bI507<7UCFnxYs_abU23#Dfqr?>GHDZB?Js$HFuT@wlOvuWGaZx%xil%R)CguY+ z92u4Wu9N7ID-Mc$LC5IG5-V^Em;RWy>PJbyTG#U8NtwoK}Tt2|+|7B@MMUts$39iXcGx(-x+r z{-}qK^_~M~<&jw(*@`XgS=@ORcb*b=?pPmpenTh4IM=CM1jZUi$W8&#F!u;XvI)Sg zgR5p6;{j4GD63?;ik%^Iu8`a&P4>4E)u6K^KmhJsoP$ISs<}v?tH8EkQso9PcS8DH zQA-Tx!}k>|G=vrW#}B?~c%f!f|AjpY%E273I_qdcS9o?kBM+gjmb_Aqj_ zp1!usH2$MfG-u8)Eay4LBU60oSpfWIYn(@jz;A_f}i}*Fpk;_>H2w!0*duDUw z{?o;qHE5jCYf;g-8_s%&XXBYmIaPf$FRd5D6Vdr`bzaNEi z3N^g{jYD*Cy^6-*UA;XfM4xCC z!~GqlLKNSEz{Iy2#_;t48W|cfT$opu8dj{bIQ&S3q`M|^)^8M6?Ha`M&m@zs-@x(r znLBES^@)FK$-(a~C=9T_tx>(j$^m`VVR zws=N0Ts(zdp1F3s!74Etbra+^FNIjA-yG~Gs4Q+`K+=aaZf2TJJH*IQ7DNk8bE*8W zvN__D2mWLgw)l$6=19nm&&vY8hLWwa>P(h2mrWp)TFnAn{1TuZx4+uGDFp9$Laz)& zt@1mi=-C46%Y_;MgPd*wfy3Fj!3ktE9r*6xo(cMOqO8d;}lrwtGzaV7e zDwT$IgbHOlSd~b$fki$WOuiB0-^nv} z{F`-JuxiG?cZHbuVHDgSHb9)mVFZrAeDP+unE6rRf=P6+|5|Yupz4uWHVDFc{(G5_xdhmL92)xW~51fp9uU1T-I_ z0^MXM@J&fMvgmetNy6CS>gvaSiuy<+78EM$HoO=98=#4?xLrQU>7;+3YTZ z4BKYI&?M{8jqT-SW2v$EPS{1m78{$&o2n&x*$+J&(?O>sH^%Xrk`(EX=}lG6{{Gk! zh7pw=%|nodo-*vxTZiv}TLXZs)S*t})y)E%X0}`?_Z~$OIrBPRP_j#L8*(#RHfPJX zI4Q#}K=V{n^N;$OF(QaEzin{BO}A+^T0{d$h^m?t5s1v%)>&y;P%lf%-Wu+sj4dNM z8@NU_FFG588Qu{!Ms;T)AXDp>#hGO}Wl&T3y{pfrOiEc`c>!->u69@DGECVHe%+uQ zBCJ8)EXMgfScTJIp^Mg_60j@!obm-i_?<0K+uZP{$&HW z5jaJS9`ri1v|dZt;Aw|$u!<*PmrN__2IV(L5vU_2h-uJVqZ}P@fhB?@h(K#U(^Lz} zvboNFu9YGo#&vxB+$O~cKnoVsw@>1S*?^Eq43dhWkCyILH5 z+qQrC?ay1<{vF@BL%Xu{n9`^#pFIdpe?^LAW%Qahn?&Zq8_Kwlp_qHaE{{ZniczXEis^ZEpTWb8~icQ#Ch>&CRH}`L^a}TXXX- zo11TMZl2fNEHyXVo15=wZoad**}=^$rn0uAGzeId(u63(lo3h`p^>E6jpW)@j)_o% zqg8vH^$()Ve$394(*1F6l>LHKC;mrT$Uc$*u`Xr#lZ)B-nHFYb`9l_h?^Balz>7Ub z-f^E`hV@_oU91{Son3kq;EY{*cuO}(n%aXFu^_eGD=uZ);aV;-B7@UZO3m(qSnZU< zby*+%b4B`Z<1W#vYI2*>5IG~`Q)WG=A{dA!;X`Cj-@*m_mhH;P4<4eWl*m#P5y6e| z+ng`%)dZF8Lc>1G&f5ZTCTb0U8Eud>OL9~{4cq3~8rW{J%at}rgdeJkO$+X2%*1(R zCx#{#H{F?#Iqt@Z2`c+gQ;CBGAco>mVyiS^`yEA+&`CIaoKtK zT7sDhpA1qHRXCiHIOb;>QU-}o95%@N<0{B$82EMLb#!}k?oItbbBBkuRY@yIb|vh( zBnzfd+Llp^m5{)S>0nEvgg-YG7>?RnDhQtEN+v+gifAvmtXe82A|bB~zce1CL)tt? zIKP3$GG2=jauN{~M{u)>)!^V1w*I~(6+4jkwS3eou9zfaYwWWt#=~ZJqnamk`j7k!&UwLqx+FZ?SMONIB!nB8C z&xqWX16R{kSwyR*i!z3;?ILgxpn@+moYgZ%j&nTHy1Pe5tOsBQ5=F-!KT#dg+Mww2 z(dSoCbb6kWxR|iWM}o%ro?S>xmlqP8ksX>2q@ztwsNa-)o!V`^^6_lUk>%n4(kkY| zOD$oIXO(4!52eT`;+xeHfE7I#59Fx`o2#%5jZ^9~payGVAxughj)&Y`@4k}t$lbZ< zsxFf(grBc4cSJ(N1q3TDLqsYrgGEj)*D4aznb#LdYc?R|3*WHMZL<}MUu{$j?F0Irs9r0)2bN1fCUAuD9_GBjl*9AyCK(P_yzsAVbtmn9s8g*$WA}l!;#**?dfP zN|Nh^{au?pYsf#8P!{~e1H&+b%%wu({VIMNr#5;ip=HHNqeoUn`LN_; zE7{BDBn#k+cLp^OiUyNIf!H+g`*fY4-ndTPyH3L%%;=6o)}k}49+d6siB$&BFu*@` zO8s;w6@7O`O$Sp^m4ma?S@lk^lopo`Aqa@O(mJ$ATi^VqBP*HV;K74|oS5#T0Lp8w z^_gZWiwIi#)Co&I+98>6i9;~*5{Do+5{E1ggQhJxmJ^S@GkfqS|BYN`aUXep_B=Xq zq{*}WJm=Y7;t^E$wntaA-!16~`Dqr9A<5BxENJ4P3&7EagEU*tFDl-d{qytPRd?A! zYL|+rt!kDNk;3}QU1!e2Em8ild_Ema1|?n-OlH@`@wuehxc0on`4= zM&^xO1VWNk|NEBR~8|tU(AbZDmgOg+i5oJJZ-bE?`>X^q@+e#`11BkrWpt)!m z{*w(+FpN=h2R&Uq&~^+Ixq`vkE8e!0vK~#G6OuQ{5b}J1}Yk=fs+3Wj0-zG zdJbWS)s{J|UYun+9=3UF!7pmG$}c!&Z+qgIRjKoW&HiPp#6kZ0d>=B|M())O_5{U2 z2pE;DmvO8`;{)7YRz{5OWpl*zz${N5dpYd$Mtjw((%bf|3OODF1$Xx=ol*OJ|0k|> zZ5O*Pm41bu9D`?tugJG&`KS|Zyi&hrMs1wqtM;W^Uo-~`Nt5tMF4a^*akYKbD_k4` z*?z^H`tJ+qCo4HT}j!{E->0`{th*iVF7o(;{yqTq-m1Ct|71sh|K!ETL(;6uXHp zc4RciD{Bun%cTW@LP|!470egBXbo?tKTE})`e&e2xTDf19Sbg#24w#)2>Zz5Rj>qN zTnhRrR?7@BckJPGy)*nIr&NoT_O+5ZeG*z6-y}gv-%zxD4bmkUICw`-FiqA6?ONU$ z-j1?Ty;fJ%NnYL9Bk`DW-UHETle;|=nh~F@rX&Tartjq#Hu`(}CQ)oLdVq{wz(7gc zH!J{}iS6YcNfq?>Tx2AE?zaDpM~_D^J%f$lJGjJm(PfWohH=S?-g=R)5#@Rj76R^u1L#670py>jo>$d zKT5z0gyS_|!R2nn{NkdTw46xJMl7SPN(t8bd#hQum^DJ%fX_+mkp}ATw3O9T%B)iz zfs}L*{NSzY-$o7O)R`2WeRkFZrN6ai{XMc!1R*1^Sqnf`f|1RD7LlDm={_q}dZ;Tv zS;@`PVBx(4N#X?KdIXbQ?tw?lOSTcvFe#g*DD`hw9Eq7&@+=7I7&etgRtu>=Tp#(E zK>SEw;=<9G=yp^iBFGEXv8oo38Ll$DgTST!b)tU|OceZb9&rB|iw?A8c_Ibh{30KP zq$W@bqPX%6au5=qX@XNqsWWTaq+sNt5hUF6eA09D{nQ~yA#+z8O{4XRn28bu+4k=o zN;^|;vK;;*Iz|x6wUWt+yrD#DxmI!zk2jKNO`z6*i(VqWGcQ%|`5CaUANi|$ZYa;o z(pS*~`1GkdeZah6SgeI%nu4e^S7lCM;j*bFwvD$*uTvvGP^VPi5)Z76gJL5fi|lpk zzc&L>OBeh)?e@|K(@6>_!PjVJQedVjuwIyZcfQ*VyDFLrQGI5OY4VjLJ{9cV>;-yC z)f}}X`^yPWgU4rEiyC}Y%sLs@euO`^onLV^JJgIG%rp-u61-8>nQzgPHd|%0#XwPm zUSKC$+aP;Bap_H8EU>yX3hbAY#6$81-+m{1zv|%nAF!>%a*(CO8;9!lor2+0gH3a( zZpX#1Q(pRhZz*fa=VC$Dz>tgudKUY2FRqFcYR8664c@rp(H3uQ(pCd!J(Bkmcpdf) z>?>v}(DSj{W}hBy`M@`VwltM~eY9m#%QugR9Dk!PoN`H@qAbrk0Qy=QD{`$ex@=Cfskb0n$ zx%ayRoSfBEAR_*Nb*buQ4B^rA;Un=OVKCtP@aaJ3X`pQb*EpT#jd)pELa1dDK!p&0(lybelB9-1eJ?4h$C@p_0aoPkR>0wGGOcaTKJlyd`I*3i>YtwvH$M^7`T0=u6K;;54>Uiy zUy1wF9m&s!b9tJ*_XI7oMhz+Hq&@V)y?dsOZn8AJRB zr&zO~-~c<3*j~u0plJmx67T|;fU*J-smHiEhD$NQ=~eRH^{1>SCn{D;G7c`k@Fzx{ zo2t%_H_a|)LdNC&-!JSIf@Jlnaj+p6g`?#bz7FGbg$(x2-oPO)iR%_gVK$?XgB=FGh%48Er$^jGSV zHI9N9LqxFf$nQ*$ciJ=>$FFhJ?rdpm+j2{!5(45f3Qc`*bi*Y0X1Os z1`c7LOjh@a*I}$!PY8A&5K7Js5K_oDq;%S$FMOL=o+#r0#Q<__h+x!|>3LB~M)_xe zF`CNe_&}fGlR7xIS(`)+8ulzn2XGt%2W-HjUrDRq1oHIazu`THg+mc8{D@DlqlK66B z&o0BB-JsUVZAnHh#+o{ua6C-pMlS5%k}y%oCdip-W3@>@o*dC6U2*s?=YqVfkM?8m zKjUsTK>`GrfZthrOEpPc(gUcoI<5ZZdUvlH3X5wa`}vr!=v5 zO}Qh+EWfdp&rRYFp-LqihcU%F0`w(fcX-5>pST@o=|FKgf(*+rJ|hJQi%M#4K1Y|W zkNQH5MY{x%7)wbzc!vbMmoyt)YND}up3M19Njypw7*zABs+t9{AyorOx^tD{F-G$| zk_D5HdxAA&%>XudZt;8gRd!qvCLg%flcgOaJqhnv8ZcP%ZQ4U*)w2yFho5?)-uiF z^gm$$$n7*`;WXdx&CEJYnO$K{ND9^)I8E6`FP(>YqnGqo9c59hQ?0#MH+m^kJk$_A z2gmDdbsF=IX!^$WqL|wFhzXQl*asFPCnV<86R zX3e0)OTtF-iIFobPNaqCzQ0xPu@GnoLDnFv(2y$R1D(}N!m{UZLHT$l7f7y-j4 zjyFNn?sb1WB)Z%RW+L72NoFlrk0nnFf9Lc9d0|e zLU!x(LH+HxW@h^AyAn$4hy2)H$c2;vmz|$&w|^W=lva2RrzDqQq$Lh?^A^bMKq|Lt z##9oz_uomoXd#!uq+gU zvl?@#F_vL`EY?Uo_obki^}IwYly@YzG^BNp^FDJt>HtT0cNDsiae#zpae*=oAk(W3 zUOJeXE0PiZm{6my_!w_7r15PPS%xA|Oq2pc>_~+lPy+p)<<|ic{?7osLL_Y^Q<492AOJzB?+=&JnnX@hE zHx>#!v1DK*7*k4UNxT?g*A$Tb5M}KiIhZQ+9LT8qRt-u`CG26p`0Rsu16==X!$h*9$CK5$cPF^ zHl(S+0srWGL?*3yGc|0n$kOCFpYu9RzfC6{7T2@Q_AC7J0av#CSoamcI9CEpEAx2i zxd3MfzF6kSgaf0#F4zC~F^uTWe$VL($xcAmSdDyKDQ=zFD_Dn`@N0x!cYX0Ca)5|4 z8!n#9(_MY{9YDpI8Pb^zlLsZUjO_aEFaEboQ^Du-vZutUZUFCDsq?$_quG^vX{G+^ zlijOFtn+bGI#%M}vKn!O#{e*05m>K50!%J##`fepKiPUEbg92+YA3ZYH*RZH7ggod z1C1H2jxnacJo8G%U54n;KZbQ>456TJa@Iz_;NAlOIYqUj_bv#a;PX=NBYsh3LDxIw z^e?4!YkCkY6{O?o?npU2hMzl?kvhHVFJslW+FD0#s0Y$ztp3#f=^q1*JF|CtuBd$W zv)Z&&5s-&`U%+Qvb}L9xo`8_gWvRSZpl|28P+8x@i6oE&fZnXcQVZ^eAJ7u=r#`tzW4SPfv}e_jV%!@K=>djR#*ny; zA$@f1ka%;lAw3vFdT?q;4?Vph(NYYF7Ca=bVsssfRftw{cZp8DBcMP$_^w7+mM0{? z;n@o&j*`$ie#5gjzZ;&t`E3;hJbUvS+%?bM{N|*+y#8p`G?0r)8GaF|9rV_Xtfcqc zm(}Ogk8>G5ya4rFxtF_7pTgbI=I#%kLRFI#GbcZ66}`>c!&ZwJ@CgbQ5}b%m8XD`o zv}$Ifm(U0BW<5nnI&5|0dT}M*_@z}d8#Rh*W`jn**7WidJq{JZASCUJlrl2FtxYe# z(eoBt)YDMbJQLaFYqWe)h_)6X4--x?A3i{KW2x3bc&m*Y?VrOPh#+>F1DK@_yCK>q z!umz3*8W4B!+3n?bcmOIlKt~5r#LXuOeyS$qPM%lcb}n%bu>;=+rj4!Ke{ly&jkr~ zQz2~RP^@lp;_ljsyCW=9i0a{f)!CUX5$Q)7mYh!W0SOtT#B|bs7tHGeDoHIaOQLY- z-lQxmR;NYiC<3QJ*)jZ8Qum-tpsp67VqCICad?egzixokCZNT zI%{#NkOI!Lsy;=X9?*dOheu;sA=QerHnscg0o@WaO{-OR4rHN8LZsA;^n8Zd;luZ2 z^%4FIKf8%y(V_}!(gIj0?Bt8izP7qsRH5nM&(an3N8d;lzuaK}<0>%I$X;fEvnM5g zb~e5QSJj6q5Cn0NJc=L;`7x8=qp(7vM`Qelt-O?|)+lq;7xoGVLQuZ z;87T7TVb5|#Y2|~O`!WNkys(G`0;cA->+M7TY&^fLrbtBTolRBT9vEPSIDrutaJ`5=otQBOh9=ckK&h~v^1_? zDtvjAeSoUf#Dmg10y!Z*V)1zeu3h=?x90S2QU=n(W|q|}AZu!DB^N}QdG!sA-=uH3 zno)hdD|{CXK63K0JW6jUT|APifBfXda^lTgHr~wh-B#A_@){HS6P>TAP&+}&9d$vjw zuU$X*#uXs~;Y>f_4a)MZmO8eWAsI+UG;;KY7mE(?Y^k_K_aa%hH+8kf7VbNmN0aK` zt0?lOdbpD5-n`?);y?K}Q^lb`Pf`5K|8uH%5Z2Se6N}kuF*<0ZRyh2rErYcmzC~Cl1%U2(7YKRK|xcSMe-;W9l zRR|7j9#@%BB`K<=s#L-%s+0@Z&g}CCUL5;cRQc(tDnHd!iNn)oJDhQIdU&1{P+n86 zygtoOZkw=43RY(m6;~*8YTyyX_x~arpngD4c&Q|aE@MYO?yOx>OdW-w#F(MkT=DqZ zk!AIsm&papF;!5?yWb1r|Ffq_U&8yw%aCv@>2;(F<$}MGQlWNtWO@C&`!7+_6>?Nq z%s~<8-p~>gpwJ1QU~d&p{WmS0N6+?fmT_*0yYthN2;;4I0_YC?-3p`B{^OWkK)#an zclI&rO!mi)_ec1wa(m%(#XAwT>*u?PT~3=M8-y@OJF@u{YPTmkzv<>=OAr{4+i+SP zgF)bA`yT^QaFw8rH)SZmr1=G^NVSr!xv-Ck>qfC^_zFUn?-TfOJybz1f+IV3GZ9g+ z(yJDSiP2%#p+2SwlE=r9anG>D$b^0*dF?2YQ|Q5>!@aI3)1fyn=1Y==a7G6eWj+U;ucx-vT{Ny|sH@sbcm><;^jZ;pC?q^$60?vQT< zV*&Bw!GVq`%aJlb#ag7p(5@x@dJ_=B(2d-IAl0ec$c27H$kMb?LL`x`Hv}@BBZ&r{Y5{53Q#!m3z&`QtHyB{U`_3Q;;*G@b{=7^Um2iTzZ3WZI z(Y&Hi)-^*8S+L$eyt~LKq~m#+b1qlzMX5Yiq|O)u$le%$#;Rucep$ScL9ZFjjy0$) z9}g^5bKFfdqdbt7dg?(RXjik-%2hh4Tr{Dq?Mu}xQnO0cJke?^3tgz1`%*Qp(pj^p ztz%Xic3X?3YJMrY=Mqu~VGmVvv^CYEsB-Fo#*8t|<1gzO?UN;9Ieu%R9k>MOELv&f z`!1piu}UXhbC)HKUB}&Tbj_@;q-%~;?xAZ2T9UJ;lI zM@0I!LrKJ0j<2!on`(?LG0EzX0!yQyCUp}CD?-Q}@G18QU&ZV@6+si{&3U*kTyRv~F4A2OWgi3V6;W`MDA->FN*xWp!&Z zma?K+6h4%aO=VNe_1T=n&%w{O(H zX~c+ODnf8GQCuF?+3!DCG}6UB7(h9A(Z zv0KA-{$z5QKo8g6mKET5{NsZb+<}>5uoQLF#bUQ~BYsz^?MCpxJS-KjXE$WK*x2{I;t#L7ZnRv8>l*Wg7tJQbj_)mWHM+wYU~tFk43U5?KU!46&34eGviWZco}`x2GXN zE_ZNgv!tZKQxq?M;F2WJED&kMJ6fX>wdOE=sg){n`RSYoCfy$_DAFMoH{XQ$k*CQnasr8Wao?J%YQ22VA;?$~I(iijatry|mWEg!h| zPtQ4EI?nw=AHVMlh59;pI+Y6XPV8M@l^(tIzr69e<7yB5{M-NPU7yNTaXQDI-X0Uj zbk(}Oh5WBmF?<(v_~2LH{@?#%^Q7vdA9>*T1D|@sepTKY_j-LOz{IUnFO#>ZP1GkA zw3lZt1BW%GFeR{3?PE+xnwgfTqMVuvmdBmWI2|)B3pNB=7Hld&vLfl0tm0Isnm{3N zpI7L~_7kymq#U`Ozw`|GW=5RE}d2PCEu3eXewVm*|$+M=n zDU+J=sKr;W{QuZ{7ic@ry1@Ir?0s(goRggrBqbSn&z?A(4DHv5wGD08N8U4BT41z{ zIE$I(`UYLDHLQejk&@}uS%s#w#fTNEREQF!(hjY17ep8lLR+wEkt!GK44-5eKdo2= z6$P}=@AvyZ&%5_NNlpnC5S&nQ_WM5XbHDuW|NjG$<%keejRSIG2(?WcgiuqVv9ss` zt!c_b0y=rgn zhh(aH)3uThB``6_(Du)h%o2aUxA#|yMeaJF6x&c7S+yT1PG0Jh>r3gaFjeZ#Q!6ET z*XY~$glOw^S^j!_4_~pqEuA0a*ufC?Fw5Xh7}q=3ms0gk4@1a6shc-w>({P7YNg$5 zpp(Zr`!7B~ti|EqZDD%AF=hMHQwh37v`0)>OgYe!mhEjOET;tUX4$^egt?U9fo|Vx zLY)#MA=>ww&`SyIrLz5i3As{tP5ZkhY)UyiiS02HmQsRLLHp+>IG~;g|UZscJaaPlNHEcqrKsQ3H{zrpYt{0RG4*^Xunasx(b z4jIgN)|t)sJ?2;NWE#Qe=H#kI(wpPn8&VRVw1t3N1@{* zou!lS&^IbPKNZ!C9uYxqn~}t3bA?LuEUrI9dNMBhAk7@Qjh4Hlyl}pOctS)%^tA3u zDOjOqu&-G(x>+LS%*AK+j`)0>lRX#}umaM;Nas)dbQ7eC6fQy9bNz#xvzOwr2|Md< z4xS$8CPig%s&pEXqG~^0$ifn(6uxPu#eNs&Az>ha$)%&Y<*IKm#6sc5r6QS2LcIm5 zxg;P5DHo?3bjbfCd9c-LPl)AxdJVup5V%#G;* z2H-_;9`@_iYVo~@x%hdyGL>_ROI84y42Fos3q(?%o48H3CFV6)wJWt)NnPP_afIHg z;Z|V=At$)l476$oDWE{$zQ<{+_UKfE#_#%s0lU^ki?jn?VVp!h6kyb>ft`Ayv8-^I z3`-036FYbtTv(3yA;$3vKX^hn{48HkoX-z@AoquRN!3IeLjz23<=z7DD_+J^GE9Zv zmXX3Ra6;$?)2jOy-_^@J1d+&-lEIZNvD{lmTxYrNF1ts1m)(|hm)#>J4l3i1d%GLBpDuulbEH&F$-I+p z!Hq}&8Y?6NugD8s7ws@o`}D7pjQDZ&Uxav}Yb_Id>{Dxr8a9dT&HU`F@9h$KzQ^Y) z1QgFfAdJ^XNqJ0Q%*@Hr+5OatyVt_K&j3te@K_-vx^_(Hiu}`nBO4SRC+kR zS(7g9tS>#k-oxapu;$bb$F=flk3H~S(500#l2MnN{JAWgQZMF3n?2~r7$5+FDMy2r z{C^-7(s)xxOQxgYf+`&_HeXnt!Oy02UmZ?9@L_@#fcAQ@LJbl0rKtzkWJ#LHMg=bP zk{%KPx5FogeU&b02rD!MLtZiKZ4GS9nyV{@ziH$HFPk-^F%e=jaz;!?o9vQCA+I&r zo#?qJ)Yahpy0`(J`NfS9%ocUuv>Xi$Z_;-jt9c+sa&DAzV8v;7d{iQ-Bczh*j()|o z1J78CKGJy2m-9+U4o{aa?N3l)sc_j4lMjl|Zaw5o&_$ybFlpV0*3ZcgOwGx&j+>Kb z$2j?eiS$=LDdQ>ORLJe9bxs0P=O)k5c(}|>o}-kcZs=HuQ~xl<&h_|rbe4x6QA|gC zb#$5)_=U}YC4sSNRnUQNhJuN$)5Qr$a$IrMpQ$*Lnu*jAd%LMSg0F_Ql$0G3mDgwZ z{YL36jqQW*1ceY!cl+h-+d)Be~aFCl%960sm~pyw-KS+-xgXOwL+F~ z?{W=ne(9>wu_RT9(z}^WWNSni*(a4)TufBpkq&F4`CQT?3WnVV{g&7=lgX&d^lvOz<2Z$Eq>(;^F*5%^F9&8ME zkq>=`$T5W5yzB@%7QzrxB~HrlZ~JU(CONJfiv!9fRd^p+h2o{}2=KOTjzk5zk~xCpaRl~LD@jp z2{fTx-sO-{)^))!br+nGfnie$dgLxr=dlz)-zgnK&w26( z(}AuCfaD}a*lFCI1#dR|Wr*1}Aj-MhD9l=4B&nlM6C42yh`N)|38U~!W=HTMt{DmJi}M1IbQ^oY=(X@V49m@0C95@_hv^m=s%HMQ z=5yLz{m`##3INH`S^2Z z(Jf#^oiE$Fr<~AYn(4kfr);pFYWRV2GU7Zk)Qi&1@{A72TF5pE>rYY=>Biw-ZdRER z$PGltiR3##8CwufCr?sR@I~w>=jE#F)(&3%;%g7xaMRI)S6!2Xdf?Izn^apdVJf71 z$f|IKf}U%&vY+NC#XB8h>2Ku%|~yk;J@D5)|c-~78&hkXiIs_0NX5L0=)@e(Prj)mOIv(MXBLoq1 zy7fmTySU~}I5yY5oF)JsOcui$oiO6}@mV5wy%7*|@2oeTko_dIoa;2>I2*<2RylzJ& zvEzMiYLHVyd49L= z7F=T49HDO*c3yAy>R-?b8v}A9%}e*!ZXo4h%DR-}e`5e6gfQ%)<)zI<)}>y4l0#q308x5R8tS~7LtA~FX?(6ovi z#j}r(Z#el*RS5(^P*zDhM=fp|_L6i5bP9xfGkakvJ)#>ZKXLm7-~-%uK17cf_Qc53 z0y+zBpTQflyt{KWs8t@Y0YCeNqlydWhPORZ3g;OsaEBE;ezm&T>zF-_Zp@&io*|IZ ztv^T{jwUCTd$nN;hM&TmAbkB(**%Zf0UIT<5!;1JX3^6@m)dRWifO z$kX7-3T*8{p3(tl0pT^0@6^4OdFdK?pmG{c2}Id6!l;GinSO#%93}`X*a`y>DfN=! z^93?it&GIqq9nqpWb+t%Qq;5Ry%GVJ;?BVYjtkD0pl=I)YSQ_W%wO(5bmoMqs5ox% zi17m5VSUBw8bU^!g0n%oC5>K~7VQpSLtEp9Ptp*WDW;|&3X;P{F)D#fFQW=xirMtS z1`}?xO|Z%>v7CoAozHYhcEAO!2v4RY9_$EKEP0|Fx#RX?co-LhFV@xkRXq)a6UIrv zK>jLZ<0Okvg|@?2%^0~g*c$hH1N>kEl*2EI+Y%^TymdbqcX92^L)g9=+`A*u%NL;> zkY$KH79Q_h+X*lVfP^^0#U5?a3U?8PfGQsR(%*mnGoO6lgD<-Bnkm};n$aSLZM9HL zvEgFS@RV-)`;nvXL#h~Vr=Um<#X{rMBx1e_ffGA}>aT1Tt%v;z4%#$bdkheGuOfwp z;F+fySdso|7AaA_gGxz{{t|D%A<7zM6%HY$%6Tt59UYV{v>KY9s?pHt`=5Lm6 zZ67R%zvGINKyMj}bVKsIFJii(T+02Bm*NQ~k$AQsBo&ifLdu{EOR-#h3=a*SI@HiC z=w~hI;vpD8y|6;^S%U1c*hz*`~3vR9ZdqjIa- zcU%?26neZX9!CzUOyk?+(393FPh9%Ql7O4tw_;&B`M)x^fPutVv$rE8k@T?fH-$RSV+ABD!L5MxnNnWe@iq^ z>J}g|O`gJhLTG($=96BrPD!X1k>iEL5kubde^lAS09%NN5eb@* zg`1)fF9JDD-;oL^N@PM{k8aE0w;Y<0B%Gk{PHYM(!jswqekA|+Sp?tnLu=xJ1YhV8 zxHX=Y{sEzZOyY~MYo}+k)ENhICXNi2S^Gzr6%{}q&3ouMEaBmr7N#hhnO5x!S>l`D zH~MGKK#3!)H^N{b8CcOK-8ctdVB_rdWB;bw4plmZp|=39D(E|>yg@I+l6v@?O8{2q z$Rji8kV68|9=;LfnPgP^uI>pKKdX$OJpv`11bTChk3-WSffdT|L&FYD(~tzi4x1Z1 z53D3-bXy77WXRzCjYrJ!O%+J)WJ(YLF+IWGti8Shkg0AlA%x(tk{}_h1g?MI9X=sN ze&D?f?~G$7 z?2JE>c^~hLDDhSAjQf$pp8C$X&)i>OXKXxTc1Eh;gq;y)CsVt^;B!)yd1oB?>UPGT z)=9u!gwCsj1)RCC*Q=$AL8uaYC8FxY{>>a6)$N{(LBrhFNO-N_#e^)%=I{ewLB19; z$P89qI4L(9;Vl3yT(+4RQ%K*amcxd^l%4*>{g+8*a}~z{HnVS1V7~Qy3zTNp7xtr=5r0HiC%lgYw|k9D&2duP?0UmXNsTYmd_R1uOqyBpc{d6;1YZd;e91@pNDaTzTdHm?)JR+ow(?tqu2!EoMXCHGOu!5nj0!DMeD&QANH z&q;z2&g+O&38bjVh7AA;ep!XJ1NZC!=aX-~Fr`7r0s2fA#jzMG9&bwE%Zmn&Lt#V$ z`2Kt$9{f9lV>$-zL-#~7VIM8}sFnH%?)Yg9e=gpqeu-Cke=Lb%4&*R6pwUMbK>vKs zXrDGf@2k}fU?KX>WXOB(;AcVG2Kdu#vVh65Wtl93@IH>8v)G{S!Cw2gy~FwG0M@;K{f7mv$4iNiwf7OK{sCgtLIuK{#Dg8n0_ z7u_mIOnbRKie4hAH7Cc8b&OjN5g4f-cJC(%7bRJ64Dco6upR)GMWnT)0O@rRPTF_< z4gNYS?I-w2QA!U#z>|d0-`CYrva3zfMrH)vk5Y`Fyc2T`$qgpBuRsFn7yOePF}5oS zO!%^OGy`*|t-ju`YQcyCfsUEOk0gpa1Aii(po3T?$3do&pZo9tpf;SBe*!5cJ6vX* z9Rm{Qm_zMR_x2DQDK663NP8!U|EECvIGe@rLo-^mI^?N9-pnRyA?*#er^Y~>xiMr) zh{1ZuiOxK9ybw%-7|E@)H6+1|B;Ts`G@F~vA+efh$gc@&gI=@5LXVa#9coaH0q8cu%`715 zs1EFNe<7R`bmKhD#$b*13X_=3WTvHI(_MEWOpIXEHA|vIMDYV!?;F247zq`(8<2~K zIbs|CduVd;l@Vrtnq0i0cRV9;qn1BoaxqGL)#Tz4wriXOQ2r?}66R)69+HvRaMa+v z!%@Kr>ioilKzIb5$>U;*ixBV~RR>()jLt8RZ&K zD0RnOMV%gon1cV!c*JlrqALWoq8zXW*lkBp%WMWpw^0z3oUjDYO zo`}n;%;e4`QFk;pyg8J_&+Qn-P6!npzj>KV7;s1$KoZY}CDrHa97l$vPFPuR5m25u-BjL>6d< z%rOfzL*_UNUq*MD>uEES*{ii`KoJ@Sr*_W+z^e5?GeLP_)Fd*XO&8x0gddtdL>IZ< z(f<}6V=@99I2{3ByD@;OrpCbZrXd&yV=LL>-lH%ti{T@Tz=(0HkuB&+kab{6*%3nn zclLZqwki*wd^H)L$M*Q~Q3Aaj#EXNp({!0yaXn2!;{asTXN$yYsz8ZAT{G?XhdEaG76$N2#!#4(IEMZzqj~f1 zz5DG%H33S*+fS!A&2+~pzsyCfId)YAd%Ikf1)L3T+ONS#wVyOvy#B)a*Vj+IIl@7(%Jd*$f(-{&7J;$Go3 zJhqAk() zh!x=2ytSfV422FX3RSPnwg-`jM5h->XwUjjSiX(C%SX-E@wH{@kgi@~T`;v9Wc*Pr zA@{ak#mS*(HkE{On86WZEv7BhlL0h*iad+KW-K-yqi3mdtHMY)z(jZob??BP@I>m~ zAzVWR*S!PGk*jnDsfAciGg9y)?1yFyiBaKY@P)b>q@ZWJ)-jaKNbH9&Fip~&khDmA z38F!rK;6EQP(`kabh)+%?vmgq>U?7(3>f_aRt6j~7Xa1byu9Tya}RHc=2+fRPLrr$ zs3e5|N{ZY?hMlZ`sFv&GI%bH-)$4Pv$L`GHFcZeAD?5m3S13_qE96!2HeJqgi!t(3QT}r5fw>%Ksm+@?Si0<=@;RmJ9@( z0K3}5#ZeKG3@Q$U(;}oaoCaDrEbrg?(unHpj=}Q1xb3A8h&qT@UT^ANq3U=&+`U4j z@p?n|8lJMnq3#v-hOY;^SDwG{<54Y8Fd*IzdVyd^h{v&0)DnbHGiB$AFY_8PVetZ7 zdxD$2NX{@^f<<-!L0G|8n5|2u=CIg_O3e|l-`6#tdh_n?mEOE9Ju9~veqfbOQ&rYD^fG{}q5Ybz6w!QC~Z;BZ-z>QFxmoa(}GrKcw-*(41;1&u20SI01;!^J|S5$ zb-}qwNm05FW}yQZBAZrYs)muh{{?Pi=K%i1qTq(%h^dha>x2Bv2&8fuSR?PJX6OyR zR-+n739IQ}0>0I4{{SlTj-3FuRf1^+LJU9_83qtjae!spQ_%o6Q4Sam;$by22RaIh zyd3@%$SL;@*v!py?Vmwe%?7n60VO!!Vs%CPGpgwGp0Uqy2>1+1Lw)t|Q^H?Z#rVa; zh`uWaK=3tidCA#RTo_e?)ENb1P@pIB)L>0y$>^?`dphDB5Cl0lG-=L-V){~{e;o$- z=^ByL+@kRssZ(-Dd+6a?z~^=t)YHeQjSj{klH&t|xi{eIy3jAR%Z{~A&=pGMZ6%AMH+e1H!b)oT!xy!d0V3|vaE4hf(8a4#A4 zjgrF}PKB_%PWKm+CQ&R&(#x$s708k#C42p;Se7KQ5m@<{r~KO+-tD${5NE~jO&==* zT?5igSNYNRXDd8F{oR$qW8QX4)QZ`uj~JJd0ZH(xl@yWGcfaJ?(ad$DStCe7J6<`O zHJl`2B}J?KD-op_baIYJm*$Xi=^2-@f8D5;KBkD5l$wuVl$4vfPDUUiW#&9r+C&ag z=_ey+LOFM}PnsN;Yi7ESlak|xr%T$XZAFjl88Ss8WJdEm(?&01VD_#XJfJUjUe_t} z_?Y6U)4(K3^@U|Z(kq0k2g`U0dIi@EYR~O@f?h!(7djM7962ydQPwN)kZ3=Jq+eOD zklf?4J^{7CSf3#9kqHBa@8D-anvS~wlUM=Bf>=(o=n}$c4LPxgaDJjos1hNW zbV6As0K5tz!`Lq%y!lufc6+k6DpD!xl2i;=DIohN-gA4=uCxb#9iejEcy{ldJgH0g z(l?6w7otM_yN5QW{w4GM_svE1rzyRxTaQy<0j?IQe6=hjf(=a`O?tdUmKfSE~BvJJ@-?G9_0MSpM)_2 ztB6OUGD<$1dU6C)$$?}lOzv>iNn|@G6OKD5Llqq1)uc8hI}T967WJg^iRwqgghkPC zP7gT)@d=`WsdRWe`apV>E>%vSR=ZA}l?GRLJ#x6gQ+u=9`W|5`QZfP4J=$T9(2-I7 z_URrqp`N1Q9l>lPYk)Jf37CV*h#N`K?CfR6WpdM8Z+z3-jBfa)QB{QjzBE+p# z;U1kjtGo7i?x3#aP!yok;VBzl`m;T$nq8=T`O{!d3*A zN*iHqG!`0%mL3)LZqnfhfpFyW-{S;!@Sc+C(iqC8Bh)5Ima{jq0^xtNgNf+PS3k)cWxA6{bSW#B))!##u1( z8g4SVca$QT4`iXCl+IA5YHb#Xs5q`U8X~EPBqa5D>Ya7xxtrbb1vxyZVO~=XUe`-GZukc`(RYbjfb7 z>m4jLUCa(Z0W_ko?l|_)NKiM`&5(iBmy(E^#z?8oS>rK;V2N47K}|uG;w8n;RH6uB z?7~J5^*60|DO)@WXRIYcfDdf)?F>CPDk3MDQ>$Vm5_X-BHojChg8zqy2@epbb{9vO z$;4D8IB`wcn$XHv6J2Q*JO}ZDpnxbeSup*Q$pRr`0Jh{-(QNCC`$7Sfl%8;TnMNLc|z-Ym-t$w0*=jVMA0Vu3rldz)RXyNf^_RPVR@!t}N}mh9Le?5-aj769FkO-mt8rzm>}F-H z*w!d(MJ}$aHT0fC0DTrrAvZ?xg+qLNf-QZ5s~%`Muv+6o=NzWO)kAGia^0M&LXyUQhRO5}pu|8vmY7 zX!C8{-qdgS_w;RHeoOG~Az@*|ZX+w~H#PG>_Ey`}nQaj;cHTxYc#XJ+xfOt?7Z1&V z17i1YSFJ&!dN_Izo0_u%X_#z8n?5myT$jD0GaM;83c7!G0r+ap@D~`!h#A5{HkTqw zw#cM!Ob`U{93ZYjYlg-EKO+j$H}w@{+iX2&bd}6%C_pZx_)94I@eYI3pwafWt1+NY z7PU+PJz>t9d8Vaq-!=ZkmTM1>-vUc)Z|2qG&o*h9@Kr&c90AS~or+TIdQrQKKLfh#fe(3+goN~ndM#GMjGD4Jh->^Y}k`wd~EKd=luwv6(uJ!T<3v_>m89c-r4M{^V)De*AX&v`-(c z)2AKsR1)h{Py5??VHk}h4^O*NG@wn~8*ja&HP%ae%dNkOPv#Ka{c3gFt(UA+KHPHa zC7L)1T_F|(WeIUWIv9Ag<+>VTU`r{5T&@@`j6=(Y*qlucgY*gfw7FE~Jzbf1S>{8j%)2O)0|m}xn2U?kxXe4cGHab%Ea`4x*n_8Gxmyg{ zyN#8ge`~`Oc~1>LvhhsB&SL)jVudHb_w)FHG=PW z^4&E4Boa6{emji>P9uSbfdsDp<_N%oPit1Lg+l6dB*%vp$q{)k5?;&!4Bj~Wg|85@ z@v{@lK1Ap$#*l6xZVtT^VwwsugL>Sr!lbWCWw>N5OpOpWcLsD{$)s<+Jz6&D14Dle zO!{)C&dQzYdK989ySaf$-})Y9lRkU2kx8HCkf>_0POu5IZ1vaMq_0N)M0?q!Z@mrT z$xQmXTT6}jJEaa%wJ>BNl3v7lsfR~#S;UbrVpj0b#hxwNLlZ+Gz>mt^PB3_O(cq#O z*U%BHZTcFzs;Ua(Q%o$aLIsjt{4fg)bw~))G_XQdTxW}|CYfnHB|>@W;}+2*G(`y) z%GJ)Q57A~$8b|adh;dFr92W7RX`ia2;MmqM8ypQ)z@}VAsR(m`Q|edgk^9E|8wjRyhMFL(IhpY;l|)grPBy3T`2SgG6cdd+ec|5 zGKfmQE*^t%2|wE~E~(Ec z&cwI`kF~8U%fxWn)Re?PU?MmlO=%D(<=87r(Fq=VBP_+5B?3C68_bFCVGt+Ok&7A< zFHQ$>iViKR!t6RkyrxYYbM6}_>_3(t+#8&aPLA77paKFkm{t+R|3XGtv9-Og+$W6l zG%?ZyuWBE`%WAPH?mTZFwYM3D_Kwz8{EDqJ){UNVW2s?cgQW^hqQ19!EdnB@;-MpV z5E`R>D?i~Dho#JNL{eg=De$m# zNtkH)5vj8GqyU7RVz39(tFlrJrei=JEm#@efCZl$YS+X;}}g zk{&7bK2-9Hg^#wP%ES-)PCEBnn~ZI#sn2iA6{zgA%B*delVTR9%_%?6BiH?ymfl&% z?D2*TsMDD2D>JMyEdd*rEP%~ImqEC3vHQUFV6w8Q^z@uf%^3DFtbK&@U=&U%4QC_G z!T)}k{*RcPz+Hq3>^Eodlc|uJ|7gKa2$)Uq6E=?qJz4>>|J_Ov8FStcr=xuNf?vnm zlU)?Cy-z*J$-WIjq7Ql<;swg!z z(5`Z}i=A}v;8o8I@Qk{L!@CA3ejz^U32y*cNNY;sI{n-eY87AEDhgoEX8X-AeV}O1 z9Nv`gSr6rv7$;(Iv?~yfDTj3grZ$hY^}3^TEQ28 z`N={+10kR{EdK6GPgaL$cm!Yns(S@M@%riRmFG{TXHG_}$|@bmkSD{4!tfXN3uD)F znJDZ7OQF;-1rgJZG1G4xu>eI{Nv(Iu5VJAqUy2|MttO-|mKf5rc{`shPLK0^ce zP&Eck94;9wPy|=Vu-(8$0W3Qo5|)y#Eoxp1SfJCLQ$~chSV~U85>FBk=s4fba;eP@hwtH^Q_8JOhd< zA8)WaVsdB)>(;OLtPoHaDj+72AYehfiJ~$7JCh%#*-&>g=arAto|?7DS^Lff2h6z$jjdW&M(-J;jfXcozx{ru%Hc^}gVcV?eW&pi{$CGftc)Nj=@GCbs0v zEHFv@H}}kwe5?RJk>lMzc#^LIrzrXmU&)e-LRusrkEsr^>F7Ow2ns~c67+g}f?g#& zT-iQ89kGC7%@h=yir4e_^BFJ$Np`}Icc>*>xGl~p-u$H}yH#Y%gj26iuu09juRq_t zCK&h1?lr-WZ&SevVw)S;s2x7H|Wqs z?p(@j6i9wnscy+NlVZ>LL?16Gp1=24U^G~wI|W9z8T?9JF|&H-9fO&l z*#RP7ndjdEIc9C8sjej?A<(wx?;1}p^kuF1zR~P9vo_@m&1J&ewv`1E0R3EE_NFUe z$P;UnV4^S*R2)*F7`42 zGyuJ58&0C&E#Uo(UT84if4(8U{_wX7zV2-I178O_3%(w{6f)s7p6;R>cX)c^<-X@z zd%53j$g|UA+4x4a0MLU^o@T=lzI~lzkWR7T0M|_Q1RD+=urV7h8GY(3_$%h6%sZM< z$Lnxq{A5y@fPxZkUq@|&**6MyFy|$CmRWqbnh{7RWt1S}^_r1LuWbU-IoQh-(IC=c zPg452Ua0hx?z$+zZ^Dsy5IP8F_!C}Mc1czNKJ*~+fK@ZkzNES~L^x*Uf{su%5{)U|3 zpV{aHKfE~6rzNGz84#}^g4r-k^fzRIZ#?+||3RIus61XC_Wp8l!f-Q<=)}V${vF?hLNh<+l!7nj?}mgIb=?m--USJSzdr(iqRno z+l@@gpeOKg2A(wO2}sA{Vp4hKu9S=~NLR1!(BdggGeV19Ar!$8F7|GaftCCqH7od0 z+;)DcxNJe`1q@+kuR~o@w;xO$EwdhTz0?a~XFu`0__2)j*9~&xdlB2D0{4h}?H*1l_mttmau2KH4oB?D1Vh)IM>RYn0YhB$ zZXSov(}Oox?_*T@g2}ZGEkSv&S1!PcDn;IvHOr)=C2{G-s%y&ctQ+Ru03ZZRR0M+% zMSQxrcP;LCH;PAkiXJ0(YH?EsKbxq<2Etk1LP9Z`b-yCwK#aoriTjXI?ciriR~yB> z`^1ZBkz_Rdz=y;3;tk_6mTn{OJMlsoUy)5(osY~%W9oQ{xgiy+^VRBF?*gIXgXlcb zOUK`9tG&b5FZlIPmF|XPV#t(IK;dZOm`JK+Si_gajRF-L#|RM-cVs@)n5S{_KNXz( z>XU=)2b5(+q$y=t9Bd8>g7?&Ed1ho4p9%?O}ODyKtT58E=7y?fa9LR=-$ z<4db!PkJm?lb#h|1q4EE7x2YajE@twrGPwa&qz&LdNZclFC{PuZd|rxT?i7@U#ZjX z-%5YLAzN&Vwu!KU_5h9bq0K3QSYIYJ`FrCF;a$>qtxx>h5;;^ALdYbS^0A*(u;d*p(nPW*!0J8OI_n1*As=h@!nX zsnd6xpRzkv#JIeP;A_@Fvke0!0Mw(h;jdQ~w%e*x776%X0X&=Df%d^=@6aYqeIeiH zkPz|{7l$xpn2jyosvK>DDf#5hpCoS$W0?8&J++;gKc~FCHyU`c2po&O&iw{IHG{ay zHlQ*+)o_N<^}Jd{Wn3^Y>X@kp>11>}#tvr3&>{sHCf-X^?lU$D!1QTC)Qi)#mCQEE zAvmT?N2Fk4UUvwkzy%`_El#6yOdLBblJsGfG7o8V>h)bzSk;B`b1imf6vE2WeWQCjW3%~J!M=-3#GVr? zUrd#A_a|YnpN3#JR~>h_8OoTF34ytgt_K3|_A!^o3xPgp{xScVvVNm!UaxvVyr5Ec z%qS;aMkL&wF%P{UA>{SxYVh9Gj$gOAIMv13Xt`v`7jHoLnj*^jUsJn=Lj9##+RC zq*IrAn^4MC(LNm>w_edK@8h=P8q9Eo?ee1@h}(v`cPZC#tBi7~-Q~cL`b()V(HKT$ zVxXI@ni_kYCOd?3fSH4KOE}2x%#B~@OLtFBbYx-tgG3=y%rfY;g7#8;@SW@L-Wol{?EXqofSyDA%S`? zO6h3wpr#}AS=?EiI7Pm(uA17Vvma)c5&^O3V|&u^`?|S~%qt4~Jm&lWI+%-mX3mdQ`=4Zdz!NlExdiYxPpnXDziBHNo4%3YXGt~Y4m+P7g4!pERT z;#MwnpVJ{VyEJn|NKGQk*{OCW!HjPQA=KAmw5Y2)hCK+Usp^(lut+0M?k=erc?>eJcwJM%IW6q6%= zqL4kVKHbU53|8Oe;k@^>nkB{o5jyRNkBcK{2h1Vo5|xVDb)G}E!ggg`C6>HbbI|hD zr_mRYv2Z4LMS!zQBzYc(Y*x;DC#JNlU36uYBz6HL2L}#qVrmt;^t{6q6_~+X{C~W2 zOs1Jgo@^*bg{k-+W_#9>EICBk@SS2FUWczGbeiqFc3H|MKTd-V1^{et7X%M+(Hc=_ z6L{#gj+47~JP>2h5_w!)xdI56B)FIzUvA3%Ydsfwkw7_ zxi%F;Fny9#qvXMVw#8E^2#%n^bJB|x1|$@0dwO^Jf=4Gii4Y?}gh0g(e<1({Uwgyb zxxdkLiRE%XQqJ+K2!CvyKStcA5B&6j|JmNx7s7M3bb$4nClLPTy6e~Ct#p!7_w zAAhogWE-Dk4AZ7gGH;&bWR}+*vrzoPU7r}Ox11caa)OiCkRe-wk`&9;zz&oRcflw~ zA(lL2{8qLIM39QG&oS1Uo*<}a1#RmovOB}Rdwi&+rN_vyiiWK#TNHv# zA?UP~L~byq6DV@uuiimD=YpPtI7kqoN$4~6xe2Uq-+@|*24^IsN?Vu;(yxLL5^x!j zup#|iKSB|{Jnr}2GR+Q5HE)4)73LwZ{=?hph)+zfcwCN2R4y|W74n{%cP61B)>GvZ zyq--b;e2}SmcaR^5B~JQf9Ka3)3c}J;HOQWAE)>A*Ppce{nJ(=fS2|DvtI)%k;GW_ zPHH1^68{Sgi@(O!A#WQ;G8d*NMaAy*w@2;*mPH_QZjS^~`ixb+Pze~fK9ITUgx>l< z=IN@)|Mf>d^Vc80{}0}Ce-Tm3;a%1TGB040s^O2JgrIVziZDBhipTdt?ovo?0Q1O_ zZdnMB1@DOn`TwMQAwOQ1{p{^J))-g8qt9XJ4x|rO4O~NF-G1zsKK3vFbx89=(=OM( zf}hQ!TGaP^-uk@mZdbJb$|J!Uh?R`!=gQ$xC6&G9d6M%mpGgEcycsNlYpO(eNh^@? zUVqC^cuy!29=;c2EfusJ8`8VGZz;zjCb0_W&gw*WoxBFb6iH)lSer}(5^g~tjGVn6 zijGvs1{1c-I5c!-V9TwJE+a??3`D^b1W9LXAGE04R2qTtI%Q*Q$%lj{6W$bMrwJX8 zGwTZ=E|`#V_={2;__eMB4^G8cZ=_b5YkZP?L|@2^XcI1libyxQ-gD|=F#3{YKwk$z zm^FtG49C%LfDjBz5rtr{(P=>;7!Pv_N)1KNN;GhUiU8AaI|=fsiM(DzXv{3~Qo=)h z&`+;XqBb{{fOQwhOiRGJG`Gh(30QnU{xU5A>(Wk?fXxja1cqbX5dv;F+VC|W*;D1QTYi}w^67pUWjaw0-aSertX0JFdi^hdRF=d>< zY0)?xq^ALV+K<0MQhHIGa)AoY?%oiOeY!WK>cr<&JBTE35-_KI*P){YPb~L(@j3pA z9#e;mxN8fKar}WKURZ8q$7$?@-Zdy)V94N)?tMcm(L`{a{(AY{x)^AkEZKo zKC1O}m;t2aj}dFY!l)w-h0}mR4L@8wOx?hb>vK6su5+QIzk6-jaY?^7UJ*^|2_C| zVeK#V|NZw%HJGzmxRv>$2E^uSoS1*6K6(CFGn+H!P(RtZkb_e-j5RBI%2j8RFsdfZ zaoQJAHCk~i!g4TI&3JFASei7!Of2l|YIRdK%sPTe7Bk-7l#H^{G$XsW#-3B{+@(Ou zbetDwnE640e8;1T!SE;1BDW%+W9cE;X8lQB&f@)OInA6HFovxpTyOB9(a;m z`)i{oWLIxP0JslWndx&lw)kAt9^GrUM{}X!r9a^mps@w8Gf_Ump78Hx*4W!Srd&{` z9O)mM9O-dGRB-XwbOr{P^0iwpPf`r@x0hcC24lY|B|&y`oL@YP3>0TE&P~V*SnUbw zb55us+|3^w=10F>m-CaTG2?k+gJCfflNkV$cdSvF(zsR~5%f9bOG{1%b}x)MDU$-! zFnA_^UKMHV<^_J3PBx^L66{LtUYykgY~nt3p`-0B_>k;D0MX6Dz6Lvx1?^t=lkHyc zP6w)CA#X?Eo+LMY1JtSZ=1T0M#% z{L{5s;m@P?4!;Q8Qvtu=k1Q7x2q{D(YH-xUf8zB?6T13Ei`;ux9FVC-qu5>P;G7 znUt)?9#;i&Mds)}JlBJOVYX`<6*q*v@Ton|yPF`I7+H-oJUf#UkVa$uZF$+K-bo0e zqGXsM4%{>euj#Dho@id~Q_>^%RlSSjuULxtqo1-OoDD0w#WgXCaSjUzw6BaS0KK-C!e zfuB7mkNgmHr{oNV@{%(V`2pg_$Pb*+Bn>%fOPt{7@wq!|3Vy$PbnK zWu-(N%@JQ=K|P*g+Z6dh6L4xX@`J;P7)&u3BR|9#Xv`6jYhC0Ax$kl0hl<7Km>(bc zfdRThs_DoN_SCGW68V8PCXpXvAi0s!YBz}dP|5G%K>YXl=yrx&2@*FDV$&Nhaq@{g zFVn`0A8v_UqH3&f_7IcSh<*A8dJLKuCcv|FXl@jdUU|at#b6oTvl7lQCit6x1ar?VOT31=)v!?BRjS zh98bQz_v=xRv^Hjd)hN3hGt|M@5Y!*s_&i%JP?mVQZUcp(mX4+QYs(*uF=PM$9i+` z@IouPmXJo(p7sk7pJt}Lo4%WagkDQb=A*r{+N&M+4Ct)m3uoFdfCOgR zca)*PVdOLIXCvi}Qz`dF$}LkV$0Fs(RLZ@PLW_c7sT`}izP54`FB-Rqj{^wy{OTT{ zlg~$azLrU{d)XdXybNh9_HM7(A6llETPAPwrh+lwy>85o>+bQOuaasx0Ajpq?|-}; z`lZD{ciVZ@VF+~S=0AvKPP65-{>}m|-%xv~Ka#C8bHonU%l`swC+l%6Z{_cRjo1os z3Yg4VRUWY9B20cmhsX1t;n5}b0A3xCK7}WU-4?vdQRgK=MXWSuq6*eC>&`+N2%-&X z;5`|8A*_)o_2lydxI`2P#Z0Mi7IB^{eM^*pvX78}%787>Zz-e^MnrZwjO(H7@Qvb< zKFCdo!{~oW|H&I8AX<<6a{UM-7AlAXmUSeZlUBDT8zxQa{1`7AltN=cDlJ1Vp?CXD z;Cl!MG%uRL<$1sGr0~o$xcU{i!Ce*l347^^C;*U#39Yb{ldU#%dL~9 z+Rq6pzK44bw-f{H+Q$}M2@#9jh7WYG3B(gJ;Q-vns8sd`iort{lg&tL$&FzCJEoPS zXnk4~Ty?8J@4Ho?)ULhqza@XVRW`{^@Mk)SHo`F0Ic1pUqKy!HSS{hfKT47Nqug$Z0x7 z5oRqmoyNsMvrP3U_vb6Gm?vS~`8WtH2(j5`Gf6dvkzK6guX`vbsf2zbPFo z=L^mtRus(oOE3IBH?a{>%C&b?-_P-h1uSm`CiD*AS#aB*BrwaH&n$`wRgRTa_xQp(s`ilqzsX_n)xdckjy>nO-*$r_9#?Q zu5G(=?IXprV0m;!oL?j28{?(LwI?1LEv!*saA*bIguMOs)3+==uO0qpOn$Mezjbf> zL>8uf@fE}bY7Zh@hNfD30EvhHE3N^O%{7CUG}SY=`M5?e%Xpk$tDa?2xz@XKZTqwE zL*Po+2GL2mmdZZ(n14H(+2-Z0z4ptSp1xEw4)Pr8K7ZcYxvW)eEDviN-_#%IXtL9p)jItgst}{356_Iqr57lJ~SO%SCauPACY&kZI}^x zGp3y%$er}QDA&d}-l}$Xe<+TsU>X~K0BGnL-HU){oG4s%*n4Osz(&>1VW>rx_~!SG z{!y;iUcvMvaI*%$mQ-M`_t>gJVa8^ss!69Y`HlXL;BRj48CR9&nLxX=TD_}QtHQE# zX0YTU&(Vm@>EwE=V3e?r?aZVZ1-l0=m9d+sM*^s? z1m_#Q+V@o(ul9W)ppE}M+|~Z$>iE?@Rvo|Ee-zh7cB>sFPP*EMZ$z*htNpI-X}sF^ zBT+o{)rMUAimScxoUq!df>T-T`oK4QwQ;oG@N6GuD{uVo;jZ>8PdM8*b zvmGVA>eW8-Rh{j_r*O76o)cC(Rd6b+4Ffp%;wdT!fzrpR2~5%)AcKJA124VLXALgp!s)6mn8s41eyPV-p)s`n1;r&XN2FYwgy`gdlHw|u zzeItVkeK8>GRcX#*q5^Gg3ZXe8tD=%SE>ge_LB&cln!AI$mGJ`6Sp*)s*}e}g;*FG ztc1FO+u=#p$R8SRB9w0xg6pPED1UVbas2e-c=*jKZcr%bC^Va9>?re=M+2T z-_I^qnZyN%E$MPLRSRO#<%W)AX8<2jbGytz(y=8})v_HVcA@K-gR!Yj*QL-gqmM{i zY<>kd8=w}jts{#Y);yJSs8oNCEwN!|v zLh#HMoC$^C?|&=FopS_3*EOF6cCrx+Ps^KG^5nYI$(#QPEG)4KPcQ5V3wuXRoFq~~ z6P$ZsIJk~^LM#J>^JVxu(nJM{mey#)2q5^&iRtoAvW1%O!SX41tSdJo7r7CAY9<1I z3L?Qo4RR01aV7bnY{ajmoAQ)Mne^Z!way|nQ1qYsc#<3ugcVFguDNZ5u0>pUVs0r1 zL4UNoZ6tcnQl)^JqW11Rtab(XCFT{>H6CC^xyZDYx>lW#sY(#GO;;j})Jd|j&~(1t z>+|KtP|eW&z5!)Z6BpA2LdErUD(jk52D@7=r-(=slPUEL;KUsgv`iUmfJhYXn=U~4 zU=aZ26Qs^qH&clBCjUVlU9EBwiVEU|@DPwd{gr_T0JdI(D!CveybI?U$b`zxWK-b( zLDGO&7W;?c%A!AuIMVYo2hpHW1TiA{k;YnRdJ%^|6@pYy=37kOq}5UC(-xx5?LJd( z6eJAyR4Bw;88!<$sd6*a`a+`JBx~jxJyBU}v|Vh)NvF6QhK?v~=3WshYc@q7mTFHk zkffSt0Ljd08QVUHKmIL7rzDNIy`Wi=rY@T0q|Zr$z+^*1AP+|X0aBT3l^=5LVEaJS zG$~Q)fW+*J4kB7vFgtah#ZOFV{47!mZOaF0gFW-CG1cJCuTgl>ca~U7V3C2r=L+Kb znze5R3?4~tERf-SYDcn!z)I#yOAIdINg6$7jtQg@8b$v8 z^$dix>DJVZ(SUeWD7YZ5*>r~>Let(q_28i&i~Bnm2Ky-yTX8c`jv)%*4NICSWsiHZ zmuT3jP;@m@9&UwtC=z_vUi^Cw=NhrEVB@~@>mtt{DETy(t=Pnc-bn@|`=twdYUBq0+ z7PIb3(H`3tMm$-^TT2c_=g3YLijEAEP;`~h+OqUO_(}sO4Mo?HMW4zX&2ZnS+yS2O zh%Z8rs}Bb91XH?2hEW(;14NLadCt23VoxKy1#BuGPRYI@(M_yf*kZakf-{ui-?LO= zg$IL)$(%Sj)I~jj)EsX?gZpv(LAfvJgu?Baa*px5Cmd#lz{p^MimCF#&Tk+03?`$U zIRNY1pa)ogo7?j2>}*k$^(+Dt5ldhgAh<@>9Fr~qCX56QIlL8fL+_FPsu~Q~3rhxd zAv4z>v~Pa(okjcI?dSLz-nThSa2}%263yIYiSKH!J(5dI#vgcYc$cme%qqXm)q3a2?X4 z5`Y_bRy&SF=Yr_KDi;Z&-PMA0dX==6qUU&R zAxaPg?M^7`fUP~RFCynzD^U1pG+ddKEjfvy2T=p(#e^`#1pB$Uh5iZ+DTYmF>QUex z4t3xj0r^EyZM2%IhP+~hp>k!stXkG`Os^zEDb-4#!O^>66KsskjMCS z?9MII6fh@)pA3MtJ|5dZ!FkLeSHxZB&2>4@c9rGDs=`T+p&}q$q4Q9W)gI-CP0Uh+ zHx-<*NA92D3!}vE&r$LO05_DC>t=B&R7GoU7?>tSf)Qj81nwJwe*Nh=w*YLcwMUq` zmrV(oq`DWrfClQba83f@J&(8w*`KZ5JfKTz_@pe+5ih%+hoOrRmMtxx>d*&ZHE_ZmB|u4 zGP#phE$$WzdoiA18WWb}Lkv1TKuG8xXG$+c{UyaWUFqz2Y+k#?CT6NSd~Bctdl)qZ z#RQU^_O$ELy1J-o_@-sX&XGwrh#IwxEq<_X6EVJI8eXzF=p4lmuv%g5p@)S zMg)0t9qdb5QC9$!S2DMZ8iPi~Oy+wzs+IAWn8-|V@e*1$;IXakeg4c2 z50QJdI$$6*AD%y&P4gqXFD6qJ@Q-<7frjr53Jaj(&WIJCghvf}7~82QM-Z*pZS%Ya z+)piFZ5kWK2BNxOhLDX~N{1UxO75=YT|^n9ab+7RjKOkU3>}7*6TF2}EB6pIDjR7s zMPqY7ywzyxp^e(gw8E@HXm#G2fiFk74~-145LUpBomX*tum7bL2IsK2MkQw~%y$Pl zfty51|Kn;jkJu04%2mi95U?~6=rOaX3r(2wVcG5eN5Ig7a8}NM*a$8o2_qn*(WP1S z5|DTk51d}CsG8*@hv3TJW_e$;Y1?SIMGnEA>=%MVD+m-lm3|>gAuuLPQivI94=Dsj z9VwnVgs;$PiSHZ>#)c5X<>><~hOgMn^huo0GFf5M3&57G<6<}9(C;PM2gNHDn8c|H0{1d+aebQa`Fhr?7JaCkq7fN{9MFWP6GUX zoK9s%b7QB<-VR35{g+m%A1f8UJ02JJJBx%ZG2!fq+o{_QvKJO=!E!bK*jV9FH4E-y zfkYE43XND21bQye0=saKqlJR((u5;kV?5zEpB-@UUP%*L_w zEUpxn#*oxS<@8D!m+=sTNK95*)CFFqmj=8z_ATip-;%K1XGM~&I*ZiMUogZLejt}v zX&EE}({1$$An&5~nn8@s(YlR?KR4&yGiepG%;>+5Bi|$EttYF9>6W|AyzCa%raCT! zfv^;L59#Y;7qF;-Ywa7PmHhzw*+r&me9nSivY)zxdiK_AfE%t*ceZUjJEnrns1E8jvC?}$0qRKQVAYiA3AJ@tmv{orm2T!SFmOy(vUY3Ag?#^YC zUWL^Iq}Qf_j7GYt-D>X<}>uX7OoNViE7wEu#CbC>@| zh;O_M_B$U3_O<^vwQ&5fP~a>_89YV#21?5YeDw2(y{vIjh@j>+Q;?UbmraZBO`lF5 zv5$_e+LP=!F>b>)b$=7S#J!bFFd|yC@yqw|_c2e|b|B0@U7vq;dE491X4Sp|GM5>r ziw=_gK`Y~`6p_b4uqTUjA;w*b>`<^d;5>ZC9gt$G#|$WlUj`8sxgUh;nf)P9@!SvXzid()FePU6sZgLXJST$v&b7QJ%fzxU zQ~*P8Mp`$TpUFrzy!9TYn2-arDUvc%@KhGi+d|CgPnT1 z#S1*wpDZj7Ev)}G`#-uxABE%1d=lh&^dOppz2y*5LmtHlJM{}oc*_8~&xUxOaj=?6 z%v=&I9{jIRQF2k33}{yA9E2j$wFD)hC`FuEwgBTnq%((Z-P@;po9*8I)VF^3_7}d@-P>QTd;8qFx6iM8`#axyUAZr>dwcQkPS<(e zy0=5?-hN`;+soFy{nWa*8`r(P%C~XruU_}|+I2Z!x9;2P*M0lNb>H5!?%P|}y}f#t z*C0`75_cJrI}Vuaf;^NdTamPFUMhl@^|}GMgrm+BPzPEa06s-c#98LP>iFAJQbQ%$S(;Pyp+z#*-aK{+cO zD$Z6XROfuPM}GSK960!=wVOCuTvqiY(52Q)=T#oYv()=sL|~B3@CZ~ZXyE5uv=IF4 z9v(vjLumy3hzCFtp=BnaZm-p90>bt-Hl#jlz=)_}RpYSjJkup@tpa~mfQt65H@sg) z43*g{qIT}cuo3QaqQcO7)rswgK3|!$RnD;tL!fz01Z61Z$9+&sHT5~X2BtoT2e~Y8 z#1qjj-^^k4R3e$Pt}dXKmV^vkpM+6A(0>q0(i?;WO3`0JiB>eAcKOHm5w%rNB}{3RFmAqMp7r$yj)NR1kvLQ2|`Hwg*;8e#P+>tVGaftH))weltF+TaV) zS|%Cu1j4S&GKesbLr7}vE3jo{ES-`+B9SfrC$2Dx&@EmbbbM{8gAiW07E%{;?@&*Y z@kbrW-gfrs?6@)rR_H@WFeDt_eo-q2?8H|-MV`eF8ZS1Ga2U{WYf&<-h%x8Q6m%Or z5GSMd^5OY2}4>`{nK2egz(wo21*a{6>?C;a?CL zks&!okv8&uVl{X$&?tCOhq{Eyv*|Sl>@wyCo~fDjwyfM|VafXP2vJ3Q zu8TF++V#~C@S47j1^y|S25pu9XSsAr2K+{HgD{j;P+F5d0662rSpt$s7SbIIP9g-% zm&#Lw)y{L{$vEc+ctS#ofaqGG4g8AT8CRV1-{3c_3*ggIV<{_tKL_8Gnm>2%@N(vp zA!kX*cv4_g5b<=AG9|19q`~v7T@4xt%UotEbc~R1Ckks_A1hK9LX+Im*iEDS-c6lS zD#uqpy?$v2r1hSxp9{&JGeB({!X92bR;A^zy9#^Tjl)Sknbr;7F7~|U->^t zjXgn&v~cs#M%$Lp*qlW_Q-YsC`}WrbkMRk9lKl?E%UC1}do9{O)2(7(u{HD`ObDNRZUrRM!6>`DnbrL;$0Pr~CxdF@0jq< zQi6(@_Rmb%mJ+J*L>#&`PlR*qEN*QvNTkBr82n+bPN4c41Q0QB^1#y z;zK4za1}KRlDfr3SH#}?{< zo7h4*kekR3TZr13WB{~SKgaC@?T;D5OC^ReTOI}L_G}qLD6^!y49%nF$0U@QF)TA< z7@^H{YJ=sXUaYBu^%7r3D!T1RP4SSrRLMW{LX9P=ozCs7cBO8HiYoX^drGdm5@Shk zACNSSX<#4nzwn?k52itOE+|%)BP@CnXO~KxI}U%pn~%wunu_*}lb_Tj!=J(HLiIXR zoS!t5W62%29kYXNxjTs#$if%cy2^||cU{410@7N%V zBbelDos`Xx1+O#o0cSBm=Y~_R;lOt9kxu4wb$iQC|K<*uQ`b~Q9Hw<)c_lzt2_d7A zK^ThC(1@=brU1h2?QqTdh{G7H%&`gjs6B8FYi5BoBnKd((ghnGV10-ET{IL7x6ut~ zYe^e+a$u$ClJ;F6JL_!*A#1@PMhjYxBK$rl2y12xqnL3g zEKX=g_%q*}!I({NHjYgQTUf?SBbAX~gu7IbGJ}MGfsJUz`K58!E8gJ7?JCY?+T!3i z2rq&Kjy5iO8D?fm+0AIKTfR)fp4<2^{aKcsS|kC zU_K6nJ4>#e|FfgOn}RleFs&yYpRllJx&zoW-_By{=Aq-@V6Ey-R`sTHj(|-&d)+HJ zKbWezw=h}#Hiby}8dq;DCihyH4A%baHm_L1PYq^~Nu7uz9-=ATdyt^48du^RU{vu$ zOd$EW5EICnfXP@%*7(b3uEe*^I%{JU|Nni;$v^LiB^_5yP+>=HZ{uz zmf)!3pILJ`!quQJ`Sh+snp%!K)XuMPQzGV=gfHM8_Av$2xJdn(A#ndmiOh+YT2X@{3!x5FEV)Ig)!o+vuS z3s4Gf#(Zf(c?^8ga=Re5IqSkslBJi@k;k6$u+K>BT)86)= zFZb~$#K~gA&V(vSk;Zs!45JhF8XPqkNL=JGZSpVb!rDa)fJLG!5fdK>t&>b|Wc|(T zGDG|g>u*9Sa?Bi?YbxE!*d!qtRG0|}@5O$gfT$kh?~3IXZpT4jBp=x_ye(+Tq+)>B z8jfb$pJDy32w_%wbdos9i6kM<;+P;~2-Mgceu|jlRJ$G@k(0JV?+E53L+?PC2^xC} z1JJEuuaK&fUJQ}A*lze%_BaVdCh7S4aO}{o;dj=M{79)pINCq;C1rlIAGL z8uigPs(}0Wp9?WCP@)a~uTAe1&S-;~fflfP5d8c)gHoj-?WXx-4wM33TGJW> zdbl`ZlD2V06Vfp@DoIF!N@Chzf-;})_xbJp?sF+DytVBVrRUxIxA*>CpWkzTo}a|I zyg`?yZP6bCJOfwJf-EWZVfZOK`H7(xcO=2I)j_+Ett9t%BqSYgr%5rQ1>~5#O&?C{V_7Sz6Pc>&qAmvm3O2?_0 z;}4mx@_NU<$$!xL5z(igGs2q>iBTX&0as#FuVKdthhQTw1kwS9!hJL*6e-6V3UM}n zEdpSSga-ybjWYELm_N0E{v8Got!Y(X+Q+gFNuCIk5!JxGHWMs)1t|yG77jeu6eezO z*pTDz3*T@3IngiesGwS0;-;^Xg{hNT;JTbfNjBMvyOKyXvOR<_WIkQ-7TTlJP4K*J z5FS}4DDz@v%!W1(DIaWPvA>lL0+VV)!F;jU&UV2F#JfkCaEa^}eu1S>&_+DLhnw6T z;1)WoekxLpbYerv><}}d4u1C6{Y&6tlwo>l-g|C7`EL5$uz9Kz;d7|5zn>?t172Y< zwu+dsa0ng{5TIR;&%5fTIK(0c+lkg<l6L+!ZrXMsC$ZoVW5FB%?mB2XO7OisEsTji=1FGL2pF#p?yR~}f&QsDU zaB$nKF2F2g9<-QLS>GGA!1_=NEsKRwUntgeDKXWz~SMI?bv0$8XI<2)9r6)lW2WR1HK%$O5MfYR4uNm_R?X-eYalj%eS zC8z7L7eN{h%MjWVX`lQT=qxm4b~uU_w(9N7w!?;S3SChsbzN4!UbtP|FbGji{i+dp zj^#LnOUZULh7YcibpT$qX>0oRb5_XsOsEt!HeQfLA&&TiFdp816SkE@YVq! zzix(wKjjEQgI4(Q0Th9adS_V!ruXkJ2N7sQR7j-+8a4@Bon8CLdSK& z=yP#npcEF;kkmIvEH#WMl(#fH0~Lt2HJA^c9oK#q)6BpSOv-G4*M-F4Kwm2r7qg{X zeq)j;;-QNg=wK^?ogHAK$~5aw=g5_&6Lcymp1Z)HQrf9A(EXL||9iV0)s7-}O|=*%fPZ5H%aiFlqL z3E@WKT#Vv)F+tVj?m4(+%C-vK>SDWSxzI28*zCDfTx5lw?OcJL#PIf9ufYcAdifwK z1*BEdo8f)%JV6-7ZoU-DBD1$dN7JEFswXJ{;oQU1 zO9z>*%)`4TeW1_*4=Kf{C%tF%T5UtR#CNpKYirW(jAM-Noz{}68RpZ{2;!j$K@9U51@C^&~I1b7y)Fint`P@-fSSdp&E>+cE2#?SAE1rq>DJeMI)uCGT7-b16D?oAG1gA@bNc$2*hL%>p35>O(vtB(ms(5X97Qd)hD8tqR!**(fd^zb5fEe! zx9+}>J-Nums|ZWNQ|JOCk(RPifH=v72C5Rx4Hh3>g784sVZ0*oqWDA4hpKnbJD_Mq zs8V33kaxZ9wzjATd6+eoHhYQLYo?)LPgYHWrvKZY1QcD~Btv(7}{85agA#SdvU3OI&;;@f4JPRVxPQP1fxzNr3i|xJ5SDJ(y7(+x{FY zg4od%iIp!oWVwBW=4vTDf~nVtwT-oxn|`Q>ZLGcA6sc<{taeezsPutY_2#L|_(Hg* zDO2<(U!3a};xljOomr;e1%8#7mwxMOcTf^!bQH%+qnb1toio5-ZXo*w+B~jTjTJhh zfod$UMp8m~a!AQLe$VX8=477c=wk-2c$U!I<|Ybyc;_FUerP(}d+5VQ?%jM@m0fxh zsNx|pyT=~GQt|*La9J0ho*h2&>}N8VTqkk~U-Q-lz{Bh{ka735pT}1V zR^kEPig#8$y(}u(=qXFgA`fpO1eG=%@f$k8f~J>>5&)nV?rT{;{N95Xa0VZ}R!>nF z&$>`x^$|Em7gjw`J=F2o9e;TGbH#=)fm)^Al4{6jG0}mt{(_9=dMKHqP@ugqVQ0ko zhNBaR=JlMum0ZE~u8RvKIZtQGAo)=Z%O{>w3@DcYF>(N40^`{uI9(Nt(@>O(m( zEybQXt6w9%-;rm*zsW7?3b+*gmKQG;P<=YNwi?9nHQ@qW>1q!w}@|Vw%7J&2Rxd58jem~LHr@Ot?;h%6ig{Gyts#S?H zS3KpsZB_m<)_43@W$w^`(D*hqW&N*EbGEpUl(=?)6czwXdTgu(>b>R7cV@LV7@0?| z=X&z>IdC#^ME9i+=%r0-Uvh@%SxtBArG>RG;n~=&>Hbbl_qNU9)6{+Gi+Txzsroc~ z3Ff!^(tqEuCbVGPmtOf6YC2_YO_0#MzD|!6=F4!UgYL}9jh(M%a{|-ded%_+gmG~7 z#7Pg`sp->tiNl9iza%WudFjww)suMXVLtE@@Cukt)K3XN7CT;#5Y5sQB>eV37FO^$ zuMMnKft_5|`l9}Q_S;a-AS00!5&3NLTCw2(^kh0G*o5vBL5?Wff<4B5ARHbP=ZlCp z813T@g=wLmbNSl2RSR=NvN6K6r;LPait*Ma`iM{)+DAOF7Qm~LadFowt8v#E#GS&@OsIS6 z6H>AG3yeuiz#ybSX_bu{QMJ&iX9)`vj~@xq5sr6oxA4q-$`7WEC4M+Lyy6Gf5F**M z=I6WIFkRZi?x%u`a_Oige!sqj`X_Hio_ z0J1(}ZBjY`pYJ1;96lRNNhp?htYd$yIoGF0)i>#w-I>0Lq-P=#uwrgO8POw~fjt_m zZ+Za6IO#@2$d?wF_r3*Ad;~VqH=(OMs=kSpP-X$vMT#bTb%bqg^7P#TcXFq&E8;b) z6iwZLQq0Bm(F00R3fhktH-cP}v(V%ybP-(jmaCsJxDh`hm8hPvqdSko zu-<(fhV}GySkt?YcXYpAURSx@N#Q+PvwW%uqP?~s>``~5eV5*)x5>4<>TUP?Q|bLs zNV8IM&8d)Yv;z<(#Af&|9fOT7htVSExE1XdKZXBL!U_s;x&V);3fAScc&Z~2s89q3 z`TQHU65zEFb;OLG(d`?R3BXwBnW7rD68uM^xFB^(OW$kM91W$DO@qTk_d(){_P@MX z6=x5cL_ec^TW81kaAgLUsjHDs*CUSU6YO~oSo4OVauPF0hAmiTRK+E8G2(w5zRUys z-LG#}FE_2Zzw27F*I!er)^?u-mSrNitiopIzPgt5UNWOm z)N~YR!n(N0Ejp6vs$wbYL>wCwQ^&{qs3=Fh^+UODh-U&1zRNNoQv65Sgn!IHQ zXyp7GAYMpS4n|U84q8TSC$W51$h7p;X~V1jU=x;t35BtC`DpUz(vxFEr8?n9-$w~A zNRX{9%E|_geGv(^??0>RbY;CMj^V0vuC0@f;4u*Q8`LTN`>f7F2wS|N&Zwc6&GpMb zIks1;_r^ltKn=fE!uo&M^ly5k5 zdyjb^R(S|#)NIz5oq?4b7DL+<7lr|*2_wp3phspy>G0?@za~vnPUNUB^sa}}Rz+x` za$WAP@@@8P1Vr%pzyN}@#iy2D;3k|%R`wOCxbPP#_LO^QIC1DNRRYU`O{}f}lndHD z%{^OZLEz|n=9q5VlL$E%*vR8}39*+ia9>4DU+6w^j7YT*edL&tYGHr1X`5UbVVTvk z_;vJZmiHKyNR*ntfb4*Ah4hMkNISz1=*N7Fh!LUu7M}L! z;mS6xjbJ}&JbaD1ZoiCR(2PU*k~kK;(IP(B!B!CkjC}VY|g%;}}CFA&4Bj;XhKkvfKAT{|ge4n-^n>S973N;yXCa~-% z)q;AoSdxli2av&-g95A7>&_UoT-?ib^IZ$=3K3UCl3lS6ueNnuO9(VKiUM$~jdjB{)(#u)x5z;&TL z+dN=8J(NoQf*qjR7!s+2jouMAc!gzWqSZZoic!6FIv|-ob)cBs9UVxYXh%7#s#*tC zrM9-9xKId6&!%`asDHp0^M`HIT!c^}@qb0m0;(JrQT_PPY)JT0?tKFwFvTtRl+Ff8 z3?HnGZQx>KmU4z2W^~y*T$Qa})Hw;}@!P4hv7n4%(xhH$jM!>wYtvLX6RhdJrWwJ) z`0OZsjvlilL`}(SV)c9gj^D<7MBP657aZB`I+(2u?}G3Hu?QthJ}cGRHu-{~Ki1C1 zxHwl*T{BmTg@bq)4AWApt-ysRW4#2Dz8#muufkF7H9yyz@pEZG?Ng+81OfI~1A<^v zRlAyHMa{=A7ynjUV5C&8V6~=kqOEAjZ!FD~vM64KqdiV~r{}LLk(L)OSi&B)48q!D8@7PULZp=%mNZ~Lm7R$zgj?#R%bs&=bG%$MM_qT zWx%@@;oaS2A(iAGiEv2Ww(MUUKFo5wN3FqVart5F1!zwMKPb+OL;Ig)eO`KeeZH^v z&++w1Ia-e6H}O&5H~8&HSa)qtk)9N4Z|4x`pCm(3e{)7Y$_Z|Cu>Tg=S^1gI{^`)1 z3R5EgVQ)ml+bh|1^Z$loY0f`JSp) zf3KuMc(ue~IH{36z7y^q`q zolEn90(^4UDI_t!TE59@)=tXJ1#}X_8`ETxX@)88t4NT~bIg#aXT*j`2JN-ZXR|ik z3%N$Cr@Ru~hBVO;(dY5|%Az<}k{65_wLc&Y;mQdY4GC3NwVY4IUAM>XU!LEe<(9t*}U4)DQmp$0Ji9e2icP z0f7mF>wyQFukwcf;@x?<8KkowI7L)vG++U=PUpwbVMs^sn|z<7K>3sCs5qj*Sy@gK zo_}<#y=riRm8*X=9fMT=sDNS@jmiCJ>JwQd^YF=-PlWLDeRm+J_3wBdBd5=@>SKve zD<#kDu~ii~Sc~`e;EFW3N7fB4J-Bc7Kq64%_FS{caZoA01v@?IlY}+pQnE(Q zbrBMxc5%`X0_MJg*%uFubSC9ndYA&FTIFZ%-p@V;?Cd(CV%7cK8CN?h0g0ij^+-pI zPs40`OfS+fqe%NS)SRzh71PFQ7?fCy$ye<07BYxIEHpW2rdb)Fg}p5eo&ZUD2O(K- z5P}Y8t5(#{*{WLscg!k!wh$iQ`c~>JxqaiupL>Gk0>sMZ>Lj|xUk?of$Z8i-1!`w4 zo3zFrn7xJ4z@pfLv&T?VEN7uz)1jPyAXm;EDVtC@`uxTsqY*VZV{*4s;Q&}Ulc@3d ziPo&FpRw;$j!{K5Ue-DtRo(|G$SI)u{2%$z&g@6SYm06Za}DaT>{3Z~;m$rwxh$vi zu>k5IHTbj})f+Xc)tG0k%l&e57+wqTjc0IUNKoyEA~7BgQ7|y{UkSw zaqY}9R#2nC>5IwQ$mxs4(-NT5NE5l~oQvdrq`ib_SFph56`rUU7pu*3rk!Rf&VpkmU8@=3RcQQrQ&6|wCVfp ze9uL!2kVp6-p5zwSXbtCX=U`gW@SFM;mW*jgOz#RhAZ>24Oiw9$#n}KlXZ4Cp*4V6 zc1B+m>e0X3w^;VG9C+dO6x5feJb#nUl!g2`LeK_j(<2cXA@a%OKRWu0L;+HiD1Ojt zlYto0j>?CVX-*DucXVQjqQr|03t&#v+ zvs8p;iS21^u-Oj*w}!zklbRkJ>^O3O+Z!3Q6`Eu5+3Pz6NeJ}bvbk(ZnXLf^{ zrs^lv4<5#o)rpi_yW32^GrPjZ&<~d`wxeVprW&7m2m&j~C^GCfB0JMLf=rNurt>;0 zQaExZ4VCyI6Ax<hE%U5l%Jp{*+kiwDDgc6x=r7oN-Bb@?r`)b{8ntD0x z=Mia`iZQ)*tR<1N>`VAk_={`O+KC5`#B@v&t>SIfnInnEo*nIlx(anR4PlQC>jvmg zr~{N4?noe#iaQFGTVi>im|!u;vdF-RxJbM-d|tOmZHpi1yaf|xt{@e`e(P$PDtMxL z+*kE(6kzQ6IuM`}w0r<&`)$hM^s51;bE&)KY#3aG(KG zk4bzpdT_+kXj4`C)OWyJzPz#|$l|nJCR95Y6tu-hJP&kADOX5CMmlnZWlPTzkN4`z z4y=t-gu_8%!&%h4nx&Pa!ushFEi|FT4e=z>$(2kU4I(>fUZY*Hi_MHqw0OH#FP?H6 z(_tGJwXZd7CQkbWlfSxK&JxHq&FK|8PB71o3snzOx3X2;V)x1OcB9@@e1w?BENWqB zMSde%n1#vj(RdIi>02;XXl-CmD|L0Kg;!LSyK1?kNfsCVqdG4}>Fg|NF;xFIT?{dh z&i#R_nb+duDQgRygf<6bbehE28{QwLJA^-lbi$AKC9}ewUP4Zt9kYQXWQyg_%EdAZ z>s}Wov=lo0JRrmi<3@>EAEhv9@RGW*9n?i)1cyhjs{sVUD_Ek~@ML!Y2M7sjf++?# zcov@vh8IGYrUV3rXqqpVEP1g$bhDtJu5UXVDkDF9r><^SqAoUw7cT0;JC>DI`Z)kf zY5-8(0bsfJ`~$(1926}T3`5z@+ZNj-CRa0ev>6gc3&JgU@C+%QQ$6!dcZeFeEJx$F zO5cN{i2V8C3K@z(YgssiK5m=58zk?g>Fvyx5p9qkphG(O8{2E`5PIHAJ6Sez{s&a5 z3hl3urQiv7W>0@cb`p@?J3Tw~nWW~j`xBA-TXjW!w(Kc;!6Y?U);bB#ZR;4@saaC# zVbn(r$`<%ya+TC*<};OZf~uN41a0?>zSK84S&59)953ftK?EcNj3$eBIL3~5a8-#; z8(DcojpBnwy9_DRNZJ?MEr9{qEz7;OTb475j@$6i`Cj9!r)LH3r;6UsO!ogL4!S z2H+zv3S~B}`W_;)(o+^Iun%wPX96fVhn00X$~1+7T@NtE>!(Ft&NM}V+;bwuJL)-0S;SSEZ>x#9?C zp`D48i;b!X;wwx}pV(*09(LD>fkUkNlgmS{Z?<^C5SIi^cxelA3+eW{OdNV~iB&?v z;2#o;=m%adkd>5!^|N4|QsRrjdX_O&Abob$z;xOz>{>$#v#P`zLOzWUXO)M0)T?iJ zWQA~q`d1M%Q-X`XoX-Jxl4=TVU9A= zFmb;;yN8u25>Msl4*F>m?|Q2V=>mDIs&%izhAY-9UB~H9S~T`re}^*8Xm8;x+ggD5 zy#CtNcV_kW$?dRtqV9O+5Ht*m;*VVm1QYn}32;b40io9_@zC4oBhq&zydPcAUBH0`mJsC!lSLHOW&ITgf?OpEkJo91ma?@5=-mZf`6UVZ+|Uwt8=KK zBv<_gy#M{bLI8!1nK`~R-)~FvaI-Y<<_-$Hzm=AZcf{N{u0)V8b{hcK-iR_FU;_!} zT#FJqmE%Rcceb`NirL9KK^%awxTqOUGg24@64<3TF<#guU5#+^U@GdM2E;AD6%v=< zV%xfCh9J~i7lp*-x9F^k0%A3W0Mh2K3EGnzqRp zz&~V>(*}}-fTtIj5(Vc5;yc?RYAt%z{!)L=p0V!2VD}q?efQVm=HqKIBXy6v7Ps41 z;Zo=cQftct1iKc0r45pO`xW~Z2YFFuy}m6iNzE{O6Om)nFfXz!E3>TE>koz~zF>rv z2?`cc>L6mUls?H>l!~!(Pys*nYhHWp3Pf%u;AAKCLRnv4OwwAx4Ge^`dE9%mG^(&4 z@-S2R^?Azvzu;pArCYeRDH^+|qG-#$`h$mQq5#oPUcD+MJJd-@T@o&68DNB=Q;RKU zp$UNzmuhw`^X!f55F2k~aX1nmOUZhe8Al(2|CcbwxROM6YL@HI+~mPc zAtua9K0Hpm_K%YkDwA$F3tl{*ymgTQv{G&gcKD^1^`5EBbxb&G&P+8L3rnZT#$4Q5 zmA76t9TD_-Q>!8y-2}7T9y1O_W-Q7$B3Z&}E(m?-P_2uS{N;tfEm5st3p8^Z2(TLb zG#!xUz1EzIL;5%_mEv}hx+J$+HiEB(A2oMNq#2mDD#C?B7UBY7ij@YRmK{+*oG^M* zg_xyxss3V_z&Je~){%EFf%}vm4cSZjb3*!&j#0Bv6BmdlojS^Om62syIn3)v zZ@Me1PpGd!gFpF;6R3>wbafIQT%#arl;etM$Pdzxd_61|)>-|>GL3!de@)@bU(~pm z6NN87aO*kp754oH&{b_z{>RzIx&tjJgfb! z*!m`@c$EtQkXgSPkP}sT@YZv%&v(L@4_$NH!SkjCS_QF~e~}3^q+*bhwxm~;R(zY+ zA88_#$yk$(zh2~XK6x3TN%#b;g~otC_h-NNWJ095SB5J5nxru?#0b`< zOM&MTtaDfc@(yZO9s^*E#48JxMV{6{2;eI!&;F?@t24t5l#x6@bJb`GpqIE=?(vpD zp5xv*SDip*KK7M>60D)tv}Jp|!G;gnkn~)QM-x~pl$ts@F<}F-@>X<;XQ9im2B8uV zzhc(3xz~7_hq&zSlSV)e5KAa-WF}LT=MDu&$doKrI20A~PuJLxVi|pmj?HJBA|s0A z61I)jgemj$kCl$!hm<5uO>9BV6tEVb8DPLi|Hr&ypq=K6q%eifh=7dd=En=0#`Q_e z=eyr`7d>297}wYk>O13({5L0Z_Xlw|KdvX-Js5X$ara2vjmGtpd3N~yu4Nc^Z;QLZ zxPA)HJ`{IQE8Tr6?jTxf?@Mt9?b6*Vf77)8gTq6?>T7Pz0k)LFbwX z0Z64KVug@1TT3|0M63uR=5hq_&;UswrQ()|CB;rvjIT}#x2|PDFAHpvG$hekaJkAd zKoMZM$}(VO8$}(<07Zc427I+BJep2bJ^8RfMc9EXOhO`woTZUz?^yun1uTdX6{Q!k z(m-@3FB-z(i<85ZkKL)WK_}NJ)jvpud{RmkKxIp^jxe?4LH6eFGqoe`8fh?d-ea)H znZ;q@X?~y0{Gt&9Ip|I@0Tn7B=S*^XF!jo6N|{fC)sJ`+g1a@yl`KZdr38m} zp{*bM&tZ^GDwCtcpekem^sGKxOXAe546;$RRy{m2rudX3HS2aTiG5#Ie*{|6msj8TeJ9YZVDFb%kzRcm=wRm8E-YA4w8ePUtO%#qch%^byMc>J*3?v$WQdIaAEg|sJwL)w9}y?A9>u$A@@ zO$l`nhu-83Kqf=$@64*0K-4gtCmfW{QNKWDU)dCyIHTj)1V*vZyG=N0UZa@p*=Iv) zhsJbA@vc;iE~;C+I~DI}KMkY&lq4h}Rp0i4wwipMTJImeK za$CKMGVO)KQa`!t42>n0u8ETvC%?peXf_B=4ow?1MEt!YXS7e5q2w56-M6Zl&51SO zE^3IJrf)^3Vvdh5gVy7J_%hrrwxA@f&-#BBEnC4d@8J#J!DxsEa^*2bD*~hW2fVuX z$--zw{kvD+4T{^rRU&@X?6UFG0f3StjrLcSPi|fmBPrPob)HZLJIxfTxo^mm?>CD9 zlpHrL_h$bRe3YyjY7lQ_3aj!JNl+6agC=*Ik?n;xEdE`GP}j6CkqllSP;OQ~ITZt?P}Zb>><4YO{Q`|8s)+tC?z5lAQ$WyjJV z>XVusMtVSJS_Sv4A5Ua@{zTIQb1qq#G0B#V17p~2Qho}N%&=1C=%-^U749nP3809p zJgeNpl-$rahpD*{hY1IYrK0{4{3}-&W?pUid{;;SXH0Q$lL*5zQ0|{H1#nFN)vtv8 zQn*|4{1o3z*@xtcfv$*GF{ZO7Rs_JJzXO)I-cjGnkEQAQtF&0(f#O^x44?Fpc<5&vnQ8DR%>I{re}sB1;>5%5Hka*ua8P8v*>Q zN%O9QhQoncaFXFHygL~=SLTe}R? z9^JCxRPBRGo80}}u`LKQ@~+cEI9eMbnMn@~q-GSLdQAJ#{n#JHSQkWbZCPJN-^xq= z9liwDDvJ@k5w_*1V3w}ik>6^8WPDWV4C307uoKGKuLqln31I|Odc7*`tH$fg@$1t5 zfMcFH;?QZv%J&jICconR)q&{XGNNtI1#_vc^QL_NXv~NRJ*Fp2`4WxmB5hS!|1bN% z*Ev%L_r(uj5}S#ZZJ zUkRU$hGO2DIKek)yE(lKq(}oSQ_r4teLKFhTP%!|unO>Dya+sdFNcCi|JU?KXtV7P z39GCIsh~xgNJc9zo~f05PqJK7OY_Do&CmINMBWD|2h+ z+k}vVKL!K_mZI(~=|c0bS|hvwkHWwv^&kQJDTMG+hVa7aTuFFom9-Tgh&tLO*B`li z!Ac9vO}P;syF7&epYMDEMbi+=Rh&klX3DO<$5gK!9@kW@HT_OhP0)_UBA=2~3nGz> zAgB(H#CX)`x8&f#5*rKXiPxvl17PafEU~&&kODWf9L0;hS5^e)rsFQ4Vn|E zQiGDq%tsAEg|rY#GG1)7Wz?fu$=TA>T6oWb?1dT3++G|uZm5c3`J(tqxRVysS-zUx zB(`^6VGiRJ=G6`zHzl9YKzx)gLk}7U2b>^I)`XoFdN^G!%hiPWi3!ud*$Bb{%dSy) zf(iCYO+Jr5hp-ajs&%PcR3sHQio=si3FGRU!-wK}ZTc^r>=nU!v?H8@OEKEL`u4wH zk#0S&Kdp=2TMcKOiHB;3>jX*REB^(XY|Ful4Rg2$^Qb5<5Q_h4ARTP-h4Qp@x{ zu$<`_wbOfjBljzQ+OK|FUkztp#em5}!r0O$@)YG0W=TUG)_3ZY#pV-$P;&)vg#klGsYG+C#zi|x5SLFDL9LHLZ&s86a0IMHDmJY|Jpt&Lfcws{6(E^`+9Q(EFrgYu%|_+^A}i z#ZcyK5at_pkqGmJwyh8Ib+RN&GQ}*+7yF7jCf#*}`KtN|^QDUp^L4koVZPSYK|s|{ z2|Eh&RlUB42N%ntdZWg!fYUfL{XdPwuj!Vb%v`X)*70P)&WT26j;c~vl-(+dJ@wOq zp-gUANGw%S`mG?4baJgfP2R|s&BU=kO_lW&Pn4aS-SgN_%IaZ53%7SPx7gUY{jKH} z^$oZ0ZEm@KM_g;<1+DF%V*9K%J`LO>jOna~Ik}E9G)|Q*_UK}ZbtUG$`%77g0)qB+ zve-bdXZW)=u$Y}JR%~#xm<~h=H($)~%yFD7<}-K99di|2yWE|z`6%LC4s%Q5p>P{5 zliMggN^YZlJr6@5Y5^F`rv)QUCa*a~x)lep??0{yx*0$kMp{ra<}a(^osl!PEj}q* zWu0E!yk*PegcDD)lZXQ6EG;X8zWo)d{6{l%f4*FS3NniZYV)HN$=>zWn)aMAm1DWo~`rmYMgrL{#YLx|McD%Q4086Hj;(ru?A znv|4db@!ULkcij5)fClID(LlkuDMYD3RZ8NSZD6Wf1NUg3dQwg0Ts9)MuOXPJxdiz zCf6ocXe4#rAX(_!58B^Ru}Ipl$`*RWMR3-5;cu8O)G0ydi#_$HvY(ueVe)0Q=RYd+$EW=82^@&GFn9|cs2yv&+FUuRA>_M?y7f5nLc_7 zFowKZ%~Ua}slJ7$wc=8-5nrF)l-SCvTV^^3n1WM_t)f2u=x%+Ks(=6Lk4{S=n^G2} zx9{t`eb4H*;SXXr(A*h1c9qytWNi#996Gg+)5^r>wG}<2ibN9_)n{fE(Obs=e`Yg0 zjg&uN+xhwsayhy~49`-`2xZrQqwjq!>jXi+-iQ!bnFL=SHW2B{Wdm^_9Gez2SKsv2 z70o-G&HK<3;m>2OXu*`R9=%p(mv@t+oT{7?JWcS5#@vF&D$Fe+oo|b#r1uDio>Fn8 zHn&JKBXf&ZijnZMV~P^jC`7sonOme%!ra2MnYo1<84vo>H1ND@ZlM&v%VRaS3_P21 z`DB!`y@=?BvP?7Ix_i1DRTYgT4YqVjf(gtmWWh$$lgupu<}$s4JUdiwfvcnd!)pxE zV6_?z@i^rZbuBixl;YZid88KKnVtHK>{*SurBwXS8fyzZG->~bB0^zWb=T?g6oEnI z$h|7ZK=O=iuA%ALJ?Bo*J6GB1_>4NdoT6v*vVy*(>{p&xJ)Fw&A;pcgNcrUQ(No0v zmD)R~l?oCuqhf(cgmsd>A1kh|s^$y5IC{Qq%de7ZDk~N*zaz?hG0sxmJG2gQNv@!38%Q8gQOQiG)w{+Uj*&Os+ z2QC3U%9BSQTxSv?1|gNW&Q>nrNYfL90qqYaPb=kbr=~^no^ebGc5_WyH~2y>L13YB z{7O0OD{Z&t34|fkb<~t~d(qem>@>|6o|Z|6e2@$qdy8P^>z5p;_aLt9MT@CvRc-Z! z2UxV1NQlYnM@UrnFqTTt8jC55k0xB#h6O=|YUU<#)UkKvw7$38iw6SVzx+Ub;W;vy zzT_MmBvpyH&7)PqOk8NE5SzQdS(8ubZEDuyuT4d#A+#yz-jh<$xzH)--otir3c9@q zm4Ys1pnJH;hXZ?meD@|1#BWCCiFBqO;!_kdcG8t^@A`gwp}j$3|8l9)7zeUOQO7T!F;Bsq`v!B z4ghls8D@d|_qX>ZyA%f503q3zOLlcZ+xb(73Aj8^m>eiS893~Dz}dY>01Vk_yUL6W zFMn=u*}0??1Vz)u8s4qq2dk{C3`s%Z$VUOEW<6+NVffD__SOaZ=x_pZl}= z*GH4TUX+H-Ga=b*-B(N_;oE^~bavFEL?0?5Xy9eRkw7TLd})r{6bxYnPSJF-0{!Vg z{LxT)NbzxIz=o!6K6-nwGt28|9RLRn>!%)A8u7`n{#VcC-BCTbxawa7TH+)$(6IJ& zUuAqk@`3{w88dSxjbt{w4(f%A<7KkRu6h|s=fFigKk;J7$HN&Ps!Ft^eSyf3Tz55` z1DK(%#GMThea{OwE{_0>)+N(Xy>H**wtUe1S2P#tw`3AWN`$77?aZ!x=ee?Lm!{bD zp`FS{gd|nPm!1WBs@i6WF)I@LS+nSLCr$a8{DG&1@K%V zE~3{8ll1CIxI|4{*f$OGMD1``G{}IrL1v>hw}4EgafygJOB*p^=Dye^h%5gTj*OIE z;&I_;Pzk|OoO@t8r-(GVIA_mOcT7Hy8+L3M!JF=m7$EgU3B6*R)Q((4TQorfYBIIa zOKre=u;OmR1Vf|d{Jl<2u+juVF7KXjX-dccO7gt&I42Xle z(%9({N+X3}igO4NNnW75?uk}p5$u$_M9#3wrQe!dD+LpZjjIPuBTNwT{u$G`C=lFg zp7|euK9itT1f3u8rxB`E1ZOegPuuFq50R&ri#-4gt~gXD;gv;5xRTrDY4hN;&V%G& zqaN^4tHM@99dnnO@bSA1TPSu#O1OqCgj@qA1r5VM3vo7s6b%=b%A5B~N*g~`Kn<8g z&;c?Pb0A=oC@dF}UR}Tuf%Y`_)Rm+-vXcjnKVmOs#-T4bEt*^Q6Q5n?)JxP#08H#N z*5?LE7lMA{R4v@(W5dK<#pJ6Rb}_vZ`n-imMmDN0k8sHaH=IF)(8q`Ro*tnMi6%6J z*jssg;WR5Mxj}xL8}+LFM6<%p{a-aJ+$E&>`&CAx`^*nVij&xMHVjBHSYo=GX}ACDw9n05)KxYC(? z4NP&nJ@O}TRjq!o+&(yM^@B6o2hy!{s^WO|)W)-y@)Dm|^HxsQpzt}7FvxV(94zc~% zzbJIj4=vhfUZisJ0W~R&9B7FVJ?A2UW~hCRHmu6wdaCUvoFd{Nil?=SAzq z*?T-|+PZ`HgljWc3|Nvm11^bCNd+!78;grNB7P^Ia3n7r29Uz$GH#zrfzgWBE`+>B z0@WmAqsQ>os2X&PX=u9(WsXlGJvR`F1|^pan)2rZV! z{9*Q=qW1PgP@tw>svw2_McCi(6s*&CDeUhn$7kK4nCoZ#|NQPv`f*S{4(fgQXb4fl zlo5ZtTRWcO?^K}iu_Bo7BB-NNu#AP#vEK-_BIhZjVZrIm88xIKM5zIjGIbC@AK?h1 zu{@ijO82DtiqKOiAD^`oNSe}lN?NL^IA=(vj7-ft)gZ2@sq`N@j+TcHLH+#C&@%!quxJ{XE*7{2)9 z0>!6TE)&q1xzV?obU0XBrNe>Ilm)`PNGMuhQmn%fLx&Eh2RbZ6heN(_^fhQ|_)5wl zah?N}@k!`#27cK(90YXhaP%@doK-J}4yV`A;pio9uqybY z{W5%#`^7>VfPP1=Bp|B4ZuyV~1ijbVkyWK#Uoj#}Kwj4Ctok>gv4x8qRc+#fNdV ziI7ANrgU76#;?;U7vglvmk>b5hy}&>NU2J?9o#!KwG2pn$!4YTz{@5{(Pc`+Qpjmz zRNwx(6&3-=t*?4B)1+`!xWjpRB9Y(oOsw!}R6qfVB_*BUy&0m1?Hy9?BmN@AD3KYT zi+!~^e?*TA)s3c15xrm)U_}6DtYVea!gM6fg)<$H{$j2kG9gT>lK6mKd~UE2EgN^Z z8o_99qcUt!2A6Cbz}c&{@H;VM%6?8>7*1#LPf`-!TAXis+GTw^SUh$&Fj(+t0jx5ZsaH;D02q- zDFg60tl z(61k{;DJ;0GIAtfllE?3>30N0PUSiSbQcTK05|^Ez`u*xOfGI!hA6?$EG`InheX{N zngyZD@=0mz(4$D`bg>5~54tBGRf{A+@hGhfzW&Czlj#E0$-?_#*FClNqH$ zmhrAb#GO%K2JLip(s2X@)|II;^UaVoQDIu8H|>nba6wjFf&5Sp)H9-1fejoFnv$xRG}RB1D?B$5_K{M#^XO z9J=xjoRUZ?1G;qFN8<6om!g|cscsrjDOrP-Sn(@;xGLThOhONn9c+MP37W@RCxnd# zTgX2#p%4$-0%Y_H4QFzPTItPW~74XJDhc zE`8J3+A(BeWRLYkWWEGGR|fX7HLGXsRb1pqAp=P!VO+=XX27AI`!o5!`LT`)=H%fL zHkSuGVVeXbLuh#CQ~6Cuq(EHY8N9S}@8I3& z{4C^WC}8*)mY-Ge`SxgFYx|uv27Fh4{l@VTS2f?I&kV z&R#-2p)pu*AtXuf^dp4@QhbSsOUdM`LpeL~9`-FbdW}wR>Zf^A*eedx(g%lSbS}o6 z#E$=uXhWOJl!y1?UJlw`$~{0~&}RN6t8$ObWNZfWkE5qf14|M^A>kP%oV+x*G_DGW zKoVtvSQfQrId8ZTHyt7_i}&An-V$lN`tqSz;wj0k2^AaVB*IX{{zVST3 zOIPpV!yz96yy9RtK{x6 ze^w%cV?nQ!PMA=$tQ}o7=N_rQyG=Hn@VAim#>9~6(TzwJv$Q~Qn5i`xlmFW)E(B3< zHGY#vu7%M2)rstld#>Z^L0u7PlJ-)@vf9J3Npku+oy6Wyaa>oRLl}qwoRAAk3C%eA zv;heUsvzT4Ir-3>rd|JJo+PzOGh(`0?qPhX=fDY}P4>m&J)G3^y1it@`%c~qyikC7 z*oT&Zp%l*u7>EI&6DXqGfu_+_jY;3YfcSo^ad5QhFWrmjIF`@dNbXJ}D?-eJ3Dr9&7judXTh}oCfry zsR2Y6M=F)3$fcyJe@r|b(1CWSg?c_e7dJQRQCyQb35uRg)AIN(ajtCR znn-LG#Eki8sV_&{s-2Jsq(c6 z{Wf2_1DNWm1^&dft_o<1@tXq>zu~sEPU@}dIh)}~*~EO`@{zj;9k1WMa#xUZoT~?E z-i?Y-$?(zdU2{iPpNJjgyPihrvudf|32^NUdOZDZPUV~#id;(sQAoFgLr9W2-gQf-Q114PGj>+dX z%&+uCeK2Z1ptf%*S<*L(qIa;DB)sI-SQ`WBq!1xWfceAFfxxUzyC*8n0uu(#ltdCO zk*x4+;)t4BE~w8lr$r>30)jHMjt z@*EW&DVTWeBtJnD&o}ctG5JLcA5S!!a2xhH(MrK&m>lPEdRVJ)6f{7+=B)!qykn!b zdDn8F8xBE0ZhskV7Ih7cWCZc4H^DIQjyeFt=O?96K6~MasP$^P{rRGp`(}}Tc5jr) zMq62Kvl3|)=_jJLDz;Cu=?Z#RV*7xzTbCBfJxn#|dTneU*t3GIdI|4$V*6NY9L@Gk zev4fkUfh6{*M4WVhp48MhGbbp5`hn7wmPhD)D7z!`5~(LomtId&3~I=eIzkh8`d{e zSl@!e`jAFM6A-Twb%(YWZCKy;dIyi+NpgG3-%7?Ye13l6h5X2Ap z5=I1@3O6~fA4;_9R0!TOgV8b{aKS>11nA2QMoj)EbCWcXG~$|g1&IWs_@PoU8x0m0 z=IxB4IL+7zIrap-!bHITus-p4KLbEfCh8 zVZJN(G+_v@>P64IB=H>~3%1tC`a!53(=N-o7=oDTzKel<7k7-1dfV1U-tJiMRlJKQaKk*U&JU>XO%BSnBi*oStXDsp9B8z34af9X2V+< z?RVaJ^F2PZ;rJwf4<`B1gripiM>ks>tzm;(^ZNbYxGSqqnf(10oZ6)qU^o7*icTgK z&FlkMZ@4f0--4uZ*=cN4u$}xJz-%OX(Veh3wfCl6bACg`PX*l*4ihpsBZ_v!kNhBj zIitSo(7#DGVdqMzAf8Ys0+S(Hy#Nri6hX{|5jh+$Z|vST`P_m&NoVef56+|f0jwfX zu5tOwep5mR=z=*tfGxe0m3!0~u!fE`08#KFkFCzA?`I~IYKH=Nc5o$oek^2Td0s@I zZ1VaRlh17SFtyE);j=LYbp;2BCZTv}{<#5g>f%CHlHno#%hBt@nkvJF)TvzN_2-() zqW;_FlI+;J^fEM`{3#`jSf4!7T#DUHrDff|(5t`Dl=fYxKO4+m7~P+HD^2NQG{NFqVddeeeO66<{c>|z)L&{YOMl`Qdal!#{n-nH zcmYdy!0q zK6&pcHsArj+k;+kID8OyDJ~WOOc4<9w!AAQhaIb4#WCQTAsT53!|SCOfe^~Dm_DJQ z|Kh0h3ONB8dalc<`a!EnttGF1jV;x*-OPY@o0DQGH-`G7g?dOAh&=SiwDA@cubVOX_NCYp<(WIq75rzZpJi36ZKa@5?Y@*@3f)EakHz^bO~^CijD)9BWp+CFpP#48 zVsv_TDtn|P=~VZO1kEfxienPM{Y_^UrUTmPe@~A|AdlLD)}?z)!c*5BlVGv{o6N?w zBtdSOIK&dIJkGL9C0+v>h5q531-zrQw%361&I`3MdC&AiHuaaGIC>`cgL*+(LS(vy zrr9GCeT#fBtu4V0d~rHa9o0MNWDt|j`}(>kKV1bRqo{ICVs!gtgprZZc>ARU@0HO`GuPTQ*%TP9T7wcpEJ3=KVFh{Q;e*Gh-;grOMLIc7AZ{|u#mSD= z*(%akv%w18a_tJ87Ik=EKMem=>&jHze5}gWe)(tJFD=|?o6YpQ)Z^@Z)Dw1k3Q{h% zqVZ`4eK-BQuIcBM-fhVGGOwdN6_K6%JqS<#L{TSU z#NognwKN>>E&*YhIA0OKG8FW-Y^ zq?#!4oyX0S0*ezbZ19BmfhYE)Qwf`=I7jPr9%GSB z3#^01@5nu5F|~Z4PM&-Tu=9pt^3X2psfeHx9nLwnG zV_%or*M9^DH!mx{Jehjw?OPL5C(74kH`78$b>n1NQdFltz_-Vg)7&+`Ej!*C~@Ww8?5mJL#33vQe7dNTVT>wDJ$`WZjR5{eybWZ^sR5uoaPtOrrBi$ zv}&4?^is=9iFPEdOb2ZVgO^=+YU8qDdNQ?L+NdT-46f1w2^ zWw%e7M)TB5nbx7?|GM1+HGjE;wz3|IyQohdl@^*Ojzt8i%KgE{q*whfG~V2?k&`pBzS^act| zC8hS%04Z#2D;`#?RP4z(VMK!J1Vo~z%pMzrZDE%=aGv2sP%q~%(5n;3LIdrnl?PuW zp>Foiy|8N$TLCO02#u?12sgyZtzq|JspRWUqk1FcOSD+h>A@?CXQmmD=qKonwh`z> z+elcsn%h_Zh~xac_mn)sFVPxsgmYf)pF~aYNKYuwFs|xZS60DDj%CEm_Qlm)NrSG-`j>lNz6R@6 z^zILX^yeKcc=;h#vc-bJ+oqt*9-S7m|0FS4Wql@nf@tQt-i0(DbhOluG$y9yNn?`I zg0iNeWZ2A}luJXVVaz3058Z%ZiAl?H=%?idwif!F<}}HH)?B>^`Z=(hPSDSR=qGw5 zV2}Cg(ogrp#v$f4rkH+&7y<(Zg_H(1ho}Za!zxE33u1Y|th^9}HJ|*-B4afyqY2?$ z0u#sUQW7Cxx)<76mYb1mo4g4=Oc2=*Sk;Z92t?e$yc)YNgFdJ)hD*Vm5}7k{eL+aA zjH1*C`Qg}ye<xo92eiy(_9K<3we=i?Xk=9`CnCMU% zlN8ILQ$)wI65+}yschvJUhg%WqZWf@G!*wbP%Oc-fnr%&4aLD**|BbRkndAx;M=R8 zJOf{l*)y`Iv2@TWp!fu!*!Ct_Y60X@fUN0FF6&-gF@nbi$nJ^(8L}LJOoOsnq0P{W zp(ntx2S5e|xhqLA!$20KgVa`*WMGy5{g&MUZG~?zA#{cV`4SLB=%z>w(<`xm-{c*R zc5YxE&=t2Gb1uz+rd+tk8O@-i!K$-tm%sAR=*-N_q7}&DO&0o3rec~;zRsBuFeELO z;f8a5MMF|_IWxqk;zFFeOH4NWgJ8D=R6uY2$=Ah(3-{|sg8~a^2aP8G;0$Tue~&*F z6X1hX#DI$is9KHQ><;a~?hu%5q9w*rEEcF0vtUD>#MoT_x;rSWD7=BKJkYsB5)7n> zksZ-KcH23-_XK)De-y>U&ldPcezs2T-xB%Vgk6}Jfb6xM5W&;@%TB=$+$(z_v5xiK zbf7+q`_uQ;@7MLC{P79Tf1^yI?fe&jIu*B6dP*ui=rzz!qEm(kEHGWI^t|fG;P0f6 z1m=~H1Ya6Q23tsKj|^sS!#c4yw02z~X+UTD-aC-pp7j8*sE zU>gvbfLsQTrg>zvLSJb*H#{=*|t4SfIyv_7lB;n z%jDm}P!a5<*a5jN(ud|WGWG`N2>iIH89EJqpewsLM}RsM8^)Y0lC_@W1Xej5nl;Cr z<^B!|p9%j=-*#;u&l97NkLXl<4G`p=B1sJt&c+3Ii@QxcamWr7`J=`xNvFgQD^qCK zrD+-Yf)ANf6!H5e|A+Z)SXOOjy3G%=mO(>WRry`vXBA#oB8E)46fSfKM=L9F`3WT_ zOJX-Hl&Ws`Flem41{G+>x2deJbwTTbgZ|Vn1I25)U#=dZ(7))61wVmRu-_m`a}tM( zeHbEIpRgtg;`fMuc~ggfG0FddC2M;S7Y|hd}OlOgb2arQ_7m9 z%|x5Le4lnYpu;C~dNx-P!J=e0MTIt`TI9tkd~mfGXOOvUf3%~eENv44b^%-v`g#4IUCBTdd(djtNm5{X1RqOkZdiX@|) z64wKRB0iO!CUVWL6rvA)e{#<}hudV;rVb75*D~*n;S4vXlW6}nl?|OtiJ;UQOF3Q)@u!+y`N8GAkHh%)8pi*sDDu#jOWNr; zUhT{>XZYyqgpkW}cv><#&3q%Ej)kn6N#9d(X>Mt)3xpJJc1pJ`5!(ek)Z{f{5q68C zSG4*4pWi#}*-xvDS1<3Q%!+R~?^DZrxkHz2FI>5=R1tV323kCG2{*uRhpiT8?b22@ zK-9EJ1NEEK+gB}$<8LZRty3G{MYGbK>*y4HmaGwO$t|XqlJeG{MG=+G^j1_KrU}?T zLfZC7@W48%9}dhGZz89MDpRX6^GK%bmwUt~@v>z4X;vS~&ohHqNZhOsO%%e6zF60X zu=z{9&|ezyJ~A?2;5LOf6O(L(8f@6d(LbwV#v+ag6`|hdeW|Q6l9JSi4(jZWtR7rq zUoABv`xFeGY>k{1SFhw3p(Y9g(!wP;X(`c(znjyjn&T+_g|y~aC-vcceSiC*pOx`U zugs?!;!Qk4E~zhcBVsZuG+^j;aw!W?Pm2}oiHAk(5(D12LX7WiZEi?C5&!ep(8}?w zKHIhpK0^fJ;}n&7HUKhwYb2U&oeoypNwmsRtkVgAJXuKmE!)DA z!rF&r<)c}&K70M%!z)_mXv!WStp4tqQUPcH9L3q9WNYj)y=aeqtHu8qj##O;ND7u9 zmr0C4Z+A^c^fu>Jtj~&RBn#%x=-`y1G4o_>j#+=!bYEE#Wa?n|$Ra$xO*1-@5$zCx zCrs(y%X2kD@c7>R@O1F+Exb;L+@PO-)-PP~@As}`msVH}esQO`Kts)>d)Td z1vT(^{x*sD!<&}K@B;g-Z8zBa4PMh}s!Q2kv^)HNT<($hF7co9gNHYvXi-+1*F5qZ zK!J^PnGHLabPH)fo%o4gxB~DU>@6;xE>wdTXm)seiI#be=uU2cictG2E4KTsmrlo5 z&{t^jTF)O2+_D|k|>%L}M(Ik}) zsSI&sjc=cU&ytaLV1S_l(3yM09za_LXJA{~R~BG~kPBiZzFB!0LXj4U#_-zy#S=Wk zXfE2EEygBNXaEWfgGa!zvt$A=ST5-P?O$7Alaux!iJMlL=Jj2jVhM7o_`XhY;bN+I zpi@{{=u2Z5)GLQjVD={0E(#;z4ZsLAvSg-6`WViUfB#kJIC13mNmOB)`;F)5U>zaIXim8!74GwdPIwG4AY|i53-FgThHC5d{k`W#a@;Z zTcS;s+78DcO`x#xYE^;Xn)rcjMRL1X{}wg1XN|E9wPA{YF+;5innvQi)s1vJl5^8` zgq|}^30Pyq$6Qg?t%&}liUhkWMlCai==gsSr2eZPbV&VlQ4AxW4~zgBwq^-onS^mk zqj1x7#3@?)PR+8tI$ZSdH8-prI{2#Tf{8&ra(ZJz@}(!6XQ#m9I9vFJ-#{X)?1HZR z{7rRv32p@y=dI4|Q+hctlnkB&CqC%5Ah{Lfn11DzEB8L@)zjXURqqx4cuAu?xx@} z>z$Trinhg#+o6@Voe&tFF82HOy?Bwwf7z>S#zx$JjtXXNCO2}CvQA4B1+9AkiUp=ZK)rjDWB1Me8#`mGyX^7%7l4dFwG6! zFkc5jumU*6eE@*)L^j9OIxG5vi(6_e* zmVslRn`Xqv(2Oc;>L>bY@|6?#Zn=2ruzx{>k(aZboy`6zN7M9LT?jD}*HUX1Tl?bzLdkmGEzFyCN&xR$8-T7;Y8eW(2i%2PSln zdAMq^tNF7a*Q@8H`LoEfo5b(d{G}{Va-iwWG=F{vNK{je=8t+|gq^Dw=E{=AOh@*` zAahj^XCui?$qusZe4Y`b4b z5g9DTqS3zyy$aENie|*z_yT@lmChGNWbRqJIkYG_8l=g6DB@^Ii7spBAxxV`7EtXt z1^g@(>Pzeh9SKiv$`iEX=<$oDER4tWLcL+bvBDYY&qF8Ecdd7^hj08AeA>O4p++mP z5S?brityO)rDzuz!enFF6s5kZxeKQ?UORebPJyN?=gf6u4jP$ zeFEXX@x6uc-|rxm&`~Bmx#%|b<|MZRVuC0abwqp6i;Ftae^UggHVOh2h3Wub8Nkco ze*)^W`Pzl2MU*eRUq)9LEv^kD*}OcqlO$G&fcHJ zhvNx%I(j;Oo-W&5#D|MjoW+N0F4>EXPh1lp?nxLhcf`A~|AoktPaIpC$zf{ReXhAS zZV0@z&L>V9GNCX@fq`)q=?pVlk&iv+$EVr=96H4YzX}$tjSr{mYvaR<7Ew1&ai5i7 z?Vt(-YXuEu5?_Vct$?Gi&&W1=j*%>ZCm|+LZ%4J+=ZdABym^2;ZCWiK_~bVSQlGzx zbFr%Vy-z;8_c55-MlR1UTaun{WLU$&RZ=^;-SL&v`Mn3P0EC42lhMk2Y(a?3gkXL)Bqp)P~l?y&Z+;>YF-sF>z45s zluqv#Ry9K-8m0tN%qdhdW#bGPi6TR2wyZZF?J!j4O{NWAv4q_l-zjDeVI|ZSY$NSK z?T*R2hMra^3~By>784sBXBw(%(ovW;kt5eYLbU2qq487fRQKr8bv7AGW7~-obK&XM z+nIIGCZj333Z4C?367{&(1-nK1FRLLjn*Ob6vyYV z{>a92c;)dq+?gFK93G#;?}xp7&n!e$WF1DM0&i@+UalU_N>OHU|Lz{ZZwi>!nrn#_ z67IM@g|yR6rkIyXO*DFf_14-q@-SIzU&L$5L)1uP9v#Xo3UEp)l704XUC=SuKI(#* z#kSVbd=i}6Pp)#oE-i?#LYZv3us&-d17rs0albx;5W~-8Va2_^Z;}7c3i6pZXI%)X zYI-Et&Hkwfzl23%lXH{>`I2zaLtjLjJj9}7^)*5U;3Aa4GgTmE$qkLeimWAa(86V# z$^MOpR!eRI|$w_#`P!E*TwoX{KPRl%u~mj z^PsR`{7d1EZ0a}$X;}9;28nsaPbH8i&RW9WgLE(wW3Uz$9Qe~ZS=Qw`Lz%a>@Y5VL z0FBZ(S2pSKUi)boVNPG8SR7Js~?=(W@UQzQsy3Lk43^WX=4P@ z5wZ*U27>~@K98)Q$Q!-c)7JfLy)q+jJbOV~{rSgGJ>+eD_0MaoKcDJ5Of)Cc^>AUEqD}&cR;I%^cW$7Q=@dcZ zUD>NSnXY*won1GmIhoEeHa+bBewt3Ei?iqup)Jb~C7Vm2_Fdn1{iwEIH;2*neMTLC zDv>-7#KxvWMR5t9WjdEG>o@=9%|-fEehWU_na-f=uR5a6$rX5df0mA-8*G9rnZ42; zMTh7gN6|sZ$&{6JUE+Dga^h`Q8VT-6ubSa*2ZFz3=*icm4jCAN=s6uR3_;719KOxglfV7?Yjg z>A`|F0f32rY7_a-XUP?USqF@`8p%wCt_h_rutI?v}bMtw^`5It*s{|MI2XFp( zR%ersFB0^mGl29zjMMkr{7)iZgjoToy}~b{gktr0|I~8jW={ALD&zWtH=ieRytM}J za2mu}DxfmIMK#I?5C>vQo1_yvKuU@05PssmJ%p@p=FDzkaYXNNXr6&82QGf6p!v3f z>{#B}&<>26b)iHKvQ?5)$^4o4?2vcIpci}#LRAPaP&;cq!Oc47N~$y# zTHG&-3)0sSM&t2jHa!n6j*7%*n?tc2=JP|afAPKoSR-Xof4_Wd3w6MKRZfIBe_c9# za(6;>Q`WoaPPpMIl+PVQ8y?4Q!6^0;#98TyrPF{NBCkXZ_8TZsebUz<8#ZGw%G3Y= zWJ$w1Jgm{9!s0e~p!u~D-TfuGWCn(V)6H`9z1*HME$dUBzX?H@X$h^aeLltOPWgj$ z1QWsWr;;0ba90)|{DaWI?$yR$fy&Fo2r~k%(qX9ITA+lcfRhu0{e@SiGJs(f>XUwe zRKjao#rm^x>dKSPk|WaRBM0`x zl@Nzv>ZHmZe;_Gt5Z-!P0Lt;>7S!lEjuI6(gN_j$G9GWeV9Jr3Wawg@ldfMerNjEA z`zF8U)W1%+f!8ikuo9#qHI)kix_JuQ;12TS`7zG#DWL>yK>%syr7$?@QwxWZw%dDT zLx~J=Pe@2nkMuKJxvynTeI6Aj6)rD_8OPP9+f?E<9VP3F8-Dasl}_@ zwr7_;M42trtZ=Ti&Bz_rfN%_kEw$~ zyA@Z0)1qWwlT=<8an%WLjFe@l3i9>VfMv7@q#I$KsTql4=E&dbm$w*^kftG1z$bb5 zkvHD0lct#QPYTmwa|u0z_gaJtqo{+Cs7vuTs2uT-SVG4KSoG^7=p21|D}t|!fCDs* zb9#k2(QZTwDUcKzkm~`EI5Kv%Dk%1GALk7{7?$h_0ZHfwnM|X}`r$XR)Baq087+oh zn89jmouxE^FY}V1Pw**RAxO*V1PrC&l#Dc8z69))Gn*)YP@zS7W1w5CN#3$1%-Z44 z-U$Lg!KZ*PmXUfDKP~w#fR-IG3})LO@2iHs@jWX|v`<O0(Vx z0~T8NI31!z=cQC2M;xOWlzTL(yJA9}wUW2f$j!GYI@@c^H|>_Tx<@nDyeWQ91g5=T z)_wE7nn+9=7)MEE06H9DbYg@K9JSawoJ~%VCP&N1u#oNSVoG;GZr{8J!~n`{ zP@uyBLbm8>xF+YUn@7zH)?P%8jND4bU5$H8%nw>A&-qMx?G>MuYNh_EAN*Q0!twYyGU?p+QJ~tEMCG`>>6X;%nP{# zPR{2Cf^)8ZJWd8)&0&P?nq8f_1YN@bYd2HzY|V|pmngeOOedXsOugH}#u8h@1Pz+= zGo;J-GEOsB`y?c(L>QDyKC7f`8frfIOHv|gP27=SGu)7g@tIWTZsP2q-m)tC${qOR zVDf7xi0es5pY_J?mHt)>1LhDeJM_y<=3GL}L@<<1_@S>nXI&a%r2>XpbRIXEbA}$yi<_8Zk+3;O&z4L&{hQC(r_Y9Hge1=E~mub*`3GomdO}{WRI+$d(^}!@90-h+o zd;dh84+34vVN9i2C=t*U&ca$})eo>O&NwvbA*TU+NwHOJ)jxY_EVL(xX!|o`rJ4<3 zYdu{10($~?WA+4LQ-2ip1Re_G+8G-`#F2ye_=G)yhq5Q|lk5q*J)>Q_qCKg;)ZNTW z4C}$ePSV&7%B}KNwkK#rZW*pHG+5x%RU&d=MzbPMzr*J-ooIQ?= z*Y-e$0%ew$%l&4&hO_NBB?j*PIOBEh56RrjFgMtp#Tt7jT7R!e3kl!b3*J*=y*Aqc z(q7|!b`qd^JmfyoUb8=BOk^Q)*6V+%N}r?$4zOfusEk&%Uybs;73t30Da8vz@j@|O zI@c}*)z(hHH`XO%v;=&RsF;mfyLuCMmmpem!cxO!60$j^*s?U^053|iP7iEP8p+V& z|LHhytsPuRvLwm;RmBu)#*@en)Q){mjqs*I;k*WniXlKqV+7{h{g49bNqJ{nSs-SC zFZz#n1mTz{StYAxgbJ)DOsawR*2QSkAMBOHgJR{$MM+>;e`0cKdS-TR#meIbtDbiJ z>Jv^pX${hMX%RwyrZ5~jAcg5G2lq~CU$#3g{V4X{cv7~zb+_K5{|_vx-{;k&x)dUpBCx%=O8 z3;$DhtMlC#ba!gLd-Y}9ZOV5a(%t%e_jkJM`uT0$b?xoAoV#`T&1-e1Ih);^cFVXC zK4D|Iek!Q9s%7@{A^=_?Liuoeq%^^}l6q_?9@_mJve0vj6kBX89^Cz$$P>)<-niz( zJrguB_A(A~Cr15^>7W=HZ|Tal8#tx)x| z?R6-lrxz!In`U0&#?Y5A1&Sq^ML7qk(AtdLg9|8V%KJvl#i8VV`*h@e1KBd2@@R%Q zSOp;{%}42V>Ru^x;UHUU#z1|nI3L0_C?uagm{S`^Lmb(*VzMkMQeNO9A#8(ki1+0Q zj5%EgwmD(}MJQ)~z!SCMvE5$&DrMw=T%d9R(3ngPhL=%*r9mEJbF^@CibMUIKbG26 z)6PVw{Ms_5UeCvNa0XK@nzidf!&@A#adYGiX>^Fe66kv|fdS-w1c!)^k1(ni5hv6h z3cIDR>{(`0^oU6bqnc*69K4wb;E}N>A@NUi8VKlTCzd>`6H8DaP+M@`QiE7e#XgV_ z-Xq_6#r~I`zorHCu^3ukI;8|3I~m1=_(>)F(u;J_B6OPD`IucPG<1)b6%SCrvnMydq*I-&gZKtQs~_^FM7oaM_B8g3-~FojqTH*ArP zP;1&F-9NKwrO#aVDo{)VV|K-L3qX*b3md>2#!%#-GDZJG0oGm7f~2hI_*PmG{Ho_t zMb7@!g4qnPftO_F=7BaqPbirgZNK$O2wX{MtYA$%-;_dnHImSwYO_K{h;>ao6`ISa zpx1tP^z9N~ibUaE<}X1JNv>v74BlQz$4+xBmfEt{A{GUCl}S+x%&Jj$vHkYKMh0Wu z8w4H9jj(QIu3hYxb-sPsIr>SVnCt91u3o9@%h#G`wCT-$u&VH`Q3{~V5nID_f{yTg@G6R8<|^i=>X(`{e1t%!R(eXe%@hmzE z9X-g!8X>5`A0QJ2>q2qho#zbuB=&0gM0^a_;5c2A7endp0LW$udv9A*R{bO8zza+ zqFrS@n>o9gUF)e8tiiUyQrb?VF_F&$^}MbwWG@&A$~y#Xf_9%14}1cOEc(Ul$40hT zxFao}sI*p5P^b%}L{ry+bsHj0!o^nkgU@I~89UO!*pLkV36l@L?VSAswwmf-x0jKK zpcE;f$o}NpE)(sfi0#sr7qHXm+u7}!-;oiN^mOg!-$~enJ8wra!e7oDONl4kh!U?B zQD%b)fT4MYkQlxiW{<0}VX8~uiyjDcx>P-o^`)yy8VFJ=SZ&Abs0$I3n9A?x7L&Yd=ef0r~7a?Sm<`O4=^Yr9MwsifPgm&oG0yMHO?EkyP?QQ;``9DdCp-*YsDe zn3<6#WF{-A8vKvvkd2is6e#8f>*eT%5Tu$kF3&E~fB%gaq3oFY{blV_cTvMsvqZqhyGCsDs zL=bYhyl0Y*wBxNR{c&6|{04(AUS{55d?VM<>YOgmZstaOBT?I$)W!(kt{u#sNCOir z)^`D`h}UT)f#o`~y+UJn55$Xx1S95aN`oVaUr9!jR*gHWX;V&TP>xE##(kYvVXVy` z{>L;03Rbawa47Bsws;%rqV}LNiBv`L=2VO#B3%BW__S1vGGe?K6d$#4kspY3tl3gO zpB>sirTDi)G-Cf(T(2LZMm&dW=-hAd18b=R;pO$crG6n5v90>;{nRN$!bailLA+CT zqUiC{!K=aRedbGq<=E^!p4rXDDQ`!e#5IT-bt$lGrN9<%m)c|f zodSEJnQBKVuwh3?luc3kEE}s;qNF0n@$7Y0;^_#AR(2?SuK8~gU}fnLf9Yru1UA98fG`S#`gw;J)T6511PcWE z8GK2?_GwaNNiJ77ww_4?R-(X-xYGfYk(JJOqWF+8X7-{T-sa? zB3IsiaYPJ}pA_qh3M5xNkY{8|G$tHAjCEFFqOZ1M187h)`gV;ZH$|0u&hYBt3?|V9 z5mB}%_{Wvzic`p+*0`CFvzfqNrZZFwcB(-n>APqB2&ua1vox5SB0>`1ft2%DOgnVz*LPg zd;Iv)GTt{MGyW-?PC%r^(xuHf+KB84CJxvK$XH{En{3>`aMtwGPJB1cfu_M6ve&|R zk2Dy@@lt`gQuNvf1hSC?Rq1^fIxwJkOjfYFs_-GXKSc3BE5Rmxz%+~L2ajwT?QF&b z7zLy2<1d7D*w)&rW_mh>w@U$WmyRTHg>gq2XX1@1(9Nu15;#rxPeM7vQtG;hrg?O3y{lNt?o)OrpGM7CGM200FJHM&1s3YS$p=@# zt}FImfna`_&M*Fy;3T_%UDZiBNF=d+Zwi(ZF`;Q!7>Q|DtzWP+0OL-$X|#j0oZ(_` zU5dxHuHLqAL-A6=4aFnjM#4w4rA}hV)-Cmgv9q70qg^kE>(jK$C7zfRx;xsG?L&wr zH&T%!PQAmdpMR^3;w^v!fc}h>*8;?z#;Nlp>{lvEF zk%YMajvv#$XJCy9ihsPBzYF`{BLf0ub(opF! zJ|@L{QEyEjY)2MrdN+`F;FRJI;;bHJR+@))Y&-5hqZjIJ>s7331njyh;$5kwnu-#3JrWfflyd=v~p9u&anlYl`)QSS|Z0sgW^?ppOi*E(R7H zTx4gN;|FFN)2?i>^3ZZjhgWm(L1~!f$6z=1UVGII$%h_VGhv^_47$PS%^KrGb~Rnxsnr^0a&xA7P%-Tr_9xaYc+9Xv-Se? zPx@RyFU(DlNQvJ09-!1jUmE>pCI`Azkk}{Z_mhIw#=brm@yP2YSXh-9Y7X~jNTA! zkK+jj@9>cTSX;GAl3RxKvAjOY=%ZRP5tHFTB>` zN5~|ya%EW)J(~Yh+Mr7C_jRQP(q-9xFJCGsIF;7zw{>YC$)EfaC1re~_uw^vAQ{3` zpMpGDYt{EqzN{Pt>e71jLa*(@Kl=_I^$QcD7be@U<{G9PXzCZHN2N3EH}VU!?ce9i zxls)(+JDTYE2D|F3Mcj2{@9Tsnk|Q606=A`LdOiOGAg@|t$0xOSQtrYKhV%<};^L)jcn2lsBF!cl7gO8wsSL6!K1#wek&f3Rh#x62_SkeC zlGM+1IUEuFGQJ7|K=S`XpB;lK6H~8!C;2d7ehY7AOfTYmrRwF~!TeNky)x)zt`~J; zaJ_K5^V6^}*rLVtV#>p>5rw)KUtBcdcm)?K<~P)c_%hX2n!$R2zA+R{ix^Ft$4zg_ zs4exbor&zeA#ARPgU_0y7MCRUhAat*ZI(w7RG@2x8p2+~mMPU88#%-YsbZ>jALghu z^u!nd`euspHRwBYlP3ck+V=LMbB8di!a239`i@=I^uV&=UCJ{qFdhixMSnqL@@i3f zc6cmH1sDjogSnc?=MYmR`2ojMObTM_H7?R$_7OOka0fP!1Apoqd0w|WxB^iiI*55| zCe9yD;mC4cl*_jvl}?`BOrFOXzI=IM`Vsy#E6J|p4B=T(dip6U8GWM@DN&CaQ18?u z7IeanNuIx<^6M)NzK*5g1rt@fZqB|lF(Ju08t%84HvrL&=)thwuY5WI=dI3HEr}*( z274`^&UC_~1B%h6or($Gaohf@R0tJBkBDfn6rKWOBJdKYrGO>hX z;^}@{NYd$JZb8ziU<-TWp9!WyI)!1=$i}k_<4Y^hK_Fpq>aBu1!(PprEDZbC4xrD9 zonc-o{(n$ugu_h=MXfk1ew0Qy!v3+*%Dn`W)&bH8r*>uil7tm2C+g*WK)3cyBb?vL z+$0~`({KN2guAyBr$)Y8wj;8Z{wcNI>uxxcHIGp9worJKN|@5iZaCB2s9xJpUB#VD zLb~3?bw#fM6y=;*?5*0@oO8Z#62~fwW2}b6F$tcCW6GD*Um%W+-&q_RFSwNN_yoC_=;|b?9EWPUiX5LR0zFU1DA&`)|&a zr;r-Ilny^sErpacC4ahmxFv<)boW_zkl%;KShwJNrm-a8_@31^g6GT>eQj`!1oLtG zw_|ZY7R7{5tF84`wr;843i9iOtst#R_=~AjNE`|c#ovI$Q2)$hdIlE^r%x&FzWkS@ zc2Zh#bVCkA0xk2p6h#C?g}_HEa+T(t$@ZVko#IRJ&08<2pHaxsV%iw=?pOZO>BS1) z_FhEIx}9rOn>~*ws7;%j!259HxQ|p6E7S9p+e!0$4Gyv#nR2$YRgHI73=vKM3SxR8 ziZk9T>Z5_tw8%Cg94Rk?V-pA+W0@5zcIXpmi8l7H7ap`X$jJuIZ-rG9_&Cw)^&IgH zXz+%W2AhXjX>cl=*b)gIIG4g&VJ|m&VQ0q6(syzN*W>r1yeCLjaq{qhqKIG*UZxrd z0+FIEYa6TkMX{E%cB&Ve6YZadiIlvgS67>4aUbz#xVa>zm^*b|_6YU`12bXxE02ifxoTAfY#`8%EAAr_Cy4(P!wncYn0-_lTFTrNLdQu?F@IQO#E1etx~MA8o+WZ z6fMy#jC(Nj5c!wO7COpxUV7`4sUky3H{Qr;N+k_L=uUr!hD#d(pt%Q?J)}X zrL zbU(YjeVeWy;LqUI0|$_&mw0LqT2xUz%dQT)lCoT3rWHgM*=f2lL(25ayO>9f{Z-}t zDh7C_>5248$dz_Z_6HF$7zW}-gL<{My!MWw{e||Q_!)dd-gAy4$Lv%K!r=-K%Tv~M zjzpF>tirlgW7HiHs8a6$hEu9n7X5bD>!|kB_GW%uEp?&bp*Jn8&;{+uN(G>%dchWR zC8M+%bY4aK7q3p{R~dJDm8dEe_A1(jYmtML@D*@+2KB+(f zxvd``^jB8OepFqab(aMn*6P)>9}2MRCg%*~tqXldY&%K1?h++WyTZQF^kHN#Ys!_q zvKMK$G_9ov%Ih}FGeOuaK}3VArWt4~!Dg$n)X=GVo`s+$3wSz(lkLu=$VeM+*u_VF zgapHuja>fS7=O}|CzYN`o!k#R#u*xF(KggvW;KvoL``EWs z3a$|zlq4=2VwF-lWH=@6?b(t@Zagotd&Ngo%O4d1Y*I*DwS%Um9DJ09D3+f?=zEky z({O@PeS7-_9mZ;?wHF3ZJGg!ZOdai6;siB4!j|53dzs9)z7sBlz&e|!74 z8WIe>pinvz=jF_k#F0mVy-^#-2-XLtpuuLo&=9ftvm($dZ3MV+6vx)Gt#cTw$r11B zl_U?Vo>Pj=k;Gf7v%y0v1QHaUkqhs_O|>h1elEQ~mwE;0!i6hwX^!SD67*||ca*{x zMbbDlwE!+41=)`|hXV6lQoewU4|ef^S1Q@`xd~7rlWCdHW(Ue_*3bw082`a-NIwGL z*>?!$7C4E~E8084{!YKEHV)wLbQMx()~vGL$=^w^W$XyHm3Udp-RV`DrBHER2BDB| zFZ(Mq0h&Rx2SU(t0_>hhS+}%%6PC30GN;wYORO^eeS)Uy|2sBSEHijqOISB-S~MOa zIQ)^Wn&tGG5(V;mKZR3u{YY?-DOz*}<*{gq1B!|`mxlW(EU4n+8^QdEG7x_Jc=oD% zDPgrpl2)|*vxdb95@7eF|HmH<^gvca^gooth?N3>lI!JU7^?zy{++p>-q*)ms*F?!*ID1>|;iu!TDGecr*7L%{aY_ zzMGbzD~io+W){L|1`YW^b5zTJgzuBxPClUIp`~~dLP5ecO%gt>+XsJxnMqT0N|7A< z5^>;9w&-Zi1?B3ReZuO>Sccr_i~gGR|X+n82yaMCde?`E8B z^j@k2LO#yYFopIslpTJY93ge_wJdiaGy29%CqFWg0488#MWm<}z$0~_AeA#L$!(r8 z!Z{s5m!}No=}$;lq9Meb@V-O$s7)YOu`-buE?&rg?Oj9`$)X(3BI%b26%>3NR4Pe< zwYesmU6e`^9!n{X>zpQ%!-||HQWjZ8w)4t8bP;;JC`}{_SAqdLQH=ozVgN!& ziFA3FnxT@ps|I<*bZJ5~p$L>%%Ew_2LslnvVF%2Hm|ho!WW}tObRJu2Q%fG#KUSVD zz!rPUXaF19@4uAPeA9z>9OoB$Fra{ey$_jEdfQ7WGpOg*uy-M|@x*?b9 z`y}<#7xTfA;D#J1jnLOjCAQb1RxXp^71oB#_5yBjPt0H#=?t6`A&Ju)3k6RUhjGzQ zAuV`>ff)r><4v3t^coly=M{~xFjpg| zyqq(F==?0r=*l@GL%`S#0Q9eLct|liSxj<3wJ@%o&W>R@y`UJQBCPiX+`}cDNzctaimzkPzHFjT@0>smy6hWaJ^;i2#bq z)t(qL7&Qs>7e%AQ)r&Bjr5Pap7m=bNmw9|(j&eg^sF+H zRa+4k_EE6f%@};UNCB#xPAVKJK=BMt3Rc%C1?$%k+o3;UfrEWNTnbhipE$C({3!%e zN0jD$>==$ZC>(Wco(_(OIyO(o=ILjT}^M~{E-}Up!IF~;BY!nM-$;OuA_V>gPATzW(KI=NEPRH02 zFeMEx7OhPZX;C2{VM+2q)v#U)#3X7Dz7NwO zF))nUX-v}kGLV&3Ss-%tHWWp>cklbZ0wdxhuzT&x=vf*gf>V2WzGWP()fmWRp`+g{ zZ}npYP~zbeEc88vHsTS5r@N0aR0qXS9h;|vV-!Aq44w`Oo{r7a!NJp~R~%Dv9*i>h z*gPE^Jbm&QemW@p^r>U>bhORWH4B`-g?5YiEcaozJeZsf$bZWuY(RReYsrU^Y~gTb zYFMu7!YPLpTFA<-Sc#g`01zjcryV87gz$S=IrQN|TwYPCV3!=5kr8Dr z*N-fxzioe9L|K{PY}&h`9QL`PE<8I)?~mqMM!u$>e9Y82oH2fX6efH@bScfxL zg>9CK>vmf4AGH^d&nog*4M_`FkGiLN2tV#>mydj`f|gO{4U`@Bu5M?3jajD>av|Y6 zZ6j!fch$x1OnccCOzn4xUwOZ@E^-qX%b}&)saD4X6+ET7Q;MSf4w3uRbxKjTU&@zN zt6W(wjWk!j>^TROFLl64dSPPp*`)Je`GqN`t}3;qtgTT?-{KPhQZ044wW{{vTw0GZ zWA$98-|e>#ZqspT}f(=H&4uyn1quGXc(7wAHr3t zVRyuhdO+O}8lrF+o4QQ*rL%HI2aaE_P`6d#FiGT!eN1&AuBG-b z+QZzTapOCDF0w}@XkI1!VQ}x6i8*upe#`@!wz`NOblJD_1o08xs((JM0d$E6xl-c6 z`w7ceGD-A;@&n`;(W&S9i0F8E-H>g~HyW{P`BNgT{b{LBNg84;UA2{L``ViFsbM4D zR|J+tUuf&%x;eY?$$8SD(8;rO{OF0Uc{~r9NUwC=?r;`u9M^m*ELzhP`QCuk$fpI! z*=45g@@Y|kg9V)jM3qyEd#%i(*mv@IFfM6BeI!FpKvDxB@@{eRQn`cc9^5VjZ})KS zwZe_yEf=)wafcbaS#CW*+j=_Yc?a6mmu7OlWbNbxsUSkmd}ikal*lD(KJ2f8$6U{X zT9|h{tE$1}?3sL;Cs}B9$9_`xq#YfLe!ppfyAf;V@cQe1yhpSKB&=;qGP1RU8&A+W zu2Yx@J=ckd^O#DB`j|hGqrw7EsOF2{I54e(o-J{ zLIogo@c}MghgX-jEUsig5DU*5!~=Q8F<&uvXt4#)c4dZ&R`OQ^dEq!KAm*w!U(}ix zF7dTIlwDDw$tx|#}V$B9y{!R!Q=Je$QPAw*r)Pm#p4c|npDvqpVGdd zCc=Z<&+c7%p5DHD!P`y2(Sh3s@3-Txa`lW2@6_=g@Z$ZX@&HyvWlKTf?4MQ$K`CDJS2TSM#FmvN%~L(eGOEla9{kaPAS%& zUdY?2_w?fAXBN_SlbyGLVbT>$y%;84ucZ7*yRJ^2sP3Paa)wa0RJe#et;5I`%|R?L7(wNOQw1j<*Y_ zl}xX_$l(nfEUcf+@WHDhs(0_l4<)LXZ@~nboP8Q%+duZGVf_|t3vU`|EFNc9<1&0kUc+cNT+#<`N*=nU#%>8gwHxrR4%6ST;Gsy zM(>Ed;;d3=WB3NGHQ0@Y5&99ivL5ev|oA?0kFON zh=Q-FpdSUoWt=thDT;E8?>9&9i>e6_@s)%eEDUY&p1takJw^L8tZz#Y0nyEntw6pk zw1_~m1VRKr;B48!H&+G;VB71WBLJMVQ?)yU{ilS-`yKW-;-3@tA9UD1eJuPh97sJ- zBXLnxd%Ze1F{*;y3%a;brMUZwn|VXV>d^y{0~1_{R;r5gk}MqZxPl@`EaW6f*A8LO z^%yxYDXoJX+&LnkV7UZTu)K3x6%j93UKre9c_%W<3!fo450*Da14Z&iv%GUn(f&bf zfR5$ui{+hE$HekRq8_olbK1i}#`H{i>cp>T$$vjr!JV9&NhN;!(;Rt0{31wlDk3h} z0-8~}+k+Yh3p?S8po_wz^hp%Zm>6$pnC{Q9$|C9|ZF8Yi{cbE+-Q4wtN)s06Yo8H0 zmCE>`YDI~}Jib};7}KY}K}kzQJ!eNLpG#(}lGIoZ%4Rn0%d3~>LdX^$CeDd371b>| z1w-I5soxQhExv6YK+p~b=_yh~Lk6<~x_pOgoOoL%Fqd0wCz#k32vqWeQdfFF4y@5P zYsBQ}Nlx>fyEN)gVhcuHa@L}SU0NM{g}SsZcWL(61dY~g$qBkPF=opI66`5`1~Q1F z{Z!S365P2|C;^4wT>g!R5>P0G1iNYtObjqjj5L2vDL8}JaR~eoBB4OPQ~VA-Q9E1g zqvdnRv9ipEZe{ro&j*MB1pMHE&)hH&77a^Ez~|gxsfGoV3mZN^orio3d=3CUf0Ph8 z%0;oOn&JqrKz-pfj;y`%miVj&$DR!*h3yd8lAG-9b1t4IBF=?&lU1BTGZ}8OLdo!N z;!8olENV6Qq&Z=+vruno9L&Z_uL;(I+P~pxp>z;LA@vDPjAPsr_0db7G@gJqm5XQ=Bo>+_zyh4AUt+;6?Xn!4fJqU-!JI25K&YXMKmf6s3d*dZ zYf7p#B_%^dUQ?dLA?Y7n&qzWtFV-~W;7u!GXvjhe(dGlKyN}w01WKHCYoRmy>h3K%Qg`=Kq>e(+&>~Vtp(TchcM}I4SMYAYkZ_}^lL44aJLedG z5@{1H-RQtvZ97>I)?v@#B#^4I{Pfk( zM38PLs4Z=q$YY363QtQ=(+r{!)2x~mK|mV`Y6~-yLmb8gH35J`LG)i)NaI454L1}* zS{4qO^^uT9cD*%XKB9~tszpevLP+~?n!|J|H^p?dY?FTACdu3NV2@SAm7!tK0$n>U zRD%Wx)GOV>D+_|2k|cIBHC=o4tpEjIZM%*_QtLh2fUz0CcovU2t-ETUwSDlKRp4?4 zvst^AY+FN*@wCDHh}nE?g{%cu87U}OINwF|M+u@r{xp&5atbODlLy6bqOB%%8s{QX z=b}u3H9?;efy0t`2jg&ByicO!d7o|`;g9p;{eiZuxGnmHDT2g?0z;b~2%4fRA|s`k zF?cwVxI5*p#aM?F+eX6sfWc~EV4y=2Yc02Pv(v>mg#z`5Z7nD&lug|EZ>d%E5-pM} z5rlZ}o1i^7MHEvhc~Jpydx688A!@4$%H|K>E9VWs;X3tvgYGws?lAw<61zImgX5|Ie=*{u z#-un9Q<|8%JyFJ_Ui%DC5+X{e>&+N4xkftc>1P8I=tb@cBfBsFsNfs=d%$Wp&=UdgZJI8Zkl zj$>2QP0^nzcxt&R!l%#qh95*1&<88^p_c*)n7j`d1xc^;tl&({n(4l=cxhXxFjBw8 zXiE$JnBT;mM6sw9b%Nfy;s=UUhKWTWl!zuamW_3@r9J~bfJRPxM)iEDqP$gpn@Utu zv=x7a%I=X)yPq`$fCV2u{`1Zsv_9acd}{7Gf>U!0_h>_1BZ|lGTbIH;KjqMKY-3f+ z;hbX|>qryz4#M59lhcf`STq!VXTNJ(i63OJK$#H|{}IgbZz7dXY*PlW z<9-us>ckC@5(v-3I;j2ER_IuE_HjNh90tR}s0ds4y?mF`_f)m;dx)h@V088wp9bIjTfa6z>?cW)P0Jd_9m!UjlY|X8hgfTh1 zHU~ZX9*OYd(7_xSa)|6#Bj>j#AUo@Xs}m;1{wpV{iHUlm*H<#CY~12vO6f+KJ_jt+ zgD)$SD`ddGB>#uKw;PW8hRz4A2DSGua2jc)n^au^A*zgzHK=UZ2Xd%jiSohyZpXEB zFYjA8=6qm%)1qF|yOA4Q*Av59+lww9H~SQsurDn&Vfz#wo3IR#36d8gmwFnOe$ zWywiQr*--e80J-)5*ZBkdyd>cmjoR!L`b6uzy8&c!t^>Ukn+*tem9;+@)H_-J)0tV zfUYJLm5?#}^Mq57-WoGQ$^|H!WRpTmW`Gzub`Y2_`(UX1HZ%UfM$L=kJ z^9l9{JTMQzTj(I&U|4>1G{q1W2QU;fWv&qbPX0l(tzv?ffXbZEj}mf+F7OYMyOEvQ z28ZbWlD&i?i!Fd}yV_0vvZrX*J{CV>RnY#WK3{=V_e(P^F2_bF^ibZ!iF+l;L^6fA zQ+1mV6>-^Qp~Yg>`4PkNGj=-w^CtSSiS~1<+5W>3fPxZ?Pe_K09xi={+m*gcc7loa zi~3|XN1uSCJWBV}Wyonr_+e9f2hs+{Q*}Q${^8`>K$l9@ zguM>6E}4s?NKt0E87Yj1JW#KAkVl$+65bn)L~)00nU#%IoZu;REjIjqB0&_V1OlB% z!h;Qw$YA?SffC!}2B#C6v&+=TK(G17qHOs@OIi-z%BsuQg^v`|!nNKQf@gUr;9+#6 zjqN*n0i(0ta!9ZIpLC)PB|ViWZUm&rOUMU;9p1;K*h@{lao`iugG4Y>o!73CgFzTp zlCKtDvAW4O>!9q#G!%Vb9ivm-IH|uifd?QHJ2wRWCly(4XdNXCaS0sPz(XoCpH zv22R43h;`hG!;M5*?H1fXqK3rB^9KvsLDy{D8{#xu}LJp1_p5>R-}R`1sF^uz*q)^ zBM}>$(41b<_L=b0VIQxBMrK)2Xw#0NF&a9XcWtSqKEP+l_ExJ`*tW78g*@!2go+~a zTT|y$V(MVvU_ky8?ybT!oFJ(!Y4+rdbB7aaL!Sbb5*|n`cdgXXsH}cquu)Us9gQLH zqNbjGYgLsO*GeBC?-f)w#us#BYdfXg&DEN~j=HlR=d=`IYY!Eew;S$&!33vf)9U{EldzprZf+nbC?U3 z$bBy4%`@jdXC`#Ey|j?deLmM#)MK`f{O59kiR|U+qm=*LU*XXz$25K1Z=L^quC3!^ zKSwUmbG_E@a-i#Nf|y>^zL13IoRXJ1-3Y|nQirXT3_+&i!B=^x@z~?Jn0-SP<~zCa z_L&s=c6xEVsD^w5WO#!xeWCQoWw>kS1gJV~EX>eDFhT-^_^= zf>KsNMa*znS*36!TYzn;+a&h_f4zPS=34Y!@}rZj?s&j&ldtBu+Tg43rt7yiU3IH^ z(vWmCj)JQ-fK}-I+?h~osWZ8lEn$F_;+9ISE@MR;Xd2EtJ zIL&KfbP%kBxNw@6h)F-(wt8iJ6At^}iBa1;GYjrQw@ry1_U$GgZGxS`O`Z888CS#ZpNfNlgbQ%1aK5ZI>N)Lqu zSvf%++u9q*Z&- z1n;x@Dx3GUoK%P}(1 zB}`6OoMdw1G3zPEy+W8do+o!y;b+N8UP@zM!^w~Zi&fL@l(2@Nh#f!J3sILRt%W>} zX^V&ilIV&9*jTOE!W4idoEBH>(NI4islE>`_t|pta*~)K3CUdz*#e5!-n{$PqTMLh zFcM6RKV;>Yj;gKH5S2M;_A;(XUNT#q> z`a&F|p7$}J5Nbk$pkAro>aB4NgC-M7w;dS}&jsjA(PRwN2REyOEPi@2fpD#$3$cla z@Bi9Q^|lY{)~i|rx7I1hU^jkP*Gbke5L(nV^aso^h#!cI5QLaitw?*_ zr*DIx0@K1RYsSCb&qufTqrBVzRDx8dLw>=kY7kWsBqaUa75CjH{t8qm{A1U(=mmY$ z#B%)hy?R4>O1cFv-|m#%jc3PU_#`rZUDz+lq>wKLzIbh-;W+=5;)-K#9h)SunJ?re>l9WbL;6VU1}P(iJZ)Qr!#>%?PZ)w#Y5$m z@%hInI>P%=6HguheW_Ea_#qygcLuvd_Q>r@hEMeYFXCZX3QEW>;%S0|kV z@E5aaU`=2f{g(u#4{}fX(wg)I?oq{DSNA}#Bp{DxO;F|LoO3!2!34O0{B56^W$l)%LYwccQ$heA=&weZIgt_pfd2Mf8iqvLr51e zGLtl=KLEvC7wE?eP$F&h&B2fWXQB>7*;!cd710>t%|Vo+Sould+|J)m9{4=R4L^M; zr_-80ajzy;FTf^X2zV(;&0rK|oI_&o{6!h_WnWCVq@5G~*7${V7U-us5;WCr&KPC2 zFYpDh7dHG1QUUwfB7tTb(+$mPD-EHqbHiyo7n0@<)H2iVa9RdFfUJBa4kPm`;=;Vc zbcz@|UV`Fe`&GSBHGTs*CFsWK1?d`^sX)-=J`&L9v&#Bn&wN6eB&XwyM!C{th&5x9 zy+9mPhD}C{(wCwIiAq{II%~0_65UzYUv0hi>g(wc;FK2HuX|&(uUY&=x_3!taD@}I zFvU4x6)4!>=S%`9FNItxIgtp!^v}V%3(;^s&JNT8ic+j<7}+$qF*MBG7#dEFp*o{o z+lU|hffE`AFUge{J#L_39tsWflhAN%qpn?{;Zz^v%4k@Ry^WN=M;wsw52V8A&DhG} zEn>YpoSZQ5Pve|?1_g*>rGq4fdN~oDMRW2fsO8Y-hRsc~244{xH+J53(?CWB{ z;V8O*ADb{R@xa8@I`K&a>x#grT@NRf5DhPyd8~y3twugclk-y=fmjTVXqLHg zeg<@)W11zg>-c8H-&xHMK>;3`8KJgbGp+4Ct&2DgaYmPCH#2L8({1r9oVQ$@aki2n za4SXLY%>kA6Oo%R#CqanGGG&9evmW%hH7^WUnS13n!T)o{m;h~prVTPzK{wl)!BlkN(Bay*tT z2RS@Q_5dSYrqwK5%lkHyKzE#?2NfO2HBAYmWeplyakE1SKY|aC5ZnbUeJEe-X;y@W zGa3%VpCUSm$U?^{o)HDuWDrB%A=x5(8HRmS#O(UzT68PVJ#?$2#Yni)%|5kllK!g` z8(13ABk&P8>hxlBQ*6U<1mm%dLc*);NvLdwb9Bj5#q^k!^;8KkMHk+8$(@nKsGAZR zk=kYUkrEZJGsH?)Hw7jITvY*+t|8;YC-j9Zqji8&c6N^Hf+1qwT@U@Ta3ddVsBY2q zfDzebG$j#DkIO3iP1GDB5=>9qgl@olNc*30M&KUvx_G zu9u&IiE_0iW!%#EVB$sXap#_QN^#Sbdh(0PidVM3tPBJv{+bvQyd#IzDa8jb_hY;| zDXK5nSns|33_$gRmlL6|sVI59ykJi|`TxA@Qfvt2yRG3w|NpCOO*ZDXN6Vd;v#p^m z++D_%gkIQ_8!x-7OHP$)<)M+$iS5l}4}Y<}tGk0MK*Rg)%Wjy5&7$FRXBW@Ho;(YG zzvL|J{-6f4YKd96>NRKbWK+z-^E3-5kI%wGSNid27H)dgnasjNSAOhq%)+X6_bm%& zfm-t{oZQ|x_Ha{sb9cAzETFTtwqz`Qs4cn;8HM3>q=0IRXO^sp>;mk+T9+(fzT7jq zJgqC-++A(RS9-s}B69kc>iH~a4M;zu`YnE6Q;wOWx705HBx~|~Zg20t>$al3`8&YB z0ur|m-m6X4?)n&||DK{pdF7w<%1xBmCPAT9DSSM5&(pYDb$amvwdDX#691~2^yOFi za&W^yG_ZYnh`-29foW^FOJRwFnj3Mwhih(raq-+-z2CXH{=jpy=Sa;>*3ORXKP2pB z=^!TP*7#$k=Fn`1tN?5|$nWiQE*8JYJCtv4e+Yt?)S&!ia01fjSjZ3}hT-4|I;P0` zne&8xZ+h4RNBBwP_QV!*mkaqIxL|#il6#6)4R%+2J+q8g*F3LK-d%K|ozEx3@o1*E{=9Pt19?aU`4A!OY1=ALE?7=W1ZT*S=RjCk_5_ zY)<|{Mbpg5$M&6*PwSb4Rp#WH$2KQ)_~7T{3Q2fS8Z_o1Y>2cpr8v*igi9^soyvTl z2H%4jqZyJ?uS|RdOGOZ?%Qvu7dAuH7pWccr%2aaJ5bC_4Iz#MjA-puWC;YL06{`6u zB@&+0ral9k5mfovihY!2cCww3x(WTH=sf-^f;nVP@c@2CZB!swJd4+9;kT%WpXWtG zN_MJ9dP;snokdj-60s7E4>23@;cqolyirlju0I0VC|~%l3bq`>vI6`BB4WLyKJli3 zO>0m69XS~l=h&N3;NNfGbGX%dZsGcW4JNz}eak9(w7>Ohmj?rFr z8g`FO!}FdT)3E8tO#_ZO2Z+SKO|Xt?TPuwf+?#+KWIK5oBy5=! zmt;9!QhH^LP+vO3vQ#0OGPwX+50(v~!rXIgUu!=hZ7EAXD*XYLKBHz0oqK10NkGIc z!Z2AtSN6&ZYmtQ5Psj+VBQS09odEJCkuCHC)hmj&*iiiS54J)DTGWV4E6bZH&qV0p?%TXnS20-CzlW3b5OL}e6B0ZZZ>pi%bFr9%Pb2zsu(WjPx&MU|Y>eXN_oV!$V#Q&8s(@ zjj9{Alm0wu1~GzqCQ;H4f1hGhfa>+{JjY(6-k)mRh8r_IVL@zzBo5?tq!kkur zaGkOZ!aP)_n-<(uJ2OH3DY2nO9eE7RALu3KRGJ}#Q0w4aWVAw&Q~)DWN?d7Y>?wpX zUR5FJHs#Yx`Deo@JhM!s#6aK);9)+I=IJH6(|8Rq)Mn;kg^`^OR_%@l>vP*3H<`Nv zBS<{vEgTB!bJ>()`h49!`akOD#;2lRZj>>h&ZHLA2S(mO@8abh5BZECn2=|d3gEz} zMD(J1W+{}+9fX35QEQXuwlfznzDFm|Jr5Mu1M1W^cq6^L_}lUOSs3$C8f8AZC|M}` z?S=SKB3Q^psy=tvUyGmV&CC=7siw8$L#N*n9=waL@hY>R|DQ<&iEx~@MNW9aR$xA8oTENyyjJ6NG_Lvb`*2{-UD3?G@L$;1+(O;DB#jE&uu|v!vZ=4Pr!5aMMws4lZ(>Z%mwK zoTCJ4tJ>?FDwEvsaG}g73}4Ns#4V38o8x!CZpL}+@ukvfhf5*fnGX#Pw1yoH12RS^ zO_a>D;7H~LfmtF+ZyV%dk@E@srV0}%vdiCjECc!kivNuM*t{M4QwRC$^ocn-}^Uoynl1neA+ z-+6w<3oaF@mG8@>#wqcrZlMih6V+#;G-y*RKv_2?WU`zif9#}&F}zbUUToRNZx`G_ zRHj)n(_Vc>q!Xi)&3*RAWk~iZot;sJ*rAZ!dI+*$YH8}h+gca#O1O@y^-ea?)_C_8 zCZcNLeaLACo#2bAJq;mj)m#B8FBuWaiH3dP%&vA{tJ;6JeIt2*zB$&6|VOiC>&5aGmBtGSCVvcS&3 z=*z=BJZXCv9o9yfg)G+7>(MzmK~tF!!b(&X?{Zp>49gPJ1ZD6P&R3_I)}ZP26w|`= z;l1R~5yXKFCNDTQ!MFAyu%782L&rZ>lcF}bfw7U&U*CDN%Hc?_`$KB-qCh)o?!WpFHFsGiWfZfHf%Q>X`r3yHHScOWB~>~ zE;zEFZ&H!i;y9a2&&*Tw2ls@+4Czl^5U@*VHxi_I>3on zetwJZ0XERJEBhtxQ}&)xQ=IxC%*0i4wDP_c4hrYC@KWh;9m?%F-oQsr`v9EY7qQ{% z!b_NDPW)ufg#h=Y%H`(+hfIS!TVS2PcBXs5{m=_3OM%!dwjPw7y$b30w(v!GF#4fA1IK7$KzkkZ!^gt5st*nnYJiK*~aBbsf5DwdtU4Tds*Vh z3_}7+AB$C4m}5oN29FIUGAE6+#l(d|5O4}1OYAl9FUldd5ENpw^Bu$`OPTYkaqeMF z$34h}6PQZM@OnuGEvq$hcCkzeuTvu9+UtGv!8kj7q)! zNU3)e4?UYdLMOwvk~D|&lWaBo^ppD9i$5{$6J@K94_#9fM*I2e?5xE#SV*o^AS9_^ zqC)uHbA=-G=GXNm(th_Q5{X^|!eM=s+$Q^Tk!Z*QG@D0P_Uw`oGCaGu(v}kYhRlv? zQO;vvK`>Y6*~NA8?8<(tPSmM4#dOh+(+e=Sm@Tz$f30Hnl93Mv`hMmkxJKbDDnwHV zzNAa+Fx$sIiVllnRmd*{f02F6Z^B^0@Ch}E0~Xq7Un#5344ojbVK0h4Ds}nmvnA;6 znNi^%s&ExAli&vn5ibk(EnyWCwJ;?Js~8O0sGA}XvWmITJ)(r0#a6L;_Te%Z1oW3O zc~n8>#E&^aH!NEAq!EW3J~ntVLR-YF!Qx8olJcc8y!WjXvCtwFQIm)@@1^BcT^>8n z-G=dRe8BO9BBAwsS+fv>k3or;FdB&rM#64D4|M?mdKCcRQF1pkhoNTX#9F3i=0p zw2ccr@01`!Eee66R8OwRcpE}x#dV6Das^#K#i+!+Q9e_!>RDny_0#c*e!HODwKYXrK zm5P9`wEueTTa?qlt-ZHDU(hzFft?{NA|#>BVW0e}-6JKVM5Fd*dhfLE;N=^2{UCp= zd;JGV1)u{6WMA8ZZ7?tIS(F-b;#0G2u-?Po;70_GVxE{6j^|?a5X&w-;Lb=)nH})B zuY6F24LSi1is%A3Lb!tGu6;M15;jri-z_x(Z6gEx@6fCjr0|ecil!v>i&3wo zSrHqGX<{>X8@B6In_9|K?Qd`F2^CFkE72dZvRMO7wX`TLrmFQY#pkPPS4`{S4@CnB zagt@Akawc7zD+fCK*8+y503r4@F&Fnj#;$(=i-NlWr!&k;{g#7s2?ug_Xi{R&g9NY z%gp#?I>(A&>FKwZaEv+@vumb>M^o7jm6eo1kqqq+E<&Z$ThR=GDMF4Q$7ai(RY#FQ zL^5sEVo!r>kR1IjL=Sj2+4G4Nne16TMF)amld*=EN@io%G~mFu#8q_!6HS2foPc*Y zXv=m#q!RVojNSz5bSJ_D(1XvR2JdHSZ+xl+cRr#6u1mNTp~Q?r#(V1lMxNY_%37=+ z;*nNHOdx)5|H?Rs&%B{^G6st#9JgMv{UV4waw#ASd%~`O9TJa`tLJ^*QNv<-_)+In z4Ql$Rjf$ph8f;NqN=Jth7^U!><1{oacB0bFg~%8 z%t$qEYPDPXS9Q>UJk;Z~dS6!QfO|^o_PhCoUfaFfZ@-jFCwy>yshxw0e;S}RSoVvQKOqu0tUg`$f~PG;PFB7p%u{D;OprHmi>iw z7PNpi5>XJ|!tnI{GI*fHy5qoe+2$im#0nyaE~O==LrfluVnGq@qAvF4fNg5%gHqhI zHV_;mmFid{^{PKMoiXm`_J+GZ2z~&8)Hm7F!ez6I& z$OYD!f+|W&g^V4D6I;QRoLJ0Oc!?=kEh{N&TjlkogA0OnxEl-B;R}w+jz}GT5T(Q< z)NLu%5UgE<>F`8hI{b)@!DZt9n(^`~s+|!u!>nu{Tq}y7dnZK2p;@*TX?po$Q`hy) zx$7!Ett-~CJi#cXkR9v^IWz%YP{Yz#^S{``|1Vw{MOoFz6F`e}zu_RRsUl!Pv>MGF zNV^=oy+$U-EST?i6Q66|;FRJdFzrdiAuwtFwTy%>CFs*rZn*ik#94W}R1q`u#D#Nz{pAQ&${ry0eMkZRGWcDNtud z)MxOOh)JpiDe#k(9^3M6FgM4jJr@nDMfy2zjaLn$b`AP2$~tm&F`)g`;{AI$pH2*z z9>#$G(&(hqQCW+e6J1)L-myMIi$&)*`UHt393^2RIQcy7X@CtnpquzBETwyia z-hU@c_$HD&H~oK)B*CA}gE!1PyGZ~2*HFH~QIzywJfUX6CN!YHAHR{0pM>lGA#MI6 zwEj=R;eO%>g8P$jxPKuv)6uiKM-rJm30L@bh-$_auWRIkz zkrGat4RmgfVGjRFeV{<$5szn`Fs?Z;eu>+Zh7G=|q9iE<&@SX>(obM!Xdpe&88eoH zk7GxtYx0v-XbNQ>zCibcT)Yl9Op5*j1O`g{{Cf&X@I8B%60ABjzF-RdgCN zk>o|MSx!<@Ghv9ASlkx*JtOlG(kx+5^Ny>0n(m$=u(M}mC!MeMfT^UzrGk-pe=u{SWPkNT!k@7<_sC6lxEqL&V@~n@(X7T$K+p`ZRJV^1xur~5*i~7crAkRNXAY$q!_skqPT^+^r>2F}Xi9MIuIL`rCHPPGfP%5tZ-KUsvf*+# zfR%+yYs8*>sl{??sFc!7z5?1Wz&{I`z2>xaQfeqrID5A zro)uu^1bv*nkg%&mP>MZ1(v=X=L3Ty3<28z4Unr9`=%2f)icXifG0&i3EKz#7^vj| zB6Cb4yOk*rkK>e7BJojt^x7-pl+*{(9Ep?97Rh? zC3feGQ4E$1_9@2>Wa0(aXD7;`D^WBIs4NF)1Pr_oirp-k8(MfR!$=M8a)v({XTm2 zWP)i{Ij1l)e<|`(wcmMUPtl%$ar4F#Bp-0vgEqG=*GM#gKe5ZQZPM!m(bChK1(Gfv zsQ(w8T)|#M&uk|lF1l>2q{t*^;bi*<57S65=S$^t>vTI6b;`23I5F!R_0?qlN;}>* z3_87w(0e)G`e+OV8tR&r4!#lUBuil(Jm%LD=0Z}kXOECYMU@}fEozpv5Gd*86r*}D?)7v&8Hg)9m7H7uznRt#28O*cI zA<_s7B{AA?9n6vS$XY@cvBEps+4y7=!OkS#IdDEE(;ohZEM}Zi(f&*ndLz~C3FJ}P zSbXEoZ3=(YO6&Hk`4(L|xA)~+uJ84=e3kBkvZ!`i1LL)LS*Q;`qrgMtI`Qki!?7jo zHsSSaUcbmCSRwS4I6}D3rphs#yaiVt<~62(1qNT2?aw&wfxN@yn9*5=2vIq+MUQzA z`jb(Af$JKYdwkLPDBA7o@1JChRH_e9VD>zTz}SS+$C`mL<7J<|h~dpW$GUm({L~xF(Q- z1eVFqR*)I7!6F#=#701$num2T9O>;Qm<;V0;1pzmdzXGz2MjC6B$2_GJ_XUJ6h_ix zAJeqx#pIji=z`$HHA}aEU);dkPqx--E0TZ(AO`=b$X;Pqq%lAvC#D2oATWWn z1EsZ?ivp!V;@;ZwhA7(;Je~$r=)e%kwLK>$Yfl)Dg4P{*65AEt)gOMf_q@1$ZStG* zE!Q{2H9ODT6kh#7K019$fm4V(70jg&u*~D^fP$20%fwi7yadZTssrc|uO=y9Xl?d; zW#|V2u!XP|_&;8olMaOn1b?dbb3JQCig1~$@m6c!dDR|)HRj=$E4qvM-XmfZc!~jK z%)t85O~4&3eNG)S>%P0vag8n8K9*I2$wIqyG*nm$K10; zN_vMbpcy{?SUrwC0RRtItlr@q>7TJiPu`J!BT=OzBt3n|P<8<6>460FqVmku{-me> z!cJqApU=;Mgs1H?of~`~IQ-yy4U*mATvQGeI*j1^nkrZgC8P_^5*k^y5BO|=_H#c3 z%S;ZgX7?FCpdg$IOd|?lg&~J>NkRp3pHaP_sj>&$^C`LZ>RTy%A%)jr{wH6<1^PsD z{fz1yE)ZAw`vAF}Ub9NP)`sf&^QC@%8;uh`FHq`z@jX_(X8a@0YN3E)P>RHNPuOtn^7NA#?l;C+k%_DYV*KXT%}@ z-4wMlA`QEAjeXZB7vmVZ9(w3XD;z^tGU7CJoP@-h;sYcp%6H_OZA!G#prI+b>B~3h zQ5DG6f-#FO23JVUzW8DamS>dG2xt#>?oAalBq1Wie_hEAfZ))3?phq1DIfza`afl@ zJ4Z>B#mCK0dowf7;?7qj-!^S!{D`#%D9wGkSsh2JnV};In?2nL4WgQn>XG@Tz3_B* z01LV@vpiZ#HBH0r$9AfmB4}U1ZIMEt_K;;kTwqZPmi3=GL5i@N{b)Sgn)|j;;{hi7 zg=%i_O+59b?GS$5N88a}{u-*cuA|=_K4S^$N^!Z8(e&sV@>iB&CVfJK^g%)Z61PFSooHp525}7WRf3~l(@X==jn08U88hi!e^FdA|-3qZivVG*T8QMHuC;pBiXu+ z$4jV58;DMSQyRg+W7)difj`ja=okiHk$RFUW-Lb=w;& zWwfLCs$Me_@m07TLo9J_0KZSRzY+N|rIHcAKsyd1zG`OK_^K}-)&auKEjhxMI&|zn zBZQ6i5J`hHoIK(@S|4bDn`3}y z9o_*xI6^yPBHe}C2bU#LR5&C56QWdhAtbaN7oOiLHx=1XhV{zHi3xPFML7}9`o^av z>ES^-a1|1QN~MUe#H8t=%P3BM`fIE{mxxsA$vRkP`*SS+y5GyMum7%W;p zh0!B!6G*GVIesWa8y7OTRORUM*Tx~GApc(umv6r%T@OvlA}!-#QAs*XP9phqR+OyL zy1wtTx6ueEXqbOg1O0XvN4$!KM2lsO>evCTN%gu@yfDv>ivwEM+oXwcGtOmQmf3v& zQ+27A9r0(5I!~UF(K(his zLy-3rZC9MBcS`!Q&2*VhHVLkikDAP(=|l8u9})f{a-9a4SQ?;WwZVQ{ zL3#z*0w^1IjufOE}(Y;1zf7)Q6mj-uC#;7>wq8ruR!X`YbIAJAPI&ij)NY(yCkL^#Y zBR95V(D{>q&Lg>kPc(EM$({ENv2VDTj{H94J1Cj}ayPF7GGlWPIRs;qy~0i6+Hb!y zrM(;FtOE<;^}Qftoud|tv5a-%d_b6ko$}7A_mi>CU)I7TB312oa&rgs8FX#%_DbQa zbrAYeggy_|2Au^#)etJuN&UkKX8#6%a+29qa6TU2!ucG|7IL;+@8%#-%4IHYYH*nn zO#0GK2A{-lMLcW@z7WkLg>UJ_8a-QRF;f!Fy-4)+^oY7hHx-}b758}38P0`uQW zO;&-1q_DCUj4N%OaN6VMv3Qd@N5Ou)Ny(yZM^QB!K3ZJhJTD{xZmESB%|$!At(+t1 zp7y0Z`V|y6FHv)$w`y`m1wnP*su6V=RR!{}&;}yrj0=7R99UvpxR||-U8frT< zc|6{``}Td*f%m^wWn=2Vw_T%xDR5R&2kuIEz#ihE49uQADV)VerBsN)_1eKZ`yp*g zl0>1vgiE9}I;7f+JB1q@S2Gv6beS+7GKRXfFp?X0F)zz+Y0sG767TgD)lmz%pVrh7 z)luhMjSUad_SETiV-16qeV0_IMRJ5ga29VI=UPSJ%6L2nC^-w+BbS1~L(|A!Wehj} z^}_$O_!E3+8GnMMhx<;sB0Lp`%lSDGl?MEw5-AWI=NwZCrb0mjq9A7r2x3*Qh$uOM z3S@jkbt=A(0?j63CLJqeg}#q>2)#au`Oxd92(y|E&|dp_Y2evm?OfzNRj?L&ki;5e zU>=o;{dyJ{$6)TC%~bZYTO9Fq&#UjCd_9j7us5U2l%i7`E<8po!UEJ&Z3Lx_qLsc? zh8WInJ_#utRLM86Ph4p4awZ18-6u&Iv!Xevp+j^0cmyZWam@s5;lyCC(tInQ;fyY4 zo4IC1vr?DGHG^hV^E6!^->hy0`a^lD_jpnD;j^KN^Uo|WTW2lJf-*Q0?^>+IJ@%w$QKvtkrvs2^adD&<%^ z{x8;kv3C%@L1j_ZJ?W+Zjsa&ZO9{vijyk10d?>}F!z2EOLy+K#UV`FtXHXgJGV_AD=)C2}iNDBEF;1+@N5wcXyB!td#Cz|k7$*j& zqhg$R>Kzs1#9QsC7$+WEN5wet;W;YCi9gy=F;0A>kBV{Px^YyD6Q`V`Vx0H{Kb6M$ z*(8^xlba;t5Dlsy?ByYokrX#+lOniN!YB3!grQ+Cq%4dQSUJPkVY8@oeGRI-be2HM zi8yZEv;Zit68@Jgxj2c~@lHpSAdo%>Cl0-!wjAAo&Io}x+Cmk;aX$;}!+uNVi#;~~ z=G;{nYI+hvQ836t6Q{c%IinuX@KDd$N(1+Y>&9D-)3^#5=y}9tPccBvgk1;F5+G?$ zkp0BTDi!OAYD}>-{VtIWU)gmg5=`b4OjgwKnrtJ3@5)o8*Ugxd9KD(JJ;;wzOY|&H zqA<`U)AP>n?y5H|zoIPya*B042LKGQO%n<|p}I&Y%wPh_wOm4QJEEfG490=b)? z2BnO2Fng#*+MZqMn#Y45p$cG#(j?J`!6hZX>l`!`-=n)^c$99enl06876qVRkY_?V zg*1zPZH`38?X_=Z9safJY_HJuE&Oo?2j(FsBz1aKu?X@!MnZKU zi4*2_@?FKu7#lznj9tzGA2BLd%-0#FrM|>8C0#Y)oPaIeIi%TPc^NCeuqbKm?C5;# zjJ&p>Ieyt5;fy;@O{N|L(iH{^3?lVEP-0V+BpyX=q3-7>DX`t^m^w%1O-0R5dQy_E zOcX)@eawyXyJ|&$-F4S#12BzWX#-aaiqV62t|rV35+hIMNdF?bM#Ql3-foBQ3rP^yNqpT`Olas6m65cjCs~Jk}##5 zYL>~VhQehPsCArb>;!fMKr2-&2B8Rf*kmWK%wFeLWNO3LMrBJ^X0M<+9Q(MXcK$p> zgqJ4KSH_BKQOnuG;tM1%ta8=#!wP;VosAXjhml^b_y)LI9a%r9;%S-Qb#9+`(A5(=l{ zWklf!mHeCFgJgL$lR5<4y`UQ5-4<0*0f1m^6k1W~ z7-IBAWkOI9IT;u!=?&~oIT@CYg}>Y~u6{4yKlWm3%y*1=2)6?DmB&zRdc+iJbg|^w z1Usg49F~Kl(V@OE!2*GBE19XYTrhUaU>EsO@J^dBXG|@qqu4K&lGv!&Q5EYEDlB=U z7>_wL;L>C>V@}{^C;MgCDBQ83v=dfJRY*%+XS8sI&>1ZX>oZzO82>`$DG@EK)xjy= zgm1zd!q0EzI=^K-w;IHtj+!xqdt3@886u}1B+dlSr$K&NXHBwI zAc{;8@WfLA14y@aa^~cOSjFawZ@hUEr$SE-GB}RFvr1ua2j!0M=K}b)87}F(F{gTC zfxp4ib^bIAu936-9CytZU&-Cjgu4N|(;fdq6rIt_Y*&i!M^5B=UoH!a_P6{3WEr!Bx<6l5{I?EXc8`Yw~iy;?FUorqKbF zRnBvgdNrqU6|Po#VL^e@d3j@)UgY^dLR1!&m!U=M1>^*U5>9a?kv z6^I5p0Rscs5%m)<*Tovt1CxDuSv{8a#=~?*_K!%KI+h|+cHJZs=PmmjD zCg&65jUpyta{L4{Q|+o4dP=_|ECzOM-_Nt%njrAu5e#0^Dw+>BOx6eCj?_G2*j*$NQ?QQY)*fTBl?I)cq}# zlU*7*dkf`d%FIa-iE7Y_=!^KJ?jm#y>SJB>2WWDiy=)VR zVWN~`(m{?06e@RG1+>Ngl4|hNweW;;uh)jv%4&DDy+vQ+v=d;^){ISHFIj-W>H`3r zE*iT`MWug3AxYCag|mpTvENXFg$oUwaCSQ-8Gpyi z&qz)L*>3Rw=FM=TG0lr9Vx9kR_qb>Y5%DcxISgMN2q0zP5$d`)Wi&`bE0GwcPhdHcNtL=QE+uddV%5HRJ^O|qgBHhnF?M1ddE4BL zF*GBdNPrN5=F?pP?DuJU>I6o+g3-2~ z09BAH&rd8h#c287Djq;x${JLJK<1aCJv5sA0tnu<9VK(c>>3r~j1pmb`CypUG4{$1 ziZ(`+79f8JFzB2vW}T=FVrv~G8B1I`gAAV>FmNfwl?=V%)Uj~SRvl#G zs1f^J{dAtkSXIypysWXvlJ4qI-p&NOr-7Wjb(jalN&C)-f@HvtVlJdJJ3x*jxdI3U z61zd2mqGG#5z|Y*rIAUW#e9m|bXaqj_C%CuU`G5K_RiB_YX1grhN1)dH~3o}|Av1| zo%2P0t~IL!!1&7tlh7=#{!y?zgkkYX*|L?!U&0$*w}WR6Jz*o)oCbIyt@31>p3Gy7 zmXC!kohPhf0$=Owjw(DUTj61l`Q{OQ;|Dpz-JNfEvRh9eWnpJstiEveU5qX-?DZD} z#^K5O9i9|VEP!LpoK7b#G6|+S#K!tS!(H?j|G;v|aNPkZJHe5y3NLXGak40Ra0M^H zA(ZSifuGz2$kt0z9lDZ?zN9;%et3ykh)h$EoSN+0_sXA2yemrQ^Yp$~%JwYp3f(?c z-7)}fcUQNJjoT-xTdsHdHK2A*~vc3`6}*z?(hlImNw^<^6Ph7Eh4P2K?CG%$MBRUjX$$LhKQ zw9;Vjd;AIEsln0Kl~wg%mW|mb5;bW+*}; zm({^68XqGBr5VufpAXxv_mD<^UMT^*#zDxG;PdGCQXt5xmL)P{sboB%-J< zY8;QG8|Rlpqv<+onGiAvl!1=Z>aiX-6zsp0lH7uoFf0jxI$0#HSgaKTsd?cFtFT_? zQB=u2v;mysSc58tpW4l6nc&**>6Bp$czzdx;r_FwAo6Tgh^I2$fhcPKd0kc>K1=9^ zH&9l1$9!q5O?;Xve<4Ktjwg{&b$n7g4^`*%A;1VK-ZY60I;pEVtY{W&8u1P?YeTr{ zt8QON!zR0eK-ffUEd%`)L0arp<#y6@G3q{@^dzyB>xaAsr3&{N(8sC<7P-@E688SX zBnDWDB^PHfmk{i!=Ku3}Jgd0d(!5C@VtyK~XFQyt{)7*8`~O!B^*X!iVW`v~RhZYH zp?VGdH`L`m)NB5^8tT)#b=AXAsX;^a8Z=a|q5pSUG*?jYS2)<1`X9~ z=)a*Z^Pyg6Ybn`&-#_)Zs??yNdJP(?*U*1M{ZBsBr@vmU>JxU=!>Uq)hUztFs9r<= z4K<>reg9Gob+=vhFw}^ayao-`Yv{kBwtZE%Jyi{Lvt9KtRBF(wdJP(?*U*1M{c#`a zLtm?gy4|jN7%DYrs9uAH>NWJ=P$OD;>Icg>kI=kv&RU^0L zHE5__L;nr+e~oBqZ#C5IcGbgBslm{aeW#&%4gELNh?egDPBqkP)x%IDx8yZws9rZLf@;K>%F@7 zds^QfR+SpGSG@)e)obX#p+;h}?cXZ^-fUMr3^fuPuR%lg8v1XjKjy3Y#J^TUeaNnQ z7%DYrRlNoc)obX#p+>Z{;UB7@KKYd%0l3<$R)g(TtD*mf8i~!LwP=2KuL_q)Y`g{y z)obY6p^n$9y3MY70N^-OuR%lg8v1XjC;48z`|m3NzR9k77%DYruX+s{s@KqeLyZ9Z z)c32Q?zF2Oh8kDZYtT@=hW;CB6wUY5bai*HJa81vy#@`{Yv{kBMup3cS`U1eUG=c4 zQQ_h>XsBL8{|&X$?mt}t@O5_8!%!>jzI~^mdJX+I)VNo#``c=$`ySU-4?~T6)oajD zy@vi9YGfgvs%BD zQXHz+prLvV{X5ieS9^8Ow|d;G)ljVl8>-dNe?zU<{U@rSKKXY&4z*(Utp*L%Yv{kB z#=W}hZ>pg_Vplz2;JB(@gNEug^xsgUXuhvr)!la0!%(AW?lowrUPJ#4H3INM^ zS3L|h05l)R@`jbU6>uR%lg8v1Xjaj)*KjgC8dH9E$<>NRMn zUPJ#4HLmIoBM@5EyLu%yaaFwr4b^MtzoFu}s8~mv%>zP1-P@~qiW*EgYBgx6UPJ#4 zHLmK@|4|W}C+w;Rv=mp>YtT@=hQ1x@?tiR?y2Gw|AF9`2d(~>_zoAAU?(y$dLmjuP z9)=o)IIlrN^&0wbs1bnoe4`rb<95}NWJ=P~)mTZW<-g z)$v|+OL0}b1`X9~=)a-Hy}GgX9DS-+c0cY_uR%lg8v1XjaaFG|CSR-iwCN6ez`$`; zy#@`{Yv{kBMq+c%lNGV~s9p6i)JSZ+1`X9~=-Z)g+gA;Bvt9K*RIfop^&0wbs4sSQ ze{a32yLw$!Y7o2cHE5__L;npmqNS}h2Hw~!299XSYtT@=hW;CBwAtKUyHnoOt2<@1 z*?0{as@KqeLyZ8uvzAxy>D6~X0VuqciQ0u0q{M& z9#e>`>NRMnUPIpw_3rvq@=d*-N{&PI8f>px4gELN2*6L(XX1ADdL}Lcu-BlWdJX+I z)TnUTQ=_HFdqqo8;o>!Ds9r<=4K=RnM{B>OYkKutimU21XsBMp%ntQ4F_J66x%Q9H za#C01(oTeyuEJ88pQPdjTj&^2{G1js(?4k05u!3-NmS@%F)gjLz4q!5?L~&q}S##pDI8mpDI8m zKXPvw9xHZc)@y6d(yvhs-K}CJ6R2sdcxZ0yPmDjS7h=3ys`j9m4=8N>k6Uy;3)U2_ zc!zccf6pN_A59Vy`6A+yD+1iPlmrqqgcV#35MI+0at+$Zo8hsKJvNsh&J3FzLbzMHf&Nfjx5%phYY@OZ-OsH^xBwN_{|tDs`K^G72ZDlqr50^E?BjBf zOIEH)&*>>&4=}UdZ^rC>9#o&XtNQGFk?7M3I+ZK1r!d`s4MBwpkj+RCHT29vo6=dr zWM4lA9KqgEyitA2*A{iRjym;c!fA9zi7hN%U&z-einxkGlXc(Z_00&uuxyswIpO763askEuv;v)6STh4zO6bvh%z%Z=^Arew}wlIllgVmQEFq0nV?2TpK`eSXD zU{TL)fq^CR&al38NZBH+wKfs9bvx*{uOeKY@``Ce2jlNRv1y?d565Er^*0yABK!3s zi}J_E_UlC!MU5i+RgrSNSWHjCbOU?YFkndcz|?u`{;qhH5PRPcwi^^N-eyUYL8vhO z2;3|q=M?#L5*w>USz{3_>z(GfvktaFt*8??U1X;$6T-_e_n<`PoSs?SJ-sQ|Rc@VB zfhz&|D#aIBE~<1nX}=8Nn!X$Yb2xFzr&t(J`txQL_Rc%z0qJ?L7mg3X!o5d^{a8e* zcXfC_$$-NBP6Cp41OAWTyZry0CCW@HB(3Z!10h?hBp&dHuwgur6%lm6&ct-o+C zPZLhRjBdq@3>OBmy-IG;Oc5-UTR~$I3#be7;9AE=lH9&@;;DpBjGfVlLPP6ej`^;* zZJ5Gxy3`lVwU+wAc?L#NixK%mF=A^JBdlfhL2P36ElJ>D`aG!UB4?I~@k$zqMJ_03 zVuuNKTn8Q23_9yOny$>>R5G`xWxq~8!R_Z;K;r!M{2;)d$BzZ<<-J$?6XrlqPLEFr zVw@A#xMsPGa(8i1Mnk$dEE8+HI515Pvt6e-v7F7F94F7GW%|obG+UEk9hjWMhgo7# z3}`BNBo4W8IINW?pGXfT#4esJ#4a5Hjt-pVVV5y$B^vdwD0rqHGZP}`qyx% z7!W;U1H#RK$&n98u0Zte48j=lGCd^$yYR8{Y%;D!m}e}$vzv#U6goXAW`owsNxmeg z_OICCSgsYqSE1%1Uh$%aDV%2fC;#vasz_BnbG{5R@2whswg; zDtZ42lnjWP5x4%4s^s{FIKCk&9Qj<4D_o*ddK$QgIR^N}Lvx&8D@Ia(0;PueV}Fp8 z%<=?__AI{u<(zLN-a;Fq?4Qp_S2z|(M-<*Jwfec9KrK&|D^Thk56Vv-JKkVVK(wT$ zY+E8ouJoaEmf9q=2O;g~YogAOt%>GE?5OlP%RReVOPthxLwWev9EzocQ4#~?e^Ne< zk=)te7|!1Yj|SRuJ~{V`4p16mD?~5u$pI-W^9x)>C>~WrI#q><@RV*HaVZ~+1==qv z_8LWSC}N=fEZp-Y#mdOK+Nn?E8Ei5@De4XRqR z1nf#GlOVa&Zj=&Ef&?jdPUq58O;W|Wtf(a{uAS?X&9&>3VMt@L<^oE?9rDZ~lOk<> zWzC3ER#mlT3=4pz>p`ed;x}?nqxt25kET-Osz3DWNmImWy)8i?`!`mZVc`&S^@tXP zQY5%MTc!S%Rc1}ie~vwbV&81bu`o4i%ZRl1ynb@riOVxtZM?q}pI#TASr zxMiws{*_Lxop)vUt@7If#+q25%yO>N|1C&Lf2go%5>3S+-NL*o=DpD3v*+HtzYtP3a-JPa`>qTvb+YHw@ zu+?Gdn;rPd447m78>VLqIHP4;l@K#)3G}K*X^0BgNSAu;Bq{;p31%HgVH7F`1ft9Jm+wUR(wp$putv$BKUvetY8> z*>QO(XA_95Ym0Lu$|dOcw3uujOTNigvBYDI;@UYvsMcdBI(x|ZL`G;#4BJ+{&x<_^ zrS>>otYQcDy-(QbtWSDg@rcNO%5moZ1k{Vv3FEPfwqP9GUTuY8rOkq#U8uv2UKO!XR@@} znVo4&`Xh78mfP|9VIilsShr~EXUw?9_mzg

    Hu8A}hqgX1q7I%kPU*4ax`Ntxny0{UeWZ*HO+%*>DcrFF;OKLz&{i+s-1JZi>t%%o7(`PR)Rl6?dIyAWw#k9(39cf zVX(OBY-*Fz6YXihWQ>KPTy+miIy6L}6oSehXrPEzUsez*7LEa=PRhs*$d!n;tP)1M zwaeSg9INda?Tl>~2BE`8Yl=4>_7sEf2c|)n3>S2n*+1~qTFuP-ECauAXE@n(QaR{b z@WDbxSs6ijp^}nQ(vb8Fbifm6p|Q(|ZaeB!37<{GuDOyLQ~#oaV9iAp>vPNx;2~j5 zK(!-SCdEzj7YKR1SMMyI_|A$+RKw{N z``IWqEP%4gd3C2B_|?%li@oJW?33e1(_KUs@FacI$Ayc;4#oL zl8gECqOq{?oyr`J2erO5rY>xhEClPeO){q2r;UlHBM8edV)srQ1~Nw3&O>wU2nabk zLg#XXK1+Vkjcz|^F~totkLR@?w6vWjbq|=^U6V=^P~NQz$J1 zm>(^JL=5wThO;T|9Ds6yH6>*_c|cgVq_@wH?xVQPrMNZKlB@)*B}1(Y1u zos8OV7R0QKXbmI_q%PwE8IrCI`tr#(0Z+bL*#roX{J7f$@^I(Xu5qc{7rHC~EK|27 zfHtE6!RV?o`6jLLjylAZy5h*hU9krwWwe3zjx29nFD2$o1bk{3&hmXU<0dhMhgRNL&n)5|UT( ziB6%MlAP!_I)=jJq0aVu=^ePY*C|}sU=(t|K$boB*X8&Kj{e>_z6M8s4ep5w!#g>y z(!LG!01>`aU`ZUtyrC$>6D|1jx+r9w3q8`j$@5vP?ooiKbXDUA9lN}8e zomaOUy&9G>?9A~olkd_X#ac;&&Q?J3%q9%ulR->iJK{5l2A8rMT7j`b(!nt&R}HGO z(JqVKJ&pkG36|v66TKa^+HFgfVoKgT0shKVZhzA8Md+B5@URUK=zLYSN*s)<>_uY| zpLCc!p4eoAphY(;-2=CalHr%di zE~vRSfUyo(21=BgUCn#MQ&^fcb4mo>zsHv&fw2bd9go5FGH=po#2#d4eHshyB?4%<9W zwa?(Wsol>%e%<=%b1$ZV$KZnQ=TaF?_AfQXn$LH0b`(5JvYbODzMwu9-`@=PH}4oN zujFEp8TPPFXpQ!5en@)&F9`OuuE0oNfWd0oq<1TJK9#!G{!v^y7zP!)R{g#?20=}B zLG+I`eXH}dbuVZ^@F0CrbV5|4T4ps_6~*)0>e~uEpJkOZ%#q zL>-K;NZetdrfMduLx|v-oQ~#BN8!WZ8pKmgr%Z}245HgmDDf0D3Xv=RY`)AHQVzMj zW1@2Cu4`O5l+tBm1RxIJK*`$#cs1mZo0`bU8JP9)51@8BIc)qOAATL*4LcC*b^x(( zFhPb`&9L4mK6xTcfjkhs&{$9Tp15b9^I3ji!Uz+_Kv)tpq(ewQc_LF`KdhI#`I#bU z2%?KukEoiSf9M7NiIhf~Rg_v(Y`hwLh7o0(e(DX6-$=KgrQ3GsUuPv2Nx>q{i*KAc z|rc|r6V?$O;e0z&#uE5%b$04c%$M{bso-u z6SZgLQX!M#Q3Xa(w+QEB0O9*aTbV_{ovFCG>Fl*EL~z5#yXG~c8aG(8_&4mPSnG{T zs+rH)S5yRgdVW=3X(2GCRxSdA>O3u4C%6!o?(PnQ?(TprB4~nr5j4pIOu!6CD}n(W z>Y)2L@PpQCY9EIILwe6OR)J2?tFzJz5kA5^2Asz>nC@?E^w7J&M*n;9IVS&($S@(_ z97vV~L&+H-JIA7nOesb-<#pFBMw+*re9MGrq8KSZ_X*NuEk>r@4VukM0 zE-}*B3C%D{P{Ta(;d&XWNZk}8jS2Y2sxzK=SRAY%1_ob!@)m~{@wogXUkWs zJ!@#~(D2X4(u|Wga^D+=%dV+FZANdP;@=PtPenMN2}_KZ75hD`7s zEQfvp5+?nG)R}IZ$mD`22R|Yvt>Ba8IAwDZO=LniKmDn+VAoJIG@7m(VQRcJNu%x5 zWaNyV3CQ!-ZIZ?uT|-Y*iNt3#=BvZvH_KJKROFfg6X1!xqEkac{yFrpxMn^?5noI~ zwW#U=Rn2ogF&7#WUd-`;@H8MNT2a6XtrBrBx(_jFx7YW`7ute;FN2KL^PD=m^;qfX zhmVzx?wvP_OV6QnBO^y$X6;E}eJ(reBeMwy$L9228yO==v*`my!7rDHyBJM=u|2Sa zTW}dxqdV4J!j%=|mx!gPpM&J$a}e;7;_ty*7^lEy2_MVlvuzc^d|u(-C~%yf!5PO7 z!F)@k_;Ni<>%oqWA_cpe)Ph#bj@=jX{Y%5vYww4_AyV1sO-N(|D^cpCrc?%T>Nz&? zf0ObSG@Lm1Dzq%Jr~@-Xzh;w@ELLeuP0Y)Jf`w5!C`>B`t9G|GnqXu%bse0vw-3>e zu#yI`h~K5_ef%ljU6_|-#k379OI%KoI(9(xUN%TEWl;KJ;X;23+tq+l@B?S2PH=_5 zU}`IztWm030di0Y^}!l$VoI%kqe28iI1s!*i(5~y!{Vltb3$QscOVKf;Ys9`@Slyp zkrZh{&Fx~+G-R5OBVQescb~J!S83kF9m?j(B)JaR1wbQN%zM{kKf}=d*y1czUz?jd zJ+62ej_pau4GH8xfssHX?6y!^92(^0o>aZ9)}IP&;kj5z^(o4NWyZvC&dYWATroDP zcS(@%otFo=rN`IAR%53QwIB!cMP0h}TC=`be5|cKU&v*E#j}%ix|2T{JOjMEoKtZ- zLv77OFciEHs`E>HcWMgAp4@`1!L#sE>o|?RrJWMM3=z+~s|ig&zide7uL-MURLMF; zZ&?%0S5a<7NEMosi`Rs=oV~=46JZv5$2u_|uM-THjg=IlT+Z+0(dhc0x56Br zW+f#&XFlL2-oh2{Ts*p-@{1R=igz0GOJCDVnMuG_$C~I_^NupVTRV4+c-fHmZ~a7{@u33^P@i6)sRBqx27R!sH&&HFUp8$GmTUxD_MrD42-af3 z?Qlh{IENDz!3vmV6KGAY6|Ziw3*gm3W}QaOUE24Y?YIU04ABi9QVPpq-*H^w^=yEl z9S5VWf=qp9ICunt0`q#u*|qPu0qr<>A$ILJ_-OFo)H}{YEwJOJRN;T@@#R#@-)U6% zUqZ@|;-Agz~?^{=1anQL(CCD znY_)aRKBwEVUb{z+QAv%;g|wnSe-%n9E)hhlPEx_9iPrmO|OSKAU}*Ce<0fAz8mvZ z4xA+}u@Y_4@FqXXhF~P^)?o3e4ADgbLwgQ_(U2Ya;4~?-*eZuWh#*1po@FY5Z^S#nZs$aZK45k+e9kY5B57*8)~H>K4e{A6}Pd%$#1 z#ntmhSSJ4Qh|`vJA)B0k6vAKDg?2httNN)-Aj|*HhOR;Hx(Y4?3xc-9NKPzYB1@?q zI;&VPnWlM-0}`glbBJ}Qm8tIk9h;vgxLOI2fG|K-kjL*(@vh9Y__#x%az6339NV&2 z&;`&r9!;$uyyav>M$8-PJ}Hs^cJfnum|EV397Yo{B6S zou2-;1NZd5P1Vz*L~ZvSgxc<#lG=i`reqCf767(-U#EqDtv&Vi_pYA{?C^IQVEs~z zd$pnitgtaB93rspo~o~pX&Iw+7m9e|?v+xLyicg9J_=8yjiWFI8vz5j zWGpBGhtFTxoIfwm)1>+jqR)x4-iLI2Sq}Mx-6YVO(PcTtHuv%FLK)MU(+3?>^8Yag z4Ab!^&yANlnD*+cQ}NSXQp#7Sn5$k%y{IS zQ<5gqJN!)&HYgbf{@5@)$nfTG0|UfIut-%#4}0+{y@+~i;)`$>(yrobw4j8oiV?~r zST8^wfn_VaQ{UyqEp>}A{YQ62Vz1zIE54CVg`xK22iYG!E< z1!-c&!b|W6xGfB>IVE}L-dD*vcyM&^zsTnR8ywoYf%DMS4UXoUDER3sH=;T~)v6-_ z1C&uGO8K(6L`p*j65hA>voLve-7g(Ofsw2(KGfg?kIjVo#k_v3x0+MUz=ZRLL@1GE z#?KqN8ejTHB?B_&hBWFyyL{jB$-ZxsPZ)mmZ{7DK4^MyIJx=Ji83?*O-cT^(I#I_O zG`)~H${ELsfe^#TQRw@gzd1sE-bgbM zHKPEDY46PjEj2uNzzgzKLI={{BXG*LfMpI;f#L3Up$cY)yW;lwfu^9bPuKnpxtyl=?h0;6cr z6%12G(nY=>CTbItm{Q8;k8mI&_j#|jU>s4z!Z8{BitOfpr#uru?dde6$?Gsl3BlO z8!>&j>^9kandT-bu4i+dk}SuskkGeUqvlCTA{?nxaP(|JC+an@XJ?E2IhK|)5(Ppu zU&jWKjewWRtr5n-(78=B!3U2+1c=HBgKs8BO_iqXNJB&yQG981UNNHh(uycfPn=%r z#EJbc28XQTazGIAT%P>?v#&f2z-UI|`JrcDDLAoXXOP6}t6L|Y@2YNrJ)T}!-E#fI zAKHgWn0rRDf<~a6vXxbov-IG?%UXYx8Y~N*JO6;&-x-_V$_Jb5mO=h&;Kh-KXGp|J z=x9Qm4hZ&!t?->cpm2c-n`|o!V}RS23g@ek>^Kw>$N)zP6}D7}M=ymO@=U`YtMCLb z)ahPKdujNb3g@XX4NVFmIBEFG_o~koZc3;)3|eqSKPU>G)B(CLPBWgjjm=+>XM^cr zGB}{|3=TG}`!_sO)v7UNKBik$sOY~orc9w3((s2W98{skl=+w*SEnrMD<)M6ZA^cn zH*KCE>=fFVKBK}#)tJm4nT9W`&<3h8$>hkGjJ#U-RM5W0ba!g;QAKN#wo+}#Y0sz> ze+a-=Eu3ChDodtz!BYQOB$khI3-q2+24V-D=xES)JW=HYDy1)D6jUQkXC#oOQJtnT zhd!(MWUm>1OlS%ItIXkA*k)TFd>|>dz?Gv=b|Q7`_M9_v7KFt`A)B!}RQt3NQNH%D zC8F!1H@=MD&M@X-SL7uRE*o`8M0nh(<6>IiT$TeW{45*MN#Sgj!cBGwbH@Kco1jAz zg}JPY`OPLXzmpF#ZI9t%n55YCtQPBx*A+w_IE1_lbAR+)c*m~b+wh*V7rJkI>UeHiMdGmXdAtP=LCEB3I0F*r&#XH^1x&i|2gp!>fh$r6S8 z(I$n7Y>I4DH`+xPE0Qc*m>H7up9n&HmEy@yp&57n@F4&oJo*ejyOQu&UVLZ1ga{-R z)h`M#=_|xL4w}rwT4K>*NZ1RhIoP}HbfWGMx zAQkjke;VOyTgLSm`qa@oK+}Y5qC7LD(D4!j&ll-8Z=n>m%C;7q3Nq#Y0eZoUF8(GEWRWCPq zrf$*)6P)9L;41HL+5a-|4RUF$&mqGJnftoIhXh>4FWy5=NaXRB>JoT#_(JBrz~j@S zcNogyBIP_NWY;;x1vP1#V>{$vyYy4=MdB;U!Z)<*j3DDv$Tp z4-TY;4dgH{JsPWd1mff#pWB(6TRa4-go4h6DL6(K=wR(haf)m}U{|nrqirr%B8|Ss@x0$JqWQ zsJD1`9p6P>m&Zs?kHhQC!RxU#w_8mtgKSzdUUq^gXgfh;ltieYW@p${&CN%j@hg}lWgqUv2zc&3KW{0w|WCU%pRAPLcM|Ux5H8iY^ouG_( zr9y2rTJNtdlx~WW{b_#-Wr=z6MP{B{fO+!$m3eZ({^rSs%#&wMG*346H&5Oc&6BEF z#-c9UWVAdzuR8Vz{W@c_(yVr+hxUD@>MG1J#lI<8(GQ zEuhu?Q?=YMFEjRtXTsy~Jsg#WadLxzZUsSZb#-(FJU{nD7}E37j=xF=Wn!Fu5j(ADvG0dsfEOkko|(f!0+ANbRJJ zu5aa9c4s}a`H=-*ULaagn>Q7pK42Ej&|?)#ozVJ7XK5?dMOu9O1PQPp2HA7JG0aD{ z0lV|m@YwUz&_uS1*Y@}~TwTFIA{ShAYUp6!189nu1rQQ1%lhk6qt4LA;f&Dgb#CkuR?tSBCXn`J0C4p`{aiIBww;YY_f9No_G z{$@AYl1Yt}ujZ+Cy`YEWDxYbz&{7=eINPqIKF+p{lWp;k+TTufoQ=Yi?LYB28-)gc zIFAVc)8c)DV48TBai?7k&Jb6nSEzx)3*#xa>kg-LU!JOS6z<e2w5Ky#3;z(>cCZB=+C=`7Q9nX}J3> zKdXHf{sM)O$3TxS>{{jh~~C)Kd$O?5*6QcJb*V)X_^>3rK(^c@ z-0%r`65$jV(`Ir}M3+GYstkYn?Qy+r(R3Y>$0-*iTQmhmZVpE*s}cL!KrApJ6fzEX zCC{)_PnjlD5vzeEK~>_sSlZXKcvIvgIu#$LWZo+dIygvG?MNyrdYQfCK+{ArLU=zTZR4LzxA9b~9%3*a+xKp>57M=^l+yAkW}5GF z@z^bqnvM-Y6v;6lYp?Ej&Q>lwM=SoSKt_1)(*U0(v_7mHt-}WfK21UdzN`X1(jtn9 zFeL1Ji(-gmIVBSBU}mZWIgkF&0*TzhPHLWW2FUa;B@hoi(1T6*am5v~u}S~bC%rNj5V*{3T>i#pY|C5k zO7Tj`<<;&Tv*yz>eQTM#b#y#gXHlb;wDa4-v&ov`nFVm#=#+p2;2m+?>lsZdi{g`? zh2?{|Ucw;KjpvaJbF{Uoovy=**V@F*PhQzhHgY3F-K;ZXHRT1En@&|rqZ`oSMp^WB;d8&pWp^9oWKg2byR?NvR22_H}f-0MG|V* zfW^jv;(ZIP4=bc?NcymgS5NLDF>a35G1V}v$PLm?m{Nk}$MM%U+SzrA!IYJse6+SN z`RG3<$ph-srOwPm6mo1ms50m3!?EJ4dn984BsUN&W8p z1TTs7@tNhA5D|#gib0`n_Y|?`&^9^u#kAWdUb(Be;n; zYx_X)^&c5Y0TIJlX-y)h&@4kKrFAogD?^9nteAWgi~_>k3EEDI{?=ik?&s9^Iw&tG z4v*S&WirP*Cb-A3rPkRB{d)r?^@^@Wy>dmlXSG{syq)0mSGW3fwvKn}$bvjVXvY5rmQBc<6(xT@He~l12XH2zh5XmIk85rw zoSW?Wy8DRbhc54awMPGT7A{|JLspl2LB;tq`v(U% zq>nq9AGCAJ`C-YH0mIPBk~>cG;pXoW6#n@rd;Dlwr$`2qc+6yjyuDEk4$%>_NWqD1 z7^<%p*%ixN&14gfPg)FUo|GTsmTh~fOHWc{V1?_@J?n1t{?XfCOS4r+(s|hw3`eSS z+wwl{+!KhWM||c!5SJAeZql+oh7-3pxH!zOl~hY7DygK?LY=kY2+y0CH`#Qifw;fy z3F2xc)@;tT6Vhhn5S|0Iu{qPf;$SA zd*L!KED^UM+K^}&F71Ug{e_eLXDRP39V;G&>8=c0ACeP~cyEr4SxjwO79}F8EC1zk z@5rmHlk{6sebaj@l@7W_UPq!9AEWr&c{CuI`7);Slr_LO7CjgB-daoF?~MZ?h5umaey=CmV@ zLJ@I*sBQ#=7A&2G0$a+sNj@vP%8L)o({ji`$Rj4{-~`UtGRU~2o11IdFOt43 z-bcfubjZcK#*g;Y^Ut*mH@lq;2TNaVDMD6-A5)Nh?JlIcN0Q=Z;#OHh)&(wyO-9Qq z)GX{Tk30E%TQK=}vO7=sTBzE>Y3rs+yg@4uqfROH$>odE82d^uBnO-v3yFksVanG zVyY3EU~wRJ2r;C){(Yi^lM$mn(#EIEN8&CFl5;dWvYBGRVE(X}g0dhxCQO?wZBKS= z{S1UpZR(G%8XRbRA)jf^v6|W=`%(yyRNj^tT92Y5O%nr$=5NQ1pP>bjzNmIFkrp1_ zd}j-{?V>)89i>ArRv$?wAa)>KIEd|piPUU`%d+ATvtr8icwRdXmw7yKc?wZK@3NRd zI&MG;@YHK~;nJrvS#2(<#71Kd?G;!AYm2X3c^&P$*F=;Ixvp(lXphUA^r~inD0Neo-qJE&_=K?@r=e} ziS>A{9^-f{b?nOQG_z)mC$izD?hdO?_`gFU3kOoJ0(a7z9!U*s>v$*G%Tp`7Q=jfb z)Md9?$DvQ|{|2ImJZHFz^kArP&gu9T5k54V3}{gp;kt##q0bMW^ag#fOqIOj3Rww8 zB2>jU?aT-nV2y@A{1>@PlIdpmJ8CO}q%$O}1#Cb?m_(LjAwZrs$BbpX11l^4B&ZR~ z_U9mUD~rc!O%pqPpWd;5pUm&zi1z8zbE`gi^tglc>G(XwD0?5<)9*Qr>bjTYrRPI)}DkAIn&g%QDxLLI|Axk4^IhH%_`2H0Vn0=+^F)n~VK*rSrDlT}IuSN67nIV6M zjWRxy%5zbbm9jcesSLYF)TZFdR)t4cF1fv1_p;)PO_+)liTPf!+CLaH$t9K8^6IeX z+Hn!WN0PCy>1HkOLOgbn!82EkWe6ePTPTPCELnH?&HZGS6d%{xj%NsB2x=}hn!{Xn zw*c%>cS5`|L6|>7#a9=ZaEJaCxMheeE11qmBhm7h_~^%M?3zlIqg9W&lp?yM(>^s8(I)q)dB=nvF^5q+XI|nAv2v=XzJv3( zJGWVmh?Lqm=!877S*llwV$+KyBq-$@CA_hoP2xM}lE;~Umr-p+`-M1SxQ z&sT{N(xGdqqZq7eqn#R$cAbYZ8`INln$c zWx2Gl&2)~!cAIo2Tv#+SehY_0mfBsY8L@m6rO9?TMXAwQEFpX*X%~pwKT3zGa)=#*rDq|P|8!Fo_n}~vWzgN z!ZdGUy0jK)*sZtNJrsnU6JPpudr83^zwTApF^u?9*l;}`(q(XP*yA4-Pb^SB49+X} za7N3ttTlD$Y1!J0T^FvmWXQ~8v4yApT*pS&lB3pQ4~|0-?Nkw)g68w?14C}hN=Hvx zi7hJ)|8k>_EG^^8;8VP8eHp`3vb3&|!t)ZGU=a}GNhR=9g+D7@472lU_gA57$*Jku z;cJ6my+^XFfp!rrXs#a)5z1w6pT)(qycK>71?F zvB2R4)@bz_SB=sb*h=}AH$_x{?Lwra8J1OY#+9@}I}nOxZN*pPA)yC-`&NgOjKDO* zsU|w`w773Bpn<5O2mH!OO9R)uSWz9=fZF^Zo{mZwhx(y3QG+y@g#oY~7iqZnKL82~ zpQ>8meNz8nIqms_6P_!;D~^Kpn7=FVfp5$|G4Tm-auesvUt&MD1&@+r;9 znHQ=^^&mQ-io6b35B(LX?~c?7iW2h@!_>R)n8$TBbY8LnPs$fd;cZu^ugptb0okXq zMg}M;xUGf~e92Ny#MJl8bw8eT3Rb89o+`dUO)Hd-q9Qo7=CLH{_9KstLlz)6XVN4+ zKfSfSw%rJu`~Ct!I~t9$u_^q;EEIBIce695_zPS;E7kTe@FIptXVbRcK#usE=ZbM3 z);XR$2uIj~IK3nvblR>E}WA6?YpO|Y<3^|wH9DrsXbb?8_i070;>{m<)#yN^ZVgq&YLES7O_-4Y15v{`} zGaO`L+xwv{TlffwGc?$^Y*gW?e+3Y< z2o`9PD>E1*HRgi}a?EFekELO87bJo@RtS*zBZdIQ@`ibyG_NTKji$mK>63qiVG^S@ z?D*GkWzqFE1sGyLCjncVriNn!lsRF)dD20-(PSj&|1@8w_YzHGga5bpEYiTMnY!S! z2_>WJN2$pSb-YL1&DU|J&ie?Pj|z@zsz{Nx==ms%Yx$740+J?mWN)VIW*=0~+LL0O@*rq+G1uAp3bxTRz<@#`1*kD>I zO*uzyYFUsHJImU&M7DAsV~0{;+-7|w%~@v03uYdRtEFwNX=nAob(p2?Sg7GTz8;Qo zIrOv=qTij8w9V4jxT?f}#}CvB5+E?x*WeD|SuF1~*c+{;NCxS~1wUEw41t=u4~2@f z164&(u;j4yvkxTUnIt^HuL;-y2dIaG><+sCx}7wu5b%t5=$hL=V035)zlYKxFmmSX zpZGqExIXRiY=c%Iq7z}S_b#;2%dgre(O;e%O+|jV@m1##ZTBHeMh>yAvh;04Xg4&? zvzUH^Mbgnn7AZ&P5VEuNORp{@_et8#rT1;eP`^}OASj0%E3#NZcS<0V>rAi>QpgZC z;7pmGqk9y}hqTP1TLp*&?!_V`IHu+Ou3!kTpF&7tyR_}3VzK#g=c@1@`+&CZB;l@S zBD67KJ@xAu#eAQQj`ccWzc5SvnsJ11_YWR6gs_+2LqQ0?TZwU z{IL>|o^+`k19>{mJ|)SF^T?`5-wW1NS1*DCq2Y%t+gO>4`5Fp$K0dBOjzIoBzD*&8 z8_6ubWSRYGv2T%itTHT!Y4Hqq?3O60i-zk<(m&!SPC;qCQtdjXpY-QWSeIx=Qg)KVTV-LZ6}YPO!BFQ}Ywye9~^>|(i7nfA~b zMJ|V>!7Ce;zlroZa;F*)<~9$efqLhwQMG5PGb-!yYt^PVp4pp66h(wIy|F3ov?lsz zB!2#IlGXo-8$%d+wG`AQ7QF(nX%c1Pd_YHP0{$>jTounYUsxHdxsMw64DC2Pycx*oNhn9_*QHXc|LE$IDu0IFqD3!-0(qQ1O0;F1}QZ9kPVG(GBaX@;q(0kNr za~p3=12%LvHN}^t%xIjF-1OO(iyrVJzOkW{&u?4vN^0?o3n@&Q`R`>Tq>O2b39Kln zyxfYt^OzT=V0knlD6@r)MBz)Mw6bxaKDBZ9yJgggD9Hk^bZiF`RV;+}Fck|thPsvp zoz{{HfiFjMD5LH(W@fb!m$EV|*-F z*!}HsuS>F1O858}Ke4z^6fwwo!pw!Lv_&^q4+3GOYfer!Tni*h%7~zmneHK^=I2Od z2vtgd!|Qv}HDIl<=RL^wz)vXFQm7cRd*1Wvuz2lnhbQ)sFw6YKKr3s*6A5>SQsxi| zH_@agU_xyisZCpNP6sa4bqog5MrU4K4qS@3n-ih1jnU&$MW5ywcXKXXr@)0*u4~UV zl~p-Z&bgF_1Fti)j$#^aTvyI52VZx`I`(6FDRN(S>AH2`JE`#|8tZFO^5g7$MyiFM zlxb~pR3K(hQc2U@4OszULd&GKNNSS^E9F?RS*ms2cAIyJEq64TxH-FvV)kY7ain=GYHj-g|W61!VqxguY1(|M6)#`qMHcydK}j#}d|sTb1LY z$iK(A(v|FaPOFvSt#~gitBT7m6%jNP^7839JgHPg%lL|*sjeyTFHVl(C{ew4s>J)T zE>@5~Yf0_^{m~KQN`4%+B1#fdiZ)TpTDU;t>5~sZp!u^&0?iMn7icz$IiedY%&Ei} z1D1d&n#L-_!T13X_**w^!UB}ycEz`)l4z6@mMQTy^c6V)pQZ|u4}N3ap|O&r5P#3qwGV)lw$suehHv?;Oha_aS?9gL3u!I!NAOJ7ok0lcSIuxzUsvU@u z&Pa1?!hP8A-ElzKl~Py}rJ|!lGKjG;C1*=hXHF9KW{TDgT@I+xeBdtj=3OHMZL;S1 zy~sQan;GM)n~&$QXjjdf$q8Cjoyg)8x!R^RwM`e)Htto{Hm_w>!+y-_2u=1Qm0|sm zCVRg|W!w^-?rnu@GIn^49v)cr*62l?;{t~j zrY-ZMhK`Wige780(P@Hs;0WxlxP5*($hWdn$@9_R+D!F%>!?idQG?;fWbJ5;N1hoRAjuELG31g(oE*vu^?bZi}j zs{Z8K%I0~Rpkr1W(j+$otHuln^&Wb&%BU^P@uU$iu1CD+pd&8o5i5fGp*O8Kt7;l= zwxg*qOwXRA`#IYEA*%HR=Q zb{uN=_ol7 z8A-{{snS}2N~PAYXN&xfb35+AWv>_`C|>^)nAg0Mm1JTjI`V+#u4E7icXnl>48(J> zD>=+d@s$AuoRG7bu_M+xm|g71gcC;gX4THYB%q_#@Y^6{nv$e#M%&aUXqgcfzh1t- z+nuRg>g;N!05Fj%b#p3)Pxo{eQ0(tVRGC379K=!cBbsbW=&#=V&LLzT;CzA0#npao zh709pfJTFC12`3>a+Gk-ILkO719)+bIFFc1pb$^e;h$T-jsKlRH~|lsmJNhq0$2j% zWBwiE@L<`vty|q8BR7>aMlEM&W0ahnh}e_W9r~0b#ccY?x{f|?GOAZ*bCgG@$=^h4 zX1qODMf8>Z;1^#x+NvN^msZHs(W2+#Bi(TmR|-3c<>wH$e;r6eNYC2IwmQ zT~QP~qce*!!_ie<4J#>o(O9@k*ZcTWyt{yuP?H@#r%})`uhgg0b0=f#K;OO@aE(M3 z@N05QFBeB{!a-@rVa&0lF43KNX88N=F6q}eLFii-Ea+nuYq~Bma$*c}r*Su;cmURX=YH< z0e|pGIvyt#neMa!@Tsb_bm*oJ=;c%}x2%a{j3MaYQ?0Ov`UHgAoy za8MCrqj_)cxI|kV&&l^FT{BaW1W2}2Gtn#-}aexeHw46YNp?uuh4re z9H3d+!M)C+L20?<4{r`i%#?k>ZzTxmdsV{!yvR;rSWl!SwkH3FRI|qVn>b+YwK;vt z7|OL zEr|F2K*3^kI86#`Wb6$`!1+(?4d2phjsj^WQbgp4b=@VR@>3St(6nmuw887Jv_i2| z!Dlujn^K*DA=Q~gX~UE$-jHSTQgPx`;K);P7Px7Apxt~$vSHWCw>}tN{u{#XuSnv5 zzgjk5amve=e)FrRz3{AGv#1Pel%q?mjRC@Z^p6C*eOApHrw-vxBpc)C-GKEL@;vMg=j{Zg56JfrTY^Q$RQkkVRS_iNYwS zxP9rGB~~aEzuCC2N*R;>c}6*)`8=SUX=EE~8qfEi*RurIfY;B{El2-BlW*jbRe2qd zHU2}woGc8FKEuzhBy7lv@60EV#urrFu8RqGXvH6Vmg1r=e!CY7f$rP*;dcsQ<>Kuzl2*(D1U84xinHY@3~|kLYpcO@ zPw-SJB@yYZZpJrv#`n!N0YraNQ6@YfMZyzaX+s-|Yv#i48V7&|Rh%#VLQF7(qB=8m zq$4Cj=Piq2P8GN!e5%CnmaUXGI0+a{@@O+wGPLO5MFFO?5SJ> zn+InGrX?Dmeh@hb9EZqeeriI%xlS=E&^cIE2MZBp5f+flqEe-KQikj%L~TJisywl1 z0xCZ>$Npy&zQZ|#173~Ys7b-)z+5IYHQ_lXsvgI z@@VoLn(GxP#h%P2n9OEtBC4}FwqjOWYs_kE!_TEP*{j!;Nz{7nISU-0h8O)#JG+q3 zb?9i&F-5JJe3y|6q`^;aX(zWxYZWp*<~KhV|vY$?Wcw{zm%M<4qbAy5@96^h6Q;o7?~qc)383niZ<;r$bD zw|3pOI3g`+ruE-_tP|`E#r2R^I&>tB3p!O|aZuW5PN^HeA)ISOuXP7w?ut%VznzlI z4I3=PtW-E*=PlTX?07n%YKIrjLt|?am!56bGqi*70@X9ob_{pujs+L>5p1-4GLJKr zxM<3QJ`LNhL(4fg{L^NB#3HO13y;5->rbj&1Z0&L6;U(-(<>ovOmMpeLAx9a*^`lGRK*Tuigc zq5=??23Z<8P({!>b|da$67b_GFc#K0!>)$C9ml>>p(GK&#Jb@nA)#?ek~PU)FIe+1CHgrQ5e4My0Go%FCy zbTl>&a4P)M@fKIvgdO2oJs zxrfs_#fKC<&Ab4BYpWHYG)Ok>crF+*zrM^BVu#t*6SLUXj>Bo|w+?R(_jR>}Ezo^j zXK)^$>T1uTK^{t5zg!b?^QINh49gBL>;@us7mAlz#1d&oJ)TP*PfF$>VU-l&9h=9& zuTUxK= z{d)A(?&1NQ{8xD|#1KTEBj=uA3k`wC1A-I$)7d;7r$`+L@^e1F;Z_zm`}>lJBuvN& z5yeDm)nup7~)`#?s-iqXkD81Gzx1LsbOeLvhxs-rPL1X#qaMqeTt`s`cMZ2+S;|NbmT(Vs-^72h1gi&?>tJDKm5w~xs zHBG%$T~u2)LA}DhjVt5y_&ZmQWYo)~FppjJ{+d&WEL)`-EX!gkSCSOt*{vs9B!9q3 zthD(m%0Wi2ibg=6$~2xRwzstz?8T=}C^K`KNsTTDX8FhTDL~rQufVglr+V~%gL-UjKe=UH|uJK40A~j}G zr-a`flQUOC{(8JXCUQ^k!Zkk7cAU3jc*XaM4Pd1t_b`#V7!J!XAbwx7^(I?=+L4r| zrbxPAQ7uV#l!sh07pY&+|L{t5UQ5G^$IhLHg5;^&{vrWRuF7#8+HnIqga0k; zye#1w4mMG64?k9LK^&GHcrm*|JP1>=&0M`AXn9wM7tGCHT}(D+2BCbq7snFb7hhAa ztk=#K<&Lsjzg)w#L^pwNTOKTGYISH{G?EI;irq@~0eISPDfXrVT$&M^FIB9)!K=*( z{}VxtHxJFb3%Am4$1X5J$E^!td(K3MuK&&v1j63ok!Slud%|Ka-o8K$vRBkEEiRc; z^~1qb3nlKU8UzzY-V}exmwa*Y9r(BSPT2Y&Y6eA={FO=D#)(kPMUVi-zz{mcaW=JS z>}3@pMSKUxTEt3xY5p_}#QBq2kQilO^t3U@L-Lv#ZXkX23^#D}M$lR1ZW3$qI^-mA zS_F7Va>&AWs~D9o!E_V&KmPW(wn8NEQU^vN{zhMAl%B?oy6a|BH3B12|0uqU2`J6* zsIzg>S-H&A3}1*vE-`K2LMzh00awc9^_xyS;HxBvNim7=jbOtLby>_ro?L=D5Y9%~ za3vZ;M07TPjVnqvoC`P7fco~(Xe%zyxZR#x+H=KzElCEWr6dE%Oy5#xNgB)a>SQEh z$-iMoR7bIJ*mhnQ3Xy7d0t#t#P)JNv{$IOAb9Sr*O)!U)YN7?Q`r@t+QiRi$*e7=9 z4g^-_gkadN@sR8XfUq${LMYbin`?WmF07Pl;Mf`!({CO3p+U+(9L_2DiWyT4O67D< zoFC^M^l}&X@E!DO&xbblFpl4L^AT2YwlmP2|*x!+?kE}RA-&h)z6xS%!T zWB6q|uURd``D_Z-$c0yrg#HL-+&TiBBWwtjum}V73n(xjp zn=H-wrfq?L4{7TcYLvy2^S&0ljr!!m@S=<28NvZvqghl;Zux}>AS~a&SeRQp+0rL(C-gc%OBjMQvBwGug9O3{Cc}m1G+JE)s z#emLb7mwX>m7r@7g%9TlN7r9bW|yDGNyW?W6AUG-*VUMO3awYzU)}YSrT{Zhun4=S z(RDqiN*|u4(qB4YPalX;(rYS{hiSS>+;p}_rI!1z^=3U(Q{_)gVXBk{PEwytmEFe4 z-fN%A5PH0J5yj;*I`1#-q4onq;^uTe9q}}f_(v0TR?n^0&aRnQ>l@I7)si^8pRol( zYGUgYr$HFTA~gunx#cj%Yd}I0KWFh8IKGuY**<3DUtDGu!b!p}uDGD*bc>0d}rNp{|L&iqE2CRy6rKeI?kbV!z0jN)^* zAuHZ5W>1nZWaPB$(c8|^t>rR#MAu^2_&^f0v9X;|b@ThqU8UhY|4O2a!dVSdp{(qn z@NQ=RYhNCBsIX*SUHrv?tG`**pK$Q%ulU+D)&Jj2n$MNE9!9J`93|69SI{bKBos6) z9vrsvco@}O$zA4!<+RkJwUI=hK%a*~(wgbI*;x)t?4rpm{S>FZ_H#<8FgrZC(~anc zaESeW_;BXrjcnV92r^SM?r(VlcK5eExk1b{0+M-9@&R4Mo<+$*ZLG z4qv>+?6<7KdEb; zhponRh!B1HEGzgN{`1?RO8KL-wzY(EqAQ<&*Z}H5$nn3;Plugi_s#I?YRzs zXn6W=f;Vhb+HI^&;c3dr-W|471)EY_5k2X4*semfnc*4=w|e2EOx!a$bKx)}2D>g9IwZB|o9s6%6z-5)$LX%+9pdWMxWHJ0rVC;fjC&6fQ;BO=R8*8SHrxS)^r zTirizdR7QW^xy|N6zDDQM1PY(8rA7pSG5~glmvh?Mf{@TE2g={5?crAGrdUC`oC&I zbcnVdS$9TgSDr%%zQ)M|kI252SQu?Q|HZueWK7|0nOZP#ub3D|0ZaECvK6d2c))}e zF&$rPs%jVSS1nooq61tmzd+2xEh){7LRFHuxQr@b@=R4LqaE_=M844&Ms!FxDU|e;KDVk_rDsvm=+`pM`OLt&~L9vPy z&$^e2?>>}@7srZcXnv(XG)Q1F*iJg$fTlv*rFm|907`(qh^SCZNe66mn8l6)NhuLp z<$(Fqq6`gt(Qy^u8j`i7a-Z_Zg;woIk_+xM^&!O?10&{1*rQKIJgZUjYm;M%{$haScBoR^{ccrQQ449&~9Xv-hu>~ zuBRDi?%pr7X4BB>SW&w5o955pFinlMwRG8ovAOHOAN|F%>K#!upN`C*3 z2F`Z1z#HNM+16$4dk&N*odaMJJW(3bL#xh*RyYiTi{3PU()?ggMmcg|5@E?1C|KeN2m626f9Fn-I5cvS3;U zF;1$Dkw%|MJKQPcLpaRD+>-@08Gq_~3y)dF#uV6l98BS9Ck0CWp)iL=2^P7UJFC$c z?{E6b0|r$Xxg1?M_BX=YuU-5@UHjrGYQN`8hhDqKNuG)65Fpg} zQG`G%11gf9vzuZ)WjZRx3&naudMWw$IK_J0)mL&8>Y`XPuyn^pmFLwsFBQd+r`iGs z3897v61v=_u#0+Q!5BVn+9w&ma&mJV!jt_ir?x19Jt^1j(K%&oH2N6;V6V}|XY@yk zL6GfMUH_&2$U@1|ZRfi9!0-rdS>-s_{7l{7j!LrlKH{113g}oDhNh7D1p17q)Uj7~ zpAeQ^SJh2j+265G4yUpSHr)vWf8qv)M07#@XyDs*@iqO41HVVt-_)Nt@Eu%F8u;|p z@44jxRsXbpRQMt?qXM+Z?*%7%2=_($?&hU z3X3j4OcfQJ(Z5B>v7L3=XIwd)^d056XO$}*B%FZQ0=OZK0IxjKQm4-PG96tpB zSia)Gjg+?J&+G)Y@Zflw;9g+ke-ATFcHO>&!tCb{9> zqzi~5ARuliu84|?g5ttBs3_<}K}6he9du9uVZ>1p2Ny)t|L6OhbKdv8Taq?m#{c~0 zwol&oEYDe=^PK0L^PJ};1Nvm1@OXr23xyIslhTCDF>RBsvrjPzBjT`XT==U54aqrf z7~d`3$(h;rIu%OkOJr}<`#1DQHv$;AxAC6-a@oMuIxmqh$=PZ;{fj-VF*Izgp{?bd zJ~b4ZSqxX zW_Nb`{|$>$4M(Nah&F>VQKwQzrRV`ZWI*&;-mUVnkDqEfRjK)gvu|<{(>~OdJz5$l z==NIiMm=2!Yx=UqY&7YK2IwBwC)cU%{R%D8XLa*kLM=8i-_>q7jkGZvj*A{;!%A*U$^t4)>5%;Oo`@$kRBaBDRlH* zr0^X$NkiENRZut>lZGUCU_b)9sYMg}^%s#+QM0r!PcYhEhH&v1%ezqVKWz6R5Tg0GU=&Uqf)@VAxk;oFi ztd+z7_iEJG(ax~`NRXJt7e4BL>Ek3%XQ@r|*#t)C{{ECLQX)FjYIs-tGw0LRpOv_6 zaU6}wxUZwJwJx0}qjU&RS+f!lIEmBe0+h_&BGtc(R1G&I^Q2GaOlSUwMrG+BeqYa2 zHfd`~m#S^-8g86TrLWnw>7gvXV+Kut$Mq6D%31eh6>Ue;!7j4sigi{)w9cwq#e10CsiGdfijQl)f44k* z6`$6mUtYzNX*J(ebf`6rM$Y(|br_Q(bsLXbsHOZT(it?_dA3He%`N!$l>Llxh%A_m zZQ3y2Vfvv>t1J2tsfv;2?gBMtSD3P)(@4l(_p1?&Bv>i75k$pa4`Dl!xUWtU*@ziy zX^>(I6~^mG(O4aQV)G9K+UR(meY^dqXG`;_6twWPWYRb7ES5P_y(&pt6Rs5- zzK88B+N8)vL(>lC&%KwB{|=^bDe|tb!49VIh218a!u=Dj#n0k?*uh%dIpJDdKH-*} zcyHb@;aYG5X@c~?;m`zAxNO2LIdQ6`iCCYP|xnwtKd zlU<1tWY-bD%zA#Ad?9Jv$>X|EhuFguJo-gCj~~=KB7D?3OrAGEaujur<0H8sv59vn z(wU?IgbKgd^AZuwG9_qj_;vppHSNxk!UF3;EpE7wi%3FT8jYqj;UY7o&t#4By_GvD zyt*a27W`#Wq+3cq_#Fhrnc1uL&>j*^d8ghV)gQmT@eST91jwq?B7=?(eiuZH9O^NH z1n<<#Z}i76*uTMhMQW>~15~sQwcc`Kmjou6+hMcFt`*xJ#uSq&5sZ-#6Gx@mD22>G zB#C(aVBPB%ziKnTtT1nDsBpWuY&f0aNV$;&m6lU17P?l3&U2EYuzc~Bqw)*Rn?bGm0e=TBn&-mfzT=o8<*=lvNLHZtaIp8&&k|DF9>pB zjjHyVU?SS8al7Xubg%XfS+OMc4xw%B9WvT=t^pR9U02IP^D*fpC~! zj~3!jyw{-xEe0G3iJsjj;WK(t-7)qWj&>$%noe%?Cz<|i+FF$lYDK$u7&TkXMuA$K z0p>nMT%ujr|ifYzU%{5(FdzmLq2d5&k`H6GlKyD>YXro2us$74^t|P*c<06;we^| zF_H+D3@4C8Fi<#wB!V5l(UQ&^9O(z<&l;6tv7IC>dEn0qldc?(ryJh)C^!&!vQ*}VpxcXAMdgf7XUbY!} zWz!l;<7QxY_NeUvr76~V?HtMp<7Ugzu}8lTkAz@Ww@BW$7|KPN#_ao?9h54=YCnEx zL@WsIC&Ox<8QIYZTb(wOIgU1OisRowi^$lwQxD33W@FYHXd^9!9h*$&DY)$A`Kdf>Ndls1iLp=%#fbDl_M_i^<}c)8b!^c zO|3=d+g@!N+jV%@dq#HbXtVT}^(8{RJT=F^{MQ^?NVv{uPH8U3R&M&61YJPb+;y_b)bv$S8jLJVO}rAvL`@jW+id4) zAFfQ~b{$XeNHVqI)fNr(g-#G{P%Bbiw*yG=8{O7l3Khe&mQN;OykmA#Q(I_zMlfcc zzc7`^V+b4J#4Cx)i2d0(P{aOf1?uP2!eitblyWOj|8>W69gkp@@q*RFdv}G=c5mnh zn^I9*a$f<~$qY;>Bf6G2YPY;O^g;LmO^xH17uP5#`;0HE=@<1f(qj&pOoI5_1skYky-R0X6Ec!dHSd= zW;enc96kqeMxd%zL&iRu9s=wH1wYHi^sp0MPR{AO#vu>~Y8kD>UYpfvwQ_BMl4y|z z|3FJD=4Y5k$)LF+E;wB+346!-J0j{sZ@EWroW|H2hfC0xPnpUf;ZHBa7)|}X2 zP35Ze%v_pQ<5Pb){jmLzlr@OOV(uGH_ILmOt3G>si?bg`(Z3(z+FOi!jEXLXdw0rB z2yc#xjb*&)H2Fm?5v$%86W2OU;6dG5vRF^vYpp1$TuZp{4z<3fw3m>F@s>@961 z4(qhmnC2wv(J^7=#0QfvR99k3tSFnyHS2LE=+&UyTyDI4Cy3ML@>F6jZ#oLHtlpqF z!)BT(+KQkSQ`|-cruXAb2|rUti!7TW8?qnMcEPOdrxUvd*K60{iJhP=$EwkdT1KIr zJT-%cl7^zR-YH;a2DIc_jrNq^Qy&c<{V|LqrrWtj_HX>*@U{r8IpPHJJkk}{W~rm+Pto48CR=L0ob-?GLyc=mx|7LDcL5&yFj?zc zeG|wq=5wx+fqTom6Y}#D>>Wq?<6b%+1LZ2sq(S9;d?;7eKsmx{?lsk&YMcpVwgit9 z*mV$r?Bd)LX@?!j?!OCX0^R%HRm2_1XN9A6Nn&n<#tTv%6__;X4+&z4_JfJ(|8y4Xo-ot6!v7nV8XI#rM50~f^7uohzOloz@;w*H_u(~~55?mfJ z04Fmmc@qMWi@!2QpEpBbmB_0Pogq1!Y@-oe#x*1hHDtdf1#_vJ!7@z2yqsm!!Fob^ zQXb2+v&*s6f>xt&CNT2u%raGBj$~UY+W~QrMo>?8N zpGwck7t!>Z%dw7E5p8XhN`D!)ulL&1D4`nabYV=$5KW%iB}C zEDhOfzD1_9gEbJD^q^;~{n__vF+x!1=;I7LbvTnah4Foe=iFVn-BhPux9a3=C)Z&& z=;W=uUfW+Ru}xQh)<&bNT815o?LbMhd#O9V+kv9Z#(<)3VXsBRq&-^uDgcG0JKPp) z#Wu@P)Wyx>_tN=(iO;yq-fIi{1~~P-DQ9R^s-81(*&3#1t3AoOGuu?BGY=)FtkP>J z_GAw%QMQK7D%J%oY=<=9H*>bKIU#bo)fxe=o6{e`ww7o?rqJyhaxBwqpf1-Mp7XBs z_ve}}Sc6_TSj4Y<==k$GZBRBXNpvDQr{W_G*%_XG@jQmWU$aRbHZ?me4Q8)72 z*?AsUnZLTd#rR2$V$`hQ`=)J2Yy5e8i}B+c#i-jFKdceCW|_Y@C0S+-hV7#5G3cYb zl1FIdr^3t@pZ?Fx?kt|fK$@Dp>iu`IPSSzDJL0d8qszk%Csq;|3ui`z^Pm(9WR7gi zd}c*Bx2tTk#)A`=?fM(RF$IRP%4SB^tPSGp($57>T(;-7-FBo;C%3I9K+Sar)*G?S z@7PR@)JNyiH4yB!Fl1kwtv9>T_(slj(bW0Hu1uAwlOVtOnxbCCA?JRG9jQ|ou&I+b zH7WK%cIV%ng5K&*FzuoxUx!L%Z+k20F!9>e3~gy<8|F1IJ8~9hH$J>vmC9c0zB?;g zbX%uR%f;REux(V0J1z+7>V`=eAs$=eGBP0bq|~t~F3Z`+YI)d1SXsgcDogku?@W}i zGWL%~ja?y!D9iMxSX4_;;163alIV3xw^{4B>nI#)()Bm zxqm1=)JR)dBV|q0MMl6Us8xj{Zk=4Q%>Fc=S~@K15r5(s&9pk4B%iBwOlkA%m{H%Q zyC36=`WY>GDxw-mP%<~wn|}SHGvNF|VSenger|lJ-lDGq*GRQS0X|k1RUN42R0F#! zD$${f2r=?g47Y5Q$Xa_knHgC2B4?8F+IxQU@BjR()C(Lt-BG;OWC?BNk(8)q%HCd4 zun}|GTAR5$_i>-P0r_gq)wPLkt^d|NlI8ojxjSb;wo)HCv=c<%#XgVaN=B5uGx`VxVeTGYE@PV=OvmI_J^!I7|jyLC< zr#CGqCwVK-tr%ys94ANY>7*{+l z1dJ;lO8b)ndN{@>wMjB&-|+RC`%DZL8% z{0E@or^Z=&bdV;s^y3*}%5yyNxJWjxc-Ind?6Lpf%#0?JB*xQN+;|&TW9eI0b;`!4 zxs&YFb}*yM>?|#8Ti)0w^Srhc%ml{46_Wu;u=vp2uFZAy(LRcJa!3@(OUUUP%H~Qp zUsT6>UyWFY8cSz3d;C2dGAv8q=jJniz#+b#-gUQVB4zU7%Wt*i6$&VMMIxDI31%K9 zgs|wTUh+H#5!J>v6KR@cfjYmD^=>obPU9w<V-n!gXbp%el1&g@Buq5TQ-{Sd zFu^-DilNWtrJCl~Cr|4sEXa%%1uZU>Ba(}nQ%Hd)l?~44Sxuqezu8B`BPkJ4DE)(J zO;PX!)%}#nm)VYtup|bx1>|0cbp=AbrRsL)HbBQ6tHXXI~zfmNI8&U zXiH4tg^Ob>1l*W!h{LlBUAUO%%6=xAL$(PV(0VgQZO`wXC(7q-g73 zwQNKB?R!ihV~q&wAgZ97xwqc3j!wIc#@>(15^%(*U=MP^gMvNHqpV=?OLQ_n_%)0! z^2exaermt$9@&FQL*%3er+JNh8bm(swb5RGmmWR*ztT+yfG@J8n4a)$C7CsoXk&yv zPn7hvy`F>=mnra7JF-m}FO830)y@fm7@Q&>5w}^1k zUz@XUlf948baiu_Xu_knKtaYdTt9a4kB9(s0-DC`^0t{I!cMooM4 z@=hdlUq*wCi}W3P!AepmK`HtU#@hYH=+=p3IuYC&&3waaVY3SxuX*j-v731!Sszd~ z)23%};SM&5bPI26Y}%JW;pbrKOKBoTB)oZ-^z5bz#C^&UD?;}z58XfF$6T}^BW_(F z6YEyRr{*xhiL1pd?di8=jo{vvwU(#|edsz1z5BYFp>O?wvBSsT@7Q4_?q@9Yw$E7T zKGanVUVk7MCpDO~P+6k4zRrfg<5#;OpxE3JVgLNrzpl0`m3qI~-c_3r`ufM>(97*T z8T!EQ;?Vo;JsJA+FXGTA?L8U#=x^fC2kku>`qo6sudhn^fhS|!`|Ul6dwrtJZ>=ix zHIK%)SJ`_K_sK`%(8ujP8G7??TnSGDBhiC%c9suwPKCN9;}f7W}lRKHGyK5FmD(AyKOacfm;T#{&wr>n|* zW1@eruj=2+pNLEKY*mf!PqfCpRViQp&NUG|zt!H8Wq$PIap;5go(%ncBJ|f$XxYfV z_f8u}Pv22Fj-I|JPWefDZ%U3%lZj0NlPsSYVNYImVpQJ8?L8TK+k4hVp&zsNWay2F z(Cc03o~g#;{e?V)Tv=RBznwpo?BJbOo-EFB`IRS2*4!__P}A&gCE00N*!1$S=ZR4? z*?gY8;Vb$!oFqnF)5dUZs;vGQT9N86Y&=-j9QRNu8xmp>N1Zd8b|X2Y;)Wsg17X>+ zR!FD_f-OVV>HYN&32|x7UcO`&ZZdPm5Xtd(uGBL}2Zt6UZjK)nu9- zoG0aB)ihU)Z|w*R0X_)7)RJBt8b&c^Yjav!$C>cZWD0X5nHjDi;XM8K zbW^qGCA-$}d~l~4o_E)Z`KK98yZT0S{6oACA2q7}pvRzbT;anIc0N90L;9L3AL7i3 z+A94vzQI%*1rrWN=5BkWW=5`$tvEG`7Zql^3LIfoS0PG=IHG~N??}nc{Ka2QAf4SuIf2)r8xn~j)FZC(j{?~-(wchjRPbWP8=sn;6mxSkUyyt5# zlic7~OZNLd^S}K*5%bMH=6C;8!}Ik|Bs_miN~+{LA9bE$LfM0y-^YHJ@LTHqu1ePD zOFr*=FG&>Wmp<=H9#3R*hmU#VV~Hf2U6Q{~CizK^{mWk_u;1&>Ua}7*3*>v^=|>U~ z@Alce^H+&%7P)L5deZqhZsEw@+by(S3@vf{J6vU|Y0}!pXM5YnU97r(8JFuNUv=?z zF@m!hm|-)g?$C;;a6H2=iW5_+Lt9N@7Zr$kHwM%0l5oI&+hCS;k|Jvg}EDN z_Mq$oJmSJUfbx1sVHYU!&tFYt4`+w`fw`LRvNznoQQf;5vcKkONW{7PQ}`W094$7} zsqA~?ev0CR2tSm^^WzAAko(~fArLxeeY;xMA7Y1p>HU+A=OrBrMgI-io3Htj4iOPl z%z4GT%bd?%hjX?y?<8GMww)TZZ%CJHMRD?iQu^gq-Qf`8PQiM9vSr ztIYY0a_3X!&g-rt@}3dyt#a-iIq#KopUC;JoN+OplJmeQ^eykk`TWTF9XaRAsfKja z_nkSedUxMTSlmJnels}Zs2_3T$l*QMXf1@eCqANftA4e1yGnK#;)E|}bS0c0V~TUm zIG-;yxg!1k*$`&LD?e(5xq&b%;9zaKnxoZ zn_M1fu!zupV|bYwUZ#bYba>eY(;xbpJ#Y9h&nX=q*+5v;gz7Kgq~bQ15{L-2bMx-ZZOGbU2b~diq1NMn*7)&3hEsw^>*hoBQrA#l}jlzGJVk~xlb&)k%r?(QnL>39PaHK z>K$kuNj0|cXQZKJB-PMN$d=xrjhRjPZJCkOzP$sRbA7$tnc<-g%|~ZS`3r{g16_F) zZDcoVkWywtvCtpdFm+h(Kv$uEurE)$Ivi`Y2(dhJe(PYqOWj!LJfD|Lp{RVf6pG!M zT(M`kKR+;3TAb;j-6%>|ZXi<_=-ZYl4fkgThWk77#VBxblu}o&uP=kEm@k!j3j-7^ zV_uzog|1B@t-~(t9n5T?`uSp%QKo;m1VKO!_~ttM@*(nW!vmWJ3R|F{q1;ejWui2# zp<#LlhVnfmLq06L&C;L zNu?UuIYS9Ftei7-;O5@q&~UCVLk1gqdopf7X1ca@^-)CCcdC1M$|xIGZPrwM3`$5} zbVSl>JU0;KmzrDb?b%2>YE%@Vx?cJd??ZFNVs2Xo)E;P*7NW7cHnwFFu{Y+3m|jer z4IbZoQoBGu8iB4wDB;x1P=O?ORyhahk|8w`^n<4?q$ zHQR;MMoX8Yx%jnE53VT7U=IG6>nrAS-P>rg-qKJ>IWU(&S%p4P7V~<}Je&FufAYm* zp{OW359Tx?BKMqrF`us3Ba#VToIU+C&He*JaA77TVIR>w46%1>5@6Q<_JAWoshqZWYtifVoGn0X3(h@qLUKV8N=1f?+N_BD=UtL{@1q{SVH)cky|NXJz~ zvfe^4gd28e`U^1qAsmbrn~HhTh76J3ZrWA&vynpvjqxlPnY$p&cIETkF2;Ut zD2dJZ8l0XhZt9k#)-f`rLy3$u>Tly>{>(vQ%okO5n{!4Q@S8h4V62rCpaaj!6{5i3 z-tn)gkxU!HiHXSxvZXJh21sq$NP}l+x`6?c_?W#V{z{Hb(P6IEOj0r5Qb>Ppa8S{C zub$*hlq3^TaenJi6xrx=?C2)x#-h4fwwG?L6eTKisa@ECf+$A?85l~rsm4k8xuIUo zO2z}a3tc9wr)CCn8JU~l#F5-w-XQ0my)NTu4*8!|Ku90LR~E&hRs=&u_@!Fv+;i5l z0CDlc5Ud&K3R#v&ALLpujap$dkd*qgwmi5)5^7+h-T4iOgD_6KY&5u4OSNPq!N=%#edXqUmO!c2jfMVvD+|YbYfBd^qe5$u zg)V$)TWhJ<#ae^~Y)cnYY`)aeTWDT-%<^NpHXPlxqVvekBab|4gNp#oH7{#fy1ZqH zM}qmb+PG^el0}i_Pgm=a#~i)ns1-|Bsq?J`>11%TG4$} zetGVQqgO0x?d$CWS!yi} z(;pHSZe@|zwP~X1 z$K<%7-xYL%#L?e7(0hLG!1+0ion!pi+1%gSS11g&I|eW*?IAkp?%mdIY-2J=0|B%< z-!3|AUfQyxW!bW$jjr8tbTTri*)njTq0(feUQOA|LK;A9xJ*h0HH+`|gk1VN$PlBE z4*|AHctB{KlmfGG%5BIu!+5vl`ZeSsNXM%RgOid$?G_mxa6El7I<rEN@c+XlK?``P&0+B#5ZAF?IOgh(@)xJ#~` zm0hl{GWiLzOviaVksi}>#PY+t^X$iJQRph?Nw~F(@jQ{nikW>#=(#Srt%@g7ysyFzGVN$kn-G`pT86;iN}u;4F}Je- zoCw2+d0fr))nv4MEee95CQ|CCMPSUW>;SQ38<>ouhJn<_CeAQ>cFhpzW)7?p3m|{| z$TD4IyVT$gd`k>dto#_e`X;@epUUuunA^C1u04zqyl<4f*(`!citzvZ2UWEao>% zMlNo8&|oiHV|M3TO(LG0Mycj|1G&w;JxG%iNk8IHNuF)9@}#6XJkYDb+d4>x+V;jo zQjH0uMq7}Jr9HtxXkA&>ixhn`jm$(gY3a9*vMBJ`?BEm}M!c<=bZ#;^?QK$eloU-d zSYjJCv0b<#-Y#tIFLXyycVH(qqDZmU$x2J}?*!8dP1{?DQko#G)nub?4IRol!5r!{ zrjWr7(pfwd*&uG;%%<)H6O4L2DQG0qbVS$P=Z;~M+djc>B19M0M?rEb^KXFJhk9h0k=8&|X(IhjR2ztqg;4gGXP z%Q4F*GvWFxm`Q4#AcsZ5^-!VEr<#Pbg-Ob%=J1cUD*E$7xrtD8G@!y1?4Ru%o?F7Z zu(q)yO}3JCDu~jhKO?O=%pGt_68_*bp`8*=Jf(glZHGd!{A@E)Qd!U}b-NI!cVJLj zbW*iQ(U_VIGj1oq>xBeWIL-h$cpNDBxRr;@lxv`CBZ(xmEpx8}8@q3(I zb=l*v+9`JT_MihmV$yz$BUfqmHJMI;Q_M__ESB!X0#ofQY9F#hZ--OdQ*xVg>${4* zgF{}U)HfJXa*=~tlui2zkVsIWq!MVI*6HeTMcSC(%5H=041^pjJ2!)72Z!^h6!hha zRV115$NR}pk&q}km$_iLpt{w=Ee{^qCswTtYOFM_8a6eOj*hCK-wZ>>mn6>16(p8O zebePK$h&dxNRwqKv$?k*brmT(chmeR4-^J`{HPyYOs}cqbOLh&+vv(o`GLx2kXoSh zRL~bgN5EpZbN|E`m3AP9?nprSTz{QDgX@?=FUgszXD4wt)rm_#Q3F$GDvv(bD>g|@ z!ZdZM94e?8$d%KuNK2K$s?pE5hOZi3rS^)Hbl@SmzCsVGUAYZtknXl1Qb*2tW#X## z=cI6pI!1~lRe`Q^>V*#F0!Bh{MV=#5dIvUCB}NF9kv>N6=6q#WsJ}-Ijxk*q#kyXH zIPH=8DOX0QO5mLANmV6_DfEz9!HF7IgzrREq$92Rbd1dH5T_9Z7NnHnw8K(6J5A%D zAhKJBW)et@5zDxf@vE*RpAoQO1NOt%B4X*Um5k>)SX647AT;IQ|<`jg2emJyneZH>hd` znWrPqy7Q#hcA{Ntq;0tdWg%FF-^R=*6`vxduGGjLW$ZbDM|IIU^9r%lI_DFJqReo$ zSTM2+ii2LGX=n?3sAICd)b1!@>Yj%9o0RI2PfY(X;$rF_RoRUTm!BUASK^!v{kkD$$r6OIg z!%Wvp1ngymp!vK-YNzfd0_`bn>D27#ij3|ZrP8Lfp&59vz(h(l@NBTFNw5%eHO14M zxluiI!-&KOhNPPBBhCslu|=EJi3pyjiY}34OOZtdGs35w_u#R2%x#^rO2ktL+;;#z z!o7l`SBliE8dwg&d%2p&iuj4JT|)Vje&NPS!f!7(xBNv`lWSJhixe#@e6;B+rDl{FX=oj3Sd~hz46Ma?8mg^$ zl6{-j5iNyc&*EiEmMm*aB}ix%m*YyA;ib!tuJ-Mf@3um3GhCz?q7Nw?T7UHW^8WyEyo>F?DZe%Q!W3Cqoi+0h$v2zP(u)iZsnS}+0yB1a8FG@{6 zQLhViAwe=7%`}@OfL-8GlPyX`>f;ic(P6E)h-G{5$tiK)W9=S;Ul0XG3fjdB>U5+o z-%uJ79wWlbDk4ax4U8+ZVAXL z;MNcfGol+!-#23n!{2x-mm@66-HT}f2#U-1$>Ww?v&ezN>(&=Xq4*Tp>s{ep=>daIs)jKtnI+$npHSp3H96aToQsD+A`YC)` zsJY z2tOqbr4cLheI;5XUvh;xhp21pY6I9TgNFxokt0xnPo$XFWT*SFL&%4^KzpKioh_Bm z&X*wXSo$i(uO3@cLva9+mIZUJN7qR5Y*u6{^LK9ks!h(JN&KFl-7`p~&N$B1{al`E zlXH+h5L|i(grQxcF8^fb+b)%Q^x$+0;ou?*`alwq?nIzC+&);~_QD0l8Xq%NT!XA*z@ zDn~;Ld8&VX>*l-9iiFy@z}hQpKfhE^#hDx9-ak!!>pUGLTg&KrE+j#(*d3+z-tI^e z_6?OA9%8=^;%;|zp6Eb&Sr>Z>MHk{bH1%%Esd|5y=PaI|^yLm?;WN0^Uia{v%k#%P z)h@1Zaiy3@ee&9N>1q$cdb?+HFs*juba|%3QHZEUiWg;CT3XKag6zeaLo>EjTv^pp z%Gz7>Bv4;OQP`=nJ7I}o4`0R`!)n;b7pD4|BS~E?$1}apCs+mYo?E0 zY6}a?g^Mzz-pxG&bA)uxVNhv~2=4o7po0?b-3dInX1cY?t@U)G@QHM$kgkn~dFfO; z{x72ZOL&H#<|_5G_E>D0#s->`^PqJ67p``EMDdgTh=5KsiEUsK_9EhGJcXaq)p!g) zxiw#epWK!4#lON&;o@uIC%49L_{qI%aPX9SH}6(F(cA7d+?wPnrjjnl-`!l|9X0_{ zM{Y8Pdqq{VLtER~HVL_QJ0jX*NUdCxP8~(wW8$x|`K=27730M}!6rV3zbwxG&hg@( zYcp$A{(O_d${%s%1kz(R?u8+0p9aXI)JY{-^d3FAhJs1J9P9-23Ro|Kt|0 z2tT>^4Gx}i@8{i>V^H1--`|G^_gL{OdBy=1_|Nn1z?&-jyOM`wD)8r3xI=m5t@6$X zJe75vIViEEt!;Q$(J2Bc5uBx%1Rf!`s(d8jq4af>+GVI888$?lTpyE%sL?PDQ=)HbxOJfaiF zRcGxamHf%8-$7^VG1OnHc}L*oQA%8qAJQe#crQ|iwf>5$GZ+)ai8c<)f@|iII+Qwz zzQku101JUdK=`Tu4+CvoY<{WdDS_1wrF!n$Z9R_mhEy=j>>;dE!{g?5Aqnc%g7Wz3M@ zbO_EhGJUho);XukD9u{PXSkT>Jq(z0W6Kw7Sq2g)IaG6l>Z!S1V_xH1a+Jd4FPTGf zz{#hiQ)d93zyR=S;I+Uzf%gC(1U?CTUQaI@?TKupwhQj}^L{YE++zlw{8l@J=r_IC zWbnIfEauti@q5NhLH(R6tkSu{_x4JsP9}|i1~m6*zlBx5_E*eU3X2H#SE9SXZY~B7 z?BNpfyzv3>LOR#+E*>1pcn|NQ-QccQhtKYxPQ8S9J3}zf=Na6~aEpFASTm@unh*43 z%8;QVM}C=-Un@rEMFX6<*?OZpfh(Gm3>(tQ@h*NG+$+7i7JmtG#LqS6g8L@kPXT^? zppz4RMtLtEtP~p!)BV9a?1Oh+fgJAaSI3p>Y6T$!J zuGCr}>*yX~4SGkD$deCh3CO_R&8Bmnapw}w##?7z>x*u>fo&J|+ZabLC!I}z)(ssC z)2SEnd<}(B9@-m(MUeDn^c%Wi&0WLnxonnF3bU>=PEwYpoAJWB^P40hnLW6}9^i&t zUy02!ncFZ^bGB1ga}Tej#lkFBrLC8sc;GBpgmYWRMoLwY*^-LS|0450RnZutOu7TX zyaHk+^J_5mwh?;-E86bLu$u{2qh3Jh;{o0az#tIp+J$FpON(w&!s}z(H*C<*0(R=A zeZhA2w%IrC(q`LFpz&mG0523EHH;mi-c>g+i$c>Ts{@4n?nmmRpzHy?Vbw&Gg1HYYab zT1_Fy_ExCh1wa@>8naT;vpqVPt`qQU@0~nBtBJ;I#81fY(dnC`5Ki?6hK360rZ{3D zBlcRz32)LW0zod87;f9YDu;2}1dFvju6Q43Z)j8U0t*9fT<5VDkt^j}y1TmjN`1pU znd36^^w=T6+eEnvZN*fgWrh@h=9Y39Xp)!>G*gSj*wV#!7h)e9qYB>isn9y?O@!FCR8i>;XoP}S-pmGrA>0M zZ5^s+ZK(LFHy33KaIEqeuJQ;kIoh~~QlqX)6z}aZOT(B$+l1y)l$X-0$j0(8e-!^J z`a$0M)k9I(nDrM8#VsI|@ME@!DWV@uh~a+U*C9r-V~>pcO~Jm7{;?(BSk$+h!*~H$2u28Ld zMEeMYIvh6}M^2v)s;E8rc0VwbQq=zpkS%y5hLxHb1|t4b;j6ule2v(e{~7b8QoAX3 zRU<~{hGj`oHvZ4{ooFShMRKKluGqD)tk7Y91<(0d<=HIOref=t8|>8%64y{Uds3^- zCxcdt)#|4-AKT?0SgfpGHFF}5*eDh)RUu7(rIx^21GcekHxFj!x5S)$ex|k6H*8?) z<@r_c_7nUWuDCcHSS-?KB_^(_jKatjtV1&tpJAcxqr|k7D!Gr)$KpVWv*TPy*kzn3 z!A|_>g83#`Pj&dk#ypjfeq)SvMHzFvD^lGtUpD3x(=^n|M_oBzP^8g#gHr`;hc95F zEeudUB86LT^jGK7pBsRc8E+v`jfGmx6^((IV?U4SvkTA~xlM(Y*v zn%Sd__yiw$?4|A;^cyco+dhcb8f^2-Bw|NM0Rx!AV$ZUIBl(EtTZ1XZc%IEv@TK)p zx+Y-OW=-Ro|4oJr?G;#5JoP{}h>*sis{~aww0@;+{pd7p@(1aPi_*BRal|}a(Q2^5 zNzdov*`v2iOW6dGO5OZ2cbm^lo4TY(x)`Sujy@EeE##)!#2fiB!#fD0vscV!C(S4v+ z;aK=a#B0rTczfE;7Vzm-dU<=pt^`UK{>*56(TP-jz zXX;f$O9N6mH7!w3wmShpYo)jt?OgX`3)HP`H2Sum7|PQN>zFYJ#myg(Ng1W+1Xb6+ z&;hqr@r0T=rO@fyi~W$$Ls1_G57y+Xb7|X;J&otCxJRRr@~(?g(*n9!K@`ynzXl7o z__aI!94G8o98(y=c8Z7F&!S_>h{T2p3PHm?-;yl7y1#5e=a=~MXCOak!e?RxfW=JFZ=l>QOP;0;nqXC<8CK%yD?NhoTYnNqI~L zGt;7ahVfypAm`obUQA6kbtTBy6JbQkZfUd~Q-edOO~hf0x#gD6BTVZ-zOJ$BEB+Ak z_R3k`czVe8e-+cNO@MHq(N=0|tclQ%Ye}*wqe>&Evvp2pR^)voeFcKE4zr{l5s?dv zD9G{e?Gi5n%BWQ2=Y_GUNV|JJrRPkfs3@`e;Qs}w;<+Huh@Dj@J~7e4r5EvX*GGOs z*l4{XXfifgY}l+rF`4~nonVH29lPgg-)1OnMDjmf+uH~jcJAXf9-{FSXe$~~@vco_ zdxZ8R+NdKd8F4#>j4`-$GG({?`d=ewR8#9j1dUPmOUoFu$aoZXAB`r5q%wKQzbcjI zZgkZgd6mPfdI%=AFv|lORjRgrF-7^4*s7I^k8C3`mV3(aG><8asg;$z?m_LXeDu;|mbrFM9w;AyA5EJ$V~54nIA4#4N9ATq zjbofRvbuQ3&v5FktbO{mH*tJm-R-`paA6mxdRZPtaUD_P6t_G^-Bo*ev>HqH@(49K zj*l|x+D3@uTGy24H@@?qQF&Q;f^j+M8>ys>Cg|4WdOp46D#?3AoY1=?IoG?DciBNt z=-(>7Sbm@rb}oD}UMvk~NfHBVo-J1&(1iSAj#JIjd?sRM$Iec298(Z4VqMp7ddD0n z;$#f`NQB@6F&OFTZpLpr)^-W3+5*ICpRB$U#!jy8!17dC&U4&#@}>2d|A|*r@IZOh zh37+moG5c*&a;rQ6bJ{thY z8DMQ5w!Qmf)x$g%$I6H-*3BrF)r|lv^!=)1APk#^uTA)TyotlncD@?j&j-sYccPS_ ze&uYgtSlB>u9jF8z5{J*+4?`DPY@xdWR9T0S)mB1D2Wj)XPG=|EfF+9D;hV6m`LNl zeO}lOjz1bRPgG(2ymvHaS}E%J40v2Td!1hgvFBn6s^yK&T{(T7AYvr7Mt#IlqV{gC z6>U3s%jgN!<}&fIv0-yb*jMNul<(9N!=k#6@2Gw>amB{?b0U2CHm#$utg#zaqhiTo ztgd`yN2$25az2*WzKB?Fa=4FHf0aYO{}b0@Mf+si?Xb93HhILn*mY1@VD-AQEVzO( zm4#KTN6W&h)}~Qtf_MHuA_UfDe4}&nXn@va)Q}yFYNdFpQIPUojne;{5%zq8*E;vJ1!_l{2Hsr&k!JQqz#rCv9fwtGb(-e+(tzo5%> zAMQ8N&MzM3ZX^DoKQ6(od$zCRsk@jr^87G?r(EdzEu^;zx7y>CJm>Sgf#(zC`^HzM z?S68IcN=cC^Zh&*^L&=)dg$QH%eha9JH&e>Zrw2rHfpk5%v89xH*C4N#`dk8r6G-4 z^VFh)-K=J31;+#f_iA@|q(koQVkLp)zsdH3nl(6f4V({e*Jw)|Af8j9V?92oBFb9Nv22sy{`&bC{H@pP)D8zuQn zXgyC*GPRI*S3q@0fHgO&s;{=YFUv=T~cjs zZr*t|f1)_bcIv<0F4Bn_ zUCuCVd|lYb2u8xz*!Yg^`vnV1V@7j^ipGrQ>?w^IO+t(;AVwDdC|9uX=aJ6|*Mt>h za|G)JmU(dvg895qx2j6JsO!yQo23#A@VJx$o{M%?#I9fHxPGBn$AXeCV8k?&**H6q z^o;vSHqpJ~TxN3YmxR0V5RhAS{-{BEvjL36J5!zv5VN?)^JX_K1*S4c8U7J0tT59S`nXRT zO}m(|Fq1PXs9C)XJYm&zcR&7kK6fum1HnJ*{e$~#+>%WhaHw*X`_Zrd;)eN#GhD{wb(Kk#$l zSHLsC-+^h@Ab$h<1NsFTz0h2-`=Sc>D{!wLr_CPNzx{$uhF9Wx6%gE)@c!zA`%>QZ z3uU>C_t>U~i7hovs{SKT)i=}5R)^LK>htwxK8L-)wS2I4Xf>?XTgA*4i@9wb?r)Gb zb50%?0mAxZY{J=>#@vplsYDPvL0tAl9PfB35{1j>WgTxhFr9ia`91)#DSwi3vX~RT zgm0Xp+#joQ?gT-$*b#*7lMtzp*7ISmlAVJ4J-p8Y+#KuXL5=DVehhP_abe99-1Fr!% zNg1Zb9@nMs?__cI5q)F?V=HF#L*MP7;zpmPKraVE%i__A(%DsfB6bg(na`n(vY{h;11 zij7%*O6D|ryS6zuANs%m(Qt|V0cQ<3PVWxz4S|<^31-P??Ozgr+xVT*wzNvVh0>lA)rRxY+OBFw`bQc;AE}Lnyt;Aw*Ex4 z11i3VqL1iQ`_g~ao~RPh@>MBlM{J({q*51N?`-2!#mHu!?B_v7!cD!CaJF$%(2gjC z{V(pm<0l)3Ih5G!dp^A1iJ$Pok0~p41<<@?Yx9yq^Ah%Bf>~!o0{1C9Y4o$wOKrg( z@CM>OPNPd^tB%TyX5WQ_)_mMw68BKzR@)7C#u;@U5NvlBD2&SXk8Y!Uyh9M7N*|`M z4lz;7KGzl3_bdokJiNMXl19{5Ws+@lK4aPXJ^JEJu-pERWhcG5PM8R5H=f$AoXwBxe@SmI?m0HaQj&hQ0h%&Y1sk z8MS?Ax5aksT83hnl{itE7tdyq9m@ATXtp-}4dk~(d6ac)gnNa@4f|QahJJA0LpzAC z1ow{GV6EiyUpRn057K=rW2c2@a4UbEa|ZWIaLYbmaOZJna66L*Htm}@+|yU+$wILhZBAh@4`w@IGcjk3Ii=c!C!atB_wfxL9~>DJoo5^H)l zj}kqa?>2q_TfD#pz-B-{&4Yp~<=_|GS2<_o9?#xN*gJV`1+MlM$wU!YadSEAb^Tg{Vg^Er&kFh(2xhkw=x6Z+FQ z>TgQLZ>oNY`~qDDz8P%xDvD%k#r**ALEuKQC{rP;*D)cjO2b#Hw_nU!_0O2S4 z{U|V5G!)7?-t&|3$^(}%W^$DCI?s!P`vbTS#Xq<|f?M-ca9@dA`V+x@1#Zz6A3e`E zpOHI57f3L6oA2&iFn@tWarZ%QwlCQJFgwS=zBTf5Xjiko8`SvSfsWfOvU#ZVe0b0X z@^}VN`|20Q(Jj26x`kD!`Z!8X;!#PsEM{mri975qA z+H>ZN_Hs`_jADKfja>4;x7QoUtwPa{uLMjz65+3m;Nd;esh9C=psyu62KVo1tPJjN0N(_@1$-O0ANUUN zUEq7b_kkY(KLq{_cmVhj@MGZL0dZxO=|SKpz)yjn0m*b9!u>Gt2#`$o=eU*bqrhW; z()|x0iTexOj|0C1lDPkgTe!ahehmosH$W2i3EaO0o&=J(zr!uu{{o%@g!_9SiTelK ze+2#nBypd{E!;l?qG{oZ_L8{I;{Ge}Hz0}ocih4ip*8@*ZR9xxmuC(k7AUbtmByo?#E!-76j{<~yG|wdNF}T}+V}T^@ zO5DOdj^`>sxH>mV;vSDX3#WL(Gl_c=?vsIaKoa*9+`>JT=V^d& zPv?0Ca3*jT@B-j$U_EdSa4sOadLhsAfENKT288<(p2>V)iu<2{mjTIqUyfV(w)5-& zgq!2p2^`G3_(B)&-9R4L0NAvlGn?=wRyU9EIgK9Kwr4|iD_^2?-&-kx-vdsDr+D&4 z;RELbn}9xGA^yszpZ5VkdozQ8>V5%G1WLdVFbr%4wg9U8R-W5{3xSIOmH!nyH6AYp zUJ1Mk2;=b*-d_y}_fnph0ha@>0fc)6PvKq(Tm`%qxEgpJFbjA+Alx_bd?WBC;2J=< zZ|12ozXiA!cq{NW;O)RWfOi7IeHYK`fOiAe1Hyd|&-Vi23Z9Mf0bHJx@e+Kw0 zaC=4A=Wu@>xC6Ko_yQ2-tS|EZufPcKB|ty*xBB(VxbFh)2EI}eb`S2a0`~%61MUL? z-q(5m2JlVbTfn!0P|o{#S2@1}d>8m05b(aw`wxI00{;f^zvk~XnZFb^4L?4k@3N6M zz51y$9%s+I(ACzqInSRL=DSw*^L2*vxm%UPPbz=->anWYANfaLStzx&C1s&G>zls+ z%>18y^P5lo@Ut)d?ujRFI^wq6IiJ7xq^qVcK6Ur@)-A_Av2W@l+iv>V@X8;jKEL7p z4_x)RuiP-}(070TqMc@+{fndjdd~2FKJxhCZ+YUdESq|mR|mb7j-vXP$wmoWK+J~ZBVoaM$y)`PMi0zjFk%) z)h&g`4M;0}bvp}N^W9XguytkkHXSn-sjninwMn;Nh(<6J)(1Wx-d2)fKQ~M26mymB z2`V8j%yDNHy)A7(YuByJbK5}Is=DQ^6;K$YIGnP#wOw?P|Gde?7q2RJrOq6B!+ZbK z{);DuE+4%A>Yu;*=Fk6p_S;wd;Kz4#yy-)OcU*VNnmUD^%(iv)EclP3u?1A1!HV?= zXZzZ~21#2R=X-oXmnN>?5<6T`Pa1smgrS-(cIn&m6tcWl?$d99jLPG#Dknqizf*k; zCmDrj4r^<(ualJ*eus-&o>`KYRMLEsx^vyRL02k_uMYC%XK5Ga2Ul-f8R|(fO&tTB zOlf7T3JVHX-C_9 zyu7@;k*dky!d@6_l7g8@*6;pE<7JPfb~<}{sr!UJ)IpMquyLan3E4}Qx^b=2iB9T^t#9-F>?r=z(`s}4YJ>}$U z-u3X87ysp?hIgDen7}D7{p8IZwa1aolA0jv4Iuk`TRFE&)GB5Ue&8hOVzLL_b6M6= zN9aFF?^JRjNL#w+o5$$9^4=?_uUey{d~}zwgCug@fai3gtL}?1Mo-u_aHgq3)GC-s zAzWtRgIoz2EJ>}^3BS-uxMK{#$zrQ0{q^!y)?|@bZ*0A3)85xybLLYA6sNqZwBG@V zLe`j|ZU0V_V!Wl7K=6H6gR!ys2oHaF>(4&*AK%{agD(xd@zC#n`VTLvMvd+cd1(3*NTp4Q&_iXtUJ8E2Hch1OS8RN1RF0P@R+}UvjQwPJDn3h26Hc zHTJ9{M^$jJ63TJWQf8gR?*;9PGlxo)l?Y7#&FXGHSEdzPqCMe9TzX0jO+YY3wCXs zgng9<*htY{&5wW|1OE=}M7Z{N9_0Nez)yjn0S^ITujXOi9|3+2JPJGp)WiFaQNn&P zO4#G0g#B`qu>Tw->{p|N{d$zJ-;5IW#3*6E9VP6^QNn&VO4xsm5*E(9v}bn=efSj5 z&}YBr{SUw&fjnumR`=E&whD8d8m%g3q2ZW$M&v{*8m7 zYK^a;l^FD$3e;a^3VJ#>;BLWPjVX1km60-Ab+~l4S~ljTq?I}DIDOe?LvS`{mWKR3 z#Y=KN)l+|4#?e=-Vq!1{(W69Yh8czk>nso!bEE7Ni+_ zy8^oby90B9pwlJ&fCWHsSL!OxC+_k@`1^TR`XT;@c|Qm^104|_5zpHLy!D0t{Aq>4 zp!~x;uk@W}MjUr9{NOr@7UA!WpBXhcaR9CM!hkM|?t`D|CD@mDr@yFEz}SC3{D-(p zR`O!a{qU>9+4s6V|$$(?j_H@6x}3J=XlmF7#IWvxI8&$cE5n z=iJa+Eb*CZ>@Y0pqtaZ|u`iD6_EKa7!fU?S^{FknZO`Yd@9WNHj4}&tbiL0x)b%Lb zUhI2L&SxsutM#O{=h1%QOzu;Z=|J3bd8(Wl-sb_LmxF+VfkS{pfd#-qU=eT_a5%6S zXa-tqc=K8g2}fpx$sz^OoxQ%>Xkbl?o&Od#lT1)a0t9u# zGU)HT0Kc<=^+2fSIlP|>ybw4K2>Lqp^mpphF^j@+T$_C3^U|q}w58;Au3~{}M@6pFaoJ0Q6qVl`R&Q{I zB#f856#Rek5CW9;%kX=-2TDLmdImTr^rmq_mAo^yDWS`L5}mjo)A6znr+|!6E%&+i zNZ~hR#8Y{-lTU|-5G4N`ew`jD1Li+E+B!NGbtL0-5#H^AYgPR@1Vf6d?(r8vDfK|N zKG79lC<+PiR6cp|H+Uc}W?Z->CnFA6{C9S2Vg$wSA?`*GDwmz0M7MglF=QScujo*& z+?j+X%_hOS_Ti2d_>$3=@N|9Vse{^KGbzs7C1O!9{zo=+URU36ci#H8WKoE32C(_#0JSSM))9uCo73hg}^GvgN-6P@N19t{D z;*>5iLw^h~uJ~jYF6dgYsJ0}bdz@iDlN@mY?lAWic`pG&z%Z~G2w_`zSJ+ly8*m{I z(vjV~R{$3SuLSf9bNs7tUjn=uxD>by2w`FVzZ^f!|F7X0=J+dkzY@3#cr9=>5M+bb z@&09V7T0o%y8^TyP*y^&vNr;p>uccslQ(A6jKbN6d!vot!Z zD6>DkoAbP%n&0N_1)UiTC^(}m*2L#9m&;C9FkOT&bULz7Hi$x}JDYmJ?G2@@wVBX6 z&OrhT{{Akrcb=YSp6p(o>vp!2&JFtfmNVY#^Tl4SQeS9R9oE>Y<%E1>E|3W8+|+m? zTw=B4Bp2u00r<+M^NJi?tfj#)6X^4^IPf9bBDBk`ygLqoDol^x&~RL)HVdJh1Mhj9 z_clPJz9e&a^GHMfu1qS^%P{F3*f5YA7-={$c&s~P?HSoK9?Ljw&$Y&rEXN>JW@Kc6 zS=o-;<2)8qz5^3^YONu+-A-Q8xumdU>yj5;m@gKZ2W9YtD%dshjXA6&bz(z|!E6>a z^eOcoGv*+B0Ye)@9X?2UyOExLBMmLz%TRW_qB?4STm01vcb&Z$D!__C;HjMp7GWF! zo5E(k!|~Z}_$(T+37^7(MH};5W2~(e?iVdsRO;$QbVJ?k-I!O#<(j*&s7*eEzgz;TM)cVbBf_I146D zXb&zeIm3HUsR?dww!-o{bKPOabt{hODicyUYPa0s?&}s+vz_f&U|!R)3v;_SOhO?j zjxFII%JeISDt{C7{qE1OF-f<9tF*U>U5W{@?hS>t_|_fKe1G##%IEuc?#H{K9)!Q)r(~aWr1Eu2DV0^DOAK9q0#M30w_a4}1dnBJd610pM}qkH8Gt zc~4*&a02j8KoNKq@HRkWi=h^cF}Z(@9kDZcif8Y`^IsVcDyPCEKa2oDKK&Ga`h_w2 zCBl;KFITwl!ad%VU)ZDBpK@rACb-{48V4ub)2M*jE`&eKdouoGxYdRsyqWaWzQNrA z{yf}{rm9bdWQ2zqv7OP%o=7P(ymW;zMDD)die_P_W&OuJp3qP>Z|xomK>eDlN|1zpH3c^(aCdt5MxJWxL7wWzZ9e>!JT;~+_3kTqiqE})r{cfOKf{_z_wLlLVLdhT z=jxsKCti9lZT>ajKH%%XH-K*f-vYi3+z)&Q_%850;QPQ2fFAx#D`zq{f_&Cw5d+c5cNP(~i^x=No2qO}n&EDz z=!h{v@vuZ1A+d2?Q+t%MWGJ)f@iE^21Na5-IBM)xg=FTJ zfSt*4Ndft)*X_!hNi*t+@iCUnCd#Zw(6^q6*JH``tjec8`z7V#>vk;TbF{e5L&PY||dxHMGg7ll|UOTWc0 zl;z_VJ5K8pxW{wPo5KmjtKos%W>%zL+2~2q4dwhD@Baln1^gcP0}$xwkG%g0cpCUK zpkFA@Gr0c(JPZ64_!|(y!d}wf@zY+C1O{;zwMzr50XujWKqW%7FoC-nC84xLpfo$0ss&dB$K*O#zb4PtQ+x|?;Soi>7%SM5yK z+^&w|8bSCd_8uQfgsH$rkS&-y4)5q_?&vtIqobAoz+t=|($UePa~>Tsyt0EGc6RVP zj^i;8SjB%HEBVjkr5&7Z=u~J?N5@4S9T#_`nrNSceVgvY`_8}|U>BhFZX@kXSm;~H zVY}iE{A4%Ycb6N`T5C@r#NA8pfYw<10{Vr%-VgWwzyZMXfCGUL7U&>@pXgv7&%m$e z^L`L;FmMQPC=lpi0q+ZeMZjUe;XuG!%)9WKffm5FExNH=(4Frb?uqn?qjin9)G)x9 zE9Exi-AL{B_R|X8-CUqTSli4_A%}UL+ngl_m|x(Tfx&T7CrfD7{XE4Zos_3Hopu$6 z2jM>@w#0Janz_TF!g0q9d-})5q5iF&5kga96|MB?5)TO( z!8J?S#VX|~N_0QrE(LFyhXk$~vkVilSbhQ3@;w5)e4#>*)Nbpv87#-L8 zBcNKoM}c>=hcR%i{R6V{JqEls4+-2__3a%9h&+XREO;wDByelg*Qg}I7VdH2t@1EB zuBW;P+vWXyaF6$pz^_$qCqQ|8<(&m@wTA?5jdF|r%~GGY_cN z@8^Q|LJtYtTJ`qRM1*em=Yjho4+;FJ+?tpIhRXV4;=aT~B7Uv>n85<3qZNIV(Q$1KkC0X80(gTSM#nXIs0{Z4@QNNr$MsT9 zgzd^*0(Z#6==co73fy7vHhW0m*6J$=)!Ew(NvPgiz}@O0fnTG)2n~olg}V*B3q2%o zYvmop&L{=pUj(kszy%5X8hM+0=a+-9 z{s0$nK!>n@sxx|Bba*ug;R69S=z#tif1sS~Aa@8G3~(U_^v_smseAwgKM*z);O-vK zKV#7g`(I!P8wPOU2lUTa^z!^K*a(1&JfK6^gMJ2c_u-ceKtlONf%wq}bO`f@HU?9t z&F{AbguMrFF$Z)A`=@b*mIcKW7Q)^KxYz^w@37$W3&>`%f%Kev@1yt@#fXh0d{|0;TG5;%Tbh)zuHs^r;8~pxu&2Kmw{}fCfDU2)P}kpI`-kx*09$%M|BT7j1GrZQ^v_smGv^l+oo_wBH5|}CW5GtxZzvku2ym|t=)b{& zt)t&ysLXEwuIYgO8N0t*gh4~tW`JusphMU{_37dLI7|}gvbO^K+XFh3#vj^ye}4)C zhVpL%xb_1&g#AILqpndN485q_(KqG)=0j&Z;pJRc?kIVUn4X_(x z3pn9^ahU;z4|g=<1vra2IL83zEWbF|t=&PIpO|4-O5hjLCrSpW812C))pe}uY`T=1a6g~j_gMfwrL1&!T0bM=dTVzm2+Xuw&kT%5gf=TNZ@IyRt z7{3ij8w`P8etY1D+8c8p;@5`!hw}c#|A~nMopW=7IH0gw`+lHw+*7uk&-X5aca0V2bM>Z4W3z1_?kOkb~YM5RHKPL+6xdAy#8EQ_0e! zaP%1)6bEzG2Ay9+4pbRUp8y8%r~;#LW~TqudEh9BLkZ$TX+U!{;ORdYXFBy=MlJucnkllewqgLKHN_j<_CzW0qo))3^AZ`1oGKggAugP<^b)9 zK*p8L4jse%SN*%cFapN#pp^+^3KDG}<3QGIKlm4?@ByzA=CL|ph1x%@2Q|lRs6dZ3 z+8rt&v_OS8P;3~0HE^5#Vw!*hQv)Y6z*+)H1{{30qmMQj4v=6?#Cs?a^oi8Jrh;Zv z!7yeW@D4ydVQ&N^lJUs)ZH_?SWDuV*ns@Kte79n-GXN(D2;-kT@S_>q(S#zKPx!PY?h*uHuJ3mu1wCE~WivFpjkkLKB4GBpmINMpFcabn7Wg@{z?XuhWtSq7{3efW8@Y79eHB$eOsekXojWzo`eLTK=VV>$S;e5zuH;@)EnYi zJ!orSsQr(%HoReL_zPq3*S7vIG2q6pHa0oH%!20xGat`^>v=$50AU=6hP43v|5NiZ z5fKRqApzQ*IDQNVYY`8Q^%c+(pk+WSfK~ym1KI$z3Frr)Z9qGK_5fj@fMXp21eNVD zfv&;A5EKqwV}Ac0{`a(@JTY-GzYqo^W0(p8p*7gQz90YZ>4#YcX(|6Pz5jRFgTt)V zu7SpY)`=K@6S!st^c9Q^%=lRZ*Gqt)@iPs+Q=nZKPe9Ok#`r@I{9q-AdHA7cjt{Ky zogLl(bqR_&DEZerzmH%sHVfs!ybG;uF@9W7W@ych@e>~SLC~)?*BU76ArWik{RQTE zz&{Cb3_1V8ngJ{{CqWKmgQoSrV$~1qE&c*y@;!fm#rPM&Sc2vdE^Z*k#l@|F{-<&Q zQ&niC3fg0TrHQe51bst6-(P>XRmHuZ21tk-2!#Fr;}tVLG2_)3tl^+D!b^ad0b%?X z!S#7S(D&%SGmm2f`*7g(ixmS1;vdKOp)jRGet&Qc@xf#6`+{qMLw*Ht4LuL0z--P9 zNThRr;4`cd!?27&IP`sm0)$>mLMB8Zky)_TL|a<}L;l|_FBg!Txe3x?1O)N&9|40F z^PJ*6C8SPaUt|g@HsH8}45^!etqHV(40N>)68KeU26td?5N*Z-L^Z;E#6l`9 z&^kgu0S@LeCrHYV0n)Py(&j|llSd1RF@Uu{*ef|S2Lgp0K$*{e>p}%b>t_8!9{-r$ z|3UnNscnCU1YOC0wfy&6)@y%#gN}yTv%!p&H84(20>V2q9#4R4{DbibZlN9Y4km!c zC&PiT^Y8xH?>0v>XBS8C(F?7i&}(DB{EVp(*hPH=-apVYZ%VMg2IS@C<>Te&72p-* z72*}<72y@-731UO7ZczW z;1l2%5D*X)5E2j;5D^d+5EJAT z6%Z8^6%rK|6%iE`6%zwR6a(3dfuzL%Obi6=x6ZHsuwPgE7tnU`VF$hb4moLP>N?~? zFyi(HVK<@A&)?2BfbO>kTy-P<8b6>P`po(5%sQY?u3wJ9AQkhuNb!nn+67@TN%m-!MuH|3t>-$}Cy!i;%ulNk5$%F2)ZqQVX zcCpb%*f=@De#Ng6#Nn@4E5P)J^bB^u?xe#YrawY>?;z-TW>_)tb3GaRr+@Uj< zQ^#Jrtwa4$otjX>_1Ue3@_XNN(0|g-N2Q~GMqKjh(r#J;{h@mAm}=>5hsW1tmn*D5 z{ET&6)3hO?CV{I9nYc7qzCqdnpE7bSFu^|q=&n83(`NS zZq8Am1X-?hG1BdA+|Q9vo#0c2owGS9n>)3;UWrhi)R8nLi)mTxB}?%Zu+TD0UQza< zD)*Sj8@iy6bpsBPLu`sd*>=j9i~Q|0AW8zv{`NI&sxXzFAuwCEsjp@p6xzl(TFZL*2+!iCQ*+Ce;k9#trmm5q=<j?rGmoA+Qp@S`d;jNm-Hv()tMtv%{@Kw#${Csq+v=l zvBC3IFB<(*^x$N~3w_Vv?(Al>d8MYu%a951>~?DMekbpVzHtc!Cf&Uc-F4~I=6F%c zrwW>E*5m5}Co0avecyf?v%4nR%J0L&I(#w?$>7B^7-k!EdryCR`O4Bylwe`41;sJJ z0t4z~;rqx>uzIS3b9%UU;zf+*j&43eBKZv|W7aOFdB5?{I986Wi=sb!d3dv(#jeI$ zqY*yt24f{)Mu=c7D$X}^2x?Nrku7)R^~w1@9hr0&s?QsJK~-*e8u#>aC_ayM{k1j% za@eG3d9#o;XNP$63ir!MYBJj9Ct|X}uD&P43af1#nW!c!3Gv8oJQ%ywtj&Ic#EL$0 z^>|>uk)CxF%|MmeTso=avO*R`we!UiW)-%&Ct;LP)*qviXRlDD<{N!03@@Y`Y@Bm9 z=IbSyB=FMuO2I|iC1^@nv(rpEqH?`pR&SF6N5<wUCHs^Sp94hb9-KY7GC&HH#HyN@sbD@NM zbr}BkVW31zE$x?|X_ST^m(<9;eonW4=bSp}`IgSDH*bczQx7JMI{PdxLXz~fX`4M< z#)Yh#fgo{*mv}%(Vz(U`6GL@$t^-@57OvvoE+I{wxE+;V%)oeoybacqataUb_d|f- zljYO?;hor~i@b_R<}Iv9WaIq~H3{)b?H5U?^2AqeU$s47shhKqR<5VPo-{1?S=uKS z&H8+$IOX(~OldWr&|j(HrO7Axg6HJj;M;VkiG*w6&upi^ykg_}GJl=a zCEA~}PX~YO)QaO~=1D=y`_35^VJn33_61S5NO;cCeQaq`+aSj?FmI>Iy-_=hAieFz zT1NBsyN^^V+3SULiv9#mhlwYiYX+BFsMfQ?XiqWp1=&n@Y$o#2@@~nyB6xGcX7LQM zH;#C-c#2RXDbB_eG7(hPm-;7j=^is4qA{Z`SjQ#ZQg!tEc}>;}4asr+z+%|g1;{{pESGlxv@}>E8iR@crN@t(d&sXADvNUqFTOWcdpoN!d!+= zks*vGaBWhlR$|U4aN~pxH##)r0*LxmWVG+QIOJaYwxAqG;n#fY;eMAbcABY zo!3Y4d_~OiLPgtotq-)kO5RZ_Vb68R*VIzNG&2!rNA(1lxKqAS7qbR)=c`Zy&uEj)ap~+Z@e?9zz-jO)%fLlTc?0j}}BBv$A^@g8zC*L=aV^fsYbzja) ze!jZh#vSTJV;I@(Isp$$zwG8ERV&m2-zt8M)a~O6GwHh?#VIRJ?)ph+c;(WT%})Zs zl8xRLYWzHGcGs?&*IvO`-5vpY@D8NR>ea@1A-$(q0SQy3Na;ibFNd?4e`zn))E*rZ zHC`Hl{v%8J9@PMbB?I+!m||$;`YV}iT(ivCd667`Wb)~6{0^j7unWsw5ID zX^g!v*M3MJt7Kqv1b)tw9 z;fkl#8v_4+bc`RRO=6+dAN!UQ?+a1@H@?o0r&pSbNPI`+tr_(gPMvGgj=%g_dAVKV z$Mo=l*M$#ob~1D>>3pc&4zxfL-$LD%JFCL?sYtQF!=fJ58oJz`JJtQ^ZlOwk{#YgK zOg^VCl0NVumyU6NKjjRWV!z1Pkol>fmCNECM57F-$Z4i_d93(kr#`t<%do33{?yZZ zI#{~_Fhb21nZg@nk|Ez;I`~RtkwsZ~^il;-sc@8XM~=034Lag#%QH;T+dtwRxkfm~ z*m68WgiCOVl&Id5%#P$5NA-z_KvC>3l&UL3eQrw-C3JIVA9C zvaQ9tk_NrU_CV}uclgv;@$>m;Q06H3QTBTmy)wwf_1XD!lkSA=6}tCJxM_MIkXccBtanT-AJ)`W{K#NE~lT7A7NypUMa0hU|Gw}D%};&cg>=Ys-qD- z(-R(VvY&H(e6!UgJts&Jl$tnLeuZ94U8Hg|n)wLrRXJ>V#QhSSSSF`LaZN_VjR)96 zeXglT#lBpYY0Y~|-uUBAbo+yr^T($@$JCHtrKvu9Whg~2IqbDv=NX~MQuu7xRQK_+ zfWk6@hPw^LjO3pe>E0+zoJ2i|bmS7C%cWM<5TLd#&{{s3VDWQQO%a~PftB7GZ)0ue zb7wnlCJCt^^V`U6IvysG zy~}q(Dy6?Z73Y1u#rH)c6^@c+gx!?58(o)}SGNWVhGWoR-}YF6d|D!l>8$TWm@csz29Zij@Kb}o3#9Oo)oO!30aE` zZMCJSt>j|iBTTSQ&*l5^<+dX2t@}ykkFyPeGVu*Oog!2aS1V;rgYIq2sC3-Y?dF{z zzmauyXU1hcE#3F3*SSpH(%kos_b@=6`A1)7uaS71}?uP zo7llYMM}Ln$7vSqq~5oyRNyTGbBGdFbCtZIIvXaLdW}w)e*%i@#; z6_d1!{Ue8rok!J19ydqp>5=jZL0HW}pHj)Z!qZO=lA<0BXTNe|BX8AC;t9LeMVaQ? z^KSX!bKNuc{@Ywvap+B>nf)~%U#2>FioZ6A%$U^Md4YK$@QFd;Gw+4Q1aZpbdo!FC z7U3HSx{qfBr4{Io)WAGbUsZ@pZ#BReTfQP1=g1OUip&(ED^{U0aK~tye zoXMM85^|q5Rg|0xy7MJgt6Xf(qqNvyeTNYp17Kj?&4Wm{! z+9F9{kkAk@vu5@b7J=tfZK&5-hI0gZU*x1d8i+*Xtg|io@wlAQ*WO?}KaC;?O>qBE zEOBCBC{A4HOBYP#%U-`CT?9eoJBiRE(>O?`>Ai>%GyBMu^AV^0N99l-mg?McrEs^( zFNtm!6Ij9Cepw=a&~>pQLm$`Uac3Y|?R4ICk4CB~vKHlNY|6tYyE^LQJ;hc74{2I+s?;Q+;^?RUM7PRL&at@?ZJw`6 z?=ju0xGFNFj5-sCdSw+uN;MWjyZlCVeC&h-V-c<^B|YUA7VC`+Ds823_UkRNbfim& zxF@LNMHSCB62x|BNC$HJJ(b4sk}?xrutLsGQbax=N=l(3z==9?A1n9L2(7zUNIlN- zv{AyiuC(IiTtt7;eDTqI!|J_3$8MU>S1E|(%sw6K-^}rejygI?%|RQYxR+}Zl-PQ? zdeG1Iia3IUw(h}Ciz6>Svqdw%ooXR>p5(6C#>UB`5pd2wOMjcDRb}U@#Q6C9N=nO@ zz}0s6?ew#`;o6VP7MrWhEqJBLQEhGSmzJ>h=G<>c-#Ob&y{x}Cx&FXVihto;OZQ1* zc)qZdzEh28m!yT-54mU6NUft?LaOt){>0cbqU5^l)aAx$jE-uP@TZ2+pT7=l!AxsT zYqAe8x|erNwpH)^w8an*sH`!K9KL~0qr zTE@Gz3Z^PMm1NT6(dNoWzy9#zy1tWkc801Y9A#L<^!|lylD6bjATR6(52M@l(xm-+= zG#$@0lEVBm3~l+w-PAl~7gX>|DWQGWLyqB5!e1N}vhd=#2ifm`pIC73Jdh2up4oo7 z(xYq)d-(o@C6l$AM<%B%&6f#bRGzGD5cTqwQNXh0=a8~ZGA7|`q6(|crNR+k%ThO* zU^kf~UJtJA4v|kG7x3PCT6!lo&uRTkp+2mbJ>5h}4{U{0s*nBE(11(I0r@ z^o!7exaU1IThTgF{i7m`p%33*xUf!4@xw{J{rt4SOlqEN&pSOwgkBJl$5{#UOu_t~ z^2@FMw8fA6V@=}duIMq##1N(>QPf{@AMqRY9`d@JNQ__l8DW{PYy2+Wl;h!)J_U?H zj81)d;!gHh`J&vF=LqQ%F3P8;E_bRa$6ULa9hsBRphbEv!@H>Bwkm7xfY;D^_cuC0 zRkK$HtyyX{q-}+-r5h-tv*r_&-hUx(B{};|_)#aRFFw4nK;6Cg9F?K!%kM?>7I@A_ z!aj1`kOXw^kW~(P_sTUm;rlHDQ)An zQXGGF&Iji2RL`rm^@@caO62R(SG&%=CSx-m_hQx0i_=t4+KvfMh7l!OwhpxBeOo5> z*sREWfRd!B7WYVdp{wJ_&2>LO4%v2oo4W<|_E}+M*j<@q9QgSYF~wOMAD=5uy5%Ns zi&7s=J6HcD#%tZypG%{drx}S;YGD!TV*2`8l9ns$DQkGoLgv75h>gu!)5&iH5@XcE z%wH~$AAfnbQIwAD$&M1aLb*l{+Ch9sYLd zKFn@=g#x!_)B)>5j_0}^$1A^Iqsy%3Z5_y#X z6-Y^(-`!K)nv;9nf8dr~VKV9Gd=#Dj$DxtTcdxXb7a7xEzAJy}Qjxs(m2;2sBVSQd z4W{*e_@HgmHqsnMOyvoMD>B1)q#Zjl8=<_>S-4Pj%FJ>+iED zr^#$ViR?AG9TMbY%}wef-S=QEnS~%0Hm>8#^r#lDlfTt9&1<|C)AH`WUHA$o<7^ZxwZdEjen=BMX zJBK7sM_z0r30(en1%w*%SXxN zRC66x=Rb)Gi?|-Yv0F%is6R)3A>E{2^V&5>;<(~(G@TZC@9xlGkGhktJ0K$2C^8o` z&UKOt`WStTt|uM{LfBr^6!KzWFgwybV>ua~L3>inZG)HAcOKiiih;JjoFd={=hkXb zLX9A^kupX8CPHlEBAdF(b%U#-mdJ1UI&|Eey;Uv*UOuvh>TgQel(Z}+he$znbIGN>?f_ua%o>Bi>REE>z$6yRsY@6}b>ZZE% z$xwlJ-Vyo^tsJu>P6!yh1Y3V^=|_h3`f}go=e5*z1nRWwSM1z`8-wr)nvIf;VX_x* z?qY|KEfzS(6ur5=^$f;&;*(|8S5Jkj^S*YmY$Yg`_mk?CT5rW+8jtF3FDNb*?d5kZhHvb= zot9`EQ1SACkKT24d_%&M*Uxh-a*}(G+V?|@?op?2A^G7qC28x-$j#+s6uQ~gdo?9A z;uZJi;ZIU=WR%Z^RB1h^S%vZE(acRBvFz^&1P9Vn=%?Ox6uxJGe<~h#Aw`60bdu_!?xr-=hky&mmVG$Ml;$tyJ!`@mhlw0} z7oNZjyEr0sUMa((bB5(Z>BXoXD%|4sq|mN*jy!z-@$qC!Mm< z?iPD1(TNJaf}iJnOepd1w8b*M?@xWh%JNpQ-H+6@)9q?kRh@+$p^YqKfe8JU1J_7q zY&*TS{oO8Qtu4ytwOX$$BYi#|t7%e_`9DYr7I|7{xnJ%zRwnW9xNp$+v8<*t)bW`M z{~&MJX^x?CqrHwowX;_0Z|7mRS-X%KPI}%hJfVJ{vh{^U`m!D@AvN|*k<1smHRo8V zn=?G9@#$MfS&xKHUFR6#520TroB47&Ij}ReqG+**E!C$1HZVGBWT%L&U8<Abn+KC}r7NObQik?HA`_;N~|6SVf75a4F!5C<<9RnlFq63!E7e zRtRU?+lVA@a!nPj+GU#Ekm2jsexCch3ujh5X5jfFCQelMNC&i{naUNvvxL?yr`If<}LH(P8o2_vJoq9alB%oz!DTfJcwPaLF; zh9r?SG^*p(u}{x#`K=|OkbAJxMkR@_t6iES-!U}m)$mb=qS(oAK6u?jKs;kvY+R2_ zyIlbq`Q0xJ)Vtva8W8FKJQ94er>2uVR0ir{drJPPkvh8IL=ed3T|Uyn6e!<$FWx zE?@V6dD~?v1jp8K;$T6KsYmO*uuCWbTBcYr0_$k*D-#^DLwLyR6lG7FKfK5a8@i*> zHQoy=LIjfxGI2YMMki@Z9Z|nCO>0d^b~#K(X^8w2ZCB2tFBEs5Jk3nM{iM*%yEv0! zf{FC$v8U-b4SKv-2|uR0`i{Xb#Ai5;K7T_68RJdqn(f<(Ywqc;#N0G6;v7%9 zLV!JqZ_gD~VD*Ic;Z+@aojxqpbZr*GmrG^zZZISj^8BO0J7l!>yWcjRZw+56R+TU{G6MLP0M%9o+{IXj_7vHWn&EYLmI9cas$u_4g zdA#trQq~XD_OPN9Q?~_O;-UPLR|z$?lu)mHR1KG(N#3rlc-jXu!bb|2OZ%?YwLOUA zT>RvO`T$e2?=V+0&tJNBvB*-=MRRr+@nWerv_Cv&e^wkXVWAaUUO((phK0 zN#9&!I(@>+qmFC0vejLJ=GM&um8H}jqjs$}!?f5q@;8gM1lp+GnG>WZW|T(|2=U^d z=XLc=vOWe74?NqRr)i1cwR)yV6Ij$ONxEvxMDf1=)QfMnpSw>DwYfCR-a!O~XEe=I zq?GovUkFXkPNKE4S3R~gO386y@n{-FP!UDw>R|H8lCi5+Vadwwk5>`SYTu3RzH|h; zo#*jni&LZ1%hC6%V?s6gpi?}th!{`PJNanC1g*$huD(ThTUD-gVT^QeklH) z|0E$j3uCN6i)P6M(g{*xVcLGma%EZeRJSP6kEI_o?kSFv-hF13W6tWMW5{Tf=9k+> zpI*0A^(BxWsa&dDnpk5;rIHYC6#6bQS0Xy&`-HJJ8R2*zDlL0p@$F#w>C1i>VdoQB z^b;Lfz2V>Qjuzt(qK+!iQYx$m?h%xBbIT6Irh&&oSs*RB-auB_5ttI9%`wQEsH*4Dv(= z;<@*i%ha!l92O<@=<{swN!a*92a(Ac5n*N&R4=sf2pQPkd%$DGJrQTWt#N7-jkOO} zZBWZTcsTrXt#NrwqQri@*?=4ejv$ZI6V^est)IFhHVr>IOS8=KQ!_gH6BB_@)DxQS zh0o!%yrY9zm;BQ8q~^$nnEfS+wSC6Fq<5+m^It_=7Zc?vYiP4{f9Ex+5};1|nai+F zPpD5lHrrIy5$6^KP1v+qF^AjJTMOk`kzKD5KjixD%5qPUtdNHFdp$o+XMA(Cj)O@} zidEJw-;@oPQYFMEAw*@^j6UgGs49afsUlH3o3r8@JG1-qB&7nc=+0!E2>zaSQ#*i! z$?1DT0_DT<E3Q0A!2**~XycJm1tC{?nyy(y8_bGkfGkrf7{b1H| zmY3>XsGG#*p1rrkezy5AvZzY{q zI-N;H`nrbI%@>SF^;oQt*jW{V4h6Xvi>))TD_^l%f`=#&@=tr46mJny57lC&TFYM; zwhsPi_2J$VO8>MjaT|Q3!@7%)m z(=TVV*GZR+T>VICYbQe!`{Sg(FVgF%a0)&3dh4I|Y;=@=GmHwEsky8MGEh!@iPz zD5k{D_pW$-gVtNE8}B8Isaz+~J?>;02@h{YNp>shjy_VsD8E8^@Y?ET1HCOW@six} zJIqb}1!L>?g{E2Hr}KLy6OTz~D+p=^F-h1`zX<)4KEO3u(?p;sxhN{S8H*x|Rsi>h_)y^(N zP12l{Kj3and{faZ8((YWpID4(H_~t&)2)!EEu&X1JVL>8)YG8DOVjQ>$?0kP zFiQlkO;fyl>$3OP=Wq!XgfneQ?c*`rv{&(34cR*x#L*PfZ!anL_+njjPh5DbnmLF_ zmvfh=?Zg_0d{ZEI2alc3ubaz;VnBdY>YY~dqh1Ee&MX!d_Y2>1%(C+pk++mdy)wj8 zBPD8`?J_-FLo_GoOjRFDOY)iOg;#99!9|o&R%GRVX^SXVwC8;I&WiXosa8fVf|Q&E zj=H!!i=arQADvqy{wU`^L}tCJE;Fh`<;z$%;F)}6LYA3b*3w6UOyCZ`$Bvh!iZBgW zM)UK*Dmzu9AGAs<);3jLV!tv=)kKh;<@#O#kFTfME_kSl%<9AfUDGZ0)Cl6$j#PwULNT8hvapOMRDIWz}H%Hx4#)j9G8fzK?ZLL9lhxu+!|Sik83(=40mXFfeVYu zbeRls?Xxy}N6jkq)+#HrVq>m1CU2Sc969~#v=?-AO$Li3B$^S$9dNVWBSxksPxi=5 zibLfud(vF_n5e8uGsi#57Wp1`u$7$HOToeEGegvKhJjnh$j%@y2k0pA~cWAGi zQ9n)l_GKpaW07ZVi~SeZcs^=Sq{RmaUfQ~LqAuUMF`PUP!OP7nZ{%bNEb0M~dLZ}q zcMDYr7z|4nY{0SrVgtm87DIsKFCk`dHg+CMpf;EXh+>-YaGL-LSvwv_GYg>1)6ty= z2#)ZeHIj3iI0D9UC+-_|Tzr6c)WlrWMA%r!SV&03oCj!j23qL2tZx{*8`)TM@pAKX z^YNjffVZ66(TNc)MD;sAMl>(4}AHje!uc5WbNYia;kwoR?g9NEz^0Pi*%`|k)q zfq)&_W(-h<4{-wVu%v-s>?E+=4X(A1?~hZA%mq1!0O<)}U@*4CvJCAg*ngaU+d(D?^7@c&Qoo&QND0^<7rLpTt9Fun;yiT`lxKoa*)chGVh z7{+%7pkw;?HK!pYV*uI9#0Vb%E!5~Buirsvb=JQk{4F&mAZG}IZU6}fwBbsOZ=b7w z|NmbMOJO1aWiQ~-2NN9h4?PYcSr11$YldH14;a|4y8y0yUM}c|?*jY2O`LxbAOTYw z5Qcy(?EGCW8Iu>f%)nkcrjlrDJa)i36@wAOK@ow31f*BQ%n7Yy0urhDm6|!EpT&%U z%>sygs2Hh0&2yTE3&>Nk1B+H>KpTV+dIoa>`r+*?AbA~-7G!K{zd;~5R-h2e0#p>( z2>tEDSO;|aEtCL+M9^k2e}n%kVHO%N{xD;d7F^Q-g2t%Jfirftbuo9u_MC+6cQE@6*Dq60W!^AOn;fkbZ>Kt>-~K$!so;nX8@(e%m`%7XQ;p{wclxXg2@R#DCZ3Kbp<|cN72L z&_Mqi>hK>OLH`>X=>M<7zdhCeN5|5CH1Yo>oeY*Te{q*TzIgew*mci?q=qzUcJ^fP z3E>TiuH2OTxvSwu_ljLo71sNu5WJo z8+R@w@-N`aZdrU*`Ectz%Qp6GgH(3M{f%=;On2XFesw>RzDfeGcXLsbwd#}86bwIi zKK^O0n^h<&zCU4~)w!j6nlIHqXf|2Jw=+kzHNmZf7ry4)U8o3pS;r|RMf2rIdP(fl zzEys*{*BwmFMrPTzdKf?-d-d%*uFm0%5*)7rQq7E1eq{BVSPo4KZ>xG{LD|E=mPrgh|e(=K7-j3 zg^JrLYw@gd#YwK`M!Vo03A8_&`F!{ujQD?m_seQZORN2)@mZ4ey6bq-%6wjkP2T*> zq<&r`O+nYSu*%u`q9}eIai${6S+F- zpNXCdw;fCGMj{>jA8GKhUBA+cBN_ZwPFIKMp6>^`gh50=Ls0B4zWcS=^0B+-dr=gb zjyYm4mE&xUKgV-P?>XspMj=|b7$xwxBYkK>d@O(XCJ-dQ5Hz3mbWCBsnQ}xd%~$31 zm-;<=8kOLDxr?&FtqjIx=Fj(7pD;b`M^cf^ZRb=QW2Nk^TqVc00; zYRBXWODLh}eBH~aELuvU(RMoVs!wV%_ILT4h8NP*IlQ;WVlM|AsZX$x`PnAtJj5+V z1R*+ z$50p5klSHzf6Xm^+!nUT?8=lh|M6|T`%|R;1p^9~TX!F%#kA54REtVp(){){Z`UKYoAgUbaQ8j3L&!w7#%07MtswH7#Q{>Tc5a6xs_ZZ9|+k z=6+n0w|p4AuIn@OIP|8fONQ7pE^K`d_k^eroQE=Mt+)X;ZgM$_;V2U3S{Ya5+`&5Td+Sr-vfj+nzk5U)EUAKER+B%?El0_#!|4KfWXDzHq z>t`gbAvjNCSs98wv!c~muIG+8>vs*Wq7P5v&8@F@Sjs3U+R|;`ni5_Lzjf6z-6nnH zqgUIB9j`7up4g{H2u=_nuj=h8dFsguo!ROg&zd?FarMXJXErEB>^HYxKZ2#$7tK{R zQJ+XlI#>Qk=+*nCa2ghiP2)?gI6WrVjhk{m`|sV7PNeP9lQH=;<}R$ctt=c1YnYT- zNBl%;$G<-9F)mvb`z`tYbBf!#c9J@}3)XK}lj3BOx)wPTj%D*Xvf|b92Gpwi`*XPM zHL2FB@F^~womyZsWf9V>+sqJtr(Z~Vfh0EJrSLeA3k?nk{GJd`L0Tnc8=!q#V}fn! zl=Ca^5v`f7&?%mwd6dBAczIE~`G?Vrx0du;dX}Y3Uh3cyWWFeUs&3`tar`UEw#`bj za_5)vShgmFw!&<0?d12QWkZ38>w`6U{ag;F3(117ImLX7O1g3%D!WJ2TD&>8?s_rx zW2(Q#&(!|q1w(3utz6E@P&vo4Vfq;cFK6JJ}ZK+#lv= zC48UX#OsBr!^Y#@oS-;9oFhqldWBzVmCJ(g)N_9B_#g52Ssyg#N;A%RYteIiho#YE z)Un&}MTW%FPrm*(bQ{`_+b+tueIbfaCpXH#n|59fd^xD5X@13iR@c2Fj6TMil{?zv z`zZefhe0dfa`oXS12olGt8sO49bG}G8Eif6#2K@`%U;iRUis&=bUerPMRYJ3S?f_t z6le7OxMP6Fpyg}jZi3=#TFh!pDmrt_S(x{8+fTz<*WBo}#-zKraJiquFV9rvBR`a` ztjTf7BQ>^T7T>IfaUFwGBq-7-b0J5lUCNgi-7b8%UZ-c0>ZbGFxMwn-gCZVZa7oa! zh1?-@JBT>ngE%F~_d%+%MB%iM-g|@=RnLW-vFa0P?b?R^&Y}JJDM8nI;^!{4O4*BM zygQBC&V1F?fA3z`2xF)yp>NwdoOnu5_J`__ZwVXk{U37K2DM3GZU-2=mLG@b( z$#sqpQq4DCaGg3`GqwiY+ zoW{;!tqm@)pXhRJroi9MjJ~FZYj4n~N^W=WDqS>}jv(`Ms7v^8e}X>$4zGr^ z{D9I*6AxCK=rAiqy5~(*9pco2$}_KaaC=sg-qHk4F2nH#vlMx2LoRU3?U-cI+K?+o zMRQBuLM|jJKHd%HKdKU`{iwR57sV z*25)7Vjp#0qvG9kuJN1ck67Cpr{z9QRb(Sc z4A&o4z1q|lhvl(<%7hOiDxbfYSuX!B{~qbANz!+Z!h964@t6wgDp128w4SW#OtHEz zxbPBdkf`|W_L-#{4Z#;GWwIBgFJIA<_)vgRt?Wv^vHpZ#mC|Q5QlGjPY(ckHET=+Jrcr&HU zIl8WS6;oB_+GjuF@wOhvyqogRH1=Y#&L0-5Q>?hKaemY>Td(tBM*Oz z@bx>MrH}JW&G@Bwp4#H3YdEGS8U^*$?(P!bR>yA%bO}C>y+ax#Y+x|Q>5N-Dqc->O z+|T&zqaqc|qHuZ6)a)leublCD_vUCOXAn|n1Zm)s-!{|Cu#iCb)|b4ifGtWrY|lBBaQxU-1Rs6VRP0<=&7PXQkHQ7lm7U&1lART!4=Dm! zOJBq0%XeG+)IDvh-!{LBvYO-0G~K;zVo8mEqjsyvsp*MF=Z*xIB;%DVBl08_yz5zn zX)kdrBW_HQ{Ge_$#$WBatY9mDYNheh{mqHhAMHzxPliKvDsG8eT_3iIrx|n6xxoH# z(~Rsykbqj~OXh2&D{&hEM8@uUgYGy_SGx(rt=a-g|w*raO_7Ktx}bYhMX@W zld2a`h-%VFVC7MHHO3rCH`Sl|aF&9$WviN@$kniee<_GX{~kAj_Mw)Ah%^1J)l+d9kL3=lzW;A!xTNOj~N&TJ1P{I_S0O1bLk~EH+>g>+LbsUK{Ofv z!ClbL-K{jVx3NJb0!F*1*k-tLsSPF^P%U=l8i(U(=WgX!y4_gWOtuiS#Zr~H21(YZ zSrQ(xo&58=?8ZzIPQjJzA?EEgcMQ~|bi9Oc@p4WHa`^6qy`bUV zi`KH`BO~`qLVsF#uACE<;`}+0FZ1mv*z84;-@(@TVf7m>JQ+)mzTlvzdiJCBB4g8JJ zqoGs7&!6NK4b5{0%(pw|pFeHadijD?VM|PM3<9e)jNnXL&1VYSA&&|<5en%V+s+fU zxgV2*qqV1y$7o?U@nzb}=btuQ{h<}b#f(t&yYMu{)a>f&+$LAaR^G#pO!418>DK&qm`=II*9* z3@iq39yg~JmUex=7B<3gbFBA~XEdXKaQS>s|G`hfq_p<0ae2KRB$DOROO=}b+Ehd2=uRV{dN0VK%`Lglh8;RAZamkm@f?BDMBr*-D$u(FSC3F_(&TgkSoXd;0 z`0%ZwAR-I&tY996_vcmY{Bx? z^33zxIc98jg3m#Ow_XXyGMYWZcPvlc>P*aux^FL6bwg4&f_M^dv!NlzsWSY*%u(X! zya-x?@~Wyc@~f`3(GBu;`lr7&6|?0iqLx1_l&+!(gv$#>>G;(~7_14{CW^)*8cl|p zP8z7+jb%Qr9YI_ms^cx6q$M(&@4g`atmw)>({i$oKbIU6WjrhO@JF-AP3!Y~lbcpZ zlDf@hyt{kXs7>1*M^e8T>RA-#3d@(`VUqB^;5gxWFE}?~v!I`(=4LzX0I}|Tb?m%a zO?%K>X+sL);#_>@oLDC7yiX2`ammI0cxEYL(~!vG$&{V~@l+}zpM}U!N#xez;uHUT zmZQ9uUFuY)BXufr=fgAlxsIKcY0NPw=i@WeI9b%8?2g-pSc?&X*GU>^P(=E66HLAg z8>pG+vn8(JNG?u&O`Kd<*m+Yo48O;pt7d1H@9l!J6xFc|1#sng_5f&eomnZrb zB`9B)-*)<%WMvrmuuu7dh7z4ihE6xz>!8Y0XO+g|a!1W#lpT%=tml~XUP>91_>kvw zySr&ZDuG>kHxei3!qh{9M*(ibQFQ{{wT?&C;y-zAyvo$<*SB4tT96lvmQuohgmBMW4mMe3Kj^`OOI{aVU z-Fa9|?f?J%b|abRnIuBVJWrKEnPnzv(mZI+7^O%^6f#Dmib6_M6qPXvnKFmWWFCLd zvq8BopU>}mfA2r;M^T22}hLc z{5ZXN(Y&E@gVnm8sz2wE^~A-=OF~lu9?H%RUFd&wo%X@_pmp;~-CJkB)ok(7;$T7B znf(tAcr$fxe~k@BPbGI(&F_Bt&PZFE><7nF2lq5y(`5J1!DHe&bZ~EF8{@xZ;Qp31 z>L||L{33R$a*n~v+c`Jd>jwt(O?wjJ>N;mi<7cf`P0_gNu61qV(#t{4R$En6_LUsH zbTz+Lagm?Ws-AKR5WWzRAtaLtgj{Yf$U;;J35VjElZrN*kjZx$f$P zXa4btFH$!R)QxYOnD}%FJE9Lzz$?wN^gH_G#cbr`_dg;9l$@wljm-LyM zXSQdq`^_CQ+&aoP%-*rQtISXRe!-JYzbe@87xGC~`Ga>_k@ujLqpN?EurFhHJ7_3|=j|=`wM5o6RzF&pvPZ z{bRwimJNc;Y7`&$`EWe4^|AM>B8-ENEYWZKqPUmsrH3Uw)f5cd1g(1&v@vhj2osH7 zzXs_Y4)51%pLK-1W-ALp9QHKQicM;~D&#jv1Q1krpfC60&on*h^ zUw-aWIalk~^8=#`2R4bYTK_5i>dUl?KOf&}HEz|cx;oBDnCU-_X0HQ_mW4t!J4j z7VdQ%%75kmI}SbnkDrRpB(Fa@l{n|qKiawfXn*~)t-1Zr_Qb3|+bZw>XnTch`J=6L zJpGS$XPZ}lw7*CPS8r=3k4UZFt{-vbY4vv3Cq5ghwY}aeJx;3D-rY0C=wY?CY{r>Z z>#DU+YaHK`Sk=~KkFM68s`k^>vK?~#tJ)@Ur=NSo|89FNY?j=q@cQp|V4Qs~4`09E z?ZeX*hc~s1`P~kCdH!On%!11HZlA`Q#*=3HRJL;?S_B@AaEPdE52~m1UUmJsi?0^qODn6>uA41h4o<-?TL5BTgYBG^X^PVJ3K^rxvu77hwl~baXXS6)uQ81 z?6 zK2g@LziW)rt5CNVJR3hRuqXlaW)+j!mYixpS!g+ZH(^vJ;X+ifLbx zyRhBPVa~Hf7ke#TPkT=XsTGgbls;;?BE#?ybMsgC+EZlTV`rF2_rCY2T8ZzkRO_nl z9qEW2L)!;%2JrLB?>p&U3m#EdZf%zXj7YmoEzvP#?4m1^M!qS%DW%!K$gQaMka`zX zO4Z!1uae5O{${1PD#AeO)$1c0>&8kg)LkfB=h-?-uaemC0hh0OUB9+PH!$2u>&=4a z^G)}zoH)vNncB_BQIl-PM`{kK9i?)^!stj~;8t^m2YD06+bBT1@Wt>A=8a($51No}CNSaoZL7`Te#U)>$1L zYZd=IeAQsorGD?Eb(KS#byA(ME6{wCy0%H7;2U8rdK2Le(AOI$o$sPCTD?7#t{F&tYxiL zo0cwotvhi1$k%J84Z4}P;QGtWaY^SMzFFhC`91*Ltxz-@7%==;q&lgokY}tG@OjUlt9#p>Z@$c1noCvaC$KZ=Y`ZtzY=0 zP`+f*nYk{#Z~wUY^zhcWH@YRVEAyw54X%?Tdu2-#*GHo->wDm~w64I`_A(w|BSCNgTT0*wkul?t-g!8ylS| z8PN7Zi$TX_j0RoQ*YlY1twWBv=I+t4vWp&_xY%t>vmI{Rhj%us^V}q~%>wN&Z46Is z@}AHu?NY>GDf2oT-OXJ-c$z(2`a);@StX;V$E-6fx1MV|KCjW5txKP}J+Rh&otd#T zvVGR|m&14NX}s-p)7V3=+tz{z?b#%E&;b&hSm zy>7p*y?@tEPG5#Jc5U)3ZDX&Ng*Cf%gdq-@& z-D@XwU*oWF>zb^o@3%Y}Wfoj-`!~B+OG%ZL3fj#zZ*Zu@@gjP|pA*SNe|A$9qlXPV!F6Z=cvTI_GI zZ?Bj9%BoFZxqmE|Ae>8m!6n6eErgM6ZG6)hi=_#w`a$j z0rh&%O@G*WYOKVocFl9A)fEadKE%&4?b=j&_k?plK7Pv0>F#{PdFjn@l2Z+K?fe>A zbo<8>i_UE(g-QBe8=0LVX)@)<{pAPEBWh}AuDSlHlXlCP#j}*#87YjknmPSR=nSib zlC>VLz5B1;oYlQs*vb9gt<4gzO_uVpS#o%hN6mMk;T_Tp11wJTGQBu)+;iQ{_eS;$ zNln)L{Og+}LTP@pv@O@O2FDanJo0Oy_QmMohA+}D9``jOJ$8JQY_-GmGjFPIA(&mNjjjr_z9v5$LX)^Ofoyed@k8)Z*8xh$3mSueZ zExCT~&5Jz49!)v?twGin-SpHho90frex{A}{e{;`e_pfe^1jWnKB_w>KM1Hje}+-y zqpi&!l}HkGpR`O^zf9v(p5ckPhGyDrnuWE$?cDC``w^~=ZmCQ4oc7BGuZ}io?WwOP zZ*6twR^6MrUt@zTO`GkwS z^-5mxuAk+*sJ4v@E#2d6*ZnfymMvB``tUP9VD{_G=H~CM3^R@9&p5bLdcfAJ zp4~IH7%#l>ZMb{)*D}7p^k;l~mHzd5-q(6lzwPs}$S~AM-S=uhp7$4nuBpKXcW?eW z=Wy3xZ{v51TxM-5y1Q!VKo?`z)JI=lg*I65WmB`~0i*JdHY_nr*(aHHq5r~H4O7l5 zACBEH=#b*csrO9c_BDIFOf9VGk(%?y^|aZtX!@odca7Ziwm+4`D+~hSWOs$o#vaIK? z(0zW|H4NWe*7+9LSxt9(hE{Nlfxb(eLeoKg-?=vJ);;KJ!P7Y#Hm}~cN;|ox#$v;5 zi+vj0O#Ru{IM}LVz`$F5GPgwp2U;!lKKgZp#%qO!nFpowj=h(e`^e(UrX?SA#vDCh zy!?YlX6mbZt=in~)pTc6tJJVw&77^e-Y_xSn6Yi;hCQ9n{=A(hmWr5`D}&G-4&n7wPuUZ>FP zkGexur|+B+@4wY_c%+}wkkB!{9v>e}iR-s)NZ$IG&gV`PMKwL#%6hubu3A~81@C^m zxjLzJ*d84_^AS&GdYnq$8Pf4kVtBE>*{;UMjv-0rBagpocc;!S!=ZjNJ!^kg{x$vi zv#6>0@=3L0uAlQOa{bkG6m)x4NqGQ}X{c5?UU{swk08`NH|q0+VU>w@C1 z5q8nJmQB~!-!ZMFr)&87<9<3;OC7RwuXk)=yQhzV)4IeRXiHYc2@| z*E>8|;6Kg9)x=ybXQkeFAB$<-{65;|H$0Y}Js|3qvAHDfbLYc3D+{gcKg;J&PED5- z_UnCr`=L(Ky0gPqIQMC0JW~3^3!ixj+D)Ct?G2O|ekqiGqZ;8cr(oMfrStE#4u4uJ z^P=a)klwl4(p`Qnm>lN1K-OZZ)-a_st1!=9z6Cxd*(Or;^2b@KpQKJuyaiVru&hc#z|YRet!OGTIq;!4Ns+f^p?A0(0)#Zw^iW~MQD{2O9+1@61=CX$RbH^o(iLHIQ2h>*^$qGE(;&7TwXM zx$7pIt#2n5wQ&E`d3?j}`Q6V9dH5@2vcJQ=&>N06X)6|et93hL$dkBQQziAsW!sw0 z`dYW(b*=W-$M2I&x%X+*5RW+}4_%`g`z=sh-si>FIP;K059b)<--xa=E=((GVDn{* zcem~nty{lf;_jLa8;riwcHE;4N~_hLdH0eX8LRkGf8#vEBbGKt9)%BI*=*Xl=r*5^ zzJI7K|ESHyb@O+`&TH7QU#Z8%67T(&y;{uEwppxJc(O+`?MEuJ6vEnlZsINez9sHmB6+R3rD=r!Q~4SiT06%3zM2=@x!t<) z@q23cdu}-OK(*`9)OV3@_slrw|DrJI!>vRY3#$Y5_nSA%xK}HGec)x5@w4?MvrsI|MZquTH5VsG<9vh(OrXQFHhUGUu~T0qdxWXgKai0L5H_JI{abCk6!N9 zM{cd~ZIB$6*+#i{!13WtceO=9{cYfo}b@|*O z@a*h1IR_iopY*y!X_(7L^*(6_A9>tuI;EqHO3b(oEw^hPdA%-n&AKU{GLNjj)KV$y z(%x97ko}2WlAKlIe8QfVo(h;&Z^+HIE_(vI6&9aa(EMAokk#g^f>xO?TpZbb>DoJA zc6@ujbYJqbpIcf59xLiO&12=)$hKF_BZe(gcIi6xPG6gib2|54X{746K6urjtJ7pg zrZ~S_7TN!;Woc^H)&A-}()39sFov{h`qc=kHzWnsgbwUEAor z$>7!KIT| z#NOX1lcas7IPlonW{L?C`?C z71lnz-@$Cr=vudo<{r0}+}2Alxv4%hwB^MOpImKDOAO}R{#M`Pyk?k|TWS~6R`(Ab zUA;r`+_GHRl9(><2bKvzYA#K-JUDGMO-=0ma*QHlsux9)y&t{8bPFzdB5;Jp| zui@zP=VRBs)N>4YrPsJeyJ(kLzcQx1vz!yq-Fi-o@0~6;==saxg@(6tvSH?p^@cON zW0(FqZSbRKZpRP3E|t1ygioEjEIE4duvCYG!%y{SIOxLjzDC8DTV3qDGB0Z3+%Y;{ zhnCOIGtKw%oS!wRMZdXIcegX?m33V1exYWc$X!`U*11t%f0n9^y0LcR$@PAHOjmxL zboA$&k6R`rUcP?xiqzbV+Y5T{niiUoZ?7=nY0c$*Q?hcUM!oRr?yHzgEcEYeN{bhK=#|mfg3NYF1#9Vn)Kyu=&jS9E;R<^Bwp9Qaa`GB z|MJQGn~#?BZtakB(J!ehv(mQ>F%>!bN&@6?JFb>7Z=8{5yKFcT~~hR$`*5_&kh{ABBjEA9nlFI zJ;Vf!D-LRsl1gh+K1R&w`gA(E|F;r6Ul2X}MEsdZ+sQ4Ylu!;fn_IM|jQn_^IQ^iugT zOk%t%eiH-V>Djx8QFbi2Th)alnX60r0ZLUDRt$Py?PgQ$ZR{P~?JLKmadB`gAL_Jf zy>w!|i0!6wJ$0e2R;}HewoVH-Cm5}+Mo8n$5tyJOtS~q9aAnpF4lqDvbp~c#$~fI^H3V1G(u~l)(E{R zGygs9|J}H4sXSIO|H|bN9ob?WxBuy&NgSuf5Tc&E_$$VpvgZi7YSWu2SALA!v67V=$8cu*|>b_qRpW{WZkd>i!!XU992_0Z&7`Dc824>1&z}v%c2hb1Y<^co<+zUXdF^ziU47hN z-v;Gl$copsI9|H4J&NDO=S{_+UuB;o<)12L1Lr#P`O4=3$C3UX`HNw*SZrSI3{lE4 z`HUS_ahNJwsT~KQmu+_CgL{>KtT?Nxsi{`nMof>->xwHXKQHXfISN&bb5?nLx+|YR zo=U3Ss@+W-_R1h-rsa37yr;35S@{TQmG`W?!r9eCY(jrbwemVU#*MWqAF8hUOwFv^ ztenLKCZ0+Q&7B;|Kj-x9UCKVh#K)hOSa!}$z@f5LpJu-Y9Oij6XYgTh?R;}%M)ESZ zb+BMOJllV-smf2lvRxME31x>!k94&!TU^FLE?e^IhwcA!P5-ejoE@C_h!$JQzn+ml zZzq=TUmvjyHCFzVR1D5o{=r)|qFec@F2A6B@ZF(9hd4PnxC|@bqhLdx=i=2s$`$VL zz(VkGR(4!{WwJR+xiHqCI0wW|8e;$F_mRyNM-VfHv-v;A0seg()h?<^#`Ae|mUp@5S7_>K23kyhX)6 zRn_%_RmEY|s!RU7nX!}I@0*kzg>J>%ovJRf6GQ&7r^{1Od1yDkFodw!@YGrU>Oa44 zQuX+&Zd`Tc&|#I!P<56BUWT0U!JKjZDrZxDNgXSm*{Y5BFE{)9@Wxd){O1a&xP95$ zuDX?a`D*(81d2C^vL&m!iDBj9|G6FhZQ&}W_K$mrF}bTIXJubGy=re})z$;cUA`?< zyPEMNThy1D#mZmCMyo_t9*EC-1|u4>CV9`RIBh`r?rg<{e72iA*;(0h6unu+&i@}5 zl^;_7d+A&(t(?r1R5et2|7SQFL&b8I?UD0H z%J=olHm{12vHw_%vbQC%ZL!-@Mx9^E-{8chRhM_;a^CX(xJ-k~%H9LZF0Hz4cPy>|M8mtG-0d<#QLKg^FvdZd)~{|I7O2 zF4Z3Ye|j*^|8*+=^~$OxDPP5ZdldhD^8cJd`T1kY_Z$9vTK;}b)mq^-z&aB9GymGo z%Ffy0%3$YWyB1$}E8Ys6#eJ&gQZ+3xugcs0b(&Ro`scPJo~N>vKHtj8`R`M&yoJ~S zsG3mK-2Xbqs-2d~XQc8$@H*Sl-CuiyPX7gY-%cb&+)65$ojGmb+XioEhOZbNbaig) z*e&}Mu1jWnH%wpIQzeU`_goCqoqi_X@rq(h6qmQ#PM);d_Q-cs`s?f+C+nRLtF7Xo zUeax!kBk28n1neuSAB7tJZab#yX>CPxvDY=Im>2dl-9bJJ!(VL)k&sj!)W}`?nqIK3X;DctW2;wObF=-*PYvmufUm#O1E1SY4F9zEOZq#H1!8yl_xof|-q5s6P5FQ>JKGLlvBa*g z*dZ<(#g=yvJFBt}Z^q!Yw=y?lz1x?+>98v}pRMH2OPtMJEFE|vt9?bYH+OM&aI&d> zQMHK}o0N?aEM{17sks?Dy<(3+e1tHA%9gl^u^Xpu5j)_O6E&k(#l*?MhHs*dR*vQs zi*IZzzHoT5PtI9;oXhtb#g4DLgDZPVCVUa0JBFz#nOSi@7k2SI`7%l6=~|^;OyMZ%-qe28<#yI9<-CWy;x8t zW+ysP#l>{u@oj*!5{W5@Yb*D1xt0y4Y{m}u7FAD{ct>$^a&=_KgXOLqOuE`$L$y6? z9=+&AwKHc(YBBP*Bj^1Qn~($hp-$yzwc^_c`!E&`PJAQf)JWn)S5@vb<74J}MO#>-qXrMo|FbS4$gAX#Q#+);}TDkN^5(fN1bj+0Q+Y_8<(#5DbNQ4hX4X{5Kq$(1M-exoheZpPPDUMNVJP zYR%D&B@?Y4U-&xS^GBD^Sy#0VRG+Rkx350`5w!o?i3HtL${%}2Cej(NGt#7VxKHKs z>+<_Zj6wl@C8N2`PWBy#la=jVw0qP3_gMMXuZ5&T>gceFYmUzC@sX-2XkXP-gNo4 z>cB4eWBP2wV_463Te01jeFBxux%@vd|FO(pd?^;+#V4CP)3Y&XkgL7Bld)sxE}R#Q zFZcFJ^SI-@@;ywcrVP)l$Nd7ib1!j!V^7{3%BHNeh(EIoQlhU-U9od#MMd#f1a_sF zGTl|>kB|?6s+}?ub1q~r$i-Qle2i&Xa!x_9{>0xz#J2Uv1WY)kBTK`ZxO>?&_$Z=_ zN9;{ z9`llxRHPIqG?8?b)RgL7r?+8K#iml4QkqIFq+3W$m5h=|OY@IMR-5@MNVFufEU|*5 zwO9!(k%B}@x`m_#YvWHfq!cBM<>jS@N$H6BGUva%Y4YqkSNvIkx6VFNn|Uo8u&&M6 z&c$o_-*@3;kX3gWr!~XKKyQ?hj@~G}DdK*W_cyP&e>XKwq{kWbdi2ov_oT$ORc-G5 zs@=a9uid}TpZy*S@!Bc>yGP~xYFFI8hk1`4y8Nfx^))Km=DOzHbi0+k(*8gEj|KmK`NQk> zzx>Gj<40Ef{4q!0=H;IaySY?6f0baRXm)6JK!)-*6k>QG_252U-1u=ror=8ZwZD9K?1ewk@$e)r2Byp*HHEF6yB^ z8lWK>p)s1EDVjkHg4Y5q(F(2625r#}N@x#dbbtyvq7zin8C}p7YEVZvXrMcKfW1Je zUSx0dL0|Mke+7UM7;6EG2zFc}7zf~lB> z>6n3;n1$Jx14GP(5$3@dCJ_CO;`2hh=g)@)EMWy}*uWNc5PgykaKr*Q!5J=ag&W-A zfnTf{PjVp^VKJ6qDVD>4_s|ujH+--XzL>)0e&i~whIo(mC#REZ$N;QE5!PcSmj{xw z$qi%>Hew#_O{6IqLRyiVNlS7IxfR>tKzj%2M2h`nS8_KQhCSGea6}*yUR)nVMk5B^ zwByKl?8hqF2gn2@!k_j*at(QiJd7k9K{A54JcT@p<4D6PWaD&2=8$KQTajnU^Eg+L z7s!jq!zJY7G74}7S8)x6xQ-jRiCeghJGhH`xQ_>Th({>KV?4oAl;9bj;{{&g6<*^l zO7X5D-;*Ek5yAL`41C5He8o3>#}E9(FG#UYB#;JsXHudrMGo>%Kn>J{BE-Ii*ng;l zx~PZxXn=-jgvMwBab9at^U@qG&=RfC8g0-P?VyC6EQd1L0V?Q-PEbW>h+3SkP=h+U zK?B{<17hE!7etNDAegK3K0pq^P=qplQLi(f%e6>tjDQ!H>yWFVivWzoD2#?4^f3lw zF%IJ~0gilESO6zD!wv3OhEn(<4yzE4)z}Yz96$ihV;wGFJuYGc5)p)h*oZ^egu@6% z5<+kUn~{tyNWoSdMJQ6S4acw@$FT!x*ok!PLI!r@1j29LzaCHi~cucX1E*@c<9;2*r4eCwPhyJi~Lmz)QTsYrKbe z{Gad{U+@lJ@eMz)2tTnHzpwUkei1WGkf;gvpZ;11{_dyegzKo_2{V2^K z`clOC*hPOz3y6Lcan5Vemm=vNWv#TI=k?I8M9l%R?B5dAC4 z5d9_{Ao@#GAo@VWcW%-D(FvjtL=~bRq%%Z6Mi+=ajII#<7-|rG7vk@Lq7Om*-A{by z6@Tj!-+Q}5eD|FPW0=4cW-!NmSilliu!aq6VF!CSz!3}J1ZTLw6>e~c2RyM5i?A3= zuoTPSh2`+a8U$c1)?qyYu>nEYh)oDa2sUF2wjvbUupK+F6T7e*Vc3Jc2uB1W5rt^P zU>{-;hj{GA0VE(12XP38k%S{iMhcE16~}NKX-G!~P9P5_k%=sv!f9k92e~+dvp9$I zxPXhegnV2^0j}UGuAvauaRWDT3%5~(JGhH`xQ_>Th({>KV?4oAl;9bj;{{&g6<*^F z-r^lf@g5)W5uflGU+@**@Et$!6Tcw7G)W*08OTBo@=!nx)Py2xfxZW+I;e|!sE-C{ z2vOVD7){U=&Cnbz&=RfC8g0-P?VyDAP(}x+pd&g#6`j!qU7-ebbb|)EqbH2f3nu6d zQ}lru`obLjFdzM4fdR0@Kv-cAtT7li7y?@ig&l^$9>d`PO*lde3!n`rjDRzAAo`nh z;fj%P!zj38G(4aOPv~PI#$XY~Vll>H3C3e7CSVyvO{W(oVL2va1q|SgDe%Emti&|< zVt0L>^KQK6NC~-zlqUC*GGsU@OGc1#WF#q1Mv)3+G+Bd;A#0NRNJTQ1tVPC=waIw0 z4!NJKOCBKWkqKmdGLdXR9wZx*hsZ|cVX`rqL^dIhkWI;CvKg5|HYbmgEyz@|C3%c& zMII+xlWAldGM#KoW{~a36QmM(l59_AlFDQj*?~Mos*tD2j${t0N?stlk-PbF>=?`X zBNvc+NGEbH=}d-`E@TAhN=A}yWEAO6Mw1?74CzVkBNvjfq1CihM#wlTXPQvV`16J|knv=VToD zf{Z6$lKaV5Gd_j3F3` zVHgfgXh9nzpaWfu#3+o09`rE=V=)fnF#!`X36o)fDVU0Bn2s5kiCLJ9IWWXr7-1fa zVFFW_!5s5p0ZUlH8aA+n9qi!%M=XF7oZ$jjxWOGB@Weta!eT7JQY?cPmSY9H;e(a% zg&$U7HTw8jaBg zP0$%i=z{j>3T3FF1Jt2{Zs-UNbV7HiVgPzzAbMgDdSNhnV+i_SDEeX;`e8WwV>C3O z2QBDB8)Gm6W1)j_(8Wwl!fY785L01<>9E8+Siu<9Fo6wBVGA?Z!5sFO4+mIa6}+$- z%i)g|SOaeazz1uw66@fL_3*TAQVB^hK<;cP1u28>_iB5VKbt!8!-sOKI}m( z_9719h(`qWBN7J?g%l*KjI(gTIk@6HvTz-ja0B_c2{+t= zJD%bmN^l>~un^C&2rp2K&+xz(c;YJ-;~SRXJ4(^1J81mx3BuO!gwjV;&q~ zj0G@(6HMU@Gq}JUu9y!uSil{Y@PHLOVU2~b!6MjVG3>Ae_E-uBEQ2?Ea72amf@I_) z1($IY1xUpe9K%%{$2Fv(5b3y%4BWs8+{8)TLMCn_3q?4EJ2;KI$i_Y7;68Hk0B7(J zXYmN

    O$QN7^16MtJ6QjE8AI|V{1_$n9=;TnZ=xADD+b)6<~Zh&k8sNba23^Din6aUij zRZst-1>#fqNZGeoX^*`V?piTND1OaUFvCPj2y<@=h?v!?p7De7#Vp|2Axbf*^I~QI z;_ip#r+_kxzxXBMQC ziXynBy<9!Grn`#BGhBlR>@m;%M-w-_VV%d3V+^?0U=5a&+tju#QwIOb!b4iwQWs|r zi)IkKI9UE&Y$C7z6)}p?Y zH@02=kl0}H4kD+M{K$zy|72k@s7JwJKb?5@8V{riK=kg}`NNXNWr`%)!T4!!FQ%GG zY|%|BH}Lm(w{dEw^YOEZaXhwg7{SavT0A_8W?r4xEK=@EVi2DjbNsw#*C&pE9Bk9C zAV{Iv^NR_!c$N#2RJc0=4nV>=!H3mD1=V|q*HFD;1!>4%PBe1(LIOW+7id%^02y?? zp?TB!)G_2!gI`S)J*>A@3d+Tu(;XyP(jpyBO!(tn)>f{=f~D+zu%pg(`&Eo&dV9K zKTPbyVQ{DpJOExg3i=3cYF?CcV9>J)XQu_@wXigoIxfg|vrq@{GMWD6LaN z+YfE79H^mfMleB{ItKa+p6tWD_#%4kn9b`2V7lu@aL(`uH;{YrgO6vi$C$)-Dr zpVe#>9=rK}n~tV~>AB-JuN%1`R1GN~o;poD3!d6XH(Sk!?#ao+eG0KVqnS)8WD?^k zZZuLK9y^?;Mp~#s9{*VN8|~;K)ZhYgi502m9>2M72*F6Fuo{~x&44(d0rK(9-4s4) zJsLipGMsopm#gs!o2%`x0m8qc(-M$(>i}~mAa3q(U0 zN&E~T5%>XA=aVFh(3_)>l&__Z7i><-#YXmw%}9Bcmb^T3bHcstc%9bwgfpi>xi^1D7V6@Lw2;3h zXKhX-{=1&7&0E(pbib@^{Gg7AsV5QuNsC}2wt%M7f}&-p@2`?qzKE_ zxUj321(Q*H#Y`ye7U7%~d~&mq7M4=sgY1n@$?JqBvp!LlL?%c+AbLzmq>jRZs*(>Hf5=c3Ie z5CXz7f}7-*WJU0h}?no;t7fy6mr1e8n4j0MC7Ee8}o^%PTAFD)}RwqqoNJ-l>O zhTE5w7^@;DrfTR}xFH{2k1j7U77@VI6xXJFgTBlaWyVtUaIirFv#1iFXjhgQOKVv} zFP_x_u}o2}DlyhTPY^&pW?V!hx)pw=)YyoFhEPhOG^)AkXUmMGtD)d=E;t4Z6aA}8 zj5S#l4kj3gr@&LRy=%&hB^g)IS2AK$$wI}vw!~OVv0M@gaHaUTDY%`M8H;fUe7_i= z0;+R*a9xS9sLNpJ=V>N&b3@UC>r0G9@k9xIK&682!W8X>5@T^O#bt>n3q=gtE1)k;HTw-j1t&;=b0^we8ZDxE+iLnv84Zxrn z8lMiqExNVDSkxgn3k?)?0C>_3xvk7tC&mB)+_4JM0|TMk%Zzmm1APV^Y&agIfp^E| zLpoL!SBIoj7UrUmv^ixopt&s@u52^nGzi;M z`ex!k$XKOi^A@;MiSHyxcM4E3F|VyCws{w)Wc#|wssd|3ftA3lF%y7*Ux@DBoDg-p zj?G#ky<#?W&t|*?%R=Vwl|g0O#9!FFS(G`O>M-~9FC9jg(Q-wp4h$dkQ2qWAPYNkN z9k@J;43m+&k7C|!he=J)JpCl&;AKaV~wmY|m3+%c>S1;y6L^hI}ABR2Yii} zWi_~uM!#lx+)?x%UDTi-*}S9hRaY7+?YkeM=e)2ewt~&=j{q8&=3-@v$GYJ}(HA%G zk=~GEGA}Rj(t^Ce9%lvH3JR9sf-O^dbaNAlsW>Rl+L|?b*+CW(KXTbgmg*odx*7N13Krq82Ru}1C5zY5sC_jY{tr2S@@(YDP zCoo|9^XD0xVfo=6UI!-ljXksNCMfbkA55-ECtRRo;E6siuR~M#ZKr!0xfL zv+r6jsrO!%`QD=_9Vb9R1QbzGK~%&A6+}P~7ZgRs6?afk6gS*)-$k9@IX7>ORKRQqE2L&Tba-KL7(0%q*-lV$r z5X@jkC2SER!Avk+H0Ggt*)htKU)pkFW~SPht0`GTB+pR==bJ}G=MVykN&E_tg# zDvyZFE!KKqC;%6QfJ45#rLg)o7#tRH-Y0GlkGKDoEqga7;*=KUAa8|!jVxv_2Bs_I zDTo#n@K@E_ma8H+{0cXM}`&x?3Ru( zajzE%+)cdfv%>vI6h#UyRRViJPS*VTmPh7Yv0=S?P899t{omN~hs+4bvwbsJyK^s7 z7H~jb@LOAQRVC6lBe}IbRf%s;&Y799V>${Zdnm-Kcu{_5%j_+)iMV*fz`n!nV~U4D z%K~A2cZ-*n(I=}MJm!c~2W<9ov{qtJ9B@VvmbOGvLA`JC1psJ-dM9GX2w(BonBOZA z_agig_+(AlC<2}HweN4qhh8S(OM07n7@X0^a|0)p$Oy<7*oA(IY&iKWfXS|+|DaH~ z0*;xW59}8?5)pp>uuv;ak4zLD#li|nMt6QxtQD~`DJKdKcswF>_2WXVM20tHF$2>^ ziojKWQm7T284E_fN*PZW4_iN-e3J#m2nsL)ONvV=Mg7^9Et!xj!GHgJ%O+gxBt(}s z3kY#_!es7bj@Iy~bw$Q3z*_SclSd*H^l?J7Anv2It(w1_K0|I}DkzlXgjJivP5#xE z%cri$jmFT2+-xgvuHKlZ`x^;?SI+ymcwq$MNWM6#8Q1}CcvOeofKAJ<3q4>W*NvVp z(x$xBer(-8{XIfwj_HV`4#j37w(vJwq?zD^2|wexD^&h>`JK+v5nILoNB(vTa(o3d zD1Fg@<2&7DZ;j=d$F{%tfYBVypaelu|21t7M8WcbM$I0#XA%DH9Q;uQ_*2yZI1$3n|`W|_7DG_x2~BXG+| z_w4gZwAEZP4JKY0%$UG5&GSpN<%U>>fMtVAWRDqNP@*k*Z2$$yuY@N0=C1OrGHo$c zwb-7yF2FE?(r1@xOLU1DE+ttq4XmUWmT2qYxeM0F!9a&$EADejv?WbWhArX|#_uN3 z&5J6xP0L=+mYdtCcn)7P_dc}dYv*!lBO=zj;Tw-3wL`nBtq*E}YW6K%6 zr9@k7uoNogXeCN?nEKWdZK;D|-(-=*rdyO9-!^%5S18z_*QBA0ilaf~-CQZcw=gAQ zl_kYD(F6?Rw-;(fJ=kPJ7a^Dm0Kg94QK%K}c4U>Y3&kT6?3j8>u^o_nOL@J!dqlk! zphqwJE&_~LCj$`2h+bpy_-SSj()CU)JWw+**e{ys0c*YgZp1tkv(L|QtFN4_14 zAD=|Q6D&=)bH=+XkMvf&r2_(fu;T3l1HZNvQ|+j`LefpneZzC%dn!G&^x(YH^BNFW z4rk-Gm}q?@+-(PpIx@WMS*-U~^4+$EDu>7P#rown5!+e(V}<{x3-H>9y<^!WYI@Uv<1KG zkwtYpADdrHu0B?xEy#T`+7K&@0N3Wo{dk$SKu1YL^4ek*iPz~TO0*3X&!Z^qkwJ~< z*!g6cwzl9|Mxn4M@FLazRGGGzX#z9E!PW}dUR!sRXd4mos6w-h?I_9L_|7@Mdx(gK zYV07#41~sWwNIBwcn)7*V!_W;rgy^8Vd+rGg&7DvBrrca{h@T8aSWOao>Zn3*SKr? zLz!4@!5pH1hto7_v!9#(P~=)vkp7zCih0foKR^AUkYx5^%%BwBCV-sI-P0cmKs^tf ztq9){^8Wr8Dl<^mKwJUyB_6DpiaEf%FIHv&S_O-MAq!4aNJ_iDrQB0FC4+s~$Ui4_ zc7YBe2g>puPr7*PQzeW62);0;|t_(AXr0?8uWz0 z3nQS?##iS&@1W_;;^`WKuVb2u_O%joBoax$Hc&PP|6t%@cki6XN-h#^9{sThLZl79 zKIi_xqM|n+vr}+++yn0$6>V5|FbRy=VvtCH)K5?flg|Ta0O-$!H0FBWtT@F2epxtg zc>vW{S6gyB@vX|v+!}+-)($a!AKP9PRxb(F_uHgS^L7FwHFnsg>wAPH-ghcSUZWsd zVq4N1<&xN|9021-Sz~_`em5TvFs>OoOP-I&McVwn%Ku0bGd;lb@CM_1m6PSBi|)r7 znXxwzj%FEO)9$Y0xDrFY2O1`K0IpbHaSmiz|9)j}vRxQcP)q)h=3;vYud!Q_le(SL z1OT(Maw^%CP}<;%-HCQuV}=$I zJYWc|KPu6d)PV`o&LkBMEYeYaT&At(V}|0h#RGb1c+pQvv<29vg2t9+&LXP*X^FO; z?S>|{4s2dD;mY!}5^aU{AuntM#hUrc^nYHtb{2JFv9C;!#3%H=n_XobJ3v)4@bW~= zO~eepm_FoyLTrQg2F!%5h2^@xtejM0S(I9g!z(SiOcA>S;D2Zx_<{PX%2LjjT!KDH zcO0|S5!tyD%5BSgMGgT3!NvuAFziC!?O#`pNwPDenTYJJgi0@d(pQbk`u{v~cT#p>%dt=_Ahz zX1dO)?if;Vn~)lNMs;G5mHCS_kAXWjTG@*2oLfC2dHRwJV=u9|imHZy4=(bEVtEV1 z87u|+D*u^PqwG4*W50G@)hgSJ5C`7N`Bfr>F^W_ZF2n`ZNA@pvT2k3OtN5(y5hNrx zq_8G*@a*b86n<$}y%<*ufhiQ*l0|5~yfStaM{JbHZd_Q+L;MM1gYZq=8%;T3nYM4j zH6YieB15P7&nfY?U<-qiHObD1eIgri+rJ6$hFYaaPybSn0&2pis$p;Sn~6?ja3 z#R=n>fUOV43DU<{3!huvn=?z#{DgT;pI7~hB--083ueQ?-t(&~GapRfI#{{}!*hak z4_wAWdo?yLuFf}YtOd64@-T7obz^dWFW>}wpu5>gDrWQ4JYE!ngtY~_F$e&^u*5@# z!&X4LOvBp_F)Yh|QT3Xsap-9b&9aD0-X7kZ$BQpXl^k2l%+uQ|*$_dm^RZ>YAL^~F zGM4fMIz+}&e3#cQD_)xH+38qpkswF$9S2(GOJ=(*LyplwjL?jU=2QF3!S*7|hoBA1 z%#G08acqPvFR31^l$Kol=6u1@bL?wgWiMXuW+dZ?_C?;Blon^p#$A~8^}`$t&2I8N zd?kPHTw1;P0r$XV-_}!6O&xt^`TmD15y<@H)fq$FeY1UWJCnF7I8W}I$C*5ehw0hSA?g}2>djvPwH0%YA>?u_Y z<;cc<^vFkK*OxIDcNJ|kUfNk-T!6Knhsi2+Jj$1@@v`bA2WdJ!`^9sIkZ?eQ19mqlN9yR- z<<-j{aQEi$f#-H`NtZ!t8lCobETc~cvKmk32FWi2?*NMHB4<1hqV|G5>R5^9|AeD* zf3i1a@TVd-c+%>KpB;D?Y?6hJz3Ztjuio&WzeVDX3HMehZOYXTI{Vrpv2u+3nzQpH zd}H$xo!I4qB>)~QTJo3mR#MkU?9(-iRMuD@xZ7R149H8wY+KHsHc187` z2k~v|&5s#HphM1P9-Syp6&uwtH(E{rNX8A`)WM%-@n(Wn!n+$Z1ce?fGwoMY@BPnP z+&-`OOHFwEf`j=7b2w^;t3jK;Vf-Ry5PWrR5N;OZg#6oSfmW%a%X@r_P0 zLc>$=>RPm~B|@6rd~@`HNvqz$Ev5(A`P|oBJ5m)b@)2T+6*V%0S5@!-4|l_*h72@= zVyo?FP?d-fluWk_zMBu&7aoz#0E#`^LK}Hc-FbEOw*N8#r7-+R4_)58>o4x|^DPS(5XN3a4cC3YJ7;ee1o z$vidLkR|cWQwi+QY@(QN*oGN;S5?oMTVAN}ndFQ(_e^FH>MDUj4hKtnw7G1Gv{xVM z=U8jL{*XV%TJxIf9r@4V_&BQn9+xk?4RfVrkh?mI+lS2|4sQ#ZSAVC(Yv3M9q&R7{dRQ<-O1_-(0=(5Y9Ve&gmix=7!G#Ev9-;(>v9~{Ioizs1cftSFA#M?tP{ot+Dzsq9O`h07Qbp$+$ z|D=TOsw8_b#VP28g3*O0fM_;L`P-@_O2;}b_GU>5*6VbZZ?0~Wn)}6wsBf<-BMSW! zU}Nv74ykv8@lLqDJyiiV94(0?N0XZ~u^}S6fOvcy)C25$ZYeQY>v$NWes2InM+4k5 z-&s9&(rS?)ejhEM`E+qU%&m28iQtkCNV0VOz^ z8L1zxy27f}XXi!nzU#67I-WVE?YAj7lX8^vjiB4`VnGSiaYJBJ=y3P^;P&dyUH!&S zgd~gvFDwIeiR0Mzu!kK!9B*g;cps_$sSsu2S1q2(kBXmAzD$1lSjm$T{MiVAv}A_b zUi9&j?PHJ`k*f!muvCMP`H49r9`PK37!AuAB_|)s{3okYC{<$cxX}6H+*q=$2}C~R zKbWT$j$cq`0KCKsOf*|G-+ZM^Eu0I00WnjP48 zRfB$R{@^sK24V|W_@(mFN6^k=Fde zq3K9(e(}(BWHj$NG##1EFO_|oTo*PF0v7mp>2VksUp_P=wVGe4&Xp=9LU@%K!9fin z!eQ^L)iV#vNfi{{H%t_SWxW$jtrm7Sdzsc*HX&?*Cqd_2@ni36)rW6F*x^s%QcEtV z_f~bOn_VvU`+9ZvX-Iq>AcQ}WViUFlj7M^=g-+NWDFClH?52F==UMq}te6<38o7}U zD+zNSoFVjRXb16)>P3?_l0%TV!;`&IFGx=rtZXy{X;sAb@|zy(XTql*A?0zI?(n(} z!x-lxnA^rTC)12#p%5kXC1Uha2r<7^GKL6rVNv4+V1k&eL+#tu9rD&Z(ZVnWIxTOD z?jffT5Eu2Z8hjB38PKgTA71-TRV~+m8TPxA&kI=)9lb1kMKL}nBp@yU99l&@E)O-HO`iA2UdIWsSb3u{ z5JiCvwQNBg!3Uaa@`K6Ogos6Tbl73IdLj7h{jkXSsaUIOK*({W!m!W{epEfGAN-Dn zy26u;yectttHXX7Yi1(rMTGG|qth?CmZ%IXKdzp8*sh+yR&q^*DAW^BfH*esZa%34 z4J&=CQ^CB!3;)i^uF1o^A#);`xShgkXR0*P|za3DY- z!s&pSdr^s?(nHoo&I2oT%elXLT<#m#yV&H|rMpM|L3pEnQ=Pp{&T8}|MvRVMsF0sW zLqbyPe>=@-3uri4FDUN9$}kRpH_d4eAB59Bvo?{Nsvwnzav#zuF~+=3a8_-KD?ZY- z)_mGbj-xl9UdyBIO<(TF?oBiysT~q7dH5E@wt04KoK=YgSUfgbIRPZ-6S_niCCTjB z9${~L_}}4D0F@q#pmXNmV}Px$5m}0Z-A`aC&zO6UtfX#WQTDQkMPb68Tbo=p8+ngo zbnJnU)R9MAA^cu_B?6{q8SI_e#+j@KO7FGts&m7dOM0yl+bjLwn6*OoCndse5TH7~67 zx93FWDX?d}WAX6mFp}rY9LyS5%+yso`q@&X1g5|PC)gbs})2UO^pTsgD(qWsg9dM51ZrqVUZ zE(ID2k1+Kk%&uJ3u&qmGGIpzZ=`_==27QX8KJo>WzYvUh@ic=OJ_wKUWi@+Ht0g1h zEj+R!Z>8tE8wZMQn@Wt98!ke&`I2c)0|zkFRY*X1kQNOycV?o?IVRfQZoCL_p##8K` zY>%`tVK&+>XczmJPjj(Zg+Q{JIL6xsx6XO_G#7jLAYAM#CcpQ(=@?XhkfFkO)eD_h z)OL%l!nh62=)y>4Q(oqAsST;vAUNRb&XGsHvUbXBjykjizpD1nb2%>8?$xy?kCRF1 zv!xTtpVVkX?zy!$qDGcv%LElL`}>xKeOZ8PFLIDn#9f2O##9k?)^$fiX%-PYeJFKy zW$oW5bUu`w!zi9P$8v}kl}L9Kx(HN9)fJVROoZsW4cu7*h#mH9= zEc+ew>o(3M3Ke8fgbH}5i&PrNR&C6{nO~USM3K)JI`(Tfj(vQOzPIam-XQe4ml#@d zXz@dfQl0DAv{KqCbu+@JwTIIpjroOXiVcz>`5HE^EfYvyrSt@(z@lM7pMBw8)@ z<9NXH_1fBF@{)VwF4karYmriOfY}-8J>L&K&%Lfhw6Ew`ts~Xv2R43hj&gmOwk8g7 zZp70ln9-W{#xiYf#S|)+?0E1w2<#h5wB<}4%>Pi}x2O*X>WyXEGN%v%MxL$UaG1 z5^d4GQ!s32Mvf7@_xyL1X$#e#6b%z@7`Tof-cq71BKy?qDL@AvHo&I(&N6MWcd&>u zP>^?Hgr442rY$Kw6ACBRwx+9V!L4Q5M${qHnBsf@&PY|>U7{^-Ep8O9Kn*iu(|S*d zw$zSd-NtkwLbV?o(D#;Ui}bci5z6)Sn8ne$twdX1TxK|oma%9iH>SR?MB7NQ0zI~E z4_uYWLj3*`ZDUted5&4tsC&hZ`+*Yai{b0cvh~5*jwETFjn=`$;8fCT3xPlERed~U zY?gum8ld-~Nex196YOn?*O9fuu;;_I6SB)uwx4m!(LjJ;Gou^G-8+bYxvK5nUgAO# zT&RZ7%y$X-5gz}@0Ghi5O8}CEqo!${8;VO{8)W3s!ju= zaeDx7g)sN1l>-EGy&LiInl4HG! zNM=KfPu2FM5%E#ou6F<@H8aY}ug=h6)4`hKe;5!`@Nr*V_kr${wGP-QgisfF`1xuUaDNMz4SvU$wAOclh(rQSU7A2x8fU zVG6||fJ-WL=}*@R6m~7lM`Shte4pvZ#%E@g6*HQjtsS1bk@HsC!CqOMk|5c;YNzBr z39<%vRCmw1kaLu*oV=rsR_i`j`{(rbi~59+C6-S>qM)1H_ynVYsHBMHEY(B0Hx^S7 zs;(>$SYc3Ij}Vs~x-*}z{oC-N9|zh)9==CtOO7vpU{ho%*svRRKsDP?#D>d7j+=rj zt1dU5Yd_S%-L+HF4o;y??8v7|KuV}Jrz79r!bXJx8Gc>pA}e@Ta9qzMN@Pi8w*~u1 z8-{RvVPhkgYWgXQ(3rni zJCKe!z23spsk3*rwbVw|P?Q4_ZRiud1AesenUNc!s46dCXsL+9Iv^Y9TSL!?*_E=Q zO(5)hLw(;<^K+AFav~9MjPw+b6Ci^YpjL!`Q9osA1mh=ms$l>U1~p!fg+N2Na;TFp z)&7Q^_2Egv=n-4DAGsA7nVvfocR%A3_2t^^>#A*CV~9FeMi<`DL=%qb1z(v%PplsF zCz{#2meAKMw&;} z9`xJ7(l$<0cJ!?ok%rw&{TA>OYUG-01L@T9L;n7puZ7wCLG74+;Tka?!uD=}w`bNQ zISGOW_B9@rdELb2RnuY(2FDNQJWi|mqshA;fOe*Zo;$ISA3I9$<03Pu>RRl)G5CP= zt3q@6$>e^3d*aoJrJsRTClb^@Ez%F%2hmZ8kqBWyp!}@%P!SrDVcrw1wBl|Q{k+Iw z$W97m957pUlyEwJQKXUU2(L^~rBtCzl3%~98N1e1%}HnMaa%Zf(;tf&P4~DngqJHn z|B5}_rhziB!ewubb%xLH?=t%cKj2@F+r3|y!>F{nV^GV|;bZ9Ne0 zF1DNG)7Z~w(K#jBQa%doIIQA0;zuD&)4ucybDq*Xb4mfde{=DyzhTo3` zUzNF@INAEz%kFNpL5ul=^>3}GUe^w6oq`>Or|OPYXGA#Yq;=5 zs9ij}e*PgINcOgy!Z90!jzioO*%e9Qk^9 zq|iX&V%xSFUo__j%OQmXXq!-lDF+7hbL$g9(o_|>+#+bvCdPV5o zz@r&D=nLv6?|OI&9mf^wRoY74e69S^Z&sj>vdFsLShHCy9Byy#D9%BjeRU zuBh9)P8N?@l0-`qD1v{)1_4}96ef=>XRLvTp`L_hg5y}^v|ho>A=m102rwSC45eh^ zV=P~xq^DQbXHu*h&GEmg$V{pSR0&Zp@R3{(o!ESJkwykvRK+5MfdVfg;w$Tq*qpH` z$W3^IE-iI}*A(dh&bFt(bpTaCtuu&TTR&~)Sx@d5#%0`Rg<7uQ@P+tj1%@5IuKq8x zyU*F08)4>E^+z)kuEcG4rqbtX=gw2x-8Ev*lf1RX^sz}%TG_@7)I+*}`NvjpE4E|Z zy?Sl~Mcl6bgxOt$jid{g;F`IOT5rCg$isyvcZ8RzMcD+zuOD1nq!B<6A0}&Ts#JcE z;Jt2cXJIt2FR|_7rPV^4y0>W{PvW?5EYVhVU0VszkpMz(V>Ng~iMASK=zvNKWi$}A zesp7rwz}dOh~v=?WT<8ZZz|Ci0=9uKN(?j&G;bK+RH7|D-+ z5rQ}cw1sp=WW1$BTgwAAf$|BN5-?1G`PMRRskRw9b#XYT5ar#sm1v85XQ)tLVM7Gn z!BB55)0V-qgCRtr>c>?7_7ZJ@@=&7)RhY?0z9{B9%CwEa!+}Hvx@d$1rMHx5i&roH z#8D*r=T&vyS)#4ri{TgRs}a{41k<}pw51q}Q;!NP&QK`I_%?e>=$6T^^ zmuV|}DFJwzaGBAhFyB+6ZR8XAgR=x%O5x7Y-&>+>Y!c84R0L8`c4&sTm1`Tq!44>& zGMUEa`^vPn09vX9rQ$2Q+4q-ei`*X(vl4?@aZv^MK>enJ6wXb`QX7^b{nTF)QV#!_ z)I!!QBu{=jSO1qn%@}WiA^Gw#$qrei+z2fCJd?sjO(|_f!kwM3g94cM!4iqK;hBKt z|3hsoU0v*`mxm{7adFR}Y?KD4K-<>ikHs~5_u;j>tC$%gI>e-kBMz@M= zE_6UkkZB;ok*t97{F%9p*lm8cUdvPsqcc{hu))K+tH@nr(BmEt4m)WtB5M!yQvXtYXCL!AF=rxD&5@`M6C4<}YJZu`-^_9* z*?HoLs?z*Qk<*EIy#w&5w3TcE+o6g4LdKq3@ zzgfTjKQEbJIhT?6{Z`%TPg{BQfzhXv0QKATKhG>qNv*IY#qUUA#^0&;h#Io3tVX0L z5&4@%KR_fgI%lNZ86EcsLWzq5(&VERdyrzwW2?0i>-H0N&s@0&kUk>7!SV;j0AQrJvt`Ve_bC ztG?!~1W`voxd|rXfb3GBSm@MR_7Cbo+Nf+NvbJ*cdK^n&&Ok3@<+$kV2tyXEKzDvv z|J$^eUPY$8+VTX=fguS}GWrSVuv*k#V)sXNE&cH0)|Y(&FXhv#ux45SJEJ|~7mK7L zLKl0z7=AItWBcAwEO2j|$#(!(s|xA!my zG5BvTI8~U16QXS*< zmHo&XN=!`@UfOtEUXrKSC={W-2W7}DI zaYOo-jJu+;L53s=3x=3;=g#Cn@(149(=m^2Jvr)jE<3m*@e-s*dPzg7_Z`fbQd!)W z9^9eCpwdIVtg(~^up5E6pfBw^(84sncli^9&%SPFts}_H>;a@EjUE9P5?%t34ot-m zNT0|iTz6odF%2U^;tnBwE^h?+6FNY)q@(OQw$d^Hhm)6uN~@EowL~xAEs%Sp_Dm^W zp&m5U%gY;`++H@&ZzFxBe}boM=nxeQ3eBn>YNAJ07MOu-l%N7(STrm{bVcJS6MEbL z&gJx*exLE5dwf3wNDko+mpZojgzqu?jyI~;|tX*_<$ z7l9UU-k?rRh;7&GMjD!HU+PlB#)0)7W7Z$jg1>-Jm%fV!Yq68xoG?AB0`_R zrKVIL6KFc5YJ3-|yBiue&Un}-bvvg4>t?tBVcMZUyU$?UAdn(ZxL_`YgRyPh*tltC z<9+Hv2H{ab*l6Oz09XC>`$FrFh zdvrFBT1XuUdsh=pKn0rIdnPx534~k|zB>q_DeyGj+pu$bIdPrbiA)A8i|-Zz$5{wz zY9t11lJDMaghDXE96NW8oY_B8AnbM8;oGM?<@7brUy63DF1DAS z`cwu1*v(-tK~>MRBF%T+KmDOln~k-QhzZRe?9PG@%>E`i%@5B0pl%?^phHUFfUlc0If^7)8mPeTy{~e9L&)%heXK95>0BTz6B=%cNoz+!Q z*9W_3h$?glwio^G;LgTn>0^*gd}5qJT%XK%vBw}W>K(}BoYZYEx7XUIN2O2DC&&$j zAWj&tE7sgkAN=s7bMTCY$NUHrkbIL90_!)8&rtlx-hj--I|aG0(x;D+B2H+5At2;D z`qpO~t-RwWC6&@W09Awz9>#E?;A6!8U6Yr03V?nDH3nyCa>Z)=xyeo7I{}!{#gbx> z^0GePID(P}^jq=2Nt#*ET(S6Dl|L-n2<8Z&xnff91!R1;n9ziS#l(tCh9UZehFLUM zDsgeGp0oNF8&94ux=$8dMsS55=$W+r9n={26Q30vQ1<9w zYy5NWNoS8f#i8$QoR`k5gW-vM`e0HE4axLcj4w}%x>S!bozzm2m%fFrgMs$7i;{j6 z0H5P_g7OirkufZ6<_&ka9QF0a>fj(Ul_vy;z;h)|F(W0LjX$<*@(}Q#6l}3Q0wd(Z z7D1mti*^IN&jKIH#ZbBC8=Lf+oLEk;8-HwJ--Fx^#`H0K4lIf26&naYjL1Iung>qE z9P0I(jk6BRJ=p{+BD!ty2DXJh)5tlGe5R!ZqyjW{v610G01$(q4&<;G7NkYWfBJBx z>RXNdxnm%iAQGlc&w(0YK!LUwqMB$)4lB!kyKr3! zMOgw*^rK_e8jx+@Dc%wW2}C^HN>H2U{(QG^OLVRYVO+pA3Y!*3yRUdl$AN_s5c-1S zv|{CZgS z)W{*Y0C{Qff``_Rinjzuqnj=b+3Db4@Z;hwh0qbmITYDc3~c?M6mA({nHTApO7SiV zQQ4msZW)qjp|k`a)CmIg;C@!TCH!Q{s37hL6;0LtdEu53{%58s?kCd;`}bcIZ^@<@ ze_tQs5Am5D{IYOMxI(JfoFl@8i-s2bs(4GhGkM$@v|%f@>HWHR%h)1Jg*=D% zmuN}ejD$wuQu$qbU&48!nggaCfw!Xw zEup6uZW+gbheI7HH?imi&e;nm?n==tk6ulpf{jpul||@|dhIpAZDR|_2Sk!uYzF1u zkd22qNU9E?wyvV{aLz&>3Z4bWNHv{BD-<&)h5?%HM+k;LW8=fA>`q?+d=b|$orEjU z4rrX7tDjr=94*7M*q9oqL3wz^IL};|uY92H^Tb-@g+L%GcI=-w=l*P!WS@(cRG>g_ zHKOwuP8&l+mN~$K@WbsXK!T$7F+%k%?E#u_cwZYs{5j8kK?oc0y#wM4upQ7T(WT7nCU|1RySsT zS%=KzM8syvM3)sjov0m)!ri4)jt7x9Wc^r#SS-oZgfE`*IKHrSM|%yIZmzf;T{h)$ zh>+pbcv#v}>M^|VB~u;;$+{R0g;-&+YBAiGE=Wi(I5V+7?`_jbT=jIQ7IBaeww|Q= zeFLDR_p%c2GEPVy@h8`hIfTmSO7SY>` zd^=KE0)4XU)@vp_N_d?krUp&l32c%Z#%njfY?@)H9ubnD*=ywQU$<~{Q9g9gSr-Hu zC?xIGMMS0(4EQ3>DpxH$CFdZk2Z$bVqTDMWPTaO0E*0``YjqKndMop{yaZ}5Zz!n_ zoz5~kEKo#O*SpbQDo;IP>;F3P>V>lpn`8I#G_w%+Udnf59Is!na)!}61L!y2`Vu)K zA*}C=Jzdx!p@4kW&8a-E%ALzK#!4SiP-lR*kSbF?E z+IR4s%cZ&^8M^B+N1U42eryn3lT6zNDYeDtTG;R5b&3pZjJqo1;{`Nb*nfm~0b+x? z77wG|uV3i<)Jo#2G)-EHR`#qdrrEsYrT9|&9QJY>*DyWf zjo-93J_)732|eN~a*CQzgcn1^ElO8}Bo^x@a(F}8(;FAAn0ko$_|MHv>Z*zK(AidIBUFxkp{@b+sycXoWsP-Vh{1_WE?g3 zVXU-FbUW}@2(_ULaMMEH^DUR@oc6IB{xcF$g8tm1;-!-%WVc1S2Cg_g5o-$s5yd>< zsJI{w_4MY2e2z$FJxV%D#?p#d(x^QmJBRK)MqTs*K#h)U1bKZ>Po_H5%Uc$nH2Btv zS6t&@TXF7JKfq!*As+3m25N@mcfs@~EE7|FXo#93fq? zl1QG|rW@^xJffn<%ddHxt0Sdn72*UaRiOKX4mb~j^Uj51lL&W=XVs`JPzsA#G+>Cr z+0|_YHAGgtcP(7_Ksqb9KDREcBnMWCWeBl65sBnqO71h# za1MDC{8m9(p^NbDg%m=Oz>xa%;(cBhXoaYoi?1iOSD-(q0aFhv7j`>HlON0f0&^sP zSPaQ)0&6kBTtN(1{ph`^Ee-i7UxK?DRegugOCyRF3Tk0hB& zaNv=oU>j{}?<>*PvR%c4ysbDPQ*Z3Pf8of<<6PhRkA6)qB^iy&3%neZQH^f%>Lkka2KbNM2liV@t zDa<42w}T9&fEs>yVO#paQF(PXP;5hS#-m<_gKBKvUg8QVNDGivw}I^fj?#jUlxd4( zJxRR~ivUz0&>k$)RwU60Q>iB4Vx&4BE76wFROqN;2Wy*Pjf0PuXlr6=j{dw7!brr_ zIr>D2wrKle^a{ETjTcba$|p;-1&ked7~eCK343(^QzhDBjEVY6Ks6TVnH%eOEJ&vC zG{{#EzN%+XT%~vo2GYHA;nA6SSHO$fOJepIoJD#+59on1Cm|YwMF5g<9zqp}RD8O` zIm!AHvj?23x(FG1acSQRb@h)bhdl)IH0G^fZ);yiXbpA$WXtb80 z;>$z+-=8r4a6N$1f7qY#Z2-6|3b*?Qe*X_d_96J+>JHc3p=>?ebWx@zc#XDdsA_Ll zjP0uNh^>bUJpkmID3NXTwk9FMR;GEMHSZCBxZhjiAb+Ur(5xNmpNWWXdvBzF#P4>f zs{N3Q4|^yd7qAlYMr&j^R{wb0pZe=dd$%tm579la?V*_;wj&O(ts%hV+d1KOm(_H; z;HfWlR`zWBugfGF+NXO<>(RD{f;ATt_VztX9fq>Ht-2y9XZ!XM%WjYNt%(b=?V%`c XMcoL5_;yKJ-}c9=?L8~oxrqNCYd;95 literal 1289088 zcmeFadz2klb?#e_{ixczx3u-LWLuJJZ@2B1E!!=r)h$78;&NgG$;lyioO8$Ek8>kh zQVZNN5|%{*clh823=zR3N-!XZ0Rc|zfJp?{SP%oUv55c?WMYU20t6TkU_b$rIKhPb z`{t^uUHwo?Mm#dO*MhF9wX0UGHP?K6bFR5od85}q&-Xmf|3u|?pB>zIqxWo||8wHs z`0VgTUF$CwzW!7=r>Lp}$GRPLR&!^byF<5c=a0LO6VH3Lr+QSpo!Yl^tzs^std3qK z{Gj*0_}RgByPLnN>c#x0*66SQVv79jR;9F|E%gR3&f0Fr;KeWY zZ{Q6#c+YMYH|71M?h0?gE7 z=+(iEYHvurI#30jt67I!>%6?V*2Un48+d$P?@iem&*lW@R1|-4PWu0ypU`GC!rrQm z(4TB*tP|Dks>@F4jg6*gt)4MfRvkyHjiXgYH*UUPX9@#j$acs@%vGrraqict}WwX?9uB&0&*b8@V*tlk7)ta$QBg3mk*KgjqWwST; z)DLdnzHMy7+M$unL&GCu>qpkEUOzZE>@{b3aH)IaABQ!T#H;)ai9~oV{X7#L(p5q=Bt%uePts7gjc5vg+;OLf3tA=N#aQ58w zJU6^)cyQzDv2~-H2gla0S+~YpISZXi&ke2{8eX$;)#fd0){TygtzPe4I?Hnx_1gJ| z*U-rwV_PhrF|9sigGs;f-TMt4D^{Z5m!bIJRcp zpm*6U&t>%eA3Jwkv+X$>)~y{L8e6+|{mAgm-LYfzMH_}T zuG>61yl&0rv5l+OZiWK0N${o9YO+eMe8HA2V>>o%*)pir zZkn}*?CoRQVArwhHw>>D-neQM@nBpwjty>{bvVz0 zsyk^zr&0A)b z=j?^XH6vSAuU|K^YTcTR8;4PxYi4~g;~I7Lnr)k(Gq!ERwii5aBZO8HM%Iq3A06E| zxEdiHUJbEllkaS}N;O@x4Zhn9ql}Cp2Wv*w3=eyYXW`&X3`#ZZeD03zf4E`Ysx6yF zMuut27CNw%SwKEHN)s7wT&FV4uZ55_>bp7a>p-pSN<+C*Yl4*^<=K2jA zuXzrFjB#1NZVNVeXzk|FP1v+qIe*5Awr?CC+k|x++_ZLd-Qe00EY%j6>&aJiy;J`i zSFhi=*6JEwyK!{Y?3!RYqh+;-(blgT+Pr1`$d*l8Mn~4I+p>CAM$6PwX@oa!-+t}b z=r-fo(RIU{R<9afJF<3kWbLLkgR}N-dR5SS3s7t4HQQm{_3MV$ZQMMD4qgS@tev&0 z%Vrw77i`-+w&j{_fVNG8n>LM(tlqee{|#>$+B|DrgVXAAJbdl;?bnZqlh;ty=-9~c z5U_0X`k|p&#cIv8+A@w3I|MzN7d5;n@#4f=7}vspk}zByuc?Lg3ipIjSi^ITy&zf;tCFF59M}BVtA{}p zc(pi=xG4;&t5)|a9w*!s*Xm&<@B-iGDpP4q4Z zAlIVWqD8*9HmX3|D5Cb-Fg>LrO8dO{>}r)NASnli!x~!WsOouR5RCAL`u&=>E~LZM zXl>v(+Mx^Ue-wyP5R*r%dN`=lP8Ir*niIr+I4=mQkgrY?8-2c7?~B4d-S+*6|JQ;( zDygd6Z-jk)qKZa^|GY+napDd1)rXRPy&j!?c0hmOiH7H`hjP3ytT5=bw&GVBP?x)P z|ANp4L>I+>en2mM?wg%ojQ3p2WZ`BAwd-+d4^0(G^#S(WT zb@DP;!AtE~-B1mxSDO3<47J41{#t8clqxm-ro^@y!nAZ%k9*#V6|hz9+;h+6g})`j zPy5fi_~M1M{Hm+_p61sY;$QoZu0n)LbzYSpTS3c)jWSWf{+m`&USLGhySYtrP_@p8 z-!%++HVi7^)6Y3a_vj}6tIvb~b$edPv?J7pzqq3k(7|dIUY`74rDDt)R9cK9Jl?Wb zsn)E?&?|eJPE2>$uao~EVEiZIm&@Iir#HZgI;dNS5b8-{-uDb7^0zNpfWq~DXJOd5 zq}HfhSZM~&_dg$fKHBb0L{%7nK`kb1m`+R_@S644hYM@Z8++dN9WM&J_UN|JYhU#L zjBU7n+vxMJfA033-jS#^`uyi#`=Sk-o;$kjIb+v*e-_WpPB&~E+xdrMW81txj@z5I zKkxb1j_n-Vu;~RmcHjVcpNJNzOaS6~B1z91-LMgKf`Q!~x5pH$87gO7}n){pY zZ{At_``hMSjRW=j>K|zSQS+~xf786Dd3W=D&385THScQvE&txz{8IB+^B?2;>;F_a z+IU6ti}4%khobwV+oLbVN9+F>zpwH3`d4e8tlimoO?XdZcl5WF$La^;Pjbf>_*GBc z**H@BSpCnU{mrA{-SN@z&uT}*qv8AOI=(mlyZV00KF-x$@dxVfi&c7e{I?uG7XMBC z{rtA_Rn5B_cQ@{7ytZ*m<5+xa<&Bm1H(b?UZ9G)_=jhwvE1L)7k3?UI?{0i1{**QD z%kdYp#(k~!548uYZ>-Tj<;()i=7OaEFw%>7@=n)=oFosIXzpN#L} zeSggVABt{{UmD*TAFusI&Asul#?8&UYp;#IUi-)T7itgIjy4Z9zgYW7d?fx*{n6SF zY9DHTHvaqOz3~rfuL$p}90~p|zO{a5@b>5E9}_}_^?(zrP~(fDNb!`1Kl$K#JT zep3HR^WURyM!VuaZQjw)+a8HN9ep$YyT%jsZ#F(vy{r1D|HJqLjUU&)+c;SLK=ohz z@5lEw{-b`p@ekGaR8ROnia*S6-)}rreSh_b{=dgRu7A66xca{8_x!o{~7&#q*i?^dQJ4L_=AmCMBi(Cwt9Ein%=3=Q zH=+;K{xyD(-u^iHO7z3(6VZdwJ1WPbd!xtVPc&|deiVHzy1)6A<^!w*F{dx3- z=(gIss((@ckJ=9_yDG11e69Y0cz^A4)xV35Mt9V{T764pPyD&~N8x*;f3Mx%_)hi9 zm4_OCQQcd4N%$cf?{8Q4G~QPE=lI9rU&eo1|9o_F?RC{d_0QGc5Wl(p>e^f4+u}FH zx5jUc-yZ)3f8WIQo8z}}+#5ep{c!W2;=iulQhTEMK={F?&cu1IsUB=TQF%@9(dL2p z#wZ8zToq> z(P#DOL(#XhzPzRWC$-z-!{O(nPc{#Px7H;Wz572Gz9fE0{BZr@`iba7^m_h1%D?aM z@7w(QF8@AK|788^&967V7kw}K*NWPBD7?M#sph@myX&6}4>vy;_G&A?{r32;s-KTu zQ+YgaG*vt9h;Og{dF9^%C)qdEUfXz}{$`~9?)vL$e^tA^_Lr5tjqlgr5`8rOeC+Po z-TX=XZdBf98}~>$ zd28b(nYQ{fxa_sHe~Dh%_)7EB#%*71eya9heWLL&bo`TOzwh4i=S@eKp1$aD{x|CH zssCH`?#jX1n;LJ7zE%Gyz5mDN`@=WRNb@%|US0h{bW`Q4S+Doj|Frg%`WKq7uOEru zTzggZf!cfP2N~nfH1CgRrrSH?|75HlZaxfdXcU$5O&`z)p3tKOaQ&g;0!&wto>eP+GxZd`i#Ww+J;vi`>A zt<4`b-rTseep}^D%{O2U@2kF{_7~Z0Pc-gqd@5t%H#hI9-$SK$H{J)6zO-?~SoM9B z9IPLy{Ac4WJfpLFsO_G{+iIU}ys7$7{FBBP;+NO%p_aQF2jh1~dz)`+el~g&hWVbx zKV@&3XnwBow&Gy&wPV(t zPql}G3Rms%PLr#B!FcbXRTS(h7p$gWqFk_+g1zN}H5A<9k9RKN_(U+?>F2mF98W7d zI_Glw7)2K(;Xtskvy#gL;dtk)yoaIsa4Z~87Pf~Ie;}xIsycX`F!7Vhitt!C5_+^W z8t?Y42))kb3B65^hvVHgm-?-#yK8!AVJA-1ofG^zFZlVm!xxc*A(qU<* zk<91L$GLNXyEAXl)MFB+{afeYhWO3I!ylNKc;Rs1C7vi=={vhgGh0vfFXPa7s=ty$ zG#m_Yh=+rI4z=MRNy0gJLb#^XK%aujh$^5d}0lThzDiv#aj$bt8?Iq;r74!kGgaQUw@uA$n-n+AfuZdHfM)y687 z@%Eq8AVuV^6*aMXM4pXhrGT8ya@_88v7LupSZzq|KCLm zw(9*f`jHT(;CL+T#%b&7);vAg?*{ZT29$AK*{SC=t6tSwm{diqvpCd6v#O(6HED@v zRY$X`BSBS!%xGqBnSo|uMl+2(H1i$JLPxW#;bP;aW7KAnGhvglR@r4RUB+(Qe$dkZ zTxR@zGR8NazYLzwxxJEHJ`!Hxm=qd{HF38^wnf9iV*$rI;iBQ-giia0gWKp#`muc~ z+RG^yCrXk#0<^Tky;OZ8A z5)qehU|>*g8qhW`YL6Q&(namRtakhv-ZmfJOZ%{`S-R zjL-;qP6_pU3Q?x?$2vK;8C;PBkZa|js#BP$Z?05UYH1Ro36QdfXiO-fMzWjE`5~kIwTgZnLfblzvSQ z`)yO)$HQ)LRco$Ohe^} zfs>f6b~7+kGSzI(b}f6ebxur(upZT#&A8d*t?!4^(y;yIdMi%+OT8C$mLN_3Q@j^B z{nG^xq+WY-W(drzVF=EXAvm8QV+e27ke)Xj>U~{FymXD<{yYscOIz0w0(T%Vx=63$9hIc+`jK>ogG0V? zsYumzmT0j+Qc0!ub!Z(gN5Z));`N*@<$i0xAzzp7%`*(xQkM@z*Q-3v3mSR}zq&VfJ|I%ZG6A8@(}PS|%AB3%qJa+x0X#9oHu zelKl3tz~@nKjA8vuQ~uf{@bS?`bh0*tsqSb?^75cGMi$^jp|Yxa&&-nRQhQH#;7S%3peTL z)WQU($?6Q0t<7;5%IPG+Pp6C%uB&DpuDTkydErW7Bmq2f~z5fNociCu9muSEE6pmECC?1Rv0F0Qd$*uuFB2L)t>3z zR? zy@ZOi=1o~ECv-$5-@pM#a>(!U#^+<)GESs_aN|h3Db^=`CC5xU3L~kYDnHs>C8(-+Cx&s$6u% zRGrBJeH=wq_GdGJMnFy_(8qEVRdtrx$J`jd9V_5WuC+|tR~;x0eGKSjw|4=$f_zos zU;4t(=?wV_+i$OBU7=+&;m1&BY_TJq6tPH$7Ou_Q^BFpQgic2#bx?QmH61!mQwN9h zPCMub8g3?nSw?(Gheq(rKP$bkIHTy{9z~6p5a3$cxmxyEy?9(lFy3RCoSop9oLZzbYqKpOy zW}?AaU=KJIwP4^28thE!1ihXqm6EP-iX?NG%?*N-Gwvqg8z{Cpt6Y0D)14qAyxl_ReB; z4=awLW-Q2f=p}dkjBE?>5@V7*5)_(pi;gJc-Qv{U=o-B#UEr8xUbAliy`l&m8sKvO zGGW`xiO@yqV5So3D1=-Q{GRE)q$cN`xTc) zCKm|=laiJ#;UpFzzZPeC{bWnppXOanjE+@qbd6nq_4c`01RNL z?O2#OhFtH3DL>%%(M}r$BmZSC48F1xrwhxY0MX||W&$UQDMR~GOc~nKTD99t=MkcV zo(qkhxhWBR|N0{z{B}*Od&YE?o2kB>OoX7>0Fc3Az@jUHZJsIgG~ zP(-8;4~SU-x(9TeRV8&7O;<`ML4SnQN;VD$SIkcMo#6D8&}{|+BNr~H83;s#H0J4H z6I2T@;VPK^cOX!>kl!aWfB_qjAwzETf)OT#Yx4*}HPd=ML|6(9L<$Ym(i(ArFs;$* zs>yz) zG3iMuJ_E7kA(U+^dL$!liP&z8rcl{A`*h5LG7bFLBm2yL{j1PyoKeL2{S61I%n&ZP zT8)$;JT8*ME|2KQ03G9)9z(}v6`3K7#Hy1i1V_?arYfp^`t|h5l-E=;sbEoy!PH$0 zrk2%dAqh1L6}&GUtRn5)WU0fw z%(E?ZxE7GN6m{Df?1hWbDEZ7riS8Cft2!uzb+oZFXMyQN!Q^DF?39h*+_Jb-gZ#cU4*k`Q7l1dTK6jy{@e)*YuoYB$pC zRKx+M!>DVJ#gcPvmPtx%3d6N(X72r-)HP*#(tW%F?T_?d0u@nrqT(f@A~R1x&$&Wv z?iNZzZQiWoPvYRO@WZHShp{gWJ~{uv!6MgOx` z87O!%K2u-c;w-H7@&-a;!Kbg@|L(66e2QEwI}brArdCn-$&>+vk}{C(I2IJQT-DN) zHuvl?TMj6a*5n?i=;3E^{7IcHjgK4AA_979WbDG#C@B-CB3ykMj#e&a=xUqPE*vcv zAS#CoehNp+Kt8bmgGKG@2)^K-ogp5G`l!PMW=1^lKmedYzzzhkUk_(#EQlIBEG@~n z9k%0CoMkAijI;LYF~dzoHU!}eji=Z)m*IW_5+k>J~DKRAoSwob>T~#f8qI zv|qmUg&LDxB%6phM|yoqCn!_I=Wv+EN)~W+u(+C+oFy@bb488)MLBf0={`{df$4Y7 z!evZSn$OTmf6ZwHTzwy{;bvt_fdE;bqHxPoedY$Gy@oiTI|n&{T(g-%l26P&NKv?X z5j`x+Ve|2JflV>9W*6v5n^WathV~R$2E-6AP7F6IGsChB3_c4*(+Htx8lkW#%3UZL zuJqD20kytMq=kOXsW(P>51s<#<_zB48uKoCaoRQBko1c?3|(UG2*V9^x`miRvhjY38C zGzKqg!~?Di>QEYO70-zknF64V0hdk6jrmG_W)yvmlKJseQX$RILLHYh-ivwAd!^YCb*9?sJvBDgG$c*eoc zNmv{a&W2edRcWFQs#4mS$w40$D&FSctQ!$fU$GwHQiFr(=iUSH4DL(+pm{9xl>@@~ zOaWn7282~NmEr(lU68}1%afRAGL2@K3JBwq145gwqY?vzJ#>gz*K;*{3P5NkZz?7{ zjcV2S*)idhFz9l0Vn--gZRQlXJ~~ABh~H=MDl>n_4Y^mx-}#b;b#B}I9b%wa^j?bB zdTbGAF2otA{W|_%C9P9`HT|xhlr)i9Gj)12f6SD^y;AjFAgE0v zrT%r|IAg7zCz;}bP@3r(l!o5}u*S|#?VA3-O#N2qsFg0w!66TuzCDKnx_=1=bieZA z7#L|^)ULE|^(mgTFQQav-#H2d&MJ@=xrhCz`3YOm?A_K<1!qI?oSX*|p59^7j>FjDK5sDzPRv9HF_k z)4~MRF86;Q6cp|x3V9=3RqHS(M|mV7#iT;uHv*c*$tDzHz#j%*Z3SY2jK;ZNb3nkQ zYBD!jmNfL5M$$-@{m!{i91oAOt;yVTyN%TQ?Swy1-w{`)vM` zVROz$mbTb%Xu|rF0Sba&82x-wT!A_+Kp7=eE*7E?Ke|+SOnil0oGcOsnPe)lt;SiT zDrV{s$eK@vz*&(s(Q#H`5U1sI8fhj^fXOeLL5(ZcJUNYM+FX$QvOHSV4-ZySMqMB) z{medziQ%lDBUub%ls#C1JZYXR6;8^@lu()LaymK*#hfcT14V0Tf^N?}WDxrR57UY1 z5!mVXlmVu^XgV(2!8r3*j@ET9?R@|!nj_4IPX%)i7g!RKmZ5SPrJe#R3y)sa>dImn zAT_-0GNs{KM&$ow@=M&&VzYOM zG3k;y@YvJ6dJfOoge9M(Ja4jlbsLZbW66Wq*vIq$BXU9&QGsQ8UhE8$^slUvUT{p= z0m;^Gr{6PNZKKpZ4+w_2=*W9Kj|6^@c9f5BCktq``57}ZkZ`j{DkFzVC*gMN#|q3+ zi~rAcsRxT(wClNnZv9fF* zpoqKugppAIz4klAdTU8*D|W9ND-4gi;~bET9nvv=qTykmvP&LGT&$O}r6nIu%$$ZauLhNI)K` zPLrB@4gzRURi8>14+qa?1eT=OzM6)_Ub~oPrXk43-t4M+goQ{(60!4g#Y+BHvRr8W zS-cBb0nV#)bx~w%w_(AK#cB8q>eQ=}cszYHuOjW+s#<88YH8%PM0qI{P5Ply{18m~ zA*&<}CzUE5fg*fst=>pyhqZA}N7ID1r7HD#@aOV2MMJaCHN& zMqw=yzs&kngLm=dKoj{8H$j17c$17%H0Y-_)R#^g>Pvs-Lro)kgYEqs1{(%+ob}5a zYzB>i?ADbY$l1zld^(GD)sJf7kf3NdXdRi8d@%`b_ib+jm9I^Ya%jJb=>_PuLQEDL zr)hAcotC7#vo(CY1dEhlH<6;$lVov_Oi0qZ$yF_uFsWQz4*xw1@|bdlN>a`UKe;>U z@wf2W!F12w?6v8B3feD|LxGYU4mNWz^?))rou2x76=JHIQQ6dtHtxXY18)9lK$Bh- zuIx37EZq#HO~`V$Z(eJV;4c64qZJ*n#f%j)&k6O5l>hz zYdTjavYgU+?=cen(+)Qdz1}PLo?GU`}XRjY>blz*`xE8aK=E+XRm6^~g#gOni;5m5rlpW1d5}cRL);(E=T)x0XBr+PTHvkm6 z#Jd1mDDjSwWQ8MXb}rqnW{G#=dA%A~R8^2SoSu$dgOj*xneL0O<{qsHz^h)Sz(q@8 z0(rB?g~Em=!NQ~8Iv}qWg0>&2xdaE*<%&+5Cjjg}C8>_tG&NmPK*JZW+t?oql!odh zj#znGEV#dv7JC?ySvXDj-%D(@f%yiR*JQ}nLTN~-LTTpsb2J}*uc8>qg^J!lvtoM0 z7PGFz))bSbtqX-_p6Yi6J)Y_(9Egi8A#+qGZTAua#vZ zw9uEQdzs&ce99-+Epo`^k>%20sSt>P(y;598}%w5M{8;tw%`#k&~Uf|Z7_}%EQKA) z{|FI$OkoPerS*^l-tcn!ZrIrc4Rtj6Ax3vTEu$;eLRwD?Pm+gjD}cn&oylr~nW&#q zO%zcLrzQgBEhx&6Cz+I@3}CZtut642)51mb`lK3;koq%CHRS2_MpvuxG*rV3ol{(5 zZb>yjN2vywXR;2Qnychyi-E)9_NqE|${|+N&AQ)^<;gPcC(*-23mVDXT&+;QEpe-a zmFDeTqGKV0*OE#x!6xeQh#1Z-nmDFYiYzwblA{zG5hW*uf=qWz4H_MjG!aVeo+MKW z>sL;H(xe20I+K|n1?J?L!no7dGvB855{&5HB9|?KZ}dnpcMUIF=tmZ0Mnf?38U=Cl zDc2~71%kPO(UG?kXgd`EJJ2<0}PqX4R9g7Bf3bx z8c{S><#~>)rN45OCH7O;p0}7Dw~39*B#N5fXr$TVCk(4e!Bc%4S?O^paP+toI2u?A z96erA;JjQ496eqr@TL}Fxm@_{W%ib0r9637#bn9=&+---jk%MI1``t{qe1n0CoPcC z$lgi+i+9Fa!QG3|U^H$MX?S3-SO>CBG8%>_lhP!}6JjCieOyN>eoRMdIl)oskH&my zU{P=Jq#}Y)bE(d;NIGXtR5ksuj7J0G0X{inj+K?qi*4W*F=!ejT3Q!XU+oqc$WHkdsSE5%NnOC8>_*b373M+-cSvLrJ`-JpZzj<# z2^<-Y)%0EooUPu@@VOkGX{)s@CGu=nyRFvd&ASXz_k4!MTj$IT;_`nQZGY12u%d6^@jtT|8F;RU2c ztB5k-W($cZvUTwv+b0O2m4^Jz>Z0j*Ve-oOV^T{eofrru;U>_|Wfj4@i)@9Wn~o~! zH8PJu+xKdNoqRDXb(OUu&XkivJ*&a&ZY_gB_)+CnY-DtmrgV zeN0n-F6Y@6vr4ap)4ZsXUQz+q9LMtA-G|wkS&0!Y#nm%6|fAD;?lLeia^ZOT9iIc}J3eI>9B<&@C^N$Q-=W5ZuGL z_RU0BfgM{pW}@p>j{O`(w`$>X*Kgw*7aTDOF4kpe+5}hftgE5c8G||7VG7ot5zLWV z3!B;MqD;GLOl+kzBWo+9VZ9@4K49XVE&yxLs9HK-lRSReMnOe%m;k8~BF)b)bV+&V z0n;UuGc1@%#(V2!SY(mB>AFOFq2(L@7!MxEM$<&ivj1c|E$ZafJX+K#Uk~0}{|~z= z;*?%sIc>0jjRjNQmSbM*aiIrKy~o8YizVhH=81UeuV0t;U)`;po9W1OAYGEw&ebGp zXHgop55)vypg4jgb0%%Y&Pld5Ra#C~h3gZ3D03=B+XW72=?<|*;nFkX5}6ZIk0;>~ z+ScQdR@L@Cc-&Vi^Z;!Vmzq(__gbU5nv2w!(s-0u)+I_6)}~ik4;J*Ir86*1d0D^= z25A3JtpfHL)4;@*q*Z{IeHNzd386?`%OW*CB0>0<`9gWAwJcVXAs5(3$OTmj<^q4^4YvRt$R zp9yHXDQ2Be2jEt-P#6)jP#78+W#LjY%Gem|FxF-E4!aEr=y0ttH<(k~qCg}d$Y3PM zx>!xjK52F-uyPm`FYb5aJBu%$VeElGG*P2GQ5ONiS>(F}x+GHk6IM;4kt&a96V*HL^yuHry zNiKCT)i;TR2Na{Cm_ z1c#NM)TT$oI3^ne8~ARrSb$&^n04zp-eh;a%Yh$eL`WMY7CI+I zi$XJK#&lj(#Ot+K$4jP!Ma1A1*TZ?ZgK;dJ2aEk23%kp5=fMit38$qD9r%T;n9kgc zXEHrs80M0E5WF;}w#PU)Esk-ty83HJKKV`(_n$1|2p7)4IH!Rma$GnAl5i01Niojh zsYv4V7-tqFF@{EB|>q@FLUE z+%x%P`Nm7;sU9dZo8bmjdU?Dg;Iz7_mFheui5YZ$wb}%D%6G>T<&lYEHqJtS&mt;vX z9hxl@$iibw(g(7{G~(!3D#gdH8Tp5kg)f4!Ey2nmWcHOnF;#Y9rRmF=na z<)so;VT*tfdQ7wJ%RAdB&pZ>fZnCmG{Rr}4rn53_>HI>4bfgMh091hH=eWfzRO>|} zeC3H?QiUFgUNas)?H02{Ila7IF99$?XzihCf^553l>>GXWy{?+|7t)(A)YY!lhFO7 zJfY!Ft{E4l($_*XC1@5NfP*YNz~Igh9*A5V2glC<7CdMGi<=98)r%!4?Ewt?fZLp} zuTK{fe*5KG(%eGaqG|G|B?sM>$>TEJkSxpPu_g@5hJG#_;oMf{dfhs5ZA60?D~P4| zi#cI5Nluu=M8Sb9N&E@9&H98eor?|1w^)krFr`Ph{5rwIMU+0QKOI+#_lSPhh6eUJ8PTWd!PL8?X zUVTG&$&K|F2RD$WJ5gg@Y`=%vt3Wz{emZdNI3Ylv0T}P}b6lq_&I3FV(!}cNuIl)^ zx~s2LFzrO&O`Tzi4py=-=ia$Dbw(2R<3#JG&N_bFL;m9&jyH3>kmGF}&*G>})z9O2 zlLF4#&sB@y^8@2^n;{9Pi}%e2({WeG$h;c>Z#gSI0Y-aNNsrCC6K+ z=Tb;nr`^GgFgZJ)k@87GTX$Z|I|ul~dE(AUKm%Z%+cf8@3zbb0mF};FyqTVG+QR`d^1R)jm!-%aZRj@BAOGYhKdFx=&Ge;^`Gs23%#LQHX|db- zT4f&~y5n6e>%mIC`Z>P3WlJQr{?XIze8tZPcacv0wAR@|V6mY460I4s1=E$3N3{Bf zm@s?R^2g^rq{>F=ZJ)R=+nxt0RYLWFqQJxzxE8CDzK*><>dezsrL&OQSS6ic(ztB@ zw(BJR_;j$wIY}dP}*p+JQ9E&y}z@bkRfwhu(l1nW!Y|lc#KM zUD8L0@Qmu%(z;kllxI}OXln&4%+IWj&8-2gH#wtrY-wGj+=er%V^ixwr6ZhC9b>Hv zw9(`l)iK)Y*EXV|Q&Zv&ft#cfDKLh^Q zg6(IMdxK#=t@;wsKckL}wibipv7>@5%zt9+lXcGMr%$kj%Y$D(ApLous#)7ikpcv( zd9eDdEXwUti$QY*QcD|8@%CY|uH?Hjx9W(6zePvEQyip#{Hkm`d1O&K zAg7sEna8Uuk3vHFRjhlCHnnC^epzu%@Va3rL1urBoh6*P-DSWl29#K6CUDgOTnz*_ z2_Vr+fYWL-vK`PI%h_ya)HC3ov8!4Q?J{})$M5r~GLm?bZN#z+2}qo>={-zdR}U7K zdcefE@N=T3v_S~ch^}FkPOERFo&bLcTUyO>0>vF*_vlz#nfTwSTtN-TJOJzfVUz|NyI%{+*2iq}v5}8{#Yp%p-SOYDX9Qq${dAsKq6fIy zdEs(89n7n=e{2!1g@CNSYdd|k7B4h?$7kO08Si(r^I+02z;S^8 zO%4`F?Vg>OlW{WS3}QZslabIG@r;)wB`4#Eln747VIASDLpnkcysx@;)aEbSvNAtK zwfh4{imT6sbAa%TqH~UP&*f$m9gFGBZ*Jd{c>-P0jR=D)&_?0PA5nTAZic;LaJ-E_ zJrKN5`-@0To~Y{0cNX)H+-N1Yxuwg7Nsn2{t)-G9R)Y7Pk7Rn-O4#PED8U7x=9_vY z8tof280m#hgK7>cOqa7~k1Y#kZwL! z!Gr`TxCOElC3~#|(iJ6lSPA4UN?>E2VhD=yb%D%>HDGcL^4wEN3WSq7L(EGY;!?JT&h#*GsEIEhKnN9#aVC>!2$$%tqB*?v zStCm9;FY&jm@DCUv#r{)SWHG)TH39OoD81GsewLh%kA}%DOKs1Kvb1%m`5u)XlMS*pgPG$vA@CIjaC;Uv| z_)z_Iv|75Cxg-F32kZ_5ELVXzE-mrZLCUqp$pPt zw%iFwEuFakKCk^xb>6S5TR)IrF&tR7P`n)&&DTB&2`uZN~BDf41k`R zO_4HjD>n6MNpycg0X6SKMy{Y_>7>y#te|zUSwvq8;D@=7cL^87*07K$v}3YxRa{;Z=%DAK(eYG@l+w2pA3OlNmmld(cDGA`gK~1 zkY-Pv5XhLyW3t>c@L1D$OkAK)FbnbBznsV7oX6tnJk}78H5`wzc0J=Ujj`LFfiuv? z>F^jBoz}A%3M1iojOf4Evc=-cMT&neV%E4xP=$6yF47G%I>v*QBeu^XM0rkniL+SR z2MHFv#NA+O77NWlF`|XLz?b&qO27(NUNT~K1v9S(Tq0UV=|$t|*KT>xW8Qk$(=~G> zU*tOTAR-yS&z7$-fd5dw#sGe_eEp%`1>B{1I&vu4w4$dVpq8oIe$nD!wl%*~6emn; zKHk%snf*V3X3bDq>g@mVQppi}>R74du$3Gsl^nE^!@Uytlb2^Mn=@7ixQ7iDu;A(P z_Sa?GFw1>iA9m!Vt=*?wS%q<=gwNIJz$}KiPL_+ms!CIcAvGvp(o{HqOQt|NoD!Lf zyi+}sl9%+%PF^CbVS1u?3O!EAu3kOrl(Quc!eTvs@vl~&F$rdwa!u%f@?F@StT$2* zdw~!M;!LCO3}25}P(B|jmVD)(?U`e%n9kRL<~w!wFL?=boStQdJBx6iM^midUFrD>N~vYP33FE z{)qzxF4lZr`dIn;(cZ;b&eun~SlUU0du>-7)7{ESS9n8eYA?q^PwmkKifXrxD@T|) zfiswty@_K=njs^^CJPF6&;v3cFe>T27~)J(S&Qx}U*FlgaG?w{u?8uK9|&SY2dP54 z5;+@UP9U}>21PeBdAUGol+8P%uAdKDoLx-Vhs;UT}^XC@l3MV z4jmRMVTc~-QQ3y56x%U^lwvzZkWy@i$F$#XI2TLR4Y=y*=Kxao$tD-rc*H#`vXS zTRKSuxMEZ%Grk+hGQ&R%EZB^;Q$jzA5`>l#`dXCiwh~%flt2~kfkJtS z3>uyrnD;*NiJxI5)P=_{z#UpKupOnNPHVHCN`SbV+{V;A#1%r8ypbvcWM!1E&`fOD=E! z5a6KsT-k$;PLKNt%TjaQIx_2Q>@Rbn6~mdo0$)eC|Ays$~o>-P+*`6pE?Z z>@NN&EJ4~G8TCNwzJh+*%oE>6L70N1Xl`)iUim0X&Bc^K&tUDuB!EIOs_XrwV zP8mKaVPBIMt_y#RMokUj#tN_E+AUZmY!VA@>Sp#6*)iC(A0*57ZJ+8XXofl6HwD?=QN9M*?k!*M z>0K}gCy(et5JhxFy~9%@x?LC1HMm(xx$>YKTH(KJL^o%V=@DJRGW1YqJ{`psf2K};QB)N&o@3Ho+$6Ti`HAnq5}?niX{2k?@vK@A>{6IxLvD;EJQ^N z);{ZL5h|_Lkum3Y1uJZaZC$WumcdR%%LvITE}KaVK^$EV7@KNHu_j#eK~ObJ*H}O? zTyc%|UtV1B4)rI$cD>*+w}Qt37@!NiDnlor3nh2pSLBG`XO1H@nvYdwR~e{*;Bg3S z;VoD@k4}eEqtl^t`DtB7!%)O1a24IM^;!k2vI59q!SLb{NK$|*?%?I@*kSX*dqM|A zZe_8Cnz1)@ae+I+UU>Qd%Af>__M;54@bvjTzNshYi{fFz+~(x)G--rlVh#dzWQV_6cKF$78P<}+uMeR%HG?^hdKXJ!V9}rXYA2fH z!qWzKO5y3c@L1PjElhHL(kO+enE@_`rw0P4wKSuir6EF1EHS1fBGiLUIocp0 z+~o-M1gYeLbts6^fVv2^W=Hj;Z%)Hc?d1=PXvGc0}FrJ%hnV&QB z{OQb0+UT0pGGEQL%DI0eP|6G6A>N*{NTIt%#dM%)1nS9a?Ri32D-YR{D( zbpnd5J5;-$UtG2K<}XtlDWb*OjHm?gZQokmr-Y2`MOnv~=-ArDc_qz*a9m$9k1a!; zuc9;}C663nmMBXexmTmd)xG&<8@=R_2bf{<({D{l9x+V&^K`csUezMmBw5HT3?#Ad zScvi>ONJ$T!%k zjq(7OBdQH|K}HTXS`pmFsX6+$xNH--7M5*7mWgGXOcYsH+Sl0f=v$LB zkSk;$&z;Yds5WN`w(_UQtFz*}&s&6{E7A!)v7bekG(-35AEktJ#S>PLOo<$}2PPA#HhG`jc z)h&8ZQWVDMnVx{3^M46(;p_PAZ0GP-o=MJ>x%y{M$=1?$UZ-Yj@jJtJeUTPGudTd?X$a?WIdqLx^zFGSF(LiNj?aaGIIECdT{W6%k~%4S`) zKREkZm4H%_>~+vSMvBd7cjiHm2t(4?AgTOroc-kNUW%ISWiP~F z9z=oL;P{_;iW;vt$ZFV6vl$0e2lQlGf6pnjJ}fV-QP%pm6v`Zclm+yf;ylOAnJR}- zXNaL>*>!j+w$8@V>hjo0lw3^K9d`L*jZT(GhuuAaTayqwIj$pERQbAES!(JkTgYRx zy3)K_R&GHMi+T_x;VO@MOkm?(>cW#Hxhd+?41DH<=EcI2E($7|+1}(-QV`7v^8q|! zRuAT;!m<#_aWpMRM5!SZjG`OTxVeavMW771U%|%9D3+-JzLf)bB1{7CP+b5X76q&c zz~cI<+Wi1n5WI*<7?SKm2~C!9A`mY{=rFsoAWjFfHb_cH$b?X$#RtbP-k`ODPYH02}Wtk9E`@s7W~Y`!UFp!llhkoeP{#u6~e=6O%8m>tj8tM)E7X59pG5R zC-}iekK}e;#hktFwn^}Y&e&5TgQe`|5eVn8G_XlNyxq6dR1vo=F_eB%cVMa}T5N=q z?a;Hl!l89c$5i~e0tdtagyVs}?tQwj^OtKO#XQbB=CCufjt+M6k(bod_gd(^W;4V< z6zFma3oUZq4+1%3YSr|WpS|?G$*EO{pShF`y^--_Uk|yI%?ur#Bz=zt^>ww*mlJu* z4m!6g7?fTIwXCqfaTqG9}U6QQ29)Zq`9TsuXHHR z=71)dG-IUf6(nOhKEisD#lRA3DYW)Ts7yaoUjssfT>`~k;<*@`i%N!qyWE>}(YE@n zpXj08C8M^KiqNu4IG|TA~~aT0)M6%g0}gfu(19rL({a8Ir`GAG@-KGOseNN;_Ap57S{2tM&Zer2B{cH2)WUN^{t>I|c& zQPIXuVWOc>G?!$sHxz-x9L1mcF(nH_ZRLwSTWLb@3|myaz+a{ndi}NYyPbLzgYC~- z?%}s#42X9R@|1KZD)MFra?qCa@K=U8WxWU@X`2r$0FTXO{bvwUkv%clG%ScW6 zL7BVDZ*O)`?gWXAUvoB)_2!d|I~*?u@4E6Q{>zeOS0=AWYMirb_GMQ_uegcng|sL!yFe5&h^j&*%2v03#VU59X5rR3RldUIRZ#}b^iBrbeO{Mftvb<6 z-=iYiD;Ff8guZ5aw~F-HPMjuNE7BZ*0QZ0KgZe5xdtWs>c<^kD_h8&*m#P&oKZHlf zmvH7k#Z$)7O>S+81R0@}_`;QludWs*-j%UrCliN293*ygyv0O3ZTOJdU|&&|VTYHW z)gfJKAo!o1nz<(RXJS-q&~Kq;I%Mj#zJZfIGTQL7n*PIr1S8+fER_v$0aUE^<)AO!_WBl|>{_oP>Fsu6+U=Z;Ig!?!)1UskF%4qG`t`Gkv_cT1KmXpSTfurv5(#B3EixAJ+1V&WKjKgAnDGAyd) zT}gl)amlj>W%8VLyRg~*1e@%-H^ZP%qy5gBhUriUA8I1KHbb2^Mi;=+4pD=BZ zH1z$gzJ65z5nnb6L8J`@$N!a`yq_f60_t|OQxMxcI%8rosEW~?5lIW z_)=0=Y5x&B)`)wwu0cYP?~ji8H_f0$TC(pL_CET|o-VnbpyhPKy4Y_OP0~Ubx1y1V zgW2Tt<;oPRB;wvxKY68A}C;G+d#g1(U zNXq^52lHf$5NSy7;(CQ!k8I27?kr!E%XnM)n&gXH%h$K`E{IF72-+j;=M-x}fhO0X zP}X!mz1U5F!WRca6f(Rv@j}hdNWBC{P%0z}Xpe16N)~Bt#686!stW5U>5oNth*OTNeGgLI02pcV)J%)jt=&1vH9(r5nE<+E36=| zun#XXw<5ekLla6?`IL#A2O$u>b3S)TVokB`3C$5SlJ-7c8PiAUQ}w_t(~%6KLP}F| za_J|J1m0GyYgCY?X2rOEAC%%rCx`)7VZp2k0L>LaKl}Bd6@+iB_tf5Atw#=qt@)Ir z4A@b)ni8(Ez5L}UnQ8zC8T)nfPg+;Cy;K*tAs+1FMPTVfTc~iDohwa4EV~^9V95&s z(Gl8?>&lJ#l|B~hG|hg>Y3x;@MA3s%V^o0&k%?9Ymp*tSh9bz!cTaUafCl2Ql25N& zOKFR74DjAK66AJHaASBy0C8!I&4ecrTvN~*^X9fswUBy%pfQ%(ClkLKM-A|@l=pc} z$E~6o!val-Usuzk1d|DE(t@L5S6mtw+^53F1MMAlzD`lbo~9(llRSv%l+un6MALui zOZ6!_NJ_-?)DVQtR-H&_oc9!Rj_95a1mT&+(C2ID|@wBW~nA%H^4C?6~dT4r^#j%Qpi`%8&5d|smbZb!b7 zWE>`2NXGXn_I|?n%5MU-VYUM)+)RN8z-4bYs-fpH-1htOs)3jdH5Ln$%_yO+6BuuW z_sMX?6B7*T)ocWpSwt^gp@Q#}3OXwITB+b{6?~yoFi!=a=@ld_vy*`Vs^kVn@E!1b z9r?TJi$qTmJT{V%mo5E_%4n~~ljH{FHZ63i4vrTfKASSpp#fe5ukv&WS>}38;6|(Z z9iQdIo$yssCigBYL7U|R;B?tY_C{vYG&^O7zl@^tFdm^rDTq^0FIy8hJ;dhB zS3}O~({`wf^-Dcw90>iQ)B(^DpPr^}>8=i}CTw$&!#1`%X`G%np6+^W0ZU?_MEa8Q zHFEjyy>*OA2TcE^eErShB9NtxZQ2JzhKGTAv;b)mbUYXbLR(xEHgf+`h6pVO7|oV7 z*QTf1IQTD*R$CPV-Dmpw<&!XZ^iM?hJzoQ?>JU9{TMjtCkG7~fBgmwKLP zYQXpdTxV(_lx28N`5F#ZMNncA0?YM~&An&% zt0)utV%y*n=#pJ>zJ{ab3`fZ`=_3%r=$PSvsh{F0${>}>!JI9A&rKjf{T|^N!D@QIy7h!X zCg)&P?VA$LaimRp&y z%pn8Ekl0JXPZ^Q|I3hC9&uw`Mm)=)`^7*7G=X#+^eJ}}QJp^0f&jmwOitIt)iF8 zif7{>%7C_AH$N#&cAT=w#^SJ_o6wn#m=K`J9JXfF7OYIqHJjRo29Rcsf2L$G3NlWG z%5fk``CuiFCHLj!qH_N<2qj%exl=!f*l?)~*&A2IT`g%z_jyW&o&`vvXm)nbE}wU>&@kJ8LmPFgbWnOglYo2aod?tVUiLzMRtH z1>ekmlD*5)Rz;O8N;m+1d*W}^9C|&yVk;o7#-8kz_K$2O0XVNj-l*Gd;$T2)Y+6=>Ub}9^?H{SeBx=+g;kcaVTZPM*| z$b-&99+KzrU!dh`-pU!gWLS+sNk|?{U>(C?XL`ew0NIm|W zeHZ*d&S>{2~32_R$PPh*Xa89t^g+N^hEC-pB@@3~# zJRwXa1lQ5;O!X?+BPwQlRj#$qVWcFdD1C`$OBFGqoaonj%CS0;i}+;agjmU4k_C#2 zFOSWlKVff=PktQ60P12eG?AuCOcuVzlEz!nmoUfN7A zcZR;^4E^QV^sh#P;5+W@Ac%v_?e1iw=M`@Hxu&SwUzV>i^KU9&6LouS`5H6->hkr= zdlzDG)|0ykXg_2o*3gT9at$C!O8Z;!84x#fUcZvB6hmjt8Uv`(%gd2E2*B)ti$M^^ zC{HubJPqVRf1cyq-oy*I$X1Ckq|@eFiWom18c+jve>Bf&6xl&wp91h)ZF95I{d!T5(Cg_mmN%1Pp-2RHirI@EP)pdd?E6vb2 z|Iew?JWA#wXHJ#osSY;x^ms3lC1yjr5<^pDvZNFVrWp$=C?4jf4{$U^9U$rigraEmV6HAZ^PoTP9Exfbvm#DdV{KUuWe#MBd zwKj8llgL`H{43T&I+W*}uz;YKbHe)GEcuJ@MpBnMk6E{5t(cfejZJ-(rPWXtl~#ao zLbf}dsM%>Y5GOV(ju$GWoa$u_lQLmE;~z*3oA+P zV|%r}Z%liKTr$tEY2J+Gd}NAOZN93?v@I_X0LiV$K#s*M<0w(^N4q&$;!}?2>%7gJ z3HCcmu>`jhN@w@9_WPJT)Cqhzr)}Wk+^(5i93i&0ErFe1BWe2;W1)$s?u08rRl~} zbDO=f)b<1USZ15$OiMDec~RYH)1t-h5}wAASGlnyR;K-vYHl!D$(F9j0$Mz1R^L_& zdCJcu9eS2&=M|IUS}R;!Yk+NC!LXOQX=h)6_z84l29Pc8^;r2D-T7$w8bJI=`TA(@ zBFk+l!qD_f*nl4R=?a5uX|^arG!WD$Yfiy7K@VFU;itQDKg1qBmG@vHU*zGbGYn+@ ze%7UHX*%pU;Igk-8zNsx`G*5;)sKCQI$!mJNV4ijiS~eQg8G`0XNd7qqnQ@>1k7aW zGjmC-*e9MInWd}{KiRDlfgCFBmeX5L#1a$xLTB|#GEb??u6OZ%T~Ba(&$YB<9Vc4$ zwgVRN|5cf1)i9M@&nwgFP9pu!)h$E-hJ$NWI@pa8(z!fKE|?R-4k6Oc-WoT`RMVQ&M<{JUyURM3rIebN@2)d6P+MfR z3oH{6VzZ`F9+=Dkl|&O;PPOZVX10=GYg+vSOtKPQJArGZZ5>qSXIY614(M^><)vg9 z0&sl6mbnt;=?Hfz19~pIespWfhRU|Q%4+tb4N)LGDP9U)c*z`FnWnvoWJs-JvOvM_6<#5hxcEK`KQUGWdlYKYFf#}o zvPCcbd}gyJoEmSXj?f8CZAH$npk9L+%X-cB6obc*FVkTP0ro_+7XmB<)#)`susi_? zdbycAF%m2(wl|^G&XebgmTY`s`P}SPqYnd9ax*#CCYJ1rvp@yx_P|;QZ8y<93ns-L zK8xIR4pY|7)U|fTHh?(U+8N6mf<47a*h0(1f0hzm1~*2p*smUdXm{~P?@77=7doTd2+FT16? zO&RgVGuf-9>C884@wGA+X&zwbI}QmCbLj7YjCt^iZl4ae!^QmGM^yTdGaj3#E$W`m zW>byf;I}xS3D|nd?O-uaQnG}?v$QUY?YtCw=BvS4qA*Xw#TVKnL2N3Vng;Qeor$(X zQMqk;IoP*T+P!b57|nW7T3 zqJ{26ukoR3DMV&9I2gaou@x3f(`u2?9F!=u$`@K#>=DJLrHA}vO7i!jL&)SMAdXlet$!zyP=Z9K6mJtC*!Aa@WpRknh8LNbeP6XtTN zV5+^n-Rh2PhR8LC&72Kd9o2@Xbnl2ZI^{@X`iyRz9}Wdp=&3eY;G5u8=^eJfv!%+% zS>}n_v*k6?j!3&`h|6zkS4$BGggRtHLe?2`n#@gFw(6UvhSJkYK3|tFLFMV8tTvX9 zTWy0vFjC^G&E^eUZMK|za&4r=s5Zb|4P_xxXqB5(g%;)5kb*F7CJ5r zD|OI}O)bdu@;m!uQqdmm{Ni>z_>O}cy{(DwSw z&A%@w+Orq-ZnGC=NqaT}tcj_jd-={t!nSk(vSiRy_?CjhMS=1>%&v$dhPq}kRvLt6 z7R$^73~Bqn0YA9~tkyFT?C%Pv0F(<8fz6ZsSBm#=Kv077i1{OSRA8_kVWSBhj|Es> zjz@#eoLufFu@k>N9PmDlhgmyd?>#N{B3z?<`6=urk=O$xNF-QcX(A!Ji@eQ)1; zyZd(c?H|+KGt(oz_iBu1uw-i;M-sM+%&S7O4KkQ5b}L)8Re|8$a`m`UXe^BXFwp>G zm;}L2HZmDDA{iEWnGm@$8$|6GWZW{so)|4Vn-yfXRtu9w1(Tp;XDKDj27&kU{ho8) z_r2Y>)iXw5uY$(z_nvd#`#$G=&U2pM=Q$=AmcI%G=+|2OTGbzB=y&Tek|GfR^05ZM zSQ+550JBrYsZ2)58_s%;4qf3-dvTd`syFrh%B6!>y{EEaBitgrl#bJ&Gx=fDuW;6A z|HHNNC_veMH_8?>0}Z)Qt!BsTfa0clAbv&nurW#UOB+<>DaxdBqJhb9W+!@}jI8)u z$v*~_MyHvDi#%nBrd{If*|=v6s?m73gt4fS=;6k~Y|$%@3}hod{k^SWsGW1U6Km(7 zP4h-&qM@AwwK_bq#rZ#JA#$nU_4nWI8Wx%pG9)Az^EK3ZXdY_M;361gZv@SK{P(% zXetEoRW(&hb4uBj${LWg;_QO~)wu1Ph7sF`pn3({3zW;3K=NJOXx7-dfyc>z&gl@B zZo~~dRA%94q7l_-i3BQ%0ds6=0}tso+DP8DAn!oM6jF#Of4D%Ik8WTc$|cvCLV6zL z$r_+4$|k$L<@rF~rU@sBfqlsr&4S~`V=I2r25qX|ow8r7f-w7e=A2WE8{r+gCE=6> zb}9ORNHb*u8S95bZEC)HPC^d^G1jG!a!MtrT-9POp=x}Z$Z)Z0urX-+N|<_Q zgA)fTE%2%uBxz}R+`n>f>oQld#pk&eRJ&k-#I@3T*;A@=xaj^Jwk$#dHsqV)4iQKE z_^3`iKcjWX9FBB1E6Q_trn6Z*&+v*R&X9t*I`20J&~jxnaClfY_Aqj8c$fwuC6ble zDt=23Sk?oeyEB;p zgy#pJphKX~fJH%N5t|4va+<8vC5P)+^idVH;S9Rd3EvP`;_RTe^*-1~^ZnX<7J5l) zGY@MPEpp3M339&OCbbO*h?Jp%0u2x+nQW37y>FkJQO!<&gVSp!Z;{KEXZ-oDxoL$$ z{ms;Sh_JP^VL0Ot4Tp)mli5r@nE}@$lPC6&x~}1nzFiDQMI2%$;PvS{nV2)MJ2|of zr#nrnysJ1z%hD-5>%2QkFElUe=bRKCp(!4DR)opJzEk5~A1(=KyD(f}3Df*z@_S zhmbZ1UUMIOfVYkkp}6Y4mqQ<_05nxxao%+%do@X>g?VF@lg( z%fOqu2&Bi+$0>XyNjjY2`W%it>FLa?JOq02;W`y%=w%jCt3x{fW}jDsDBuFEh180~ zo1KU604VH3#%|XW?`fS6E!55O>kZOe4(VSItbMrKqU+lnR=@ zB2YJY@>KCM&q$#+c#QjAqHno;85SP*y+~h4KLyK^z8B~VucOyw8T0pf`ew@CvasmW z!5fRIPqg<;(_(+WLN95P*@>R{Tc2jgIv{*V`m!{A#`Z*{Z=pTft|=#phvpap^oP zItH+Mtwl1G}2NqoQ855^1!sGNV_D98@tVmTH+oq?55|A2Er+v76Fg zGTtO&%?TPd`hpBpwh7+{lJxy(vNair!tP&EWlYEuzraZ0(nh1=(u2P>^Xb3bYTILY zM(2#Z7pU(hsr%}d;G^_$!We9aBo0sy*2RD}C|kv4B{;lR(&1pN86Vq1oeOa%X^HRF zO5u%#8+1Y8CaV>pGN)!#!V7Fp3HACUF)JlFsytFEXoChgzrRO|6K={jXcF$qHfR)X ztuPPi_gAXmUbId#F$YO;GMh(fz?S1b2z5Qd}`9z~LgKKhzv_+n5M z+yM*=zepi4%jmv!`8~n`(h^Q*VMC!=G27BaA!||Re;j{i^g-bhg*v43m$Dk|p=78L zpY2&R>7va{e?r@XwDyYDgL6|dBr!=iP6wZv9FZ$lG5oYSRqUGM&ahi3kHH4 z5p{*|0$(c=z=hkCPj?WXKlskscd68;e2H?4@p_W;v4WY&3*xQRihhpthap!~qY)2% zyc23HK9A0`K^xVNsASNE1oR2Mzk@3#x@$AC)xHol?DORT^E#4C&-S2EPT#Gd5CMmB;vyrt^#)<%h%$>}X;t4{2`g;HLqNqNku0 z?fx&Qg{m?xH4x2wrug88IctU-#X_4v>3ymdl*9}8%y|>sS9w>ZF3XC(omkNqUT?k4 z?s6t0vu;OKw$^4b2pK9o?xY+6sB&0XgoqUsLLmg9U}Bw~+5!t^(WMrTVQoD$>R@*> zQG9+cG%Ow}K2Z5RYL*CJAdxU!h67$j8FoMS8$b6Ojc=Kw&PqNA#?6fAmm3GeHB22u zQh5*ywKpLKeu$ysYRBDtAUD040Uok7hm*k7=IDTsZ(jgI5t}|Bj&i1>b=H;gs!YqW zBx2CwJ952hc3}G)HBKZ+A%ar!)BB=&iCQs}L6Xt7A(dST+HP=~l)C5@bYW2YG^e8E zU`YbWSPHWD*0F@eus4=K&Yvrmh~`be5>d3BSi0^6r4l{Yg&uiekmmhKs`{2~r*Wh4 zqWi{;#tZHnHyVT8HEuKp>ulU;3|8DOMkB7G*UxB#tmjJEH-~6}#aLrcmGb6b5~Bfq zTvCp~9E3btqmN4xmC!plTT3)poTW7=Tjyw1^f~8#E8GFmrbi)LlzC|?Sw(MPRKsS3 z1eP{Op~X*RC&f>}xU7(g_^lyR2mNeECU&Pej_Z1!#{8eDtRHD6s3@aew12RX4otLw zlL%SwaTk^Cf+LL7!n=mqXtb^*tXx zlkj;@>$yzE6EY(jP0wJk$wD!+i$)X9$I+5W1qLXuGk?h}4P=PA12c3VF36g36^tgl zkTv5f7)|&pD&tB_@-eooU78}v^B8_tat&<*U_^4CbPU9ADPhoZJU66_GG?8!M`Zg@C?TvCZ>x55py9ec;?xjFLka1ZiA z2L?x~knmSfdj*NsKEdej*6%x&&4VhXG%=zd=_f5TeGF^uHH{CE4v7V*vm(kKhO}DJ zo?d!1!wsmBfkJtW@QL*aUivzdq9#i9^2m8xK?BZV&Uq)CT5&QLY22BfgmA{S6(6fC zyp7aYrGj*THbZy$$}(h(9?9Qeq6x7z^Br*hp(du9i=X^gwYfka;wMszY;sDevc^k? z&S(iHXPO305RihB#Uuz1<0A^01uM9Ug^hKeAW$h55gRrQH-rsM6Avg6qDi7%;zUzK zyF`nX{2FGgw$PN^ti%&#R$my6ssd2_M8{;Tqaf?nh zWyg*yiC0d}w7hMId0m5E8)At8+S^%~Xc%IZiH0FonrLzDwvy`;T{0MAFuwQ+h7?RP zI1eANAA(*h-gPDE#zB+49-H>%C>e?z$#UJp`t+7$WQ8f_R6^%F?s}U5)Sg)W%4&sv zwH_CS)U@mpi9A=G*l@|LOv6_8ks|#;Csl^2eBeKb{U)S4y2^wyRRATuk{wqds~d-vC#}P*hT_jw*e4gN0PS>V|JQ z?HJUhDS3)((=bO6C#YQLyd?r&MnRQQzP=Ly>*EL@RpK@y05s^BlgS8J4+wY;wXB=p zRw3X`qF%ggoiy4viM*cMLo3(m8zmM(d~c^#F4e9Q)KM!x)6t1wygwRv<&IusUaf`) zv0xK-k~^9&qOroBer&FiFnTeg^i_d~pg+GzUloX8Ap_(E`pT8-1R+=?ea3~JXC|ro{Wy*$2OVDJ_A2Nw z7lICpG3d}&&>?Ont%45E5GS}R>;`qv;pr-F;Gn}(^)^9=C+ls34o}qEk5`SrHLbkL z&ptWmK#Hb<4wyO$I(W}Pr#Lzmbf9Gye5Kxe%?2I*!*6fN8oAY=gIuDx_`jxu4s*ft ztfgBIbXd(U0~Jxtt_3pwtb-0=TXE2J(^9mqn>dMe-AcVB=uoM*=(?qPi+xh^99A02 z2nJ%(b)_nucDzA3B(sxrRhA^GL_N$fnh<74`2o9x8TQFQOo0eYqJ5}yWgCrYpY*=; zy}}IacCz)Z%rOzew!wfdN>{AMpjZ_N8??bR2viF|0 zyZPg&<6uDs?#a(T8_x~HI~(Ez$^)FxjiMaB@8;{_16ok{PBc{R-F=omsAKM(@-CvF@)ux=vqqsW^mP$d8bM$P>GZNl6PIL#`n+{&N ze0tc+_Yh+RQy=FcnYXzUgvbdOv`Iy^+F3U#jMSPPHug)Ibi=PxmmXj@PkkM=oP`q- zpF71V#K_=ZbQO03Rj>SVY=yy`pGsjasS_d0#bckD(mB5<=q84XQ==JY#m=xalog8+ z7yc=x#amJ$EM~=aiyu_R2$^M58UHnb8urGS*%5d^>-X^sa>0x^5fKN4vSL%k4o6bN z`fM6hc~fZC?7Y?OjP9aDc3BgXj??8^h_jS$@x<6$oc&sOi^r?CIJ^BVu47N7HHUjG zthrYRhlRv)o7CQkWs|u@c*?lf;*42Ead}QKZ zW2@dO$Pi$-|=r&=*%niYj^in*ImbOU_(blN1tlWE_u;CpkEQOqsRN zddsG$_A4&2u`kr0sk}ZX6-(s1+#yBYj+97@O%SFYzCZ(4Prf`{pjXzlvFV6g%1kmU zB(>%DCB6ouCepMlS(p$NwCn52FJoUovhX=sla8CQ@SQUUqbQTk0N_|)f}m-k2cLH8 zk&Xb?v-9Jjtn;-{mb1<@Rd3+vN(e!PxM|waW}O0Tnc$PGF3#D;!6K?kd5ChA0eKn% zkKsSCbsfCDfw9J`isO^AVx>bag9>rMWe{Y3a2ce{fL3;dtn)$^E+Ry7%_W~Ui#VtK z5=bV)DL~@yd83}h4q3Sc$^G=<^^^%2y@M$*RC;!5*>q)~wPwVYf!3N4TLxNdMy$MX zWZf~IWW=J4C_yy@ToRYlP6egLwp+D6vv5Kx9R*6zw|c>VMH5!;y;}{k zGQ$3Fj-8jT#Q{o5)ygQ-QF`wGoJ?C`)`^JVPm?tOV5#RwqR$Ws9BG!kZZeK6K2HlGZJ3kRr))qmT zDEH@Tq&nOp=~Y&_Y8J0+GF*z%p%Qpg5vXW*L%H+?JX4CbI9-e8X%<`!BZ)1XWV1Hk zBENE0ynpo5oChCRkbYAOyQ1S+$nhJcu?;aThe$PVh(Bo;{q0O^fE9I(M%o9$Jbe&j zk+i1o%r)HXJ3|dNj9HAAhOJkokg5>sV1q?eNg4{sA=t|fvb|by6GN7?MyXdxYYclP zw`+}6GMdx~XN-l75MR+VtY&>YX^|hRl*mZ~A*WvJrSi_Bv4t&kh_sLK#<4aZEz;xI zB1c#2eL8nx$`$14&+Glt6kS|idkhpK9AV^hkpq&Zvw5*Tb-*TaBu)TYpXF_j4iKua zW|Y6ixKzTj&6H@;V>=5bNUBTPZ#TMsg)^SwrsYT8q)`&4;19vv5G-YBPz_MpG2uSnJq z@~wgMl}l+4`jj9Zt4Yk-(EcN8758}{Fx#X=YF3A=9VVH)=C+=(MWnqfhQP)u<%?0( zXG)f)N^XX9NL@*X?DHGaH`uDv)?vkz*kRB9iIGzNhuZ7WmiK_PBA%%;^H`>&2`b?k z6VBU;-@~NO;$-r6O>tUj;IzxI_W}*hixKl>=CsN&2@o1&bKHG=G)K^RPWdOM%@{FL z!Jb8nn=)#~wB#EhYPKo{?eH7%W>=fOGW@OjJoKoG7Fk)JVxhTOZ9vs^xk9?hu z{$}y}zrV#>v5u5rBH)8Bca+0=gt_P=>?!GWwAB|hn^7Z1EjdWg`FuEi3U3A|oq0h&;`CVT2yIjavCGQ{@ zCc3g0Hy2{6m$)EByqHRoM);AT@!k24Sqn50il@X2itF!mN2=@tW$eHc;3aP#DMpI zutgH4{|MiRBP@scg`SWD- zsCKxfm~>v%I~m-M3*#6qJ3a@z7%{wH)~LoDV@?eg?^0|u@rc&E*sd>Qm*#xs#YTB& z)lu4!V*+q#Vgl%KK04bC9%m6qLnjgGaSv%$x0-rE%|;-0HXRLq6h{hO%u0({!5=HS zgRS{jTTOdOhBKOY%}>{k5{fs&pa&$fL*Q)Slm-%)G`~!{*tun8s3@e?vG-hs*A2_q zj*ZwsI6|MijFqVZbJlW(W5m@7qofOA9@s&8&JlGm{}FXQewC+J9l{V!U-(!?F~fFo zfrpz7IW9-pF3JJHo5?>&X)el2_E`$;VXx%NgIC}ZP*m$O z`h}t__I6LGzoTP#AP)h`)JDhNb zZ``h>hXBTqCY9v@l1Y^%u!2twF*D#otIx%(3LlzCPMUcj)LBoJh(@`;<=l$*E7Kr# z5Vm@4qIpBI^gcVi+bV)Fz7oRp)R%&ab4*+d_c8x+XSNH%?_o7SxBAE`X1l4VzLb$K zQUXhKjY*Eo<0k)VfZ5bzGS*?@p*E=2`DFg`w`=@Gq(Fji(2ze7q;O(3BWS|x=E#SK zO_rAYvp@g4&+q>h>h|#Hi%qF^;!{KAA9Xke+h?1RP2}^53u%q0*;=7!W313wZJO~y zWuZ|Tls|t13gghX?BU@IU$qD);T_Gp!=~Q3S-kq?FMpZivI+CCNCmvo z!;T$K5A(9skeHSZ7Bq;uHWGon-9a9FL6H)#?wg9P7q@Z;7A<6kZp2(O}sbg1QkVo zst+Rfu-(6t3tOB1OK4rf2#V)jP5sn432K5=2=uojA;6A$m zRfP+>sGerF-(-~hz6$6%y%;fR#ZiJm@5mhzWG$N+!Ur8%-=U*H2b4@NlFY~#fT#^H z6Ix=ydJGKXjiuI7@s|t)mF6M_Z&R_-L`s*UH4zpA7NeCFakbY=6JZ(gG+8D|714Sa z*G05gCNkg;-Lx_?1((`W8DSlL6_gPY!>o+Fatq4Hhqfytuhf+h@D;HcwGmKSc9{IZ zdf5>K9+_Sx`G;6NEL*$q{z4xS@%=fNm`W*TU76PF>j9y#Z9O0eCa;In%~B&diEGyS zIcBLoPS8kBl9wxuMAC^jv`WIyUKf?*hi_ITL6yE`m4tQr7b;0oA5b%r`oOAA>cd~C zB->RIY7AK?p~jGvlBY!aK`UYRc~}5nu~wpR$X81%G23{2SyPsUMFuj})uPZW$dTWy$B7-6>Frky+1%yEfe z{Mc|oE5|1i{uSs{$lKS<8dIVAp^eBEmQ>0bbHE3?3}=M0#xg72cnpJ$VE^HC%?)CQ zVNWAdAIpnJ-U$31!^ZZ-lcki)Y-h?Pp)-CVZ$yhVnZ=aLaCo^_!)}YG zR=&XnjVVY6bny--uI=q3SbQ3it$nH$g2OBZcR* z`0pZRPv5t1qbgK6M!(8Rr!2wk|PF6A*M6#AR|+l9|Q;4 z)HVm1%i=-iO;EJMQ}Y)NvfzKaMF$z%zT_Zte1&=SybwnSGdxS?RM!dI8ME7&gmzNI z`SmsjSyv7sJIF*xkaxFqkSUro<{&dMB?lP`L{*JSE&>mc+xB^kev#=VUD>Df99QY? z7g<3(jF<)Opq)~VQ7eQK(&aNlP$2Rq`pix$(zw%SrZD`Uk9JCY50~4x&n$So9s9F8 zuI6WJD0m@t4Huy#DVHRJC34|sN!^P@qwIWIlYnhgIh0EehbNal8>fxE7kJMyzu|cr zMyXtunse$B5x#G$7@v;r8R8RSX*vO4<;CGMEe^0KPZo*=GvyCc8pI*4N`v6UoZ<SCS4&Ipdy@?r>!(*+kQ4kv$$ylay6*BGz#iVd=o#bOwd zvp3c&=Vz1HKMeK?N(MH?gpMoy}O$;$J?ATL6y6c~g^fWk=z5;7rp z0cj}`tWE2tIpvNxG;lDSowhkfu%0xr@D$^pJ*n1rw{IZIZz%z_u1p^j4-6C%4?*aU@WJTRyYovFf& zmsNp6!OA18CzuLd&$UuDF1})H5Ap*@A4XJF;Zi9vM^`t}nJCZ%e)suE5x}JtG|MnJ z$l>1X27t_Kv|yAAkCx*M5Ws4CLP`QDL&!)y=-3YZH%NL1GZ0@>r`~Db3GdFniyG?Yolt{CaKs114y6DGjbbUip~9xNIvUJ`;AydfDS zJ70}BU~8oGJbrz#wy~ul`U<2Lcb~)&LxJDRE!VazAgy>1Mof0 zvRQ}Q#2#A;;#wusz}L%T(kAb9iGavF4*8R5W{aC7rWomn>PPEKMH#fDi%B{DqMtO8Exa`w%Jw=mIU z?p)PcnDVRfL)<1h#-z)u*%+{E$l6>g;(|Ej1x9!PP#+?tL6}kmsE$2qr43Cqj+

    l0zi+GAWlt79;+8&UgC#2B}GUyFi^no1uLLU8~fc~g~0jPyu%VyZlw8t=?O7yk3(^u+u!aq6VTVavFAg$UKst~UNk?)r=|noi6>eCH z;at9q)Fi#g(7? z3%e19J=lwIL?9ATh(-+dAr^6n$9^0@0upf$hj184ID%xP;3!gY49Ag%bY$QJP9hUo zIEB;5Mh9Ip)Ixmau{~Y+wsJcwsqKz#BeT319eO6;{I^YY>37Scmlp#0CUmBQ_xz zA=r#9*oshWgK`wFS*V~RIzbhk(FI+h26c3U2D+mcdZQ2eq96KW00v?Z24e_@Vi<-) z6I#&52!VhZ@fVD8^{Ungw zfFNu{2sUF2wjvbUupK+F6T7e*Vc3Jc2uB1W5rt^PU?1i)pICAZ8AlqE@uV%epR^_u zNO4SJBI!pSBzKU9NCWyi4wFeZf@GxNC{l3@$B~9~WZ(o&A`@9Sg-tk(6-+mqT*rFL zA#-sCXK@Z@Tz{UtfQ!h(CFJ8W3UCEiaSesIjvKg%TeyvF62422AJP67+Z~jljSd(A z73iQNbkPYTp^8!HjM3-XW)G!9>7>jNg2MvrzcT7MJOhiviLN824Zy2Btrl2pT zq93NAKc-^;+(v?Nm04DvTEDhAON)F|>dEs2yY)Im8Tf76mkQQXMi zv?Njr=#Cobftu(EMf5@~^hRy;K^^o(UGzge^hbRRKm!a!LkvPA3`S!NK@$u`Qw&2h z3`cWlq6M_j6542m5oiq^w1F<#VkFvO6qGO;?V$%{=%WM1Km}vb5#!JaCaK}z~U>7{G8w(MJ#n_7_2**-HU>PFeg(xgXG*%!6 z-q;5p#9}4l;EQ-Sb}|#7S9of7l_A8?8hq{z-uJn4HEGd2k{OQ>#)sY5*A=G zoL~TFOo0og!WGlthUsv}40vEBJTVIkF&m382a92dC76q)Fv2p-gBOgk941%+Q+UG+ zJ}}2h>~79>gfN`M9%N!KvJj3_h`?z?A{$Z2K{Rp^gEQENvxvnx#Nj;RaRK{r5eJZm z1YANQ@^KKCaR>!Cj4MdORUE-JB%=^1xQ?T^fmGbYG2Fs&+(sITkd8aZz+GtCv(BIe zV`#$!BVY<0m_Zlj7>W591q+OZCG=ngeOO}*Y%mtK7zaCyhdn010TbbfNmzi%aDoAx zF$FG|3Rg^n8>Yh@GvI-l@Wd=E#B40W94v+*mS8TH!U)T-8^;ia_s}lk%0)D zKqO8g3YmyT7GiJ;`*0eu$VMD;5RY8!#~B>JStQ^b5^){}aRG;L5r>h7BwWG~4?R&I zz0d%?(GY#m2z}8Q{m=yc(G&yF3|gU|wl(Go+@3PaHv!_Wr9(H5F$2Q4T;8|^Uy zN$i@58n(M6-*%+#@&5_j$4NXuCLSUSk8ld2&+arHBO6bU15vvzYPd!1wy5D2wcDbG z`#H|x19#0qUa>8lnjrqZyi_1$Y}NtKn`*wn1yOLtC_m5;{N`9iakM zbV3(&h8nt}8`RMq8t4f<9zt)j7y6d%*HGjVh)Tj7si+eQ<%UUX0X6~SiurD zu!bFM;Q;ZnUI0fp!wIf%fjiu=5T00!MOca@SOx>un-?i+SG~y zV0^?@Y(XeQO|1A1C%((=B$a4~k-M=Mdk}$eL?a6Eh(iJn;4lv12$FCVDM-U{WZ)A{ z;}o(hGMCK3nTk9|p2hi!yhvWaCFJ2U@^J+PxQ45^jzZkT4cx{p6k#3i;tuZP9vH$AR3=^`bA^ zp39xc9kj(U3%f|s7cTn5y~s$~D@f4?<4wlU_9I1~&nj|1?bW0X{Ke(8*N_Lv08$r0 zNTx0NhD0Bc=ra<1N4k9GiJERvYc6WeMXkB0Id_MsITtm}qPAJoG>h71QPV7Hn*$*u zrCC;k?#u6@23^#miyHLR5H;wc7Ciy7+)mV-i`sKha~=dyb1rJnMa{XWJr_0SqV`L^QeGs+hq6S^m*o#_wQDZM^?M02fsI|WY z@i_A#mj5cmbgw}yPa#C@`VCyiEr{CnBHYGZ+`)a^gQ!^-wd<$VD?(Ez>B5WUd| zeb5+v(FFa_6#dZ*1JE1;(E@|e5`)nSL(m#S(FViN7Q@jFnoxok+Cv-47=aGZfeLid z5hKwFqo9h>=nOq{fj+uo4Ad|d>KKP^7!M6hKzB?88*W+6zJ#0%X&68TQy`0}ki#^{ zV>%Qt12r%cH8Bf{n2lPPgW51e9n3{t7@;1FF%PCNfjP`zf%&k4C2U{~JJ`Yj_E-Q% zIKv69aDhAA;0X^b!a^*;Vl2Z_EQc4?z#nT7fc0314G6?W1R)rkuo)rPiY?fNQ0%~V z?7~ijVK??-4soW&WO$2nZY1?1rb@^J|TxQwf~f<1X&tKJMWm9-tVH z@C1)hf~R!M~|)UJz~by2%6YSu;Vx~N$fwd!M~|)UJz~by2%+3{kso0#Une z3Q@am22s0i4pF;4AEI{M0-|6*M+EA9|=*jJ_@2{UDU3NnsrgT zE^5|A?YgL07q#o6W?j^-i<)&&yDn&)Ut{iR#D3;YFI@rtEgcWwXC9sRn)SI8dg!uDr#6oEvu+u z6}7CQhE>$EiW*i?%PMMEMJ=nSVHLHkqJ~w}vWgm3QOhc7SVb+Xs9_bgtfGch)Ut{i zR#D3;YFI@rtEgcWwXC9sRn)SI8dg!uDr#6oEvu+u6}7CQhE>$EiW*i?%PMMEMJ=nS zVHLHkqJ~w}vWgm3QOhc7SVb+Xs9_bgtfGch)Ut{iR#D3;YFI@rtEgcWwXC9sRn)SI z8dg!uDr#6oEvu+u6}7CQhE>$EiW*i?%PMMEMJ=nSVHLHkqJ~w}vWgm3QOhc7jYZ9| zs5KTf$D-C))EtXiV^MP~YK=wBv8Xi`HOF4ury;M;jeNr>LEHFj6oQRAsCKf(1IpLKpVQy!6=M`9!6sf^f3-&F#+Q-2@_#} z$(V{Mn2u?fi5ZxUSun&L7-24qF%PCNfjP`zf%&k4C2U{~JJ`Yj_E-Q%IKv69aDhAA z;0X^b!a^*;Vl2Z_EQc4ou>vdM13&m;HCAB_{IM1RSdVqsfIw_S5Q4D@A&}>NW;3Zk zZXs)sTgjSaD5*$pBgNwn!*1-w9z-AhMRE5Er?pw&k(h!U$79*un5l~YE!>r3BJJ#^;rJp zWPNf4*?{yW8+lC8;gWE*ll z*_I3>+mRbcB{GO?Pi`cY$xUPjGMH2$L;eqO?*SIYvi)mMPaZNzP=Z7O$vH}pB%okG z6j5?evJym03_&m;C?WzTP$Z+E2uc zHQim^U0q$>)3bi7z|H`>53D@cEU*e-v%xBY%>g?T?0&FHU=M&*273^!3Rv_@Rt0+) ztQy!{uYyntZu!Ugtz!rhk2U`r*0Bi|ZL$IY_ zjlhEUlrKdV5`AefUN;*3AP@r z71&0w_@1N%tTp6Y!PAVx1-lUJ3$XpbOJEV?Ux9T3`vL4?uphxXgZ%_{ z3D_@SmxBEY)&=ZOu*<+YN<;kvs{v174d4ZY1KvOc-~&VgzCaY<2doABfoNbQunt%S z!~g-ndLR(k00aRWfnZ=05CUulLV;Ky3`hhLfE#jfe*ta+HNY+43~(Db3)}%}fliA-wI5?BC80Sf_XU=bh#I03T2Vn7aX z24()50P27j;0gEv7-PgAtSQ)4 zU{?ZxKmZU71OX91IItFo0^$HRkO0I3TYy9$8At-Q0b79`z;<9KkOHIuslaYv7qACN z2lfIPz&;=o$Of{2{Xh|0iD1dpd07{?g4j!2f%&c5%3Ur0z3wu0zJTU;2F>lyafh;cfd#B z1AyA`T=;H?+VMQFs2x877PaH3AxG^vYRFMLjv8{*j-!Sgwc~|gQ9F(ra@3Bah8(ry zs3Aw~IBLjIJB}K1)Q+Qu9JS-9AxG^vYRFMLUI7-hScDxxZ zYRAukMeTSCSk#WAh8(rys3Aw~IBLjIJAM%?YR6GSj=tgOBaXh|=p$|oSOMr8jy~e( z8;(BW=o^mTgV8q}eZ9DT&mHynM$(Kj4@#L+h#eZ9DT&mHynM$(Kj4@#L+h# zeZ9DT&mHynM$(Kj4@#QgwY0DZ&JM;v{_(MKG8!_h|^eZ$d59DT#lM;v{_(MNm> zkO-h}IQodAZ#ephqi;C+h@)>f`iP@%IQodAZ#ephqi;C+h@)>f`iP@%IQodAZ#eph zqi;C+h|9uvd>LQ{AO|P_^1w_$5l{w{098N*K;LjRKpW5kbO9YeAJ78~0RzAoFak_~ zS%4W}3d{z~0ZYIFum-GvIe-md2iO900eip!ms{vA|{^ z4qyWbKs>MoNCc9BBp?OY0qh2L0egUS0DY#<1N(qXAREX6_5(S@7s2aN52 zF&;3s1IBp3*bW%u0b@I0j0cSEfH590wgbj^z}OBL;{jtkV2lTh?SL^JFt!86c)-{W z7~=tBJ7A0ljO~Ci9x%28#(2Qk4$r`z0*f&oFt!86c)-{W7~=tBJ7A0ljO~Ci9x%28 z#(2Qk4jAJBV>@7s2aN52F&;3s1IBp3*bW%u0b@I0j0cSEfH5A>(a9CQFA!jtgC&Dq z0hR*R4J;L`J6IZ653qEwo?xed^#Ush)*Gx4SRb&$V12=gfb|1Adj#%XfCV4}SOT(u z6(9##12X^{KpvO_C;+yAB47v11ndDNU@o8x%mY*a2S62Y1k`}}fI6@M&;S+!n!qAJ z3vdFofyICh;0)*jO8`A!DWDIa7Qp~OErKE73K#*3R01A4#XawGpcbJ6 z_y*hnz5_RbAHXeO7`P3f76CO0s6{|c0%{RZlYm+T)Fhx50W}GzMLShPngrA$pe6ye2&hRwEdpv1P>Xrs72TTpcWwoKrO;f0JR9I0BRA^0MsJv0#J*v8$c~WI)GXP z)Fhx50W}GzMLWCmpcbJRKrKQE zfLer70JR8Z0BRA+0n{QK2T+S}0zfT71%O(FN&vM8CjrzVpe6ye2&hRwEdpv1P>XShPngrA$pe6ye2&hRwEdpv1P>bLLpcVl&38+OtO#*5WP?LaK1k@y;76CO0s6{vm zpcVl&38+OtO#*5WP?LaK1k@y;76CO0s6{|c0%{RZlYm+T)Fhx50W}GzMLa zMX&}?i(mtw7GVy6S_E4FwFq_qY7y)K)FR9UP>V1RKrMm;fLa7c0JRA70n{Qa08opt z5I`-$A^^1rP5^2VP?LaK1k@y;76CO0s6{|c0%{RZlYm+T)Fhx50W}GzMLXShPngrA$pe6ye2&hRw zEdpv1P>XShPngrA$pe6ye2&hRwEdpv1oZwLw!Kt1M&x2!0cvlbac;Nj$PWYam z2@nAKE>5J;?*)BAaIyt7VIEG40bB;Br2xlxdfBA(Y)Ci4_He>7r;IQ2R~qsY>51*) zG8O<+a%`V)UDl*D$D}mpq%_y0H20)5&!jZ(r1bxE`HGXaHFHv0X;NBwQd(tFT6I!d zZBkl&Qu=?o{8f{-6)-6sI4K=8DIGj19Wp5$Iw>7CDg8fPe(j`fp|3EuZ=8}QrSWrY zYdB4$akcN3WOh^(L+_{Cr~FiDfW9aSf6t zrSTfc114Wb6YEYak9qtJ7R$m3{fsBl=u142#?L3#ok-)n$;;co`Up;FQ450;rss{O z#?lk>&`!+5{s#L9oF=EypACK3aKbe9FB56>nZpTv)Fv;3zH>O?yYAd6o^-Z&E<;#S zidijZ&LXMg3YZu42xydg3X+DUgr!G%gh^B{5jNwg2%9TUGtFj@{`%kYkN%dg{ae27Z+Yz7Czszao>w(7kv16}6xT08LqkJT zLrX(jLq|hbLr+6r!$8AOQ$tfzQ%h4@Q%6%*Q%_T0(?HWuOG8UjOG`^zOGisrOHWH* z%RtLeTSHq@TT5G8TSr@0TTfeG+d$h;M?*(bM@vUrM@L6jM^8sz$3VwWS3_4*S4&r0 zS4US@S5H@8*Fe`$PeV^rPfJf*Pe)HzPft%@&p^*mUqfG0UrS$GUq@e8Ur%3O-$38c zK*K=OK+8beK*vDWK+izmz`(%J5O&cJHg5<;8^UUa@X^FM@P%W9yFE!64$E6ZQ%hS% zS5IFfY$9b~=;iGLg8*x+`}4_c3rj0&Y^O7Z`AG`EOaO`*g#q@@qT?$=rTzbze8$Hl zo4*=IcKrL8WKy1vFtV4Y)adxpWDXi9nTN_p@RJ2-Q^W-cA+iX0 z8n-x2f+x-Q+`L?TFE2?Y6#2HKuPC*epL!+YNyZ2c7yLJ~b zIE+pG1Jcs@!#tV>23@_+)_(dq;;iWgYfcV2_Y@;K2VGEHg(|{6JImLJ%a+AqV=ZRObmF#S za4ccE$nbE`Z8#(-bL{jfJg{LyC|iZf!TvIhVN8*5CU~{D6ZZQ@ak9^EpXbA!sKF=9 zk+z=}KPTs`QBqsHAw!9}f-cWt!=Xr<5?5iwZd*>%QJG6MWbm8QI2o)1T{ES;BJzI4 zY4Z{ibRH@*E@c}vh{jD}G5FFv*}o`pRdf+ve%?8($XVJetVrTD` zmV=?(3Jhd;h`9?H%0wGmVU`?UpWg_we=Gxw{zFxN!MN z?~5UBerr2lKX&5D`o^Zt?xFVyTXy6ZG&Wth*m3`XZC1new)PG?$N5W_xqEC&O{=V~ zX*$<-v7295Wci9OUw@CVL)JWg!Xp_PCNANzY4hwva(HZ<4 z3wQ+>&I}4woP$MSQWz96jDp2QrBOKP1dkBSks-!d${^FGaxI`*QdB7fm7mVbWki*j z;UN`54V=NgM2oMWh|=SKQI;`;SyNfqRH)D>D4LuqR8cx7g#WUKa=6hk%K^D5UiOAFogxLKsHI zi}*88wtsk7$oTN=FxDju_Bb)-?dVW=V=GNlQh9z)k1BE;T;Dv7Z!<0*qr?!a$p$s-qb!%ik*)251hC@`Gnixv}S; z&79~L_a~nWc|1n^3|zrSEvAutvHffCnv(g z1wLQ=bL7FX@q%H*!hg;R4TF)Y!^5KgIcI#F!&y=~;1EchA*zW}L>W;;!NLJRAUTKr&eyui_;9Kt1_cAC+Sd>x^Tj5J%0t%-lJ1o*=q8HpXiMIikX zKZOZG1&pl&Wq*ao-l4^vWBd2Fzwnb1{(E9>Hfl%Aa4m14`(|Uskj$h;6WeEz&cmr@ z^J(GchDV0QMq-~sQR>sqj&9}`gX35UTyF08xCRgJQq+-=4xB@bkj6@W2$2s)AUXkV zUvvR;bnlv^U@wP9Vta6{<|m^E0Q*Xk1Yj{EFr?Vgs0VQV$D7&6c+0|aN=kn08M7mM z%@;YJE-9H=QnH}7q$ECfw3cxmmQhqvTH;(fy5yainsSaxstaG@IfIS9#rgknny7{G z1^9Rj|7Fpc|B@SZ|M~L?bBgJo45X}8NV=NZGWZ#5^vM32OCj+Y|CND+;G_(wJV@q~ z@|c|X+Bg2OEI&LcYH2X!lt~g?Qt07oXlO_h5t$=0Lv)%viDpZh&g(F~EW8&oA<@Tb z7(bFG@rj8jDE>7Ue28?&!ZyrVe|^Gd|5tJ}VX0)mX*FAbo3L;cnmdoGM*KgX{&nyE zZ*u=-=t!9Lx4|PRe;+;a@83<>6Vs;wHv7Xw=8u(+*+g?jD|FUx8Mx3J44e4haogy(T;& zGHPw~x|sDFHg4MdXG8ywQp?5p;J11BjT%q@Ch}V$J!Mk90@6~G@)seE_kxM#KS5e( zQXV@uybr*{B$yN?osY%G%_lHbWU9neIVoi+ZK+vOHi`=rmn*K+h|pkbq#7JBC^e`v zxMu#ye9)Y1CuBF%&UB&E!hnVBg;~ogmR(-<-0PRuRBxTY1%V-fI|56>FNgPqb8JxF z;ILtJ!k&bhgog>_6vdSJDbX2+GcIL(I3P|N`$r?gD91E98OAM!6ebul&Y&`xFp4mlMy65;>ga4T8SZ_!A{}0RkF8B1(Fn#^83dHgWH3idV$jJf3Jadt zuxxm^qr+qQXrVBEF+A5X>6Fo;VS982l|s-*%csKgH-$manB>uokSR%*1V3~9>ol1iO$Q00=jvY6PLO}5}1}sLSGMHmk z05?@SoggzA6cU3$VU5=)><7G+g8`rEPyuun3?V&M#c-?yjY1}vWI78ff}oC75*{>z zPKPKq6e?5+i9sH#FESiGJOY!bG&+MqfxUpyqDS|cpwi%cL7l^9=ycc>CW$^)jSMn_ z20IMjkD#f+mwy(CLK&-33Y`L9OPNeKi!1_$mS)ii*yLE{Lc3%!SyVW$R2q{;r!r{x zQgO7NS>QjxqB2MXyhecfCqp3wjWpI|s8A2^*iIucnRGZ81e8K!Of)1inE?;)bOwzI z<6gr)(4Z71?A+LiAP5SbiD#TahBHKm>WA8cvd5bpjn1S};1HN}cur%I2xv4c___?P z9HWhe%%qYDI1Vb*4g3R_52#HleEl15q6EwaoiG*J6B*ydK*M6d=-*T(ZM@|&&}{^E z5NZma$)Ke%VPtPIjOsn!p5Z$onF0HV6-I&^Hw&6Hd`%=1a301_CWAqT;l7#B(%~tI zhK-TVfGUGC0aZEL_?b)?h#T55>=y~ICHyJ_E7K^@7mr^OIG``QoFTDL)eMaeD+AUh z)9DN{oUyTM5H39`6`DIVFSs};SS7eVbntY7Iez`X&5Q(%AG~DnYN63-P%&5noMa}v zN*uqeXwYs50(t?wHW^GZgG7e223H*g7N@~;@o0M>!$n1dGYJhHn=X7ohU$WA7upSM zg8}t1+B68b)ZmC2(4lGS53yu>SCUytr6+oLcz z$lNfPyb*LZOhS+>3fnM*>jN$a3Wb;s4S^H$&?KlZR~ULrBitA)%OG=-rxPXw7okN^ z2q=#NjT-8mlPod1473iiAX%JjI9l(5@To1-G(or$WCkb88+sTPK``vdVz4I!q0fWO zj>bvIu?T-EG;?@DnMS5k_^8~Fq{AM%LuW^#NRX$)KXbUe;r7Exft6Hg+Qe+K46zpa zGZsOoJb>};ji6*KmCU4bkO>8XsYd1!q-tQ(hX0BzF0vFHAVD#JMPQdGMocof)9%(w{|Ke#`@%OGz2N9KZVm;RW3&Gj9OnBixLC-fmh=mei9#r;Np{kd@&p{9oCSA z<5nkB$(Do=?8P%;FAZMxDnYBqYnrME-R3dqFXRbPs0$K#DxpKS;nbvVAoO@=LQQi} zG+{rmbPhs`A_un)Q-X`E!-AJ7MK%xN*=~+QklX8c%!b`f<9~at)n4pXpxGenWwIe&!+4{`140`VXD(T6wTOZxyM-D`4f#rvaq< zUV)^ZyrARHq(k2JN{8KeofkIz(F?k%fi**fS@^{77Z@`N`;z5qYHDta{QNEb;I)lc zaJZkBZ;Vt(RHT^#z2%|9{ZG?> znf0Gm`9J%7H9SK6)#ci9YG79j_gcJSWXHN%=+?ls6Yh!xO_^fMQkqIgP3~;5bHwHk z&79!)-|B9m?O}II;ehVe7}n*2H5H-we{{KUDM6n-+67~``)4cI^}-DRt`{b;5V~J3 zrZ;rIoc{-%FLb?B@zKtAqU&W5@N$)i`mY_Y##pz@L8Zu#b-DEM4j0~|lF3<2r1AF5`Vfu=m4n;;U*3>fZZNEIl)uY zf9d#qiClP^hi;Ghr`v;0k3qr?&nq(0FJv{m0uGb%4ZsH-Desukx4M&Nz;BQ~QNdo3 z@aA}xpH#SCh*v-;Jkm%-2Sl!dryN`_0$u@!`Tu>z@e)Uu3tQ_K?jH>AHUDhzzm_|( zJYLN3%#=Wt0zXzy=!bJj0wPk9TvDErgDRfNN*X=NVFkG=BCZLq~D%;hemSs zei7381c#Tt9JjPVDNmju^ub0Sr%TT@*U595?)7x>P@tDH^u5jp>%Ephzq{1$X5LcL zeQ6iycb7rGyS%^m>53b#z1*Zo1MWjatjE839jg}N1y6bK5?2l0_u{i1C-ha=AOG8B z145%BaABA~u{^kW{>SB42ahd2u`IY)jxIZq4~M+mq_UzRuRbY{@16xF<^3U#zwb}1 zKM(RUkpH*)3dg19v&zpW$j{d!BFbCQS1km0f2LYMghxbJh@axjzsli-a{l8nPcFyH z*B8pc!_3~fpbN%1ej~UOyVh~pZwHl<+czAs9 zVktKX{Ol}vN$kul5Tksik*?$zp9}ktBn!*L4VGe3_{=SE4=^d zyICJ){!|iSx>t>r{e<$%K!{hpH&V9$bH{DwL+bha71!&h#cCH&Oul-3mylp4z=&#y}P$Fbj?Zm3jelpIXxi>*V`7ecovWNG)FjJ6B+#O z_d&7V`Rm1p?1%3{KeZ1&^!N(FnHlqF{o9!gIyapY-jV#fnaqFp z;pk_4_?H9TX~3}nIAJ~FgmsD2Yv>1X!aBtX>lG)gTbxGg7ZX^=I6*y++Fs7Zqt79) z*V>BU2YkowyhfWF?l=`sy7_v(kFH?ZZTXvbQvUjVX9j$PCI5f@-s9S3oZjX1X~4`l zk3O@+jQ^XTpO}A@%U;F>x%_2UF50hcGCdOe^mwh^7gg?|`WQE-u=edwvR(TgM#S)` zsNDUOK|5EioO^e7!0Wl&_lq%NzT2$H*Sq9co%rl=aQrjdeKlD-9jw+rdp={oFjH1c z>{oc~x{AP*kkqeIJTE>EY5bmlICI68LFP%_;)U+ostpaYbLvm|EAMz5Kjoz^d!0)8 z+A?PisbS$An%7Q9FSu$vZ;wdOtk-8BmYXz*Dake3KUw!^%Uti*PWQM^9MLHck?0O8 z(GD|Nb0>LK`3v(D-|!^*UeeW+otKMSrb;e6@r!4bZEf?f!6D;2A0GS95Ni-1heT%m z-n;f#;?lXEUp1=d5?gnhZZ^KteoWQfK)2!Oq1kVA+uiK9S#+F@N{TO=M_+XEY3};= zz1!mUurhAG9kh&)6t$aXG5j{~8|U{6FW(CC6?pDHEbzns$`{Az=pK2|J2wpC=j=N} z`c)<4yf>>tYFoTdWr}-*)S(JpXE%?~DrfKdN9uaJiY`bbl<0ijCjEAUa%Y`omB80t z*-j#=zwel;v7d8EMNm$t-VEu>+9&6H+t&Ez&H?&4j>OP_y5>XKYgIyLa9G*C=bmxJ z^37^`^OXm_OOuqYXDTjr4@{?14eA9<-zh|~>p%2=rs^|kZ9)5XzFw1_`&Mn0cAJ%b z($}M#T{M%ITkVmM?imgoIJJJ>q2s?+g$teWoq4yG_QrGB6KhcuN{8sn2JO_D$#Q4I zN`o}$-sf(lE$R6dbEo?DvCO3UMScbmmO8>Yn7s8NW-fi)zoX z)xOcJLJE!UMAJ#Yitc)I6 z&s#U;!0XpDDp`78O*%>rm-Q`9@!DM(Es;Wr;!fCmQA93q@a*S;p2}?<{#2Lq776m;c0F+BStFfvYQM_TcU%jZU3Q+JdsK$);Ux;}6p7e^<1FtGDid-CbO22;}IVF_+@>%dgGp#hP%fiM# zV|SHH-q`s(eP!&uLX+e6!-qd*DOgn=h@SnVga3NA#E*-bv6_1yC3Nz4TP`s-U-VNv zcF=Uw_V5`&{%>>@ZT*dV-Ahx&%R2^WBtJ{7`{wos^_HCUxf>tIFgLx^xNDW8%(JNZ zrpddHP+u2VMZ1!pmAE=R=PuT}N;`5xNN7z$jk3_h&rwLqAl{VoRPK_1c8oTNTJDc-h zy^jCOjp2u70}qo8^Vp8(-<}(h6n9K6vzlX>d|Ar-wExKwg`+aX-9pm@`-Y2Zy-9~h z-p@UKxu86)Y|X*qDihCEv*<9HjeOasMB2~amUb|x;&r~Nynk_Q?m^;lSySda^H|DfY zjc(LN`OL>II_f)|&&@qDYequ4SVf(17%}{=r1H?!9Rsq_G6&u~ymBt4SjgSRLnLbL z_Pu%Om#0tb-*){6r=FXD;JY7oH5>dTSN5~NwLb=x$|^S&+fvN4HllH*?C<% zdgMY@sMKBR32|CyzT58F@u{I(Bfj?iaIsyLx$E=t*!zdJ1ulrnH$0(AxUY}NPTNY! zv{-bRo*1$&?(^O5*i$75#b1zeF`4Fw& ztbTUyUTeNzqQlL?=W$Pr-jZPly$rsVUW?BHKZTZMomXD})beb`hl6`gifmXOX6m}U zKlsYW1N$rvbPT9u(+V}i6_^bgi49H(2}i8YTEbDdeN`wE2q1p>07?uP50wd zY-u!6ktB!3i)bGC$)t;YaYY_G( zbC>kmLAOh{hRl2#UFn?1>Z^*Kf}++l-ti8Hyuaws{`P5xVw=D+yVtiHdcPbP^3>kB z;$B^8x9S?-{)0N_RSek`r#eb99b$Gith_M$bM&0thq(#PKXbc>`aQ+$zL%){WYEXZe^YAG8dv0#WgP)#m~69C9-=*e30HAEuT&GzH7c7x?PmCPm@AE zFyeOnZG*$|D^wc(dstIw108=m#~CdH502_e(JE<1E} zj>WpS+&i6wz4DnaBZsy$-dnW9f8pxa%VVzZkW3F&P}{Zg%M&e|HTQzp)lLtN-W9JU zecacy?|Mg4ZocBJYdra{*+ZL7{Ax}px^(>{oh@)(#w*xO%%n2^){i(3rqoh)P>heL zX4^nP>wyYmTBM%F^Q%8S8=^~geQG_hoz8Fd^FgEW*)pNds`sC)RBeTvhtmfxeB7Z* z;}^-c7k5wZJB36CW$NrFnN9`3U)6Ys2W)Tb%g`ssUI+v7VK{m)20Xug|2em9uKKWhi|(e0h) z^$CyNR~yXPA;fvy`i^bof>OE6wJG#z1NV2{Tgux1NUp2g__9cT$DN{Y$z3nl9Nov} zy{MPiN#VHtrFfpC^!0fihuxL^RwrIdj=UqeGJqu2I%lo1S=+5AcP=GH^vouGy4OFG z)e(JBgk$*FE|*25HGHc+U2AbZd+=IJN7aD8`A)(3HBH%_d!9*_#c4R3+uoh`zD=EU z)!>1G$dQeK3tV|}%g-1$|DxY|f8dJvmfj(L=Dh-YjfSl@YF58|3M4{=?bCLtnXVP; zKVX08cd{1MF?Cr^?e*KD3RaIU{F-UD<>~uDbH#I8pK9z~{B`4})R)T!ly9D0_c_J% zQASR?%QTH|k#+H}y0kP>1&z)TCnRU-Eed>|mtt05nZ5P>xg3S7tuL(}zNje_;`RKT zdA9kRyAfaM%*V(09|)W|S^sLN`it!yp~xk?UAsm6pS+bW_tjZ$C&s_`@{GFn!$Es= z`Ws32xGS#=8xIDwB-@;^C>$`KztGL(2S*U~)J&I`0t=P+j~=Vydib*C8L#Nqw9@d} zH(Oo}&DoP1@nBESr&CLK1N>G>zgu=@>w;Tnrhe#AacVU^7qChr!Z|!4V*b9A;VDKJ zr&`RO-torKcY$ofi62?a?}yTFzDPLcy-6;(?15pEeT~gm>nGubZta<^bGIebq-v$q z?g*OC|2b}B)uB>fKP@x$;!ye#=kO!@y^^{be*fm#;KXrhOH{Ho@oP$wo`*-DN+iAE z)uO)4**~)isrogt2K=@vxrIl6E;Nq4d4YONB}vHj>v`LkCR4Ty#t+N9dJy4|mGk+& z2e*rV>CxJ^J-2Rnnw~p#vxaZLEc05Cd^UsjeW#{m+l!38j=GUWt7GkKqTm0zy`T5j zd#}#J+69B}cD$+neJS2)V`%M_i_K|4eQL*ie{b+vCB_lZ@Vz3U?dbaUUnZ)i(hCc` zgbz3}R}^sNHPX-Q40yr&L#)-C<73BsyHH#C_pOije0%os$F;%MqYt*Z)~q!OTJazx zOS~t_)n;bqH$OpElJ=r)jj}FN-tRkA_a%1Oj+M;?$^$BDF7Gzw21X8w)R-ICKFD|| zzO>3QzoqEyquvEJq5`#3dV{^Z9X_V}3r&B~otybuMB>u-TB(X?&+A%)NpkMH)dVCS zMhdoboMDfY6qTPoUAshXN_uu2_e;;FPJwB%&jW(r?fWwFlrQbTz6+&;pLd?~`8559 z+0HAc5`I-@21Qh!+*n!gElr~#x3}JRqs@&+j~}?*_;PAvSuf9*MsAfO+k=XYEY=TQ z4?38BN{zq9zP!!p@~y1kb4gawp6fe(58kaQ+Q4YKxaRc^`;Eag5AWn~yK>)d@j3je zZu>8`eK9KT*niiAyEm&dMkhXIUDdYRtu2l_`6Nc{uX?_pdzDWwvDR>*i&FTL_TOjT zP5+%?c}#Ju-$0e)z7|u3;sTTP8SPqce$VulF^Nb%Gjpr|HSsu)MP{y>bm`0@c^xJ8 zh)>>E`HqT*EsQIRG|uUGj?`a1Jw$%A=hU{JD>uHMZD6MI^I4hYopn-WbmGTQj2@<*43@?>yK<2n&;|Rrx3B$%~`&7cghyW%&>2K zo!=ONMsH3iRu9TQtJ|UH%<**_b=!*vCy$m^+<&8%_~u$tID}533y3lr3L&?K~$-9=l5TZ%&1#(zitA115viQf+G*wvv`*@q; z!@hotKlXi7J@vgb^Px=Eo;eMhXRelu{{H0alKMT$o7XMV8=Tg*_Ra7P*J|fk4;Yq{Jm@tXS&SGcjvck-Mj42+57MMtyA?%J^J=LMg96puG%VC^>!-x>I43` zqFX;tQ=XPg%acFc(y*xO$RftZqgL6S9(F@E+99vz9@~?s`tnNbmIta!xXklchozo* zaprKtlY&KOgLoHQpLg}7yksGxeBX`2LYpc1B&p5n>mn}6U2~7Wq{x-xPr8$8ykyr8 z)z~jberWNkvq{^+uLSADe4k!*_3X$kqg-?4`SnS0-8J%gzl)lZ?Jn_sd8YTN;-%IZ zxx<&cKdElIa*x|m(=FHR<-4{z?(i6KlLfmoHbrjj@F@PKw7Y$u&%FGojqSFlpFRlK zUn~05wDCmJWij%8nfr^ZT2^^wx0f$}J$$5PcImE_ogZ4ynptFrKl^cP8Y`&Q^TIyS zJ1Z|APQLz-*Yhf^?@V=r)a7$^$$g~7Qw&V!$gY%ox61frNuMk+lk<5J*V<;i3~9d; z9O2cu-`n>WrR`a5bvD3s`A(i*=C_uX^oTPldtXs`PHG59a8{o^Yi#>5x?xv~ZJ4|A zhqg+^B74!H&i<;8qMUlw#|@@yEqWvs%&GXS;_1#-pZjeyJe;;?$TF7geonx!E2;GtjYw_t0{-fuxdyiMPRnA=BO#C)$FN)@|s z|JHz>>W(rqbs3Y*HsQ~rcPE!5ekTGKXsd_(D#9XqX}hlRE<>~PSccAU296- zq~v$2(q>t-7I{=_YWg|PsJQMBL%%BUDP5nx$<)JHB$a)Gv$t`_-MVL8p*%H82P<>W z^Bg>WyghLF4vt&CQPjtmq83@Xy2e|7P}@mwOy43jEU>b3Xy{qjfXUSJ)f*z-9SHK= zlG){8<2-MARKDvC#q&vLLal1@y_H;h6=JCN*V%>cOReYh-ht8C6GfKUs*T|>8UVVi+CG*sk zo0pvH0=O%xWw?J`*tfycgl4R)be?ZTZ{UKBHJftIq&%lC&so3yY^ue{^7K^CkI~|7 z@6D&ZTY2*K^kw=d4ThYL>Z|SccuhRn>aTv`u}yGL+q3#wV@MV27>D|MN_oVH2rRX-i%8ZIyu~_hp_}0a0_oHWbE!cc3Jt81m zPOhZy?2XQwoG%_FN8e~|QZ2vZ``T)5o1^>QDtqty`)2hgn(}iF8ozgK@Ot#)M#Ef% zg4E9IZ?>0L_Sa?@T9;3+?D$|GckJEN$16Ymw4AGQsy(i|eRXn1#~ZfhPeJ#u9u`M- zk{ya;5@zI?8t3SK{(9?*hGp&)m)=ko_Z3pYhttWwemZTP8hY@`4%^K=yK8Pn9@?zq z+~Iq~J^KDZm$ftK0&R!llqkRL_4Y2jFmU^La^_c>%;$uLM+ruYjH9fs3pVCh>c)g; z2Bx1^*q*+ZloN6;BVL4e7Of_#Qsrwcn{@fTq0b|M9c^+>0~_u~F36SGM|HpQR`}bHEZFA%T$wDp+R-s(;E)B9g)1>bnyr>V?vlSoe;NZ)hgu%)+})`*c^ zRlc59N%Wc4oycR7LfL}vGZL+`|poTj-gm4@Z??zkZJHW;zis7MrTMenE0I6? zTB+&AT0Ld%mS6Q2>tA$cG<7+M1yI-S(lR+65PX8xpr6E7d9KAS$E-j$agNxWvI~Wa zCBn5IvO9Q{{iZ#05vjRZobPBp#jEwU@ss&G_k{{1yph*rmWjLia&#YEZ32JtNB^!i zxi&ssAGh!d7AN-T-#>qH|0G9Km;RRj@vnLF1OJ*|(E6`=IdtRvGyU~4dHf@(`o=#q zA*C3EL*@OMzLWR<$-pb+8q32Fv~=_44$je6D7($MZ>fYSU%0 z)JgdZYV}7d|K!c#iH`H}pZx1>RN0H$|Kz>PF1%e={#Ty8UZh68`|e-)U1gzK>yncH z%2zGrFcV%?{8v8j-L-bHmfI8g+(bchuZ7DJC-Rq$P1|+$SlF?Nyb-U|Cxx`D?GyP# zR>rq=hGuc&dGi;XXZ5-nX?f%M1&XwT>A2~5+!FqT&gZWHTiUGgOH%UE9P;tKv?qfE{zIiq>=*jcB97?(G+?`s~- z^BtZm^}x1Hw50*OJz%<%H2u?7?$Zmh*JfgtZ6!EG^X-CX`DTLkn9p^-C-Q5?3(?g1 z(?|qgt{(^q zTk&Q?yVmA3$RCj<#rN*`*ee>}d z=~Ccu0LRv2PNads>ODNAr1jS8soZZ;SF!s_^9^owvhQ|fS?|gZpFOnZt-sIFEeqx( z#VX$~n6oh0vA}#5SE1a!l^)f*c4hl9o^>pk+i@eLWA@kQRi8!)i+fkebbpXO${naYZISBN6NYubeeNR z?*rLdCPzeGVgBJ=eikdk$;X9$$=iBv zMQ`EKhF>Z6tH0fP_>=QBpU#4g5j)9sEyFW{F0Kh`nzembYpl3}@W=Io)`pHYgFBWO z-S1d)_uam-(^sDl?TFgff9Zy0s$*Y+$k9gecLR$|6o${=(LCbw?$O)B*^LcZKBc8Wf~D6Ta$J0$-Bf+Ic~)j-TmSal)t)t* zcOIFwxZ{Py^q)oLmr@M;9#>qQJJZtR*bHNVdp}Mj{T6!Ecszi1)(X%K&HTQ>@SfV)GV0<~-SF4@$!Y6fb<_JcTy~Gte)#MD>#FRsp`@d0 z-kH=X<_6UB%gQu&mlbifui}2A#XPUvRjO3MefQQbqIFlk8OP@bex;(R%!>&>dSdRk zt(1>hWVR%)^2(WfgJn~9pCMK+mG3BJx&L~3Jt@rnv^;s?+!FO_#X& zi@OX{qu)n8%$2@aX>#1lCt%*CHJu@Qrd;kbn0|elQ9Z@OsNLRfoy)N7ML+Xg+fwR= z-sX1IxgrPG9xzk%<$mj(BeBNfyM){MjD-1G4L6P%L%^Ut(SDJiV|<@)?zc+2BIWVA zF0g4;_SNZ*9aDB>Z+^Y@S)lb`OVj28>DIgN%nlt9+k0pkANjB8T1VLY!-x+LxTtuuS;qldAA zJ83M<#{-2rrtu3)gzg+(He@rmA@j}6bJw4}xFdR*vAS&Pq2NPWvf0h$yjv8nvf|@k4oeIQ-l4@)c9wZ8n*bHurecj=cv(j&>ufWStj})Qf^WVp9ZuT@w+kADt zUG!j1_R)|d2Zs!JwcQ(^i#wMRY%bQ-3o4A;O`pqG_$UdJbLU_E_2uiOi|P^gA~xT5 zAkOn2KJ+8!(ZgS_Rw_y?%p-KWY}(EcLW_SrjXUXgjAhZXDy44Z&u zOJC)<1XT3xToFodRFAB;XfHBz>u+qY+CD|{#nC4zeeBC~YWL_9 zm3qP2VQnHqvQO*o6`e_(?PfKH#Cb-dKlq*!-|d$#c1H>=YvwLUpVE6#^o`{%^#`lU z^|LM|M^Akelh?brYM8$@%et|4W`_IXyO$*bpRVuv_`54)<|m0d9fd=So~3a4yLc4z zW>4+yBPy+5iB_b=s(tNnYj$_@wU7|WlYST>`Qwvi)atdho9)7nQ}=EwauScRx1$FJ zJbu7)-}*;s`YIohgAbCal&N7esx4-@CEhBwZn?03d3w_Fq?5(gwdu0kIkjry zKj^LcP&i$%dsTEobQ`Wa87d_DPb$t;eBQ3+;wom2$VmrH*Ulot(LB&eB z!3{NhCHo(IFmcPgbiqmY(#5hD85!~EVJV;LjB1K3etZ{^tTmL4zS_O+bYggMz=7dV zCqe33r4jD#XEQpcB(~PtubQ@H%IDuVQ0Ht zc=TkeiD9HyRBi9~fgJud-!nwsrp&o{R-n)AObM~%y8ikBfiu@+s!Deoo#Hs>+~ZwV zBJwg;IZwEn<>{aql(k`L#=$2ZYwZp^=RA}9Ys+TWcbi(?4)>MMPGv+SHcm-%5fnf5 za=q`gTa}m6W*onJGIZHTxA(z?jPJlI`EV*oE8n?1slP`s`^^ z*HjA21CR6N6pAi6k$;e`IP#V69`$|(dtsikSdt7iW+bO1*@EddbjxaZm!h)u(x%ya zi=FHv%NW8&x*wv1Rn^mf+K#1o@Tj)ssI>@3dMiinza`wA`zLXc|0N==&pZ z%2uY4@U->fEysf{-1v3IdTPmb=JLZ?N5ga4zF3%Kf& zT&5R0t0S#g@oMv?YIa@#8{_tz4bD?!-}f(zkC4oo~`la~0pcn*1ngM0n4Kt(_Kf?+aP& zjeU!qnjY4uzuoQ>@{XS?PEAfpaq#wwAICz9F0B$y<2$%SG$txPtv=Z*U=tDg-Px+& zvpap~sj}j|tqjye381DSKx<^EC!zZU_bz8jmp0PZWW0R8Fe{Ef4$lg75wFQg) zXSPj@gUH99TPc#?8lLw{TDjgT8F-{V ztF%>qZMpbsh0ga$&VilAoL!L>x9`e6TeE#hWR$lb?cx?Y$HbLORFl61-xR28Y%?f) z;N?e@eN(KuxTQNF^c(%=qS{8HTTlDxfm8Bi>*e|J5jrAXHst31M9&Hf;c$ney9l@M z-Q*#KW9wGj-rp{D?bGb4uRAIIn(dj|mn_ILN7gLLi&{fnxp}s!R6{^s%;BWliG6L} zB;K12t5nXjTrRF+zC5~aL-QhQuXuf3F17FR+n-!plC!$->c&coyo>ewcUNZ5DbJAYit70iBcr;=m$5g88i^sECCmzKo} z*tnY!$?DN-^0_n&m_8d6Mc_csmX1QcwD?Df9I_i+iv~tf3u?8jcqcVcB-!FJHE&8jXqk#HN^~|9#)cbZ*@>Z{Dxu9hfT9y zm2o-~d=72FKCVA_ZVz%w-*qe@7WaIeGi%+7zUNVeg2`(*;&l3dl=)?zdcMNx=Di|r zhrHQ^hEroV=8EeSS@YdqkjoO_w|zX_p?9~`Hsv=7T2z}-j(7HZJl(2S1y%Rvn{5$U z;!q^_c;v_i(WZbMxNw1fu!YM z=AHa`V3EXe4ZF(<)%#X{(C&FW%c?N{-p2jIl}9%bgXUSn$F{$wGu37vbW9rX+^Z;= z>R5h+xjkn0`DY4BXKOzcyg%Y{b$fsJ>CX=;BUc8T;5+Uo($w{_V0ZS+!J?kLkSk?o zk3z3i@O^3esPw?X^`oMi(d1$Nq_TKC9TYi1|#jkwkE{}ZV#7E;7 zZ1d&I_uP8PS?BD%_l9r%-Tn4UZg|PMGoH8VgTJWUy5jD?{9xUyjt{OnYW}K6{_x4Q zBd1*S$=fe_sP_ru;58Tj_K0Ji^Nr7b@8$nxo^aW&FX%e!lvm$*#)^-h19R?tb!P zFFpBNx9A6MJo=w!zv9N^^9$bhUn6f=^*ej*hu^-Pd35FV$DZ_&<9cp9uw&UJ!}q`M zZ1?Yd4_x|&il~o&KS_?l^0kPrmNRm;L8O_rLomL+^Pgdgae=`0n367ripr>19t` zbMyVZ=alxm|K1aNSNl6hmz93|g%*kz1DT{J;T6oPXPgJHEc?$9J6kt)IN;nP2&H z_`=^@vN-(k&aWI@v&$LJI{mYceR7*W-_W!1nX8}sgJ=EW*wWJX1Zz(I!D~K{9`?q^ zUjLiB7M^zVqeorz@LzT~?41ukdf}Vm>%O(ZJNvfbs{ffshBvgLmHc z>iEz-{{6R=cU-onbo27Bguj`;%O_s)^AG)FbnOpHE8c$dW7}-JdbiIm+;Hjk(Mxap z+Bc6sm7#IvWnH)3 zw3~JIr?33_nyb$F=aUb-ara+;aR2X~bgUH}{m(aj^U1$G{IUHmyzW=uykXuuK79K1 z=BE$*#CLu%u+@HdKI3rvk}I#8xBI<^?R3mDuL{GxZ(DTsga5qZEw2y0_4=P({+`RP zx%#d@f9&*)AHC#dH+qN7zvY6C-EM!?cdk6}@;^n--2L|(pZ~GW{SUhPuOk=q|Nil- z-}2(0KI_)<0f!v$k*n@m{H#|T=5GDOHK!f7{>KdD3g&*>(LBN51o?E482ZMu=bbt4)ALT7 zcj~+o=N&We$a%Y!7L}HjR)B#2H&`T2C?8WkqT{TNQ#zjC`PI(ziCpkc(I;$%BSE+Z zgfM|0a1(4*TDeO7d+4pJD1+XXPxJ2zUX$-tL?~YPA>f3N|EVwVhwLnfV${E-hkfPG zx4x2J56wRB>d$`8TXXVR=k2*_)pe^@ZMSsQs`pf~kNNNUZ^*yFN%EUCvvSqSRp({* z^TuEP{I?h0c-@zt1Bi2Lh#E4GD3H$l_9-fwcWwN;UDdf=+jXitS9iDTyz?M{n77+a zPi}rpowYfCp`zxJ;Sou01RQTEGr>c?}Qv*}{*V9Sw|{@d>*bHn__rP8NA2HhU$(gR`((fPjepd4Z9JMkf8lQPpa1M_ zI^{#7=`Z&D`LAsJz}d0!$ejO-o#wycRke?-=XE^ynOf~|zV*82ytIBae|}HtCGNAf z`cH5Box95mcIjPG;3mKO*ws%;UlusVn;*MD9T&L5habCH9Tzyi=RAJ9I_}MJck>^= zR~;8PxMhz&sE!LbSs(9r}1DZy#-r9Jh7a%$Dgl`-5l5S)5l+? zjtiX8pB@(mesmOa+|Rrx=Bwiea~#e~pAd$8Vuv{{#(tt#9iN!vRQ7*ji8?NDBWs`7 zTOAiTj-#Jgsg6&}aTT9=;_d4As2t~T=@YR!emKV+-1tPlIxcVk_dPM9j*GnfGoO67 zIxh0$uYK|;b-W_aTkrbh3F^4WBfs^@Q`K>i*ByECbai}Eo@YJj$+OgPk@r0J$qUr+ zHF+NL`%hk^j*GnDtxsO2j*C3qqfdTc9bb^=&7QycYIWS5=dpI&e1kf^KF=#H-+Z$= zes-Sci8kM^j_=9yF7MrZuR6Xu&x3q?^MmSmex8^3+UCdAvAh#z{pmj!aq9e^#O?3g z{H**ai=%hsv?oPe`xPh%%D*Dc?5WBOG)VHe?}~Zz)!&Oa>^ctKE;Xg#0VwT>nvu)$!eV-0AhDes#QO9tT=f8d1kb z=W&_0m)@<8i#W-_r7W~x#4S!K5sKzt5l6V7bgDZ3TJG2XsC2qIF8u7!yIe$3(J8`N>(SA4R3vpO#9 z{EN%CtK-7%zP5a?I^HI?gYPLnsE!M}w7cVRbzInq+fq?0A8eo7Z98{Jr4DO(Zbz-^ zcz*Wx+^$*Q@iKK>*cl(_c#S&VA>a2uPmQ*mU+lwIbXe+m;Uy=We#VQ;8-CN#tB%io z=^rmT<+uO&sfRk2sN-KBeCwqH-#P7`tvmNt$Kz{VD9p)gkOruTdk5FJnQwOSbHeZT zZo8enZEx?kj=p_y&Mgz3^C~H(q3B0S#5Pg&gGNz)o!{l>6vyx3Z*2amc4IpAFU5H# zg&9zL)!KD9en!J_|Cr-*`P+og*HB|sw13_t-!E#citmb<)4J`+Yl@2Vzck_b`Fncf zwpj%f{4ceaz&XYFBzV;Ii{od~$Blg>HDHZ>vs->V(Yv0ihO4-aBnDeQcXr>kb|$Fd zD%z1*F6|V@_wnw8&#gHAsN?A;Vqv=l<$QG8FW4ZvEficFlf& z&2HJx|IU8yc2f59xGS@tYaUTQ4?Akdh4c7#?QDrn6Joy#hzwDR%v|jhn+Gj1Eci8XUO;;{lJnu8VI{MAlhQ;%) z+IRK3@1DPS-Yy?{=k0H~ZSig=e0}4~zx;wF```D6U*7(by_TGE<38Qj{OzzMv^$Ic zisyeM?+4gLuDxdQJx}btpy49BbzJg?UFR))Y(Cs z1GWJxaulTyET=RG2eKL2UKALfXW3!ed2Fd@vC`AiD}*K6N*7l`%6eBPkPJt`Fi*Yb z@M3-{cE{rT`vmKI_nLp;0=m6AFgy|rM9CnR$ojngNrmrK0$mFX*SC|@3R51CJF$Ju z3SwaevpmyCy{4B(#_Q)eMOtuQN>9-Lr>y3v3xhMO@p?0p}rUBzMUFcV1{=0 zlJRXZ3W|ngK)^5UC8n)~mKLOuE}gCGegL1e?l?hA?|56aXay?Eq7{CvIFX};M&bpr zmiW+E(~9W?ZX7_nU>jcOn$Ew~S}8iLr)NwnnrAs?sK=@41#V*aaxaWdleh0dpzUgA z@{G0c&&1grsK1;2!Zi@hER|s?&m^_gm7yf9tW7o*cRBX+ieu?Ul$x5)u!02J;$pV0 zd5&p%hQXK(L-Uz$n&M((?%xR$%b@2X$JY|m(`Yh+``d&g&er0Tu8#lxklt2pCa_x$ z_4F*2AKM1QLY>2$NZ0W|ORB@``-4r4E_FCESjiYniw9ew$WAMs?O2IzL+0%#Q4|MU zJ5AEaw&OJPJX3dk|5>{hqk%Wj(=(79T(rTj72l77Fi31okF>xGpF){%5^5eYTN zN{wWjy<4@@a;|BSX+)uCTV~`XRwPg6@MN|fd5#qtHWTnU2eoP~8eG4rBJZrf8r4=g z=#yYUf)keUBV6VcGvV1nU-;b}TeD;mCaxQ4Uh2E9A8AqIKlkJrFHrSfnrexvf3sqR zUK}`{87DkmtlopeN@;3wXeOb|$nbfm%=nhnJ6klGrpeG$_tfy!N}9TnZkqIVV$%IG z$XPO-#MM*V3GE2J{pU9?pRCEll|c5zVyst(Rg0B4gqh#BS+O*WM%7{?Yl3Up5g*1@ zY`mbhv+k?tf>mR~L7G$;cggC68Jn;2!z=i7dG=u?3833|j4<$h%XPJgyR=vVq2G7Y zz)xJmie6Z|%VHWpjx|(G*u=9I^TP@x|DhhRL^1fnLwy2(&VQk!cT>rXI(#f$OekBuNqlzLTOyV7;W)*0P+4talkhOQsh~S=N-A zYi?@z9+C`Z7$U>q%T^Vpsppz0>w=}Z=qtQ5Zr=Lk?92?PWQ@>aN)?T^{9^?H20x%d zgQe6l)ELlwGqycHvLj3w!;fCJa>A=ru{W|?ZfLzaz@&^B`)0h6v{0cR-acqJ&jycBUp9mn@HN`cMOx%U_W7VkJ4AXbHcXQ3}b3l zl6SRezW5Usr)6r1YgvJ>o3SBRpyLx=4=oI{*wA$Il@s?#Fr0UOafxhBRuU&=GN5A- zA#K3|FZZn_OgNYQEMz0H^=(&7c;9N{tu$G@V3S;6%Bu`U_LN( zjnL!&ozxQV%*F}P!z9p5Y$Bd_yZ#BUl+9jtfZ=4a>0NxGVsfbA8y9&X%sDn?{$o}p<*VtIy>$k<@yc|KdDgBVYg znDHAX?n^A3QG9D9`P(dWtZjE# zur2OLGBki2vcopZ$9%Hmi`nqDTcIwwebaY$+Gf?5?`pfjGgMtu9azK6628r9zZf1$ z{D7bPiwFWgp(IitaYu<3S*Fjr=jaJ0Z=x@VlV~swunZDbY1|AEM@V}2W`7LUukYVf zA-=%#!(cTsEKlUHx9Sn@Wr80Vn1(a)`O;t|*|?rN=!?x*WBvz8Xk$+}w&_iLo;-*Q zsbD~j#}M5jrZLuxVRB)scj7a%JI>d)K<*c_%C`*h5ChKlS<`A~kL@LOTv^Xcscw{> z3R%^WZxT}t|0i+wterWor^MtN8VLHUha{COtf1V?!68}crPl%t6V%mN1x+J}Jl7rW z3k+Ti|Fzlch6hvjgVBMZ!G2zWJpWKKEVqx@<%FTlFm>IK;pHu9`NI2?jq;vX4z5iG zxCee!v6m4!mdpG2{K!iM7)hFDntuJIH@)3~!6B3^8Ye?C1Pt(g#qk(T4{QVbK-?SaYH{@)H?FY?nc@pwinEdF$y}F%YaD?i(Cg&gdN) z*?TY$$~XM=T3@Xjj7R&~${WAHG$Vd1YmTj%jKD?ybj^>SjV^+ zmbkCnm>gEJ;af>@6ZYE3L)()>{Ls_hX zhbssMt_v2iYsl^8(~ovAx3!wDi^b$cto=!Z z;F)6uj+riLzX=v}mnGef@mGS#2$t?H(U-Sb0cH~MK?4)m$ILj02{*cqC`|u|!EN%Y zSTXSeaV+DZV72Iwq4~nNsxUx?WKb@f*<8q1Q`2?g)RWLlq7e&ocHghz#Yhii`@r~R zL9GZf!NdN+Xl-S1h~-K~jEhU^M>@6u3mt)wJ>1%;%^TS}TViSrNEB51*mP3O#x{v# z#N_;N`I0T{pk!kZjqJH{%z!QC*3y9u)uF)wp^MHotWml-i|G|pJ`0318v9(s2-vHL zGdpZOw!hbowTH@9aOtUFs5Zr8H28FkQ*8SX4Lo=6O<6pS4RxrxfeZ0A)%;+!m+Lcm zHd`ePxl!+V_SAW zG1g_dCI>(}NlIM7ade5^6Cm^UT{PyA8@6|bV{zYE53Ua+j@)NEzNZtAv+Yoeec#^i zsp|$^ydIrR!-{NNS8e|V4dXhWL8Bwp`Z~R0z09`kFWh>F;32WDs5B*ebgSw_&2T=l$OmMexOp-8ro zWQc4=a^UW5>^^Z-xZJB7xXV7@4HG?LyD*(NB8KXQjOT6SxMwL z3Lt+Ulu_&_#9Y$Y(^A6>32D8hajUN{Zz}1sRN0p}1YAavFnxOJc!3!as77_uii7|5 z8eT;G`kml+NYpFm6V8qwF;pN^L1LRhGTlYwMVf9DW+Z*?_=K0T%trUCe z$R@E{h7!h6q@)BYX*KB=qberVKm>>|u8!%JgaylF;gj0EXc7u%@4RcTjx@f=pR-q0Zi8?`xXSfaX&sMOWoW2 zFx8WUT)Y#LJPT8d>f&!$Q2Dne{kj4=tk^WnmLeJVtldy7@(CgwEFhA3>kHVk37s2c zlgYruden}~l7;=cXMPBSwOYzq%EsjE5 zezRJSF||H5_lm+Kj5HUM)n|Bxk+o*eu^ljp2hH9=yrAYnvqD&@ZJMFxd;FrWo&I}} zM3!e^&sm8Jj3nr9$B|#hh#?d~+(vmY_?#pf(DU|*V|vCqx1mF8#$)dJaKh+7^e7tO3 zOZ~u;h_D~O>*?7Itg2*~H6Of87zJi9)ZqV(u3w+|f5UHTSZV5B(^Roql|@G*Sr#ws z3y>0IWIQSmT9}9s)894a+!q!5AmD@T!~QQ2`l%O^3+x3rJ2N z6p0s^mX-MW#$}DSkVOI~jAPURBz-X(2q*!Y_$QfzFx>R?9I;^L*v$ydMGoOP%{%1j z*$q0mW&l56K}i|~o_^@lvm5M$v;vS5;$EyOzW(l~XE%@|!b}JE0Mbc-wC{O(b^|z0 zpy{EPimgkM`MvEJ{aF^IX7mH#i2{dx$4OG$z~FsvY2|$Nkuo5*G%W6A?$gPyAn=K1 zk;s%ni!7i%9FYZ5`&udh3y)Nq!b*kFVWpc(lZ4kuq9&63&amc_d@+jzwjJTy?_jVY z?Z^1`8(N+xy2ZT?NBh@S){hR$z73p1j|?B+CD=h6u)|CGm>qLC&yH5&>T2>|oa4rJ zKvq7DH5Hw*k0@;#n^Ik@!q^K(N64BHA#B)fn-Z%@V^(Mn{K$qca--Ruj7p+Mfg5Cl z*Rg^?=VAkqBK6!OOHa?)S$oHn-qrg$v>5XoT0Q^K zm_u7OG`Mc*26Au1Gs?EvK!Sk@hdwzs(B%5Dr5OY}h#5I)KnV-jBA_tVaiw)F;rnCr ze{J3|aqdxNlLFS)L9^)W{sR4WjxTLDv}sgwlgY>kJ(&f=1r^mt>fLrspj{L#82CHy zpULyFU?q`>7IyUNp~2Dhp}bR;WF1& z#L~jCv8`gRjSKEovQ47TBVR~33(UUm1L6u!D&^xfn_~h)k?cT(wrx|ioBE?gCmWIHK!6^bc|C=V-x<qb3#9#_>yOAk3zBg7UZK;R^T z&y-rn!dj1?kx`&%5=@d|1c(!hiMs(3Ou?h!x;kaFgrh%OngqoPFo$h$7=)kT5EX#a zjtPo^VxsC26svh=X`hLgnmS^j0mfMY6)upis<ZYN^G%bJzqbs zE%yQThU)4Z0w@T3j9tuA7hJOE*yoq_$oIcB!!q+4mj|&fvW>1)@ob-^Q_tiJQW$`- zts57Vrim9VFp<1D;=UGEvs7%82{xiHmUbLt`6}}aq*Zo%)FK2nm30Bmmu^#T_odPb zwFz$77D(8gyjK!hM2;PNoG(93&E_ZoM+Wj;)?9=9_E(;!{h+0kTT%5)btNS$nttKa zv>$X0d<90QriYZ{==xVnV}X;DUrP?CBG9&Y@Zf<;g9l{bwot<23!>P-BK+FZv=>cD z{V^#H8>BT$*-P;vQX?SKp_K=)OF z0g2lh6F&=$RBD!u80vwGXJU(428NDg_XF4UsH^QPN~jTcP?DI+P8JMT-d? z;~Lm{=u;J%Tw0pF42a9fv{M`Ssqm*5-+G!Z8btEooI8q0%!Eg|o#&@5lM31%91Z_;<+x%ha4dMj0s^L>^p(^Af zcvBhG4ev*#_qaD(wdgB;w{WrWfg5W zvlxdAOps<40rH=gR<+TSSxiU(T~Y-OH%(FA3t>QNdjO3Y3Xp|_#?`Yo$^z&p;H4nc z2;f-L?4Om)EIOVgNXB+cRt-pzY)WVkD4mstFlRVFFYVSB4EKfbsqo^40$5cJ2o7E; z=3ej0`3qT;2@CSt6X0g@MR_rlWK-Q8q-?v8q(#@XF}w+H!p6so6_B*9h1a$2-Q-fDgsTd&uB7wlMBxr^I zhbLYxSb>H=_fg}=K1Ox2rN;qg?{%dMnln*(%trbyamO!*Z7ti13p~1 zq12N5mmu+hQb(6Dsf}$n{}ZQW#dcXeGgZ{YpQ$GV9D+F+Uq6@AMuX-cP5`nCL!Rrq zHzK1FH4K__(3Ke_^T2_`(GpSr~79iSEPY$WM#W^Z4CNX(Bg`F*G(9r*EnI}T)XV0CC@H0a+y zdmhP0az`FO4-)PnTu>pVx^r`Bk8y)iHWEdFuy;;lu_F6`I&|{pH9RuBDF((NsRRrF zC-iT5ni|VEEWSn+L~0x0W9;~MrF~nDjMhn=e6tk#hQh{!9FPK?G|+`J4{p_4OM3o} z7?OEjdH=@zGVj%JEOuGECXBGiu>9Ybb{Y3x^Tnpg11wSoBvmm3Vem;DH*|vZwvy4( z;$I4~1~iW1FYD7%d_IkMc+3_?=I;+}m>4E3KbV{`2o&=4n)}C6Bci^Tv=clq=o7+T zeRL8Lcp;RcJPmyR?QLA2Oqi2`a!r7N!^z{^Q5wm1?xqwEAGNX<)W^(<48hP~E;OC2 zECZ$t-_E|sEkh#-(W4DRp-)&YwEt9^g8n3{&8PeTc!cA`FxSDHqWyU$-Yv!NRMAV` zn$5%rojXfj(>qo?&k|o$#3$6!=jcjVHY})WK-IirxSxOym9FD5LtlT>utl-8!9PXy_nf9-vf?0ujZ02f3s0f&YI-`>4V zL#xq*%~&r%0!SyrO{+sg%nYIy(9`0-wC`$Q4}wD`sm`_%R7zh{>&M(C zT&xZ=f+K#1RV^g0m1l<4NTy$@vJD(K{Gc|cK_|NZUxRm&!3(kq>SWmwg!dE{zM5=8_*{?j}X#Fn8+V(zlnripe)>DVF$+ISpRHeKL95b_>mb$ z@b$4=C6ASMXm~sMI;F5H>qdnQ$|n*GyCBwy6GDZPKHkQ)zyJdKfu@207Yt(V6D4h; zVFf0dty);KD;Y;?8dUo$LnBX?_MJr!;BERrn@e>btZ}4&yp>#afhSL(oY5d{^@aEb zpxj~Qov|e#qt9-r*$UzAbC6bLSZHzk^qA3_08^mq1nbZ_ysfhd0b@;)jD&v+ero-Q z@@!y8V5bLwWPplEF}e)n$Z`{aZan)NEXax$s}B-m6=V#3ShRH`II7%sM2tNS>QNwA z_+u0-r$?9DKpkobLPREzMFqD@k101o9TjD##NfEwAp(oS?^zv4RUAA59zYHiG!YJ| z=NwyZ%!<_Q+y?3(n#e*$IcOB(Em)ExSe$(`fw}#-vPVrz>u9qY#Wjo{Xj`&2LjeDT ziV36r_;MrOe!NXUux*{Jm<%5qPy+X?LuL7pZc{(Gjj0T`8A25r;8%FeLnr-E zdHERoj?SEdO-#*C`k7ft$k{{{g+LOa_EVbXR|^Dm&L_pGsLB{j4VO0*yGi1HcqS7A zN;~2>(5}IB4@HW8YI*mDy|li%vVBlmPT6Gjl3^I^ zC&Q3AY4QZbf?3VU^-{e-S^jC|^=(J?VM2#lhv0H!fPY{0S*tKVX+Y|gOc3CP;lVca z(ekWe30UT0Q0Z8oCYf;8{8)M6#G9?sGpYIYlmM!zNFUspY3x81KuCfu#{c*X+JNK~ zVtmMMNWj266MUk)qz>z@vD4LIr%}I>Gg>S^SPb&RJ{`a)v=lI&v1MUiesWg!Z#a|V zfJz9eD=`i%@APtG)TY_8UO54AFw~|Px@7t(!y3n*b(Fba=rdkAl`mPe5{^1L zU38B~cTM&5suLGHTDs!4LF zN-wAnR!JNBYey5g0`QcdDYwJ!lhqp+Ll8y-ih_ARJ|;8u5uYtL@*Eb{bx!1;Z6Sr> z!jwjU2T~Vc?Vyz%%hk^;KP6Ey)^g9+!2Rnku8qa4$Hod~8whu9C(@W(NOAfgOs_ zZCikN$f7W!fVA|9b}VcBk+|KnZI zezocL!HP>RmWfA{QIGxjtL44M`XI$@YXmgqlO!*B6@FSF>_ho2_5(X64WMJBr|#Fv zV{7DFrte@^Od)JZOiJvjhSo1C4`vgeUT8@ty)sz6I*Av{VmL68LlJVgMWb~i0Eqmr z4D`(V%zz4T>=S^2R*|AgBo?f%mlsatdrlOXE7-~j=RnAJu&!NP9&Pk}TA0bTvhUrc z{(GgAzqRWO(aEsf!MJ3=eeJ{2uLs{KFK)bUqx;)nQLKY+e!n3BI!5vr z7vMUimo(WtQ`y)mk=)1D4z-MC=oBnc%Hw>qyr$_+WW(wo92Po>!o9XE-Tl=UBv|1zcG7RD*#4_hzVgvWT;62*w*FKn-fw82S+7wD(a(1k$t;77XTxC z0ugJY*p43Yb^kl%iK;v@J`gBLg4F}ihGbD&EDl^H8`Farx9eRo$2%qHS%f9o;$wh{ zfcU@5c+2K{vl7l2;M2I`?XiDnQZ#^{z)Hvju|TUqP)@x#6|RUDeQ)+QAYe;`E)xta z6C|4!Us*27=UPb1u<)}jiKqQm6>LB`fFM0CwfP5*~ep8|_C;#>HeI>hph7yqa&Q(aIl)E-bP8zFG#gjY31 zBwMVhiW-C0rK4#De3XJCGfZ)Ge_Y;Y`WuhdC!xJEg>t04LIvKFS_EQ1Fwf z7K)r&*7uN}a(%=UZSYZk_|x+AvNV|(%z7f_au15(L;%(br-g6`CKhaq=n=xZ5neOpX@Lk3 zr6!ms6AfY}0wG!Z+A<>Y&fpLeM8Z(y)N~AeFeW+^Fl2mq=t)7HW9+jUdgFFmh$qNv9|< z24&=s7idIzemN(1nWFZA@CQaORsehXP31is4CSfpo*dUyEIKK)m9B};CL(uktpBRq zr*atCO9NIG2kYrWUZU#LeF*g|pTDdj&@CK#R8@FCAx5BEO;#ePp&hgwS(jq-H|3e>=?Gsb&7}w>>F6UnzDu#bum85Z ze?5>lG|KD|ME?QVF9!;WxIV7)C>}OoQ&10Dz7QU5#3)4iFb-}m@7`oaW_%T6+G<&s zj`9+rCRak>K;FIRmNGcUX#gb^97*O)hG+ssU)=!a9Q#$W;l*P#0k*raIXhB-lK!VZ{f@1gg<TulDae>_) zi^96Syke%CVmw$`mkh1uHL~0jbwo$OM6k?8j{1&r+BCqoAWNpYl`L!DpzJHINg@GS z2B4BDB0J`v%Dawv*?5Op5-6|U#+W3=kr9i@YGL3Pk}5z7O61S2;Lqh<8{T%KZ&`<1 z3583&WDSZy3R(h<@*5u_oaoM{CU!_@S9br%P9<__Sa&sT!8OBC?KkpHAlxD5A(j5< zoD2W+RLMLj11p_4&0)ZAmfr4ngP)y%9 z_o)@&zgpzN3W#PD9G%+V%99Y5sacVipve?MpcS+pzz*`4kUIT+?r#^2BbP&qEDj+T z4b1z?FYK*mI4%|5#eu901#xxb1LZfh`&NP7j3#1h7P%Po-Zc2}fkxsZK@ag@xsiFi ziD0yWTr^uQR`(BvBAwdjgC2qJ3Nj>0ToWUGsQlKMF0_hVHS<3D6A}4ftJRR-^f4yn zKI7ow@xY3S$8x3*IrgDZfTg0IJ{E3mE7AT@-lJ&=Z)u~bNZbB|MlmavzKeZTA()^S zaV->eQ2H_N@|7`1Q`1(TP zRh&=FrH}g-|JP9!f6&)vT-SEIaY&4x)AIFRS&qXPZk&q)qbHn4Zq^ReoP7$ zD~2rutr-~=6JGXsD@_Q3*vaN{?hVY~Tp)Fabu>UFJR{7aJ`FBSI8~8~HeIrM-jN-RQQyM2&s})VWwt4cSIuu#^<>Zr zFPge&P4Q4FPzeoTSRs)~;Va}129dO*JN6#e5Z51HsH|E+ z)m&&eYhDsDM(WXl-)VjfW4~`5)6s_EVATx=^q@?ics{Dffd|av*pAAi>Q7rH7L|fp zIIg3HikUEku!ZmjRTwA|cMgj#F{sjtD;7&SITuFy~b5Q)fYC&33gW&*yQ$dP2FIAL}hG!FV; z$E^50c;cY-PzM3dfT4Zjth7p7Q4D63%m^^vBt5BPHUpPPo52pN>C|x%NOqEwI~pOe zH9w-h1dmilM5L^bpffIy;ZolwC>8BPvwB-mYKKr3VKccQJdM^V9kq1Og!8Zc5FX_r zPzJ#?h*1VdlK$b2*0A2fdTL&66#|tl-$6}R7-ca)egD*sMl97-3wEYsAFvo&$aau% zmui3iBW*AGIxmFEE|MJZ84>X#I_#g;u~XdvVT!YNvu`6b9SjWxo0Riw*#debP(vOY zfc-~1ddAQ6v8#fb>Rc|iU1T^Z8by&6K5y@1bMl5nQ=aNn!ewkiQQ&{PW9jrZpiJ+q zBHOAwDrj!%6UnucqJddg`$R```C48zm)FHM7R|v02abK!EN{I77Y?mnxoN%N`*JTu+&&?($QNKj%ChwpGcFBwN#U65&Bk1B!c< z0aW>kPj^g|Xvi}3sJer2fHxBrE7ak&&vX=p(JeBKSqe(chDyL7IrPvxd;gyI@JOK( zL#7WuOo(q4TFGZSmXDcE4T9NOQ(4l#MOy^HG^GESVgc+H}Pd0f8D{*9pmX(z|DO zP@vp8amB;PibY_ZjLRTFJASJOs$uw?j!XW}616qxs7zlqxrw@B?S=}Ov}kRXWbzq2 z*bO0eJOZj#`h}^scftXI#1woJ#4*`maKcDRIpMjpXN|(8K^O#Y8zD?1@$~cRJBI=Q zW>7s@O!E+l`c$R)8o+xirr?r+2MFvMFj0d_Yj(u*n>@5)b)sG)*(gp}1I~GPZFN0} z_JjMY0}SDm?j*saLiw_jkgk_#i%X&xu@KL`pd+t8nugPhxdhV`K~MS>@k#fK?VJlt z)IdhoA<-k`)n97kT%Ao%haKD}LFdDj_~kav1?v)GBM@Q&*UX~&m3GdJsLZ!rNDu^M z8S59eaW3jjVB>^%10i^jUWL?xFIN_!3oa@09;o|-$uzr%1{#M5n zXp7-d39eWuE>eId8AU%K`hQtRW5#~mlAzXsFc=0Fo3W(X5XH$3O5}pD6D*6f;@{y! z*_dFOYeAZepzyal+QIrWR02r=8=x^j-UiFD*}L z47NpGtgfOu_e{$Nh7!V6>-u@b0ZA^!A|AI{F)pypPi5{49=&qieasN)5_Rd7l* z4=T^|T-7mqVnGOdkj^FZCUwSe`24sCQnr9W)I5URgHs_h?yk?HlmuK5nN9M`sc#Xq z`bj;6IBheh&B8kP0Nlv296ULgrz7pB9gAvrm=&qZbcBfj0R%~dGt(5%vPl7iB1n0O z4&Wv`W{6(h@diS?)T?K-I4ax>ZLE3*kYMO5kOhYMJWMFU{aHt2X>!XL{*dsCN*AFI zg7fLxpt>b{@$;s2uSMjE^iriS=~prHKIG2{jDgi8QvzNwx~8MC0HD5>7oi*VQ}zJL z@d+$ZIKz?&J>#_<;EEb5Bn$eSoCUQ`;RZ_=5Ca<~Nb)RxV*jEeFM-~YVR7B8j4k7! z>u04M^Pn4MHHgR1r#7367~GXufANhSh`qJ;_9m)2Xr=>6Hlhb7mF>Umn8oxd(OQkF zUwj4O_=|&^X0>rr>j0r};sF!E0s5%qR<)s&1Ad%6mgIc060iG6B-5Lf9J@soD{JiDbVF{Y{1 z0+Qq!S}Pt~AH*lHNWi^GPJ>D=>*kI(*A(NnOe6&gYy@;MO30-q)CS(tu}2Z}t1sP6 z6|BnnYlRX`AZ`t)?nb^GeqPu5UB^h#RQr2;X3|Xzlx641GvR^-gH(|EIu8!#)V;N1 z_K}+TrGP}hQ=kyHXc9&C@8`t6z=#28jz9_Jo)qACw{^6`qC)P()m&65a8{`Mvj5Q5 z+2BY?MhJkG(6N=ef9z-vQp!Y;PQ|nOHm;M;!18?Pw(nDqBOlN4ht&V&0ubj zsBZ?qpRZJ?T6sNfY>*4??%2CGL`JpEd_<9jh)r^Ok-^4AJy7a8_jK&sKwGWgb9=x} zE$D73cDy9Xn6RzoR-kL`qD;28*hzO=F{_vA9a@BPFPmDcmO!{MM!nnJ3Y;Q?;Co zO$Bwtz*bf0a`d;k^;ZGJ0azn=MP`q8p#Qz&sCI{BTHQ#@ewGL2{T&;owecwzYIs~3 z#`L#7z({Q0U|mV}331K?bGP*|Bw;&%#DrL*6rDWS(WnHN&!>9+FN0N3CNYc?MIII; z)#$KNU4#ZWNu@#wGntC?=%G1J{2`kIi#SS6un3Ty(jT7t#gcUyMUW5>B`7DT5B|~C zCJfjLH!sB`^O3Z0<45NHh6Bnx1=UAMmmbEU{pbYnfQLJ^qEG|XVU~ zR!rEigyySV!MCuV!X;v=2~Jn4Mc*#bE-TFc`nzfhp-oUN%Y@Zg0AtHVBLl4IM>>x$ z6)m+2mq1K6ibqYQnJlCXLh82VqZR5CSxS^|vAn(XXA$e7b`BPQ;pLz`?YK8;W zk89%^Kx_#6e&T<0!da?{c>74)fFq=S1kfb41i{BTVd=|j(OL}>Mg?wwC(N$Y>Lkd% z>mO&A9KXz}3eOt3%|24^xgvR2>o~)>CAkqI4%>y{2njdh+^BczpXi)fC4ekPh)=+g ztl{uQ_9r{{=zSe)QL*&33ZZInzX0!s2AEnn>3K9sM$W+LG}Q<|I#3wInnvuz z<`v09U4yI$G(WV><{2|@3IQ;pFw{|-wr%@%@~KW|!mx$PjAvXxJQ7a?y#^05k|4#< z|LLb@sJg7|5G@gCg;$*7kD~ z!?13VWrSRKBHl*ZIyCb3$#kvaS?ku3$r$PMjVTBj&=w6d+hqQ+ZgD-S0&rk@N#Wt{oq>DqCnGUEiNbMvjHn_uJc|-nY>)IWlMZkkgVGcsGp06$P5u1N zCTxb{jH?t;3kXZ#&uCM(!bm}FagXiNAgoKn?d?}XJ%;7tXyG1^p1nFa#r9^3k5Af3*vxgPknKNzZn4Ar%gYs9`| z4ZugE{21aa{p-_M1lxD^LB3fCSfd((MxP!T7k9485e{31Z%7TGu(60HbL&>@extLE z98nRPoV^55(c*$j=6)y!4vH#-k|AG(2Gk0_**Q&KDNtq!wP+TZ9_ao(>(c3-2#8#Z zBZ~V>&-C5I`qp$$1Z(VI`eRXp;c&y~vj4qX4Jy_YbJ)PpeTqAL?eaO94g_NmeF;Mk z)*Fr5?{9aGtvqTd$;yZh3MaIWjm&i&rI1aY-ZsB8CwE9aI*B6aY*1t(a1>wBIW(cS z)(|lRlRcygt;K|B<+5gRj7c4eIs@K!=j72u;h5@lu>O%Ar(yWLxecSk^GQ)IOd1B| zHi4O3*|}?GUrh$LtO+Y%LG_X92f+#$8DbuQA?f$$q+3GKO+^Dlil;$2+CShb!z1zA z&^$DBhC%FTA<*RndD=|U8~AHH2&`a9CR{E^in{CA>fL9jW#8ku6riws+*p@EzP2zDMk_9|0U$K=m(bv=96WK>)MWKS=`RKF82to`F&lE(8yFVq z%j`Auu4srJwt7FjY~X5|W@*3!sOnB)^QWEr&1Q3izpviH8$`KO7f7N-CJzX}&$g^N zqK(2oN0vpFXgBt5|Ra1yq+(dybHceZ$;b#U>_9@&K+<29@dF*r_P*rw-1kY?)%3?TbC& zgt&b=1BFTl^OtiwR8nY5Db7X!!Y1|OMmNn#$H2mC5-j(42>@GM{a2mqn&R99j0f|? zNiA&C9QU4vHPTQnhO#6qrIaWlL7_Rm{p&ft9`PAWd@&x7z(H#7H?tX5AXZD|2sv%A z=LXrc-*&R7wNmkTNsMj?v%ZadYzvFXbL#9ZR*{ImqyhzI6~PTxSpG8zw4Yc{5a8+7-DFt zV1&lhPi~!)Q4jMJB~cWzYdF%RuYW&b3Jt+2wr^tcIQFQbQ~Jlc8{Ib3{AG&P0L8#; zj_it-8U10V4S}KxJqt4D*zzTq^2a&pClI$pNgYij4x~u-_RbyDIBR8@w#ql%(b>cw z)FQ|@0tJeg=Uzay4X8L+aWMkZKXn>;Bvd&@jSSCqxji8GF)3>#G^rZ)4ExWWtEct~ zn&erVW-Cm=xjOoCs#J%dXM<98NaC;fi#Xk)Mggs&O%cVNook+=D`DB)_0(O=IOy)q zy$XvAw)@qi!D>X~TXDMO*>-q9hM?gi;U$4E3YL*QLtw#%JCr$g&;J0v2fqD)l5Q9- zQY#`zaBt^3^0l$S$5yBm&?xmV?=-ftJnbJ)H!fHP*zR!+obWI0pk7dnwW0j<$QHo! zlp23+=VSnxF$JWg|AB3Ip>toSm-#e8(kp3#Lgxo|6eSW=$s(1!8Br~_0Vm^S8Tiuv z-{wBJhANKacnwpgU-ru=z^A0O? zO9g@VQuvp~rE99g>-z%{22jfr8cl53FjZmIdeC(F4|G-vbEnu`SEx;PqJfbQ`<|Yq z8)4{)GZ$5SRCP#4Q^x~SxQILx3c&JsaPIa>!?&Q?Oazhy09bb)nv*@iqDm;idg472 z(6j7^=j1AsV$0Oq=Hx0+`a=riN;(Azw8VV$ zU#aM6r=p|4L4DqUxef|#$^!mbe?xVom|C_QGo|g)0YT6wjFiGc6S6WIk9F>uH`fY8 zrfldj`5Iv(TQ|5tY!?_yK%@C09*7)iWIjH3GhR;#slV(we-MkQ-bQF&%ErweOO?q$hQ+H`{34qur5fyPg z(+Upj8qc-c(wxdEHER@wK9V5p@g81sK&?x9cvqG?C?Xbh4{tWQFSl%{tb^B=8b&Hx zgTzjb=<3OK$G0KiipOc=2ANXA9}&QA-Y;c`6?$LE&QjVsJiO`1u9<=Ocwvkk{Ps`> zIBl3mb+v6az?cuG7-U}r5;dqOj_#UsXfQ>N3FXefB&c{qq2!pZgC_Kp6jCrJzBoYm zFM3CZtA`{tV{}6AYDt~v=Gd-g(!iXlLcE(%Dd`!`T^XhWwg?0lB>928CosPp*Oi5` zTU|!gIN|`bkcM57iNb>L`<3T0Mg@^NZ}*G6p3UT`@fQ+X-EJ33^&uXn=s^=4ib< zZwx(W{`bBZnI{Sg@hlRbBERT^UHkM_n2HOs|k6LqF>V_KCEoM#Bu-0|3i% z5mR4*GPdRiCw1*I;W%vNvFDVL0++_K8yP;C-V#^0RYy;Eu!t~0Hda2=wV(_kMtrK<%UR{fqWGT^`xEo}Q6D zkQ@92W+Y6wd&`f#l1+%Hl2NNF**OETKzO~_Y-5B%Q{&X9U6{}3oXcp(R}ujpf4vjyi8k|vlT z9RWE}QfGX8?uRYx3|`_fOA^Z_wH<$=Ypm>XuC_$6e1m+dZP48!6=9}CQ`D!5 zLykn;1DU*~1Toe(2&rX&yfKbF4@~};K9v$?n5<9la6`u+)gvB`OV3->15E z%ZBT4E6t?!)eVENu&zt!zyT9!HW-onAVDJP(KIJ)57PRQ9pW^-m2(U%E!CN)G9=10XP z_YdNQnHZtW*GW%$^AKxwZp{0T3kHoys(A)uf(&gyF4t*lXJutWe=#TzE; zqQN-+`L3BtNKq;UaXLB#03OL*CTDNSZ5c73CP)t8mI6+zN9SxwW7Pgaj0dfYfgC+F zb6@Cs>gz!vt`F=0jSBi3S!MHFu@5r~vtd1&u6?Kz^C6Ul)f(zTQFI=DK#|GVG{D4N zI3$puA!Y2(p#4;+N)b>(3J82k8lT^_U+?SIjgD;F2I(ckyCJzpvJK+An2L8owO$xs z(8ipMNz&Ocs8Wgsv2T=qaZ3gR>=O>uuYfTeDxB^3OHC_Ui{cy+PaQ$h?+}T?Knx6? zg7R*P*@?X-U+$_j?kD;CJ5W?iW@uXLRDYtx0yqi5JIZ^%(sg`kvNxC|t+I6;Q$jGz zFiQ_@5|?D6TU($sDq9Yi;;nHuk4t_OYD(lLNJn8ZrWelrMvu}q^a}}v`fzUsj{Q|! zu}Rm`#^SSVJ+vjPRO?RZ|a|Kx4IKWivD*M1+huMIRZuNAK%x zd90Ai!PlYt0i1mxlA?>d8tH`_$CYroz~PdOM>IUpA0VLOdhA$9^o_PIiVlka)J5K0 z3lq=2q-#7o{|KZ`7%;V7RyB*&0Yy{}{e*1u2+9h&IfPvk17yHIanam_Av6b zpL88OH&#hTIiW1a%m%Sxv`HxYgp^OWU}yYk*QPnTvI>vqR%>=`(01@PQ?MiH0y+s9Vh}mkbUE3BG!SxUW>zJ`&F9>GeXQhg4*T-T z5R@|0YrFREU07eP^2meYhFKv|i2VzKnBGN#fkG2D=&uZe^{MnDPOO4v=(bS!=D3)| zmhWlSFQ$7h&@V}vj98t|us6Kxx)zL;N5!`9>1h<7h7L&xYclI91hHyS&q0nI%&=!( z-vxP0Q?za+){zo9kQa{x4z@S8nv(zKt)_&CV1j~pSho#^@eN&X$%%8aZp5=kL^Y%r<28)fN4Dg&7*}v`@ozRnM zj^54Uof7`~P1gb0^3yWxH@pr%Fe9JHEGv<#m*Xs#!ZcKR&%^6I zJdAPB&F$xD5deev35fzo5gjnvw{-2Etz~sQTVolIIL=Equ!!UJV03^>%*456KI<;* zDj~5Xa%m_V{jTddy`xGRqCzYX+7oQmTe}+hIHzV?%m~3PdYF?lm%h-_?lI zp0)?b3%{*vZNAJjho#377>r$T#{Qipr-2gy3rs265S^et6kcmIAAyKyf9P7F3WT?; zJXi}6lnEk|wV=R}(feap&y;I*oJhihu0u7?hNH{{^)K?n;q6^xnI%}ZnPN(Kf=7ym zs_Qd_zrF;W5;%@rA6ZJ@Nc=mxro<9g`$b(HA`8nVi9U2%cc2l`!I|7%$&kN^va8{_o@gxAvaZhWc6 zDUZ#yUX?iPll@Y+PwItr^mKNBKDK3m-;Z6u1W+>L;t`3k)qAdGzAO#F)|!;Bhe!dj zxZT&apytyyLFD)5*(E2g1Rpadm5tI9kRU|~At5`$&cV_D+YIelNNXYCN6c8GPh->h z``iWzu*@iFFlGFSP_cV|*Y4Awmw>7(NWo;)JCRgBCkd55BGu-)56tb2JAiOwVu@Nv zrB)D`4|XkS$96!ptSXD6O&Etg7kC-|Fgd`7X8777yiS{jP$UK7?b#1^g1H6iUMdx{#poo7$=V=Ng{@;{0%y0lZ**Yo zmNErMfabBT8m;gwf^1<$VNFHyhrm0vuJPktec6ar__sn0il9;>KT^wYEF0KobfkA- zmK;&pu4SxBS*gINEJlPGspPWZ<3MVWVx1?tM#s2=l~x2yjJF0qPNiPPoQCt5R#z{n z0T;L|C)IpC7fml%q7m{$Y>$gS*)>yMOLBRpWkew~Qv4QeZYx_5K(9v{&+`LBPElSw zta}#x3OG(pP*)nRg@KEl9NxWFIY~tnu#P}gZZ?t5nP~}T_?rY?CTc>FDFQ+b*YFYD z?-}zdw&)G6P^Fi=O^Q20EP_Fycx-%=20D(Y+iGNk4dEy~vO7m6jDZp;2qwmfcp$Wu z2!aF+Wkp6xGL-a^lOEN^xzOJRphhXJU=1)d`{?d;f?ggV(;x*AUuuU*^$9A_5Yp9{+k*p0J;curc@(ccP-NOw@1y*(N zQ<7t#k0q_3&tIW8*yGLA#gmO0j&I`?nG_3I2@1t2 zz6;^x_jfNHALbm3w5idXQahe;Baxw|?`#;4L&(-aOnsnx&QZytnydLPY*#*BgBhLB zJ%8*Ts~BJ_Kx*kFz`3M2h$}+CM+?UZidbi%2f`8#8X;&Rdxq>EJn{i~ijPe5fr#UP=x{hU zLN3ieZF;AfRI0^s#E21|G&lX|^iD%KE=kGrgQ$qmdNV&Zz0(M_T9`OK!U+J>W9#GH z$1AM}28D_=Yeze4?-UmL3} z0T3$s&}Y-nY}O>Z64sY6`b77tiC`vm=dgJ}Wm8Pv)zfq6p}A(sd)~t!)6fsZ_he!l z=(>8=C%eB`kX&YQy4<>z;Xgf+%f>aR(KP1r4Ph-ZGJ@7=BV&`t!AM3S(FWV2pHeyM zSf_XI(Xe1o5f-SxZyJ!E%R*`Zv8hP+J&H;2c1-V#>5U8IvjHO${U;tvYES=E_s+Sq zCc~lzAYFOz;G2U@MdZ6C*9Yzc0Ga6{9Kf|5=hNM}XEb@xP!jO;W;L!zQ9b4oRrSQ; zDOVu%1G>($KhxH21W+{MZ&JJkbcPb%&vs8pl-e+!T4%k{%OS{%+#X^zh=dqtwsj{7 z8eIXF8FjU(9Xa9Wx|^`VjYFvIJ0U82R(E4j!NgSw8Tk$x_M$@L#LO~54u8Hohng$T zcdB<%x7rUcuU1+gg%y1`Q+K1G-5Qm6RAE6e)3%=C-)}|n< zEYz6S8TFY=m~1`H?xg9DEXWGLu{Obo&56-0+k!)h+$oHw@Lztpd(IFQ@LUpEgrSAZ zxe4_9D|2@#%8VR4VLKO*1h^qD+!BjN1ilC}g2x~q1w+tRyLYPX-&t%G`4lPAV~+%&UFf<@&X<|{MA6fcE-8+uCWX1$hQE7a} z2pT4k+XaT82&FKGD(GB z-fqX^UJ}35efD(V6hZRs?)}CF)0KOP&C{6U%3V&(UBTE$zNg0i2Y{QB2YO66$E`s7 z&XznM08i+uP^}`U1Z+mXq8r8WCXmRR2Zz=Uug5rFtUk}8M2m8ME@ew8$BqaOq)nK= z1~n9RnrPqcp0iM<&b^|+Xz37wIcAuAuY2#@bZk``)PNBPt9iO`5TPX~R8V7K00MB0 zup@azaxGWHd-b2Tf9b&0d{0M< z1D-Qt63NxGyONp*{cO6<7{~<}I>2gc3=iw)-OZrsOb&}ds`B?KkuJ$KEgR^~0XIcl%V6K2TDM`&{C2NH1vmwcT@t=~Nh*223aV zoRk7t)I1P(A>R9owgRO(G|Ry9B1%Mb-@dMUujyqL(@IXIWj31XIGr}Lb z?A~3h|w9n8DUji+~Y&ax5>2Nw_;uIfXZNA0SYg zEkn`}4KQ>>IGEL`ZH4YH=fIv7bDhoz9mgl30#+2ypP=qd#DZkqWrZ2DNavLaCm?LZlb0jh??B2hhgE^jO^dgK~13@|TKPiXS(%a_XQu%Vi zOG8`%)&j1C+>`%@xw{l%vl>-L)B}@pfwcLLb9kxPv}_cf1+_uxZ*qJ0459s`RPkE_ zDaj%_65T--V6quCr=pY&^`I88M}!#?oP6g`GuQ@exf?7Q6q=bxw#IGe?E&j|#1iTxGc)m<&#g~GPX3ZZpA z14GIPK#^-6*&j(Z+&x$8Om*Bk1F#kpTaZ#lrTL*A-qYQv*_{X#CLER74<1>m?}`$= zl%xTz)g2sua4Qh}qLX=Vce~&%Ai-t?tQAHE@?W9;mu{o+Z8ZyONf41VSC&H!$pvDH z?fzejQ)LK|0k!zSGLETp-4I+nd!jtm* zd-q@yU}TQH8S)kPcQ*mCtNn~FY{Q(vj^9Qx=ox)|WS=1IAghW(52d>11Kq3VP&Y1H zv?`H+;vVbyHA+YW})V*vLTA{}t?tZM4=XG0ISQ$Cy z9tv1mtW^_#Z(lf(4oK3ezgP%Af}NWWl4kytcTk)F(Z57F~f8abd*kAR^ixI*<5*=D8KkJ#hmVjk4CNOonQis8+mBx>BH|iJSQ3a#@ zsxU;c1=VQGWE|R*FoTOD0?7NN`0dfB*XzK|BB@|gZs(+ch?9Rx*rXv0no|ejhWBq` z`UoMRxO$O^+C}K1Q|QT0Uk z9$9E+44ib?z0!ne+M#OCD()^d=;26tvqmKZySZPf%@Xs4-^dL7vx zBo9Twg?9K>yESgljphMlWkgTbA%Y}S?*oH`2=wn@rA`gh4 z9PikzGEs$AY_4j}&7d*Fd@`X1RW*0qR( z3rSXSn9dE6@S)s-B#iz3tqz!EM^ix39G19L(1`*jMX$Z5XK)D3DYaYBs6$4c3K-zc zRD@Wr^MS1v=Z)shLWbQU*U;ZjSxp`elVC>;*F`JB#|$_qf60RrJc zz)yzWxN+~#e9e2Zp8eXOpLXs0VxGhoEPJ7H( z#llvDdjTFbvQ8Ml_Ofsy$QbTAZKTj!^irj~S?c0-;yF!XBXUezfIOcVIC?$2+kCXZ z!7RvS`S*%&M%$L@3eK&l60LUs5yCt^LQ&C)1AkW06$(rSh&&i;5o}jC zIEj@~>`|<2BvldPg)KxH5!f_^cvj#w7HTK6H-=L}Or8x6b%g>KiH;z$2Jqn!h0gzu zWIg+>HGh5KNFt3LM=nq}7O3_#_DEA?^QR1S61(0Q)u5;+BMMrM?NI19I7l#2I(1+J zsCF^yQs51UzzY0-<+Oo4vwb9^cBeJQLl3U;H4_fEPqCuq{6~QftsK1FkbNQ&IDO#n z=8s$wd~vk6$Zt<$PuSp?&czAyKO?t2vs1PZY}`Z7rQ3&ppXSDA46JZ%^EUiJ;_S_S zld=RS8)1S~u8X!6yDMr1kPvWTICCH&m$fTpqwbS#$k`9c7LnkdGuA3k6lu*Q2Rv&) zl%|_Dmc5fd9y}jKZS6W%1g`l);!RCM7quJ>sEHDuJX9W3lnVxW!i3T>Tn0<$ zA?e6Yci})uS_HhV*hQ}d3J&BGRsZC`JKE;MlrR?OmvYR+_NG5Uqnk{4xZqXfD7Iyr zh&6>OIkXbP{M5j^(o>)fl(6kPC@nfz`hYTcCt-&|MVHf+x(Dx?Lv;@mr7#FSJy1E# z0=NeRzz6}y6K<&9Mb)Q5vnqlY=KzQ%{}0TItLH{0Y&I4q;kZHdR#z^mJ{2Wc+)XjM zMsbq(FTQlZZEbk3$k?D&v4mJw?h}Nqt_C&}`#<=@ymglin5|=%1p?l=ZO?rs_uu~Z zZ9E^b4y9v3%OXN~*;6kcc=wuJ$9QTHunwemp|~tYJ2^;p&Nmh~SF$KSIs$Q=$=Nw# zv^jhm8a`D3`0+F073m?s$aRoMS>-Kb7;`@;Ez>Q6(9H)7uN){V#f_#V_!;gBv}$~F zuNv5A!)GZ)xX<)r8m;!(fgT_M*rWmti{X%DMr=>79&lP8{>sAP!3jj}J+lpY??r`# zs7r9Fc#x^;HFQX2&$)Ko0aJAwkqjY#Kyt zR`;$YVJ+rHheQE_`EnW*|7)`o&*V%*Y)39Cx(kA;*-p0e_T;}oAlu$5ngMQE&>lXn zhQu;)@VWk9l9bYjAt~!Pg~XUrTsM%afA^Z@Dhg(kbviLTLi%&W8h}*5t9S?tUO&(a zofa4#pdO&0Q*}jQBfcSD7!pA?dcsgk6L-TyWQUJVLhiXJ_uM!zvr2~|>#>6}FD9dy zkhXW?UPhxYuSH4OmscWziRg1UKq8^$S~m@NEhk!T+_-IKS7Uy5MwT%#u6ziAStrM4 zZm_f5A88K0eWC^=h_F+`>l`fg&3(8AiGj%*PdyegPHcs@3}n>4v<9$}xXE`%G+&&_ zSuS+k=_>oEB~ZmBj<~hEVTcKap-bJDV;mN!dD}prFk}K-X~IB=_$I7V+){asNz`v2 z&^vG7*m*{RexYjsOeC^wT>)o{@5UV)&OrbeNAq{~rJCoGB~dSok==rCuWFwk*sl=L zb*7jl%q$jfuJA)8mamSM9kh@@Q>j3Kf3@!#cz?dtw(A2?bKSBf5iHsfEoOUgu>z3O z1(u6e`OYhdAby{xm7uz^(i4C=Vj|>l?GU(>8s6QL?MMXGef9`2QgWF4_f!x5u-(Sv z7$P4q7a!l~d#gumaAYa6X^6Sfx`q(_g{I7;4II|?SLhd zW(MIG%icqTa#6%L&_zOe8{ObX2fD@*XSOyi6A%YKw&6Ln+T$CZL$5tC(0k%o23J!_ ztgxKL)ddI?QvFX3AbOM(w9}{numYIj$z&5;u0z*RhM){+&?}+jsjJ~r1A8Sed8hb^ z)gui5$tA9uYHqR0=$Imt0o+~0k%(5F?nMCt!F`WOBdEAKsdW5IFDBIt$Zq)hVI)CV z6~)gEY%6x@i#@`Xl26*2Z>rK!Ei!cY_N-7F5LfV}>bqNvtucoMX#i&j1ja89yuEO$ zuHXiw0GT8?d-KCvA}SXt))jXj)G;}IDTzu|s!on2qSLN}V4kZ)!B++{gkO5}9g*_--#YJv$_Ua?mx1$&w*pf3JE}inj-Eo2`(w zaJ&c3_p49E$>Sg$1tpj2w;p&uXx>80AV0`Op!s3x;{XD79p{JDry@XB5K;wmL&sN9 zQP0aGz)4B&YDh<>*aw}?)G70XJc_;5(O`iD*Rv0pSEIv%8~5w)WcEi_|d@M zCr9_>`a;6*SrX3}2s4gG;%TCKq&VoR|G3Xz77;{){G7&?1B6!fe=@Ll;i&8Ia)`c3 zdQyo7JD-}I5eaw`R~7@)L(hDr`Wggqgwcnp*nm_a_9#ECK9!ixkQJlV;Q>{RqMub? zgRt2_&AOJ3=QaL}>Z{eKGNI#BlR&}=t!SEjKOgv)t`wY_3RvySSH)l9a4rHa(&8MSPoG`TSlFikUbKC@g*}fF(k}<>PSHfd zHCnFvXKxH0S%L`>BKy_A_9iiiY^fcR8)KyzZChC&jJ1J8YxFH(%EkyB|9W5~!GC35 z!@n8$cZ=ud-4OH@R)<(^Xukd1f&Q@qFrdWJ1LhGS6Bgk9?*`shc(qra^x9EuG_@e7 zApV#>5KML_susi_zfTZ1g_e4T-mH|nW@F#&EljU;1GftpqdAYI*$|qE|*Xw`Rsl zXA3$LuutM6hc@ho2c>Eg5&w#c{fR>(q(7j$IBpQx#>Wiy z1*n%+4ncUa&S)2*d=&wc{K(*2y4bvhw6JB%TlpvNary_E2w-WF9sbFmS}xeK143FtWVKgyFR?CNV(OIn>QD zd2g;xJ9!!JP?+t6!R>`R*SS}csPs$N(f}2h>G2WeTLw)RysaIoP2ML4|1B#8wBSs) z?@_o$@#!QT>%!k<@y}}hdkR?P4Prx?#dJA^vDPUPB3J|>GNRn&#qo)QE11?)mLRqN zj>06QI&hwpg;1S(O>_irBkX@RM7BdJgdysYCk^(wc?zQ-h~&@=AOb)ZJbCb~O_~t2tt%?lU!$bs+4XBP}iFlJz{#R&kDe!_UfGD(YgBMPWn^@hhyr&Lg?cNB^1gD>;5(W&`l*JWR>Ml{_~5@lkl>T1 zv$3E_ees~SRWvX1&({thp0tSeNBFQ@j?7v3S?u( zc33qShFXZP3zTgtSgLhtZ?-ERMvqDcE_oIKpnDl-XA$g{K;XKBLS|Y#Bx75dEcy`cSY%Mu5orX=`tXdgBJfS)u)0^90aI^ z#xal7_x)Qr6;|z~y@eA~vcwQmq3j-@(&g&64fY;43K~Fx{7t_@Y-^nOcD9w`*GiJd z^bp@p9!YT(1YUPXhT+=C*F$@tzk`cMbVqNF3~7J{1f8UEwgKPy&cSV~j^_hsq66b~ zwtXDbXn~@fMBj}TjrRG$(k%&aGt!c{tNMXKQHv6z65tgXKv1;ru0E9s_QyjwF2sfl z%DZyUU?%L%Cu{>U5Xr(7hyi7B1Ov$`bN^?L`GishkIMTY$n|1Y`2g#%&LZy~{V zFjD>!Mv1i~xCY9Q0tQTeQ-Fzh@ZtDC3y|t~9EIi=%6#Y;Q3k|&vF5;)QCF<{2G!!y z&7&J>XoXaQgt1s>GsnP@Z%ZKttETAw!Tq`I-4BMrcx8>yvN#(BN@zSnB~4mEoEr@%Gr-LsE;f z)l(iK?X~m*`3?7gnv?ixe zO<58F4H^l%Fi)3znbYf}_$S+(ka&Qm@fRsS0oE<`EnR1u5AnmY!zanuwy*ah?5RxTklhYJ)kQgq(=D)vNwbvEP&&udFj)RN$Qqf%qRtt*Sc<65(Xf7 zDjY8bf~Ku|f%(kfAzSGu9x4TK6>=VuhNPWd1fRSy3u3b%W|q&~Zk46j#NxG{-AEw_ zc~71Rv0Vu3Lb2m74faVyQn09O4GI2>ZJte*0yAeCd~81?l2@!R4;I-4>vxb7b8IoM zN`!IoHbR&>LNcBf8ohz948E;fa7>zOCt$ox7KU0?C~O57(x~eN`d0_bAmQmeS*)SR zz@mqIHGXceirfZFd)6Uq651qQ z3#(+&uyRS-aGCI8C-^2OplqitTW-99x63!%_OkVOHWKk8Lgf4CHJIMF221nRL0-F% z9w{(Uo$wUr+k^YBUWhBbsVpFD9}luB&DcV&(57rfw;>Gm@ATx+`w;U10s3T9^pK73 zR!>c#q6x!`yartWHu~>X&nHnDXJ6wHh(A4@rtc5-&60uL6U7-CU(kPK`TSt;O-bRl z;~^wm!!Qv=+($yD9SNylqQ6Eo75;Frd+-c*DYD?t@K|kFxGxPJo>h#~h-8H^7$dt4 z83E~fen=Yy?<(6WC{k3#i-p>em;ge{^PHD^(x{<~#M?qn4zGn$wfUp!E6GHi3^Wjc z5a4vA-ap>JN&;Ap!x?h-pt~VD|6~v~r1m=A8B#xM<|cna2a3+_WQ@J`E7iYFpwUjI zgi3uFc_TynY4xdqw+V)pk_}#^#8N-oz@3Q#SEfV84;Gj2zFK`M-XAF4sK!HkMk~ns z`JlS>jZ2o19}^wlrQ{8gcV~L>7lXMjRZin6Fd{NC#Byc~23m|fJoJ4`F*W#QZ>lHw z<*?ra5Qb9Vt8VzKUYvs-a#o=o5Naxc?byHW%}3P;uJNi7-WaMxs`Hz{Wg700GYCTL zIS+w9kwlZjIRScbgrEql*uXpyPe)zCbbi~L4+Cq7eyW2o3rjsxgWr|O^zdd0@DMkt zM^o60o!?h~I|$XtVt_Bi@SoV+UaP*Ou_)p#Ga;sGl!Jx^0xm#Di|H{ikAgie%V-QU>PT}13 zjv2~Omm8diKL1CCD(Ye)R0J@A6hZ_MP<1)B>O>I%guH8F<3i0#Ij&bv%jwg z95y8L7Jvb`P)dBl(6&|8tVJpaysG;q?+h8Ul!B)k(?p-)N)DyH+ zteNZxG=Xv8#UD~VwKvmZ2aJ%2hTH@iB#|1P)|+WjY$GKD=0ME>4ytqdQ14N08PtVa zjZh2X%^FcD6{RzVy6Y-?P_m(ip>ijJme!d=`qRT zCpF7=&-0GtLXETx~_OhWIs)MOE3V^jA2snO;kiuiF^%dWYm3# zIDEy6t{j@|>)Wo>uIlp@P)Y3b6%bJD^A+^k)qTE#QM;zkS1@bW_W24{?Q?y;f?d0= z&sT72*Z27fZtaHtUO`i8H?q5R3M&?pLLJgH(+w%uCI5a`*;3C%U{=Vd8c5fJJiK#$Xm|MLw~zp@D z_?Z;7Tq(PTkd{@=x@D*Yw-T8N`qQ?B83I6d%ei$Z^PcFrwrNb)NUWOCO;B_-xNYd) zEpy#lQ9v#hnPKIQq5ltLP~6x#nTls_*;0vZp1_Kj z<)HalX~vTpQ2uJL=w?NDnv%hC^^Zi-$3zqHE(*U<6x=yf8F!E719C6;&7mHRLaO@t zp&sC2Tn9-Z(ho3y5hxbpuAyE~I8kmzyOll=zBYlX7wOkTb!Asd-nK)1nFg(z{Ftoqyp-8n!JvYfX0bOB697HSh%w?f0;B5h>3ej@; zHS*||gQNuA0FlD9?;AR>m(!7`-CsV(MHL3A5A-5MKa6krz{b8KaC*anu!xo}^({wv zaA?o8OqYIBeip&#>?P#h7hcIp)VSsa{RyIsvSdgN1+1p8Mh{h(1It>`<)gBt`f8vX z4-b`OqAN7BfLx<=hFPByIgj+@?PJpebY(=4L&P`Q{$lwDhM%U7qH_Qm8Q`g{JX(GV z;gD!B^g-j&Dc8-%hWer|FfEU_E()5i6j5MyL3)fI@5h@ehT#UXvS6AbN_FNFLv6{j zN$FY2$tZll{LkelD!_` z3D?x0Dm?*vJ1+{}1(>^9ru%f65QYX4bu;S0hQZ6Ch0nDAy34LswEF=^|Xx6JJG!`CP?shvPD42RP{1;yUcQ&kt1= zI)Nplqp$AcID)*isl8BrDvL&F7@&1W@I#Nsf3f1X^I;{Sk`LekpbV(QuMKT!%_mlw z6)f9ah*tq`BdV(Cv7#gS^`V1Sb(uJNR%51f3FXH0+$?I2B6vylSU3-e2_lKkEuu$^ z`*8e?yxZ31>@S{h%PRN;LL%In!Q5vSi7)Dq%;^7ybT6PI*tY-8q0V+Hl_w<*_*P%$ z)oS1F%e;E+JAIkgsC~C`GnwERV2X#SA#6QQ`Cj=$mQ#rBRztjpyw-`l?^l?DCYLDO z(>I098<{_-5C}n9Hj$%1Q3F9bTd{sPbkrMq6QjR-cK$GIf`W)ZmxOCa;?KDkmM&Am zL1FnWXIi|^A%t|>hNnpz$$P2)*(D?^75V%S_&8olhVt^zR0mMw#?0O%W#Gl8>xR5N z-6A^FJ~jwH8Y;o0Wz$0`9E>sySh$csE-_X^B?a~>aiB`8Qt^J0BSTW?X4lfg;=4y< z51L(esyWuIuPlxoa(u>Ly#?DB)B6Z?t zL$LV@Ie#V&1?CuR(KUeb=yG|he{K0D9!wNg*ni; zAsOJshN1%=$oWNYA}I?nHHcMczJxExQvY)3h^8A;xfrrdfEZNV!j*bR%gc%87aVvO zNUFmUh{9hDb&VWG;7$~@qA)_O{@CH&T#1g6FoWXmLxG{2VVlS8_74ymDw7yDoceh{wo5b8p=SG!kUK#W49q$?;Kzph-aMd&=7EKa z)dV_ZI>!(1Szl44NeWMI<JRo;>__-OQ4Lt@*CvAAts* zZ&Hwo(KJ0}xMa~6>23@WU?H1ffbM{H>hR7iYe>bjl9z(bZJ$VFmEW}p!+7iVf>-T& zQ8za(OBjUhMZ^TTmHNA>IHwK&(3yZ;_A?KWnwz)@r7;gp_f--*?16IDe=Ib(R^v`R`fXte53ZCUoJ z=ASv-dmv_ZuXw#IM37PJM<2$ryt9TYV$ET4VepJoN=U7URN(C4{~S-xhUfJCB_ut> zyoN$8h)n9m*15x#TaEA;9i$$0oKLBhMgDoix4t0@r`m}oE<_$SFS)YF8oxNhg5I*_ z?fmndQM`4pS)kL0i!8_M0XrlQUZ%RDi(hq>$MkIB2g>tuw zy&&E@m}98H`NIddBlwmTu|dyBh%OP0C!_;3?}92@IkKHFPhqh~sQ3HEg%#!qKqb&r z!L|}9Qq!_OIqa+*Nw;!1BPkOmeXvgr@3st(jx`_IK|7VszoCSm9_|Y0j1n-YSPxJ> zlFwG)Tr^z1)yGS%86YHLA!_Q8dU5}DktiV5LZ~c8kPh)O#UiMt1vhD`Z8!gKRRBk#~ zRCqTKC0wfX*O&Vh256N z6=J|~)$qS{QvE0-Kgo}ibwrP* z1{QKBK42Opis-z=2yogEol!Z@k=)|7{A~Z;L`n=gk_?7Wkx{ocuO99f01K!K@C~3A z2FM&myJomE;G*SDw+UCdWp!kT@I1n06T!d;a_6O9}fIJ?f z;}Pkf8?NqjfWo1RMFkp%p@UxDb=9X5e-#DxtyEJnB2=&6z*I3v9GJXtm0e_}j2o&? z1(3(Yfg!@`g0hTd+*o}oAL(ri%Pi6e5uh_}8m`_fg`19O284MeY6H)^x%yOSn%Ewi z8KNdZS4p|0`c&fTPRd0bh4Kz`uv@E7Mc5P?4i#-AT?_zuw^g4C)g5|fKxXj4@yLvC zA3o*{TeA~xB~yJjND5LYQQSWWzxh=PV8s>zF(noO?8s{jXr{!#r{PT%4Rjq_C%Qlu zPE7tC!~ZSG-h?3REzq;sQ~oixBqkPhY)`>pKm~@hW`Nst#51QP6``fq4_6>>> z$pG|h5%xq$^X}odWM6k3)v)Be5TMDWI?_oq(xT6iA7sGyuN<`0gFvzV_5fg(;o1buQjTAHfJ9W|*4= zp8JLBDHZg-0M{?be(XGC5%&%EgjneiGy3pD(cQ6tn8x>~wNO$b2&&xf1rP`Plcrfj z*>LS08sgUh zGHdip`INE$#R_Q>%xvIQ!lRDiqw3+K!`-)+0SiWGHHif;U0ySKY`CammSryJ`glQo z()Sh`0cI%33DE+GrIA6XMpfP99pdA_k87Vfe)GmcjA)BL@X%AgyutZ+64(yoQq9xA4>tVD@ZYz5 zj65qy&3E=EFO+l<*Phv_UGfd_|D~A@+z;tM=_!15cm)^U_UxRW#b7k&I|z9jd<=bI z+?7HQmCu#U({+N&7>Qd_eFS4n?fKykHiLb7?&G&C5XvsPjQ1hyA_R%iR*`QJD=!Qm z%g5*doKOEEt(>n&jWwZi3R^`n#oA7lv1q{5`gc-(teYr!VWvfzBXV_C{NnIs|Ec6> zX_7r7^UhiVtsgmVO!0wIN%wt|pQZiO{_CZ`OQj+Cz3i8A-LFIfL%v*bqb`2AH}BEr zZ}4nI88(-bJV5@mPa{ZT>(%!!|FyKl#C^&k6RhI*zdx9oU09lr{`FsX--VKA1ri1Q zC8(}&I?xXGzt)>A(Ko?eqMVL9UBHWFetmeU<-M*Lqh#P3I7V$>g8*=J$bRC-3XSE^ z{l;!7_F*ATQb@rA0JBLQ?3=sgs?aSkMT%WOD3IrM{ad@`sz9a!juhN0u-gPu^X=Vo z)d0s;@FD@hNYXR^PKjs?q5=|k3b+taAV%i0)L7y;=xUaW92g+?@0A!!83b4a6;VSa z`?BNjmm6!Eq{t}U1&B#2KPWNQByj=L;A2apA$t@3u*_KB_5&OQq`;vCw)s+tu@)U% zLT*dUMKDBjUM?}#p-GBxH0({}{lEtOsMOdPVKA}nL-!tD$d5~m^~e*D*%#Y*@H?^o zlQLtupcY|bXH!5x?e>-7F5z)nL}{NLidQ$FJ?i|l#6|h6++s}?-xY*Xf}fQbE8^U^ zeFj(;D|FrCSIdlbDORIj3|YFb=JXZL&6N}-DIOi* z1@p5KUeL;jY3T&8R$QbEgI^9G*pyH%`(Xu1S!!?6$-I@2H;9_6U3HMeSS`R#MqC#Q z)ZkYodx&XYB;EWBF-wYS+EQnczdCU1o z!7L$H3T$F>PHKK~@3aVyLykw@QVTPl-&Qa8BG?Ft81Qr`_$A-?UG=GakYKJC*q+!f zI=c4z>Qg~MB&DD(g0itsQ~9;(g;g4~*apCdMAitZvHyqaQ(?)1jtC~32zS#RvHrM` zsR%U?*hV)PTz_o-sX}cd5*MLZXS2nQ33bw=HuZ&!n4+N&91gK{1#Zn%DR>{<&x;`* z52+Sqy#$V_>mTUn#n5hJGo)CfqFDsD=!5;d7`SCBQYNP}2Qxse4{hqai7r=o+w&@A z6Q$z9OQHiTq&w}o>W4RdR z@7PMSAf1PbJfLK-fDUFG$89PG3Ta--ZoSgzAkCeY2603Q5gL&oL@cs1`=guoN%Gcq z@Zb(8agIPB3MnkX%wXal+tht2lbkRF4UNzM0-w;LjxRIT;#h^JN_zzGXy|;r%vdQg zQBG4}lcN}GoKR*gx}+NJL*yuq=ObKMVyuopNhFTwZ29QenJ1PQiyVbPdxRGXDSQw< zCzTls;*NA72w)70^54m2#)32_pJ* z=&fSyXr5MLtOE{{Gc}_1th;fno?c=sM=4wo6U%1>*~L&jqs&;iNCZxJ)HoV7ZuQI( zW2vm6f-Ia5m=XNztP*2^P1`6h;^gjO38a~4Zz_qCMfVgu5uO2-&l2^*b2c@3Hj2qp zT0&Z`N9~>dndmX~f~*6!Cz^rw7C~lKwcy-M#f`ir+?L1_5$Z(2c_sqzTBoPxx|ESc z8bzfucHX8pwS>B9%vzKXVNac&TNH%}LIBZ(^nrr{6OPpLH&sNGS|nV|fVnAsbC%Er zW$&cr8X>4$+8T}x;@G}$Q)PIF1N0hL2Vejb0rN=vWc8_d2{5@usRD?aMtS8^)u-YK zne;d?d9V_Dk56x4st7-OtgN8z;7I2L7j3GH>9BAWv|Z}-h)xlCnip?iDgq&JkYFA{ zUqs3;sXi5%9}3oVqBW4HRBA4*K9wL=5FN0=(YTr(tCww}mmwASv}(N;F*KLIjY(Pj zC@>*;(qs)T-=wz*TEulH;|$G;+J_MI93D|Xs~M12aK)zS;(MJ1Xg#Y{*Qr>riiV$l!*ZVbK8Y$~R| zsjnYF4>2xXEm^G*yL1WMxK&}z*9tL zizGoqsoeS8rqPu1k+sgQ+w}IPqDw0rw-p1qO?1I|C;@zn#0t%z%tyU1;2ekGYgyNC zg3?5-Lcl`BoC2Gv#Nvz3#Eo{LsSu?^A_Z>Wu<0Eg+e&TTn`|24)L;>oij&o;HR=>X zD5vQvbZ7I%P0H$_hort^q7cf*aC~Gc5IT0L&H`T1ZrXHk&p!r&5;t$^4J5|KG!T`4 zun1VG2i7f{j&5?s{E!3_LV_%Db7(jnc!uk6ilY~`32QdbmipV{L; zyrt*&*e9A}9tvPPa58NKJm*BI>pfJR1ZYzS)}-lp_2M!{`O&v;t^SHYdI}wrA{`x9 zLLvLMe%yVnc6;?Vj}kUZ10)hE5N>REcWm@k5tNikNI+4`Rsm$(xoOknWDG=laj_A` zpWo!ILi>T-5_x|rpKAkYGK>w+s~MT>(J29qfw8ksj1_u!m5s4tJg;q&s0cDGcCGr| zo3=NdNrbD3>8*T5sPe7#*(LzLjx#OQa#K;M(el50Htn&zQ=}dKgakb~<4w!X<>DX6 z{i31|Bm+*!y_<%(Ta6>ac(L(?OξDcfv1@i*tzrS!qcrN!70<;?ra-hfafFo?1d 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