@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/mdcos.py
CHANGED
|
@@ -36,7 +36,7 @@ from . import (nodefile, routing, chain, subgraph, forgetting, protect,
|
|
|
36
36
|
self_state, predict, evolution, weights, pooling,
|
|
37
37
|
writelimit, reach, trust, roleviews)
|
|
38
38
|
from .fsutil import (FileLock, atomic_write, append_jsonl, read_jsonl,
|
|
39
|
-
count_jsonl)
|
|
39
|
+
read_jsonl_tail, count_jsonl, publish)
|
|
40
40
|
from .security import (Principal, TenantRegistry, AccessDenied,
|
|
41
41
|
SENSITIVITY_ORDER, DEFAULT_SENSITIVITY, _rank)
|
|
42
42
|
|
|
@@ -326,6 +326,27 @@ class MdCGOS(MdCG):
|
|
|
326
326
|
os.makedirs(self.trash_dir, exist_ok=True)
|
|
327
327
|
self._audit_writes = 0 # 进程内写入计数(轮转探测节流,稳态零 stat)
|
|
328
328
|
self._audit_index = None # 归档索引缓存(懒加载)
|
|
329
|
+
# 审核队列增量对账缓存(issue #32):inbox/decisions 均为 append-only
|
|
330
|
+
# (写点全在 strict 锁内经 append_jsonl,无轮转/截断),propose 的
|
|
331
|
+
# 幂等对账从「每条全量重读 O(M+D)」改为「stat 尾部增量」——跨进程
|
|
332
|
+
# append 由 stat 自动并入,无需显式失效。
|
|
333
|
+
self._inbox_idx = {} # phash → [(pid, node_id)] 文件序
|
|
334
|
+
self._inbox_idx_size = -1 # 已并入的 inbox 字节偏移(扫描水位)
|
|
335
|
+
self._inbox_idx_mtime = -1
|
|
336
|
+
self._pid_st = {} # pid → 最新裁决记录
|
|
337
|
+
self._pid_st_size = -1 # 已并入的 decisions 字节偏移
|
|
338
|
+
self._pid_st_mtime = -1
|
|
339
|
+
# review 治理面全记录增量缓存(issue #35):review_stats/review_list/
|
|
340
|
+
# review_decide/_cascade_dedup/verify_review_record 单次调用内曾各自
|
|
341
|
+
# 全量重读同一 jsonl(inbox 2 遍 + decisions 1-4 遍)——改为 stat 尾部
|
|
342
|
+
# 增量的全记录缓存(与 _pid_status 同款对账),调用方内存线性扫。
|
|
343
|
+
# 注意:缓存列表为内部状态,调用方只读不得变更。
|
|
344
|
+
self._inbox_recs = [] # inbox 全记录(文件序)
|
|
345
|
+
self._inbox_recs_size = -1
|
|
346
|
+
self._inbox_recs_mtime = -1
|
|
347
|
+
self._dec_recs = [] # decisions 全记录(文件序)
|
|
348
|
+
self._dec_recs_size = -1
|
|
349
|
+
self._dec_recs_mtime = -1
|
|
329
350
|
# 热缓存挂载(issue #28,2026-09-23):读侧(search_rrf)与失效侧
|
|
330
351
|
# (writepipe)早已接好,唯独无人调 attach——生产恒 None,重复 query
|
|
331
352
|
# 无法命中。默认关(env 未设 → get(self) 恒 None,零变更纪律);
|
|
@@ -336,10 +357,14 @@ class MdCGOS(MdCG):
|
|
|
336
357
|
_hc.attach(self)
|
|
337
358
|
# 检索读缓存挂载(issue #31 P2,批次 21):MdStore 理论落地——节点
|
|
338
359
|
# 文件解析产物常驻(567 池实测 572 次 open/查询 → 缓存后 ~0),写失
|
|
339
|
-
# 效走 _dirty
|
|
340
|
-
#
|
|
341
|
-
|
|
342
|
-
|
|
360
|
+
# 效走 _dirty 代际哨兵(所有写路径必然标脏,无漏挂面)。
|
|
361
|
+
# 默认**开**(issue #31 后续修复):生产检索入口(本类 search →
|
|
362
|
+
# _read_many)默认态每查询全池 open+2×realpath+frontmatter 解析,
|
|
363
|
+
# 3300 池实测 ~602ms/查询、万级外推 ~2s+(O(n) 线性);开关早已
|
|
364
|
+
# 存在但默认关 → 生产恒不装配。MDCG_READ_CACHE=0 显式关闭(opt-out
|
|
365
|
+
# 退出阀:跨进程改写须本进程立即可见 / 内存受限的部署回到旧行为)。
|
|
366
|
+
from . import readcache as _rc
|
|
367
|
+
if _rc.enabled():
|
|
343
368
|
_rc.install(self)
|
|
344
369
|
|
|
345
370
|
# ================= 6. payload-free 审计 =================
|
|
@@ -457,7 +482,7 @@ class MdCGOS(MdCG):
|
|
|
457
482
|
name = "_audit.%06d.jsonl" % self._next_shard_seq()
|
|
458
483
|
dst = os.path.join(self.audit_archive, name)
|
|
459
484
|
try:
|
|
460
|
-
|
|
485
|
+
publish(self.audit_log, dst) # Windows 短重试:AV 短锁不误判抢输
|
|
461
486
|
except OSError:
|
|
462
487
|
return None # 抢输(文件已被移走)→ 让位,不报错
|
|
463
488
|
scale = self._audit_count_shard(dst, size) # 分档:有界扫描 / 只读量级
|
|
@@ -580,10 +605,22 @@ class MdCGOS(MdCG):
|
|
|
580
605
|
"""在父类 add 之上:写入 role(frontmatter + 索引),默认 role=None(知识)。"""
|
|
581
606
|
if role is not None:
|
|
582
607
|
kw["role"] = role
|
|
608
|
+
# 会话归属必须落 frontmatter:本文件 session 过滤的文档即写「frontmatter.session,
|
|
609
|
+
# 写入时自动落盘」,但父类 add 的 fm 固定字典里没有该键、_stage 只把它放进索引 →
|
|
610
|
+
# 实际只有索引带归属,任何 rebuild_index() 都会抹掉(实测 73 节点里 56 个无
|
|
611
|
+
# frontmatter.session,而 _audit.jsonl 的 add 事件却 152/152 都记了 session:
|
|
612
|
+
# 写入口径与落盘口径脱钩)。与 role 同款:显式给就尊重,否则取 self.session
|
|
613
|
+
# (MdCGSecure 由 principal.session 注入,mcp_server 按请求切换 cg.session)。
|
|
614
|
+
_sess = kw.get("session") or getattr(self, "session", None)
|
|
615
|
+
if _sess and not kw.get("session"):
|
|
616
|
+
kw["session"] = _sess
|
|
583
617
|
nid = super().add(node_id, content, layer=layer, **kw)
|
|
584
618
|
e = self.index["nodes"].get(nid)
|
|
585
619
|
if e is not None and role is not None:
|
|
586
620
|
e["role"] = role
|
|
621
|
+
if e is not None and _sess:
|
|
622
|
+
e["session"] = _sess
|
|
623
|
+
if e is not None and (role is not None or _sess):
|
|
587
624
|
self._dirty[nid] = e
|
|
588
625
|
self._audit("add", nid, layer=layer, role=role,
|
|
589
626
|
payload_hash=_sig(content))
|
|
@@ -727,6 +764,7 @@ class MdCGOS(MdCG):
|
|
|
727
764
|
q = unify_query(q)
|
|
728
765
|
if not q:
|
|
729
766
|
return [], {"tier": None, "reason": "empty_query", "scanned": 0}
|
|
767
|
+
self._maybe_reload_index() # P1b-2:读面代际感知(route/read/search 链)
|
|
730
768
|
pool_cfg = pooling.resolve(pooling.from_env(pools))
|
|
731
769
|
entries = self._candidates(layer=layer, roles=roles, include_work=include_work,
|
|
732
770
|
session=session, branch=branch, validity=validity,
|
|
@@ -758,6 +796,12 @@ class MdCGOS(MdCG):
|
|
|
758
796
|
if context is not None:
|
|
759
797
|
ctx = context if isinstance(context, dict) else {}
|
|
760
798
|
route_bucket = routing.bucket_dir(routing.route_key(ctx, ctx.get("tags")))
|
|
799
|
+
# 与 MdCG.search 同一纪律(X2):无域信号时不施加桶约束,否则 route_key
|
|
800
|
+
# 回落的 orphan 会被当成真桶,下面 T0/T1 阶段读到该桶并因 valid>=min_results
|
|
801
|
+
# 直接 return,**全量阶梯永不执行** —— 标了 domain: 的节点整片不可见。
|
|
802
|
+
# 本覆写才是生产路径(MdCGSecure → 本类):缺此条则 MCP 端依旧丢召回。
|
|
803
|
+
if route_bucket == routing.ORPHAN:
|
|
804
|
+
route_bucket = None
|
|
761
805
|
big_domain = routing.big_domain_classify(terms)
|
|
762
806
|
big_scores = routing.big_domain_score_breakdown(terms)
|
|
763
807
|
# S1/S1b/S2 收敛 + S4 审计——与 MdCG.search 共享同一实现(issue #25:
|
|
@@ -771,9 +815,15 @@ class MdCGOS(MdCG):
|
|
|
771
815
|
stat["gates"] = gates
|
|
772
816
|
neg_coverage = self._neg_coverage(terms) if include_neg else []
|
|
773
817
|
|
|
774
|
-
#
|
|
775
|
-
def try_stage(docs, tier):
|
|
776
|
-
|
|
818
|
+
# 生效条件:scored 为 None 时对 docs 调 self._score(docs, q, qb, pool_cfg),非 None 时直接以之为打分结果;分数 >0 的条数达到(search 作用域内的)min_results 就返回 self._emit(scored, k, tier, stat, route_bucket, record, len(docs), ...),否则返回 None。
|
|
819
|
+
def try_stage(docs, tier, scored=None):
|
|
820
|
+
# P2-4 回移(v7 N51 留档修复,缺陷迭代第 14 轮):支持传入已打分
|
|
821
|
+
# 结果——此前 T2/T3 均为「打分截断后又对 picked 重打」(600 池实测
|
|
822
|
+
# _score 4 调 [0,0,600,500]=1100 文档,500/1100≈45% 冗余)。与基类
|
|
823
|
+
# try_stage(mdcg.py P2-4,批次 31)同模板;_score 在单次 search 内
|
|
824
|
+
# 是纯函数(docs/q/qb/pool_cfg 均不变),透传不改变任何分值与输出。
|
|
825
|
+
if scored is None:
|
|
826
|
+
scored = self._score(docs, q, qb, pool_cfg)
|
|
777
827
|
valid = sum(1 for _, s in scored if s > 0)
|
|
778
828
|
if valid >= min_results:
|
|
779
829
|
return self._emit(scored, k, tier, stat, route_bucket, record,
|
|
@@ -781,6 +831,22 @@ class MdCGOS(MdCG):
|
|
|
781
831
|
big_domain, big_scores, pool_cfg)
|
|
782
832
|
return None
|
|
783
833
|
|
|
834
|
+
def _prescored(docs, scored_all):
|
|
835
|
+
"""P2-4 透传形态:按 node_id 键表把 docs 映射回已打分的
|
|
836
|
+
(card, score) 原对(恰为 _score 对该 docs 的输出,picked 序)。
|
|
837
|
+
|
|
838
|
+
键口径:scored 元素是 (card, score),card["id"] = fm.get("id")
|
|
839
|
+
or e["path"],恰为 doc_key(doc)[0](V21-6 取 [0] 作键同源)——
|
|
840
|
+
**不可**对 scored 元素调 pooling.doc_key(其形参是 (entry, fm,
|
|
841
|
+
content) 三元组;基类 reach 分支旧模板即此形态错误,MDCG_REACH=1
|
|
842
|
+
时 KeyError: 1,见 mdcg.py 同修)。**不可**自组 (doc, score) 对
|
|
843
|
+
(_emit 排序键读 x[0]["frontmatter"],x[0] 须是 card dict)。
|
|
844
|
+
docs 必为 scored_all 的子序列(cut_by_relevance 只选不造),
|
|
845
|
+
键表必命中。
|
|
846
|
+
"""
|
|
847
|
+
smap = {c.get("id"): (c, s) for c, s in scored_all}
|
|
848
|
+
return [smap[pooling.doc_key(d)[0]] for d in docs]
|
|
849
|
+
|
|
784
850
|
if route_bucket:
|
|
785
851
|
in_bucket = [e for e in entries if e.get("bucket") == route_bucket]
|
|
786
852
|
if in_bucket:
|
|
@@ -819,11 +885,15 @@ class MdCGOS(MdCG):
|
|
|
819
885
|
stat["cap"] = GLOBAL_CAP
|
|
820
886
|
# 与 T2 **无条件**同序调用(不可按 cap 短路:cut_by_relevance 还会写
|
|
821
887
|
# 池账 pool_taken/cands/lost,短路会让 meta.pools.taken 缺失 → test_p43(13) 红)
|
|
822
|
-
|
|
888
|
+
scored_r = self._score(hits_r, q, qb, pool_cfg)
|
|
889
|
+
hits_r, _rep = cut_by_relevance(hits_r, scored_r,
|
|
823
890
|
GLOBAL_CAP, pools=pool_cfg,
|
|
824
891
|
key_of=pooling.doc_key, stat=stat)
|
|
825
892
|
pooling.record_audit(stat, _rep)
|
|
826
|
-
|
|
893
|
+
# P2-4:打分结果经 scored 透传,不再对同一 hits_r 二次 _score
|
|
894
|
+
# (与基类 reach 分支同模板)。
|
|
895
|
+
out = try_stage(hits_r, TIER_GLOBAL_LIKE,
|
|
896
|
+
scored=_prescored(hits_r, scored_r))
|
|
827
897
|
if out:
|
|
828
898
|
return out
|
|
829
899
|
# 收敛阶段未产出结果 → 继续走下方全量 T2/T3;此时必须**改写 reach 审计**,
|
|
@@ -844,22 +914,27 @@ class MdCGOS(MdCG):
|
|
|
844
914
|
or (semantic_on() and d[1].get("semantic"))]
|
|
845
915
|
stat["pre_cap"] = len(hits)
|
|
846
916
|
stat["cap"] = GLOBAL_CAP
|
|
847
|
-
|
|
917
|
+
scored_h = self._score(hits, q, qb, pool_cfg)
|
|
918
|
+
hits, _rep = cut_by_relevance(hits, scored_h,
|
|
848
919
|
GLOBAL_CAP, pools=pool_cfg,
|
|
849
920
|
key_of=pooling.doc_key, stat=stat)
|
|
850
921
|
pooling.record_audit(stat, _rep)
|
|
851
|
-
|
|
922
|
+
# P2-4:截断打分结果透传,T2 腿不再对 hits 二次 _score。
|
|
923
|
+
out = try_stage(hits, TIER_GLOBAL_LIKE,
|
|
924
|
+
scored=_prescored(hits, scored_h))
|
|
852
925
|
if out:
|
|
853
926
|
return out
|
|
854
927
|
|
|
855
928
|
stat["pre_cap"] = len(docs_all)
|
|
856
929
|
stat["cap"] = GLOBAL_CAP
|
|
857
|
-
|
|
858
|
-
|
|
930
|
+
scored_a = self._score(docs_all, q, qb, pool_cfg)
|
|
931
|
+
picked, _rep = cut_by_relevance(docs_all, scored_a,
|
|
859
932
|
GLOBAL_CAP, pools=pool_cfg,
|
|
860
933
|
key_of=pooling.doc_key, stat=stat)
|
|
861
934
|
pooling.record_audit(stat, _rep)
|
|
862
|
-
|
|
935
|
+
# P2-4:T3 不再对 picked 重打(600 池实测此腿冗余 500 文档/查询,
|
|
936
|
+
# GLOBAL_CAP=500 截断后 picked ⊆ docs_all,键表回填即得终榜打分)。
|
|
937
|
+
scored = _prescored(picked, scored_a)
|
|
863
938
|
return self._emit(scored, k, TIER_GLOBAL_SCAN, stat, route_bucket,
|
|
864
939
|
record, len(picked), judge,
|
|
865
940
|
context, neg_coverage, big_domain, big_scores, pool_cfg)
|
|
@@ -915,6 +990,16 @@ class MdCGOS(MdCG):
|
|
|
915
990
|
return []
|
|
916
991
|
ctx = context if isinstance(context, dict) else {}
|
|
917
992
|
b = routing.bucket_dir(routing.route_key(ctx, ctx.get("tags")))
|
|
993
|
+
# 「无域信号」等价于「没声明条件」:条件桶路此时必须**弃权**,不能把
|
|
994
|
+
# route_key 回落的 orphan 当成一个真桶去硬过滤——否则凡是被标了 domain:
|
|
995
|
+
# (因而落在 cond_* 目录)的节点,对这条查询就整片不可见。
|
|
996
|
+
# 实测(带 context 但不含 domain / observation_position):弃权前只扫
|
|
997
|
+
# orphan 桶 10 个节点、目标节点召回丢失(T1_bucket_scan/scanned=10);
|
|
998
|
+
# 弃权后由词法/实体/图路正常召回(T2_global_like/scanned=75)。
|
|
999
|
+
# 与上面 context is None 的既有早返回同构,也贴合 test_p0 的
|
|
1000
|
+
# 「桶路由平均 recall@10 ≥ 0.9(加 context 不丢召回)」不变量。
|
|
1001
|
+
if b == routing.ORPHAN:
|
|
1002
|
+
return []
|
|
918
1003
|
inb = [e for e in entries if e.get("bucket") == b]
|
|
919
1004
|
if not inb:
|
|
920
1005
|
return []
|
|
@@ -939,7 +1024,7 @@ class MdCGOS(MdCG):
|
|
|
939
1024
|
"content": c, "path": e["path"]}, 1.0))
|
|
940
1025
|
return out
|
|
941
1026
|
|
|
942
|
-
# 生效条件:当 seeds 非空时,仅取前 5
|
|
1027
|
+
# 生效条件:当 seeds 非空时,仅取前 5 个种子,种子的 edges 取法为:其 id 在 entries 的 path 文件名映射(by_id)中时经 self._read(e_seed) 读 frontmatter(读缓存覆盖——get 裸 open 不在缓存包装面),不在映射中(fm.id≠文件名等形态)时回落 self.get(n["id"])(旧口径,冷路径);从 edges 取 target(dict 取 target,否则 str(edge)),若 target 在 entries 映射中且不在 seed_ids 中则读取并追加分数 s*0.5;seeds 为空返回 [];depth 默认 1 但本段未使用;
|
|
943
1028
|
def _path_graph(self, query, entries, seeds, depth=1):
|
|
944
1029
|
"""图扩展路径:从词法种子沿 edges 一跳扩展。"""
|
|
945
1030
|
if not seeds:
|
|
@@ -951,10 +1036,24 @@ class MdCGOS(MdCG):
|
|
|
951
1036
|
by_id[nid] = e
|
|
952
1037
|
out = []
|
|
953
1038
|
for n, s in seeds[:5]:
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1039
|
+
# 种子读改走 self._read(v9:92/v13 留档修复,缺陷迭代第 14 轮):
|
|
1040
|
+
# get 裸 open+nodefile.loads,不在 readcache.install 的包装面
|
|
1041
|
+
#(只包 cg._read)——search_rrf 默认 paths 含 graph,每查询固定
|
|
1042
|
+
# ≤5 次盘读(40 池实测 Q2 增量恰 5,graph 零产出也在边循环前
|
|
1043
|
+
# 发生);边目标(下方 self._read)早已同形。种子只要 frontmatter
|
|
1044
|
+
# 的 edges(content 不解封不用),种子来自 lexical(entries/
|
|
1045
|
+
# _candidates 派生,MdCGSecure._candidates 已过 _readable)——
|
|
1046
|
+
# 绕过 get 的读隔离门无损失,图路扩展产物本由
|
|
1047
|
+
# MdCGSecure.search_rrf 终态二次过滤兜底。by_id 不含(fm.id≠
|
|
1048
|
+
# 文件名等形态)回落 get 保旧口径。
|
|
1049
|
+
e_seed = by_id.get(n["id"])
|
|
1050
|
+
if e_seed is not None:
|
|
1051
|
+
fm_s, _c = self._read(e_seed)
|
|
1052
|
+
edges = (fm_s or {}).get("edges") or []
|
|
1053
|
+
else:
|
|
1054
|
+
node = self.get(n["id"])
|
|
1055
|
+
edges = ((node or {}).get("frontmatter") or {}).get("edges") or []
|
|
1056
|
+
for edge in edges:
|
|
958
1057
|
tid = edge.get("target") if isinstance(edge, dict) else str(edge)
|
|
959
1058
|
if tid in by_id and tid not in seed_ids:
|
|
960
1059
|
e = by_id[tid]
|
|
@@ -1210,6 +1309,10 @@ class MdCGOS(MdCG):
|
|
|
1210
1309
|
q = unify_query(q)
|
|
1211
1310
|
if not q:
|
|
1212
1311
|
return [], {"tier": None, "reason": "empty_query", "paths": {}}
|
|
1312
|
+
# P1b-2:读面代际感知——必须在热路径 query 缓存查询**之前**:旧索引
|
|
1313
|
+
# 代里缓存的命中结果对新写入不可见(静默错答),重载成功时
|
|
1314
|
+
# _maybe_reload_index 已顺带失效 hotcache/readcache。
|
|
1315
|
+
self._maybe_reload_index()
|
|
1213
1316
|
# 热路径:query 结果缓存命中即返回(不改 RRF 核心)
|
|
1214
1317
|
# 时间算子**显式启用时绕开缓存**:缓存键不含时间参数,命中会返回
|
|
1215
1318
|
# 未按本次窗口过滤的结果(静默错答比慢更贵)。
|
|
@@ -1228,6 +1331,9 @@ class MdCGOS(MdCG):
|
|
|
1228
1331
|
"judge": bool(judge), "judge_ranking": bool(judge_ranking),
|
|
1229
1332
|
"goal_text": goal_text, "context": context,
|
|
1230
1333
|
"early_stop_threshold": early_stop_threshold,
|
|
1334
|
+
# 进程级开关也改口径(v14 只补了参数面):MDCG_SEMANTIC /
|
|
1335
|
+
# MDCG_EN_ATOMS / MDCG_UNIFY_QUERY 等不入键即跨口径命中(2026-09-24 修复)。
|
|
1336
|
+
"env_switch": _hc.env_switch_key(),
|
|
1231
1337
|
}
|
|
1232
1338
|
# 不可稳定进键的参数(自定义可调用 query_expand):非默认即**绕行**
|
|
1233
1339
|
# 缓存(读+写双侧闭合)——fail-closed,宁可不用缓存也不串味。
|
|
@@ -1631,17 +1737,18 @@ class MdCGOS(MdCG):
|
|
|
1631
1737
|
"""
|
|
1632
1738
|
phash = _sig(content)
|
|
1633
1739
|
with FileLock(self.inbox_log, strict=True):
|
|
1740
|
+
# 增量对账(issue #32):原实现每条全量 read_jsonl(inbox) +
|
|
1741
|
+
# read_jsonl(decisions)(锁内 O(M+D)/条、批量 O(M²),旧格式行
|
|
1742
|
+
# 逐条重算 sha1);改走 stat 尾部增量索引——文件序与覆盖语义
|
|
1743
|
+
# 与原全量扫描逐位一致(同 phash 按文件序取,已裁决优先 break)。
|
|
1744
|
+
ph_idx = self._inbox_phash_index()
|
|
1634
1745
|
st = self._pid_status()
|
|
1635
1746
|
dup = None
|
|
1636
|
-
for
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
if rh != phash:
|
|
1640
|
-
continue
|
|
1641
|
-
s = st.get(r.get("pid")) or {}
|
|
1642
|
-
dup = {"pid": r.get("pid"),
|
|
1747
|
+
for pid, nid in ph_idx.get(phash, ()):
|
|
1748
|
+
s = st.get(pid) or {}
|
|
1749
|
+
dup = {"pid": pid,
|
|
1643
1750
|
"status": s.get("status") or "pending",
|
|
1644
|
-
"node_id":
|
|
1751
|
+
"node_id": nid}
|
|
1645
1752
|
if dup["status"] in ("accepted", "rejected"):
|
|
1646
1753
|
break # 已裁决的最有信息量,优先返回
|
|
1647
1754
|
if dup:
|
|
@@ -1675,14 +1782,135 @@ class MdCGOS(MdCG):
|
|
|
1675
1782
|
"dup_of": None, "dup_status": None}
|
|
1676
1783
|
return pid
|
|
1677
1784
|
|
|
1678
|
-
#
|
|
1785
|
+
# 生效条件:stat decisions_log 失败(OSError)时把 _pid_st 置空 dict、两扫描键置 -1 并返回空 dict;size 与 mtime_ns 均与缓存键一致时直接返回 _pid_st(零解析);缓存键为 -1(首装)或当前 size 小于已扫描偏移(外部非合作重写/回退)时经 read_jsonl_tail(path, 0) 全量重建 _pid_st,否则经 read_jsonl_tail 从已扫描偏移起增量并入,逐条把 r.get("pid") 为真值的记录写入 _pid_st(后并入覆盖先并入);水位存 read_jsonl_tail 返回的真实 EOF 偏移(end 为 None 时回落 stat size),mtime_ns 存读前 stat 值,返回 _pid_st。
|
|
1679
1786
|
def _pid_status(self):
|
|
1680
|
-
"""pid → 最新一条裁决记录(多轮再审批时取最后一轮)。
|
|
1681
|
-
|
|
1682
|
-
|
|
1787
|
+
"""pid → 最新一条裁决记录(多轮再审批时取最后一轮)。
|
|
1788
|
+
|
|
1789
|
+
增量缓存(issue #32):原实现每调用全量 read_jsonl(decisions_log)
|
|
1790
|
+
(json.loads 逐条),propose 对账每条 O(D)、批量 O(M×D)。append-only
|
|
1791
|
+
契约下改为 stat 尾部对账:size/mtime 未变零解析;size 增长只解析
|
|
1792
|
+
新增字节;size 回退/首装全量重建兜底。mtime 单变 size 不变(touch
|
|
1793
|
+
类)走增量窗口=0 行,结果不变。跨进程 append 由 stat 自动并入。
|
|
1794
|
+
水位口径(竞态修复):存 read_jsonl_tail 返回的**真实 EOF 偏移**,
|
|
1795
|
+
不存读前 stat 的 size——stat→read 窗口内的并发 append 本轮已并入,
|
|
1796
|
+
水位偏小会让下一轮重读同一段(本缓存是 dict 后写覆盖不受损,但与
|
|
1797
|
+
列表型缓存共用同一口径)。
|
|
1798
|
+
边界(与检索读缓存同款诚实声明):外部**非 append 形态**重写文件
|
|
1799
|
+
(同 size 改内容)不在合作写者协议内,不保证可见。
|
|
1800
|
+
"""
|
|
1801
|
+
try:
|
|
1802
|
+
sti = os.stat(self.decisions_log)
|
|
1803
|
+
size, mtime = sti.st_size, sti.st_mtime_ns
|
|
1804
|
+
except OSError:
|
|
1805
|
+
self._pid_st, self._pid_st_size, self._pid_st_mtime = {}, -1, -1
|
|
1806
|
+
return self._pid_st
|
|
1807
|
+
if size == self._pid_st_size and mtime == self._pid_st_mtime:
|
|
1808
|
+
return self._pid_st # 无新内容(零解析)
|
|
1809
|
+
full = self._pid_st_size < 0 or size < self._pid_st_size
|
|
1810
|
+
if full:
|
|
1811
|
+
self._pid_st = {}
|
|
1812
|
+
src, end = read_jsonl_tail(self.decisions_log,
|
|
1813
|
+
0 if full else self._pid_st_size)
|
|
1814
|
+
for r in src:
|
|
1683
1815
|
if r.get("pid"):
|
|
1684
|
-
|
|
1685
|
-
|
|
1816
|
+
self._pid_st[r["pid"]] = r
|
|
1817
|
+
self._pid_st_size = size if end is None else end
|
|
1818
|
+
self._pid_st_mtime = mtime
|
|
1819
|
+
return self._pid_st
|
|
1820
|
+
|
|
1821
|
+
# 生效条件:stat inbox_log 失败(OSError)时把 _inbox_idx 置空 dict、两扫描键置 -1 并返回空 dict;size 与 mtime_ns 均与缓存键一致时直接返回 _inbox_idx(零解析);缓存键为 -1(首装)或当前 size 小于已扫描偏移时经 read_jsonl_tail(path, 0) 全量重建,否则经 read_jsonl_tail 增量并入,逐条以 r.get("payload_hash") 或(缺键时)_sig(r.get("content") or "") 为 ph 把 (r.get("pid"), r.get("id")) 追加进 _inbox_idx[ph](文件序);水位存 read_jsonl_tail 返回的真实 EOF 偏移(end 为 None 时回落 stat size),mtime_ns 存读前 stat 值,返回 _inbox_idx。
|
|
1822
|
+
def _inbox_phash_index(self):
|
|
1823
|
+
"""inbox 的 phash → [(pid, node_id)](文件序)增量索引(propose 对账用)。
|
|
1824
|
+
|
|
1825
|
+
与 _pid_status 同款 stat 尾部对账(issue #32)。存量旧格式行
|
|
1826
|
+
(无 payload_hash)在**并入时**现算一次 _sig(此后常驻,不再
|
|
1827
|
+
每条 propose 重算——旧路径每条重算 M 次 sha1)。
|
|
1828
|
+
水位口径(竞态修复,同 _pid_status):存真实 EOF 偏移,不存读前
|
|
1829
|
+
stat 的 size——窗口内并发 append 不得在下轮重复并入。
|
|
1830
|
+
"""
|
|
1831
|
+
try:
|
|
1832
|
+
sti = os.stat(self.inbox_log)
|
|
1833
|
+
size, mtime = sti.st_size, sti.st_mtime_ns
|
|
1834
|
+
except OSError:
|
|
1835
|
+
self._inbox_idx, self._inbox_idx_size, self._inbox_idx_mtime = \
|
|
1836
|
+
{}, -1, -1
|
|
1837
|
+
return self._inbox_idx
|
|
1838
|
+
if size == self._inbox_idx_size and mtime == self._inbox_idx_mtime:
|
|
1839
|
+
return self._inbox_idx # 无新内容(零解析)
|
|
1840
|
+
full = self._inbox_idx_size < 0 or size < self._inbox_idx_size
|
|
1841
|
+
if full:
|
|
1842
|
+
self._inbox_idx = {}
|
|
1843
|
+
src, end = read_jsonl_tail(self.inbox_log,
|
|
1844
|
+
0 if full else self._inbox_idx_size)
|
|
1845
|
+
for r in src:
|
|
1846
|
+
ph = r.get("payload_hash") or _sig(r.get("content") or "")
|
|
1847
|
+
self._inbox_idx.setdefault(ph, []).append(
|
|
1848
|
+
(r.get("pid"), r.get("id")))
|
|
1849
|
+
self._inbox_idx_size = size if end is None else end
|
|
1850
|
+
self._inbox_idx_mtime = mtime
|
|
1851
|
+
return self._inbox_idx
|
|
1852
|
+
|
|
1853
|
+
# 生效条件:stat inbox_log 失败(OSError)时把 _inbox_recs 置空列表、两扫描键置 -1 并返回它;size 与 mtime_ns 均与缓存键一致时直接返回 _inbox_recs(零解析);缓存键为 -1(首装)或当前 size 小于已扫描偏移时以 read_jsonl_tail(path, 0) 全量重建,否则以 read_jsonl_tail(offset) 结果 extend 增量并入;水位存 read_jsonl_tail 返回的真实 EOF 偏移(end 为 None 时回落 stat size),mtime_ns 存读前 stat 值,返回 _inbox_recs(内部缓存,调用方只读不得变更)。
|
|
1854
|
+
def _inbox_records(self):
|
|
1855
|
+
"""inbox 全记录(文件序)增量缓存——review 治理面单次调用共用一次解析。
|
|
1856
|
+
|
|
1857
|
+
与 _pid_status 同款 stat 尾部对账(issue #35):原 review_decide 线性
|
|
1858
|
+
扫 inbox 找 pid、_cascade_dedup 再全量读一遍(2 遍/调用);改走本
|
|
1859
|
+
缓存后稳态零解析,调用方在内存列表上线性扫(万条 ~1ms 级,无 IO)。
|
|
1860
|
+
水位口径(竞态修复):存 read_jsonl_tail 返回的**真实 EOF 偏移**,
|
|
1861
|
+
不存读前 stat 的 size——stat→read 窗口内的并发 append 本轮已并入,
|
|
1862
|
+
水位偏小会让下一轮 extend 重读同一条(review_list 重复 pid、
|
|
1863
|
+
_cascade_dedup 连写两条 reject),且稳态 size 只增、永不自愈。
|
|
1864
|
+
"""
|
|
1865
|
+
try:
|
|
1866
|
+
sti = os.stat(self.inbox_log)
|
|
1867
|
+
size, mtime = sti.st_size, sti.st_mtime_ns
|
|
1868
|
+
except OSError:
|
|
1869
|
+
self._inbox_recs, self._inbox_recs_size, self._inbox_recs_mtime = \
|
|
1870
|
+
[], -1, -1
|
|
1871
|
+
return self._inbox_recs
|
|
1872
|
+
if size == self._inbox_recs_size and mtime == self._inbox_recs_mtime:
|
|
1873
|
+
return self._inbox_recs # 无新内容(零解析)
|
|
1874
|
+
full = self._inbox_recs_size < 0 or size < self._inbox_recs_size
|
|
1875
|
+
if full:
|
|
1876
|
+
self._inbox_recs = []
|
|
1877
|
+
src, end = read_jsonl_tail(self.inbox_log,
|
|
1878
|
+
0 if full else self._inbox_recs_size)
|
|
1879
|
+
self._inbox_recs.extend(src)
|
|
1880
|
+
self._inbox_recs_size = size if end is None else end
|
|
1881
|
+
self._inbox_recs_mtime = mtime
|
|
1882
|
+
return self._inbox_recs
|
|
1883
|
+
|
|
1884
|
+
# 生效条件:stat decisions_log 失败(OSError)时把 _dec_recs 置空列表、两扫描键置 -1 并返回它;size 与 mtime_ns 均与缓存键一致时直接返回 _dec_recs(零解析);缓存键为 -1(首装)或当前 size 小于已扫描偏移时以 read_jsonl_tail(path, 0) 全量重建,否则以 read_jsonl_tail(offset) 结果 extend 增量并入;水位存 read_jsonl_tail 返回的真实 EOF 偏移(end 为 None 时回落 stat size),mtime_ns 存读前 stat 值,返回 _dec_recs(内部缓存,调用方只读不得变更)。
|
|
1885
|
+
def _decisions_records(self):
|
|
1886
|
+
"""decisions 全记录(文件序)增量缓存——review 治理面单次调用共用一次解析。
|
|
1887
|
+
|
|
1888
|
+
issue #35:原 review_stats 全量读 decisions 统计、verify_review_record
|
|
1889
|
+
全量线性扫找单条 pid(每调用 1 遍整文件 json.loads);改走本缓存后
|
|
1890
|
+
稳态零解析,调用方内存线性扫。与 _pid_status(pid→最新)互补:
|
|
1891
|
+
本缓存保全量记录(多轮裁决历史、by_decision 统计口径)。
|
|
1892
|
+
水位口径(竞态修复,同 _inbox_records):存真实 EOF 偏移——窗口内
|
|
1893
|
+
并发 append 不得在下一轮重复 extend(review_stats 计数虚高/翻倍、
|
|
1894
|
+
永不自愈)。
|
|
1895
|
+
"""
|
|
1896
|
+
try:
|
|
1897
|
+
sti = os.stat(self.decisions_log)
|
|
1898
|
+
size, mtime = sti.st_size, sti.st_mtime_ns
|
|
1899
|
+
except OSError:
|
|
1900
|
+
self._dec_recs, self._dec_recs_size, self._dec_recs_mtime = \
|
|
1901
|
+
[], -1, -1
|
|
1902
|
+
return self._dec_recs
|
|
1903
|
+
if size == self._dec_recs_size and mtime == self._dec_recs_mtime:
|
|
1904
|
+
return self._dec_recs # 无新内容(零解析)
|
|
1905
|
+
full = self._dec_recs_size < 0 or size < self._dec_recs_size
|
|
1906
|
+
if full:
|
|
1907
|
+
self._dec_recs = []
|
|
1908
|
+
src, end = read_jsonl_tail(self.decisions_log,
|
|
1909
|
+
0 if full else self._dec_recs_size)
|
|
1910
|
+
self._dec_recs.extend(src)
|
|
1911
|
+
self._dec_recs_size = size if end is None else end
|
|
1912
|
+
self._dec_recs_mtime = mtime
|
|
1913
|
+
return self._dec_recs
|
|
1686
1914
|
|
|
1687
1915
|
# 生效条件:实例已构建(内部先读 _pid_status());返回其 status ∈ TERMINAL_DECISION_STATUS(accepted/rejected/noop)的 pid 集合,status=="needs_reapproval" 视为未关闭、不计入;
|
|
1688
1916
|
def _closed_pids(self):
|
|
@@ -1695,7 +1923,9 @@ class MdCGOS(MdCG):
|
|
|
1695
1923
|
"""待审核候选(含被红队打回、待再审批的条目)。"""
|
|
1696
1924
|
st = self._pid_status()
|
|
1697
1925
|
out = []
|
|
1698
|
-
|
|
1926
|
+
# issue #35:走全记录增量缓存(稳态零解析)——review_stats 链路单次
|
|
1927
|
+
# 调用共用一次解析;rec=dict(r) 复制,不改内部缓存。
|
|
1928
|
+
for r in self._inbox_records():
|
|
1699
1929
|
s = st.get(r.get("pid")) or {}
|
|
1700
1930
|
if s.get("status") in TERMINAL_DECISION_STATUS:
|
|
1701
1931
|
continue
|
|
@@ -1716,7 +1946,9 @@ class MdCGOS(MdCG):
|
|
|
1716
1946
|
视图复用 review_list/_closed_pids,避免在此复制第二份终态判定逻辑。
|
|
1717
1947
|
"""
|
|
1718
1948
|
by = {}
|
|
1719
|
-
|
|
1949
|
+
# issue #35:走全记录增量缓存(稳态零解析)——原每次全量逐行
|
|
1950
|
+
# json.loads,与 review_list/_closed_pids 链路重复解析同一文件。
|
|
1951
|
+
for r in self._decisions_records():
|
|
1720
1952
|
d = str(r.get("decision") or "").strip() or "unknown"
|
|
1721
1953
|
by[d] = by.get(d, 0) + 1
|
|
1722
1954
|
return {"records": sum(by.values()), "by_decision": by,
|
|
@@ -1845,7 +2077,9 @@ class MdCGOS(MdCG):
|
|
|
1845
2077
|
closed = []
|
|
1846
2078
|
with FileLock(self.decisions_log, strict=True):
|
|
1847
2079
|
st = self._pid_status()
|
|
1848
|
-
|
|
2080
|
+
# issue #35:走全记录增量缓存(稳态零解析)——原与 review_decide
|
|
2081
|
+
# 开头的找 pid 扫描重复全量读同一 inbox(2 遍/调用)。
|
|
2082
|
+
for r in self._inbox_records():
|
|
1849
2083
|
bpid = r.get("pid")
|
|
1850
2084
|
if not bpid or bpid == pid or bpid in st:
|
|
1851
2085
|
continue # 自己 / 已有裁决记录(含 needs_reapproval)跳过
|
|
@@ -1896,7 +2130,9 @@ class MdCGOS(MdCG):
|
|
|
1896
2130
|
return {"ok": False, "error": "node_not_found"}
|
|
1897
2131
|
fm = node.get("frontmatter") or {}
|
|
1898
2132
|
pid, rnd = fm.get("pid"), fm.get("round")
|
|
1899
|
-
|
|
2133
|
+
# issue #35:走全记录增量缓存后内存线性扫(稳态零解析)——原每次
|
|
2134
|
+
# 全量逐行 json.loads 整份 decisions 只为找一条 pid+round。
|
|
2135
|
+
src = next((r for r in self._decisions_records()
|
|
1900
2136
|
if r.get("pid") == pid
|
|
1901
2137
|
and int(r.get("round") or 0) == int(rnd or 0)), None)
|
|
1902
2138
|
if not src:
|
|
@@ -1911,10 +2147,11 @@ class MdCGOS(MdCG):
|
|
|
1911
2147
|
"verify_hash": src.get("verify_hash"),
|
|
1912
2148
|
"source": "hippocampus/decisions.jsonl"}
|
|
1913
2149
|
|
|
1914
|
-
# 生效条件:decision 须为 DECISION_ACTIONS("accept"/"reject"/"edit"/"merge"/"noop")之一(否则 raise ValueError),inbox_log 中须有 pid 匹配记录(否则 {'ok': False, 'error': 'pid_not_found'}),且 pid 不在 self._closed_pids() 中(否则 'already_decided');edits 为真值且含 "verify"、或 redteam 为真值且含 "verify" 时返回 'verify_readonly';last_status=="needs_reapproval" 时须 redteam.verdict 归一化为 "pass" 且 round_no>last_round(否则 'reapproval_required' / 'round_not_advanced');rt_v=="reject" 或(decision=="reject" 且 rt_issues 非空)时记 needs_reapproval 不落节点;decision=="accept" 且 rt_v 为空且 _redteam_required() 为真时返回 'redteam_required';其余 accept/edit 按 item(edit 时用 edits.get 覆盖 content/tags/layer)add+flush 落节点,merge 须 merge_into 或 item.extra.merge_into 指向的节点存在(否则 'merge_target_not_found'
|
|
2150
|
+
# 生效条件:decision 须为 DECISION_ACTIONS("accept"/"reject"/"edit"/"merge"/"noop")之一(否则 raise ValueError),inbox_log 中须有 pid 匹配记录(否则 {'ok': False, 'error': 'pid_not_found'}),且 pid 不在 self._closed_pids() 中(否则 'already_decided');edits 为真值且含 "verify"、或 redteam 为真值且含 "verify" 时返回 'verify_readonly';last_status=="needs_reapproval" 时须 redteam.verdict 归一化为 "pass" 且 round_no>last_round(否则 'reapproval_required' / 'round_not_advanced');rt_v=="reject" 或(decision=="reject" 且 rt_issues 非空)时记 needs_reapproval 不落节点;decision=="accept" 且 rt_v 为空且 _redteam_required() 为真时返回 'redteam_required';其余 accept/edit 按 item(edit 时用 edits.get 覆盖 content/tags/layer)add+flush 落节点,merge 须 merge_into 或 item.extra.merge_into 指向的节点存在(否则 'merge_target_not_found'),且写入前经与 add 同款双闸(N131,2026-09-25:principal 在位先 require_layer_write(目标层, 目标敏感度)——与 MdCGSecure.add 同序同错型;再 protect.guard_write(override=override)——self/anchor 层、immutable 标记,拒绝抛 ProtectionError/AccessDenied 且提案留 pending 不落库)后追加内容并定向索引 upsert(_node_entry 同源条目写入 index/_dirty)+flush,reject 与 noop 只记裁决(status 分别为 "rejected"/"noop");最后统一 _record_decision + _cascade_dedup + flush 后返回 result。
|
|
1915
2151
|
def review_decide(self, pid: str, decision: str, edits: dict = None,
|
|
1916
2152
|
merge_into: str = None, reason: str = "",
|
|
1917
|
-
redteam: dict = None, issues=None
|
|
2153
|
+
redteam: dict = None, issues=None,
|
|
2154
|
+
override: bool = False):
|
|
1918
2155
|
"""审核裁决:accept / reject / edit / merge / noop。
|
|
1919
2156
|
|
|
1920
2157
|
accept → 按 inbox 原样写入
|
|
@@ -1936,7 +2173,9 @@ class MdCGOS(MdCG):
|
|
|
1936
2173
|
"""
|
|
1937
2174
|
if decision not in DECISION_ACTIONS:
|
|
1938
2175
|
raise ValueError(f"未知裁决:{decision}")
|
|
1939
|
-
|
|
2176
|
+
# issue #35:走全记录增量缓存后内存线性扫(稳态零解析)——原每次
|
|
2177
|
+
# 全量逐行 json.loads 整份 inbox 只为找一条 pid。
|
|
2178
|
+
item = next((r for r in self._inbox_records()
|
|
1940
2179
|
if r.get("pid") == pid), None)
|
|
1941
2180
|
if not item:
|
|
1942
2181
|
return {"ok": False, "error": "pid_not_found"}
|
|
@@ -2003,13 +2242,24 @@ class MdCGOS(MdCG):
|
|
|
2003
2242
|
if item.get("verify"):
|
|
2004
2243
|
extra["verify"] = item["verify"]
|
|
2005
2244
|
extra["verify_hash"] = expect
|
|
2245
|
+
# 归因保留(P1,2026-09-26,DSH 端在役实测:5 个裁决节点 writer 全被
|
|
2246
|
+
# designer-cli 覆盖、会话 id 各自随机):提案记录的 actor 是 propose
|
|
2247
|
+
# 时库端快照的原始写入者身份(见 propose rec 的 "actor"/"session" 字段,
|
|
2248
|
+
# 非裁决期客户端输入,不可伪造面)。accept/edit 落盘若不显式带上,
|
|
2249
|
+
# _attribution 的 setdefault 会把 writer 缺省成裁决者——原始写入者
|
|
2250
|
+
# 就此不可追溯。writer 保留原值(提案 extra 里已带 writer 的存量提案
|
|
2251
|
+
# 不覆盖,向后兼容);裁决者身份不丢失,改记 reviewer 字段。
|
|
2252
|
+
if item.get("actor"):
|
|
2253
|
+
extra.setdefault("writer", item["actor"])
|
|
2254
|
+
extra.setdefault("reviewer", self.actor)
|
|
2006
2255
|
nid = self.add(item["id"], content, layer=layer, tags=tags,
|
|
2007
2256
|
condition_space=item.get("condition_space"), **extra)
|
|
2008
2257
|
# 索引增量收尾(2026-09-16 取证):add 只把条目放进本进程内存 _dirty,
|
|
2009
2258
|
# 未达 autoflush(64) 阈值时进程退出即永久丢失——裁决进程(review_cli /
|
|
2010
2259
|
# MCP op=review)通常只写 1~2 条,不 flush 则「节点在盘上但索引无条目」,
|
|
2011
2260
|
# 其他进程与重载后的长驻进程都检索不到,只能靠某次全量 rebuild 偶然救回。
|
|
2012
|
-
# merge
|
|
2261
|
+
# merge 分支同样直写节点文件,其收尾已改为同款定向 upsert+flush
|
|
2262
|
+
# (issue #34,见 else 分支注释)。
|
|
2013
2263
|
self.flush()
|
|
2014
2264
|
result.update(ok=True, node_id=nid)
|
|
2015
2265
|
else: # merge
|
|
@@ -2017,6 +2267,22 @@ class MdCGOS(MdCG):
|
|
|
2017
2267
|
tgt = self.get(target) if target else None
|
|
2018
2268
|
if not tgt:
|
|
2019
2269
|
return {"ok": False, "error": "merge_target_not_found"}
|
|
2270
|
+
# 写保护(N131,2026-09-25):merge = 对目标节点的一次**覆写**
|
|
2271
|
+
# (_write_node 追加正文),与 accept/edit 的 add 落点同受写保护——
|
|
2272
|
+
# 此前直写绕过双闸,orchestr forbidden「anchor/self/goals/knowledge
|
|
2273
|
+
# 层」被打穿。与 add 同序同错型:principal 层写闸在先(对照
|
|
2274
|
+
# MdCGSecure.add),引擎级 guard_write 在后(对照基类 add :1311);
|
|
2275
|
+
# 闸必须在 _record_decision 之前抛出——审计节点写入本就在其
|
|
2276
|
+
# try/except 内,闸放那里会被吞成 record_error 形同虚设。
|
|
2277
|
+
_p = getattr(self, "principal", None)
|
|
2278
|
+
if _p is not None and hasattr(_p, "require_layer_write"):
|
|
2279
|
+
_e = (self.index.get("nodes") or {}).get(target) or {}
|
|
2280
|
+
_p.require_layer_write(
|
|
2281
|
+
_e.get("layer") or tgt["frontmatter"].get("layer")
|
|
2282
|
+
or tgt["path"].split("/")[0],
|
|
2283
|
+
_e.get("sensitivity") or DEFAULT_SENSITIVITY)
|
|
2284
|
+
protect.guard_write(self, target, override=override,
|
|
2285
|
+
actor=getattr(self, "actor", None))
|
|
2020
2286
|
fm = dict(tgt["frontmatter"])
|
|
2021
2287
|
merged_content = (tgt["content"].rstrip() + "\n\n" +
|
|
2022
2288
|
item["content"].strip() + "\n")
|
|
@@ -2028,7 +2294,24 @@ class MdCGOS(MdCG):
|
|
|
2028
2294
|
fm["non_applicable_conditions"] = neg
|
|
2029
2295
|
self._write_node(target, os.path.join(self.root, tgt["path"]),
|
|
2030
2296
|
fm, merged_content)
|
|
2031
|
-
|
|
2297
|
+
# 定向索引 upsert(issue #34):merge 只改一个目标节点,原
|
|
2298
|
+
# rebuild_index 收尾是 _scan_nodes 全库 O(N) 逐文件扫描
|
|
2299
|
+
# (1500 池实测 ~97-247ms/次),与 accept/edit 的 add+flush
|
|
2300
|
+
# O(1) 增量收尾不对称。改为:单文件条目重算(_node_entry 与
|
|
2301
|
+
# _scan_nodes 同源唯一真源——字段集零漂移)+ 内存索引/增量
|
|
2302
|
+
# 日志定向写入 + flush。不走 _stage:目标已在索引,_stage 会
|
|
2303
|
+
# 虚增 bucket 计数;merge 不改 tags/目录,bucket 不变。
|
|
2304
|
+
# _dirty 置数同时推进 readcache 代际(写盘后同实例不陈旧)。
|
|
2305
|
+
try:
|
|
2306
|
+
tp = os.path.join(self.root, tgt["path"])
|
|
2307
|
+
with open(tp, encoding="utf-8") as f:
|
|
2308
|
+
fm2, c2 = nodefile.loads(f.read())
|
|
2309
|
+
e2 = self._node_entry(tp, tgt["path"].split("/")[0], fm2, c2)
|
|
2310
|
+
self.index["nodes"][target] = e2
|
|
2311
|
+
self._dirty[target] = e2
|
|
2312
|
+
except OSError:
|
|
2313
|
+
pass # 单文件重读失败(极罕见):条目留旧值,下次 rebuild 对账
|
|
2314
|
+
self.flush()
|
|
2032
2315
|
result.update(ok=True, node_id=target)
|
|
2033
2316
|
|
|
2034
2317
|
# status 映射统一出口:reject→"rejected"、noop→"noop"(同为终态,见
|
|
@@ -2074,7 +2357,7 @@ class MdCGOS(MdCG):
|
|
|
2074
2357
|
h = _sig(content or "", 16)
|
|
2075
2358
|
dst = os.path.join(self.trash_dir, f"{node_id}.md")
|
|
2076
2359
|
try:
|
|
2077
|
-
|
|
2360
|
+
publish(src, dst)
|
|
2078
2361
|
except OSError as exc:
|
|
2079
2362
|
return {"ok": False, "error": str(exc)}
|
|
2080
2363
|
append_jsonl(self.deletions_log, {"t": time.time(), "id": node_id,
|
|
@@ -2098,7 +2381,7 @@ class MdCGOS(MdCG):
|
|
|
2098
2381
|
def is_tombstoned(self, node_id: str):
|
|
2099
2382
|
return any(r.get("id") == node_id for r in read_jsonl(self.deletions_log))
|
|
2100
2383
|
|
|
2101
|
-
# 生效条件:当 node_id 传入时,若 deletions_log 中存在该 id 且 force=False 返回 tombstoned 拒绝;否则检查 trash_dir/{node_id}.md,os.path.exists 为 False 返回 not_in_trash
|
|
2384
|
+
# 生效条件:当 node_id 传入时,若 deletions_log 中存在该 id 且 force=False 返回 tombstoned 拒绝;否则检查 trash_dir/{node_id}.md,os.path.exists 为 False 返回 not_in_trash;可打开则读取并以 add(override=True) + trash frontmatter 元数据全量透传(显式形参逐项传,其余键经 **extra 回写;lifecycle_state 仅在 active→该态合法迁移时透传)恢复、移除 trash 源、audit,返回 ok True/id/forced=bool(force);
|
|
2102
2385
|
def restore(self, node_id: str, force: bool = False):
|
|
2103
2386
|
"""恢复:若在删除清单中且未 force → 拒绝(恢复时删除检查)。"""
|
|
2104
2387
|
tomb = [r for r in read_jsonl(self.deletions_log) if r.get("id") == node_id]
|
|
@@ -2111,13 +2394,52 @@ class MdCGOS(MdCG):
|
|
|
2111
2394
|
with open(src, encoding="utf-8") as f:
|
|
2112
2395
|
fm, content = nodefile.loads(f.read())
|
|
2113
2396
|
layer = fm.get("layer", "knowledge")
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2397
|
+
# 恢复 = 原样放回:add 是**全量重建 fm**,只传个位数字段会把 edges/
|
|
2398
|
+
# depends_on/验证态/生命周期/created_at/语义摘要/证据计数等元数据永久
|
|
2399
|
+
# 抹掉(下方 os.remove 删 trash 源后不可再恢复)。故 add 显式形参对应
|
|
2400
|
+
# 的键逐项透传;其余键(created_at/evidence_*/protected/temporal/
|
|
2401
|
+
# derived_relation…)经 **extra 回写(fm.update 通道覆盖默认值)。
|
|
2402
|
+
# _skip 必须覆盖 add 全部显式形参名——**extra 撞形参名直接 TypeError。
|
|
2403
|
+
_skip = ("id", "layer", "tags", "condition_space", "importance",
|
|
2404
|
+
"confidence", "verification_basis", "edges",
|
|
2405
|
+
"non_applicable_conditions", "derived_from", "relation",
|
|
2406
|
+
"semantic", "depends_on", "valid_from", "valid_until",
|
|
2407
|
+
"effective_from", "effective_until", "believed_at",
|
|
2408
|
+
"verification_state", "override", "consistency",
|
|
2409
|
+
"on_conflict", "state", "sensitivity", "derived_relation",
|
|
2410
|
+
"content", "node_id")
|
|
2411
|
+
_extra = {k: v for k, v in fm.items()
|
|
2412
|
+
if k not in _skip and v not in (None, "", [], {})}
|
|
2413
|
+
# 生命周期状态走迁移裁决:restore 时节点已摘索引、按新节点(active)
|
|
2414
|
+
# 起判,active→archived 跳级会被拒——不可迁移时不透传(落 active),
|
|
2415
|
+
# 不让罕见 corner case 阻断恢复主流程。
|
|
2416
|
+
from . import lifecycle as _lc
|
|
2417
|
+
from . import provenance as _prov
|
|
2418
|
+
_ls = fm.get(_lc.STATE_FIELD)
|
|
2419
|
+
if _ls and _lc.can_transition("active", _ls):
|
|
2420
|
+
_extra[_lc.STATE_FIELD] = _ls
|
|
2421
|
+
# 验证态同样按新节点(unverified)起判:非法值透传会被 require_transition
|
|
2422
|
+
# 直接拒掉,过滤为 None(落 unverified)保证恢复不被脏数据卡死。
|
|
2423
|
+
_vs = fm.get("verification_state")
|
|
2424
|
+
self.add(node_id, content, layer=layer, tags=fm.get("tags") or [],
|
|
2425
|
+
override=True, sensitivity=fm.get("sensitivity"),
|
|
2117
2426
|
condition_space=fm.get("condition_space"),
|
|
2118
2427
|
importance=fm.get("importance", 0.5),
|
|
2119
2428
|
confidence=fm.get("confidence", 0.6),
|
|
2120
|
-
verification_basis=fm.get("verification_basis")
|
|
2429
|
+
verification_basis=fm.get("verification_basis"),
|
|
2430
|
+
edges=fm.get("edges"),
|
|
2431
|
+
non_applicable_conditions=fm.get("non_applicable_conditions"),
|
|
2432
|
+
semantic=fm.get("semantic"),
|
|
2433
|
+
depends_on=fm.get("depends_on"),
|
|
2434
|
+
valid_from=fm.get("valid_from"),
|
|
2435
|
+
valid_until=fm.get("valid_until"),
|
|
2436
|
+
effective_from=fm.get("effective_from"),
|
|
2437
|
+
effective_until=fm.get("effective_until"),
|
|
2438
|
+
believed_at=fm.get("believed_at"),
|
|
2439
|
+
verification_state=(_vs if _vs in trust.STATES else None),
|
|
2440
|
+
derived_from=fm.get("derived_from"),
|
|
2441
|
+
relation=fm.get("derived_relation") or _prov.DEFAULT_RELATION,
|
|
2442
|
+
**_extra)
|
|
2121
2443
|
os.remove(src)
|
|
2122
2444
|
self._audit("restore", node_id, forced=bool(force))
|
|
2123
2445
|
return {"ok": True, "id": node_id, "forced": bool(force)}
|
|
@@ -2365,7 +2687,7 @@ class MdCGOS(MdCG):
|
|
|
2365
2687
|
# 召回热路径,只在显式调用时工作。
|
|
2366
2688
|
|
|
2367
2689
|
MAINTAIN_ACTIONS = ("stat", "history", "importance", "longterm",
|
|
2368
|
-
"prefeed", "separate", "rollback",
|
|
2690
|
+
"prefeed", "separate", "rollback", "reload",
|
|
2369
2691
|
"backfill", "backfill_rollback", "backfill_history",
|
|
2370
2692
|
"cap", "cap_rollback", "cap_history",
|
|
2371
2693
|
"exempt", "exempt_rollback", "exempt_history",
|
|
@@ -2410,7 +2732,7 @@ class MdCGOS(MdCG):
|
|
|
2410
2732
|
"discard": "已丢弃(不写)", "defer": "留待复核(不写不并)"}.get(dec, "")
|
|
2411
2733
|
return out
|
|
2412
2734
|
|
|
2413
|
-
# 生效条件:act 取 str(action or "stat") 去空白并 lower 后按分支分派(importance / longterm / prefeed / separate / stat
|
|
2735
|
+
# 生效条件:act 取 str(action or "stat") 去空白并 lower 后按分支分派(reload / importance / longterm / prefeed / separate / stat 等,reload 只触发 _maybe_reload_index 并返回 {"reloaded": bool});未识别 act 走 stat 兜底;只读与写层 action 库层不鉴权,apply 类批量改写由 MCP 分发层 require_admin 把守;
|
|
2414
2736
|
def maintain(self, action="stat", layer=None, limit=None, apply=False,
|
|
2415
2737
|
min_delta=None, max_rows=None, force=False, keep=None,
|
|
2416
2738
|
mode=None, snapshot_id=None, batch=None, entry_ids=None,
|
|
@@ -2421,8 +2743,15 @@ class MdCGOS(MdCG):
|
|
|
2421
2743
|
|
|
2422
2744
|
只读 action(stat/history/longterm 预演)与写层 action(prefeed)不受
|
|
2423
2745
|
管理权限约束;apply 类批量改写由 MCP 分发层 `require_admin` 把守。
|
|
2746
|
+
|
|
2747
|
+
reload(P1b-2,DSH 建议 2):显式触发 _maybe_reload_index(签名变化
|
|
2748
|
+
才真重载),返回 {"reloaded": bool} 供外部探活常驻进程的读面代际。
|
|
2424
2749
|
"""
|
|
2425
2750
|
act = str(action or "stat").strip().lower()
|
|
2751
|
+
if act == "reload":
|
|
2752
|
+
# 探活/显式刷新:无副作用面(签名未变时是零开销 stat + False)。
|
|
2753
|
+
return {"ok": True, "op": "maintain", "action": "reload",
|
|
2754
|
+
"reloaded": bool(self._maybe_reload_index())}
|
|
2426
2755
|
if act == "importance":
|
|
2427
2756
|
return weights.recalc(self, layer=layer, limit=limit, apply=apply,
|
|
2428
2757
|
min_delta=(weights.APPLY_DELTA if min_delta is None
|
|
@@ -3529,10 +3858,10 @@ class MdCGSecure(MdCGOS):
|
|
|
3529
3858
|
self._index_sensitivity(nid, sens)
|
|
3530
3859
|
return nid
|
|
3531
3860
|
|
|
3532
|
-
# 生效条件:sens 取 sensitivity or DEFAULT_SENSITIVITY,先 principal.require_layer_write(kw.get("layer") or "contextual"
|
|
3861
|
+
# 生效条件:sens 取 sensitivity or DEFAULT_SENSITIVITY,先 principal.require_layer_write(kw.get("layer", "knowledge"), sens)(缺省与基类 MdCGOS.propose 的 layer="knowledge" 一致——此前误用 or "contextual" 导致校验层与落盘层错位:knowledge-only 身份被误拒 / contextual-only 身份入队成死提案,2026-09-25 缺陷 #5),再转 super().propose(node_id, content, sensitivity=sens) 并返回其结果。
|
|
3533
3862
|
def propose(self, node_id: str, content: str, sensitivity: str = None, **kw):
|
|
3534
3863
|
sens = sensitivity or DEFAULT_SENSITIVITY
|
|
3535
|
-
self.principal.require_layer_write(kw.get("layer"
|
|
3864
|
+
self.principal.require_layer_write(kw.get("layer", "knowledge"), sens)
|
|
3536
3865
|
return super().propose(node_id, content, sensitivity=sens, **kw)
|
|
3537
3866
|
|
|
3538
3867
|
# 生效条件:sens 取 sensitivity or DEFAULT_SENSITIVITY,先 _rank(sens) 并 principal.require_layer_write("goals", sens),再转 super().add_goal,最后按 gid 调 _index_sensitivity 并返回 gid。
|
|
@@ -3549,6 +3878,16 @@ class MdCGSecure(MdCGOS):
|
|
|
3549
3878
|
self.principal.require_layer_write("goals", DEFAULT_SENSITIVITY)
|
|
3550
3879
|
return super().set_goal_status(node_id, status)
|
|
3551
3880
|
|
|
3881
|
+
# 生效条件:verdict(转 str 去空白 lower)为 "falsified" 且索引中存在 node_id 条目时,先取该条目 layer 与 sensitivity(缺省回落 DEFAULT_SENSITIVITY)调 principal.require_layer_write——falsified 删除 = 对原节点层的一次删除写(N130,2026-09-25:verify 角色 layers_allow 本就只含 rejected/contextual,不得改/删被验证内容所在层,tokens.py verify spec forbidden 显式列明),与 add/add_rejected 同一闸口;随后把 override 原样转 super().verify(受保护节点的 guard_forget 快照留痕在基类 falsified 分支内)。verdict 非 falsified 或索引无此节点时不加闸直接透传(not_found 语义由基类维持)。
|
|
3882
|
+
def verify(self, node_id: str, evidence: str, verdict: str,
|
|
3883
|
+
override: bool = False):
|
|
3884
|
+
if str(verdict or "").strip().lower() == "falsified":
|
|
3885
|
+
e = (self.index.get("nodes") or {}).get(node_id)
|
|
3886
|
+
if e is not None:
|
|
3887
|
+
self.principal.require_layer_write(
|
|
3888
|
+
e.get("layer"), e.get("sensitivity") or DEFAULT_SENSITIVITY)
|
|
3889
|
+
return super().verify(node_id, evidence, verdict, override=override)
|
|
3890
|
+
|
|
3552
3891
|
# 生效条件:sens 取 sensitivity or DEFAULT_SENSITIVITY,经 _rank(sens) 与 principal.require_write(sens) 后把 m 基于 meta 复制并 setdefault tenant/session、harness 与 unit 为真值时补入,再强制 m["sensitivity"]=sens,text 经 _seal_content("_recent", text, sens) 后连 tags=tags 一起转 super().remember_event(window 为 None 时不传该参,否则带上 window)。
|
|
3553
3892
|
def remember_event(self, role: str, text: str, tags=None, meta=None,
|
|
3554
3893
|
window=None, sensitivity: str = None):
|
|
@@ -3571,15 +3910,57 @@ class MdCGSecure(MdCGOS):
|
|
|
3571
3910
|
# ---------- 读:密级过滤 ----------
|
|
3572
3911
|
|
|
3573
3912
|
# 生效条件:e 的 sensitivity 为假值时先 _read 回填为 fm 的 sensitivity or DEFAULT_SENSITIVITY,再以 self.principal.allows(sens) 的布尔结果为准。
|
|
3574
|
-
def _readable(self, e) -> bool:
|
|
3913
|
+
def _readable(self, e, session=None) -> bool:
|
|
3914
|
+
"""读可见性单点判定:clearance × sensitivity(密级)∧ 会话绑定(归属)。
|
|
3915
|
+
|
|
3916
|
+
会话绑定(issue #35 设计定稿 2026-09-23,使用者拍板):
|
|
3917
|
+
· public / internal = 跨会话共享档——任何通过 clearance 的会话可见;
|
|
3918
|
+
· private / secret = 会话绑定档——仅归属会话(frontmatter.session)
|
|
3919
|
+
可见;can_admin(设计者,需整体判断)豁免。
|
|
3920
|
+
· 绑定档无归属(存量节点):fail-closed,仅设计者可见(宁可少读,
|
|
3921
|
+
不可越权——与下方 sensitivity 回填同款纪律)。
|
|
3922
|
+
· 查询侧 session 参数(含 "*")不参与绑定档判定——身份不可自报;
|
|
3923
|
+
共享档本就跨会话可见,故该参数在 Secure 层不再改变任何可见性。
|
|
3924
|
+
"""
|
|
3575
3925
|
sens = e.get("sensitivity")
|
|
3576
3926
|
if not sens:
|
|
3577
|
-
#
|
|
3578
|
-
#
|
|
3927
|
+
# 索引可能被「无密级上下文」的实例重建而丢掉该字段:按盘上真相回填。
|
|
3928
|
+
# V21-2(批次 34,外部报告):`_read` 失败(返回 None 头)时**不得**
|
|
3929
|
+
# 回落宽松默认——那会把受保护节点判成 internal 对平级可见
|
|
3930
|
+
# (fail-open)。密级未知 = 按最高档 secret 处理(fail-closed:
|
|
3931
|
+
# 宁可误禁,不可越权)。
|
|
3579
3932
|
fm, _c = self._read(e)
|
|
3580
|
-
|
|
3933
|
+
if fm is None:
|
|
3934
|
+
sens = "secret"
|
|
3935
|
+
else:
|
|
3936
|
+
sens = fm.get("sensitivity") or DEFAULT_SENSITIVITY
|
|
3581
3937
|
e["sensitivity"] = sens
|
|
3582
|
-
|
|
3938
|
+
# restricted = 错误处置标记(批次 28 分型,使用者裁定):不走纯密级
|
|
3939
|
+
# rank——密级底线 internal ∧ 错误处置链路角色(designer/orchestr/
|
|
3940
|
+
# verify,或 can_admin)。平级(record/worker)与下游(output/guest)
|
|
3941
|
+
# 限读,处置链路必读——故意的冲突设计在此消解。
|
|
3942
|
+
if sens == "restricted":
|
|
3943
|
+
from .security import can_read_restricted
|
|
3944
|
+
return (self.principal.allows("internal")
|
|
3945
|
+
and can_read_restricted(self.principal))
|
|
3946
|
+
if not self.principal.allows(sens):
|
|
3947
|
+
return False
|
|
3948
|
+
if _rank(sens) >= _rank("private"):
|
|
3949
|
+
if self.principal.can_admin:
|
|
3950
|
+
return True
|
|
3951
|
+
# 绑定档判定**恒用 principal.session(服务端身份,不可伪造)**——
|
|
3952
|
+
# 查询侧 session 参数(含 "*")不构成豁免:调用方自报的会话不能
|
|
3953
|
+
# 越过身份看别人的 private(与 _attribution「env 固定会话时请求
|
|
3954
|
+
# 声明一律忽略」同款纪律)。
|
|
3955
|
+
nsess = e.get("session")
|
|
3956
|
+
return bool(nsess) and nsess == self.principal.session
|
|
3957
|
+
return True
|
|
3958
|
+
|
|
3959
|
+
# 生效条件:nid 在 index["nodes"] 中有条目且 _readable(e) 为真时返回 True,否则(无条目/不可见)返回 False;作为 chain.adjacency 的读隔离谓词(cg._chain_visible)被消费——关系链面(causal_chain/explain_chain/causal_path/检索 chain 路 provenance 经 expand_from_seeds)由此单点隔离,不可见节点的出边、边条件文本与下游拓扑不再进入链展开;实例身份稳定(_readable 绑定档恒用 principal.session),与 adjacency 缓存无串台。
|
|
3960
|
+
def _chain_visible(self, nid) -> bool:
|
|
3961
|
+
"""关系链可见性谓词(chain.adjacency 读隔离闸,2026-09-25)。"""
|
|
3962
|
+
e = (self.index.get("nodes") or {}).get(nid)
|
|
3963
|
+
return bool(e) and self._readable(e)
|
|
3583
3964
|
|
|
3584
3965
|
# 生效条件:先以 limit=None 取 super().list_goals(status=status) 的全量,再只保留 index 中 _readable(e) 为真的目标,limit 为真值时返回 keep[:limit]、否则返回全部 keep。
|
|
3585
3966
|
def list_goals(self, status=None, limit=None):
|
|
@@ -3604,6 +3985,13 @@ class MdCGSecure(MdCGOS):
|
|
|
3604
3985
|
continue
|
|
3605
3986
|
if not self.principal.allows(m.get("sensitivity") or DEFAULT_SENSITIVITY):
|
|
3606
3987
|
continue
|
|
3988
|
+
# 会话绑定档(private/secret):近期事件同口径——仅归属会话,
|
|
3989
|
+
# 设计者豁免(issue #35 设计定稿,与 _readable 一致)。
|
|
3990
|
+
msens = m.get("sensitivity") or DEFAULT_SENSITIVITY
|
|
3991
|
+
if (_rank(msens) >= _rank("private")
|
|
3992
|
+
and not self.principal.can_admin
|
|
3993
|
+
and m.get("session") != self.principal.session):
|
|
3994
|
+
continue
|
|
3607
3995
|
if crypto.is_encrypted(r.get("text")):
|
|
3608
3996
|
t = self._open_content("_recent", m, r["text"])
|
|
3609
3997
|
if t is None:
|
|
@@ -3618,18 +4006,18 @@ class MdCGSecure(MdCGOS):
|
|
|
3618
4006
|
self.principal.require_admin("clear_recent")
|
|
3619
4007
|
return super().clear_recent()
|
|
3620
4008
|
|
|
3621
|
-
# 生效条件:在 super()._candidates(layer=layer, roles=roles, include_work=include_work, session=
|
|
4009
|
+
# 生效条件:在 super()._candidates(layer=layer, roles=roles, include_work=include_work, session=None(会话判定上收本层:基类硬等值过滤会误剔共享档的他人节点), branch=branch, ...) 的结果上,只保留 self._readable(e, session=session) 为真的条目(clearance × sensitivity ∧ 会话绑定,见 _readable;时间算子在父类候选层单点已过滤,此处只叠加读可见性)。
|
|
3622
4010
|
def _candidates(self, layer=None, roles=None, include_work=False,
|
|
3623
4011
|
session=None, branch=None, validity=None,
|
|
3624
4012
|
start_time=None, end_time=None, start_operator=None,
|
|
3625
4013
|
end_operator=None, time_axis=None, view=None):
|
|
3626
4014
|
out = super()._candidates(layer=layer, roles=roles, include_work=include_work,
|
|
3627
|
-
session=
|
|
4015
|
+
session=None, branch=branch, validity=validity,
|
|
3628
4016
|
start_time=start_time, end_time=end_time,
|
|
3629
4017
|
start_operator=start_operator,
|
|
3630
4018
|
end_operator=end_operator, time_axis=time_axis,
|
|
3631
4019
|
view=view)
|
|
3632
|
-
return [e for e in out if self._readable(e)]
|
|
4020
|
+
return [e for e in out if self._readable(e, session=session)]
|
|
3633
4021
|
|
|
3634
4022
|
# 生效条件:在 super()._neg_coverage(terms) 的结果上,只保留 self._readable(e) 为真的条目。
|
|
3635
4023
|
def _neg_coverage(self, terms):
|
|
@@ -3667,13 +4055,11 @@ class MdCGSecure(MdCGOS):
|
|
|
3667
4055
|
keep = []
|
|
3668
4056
|
for r in res:
|
|
3669
4057
|
e = self.index["nodes"].get(r[0]["id"], r[0])
|
|
3670
|
-
if not self._readable(e):
|
|
3671
|
-
continue
|
|
3672
|
-
# 会话归属同为候选资格:图扩展会把**别的会话**的节点顺着边带回来,
|
|
3673
|
-
# 绕过 _candidates 的会话过滤 → 自动召回照样串台。故与 validity/view
|
|
3674
|
-
# 一并在此兜底("*" = 跨会话视图,不过滤)。
|
|
3675
|
-
if session and session != "*" and e.get("session") != session:
|
|
4058
|
+
if not self._readable(e, session=session):
|
|
3676
4059
|
continue
|
|
4060
|
+
# 会话归属兜底已并入 _readable(session 传参):共享档跨会话可见、
|
|
4061
|
+
# 绑定档(private/secret)按归属会话判定(图扩展绕过 _candidates
|
|
4062
|
+
# 的串台路径由它一并挡住,与 validity/view 同层兜底)。
|
|
3677
4063
|
if view is not None and not roleviews.matches(e, view):
|
|
3678
4064
|
continue
|
|
3679
4065
|
if validity and trust.is_expired(e, now=now):
|