@furongjun1999/dsh-memory 0.4.8 → 0.4.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -26
- package/codebuddy/CODEBUDDY.md +196 -195
- package/codebuddy/README.md +13 -1
- package/codebuddy/mcp.json +9 -0
- package/docs/GBrain/345/217/257/345/200/237/351/211/264/347/202/271_/347/201/265/346/236/242/350/220/275/347/202/271/344/272/244/346/216/245_20260919.md +169 -0
- package/docs/README.md +1 -1
- package/docs/discipline/harnesses.yaml +18 -7
- package/docs/discipline/templates/full.md.tmpl +4 -3
- package/docs/experiments/linkref_backfill/candidates_20260917.json +726 -0
- package/docs/experiments/linkref_backfill/candidates_internal_20260917.json +602 -0
- package/docs/experiments/linkref_backfill/candidates_internal_v2.json +603 -0
- package/docs/experiments/linkref_backfill/candidates_secret_20260917.json +884 -0
- package/docs/experiments/linkref_backfill/candidates_secret_v2.json +789 -0
- package/docs/hive//345/244/232/347/253/257harness/351/200/232/344/277/241/345/245/221/347/272/246_v0.1.md +42 -0
- package/docs/hive//346/243/200/347/264/242/346/224/266/346/225/233/345/256/236/346/265/213/344/270/216S1b/350/256/276/350/256/241_v0.1.md +43 -0
- package/docs/hive//346/243/200/347/264/242/350/267/257/345/276/204/344/270/216/350/256/244/347/237/245/347/273/223/346/236/204/345/245/221/347/272/246_v0.1.md +102 -0
- package/docs/hive//347/234/237/345/256/236/345/272/223/347/253/257/345/210/260/347/253/257/345/256/236/346/265/213_S1b/344/270/216/345/217/254/345/233/236/346/235/203/350/241/241_v0.1.md +57 -0
- package/docs/hive//347/234/237/345/256/236/345/272/223/347/253/257/345/210/260/347/253/257/345/256/236/346/265/213_S7/345/200/222/346/216/222/345/200/231/351/200/211/345/261/202_v0.1.md +126 -0
- package/docs/hive//350/234/202/345/267/242/345/217/214/345/256/236/344/276/213/344/272/222/351/252/214_/350/256/276/350/256/241/345/256/232/347/250/277.md +503 -0
- package/docs/mdcg/D_meta_/345/267/245/347/250/213/345/214/226/346/226/271/346/241/210_v0.2.md +216 -0
- package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.5.md +631 -625
- package/docs/mdcg//344/273/243/347/240/201/350/257/204/345/256/241/344/270/216/346/235/241/344/273/266/345/214/226/346/263/250/351/207/212_/345/245/221/347/272/246_v0.1.md +82 -0
- package/docs/mdcg//345/205/250/345/272/223/344/273/243/347/240/201/350/257/204/345/256/241/344/270/216/346/235/241/344/273/266/345/214/226/346/263/250/351/207/212_/350/256/241/345/210/222_v0.1.md +600 -0
- package/docs/mdcg//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +47 -45
- package/docs/mdcg//345/255/220/344/273/243/347/220/206/351/205/215/347/275/256/346/240/207/345/207/{206_v0.4.md → 206_v0.5.md} +92 -4
- package/docs/mdcg//347/201/265/346/236/242/350/256/260/345/277/206/345/212/250/350/257/215/345/215/217/350/256/256_v1.0-draft.md +172 -0
- package/docs/mdcg//347/216/257/344/272/214_/347/231/275/347/256/261/345/241/253/345/205/205/346/265/201/346/260/264/347/272/277_v0.1.md +31 -0
- package/docs/mdcg//350/267/250/347/253/257/351/252/214/350/257/201/344/270/216/345/220/214/346/255/245/345/215/217/350/256/256_v0.1.md +93 -0
- package/docs/theory//345/271/266/345/217/221/345/277/205/347/204/266/346/200/247/347/220/206/350/256/272_v0.2.md +2 -2
- package/docs/theory//347/220/206/350/256/272_/346/234/272/345/210/266_/344/273/243/347/240/201_/345/256/236/351/252/214_/347/274/272/345/217/243/347/237/251/351/230/265_v0.1.md +3 -3
- package/docs/theory//350/256/244/347/237/245/344/273/243/347/220/206/344/270/216/346/224/266/346/225/233/347/273/223/346/236/204_/346/235/241/344/273/266/350/256/272/351/207/215/346/236/204_v0.1.md +2 -2
- package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +434 -433
- package/docs//347/201/265/346/236/242/350/207/252/346/210/221/346/224/271/350/277/233/345/267/245/344/275/234/350/256/241/345/210/222_/345/244/226/351/203/250/347/240/224/347/251/266/347/263/273/345/210/227/345/220/270/346/224/266_v1_20260919.md +286 -0
- package/dsh/README.md +33 -0
- package/dsh/cordis.yml.example +13 -7
- package/dsh/hive-mcp-probe.mjs +94 -0
- package/dsh/hive-mcp.example.yml +62 -0
- package/dsh/update-lingshu.bat +11 -0
- package/dsh/update-lingshu.ps1 +337 -0
- package/lib/hooks.d.ts +3 -0
- package/lib/hooks.js +17 -23
- package/lib/index.d.ts +4 -2
- package/lib/index.js +29 -6
- package/lib/lib/datapath.d.ts +76 -1
- package/lib/lib/datapath.js +199 -13
- package/lib/lib/mdcg_client.d.ts +40 -3
- package/lib/lib/mdcg_client.js +46 -22
- package/lib/lib/mutual.js +4 -4
- package/lib/lib/token_store.js +4 -5
- package/md_cg/audit.py +17 -2
- package/md_cg/autonomy.py +86 -15
- package/md_cg/backfill.py +36 -1
- package/md_cg/backfill_bigdomain.py +34 -0
- package/md_cg/bench6_arms.py +28 -1
- package/md_cg/bench6_common.py +10 -1
- package/md_cg/bench6_competitors.py +6 -1
- package/md_cg/bench_axis_domain.py +9 -1
- package/md_cg/bench_blind_comp.py +7 -1
- package/md_cg/bench_en_atoms_public.py +9 -0
- package/md_cg/bench_governance.py +348 -0
- package/md_cg/bench_lme_zh.py +16 -1
- package/md_cg/bench_locomo.py +2 -1
- package/md_cg/bench_locomo_zh.py +16 -1
- package/md_cg/bench_locomo_zh_public.py +4 -1
- package/md_cg/bench_longmem.py +2 -1
- package/md_cg/bench_membench.py +27 -1
- package/md_cg/bench_p0.py +4 -1
- package/md_cg/bench_progressive.py +13 -1
- package/md_cg/bench_role_views.py +238 -0
- package/md_cg/bench_task_ab.py +8 -1
- package/md_cg/bench_task_ab_llm.py +13 -1
- package/md_cg/bench_unified_en.py +6 -1
- package/md_cg/bench_zh_mad.py +20 -1
- package/md_cg/blindspot_tickets.py +123 -0
- package/md_cg/branches.py +12 -1
- package/md_cg/build_postings.py +73 -0
- package/md_cg/ccgc.py +67 -2
- package/md_cg/census.py +5 -1
- package/md_cg/chain.py +24 -3
- package/md_cg/codeindex.py +134 -17
- package/md_cg/coldverify.py +265 -0
- package/md_cg/comment_gate.py +338 -0
- package/md_cg/cond_compose.py +190 -0
- package/md_cg/cond_facts.py +155 -0
- package/md_cg/cond_template.json +107 -0
- package/md_cg/condition_anchor.py +143 -0
- package/md_cg/conformance.py +69 -4
- package/md_cg/consistency.py +24 -1
- package/md_cg/consolidate.py +53 -2
- package/md_cg/corpus.py +4 -0
- package/md_cg/crosscheck.py +42 -2
- package/md_cg/crypto.py +35 -1
- package/md_cg/d_meta.py +310 -0
- package/md_cg/datapath.py +201 -26
- package/md_cg/docindex.py +122 -1
- package/md_cg/eval_common.py +29 -1
- package/md_cg/evidence.py +27 -1
- package/md_cg/evolution.py +21 -1
- package/md_cg/export.py +11 -1
- package/md_cg/forgetting.py +23 -1
- package/md_cg/fsutil.py +18 -1
- package/md_cg/hotcache.py +214 -0
- package/md_cg/hyperedge.py +251 -0
- package/md_cg/identity.py +18 -1
- package/md_cg/insight.py +17 -1
- package/md_cg/lexicon/build_cedict_en_zh.py +9 -0
- package/md_cg/lexicon/build_standard_en.py +171 -168
- package/md_cg/lexicon/expand_en_zh.py +6 -0
- package/md_cg/lifecycle.py +12 -1
- package/md_cg/linkref.py +281 -0
- package/md_cg/links.py +29 -1
- package/md_cg/mcp_server.py +362 -43
- package/md_cg/md_whitebox.py +53 -1
- package/md_cg/mdcg.py +1003 -27
- package/md_cg/mdcos.py +558 -36
- package/md_cg/metacognition.py +37 -2
- package/md_cg/migrate.py +4 -0
- package/md_cg/migrate_aeis.py +221 -213
- package/md_cg/migrate_roleplay.py +8 -0
- package/md_cg/migrate_wisdom_graph.py +14 -1
- package/md_cg/mreview/__main__.py +3 -0
- package/md_cg/mreview/bundle.py +8 -0
- package/md_cg/mreview/candidates.py +9 -0
- package/md_cg/mreview/govern.py +21 -1
- package/md_cg/mreview/locate.py +34 -0
- package/md_cg/mreview/pipeline.py +29 -1
- package/md_cg/mreview/ruleset.py +16 -1
- package/md_cg/nodefile.py +233 -3
- package/md_cg/pooling.py +23 -1
- package/md_cg/postings.py +298 -0
- package/md_cg/predict.py +89 -9
- package/md_cg/progressive.py +3 -0
- package/md_cg/protect.py +14 -1
- package/md_cg/protocol.py +372 -0
- package/md_cg/provenance.py +262 -0
- package/md_cg/reach.py +453 -0
- package/md_cg/refindex.py +47 -2
- package/md_cg/refine.py +20 -1
- package/md_cg/roleviews.py +89 -0
- package/md_cg/routing.py +76 -0
- package/md_cg/scrub.py +63 -2
- package/md_cg/security.py +26 -1
- package/md_cg/self_state.py +64 -1
- package/md_cg/selfreport.py +151 -0
- package/md_cg/semantic/canonical.py +5 -0
- package/md_cg/semantic/en_normalizer.py +364 -355
- package/md_cg/semantic/zh_en_atoms.py +139 -136
- package/md_cg/signer.py +41 -1
- package/md_cg/sources.py +583 -547
- package/md_cg/statushdr.py +179 -0
- package/md_cg/stg.py +59 -18
- package/md_cg/subgraph.py +23 -0
- package/md_cg/sustain.py +56 -1
- package/md_cg/tasks.py +26 -2
- package/md_cg/test_autonomy.py +26 -0
- package/md_cg/test_bench_governance.py +102 -0
- package/md_cg/test_blindspot_tickets.py +166 -0
- package/md_cg/test_ccgc.py +10 -0
- package/md_cg/test_codeindex.py +338 -0
- package/md_cg/test_comment_gate.py +187 -0
- package/md_cg/test_cond_compose_anchors.py +76 -0
- package/md_cg/test_condition_anchor.py +82 -0
- package/md_cg/test_d_meta.py +412 -0
- package/md_cg/test_datapath_root.py +188 -0
- package/md_cg/test_gain_gate.py +47 -1
- package/md_cg/test_hot_cold.py +187 -0
- package/md_cg/test_hyperedge.py +245 -0
- package/md_cg/test_linkref.py +306 -0
- package/md_cg/test_md_access_parity.py +15 -3
- package/md_cg/test_mr_m1.py +108 -18
- package/md_cg/test_mr_m3.py +8 -1
- package/md_cg/test_p26_refindex.py +49 -20
- package/md_cg/test_p27_docindex.py +236 -2
- package/md_cg/test_p2_mcp.py +1 -1
- package/md_cg/test_p31_insight.py +24 -0
- package/md_cg/test_p44_md_whitebox.py +14 -1
- package/md_cg/test_protocol.py +243 -0
- package/md_cg/test_reach.py +378 -0
- package/md_cg/test_reach_keys.py +201 -0
- package/md_cg/test_reach_meta_exits.py +145 -0
- package/md_cg/test_read_clip.py +8 -4
- package/md_cg/test_retr_s1.py +340 -0
- package/md_cg/test_retr_s1b.py +209 -0
- package/md_cg/test_retr_s3.py +194 -0
- package/md_cg/test_retr_s4.py +163 -0
- package/md_cg/test_retr_s5.py +200 -0
- package/md_cg/test_retr_s6.py +157 -0
- package/md_cg/test_retr_s7.py +385 -0
- package/md_cg/test_retr_s8_time.py +316 -0
- package/md_cg/test_retr_s9_edges.py +286 -0
- package/md_cg/test_retr_s9_entity_ctx.py +175 -0
- package/md_cg/test_review_conformance.py +59 -2
- package/md_cg/test_role_views.py +354 -0
- package/md_cg/test_subproc_encoding.py +188 -0
- package/md_cg/test_trust.py +361 -0
- package/md_cg/test_units_poll.py +71 -0
- package/md_cg/test_v14_fixes.py +397 -0
- package/md_cg/test_validity_filter.py +280 -0
- package/md_cg/test_wisdom_md_store.py +7 -3
- package/md_cg/test_writepipe.py +5 -1
- package/md_cg/theory.py +16 -1
- package/md_cg/tokens.py +40 -8
- package/md_cg/tool_face.py +13 -2
- package/md_cg/trust.py +943 -0
- package/md_cg/twophase.py +12 -1
- package/md_cg/units.py +132 -10
- package/md_cg/vision_evidence.py +24 -1
- package/md_cg/weights.py +24 -1
- package/md_cg/whitebox.py +32 -1
- package/md_cg/whitebox_kb/data/verify_cache.json +21210 -365
- package/md_cg/whitebox_kb/data/verify_savings.jsonl +5078 -0
- package/md_cg/whitebox_kb/wisdom/audit_log/chain_heat.json +10 -10
- package/md_cg/whitebox_kb/wisdom/code_compose.py +113 -6
- package/md_cg/whitebox_kb/wisdom/code_solidified.json +1 -1
- package/md_cg/whitebox_kb/wisdom/verifier.py +340 -55
- package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db +0 -0
- package/md_cg/writelimit.py +18 -4
- package/md_cg/writepipe.py +178 -7
- package/package.json +2 -2
- package/skills/skills/designer-perspective/scripts/__pycache__/designer.cpython-310.pyc +0 -0
- package/skills/skills/designer-perspective/scripts/designer.py +17 -1
- package/skills/skills/designer-perspective/tests/selftest.py +3 -1
- package/src/hooks.ts +17 -21
- package/src/index.ts +33 -6
- package/src/lib/datapath.ts +211 -13
- package/src/lib/mdcg_client.ts +64 -25
- package/src/lib/mutual.ts +411 -411
- package/src/lib/token_store.ts +4 -5
- package/zcode/AGENTS.md +196 -195
- package/zcode/README.md +4 -0
- package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-shm +0 -0
- package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-wal +0 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""test_subproc_encoding.py · 子进程**文本解码口径**的机械守卫(第15条纪律的守卫面)
|
|
3
|
+
|
|
4
|
+
现场(2026-09-20 取证):桥的子进程被注入 `PYTHONIOENCODING=utf-8`,其**后代**因此往
|
|
5
|
+
管道写 UTF-8;但后代读子进程输出时默认 text 编码取自 locale(Windows=cp936),两侧
|
|
6
|
+
口径不一致 → 读线程崩死、子进程诊断静默丢失:
|
|
7
|
+
|
|
8
|
+
Exception in thread Thread-N (_readerthread):
|
|
9
|
+
UnicodeDecodeError: 'gbk' codec can't decode byte 0x82 in position 181
|
|
10
|
+
|
|
11
|
+
两道防线(本守卫管第二道):
|
|
12
|
+
① 运行期:桥子进程一律以 `PYTHONUTF8=1` 启动(PEP 540,默认 text 编码=utf-8);
|
|
13
|
+
见 `src/lib/mdcg_client.ts` 的 `mdcgChildEnv()` 与 `test/python-utf8-mode.test.ts`。
|
|
14
|
+
② 源码:**任何**文本模式子进程读取都必须显式声明 `encoding=`(配 `errors=`),
|
|
15
|
+
不依赖 locale——脚本/工具也可能被非桥路径启动,那时没有 ① 的保护。
|
|
16
|
+
|
|
17
|
+
判据(**AST 判定,非文本窗口**——2026-09-20 改进):以 `ast` 解析每个 git 跟踪的 .py,
|
|
18
|
+
对**真实的调用节点**判定:
|
|
19
|
+
· 子进程调用 = `subprocess.<run|Popen|check_output|call|check_call|getoutput|getstatusoutput>`
|
|
20
|
+
(含 `import subprocess as sp` 与 `from subprocess import run` 两种别名形态)
|
|
21
|
+
· 文本模式 = 调用处出现 `text=<非 False>` / `universal_newlines=<非 False>` / `encoding=`
|
|
22
|
+
· 违例 = 文本模式却无 `encoding=` 实参;`**kwargs` 透传且**该调用确有管道**
|
|
23
|
+
(`capture_output` 或 stdout/stderr=PIPE)而无 `encoding=` 报「不可判定」——口径无法
|
|
24
|
+
确认即不合格,须显式声明;`os.popen` 直接违例(该 API 无法声明编码)。
|
|
25
|
+
注:输出落 DEVNULL/日志文件且无管道的 `Popen` **不判**——它根本不经过解码器。
|
|
26
|
+
> 之所以不用文本窗口:窗口会把**字符串字面量与文档样例**当调用(守卫曾因此自伤,
|
|
27
|
+
> 把自身源码里的 `"os.popen("` 字串判为违例)——AST 只认语法意义上的调用,无此缺陷。
|
|
28
|
+
|
|
29
|
+
自检:被检查的文本模式调用点低于 FLOOR 视为失败——防止「扫描范围意外为空 → 假绿」。
|
|
30
|
+
"""
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import ast
|
|
34
|
+
import os
|
|
35
|
+
import subprocess
|
|
36
|
+
import sys
|
|
37
|
+
|
|
38
|
+
FLOOR = 12 # 本仓文本模式子进程调用点的下限(低于它说明扫描面失效了)
|
|
39
|
+
SUB_FUNCS = {"run", "Popen", "check_output", "call", "check_call",
|
|
40
|
+
"getoutput", "getstatusoutput"}
|
|
41
|
+
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# 生效条件:无入参;返回仓根下 git 跟踪的 .py 相对路径列表(git 不可用时返回 None,调用方据此跳过并说明)。
|
|
45
|
+
def _tracked_py() -> list[str] | None:
|
|
46
|
+
try:
|
|
47
|
+
r = subprocess.run(["git", "-C", ROOT, "ls-files", "*.py"],
|
|
48
|
+
capture_output=True, text=True,
|
|
49
|
+
encoding="utf-8", errors="replace")
|
|
50
|
+
except OSError:
|
|
51
|
+
return None
|
|
52
|
+
if r.returncode != 0:
|
|
53
|
+
return None
|
|
54
|
+
return [ln.strip() for ln in r.stdout.splitlines() if ln.strip()]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# 生效条件:v 为 ast 常量节点时按其布尔取反判定;非常量(变量/表达式)一律按 True 保守处理。
|
|
58
|
+
def _truthy(v: ast.AST | None) -> bool:
|
|
59
|
+
if isinstance(v, ast.Constant):
|
|
60
|
+
return bool(v.value)
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class _Scanner(ast.NodeVisitor):
|
|
65
|
+
"""单文件扫描器:先收别名导入,再按 AST 调用节点判文本模式与 encoding 声明。"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, rel: str, bad: list[str]) -> None:
|
|
68
|
+
self.rel = rel
|
|
69
|
+
self.bad = bad
|
|
70
|
+
self.sub_aliases: set[str] = set()
|
|
71
|
+
self.func_aliases: dict[str, str] = {}
|
|
72
|
+
self.checked = 0
|
|
73
|
+
|
|
74
|
+
def visit_Import(self, node: ast.Import) -> None:
|
|
75
|
+
for a in node.names:
|
|
76
|
+
if a.name == "subprocess":
|
|
77
|
+
self.sub_aliases.add(a.asname or "subprocess")
|
|
78
|
+
self.generic_visit(node)
|
|
79
|
+
|
|
80
|
+
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
|
|
81
|
+
if node.module == "subprocess":
|
|
82
|
+
for a in node.names:
|
|
83
|
+
if a.name in SUB_FUNCS:
|
|
84
|
+
self.func_aliases[a.asname or a.name] = a.name
|
|
85
|
+
self.generic_visit(node)
|
|
86
|
+
|
|
87
|
+
# 生效条件:node.func 为 subprocess.<func> / 别名 / os.popen 三种形态之一时返回 (kind, funcname),否则 None。
|
|
88
|
+
def _callee(self, node: ast.Call) -> tuple[str, str] | None:
|
|
89
|
+
f = node.func
|
|
90
|
+
if isinstance(f, ast.Attribute) and isinstance(f.value, ast.Name):
|
|
91
|
+
base = f.value.id
|
|
92
|
+
if base in self.sub_aliases and f.attr in SUB_FUNCS:
|
|
93
|
+
return ("subprocess", f.attr)
|
|
94
|
+
if base == "os" and f.attr == "popen":
|
|
95
|
+
return ("os", "popen")
|
|
96
|
+
if isinstance(f, ast.Name) and f.id in self.func_aliases:
|
|
97
|
+
return ("subprocess", self.func_aliases[f.id])
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
# 生效条件:kws 为调用处关键字实参表;capture_output 为真或 stdout/stderr 取 PIPE 时返回 True(该调用确有管道)。
|
|
101
|
+
@staticmethod
|
|
102
|
+
def _piped(kws: dict[str, ast.AST]) -> bool:
|
|
103
|
+
if "capture_output" in kws and _truthy(kws.get("capture_output")):
|
|
104
|
+
return True
|
|
105
|
+
for key in ("stdout", "stderr"):
|
|
106
|
+
v = kws.get(key)
|
|
107
|
+
if isinstance(v, ast.Attribute) and v.attr == "PIPE":
|
|
108
|
+
return True
|
|
109
|
+
if isinstance(v, ast.Name) and v.id == "PIPE":
|
|
110
|
+
return True
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
def visit_Call(self, node: ast.Call) -> None:
|
|
114
|
+
callee = self._callee(node)
|
|
115
|
+
if callee is not None:
|
|
116
|
+
kind, fname = callee
|
|
117
|
+
if kind == "os":
|
|
118
|
+
self.bad.append("%s:%d: os.popen(...)(该 API 无法声明编码,"
|
|
119
|
+
"改用 subprocess.run)" % (self.rel, node.lineno))
|
|
120
|
+
else:
|
|
121
|
+
kws = {k.arg: k.value for k in node.keywords if k.arg is not None}
|
|
122
|
+
has_star = any(k.arg is None for k in node.keywords)
|
|
123
|
+
text_mode = ("encoding" in kws
|
|
124
|
+
or ("text" in kws and _truthy(kws.get("text")))
|
|
125
|
+
or ("universal_newlines" in kws
|
|
126
|
+
and _truthy(kws.get("universal_newlines"))))
|
|
127
|
+
if text_mode:
|
|
128
|
+
self.checked += 1
|
|
129
|
+
if "encoding" not in kws:
|
|
130
|
+
self.bad.append(
|
|
131
|
+
"%s:%d: subprocess.%s(...) 文本模式未声明 encoding="
|
|
132
|
+
% (self.rel, node.lineno, fname))
|
|
133
|
+
elif has_star and self._piped(kws):
|
|
134
|
+
# **kwargs 透传且确有管道:口径不可判定(可能被调用方注入 text=True)→ 不合格。
|
|
135
|
+
# 输出落 DEVNULL/文件且无管道的调用不判——不经过解码器,无该缺陷面。
|
|
136
|
+
self.bad.append(
|
|
137
|
+
"%s:%d: subprocess.%s(..., **kwargs) 无法确认编码口径,"
|
|
138
|
+
"须显式声明 encoding=" % (self.rel, node.lineno, fname))
|
|
139
|
+
self.generic_visit(node)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# 生效条件:无入参;扫描 git 跟踪的 .py 返回 (违例列表, 受检文本模式调用点数, git 可用性)。
|
|
143
|
+
def scan() -> tuple[list[str], int, bool]:
|
|
144
|
+
files = _tracked_py()
|
|
145
|
+
if files is None:
|
|
146
|
+
return [], 0, False
|
|
147
|
+
bad: list[str] = []
|
|
148
|
+
checked = 0
|
|
149
|
+
for rel in files:
|
|
150
|
+
path = os.path.join(ROOT, rel)
|
|
151
|
+
try:
|
|
152
|
+
with open(path, encoding="utf-8", errors="replace") as fh:
|
|
153
|
+
src = fh.read()
|
|
154
|
+
except OSError:
|
|
155
|
+
continue
|
|
156
|
+
try:
|
|
157
|
+
tree = ast.parse(src, filename=rel)
|
|
158
|
+
except SyntaxError:
|
|
159
|
+
bad.append("%s:0: 语法错误,无法 AST 判定(守卫拒绝在未解析文件上放行)" % rel)
|
|
160
|
+
continue
|
|
161
|
+
sc = _Scanner(rel, bad)
|
|
162
|
+
sc.visit(tree)
|
|
163
|
+
checked += sc.checked
|
|
164
|
+
return sorted(bad), checked, True
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def main() -> int:
|
|
168
|
+
bad, checked, git_ok = scan()
|
|
169
|
+
if not git_ok:
|
|
170
|
+
print("SKIP:git 不可用,无法枚举跟踪文件(本守卫需要 git)")
|
|
171
|
+
return 0
|
|
172
|
+
print("受检文本模式子进程调用点:%d(下限 %d)" % (checked, FLOOR))
|
|
173
|
+
if bad:
|
|
174
|
+
print("✖ 不合格的文本模式调用点 %d 处:" % len(bad))
|
|
175
|
+
for b in bad:
|
|
176
|
+
print(" ", b)
|
|
177
|
+
print("修法:补 encoding=\"utf-8\", errors=\"replace\"(不依赖 locale)")
|
|
178
|
+
return 1
|
|
179
|
+
if checked < FLOOR:
|
|
180
|
+
print("✖ 受检调用点 %d < 下限 %d——扫描面失效(文件枚举/判据坏了),"
|
|
181
|
+
"不得视为通过" % (checked, FLOOR))
|
|
182
|
+
return 1
|
|
183
|
+
print("✔ 全部调用点均显式声明 encoding=(子进程文本解码不依赖 locale)")
|
|
184
|
+
return 0
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
if __name__ == "__main__":
|
|
188
|
+
sys.exit(main())
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"""可验证记忆单元(trust)验收:状态机 / 依赖图 / 双时间轴 / 失效传播 / 闸门 / 输出协议。
|
|
2
|
+
|
|
3
|
+
运行:python -m md_cg.test_trust (或 python md_cg/test_trust.py)
|
|
4
|
+
范式:无第三方依赖的直接运行脚本(assert + 计数 + 非零退出),对齐 md_cg/test_*.py。
|
|
5
|
+
|
|
6
|
+
覆盖面(每个断言都在验「语义」而非「实现细节」):
|
|
7
|
+
① 纯函数裁决:can_transition / check / require_transition
|
|
8
|
+
② as_deps 解析与 parse_time 容错
|
|
9
|
+
③ validity 双时间轴四态
|
|
10
|
+
④ state_of 缺省语义 + stamp 幂等
|
|
11
|
+
⑤ set_state 端到端(负路由 / 幂等 / 审计留痕 / 保护节点)
|
|
12
|
+
⑥ cg.add 覆写继承(全量重建 fm 不丢验证态与依赖)
|
|
13
|
+
⑦ 依赖反查索引 + 缓存失效
|
|
14
|
+
⑧ 一跳同步传播 mark_dependents(预演 / 落盘 / 幂等)
|
|
15
|
+
⑨ 多跳异步 propagate(BFS 可达 / 默认预演)
|
|
16
|
+
⑩ 写入闸门 E050 / E051 / 哨兵与自述豁免(真源 nodefile.declares_dependency;
|
|
17
|
+
收窄裁定 b:仅 `@<节点 id>` 显式引用算依赖声明,自然语言自述不算)
|
|
18
|
+
⑪ 输出协议 statushdr(四态符号 / 开关)
|
|
19
|
+
⑫ 巡检 patrol 只读 + catalog 自描述
|
|
20
|
+
"""
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import shutil
|
|
24
|
+
import sys
|
|
25
|
+
import tempfile
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
from . import trust, statushdr, nodefile, writepipe
|
|
29
|
+
from .mdcg import MdCG
|
|
30
|
+
except ImportError: # 直接脚本运行
|
|
31
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
32
|
+
from md_cg import trust, statushdr, nodefile, writepipe
|
|
33
|
+
from md_cg.mdcg import MdCG
|
|
34
|
+
|
|
35
|
+
PASS = 0
|
|
36
|
+
FAIL = []
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def ok(cond, label):
|
|
40
|
+
global PASS
|
|
41
|
+
if cond:
|
|
42
|
+
PASS += 1
|
|
43
|
+
else:
|
|
44
|
+
FAIL.append(label)
|
|
45
|
+
print(" FAIL: " + label)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def eq(got, want, label):
|
|
49
|
+
ok(got == want, "%s(got=%r want=%r)" % (label, got, want))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def phase(name):
|
|
53
|
+
print("--- " + name)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
A, B, C, D = ("mem_1789810000001", "mem_1789810000002",
|
|
57
|
+
"mem_1789810000003", "mem_1789810000004")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def ccg(node, deps=None, **kw):
|
|
61
|
+
"""构造一条 CCG 契约正文(六要素齐;子功能按 deps 给出,**显式 @ 引用形态**)。
|
|
62
|
+
|
|
63
|
+
注意:跨节点依赖的声明形态是 `@<节点 id>`(收窄裁定 b,2026-09-19)——
|
|
64
|
+
自然语言自述子功能不构成依赖声明,故此处必须带 `@`,否则本文件的闸门断言
|
|
65
|
+
测的就不是依赖链路了。
|
|
66
|
+
"""
|
|
67
|
+
sub = ("依赖 " + "、".join("@" + d for d in deps)) if deps else "无"
|
|
68
|
+
return ("# 功能名:%s\n# 生效条件:无条件\n# 子功能:%s\n# 执行:无\n"
|
|
69
|
+
"# 验证方式:test\n# 不适用条件:无\n\n正文。" % (node, sub))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def main():
|
|
73
|
+
tmp = tempfile.mkdtemp(prefix="mdcg_test_trust_")
|
|
74
|
+
cg = MdCG(root=os.path.join(tmp, "kg"))
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------------- ① 纯函数裁决
|
|
77
|
+
phase("① 裁决")
|
|
78
|
+
ok(trust.can_transition("unverified", "verified"), "unverified→verified 合法")
|
|
79
|
+
ok(trust.can_transition("verified", "doubted"), "verified→doubted 合法")
|
|
80
|
+
ok(trust.can_transition("unverified", "doubted"), "unverified→doubted 合法(负证据)")
|
|
81
|
+
ok(not trust.can_transition("verified", "unverified"), "verified→unverified 拒绝(回落)")
|
|
82
|
+
ok(not trust.can_transition("doubted", "expired"), "doubted→expired 拒绝(跳级)")
|
|
83
|
+
ok(not trust.can_transition("unverified", "nope"), "未知状态拒绝")
|
|
84
|
+
eq(trust.check("verified", "verified")[1], "noop", "同状态查得 noop")
|
|
85
|
+
eq(trust.check("verified", "unverified")[1], "illegal_transition", "非法迁移码")
|
|
86
|
+
ok(trust.check("verified", "unverified", protected=True, override=False)[0] is False,
|
|
87
|
+
"受保护节点拒绝降级")
|
|
88
|
+
ok(trust.check("unverified", "verified", protected=True, override=False)[0] is True,
|
|
89
|
+
"受保护节点允许升级")
|
|
90
|
+
ok(trust.check("verified", "doubted", protected=True, override=True)[0] is True,
|
|
91
|
+
"override 放行合法降级(verified→doubted)")
|
|
92
|
+
ok(trust.check("verified", "unverified", protected=True, override=True)[0] is False,
|
|
93
|
+
"override 不豁免非法迁移(合法性优先于保护)")
|
|
94
|
+
raised = False
|
|
95
|
+
try:
|
|
96
|
+
trust.require_transition("verified", "unverified")
|
|
97
|
+
except trust.TrustError:
|
|
98
|
+
raised = True
|
|
99
|
+
ok(raised, "require_transition 非法即抛 TrustError")
|
|
100
|
+
ok(trust.is_downgrade("verified", "doubted"), "verified→doubted 是降级")
|
|
101
|
+
|
|
102
|
+
# ---------------------------------------------------------------- ② 解析容错
|
|
103
|
+
phase("② 解析")
|
|
104
|
+
eq(trust.as_deps(["b", "a", "b"]), ["a", "b"], "as_deps 列表去重排序")
|
|
105
|
+
eq(trust.as_deps("x"), ["x"], "as_deps 单串")
|
|
106
|
+
eq(trust.as_deps(None), [], "as_deps None")
|
|
107
|
+
eq(trust.as_deps([None, ""]), [], "as_deps 过滤空值")
|
|
108
|
+
eq(trust.as_deps("a, b"), ["a", "b"], "as_deps 逗号串")
|
|
109
|
+
ok(trust.parse_time("2020-01-01") is not None, "parse_time 日期串")
|
|
110
|
+
ok(trust.parse_time(None) is None, "parse_time None")
|
|
111
|
+
ok(trust.parse_time(1700000000.0) == 1700000000.0, "parse_time 数字直通")
|
|
112
|
+
ok(trust.parse_time("不是时间") is None, "parse_time 非法容错")
|
|
113
|
+
|
|
114
|
+
# ---------------------------------------------------------------- ③ 双时间轴
|
|
115
|
+
phase("③ 时间轴")
|
|
116
|
+
eq(trust.validity({"valid_from": "2020-01-01", "valid_until": "2030-01-01"})[0],
|
|
117
|
+
"active", "区间内=active")
|
|
118
|
+
eq(trust.validity({"valid_from": "2099-01-01"})[0], "not_yet", "未来=not_yet")
|
|
119
|
+
eq(trust.validity({"valid_until": "2001-01-01"})[0], "expired", "过去=expired")
|
|
120
|
+
eq(trust.validity({})[0], "unknown", "无时间轴=unknown(不受限,非 active)")
|
|
121
|
+
eq(trust.validity(None)[0], "unknown", "非 dict 亦 unknown(不猜测)")
|
|
122
|
+
# 2026-09-19 阶段一:规范键优先 + 别名回落 + 信念时间(第三类语义)物理隔离
|
|
123
|
+
eq(trust.validity({"effective_from": "2099-01-01"})[0], "not_yet",
|
|
124
|
+
"规范键起点生效")
|
|
125
|
+
eq(trust.validity({"effective_from": "2099-01-01",
|
|
126
|
+
"valid_from": "2020-01-01"})[0], "not_yet",
|
|
127
|
+
"规范键优先于历史键")
|
|
128
|
+
eq(trust.validity({"effective_until": "坏值",
|
|
129
|
+
"valid_until": "2001-01-01"})[0], "expired",
|
|
130
|
+
"规范键不可解析 → 继续回落别名(不猜测定案)")
|
|
131
|
+
eq(trust.validity({"effective_until": "2001-01-01"})[0], "expired",
|
|
132
|
+
"规范键终点生效")
|
|
133
|
+
eq(trust.validity({"believed_at": "2001-01-01"})[0], "unknown",
|
|
134
|
+
"信念时间不参与时效判定(第三类语义)")
|
|
135
|
+
ok(trust.believed_at({"believed_at": "2020-01-01"}) is not None
|
|
136
|
+
and trust.believed_at({}) is None, "believed_at 读取器(epoch/None)")
|
|
137
|
+
|
|
138
|
+
# ---------------------------------------------------------------- ④ 缺省与幂等
|
|
139
|
+
phase("④ 缺省/幂等")
|
|
140
|
+
eq(trust.state_of({}), "unverified", "缺字段按 unverified")
|
|
141
|
+
eq(trust.state_of(None), "unverified", "None 按 unverified")
|
|
142
|
+
fm = {}
|
|
143
|
+
r1 = trust.stamp(fm, "verified", reason="t", actor="t")
|
|
144
|
+
eq(r1[1], "ok", "stamp 首次迁移 code=ok")
|
|
145
|
+
eq(trust.state_of(fm), "verified", "stamp 落字段")
|
|
146
|
+
before = json.dumps(fm, sort_keys=True, default=str)
|
|
147
|
+
r2 = trust.stamp(fm, "verified", reason="t", actor="t")
|
|
148
|
+
eq(json.dumps(fm, sort_keys=True, default=str), before, "同状态 stamp 不重复写")
|
|
149
|
+
eq(r2[1], "noop", "同状态 stamp code=noop(幂等不写盘)")
|
|
150
|
+
|
|
151
|
+
# ---------------------------------------------------------------- ⑤ set_state 端到端
|
|
152
|
+
phase("⑤ set_state")
|
|
153
|
+
missing = trust.set_state(cg, "mem_not_exist", "verified")
|
|
154
|
+
ok(missing.get("ok") is False and missing.get("error") == "node_not_found",
|
|
155
|
+
"不存在节点负路由 node_not_found")
|
|
156
|
+
cg.add(A, ccg("上游"), layer="knowledge", tags=["t1"])
|
|
157
|
+
r = cg.set_verification(A, "verified", reason="首次", actor="tester",
|
|
158
|
+
evidence="ev-1", method="test")
|
|
159
|
+
ok(r.get("ok") and r.get("changed"), "set_verification verified 成功")
|
|
160
|
+
eq(trust.state_of((cg.get(A) or {}).get("frontmatter")), "verified",
|
|
161
|
+
"fm 落 verification_state")
|
|
162
|
+
eq((cg.index["nodes"][A] or {}).get("verification_state"), "verified",
|
|
163
|
+
"索引快照同步(免读文件)")
|
|
164
|
+
r2 = cg.set_verification(A, "verified", reason="重复", actor="tester")
|
|
165
|
+
ok(r2.get("ok") and r2.get("changed") is False, "幂等:同状态 changed=False")
|
|
166
|
+
led = trust.load_ledger(cg, node_id=A)
|
|
167
|
+
ok(len(led) >= 1, "审计台账留痕")
|
|
168
|
+
ok(all("to" in x for x in led), "台账含 to 字段")
|
|
169
|
+
bad = cg.set_verification(A, "unverified", reason="回落", actor="tester")
|
|
170
|
+
ok(bad.get("ok") is False, "非法迁移负路由不抛")
|
|
171
|
+
|
|
172
|
+
# ---------------------------------------------------------------- ⑥ 覆写继承
|
|
173
|
+
phase("⑥ 覆写继承")
|
|
174
|
+
cg.add(B, ccg("下游", [A]), layer="knowledge", depends_on=[A],
|
|
175
|
+
valid_from="2020-01-01", valid_until="2030-01-01")
|
|
176
|
+
cg.set_verification(B, "verified", reason="t", actor="t")
|
|
177
|
+
cg.add(B, ccg("下游 v2", [A]), layer="knowledge", depends_on=[A])
|
|
178
|
+
fmb = (cg.get(B) or {}).get("frontmatter") or {}
|
|
179
|
+
eq(trust.state_of(fmb), "verified", "覆写后验证态继承(防静默打回)")
|
|
180
|
+
eq(fmb.get("depends_on"), [A], "覆写后依赖继承")
|
|
181
|
+
eq(fmb.get("valid_from"), "2020-01-01", "覆写后时间轴继承")
|
|
182
|
+
# 阶段一:规范键与信念时间的覆写继承(同 lifecycle/verification 的坑)
|
|
183
|
+
cg.add(B, ccg("下游 v3", [A]), layer="knowledge", depends_on=[A],
|
|
184
|
+
effective_from="2021-01-01", effective_until="2031-01-01",
|
|
185
|
+
believed_at="2022-01-01")
|
|
186
|
+
fmb3 = (cg.get(B) or {}).get("frontmatter") or {}
|
|
187
|
+
eq(fmb3.get("effective_from"), "2021-01-01", "新规范参数落规范键")
|
|
188
|
+
eq("valid_from" in fmb3, False, "落规范键时剔同族历史键(禁同族双写歧义)")
|
|
189
|
+
eq(fmb3.get("believed_at"), "2022-01-01", "信念时间写入落 believed_at")
|
|
190
|
+
cg.add(B, ccg("下游 v4", [A]), layer="knowledge", depends_on=[A])
|
|
191
|
+
fmb4 = (cg.get(B) or {}).get("frontmatter") or {}
|
|
192
|
+
eq(fmb4.get("effective_from"), "2021-01-01", "覆写后规范起点继承")
|
|
193
|
+
eq(fmb4.get("effective_until"), "2031-01-01", "覆写后规范终点继承")
|
|
194
|
+
eq(fmb4.get("believed_at"), "2022-01-01", "覆写后信念时间继承")
|
|
195
|
+
|
|
196
|
+
# ---------------------------------------------------------------- ⑦ 反查索引
|
|
197
|
+
phase("⑦ 反查索引")
|
|
198
|
+
idx = trust.dependents_index(cg)
|
|
199
|
+
ok(B in idx.get(A, []), "反查:谁依赖 A")
|
|
200
|
+
eq(trust.deps_of(cg, B), [A], "正查:B 依赖谁")
|
|
201
|
+
cg.add(D, ccg("新下游", [A]), layer="knowledge", depends_on=[A])
|
|
202
|
+
trust.invalidate_cache(cg)
|
|
203
|
+
ok(D in trust.dependents_index(cg).get(A, []), "写后缓存失效可重建")
|
|
204
|
+
|
|
205
|
+
# ---------------------------------------------------------------- ⑧ 一跳传播
|
|
206
|
+
phase("⑧ 一跳同步传播")
|
|
207
|
+
dry = trust.mark_dependents(cg, A, reason="预演", apply=False)
|
|
208
|
+
ok(dry.get("dry_run") and len(dry.get("dependents") or []) >= 2,
|
|
209
|
+
"预演列出全部下游且不改盘")
|
|
210
|
+
eq(dry.get("dependents"), sorted(dry.get("dependents") or []), "下游有序输出")
|
|
211
|
+
ok(trust.state_of((cg.get(B) or {}).get("frontmatter")) == "verified",
|
|
212
|
+
"预演未改盘(B 仍 verified)")
|
|
213
|
+
wet = trust.mark_dependents(cg, A, reason="上游变动", actor="tester", trigger="t")
|
|
214
|
+
ok(wet.get("changed") >= 2, "落盘:下游全部标存疑")
|
|
215
|
+
eq(trust.state_of((cg.get(B) or {}).get("frontmatter")), "doubted", "B 已 doubted")
|
|
216
|
+
eq(trust.state_of((cg.get(D) or {}).get("frontmatter")), "doubted", "D 已 doubted")
|
|
217
|
+
again = trust.mark_dependents(cg, A, reason="重复", actor="tester")
|
|
218
|
+
eq(again.get("changed"), 0, "重复传播幂等(changed=0)")
|
|
219
|
+
none_dep = trust.mark_dependents(cg, C, reason="无下游")
|
|
220
|
+
eq(none_dep.get("changed"), 0, "无下游不报错")
|
|
221
|
+
|
|
222
|
+
# ---------------------------------------------------------------- ⑨ 多跳传播
|
|
223
|
+
phase("⑨ 多跳异步传播")
|
|
224
|
+
cg.add(C, ccg("中继", [D]), layer="knowledge", depends_on=[D])
|
|
225
|
+
cg.set_verification(D, "verified", reason="t", actor="t")
|
|
226
|
+
cg.set_verification(C, "verified", reason="t", actor="t")
|
|
227
|
+
cg.set_verification(A, "expired", reason="时间到", actor="tester")
|
|
228
|
+
dry2 = trust.propagate(cg)
|
|
229
|
+
ok(dry2.get("dry_run"), "propagate 默认预演")
|
|
230
|
+
ok(dry2.get("reachable", 0) >= 2, "预演可达多跳下游")
|
|
231
|
+
wet2 = trust.propagate(cg, apply=True, actor="tester")
|
|
232
|
+
ok(wet2.get("dry_run") is False, "apply=True 落盘")
|
|
233
|
+
eq(trust.state_of((cg.get(B) or {}).get("frontmatter")), "doubted",
|
|
234
|
+
"一跳下游 doubted")
|
|
235
|
+
eq(trust.state_of((cg.get(C) or {}).get("frontmatter")), "doubted",
|
|
236
|
+
"二跳下游 doubted(BFS 可达)")
|
|
237
|
+
|
|
238
|
+
# ---------------------------------------------------------------- ⑩ 写入闸门
|
|
239
|
+
phase("⑩ 写入闸门")
|
|
240
|
+
ok(nodefile.declares_dependency(ccg("x", [A])), "显式 @ 引用判为 True")
|
|
241
|
+
ok(not nodefile.declares_dependency(ccg("x")), "「无」哨兵判为 False")
|
|
242
|
+
ok(not nodefile.declares_dependency("# 功能名:x\n\n正文"), "无该行判为 False")
|
|
243
|
+
ok(not nodefile.declares_dependency("# 子功能:无(不依赖其他单元)"),
|
|
244
|
+
"哨兵带括号说明仍为 False")
|
|
245
|
+
# 收窄裁定 b(2026-09-19):区分「自述子功能」与「跨节点依赖」——只有显式
|
|
246
|
+
# `@<节点 id>` 引用才构成依赖声明;自然语言自述不算。否则 CCG 编译产物(六要素
|
|
247
|
+
# 必含「子功能」行)落库后会被 E050 永久锁死(test_ccgc V16f 即该死锁的实证)。
|
|
248
|
+
ok(not nodefile.is_dep_sentinel("无法确定依赖范围"),
|
|
249
|
+
"「无法确定」不得误判为哨兵")
|
|
250
|
+
ok(not nodefile.declares_dependency("# 子功能:无法确定依赖范围"),
|
|
251
|
+
"自然语言自述不算依赖声明(收窄 b)")
|
|
252
|
+
ok(not nodefile.declares_dependency(
|
|
253
|
+
"# 子功能:按扩展名把文件路由到对应摄取器"),
|
|
254
|
+
"CCG 编译产物形态的自述子功能不构成依赖声明(V16f 死锁根因)")
|
|
255
|
+
ok(not nodefile.declares_dependency("# 子功能:联系 user@example.com"),
|
|
256
|
+
"邮箱形态的 @ 不误判为引用(@ 左侧为标识符字符)")
|
|
257
|
+
ok(not nodefile.declares_dependency("# 子功能:@所有人 知悉"),
|
|
258
|
+
"中文 @ 不误判为引用(id 须以 [A-Za-z0-9_] 起头)")
|
|
259
|
+
eq(nodefile.dep_refs("依赖 @mem_a、@code_b 与 @mem_a(回指 @mem_a)"),
|
|
260
|
+
["mem_a", "code_b"], "多引用去重保序提取")
|
|
261
|
+
eq(nodefile.dep_refs("@code_3552109b6163"), ["code_3552109b6163"],
|
|
262
|
+
"行首 @ 命中(左侧非标识符字符即可)")
|
|
263
|
+
eq(nodefile.dep_refs("无"), [], "哨兵值提取为空列表")
|
|
264
|
+
g1 = writepipe._gate_deps({"cg": cg, "nid": "mem_x1",
|
|
265
|
+
"a": {"content": ccg("x", [A]),
|
|
266
|
+
"depends_on": [A]}, "verdict": {}})
|
|
267
|
+
ok(g1 is None, "声明且目标可解析→放行")
|
|
268
|
+
g2 = writepipe._gate_deps({"cg": cg, "nid": "mem_x2",
|
|
269
|
+
"a": {"content": ccg("x", [A]),
|
|
270
|
+
"depends_on": A}, "verdict": {}})
|
|
271
|
+
ok(g2 is None, "单值形态同样放行(as_deps 容错)")
|
|
272
|
+
g2b = writepipe._gate_deps({"cg": cg, "nid": "mem_x2b",
|
|
273
|
+
"a": {"content": ccg("x", [A]),
|
|
274
|
+
"depends_on": "%s, mem_ghost2" % A},
|
|
275
|
+
"verdict": {}})
|
|
276
|
+
eq((g2b or {}).get("error"), "E051", "逗号串逐项解析(含悬空即拒)")
|
|
277
|
+
g3 = writepipe._gate_deps({"cg": cg, "nid": "mem_x3",
|
|
278
|
+
"a": {"content": "# 功能名:x\n# 子功能:依赖 @mem_ghost\n"},
|
|
279
|
+
"verdict": {}})
|
|
280
|
+
eq((g3 or {}).get("error"), "E050", "显式 @ 声明却无字段→E050 硬拒")
|
|
281
|
+
ok((g3 or {}).get("committed") is False, "硬拒体 committed=False")
|
|
282
|
+
g3b = writepipe._gate_deps({"cg": cg, "nid": "mem_x3b",
|
|
283
|
+
"a": {"content": "# 功能名:x\n# 子功能:依赖某物\n"},
|
|
284
|
+
"verdict": {}})
|
|
285
|
+
ok(g3b is None, "自然语言自述子功能不触发闸门(收窄 b 的行为面)")
|
|
286
|
+
g4 = writepipe._gate_deps({"cg": cg, "nid": "mem_x4",
|
|
287
|
+
"a": {"content": ccg("x", ["mem_ghost"]),
|
|
288
|
+
"depends_on": ["mem_ghost"]}, "verdict": {}})
|
|
289
|
+
eq((g4 or {}).get("error"), "E051", "依赖悬空→E051 硬拒")
|
|
290
|
+
eq((g4 or {}).get("missing"), ["mem_ghost"], "E051 回带缺失目标")
|
|
291
|
+
g5 = writepipe._gate_deps({"cg": cg, "nid": "mem_x5",
|
|
292
|
+
"a": {"content": ccg("x")}, "verdict": {}})
|
|
293
|
+
ok(g5 is None, "哨兵值不触发闸门")
|
|
294
|
+
names = writepipe.default_pipeline().names()
|
|
295
|
+
ok("deps" in str(names) and "trust" in str(names), "闸与观察者已注册默认链")
|
|
296
|
+
|
|
297
|
+
# ---------------------------------------------------------------- ⑪ 输出协议
|
|
298
|
+
phase("⑪ 输出协议")
|
|
299
|
+
head_b = statushdr.render(cg, B)
|
|
300
|
+
ok(bool(head_b), "doubted 节点有状态头")
|
|
301
|
+
ok(statushdr.MARK_DOUBTED in head_b, "状态头含存疑符号")
|
|
302
|
+
head_a = statushdr.render(cg, A)
|
|
303
|
+
ok(statushdr.MARK_ABNORMAL in head_a or statushdr.MARK_EXPIRED in head_a
|
|
304
|
+
or bool(head_a), "异常/过期节点有状态头")
|
|
305
|
+
ok(isinstance(statushdr.attach_heads(cg, {"node_id": B}), dict), "attach_heads 返回 dict")
|
|
306
|
+
ok(statushdr.enabled(), "状态头默认开启")
|
|
307
|
+
|
|
308
|
+
# ---------------------------------------------------------------- ⑫ 巡检与自描述
|
|
309
|
+
phase("⑫ 巡检/自描述")
|
|
310
|
+
rep = trust.patrol(cg)
|
|
311
|
+
ok(rep.get("readonly") is True, "巡检只读标记")
|
|
312
|
+
ok(rep.get("nodes", 0) >= 4, "巡检统计节点数")
|
|
313
|
+
ok(rep.get("doubted_count", 0) >= 2, "巡检统计存疑积压")
|
|
314
|
+
snap = trust.state_of((cg.get(B) or {}).get("frontmatter"))
|
|
315
|
+
trust.patrol(cg)
|
|
316
|
+
eq(trust.state_of((cg.get(B) or {}).get("frontmatter")), snap, "巡检不改状态")
|
|
317
|
+
cat = trust.catalog(cg.root)
|
|
318
|
+
for key in ("layer", "question", "states", "state_field", "deps_field",
|
|
319
|
+
"time_fields", "ledger", "discipline"):
|
|
320
|
+
ok(key in cat, "catalog 含键 " + key)
|
|
321
|
+
ok("unverified" in cat["states"] and "doubted" in cat["states"], "catalog 状态枚举")
|
|
322
|
+
s = trust.summary(cg)
|
|
323
|
+
ok(isinstance(s, dict) and "doubted" in s, "summary 轻量摘要")
|
|
324
|
+
de = trust.describe(cg, B)
|
|
325
|
+
ok(de.get("ok") and de.get("depends_on") == [A], "describe 单节点全貌")
|
|
326
|
+
ok(de.get("depended_by") is not None, "describe 含反查")
|
|
327
|
+
|
|
328
|
+
# ---------------------------------------------------------------- ⑬ MCP 面接线
|
|
329
|
+
phase("⑬ MCP 面:status op / 状态头 / 写闸硬拒")
|
|
330
|
+
from md_cg import mcp_server as MS
|
|
331
|
+
st = MS._cg_call(cg, {"op": "status", "node_id": A})
|
|
332
|
+
eq(st.get("ok"), True, "op=status 单节点返回 ok")
|
|
333
|
+
eq(st.get("state") or st.get("verification_state"), "expired", "status 透出验证态")
|
|
334
|
+
ok(bool(st.get("status_head")), "status 带状态头")
|
|
335
|
+
ok(isinstance(st.get("ledger"), list), "status 带履历台账")
|
|
336
|
+
ok("depended_by" in st or "dependents" in st, "status 带下游反查")
|
|
337
|
+
sm = MS._cg_call(cg, {"op": "status"})
|
|
338
|
+
ok(isinstance(sm.get("summary"), dict), "op=status 摘要形态返回 summary")
|
|
339
|
+
ok("status_head" in sm or "status_heads" in sm, "摘要形态亦挂状态头")
|
|
340
|
+
sl = MS._cg_call(cg, {"op": "status", "action": "ledger"})
|
|
341
|
+
ok(isinstance(sl.get("rows"), list), "action=ledger 读台账")
|
|
342
|
+
|
|
343
|
+
# 写闸硬拒(MCP 端到端):正文声明依赖却无可解析目标 → E050 / 悬空 → E051
|
|
344
|
+
r50 = MS._cg_call(cg, {"op": "write", "content": ccg("闸门缺声明", [A]),
|
|
345
|
+
"content_kind": "text", "layer": "knowledge"})
|
|
346
|
+
eq((r50 or {}).get("error"), "E050", "写闸硬拒 E050(声明依赖却无 depends_on)")
|
|
347
|
+
r51 = MS._cg_call(cg, {"op": "write", "content": ccg("闸门悬空", ["mem_ghost_zz"]),
|
|
348
|
+
"content_kind": "text", "layer": "knowledge",
|
|
349
|
+
"depends_on": ["mem_ghost_zz"]})
|
|
350
|
+
eq((r51 or {}).get("error"), "E051", "写闸硬拒 E051(依赖目标悬空)")
|
|
351
|
+
ok(not (cg.get("mem_ghost_zz") or {}), "悬空目标未落盘(闸门真拒非事后回滚)")
|
|
352
|
+
|
|
353
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
354
|
+
print("\nPASS=%d FAIL=%d" % (PASS, len(FAIL)))
|
|
355
|
+
if FAIL:
|
|
356
|
+
for f in FAIL:
|
|
357
|
+
print(" - " + f)
|
|
358
|
+
sys.exit(1)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
main()
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""units 复核通道口径验收(候选新增测试;按互验设计 §7.6,本轮不参与判据面)。"""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
|
|
10
|
+
from . import units
|
|
11
|
+
|
|
12
|
+
_ok = 0
|
|
13
|
+
_bad = []
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _check(name, cond, detail=""):
|
|
17
|
+
global _ok
|
|
18
|
+
if cond:
|
|
19
|
+
_ok += 1
|
|
20
|
+
print(" ok %s" % name)
|
|
21
|
+
else:
|
|
22
|
+
_bad.append("%s %s" % (name, detail))
|
|
23
|
+
print(" FAIL %s %s" % (name, detail))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _mk_job(root, job_id, payload):
|
|
27
|
+
d = os.path.join(root, job_id)
|
|
28
|
+
os.makedirs(d, exist_ok=True)
|
|
29
|
+
with open(os.path.join(d, "result.json"), "w", encoding="utf-8") as f:
|
|
30
|
+
json.dump(payload, f, ensure_ascii=False)
|
|
31
|
+
with open(os.path.join(d, "status.json"), "w", encoding="utf-8") as f:
|
|
32
|
+
json.dump({"job_id": job_id, "state": "done", "model": "stub"}, f,
|
|
33
|
+
ensure_ascii=False)
|
|
34
|
+
return d
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main():
|
|
38
|
+
print("=" * 68)
|
|
39
|
+
print("md_cg units 复核通道口径验收")
|
|
40
|
+
print("=" * 68)
|
|
41
|
+
tmp = tempfile.mkdtemp(prefix="mdcg_units_poll_")
|
|
42
|
+
_check("DEFAULT_MAX_TOKENS 对齐统一默认 200000(文档口径)",
|
|
43
|
+
units.DEFAULT_MAX_TOKENS == 200000, str(units.DEFAULT_MAX_TOKENS))
|
|
44
|
+
_mk_job(tmp, "j_empty", {"ok": True, "content": "",
|
|
45
|
+
"usage": {"completion_tokens": 2048,
|
|
46
|
+
"completion_tokens_details": {"reasoning_tokens": 2048}}})
|
|
47
|
+
r1 = units.poll("j_empty", jobs=tmp)
|
|
48
|
+
_check("poll:空正文不报成功(防假死锁)", r1.get("ok") is False, repr(r1.get("ok")))
|
|
49
|
+
_check("poll:空正文给出可归因错误(指向预算)",
|
|
50
|
+
"空正文" in str(r1.get("error") or ""), str(r1.get("error"))[:90])
|
|
51
|
+
_check("poll:终态仍被识别", r1.get("terminal") is True)
|
|
52
|
+
_mk_job(tmp, "j_good", {"ok": True, "content": "verdict: ACCEPT"})
|
|
53
|
+
r2 = units.poll("j_good", jobs=tmp)
|
|
54
|
+
_check("poll:正常正文仍报成功且正文保留",
|
|
55
|
+
r2.get("ok") is True and r2.get("content") == "verdict: ACCEPT", repr(r2)[:80])
|
|
56
|
+
_mk_job(tmp, "j_fail", {"ok": False, "error": "boom"})
|
|
57
|
+
r3 = units.poll("j_fail", jobs=tmp)
|
|
58
|
+
_check("poll:显式失败原样透出", r3.get("ok") is False and r3.get("error") == "boom")
|
|
59
|
+
chain = units.plan()["chain"]
|
|
60
|
+
_check("plan:第 1 序声明承认「调用方显式传 model」路径",
|
|
61
|
+
"显式传 model" in str(chain[0].get("when") or ""),
|
|
62
|
+
str(chain[0].get("when"))[:100])
|
|
63
|
+
print()
|
|
64
|
+
print("PASS %d / FAIL %d" % (_ok, len(_bad)))
|
|
65
|
+
for b in _bad:
|
|
66
|
+
print(" - " + b)
|
|
67
|
+
return 1 if _bad else 0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
sys.exit(main())
|