@furongjun1999/dsh-memory 0.7.0 → 0.7.2
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 +8 -6
- package/codebuddy/CODEBUDDY.md +5 -5
- package/docs/eval/cons200_/345/206/262/347/252/201/346/243/200/346/265/213/351/200/211/351/235/242_/345/256/236/346/226/275/350/256/260/345/275/225_v1.0.md +223 -0
- package/docs/eval/cons200_/345/206/262/347/252/201/346/243/200/346/265/213/351/200/211/351/235/242_/345/256/236/346/226/275/350/256/260/345/275/225_v1.1.md +340 -0
- package/docs/eval/issue50_/345/205/203/346/225/260/346/215/256/351/200/217/344/274/240/344/270/216/345/205/234/345/272/225_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +437 -0
- package/docs/eval/issue50_/345/215/212/351/207/215/345/244/215/345/276/205/345/256/232/345/244/215/346/240/270_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +488 -0
- package/docs/eval/issue50_/345/276/205/345/256/232/345/244/215/346/240/270/345/205/245/351/230/237_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +428 -0
- package/docs/eval/issue50_/350/257/273/351/235/242/344/277/235/346/212/244/345/217/252/350/256/244/346/230/276/345/274/217/346/235/245/346/272/220_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +271 -0
- package/docs/eval/issue50_/351/207/215/350/246/201/345/272/246/345/220/214/346/272/220/344/270/216/344/277/235/346/212/244/350/257/255/344/271/211_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +306 -0
- package/docs/eval/issue51_/344/270/200/351/224/256/345/256/211/350/243/205/345/244/261/350/264/245_/345/275/222/345/261/236/345/210/244/345/256/232_v1.0.md +58 -0
- package/docs/eval/issue52_/346/235/241/344/273/266/345/205/210/350/241/214/344/270/216/346/210/252/346/226/255/345/217/257/350/247/202/346/265/213_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +360 -0
- package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/241/346/241/243/344/275/215/345/215/225/344/270/200/345/205/245/345/217/243_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +651 -0
- package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/242/345/217/230/346/233/264/345/215/225/344/270/216/345/233/236/346/273/232/345/216/237/350/257/255_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +439 -0
- package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/243/345/207/206/345/205/245/350/257/273/346/225/260_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +1337 -0
- package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/244/346/224/266/345/256/230/344/270/216/345/205/250/351/223/276/351/252/214/346/224/266_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +1817 -0
- package/docs/eval//345/217/221/345/270/20309_/346/243/200/347/264/242/351/235/242/344/270/211/346/211/271/346/224/266/345/217/243_v1.0.md +66 -0
- package/docs/eval//345/217/221/345/270/20310_/350/257/273/351/235/242/346/215/237/345/235/217UTF8/345/256/266/346/227/217/346/224/266/345/217/243_v1.0.md +71 -0
- package/docs/eval//347/254/2543/345/261/202stg/347/273/223/346/236/204/347/264/242/345/274/225_/345/256/236/346/226/275/350/256/260/345/275/225_v1.0.md +250 -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 +25 -25
- package/docs/plans/stg/346/235/241/344/273/266/345/214/226/344/270/216/347/273/223/346/236/204/347/264/242/345/274/225_/350/256/276/350/256/241_v0.1.md +124 -0
- package/docs/plans//350/256/260/345/277/206/350/207/252/345/244/204/347/220/206/344/270/211/346/241/243/350/207/252/346/262/273_/350/256/276/350/256/241_v0.2.md +207 -0
- package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +4 -4
- package/md_cg/admission.py +718 -0
- package/md_cg/autonomy_modes.py +642 -0
- package/md_cg/bench6_arms.py +10 -6
- package/md_cg/census.py +2 -1
- package/md_cg/consistency.py +133 -8
- package/md_cg/forgetting.py +38 -6
- package/md_cg/freshness.py +17 -1
- package/md_cg/lifecycle.py +30 -2
- package/md_cg/mcp_server.py +63 -7
- package/md_cg/mdcg.py +157 -31
- package/md_cg/mdcos.py +675 -19
- package/md_cg/mreview/locate.py +1 -1
- package/md_cg/protect.py +79 -4
- package/md_cg/provenance.py +1 -1
- package/md_cg/review_cli.py +31 -2
- package/md_cg/rollback.py +420 -0
- package/md_cg/rollback_cli.py +104 -0
- package/md_cg/routing.py +10 -1
- package/md_cg/stg.py +400 -50
- package/md_cg/stgidx.py +281 -0
- package/md_cg/test_autonomy_admission.py +1098 -0
- package/md_cg/test_autonomy_modes.py +1972 -0
- package/md_cg/test_b1_auto_id_multiproc.py +7 -0
- package/md_cg/test_b1b2_write_face.py +7 -0
- package/md_cg/test_b3_merge_keeps_content.py +7 -0
- package/md_cg/test_cons200_scan_selection.py +791 -0
- package/md_cg/test_corrupt_utf8_read_surfaces.py +451 -0
- package/md_cg/test_generation_guard.py +15 -9
- package/md_cg/test_health_corrupt_utf8.py +37 -0
- package/md_cg/test_i50a_half_dup_defer.py +408 -0
- package/md_cg/test_i50b_defer_to_review_queue.py +545 -0
- package/md_cg/test_i50c_meta_passthrough.py +608 -0
- package/md_cg/test_i50d_importance_source.py +531 -0
- package/md_cg/test_i50e_readside_protection.py +656 -0
- package/md_cg/test_index_crossprocess_reload.py +39 -24
- package/md_cg/test_issue39_utf8_stdio.py +9 -1
- package/md_cg/test_issue52_scan_condition_first.py +677 -0
- package/md_cg/test_issue53_cipher_health_hash.py +385 -0
- package/md_cg/test_linkref.py +7 -0
- package/md_cg/test_mode_parity.py +1393 -0
- package/md_cg/test_mutation_rollback.py +805 -0
- package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +1 -1
- package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +7 -0
- package/md_cg/test_p2_mcp.py +8 -0
- package/md_cg/test_p9_forget_protect.py +7 -0
- package/md_cg/test_p9c_dedup_hints.py +7 -0
- package/md_cg/test_policy_required_ccg.py +5 -1
- package/md_cg/test_protocol.py +7 -0
- package/md_cg/test_stgidx_index_parity.py +1020 -0
- package/md_cg/test_writelimit.py +36 -9
- package/md_cg/test_writepipe.py +5 -2
- package/md_cg/weights.py +15 -1
- package/md_cg/writepipe.py +101 -2
- package/package.json +1 -1
- package/skills/plugin.json +1 -1
- package/zcode/AGENTS.md +5 -5
package/md_cg/mdcg.py
CHANGED
|
@@ -1451,6 +1451,12 @@ class MdCG:
|
|
|
1451
1451
|
# (自身写入已由 _stage 落进内存索引,计入签名会让每次自身 flush 都被
|
|
1452
1452
|
# 判成「他进程变化」→ 自身写入触发全量自重载)。flush() 里登记。
|
|
1453
1453
|
self._own_shard = None
|
|
1454
|
+
# 第 3 层结构索引(设计稿 §3.2):装载期内存倒排,**首次 stg 访问时**才构建
|
|
1455
|
+
# (惰性);写路径经 `_set_index_entry`/`_remove_index_entry` 同钩增量维护,
|
|
1456
|
+
# 整体换索引的路径(重载/compact/rebuild)经 `_invalidate_stg_index` 失效后
|
|
1457
|
+
# 由下次访问重建。占用面:1.7 万节点 ≈ 三张小表 + 位序/分类两表(内存派生,
|
|
1458
|
+
# 不改磁盘格式)。
|
|
1459
|
+
self._stg_index = None
|
|
1454
1460
|
self.index = self._load_index()
|
|
1455
1461
|
# P1b-2(2026-09-26,DSH 在役复验):索引只在本行装载一次,此后
|
|
1456
1462
|
# read/get/search 全走内存态——其它进程(review_cli autoflush=1)
|
|
@@ -1671,6 +1677,10 @@ class MdCG:
|
|
|
1671
1677
|
self._dirty_replay_superseded = superseded
|
|
1672
1678
|
idx["buckets"] = self._count_buckets(idx["nodes"])
|
|
1673
1679
|
self.index = idx
|
|
1680
|
+
# 第 3 层结构索引随整体换索引一并失效:重载取回的是**另一份**快照
|
|
1681
|
+
# (他进程写的新记录/新盘面),旧表按身份判代际必然不符——显式丢表,
|
|
1682
|
+
# 下次 stg 访问按新快照重建(惰性失效重建,与 rebuild/compact 同路)。
|
|
1683
|
+
self._invalidate_stg_index()
|
|
1674
1684
|
self._index_sig = sig
|
|
1675
1685
|
# 旧索引代里的派生缓存一并失效:query 结果缓存(hotcache,默认关)
|
|
1676
1686
|
# 缓存的是旧候选池上的结果;解析读缓存(readcache,默认开)按 path
|
|
@@ -1785,13 +1795,20 @@ class MdCG:
|
|
|
1785
1795
|
atomic_write(self.index_path, json.dumps(idx, ensure_ascii=False))
|
|
1786
1796
|
ShardedLog.clear(self.index_log_dir)
|
|
1787
1797
|
self.index = idx
|
|
1798
|
+
# 第 3 层结构索引随快照替换失效(快照重建过,旧表与它的划分关系不再
|
|
1799
|
+
# 有据可依):下次 stg 访问重建;不在此处立即重算——compact 是收尾路
|
|
1800
|
+
# 径,重建成本挪到真正需要索引的那次读(惰性失效重建,同一选)。
|
|
1801
|
+
self._invalidate_stg_index()
|
|
1788
1802
|
# P1b-2:自己写了快照 → 主动刷新签名,防止后续读路径把自己的
|
|
1789
1803
|
# compact 误判为「他进程写入」而做一次无谓重载(自写自载循环)。
|
|
1790
1804
|
self._index_sig = self._index_signature()
|
|
1791
1805
|
return idx
|
|
1792
1806
|
|
|
1793
|
-
# 生效条件:self._dirty 非空时才在 FileLock(index_path) 下(必要时新建 ShardedLog)逐条 append、清空 _dirty 并关闭分片句柄;self._dirty
|
|
1807
|
+
# 生效条件:self._dirty 非空时才在 FileLock(index_path) 下(必要时新建 ShardedLog)逐条 append、清空 _dirty 并关闭分片句柄;self._dirty 为空时立即返回、不写任何记录;**不动 self.index、不失效第 3 层结构索引**——flush 只落索引派生物(分片日志),内存快照与其划分表本就不变(「flush 后表与快照一致」由守卫钉住);
|
|
1794
1808
|
def flush(self):
|
|
1809
|
+
# 第 3 层结构索引(stgidx):flush **不失效**——它不改内存索引(记录早在
|
|
1810
|
+
# `_stage` 时就已同钩进表),若每次 flush 都丢表,惰性重建会在写读交替
|
|
1811
|
+
# 负载下每轮重扫(与 readcache「写谁失效谁」同款纪律)。
|
|
1795
1812
|
if not self._dirty:
|
|
1796
1813
|
return
|
|
1797
1814
|
# 世代/互斥防线(批次9,M3.3 落地):append 必须与 compact/rebuild 的
|
|
@@ -1843,7 +1860,7 @@ class MdCG:
|
|
|
1843
1860
|
self._log.close()
|
|
1844
1861
|
self._log = None
|
|
1845
1862
|
|
|
1846
|
-
# 生效条件:以节点文件绝对路径 p、其所在层目录名 layer、解析出的 fm 与 content 为入参,构造含 path(相对 root 正斜杠)/layer(fm 回落 layer)/role/content_kind/session/tags/bucket(父目录名,等于层名时 None)/bucket_zh/importance/created_at/verification_basis/has_neg_conditions/content_hash/temporal/spatial/time_window/lifecycle 与 trust 状态字段/branch_id/branched_from/evidence_count/big_domain/observation_position/subgraph/edges/protected/protection_reason/immutable/self_state/derived_from/derived_relation 各键的条目,经 _strip_empty_gate_fields 清洗后返回;
|
|
1863
|
+
# 生效条件:以节点文件绝对路径 p、其所在层目录名 layer、解析出的 fm 与 content 为入参,构造含 path(相对 root 正斜杠)/layer(fm 回落 layer)/role/content_kind/session/tags/bucket(父目录名,等于层名时 None)/bucket_zh/importance/importance_source(issue50-d:fm 键缺省 None)/created_at/verification_basis/has_neg_conditions/content_hash/temporal/spatial/time_window/lifecycle 与 trust 状态字段/branch_id/branched_from/evidence_count/big_domain/observation_position/subgraph/edges/protected/protection_reason/immutable/self_state/derived_from/derived_relation 各键的条目,经 _strip_empty_gate_fields 清洗后返回;
|
|
1847
1864
|
def _node_entry(self, p, layer, fm, content):
|
|
1848
1865
|
"""单节点索引条目——_scan_nodes 与定向 upsert 共用的**唯一真源**。
|
|
1849
1866
|
|
|
@@ -1875,6 +1892,10 @@ class MdCG:
|
|
|
1875
1892
|
# S1b 跨语言收敛免读文件可判。
|
|
1876
1893
|
"bucket_zh": fm.get("bucket_zh") or None,
|
|
1877
1894
|
"importance": fm.get("importance", 0.5),
|
|
1895
|
+
# issue50-d(2026-10-02):重要度**来源**入快照("hint"|"heuristic",
|
|
1896
|
+
# 旧库无此键 → None)——「这条 0.8 是显式声明还是启发式分」免读
|
|
1897
|
+
# 文件可判(与既有扁平键同理由:检索期/审计期不读盘)。
|
|
1898
|
+
"importance_source": fm.get("importance_source"),
|
|
1878
1899
|
"created_at": fm.get("created_at", 0),
|
|
1879
1900
|
"verification_basis": fm.get("verification_basis"),
|
|
1880
1901
|
"has_neg_conditions": nodefile.has_non_applicable(content),
|
|
@@ -1979,6 +2000,10 @@ class MdCG:
|
|
|
1979
2000
|
atomic_write(self.index_path, json.dumps(idx, ensure_ascii=False))
|
|
1980
2001
|
ShardedLog.clear(self.index_log_dir)
|
|
1981
2002
|
self.index = idx
|
|
2003
|
+
# 第 3 层结构索引随全量重建失效(新快照 = 新盘面语义):下次 stg 访问
|
|
2004
|
+
# 重建——重建后的划分不变量由守卫钉住(写/删/rebuild/compact 后三表与
|
|
2005
|
+
# 快照一致)。
|
|
2006
|
+
self._invalidate_stg_index()
|
|
1982
2007
|
# P1b-2:rebuild 同 compact——写完快照即刷新签名(自写不自载)。
|
|
1983
2008
|
self._index_sig = self._index_signature()
|
|
1984
2009
|
# broad=True:rebuild 以盘面扫描为准——ccgc/crosscheck/backfill 等
|
|
@@ -2046,7 +2071,8 @@ class MdCG:
|
|
|
2046
2071
|
derived_from=None, relation: str = provenance.DEFAULT_RELATION,
|
|
2047
2072
|
semantic: str = None, depends_on=None, valid_from=None,
|
|
2048
2073
|
valid_until=None, effective_from=None, effective_until=None,
|
|
2049
|
-
believed_at=None, verification_state: str = None,
|
|
2074
|
+
believed_at=None, verification_state: str = None,
|
|
2075
|
+
importance_source: str = None, **extra) -> str:
|
|
2050
2076
|
"""写入一个节点。
|
|
2051
2077
|
|
|
2052
2078
|
verification_basis: 外部验证基底(白箱信任的硬门槛),
|
|
@@ -2236,6 +2262,11 @@ class MdCG:
|
|
|
2236
2262
|
"evidence_count": 0, "positive_evidence": 0, "negative_evidence": 0,
|
|
2237
2263
|
}
|
|
2238
2264
|
fm.update(extra)
|
|
2265
|
+
# issue50-d(2026-10-02):重要度**来源**声明("hint"|"heuristic")落
|
|
2266
|
+
# frontmatter——下游免读文件即可判「这条 0.8 是显式声明还是闸门启发式
|
|
2267
|
+
# 分」。缺省 None 不落键(既有调用方 fm 形态逐位不变,零回归)。
|
|
2268
|
+
if importance_source:
|
|
2269
|
+
fm["importance_source"] = importance_source
|
|
2239
2270
|
if bucket_zh:
|
|
2240
2271
|
fm["bucket_zh"] = bucket_zh
|
|
2241
2272
|
# S1 大域先验:写入时固化「内容 → 大域」(契约 §3 S1)。
|
|
@@ -2435,8 +2466,14 @@ class MdCG:
|
|
|
2435
2466
|
fm["derived_from"] = parents
|
|
2436
2467
|
fm["derived_relation"] = derived_rel
|
|
2437
2468
|
# 重要性 ≥0.7 自动打保护标记(对齐 tool_table:≥0.7 触发不可遗忘保护)
|
|
2469
|
+
# issue50-d(2026-10-02):自动保护只认**显式声明**——调用方随 add
|
|
2470
|
+
# 下传 importance_source 时,"heuristic"(闸门启发式分)过线**只落分、
|
|
2471
|
+
# 不打位**(remember_gated 的裁决面文案已如实说「未落保护」);
|
|
2472
|
+
# importance_source 缺省 None(add 的全部既有调用方都不传)⇒ 本条件
|
|
2473
|
+
# 恒真、既有行为逐位不变,零回归由构造保证。
|
|
2438
2474
|
if (float(importance or 0.0) >= protect.AUTO_PROTECT_IMPORTANCE
|
|
2439
|
-
and not fm.get("protected")
|
|
2475
|
+
and not fm.get("protected")
|
|
2476
|
+
and importance_source != "heuristic"):
|
|
2440
2477
|
fm["protected"] = True
|
|
2441
2478
|
fm["protection_reason"] = (f"importance={float(importance):.2f}"
|
|
2442
2479
|
f"≥{protect.AUTO_PROTECT_IMPORTANCE}")
|
|
@@ -2459,20 +2496,20 @@ class MdCG:
|
|
|
2459
2496
|
os.remove(_old_real)
|
|
2460
2497
|
except OSError:
|
|
2461
2498
|
pass # 删除失败不阻断主写路径(残留双文件退回旧行为,可重建兜底)
|
|
2462
|
-
#
|
|
2463
|
-
#
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
self.index["buckets"][_ob] = _left
|
|
2469
|
-
else:
|
|
2470
|
-
self.index["buckets"].pop(_ob, None)
|
|
2499
|
+
# 旧桶计数递减由 `_set_index_entry` 单点接管(issue #53-B):本处
|
|
2500
|
+
# 原先手写一份「仅桶变化时撤旧」——并有一句注释把「同桶覆写的
|
|
2501
|
+
# _stage 自增」当成既有口径,而实测那正是计数虚高的病灶(覆写一次
|
|
2502
|
+
# counts +1,{'orphan': 1} → 2)。撤旧/加新统一在单点按**归属
|
|
2503
|
+
# 变化**判定;若此处再手写一份,换桶覆写会被撤两次、同桶覆写
|
|
2504
|
+
# 仍虚高,两处口径必然分叉。本处不再动计数。
|
|
2471
2505
|
self._stage(node_id, _strip_empty_gate_fields({
|
|
2472
2506
|
"path": os.path.relpath(path, self.root).replace("\\", "/"),
|
|
2473
2507
|
"layer": layer, "tags": tags, "bucket": bucket,
|
|
2474
2508
|
"bucket_zh": bucket_zh or None,
|
|
2475
2509
|
"importance": importance, "created_at": fm["created_at"],
|
|
2510
|
+
# issue50-d:重要度来源入快照(与 _node_entry 同口径——写入后即可
|
|
2511
|
+
# 免读文件判「0.8 是显式 hint 还是启发式」,不等全量重建)。
|
|
2512
|
+
"importance_source": fm.get("importance_source"),
|
|
2476
2513
|
"verification_basis": verification_basis,
|
|
2477
2514
|
"has_neg_conditions": nodefile.has_non_applicable(sealed),
|
|
2478
2515
|
# P2-1 / P4-2①:与 `_node_entry`(重建路径)**同口径**——写路径的
|
|
@@ -2973,17 +3010,71 @@ class MdCG:
|
|
|
2973
3010
|
atomic_write(self.recent_log, "")
|
|
2974
3011
|
return len(recs)
|
|
2975
3012
|
|
|
2976
|
-
# 生效条件:无条件把 entry
|
|
3013
|
+
# 生效条件:无条件把 entry 记进 _dirty[node_id] 并经 _set_index_entry 写进 index["nodes"][node_id](桶计数与 stg 结构索引同钩,见该函数);当 len(self._dirty) >= self.autoflush 时调 flush()(autoflush 为 0 时每次标脏都立即 flush);
|
|
2977
3014
|
def _stage(self, node_id, entry):
|
|
2978
3015
|
self._dirty[node_id] = entry
|
|
2979
|
-
self.
|
|
2980
|
-
if entry.get("bucket"):
|
|
2981
|
-
self.index["buckets"][entry["bucket"]] = \
|
|
2982
|
-
self.index["buckets"].get(entry["bucket"], 0) + 1
|
|
3016
|
+
self._set_index_entry(node_id, entry)
|
|
2983
3017
|
if len(self._dirty) >= self.autoflush:
|
|
2984
3018
|
self.flush()
|
|
2985
3019
|
|
|
2986
|
-
#
|
|
3020
|
+
# 生效条件:无条件把 entry 写入 index["nodes"][node_id](同键覆写 = dict 原地更新,物理位序不变);桶计数只在该 id 的桶归属**变化**时调整(经 _bucket_shift 撤旧加新,同归属零动作);self._stg_index 非 None(第 3 层结构索引已构建)时同钩增量增/改该 id,为 None(未构建/已失效)时**不构建**——首次 stg 访问才构建,写路径零额外成本;返回 None。
|
|
3021
|
+
def _set_index_entry(self, node_id, entry):
|
|
3022
|
+
"""索引条目写入的**单点**(桶计数 + stg 结构索引同钩)。
|
|
3023
|
+
|
|
3024
|
+
「同钩」是设计稿 §3.2 的字面要求:写路径的增量维护若与桶计数分家,
|
|
3025
|
+
两处口径必然漂开(本仓既有教训:同一判据写两份 = 迟早各漏一次)。
|
|
3026
|
+
无效化(rebuild/compact/重载)后 `_stg_index` 为 None ⇒ 这里退化为
|
|
3027
|
+
无操作,读侧「首次需要时构建」的惰性路径接管。
|
|
3028
|
+
|
|
3029
|
+
issue #53-B:同 id 再装入(add 覆写 / backfill 等 _stage)此前对 entry
|
|
3030
|
+
的桶**无条件 +1**——旧条目的计数从不撤下,覆写一次即虚高 1(实测
|
|
3031
|
+
{'orphan': 1} → 覆写后 {'orphan': 2}),health 分桶读数随之失真。
|
|
3032
|
+
改为「归属变化才动计数」:撤旧、加新,同桶覆写零动作。
|
|
3033
|
+
"""
|
|
3034
|
+
_old = self.index["nodes"].get(node_id)
|
|
3035
|
+
self.index["nodes"][node_id] = entry
|
|
3036
|
+
_ob = _old.get("bucket") if _old is not None else None
|
|
3037
|
+
_nb = entry.get("bucket")
|
|
3038
|
+
if _ob != _nb:
|
|
3039
|
+
self._bucket_shift(_ob, -1)
|
|
3040
|
+
self._bucket_shift(_nb, +1)
|
|
3041
|
+
ix = getattr(self, "_stg_index", None)
|
|
3042
|
+
if ix is not None:
|
|
3043
|
+
ix.add(node_id, entry)
|
|
3044
|
+
|
|
3045
|
+
# 生效条件:无条件 pop index["nodes"][node_id](不存在则无操作);被 pop 的条目经 _bucket_shift 撤其桶一票;self._stg_index 非 None 时同钩摘除该 id(未构建即不动);返回被摘除的条目(不存在返回 None)。
|
|
3046
|
+
def _remove_index_entry(self, node_id):
|
|
3047
|
+
"""索引条目摘除的**单点**(与 `_set_index_entry` 对称:桶计数 + 结构索引同钩)。"""
|
|
3048
|
+
e = self.index["nodes"].pop(node_id, None)
|
|
3049
|
+
if e:
|
|
3050
|
+
self._bucket_shift(e.get("bucket"), -1)
|
|
3051
|
+
ix = getattr(self, "_stg_index", None)
|
|
3052
|
+
if ix is not None:
|
|
3053
|
+
ix.remove(node_id)
|
|
3054
|
+
return e
|
|
3055
|
+
|
|
3056
|
+
# 生效条件:bucket 为假值时直接返回;否则 index["buckets"][bucket] 计数 +delta,结果 >0 时写回、<=0 时删除该键;无返回值。
|
|
3057
|
+
def _bucket_shift(self, bucket, delta):
|
|
3058
|
+
"""桶计数 ±1 的**唯一实现**(归零删键)——issue #53-B 的单点收口。
|
|
3059
|
+
|
|
3060
|
+
此前 `_set_index_entry` 只加不减、`_remove_index_entry` 与 `add()` 内
|
|
3061
|
+
各手写一份递减——同一判据三份实现,旧条目归属变化时三处口径必然分叉
|
|
3062
|
+
(本仓既有教训:「同一判据写两份 = 迟早各漏一次」)。
|
|
3063
|
+
"""
|
|
3064
|
+
if not bucket:
|
|
3065
|
+
return
|
|
3066
|
+
left = self.index["buckets"].get(bucket, 0) + delta
|
|
3067
|
+
if left > 0:
|
|
3068
|
+
self.index["buckets"][bucket] = left
|
|
3069
|
+
else:
|
|
3070
|
+
self.index["buckets"].pop(bucket, None)
|
|
3071
|
+
|
|
3072
|
+
# 生效条件:恒把 self._stg_index 置 None(丢弃已构建的第 3 层结构索引)——凡**整体替换 self.index** 的路径(重载/compact/rebuild)调它:表按身份判代际(`StgIndex.index_obj is self.index`),显式失效让下次 stg 访问重新按新快照构建(惰性失效重建,设计稿 §3.2 二选一之选);恒返回 None、不抛。
|
|
3073
|
+
def _invalidate_stg_index(self):
|
|
3074
|
+
"""第 3 层结构索引的失效单点(整体换索引的三处编排点都调它)。"""
|
|
3075
|
+
self._stg_index = None
|
|
3076
|
+
|
|
3077
|
+
# 生效条件:先经 _remove_index_entry 摘除 index["nodes"][node_id](桶计数与 stg 结构索引同钩),随后无论是否命中都把 _dirty[node_id] 置 None(删除 tombstone)并立即调 flush() 持久化;
|
|
2987
3078
|
def _unstage(self, node_id):
|
|
2988
3079
|
"""摘除索引条目并**持久化**——与 `_stage` 对称的删除原语。
|
|
2989
3080
|
|
|
@@ -2993,14 +3084,7 @@ class MdCG:
|
|
|
2993
3084
|
幽灵条目的代价:检索白跑候选、`ref action=prune` 因 `cg.get` 取不回
|
|
2994
3085
|
而够不着它,`check` 的 dangling 永不归零(本机实测累积数百条)。
|
|
2995
3086
|
"""
|
|
2996
|
-
|
|
2997
|
-
if e and e.get("bucket"):
|
|
2998
|
-
b = e["bucket"]
|
|
2999
|
-
left = self.index["buckets"].get(b, 0) - 1
|
|
3000
|
-
if left > 0:
|
|
3001
|
-
self.index["buckets"][b] = left
|
|
3002
|
-
else:
|
|
3003
|
-
self.index["buckets"].pop(b, None)
|
|
3087
|
+
self._remove_index_entry(node_id)
|
|
3004
3088
|
self._dirty[node_id] = None # None = 删除记录,随 flush 落分片日志
|
|
3005
3089
|
self.flush() # 删除不可延迟到 autoflush 阈值
|
|
3006
3090
|
|
|
@@ -3031,6 +3115,17 @@ class MdCG:
|
|
|
3031
3115
|
node_id, fm.get("sensitivity"), declared_sensitivity, sealed,
|
|
3032
3116
|
has_sealer=(type(self)._seal_content is not MdCG._seal_content))
|
|
3033
3117
|
atomic_write(path, nodefile.dumps(fm, sealed), durable=durable)
|
|
3118
|
+
# issue #53-C:写盘口同步索引内容指纹——「所有写盘点都应走这里」的
|
|
3119
|
+
# 同一收口面延伸。元数据更新族写点(update_tags / append_edge /
|
|
3120
|
+
# append_subgraph_node / set_edge_condition / backfill_* …)走
|
|
3121
|
+
# 「get() 解密 → 本方法重封」时密文每次变化(AEAD 随机 nonce),
|
|
3122
|
+
# 而它们只同步自己关心的索引键——本键若不在此收口,索引 hash 会停在
|
|
3123
|
+
# **旧密文**上 ⇒ 启动对账每次报 hash_drift 假阳性(实测可经 compact
|
|
3124
|
+
# 快照与进程重开持久存活,直到某次全量重建;报告 issue #53-C)。
|
|
3125
|
+
# 新节点首次写入时条目尚未装入(_stage 紧随其后,值同源)→ 取不到即跳过。
|
|
3126
|
+
_entry = self.index["nodes"].get(node_id)
|
|
3127
|
+
if _entry is not None:
|
|
3128
|
+
_entry["content_hash"] = nodefile.content_hash(sealed)
|
|
3034
3129
|
return sealed
|
|
3035
3130
|
|
|
3036
3131
|
# ---------- S1 前置元数据:域标签回填 ----------
|
|
@@ -3045,7 +3140,8 @@ class MdCG:
|
|
|
3045
3140
|
走 `get()`(解密)→ `_write_node()`(重新封装),保证加密库不会双重封装。
|
|
3046
3141
|
"""
|
|
3047
3142
|
st = {"seen": 0, "already": 0, "written": 0, "no_signal": 0,
|
|
3048
|
-
"unreadable": 0, "
|
|
3143
|
+
"unreadable": 0, "ciphertext": 0, "index_synced": 0,
|
|
3144
|
+
"dry_run": bool(dry_run)}
|
|
3049
3145
|
# 前置:功能未开启时**不做任何写入**——域标签是 S1 的元数据,默认口径不得被改变:
|
|
3050
3146
|
# 否则「回填写索引」会与「_scan_nodes 默认关剥键」冲突,写/重建两条路口径不一致。
|
|
3051
3147
|
if os.environ.get("MDCG_RETRIEVAL_PIPELINE") != "1":
|
|
@@ -3075,6 +3171,17 @@ class MdCG:
|
|
|
3075
3171
|
if content is None:
|
|
3076
3172
|
st["unreadable"] += 1
|
|
3077
3173
|
continue
|
|
3174
|
+
from . import crypto
|
|
3175
|
+
if crypto.is_encrypted(content):
|
|
3176
|
+
# issue #53-A:密文不可分类——无密钥实例(backfill CLI 以基类
|
|
3177
|
+
# MdCG 打开库)的 get() 把密文**原样**交出,base64 噪声会碰巧
|
|
3178
|
+
# 命中大域词表(实测唯一命中=「经济」:词表唯一拉丁词 GDP 被
|
|
3179
|
+
# 三连子串撞上),假标签再被 already 分支永久固化。此处**单列
|
|
3180
|
+
# 计数**并跳过(不与 no_signal 叠加:各计数是互斥分区);
|
|
3181
|
+
# classify_text 单点也已同源设防(双层:此层给可观测计数,
|
|
3182
|
+
# 单点层保证任何调用方都不会误分类密文)。
|
|
3183
|
+
st["ciphertext"] += 1
|
|
3184
|
+
continue
|
|
3078
3185
|
try:
|
|
3079
3186
|
dom = routing.classify_text(content)
|
|
3080
3187
|
except Exception:
|
|
@@ -3159,6 +3266,8 @@ class MdCG:
|
|
|
3159
3266
|
try:
|
|
3160
3267
|
with open(p, encoding="utf-8") as f:
|
|
3161
3268
|
fm, content = nodefile.loads(f.read())
|
|
3269
|
+
except UnicodeDecodeError:
|
|
3270
|
+
return None
|
|
3162
3271
|
except OSError as exc:
|
|
3163
3272
|
# C-3(FI-M02 / N134):瞬时读失败**不得静默**——记账(真缺不记)。
|
|
3164
3273
|
# 返回值语义不变(None = 不可读),本方法不入任何缓存,本处无负缓存
|
|
@@ -3199,6 +3308,8 @@ class MdCG:
|
|
|
3199
3308
|
try:
|
|
3200
3309
|
with open(p, encoding="utf-8") as f:
|
|
3201
3310
|
return nodefile.loads(f.read()) + (None,)
|
|
3311
|
+
except UnicodeDecodeError:
|
|
3312
|
+
return None, None, None
|
|
3202
3313
|
except OSError as exc:
|
|
3203
3314
|
if self._note_read_oserror(p, exc):
|
|
3204
3315
|
return None, None, fsutil.READ_FAIL_TRANSIENT
|
|
@@ -3611,7 +3722,7 @@ class MdCG:
|
|
|
3611
3722
|
try:
|
|
3612
3723
|
with open(full_path, encoding="utf-8") as f:
|
|
3613
3724
|
fm, content = nodefile.loads(f.read())
|
|
3614
|
-
except OSError:
|
|
3725
|
+
except (OSError, UnicodeDecodeError):
|
|
3615
3726
|
continue
|
|
3616
3727
|
if any(t in content for t in terms):
|
|
3617
3728
|
neg_coverage.append(e)
|
|
@@ -4664,15 +4775,29 @@ class MdCG:
|
|
|
4664
4775
|
h = routing.bucket_health(self.index.get("buckets", {}),
|
|
4665
4776
|
total_nodes=len(self.index["nodes"]))
|
|
4666
4777
|
h["total_nodes"] = len(self.index["nodes"])
|
|
4778
|
+
# issue #53-B③:分桶读数的**覆盖范围自述**——分桶物理上只存在于
|
|
4779
|
+
# BUCKETED_LAYERS(只 knowledge),非分桶层节点不进 buckets 计数。
|
|
4780
|
+
# 此前读数不带范围,报告方把「nodes=4 / total_nodes=505」读成
|
|
4781
|
+
# 「统计建立在个位数样本上」(实为「knowledge 层 4 个带桶条目」)
|
|
4782
|
+
# ——加一行的成本买断这个歧义。
|
|
4783
|
+
h["bucket_scope"] = list(BUCKETED_LAYERS)
|
|
4667
4784
|
# 5 要素完整度(全节点扫一遍,可能慢但只在 health() 调用)
|
|
4668
4785
|
layer_stats = {}
|
|
4669
4786
|
neg_stats = {}
|
|
4787
|
+
# PR #54 的跳过是**静默**的:被跳过的节点不进 layer_stats,读数对
|
|
4788
|
+
# 「少算了几个」无自述(跳过与不存在在统计上同形)。加显式计数:
|
|
4789
|
+
# 纯增量、与既有键无冲突,与 bucket_scope 同构的扩键流程。
|
|
4790
|
+
# skipped_unreadable=0 ⇔ 索引内每个节点本进程都读盘成功。
|
|
4791
|
+
skipped_unreadable = 0
|
|
4670
4792
|
for e in list(self.index["nodes"].values()):
|
|
4671
4793
|
full_path = self._node_disk_path(e)
|
|
4672
4794
|
try:
|
|
4673
4795
|
with open(full_path, encoding="utf-8") as f:
|
|
4674
4796
|
fm, content = nodefile.loads(f.read())
|
|
4675
|
-
except OSError:
|
|
4797
|
+
except (OSError, UnicodeDecodeError):
|
|
4798
|
+
# 与索引重建同口径:损坏的非 UTF-8 节点不应让健康端点
|
|
4799
|
+
# 整体崩溃;跳过该节点,保留其余盘面读数。
|
|
4800
|
+
skipped_unreadable += 1
|
|
4676
4801
|
continue
|
|
4677
4802
|
cpl = nodefile.ccg_completeness(content)
|
|
4678
4803
|
layer = e["layer"]
|
|
@@ -4693,4 +4818,5 @@ class MdCG:
|
|
|
4693
4818
|
neg_stats[layer] += 1
|
|
4694
4819
|
h["ccg_by_layer"] = layer_stats
|
|
4695
4820
|
h["neg_memory_counts"] = neg_stats
|
|
4696
|
-
|
|
4821
|
+
h["skipped_unreadable"] = skipped_unreadable
|
|
4822
|
+
return h
|