feat(provenance): 阶段2 可溯源判据重定义 + 实测 lcs_len/prefix_hit60 入 jsonl (t_dc412532)
AC1 先量后判: jsonl 增记 lcs_len(最长公共子串) + prefix_hit60(前60字命中)。 AC2 判据重定义: 空原文(session 已压缩) -> unknown/source_not_available(不再误判 false/not_in_source); 摘要低 LCS -> unknown/derived_low_lcs(正常改写不可证伪); 命中 -> true(片段可溯源)。依据 24h 分布 true=1/false=8/unknown=193, 8条 false 全来自 session-compress(摘要器)=结构性误判。 AC3 unknown 处置=放行(fail-open): 94.6% unknown 为 writer 侧盲区(无 source), 非内容不可回证; 拒 unknown = 99.7% 拦 = 掐死织忆。 AC4 行为不变: 仍纯旁路观测, 不拒任何写入。
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// 织忆 MemoryWeave — commit 写入的「引用可复算」旁路观测(阶段 1:只观测,不拦截)
|
||||
// 织忆 MemoryWeave — commit 写入的「引用可复算」旁路观测(阶段 1→2:只观测,不拦截)
|
||||
//
|
||||
// 动机(t_3fbb6cbd):Akasha 的确定性校验器(sourceRange + quoteHash,fail-closed)
|
||||
// 是唯一值得移植的东西。织忆现在的写入无法回证(09-25 实测卡点)。
|
||||
@@ -9,12 +9,21 @@
|
||||
// 开启时对每次 commit 做旁路观测(异步 goroutine,不阻塞、不改响应、不改写入路径),
|
||||
// 结果追加到 ~/.hermes/logs/zhiyi-commit-provenance/YYYY-MM-DD.jsonl。
|
||||
//
|
||||
// 判定降级(AC2):
|
||||
// 判定降级(AC2 重定义,t_dc412532 阶段2 设计输入):
|
||||
// (a) metadata 有 source_uri + quote + quote_sha256 → 复算 sha256(quote) 比对;
|
||||
// 不等 observable=false, reason=hash_mismatch
|
||||
// (b) 无上述字段但 source=="session-compress" 且 session_id 存在 →
|
||||
// 去 ~/.hermes/state.db(只读)取该 session 消息文本,查 content 前 60 字符
|
||||
// 逐字命中(或最长公共子串 >=60)→ 命中 observable=true,否则 false reason=not_in_source
|
||||
// 去 ~/.hermes/state.db(只读)取该 session 消息文本做回证。
|
||||
// 观察期实测(n=205, 24h):true=1 / false=8 / unknown=193;全部 8 条 false
|
||||
// 均来自 session-compress —— 判据「前 60 字逐字命中 / LCS≥60」对「摘要/派生内容」
|
||||
// 结构性必 false(摘要不会逐字出现在原文里)。AC2 修正:
|
||||
// * 原文不可得(session 已压缩/清除)→ observable=unknown, reason=source_not_available
|
||||
// (空原文 = 无法证伪,不是证伪失败 —— 旧逻辑误标 false/not_in_source)
|
||||
// * 前 60 字逐字命中 或 LCS≥60 → observable=true(引用片段可溯源)
|
||||
// * 其余(摘要低 LCS,属正常改写)→ observable=unknown, reason=derived_low_lcs
|
||||
// (对派生内容无法证伪 —— 摘要重述≠不可溯源)
|
||||
// 同时把实测数值 lcs_len / prefix_hit60 写进 jsonl(AC1 先量后判),
|
||||
// 供阶段3 转硬闸时按分布定档。
|
||||
// (c) 其他 source → observable=unknown, reason=no_derivation(不要瞎猜成 false)
|
||||
package routes
|
||||
|
||||
@@ -46,7 +55,9 @@ const stateDBPath = "/home/muc/.hermes/state.db"
|
||||
// provenanceLogDir 观测结果落盘目录
|
||||
const provenanceLogDir = "/home/muc/.hermes/logs/zhiyi-commit-provenance"
|
||||
|
||||
// minCommonRun 判定「可复算」的最长公共子串下界(字符)
|
||||
// minCommonRun 判定「可溯源」的最长公共子串下界(字符)。
|
||||
// 依据(t_dc412532 阶段2 实测分布):观察期 session-compress 摘要 content_len 均值 ~450,
|
||||
// 要求 LCS≥60 取「摘要中至少出现一段可对回原文的片段」,过低会把无关噪声当溯源。
|
||||
const minCommonRun = 60
|
||||
|
||||
// observeCommitProvenance 旁路观测一次 commit(异步调用,永不阻塞写路径)
|
||||
@@ -67,6 +78,8 @@ func observeCommitProvenance(agentID, content string, metadata map[string]any) {
|
||||
|
||||
observable := "unknown"
|
||||
reason := "no_derivation"
|
||||
lcsLen := 0
|
||||
prefixHit60 := false
|
||||
|
||||
// (a) 显式 quote + hash
|
||||
if metadata != nil {
|
||||
@@ -77,22 +90,24 @@ func observeCommitProvenance(agentID, content string, metadata map[string]any) {
|
||||
observable, reason = decideQuoteHash(quote, qh)
|
||||
} else if source == "session-compress" && sessionID != "" {
|
||||
// (b) session-compress:去 state.db 只读回证
|
||||
observable, reason = observeViaSession(content, sessionID)
|
||||
observable, reason, lcsLen, prefixHit60 = observeViaSession(content, sessionID)
|
||||
}
|
||||
} else if source == "session-compress" && sessionID != "" {
|
||||
observable, reason = observeViaSession(content, sessionID)
|
||||
observable, reason, lcsLen, prefixHit60 = observeViaSession(content, sessionID)
|
||||
}
|
||||
|
||||
// (c) 其余保持 unknown / no_derivation
|
||||
|
||||
line := map[string]interface{}{
|
||||
"ts": time.Now().Format(time.RFC3339),
|
||||
"agent_id": agentID,
|
||||
"source": source,
|
||||
"session_id": sessionID,
|
||||
"observable": observable,
|
||||
"reason": reason,
|
||||
"content_len": len(content),
|
||||
"ts": time.Now().Format(time.RFC3339),
|
||||
"agent_id": agentID,
|
||||
"source": source,
|
||||
"session_id": sessionID,
|
||||
"observable": observable,
|
||||
"reason": reason,
|
||||
"content_len": len(content),
|
||||
"lcs_len": lcsLen, // AC1: content 与原文的最长公共子串长度(实测数值)
|
||||
"prefix_hit60": prefixHit60, // AC1: 前 60 字是否逐字命中原文
|
||||
}
|
||||
appendProvenanceLog(line)
|
||||
}
|
||||
@@ -106,18 +121,21 @@ func decideQuoteHash(quote, quoteSHA256 string) (string, string) {
|
||||
return "false", "hash_mismatch"
|
||||
}
|
||||
|
||||
// observeViaSession 去 state.db(只读)取 session 消息文本,回证 content 可复算。
|
||||
func observeViaSession(content, sessionID string) (string, string) {
|
||||
// observeViaSession 去 state.db(只读)取 session 消息文本,回证 content 可溯源。
|
||||
// 返回 (observable, reason, lcs_len, prefix_hit60)。
|
||||
// AC2 语义:对派生/摘要内容不做逐字强判 —— 原文不可得→unknown(无法证伪),
|
||||
// 命中→true(片段可溯源),低 LCS→unknown(摘要改写正常,不可证伪)。
|
||||
func observeViaSession(content, sessionID string) (string, string, int, bool) {
|
||||
db, err := sql.Open("sqlite", "file:"+stateDBPath+"?_pragma=query_only(1)&_pragma=busy_timeout(3000)")
|
||||
if err != nil {
|
||||
return "unknown", "state_db_open_failed: " + err.Error()
|
||||
return "unknown", "state_db_open_failed: " + err.Error(), 0, false
|
||||
}
|
||||
defer db.Close()
|
||||
rows, err := db.Query(
|
||||
"SELECT content FROM messages WHERE session_id=? AND role IN ('user','assistant') ORDER BY id",
|
||||
sessionID)
|
||||
if err != nil {
|
||||
return "unknown", "state_db_query_failed: " + err.Error()
|
||||
return "unknown", "state_db_query_failed: " + err.Error(), 0, false
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -132,27 +150,30 @@ func observeViaSession(content, sessionID string) (string, string) {
|
||||
}
|
||||
srcText := sb.String()
|
||||
if srcText == "" {
|
||||
return "false", "not_in_source"
|
||||
// 原文不可得:无法证伪,不是证伪失败(旧逻辑误标 false/not_in_source)
|
||||
return "unknown", "source_not_available", 0, false
|
||||
}
|
||||
if provenanceMatch(content, srcText) {
|
||||
return "true", ""
|
||||
match, lcsLen, prefixHit60 := provenanceMeasure(content, srcText)
|
||||
if match {
|
||||
return "true", "", lcsLen, prefixHit60
|
||||
}
|
||||
return "false", "not_in_source"
|
||||
// 摘要/派生内容低 LCS 属正常改写,无法证伪 —— 不算 false
|
||||
return "unknown", "derived_low_lcs", lcsLen, prefixHit60
|
||||
}
|
||||
|
||||
// provenanceMatch 判定 content 相对 srcText 可复算:
|
||||
// content 前 60 字符在 srcText 中逐字命中,或 最长公共子串 >=60。
|
||||
func provenanceMatch(content, srcText string) bool {
|
||||
// provenanceMeasure 计算 content 相对 srcText 的可溯源实测指标:
|
||||
// prefixHit60 = content 前 60 字在 srcText 中逐字命中
|
||||
// lcsLen = content 与 srcText 的最长公共子串长度
|
||||
// match = prefixHit60 || lcsLen >= minCommonRun
|
||||
func provenanceMeasure(content, srcText string) (match bool, lcsLen int, prefixHit60 bool) {
|
||||
if content == "" || srcText == "" {
|
||||
return false
|
||||
return false, 0, false
|
||||
}
|
||||
first := content
|
||||
if len(first) > minCommonRun {
|
||||
first = first[:minCommonRun]
|
||||
}
|
||||
if strings.Contains(srcText, first) {
|
||||
return true
|
||||
}
|
||||
prefixHit60 = strings.Contains(srcText, first)
|
||||
// LCS 兜底(受限输入防打爆)
|
||||
c := content
|
||||
if len(c) > 2000 {
|
||||
@@ -162,7 +183,8 @@ func provenanceMatch(content, srcText string) bool {
|
||||
if len(s) > 20000 {
|
||||
s = s[:20000]
|
||||
}
|
||||
return lcsLength(c, s) >= minCommonRun
|
||||
lcsLen = lcsLength(c, s)
|
||||
return prefixHit60 || lcsLen >= minCommonRun, lcsLen, prefixHit60
|
||||
}
|
||||
|
||||
// lcsLength 两个字符串的最长公共子串长度(O(n*m) 动态规划,取较短者做列)。
|
||||
|
||||
Reference in New Issue
Block a user