@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.
Files changed (202) hide show
  1. package/README.md +618 -572
  2. package/codebuddy/CODEBUDDY.md +10 -10
  3. package/data/policy.json +27 -0
  4. package/docs/README.md +1 -0
  5. package/docs/discipline/harnesses.yaml +31 -0
  6. package/docs/discipline/templates/full.md.tmpl +1 -1
  7. package/docs/discipline/templates/rules.mdc.tmpl +1 -1
  8. 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
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +100 -0
  31. 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
  32. package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +57 -9
  33. 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
  34. 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
  35. 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
  36. 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
  37. 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
  38. 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
  39. 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
  40. 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
  41. package/lib/bridge.js +24 -2
  42. package/lib/hooks.d.ts +31 -2
  43. package/lib/hooks.js +219 -16
  44. package/lib/init.js +16 -1
  45. package/lib/lib/prompt_safety.d.ts +52 -1
  46. package/lib/lib/prompt_safety.js +76 -1
  47. package/md_cg/audit.py +652 -379
  48. package/md_cg/bench6_arms.py +1 -1
  49. package/md_cg/bench_e2e_locomo_qa.py +11 -4
  50. package/md_cg/bench_p0.py +1 -1
  51. package/md_cg/branches.py +2 -2
  52. package/md_cg/ccgc.py +1071 -1006
  53. package/md_cg/chain.py +48 -1
  54. package/md_cg/consistency.py +97 -9
  55. package/md_cg/consolidate.py +34 -9
  56. package/md_cg/crypto.py +59 -26
  57. package/md_cg/docindex.py +16 -1
  58. package/md_cg/evolution.py +17 -1
  59. package/md_cg/export.py +1 -1
  60. package/md_cg/forgetting.py +309 -13
  61. package/md_cg/freshness.py +511 -0
  62. package/md_cg/fsutil.py +454 -8
  63. package/md_cg/generation.py +409 -0
  64. package/md_cg/hotcache.py +4 -1
  65. package/md_cg/identity.py +3 -3
  66. package/md_cg/insight.py +24 -3
  67. package/md_cg/linkref.py +1 -1
  68. package/md_cg/logref.py +327 -0
  69. package/md_cg/mcp_server.py +4261 -3818
  70. package/md_cg/mdcg.py +4695 -3547
  71. package/md_cg/mdcos.py +4672 -4159
  72. package/md_cg/mreview/pipeline.py +15 -1
  73. package/md_cg/nodefile.py +713 -576
  74. package/md_cg/protect.py +158 -11
  75. package/md_cg/protocol.py +41 -2
  76. package/md_cg/provenance.py +22 -4
  77. package/md_cg/reach.py +4 -4
  78. package/md_cg/readcache.py +76 -15
  79. package/md_cg/reconcile.py +1 -1
  80. package/md_cg/refindex.py +246 -37
  81. package/md_cg/refine.py +1 -1
  82. package/md_cg/routing.py +32 -5
  83. package/md_cg/run_tests.py +17 -0
  84. package/md_cg/scrub.py +30 -3
  85. package/md_cg/security.py +47 -1
  86. package/md_cg/self_state.py +5 -5
  87. package/md_cg/semantic/canonical.py +22 -0
  88. package/md_cg/semantic/unify.py +73 -22
  89. package/md_cg/semantic/unify_fixture.json +25 -0
  90. package/md_cg/sleep.py +1297 -0
  91. package/md_cg/sources.py +23 -6
  92. package/md_cg/srcindex.py +352 -0
  93. package/md_cg/stg.py +47 -3
  94. package/md_cg/subgraph.py +2 -2
  95. package/md_cg/sustain.py +1436 -1168
  96. package/md_cg/tasks.py +469 -470
  97. package/md_cg/test_auto_defaults.py +424 -0
  98. package/md_cg/test_b1_auto_id_multiproc.py +277 -0
  99. package/md_cg/test_b1b2_write_face.py +417 -0
  100. package/md_cg/test_b2_sensitivity_landing.py +223 -0
  101. package/md_cg/test_b3_merge_keeps_content.py +576 -0
  102. package/md_cg/test_b4_shard_dir_selfheal.py +843 -0
  103. package/md_cg/test_b4_shard_dir_selfheal_guard.py +238 -0
  104. package/md_cg/test_boundary_hit.py +410 -0
  105. package/md_cg/test_c3_transient_read_negative.py +793 -0
  106. package/md_cg/test_c8_search_rrf_gates.py +502 -0
  107. package/md_cg/test_ccg_form_parity.py +188 -0
  108. package/md_cg/test_ccgc.py +28 -3
  109. package/md_cg/test_en_pipeline.py +22 -13
  110. package/md_cg/test_generation_guard.py +352 -0
  111. package/md_cg/test_govern_directread.py +17 -4
  112. package/md_cg/test_h2_session_view_norm.py +233 -0
  113. package/md_cg/test_h4_sustain_snapshot.py +1387 -0
  114. package/md_cg/test_hive_ingest.py +285 -285
  115. package/md_cg/test_issue39_utf8_stdio.py +307 -16
  116. package/md_cg/test_issue43_default_policy.py +865 -0
  117. package/md_cg/test_legacy_p3_node_id_type.py +375 -0
  118. package/md_cg/test_linkref.py +10 -3
  119. package/md_cg/test_lock.py +2 -2
  120. package/md_cg/test_logref.py +1109 -0
  121. package/md_cg/test_m3_h9_bucket_health_protect_mark.py +301 -0
  122. package/md_cg/test_m3_h9_semantic_guard.py +525 -0
  123. package/md_cg/test_mr_m2.py +15 -3
  124. package/md_cg/test_n130_verify_falsified_protect.py +1 -1
  125. package/md_cg/test_n139_dek_provision_failclosed.py +185 -0
  126. package/md_cg/test_n176_link_trust.py +221 -0
  127. package/md_cg/test_n178_units_jobid_gate.py +212 -0
  128. package/md_cg/test_n184_keys_concurrent_provision.py +307 -0
  129. package/md_cg/test_n195_writepath_reload.py +283 -0
  130. package/md_cg/test_n196_stale_gate_skip.py +169 -0
  131. package/md_cg/test_n197_n208_write_face_gates.py +471 -0
  132. package/md_cg/test_n198_tokens_corrupt_failclosed.py +225 -0
  133. package/md_cg/test_n199_tokens_concurrent_write.py +378 -0
  134. package/md_cg/test_n201_proposal_visibility.py +382 -0
  135. package/md_cg/test_n202_session_notes_visibility.py +461 -0
  136. package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +542 -0
  137. package/md_cg/test_n206_stdio_jsonrpc_type.py +403 -0
  138. package/md_cg/test_n209_verify_write_face_gates.py +461 -0
  139. package/md_cg/test_n212_n213_n224_generation_gates.py +519 -0
  140. package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +563 -0
  141. package/md_cg/test_n225_nonobject_load.py +1110 -0
  142. package/md_cg/test_n230_dirty_replay.py +375 -0
  143. package/md_cg/test_n62_tenant_bind_failclosed.py +200 -0
  144. package/md_cg/test_neg_condition_hits.py +333 -0
  145. package/md_cg/test_neg_tail_honesty.py +663 -0
  146. package/md_cg/test_none_id_write_guard.py +4 -3
  147. package/md_cg/test_opt_batch1_md_cg.py +451 -0
  148. package/md_cg/test_p1.py +5 -5
  149. package/md_cg/test_p11_consistency.py +161 -12
  150. package/md_cg/test_p26_refindex.py +2 -2
  151. package/md_cg/test_p27_docindex.py +777 -774
  152. package/md_cg/test_p28_refcheck.py +740 -13
  153. package/md_cg/test_p29_session_ingest_export.py +2 -2
  154. package/md_cg/test_p2_mcp.py +12 -1
  155. package/md_cg/test_p2_six_elements.py +463 -0
  156. package/md_cg/test_p30_maintain.py +33 -2
  157. package/md_cg/test_p31_insight.py +1 -0
  158. package/md_cg/test_p3_legacy_closure.py +433 -0
  159. package/md_cg/test_p4_freshness.py +680 -0
  160. package/md_cg/test_p8_subgraph_chain.py +14 -1
  161. package/md_cg/test_p9c_dedup_hints.py +276 -0
  162. package/md_cg/test_policy_required_ccg.py +426 -0
  163. package/md_cg/test_protocol.py +22 -0
  164. package/md_cg/test_rank_parity_score_mode.py +522 -0
  165. package/md_cg/test_read_face_input_gates.py +363 -0
  166. package/md_cg/test_read_face_semantics.py +489 -0
  167. package/md_cg/test_recall_face_guards.py +812 -0
  168. package/md_cg/test_rejected_credential_forms.py +188 -0
  169. package/md_cg/test_rejected_redact.py +146 -0
  170. package/md_cg/test_retr_s1b.py +2 -2
  171. package/md_cg/test_retr_s5.py +20 -7
  172. package/md_cg/test_review_conformance.py +21 -3
  173. package/md_cg/test_security_audit_v21.py +2 -2
  174. package/md_cg/test_semantic_canonical.py +5 -3
  175. package/md_cg/test_server_version.py +67 -0
  176. package/md_cg/test_sleep.py +611 -0
  177. package/md_cg/test_sleep_p1.py +784 -0
  178. package/md_cg/test_srcindex.py +171 -0
  179. package/md_cg/test_tenant_env_override_warn.py +63 -50
  180. package/md_cg/test_time_core_lint.py +968 -0
  181. package/md_cg/test_token_lowercase_form.py +315 -0
  182. package/md_cg/test_unify_default_off.py +701 -0
  183. package/md_cg/test_unify_scope.py +182 -0
  184. package/md_cg/test_v21r1_package_version.py +310 -0
  185. package/md_cg/test_writepipe.py +10 -4
  186. package/md_cg/tokens.py +225 -95
  187. package/md_cg/tool_face.py +4 -4
  188. package/md_cg/trust.py +49 -5
  189. package/md_cg/units.py +204 -6
  190. package/md_cg/weights.py +4 -4
  191. package/md_cg/whitebox_kb/aeis_core/time_core.py +8 -0
  192. package/md_cg/whitebox_kb/wisdom/knowledge_points.py +1 -1
  193. package/md_cg/writelimit.py +42 -8
  194. package/md_cg/writepipe.py +651 -554
  195. package/package.json +4 -2
  196. package/skills/plugin.json +1 -1
  197. package/src/bridge.ts +25 -2
  198. package/src/hooks.ts +229 -16
  199. package/src/init.ts +15 -1
  200. package/src/lib/prompt_safety.ts +88 -1
  201. package/utf8_boot.py +237 -0
  202. package/zcode/AGENTS.md +10 -10
