@furongjun1999/dsh-memory 0.4.8 → 0.4.9
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 +54 -26
- package/codebuddy/CODEBUDDY.md +196 -195
- package/codebuddy/README.md +13 -1
- package/codebuddy/mcp.json +9 -0
- package/docs/GBrain/345/217/257/345/200/237/351/211/264/347/202/271_/347/201/265/346/236/242/350/220/275/347/202/271/344/272/244/346/216/245_20260919.md +169 -0
- package/docs/README.md +1 -1
- package/docs/discipline/harnesses.yaml +18 -7
- package/docs/discipline/templates/full.md.tmpl +4 -3
- package/docs/experiments/linkref_backfill/candidates_20260917.json +726 -0
- package/docs/experiments/linkref_backfill/candidates_internal_20260917.json +602 -0
- package/docs/experiments/linkref_backfill/candidates_internal_v2.json +603 -0
- package/docs/experiments/linkref_backfill/candidates_secret_20260917.json +884 -0
- package/docs/experiments/linkref_backfill/candidates_secret_v2.json +789 -0
- package/docs/hive//345/244/232/347/253/257harness/351/200/232/344/277/241/345/245/221/347/272/246_v0.1.md +42 -0
- package/docs/hive//346/243/200/347/264/242/346/224/266/346/225/233/345/256/236/346/265/213/344/270/216S1b/350/256/276/350/256/241_v0.1.md +43 -0
- 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 +102 -0
- package/docs/hive//347/234/237/345/256/236/345/272/223/347/253/257/345/210/260/347/253/257/345/256/236/346/265/213_S1b/344/270/216/345/217/254/345/233/236/346/235/203/350/241/241_v0.1.md +57 -0
- package/docs/hive//347/234/237/345/256/236/345/272/223/347/253/257/345/210/260/347/253/257/345/256/236/346/265/213_S7/345/200/222/346/216/222/345/200/231/351/200/211/345/261/202_v0.1.md +126 -0
- package/docs/hive//350/234/202/345/267/242/345/217/214/345/256/236/344/276/213/344/272/222/351/252/214_/350/256/276/350/256/241/345/256/232/347/250/277.md +503 -0
- package/docs/mdcg/D_meta_/345/267/245/347/250/213/345/214/226/346/226/271/346/241/210_v0.2.md +216 -0
- package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.5.md +631 -625
- package/docs/mdcg//344/273/243/347/240/201/350/257/204/345/256/241/344/270/216/346/235/241/344/273/266/345/214/226/346/263/250/351/207/212_/345/245/221/347/272/246_v0.1.md +82 -0
- package/docs/mdcg//345/205/250/345/272/223/344/273/243/347/240/201/350/257/204/345/256/241/344/270/216/346/235/241/344/273/266/345/214/226/346/263/250/351/207/212_/350/256/241/345/210/222_v0.1.md +600 -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 +47 -45
- package/docs/mdcg//345/255/220/344/273/243/347/220/206/351/205/215/347/275/256/346/240/207/345/207/{206_v0.4.md → 206_v0.5.md} +92 -4
- package/docs/mdcg//347/201/265/346/236/242/350/256/260/345/277/206/345/212/250/350/257/215/345/215/217/350/256/256_v1.0-draft.md +172 -0
- package/docs/mdcg//347/216/257/344/272/214_/347/231/275/347/256/261/345/241/253/345/205/205/346/265/201/346/260/264/347/272/277_v0.1.md +31 -0
- package/docs/mdcg//350/267/250/347/253/257/351/252/214/350/257/201/344/270/216/345/220/214/346/255/245/345/215/217/350/256/256_v0.1.md +93 -0
- package/docs/theory//345/271/266/345/217/221/345/277/205/347/204/266/346/200/247/347/220/206/350/256/272_v0.2.md +2 -2
- package/docs/theory//347/220/206/350/256/272_/346/234/272/345/210/266_/344/273/243/347/240/201_/345/256/236/351/252/214_/347/274/272/345/217/243/347/237/251/351/230/265_v0.1.md +3 -3
- package/docs/theory//350/256/244/347/237/245/344/273/243/347/220/206/344/270/216/346/224/266/346/225/233/347/273/223/346/236/204_/346/235/241/344/273/266/350/256/272/351/207/215/346/236/204_v0.1.md +2 -2
- package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +434 -433
- package/docs//347/201/265/346/236/242/350/207/252/346/210/221/346/224/271/350/277/233/345/267/245/344/275/234/350/256/241/345/210/222_/345/244/226/351/203/250/347/240/224/347/251/266/347/263/273/345/210/227/345/220/270/346/224/266_v1_20260919.md +286 -0
- package/dsh/README.md +33 -0
- package/dsh/cordis.yml.example +13 -7
- package/dsh/hive-mcp-probe.mjs +94 -0
- package/dsh/hive-mcp.example.yml +62 -0
- package/dsh/update-lingshu.bat +11 -0
- package/dsh/update-lingshu.ps1 +337 -0
- package/lib/hooks.d.ts +3 -0
- package/lib/hooks.js +17 -23
- package/lib/index.d.ts +4 -2
- package/lib/index.js +29 -6
- package/lib/lib/datapath.d.ts +76 -1
- package/lib/lib/datapath.js +199 -13
- package/lib/lib/mdcg_client.d.ts +6 -3
- package/lib/lib/mdcg_client.js +6 -5
- package/lib/lib/mutual.js +4 -4
- package/lib/lib/token_store.js +4 -5
- package/md_cg/audit.py +17 -2
- package/md_cg/autonomy.py +86 -15
- package/md_cg/backfill.py +36 -1
- package/md_cg/backfill_bigdomain.py +34 -0
- package/md_cg/bench6_arms.py +28 -1
- package/md_cg/bench6_common.py +10 -1
- package/md_cg/bench6_competitors.py +6 -1
- package/md_cg/bench_axis_domain.py +9 -1
- package/md_cg/bench_blind_comp.py +7 -1
- package/md_cg/bench_en_atoms_public.py +9 -0
- package/md_cg/bench_governance.py +348 -0
- package/md_cg/bench_lme_zh.py +16 -1
- package/md_cg/bench_locomo.py +2 -1
- package/md_cg/bench_locomo_zh.py +16 -1
- package/md_cg/bench_locomo_zh_public.py +4 -1
- package/md_cg/bench_longmem.py +2 -1
- package/md_cg/bench_membench.py +27 -1
- package/md_cg/bench_p0.py +4 -1
- package/md_cg/bench_progressive.py +13 -1
- package/md_cg/bench_role_views.py +238 -0
- package/md_cg/bench_task_ab.py +8 -1
- package/md_cg/bench_task_ab_llm.py +13 -1
- package/md_cg/bench_unified_en.py +6 -1
- package/md_cg/bench_zh_mad.py +20 -1
- package/md_cg/blindspot_tickets.py +123 -0
- package/md_cg/branches.py +12 -1
- package/md_cg/build_postings.py +73 -0
- package/md_cg/ccgc.py +67 -2
- package/md_cg/census.py +5 -1
- package/md_cg/chain.py +24 -3
- package/md_cg/codeindex.py +134 -17
- package/md_cg/coldverify.py +265 -0
- package/md_cg/comment_gate.py +338 -0
- package/md_cg/cond_compose.py +190 -0
- package/md_cg/cond_facts.py +155 -0
- package/md_cg/cond_template.json +107 -0
- package/md_cg/condition_anchor.py +143 -0
- package/md_cg/conformance.py +69 -4
- package/md_cg/consistency.py +24 -1
- package/md_cg/consolidate.py +53 -2
- package/md_cg/corpus.py +4 -0
- package/md_cg/crosscheck.py +42 -2
- package/md_cg/crypto.py +35 -1
- package/md_cg/d_meta.py +310 -0
- package/md_cg/datapath.py +201 -26
- package/md_cg/docindex.py +122 -1
- package/md_cg/eval_common.py +29 -1
- package/md_cg/evidence.py +27 -1
- package/md_cg/evolution.py +21 -1
- package/md_cg/export.py +11 -1
- package/md_cg/forgetting.py +23 -1
- package/md_cg/fsutil.py +18 -1
- package/md_cg/hotcache.py +214 -0
- package/md_cg/hyperedge.py +251 -0
- package/md_cg/identity.py +18 -1
- package/md_cg/insight.py +17 -1
- package/md_cg/lexicon/build_cedict_en_zh.py +9 -0
- package/md_cg/lexicon/build_standard_en.py +171 -168
- package/md_cg/lexicon/expand_en_zh.py +6 -0
- package/md_cg/lifecycle.py +12 -1
- package/md_cg/linkref.py +281 -0
- package/md_cg/links.py +29 -1
- package/md_cg/mcp_server.py +362 -43
- package/md_cg/md_whitebox.py +53 -1
- package/md_cg/mdcg.py +1003 -27
- package/md_cg/mdcos.py +558 -36
- package/md_cg/metacognition.py +37 -2
- package/md_cg/migrate.py +4 -0
- package/md_cg/migrate_aeis.py +221 -213
- package/md_cg/migrate_roleplay.py +8 -0
- package/md_cg/migrate_wisdom_graph.py +14 -1
- package/md_cg/mreview/__main__.py +3 -0
- package/md_cg/mreview/bundle.py +8 -0
- package/md_cg/mreview/candidates.py +9 -0
- package/md_cg/mreview/govern.py +21 -1
- package/md_cg/mreview/locate.py +34 -0
- package/md_cg/mreview/pipeline.py +29 -1
- package/md_cg/mreview/ruleset.py +16 -1
- package/md_cg/nodefile.py +233 -3
- package/md_cg/pooling.py +23 -1
- package/md_cg/postings.py +298 -0
- package/md_cg/predict.py +89 -9
- package/md_cg/progressive.py +3 -0
- package/md_cg/protect.py +14 -1
- package/md_cg/protocol.py +372 -0
- package/md_cg/provenance.py +262 -0
- package/md_cg/reach.py +453 -0
- package/md_cg/refindex.py +47 -2
- package/md_cg/refine.py +20 -1
- package/md_cg/roleviews.py +89 -0
- package/md_cg/routing.py +76 -0
- package/md_cg/scrub.py +63 -2
- package/md_cg/security.py +26 -1
- package/md_cg/self_state.py +64 -1
- package/md_cg/selfreport.py +151 -0
- package/md_cg/semantic/canonical.py +5 -0
- package/md_cg/semantic/en_normalizer.py +364 -355
- package/md_cg/semantic/zh_en_atoms.py +139 -136
- package/md_cg/signer.py +41 -1
- package/md_cg/sources.py +583 -547
- package/md_cg/statushdr.py +179 -0
- package/md_cg/stg.py +59 -18
- package/md_cg/subgraph.py +23 -0
- package/md_cg/sustain.py +56 -1
- package/md_cg/tasks.py +26 -2
- package/md_cg/test_autonomy.py +26 -0
- package/md_cg/test_bench_governance.py +102 -0
- package/md_cg/test_blindspot_tickets.py +166 -0
- package/md_cg/test_ccgc.py +10 -0
- package/md_cg/test_codeindex.py +338 -0
- package/md_cg/test_comment_gate.py +187 -0
- package/md_cg/test_cond_compose_anchors.py +76 -0
- package/md_cg/test_condition_anchor.py +82 -0
- package/md_cg/test_d_meta.py +412 -0
- package/md_cg/test_datapath_root.py +188 -0
- package/md_cg/test_gain_gate.py +47 -1
- package/md_cg/test_hot_cold.py +187 -0
- package/md_cg/test_hyperedge.py +245 -0
- package/md_cg/test_linkref.py +306 -0
- package/md_cg/test_md_access_parity.py +15 -3
- package/md_cg/test_mr_m1.py +108 -18
- package/md_cg/test_mr_m3.py +8 -1
- package/md_cg/test_p26_refindex.py +49 -20
- package/md_cg/test_p27_docindex.py +236 -2
- package/md_cg/test_p2_mcp.py +1 -1
- package/md_cg/test_p31_insight.py +24 -0
- package/md_cg/test_p44_md_whitebox.py +14 -1
- package/md_cg/test_protocol.py +243 -0
- package/md_cg/test_reach.py +378 -0
- package/md_cg/test_reach_keys.py +201 -0
- package/md_cg/test_reach_meta_exits.py +145 -0
- package/md_cg/test_read_clip.py +8 -4
- package/md_cg/test_retr_s1.py +340 -0
- package/md_cg/test_retr_s1b.py +209 -0
- package/md_cg/test_retr_s3.py +194 -0
- package/md_cg/test_retr_s4.py +163 -0
- package/md_cg/test_retr_s5.py +200 -0
- package/md_cg/test_retr_s6.py +157 -0
- package/md_cg/test_retr_s7.py +385 -0
- package/md_cg/test_retr_s8_time.py +316 -0
- package/md_cg/test_retr_s9_edges.py +286 -0
- package/md_cg/test_retr_s9_entity_ctx.py +175 -0
- package/md_cg/test_review_conformance.py +59 -2
- package/md_cg/test_role_views.py +354 -0
- package/md_cg/test_trust.py +361 -0
- package/md_cg/test_units_poll.py +71 -0
- package/md_cg/test_v14_fixes.py +397 -0
- package/md_cg/test_validity_filter.py +280 -0
- package/md_cg/test_wisdom_md_store.py +7 -3
- package/md_cg/test_writepipe.py +5 -1
- package/md_cg/theory.py +16 -1
- package/md_cg/tokens.py +40 -8
- package/md_cg/tool_face.py +13 -2
- package/md_cg/trust.py +943 -0
- package/md_cg/twophase.py +12 -1
- package/md_cg/units.py +129 -10
- package/md_cg/vision_evidence.py +24 -1
- package/md_cg/weights.py +24 -1
- package/md_cg/whitebox.py +32 -1
- package/md_cg/whitebox_kb/data/verify_cache.json +21210 -365
- package/md_cg/whitebox_kb/data/verify_savings.jsonl +5078 -0
- package/md_cg/whitebox_kb/wisdom/audit_log/chain_heat.json +10 -10
- package/md_cg/whitebox_kb/wisdom/code_compose.py +113 -6
- package/md_cg/whitebox_kb/wisdom/code_solidified.json +1 -1
- package/md_cg/whitebox_kb/wisdom/verifier.py +340 -55
- package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db +0 -0
- package/md_cg/writelimit.py +18 -4
- package/md_cg/writepipe.py +178 -7
- package/package.json +2 -2
- package/skills/skills/designer-perspective/scripts/__pycache__/designer.cpython-310.pyc +0 -0
- package/skills/skills/designer-perspective/scripts/designer.py +17 -1
- package/skills/skills/designer-perspective/tests/selftest.py +3 -1
- package/src/hooks.ts +17 -21
- package/src/index.ts +33 -6
- package/src/lib/datapath.ts +211 -13
- package/src/lib/mdcg_client.ts +12 -8
- package/src/lib/mutual.ts +411 -411
- package/src/lib/token_store.ts +4 -5
- package/zcode/AGENTS.md +196 -195
- package/zcode/README.md +4 -0
- package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-shm +0 -0
- package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-wal +0 -0
package/md_cg/mdcos.py
CHANGED
|
@@ -34,7 +34,7 @@ from .mdcg import (MdCG, expand_query_terms, bigrams, normalize_en, STATE_ACCEPT
|
|
|
34
34
|
from . import (nodefile, routing, chain, subgraph, forgetting, protect,
|
|
35
35
|
identity, consistency, metacognition, crypto, sustain,
|
|
36
36
|
self_state, predict, evolution, weights, pooling,
|
|
37
|
-
writelimit)
|
|
37
|
+
writelimit, reach, trust, roleviews)
|
|
38
38
|
from .fsutil import (FileLock, atomic_write, append_jsonl, read_jsonl,
|
|
39
39
|
count_jsonl)
|
|
40
40
|
from .security import (Principal, TenantRegistry, AccessDenied,
|
|
@@ -64,6 +64,7 @@ DEFAULT_BUDGET = 1200 # recall 默认 token 预算
|
|
|
64
64
|
DEFAULT_MAX_ITEM_TOKENS = 250
|
|
65
65
|
|
|
66
66
|
|
|
67
|
+
# 生效条件:text 为假值(None/空串)时返回 0,否则按「CJK 0.6/字 + 其余 /4」计算并返回 int(cjk*0.6 + other/4) + 1。
|
|
67
68
|
def est_tokens(text: str) -> int:
|
|
68
69
|
"""确定性 token 估算:CJK 0.6/字 + 其余 /4(与白箱 adapter 口径一致)。"""
|
|
69
70
|
if not text:
|
|
@@ -74,6 +75,7 @@ def est_tokens(text: str) -> int:
|
|
|
74
75
|
return int(cjk * 0.6 + other / 4) + 1
|
|
75
76
|
|
|
76
77
|
|
|
78
|
+
# 生效条件:text 非空且 max_tokens > 0 时返回 est_tokens 口径 ≤ max_tokens 的摘录,text 为空或 max_tokens ≤ 0 时返回空串 '',est_tokens(text) ≤ max_tokens 时原样返回 text。
|
|
77
79
|
def excerpt_tokens(text: str, max_tokens: int) -> str:
|
|
78
80
|
"""按 est_tokens 口径截取正文前 max_tokens 的摘录(用于 recall 的单条上限)。
|
|
79
81
|
|
|
@@ -103,10 +105,12 @@ def excerpt_tokens(text: str, max_tokens: int) -> str:
|
|
|
103
105
|
return out
|
|
104
106
|
|
|
105
107
|
|
|
108
|
+
# 生效条件:text 为可调用 .strip().encode("utf-8") 的字符串时,返回 hashlib.sha1(text.strip().encode("utf-8")).hexdigest()[:n],n 默认 12 只决定摘要截取长度。
|
|
106
109
|
def _sig(text: str, n: int = 12) -> str:
|
|
107
110
|
return hashlib.sha1(text.strip().encode("utf-8")).hexdigest()[:n]
|
|
108
111
|
|
|
109
112
|
|
|
113
|
+
# 生效条件:term 与 text 均非空时,term 整词出现在 text 中返回 1.0;否则仅当 term 长度 n≥2 且存在长度 L 满足 2≤L<n 的最长命中子串时返回 0.5*L/n;term 或 text 为空、term 长度 <2、或无此类命中子串时返回 0.0。
|
|
110
114
|
def _term_degree(term: str, text: str) -> float:
|
|
111
115
|
"""词在文本中的分级命中(0~1):整词出现 1.0;否则取最长命中子串的长度比 × 0.5。
|
|
112
116
|
|
|
@@ -129,6 +133,7 @@ def _term_degree(term: str, text: str) -> float:
|
|
|
129
133
|
return 0.0
|
|
130
134
|
|
|
131
135
|
|
|
136
|
+
# 生效条件:tw 为 {词: 权重} 映射(None 视作空),只累加 t 不以 "__" 开头且权重 w > 0 的项,返回 num/den;tw 无有效项(den 为 0)时返回 0.0,text 任意(转交 _term_degree)。
|
|
132
137
|
def _weighted_coverage(tw: dict, text: str) -> float:
|
|
133
138
|
"""词权 × 分级命中的加权覆盖率 ∈ [0,1]。"""
|
|
134
139
|
num = den = 0.0
|
|
@@ -150,6 +155,7 @@ def _weighted_coverage(tw: dict, text: str) -> float:
|
|
|
150
155
|
# 条件空间重合率是《激活引擎》cond_match 已被认证的度量。
|
|
151
156
|
# ---------------------------------------------------------------------------
|
|
152
157
|
|
|
158
|
+
# 生效条件:content 中某行以 "#" 开头、含 name、且以 ":" 或 ":" partition 出的 head.strip() 恰等于 name 时返回该行 val.strip()(首个命中即返回);无此行使返回空串 '',content 为 None/空按空串处理。
|
|
153
159
|
def _ccg_field(content: str, name: str) -> str:
|
|
154
160
|
"""取 CCG 正文中 `# <name>:` 那一行的值(确定性扫描,无正则回溯风险)。"""
|
|
155
161
|
for line in (content or "").splitlines():
|
|
@@ -165,6 +171,7 @@ def _ccg_field(content: str, name: str) -> str:
|
|
|
165
171
|
return ""
|
|
166
172
|
|
|
167
173
|
|
|
174
|
+
# 生效条件:当 fm 为 dict 且 content 为字符串时,返回从 CCG 正文、state_attributes.comment 与 non_applicable_conditions 三处合并去重后的 (生效条件列表, 不适用条件列表)。
|
|
168
175
|
def _declared_conditions(fm: dict, content: str):
|
|
169
176
|
"""节点声明的条件证据 → (生效条件列表, 不适用条件列表)。
|
|
170
177
|
|
|
@@ -175,6 +182,7 @@ def _declared_conditions(fm: dict, content: str):
|
|
|
175
182
|
"""
|
|
176
183
|
pos, neg = [], []
|
|
177
184
|
|
|
185
|
+
# 生效条件:当 bucket 与 val 传入且 str(val).strip() 得到的 s 非空、s 尚不在 bucket 中时,将 s 追加到 bucket;s 为空或已存在时不追加;
|
|
178
186
|
def _push(bucket, val):
|
|
179
187
|
s = str(val).strip()
|
|
180
188
|
if s and s not in bucket:
|
|
@@ -195,6 +203,7 @@ def _declared_conditions(fm: dict, content: str):
|
|
|
195
203
|
return pos, neg
|
|
196
204
|
|
|
197
205
|
|
|
206
|
+
# 生效条件:neg_texts 非空且 tw 中存在长度 ≥ 2、权重 ≥ min_weight(默认 0.6)的词,其对该拼接 blob 的 _term_degree ≥ 0.5 时返回 True;neg_texts 为空或无此类词时返回 False。
|
|
198
207
|
def _neg_hit(tw: dict, neg_texts, min_weight: float = 0.6) -> bool:
|
|
199
208
|
"""不适用条件是否被查询词**整词**命中——条件级负路由。
|
|
200
209
|
|
|
@@ -212,6 +221,7 @@ def _neg_hit(tw: dict, neg_texts, min_weight: float = 0.6) -> bool:
|
|
|
212
221
|
return False
|
|
213
222
|
|
|
214
223
|
|
|
224
|
+
# 生效条件:a 与 b 均为可取 a[0]、a[1](b[0]、b[1])并能 float() 的两元素窗口,否则(TypeError/ValueError/IndexError/KeyError)返回 0.0;交叠 hi > lo 时返回 min(1.0,(hi-lo)/span),span ≤ 0 返回 1.0,hi ≤ lo 返回 0.0。
|
|
215
225
|
def _window_overlap(a, b) -> float:
|
|
216
226
|
"""两个时间窗 [t1,t2] 的交叠比(0~1)= 交叠长度 / 较短窗长度。"""
|
|
217
227
|
try:
|
|
@@ -226,6 +236,7 @@ def _window_overlap(a, b) -> float:
|
|
|
226
236
|
return 1.0 if span <= 0 else min(1.0, (hi - lo) / span)
|
|
227
237
|
|
|
228
238
|
|
|
239
|
+
# 生效条件:cs 为映射;仅当 cs["observation_position"] 有值才把 q_domain 与它的 domain_similarity 以权重 2.0 计入,仅当 observation_tool/existence_constraint 有值才以 tw 取最佳 _term_degree 并按 1.0 计入,仅当 cs["time_window"] 与 ctx_tw 同时有值才按 1.0 计入 _window_overlap;缺信息槽不进分母,den 为 0 时返回 0.0,否则返回加权平均。
|
|
229
240
|
def _slot_overlap(tw: dict, cs: dict, q_domain=None, ctx_tw=None) -> float:
|
|
230
241
|
"""condition_space 四槽的加权重合度 ∈ [0,1]。
|
|
231
242
|
|
|
@@ -253,6 +264,7 @@ def _slot_overlap(tw: dict, cs: dict, q_domain=None, ctx_tw=None) -> float:
|
|
|
253
264
|
return (sum(w * v for w, v in parts) / den) if den else 0.0
|
|
254
265
|
|
|
255
266
|
|
|
267
|
+
# 生效条件:verify 为真值时返回 (norm, _sig(norm)) 二元组,norm 为 json.dumps(verify, sort_keys=True, ensure_ascii=False, separators=(",",":"), default=str);verify 为假值(None/空)时返回 ('', '')。
|
|
256
268
|
def _verify_norm(verify):
|
|
257
269
|
"""判据规范化 + 指纹:判据由 propose 声明,指纹不一致即视为被改写。"""
|
|
258
270
|
if not verify:
|
|
@@ -263,17 +275,43 @@ def _verify_norm(verify):
|
|
|
263
275
|
return norm, _sig(norm)
|
|
264
276
|
|
|
265
277
|
|
|
278
|
+
# 生效条件:无 required 形参;当 os.environ 中 MDCG_REDTEAM_REQUIRED 去空白小写后为 "1"/"true"/"yes" 时返回 True,否则返回 False。
|
|
266
279
|
def _redteam_required():
|
|
267
280
|
return os.environ.get("MDCG_REDTEAM_REQUIRED", "").strip().lower() \
|
|
268
281
|
in ("1", "true", "yes")
|
|
269
282
|
|
|
270
283
|
|
|
284
|
+
# ---- 审核队列裁决动作(治理层)· 2026-09-19 阶段一 ----------------------------
|
|
285
|
+
# **命名空间隔离**:`noop` 一词在 lifecycle.py / trust.py 中已被占用为**状态迁移
|
|
286
|
+
# 结果码**(`check`/`stamp` 返回的 code,语义「同状态、不写盘」);这里的 noop 是
|
|
287
|
+
# **审核队列裁决动作**(语义「已评估、判定不改变任何现有记忆」)。沿用
|
|
288
|
+
# `lifecycle_state` 改名先例(同结构两义必生歧义):两处**不共用常量、不互相
|
|
289
|
+
# import**,靠各自的常量名与返回体区分——迁移结果码出现在 `(ok, code, why)`,
|
|
290
|
+
# 裁决动作出现在 `review_decide` 的入参与 decisions.jsonl 的 `decision` 字段。
|
|
291
|
+
DECISION_ACCEPT = "accept"
|
|
292
|
+
DECISION_REJECT = "reject"
|
|
293
|
+
DECISION_EDIT = "edit"
|
|
294
|
+
DECISION_MERGE = "merge"
|
|
295
|
+
DECISION_NOOP = "noop"
|
|
296
|
+
|
|
297
|
+
#: 合法裁决动作全集——`review_decide` 的唯一校验源(原先硬编码在函数体,与
|
|
298
|
+
#: status 映射、工具面 schema、CLI 子命令三处各写一份,加动作必漏)。
|
|
299
|
+
DECISION_ACTIONS = (DECISION_ACCEPT, DECISION_REJECT, DECISION_EDIT,
|
|
300
|
+
DECISION_MERGE, DECISION_NOOP)
|
|
301
|
+
|
|
302
|
+
#: 终态裁决状态集(写进 decisions.jsonl 的 `status` 字段):三者都把提案**关闭**。
|
|
303
|
+
#: `needs_reapproval` 刻意不在列——红队打回后仍待再审批,必须继续可见。
|
|
304
|
+
TERMINAL_DECISION_STATUS = ("accepted", "rejected", "noop")
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# 生效条件:以任意 root 构造时按其拼接 audit_log/hippocampus/trash 等路径并 makedirs 创建 hippocampus 与 trash_dir(exist_ok=True),autoflush 透传父类、actor 存入 self.actor;
|
|
271
308
|
class MdCGOS(MdCG):
|
|
272
309
|
"""MdCG + 记忆 OS 七项能力。"""
|
|
273
310
|
|
|
274
311
|
HIPPOCAMPUS = "hippocampus"
|
|
275
312
|
TRASH = "trash"
|
|
276
313
|
|
|
314
|
+
# 生效条件:传入 root 时以 super().__init__(root, autoflush=autoflush) 初始化父类,把 actor 存入 self.actor,并按 root(及类常量 AUDIT_ARCHIVE/HIPPOCAMPUS/TRASH)拼出 audit_log/audit_archive/hippocampus/inbox_log/decisions_log/trash_dir 等路径,同时 makedirs(hippocampus/trash_dir, exist_ok=True)。
|
|
277
315
|
def __init__(self, root: str, autoflush: int = 64, actor: str = "system"):
|
|
278
316
|
super().__init__(root, autoflush=autoflush)
|
|
279
317
|
self.actor = actor
|
|
@@ -291,6 +329,7 @@ class MdCGOS(MdCG):
|
|
|
291
329
|
|
|
292
330
|
# ================= 6. payload-free 审计 =================
|
|
293
331
|
|
|
332
|
+
# 生效条件:当 op 与 node_id 传入时,构造含 t/op/id/actor/session 并合并 meta 的记录,尝试轮转后追加到 self.audit_log;追加过程中的 OSError 被吞掉;
|
|
294
333
|
def _audit(self, op: str, node_id: str, **meta):
|
|
295
334
|
"""只记事件与载荷哈希,绝不记录内容(payload-free)。"""
|
|
296
335
|
rec = {"t": time.time(), "op": op, "id": node_id, "actor": self.actor,
|
|
@@ -302,6 +341,7 @@ class MdCGOS(MdCG):
|
|
|
302
341
|
except OSError:
|
|
303
342
|
pass
|
|
304
343
|
|
|
344
|
+
# 生效条件:limit 为 None 时返回 audit_archive 各分片与 audit_log 依 paths 顺序 extend 的全部记录;limit 非 None(含 0)时从 reversed(paths) 读取并在 len(out) >= limit 时停止,返回 out[-limit:](limit=0 时立即返回空列表);
|
|
305
345
|
def audit_records(self, limit: int = None):
|
|
306
346
|
"""审计记录读取——轮转后跨分片按时间序(旧片在前)合并。
|
|
307
347
|
|
|
@@ -324,6 +364,7 @@ class MdCGOS(MdCG):
|
|
|
324
364
|
|
|
325
365
|
# ---------- 审计日志分片轮转(治本:给无上界增长装上界) ----------
|
|
326
366
|
|
|
367
|
+
# 生效条件:当 self.audit_archive 可被 os.listdir 列出时,返回其中以 "_audit." 开头且以 ".jsonl" 结尾的名字升序列表;listdir 抛 OSError 时返回 [];
|
|
327
368
|
def _audit_shards(self):
|
|
328
369
|
"""归档分片名,序号零填充 ⇒ 字典序 == 时间序。"""
|
|
329
370
|
try:
|
|
@@ -333,6 +374,7 @@ class MdCGOS(MdCG):
|
|
|
333
374
|
return sorted(n for n in names
|
|
334
375
|
if n.startswith("_audit.") and n.endswith(".jsonl"))
|
|
335
376
|
|
|
377
|
+
# 生效条件:当 self._audit_index 为 None 时,尝试读取 audit_archive 中 AUDIT_INDEX 并过滤 value 为 dict,读取失败或数据非 dict 时 idx 为空字典;随后把 self._audit_index 设为 idx 并返回;非 None 时不重读直接返回;
|
|
336
378
|
def _load_audit_index(self) -> dict:
|
|
337
379
|
if self._audit_index is None:
|
|
338
380
|
idx = {}
|
|
@@ -347,6 +389,7 @@ class MdCGOS(MdCG):
|
|
|
347
389
|
self._audit_index = idx
|
|
348
390
|
return self._audit_index
|
|
349
391
|
|
|
392
|
+
# 生效条件:当 idx 传入时,尝试创建 self.audit_archive 并把 idx 的 JSON(ensure_ascii=False, indent=1, sort_keys=True)原子写入 AUDIT_INDEX;OSError 被吞掉;最后把 self._audit_index 设为 idx,无返回值;
|
|
350
393
|
def _save_audit_index(self, idx: dict):
|
|
351
394
|
try:
|
|
352
395
|
os.makedirs(self.audit_archive, exist_ok=True)
|
|
@@ -356,6 +399,7 @@ class MdCGOS(MdCG):
|
|
|
356
399
|
pass
|
|
357
400
|
self._audit_index = idx
|
|
358
401
|
|
|
402
|
+
# 生效条件:当 AUDIT_ROTATE_BYTES > 0 且 _audit_writes 自增后能被 AUDIT_PROBE_EVERY 整除,且 audit_log 的 getsize 不小于 AUDIT_ROTATE_BYTES 时,调用 rotate_audit;AUDIT_ROTATE_BYTES<=0、未到探测间隔或 getsize 不足/OSError 时直接返回;
|
|
359
403
|
def _rotate_audit_if_needed(self):
|
|
360
404
|
"""写前闸门:活动日志达阈值即切分(把 stat 摊到 1/AUDIT_PROBE_EVERY)。
|
|
361
405
|
|
|
@@ -376,6 +420,7 @@ class MdCGOS(MdCG):
|
|
|
376
420
|
return
|
|
377
421
|
self.rotate_audit()
|
|
378
422
|
|
|
423
|
+
# 生效条件:当 reason(默认 "size")传入时,在 FileLock 下若 audit_log 的 size >= AUDIT_ROTATE_BYTES 则 os.replace 为 _audit.%06d.jsonl 归档并更新索引/剪枝/自述留痕后返回 {"shard","bytes","events","pruned"};size 不足、getsize OSError 或 os.replace 失败时返回 None;
|
|
379
424
|
def rotate_audit(self, reason: str = "size"):
|
|
380
425
|
"""把活动审计日志切分为归档分片(os.replace 原子,不重写一个字节)。
|
|
381
426
|
|
|
@@ -418,6 +463,7 @@ class MdCGOS(MdCG):
|
|
|
418
463
|
pass
|
|
419
464
|
return {"shard": name, "bytes": size, "events": events, "pruned": pruned}
|
|
420
465
|
|
|
466
|
+
# 生效条件:当 path 与 size 传入时,size 为 None 则先 getsize(OSError 返回 bytes/events 0 exact True);size <= AUDIT_COUNT_MAX_BYTES 时返回 count_jsonl(path) 精确条数 exact True;超过时返回 _log_scale(path) 的事件数 exact False;
|
|
421
467
|
def _audit_count_shard(self, path, size: int = None) -> dict:
|
|
422
468
|
"""分片条数读数:有界分片给精确值,超大历史分片只给量级。
|
|
423
469
|
|
|
@@ -436,6 +482,7 @@ class MdCGOS(MdCG):
|
|
|
436
482
|
s = self._log_scale(path)
|
|
437
483
|
return {"bytes": size, "events": s["events"], "exact": False}
|
|
438
484
|
|
|
485
|
+
# 生效条件:在已用 root 构造的实例上遍历 _audit_shards() 的分片名,对 `_audit.<n>.jsonl` 形式中 int(n) 成功的取最大值 top(解析失败 continue、无可解析项时 top=0),返回 top+1。
|
|
439
486
|
def _next_shard_seq(self) -> int:
|
|
440
487
|
top = 0
|
|
441
488
|
for n in self._audit_shards():
|
|
@@ -445,6 +492,7 @@ class MdCGOS(MdCG):
|
|
|
445
492
|
continue
|
|
446
493
|
return top + 1
|
|
447
494
|
|
|
495
|
+
# 生效条件:当 AUDIT_KEEP_SHARDS > 0 且分片数超过该值时,计算 gone=shards[:-keep] 并逐个尝试 os.remove、成功则从索引 pop(OSError 则 continue),最后保存索引并返回 gone;keep<=0 或 gone 为空时返回 [];
|
|
448
496
|
def _prune_audit_shards(self):
|
|
449
497
|
"""保留最近 AUDIT_KEEP_SHARDS 个分片、淘汰更旧的(≤0 表示不淘汰)。"""
|
|
450
498
|
keep = self.AUDIT_KEEP_SHARDS
|
|
@@ -464,6 +512,7 @@ class MdCGOS(MdCG):
|
|
|
464
512
|
self._save_audit_index(idx)
|
|
465
513
|
return gone
|
|
466
514
|
|
|
515
|
+
# 生效条件:在已用 root 构造的实例上以 _log_scale(audit_log) 取活动读数为 active,对磁盘现有分片 present 中未登记者按 _audit_count_shard 采纳进 idx(OSError 则 continue,有采纳才写回索引),返回 active 并附 shards/shard_bytes/shard_events(均只统计 k in present)/oversized(非 exact 分片数)/total_bytes/total_events(active["events"] 为 None 时取 None)/total_exact/rotate_bytes=self.AUDIT_ROTATE_BYTES/keep_shards=self.AUDIT_KEEP_SHARDS。
|
|
467
516
|
def audit_scale(self) -> dict:
|
|
468
517
|
"""审计面量级读数(O(1) 稳态):活动文件实时读数 + 归档分片索引缓存。
|
|
469
518
|
|
|
@@ -510,6 +559,7 @@ class MdCGOS(MdCG):
|
|
|
510
559
|
|
|
511
560
|
# ================= 2. role 分层索引 =================
|
|
512
561
|
|
|
562
|
+
# 生效条件:当 node_id 与 content 传入时,先若有 role 非 None 则放入 kw,调用父类 add 得 nid;仅当索引中 e 非 None 且 role 非 None 时把 role 写入索引并标记 dirty;随后记 audit 并返回 nid;
|
|
513
563
|
def add(self, node_id: str, content: str, layer: str = "knowledge",
|
|
514
564
|
role: str = None, **kw) -> str:
|
|
515
565
|
"""在父类 add 之上:写入 role(frontmatter + 索引),默认 role=None(知识)。"""
|
|
@@ -524,8 +574,11 @@ class MdCGOS(MdCG):
|
|
|
524
574
|
payload_hash=_sig(content))
|
|
525
575
|
return nid
|
|
526
576
|
|
|
577
|
+
# 生效条件:在已用 root 构造的实例上遍历 index["nodes"],恒剔除 layer 为 rejected/unresolved/goals 的节点,session 为真值而 e["session"] 不等于它时剔除,e["branch_id"] 不在 (None, branch) 时剔除(branch=None 时只留 branch_id 为 None 者),validity 为真值而 trust.is_expired(e) 为真时剔除(**只排已过期,not_yet 保留**),layer 为真值而 layer 不等时剔除,roles 不为 None 时仅留 role 落在 roles 内的节点;view 为真值时 role 维度裁决权移交 roleviews.matches(receipt=工作角色白名单须绕过默认剔除才可达,非法 view ValueError),view 为假值且 include_work 为假时剔除 WORK_ROLES 角色,时间算子启用时按 time_axis 轴过滤(效力轴不可判定 fail-open、观察轴不可判定 fail-closed 并入 self._time_filter_stat),其余收集进 out 返回。
|
|
527
578
|
def _candidates(self, layer=None, roles=None, include_work=False,
|
|
528
|
-
session=None, branch=None
|
|
579
|
+
session=None, branch=None, validity=None,
|
|
580
|
+
start_time=None, end_time=None, start_operator=None,
|
|
581
|
+
end_operator=None, time_axis=None, view=None):
|
|
529
582
|
"""候选池:按层 + role 过滤。默认剔除工作角色(工具输出/命令/编辑)。
|
|
530
583
|
|
|
531
584
|
session:会话归属过滤(frontmatter.session,写入时自动落盘)——
|
|
@@ -533,7 +586,24 @@ class MdCGOS(MdCG):
|
|
|
533
586
|
branch:分支可见性(记忆演化分支④)——None(默认)时分支实验
|
|
534
587
|
节点全部隐身(实验不污染主支检索);branch=<id> 时主支 + 该分支
|
|
535
588
|
可见、其他分支仍隐身。
|
|
589
|
+
validity:时效过滤(**显式启用**,缺省 None 不过滤)——真值时剔除
|
|
590
|
+
**已过期**(valid_until 已过 / expired)节点;**未生效(valid_from
|
|
591
|
+
未到 / not_yet)一律保留**,因其与「已失效」语义相反(scrub 纪律
|
|
592
|
+
「valid_from 绝不并入 _EXPIRY_KEYS」),预约/计划类记忆在生效前仍可召回。
|
|
593
|
+
判定走 trust.validity 唯一真源;时间轴缺失/端点不可解析 → 不过滤(不猜测)。
|
|
594
|
+
start_time/end_time/start_operator/end_operator/time_axis:时间算子
|
|
595
|
+
(阶段二 4.1,**显式启用**)——按 `time_axis` 轴把候选收敛到查询时间窗内。
|
|
596
|
+
轴未指定 → 回落 `effective`;轴/算子非法、孤 operator、start>end 一律
|
|
597
|
+
`ValueError`(fail-open 只针对**节点缺字段**,不针对**调用方误用**)。
|
|
598
|
+
效力轴缺字段 fail-open(保留);观察轴缺字段 fail-closed(剔除)。
|
|
599
|
+
view:角色化读取视图(第四阶段 6.1,**显式启用**,缺省 None 零变更)
|
|
600
|
+
——roleviews.ROLE_VIEWS 规则表:main/verifier 声明 content_kind/
|
|
601
|
+
layer 资格并沿用「剔除工作角色」默认口径;receipt=工作角色白名单
|
|
602
|
+
(恰是默认候选池剔除的补集,须接管 role 维度裁决才可达)。非法
|
|
603
|
+
view ValueError(fail-closed 只针对调用方误用);roles 显式参数
|
|
604
|
+
仍优先于 view。
|
|
536
605
|
"""
|
|
606
|
+
now = time.time() if validity else None
|
|
537
607
|
out = []
|
|
538
608
|
for e in self.index["nodes"].values():
|
|
539
609
|
if e.get("layer") in ("rejected", "unresolved", "goals"):
|
|
@@ -542,20 +612,50 @@ class MdCGOS(MdCG):
|
|
|
542
612
|
continue
|
|
543
613
|
if e.get("branch_id") not in (None, branch):
|
|
544
614
|
continue
|
|
615
|
+
if validity and trust.is_expired(e, now=now):
|
|
616
|
+
continue
|
|
545
617
|
if layer and e.get("layer") != layer:
|
|
546
618
|
continue
|
|
547
619
|
r = e.get("role")
|
|
548
620
|
if roles is not None:
|
|
549
621
|
if r not in roles:
|
|
550
622
|
continue
|
|
623
|
+
elif view is not None:
|
|
624
|
+
# 角色化读取视图(第四阶段 6.1):view 非空时接管 role 维度
|
|
625
|
+
# 裁决权(receipt=工作角色白名单,须绕过默认剔除才可达),
|
|
626
|
+
# 并叠加 content_kind/layer 资格;非法 view ValueError。
|
|
627
|
+
if not roleviews.matches(e, view):
|
|
628
|
+
continue
|
|
551
629
|
elif not include_work and r in WORK_ROLES:
|
|
552
630
|
continue
|
|
553
631
|
out.append(e)
|
|
632
|
+
# ---- 时间算子(阶段二 4.1):候选层**唯一**过滤点 ----
|
|
633
|
+
# 与 validity 各司其职,互不替代:validity=「是否已失效」(默认关、只排过期);
|
|
634
|
+
# 时间算子=「是否落在查询时间窗内」(轴 + 算子显式启用,双端可空=无界)。
|
|
635
|
+
en, ax, why = trust.check_time_args(start_time, end_time, start_operator,
|
|
636
|
+
end_operator, time_axis)
|
|
637
|
+
# 该属性供调用方(search / search_rrf)取审计块;紧接调用后即读,
|
|
638
|
+
# 无跨调用竞态(库按实例串行使用)。
|
|
639
|
+
self._time_filter_stat = None
|
|
640
|
+
if not en:
|
|
641
|
+
if why:
|
|
642
|
+
raise ValueError(why) # fail-closed:误用不静默降级
|
|
643
|
+
return out
|
|
644
|
+
qs, qe = trust.parse_time(start_time), trust.parse_time(end_time)
|
|
645
|
+
out, _dropped, _missing = trust.filter_by_time(
|
|
646
|
+
out, ax, qs, qe, start_operator, end_operator)
|
|
647
|
+
self._time_filter_stat = trust.time_filter_meta(
|
|
648
|
+
axis=ax,
|
|
649
|
+
mode="endpoint" if (start_operator or end_operator) else "overlap",
|
|
650
|
+
start=qs, end=qe, start_operator=start_operator,
|
|
651
|
+
end_operator=end_operator, dropped=_dropped,
|
|
652
|
+
axis_missing=_missing, applied=True)
|
|
554
653
|
return out
|
|
555
654
|
|
|
556
655
|
# ================= 资格判定(legacy 记忆豁免) =================
|
|
557
656
|
|
|
558
657
|
@staticmethod
|
|
658
|
+
# 生效条件:node_dict 的 frontmatter 中 ccg_exempt 为真且 content 去空白后非空时返回 DEFER 记录,否则以 (node_dict, query, context) 转 MdCG.judge_qualification;
|
|
559
659
|
def judge_qualification(node_dict, query: str, context=None):
|
|
560
660
|
"""在父类四态之上支持 legacy 记忆(迁移进来的自由文本)。
|
|
561
661
|
|
|
@@ -573,6 +673,7 @@ class MdCGOS(MdCG):
|
|
|
573
673
|
|
|
574
674
|
# ================= 检索(含 role 过滤 + 四路召回) =================
|
|
575
675
|
|
|
676
|
+
# 生效条件:当 terms 传入时,仅对 layer 为 rejected/unresolved 且 _read 返回 fm 为真、_open_content 后 content 含任一 term 的节点收集到 out;否则跳过;
|
|
576
677
|
def _neg_coverage(self, terms):
|
|
577
678
|
"""负记忆覆盖:查询词是否已被 rejected/unresolved 覆盖。"""
|
|
578
679
|
out = []
|
|
@@ -587,24 +688,38 @@ class MdCGOS(MdCG):
|
|
|
587
688
|
out.append(e)
|
|
588
689
|
return out
|
|
589
690
|
|
|
691
|
+
# 生效条件:query strip 后为空即返回 ([], {"tier": None, "reason": "empty_query", "scanned": 0});pool_cfg 由 pooling.resolve(pooling.from_env(pools)) 解析;_candidates 为空即返回 no_candidates;其余与 MdCG.search 同构(T0–T3 阶梯 + 资格判定),并按 roles/include_work 默认剔除工具输出与命令类 role,view 真值时移交 role 维度裁决权给 roleviews.matches(见 _candidates),validity 真值时候选层剔除已过期节点;
|
|
590
692
|
def search(self, query: str, layer: str = None, k: int = 20,
|
|
591
693
|
context=None, min_results: int = 1, record: bool = True,
|
|
592
694
|
include_neg: bool = True, judge: bool = True,
|
|
593
695
|
roles=None, include_work: bool = False, pools=None,
|
|
594
|
-
session=None, branch=None
|
|
696
|
+
session=None, branch=None, validity=None,
|
|
697
|
+
start_time=None, end_time=None, start_operator=None,
|
|
698
|
+
end_operator=None, time_axis=None, view=None):
|
|
595
699
|
"""在父类语义之上加 role 过滤(默认剔除工具输出/命令/编辑)。
|
|
596
700
|
|
|
597
701
|
返回 (results, meta),与 MdCG.search 完全同构(T0–T3 阶梯 + 资格判定)。
|
|
598
702
|
pools:§七 召回分池(None=关闭原行为 / True=内置表 / dict=自定义表)。
|
|
703
|
+
validity:时效过滤(显式启用,缺省不过滤)——只排已过期,未生效保留;
|
|
704
|
+
view:角色化读取视图(第四阶段 6.1,显式启用,缺省 None 零变更);
|
|
705
|
+
详见 _candidates。
|
|
599
706
|
"""
|
|
600
707
|
q = (query or "").strip()
|
|
601
708
|
if not q:
|
|
602
709
|
return [], {"tier": None, "reason": "empty_query", "scanned": 0}
|
|
603
710
|
pool_cfg = pooling.resolve(pooling.from_env(pools))
|
|
604
711
|
entries = self._candidates(layer=layer, roles=roles, include_work=include_work,
|
|
605
|
-
session=session, branch=branch
|
|
712
|
+
session=session, branch=branch, validity=validity,
|
|
713
|
+
start_time=start_time, end_time=end_time,
|
|
714
|
+
start_operator=start_operator,
|
|
715
|
+
end_operator=end_operator, time_axis=time_axis,
|
|
716
|
+
view=view)
|
|
717
|
+
_tf = getattr(self, "_time_filter_stat", None)
|
|
606
718
|
if not entries:
|
|
607
|
-
|
|
719
|
+
_m = {"tier": None, "reason": "no_candidates", "scanned": 0}
|
|
720
|
+
if _tf:
|
|
721
|
+
_m["time_filter"] = _tf
|
|
722
|
+
return [], _m
|
|
608
723
|
|
|
609
724
|
terms = expand_query_terms(q)
|
|
610
725
|
# qb 与 _score 文档侧口径对齐(文档侧已是 normalize_en 小写化),
|
|
@@ -612,6 +727,13 @@ class MdCGOS(MdCG):
|
|
|
612
727
|
# + 英→中语素 bigram 补充(与 mdcg.search/_lexical 打分口径同步)
|
|
613
728
|
qb = bigrams(normalize_en(q)) | en_zh_bigrams(q)
|
|
614
729
|
stat = {"scanned": 0, "query": q}
|
|
730
|
+
# 时间算子审计(阶段二 4.1):候选层过滤在 `_candidates` 已完成,此处把
|
|
731
|
+
# 审计块交给父类 `_emit` 落进 meta —— 与 search_rrf 的 `stat["time_filter"]`
|
|
732
|
+
# 同构。缺此步则过滤**确实生效但审计不可见**:调用方无法区分「未启用过滤」
|
|
733
|
+
# 与「启用了、但候选全 fail-open 保留」,属静默(审计不可见即不可裁决)。
|
|
734
|
+
# 仅在启用时落键,保默认关时 meta 键集合不变。
|
|
735
|
+
if _tf:
|
|
736
|
+
stat["time_filter"] = _tf
|
|
615
737
|
route_bucket = None
|
|
616
738
|
if context is not None:
|
|
617
739
|
ctx = context if isinstance(context, dict) else {}
|
|
@@ -620,6 +742,7 @@ class MdCGOS(MdCG):
|
|
|
620
742
|
big_scores = routing.big_domain_score_breakdown(terms)
|
|
621
743
|
neg_coverage = self._neg_coverage(terms) if include_neg else []
|
|
622
744
|
|
|
745
|
+
# 生效条件:对 docs 调 self._score 后,若其中分数 >0 的条数达到(search 作用域内的)min_results 就返回 self._emit(scored, k, tier, stat, route_bucket, record, len(docs), ...),否则返回 None。
|
|
623
746
|
def try_stage(docs, tier):
|
|
624
747
|
scored = self._score(docs, q, qb, pool_cfg)
|
|
625
748
|
valid = sum(1 for _, s in scored if s > 0)
|
|
@@ -643,6 +766,48 @@ class MdCGOS(MdCG):
|
|
|
643
766
|
if out:
|
|
644
767
|
return out
|
|
645
768
|
|
|
769
|
+
# T1'(reach):大域收敛 → 条件门控 → 图扩散(2026-09-18 新增)。
|
|
770
|
+
# 层级仍报 TIER_GLOBAL_LIKE(它就是「全量 LIKE」阶段的收敛实现,test_p0 契约
|
|
771
|
+
# 「无 context 走全量阶梯」不因优化而变);收敛与否由 meta.reach* 字段区分。
|
|
772
|
+
# 收敛集是 LIKE 命中集与词法打分>0 集合的**超集**(必要条件倒排,见
|
|
773
|
+
# md_cg/test_reach.py 包含性断言),故本阶段不可能丢召回;任一不适用条件
|
|
774
|
+
# (MDCG_REACH 未开启 / 单字符 term / 索引不可用 / 收敛集为空)整段跳过,由下方全量阶段兜底。
|
|
775
|
+
# 默认关(契约 §7「不在默认路径上启用新行为」):整段不进入 → meta 键集合与改动前逐字节一致。
|
|
776
|
+
reach_entries, _rstat = None, {}
|
|
777
|
+
if reach.enabled():
|
|
778
|
+
reach_entries, _rstat = reach.narrow(self, entries, terms, qb, context, q=q)
|
|
779
|
+
if _rstat:
|
|
780
|
+
stat.update(_rstat)
|
|
781
|
+
if reach_entries is not None:
|
|
782
|
+
_pre_scan = int(stat.get("scanned") or 0) # 收敛阶段读盘基线(供回退审计)
|
|
783
|
+
docs_r = self._read_many(reach_entries, stat)
|
|
784
|
+
_dif = set(_rstat.get("reach_diffused_paths") or ())
|
|
785
|
+
hits_r = [d for d in docs_r
|
|
786
|
+
if self._like(d[2], d[1], terms)
|
|
787
|
+
or (semantic_on() and d[1].get("semantic"))
|
|
788
|
+
or d[0].get("path") in _dif] # 图扩散补召回:无词面命中也放行进打分
|
|
789
|
+
stat["pre_cap"] = len(hits_r) # 与 T2 同序:截断**前**的候选数
|
|
790
|
+
stat["cap"] = GLOBAL_CAP
|
|
791
|
+
# 与 T2 **无条件**同序调用(不可按 cap 短路:cut_by_relevance 还会写
|
|
792
|
+
# 池账 pool_taken/cands/lost,短路会让 meta.pools.taken 缺失 → test_p43(13) 红)
|
|
793
|
+
hits_r, _rep = cut_by_relevance(hits_r, self._score(hits_r, q, qb, pool_cfg),
|
|
794
|
+
GLOBAL_CAP, pools=pool_cfg,
|
|
795
|
+
key_of=pooling.doc_key, stat=stat)
|
|
796
|
+
pooling.record_audit(stat, _rep)
|
|
797
|
+
out = try_stage(hits_r, TIER_GLOBAL_LIKE)
|
|
798
|
+
if out:
|
|
799
|
+
return out
|
|
800
|
+
# 收敛阶段未产出结果 → 继续走下方全量 T2/T3;此时必须**改写 reach 审计**,
|
|
801
|
+
# 否则 meta.reach 谎报 converged、A3 也会把「收敛读+全量读」双计当成收敛(r14 复核取证)
|
|
802
|
+
stat["reach"] = "reverted" # 已回退:本次结果不是收敛路径产出的
|
|
803
|
+
stat["reach_reverted"] = True
|
|
804
|
+
# 收敛阶段确实读过盘 → 如实暴露其读盘量,A3 可据此扣减(**不**回滚 scanned:
|
|
805
|
+
# 累计读盘是事实;也**不**清除 reach_build_docs:首建成本真实发生过,r16 复核取证)
|
|
806
|
+
stat["reach_reverted_docs"] = int(stat.get("scanned") or 0) - _pre_scan
|
|
807
|
+
for _k in ("reach_seed", "reach_diffused", "reach_hops", "reach_diffused_paths",
|
|
808
|
+
"reach_fresh_nodes"):
|
|
809
|
+
stat.pop(_k, None)
|
|
810
|
+
|
|
646
811
|
# 截断依据=相关度(同 MdCG.search:cap 值不变,改的是拿什么排序)
|
|
647
812
|
docs_all = self._read_many(entries, stat)
|
|
648
813
|
# 语义资格(MDCG_SEMANTIC=1):fm.semantic 节点无条件入池
|
|
@@ -670,6 +835,7 @@ class MdCGOS(MdCG):
|
|
|
670
835
|
record, len(picked), judge,
|
|
671
836
|
context, neg_coverage, big_domain, big_scores, pool_cfg)
|
|
672
837
|
|
|
838
|
+
# 生效条件:当 query、entries、stat 传入时,先以 terms/qb 从 entries 读取文档并保留 LIKE 命中或(semantic_on 且 frontmatter.semantic)的项;若 hits 为空则按 importance/created_at 降序取前 GLOBAL_CAP 作为兜底;随后对 hits 打分并按相关度与 importance 降序排序,超过 GLOBAL_CAP 时截断并更新 stat,否则返回全部 scored;
|
|
673
839
|
def _lexical(self, query, entries, stat):
|
|
674
840
|
"""词法路径:LIKE 预筛 + 二元组相似度(口径见 mdcg.SCORE_MODE)。
|
|
675
841
|
|
|
@@ -696,14 +862,16 @@ class MdCGOS(MdCG):
|
|
|
696
862
|
# 故本路不做「先全量打分再截断」。
|
|
697
863
|
hits = sorted(
|
|
698
864
|
docs, key=lambda d: (-float(d[1].get("importance") or 0),
|
|
699
|
-
-float(d[1].get("created_at") or 0)
|
|
865
|
+
-float(d[1].get("created_at") or 0),
|
|
866
|
+
str(d[0].get("id") or ""))
|
|
700
867
|
)[:GLOBAL_CAP]
|
|
701
868
|
stat["cut_order"] = "importance_fallback"
|
|
702
869
|
scored = self._score(hits, query, qb)
|
|
703
870
|
# 恒按相关度排序(原实现仅超 cap 时排序 → ≤cap 时返回 entries 枚举序,
|
|
704
871
|
# 下游 seed/融合拿到无语义依据的顺序,不可复算)
|
|
705
872
|
scored.sort(key=lambda x: (-x[1],
|
|
706
|
-
-float(x[0]["frontmatter"].get("importance") or 0)
|
|
873
|
+
-float(x[0]["frontmatter"].get("importance") or 0),
|
|
874
|
+
str(x[0].get("id") or "")))
|
|
707
875
|
if len(scored) > GLOBAL_CAP:
|
|
708
876
|
stat["pre_cap"] = len(scored)
|
|
709
877
|
stat["cap"] = GLOBAL_CAP
|
|
@@ -711,6 +879,7 @@ class MdCGOS(MdCG):
|
|
|
711
879
|
return scored[:GLOBAL_CAP]
|
|
712
880
|
return scored
|
|
713
881
|
|
|
882
|
+
# 生效条件:context 为 None 时返回 [];否则用 routing.bucket_dir(routing.route_key(ctx, ctx.get("tags"))) 取桶(context 非 dict 时 ctx 按 {} 处理),entries 中 bucket 等于该桶的经 self._read_many + self._score(docs, query, bigrams(query)) 返回,无命中则返回 []。
|
|
714
883
|
def _path_bucket(self, query, entries, context):
|
|
715
884
|
"""条件桶路径:命中路由桶的节点优先。"""
|
|
716
885
|
if context is None:
|
|
@@ -724,6 +893,7 @@ class MdCGOS(MdCG):
|
|
|
724
893
|
docs = self._read_many(inb, stat)
|
|
725
894
|
return self._score(docs, query, bigrams(query))
|
|
726
895
|
|
|
896
|
+
# 生效条件:当 query 与 entries 传入时,仅当 entry 的 tags 中存在长度 >=2 且 t 在 query 中或 query 在 t 中的项时,读取并追加该节点及分数 1.0;否则跳过;返回 out;
|
|
727
897
|
def _path_entity(self, query, entries):
|
|
728
898
|
"""实体路径:tags 命中。(返回节点字典,与 _lexical 同构)"""
|
|
729
899
|
out = []
|
|
@@ -740,6 +910,7 @@ class MdCGOS(MdCG):
|
|
|
740
910
|
"content": c, "path": e["path"]}, 1.0))
|
|
741
911
|
return out
|
|
742
912
|
|
|
913
|
+
# 生效条件:当 seeds 非空时,仅取前 5 个种子,从每个种子节点的 frontmatter.edges 取 target(dict 取 target,否则 str(edge)),若 target 在 entries 映射中且不在 seed_ids 中则读取并追加分数 s*0.5;seeds 为空返回 [];depth 默认 1 但本段未使用;
|
|
743
914
|
def _path_graph(self, query, entries, seeds, depth=1):
|
|
744
915
|
"""图扩展路径:从词法种子沿 edges 一跳扩展。"""
|
|
745
916
|
if not seeds:
|
|
@@ -768,6 +939,7 @@ class MdCGOS(MdCG):
|
|
|
768
939
|
"content": c, "path": e["path"]}, s * 0.5))
|
|
769
940
|
return out
|
|
770
941
|
|
|
942
|
+
# 生效条件:当 seeds 非空且 seed_map 非空时,用 relation_types 或 CHAIN_TYPES_DEFAULT、max_depth 或 MAX_DEPTH_DEFAULT、decay 调用 chain.expand_from_seeds,仅保留 best 中仍存在于 entries(按 id(e))的节点,读取成功者加入 scored 并按分数降序返回 (scored, prov);seeds/seed_map/best 为空返回 ([], {});
|
|
771
943
|
def _path_chain(self, query, entries, seeds, context=None,
|
|
772
944
|
relation_types=None, max_depth=None, decay=0.9):
|
|
773
945
|
"""关系链路径:沿 causal/sequential/applies_to 边**多跳**扩散。
|
|
@@ -818,9 +990,10 @@ class MdCGOS(MdCG):
|
|
|
818
990
|
prov[out_id] = {"chain": info["chain"]["nodes"],
|
|
819
991
|
"conditions": info["conditions"],
|
|
820
992
|
"depth": info["depth"]}
|
|
821
|
-
scored.sort(key=lambda x: -x[1])
|
|
993
|
+
scored.sort(key=lambda x: (-x[1], str(x[0].get("id") or "")))
|
|
822
994
|
return scored, prov
|
|
823
995
|
|
|
996
|
+
# 生效条件:以 expand or expand_query_terms_weighted 作扩展函数并对 query 调用(结果为假值按 {} 处理),从其中 pop "__source__"(缺键为 "whitebox")得 source,tw 为空返回 ([], source),否则对 entries 中存在 _weighted_coverage>0 的条目按 0.6·cov+0.3·aff+0.1·ctx_aff(context 非 None 时以 route_key(ctx, ctx.get("tags")) 得 ctx_domain,context 非 dict 时用 {})打分,返回 (out, source)。
|
|
824
997
|
def _path_fuzzy(self, query, entries, context=None, expand=None):
|
|
825
998
|
"""模糊路径(分级隶属度):返回 (scored, source)。
|
|
826
999
|
|
|
@@ -837,7 +1010,8 @@ class MdCGOS(MdCG):
|
|
|
837
1010
|
return [], source
|
|
838
1011
|
dom_scores = routing.big_domain_score_weighted(tw)
|
|
839
1012
|
dom_total = sum(dom_scores.values()) or 1.0
|
|
840
|
-
top_domains = sorted(dom_scores.items(),
|
|
1013
|
+
top_domains = sorted(dom_scores.items(),
|
|
1014
|
+
key=lambda kv: (-kv[1], str(kv[0])))[:3]
|
|
841
1015
|
ctx_domain = None
|
|
842
1016
|
if context is not None:
|
|
843
1017
|
ctx = context if isinstance(context, dict) else {}
|
|
@@ -906,6 +1080,7 @@ class MdCGOS(MdCG):
|
|
|
906
1080
|
"content": c, "path": e["path"]}, round(score, 6)))
|
|
907
1081
|
return out
|
|
908
1082
|
|
|
1083
|
+
# 生效条件:当 query 与 entries 传入时,目标文本 gt 取 goal_text(非 None)或 self.goal_text(),goal_text 为空串则 gt 为空并返回 ([], "");strip 后为空返回 ([], "");扩展词表为空返回 ([], "");否则跳过 goals 层节点,仅保留目标词覆盖 >0 的条目,按 0.7*cov+0.3*域亲和封顶 1.0 打分,返回 (out, gt);
|
|
909
1084
|
def _path_goal(self, query, entries, context=None, goal_text=None):
|
|
910
1085
|
"""目标定向路(白箱第 5 篇第 3 章「目标」):用当前目标给召回定向。
|
|
911
1086
|
|
|
@@ -929,7 +1104,8 @@ class MdCGOS(MdCG):
|
|
|
929
1104
|
return [], ""
|
|
930
1105
|
dom_scores = routing.big_domain_score_weighted(tw)
|
|
931
1106
|
dom_total = sum(dom_scores.values()) or 1.0
|
|
932
|
-
top_domains = sorted(dom_scores.items(),
|
|
1107
|
+
top_domains = sorted(dom_scores.items(),
|
|
1108
|
+
key=lambda kv: (-kv[1], str(kv[0])))[:3]
|
|
933
1109
|
stat = {"scanned": 0}
|
|
934
1110
|
out = []
|
|
935
1111
|
for e, fm, c in self._read_many(entries, stat):
|
|
@@ -948,13 +1124,17 @@ class MdCGOS(MdCG):
|
|
|
948
1124
|
"content": c, "path": e["path"]}, round(score, 6)))
|
|
949
1125
|
return out, gt
|
|
950
1126
|
|
|
1127
|
+
# 生效条件:query 去空白为空→empty_query、候选为空→no_candidates;否则按 paths 各路召回后融合(fusion=="max" 取各路最大贡献、否则求和),recall_only 中的路只以 0 分补池不参与打分,judge 与 judge_ranking 同时为真时对 fused 前 max(k*2,10) 条做资格裁决(REJECT/BLINDSPOT 剔除、DEFER 降权 0.5),否则直接取 fused 前 k;validity 真值时候选层剔除已过期节点,且 query 缓存按 validity 分键不串口径。
|
|
951
1128
|
def search_rrf(self, query: str, k: int = 20, layer: str = None,
|
|
952
1129
|
context=None, roles=None, include_work: bool = False,
|
|
953
1130
|
judge: bool = True, paths=("lexical", "bucket", "entity", "graph"),
|
|
954
1131
|
record: bool = True, query_expand=None,
|
|
955
1132
|
path_weights=None, recall_only=None, fusion: str = "sum",
|
|
956
1133
|
goal_text=None, judge_ranking: bool = False,
|
|
957
|
-
session=None, branch=None
|
|
1134
|
+
session=None, branch=None, validity=None,
|
|
1135
|
+
early_stop_threshold=None,
|
|
1136
|
+
start_time=None, end_time=None, start_operator=None,
|
|
1137
|
+
end_operator=None, time_axis=None, view=None):
|
|
958
1138
|
"""并行多路召回 + RRF 融合。返回 (results, meta)。
|
|
959
1139
|
|
|
960
1140
|
每路各自排序 → Reciprocal Rank Fusion:
|
|
@@ -984,16 +1164,66 @@ class MdCGOS(MdCG):
|
|
|
984
1164
|
REJECT / BLINDSPOT 剔除,DEFER 降权 ×0.5,ACCEPT 保位。候选池取
|
|
985
1165
|
fused 前 max(k*2,10) 再裁决补位。语义联系可以是认知噪声(等权 RRF
|
|
986
1166
|
双重奖励「多路都靠前」的干扰项),唯有条件证据可授予优先级。
|
|
1167
|
+
validity: 时效过滤(显式启用,缺省不过滤)。**只排已过期**,未生效
|
|
1168
|
+
(valid_from 未到)保留——两者语义相反(scrub 纪律「valid_from 绝不
|
|
1169
|
+
并入 _EXPIRY_KEYS」)。详见 _candidates。
|
|
1170
|
+
start_time/end_time/start_operator/end_operator/time_axis:时间算子
|
|
1171
|
+
(阶段二 4.1,显式启用;语义与 fail-closed 规则见 _candidates)。
|
|
1172
|
+
启用时**绕过热路径缓存**(缓存键不含时间参数,复用会串味)。
|
|
1173
|
+
view:角色化读取视图(第四阶段 6.1,显式启用,缺省 None 零变更)。
|
|
1174
|
+
view **进热路径缓存键**(不同视图候选资格不同,不入键会跨视图
|
|
1175
|
+
串结果);候选层语义与 search 一致,详见 _candidates。
|
|
987
1176
|
"""
|
|
988
1177
|
q = (query or "").strip()
|
|
989
1178
|
if not q:
|
|
990
1179
|
return [], {"tier": None, "reason": "empty_query", "paths": {}}
|
|
1180
|
+
# 热路径:query 结果缓存命中即返回(不改 RRF 核心)
|
|
1181
|
+
# 时间算子**显式启用时绕开缓存**:缓存键不含时间参数,命中会返回
|
|
1182
|
+
# 未按本次窗口过滤的结果(静默错答比慢更贵)。
|
|
1183
|
+
_time_on = any(x is not None for x in (start_time, end_time,
|
|
1184
|
+
start_operator, end_operator))
|
|
1185
|
+
from . import hotcache as _hc
|
|
1186
|
+
hc = _hc.get(self)
|
|
1187
|
+
# 口径参数**整体入键**(v14 缺陷 C 修复):include_work/roles 改变候选
|
|
1188
|
+
# 资格、paths/fusion/judge_ranking/goal_text 等改变排序——不入键会让
|
|
1189
|
+
# 默认查询命中工作角色口径的缓存(资格泄漏),方向与时间算子同属
|
|
1190
|
+
# 「静默错答」。清单真源 = hotcache._KEYED_EXTRA。
|
|
1191
|
+
_cache_extra = {
|
|
1192
|
+
"include_work": bool(include_work), "roles": roles,
|
|
1193
|
+
"paths": tuple(paths or ()), "path_weights": path_weights,
|
|
1194
|
+
"recall_only": recall_only, "fusion": fusion,
|
|
1195
|
+
"judge": bool(judge), "judge_ranking": bool(judge_ranking),
|
|
1196
|
+
"goal_text": goal_text, "context": context,
|
|
1197
|
+
"early_stop_threshold": early_stop_threshold,
|
|
1198
|
+
}
|
|
1199
|
+
# 不可稳定进键的参数(自定义可调用 query_expand):非默认即**绕行**
|
|
1200
|
+
# 缓存(读+写双侧闭合)——fail-closed,宁可不用缓存也不串味。
|
|
1201
|
+
_bypass = query_expand is not None
|
|
1202
|
+
if hc is not None and not _time_on and not _bypass:
|
|
1203
|
+
cached = hc.get_query(q, k=k, layer=layer, session=session,
|
|
1204
|
+
branch=branch, validity=validity, view=view,
|
|
1205
|
+
extra=_cache_extra)
|
|
1206
|
+
if cached is not None:
|
|
1207
|
+
_results, _meta = cached
|
|
1208
|
+
_meta["cached"] = True
|
|
1209
|
+
return _results, _meta
|
|
991
1210
|
entries = self._candidates(layer=layer, roles=roles, include_work=include_work,
|
|
992
|
-
session=session, branch=branch
|
|
1211
|
+
session=session, branch=branch, validity=validity,
|
|
1212
|
+
start_time=start_time, end_time=end_time,
|
|
1213
|
+
start_operator=start_operator,
|
|
1214
|
+
end_operator=end_operator, time_axis=time_axis,
|
|
1215
|
+
view=view)
|
|
1216
|
+
_tf = getattr(self, "_time_filter_stat", None)
|
|
993
1217
|
if not entries:
|
|
994
|
-
|
|
1218
|
+
_m = {"tier": None, "reason": "no_candidates", "paths": {}}
|
|
1219
|
+
if _tf:
|
|
1220
|
+
_m["time_filter"] = _tf
|
|
1221
|
+
return [], _m
|
|
995
1222
|
|
|
996
1223
|
stat = {"scanned": 0}
|
|
1224
|
+
if _tf:
|
|
1225
|
+
stat["time_filter"] = _tf
|
|
1226
|
+
_tf_meta = {"time_filter": _tf} if _tf else {}
|
|
997
1227
|
ranked = {} # path -> [(node, score)]
|
|
998
1228
|
fuzzy_source = None
|
|
999
1229
|
chain_prov = {}
|
|
@@ -1023,8 +1253,11 @@ class MdCGOS(MdCG):
|
|
|
1023
1253
|
per_path = {}
|
|
1024
1254
|
ro = set(recall_only or ())
|
|
1025
1255
|
for name, scored in ranked.items():
|
|
1256
|
+
# 终键 nid:此处 rank 直接进 RRF(w/(K+rank)),并列若回落输入序
|
|
1257
|
+
# 会改变融合分——同分同 importance 必须由 nid 定序才可复算。
|
|
1026
1258
|
scored = sorted(scored, key=lambda x: (-x[1],
|
|
1027
|
-
-float(x[0]["frontmatter"].get("importance") or 0)
|
|
1259
|
+
-float(x[0]["frontmatter"].get("importance") or 0),
|
|
1260
|
+
str(x[0].get("id") or "")))
|
|
1028
1261
|
per_path[name] = len(scored)
|
|
1029
1262
|
if name in ro:
|
|
1030
1263
|
continue
|
|
@@ -1054,11 +1287,32 @@ class MdCGOS(MdCG):
|
|
|
1054
1287
|
for scored in ranked.values():
|
|
1055
1288
|
for node, s in scored:
|
|
1056
1289
|
node_by_id.setdefault(node["id"], (node, s))
|
|
1057
|
-
fused_all = sorted(rrf.items(), key=lambda kv: -kv[1])
|
|
1290
|
+
fused_all = sorted(rrf.items(), key=lambda kv: (-kv[1], kv[0]))
|
|
1291
|
+
|
|
1292
|
+
# 温路径早停判据(2026-09-19 热温冷分层):
|
|
1293
|
+
# top-1 RRF 分 >= threshold 且 top-k 全部来自 >=2 路共识 → 提前返回
|
|
1294
|
+
# 不改 RRF 核心算法,只在融合后判断
|
|
1295
|
+
early_stopped = False
|
|
1296
|
+
if early_stop_threshold is not None and len(fused_all) >= k:
|
|
1297
|
+
top1_score = fused_all[0][1]
|
|
1298
|
+
if top1_score >= early_stop_threshold:
|
|
1299
|
+
# 检查 top-k 是否全部来自多路共识(prov 中 >=2 路)
|
|
1300
|
+
topk_ids = [nid for nid, _ in fused_all[:k]]
|
|
1301
|
+
multi_consensus = all(
|
|
1302
|
+
len({p["path"] for p in prov.get(nid, [])}) >= 2
|
|
1303
|
+
for nid in topk_ids
|
|
1304
|
+
)
|
|
1305
|
+
if multi_consensus:
|
|
1306
|
+
early_stopped = True
|
|
1058
1307
|
|
|
1059
1308
|
quals = {}
|
|
1060
1309
|
filtered = 0
|
|
1061
|
-
if
|
|
1310
|
+
if early_stopped:
|
|
1311
|
+
# 早停:直接取 top-k,不做 judge_ranking
|
|
1312
|
+
fused = fused_all[:k]
|
|
1313
|
+
judge = False # 早停时跳过资格判定
|
|
1314
|
+
judge_ranking = False
|
|
1315
|
+
elif judge and judge_ranking:
|
|
1062
1316
|
# 证据防火墙:语义/词法融合产生候选(不要漏),资格授予优先级(不要错)
|
|
1063
1317
|
kept = []
|
|
1064
1318
|
for nid, fs in fused_all[:max(k * 2, 10)]:
|
|
@@ -1070,7 +1324,7 @@ class MdCGOS(MdCG):
|
|
|
1070
1324
|
filtered += 1
|
|
1071
1325
|
continue
|
|
1072
1326
|
kept.append((nid, fs if st == STATE_ACCEPT else fs * 0.5))
|
|
1073
|
-
kept.sort(key=lambda x: -x[1])
|
|
1327
|
+
kept.sort(key=lambda x: (-x[1], x[0]))
|
|
1074
1328
|
fused = kept[:k]
|
|
1075
1329
|
else:
|
|
1076
1330
|
fused = fused_all[:k]
|
|
@@ -1084,23 +1338,42 @@ class MdCGOS(MdCG):
|
|
|
1084
1338
|
results.append((node, round(fs, 6), qual, prov.get(nid, [])))
|
|
1085
1339
|
if record and results:
|
|
1086
1340
|
self.record_access([r[0]["id"] for r in results], "RRF")
|
|
1341
|
+
# 热路径:写 query 结果缓存 —— **同样受 `_time_on` 约束**。
|
|
1342
|
+
# 缓存键不含时间参数,把被时间过滤的结果写进去,会让后续**默认查询**
|
|
1343
|
+
# 命中那个子集(串味方向与「读」相反,但同样是静默错答:少了 4 条
|
|
1344
|
+
# 却看不出原因)。绕行必须读+写双侧闭合。
|
|
1345
|
+
if hc is not None and results and not _time_on and not _bypass:
|
|
1346
|
+
hc.put_query(q, results, {"tier": "RRF", "scanned": stat["scanned"],
|
|
1347
|
+
"paths": per_path, "fused": len(results),
|
|
1348
|
+
"judge_ranking": bool(judge and judge_ranking),
|
|
1349
|
+
"judge_filtered": filtered,
|
|
1350
|
+
"early_stopped": early_stopped,
|
|
1351
|
+
"expand_source": fuzzy_source,
|
|
1352
|
+
"goal_used": goal_used,
|
|
1353
|
+
"provenance": prov, **_tf_meta}, k=k, layer=layer,
|
|
1354
|
+
session=session, branch=branch, validity=validity,
|
|
1355
|
+
view=view, extra=_cache_extra)
|
|
1087
1356
|
return results, {"tier": "RRF", "scanned": stat["scanned"],
|
|
1088
1357
|
"paths": per_path, "fused": len(results),
|
|
1089
1358
|
"judge_ranking": bool(judge and judge_ranking),
|
|
1090
1359
|
"judge_filtered": filtered,
|
|
1360
|
+
"early_stopped": early_stopped,
|
|
1091
1361
|
"expand_source": fuzzy_source,
|
|
1092
1362
|
"goal_used": goal_used,
|
|
1093
|
-
"provenance": prov}
|
|
1363
|
+
"provenance": prov, **_tf_meta}
|
|
1094
1364
|
|
|
1095
1365
|
# ================= 7. budget-driven pack =================
|
|
1096
1366
|
|
|
1367
|
+
# 生效条件:query(配合 use_rrf 取 items)逐条按 budget_tokens 与 max_item_tokens 装包:若 used+t > budget_tokens,则 max_item_tokens 为真且 room=budget_tokens-used 不小于 min_excerpt(max_item_tokens 为真时取 max(1, min(50, max_item_tokens // 5)),否则为 0)时按 keep=min(max_item_tokens, room) 摘录,摘录后 est_tokens<=0 则该条以 excerpt_empty 进 skipped 并 continue;否则以 oversize_or_over_budget 进 skipped 并 continue;未超预算则计入 used 并 append,include_recent 为真时再按 left=budget_tokens-used 追加 recent_limit 条近期事件(逐条 est_tokens 不超过 left 才计入),最终返回含 pack/tokens_used/budget/skipped/recent/meta 的 dict;validity 真值时向候选层透传时效过滤(只排已过期)。
|
|
1097
1368
|
def recall(self, query: str, budget_tokens: int = DEFAULT_BUDGET, k: int = 20,
|
|
1098
1369
|
layer: str = None, context=None, roles=None,
|
|
1099
1370
|
include_work: bool = False, judge: bool = True, use_rrf: bool = True,
|
|
1100
1371
|
paths=None, query_expand=None, fusion=None,
|
|
1101
1372
|
goal_text=None, include_recent=False, recent_limit: int = 10,
|
|
1102
1373
|
judge_ranking: bool = False, session=None, branch=None,
|
|
1103
|
-
max_item_tokens: int = DEFAULT_MAX_ITEM_TOKENS
|
|
1374
|
+
validity=None, max_item_tokens: int = DEFAULT_MAX_ITEM_TOKENS,
|
|
1375
|
+
start_time=None, end_time=None, start_operator=None,
|
|
1376
|
+
end_operator=None, time_axis=None, view=None):
|
|
1104
1377
|
"""按 token 预算装包:装到预算花完为止。
|
|
1105
1378
|
|
|
1106
1379
|
装包策略(2026-09-14 调整):
|
|
@@ -1119,10 +1392,17 @@ class MdCGOS(MdCG):
|
|
|
1119
1392
|
include_recent=True 时,把近期事件窗口(第 5 篇第 3 章)附在包后,
|
|
1120
1393
|
保证当前任务的连续性;它不参与 RRF 正排,但计入 token 预算。
|
|
1121
1394
|
返回 {pack: [...], tokens_used, budget, skipped: [...], recent: [...], meta}
|
|
1395
|
+
validity:时效过滤(显式启用,缺省不过滤)——只排已过期,未生效保留。
|
|
1396
|
+
start_time/end_time/start_operator/end_operator/time_axis:时间算子
|
|
1397
|
+
(显式启用)——按 time_axis 轴把候选收敛到查询时间窗内;轴未指定回落
|
|
1398
|
+
effective;误用 fail-closed(ValueError);节点缺字段按轴策略处置。
|
|
1399
|
+
view:角色化读取视图(第四阶段 6.1,显式启用,缺省 None 零变更),
|
|
1400
|
+
透传 search_rrf/search 候选层。
|
|
1122
1401
|
"""
|
|
1123
1402
|
if use_rrf:
|
|
1124
1403
|
kw = dict(k=k, layer=layer, context=context, roles=roles,
|
|
1125
|
-
include_work=include_work, judge=judge, session=session
|
|
1404
|
+
include_work=include_work, judge=judge, session=session,
|
|
1405
|
+
view=view)
|
|
1126
1406
|
if paths is not None:
|
|
1127
1407
|
kw["paths"] = tuple(paths)
|
|
1128
1408
|
if query_expand is not None:
|
|
@@ -1134,11 +1414,24 @@ class MdCGOS(MdCG):
|
|
|
1134
1414
|
kw["judge_ranking"] = judge_ranking
|
|
1135
1415
|
if branch is not None:
|
|
1136
1416
|
kw["branch"] = branch
|
|
1417
|
+
if validity:
|
|
1418
|
+
kw["validity"] = validity
|
|
1419
|
+
for _k, _v in (("start_time", start_time), ("end_time", end_time),
|
|
1420
|
+
("start_operator", start_operator),
|
|
1421
|
+
("end_operator", end_operator),
|
|
1422
|
+
("time_axis", time_axis)):
|
|
1423
|
+
if _v is not None: # 时间算子透传(None = 未启用,不入参)
|
|
1424
|
+
kw[_k] = _v
|
|
1137
1425
|
results, meta = self.search_rrf(query, **kw)
|
|
1138
1426
|
items = [(r[0], r[1], r[2], r[3]) for r in results]
|
|
1139
1427
|
else:
|
|
1140
1428
|
res, meta = self.search(query, layer=layer, k=k, context=context,
|
|
1141
|
-
judge=judge, session=session, branch=branch
|
|
1429
|
+
judge=judge, session=session, branch=branch,
|
|
1430
|
+
validity=validity,
|
|
1431
|
+
start_time=start_time, end_time=end_time,
|
|
1432
|
+
start_operator=start_operator,
|
|
1433
|
+
end_operator=end_operator, time_axis=time_axis,
|
|
1434
|
+
view=view)
|
|
1142
1435
|
items = [(r[0], r[1], r[2], []) for r in res]
|
|
1143
1436
|
|
|
1144
1437
|
pack, skipped, used = [], [], 0
|
|
@@ -1192,6 +1485,7 @@ class MdCGOS(MdCG):
|
|
|
1192
1485
|
|
|
1193
1486
|
# ================= 1. Fix pairs 自动挖掘 =================
|
|
1194
1487
|
|
|
1488
|
+
# 生效条件:当 events 传入时,兼容显式 {error,fix} 对与含 text 的事件序列;后者在错误行长度 >= min_len 后向后 lookahead 条内寻找 _FIX_RE 命中的行配对并 break;去重后为每对 add 知识节点和 add_rejected,返回 pairs/rejected_ids/knowledge_ids;
|
|
1195
1489
|
def mine_fix_pairs(self, events, lookahead: int = 4, min_len: int = 6):
|
|
1196
1490
|
"""从行为日志挖掘「错误 → 修复」对。
|
|
1197
1491
|
|
|
@@ -1260,6 +1554,7 @@ class MdCGOS(MdCG):
|
|
|
1260
1554
|
|
|
1261
1555
|
# ================= 4. 审核队列(inbox → decisions) =================
|
|
1262
1556
|
|
|
1557
|
+
# 生效条件:当 node_id 与 content 传入时,在 strict 锁内按 payload_hash 查重;命中同内容提案(无论 pending/accepted/rejected,已裁决优先 break)时幂等返回既有 pid(info=True 返回 dedup 字典),未命中则生成新 pid 入队并返回 pid(info=True 返回 dedup False 字典);
|
|
1263
1558
|
def propose(self, node_id: str, content: str, layer: str = "knowledge",
|
|
1264
1559
|
tags=None, condition_space=None, verify=None,
|
|
1265
1560
|
info: bool = False, **kw):
|
|
@@ -1324,6 +1619,7 @@ class MdCGOS(MdCG):
|
|
|
1324
1619
|
"dup_of": None, "dup_status": None}
|
|
1325
1620
|
return pid
|
|
1326
1621
|
|
|
1622
|
+
# 生效条件:当 self.decisions_log 可被 read_jsonl 读取时,仅 r.get("pid") 为真值的记录写入 st[r["pid"]],后出现记录覆盖先出现记录,返回 pid → 最新一条裁决记录的字典;
|
|
1327
1623
|
def _pid_status(self):
|
|
1328
1624
|
"""pid → 最新一条裁决记录(多轮再审批时取最后一轮)。"""
|
|
1329
1625
|
st = {}
|
|
@@ -1332,18 +1628,20 @@ class MdCGOS(MdCG):
|
|
|
1332
1628
|
st[r["pid"]] = r
|
|
1333
1629
|
return st
|
|
1334
1630
|
|
|
1631
|
+
# 生效条件:实例已构建(内部先读 _pid_status());返回其 status ∈ TERMINAL_DECISION_STATUS(accepted/rejected/noop)的 pid 集合,status=="needs_reapproval" 视为未关闭、不计入;
|
|
1335
1632
|
def _closed_pids(self):
|
|
1336
1633
|
"""已被终态裁决关闭的 pid(needs_reapproval 仍视为打开)。"""
|
|
1337
1634
|
return {pid for pid, r in self._pid_status().items()
|
|
1338
|
-
if r.get("status") in
|
|
1635
|
+
if r.get("status") in TERMINAL_DECISION_STATUS}
|
|
1339
1636
|
|
|
1637
|
+
# 生效条件:在已用 root 构造的实例上遍历 self.inbox_log 记录,其 pid 在 _pid_status() 中 status ∈ TERMINAL_DECISION_STATUS 时跳过,其余复制该记录并写入 status=s.get("status") or "pending"、round=int(s.get("round") or 0)、issues=list(s.get("issues") or []) 后返回 out。
|
|
1340
1638
|
def review_list(self):
|
|
1341
1639
|
"""待审核候选(含被红队打回、待再审批的条目)。"""
|
|
1342
1640
|
st = self._pid_status()
|
|
1343
1641
|
out = []
|
|
1344
1642
|
for r in read_jsonl(self.inbox_log):
|
|
1345
1643
|
s = st.get(r.get("pid")) or {}
|
|
1346
|
-
if s.get("status") in
|
|
1644
|
+
if s.get("status") in TERMINAL_DECISION_STATUS:
|
|
1347
1645
|
continue
|
|
1348
1646
|
rec = dict(r)
|
|
1349
1647
|
rec["status"] = s.get("status") or "pending"
|
|
@@ -1352,6 +1650,27 @@ class MdCGOS(MdCG):
|
|
|
1352
1650
|
out.append(rec)
|
|
1353
1651
|
return out
|
|
1354
1652
|
|
|
1653
|
+
# 生效条件:实例已构建(内部读 decisions_log、_pid_status()、review_list()、_closed_pids());返回 records=decisions 记录总数、by_decision=按 decision 值分组计数(decision 为假值时归入 "unknown")、proposals=有裁决记录的 pid 数、pending=review_list() 长度、closed=_closed_pids() 长度、noop=by_decision 中 noop 计数(缺省 0)、terminal_status=终态状态集清单;
|
|
1654
|
+
def review_stats(self):
|
|
1655
|
+
"""裁决动作分布统计(含 NOOP)。
|
|
1656
|
+
|
|
1657
|
+
存在意义:NOOP(已评估、判定不改变任何现有记忆)若只落在 jsonl 里、
|
|
1658
|
+
没有统计出口,则「这条候选被评估过」在治理面不可见——与「静默忽略」
|
|
1659
|
+
等价。口径=decisions.jsonl 逐条记录(权威源,不另建统计文件);队列
|
|
1660
|
+
视图复用 review_list/_closed_pids,避免在此复制第二份终态判定逻辑。
|
|
1661
|
+
"""
|
|
1662
|
+
by = {}
|
|
1663
|
+
for r in read_jsonl(self.decisions_log):
|
|
1664
|
+
d = str(r.get("decision") or "").strip() or "unknown"
|
|
1665
|
+
by[d] = by.get(d, 0) + 1
|
|
1666
|
+
return {"records": sum(by.values()), "by_decision": by,
|
|
1667
|
+
"proposals": len(self._pid_status()),
|
|
1668
|
+
"pending": len(self.review_list()),
|
|
1669
|
+
"closed": len(self._closed_pids()),
|
|
1670
|
+
"noop": by.get(DECISION_NOOP, 0),
|
|
1671
|
+
"terminal_status": list(TERMINAL_DECISION_STATUS)}
|
|
1672
|
+
|
|
1673
|
+
# 生效条件:当 pid 传入时,从 decisions_log 读取并仅保留 r.get("pid")==pid 的记录,映射为含 round/decision/status/redteam/issues/t/actor/record_node_id/record_hash 的列表返回;
|
|
1355
1674
|
def review_rounds(self, pid: str):
|
|
1356
1675
|
"""某提案的裁决轮次历史(红队打回 → 修复 → 再审批,可追溯)。"""
|
|
1357
1676
|
return [{"round": r.get("round"), "decision": r.get("decision"),
|
|
@@ -1369,6 +1688,7 @@ class MdCGOS(MdCG):
|
|
|
1369
1688
|
AUDIT_TAG = "review-record"
|
|
1370
1689
|
|
|
1371
1690
|
@staticmethod
|
|
1691
|
+
# 生效条件:从 rec 取 pid/round/decision/status/redteam_verdict/issues/verify_hash/reason/actor/t 十键(缺键回落 None)做 sort_keys 紧凑 JSON 序列化后返回 sha1 hexdigest;
|
|
1372
1692
|
def _record_hash(rec):
|
|
1373
1693
|
"""裁决记录指纹:外部审计方按同规则重算即可验证未被篡改。"""
|
|
1374
1694
|
keys = ("pid", "round", "decision", "status", "redteam_verdict",
|
|
@@ -1379,9 +1699,11 @@ class MdCGOS(MdCG):
|
|
|
1379
1699
|
return hashlib.sha1(norm.encode("utf-8")).hexdigest()
|
|
1380
1700
|
|
|
1381
1701
|
@staticmethod
|
|
1702
|
+
# 生效条件:对任意 pid 取其 '_' 分段末段、round_no 经 int() 转换后拼成 f"rev_{末段}_r{轮次}";
|
|
1382
1703
|
def _audit_node_id(pid: str, round_no) -> str:
|
|
1383
1704
|
return f"rev_{pid.split('_')[-1]}_r{int(round_no)}"
|
|
1384
1705
|
|
|
1706
|
+
# 生效条件:当 rec 提供 record_node_id/record_hash/pid/round/decision/status/redteam_verdict 等键时(缺键会 KeyError),构造审核裁决 body,并调用 self.add 以 layer="self"、override=True、role=AUDIT_ROLE 等写入,返回 nid;若 self 有 principal 则额外传 sensitivity=principal.clearance;
|
|
1385
1707
|
def _write_review_record(self, rec):
|
|
1386
1708
|
"""把一轮裁决写成 md 记忆节点(self 层)——外部来源可读、可复核。"""
|
|
1387
1709
|
nid = rec["record_node_id"]
|
|
@@ -1419,6 +1741,7 @@ class MdCGOS(MdCG):
|
|
|
1419
1741
|
audit_source="hippocampus/decisions.jsonl", **extra)
|
|
1420
1742
|
return nid
|
|
1421
1743
|
|
|
1744
|
+
# 生效条件:当 pid/item/decision/status/reason/round_no/rt_verdict/issues/vhash/result 传入时,构造 rec 并写 record_hash,把 result 中 ok/node_id/error/state 保留;在 strict 锁内追加 decisions_log;随后尝试写 md 审核记录,成功补 record_node_id/record_hash,异常则补 record_error 并 audit review_record_failed;最后 audit review_decide 并返回 result;
|
|
1422
1745
|
def _record_decision(self, pid, item, decision, status, reason, round_no,
|
|
1423
1746
|
rt_verdict, issues, vhash, result):
|
|
1424
1747
|
"""落盘一轮裁决:jsonl(权威)+ md 审计节点(可复核、供外部审计)。"""
|
|
@@ -1450,6 +1773,7 @@ class MdCGOS(MdCG):
|
|
|
1450
1773
|
record_node=rec["record_node_id"])
|
|
1451
1774
|
return result
|
|
1452
1775
|
|
|
1776
|
+
# 生效条件:当 pid、phash、decision 传入且 phash 非空时,在 strict 锁内对 inbox_log 中同 phash、非自身、且 _pid_status 尚无记录的纯 pending 兄弟提案写入 rejected 裁决,返回 closed 列表;phash 为空返回 [];
|
|
1453
1777
|
def _cascade_dedup(self, pid: str, phash: str, decision: str):
|
|
1454
1778
|
"""主提案终态裁决后,同 payload_hash 的其余纯 pending 提案自动出清。
|
|
1455
1779
|
|
|
@@ -1487,6 +1811,7 @@ class MdCGOS(MdCG):
|
|
|
1487
1811
|
self._audit("review_cascade", b, cascade_of=pid, decision=decision)
|
|
1488
1812
|
return closed
|
|
1489
1813
|
|
|
1814
|
+
# 生效条件:遍历 self.index["nodes"];仅取 layer=="self" 且 tags 含 AUDIT_TAG 的节点,pid 为真值时再要求 tags 含 f"pid:{pid}";按 (created_at, id) 升序返回不含正文的条目;
|
|
1490
1815
|
def review_records(self, pid: str = None):
|
|
1491
1816
|
"""列出裁决记录节点(self 层 / audit 标签),供外部来源审计。"""
|
|
1492
1817
|
out = []
|
|
@@ -1503,6 +1828,7 @@ class MdCGOS(MdCG):
|
|
|
1503
1828
|
out.sort(key=lambda r: (r.get("created_at") or 0, r["id"]))
|
|
1504
1829
|
return out
|
|
1505
1830
|
|
|
1831
|
+
# 生效条件:当 node_id 传入时,若 self.get(node_id) 无节点返回 ok False error node_not_found;否则从 frontmatter 取 pid/round,在 decisions_log 找 pid 且 int(round or 0) 匹配的记录;找不到返回 source_record_missing;找到则用 _record_hash(src) 与 frontmatter.record_hash 比较,返回 ok=bool(expected) and actual==expected 及明细;
|
|
1506
1832
|
def verify_review_record(self, node_id: str):
|
|
1507
1833
|
"""复核一条裁决记录节点:重算 record_hash 与 decisions.jsonl 比对。
|
|
1508
1834
|
|
|
@@ -1529,22 +1855,30 @@ class MdCGOS(MdCG):
|
|
|
1529
1855
|
"verify_hash": src.get("verify_hash"),
|
|
1530
1856
|
"source": "hippocampus/decisions.jsonl"}
|
|
1531
1857
|
|
|
1858
|
+
# 生效条件:decision 须为 DECISION_ACTIONS("accept"/"reject"/"edit"/"merge"/"noop")之一(否则 raise ValueError),inbox_log 中须有 pid 匹配记录(否则 {'ok': False, 'error': 'pid_not_found'}),且 pid 不在 self._closed_pids() 中(否则 'already_decided');edits 为真值且含 "verify"、或 redteam 为真值且含 "verify" 时返回 'verify_readonly';last_status=="needs_reapproval" 时须 redteam.verdict 归一化为 "pass" 且 round_no>last_round(否则 'reapproval_required' / 'round_not_advanced');rt_v=="reject" 或(decision=="reject" 且 rt_issues 非空)时记 needs_reapproval 不落节点;decision=="accept" 且 rt_v 为空且 _redteam_required() 为真时返回 'redteam_required';其余 accept/edit 按 item(edit 时用 edits.get 覆盖 content/tags/layer)add+flush 落节点,merge 须 merge_into 或 item.extra.merge_into 指向的节点存在(否则 'merge_target_not_found')后追加内容并 rebuild_index,reject 与 noop 只记裁决(status 分别为 "rejected"/"noop");最后统一 _record_decision + _cascade_dedup + flush 后返回 result。
|
|
1532
1859
|
def review_decide(self, pid: str, decision: str, edits: dict = None,
|
|
1533
1860
|
merge_into: str = None, reason: str = "",
|
|
1534
1861
|
redteam: dict = None, issues=None):
|
|
1535
|
-
"""审核裁决:accept / reject / edit / merge。
|
|
1862
|
+
"""审核裁决:accept / reject / edit / merge / noop。
|
|
1536
1863
|
|
|
1537
1864
|
accept → 按 inbox 原样写入
|
|
1538
1865
|
reject → 丢弃(只记裁决,不落节点)
|
|
1539
1866
|
edit → 用 edits 覆盖 content/tags/layer 后写入
|
|
1540
1867
|
merge → 合并进已有节点 merge_into(内容追加 + 不适用条件并集)
|
|
1868
|
+
noop → 已评估、判定**不改变任何现有记忆**:只留痕(decisions.jsonl +
|
|
1869
|
+
审计记录节点)并关闭提案,不落业务节点、不进负记忆
|
|
1870
|
+
|
|
1871
|
+
noop 与 reject 的区别是语义而非路径:reject 是「否掉这条候选」,noop 是
|
|
1872
|
+
「评估过了、无需改动」。二者都不写目标节点,故 noop 不可借道绕过 accept
|
|
1873
|
+
的写入门控(它根本不写)。此处 noop 是**裁决动作**,与 lifecycle/trust 中
|
|
1874
|
+
同名的**状态迁移结果码**分属两层(见模块常量区注释)。
|
|
1541
1875
|
|
|
1542
1876
|
两条验证纪律(借自任务分级协议的验证端):
|
|
1543
1877
|
1. 判据只读:verify 由 propose 声明,裁决阶段传入不同判据 → verify_readonly。
|
|
1544
1878
|
2. 红队门控 + 再审批:redteam.verdict=reject(或带 issues 的 reject)不落库,
|
|
1545
1879
|
该 pid 转 needs_reapproval;修复后必须带 round 递增的 pass 再审批。
|
|
1546
1880
|
"""
|
|
1547
|
-
if decision not in
|
|
1881
|
+
if decision not in DECISION_ACTIONS:
|
|
1548
1882
|
raise ValueError(f"未知裁决:{decision}")
|
|
1549
1883
|
item = next((r for r in read_jsonl(self.inbox_log)
|
|
1550
1884
|
if r.get("pid") == pid), None)
|
|
@@ -1594,7 +1928,12 @@ class MdCGOS(MdCG):
|
|
|
1594
1928
|
|
|
1595
1929
|
result = {"pid": pid, "decision": decision, "round": round_no,
|
|
1596
1930
|
"redteam": rt_v or "absent"}
|
|
1597
|
-
if decision ==
|
|
1931
|
+
if decision == DECISION_NOOP:
|
|
1932
|
+
# 已评估、判定不改变任何现有记忆:不落业务节点、不进负记忆,唯一产物
|
|
1933
|
+
# 是下面 _record_decision 写的留痕(jsonl + 审计记录节点)。放在最前,
|
|
1934
|
+
# 免得将来有人往 accept/edit 分支加副作用时把它卷进去。
|
|
1935
|
+
result["ok"] = True
|
|
1936
|
+
elif decision == "reject":
|
|
1598
1937
|
result["ok"] = True
|
|
1599
1938
|
elif decision in ("accept", "edit"):
|
|
1600
1939
|
content = item["content"]
|
|
@@ -1636,9 +1975,12 @@ class MdCGOS(MdCG):
|
|
|
1636
1975
|
self.rebuild_index()
|
|
1637
1976
|
result.update(ok=True, node_id=target)
|
|
1638
1977
|
|
|
1978
|
+
# status 映射统一出口:reject→"rejected"、noop→"noop"(同为终态,见
|
|
1979
|
+
# TERMINAL_DECISION_STATUS)、其余→"accepted"。
|
|
1639
1980
|
result = self._record_decision(
|
|
1640
1981
|
pid, item, decision,
|
|
1641
|
-
"rejected"
|
|
1982
|
+
{DECISION_REJECT: "rejected", DECISION_NOOP: DECISION_NOOP}
|
|
1983
|
+
.get(decision, "accepted"),
|
|
1642
1984
|
reason, round_no, rt_v, rt_issues, expect, result)
|
|
1643
1985
|
# 级联出清:主提案已终态,同内容兄弟提案(重复入队产物)自动关闭
|
|
1644
1986
|
casc = self._cascade_dedup(
|
|
@@ -1654,11 +1996,13 @@ class MdCGOS(MdCG):
|
|
|
1654
1996
|
self.flush()
|
|
1655
1997
|
return result
|
|
1656
1998
|
|
|
1999
|
+
# 生效条件:无输入形参;调用即返回 list(read_jsonl(self.decisions_log)),记录内容取决于 decisions_log 可读结果;
|
|
1657
2000
|
def decisions(self):
|
|
1658
2001
|
return list(read_jsonl(self.decisions_log))
|
|
1659
2002
|
|
|
1660
2003
|
# ================= 5. tombstone + 恢复时删除检查 =================
|
|
1661
2004
|
|
|
2005
|
+
# 生效条件:当 node_id 传入且索引中存在该节点时,先经 protect.guard_forget(override=override) 保护检查,随后尝试把源路径 os.replace 到 trash_dir/{node_id}.md;OSError 返回 ok False error 字符串;成功则写 deletions_log、_unstage、缓存失效、audit,并返回 ok True/id/tombstone;索引无此节点返回 not_found;
|
|
1662
2006
|
def forget(self, node_id: str, reason: str = "", override: bool = False):
|
|
1663
2007
|
"""软删除:节点文件移入 trash/,写入删除清单(payload-free)。
|
|
1664
2008
|
|
|
@@ -1690,12 +2034,15 @@ class MdCGOS(MdCG):
|
|
|
1690
2034
|
self._audit("forget", node_id, reason=reason, payload_hash=h)
|
|
1691
2035
|
return {"ok": True, "id": node_id, "tombstone": h}
|
|
1692
2036
|
|
|
2037
|
+
# 生效条件:无输入形参;调用即返回 list(read_jsonl(self.deletions_log)),记录内容取决于 deletions_log 可读结果;
|
|
1693
2038
|
def deletions(self):
|
|
1694
2039
|
return list(read_jsonl(self.deletions_log))
|
|
1695
2040
|
|
|
2041
|
+
# 生效条件:当 node_id 传入时,返回 deletions_log 中是否存在 r.get("id")==node_id 的布尔值;
|
|
1696
2042
|
def is_tombstoned(self, node_id: str):
|
|
1697
2043
|
return any(r.get("id") == node_id for r in read_jsonl(self.deletions_log))
|
|
1698
2044
|
|
|
2045
|
+
# 生效条件:当 node_id 传入时,若 deletions_log 中存在该 id 且 force=False 返回 tombstoned 拒绝;否则检查 trash_dir/{node_id}.md,os.path.exists 为 False 返回 not_in_trash;可打开则读取并 self.add(override=True...) 恢复、移除 trash 源、audit,返回 ok True/id/forced=bool(force);
|
|
1699
2046
|
def restore(self, node_id: str, force: bool = False):
|
|
1700
2047
|
"""恢复:若在删除清单中且未 force → 拒绝(恢复时删除检查)。"""
|
|
1701
2048
|
tomb = [r for r in read_jsonl(self.deletions_log) if r.get("id") == node_id]
|
|
@@ -1729,6 +2076,7 @@ class MdCGOS(MdCG):
|
|
|
1729
2076
|
SESSION_TAG = "session"
|
|
1730
2077
|
|
|
1731
2078
|
@staticmethod
|
|
2079
|
+
# 生效条件:对任意 session 与 summary(None 分别按空串处理,summary 另去首尾空白)取 sha1 前 12 位拼成 "sess_{sig}",同 (session, summary) 得同 id;
|
|
1732
2080
|
def _session_node_id(session, summary):
|
|
1733
2081
|
"""会话要点节点 id:同 (session, summary) → 同 id(幂等覆盖,不新增)。"""
|
|
1734
2082
|
sig = hashlib.sha1(
|
|
@@ -1737,6 +2085,7 @@ class MdCGOS(MdCG):
|
|
|
1737
2085
|
return f"sess_{sig}"
|
|
1738
2086
|
|
|
1739
2087
|
@staticmethod
|
|
2088
|
+
# 生效条件:content 中「执行」字段真值时返回其前 500 字符,否则返回首个非空行去空白后前 500 字符,全空或无行时返回空串;
|
|
1740
2089
|
def _session_digest(content):
|
|
1741
2090
|
"""从会话节点正文取一行摘要(`# 执行:` 优先,否则首个非空行)。"""
|
|
1742
2091
|
v = _ccg_field(content, "执行")
|
|
@@ -1747,6 +2096,7 @@ class MdCGOS(MdCG):
|
|
|
1747
2096
|
return ln.strip()[:500]
|
|
1748
2097
|
return ""
|
|
1749
2098
|
|
|
2099
|
+
# 生效条件:当 summary 传入且 strip 后非空时,session 按显式入参、self.session、日期依次回落;conditions 为假值时回落默认条件;用 SESSION_TAG 与 session 标签调用 add,返回含 ok/id/session/layer/basis/tokens 的字典;summary 为空则 raise ValueError;
|
|
1750
2100
|
def session_note(self, summary, session=None, tags=None, layer="contextual",
|
|
1751
2101
|
importance=0.6, sensitivity=None, conditions=None,
|
|
1752
2102
|
basis="data"):
|
|
@@ -1785,6 +2135,7 @@ class MdCGOS(MdCG):
|
|
|
1785
2135
|
return {"ok": True, "id": nid, "session": session, "layer": layer,
|
|
1786
2136
|
"basis": basis, "tokens": est_tokens(content)}
|
|
1787
2137
|
|
|
2138
|
+
# 生效条件:当 session 传入且为真时仅保留 tags 含 f"session:{session}" 的项;保留 tags 含 SESSION_TAG 或任一以 "session:" 开头的索引节点,_read 的 content 为 None 则跳过;按 created_at 降序后返回前 max(1, int(limit or 5)) 条,limit 为假值(含 0/None)按 5 处理;
|
|
1788
2139
|
def _session_notes(self, session=None, limit=5):
|
|
1789
2140
|
"""按时间倒序取会话要点(索引过滤 + 惰性回读摘要)。只读,不写盘。"""
|
|
1790
2141
|
out = []
|
|
@@ -1804,9 +2155,10 @@ class MdCGOS(MdCG):
|
|
|
1804
2155
|
"layer": e.get("layer"), "tags": tags,
|
|
1805
2156
|
"summary": self._session_digest(content),
|
|
1806
2157
|
})
|
|
1807
|
-
out.sort(key=lambda n: -n["created_at"])
|
|
2158
|
+
out.sort(key=lambda n: (-n["created_at"], str(n.get("id") or "")))
|
|
1808
2159
|
return out[:max(1, int(limit or 5))]
|
|
1809
2160
|
|
|
2161
|
+
# 生效条件:limit 经 max(1,min(int(limit or 5),50))、budget_tokens 经 max(200,int(budget_tokens or 1200)) 归一后逐段取数(include_state 为真才取 self_state),每段异常只把段名追加进 degraded,再由 while 循环按预算交替裁 recent/notes 尾部、任务段最后才裁并置 tasks_truncated。
|
|
1810
2162
|
def session_recall(self, session=None, limit=5, recent_limit=10,
|
|
1811
2163
|
budget_tokens=1200, include_state=True):
|
|
1812
2164
|
"""按需恢复:一次调用返回「可续接的上下文包」(替代 hook 自动注入)。
|
|
@@ -1908,6 +2260,7 @@ class MdCGOS(MdCG):
|
|
|
1908
2260
|
"会话开始时显式调用本 op 一次即可续接。")
|
|
1909
2261
|
return pack
|
|
1910
2262
|
|
|
2263
|
+
# 生效条件:以 self.recent_events(limit=max(1, int(limit or 40))) 取事件(limit 为 0/None/"" 等假值时回落 40,异常时 evs=[]),session 为真值时按 (r.get("meta") or {}).get("session")==session 过滤,正文按行去重收集长度≥8 的要点、user 行优先排序后取前 max(1, int(max_points or 8))(max_points 假值时回落 8)拼 summary,note 为真值时再调 session_note 写入并在 out 附 written_id。
|
|
1911
2264
|
def session_compact(self, session=None, limit=40, max_points=8,
|
|
1912
2265
|
note=False, importance=0.5):
|
|
1913
2266
|
"""上下文压缩摘要:把会话近期事件压成要点(可选写入会话节点)。
|
|
@@ -1965,6 +2318,7 @@ class MdCGOS(MdCG):
|
|
|
1965
2318
|
"refine", "refine_gate", "refine_history",
|
|
1966
2319
|
"refine_calibrate")
|
|
1967
2320
|
|
|
2321
|
+
# 生效条件:当 content 传入时,先调用 forgetting.prefeed 得到裁决 vd;write=False(默认)返回预演 out(written/reinforced 为 None);write=True 时按 vd["decision"] 为 "write" 则用 node_id 或 _prefeed_id(content)、importance_hint 为 None 时取 0.5 调用 add 并写 written;为 "reinforce" 且有 duplicate_with 则调用 forgetting.reinforce 写 reinforced;discard/defer 不写不并;最后返回 out;
|
|
1968
2322
|
def prefeed(self, content, layer="contextual", role=None,
|
|
1969
2323
|
verification_basis=None, importance_hint=None, node_id=None,
|
|
1970
2324
|
write=False, tags=None, conditions=None):
|
|
@@ -2000,6 +2354,7 @@ class MdCGOS(MdCG):
|
|
|
2000
2354
|
"discard": "已丢弃(不写)", "defer": "留待复核(不写不并)"}.get(dec, "")
|
|
2001
2355
|
return out
|
|
2002
2356
|
|
|
2357
|
+
# 生效条件:act 取 str(action or "stat") 去空白并 lower 后按分支分派(importance / longterm / prefeed / separate / stat 等);未识别 act 走 stat 兜底;只读与写层 action 库层不鉴权,apply 类批量改写由 MCP 分发层 require_admin 把守;
|
|
2003
2358
|
def maintain(self, action="stat", layer=None, limit=None, apply=False,
|
|
2004
2359
|
min_delta=None, max_rows=None, force=False, keep=None,
|
|
2005
2360
|
mode=None, snapshot_id=None, batch=None, entry_ids=None,
|
|
@@ -2138,6 +2493,19 @@ class MdCGOS(MdCG):
|
|
|
2138
2493
|
min_jaccard=min_jaccard,
|
|
2139
2494
|
min_cluster=common["min_cluster"])
|
|
2140
2495
|
return refine.history(self, limit=limit or 100, batch=batch)
|
|
2496
|
+
if act in ("comment_gate", "comment_gate_gate", "comment_gate_verdict"):
|
|
2497
|
+
# 环二:代码符号「条件化注释」抽样闸门(复用 refine 范式,对象换成 code_ 节点)。
|
|
2498
|
+
# plan 出工单(只读);apply 只落抽检留痕(**不改节点**)但决定扩批放行;
|
|
2499
|
+
# gate 复算通过率。绝不盲跑全量。权威实现见 md_cg/comment_gate.py。
|
|
2500
|
+
from . import comment_gate as _cg_gate
|
|
2501
|
+
who = actor or getattr(self, "actor", "maintain")
|
|
2502
|
+
canon = ("comment_gate" if act == "comment_gate"
|
|
2503
|
+
else "comment_gate_verdict")
|
|
2504
|
+
return _cg_gate.run(
|
|
2505
|
+
self, canon, ids=ids,
|
|
2506
|
+
n=(extra.get("sample_n") or extra.get("n")), seed=extra.get("seed"),
|
|
2507
|
+
apply=apply, verdicts=extra.get("verdicts"), actor=who,
|
|
2508
|
+
note=extra.get("reason"), batch=batch)
|
|
2141
2509
|
if act == "stat":
|
|
2142
2510
|
nodes = self.index.get("nodes") or {}
|
|
2143
2511
|
by_layer, imp_sum, protected, missing_basis = {}, 0.0, 0, 0
|
|
@@ -2167,6 +2535,7 @@ class MdCGOS(MdCG):
|
|
|
2167
2535
|
"note": "只读盘点;apply 类动作需管理权限(require_admin)。"}
|
|
2168
2536
|
raise ValueError(f"maintain 未知 action:{act}(可选 {list(self.MAINTAIN_ACTIONS)})")
|
|
2169
2537
|
|
|
2538
|
+
# 生效条件:action 为假值时回落 "promote",随后按 str(action or "promote").strip().lower() 分派到 promote、promote_rollback|rollback、promote_history、induce、contextualize、contextualize_rollback|relayer_rollback、contextualize_history 各路实现,全部不匹配时抛 ValueError。
|
|
2170
2539
|
def consolidate_run(self, action="promote", **kw):
|
|
2171
2540
|
"""离线固化面(P1:promote;P2:induce/run)。"""
|
|
2172
2541
|
from . import consolidate
|
|
@@ -2226,8 +2595,10 @@ class MdCGOS(MdCG):
|
|
|
2226
2595
|
INSIGHT_ACTIONS = ("window", "record", "verify", "list", "report",
|
|
2227
2596
|
"reconstruct", "learn", "outlook", "catalog",
|
|
2228
2597
|
"fork", "branch_rewrite", "branch_search",
|
|
2229
|
-
"branch_merge", "branch_discard", "branches"
|
|
2598
|
+
"branch_merge", "branch_discard", "branches",
|
|
2599
|
+
"tickets")
|
|
2230
2600
|
|
|
2601
|
+
# 生效条件:act 取 str(action or "outlook") 去空白并 lower;只读分支 window/list/report/reconstruct/outlook/catalog,记账分支 record/verify(写 contextual 层),落库分支 learn(apply)/reconstruct(apply) 与六分支 act(fork/branch_rewrite/branch_search/branch_merge/branch_discard/branches);落库与分支类由 _insight_call 的 require_admin 把守;
|
|
2231
2602
|
def insight(self, action="outlook", **kw):
|
|
2232
2603
|
"""洞察条件层 + 情景重构 + 盲区学习 + 结构洞察(P2)。
|
|
2233
2604
|
|
|
@@ -2311,6 +2682,18 @@ class MdCGOS(MdCG):
|
|
|
2311
2682
|
limit=(kw.get("limit") or 3),
|
|
2312
2683
|
window=(kw.get("window") or 200),
|
|
2313
2684
|
actor=actor)
|
|
2685
|
+
if act == "tickets":
|
|
2686
|
+
# 盲区消解票据(阶段三 §5.4,opt-in):盲区 → 四类任务卡
|
|
2687
|
+
# (research/prototype/grilling/task)经 tasks.upsert 落库挂图。
|
|
2688
|
+
# apply=True 属批量落库(MCP 分发层 require_admin 把守)。
|
|
2689
|
+
from . import blindspot_tickets as _bt
|
|
2690
|
+
types = kw.get("types")
|
|
2691
|
+
return _bt.make_tickets(
|
|
2692
|
+
self,
|
|
2693
|
+
types=(list(types) if isinstance(types, (list, tuple)) else None),
|
|
2694
|
+
limit=(kw.get("limit") or 10),
|
|
2695
|
+
min_blindspot=(kw.get("min_blindspot") or 0),
|
|
2696
|
+
apply=bool(kw.get("apply")), actor=actor)
|
|
2314
2697
|
if act == "learn":
|
|
2315
2698
|
lkw = {k: kw[k] for k in ("blindspot_id", "limit", "horizon",
|
|
2316
2699
|
"max_branches") if kw.get(k) is not None}
|
|
@@ -2353,6 +2736,7 @@ class MdCGOS(MdCG):
|
|
|
2353
2736
|
AUDIT_INDEX = "_index.json" # 归档索引:分片 bytes/events 缓存(稳态 O(1))
|
|
2354
2737
|
AUDIT_PROBE_EVERY = 32 # 每 N 次写入探测一次大小(把写入税摊到 1/N)
|
|
2355
2738
|
|
|
2739
|
+
# 生效条件:当 path 传入时,先 os.stat;OSError 返回 {'bytes':0,'mtime':None,'events':0,'exact':True};size <= AUDIT_COUNT_MAX_BYTES 时返回 count_jsonl(path) 精确条数 exact True;否则读取末尾 est_sample 字节估算 events、exact False 并附 note;
|
|
2356
2740
|
def _log_scale(self, path, est_sample=256 << 10):
|
|
2357
2741
|
"""日志量级读数(O(1)):以元数据为主,条数只在与规模相称时才精确。
|
|
2358
2742
|
|
|
@@ -2395,6 +2779,7 @@ class MdCGOS(MdCG):
|
|
|
2395
2779
|
% (self.AUDIT_COUNT_MAX_BYTES / 1048576.0, est_sample >> 10))})
|
|
2396
2780
|
return out
|
|
2397
2781
|
|
|
2782
|
+
# 生效条件:在已用 root 构造的实例上以 self.health() 为基底,附加 os 面读数(_role_counts、review_list/review_records 长度、_log_scale(deletions_log)["events"]、audit_scale 的 events/exact/bytes/shards/total_bytes/total_events/total_exact/oversized、last_d_records 长度、goals 的 total 与 active_goals(limit=0)、recent_events(limit=0)、protect_stats 去掉 ids/immutable_ids、forgetting/identity/consistency/metacognition/self_state/evolution 的 summary)后返回 h。
|
|
2398
2783
|
def health_os(self):
|
|
2399
2784
|
h = self.health()
|
|
2400
2785
|
audit = self.audit_scale()
|
|
@@ -2439,6 +2824,7 @@ class MdCGOS(MdCG):
|
|
|
2439
2824
|
}
|
|
2440
2825
|
return h
|
|
2441
2826
|
|
|
2827
|
+
# 生效条件:无前置;遍历 self.index["nodes"] 按 e.get("role") 或 "(none)" 计数,返回 role → 计数 dict(不过滤、不排序);
|
|
2442
2828
|
def _role_counts(self):
|
|
2443
2829
|
c = {}
|
|
2444
2830
|
for e in self.index["nodes"].values():
|
|
@@ -2453,23 +2839,28 @@ class MdCGOS(MdCG):
|
|
|
2453
2839
|
|
|
2454
2840
|
# ============ 8. 嵌套子图 + 关系链(结构要素的可递归化 / 因果链=条件链)============
|
|
2455
2841
|
|
|
2842
|
+
# 生效条件:当 node_id 传入时,以 max_depth(默认 None)原样调用 subgraph.expand 并返回其结果;本函数不改变参数;
|
|
2456
2843
|
def subgraph_expand(self, node_id, max_depth=None):
|
|
2457
2844
|
"""递归展开嵌套子图:`max_depth=None` 数据驱动(展开到自然耗尽)。"""
|
|
2458
2845
|
return subgraph.expand(self, node_id, max_depth=max_depth)
|
|
2459
2846
|
|
|
2847
|
+
# 生效条件:当 node_id 传入时,以 max_depth(默认 None)原样调用 subgraph.flatten 并返回其结果;本函数不改变参数;
|
|
2460
2848
|
def subgraph_flatten(self, node_id, max_depth=None):
|
|
2461
2849
|
"""摊平为「节点 + 对称父子边」(part_of / parent_of)。"""
|
|
2462
2850
|
return subgraph.flatten(self, node_id, max_depth=max_depth)
|
|
2463
2851
|
|
|
2852
|
+
# 生效条件:当 limit 传入时,以 limit(默认 50)调用 subgraph.validate 并返回其结果;本函数不改变参数;
|
|
2464
2853
|
def subgraph_validate(self, limit=50):
|
|
2465
2854
|
"""树一致性:多父 / 环 / 悬空 / 自环(不一致 → 该层判定应退回 DEFER)。"""
|
|
2466
2855
|
return subgraph.validate(self, limit=limit)
|
|
2467
2856
|
|
|
2857
|
+
# 生效条件:无输入形参;调用 subgraph.roots(self) 并返回其结果;
|
|
2468
2858
|
def subgraph_roots(self):
|
|
2469
2859
|
return subgraph.roots(self)
|
|
2470
2860
|
|
|
2471
2861
|
# ---- 主动遗忘(写入侧三问闸门)+ 写保护盘点 ----
|
|
2472
2862
|
|
|
2863
|
+
# 生效条件:kw 中 gated 为假值时旁路直接 ACCEPT 写入并返回 bypass;否则 writelimit.check 非 None 时按 CONVERGE→MERGE 并经 converge_into 并入 target、DROP/DEFER 只记 forgetting 日志,无限流拦截时按 forgetting.assess 的四态处理(ACCEPT 走 add,ConsistencyError 或 written 为 None 转 DEFER;MERGE 走 reinforce;DROP/DEFER 不落库只留痕)。
|
|
2473
2864
|
def remember_gated(self, node_id, content, layer="contextual", **kw):
|
|
2474
2865
|
"""写入情景层记忆前的**主动遗忘闸门**:三问 → 四态。
|
|
2475
2866
|
|
|
@@ -2558,46 +2949,56 @@ class MdCGOS(MdCG):
|
|
|
2558
2949
|
"actor": self.actor})
|
|
2559
2950
|
return out
|
|
2560
2951
|
|
|
2952
|
+
# 生效条件:当 limit 传入时,以 limit(默认 100)调用 forgetting.history 并返回其结果;本函数不改变参数;
|
|
2561
2953
|
def forgetting_history(self, limit=100):
|
|
2562
2954
|
"""遗忘裁决留痕:为什么没记住,与为什么记住同样可查。"""
|
|
2563
2955
|
return forgetting.history(self, limit=limit)
|
|
2564
2956
|
|
|
2957
|
+
# 生效条件:无输入形参;调用 protect.stats(self) 并返回其结果;
|
|
2565
2958
|
def protect_stats(self):
|
|
2566
2959
|
"""写保护面盘点:受保护节点数、分层分布、自动保护命中数。"""
|
|
2567
2960
|
return protect.stats(self)
|
|
2568
2961
|
|
|
2569
2962
|
# ---- 身份特征识别(智能论 v3.4 位置效应 + 扮演论三接口)----
|
|
2570
2963
|
|
|
2964
|
+
# 生效条件:当 subject_id 与 text 传入时,以 subject_id、text 及 **kw 调用 identity.observe 并返回其结果;
|
|
2571
2965
|
def identity_observe(self, subject_id, text, **kw):
|
|
2572
2966
|
"""memory 接口:记录主体行为证据(供位置效应推断)。"""
|
|
2573
2967
|
return identity.observe(self, subject_id, text, **kw)
|
|
2574
2968
|
|
|
2969
|
+
# 生效条件:当 subject_id 与 text 传入时,以 subject_id、text 及 **kw 调用 identity.set_anchor 并返回其结果;
|
|
2575
2970
|
def identity_anchor(self, subject_id, text, **kw):
|
|
2576
2971
|
"""anchor 接口:写身份锚点(不可遗忘;role/user 不得进 self 层)。"""
|
|
2577
2972
|
return identity.set_anchor(self, subject_id, text, **kw)
|
|
2578
2973
|
|
|
2974
|
+
# 生效条件:当 subject_id 与 trait 传入时,以 subject_id、trait 及 **kw 调用 identity.add_trait 并返回其结果;
|
|
2579
2975
|
def identity_trait(self, subject_id, trait, **kw):
|
|
2580
2976
|
"""values 接口:写条件触发的特征 / 特化价值观(落结构层)。"""
|
|
2581
2977
|
return identity.add_trait(self, subject_id, trait, **kw)
|
|
2582
2978
|
|
|
2979
|
+
# 生效条件:当 subject_id 传入时,调用 identity.profile(self, subject_id) 并返回其结果;
|
|
2583
2980
|
def identity_profile(self, subject_id):
|
|
2584
2981
|
"""主体画像:身份锚点 + 位置效应 + 条件特征(不止「用户画像」)。"""
|
|
2585
2982
|
return identity.profile(self, subject_id)
|
|
2586
2983
|
|
|
2984
|
+
# 生效条件:当 limit 传入时,以 limit(默认 0)调用 identity.positions 并返回其结果;本函数不改变 limit;
|
|
2587
2985
|
def identity_positions(self, limit=0):
|
|
2588
2986
|
"""所有主体的位置效应分布(谁在记录/反思/验证/输出/维生)。"""
|
|
2589
2987
|
return identity.positions(self, limit=limit)
|
|
2590
2988
|
|
|
2989
|
+
# 生效条件:当 limit 传入时,以 limit(默认 100)调用 identity.history 并返回其结果;本函数不改变 limit;
|
|
2591
2990
|
def identity_history(self, limit=100):
|
|
2592
2991
|
"""身份操作留痕。"""
|
|
2593
2992
|
return identity.history(self, limit=limit)
|
|
2594
2993
|
|
|
2994
|
+
# 生效条件:无输入形参;调用 identity.catalog() 并返回其结果;
|
|
2595
2995
|
def identity_catalog(self):
|
|
2596
2996
|
"""自描述:位置效应表 + 扮演论三接口。"""
|
|
2597
2997
|
return identity.catalog()
|
|
2598
2998
|
|
|
2599
2999
|
# ---- 节点间自动冲突检测(三级决策:情绪 → 反思 → 递归反思)----
|
|
2600
3000
|
|
|
3001
|
+
# 生效条件:当 content 传入时,以 layer/condition_space/non_applicable_conditions/tags/exclude/limit/depth/auto_flywheel 的传入值或默认值(limit=consistency.MAX_SCAN、depth=consistency.MAX_DEPTH、auto_flywheel=False)调用 consistency.check 并返回其结果;
|
|
2601
3002
|
def check_consistency(self, content, layer=None, condition_space=None,
|
|
2602
3003
|
non_applicable_conditions=None, tags=None,
|
|
2603
3004
|
exclude=None, limit=consistency.MAX_SCAN,
|
|
@@ -2613,20 +3014,24 @@ class MdCGOS(MdCG):
|
|
|
2613
3014
|
exclude=exclude, limit=limit, depth=depth,
|
|
2614
3015
|
auto_flywheel=auto_flywheel)
|
|
2615
3016
|
|
|
3017
|
+
# 生效条件:当 limit 传入时,以 limit(默认 100)调用 consistency.history 并返回其结果;本函数不改变 limit;
|
|
2616
3018
|
def consistency_history(self, limit=100):
|
|
2617
3019
|
"""冲突判定留痕:为什么冲突 / 为什么放行。"""
|
|
2618
3020
|
return consistency.history(self, limit=limit)
|
|
2619
3021
|
|
|
3022
|
+
# 生效条件:无输入形参;调用 consistency.summary(self) 并返回其结果;
|
|
2620
3023
|
def consistency_stats(self):
|
|
2621
3024
|
"""冲突面汇总(供 health / 运维审计)。"""
|
|
2622
3025
|
return consistency.summary(self)
|
|
2623
3026
|
|
|
3027
|
+
# 生效条件:无输入形参;调用 consistency.catalog() 并返回其结果;
|
|
2624
3028
|
def consistency_catalog(self):
|
|
2625
3029
|
"""自描述:三级决策 + 四态 + 递归约束(供协议对照验证)。"""
|
|
2626
3030
|
return consistency.catalog()
|
|
2627
3031
|
|
|
2628
3032
|
# ============ 独立元认知(观察自身认知的二阶单元,不参与裁决)============
|
|
2629
3033
|
|
|
3034
|
+
# 生效条件:在已用 root 构造的实例上以 window(缺省 50)转调 metacognition.report(self, window=window) 并返回其结果。
|
|
2630
3035
|
def metacognition_report(self, window=50):
|
|
2631
3036
|
"""元认知报告:轨迹 / 校准 / 盲区 / 信任 + 确定性建议。
|
|
2632
3037
|
|
|
@@ -2636,46 +3041,61 @@ class MdCGOS(MdCG):
|
|
|
2636
3041
|
"""
|
|
2637
3042
|
return metacognition.report(self, window=window)
|
|
2638
3043
|
|
|
3044
|
+
# 生效条件:在已用 root 构造的实例上以 window(缺省 50)转调 metacognition.trace(self, window=window) 并返回其结果。
|
|
2639
3045
|
def metacognition_trace(self, window=50):
|
|
2640
3046
|
"""信息差轨迹 D(t) → dD/dt(方向)→ d²D/dt²(情绪)。"""
|
|
2641
3047
|
return metacognition.trace(self, window=window)
|
|
2642
3048
|
|
|
3049
|
+
# 生效条件:在已用 root 构造的实例上以 max_scan(缺省 2000)转调 metacognition.calibration(self, max_scan=max_scan) 并返回其结果。
|
|
2643
3050
|
def metacognition_calibration(self, max_scan=2000):
|
|
2644
3051
|
"""自信校准:期望正确率 vs 实际验证通过率(过度自信 / 过度保守)。"""
|
|
2645
3052
|
return metacognition.calibration(self, max_scan=max_scan)
|
|
2646
3053
|
|
|
3054
|
+
# 生效条件:在已用 root 构造的实例上以 limit(缺省 20)、window(缺省 200)转调 metacognition.blindspots(self, limit=limit, window=window) 并返回其结果。
|
|
2647
3055
|
def metacognition_blindspots(self, limit=20, window=200):
|
|
2648
3056
|
"""盲区地图:反复 BLINDSPOT 的查询邻域 + 未解问题清单。"""
|
|
2649
3057
|
return metacognition.blindspots(self, limit=limit, window=window)
|
|
2650
3058
|
|
|
3059
|
+
# 生效条件:在已用 root 构造的实例上以 window(缺省 100)转调 metacognition.trust(self, window=window) 并返回其结果。
|
|
2651
3060
|
def metacognition_trust(self, window=100):
|
|
2652
3061
|
"""P_gap(信息差置信)+ P_trust(验证稳定置信)+ d²T/dt²(情感)。"""
|
|
2653
3062
|
return metacognition.trust(self, window=window)
|
|
2654
3063
|
|
|
3064
|
+
# 生效条件:传入 query,并以 k(缺省 5)、min_sim(缺省 0.25)转调 metacognition.self_check(self, query, k=k, min_sim=min_sim) 并返回其结果。
|
|
2655
3065
|
def self_check(self, query, k=5, min_sim=0.25):
|
|
2656
3066
|
"""元认知闸门:回答前先自问「我对这件事的认知状态如何」。"""
|
|
2657
3067
|
return metacognition.self_check(self, query, k=k, min_sim=min_sim)
|
|
2658
3068
|
|
|
3069
|
+
# 生效条件:在已用 root 构造的实例上以 limit(缺省 100)转调 metacognition.history(self, limit=limit) 并返回其结果。
|
|
2659
3070
|
def metacognition_history(self, limit=100):
|
|
2660
3071
|
"""元认知留痕(倒序)。"""
|
|
2661
3072
|
return metacognition.history(self, limit=limit)
|
|
2662
3073
|
|
|
3074
|
+
# 生效条件:在已用 root 构造的实例上转调 metacognition.summary(self) 并返回其结果。
|
|
2663
3075
|
def metacognition_summary(self):
|
|
2664
3076
|
"""一句话元认知状态(供 health / 面板)。"""
|
|
2665
3077
|
return metacognition.summary(self)
|
|
2666
3078
|
|
|
3079
|
+
# 生效条件:不带 self 转调 metacognition.catalog() 并返回其结果。
|
|
2667
3080
|
def metacognition_catalog(self):
|
|
2668
3081
|
"""自描述:观测面 + 理论出处 + 独立性约束。"""
|
|
2669
3082
|
return metacognition.catalog()
|
|
2670
3083
|
|
|
3084
|
+
# 生效条件:在已用 root 构造的实例上以 window(缺省 200)转调 metacognition.d_meta_face(self, window=window) 并返回其结果。
|
|
3085
|
+
def metacognition_d_meta(self, window=200):
|
|
3086
|
+
"""D_meta 观测面:边界压力向量(三代理各自 [0,1],不合成单值)。"""
|
|
3087
|
+
return metacognition.d_meta_face(self, window=window)
|
|
3088
|
+
|
|
2671
3089
|
# ============ 自我状态层(薄自我 + 富索引)============
|
|
2672
3090
|
# self 层只放状态卡(单例)+ 关系节点;九项自我信息只登记当前值与指针,
|
|
2673
3091
|
# 具体任务/人物/会话/时间/信任的细节由认知图按五维索引连接(不搬运内容)。
|
|
2674
3092
|
|
|
3093
|
+
# 生效条件:在已用 root 构造的实例上以 subject(缺省为模块级常量 self_state.DEFAULT_SUBJECT)转调 self_state.snapshot(self, subject) 并返回其结果。
|
|
2675
3094
|
def self_state_snapshot(self, subject=self_state.DEFAULT_SUBJECT):
|
|
2676
3095
|
"""读自我状态卡(薄):信息差/信任/情绪/情感/短期记忆/重要性/身份/关系。"""
|
|
2677
3096
|
return self_state.snapshot(self, subject)
|
|
2678
3097
|
|
|
3098
|
+
# 生效条件:subject 缺省为 self_state.DEFAULT_SUBJECT,且当 kw 中 session 为假值(缺键/None/"")时置为 getattr(self, "session", None),随后以 (**kw) 转调 self_state.refresh(self, subject, **kw) 并返回其结果。
|
|
2679
3099
|
def self_state_refresh(self, subject=self_state.DEFAULT_SUBJECT, **kw):
|
|
2680
3100
|
"""刷新状态卡:聚合九项自我信息 → 写卡 + 版本链留痕(幂等)。
|
|
2681
3101
|
|
|
@@ -2685,44 +3105,54 @@ class MdCGOS(MdCG):
|
|
|
2685
3105
|
kw["session"] = getattr(self, "session", None)
|
|
2686
3106
|
return self_state.refresh(self, subject, **kw)
|
|
2687
3107
|
|
|
3108
|
+
# 生效条件:subject 缺省为模块级常量 self_state.DEFAULT_SUBJECT,以 (**kw) 转调 self_state.bootstrap(self, subject, **kw) 并返回其结果。
|
|
2688
3109
|
def self_state_bootstrap(self, subject=self_state.DEFAULT_SUBJECT, **kw):
|
|
2689
3110
|
"""会话启动加载:状态卡 + 关系 + 最近留痕 + 五维索引(跨会话自我续接)。"""
|
|
2690
3111
|
return self_state.bootstrap(self, subject, **kw)
|
|
2691
3112
|
|
|
3113
|
+
# 生效条件:传入 frm 与 to,以 (**kw) 转调 self_state.relate(self, frm, to, **kw) 并返回其结果。
|
|
2692
3114
|
def self_state_relate(self, frm, to, **kw):
|
|
2693
3115
|
"""写一条有向关系(自我 ↔ 其他智能),reciprocal=True 时双向。"""
|
|
2694
3116
|
return self_state.relate(self, frm, to, **kw)
|
|
2695
3117
|
|
|
3118
|
+
# 生效条件:以 subject(缺省 None)与 direction(缺省 "both")转调 self_state.relations(self, subject=subject, direction=direction) 并返回其结果。
|
|
2696
3119
|
def self_state_relations(self, subject=None, direction="both"):
|
|
2697
3120
|
"""列出关系节点(按 subject 过滤出/入)。"""
|
|
2698
3121
|
return self_state.relations(self, subject=subject, direction=direction)
|
|
2699
3122
|
|
|
3123
|
+
# 生效条件:传入 dim 与 value,以 (**kw) 转调 self_state.index(self, dim, value, **kw) 并返回其结果。
|
|
2700
3124
|
def self_state_index(self, dim, value, **kw):
|
|
2701
3125
|
"""按五维索引(task/person/session/time/trust)反查具体详情节点。"""
|
|
2702
3126
|
return self_state.index(self, dim, value, **kw)
|
|
2703
3127
|
|
|
3128
|
+
# 生效条件:以 subject(缺省为模块级常量 self_state.DEFAULT_SUBJECT)转调 self_state.dimensions(self, subject) 并返回其结果。
|
|
2704
3129
|
def self_state_dimensions(self, subject=self_state.DEFAULT_SUBJECT):
|
|
2705
3130
|
"""状态卡登记的五维索引标签。"""
|
|
2706
3131
|
return self_state.dimensions(self, subject)
|
|
2707
3132
|
|
|
3133
|
+
# 生效条件:以 subject(缺省为模块级常量 self_state.DEFAULT_SUBJECT)与 (**kw) 转调 self_state.audit(self, subject, **kw) 并返回其结果。
|
|
2708
3134
|
def self_state_audit(self, subject=self_state.DEFAULT_SUBJECT, **kw):
|
|
2709
3135
|
"""自我信息一致性审计:单例/版本链/时序/派生自洽/跨面一致/身份/关系/保护/索引。"""
|
|
2710
3136
|
return self_state.audit(self, subject, **kw)
|
|
2711
3137
|
|
|
3138
|
+
# 生效条件:以 limit(缺省 100)、subject(缺省 None)转调 self_state.history(self, limit=limit, subject=subject) 并返回其结果。
|
|
2712
3139
|
def self_state_history(self, limit=100, subject=None):
|
|
2713
3140
|
"""自我状态留痕(倒序,含版本链 hash)。"""
|
|
2714
3141
|
return self_state.history(self, limit=limit, subject=subject)
|
|
2715
3142
|
|
|
3143
|
+
# 生效条件:subject 缺省为模块级常量 self_state.DEFAULT_SUBJECT,session 为假值(None/"")时回落 getattr(self, "session", None),随后转调 self_state.summary(self, subject, session=session or getattr(self, "session", None)) 并返回其结果。
|
|
2716
3144
|
def self_state_summary(self, subject=self_state.DEFAULT_SUBJECT,
|
|
2717
3145
|
session=None):
|
|
2718
3146
|
"""一句话自我状态(供 health / 面板;session= 会话归因切片)。"""
|
|
2719
3147
|
return self_state.summary(self, subject,
|
|
2720
3148
|
session=session or getattr(self, "session", None))
|
|
2721
3149
|
|
|
3150
|
+
# 生效条件:不带 self 转调 self_state.catalog() 并返回其结果。
|
|
2722
3151
|
def self_state_catalog(self):
|
|
2723
3152
|
"""自描述:九项自我信息 + 五维索引 + 审计规则。"""
|
|
2724
3153
|
return self_state.catalog()
|
|
2725
3154
|
|
|
3155
|
+
# 生效条件:传入 node_id,relation_types 为假值(None/""等)时回落模块级常量 chain.CAUSAL_TYPES,连同 max_depth(缺省 chain.MAX_DEPTH_DEFAULT)、direction(缺省 "out")、max_chains(缺省 50)、sort(缺省 "strength")转调 chain.walk 并返回其结果。
|
|
2726
3156
|
def causal_chain(self, node_id, relation_types=None,
|
|
2727
3157
|
max_depth=chain.MAX_DEPTH_DEFAULT, direction="out",
|
|
2728
3158
|
max_chains=50, sort="strength"):
|
|
@@ -2735,12 +3165,14 @@ class MdCGOS(MdCG):
|
|
|
2735
3165
|
max_depth=max_depth, direction=direction,
|
|
2736
3166
|
max_chains=max_chains, sort=sort)
|
|
2737
3167
|
|
|
3168
|
+
# 生效条件:传入 node_id,以 (**kw) 转调 chain.explain(self, node_id, **kw) 并返回其结果。
|
|
2738
3169
|
def explain_chain(self, node_id, **kw):
|
|
2739
3170
|
"""人类可读链式解释:「什么条件下 → 发生什么」。"""
|
|
2740
3171
|
return chain.explain(self, node_id, **kw)
|
|
2741
3172
|
|
|
2742
3173
|
# ============ 生成式预测 / 因果推理 ============
|
|
2743
3174
|
|
|
3175
|
+
# 生效条件:在已用 root 构造的实例上按原值透传 start_id/blindspot_id/horizon(缺省 predict.HORIZON_DEFAULT)/max_branches(缺省 predict.MAX_BRANCHES_DEFAULT)/sort(缺省 "composite")/limit(缺省 0)/semantic(缺省 True)调用 predict.routes 并返回其结果。
|
|
2744
3176
|
def predict_routes(self, start_id=None, blindspot_id=None,
|
|
2745
3177
|
horizon=predict.HORIZON_DEFAULT,
|
|
2746
3178
|
max_branches=predict.MAX_BRANCHES_DEFAULT,
|
|
@@ -2751,6 +3183,7 @@ class MdCGOS(MdCG):
|
|
|
2751
3183
|
max_branches=max_branches, sort=sort,
|
|
2752
3184
|
limit=limit, semantic=semantic)
|
|
2753
3185
|
|
|
3186
|
+
# 生效条件:传入 predicted_node_id 与 actual_node_id,并以 hit(缺省 None)/note(缺省 "")/actor(缺省 "predict")/sync_self(缺省 True)转调 predict.feedback 并返回其结果。
|
|
2754
3187
|
def predict_feedback(self, predicted_node_id, actual_node_id=None,
|
|
2755
3188
|
hit=None, note="", actor="predict", sync_self=True):
|
|
2756
3189
|
"""预测反馈(D-006):命中 → 边置信度 +0.05;未命中 → 登记 rejected。
|
|
@@ -2762,18 +3195,22 @@ class MdCGOS(MdCG):
|
|
|
2762
3195
|
hit=hit, note=note, actor=actor,
|
|
2763
3196
|
sync_self=sync_self)
|
|
2764
3197
|
|
|
3198
|
+
# 生效条件:在已用 root 构造的实例上以 limit(缺省 20)转调 predict.stats(self, limit=limit) 并返回其结果。
|
|
2765
3199
|
def predict_stats(self, limit=20):
|
|
2766
3200
|
"""预测统计:调用数 / 路线数 / 命中率 / 动态阈值。"""
|
|
2767
3201
|
return predict.stats(self, limit=limit)
|
|
2768
3202
|
|
|
3203
|
+
# 生效条件:不带 self 转调 predict.catalog() 并返回其结果。
|
|
2769
3204
|
def predict_catalog(self):
|
|
2770
3205
|
"""自描述:D-001~D-006 决策、权重、校准参数、与 AEIS 的差异。"""
|
|
2771
3206
|
return predict.catalog()
|
|
2772
3207
|
|
|
3208
|
+
# 生效条件:传入 a_id 与 b_id,以 max_depth(缺省 5)转调 predict.causal_path(self, a_id, b_id, max_depth=max_depth) 并返回其结果。
|
|
2773
3209
|
def causal_path(self, a_id, b_id, max_depth=5):
|
|
2774
3210
|
"""因果路径推理:A 能否沿因果/时序边到达 B(伪因果防护的完整语义)。"""
|
|
2775
3211
|
return predict.causal_path(self, a_id, b_id, max_depth=max_depth)
|
|
2776
3212
|
|
|
3213
|
+
# 生效条件:传入 a_id 与 b_id,转调 predict.causal_gate(self, a_id, b_id) 并返回其结果。
|
|
2777
3214
|
def causal_gate(self, a_id, b_id):
|
|
2778
3215
|
"""D-002 伪因果过滤门 → (准入?, 理由)。"""
|
|
2779
3216
|
return predict.causal_gate(self, a_id, b_id)
|
|
@@ -2781,6 +3218,7 @@ class MdCGOS(MdCG):
|
|
|
2781
3218
|
# ============ 演化账本(md 载体:规律 + 状态,可回滚)============
|
|
2782
3219
|
# 每一次修改 = 对一条缺失条件的补充;记录的是认知规律与状态,不是实现。
|
|
2783
3220
|
|
|
3221
|
+
# 生效条件:在已用 root 构造的实例上转调 evolution.record,其中 kind 为假值(None/"")时回落模块级常量 evolution.KIND_CONDITION_GAP,**extra 为空字典时传 extra=None,node_id/pattern/missing/action/evidence/source/before/after 原值透传。
|
|
2784
3222
|
def evolution_record(self, node_id=None, pattern="", missing="", action="",
|
|
2785
3223
|
evidence="", source="", kind=None, before=None,
|
|
2786
3224
|
after=None, **extra):
|
|
@@ -2791,36 +3229,44 @@ class MdCGOS(MdCG):
|
|
|
2791
3229
|
kind=kind or evolution.KIND_CONDITION_GAP,
|
|
2792
3230
|
before=before, after=after, extra=extra or None)
|
|
2793
3231
|
|
|
3232
|
+
# 生效条件:在已用 root 构造的实例上以 limit(缺省 50)、node_id(缺省 None)、kind(缺省 None)转调 evolution.entries,返回 {"entries": ...}。
|
|
2794
3233
|
def evolution_entries(self, limit=50, node_id=None, kind=None):
|
|
2795
3234
|
"""账本条目(倒序)。"""
|
|
2796
3235
|
return {"entries": evolution.entries(
|
|
2797
3236
|
self, limit=limit, node_id=node_id, kind=kind)}
|
|
2798
3237
|
|
|
3238
|
+
# 生效条件:传入 entry_id,转调 evolution.show(self, entry_id),返回 {"entry": ...}。
|
|
2799
3239
|
def evolution_show(self, entry_id):
|
|
2800
3240
|
"""单条演化条目。"""
|
|
2801
3241
|
return {"entry": evolution.show(self, entry_id)}
|
|
2802
3242
|
|
|
3243
|
+
# 生效条件:传入 node_id,以 limit(缺省 50)转调 evolution.history(self, node_id, limit=limit) 并返回其结果。
|
|
2803
3244
|
def evolution_history(self, node_id, limit=50):
|
|
2804
3245
|
"""某节点的演化史(倒序)。"""
|
|
2805
3246
|
return evolution.history(self, node_id, limit=limit)
|
|
2806
3247
|
|
|
3248
|
+
# 生效条件:在已用 root 构造的实例上以 limit(缺省 10)转调 evolution.patterns(self, limit=limit) 并返回其结果。
|
|
2807
3249
|
def evolution_patterns(self, limit=10):
|
|
2808
3250
|
"""规律统计:哪一维条件反复缺失、由谁触发、哪些规律重复出现。"""
|
|
2809
3251
|
return evolution.patterns(self, limit=limit)
|
|
2810
3252
|
|
|
3253
|
+
# 生效条件:在已用 root 构造的实例上转调 evolution.summary(self) 并返回其结果。
|
|
2811
3254
|
def evolution_summary(self):
|
|
2812
3255
|
"""一句话演化状态(供 health / 面板)。"""
|
|
2813
3256
|
return evolution.summary(self)
|
|
2814
3257
|
|
|
3258
|
+
# 生效条件:传入 entry_id,以 dry_run(缺省 False)、note(缺省 "")转调 evolution.rollback 并返回其结果(含 note 为空时原样传空串)。
|
|
2815
3259
|
def evolution_rollback(self, entry_id, dry_run=False, note=""):
|
|
2816
3260
|
"""把某条演化撤回其 before 状态;撤销本身也记一条条目。"""
|
|
2817
3261
|
return evolution.rollback(self, entry_id, dry_run=dry_run, note=note)
|
|
2818
3262
|
|
|
3263
|
+
# 生效条件:不带 self 转调 evolution.catalog() 并返回其结果。
|
|
2819
3264
|
def evolution_catalog(self):
|
|
2820
3265
|
"""自描述:载体 + 原则 + 字段 + 可回滚范围。"""
|
|
2821
3266
|
return evolution.catalog()
|
|
2822
3267
|
|
|
2823
3268
|
|
|
3269
|
+
# 生效条件:以 root 构造并把 **kw 透传父类,principal 为假值(None)时回落新建 Principal()、否则用传入的 principal,其 actor 作为 actor 传给父类,master_key 原样交给 _init_crypto;
|
|
2824
3270
|
class MdCGSecure(MdCGOS):
|
|
2825
3271
|
"""带权限的记忆 OS:租户 + 密级(clearance)× 节点敏感度(sensitivity)。
|
|
2826
3272
|
|
|
@@ -2831,6 +3277,7 @@ class MdCGSecure(MdCGOS):
|
|
|
2831
3277
|
· 审计带 tenant/actor/session(可追溯到哪个会话做了什么)
|
|
2832
3278
|
"""
|
|
2833
3279
|
|
|
3280
|
+
# 生效条件:传入 root 时 principal 为假值(None 等)则新建 Principal(),以 self.principal.actor 作 actor 调父类 MdCGOS.__init__(root, actor=..., **kw),再设 self.session = self.principal.session 并执行 self._init_crypto(master_key)(master_key 缺省 None)。
|
|
2834
3281
|
def __init__(self, root: str, principal: Principal = None,
|
|
2835
3282
|
master_key=None, **kw):
|
|
2836
3283
|
self.principal = principal or Principal()
|
|
@@ -2843,6 +3290,7 @@ class MdCGSecure(MdCGOS):
|
|
|
2843
3290
|
|
|
2844
3291
|
# ---------- 私有内容加密(密钥即访问权 + 身份一致性识别)----------
|
|
2845
3292
|
|
|
3293
|
+
# 生效条件:master_key 非 None 时取 self._resolve_master_key(master_key)、为 None 时取 crypto.load_master_key();kek 为假值时置 _crypto_error="no_master_key" 且 self.kek=self.dek=None 并返回;否则置 self.kek 并调 crypto.provision_dek(self.root, kek, tenant, actor, clearance=...) 得 dek;捕获 crypto.CryptoError 或 OSError 时置 _crypto_error=str(e) 且 self.kek=self.dek=None。
|
|
2846
3294
|
def _init_crypto(self, master_key=None):
|
|
2847
3295
|
"""解析 KEK(显式 → 环境变量 → 仓库外主密钥文件),签发本身份 DEK。
|
|
2848
3296
|
|
|
@@ -2867,6 +3315,7 @@ class MdCGSecure(MdCGOS):
|
|
|
2867
3315
|
self.kek = self.dek = None
|
|
2868
3316
|
|
|
2869
3317
|
@staticmethod
|
|
3318
|
+
# 生效条件:master_key 为 bytes/bytearray 时直接取字节,否则转 str 去空白后若该串是存在路径则读文件内容,len==64 走 bytes.fromhex、否则走 base64 解码,所得长度不等于 crypto.KEY_LEN 时抛 CryptoError;
|
|
2870
3319
|
def _resolve_master_key(master_key):
|
|
2871
3320
|
"""接受 32B bytes / 64 位 hex / base64 / 密钥文件路径。"""
|
|
2872
3321
|
if isinstance(master_key, (bytes, bytearray)):
|
|
@@ -2885,17 +3334,20 @@ class MdCGSecure(MdCGOS):
|
|
|
2885
3334
|
raise crypto.CryptoError("主密钥须为 32 字节")
|
|
2886
3335
|
return k
|
|
2887
3336
|
|
|
3337
|
+
# 生效条件:调用 self._init_crypto(master_key);master_key 为 None 时回落环境变量与主密钥文件,密钥缺失或长度非 32 字节抛 crypto.CryptoError;成功后返回 crypto_status();
|
|
2888
3338
|
def unlock(self, master_key=None):
|
|
2889
3339
|
"""运行时解锁(显式密钥 / 重新加载环境变量或主密钥文件)。"""
|
|
2890
3340
|
self._init_crypto(master_key)
|
|
2891
3341
|
return self.crypto_status()
|
|
2892
3342
|
|
|
3343
|
+
# 生效条件:无前置;丢弃内存中的 self.kek / self.dek 并置 _crypto_error="locked"(已落盘密文不受影响);返回 crypto_status();
|
|
2893
3344
|
def lock(self):
|
|
2894
3345
|
"""锁定:丢弃内存中的密钥(已落盘密文不受影响)。"""
|
|
2895
3346
|
self.kek = self.dek = None
|
|
2896
3347
|
self._crypto_error = "locked"
|
|
2897
3348
|
return self.crypto_status()
|
|
2898
3349
|
|
|
3350
|
+
# 生效条件:调用 crypto_status 时,返回当前加密状态字典;unlocked 取决于 self.dek is not None,kek_fp 在 self.kek 为假值时取 None,否则取 crypto.kek_fingerprint(self.kek)。
|
|
2899
3351
|
def crypto_status(self):
|
|
2900
3352
|
"""当前加密状态(不含密钥材料)。"""
|
|
2901
3353
|
return {
|
|
@@ -2911,6 +3363,7 @@ class MdCGSecure(MdCGOS):
|
|
|
2911
3363
|
"envelopes": crypto.envelopes(self.root),
|
|
2912
3364
|
}
|
|
2913
3365
|
|
|
3366
|
+
# 生效条件:sens 取 sensitivity or DEFAULT_SENSITIVITY,若 sens 不在 crypto.ENCRYPTED_LEVELS 或 content 已加密则原样返回 content;否则 self.dek 为假值时写 seal_denied 审计并抛 crypto.LockedError,有 dek 时 seal_node 并写 seal 审计后返回密文。
|
|
2914
3367
|
def _seal_content(self, node_id, content, sensitivity=None):
|
|
2915
3368
|
"""私有内容(private / secret)写入前加密;无密钥 → fail-closed。"""
|
|
2916
3369
|
sens = sensitivity or DEFAULT_SENSITIVITY
|
|
@@ -2931,6 +3384,7 @@ class MdCGSecure(MdCGOS):
|
|
|
2931
3384
|
"actor": self.principal.actor})
|
|
2932
3385
|
return sealed
|
|
2933
3386
|
|
|
3387
|
+
# 生效条件:content 为 None 或非加密时原样返回 content;加密但 self.dek 为假值时写 read_locked 审计并返回 None;crypto.open_node 抛 CryptoError 时写 open_failed 审计并返回 None,成功则返回明文。
|
|
2934
3388
|
def _open_content(self, node_id, fm, content):
|
|
2935
3389
|
"""密文解封;无密钥 / 身份不符 → None(不可读),失败留审计。"""
|
|
2936
3390
|
if content is None or not crypto.is_encrypted(content):
|
|
@@ -2953,6 +3407,7 @@ class MdCGSecure(MdCGOS):
|
|
|
2953
3407
|
|
|
2954
3408
|
# ---------- 索引:把 role / sensitivity / 写入归属一并索引 ----------
|
|
2955
3409
|
|
|
3410
|
+
# 生效条件:对传入的 kw 生效——writer/session 缺键时分别落 self.principal.actor 与 self.session,self.principal.harness 为真值时 harness 缺键才落该值,已存在的键一律不覆盖。
|
|
2956
3411
|
def _attribution(self, kw):
|
|
2957
3412
|
"""写入归属注入(归因维度,不参与授权)。
|
|
2958
3413
|
|
|
@@ -2966,6 +3421,7 @@ class MdCGSecure(MdCGOS):
|
|
|
2966
3421
|
kw.setdefault("harness", self.principal.harness)
|
|
2967
3422
|
return kw
|
|
2968
3423
|
|
|
3424
|
+
# 生效条件:在 super()._scan_nodes() 结果上逐节点重新 _read,仅当读出的 fm 为真值时把 role、sensitivity(假值回落 DEFAULT_SENSITIVITY)、writer、session、content_kind(角色化读取视图的候选资格维度,第四阶段 6.1)写回该条目。
|
|
2969
3425
|
def _scan_nodes(self):
|
|
2970
3426
|
nodes = super()._scan_nodes()
|
|
2971
3427
|
for nid, e in nodes.items():
|
|
@@ -2975,8 +3431,10 @@ class MdCGSecure(MdCGOS):
|
|
|
2975
3431
|
e["sensitivity"] = fm.get("sensitivity") or DEFAULT_SENSITIVITY
|
|
2976
3432
|
e["writer"] = fm.get("writer")
|
|
2977
3433
|
e["session"] = fm.get("session")
|
|
3434
|
+
e["content_kind"] = fm.get("content_kind")
|
|
2978
3435
|
return nodes
|
|
2979
3436
|
|
|
3437
|
+
# 生效条件:仅当 self.index["nodes"] 中已存在 nid 时,把该条目的 sensitivity 置为 sens 并标记进 self._dirty;nid 不存在则不做任何事、无返回。
|
|
2980
3438
|
def _index_sensitivity(self, nid, sens):
|
|
2981
3439
|
e = self.index["nodes"].get(nid)
|
|
2982
3440
|
if e is not None:
|
|
@@ -2985,6 +3443,7 @@ class MdCGSecure(MdCGOS):
|
|
|
2985
3443
|
|
|
2986
3444
|
# ---------- 写:权限校验 ----------
|
|
2987
3445
|
|
|
3446
|
+
# 生效条件:sens 取 sensitivity or DEFAULT_SENSITIVITY,先 _rank(sens) 并 principal.require_layer_write(layer, sens),再 _attribution(kw) 后转 super().add,最后按下发的 nid 调 _index_sensitivity 并返回 nid。
|
|
2988
3447
|
def add(self, node_id: str, content: str, layer: str = "knowledge",
|
|
2989
3448
|
sensitivity: str = None, **kw) -> str:
|
|
2990
3449
|
sens = sensitivity or DEFAULT_SENSITIVITY
|
|
@@ -2995,6 +3454,7 @@ class MdCGSecure(MdCGOS):
|
|
|
2995
3454
|
self._index_sensitivity(nid, sens)
|
|
2996
3455
|
return nid
|
|
2997
3456
|
|
|
3457
|
+
# 生效条件:sens 取 sensitivity or DEFAULT_SENSITIVITY,先 principal.require_layer_write("rejected", sens) 与 _attribution(kw) 后转 super().add_rejected,最后按下发的 nid 调 _index_sensitivity 并返回 nid。
|
|
2998
3458
|
def add_rejected(self, hypothesis: str, reason: str, sensitivity: str = None, **kw) -> str:
|
|
2999
3459
|
sens = sensitivity or DEFAULT_SENSITIVITY
|
|
3000
3460
|
self.principal.require_layer_write("rejected", sens)
|
|
@@ -3003,6 +3463,7 @@ class MdCGSecure(MdCGOS):
|
|
|
3003
3463
|
self._index_sensitivity(nid, sens)
|
|
3004
3464
|
return nid
|
|
3005
3465
|
|
|
3466
|
+
# 生效条件:sens 取 sensitivity or DEFAULT_SENSITIVITY,先 principal.require_layer_write("unresolved", sens) 与 _attribution(kw) 后转 super().add_unresolved(question, known_clues, goal),最后调 _index_sensitivity 并返回 nid。
|
|
3006
3467
|
def add_unresolved(self, question: str, known_clues: str = "", goal: str = "",
|
|
3007
3468
|
sensitivity: str = None, **kw) -> str:
|
|
3008
3469
|
sens = sensitivity or DEFAULT_SENSITIVITY
|
|
@@ -3012,11 +3473,13 @@ class MdCGSecure(MdCGOS):
|
|
|
3012
3473
|
self._index_sensitivity(nid, sens)
|
|
3013
3474
|
return nid
|
|
3014
3475
|
|
|
3476
|
+
# 生效条件:sens 取 sensitivity or DEFAULT_SENSITIVITY,先 principal.require_layer_write(kw.get("layer") or "contextual", sens),再转 super().propose(node_id, content, sensitivity=sens) 并返回其结果。
|
|
3015
3477
|
def propose(self, node_id: str, content: str, sensitivity: str = None, **kw):
|
|
3016
3478
|
sens = sensitivity or DEFAULT_SENSITIVITY
|
|
3017
3479
|
self.principal.require_layer_write(kw.get("layer") or "contextual", sens)
|
|
3018
3480
|
return super().propose(node_id, content, sensitivity=sens, **kw)
|
|
3019
3481
|
|
|
3482
|
+
# 生效条件:sens 取 sensitivity or DEFAULT_SENSITIVITY,先 _rank(sens) 并 principal.require_layer_write("goals", sens),再转 super().add_goal,最后按 gid 调 _index_sensitivity 并返回 gid。
|
|
3020
3483
|
def add_goal(self, goal: str, sensitivity: str = None, **kw) -> str:
|
|
3021
3484
|
sens = sensitivity or DEFAULT_SENSITIVITY
|
|
3022
3485
|
_rank(sens)
|
|
@@ -3025,10 +3488,12 @@ class MdCGSecure(MdCGOS):
|
|
|
3025
3488
|
self._index_sensitivity(gid, sens)
|
|
3026
3489
|
return gid
|
|
3027
3490
|
|
|
3491
|
+
# 生效条件:先 principal.require_layer_write("goals", DEFAULT_SENSITIVITY),再转 super().set_goal_status(node_id, status)。
|
|
3028
3492
|
def set_goal_status(self, node_id: str, status: str):
|
|
3029
3493
|
self.principal.require_layer_write("goals", DEFAULT_SENSITIVITY)
|
|
3030
3494
|
return super().set_goal_status(node_id, status)
|
|
3031
3495
|
|
|
3496
|
+
# 生效条件:sens 取 sensitivity or DEFAULT_SENSITIVITY,经 _rank(sens) 与 principal.require_write(sens) 后把 m 基于 meta 复制并 setdefault tenant/session、harness 与 unit 为真值时补入,再强制 m["sensitivity"]=sens,text 经 _seal_content("_recent", text, sens) 后连 tags=tags 一起转 super().remember_event(window 为 None 时不传该参,否则带上 window)。
|
|
3032
3497
|
def remember_event(self, role: str, text: str, tags=None, meta=None,
|
|
3033
3498
|
window=None, sensitivity: str = None):
|
|
3034
3499
|
sens = sensitivity or DEFAULT_SENSITIVITY
|
|
@@ -3049,6 +3514,7 @@ class MdCGSecure(MdCGOS):
|
|
|
3049
3514
|
|
|
3050
3515
|
# ---------- 读:密级过滤 ----------
|
|
3051
3516
|
|
|
3517
|
+
# 生效条件:e 的 sensitivity 为假值时先 _read 回填为 fm 的 sensitivity or DEFAULT_SENSITIVITY,再以 self.principal.allows(sens) 的布尔结果为准。
|
|
3052
3518
|
def _readable(self, e) -> bool:
|
|
3053
3519
|
sens = e.get("sensitivity")
|
|
3054
3520
|
if not sens:
|
|
@@ -3059,6 +3525,7 @@ class MdCGSecure(MdCGOS):
|
|
|
3059
3525
|
e["sensitivity"] = sens
|
|
3060
3526
|
return self.principal.allows(sens)
|
|
3061
3527
|
|
|
3528
|
+
# 生效条件:先以 limit=None 取 super().list_goals(status=status) 的全量,再只保留 index 中 _readable(e) 为真的目标,limit 为真值时返回 keep[:limit]、否则返回全部 keep。
|
|
3062
3529
|
def list_goals(self, status=None, limit=None):
|
|
3063
3530
|
"""读隔离:只返回当前 clearance 可见的目标(active_goals/goal_text 同源过滤)。"""
|
|
3064
3531
|
out = super().list_goals(status=status, limit=None)
|
|
@@ -3069,6 +3536,7 @@ class MdCGSecure(MdCGOS):
|
|
|
3069
3536
|
keep.append(g)
|
|
3070
3537
|
return keep[:limit] if limit else keep
|
|
3071
3538
|
|
|
3539
|
+
# 生效条件:先以 limit=0 取全量,逐条跳过 meta.tenant 与 self.principal.tenant 不一致的、以及 principal.allows(meta 的 sensitivity or DEFAULT_SENSITIVITY) 为假的,文本加密时解封失败(None)也跳过,最后 limit 为真值返回 keep[:limit]、否则返回全部 keep。
|
|
3072
3540
|
def recent_events(self, limit=20, roles=None, since=None, newest_first=True):
|
|
3073
3541
|
"""读隔离:只返回本 tenant 且当前 clearance 可见的近期事件。"""
|
|
3074
3542
|
out = super().recent_events(limit=0, roles=roles, since=since,
|
|
@@ -3089,46 +3557,96 @@ class MdCGSecure(MdCGOS):
|
|
|
3089
3557
|
keep.append(r)
|
|
3090
3558
|
return keep[:limit] if limit else keep
|
|
3091
3559
|
|
|
3560
|
+
# 生效条件:先 principal.require_admin("clear_recent"),再转 super().clear_recent()。
|
|
3092
3561
|
def clear_recent(self):
|
|
3093
3562
|
self.principal.require_admin("clear_recent")
|
|
3094
3563
|
return super().clear_recent()
|
|
3095
3564
|
|
|
3565
|
+
# 生效条件:在 super()._candidates(layer=layer, roles=roles, include_work=include_work, session=session, branch=branch, validity=validity, start_time=start_time, end_time=end_time, start_operator=start_operator, end_operator=end_operator, time_axis=time_axis, view=view) 的结果上,只保留 self._readable(e) 为真的条目(时间算子在父类候选层**单点已过滤**,此处只叠加读可见性、不重复判一次——重复判会让 dropped 计数与 meta 脱钩)。
|
|
3096
3566
|
def _candidates(self, layer=None, roles=None, include_work=False,
|
|
3097
|
-
session=None, branch=None
|
|
3567
|
+
session=None, branch=None, validity=None,
|
|
3568
|
+
start_time=None, end_time=None, start_operator=None,
|
|
3569
|
+
end_operator=None, time_axis=None, view=None):
|
|
3098
3570
|
out = super()._candidates(layer=layer, roles=roles, include_work=include_work,
|
|
3099
|
-
session=session, branch=branch
|
|
3571
|
+
session=session, branch=branch, validity=validity,
|
|
3572
|
+
start_time=start_time, end_time=end_time,
|
|
3573
|
+
start_operator=start_operator,
|
|
3574
|
+
end_operator=end_operator, time_axis=time_axis,
|
|
3575
|
+
view=view)
|
|
3100
3576
|
return [e for e in out if self._readable(e)]
|
|
3101
3577
|
|
|
3578
|
+
# 生效条件:在 super()._neg_coverage(terms) 的结果上,只保留 self._readable(e) 为真的条目。
|
|
3102
3579
|
def _neg_coverage(self, terms):
|
|
3103
3580
|
return [e for e in super()._neg_coverage(terms) if self._readable(e)]
|
|
3104
3581
|
|
|
3582
|
+
# 生效条件:node_id 在 self.index["nodes"] 中存在且 self._readable(e) 为假时返回 None,否则转 super().get(node_id)。
|
|
3105
3583
|
def get(self, node_id: str):
|
|
3106
3584
|
e = self.index["nodes"].get(node_id)
|
|
3107
3585
|
if e is not None and not self._readable(e):
|
|
3108
3586
|
return None # 读隔离:不可见即不存在
|
|
3109
3587
|
return super().get(node_id)
|
|
3110
3588
|
|
|
3589
|
+
# 生效条件:把 *a/**kw 原样转给 super().search_rrf 后,仅保留其结果中每条以 self.index["nodes"].get(结果节点 id) 为索引(索引缺该 id 时用结果节点自身)经 self._readable 判为可见的条目,且 kw["validity"] 为真时该条目经 trust.is_expired 判为未过期者,kw["view"] 为真值时该条目经 roleviews.matches 判为满足视图资格(非法 view ValueError),再返回。
|
|
3111
3590
|
def search_rrf(self, *a, **kw):
|
|
3112
|
-
"""RRF 路径里的图扩展会绕过 _candidates,这里显式再过滤一次。
|
|
3591
|
+
"""RRF 路径里的图扩展会绕过 _candidates,这里显式再过滤一次。
|
|
3592
|
+
|
|
3593
|
+
时效过滤(validity)同属候选资格:图扩展会把过期节点重新带回结果,
|
|
3594
|
+
故必须在此与读可见性一并二次过滤,否则过期节点经扩散路径绕过 _candidates。
|
|
3595
|
+
角色化读取视图(第四阶段 6.1)同理:view 声明的候选资格对图扩展
|
|
3596
|
+
扩散路径同样生效,在此一并兜底。非法 view 由 roleviews.matches 的
|
|
3597
|
+
fail-closed 直接抛出(与候选层同口径,不会静默放行)。
|
|
3598
|
+
时间算子(阶段二 4.1)同理由此二次过滤——判据复用 `trust.filter_by_time`
|
|
3599
|
+
的同一实现口径(`window_matches_node`),**不另写一套轴/算子判断**。
|
|
3600
|
+
边界:本层计数不并入 `meta["time_filter"]`(该块以父类候选层为准),
|
|
3601
|
+
二次过滤只做「不放进结果」的兜底,差额如实不记账。
|
|
3602
|
+
"""
|
|
3113
3603
|
res, meta = super().search_rrf(*a, **kw)
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3604
|
+
validity = kw.get("validity")
|
|
3605
|
+
view = kw.get("view")
|
|
3606
|
+
now = time.time() if validity else None
|
|
3607
|
+
_en_t, _ax_t, _why_t = trust.check_time_args(
|
|
3608
|
+
kw.get("start_time"), kw.get("end_time"), kw.get("start_operator"),
|
|
3609
|
+
kw.get("end_operator"), kw.get("time_axis"))
|
|
3610
|
+
keep = []
|
|
3611
|
+
for r in res:
|
|
3612
|
+
e = self.index["nodes"].get(r[0]["id"], r[0])
|
|
3613
|
+
if not self._readable(e):
|
|
3614
|
+
continue
|
|
3615
|
+
if view is not None and not roleviews.matches(e, view):
|
|
3616
|
+
continue
|
|
3617
|
+
if validity and trust.is_expired(e, now=now):
|
|
3618
|
+
continue
|
|
3619
|
+
if _en_t:
|
|
3620
|
+
_m, _miss = trust.window_matches_node(
|
|
3621
|
+
e, _ax_t, trust.parse_time(kw.get("start_time")),
|
|
3622
|
+
trust.parse_time(kw.get("end_time")),
|
|
3623
|
+
kw.get("start_operator"), kw.get("end_operator"))
|
|
3624
|
+
if _miss:
|
|
3625
|
+
if _ax_t == "observed":
|
|
3626
|
+
continue # 观察轴 fail-closed(与候选层同策略)
|
|
3627
|
+
elif not _m:
|
|
3628
|
+
continue
|
|
3629
|
+
keep.append(r)
|
|
3630
|
+
return keep, meta
|
|
3117
3631
|
|
|
3118
3632
|
# ---------- 管理:需 can_admin ----------
|
|
3119
3633
|
|
|
3634
|
+
# 生效条件:先 principal.require_admin("forget"),再转 super().forget(node_id, reason, override=override)。
|
|
3120
3635
|
def forget(self, node_id: str, reason: str = "", override: bool = False):
|
|
3121
3636
|
self.principal.require_admin("forget")
|
|
3122
3637
|
return super().forget(node_id, reason, override=override)
|
|
3123
3638
|
|
|
3639
|
+
# 生效条件:先 principal.require_admin("restore"),再转 super().restore(node_id, force=force)。
|
|
3124
3640
|
def restore(self, node_id: str, force: bool = False):
|
|
3125
3641
|
self.principal.require_admin("restore")
|
|
3126
3642
|
return super().restore(node_id, force=force)
|
|
3127
3643
|
|
|
3644
|
+
# 生效条件:先 principal.require_admin("review_decide"),再把 *a/**kw 原样转给 super().review_decide。
|
|
3128
3645
|
def review_decide(self, *a, **kw):
|
|
3129
3646
|
self.principal.require_admin("review_decide")
|
|
3130
3647
|
return super().review_decide(*a, **kw)
|
|
3131
3648
|
|
|
3649
|
+
# 生效条件:先 principal.require_admin("evolution_rollback"),再转 super().evolution_rollback(entry_id, dry_run=dry_run, note=note)。
|
|
3132
3650
|
def evolution_rollback(self, entry_id, dry_run=False, note=""):
|
|
3133
3651
|
"""回滚是管理操作:撤回结构变更 → 需 can_admin。"""
|
|
3134
3652
|
self.principal.require_admin("evolution_rollback")
|
|
@@ -3136,6 +3654,7 @@ class MdCGSecure(MdCGOS):
|
|
|
3136
3654
|
|
|
3137
3655
|
# ---------- 身份/审计 ----------
|
|
3138
3656
|
|
|
3657
|
+
# 生效条件:无入参,返回 principal.as_dict、root、按 SENSITIVITY_ORDER 中 p.allows 为真筛出的 readable_sensitivities、index 中 _readable 为真的 nodes_visible 与 nodes_total、encryption(crypto_status);tokens.role_spec 可用时另补 role_label/duty/forbidden,导入或取值异常则跳过。
|
|
3139
3658
|
def whoami(self):
|
|
3140
3659
|
p = self.principal
|
|
3141
3660
|
out = {"principal": p.as_dict(), "root": self.root,
|
|
@@ -3155,6 +3674,7 @@ class MdCGSecure(MdCGOS):
|
|
|
3155
3674
|
pass
|
|
3156
3675
|
return out
|
|
3157
3676
|
|
|
3677
|
+
# 生效条件:对 meta 先 setdefault tenant/session/clearance,并在 principal.harness、principal.unit 为真值时补入同名键,再转 super()._audit(op, node_id, **meta)。
|
|
3158
3678
|
def _audit(self, op, node_id, **meta):
|
|
3159
3679
|
meta.setdefault("tenant", self.principal.tenant)
|
|
3160
3680
|
meta.setdefault("session", self.principal.session)
|
|
@@ -3166,6 +3686,7 @@ class MdCGSecure(MdCGOS):
|
|
|
3166
3686
|
meta.setdefault("unit", self.principal.unit)
|
|
3167
3687
|
super()._audit(op, node_id, **meta)
|
|
3168
3688
|
|
|
3689
|
+
# 生效条件:在 super().health_os() 结果上写入 os.sustain(sustain.summary(self))与 security(principal 的 tenant/clearance、_sensitivity_counts、crypto_status)后返回。
|
|
3169
3690
|
def health_os(self):
|
|
3170
3691
|
h = super().health_os()
|
|
3171
3692
|
# 持续性自维持(常驻 / 心跳 / 会话续接):只读摘要,不做巡检
|
|
@@ -3178,9 +3699,10 @@ class MdCGSecure(MdCGOS):
|
|
|
3178
3699
|
}
|
|
3179
3700
|
return h
|
|
3180
3701
|
|
|
3702
|
+
# 生效条件:对 self.index["nodes"] 遍历生效——按每条 e 的 sensitivity(假值回落 DEFAULT_SENSITIVITY)累计计数并返回该字典。
|
|
3181
3703
|
def _sensitivity_counts(self):
|
|
3182
3704
|
c = {}
|
|
3183
3705
|
for e in self.index["nodes"].values():
|
|
3184
3706
|
s = e.get("sensitivity") or DEFAULT_SENSITIVITY
|
|
3185
3707
|
c[s] = c.get(s, 0) + 1
|
|
3186
|
-
return c
|
|
3708
|
+
return c
|