@furongjun1999/dsh-memory 0.6.0 → 0.7.0
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 +618 -572
- package/codebuddy/CODEBUDDY.md +10 -10
- package/data/policy.json +27 -0
- package/docs/README.md +1 -0
- package/docs/discipline/harnesses.yaml +31 -0
- package/docs/discipline/templates/full.md.tmpl +1 -1
- package/docs/discipline/templates/rules.mdc.tmpl +1 -1
- package/docs/eval/N225_/347/264/242/345/274/225/346/227/245/345/277/227/351/235/236/345/257/271/350/261/241/350/243/205/350/275/275/351/235/242/347/261/273/345/236/213/351/227/270_v1.0.md +418 -0
- package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/344/270/216/351/224/256/347/261/273/345/236/213/351/227/270_v1.1.md +441 -0
- package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/345/212/240/350/275/275/344/270/216/345/207/255/346/215/256/346/230/216/346/226/207/351/230/237/345/210/227_v1.0.md +398 -0
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/200/346/211/271_/346/216/245/347/272/277/344/270/216/347/255/211/344/273/267/345/217/230/346/215/242_v1.0.md +498 -0
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/211/346/211/271_/351/227/250/347/246/201/350/275/254/346/255/243/344/270/216/350/260/203/345/272/246/346/255/242/350/241/200/344/270/216/351/227/250/346/216/247/346/224/266/345/217/243_v1.0.md +669 -0
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/272/214/346/211/271_/344/276/235/350/265/226/351/200/217/344/274/240/344/270/216/345/257/271/346/213/215/345/217/243/345/276/204/344/270/216/350/264/237/347/274/223/345/255/230_v1.0.md +683 -0
- package/docs/eval//345/207/272/350/264/247/351/235/242/345/206/222/347/203/237_/350/277/233/350/264/247/351/227/250/347/246/201_v1.0.md +460 -0
- package/docs/eval//345/217/221/345/270/20307_/345/244/226/351/203/250/346/212/245/345/221/212/345/233/233/346/211/271/344/277/256/345/244/215/344/270/216/346/217/222/344/273/266/351/235/242/345/212/240/345/233/272_v1.0.md +205 -0
- package/docs/eval//345/217/221/345/270/20308_/350/207/252/350/277/255/344/273/243/344/270/216/347/235/241/347/234/240_/345/233/276/346/243/200/347/264/242/350/267/257/344/270/216/346/235/203/351/207/215_v1.0.md +97 -0
- package/docs/eval//345/275/222/344/270/200/345/261/202/347/274/272/347/234/201/347/277/273/345/205/263_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +458 -0
- package/docs/eval//346/200/247/350/203/275/344/270/223/351/241/271_/345/206/267/346/237/245/350/257/242/344/270/216/345/206/205/345/255/230_v1.0.md +218 -0
- package/docs/eval//346/225/205/351/232/234/346/263/250/345/205/245/345/256/236/346/265/213_v1.0.md +10 -0
- package/docs/eval//347/274/226/347/240/201/351/235/242/345/211/215/347/275/256_/345/205/245/345/217/243/350/207/252/344/277/235/350/257/201UTF8/344/270/216/346/226/207/346/234/254open/345/256/210/345/215/253_v1.0.md +646 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v20.md +283 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v21.md +224 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v22.md +223 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v23.md +293 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v24.md +303 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v25.md +230 -0
- package/docs/hive//346/243/200/347/264/242/347/256/227/346/263/225/345/217/243/345/276/204/345/257/271/347/205/247_v0.1.md +136 -11
- package/docs/hive//346/243/200/347/264/242/350/267/257/345/276/204/344/270/216/350/256/244/347/237/245/347/273/223/346/236/204/345/245/221/347/272/246_v0.1.md +52 -2
- package/docs/hive//350/234/202/345/267/242M6_ingest/345/256/236/346/226/275/350/256/241/345/210/222_v0.1.md +1 -1
- package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +100 -0
- package/docs/mdcg//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +40 -40
- package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +57 -9
- 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 +7 -2
- package/docs/mdcg//347/235/241/347/234/240/345/221/250/346/234/237_/350/277/220/347/273/264/345/211/215/346/217/220/344/270/216/347/273/264/346/212/244/346/214/207/345/215/227_v1.0.md +183 -0
- package/docs/mdcg//350/256/244/347/237/245/345/233/276_/347/264/242/345/274/225/344/270/216/345/267/245/347/250/213/350/247/204/350/214/203/345/214/226_/350/256/241/345/210/222_v0.1.md +39 -3
- package/docs/plans//345/205/250/344/270/255/346/226/207/347/274/226/347/240/201/344/270/216/350/234/202/345/267/242/344/273/273/345/212/241/346/240/207/350/257/206/345/245/221/347/272/246_v2.0.md +220 -0
- package/docs/plans//347/234/237/346/272/220/347/264/242/345/274/225_/351/200/232/347/224/250/346/234/272/345/210/266_v0.3.md +227 -0
- package/docs/plans//347/235/241/347/234/240/344/270/216/350/207/252/350/277/255/344/273/243_/345/212/237/350/203/275/344/274/230/345/214/226/350/256/276/350/256/241_v0.4.md +547 -0
- package/docs/plans//350/234/202/345/267/242/346/250/241/345/236/213/345/257/206/351/222/245/351/205/215/347/275/256/351/235/242_v1.0.md +385 -0
- package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +460 -460
- package/lib/bridge.js +24 -2
- package/lib/hooks.d.ts +31 -2
- package/lib/hooks.js +219 -16
- package/lib/init.js +16 -1
- package/lib/lib/prompt_safety.d.ts +52 -1
- package/lib/lib/prompt_safety.js +76 -1
- package/md_cg/audit.py +652 -379
- package/md_cg/bench6_arms.py +1 -1
- package/md_cg/bench_e2e_locomo_qa.py +11 -4
- package/md_cg/bench_p0.py +1 -1
- package/md_cg/branches.py +2 -2
- package/md_cg/ccgc.py +1071 -1006
- package/md_cg/chain.py +48 -1
- package/md_cg/consistency.py +97 -9
- package/md_cg/consolidate.py +34 -9
- package/md_cg/crypto.py +59 -26
- package/md_cg/docindex.py +16 -1
- package/md_cg/evolution.py +17 -1
- package/md_cg/export.py +1 -1
- package/md_cg/forgetting.py +309 -13
- package/md_cg/freshness.py +511 -0
- package/md_cg/fsutil.py +454 -8
- package/md_cg/generation.py +409 -0
- package/md_cg/hotcache.py +4 -1
- package/md_cg/identity.py +3 -3
- package/md_cg/insight.py +24 -3
- package/md_cg/linkref.py +1 -1
- package/md_cg/logref.py +327 -0
- package/md_cg/mcp_server.py +4261 -3818
- package/md_cg/mdcg.py +4695 -3547
- package/md_cg/mdcos.py +4672 -4159
- package/md_cg/mreview/pipeline.py +15 -1
- package/md_cg/nodefile.py +713 -576
- package/md_cg/protect.py +158 -11
- package/md_cg/protocol.py +41 -2
- package/md_cg/provenance.py +22 -4
- package/md_cg/reach.py +4 -4
- package/md_cg/readcache.py +76 -15
- package/md_cg/reconcile.py +1 -1
- package/md_cg/refindex.py +246 -37
- package/md_cg/refine.py +1 -1
- package/md_cg/routing.py +32 -5
- package/md_cg/run_tests.py +17 -0
- package/md_cg/scrub.py +30 -3
- package/md_cg/security.py +47 -1
- package/md_cg/self_state.py +5 -5
- package/md_cg/semantic/canonical.py +22 -0
- package/md_cg/semantic/unify.py +73 -22
- package/md_cg/semantic/unify_fixture.json +25 -0
- package/md_cg/sleep.py +1297 -0
- package/md_cg/sources.py +23 -6
- package/md_cg/srcindex.py +352 -0
- package/md_cg/stg.py +47 -3
- package/md_cg/subgraph.py +2 -2
- package/md_cg/sustain.py +1436 -1168
- package/md_cg/tasks.py +469 -470
- package/md_cg/test_auto_defaults.py +424 -0
- package/md_cg/test_b1_auto_id_multiproc.py +277 -0
- package/md_cg/test_b1b2_write_face.py +417 -0
- package/md_cg/test_b2_sensitivity_landing.py +223 -0
- package/md_cg/test_b3_merge_keeps_content.py +576 -0
- package/md_cg/test_b4_shard_dir_selfheal.py +843 -0
- package/md_cg/test_b4_shard_dir_selfheal_guard.py +238 -0
- package/md_cg/test_boundary_hit.py +410 -0
- package/md_cg/test_c3_transient_read_negative.py +793 -0
- package/md_cg/test_c8_search_rrf_gates.py +502 -0
- package/md_cg/test_ccg_form_parity.py +188 -0
- package/md_cg/test_ccgc.py +28 -3
- package/md_cg/test_en_pipeline.py +22 -13
- package/md_cg/test_generation_guard.py +352 -0
- package/md_cg/test_govern_directread.py +17 -4
- package/md_cg/test_h2_session_view_norm.py +233 -0
- package/md_cg/test_h4_sustain_snapshot.py +1387 -0
- package/md_cg/test_hive_ingest.py +285 -285
- package/md_cg/test_issue39_utf8_stdio.py +307 -16
- package/md_cg/test_issue43_default_policy.py +865 -0
- package/md_cg/test_legacy_p3_node_id_type.py +375 -0
- package/md_cg/test_linkref.py +10 -3
- package/md_cg/test_lock.py +2 -2
- package/md_cg/test_logref.py +1109 -0
- package/md_cg/test_m3_h9_bucket_health_protect_mark.py +301 -0
- package/md_cg/test_m3_h9_semantic_guard.py +525 -0
- package/md_cg/test_mr_m2.py +15 -3
- package/md_cg/test_n130_verify_falsified_protect.py +1 -1
- package/md_cg/test_n139_dek_provision_failclosed.py +185 -0
- package/md_cg/test_n176_link_trust.py +221 -0
- package/md_cg/test_n178_units_jobid_gate.py +212 -0
- package/md_cg/test_n184_keys_concurrent_provision.py +307 -0
- package/md_cg/test_n195_writepath_reload.py +283 -0
- package/md_cg/test_n196_stale_gate_skip.py +169 -0
- package/md_cg/test_n197_n208_write_face_gates.py +471 -0
- package/md_cg/test_n198_tokens_corrupt_failclosed.py +225 -0
- package/md_cg/test_n199_tokens_concurrent_write.py +378 -0
- package/md_cg/test_n201_proposal_visibility.py +382 -0
- package/md_cg/test_n202_session_notes_visibility.py +461 -0
- package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +542 -0
- package/md_cg/test_n206_stdio_jsonrpc_type.py +403 -0
- package/md_cg/test_n209_verify_write_face_gates.py +461 -0
- package/md_cg/test_n212_n213_n224_generation_gates.py +519 -0
- package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +563 -0
- package/md_cg/test_n225_nonobject_load.py +1110 -0
- package/md_cg/test_n230_dirty_replay.py +375 -0
- package/md_cg/test_n62_tenant_bind_failclosed.py +200 -0
- package/md_cg/test_neg_condition_hits.py +333 -0
- package/md_cg/test_neg_tail_honesty.py +663 -0
- package/md_cg/test_none_id_write_guard.py +4 -3
- package/md_cg/test_opt_batch1_md_cg.py +451 -0
- package/md_cg/test_p1.py +5 -5
- package/md_cg/test_p11_consistency.py +161 -12
- package/md_cg/test_p26_refindex.py +2 -2
- package/md_cg/test_p27_docindex.py +777 -774
- package/md_cg/test_p28_refcheck.py +740 -13
- package/md_cg/test_p29_session_ingest_export.py +2 -2
- package/md_cg/test_p2_mcp.py +12 -1
- package/md_cg/test_p2_six_elements.py +463 -0
- package/md_cg/test_p30_maintain.py +33 -2
- package/md_cg/test_p31_insight.py +1 -0
- package/md_cg/test_p3_legacy_closure.py +433 -0
- package/md_cg/test_p4_freshness.py +680 -0
- package/md_cg/test_p8_subgraph_chain.py +14 -1
- package/md_cg/test_p9c_dedup_hints.py +276 -0
- package/md_cg/test_policy_required_ccg.py +426 -0
- package/md_cg/test_protocol.py +22 -0
- package/md_cg/test_rank_parity_score_mode.py +522 -0
- package/md_cg/test_read_face_input_gates.py +363 -0
- package/md_cg/test_read_face_semantics.py +489 -0
- package/md_cg/test_recall_face_guards.py +812 -0
- package/md_cg/test_rejected_credential_forms.py +188 -0
- package/md_cg/test_rejected_redact.py +146 -0
- package/md_cg/test_retr_s1b.py +2 -2
- package/md_cg/test_retr_s5.py +20 -7
- package/md_cg/test_review_conformance.py +21 -3
- package/md_cg/test_security_audit_v21.py +2 -2
- package/md_cg/test_semantic_canonical.py +5 -3
- package/md_cg/test_server_version.py +67 -0
- package/md_cg/test_sleep.py +611 -0
- package/md_cg/test_sleep_p1.py +784 -0
- package/md_cg/test_srcindex.py +171 -0
- package/md_cg/test_tenant_env_override_warn.py +63 -50
- package/md_cg/test_time_core_lint.py +968 -0
- package/md_cg/test_token_lowercase_form.py +315 -0
- package/md_cg/test_unify_default_off.py +701 -0
- package/md_cg/test_unify_scope.py +182 -0
- package/md_cg/test_v21r1_package_version.py +310 -0
- package/md_cg/test_writepipe.py +10 -4
- package/md_cg/tokens.py +225 -95
- package/md_cg/tool_face.py +4 -4
- package/md_cg/trust.py +49 -5
- package/md_cg/units.py +204 -6
- package/md_cg/weights.py +4 -4
- package/md_cg/whitebox_kb/aeis_core/time_core.py +8 -0
- package/md_cg/whitebox_kb/wisdom/knowledge_points.py +1 -1
- package/md_cg/writelimit.py +42 -8
- package/md_cg/writepipe.py +651 -554
- package/package.json +4 -2
- package/skills/plugin.json +1 -1
- package/src/bridge.ts +25 -2
- package/src/hooks.ts +229 -16
- package/src/init.ts +15 -1
- package/src/lib/prompt_safety.ts +88 -1
- package/utf8_boot.py +237 -0
- package/zcode/AGENTS.md +10 -10
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""负记忆里**凭据形态**的覆盖守卫(PR#44 复核补,2026-09-28)。
|
|
3
|
+
|
|
4
|
+
运行:python -X utf8 -m md_cg.test_rejected_credential_forms
|
|
5
|
+
|
|
6
|
+
为什么另立一套(与 `test_rejected_redact.py` 的分工):那一套守的是
|
|
7
|
+
「命中禁表 → 原文不落盘」的**主路径**;本套守的是 PR#44 复核时实测出来的
|
|
8
|
+
三个**未覆盖形态**——主路径绿了、凭据照样能落盘:
|
|
9
|
+
|
|
10
|
+
F1 全形态令牌 `mdcg1.<role>.<id>.<secret>`:只有 id 段命中禁表,负记忆脱敏
|
|
11
|
+
按命中片段替换 → 掩掉 id 却把 **role 与密钥段**原样写进 rejected。
|
|
12
|
+
令牌的可用性靠 role+id+secret 三段凑齐(`tokens.parse_token`),而 id 早已
|
|
13
|
+
可公开查得(仓库/npm 曾发出),故密钥段落盘等于凭据可被拼回。
|
|
14
|
+
F2 脱敏必须是**跨度合并**而非逐条串行 `re.sub`:串行时窄规则先把 id 换成
|
|
15
|
+
占位符,宽规则再也匹配不上那段文字,密钥段漏掩(实测反例即 F1 的成因)。
|
|
16
|
+
F3 `tags`:正文命中禁表而 tags 夹带凭据时,tags 原先原样进负记忆。
|
|
17
|
+
F4 防误伤:泛形写法 `mdcg1.<role>.<id>.<secret>`(文档用)不得被拒。
|
|
18
|
+
F5 跨闸口径:写入闸门禁表与发布门禁 `check_publish_artifact.R1_CONTENT_RULES`
|
|
19
|
+
对同一合成令牌必须同判——两闸分叉正是本缺陷的根因。
|
|
20
|
+
F6 判别力自证:无凭据的正文经脱敏后与原文逐字相同。
|
|
21
|
+
|
|
22
|
+
实验纪律:临时目录 + 临时 policy(其 forbidden 拷贝**真源** data/policy.json,
|
|
23
|
+
故本套同时钉住真源不丢这两条令牌口径);哑值 `tk_0123456789abcdef` 与
|
|
24
|
+
`SYNTHFRAG...` 均为合成串,**非真凭据**。
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import io
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
import re
|
|
32
|
+
import shutil
|
|
33
|
+
import sys
|
|
34
|
+
import tempfile
|
|
35
|
+
import traceback
|
|
36
|
+
|
|
37
|
+
from . import audit, writepipe
|
|
38
|
+
from .mdcos import MdCGSecure
|
|
39
|
+
from .security import Principal
|
|
40
|
+
|
|
41
|
+
ID = "tk_0123456789abcdef" # 哑令牌 id(与禁表同形)
|
|
42
|
+
FRAG = "SYNTHFRAG_do_not_use_0123456789" # 合成密钥段,非真凭据
|
|
43
|
+
# 变量名刻意不叫 FRAG_*:发布门禁 APIKEY_LITERAL 规则会命中
|
|
44
|
+
# `secret = "<16+ 字符>"` 这种**形似密钥赋值**的行(本条即被它抓到过一次)。
|
|
45
|
+
FULL = "mdcg1.designer." + ID + "." + FRAG # 全形态令牌(合成)
|
|
46
|
+
GENERIC = "mdcg1.<role>.<id>.<secret>" # 泛形写法(文档用)
|
|
47
|
+
|
|
48
|
+
_ok = 0
|
|
49
|
+
_bad = []
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def ok(name, cond, detail=""):
|
|
53
|
+
global _ok
|
|
54
|
+
if cond:
|
|
55
|
+
_ok += 1
|
|
56
|
+
print(" ok %s" % name)
|
|
57
|
+
else:
|
|
58
|
+
_bad.append("%s %s" % (name, detail))
|
|
59
|
+
print(" FAIL %s %s" % (name, detail))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _repo_root() -> str:
|
|
63
|
+
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _policy() -> dict:
|
|
67
|
+
with io.open(os.path.join(_repo_root(), "data", "policy.json"), encoding="utf-8") as fh:
|
|
68
|
+
return json.load(fh)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _files_with(root, needle):
|
|
72
|
+
hits = []
|
|
73
|
+
for dp, _dn, fs in os.walk(root):
|
|
74
|
+
for f in fs:
|
|
75
|
+
fp = os.path.join(dp, f)
|
|
76
|
+
try:
|
|
77
|
+
with io.open(fp, encoding="utf-8", errors="ignore") as fh:
|
|
78
|
+
if needle in fh.read():
|
|
79
|
+
hits.append(os.path.relpath(fp, root))
|
|
80
|
+
except OSError:
|
|
81
|
+
continue
|
|
82
|
+
return hits
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _mk_cg(tmp, name):
|
|
86
|
+
p = Principal(tenant="default", actor="t_forms", role="designer",
|
|
87
|
+
can_write=True, can_admin=True)
|
|
88
|
+
return MdCGSecure(os.path.join(tmp, name), principal=p)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _masked_regions(text):
|
|
92
|
+
"""把占位符折叠成统一记号——用于比较「哪些字被掩」,与规则序号无关。"""
|
|
93
|
+
return re.sub(r"\[已过滤:禁表#\d+\]", "<MASK>", text)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def main() -> int:
|
|
97
|
+
tmp = tempfile.mkdtemp(prefix="cred_forms_")
|
|
98
|
+
old_policy = os.environ.get("MDCG_POLICY_FILE")
|
|
99
|
+
try:
|
|
100
|
+
_run(tmp)
|
|
101
|
+
except Exception:
|
|
102
|
+
traceback.print_exc()
|
|
103
|
+
_bad.append("未捕获异常")
|
|
104
|
+
finally:
|
|
105
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
106
|
+
if old_policy is None:
|
|
107
|
+
os.environ.pop("MDCG_POLICY_FILE", None)
|
|
108
|
+
else:
|
|
109
|
+
os.environ["MDCG_POLICY_FILE"] = old_policy
|
|
110
|
+
print("\n凭据形态覆盖守卫:%d 通过%s" % (
|
|
111
|
+
_ok, (",%d 失败:%s" % (len(_bad), "; ".join(_bad))) if _bad else ""))
|
|
112
|
+
return 1 if _bad else 0
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _run(tmp):
|
|
116
|
+
src = _policy()
|
|
117
|
+
policy = os.path.join(tmp, "policy.json")
|
|
118
|
+
# 只搬 forbidden:保持与真源同口径(required 用不着,本套不走缺要素路径)
|
|
119
|
+
with io.open(policy, "w", encoding="utf-8") as f:
|
|
120
|
+
json.dump({"forbidden": src["forbidden"], "required": ["PASSED"]}, f)
|
|
121
|
+
os.environ["MDCG_POLICY_FILE"] = policy
|
|
122
|
+
rules = {"forbidden": src["forbidden"]}
|
|
123
|
+
pipe = writepipe.install_default_gates(writepipe.WritePipeline())
|
|
124
|
+
|
|
125
|
+
print("== F5 跨闸口径:写入闸门与发布门禁对同一合成令牌同判 ==")
|
|
126
|
+
import importlib.util
|
|
127
|
+
_sp = os.path.join(_repo_root(), "scripts", "check_publish_artifact.py")
|
|
128
|
+
spec = importlib.util.spec_from_file_location("_cpa_forms", _sp)
|
|
129
|
+
cpa = importlib.util.module_from_spec(spec)
|
|
130
|
+
spec.loader.exec_module(cpa)
|
|
131
|
+
pub = [rx for _n, rx, *_ in [tuple(x) + (None,) if len(x) == 2 else x
|
|
132
|
+
for x in cpa.R1_CONTENT_RULES]
|
|
133
|
+
if hasattr(rx, "search") and rx.search(FULL)]
|
|
134
|
+
write_hit = [p for p in src["forbidden"] if re.search(p, FULL)]
|
|
135
|
+
ok(bool(pub) and bool(write_hit),
|
|
136
|
+
"F5 两闸同判命中(发布 %d 条 / 写入 %d 条)" % (len(pub), len(write_hit)), repr((pub, write_hit)))
|
|
137
|
+
|
|
138
|
+
print("== F1 全形态令牌经写入闸门 → 凭据不落盘 ==")
|
|
139
|
+
cg = _mk_cg(tmp, "root_f1")
|
|
140
|
+
out = pipe.execute(cg, {"content_kind": "text", "layer": "knowledge",
|
|
141
|
+
"content": "PASSED 用 %s 调发布接口" % FULL})
|
|
142
|
+
ok("F1 命中 → rejected", out.get("moved_to") == "rejected"
|
|
143
|
+
and out.get("committed") is False, repr(out.get("verdict", {}).get("state")))
|
|
144
|
+
ok("F1a 库内无密钥段明文", not _files_with(cg.root, FRAG),
|
|
145
|
+
repr(_files_with(cg.root, FRAG)))
|
|
146
|
+
ok("F1b 库内无 id 明文", not _files_with(cg.root, ID),
|
|
147
|
+
repr(_files_with(cg.root, ID)))
|
|
148
|
+
rid = out.get("id")
|
|
149
|
+
body = ""
|
|
150
|
+
if rid in cg.index["nodes"]:
|
|
151
|
+
_fm, body = cg._read(cg.index["nodes"][rid])
|
|
152
|
+
ok("F1c 负记忆整段被掩", "[已过滤:禁表#" in (body or "") and FRAG not in (body or ""),
|
|
153
|
+
repr((body or "")[:120]))
|
|
154
|
+
|
|
155
|
+
print("== F2 跨度合并:与规则顺序无关、密钥段零残留 ==")
|
|
156
|
+
one = audit.redact_forbidden("PASSED 用 %s 调接口" % FULL, rules)
|
|
157
|
+
ok("F2 密钥段与 id 均被掩", FRAG not in one and ID not in one, repr(one))
|
|
158
|
+
flip = {"forbidden": list(reversed(src["forbidden"]))}
|
|
159
|
+
two = audit.redact_forbidden("PASSED 用 %s 调接口" % FULL, flip)
|
|
160
|
+
ok("F2a 规则顺序颠倒 → 掩码区域相同", _masked_regions(one) == _masked_regions(two),
|
|
161
|
+
repr((_masked_regions(one), _masked_regions(two))))
|
|
162
|
+
|
|
163
|
+
print("== F3 tags 夹带凭据 → 负记忆里的 tags 同样被掩 ==")
|
|
164
|
+
cg3 = _mk_cg(tmp, "root_f3")
|
|
165
|
+
out3 = pipe.execute(cg3, {"content_kind": "text", "layer": "knowledge",
|
|
166
|
+
"content": "PASSED 正文含 %s" % ID, "tags": [FULL, "正常标签"]})
|
|
167
|
+
rid3 = out3.get("id")
|
|
168
|
+
fm3 = {}
|
|
169
|
+
if rid3 in cg3.index["nodes"]:
|
|
170
|
+
fm3, _b3 = cg3._read(cg3.index["nodes"][rid3])
|
|
171
|
+
ok("F3 负记忆 tags 无密钥段", FRAG not in json.dumps(fm3.get("tags"), ensure_ascii=False),
|
|
172
|
+
repr(fm3.get("tags")))
|
|
173
|
+
ok("F3a 库内任何文件无密钥段明文", not _files_with(cg3.root, FRAG),
|
|
174
|
+
repr(_files_with(cg3.root, FRAG)))
|
|
175
|
+
|
|
176
|
+
print("== F4 防误伤:泛形写法不被拒、正常正文不受影响 ==")
|
|
177
|
+
cg4 = _mk_cg(tmp, "root_f4")
|
|
178
|
+
out4 = pipe.execute(cg4, {"content_kind": "text", "layer": "knowledge",
|
|
179
|
+
"content": "PASSED 令牌形如 %s ,照此书写" % GENERIC})
|
|
180
|
+
ok("F4 泛形写法过闸(不被拒)", out4.get("moved_to") != "rejected",
|
|
181
|
+
repr(out4.get("moved_to")))
|
|
182
|
+
text = "PASSED 一条与凭据无关的正文,含 mdcg1 一词与 tk_zzzz 哑值"
|
|
183
|
+
ok("F6 无凭据正文脱敏后逐字不变", audit.redact_forbidden(text, rules) == text,
|
|
184
|
+
repr(audit.redact_forbidden(text, rules)))
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
if __name__ == "__main__":
|
|
188
|
+
sys.exit(main())
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""负记忆脱敏守卫(issue #43):命中禁表被 REJECT 的写入,凭据原文不得随 rejected 落盘。
|
|
3
|
+
|
|
4
|
+
背景:writepipe._gate_audit 的 REJECT 分支把正文前 200 字交给 cg.add_rejected,
|
|
5
|
+
命中 forbidden(凭据形态)的内容虽被拦下、committed=false,原文却写进
|
|
6
|
+
`rejected/rej_*.md`,按返回的 id 可原样读回——拦截之后又把凭据存了一遍。
|
|
7
|
+
|
|
8
|
+
断言:
|
|
9
|
+
R1 命中禁表 → moved_to=rejected;库目录下**任何文件**都不含哑值明文;
|
|
10
|
+
rejected 节点正文带占位符「[已过滤:禁表#」;evidence 仍给出命中模式(可审计)
|
|
11
|
+
R2 凭据跨 200 字截断边界 → 残片(哑值前 8 字符)同样不落盘(先脱敏后截断)
|
|
12
|
+
R3 缺必需要素的 REJECT(可修正,无禁表命中)→ 负记忆保留原文(行为不变)
|
|
13
|
+
R4 redact_forbidden 单元:多处命中全替换、非法正则跳过不抛错、无规则原样返回
|
|
14
|
+
|
|
15
|
+
红基线:在改动前的 main 上跑本文件,R1a/R1b/R2 为红(其余两态皆绿)。
|
|
16
|
+
实验纪律:全程临时目录 + 临时 policy(MDCG_POLICY_FILE 只指向临时文件);
|
|
17
|
+
哑值 `tk_0123456789abcdef` 仅匹配禁表形态,**非真凭据**。
|
|
18
|
+
运行:python -X utf8 -m md_cg.test_rejected_redact(退出码 0 = 全绿)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import shutil
|
|
24
|
+
import sys
|
|
25
|
+
import tempfile
|
|
26
|
+
import traceback
|
|
27
|
+
|
|
28
|
+
from . import audit, writepipe
|
|
29
|
+
from .mdcos import MdCGSecure
|
|
30
|
+
from .security import Principal
|
|
31
|
+
|
|
32
|
+
FAKE = "tk_0123456789abcdef" # 哑值,非真凭据
|
|
33
|
+
TK = r"\btk_[0-9a-f]{8,}\b" # 与 data/policy.json 批次71新增模式同形
|
|
34
|
+
|
|
35
|
+
_ok = 0
|
|
36
|
+
_bad = []
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _check(name, cond, detail=""):
|
|
40
|
+
global _ok
|
|
41
|
+
if cond:
|
|
42
|
+
_ok += 1
|
|
43
|
+
print(" ok %s" % name)
|
|
44
|
+
else:
|
|
45
|
+
_bad.append("%s %s" % (name, detail))
|
|
46
|
+
print(" FAIL %s %s" % (name, detail))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _files_containing(root, needle):
|
|
50
|
+
hits = []
|
|
51
|
+
for dp, _dn, fs in os.walk(root):
|
|
52
|
+
for f in fs:
|
|
53
|
+
fp = os.path.join(dp, f)
|
|
54
|
+
try:
|
|
55
|
+
with open(fp, encoding="utf-8", errors="ignore") as fh:
|
|
56
|
+
if needle in fh.read():
|
|
57
|
+
hits.append(os.path.relpath(fp, root))
|
|
58
|
+
except OSError:
|
|
59
|
+
continue
|
|
60
|
+
return hits
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _mk_cg(tmp, name):
|
|
64
|
+
p = Principal(tenant="default", actor="t_redact", role="designer",
|
|
65
|
+
can_write=True, can_admin=True)
|
|
66
|
+
return MdCGSecure(os.path.join(tmp, name), principal=p)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _rejected_text(cg, rid):
|
|
70
|
+
fm, content = cg._read(cg.index["nodes"][rid])
|
|
71
|
+
return content or ""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def main():
|
|
75
|
+
tmp = tempfile.mkdtemp(prefix="rejected_redact_")
|
|
76
|
+
old_policy = os.environ.pop("MDCG_POLICY_FILE", None)
|
|
77
|
+
try:
|
|
78
|
+
_run(tmp)
|
|
79
|
+
except Exception:
|
|
80
|
+
traceback.print_exc()
|
|
81
|
+
_bad.append("未捕获异常")
|
|
82
|
+
finally:
|
|
83
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
84
|
+
if old_policy is not None:
|
|
85
|
+
os.environ["MDCG_POLICY_FILE"] = old_policy
|
|
86
|
+
else:
|
|
87
|
+
os.environ.pop("MDCG_POLICY_FILE", None)
|
|
88
|
+
print("\n负记忆脱敏守卫:%d 通过%s" % (
|
|
89
|
+
_ok, (",%d 失败:%s" % (len(_bad), "; ".join(_bad))) if _bad else ""))
|
|
90
|
+
return 1 if _bad else 0
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _run(tmp):
|
|
94
|
+
policy = os.path.join(tmp, "policy.json")
|
|
95
|
+
with open(policy, "w", encoding="utf-8") as f:
|
|
96
|
+
json.dump({"forbidden": [TK], "required": ["PASSED"]}, f)
|
|
97
|
+
os.environ["MDCG_POLICY_FILE"] = policy
|
|
98
|
+
pipe = writepipe.install_default_gates(writepipe.WritePipeline())
|
|
99
|
+
|
|
100
|
+
# ---------- R1 命中禁表:凭据不落盘 ----------
|
|
101
|
+
cg = _mk_cg(tmp, "root_r1")
|
|
102
|
+
out = pipe.execute(cg, {"content_kind": "text", "layer": "knowledge",
|
|
103
|
+
"content": "PASSED 用令牌 %s 调发布接口" % FAKE})
|
|
104
|
+
_check("R1 命中禁表 → rejected", out.get("moved_to") == "rejected"
|
|
105
|
+
and out.get("committed") is False, repr(out))
|
|
106
|
+
hits = _files_containing(cg.root, FAKE)
|
|
107
|
+
_check("R1a 库目录无哑值明文", not hits, repr(hits))
|
|
108
|
+
rid = out.get("id")
|
|
109
|
+
body = _rejected_text(cg, rid) if rid in cg.index["nodes"] else ""
|
|
110
|
+
_check("R1b rejected 正文带占位符", "[已过滤:禁表#1]" in body, repr(body))
|
|
111
|
+
ev = (out.get("verdict") or {}).get("evidence") or ""
|
|
112
|
+
_check("R1c evidence 仍给出命中模式", TK in ev and FAKE not in ev, repr(ev))
|
|
113
|
+
|
|
114
|
+
# ---------- R2 跨 200 字截断边界 ----------
|
|
115
|
+
cg2 = _mk_cg(tmp, "root_r2")
|
|
116
|
+
# 哑值起点在第 190 字:截前 200 字剩「tk_0123456」(仅 7 位 hex,已不匹配
|
|
117
|
+
# 禁表模式)——若先截断后脱敏,这段残片会漏过脱敏直接落盘。
|
|
118
|
+
content = "PASSED " + "x" * 182 + " " + FAKE
|
|
119
|
+
out = pipe.execute(cg2, {"content_kind": "text", "layer": "knowledge",
|
|
120
|
+
"content": content})
|
|
121
|
+
frag = FAKE[:8]
|
|
122
|
+
hits = _files_containing(cg2.root, frag)
|
|
123
|
+
_check("R2 截断边界残片不落盘", out.get("moved_to") == "rejected" and not hits,
|
|
124
|
+
repr((out.get("moved_to"), hits)))
|
|
125
|
+
|
|
126
|
+
# ---------- R3 缺必需要素:原文保留(行为不变) ----------
|
|
127
|
+
cg3 = _mk_cg(tmp, "root_r3")
|
|
128
|
+
out = pipe.execute(cg3, {"content_kind": "text", "layer": "knowledge",
|
|
129
|
+
"content": "缺要素的正文,补齐后可重写"})
|
|
130
|
+
rid = out.get("id")
|
|
131
|
+
body = _rejected_text(cg3, rid) if rid in cg3.index["nodes"] else ""
|
|
132
|
+
_check("R3 缺要素负记忆保留原文", out.get("moved_to") == "rejected"
|
|
133
|
+
and "缺要素的正文,补齐后可重写" in body, repr((out.get("moved_to"), body)))
|
|
134
|
+
|
|
135
|
+
# ---------- R4 redact_forbidden 单元 ----------
|
|
136
|
+
rules = {"forbidden": ["(unclosed", TK, "", r"\bsk-[A-Za-z0-9]{20,}"]}
|
|
137
|
+
s = "a %s b %s c sk-%s" % (FAKE, FAKE, "A" * 24)
|
|
138
|
+
r = audit.redact_forbidden(s, rules)
|
|
139
|
+
_check("R4a 多处命中全替换", FAKE not in r and "sk-AAAA" not in r
|
|
140
|
+
and r.count("[已过滤:禁表#2]") == 2 and "[已过滤:禁表#3]" in r, repr(r))
|
|
141
|
+
_check("R4b 无规则原样返回", audit.redact_forbidden(s, {}) == s)
|
|
142
|
+
_check("R4c 非法正则跳过不抛错", "[已过滤:禁表#1]" not in r)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
if __name__ == "__main__":
|
|
146
|
+
sys.exit(main())
|
package/md_cg/test_retr_s1b.py
CHANGED
|
@@ -83,7 +83,7 @@ def main():
|
|
|
83
83
|
|
|
84
84
|
root = tempfile.mkdtemp(prefix="retr_s1b_")
|
|
85
85
|
cg = _build(root)
|
|
86
|
-
b_sense = [e.get("bucket") for e in cg.index["nodes"].values()
|
|
86
|
+
b_sense = [e.get("bucket") for e in list(cg.index["nodes"].values())
|
|
87
87
|
if e.get("path", "").endswith("s1.md")][0]
|
|
88
88
|
check("写入侧确实落了桶", bool(b_sense) and b_sense != "orphan", str(b_sense))
|
|
89
89
|
|
|
@@ -142,7 +142,7 @@ def main():
|
|
|
142
142
|
cgi.add("a1", "感知系统 记忆", "knowledge", tags=["domain:感知系统"])
|
|
143
143
|
cgi.add("b1", "图数据库 邻接", "knowledge", tags=["domain:图数据库"])
|
|
144
144
|
cgi.flush()
|
|
145
|
-
bi = [e.get("bucket") for e in cgi.index["nodes"].values()
|
|
145
|
+
bi = [e.get("bucket") for e in list(cgi.index["nodes"].values())
|
|
146
146
|
if e.get("path", "").endswith("a1.md")][0]
|
|
147
147
|
# 对照组必须用**同一个 min_results**(否则比的是两次不同检索,不是回退等价)
|
|
148
148
|
_setenv(MDCG_GATE_S1B_BUCKET=None)
|
package/md_cg/test_retr_s5.py
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
- 相邻抑制:与 rejected 节点有边相连(文本不重叠)的正候选同样被抑制
|
|
10
10
|
- 无关候选不被抑制;λ 可配(0=不抑制,1=压到 0 且不低于 0)
|
|
11
11
|
- 阈值真的在过滤:部分重叠候选在 thr=0.6 下不抑制、thr=0.4 下被抑制
|
|
12
|
-
-
|
|
12
|
+
- 负覆盖条目不参与分数竞争:分数恒哨兵 0.0(负性走独立字段),S5 的 λ 只作用于
|
|
13
|
+
正候选——H10① 2026-09-30(改前该条目被赋 1.0,高过任何真实候选)
|
|
13
14
|
- 无负记忆命中时不落 s5 审计;幂等(重复检索 / 索引重建)
|
|
14
15
|
"""
|
|
15
16
|
import os
|
|
@@ -61,6 +62,12 @@ def _scores(results):
|
|
|
61
62
|
return {r[0]["id"]: round(float(r[1]), 6) for r in results}
|
|
62
63
|
|
|
63
64
|
|
|
65
|
+
def _neg_card(results):
|
|
66
|
+
"""结果里的负覆盖条目卡(判据:独立字段 negative_coverage)。"""
|
|
67
|
+
return next((r[0] for r in results
|
|
68
|
+
if r[0].get("negative_coverage")), None)
|
|
69
|
+
|
|
70
|
+
|
|
64
71
|
def _build(root):
|
|
65
72
|
cg = MdCG(root)
|
|
66
73
|
# 三个正候选都含查询词(都会成为 T2 命中),差别只在「与负记忆的关系」:
|
|
@@ -90,8 +97,13 @@ def main():
|
|
|
90
97
|
base = _scores(r_off)
|
|
91
98
|
check("默认关:meta 不含 gates 键", "gates" not in m_off,
|
|
92
99
|
str(sorted(m_off.keys())))
|
|
93
|
-
check("
|
|
94
|
-
|
|
100
|
+
check("默认关:负覆盖条目分数=哨兵 0.0(不再冒充 1.0 候选,且不高过任何"
|
|
101
|
+
"正候选;负性走独立字段 negative_coverage)",
|
|
102
|
+
base.get("rejected/h_rej.md") == 0.0
|
|
103
|
+
and (_neg_card(r_off) or {}).get("negative_coverage") is True
|
|
104
|
+
and max(v for k, v in base.items()
|
|
105
|
+
if k != "rejected/h_rej.md") >= base["rejected/h_rej.md"],
|
|
106
|
+
str(base))
|
|
95
107
|
|
|
96
108
|
# ---- 2) 子开关未设(总开关开、S1/S2 显式关)→ S5 不生效 ----
|
|
97
109
|
_setenv(MDCG_RETRIEVAL_PIPELINE="1", MDCG_GATE_S5_NEG=None,
|
|
@@ -122,8 +134,9 @@ def main():
|
|
|
122
134
|
sc5.get("pos_clean") == base.get("pos_clean")
|
|
123
135
|
and "pos_clean" not in _sup,
|
|
124
136
|
"%s vs %s sup=%s" % (sc5.get("pos_clean"), base.get("pos_clean"), sorted(_sup)))
|
|
125
|
-
check("
|
|
126
|
-
sc5.get("rejected/h_rej.md") == 0.
|
|
137
|
+
check("负覆盖条目不再参与分数竞争(S5 只降正候选;条目恒哨兵 0.0)",
|
|
138
|
+
sc5.get("rejected/h_rej.md") == 0.0,
|
|
139
|
+
str(sc5.get("rejected/h_rej.md")))
|
|
127
140
|
|
|
128
141
|
# ---- 4) λ 可配:0=不抑制;1=压到 0 且 ≥0 ----
|
|
129
142
|
_setenv(MDCG_NEG_LAMBDA="0")
|
|
@@ -159,8 +172,8 @@ def main():
|
|
|
159
172
|
r4, m4 = cg.search(QUERY, k=10, judge=False, record=False)
|
|
160
173
|
sc4 = _scores(r4)
|
|
161
174
|
g4 = (m4.get("gates") or {}).get("s5") or {}
|
|
162
|
-
check("λ=0.25
|
|
163
|
-
sc4.get("rejected/h_rej.md") == 0.
|
|
175
|
+
check("λ=0.25:审计记 0.25、负覆盖条目仍非分数(哨兵 0.0,不随 λ 变)",
|
|
176
|
+
sc4.get("rejected/h_rej.md") == 0.0 and g4.get("lambda") == 0.25,
|
|
164
177
|
"%s %s" % (sc4.get("rejected/h_rej.md"), g4))
|
|
165
178
|
|
|
166
179
|
# ---- 6) 无负记忆命中:不落 s5 审计 ----
|
|
@@ -204,7 +204,14 @@ def main():
|
|
|
204
204
|
pids = []
|
|
205
205
|
for p in procs:
|
|
206
206
|
out, _err = p.communicate(timeout=120)
|
|
207
|
-
|
|
207
|
+
try:
|
|
208
|
+
pids.append(json.loads(out.strip()))
|
|
209
|
+
except ValueError:
|
|
210
|
+
# N170 诊断增强:worker 崩溃时 stdout 为空,裸 json.loads 只会
|
|
211
|
+
# 报 JSONDecodeError 盲盒——把 returncode/stderr 带出来定位。
|
|
212
|
+
raise AssertionError(
|
|
213
|
+
"worker propose 输出非 JSON(rc=%s)stderr=%s"
|
|
214
|
+
% (p.returncode, (_err or "")[-500:])) from None
|
|
208
215
|
check("4 进程返回同一 pid", len(set(pids)) == 1, str(pids))
|
|
209
216
|
inbox2 = os.path.join(root2, "hippocampus", "inbox.jsonl")
|
|
210
217
|
with open(inbox2, encoding="utf-8") as f:
|
|
@@ -269,8 +276,19 @@ def main():
|
|
|
269
276
|
outs = []
|
|
270
277
|
for p in procs:
|
|
271
278
|
out, _err = p.communicate(timeout=180)
|
|
279
|
+
if not out.strip():
|
|
280
|
+
# N170 诊断增强:空输出先亮 worker 现场(rc/stderr),不再
|
|
281
|
+
# 留给下方 json.loads 一个盲盒崩溃。
|
|
282
|
+
print(" [DIAG] decide worker rc=%s stderr=%s"
|
|
283
|
+
% (p.returncode, (_err or "")[-500:]))
|
|
272
284
|
outs.append(out.strip())
|
|
273
|
-
|
|
285
|
+
douts = []
|
|
286
|
+
for o in outs:
|
|
287
|
+
try:
|
|
288
|
+
douts.append(json.loads(o))
|
|
289
|
+
except ValueError:
|
|
290
|
+
douts.append({"ok": False})
|
|
291
|
+
check("4 项裁决全部成功", all(d.get("ok") for d in douts),
|
|
274
292
|
str([o[:40] for o in outs]))
|
|
275
293
|
drecs = cg3.decisions()
|
|
276
294
|
check("decisions 读回恰好 4 条(无交错丢失)", len(drecs) == 4,
|
|
@@ -305,7 +323,7 @@ def main():
|
|
|
305
323
|
|
|
306
324
|
def _neg_n():
|
|
307
325
|
"""负记忆条目数(rejected 层)——NOOP 不得增加它。"""
|
|
308
|
-
return sum(1 for e in cg5.index["nodes"].values()
|
|
326
|
+
return sum(1 for e in list(cg5.index["nodes"].values())
|
|
309
327
|
if (e.get("layer") or "") == "rejected")
|
|
310
328
|
|
|
311
329
|
nb = _neg_n()
|
|
@@ -224,11 +224,11 @@ def v21_8_sensitive_family():
|
|
|
224
224
|
"service-account.json"]
|
|
225
225
|
for fn in leaks:
|
|
226
226
|
p = os.path.join(base, fn)
|
|
227
|
-
open(p, "w").close()
|
|
227
|
+
open(p, "w", encoding="utf-8").close()
|
|
228
228
|
check(f" 拒读:{fn}", m._sensitive_read(p) is not None)
|
|
229
229
|
for fn in ("notes.md", "readme.md", "data.json"):
|
|
230
230
|
p = os.path.join(base, fn)
|
|
231
|
-
open(p, "w").close()
|
|
231
|
+
open(p, "w", encoding="utf-8").close()
|
|
232
232
|
check(f" 不误伤:{fn}", m._sensitive_read(p) is None)
|
|
233
233
|
|
|
234
234
|
|
|
@@ -209,7 +209,8 @@ def t_en_unified(tmp):
|
|
|
209
209
|
# 本段只测**语义摘要路**:显式关掉统一归一层(MDCG_UNIFY_QUERY=0),
|
|
210
210
|
# 否则英文 query 会被归一成中文原子、经词法路命中 gold,语义路的增量
|
|
211
211
|
# 与它的负证都会被掩盖(2026-09-24 修复:旧写法把两个开关混在一起,
|
|
212
|
-
# 归一默认转正后该负证恒红)。
|
|
212
|
+
# 归一默认转正后该负证恒红)。2026-09-30 缺省翻为关后本行仍**显式**写 0
|
|
213
|
+
# ——不靠缺省,免得下次动缺省时本段的隔离前提又静默改名。
|
|
213
214
|
os.environ["MDCG_SEMANTIC"] = "1"
|
|
214
215
|
os.environ["MDCG_UNIFY_QUERY"] = "0"
|
|
215
216
|
res, _m = cg.search_rrf("Did you eat horse meat today?", k=5,
|
|
@@ -222,12 +223,13 @@ def t_en_unified(tmp):
|
|
|
222
223
|
paths=("lexical",), judge=False, record=False)
|
|
223
224
|
top1 = res[0][0]["id"] if res else None
|
|
224
225
|
ok(top1 != "gold", "语义路与归一路全关:英文原题不命中 gold(词法零交集)")
|
|
225
|
-
#
|
|
226
|
+
# 归一层**显式开启**(MDCG_UNIFY_QUERY=1)时的口径:同一 query 经归一即命中
|
|
227
|
+
# (缺省自 2026-09-30 起为**关**,故本段必须显式设 1 才有归一路)。
|
|
226
228
|
os.environ["MDCG_UNIFY_QUERY"] = "1"
|
|
227
229
|
res_u, _mu = cg.search_rrf("Did you eat horse meat today?", k=5,
|
|
228
230
|
paths=("lexical",), judge=False, record=False)
|
|
229
231
|
ok(res_u and res_u[0][0]["id"] == "gold",
|
|
230
|
-
"
|
|
232
|
+
"归一层显式开(MDCG_UNIFY_QUERY=1):英文原题归一后命中 gold: %r"
|
|
231
233
|
% [r[0]["id"] for r in res_u[:2]])
|
|
232
234
|
os.environ.pop("MDCG_UNIFY_QUERY", None)
|
|
233
235
|
finally:
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""test_server_version.py · SERVER_VERSION 与包版本联动守卫(issue #42)
|
|
3
|
+
|
|
4
|
+
根因:md_cg/mcp_server.py 与 hive/hive_mcp/mcp_server.py 的 SERVER_VERSION
|
|
5
|
+
硬编码 "0.1.0",从未随发版联动——MCP 握手 serverInfo.version 向客户端自报
|
|
6
|
+
假版本(外部用户实测:包 0.5.1 握手自报 0.1.0)。
|
|
7
|
+
修复:SERVER_VERSION 改为 _package_version() 动态读包根 package.json。
|
|
8
|
+
|
|
9
|
+
守卫断言:
|
|
10
|
+
① md_cg.SERVER_VERSION == package.json 的 version(动态联动)
|
|
11
|
+
② hive_mcp.SERVER_VERSION == 同一 package.json version(同族同源)
|
|
12
|
+
③ 两值 != "0.1.0"(防回退硬编码——当前发布版本必非 0.1.0)
|
|
13
|
+
④ _package_version 对坏路径回落 "0.1.0" 不抛(保底契约)
|
|
14
|
+
"""
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
import unittest.mock as mock
|
|
19
|
+
|
|
20
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
21
|
+
|
|
22
|
+
PASS = 0
|
|
23
|
+
FAIL = 0
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def check(name, cond, detail=""):
|
|
27
|
+
global PASS, FAIL
|
|
28
|
+
if cond:
|
|
29
|
+
PASS += 1
|
|
30
|
+
print(f" [ok] {name}")
|
|
31
|
+
else:
|
|
32
|
+
FAIL += 1
|
|
33
|
+
print(f" [✘] {name} {detail}")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
37
|
+
with open(os.path.join(ROOT, "package.json"), encoding="utf-8") as f:
|
|
38
|
+
PKG_VERSION = json.load(f).get("version")
|
|
39
|
+
|
|
40
|
+
print(f"== 包版本基准: {PKG_VERSION} ==")
|
|
41
|
+
|
|
42
|
+
# ①② 动态联动
|
|
43
|
+
from md_cg import mcp_server as mdcg_server # noqa: E402
|
|
44
|
+
from hive.hive_mcp import mcp_server as hive_server # noqa: E402
|
|
45
|
+
|
|
46
|
+
check("① md_cg SERVER_VERSION == package.json version",
|
|
47
|
+
mdcg_server.SERVER_VERSION == PKG_VERSION,
|
|
48
|
+
f"got {mdcg_server.SERVER_VERSION!r} want {PKG_VERSION!r}")
|
|
49
|
+
check("② hive SERVER_VERSION == package.json version",
|
|
50
|
+
hive_server.SERVER_VERSION == PKG_VERSION,
|
|
51
|
+
f"got {hive_server.SERVER_VERSION!r} want {PKG_VERSION!r}")
|
|
52
|
+
|
|
53
|
+
# ③ 防硬编码回退(issue #42 的 0.1.0 即硬编码脱节形态)
|
|
54
|
+
check("③ 两值均非硬编码 0.1.0",
|
|
55
|
+
mdcg_server.SERVER_VERSION != "0.1.0" and hive_server.SERVER_VERSION != "0.1.0",
|
|
56
|
+
"出现 0.1.0 说明动态读取失效回退")
|
|
57
|
+
|
|
58
|
+
# ④ 回落保底契约:读 package.json 失败 → "0.1.0" 不抛(保底不阻塞启动)
|
|
59
|
+
try:
|
|
60
|
+
with mock.patch("builtins.open", side_effect=OSError("deny")):
|
|
61
|
+
v = mdcg_server._package_version()
|
|
62
|
+
check("④ _package_version 读取失败回落 0.1.0 不抛", v == "0.1.0", f"got {v!r}")
|
|
63
|
+
except Exception as e: # noqa: BLE001
|
|
64
|
+
check("④ _package_version 读取失败回落 0.1.0 不抛", False, f"raised {e!r}")
|
|
65
|
+
|
|
66
|
+
print(f"== 结果: {PASS} 通过 / {FAIL} 失败 ==")
|
|
67
|
+
sys.exit(1 if FAIL else 0)
|