@@ -0,0 +1,315 @@
1
+ # -*- coding: utf-8 -*-
2
+ """令牌形态「只认小写」的守卫(批次79,2026-09-28 使用者裁定)。
3
+
4
+ 运行:python -X utf8 -m md_cg.test_token_lowercase_form # 正常跑
5
+ python -X utf8 -m md_cg.test_token_lowercase_form --head-baseline # 判别力自证
6
+
7
+ 裁定原文:「不认大写。统一只认小写。」落点=**受理面单点** `tokens.parse_token`
8
+ (`verify_token` 的唯一入口)。改动前该函数返回 `role.lower()`、且对 token_id 不做
9
+ 任何形态检查——即受理面比识别面**更宽**:`md_cg` 自己的检测表
10
+ (`data/policy.json` forbidden #9 `\\btk_[0-9a-f]{8,}\\b`、`src/hooks.ts`
11
+ SENSITIVE_PATTERNS 的同形规则)**有意**不含 `A-F`,而受理面却会接受一个含大写的
12
+ token_id——检测面漏过的形态照样能通过校验,两面的松紧就倒挂了。
13
+
14
+ 本套断言分七组:
15
+ G1 放行面(真令牌通过;secret 含大写仍通过——base64url 必含大写,不得误伤)
16
+ G2 token_id 含大写 → 拒(`TK_` 前缀、全大写 hex、混合大小写 id 三种)
17
+ G3 role 含大写 → 拒(改动前 role.lower() 会**静默接受**)
18
+ G4 前缀含大写 → 拒(既有判据,钉住它别被后续重构放松)
19
+ G5 端到端(真临时令牌库:合法令牌过闸;同 secret 的大写变形令牌抛 TokenError,
20
+ 而不是落到「令牌不存在」这类下游软失败上——形态问题必须在形态层拒掉)
21
+ G6 生成面自证(**推论前提**:`issue` 只产小写 role/token_id ⇒ 大写形态没有合法来源)
22
+ G7 口径面(识别面**仍然**宽:禁表两条原样存在、hooks.ts 的 role 段仍 `[A-Za-z]+`;
23
+ 受理面已窄。二者关系=「识别面宽、受理面窄」,不是「两面都收紧」)
24
+ G8 三面口径(2026-09-28 使用者补裁「大写也转小写识别,以后写统一小写,现有的大写
25
+ 内容就不动了」):入参面宽容(role_spec/as_unit 的大写照旧归一,收窄只减不增)、
26
+ 令牌面从严(同为大写 role 的令牌字符串仍被拒——两面**有意**不同)、
27
+ 存量不迁移(test_p46 C2 那条存量断言必须还在)、写面统一小写(issue 产出恒小写)
28
+
29
+ **红基线自证(--head-baseline)**:把 `parse_token` 临时换回改动前的实现
30
+ (`return role.lower(), token_id, secret`),G2/G3/G5/G8d 必须转红(实测 11 红)。若红
31
+ 基线不红,说明这几条断言没有判别力(删掉新分支也照样全绿)。
32
+ """
33
+ from __future__ import annotations
34
+
35
+ import contextlib
36
+ import io
37
+ import json
38
+ import os
39
+ import re
40
+ import secrets
41
+ import shutil
42
+ import sys
43
+ import tempfile
44
+
45
+ from . import tokens
46
+ from .tokens import ROLE_SPECS, TokenError
47
+
48
+ _PASS = []
49
+ _FAIL = []
50
+
51
+
52
+ def ok(cond, msg, extra=""):
53
+ (_PASS if cond else _FAIL).append(msg)
54
+ print((" PASS " if cond else " FAIL ") + msg + ((" ← " + str(extra)) if (extra and not cond) else ""))
55
+
56
+
57
+ def _rejects(token: str, want: str = "") -> tuple:
58
+ """调 parse_token 判拒:返回 (是否被拒, 错误文本)。"""
59
+ try:
60
+ tokens.parse_token(token)
61
+ return False, ""
62
+ except TokenError as e:
63
+ t = str(e)
64
+ return (want in t), t
65
+ except Exception as e: # noqa: BLE001
66
+ return False, "%s: %s" % (type(e).__name__, e)
67
+
68
+
69
+ # ---------------------------------------------------------------- 旧实现(红基线用)
70
+ def _old_parse_token(token: str):
71
+ """改动前的实现原文——仅用于 --head-baseline 的判别力自证,不参与生产。"""
72
+ parts = (token or "").strip().split(".")
73
+ if len(parts) != 4 or parts[0] != tokens.PREFIX:
74
+ raise TokenError("令牌格式非法(应为 mdcg1.<role>.<token_id>.<secret>)")
75
+ _, role, token_id, secret = parts
76
+ if not role or not token_id or not secret:
77
+ raise TokenError("令牌字段缺失")
78
+ return role.lower(), token_id, secret
79
+
80
+
81
+ # 合成件:secret 刻意含大写(与 token_urlsafe 的真实产物同分布),用来把「只认小写」
82
+ # 的适用面钉在 role/token_id 两段上,而不是顺手把 secret 也收紧。值里带 `dummy`/`fake`
83
+ # 标记是**发版门禁的既有约定**(scripts/check_publish_artifact.py::FAKE_TOKEN_MARKERS):
84
+ # 测试里的合成凭据必须一眼可辨为假值,否则 R1 凭据面会把本文件判成真实泄露(本批次首跑
85
+ # 实测命中 APIKEY_LITERAL + TOKEN_MDCG 两条,即按该约定改正而成现在这样)。
86
+ DUMMY_SECRET_UC = "Fake-Dummy-aB3xY9zQ7wV1uT5sR2pN8mL4kJ6"
87
+ HEX_ID = "9f3a2b8c7d4e" # token_hex(6) 形态
88
+
89
+
90
+ def _tok(role: str, tid: str, secret: str = None) -> str:
91
+ """拼一条令牌:经 make_token 而非字面量——测试源码里不留「像真凭据」的整串。"""
92
+ return tokens.make_token(role, tid, secret or DUMMY_SECRET_UC)
93
+
94
+
95
+ def g1_allow():
96
+ print("\n[G1] 放行面:合法令牌与含大写 secret 的令牌都通过")
97
+ good = _tok("designer", "tk_" + HEX_ID)
98
+ role, token_id, secret = tokens.parse_token(good)
99
+ ok(role == "designer" and token_id == "tk_" + HEX_ID and secret == DUMMY_SECRET_UC,
100
+ "G1a 合法令牌原样通过(role/token_id/secret 都不改写)")
101
+ ok(secret == DUMMY_SECRET_UC and any(c.isupper() for c in secret),
102
+ "G1b secret 含大写仍然通过(base64url 必含大写,不得误伤)",
103
+ "样例 secret 大写字母数=%d" % sum(1 for c in secret if c.isupper()))
104
+ ok(tokens.parse_token(_tok("guest", "tk_" + "0123456789ab"))[0] == "guest",
105
+ "G1c 另一角色同样通过(避免只对 designer 生效的伪断言)")
106
+
107
+
108
+ def g2_id_uppercase():
109
+ print("\n[G2] token_id 含大写 → 拒(受理面比识别面宽的那个洞)")
110
+ for label, tid in (("TK_ 前缀大写", "TK_" + HEX_ID),
111
+ ("全大写 hex", "tk_" + HEX_ID.upper()),
112
+ ("混合大小写", "tk_9f3a2B8c7d4e")):
113
+ hit, err = _rejects(_tok("designer", tid), want="小写")
114
+ ok(hit, "G2 token_id %s → 拒且文案点明「小写」" % label, err)
115
+ ok(tokens.parse_token(_tok("designer", "tk_" + HEX_ID))[1] == "tk_" + HEX_ID,
116
+ "G2 正对照:同一形态换成小写即通过(拒的是大小写,不是 tk_ 前缀)")
117
+
118
+
119
+ def g3_role_uppercase():
120
+ print("\n[G3] role 含大写 → 拒(改动前 role.lower() 静默接受)")
121
+ for label, role in (("全大写", "DESIGNER"), ("首字母大写", "Designer"),
122
+ ("别名大写", "VERIFIER")):
123
+ hit, err = _rejects(_tok(role, "tk_" + HEX_ID), want="小写")
124
+ ok(hit, "G3 role %s → 拒且文案点明「小写」" % label, err)
125
+ ok(tokens.parse_token(_tok("designer", "tk_" + HEX_ID))[0] == "designer",
126
+ "G3 正对照:全小写 role 通过且不被改写")
127
+
128
+
129
+ def g4_prefix():
130
+ print("\n[G4] 前缀含大写 → 拒(既有判据,钉住别被后续重构放松)")
131
+ hit, err = _rejects(_tok("designer", "tk_" + HEX_ID).replace("mdcg1", "MDCG1", 1), want="格式非法")
132
+ ok(hit, "G4a 前缀 MDCG1 → 拒", err)
133
+ hit2, err2 = _rejects(_tok("designer", "tk_" + HEX_ID).replace("mdcg1", "Mdcg1", 1), want="格式非法")
134
+ ok(hit2, "G4b 前缀 Mdcg1 → 拒", err2)
135
+ ok(tokens.PREFIX == tokens.PREFIX.lower() and tokens.PREFIX == "mdcg1",
136
+ "G4c PREFIX 常量本身即小写(前缀大小写敏感的前提)", tokens.PREFIX)
137
+
138
+
139
+ def g5_end_to_end():
140
+ print("\n[G5] 端到端:真令牌库上,合法令牌过闸、大写变形令牌在形态层即拒")
141
+ root = tempfile.mkdtemp(prefix="mdcg_toklower_")
142
+ tf = os.path.join(root, "_tokens.json")
143
+ try:
144
+ rec = tokens.issue("designer", path=tf)
145
+ tok = rec["token"]
146
+ p = tokens.verify_token(tok, path=tf)
147
+ ok(p.role == "designer", "G5a 合法令牌 verify_token 通过(role=%s)" % p.role)
148
+ role, tid, sec = tokens.parse_token(tok)
149
+ up_id = tokens.make_token(role, tid.upper(), sec)
150
+ try:
151
+ tokens.verify_token(up_id, path=tf)
152
+ ok(False, "G5b 大写 token_id 的变形令牌必须被拒", "竟然通过了")
153
+ except TokenError as e:
154
+ ok("小写" in str(e), "G5b 大写 token_id 变形 → TokenError 且文案指向形态(非下游软失败)", str(e))
155
+ up_role = tokens.make_token(role.upper(), tid, sec)
156
+ try:
157
+ tokens.verify_token(up_role, path=tf)
158
+ ok(False, "G5c 大写 role 的变形令牌必须被拒", "竟然通过了")
159
+ except TokenError as e:
160
+ ok("小写" in str(e), "G5c 大写 role 变形 → TokenError 同上", str(e))
161
+ msg = _reject_text(up_id)
162
+ ok("小写" in msg and "不存在" not in msg and "不匹配" not in msg,
163
+ "G5d 拒因是形态层(不含「令牌不存在/密钥不匹配」这类下游软失败文案)", msg)
164
+ finally:
165
+ shutil.rmtree(root, ignore_errors=True)
166
+
167
+
168
+ def _reject_text(token: str) -> str:
169
+ """取原始错误文本(不做 must-contain 判据),供 G5d 反向核对。"""
170
+ try:
171
+ tokens.parse_token(token)
172
+ return "(未被拒)"
173
+ except TokenError as e:
174
+ return str(e)
175
+
176
+
177
+ def g6_issuer():
178
+ print("\n[G6] 生成面自证:大写形态没有合法来源(推论的**前提**)")
179
+ hexes = [secrets.token_hex(6) for _ in range(64)]
180
+ ok(all(re.fullmatch(r"[0-9a-f]{12}", h) for h in hexes),
181
+ "G6a secrets.token_hex(6) 只产小写 hex(64 次采样零违例)",
182
+ [h for h in hexes if not re.fullmatch(r"[0-9a-f]{12}", h)][:3])
183
+ ok(all(k == k.lower() for k in ROLE_SPECS),
184
+ "G6b ROLE_SPECS 全部键为小写(签发面 role 取值域即小写)", sorted(ROLE_SPECS))
185
+ # 签发面另有一层归一:issue 经 normalize_role 把小写化做在「我方自己发」这一侧。
186
+ # 这不算「认大写」——产出物恒为小写;受理面收的是外部来的串,没有这层信任。
187
+ # 结构断言(不写整串字面量:测试源码里不留「像真凭据」的串,见 DUMMY_SECRET_UC 处注释)
188
+ ok(tokens.make_token("designer", "tk_" + HEX_ID, DUMMY_SECRET_UC).split(".") ==
189
+ [tokens.PREFIX, "designer", "tk_" + HEX_ID, DUMMY_SECRET_UC],
190
+ "G6c make_token 是纯拼接(四段原样,不在拼接层做大小写变换)")
191
+ root = tempfile.mkdtemp(prefix="mdcg_toklower_iss_")
192
+ try:
193
+ tok = tokens.issue("DESIGNER", path=os.path.join(root, "_tokens.json"))["token"]
194
+ ok(tok.startswith("mdcg1.designer."),
195
+ "G6d 即便调用方传大写 role,issue 产出仍是小写(签发面归一生效处)", tok[:24])
196
+ finally:
197
+ shutil.rmtree(root, ignore_errors=True)
198
+
199
+
200
+ def _hooks_rules() -> list:
201
+ """从 src/hooks.ts 源码里取出现行的 `/…/g` 字面(JS 与 Python 在这些构造上同义)。"""
202
+ repo = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
203
+ src = io.open(os.path.join(repo, "src", "hooks.ts"), encoding="utf-8").read().splitlines()
204
+ out = []
205
+ for i, line in enumerate(src, 1):
206
+ m = re.search(r"re:\s*/(.+)/[a-z]*\s*,", line)
207
+ if m:
208
+ out.append((i, m.group(1), line))
209
+ return out
210
+
211
+
212
+ def g7_detection_face():
213
+ print("\n[G7] 口径面:识别面**仍然**宽、受理面已窄(不是两面都收紧)")
214
+ repo = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
215
+ pol = json.loads(io.open(os.path.join(repo, "data", "policy.json"), encoding="utf-8").read())
216
+ forb = pol["forbidden"]
217
+ vals = list(forb.values()) if isinstance(forb, dict) else list(forb)
218
+ ok(any(v.strip() == r"\btk_[0-9a-f]{8,}\b" for v in vals),
219
+ "G7a 禁表 id 规则原样保留(未顺手加 A-F / re.I——那会把「只认小写」变成误报源)")
220
+ ok(any("mdcg1" in v for v in vals),
221
+ "G7b 禁表全形态令牌规则原样保留")
222
+ ok(all("(?i)" not in v for v in vals if "tk_" in v),
223
+ "G7c 令牌相关禁用规则不带 (?i)(大小写敏感是刻意的)")
224
+ rules = _hooks_rules()
225
+ full = [(ln, p) for ln, p, _ in rules if "mdcg1" in p]
226
+ bare = [(ln, p) for ln, p, _ in rules if "tk_" in p and "mdcg1" not in p]
227
+ ok(len(full) == 1 and len(bare) == 1,
228
+ "G7d hooks.ts 恰好各有一条全形态/裸 id 令牌规则(行号=%s)"
229
+ % ([ln for ln, _ in full] + [ln for ln, _ in bare]))
230
+ if not full or not bare:
231
+ return
232
+ up_tok = _tok("DESIGNER", ("tk_" + HEX_ID).upper(), DUMMY_SECRET_UC.upper())
233
+ lo_tok = _tok("designer", "tk_" + HEX_ID)
234
+ ok(re.search(full[0][1], up_tok) is not None and re.search(full[0][1], lo_tok) is not None,
235
+ "G7e 插件全形态规则**行为上宽**:含大写的令牌也整条命中(拦得住)",
236
+ full[0][1])
237
+ ok(re.search(bare[0][1], "tk_" + HEX_ID) is not None,
238
+ "G7f 插件裸 id 规则匹配小写 hex id", bare[0][1])
239
+ ok(re.search(bare[0][1], "tk_" + HEX_ID.upper()) is None,
240
+ "G7g 插件裸 id 规则**有意**不匹配大写 hex id(识别面固有缺口=本批次修的另一半)",
241
+ bare[0][1])
242
+ # 缺口闭合:识别面漏掉的大写形态,受理面必须拒 ⇒ 两面组合后无缝隙。
243
+ # 反过来说,若受理面像改动前那样认大写,G7g 的形态就是一条真的凭据通道。
244
+ hit, _ = _rejects(_tok("designer", "tk_" + HEX_ID.upper()))
245
+ ok(hit, "G7h 识别面漏(大写 hex)⇔ 受理面拒:不存在「检测漏 + 受理收」的缝隙")
246
+
247
+
248
+ def g8_three_faces():
249
+ """三面口径(2026-09-28 使用者补裁):入参面宽容、令牌面从严、写面统一小写。
250
+
251
+ 裁定原文:「已有测试把大写归一当预期行为。大写也转小写识别,以后写统一小写,
252
+ 现有的大写内容就不动了。」故本组**同时**钉住三件事——G8a/G8b 是「不要顺手收紧入参面」
253
+ (收紧会打断存量语义与 `test_p46_unit_scope.py` 的 C2),G8c 是「令牌面照旧拒」
254
+ (批次79 的裁定不被本组软化),G8e 是「存量断言不许被删」。
255
+ """
256
+ print("\n[G8] 三面口径:入参面宽容 / 令牌面从严 / 写面统一小写")
257
+ spec = tokens.role_spec("RECORD") # 大写 role 名照常解析
258
+ ok(spec.get("clearance_cap") is not None or isinstance(spec, dict),
259
+ "G8a 入参面宽容:role_spec('RECORD') 照常解析(大写经 strip+lower+别名归一)")
260
+ ok(tokens.normalize_role("RECORD") == "record"
261
+ and tokens.normalize_role(" ReCorder ") == "record",
262
+ "G8b 入参面宽容:'RECORD' / ' ReCorder ' 均归一为 record(别名 + 空白一并归一)")
263
+ from .security import Principal as _P
264
+ owner = _P(role="designer", actor="t", can_write=True, can_admin=True, clearance="secret")
265
+ narrowed = tokens.narrowed_principal(owner, "RECORD")
266
+ ok(narrowed.unit == "record" and narrowed.can_admin is False and owner.can_admin is True,
267
+ "G8c 大写 as_unit 只减不增:unit=record、can_admin 恒 False(无提权面)")
268
+ hit, err = _rejects(_tok("RECORD", "tk_" + HEX_ID), want="小写")
269
+ ok(hit, "G8d 令牌面从严(对照):同为大写 role 的**令牌字符串**被拒——两面有意不同",
270
+ err)
271
+ src = io.open(os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
272
+ "md_cg", "test_p46_unit_scope.py"), encoding="utf-8").read()
273
+ ok("大小写归一(RECORD -> record)" in src,
274
+ "G8e 存量不迁移:test_p46_unit_scope.py 的 C2 存量断言仍在(口径的既有凭据未被改掉)")
275
+ tok = tokens.issue("designer", path=os.path.join(tempfile.mkdtemp(prefix="mdcg_cap_"),
276
+ "_tokens.json"))["token"]
277
+ parts = tok.split(".")
278
+ ok(parts[1] == parts[1].lower() and parts[2] == parts[2].lower(),
279
+ "G8f 写面统一小写:issue 新签发的 role/token_id 段恒小写(以后写一律小写)")
280
+
281
+
282
+ def _run_groups() -> int:
283
+ _PASS.clear(); _FAIL.clear()
284
+ for g in (g1_allow, g2_id_uppercase, g3_role_uppercase, g4_prefix, g5_end_to_end,
285
+ g6_issuer, g7_detection_face, g8_three_faces):
286
+ g()
287
+ return len(_FAIL)
288
+
289
+
290
+ def main() -> int:
291
+ if sys.stdout.encoding and sys.stdout.encoding.lower().startswith("cp"):
292
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
293
+ base = "--head-baseline" in sys.argv
294
+ if base:
295
+ print("!! 红基线模式:parse_token 临时替换回改动前实现(role.lower()、不查 token_id 形态)")
296
+ print(" 期望 G2/G3/G5/G8d 转红\n")
297
+ tokens.parse_token = _old_parse_token
298
+ globals()["tokens"] = tokens
299
+ groups = (g1_allow, g2_id_uppercase, g3_role_uppercase, g4_prefix, g5_end_to_end,
300
+ g6_issuer, g7_detection_face, g8_three_faces)
301
+ for g in groups:
302
+ g()
303
+ if base:
304
+ print("\n== 红基线判定:应有失败 ==")
305
+ print("红基线失败数 = %d(>0 才算断言有判别力)" % len(_FAIL))
306
+ for f in _FAIL:
307
+ print(" 红:", f)
308
+ return 0 if _FAIL else 1
309
+ print("\n令牌形态(只认小写)守卫:%d 通过,%d 失败" % (len(_PASS), len(_FAIL)))
310
+ return 0 if not _FAIL else 1
311
+
312
+
313
+ if __name__ == "__main__":
314
+ with contextlib.redirect_stdout(sys.stdout):
315
+ sys.exit(main())