@furongjun1999/dsh-memory 0.6.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +49 -18
  2. package/docs/README.md +1 -0
  3. package/docs/eval/cons200_/345/206/262/347/252/201/346/243/200/346/265/213/351/200/211/351/235/242_/345/256/236/346/226/275/350/256/260/345/275/225_v1.0.md +223 -0
  4. package/docs/eval/cons200_/345/206/262/347/252/201/346/243/200/346/265/213/351/200/211/351/235/242_/345/256/236/346/226/275/350/256/260/345/275/225_v1.1.md +340 -0
  5. package/docs/eval/issue50_/345/205/203/346/225/260/346/215/256/351/200/217/344/274/240/344/270/216/345/205/234/345/272/225_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +437 -0
  6. package/docs/eval/issue50_/345/215/212/351/207/215/345/244/215/345/276/205/345/256/232/345/244/215/346/240/270_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +488 -0
  7. package/docs/eval/issue50_/345/276/205/345/256/232/345/244/215/346/240/270/345/205/245/351/230/237_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +428 -0
  8. package/docs/eval/issue50_/350/257/273/351/235/242/344/277/235/346/212/244/345/217/252/350/256/244/346/230/276/345/274/217/346/235/245/346/272/220_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +271 -0
  9. package/docs/eval/issue50_/351/207/215/350/246/201/345/272/246/345/220/214/346/272/220/344/270/216/344/277/235/346/212/244/350/257/255/344/271/211_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +306 -0
  10. package/docs/eval/issue51_/344/270/200/351/224/256/345/256/211/350/243/205/345/244/261/350/264/245_/345/275/222/345/261/236/345/210/244/345/256/232_v1.0.md +58 -0
  11. package/docs/eval/issue52_/346/235/241/344/273/266/345/205/210/350/241/214/344/270/216/346/210/252/346/226/255/345/217/257/350/247/202/346/265/213_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +360 -0
  12. package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/241/346/241/243/344/275/215/345/215/225/344/270/200/345/205/245/345/217/243_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +651 -0
  13. package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/242/345/217/230/346/233/264/345/215/225/344/270/216/345/233/236/346/273/232/345/216/237/350/257/255_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +439 -0
  14. package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/243/345/207/206/345/205/245/350/257/273/346/225/260_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +1337 -0
  15. package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/244/346/224/266/345/256/230/344/270/216/345/205/250/351/223/276/351/252/214/346/224/266_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +1817 -0
  16. 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
  17. 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
  18. package/docs/eval//345/217/221/345/270/20309_/346/243/200/347/264/242/351/235/242/344/270/211/346/211/271/346/224/266/345/217/243_v1.0.md +66 -0
  19. 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
  20. package/docs/eval//347/254/2543/345/261/202stg/347/273/223/346/236/204/347/264/242/345/274/225_/345/256/236/346/226/275/350/256/260/345/275/225_v1.0.md +250 -0
  21. 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
  22. 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
  23. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +88 -0
  24. 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
  25. package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +47 -11
  26. 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
  27. package/docs/plans/stg/346/235/241/344/273/266/345/214/226/344/270/216/347/273/223/346/236/204/347/264/242/345/274/225_/350/256/276/350/256/241_v0.1.md +124 -0
  28. 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
  29. package/docs/plans//350/256/260/345/277/206/350/207/252/345/244/204/347/220/206/344/270/211/346/241/243/350/207/252/346/262/273_/350/256/276/350/256/241_v0.2.md +207 -0
  30. package/md_cg/admission.py +718 -0
  31. package/md_cg/autonomy_modes.py +642 -0
  32. package/md_cg/bench_e2e_locomo_qa.py +11 -4
  33. package/md_cg/chain.py +47 -0
  34. package/md_cg/consistency.py +133 -8
  35. package/md_cg/forgetting.py +38 -6
  36. package/md_cg/freshness.py +527 -0
  37. package/md_cg/generation.py +409 -0
  38. package/md_cg/hotcache.py +4 -1
  39. package/md_cg/lifecycle.py +30 -2
  40. package/md_cg/mcp_server.py +191 -26
  41. package/md_cg/mdcg.py +594 -39
  42. package/md_cg/mdcos.py +869 -38
  43. package/md_cg/nodefile.py +74 -1
  44. package/md_cg/protect.py +79 -4
  45. package/md_cg/provenance.py +1 -1
  46. package/md_cg/review_cli.py +31 -2
  47. package/md_cg/rollback.py +411 -0
  48. package/md_cg/rollback_cli.py +104 -0
  49. package/md_cg/semantic/canonical.py +22 -0
  50. package/md_cg/semantic/unify.py +73 -22
  51. package/md_cg/semantic/unify_fixture.json +25 -0
  52. package/md_cg/sleep.py +1297 -0
  53. package/md_cg/stg.py +400 -50
  54. package/md_cg/stgidx.py +281 -0
  55. package/md_cg/sustain.py +146 -9
  56. package/md_cg/test_auto_defaults.py +424 -0
  57. package/md_cg/test_autonomy_admission.py +1098 -0
  58. package/md_cg/test_autonomy_modes.py +1972 -0
  59. package/md_cg/test_b1_auto_id_multiproc.py +7 -0
  60. package/md_cg/test_b1b2_write_face.py +7 -0
  61. package/md_cg/test_b3_merge_keeps_content.py +7 -0
  62. package/md_cg/test_boundary_hit.py +410 -0
  63. package/md_cg/test_cons200_scan_selection.py +791 -0
  64. package/md_cg/test_en_pipeline.py +22 -13
  65. package/md_cg/test_generation_guard.py +352 -0
  66. package/md_cg/test_h4_sustain_snapshot.py +14 -4
  67. package/md_cg/test_i50a_half_dup_defer.py +408 -0
  68. package/md_cg/test_i50b_defer_to_review_queue.py +545 -0
  69. package/md_cg/test_i50c_meta_passthrough.py +608 -0
  70. package/md_cg/test_i50d_importance_source.py +531 -0
  71. package/md_cg/test_i50e_readside_protection.py +656 -0
  72. package/md_cg/test_issue39_utf8_stdio.py +9 -1
  73. package/md_cg/test_issue52_scan_condition_first.py +677 -0
  74. package/md_cg/test_linkref.py +7 -0
  75. package/md_cg/test_mode_parity.py +1393 -0
  76. package/md_cg/test_mutation_rollback.py +805 -0
  77. package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +1 -1
  78. package/md_cg/test_n212_n213_n224_generation_gates.py +14 -8
  79. package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +7 -0
  80. package/md_cg/test_n230_dirty_replay.py +375 -0
  81. package/md_cg/test_p2_mcp.py +8 -0
  82. package/md_cg/test_p2_six_elements.py +463 -0
  83. package/md_cg/test_p3_legacy_closure.py +433 -0
  84. package/md_cg/test_p4_freshness.py +680 -0
  85. package/md_cg/test_p8_subgraph_chain.py +14 -1
  86. package/md_cg/test_p9_forget_protect.py +7 -0
  87. package/md_cg/test_p9c_dedup_hints.py +7 -0
  88. package/md_cg/test_policy_required_ccg.py +5 -1
  89. package/md_cg/test_protocol.py +7 -0
  90. package/md_cg/test_rank_parity_score_mode.py +6 -0
  91. package/md_cg/test_semantic_canonical.py +5 -3
  92. package/md_cg/test_sleep.py +611 -0
  93. package/md_cg/test_sleep_p1.py +784 -0
  94. package/md_cg/test_stgidx_index_parity.py +1020 -0
  95. package/md_cg/test_time_core_lint.py +968 -0
  96. package/md_cg/test_unify_default_off.py +701 -0
  97. package/md_cg/test_unify_scope.py +182 -0
  98. package/md_cg/test_writelimit.py +36 -9
  99. package/md_cg/test_writepipe.py +5 -2
  100. package/md_cg/weights.py +15 -1
  101. package/md_cg/whitebox_kb/aeis_core/time_core.py +8 -0
  102. package/md_cg/writepipe.py +101 -2
  103. package/package.json +3 -2
  104. package/skills/plugin.json +1 -1
  105. package/utf8_boot.py +237 -0
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)