///|
/// Compare literal document snapshots. Rename requires a unique exact-content match.
pub fn compare_documents(
before : Array[Document],
after : Array[Document],
) -> Array[Change] {
if !validate_documents(before).is_empty() ||
!validate_documents(after).is_empty() {
return [
{
kind: "invalid",
path: "",
previous_path: "",
detail: "输入校验失败,无法比较",
},
]
}
let old : Map[String, String] = Map([])
let fresh : Map[String, String] = Map([])
for d in before {
old[d.path] = d.content
}
for d in after {
fresh[d.path] = d.content
}
let removed = before.filter(d => !fresh.contains(d.path))
let added = after.filter(d => !old.contains(d.path))
let renamed : Map[String, Bool] = Map([])
let changes : Array[Change] = []
for d in added {
let matches = removed.filter(r => r.content == d.content)
let new_matches = added.filter(r => r.content == d.content)
if matches.length() == 1 &&
new_matches.length() == 1 &&
d.content.trim() != "" {
renamed[matches[0].path] = true
changes.push({
kind: "renamed",
path: d.path,
previous_path: matches[0].path,
detail: "内容完全相同且匹配唯一;按重命名处理",
})
} else {
changes.push({
kind: "added",
path: d.path,
previous_path: "",
detail: "新加入的文档",
})
}
}
for d in removed {
if !renamed.contains(d.path) {
changes.push({
kind: "removed",
path: d.path,
previous_path: "",
detail: "当前资料集不再包含此文档",
})
}
}
for d in after {
if old.get(d.path) is Some(content) {
if content != d.content {
changes.push({
kind: "updated",
path: d.path,
previous_path: "",
detail: "原文内容发生变化",
})
}
}
}
changes.sort_by((a, b) => a.path.compare(b.path))
changes
}
///|
/// Carry manual choices across only unambiguous content-identical renames.
pub fn reconcile_preferences(
before : Array[Document],
after : Array[Document],
preferences : Array[Preference],
) -> Array[Preference] {
let changes = compare_documents(before, after)
let result : Array[Preference] = []
for p in preferences {
let rename = changes
.iter()
.find_first(c => c.kind == "renamed" && c.previous_path == p.path)
match rename {
Some(c) =>
if !preferences.any(other => other.path == c.path) {
result.push({ path: c.path, topic: p.topic, title: p.title, })
}
None => result.push(p)
}
}
result
}
///|
fn markdown_text(text : String) -> String {
text
.replace_all(old="\\", new="\\\\")
.replace_all(old="[", new="\\[")
.replace_all(old="]", new="\\]")
.replace_all(old="*", new="\\*")
.replace_all(old="_", new="\\_")
.replace_all(old="`", new="\\`")
.replace_all(old="<", new="<")
.replace_all(old="\n", new=" ")
.replace_all(old="\r", new=" ")
}
///|
/// Portable inventory with original paths and line numbers; no guessed heading anchors.
pub fn export_index(atlas : Atlas) -> String {
let rows = [
"# MoonAtlas 知识库索引", "", "由目录、显式分类和原文标题生成;同名标题仅表示文字线索。",
"",
]
let topics : Map[String, Array[Page]] = Map([])
for page in atlas.pages {
if !topics.contains(page.topic) {
topics[page.topic] = []
}
topics[page.topic].push(page)
}
let names = topics.keys().to_array()
names.sort()
for topic in names {
rows.push("## " + markdown_text(topic))
rows.push("")
for p in topics[topic] {
rows.push(
"- **" + markdown_text(p.title) + "** — " + markdown_text(p.path),
)
for s in p.sections {
rows.push(
" - " +
markdown_text(s.title) +
"(L" +
s.evidence.start_line.to_string() +
"–" +
s.evidence.end_line.to_string() +
")",
)
}
}
rows.push("")
}
rows.push("## 跨文档标题线索")
rows.push("")
for c in atlas.concepts {
if c.paths.length() > 1 {
rows.push(
"- " +
markdown_text(c.label) +
":" +
c.paths.map(markdown_text).join("、"),
)
}
}
rows.join("\n") + "\n"
}