mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-08-06 18:05:47 +00:00
**New Rust modules (batch 2 — 9 files)** - src/deepseek_adapter.rs — DeepSeek/Ollama chat + DeepSeekProver - src/ene_cloud_credential_manager.rs — ENE cloud credential + node balancer (SQLite) - src/enhanced_swarm.rs — enhanced swarm stub - src/gemma_integration.rs — SQLite task queue for Gemma 4 model tasks - src/hyperbolic_encoding.rs — Poincaré disk math, HyperbolicManifoldEncoder - src/knowledge_ingestion.rs — WolframAlpha, OpenMath, nLab wiki adapters - src/manifold_perception.rs — filesystem manifest scanner / topological report - src/s3c_lean_review.rs — CLI adapter submitting S3C.lean to Gemma4Integration - src/search_adapter.rs — Google (stub) + Brave search providers All 9 wired into main.rs as mod declarations. **Test fixes (6 pre-existing failures → 0)** - s3c.rs: fix shell decomp width formula (a+b not a+b+1); correct test expectations for n=9 (b=7, not b=1); invariant a+b=2k+1 not 2k - math.rs: fix test_avg_chain expected avg to 10/6 (all-pairs average, not just A→* paths) - ene_core.rs: fix AES-GCM decrypt AAD mismatch in retrieve_sensitive_data — SELECT now fetches pkg column and passes it as AAD (matches store path) - hyperbolic_encoding.rs: fix Möbius transform formula to standard gyrovector form: denom = 1+2⟨a,z⟩+‖a‖²‖z‖² (was missing ‖a‖²‖z‖² term, had +‖z‖² instead) — satisfies T_0(z)=z identity **cargo test: 145 passed, 0 failed** **Delete 35 Python source files** now superseded by Rust crate: All 4-Infrastructure/infra/*.py and embedded_surface/server.py removed. **Deploy scripts updated** to use rs-surface binary instead of Python: - gcl_edge_in_place_upgrade.sh: CURRENT_SERVER → rs-surface binary; validate with test -x; smoke-test exec binary directly; rollback saves rs-surface - xen_alpine/install_rs_surface_openrc.sh: SERVER_SRC → musl release binary; drop python3 from apk; install as rs-surface (not server.py) - xen_alpine/run_qemu_alpine_surface.sh: default SURFACE_IMPL=rust; RUST_BIN var for musl binary; else-branch copies rs-surface; boot script exec binary - recover_credential_server.sh: upload rs-surface binary; ExecStart → binary with RS_SURFACE_PORT=8444 (credential endpoint built into rs-surface /credentials) - nixos-setup-cred-server.sh: same — ExecStart uses /opt/rs-surface/rs-surface Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
559 lines
21 KiB
Rust
559 lines
21 KiB
Rust
#![allow(dead_code)]
|
|
//! manifold_perception.rs — Topological manifest scanner for the research stack.
|
|
//!
|
|
//! Port of manifold_perception.py (390 lines).
|
|
//! Walks the filesystem under a given root, classifies every file, computes
|
|
//! per-file SHA-256 digests and line counts, parses Lean 4 metadata, identifies
|
|
//! structural "holes", and returns a `ManifoldReport`.
|
|
//!
|
|
//! Pure `std`-only directory walking; uses the `sha2` crate (already in Cargo.toml)
|
|
//! for file hashing.
|
|
|
|
use serde_json::{json, Value};
|
|
use sha2::{Digest, Sha256};
|
|
use std::path::Path;
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// §1 Core types
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
/// A single classified file in the research stack.
|
|
#[derive(Debug, Clone)]
|
|
pub struct ManifoldArtifact {
|
|
/// Absolute (or relative-to-root) path string.
|
|
pub path: String,
|
|
/// Coarse kind: "lean", "doc", "infra", "data", "other".
|
|
pub kind: String,
|
|
/// Number of lines in the file.
|
|
pub lines: usize,
|
|
/// First 16 hex characters of the SHA-256 digest.
|
|
pub hash: String,
|
|
}
|
|
|
|
/// A structural gap detected in the manifold.
|
|
#[derive(Debug, Clone)]
|
|
pub struct ManifoldHole {
|
|
/// Human-readable centre / location of the gap.
|
|
pub center: String,
|
|
/// What kind of artifact was expected here.
|
|
pub expected_kind: String,
|
|
/// "low", "medium", or "high".
|
|
pub severity: String,
|
|
/// Free-text description.
|
|
pub description: String,
|
|
/// How many expected items are missing.
|
|
pub missing_count: usize,
|
|
}
|
|
|
|
/// A topological boundary dimension of the manifold.
|
|
#[derive(Debug, Clone)]
|
|
pub struct ManifoldBoundary {
|
|
pub dimension: String,
|
|
pub present_count: usize,
|
|
pub description: String,
|
|
}
|
|
|
|
/// The full manifest report produced by `build_manifold`.
|
|
#[derive(Debug, Clone)]
|
|
pub struct ManifoldReport {
|
|
/// ISO-8601 UTC timestamp of when the report was generated.
|
|
pub generated_at: String,
|
|
pub lean_count: usize,
|
|
pub doc_count: usize,
|
|
pub infra_count: usize,
|
|
pub total_lean_lines: usize,
|
|
pub total_doc_lines: usize,
|
|
pub total_theorems: usize,
|
|
pub total_sorry: usize,
|
|
pub holes: Vec<ManifoldHole>,
|
|
pub boundaries: Vec<ManifoldBoundary>,
|
|
pub artifacts: Vec<ManifoldArtifact>,
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// §2 File classification
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
/// Classify a file path into a coarse kind string.
|
|
///
|
|
/// Priority order matches the Python port:
|
|
/// - `.lean` → "lean"
|
|
/// - `.md` / `.rst` / `.tex` → "doc"
|
|
/// - `.rs` / `.py` / `.sh` / `.toml` / `.json` / `.yaml` / `.yml` → "infra"
|
|
/// - `.db` / `.sqlite` / `.bin` / `.parquet` → "data"
|
|
/// - anything else → "other"
|
|
pub fn classify_artifact(path: &Path) -> &'static str {
|
|
let ext = path
|
|
.extension()
|
|
.and_then(|e| e.to_str())
|
|
.unwrap_or("")
|
|
.to_ascii_lowercase();
|
|
|
|
match ext.as_str() {
|
|
"lean" => "lean",
|
|
"md" | "rst" | "tex" | "txt" => "doc",
|
|
"rs" | "py" | "sh" | "toml" | "json" | "yaml" | "yml" | "lock" => "infra",
|
|
"db" | "sqlite" | "sqlite3" | "bin" | "parquet" => "data",
|
|
_ => {
|
|
// Check filename fragments for known infrastructure files.
|
|
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
|
let lower = name.to_ascii_lowercase();
|
|
if lower.contains("makefile")
|
|
|| lower.contains("dockerfile")
|
|
|| lower.contains("cargo")
|
|
|| lower.contains("lakefile")
|
|
{
|
|
return "infra";
|
|
}
|
|
if lower.ends_with(".olean") || lower.ends_with(".ilean") {
|
|
return "lean";
|
|
}
|
|
}
|
|
"other"
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// §3 File utilities
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
/// Compute the SHA-256 of a file and return the first 16 hex characters.
|
|
///
|
|
/// Returns an all-zero string on read error rather than propagating.
|
|
pub fn sha256_file(path: &Path) -> String {
|
|
let data = match std::fs::read(path) {
|
|
Ok(d) => d,
|
|
Err(_) => return "0000000000000000".to_string(),
|
|
};
|
|
let mut hasher = Sha256::new();
|
|
hasher.update(&data);
|
|
let digest = hasher.finalize();
|
|
// hex::encode gives 64 chars; take first 16.
|
|
let full = hex::encode(digest);
|
|
full[..16].to_string()
|
|
}
|
|
|
|
/// Count the number of newline characters in a file.
|
|
///
|
|
/// Returns 0 on read error.
|
|
pub fn count_lines(path: &Path) -> usize {
|
|
let data = match std::fs::read(path) {
|
|
Ok(d) => d,
|
|
Err(_) => return 0,
|
|
};
|
|
data.iter().filter(|&&b| b == b'\n').count()
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// §4 Lean 4 metadata
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
/// Count Lean 4 syntactic occurrences in a source file.
|
|
///
|
|
/// Returns a JSON object:
|
|
/// ```json
|
|
/// { "theorems": 3, "defs": 7, "inductives": 1, "structures": 2,
|
|
/// "evals": 0, "sorries": 1 }
|
|
/// ```
|
|
pub fn parse_lean_metadata(path: &Path) -> Value {
|
|
let text = match std::fs::read_to_string(path) {
|
|
Ok(t) => t,
|
|
Err(_) => return json!({ "theorems": 0, "defs": 0, "inductives": 0,
|
|
"structures": 0, "evals": 0, "sorries": 0 }),
|
|
};
|
|
|
|
let theorems = count_occurrences(&text, "theorem ");
|
|
let defs = count_occurrences(&text, "def ");
|
|
let inductives = count_occurrences(&text, "inductive ");
|
|
let structures = count_occurrences(&text, "structure ");
|
|
let evals = count_occurrences(&text, "#eval");
|
|
let sorries = count_occurrences(&text, "sorry");
|
|
|
|
json!({
|
|
"theorems": theorems,
|
|
"defs": defs,
|
|
"inductives": inductives,
|
|
"structures": structures,
|
|
"evals": evals,
|
|
"sorries": sorries,
|
|
})
|
|
}
|
|
|
|
/// Count non-overlapping occurrences of `needle` in `haystack`.
|
|
fn count_occurrences(haystack: &str, needle: &str) -> usize {
|
|
let mut count = 0;
|
|
let mut start = 0;
|
|
while let Some(pos) = haystack[start..].find(needle) {
|
|
count += 1;
|
|
start += pos + needle.len();
|
|
}
|
|
count
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// §5 Directory walker
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
/// Recursively collect all regular files under `dir`.
|
|
///
|
|
/// Silently skips unreadable directories. Ignores hidden directories (those
|
|
/// whose name starts with `.`) to avoid traversing `.git`, `.lake`, etc.
|
|
fn walk_files(dir: &Path, out: &mut Vec<std::path::PathBuf>) {
|
|
let rd = match std::fs::read_dir(dir) {
|
|
Ok(r) => r,
|
|
Err(_) => return,
|
|
};
|
|
for entry in rd.flatten() {
|
|
let path = entry.path();
|
|
let name = path
|
|
.file_name()
|
|
.and_then(|n| n.to_str())
|
|
.unwrap_or("");
|
|
// Skip hidden dirs / files.
|
|
if name.starts_with('.') {
|
|
continue;
|
|
}
|
|
let meta = match entry.metadata() {
|
|
Ok(m) => m,
|
|
Err(_) => continue,
|
|
};
|
|
if meta.is_dir() {
|
|
walk_files(&path, out);
|
|
} else if meta.is_file() {
|
|
out.push(path);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// §6 Hole detection
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
/// Detect structural gaps from the parsed Lean metadata of a set of artifacts.
|
|
fn detect_holes(artifacts: &[ManifoldArtifact], lean_meta: &[(String, Value)]) -> Vec<ManifoldHole> {
|
|
let mut holes: Vec<ManifoldHole> = Vec::new();
|
|
|
|
for (path, meta) in lean_meta {
|
|
let sorry_count = meta["sorries"].as_u64().unwrap_or(0) as usize;
|
|
let theorem_count = meta["theorems"].as_u64().unwrap_or(0) as usize;
|
|
let def_count = meta["defs"].as_u64().unwrap_or(0) as usize;
|
|
let eval_count = meta["evals"].as_u64().unwrap_or(0) as usize;
|
|
|
|
// Sorry without accompanying theorem — unfinished proof.
|
|
if sorry_count > 0 && theorem_count == 0 {
|
|
holes.push(ManifoldHole {
|
|
center: path.clone(),
|
|
expected_kind: "lean".to_string(),
|
|
severity: "high".to_string(),
|
|
description: format!(
|
|
"{} sorry(s) with no theorem statement — proof gap",
|
|
sorry_count
|
|
),
|
|
missing_count: sorry_count,
|
|
});
|
|
}
|
|
// Has defs but no #eval — possible dead code or untestable fragment.
|
|
if def_count > 0 && eval_count == 0 {
|
|
holes.push(ManifoldHole {
|
|
center: path.clone(),
|
|
expected_kind: "lean".to_string(),
|
|
severity: "low".to_string(),
|
|
description: format!(
|
|
"{} def(s) with no #eval — consider adding executable tests",
|
|
def_count
|
|
),
|
|
missing_count: def_count,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Structural gap: no Lean files at all.
|
|
if artifacts.iter().all(|a| a.kind != "lean") {
|
|
holes.push(ManifoldHole {
|
|
center: "stack root".to_string(),
|
|
expected_kind: "lean".to_string(),
|
|
severity: "high".to_string(),
|
|
description: "No Lean 4 source files found under the stack root".to_string(),
|
|
missing_count: 1,
|
|
});
|
|
}
|
|
|
|
// Gap: no documentation files.
|
|
if artifacts.iter().all(|a| a.kind != "doc") {
|
|
holes.push(ManifoldHole {
|
|
center: "stack root".to_string(),
|
|
expected_kind: "doc".to_string(),
|
|
severity: "medium".to_string(),
|
|
description: "No documentation files (.md / .rst / .tex) found".to_string(),
|
|
missing_count: 1,
|
|
});
|
|
}
|
|
|
|
holes
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// §7 Public API
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
/// Walk `stack_root` and produce a `ManifoldReport`.
|
|
pub fn build_manifold(stack_root: &Path) -> ManifoldReport {
|
|
let mut paths: Vec<std::path::PathBuf> = Vec::new();
|
|
walk_files(stack_root, &mut paths);
|
|
paths.sort();
|
|
|
|
let mut artifacts: Vec<ManifoldArtifact> = Vec::with_capacity(paths.len());
|
|
let mut lean_meta: Vec<(String, Value)> = Vec::new();
|
|
|
|
let mut lean_count = 0usize;
|
|
let mut doc_count = 0usize;
|
|
let mut infra_count = 0usize;
|
|
let mut total_lean_lines = 0usize;
|
|
let mut total_doc_lines = 0usize;
|
|
let mut total_theorems = 0usize;
|
|
let mut total_sorry = 0usize;
|
|
|
|
for path in &paths {
|
|
let kind = classify_artifact(path);
|
|
let lines = count_lines(path);
|
|
let hash = sha256_file(path);
|
|
|
|
let path_str = path.to_string_lossy().into_owned();
|
|
|
|
match kind {
|
|
"lean" => {
|
|
lean_count += 1;
|
|
total_lean_lines += lines;
|
|
let meta = parse_lean_metadata(path);
|
|
total_theorems += meta["theorems"].as_u64().unwrap_or(0) as usize;
|
|
total_sorry += meta["sorries"].as_u64().unwrap_or(0) as usize;
|
|
lean_meta.push((path_str.clone(), meta));
|
|
}
|
|
"doc" => {
|
|
doc_count += 1;
|
|
total_doc_lines += lines;
|
|
}
|
|
"infra" => {
|
|
infra_count += 1;
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
artifacts.push(ManifoldArtifact {
|
|
path: path_str,
|
|
kind: kind.to_string(),
|
|
lines,
|
|
hash,
|
|
});
|
|
}
|
|
|
|
let holes = detect_holes(&artifacts, &lean_meta);
|
|
|
|
// Boundary dimensions — coarse topological summary.
|
|
let boundaries = vec![
|
|
ManifoldBoundary {
|
|
dimension: "lean".to_string(),
|
|
present_count: lean_count,
|
|
description: format!("{} Lean 4 source files, {} lines", lean_count, total_lean_lines),
|
|
},
|
|
ManifoldBoundary {
|
|
dimension: "doc".to_string(),
|
|
present_count: doc_count,
|
|
description: format!("{} documentation files, {} lines", doc_count, total_doc_lines),
|
|
},
|
|
ManifoldBoundary {
|
|
dimension: "infra".to_string(),
|
|
present_count: infra_count,
|
|
description: format!("{} infrastructure files", infra_count),
|
|
},
|
|
ManifoldBoundary {
|
|
dimension: "theorems".to_string(),
|
|
present_count: total_theorems,
|
|
description: format!("{} theorem declarations found", total_theorems),
|
|
},
|
|
ManifoldBoundary {
|
|
dimension: "sorry".to_string(),
|
|
present_count: total_sorry,
|
|
description: format!("{} sorry occurrences (proof gaps)", total_sorry),
|
|
},
|
|
];
|
|
|
|
let generated_at = chrono::Utc::now().to_rfc3339();
|
|
|
|
ManifoldReport {
|
|
generated_at,
|
|
lean_count,
|
|
doc_count,
|
|
infra_count,
|
|
total_lean_lines,
|
|
total_doc_lines,
|
|
total_theorems,
|
|
total_sorry,
|
|
holes,
|
|
boundaries,
|
|
artifacts,
|
|
}
|
|
}
|
|
|
|
/// Build the manifold and serialize it to a `serde_json::Value`.
|
|
pub fn build_manifold_json(stack_root: &Path) -> Value {
|
|
let report = build_manifold(stack_root);
|
|
|
|
let artifacts: Vec<Value> = report
|
|
.artifacts
|
|
.iter()
|
|
.map(|a| {
|
|
json!({
|
|
"path": a.path,
|
|
"kind": a.kind,
|
|
"lines": a.lines,
|
|
"hash": a.hash,
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
let holes: Vec<Value> = report
|
|
.holes
|
|
.iter()
|
|
.map(|h| {
|
|
json!({
|
|
"center": h.center,
|
|
"expected_kind": h.expected_kind,
|
|
"severity": h.severity,
|
|
"description": h.description,
|
|
"missing_count": h.missing_count,
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
let boundaries: Vec<Value> = report
|
|
.boundaries
|
|
.iter()
|
|
.map(|b| {
|
|
json!({
|
|
"dimension": b.dimension,
|
|
"present_count": b.present_count,
|
|
"description": b.description,
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
json!({
|
|
"schema": "manifold_report_v1",
|
|
"generated_at": report.generated_at,
|
|
"lean_count": report.lean_count,
|
|
"doc_count": report.doc_count,
|
|
"infra_count": report.infra_count,
|
|
"total_lean_lines": report.total_lean_lines,
|
|
"total_doc_lines": report.total_doc_lines,
|
|
"total_theorems": report.total_theorems,
|
|
"total_sorry": report.total_sorry,
|
|
"holes": holes,
|
|
"boundaries": boundaries,
|
|
"artifacts": artifacts,
|
|
})
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// §8 Tests
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::io::Write;
|
|
use tempfile::TempDir;
|
|
|
|
fn make_temp_file(dir: &TempDir, name: &str, content: &str) -> std::path::PathBuf {
|
|
let path = dir.path().join(name);
|
|
let mut f = std::fs::File::create(&path).unwrap();
|
|
f.write_all(content.as_bytes()).unwrap();
|
|
path
|
|
}
|
|
|
|
#[test]
|
|
fn classify_lean() {
|
|
assert_eq!(classify_artifact(Path::new("foo.lean")), "lean");
|
|
assert_eq!(classify_artifact(Path::new("bar.olean")), "lean");
|
|
}
|
|
|
|
#[test]
|
|
fn classify_doc() {
|
|
assert_eq!(classify_artifact(Path::new("README.md")), "doc");
|
|
assert_eq!(classify_artifact(Path::new("paper.tex")), "doc");
|
|
}
|
|
|
|
#[test]
|
|
fn classify_infra() {
|
|
assert_eq!(classify_artifact(Path::new("main.rs")), "infra");
|
|
assert_eq!(classify_artifact(Path::new("Cargo.toml")), "infra");
|
|
assert_eq!(classify_artifact(Path::new("script.py")), "infra");
|
|
}
|
|
|
|
#[test]
|
|
fn classify_other() {
|
|
assert_eq!(classify_artifact(Path::new("image.png")), "other");
|
|
assert_eq!(classify_artifact(Path::new("video.mp4")), "other");
|
|
}
|
|
|
|
#[test]
|
|
fn count_lines_basic() {
|
|
let dir = TempDir::new().unwrap();
|
|
let path = make_temp_file(&dir, "test.txt", "line1\nline2\nline3\n");
|
|
assert_eq!(count_lines(&path), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn sha256_file_len() {
|
|
let dir = TempDir::new().unwrap();
|
|
let path = make_temp_file(&dir, "test.txt", "hello world");
|
|
let h = sha256_file(&path);
|
|
assert_eq!(h.len(), 16);
|
|
assert!(h.chars().all(|c| c.is_ascii_hexdigit()));
|
|
}
|
|
|
|
#[test]
|
|
fn count_occurrences_basic() {
|
|
let text = "theorem foo : theorem bar : sorry sorry";
|
|
assert_eq!(count_occurrences(text, "theorem "), 2);
|
|
assert_eq!(count_occurrences(text, "sorry"), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_lean_metadata_counts() {
|
|
let dir = TempDir::new().unwrap();
|
|
let src = "theorem foo : 1 = 1 := by rfl\nsorry\ndef bar := 42\n#eval bar\n";
|
|
let path = make_temp_file(&dir, "test.lean", src);
|
|
let meta = parse_lean_metadata(&path);
|
|
assert_eq!(meta["theorems"], 1);
|
|
assert_eq!(meta["sorries"], 1);
|
|
assert_eq!(meta["defs"], 1);
|
|
assert_eq!(meta["evals"], 1);
|
|
}
|
|
|
|
#[test]
|
|
fn build_manifold_smoke() {
|
|
let dir = TempDir::new().unwrap();
|
|
make_temp_file(&dir, "a.lean", "theorem foo : 1 = 1 := by rfl\n");
|
|
make_temp_file(&dir, "README.md", "# Hello\n");
|
|
make_temp_file(&dir, "build.rs", "fn main() {}\n");
|
|
|
|
let report = build_manifold(dir.path());
|
|
assert_eq!(report.lean_count, 1);
|
|
assert_eq!(report.doc_count, 1);
|
|
assert_eq!(report.infra_count, 1);
|
|
assert_eq!(report.total_theorems, 1);
|
|
assert!(!report.artifacts.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn build_manifold_json_schema() {
|
|
let dir = TempDir::new().unwrap();
|
|
make_temp_file(&dir, "x.lean", "sorry\n");
|
|
let v = build_manifold_json(dir.path());
|
|
assert_eq!(v["schema"], "manifold_report_v1");
|
|
assert!(v["holes"].as_array().is_some());
|
|
assert!(v["artifacts"].as_array().is_some());
|
|
}
|
|
}
|