@furongjun1999/dsh-memory 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +618 -572
- package/codebuddy/CODEBUDDY.md +10 -10
- package/data/policy.json +27 -0
- package/docs/README.md +1 -0
- package/docs/discipline/harnesses.yaml +31 -0
- package/docs/discipline/templates/full.md.tmpl +1 -1
- package/docs/discipline/templates/rules.mdc.tmpl +1 -1
- package/docs/eval/N225_/347/264/242/345/274/225/346/227/245/345/277/227/351/235/236/345/257/271/350/261/241/350/243/205/350/275/275/351/235/242/347/261/273/345/236/213/351/227/270_v1.0.md +418 -0
- package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/344/270/216/351/224/256/347/261/273/345/236/213/351/227/270_v1.1.md +441 -0
- package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/345/212/240/350/275/275/344/270/216/345/207/255/346/215/256/346/230/216/346/226/207/351/230/237/345/210/227_v1.0.md +398 -0
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/200/346/211/271_/346/216/245/347/272/277/344/270/216/347/255/211/344/273/267/345/217/230/346/215/242_v1.0.md +498 -0
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/211/346/211/271_/351/227/250/347/246/201/350/275/254/346/255/243/344/270/216/350/260/203/345/272/246/346/255/242/350/241/200/344/270/216/351/227/250/346/216/247/346/224/266/345/217/243_v1.0.md +669 -0
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/272/214/346/211/271_/344/276/235/350/265/226/351/200/217/344/274/240/344/270/216/345/257/271/346/213/215/345/217/243/345/276/204/344/270/216/350/264/237/347/274/223/345/255/230_v1.0.md +683 -0
- package/docs/eval//345/207/272/350/264/247/351/235/242/345/206/222/347/203/237_/350/277/233/350/264/247/351/227/250/347/246/201_v1.0.md +460 -0
- package/docs/eval//345/217/221/345/270/20307_/345/244/226/351/203/250/346/212/245/345/221/212/345/233/233/346/211/271/344/277/256/345/244/215/344/270/216/346/217/222/344/273/266/351/235/242/345/212/240/345/233/272_v1.0.md +205 -0
- package/docs/eval//345/217/221/345/270/20308_/350/207/252/350/277/255/344/273/243/344/270/216/347/235/241/347/234/240_/345/233/276/346/243/200/347/264/242/350/267/257/344/270/216/346/235/203/351/207/215_v1.0.md +97 -0
- package/docs/eval//345/275/222/344/270/200/345/261/202/347/274/272/347/234/201/347/277/273/345/205/263_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +458 -0
- package/docs/eval//346/200/247/350/203/275/344/270/223/351/241/271_/345/206/267/346/237/245/350/257/242/344/270/216/345/206/205/345/255/230_v1.0.md +218 -0
- package/docs/eval//346/225/205/351/232/234/346/263/250/345/205/245/345/256/236/346/265/213_v1.0.md +10 -0
- package/docs/eval//347/274/226/347/240/201/351/235/242/345/211/215/347/275/256_/345/205/245/345/217/243/350/207/252/344/277/235/350/257/201UTF8/344/270/216/346/226/207/346/234/254open/345/256/210/345/215/253_v1.0.md +646 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v20.md +283 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v21.md +224 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v22.md +223 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v23.md +293 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v24.md +303 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v25.md +230 -0
- package/docs/hive//346/243/200/347/264/242/347/256/227/346/263/225/345/217/243/345/276/204/345/257/271/347/205/247_v0.1.md +136 -11
- package/docs/hive//346/243/200/347/264/242/350/267/257/345/276/204/344/270/216/350/256/244/347/237/245/347/273/223/346/236/204/345/245/221/347/272/246_v0.1.md +52 -2
- package/docs/hive//350/234/202/345/267/242M6_ingest/345/256/236/346/226/275/350/256/241/345/210/222_v0.1.md +1 -1
- package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +100 -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 +40 -40
- package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +57 -9
- package/docs/mdcg//347/201/265/346/236/242/350/256/260/345/277/206/345/212/250/350/257/215/345/215/217/350/256/256_v1.0-draft.md +7 -2
- package/docs/mdcg//347/235/241/347/234/240/345/221/250/346/234/237_/350/277/220/347/273/264/345/211/215/346/217/220/344/270/216/347/273/264/346/212/244/346/214/207/345/215/227_v1.0.md +183 -0
- package/docs/mdcg//350/256/244/347/237/245/345/233/276_/347/264/242/345/274/225/344/270/216/345/267/245/347/250/213/350/247/204/350/214/203/345/214/226_/350/256/241/345/210/222_v0.1.md +39 -3
- package/docs/plans//345/205/250/344/270/255/346/226/207/347/274/226/347/240/201/344/270/216/350/234/202/345/267/242/344/273/273/345/212/241/346/240/207/350/257/206/345/245/221/347/272/246_v2.0.md +220 -0
- package/docs/plans//347/234/237/346/272/220/347/264/242/345/274/225_/351/200/232/347/224/250/346/234/272/345/210/266_v0.3.md +227 -0
- package/docs/plans//347/235/241/347/234/240/344/270/216/350/207/252/350/277/255/344/273/243_/345/212/237/350/203/275/344/274/230/345/214/226/350/256/276/350/256/241_v0.4.md +547 -0
- package/docs/plans//350/234/202/345/267/242/346/250/241/345/236/213/345/257/206/351/222/245/351/205/215/347/275/256/351/235/242_v1.0.md +385 -0
- package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +460 -460
- package/lib/bridge.js +24 -2
- package/lib/hooks.d.ts +31 -2
- package/lib/hooks.js +219 -16
- package/lib/init.js +16 -1
- package/lib/lib/prompt_safety.d.ts +52 -1
- package/lib/lib/prompt_safety.js +76 -1
- package/md_cg/audit.py +652 -379
- package/md_cg/bench6_arms.py +1 -1
- package/md_cg/bench_e2e_locomo_qa.py +11 -4
- package/md_cg/bench_p0.py +1 -1
- package/md_cg/branches.py +2 -2
- package/md_cg/ccgc.py +1071 -1006
- package/md_cg/chain.py +48 -1
- package/md_cg/consistency.py +97 -9
- package/md_cg/consolidate.py +34 -9
- package/md_cg/crypto.py +59 -26
- package/md_cg/docindex.py +16 -1
- package/md_cg/evolution.py +17 -1
- package/md_cg/export.py +1 -1
- package/md_cg/forgetting.py +309 -13
- package/md_cg/freshness.py +511 -0
- package/md_cg/fsutil.py +454 -8
- package/md_cg/generation.py +409 -0
- package/md_cg/hotcache.py +4 -1
- package/md_cg/identity.py +3 -3
- package/md_cg/insight.py +24 -3
- package/md_cg/linkref.py +1 -1
- package/md_cg/logref.py +327 -0
- package/md_cg/mcp_server.py +4261 -3818
- package/md_cg/mdcg.py +4695 -3547
- package/md_cg/mdcos.py +4672 -4159
- package/md_cg/mreview/pipeline.py +15 -1
- package/md_cg/nodefile.py +713 -576
- package/md_cg/protect.py +158 -11
- package/md_cg/protocol.py +41 -2
- package/md_cg/provenance.py +22 -4
- package/md_cg/reach.py +4 -4
- package/md_cg/readcache.py +76 -15
- package/md_cg/reconcile.py +1 -1
- package/md_cg/refindex.py +246 -37
- package/md_cg/refine.py +1 -1
- package/md_cg/routing.py +32 -5
- package/md_cg/run_tests.py +17 -0
- package/md_cg/scrub.py +30 -3
- package/md_cg/security.py +47 -1
- package/md_cg/self_state.py +5 -5
- package/md_cg/semantic/canonical.py +22 -0
- package/md_cg/semantic/unify.py +73 -22
- package/md_cg/semantic/unify_fixture.json +25 -0
- package/md_cg/sleep.py +1297 -0
- package/md_cg/sources.py +23 -6
- package/md_cg/srcindex.py +352 -0
- package/md_cg/stg.py +47 -3
- package/md_cg/subgraph.py +2 -2
- package/md_cg/sustain.py +1436 -1168
- package/md_cg/tasks.py +469 -470
- package/md_cg/test_auto_defaults.py +424 -0
- package/md_cg/test_b1_auto_id_multiproc.py +277 -0
- package/md_cg/test_b1b2_write_face.py +417 -0
- package/md_cg/test_b2_sensitivity_landing.py +223 -0
- package/md_cg/test_b3_merge_keeps_content.py +576 -0
- package/md_cg/test_b4_shard_dir_selfheal.py +843 -0
- package/md_cg/test_b4_shard_dir_selfheal_guard.py +238 -0
- package/md_cg/test_boundary_hit.py +410 -0
- package/md_cg/test_c3_transient_read_negative.py +793 -0
- package/md_cg/test_c8_search_rrf_gates.py +502 -0
- package/md_cg/test_ccg_form_parity.py +188 -0
- package/md_cg/test_ccgc.py +28 -3
- package/md_cg/test_en_pipeline.py +22 -13
- package/md_cg/test_generation_guard.py +352 -0
- package/md_cg/test_govern_directread.py +17 -4
- package/md_cg/test_h2_session_view_norm.py +233 -0
- package/md_cg/test_h4_sustain_snapshot.py +1387 -0
- package/md_cg/test_hive_ingest.py +285 -285
- package/md_cg/test_issue39_utf8_stdio.py +307 -16
- package/md_cg/test_issue43_default_policy.py +865 -0
- package/md_cg/test_legacy_p3_node_id_type.py +375 -0
- package/md_cg/test_linkref.py +10 -3
- package/md_cg/test_lock.py +2 -2
- package/md_cg/test_logref.py +1109 -0
- package/md_cg/test_m3_h9_bucket_health_protect_mark.py +301 -0
- package/md_cg/test_m3_h9_semantic_guard.py +525 -0
- package/md_cg/test_mr_m2.py +15 -3
- package/md_cg/test_n130_verify_falsified_protect.py +1 -1
- package/md_cg/test_n139_dek_provision_failclosed.py +185 -0
- package/md_cg/test_n176_link_trust.py +221 -0
- package/md_cg/test_n178_units_jobid_gate.py +212 -0
- package/md_cg/test_n184_keys_concurrent_provision.py +307 -0
- package/md_cg/test_n195_writepath_reload.py +283 -0
- package/md_cg/test_n196_stale_gate_skip.py +169 -0
- package/md_cg/test_n197_n208_write_face_gates.py +471 -0
- package/md_cg/test_n198_tokens_corrupt_failclosed.py +225 -0
- package/md_cg/test_n199_tokens_concurrent_write.py +378 -0
- package/md_cg/test_n201_proposal_visibility.py +382 -0
- package/md_cg/test_n202_session_notes_visibility.py +461 -0
- package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +542 -0
- package/md_cg/test_n206_stdio_jsonrpc_type.py +403 -0
- package/md_cg/test_n209_verify_write_face_gates.py +461 -0
- package/md_cg/test_n212_n213_n224_generation_gates.py +519 -0
- package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +563 -0
- package/md_cg/test_n225_nonobject_load.py +1110 -0
- package/md_cg/test_n230_dirty_replay.py +375 -0
- package/md_cg/test_n62_tenant_bind_failclosed.py +200 -0
- package/md_cg/test_neg_condition_hits.py +333 -0
- package/md_cg/test_neg_tail_honesty.py +663 -0
- package/md_cg/test_none_id_write_guard.py +4 -3
- package/md_cg/test_opt_batch1_md_cg.py +451 -0
- package/md_cg/test_p1.py +5 -5
- package/md_cg/test_p11_consistency.py +161 -12
- package/md_cg/test_p26_refindex.py +2 -2
- package/md_cg/test_p27_docindex.py +777 -774
- package/md_cg/test_p28_refcheck.py +740 -13
- package/md_cg/test_p29_session_ingest_export.py +2 -2
- package/md_cg/test_p2_mcp.py +12 -1
- package/md_cg/test_p2_six_elements.py +463 -0
- package/md_cg/test_p30_maintain.py +33 -2
- package/md_cg/test_p31_insight.py +1 -0
- package/md_cg/test_p3_legacy_closure.py +433 -0
- package/md_cg/test_p4_freshness.py +680 -0
- package/md_cg/test_p8_subgraph_chain.py +14 -1
- package/md_cg/test_p9c_dedup_hints.py +276 -0
- package/md_cg/test_policy_required_ccg.py +426 -0
- package/md_cg/test_protocol.py +22 -0
- package/md_cg/test_rank_parity_score_mode.py +522 -0
- package/md_cg/test_read_face_input_gates.py +363 -0
- package/md_cg/test_read_face_semantics.py +489 -0
- package/md_cg/test_recall_face_guards.py +812 -0
- package/md_cg/test_rejected_credential_forms.py +188 -0
- package/md_cg/test_rejected_redact.py +146 -0
- package/md_cg/test_retr_s1b.py +2 -2
- package/md_cg/test_retr_s5.py +20 -7
- package/md_cg/test_review_conformance.py +21 -3
- package/md_cg/test_security_audit_v21.py +2 -2
- package/md_cg/test_semantic_canonical.py +5 -3
- package/md_cg/test_server_version.py +67 -0
- package/md_cg/test_sleep.py +611 -0
- package/md_cg/test_sleep_p1.py +784 -0
- package/md_cg/test_srcindex.py +171 -0
- package/md_cg/test_tenant_env_override_warn.py +63 -50
- package/md_cg/test_time_core_lint.py +968 -0
- package/md_cg/test_token_lowercase_form.py +315 -0
- package/md_cg/test_unify_default_off.py +701 -0
- package/md_cg/test_unify_scope.py +182 -0
- package/md_cg/test_v21r1_package_version.py +310 -0
- package/md_cg/test_writepipe.py +10 -4
- package/md_cg/tokens.py +225 -95
- package/md_cg/tool_face.py +4 -4
- package/md_cg/trust.py +49 -5
- package/md_cg/units.py +204 -6
- package/md_cg/weights.py +4 -4
- package/md_cg/whitebox_kb/aeis_core/time_core.py +8 -0
- package/md_cg/whitebox_kb/wisdom/knowledge_points.py +1 -1
- package/md_cg/writelimit.py +42 -8
- package/md_cg/writepipe.py +651 -554
- package/package.json +4 -2
- package/skills/plugin.json +1 -1
- package/src/bridge.ts +25 -2
- package/src/hooks.ts +229 -16
- package/src/init.ts +15 -1
- package/src/lib/prompt_safety.ts +88 -1
- package/utf8_boot.py +237 -0
- package/zcode/AGENTS.md +10 -10
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# 功能名:负覆盖条目诚实化(H10 ①②③④)· 基类与生产路径同口径(H7⑦)· 飞轮建单只限
|
|
3
|
+
# REJECT(H11⑥)· 工单语义 slug 身份与现场刷新(⑤⑦)· CCG 哨兵直连入口同口径(⑧)
|
|
4
|
+
# · M3 呼叫点传节点总数
|
|
5
|
+
# 生效条件:md_cg/mdcg.py 的 `_neg_tail`/`_primary_slots`/`_emit`/`_compute_d`/
|
|
6
|
+
# `search` 候选收集/`add_unresolved`/`_refresh_unresolved`/`_ticket_slug`/
|
|
7
|
+
# `health`·`flywheel_step`、md_cg/consistency.py 的 `check`(哨兵漏斗 +
|
|
8
|
+
# `FLYWHEEL_TRIGGERS`)、md_cg/routing.py 的 `bucket_health(counts,
|
|
9
|
+
# total_nodes)`、md_cg/mdcos.py 的 `_candidates` 负层过滤(真源导入
|
|
10
|
+
# `NEG_ROUTE_LAYERS`,不留同值副本——C6/C7)同口径时成立;
|
|
11
|
+
# 沙箱条件:所有库根一律 tempfile.mkdtemp,
|
|
12
|
+
# 绝不触在役库/在役服务。
|
|
13
|
+
# 子功能:
|
|
14
|
+
# A 负覆盖尾诚实化:①分数=哨兵 0.0 + 负性走独立字段(不再冒充 1.0 候选)
|
|
15
|
+
# ②条数计入 k 预算(len(results) ≤ k,含 k=0/1 边界)④位置在尾部、真 id 走
|
|
16
|
+
# `node_id` 字段(`id` 仍是落盘路径——既有消费者口径一字不动)
|
|
17
|
+
# B `_compute_d` 分母只数真实候选(提示条目不进分母,D 不再被虚高)
|
|
18
|
+
# C 基类与生产路径同口径:负记忆/目标槽不进正排,同一负节点不再双列
|
|
19
|
+
# (负层元组真源单点:mdcos 直接导入 mdcg.NEG_ROUTE_LAYERS,不留同值副本)
|
|
20
|
+
# D 飞轮只对真冲突 REJECT 建单;DEFER/BLINDSPOT 不再自动建单(判定照旧返回)
|
|
21
|
+
# E CCG 哨兵「不适用条件:无」的直连入口形态(consistency.check / cg.add)与
|
|
22
|
+
# mdcos 入口同口径,且真负条件仍判冲突(防放宽)
|
|
23
|
+
# F 工单身份=语义 slug(同 slug 即同工单);同题再检出时「# 现场:」刷新、
|
|
24
|
+
# id/created_at 不变、零新建、同输入零写盘
|
|
25
|
+
# G M3:节点总数传进 bucket_health(空库 vs 有节点零分桶可区分)
|
|
26
|
+
# 执行:python -X utf8 -m md_cg.test_neg_tail_honesty
|
|
27
|
+
# python -X utf8 -m md_cg.test_neg_tail_honesty --drop-fix=all
|
|
28
|
+
# python -X utf8 -m md_cg.test_neg_tail_honesty --drop-fix=neg_score
|
|
29
|
+
# 验证方式:本文件自跑(自带断言计数与退出码 rc=0/1);定点变异自证见 --drop-fix
|
|
30
|
+
# (把每处修复逐点抽回改动前语义 → 本守卫必红,且红项必须覆盖预期集合;
|
|
31
|
+
# 恢复后必须转绿且无残余)。
|
|
32
|
+
# 不适用条件:情感交互|闲聊|纯查询无改动;「对方节点被覆写但工单**未**重发」这一
|
|
33
|
+
# 形态不在本守卫面内(无新鲜现场数据可依,重算需持久化结构化现场——见
|
|
34
|
+
# FixReport 的 gaps);负覆盖条目在 MCP 结果面的呈现(`_node_view` 只透
|
|
35
|
+
# id/path/frontmatter/content)不在本文件面内,属调用方渲染层。
|
|
36
|
+
import hashlib
|
|
37
|
+
import inspect
|
|
38
|
+
import os
|
|
39
|
+
import re
|
|
40
|
+
import shutil
|
|
41
|
+
import sys
|
|
42
|
+
import tempfile
|
|
43
|
+
|
|
44
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
45
|
+
|
|
46
|
+
from md_cg import consistency, mdcos, nodefile, routing # noqa: E402
|
|
47
|
+
from md_cg import mdcg as _mdcg # noqa: E402
|
|
48
|
+
from md_cg.mdcos import MdCGOS # noqa: E402
|
|
49
|
+
|
|
50
|
+
PASS = 0
|
|
51
|
+
FAIL = 0
|
|
52
|
+
FAILS = []
|
|
53
|
+
_RUN = [0]
|
|
54
|
+
_SANDBOX = tempfile.mkdtemp(prefix="mdcg_neghon_")
|
|
55
|
+
|
|
56
|
+
QUERY = "牛肉面 汤头"
|
|
57
|
+
NEG_REJ = "# 假设:牛肉面 用工业浓汤宝\n# 否决原因:偏离手工工艺\n"
|
|
58
|
+
NEG_UNR = "# 问题:牛肉面 汤色发黑\n# 目标:(未设定)\n"
|
|
59
|
+
SENT = ("# 功能名:部署面端口约定\n"
|
|
60
|
+
"# 生效条件:载体/位置:prod 集群;方法:部署清单核对;约束:无\n"
|
|
61
|
+
"# 子功能:登记各监听端口\n# 执行:核对 manifest 的 ports 段\n"
|
|
62
|
+
"# 验证方式:test\n"
|
|
63
|
+
"# 不适用条件:无\n\n网关监听端口 8080/HTTP\n")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def check(name, cond, detail=""):
|
|
67
|
+
global PASS, FAIL
|
|
68
|
+
if cond:
|
|
69
|
+
PASS += 1
|
|
70
|
+
print(" OK %s" % name)
|
|
71
|
+
else:
|
|
72
|
+
FAIL += 1
|
|
73
|
+
FAILS.append(name)
|
|
74
|
+
print(" FAIL %s %s" % (name, detail))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _mk(tag):
|
|
78
|
+
return MdCGOS(os.path.join(_SANDBOX, "lib_%s_r%d" % (tag, _RUN[0])),
|
|
79
|
+
autoflush=0)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _neg_lib(tag):
|
|
83
|
+
"""3 正候选(含查询词)+ 2 负节点(rejected/unresolved,含查询词)。"""
|
|
84
|
+
cg = _mk(tag)
|
|
85
|
+
for i in range(3):
|
|
86
|
+
cg.add("mem_%d" % i,
|
|
87
|
+
"# 功能名:样本%d\n# 子功能:牛肉面 汤头 熬制 工艺\n" % i,
|
|
88
|
+
layer="knowledge")
|
|
89
|
+
cg.add("r0", NEG_REJ, layer="rejected", verification_basis="test")
|
|
90
|
+
cg.add("u0", NEG_UNR, layer="unresolved", verification_basis="data")
|
|
91
|
+
cg.flush()
|
|
92
|
+
return cg
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
#: CCG 六要素齐备 + 生效条件命中查询词 → 资格判定 ACCEPT(B 组要一个真「有效命中」,
|
|
96
|
+
#: 否则 accept=0 时「含尾/不含尾」两种分母都得 1.0,对照断言没有判别力)。
|
|
97
|
+
ACCEPT_NODE = ("# 功能名:汤头工艺基线\n"
|
|
98
|
+
"# 生效条件:牛肉面 汤头\n"
|
|
99
|
+
"# 子功能:登记熬制工艺\n"
|
|
100
|
+
"# 执行:按基线核对\n"
|
|
101
|
+
"# 验证方式:test\n"
|
|
102
|
+
"# 不适用条件:工业化浓汤宝\n\n"
|
|
103
|
+
"牛肉面 汤头 熬制 工艺:小火 6 小时\n")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _d_lib(tag):
|
|
107
|
+
"""1 个 ACCEPT 正候选 + 2 个非 ACCEPT 正候选 + 2 负节点(B 组分母对照用)。"""
|
|
108
|
+
cg = _mk(tag)
|
|
109
|
+
cg.add("acc_0", ACCEPT_NODE, layer="knowledge",
|
|
110
|
+
verification_basis="test")
|
|
111
|
+
cg.add("mem_1", "# 功能名:样本1\n# 正文:牛肉面 汤头 备用配方",
|
|
112
|
+
layer="knowledge")
|
|
113
|
+
cg.add("mem_2", "# 功能名:样本2\n# 正文:牛肉面 汤头", layer="knowledge")
|
|
114
|
+
cg.add("r0", NEG_REJ, layer="rejected", verification_basis="test")
|
|
115
|
+
cg.add("u0", NEG_UNR, layer="unresolved", verification_basis="data")
|
|
116
|
+
cg.flush()
|
|
117
|
+
return cg
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _count_calls(obj, name, fn):
|
|
121
|
+
st = {"n": 0}
|
|
122
|
+
real = getattr(obj, name)
|
|
123
|
+
|
|
124
|
+
def _spy(*a, **kw):
|
|
125
|
+
st["n"] += 1
|
|
126
|
+
return real(*a, **kw)
|
|
127
|
+
|
|
128
|
+
setattr(obj, name, _spy)
|
|
129
|
+
try:
|
|
130
|
+
return fn(), st["n"]
|
|
131
|
+
finally:
|
|
132
|
+
setattr(obj, name, real)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ---------------------------------------------------------------- A 组(H10①②④)
|
|
136
|
+
def group_a():
|
|
137
|
+
print("\n[A] 负覆盖尾诚实化:分数哨兵 / 独立字段 / k 预算 / 位置")
|
|
138
|
+
cg = _neg_lib("tail")
|
|
139
|
+
res, meta = cg.search(QUERY, k=3, record=False)
|
|
140
|
+
tail = [r for r in res if (r[2] or {}).get("negative_coverage")]
|
|
141
|
+
prim = [r for r in res if not (r[2] or {}).get("negative_coverage")]
|
|
142
|
+
check("A1 负覆盖条目不冒充候选:分数恒字面量 0.0(哨兵常量本身也是 0.0),"
|
|
143
|
+
"不带 negative_coverage 的候选分数都 ≥ 它",
|
|
144
|
+
len(tail) == 2
|
|
145
|
+
and _mdcg.NEG_COVERAGE_SCORE == 0.0
|
|
146
|
+
and all(r[1] == 0.0 for r in tail)
|
|
147
|
+
and min([r[1] for r in prim] + [float("inf")]) >= 0.0,
|
|
148
|
+
"tail=%s prim=%s const=%s" % ([r[1] for r in tail],
|
|
149
|
+
[r[1] for r in prim],
|
|
150
|
+
_mdcg.NEG_COVERAGE_SCORE))
|
|
151
|
+
check("A2 独立字段承载负性(card.negative_coverage/neg_layer,"
|
|
152
|
+
"机器可判,不靠分数位反推)",
|
|
153
|
+
all(r[0].get("negative_coverage") is True
|
|
154
|
+
and r[0].get("neg_layer") in ("rejected", "unresolved")
|
|
155
|
+
for r in tail)
|
|
156
|
+
and all("negative_coverage" not in r[0] for r in prim),
|
|
157
|
+
str([(r[0].get("id"), r[0].get("neg_layer")) for r in tail]))
|
|
158
|
+
check("A3 真 id 走独立字段 node_id(id/path 仍是落盘路径——既有消费者口径不动)",
|
|
159
|
+
[(r[0].get("id"), r[0].get("path"), r[0].get("node_id"))
|
|
160
|
+
for r in tail] == [("rejected/r0.md", "rejected/r0.md", "r0"),
|
|
161
|
+
("unresolved/u0.md", "unresolved/u0.md", "u0")],
|
|
162
|
+
str([(r[0].get("id"), r[0].get("path"), r[0].get("node_id"))
|
|
163
|
+
for r in tail]))
|
|
164
|
+
check("A4 位置在**尾部**(主结果全在前;旧注释「(首条)」与代码相反)",
|
|
165
|
+
[bool((r[2] or {}).get("negative_coverage")) for r in res]
|
|
166
|
+
== [False] * len(prim) + [True] * len(tail),
|
|
167
|
+
str([(r[0].get("id"), (r[2] or {}).get("negative_coverage"))
|
|
168
|
+
for r in res]))
|
|
169
|
+
|
|
170
|
+
print("\n[A'] k 预算:总数恒 ≤ k(改前 k=3 → 5)")
|
|
171
|
+
ok_len = True
|
|
172
|
+
detail = []
|
|
173
|
+
for k in (1, 2, 3, 5, 20):
|
|
174
|
+
r, m = cg.search(QUERY, k=k, record=False)
|
|
175
|
+
nt = len([x for x in r if (x[2] or {}).get("negative_coverage")])
|
|
176
|
+
np = len(r) - nt
|
|
177
|
+
b = m.get("k_budget") or {}
|
|
178
|
+
detail.append("k=%d len=%d prim=%d neg=%d budget=%s" % (k, len(r), np, nt, b))
|
|
179
|
+
if len(r) > k or b != {"k": k, "primary": np, "neg_tail": nt}:
|
|
180
|
+
ok_len = False
|
|
181
|
+
check("A5 结果总数 ≤ k 且 meta.k_budget 与实取逐位一致(尾巴计入预算)",
|
|
182
|
+
ok_len, "; ".join(detail))
|
|
183
|
+
check("A6 主结果实取 = k − 提示条数(_primary_slots 单点)",
|
|
184
|
+
_mdcg.MdCG._primary_slots(3, 2) == 1
|
|
185
|
+
and _mdcg.MdCG._primary_slots(1, 2) == 0
|
|
186
|
+
and _mdcg.MdCG._primary_slots(0, 0) == 0
|
|
187
|
+
and _mdcg.MdCG._primary_slots(5, 3) == 2,
|
|
188
|
+
"slots=%s" % [_mdcg.MdCG._primary_slots(*x)
|
|
189
|
+
for x in ((3, 2), (1, 2), (0, 0), (5, 3))])
|
|
190
|
+
r0, m0 = cg.search(QUERY, k=0, record=False)
|
|
191
|
+
check("A7 k=0 → 零结果(提示条目也不超发;改前 k=0 仍返回 3 条提示)",
|
|
192
|
+
r0 == [] and "k_budget" not in m0, str(r0))
|
|
193
|
+
|
|
194
|
+
print("\n[A''] 审计面与卡片同源")
|
|
195
|
+
res3, meta3 = cg.search(QUERY, k=3, record=False)
|
|
196
|
+
rows = meta3.get("neg_coverage_tail") or []
|
|
197
|
+
cards = [r for r in res3 if (r[2] or {}).get("negative_coverage")]
|
|
198
|
+
check("A8 meta.neg_coverage_tail 与入榜条目逐条同源(含 node_id/score=0.0)",
|
|
199
|
+
len(rows) == len(cards) == 2
|
|
200
|
+
and all(row["id"] == c[0]["id"] and row["node_id"] == c[0]["node_id"]
|
|
201
|
+
and row["score"] == c[1] == 0.0
|
|
202
|
+
and row["layer"] == c[0]["neg_layer"]
|
|
203
|
+
for row, c in zip(rows, cards)),
|
|
204
|
+
"%s vs %s" % (rows, [c[0]["id"] for c in cards]))
|
|
205
|
+
check("A9 meta.covered_neg 口径不变(全部覆盖路径,不只入榜者)",
|
|
206
|
+
meta3.get("covered_neg") == ["rejected/r0.md", "unresolved/u0.md"],
|
|
207
|
+
str(meta3.get("covered_neg")))
|
|
208
|
+
cg.close()
|
|
209
|
+
return cg
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# ---------------------------------------------------------------- B 组(H10③)
|
|
213
|
+
def group_b():
|
|
214
|
+
print("\n[B] _compute_d 分母只数真实候选(提示条目不进分母)")
|
|
215
|
+
cg = _d_lib("d")
|
|
216
|
+
res, _m = cg.search(QUERY, k=5, record=False)
|
|
217
|
+
prim = [r for r in res if not (r[2] or {}).get("negative_coverage")]
|
|
218
|
+
acc = sum(1 for r in prim if r[1] > 0 and r[2]["state"] == "ACCEPT")
|
|
219
|
+
d = cg._compute_d(QUERY, res)
|
|
220
|
+
d_naive = max(0.0, 1.0 - acc / len(res))
|
|
221
|
+
check("B0 前置:确有 1 条 ACCEPT 有效命中(对照断言才有判别力)",
|
|
222
|
+
acc == 1 and len(prim) == 3 and len(res) == 5,
|
|
223
|
+
"acc=%d prim=%d all=%d" % (acc, len(prim), len(res)))
|
|
224
|
+
check("B1 同批结果的 D 与「只喂正排候选」的 D 一致(分母不含提示)",
|
|
225
|
+
abs(d - (1.0 - acc / len(prim))) < 1e-9,
|
|
226
|
+
"D=%s / 仅正排=%s" % (d, 1.0 - acc / len(prim)))
|
|
227
|
+
check("B2 对照:若把提示算进分母,D 会变成另一个数(断言有判别力)",
|
|
228
|
+
abs(d - d_naive) > 1e-9,
|
|
229
|
+
"D=%s / 含尾分母=%s" % (d, d_naive))
|
|
230
|
+
check("B3 结果全是提示(无正排候选)→ 如实报「完全空白」1.0,不拿提示充数",
|
|
231
|
+
cg._compute_d(QUERY, [r for r in res
|
|
232
|
+
if (r[2] or {}).get("negative_coverage")]) == 1.0)
|
|
233
|
+
check("B4 空结果仍 1.0(既有语义未变)", cg._compute_d(QUERY, []) == 1.0)
|
|
234
|
+
cg.close()
|
|
235
|
+
return cg
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# ---------------------------------------------------------------- C 组(H7⑦)
|
|
239
|
+
def group_c():
|
|
240
|
+
print("\n[C] 基类与生产路径同口径:负记忆/目标槽不进正排(不双列)")
|
|
241
|
+
from md_cg.mdcg import MdCG
|
|
242
|
+
root = os.path.join(_SANDBOX, "lib_base_r%d" % _RUN[0])
|
|
243
|
+
b = MdCG(root)
|
|
244
|
+
b.add("mem_0", "# 功能名:正\n# 子功能:牛肉面 汤头 熬制 工艺\n",
|
|
245
|
+
layer="knowledge")
|
|
246
|
+
b.add("r0", NEG_REJ, layer="rejected", verification_basis="test")
|
|
247
|
+
b.add("u0", NEG_UNR, layer="unresolved", verification_basis="data")
|
|
248
|
+
b.add_goal("牛肉面 汤头 标准化", priority=0.6)
|
|
249
|
+
b.flush()
|
|
250
|
+
res, meta = b.search(QUERY, k=5, record=False)
|
|
251
|
+
ids = [r[0].get("id") for r in res]
|
|
252
|
+
check("C1 同一负节点不再双列(一次作正候选、一次作覆盖尾 → 改后只出现一次)",
|
|
253
|
+
len(ids) == len(set(ids))
|
|
254
|
+
and sorted(ids) == ["mem_0", "rejected/r0.md", "unresolved/u0.md"],
|
|
255
|
+
str(ids))
|
|
256
|
+
check("C2 负记忆 ID 形态唯一(带层前缀的路径),无「无前缀版」孪生条目",
|
|
257
|
+
not any(str(i).startswith("r0") or str(i).startswith("u0")
|
|
258
|
+
for i in ids), str(ids))
|
|
259
|
+
osr = MdCGOS(os.path.join(_SANDBOX, "lib_os_r%d" % _RUN[0]))
|
|
260
|
+
osr.add("mem_0", "# 功能名:正\n# 子功能:牛肉面 汤头 熬制 工艺\n",
|
|
261
|
+
layer="knowledge")
|
|
262
|
+
osr.add("r0", NEG_REJ, layer="rejected", verification_basis="test")
|
|
263
|
+
osr.add("u0", NEG_UNR, layer="unresolved", verification_basis="data")
|
|
264
|
+
osr.add_goal("牛肉面 汤头 标准化", priority=0.6)
|
|
265
|
+
osr.flush()
|
|
266
|
+
res_os, _m2 = osr.search(QUERY, k=5, record=False)
|
|
267
|
+
check("C3 基类与生产路径(MdCGOS)结果 id 逐位一致(同口径,非各写一份)",
|
|
268
|
+
[r[0].get("id") for r in res_os] == ids,
|
|
269
|
+
"%s vs %s" % ([r[0].get("id") for r in res_os], ids))
|
|
270
|
+
check("C4 目标槽(goals)不进正排(与 _candidates 同口径)",
|
|
271
|
+
not any(str(i).startswith("goal_") for i in ids), str(ids))
|
|
272
|
+
res_off, _m3 = b.search(QUERY, k=5, include_neg=False, record=False)
|
|
273
|
+
check("C5 include_neg=False → 负节点完全不出现(改前仍作正候选出现一次)",
|
|
274
|
+
[r[0].get("id") for r in res_off] == ["mem_0"],
|
|
275
|
+
str([r[0].get("id") for r in res_off]))
|
|
276
|
+
# ---- 真源单点(2026-09-30 补强):生产路径不留同值字面量副本 ----
|
|
277
|
+
# mdcos._candidates 旧写作裸元组 ("rejected","unresolved","goals")——与 mdcg 的
|
|
278
|
+
# NEG_ROUTE_LAYERS 同值异构:改一处漏一处,正是 C 组「同口径」要防的漂移。
|
|
279
|
+
check("C6 生产路径(mdcos)不留负层元组字面量副本",
|
|
280
|
+
'("rejected", "unresolved", "goals")' not in inspect.getsource(mdcos),
|
|
281
|
+
"mdcos.py 仍有字面量副本")
|
|
282
|
+
check("C7 MdCGOS 与基类共用同一常量对象(导入非同值重写)",
|
|
283
|
+
mdcos.NEG_ROUTE_LAYERS is _mdcg.NEG_ROUTE_LAYERS,
|
|
284
|
+
"%r vs %r" % (mdcos.NEG_ROUTE_LAYERS, _mdcg.NEG_ROUTE_LAYERS))
|
|
285
|
+
b.close()
|
|
286
|
+
osr.close()
|
|
287
|
+
return b
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# ---------------------------------------------------------------- D 组(H11⑥)
|
|
291
|
+
def group_d():
|
|
292
|
+
print("\n[D] 飞轮建单只限真冲突 REJECT")
|
|
293
|
+
|
|
294
|
+
def _unres(cg):
|
|
295
|
+
return [n for n, e in list((cg.index.get("nodes") or {}).items())
|
|
296
|
+
if e.get("layer") == "unresolved"]
|
|
297
|
+
|
|
298
|
+
cg = _mk("fw")
|
|
299
|
+
cg.add("discipline_prod", "# 功能:生产纪律\n# 执行:禁止删除生产数据\n",
|
|
300
|
+
layer="self", tags=["discipline"],
|
|
301
|
+
non_applicable_conditions=["删除生产数据"])
|
|
302
|
+
cg.add("k_offline",
|
|
303
|
+
"# 功能:离线批处理\n# 生效条件:离线环境\n# 不适用条件:生产环境\n",
|
|
304
|
+
layer="knowledge", non_applicable_conditions=["生产环境"])
|
|
305
|
+
cg.flush()
|
|
306
|
+
check("D1 判据单点:FLYWHEEL_TRIGGERS == ('REJECT',)",
|
|
307
|
+
consistency.FLYWHEEL_TRIGGERS == ("REJECT",)
|
|
308
|
+
and cg.consistency_catalog()["auto_flywheel"]["triggers_on"]
|
|
309
|
+
== ["REJECT"], str(consistency.FLYWHEEL_TRIGGERS))
|
|
310
|
+
r = cg.check_consistency("删除生产数据", layer="self", auto_flywheel=True)
|
|
311
|
+
check("D1b 真冲突 REJECT 仍建单(收窄≠静默关掉飞轮)",
|
|
312
|
+
r["verdict"] == "REJECT" and bool(r.get("unresolved_id"))
|
|
313
|
+
and len(_unres(cg)) == 1,
|
|
314
|
+
"%s / %s / %s" % (r["verdict"], r.get("unresolved_id"), _unres(cg)))
|
|
315
|
+
n0 = len(_unres(cg))
|
|
316
|
+
r = cg.check_consistency("# 功能:生产批处理\n# 生效条件:生产环境\n",
|
|
317
|
+
layer="knowledge", auto_flywheel=True)
|
|
318
|
+
check("D2 DEFER(待确认)不建单,判定仍如实返回",
|
|
319
|
+
r["verdict"] == "DEFER" and r["conflict_strength"] >= consistency.CLASH_LOW
|
|
320
|
+
and not r.get("unresolved_id") and len(_unres(cg)) == n0,
|
|
321
|
+
"%s/%s/unr=%s/n=%d" % (r["verdict"], r["conflict_strength"],
|
|
322
|
+
r.get("unresolved_id"), len(_unres(cg))))
|
|
323
|
+
e = _mk("fw_empty")
|
|
324
|
+
r = e.check_consistency("# 功能:某情景\n# 生效条件:某个特殊条件\n",
|
|
325
|
+
layer="contextual", auto_flywheel=True)
|
|
326
|
+
check("D3 BLINDSPOT(空库无可比对)不建单(改前每写一条建一张工单)",
|
|
327
|
+
r["verdict"] == "BLINDSPOT" and not r.get("unresolved_id")
|
|
328
|
+
and not _unres(e),
|
|
329
|
+
"%s / unr=%s / n=%d" % (r["verdict"], r.get("unresolved_id"),
|
|
330
|
+
len(_unres(e))))
|
|
331
|
+
# 写链(writepipe)与 add() 的自动建单同口径:REJECT 才建
|
|
332
|
+
cg.add("k_x", "# 功能:x\n# 生效条件:离线环境\n", layer="knowledge")
|
|
333
|
+
n1 = len(_unres(cg))
|
|
334
|
+
cg.add("k_y", "# 功能:y\n# 生效条件:生产环境\n", layer="knowledge",
|
|
335
|
+
consistency=True)
|
|
336
|
+
check("D4 add(consistency=True) 的 DEFER 同样不建单(同一判据,非各写一份)",
|
|
337
|
+
len(_unres(cg)) == n1, "%d → %d" % (n1, len(_unres(cg))))
|
|
338
|
+
cg.close()
|
|
339
|
+
e.close()
|
|
340
|
+
return cg
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# ---------------------------------------------------------------- E 组(⑧)
|
|
344
|
+
def group_e():
|
|
345
|
+
print("\n[E] CCG 哨兵直连入口同口径(不新造第二张哨兵表)")
|
|
346
|
+
cg = _mk("sent")
|
|
347
|
+
cg.add("z_a", SENT, layer="knowledge")
|
|
348
|
+
cg.flush()
|
|
349
|
+
vd_list = consistency.check(cg, SENT, layer="knowledge",
|
|
350
|
+
non_applicable_conditions=["无"],
|
|
351
|
+
auto_flywheel=True)
|
|
352
|
+
vd_none = consistency.check(cg, SENT, layer="knowledge",
|
|
353
|
+
auto_flywheel=True)
|
|
354
|
+
check("E1 直连 consistency.check:显式哨兵列表「无」与不传同判"
|
|
355
|
+
"(ACCEPT / strength 0.0 / 无工单)",
|
|
356
|
+
vd_list["verdict"] == vd_none["verdict"] == "ACCEPT"
|
|
357
|
+
and float(vd_list["conflict_strength"]) == 0.0
|
|
358
|
+
and not vd_list.get("unresolved_id")
|
|
359
|
+
and not (vd_list.get("conflicts") or []),
|
|
360
|
+
"%s/%s unr=%s" % (vd_list["verdict"], vd_list["conflict_strength"],
|
|
361
|
+
vd_list.get("unresolved_id")))
|
|
362
|
+
nid = cg.add("z_b", SENT, layer="knowledge", consistency=True,
|
|
363
|
+
non_applicable_conditions=["无"])
|
|
364
|
+
fm = (cg.get("z_b") or {}).get("frontmatter") or {}
|
|
365
|
+
check("E2 引擎直连入口 cg.add(consistency=True) + 哨兵列表 → ACCEPT"
|
|
366
|
+
"(改前 DEFER/strength=1.0/建单)",
|
|
367
|
+
bool(nid) and (fm.get("consistency") or {}).get("verdict") == "ACCEPT"
|
|
368
|
+
and not (fm.get("consistency") or {}).get("unresolved_id"),
|
|
369
|
+
str(fm.get("consistency")))
|
|
370
|
+
check("E3 哨兵表复用单点:mdcos._is_null_condition 判「无」为真值、"
|
|
371
|
+
"真条件为假(未新造第二张表)",
|
|
372
|
+
mdcos._is_null_condition("无") is True
|
|
373
|
+
and mdcos._is_null_condition("(无)") is True
|
|
374
|
+
and mdcos._is_null_condition("无法确定") is False,
|
|
375
|
+
"%s/%s" % (mdcos._is_null_condition("无"),
|
|
376
|
+
mdcos._is_null_condition("无法确定")))
|
|
377
|
+
# 防放宽:哨兵剔除不得把真负条件一起吞掉
|
|
378
|
+
real = ("# 功能名:本地开发约定\n"
|
|
379
|
+
"# 生效条件:本地开发环境\n"
|
|
380
|
+
"# 子功能:登记本地端口\n# 执行:核对本地清单\n"
|
|
381
|
+
"# 验证方式:test\n"
|
|
382
|
+
"# 不适用条件:本地开发环境\n\n本地监听端口 8081/HTTP\n")
|
|
383
|
+
vd_real = consistency.check(cg, real, layer="knowledge",
|
|
384
|
+
non_applicable_conditions=["本地开发环境"],
|
|
385
|
+
auto_flywheel=True)
|
|
386
|
+
check("E4 防放宽:真负条件(自否定形态)仍判 REJECT 且仍建单",
|
|
387
|
+
vd_real["verdict"] == "REJECT"
|
|
388
|
+
and any(c.get("type") == "self_negation"
|
|
389
|
+
for c in (vd_real.get("conflicts") or []))
|
|
390
|
+
and bool(vd_real.get("unresolved_id")),
|
|
391
|
+
"%s/%s" % (vd_real["verdict"], vd_real.get("unresolved_id")))
|
|
392
|
+
cg.close()
|
|
393
|
+
return cg
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
# ---------------------------------------------------------------- F 组(⑤⑦)
|
|
397
|
+
def group_f():
|
|
398
|
+
print("\n[F] 工单身份=语义 slug;同题再检出时「# 现场:」刷新")
|
|
399
|
+
cg = _mk("ticket")
|
|
400
|
+
cg.add("k_x", "# 功能名:对方节点\n# 生效条件:条件A\n", layer="knowledge")
|
|
401
|
+
cg.flush()
|
|
402
|
+
Q = "牛肉面 汤头 隔离级别"
|
|
403
|
+
rep1 = {"query": Q, "expected_state": "ACCEPT", "actual_state": "DEFER",
|
|
404
|
+
"missing": "缺区分条件",
|
|
405
|
+
"detail": [{"type": "same_condition_divergence", "with": "k_x",
|
|
406
|
+
"same_condition": 1.0, "slot_overlap": 1.0,
|
|
407
|
+
"conclusion_overlap": 0.69}]}
|
|
408
|
+
fw1 = cg.flywheel_step(rep1)
|
|
409
|
+
id1 = fw1["unresolved_id"]
|
|
410
|
+
c1 = (cg.get(id1) or {}).get("content") or ""
|
|
411
|
+
fm1 = (cg.get(id1) or {}).get("frontmatter") or {}
|
|
412
|
+
check("F1 工单身份=slug(可读 id:`unr_<slugify(topic)>`,且过节点 id 判据)",
|
|
413
|
+
id1.startswith("unr_") and "隔离级别" in id1
|
|
414
|
+
and _mdcg._NODE_ID_RE.match(id1) is not None, id1)
|
|
415
|
+
check("F2 首次建单带「# 现场:」且含数值",
|
|
416
|
+
"# 现场:" in c1 and "结论差异 0.69" in c1, repr(c1[:120]))
|
|
417
|
+
|
|
418
|
+
# 覆盖写:对方节点被改写(同 id 覆写既有节点)
|
|
419
|
+
nid_over = cg.add("k_x", "# 功能名:对方节点\n# 生效条件:条件B\n",
|
|
420
|
+
layer="knowledge", override=True)
|
|
421
|
+
check("F3 前置:对方节点确已覆写(正文换新且同 id)",
|
|
422
|
+
nid_over == "k_x"
|
|
423
|
+
and "条件B" in ((cg.get("k_x") or {}).get("content") or ""),
|
|
424
|
+
str(nid_over))
|
|
425
|
+
# 同题再检出(措辞不同、现场数值已变)→ 同一张工单、现场刷新
|
|
426
|
+
n_before = len(cg.index["nodes"])
|
|
427
|
+
rep2 = dict(rep1)
|
|
428
|
+
rep2["detail"] = [{"type": "same_condition_divergence", "with": "k_x",
|
|
429
|
+
"same_condition": 1.0, "slot_overlap": 1.0,
|
|
430
|
+
"conclusion_overlap": 0.12}]
|
|
431
|
+
fw2 = cg.flywheel_step(rep2)
|
|
432
|
+
c2 = (cg.get(id1) or {}).get("content") or ""
|
|
433
|
+
fm2 = (cg.get(id1) or {}).get("frontmatter") or {}
|
|
434
|
+
check("F4 措辞/数值不同的同一问题 → 同 id、零新建(不重复建单)",
|
|
435
|
+
fw2["unresolved_id"] == id1 and len(cg.index["nodes"]) == n_before,
|
|
436
|
+
"%s vs %s / n=%d→%d" % (fw2["unresolved_id"], id1, n_before,
|
|
437
|
+
len(cg.index["nodes"])))
|
|
438
|
+
check("F5 「# 现场:」已刷新为新数值(旧数值不再残留)",
|
|
439
|
+
"结论差异 0.12" in c2 and "结论差异 0.69" not in c2,
|
|
440
|
+
repr([l for l in c2.splitlines() if l.startswith("# 现场")]))
|
|
441
|
+
check("F6 刷新只动正文:id/created_at/归属(fm)保持建单事实不变",
|
|
442
|
+
fm2.get("created_at") == fm1.get("created_at")
|
|
443
|
+
and fm2.get("id") == id1
|
|
444
|
+
and not (fm2.get("consistency") or {}).get("verdict"),
|
|
445
|
+
str({k: fm2.get(k) for k in ("id", "created_at", "session")}))
|
|
446
|
+
# 幂等:同输入再调 → 零写盘
|
|
447
|
+
(_r, nw) = _count_calls(cg, "_write_node",
|
|
448
|
+
lambda: cg.flywheel_step(rep2))
|
|
449
|
+
check("F7 同现场再检出不制造脏写盘(正文逐字相同即零写入)",
|
|
450
|
+
nw == 0 and (cg.get(id1) or {}).get("content") == c2,
|
|
451
|
+
"writes=%d" % nw)
|
|
452
|
+
# 身份含实际态:不同故障态是不同工单
|
|
453
|
+
rep3 = dict(rep2)
|
|
454
|
+
rep3["actual_state"] = "REJECT"
|
|
455
|
+
fw3 = cg.flywheel_step(rep3)
|
|
456
|
+
check("F8 身份含实际态:DEFER 与 REJECT 是两张工单(不误并)",
|
|
457
|
+
fw3["unresolved_id"] != id1, str(fw3["unresolved_id"]))
|
|
458
|
+
# ⑦ 的判据面:**问题模板/期望态措辞**不进身份——同一主题+同一实际态换个写法
|
|
459
|
+
# 仍是同一张工单(改前 sha1(整段 question) 会各建一张)
|
|
460
|
+
n_b3 = len(cg.index["nodes"])
|
|
461
|
+
rep4 = dict(rep2)
|
|
462
|
+
rep4["expected_state"] = "无条件"
|
|
463
|
+
fw4 = cg.flywheel_step(rep4)
|
|
464
|
+
check("F10 期望态措辞不同(同一主题/实际态)→ 同 id、零新建",
|
|
465
|
+
fw4["unresolved_id"] == id1 and len(cg.index["nodes"]) == n_b3,
|
|
466
|
+
"%s vs %s / n=%d" % (fw4["unresolved_id"], id1,
|
|
467
|
+
len(cg.index["nodes"])))
|
|
468
|
+
# 语义 slug 单点:规范化后再 slug(空白/非法字符差异不产生新工单)
|
|
469
|
+
a = cg.add_unresolved("为何 X 出现 DEFER?", topic="某主题|DEFER")
|
|
470
|
+
b = cg.add_unresolved("为何 X 出现 DEFER? ", topic="某主题|DEFER")
|
|
471
|
+
check("F9 同 topic 的空白/标点差异不新建(规范化→slug 同一身份)",
|
|
472
|
+
a == b, "%s vs %s" % (a, b))
|
|
473
|
+
cg.close()
|
|
474
|
+
return cg
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
# ---------------------------------------------------------------- G 组(M3)
|
|
478
|
+
def group_g():
|
|
479
|
+
print("\n[G] M3:节点总数传进 bucket_health(空库 vs 有节点零分桶)")
|
|
480
|
+
# 有节点但零分桶:全节点落在非分桶层(BUCKETED_LAYERS 只有 knowledge)
|
|
481
|
+
cg = _mk("m3_nobucket")
|
|
482
|
+
for i in range(3):
|
|
483
|
+
cg.add("s_%d" % i, "# 功能名:自我%d\n# 正文:某内容\n" % i,
|
|
484
|
+
layer="self")
|
|
485
|
+
cg.flush()
|
|
486
|
+
h = cg.health()
|
|
487
|
+
check("G1 有节点但分桶表为空 → ok=False / reason=empty_buckets(不冒充健康)",
|
|
488
|
+
cg.index.get("buckets") == {} and h.get("total_nodes") == 3
|
|
489
|
+
and h.get("ok") is False and h.get("reason") == "empty_buckets"
|
|
490
|
+
and h.get("nodes") == 3,
|
|
491
|
+
"buckets=%s health=%s" % (cg.index.get("buckets"),
|
|
492
|
+
{k: h.get(k) for k in ("ok", "reason",
|
|
493
|
+
"nodes", "total_nodes")}))
|
|
494
|
+
cg.close()
|
|
495
|
+
empty = _mk("m3_empty")
|
|
496
|
+
h0 = empty.health()
|
|
497
|
+
check("G2 真空库(零节点零桶)→ ok=True / reason=empty(不误报)",
|
|
498
|
+
h0.get("ok") is True and h0.get("reason") == "empty"
|
|
499
|
+
and h0.get("total_nodes") == 0,
|
|
500
|
+
str({k: h0.get(k) for k in ("ok", "reason", "total_nodes")}))
|
|
501
|
+
empty.close()
|
|
502
|
+
one = _mk("m3_single")
|
|
503
|
+
one.add("one", "# 功能名:单桶\n# 正文:知识面 内容\n", layer="knowledge")
|
|
504
|
+
one.flush()
|
|
505
|
+
h1 = one.health()
|
|
506
|
+
check("G3 单桶库如实读数(不再说成「巨桶」;ok 仍为 False 未放宽)",
|
|
507
|
+
h1.get("buckets") == 1 and h1.get("ok") is False
|
|
508
|
+
and h1.get("problems")
|
|
509
|
+
and not any("巨桶" in p for p in h1["problems"]),
|
|
510
|
+
str(h1.get("problems")))
|
|
511
|
+
src = open(os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
512
|
+
"mdcg.py"), encoding="utf-8").read()
|
|
513
|
+
check("G4 传参契约在呼叫点显式可见(health 传 total_nodes=,非靠默认值)",
|
|
514
|
+
re.search(r"routing\.bucket_health\(self\.index\.get\(\"buckets\", \{\}\),"
|
|
515
|
+
r"\s*\n\s*total_nodes=len\(self\.index\[\"nodes\"\]\)\)",
|
|
516
|
+
src) is not None,
|
|
517
|
+
"呼叫点未传 total_nodes")
|
|
518
|
+
one.close()
|
|
519
|
+
return one
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
# ---------------------------------------------------------------- 变异(自证)
|
|
523
|
+
def _pre_fix_neg_score():
|
|
524
|
+
_mdcg.NEG_COVERAGE_SCORE = 1.0 # 改动前:提示条目分数恒 1.0
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def _pre_fix_k_budget():
|
|
528
|
+
_mdcg.MdCG._primary_slots = staticmethod(lambda k, n_tail: int(k))
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def _pre_fix_d_denom():
|
|
532
|
+
_mdcg._is_neg_coverage = lambda card: False # 改动前:分母含提示条目
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _pre_fix_base_dup():
|
|
536
|
+
_mdcg.NEG_ROUTE_LAYERS = () # 改动前:负层/目标槽进正排
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _pre_fix_sentinel():
|
|
540
|
+
mdcos._is_null_condition = lambda value: False # 改动前:哨兵当真实条件
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _pre_fix_flywheel_scope():
|
|
544
|
+
consistency.FLYWHEEL_TRIGGERS = ("REJECT", "DEFER", "BLINDSPOT")
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def _pre_fix_ticket_slug():
|
|
548
|
+
_mdcg.MdCG._ticket_slug = staticmethod(
|
|
549
|
+
lambda question, topic=None: hashlib.sha1(
|
|
550
|
+
str(question or "").encode("utf-8")).hexdigest()[:10])
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _pre_fix_refresh():
|
|
554
|
+
_mdcg.MdCG._refresh_unresolved = lambda self, node_id, entry, content: False
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def _pre_fix_health_total():
|
|
558
|
+
_orig = routing.bucket_health
|
|
559
|
+
routing.bucket_health = lambda counts, total_nodes=None: _orig(counts)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
_MUTATIONS = {
|
|
563
|
+
"neg_score": (_pre_fix_neg_score, ["A1", "A8"]),
|
|
564
|
+
"k_budget": (_pre_fix_k_budget, ["A5", "A6"]),
|
|
565
|
+
"d_denom": (_pre_fix_d_denom, ["B1"]),
|
|
566
|
+
"base_dup": (_pre_fix_base_dup, ["C1", "C2", "C4", "C5"]),
|
|
567
|
+
"sentinel": (_pre_fix_sentinel, ["E1", "E2"]),
|
|
568
|
+
"flywheel_scope": (_pre_fix_flywheel_scope, ["D2", "D3"]),
|
|
569
|
+
"ticket_slug": (_pre_fix_ticket_slug, ["F1", "F9", "F10"]),
|
|
570
|
+
"refresh": (_pre_fix_refresh, ["F5"]),
|
|
571
|
+
"health_total": (_pre_fix_health_total, ["G1"]),
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def _snapshot():
|
|
576
|
+
# 静态方法必须取**原始描述符**(`MdCG.__dict__[...]`):`getattr` 会经描述符
|
|
577
|
+
# 协议退回裸函数,写回时就会被当成实例方法(多收一个 self)——变异自证第一轮
|
|
578
|
+
# 实测踩过(`_primary_slots() takes 2 positional arguments but 3 were given`)。
|
|
579
|
+
return {"NEG_COVERAGE_SCORE": _mdcg.NEG_COVERAGE_SCORE,
|
|
580
|
+
"_primary_slots": _mdcg.MdCG.__dict__["_primary_slots"],
|
|
581
|
+
"_is_neg_coverage": _mdcg._is_neg_coverage,
|
|
582
|
+
"NEG_ROUTE_LAYERS": _mdcg.NEG_ROUTE_LAYERS,
|
|
583
|
+
"_is_null_condition": mdcos._is_null_condition,
|
|
584
|
+
"FLYWHEEL_TRIGGERS": consistency.FLYWHEEL_TRIGGERS,
|
|
585
|
+
"_ticket_slug": _mdcg.MdCG.__dict__["_ticket_slug"],
|
|
586
|
+
"_refresh_unresolved": _mdcg.MdCG.__dict__["_refresh_unresolved"],
|
|
587
|
+
"bucket_health": routing.bucket_health}
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def _restore(snap):
|
|
591
|
+
_mdcg.NEG_COVERAGE_SCORE = snap["NEG_COVERAGE_SCORE"]
|
|
592
|
+
_mdcg.MdCG._primary_slots = snap["_primary_slots"]
|
|
593
|
+
_mdcg._is_neg_coverage = snap["_is_neg_coverage"]
|
|
594
|
+
_mdcg.NEG_ROUTE_LAYERS = snap["NEG_ROUTE_LAYERS"]
|
|
595
|
+
mdcos._is_null_condition = snap["_is_null_condition"]
|
|
596
|
+
consistency.FLYWHEEL_TRIGGERS = snap["FLYWHEEL_TRIGGERS"]
|
|
597
|
+
_mdcg.MdCG._ticket_slug = snap["_ticket_slug"]
|
|
598
|
+
_mdcg.MdCG._refresh_unresolved = snap["_refresh_unresolved"]
|
|
599
|
+
routing.bucket_health = snap["bucket_health"]
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _run_all():
|
|
603
|
+
global PASS, FAIL, FAILS
|
|
604
|
+
_RUN[0] += 1
|
|
605
|
+
PASS, FAIL, FAILS = 0, 0, []
|
|
606
|
+
group_a()
|
|
607
|
+
group_b()
|
|
608
|
+
group_c()
|
|
609
|
+
group_d()
|
|
610
|
+
group_e()
|
|
611
|
+
group_f()
|
|
612
|
+
group_g()
|
|
613
|
+
return PASS, list(FAILS)
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def main(argv):
|
|
617
|
+
which = None
|
|
618
|
+
for a in argv:
|
|
619
|
+
if a.startswith("--drop-fix"):
|
|
620
|
+
which = a.split("=", 1)[1] if "=" in a else "all"
|
|
621
|
+
if which is None:
|
|
622
|
+
ok, bad = _run_all()
|
|
623
|
+
print("\n==== 负覆盖诚实化守卫:%d 通过%s ====" % (
|
|
624
|
+
ok, ",%d 失败:%s" % (len(bad), "; ".join(bad)) if bad else ""))
|
|
625
|
+
return 1 if bad else 0
|
|
626
|
+
names = list(_MUTATIONS) if which == "all" else [which]
|
|
627
|
+
rc = 0
|
|
628
|
+
print("\n=== 定点变异自证(抽掉修复 → 守卫必红 → 恢复 → 转绿)===")
|
|
629
|
+
for name in names:
|
|
630
|
+
if name not in _MUTATIONS:
|
|
631
|
+
print(" 未知变异 %r(可选:%s)" % (name, sorted(_MUTATIONS)))
|
|
632
|
+
return 2
|
|
633
|
+
fn, want = _MUTATIONS[name]
|
|
634
|
+
snap = _snapshot()
|
|
635
|
+
fn()
|
|
636
|
+
ok, bad = _run_all()
|
|
637
|
+
hit = [n for n in want
|
|
638
|
+
if any(b.split()[0] == n or b.startswith(n) for b in bad)]
|
|
639
|
+
missed = [n for n in want if n not in hit]
|
|
640
|
+
print(" [%s] 红项 %d 个:%s" % (name, len(bad), "; ".join(bad) or "(无)"))
|
|
641
|
+
if missed or not bad:
|
|
642
|
+
print(" FAIL 变异 %s 未打红预期断言:%s" % (name, missed or "全绿"))
|
|
643
|
+
rc = 1
|
|
644
|
+
else:
|
|
645
|
+
print(" ok 变异 %s 打红预期断言 %d 项" % (name, len(hit)))
|
|
646
|
+
_restore(snap)
|
|
647
|
+
ok2, bad2 = _run_all()
|
|
648
|
+
if bad2:
|
|
649
|
+
print(" FAIL 变异 %s 恢复后未转绿,残余红项:%s" % (name, bad2))
|
|
650
|
+
rc = 1
|
|
651
|
+
else:
|
|
652
|
+
print(" ok 变异 %s 恢复后转绿(%d 通过,无残余)" % (name, ok2))
|
|
653
|
+
return rc
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
if __name__ == "__main__":
|
|
657
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
658
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
659
|
+
try:
|
|
660
|
+
_rc = main(sys.argv[1:])
|
|
661
|
+
finally:
|
|
662
|
+
shutil.rmtree(_SANDBOX, ignore_errors=True)
|
|
663
|
+
sys.exit(_rc)
|