@furongjun1999/dsh-memory 0.6.1 → 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.
Files changed (48) hide show
  1. package/README.md +48 -18
  2. package/docs/README.md +1 -0
  3. 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
  4. 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
  5. 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
  6. 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
  7. package/docs/hive//346/243/200/347/264/242/350/267/257/345/276/204/344/270/216/350/256/244/347/237/245/347/273/223/346/236/204/345/245/221/347/272/246_v0.1.md +32 -2
  8. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +88 -0
  9. 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
  10. package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +47 -11
  11. package/docs/mdcg//347/235/241/347/234/240/345/221/250/346/234/237_/350/277/220/347/273/264/345/211/215/346/217/220/344/270/216/347/273/264/346/212/244/346/214/207/345/215/227_v1.0.md +183 -0
  12. 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
  13. package/md_cg/bench_e2e_locomo_qa.py +11 -4
  14. package/md_cg/chain.py +47 -0
  15. package/md_cg/freshness.py +511 -0
  16. package/md_cg/generation.py +409 -0
  17. package/md_cg/hotcache.py +4 -1
  18. package/md_cg/mcp_server.py +128 -19
  19. package/md_cg/mdcg.py +509 -22
  20. package/md_cg/mdcos.py +203 -22
  21. package/md_cg/nodefile.py +74 -1
  22. package/md_cg/semantic/canonical.py +22 -0
  23. package/md_cg/semantic/unify.py +73 -22
  24. package/md_cg/semantic/unify_fixture.json +25 -0
  25. package/md_cg/sleep.py +1297 -0
  26. package/md_cg/sustain.py +146 -9
  27. package/md_cg/test_auto_defaults.py +424 -0
  28. package/md_cg/test_boundary_hit.py +410 -0
  29. package/md_cg/test_en_pipeline.py +22 -13
  30. package/md_cg/test_generation_guard.py +352 -0
  31. package/md_cg/test_h4_sustain_snapshot.py +14 -4
  32. package/md_cg/test_n212_n213_n224_generation_gates.py +14 -8
  33. package/md_cg/test_n230_dirty_replay.py +375 -0
  34. package/md_cg/test_p2_six_elements.py +463 -0
  35. package/md_cg/test_p3_legacy_closure.py +433 -0
  36. package/md_cg/test_p4_freshness.py +680 -0
  37. package/md_cg/test_p8_subgraph_chain.py +14 -1
  38. package/md_cg/test_rank_parity_score_mode.py +6 -0
  39. package/md_cg/test_semantic_canonical.py +5 -3
  40. package/md_cg/test_sleep.py +611 -0
  41. package/md_cg/test_sleep_p1.py +784 -0
  42. package/md_cg/test_time_core_lint.py +968 -0
  43. package/md_cg/test_unify_default_off.py +701 -0
  44. package/md_cg/test_unify_scope.py +182 -0
  45. package/md_cg/whitebox_kb/aeis_core/time_core.py +8 -0
  46. package/package.json +3 -2
  47. package/skills/plugin.json +1 -1
  48. package/utf8_boot.py +237 -0
