@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
"""G6 · node_ 结构提炼抽检工单与扩批闸门(P40 设计态验收)。
|
|
3
3
|
|
|
4
|
-
对照 `docs/认知图_G4-G8缺口裁定单_v0.1.md` §四 G6:`node_*`(P37 派生记忆)
|
|
4
|
+
对照 `docs/mdcg/认知图_G4-G8缺口裁定单_v0.1.md` §四 G6:`node_*`(P37 派生记忆)
|
|
5
5
|
自带 `ccg_exempt=true`,只声明「功能名 / 生效条件」,`子功能/执行/不适用条件` 全缺。
|
|
6
6
|
条件 A 要求**先抽 20 条做提炼预演、人工核对提炼口径,绝不盲跑全量**。
|
|
7
7
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
"""G7 · 自主演化巡检(把「有能力」变成「有驱动」)(P41 设计态验收)。
|
|
3
3
|
|
|
4
|
-
对照 `docs/认知图_G4-G8缺口裁定单_v0.1.md` §五 G7:固化 / 重要性 / 去污染三类自我
|
|
4
|
+
对照 `docs/mdcg/认知图_G4-G8缺口裁定单_v0.1.md` §五 G7:固化 / 重要性 / 去污染三类自我
|
|
5
5
|
演化动作早已具备,但**没有任何驱动源**周期性问「现在有多少该固化 / 该重算的候选」,
|
|
6
6
|
于是能力被饿死。本项给常驻循环挂上**只读**演化巡检 + **受控**自愈。
|
|
7
7
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
"""G8 · 派生溯源(新增即建链 + 失败不阻断 + 悬空可检出)(P42 设计态验收)。
|
|
3
3
|
|
|
4
|
-
对照 `docs/认知图_G4-G8缺口裁定单_v0.1.md` §六 G8:节点拆并/派生后**血缘丢失**,
|
|
4
|
+
对照 `docs/mdcg/认知图_G4-G8缺口裁定单_v0.1.md` §六 G8:节点拆并/派生后**血缘丢失**,
|
|
5
5
|
库根无 `_link.jsonl`、frontmatter 的派生字段全空,事后无从回答「这个节点从哪来」。
|
|
6
6
|
本项只对**新增**节点常态化建链(历史不回填),且立三条硬约束。
|
|
7
7
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
"""§七 · 召回分池与降权(P43 设计态验收)。
|
|
3
3
|
|
|
4
|
-
对照 `docs/认知图_G4-G8缺口裁定单_v0.1.md` §七:索引/产物类节点(`code_`/`doc_`)
|
|
4
|
+
对照 `docs/mdcg/认知图_G4-G8缺口裁定单_v0.1.md` §七:索引/产物类节点(`code_`/`doc_`)
|
|
5
5
|
词面更易命中,在 `GLOBAL_CAP` 单条截断点**先把知识节点挤掉**。本项把截断额度
|
|
6
6
|
**显式分池**(不是抬高额度),并立四条纪律。
|
|
7
7
|
|
|
@@ -41,7 +41,11 @@ PASS = FAIL = 0
|
|
|
41
41
|
FAILS = []
|
|
42
42
|
|
|
43
43
|
BODY = "召回截断 索引 分池 内容"
|
|
44
|
-
|
|
44
|
+
# 多词查询:索引类含全部查询词、知识类缺「分池」→ 相关度有**区分度**。
|
|
45
|
+
# 原语料下 16 条分数全等(覆盖度口径),截断谁入榜只能由传入序决定——而传入序
|
|
46
|
+
# 本身不确定(目录枚举 + 并行 prefetch),断言 `off=6` 实为顺序抽签的产物。
|
|
47
|
+
QUERY = "召回截断 分池"
|
|
48
|
+
KBODY = "召回截断 索引 内容"
|
|
45
49
|
|
|
46
50
|
|
|
47
51
|
def ok(cond, label):
|
|
@@ -71,7 +75,12 @@ def _snapshot(root):
|
|
|
71
75
|
|
|
72
76
|
|
|
73
77
|
def _mk(root):
|
|
74
|
-
"""6 个索引类(code_/doc_)+ 10
|
|
78
|
+
"""6 个索引类(code_/doc_)+ 10 个知识类。
|
|
79
|
+
|
|
80
|
+
§七 的真实机制是「索引/产物类节点**词面命中率更高**」(正文短、查询词占比大),
|
|
81
|
+
故索引类正文短、知识类正文长(含背景说明),使索引类相似度**确实**高于知识类
|
|
82
|
+
——挤占由相关度驱动,不依赖写入顺序(截断依据已改为相关度,见 `cut_by_relevance`)。
|
|
83
|
+
"""
|
|
75
84
|
cg = MdCGOS(root, actor="test")
|
|
76
85
|
for i in range(4):
|
|
77
86
|
cg.add(f"code_{i}", f"# 功能名:索引条目 {i}\n# 生效条件:条件 I\n\n{BODY} {i}\n",
|
|
@@ -80,7 +89,10 @@ def _mk(root):
|
|
|
80
89
|
cg.add(f"doc_{i}", f"# 功能名:文档索引 {i}\n# 生效条件:条件 D\n\n{BODY} {i}\n",
|
|
81
90
|
layer="knowledge", tags=["index"])
|
|
82
91
|
for i in range(10):
|
|
83
|
-
cg.add(f"kp_{i}",
|
|
92
|
+
cg.add(f"kp_{i}",
|
|
93
|
+
f"# 功能名:知识 {i}\n# 生效条件:条件 K\n\n{KBODY} {i}\n"
|
|
94
|
+
f"备注:本条为知识类条目,含背景说明、推理链与引用出处,正文较长,"
|
|
95
|
+
f"查询词在全文中的占比因此显著低于索引类条目。\n",
|
|
84
96
|
layer="knowledge")
|
|
85
97
|
cg.add("n_rej", f"# 功能名:被否决\n# 生效条件:条件 R\n\n{BODY} 否\n",
|
|
86
98
|
layer="rejected")
|
|
@@ -238,12 +250,27 @@ def main():
|
|
|
238
250
|
# 但「乘数可复算」这一被测语义不变。
|
|
239
251
|
base = {r[0]["id"]: r[1] for r in off}
|
|
240
252
|
sc = {r[0]["id"]: r[1] for r in on}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
253
|
+
|
|
254
|
+
def _pick_idx(d):
|
|
255
|
+
"""挑一条「关闭态也有基础分」的索引类入榜条目。
|
|
256
|
+
|
|
257
|
+
截断依据改为相关度后,具体哪条索引入榜由分数序决定(不再是固定的
|
|
258
|
+
`code_0`——那正是按传入序截断时代的产物),故动态取。
|
|
259
|
+
"""
|
|
260
|
+
return next((i for i in d if PL.pool_of(i) == "index" and i in base), None)
|
|
261
|
+
|
|
262
|
+
iid = _pick_idx(sc)
|
|
263
|
+
ok(iid is not None, "(8) 分池态含索引类条目(额度保底生效)")
|
|
264
|
+
ok(iid is not None and abs(sc[iid] - base[iid] * 0.6) < 1e-9,
|
|
265
|
+
f"(8) 索引类命中分 == 基础分 × 0.6({iid}: base={base.get(iid, 0.0):.6f} → "
|
|
266
|
+
f"{sc.get(iid, float('nan')):.6f})")
|
|
267
|
+
# 知识类权重 1.0 → 命中分应与关闭态同源分一致。不硬编码编号:关闭态额度
|
|
268
|
+
# 被索引类占满,只余 2 条知识类入榜,与分池态取的 6 条未必有同编号交集
|
|
269
|
+
# (全同分下由稳定序决定),故按「关闭态知识分基准」比对。
|
|
270
|
+
base_kp = [v for k, v in base.items() if k.startswith("kp_")]
|
|
271
|
+
sc_kp = [v for k, v in sc.items() if k.startswith("kp_")]
|
|
272
|
+
ok(base_kp and sc_kp and all(abs(v - base_kp[0]) < 1e-9 for v in sc_kp),
|
|
273
|
+
f"(8) 知识类命中分不被削(权重 1.0;base={base_kp[:1]} → on={sc_kp[:2]})")
|
|
247
274
|
ok(PL.weight_of("code_x", {}, True) == PL.WEIGHTS["index"]["weight"],
|
|
248
275
|
"(8) weight_of 与表内系数一致")
|
|
249
276
|
alt = {"knowledge": {"cap_ratio": 0.5, "weight": 1.0},
|
|
@@ -251,9 +278,10 @@ def main():
|
|
|
251
278
|
"negative": {"cap_ratio": 0.1, "weight": 0.5}}
|
|
252
279
|
on2, _m2 = cg.search(QUERY, k=20, record=False, pools=alt)
|
|
253
280
|
sc2 = {r[0]["id"]: r[1] for r in on2}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
f"{
|
|
281
|
+
iid2 = _pick_idx(sc2)
|
|
282
|
+
ok(iid2 is not None and abs(sc2[iid2] - base[iid2] * 0.3) < 1e-9,
|
|
283
|
+
f"(8) 换表即换行为({iid2}: base×0.3={base.get(iid2, 0.0) * 0.3:.6f} → "
|
|
284
|
+
f"{sc2.get(iid2, float('nan')):.6f})")
|
|
257
285
|
ok(PL.caps(10, PL.resolve(alt)) == {"knowledge": 4, "index": 4, "negative": 2},
|
|
258
286
|
f"(8) 换表额度同步可复算(实得 {PL.caps(10, PL.resolve(alt))})")
|
|
259
287
|
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""md_cg · Pi⑦② read 截断 + 续读提示(_clip_text / _node_view / schema 落点)
|
|
3
|
+
|
|
4
|
+
验收三件事:
|
|
5
|
+
① 单次 read 返回受「2000 行 / 50KB」双阈值约束,且**不静默丢内容**——
|
|
6
|
+
截断必带 total_lines / total_bytes / next_offset / note;
|
|
7
|
+
② 续读按 offset 连续取回可拼回原文(不丢不重,可裁决的等价性判据);
|
|
8
|
+
③ 小内容行为逐位兼容(不新增键、content 原样)——防回归。
|
|
9
|
+
|
|
10
|
+
运行:python -m md_cg.test_read_clip
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
from .mcp_server import KERNEL_TOOLS, READ_MAX_BYTES, READ_MAX_LINES, \
|
|
17
|
+
_cg_call, _clip_text, _node_view
|
|
18
|
+
|
|
19
|
+
_ok = 0
|
|
20
|
+
_bad = []
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def check(name, cond, detail=""):
|
|
24
|
+
global _ok
|
|
25
|
+
if cond:
|
|
26
|
+
_ok += 1
|
|
27
|
+
print(f" [ok] {name}" + (f" · {detail}" if detail else ""))
|
|
28
|
+
else:
|
|
29
|
+
_bad.append(name)
|
|
30
|
+
print(f" [FAIL] {name} · {detail}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _FakeCG:
|
|
34
|
+
"""只实现 read 面所需的最小取节点契约。"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, nodes):
|
|
37
|
+
self._nodes = nodes
|
|
38
|
+
|
|
39
|
+
def get(self, nid, *a, **k):
|
|
40
|
+
return self._nodes.get(nid)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def main():
|
|
44
|
+
print("[A] _clip_text 双阈值与续读语义")
|
|
45
|
+
small = "第一行\n第二行\n第三行"
|
|
46
|
+
c = _clip_text(small)
|
|
47
|
+
check("A1 小文本原样返回(note 空串、无截断位点)",
|
|
48
|
+
c["text"] == small and c["truncated"] is False
|
|
49
|
+
and c["next_offset"] is None and c["total_lines"] == 3
|
|
50
|
+
and c["note"] == "", str(c)[:160])
|
|
51
|
+
|
|
52
|
+
many = "\n".join(f"line-{i:05d}" for i in range(READ_MAX_LINES + 500))
|
|
53
|
+
c = _clip_text(many)
|
|
54
|
+
check("A2 行数上限:超 2000 行截断且给出续读位点",
|
|
55
|
+
c["truncated"] is True and c["returned_lines"] == READ_MAX_LINES
|
|
56
|
+
and c["next_offset"] == READ_MAX_LINES + 1
|
|
57
|
+
and str(READ_MAX_LINES) in c["note"], c["note"])
|
|
58
|
+
|
|
59
|
+
wide = "\n".join("x" * 60 for _ in range(3000)) # 约定 180KB > 50KB
|
|
60
|
+
c = _clip_text(wide)
|
|
61
|
+
check("A3 字节上限生效(行未满 2000 但字节先到顶)",
|
|
62
|
+
c["truncated"] is True and c["returned_lines"] < READ_MAX_LINES
|
|
63
|
+
and len(c["text"].encode("utf-8")) <= READ_MAX_BYTES
|
|
64
|
+
and str(READ_MAX_BYTES) in c["note"], f"{c['returned_lines']}行/{c['total_bytes']}B")
|
|
65
|
+
|
|
66
|
+
# 续读拼接等价性:按 next_offset 连续取回,拼回必须与原文逐字一致
|
|
67
|
+
got, off, hops = [], 0, 0
|
|
68
|
+
while True:
|
|
69
|
+
c = _clip_text(many, offset=off)
|
|
70
|
+
got.append(c["text"])
|
|
71
|
+
hops += 1
|
|
72
|
+
if not c["truncated"]:
|
|
73
|
+
break
|
|
74
|
+
off = c["next_offset"]
|
|
75
|
+
if hops > 10:
|
|
76
|
+
break
|
|
77
|
+
check("A4 续读拼接 == 原文(不丢不重)",
|
|
78
|
+
"\n".join(got) == many and hops == 2, f"hops={hops}")
|
|
79
|
+
|
|
80
|
+
c = _clip_text("a\nb", offset=99)
|
|
81
|
+
check("A5 offset 越界:返回空并如实说明(不假装有内容)",
|
|
82
|
+
c["text"] == "" and c["truncated"] is False and "超出总行数" in c["note"],
|
|
83
|
+
c["note"])
|
|
84
|
+
|
|
85
|
+
c = _clip_text("y" * (READ_MAX_BYTES + 100))
|
|
86
|
+
check("A6 单行超字节上限也保底返回一行(不返回空)",
|
|
87
|
+
c["returned_lines"] == 1 and c["truncated"] is False
|
|
88
|
+
and len(c["text"]) == READ_MAX_BYTES + 100, str(c["returned_lines"]))
|
|
89
|
+
|
|
90
|
+
print("[B] _node_view 兼容与截断字段")
|
|
91
|
+
node = {"id": "n1", "path": "/p/n1.md", "frontmatter": {"layer": "knowledge"},
|
|
92
|
+
"content": "短内容"}
|
|
93
|
+
v = _node_view(node)
|
|
94
|
+
check("B1 小节点键集不变(零回归)",
|
|
95
|
+
set(v) == {"id", "path", "frontmatter", "content"}
|
|
96
|
+
and v["content"] == "短内容", str(sorted(v)))
|
|
97
|
+
check("B2 空节点维持 None(旧契约)", _node_view(None) is None)
|
|
98
|
+
|
|
99
|
+
big = dict(node, content=many)
|
|
100
|
+
v = _node_view(big)
|
|
101
|
+
check("B3 大节点截断 + 审计字段(truncated/next_offset/note)",
|
|
102
|
+
v["truncated"] is True and v["next_offset"] == READ_MAX_LINES + 1
|
|
103
|
+
and v["content_lines"] == READ_MAX_LINES + 500
|
|
104
|
+
and v["content"].split("\n")[-1] == f"line-{READ_MAX_LINES - 1:05d}",
|
|
105
|
+
str({k: v[k] for k in ("truncated", "next_offset", "content_lines")}))
|
|
106
|
+
v2 = _node_view(big, offset=v["next_offset"])
|
|
107
|
+
check("B4 按 next_offset 续读可取回尾部且拼回原文",
|
|
108
|
+
"\n".join([v["content"], v2["content"]]) == many
|
|
109
|
+
and v2["offset"] == READ_MAX_LINES + 1, str(v2["offset"]))
|
|
110
|
+
|
|
111
|
+
print("[C] 工具面落点(schema + 分发透传)")
|
|
112
|
+
cg = next(t for t in KERNEL_TOOLS if t["name"] == "cg")
|
|
113
|
+
props = cg["inputSchema"]["properties"]
|
|
114
|
+
check("C1 cg schema 暴露 offset 且描述含续读语义",
|
|
115
|
+
props["offset"]["type"] == "integer" and "next_offset" in props["offset"]["description"])
|
|
116
|
+
check("C2 read 面描述声明截断与续读(调用方可知情)",
|
|
117
|
+
"next_offset" in cg["description"] and "截断" in cg["description"])
|
|
118
|
+
|
|
119
|
+
fcg = _FakeCG({"n1": node})
|
|
120
|
+
out = _cg_call(fcg, {"op": "read", "node_id": "n1"})
|
|
121
|
+
check("C3 未传 offset:行为与旧版一致",
|
|
122
|
+
out["content"] == "短内容" and "truncated" not in out, str(out)[:160])
|
|
123
|
+
fcg = _FakeCG({"n1": big})
|
|
124
|
+
out = _cg_call(fcg, {"op": "read", "node_id": "n1", "offset": READ_MAX_LINES + 1})
|
|
125
|
+
check("C4 offset 经 _cg_call 透传到节点视图(真实续读通路)",
|
|
126
|
+
out["offset"] == READ_MAX_LINES + 1 and out["truncated"] is False
|
|
127
|
+
and out["content"].startswith(f"line-{READ_MAX_LINES:05d}")
|
|
128
|
+
and out["content"].endswith("line-02499"), str(out["offset"]))
|
|
129
|
+
|
|
130
|
+
print(f"\n结果:{_ok} 通过 / {len(_bad)} 失败")
|
|
131
|
+
if _bad:
|
|
132
|
+
print("失败项:" + "、".join(_bad))
|
|
133
|
+
return 1 if _bad else 0
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
if __name__ == "__main__":
|
|
137
|
+
sys.exit(main())
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""md_cg · 审核队列 conformance(数据健康不变量,pi storage conformance 口径)
|
|
3
|
+
|
|
4
|
+
把存储承诺写成断言,防止未来改动静默破坏:
|
|
5
|
+
① strict 锁语义:不能丢的写超时必须显式报错,而非静默放行退化为无锁并发
|
|
6
|
+
② 幂等对账:同内容重试(超时重试/崩溃恢复)→ 幂等返回既有 pid,不重复入队
|
|
7
|
+
③ 已裁决内容的对账:accepted/rejected 内容重提 → 返回原 pid + 终态
|
|
8
|
+
④ 存量兼容:无 payload_hash 的旧格式记录参与对账(现算)
|
|
9
|
+
⑤ 级联出清:主提案终态裁决后,同内容存量兄弟提案自动关闭
|
|
10
|
+
⑥ close/reopen 状态保持:重开实例后队列视图一致
|
|
11
|
+
⑦ 并发幂等(真多进程):N 进程同内容并发 propose → 恰好 1 条、pid 全同
|
|
12
|
+
⑧ 并发不丢(真多进程):N 进程×M 条不同内容 → inbox 读回恰好 N×M 条
|
|
13
|
+
⑨ decisions 并发不丢(真多进程):并发裁决 → 记录恰好 N 条且指纹完整
|
|
14
|
+
|
|
15
|
+
运行:python -m md_cg.test_review_conformance
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import hashlib
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import shutil
|
|
23
|
+
import subprocess
|
|
24
|
+
import sys
|
|
25
|
+
import tempfile
|
|
26
|
+
import time
|
|
27
|
+
|
|
28
|
+
from .fsutil import FileLock, append_jsonl
|
|
29
|
+
from .mdcos import MdCGOS
|
|
30
|
+
from .security import Principal
|
|
31
|
+
from .mdcos import MdCGSecure
|
|
32
|
+
|
|
33
|
+
PASS = FAIL = 0
|
|
34
|
+
FAILS = []
|
|
35
|
+
|
|
36
|
+
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
37
|
+
|
|
38
|
+
WORKER = '''# -*- coding: utf-8 -*-
|
|
39
|
+
"""conformance worker:子进程内建 MdCGOS 执行单次 propose / review_decide。"""
|
|
40
|
+
import json
|
|
41
|
+
import sys
|
|
42
|
+
|
|
43
|
+
sys.path.insert(0, sys.argv[1])
|
|
44
|
+
from md_cg.mdcos import MdCGOS
|
|
45
|
+
|
|
46
|
+
root, mode = sys.argv[2], sys.argv[3]
|
|
47
|
+
arg = sys.argv[4] if len(sys.argv) > 4 else ""
|
|
48
|
+
cg = MdCGOS(root, actor="conf-worker")
|
|
49
|
+
if mode == "propose": # arg = content
|
|
50
|
+
print(json.dumps(cg.propose("w-node", arg, layer="knowledge", tags=["w"])))
|
|
51
|
+
elif mode == "propose_n": # arg = json list of contents
|
|
52
|
+
out = [cg.propose("w-node-%d" % i, c, layer="knowledge")
|
|
53
|
+
for i, c in enumerate(json.loads(arg))]
|
|
54
|
+
print(json.dumps(out))
|
|
55
|
+
elif mode == "decide": # arg = pid
|
|
56
|
+
print(json.dumps(cg.review_decide(arg, "reject", reason="conformance")))
|
|
57
|
+
'''
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def check(name, cond, detail=""):
|
|
61
|
+
global PASS, FAIL
|
|
62
|
+
if cond:
|
|
63
|
+
PASS += 1
|
|
64
|
+
print(f" [PASS] {name}" + (f" · {detail}" if detail else ""))
|
|
65
|
+
else:
|
|
66
|
+
FAIL += 1
|
|
67
|
+
FAILS.append(name)
|
|
68
|
+
print(f" [FAIL] {name} · {detail}")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _ensure_worker(workdir):
|
|
72
|
+
wk = os.path.join(workdir, "worker.py")
|
|
73
|
+
if not os.path.exists(wk):
|
|
74
|
+
with open(wk, "w", encoding="utf-8") as f:
|
|
75
|
+
f.write(WORKER)
|
|
76
|
+
return wk
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _run_worker(workdir, root, mode, arg="", timeout=120):
|
|
80
|
+
"""子进程跑 worker(argv 列表 + UTF-8 + shell=False,纪律 15)。"""
|
|
81
|
+
wk = _ensure_worker(workdir)
|
|
82
|
+
env = dict(os.environ, PYTHONUTF8="1")
|
|
83
|
+
return subprocess.run(
|
|
84
|
+
[sys.executable, wk, REPO, root, mode, arg],
|
|
85
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
86
|
+
timeout=timeout, env=env, shell=False)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def main():
|
|
90
|
+
workdir = tempfile.mkdtemp(prefix="mdcg_conf_wk_")
|
|
91
|
+
root = tempfile.mkdtemp(prefix="mdcg_conf_")
|
|
92
|
+
root2 = root2b = root3 = root4 = ""
|
|
93
|
+
_ensure_worker(workdir)
|
|
94
|
+
try:
|
|
95
|
+
cg = MdCGOS(root, actor="test")
|
|
96
|
+
|
|
97
|
+
# ---------------- ① strict 锁语义 ----------------
|
|
98
|
+
print("\n【1】strict 锁:不能丢的写超时报错,不放行")
|
|
99
|
+
lock_path = os.path.join(root, "hippocampus", "inbox.jsonl")
|
|
100
|
+
blocker = FileLock(lock_path, timeout=30.0)
|
|
101
|
+
blocker.__enter__()
|
|
102
|
+
t0 = time.time()
|
|
103
|
+
raised = None
|
|
104
|
+
try:
|
|
105
|
+
FileLock(lock_path, timeout=0.3, strict=True).__enter__()
|
|
106
|
+
except TimeoutError as e:
|
|
107
|
+
raised = e
|
|
108
|
+
check("strict 占锁下获锁超时抛 TimeoutError", raised is not None,
|
|
109
|
+
str(raised)[:60])
|
|
110
|
+
check("strict 超时不做长等待", time.time() - t0 < 5.0,
|
|
111
|
+
f"耗时={time.time()-t0:.2f}s")
|
|
112
|
+
soft = FileLock(lock_path, timeout=0.3).__enter__()
|
|
113
|
+
check("非 strict 维持放行语义(best-effort 簿记不受影响)",
|
|
114
|
+
soft.acquired is False, f"acquired={soft.acquired}")
|
|
115
|
+
blocker.__exit__()
|
|
116
|
+
|
|
117
|
+
# ---------------- ② 幂等对账:pending ----------------
|
|
118
|
+
print("\n【2】幂等对账:同内容重试返回既有 pid")
|
|
119
|
+
c_body = "# 功能名:conformance 幂等探针\n\n同内容重试不应重复入队。\n"
|
|
120
|
+
p1 = cg.propose("dup1", c_body, layer="knowledge")
|
|
121
|
+
p2 = cg.propose("dup1", c_body, layer="knowledge") # 模拟重试
|
|
122
|
+
p3 = cg.propose("dup2", c_body, layer="knowledge") # 换 node_id 同内容
|
|
123
|
+
check("三次 propose 返回同一 pid", p1 == p2 == p3,
|
|
124
|
+
f"{p1} / {p2} / {p3}")
|
|
125
|
+
pend = [r for r in cg.review_list() if r.get("id") in ("dup1", "dup2")]
|
|
126
|
+
check("inbox 恰好 1 条该内容提案", len(pend) == 1, f"pending={len(pend)}")
|
|
127
|
+
info = cg.propose("dup1", c_body, layer="knowledge", info=True)
|
|
128
|
+
check("info=True 透出去重信息",
|
|
129
|
+
info.get("dedup") is True and info.get("dup_of") == p1
|
|
130
|
+
and info.get("dup_status") == "pending", str(info))
|
|
131
|
+
|
|
132
|
+
# ---------------- ③ 幂等对账:已裁决内容 ----------------
|
|
133
|
+
print("\n【3】幂等对账:已裁决内容重提返回原 pid + 终态")
|
|
134
|
+
d = cg.review_decide(p1, "accept")
|
|
135
|
+
check("accept 落库", d.get("ok") is True, str(d.get("error") or "")[:60])
|
|
136
|
+
again = cg.propose("dup3", c_body, layer="knowledge", info=True)
|
|
137
|
+
check("accepted 内容重提幂等返回原 pid",
|
|
138
|
+
again.get("dedup") is True and again.get("dup_of") == p1
|
|
139
|
+
and again.get("dup_status") == "accepted", str(again))
|
|
140
|
+
check("重提未产生新队列条目",
|
|
141
|
+
not any(r.get("id") == "dup3" for r in cg.review_list()), "")
|
|
142
|
+
|
|
143
|
+
# ---------------- ④ 存量兼容:无 payload_hash 旧记录 ----------------
|
|
144
|
+
print("\n【4】存量兼容:旧格式记录(无 payload_hash)参与对账")
|
|
145
|
+
# 独立内容探针(c_body 在【3】已被 accept,会抢先命中终态分支)
|
|
146
|
+
leg_body = "# 功能名:存量兼容探针\n\n旧账无 hash 也要能对账。\n"
|
|
147
|
+
old = {"t": time.time(), "pid": "prop_legacy0001", "id": "legacy1",
|
|
148
|
+
"content": leg_body, "layer": "knowledge", "tags": [],
|
|
149
|
+
"condition_space": {}, "verify": {}, "verify_hash": "",
|
|
150
|
+
"extra": {}, "actor": "legacy"}
|
|
151
|
+
append_jsonl(cg.inbox_log, old)
|
|
152
|
+
leg = cg.propose("dup4", leg_body, layer="knowledge", info=True)
|
|
153
|
+
check("对账命中存量记录(现算 hash)",
|
|
154
|
+
leg.get("dedup") is True and leg.get("dup_of") == "prop_legacy0001"
|
|
155
|
+
and leg.get("dup_status") == "pending", str(leg))
|
|
156
|
+
|
|
157
|
+
# ---------------- ⑤ 级联出清:同 hash 存量兄弟 ----------------
|
|
158
|
+
print("\n【5】级联出清:主提案终态后兄弟自动关闭")
|
|
159
|
+
sib_body = "# 功能名:级联探针\n\n存量重复入队产物。\n"
|
|
160
|
+
main_pid = cg.propose("casc_main", sib_body, layer="knowledge")
|
|
161
|
+
# 手工构造存量兄弟(绕过 propose 的入队查重,模拟修复上线前的旧账)
|
|
162
|
+
for i, nid in enumerate(("casc_sib_a", "casc_sib_b")):
|
|
163
|
+
ph = hashlib.sha1(sib_body.strip().encode("utf-8")).hexdigest()[:12]
|
|
164
|
+
append_jsonl(cg.inbox_log, {
|
|
165
|
+
"t": time.time(), "pid": f"prop_sib{i}000000", "id": nid,
|
|
166
|
+
"content": sib_body, "layer": "knowledge", "tags": [],
|
|
167
|
+
"condition_space": {}, "payload_hash": ph,
|
|
168
|
+
"verify": {}, "verify_hash": "", "extra": {}, "actor": "legacy"})
|
|
169
|
+
sibs_before = [r for r in cg.review_list()
|
|
170
|
+
if r.get("id") in ("casc_main", "casc_sib_a", "casc_sib_b")]
|
|
171
|
+
check("裁决前 3 条同内容提案全部可见", len(sibs_before) == 3,
|
|
172
|
+
f"n={len(sibs_before)}")
|
|
173
|
+
dmain = cg.review_decide(main_pid, "accept")
|
|
174
|
+
check("accept 成功且级联出清 2 条兄弟",
|
|
175
|
+
dmain.get("ok") and len(dmain.get("cascade_closed") or []) == 2,
|
|
176
|
+
f"cascade={dmain.get('cascade_closed')}")
|
|
177
|
+
left = [r for r in cg.review_list()
|
|
178
|
+
if r.get("id") in ("casc_main", "casc_sib_a", "casc_sib_b")]
|
|
179
|
+
check("裁决后队列无同内容残留", len(left) == 0, f"left={len(left)}")
|
|
180
|
+
decs = [x for x in cg.decisions() if "cascade_dedup" in (x.get("reason") or "")]
|
|
181
|
+
check("级联裁决落 decisions 且带说明", len(decs) == 2
|
|
182
|
+
and all(x.get("status") == "rejected" for x in decs),
|
|
183
|
+
f"n={len(decs)}")
|
|
184
|
+
|
|
185
|
+
# ---------------- ⑥ close/reopen 状态保持 ----------------
|
|
186
|
+
print("\n【6】close/reopen:重开实例队列视图一致")
|
|
187
|
+
cg2 = MdCGOS(root, actor="test-reopen")
|
|
188
|
+
rl2 = cg2.review_list()
|
|
189
|
+
check("重开后 review_list 与原实例一致", rl2 == cg.review_list(),
|
|
190
|
+
f"n={len(rl2)}")
|
|
191
|
+
|
|
192
|
+
# ---------------- ⑦⑧⑨ 真多进程并发 ----------------
|
|
193
|
+
print("\n【7】并发幂等:4 进程同内容并发 propose")
|
|
194
|
+
root2 = tempfile.mkdtemp(prefix="mdcg_conf_p_")
|
|
195
|
+
content = "# 功能名:并发幂等探针\n\n四进程同时入队只应产生一条。\n"
|
|
196
|
+
procs = [subprocess.Popen(
|
|
197
|
+
[sys.executable, os.path.join(workdir, "worker.py"), REPO,
|
|
198
|
+
root2, "propose", content],
|
|
199
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False,
|
|
200
|
+
env=dict(os.environ, PYTHONUTF8="1")) for _ in range(4)]
|
|
201
|
+
pids = []
|
|
202
|
+
for p in procs:
|
|
203
|
+
out, _err = p.communicate(timeout=120)
|
|
204
|
+
pids.append(json.loads(out.strip()))
|
|
205
|
+
check("4 进程返回同一 pid", len(set(pids)) == 1, str(pids))
|
|
206
|
+
inbox2 = os.path.join(root2, "hippocampus", "inbox.jsonl")
|
|
207
|
+
with open(inbox2, encoding="utf-8") as f:
|
|
208
|
+
lines = [ln for ln in f if ln.strip()]
|
|
209
|
+
check("inbox 恰好 1 条(无重复入队)", len(lines) == 1,
|
|
210
|
+
f"lines={len(lines)}")
|
|
211
|
+
rec1 = json.loads(lines[0])
|
|
212
|
+
check("入队记录带 payload_hash", bool(rec1.get("payload_hash")), "")
|
|
213
|
+
|
|
214
|
+
print("\n【8】并发不丢:4 进程×5 条不同内容")
|
|
215
|
+
root2b = tempfile.mkdtemp(prefix="mdcg_conf_p2_")
|
|
216
|
+
contents = [json.dumps([
|
|
217
|
+
f"# 功能名:并发不丢 {i}-{j}\n\n第 {i} 进程第 {j} 条。\n"
|
|
218
|
+
for j in range(5)]) for i in range(4)]
|
|
219
|
+
procs = [subprocess.Popen(
|
|
220
|
+
[sys.executable, os.path.join(workdir, "worker.py"), REPO,
|
|
221
|
+
root2b, "propose_n", contents[i]],
|
|
222
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False,
|
|
223
|
+
env=dict(os.environ, PYTHONUTF8="1")) for i in range(4)]
|
|
224
|
+
for p in procs:
|
|
225
|
+
p.communicate(timeout=180)
|
|
226
|
+
with open(os.path.join(root2b, "hippocampus", "inbox.jsonl"),
|
|
227
|
+
encoding="utf-8") as f:
|
|
228
|
+
all_lines = [ln for ln in f if ln.strip()]
|
|
229
|
+
recs = []
|
|
230
|
+
bad = 0
|
|
231
|
+
for ln in all_lines:
|
|
232
|
+
try:
|
|
233
|
+
recs.append(json.loads(ln))
|
|
234
|
+
except ValueError:
|
|
235
|
+
bad += 1
|
|
236
|
+
check("4×5 并发后读回恰好 20 条(一条不丢)",
|
|
237
|
+
len(recs) == 20 and bad == 0, f"read={len(recs)} bad={bad}")
|
|
238
|
+
check("pid 互不重复", len({r.get("pid") for r in recs}) == 20, "")
|
|
239
|
+
|
|
240
|
+
# 【8c】pid 唯一性守卫(2026-09-16 取证修复):旧式
|
|
241
|
+
# "prop_" + _sig(node_id + time.time()) 无进程熵,同一时刻多 worker
|
|
242
|
+
# 用相同 node_id 入队会碰撞——本项在单进程内高频连续入队,把
|
|
243
|
+
# 「同一时刻多次 propose」这一碰撞条件压缩到必然暴露,防回归。
|
|
244
|
+
root2c = tempfile.mkdtemp(prefix="mdcg_conf_p3_")
|
|
245
|
+
cg2c = MdCGOS(root2c, actor="test")
|
|
246
|
+
pids2c = [cg2c.propose("same-node", f"# 功能名:唯一性 {i}\n\n内容 {i}。\n",
|
|
247
|
+
layer="knowledge") for i in range(20)]
|
|
248
|
+
check("同 node_id 高频连续 propose:20 pid 互不重复",
|
|
249
|
+
len(set(pids2c)) == 20, f"uniq={len(set(pids2c))}/20")
|
|
250
|
+
check("pid 复现格式(prop_ + 12 hex,调用方依赖)",
|
|
251
|
+
all(isinstance(p, str) and len(p) == 17 and p.startswith("prop_")
|
|
252
|
+
and all(ch in "0123456789abcdef" for ch in p[5:])
|
|
253
|
+
for p in pids2c), pids2c[0])
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
print("\n【9】decisions 并发:4 进程各裁决 1 个 pid")
|
|
257
|
+
root3 = tempfile.mkdtemp(prefix="mdcg_conf_d_")
|
|
258
|
+
cg3 = MdCGOS(root3, actor="test")
|
|
259
|
+
pids3 = [cg3.propose(f"cc{i}", f"# 功能名:并发裁决 {i}\n\n内容 {i}。\n",
|
|
260
|
+
layer="knowledge") for i in range(4)]
|
|
261
|
+
procs = [subprocess.Popen(
|
|
262
|
+
[sys.executable, os.path.join(workdir, "worker.py"), REPO,
|
|
263
|
+
root3, "decide", pids3[i]],
|
|
264
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False,
|
|
265
|
+
env=dict(os.environ, PYTHONUTF8="1")) for i in range(4)]
|
|
266
|
+
outs = []
|
|
267
|
+
for p in procs:
|
|
268
|
+
out, _err = p.communicate(timeout=180)
|
|
269
|
+
outs.append(out.strip())
|
|
270
|
+
check("4 项裁决全部成功", all(json.loads(o).get("ok") for o in outs),
|
|
271
|
+
str([o[:40] for o in outs]))
|
|
272
|
+
drecs = cg3.decisions()
|
|
273
|
+
check("decisions 读回恰好 4 条(无交错丢失)", len(drecs) == 4,
|
|
274
|
+
f"n={len(drecs)}")
|
|
275
|
+
check("每条裁决带 record_hash 指纹",
|
|
276
|
+
all(r.get("record_hash") for r in drecs), "")
|
|
277
|
+
check("重开后裁决状态保持",
|
|
278
|
+
len(MdCGOS(root3, actor="x")._closed_pids()) == 4, "")
|
|
279
|
+
|
|
280
|
+
# ---------------- ⑩ MdCGSecure.propose info 转发 ----------------
|
|
281
|
+
print("\n【10】安全层转发:MdCGSecure.propose 幂等透传")
|
|
282
|
+
root4 = tempfile.mkdtemp(prefix="mdcg_conf_s_")
|
|
283
|
+
sec = MdCGSecure(root4, principal=Principal(
|
|
284
|
+
actor="conf", clearance="private", can_write=True, can_admin=True))
|
|
285
|
+
sec_body = "# 功能名:安全层幂等\n\n转发链上的去重。\n"
|
|
286
|
+
sp1 = sec.propose("sec1", sec_body, layer="knowledge", info=True)
|
|
287
|
+
sp2 = sec.propose("sec1", sec_body, layer="knowledge", info=True)
|
|
288
|
+
check("安全层两次入队同一 pid 且 dedup 命中",
|
|
289
|
+
sp1.get("pid") == sp2.get("pid") and sp2.get("dedup") is True,
|
|
290
|
+
f"{sp1.get('pid')} / {sp2}")
|
|
291
|
+
with open(os.path.join(root4, "hippocampus", "inbox.jsonl"),
|
|
292
|
+
encoding="utf-8") as f:
|
|
293
|
+
sec_rec = json.loads(f.readline())
|
|
294
|
+
check("info 不泄漏进 extra",
|
|
295
|
+
"info" not in (sec_rec.get("extra") or {}),
|
|
296
|
+
str(sec_rec.get("extra")))
|
|
297
|
+
|
|
298
|
+
print(f"\n===== conformance:PASS={PASS} FAIL={FAIL} =====")
|
|
299
|
+
if FAILS:
|
|
300
|
+
print("失败项:", ";".join(FAILS))
|
|
301
|
+
return 1 if FAIL else 0
|
|
302
|
+
finally:
|
|
303
|
+
shutil.rmtree(workdir, ignore_errors=True)
|
|
304
|
+
for r in (root, root2, root2b, root3, root4):
|
|
305
|
+
if r:
|
|
306
|
+
shutil.rmtree(r, ignore_errors=True)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
if __name__ == "__main__":
|
|
310
|
+
sys.exit(main())
|