@furongjun1999/dsh-memory 0.5.1 → 0.6.1
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 +588 -552
- package/codebuddy/CODEBUDDY.md +10 -10
- package/data/policy.json +27 -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/DSH/346/227/245/345/277/227/347/264/242/345/274/225v2_/345/217/202/350/200/203dsh-TUI_v1.0.md +248 -0
- package/docs/eval/DSH/346/227/245/345/277/227/347/264/242/345/274/225/346/225/210/346/236/234/351/252/214/350/257/201_v1.0.md +209 -0
- package/docs/eval/DSH/347/253/257/347/274/272/351/231/267/344/270/223/351/241/271_v1.0.md +254 -0
- 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/P1b2_/350/257/273/351/235/242/344/273/243/351/231/205/344/277/256/345/244/215_v1.0.md +124 -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/217/221/345/270/20306_/344/270/200/351/224/256/351/205/215/347/275/256/344/270/216DSH0172_v1.0.md +171 -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//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_v18.md +183 -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_v19.md +207 -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/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 +20 -0
- 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 +12 -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 +21 -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//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//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/dsh/cordis-patch-profile-web.example.yml +35 -0
- package/lib/bridge.js +24 -2
- package/lib/cli.d.ts +3 -0
- package/lib/cli.js +66 -0
- package/lib/hooks.d.ts +31 -2
- package/lib/hooks.js +219 -16
- package/lib/init.d.ts +88 -0
- package/lib/init.js +302 -0
- 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_p0.py +1 -1
- package/md_cg/branches.py +2 -2
- package/md_cg/ccgc.py +1071 -1006
- package/md_cg/chain.py +1 -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/evidence.py +585 -582
- package/md_cg/evolution.py +17 -1
- package/md_cg/export.py +1 -1
- package/md_cg/forgetting.py +309 -13
- package/md_cg/fsutil.py +454 -8
- 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 +4152 -3818
- package/md_cg/mdcg.py +4208 -3462
- package/md_cg/mdcos.py +4491 -4137
- package/md_cg/mreview/pipeline.py +15 -1
- package/md_cg/nodefile.py +639 -575
- 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/review_cli.py +49 -4
- 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/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 +1299 -1168
- package/md_cg/tasks.py +469 -470
- 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_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_govern_directread.py +17 -4
- package/md_cg/test_h2_session_view_norm.py +233 -0
- package/md_cg/test_h4_sustain_snapshot.py +1377 -0
- package/md_cg/test_hive_ingest.py +285 -285
- package/md_cg/test_index_crossprocess_reload.py +301 -0
- 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 +513 -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_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 +166 -0
- 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_p30_maintain.py +33 -2
- package/md_cg/test_p31_insight.py +1 -0
- 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 +516 -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_cli_attribution.py +177 -0
- package/md_cg/test_review_cli_visibility.py +235 -0
- package/md_cg/test_review_conformance.py +21 -3
- package/md_cg/test_security_audit_v21.py +2 -2
- package/md_cg/test_server_version.py +67 -0
- package/md_cg/test_srcindex.py +171 -0
- package/md_cg/test_tenant_env_override_warn.py +63 -50
- package/md_cg/test_token_lowercase_form.py +315 -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/wisdom/knowledge_points.py +1 -1
- package/md_cg/writelimit.py +42 -8
- package/md_cg/writepipe.py +651 -554
- package/package.json +8 -2
- package/skills/plugin.json +1 -1
- package/src/bridge.ts +25 -2
- package/src/cli.ts +65 -0
- package/src/hooks.ts +229 -16
- package/src/init.ts +361 -0
- package/src/lib/prompt_safety.ts +88 -1
- package/zcode/AGENTS.md +10 -10
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""md_cg · review_cli 裁决落盘跨进程可见性守卫(P1b autoflush 第二落点,DSH 端在役实测)
|
|
3
|
+
|
|
4
|
+
缺陷(修复前):`review_cli._cg` 构造 `MdCGSecure(_root(args), principal=p)`
|
|
5
|
+
不传 autoflush → 缺省 64。库的索引增量链路是「写入先经 `_stage` 入内存
|
|
6
|
+
`_dirty` 与内存索引,攒够 autoflush 条才 flush() 落分片日志 `_index_log/`」;
|
|
7
|
+
短命进程的**正常退出**另有 main finally `cg.close()` + 库层 atexit 双兜底
|
|
8
|
+
(mdcg.py `_LIVE_CGS`,2026-09-16 取证),但**被 kill / 崩溃退出时两者都不
|
|
9
|
+
执行**(兜底刻意只覆盖正常退出这一条路径)——裁决落盘的少量节点停在内存
|
|
10
|
+
索引、分片日志从未落,盘面节点文件在而索引链路断。DSH 端在役实测:裁决后
|
|
11
|
+
MCP 读面 read null。对照:mcp_server.py:3695-3702 同症状 2026-09-16 已修
|
|
12
|
+
(server 级 autoflush=1 兜底),review_cli 这个短命工具落点被遗漏——本修复
|
|
13
|
+
补齐,与 server 同款。
|
|
14
|
+
|
|
15
|
+
守卫(四组,全部临时库 + 哑主密钥,绝不触真实 ~/.mdcg):
|
|
16
|
+
① 真实在役入口:subprocess 跑 `python -m md_cg.review_cli accept`(正常
|
|
17
|
+
完成)→ 进程 B(全新 python 子进程 _load_index + read)断言业务节点与
|
|
18
|
+
审计记录节点均可见。
|
|
19
|
+
② kill 形态可见性:进程 A' 等价直接调库落盘 2 条(无任何显式 flush)后
|
|
20
|
+
`os._exit(0)`(模拟被杀:close/atexit 全部跳过)→ 进程 B 全新子进程
|
|
21
|
+
断言读回可见。
|
|
22
|
+
③ 落账契约(autoflush=1 的直接可观察效应,**修复前红**):② 的进程 A'
|
|
23
|
+
被杀退出后,盘面 `_index_log/` 必须已含其全部写入的索引记录——"写入即
|
|
24
|
+
落分片日志",与 mcp_server 的 server 级 autoflush=1 同一契约。
|
|
25
|
+
④ 同病工具面:evidence._open_cg / scripts/linkref_backfill /
|
|
26
|
+
scripts/migrate_restricted 同样补 autoflush=1(前两者行为断言
|
|
27
|
+
.autoflush==1,后两者源级断言构造语句带参)。
|
|
28
|
+
|
|
29
|
+
如实边界:本仓 HEAD 上 ①② 的读面断言在修复前已绿——正常路径由 09-16 的
|
|
30
|
+
review_decide 显式 flush(mdcos.py)+ close/atexit 兜底覆盖;kill 形态的
|
|
31
|
+
重开读面由批次39(2026-09-25)`_load_index` 指纹失配全扫自愈兜住。修复前
|
|
32
|
+
红的只有 ③ 的落账契约与在役旧读面(0.4.x 副本 `_load_index` 盲信快照)——
|
|
33
|
+
后者正是 DSH 端观察到 read null 的形态;本守卫把读面断言一并钉住,防上述
|
|
34
|
+
两层兜底未来被移除时旧症复发。
|
|
35
|
+
|
|
36
|
+
运行:python -m md_cg.test_review_cli_visibility
|
|
37
|
+
"""
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import argparse
|
|
41
|
+
import os
|
|
42
|
+
import re
|
|
43
|
+
import shutil
|
|
44
|
+
import subprocess
|
|
45
|
+
import sys
|
|
46
|
+
import tempfile
|
|
47
|
+
|
|
48
|
+
from .mdcg import ShardedLog
|
|
49
|
+
from .mdcos import MdCGSecure
|
|
50
|
+
from .security import Principal
|
|
51
|
+
|
|
52
|
+
PASS = FAIL = 0
|
|
53
|
+
FAILS = []
|
|
54
|
+
|
|
55
|
+
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
56
|
+
|
|
57
|
+
DUMMY_KEY = "ab" * 32 # 哑主密钥:子进程绝不触真实 ~/.mdcg/master.key
|
|
58
|
+
|
|
59
|
+
KILL_CHILD = r'''
|
|
60
|
+
import argparse, os, sys
|
|
61
|
+
sys.path.insert(0, sys.argv[2])
|
|
62
|
+
from md_cg import review_cli
|
|
63
|
+
# 等价直接调库落盘:走 review_cli 的生产构造路径 _cg(与 CLI 同一 autoflush),
|
|
64
|
+
# 随后裸 add 两条(无任何显式 flush)——覆盖 review_decide 之外的写入形态。
|
|
65
|
+
cg = review_cli._cg(argparse.Namespace(root=sys.argv[1]))
|
|
66
|
+
sys.stderr.write("staged=0 autoflush=%d\n" % cg.autoflush)
|
|
67
|
+
cg.add("vis-node-1", "P1b 可见性守卫节点一:kill 形态落账。", layer="knowledge",
|
|
68
|
+
tags=["vis:p1b"])
|
|
69
|
+
cg.add("vis-node-2", "P1b 可见性守卫节点二:kill 形态落账。", layer="knowledge",
|
|
70
|
+
tags=["vis:p1b"])
|
|
71
|
+
os._exit(0) # 硬退:close / atexit 一律不执行(模拟被杀)
|
|
72
|
+
'''
|
|
73
|
+
|
|
74
|
+
READ_CHILD = r'''
|
|
75
|
+
import sys
|
|
76
|
+
sys.path.insert(0, sys.argv[2])
|
|
77
|
+
from md_cg.mdcos import MdCGSecure
|
|
78
|
+
from md_cg.security import Principal
|
|
79
|
+
p = Principal(actor="vis-reader", clearance="internal", can_write=True,
|
|
80
|
+
can_admin=True, role="designer", auth_mode="test")
|
|
81
|
+
cg = MdCGSecure(sys.argv[1], principal=p)
|
|
82
|
+
ids = sys.argv[3].split(",")
|
|
83
|
+
print("READ " + " ".join("1" if cg.get(nid) is not None else "0" for nid in ids))
|
|
84
|
+
'''
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def check(name, cond, detail=""):
|
|
88
|
+
global PASS, FAIL
|
|
89
|
+
if cond:
|
|
90
|
+
PASS += 1
|
|
91
|
+
print(f" [PASS] {name}" + (f" · {detail}" if detail else ""))
|
|
92
|
+
else:
|
|
93
|
+
FAIL += 1
|
|
94
|
+
FAILS.append(name)
|
|
95
|
+
print(f" [FAIL] {name} · {detail}")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _child_env(extra=None):
|
|
99
|
+
"""子进程环境:哑密钥 + UTF-8 + PYTHONPATH 指向本仓(纪律 15)。"""
|
|
100
|
+
env = dict(os.environ)
|
|
101
|
+
env["MDCG_MASTER_KEY"] = DUMMY_KEY
|
|
102
|
+
env["PYTHONUTF8"] = "1"
|
|
103
|
+
env["PYTHONIOENCODING"] = "utf-8"
|
|
104
|
+
env["PYTHONPATH"] = REPO + os.pathsep + env.get("PYTHONPATH", "")
|
|
105
|
+
env.update(extra or {})
|
|
106
|
+
return env
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _run_cli(root, argv):
|
|
110
|
+
"""跑一次 review_cli(包内入口 python -m md_cg.review_cli,真实在役入口)。"""
|
|
111
|
+
return subprocess.run(
|
|
112
|
+
[sys.executable, "-X", "utf8", "-m", "md_cg.review_cli"] + argv,
|
|
113
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
114
|
+
env=_child_env(), cwd=REPO)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _spawn(code, root, arg3=""):
|
|
118
|
+
"""独立子进程跑一段内嵌脚本(进程 A' 杀退形态 / 进程 B 全新读面)。"""
|
|
119
|
+
return subprocess.run(
|
|
120
|
+
[sys.executable, "-X", "utf8", "-c", code, root, REPO, arg3],
|
|
121
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
122
|
+
env=_child_env())
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _propose(root, nid, content):
|
|
126
|
+
"""以原始写入者身份入队一条提案(复刻 DSH 端 agent 形态)。"""
|
|
127
|
+
cg = MdCGSecure(root, principal=Principal(
|
|
128
|
+
actor="dsh-memory", clearance="internal", can_write=True,
|
|
129
|
+
can_admin=False, role="recorder", session="sess_writer_orig"))
|
|
130
|
+
try:
|
|
131
|
+
return cg.propose(nid, content, layer="knowledge", tags=["vis:p1b"])
|
|
132
|
+
finally:
|
|
133
|
+
cg.close()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _log_ids(root):
|
|
137
|
+
"""读盘面分片日志的记录 id 集(落账契约的观察点)。"""
|
|
138
|
+
log_dir = os.path.join(root, "_index_log")
|
|
139
|
+
if not os.path.isdir(log_dir):
|
|
140
|
+
return None # 目录不存在 = 一条都没落过
|
|
141
|
+
return {rec.get("id") for rec in ShardedLog.read_all(log_dir)
|
|
142
|
+
if rec.get("id")}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def main():
|
|
146
|
+
roots = []
|
|
147
|
+
try:
|
|
148
|
+
# ---------- ① 真实在役入口:review_cli accept → 进程 B 读 ----------
|
|
149
|
+
print("\n【①】subprocess 跑 review_cli accept(正常完成)→ 进程 B 读")
|
|
150
|
+
root_1 = tempfile.mkdtemp(prefix="clivis_1_")
|
|
151
|
+
roots.append(root_1)
|
|
152
|
+
pid = _propose(root_1, "vis-acc-node",
|
|
153
|
+
"P1b 可见性守卫:accept 落盘后其它进程立即可见。")
|
|
154
|
+
r = _run_cli(root_1, ["accept", pid, "--root", root_1,
|
|
155
|
+
"--reason", "可见性守卫"])
|
|
156
|
+
check("review_cli accept 退出码 0", r.returncode == 0,
|
|
157
|
+
(r.stdout or r.stderr or "")[:200])
|
|
158
|
+
b = _spawn(READ_CHILD, root_1, "vis-acc-node") # 全新子进程 _load_index+read
|
|
159
|
+
m = re.search(r"READ (\d)", b.stdout or "")
|
|
160
|
+
check("进程 B 读回业务节点 vis-acc-node 可见",
|
|
161
|
+
bool(m) and m.group(1) == "1",
|
|
162
|
+
(b.stdout or b.stderr or "")[:200])
|
|
163
|
+
pa = MdCGSecure(root_1, principal=Principal(
|
|
164
|
+
actor="vis-reader", clearance="internal", can_write=True,
|
|
165
|
+
can_admin=True, role="designer", auth_mode="test"))
|
|
166
|
+
try:
|
|
167
|
+
recs = pa.review_records(pid)
|
|
168
|
+
finally:
|
|
169
|
+
pa.close()
|
|
170
|
+
check("进程 B 侧审计记录节点可见(review_records 命中 1 条)",
|
|
171
|
+
len(recs) == 1, str(recs))
|
|
172
|
+
|
|
173
|
+
# ---------- ②③ kill 形态:调库落盘后 os._exit → 落账契约 + 读 ----------
|
|
174
|
+
print("\n【②③】进程 A' 调库落 2 条即 os._exit(模拟被杀)→ 落账契约 + 进程 B 读")
|
|
175
|
+
root_2 = tempfile.mkdtemp(prefix="clivis_2_")
|
|
176
|
+
roots.append(root_2)
|
|
177
|
+
a = _spawn(KILL_CHILD, root_2)
|
|
178
|
+
check("进程 A' 经生产构造 _cg(autoflush=1 回显)",
|
|
179
|
+
a.returncode == 0 and "autoflush=1" in (a.stderr or ""),
|
|
180
|
+
(a.stderr or "")[:200])
|
|
181
|
+
ids = _log_ids(root_2)
|
|
182
|
+
check("③ 落账契约:A' 被杀退出后 _index_log 已含全部 2 条写入记录"
|
|
183
|
+
"(autoflush=1 写毕即落日志;修复前 autoflush=64 未达阈值=红)",
|
|
184
|
+
ids is not None and {"vis-node-1", "vis-node-2"} <= ids,
|
|
185
|
+
"log_ids=%s" % sorted(ids or []))
|
|
186
|
+
b2 = _spawn(READ_CHILD, root_2, "vis-node-1,vis-node-2") # 全新子进程读
|
|
187
|
+
m2 = re.search(r"READ (\d) (\d)", b2.stdout or "")
|
|
188
|
+
check("② 进程 B 读回 kill 形态的两条节点均可见",
|
|
189
|
+
bool(m2) and m2.group(1) == "1" and m2.group(2) == "1",
|
|
190
|
+
(b2.stdout or b2.stderr or "")[:200])
|
|
191
|
+
|
|
192
|
+
# ---------- ④ 同病工具面:三处同款补 autoflush=1 ----------
|
|
193
|
+
print("\n【④】同病工具面 autoflush=1")
|
|
194
|
+
cg_rv = None
|
|
195
|
+
try:
|
|
196
|
+
from . import review_cli as _rv
|
|
197
|
+
cg_rv = _rv._cg(argparse.Namespace(root=root_1))
|
|
198
|
+
check("review_cli._cg 构造 autoflush==1", cg_rv.autoflush == 1,
|
|
199
|
+
str(cg_rv.autoflush))
|
|
200
|
+
finally:
|
|
201
|
+
if cg_rv is not None:
|
|
202
|
+
cg_rv.close()
|
|
203
|
+
from . import evidence as _ev
|
|
204
|
+
cg_ev = _ev._open_cg(root_1)
|
|
205
|
+
try:
|
|
206
|
+
check("evidence._open_cg 构造 autoflush==1", cg_ev.autoflush == 1,
|
|
207
|
+
str(cg_ev.autoflush))
|
|
208
|
+
finally:
|
|
209
|
+
cg_ev.close()
|
|
210
|
+
for rel, anchor in (
|
|
211
|
+
("scripts/linkref_backfill.py",
|
|
212
|
+
"cg = MdCGSecure(a.root, principal=p, autoflush=1)"),
|
|
213
|
+
("scripts/migrate_restricted.py", None)):
|
|
214
|
+
with open(os.path.join(REPO, rel), encoding="utf-8") as f:
|
|
215
|
+
src = f.read()
|
|
216
|
+
if anchor is not None:
|
|
217
|
+
ok = anchor in src
|
|
218
|
+
else:
|
|
219
|
+
# migrate_restricted 构造跨多行:断言构造语句带 autoflush=1
|
|
220
|
+
ok = re.search(r"MdCGSecure\([^)]*?auth_mode=\"test\"\), "
|
|
221
|
+
r"autoflush=1\)", src, re.S) is not None
|
|
222
|
+
check("%s 构造语句带 autoflush=1(源级断言)" % rel, ok)
|
|
223
|
+
finally:
|
|
224
|
+
for r_ in roots:
|
|
225
|
+
shutil.rmtree(r_, ignore_errors=True)
|
|
226
|
+
|
|
227
|
+
print("\n" + "=" * 64)
|
|
228
|
+
print(f"PASS={PASS} FAIL={FAIL}")
|
|
229
|
+
if FAILS:
|
|
230
|
+
print("失败项:" + ";".join(FAILS))
|
|
231
|
+
return 0 if FAIL == 0 else 1
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
if __name__ == "__main__":
|
|
235
|
+
sys.exit(main())
|
|
@@ -204,7 +204,14 @@ def main():
|
|
|
204
204
|
pids = []
|
|
205
205
|
for p in procs:
|
|
206
206
|
out, _err = p.communicate(timeout=120)
|
|
207
|
-
|
|
207
|
+
try:
|
|
208
|
+
pids.append(json.loads(out.strip()))
|
|
209
|
+
except ValueError:
|
|
210
|
+
# N170 诊断增强:worker 崩溃时 stdout 为空,裸 json.loads 只会
|
|
211
|
+
# 报 JSONDecodeError 盲盒——把 returncode/stderr 带出来定位。
|
|
212
|
+
raise AssertionError(
|
|
213
|
+
"worker propose 输出非 JSON(rc=%s)stderr=%s"
|
|
214
|
+
% (p.returncode, (_err or "")[-500:])) from None
|
|
208
215
|
check("4 进程返回同一 pid", len(set(pids)) == 1, str(pids))
|
|
209
216
|
inbox2 = os.path.join(root2, "hippocampus", "inbox.jsonl")
|
|
210
217
|
with open(inbox2, encoding="utf-8") as f:
|
|
@@ -269,8 +276,19 @@ def main():
|
|
|
269
276
|
outs = []
|
|
270
277
|
for p in procs:
|
|
271
278
|
out, _err = p.communicate(timeout=180)
|
|
279
|
+
if not out.strip():
|
|
280
|
+
# N170 诊断增强:空输出先亮 worker 现场(rc/stderr),不再
|
|
281
|
+
# 留给下方 json.loads 一个盲盒崩溃。
|
|
282
|
+
print(" [DIAG] decide worker rc=%s stderr=%s"
|
|
283
|
+
% (p.returncode, (_err or "")[-500:]))
|
|
272
284
|
outs.append(out.strip())
|
|
273
|
-
|
|
285
|
+
douts = []
|
|
286
|
+
for o in outs:
|
|
287
|
+
try:
|
|
288
|
+
douts.append(json.loads(o))
|
|
289
|
+
except ValueError:
|
|
290
|
+
douts.append({"ok": False})
|
|
291
|
+
check("4 项裁决全部成功", all(d.get("ok") for d in douts),
|
|
274
292
|
str([o[:40] for o in outs]))
|
|
275
293
|
drecs = cg3.decisions()
|
|
276
294
|
check("decisions 读回恰好 4 条(无交错丢失)", len(drecs) == 4,
|
|
@@ -305,7 +323,7 @@ def main():
|
|
|
305
323
|
|
|
306
324
|
def _neg_n():
|
|
307
325
|
"""负记忆条目数(rejected 层)——NOOP 不得增加它。"""
|
|
308
|
-
return sum(1 for e in cg5.index["nodes"].values()
|
|
326
|
+
return sum(1 for e in list(cg5.index["nodes"].values())
|
|
309
327
|
if (e.get("layer") or "") == "rejected")
|
|
310
328
|
|
|
311
329
|
nb = _neg_n()
|
|
@@ -224,11 +224,11 @@ def v21_8_sensitive_family():
|
|
|
224
224
|
"service-account.json"]
|
|
225
225
|
for fn in leaks:
|
|
226
226
|
p = os.path.join(base, fn)
|
|
227
|
-
open(p, "w").close()
|
|
227
|
+
open(p, "w", encoding="utf-8").close()
|
|
228
228
|
check(f" 拒读:{fn}", m._sensitive_read(p) is not None)
|
|
229
229
|
for fn in ("notes.md", "readme.md", "data.json"):
|
|
230
230
|
p = os.path.join(base, fn)
|
|
231
|
-
open(p, "w").close()
|
|
231
|
+
open(p, "w", encoding="utf-8").close()
|
|
232
232
|
check(f" 不误伤:{fn}", m._sensitive_read(p) is None)
|
|
233
233
|
|
|
234
234
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""test_server_version.py · SERVER_VERSION 与包版本联动守卫(issue #42)
|
|
3
|
+
|
|
4
|
+
根因:md_cg/mcp_server.py 与 hive/hive_mcp/mcp_server.py 的 SERVER_VERSION
|
|
5
|
+
硬编码 "0.1.0",从未随发版联动——MCP 握手 serverInfo.version 向客户端自报
|
|
6
|
+
假版本(外部用户实测:包 0.5.1 握手自报 0.1.0)。
|
|
7
|
+
修复:SERVER_VERSION 改为 _package_version() 动态读包根 package.json。
|
|
8
|
+
|
|
9
|
+
守卫断言:
|
|
10
|
+
① md_cg.SERVER_VERSION == package.json 的 version(动态联动)
|
|
11
|
+
② hive_mcp.SERVER_VERSION == 同一 package.json version(同族同源)
|
|
12
|
+
③ 两值 != "0.1.0"(防回退硬编码——当前发布版本必非 0.1.0)
|
|
13
|
+
④ _package_version 对坏路径回落 "0.1.0" 不抛(保底契约)
|
|
14
|
+
"""
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
import unittest.mock as mock
|
|
19
|
+
|
|
20
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
21
|
+
|
|
22
|
+
PASS = 0
|
|
23
|
+
FAIL = 0
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def check(name, cond, detail=""):
|
|
27
|
+
global PASS, FAIL
|
|
28
|
+
if cond:
|
|
29
|
+
PASS += 1
|
|
30
|
+
print(f" [ok] {name}")
|
|
31
|
+
else:
|
|
32
|
+
FAIL += 1
|
|
33
|
+
print(f" [✘] {name} {detail}")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
37
|
+
with open(os.path.join(ROOT, "package.json"), encoding="utf-8") as f:
|
|
38
|
+
PKG_VERSION = json.load(f).get("version")
|
|
39
|
+
|
|
40
|
+
print(f"== 包版本基准: {PKG_VERSION} ==")
|
|
41
|
+
|
|
42
|
+
# ①② 动态联动
|
|
43
|
+
from md_cg import mcp_server as mdcg_server # noqa: E402
|
|
44
|
+
from hive.hive_mcp import mcp_server as hive_server # noqa: E402
|
|
45
|
+
|
|
46
|
+
check("① md_cg SERVER_VERSION == package.json version",
|
|
47
|
+
mdcg_server.SERVER_VERSION == PKG_VERSION,
|
|
48
|
+
f"got {mdcg_server.SERVER_VERSION!r} want {PKG_VERSION!r}")
|
|
49
|
+
check("② hive SERVER_VERSION == package.json version",
|
|
50
|
+
hive_server.SERVER_VERSION == PKG_VERSION,
|
|
51
|
+
f"got {hive_server.SERVER_VERSION!r} want {PKG_VERSION!r}")
|
|
52
|
+
|
|
53
|
+
# ③ 防硬编码回退(issue #42 的 0.1.0 即硬编码脱节形态)
|
|
54
|
+
check("③ 两值均非硬编码 0.1.0",
|
|
55
|
+
mdcg_server.SERVER_VERSION != "0.1.0" and hive_server.SERVER_VERSION != "0.1.0",
|
|
56
|
+
"出现 0.1.0 说明动态读取失效回退")
|
|
57
|
+
|
|
58
|
+
# ④ 回落保底契约:读 package.json 失败 → "0.1.0" 不抛(保底不阻塞启动)
|
|
59
|
+
try:
|
|
60
|
+
with mock.patch("builtins.open", side_effect=OSError("deny")):
|
|
61
|
+
v = mdcg_server._package_version()
|
|
62
|
+
check("④ _package_version 读取失败回落 0.1.0 不抛", v == "0.1.0", f"got {v!r}")
|
|
63
|
+
except Exception as e: # noqa: BLE001
|
|
64
|
+
check("④ _package_version 读取失败回落 0.1.0 不抛", False, f"raised {e!r}")
|
|
65
|
+
|
|
66
|
+
print(f"== 结果: {PASS} 通过 / {FAIL} 失败 ==")
|
|
67
|
+
sys.exit(1 if FAIL else 0)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""真源索引契约层守卫(P0):通用层覆盖任意文件 / 细化层仅可读文本 / 伴随文本挂通用层 / 回读逐字节。
|
|
3
|
+
|
|
4
|
+
判据姿态(P0 两次踩坑的产物,勿简化回「猜粒度」):
|
|
5
|
+
· **不猜提取器粒度**:细化层单位数与 `docindex.extract` 条数**平权**(units == extract + 1),
|
|
6
|
+
H1 节合并、过短节并入等语义归提取器,本层只保证「它切几节我就几条」;
|
|
7
|
+
· **哈希单一实现**:行区间哈希委托 `codeindex.region_hash`(sha1 前 12 位);本模块自写第二份
|
|
8
|
+
的行为已被 5b/5c 抓出并删除——两处算法会让漂移检测永远 hash_match=True;
|
|
9
|
+
· **伴随文本挂通用层**:媒体不进可读文本分支,挂错层等于废掉多模态语义通道(6b 抓出)。
|
|
10
|
+
|
|
11
|
+
实验纪律:全程 `tempfile` 临时目录 + 合成件(含**假魔数**的合成二进制,绝不用真实媒体/私有数据);
|
|
12
|
+
不写任何库面、不碰在役库、不出网。运行:退出码 0 = 全绿。
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import io
|
|
17
|
+
import os
|
|
18
|
+
import shutil
|
|
19
|
+
import sys
|
|
20
|
+
import tempfile
|
|
21
|
+
|
|
22
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
23
|
+
|
|
24
|
+
from md_cg import codeindex # noqa: E402
|
|
25
|
+
from md_cg import docindex # noqa: E402
|
|
26
|
+
from md_cg import srcindex as S # noqa: E402
|
|
27
|
+
|
|
28
|
+
PASS = FAIL = 0
|
|
29
|
+
FAILS = []
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def ck(cid: str, ok: bool, detail: str = "") -> None:
|
|
33
|
+
global PASS, FAIL
|
|
34
|
+
if ok:
|
|
35
|
+
PASS += 1
|
|
36
|
+
else:
|
|
37
|
+
FAIL += 1
|
|
38
|
+
FAILS.append("%s %s" % (cid, detail))
|
|
39
|
+
print(" %s %s%s" % ("OK " if ok else "FAIL", cid, "" if ok else " " + detail))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def w(path: str, data) -> str:
|
|
43
|
+
if isinstance(data, bytes):
|
|
44
|
+
with open(path, "wb") as f:
|
|
45
|
+
f.write(data)
|
|
46
|
+
else:
|
|
47
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
48
|
+
f.write(data)
|
|
49
|
+
return path
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
DOC = "\n".join([
|
|
53
|
+
"# 合成文档标题", "", "这是一段引言。", "",
|
|
54
|
+
"## 小节一", "", "小节一的正文,足够长以通过提取器的最小长度判断。", "",
|
|
55
|
+
"## 小节二", "", "小节二的正文,同样足够长,并带一行说明。", "",
|
|
56
|
+
"## 小节三", "", "小节三的正文。",
|
|
57
|
+
])
|
|
58
|
+
CCG = "\n".join([
|
|
59
|
+
"# 功能名:合成样例", "# 生效条件:仅用于 P0 契约层守卫;合成件、非真实数据",
|
|
60
|
+
"## 小节一", "正文一",
|
|
61
|
+
])
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def main() -> int:
|
|
65
|
+
tmp = tempfile.mkdtemp(prefix="p0_srcindex_")
|
|
66
|
+
try:
|
|
67
|
+
md = w(os.path.join(tmp, "说明.md"), DOC)
|
|
68
|
+
ccg = w(os.path.join(tmp, "带条件.md"), CCG)
|
|
69
|
+
png = w(os.path.join(tmp, "pic.png"), b"\x89PNG\r\n\x1a\n" + b"\x00\x01\x02" * 40)
|
|
70
|
+
w(os.path.join(tmp, "pic.md"), "# pic 简介\n\n这是伴随文本(sidecar)")
|
|
71
|
+
mp4 = w(os.path.join(tmp, "clip.mp4"), b"\x00\x00\x00\x18ftypmp42" + b"\x00" * 64)
|
|
72
|
+
raw = w(os.path.join(tmp, "blob.dat"), bytes(range(256)) * 4)
|
|
73
|
+
weird = w(os.path.join(tmp, "t.txt"), b"abc\x00def") # NUL ⇒ 不可读
|
|
74
|
+
fake = w(os.path.join(tmp, "disguised.txt"), b"\x89PNG\r\n\x1a\n" + b"\x00" * 16)
|
|
75
|
+
empty = w(os.path.join(tmp, "empty.md"), "")
|
|
76
|
+
|
|
77
|
+
# ① 通用层:任意文件都有 ≥1 条 whole 单位(含空文件/不可读二进制)
|
|
78
|
+
for label, p in (("md", md), ("png", png), ("mp4", mp4), ("raw", raw),
|
|
79
|
+
("nul", weird), ("empty", empty)):
|
|
80
|
+
us = S.units(p)
|
|
81
|
+
ck("1a[%s] 通用层恒在" % label,
|
|
82
|
+
bool(us) and us[0]["span"]["unit"] == "whole" and bool(us[0]["span_hash"]),
|
|
83
|
+
"units=%d" % len(us))
|
|
84
|
+
|
|
85
|
+
# ② 细化层:与 docindex.extract 平权(不猜粒度)
|
|
86
|
+
for label, p in (("md", md), ("ccg", ccg)):
|
|
87
|
+
text = io.open(p, encoding="utf-8").read()
|
|
88
|
+
want = len(docindex.extract(text, path=p)) + 1
|
|
89
|
+
got = len(S.units(p))
|
|
90
|
+
ck("2a[%s] 单位数 == extract+1" % label, got == want, "units=%d want=%d" % (got, want))
|
|
91
|
+
ck("2b[png] 无细化层", all(u["span"]["unit"] == "whole" for u in S.units(png)))
|
|
92
|
+
ck("2c[NUL 文本] 判不可读、不细化",
|
|
93
|
+
all(u["span"]["unit"] == "whole" for u in S.units(weird)))
|
|
94
|
+
ck("2d[空文件] 不细化且不抛", len(S.units(empty)) == 1)
|
|
95
|
+
|
|
96
|
+
# ③ 类型判据:魔数优先于扩展名;未知类型不拒
|
|
97
|
+
ck("3a[魔数优先] .txt 内容为 PNG ⇒ image/png",
|
|
98
|
+
S.sniff_type(fake) == "image/png", S.sniff_type(fake))
|
|
99
|
+
ck("3b[扩展名兜底] .md ⇒ text/markdown", S.sniff_type(md) == "text/markdown")
|
|
100
|
+
ck("3c[未知不拒] .dat ⇒ octet-stream",
|
|
101
|
+
S.sniff_type(raw) == "application/octet-stream", S.sniff_type(raw))
|
|
102
|
+
|
|
103
|
+
# ④ 哈希判别力:改 1 字节 ⇒ file_hash 变;旧单位回读 hash_match=False
|
|
104
|
+
u0 = S.units(md)[0]
|
|
105
|
+
h0 = u0["src"]["file_hash"]
|
|
106
|
+
w(md, DOC.replace("这是一段引言。", "这是一段引言!"))
|
|
107
|
+
u1 = S.units(md)[0]
|
|
108
|
+
ck("4a[1 字节改判] file_hash 变", h0 != u1["src"]["file_hash"])
|
|
109
|
+
ck("4b[陈旧检出] 旧单位回读 hash_match=False",
|
|
110
|
+
S.read_unit(u0).get("hash_match") is False)
|
|
111
|
+
ck("4c[新鲜一致] 新单位回读 hash_match=True",
|
|
112
|
+
S.read_unit(u1).get("hash_match") is True)
|
|
113
|
+
|
|
114
|
+
# ⑤ 行区间回读逐字节 + 与 codeindex.region_hash 单一实现
|
|
115
|
+
lu = [u for u in S.units(md) if u["span"]["unit"] == "line"][0]
|
|
116
|
+
got = S.read_unit(lu)
|
|
117
|
+
lines = io.open(md, encoding="utf-8").read().split("\n")
|
|
118
|
+
want_text = "\n".join(lines[lu["span"]["start"] - 1:lu["span"]["end"]])
|
|
119
|
+
ck("5a[区间逐字节]", got.get("text") == want_text, repr(got.get("text"))[:60])
|
|
120
|
+
ck("5b[区间哈希一致]", got.get("hash_match") is True)
|
|
121
|
+
ck("5c[哈希单一实现] 委托 codeindex.region_hash",
|
|
122
|
+
codeindex.region_hash(lines, lu["span"]["start"], lu["span"]["end"])
|
|
123
|
+
== S.region_hash("\n".join(lines), lu["span"]["start"], lu["span"]["end"])
|
|
124
|
+
== got["hash"])
|
|
125
|
+
|
|
126
|
+
# ⑥ 伴随文本挂**通用层**(媒体条目的语义通道)
|
|
127
|
+
comps = S.companions(png)
|
|
128
|
+
u_png = S.units(png)[0]
|
|
129
|
+
ck("6a[sidecar 发现]", any(c["kind"] == "sidecar" for c in comps),
|
|
130
|
+
str([c["name"] for c in comps]))
|
|
131
|
+
ck("6b[挂进通用层] media 条目 text_view 含伴随文本且带 companions",
|
|
132
|
+
"伴随文本" in u_png["text_view"] and bool(u_png.get("companions")))
|
|
133
|
+
|
|
134
|
+
# ⑦ 未实现 locator 必须显式不静默
|
|
135
|
+
bad = S.read_unit({"src": {"path": md}, "span": {"unit": "time", "t0": 0, "t1": 1}})
|
|
136
|
+
ck("7a[time 显式不支持]", bad.get("ok") is False and "time" in bad.get("error", ""),
|
|
137
|
+
str(bad)[:80])
|
|
138
|
+
|
|
139
|
+
# ⑧ CCG 条件面进入细化层(且与该文件的 CCG 行相关)
|
|
140
|
+
ccg_units = S.units(ccg)
|
|
141
|
+
ck("8a[条件面非空]", any(u.get("condition_space") for u in ccg_units),
|
|
142
|
+
str([bool(u.get("condition_space")) for u in ccg_units]))
|
|
143
|
+
ck("8b[CCG 行落在细化层]",
|
|
144
|
+
any("生效条件" in (u.get("text_view") or "") or
|
|
145
|
+
"生效条件" in str(u.get("condition_space"))
|
|
146
|
+
for u in ccg_units if u["span"]["unit"] == "line"),
|
|
147
|
+
str([u.get("text_view") for u in ccg_units if u["span"]["unit"] == "line"])[:90])
|
|
148
|
+
|
|
149
|
+
# ⑨ 能力自陈与实现一致(契约漂移守卫)
|
|
150
|
+
cap = S.capability()
|
|
151
|
+
ck("9a[units 自陈]", tuple(cap["units"]) == S.UNITS == ("whole", "line"))
|
|
152
|
+
ck("9b[未实现项自陈]", set(cap["not_implemented"]) == {"time", "region", "byte"})
|
|
153
|
+
ck("9c[零依赖/不写状态]", cap["deps"] == [] and cap["writes_state"] is False)
|
|
154
|
+
|
|
155
|
+
# ⑩ 不存在路径:fail-closed 抛,而不是静默造条目
|
|
156
|
+
try:
|
|
157
|
+
S.units(os.path.join(tmp, "不存在.bin"))
|
|
158
|
+
ck("10a[不存在 fail-closed]", False, "未抛")
|
|
159
|
+
except OSError:
|
|
160
|
+
ck("10a[不存在 fail-closed]", True)
|
|
161
|
+
finally:
|
|
162
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
163
|
+
|
|
164
|
+
print("\n结果:PASS %d / FAIL %d" % (PASS, FAIL))
|
|
165
|
+
if FAILS:
|
|
166
|
+
print("失败项:" + ";".join(FAILS))
|
|
167
|
+
return 0 if FAIL == 0 else 1
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
if __name__ == "__main__":
|
|
171
|
+
sys.exit(main())
|