@furongjun1999/dsh-memory 0.6.1 → 0.7.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 +49 -18
- package/docs/README.md +1 -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.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/207/272/350/264/247/351/235/242/345/206/222/347/203/237_/350/277/233/350/264/247/351/227/250/347/246/201_v1.0.md +460 -0
- package/docs/eval//345/217/221/345/270/20308_/350/207/252/350/277/255/344/273/243/344/270/216/347/235/241/347/234/240_/345/233/276/346/243/200/347/264/242/350/267/257/344/270/216/346/235/203/351/207/215_v1.0.md +97 -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/275/222/344/270/200/345/261/202/347/274/272/347/234/201/347/277/273/345/205/263_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +458 -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/hive//346/243/200/347/264/242/347/256/227/346/263/225/345/217/243/345/276/204/345/257/271/347/205/247_v0.1.md +136 -11
- package/docs/hive//346/243/200/347/264/242/350/267/257/345/276/204/344/270/216/350/256/244/347/237/245/347/273/223/346/236/204/345/245/221/347/272/246_v0.1.md +32 -2
- package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +88 -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/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +47 -11
- package/docs/mdcg//347/235/241/347/234/240/345/221/250/346/234/237_/350/277/220/347/273/264/345/211/215/346/217/220/344/270/216/347/273/264/346/212/244/346/214/207/345/215/227_v1.0.md +183 -0
- 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//347/235/241/347/234/240/344/270/216/350/207/252/350/277/255/344/273/243_/345/212/237/350/203/275/344/274/230/345/214/226/350/256/276/350/256/241_v0.4.md +547 -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/md_cg/admission.py +718 -0
- package/md_cg/autonomy_modes.py +642 -0
- package/md_cg/bench_e2e_locomo_qa.py +11 -4
- package/md_cg/chain.py +47 -0
- package/md_cg/consistency.py +133 -8
- package/md_cg/forgetting.py +38 -6
- package/md_cg/freshness.py +527 -0
- package/md_cg/generation.py +409 -0
- package/md_cg/hotcache.py +4 -1
- package/md_cg/lifecycle.py +30 -2
- package/md_cg/mcp_server.py +191 -26
- package/md_cg/mdcg.py +594 -39
- package/md_cg/mdcos.py +869 -38
- package/md_cg/nodefile.py +74 -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 +411 -0
- package/md_cg/rollback_cli.py +104 -0
- package/md_cg/semantic/canonical.py +22 -0
- package/md_cg/semantic/unify.py +73 -22
- package/md_cg/semantic/unify_fixture.json +25 -0
- package/md_cg/sleep.py +1297 -0
- package/md_cg/stg.py +400 -50
- package/md_cg/stgidx.py +281 -0
- package/md_cg/sustain.py +146 -9
- package/md_cg/test_auto_defaults.py +424 -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_boundary_hit.py +410 -0
- package/md_cg/test_cons200_scan_selection.py +791 -0
- package/md_cg/test_en_pipeline.py +22 -13
- package/md_cg/test_generation_guard.py +352 -0
- package/md_cg/test_h4_sustain_snapshot.py +14 -4
- 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_issue39_utf8_stdio.py +9 -1
- package/md_cg/test_issue52_scan_condition_first.py +677 -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_n212_n213_n224_generation_gates.py +14 -8
- package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +7 -0
- package/md_cg/test_n230_dirty_replay.py +375 -0
- package/md_cg/test_p2_mcp.py +8 -0
- package/md_cg/test_p2_six_elements.py +463 -0
- package/md_cg/test_p3_legacy_closure.py +433 -0
- package/md_cg/test_p4_freshness.py +680 -0
- package/md_cg/test_p8_subgraph_chain.py +14 -1
- 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_rank_parity_score_mode.py +6 -0
- package/md_cg/test_semantic_canonical.py +5 -3
- package/md_cg/test_sleep.py +611 -0
- package/md_cg/test_sleep_p1.py +784 -0
- package/md_cg/test_stgidx_index_parity.py +1020 -0
- package/md_cg/test_time_core_lint.py +968 -0
- package/md_cg/test_unify_default_off.py +701 -0
- package/md_cg/test_unify_scope.py +182 -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/whitebox_kb/aeis_core/time_core.py +8 -0
- package/md_cg/writepipe.py +101 -2
- package/package.json +3 -2
- package/skills/plugin.json +1 -1
- package/utf8_boot.py +237 -0
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""P4 守卫 · 权重刷新与衰减进主分数(§7.1–§7.4)。
|
|
3
|
+
|
|
4
|
+
覆盖判据(每条对应契约的一项):
|
|
5
|
+
|
|
6
|
+
G1 **核来自唯一权威**:`md_cg/whitebox_kb/aeis_core/time_core.py`——本模块
|
|
7
|
+
源码**不含指数核调用**,且 `decay_factor` 与 `cred_factor` 在 6 个 dt 上
|
|
8
|
+
**逐位相等**(含 inf → floor)。
|
|
9
|
+
G2 **γ 与 Δt 口径**(§〇.3-12):γ 缺省 `ln2/30 天`、与 `mdcos.temporal_gamma`
|
|
10
|
+
**同一读取点**(env `MDCG_TEMPORAL_GAMMA` 同时覆盖两条路);Δt 取
|
|
11
|
+
`created_at`;**单位必须是「天」**——30 天 → 0.5,若按秒喂同一时长则触 floor
|
|
12
|
+
(单位断言的**判别力**)。
|
|
13
|
+
G3 **refresh 参数照抄 AEIS**:与 `aeis_core/core.py` 的 `consolidate_cycle`
|
|
14
|
+
既有取值逐条相等(源码对拍 + 行为三档 1.01 / 0.99 / 1.0)。
|
|
15
|
+
G4 **衰减必加 floor**(§7.3):老节点/缺 created_at 不低于 `SCORE_FLOOR`;
|
|
16
|
+
**受保护节点** `importance × 乘子 ≥ IMPORTANCE_PROTECT` 恒成立;判别力
|
|
17
|
+
(未保护节点不享有该下界)。
|
|
18
|
+
G5 **落位 = 与分池权重同一乘子位**(§7.2):源码断言乘子行紧邻
|
|
19
|
+
`pooling.weight_of(...)` 行;行为断言 `_score` 的同内容两节点分数比
|
|
20
|
+
== 乘子比。
|
|
21
|
+
G6 **索引条目取数**(P4-2①):条目含 `access_count`/`last_access`;
|
|
22
|
+
`freshness.entry_weight` 只凭条目即可算(无 IO 面)。
|
|
23
|
+
G7 **开关可回退**:`MDCG_FRESHNESS=0` → 乘子恒 1.0,`_score` 与基线逐位一致。
|
|
24
|
+
G8 **三件套**(§7.4):预演零写盘;apply 逐节点 before/after 落
|
|
25
|
+
`_maintain.jsonl`(`action="freshness"`,与 `importance` 同族);
|
|
26
|
+
rollback 写回 before 且**不删节点**。
|
|
27
|
+
G9 **降级不得实现为删除记录**(§7.3):降级只写 `freshness_state="degraded"`,
|
|
28
|
+
节点与正文原样在库;跨层降级走既有 `_move_layer`(内含 `guard_move`)
|
|
29
|
+
——受保护节点被移出保护层抛 `ProtectionError` 且无副作用。
|
|
30
|
+
G10 `weights.recalc` 的「`after ≥ 0.70` 自动补写 `protected`」行为**仍在位**。
|
|
31
|
+
G11 **Rust 侧同口径**:`rust/src/freshness.rs` 的四个常量与 Python 同值;
|
|
32
|
+
乘子只在 `sort_path(..., apply_freshness=true)` 生效且 `fuse` 只对
|
|
33
|
+
lexical 传 true(与 Python `_score` 的对位面一致)。
|
|
34
|
+
|
|
35
|
+
运行:
|
|
36
|
+
python -X utf8 -m md_cg.test_p4_freshness
|
|
37
|
+
python -X utf8 -m md_cg.test_p4_freshness --mutate
|
|
38
|
+
python -X utf8 -m md_cg.test_p4_freshness --mutate --list
|
|
39
|
+
|
|
40
|
+
退出码:0 = 全绿;1 = 有失败 / 变异未按预期转红;2 = ANCHOR-MISS。
|
|
41
|
+
|
|
42
|
+
硬边界:库根一律 tempfile;不动在役数据根;不 git add/commit/push。
|
|
43
|
+
"""
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import hashlib
|
|
47
|
+
import json
|
|
48
|
+
import math
|
|
49
|
+
import os
|
|
50
|
+
import re
|
|
51
|
+
import sys
|
|
52
|
+
import tempfile
|
|
53
|
+
|
|
54
|
+
# ── 隔离前置(必须在 import md_cg 子模块之前)──
|
|
55
|
+
_TMP0 = tempfile.mkdtemp(prefix="p4_fresh_")
|
|
56
|
+
os.environ["MDCG_AUX_ROOT"] = os.path.join(_TMP0, "auxroot")
|
|
57
|
+
os.environ["MDCG_MASTER_KEY"] = "12" * 32
|
|
58
|
+
os.environ["MDCG_TOKEN_FILE"] = os.path.join(_TMP0, "tokens.json")
|
|
59
|
+
os.environ["MDCG_ROOT"] = os.path.join(_TMP0, "cgroot")
|
|
60
|
+
for _k in ("MDCG_TOKEN", "MDCG_TENANT", "MDCG_CLEARANCE", "MDCG_SESSION",
|
|
61
|
+
"MDCG_RETRIEVAL_PIPELINE", "MDCG_POOLING", "MDCG_FRESHNESS",
|
|
62
|
+
"MDCG_FRESHNESS_NOW", "MDCG_TEMPORAL_GAMMA"):
|
|
63
|
+
os.environ.pop(_k, None)
|
|
64
|
+
|
|
65
|
+
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
66
|
+
_REPO = os.path.dirname(_HERE)
|
|
67
|
+
sys.path.insert(0, _REPO)
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
71
|
+
except AttributeError:
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
from md_cg import freshness as F # noqa: E402
|
|
75
|
+
from md_cg import mdcg as M # noqa: E402
|
|
76
|
+
from md_cg import nodefile # noqa: E402
|
|
77
|
+
from md_cg import weights as W # noqa: E402
|
|
78
|
+
from md_cg.mdcos import MdCGOS # noqa: E402
|
|
79
|
+
from md_cg.whitebox_kb.aeis_core import time_core as TC # noqa: E402
|
|
80
|
+
|
|
81
|
+
REF = 1_800_000_000.0
|
|
82
|
+
DAY = 86400.0
|
|
83
|
+
|
|
84
|
+
PASS = 0
|
|
85
|
+
FAIL = 0
|
|
86
|
+
FAILS = []
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def check(name, cond, detail=""):
|
|
90
|
+
global PASS, FAIL
|
|
91
|
+
if cond:
|
|
92
|
+
PASS += 1
|
|
93
|
+
print(f" OK {name}")
|
|
94
|
+
else:
|
|
95
|
+
FAIL += 1
|
|
96
|
+
FAILS.append(name)
|
|
97
|
+
print(f" FAIL {name} {detail}")
|
|
98
|
+
return bool(cond)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
BODY = ("# 功能名:{n}\n# 生效条件:无条件\n# 子功能:无\n# 执行:占位\n"
|
|
102
|
+
"# 验证方式:compiler 复现\n# 不适用条件:其它会话\n\n{t}\n")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _mk(cg, nid, layer="knowledge", **kw):
|
|
106
|
+
cg.add(nid, BODY.format(n=nid, t=f"{nid} 正文 苹果 章节"), layer=layer, **kw)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _lib(extra=None):
|
|
110
|
+
root = tempfile.mkdtemp(prefix="p4_lib_")
|
|
111
|
+
cg = MdCGOS(root)
|
|
112
|
+
for nid, kw in (extra or (("k_new", {"importance": 0.5,
|
|
113
|
+
"created_at": REF}),
|
|
114
|
+
("k_old", {"importance": 0.5,
|
|
115
|
+
"created_at": REF - 300 * DAY}))):
|
|
116
|
+
_mk(cg, nid, **kw)
|
|
117
|
+
cg.flush()
|
|
118
|
+
return root, cg
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _src(rel):
|
|
122
|
+
with open(os.path.join(_REPO, rel), encoding="utf-8") as f:
|
|
123
|
+
return f.read()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _tree_hash(root):
|
|
127
|
+
h = hashlib.sha256()
|
|
128
|
+
for dirpath, dirs, files in os.walk(root):
|
|
129
|
+
dirs[:] = sorted(d for d in dirs if not d.startswith("_"))
|
|
130
|
+
for fn in sorted(files):
|
|
131
|
+
if fn.startswith("_"):
|
|
132
|
+
continue
|
|
133
|
+
p = os.path.join(dirpath, fn)
|
|
134
|
+
h.update(os.path.relpath(p, root).replace("\\", "/").encode())
|
|
135
|
+
with open(p, "rb") as f:
|
|
136
|
+
h.update(f.read())
|
|
137
|
+
return h.hexdigest()[:16]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _score_pair(cg, q="苹果 章节", protected=False):
|
|
141
|
+
docs = []
|
|
142
|
+
for nid, created, ac in (("k_new", REF, 0), ("k_old", REF - 300 * DAY, 0)):
|
|
143
|
+
docs.append(({"path": f"knowledge/{nid}.md"},
|
|
144
|
+
{"id": nid, "tags": [], "created_at": created,
|
|
145
|
+
"access_count": ac, "last_access": 0,
|
|
146
|
+
"importance": 0.5, "protected": protected},
|
|
147
|
+
BODY.format(n=nid, t=f"{nid} 正文 苹果 章节")))
|
|
148
|
+
return cg._score(docs, q, M.bigrams(M.normalize_en(q)))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def g1_core_authority():
|
|
152
|
+
print("== G1 核来自唯一权威 time_core(本模块无自写指数核)==")
|
|
153
|
+
sc = F.selfcheck()
|
|
154
|
+
check("freshness 源码不含指数核调用", sc["no_local_exp_core"] is True, str(sc))
|
|
155
|
+
check("核经 time_core.cred_factor", sc["uses_cred_factor"] is True)
|
|
156
|
+
g = F.resolve_gamma()
|
|
157
|
+
ok = True
|
|
158
|
+
detail = []
|
|
159
|
+
for dt in (0.0, 1.0, 7.0, 30.0, 300.0, float("inf")):
|
|
160
|
+
a = F.decay_factor(g, dt)
|
|
161
|
+
b = TC.cred_factor(g, dt, F.SCORE_FLOOR, F.SCORE_CEIL)
|
|
162
|
+
if a != b:
|
|
163
|
+
ok = False
|
|
164
|
+
detail.append((dt, a))
|
|
165
|
+
check("decay_factor ≡ time_core.cred_factor(6 个 dt 逐位相等)", ok, str(detail))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def g2_gamma_and_unit():
|
|
169
|
+
print("== G2 γ 与 Δt 口径 / 单位必须是「天」==")
|
|
170
|
+
from md_cg.mdcos import temporal_gamma
|
|
171
|
+
from md_cg.links import DECAY_DAYS
|
|
172
|
+
check("γ 缺省 = ln2/30 天",
|
|
173
|
+
abs(F.resolve_gamma() - math.log(2.0) / 30.0) < 1e-15,
|
|
174
|
+
str(F.resolve_gamma()))
|
|
175
|
+
check("γ 与 mdcos.temporal_gamma **同一读取点**(缺省同值)",
|
|
176
|
+
F.resolve_gamma() == temporal_gamma())
|
|
177
|
+
check("半衰期真源 links.DECAY_DAYS == 30", DECAY_DAYS == 30.0)
|
|
178
|
+
os.environ["MDCG_TEMPORAL_GAMMA"] = "0.5"
|
|
179
|
+
try:
|
|
180
|
+
check("env MDCG_TEMPORAL_GAMMA 同时覆盖两条路",
|
|
181
|
+
F.resolve_gamma() == 0.5 and temporal_gamma() == 0.5)
|
|
182
|
+
finally:
|
|
183
|
+
os.environ.pop("MDCG_TEMPORAL_GAMMA", None)
|
|
184
|
+
os.environ["MDCG_TEMPORAL_GAMMA"] = "abc"
|
|
185
|
+
try:
|
|
186
|
+
check("非法 γ 回落缺省(不抛)",
|
|
187
|
+
abs(F.resolve_gamma() - math.log(2.0) / 30.0) < 1e-15)
|
|
188
|
+
finally:
|
|
189
|
+
os.environ.pop("MDCG_TEMPORAL_GAMMA", None)
|
|
190
|
+
g = F.resolve_gamma()
|
|
191
|
+
check("单位断言(天):30 天 → 0.5", abs(F.decay_factor(g, 30.0) - 0.5) < 1e-12,
|
|
192
|
+
str(F.decay_factor(g, 30.0)))
|
|
193
|
+
check("判别力:同一时长按**秒**喂 → 触 floor(喂秒会快 86400 倍)",
|
|
194
|
+
F.decay_factor(g, 30.0 * DAY) == F.SCORE_FLOOR,
|
|
195
|
+
str(F.decay_factor(g, 30.0 * DAY)))
|
|
196
|
+
check("Δt 取 created_at(entry_weight 用 created_at 算天数)",
|
|
197
|
+
F.entry_weight({"created_at": REF - 30 * DAY, "importance": 0.5},
|
|
198
|
+
now=REF)[0] == F.decay_factor(g, 30.0))
|
|
199
|
+
# 粒度=整日(向下取整):同为「1.x 天」的节点与「1 天」同值;「0.x 天」不衰减
|
|
200
|
+
check("粒度断言:Δt 取整日(1.5 天 ≡ 1 天)",
|
|
201
|
+
F.entry_weight({"created_at": REF - 1.5 * DAY, "importance": 0.5},
|
|
202
|
+
now=REF)[0] == F.decay_factor(g, 1.0))
|
|
203
|
+
check("粒度断言:当日写入(0.9 天)不衰减",
|
|
204
|
+
F.entry_weight({"created_at": REF - 0.9 * DAY, "importance": 0.5},
|
|
205
|
+
now=REF)[0] == F.decay_factor(g, 0.0))
|
|
206
|
+
check("可复算:同一 (created_at, 日) 上两轮乘子逐位一致",
|
|
207
|
+
F.entry_weight({"created_at": REF - 3 * DAY - 100, "importance": 0.5},
|
|
208
|
+
now=REF)[0]
|
|
209
|
+
== F.entry_weight({"created_at": REF - 3 * DAY - 100,
|
|
210
|
+
"importance": 0.5}, now=REF + 10.0)[0])
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def g3_refresh_params():
|
|
214
|
+
print("== G3 refresh 参数照抄 AEIS consolidate_cycle ==")
|
|
215
|
+
src = _src(os.path.join("md_cg", "whitebox_kb", "aeis_core", "core.py"))
|
|
216
|
+
check("AEIS 源码含 rehearsal_threshold = 0.7",
|
|
217
|
+
re.search(r"rehearsal_threshold\s*:\s*float\s*=\s*0\.7", src)
|
|
218
|
+
is not None)
|
|
219
|
+
check("AEIS 源码含 degrade_threshold = 0.2",
|
|
220
|
+
re.search(r"degrade_threshold\s*:\s*float\s*=\s*0\.2", src)
|
|
221
|
+
is not None)
|
|
222
|
+
check("AEIS 源码含 gain = 0.01",
|
|
223
|
+
re.search(r"gain\s*:\s*float\s*=\s*0\.01", src) is not None)
|
|
224
|
+
check("AEIS 源码的降权是 −gain(同一档语义)",
|
|
225
|
+
re.search(r"update_node_importance\(n\.id,\s*-gain\)", src)
|
|
226
|
+
is not None)
|
|
227
|
+
check("AEIS 源码含 `% 10 == 0` 档",
|
|
228
|
+
re.search(r"%\s*10\s*==\s*0", src) is not None)
|
|
229
|
+
p = F.REFRESH_PARAMS
|
|
230
|
+
check("参数取值与 AEIS 既有取值逐条相等",
|
|
231
|
+
(p["rehearsal_threshold"], p["degrade_threshold"], p["gain"],
|
|
232
|
+
p["cycle"]) == (0.7, 0.2, 0.01, 10), str(p))
|
|
233
|
+
check("参数出处写在 paramsSource(REFRESH_PARAMS.source)",
|
|
234
|
+
"consolidate_cycle" in p["source"] and "core.py" in p["source"],
|
|
235
|
+
p["source"])
|
|
236
|
+
check("行为档①演练:+gain", abs(F.refresh_factor(40, 1.0, 0.8) - 1.01) < 1e-12)
|
|
237
|
+
check("行为档②降权:−gain", abs(F.refresh_factor(1, 1.0, 0.1) - 0.99) < 1e-12)
|
|
238
|
+
check("行为档③其余:1.0", F.refresh_factor(3, 1.0, 0.5) == 1.0)
|
|
239
|
+
check("从未访问不得白拿刷新(last_access=0 ⇒ 1.0)",
|
|
240
|
+
F.refresh_factor(0, 0, 0.9) == 1.0)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def g4_floor():
|
|
244
|
+
print("== G4 衰减必加 floor / 保护线不被穿过 ==")
|
|
245
|
+
g = F.resolve_gamma()
|
|
246
|
+
check("老节点 ≥ SCORE_FLOOR",
|
|
247
|
+
F.decay_factor(g, 10000.0) >= F.SCORE_FLOOR)
|
|
248
|
+
check("缺 created_at 的节点仍带非零分(不乘成 0)",
|
|
249
|
+
F.entry_weight({"created_at": 0, "importance": 0.5})[0] > 0.0,
|
|
250
|
+
str(F.entry_weight({"created_at": 0, "importance": 0.5})[0]))
|
|
251
|
+
check("缺 created_at 的衰减项触 floor(decay 本身 = SCORE_FLOOR)",
|
|
252
|
+
F.decay_factor(g, float("inf")) == F.SCORE_FLOOR)
|
|
253
|
+
for imp in (0.70, 0.8, 0.95, 1.0):
|
|
254
|
+
w = F.entry_weight({"created_at": REF - 3650 * DAY,
|
|
255
|
+
"importance": imp, "protected": True}, now=REF)[0]
|
|
256
|
+
if not (imp * w >= W.IMPORTANCE_PROTECT):
|
|
257
|
+
check(f"受保护节点 importance={imp} 乘子不穿保护线", False,
|
|
258
|
+
f"imp*w={imp * w}")
|
|
259
|
+
return
|
|
260
|
+
check("受保护节点:importance × 乘子 ≥ IMPORTANCE_PROTECT(跨越 1/10/100 个半衰期)",
|
|
261
|
+
True)
|
|
262
|
+
w_free = F.entry_weight({"created_at": REF - 3650 * DAY,
|
|
263
|
+
"importance": 0.8}, now=REF)[0]
|
|
264
|
+
check("判别力:未保护节点不享有该下界(乘子触 floor)",
|
|
265
|
+
abs(w_free - F.SCORE_FLOOR) < 1e-12, str(w_free))
|
|
266
|
+
check("保护下限口径取自 weights.IMPORTANCE_PROTECT(单一真源)",
|
|
267
|
+
F.IMPORTANCE_PROTECT == W.IMPORTANCE_PROTECT == 0.70)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def g5_same_multiplier_slot():
|
|
271
|
+
print("== G5 落位:与分池权重同一乘子位 ==")
|
|
272
|
+
root, cg = _lib()
|
|
273
|
+
try:
|
|
274
|
+
os.environ["MDCG_FRESHNESS_NOW"] = repr(REF)
|
|
275
|
+
src = _src(os.path.join("md_cg", "mdcg.py"))
|
|
276
|
+
lines = src.splitlines()
|
|
277
|
+
i = next(n for n, l in enumerate(lines)
|
|
278
|
+
if l.startswith(" def _score(self, docs, q, qb"))
|
|
279
|
+
seg = lines[i:i + 120]
|
|
280
|
+
jd = next(n for n, l in enumerate(seg)
|
|
281
|
+
if "raw * pooling.weight_of(" in l)
|
|
282
|
+
kd = next(n for n, l in enumerate(seg)
|
|
283
|
+
if "raw * freshness.entry_weight(" in l)
|
|
284
|
+
check("乘子行与分池权重行同处一个语句块(中间只有注释、无空行)",
|
|
285
|
+
0 < kd - jd <= 12
|
|
286
|
+
and all(l.strip() for l in seg[jd + 1:kd]),
|
|
287
|
+
f"pooling 行={jd} 乘子行={kd}")
|
|
288
|
+
check("乘子直接乘在 raw 上(同一乘子位,不新开乘子链)",
|
|
289
|
+
"raw * freshness.entry_weight(e, fm=fm)[0]" in seg[kd])
|
|
290
|
+
# 行为:同内容两节点,仅 created_at 不同 → 分数比 == 乘子比
|
|
291
|
+
scored = _score_pair(cg)
|
|
292
|
+
m_new = F.entry_weight(cg.index["nodes"]["k_new"], now=REF)[0]
|
|
293
|
+
m_old = F.entry_weight({"created_at": REF - 300 * DAY,
|
|
294
|
+
"importance": 0.5}, now=REF)[0]
|
|
295
|
+
got = scored[0][1] / scored[1][1]
|
|
296
|
+
check("_score 的同内容两节点分数比 == 乘子比(乘子确实进了主分数)",
|
|
297
|
+
abs(got - (1.0 * m_new) / (1.0 * m_old)) < 1e-9,
|
|
298
|
+
f"got={got} want={m_new / m_old}")
|
|
299
|
+
check("旧节点被衰减(ratio > 1 且旧节点分更低)",
|
|
300
|
+
scored[0][1] > scored[1][1], str(scored))
|
|
301
|
+
finally:
|
|
302
|
+
os.environ.pop("MDCG_FRESHNESS_NOW", None)
|
|
303
|
+
cg.close()
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def g6_index_entry_data():
|
|
307
|
+
print("== G6 取数:access_count / last_access 进索引条目(检索期不读盘)==")
|
|
308
|
+
root, cg = _lib()
|
|
309
|
+
try:
|
|
310
|
+
e = cg.index["nodes"]["k_new"]
|
|
311
|
+
check("条目含 access_count", "access_count" in e, str(sorted(e)[:5]))
|
|
312
|
+
check("条目含 last_access", "last_access" in e)
|
|
313
|
+
check("条目含 created_at(Δt 的输入)", e.get("created_at") == REF,
|
|
314
|
+
str(e.get("created_at")))
|
|
315
|
+
w, why = F.entry_weight(e)
|
|
316
|
+
check("只凭条目即可算乘子(entry_weight 无 IO)",
|
|
317
|
+
isinstance(w, float) and why.get("unit") == F.DT_UNIT, str(why))
|
|
318
|
+
check("单位依据如实标为 day", why.get("unit") == "day")
|
|
319
|
+
finally:
|
|
320
|
+
cg.close()
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def g7_switch():
|
|
324
|
+
print("== G7 开关可回退(MDCG_FRESHNESS=0 与基线逐位一致)==")
|
|
325
|
+
root, cg = _lib()
|
|
326
|
+
try:
|
|
327
|
+
os.environ["MDCG_FRESHNESS_NOW"] = repr(REF)
|
|
328
|
+
on = [round(s, 12) for _d, s in _score_pair(cg)]
|
|
329
|
+
os.environ["MDCG_FRESHNESS"] = "0"
|
|
330
|
+
off = [round(s, 12) for _d, s in _score_pair(cg)]
|
|
331
|
+
os.environ.pop("MDCG_FRESHNESS", None)
|
|
332
|
+
check("关时乘子恒 1.0",
|
|
333
|
+
F.entry_weight(cg.index["nodes"]["k_new"])[0] == 1.0
|
|
334
|
+
if os.environ.get("MDCG_FRESHNESS") == "0" else True)
|
|
335
|
+
check("开/关两态下分数不同(开关真的有作用)", on != off,
|
|
336
|
+
f"{on} vs {off}")
|
|
337
|
+
check("关态 = 无乘子基线(两节点分数相同)", abs(off[0] - off[1]) < 1e-12,
|
|
338
|
+
str(off))
|
|
339
|
+
finally:
|
|
340
|
+
os.environ.pop("MDCG_FRESHNESS", None)
|
|
341
|
+
os.environ.pop("MDCG_FRESHNESS_NOW", None)
|
|
342
|
+
cg.close()
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def g8_triplet():
|
|
346
|
+
print("== G8 三件套:预演 / 逐节点留痕 / 反向 apply ==")
|
|
347
|
+
root, cg = _lib()
|
|
348
|
+
try:
|
|
349
|
+
os.environ["MDCG_FRESHNESS_NOW"] = repr(REF)
|
|
350
|
+
src_a = _tree_hash(root)
|
|
351
|
+
pre = F.recalc(cg, apply=False)
|
|
352
|
+
check("预演:dry_run=True 且零写盘(源面逐字节不变)",
|
|
353
|
+
pre["dry_run"] is True and _tree_hash(root) == src_a, str(pre["written"]))
|
|
354
|
+
check("预演给出逐节点 before/after 样本", bool(pre["samples"]),
|
|
355
|
+
str(pre["samples"][:1]))
|
|
356
|
+
check("预演不改 frontmatter",
|
|
357
|
+
(cg.get("k_old")["frontmatter"].get("freshness_weight") is None))
|
|
358
|
+
res = F.recalc(cg, apply=True, min_delta=0.0)
|
|
359
|
+
check("apply:written ≥ 1", res["written"] >= 1, str(res["written"]))
|
|
360
|
+
fm = cg.get("k_old")["frontmatter"]
|
|
361
|
+
check("apply 写入 freshness_weight(与乘子一致)",
|
|
362
|
+
abs(float(fm.get("freshness_weight")) -
|
|
363
|
+
F.entry_weight({"created_at": REF - 300 * DAY,
|
|
364
|
+
"importance": 0.5}, now=REF)[0]) < 1e-9,
|
|
365
|
+
str(fm.get("freshness_weight")))
|
|
366
|
+
log = os.path.join(cg.root, W.MAINTAIN_LOG)
|
|
367
|
+
recs = [json.loads(x) for x in
|
|
368
|
+
open(log, encoding="utf-8").read().splitlines() if x.strip()]
|
|
369
|
+
mine = [r for r in recs if r.get("action") == "freshness"]
|
|
370
|
+
check("逐节点留痕:_maintain.jsonl 有 action=freshness 记录",
|
|
371
|
+
bool(mine), str([r.get("action") for r in recs]))
|
|
372
|
+
check("留痕含 before/after(逐节点)",
|
|
373
|
+
all(("before" in r and "after" in r) for r in mine),
|
|
374
|
+
json.dumps(mine[:1], ensure_ascii=False))
|
|
375
|
+
check("与 weights 的 action=importance 同族(同一 log 文件)",
|
|
376
|
+
W.MAINTAIN_LOG == F.MAINTAIN_LOG == "_maintain.jsonl")
|
|
377
|
+
rb = F.rollback(cg)
|
|
378
|
+
check("rollback:reverted ≥ 1 且写回 before", rb["reverted"] >= 1, str(rb))
|
|
379
|
+
fm2 = cg.get("k_old")["frontmatter"]
|
|
380
|
+
check("rollback 后 freshness_weight 回到 before",
|
|
381
|
+
abs(float(fm2.get("freshness_weight") or 0.0) - 1.0) < 1e-9,
|
|
382
|
+
str(fm2.get("freshness_weight")))
|
|
383
|
+
_p = (cg.index["nodes"].get("k_old") or {}).get("path") or ""
|
|
384
|
+
check("rollback 后节点仍在索引(不删记录)",
|
|
385
|
+
"k_old" in cg.index["nodes"] and bool(_p)
|
|
386
|
+
and os.path.exists(os.path.join(cg.root, _p)), _p)
|
|
387
|
+
check("rollback 自身再记一条留痕",
|
|
388
|
+
any(r.get("action") == "freshness_rollback"
|
|
389
|
+
for r in [json.loads(x) for x in
|
|
390
|
+
open(log, encoding="utf-8").read().splitlines()
|
|
391
|
+
if x.strip()]))
|
|
392
|
+
finally:
|
|
393
|
+
os.environ.pop("MDCG_FRESHNESS_NOW", None)
|
|
394
|
+
cg.close()
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def g9_degrade_not_delete():
|
|
398
|
+
print("== G9 降级 = 标记(不是删除);跨层降级走 guard_move ==")
|
|
399
|
+
root = tempfile.mkdtemp(prefix="p4_deg_")
|
|
400
|
+
cg = MdCGOS(root)
|
|
401
|
+
try:
|
|
402
|
+
_mk(cg, "k_prot", importance=0.8) # ≥0.70 ⇒ 自动打保护
|
|
403
|
+
_mk(cg, "k_plain", importance=0.5, created_at=REF - 300 * DAY)
|
|
404
|
+
cg.flush()
|
|
405
|
+
os.environ["MDCG_FRESHNESS_NOW"] = repr(REF)
|
|
406
|
+
e0 = dict(cg.index["nodes"]["k_prot"])
|
|
407
|
+
check("夹具前提:节点受保护", bool(e0.get("protected")),
|
|
408
|
+
str(e0.get("protected")))
|
|
409
|
+
check("bind_move_layer 现绑既有 `_move_layer`(不新写搬迁路径)",
|
|
410
|
+
F.bind_move_layer(cg) is True)
|
|
411
|
+
res = F.recalc(cg, apply=True, min_delta=0.0)
|
|
412
|
+
fm_p = cg.get("k_plain")["frontmatter"]
|
|
413
|
+
_pp = (cg.index["nodes"].get("k_plain") or {}).get("path") or ""
|
|
414
|
+
check("衰减到阈值下的节点被标 degraded(**只标记**)",
|
|
415
|
+
fm_p.get("freshness_state") == "degraded", str(fm_p.get(
|
|
416
|
+
"freshness_state")))
|
|
417
|
+
check("被标 degraded 的节点与正文原样在库(降级 ≠ 删除记录)",
|
|
418
|
+
os.path.exists(os.path.join(cg.root, _pp))
|
|
419
|
+
and "k_plain" in cg.index["nodes"], _pp)
|
|
420
|
+
check("受保护节点**不被**降级(保护下界生效)",
|
|
421
|
+
(cg.get("k_prot")["frontmatter"].get("freshness_state")
|
|
422
|
+
in (None, "")), str(cg.get("k_prot")["frontmatter"].get(
|
|
423
|
+
"freshness_state")))
|
|
424
|
+
check("报表给出降级计数", "degraded" in res, str(sorted(res)))
|
|
425
|
+
# 跨层降级:受保护节点被移出保护层 → guard_move 抛错且无副作用
|
|
426
|
+
import md_cg.protect as P
|
|
427
|
+
before = cg.get("k_prot")["frontmatter"].get("layer")
|
|
428
|
+
try:
|
|
429
|
+
cg._move_layer("k_prot", "contextual", reason="guard 试")
|
|
430
|
+
moved = True
|
|
431
|
+
except P.ProtectionError:
|
|
432
|
+
moved = False
|
|
433
|
+
check("受保护节点降级被 guard_move 拒绝(ProtectionError)", moved is False)
|
|
434
|
+
check("拒绝后节点层位与记录未变(无副作用)",
|
|
435
|
+
cg.get("k_prot")["frontmatter"].get("layer") == before
|
|
436
|
+
and "k_prot" in cg.index["nodes"])
|
|
437
|
+
check("fixture 未因降级被删除(降级 ≠ 删除记录)",
|
|
438
|
+
"k_prot" in cg.index["nodes"] and "k_plain" in cg.index["nodes"])
|
|
439
|
+
check("非保护节点可被搬迁(同一闸的两侧对照)",
|
|
440
|
+
isinstance(cg._move_layer("k_plain", "contextual",
|
|
441
|
+
reason="guard 对照"), dict))
|
|
442
|
+
finally:
|
|
443
|
+
os.environ.pop("MDCG_FRESHNESS_NOW", None)
|
|
444
|
+
cg.close()
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def g10_weights_autoprotect_kept():
|
|
448
|
+
print("== G10 weights.recalc 的 ≥0.70 自动补写 protected 仍在位 ==")
|
|
449
|
+
src = _src(os.path.join("md_cg", "weights.py"))
|
|
450
|
+
check("weights.py 仍含 IMPORTANCE_PROTECT 常量与自动补写分支",
|
|
451
|
+
"IMPORTANCE_PROTECT = 0.70" in src
|
|
452
|
+
and 'fm["protected"] = True' in src)
|
|
453
|
+
root = tempfile.mkdtemp(prefix="p4_w_")
|
|
454
|
+
cg = MdCGOS(root)
|
|
455
|
+
try:
|
|
456
|
+
_mk(cg, "hub", importance=0.2, verification_basis="formal_proof")
|
|
457
|
+
for i in range(12):
|
|
458
|
+
cg.add(f"leaf{i}", BODY.format(n=f"leaf{i}", t=f"leaf{i} 正文"),
|
|
459
|
+
layer="knowledge", importance=0.1,
|
|
460
|
+
edges=[{"target": "hub", "relation_type": "causal"}])
|
|
461
|
+
cg.flush()
|
|
462
|
+
rep = W.recalc(cg, apply=True, min_delta=0.0)
|
|
463
|
+
fm = cg.get("hub")["frontmatter"]
|
|
464
|
+
check("重算后 after ≥ 0.70 ⇒ 自动补写 protected",
|
|
465
|
+
float(fm.get("importance") or 0) >= W.IMPORTANCE_PROTECT
|
|
466
|
+
and fm.get("protected") is True,
|
|
467
|
+
f"imp={fm.get('importance')} protected={fm.get('protected')}")
|
|
468
|
+
check("recalc 报表结构未变(预演/留痕/回滚三件套仍在)",
|
|
469
|
+
{"dry_run", "samples", "log"} <= set(rep), str(sorted(rep)))
|
|
470
|
+
finally:
|
|
471
|
+
cg.close()
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def g11_rust_parity():
|
|
475
|
+
print("== G11 Rust 侧同口径(常量 + 生效面)==")
|
|
476
|
+
rs = _src(os.path.join("rust", "src", "freshness.rs"))
|
|
477
|
+
for name, val in (("SCORE_FLOOR", "0.001"), ("SECONDS_PER_DAY", "86400.0"),
|
|
478
|
+
("IMPORTANCE_PROTECT", "0.70"), ("DECAY_DAYS", "30.0")):
|
|
479
|
+
check(f"rust 常量 {name} 与 Python 同值",
|
|
480
|
+
f"pub const {name}: f64 = {val};" in rs, name)
|
|
481
|
+
for name, val in (("REHEARSAL_THRESHOLD", "0.7"),
|
|
482
|
+
("DEGRADE_THRESHOLD", "0.2"), ("GAIN", "0.01")):
|
|
483
|
+
check(f"rust 参数 {name} 照抄 AEIS({val})",
|
|
484
|
+
f"const {name}: f64 = {val};" in rs, name)
|
|
485
|
+
ret = _src(os.path.join("rust", "src", "retrieval.rs"))
|
|
486
|
+
check("sort_path 带 apply_freshness 开关",
|
|
487
|
+
"pub fn sort_path(hits: &mut [Hit], docs: &[Option<Doc>],"
|
|
488
|
+
" apply_freshness: bool)" in ret)
|
|
489
|
+
check("fuse 只对 lexical 路开乘子(与 Python `_score` 对位)",
|
|
490
|
+
'sort_path(&mut h, docs, *name == "lexical")' in ret)
|
|
491
|
+
eng = _src(os.path.join("rust", "src", "engine.rs"))
|
|
492
|
+
check("graph 种子排序带乘子(种子取自 lexical 路)",
|
|
493
|
+
"retrieval::sort_path(&mut v, docs, true);" in eng)
|
|
494
|
+
check("Rust 侧有核/参数的单测(半衰期、floor、保护线、演练闸)",
|
|
495
|
+
rs.count("#[test]") >= 4, str(rs.count("#[test]")))
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
# --------------------------------------------------------------------------
|
|
499
|
+
# 定点变异(只改本进程内存对象/临时文件,绝不改仓库文件)
|
|
500
|
+
# --------------------------------------------------------------------------
|
|
501
|
+
|
|
502
|
+
def g12_rust_parity_e2e():
|
|
503
|
+
"""P4-2② 双侧同改的**端到端逐位对拍**(判别力夹具,非 rank_parity 现有语料)。
|
|
504
|
+
|
|
505
|
+
为什么必须另造夹具:`scripts/rank_parity.py` 的语料是**新建节点**(Δt=0 天
|
|
506
|
+
⇒ 乘子恒 1.0)、无 edges/无时间参数/无六要素后两行 ⇒ 对 P4 乘子**零判别力**
|
|
507
|
+
(实测:改前 10/10、改后 10/10、改后 Python + 改前 Rust 二进制亦 10/10)。
|
|
508
|
+
只有把「同分不同龄」的节点放进去,乘子才改变路内名次,两侧才可能劈叉。
|
|
509
|
+
|
|
510
|
+
夹具:6 个**正文逐字相同**(词法分并列)的节点,created_at 跨度 0/30/300 天,
|
|
511
|
+
importance 随年龄递增(未乘者按 (-score,-importance,id) 会「越老越靠前」);
|
|
512
|
+
`MDCG_FRESHNESS_NOW` 钉死 → 两侧同一参照时刻。断言:
|
|
513
|
+
① 改后两侧 top-k **逐位一致**(Python `search_rrf` vs Rust `--serve`);
|
|
514
|
+
② 夹具**有判别力**:把 Python 侧乘子关掉,名次与 ① 不同(否则本对拍空转)。
|
|
515
|
+
二进制缺失 = fail-closed 红(「跑不起来」永不算通过)。
|
|
516
|
+
"""
|
|
517
|
+
exe = os.path.join(_REPO, "rust", "target", "release",
|
|
518
|
+
"mdcg-eval.exe" if os.name == "nt" else "mdcg-eval")
|
|
519
|
+
if not os.path.isfile(exe):
|
|
520
|
+
check("Rust 二进制在位(逐位对拍的另一侧)", False,
|
|
521
|
+
f"{exe} 不存在——先 cd rust && cargo build --release")
|
|
522
|
+
return
|
|
523
|
+
sys.path.insert(0, os.path.join(_REPO, "scripts"))
|
|
524
|
+
import rank_parity as RP # noqa: E402
|
|
525
|
+
py_mode, conflict = RP.pin_python_score_mode()
|
|
526
|
+
if py_mode != "jaccard" or conflict:
|
|
527
|
+
check("Python 侧口径可钉住(eval_common.use_jaccard)", False,
|
|
528
|
+
f"{py_mode} / {conflict}")
|
|
529
|
+
return
|
|
530
|
+
root = tempfile.mkdtemp(prefix="p4_parity_")
|
|
531
|
+
cg = MdCGOS(root)
|
|
532
|
+
ages = (0.0, 0.0, 30.0, 30.0, 300.0, 300.0)
|
|
533
|
+
for i, age in enumerate(ages):
|
|
534
|
+
# importance 全部 < 0.70(避开自动保护 → 不受保护下界豁免衰减)
|
|
535
|
+
# 且全部 ≥ 0.20(避开 AEIS 降权档),年龄越大 importance 越高 ⇒
|
|
536
|
+
# 「未乘者越老越靠前」,乘子一到即翻转
|
|
537
|
+
_mk(cg, f"p{i}", created_at=REF - age * DAY,
|
|
538
|
+
importance=0.20 + 0.04 * i, tags=[])
|
|
539
|
+
cg.flush()
|
|
540
|
+
q = "苹果 章节"
|
|
541
|
+
os.environ["MDCG_FRESHNESS_NOW"] = repr(REF)
|
|
542
|
+
try:
|
|
543
|
+
py = [(x[0]["id"], round(float(x[1]), 6))
|
|
544
|
+
for x in cg.search_rrf(q, k=6, record=False)[0]]
|
|
545
|
+
os.environ["MDCG_FRESHNESS"] = "0"
|
|
546
|
+
off = [(x[0]["id"], round(float(x[1]), 6))
|
|
547
|
+
for x in cg.search_rrf(q, k=6, record=False)[0]]
|
|
548
|
+
os.environ.pop("MDCG_FRESHNESS", None)
|
|
549
|
+
try:
|
|
550
|
+
proc, rs_mode = RP.rust_open(exe, root)
|
|
551
|
+
except Exception as exc: # noqa: BLE001
|
|
552
|
+
check("Rust serve 可拉起且自报口径", False, str(exc)[:200])
|
|
553
|
+
cg.close()
|
|
554
|
+
return
|
|
555
|
+
try:
|
|
556
|
+
rs = RP.rust_topk(proc, [q], k=6)[q]
|
|
557
|
+
finally:
|
|
558
|
+
pass
|
|
559
|
+
rs2 = [(i, None if s is None else round(float(s), 6)) for i, s in rs]
|
|
560
|
+
print(f" py(new)={py}")
|
|
561
|
+
print(f" rust ={rs2}")
|
|
562
|
+
print(f" py(off)={off}")
|
|
563
|
+
check("① 两侧 top-k 逐位一致(同乘子、同口径)",
|
|
564
|
+
[i for i, _s in py] == [i for i, _s in rs2],
|
|
565
|
+
f"{[i for i, _s in py]} vs {[i for i, _s in rs2]}")
|
|
566
|
+
check("② 夹具判别力:关掉乘子后名次不同(本对拍非空转)",
|
|
567
|
+
[i for i, _s in py] != [i for i, _s in off],
|
|
568
|
+
f"{[i for i, _s in py]} vs {[i for i, _s in off]}")
|
|
569
|
+
check("② 乘子使「新节点靠前、旧节点靠后」",
|
|
570
|
+
[i for i, _s in py][0] in ("p0", "p1")
|
|
571
|
+
and [i for i, _s in py][-1] in ("p4", "p5"),
|
|
572
|
+
f"{[i for i, _s in py]}")
|
|
573
|
+
finally:
|
|
574
|
+
os.environ.pop("MDCG_FRESHNESS_NOW", None)
|
|
575
|
+
cg.close()
|
|
576
|
+
import shutil
|
|
577
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _mut_floor_zero():
|
|
581
|
+
return _patch(F, "SCORE_FLOOR", 0.0)
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def _mut_gain_10x():
|
|
585
|
+
orig = dict(F.REFRESH_PARAMS)
|
|
586
|
+
|
|
587
|
+
def restore():
|
|
588
|
+
F.REFRESH_PARAMS.clear()
|
|
589
|
+
F.REFRESH_PARAMS.update(orig)
|
|
590
|
+
F.REFRESH_PARAMS["gain"] = 0.1
|
|
591
|
+
return restore
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _mut_unit_seconds():
|
|
595
|
+
return _patch(F, "SECONDS_PER_DAY", 1.0)
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def _mut_protect_floor_off():
|
|
599
|
+
orig = F.protected_floor
|
|
600
|
+
F.protected_floor = lambda entry: F.SCORE_FLOOR
|
|
601
|
+
|
|
602
|
+
def restore():
|
|
603
|
+
F.protected_floor = orig
|
|
604
|
+
return restore
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def _mut_switch_off():
|
|
608
|
+
return _patch(F, "enabled", lambda environ=None: False)
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def _patch(mod, name, value):
|
|
612
|
+
orig = getattr(mod, name)
|
|
613
|
+
|
|
614
|
+
def restore():
|
|
615
|
+
setattr(mod, name, orig)
|
|
616
|
+
setattr(mod, name, value)
|
|
617
|
+
return restore
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
_MUTATIONS = [
|
|
621
|
+
("floor_zero", "衰减 floor 归零", _mut_floor_zero),
|
|
622
|
+
("gain_10x", "刷新步长改成 0.1(脱离 AEIS 取值)", _mut_gain_10x),
|
|
623
|
+
("unit_seconds", "Δt 单位改成「秒」", _mut_unit_seconds),
|
|
624
|
+
("protect_floor_off", "去掉受保护节点的保护下界", _mut_protect_floor_off),
|
|
625
|
+
("switch_off", "乘子开关恒关", _mut_switch_off),
|
|
626
|
+
]
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _collect():
|
|
630
|
+
global PASS, FAIL, FAILS
|
|
631
|
+
PASS, FAIL, FAILS = 0, 0, []
|
|
632
|
+
g1_core_authority()
|
|
633
|
+
g2_gamma_and_unit()
|
|
634
|
+
g3_refresh_params()
|
|
635
|
+
g4_floor()
|
|
636
|
+
g5_same_multiplier_slot()
|
|
637
|
+
g6_index_entry_data()
|
|
638
|
+
g7_switch()
|
|
639
|
+
g8_triplet()
|
|
640
|
+
g9_degrade_not_delete()
|
|
641
|
+
g10_weights_autoprotect_kept()
|
|
642
|
+
g11_rust_parity()
|
|
643
|
+
g12_rust_parity_e2e()
|
|
644
|
+
return list(FAILS)
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def main(argv):
|
|
648
|
+
if "--mutate" in argv:
|
|
649
|
+
if "--list" in argv:
|
|
650
|
+
for name, desc, _fn in _MUTATIONS:
|
|
651
|
+
print(f" {name:20s} {desc}")
|
|
652
|
+
return 0
|
|
653
|
+
base = _collect()
|
|
654
|
+
if base:
|
|
655
|
+
print(f"\n!! 基线不绿({len(base)} 条):{base}")
|
|
656
|
+
return 1
|
|
657
|
+
bad = []
|
|
658
|
+
for name, desc, fn in _MUTATIONS:
|
|
659
|
+
try:
|
|
660
|
+
restore = fn()
|
|
661
|
+
except AttributeError as exc:
|
|
662
|
+
print(f" ANCHOR-MISS {name}: {exc}")
|
|
663
|
+
return 2
|
|
664
|
+
got = _collect()
|
|
665
|
+
restore()
|
|
666
|
+
if got:
|
|
667
|
+
print(f" MUTATE-OK {name}({desc})→ 转红 {len(got)} 条:{got[:3]}")
|
|
668
|
+
else:
|
|
669
|
+
bad.append(name)
|
|
670
|
+
print(f" MUTATE-FAIL {name}({desc})→ 仍全绿")
|
|
671
|
+
print(f"\ntest_p4_freshness --mutate: "
|
|
672
|
+
f"{len(_MUTATIONS) - len(bad)}/{len(_MUTATIONS)} 变异按预期转红")
|
|
673
|
+
return 0 if not bad else 1
|
|
674
|
+
fails = _collect()
|
|
675
|
+
print(f"\ntest_p4_freshness: {PASS} 通过 / {FAIL} 失败")
|
|
676
|
+
return 0 if not fails else 1
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
if __name__ == "__main__":
|
|
680
|
+
sys.exit(main(sys.argv[1:]))
|