@furongjun1999/dsh-memory 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +572 -465
- package/docs/README.md +1 -0
- package/docs/eval/DSH/346/227/245/345/277/227/347/264/242/345/274/225v2_/345/217/202/350/200/203dsh-TUI_v1.0.md +248 -0
- package/docs/eval/DSH/346/227/245/345/277/227/347/264/242/345/274/225/346/225/210/346/236/234/351/252/214/350/257/201_v1.0.md +209 -0
- package/docs/eval/DSH/347/253/257/347/274/272/351/231/267/344/270/223/351/241/271_v1.0.md +254 -0
- package/docs/eval/P1b2_/350/257/273/351/235/242/344/273/243/351/231/205/344/277/256/345/244/215_v1.0.md +124 -0
- package/docs/eval/bench_lingshu_self/bench_self.py +140 -0
- package/docs/eval/bench_lingshu_self/self_bench_result.json +404 -0
- package/docs/eval/bench_lingshu_self//347/201/265/346/236/242/350/207/252/345/272/223/347/253/257/345/210/260/347/253/257/346/243/200/347/264/242/345/256/236/346/265/213_v1.0.md +38 -0
- package/docs/eval//344/270/215/345/217/257/351/235/240/346/200/247/350/220/275/345/234/260_P0_v1.0.md +185 -0
- package/docs/eval//345/217/221/345/270/20306_/344/270/200/351/224/256/351/205/215/347/275/256/344/270/216DSH0172_v1.0.md +171 -0
- package/docs/eval//346/225/205/351/232/234/346/263/250/345/205/245/345/256/236/346/265/213_v1.0.md +422 -0
- package/docs/eval//347/253/257/345/210/260/347/253/257LoCoMoQA/345/220/214/345/217/243/345/276/204/345/257/271/347/205/247_v1.0.md +100 -0
- package/docs/eval//347/253/257/345/210/260/347/253/257/345/271/262/346/211/260/346/261/240/350/257/204/346/265/213_/347/241/256/345/256/232/346/200/247/350/243/201/345/206/263vsLLM_judge_v1.1.md +197 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v1.md +156 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v10.md +210 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v11.md +227 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v12.md +203 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v13.md +233 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v14.md +191 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v15.md +213 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v16.md +214 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v17.md +199 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v18.md +183 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v19.md +207 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v2.md +156 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v3.md +152 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v4.md +128 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v5.md +114 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v6.md +192 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v7.md +187 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v8.md +207 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v9.md +203 -0
- package/docs/hive//345/256/211/345/205/250/345/256/241/350/256/241/345/256/236/351/224/232_v0.1.md +202 -0
- package/docs/hive//350/234/202/345/267/242/345/217/214/345/256/236/344/276/213/344/272/222/351/252/214_/350/256/276/350/256/241/345/256/232/347/250/277.md +19 -3
- package/docs/images/lingshu-moonlight-covenant-poster-preview.jpg +0 -0
- package/docs/images/lingshu-moonlight-covenant-poster.png +0 -0
- package/docs/mdcg//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +40 -40
- package/docs/theory//344/270/215/345/217/257/351/235/240/345/256/232/347/220/206/344/270/216/345/244/261/346/225/210/344/274/230/345/205/210/346/241/206/346/236/266_v0.3.md +365 -0
- package/docs//344/270/215/345/217/257/351/235/240/346/200/247/347/220/206/350/256/272_v0.1.md +343 -0
- package/dsh/cordis-patch-profile-web.example.yml +35 -0
- package/lib/bridge.d.ts +9 -0
- package/lib/bridge.js +35 -0
- package/lib/cli.d.ts +3 -0
- package/lib/cli.js +66 -0
- package/lib/index.js +7 -1
- package/lib/init.d.ts +88 -0
- package/lib/init.js +287 -0
- package/lib/lib/roleplay_web.js +530 -443
- package/lib/lib/token_store.d.ts +7 -1
- package/lib/lib/token_store.js +12 -3
- package/md_cg/audit.py +12 -1
- package/md_cg/backfill.py +16 -15
- package/md_cg/backfill_bucket_zh.py +35 -0
- package/md_cg/bench_e2e_judge.py +532 -0
- package/md_cg/bench_e2e_locomo_qa.py +368 -0
- package/md_cg/bench_e2e_qa.py +256 -0
- package/md_cg/branches.py +18 -2
- package/md_cg/ccgc.py +3 -2
- package/md_cg/chain.py +19 -4
- package/md_cg/consolidate.py +7 -6
- package/md_cg/crosscheck.py +4 -3
- package/md_cg/crypto.py +439 -437
- package/md_cg/datapath.py +395 -335
- package/md_cg/evidence.py +8 -3
- package/md_cg/export.py +3 -1
- package/md_cg/forgetting.py +2 -2
- package/md_cg/fsutil.py +48 -0
- package/md_cg/hotcache.py +255 -238
- package/md_cg/interop.py +161 -22
- package/md_cg/judgment_manifest.py +177 -0
- package/md_cg/links.py +655 -622
- package/md_cg/mcp_server.py +282 -31
- package/md_cg/mdcg.py +702 -126
- package/md_cg/mdcos.py +454 -68
- package/md_cg/mreview/govern.py +5 -4
- package/md_cg/postings.py +4 -2
- package/md_cg/readcache.py +76 -18
- package/md_cg/reconcile.py +228 -0
- package/md_cg/review_cli.py +215 -0
- package/md_cg/routing.py +28 -0
- package/md_cg/run_tests.py +211 -0
- package/md_cg/scrub.py +862 -852
- package/md_cg/security.py +385 -275
- package/md_cg/selfreport.py +3 -2
- package/md_cg/signer.py +565 -562
- package/md_cg/sources.py +3 -2
- package/md_cg/stg.py +6 -0
- package/md_cg/sustain.py +1168 -1138
- package/md_cg/test_access_hints.py +147 -0
- package/md_cg/test_branch_discard_tombstone.py +136 -0
- package/md_cg/test_branches.py +259 -249
- package/md_cg/test_chain_read_isolate.py +168 -0
- package/md_cg/test_datapath_device_name.py +203 -0
- package/md_cg/test_emit_negtail_cache.py +156 -0
- package/md_cg/test_en_pipeline.py +186 -166
- package/md_cg/test_govern_directread.py +421 -0
- package/md_cg/test_i32_hotcache_env_key.py +218 -0
- package/md_cg/test_identity_attribution.py +228 -147
- package/md_cg/test_index_crossprocess_reload.py +301 -0
- package/md_cg/test_index_durability.py +238 -224
- package/md_cg/test_interop.py +4 -2
- package/md_cg/test_interop_judgment.py +228 -0
- package/md_cg/test_issue39_utf8_stdio.py +273 -0
- package/md_cg/test_links_concurrent_write.py +188 -0
- package/md_cg/test_merge_upsert.py +168 -0
- package/md_cg/test_n123_derive_expiry_chain.py +205 -0
- package/md_cg/test_n130_verify_falsified_protect.py +185 -0
- package/md_cg/test_n131_merge_gate.py +205 -0
- package/md_cg/test_none_id_write_guard.py +165 -0
- package/md_cg/test_p1x_ref_root.py +160 -0
- package/md_cg/test_p27_docindex.py +774 -765
- package/md_cg/test_p2_mcp.py +3 -0
- package/md_cg/test_p32_backfill.py +304 -298
- package/md_cg/test_p39_verify_flow.py +90 -50
- package/md_cg/test_p47_session_view.py +60 -25
- package/md_cg/test_propose_tail_index.py +157 -0
- package/md_cg/test_read_scope_b27.py +277 -0
- package/md_cg/test_readcache_default_on.py +168 -0
- package/md_cg/test_readcache_precise_inval.py +270 -0
- package/md_cg/test_readcache_prodpath.py +55 -7
- package/md_cg/test_reconcile_v0.py +294 -0
- package/md_cg/test_retr_s1.py +6 -2
- package/md_cg/test_retr_s1b.py +67 -0
- package/md_cg/test_retr_s7.py +8 -0
- package/md_cg/test_retr_s9_entity_ctx.py +181 -175
- package/md_cg/test_retr_score_once.py +208 -0
- package/md_cg/test_review_cli_attribution.py +177 -0
- package/md_cg/test_review_cli_visibility.py +235 -0
- package/md_cg/test_review_onepass.py +170 -0
- package/md_cg/test_rrf_graph_seed_cache.py +154 -0
- package/md_cg/test_security_audit.py +155 -0
- package/md_cg/test_security_audit_b26.py +161 -0
- package/md_cg/test_security_audit_v21.py +250 -0
- package/md_cg/test_semantic_canonical.py +255 -241
- package/md_cg/test_session_isolation.py +168 -0
- package/md_cg/test_snapshot_autoclose.py +187 -0
- package/md_cg/test_tail_watermark_race.py +208 -0
- package/md_cg/test_tenant_env_override_warn.py +139 -0
- package/md_cg/test_tenant_registry_corrupt_warn.py +151 -0
- package/md_cg/test_v14_fixes.py +415 -397
- package/md_cg/test_verify_dirty_reconcile.py +157 -0
- package/md_cg/theory.py +276 -273
- package/md_cg/tokens.py +734 -677
- package/md_cg/units.py +3 -2
- package/md_cg/vision_evidence.py +4 -3
- package/md_cg/whitebox_kb/wisdom/code_solidified.json +6195 -6195
- package/md_cg/whitebox_kb/wisdom/multilang_ir.py +128 -124
- package/md_cg/writepipe.py +554 -550
- package/package.json +11 -2
- package/src/bridge.ts +434 -401
- package/src/cli.ts +65 -0
- package/src/index.ts +526 -518
- package/src/init.ts +347 -0
- package/src/lib/roleplay_web.ts +1019 -932
- package/src/lib/token_store.ts +202 -192
package/md_cg/writepipe.py
CHANGED
|
@@ -1,551 +1,555 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""写入路径拦截器链(Pi 钩子化机制移植,交接文档 §3⑥「闸门即扩展」)。
|
|
3
|
-
|
|
4
|
-
pi 机制(packages/coding-agent/docs/extensions.md):全生命周期事件总线,
|
|
5
|
-
闸门/路径保护/审批全是以扩展形态叠加的,核心没有硬编码策略。灵枢对应:
|
|
6
|
-
write 的六道闸(audit 校验 / consistency 冲突 / review 审核 / gated 主动遗忘
|
|
7
|
-
/ writelimit 限流 / 权限)的次序与启停原先硬编码在 mcp_server._cg_dispatch
|
|
8
|
-
的 if/else 流程里,加一道闸要改核心文件。本模块把它重构为显式拦截器链:
|
|
9
|
-
|
|
10
|
-
- **before 链**:拦截器按注册序执行。返回 None = 放行(链继续);
|
|
11
|
-
返回 dict = 终态响应(短路——不落盘,或该拦截器已代为落盘/入队)。
|
|
12
|
-
- **after 链**:落盘成功后依次执行(观测者;返回值忽略;异常不吞——
|
|
13
|
-
写入已成功,钩子故障必须暴露而非静默)。
|
|
14
|
-
- **ctx 为可变 dict**:a=原始入参 / cg=实例 / nid=节点 id / verdict=校验闸
|
|
15
|
-
裁决 / cvd=冲突闸裁决。拦截器改写 ctx["a"]["content"] 等字段即实现
|
|
16
|
-
REWRITE(改写后传给后续链与执行器)。
|
|
17
|
-
- **链尾执行器(_executor)是常驻环节**,不在注册表中、不提供卸载 API;
|
|
18
|
-
它调 cg.add 落盘,而角色/层权限校验(require_layer_write)在
|
|
19
|
-
MdCGSecure.add **库层内部**——是落盘必经之路,结构上不可被任何拦截器
|
|
20
|
-
绕过(反面清单:不学 pi 的全权信任,信任必须结构强制)。
|
|
21
|
-
|
|
22
|
-
默认链(install_default_gates,与重构前 _cg_dispatch write 分支行为逐字
|
|
23
|
-
节一致):audit → consistency → gated → _executor。
|
|
24
|
-
|
|
25
|
-
验收口径(交接文档 §3⑥):全部既有写入测试零改动通过;新增/移除一个
|
|
26
|
-
拦截器不改核心文件(register_before / unregister_before 即插即拔)。
|
|
27
|
-
|
|
28
|
-
**③ 两段式提交(2026-09-16 叠加)**:链尾执行器与 gated 闸(两条**真实落盘**
|
|
29
|
-
路径)各自在执行落盘前调 `twophase.begin` 落 intent、落盘后调 `twophase.commit`
|
|
30
|
-
记 outcome;崩溃在两者之间时由 `twophase.reconcile` 据正文指纹补账/标记。
|
|
31
|
-
边界(如实):`_gate_audit` 的 REJECT(写负记忆)与各闸的 propose(**未落盘**,
|
|
32
|
-
仅入审核队列)不在两段式覆盖面内——前者是短小负记录、后者本就没有落盘动作。
|
|
33
|
-
|
|
34
|
-
**④ 写提交边界(2026-09-16 叠加)**:`execute` 的两条出口(before 链短路 /
|
|
35
|
-
链尾执行器 + after 链之后)统一调 `_commit_visibility`——把内存脏索引
|
|
36
|
-
`flush()` 到分片日志,使本次写入对**其他进程**立即可见。这是第16条「写入后
|
|
37
|
-
读回确认」的跨进程前置条件(server 级 `autoflush=1` 是同一问题的兜底,
|
|
38
|
-
覆盖不经本链的写入路径)。根因取证见 `_commit_visibility` 文档串。
|
|
39
|
-
"""
|
|
40
|
-
|
|
41
|
-
import time
|
|
42
|
-
|
|
43
|
-
from . import twophase, trust
|
|
44
|
-
|
|
45
|
-
__all__ = ["WritePipeline", "default_pipeline"]
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
# 生效条件:调用即对 cg 执行 flush()(无脏数据时为 no-op),且仅当该调用抛异常而 out 是 dict 时在 out 写入 "flush_error",异常本身不外抛;
|
|
49
|
-
def _commit_visibility(cg, out):
|
|
50
|
-
"""写提交边界(2026-09-16):把内存脏索引落分片日志,使本次写入对其他进程立即可见。
|
|
51
|
-
|
|
52
|
-
根因(第4条取证):写入只经 `_stage` 入内存 + `_dirty`,须达 `autoflush`
|
|
53
|
-
(默认 64)或 `close()` 才 `flush()` 落 `_index_log/`;MCP server 常驻、
|
|
54
|
-
不 close,故单条写入在阈值前**对其他进程不可见**——`_load_index` 读的是
|
|
55
|
-
「快照 `_index.json` + 分片日志重放」,而快照只在 compact/rebuild 时重写。
|
|
56
|
-
症状即第16条「写入后读回确认」在跨进程读面上系统性误报(写入返回
|
|
57
|
-
committed=true,读回却检索不到)。
|
|
58
|
-
|
|
59
|
-
边界(如实):无脏数据时 `flush()` 是 no-op,成本只在「确有落盘」时产生;
|
|
60
|
-
失败**不抛异常**——写入内容已落盘,抛出去会让调用方误判「写入失败」而
|
|
61
|
-
重试(两段式账本已记 committed,重试即重复写入)。改为在响应里如实标记
|
|
62
|
-
`flush_error`,不静默。
|
|
63
|
-
"""
|
|
64
|
-
try:
|
|
65
|
-
cg.flush()
|
|
66
|
-
except Exception as exc: # noqa: BLE001 —— 索引可见性故障不得改写写入语义
|
|
67
|
-
if isinstance(out, dict):
|
|
68
|
-
out["flush_error"] = "%s: %s" % (type(exc).__name__, exc)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
class WritePipeline:
|
|
72
|
-
"""写入拦截器链(实例级;default_pipeline() 提供进程级默认单例)。"""
|
|
73
|
-
|
|
74
|
-
# 生效条件:无前置;初始化 _before / _after 两条空链(元素为 (name, fn) 二元组),不做任何注册、不触盘;
|
|
75
|
-
def __init__(self):
|
|
76
|
-
self._before = [] # [(name, fn)]
|
|
77
|
-
self._after = [] # [(name, fn)]
|
|
78
|
-
|
|
79
|
-
# ---------- 注册表 ----------
|
|
80
|
-
|
|
81
|
-
# 生效条件:fn 可调用时先按 name 摘除同名项,再在 position 为 None 时把 (str(name), fn) 追加到链尾、否则插入 max(0, int(position))(position=0 非 None,走插入分支);fn 不可调用则抛 TypeError;
|
|
82
|
-
def register_before(self, name, fn, position=None):
|
|
83
|
-
"""注册 before 拦截器(同名幂等替换;position=None 追加到链尾)。
|
|
84
|
-
|
|
85
|
-
fn(ctx) -> None | dict(终态响应,短路)。
|
|
86
|
-
"""
|
|
87
|
-
if not callable(fn):
|
|
88
|
-
raise TypeError(f"拦截器必须可调用:{name!r}")
|
|
89
|
-
self.unregister_before(name)
|
|
90
|
-
item = (str(name), fn)
|
|
91
|
-
if position is None:
|
|
92
|
-
self._before.append(item)
|
|
93
|
-
else:
|
|
94
|
-
self._before.insert(max(0, int(position)), item)
|
|
95
|
-
|
|
96
|
-
# 生效条件:按 str(name) 过滤 _before,仅保留 x[0] != str(name) 的项(即删除全部同名项),并返回删除前后长度是否不等以表示是否确有移除;
|
|
97
|
-
def unregister_before(self, name):
|
|
98
|
-
n0 = len(self._before)
|
|
99
|
-
self._before = [x for x in self._before if x[0] != str(name)]
|
|
100
|
-
return len(self._before) != n0
|
|
101
|
-
|
|
102
|
-
# 生效条件:fn 可调用时先按 name 摘除同名项,再把 (str(name), fn) 追加到 _after 链尾;fn 不可调用则抛 TypeError;
|
|
103
|
-
def register_after(self, name, fn):
|
|
104
|
-
"""注册 after 观察者:fn(ctx, out),落盘成功后按序调用。"""
|
|
105
|
-
if not callable(fn):
|
|
106
|
-
raise TypeError(f"after 钩子必须可调用:{name!r}")
|
|
107
|
-
self.unregister_after(name)
|
|
108
|
-
self._after.append((str(name), fn))
|
|
109
|
-
|
|
110
|
-
# 生效条件:按 str(name) 过滤 _after,仅保留 x[0] != str(name) 的项(即删除全部同名项),并返回删除前后长度是否不等以表示是否确有移除;
|
|
111
|
-
def unregister_after(self, name):
|
|
112
|
-
n0 = len(self._after)
|
|
113
|
-
self._after = [x for x in self._after if x[0] != str(name)]
|
|
114
|
-
return len(self._after) != n0
|
|
115
|
-
|
|
116
|
-
# 生效条件:无前置;返回 {"before": [...注册名], "after": [...注册名]},只暴露名字不暴露函数对象,顺序即执行顺序;
|
|
117
|
-
def names(self):
|
|
118
|
-
return {"before": [n for n, _f in self._before],
|
|
119
|
-
"after": [n for n, _f in self._after]}
|
|
120
|
-
|
|
121
|
-
# ---------- 执行 ----------
|
|
122
|
-
|
|
123
|
-
# 生效条件:传入 cg 与 a(a 为假值如 None 时按 {} 处理,nid 取 a.get("node_id") 或其假值回落 "mem_"+毫秒时间戳),任一 before 钩子返回非 None 即记 halted_by 并经 _commit_visibility 短路返回该响应,全部放行则记 twophase 意图后跑 _executor(其抛 BaseException 时记 STATUS_ERROR 并原样重抛)再顺序跑 after 链、_commit_visibility 并返回落盘 out;
|
|
124
|
-
def execute(self, cg, a):
|
|
125
|
-
"""写入请求入口:跑 before 链 → 链尾执行器 → after 链。
|
|
126
|
-
|
|
127
|
-
before 链任一非 None 返回值即终态响应(与重构前各分支的 return
|
|
128
|
-
形态逐字节一致);链尾执行器产生落盘响应,after 链只观测不改写。
|
|
129
|
-
"""
|
|
130
|
-
a = a or {}
|
|
131
|
-
ctx = {"cg": cg, "a": a,
|
|
132
|
-
"nid": a.get("node_id")
|
|
133
|
-
or ("mem_" + str(int(time.time() * 1000))),
|
|
134
|
-
"verdict": None, "cvd": None}
|
|
135
|
-
for name, fn in self._before:
|
|
136
|
-
out = fn(ctx)
|
|
137
|
-
if out is not None:
|
|
138
|
-
ctx["halted_by"] = name
|
|
139
|
-
_commit_visibility(cg, out)
|
|
140
|
-
return out
|
|
141
|
-
# ③ 两段式:闸门**全部放行**(确认要写)→ 先落意图,再执行落盘,
|
|
142
|
-
# 最后记结果。崩溃若发生在两者之间,`reconcile` 能据正文指纹回答
|
|
143
|
-
# 「那笔写入到底落盘了没有」,而不是留下一条无痕的静默记忆。
|
|
144
|
-
tok = twophase.begin(cg, ctx["nid"], a.get("content", ""),
|
|
145
|
-
layer=a.get("layer") or "knowledge",
|
|
146
|
-
actor="writepipe:executor")
|
|
147
|
-
try:
|
|
148
|
-
out = _executor(ctx)
|
|
149
|
-
except BaseException as exc:
|
|
150
|
-
# 执行器抛异常(权限拒绝/校验失败)= 写入未完成 → 账本记 error,
|
|
151
|
-
# 异常照抛不吞(两段式只记账,不改写既有错误语义)。
|
|
152
|
-
twophase.commit(cg, tok, status=twophase.STATUS_ERROR,
|
|
153
|
-
reason=type(exc).__name__)
|
|
154
|
-
raise
|
|
155
|
-
ctx["out"] = out
|
|
156
|
-
twophase.commit(cg, tok, status=twophase.STATUS_COMMITTED,
|
|
157
|
-
reason="executor_ok")
|
|
158
|
-
for _name, fn in self._after:
|
|
159
|
-
fn(ctx, out)
|
|
160
|
-
_commit_visibility(cg, out)
|
|
161
|
-
return out
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
# --------------------------------------------------------------------------
|
|
165
|
-
# 默认链(原 mcp_server._cg_dispatch op=="write" 分支,行为逐字节搬运)
|
|
166
|
-
# --------------------------------------------------------------------------
|
|
167
|
-
|
|
168
|
-
# 生效条件:ctx["a"] 经 audit.audit 得出的 state 为 ACCEPT 时返 None 放行,为 REJECT 时经 cg.add_rejected 返回 ok=False/moved_to="rejected",其余 state 经 cg.propose 返回 moved_to="review_queue"(pr 带 dedup 时再附 dedup/dup_of/dup_status 并改写 hint);
|
|
169
|
-
def _gate_audit(ctx):
|
|
170
|
-
"""校验闸:audit.audit 四态。ACCEPT 放行;REJECT 负记忆;其余入审核队列。"""
|
|
171
|
-
a = ctx["a"]
|
|
172
|
-
cg = ctx["cg"]
|
|
173
|
-
from . import audit
|
|
174
|
-
payload = {"content": a.get("content", ""), "action": a.get("action"),
|
|
175
|
-
"sensitivity": a.get("sensitivity"),
|
|
176
|
-
"topic": a.get("query") or a.get("intent")}
|
|
177
|
-
if (a.get("content_kind") or "").strip() == "hyperedge":
|
|
178
|
-
# 超边验证器(回放比对)需要锚与结构键:fm 键平铺在 a 顶层,
|
|
179
|
-
# 经 hyperedge.audit_payload 装配三键载荷(缺锚由验证器 fail-closed)。
|
|
180
|
-
from . import hyperedge as _he
|
|
181
|
-
payload = _he.audit_payload(a)
|
|
182
|
-
verdict = audit.audit(
|
|
183
|
-
(a.get("content_kind") or "").strip(),
|
|
184
|
-
payload,
|
|
185
|
-
{"cg": cg, "principal": getattr(cg, "principal", None)})
|
|
186
|
-
ctx["verdict"] = verdict
|
|
187
|
-
st = verdict["state"]
|
|
188
|
-
if st == audit.ACCEPT:
|
|
189
|
-
return None
|
|
190
|
-
if st == audit.REJECT:
|
|
191
|
-
rid = cg.add_rejected((a.get("content") or "")[:200], verdict["evidence"],
|
|
192
|
-
verification_basis=verdict.get("basis") or "test",
|
|
193
|
-
tags=a.get("tags"))
|
|
194
|
-
return {"ok": False, "id": rid, "committed": False,
|
|
195
|
-
"moved_to": "rejected", "verdict": verdict,
|
|
196
|
-
"hint": "这是审核闸门的正常行为:内容未过内容政策审核(REJECT),"
|
|
197
|
-
"已记入负记忆——不是工具故障,重试同样结果;"
|
|
198
|
-
"拒绝依据见 verdict.evidence"}
|
|
199
|
-
from .mcp_server import _proposal_extras
|
|
200
|
-
pr = cg.propose(ctx["nid"], a.get("content", ""), info=True,
|
|
201
|
-
layer=a.get("layer") or "knowledge",
|
|
202
|
-
tags=a.get("tags"), condition_space=a.get("condition_space"),
|
|
203
|
-
**_proposal_extras(a, verdict))
|
|
204
|
-
out = {"ok": True, "id": ctx["nid"], "pid": pr["pid"], "committed": False,
|
|
205
|
-
"moved_to": "review_queue", "verdict": verdict,
|
|
206
|
-
"hint": "这是校验闸门的正常行为(verdict=%s):内容未达 ACCEPT,"
|
|
207
|
-
"已入审核队列——不需要重试;落盘须由设计者权限(can_admin)"
|
|
208
|
-
"对提案 pid 裁决(agent 端无裁决权是设计),转告使用者:"
|
|
209
|
-
"python
|
|
210
|
-
"或 cg(op=review, pid=<pid>, decision=accept|reject|"
|
|
211
|
-
"edit|merge, reason=<理由>)" % verdict.get("state")}
|
|
212
|
-
if pr.get("dedup"):
|
|
213
|
-
out["dedup"] = True
|
|
214
|
-
out["dup_of"] = pr["pid"]
|
|
215
|
-
out["dup_status"] = pr.get("dup_status")
|
|
216
|
-
out["hint"] = (
|
|
217
|
-
"同内容提案已存在(pid=%s,状态=%s,幂等去重),"
|
|
218
|
-
"本次未重复入队——无需重试;落盘须由设计者权限(can_admin)"
|
|
219
|
-
"对该 pid 裁决:python
|
|
220
|
-
"或 cg(op=review, pid=<pid>, decision=accept|reject|edit|merge, "
|
|
221
|
-
"reason=<理由>)" % (pr["pid"], pr.get("dup_status") or "pending"))
|
|
222
|
-
return out
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
# 生效条件:ctx["a"]["consistency"] 为假值时返回 None;on_conflict 缺键或假值回落 "defer",仅当 verdict=REJECT 且 on_conflict=reject(返回 moved_to="conflict_rejected")或 verdict∈{REJECT,BLINDSPOT} 且 on_conflict=defer(转 review_queue,去重命中时改写 hint)才拦截,其余 on_conflict 取值返回 None;
|
|
226
|
-
def _gate_consistency(ctx):
|
|
227
|
-
"""冲突闸:节点间自动冲突检测(三级决策)。
|
|
228
|
-
|
|
229
|
-
仅 REJECT(明确判为冲突)按 on_conflict 处置:reject=直接拒绝;
|
|
230
|
-
defer=转入审核队列。BLINDSPOT(无可比对节点,检测前提不存在)恒放行,
|
|
231
|
-
cvd 审计经链尾透出(issue #26:无法比对 ≠ 冲突,入队是死胡同)。
|
|
232
|
-
"""
|
|
233
|
-
a = ctx["a"]
|
|
234
|
-
cg = ctx["cg"]
|
|
235
|
-
if not bool(a.get("consistency", True)):
|
|
236
|
-
return None
|
|
237
|
-
from .mcp_server import _proposal_extras
|
|
238
|
-
oc = (a.get("on_conflict") or "defer").strip().lower()
|
|
239
|
-
cvd = cg.check_consistency(
|
|
240
|
-
a.get("content", ""),
|
|
241
|
-
layer=a.get("layer") or ("contextual" if a.get("gated")
|
|
242
|
-
else "knowledge"),
|
|
243
|
-
condition_space=a.get("condition_space"),
|
|
244
|
-
non_applicable_conditions=a.get("non_applicable_conditions"),
|
|
245
|
-
tags=a.get("tags"), exclude=ctx["nid"], auto_flywheel=True)
|
|
246
|
-
ctx["cvd"] = cvd
|
|
247
|
-
v = cvd.get("verdict")
|
|
248
|
-
# issue #26(2026-09-23):BLINDSPOT ≠ REJECT——冲突闸的 BLINDSPOT 唯一出口
|
|
249
|
-
# 是 comparable==0(既有节点无一声明条件,含空库),语义是「检测前提不
|
|
250
|
-
# 存在」而非「已判定冲突」;defer 入队后裁决者面对同样空白(无可操作
|
|
251
|
-
# 下一步,死胡同)。故 BLINDSPOT 恒放行:cvd 经链尾 _executor 的
|
|
252
|
-
# consistency 字段如实透出(放行原因可观测);REJECT(明确冲突)维持
|
|
253
|
-
# 原拦截语义。原先两者等同拦截 → 空库首次写入恒不落盘(README 推荐的
|
|
254
|
-
# content_kind=code 通路必失败——库越空越写不进)。
|
|
255
|
-
blocked = (v == "REJECT" and oc in ("reject", "defer"))
|
|
256
|
-
if not blocked:
|
|
257
|
-
return None
|
|
258
|
-
if oc == "reject":
|
|
259
|
-
return {"ok": False, "id": ctx["nid"], "committed": False,
|
|
260
|
-
"moved_to": "conflict_rejected",
|
|
261
|
-
"consistency": cvd, "verdict": ctx["verdict"]}
|
|
262
|
-
pr = cg.propose(ctx["nid"], a.get("content", ""), info=True,
|
|
263
|
-
layer=a.get("layer") or "knowledge",
|
|
264
|
-
tags=a.get("tags"),
|
|
265
|
-
condition_space=a.get("condition_space"),
|
|
266
|
-
**_proposal_extras(a, ctx["verdict"]))
|
|
267
|
-
out = {"ok": False, "id": ctx["nid"], "pid": pr["pid"],
|
|
268
|
-
"committed": False,
|
|
269
|
-
"moved_to": "review_queue", "consistency": cvd,
|
|
270
|
-
"verdict": ctx["verdict"],
|
|
271
|
-
"hint": "这是冲突闸门的正常行为:本次写入与既有条件/纪律冲突"
|
|
272
|
-
"(on_conflict=defer),已转入审核队列待裁决——"
|
|
273
|
-
"不是工具故障,重试同样结果;"
|
|
274
|
-
"落盘须由设计者权限(can_admin)裁决,转告使用者:"
|
|
275
|
-
"python
|
|
276
|
-
"或 cg(op=review, pid=<pid>, decision=accept|reject|"
|
|
277
|
-
"edit|merge, reason=<理由>)"}
|
|
278
|
-
if pr.get("dedup"):
|
|
279
|
-
out["dedup"] = True
|
|
280
|
-
out["dup_of"] = pr["pid"]
|
|
281
|
-
out["hint"] = (
|
|
282
|
-
"同内容提案已存在于审核队列(pid=%s,幂等去重),"
|
|
283
|
-
"本次未重复入队——无需重试;"
|
|
284
|
-
"落盘须由设计者权限(can_admin)对该 pid 裁决:"
|
|
285
|
-
"python
|
|
286
|
-
"或 cg(op=review, pid=<pid>, decision=accept|reject|"
|
|
287
|
-
"edit|merge, reason=<理由>)" % pr["pid"])
|
|
288
|
-
return out
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
# 生效条件:ctx["a"] 的 gated 为假值时返 None 放行;为真值时按 cg.remember_gated 返回的 verdict 落两段式账,且仅 verdict 为 ACCEPT 时 ok/committed 为 True,verdict 为 MERGE 时记 committed 并置 moved_to="merged_into:"+merged_into,verdict 为 DROP/DEFER 时记 aborted 且 moved_to 为其小写值;
|
|
292
|
-
def _gate_gated(ctx):
|
|
293
|
-
"""主动遗忘闸(gated=true 时启用):writelimit 限流 + forgetting 三问四态。
|
|
294
|
-
|
|
295
|
-
本闸是「替代执行路径」:命中即由 remember_gated 代为落盘/合并/丢弃并
|
|
296
|
-
返回终态;未启用(gated 假值)放行给链尾执行器。
|
|
297
|
-
"""
|
|
298
|
-
a = ctx["a"]
|
|
299
|
-
cg = ctx["cg"]
|
|
300
|
-
if not a.get("gated"):
|
|
301
|
-
return None
|
|
302
|
-
hint = a.get("importance_hint")
|
|
303
|
-
if hint is None and a.get("importance") is not None:
|
|
304
|
-
hint = float(a["importance"])
|
|
305
|
-
# ③ 两段式:本闸是**替代执行路径**(自己落盘),意图必须由它先记——
|
|
306
|
-
# 若等 execute 在链后统一记,intent 会晚于本闸内部的写盘,「先行持久化」
|
|
307
|
-
# 就不成立了。落盘前的窗口因此仍然被账本覆盖。
|
|
308
|
-
tok = twophase.begin(cg, ctx["nid"], a.get("content", ""),
|
|
309
|
-
layer=a.get("layer") or "contextual",
|
|
310
|
-
actor="writepipe:gated")
|
|
311
|
-
res = cg.remember_gated(
|
|
312
|
-
ctx["nid"], a.get("content", ""), layer=a.get("layer") or "contextual",
|
|
313
|
-
role=a.get("role"), tags=a.get("tags"),
|
|
314
|
-
condition_space=a.get("condition_space"),
|
|
315
|
-
verification_basis=(a.get("verification_basis")
|
|
316
|
-
or (ctx.get("verdict") or {}).get("basis")),
|
|
317
|
-
non_applicable_conditions=a.get("non_applicable_conditions"),
|
|
318
|
-
importance_hint=hint, override=bool(a.get("override")),
|
|
319
|
-
consistency=False,
|
|
320
|
-
derived_from=_split_ids(a.get("derived_from")),
|
|
321
|
-
relation=a.get("relation"))
|
|
322
|
-
v = res.get("verdict")
|
|
323
|
-
committed = v == "ACCEPT"
|
|
324
|
-
# 结局如实记:ACCEPT=落盘完成;MERGE=内容并入既有节点(不再以本次内容成
|
|
325
|
-
# 文,指纹对账不适用,故直接记 committed 并注明去向);DROP/DEFER=未落盘。
|
|
326
|
-
if committed:
|
|
327
|
-
twophase.commit(cg, tok, status=twophase.STATUS_COMMITTED,
|
|
328
|
-
reason="gated_accept")
|
|
329
|
-
elif v == "MERGE":
|
|
330
|
-
twophase.commit(cg, tok, status=twophase.STATUS_COMMITTED,
|
|
331
|
-
reason="merged_into:%s" % res.get("merged_into"))
|
|
332
|
-
else:
|
|
333
|
-
twophase.commit(cg, tok, status=twophase.STATUS_ABORTED,
|
|
334
|
-
reason="gated_%s" % str(v).lower())
|
|
335
|
-
out = {"ok": committed, "id": ctx["nid"], "committed": committed,
|
|
336
|
-
"gate": res, "verdict": ctx["verdict"]}
|
|
337
|
-
if ctx.get("cvd") is not None:
|
|
338
|
-
out["consistency"] = ctx["cvd"]
|
|
339
|
-
if v == "MERGE":
|
|
340
|
-
out["moved_to"] = "merged_into:" + str(res.get("merged_into"))
|
|
341
|
-
elif v in ("DROP", "DEFER"):
|
|
342
|
-
out["moved_to"] = v.lower()
|
|
343
|
-
# gated 是**替代落盘路径**(自行落盘/合并后直接返回终态、不跑 after 链),故
|
|
344
|
-
# 一跳同步传播须在此单独触发——否则 MERGE 类覆写会漏传下游(非对称边界,
|
|
345
|
-
# 与 `_after_trust` 注释互指)。
|
|
346
|
-
if committed or v == "MERGE":
|
|
347
|
-
prop = trust.mark_dependents(
|
|
348
|
-
cg, ctx["nid"],
|
|
349
|
-
reason="上游节点被 gated 写入/合并(内容或验证态可能已变)",
|
|
350
|
-
actor="writepipe:gated", trigger="write_gated")
|
|
351
|
-
if isinstance(prop, dict) and prop.get("changed"):
|
|
352
|
-
out["propagation"] = {"changed": prop.get("changed"),
|
|
353
|
-
"updated": (prop.get("updated") or [])[:10]}
|
|
354
|
-
return out
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
# 生效条件:ctx["a"]["content"] 的「# 子功能:」行含显式跨节点引用(`@<节点 id>`)且 depends_on 解析为空时返回 ok=False/error="E050" 的终态;depends_on 含库中不存在的 id 时返回 ok=False/error="E051" 的终态;其余(无该行 / 哨兵 / 自然语言自述 / 声明且目标齐备)返回 None 放行;
|
|
358
|
-
def _gate_deps(ctx):
|
|
359
|
-
"""依赖声明闸(before 链:linkref 之后、audit 之前):**硬拒条件缺失**。
|
|
360
|
-
|
|
361
|
-
「声明」的界定(收窄裁定 b,2026-09-19):以 `@<节点 id>` 显式引用为界——
|
|
362
|
-
自然语言**自述子功能**(描述本单元**内部**构成)不构成依赖声明。原因:CCG
|
|
363
|
-
编译产物六要素必含「子功能」行,若沿用「非哨兵即声明」,每个 CCG 节点落库后
|
|
364
|
-
都会被自己的闸门永久要求 depends_on(E050 死锁,test_ccgc V16f 实证)。
|
|
365
|
-
依赖不是必填元数据;**只有显式声称依赖却不落字段**才是违规(声称与落盘不一致)。
|
|
366
|
-
|
|
367
|
-
为何是硬拒而非告警:依赖是失效传播的**唯一入口**。声明缺失时,「上游变了
|
|
368
|
-
下游要存疑」这条链从源头就不存在——它既不报错、也不留任何信号,缺陷以
|
|
369
|
-
「静默不传播」的形态长期存活(比报错更难发现)。故按契约缺失处理,与
|
|
370
|
-
`ccgc` 的 E 码体系同构(E050 声明缺失 / E051 目标不存在)。
|
|
371
|
-
|
|
372
|
-
张力消解(与 provenance「写路径永不阻断」纪律的边界,二者不冲突):
|
|
373
|
-
- **声明缺失 / 目标不可解析 = 契约违规** → 硬拒(本闸只做这件事);
|
|
374
|
-
- **传播落盘失败 = 运维降级** → 告警不阻断(见 `_after_trust`)。
|
|
375
|
-
|
|
376
|
-
判据基于**入参**而非落盘后回读:首次写入时节点尚不存在,回读式校验会
|
|
377
|
-
永远放行(等于闸门失效)。
|
|
378
|
-
"""
|
|
379
|
-
a = ctx["a"]
|
|
380
|
-
cg = ctx["cg"]
|
|
381
|
-
from . import nodefile
|
|
382
|
-
if not nodefile.declares_dependency(a.get("content") or ""):
|
|
383
|
-
return None
|
|
384
|
-
deps = trust.as_deps(a.get("depends_on"))
|
|
385
|
-
if not deps:
|
|
386
|
-
return {"ok": False, "id": ctx["nid"], "committed": False,
|
|
387
|
-
"gate": "deps", "error": "E050",
|
|
388
|
-
"verdict": ctx.get("verdict"),
|
|
389
|
-
"hint": ("依赖声明缺失(E050):正文以 " + nodefile.DEP_REF_MARK
|
|
390
|
-
+ "<节点 id> 显式声明了跨节点依赖(「# 子功能:」行),"
|
|
391
|
-
"但 depends_on 未给出可解析目标。依赖必须是**可解析的字段**"
|
|
392
|
-
"(形如 depends_on=[\"<被依赖节点 id>\"]),不能只是散文——"
|
|
393
|
-
"否则被依赖单元变动时,下游无处可传。补齐后重试;"
|
|
394
|
-
"若该行只是描述本单元内部构成(自述),去掉 "
|
|
395
|
-
+ nodefile.DEP_REF_MARK + " 引用或改填「无」即可。"
|
|
396
|
-
"本闸是契约闸门的正常行为,不是工具故障。")}
|
|
397
|
-
known = set((getattr(cg, "index", None) or {}).get("nodes") or {})
|
|
398
|
-
missing = [d for d in deps if d not in known]
|
|
399
|
-
if missing:
|
|
400
|
-
return {"ok": False, "id": ctx["nid"], "committed": False,
|
|
401
|
-
"gate": "deps", "error": "E051", "missing": missing[:10],
|
|
402
|
-
"verdict": ctx.get("verdict"),
|
|
403
|
-
"hint": "依赖目标不存在(E051):depends_on 指向 "
|
|
404
|
-
+ ", ".join(missing[:5])
|
|
405
|
-
+ ",但库中查无此节点——声称依赖一个并不存在的地基,"
|
|
406
|
-
"失效传播会在此处断链。请先建立被依赖节点,或修正 id。"}
|
|
407
|
-
return None
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
# 生效条件:out 为 dict 且 out["committed"] 为真时,调 trust.mark_dependents 做一跳同步传播(异常吞掉并降级),并在节点时间轴非「时效内」时往 out 写 "validity" 提示;其余情况直接返回不做任何动作;
|
|
411
|
-
def _after_trust(ctx, out):
|
|
412
|
-
"""after 观察者:落盘后触发**一跳同步传播** + 时效提示。
|
|
413
|
-
|
|
414
|
-
为何落在 after 而非 before:只有真正落盘(内容确实变了)才构成「地基动了」;
|
|
415
|
-
before 链短路路径(REJECT / DEFER)本就不跑 after 链,语义天然正确。
|
|
416
|
-
例外:`gated` 是替代落盘路径(自行落盘并返回终态、不跑 after),故它的
|
|
417
|
-
传播在 `_gate_gated` 内部单独触发(见该处注释)。
|
|
418
|
-
|
|
419
|
-
**永不抛**:传播失败只降级(`trust.mark_dependents` 内部已兜底并写台账),
|
|
420
|
-
绝不把「写入已成功」改写为失败——与 `_commit_visibility` 同款边界。
|
|
421
|
-
"""
|
|
422
|
-
if not isinstance(out, dict) or not out.get("committed"):
|
|
423
|
-
return
|
|
424
|
-
cg = ctx["cg"]
|
|
425
|
-
nid = ctx.get("nid")
|
|
426
|
-
if not nid:
|
|
427
|
-
return
|
|
428
|
-
prop = trust.mark_dependents(
|
|
429
|
-
cg, nid, reason="上游节点被写入/覆写(内容或验证态可能已变)",
|
|
430
|
-
actor="writepipe:trust", trigger="write")
|
|
431
|
-
if isinstance(prop, dict) and prop.get("changed"):
|
|
432
|
-
out["propagation"] = {"changed": prop.get("changed"),
|
|
433
|
-
"updated": (prop.get("updated") or [])[:10]}
|
|
434
|
-
# 热路径失效(2026-09-19 热温冷分层):写入后清缓存
|
|
435
|
-
try:
|
|
436
|
-
from . import hotcache as _hc
|
|
437
|
-
_hc.invalidate(cg, nid)
|
|
438
|
-
except Exception: # noqa: BLE001
|
|
439
|
-
pass # 缓存失效失败不阻断写入(永不抛)
|
|
440
|
-
|
|
441
|
-
# 冷路径入队(2026-09-19 热温冷分层):异步深度验证
|
|
442
|
-
try:
|
|
443
|
-
from . import coldverify as _cv
|
|
444
|
-
_cv.enqueue(cg, nid, action="reverify",
|
|
445
|
-
reason="writepipe:after_trust")
|
|
446
|
-
except Exception: # noqa: BLE001
|
|
447
|
-
pass # 入队失败不阻断写入
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
# 生效条件:a 的 content_kind 非 hyperedge 即返回 {},为 hyperedge 时延迟导入
|
|
451
|
-
# hyperedge.EXTRA_FM_KEYS 收集 a 中非 None 的对应键返回(其余写入零键透传);
|
|
452
|
-
def _hyperedge_extra(a):
|
|
453
|
-
"""hyperedge 写入的 fm 扩展键透传。计划决策 2:超边写入走 write 动词既有
|
|
454
|
-
审核链、不造新通道——链尾执行器是唯一落盘点,cg.add 经 **extra 落 fm,
|
|
455
|
-
故扩展键在此透传;落盘面丢字段 = 上游声明静默失效。延迟导入防循环依赖。"""
|
|
456
|
-
if (a.get("content_kind") or "").strip() != "hyperedge":
|
|
457
|
-
return {}
|
|
458
|
-
from . import hyperedge as _he
|
|
459
|
-
return {k: a[k] for k in _he.EXTRA_FM_KEYS if a.get(k) is not None}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
# 生效条件:由链尾以含 cg 与 a 的 ctx 调用即无条件执行 cg.add 落盘并返回 ok=True/committed=True,ctx["cvd"] 非 None 时附加 consistency 字段;
|
|
463
|
-
def _executor(ctx):
|
|
464
|
-
"""链尾执行器(常驻不可卸载):cg.add 直写落盘。
|
|
465
|
-
|
|
466
|
-
角色/层权限校验(require_layer_write)在 MdCGSecure.add 库层内部,
|
|
467
|
-
结构上不可被拦截器绕过——拦截器只能裁决「写不写」,改不了「谁能写」。
|
|
468
|
-
"""
|
|
469
|
-
a = ctx["a"]
|
|
470
|
-
cg = ctx["cg"]
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
"
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
""
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
pipe.register_before("
|
|
540
|
-
pipe.
|
|
541
|
-
pipe.
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""写入路径拦截器链(Pi 钩子化机制移植,交接文档 §3⑥「闸门即扩展」)。
|
|
3
|
+
|
|
4
|
+
pi 机制(packages/coding-agent/docs/extensions.md):全生命周期事件总线,
|
|
5
|
+
闸门/路径保护/审批全是以扩展形态叠加的,核心没有硬编码策略。灵枢对应:
|
|
6
|
+
write 的六道闸(audit 校验 / consistency 冲突 / review 审核 / gated 主动遗忘
|
|
7
|
+
/ writelimit 限流 / 权限)的次序与启停原先硬编码在 mcp_server._cg_dispatch
|
|
8
|
+
的 if/else 流程里,加一道闸要改核心文件。本模块把它重构为显式拦截器链:
|
|
9
|
+
|
|
10
|
+
- **before 链**:拦截器按注册序执行。返回 None = 放行(链继续);
|
|
11
|
+
返回 dict = 终态响应(短路——不落盘,或该拦截器已代为落盘/入队)。
|
|
12
|
+
- **after 链**:落盘成功后依次执行(观测者;返回值忽略;异常不吞——
|
|
13
|
+
写入已成功,钩子故障必须暴露而非静默)。
|
|
14
|
+
- **ctx 为可变 dict**:a=原始入参 / cg=实例 / nid=节点 id / verdict=校验闸
|
|
15
|
+
裁决 / cvd=冲突闸裁决。拦截器改写 ctx["a"]["content"] 等字段即实现
|
|
16
|
+
REWRITE(改写后传给后续链与执行器)。
|
|
17
|
+
- **链尾执行器(_executor)是常驻环节**,不在注册表中、不提供卸载 API;
|
|
18
|
+
它调 cg.add 落盘,而角色/层权限校验(require_layer_write)在
|
|
19
|
+
MdCGSecure.add **库层内部**——是落盘必经之路,结构上不可被任何拦截器
|
|
20
|
+
绕过(反面清单:不学 pi 的全权信任,信任必须结构强制)。
|
|
21
|
+
|
|
22
|
+
默认链(install_default_gates,与重构前 _cg_dispatch write 分支行为逐字
|
|
23
|
+
节一致):audit → consistency → gated → _executor。
|
|
24
|
+
|
|
25
|
+
验收口径(交接文档 §3⑥):全部既有写入测试零改动通过;新增/移除一个
|
|
26
|
+
拦截器不改核心文件(register_before / unregister_before 即插即拔)。
|
|
27
|
+
|
|
28
|
+
**③ 两段式提交(2026-09-16 叠加)**:链尾执行器与 gated 闸(两条**真实落盘**
|
|
29
|
+
路径)各自在执行落盘前调 `twophase.begin` 落 intent、落盘后调 `twophase.commit`
|
|
30
|
+
记 outcome;崩溃在两者之间时由 `twophase.reconcile` 据正文指纹补账/标记。
|
|
31
|
+
边界(如实):`_gate_audit` 的 REJECT(写负记忆)与各闸的 propose(**未落盘**,
|
|
32
|
+
仅入审核队列)不在两段式覆盖面内——前者是短小负记录、后者本就没有落盘动作。
|
|
33
|
+
|
|
34
|
+
**④ 写提交边界(2026-09-16 叠加)**:`execute` 的两条出口(before 链短路 /
|
|
35
|
+
链尾执行器 + after 链之后)统一调 `_commit_visibility`——把内存脏索引
|
|
36
|
+
`flush()` 到分片日志,使本次写入对**其他进程**立即可见。这是第16条「写入后
|
|
37
|
+
读回确认」的跨进程前置条件(server 级 `autoflush=1` 是同一问题的兜底,
|
|
38
|
+
覆盖不经本链的写入路径)。根因取证见 `_commit_visibility` 文档串。
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
import time
|
|
42
|
+
|
|
43
|
+
from . import twophase, trust
|
|
44
|
+
|
|
45
|
+
__all__ = ["WritePipeline", "default_pipeline"]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# 生效条件:调用即对 cg 执行 flush()(无脏数据时为 no-op),且仅当该调用抛异常而 out 是 dict 时在 out 写入 "flush_error",异常本身不外抛;
|
|
49
|
+
def _commit_visibility(cg, out):
|
|
50
|
+
"""写提交边界(2026-09-16):把内存脏索引落分片日志,使本次写入对其他进程立即可见。
|
|
51
|
+
|
|
52
|
+
根因(第4条取证):写入只经 `_stage` 入内存 + `_dirty`,须达 `autoflush`
|
|
53
|
+
(默认 64)或 `close()` 才 `flush()` 落 `_index_log/`;MCP server 常驻、
|
|
54
|
+
不 close,故单条写入在阈值前**对其他进程不可见**——`_load_index` 读的是
|
|
55
|
+
「快照 `_index.json` + 分片日志重放」,而快照只在 compact/rebuild 时重写。
|
|
56
|
+
症状即第16条「写入后读回确认」在跨进程读面上系统性误报(写入返回
|
|
57
|
+
committed=true,读回却检索不到)。
|
|
58
|
+
|
|
59
|
+
边界(如实):无脏数据时 `flush()` 是 no-op,成本只在「确有落盘」时产生;
|
|
60
|
+
失败**不抛异常**——写入内容已落盘,抛出去会让调用方误判「写入失败」而
|
|
61
|
+
重试(两段式账本已记 committed,重试即重复写入)。改为在响应里如实标记
|
|
62
|
+
`flush_error`,不静默。
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
cg.flush()
|
|
66
|
+
except Exception as exc: # noqa: BLE001 —— 索引可见性故障不得改写写入语义
|
|
67
|
+
if isinstance(out, dict):
|
|
68
|
+
out["flush_error"] = "%s: %s" % (type(exc).__name__, exc)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class WritePipeline:
|
|
72
|
+
"""写入拦截器链(实例级;default_pipeline() 提供进程级默认单例)。"""
|
|
73
|
+
|
|
74
|
+
# 生效条件:无前置;初始化 _before / _after 两条空链(元素为 (name, fn) 二元组),不做任何注册、不触盘;
|
|
75
|
+
def __init__(self):
|
|
76
|
+
self._before = [] # [(name, fn)]
|
|
77
|
+
self._after = [] # [(name, fn)]
|
|
78
|
+
|
|
79
|
+
# ---------- 注册表 ----------
|
|
80
|
+
|
|
81
|
+
# 生效条件:fn 可调用时先按 name 摘除同名项,再在 position 为 None 时把 (str(name), fn) 追加到链尾、否则插入 max(0, int(position))(position=0 非 None,走插入分支);fn 不可调用则抛 TypeError;
|
|
82
|
+
def register_before(self, name, fn, position=None):
|
|
83
|
+
"""注册 before 拦截器(同名幂等替换;position=None 追加到链尾)。
|
|
84
|
+
|
|
85
|
+
fn(ctx) -> None | dict(终态响应,短路)。
|
|
86
|
+
"""
|
|
87
|
+
if not callable(fn):
|
|
88
|
+
raise TypeError(f"拦截器必须可调用:{name!r}")
|
|
89
|
+
self.unregister_before(name)
|
|
90
|
+
item = (str(name), fn)
|
|
91
|
+
if position is None:
|
|
92
|
+
self._before.append(item)
|
|
93
|
+
else:
|
|
94
|
+
self._before.insert(max(0, int(position)), item)
|
|
95
|
+
|
|
96
|
+
# 生效条件:按 str(name) 过滤 _before,仅保留 x[0] != str(name) 的项(即删除全部同名项),并返回删除前后长度是否不等以表示是否确有移除;
|
|
97
|
+
def unregister_before(self, name):
|
|
98
|
+
n0 = len(self._before)
|
|
99
|
+
self._before = [x for x in self._before if x[0] != str(name)]
|
|
100
|
+
return len(self._before) != n0
|
|
101
|
+
|
|
102
|
+
# 生效条件:fn 可调用时先按 name 摘除同名项,再把 (str(name), fn) 追加到 _after 链尾;fn 不可调用则抛 TypeError;
|
|
103
|
+
def register_after(self, name, fn):
|
|
104
|
+
"""注册 after 观察者:fn(ctx, out),落盘成功后按序调用。"""
|
|
105
|
+
if not callable(fn):
|
|
106
|
+
raise TypeError(f"after 钩子必须可调用:{name!r}")
|
|
107
|
+
self.unregister_after(name)
|
|
108
|
+
self._after.append((str(name), fn))
|
|
109
|
+
|
|
110
|
+
# 生效条件:按 str(name) 过滤 _after,仅保留 x[0] != str(name) 的项(即删除全部同名项),并返回删除前后长度是否不等以表示是否确有移除;
|
|
111
|
+
def unregister_after(self, name):
|
|
112
|
+
n0 = len(self._after)
|
|
113
|
+
self._after = [x for x in self._after if x[0] != str(name)]
|
|
114
|
+
return len(self._after) != n0
|
|
115
|
+
|
|
116
|
+
# 生效条件:无前置;返回 {"before": [...注册名], "after": [...注册名]},只暴露名字不暴露函数对象,顺序即执行顺序;
|
|
117
|
+
def names(self):
|
|
118
|
+
return {"before": [n for n, _f in self._before],
|
|
119
|
+
"after": [n for n, _f in self._after]}
|
|
120
|
+
|
|
121
|
+
# ---------- 执行 ----------
|
|
122
|
+
|
|
123
|
+
# 生效条件:传入 cg 与 a(a 为假值如 None 时按 {} 处理,nid 取 a.get("node_id") 或其假值回落 "mem_"+毫秒时间戳),任一 before 钩子返回非 None 即记 halted_by 并经 _commit_visibility 短路返回该响应,全部放行则记 twophase 意图后跑 _executor(其抛 BaseException 时记 STATUS_ERROR 并原样重抛)再顺序跑 after 链、_commit_visibility 并返回落盘 out;
|
|
124
|
+
def execute(self, cg, a):
|
|
125
|
+
"""写入请求入口:跑 before 链 → 链尾执行器 → after 链。
|
|
126
|
+
|
|
127
|
+
before 链任一非 None 返回值即终态响应(与重构前各分支的 return
|
|
128
|
+
形态逐字节一致);链尾执行器产生落盘响应,after 链只观测不改写。
|
|
129
|
+
"""
|
|
130
|
+
a = a or {}
|
|
131
|
+
ctx = {"cg": cg, "a": a,
|
|
132
|
+
"nid": a.get("node_id")
|
|
133
|
+
or ("mem_" + str(int(time.time() * 1000))),
|
|
134
|
+
"verdict": None, "cvd": None}
|
|
135
|
+
for name, fn in self._before:
|
|
136
|
+
out = fn(ctx)
|
|
137
|
+
if out is not None:
|
|
138
|
+
ctx["halted_by"] = name
|
|
139
|
+
_commit_visibility(cg, out)
|
|
140
|
+
return out
|
|
141
|
+
# ③ 两段式:闸门**全部放行**(确认要写)→ 先落意图,再执行落盘,
|
|
142
|
+
# 最后记结果。崩溃若发生在两者之间,`reconcile` 能据正文指纹回答
|
|
143
|
+
# 「那笔写入到底落盘了没有」,而不是留下一条无痕的静默记忆。
|
|
144
|
+
tok = twophase.begin(cg, ctx["nid"], a.get("content", ""),
|
|
145
|
+
layer=a.get("layer") or "knowledge",
|
|
146
|
+
actor="writepipe:executor")
|
|
147
|
+
try:
|
|
148
|
+
out = _executor(ctx)
|
|
149
|
+
except BaseException as exc:
|
|
150
|
+
# 执行器抛异常(权限拒绝/校验失败)= 写入未完成 → 账本记 error,
|
|
151
|
+
# 异常照抛不吞(两段式只记账,不改写既有错误语义)。
|
|
152
|
+
twophase.commit(cg, tok, status=twophase.STATUS_ERROR,
|
|
153
|
+
reason=type(exc).__name__)
|
|
154
|
+
raise
|
|
155
|
+
ctx["out"] = out
|
|
156
|
+
twophase.commit(cg, tok, status=twophase.STATUS_COMMITTED,
|
|
157
|
+
reason="executor_ok")
|
|
158
|
+
for _name, fn in self._after:
|
|
159
|
+
fn(ctx, out)
|
|
160
|
+
_commit_visibility(cg, out)
|
|
161
|
+
return out
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# --------------------------------------------------------------------------
|
|
165
|
+
# 默认链(原 mcp_server._cg_dispatch op=="write" 分支,行为逐字节搬运)
|
|
166
|
+
# --------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
# 生效条件:ctx["a"] 经 audit.audit 得出的 state 为 ACCEPT 时返 None 放行,为 REJECT 时经 cg.add_rejected 返回 ok=False/moved_to="rejected",其余 state 经 cg.propose 返回 moved_to="review_queue"(pr 带 dedup 时再附 dedup/dup_of/dup_status 并改写 hint);
|
|
169
|
+
def _gate_audit(ctx):
|
|
170
|
+
"""校验闸:audit.audit 四态。ACCEPT 放行;REJECT 负记忆;其余入审核队列。"""
|
|
171
|
+
a = ctx["a"]
|
|
172
|
+
cg = ctx["cg"]
|
|
173
|
+
from . import audit
|
|
174
|
+
payload = {"content": a.get("content", ""), "action": a.get("action"),
|
|
175
|
+
"sensitivity": a.get("sensitivity"),
|
|
176
|
+
"topic": a.get("query") or a.get("intent")}
|
|
177
|
+
if (a.get("content_kind") or "").strip() == "hyperedge":
|
|
178
|
+
# 超边验证器(回放比对)需要锚与结构键:fm 键平铺在 a 顶层,
|
|
179
|
+
# 经 hyperedge.audit_payload 装配三键载荷(缺锚由验证器 fail-closed)。
|
|
180
|
+
from . import hyperedge as _he
|
|
181
|
+
payload = _he.audit_payload(a)
|
|
182
|
+
verdict = audit.audit(
|
|
183
|
+
(a.get("content_kind") or "").strip(),
|
|
184
|
+
payload,
|
|
185
|
+
{"cg": cg, "principal": getattr(cg, "principal", None)})
|
|
186
|
+
ctx["verdict"] = verdict
|
|
187
|
+
st = verdict["state"]
|
|
188
|
+
if st == audit.ACCEPT:
|
|
189
|
+
return None
|
|
190
|
+
if st == audit.REJECT:
|
|
191
|
+
rid = cg.add_rejected((a.get("content") or "")[:200], verdict["evidence"],
|
|
192
|
+
verification_basis=verdict.get("basis") or "test",
|
|
193
|
+
tags=a.get("tags"))
|
|
194
|
+
return {"ok": False, "id": rid, "committed": False,
|
|
195
|
+
"moved_to": "rejected", "verdict": verdict,
|
|
196
|
+
"hint": "这是审核闸门的正常行为:内容未过内容政策审核(REJECT),"
|
|
197
|
+
"已记入负记忆——不是工具故障,重试同样结果;"
|
|
198
|
+
"拒绝依据见 verdict.evidence"}
|
|
199
|
+
from .mcp_server import _proposal_extras
|
|
200
|
+
pr = cg.propose(ctx["nid"], a.get("content", ""), info=True,
|
|
201
|
+
layer=a.get("layer") or "knowledge",
|
|
202
|
+
tags=a.get("tags"), condition_space=a.get("condition_space"),
|
|
203
|
+
**_proposal_extras(a, verdict))
|
|
204
|
+
out = {"ok": True, "id": ctx["nid"], "pid": pr["pid"], "committed": False,
|
|
205
|
+
"moved_to": "review_queue", "verdict": verdict,
|
|
206
|
+
"hint": "这是校验闸门的正常行为(verdict=%s):内容未达 ACCEPT,"
|
|
207
|
+
"已入审核队列——不需要重试;落盘须由设计者权限(can_admin)"
|
|
208
|
+
"对提案 pid 裁决(agent 端无裁决权是设计),转告使用者:"
|
|
209
|
+
"python -m md_cg.review_cli list 后 accept/reject,"
|
|
210
|
+
"或 cg(op=review, pid=<pid>, decision=accept|reject|"
|
|
211
|
+
"edit|merge, reason=<理由>)" % verdict.get("state")}
|
|
212
|
+
if pr.get("dedup"):
|
|
213
|
+
out["dedup"] = True
|
|
214
|
+
out["dup_of"] = pr["pid"]
|
|
215
|
+
out["dup_status"] = pr.get("dup_status")
|
|
216
|
+
out["hint"] = (
|
|
217
|
+
"同内容提案已存在(pid=%s,状态=%s,幂等去重),"
|
|
218
|
+
"本次未重复入队——无需重试;落盘须由设计者权限(can_admin)"
|
|
219
|
+
"对该 pid 裁决:python -m md_cg.review_cli list 后 accept/reject,"
|
|
220
|
+
"或 cg(op=review, pid=<pid>, decision=accept|reject|edit|merge, "
|
|
221
|
+
"reason=<理由>)" % (pr["pid"], pr.get("dup_status") or "pending"))
|
|
222
|
+
return out
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# 生效条件:ctx["a"]["consistency"] 为假值时返回 None;on_conflict 缺键或假值回落 "defer",仅当 verdict=REJECT 且 on_conflict=reject(返回 moved_to="conflict_rejected")或 verdict∈{REJECT,BLINDSPOT} 且 on_conflict=defer(转 review_queue,去重命中时改写 hint)才拦截,其余 on_conflict 取值返回 None;
|
|
226
|
+
def _gate_consistency(ctx):
|
|
227
|
+
"""冲突闸:节点间自动冲突检测(三级决策)。
|
|
228
|
+
|
|
229
|
+
仅 REJECT(明确判为冲突)按 on_conflict 处置:reject=直接拒绝;
|
|
230
|
+
defer=转入审核队列。BLINDSPOT(无可比对节点,检测前提不存在)恒放行,
|
|
231
|
+
cvd 审计经链尾透出(issue #26:无法比对 ≠ 冲突,入队是死胡同)。
|
|
232
|
+
"""
|
|
233
|
+
a = ctx["a"]
|
|
234
|
+
cg = ctx["cg"]
|
|
235
|
+
if not bool(a.get("consistency", True)):
|
|
236
|
+
return None
|
|
237
|
+
from .mcp_server import _proposal_extras
|
|
238
|
+
oc = (a.get("on_conflict") or "defer").strip().lower()
|
|
239
|
+
cvd = cg.check_consistency(
|
|
240
|
+
a.get("content", ""),
|
|
241
|
+
layer=a.get("layer") or ("contextual" if a.get("gated")
|
|
242
|
+
else "knowledge"),
|
|
243
|
+
condition_space=a.get("condition_space"),
|
|
244
|
+
non_applicable_conditions=a.get("non_applicable_conditions"),
|
|
245
|
+
tags=a.get("tags"), exclude=ctx["nid"], auto_flywheel=True)
|
|
246
|
+
ctx["cvd"] = cvd
|
|
247
|
+
v = cvd.get("verdict")
|
|
248
|
+
# issue #26(2026-09-23):BLINDSPOT ≠ REJECT——冲突闸的 BLINDSPOT 唯一出口
|
|
249
|
+
# 是 comparable==0(既有节点无一声明条件,含空库),语义是「检测前提不
|
|
250
|
+
# 存在」而非「已判定冲突」;defer 入队后裁决者面对同样空白(无可操作
|
|
251
|
+
# 下一步,死胡同)。故 BLINDSPOT 恒放行:cvd 经链尾 _executor 的
|
|
252
|
+
# consistency 字段如实透出(放行原因可观测);REJECT(明确冲突)维持
|
|
253
|
+
# 原拦截语义。原先两者等同拦截 → 空库首次写入恒不落盘(README 推荐的
|
|
254
|
+
# content_kind=code 通路必失败——库越空越写不进)。
|
|
255
|
+
blocked = (v == "REJECT" and oc in ("reject", "defer"))
|
|
256
|
+
if not blocked:
|
|
257
|
+
return None
|
|
258
|
+
if oc == "reject":
|
|
259
|
+
return {"ok": False, "id": ctx["nid"], "committed": False,
|
|
260
|
+
"moved_to": "conflict_rejected",
|
|
261
|
+
"consistency": cvd, "verdict": ctx["verdict"]}
|
|
262
|
+
pr = cg.propose(ctx["nid"], a.get("content", ""), info=True,
|
|
263
|
+
layer=a.get("layer") or "knowledge",
|
|
264
|
+
tags=a.get("tags"),
|
|
265
|
+
condition_space=a.get("condition_space"),
|
|
266
|
+
**_proposal_extras(a, ctx["verdict"]))
|
|
267
|
+
out = {"ok": False, "id": ctx["nid"], "pid": pr["pid"],
|
|
268
|
+
"committed": False,
|
|
269
|
+
"moved_to": "review_queue", "consistency": cvd,
|
|
270
|
+
"verdict": ctx["verdict"],
|
|
271
|
+
"hint": "这是冲突闸门的正常行为:本次写入与既有条件/纪律冲突"
|
|
272
|
+
"(on_conflict=defer),已转入审核队列待裁决——"
|
|
273
|
+
"不是工具故障,重试同样结果;"
|
|
274
|
+
"落盘须由设计者权限(can_admin)裁决,转告使用者:"
|
|
275
|
+
"python -m md_cg.review_cli list 后 accept/reject,"
|
|
276
|
+
"或 cg(op=review, pid=<pid>, decision=accept|reject|"
|
|
277
|
+
"edit|merge, reason=<理由>)"}
|
|
278
|
+
if pr.get("dedup"):
|
|
279
|
+
out["dedup"] = True
|
|
280
|
+
out["dup_of"] = pr["pid"]
|
|
281
|
+
out["hint"] = (
|
|
282
|
+
"同内容提案已存在于审核队列(pid=%s,幂等去重),"
|
|
283
|
+
"本次未重复入队——无需重试;"
|
|
284
|
+
"落盘须由设计者权限(can_admin)对该 pid 裁决:"
|
|
285
|
+
"python -m md_cg.review_cli list 后 accept/reject,"
|
|
286
|
+
"或 cg(op=review, pid=<pid>, decision=accept|reject|"
|
|
287
|
+
"edit|merge, reason=<理由>)" % pr["pid"])
|
|
288
|
+
return out
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# 生效条件:ctx["a"] 的 gated 为假值时返 None 放行;为真值时按 cg.remember_gated 返回的 verdict 落两段式账,且仅 verdict 为 ACCEPT 时 ok/committed 为 True,verdict 为 MERGE 时记 committed 并置 moved_to="merged_into:"+merged_into,verdict 为 DROP/DEFER 时记 aborted 且 moved_to 为其小写值;
|
|
292
|
+
def _gate_gated(ctx):
|
|
293
|
+
"""主动遗忘闸(gated=true 时启用):writelimit 限流 + forgetting 三问四态。
|
|
294
|
+
|
|
295
|
+
本闸是「替代执行路径」:命中即由 remember_gated 代为落盘/合并/丢弃并
|
|
296
|
+
返回终态;未启用(gated 假值)放行给链尾执行器。
|
|
297
|
+
"""
|
|
298
|
+
a = ctx["a"]
|
|
299
|
+
cg = ctx["cg"]
|
|
300
|
+
if not a.get("gated"):
|
|
301
|
+
return None
|
|
302
|
+
hint = a.get("importance_hint")
|
|
303
|
+
if hint is None and a.get("importance") is not None:
|
|
304
|
+
hint = float(a["importance"])
|
|
305
|
+
# ③ 两段式:本闸是**替代执行路径**(自己落盘),意图必须由它先记——
|
|
306
|
+
# 若等 execute 在链后统一记,intent 会晚于本闸内部的写盘,「先行持久化」
|
|
307
|
+
# 就不成立了。落盘前的窗口因此仍然被账本覆盖。
|
|
308
|
+
tok = twophase.begin(cg, ctx["nid"], a.get("content", ""),
|
|
309
|
+
layer=a.get("layer") or "contextual",
|
|
310
|
+
actor="writepipe:gated")
|
|
311
|
+
res = cg.remember_gated(
|
|
312
|
+
ctx["nid"], a.get("content", ""), layer=a.get("layer") or "contextual",
|
|
313
|
+
role=a.get("role"), tags=a.get("tags"),
|
|
314
|
+
condition_space=a.get("condition_space"),
|
|
315
|
+
verification_basis=(a.get("verification_basis")
|
|
316
|
+
or (ctx.get("verdict") or {}).get("basis")),
|
|
317
|
+
non_applicable_conditions=a.get("non_applicable_conditions"),
|
|
318
|
+
importance_hint=hint, override=bool(a.get("override")),
|
|
319
|
+
consistency=False,
|
|
320
|
+
derived_from=_split_ids(a.get("derived_from")),
|
|
321
|
+
relation=a.get("relation"))
|
|
322
|
+
v = res.get("verdict")
|
|
323
|
+
committed = v == "ACCEPT"
|
|
324
|
+
# 结局如实记:ACCEPT=落盘完成;MERGE=内容并入既有节点(不再以本次内容成
|
|
325
|
+
# 文,指纹对账不适用,故直接记 committed 并注明去向);DROP/DEFER=未落盘。
|
|
326
|
+
if committed:
|
|
327
|
+
twophase.commit(cg, tok, status=twophase.STATUS_COMMITTED,
|
|
328
|
+
reason="gated_accept")
|
|
329
|
+
elif v == "MERGE":
|
|
330
|
+
twophase.commit(cg, tok, status=twophase.STATUS_COMMITTED,
|
|
331
|
+
reason="merged_into:%s" % res.get("merged_into"))
|
|
332
|
+
else:
|
|
333
|
+
twophase.commit(cg, tok, status=twophase.STATUS_ABORTED,
|
|
334
|
+
reason="gated_%s" % str(v).lower())
|
|
335
|
+
out = {"ok": committed, "id": ctx["nid"], "committed": committed,
|
|
336
|
+
"gate": res, "verdict": ctx["verdict"]}
|
|
337
|
+
if ctx.get("cvd") is not None:
|
|
338
|
+
out["consistency"] = ctx["cvd"]
|
|
339
|
+
if v == "MERGE":
|
|
340
|
+
out["moved_to"] = "merged_into:" + str(res.get("merged_into"))
|
|
341
|
+
elif v in ("DROP", "DEFER"):
|
|
342
|
+
out["moved_to"] = v.lower()
|
|
343
|
+
# gated 是**替代落盘路径**(自行落盘/合并后直接返回终态、不跑 after 链),故
|
|
344
|
+
# 一跳同步传播须在此单独触发——否则 MERGE 类覆写会漏传下游(非对称边界,
|
|
345
|
+
# 与 `_after_trust` 注释互指)。
|
|
346
|
+
if committed or v == "MERGE":
|
|
347
|
+
prop = trust.mark_dependents(
|
|
348
|
+
cg, ctx["nid"],
|
|
349
|
+
reason="上游节点被 gated 写入/合并(内容或验证态可能已变)",
|
|
350
|
+
actor="writepipe:gated", trigger="write_gated")
|
|
351
|
+
if isinstance(prop, dict) and prop.get("changed"):
|
|
352
|
+
out["propagation"] = {"changed": prop.get("changed"),
|
|
353
|
+
"updated": (prop.get("updated") or [])[:10]}
|
|
354
|
+
return out
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
# 生效条件:ctx["a"]["content"] 的「# 子功能:」行含显式跨节点引用(`@<节点 id>`)且 depends_on 解析为空时返回 ok=False/error="E050" 的终态;depends_on 含库中不存在的 id 时返回 ok=False/error="E051" 的终态;其余(无该行 / 哨兵 / 自然语言自述 / 声明且目标齐备)返回 None 放行;
|
|
358
|
+
def _gate_deps(ctx):
|
|
359
|
+
"""依赖声明闸(before 链:linkref 之后、audit 之前):**硬拒条件缺失**。
|
|
360
|
+
|
|
361
|
+
「声明」的界定(收窄裁定 b,2026-09-19):以 `@<节点 id>` 显式引用为界——
|
|
362
|
+
自然语言**自述子功能**(描述本单元**内部**构成)不构成依赖声明。原因:CCG
|
|
363
|
+
编译产物六要素必含「子功能」行,若沿用「非哨兵即声明」,每个 CCG 节点落库后
|
|
364
|
+
都会被自己的闸门永久要求 depends_on(E050 死锁,test_ccgc V16f 实证)。
|
|
365
|
+
依赖不是必填元数据;**只有显式声称依赖却不落字段**才是违规(声称与落盘不一致)。
|
|
366
|
+
|
|
367
|
+
为何是硬拒而非告警:依赖是失效传播的**唯一入口**。声明缺失时,「上游变了
|
|
368
|
+
下游要存疑」这条链从源头就不存在——它既不报错、也不留任何信号,缺陷以
|
|
369
|
+
「静默不传播」的形态长期存活(比报错更难发现)。故按契约缺失处理,与
|
|
370
|
+
`ccgc` 的 E 码体系同构(E050 声明缺失 / E051 目标不存在)。
|
|
371
|
+
|
|
372
|
+
张力消解(与 provenance「写路径永不阻断」纪律的边界,二者不冲突):
|
|
373
|
+
- **声明缺失 / 目标不可解析 = 契约违规** → 硬拒(本闸只做这件事);
|
|
374
|
+
- **传播落盘失败 = 运维降级** → 告警不阻断(见 `_after_trust`)。
|
|
375
|
+
|
|
376
|
+
判据基于**入参**而非落盘后回读:首次写入时节点尚不存在,回读式校验会
|
|
377
|
+
永远放行(等于闸门失效)。
|
|
378
|
+
"""
|
|
379
|
+
a = ctx["a"]
|
|
380
|
+
cg = ctx["cg"]
|
|
381
|
+
from . import nodefile
|
|
382
|
+
if not nodefile.declares_dependency(a.get("content") or ""):
|
|
383
|
+
return None
|
|
384
|
+
deps = trust.as_deps(a.get("depends_on"))
|
|
385
|
+
if not deps:
|
|
386
|
+
return {"ok": False, "id": ctx["nid"], "committed": False,
|
|
387
|
+
"gate": "deps", "error": "E050",
|
|
388
|
+
"verdict": ctx.get("verdict"),
|
|
389
|
+
"hint": ("依赖声明缺失(E050):正文以 " + nodefile.DEP_REF_MARK
|
|
390
|
+
+ "<节点 id> 显式声明了跨节点依赖(「# 子功能:」行),"
|
|
391
|
+
"但 depends_on 未给出可解析目标。依赖必须是**可解析的字段**"
|
|
392
|
+
"(形如 depends_on=[\"<被依赖节点 id>\"]),不能只是散文——"
|
|
393
|
+
"否则被依赖单元变动时,下游无处可传。补齐后重试;"
|
|
394
|
+
"若该行只是描述本单元内部构成(自述),去掉 "
|
|
395
|
+
+ nodefile.DEP_REF_MARK + " 引用或改填「无」即可。"
|
|
396
|
+
"本闸是契约闸门的正常行为,不是工具故障。")}
|
|
397
|
+
known = set((getattr(cg, "index", None) or {}).get("nodes") or {})
|
|
398
|
+
missing = [d for d in deps if d not in known]
|
|
399
|
+
if missing:
|
|
400
|
+
return {"ok": False, "id": ctx["nid"], "committed": False,
|
|
401
|
+
"gate": "deps", "error": "E051", "missing": missing[:10],
|
|
402
|
+
"verdict": ctx.get("verdict"),
|
|
403
|
+
"hint": "依赖目标不存在(E051):depends_on 指向 "
|
|
404
|
+
+ ", ".join(missing[:5])
|
|
405
|
+
+ ",但库中查无此节点——声称依赖一个并不存在的地基,"
|
|
406
|
+
"失效传播会在此处断链。请先建立被依赖节点,或修正 id。"}
|
|
407
|
+
return None
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
# 生效条件:out 为 dict 且 out["committed"] 为真时,调 trust.mark_dependents 做一跳同步传播(异常吞掉并降级),并在节点时间轴非「时效内」时往 out 写 "validity" 提示;其余情况直接返回不做任何动作;
|
|
411
|
+
def _after_trust(ctx, out):
|
|
412
|
+
"""after 观察者:落盘后触发**一跳同步传播** + 时效提示。
|
|
413
|
+
|
|
414
|
+
为何落在 after 而非 before:只有真正落盘(内容确实变了)才构成「地基动了」;
|
|
415
|
+
before 链短路路径(REJECT / DEFER)本就不跑 after 链,语义天然正确。
|
|
416
|
+
例外:`gated` 是替代落盘路径(自行落盘并返回终态、不跑 after),故它的
|
|
417
|
+
传播在 `_gate_gated` 内部单独触发(见该处注释)。
|
|
418
|
+
|
|
419
|
+
**永不抛**:传播失败只降级(`trust.mark_dependents` 内部已兜底并写台账),
|
|
420
|
+
绝不把「写入已成功」改写为失败——与 `_commit_visibility` 同款边界。
|
|
421
|
+
"""
|
|
422
|
+
if not isinstance(out, dict) or not out.get("committed"):
|
|
423
|
+
return
|
|
424
|
+
cg = ctx["cg"]
|
|
425
|
+
nid = ctx.get("nid")
|
|
426
|
+
if not nid:
|
|
427
|
+
return
|
|
428
|
+
prop = trust.mark_dependents(
|
|
429
|
+
cg, nid, reason="上游节点被写入/覆写(内容或验证态可能已变)",
|
|
430
|
+
actor="writepipe:trust", trigger="write")
|
|
431
|
+
if isinstance(prop, dict) and prop.get("changed"):
|
|
432
|
+
out["propagation"] = {"changed": prop.get("changed"),
|
|
433
|
+
"updated": (prop.get("updated") or [])[:10]}
|
|
434
|
+
# 热路径失效(2026-09-19 热温冷分层):写入后清缓存
|
|
435
|
+
try:
|
|
436
|
+
from . import hotcache as _hc
|
|
437
|
+
_hc.invalidate(cg, nid)
|
|
438
|
+
except Exception: # noqa: BLE001
|
|
439
|
+
pass # 缓存失效失败不阻断写入(永不抛)
|
|
440
|
+
|
|
441
|
+
# 冷路径入队(2026-09-19 热温冷分层):异步深度验证
|
|
442
|
+
try:
|
|
443
|
+
from . import coldverify as _cv
|
|
444
|
+
_cv.enqueue(cg, nid, action="reverify",
|
|
445
|
+
reason="writepipe:after_trust")
|
|
446
|
+
except Exception: # noqa: BLE001
|
|
447
|
+
pass # 入队失败不阻断写入
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
# 生效条件:a 的 content_kind 非 hyperedge 即返回 {},为 hyperedge 时延迟导入
|
|
451
|
+
# hyperedge.EXTRA_FM_KEYS 收集 a 中非 None 的对应键返回(其余写入零键透传);
|
|
452
|
+
def _hyperedge_extra(a):
|
|
453
|
+
"""hyperedge 写入的 fm 扩展键透传。计划决策 2:超边写入走 write 动词既有
|
|
454
|
+
审核链、不造新通道——链尾执行器是唯一落盘点,cg.add 经 **extra 落 fm,
|
|
455
|
+
故扩展键在此透传;落盘面丢字段 = 上游声明静默失效。延迟导入防循环依赖。"""
|
|
456
|
+
if (a.get("content_kind") or "").strip() != "hyperedge":
|
|
457
|
+
return {}
|
|
458
|
+
from . import hyperedge as _he
|
|
459
|
+
return {k: a[k] for k in _he.EXTRA_FM_KEYS if a.get(k) is not None}
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
# 生效条件:由链尾以含 cg 与 a 的 ctx 调用即无条件执行 cg.add 落盘并返回 ok=True/committed=True,ctx["cvd"] 非 None 时附加 consistency 字段;
|
|
463
|
+
def _executor(ctx):
|
|
464
|
+
"""链尾执行器(常驻不可卸载):cg.add 直写落盘。
|
|
465
|
+
|
|
466
|
+
角色/层权限校验(require_layer_write)在 MdCGSecure.add 库层内部,
|
|
467
|
+
结构上不可被拦截器绕过——拦截器只能裁决「写不写」,改不了「谁能写」。
|
|
468
|
+
"""
|
|
469
|
+
a = ctx["a"]
|
|
470
|
+
cg = ctx["cg"]
|
|
471
|
+
# importance 显式 null(JSON null→None)时 get 的缺省值不生效,直接
|
|
472
|
+
# float(None) 抛 TypeError 崩主写路径——回退默认 0.5(与 add 缺省同口径);
|
|
473
|
+
# 0 / 0.0 等合法 falsy 数值照传(_gate_gated :304 已是同款 None 判定)。
|
|
474
|
+
_imp = a.get("importance")
|
|
475
|
+
cg.add(ctx["nid"], a.get("content", ""),
|
|
476
|
+
layer=a.get("layer") or "knowledge",
|
|
477
|
+
tags=a.get("tags"), condition_space=a.get("condition_space"),
|
|
478
|
+
importance=0.5 if _imp is None else float(_imp),
|
|
479
|
+
verification_basis=a.get("verification_basis")
|
|
480
|
+
or (ctx.get("verdict") or {}).get("basis"),
|
|
481
|
+
non_applicable_conditions=a.get("non_applicable_conditions"),
|
|
482
|
+
override=bool(a.get("override")), consistency=False,
|
|
483
|
+
derived_from=_split_ids(a.get("derived_from")),
|
|
484
|
+
relation=a.get("relation"),
|
|
485
|
+
# 可验证记忆单元(裁定 D):依赖/双时间轴/验证态随写入落 fm。
|
|
486
|
+
# 透传而非丢弃——落盘面丢字段=上游声明静默失效(比报错难发现)。
|
|
487
|
+
depends_on=trust.as_deps(a.get("depends_on")),
|
|
488
|
+
valid_from=a.get("valid_from"), valid_until=a.get("valid_until"),
|
|
489
|
+
verification_state=a.get("verification_state"),
|
|
490
|
+
**_hyperedge_extra(a))
|
|
491
|
+
out = {"ok": True, "id": ctx["nid"], "committed": True,
|
|
492
|
+
"verdict": ctx["verdict"]}
|
|
493
|
+
if ctx.get("cvd") is not None:
|
|
494
|
+
out["consistency"] = ctx["cvd"]
|
|
495
|
+
return out
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
# 生效条件:value 传入即无条件延迟导入并转调 mcp_server._split_ids 后原样返回其结果(本符号无自身分支);
|
|
499
|
+
def _split_ids(value):
|
|
500
|
+
# 与 mcp_server._split_ids 同源(延迟导入,单一真源)
|
|
501
|
+
from .mcp_server import _split_ids as _f
|
|
502
|
+
return _f(value)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
# --------------------------------------------------------------------------
|
|
506
|
+
# 进程级默认链(单例;mcp_server op=write 一行分发到此)
|
|
507
|
+
# --------------------------------------------------------------------------
|
|
508
|
+
|
|
509
|
+
_DEFAULT = None
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
# 生效条件:pipe 传入即对其依次注册 before 的 linkref(position=0)/deps/audit/consistency/gated 与 after 的 linkref/trust(同名幂等替换),并返回同一 pipe;
|
|
513
|
+
def install_default_gates(pipe):
|
|
514
|
+
"""把默认闸以拦截器形态注册(幂等:同名替换,可重复调用)。
|
|
515
|
+
|
|
516
|
+
链序(2026-09-19 起):
|
|
517
|
+
before = linkref(解析) → deps(依赖声明) → audit → consistency → gated
|
|
518
|
+
→ 链尾执行器
|
|
519
|
+
after = linkref(建边) → trust(一跳传播)
|
|
520
|
+
|
|
521
|
+
linkref 置于链首的理由:正文引用解析是**纯读、无副作用**,且其结果必须
|
|
522
|
+
先于任何短路闸写入 ctx,供 after 链消费。短路闸(REJECT/DEFER/gated)
|
|
523
|
+
返回终态时 `execute` 不跑 after 链,故未落盘的写入不会建边——语义正确。
|
|
524
|
+
|
|
525
|
+
deps 夹在 linkref 与 audit 之间的理由:两件事都与内容政策无关,故在 audit
|
|
526
|
+
之前;linkref 之后是因为它只解析正文引用、不动依赖声明——依赖是**字段域**
|
|
527
|
+
而非正文域,顺序倒置不会互相污染,但保持「解析在前、裁决在后」的一致读序。
|
|
528
|
+
|
|
529
|
+
after 的 trust 置于 linkref 之后:建边先于传播——传播按 `depends_on`
|
|
530
|
+
反查(字段域)而非边域,故顺序不影响正确性;置于其后只为让 ctx 中的
|
|
531
|
+
边信息先落定,便于排障时读 ctx。
|
|
532
|
+
|
|
533
|
+
linkref 的**落点是 after 而非注入 `a["edges"]`**:`cg.add` 是全量重建
|
|
534
|
+
fm,注入 edges 会在覆写既有节点时清空其原有边(破坏性副作用);
|
|
535
|
+
`append_edge` 是边域窄原语且幂等(见 linkref 模块 docstring)。
|
|
536
|
+
依赖声明同忌经 `a["edges"]` 注入——走 `depends_on` 字段域。
|
|
537
|
+
"""
|
|
538
|
+
from . import linkref
|
|
539
|
+
pipe.register_before("linkref", linkref.before_hook(), position=0)
|
|
540
|
+
pipe.register_before("deps", _gate_deps)
|
|
541
|
+
pipe.register_before("audit", _gate_audit)
|
|
542
|
+
pipe.register_before("consistency", _gate_consistency)
|
|
543
|
+
pipe.register_before("gated", _gate_gated)
|
|
544
|
+
pipe.register_after("linkref", linkref.after_hook())
|
|
545
|
+
pipe.register_after("trust", _after_trust)
|
|
546
|
+
return pipe
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
# 生效条件:模块级 _DEFAULT 为 None 时新建 WritePipeline 并经 install_default_gates 注册后缓存返回,否则直接返回已缓存的 _DEFAULT 单例;
|
|
550
|
+
def default_pipeline():
|
|
551
|
+
"""进程级默认写入链(单例)。自定义闸门 register_before 即插即拔。"""
|
|
552
|
+
global _DEFAULT
|
|
553
|
+
if _DEFAULT is None:
|
|
554
|
+
_DEFAULT = install_default_gates(WritePipeline())
|
|
551
555
|
return _DEFAULT
|