@furongjun1999/dsh-memory 0.5.0 → 0.5.1
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 +552 -465
- package/docs/README.md +1 -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//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_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/lib/bridge.d.ts +9 -0
- package/lib/bridge.js +35 -0
- package/lib/index.js +7 -1
- 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 +582 -580
- 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 +280 -29
- package/md_cg/mdcg.py +616 -125
- package/md_cg/mdcos.py +430 -66
- 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 +170 -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_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_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_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 +6 -2
- package/src/bridge.ts +434 -401
- package/src/index.ts +526 -518
- package/src/lib/roleplay_web.ts +1019 -932
- package/src/lib/token_store.ts +202 -192
package/md_cg/mreview/govern.py
CHANGED
|
@@ -51,6 +51,7 @@ from ..backfill import _as_cg, _entry_id, _readable_guard, _sha
|
|
|
51
51
|
from ..fsutil import append_jsonl, read_jsonl
|
|
52
52
|
from ..mdcos import ALL_ROLES, WORK_ROLES
|
|
53
53
|
from .ruleset import _as_rules, _blank
|
|
54
|
+
from ..readcache import direct_read
|
|
54
55
|
|
|
55
56
|
#: 留痕文件名(与 `backfill._backfill.jsonl` 分立:治理动作须能独立审计)
|
|
56
57
|
GOVERN_LOG = "_govern.jsonl"
|
|
@@ -198,12 +199,12 @@ def _candidate_role(e, fm, *, sources, role_map) -> tuple:
|
|
|
198
199
|
else REASON_NO_SOURCE)
|
|
199
200
|
|
|
200
201
|
|
|
201
|
-
# 生效条件:按序判定——_readable_guard(cg, e) 为假返回 ("denied", None);cg
|
|
202
|
+
# 生效条件:按序判定——_readable_guard(cg, e) 为假返回 ("denied", None);direct_read(cg, e) 的 fm 为 None 返回 ("unreadable", None);crypto.is_encrypted(content) 为真返回 ("locked", None);fm.get("role") 非空白返回 ("present", None);否则 _candidate_role(e, fm, sources=sources, role_map=role_map) 的 role 为假返回 ("unfillable", {id,layer,reason,writer}),role 为真返回 ("", {id,layer,role,source,basis,before,had_key,writer})。
|
|
202
203
|
def _classify(cg, e, nid, *, sources, role_map) -> tuple:
|
|
203
204
|
"""单条裁决 → `(skip_reason, item|gap)`;skip_reason 为空串表示可回填。"""
|
|
204
205
|
if not _readable_guard(cg, e):
|
|
205
206
|
return "denied", None
|
|
206
|
-
fm, content = cg
|
|
207
|
+
fm, content = direct_read(cg, e)
|
|
207
208
|
if fm is None:
|
|
208
209
|
return "unreadable", None
|
|
209
210
|
if crypto.is_encrypted(content):
|
|
@@ -344,7 +345,7 @@ def role_apply(x, ids=None, entry_ids=None, layer=None, limit=None,
|
|
|
344
345
|
if not _readable_guard(cg, e):
|
|
345
346
|
rep["skipped_denied"] += 1
|
|
346
347
|
continue
|
|
347
|
-
fm, content = cg
|
|
348
|
+
fm, content = direct_read(cg, e)
|
|
348
349
|
if fm is None:
|
|
349
350
|
rep["skipped_missing"] += 1
|
|
350
351
|
continue
|
|
@@ -407,7 +408,7 @@ def role_rollback(x, batch=None, entry_ids=None, actor=None) -> dict:
|
|
|
407
408
|
if e is None:
|
|
408
409
|
rep["missing"] += 1
|
|
409
410
|
continue
|
|
410
|
-
fm, content = cg
|
|
411
|
+
fm, content = direct_read(cg, e)
|
|
411
412
|
if fm is None or crypto.is_encrypted(content):
|
|
412
413
|
rep["missing"] += 1
|
|
413
414
|
continue
|
package/md_cg/postings.py
CHANGED
|
@@ -25,6 +25,8 @@ import json
|
|
|
25
25
|
import os
|
|
26
26
|
import time
|
|
27
27
|
|
|
28
|
+
from .fsutil import publish
|
|
29
|
+
|
|
28
30
|
SCHEMA = "mdcg-postings-2"
|
|
29
31
|
|
|
30
32
|
|
|
@@ -167,12 +169,12 @@ def load_meta(root: str) -> dict:
|
|
|
167
169
|
return {}
|
|
168
170
|
|
|
169
171
|
|
|
170
|
-
# 生效条件:无条件以 utf-8 原子写 path(tmp + os.replace),写入的字符串为 obj 的 JSON 序列化。
|
|
172
|
+
# 生效条件:无条件以 utf-8 原子写 path(tmp + publish,即带 Windows 短重试的 os.replace),写入的字符串为 obj 的 JSON 序列化。
|
|
171
173
|
def _atomic_json(path: str, obj, indent=None) -> None:
|
|
172
174
|
tmp = path + ".tmp"
|
|
173
175
|
with io.open(tmp, "w", encoding="utf-8", newline=chr(10)) as f:
|
|
174
176
|
json.dump(obj, f, ensure_ascii=False, indent=indent, separators=None if indent else (",", ":"))
|
|
175
|
-
|
|
177
|
+
publish(tmp, path)
|
|
176
178
|
|
|
177
179
|
|
|
178
180
|
# 生效条件:无条件读 root/_postings.json 并返回 dict(缺文件/解析失败返回 {};内容非 dict 亦返回 {})。
|
package/md_cg/readcache.py
CHANGED
|
@@ -10,13 +10,18 @@
|
|
|
10
10
|
耗时,~230µs/节点线性退化)——「理论没有落地」。本模块即落地。
|
|
11
11
|
|
|
12
12
|
与 `install_read_cache`(eval_common.py,benchmark 专用)的差异:
|
|
13
|
-
1.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
1. **写失效哨兵(脏集精确失效,缺陷迭代第 14 轮)**:缓存条目携带
|
|
14
|
+
缓存时的 `write_gen`,新鲜度按 `_DirtyDict.path_gen[path]`(该 path
|
|
15
|
+
最近标脏代际)与 `broad_gen`(tombstone/无 path 可辨变更的整池兜底)
|
|
16
|
+
判定——**写谁失效谁**,单节点写不再使全部条目同时 miss(此前整池
|
|
17
|
+
共用一个全局代际,10k 池写读交替 38.4× 退化、线性于 N)。所有写路径
|
|
18
|
+
必然标脏(flush 依赖 `_dirty`,add/_sync_edge_entry/负记忆化……无一
|
|
19
|
+
例外),标脏即 `_dirty[nid]=entry`(带 path)——簿记在 _DirtyDict
|
|
20
|
+
钩子内自动完成,写面零挂载点,天然覆盖未来新增写路径(不会有
|
|
21
|
+
「漏挂失效」型陈旧读)。对 path-only 无失效的朴素实现能红(外部报告
|
|
22
|
+
实测的陈旧读场景)。
|
|
17
23
|
语义与 MdStore 的「写方落盘后 reload」同构,但**懒清**:检索高频写少,
|
|
18
|
-
|
|
19
|
-
天然覆盖未来新增写路径——不会有「漏挂失效」型陈旧读)。
|
|
24
|
+
写后首次检索只重装被写节点,不必写时同步清。
|
|
20
25
|
2. 缓存**解析产物** `(fm, content)`(benchmark 版只缓存原文,parse 仍逐次),
|
|
21
26
|
**并缓存文档侧检索派生物** `_doc_norm_bigrams`(归一化 bigram——批次 21
|
|
22
27
|
实测本机热点 88% 在此而非 I/O:内容不变则派生物不变,随读缓存常驻;
|
|
@@ -24,28 +29,81 @@
|
|
|
24
29
|
3. 进程内一致性边界(与 MdStore 相同的诚实边界):跨进程/外部直接改写
|
|
25
30
|
md 文件不保证可见——认知图的多进程形态(每智能体一进程)各持快照。
|
|
26
31
|
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
开关:默认**开**(issue #31 后续——生产检索入口 MdCGOS.search 默认态每查询
|
|
33
|
+
全池 open+realpath+parse,3300 池实测中位 ~602ms/查询、O(n) 线性;读缓存
|
|
34
|
+
早已实现却默认关,生产恒不装配,故默认翻转);`MDCG_READ_CACHE=0` 显式
|
|
35
|
+
关闭(opt-out 零变更退出阀——跨进程改写 md 文件须本进程立即可见的部署,
|
|
36
|
+
或内存受限场景,可一关回到逐次读盘的旧行为)。
|
|
29
37
|
"""
|
|
30
38
|
import os
|
|
31
39
|
|
|
32
|
-
# 生效条件:无 required 形参,锚定环境变量名 MDCG_READ_CACHE
|
|
40
|
+
# 生效条件:无 required 形参,锚定环境变量名 MDCG_READ_CACHE;未设或值为 "1" 时返回 True(默认开,issue #31 生产 O(n) 全池扫描修复),显式设为其它值(含 "0")时返回 False。
|
|
33
41
|
def enabled() -> bool:
|
|
34
|
-
"""
|
|
35
|
-
return os.environ.get("MDCG_READ_CACHE") == "1"
|
|
42
|
+
"""读缓存开关(默认开;=0 显式关闭——opt-out 退出阀)。"""
|
|
43
|
+
return os.environ.get("MDCG_READ_CACHE", "1") == "1"
|
|
36
44
|
|
|
37
45
|
|
|
38
|
-
# 生效条件:cg.
|
|
46
|
+
# 生效条件:cg._read_uncached 属性存在时返回其以 entry 调用的结果(穿透读缓存的盘上真值直读),否则返回 cg._read(entry)(未装缓存时两者等价,零变更)。
|
|
47
|
+
def direct_read(cg, entry):
|
|
48
|
+
"""治理/写前重查专用直读——穿透读缓存,读盘上当前真值。
|
|
49
|
+
|
|
50
|
+
读缓存默认开(issue #31 后续)后 `cg._read` 是进程内快照;治理面的
|
|
51
|
+
「写前重查」「回滚比对」语义上要求盘上真值——预演与执行之间节点
|
|
52
|
+
可能被他人/外部进程改写(test_mr_m4 D12「被人改动→不覆盖」形态:
|
|
53
|
+
path-only 命中会让重查读到预演时装入的旧值,漏判 drift)。
|
|
54
|
+
"""
|
|
55
|
+
fn = getattr(cg, "_read_uncached", None)
|
|
56
|
+
if fn is None:
|
|
57
|
+
return cg._read(entry)
|
|
58
|
+
return fn(entry)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# 生效条件:cg._read 可调用时以 (path → (缓存时 write_gen, 解析产物)) 常驻字典包装之——_dirty 带 path_gen 簿记(_DirtyDict 形态)时命中条件为「该 path 最近标脏代际(path_gen)与 broad_gen 均 ≤ 缓存时 write_gen」(脏集精确失效:单节点写只失效该节点),否则回落整代际相等校验(旧口径,非 _DirtyDict 防御);cg._doc_norm_bigrams 亦存在时同款包装其派生物(_score 热点:文档侧归一化 bigram 只依赖 content,随读缓存一并常驻);包装后 cg._read/_doc_norm_bigrams 为包装函数、cg._read_cache/_norm_bigrams_cache 为缓存字典、cg._read_uncached 为未被包装的原始 _read(direct_read 的真源);返回缓存字典。
|
|
39
62
|
def install(cg):
|
|
40
|
-
"""把 `cg._read
|
|
63
|
+
"""把 `cg._read`(与派生物钩子)包成**脏集精确失效**的常驻缓存。
|
|
64
|
+
|
|
65
|
+
失效口径(缺陷迭代第 14 轮,high):此前哨兵取全局单调 write_gen——
|
|
66
|
+
整池共用一个代际值,任意一次单节点写使**全部**条目同时 miss,下一条
|
|
67
|
+
查询全池重装(10k 池写读交替 3rep 中位 1677.6ms vs 稳态 43.7ms,
|
|
68
|
+
38.4× 退化、线性于 N;生产暴露面:mcp_server 读 op=read 与写 op=write
|
|
69
|
+
同一常驻实例)。`_DirtyDict` 现簿记 `path_gen`(path → 最近标脏时的
|
|
70
|
+
write_gen)与 `broad_gen`(无 path 可辨变更的整池兜底代际),本包装
|
|
71
|
+
据此按 path 判新鲜:**写谁失效谁**,其余条目原对象复用。写路径不必
|
|
72
|
+
再改一处(标脏即 `_dirty[nid]=entry` 带 path,簿记在 _DirtyDict 钩子
|
|
73
|
+
内自动完成)。
|
|
74
|
+
"""
|
|
41
75
|
cache = {}
|
|
42
|
-
|
|
76
|
+
# 重复 install(如显式再调)不叠加包装层,_read_uncached 恒指真原始。
|
|
77
|
+
orig = getattr(cg, "_read_uncached", None) or cg._read
|
|
78
|
+
cg._read_uncached = orig
|
|
79
|
+
|
|
80
|
+
def _fresh(p, hit):
|
|
81
|
+
"""hit=(缓存时 write_gen, val) 对 path p 是否仍新鲜(不陈旧)。"""
|
|
82
|
+
dirty = cg._dirty
|
|
83
|
+
pg = getattr(dirty, "path_gen", None)
|
|
84
|
+
if pg is None:
|
|
85
|
+
# D-4 修复(批次 23 / v20):代际改读 _DirtyDict.write_gen(单调、
|
|
86
|
+
# 永不回退)——len(_dirty) 在 flush 清零后可被新写入凑回旧值,代际
|
|
87
|
+
# 巧合回退 → 陈旧读(v20_d4_repro stale=True 实测)。防御回落兼容
|
|
88
|
+
# 非 _DirtyDict 形态(整代际相等,旧口径)。
|
|
89
|
+
return hit[0] == getattr(dirty, "write_gen", len(dirty))
|
|
90
|
+
# 脏集精确失效(第 14 轮):该 path 最近标脏代际 ≤ 缓存代际即新鲜
|
|
91
|
+
# ——单节点写只失效该节点;broad_gen(tombstone/无 path 可辨变更)
|
|
92
|
+
# 高于缓存代际时整池保守失效(与旧整代际口径等价的安全兜底)。
|
|
93
|
+
# flush 的 clear() 不推进 broad_gen:flush 只落索引派生物,节点
|
|
94
|
+
# 文件变更已在各写路径 _dirty[nid]=entry(带 path)时精确失效,
|
|
95
|
+
# 且生产写路径每次写后 flush(writepipe._commit_visibility)——
|
|
96
|
+
# 整池失效会让精确失效在生产恒不生效;rebuild_index 收尾走
|
|
97
|
+
# clear(broad=True) 推进 broad_gen(盘面重扫口径:直写文件 +
|
|
98
|
+
# rebuild 收尾的写方靠它对读缓存可见)。
|
|
99
|
+
return (pg.get(p, 0) <= hit[0]
|
|
100
|
+
and dirty.broad_gen <= hit[0])
|
|
43
101
|
|
|
44
102
|
def _cached(entry):
|
|
45
103
|
p = entry["path"]
|
|
46
|
-
gen = len(cg._dirty)
|
|
104
|
+
gen = getattr(cg._dirty, "write_gen", len(cg._dirty))
|
|
47
105
|
hit = cache.get(p)
|
|
48
|
-
if hit is not None and hit
|
|
106
|
+
if hit is not None and _fresh(p, hit):
|
|
49
107
|
return hit[1]
|
|
50
108
|
val = orig(entry)
|
|
51
109
|
cache[p] = (gen, val)
|
|
@@ -60,9 +118,9 @@ def install(cg):
|
|
|
60
118
|
|
|
61
119
|
def _cached_nb(entry, c):
|
|
62
120
|
p = entry["path"]
|
|
63
|
-
gen = len(cg._dirty)
|
|
121
|
+
gen = getattr(cg._dirty, "write_gen", len(cg._dirty))
|
|
64
122
|
hit = nb_cache.get(p)
|
|
65
|
-
if hit is not None and hit
|
|
123
|
+
if hit is not None and _fresh(p, hit):
|
|
66
124
|
return hit[1]
|
|
67
125
|
val = orig_nb(entry, c)
|
|
68
126
|
nb_cache[p] = (gen, val)
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""启动对账 reconcile v0(v0.3 T9 自稳定定理落地,落地清单 P0-3)。
|
|
3
|
+
|
|
4
|
+
**定位(与 twophase 的分工)**:`twophase.reconcile` 是**写账本清账**——只回答
|
|
5
|
+
「有 intent 无 outcome 的那笔写入到底落盘没有」;本模块是它之上的**全量 diff
|
|
6
|
+
对账**:索引(派生态)vs 盘面真源(节点 .md)逐条比对,把「索引与盘面背离」
|
|
7
|
+
收敛回自稳定合法态(T9:任意瞬态扰动后仅凭真源与协议在有限步内回到合法态)。
|
|
8
|
+
|
|
9
|
+
**三类 diff(v0 判据面)**:
|
|
10
|
+
1. **多索引条目**(幽灵):索引有条目、盘面无其文件(或该文件已归属他 id)
|
|
11
|
+
→ 摘除条目(tombstone,与 `_unstage` 同口径);
|
|
12
|
+
2. **缺索引条目**:盘面有节点文件、索引无条目(非合作写者/日志丢失)
|
|
13
|
+
→ 以 `_node_entry` 重建条目补入(与 rebuild_index 逐字段同源);
|
|
14
|
+
3. **内容漂移**:条目 path 与盘面落点不符、或 content_hash 与盘面重算不符
|
|
15
|
+
→ 以盘面为真源重建条目(**增量 upsert**,不触发全量 rebuild)。
|
|
16
|
+
|
|
17
|
+
**T9 边界(结构合法 ≠ 内容正确)**:真源自身损坏——读失败(OSError,含
|
|
18
|
+
Windows 上 Defender/索引器持句柄的瞬态形态)、解码失败(非 UTF-8 字节)、
|
|
19
|
+
frontmatter 结构非法(`---` 无闭合)——**一律不自动改写**:既不据损坏文件
|
|
20
|
+
建条目,也不因「读不出」摘除既有条目(瞬态读失败若被当「文件没了」就会把
|
|
21
|
+
一次性抖动放大成索引面永久损伤,与 N134 同型——宁可漏修,不可误伤),只
|
|
22
|
+
stderr 告警 + `_reconcile.jsonl` 留痕,是否处置交人工/上层裁决。
|
|
23
|
+
|
|
24
|
+
**耗时可控**:清单 diff 是 O(n) 索引操作(os.walk 计数级);哈希核验对每个
|
|
25
|
+
盘面 .md 恰一次顺序读 + 轻量 frontmatter 切分(`nodefile.loads` 是纯字符串
|
|
26
|
+
切分,无 YAML/全文解析、无检索打分/嵌入)——不是 `rebuild_index` 式全量重扫,
|
|
27
|
+
修复只对漂移条目做定向 upsert。整体 O(n);`MDCG_RECONCILE=0` 一键关闭
|
|
28
|
+
(缺省开)。**告警口径**:diff 非空才写 stderr 与留痕,干净库零输出零写盘。
|
|
29
|
+
|
|
30
|
+
**诚实边界(v0 不覆盖面)**:
|
|
31
|
+
· frontmatter **字段值**漂移(如盘上 importance 被直改、索引仍旧值)不在
|
|
32
|
+
v0 判据面——索引没有 fm 指纹可比(content_hash 刻意不含 frontmatter,见
|
|
33
|
+
nodefile.content_hash 文档串),该面仍由 rebuild_index 全量兜底(v1 候选);
|
|
34
|
+
· 对账是**增益**不是服务前提:本模块任何异常都不得阻塞启动(调用方
|
|
35
|
+
mcp_server.main 以 try/except 包裹,与两段式对账同风格)。
|
|
36
|
+
"""
|
|
37
|
+
import os
|
|
38
|
+
import time
|
|
39
|
+
|
|
40
|
+
from . import nodefile
|
|
41
|
+
from .fsutil import append_jsonl
|
|
42
|
+
|
|
43
|
+
#: 关闭开关:仅当 `MDCG_RECONCILE` 的 strip 后取值**恰好为 "0"** 时关闭
|
|
44
|
+
#: (缺省开;其它取值一律视为开——开关只回答「关没关」,不承担配置语义)。
|
|
45
|
+
ENV_SWITCH = "MDCG_RECONCILE"
|
|
46
|
+
|
|
47
|
+
#: 留痕文件(root 下,append-only,与 `_write_2pc.jsonl` / `_protected_audit.jsonl`
|
|
48
|
+
#: 同一「独立审计文件」惯例;**仅在 diff 非空时写入**,干净库零写盘)。
|
|
49
|
+
TRACE_FILE = "_reconcile.jsonl"
|
|
50
|
+
|
|
51
|
+
#: 留痕/告警里每类异常的采样上限(防大库首轮修复把日志/告警撑成洪水)。
|
|
52
|
+
SAMPLE_CAP = 20
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# 生效条件:读 MDCG_RECONCILE 环境变量(未设按空串),strip 后等于 "0" 返回 False,其余(含未设)返回 True。
|
|
56
|
+
def enabled() -> bool:
|
|
57
|
+
"""对账开关(缺省开;`MDCG_RECONCILE=0` 关闭)。"""
|
|
58
|
+
return os.environ.get(ENV_SWITCH, "").strip() != "0"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# 生效条件:给定 cg 且其 root 属性可参与 os.path.join 时,恒返回 os.path.join(cg.root, TRACE_FILE),无分支与早退。
|
|
62
|
+
def trace_path(cg) -> str:
|
|
63
|
+
return os.path.join(cg.root, TRACE_FILE)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# 生效条件:text 为假值(None/"")时按空串处理,返回去尾换行后的字符串(仅剥 "\n")。
|
|
67
|
+
def _norm(text: str) -> str:
|
|
68
|
+
return (text or "").rstrip("\n")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# 生效条件:content(nodefile.loads 切出的盘面正文,含尾换行)非 None 时返回 (content_hash(content), content_hash(_norm(content))) 二元组——**双形态接受集**:add 写路径入索引的指纹按封存原文(mdcg.py `_stage` 面,正文不带尾换行时等于 norm 形态),rebuild 重建路径按 loads 读回原文(带尾换行)——两 producers 各自稳定,diff 两侧都认,否则无尾换行节点会在每次启动被误报漂移(假阳性洪水,twophase._norm 文档串同款教训)。
|
|
72
|
+
def _disk_hashes(content: str) -> tuple:
|
|
73
|
+
raw = nodefile.content_hash(content or "")
|
|
74
|
+
return raw, nodefile.content_hash(_norm(content))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# 生效条件:遍历 mdcg.LAYERS 各层目录树(os.walk,与 _scan_nodes 同序同口径),逐个 .md 读取并 nodefile.loads 切分;成功解析的以 nid(fm.get("id") 或文件名去 .md)登入 by_nid(同 nid 后到覆盖,与 _scan_nodes dict 覆盖语义一致)并登记 path_owner[relpath]=nid;读失败(OSError)/解码失败(UnicodeDecodeError)/frontmatter 无闭合(fm 为空且原文以 "---\n" 开头)的登入 problems(kind=read_error/decode_error/frontmatter_unterminated,附 relpath),不进 by_nid 与 path_owner;返回 (by_nid, path_owner, problems)。
|
|
78
|
+
def _scan_disk(cg):
|
|
79
|
+
by_nid = {}
|
|
80
|
+
path_owner = {}
|
|
81
|
+
problems = []
|
|
82
|
+
for layer in _layers():
|
|
83
|
+
base = os.path.join(cg.root, layer)
|
|
84
|
+
for dirpath, _dirs, files in os.walk(base):
|
|
85
|
+
for fn in files:
|
|
86
|
+
if not fn.endswith(".md"):
|
|
87
|
+
continue
|
|
88
|
+
p = os.path.join(dirpath, fn)
|
|
89
|
+
rel = os.path.relpath(p, cg.root).replace("\\", "/")
|
|
90
|
+
try:
|
|
91
|
+
with open(p, encoding="utf-8") as f:
|
|
92
|
+
text = f.read()
|
|
93
|
+
except OSError as exc: # 瞬态/持久读失败:不猜测
|
|
94
|
+
problems.append({"path": rel, "kind": "read_error",
|
|
95
|
+
"detail": str(exc)[:120]})
|
|
96
|
+
continue
|
|
97
|
+
except UnicodeDecodeError as exc: # 非 UTF-8 字节:真源损坏
|
|
98
|
+
problems.append({"path": rel, "kind": "decode_error",
|
|
99
|
+
"detail": str(exc)[:120]})
|
|
100
|
+
continue
|
|
101
|
+
fm, content = nodefile.loads(text)
|
|
102
|
+
if not fm and text.startswith("---\n"):
|
|
103
|
+
# 结构非法:有 frontmatter 起始符但无闭合——loads 无法切出
|
|
104
|
+
# 字段(正常节点必有 frontmatter;无 frontmatter 的裸文件
|
|
105
|
+
# 按 _scan_nodes 旧例仍以文件名登入,不判损坏)。
|
|
106
|
+
problems.append({"path": rel,
|
|
107
|
+
"kind": "frontmatter_unterminated",
|
|
108
|
+
"detail": "--- 无闭合"})
|
|
109
|
+
continue
|
|
110
|
+
nid = fm.get("id") or fn[:-3]
|
|
111
|
+
by_nid[nid] = {"path": rel, "abs": p, "layer": layer,
|
|
112
|
+
"fm": fm, "content": content}
|
|
113
|
+
path_owner[rel] = nid
|
|
114
|
+
return by_nid, path_owner, problems
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# 生效条件:延迟导入 mdcg 取模块级 LAYERS 并返回(与 _scan_nodes 的遍历集合同一真源;函数内导入避免 mdcg ↔ reconcile 模块环)。
|
|
118
|
+
def _layers():
|
|
119
|
+
from .mdcg import LAYERS
|
|
120
|
+
return LAYERS
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# 生效条件:把单条异常以 JSONL 追加进 trace_path(cg)(best-effort:OSError 吞掉不抛——留痕是增益,不得反过来炸启动序列),无返回值。
|
|
124
|
+
def _trace(cg, record: dict) -> None:
|
|
125
|
+
try:
|
|
126
|
+
append_jsonl(trace_path(cg), record)
|
|
127
|
+
except OSError:
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# 生效条件:向 stderr 写一行告警(try/except 全包——stderr 为 None/closed/断管时静默放弃,N136 教训:告警通道失效不得伪装成新异常炸穿调用方),无返回值。
|
|
132
|
+
def _warn(msg: str) -> None:
|
|
133
|
+
try:
|
|
134
|
+
import sys
|
|
135
|
+
sys.stderr.write("[mdcg-reconcile] %s\n" % msg)
|
|
136
|
+
except Exception: # noqa: BLE001 —— 告警失效不追责
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# 生效条件:cg.index 须含 "nodes" 映射;enabled() 为 False 时直接返回 {"skipped": True, "applied": False} 不触盘;否则 _scan_disk 后做三类 diff:①nid 不在盘面且其 entry.path 既非问题文件也非无主——判幽灵(apply 时逐条 _unstage);②盘面 nid 不在索引——判缺失(apply 时以 cg._node_entry 构条目 _stage);③两上面向交的 nid 中 entry.path 与盘面落点不符或 content_hash 不在盘面双形态接受集——判漂移(apply 时以盘面重建条目 _stage);apply 为真时收尾 cg.flush() 持久化;diff 非空时写 stderr 告警并 _trace 留痕(各类采样 ≤SAMPLE_CAP);返回含 skipped/applied/scanned/entries/ghost_entries/missing_entries/hash_drift/unwritable_source/repaired 的报告 dict,applied=bool(apply)。
|
|
141
|
+
def reconcile_state(cg, apply: bool = True) -> dict:
|
|
142
|
+
"""索引(派生态)vs 盘面真源的三类 diff 对账(reconcile v0 主入口)。
|
|
143
|
+
|
|
144
|
+
`apply=False` 为 dry-run(只 diff 报告,不修复、不写盘)。返回报告:
|
|
145
|
+
`{"skipped","applied","scanned","entries","ghost_entries",
|
|
146
|
+
"missing_entries","hash_drift","unwritable_source","repaired"}`。
|
|
147
|
+
幂等:修复后的索引面与盘面一致,二次扫描 diff 为空、零输出零写盘。
|
|
148
|
+
"""
|
|
149
|
+
if not enabled():
|
|
150
|
+
return {"skipped": True, "applied": False}
|
|
151
|
+
index_nodes = cg.index.get("nodes") or {}
|
|
152
|
+
by_nid, path_owner, problems = _scan_disk(cg)
|
|
153
|
+
|
|
154
|
+
problem_paths = {p["path"] for p in problems}
|
|
155
|
+
ghost, missing, drift = [], [], []
|
|
156
|
+
kept_unreadable = [] # 条目在、真源暂不可读——保留只告警
|
|
157
|
+
|
|
158
|
+
# ①③ 以索引为基准走一遍:多索引条目 + 内容漂移
|
|
159
|
+
for nid, entry in index_nodes.items():
|
|
160
|
+
if nid not in by_nid:
|
|
161
|
+
ep = entry.get("path") or ""
|
|
162
|
+
if ep in problem_paths:
|
|
163
|
+
kept_unreadable.append(nid) # 真源读不出 ≠ 文件没了:不摘条目
|
|
164
|
+
else:
|
|
165
|
+
ghost.append(nid) # 盘面无其文件(或已归属他 id)
|
|
166
|
+
continue
|
|
167
|
+
d = by_nid[nid]
|
|
168
|
+
h_raw, h_norm = _disk_hashes(d["content"])
|
|
169
|
+
if entry.get("path") != d["path"] \
|
|
170
|
+
or entry.get("content_hash") not in (h_raw, h_norm):
|
|
171
|
+
drift.append(nid)
|
|
172
|
+
|
|
173
|
+
# ② 以盘面为基准走一遍:缺索引条目(仅成功解析的真源参与)
|
|
174
|
+
for nid in by_nid:
|
|
175
|
+
if nid not in index_nodes:
|
|
176
|
+
missing.append(nid)
|
|
177
|
+
|
|
178
|
+
repaired = {"unstaged": 0, "staged": 0}
|
|
179
|
+
if apply:
|
|
180
|
+
for nid in ghost:
|
|
181
|
+
cg._unstage(nid) # tombstone + 立即持久化
|
|
182
|
+
repaired["unstaged"] += 1
|
|
183
|
+
for nid in missing + drift:
|
|
184
|
+
d = by_nid[nid]
|
|
185
|
+
cg._stage(nid, cg._node_entry(d["abs"], d["layer"],
|
|
186
|
+
d["fm"], d["content"]))
|
|
187
|
+
repaired["staged"] += 1
|
|
188
|
+
if repaired["unstaged"] or repaired["staged"]:
|
|
189
|
+
cg.flush() # 启动期一次收尾落账(不赖 autoflush)
|
|
190
|
+
|
|
191
|
+
rep = {
|
|
192
|
+
"skipped": False,
|
|
193
|
+
"applied": bool(apply),
|
|
194
|
+
"scanned": len(by_nid) + len(problems),
|
|
195
|
+
"entries": len(index_nodes),
|
|
196
|
+
"ghost_entries": ghost,
|
|
197
|
+
"missing_entries": missing,
|
|
198
|
+
"hash_drift": drift,
|
|
199
|
+
"unwritable_source": problems,
|
|
200
|
+
"repaired": repaired,
|
|
201
|
+
}
|
|
202
|
+
if kept_unreadable:
|
|
203
|
+
rep["kept_unreadable"] = kept_unreadable
|
|
204
|
+
|
|
205
|
+
# 告警 + 留痕(diff 非空才发声;干净库零输出零写盘)
|
|
206
|
+
if ghost or missing or drift or problems or kept_unreadable:
|
|
207
|
+
_warn("启动对账 v0: 幽灵条目=%s 缺失条目=%s 哈希漂移=%s "
|
|
208
|
+
"真源损坏/不可读=%s(%s)"
|
|
209
|
+
% (len(ghost), len(missing), len(drift), len(problems),
|
|
210
|
+
"已按盘面增量修复索引面" if apply else "dry-run 未修复"))
|
|
211
|
+
for nid in ghost[:SAMPLE_CAP]:
|
|
212
|
+
_warn(" 幽灵条目(盘面无文件→摘除): %s" % (nid,))
|
|
213
|
+
for nid in kept_unreadable[:SAMPLE_CAP]:
|
|
214
|
+
_warn(" 条目保留(真源暂不可读,不猜测不摘除): %s" % (nid,))
|
|
215
|
+
for pr in problems[:SAMPLE_CAP]:
|
|
216
|
+
_warn(" 真源损坏/不可读(只告警不改写——T9 边界): %s [%s]"
|
|
217
|
+
% (pr["path"], pr["kind"]))
|
|
218
|
+
_trace(cg, {
|
|
219
|
+
"t": time.time(), "op": "reconcile_v0", "apply": bool(apply),
|
|
220
|
+
"scanned": rep["scanned"], "entries": rep["entries"],
|
|
221
|
+
"ghost": ghost[:SAMPLE_CAP], "ghost_n": len(ghost),
|
|
222
|
+
"missing": missing[:SAMPLE_CAP], "missing_n": len(missing),
|
|
223
|
+
"drift": drift[:SAMPLE_CAP], "drift_n": len(drift),
|
|
224
|
+
"problems": problems[:SAMPLE_CAP], "problems_n": len(problems),
|
|
225
|
+
"kept_unreadable": kept_unreadable[:SAMPLE_CAP],
|
|
226
|
+
"repaired": repaired,
|
|
227
|
+
})
|
|
228
|
+
return rep
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""review_cli · 审核队列裁决命令行(设计者/管理权限专用)
|
|
3
|
+
|
|
4
|
+
**包内入口**(2026-09-24 修复):原实现只在 `scripts/review_cli.py`,而出货包
|
|
5
|
+
`files` 不含 `scripts/` —— 于是包内所有给用户/智能体的提示(writepipe 的
|
|
6
|
+
「已入审核队列 → 请裁决」)都指向一个安装态不存在的文件。本模块把同一实现
|
|
7
|
+
搬进包内,统一入口 `python -m md_cg.review_cli`;`scripts/review_cli.py`
|
|
8
|
+
在源码树内仍可用(同一代码路径:那里 _HERE 同为插件根)。
|
|
9
|
+
|
|
10
|
+
背景:本机未配置外部验证器(MDCG_VERIFIER_MODULES)时,写入恒判 DEFER
|
|
11
|
+
入审核队列——这是诚实行为(不假装通过)。裁决权专属 can_admin 角色
|
|
12
|
+
(写入者不得自裁自决),agent 端被 AccessDenied 拒绝是设计行为;
|
|
13
|
+
由设计者在本机直接运行本命令完成裁决。
|
|
14
|
+
|
|
15
|
+
用法(root 须与待裁决部署一致:--root 或环境变量 MDCG_ROOT):
|
|
16
|
+
python -m md_cg.review_cli list
|
|
17
|
+
python -m md_cg.review_cli accept <pid> --reason "实跑测试证据"
|
|
18
|
+
python -m md_cg.review_cli reject <pid> --reason "内容有误"
|
|
19
|
+
python -m md_cg.review_cli edit <pid> --content "修正后内容" --reason "..."
|
|
20
|
+
python -m md_cg.review_cli merge <pid> --into <已有节点id> --reason "..."
|
|
21
|
+
python -m md_cg.review_cli noop <pid> --reason "已评估,判定无需改动"
|
|
22
|
+
python -m md_cg.review_cli rounds <pid> # 某提案裁决轮次历史
|
|
23
|
+
python -m md_cg.review_cli stats # 裁决动作统计(含 noop)
|
|
24
|
+
|
|
25
|
+
noop 语义:**已评估、判定不改变任何现有记忆**——只留痕(decisions.jsonl +
|
|
26
|
+
审计 md 节点)并关闭提案,不落业务节点、不进负记忆。它与 reject 的区别是
|
|
27
|
+
「评估过了、无需改动」而非「否掉这条候选」,故不可借 noop 绕过 accept 门控。
|
|
28
|
+
|
|
29
|
+
裁决留痕:decisions.jsonl + 审计 md 节点(由 review_decide 内部完成)。
|
|
30
|
+
"""
|
|
31
|
+
import argparse
|
|
32
|
+
import json
|
|
33
|
+
import os
|
|
34
|
+
import sys
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
38
|
+
except AttributeError:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
_HERE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
42
|
+
if _HERE not in sys.path:
|
|
43
|
+
sys.path.insert(0, _HERE)
|
|
44
|
+
|
|
45
|
+
from md_cg.mdcos import MdCGSecure # noqa: E402
|
|
46
|
+
from md_cg.security import Principal # noqa: E402
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# 生效条件:args 的 root 属性为真值(含 getattr 缺省 None 时回落)否则回落 os.environ.get("MDCG_ROOT", ""),两者皆空串或该值经 os.path.isdir 判定不是目录时 sys.exit 退出,否则返回该 root。
|
|
50
|
+
def _root(args):
|
|
51
|
+
root = getattr(args, "root", None) or os.environ.get("MDCG_ROOT", "")
|
|
52
|
+
if not root:
|
|
53
|
+
sys.exit("错误:未指定存储根(--root 或环境变量 MDCG_ROOT)。\n"
|
|
54
|
+
"root 必须与待裁决的部署一致——猜错会裁决到另一个空库。")
|
|
55
|
+
if not os.path.isdir(root):
|
|
56
|
+
sys.exit("错误:root 不存在:%s" % root)
|
|
57
|
+
return root
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# 生效条件:args 就绪时先构造写死权限的 Principal(actor="designer-cli", clearance="secret", can_write=True, can_admin=True, role="designer", auth_mode="local-cli"),再以 _root(args) 取到的存储根返回 MdCGSecure(root, principal=p)。
|
|
61
|
+
def _cg(args):
|
|
62
|
+
p = Principal(actor="designer-cli", clearance="secret",
|
|
63
|
+
can_write=True, can_admin=True, role="designer",
|
|
64
|
+
auth_mode="local-cli")
|
|
65
|
+
return MdCGSecure(_root(args), principal=p)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# 生效条件:rec 支持 .get 时,取 rec.get("content") 为假值则取 rec.get("statement")、再为假值则取空串,把其中的换行替换为空格,并按 width(缺省 66)切片后仅当 len(text) > width 才追加 "…",返回该字符串。
|
|
69
|
+
def _brief(rec, width=66):
|
|
70
|
+
text = (rec.get("content") or rec.get("statement") or "").replace("\n", " ")
|
|
71
|
+
return text[:width] + ("…" if len(text) > width else "")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# 生效条件:cg 与 args 就绪时按 args.cmd 分派——"list" 时 cg.review_list() 为空则打印空队列并返回 0、非空则逐条打印(tags 取真值拼接、layer/round 为假值显示 "?"/0)后返回 0;"rounds" 时打印 cg.review_rounds(args.pid) 并返回 0;"stats" 时打印 cg.review_stats() 的记录数/提案数/待审数/已关闭数与动作分布(含 noop 计数)并返回 0;"edit" 时以 args.content 加真值 args.tags(按逗号分割并剔除空项)/args.layer 组成 edits 调 cg.review_decide;其余 cmd(含 noop)以 getattr(args, "into", None) 与 args.reason 调 cg.review_decide;后两类再按 out.get("ok") 为真返回 0,否则打印 out 并返回 1。
|
|
75
|
+
def _execute(cg, args):
|
|
76
|
+
"""按子命令执行裁决(cg 的生命周期由 main 统一收尾)。"""
|
|
77
|
+
if args.cmd == "list":
|
|
78
|
+
pend = cg.review_list()
|
|
79
|
+
if not pend:
|
|
80
|
+
print("审核队列为空(0 条待审)。")
|
|
81
|
+
return 0
|
|
82
|
+
print("待审 %d 条:" % len(pend))
|
|
83
|
+
for r in pend:
|
|
84
|
+
tags = (", tags=" + ",".join(r.get("tags") or [])) if r.get("tags") else ""
|
|
85
|
+
print(" [%s] %s · %s 层%s · round=%s\n %s" % (
|
|
86
|
+
r.get("pid"), r.get("status"), r.get("layer") or "?",
|
|
87
|
+
tags, r.get("round") or 0, _brief(r)))
|
|
88
|
+
print('\n裁决示例:python -m md_cg.review_cli accept <pid> --reason "实跑测试证据"')
|
|
89
|
+
return 0
|
|
90
|
+
|
|
91
|
+
if args.cmd == "rounds":
|
|
92
|
+
print(json.dumps(cg.review_rounds(args.pid), ensure_ascii=False, indent=1))
|
|
93
|
+
return 0
|
|
94
|
+
|
|
95
|
+
if args.cmd == "stats":
|
|
96
|
+
st = cg.review_stats()
|
|
97
|
+
by = st.get("by_decision") or {}
|
|
98
|
+
print("裁决记录 %d 条 · 提案 %d 个 · 待审 %d 条 · 已关闭 %d 个" % (
|
|
99
|
+
st.get("records", 0), st.get("proposals", 0),
|
|
100
|
+
st.get("pending", 0), st.get("closed", 0)))
|
|
101
|
+
print(" 动作分布:%s" % ("、".join(
|
|
102
|
+
"%s=%d" % (k, by[k]) for k in sorted(by)) or "(无记录)"))
|
|
103
|
+
print(" 其中 noop(已评估、判定无需改动)= %d 条" % st.get("noop", 0))
|
|
104
|
+
return 0
|
|
105
|
+
|
|
106
|
+
if args.cmd == "edit":
|
|
107
|
+
edits = {"content": args.content}
|
|
108
|
+
if args.tags:
|
|
109
|
+
edits["tags"] = [t.strip() for t in args.tags.split(",") if t.strip()]
|
|
110
|
+
if args.layer:
|
|
111
|
+
edits["layer"] = args.layer
|
|
112
|
+
out = cg.review_decide(args.pid, "edit", edits=edits, reason=args.reason)
|
|
113
|
+
else:
|
|
114
|
+
out = cg.review_decide(args.pid, args.cmd,
|
|
115
|
+
merge_into=getattr(args, "into", None),
|
|
116
|
+
reason=args.reason)
|
|
117
|
+
|
|
118
|
+
if out.get("ok"):
|
|
119
|
+
print("已裁决:%s → %s%s" % (
|
|
120
|
+
args.pid, args.cmd,
|
|
121
|
+
(",落盘节点 " + out["node_id"]) if out.get("node_id") else ""))
|
|
122
|
+
return 0
|
|
123
|
+
print("裁决未生效:%s" % json.dumps(out, ensure_ascii=False))
|
|
124
|
+
return 1
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# 生效条件:argv 为 None(默认)时由 argparse 解析 sys.argv、否则解析传入的 argv(子命令 dest="cmd" 为 required,已注册 list/accept/reject/edit/merge/rounds 并带 --root 等参数),解析成功后构造 cg=_cg(args) 并返回 _execute(cg, args),finally 中执行 cg.close()。
|
|
128
|
+
def main(argv=None):
|
|
129
|
+
ap = argparse.ArgumentParser(
|
|
130
|
+
prog="python -m md_cg.review_cli",
|
|
131
|
+
description="灵枢审核队列裁决(designer 权限)")
|
|
132
|
+
common = argparse.ArgumentParser(add_help=False)
|
|
133
|
+
common.add_argument("--root", help="存储根目录(默认环境变量 MDCG_ROOT)")
|
|
134
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
135
|
+
|
|
136
|
+
sub.add_parser("list", help="列出待审条目", parents=[common])
|
|
137
|
+
sub.add_parser("stats", help="裁决动作统计(含 noop)", parents=[common])
|
|
138
|
+
for name, help_ in (("accept", "按原样写入落盘"),
|
|
139
|
+
("reject", "丢弃(只记裁决)"),
|
|
140
|
+
("noop", "已评估、判定不改变现有记忆(只留痕)")):
|
|
141
|
+
s = sub.add_parser(name, help=help_, parents=[common])
|
|
142
|
+
s.add_argument("pid")
|
|
143
|
+
s.add_argument("--reason", default="", help="裁决理由(进留痕)")
|
|
144
|
+
s = sub.add_parser("edit", help="修订后写入", parents=[common])
|
|
145
|
+
s.add_argument("pid")
|
|
146
|
+
s.add_argument("--content", required=True)
|
|
147
|
+
s.add_argument("--tags", default=None, help="逗号分隔")
|
|
148
|
+
s.add_argument("--layer", default=None)
|
|
149
|
+
s.add_argument("--reason", default="")
|
|
150
|
+
s = sub.add_parser("merge", help="合并进已有节点", parents=[common])
|
|
151
|
+
s.add_argument("pid")
|
|
152
|
+
s.add_argument("--into", required=True, help="目标节点 id")
|
|
153
|
+
s.add_argument("--reason", default="")
|
|
154
|
+
s = sub.add_parser("rounds", help="某提案的裁决轮次历史", parents=[common])
|
|
155
|
+
s.add_argument("pid")
|
|
156
|
+
args = ap.parse_args(argv)
|
|
157
|
+
|
|
158
|
+
cg = _cg(args)
|
|
159
|
+
try:
|
|
160
|
+
return _execute(cg, args)
|
|
161
|
+
finally:
|
|
162
|
+
# 收尾(2026-09-16 取证):裁决写入先进内存 _dirty,不落盘则「节点在盘上
|
|
163
|
+
# 但索引无条目」——已有 _index.json 的根重开不重扫目录,其它进程与重载后的
|
|
164
|
+
# 长驻进程都检索不到(只能靠某次全量 rebuild 偶然救回)。库层另有 atexit
|
|
165
|
+
# 兜底,但显式收尾才是正路:兜底只覆盖「正常退出」这一条路径。
|
|
166
|
+
cg.close()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
if __name__ == "__main__":
|
|
170
|
+
sys.exit(main())
|
package/md_cg/routing.py
CHANGED
|
@@ -355,6 +355,34 @@ def bucket_key_readable(bucket: str) -> str:
|
|
|
355
355
|
return s
|
|
356
356
|
|
|
357
357
|
|
|
358
|
+
# 中文词判定:至少含一个汉字
|
|
359
|
+
_ZH_RE = re.compile(r"[\u4e00-\u9fff]")
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
# 生效条件:key 缺失或含汉字时返回 [];否则从 tags(保序在前)与 text 域词(domain_terms)中收集含汉字、长度≥2 的词,去重取前 limit 个返回。
|
|
363
|
+
def bucket_zh_aliases(key: str, tags, text, limit: int = 6) -> list:
|
|
364
|
+
"""英文桶键的中文别名(issue #33:S1b 跨语言收敛盲区的修复面)。
|
|
365
|
+
|
|
366
|
+
机理:domain_similarity 的 bigram 兜底跨语言交集恒空——英文桶键 × 中文
|
|
367
|
+
query 恒 0.0 → S1b 恒 no_key_match(真实库 91.1% 中文节点恰是设计目标场景)。
|
|
368
|
+
别名**只取节点自身内容**(tags 中文词 ∪ 正文中文域词,tags 优先),零翻译
|
|
369
|
+
依赖——unify_query/en_zh_terms 桥接已被 #33 实测排除(字级直译语义毁)。
|
|
370
|
+
键本身含中文时无跨语言问题,返回 []。
|
|
371
|
+
"""
|
|
372
|
+
if not key or _ZH_RE.search(str(key)):
|
|
373
|
+
return []
|
|
374
|
+
out, seen = [], set()
|
|
375
|
+
src = [str(t) for t in (tags or [])]
|
|
376
|
+
src += domain_terms(text or "", limit=200)
|
|
377
|
+
for w in src:
|
|
378
|
+
if len(w) >= 2 and _ZH_RE.search(w) and w not in seen:
|
|
379
|
+
seen.add(w)
|
|
380
|
+
out.append(w)
|
|
381
|
+
if len(out) >= limit:
|
|
382
|
+
break
|
|
383
|
+
return out
|
|
384
|
+
|
|
385
|
+
|
|
358
386
|
# 生效条件:text 为假值或取不到任何域词时返回 None;否则返回 big_domain_classify(domain_terms(text))
|
|
359
387
|
# 的结果(无有效域信号时亦为 None,调用方据此决定是否落域字段)。
|
|
360
388
|
def classify_text(text, limit: int = 400):
|