///|
/// Input content is always literal text; this library performs no I/O.
pub(all) struct Document {
path : String
content : String
} derive(Eq, Debug, ToJson, FromJson)
///|
/// Persisted user choices are keyed by document path, independent of line numbers.
pub(all) struct Preference {
path : String
topic : String
title : String
} derive(Eq, Debug, ToJson, FromJson)
///|
/// An explicit concept-name mapping, not a semantic equivalence inferred by AI.
pub(all) struct Alias {
name : String
target : String
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct Evidence {
path : String
start_line : Int32
end_line : Int32
quote : String
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct Section {
id : String
title : String
level : Int32
evidence : Evidence
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct Page {
id : String
path : String
title : String
topic : String
assignment : String
sections : Array[Section]
tags : Array[String]
excerpt : String
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct Concept {
id : String
label : String
sources : Array[Evidence]
paths : Array[String]
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct Link {
from : String
target : String
line : Int32
status : String
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct Issue {
code : String
path : String
message : String
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct Duplicate {
paths : Array[String]
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct Atlas {
schema : Int32
pages : Array[Page]
concepts : Array[Concept]
links : Array[Link]
duplicates : Array[Duplicate]
issues : Array[Issue]
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct Change {
kind : String
path : String
previous_path : String
detail : String
} derive(Eq, Debug, ToJson, FromJson)
///|
fn problem(code : String, path : String, message : String) -> Issue {
{ code, path, message, }
}
///|
pub fn normalize_name(name : String) -> String {
let parts : Array[String] = []
let mut current = ""
for ch in name.trim().to_owned().to_array() {
if ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' {
if current != "" {
parts.push(current)
current = ""
}
} else {
current += ch.to_string()
}
}
if current != "" {
parts.push(current)
}
parts.join(" ").to_lower()
}
///|
pub fn validate_documents(documents : Array[Document]) -> Array[Issue] {
let issues = []
if documents.length() > 200 {
return [problem("limit", "", "最多导入 200 篇文档")]
}
let seen : Map[String, Bool] = Map([])
let mut total = 0
for doc in documents {
let pieces = doc.path.split("/").to_array()
let lower = doc.path.to_lower()
if doc.path == "" ||
doc.path.length() > 500 ||
doc.path.contains("\\") ||
doc.path.contains(":") ||
pieces.any(p => p == "" || p == "." || p == "..") ||
(!lower.has_suffix(".md") && !lower.has_suffix(".txt")) {
issues.push(
problem(
"path",
doc.path,
"需要 .md/.txt 的相对路径,不能包含空路径、冒号或 ..",
),
)
}
if seen.contains(doc.path) {
issues.push(problem("duplicate_path", doc.path, "文档路径重复"))
}
seen[doc.path] = true
if doc.content.length() > 200000 {
issues.push(
problem(
"limit",
doc.path,
"单篇文档超过 200,000 个 UTF-16 码元",
),
)
}
if doc.content.contains("\u{0000}") {
issues.push(problem("binary", doc.path, "不接受含 NUL 的文本"))
}
total += doc.content.length()
}
if total > 2000000 {
issues.push(
problem("limit", "", "总文本超过 2,000,000 个 UTF-16 码元"),
)
}
issues
}