@@ -0,0 +1,182 @@
1
+ # -*- coding: utf-8 -*-
2
+ """统一归一层作用域收窄守卫(2026-09-30 使用者裁定)
3
+
4
+ 口径:`semantic/unify.py::unify_query` **只对英文内容做翻译归一,中文内容
5
+ 原样不动**——query 按中文段/非中文段切开,中文段逐字保留、绝不送 segment。
6
+ 旧口径「含任一 ASCII 字母即整条归一」把中夹英 query 的中文部分逐字切开
7
+ (「自我接纳」→「自 我 接 纳」),本件钉住收窄后的语义。
8
+
9
+ **开关前提**:归一层缺省**关**(2026-09-30 使用者裁定,唯一开态 = 显式
10
+ `MDCG_UNIFY_QUERY=1`);本件第 1 节钉三态,其后各节一律**显式开**下跑。
11
+ 缺省态的三态与两侧一致性守卫见 `md_cg/test_unify_default_off.py`。
12
+
13
+ 两侧同源:期望值取自 `md_cg/semantic/unify_fixture.json`——Rust 侧
14
+ `rust/src/atoms.rs::tests::unify_mixed_fixture_matches_python` 用 include_str!
15
+ 嵌入**同一份**文件;任一侧漂移即红(两侧逐位相同)。
16
+
17
+ 跑法:python -X utf8 -m md_cg.test_unify_scope
18
+ """
19
+ import json
20
+ import os
21
+ import sys
22
+
23
+ HERE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
24
+ if HERE not in sys.path:
25
+ sys.path.insert(0, HERE)
26
+
27
+ from md_cg.semantic.canonical import is_zh_char # noqa: E402
28
+ from md_cg.semantic.unify import unify_on, unify_query # noqa: E402
29
+
30
+ FIXTURE = os.path.join(HERE, "md_cg", "semantic", "unify_fixture.json")
31
+
32
+ N = 0
33
+ BAD = []
34
+
35
+
36
+ # 生效条件:单参 cond 为真值判定、msg 为说明串;恒累加计数 N,cond 为假时把 msg 记入模块级 BAD(**不中断**)——收集式断言使「定点变异红了几项」可数(首条即停的 assert 风格数不出红项数)。
37
+ def ok(cond, msg):
38
+ global N
39
+ N += 1
40
+ if not cond:
41
+ BAD.append(msg)
42
+
43
+
44
+ # 生效条件:单参 text 为任意字符串,按 canonical.is_zh_char 切出其中的中文连续段,返回保序的段列表(无中文段时为空列表);仅供本守卫做「中文段逐字保留」的结构判据。
45
+ def zh_runs(text):
46
+ """文本里的中文连续段(判据同 unify_query:canonical.is_zh_char)。"""
47
+ out, buf = [], []
48
+ for ch in text:
49
+ if is_zh_char(ch):
50
+ buf.append(ch)
51
+ elif buf:
52
+ out.append("".join(buf))
53
+ buf = []
54
+ if buf:
55
+ out.append("".join(buf))
56
+ return out
57
+
58
+
59
+ # ---- 1 · 开关三态(2026-09-30 使用者裁定:缺省由开翻为关;唯一开态 = 显式 "1")----
60
+ # 本件测的是**作用域收窄**语义,故从本节之后各节必须在**显式开**(=1)下跑:
61
+ # 缺省已关(未设即关),若不显式开,下面 3~8 节全会退化成恒真(对实现零约束力)。
62
+ # 缺省三态守卫另立一件:md_cg/test_unify_default_off.py(两件分工不同,勿互推)。
63
+ ok(unify_on() is False,
64
+ "MDCG_UNIFY_QUERY 未设即关(缺省关,2026-09-30 翻)")
65
+ os.environ["MDCG_UNIFY_QUERY"] = "0"
66
+ try:
67
+ ok(unify_on() is False, "=0 显式关")
68
+ for t in ("I eat beef yesterday", "领养 LGBTQ 群体", "自我接纳",
69
+ " beef 报告 ", "2024 报告"):
70
+ ok(unify_query(t) == t, "开关关:一律原样返回 %r" % t)
71
+ finally:
72
+ os.environ.pop("MDCG_UNIFY_QUERY", None)
73
+ os.environ["MDCG_UNIFY_QUERY"] = "1"
74
+ ok(unify_on() is True, "=1 显式开(英文对照路要用)——本件余下各节的运行前提")
75
+
76
+ # ---- 2 · 空/None/无 ASCII 字母:原样返回(未 strip 的入参)----
77
+ ok(unify_query(None) is None, "None 原样")
78
+ ok(unify_query("") == "", "空串原样")
79
+ ok(unify_query(" 自我接纳 ") == " 自我接纳 ",
80
+ "纯中文(无 ASCII 字母)原样返回入参本身(不 strip)")
81
+ ok(unify_query("2024 报告") == "2024 报告", "纯中文+数字(无字母)原样")
82
+ ok(unify_query(",。") == ",。", "纯标点(无字母)原样")
83
+
84
+ # ---- 3 · 纯英文仍翻译(收窄不得关掉英文链路)----
85
+ ok(unify_query("I eat beef yesterday") == "我 吃 牛肉 昨天",
86
+ "纯英文仍归一:%r" % unify_query("I eat beef yesterday"))
87
+ ok(unify_query("wrote") == "写", "纯英文时态还原仍生效")
88
+
89
+ # ---- 4 · 纯中文原样(旧口径会被逐字切开)----
90
+ for t, why in (("自我接纳", "旧口径「自 我 接 纳」"),
91
+ ("慈善跑", "旧口径「慈 善 跑」"),
92
+ ("蜂群调度 依赖门禁", "旧口径逐字切开")):
93
+ ok(unify_query(t) == t, "纯中文原样(%s):%r" % (why, t))
94
+
95
+ # ---- 5 · fixture 逐位对拍(两侧同源:Rust 侧嵌同一份文件)----
96
+ with open(FIXTURE, encoding="utf-8") as f:
97
+ FX = json.load(f)["cases"]
98
+ ok(len(FX) >= 12, "fixture 用例数 >= 12:%d" % len(FX))
99
+ for c in FX:
100
+ got = unify_query(c["in"])
101
+ ok(got == c["out"],
102
+ "fixture 逐位不符 [%s] in=%r got=%r want=%r"
103
+ % (c["name"], c["in"], got, c["out"]))
104
+
105
+ # ---- 5b · 单侧钉:任务原型例(Python 侧形态)----
106
+ # 这两例**不入共享 fixture**:非中文段是未登录的**大写**词(LGBTQ),两侧对
107
+ # 「未命中词表的大写词」存在**既有**非对称(Python 保留原大小写 / Rust 经
108
+ # normalize_en 一律 lower 化——用 rust 单测实测 `领养 lgbtq 群体` 取证,
109
+ # 2026-09-30),故两侧钉不出同一期望值。此非本次收窄引入,也非本次范围;
110
+ # 但它是端到端(Python 侧)实际生效的形态,故在此单侧钉死。
111
+ for t in ("领养 LGBTQ 群体 支持 包容", "领养 机构 LGBTQ 群体 支持 包容"):
112
+ ok(unify_query(t) == t, "中夹英原型例:中文段逐字保留、英文片段原样:%r" % t)
113
+
114
+ # ---- 6 · 结构判据(对**实现产物**断言):混合 query 的中文段逐字保留 ----
115
+ # 2026-09-30 清理批次(甲2)修假守卫:本节此前比的是 fixture 的**期望值**
116
+ # (`run in c["out"]`——拿期望比期望),对实现零约束力:做「中文段也送
117
+ # segment」变异时本节 0 项红。现改为直接调 `unify_query` 并对**产物**断言:
118
+ # 中文段必须在产物里逐字出现,且其逐字切开形(`" ".join(run)`)不得出现。
119
+ # 单字中文段(如「年」)的切开形与其本身同形,故切开判据只在 len(run) > 1
120
+ # 时施加(否则是恒假的伪红)。
121
+ for c in FX:
122
+ got = unify_query(c["in"])
123
+ for run in zh_runs(c["in"]):
124
+ ok(run in got,
125
+ "中文段未逐字保留(被改写)[%s] in=%r 段=%r got=%r"
126
+ % (c["name"], c["in"], run, got))
127
+ if len(run) > 1:
128
+ ok(" ".join(run) not in got,
129
+ "中文段被逐字切开(送了 segment)[%s] in=%r 段=%r got=%r"
130
+ % (c["name"], c["in"], run, got))
131
+ # 原型例(任务口径原句)同样对产物断言:中文段逐字保留、英文片段不动
132
+ for t in ("领养 LGBTQ 群体", "AI 记忆 系统", "自我接纳", "蜂群调度 依赖门禁"):
133
+ got = unify_query(t)
134
+ for run in zh_runs(t):
135
+ ok(run in got and " ".join(run) not in got,
136
+ "原型例中文段逐字保留 [%r] 段=%r got=%r" % (t, run, got))
137
+ ok(zh_runs("领养 LGBTQ 群体") == ["领养", "群体"], "中文段抽取判据自检")
138
+
139
+ # ---- 7 · 空白折叠为单空格(段内/段间)----
140
+ ok(unify_query(" beef 报告 ") == "牛肉 报告", "前后空白+多空格折叠")
141
+ ok(unify_query("beef\t报告") == "牛肉 报告", "制表符折叠为单空格")
142
+
143
+ # ---- 8 · 异常降级为原样(不阻断检索主链路)----
144
+ import md_cg.semantic.canonical as _cn # noqa: E402
145
+ _saved = _cn.query_atoms
146
+ _cn.query_atoms = lambda _t: (_ for _ in ()).throw(RuntimeError("boom"))
147
+ try:
148
+ src = "beef 报告"
149
+ ok(unify_query(src) == src, "链路抛异常 → 原样返回")
150
+ finally:
151
+ _cn.query_atoms = _saved
152
+
153
+ # ---- 9 · 中文判据边界(2026-09-30 清理批次乙2):仅长度恰为 1 的串可为真 ----
154
+ # 事实读数(本机实测,见清理批次回执):留池条目 ⑥ 记「`is_zh_char("")` 返回
155
+ # True」——**实测不成立**:`ZH_LO <= ch <= ZH_HI` 是链式比较,对空串即假,
156
+ # 收紧前 `is_zh_char("")` 已返回 False。真实外溢面是**多字符串**——
157
+ # 收紧前 `is_zh_char("中文")` 判真(首字符在区间即真)。两处消费点
158
+ # (unify._runs / 本文件 zh_runs)均逐字符传入,故本收紧对存量行为零变更,
159
+ # 只把判据面(长度恰为 1)钉死。
160
+ ok(is_zh_char("") is False, "空串不是中文(判据面外)")
161
+ ok(is_zh_char("中文") is False, "多字符串为假(首字符在区间亦然)")
162
+ ok(is_zh_char("中a") is False, "多字符串为假(汉字+ASCII 混合)")
163
+ ok(is_zh_char("中") is True, "单汉字为真")
164
+ ok(is_zh_char("\u4e00") is True, "区间下界单汉字为真")
165
+ ok(is_zh_char("\u9fff") is True, "区间上界单汉字为真")
166
+ ok(is_zh_char("\u4dff") is False, "区间下界外单字符为假(U+4DFF)")
167
+ ok(is_zh_char("\ua000") is False, "区间上界外单字符为假(U+A000)")
168
+ ok(is_zh_char("a") is False, "单 ASCII 字母为假")
169
+ # 两侧边界语义一致性:Rust `text::is_zh` 收 char,天然无空/多字符二态;
170
+ # 本侧判据收紧后,「长度 != 1 一律 False」与「按 char 判区间」等价
171
+ # (见 rust/src/text.rs::is_zh docstring 的边界约定段)。
172
+ for ch in ("\u4e00", "\u9fff"):
173
+ ok(is_zh_char(ch) and len(ch) == 1, "单字符区间内 → 真(与 Rust is_zh 等价)%r" % ch)
174
+
175
+ os.environ.pop("MDCG_UNIFY_QUERY", None) # 归还进程环境(本件全程已显式开完)
176
+
177
+ if BAD:
178
+ print("[test_unify_scope] FAILED %d/%d 断言:" % (len(BAD), N))
179
+ for b in BAD:
180
+ print(" - " + b)
181
+ raise SystemExit(1)
182
+ print("[test_unify_scope] %d 断言全绿(fixture %d 例,两侧同源)" % (N, len(FX)))
@@ -7,6 +7,14 @@
7
7
  衰减率 γ 按对象配置;任何在本模块之外出现的衰减核实现
