@furongjun1999/dsh-memory 0.4.7 → 0.4.8
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 +92 -37
- package/codebuddy/CODEBUDDY.md +195 -185
- package/docs/Pi/345/217/257/345/255/246/344/271/240/344/274/230/347/202/271_/347/201/265/346/236/242/345/244/247/350/204/221/346/224/271/350/277/233/344/272/244/346/216/245_20260915.md +144 -0
- package/docs/README.md +111 -0
- package/docs/discipline/harnesses.yaml +215 -152
- package/docs/discipline/templates/full.md.tmpl +60 -58
- package/docs/eval//347/254/254/344/270/211/346/226/271/351/252/214/350/257/201/346/212/245/345/221/212_LoCoMo_/347/201/265/346/236/242_.md +127 -0
- package/docs/eval//347/254/254/344/270/211/346/226/271/351/252/214/350/257/201/346/212/245/345/221/212_LoCoMo_/347/201/265/346/236/242_.png +0 -0
- package/docs/eval//347/254/254/344/270/211/346/226/271/351/252/214/350/257/201/346/212/245/345/221/212_/347/201/265/346/236/242_vs_dejavu_/347/273/237/344/270/200/350/257/204/345/210/206_v7.md +202 -0
- package/docs/experiments/m4-role-probe/census.py +86 -0
- package/docs/experiments/m4-role-probe/probe.py +89 -0
- package/docs/experiments/mapped_confidence/bench_mapped_conf.py +216 -0
- package/docs/experiments/mapped_confidence/post_check.py +75 -0
- package/docs/experiments/mapped_confidence/repro_thirdparty_tol.py +83 -0
- package/docs/experiments/mapped_confidence/result_mapped_conf.json +215 -0
- package/docs/{ → hive/}/350/234/202/345/267/242/345/267/245/344/275/234/350/256/260/345/277/206_/351/241/271/347/233/256/350/256/241/345/210/222.md +14 -2
- package/docs/{README → mdcg/README}/350/257/246/347/273/206/347/211/210_v0.4.5.md +9 -9
- package/docs/{ → mdcg/}/344/270/273/344/273/243/347/220/206/345/255/220/344/273/243/347/220/206/350/256/260/345/277/206/346/236/266/346/236/204/350/256/276/350/256/241.md +1 -1
- 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 +261 -0
- package/docs/{ → mdcg/}/345/215/225/345/205/203/350/207/252/346/210/221/351/224/232/347/202/271_/347/263/273/347/273/237/346/217/220/347/244/272/350/257/215/346/240/207/345/207/206_v0.3.md +1 -1
- 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 +148 -0
- package/docs/{ → mdcg/}/346/272/220/347/240/201/347/272/247/346/236/266/346/236/204/345/256/241/350/256/241_GPT/346/211/271/350/257/204/345/257/271/347/205/247_v1.0.md +2 -2
- package/docs/{ → mdcg/}/347/201/265/346/236/242/344/270/211/345/261/202/346/213/206/345/210/206/350/247/204/345/210/222_v0.1.md +181 -181
- package/docs/{ → mdcg/}/347/274/272/345/217/243/345/215/225_P0/346/224/266/345/217/243_v0.1.md +2 -2
- package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276_G4-G8/347/274/272/345/217/243/350/243/201/345/256/232/345/215/225_v0.1.md +1 -1
- 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 +4 -4
- package/docs/{ → mdcg/}/350/256/260/345/277/206/346/223/215/344/275/234/347/263/273/347/273/237_MdCGOS/344/270/216MCP/346/216/245/345/205/245_v0.1.md +2 -2
- package/docs/plans/GridWorld/346/234/200/345/260/217/351/227/255/347/216/257/350/247/204/346/240/274_v0.1.md +176 -0
- package/docs/{ → swarm/}/350/234/202/347/276/244/344/272/222/350/201/224_v0.1.md +3 -3
- package/docs/swarm//350/234/202/347/276/244/345/220/214/351/224/231/346/243/200/346/265/213/345/256/236/351/252/214/345/215/217/350/256/256_v0.1.md +242 -0
- package/docs/theory//345/215/225/347/272/277/347/250/213/344/270/216/346/263/250/346/204/217/345/212/233/351/233/206/344/270/255_/346/227/240/344/272/211/350/256/256/347/220/206/350/256/272/346/226/207/346/241/243_v1.0.md +129 -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 +134 -0
- package/docs/theory//346/246/202/345/277/265/345/210/206/345/261/202/345/257/271/351/275/220/350/241/250_v0.1.md +145 -0
- 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/}/347/220/206/350/256/272/344/273/223/346/213/206/345/210/206/344/270/216/347/231/275/347/256/261/347/237/245/350/257/206/345/272/223/345/206/205/350/277/201_v0.1.md +1 -1
- 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 +221 -0
- package/docs/world_model//344/270/226/347/225/214/346/250/241/345/236/213_/347/245/236/347/273/217/347/275/221/347/273/234/345/272/225/345/261/202/346/236/266/346/236/204_v1.0.md +237 -0
- package/docs//345/255/230/347/256/227/344/270/200/344/275/223/346/236/266/346/236/204/345/255/246/344/271/240_/345/244/226/351/203/250/347/220/206/350/256/272/345/257/271/347/205/247/344/270/216/350/267/257/347/272/277/344/272/244/346/216/245_20260916.md +98 -0
- package/docs//345/255/230/347/256/227/344/270/200/344/275/223/347/245/236/347/273/217/347/275/221/347/273/234/346/236/266/346/236/204_/346/200/273/347/272/262/344/270/216/344/272/244/346/216/245_20260916.md +109 -0
- package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +34 -6
- package/docs//347/231/275/347/256/261/345/214/226/347/272/262/351/242/206_/344/270/211/351/241/271/347/233/256/345/255/246/344/271/240/346/200/273/346/236/266/346/236/204/344/270/216Ghidra/344/272/244/346/216/245_20260916.md +104 -0
- package/docs//350/256/260/345/277/206/350/257/204/345/256/241/347/263/273/347/273/237_/347/253/213/351/241/271/350/256/276/350/256/241/344/270/216/346/226/275/345/267/245/344/272/244/346/216/245_20260915.md +183 -0
- package/dsh/README.md +1 -1
- package/lib/hooks.d.ts +5 -0
- package/lib/hooks.js +10 -1
- package/lib/lib/prompt_safety.d.ts +48 -0
- package/lib/lib/prompt_safety.js +62 -0
- package/md_cg/audit.py +76 -2
- package/md_cg/bench_membench.py +10 -5
- package/md_cg/bench_progressive.py +48 -9
- package/md_cg/branches.py +275 -0
- package/md_cg/ccgc.py +884 -0
- package/md_cg/conformance.py +662 -0
- package/md_cg/consistency.py +1 -1
- package/md_cg/corpus.py +1 -1
- package/md_cg/eval_common.py +6 -5
- package/md_cg/forgetting.py +11 -3
- package/md_cg/fsutil.py +63 -1
- package/md_cg/identity.py +2 -2
- package/md_cg/insight.py +2 -2
- package/md_cg/lifecycle.py +262 -0
- package/md_cg/mcp_server.py +547 -140
- package/md_cg/mdcg.py +206 -18
- package/md_cg/mdcos.py +512 -50
- package/md_cg/metacognition.py +2 -2
- package/md_cg/mreview/__init__.py +25 -0
- package/md_cg/mreview/__main__.py +107 -0
- package/md_cg/mreview/bundle.py +170 -0
- package/md_cg/mreview/candidates.py +253 -0
- package/md_cg/mreview/govern.py +674 -0
- package/md_cg/mreview/locate.py +905 -0
- package/md_cg/mreview/pipeline.py +701 -0
- package/md_cg/mreview/rules/duplication.json +21 -0
- package/md_cg/mreview/rules/field_coverage.json +54 -0
- package/md_cg/mreview/rules/source_license.json +21 -0
- package/md_cg/mreview/rules/template_flow.json +21 -0
- package/md_cg/mreview/ruleset.py +238 -0
- package/md_cg/nodefile.py +38 -0
- package/md_cg/predict.py +24 -6
- package/md_cg/protect.py +4 -4
- package/md_cg/refindex.py +41 -2
- package/md_cg/scrub.py +5 -1
- package/md_cg/self_state.py +1 -1
- package/md_cg/semantic/en_normalizer.py +85 -25
- package/md_cg/sustain.py +18 -0
- package/md_cg/tasks.py +447 -0
- package/md_cg/test_action_derive.py +203 -0
- package/md_cg/test_audit_rotate.py +270 -0
- package/md_cg/test_branches.py +249 -0
- package/md_cg/test_ccg_perturb.py +1 -1
- package/md_cg/test_ccgc.py +423 -0
- package/md_cg/test_conformance.py +343 -0
- package/md_cg/test_en_pipeline.py +24 -0
- package/md_cg/test_health_scale.py +173 -0
- package/md_cg/test_identity_attribution.py +6 -0
- package/md_cg/test_index_durability.py +224 -0
- package/md_cg/test_lifecycle.py +309 -0
- package/md_cg/test_mr_m1.py +679 -0
- package/md_cg/test_mr_m2.py +587 -0
- package/md_cg/test_mr_m3.py +703 -0
- package/md_cg/test_mr_m4.py +485 -0
- package/md_cg/test_p1.py +1 -1
- package/md_cg/test_p11_consistency.py +1 -1
- package/md_cg/test_p12_metacognition.py +2 -2
- package/md_cg/test_p16_self_state.py +1 -1
- package/md_cg/test_p26_refindex.py +1 -1
- package/md_cg/test_p27_docindex.py +22 -2
- package/md_cg/test_p28_refcheck.py +1 -1
- package/md_cg/test_p29_session_ingest_export.py +1 -1
- package/md_cg/test_p2_mcp.py +7 -1
- package/md_cg/test_p38_contextualize.py +1 -1
- package/md_cg/test_p39_vision_evidence.py +1 -1
- package/md_cg/test_p40_refine_worklist.py +1 -1
- package/md_cg/test_p41_evolve_patrol.py +1 -1
- package/md_cg/test_p42_provenance.py +1 -1
- package/md_cg/test_p43_pooling.py +41 -13
- package/md_cg/test_read_clip.py +137 -0
- package/md_cg/test_review_conformance.py +310 -0
- package/md_cg/test_tasks.py +409 -0
- package/md_cg/test_tool_face.py +189 -0
- package/md_cg/test_twophase.py +286 -0
- package/md_cg/test_writepipe.py +210 -0
- package/md_cg/theory.py +1 -1
- package/md_cg/tokens.py +95 -5
- package/md_cg/tool_face.py +250 -0
- package/md_cg/twophase.py +221 -0
- package/md_cg/units.py +546 -0
- package/md_cg/vision_evidence.py +1 -1
- package/md_cg/weights.py +1 -1
- package/md_cg/whitebox.py +1 -1
- package/md_cg/whitebox_kb/data/verify_cache.json +28 -0
- package/md_cg/whitebox_kb/data/verify_savings.jsonl +46 -0
- package/md_cg/whitebox_kb/wisdom/audit_log/chain_heat.json +10 -10
- 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
- package/md_cg/writelimit.py +19 -3
- package/md_cg/writepipe.py +372 -0
- package/package.json +1 -1
- package/src/hooks.ts +10 -1
- package/src/lib/prompt_safety.ts +62 -0
- package/zcode/AGENTS.md +195 -185
- package/docs//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +0 -221
- /package/docs/{AGI → eval/AGI}/344/270/203/347/273/264/350/257/204/345/210/206/346/212/245/345/221/212_md_cg_v1.0.md" +0 -0
- /package/docs/{AGI → eval/AGI}/344/270/203/347/273/264/350/257/204/345/210/206/346/212/245/345/221/212_md_cg_v2.0.md" +0 -0
- /package/docs/{ → eval/}/345/256/236/351/252/214/346/226/271/346/241/210_/345/255/246/344/271/240/351/227/255/347/216/257AB/344/270/216/346/250/252/350/257/204_v1.0.md" +0 -0
- /package/docs/{ → eval/}/346/250/252/350/257/204_/345/205/255/345/256/266100/351/242/230/344/270/255/350/213/261/345/217/214/346/237/245_v1.0.md" +0 -0
- /package/docs/{guardrail-charter.md → mdcg/guardrail-charter.md} +0 -0
- /package/docs/{lingshu_tutorial.html → mdcg/lingshu_tutorial.html} +0 -0
- /package/docs/{memory-assessment.html → mdcg/memory-assessment.html} +0 -0
- /package/docs/{memory_score.html → mdcg/memory_score.html} +0 -0
- /package/docs/{memory_score.png → mdcg/memory_score.png} +0 -0
- /package/docs/{release_v0.3.0.md → mdcg/release_v0.3.0.md} +0 -0
- /package/docs/{release_v0.4.5.md → mdcg/release_v0.4.5.md} +0 -0
- /package/docs/{tool_table_v0.3.0.md → mdcg/tool_table_v0.3.0.md} +0 -0
- /package/docs/{ → mdcg/}/344/273/244/347/211/214/344/270/216/350/247/222/350/211/262/346/235/203/350/201/214/345/210/206/347/246/273_v0.1.md" +0 -0
- /package/docs/{ → mdcg/}/347/201/265/346/236/24282/345/267/245/345/205/267_/345/212/237/350/203/275/346/225/264/347/220/206/344/270/216/350/277/201/347/247/273/346/230/240/345/260/204_v0.1.md" +0 -0
- /package/docs/{ → mdcg/}/347/201/265/346/236/242MCP/345/267/245/345/205/267/346/200/273/350/241/250_v3.4.md" +0 -0
- /package/docs/{ → mdcg/}/347/201/265/346/236/242_/350/207/252/346/210/221/345/261/202/345/256/232/344/271/211.md" +0 -0
- /package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276_MD/347/233/256/345/275/225/346/226/271/346/241/210_v0.1.md" +0 -0
- /package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276_/346/235/241/344/273/266/347/251/272/351/227/264/345/220/210/346/210/220/344/270/216/347/224/237/346/225/210/346/235/241/344/273/266/345/217/243/345/276/204_v0.1.md" +0 -0
- /package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276/344/275/234/344/270/272/350/256/260/345/277/206/346/223/215/344/275/234/347/263/273/347/273/237_/350/257/204/344/274/260/344/270/216/350/267/257/347/272/277_v0.1.md" +0 -0
- /package/docs/{ → plans/}/345/221/275/345/220/215/346/262/273/347/220/206_/351/241/271/347/233/256/350/256/241/345/210/222.md" +0 -0
- /package/docs/{ → plans/}/347/237/245/350/257/206/345/233/276/350/260/261/351/241/271/347/233/256_/344/272/244/346/216/245/346/226/207/346/241/243_20260914.md" +0 -0
- /package/docs/{ → swarm/}/350/234/202/347/276/244/345/244/232/346/231/272/350/203/275/344/275/223_/344/275/277/347/224/250/346/212/245/345/221/212_20260913.md" +0 -0
- /package/docs/{ → swarm/}/350/234/202/347/276/244/345/244/232/346/231/272/350/203/275/344/275/223_/345/212/237/350/203/275/350/257/264/346/230/216_v0.6.md" +0 -0
- /package/docs/{ → swarm/}/350/234/202/347/276/244/347/233/262/345/214/272/346/240/207/350/256/260_v1.0.md" +0 -0
- /package/docs/{ → theory/}/344/277/241/346/201/257/345/267/256/344/270/272/344/273/200/344/271/210/345/277/205/347/204/266/345/255/230/345/234/250/344/270/224/350/207/252/347/204/266/346/211/251/345/244/247.md" +0 -0
- /package/docs/{ → theory/}/346/231/272/350/203/275/347/232/204/345/205/254/347/220/206/345/214/226/345/237/272/347/237/263.md" +0 -0
- /package/docs/{ → theory/}/346/231/272/350/203/275/347/232/204/350/256/244/347/237/245/350/277/207/347/250/213.md" +0 -0
- /package/docs/{ → theory/}/346/231/272/350/203/275/350/256/2723.4.md" +0 -0
- /package/docs/{ → theory/}/347/231/275/347/256/261/346/231/272/350/203/275/346/230/257/344/273/200/344/271/210/357/274/237.md" +0 -0
- /package/docs/{ → theory/}/347/231/275/347/256/261/346/231/272/350/203/275/347/263/273/345/210/227/302/267/347/254/254/344/272/224/347/257/207/357/274/232/350/256/251AI/347/234/237/346/255/243/350/243/205/344/270/212/350/256/260/345/277/206.md" +0 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""结构层任务实体(md_cg/tasks.py)验收矩阵(2026-09-16)。
|
|
3
|
+
|
|
4
|
+
蓝图=消除 AI 失忆四症状,断言按症状逐条对位:
|
|
5
|
+
|
|
6
|
+
① 忘记已实现的工程 → H 组:session_tasks / session_recall 任务段,
|
|
7
|
+
且**不按 session 过滤**(工程台账跨会话可见,这正是失忆的解)
|
|
8
|
+
② 计划与实际不符 → F 组:计划每轮覆盖更新 + 「计划变更」节逐轮累积
|
|
9
|
+
③ 缺核验 → E 组:done 时结果必填闸(缺一不收),拒收后**盘上内容逐字节不变**
|
|
10
|
+
④ 换表述即新任务 → A/C/I 组:身份=语义 slug(非内容哈希);同 slug 幂等更新
|
|
11
|
+
不新增节点;同族只提示、不自动合并
|
|
12
|
+
|
|
13
|
+
另含 MCP 面对位(L 组:action 推导不吞写意图 / schema 参数声明 / 缺省 list 冒烟 /
|
|
14
|
+
未知 action fail-closed)与边界(J 组:串号防护、检索面、反向关联;M 组:权限闸)。
|
|
15
|
+
脚本式 assert + 退出码,风格与 test_branches.py / test_action_derive.py 一致。
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import sys
|
|
21
|
+
import shutil
|
|
22
|
+
import inspect
|
|
23
|
+
import tempfile
|
|
24
|
+
import hashlib
|
|
25
|
+
|
|
26
|
+
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
27
|
+
_REPO = os.path.dirname(_HERE)
|
|
28
|
+
if _REPO not in sys.path:
|
|
29
|
+
sys.path.insert(0, _REPO)
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
33
|
+
except Exception:
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
from md_cg import mcp_server as M
|
|
37
|
+
from md_cg import tasks as T
|
|
38
|
+
from md_cg import tokens as TK
|
|
39
|
+
from md_cg.mdcos import MdCGSecure
|
|
40
|
+
from md_cg.security import Principal, AccessDenied
|
|
41
|
+
|
|
42
|
+
# 语料命名(互不串扰,各症状用独立任务名)
|
|
43
|
+
NAME = "记忆失忆修复-工程"
|
|
44
|
+
NAME2 = "任务实体-done闸探针"
|
|
45
|
+
NAME3 = "mcp面-任务探针"
|
|
46
|
+
NAME4 = "任务-未开先done"
|
|
47
|
+
NAME5 = "越权任务"
|
|
48
|
+
|
|
49
|
+
_fails = []
|
|
50
|
+
_n = [0]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def check(name, cond, detail=None):
|
|
54
|
+
_n[0] += 1
|
|
55
|
+
if cond:
|
|
56
|
+
print(" ok %s" % name)
|
|
57
|
+
else:
|
|
58
|
+
print("FAIL %s %r" % (name, detail))
|
|
59
|
+
_fails.append(name)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _raises(fn):
|
|
63
|
+
try:
|
|
64
|
+
fn()
|
|
65
|
+
except Exception as e: # noqa: BLE001 —— 测试需拿到任何异常作证据
|
|
66
|
+
return e
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _mk_cg(root, **kw):
|
|
71
|
+
p = Principal(actor="task_test", role="designer", can_write=True, can_admin=True,
|
|
72
|
+
ops_allow=["*"], layers_allow=["*"], **kw)
|
|
73
|
+
return MdCGSecure(root, principal=p)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _task_nodes(cg):
|
|
77
|
+
return sorted(n for n in cg.index["nodes"] if n.startswith(T.TASK_PREFIX))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _hits(cg, q, **kw):
|
|
81
|
+
res, _meta = cg.search(q, **kw)
|
|
82
|
+
return [h[0].get("id") for h in res]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def main():
|
|
86
|
+
tmp = tempfile.mkdtemp(prefix="mdcg_tasks_")
|
|
87
|
+
try:
|
|
88
|
+
_root = os.path.join(tmp, "root")
|
|
89
|
+
cg = _mk_cg(_root)
|
|
90
|
+
nid = T.task_node_id(NAME)
|
|
91
|
+
|
|
92
|
+
# ---------------- A. 身份判据=语义 slug(症状④ 的根) ----------------
|
|
93
|
+
print("\n[A] 身份判据:语义 slug,非内容哈希")
|
|
94
|
+
check("A1 中文名原样保留(语义身份可读)",
|
|
95
|
+
T.slugify(NAME) == NAME, T.slugify(NAME))
|
|
96
|
+
check("A2 路径分隔符折叠(防目录穿越)",
|
|
97
|
+
T.slugify("a/b\\c") == "a-b-c", T.slugify("a/b\\c"))
|
|
98
|
+
_trav = T.slugify("../../etc/passwd")
|
|
99
|
+
check("A3 `..` 与 `/` 全部折叠,不留穿越形态",
|
|
100
|
+
".." not in _trav and "/" not in _trav and "\\" not in _trav, _trav)
|
|
101
|
+
check("A4 空白/纯符号名拒收(无身份不建卡)",
|
|
102
|
+
T.task_node_id(" ") is None or T.slugify(" ") == "", T.slugify(" "))
|
|
103
|
+
check("A4b 纯符号名 slug 为空", T.slugify("!!!") == "", T.slugify("!!!"))
|
|
104
|
+
check("A5 已带前缀不重复叠(task_x → task_x)",
|
|
105
|
+
T.task_node_id("task_x") == "task_x" == T.task_node_id("x"),
|
|
106
|
+
(T.task_node_id("task_x"), T.task_node_id("x")))
|
|
107
|
+
check("A6 超长名截断(≥64 上限护栏)",
|
|
108
|
+
len(T.task_node_id("a" * 200)) <= len(T.TASK_PREFIX) + 64,
|
|
109
|
+
len(T.task_node_id("a" * 200)))
|
|
110
|
+
_o = T.upsert(cg, "任务 名(一)", plan="探针")
|
|
111
|
+
check("A7 归一化透明(原名与 slug 同返,不静默改名)",
|
|
112
|
+
_o.get("ok") and _o.get("slug_normalized") == "任务-名-一"
|
|
113
|
+
and _o.get("node_id") == "task_任务-名-一", _o)
|
|
114
|
+
|
|
115
|
+
# ---------------- B. 创建与读回(结构层定位) ----------------
|
|
116
|
+
print("\n[B] 创建与读回:structural 层一等节点")
|
|
117
|
+
r = T.upsert(cg, NAME, goal="消除 AI 四症状失忆",
|
|
118
|
+
plan="1) 建 tasks.py 2) 接 MCP 3) 会话段 4) 测试",
|
|
119
|
+
acceptance="四症状逐条有对位断言", boundary="不动 LAYERS",
|
|
120
|
+
condition="有跨会话任务台账需求")
|
|
121
|
+
check("B1 首次登记建卡成功", r.get("ok") is True and r.get("created") is True, r)
|
|
122
|
+
g = T.get_task(cg, nid)
|
|
123
|
+
_idx = cg.index["nodes"].get(nid) or {} # 层/权重以索引真源为准
|
|
124
|
+
check("B2 读回六要素齐备(CCG 骨架)",
|
|
125
|
+
g.get("goal") and g.get("plan") and g.get("acceptance")
|
|
126
|
+
and g.get("boundary") and g.get("condition"), g)
|
|
127
|
+
check("B3 落盘层=structural(使用者指定位置,不新增层)",
|
|
128
|
+
_idx.get("layer") == "structural", _idx.get("layer"))
|
|
129
|
+
check("B4 标签含 task 与 task:<slug>(可反向关联)",
|
|
130
|
+
T.TASK_TAG in g.get("tags", []) and ("task:" + NAME) in g.get("tags", []),
|
|
131
|
+
g.get("tags"))
|
|
132
|
+
check("B5 重要性 0.8(进入写保护域,不可被随意覆写)",
|
|
133
|
+
abs(float(_idx.get("importance") or 0) - T.DEFAULT_IMPORTANCE) < 1e-9,
|
|
134
|
+
_idx.get("importance"))
|
|
135
|
+
check("B6 状态缺省 active", g.get("status") == "active", g.get("status"))
|
|
136
|
+
|
|
137
|
+
# ---------------- C. 幂等:同 slug 即同任务(症状④ 的机制解) ----------------
|
|
138
|
+
print("\n[C] 幂等:同 slug 走同一节点更新,不新建卡")
|
|
139
|
+
_before = len(_task_nodes(cg))
|
|
140
|
+
r2 = T.upsert(cg, NAME, plan="1) 建 tasks.py 2) 接 MCP 3) 会话段 4) 测试(已更新)")
|
|
141
|
+
check("C1 重复登记不新建卡(created=False)",
|
|
142
|
+
r2.get("ok") is True and r2.get("created") is False, r2)
|
|
143
|
+
check("C2 节点数不变(换表述不产生新任务)",
|
|
144
|
+
len(_task_nodes(cg)) == _before, _task_nodes(cg))
|
|
145
|
+
g2 = T.get_task(cg, nid)
|
|
146
|
+
check("C3 未提供字段沿用旧值(goal 未被清空)",
|
|
147
|
+
g2.get("goal") == "消除 AI 四症状失忆", g2.get("goal"))
|
|
148
|
+
check("C4 已提供字段被覆盖(plan 更新生效)",
|
|
149
|
+
"(已更新)" in (g2.get("plan") or ""), g2.get("plan"))
|
|
150
|
+
check("C5 与 goals 不双写(任务走自有状态机,不依赖 goal_ 节点)",
|
|
151
|
+
not [n for n in cg.index["nodes"] if n.startswith("goal_")],
|
|
152
|
+
[n for n in cg.index["nodes"] if n.startswith("goal_")])
|
|
153
|
+
|
|
154
|
+
# ---------------- D. 结果不可被静默清空 ----------------
|
|
155
|
+
print("\n[D] 结果维护:不可静默清空")
|
|
156
|
+
T.upsert(cg, NAME, result="首版结果:tasks.py 骨架落地")
|
|
157
|
+
check("D1 结果可写入",
|
|
158
|
+
"首版结果" in (T.get_task(cg, nid).get("result") or ""),
|
|
159
|
+
T.get_task(cg, nid).get("result"))
|
|
160
|
+
T.upsert(cg, NAME, note="仅改进度,不碰结果")
|
|
161
|
+
check("D2 本次不带 result → 旧结果保留(非清空)",
|
|
162
|
+
"首版结果" in (T.get_task(cg, nid).get("result") or ""),
|
|
163
|
+
T.get_task(cg, nid).get("result"))
|
|
164
|
+
T.upsert(cg, NAME, result="")
|
|
165
|
+
check("D3 空串视为未提供 → 仍保留旧结果",
|
|
166
|
+
"首版结果" in (T.get_task(cg, nid).get("result") or ""),
|
|
167
|
+
T.get_task(cg, nid).get("result"))
|
|
168
|
+
|
|
169
|
+
# ---------------- E. done 结果必填闸(症状③ 缺核验) ----------------
|
|
170
|
+
print("\n[E] 完成闸:done 无结果拒收,缺一不收")
|
|
171
|
+
e1 = T.upsert(cg, NAME4, status="done")
|
|
172
|
+
check("E1 新任务直接 done 且无结果 → 拒收",
|
|
173
|
+
bool(e1) and e1.get("ok") is False and "结果" in (e1.get("error") or ""), e1)
|
|
174
|
+
check("E1b 拒收不留半成品节点(盘上无残留)",
|
|
175
|
+
T.task_node_id(NAME4) not in cg.index["nodes"])
|
|
176
|
+
T.upsert(cg, NAME2, goal="验证完成闸", plan="1) 造 active 卡 2) 裸转 done")
|
|
177
|
+
n2 = T.task_node_id(NAME2)
|
|
178
|
+
_c_before = (cg.get(n2) or {}).get("content") or ""
|
|
179
|
+
e2 = T.set_status(cg, n2, "done")
|
|
180
|
+
check("E2 已有任务裸转 done(无结果)→ 拒收",
|
|
181
|
+
bool(e2) and e2.get("ok") is False, e2)
|
|
182
|
+
_c_after = (cg.get(n2) or {}).get("content") or ""
|
|
183
|
+
check("E2b 拒收后盘上内容逐字节不变(拒绝不是软提示)",
|
|
184
|
+
hashlib.md5(_c_before.encode("utf-8")).hexdigest()
|
|
185
|
+
== hashlib.md5(_c_after.encode("utf-8")).hexdigest())
|
|
186
|
+
check("E2c 状态未被偷偷推进(仍 active)",
|
|
187
|
+
T.get_task(cg, n2).get("status") == "active",
|
|
188
|
+
T.get_task(cg, n2).get("status"))
|
|
189
|
+
e3 = T.set_status(cg, n2, "done", result="已交付:完成闸 41 断言全绿")
|
|
190
|
+
check("E3 带结果转 done → 通过",
|
|
191
|
+
bool(e3) and e3.get("ok") is True, e3)
|
|
192
|
+
check("E3b 状态落盘 done 且结果可检索",
|
|
193
|
+
T.get_task(cg, n2).get("status") == "done"
|
|
194
|
+
and "已交付" in (T.get_task(cg, n2).get("result") or ""),
|
|
195
|
+
T.get_task(cg, n2).get("status"))
|
|
196
|
+
|
|
197
|
+
# ---------------- F. 计划覆盖 + 计划变更累积(症状②) ----------------
|
|
198
|
+
print("\n[F] 计划与变更:计划覆盖、变更累积")
|
|
199
|
+
T.upsert(cg, NAME, plan="1) 建模块 2) 接 MCP 3) 会话段 4) 测试 5) 归档")
|
|
200
|
+
check("F1 计划每轮覆盖更新(当前态唯一)",
|
|
201
|
+
"5) 归档" in (T.get_task(cg, nid).get("plan") or ""),
|
|
202
|
+
T.get_task(cg, nid).get("plan"))
|
|
203
|
+
f2 = T.plan_add(cg, nid, "发现 add 是全量重建 fm,需先读旧 fm 再合并")
|
|
204
|
+
check("F2 计划变更可追加(变更记入独立节)",
|
|
205
|
+
bool(f2) and f2.get("ok") is True, f2)
|
|
206
|
+
T.plan_add(cg, nid, "MCP 面 action 推导顺序调整:写意图优先于读意图")
|
|
207
|
+
ch = T.get_task(cg, nid).get("changes") or ""
|
|
208
|
+
check("F3 变更逐轮累积(两轮 = 两行,差异可追溯)",
|
|
209
|
+
ch.count("- [") == 2, ch)
|
|
210
|
+
check("F4 变更不覆盖计划(两节独立)",
|
|
211
|
+
"5) 归档" in (T.get_task(cg, nid).get("plan") or ""), ch)
|
|
212
|
+
_empty = T.plan_add(cg, nid, " ")
|
|
213
|
+
check("F5 空变更拒收(不写空行噪声)",
|
|
214
|
+
bool(_empty) and _empty.get("ok") is False, _empty)
|
|
215
|
+
|
|
216
|
+
# ---------------- G. 状态机 ----------------
|
|
217
|
+
print("\n[G] 状态机:四态可切换,未知态拒收")
|
|
218
|
+
T.upsert(cg, NAME3, goal="MCP 面探针", plan="1) 推导 2) 冒烟")
|
|
219
|
+
n3 = T.task_node_id(NAME3)
|
|
220
|
+
check("G1 blocked 可切换",
|
|
221
|
+
T.set_status(cg, n3, "blocked").get("ok") is True
|
|
222
|
+
and T.get_task(cg, n3).get("status") == "blocked")
|
|
223
|
+
T.set_status(cg, n3, "active")
|
|
224
|
+
_bad = T.set_status(cg, n3, "moving")
|
|
225
|
+
check("G2 未知状态拒收(状态机封闭)",
|
|
226
|
+
bool(_bad) and _bad.get("ok") is False, _bad)
|
|
227
|
+
|
|
228
|
+
# ---------------- H. 会话装配(症状① 的机制解) ----------------
|
|
229
|
+
print("\n[H] 会话召回:带出进行中与近期完成,跨会话可见")
|
|
230
|
+
st = T.session_tasks(cg)
|
|
231
|
+
check("H1 进行中任务被带出(忘记已实现的解)",
|
|
232
|
+
any(t["name"] == NAME for t in st["active"]), [t["name"] for t in st["active"]])
|
|
233
|
+
check("H2 近期完成单列且带结果摘要",
|
|
234
|
+
any(t["name"] == NAME2 and t.get("result") for t in st["done"]),
|
|
235
|
+
st["done"])
|
|
236
|
+
_other = MdCGSecure(_root, principal=Principal(
|
|
237
|
+
actor="task_test_other", role="designer", can_write=True, can_admin=True,
|
|
238
|
+
ops_allow=["*"], layers_allow=["*"], session="sess_other"))
|
|
239
|
+
check("H3 换会话(新 Principal 同 root)仍带出任务(工程台账跨会话)",
|
|
240
|
+
any(t["name"] == NAME
|
|
241
|
+
for t in _other.session_recall()["tasks"]["active"]))
|
|
242
|
+
check("H3b session_tasks 无 session 形参(设计:台账不按会话切分)",
|
|
243
|
+
"session" not in inspect.signature(T.session_tasks).parameters,
|
|
244
|
+
list(inspect.signature(T.session_tasks).parameters))
|
|
245
|
+
pack = cg.session_recall()
|
|
246
|
+
check("H4 session_recall 含 tasks 段(上下文装配面已接)",
|
|
247
|
+
"tasks" in pack and any(t["name"] == NAME for t in pack["tasks"]["active"]),
|
|
248
|
+
list(pack.get("tasks", {}).get("active", []))[:2])
|
|
249
|
+
check("H5 预算裁剪上报字段在位(任务段最后让位)",
|
|
250
|
+
"tasks_truncated" in pack, sorted(pack.keys()))
|
|
251
|
+
check("H6 done 任务不进进行中段(不冒充未完成)",
|
|
252
|
+
all(t["name"] != NAME2 for t in st["active"]),
|
|
253
|
+
[t["name"] for t in st["active"]])
|
|
254
|
+
|
|
255
|
+
# ---------------- I. 同族提示(只提示,不合并) ----------------
|
|
256
|
+
print("\n[I] 同族提示:由人裁决,不自动合并")
|
|
257
|
+
s_exact = T.find_similar(cg, NAME)
|
|
258
|
+
check("I1 完全同名可判别(exists=True,防重复开工)",
|
|
259
|
+
s_exact.get("exists") is True, s_exact)
|
|
260
|
+
_cnt_before = len(_task_nodes(cg))
|
|
261
|
+
s_near = T.find_similar(cg, NAME + "-续")
|
|
262
|
+
check("I2 近名给出疑似同族(提示面可用)",
|
|
263
|
+
isinstance(s_near.get("similar"), list), s_near)
|
|
264
|
+
check("I3 提示不改盘(同族绝不自动合并)",
|
|
265
|
+
len(_task_nodes(cg)) == _cnt_before, _task_nodes(cg))
|
|
266
|
+
s_new = T.find_similar(cg, "完全无关的新任务-xyz")
|
|
267
|
+
check("I4 全新名不误报同族(exists=False)",
|
|
268
|
+
s_new.get("exists") is False, s_new)
|
|
269
|
+
|
|
270
|
+
# ---------------- J. 边界:串号 / 检索 / 反向关联 ----------------
|
|
271
|
+
print("\n[J] 边界:串号防护、检索面、知识反向关联")
|
|
272
|
+
cg.add("trait_probe", "非任务的结构层节点(探针)", layer="structural",
|
|
273
|
+
verification_basis="other", consistency=False, tags=["trait"])
|
|
274
|
+
check("J1 非任务 structural 节点不混入清单(层内串号防护)",
|
|
275
|
+
all(t["id"] != "trait_probe" for t in T.list_tasks(cg)["tasks"]),
|
|
276
|
+
[t["id"] for t in T.list_tasks(cg)["tasks"]])
|
|
277
|
+
_ids = _hits(cg, NAME, layer="structural")
|
|
278
|
+
check("J2 任务节点在 structural 层可被检索(按需调用面)",
|
|
279
|
+
nid in _ids, _ids)
|
|
280
|
+
cg.add("kp_task_rel", "任务相关知识:四症状的机制解与验收判据",
|
|
281
|
+
layer="knowledge", verification_basis="test", consistency=False,
|
|
282
|
+
tags=["task:" + NAME])
|
|
283
|
+
_kp = cg.get("kp_task_rel")
|
|
284
|
+
check("J3 任务知识挂 knowledge 层并以 tags 反向关联",
|
|
285
|
+
_kp.get("frontmatter", {}).get("layer") == "knowledge"
|
|
286
|
+
and ("task:" + NAME) in (_kp.get("frontmatter", {}).get("tags") or []),
|
|
287
|
+
_kp.get("frontmatter", {}).get("tags"))
|
|
288
|
+
check("J4 任务清单按 layer 过滤(知识节点不入台账)",
|
|
289
|
+
all(t["id"] != "kp_task_rel" for t in T.list_tasks(cg)["tasks"]))
|
|
290
|
+
|
|
291
|
+
# ---------------- K. 留痕:复用既有审计面 ----------------
|
|
292
|
+
print("\n[K] 生命周期留痕:复用 _audit,不新造日志格式")
|
|
293
|
+
_seen = []
|
|
294
|
+
_orig = cg._audit
|
|
295
|
+
|
|
296
|
+
def _spy(op, nid_, **kw):
|
|
297
|
+
_seen.append(op)
|
|
298
|
+
return _orig(op, nid_, **kw)
|
|
299
|
+
|
|
300
|
+
cg._audit = _spy
|
|
301
|
+
try:
|
|
302
|
+
T.upsert(cg, "审计探针-任务", plan="x")
|
|
303
|
+
T.plan_add(cg, T.task_node_id("审计探针-任务"), "变更一行")
|
|
304
|
+
finally:
|
|
305
|
+
cg._audit = _orig
|
|
306
|
+
check("K1 建卡留痕 task_open", "task_open" in _seen, _seen)
|
|
307
|
+
check("K2 更新留痕 task_update", "task_update" in _seen, _seen)
|
|
308
|
+
|
|
309
|
+
# ---------------- L. MCP 面对位 ----------------
|
|
310
|
+
print("\n[L] MCP 面:action 推导不吞写意图 / schema 可传 / 缺省 list")
|
|
311
|
+
SIG = M._action_sig
|
|
312
|
+
check("L1 task+name → open", SIG({"name": "X"}, "task") == ("open", "name"),
|
|
313
|
+
SIG({"name": "X"}, "task"))
|
|
314
|
+
check("L2 task+change → plan_add",
|
|
315
|
+
SIG({"change": "c"}, "task") == ("plan_add", "change"),
|
|
316
|
+
SIG({"change": "c"}, "task"))
|
|
317
|
+
check("L3 task+name+change → open(open 兼容 change,写意图不丢)",
|
|
318
|
+
SIG({"name": "X", "change": "c"}, "task") == ("open", "name"),
|
|
319
|
+
SIG({"name": "X", "change": "c"}, "task"))
|
|
320
|
+
check("L4 task+node_id → get",
|
|
321
|
+
SIG({"node_id": "task_x"}, "task") == ("get", "node_id"),
|
|
322
|
+
SIG({"node_id": "task_x"}, "task"))
|
|
323
|
+
check("L5 task+node_id+change → plan_add(写优先于读,不静默当只读)",
|
|
324
|
+
SIG({"node_id": "task_x", "change": "c"}, "task") == ("plan_add", "change"),
|
|
325
|
+
SIG({"node_id": "task_x", "change": "c"}, "task"))
|
|
326
|
+
check("L6 task+task_status → status",
|
|
327
|
+
SIG({"task_status": "done"}, "task") == ("status", "task_status"),
|
|
328
|
+
SIG({"task_status": "done"}, "task"))
|
|
329
|
+
check("L7 task 无签名不猜(交 _ACTION_DEFAULT 承接)",
|
|
330
|
+
SIG({}, "task") == (None, None), SIG({}, "task"))
|
|
331
|
+
check("L8 _ACTION_DEFAULT['task'] == 'list'",
|
|
332
|
+
M._ACTION_DEFAULT.get("task") == "list", M._ACTION_DEFAULT.get("task"))
|
|
333
|
+
_src = open(os.path.join(_HERE, "mcp_server.py"), encoding="utf-8").read()
|
|
334
|
+
_vals = set(re.findall(r'a\.get\("action"\)\s*or\s*"([a-z_]+)"', _src))
|
|
335
|
+
check("L9 缺省字面量在源码真实存在(防幻影表)", "list" in _vals, sorted(_vals))
|
|
336
|
+
|
|
337
|
+
out = M._cg_call(cg, {"op": "task", "name": NAME3, "plan": "1) 推导 2) 冒烟"})
|
|
338
|
+
check("L10 op=task 缺 action → 推导 open 且透出(不静默)",
|
|
339
|
+
out.get("ok") is True and out.get("action_derived") is True
|
|
340
|
+
and out.get("action") == "open", out)
|
|
341
|
+
|
|
342
|
+
def _chg():
|
|
343
|
+
_g = M._cg_call(cg, {"op": "task", "action": "get", "node_id": n3})
|
|
344
|
+
return _g.get("changes") or ""
|
|
345
|
+
|
|
346
|
+
_c_before = _chg()
|
|
347
|
+
out2 = M._cg_call(cg, {"op": "task", "node_id": n3, "change": "MCP 面追加一行"})
|
|
348
|
+
check("L11 node_id+change 真实落盘(写意图未被吞)",
|
|
349
|
+
out2.get("ok") is True and _chg().count("- [") == _c_before.count("- [") + 1,
|
|
350
|
+
out2)
|
|
351
|
+
|
|
352
|
+
out3 = M._cg_call(cg, {"op": "task"})
|
|
353
|
+
check("L12 无签名 → 缺省 list 且透出 action_derived",
|
|
354
|
+
out3.get("action_derived") is True and out3.get("action") == "list"
|
|
355
|
+
and bool(out3.get("tasks")) is True, out3)
|
|
356
|
+
out4 = M._cg_call(cg, {"op": "task", "action": "nope", "name": NAME})
|
|
357
|
+
check("L13 未知 action fail-closed(返回 error,不静默降级)",
|
|
358
|
+
out4.get("ok") is False and bool(out4.get("error")), out4)
|
|
359
|
+
out5 = M._cg_call(cg, {"op": "task", "action": "session"})
|
|
360
|
+
check("L14 action=session 会话装配面可经 MCP 取用",
|
|
361
|
+
out5.get("ok") is True and bool(out5.get("active")), out5)
|
|
362
|
+
out6 = M._cg_call(cg, {"op": "task", "action": "find", "name": NAME})
|
|
363
|
+
check("L15 action=find 同族提示可经 MCP 取用",
|
|
364
|
+
out6.get("ok") is True, out6)
|
|
365
|
+
|
|
366
|
+
_tool = [t for t in M.KERNEL_TOOLS if t.get("name") == "cg"][0]
|
|
367
|
+
_props = _tool["inputSchema"]["properties"]
|
|
368
|
+
check("L16 op 枚举含 task(客户端可见面同步)",
|
|
369
|
+
"task" in [x.strip() for x in _props["op"]["description"].split("|")])
|
|
370
|
+
_miss = [k for k in ("plan", "result", "task_status", "acceptance", "boundary",
|
|
371
|
+
"active_limit", "done_limit") if k not in _props]
|
|
372
|
+
check("L17 cg schema 已声明 task 参数(客户端传得进来)", not _miss, _miss)
|
|
373
|
+
check("L18 tokens.ALL_OPS 含 task(跨面真源同步)",
|
|
374
|
+
"task" in TK.ALL_OPS, [o for o in TK.ALL_OPS if o == "task"])
|
|
375
|
+
|
|
376
|
+
# ---------------- M. 权限闸 ----------------
|
|
377
|
+
print("\n[M] 权限闸:写权限不够则拒,角色白名单同步")
|
|
378
|
+
_guest = MdCGSecure(os.path.join(tmp, "guest_root"),
|
|
379
|
+
principal=Principal(actor="guest1", role="guest",
|
|
380
|
+
can_write=False, can_admin=False))
|
|
381
|
+
_ge = _raises(lambda: T.upsert(_guest, NAME5, plan="x"))
|
|
382
|
+
check("M1 无写权限角色写任务被拒(权限闸不可绕)",
|
|
383
|
+
isinstance(_ge, AccessDenied), _ge)
|
|
384
|
+
_rec = TK.ROLE_SPECS["record"]
|
|
385
|
+
check("M2 record 角色 ops 白名单含 task 且可写 structural 层",
|
|
386
|
+
"task" in _rec["ops_allow"] and "structural" in _rec["layers_allow"],
|
|
387
|
+
(_rec["ops_allow"], _rec["layers_allow"]))
|
|
388
|
+
_owner = Principal(actor="owner", role="designer", can_write=True,
|
|
389
|
+
can_admin=True, ops_allow=["*"], layers_allow=["*"])
|
|
390
|
+
_narrow = TK.narrowed_principal(_owner, "record")
|
|
391
|
+
check("M3 收窄身份(记录单元)可执行 task(本职:保存过程与结果)",
|
|
392
|
+
_narrow.allows_op("task") is True, _narrow.ops_allow)
|
|
393
|
+
_rcg = MdCGSecure(os.path.join(tmp, "record_root"), principal=_narrow)
|
|
394
|
+
_ro = T.upsert(_rcg, "记录单元-任务探针", plan="x")
|
|
395
|
+
check("M4 收窄身份真实落盘成功(端到端)", _ro.get("ok") is True, _ro)
|
|
396
|
+
finally:
|
|
397
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
398
|
+
|
|
399
|
+
print("\n%s %d 项断言,%d 失败" % ("ALL GREEN" if not _fails else "FAILED",
|
|
400
|
+
_n[0], len(_fails)))
|
|
401
|
+
if _fails:
|
|
402
|
+
for f in _fails:
|
|
403
|
+
print(" - %s" % f)
|
|
404
|
+
return 1
|
|
405
|
+
return 0
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
if __name__ == "__main__":
|
|
409
|
+
sys.exit(main())
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""工具面渐进披露(Pi 移植① 上下文经济学)验收:交接文档 §3①。
|
|
3
|
+
|
|
4
|
+
蓝图验收两条,本测试把它们机械化为可裁决断言:
|
|
5
|
+
① 瘦身前后工具描述 token 数对比(目标 ≥50% 削减,主面 = kernel);
|
|
6
|
+
② 检索/写入功能回归零变化 —— 展开为「工具名 / 参数名 / required / 类型 /
|
|
7
|
+
单枚举值集逐位不变」+「被投影掉的原文可由 op=help 无损取回」。
|
|
8
|
+
|
|
9
|
+
运行:python -m md_cg.test_tool_face
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
from . import mcp_server as ms
|
|
18
|
+
from . import tool_face as tf
|
|
19
|
+
|
|
20
|
+
_ok = 0
|
|
21
|
+
_bad = []
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _check(name, cond, detail=""):
|
|
25
|
+
global _ok
|
|
26
|
+
if cond:
|
|
27
|
+
_ok += 1
|
|
28
|
+
print(" ok %s" % name)
|
|
29
|
+
else:
|
|
30
|
+
_bad.append("%s %s" % (name, detail))
|
|
31
|
+
print(" FAIL %s %s" % (name, detail))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
_ENUM_RE = re.compile(r"[A-Za-z_][\w\-]*(?:\|[A-Za-z_][\w\-]*)+")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _size(tools):
|
|
38
|
+
return len(json.dumps(tools, ensure_ascii=False))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _props(tool):
|
|
42
|
+
return (tool.get("inputSchema") or {}).get("properties") or {}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _required(tool):
|
|
46
|
+
return list((tool.get("inputSchema") or {}).get("required") or [])
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _run():
|
|
50
|
+
origin = ms.ALL_TOOLS
|
|
51
|
+
origin_json = json.dumps(origin, ensure_ascii=False)
|
|
52
|
+
slim = tf.slim_tools(origin)
|
|
53
|
+
by_name_origin = {t["name"]: t for t in origin}
|
|
54
|
+
by_name_slim = {t["name"]: t for t in slim}
|
|
55
|
+
|
|
56
|
+
# ---- ① 尺寸:主面(kernel)与 full 面 ----
|
|
57
|
+
k_before, k_after = _size(ms.KERNEL_TOOLS), _size(tf.slim_tools(ms.KERNEL_TOOLS))
|
|
58
|
+
k_cut = 100.0 * (k_before - k_after) / k_before
|
|
59
|
+
_check("kernel 注入面削减 ≥50%%(%d→%d,%.2f%%)" % (k_before, k_after, k_cut),
|
|
60
|
+
k_cut >= 50.0, "%.2f%%" % k_cut)
|
|
61
|
+
f_before = _size(ms.TOOLS)
|
|
62
|
+
f_after = _size(tf.slim_tools(ms.TOOLS))
|
|
63
|
+
f_cut = 100.0 * (f_before - f_after) / f_before
|
|
64
|
+
_check("full 面削减 ≥20%%(%d→%d,%.2f%%)" % (f_before, f_after, f_cut),
|
|
65
|
+
f_cut >= 20.0, "%.2f%%" % f_cut)
|
|
66
|
+
cg_slim = by_name_slim["cg"]
|
|
67
|
+
_check("cg 工具职责行 ≤160 字符",
|
|
68
|
+
len(cg_slim.get("description") or "") <= 160,
|
|
69
|
+
str(len(cg_slim.get("description") or "")))
|
|
70
|
+
_check("每个工具职责行非空且 ≤160 字符",
|
|
71
|
+
all(0 < len(t.get("description") or "") <= 160 for t in slim),
|
|
72
|
+
str([t["name"] for t in slim if not t.get("description")]))
|
|
73
|
+
|
|
74
|
+
# ---- ② 契约逐位不变 ----
|
|
75
|
+
_check("工具名序列逐位一致",
|
|
76
|
+
[t["name"] for t in origin] == [t["name"] for t in slim],
|
|
77
|
+
str([t["name"] for t in slim]))
|
|
78
|
+
name_ok, type_ok, req_ok, order_ok = True, True, True, True
|
|
79
|
+
for name, o in by_name_origin.items():
|
|
80
|
+
s = by_name_slim[name]
|
|
81
|
+
po, ps = _props(o), _props(s)
|
|
82
|
+
if list(po.keys()) != list(ps.keys()):
|
|
83
|
+
order_ok = False
|
|
84
|
+
if _required(o) != _required(s):
|
|
85
|
+
req_ok = False
|
|
86
|
+
for key, spec in po.items():
|
|
87
|
+
if spec.get("type") != ps[key].get("type"):
|
|
88
|
+
type_ok = False
|
|
89
|
+
_check("参数名序列逐位一致(含顺序)", order_ok)
|
|
90
|
+
_check("required 逐位一致", req_ok)
|
|
91
|
+
_check("参数类型逐位一致", type_ok)
|
|
92
|
+
|
|
93
|
+
enum_ok, enum_detail = True, ""
|
|
94
|
+
externalized = [] # 被整体外置的参数:(tool, param, 原文)
|
|
95
|
+
for name, o in by_name_origin.items():
|
|
96
|
+
ps = _props(by_name_slim[name])
|
|
97
|
+
for key, spec in _props(o).items():
|
|
98
|
+
d0 = spec.get("description") or ""
|
|
99
|
+
d1 = ps[key].get("description")
|
|
100
|
+
if not d0:
|
|
101
|
+
continue
|
|
102
|
+
groups = _ENUM_RE.findall(d0)
|
|
103
|
+
if len(groups) == 1:
|
|
104
|
+
if d1 != groups[0]:
|
|
105
|
+
enum_ok = False
|
|
106
|
+
enum_detail = "%s.%s: %r != %r" % (name, key, d1, groups[0])
|
|
107
|
+
if d1 is None:
|
|
108
|
+
externalized.append((name, key, d0))
|
|
109
|
+
_check("单枚举参数的完整值集保留(调用契约不可省)", enum_ok, enum_detail)
|
|
110
|
+
_check("存在被外置的长尾参数(确实发生了分层)", len(externalized) > 0,
|
|
111
|
+
"%d 个" % len(externalized))
|
|
112
|
+
|
|
113
|
+
# ---- ③ 信息零丢失:被投影掉的原文可由 op=help 无损取回 ----
|
|
114
|
+
miss = []
|
|
115
|
+
for name, key, d0 in externalized:
|
|
116
|
+
r = tf.help_text(origin, query=key)
|
|
117
|
+
got = [h.get("desc") for h in r.get("hits", [])
|
|
118
|
+
if h.get("tool") == name and h.get("param") == key]
|
|
119
|
+
if not got or got[0] != d0:
|
|
120
|
+
miss.append("%s.%s" % (name, key))
|
|
121
|
+
_check("外置参数原文可由 op=help 逐字取回(%d 个)" % len(externalized),
|
|
122
|
+
not miss, str(miss[:5]))
|
|
123
|
+
|
|
124
|
+
tm = tf.help_text(origin, query="cg")
|
|
125
|
+
ok_desc = all(tm["params"][i]["desc"] == _props(by_name_origin["cg"])[p["name"]]["description"]
|
|
126
|
+
for i, p in enumerate(tm["params"]))
|
|
127
|
+
_check("工具级 help 返回真源全参数完整描述(逐字)", ok_desc)
|
|
128
|
+
secs = tf.op_sections(by_name_origin["cg"].get("description") or "")
|
|
129
|
+
bad_ops = [op for op in secs
|
|
130
|
+
if not any(h.get("op") == op and h.get("doc") == secs[op]
|
|
131
|
+
for h in tf.help_text(origin, query=op).get("hits", []))]
|
|
132
|
+
_check("每个 op 的段落原文可由 op=help 取回(%d op)" % len(secs), not bad_ops,
|
|
133
|
+
str(bad_ops[:5]))
|
|
134
|
+
# 分层后的按需面必须完整:op 枚举里的每个 op 都要有说明段,
|
|
135
|
+
# 否则「常驻面不再解释它」+「按需面也查不到」= 真的丢能力。
|
|
136
|
+
enum_ops = set()
|
|
137
|
+
for m in _ENUM_RE.finditer(_props(by_name_origin["cg"])["op"].get("description") or ""):
|
|
138
|
+
enum_ops |= set(m.group(0).split("|"))
|
|
139
|
+
enum_ops.discard("help")
|
|
140
|
+
gap = sorted(o for o in enum_ops if o not in secs)
|
|
141
|
+
_check("op 枚举的每个 op 都有按需说明段(%d 个)" % len(enum_ops), not gap, str(gap))
|
|
142
|
+
|
|
143
|
+
# ---- ④ help 面 ----
|
|
144
|
+
idx = tf.help_text(origin, query=None)
|
|
145
|
+
_check("help 无 query 返回全量工具索引",
|
|
146
|
+
[i["name"] for i in idx["tool_index"]] == [t["name"] for t in origin])
|
|
147
|
+
_check("索引每项职责行非空",
|
|
148
|
+
all(i["brief"].strip() for i in idx["tool_index"]))
|
|
149
|
+
r = tf.help_text(origin, query="write")
|
|
150
|
+
_check("help query=write 命中 cg 的 write 段",
|
|
151
|
+
any(h.get("tool") == "cg" and h.get("op") == "write" and h.get("doc")
|
|
152
|
+
for h in r.get("hits", [])))
|
|
153
|
+
r2 = tf.help_text(origin, query="__no_such_op__")
|
|
154
|
+
_check("help 未命中时诚实报错并给 hint",
|
|
155
|
+
r2.get("ok") is False and "hint" in r2, str(r2)[:80])
|
|
156
|
+
e2e = ms._cg_call(None, {"op": "help", "query": "write"})
|
|
157
|
+
_check("cg(op=help) 端到端可用(经分发层)",
|
|
158
|
+
isinstance(e2e, dict) and e2e.get("ok") is True, str(e2e)[:80])
|
|
159
|
+
|
|
160
|
+
# ---- ⑤ 开关 / 幂等 / 真源不受损 ----
|
|
161
|
+
os.environ["MDCG_TOOL_FACE"] = "full"
|
|
162
|
+
try:
|
|
163
|
+
raw = ms.tools_for_surface()
|
|
164
|
+
finally:
|
|
165
|
+
os.environ.pop("MDCG_TOOL_FACE", None)
|
|
166
|
+
_check("MDCG_TOOL_FACE=full 时返回完整真源",
|
|
167
|
+
json.dumps(raw, ensure_ascii=False) == json.dumps(
|
|
168
|
+
ms.KERNEL_TOOLS if ms.SURFACE != "full" else ms.ALL_TOOLS,
|
|
169
|
+
ensure_ascii=False))
|
|
170
|
+
_check("投影幂等(slim(slim)==slim)",
|
|
171
|
+
json.dumps(tf.slim_tools(slim), ensure_ascii=False)
|
|
172
|
+
== json.dumps(slim, ensure_ascii=False))
|
|
173
|
+
_check("投影不改真源(ALL_TOOLS 逐字未变)",
|
|
174
|
+
json.dumps(ms.ALL_TOOLS, ensure_ascii=False) == origin_json)
|
|
175
|
+
_check("tools_for_surface 与投影一致",
|
|
176
|
+
json.dumps(ms.tools_for_surface(), ensure_ascii=False)
|
|
177
|
+
== json.dumps(tf.slim_tools(ms.KERNEL_TOOLS if ms.SURFACE != "full"
|
|
178
|
+
else ms.ALL_TOOLS), ensure_ascii=False))
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def main():
|
|
182
|
+
print("[tool_face] 工具面渐进披露验收")
|
|
183
|
+
_run()
|
|
184
|
+
print("\n%d passed, %d failed" % (_ok, len(_bad)))
|
|
185
|
+
return 1 if _bad else 0
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
if __name__ == "__main__":
|
|
189
|
+
sys.exit(main())
|