@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,527 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""权重刷新与衰减(设计稿 §七 · P4)——**进主分数**的刷新/衰减乘子。
|
|
3
|
+
|
|
4
|
+
形态(§7.1 已裁)
|
|
5
|
+
-----------------
|
|
6
|
+
score ← score × cred_factor(γ, Δt) × refresh(access_count, last_access)
|
|
7
|
+
|
|
8
|
+
落位(§7.2 已裁):与「分池权重乘数」**同一乘子位**(`MdCG._score` 里
|
|
9
|
+
`raw = clamp(raw * pooling.weight_of(...))` 那一行)——**不新开乘子链**。
|
|
10
|
+
本模块只提供**乘子本体与读点**,接线在 `mdcg._score`。
|
|
11
|
+
|
|
12
|
+
**这条乘子影响哪些检索路**(读现场确认,2026-10-01):唯一生效点是
|
|
13
|
+
`MdCG._score` ⇒ 走它的路全部受影响:
|
|
14
|
+
· `MdCG.search` 的 T0–T3 各腿(桶内 LIKE / 全局 LIKE / 全局扫描);
|
|
15
|
+
· `MdCos._lexical`(`search_rrf` 的 **lexical** 路)与 `_path_bucket`
|
|
16
|
+
(bucket 路)。
|
|
17
|
+
**不经** `_score` 的路因此**不受**本乘子影响:`entity`(恒 1.0)、`graph`
|
|
18
|
+
(种子分 × 0.5)、`chain`、`temporal`、`fuzzy`、`semantic`、`goal`。
|
|
19
|
+
Rust 侧同款:乘子只在 `sort_path(..., apply_freshness=true)` 生效,
|
|
20
|
+
而 `fuse` **只对 lexical 路**传 true(见 `rust/src/freshness.rs` 头注)。
|
|
21
|
+
|
|
22
|
+
核的唯一权威(硬约束)
|
|
23
|
+
----------------------
|
|
24
|
+
`md_cg/whitebox_kb/aeis_core/time_core.py` 的 `cred_factor` ——该模块 docstring
|
|
25
|
+
逐字点名「任何在本模块之外出现的衰减核实现(`exp(-t/τ)`、`×(1-factor)`、
|
|
26
|
+
EMA 保持率)都是 bug」。本模块**不写任何指数核**(`math.exp` 一次都不出现)。
|
|
27
|
+
|
|
28
|
+
γ 与 Δt 口径(§〇.3-12 已裁)
|
|
29
|
+
-----------------------------
|
|
30
|
+
· γ 缺省 = `ln2 / 30 天`(半衰期 30 天,与 `links.DECAY_DAYS` 同刻度);
|
|
31
|
+
**唯一读取点**是 `mdcos.temporal_gamma()`(P3-temporal 已落),本模块复用
|
|
32
|
+
**不新建第二个 γ 读取点**(env `MDCG_TEMPORAL_GAMMA` 同时覆盖两条路)。
|
|
33
|
+
· Δt 取 `created_at`(写入时刻,主链必有)。
|
|
34
|
+
· **单位必须是「天」**:γ 是「每天」的速率,Δt 先 /86400 再喂;喂秒会让衰减
|
|
35
|
+
快 86400 倍。单位常量与断言面见 `DT_UNIT` 与 `md_cg/test_p4_freshness.py`。
|
|
36
|
+
|
|
37
|
+
刷新参数**照抄**(不自行发明)
|
|
38
|
+
------------------------------
|
|
39
|
+
AEIS `consolidate_cycle(rehearsal_threshold=0.7, degrade_threshold=0.2,
|
|
40
|
+
gain=0.01)`(`md_cg/whitebox_kb/aeis_core/core.py:4801-4831`,docstring 逐字
|
|
41
|
+
「P1-4 巩固周期:高重要度演练(刷新访问)+ 低重要度降权 + 压缩候选标记」):
|
|
42
|
+
|
|
43
|
+
· 刷新:`importance ≥ 0.7` → 计入演练;且 `access_count % 10 == 0` → +gain;
|
|
44
|
+
· 降权:`importance < 0.2` 且 `access_count ≤ 2` → −gain;
|
|
45
|
+
· 跳过:`anchor`/`structure` 层、`protected`/`no_forget`(本模块同款跳过——
|
|
46
|
+
受保护节点不因衰减穿越保护线,见 §7.3)。
|
|
47
|
+
|
|
48
|
+
floor(§7.3 · 移植隐患的第二道闸)
|
|
49
|
+
----------------------------------
|
|
50
|
+
AEIS 的 `decay_cycle` **无 floor**,而 md_cg 的保护判定是**即时读
|
|
51
|
+
`importance ≥ 0.70`**(`md_cg/protect.py:135-151`、`weights.IMPORTANCE_PROTECT`)
|
|
52
|
+
⇒ 衰减穿过 0.70 会让节点**自动失去保护**。故:
|
|
53
|
+
|
|
54
|
+
· 衰减项带 floor(`SCORE_FLOOR`,与 P3-temporal 同值同源理由);
|
|
55
|
+
· **受保护节点**的衰减乘子另有下界 `IMPORTANCE_PROTECT / importance`,
|
|
56
|
+
保证 `importance × 乘子 ≥ IMPORTANCE_PROTECT` **恒成立**——保护判定不可能
|
|
57
|
+
被衰减翻转(推导见 `protected_floor`);
|
|
58
|
+
· 保留 `weights.recalc` 既有的「`after ≥ 0.70` 自动补写 `protected`」行为
|
|
59
|
+
(本模块**不改** `weights.recalc`,只在守卫里断言其仍在位)。
|
|
60
|
+
|
|
61
|
+
语义边界(§7.3 末条,记忆里那条裁定)
|
|
62
|
+
-------------------------------------
|
|
63
|
+
**不可遗忘 ≠ 不可覆盖**。衰减属「降级/搬迁」族:
|
|
64
|
+
· 降级**不得实现为删除记录**——`recalc(apply=True)` 只写
|
|
65
|
+
`frontmatter.freshness_weight` 与 `freshness_state`,**从不删节点**;
|
|
66
|
+
· 需要跨层降级时走既有 `_move_layer`(其内已调 `protect.guard_move`),
|
|
67
|
+
**不新写搬迁路径**;受保护节点被移出保护层仍需显式 `override=True`
|
|
68
|
+
(`guard_move` 原样语义)。
|
|
69
|
+
|
|
70
|
+
三件套(§7.4,与 `weights.recalc` 同款)
|
|
71
|
+
----------------------------------------
|
|
72
|
+
预演(`recalc(apply=False)`)→ 逐节点留痕(`_maintain.jsonl`,
|
|
73
|
+
`action="freshness"`,与 `weights` 的 `action="importance"` 同族)→ 反向
|
|
74
|
+
apply(`rollback`,读回 `before` 并再记一条 `freshness_rollback`)。
|
|
75
|
+
AEIS 侧**没有**预演与回滚(`consolidate_cycle`/`decay_cycle` 均无)——
|
|
76
|
+
这是移植时的**净增工作量**,不省。
|
|
77
|
+
|
|
78
|
+
零第三方依赖。
|
|
79
|
+
"""
|
|
80
|
+
from __future__ import annotations
|
|
81
|
+
|
|
82
|
+
import os
|
|
83
|
+
import time
|
|
84
|
+
|
|
85
|
+
from .fsutil import append_jsonl, read_jsonl
|
|
86
|
+
# `_maintain.jsonl` 与保护下限的**单一真源**在 weights(同族留痕共用一份文件名)
|
|
87
|
+
from .weights import (APPLY_DELTA, IMPORTANCE_PROTECT, MAINTAIN_LOG,
|
|
88
|
+
node_importance)
|
|
89
|
+
from .whitebox_kb.aeis_core import time_core as _tc
|
|
90
|
+
|
|
91
|
+
# --------------------------------------------------------------------------
|
|
92
|
+
# 参数(照抄 AEIS consolidate_cycle;出处见模块 docstring)
|
|
93
|
+
# --------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
#: AEIS `consolidate_cycle` 的三个既有取值 + 两个结构常数(逐字照抄,不发明)。
|
|
96
|
+
REFRESH_PARAMS = {
|
|
97
|
+
"rehearsal_threshold": 0.7, # importance ≥ 此值 → 高重要度演练(刷新)
|
|
98
|
+
"degrade_threshold": 0.2, # importance < 此值 且 access_count ≤ 2 → 降权
|
|
99
|
+
"gain": 0.01, # 每档步长(AEIS 原有)
|
|
100
|
+
"cycle": 10, # access_count % 10 == 0 才 +gain(AEIS 原有)
|
|
101
|
+
"degrade_max_access": 2, # 低重要度降权的访问数上限(AEIS 原有)
|
|
102
|
+
"source": ("md_cg/whitebox_kb/aeis_core/core.py:4801-4831 "
|
|
103
|
+
"consolidate_cycle(rehearsal_threshold=0.7, "
|
|
104
|
+
"degrade_threshold=0.2, gain=0.01)"),
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#: Δt 的单位(**最易错处**):γ 是「每天」的速率,Δt 必须按天喂。
|
|
108
|
+
DT_UNIT = "day"
|
|
109
|
+
SECONDS_PER_DAY = 86400.0
|
|
110
|
+
|
|
111
|
+
#: Δt 的**粒度**:整日(向下取整)。为什么量化(本批的一处设计取舍,如实登记):
|
|
112
|
+
#: ① γ 的单位本来就是「天」(半衰期 30 天档),亚日精度对结果无信息量;
|
|
113
|
+
#: ② 仓内有「同查询两轮结果逐位一致 / 可复算」的硬纪律
|
|
114
|
+
#: (`md_cg/test_retr_score_once.py` D1b 就是这条断言)——不量化则分数随
|
|
115
|
+
#: `time.time()` 逐轮微变,两轮比较恒不等;
|
|
116
|
+
#: ③ 两侧(Python / Rust)各自取时钟,量化到同一「天」桶后跨语言同值,
|
|
117
|
+
#: 消除 ms 级时钟差带来的 6 位小数抖动风险。
|
|
118
|
+
#: 量化损失上界 = γ × 1 天 = ln2/30 ≈ 2.3%(即「同一天内不再区分」)。
|
|
119
|
+
DT_QUANTUM_DAYS = 1.0
|
|
120
|
+
|
|
121
|
+
#: 衰减项 floor:`cred_factor(gamma, dt, floor, ceil)` 的 floor。
|
|
122
|
+
#: 取值理由与 P3-temporal 同源(0.5**10 ≈ 9.77e-4 < 1e-3 —— 30 天半衰期下
|
|
123
|
+
#: 300 天 = 10 个半衰期后继续区分已无信息量);floor 让**老节点/缺 created_at
|
|
124
|
+
#: 的节点**仍带非零分参与排序,而不是被乘成 0 后不可召回。
|
|
125
|
+
SCORE_FLOOR = 0.001
|
|
126
|
+
|
|
127
|
+
#: 衰减乘子上限(乘子恒 ≤ 1.0:衰减只能降权,刷新项负责 > 1 的那半边)。
|
|
128
|
+
SCORE_CEIL = 1.0
|
|
129
|
+
|
|
130
|
+
#: 总开关(缺省开;=0 时乘子恒为 1.0 ⇒ 与改动前逐位一致,是「可回退」的
|
|
131
|
+
#: 第一道闸——第二道是三件套的 rollback)。
|
|
132
|
+
FRESHNESS_ENV = "MDCG_FRESHNESS"
|
|
133
|
+
|
|
134
|
+
#: 观测时刻的可复算冻结口(**只为守卫/探针的确定性**;未设 = `time.time()`)。
|
|
135
|
+
#: 有了它,两侧探针能在同一 `now` 上逐位比对乘子。
|
|
136
|
+
NOW_ENV = "MDCG_FRESHNESS_NOW"
|
|
137
|
+
|
|
138
|
+
#: 降级判定面(`recalc` 用):乘子低于此值的节点标记 `freshness_state`
|
|
139
|
+
#: = "degraded"(**只标记,不删除**——见模块 docstring 的语义边界)。
|
|
140
|
+
DEMOTE_BELOW = 0.5
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# 生效条件:environ(缺省 os.environ)里 FRESHNESS_ENV 为真值且归一后属 {"0","false","no","off"} 之外的值时返回 True;变量缺失/空串/命中关闭词表时返回 False;
|
|
144
|
+
def enabled(environ=None) -> bool:
|
|
145
|
+
"""刷新/衰减乘子是否生效(缺省**开**:§7.2「进主分数」)。"""
|
|
146
|
+
raw = (environ or os.environ).get(FRESHNESS_ENV)
|
|
147
|
+
if raw is None or str(raw).strip() == "":
|
|
148
|
+
return True
|
|
149
|
+
return str(raw).strip().lower() not in ("0", "false", "no", "off")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# 生效条件:environ(缺省 os.environ)里 NOW_ENV 可 float() 解析时返回该浮点值;缺失/空串/不可解析时返回 time.time();
|
|
153
|
+
def freshness_now(environ=None) -> float:
|
|
154
|
+
"""乘子用的「当前时刻」(unix 秒)。**单一读取点**(守卫与探针共用)。"""
|
|
155
|
+
raw = (environ or os.environ).get(NOW_ENV)
|
|
156
|
+
if raw:
|
|
157
|
+
try:
|
|
158
|
+
return float(raw)
|
|
159
|
+
except (TypeError, ValueError):
|
|
160
|
+
return time.time()
|
|
161
|
+
return time.time()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# 生效条件:gamma 非正或不可转 float 时回落 `mdcos.temporal_gamma()`;否则返回 float(gamma);无 IO、无副作用;
|
|
165
|
+
def resolve_gamma(gamma=None) -> float:
|
|
166
|
+
"""γ 的解析(缺省回落 **P3 的同一读取点** `mdcos.temporal_gamma()`)。"""
|
|
167
|
+
if gamma is not None:
|
|
168
|
+
try:
|
|
169
|
+
g = float(gamma)
|
|
170
|
+
if g > 0:
|
|
171
|
+
return g
|
|
172
|
+
except (TypeError, ValueError):
|
|
173
|
+
pass
|
|
174
|
+
from .mdcos import temporal_gamma # 懒导入:避免模块级循环依赖
|
|
175
|
+
return temporal_gamma()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# --------------------------------------------------------------------------
|
|
179
|
+
# 核(一律经 time_core;本模块不写指数核)
|
|
180
|
+
# --------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
# 生效条件:gamma 与 dt_days(单位=天)任意可转 float 时,返回 time_core.cred_factor(gamma, dt_days, floor, ceil);dt 非有限值(inf/nan)时同样交给 cred_factor(inf → 取 floor);
|
|
183
|
+
def decay_factor(gamma: float, dt_days: float, floor: float = SCORE_FLOOR,
|
|
184
|
+
ceil: float = SCORE_CEIL) -> float:
|
|
185
|
+
"""时间衰减乘子 `e^(-γ·Δt)`(**核来自唯一权威** `time_core.cred_factor`)。
|
|
186
|
+
|
|
187
|
+
⚠ `dt_days` 的单位**必须**是「天」(`DT_UNIT`);调用方负责换算,
|
|
188
|
+
换算单点是 `entry_weight`(`(now - created_at) / SECONDS_PER_DAY`)。
|
|
189
|
+
"""
|
|
190
|
+
if not enabled():
|
|
191
|
+
return 1.0
|
|
192
|
+
return _tc.cred_factor(float(gamma), float(dt_days), float(floor),
|
|
193
|
+
float(ceil))
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# 生效条件:access_count 与 last_access 任意(不可转数值按 0 处理)、importance 任意可转 float;按 REFRESH_PARAMS 的三档判定返回 1.0+gain / 1.0−gain / 1.0;
|
|
197
|
+
def refresh_factor(access_count=0, last_access=0, importance=0.0) -> float:
|
|
198
|
+
"""访问刷新/降权乘子(**门槛与步长照抄 AEIS** `consolidate_cycle`)。
|
|
199
|
+
|
|
200
|
+
判定序(与 AEIS 同序同门):
|
|
201
|
+
|
|
202
|
+
1. **刷新**:`importance ≥ rehearsal_threshold` 且 `access_count % 10 == 0`
|
|
203
|
+
且 `last_access > 0`(真的被访问过)→ `1 + gain`;
|
|
204
|
+
2. **降权**:`importance < degrade_threshold` 且
|
|
205
|
+
`access_count ≤ degrade_max_access` → `1 − gain`;
|
|
206
|
+
3. 其余 → `1.0`。
|
|
207
|
+
|
|
208
|
+
`last_access > 0` 这一条是本模块对 AEIS 的唯一补足:AEIS 的 `% 10 == 0`
|
|
209
|
+
在**从未被访问**(`access_count == 0`)时也为真——照抄原样会让「刚写、
|
|
210
|
+
从未调用的高重要度节点」白拿一次刷新。加此闸后「未被调用」只能不刷新,
|
|
211
|
+
不会反被奖励(语义仍与 §7.1「被经常调用的记忆其权重被刷新」一致)。
|
|
212
|
+
"""
|
|
213
|
+
if not enabled():
|
|
214
|
+
return 1.0
|
|
215
|
+
p = REFRESH_PARAMS
|
|
216
|
+
try:
|
|
217
|
+
ac = int(float(access_count or 0))
|
|
218
|
+
la = float(last_access or 0)
|
|
219
|
+
imp = float(importance or 0.0)
|
|
220
|
+
except (TypeError, ValueError):
|
|
221
|
+
return 1.0
|
|
222
|
+
if imp >= p["rehearsal_threshold"] and la > 0 and ac % p["cycle"] == 0:
|
|
223
|
+
return 1.0 + p["gain"]
|
|
224
|
+
if imp < p["degrade_threshold"] and ac <= p["degrade_max_access"]:
|
|
225
|
+
return 1.0 - p["gain"]
|
|
226
|
+
return 1.0
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# 生效条件:entry 为可取 protected / importance 的映射;protected 为假值时返回 SCORE_FLOOR;为真值时返回 max(SCORE_FLOOR, min(1.0, IMPORTANCE_PROTECT / importance))(importance 不可转数值或 ≤0 时按 1.0 处理);
|
|
230
|
+
def protected_floor(entry) -> float:
|
|
231
|
+
"""受保护节点的**衰减乘子下界**(§7.3 的第二道闸)。
|
|
232
|
+
|
|
233
|
+
推导(为什么是 `IMPORTANCE_PROTECT / importance`):
|
|
234
|
+
md_cg 的保护判定是**即时读** `importance ≥ IMPortANCE_PROTECT(0.70)`
|
|
235
|
+
(`protect.py` / `weights.py`)。衰减乘子 `m ≤ 1` 会让
|
|
236
|
+
`importance × m < 0.70` ⇒ 节点**自动失去保护**(AEIS 移植的隐患)。
|
|
237
|
+
取 `m ≥ 0.70 / importance`,则 `importance × m ≥ 0.70` **恒成立**——
|
|
238
|
+
保护判定不可能被衰减翻转。
|
|
239
|
+
下界被夹到 ≤ 1.0:`importance < 0.70` 的受保护节点(人工 `protect.mark`)
|
|
240
|
+
乘子取 1.0(不衰减),同样不损失保护。
|
|
241
|
+
"""
|
|
242
|
+
e = entry or {}
|
|
243
|
+
if not e.get("protected"):
|
|
244
|
+
return SCORE_FLOOR
|
|
245
|
+
try:
|
|
246
|
+
imp = float(e.get("importance") or 0.0)
|
|
247
|
+
except (TypeError, ValueError):
|
|
248
|
+
imp = 0.0
|
|
249
|
+
if imp <= 0:
|
|
250
|
+
imp = 1.0
|
|
251
|
+
return max(SCORE_FLOOR, min(1.0, IMPORTANCE_PROTECT / imp))
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
# 生效条件:entry 为可取 created_at / access_count / last_access / importance / protected 的映射;fm 为真值时上述五个键优先取自 fm(缺键回落 entry);now 为假值(None)时取 freshness_now();无条件返回 (乘子, 依据 dict)——乘子 = decay_factor(gamma, |now-created_at|/86400, protected_floor(取数后的映射), SCORE_CEIL) × refresh_factor(...);created_at 缺失/不可转数值/≤0 时 dt_days 取 inf(判「距参照无穷远」,由 floor 兜住);
|
|
255
|
+
def entry_weight(entry, now=None, gamma=None, fm=None) -> tuple:
|
|
256
|
+
"""索引条目 → 分数乘子 `(乘子, 依据)`(**检索期唯一取数点**,免读文件)。
|
|
257
|
+
|
|
258
|
+
`entry` 直接来自索引快照(`_node_entry` 已落 `created_at` /
|
|
259
|
+
`access_count` / `last_access`)——**不读盘**(§7.2 卡点一)。
|
|
260
|
+
|
|
261
|
+
`fm` 为可选覆盖源(写面刚落的 frontmatter 可能比索引条目新):只为
|
|
262
|
+
避免在热路径上拷贝 dict 而设,取数优先级 `fm > entry > 缺省`。
|
|
263
|
+
"""
|
|
264
|
+
e = entry or {}
|
|
265
|
+
keys = ("created_at", "access_count", "last_access", "importance",
|
|
266
|
+
"protected")
|
|
267
|
+
if fm is not None:
|
|
268
|
+
src = {k: (fm.get(k) if fm.get(k) is not None else e.get(k))
|
|
269
|
+
for k in keys}
|
|
270
|
+
else:
|
|
271
|
+
src = {k: e.get(k) for k in keys}
|
|
272
|
+
if not enabled():
|
|
273
|
+
return 1.0, {"disabled": True}
|
|
274
|
+
g = resolve_gamma(gamma)
|
|
275
|
+
ref = freshness_now() if now is None else float(now)
|
|
276
|
+
try:
|
|
277
|
+
created = float(src.get("created_at") or 0.0)
|
|
278
|
+
except (TypeError, ValueError):
|
|
279
|
+
created = 0.0
|
|
280
|
+
if created <= 0:
|
|
281
|
+
dt_days = float("inf") # 无写入时刻:不猜测,判无穷远
|
|
282
|
+
else:
|
|
283
|
+
# **单位=天**:86400 秒/天,显式换算(γ 是每天速率)
|
|
284
|
+
dt_days = abs(ref - created) / SECONDS_PER_DAY
|
|
285
|
+
# **粒度=整日**(向下取整):保证「同查询两轮逐位一致」(不随 wall clock
|
|
286
|
+
# 微变)且两侧跨语言同值。详见 DT_QUANTUM_DAYS 的说明。
|
|
287
|
+
dt_days = float(int(dt_days / DT_QUANTUM_DAYS)) * DT_QUANTUM_DAYS
|
|
288
|
+
floor = protected_floor(src)
|
|
289
|
+
d = decay_factor(g, dt_days, floor, SCORE_CEIL)
|
|
290
|
+
r = refresh_factor(src.get("access_count"), src.get("last_access"),
|
|
291
|
+
src.get("importance"))
|
|
292
|
+
return d * r, {"gamma": g, "dt_days": dt_days, "unit": DT_UNIT,
|
|
293
|
+
"decay": d, "refresh": r, "floor": floor,
|
|
294
|
+
"protected": bool(src.get("protected"))}
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# --------------------------------------------------------------------------
|
|
298
|
+
# 三件套:预演 / 逐节点留痕 / 反向 apply(与 weights.recalc 同款)
|
|
299
|
+
# --------------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
# 生效条件:cg.index["nodes"] 为 dict 时按 layer 过滤、limit 为真值才截断;dry-run 只出报表(**不接档位闸**——零写盘面照旧);apply=True 时先经 autonomy_modes.e_gate() 档位判定(补强批次 v1.2·E 面接线,位置=任何盘面写入之前):非 ALLOW(plan 档无计划)即经 forbidden_result() fail-closed 早退(零写盘、零审计行、带 hint),ALLOW(confirm/full)则逐节点把乘子(round 6)写入 frontmatter.freshness_weight 与 freshness_components,乘子 < DEMOTE_BELOW 时另写 freshness_state="degraded"(**只标记,不删记录**),乘子下降且 demote_layer 给定时经 cg._move_layer(内含 protect.guard_move)搬迁;每条 append_jsonl(action="freshness") 后 rebuild_index;返回报表 dict;
|
|
302
|
+
def recalc(cg, layer=None, limit=None, apply=False, min_delta=APPLY_DELTA,
|
|
303
|
+
actor="maintain", dry_run_samples=10, now=None, demote_layer=None):
|
|
304
|
+
"""刷新/衰减权重的重算(**可预演 + 逐节点留痕 + 可回滚**)。
|
|
305
|
+
|
|
306
|
+
apply=False(默认)只出报表,**零写盘**。
|
|
307
|
+
apply=True 逐节点改写 `frontmatter.freshness_weight`;每节点一条
|
|
308
|
+
`_maintain.jsonl`(`action="freshness"`,与 `weights` 的 `importance`
|
|
309
|
+
同族),并提供 `rollback` 反向应用。
|
|
310
|
+
|
|
311
|
+
⚠ 本函数**从不删除节点**(§7.3「降级不得实现为删除记录」):降级只落
|
|
312
|
+
`freshness_state="degraded"` 标记;要搬迁层位时给出 `demote_layer`,
|
|
313
|
+
走既有 `_move_layer`(其内已调 `protect.guard_move`,受保护节点需显式
|
|
314
|
+
`override`)——不新写第二条搬迁路径。
|
|
315
|
+
|
|
316
|
+
**档位闸(补强批次 v1.2·E 面接线)**:`apply=True` 的**写盘前**先经
|
|
317
|
+
`autonomy_modes.e_gate()`(设计 §三 plan 档 E「须命中计划步骤」)——
|
|
318
|
+
plan 档无计划 ⇒ `forbidden_result()` fail-closed 早退(零写盘、零审计行);
|
|
319
|
+
confirm/full ⇒ ALLOW,**原链原样**(逐位不变)。`apply=False`(预演)
|
|
320
|
+
不接闸:零写盘面不属「变更」。
|
|
321
|
+
"""
|
|
322
|
+
nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
|
|
323
|
+
ids = [nid for nid, e in list(nodes.items())
|
|
324
|
+
if not layer or e.get("layer") == layer]
|
|
325
|
+
ids.sort()
|
|
326
|
+
if limit:
|
|
327
|
+
ids = ids[:int(limit)]
|
|
328
|
+
ref = freshness_now() if now is None else float(now)
|
|
329
|
+
batch = time.strftime("%Y%m%d-%H%M%S")
|
|
330
|
+
planned, samples = [], []
|
|
331
|
+
before_sum = after_sum = 0.0
|
|
332
|
+
changed = unchanged = skipped = 0
|
|
333
|
+
by_layer = {}
|
|
334
|
+
for nid in ids:
|
|
335
|
+
e = nodes.get(nid) or {}
|
|
336
|
+
w, why = entry_weight(e, now=ref)
|
|
337
|
+
before = float(e.get("freshness_weight") or 1.0)
|
|
338
|
+
before_sum += before
|
|
339
|
+
after_sum += w
|
|
340
|
+
lay = e.get("layer") or "?"
|
|
341
|
+
bl = by_layer.setdefault(lay, {"nodes": 0, "changed": 0})
|
|
342
|
+
bl["nodes"] += 1
|
|
343
|
+
if abs(w - before) < float(min_delta):
|
|
344
|
+
unchanged += 1
|
|
345
|
+
continue
|
|
346
|
+
changed += 1
|
|
347
|
+
bl["changed"] += 1
|
|
348
|
+
plan = {"node_id": nid, "layer": lay, "batch": batch,
|
|
349
|
+
"before": round(before, 6), "after": round(w, 6),
|
|
350
|
+
"delta": round(w - before, 6), "why": why,
|
|
351
|
+
"protected": bool(e.get("protected")),
|
|
352
|
+
# 降级判定(只标记):低于阈值即为 degraded 候选
|
|
353
|
+
"degrade": bool(w < DEMOTE_BELOW)}
|
|
354
|
+
planned.append(plan)
|
|
355
|
+
if len(samples) < int(dry_run_samples):
|
|
356
|
+
samples.append(plan)
|
|
357
|
+
written, degraded = 0, 0
|
|
358
|
+
if apply:
|
|
359
|
+
# 三档自治(补强批次 v1.2·E 面接线,设计 §三 plan 档 E「须命中计划步骤」):
|
|
360
|
+
# E 类动作**写盘前**的档位判定单点。本函数无独立资格闸(逐节点取数在下方
|
|
361
|
+
# 循环里、取不到即 skip),故闸口取「进入写盘支路的第一行」=任何盘面写入
|
|
362
|
+
# 之前;放行分支即原链原样(confirm/full 逐位不变),plan 档无计划
|
|
363
|
+
# ⇒ fail-closed 早退,**零写盘、零 _maintain.jsonl 行**。
|
|
364
|
+
from . import autonomy_modes as _am
|
|
365
|
+
_dec = _am.e_gate()
|
|
366
|
+
if _dec["decision"] != _am.ALLOW:
|
|
367
|
+
return _am.forbidden_result(_dec, action="freshness", dry_run=False,
|
|
368
|
+
written=0, batch=batch)
|
|
369
|
+
if demote_layer:
|
|
370
|
+
bind_move_layer(cg) # 跨层降级走既有 _move_layer(现绑)
|
|
371
|
+
for plan in planned:
|
|
372
|
+
nid = plan["node_id"]
|
|
373
|
+
node = cg.get(nid)
|
|
374
|
+
if not node:
|
|
375
|
+
skipped += 1
|
|
376
|
+
continue
|
|
377
|
+
fm = node.get("frontmatter") or {}
|
|
378
|
+
fm["freshness_weight"] = plan["after"]
|
|
379
|
+
fm["freshness_source"] = "freshness_recalc"
|
|
380
|
+
fm["freshness_components"] = plan["why"]
|
|
381
|
+
if plan["degrade"]:
|
|
382
|
+
# **降级 = 标记**(不是删除记录):节点与正文原样保留
|
|
383
|
+
fm["freshness_state"] = "degraded"
|
|
384
|
+
degraded += 1
|
|
385
|
+
path = os.path.join(cg.root, (nodes.get(nid) or {}).get("path")
|
|
386
|
+
or node.get("path") or f"{nid}.md")
|
|
387
|
+
cg._write_node(nid, path, fm, node.get("content") or "")
|
|
388
|
+
e = nodes.get(nid)
|
|
389
|
+
if e is not None:
|
|
390
|
+
e["freshness_weight"] = plan["after"]
|
|
391
|
+
e["freshness_state"] = fm.get("freshness_state")
|
|
392
|
+
rec = {"t": time.time(), "action": "freshness", "batch": batch,
|
|
393
|
+
"id": nid, "layer": plan["layer"],
|
|
394
|
+
"before": plan["before"], "after": plan["after"],
|
|
395
|
+
"delta": plan["delta"], "degrade": plan["degrade"],
|
|
396
|
+
"why": plan["why"], "actor": actor}
|
|
397
|
+
append_jsonl(os.path.join(cg.root, MAINTAIN_LOG), rec)
|
|
398
|
+
written += 1
|
|
399
|
+
# 跨层降级:走后缀既有 `_move_layer`(内含 protect.guard_move)
|
|
400
|
+
if demote_layer and plan["degrade"] and _move_layer is not None:
|
|
401
|
+
try:
|
|
402
|
+
_move_layer(nid, demote_layer,
|
|
403
|
+
reason=f"freshness degraded (w={plan['after']})")
|
|
404
|
+
except Exception as exc: # noqa: BLE001
|
|
405
|
+
plan["move_error"] = f"{type(exc).__name__}: {exc}"
|
|
406
|
+
if written and hasattr(cg, "rebuild_index"):
|
|
407
|
+
cg.rebuild_index()
|
|
408
|
+
n = max(1, len(ids))
|
|
409
|
+
return {
|
|
410
|
+
"ok": True, "action": "freshness", "dry_run": not apply, "batch": batch,
|
|
411
|
+
"nodes_scanned": len(ids), "changed": changed, "unchanged": unchanged,
|
|
412
|
+
"skipped": skipped, "written": written, "degraded": degraded,
|
|
413
|
+
"min_delta": float(min_delta), "now": ref,
|
|
414
|
+
"gamma": resolve_gamma(), "unit": DT_UNIT,
|
|
415
|
+
"avg_before": round(before_sum / n, 4),
|
|
416
|
+
"avg_after": round(after_sum / n, 4),
|
|
417
|
+
"by_layer": dict(by_layer), "samples": samples, "log": MAINTAIN_LOG,
|
|
418
|
+
"note": ("dry-run:未写盘;apply=True 才写 freshness_weight"
|
|
419
|
+
if not apply else
|
|
420
|
+
f"已写 {written} 个节点(降级标记 {degraded});"
|
|
421
|
+
f"回滚见 rollback(batch={batch})"),
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
#: 惰性持有的跨层降级入口(`MdCG._move_layer` 是实例方法:`recalc` 里现取)。
|
|
426
|
+
#: 这里保留模块级名字只为**可读性**(下方 `_bind_move_layer` 现绑)。
|
|
427
|
+
_move_layer = None
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
# 生效条件:cg 具备可调用的 _move_layer 时把 recalc 用的跨层降级入口绑定为该实例方法,返回 True;否则返回 False(降级只标记、不搬迁);
|
|
431
|
+
def bind_move_layer(cg) -> bool:
|
|
432
|
+
"""把 `recalc(..., demote_layer=...)` 用的搬迁入口绑到实例方法上。
|
|
433
|
+
|
|
434
|
+
为什么需要它:`recalc` 是**模块级函数**(与 `weights.recalc` 同构),而
|
|
435
|
+
`_move_layer` 是实例方法。不新写第二条搬迁路径 ⇒ 只能绑既有那一条。
|
|
436
|
+
"""
|
|
437
|
+
global _move_layer
|
|
438
|
+
fn = getattr(cg, "_move_layer", None)
|
|
439
|
+
_move_layer = fn if callable(fn) else None
|
|
440
|
+
return _move_layer is not None
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
# 生效条件:cg.root/MAINTAIN_LOG 可读出 action=="freshness" 记录后,entry_ids 为真值时按记录 id 过滤;否则 batch 为真值时按 r.get("batch")==batch 过滤;否则取最后一条记录的 batch 再按其过滤;无记录返回 {"ok":False,"error":"no_records","reverted":0},过滤后无记录返回 {"ok":False,"error":"batch_not_found",...};对命中且 cg.get(nid) 为真的记录写回 rec.get("before") 到 frontmatter.freshness_weight(**只改这一列,不删节点**),若 reverted 且 cg 有 rebuild_index 则调用,最后 append_jsonl 写 action="freshness_rollback" 并返回 {"ok":True,"batch":batch,"reverted":reverted,"ids":ids};
|
|
444
|
+
def rollback(cg, batch=None, entry_ids=None, actor="maintain"):
|
|
445
|
+
"""把刷新/衰减反向应用(三件套的第三件;AEIS 侧没有,属净增)。"""
|
|
446
|
+
log_path = os.path.join(cg.root, MAINTAIN_LOG)
|
|
447
|
+
records = [r for r in read_jsonl(log_path)
|
|
448
|
+
if r.get("action") == "freshness"]
|
|
449
|
+
if not records:
|
|
450
|
+
return {"ok": False, "error": "no_records", "reverted": 0}
|
|
451
|
+
if entry_ids:
|
|
452
|
+
want = {str(x) for x in entry_ids}
|
|
453
|
+
records = [r for r in records if str(r.get("id")) in want]
|
|
454
|
+
elif batch:
|
|
455
|
+
records = [r for r in records if r.get("batch") == batch]
|
|
456
|
+
else:
|
|
457
|
+
batch = records[-1].get("batch")
|
|
458
|
+
records = [r for r in records if r.get("batch") == batch]
|
|
459
|
+
if not records:
|
|
460
|
+
return {"ok": False, "error": "batch_not_found", "batch": batch,
|
|
461
|
+
"reverted": 0}
|
|
462
|
+
nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
|
|
463
|
+
reverted, ids = 0, []
|
|
464
|
+
for rec in records:
|
|
465
|
+
nid = rec.get("id")
|
|
466
|
+
node = cg.get(nid)
|
|
467
|
+
if not node:
|
|
468
|
+
continue
|
|
469
|
+
fm = node.get("frontmatter") or {}
|
|
470
|
+
fm["freshness_weight"] = rec.get("before")
|
|
471
|
+
fm["freshness_source"] = "freshness_rollback"
|
|
472
|
+
# 降级标记随回滚撤销(回到降级前状态);**节点与正文一字不动**
|
|
473
|
+
if not rec.get("degrade"):
|
|
474
|
+
fm.pop("freshness_state", None)
|
|
475
|
+
path = os.path.join(cg.root, (nodes.get(nid) or {}).get("path")
|
|
476
|
+
or node.get("path") or f"{nid}.md")
|
|
477
|
+
cg._write_node(nid, path, fm, node.get("content") or "")
|
|
478
|
+
e = nodes.get(nid)
|
|
479
|
+
if e is not None:
|
|
480
|
+
e["freshness_weight"] = rec.get("before")
|
|
481
|
+
reverted += 1
|
|
482
|
+
ids.append(nid)
|
|
483
|
+
if reverted and hasattr(cg, "rebuild_index"):
|
|
484
|
+
cg.rebuild_index()
|
|
485
|
+
append_jsonl(log_path, {"t": time.time(), "action": "freshness_rollback",
|
|
486
|
+
"batch": batch, "reverted": reverted,
|
|
487
|
+
"actor": actor})
|
|
488
|
+
return {"ok": True, "batch": batch, "reverted": reverted, "ids": ids}
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
# 生效条件:cg.root/MAINTAIN_LOG 可读出 JSONL 记录后,仅取 action=="freshness" 的记录并返回其尾部 int(limit) 条(limit 为 0 时返回全部);
|
|
492
|
+
def history(cg, limit=100) -> list:
|
|
493
|
+
"""刷新/衰减留痕(最近 limit 条)。"""
|
|
494
|
+
recs = [r for r in read_jsonl(os.path.join(cg.root, MAINTAIN_LOG))
|
|
495
|
+
if r.get("action") == "freshness"]
|
|
496
|
+
return recs[-int(limit):]
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
# 生效条件:无条件返回模块自描述 dict(公式 / 核出处 / γ 口径 / 单位 / floor / 参数出处 / 开关 / 三件套名),供守卫与文档对拍;
|
|
500
|
+
def catalog() -> dict:
|
|
501
|
+
return {"formula": "score ← score × cred_factor(γ, Δt) × refresh(ac, la)",
|
|
502
|
+
"core": "md_cg/whitebox_kb/aeis_core/time_core.py::cred_factor",
|
|
503
|
+
"gamma_default": "ln2 / links.DECAY_DAYS(=30 天)",
|
|
504
|
+
"dt_unit": DT_UNIT, "dt_source": "created_at",
|
|
505
|
+
"score_floor": SCORE_FLOOR, "score_ceil": SCORE_CEIL,
|
|
506
|
+
"refresh_params": dict(REFRESH_PARAMS),
|
|
507
|
+
"protect_floor": "IMPORTANCE_PROTECT/importance(受保护节点)",
|
|
508
|
+
"enabled_env": FRESHNESS_ENV, "now_env": NOW_ENV,
|
|
509
|
+
"triplet": ["recalc(apply=False)", "_maintain.jsonl",
|
|
510
|
+
"rollback()"]}
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
# 生效条件:无条件返回模块自检 dict——核来自 time_core(本模块源码不含指数核调用)、单位常量为天、参数出处非空、weights 侧的保护下限可读;
|
|
514
|
+
def selfcheck() -> dict:
|
|
515
|
+
"""模块自检(守卫的第一条断言面)。"""
|
|
516
|
+
src = open(os.path.abspath(__file__), encoding="utf-8").read()
|
|
517
|
+
# 探针串拼接(不写字面量):否则「检查用的字面量」本身会把检查判红
|
|
518
|
+
needle = "math." + "exp("
|
|
519
|
+
return {"core_is_time_core": "from .whitebox_kb.aeis_core import time_core"
|
|
520
|
+
in src,
|
|
521
|
+
"no_local_exp_core": needle not in src,
|
|
522
|
+
"uses_cred_factor": "cred_factor" in src,
|
|
523
|
+
"dt_unit": DT_UNIT,
|
|
524
|
+
"params_source_set": bool(REFRESH_PARAMS.get("source")),
|
|
525
|
+
"gain": REFRESH_PARAMS["gain"],
|
|
526
|
+
"protect_floor": IMPORTANCE_PROTECT,
|
|
527
|
+
"node_importance_available": callable(node_importance)}
|