fix(core): 规则结论改在脱敏前算,会话路上 fabric.loader_0_12 能命中(M241)

会话 JSONL 是写前脱敏的,而脱敏的「≥64 无点号不透明串」是有损的:
fabric.loader_0_12 唯一的证据行(Sponge mixin 类路径 66 字符)被抹成
<redacted:opaque>,规则在会话路上永不命中。

修法不动脱敏策略、也不往盘上多写一份原文:
1. SessionWriter 在内存里留一个 2000 行的原始尾窗(不落盘);
2. 收尾时把这些**原始**事实交给新的 diagnose::session::conclude 跑规则,
   产出契约形状的 diagnosis payload —— 它落盘前仍过 Redactor::scrub_json;
3. 日志页优先读这条落盘结论,老会话才回落到「在脱敏文本上重跑规则」。

为什么不是「原文与脱敏文本并存」:会话 JSONL 里存原文会踩
core/tests/launch/pipeline_cases.rs 早已钉死的「事件流不得含凭据明文」。

顺带把 EventKind 纯搬移到 launch/session/event_kind.rs 守 400 行上限。
This commit is contained in:
AskaEth committed 2026-11-11 09:19:42 +08:00
1 parent 74be510114
commit edd6402fd5
10 files changed
+633 -65

No files matched your search

+25 -7
View File
@@ -28,6 +28,8 @@ use crate::dto::{
use super::env::HostEnv;
#[path = "logs/diagnosis.rs"]
mod diagnosis;
#[path = "logs/location.rs"]
mod location;
@@ -141,30 +143,43 @@ pub fn line_of(event: &Value) -> Option<LogLine> {
})
}
/// 整个事件流 → 日志行(解析失败的行跳过,容忍被 kill 时截断的尾行)。
pub fn read_lines(path: &Path) -> Vec<LogLine> {
/// 整个事件流的原始 JSON 行(解析失败的行跳过,容忍被 kill 时截断的尾行)。
///
/// 一次读完所有事件:日志行与诊断结论都从这同一份读里取,不重复读盘。
pub fn read_events(path: &Path) -> Vec<Value> {
let Ok(text) = std::fs::read_to_string(path) else {
return Vec::new();
};
text.lines()
.filter(|line| !line.trim().is_empty())
.filter_map(|line| serde_json::from_str::<Value>(line).ok())
.filter_map(|event| line_of(&event))
.collect()
}
/// 整个事件流 → 日志行(**展示面**:`payload.line`,即写盘前已脱敏的文本)。
pub fn read_lines(path: &Path) -> Vec<LogLine> {
read_events(path).iter().filter_map(line_of).collect()
}
/// 打开一个会话:日志行 + 规则引擎结论。
///
/// 结论优先取会话里**落盘的那份**(写会话时用原始行算过,见 [`diagnosis`] 模块文档);
/// 老会话没有它,才回落到「在脱敏文本上重跑规则」。
pub fn open(env: &HostEnv, key: &str) -> (Vec<LogLine>, Option<DiagnosisReport>) {
let Some(entry) = find(env, key) else {
return (Vec::new(), None);
};
let path = event_path(env, &entry);
let lines = read_lines(&path);
let diagnosis = diagnose_lines(&entry, &lines);
let events = read_events(&event_path(env, &entry));
let lines: Vec<LogLine> = events.iter().filter_map(line_of).collect();
let diagnosis = diagnosis::stored(&entry, &events).or_else(|| diagnose_lines(&entry, &lines));
(lines, diagnosis)
}
/// 规则引擎跑一遍(**规则先跑,模型补位**:这里只出规则结论)。
/// 在**脱敏文本**上重跑规则引擎(老会话的兜底路径)。
///
/// ⚠️ 这条路的证据是「写盘前脱敏过的」行:`≥64` 无点号长串(Sponge 类路径、长 hash)会被
/// 抹成 `<redacted:opaque>`,依赖那些字面量的规则(`fabric.loader_0_12`)在这里**不可能命中**。
/// 新会话通常不必走这条路——写会话时用原始行算过结论,见 [`diagnosis::stored`]。
pub fn diagnose_lines(entry: &SessionIndexEntry, lines: &[LogLine]) -> Option<DiagnosisReport> {
let stderr_text: String = lines
.iter()
@@ -355,6 +370,9 @@ pub fn session_finished(env: &HostEnv, session: &str) -> bool {
})
}
#[cfg(test)]
#[path = "logs/m241_tests.rs"]
mod m241_tests;
#[cfg(test)]
#[path = "logs_tests.rs"]
mod tests;