8
8
  (exp(-t/τ)、×(1-factor)、EMA 保持率)都是 bug——
9
9
  由 tools/time_core_lint.py 按 E5 口径机械化审计。
10
+ (P3 落地:该审计器此前全仓不存在,现落在 **`md_cg/test_time_core_lint.py`**
11
+ ——`python -X utf8 -m md_cg.test_time_core_lint` 审计全仓四形态,指数核的
12
+ **调用式 `exp(-…` 与点调用 `.exp(…)/.exp()`(Rust 写法)两种形态都入面**;
13
+ 另带登记表 + 等价性断言:`links.py` 同族半衰期(G2),以及 **Rust 读侧
14
+ `rust/src/freshness.rs` 的指数核**(G11——从 Rust 源码抽常量与核表达式逐 Δt
15
+ 点与 `cred_factor` 对拍,因 Rust 无法被 Python 调用)。自带 `--mutate` 定点
16
+ 变异自证,其中 P4-M9 就是「把该 Rust 登记摘掉必须转红」那条。
17
+ **不新开顶层 `tools/` 目录**:那会改 `WORKSPACE_INDEX.md` 的结构面。)
10
18
  规范依据:docs/概念钉死批_GPT四点评审_v0.1.md 钉死 3。
11
19
  """
12
20
  import math
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@furongjun1999/dsh-memory",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "灵枢(Lingshu·líng shū)DeepSeek Harness 插件:完整大脑——长期记忆/知识飞轮/自我认知/递归反思接入 DSH,对话自动沉淀进 md_cg 认知图(md 文档)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -20,6 +20,7 @@
20
20
  "lib",
21
21
  "src",
22
22
  "md_cg",
23
+ "utf8_boot.py",
23
24
  "data/policy.json",
24
25
  "skills",
25
26
  "README.md",
@@ -85,7 +86,7 @@
85
86
  "build": "node -e \"require('node:fs').rmSync('lib',{recursive:true,force:true})\" && tsc -p tsconfig.json",
86
87
  "test": "npm run build && node --import tsx --test \"test/*.test.ts\"",
87
88
  "prepare": "node -e \"const fs=require('fs');const tsc='node_modules/typescript/bin/tsc';fs.existsSync(tsc)?(fs.rmSync('lib',{recursive:true,force:true}),require('child_process').execFileSync(process.execPath,[tsc,'-p','tsconfig.json'],{stdio:'inherit'})):console.log('[prepare] 跳过构建:typescript 未安装(NODE_ENV=production 或 --omit=dev 会省略 devDependencies)——需要构建请用 npm install --include=dev')\"",
88
- "gate": "python scripts/check_unreachable.py && python scripts/check_publish_artifact.py && python scripts/cogmap_sync.py check && python scripts/link_check.py && python scripts/workspace_index.py --check && python scripts/verify_discipline.py --allow-missing && python scripts/gate_rust_crate_test.py && python scripts/gate_rust_parity.py",
89
+ "gate": "python scripts/check_unreachable.py && python scripts/check_publish_artifact.py && python scripts/check_publish_smoke.py && python scripts/cogmap_sync.py check && python scripts/link_check.py && python scripts/workspace_index.py --check && python scripts/verify_discipline.py --allow-missing && python scripts/gate_rust_crate_test.py && python scripts/gate_rust_parity.py",
89
90
  "prepublishOnly": "npm run build && npm test && npm run gate"
90
91
  },
91
92
  "repository": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "lingshu-skills",
4
- "version": "0.6.1",
4
+ "version": "0.7.0",
5
5
  "description": "灵枢(AEIS)自我认知技能包——白箱条件化知识(Agent Skills 兼容导出)。本质是灵枢了解自身的工具:每个技能描述灵枢在什么条件下能做什么、怎么执行、克制什么(KCCS 四要素),由条件路由图精确路由,由灵枢 MCP 工具执行验证(物理基底)。",
6
6
  "author": {
7
7
  "name": "灵枢(AEIS)· CommonTrustProtocol"
package/utf8_boot.py ADDED
@@ -0,0 +1,237 @@
1
+ # -*- coding: utf-8 -*-
2
+ """utf8_boot.py · 入口自保证 UTF-8(进程自保证,不依赖使用者 profile 的环境变量)。
3
+
4
+ 为什么放在**仓库根**、而不是 `md_cg/` 里
5
+ ------------------------------------------------------------------------
6
+ 本模块被 hive / md_cg / scripts 三处的**进程入口**共用。放进 `md_cg/` 会让
7
+ `import md_cg` 拉起整个认知图包(`md_cg/__init__.py` 会 `import .mdcg`)——为一次
8
+ UTF-8 检查付出「重包导入的启动成本 + hive/scripts 入口被绑上 md_cg 依赖方向」的
9
+ 双重代价。放仓库根 = 零第三方、零 md_cg 依赖、入口一行 `sys.path` 插入即可用。
10
+
11
+ 它解决什么(修前事实,本机 2026-09-29 实测)
12
+ ------------------------------------------------------------------------
13
+ * `locale.getlocale()` = `('Chinese (Simplified)_China', '936')`,
14
+ `locale.getpreferredencoding(False)` = `cp936`;Python 裸 `open()` 的缺省编码
15
+ 与 stdio 的控制台代码页都跟着 locale 走。
16
+ * 清空 `PYTHONUTF8` / `PYTHONIOENCODING` 后实测:裸 `open()` 读 UTF-8 中文文件抛
17
+ `UnicodeDecodeError`;`sys.stdout.write("中文")` 静默产出 GBK 字节(`\\xd6\\xd0\\xce\\xc4`);
18
+ 裸 `open(p, "w")` 写中文落 GBK 字节——**静默坏**,比抛异常更难查。
19
+ * 本机之所以没炸,是因为**环境里**有 `PYTHONUTF8=1` + `PYTHONIOENCODING=utf-8`,
20
+ 且部分入口自己钉(`scripts/run_tests.py` 给子进程覆盖、`scripts/linux_verify.sh`
21
+ export、`src/lib/mdcg_client.ts` 设 PYTHONUTF8)。
22
+ ⇒ 正确性挂在「环境变量 + 每个入口记得设」上:换台没设的机器,或新入口漏设,
23
+ 即走 GBK。本模块把该保证**下沉到进程自身**。
24
+
25
+ 三条路(`ensure_utf8` 的分支)
26
+ ------------------------------------------------------------------------
27
+ 0. **调用方不是进程入口(被 import)→ 只置 env 即返回**——绝不重启、绝不退出、绝不打
28
+ 日志。见下「被 import 时不得重启(F6)」。
29
+ 1. `sys.flags.utf8_mode` 为真 → 立即返回(不重启、不打日志);
30
+ 2. 为假 → **默认重启自身**(`-X utf8`);重启后子进程 `utf8_mode` 已开 ⇒ 结构上
31
+ 不可能二次重启(幂等,见守卫的「只重启一次」判据);
32
+ 3. 显式退出通道:env `LINGSHU_UTF8_NO_REEXEC` 为真 → **fail-fast** 非 0 退出并打印
33
+ 可执行指引(不能接受子进程的场景:嵌入式宿主、CI 调试、进程树审计)。
34
+
35
+ 无论走哪条路(含第 0 路),都会把 `PYTHONUTF8=1` 与 `PYTHONIOENCODING=utf-8` 置入
36
+ `os.environ`,供本进程随后派生的子进程继承(执行器/编排器派生子进程时的第二道保险)。
37
+
38
+ 被 import 时不得重启(F6,2026-09-29 复核实测)
39
+ ------------------------------------------------------------------------
40
+ 本助手在七个入口里都是**模块级**调用,而其中数个入口**同时是被 import 的库**
41
+ (`hive/serve_start.py` 被 `hive/hive_mcp/mcp_server.py` 导入、`hive/exec.py` 被
42
+ `hive/orch.py` 导入)。若「被 import」也整进程重启:调用方正在处理的输入会被吞掉。
43
+ 复核员构造态实测(未加本判据前):单会话解释器启动 2 次、第 3 条请求被吞、
44
+ **无错误帧、rc=0**——静默坏,比抛异常更难查。故重启/fail-fast 只在
45
+ 「调用方模块 == `__main__`」时发生;被 import 时只置 env。
46
+
47
+ fail-fast 指引必须**可照抄**(F2,2026-09-29 复核实测)
48
+ ------------------------------------------------------------------------
49
+ 指引此前一律印 `python -X utf8 <入口 __file__>`;而两个 MCP 入口的真实接入形态是
50
+ `python -m md_cg.mcp_server` / `python -m hive.hive_mcp.mcp_server`(mcp.json 直连),
51
+ 照抄文件形态会 `ImportError: attempted relative import with no known parent package`
52
+ (实测 rc=1)。故 `_guidance` 复用与 `reexec_argv()` **同一**判别(`__main__.__spec__`
53
+ 有 name 与 parent ⇒ `-m` 形态),印 `python -X utf8 -m <模块名>`。
54
+
55
+ **禁用 `os.exec*` 系列**:Windows 上进程替换与标准句柄继承的语义不可靠,而 MCP 是
56
+ **字节组帧**的 stdio 协议——重启必须显式传递 `stdin`/`stdout`/`stderr` 三个标准流,
57
+ 丢了组帧就是灾难。故本模块只用 `subprocess.run`(实测:父进程退出后子进程持有的
58
+ 管道句柄仍完好)。
59
+
60
+ 与设计裁决的一处偏离(已在回报中声明,理由是端到端组帧这条真判据)
61
+ ------------------------------------------------------------------------
62
+ 重启 argv 除 `-X utf8` 外**保留 `-m` 模块语义**:`python -m md_cg.mcp_server` 若按
63
+ `sys.argv` 原样重启,会退化成「直跑文件」(`sys.path[0]` 变包目录、`__package__`
64
+ 丢失),`md_cg/mcp_server.py:3833` 的 `from .datapath import ...` 立即 ImportError
65
+ ——本机实测 `python -X utf8 md_cg/mcp_server.py` → rc=1、
66
+ `ImportError: attempted relative import with no known parent package`。而
67
+ `python -m md_cg.mcp_server` 正是 ZCode/DSH 的 mcp.json 接入形态(该文件头注第 14 行)。
68
+ 故 `-m` 形态按 `[python, -X, utf8, -m, <模块名>, *sys.argv[1:]]` 重建,其余形态原样
69
+ `*sys.argv`;源码无法重放的解释器形态(`-c` / `-`,从命令行/stdin 读源码)**不重启**,
70
+ 只告警——那里重启会把源码丢掉(`python -X utf8 -c` 会转而从 stdin 读代码)。
71
+ """
72
+ from __future__ import annotations
73
+
74
+ import locale
75
+ import os
76
+ import subprocess
77
+ import sys
78
+
79
+ __all__ = ["ensure_utf8", "launch_hint", "reexec_argv", "NO_REEXEC_ENV",
80
+ "EXIT_UTF8_REQUIRED"]
81
+
82
+ #: 显式退出通道:置真值即不自动重启,改为 fail-fast 非 0 退出
83
+ NO_REEXEC_ENV = "LINGSHU_UTF8_NO_REEXEC"
84
+ #: fail-fast 退出码(非 0;指引文案里点名「怎么改」)
85
+ EXIT_UTF8_REQUIRED = 2
86
+ #: 真值判定集(去空白 + 转小写后比对;未设 / 空串 / 其它值一律视为否)
87
+ _TRUTHY = frozenset(("1", "true", "yes", "on", "y", "t"))
88
+ #: 源码无法重放的解释器形态(`-c` 从命令行读、`-` 从 stdin 读):重启会丢源码
89
+ _UNREBUILDABLE_ARGV0 = frozenset(("-c", "-"))
90
+ #: 结构化退出码:解释器形态无法重放时「不重启也不假装已保证」的退出码(守卫熔断未命中)
91
+ _EXIT_UNREBUILDABLE = 3
92
+
93
+
94
+ # 生效条件:env name 存在且其值去空白转小写后落在 _TRUTHY 内返回 True;未设、空串或其它值返回 False。
95
+ def _truthy(name: str) -> bool:
96
+ """env 真值判定(唯一口径:去空白 + 小写 + 白名单比对)。"""
97
+ return (os.environ.get(name) or "").strip().lower() in _TRUTHY
98
+
99
+
100
+ # 生效条件:无入参;把 PYTHONUTF8=1 与 PYTHONIOENCODING=utf-8 写入 os.environ(覆盖旧值,幂等),随后派生的子进程按此继承,无返回值。
101
+ def _set_child_env() -> None:
102
+ """置子进程继承面(第四条):PYTHONUTF8=1 + PYTHONIOENCODING=utf-8。"""
103
+ os.environ["PYTHONUTF8"] = "1"
104
+ os.environ["PYTHONIOENCODING"] = "utf-8"
105
+
106
+
107
+ # 生效条件:无入参;`__main__.__spec__` 同时有 name 与 parent(`python -m pkg.mod` 形态)时返回该模块名,否则返回 None(直跑文件时 `__spec__` 为 None)。
108
+ def _m_module_name() -> str | None:
109
+ """本进程是否以 `python -m <模块>` 启动;是则返回模块名,否则 None。
110
+
111
+ **单点判别**:`reexec_argv()`(重启 argv)与 `launch_hint()`(fail-fast 指引文案)
112
+ 共用本函数——两处各写一份必然漂移,而指引印错的代价是使用者照抄后撞墙(F2)。
113
+ """
114
+ main = sys.modules.get("__main__")
115
+ spec = getattr(main, "__spec__", None)
116
+ name = getattr(spec, "name", None)
117
+ if name and getattr(spec, "parent", None):
118
+ return name
119
+ return None
120
+
121
+
122
+ # 生效条件:entry 为入口标识或 None;本进程经 `python -m` 启动时返回 `-m <模块名>`,其余形态返回 entry 本身(None 时回落占位 `<entry>`)。
123
+ def launch_hint(entry: str | None) -> str:
124
+ """「照抄即可跑」的启动形态(`-X utf8` 之后的那一段)。
125
+
126
+ -m 形态 → `-m <模块名>`(模块名可被 python 重新 import,等同使用者的原始命令);
127
+ 其余形态 → `entry`(通常 `__file__`,绝对路径)。
128
+ """
129
+ name = _m_module_name()
130
+ if name:
131
+ return "-m %s" % name
132
+ return entry or "<entry>"
133
+
134
+
135
+ # 生效条件:无入参;调用 ensure_utf8 的模块 `f_globals` 与 `sys.modules['__main__'].__dict__` 同一时返回 True;取不到调用帧时返回 False(保守:不重启)。
136
+ def _caller_is_process_entry() -> bool:
137
+ """调用 `ensure_utf8` 的模块是否**就是本进程入口**(`__main__`)。
138
+
139
+ F6 判据本体:只在「本模块即进程入口」时重启/fail-fast——被 import 时重启会吞掉
140
+ 调用方正在处理的输入(复核实测:无错误帧、rc=0,静默)。用 `f_globals` 与
141
+ `__main__.__dict__` 的**同一性**比对:`python -m pkg.mod` 下入口模块的 `__name__`
142
+ 也是 `"__main__"`,故 -m 与直跑文件两种入口形态得到同一判定。
143
+ """
144
+ try:
145
+ frame = sys._getframe(1) # ensure_utf8 自身的帧
146
+ globs = frame.f_back.f_globals # 调用 ensure_utf8 的那一帧
147
+ except (ValueError, AttributeError):
148
+ return False # 取不到调用帧:不重启(静默重启的代价更大)
149
+ return globs is getattr(sys.modules.get("__main__"), "__dict__", None)
150
+
151
+
152
+ # 生效条件:无入参;sys.argv[0] 为 -c/- (源码不可重放)时返回 None;本进程经 python -m 启动(_m_module_name 非空)时返回 [exe, -X, utf8, -m, 模块名, *sys.argv[1:]];其余形态返回 [exe, -X, utf8, *sys.argv]。
153
+ def reexec_argv() -> list | None:
154
+ """重启自身的 argv(保留 -m 模块语义);源码不可重放时返回 None。"""
155
+ if sys.argv and sys.argv[0] in _UNREBUILDABLE_ARGV0:
156
+ return None
157
+ name = _m_module_name()
158
+ if name:
159
+ return [sys.executable, "-X", "utf8", "-m", name, *sys.argv[1:]]
160
+ return [sys.executable, "-X", "utf8", *sys.argv]
161
+
162
+
163
+ # 生效条件:entry 为入口标识(通常 __file__)或 None,cause 为原因短句;返回「可执行指引」多行文本——启动形态经 launch_hint 复用 -m 判别(-m 入口印 `python -X utf8 -m <模块名>`,其余印 `<entry>`),并含 PYTHONUTF8=1 写法、当前 locale 缺省编码、以及取消 NO_REEXEC_ENV 的出路;全为 ASCII 与中文,无任何凭据。
164
+ def _guidance(entry: str, cause: str) -> str:
165
+ """fail-fast / 无法重启时的可执行指引(照抄即可跑)。"""
166
+ try:
167
+ pref = locale.getpreferredencoding(False)
168
+ except Exception: # noqa: BLE001
169
+ pref = "?"
170
+ hint = launch_hint(entry) # F2:与 reexec_argv 同一判别
171
+ return (
172
+ "utf8_boot: 入口需要**进程级 UTF-8**,但当前解释器未启用"
173
+ "(sys.flags.utf8_mode=0,locale 缺省编码=%s)。\n"
174
+ " 入口:%s\n"
175
+ " 原因:%s\n"
176
+ "请改用下列任一方式启动(可照抄):\n"
177
+ " python -X utf8 %s\n"
178
+ " PYTHONUTF8=1 python %s # Windows cmd: set PYTHONUTF8=1 && python ...\n"
179
+ "或取消环境变量 %s(让入口自动以 -X utf8 重启自身)。\n"
180
+ % (pref, entry, cause, hint, hint, NO_REEXEC_ENV)
181
+ )
182
+
183
+
184
+ # 生效条件:text 给出后优先以 UTF-8 字节写 sys.stderr.buffer(不受控制台代码页影响),buffer 不可用时回落文本层写入;两级都失败则静默返回,无返回值。
185
+ def _emit(text: str) -> None:
186
+ """把指引写到 stderr——**显式 UTF-8 字节**,不随控制台代码页漂移。"""
187
+ try:
188
+ sys.stderr.buffer.write(text.encode("utf-8"))
189
+ sys.stderr.buffer.flush()
190
+ return
191
+ except Exception: # noqa: BLE001
192
+ pass
193
+ try:
194
+ sys.stderr.write(text)
195
+ sys.stderr.flush()
196
+ except Exception: # noqa: BLE001
197
+ pass
198
+
199
+
200
+ # 生效条件:entry 为入口标识或 None;先把 PYTHONUTF8/PYTHONIOENCODING 置入 env;调用方非进程入口(本助手被 import)时立即返回(不重启/不退出/不打日志);sys.flags.utf8_mode 为真时立即返回(不重启、不打日志);为假且 LINGSHU_UTF8_NO_REEXEC 为真时向 stderr 打印可执行指引并以 EXIT_UTF8_REQUIRED 退出;为假且该 env 非真时以 reexec_argv() 重启自身(显式传递 stdin/stdout/stderr 三流)并以子进程退出码退出;重启 argv 为 None(-c/- 形态)或子进程无法启动(OSError)时打印指引并以 _EXIT_UNREBUILDABLE / EXIT_UTF8_REQUIRED 退出;无返回值(不返回即在重启或退出的路上)。
201
+ def ensure_utf8(entry: str | None = None) -> None:
202
+ """入口自保证 UTF-8 —— 唯一调用点,**必须在任何文件/库 I/O 之前**。
203
+
204
+ 生效条件:`utf8_mode` 已开时置 env 后立即返回(不重启、不打日志);未开且
205
+ **本模块不是进程入口**(本助手被 import)时置 env 后立即返回——不重启、不退出、
206
+ 不打日志(F6:被 import 时重启会静默吞掉调用方的输入);未开且
207
+ `LINGSHU_UTF8_NO_REEXEC` 为真时向 stderr 打印可执行指引并以
208
+ ``EXIT_UTF8_REQUIRED``(2) 退出;未开且该 env 非真时以 ``reexec_argv()`` 重启
209
+ 自身(``-X utf8``,显式传递 stdin/stdout/stderr)并以子进程退出码退出——重启后
210
+ 子进程 ``utf8_mode`` 已开,故不会二次重启;``-c``/``-`` 形态(源码不可重放)
211
+ 不重启,打印指引后以 ``3`` 退出。
212
+
213
+ 不适用条件:本进程已在 UTF-8 模式下运行、或本助手是被 import 的(非进程入口)时,
214
+ 只置子进程继承面、不做任何重启——即「已在 utf8 模式」「被 import」两者都是幂等
215
+ 且安静的(子进程不会再重启)。**入口若以非 `__main__` 形态被 import,则不获得
216
+ 重启保证**(这是有意的:静默重启的代价高于少一次保证)。
217
+ """
218
+ _set_child_env()
219
+ if sys.flags.utf8_mode:
220
+ return # ②:已开 → 不重启、不打日志
221
+ if not _caller_is_process_entry():
222
+ return # F6:被 import → 只置 env,静默
223
+ target = entry or (sys.argv[0] if sys.argv else "<entry>")
224
+ if _truthy(NO_REEXEC_ENV):
225
+ _emit(_guidance(target, "%s 已置真:禁止自动重启" % NO_REEXEC_ENV))
226
+ raise SystemExit(EXIT_UTF8_REQUIRED) # ③:显式退出通道
227
+ argv = reexec_argv()
228
+ if argv is None:
229
+ _emit(_guidance(target, "解释器以 -c/- 读源码,argv 无法重放(不重启)"))
230
+ raise SystemExit(_EXIT_UNREBUILDABLE)
231
+ try:
232
+ proc = subprocess.run(argv, stdin=sys.stdin, stdout=sys.stdout,
233
+ stderr=sys.stderr) # 三流显式传递:字节组帧
234
+ except OSError as exc:
235
+ _emit(_guidance(target, "重启自身失败:%r" % (exc,)))
236
+ raise SystemExit(EXIT_UTF8_REQUIRED)
237
+ raise SystemExit(proc.returncode)