@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,968 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""时间核审计器(E5 口径)+ P3 图检索路守卫(`md_cg/test_time_core_lint.py`)。
|
|
3
|
+
|
|
4
|
+
## 为什么有这个文件
|
|
5
|
+
|
|
6
|
+
`md_cg/whitebox_kb/aeis_core/time_core.py` 的 docstring 明文点名:
|
|
7
|
+
|
|
8
|
+
全库一切时间演化必须经本模块。核形状唯一(指数族),状态 X 与衰减率 γ 按对象
|
|
9
|
+
配置;任何在本模块之外出现的衰减核实现(exp(-t/τ)、×(1-factor)、EMA 保持率)
|
|
10
|
+
都是 bug——由 tools/time_core_lint.py 按 E5 口径机械化审计。
|
|
11
|
+
|
|
12
|
+
而**该审计器此前全仓不存在**(点名了、没人实现)。本文件把它补上,并按
|
|
13
|
+
工作纪律第 18 条的口径**落在已登记的 `md_cg/` 内**而不是新开顶层目录
|
|
14
|
+
(顶层目录 = `WORKSPACE_INDEX.md` 的结构面,新增会导致 `npm run gate` 的
|
|
15
|
+
`workspace_index.py --check` 腿亮红,而新增文件本轮按契约保持未追踪)。
|
|
16
|
+
`time_core.py` 的 docstring 已在原句后补上本实现的落地路径(原句逐字保留)。
|
|
17
|
+
|
|
18
|
+
## 审计口径(E5 · 四形态)
|
|
19
|
+
|
|
20
|
+
| 形态 | 判据(正则;Python 作用在 **AST 代码段** 上——注释与 docstring 天然不入面;Rust 逐行、跳过注解行) |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `exp_kernel` | `exp(-…`(自写指数衰减核)**与点调用形态 `.exp(…)` / `.exp()`**(Rust `f64::exp`、其它语言的方法调用) |
|
|
23
|
+
| `pow_half_life` | `** (… days / DAYS / half_life …)`(幂式半衰期) |
|
|
24
|
+
| `one_minus_factor`| `1 - factor`(离散保持率) |
|
|
25
|
+
| `ema_retain` | `… * retain` / `retain * …`(EMA 保持率) |
|
|
26
|
+
|
|
27
|
+
**登记制**(E5 要求「显式登记 + 等价性断言」,不是无限豁免):`REGISTRY` 的键是
|
|
28
|
+
`(文件, 形态)`、值是 **(该文件内被接受的源码行元组, 理由)** ——同一文件同一形态可
|
|
29
|
+
登记多行,但**每一行都必须在源码里逐字命中**(否则判「登记陈化」红)。
|
|
30
|
+
|
|
31
|
+
* `md_cg/whitebox_kb/aeis_core/time_core.py` = 核权威本体,整文件豁免;
|
|
32
|
+
* `md_cg/links.py:500` `factor = 0.5 ** (days / DECAY_DAYS)` = **同族半衰期写法**
|
|
33
|
+
——显式登记在 `REGISTRY`,并由本守卫 G2 **逐点等价性断言**证明
|
|
34
|
+
`0.5**(d/DECAY_DAYS) == cred_factor(ln2/DECAY_DAYS, d)`;
|
|
35
|
+
**刻意不动它的数值行为**(P_trust 读数零位移,G2c 给读数)。
|
|
36
|
+
* `rust/src/freshness.rs:128` 与 `:139` `((-γ·Δt).exp()).max(floor).min(SCORE_CEIL)`
|
|
37
|
+
= **Rust 读侧的指数核**(`:139` 在其 `#[cfg(test)]` 半衰期断言里)——Rust 无法
|
|
38
|
+
调 Python,故「登记 + 等价证」是唯一合规路径:G11 从 **Rust 源码**解析常量与核
|
|
39
|
+
表达式,在若干 Δt 点上**逐点求值**并与
|
|
40
|
+
`time_core.cred_factor(γ, Δt, floor, ceil)` 对拍(**不只断言「常量同值」**);
|
|
41
|
+
G11e 把抽出的表达式在内存里扰动一次作**非空转反证**。
|
|
42
|
+
|
|
43
|
+
未登记的形态一律**红**(审计器非空转,G1d 用注入核证明可拦性——含独立复核抓到的
|
|
44
|
+
点调用形态 `.exp()`,那正是本守卫此前的盲区:正则只认 `exp(` 后紧跟 `-`,
|
|
45
|
+
`.exp()` 这种 Rust 写法一条都抓不到,G1c「零命中」于是靠盲区通过)。
|
|
46
|
+
|
|
47
|
+
## 守卫分组
|
|
48
|
+
|
|
49
|
+
G0 隔离与基线自证 · G1 E5 审计(含可拦性 + 对真实 Rust 核字面形态的定点证据 G1f)·
|
|
50
|
+
G2 links 同族等价性 + P_trust 零位移 ·
|
|
51
|
+
G3 **单位断言**(秒/天两喂法必须不同且天制正确)· G4 floor(含 300 天读数)·
|
|
52
|
+
G5 γ 缺省与可配 · G6 时间路接线(行为:路径分 = 天制 cred_factor)·
|
|
53
|
+
G7 **因果路两配置读数**(默认集 0 → 显式含 reference/part_of >0)+
|
|
54
|
+
S3 契约不变量 · G8 缺省与分路开关(mdcos 缺省集 + mcp_server 调用点)·
|
|
55
|
+
G9 N137 搬迁后边集合逐项相等 · G10 双侧同改披露面(键定义一致 + Rust 缺省路集差异)·
|
|
56
|
+
G11 **Rust 侧指数核登记**(常量单源 + 核表达式从源码抽出逐点等价 + 非空转反证)。
|
|
57
|
+
|
|
58
|
+
运行:
|
|
59
|
+
python -X utf8 -m md_cg.test_time_core_lint
|
|
60
|
+
python -X utf8 -m md_cg.test_time_core_lint --mutate
|
|
61
|
+
python -X utf8 -m md_cg.test_time_core_lint --mutate --list
|
|
62
|
+
|
|
63
|
+
退出码(fail-closed):0 全绿;1 有断言失败 / 变异未按预期转红;2 ANCHOR-MISS
|
|
64
|
+
(变异锚点在源码里找不到——实现改了却没同步本表,锚点漂移必须硬失败)。
|
|
65
|
+
"""
|
|
66
|
+
from __future__ import annotations
|
|
67
|
+
|
|
68
|
+
import ast
|
|
69
|
+
import contextlib
|
|
70
|
+
import io
|
|
71
|
+
import math
|
|
72
|
+
import os
|
|
73
|
+
import re
|
|
74
|
+
import shutil
|
|
75
|
+
import subprocess
|
|
76
|
+
import sys
|
|
77
|
+
import tempfile
|
|
78
|
+
import time
|
|
79
|
+
|
|
80
|
+
from . import chain, links, nodefile
|
|
81
|
+
from .fsutil import atomic_write
|
|
82
|
+
from .mdcos import (MdCGOS, TEMPORAL_GAMMA_ENV, TEMPORAL_SCORE_FLOOR,
|
|
83
|
+
temporal_gamma)
|
|
84
|
+
from .whitebox_kb.aeis_core import time_core as TC
|
|
85
|
+
|
|
86
|
+
_PASS, _FAIL, _SKIP = [], [], []
|
|
87
|
+
_SRC = {} # 变异模式下供静态组读取的源码文本(rel → text)
|
|
88
|
+
_REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def ok(cond, msg, extra=""):
|
|
92
|
+
(_PASS if cond else _FAIL).append(msg)
|
|
93
|
+
print((" PASS " if cond else " FAIL ") + msg
|
|
94
|
+
+ ((" ← " + str(extra)) if (extra and not cond) else ""))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ---------------------------------------------------------------- E5 审计器
|
|
98
|
+
|
|
99
|
+
#: 审计面之外:核权威本体 + 本守卫自身(本文件的 PATTERNS 文本即含这些字样)
|
|
100
|
+
_EXEMPT = ("md_cg/whitebox_kb/aeis_core/time_core.py",
|
|
101
|
+
"md_cg/test_time_core_lint.py")
|
|
102
|
+
_SKIP_DIRS = {".git", "__pycache__", "node_modules", "target", "lib",
|
|
103
|
+
"build", "dist", ".venv", ".mypy_cache", ".zcode"}
|
|
104
|
+
|
|
105
|
+
#: 四形态(E5):名 → (正则, 人类可读说明)
|
|
106
|
+
#:
|
|
107
|
+
#: `exp_kernel` 的两条分支缺一不可:
|
|
108
|
+
#: ① `(?:math\.)?\bexp\s*\(\s*-` —— 原有的「调用式」形态(`exp(-…`、`math.exp(-…`);
|
|
109
|
+
#: ② `\.exp\s*\(` —— **点调用**形态(Rust `((-γ·Δt).exp())`、其它语言的方法调用)。
|
|
110
|
+
#: ② 是补的盲区:原正则只认 `exp(` 之后紧跟 `-`,而 Rust 写 `.exp()`(括号内为空,
|
|
111
|
+
#: 负号在**括号外**)——`rust/src/freshness.rs` 的两处指数核因此一条都抓不到,
|
|
112
|
+
#: G1c 的「零命中」是靠盲区通过的(G1d 现在把点调用形态一并注入作可拦性读数)。
|
|
113
|
+
#: 放宽后**仍要求**①②的命中落进 `REGISTRY` 逐字登记,故不是「放水」而是「入面」。
|
|
114
|
+
PATTERNS = (
|
|
115
|
+
("exp_kernel",
|
|
116
|
+
re.compile(r"(?:math\.)?\bexp\s*\(\s*-|\.exp\s*\("),
|
|
117
|
+
"本模块外的指数衰减核 exp(-…)(调用式)/.exp(…) / .exp()(点调用)"),
|
|
118
|
+
("pow_half_life",
|
|
119
|
+
re.compile(r"\*\*\s*\([^)]*\b(?:days|DAYS|half_life|halflife|HALF_LIFE)\b"),
|
|
120
|
+
"幂式半衰期 0.5 ** (Δt/D)"),
|
|
121
|
+
("one_minus_factor", re.compile(r"1(?:\.0)?\s*-\s*factor"),
|
|
122
|
+
"离散保持率 (1-factor)"),
|
|
123
|
+
("ema_retain", re.compile(r"\*\s*retain\b|\bretain\b\s*\*"),
|
|
124
|
+
"EMA 保持率 x·retain"),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
#: **登记表**:合法出现的同族写法 (文件, 形态) → (该文件内被接受的**源码行元组**, 理由)。
|
|
128
|
+
#: 键里的文件同一形态可登记**多行**(一个文件内可能有多处同族实现,如 Rust 侧的
|
|
129
|
+
#: 生产核 + 其 `#[cfg(test)]` 半衰期断言);每一行都必须在源码里**逐字命中**
|
|
130
|
+
#: (否则判「登记陈化」红)——登记表不得空转。
|
|
131
|
+
REGISTRY = {
|
|
132
|
+
("md_cg/links.py", "pow_half_life"): (
|
|
133
|
+
("0.5 ** (days / DECAY_DAYS)",),
|
|
134
|
+
"同族半衰期(半衰期 30 天 = links.DECAY_DAYS):与 "
|
|
135
|
+
"cred_factor(ln2/DECAY_DAYS, d) 数值等价(G2 逐点断言);"
|
|
136
|
+
"**数值行为刻意不变**(P_trust 读数零位移)。"),
|
|
137
|
+
("rust/src/freshness.rs", "exp_kernel"): (
|
|
138
|
+
("let decay = ((-gamma() * dt_days).exp()).max(floor).min(SCORE_CEIL);",
|
|
139
|
+
"let f = ((-g * DECAY_DAYS).exp()).max(SCORE_FLOOR).min(SCORE_CEIL);"),
|
|
140
|
+
"Rust 读侧的指数核(`factor()` 的乘子本体 + 其 `#[cfg(test)]` 里的半衰期"
|
|
141
|
+
"断言)——Rust 无法调 Python,故按 E5 的「登记 + 等价证」路径:G11 从 "
|
|
142
|
+
"**Rust 源码**解析常量与核表达式,在若干 Δt 点上逐点求值并与 "
|
|
143
|
+
"cred_factor(γ, Δt, floor, ceil) 对拍(G11d),反证见 G11e。"),
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _reg_lines():
|
|
148
|
+
"""登记表登记的**源码行总数**(G1b 的判据面:每一行都要逐字命中)。"""
|
|
149
|
+
return sum(len(lines) for lines, _why in REGISTRY.values())
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _iter_files(root):
|
|
154
|
+
"""审计面:root 下所有 .py / .rs(跳过 _SKIP_DIRS)→ [(rel, abspath)]。"""
|
|
155
|
+
out = []
|
|
156
|
+
for dp, dn, fn in os.walk(root):
|
|
157
|
+
dn[:] = [d for d in dn if d not in _SKIP_DIRS]
|
|
158
|
+
for f in sorted(fn):
|
|
159
|
+
if not (f.endswith(".py") or f.endswith(".rs")):
|
|
160
|
+
continue
|
|
161
|
+
p = os.path.join(dp, f)
|
|
162
|
+
rel = os.path.relpath(p, root).replace("\\", "/")
|
|
163
|
+
out.append((rel, p))
|
|
164
|
+
return sorted(out)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _py_hits(rel, text):
|
|
168
|
+
"""Python:只在 **AST 代码段** 上匹配(注释 / docstring 天然不入面)。"""
|
|
169
|
+
hits = []
|
|
170
|
+
try:
|
|
171
|
+
tree = ast.parse(text)
|
|
172
|
+
except SyntaxError as e: # 语法坏 → 显式报,不静默跳过
|
|
173
|
+
return [(rel, int(getattr(e, "lineno", 0) or 0), "<syntax error>",
|
|
174
|
+
"parse_error")]
|
|
175
|
+
seen = set()
|
|
176
|
+
for node in ast.walk(tree):
|
|
177
|
+
if not isinstance(node, (ast.Call, ast.BinOp)):
|
|
178
|
+
continue
|
|
179
|
+
seg = (ast.get_source_segment(text, node) or "").split("\n")[0].strip()
|
|
180
|
+
if not seg:
|
|
181
|
+
continue
|
|
182
|
+
for name, rx, _d in PATTERNS:
|
|
183
|
+
if rx.search(seg) and (node.lineno, name) not in seen:
|
|
184
|
+
seen.add((node.lineno, name))
|
|
185
|
+
hits.append((rel, node.lineno, seg, name))
|
|
186
|
+
return hits
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _rs_hits(rel, text):
|
|
190
|
+
"""Rust:逐行正则(跳过注释行)——Rust 侧无 AST 面,注释行显式排除。"""
|
|
191
|
+
hits = []
|
|
192
|
+
for i, line in enumerate(text.split("\n"), 1):
|
|
193
|
+
s = line.strip()
|
|
194
|
+
if not s or s.startswith("//"):
|
|
195
|
+
continue
|
|
196
|
+
for name, rx, _d in PATTERNS:
|
|
197
|
+
if rx.search(s):
|
|
198
|
+
hits.append((rel, i, s, name))
|
|
199
|
+
return hits
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def audit(root=None, extra=None):
|
|
203
|
+
"""E5 审计:返回 (findings, stats)。
|
|
204
|
+
|
|
205
|
+
findings 每项 `(rel, lineno, code, pattern, registered)`;`registered=True`
|
|
206
|
+
表示命中登记表(合法)。`extra` = {rel: text} 覆盖面(供可拦性/变异注入用)。
|
|
207
|
+
"""
|
|
208
|
+
root = root or _REPO
|
|
209
|
+
overlay = dict(extra or {})
|
|
210
|
+
findings, n_files, n_reg = [], 0, 0
|
|
211
|
+
on_disk = _iter_files(root)
|
|
212
|
+
disk_rels = {rel for rel, _p in on_disk}
|
|
213
|
+
# 覆盖面:磁盘面 ∪ 注入面(注入面可含磁盘上不存在的新文件——可拦性/变异用)
|
|
214
|
+
todo = list(on_disk) + [(rel, None) for rel in sorted(overlay)
|
|
215
|
+
if rel not in disk_rels]
|
|
216
|
+
for rel, p in todo:
|
|
217
|
+
if rel in _EXEMPT and rel not in overlay:
|
|
218
|
+
continue
|
|
219
|
+
if rel in overlay:
|
|
220
|
+
text = overlay[rel]
|
|
221
|
+
else:
|
|
222
|
+
try:
|
|
223
|
+
with open(p, encoding="utf-8") as f:
|
|
224
|
+
text = f.read()
|
|
225
|
+
except (OSError, UnicodeDecodeError):
|
|
226
|
+
continue
|
|
227
|
+
n_files += 1
|
|
228
|
+
hits = _py_hits(rel, text) if rel.endswith(".py") else _rs_hits(rel, text)
|
|
229
|
+
for rel2, ln, code, pat in hits:
|
|
230
|
+
reg = REGISTRY.get((rel2, pat))
|
|
231
|
+
if reg is not None and code in reg[0]:
|
|
232
|
+
n_reg += 1
|
|
233
|
+
continue
|
|
234
|
+
findings.append((rel2, ln, code, pat,
|
|
235
|
+
bool(reg is not None)))
|
|
236
|
+
# 登记陈化:登记表里的**每一行**都必须在源码里逐字命中
|
|
237
|
+
for (rel, pat), (want_lines, _why) in REGISTRY.items():
|
|
238
|
+
p = os.path.join(root, rel)
|
|
239
|
+
try:
|
|
240
|
+
with open(p, encoding="utf-8") as f:
|
|
241
|
+
src = overlay.get(rel, f.read())
|
|
242
|
+
except OSError:
|
|
243
|
+
findings.append((rel, 0, "<文件缺失>", pat, True))
|
|
244
|
+
continue
|
|
245
|
+
for want in want_lines:
|
|
246
|
+
if want not in src:
|
|
247
|
+
findings.append((rel, 0, "<登记陈化:源码里找不到该行> " + want,
|
|
248
|
+
pat, True))
|
|
249
|
+
return findings, {"files": n_files, "registered": n_reg}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# ---------------------------------------------------------------- 沙箱
|
|
253
|
+
|
|
254
|
+
_TMP = tempfile.mkdtemp(prefix="mdcg_tclint_")
|
|
255
|
+
_GEN = [0]
|
|
256
|
+
|
|
257
|
+
_BODY = ("# 功能名:{n}\n# 生效条件:无条件\n# 子功能:无\n"
|
|
258
|
+
"# 执行:无\n# 验证方式:test\n# 不适用条件:无\n\n正文 {n}。\n")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _mk_node(root, nid, extra_fm=None, layer="knowledge", body=None):
|
|
262
|
+
fm = {"id": nid, "layer": layer, "created_at": time.time(), "tags": []}
|
|
263
|
+
fm.update(extra_fm or {})
|
|
264
|
+
d = os.path.join(root, layer)
|
|
265
|
+
os.makedirs(d, exist_ok=True)
|
|
266
|
+
atomic_write(os.path.join(d, nid + ".md"),
|
|
267
|
+
nodefile.dumps(fm, body or _BODY.format(n=nid)))
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _fresh_root(tag):
|
|
271
|
+
_GEN[0] += 1
|
|
272
|
+
root = os.path.join(_TMP, "%s%d" % (tag, _GEN[0]))
|
|
273
|
+
os.makedirs(root, exist_ok=True)
|
|
274
|
+
return root
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# ---------------------------------------------------------------- G0
|
|
278
|
+
def g0():
|
|
279
|
+
print("== G0 隔离与基线自证 ==")
|
|
280
|
+
kp = os.path.join(_REPO, "md_cg", "whitebox_kb", "aeis_core", "time_core.py")
|
|
281
|
+
ok(os.path.isfile(kp), "G0a 核权威在位(time_core.py)")
|
|
282
|
+
ok(callable(getattr(TC, "cred_factor", None)),
|
|
283
|
+
"G0b cred_factor(gamma, dt, floor, ceil) 可取")
|
|
284
|
+
ok("md_cg/test_time_core_lint.py" in (TC.__doc__ or "")
|
|
285
|
+
and os.path.isfile(os.path.join(_REPO, "md_cg",
|
|
286
|
+
"test_time_core_lint.py")),
|
|
287
|
+
"G0c time_core docstring 点名的审计器**落地路径**在位(原句逐字保留,"
|
|
288
|
+
"只在其后补落地实现——不再是悬空点名)")
|
|
289
|
+
ok(_TMP.startswith(tempfile.gettempdir()),
|
|
290
|
+
"G0d 全部实验库根落系统临时目录(绝不碰在役库)")
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# ---------------------------------------------------------------- G1
|
|
294
|
+
def g1():
|
|
295
|
+
print("== G1 E5 衰减核审计(含可拦性)==")
|
|
296
|
+
findings, stats = audit()
|
|
297
|
+
ok(stats["files"] >= 100,
|
|
298
|
+
"G1a 审计面非空(%d 个 .py/.rs 入面)——防「扫描面为空 = 假绿」"
|
|
299
|
+
% stats["files"], stats)
|
|
300
|
+
ok(stats["registered"] == _reg_lines(),
|
|
301
|
+
"G1b 登记表登记的行**逐行**在源码里命中(%d 行)" % stats["registered"],
|
|
302
|
+
stats)
|
|
303
|
+
ok(findings == [],
|
|
304
|
+
"G1c 未登记的衰减核实现 **零命中**(findings=%d)——注意:这条只在 G1d/G1d'"
|
|
305
|
+
" 两条可拦性读数也在位时才算数(否则可能靠扫描盲区通过)" % len(findings),
|
|
306
|
+
findings[:5])
|
|
307
|
+
# 可拦性:注入两种形态,审计器必须抓到(否则是空转)
|
|
308
|
+
inj_py = ("import math\n\n\ndef f(g, dt, days, factor, retain):\n"
|
|
309
|
+
" a = math.exp(-g * dt)\n"
|
|
310
|
+
" b = 0.5 ** (days / 30.0)\n"
|
|
311
|
+
" c = x * retain\n"
|
|
312
|
+
" d = 1.0 - factor\n"
|
|
313
|
+
" return a, b, c, d\n")
|
|
314
|
+
f2, _s2 = audit(extra={"md_cg/_injected_probe.py": inj_py})
|
|
315
|
+
got = {x[3] for x in f2}
|
|
316
|
+
ok({"exp_kernel", "pow_half_life", "ema_retain",
|
|
317
|
+
"one_minus_factor"} <= got,
|
|
318
|
+
"G1d 可拦性:注入四形态全部被抓(%s)" % sorted(got), sorted(got))
|
|
319
|
+
# 可拦性·点调用形态(**独立复核抓到的盲区本体**):Rust 写 `.exp()`(括号内为空),
|
|
320
|
+
# 原正则 `exp(` 后必须紧跟 `-` ⇒ 一条都抓不到。这里直接注入该形态的 .rs 文件。
|
|
321
|
+
inj_rs = ("fn f(g: f64, dt: f64) -> f64 {\n"
|
|
322
|
+
" ((-g * dt).exp()).max(0.0)\n"
|
|
323
|
+
"}\n")
|
|
324
|
+
f3, _s3 = audit(extra={"rust/src/_injected_probe.rs": inj_rs})
|
|
325
|
+
ok(any(x[0] == "rust/src/_injected_probe.rs" and x[3] == "exp_kernel"
|
|
326
|
+
for x in f3),
|
|
327
|
+
"G1d' 可拦性·点调用形态:注入 `((-g * dt).exp())` 必被抓(%s)"
|
|
328
|
+
% [x[3] for x in f3], [x[3] for x in f3])
|
|
329
|
+
# 核权威本体豁免:time_core.py 自己必须**不**进 findings
|
|
330
|
+
ok(all(not x[0].endswith("aeis_core/time_core.py") for x in f2),
|
|
331
|
+
"G1e 核权威本体豁免(注入不改写其豁免面)")
|
|
332
|
+
# 定点证据(正则不再靠盲区):不做全仓扫描,直接把 **`rust/src/freshness.rs`
|
|
333
|
+
# 里真实出现**的两条核字面形态(生产核 `((-gamma() * dt_days).exp())` + 其
|
|
334
|
+
# `#[cfg(test)]` 半衰期行 `((-g * DECAY_DAYS).exp())`)喂给 `exp_kernel` 判据,
|
|
335
|
+
# **必须命中**;同时原「调用式」形态(`math.exp(-`、`exp(-`)**仍须命中**
|
|
336
|
+
# ——两者缺一即说明新正则要么过窄(回盲区)要么过宽(放水)。
|
|
337
|
+
_rx_exp = {n: r for n, r, _d in PATTERNS}["exp_kernel"]
|
|
338
|
+
_rf = _rel_text("rust/src/freshness.rs")
|
|
339
|
+
_pin = [l.strip() for l in _rf.split("\n") if ".exp()" in l]
|
|
340
|
+
_pin += ["math.exp(-g * dt)", "exp(-g * dt)"]
|
|
341
|
+
ok(len(_pin) >= 4 and any("((-gamma() * dt_days).exp())" in s for s in _pin)
|
|
342
|
+
and any("((-g * DECAY_DAYS).exp())" in s for s in _pin)
|
|
343
|
+
and all(_rx_exp.search(s) for s in _pin),
|
|
344
|
+
"G1f 定点证据:exp_kernel 对 freshness.rs 的**两条真实字面形态**"
|
|
345
|
+
"(生产核 .exp() 点调用 + 测试行)与原 `math.exp(-`/`exp(-` 形态**全部命中**"
|
|
346
|
+
"(%d 条:%s)——正则放宽后既不漏点调用、也没丢掉原形态"
|
|
347
|
+
% (len(_pin), [s[:52] for s in _pin]))
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
# ---------------------------------------------------------------- G2
|
|
351
|
+
def g2():
|
|
352
|
+
print("== G2 links 同族半衰期:等价性断言 + P_trust 零位移 ==")
|
|
353
|
+
D = float(links.DECAY_DAYS)
|
|
354
|
+
g = math.log(2.0) / D
|
|
355
|
+
pts = (0.0, 1.0, 7.0, 14.0, 30.0, 90.0, 300.0, 3650.0)
|
|
356
|
+
bad = [(d, 0.5 ** (d / D), TC.cred_factor(g, d)) for d in pts
|
|
357
|
+
if abs(0.5 ** (d / D) - TC.cred_factor(g, d)) > 1e-12]
|
|
358
|
+
ok(not bad,
|
|
359
|
+
"G2a 0.5**(d/DECAY_DAYS) == cred_factor(ln2/DECAY_DAYS, d) 逐点(%d 点)"
|
|
360
|
+
% len(pts), bad)
|
|
361
|
+
ok(abs(0.5 ** (30.0 / D) - 0.5) < 1e-12 and abs(TC.cred_factor(g, 30.0) - 0.5) < 1e-12,
|
|
362
|
+
"G2b 半衰期刻度对齐:d=30 天处两式同为 0.5(与 links.py:53 同刻度)")
|
|
363
|
+
# P_trust 读数:links.decay_all 的公式逐字来自 links.py:500,未改其数值行为
|
|
364
|
+
before, init = 1.0, links.P_TRUST_INIT
|
|
365
|
+
days = 10.0
|
|
366
|
+
factor = 0.5 ** (days / D)
|
|
367
|
+
after_links = max(0.0, min(1.0, round(init + (before - init) * factor, 6)))
|
|
368
|
+
after_cred = max(0.0, min(1.0, round(
|
|
369
|
+
init + (before - init) * TC.cred_factor(g, days), 6)))
|
|
370
|
+
ok(after_links == after_cred,
|
|
371
|
+
"G2c P_trust 读数零位移:links 式与 cred_factor 式同值"
|
|
372
|
+
"(before=%.4f days=%.0f → after=%.6f)" % (before, days, after_links))
|
|
373
|
+
src = _rel_text("md_cg/links.py")
|
|
374
|
+
ok("factor = 0.5 ** (days / DECAY_DAYS)" in src
|
|
375
|
+
and "math.exp" not in src,
|
|
376
|
+
"G2d links.py 的衰减行**逐字未改**(本批不动其数值行为)")
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
# ---------------------------------------------------------------- G3
|
|
380
|
+
def g3():
|
|
381
|
+
print("== G3 单位断言(γ 是「每天」的速率)==")
|
|
382
|
+
g = temporal_gamma()
|
|
383
|
+
dt_days = 7.0
|
|
384
|
+
dt_secs = dt_days * 86400.0
|
|
385
|
+
v_day = TC.cred_factor(g, dt_days, TEMPORAL_SCORE_FLOOR, 1.0)
|
|
386
|
+
v_sec = TC.cred_factor(g, dt_secs, TEMPORAL_SCORE_FLOOR, 1.0)
|
|
387
|
+
ok(math.isclose(v_day, math.exp(-g * dt_days), rel_tol=0, abs_tol=1e-12),
|
|
388
|
+
"G3a 天制喂法正确:cred_factor(γ, 7.0) == exp(-γ·7) = %.6f" % v_day)
|
|
389
|
+
ok(v_day != v_sec,
|
|
390
|
+
"G3b 同一 Δt 秒/天两喂法**必须不同**(天=%.6f vs 秒=%.6f)"
|
|
391
|
+
% (v_day, v_sec))
|
|
392
|
+
ok(v_day > v_sec,
|
|
393
|
+
"G3c 且天制正确方向:天制值 > 秒制值(秒制被 γ 放大 86400 倍衰减)")
|
|
394
|
+
ok(math.isclose(TC.cred_factor(g, 0.0), 1.0, abs_tol=1e-12),
|
|
395
|
+
"G3d Δt=0 → 1.0(新近度满值)")
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
# ---------------------------------------------------------------- G4
|
|
399
|
+
def g4():
|
|
400
|
+
print("== G4 floor(不许老节点被乘成 0)==")
|
|
401
|
+
g = temporal_gamma()
|
|
402
|
+
f = TEMPORAL_SCORE_FLOOR
|
|
403
|
+
raw300 = math.exp(-g * 300.0)
|
|
404
|
+
ok(abs(raw300 - 0.5 ** 10) < 1e-12,
|
|
405
|
+
"G4a 300 天(10 个半衰期)原值 = 0.5**10 = %.6f" % raw300)
|
|
406
|
+
ok(TC.cred_factor(g, 300.0, f, 1.0) == f,
|
|
407
|
+
"G4b floor 生效:300 天权重被夹到 floor=%.4f(原值 %.6f < floor)" % (f, raw300))
|
|
408
|
+
ok(f < raw300 * 10 and f > 0.0,
|
|
409
|
+
"G4c floor 取值理由可判:0 < floor=%.4f,且与 300 天原值同量级" % f)
|
|
410
|
+
ok(TC.cred_factor(g, float("inf"), f, 1.0) == f,
|
|
411
|
+
"G4d created_at 缺失(Δt=∞)→ floor,节点仍可召回(不被乘成 0)")
|
|
412
|
+
ok(TC.cred_factor(g, 1.0) <= 1.0,
|
|
413
|
+
"G4e ceil 生效:权重上界 1.0")
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
# ---------------------------------------------------------------- G5
|
|
417
|
+
def g5():
|
|
418
|
+
print("== G5 γ 缺省与可配 ==")
|
|
419
|
+
D = float(links.DECAY_DAYS)
|
|
420
|
+
ok(abs(temporal_gamma() - math.log(2.0) / D) < 1e-15,
|
|
421
|
+
"G5a 缺省 γ = ln2/%.0f天 = %.9f" % (D, math.log(2.0) / D))
|
|
422
|
+
ok(abs(temporal_gamma({}) - math.log(2.0) / D) < 1e-15,
|
|
423
|
+
"G5b 空 env → 缺省(与 MDCG_SPREAD_DECAY 同款读取单点)")
|
|
424
|
+
ok(temporal_gamma({TEMPORAL_GAMMA_ENV: "0.1"}) == 0.1,
|
|
425
|
+
"G5c env 可覆盖")
|
|
426
|
+
for bad in ("乱写", "0", "-1", ""):
|
|
427
|
+
ok(abs(temporal_gamma({TEMPORAL_GAMMA_ENV: bad})
|
|
428
|
+
- math.log(2.0) / D) < 1e-15,
|
|
429
|
+
"G5d 非法/非正 env %r → 回落缺省(不抛、不静默取 0)" % bad)
|
|
430
|
+
ok(TEMPORAL_GAMMA_ENV in _rel_text("md_cg/hotcache.py"),
|
|
431
|
+
"G5e MDCG_TEMPORAL_GAMMA 已登记进热路径缓存键的 env 开关清单"
|
|
432
|
+
"(漏登即跨口径串味)")
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
# ---------------------------------------------------------------- G6
|
|
436
|
+
def g6():
|
|
437
|
+
print("== G6 时间路接线(行为·排名项·不 _scan)==")
|
|
438
|
+
root = _fresh_root("t")
|
|
439
|
+
now = time.time()
|
|
440
|
+
for nid, days in (("d0", 0), ("d7", 7), ("d30", 30), ("d400", 400)):
|
|
441
|
+
ts = now - days * 86400.0
|
|
442
|
+
_mk_node(root, nid, {"created_at": ts, "effective_from": ts})
|
|
443
|
+
cg = MdCGOS(root)
|
|
444
|
+
entries = cg._candidates()
|
|
445
|
+
out = cg._path_temporal(entries, now - 1000 * 86400, now, None, None)
|
|
446
|
+
sc = {n["id"]: s for n, s in out}
|
|
447
|
+
g = temporal_gamma()
|
|
448
|
+
ok(set(sc) == {"d0", "d7", "d30", "d400"},
|
|
449
|
+
"G6a 时间算子启用 → 命中集(索引标量直读)全覆盖 %s" % sorted(sc))
|
|
450
|
+
ok(sc["d7"] == round(TC.cred_factor(g, 7.0, TEMPORAL_SCORE_FLOOR, 1.0), 6),
|
|
451
|
+
"G6b 路径分 = 天制 cred_factor(d7 → %.6f);喂秒会得 %.6f(被 floor 夹)"
|
|
452
|
+
% (sc["d7"], round(TC.cred_factor(g, 7.0 * 86400,
|
|
453
|
+
TEMPORAL_SCORE_FLOOR, 1.0), 6)))
|
|
454
|
+
ok(sc["d0"] > sc["d7"] > sc["d30"] > sc["d400"],
|
|
455
|
+
"G6c 单调:越近越大 %s" % [sc[k] for k in ("d0", "d7", "d30", "d400")])
|
|
456
|
+
ok(sc["d30"] == 0.5, "G6d 半衰期锚点:d30 → 0.5")
|
|
457
|
+
ok(cg._path_temporal(entries, None, None, None, None) == [],
|
|
458
|
+
"G6e 无时间算子/区间 → 本路恒空(默认查询零变更)")
|
|
459
|
+
_r, m = cg.search_rrf("正文 d7", k=5, record=False,
|
|
460
|
+
start_time=now - 1000 * 86400, end_time=now)
|
|
461
|
+
ok(m["paths"].get("temporal", 0) >= 1,
|
|
462
|
+
"G6f 默认 paths 含 temporal 且启用时真有候选(%r)" % m["paths"])
|
|
463
|
+
_r2, m2 = cg.search_rrf("正文 d7", k=5, record=False)
|
|
464
|
+
ok(m2["paths"].get("temporal", 0) == 0,
|
|
465
|
+
"G6g 未给时间参数 → temporal 路 0 候选(缺省查询零变更)")
|
|
466
|
+
ok("cred_factor" in _rel_text("md_cg/mdcos.py"),
|
|
467
|
+
"G6h 本模块**不写指数核**:核调用走 time_core.cred_factor")
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
# ---------------------------------------------------------------- G7
|
|
471
|
+
def g7():
|
|
472
|
+
print("== G7 因果路:两配置读数(默认集 0 → 显式集 >0)==")
|
|
473
|
+
root = _fresh_root("c")
|
|
474
|
+
_mk_node(root, "ch1",
|
|
475
|
+
{"edges": [{"target": "ch2", "relation_type": "reference",
|
|
476
|
+
"confidence": 0.6}]},
|
|
477
|
+
body="# 功能名:ch1 苹果章\n# 生效条件:无条件\n# 子功能:无\n"
|
|
478
|
+
"# 执行:无\n# 验证方式:test\n# 不适用条件:无\n\n苹果 章节一。\n")
|
|
479
|
+
_mk_node(root, "ch2",
|
|
480
|
+
body="# 功能名:ch2\n# 生效条件:无条件\n# 子功能:无\n"
|
|
481
|
+
"# 执行:无\n# 验证方式:test\n# 不适用条件:无\n\n第二章。\n")
|
|
482
|
+
env0 = os.environ.pop("MDCG_CHAIN_TYPES", None)
|
|
483
|
+
try:
|
|
484
|
+
cg = MdCGOS(root)
|
|
485
|
+
_r, m_def = cg.search_rrf("苹果", k=5, record=False,
|
|
486
|
+
paths=("lexical", "chain"))
|
|
487
|
+
n_def = m_def["paths"].get("chain", 0)
|
|
488
|
+
os.environ["MDCG_CHAIN_TYPES"] = "reference,part_of"
|
|
489
|
+
cg2 = MdCGOS(root)
|
|
490
|
+
r2, m_env = cg2.search_rrf("苹果", k=5, record=False,
|
|
491
|
+
paths=("lexical", "chain"))
|
|
492
|
+
n_env = m_env["paths"].get("chain", 0)
|
|
493
|
+
finally:
|
|
494
|
+
os.environ.pop("MDCG_CHAIN_TYPES", None)
|
|
495
|
+
if env0 is not None:
|
|
496
|
+
os.environ["MDCG_CHAIN_TYPES"] = env0
|
|
497
|
+
ok(n_def == 0,
|
|
498
|
+
"G7a 默认集(causal/sequential/applies_to)在 doc_ref 型语料上候选 = %d"
|
|
499
|
+
"(只有 reference/part_of 边 → 恒空,正是「写死等于没接」的现场)" % n_def)
|
|
500
|
+
ok(n_env > 0,
|
|
501
|
+
"G7b 显式集含 reference/part_of → 候选 = %d(本批最要紧的一条:边类型集可配)"
|
|
502
|
+
% n_env)
|
|
503
|
+
ok(any(e.get("chain") for e in (m_env.get("provenance") or {}).values()
|
|
504
|
+
for e in e),
|
|
505
|
+
"G7c 命中的 provenance 带链(可审计「为什么召回它」)")
|
|
506
|
+
ok(chain.chain_types_from_env({"MDCG_CHAIN_TYPES": "reference,part_of"})
|
|
507
|
+
== ("reference", "part_of"),
|
|
508
|
+
"G7d 读取单点解析 env(顺序 = 声明序)")
|
|
509
|
+
ok(chain.chain_types_from_env({"MDCG_CHAIN_TYPES": "nope,unknown"})
|
|
510
|
+
== chain.CHAIN_TYPES_DEFAULT,
|
|
511
|
+
"G7e 未登记形态被剔除 → 回落缺省(不静默按 DEFAULT_EDGE_WEIGHT 走未知边)")
|
|
512
|
+
ok(chain.chain_types_from_env({}) == chain.CHAIN_TYPES_DEFAULT,
|
|
513
|
+
"G7f 未设 env → 缺省集一字不变")
|
|
514
|
+
ok(chain.CHAIN_TYPES_ENV in _rel_text("md_cg/hotcache.py"),
|
|
515
|
+
"G7g MDCG_CHAIN_TYPES 已登记进热路径缓存键的 env 开关清单")
|
|
516
|
+
# S3 契约不变量(docs/hive/检索路径与认知结构契约_v0.1.md:21-53)
|
|
517
|
+
ids_env = {n[0]["id"] for n in r2}
|
|
518
|
+
ok("ch2" in ids_env,
|
|
519
|
+
"G7h ③不引入未过 S2 门控的节点:chain 命中仍落在候选集内(allowed 过滤)")
|
|
520
|
+
ok(chain.edge_weight({"relation_type": "reference"}) == 0.5
|
|
521
|
+
and chain.edge_weight({"relation_type": "causal"}) == 0.85,
|
|
522
|
+
"G7i ④边权沿用既有唯一权重表(reference .5 / causal .85)——未新造第二份")
|
|
523
|
+
|
|
524
|
+
# ② 多跳 + 逐跳衰减:h1 --causal--> h2 --causal--> h3(种子在 h1)
|
|
525
|
+
rh = _fresh_root("h")
|
|
526
|
+
_mk_node(rh, "h1", {"edges": [{"target": "h2", "relation_type": "causal",
|
|
527
|
+
"confidence": 1.0}]},
|
|
528
|
+
body="# 功能名:h1\n# 生效条件:无条件\n# 子功能:无\n# 执行:无\n"
|
|
529
|
+
"# 验证方式:test\n# 不适用条件:无\n\n荔枝 起点。\n")
|
|
530
|
+
_mk_node(rh, "h2", {"edges": [{"target": "h3", "relation_type": "causal",
|
|
531
|
+
"confidence": 1.0}]},
|
|
532
|
+
body="# 功能名:h2\n# 生效条件:无条件\n# 子功能:无\n# 执行:无\n"
|
|
533
|
+
"# 验证方式:test\n# 不适用条件:无\n\n中转。\n")
|
|
534
|
+
_mk_node(rh, "h3", body="# 功能名:h3\n# 生效条件:无条件\n# 子功能:无\n"
|
|
535
|
+
"# 执行:无\n# 验证方式:test\n# 不适用条件:无\n\n"
|
|
536
|
+
"末端。\n")
|
|
537
|
+
ch = MdCGOS(rh)
|
|
538
|
+
rr_h, mm_h = ch.search_rrf("荔枝", k=8, record=False,
|
|
539
|
+
paths=("lexical", "chain"))
|
|
540
|
+
prov_h = {(k, p.get("rank")): p for k, v in (mm_h.get("provenance") or {}).items()
|
|
541
|
+
for p in v if p.get("path") == "chain"}
|
|
542
|
+
dp = {k[0]: v.get("chain") and len(v["chain"]) - 1
|
|
543
|
+
for k, v in prov_h.items()}
|
|
544
|
+
ok(dp.get("h2") == 1 and dp.get("h3") == 2,
|
|
545
|
+
"G7j ②沿 edges 多跳扩散(1 跳 h2 / 2 跳 h3;MAX_DEPTH_DEFAULT=5、decay=0.9 复用既有 chain 原语)"
|
|
546
|
+
" 实测跳数=%r" % (dp,))
|
|
547
|
+
# 链分逐跳乘衰减:同一批边权/置信度下,2 跳节点的链分必低于 1 跳节点。
|
|
548
|
+
# 直接读 `_path_chain` 的**路内分**(RRF 只吃名次,路内分要单独取)。
|
|
549
|
+
_ps, _ = ch._path_chain("荔枝", ch._candidates(), [({"id": "h1"}, 1.0)])
|
|
550
|
+
_psc = {n["id"]: s for n, s in _ps}
|
|
551
|
+
ok("h3" in _psc and _psc["h3"] < _psc.get("h2", 1.0),
|
|
552
|
+
"G7k ②' 跳数越深链分越低(decay=0.9 逐跳乘)——h2=%.6f > h3=%.6f"
|
|
553
|
+
% (_psc.get("h2", 0), _psc.get("h3", 0)))
|
|
554
|
+
ch.close()
|
|
555
|
+
|
|
556
|
+
# ③ 不得引入未过门控的节点:链上目标落 rejected 层(候选面已排除)
|
|
557
|
+
rq = _fresh_root("q")
|
|
558
|
+
_mk_node(rq, "s1", {"edges": [{"target": "q_neg", "relation_type": "causal",
|
|
559
|
+
"confidence": 1.0}]},
|
|
560
|
+
body="# 功能名:s1\n# 生效条件:无条件\n# 子功能:无\n# 执行:无\n"
|
|
561
|
+
"# 验证方式:test\n# 不适用条件:无\n\n龙眼 起点。\n")
|
|
562
|
+
_mk_node(rq, "q_neg", body="# 功能名:q_neg\n# 生效条件:无条件\n# 子功能:无\n"
|
|
563
|
+
"# 执行:无\n# 验证方式:test\n# 不适用条件:无\n\n"
|
|
564
|
+
"被否决的结论。\n", layer="rejected")
|
|
565
|
+
cq = MdCGOS(rq)
|
|
566
|
+
rr_q, mm_q = cq.search_rrf("龙眼", k=8, record=False,
|
|
567
|
+
paths=("lexical", "chain"))
|
|
568
|
+
hit_q = {t[0]["id"] for t in rr_q} | {k for k, v in
|
|
569
|
+
(mm_q.get("provenance") or {}).items()
|
|
570
|
+
for p in v if p.get("path") == "chain"}
|
|
571
|
+
ok("q_neg" not in hit_q,
|
|
572
|
+
"G7l ③' 未过门控(rejected 层,候选面已排除)的链上目标**不引入**"
|
|
573
|
+
"(实测命中集=%r)" % sorted(hit_q))
|
|
574
|
+
cq.close()
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
# ---------------------------------------------------------------- G8
|
|
578
|
+
def g8():
|
|
579
|
+
print("== G8 缺省与分路开关(mdcos 缺省集 + mcp_server 调用点)==")
|
|
580
|
+
import inspect
|
|
581
|
+
from . import mcp_server
|
|
582
|
+
d = inspect.signature(MdCGOS.search_rrf).parameters["paths"].default
|
|
583
|
+
ok(d == ("lexical", "bucket", "entity", "graph", "chain", "temporal"),
|
|
584
|
+
"G8a search_rrf 缺省 paths = 六路(两条图检索路进缺省集)", d)
|
|
585
|
+
root = _fresh_root("d")
|
|
586
|
+
_mk_node(root, "x1",
|
|
587
|
+
body="# 功能名:x1\n# 生效条件:无条件\n# 子功能:无\n"
|
|
588
|
+
"# 执行:无\n# 验证方式:test\n# 不适用条件:无\n\n草莓。\n")
|
|
589
|
+
cg = MdCGOS(root)
|
|
590
|
+
pack = mcp_server._dispatch(cg, "mdcg_recall", {"query": "草莓", "k": 5})
|
|
591
|
+
ok(set((pack.get("meta") or {}).get("paths") or {}) >=
|
|
592
|
+
{"lexical", "bucket", "entity", "graph", "chain", "temporal"},
|
|
593
|
+
"G8b mcp_server 缺省调用点:六路齐(含 chain/temporal)",
|
|
594
|
+
(pack.get("meta") or {}).get("paths"))
|
|
595
|
+
pack2 = mcp_server._dispatch(cg, "mdcg_recall",
|
|
596
|
+
{"query": "草莓", "k": 5,
|
|
597
|
+
"causal": False, "temporal": False})
|
|
598
|
+
ok(set((pack2.get("meta") or {}).get("paths") or {})
|
|
599
|
+
== {"lexical", "bucket", "entity", "graph"},
|
|
600
|
+
"G8c ⑤每路可单独关:causal=false & temporal=false → 回到四路",
|
|
601
|
+
(pack2.get("meta") or {}).get("paths"))
|
|
602
|
+
m = _rel_text("md_cg/mcp_server.py")
|
|
603
|
+
ok("or use_causal" in m and "use_temporal" in m,
|
|
604
|
+
"G8d 融合口径照抄先例形态(同一表达式内追加 use_causal 条件)")
|
|
605
|
+
# G8d' **代码面**复核(为什么需要):G8d 是纯文本断言,注释与 docstring 一样能
|
|
606
|
+
# 把它顶绿——P4 把融合口径收进 `recall_fusion_default` 后,其 docstring 里就有
|
|
607
|
+
# `or use_causal` 字样,于是「只删代码里的 `or use_causal`」能全身而退
|
|
608
|
+
# (P3-M7 变异实测:删了代码里的那截,G8d 照绿、红项=0)。故按 **AST** 判:
|
|
609
|
+
# 产出 "max" 的那个条件表达式(IfExp.test)里必须含 use_causal。
|
|
610
|
+
# (注意不能退化成「任一 BoolOp 含四路」——:3401/:3424 两处 BoolOp 也含这四个
|
|
611
|
+
# 名字,那样 M7 变异照样漏网,实测如此。)
|
|
612
|
+
max_conds = [(n.lineno, (ast.get_source_segment(m, n.test) or "").replace("\n", " "))
|
|
613
|
+
for n in ast.walk(ast.parse(m))
|
|
614
|
+
if isinstance(n, ast.IfExp) and isinstance(n.body, ast.Constant)
|
|
615
|
+
and n.body.value == "max"]
|
|
616
|
+
ok(bool(max_conds) and all("use_causal" in c for _l, c in max_conds),
|
|
617
|
+
"G8d' 「max」融合口径的**条件表达式**(AST 代码面)含 use_causal——"
|
|
618
|
+
"注释/docstring 顶不了绿", max_conds)
|
|
619
|
+
ok('paths.append("chain")' in m and 'paths.append("temporal")' in m,
|
|
620
|
+
"G8e 调用点显式列出两条新路(与 mdcos 缺省集同改,两侧不分裂)")
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
# ---------------------------------------------------------------- G9
|
|
624
|
+
def g9():
|
|
625
|
+
print("== G9 N137:降级搬迁后边集合逐项相等 ==")
|
|
626
|
+
root = _fresh_root("m")
|
|
627
|
+
ts = time.time()
|
|
628
|
+
_mk_node(root, "n1",
|
|
629
|
+
{"created_at": ts, "edges": [{"target": "n2",
|
|
630
|
+
"relation_type": "causal",
|
|
631
|
+
"confidence": 0.8}],
|
|
632
|
+
"subgraph": {"nodes": ["n2"]},
|
|
633
|
+
"valid_from": ts - 100, "valid_until": ts + 100000,
|
|
634
|
+
"effective_from": ts - 100, "effective_until": ts + 100000},
|
|
635
|
+
body="# 功能名:n1\n# 生效条件:无条件\n# 子功能:无\n"
|
|
636
|
+
"# 执行:无\n# 验证方式:test\n# 不适用条件:无\n\n"
|
|
637
|
+
"上游条件 葡萄 触发。\n")
|
|
638
|
+
_mk_node(root, "n2", body="# 功能名:n2\n# 生效条件:无条件\n# 子功能:无\n"
|
|
639
|
+
"# 执行:无\n# 验证方式:test\n# 不适用条件:无\n\n"
|
|
640
|
+
"下游结论 苹果 成立。\n")
|
|
641
|
+
cg = MdCGOS(root)
|
|
642
|
+
e0 = dict(cg.index["nodes"]["n1"])
|
|
643
|
+
res = cg._move_layer("n1", "contextual", reason="N137 守卫")
|
|
644
|
+
ok(bool(res) and res.get("to") == "contextual", "G9a 搬迁执行成功", res)
|
|
645
|
+
e1 = cg.index["nodes"]["n1"]
|
|
646
|
+
ok((e1.get("edges") or []) == (e0.get("edges") or [])
|
|
647
|
+
and (e1.get("edges") or []) != [],
|
|
648
|
+
"G9b 搬迁前后**边集合逐项相等**且非空(%r)" % (e1.get("edges"),))
|
|
649
|
+
ok(e1.get("subgraph") == e0.get("subgraph"),
|
|
650
|
+
"G9c subgraph 快照存活(递归展开免读盘)")
|
|
651
|
+
for k in ("valid_from", "valid_until", "effective_from", "effective_until"):
|
|
652
|
+
ok(k in e1 and e1.get(k) == e0.get(k),
|
|
653
|
+
"G9d 双时间轴键 %s 存活(trust.validity 免读盘判定面)" % k)
|
|
654
|
+
# 端到端:搬迁后因果路仍能经边召到下游(新路不再静默拿不到边)
|
|
655
|
+
# 种子 = 词法命中 n1(上游),目标 n2 词面无重叠 —— 正是因果路的靶区。
|
|
656
|
+
_r, m = cg.search_rrf("葡萄", k=5, record=False, paths=("lexical", "chain"))
|
|
657
|
+
ok(m["paths"].get("chain", 0) >= 1
|
|
658
|
+
and any(h[0]["id"] == "n2" for h in _r),
|
|
659
|
+
"G9e 搬迁后 chain 路仍可达下游(%r)" % m["paths"])
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
# ---------------------------------------------------------------- G10
|
|
663
|
+
def g10():
|
|
664
|
+
print("== G10 双侧同改披露面(键定义一致 + 缺省路集差异)==")
|
|
665
|
+
py = _rel_text("md_cg/mdcos.py")
|
|
666
|
+
rs = _rel_text("rust/src/retrieval.rs")
|
|
667
|
+
ok('key=lambda x: (-x[1],\n' in py.replace(" ", " ")
|
|
668
|
+
or "-float(x[0][\"frontmatter\"].get(\"importance\") or 0)" in py,
|
|
669
|
+
"G10a Python 每路排序键在位(-score, -importance, id)")
|
|
670
|
+
ok("b.score" in rs and "importance" in rs and "ida.cmp(&idb)" in rs,
|
|
671
|
+
"G10b Rust sort_path 同三键在位(先例 docs/eval/优化第一批…:218-221 的"
|
|
672
|
+
"「键定义一致」形态;**非**运行期逐位对拍)")
|
|
673
|
+
eng = _rel_text("rust/src/engine.rs")
|
|
674
|
+
ok("全部四路" in eng,
|
|
675
|
+
"G10c 如实披露:Rust 侧缺省路集仍是四路(lexical/bucket/entity/graph)"
|
|
676
|
+
"——本批**未**在 Rust 侧实现 chain/temporal,两侧缺省集不同")
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
# ---------------------------------------------------------------- G11
|
|
680
|
+
|
|
681
|
+
#: Rust 常量声明(`pub const X: f64 = …;`)——G11 只吃**源码**,不吃硬编码。
|
|
682
|
+
_RS_CONST_RE = re.compile(r"pub const (\w+): f64 = ([0-9.eE+\-]+);")
|
|
683
|
+
#: Rust 指数核表达式(`factor()` 里的乘子本体)——形状固定:
|
|
684
|
+
#: `let decay = ((-(e)).exp()).max(floor).min(CEIL);`
|
|
685
|
+
_RS_KERNEL_RE = re.compile(
|
|
686
|
+
r"let decay = \(\(-(?P<exp>.+?)\)\.exp\(\)\)"
|
|
687
|
+
r"\.max\((?P<floor>[\w:]+)\)\.min\((?P<ceil>[\w:]+)\);")
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def _rust_consts(src):
|
|
691
|
+
"""Rust 源码的 `pub const X: f64 = …;` → {名字: 值}。"""
|
|
692
|
+
return {m.group(1): float(m.group(2)) for m in _RS_CONST_RE.finditer(src)}
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def _rust_decay_value(src, consts, gamma_val, dt_days):
|
|
696
|
+
"""把 Rust 的核表达式**从源码抽出来、在 Python 里求值**(Rust 调不了 Python)。
|
|
697
|
+
|
|
698
|
+
这与 `md_cg/links.py` 的 G2 是同一路径的两侧:那边是「同族写法 ↔ cred_factor
|
|
699
|
+
逐点等价」,这边是「另一门语言的实现 ↔ cred_factor 逐点等价」——Rust 无法被
|
|
700
|
+
Python 调用,故 `REGISTRY` 登记 + 本函数的逐点对拍就是 E5 允许的唯一合规路径。
|
|
701
|
+
|
|
702
|
+
表达式里的标识符(常量名、`gamma()`、`dt_days`、局部 `floor`)全部按 `consts`
|
|
703
|
+
代入后,只允许 `[0-9eE.+-*/() ]` 字符集再受限求值。**形状不认识 → 返回 None**
|
|
704
|
+
(判红,不静默跳过)。返回 `min(ceil, max(floor, exp(-e)))`——逐字对应 Rust 的
|
|
705
|
+
`((-(e)).exp()).max(floor).min(CEIL)`。
|
|
706
|
+
"""
|
|
707
|
+
m = _RS_KERNEL_RE.search(src)
|
|
708
|
+
if m is None:
|
|
709
|
+
return None
|
|
710
|
+
syms = {"gamma()": gamma_val, "dt_days": dt_days,
|
|
711
|
+
"floor": consts.get("SCORE_FLOOR")}
|
|
712
|
+
for k, v in consts.items():
|
|
713
|
+
syms.setdefault(k, v)
|
|
714
|
+
sub = m.group("exp")
|
|
715
|
+
for k in sorted(syms, key=len, reverse=True):
|
|
716
|
+
if syms[k] is None:
|
|
717
|
+
return None
|
|
718
|
+
sub = re.sub(re.escape(k), lambda _m, v=float(syms[k]): repr(v), sub)
|
|
719
|
+
if not re.fullmatch(r"[0-9eE.+\-*/() ]+", sub):
|
|
720
|
+
return None
|
|
721
|
+
val = math.exp(-eval(sub, {"__builtins__": {}}, {}))
|
|
722
|
+
lo, hi = syms.get(m.group("floor")), syms.get(m.group("ceil"))
|
|
723
|
+
if lo is None or hi is None:
|
|
724
|
+
return None
|
|
725
|
+
return min(hi, max(lo, val))
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def g11():
|
|
729
|
+
print("== G11 Rust 侧指数核登记:常量单源 + 公式逐点等价(Rust 调不了 Python)==")
|
|
730
|
+
rs = _rel_text("rust/src/freshness.rs")
|
|
731
|
+
c = _rust_consts(rs)
|
|
732
|
+
ok(c.get("DECAY_DAYS") is not None
|
|
733
|
+
and c["DECAY_DAYS"] == float(links.DECAY_DAYS),
|
|
734
|
+
"G11a Rust DECAY_DAYS=%r ≡ links.DECAY_DAYS=%r(γ 的半衰期刻度单源,"
|
|
735
|
+
"Rust 侧无 links 模块故此处是唯一取值点)"
|
|
736
|
+
% (c.get("DECAY_DAYS"), float(links.DECAY_DAYS)))
|
|
737
|
+
ok(c.get("SCORE_FLOOR") == TEMPORAL_SCORE_FLOOR,
|
|
738
|
+
"G11a' Rust SCORE_FLOOR=%r ≡ mdcos.TEMPORAL_SCORE_FLOOR=%r(floor 同值,"
|
|
739
|
+
"与 P3-temporal 同源理由)" % (c.get("SCORE_FLOOR"), TEMPORAL_SCORE_FLOOR))
|
|
740
|
+
ok(c.get("SCORE_CEIL") == 1.0 and c.get("SECONDS_PER_DAY") == 86400.0,
|
|
741
|
+
"G11a'' Rust SCORE_CEIL=%r / SECONDS_PER_DAY=%r(ceil=1.0;Δt 的秒→天"
|
|
742
|
+
"换算常量)" % (c.get("SCORE_CEIL"), c.get("SECONDS_PER_DAY")))
|
|
743
|
+
# 缺省 γ 的**公式**(不是常量同值):Rust 写 LN_2 / DECAY_DAYS,Python 写
|
|
744
|
+
# ln2 / links.DECAY_DAYS —— 两式在 G11a 的常量单源之上数值必须一致。
|
|
745
|
+
ok("std::f64::consts::LN_2 / DECAY_DAYS" in rs
|
|
746
|
+
and abs(math.log(2.0) / c["DECAY_DAYS"] - temporal_gamma()) < 1e-15,
|
|
747
|
+
"G11b Rust 缺省 γ 式 LN_2/DECAY_DAYS = Python temporal_gamma() = %.9f"
|
|
748
|
+
% temporal_gamma())
|
|
749
|
+
# 登记面:本文件的核行必须**在册**(摘掉登记 → 本条与 G1c 一起转红)
|
|
750
|
+
reg = REGISTRY.get(("rust/src/freshness.rs", "exp_kernel"))
|
|
751
|
+
ok(reg is not None and len(reg[0]) == 2
|
|
752
|
+
and all(line in rs for line in reg[0]),
|
|
753
|
+
"G11c rust/src/freshness.rs 的 2 行核实现已登记进 REGISTRY(exp_kernel)"
|
|
754
|
+
"——摘掉登记即红(--mutate 的 P4-M9 就是这条)", reg)
|
|
755
|
+
# 逐点等价:核表达式**从 Rust 源码抽出**后求值 vs cred_factor
|
|
756
|
+
g_rs = math.log(2.0) / c["DECAY_DAYS"]
|
|
757
|
+
g_py = temporal_gamma()
|
|
758
|
+
pts = (0.0, 1.0, 7.0, 14.0, 30.0, 90.0, 300.0, 3650.0)
|
|
759
|
+
rows, bad = [], []
|
|
760
|
+
for d in pts:
|
|
761
|
+
v_rs = _rust_decay_value(rs, c, g_rs, d)
|
|
762
|
+
v_py = TC.cred_factor(g_py, d, TEMPORAL_SCORE_FLOOR, 1.0)
|
|
763
|
+
rows.append((d, v_rs, v_py))
|
|
764
|
+
if v_rs is None or abs(v_rs - v_py) > 1e-12:
|
|
765
|
+
bad.append((d, v_rs, v_py))
|
|
766
|
+
ok(not bad,
|
|
767
|
+
"G11d Rust 核表达式(**从源码抽出**)== cred_factor(γ, Δt, floor, ceil) 逐点"
|
|
768
|
+
"(%d 点;Δt=300 天 → %.6f = floor,Δt=30 天 → %.6f = 半衰期)"
|
|
769
|
+
% (len(pts), rows[6][2], rows[4][2]), bad)
|
|
770
|
+
ok(rows[4][1] == 0.5 and abs(rows[1][1] - 0.5 ** (1.0 / 30.0)) < 1e-12,
|
|
771
|
+
"G11d' 抽样读数:Rust 求得 Δt=30 天 → %.6f(半衰期)、Δt=1 天 → %.9f"
|
|
772
|
+
% (rows[4][1], rows[1][1]))
|
|
773
|
+
# 非空转**反证**:把抽出的表达式在内存里扰动(指数项 ×2)→ 逐点等式必须立刻
|
|
774
|
+
# 大面积不成立。为什么不是 8/8:Δt=0 处两式同为 1.0,Δt≥300 天处两式都被 floor
|
|
775
|
+
# 夹到 0.001 —— 这些点是「夹紧巧合」,不是等价性的证据面。
|
|
776
|
+
broken = rs.replace("(-gamma() * dt_days).exp()",
|
|
777
|
+
"(-gamma() * dt_days * 2.0).exp()", 1)
|
|
778
|
+
diff = [d for d in pts
|
|
779
|
+
if abs(_rust_decay_value(broken, c, g_rs, d)
|
|
780
|
+
- TC.cred_factor(g_py, d, TEMPORAL_SCORE_FLOOR, 1.0)) > 1e-12]
|
|
781
|
+
ok(len(diff) >= 5,
|
|
782
|
+
"G11e 反证(非空转):核表达式扰动 ×2 后 %d/%d 点不再相等(其余被 floor/Δt=0"
|
|
783
|
+
" 夹紧巧合)——证明 G11d 真的在读 Rust 源码,不是自说自话" % (len(diff), len(pts)),
|
|
784
|
+
diff)
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
# ---------------------------------------------------------------- 运行
|
|
788
|
+
def _rel_text(rel):
|
|
789
|
+
if rel in _SRC:
|
|
790
|
+
return _SRC[rel]
|
|
791
|
+
with open(os.path.join(_REPO, rel), encoding="utf-8") as f:
|
|
792
|
+
return f.read()
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
_GROUPS = (g0, g1, g2, g3, g4, g5, g6, g7, g8, g9, g10, g11)
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def _run_groups():
|
|
799
|
+
del _PASS[:], _FAIL[:], _SKIP[:]
|
|
800
|
+
for fn in _GROUPS:
|
|
801
|
+
fn()
|
|
802
|
+
return len(_FAIL)
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
# ===================== 定点变异自证(--mutate) =====================
|
|
806
|
+
|
|
807
|
+
_MUTATIONS = (
|
|
808
|
+
("P3-M1 自写指数核:mdcos 时间路不再走 cred_factor(退回被点名的 bug 形态)",
|
|
809
|
+
"md_cg/mdcos.py",
|
|
810
|
+
" score = _tc.cred_factor(gamma, dt_days, TEMPORAL_SCORE_FLOOR, 1.0)\n",
|
|
811
|
+
" score = math.exp(-gamma * dt_days)\n", 1),
|
|
812
|
+
("P3-M2 γ 缺省漂移:ln2/30 天 → ln2/14 天(半衰期刻度不再锚定 links)",
|
|
813
|
+
"md_cg/mdcos.py",
|
|
814
|
+
" return math.log(2.0) / float(DECAY_DAYS)\n",
|
|
815
|
+
" return math.log(2.0) / 14.0\n", 1),
|
|
816
|
+
("P3-M3 单位错:Δt 不再换算成「天」(喂秒,衰减放大 86400 倍)",
|
|
817
|
+
"md_cg/mdcos.py",
|
|
818
|
+
" dt_days = abs(created - float(ref)) / 86400.0\n",
|
|
819
|
+
" dt_days = abs(created - float(ref))\n", 2),
|
|
820
|
+
("P3-M4 边类型集写死:chain_types_from_env 不读 env(doc 语料上恒空)",
|
|
821
|
+
"md_cg/chain.py",
|
|
822
|
+
" raw = (environ or os.environ).get(CHAIN_TYPES_ENV)\n",
|
|
823
|
+
" raw = None\n", 1),
|
|
824
|
+
("P3-M5 N137 复发:_move_layer 的 _stage 条目不再带 edges",
|
|
825
|
+
"md_cg/mdcg.py",
|
|
826
|
+
" \"edges\": fm.get(\"edges\") or [],\n"
|
|
827
|
+
" \"subgraph\": fm.get(\"subgraph\"),\n",
|
|
828
|
+
" \"subgraph\": fm.get(\"subgraph\"),\n", 2),
|
|
829
|
+
("P3-M6 缺省集退化:search_rrf 缺省不再含两条图检索路",
|
|
830
|
+
"md_cg/mdcos.py",
|
|
831
|
+
" paths=(\"lexical\", \"bucket\", \"entity\", \"graph\", \"chain\",\n"
|
|
832
|
+
" \"temporal\"),\n",
|
|
833
|
+
" paths=(\"lexical\", \"bucket\", \"entity\", \"graph\"),\n", 2),
|
|
834
|
+
("P3-M7 融合口径脱钩:mcp_server 不再把因果路纳入 max 缺省",
|
|
835
|
+
"md_cg/mcp_server.py",
|
|
836
|
+
" return (\"max\" if (use_fuzzy or use_semantic or use_goal or use_causal)\n",
|
|
837
|
+
" return (\"max\" if (use_fuzzy or use_semantic or use_goal)\n", 1),
|
|
838
|
+
("P3-M8 links 数值行为被改:半衰期写法换成自写指数核",
|
|
839
|
+
"md_cg/links.py",
|
|
840
|
+
" factor = 0.5 ** (days / DECAY_DAYS)\n",
|
|
841
|
+
" factor = math.exp(-days / DECAY_DAYS)\n", 2),
|
|
842
|
+
("P4-M9 E5 登记表摘除 Rust 侧:rust/src/freshness.rs 的指数核不再被登记"
|
|
843
|
+
"(审计器必须转红——证明 G1c 不是靠扫描盲区通过)",
|
|
844
|
+
"md_cg/test_time_core_lint.py",
|
|
845
|
+
' ("rust/src/freshness.rs", "exp_kernel"): (\n',
|
|
846
|
+
' ("rust/__NOT_REGISTERED__.rs", "exp_kernel"): (\n', 1),
|
|
847
|
+
("P4-M10 Rust 核公式被改坏:指数项多乘一个 2(登记陈化 + 未登记命中 + G11 数值"
|
|
848
|
+
"不等,三处同时红)",
|
|
849
|
+
"rust/src/freshness.rs",
|
|
850
|
+
" let decay = ((-gamma() * dt_days).exp()).max(floor).min(SCORE_CEIL);\n",
|
|
851
|
+
" let decay = ((-gamma() * dt_days * 2.0).exp()).max(floor)"
|
|
852
|
+
".min(SCORE_CEIL);\n", 1),
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
_MUT_FILES = tuple(sorted({m[1] for m in _MUTATIONS}))
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
def _mirror_build():
|
|
859
|
+
"""最小镜像树:md_cg(除 __pycache__)+ rust/src + data/policy.json + 根级
|
|
860
|
+
utf8_boot.py(mcp_server.py 的入口自保证依赖它,缺了会在 G8 崩——镜像面
|
|
861
|
+
必须覆盖被判据读到的模块依赖)。"""
|
|
862
|
+
root = tempfile.mkdtemp(prefix="mdcg_tclint_mut_")
|
|
863
|
+
shutil.copytree(os.path.join(_REPO, "md_cg"), os.path.join(root, "md_cg"),
|
|
864
|
+
ignore=shutil.ignore_patterns("__pycache__"))
|
|
865
|
+
shutil.copytree(os.path.join(_REPO, "rust", "src"),
|
|
866
|
+
os.path.join(root, "rust", "src"))
|
|
867
|
+
os.makedirs(os.path.join(root, "data"), exist_ok=True)
|
|
868
|
+
pol = os.path.join(_REPO, "data", "policy.json")
|
|
869
|
+
if os.path.exists(pol):
|
|
870
|
+
shutil.copy2(pol, os.path.join(root, "data", "policy.json"))
|
|
871
|
+
boot = os.path.join(_REPO, "utf8_boot.py")
|
|
872
|
+
if os.path.exists(boot):
|
|
873
|
+
shutil.copy2(boot, os.path.join(root, "utf8_boot.py"))
|
|
874
|
+
return root
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
def _reds_in(cwd):
|
|
878
|
+
p = subprocess.run([sys.executable, "-X", "utf8", "-m",
|
|
879
|
+
"md_cg.test_time_core_lint"],
|
|
880
|
+
cwd=cwd, capture_output=True, text=True,
|
|
881
|
+
encoding="utf-8", errors="replace", timeout=900)
|
|
882
|
+
out = (p.stdout or "") + (p.stderr or "")
|
|
883
|
+
return p.returncode, [l.strip() for l in out.splitlines()
|
|
884
|
+
if l.strip().startswith("FAIL ") or
|
|
885
|
+
l.strip().startswith("ANCHOR-MISS")], out
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
def _mutate_mode(list_only=False):
|
|
889
|
+
print("!! 定点变异自证:逐条把判据改回改动前形态,守卫必须转红\n")
|
|
890
|
+
if list_only:
|
|
891
|
+
for name, rel, _o, _n, exp in _MUTATIONS:
|
|
892
|
+
print(" %-58s expect_red>=%d (%s)" % (name[:58], exp, rel))
|
|
893
|
+
return 0
|
|
894
|
+
base = _mirror_build()
|
|
895
|
+
try:
|
|
896
|
+
rc, reds, out0 = _reds_in(base)
|
|
897
|
+
print(" 未变异镜像基线:rc=%d 红项=%d(必须 rc=0 且 0 红)"
|
|
898
|
+
% (rc, len(reds)))
|
|
899
|
+
for r in reds[:5]:
|
|
900
|
+
print(" " + r)
|
|
901
|
+
bad = []
|
|
902
|
+
if reds:
|
|
903
|
+
bad.append("未变异镜像基线即红")
|
|
904
|
+
if rc != 0:
|
|
905
|
+
# rc!=0 而零红项 = 守卫在镜像里**崩了**(如缺根级模块)——这类
|
|
906
|
+
# 静默崩溃会让下面每一条变异都「红项不足」而看不出真因,必须响。
|
|
907
|
+
bad.append("未变异镜像基线 rc=%d≠0(镜像面缺依赖或守卫崩溃)" % rc)
|
|
908
|
+
print(" ——镜像基线尾部输出——")
|
|
909
|
+
for line in (out0 or "").splitlines()[-12:]:
|
|
910
|
+
print(" | " + line)
|
|
911
|
+
anchor_miss, mismatch = [], []
|
|
912
|
+
for name, rel, old, new, exp in _MUTATIONS:
|
|
913
|
+
src_p = os.path.join(base, rel)
|
|
914
|
+
with open(src_p, encoding="utf-8") as f:
|
|
915
|
+
original = f.read()
|
|
916
|
+
if old not in original:
|
|
917
|
+
print(" ANCHOR-MISS %s —— 锚点在 %s 里找不到(实现改了却没同步"
|
|
918
|
+
"本表;锚点漂移不得静默)" % (name, rel))
|
|
919
|
+
anchor_miss.append(name)
|
|
920
|
+
continue
|
|
921
|
+
with open(src_p, "w", encoding="utf-8", newline="\n") as f:
|
|
922
|
+
f.write(original.replace(old, new, 1))
|
|
923
|
+
try:
|
|
924
|
+
rc2, reds2, out2 = _reds_in(base)
|
|
925
|
+
finally:
|
|
926
|
+
with open(src_p, "w", encoding="utf-8", newline="\n") as f:
|
|
927
|
+
f.write(original)
|
|
928
|
+
crashed = "Traceback (most recent call last)" in (out2 or "")
|
|
929
|
+
mark = "OK " if len(reds2) >= exp and not crashed else "MISMATCH"
|
|
930
|
+
print(" %s %-58s 红项=%d 预期>=%d %s"
|
|
931
|
+
% (mark, name[:58], len(reds2), exp,
|
|
932
|
+
"**未按预期转红**" if len(reds2) < exp
|
|
933
|
+
else ("**镜像崩溃而非判据转红**" if crashed else "")))
|
|
934
|
+
for r in reds2[:4]:
|
|
935
|
+
print(" " + r)
|
|
936
|
+
if crashed:
|
|
937
|
+
for line in (out2 or "").splitlines()[-6:]:
|
|
938
|
+
print(" | " + line)
|
|
939
|
+
if len(reds2) < exp or crashed:
|
|
940
|
+
mismatch.append("%s(红=%d 预期>=%d%s)"
|
|
941
|
+
% (name, len(reds2), exp,
|
|
942
|
+
",且镜像崩溃" if crashed else ""))
|
|
943
|
+
if anchor_miss:
|
|
944
|
+
print("\nANCHOR-MISS:%s" % "、".join(anchor_miss))
|
|
945
|
+
print("退出码 2(fail-closed):变异表锚点漂移即判失败,不得静默跳过")
|
|
946
|
+
return 2
|
|
947
|
+
bad += mismatch
|
|
948
|
+
print("\n定点变异自证:%s"
|
|
949
|
+
% ("PASS(每处判据都被打红)" if not bad else "FAIL —— " + "、".join(bad)))
|
|
950
|
+
return 0 if not bad else 1
|
|
951
|
+
finally:
|
|
952
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
def main() -> int:
|
|
956
|
+
try:
|
|
957
|
+
if "--mutate" in sys.argv:
|
|
958
|
+
return _mutate_mode("--list" in sys.argv)
|
|
959
|
+
n = _run_groups()
|
|
960
|
+
print("\n时间核审计 + P3 图检索路守卫:%d 通过,%d 失败,%d 跳过"
|
|
961
|
+
% (len(_PASS), n, len(_SKIP)))
|
|
962
|
+
return 0 if not n else 1
|
|
963
|
+
finally:
|
|
964
|
+
shutil.rmtree(_TMP, ignore_errors=True)
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
if __name__ == "__main__":
|
|
968
|
+
sys.exit(main())
|