@furongjun1999/dsh-memory 0.6.0 → 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 -572
- 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/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/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_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/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_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/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 -3547
- package/md_cg/mdcos.py +4491 -4159
- 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/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_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 +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_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_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 +3 -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/zcode/AGENTS.md +10 -10
package/md_cg/ccgc.py
CHANGED
|
@@ -1,1007 +1,1072 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""ccgc.py · 对话记录 → CCG 六要素编译器(接口契约形态)。
|
|
3
|
-
|
|
4
|
-
裁定 B(使用者 2026-09-16):六要素不是注释,是**接口契约**——
|
|
5
|
-
功能名 = 签名 signature | 生效条件 = 前置条件 precondition
|
|
6
|
-
子功能 = 依赖 dependency | 执行 = 调用 invocation
|
|
7
|
-
验证方式 = 后置条件 postcondition + test | 不适用条件 = 拒绝域 rejection_domain
|
|
8
|
-
缺任一入参即**编译错误**(E001-E004),不静默降级、不猜、不发明。
|
|
9
|
-
|
|
10
|
-
裁定 A(使用者 2026-09-16):**LLM 不得自己验证自己**。本模块是三段式中间段——
|
|
11
|
-
compile(生成候选) → attest(认知图**外**的验证方签章) → link(签章通过才写入)
|
|
12
|
-
LLM 若参与,只能经 `parser=` 注入(能力外置,与 compiler 的 llm_bridge 同构),
|
|
13
|
-
且其产物**必须**过名实门(E010/E011):每个写入值都必须是对话记录的**字面子串**,
|
|
14
|
-
幻觉在编译期即被机械检出,不进「看起来合理」的灰区。
|
|
15
|
-
签章字段在候选结构里**没有位置**:`verifier` 只能由 attest 显式给出,
|
|
16
|
-
且 E041 机械拒绝 `verifier == 编译执行者`。阻断强度**如实分层**(issue #27
|
|
17
|
-
纠正):①身份归一比较(大小写/空白/括号注记归一)拦截同源字符串变体;
|
|
18
|
-
②根本保障是 `verifier_token` 凭据通路——验证方身份经 mdcg 令牌 HMAC 验签
|
|
19
|
-
(与 narrowed_principal 同一信任源),不依赖自报诚实度;无令牌时结果标
|
|
20
|
-
`verifier_identity="self-reported"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
·
|
|
25
|
-
·
|
|
26
|
-
·
|
|
27
|
-
·
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
import
|
|
33
|
-
import
|
|
34
|
-
import
|
|
35
|
-
import
|
|
36
|
-
import
|
|
37
|
-
|
|
38
|
-
from
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
from .
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
#
|
|
83
|
-
|
|
84
|
-
#
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
#
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
#
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
#
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
#
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
if
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
if
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
#
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
"""
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
return
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
if
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
if not
|
|
753
|
-
out.
|
|
754
|
-
|
|
755
|
-
return out
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
if
|
|
760
|
-
out.errors.append("
|
|
761
|
-
return out
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
"""
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
if not
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
#
|
|
884
|
-
#
|
|
885
|
-
#
|
|
886
|
-
#
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""ccgc.py · 对话记录 → CCG 六要素编译器(接口契约形态)。
|
|
3
|
+
|
|
4
|
+
裁定 B(使用者 2026-09-16):六要素不是注释,是**接口契约**——
|
|
5
|
+
功能名 = 签名 signature | 生效条件 = 前置条件 precondition
|
|
6
|
+
子功能 = 依赖 dependency | 执行 = 调用 invocation
|
|
7
|
+
验证方式 = 后置条件 postcondition + test | 不适用条件 = 拒绝域 rejection_domain
|
|
8
|
+
缺任一入参即**编译错误**(E001-E004),不静默降级、不猜、不发明。
|
|
9
|
+
|
|
10
|
+
裁定 A(使用者 2026-09-16):**LLM 不得自己验证自己**。本模块是三段式中间段——
|
|
11
|
+
compile(生成候选) → attest(认知图**外**的验证方签章) → link(签章通过才写入)
|
|
12
|
+
LLM 若参与,只能经 `parser=` 注入(能力外置,与 compiler 的 llm_bridge 同构),
|
|
13
|
+
且其产物**必须**过名实门(E010/E011):每个写入值都必须是对话记录的**字面子串**,
|
|
14
|
+
幻觉在编译期即被机械检出,不进「看起来合理」的灰区。
|
|
15
|
+
签章字段在候选结构里**没有位置**:`verifier` 只能由 attest 显式给出,
|
|
16
|
+
且 E041 机械拒绝 `verifier == 编译执行者`。阻断强度**如实分层**(issue #27
|
|
17
|
+
纠正):①身份归一比较(大小写/空白/括号注记归一)拦截同源字符串变体;
|
|
18
|
+
②根本保障是 `verifier_token` 凭据通路——验证方身份经 mdcg 令牌 HMAC 验签
|
|
19
|
+
(与 narrowed_principal 同一信任源),不依赖自报诚实度;无令牌时结果标
|
|
20
|
+
`verifier_identity="self-reported"`(诚实降级;下游已兑现消费——
|
|
21
|
+
link(apply=true) 对无凭据签章一律 E052 拒绝落库,N176)。
|
|
22
|
+
|
|
23
|
+
对齐既有实现(零发明):
|
|
24
|
+
· 验证能力外置 / 缺能力恒不通过 → md_cg/audit.py(CONTENT_KINDS + register_verifier)
|
|
25
|
+
· 缺参即 error、success 由 errors 定 → compiler/api.py(CompileResult 范式)
|
|
26
|
+
· 生效条件唯一合成入口 → nodefile.condition_space_text(本模块不另写合成)
|
|
27
|
+
· 六行写入口径 → consolidate._upsert_ccg_line(同款语义,就地实现防 import 环)
|
|
28
|
+
· 留痕可回滚 → 对齐 backfill 的 _backfill.jsonl 纪律
|
|
29
|
+
"""
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import dataclasses
|
|
33
|
+
import hashlib
|
|
34
|
+
import json
|
|
35
|
+
import os
|
|
36
|
+
import re
|
|
37
|
+
import time
|
|
38
|
+
from dataclasses import asdict, dataclass, field
|
|
39
|
+
from typing import Any, Dict, List, Optional
|
|
40
|
+
|
|
41
|
+
from . import nodefile, protect
|
|
42
|
+
from .readcache import direct_read
|
|
43
|
+
|
|
44
|
+
# ---- 四态:复用 audit / judge_qualification 的裁决语汇(不新造状态机) ----
|
|
45
|
+
ACCEPT, REJECT, DEFER, BLINDSPOT = "ACCEPT", "REJECT", "DEFER", "BLINDSPOT"
|
|
46
|
+
STATES = (ACCEPT, REJECT, DEFER, BLINDSPOT)
|
|
47
|
+
|
|
48
|
+
LOG_NAME = "_ccgc.jsonl"
|
|
49
|
+
|
|
50
|
+
# ---- 裁定 B:六要素的契约角色(术语真源在 nodefile,此处只引用不另定义) ----
|
|
51
|
+
# 单一真源纪律:术语若在 ccgc 再写一份,会与 nodefile 漂移(谁是真源无从判定)。
|
|
52
|
+
CONTRACT_ROLES = nodefile.CCG_CONTRACT_ROLES
|
|
53
|
+
|
|
54
|
+
# ---- 编译错误码(裁定 B:缺参数即编译错误,绝不静默降级) ----
|
|
55
|
+
E_CODES = {
|
|
56
|
+
"E001": "缺少源:dialog 为空(无可编译的对话记录)",
|
|
57
|
+
"E002": "缺少目标:node_id 为空或节点不存在",
|
|
58
|
+
"E003": "缺少主体:actor 为空(验证溯源必需)",
|
|
59
|
+
"E004": "源不可读:密文或权限拒绝(fail-closed)",
|
|
60
|
+
"E010": "引用定位失败:span 不是对话记录的字面子串",
|
|
61
|
+
"E011": "值捏造:写入值不是对话记录的字面子串",
|
|
62
|
+
"E020": "四槽不全:condition_space 缺槽,不构成生效条件",
|
|
63
|
+
"E021": "槽值非法:空值、待填充占位或不可渲染",
|
|
64
|
+
"E030": "验证基底非法:不在 VERIFICATION_BASIS 枚举内",
|
|
65
|
+
"E040": "无验证签章:未提供 attestation(缺能力恒不写入)",
|
|
66
|
+
"E041": "自证拒绝:验证方标识 == 编译执行者(LLM 不得自己验证自己)",
|
|
67
|
+
"E042": "验证未通过:签章判定 REJECT / BLINDSPOT",
|
|
68
|
+
"E043": "修正非法:只允许修正 condition_space 四槽",
|
|
69
|
+
"E050": ("依赖声明缺失:正文以 `@<节点 id>` 声明了跨节点依赖(「# 子功能:」行),"
|
|
70
|
+
"但 depends_on 未给出可解析目标——依赖必须是可解析的字段,不能只是散文"),
|
|
71
|
+
"E051": "依赖目标不存在:depends_on 指向的节点不在库中(悬空依赖)",
|
|
72
|
+
# N176:兑现「下游策略可据此拒绝」——self-reported 签章不构成落库准入
|
|
73
|
+
"E052": ("无凭据签章:验证方身份未经 mdcg 令牌验签(verifier_identity="
|
|
74
|
+
"self-reported)——自报名不构成编外复核凭据,落库须令牌验证方"
|
|
75
|
+
"(verifier_token)签章"),
|
|
76
|
+
# N208:换行注入——ccg 行值是单行契约,换行会拆出伪造的独立正文行
|
|
77
|
+
"E022": ("值含换行:ccg 行值/四槽值必须是单行——换行会注入伪造的独立"
|
|
78
|
+
"正文行(`ccg_field_value` 取首个命中行,正文读面即被顶替)"),
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# ---- 候选来源标识(写进留痕,可溯源到「谁说的」) ----
|
|
82
|
+
SRC_EXPLICIT = "explicit" # 显式入参(人 / 设计者提供)
|
|
83
|
+
SRC_PARSER = "parser" # 外部注入解析器(LLM 等能力外置通道)
|
|
84
|
+
SRC_RULE = "rule" # 内生规则解析器(诚实下界能力)
|
|
85
|
+
|
|
86
|
+
#: 参与名实门的五要素(生效条件由四槽合成,不来自候选,故不在此列)
|
|
87
|
+
_CAND_MARKS = ("功能名", "子功能", "执行", "验证方式", "不适用条件")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# =============================================================================
|
|
91
|
+
# 结果对象(对齐 compiler/api.py 的 CompileResult 范式)
|
|
92
|
+
# =============================================================================
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class CompileResult:
|
|
96
|
+
"""编译结果。`success = (len(errors) == 0)`——与 compiler 同口径。"""
|
|
97
|
+
success: bool = False
|
|
98
|
+
node_id: str = ""
|
|
99
|
+
actor: str = ""
|
|
100
|
+
lines: Dict[str, str] = field(default_factory=dict) # 六要素行
|
|
101
|
+
slots: Dict[str, Any] = field(default_factory=dict) # condition_space 四槽
|
|
102
|
+
spans: Dict[str, List[str]] = field(default_factory=dict) # 要素 → 引用 span 清单
|
|
103
|
+
sources: Dict[str, str] = field(default_factory=dict) # 要素 → explicit|parser|rule
|
|
104
|
+
ungrounded: List[Dict[str, str]] = field(default_factory=list) # 未过名实门的项
|
|
105
|
+
errors: List[str] = field(default_factory=list)
|
|
106
|
+
warnings: List[str] = field(default_factory=list)
|
|
107
|
+
verdict: Optional[Dict] = None # 验证单元终裁
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
# 生效条件:BLINDSPOT:缺证据(无 required 形参与模块级常量,仅有未给出的 self.errors 来源)。
|
|
111
|
+
def has_errors(self) -> bool:
|
|
112
|
+
return len(self.errors) > 0
|
|
113
|
+
|
|
114
|
+
# 生效条件:对任意 code,msg 取 E_CODES.get(code, code)(缺键回落 code 本身),向 self.errors 追加 "code 空格 msg",extra 为真值时再追加 "(extra)"。
|
|
115
|
+
def err(self, code: str, extra: str = "") -> None:
|
|
116
|
+
msg = E_CODES.get(code, code)
|
|
117
|
+
self.errors.append(code + " " + msg + (("(" + extra + ")") if extra else ""))
|
|
118
|
+
|
|
119
|
+
# 生效条件:无入参,首行按 self.success 输出「编译成功」或「编译失败(len(self.errors) 个错误)」,要素行取 nodefile.CCG_MARKS 与 self.lines 的交集,errors 列前 10 条、warnings 列前 5 条,返回 "\n".join(out)。
|
|
120
|
+
def summary(self) -> str:
|
|
121
|
+
out = ["编译成功" if self.success else
|
|
122
|
+
"编译失败(%d 个错误)" % len(self.errors)]
|
|
123
|
+
out.append(" 节点: %s" % (self.node_id or "-"))
|
|
124
|
+
out.append(" 要素: %s" % ", ".join(k for k in nodefile.CCG_MARKS if k in self.lines))
|
|
125
|
+
for e in self.errors[:10]:
|
|
126
|
+
out.append(" ERROR " + e)
|
|
127
|
+
for w in self.warnings[:5]:
|
|
128
|
+
out.append(" WARN " + w)
|
|
129
|
+
return "\n".join(out)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass
|
|
133
|
+
class AttestResult:
|
|
134
|
+
"""验证签章结果。`ok=True` 才构成 link 的准入凭证。"""
|
|
135
|
+
ok: bool = False
|
|
136
|
+
node_id: str = ""
|
|
137
|
+
state: str = ""
|
|
138
|
+
verifier: str = "" # 认知图**外**的验证方(子代理 / designer / user)
|
|
139
|
+
compiled_by: str = "" # 编译执行者(供 E041 比对)
|
|
140
|
+
evidence: str = ""
|
|
141
|
+
error: str = ""
|
|
142
|
+
token: str = "" # 签章令牌(留痕可查)
|
|
143
|
+
# issue #27:验证方身份来源——"token"(mdcg 令牌 HMAC 验签,不可伪造)
|
|
144
|
+
# 或 "self-reported"(调用方自报字符串:归一化比较拦截已知同源变体,
|
|
145
|
+
# 但根本保障需令牌通路)。审计与下游策略据此识别可信级别。
|
|
146
|
+
verifier_identity: str = "self-reported"
|
|
147
|
+
slot_corrections: Dict[str, Any] = field(default_factory=dict)
|
|
148
|
+
ts: float = 0.0
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass
|
|
152
|
+
class LinkResult:
|
|
153
|
+
ok: bool = False
|
|
154
|
+
node_id: str = ""
|
|
155
|
+
dry_run: bool = True
|
|
156
|
+
written: int = 0
|
|
157
|
+
errors: List[str] = field(default_factory=list)
|
|
158
|
+
warnings: List[str] = field(default_factory=list)
|
|
159
|
+
entry_id: str = ""
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# =============================================================================
|
|
163
|
+
# 工具(就地实现,避免跨模块 import 环——与 mdcg.py 复制 _ccg_line 的既有做法一致)
|
|
164
|
+
# =============================================================================
|
|
165
|
+
|
|
166
|
+
# 生效条件:对任意 s(含 None/空串,源码以 `(s or "")` 兜底为空串),返回 hashlib.sha1(该串 utf-8 编码).hexdigest()[:12]。
|
|
167
|
+
def _sha(s: str) -> str:
|
|
168
|
+
return hashlib.sha1((s or "").encode("utf-8")).hexdigest()[:12]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# 生效条件:对任意 cg,返回 os.path.join(cg.root, LOG_NAME)(即 cg.root 与模块级常量 LOG_NAME 的拼接)。
|
|
172
|
+
def _log_path(cg) -> str:
|
|
173
|
+
return os.path.join(cg.root, LOG_NAME)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# 生效条件:x 为 None 或非 str 时原样返回 x;x 为 str 时返回 MdCGOS(x) 构造出的 cg 实例。
|
|
177
|
+
def _as_cg(x):
|
|
178
|
+
"""接受 root 路径或已构造的 cg 实例——保持密级隔离与密钥上下文。"""
|
|
179
|
+
if x is None or not isinstance(x, str):
|
|
180
|
+
return x
|
|
181
|
+
from .mdcos import MdCGOS
|
|
182
|
+
return MdCGOS(x)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# 生效条件:委托 nodefile.ccg_mark_present——content 中含标题行 "# " + field_name(冒号可有可无)则返回 True,否则 False。
|
|
186
|
+
def _has_ccg_line(content: str, field_name: str) -> bool:
|
|
187
|
+
# 判据单点在 nodefile(2026-09-28 收口径):冒号可有可无,与写入闸门同一语义。
|
|
188
|
+
return nodefile.ccg_mark_present(content, field_name)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# 生效条件:委托 nodefile.ccg_value_has_break——v 的 str 形态含换行("\n"/"\r")则返回 True,否则 False。ccg 行值必须单行,换行会拆出伪造的独立正文行。
|
|
192
|
+
def _has_line_break(v) -> bool:
|
|
193
|
+
return nodefile.ccg_value_has_break(v)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# 生效条件:在 `(content or "").split("\n")` 中命中首个 strip 后以 "#" 开头、含 field_name、且去 "#" 后按全角或半角冒号切出的名字等于 field_name 的行→替换为 "# field_name:value" 并返回;否则若有行 strip 后以 "# 功能名" 开头→在该行后插入新行并返回;否则返回 `"# field_name:value\n" + (content or "")`;value 含换行时抛 ValueError(fail-closed:换行会把值拆成伪造的独立正文行,正文读面 ccg_field_value 取首个命中行 ⇒ 被顶替)。
|
|
197
|
+
def _upsert_ccg_line(content: str, field_name: str, value: str) -> str:
|
|
198
|
+
"""写入/替换 `# <字段>:<值>`,优先插在「# 功能名」之后。
|
|
199
|
+
|
|
200
|
+
与 consolidate._upsert_ccg_line 同款语义(就近实现,避免 import 环)。
|
|
201
|
+
唯一加严:**值必须是单行**(N208,2026-09-28)——此前 `value` 原样拼进
|
|
202
|
+
`"# " + field_name + ":" + value`,含 `\\n` 的值即可注入一行独立的
|
|
203
|
+
`# <任意字段>:<任意值>`:`ccg_field_value` 取首个命中行,正文读面据此
|
|
204
|
+
被顶替(伪造「功能名/生效条件」而调用方以为自己只改了四槽)。
|
|
205
|
+
"""
|
|
206
|
+
if _has_line_break(value):
|
|
207
|
+
raise ValueError(
|
|
208
|
+
"ccg 行值含换行:%r——值必须是单行(换行会注入伪造的独立正文行,"
|
|
209
|
+
"正文读面按首个命中行取值即被顶替)。N208 fail-closed 拒写。"
|
|
210
|
+
% (str(value)[:80],))
|
|
211
|
+
lines = (content or "").split("\n")
|
|
212
|
+
for i, ln in enumerate(lines):
|
|
213
|
+
s = ln.strip()
|
|
214
|
+
if not s.startswith("#") or field_name not in s:
|
|
215
|
+
continue
|
|
216
|
+
name = s.lstrip("#").strip().split(":")[0].split(":")[0].strip()
|
|
217
|
+
if name == field_name:
|
|
218
|
+
lines[i] = "# " + field_name + ":" + value
|
|
219
|
+
return "\n".join(lines)
|
|
220
|
+
newline = "# " + field_name + ":" + value
|
|
221
|
+
for i, ln in enumerate(lines):
|
|
222
|
+
if ln.strip().startswith("# 功能名"):
|
|
223
|
+
lines.insert(i + 1, newline)
|
|
224
|
+
return "\n".join(lines)
|
|
225
|
+
return newline + "\n" + (content or "")
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# 生效条件:对任意 path 与 rec,以追加模式写入 json.dumps(rec, ensure_ascii=False) + "\n",仅 OSError 被吞掉且无返回值。
|
|
229
|
+
def _append_jsonl(path: str, rec: dict) -> None:
|
|
230
|
+
try:
|
|
231
|
+
with open(path, "a", encoding="utf-8") as f:
|
|
232
|
+
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
|
|
233
|
+
except OSError:
|
|
234
|
+
pass
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# 生效条件:path 为假值或 os.path.exists(path) 为假→返回 [];否则逐行 strip、跳过空行、json.loads 成功者追加、单行 json.loads 抛 ValueError 者跳过,中途 open/读取抛 OSError 时返回 [];全部读完返回 out。
|
|
238
|
+
def _read_jsonl(path: str) -> List[dict]:
|
|
239
|
+
out: List[dict] = []
|
|
240
|
+
if not path or not os.path.exists(path):
|
|
241
|
+
return out
|
|
242
|
+
try:
|
|
243
|
+
with open(path, encoding="utf-8") as f:
|
|
244
|
+
for ln in f:
|
|
245
|
+
ln = ln.strip()
|
|
246
|
+
if not ln:
|
|
247
|
+
continue
|
|
248
|
+
try:
|
|
249
|
+
out.append(json.loads(ln))
|
|
250
|
+
except ValueError:
|
|
251
|
+
continue
|
|
252
|
+
except OSError:
|
|
253
|
+
return []
|
|
254
|
+
return out
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# =============================================================================
|
|
258
|
+
# 名实门(幻觉的编译期机械检出)
|
|
259
|
+
# =============================================================================
|
|
260
|
+
|
|
261
|
+
#: 值分片分隔符——值可为多句枚举,但**每个分片**都必须是原文子串
|
|
262
|
+
_FRAG_SPLIT = re.compile(r"[;;、,,]+")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# 生效条件:value 经 `str(value).strip()`(None 记 "")后为空→False;否则按 _FRAG_SPLIT 切分并去掉空分片,若分片列表为空→False;否则返回全部分片都在 dialog 中的 all 判定结果。
|
|
266
|
+
def _value_grounded(dialog: str, value) -> bool:
|
|
267
|
+
"""值是否 grounded:按分片切分后,每个分片都是对话记录的字面子串。
|
|
268
|
+
|
|
269
|
+
分片口径(而非整串口径)是为容纳「多句枚举」这一合法形态(`# 子功能:A;B`);
|
|
270
|
+
但**任一分片不在原文即判捏造**——改写、拼接、概括一律被挡在门外。
|
|
271
|
+
"""
|
|
272
|
+
s = "" if value is None else str(value).strip()
|
|
273
|
+
if not s:
|
|
274
|
+
return False
|
|
275
|
+
frags = [f.strip() for f in _FRAG_SPLIT.split(s) if f.strip()]
|
|
276
|
+
if not frags:
|
|
277
|
+
return False
|
|
278
|
+
return all(f in dialog for f in frags)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# 生效条件:span 经 `str(span).strip()`(None 记 "")后须非空且作为连续子串出现在 dialog 中,否则返回 False。
|
|
282
|
+
def _span_grounded(dialog: str, span) -> bool:
|
|
283
|
+
"""引用 span 是否 grounded:必须是对话记录的**连续**字面子串。"""
|
|
284
|
+
s = "" if span is None else str(span).strip()
|
|
285
|
+
return bool(s) and s in dialog
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# =============================================================================
|
|
289
|
+
# 候选来源:显式入参 / 外部 parser / 内生规则解析器
|
|
290
|
+
# =============================================================================
|
|
291
|
+
|
|
292
|
+
#: 内生规则解析器的线索词(确定性、可审计;不做语义推断)
|
|
293
|
+
_CUES = {
|
|
294
|
+
"observation_position": ("位于", "在本地", "在服务", "在文件", "在系统",
|
|
295
|
+
"端上", "本地", "文件系统", "服务器"),
|
|
296
|
+
"observation_tool": ("使用", "采用", "通过", "调用", "借助", "依托", "方法"),
|
|
297
|
+
"existence_constraint": ("仅", "只", "前提", "除非", "限于", "必须是", "应当"),
|
|
298
|
+
}
|
|
299
|
+
_NEG_CUES = ("不适用", "不处理", "除外", "例外", "排除")
|
|
300
|
+
_EXEC_CUES = ("先", "再", "然后", "依次", "步骤", "读", "调用", "执行")
|
|
301
|
+
_SUB_CUES = ("分", "包含", "由", "组成", "包括")
|
|
302
|
+
#: 验证基底线索词 → VERIFICATION_BASIS 枚举
|
|
303
|
+
_BASIS_CUES = (
|
|
304
|
+
("测试", "test"), ("实测", "test"), ("回放", "test"), ("断言", "test"),
|
|
305
|
+
("编译", "compiler"), ("测量", "measurement"), ("证明", "formal_proof"),
|
|
306
|
+
("统计", "data"), ("教材", "textbook"), ("公开知识", "public_kb"),
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
_SENT_SPLIT = re.compile(r"[。!?!?;;\n]+")
|
|
310
|
+
_TS_PAT = re.compile(r"(20\d{2})[-/年](\d{1,2})[-/月](\d{1,2})")
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# 生效条件:dialog 为假值时按 "" 处理,按模块级 _SENT_SPLIT 切分后 strip 并过滤空串,返回句子列表。
|
|
314
|
+
def _sentences(dialog: str) -> List[str]:
|
|
315
|
+
return [s.strip() for s in _SENT_SPLIT.split(dialog or "") if s.strip()]
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
# 生效条件:对 sents 逐句判 `any(c in s for c in cues)`,命中即 append,命中后 `len(out) >= limit` 即 break——limit 为 0 或负数时首个命中项仍被 append 后立刻 break(多返 1 项);cues 为空容器时 any 恒假、返回空列表。
|
|
319
|
+
def _pick_by_cues(sents: List[str], cues, limit: int = 1) -> List[str]:
|
|
320
|
+
"""按线索词命中挑句(命中即整句作为候选——整句天然是原文子串,不会捏造)。"""
|
|
321
|
+
out: List[str] = []
|
|
322
|
+
for s in sents:
|
|
323
|
+
if any(c in s for c in cues):
|
|
324
|
+
out.append(s)
|
|
325
|
+
if len(out) >= limit:
|
|
326
|
+
break
|
|
327
|
+
return out
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
# 生效条件:类无 __init__ 形参,实例化即成立,类属性 name 恒为模块级常量 SRC_RULE,候选能力经 candidates(dialog, ctx=None) 以 dialog 为必需入参调用;
|
|
331
|
+
class RuleParser:
|
|
332
|
+
"""内生规则解析器:**诚实下界能力**,不是主路径。
|
|
333
|
+
|
|
334
|
+
按线索词从对话记录整句摘取候选,产出天然 grounded(无幻觉可能),但覆盖率低:
|
|
335
|
+
四槽不齐即 E020 编译失败——这正是「缺能力时不假装通过」的既有纪律
|
|
336
|
+
(对齐 audit.py:未注入验证器恒 DEFER,绝不假装通过)。
|
|
337
|
+
真正的抽取能力由外部经 `parser=` 注入(LLM 等),能力外置、认知图只留接口。
|
|
338
|
+
"""
|
|
339
|
+
|
|
340
|
+
name = SRC_RULE
|
|
341
|
+
|
|
342
|
+
# 生效条件:sents(来自 _sentences(dialog))非空时「功能名」取首句(超 30 字符截前 30);pos/tool/cons 仅在 _pick_by_cues 用 _CUES 对应线索命中时写入对应槽;time_window 先按 _TS_PAT 在 dialog 中匹配,命中且 mktime 未抛 ValueError/OverflowError/OSError 才填 [当日0点, +86399],否则回落 [nodefile.FULL_TIME_WINDOW_MIN, nodefile.FULL_TIME_WINDOW_MAX] 并标 synthetic="full_time_window";「不适用条件/执行/子功能」按 _NEG_CUES/_EXEC_CUES/_SUB_CUES 命中首句写入;「验证方式」取 _BASIS_CUES 中首个出现在 dialog 中的线索。
|
|
343
|
+
def candidates(self, dialog: str, ctx: Optional[dict] = None) -> Dict[str, Any]:
|
|
344
|
+
sents = _sentences(dialog)
|
|
345
|
+
out: Dict[str, Any] = {"marks": {}, "slots": {}}
|
|
346
|
+
|
|
347
|
+
if sents:
|
|
348
|
+
head = sents[0]
|
|
349
|
+
name = head if len(head) <= 30 else head[:30]
|
|
350
|
+
out["marks"]["功能名"] = {"value": name, "span": name}
|
|
351
|
+
|
|
352
|
+
pos = _pick_by_cues(sents, _CUES["observation_position"])
|
|
353
|
+
tool = _pick_by_cues(sents, _CUES["observation_tool"])
|
|
354
|
+
cons = _pick_by_cues(sents, _CUES["existence_constraint"])
|
|
355
|
+
if pos:
|
|
356
|
+
out["slots"]["observation_position"] = {"value": pos[0], "span": pos[0]}
|
|
357
|
+
if tool:
|
|
358
|
+
out["slots"]["observation_tool"] = {"value": tool[0], "span": tool[0]}
|
|
359
|
+
if cons:
|
|
360
|
+
out["slots"]["existence_constraint"] = {"value": cons[0], "span": cons[0]}
|
|
361
|
+
|
|
362
|
+
# 时间槽:含 ISO 日期 → 该日全天(UTC);不含 → 全时窗。
|
|
363
|
+
# 二者都是**合法声明**(nodefile 已定义全时窗语义),非占位;
|
|
364
|
+
# basis 经 synthetic 标注,避免被误读为「已测得具体时刻」。
|
|
365
|
+
m = _TS_PAT.search(dialog or "")
|
|
366
|
+
if m:
|
|
367
|
+
try:
|
|
368
|
+
t = time.mktime((int(m.group(1)), int(m.group(2)), int(m.group(3)),
|
|
369
|
+
0, 0, 0, 0, 0, 0)) - time.timezone
|
|
370
|
+
out["slots"]["time_window"] = {"value": [t, t + 86399], "span": m.group(0)}
|
|
371
|
+
except (ValueError, OverflowError, OSError):
|
|
372
|
+
pass
|
|
373
|
+
if "time_window" not in out["slots"]:
|
|
374
|
+
out["slots"]["time_window"] = {
|
|
375
|
+
"value": [nodefile.FULL_TIME_WINDOW_MIN, nodefile.FULL_TIME_WINDOW_MAX],
|
|
376
|
+
"span": "", "synthetic": "full_time_window"}
|
|
377
|
+
|
|
378
|
+
for field_name, cues in (("不适用条件", _NEG_CUES), ("执行", _EXEC_CUES),
|
|
379
|
+
("子功能", _SUB_CUES)):
|
|
380
|
+
hit = _pick_by_cues(sents, cues)
|
|
381
|
+
if hit:
|
|
382
|
+
out["marks"][field_name] = {"value": hit[0], "span": hit[0]}
|
|
383
|
+
for cue, basis in _BASIS_CUES:
|
|
384
|
+
if cue in (dialog or ""):
|
|
385
|
+
out["marks"]["验证方式"] = {"value": cue, "span": cue, "basis": basis}
|
|
386
|
+
break
|
|
387
|
+
return out
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
# 生效条件:对给定 parser、dialog、ctx,getattr(parser,"candidates",parser) 得到的 fn 被调用为 fn(dialog, ctx or {}),若该调用抛 Exception 或返回非 dict 则返回 {"marks": {}, "slots": {}},否则返回该 dict 并 setdefault("marks",{}) 与 setdefault("slots",{}) 后的结果。
|
|
391
|
+
def _invoke_parser(parser, dialog: str, ctx: Optional[dict]) -> Dict[str, Any]:
|
|
392
|
+
"""调用外部/内生解析器,统一形态;异常不视作通过(返回空候选)。"""
|
|
393
|
+
fn = getattr(parser, "candidates", parser)
|
|
394
|
+
try:
|
|
395
|
+
out = fn(dialog, ctx or {})
|
|
396
|
+
except Exception: # noqa: BLE001 —— 解析器异常 = 无候选,不冒充成功
|
|
397
|
+
return {"marks": {}, "slots": {}}
|
|
398
|
+
if not isinstance(out, dict):
|
|
399
|
+
return {"marks": {}, "slots": {}}
|
|
400
|
+
out.setdefault("marks", {})
|
|
401
|
+
out.setdefault("slots", {})
|
|
402
|
+
return out
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
# =============================================================================
|
|
406
|
+
# 核心:compile_dialog(对话记录 → 六要素候选;只编译,不入库)
|
|
407
|
+
# =============================================================================
|
|
408
|
+
|
|
409
|
+
# 生效条件:dialog 空记 E001、node_id 空记 E002、actor 空记 E003,且 cg 转换后非 None 而 node_id 非空时该节点不存在再记 E002,存在任一 errors 即以 verdict{passed:False, reason:errors[0], authority:VERIFICATION_UNIT} 提前返回 res;否则按「marks/slots 任一为非空 dict → SRC_EXPLICIT;二者皆空且 parser 为 None → SRC_RULE(附下界告警);二者皆空且 parser 非 None → SRC_PARSER」收集候选,四槽逐个校验(key 不在 cand_slots 即跳过;time_window 非长度 2 的 list/tuple 记 E021;其余槽空值/占位记 E021;仅当非「src_kind==SRC_EXPLICIT 且 strict_spans=False」豁免时,值非 dialog 子串记 E011、span 非空而定位失败记 E010),clean_slots 缺必需槽或 condition_space_text 为空记 E020;五要素按同样规则过滤后入 res.lines(生效条件行只由四槽 env_text 合成),验证基底按「候选 basis → 验证方式命中 _BASIS_CUES → 默认 other 并附告警」取值、不在 nodefile.VERIFICATION_BASIS 中记 E030;最终 res.success 与 verdict.passed 同取「无 errors」,verdict 恒带 requires_attestation=True,通过时再按 nodefile.CCG_REQUIRED 缺失项补 warning;
|
|
410
|
+
def compile_dialog(dialog: str, node_id: str, actor: str, *,
|
|
411
|
+
marks: Optional[Dict[str, Any]] = None,
|
|
412
|
+
slots: Optional[Dict[str, Any]] = None,
|
|
413
|
+
parser: Any = None,
|
|
414
|
+
strict_spans: bool = True,
|
|
415
|
+
cg: Any = None) -> CompileResult:
|
|
416
|
+
"""按五环编译:前端契约检查 → 候选 → 名实门 → 四槽合成 → 生成六行 → 终裁。
|
|
417
|
+
|
|
418
|
+
入参优先级:显式 `marks`/`slots` > 外部 `parser=` > 内生 `RuleParser`。
|
|
419
|
+
显式入参在 `strict_spans=True`(默认)时**同样**过名实门——人要写条件,
|
|
420
|
+
也须拿得出对话原文依据;确需写入对话之外的声明时显式传 `strict_spans=False`,
|
|
421
|
+
该豁免计入 warnings 与留痕(可审计,不静默)。
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
dialog: 对话记录原文(编译源)
|
|
425
|
+
node_id: 目标节点 id
|
|
426
|
+
actor: 编译执行者标识(供 attest 的 E041 比对,必填)
|
|
427
|
+
marks: 显式五要素(功能名/子功能/执行/验证方式/不适用条件)
|
|
428
|
+
slots: 显式 condition_space 四槽
|
|
429
|
+
parser: 外部候选生成器(对象含 candidates(dialog, ctx),或直接可调用)
|
|
430
|
+
strict_spans: 显式入参是否也过名实门(默认 True)
|
|
431
|
+
cg: 可选 cg 实例/root——提供时校验节点存在性
|
|
432
|
+
"""
|
|
433
|
+
res = CompileResult(node_id=str(node_id or "").strip(),
|
|
434
|
+
actor=str(actor or "").strip())
|
|
435
|
+
|
|
436
|
+
# ---- 第 0 环:前端契约检查(缺参即编译错误,裁定 B) ----
|
|
437
|
+
if not (dialog or "").strip():
|
|
438
|
+
res.err("E001")
|
|
439
|
+
if not res.node_id:
|
|
440
|
+
res.err("E002")
|
|
441
|
+
if not res.actor:
|
|
442
|
+
res.err("E003")
|
|
443
|
+
_cg = _as_cg(cg)
|
|
444
|
+
if _cg is not None and res.node_id:
|
|
445
|
+
if not ((_cg.index.get("nodes") or {}).get(res.node_id)):
|
|
446
|
+
res.err("E002", "节点不存在:" + res.node_id)
|
|
447
|
+
if res.errors:
|
|
448
|
+
res.verdict = {"passed": False, "reason": res.errors[0],
|
|
449
|
+
"authority": "VERIFICATION_UNIT"}
|
|
450
|
+
return res
|
|
451
|
+
|
|
452
|
+
src = str(dialog)
|
|
453
|
+
ctx = {"cg": _cg, "actor": res.actor, "node_id": res.node_id}
|
|
454
|
+
|
|
455
|
+
# ---- 第 1 环:收集候选(显式 > 外部 parser > 内生 rule) ----
|
|
456
|
+
cand_marks: Dict[str, Any] = {}
|
|
457
|
+
cand_slots: Dict[str, Any] = {}
|
|
458
|
+
if marks or slots:
|
|
459
|
+
src_kind = SRC_EXPLICIT
|
|
460
|
+
for k, v in (slots or {}).items():
|
|
461
|
+
cand_slots[k] = v if isinstance(v, dict) else {"value": v}
|
|
462
|
+
for k, v in (marks or {}).items():
|
|
463
|
+
cand_marks[k] = v if isinstance(v, dict) else {"value": v}
|
|
464
|
+
else:
|
|
465
|
+
if parser is None:
|
|
466
|
+
src_kind = SRC_RULE
|
|
467
|
+
res.warnings.append(
|
|
468
|
+
"未注入外部解析器(parser=)且无显式入参,退回内生规则解析器(下界能力);"
|
|
469
|
+
"四槽不齐将按 E020 编译失败——缺能力时不假装通过。")
|
|
470
|
+
else:
|
|
471
|
+
src_kind = SRC_PARSER
|
|
472
|
+
res.warnings.append(
|
|
473
|
+
"外部解析器产物已过名实门(E010/E011):任一值非对话记录字面子串即编译失败。")
|
|
474
|
+
got = _invoke_parser(parser if parser is not None else RuleParser(), src, ctx)
|
|
475
|
+
cand_marks = dict(got.get("marks") or {})
|
|
476
|
+
cand_slots = dict(got.get("slots") or {})
|
|
477
|
+
|
|
478
|
+
# 显式入参在 strict_spans=True 时同样过门;豁免须显式声明且留痕
|
|
479
|
+
exempt = (src_kind == SRC_EXPLICIT and not strict_spans)
|
|
480
|
+
if exempt:
|
|
481
|
+
res.warnings.append(
|
|
482
|
+
"strict_spans=False:显式入参豁免名实门,含未经对话原文支撑的声明(已留痕)。")
|
|
483
|
+
|
|
484
|
+
# 生效条件:item 为 dict 时返回 (item.get("value"), item.get("span"), item.get("basis"), item.get("synthetic"))(各键缺失即回落 None);item 非 dict 时返回 (item, None, None, None)。
|
|
485
|
+
def _field(item):
|
|
486
|
+
"""候选项 → (value, span, basis, synthetic);容忍裸值与 dict 两种形态。"""
|
|
487
|
+
if isinstance(item, dict):
|
|
488
|
+
return (item.get("value"), item.get("span"),
|
|
489
|
+
item.get("basis"), item.get("synthetic"))
|
|
490
|
+
return (item, None, None, None)
|
|
491
|
+
|
|
492
|
+
# ---- 第 2 环:四槽校验 + 生效条件合成(唯一入口 nodefile.condition_space_text) ----
|
|
493
|
+
clean_slots: Dict[str, Any] = {}
|
|
494
|
+
for key, _label in nodefile.CONDITION_SLOTS:
|
|
495
|
+
if key not in cand_slots:
|
|
496
|
+
continue
|
|
497
|
+
value, span, _basis, synthetic = _field(cand_slots[key])
|
|
498
|
+
if key == "time_window":
|
|
499
|
+
# 全时窗哨兵是合法声明;其余须为 [lo, hi] 数值对
|
|
500
|
+
if not (isinstance(value, (list, tuple)) and len(value) == 2):
|
|
501
|
+
res.err("E021", "槽 time_window 非 [lo, hi]:" + repr(value))
|
|
502
|
+
continue
|
|
503
|
+
else:
|
|
504
|
+
if nodefile.is_placeholder_text(value):
|
|
505
|
+
res.err("E021", "槽 " + key + " 空值或待填充占位:" + repr(value))
|
|
506
|
+
continue
|
|
507
|
+
# N208:槽值是单行契约——换行会在合成出的生效条件行里拆出伪造行
|
|
508
|
+
if _has_line_break(value):
|
|
509
|
+
res.err("E022", "槽 " + key + " 值含换行:" + repr(value)[:60])
|
|
510
|
+
continue
|
|
511
|
+
if not exempt and not _value_grounded(src, value):
|
|
512
|
+
res.err("E011", "槽 " + key + " 值非原文子串:" + repr(value))
|
|
513
|
+
res.ungrounded.append({"field": "condition_space." + key,
|
|
514
|
+
"span": str(span), "value": str(value)})
|
|
515
|
+
continue
|
|
516
|
+
if not exempt and span and not _span_grounded(src, span):
|
|
517
|
+
res.err("E010", "槽 " + key + " 引用定位失败:" + repr(span))
|
|
518
|
+
res.ungrounded.append({"field": "condition_space." + key,
|
|
519
|
+
"span": str(span), "value": str(value)})
|
|
520
|
+
continue
|
|
521
|
+
clean_slots[key] = value
|
|
522
|
+
span_list = res.spans.setdefault("condition_space." + key, [])
|
|
523
|
+
if span:
|
|
524
|
+
span_list.append(str(span))
|
|
525
|
+
if synthetic:
|
|
526
|
+
res.sources["condition_space." + key] = synthetic
|
|
527
|
+
|
|
528
|
+
missing = nodefile.condition_space_missing(clean_slots)
|
|
529
|
+
env_text = nodefile.condition_space_text(clean_slots)
|
|
530
|
+
if missing or not env_text:
|
|
531
|
+
res.err("E020", "缺槽 " + ",".join(missing or list(nodefile.CONDITION_SLOTS_REQUIRED)))
|
|
532
|
+
res.slots = clean_slots
|
|
533
|
+
|
|
534
|
+
# ---- 第 3 环:五要素过名实门 ----
|
|
535
|
+
clean_marks: Dict[str, str] = {}
|
|
536
|
+
basis_from_cand = None
|
|
537
|
+
for field_name in _CAND_MARKS:
|
|
538
|
+
if field_name not in cand_marks:
|
|
539
|
+
continue
|
|
540
|
+
value, span, basis, _synthetic = _field(cand_marks[field_name])
|
|
541
|
+
if nodefile.is_placeholder_text(value):
|
|
542
|
+
res.err("E021", "要素 " + field_name + " 空值或待填充占位:" + repr(value))
|
|
543
|
+
continue
|
|
544
|
+
# N208:要素行同样是单行契约(link 会把它拼成 `# <要素>:<值>` 一行)
|
|
545
|
+
if _has_line_break(value):
|
|
546
|
+
res.err("E022", "要素 " + field_name + " 值含换行:" + repr(value)[:60])
|
|
547
|
+
continue
|
|
548
|
+
if not exempt:
|
|
549
|
+
if not _value_grounded(src, value):
|
|
550
|
+
res.err("E011", "要素 " + field_name + " 值非原文子串:" + repr(value))
|
|
551
|
+
res.ungrounded.append({"field": field_name, "span": str(span),
|
|
552
|
+
"value": str(value)})
|
|
553
|
+
continue
|
|
554
|
+
if span and not _span_grounded(src, span):
|
|
555
|
+
res.err("E010", "要素 " + field_name + " 引用定位失败:" + repr(span))
|
|
556
|
+
res.ungrounded.append({"field": field_name, "span": str(span),
|
|
557
|
+
"value": str(value)})
|
|
558
|
+
continue
|
|
559
|
+
clean_marks[field_name] = str(value).strip()
|
|
560
|
+
res.sources[field_name] = src_kind
|
|
561
|
+
span_list = res.spans.setdefault(field_name, [])
|
|
562
|
+
if span:
|
|
563
|
+
span_list.append(str(span))
|
|
564
|
+
if field_name == "验证方式" and basis:
|
|
565
|
+
basis_from_cand = basis
|
|
566
|
+
res.sources["_src_kind"] = src_kind
|
|
567
|
+
|
|
568
|
+
# ---- 第 4 环:验证基底(显式 basis > 文本线索词 > other) ----
|
|
569
|
+
basis_val = basis_from_cand
|
|
570
|
+
if basis_val is None and clean_marks.get("验证方式"):
|
|
571
|
+
for cue, enum_val in _BASIS_CUES:
|
|
572
|
+
if cue in clean_marks["验证方式"]:
|
|
573
|
+
basis_val = enum_val
|
|
574
|
+
break
|
|
575
|
+
if basis_val is None:
|
|
576
|
+
basis_val = "other"
|
|
577
|
+
res.warnings.append("未声明验证基底,落默认 other(不得据以宣称已实测)。")
|
|
578
|
+
if basis_val not in nodefile.VERIFICATION_BASIS:
|
|
579
|
+
res.err("E030", repr(basis_val))
|
|
580
|
+
res.sources["verification_basis"] = basis_val
|
|
581
|
+
|
|
582
|
+
# ---- 第 5 环:生成六行(生效条件**只能**由四槽合成,无第二通路) ----
|
|
583
|
+
lines: Dict[str, str] = {}
|
|
584
|
+
if clean_marks.get("功能名"):
|
|
585
|
+
lines["功能名"] = clean_marks["功能名"]
|
|
586
|
+
if env_text:
|
|
587
|
+
lines["生效条件"] = env_text
|
|
588
|
+
for k in ("子功能", "执行", "验证方式", "不适用条件"):
|
|
589
|
+
if clean_marks.get(k):
|
|
590
|
+
lines[k] = clean_marks[k]
|
|
591
|
+
res.lines = lines
|
|
592
|
+
|
|
593
|
+
# ---- 第 6 环:验证单元终裁(编译器自证部分;签章由 attest 独立完成) ----
|
|
594
|
+
solid = not res.errors
|
|
595
|
+
res.verdict = {
|
|
596
|
+
"passed": solid,
|
|
597
|
+
"reason": ("候选可提交外部验证:六要素齐备且全部 grounded" if solid
|
|
598
|
+
else (res.errors[0] if res.errors else "未知")),
|
|
599
|
+
"authority": "VERIFICATION_UNIT",
|
|
600
|
+
"requires_attestation": True, # 裁定 A:编译通过 ≠ 可写入,须外部签章
|
|
601
|
+
}
|
|
602
|
+
if solid:
|
|
603
|
+
absent = [m for m in nodefile.CCG_REQUIRED if m not in lines]
|
|
604
|
+
if absent:
|
|
605
|
+
res.warnings.append("六要素尚缺:" + ",".join(absent) + "(link 前须补)")
|
|
606
|
+
res.success = solid
|
|
607
|
+
return res
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
# =============================================================================
|
|
611
|
+
# attest:认知图**外**的验证方签章(裁定 A 的落点)
|
|
612
|
+
# =============================================================================
|
|
613
|
+
|
|
614
|
+
# 生效条件:a/b 任一传入值归一化(strip→去尾部括号注记→去全部空白→casefold)后为空即返回 False;否则返回两归一化结果是否相等。归一口径:尾部括号注记(如 "agent-A(复核)")视为修饰后缀剥离;中间空白全去("agent A"=="agent-A" 变体不在此列——连字符不做等价,避免过度归一误合不同主体);
|
|
615
|
+
def _same_subject(a, b) -> bool:
|
|
616
|
+
"""身份归一比较(issue #27,2026-09-23)。
|
|
617
|
+
|
|
618
|
+
原先三处 E041 判据是裸 `==`——`agent-A` vs `agent-a` / `AGENT-A` /
|
|
619
|
+
`agent-A(复核)` 全部绕过(外部报告实测 WRITTEN=6 落盘成功)。
|
|
620
|
+
归一化拦截**已知同源形态**;根本保障是 attest 的 verifier_token
|
|
621
|
+
凭据通路(身份经令牌 HMAC 验签,不依赖自报诚实度)。
|
|
622
|
+
"""
|
|
623
|
+
def _norm(x):
|
|
624
|
+
x = str(x or "").strip()
|
|
625
|
+
x = re.sub(r"[((][^()()]*[))]\s*$", "", x) # 尾部括号注记
|
|
626
|
+
return re.sub(r"\s+", "", x).casefold()
|
|
627
|
+
a_n, b_n = _norm(a), _norm(b)
|
|
628
|
+
return bool(a_n) and a_n == b_n
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
# 生效条件:verifier_token 非空时先经 tokens.verify_token 验签(失败→E041 前置错误 fail-closed),以令牌 actor 覆盖 res.verifier 并标 verifier_identity="token",否则标 "self-reported";随后依次判 strip 后的 node_id 为空→E002;verifier 为空→E003;_same_subject(verifier, compiled_by)→E041;state(verdict.strip())不在模块级 STATES→非法裁决;state 非 ACCEPT→E042;全部通过才 res.ok=True;随后恒算 token=_sha(...),仅当 ledger 为真且 _as_cg(cg) 非 None 时追加一条 ccgc_attest 审计 jsonl(含 verifier_identity)。
|
|
632
|
+
def attest(node_id: str, verdict: str, verifier: str, compiled_by: str, *,
|
|
633
|
+
slot_corrections: Optional[Dict[str, Any]] = None,
|
|
634
|
+
evidence: str = "", cg: Any = None, ledger: bool = True,
|
|
635
|
+
verifier_token: str = "") -> AttestResult:
|
|
636
|
+
"""验证方签章。**验证方必须是编译方之外的一方**(子代理 / 设计者 / 用户)。
|
|
637
|
+
|
|
638
|
+
E041 机械拒绝「验证方 == 编译执行者」——LLM 不得自己验证自己。
|
|
639
|
+
阻断强度如实分层(issue #27):
|
|
640
|
+
· verifier_token 提供时:身份经 mdcg 令牌 HMAC 验签(不可伪造),
|
|
641
|
+
E041 比较令牌 principal.actor 与 compiled_by——结构性阻断;
|
|
642
|
+
· 未提供时:回退自报字符串 + 归一化比较(大小写/空白/括号注记
|
|
643
|
+
归一),拦截已知同源变体;verifier_identity="self-reported"
|
|
644
|
+
如实标注,下游已兑现消费——link(apply=true) 据此拒绝落库
|
|
645
|
+
(E052,N176),自报名无法冒充编外复核。
|
|
646
|
+
DEFER 不构成签章(未定 = 未通过)。
|
|
647
|
+
"""
|
|
648
|
+
res = AttestResult(node_id=str(node_id or "").strip(),
|
|
649
|
+
verifier=str(verifier or "").strip(),
|
|
650
|
+
compiled_by=str(compiled_by or "").strip(),
|
|
651
|
+
state=str(verdict or "").strip(),
|
|
652
|
+
evidence=str(evidence or ""),
|
|
653
|
+
slot_corrections=dict(slot_corrections or {}),
|
|
654
|
+
ts=time.time())
|
|
655
|
+
if str(verifier_token or "").strip():
|
|
656
|
+
try:
|
|
657
|
+
from . import tokens as _tokens
|
|
658
|
+
_p = _tokens.verify_token(str(verifier_token).strip())
|
|
659
|
+
res.verifier = str(getattr(_p, "actor", "") or "").strip()
|
|
660
|
+
res.verifier_identity = "token"
|
|
661
|
+
except Exception as exc: # noqa: BLE001 —— fail-closed:验签失败即拒绝
|
|
662
|
+
res.error = ("E041 前置失败:verifier 令牌验证失败("
|
|
663
|
+
+ type(exc).__name__ + ")——验证方身份必须可凭据化,"
|
|
664
|
+
"伪造令牌与无令牌同样不予放行")
|
|
665
|
+
res.token = _sha("|".join([res.node_id, res.verifier, res.state,
|
|
666
|
+
str(res.ts), res.evidence]))
|
|
667
|
+
_cg = _as_cg(cg)
|
|
668
|
+
if ledger and _cg is not None:
|
|
669
|
+
_append_jsonl(_log_path(_cg), {
|
|
670
|
+
"action": "ccgc_attest", "ts": res.ts, "node": res.node_id,
|
|
671
|
+
"verifier": res.verifier, "compiled_by": res.compiled_by,
|
|
672
|
+
"state": res.state, "ok": False, "token": res.token,
|
|
673
|
+
"verifier_identity": res.verifier_identity,
|
|
674
|
+
"evidence": res.evidence,
|
|
675
|
+
"slot_corrections": res.slot_corrections,
|
|
676
|
+
"error": res.error})
|
|
677
|
+
return res
|
|
678
|
+
if not res.node_id:
|
|
679
|
+
res.error = "E002 " + E_CODES["E002"]
|
|
680
|
+
elif not res.verifier:
|
|
681
|
+
res.error = "E003 缺少验证方标识(verifier)"
|
|
682
|
+
elif _same_subject(res.verifier, res.compiled_by):
|
|
683
|
+
res.error = "E041 " + E_CODES["E041"] + "(verifier=" + res.verifier + ")"
|
|
684
|
+
elif res.state not in STATES:
|
|
685
|
+
res.error = ("非法裁决:" + repr(res.state) + "(可选 " + ",".join(STATES) + ")")
|
|
686
|
+
elif res.state != ACCEPT:
|
|
687
|
+
res.error = "E042 " + E_CODES["E042"] + "(state=" + res.state + ")"
|
|
688
|
+
else:
|
|
689
|
+
res.ok = True
|
|
690
|
+
res.token = _sha("|".join([res.node_id, res.verifier, res.state,
|
|
691
|
+
str(res.ts), res.evidence]))
|
|
692
|
+
if ledger:
|
|
693
|
+
_cg = _as_cg(cg)
|
|
694
|
+
if _cg is not None:
|
|
695
|
+
_append_jsonl(_log_path(_cg), {
|
|
696
|
+
"action": "ccgc_attest", "ts": res.ts, "node": res.node_id,
|
|
697
|
+
"verifier": res.verifier, "compiled_by": res.compiled_by,
|
|
698
|
+
"state": res.state, "ok": res.ok, "token": res.token,
|
|
699
|
+
"verifier_identity": res.verifier_identity,
|
|
700
|
+
"evidence": res.evidence,
|
|
701
|
+
"slot_corrections": res.slot_corrections, "error": res.error})
|
|
702
|
+
return res
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
# =============================================================================
|
|
706
|
+
# link:签章通过才写入(缺签章恒不写入)
|
|
707
|
+
# =============================================================================
|
|
708
|
+
|
|
709
|
+
# 生效条件:依次判 compiled.success 为假→返回带错误;attestation 为 None→E040;attestation.node_id 不等于 compiled.node_id 的取值→目标不一致拒绝;attestation.verifier 为真值且 == `(actor or compiled.actor)`→E041;attestation.ok 为假→E042;_as_cg(cg) 为 None→E002;节点不在 cg.index 的 nodes 中→E002;apply 为假→ok=True 的 dry-run 返回;否则 apply 为真时写入(fm 为 None 或 content 加密→E004),basis 为假值则回落 compiled.sources.get("verification_basis") 或 "other",成功后 out.written=len(compiled.lines)。
|
|
710
|
+
def _check_deps(_cg, node_id: str) -> List[str]:
|
|
711
|
+
"""依赖声明硬闸门 → 错误列表(E050 / E051)。**读面失败不误杀**(返回空即放行)。
|
|
712
|
+
|
|
713
|
+
契约口径:CCG「子功能」行的契约角色是**依赖 dependency**(nodefile 术语真源),
|
|
714
|
+
但该槽同时承载**自述子功能**(描述本单元内部构成)——判据以 `@<节点 id>` 显式
|
|
715
|
+
引用为界(`nodefile.declares_dependency`,收窄裁定 b):**显式声称依赖**才要求在
|
|
716
|
+
`depends_on` 给出可解析目标——否则「依赖」只剩散文,被依赖单元一旦变动,下游
|
|
717
|
+
无处可传(失效传播从源头断链)。自然语言自述不算声明(依赖不是必填元数据)。
|
|
718
|
+
二者齐全时目标必须真实存在:悬空依赖 = 声称依赖一个并不存在的地基。
|
|
719
|
+
"""
|
|
720
|
+
errs: List[str] = []
|
|
721
|
+
try:
|
|
722
|
+
node = _cg.get(node_id)
|
|
723
|
+
except Exception: # noqa: BLE001 —— 读面异常按「无声明」放行
|
|
724
|
+
return errs
|
|
725
|
+
if not node:
|
|
726
|
+
return errs
|
|
727
|
+
from . import trust as _trust
|
|
728
|
+
fm = node.get("frontmatter") or {}
|
|
729
|
+
deps = _trust.as_deps(fm.get(nodefile.DEPENDS_ON_FIELD))
|
|
730
|
+
if nodefile.declares_dependency(node.get("content") or "") and not deps:
|
|
731
|
+
errs.append("E050 " + E_CODES["E050"])
|
|
732
|
+
known = set((getattr(_cg, "index", None) or {}).get("nodes") or {})
|
|
733
|
+
missing = [d for d in deps if d not in known]
|
|
734
|
+
if missing:
|
|
735
|
+
errs.append("E051 " + E_CODES["E051"] + ":" + ",".join(missing[:5]))
|
|
736
|
+
return errs
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
# 生效条件:依次判 compiled.success 为假→返回带错误;attestation 为 None→E040;attestation.node_id 不等于 compiled.node_id 的取值→目标不一致拒绝;attestation.verifier 为真值且 == `(actor or compiled.actor)`→E041;attestation.ok 为假→E042;_as_cg(cg) 为 None→E002;节点不在 cg.index 的 nodes 中→E002;依赖声明闸门(E050/E051)不通过→拒绝写入;attestation.verifier_identity != "token" 时 apply 为真→E052 拒绝写入(dry-run 不拦,仅附 E052 预告 warning);apply 为假→ok=True 的 dry-run 返回;否则 apply 为真时写入(fm 为 None 或 content 加密→E004),basis 为假值则回落 compiled.sources.get("verification_basis") 或 "other",成功后 out.written=len(compiled.lines)。
|
|
740
|
+
def link(compiled: CompileResult, attestation: Optional[AttestResult], *,
|
|
741
|
+
cg: Any = None, apply: bool = False, actor: str = "",
|
|
742
|
+
basis: str = "") -> LinkResult:
|
|
743
|
+
"""把编译产物写入节点。**准入条件 = 有效签章**(裁定 A)。
|
|
744
|
+
|
|
745
|
+
E040 无签章 / E041 自证 / E042 未通过 / E052 无凭据签章(self-reported
|
|
746
|
+
不落库,N176 兑现下游消费)—— 任一命中即拒绝写入,
|
|
747
|
+
对齐 audit.py「缺能力返回 DEFER,绝不假装通过」。
|
|
748
|
+
"""
|
|
749
|
+
node_id = getattr(compiled, "node_id", "") or ""
|
|
750
|
+
out = LinkResult(node_id=node_id, dry_run=not apply,
|
|
751
|
+
entry_id=_sha("ccgc|" + node_id + "|" + time.strftime("%Y%m%d%H%M%S")))
|
|
752
|
+
if not compiled.success:
|
|
753
|
+
out.errors.append("编译未通过,拒绝写入:" +
|
|
754
|
+
(compiled.errors[0] if compiled.errors else "未知"))
|
|
755
|
+
return out
|
|
756
|
+
if attestation is None:
|
|
757
|
+
out.errors.append("E040 " + E_CODES["E040"])
|
|
758
|
+
return out
|
|
759
|
+
if attestation.node_id != node_id:
|
|
760
|
+
out.errors.append("签章与产物目标不一致:" + attestation.node_id + " != " + node_id)
|
|
761
|
+
return out
|
|
762
|
+
if attestation.verifier and _same_subject(attestation.verifier,
|
|
763
|
+
actor or compiled.actor):
|
|
764
|
+
out.errors.append("E041 " + E_CODES["E041"]
|
|
765
|
+
+ "(身份归一比较命中)")
|
|
766
|
+
return out
|
|
767
|
+
if not attestation.ok:
|
|
768
|
+
out.errors.append("E042 " + E_CODES["E042"] +
|
|
769
|
+
(("(" + attestation.error + ")") if attestation.error else ""))
|
|
770
|
+
return out
|
|
771
|
+
|
|
772
|
+
_cg = _as_cg(cg)
|
|
773
|
+
if _cg is None:
|
|
774
|
+
out.errors.append("E002 " + E_CODES["E002"] + "(未提供 cg,无法写入)")
|
|
775
|
+
return out
|
|
776
|
+
entry = (_cg.index.get("nodes") or {}).get(node_id)
|
|
777
|
+
if not entry:
|
|
778
|
+
out.errors.append("E002 节点不存在:" + node_id)
|
|
779
|
+
return out
|
|
780
|
+
_dep_errs = _check_deps(_cg, node_id)
|
|
781
|
+
if _dep_errs:
|
|
782
|
+
out.errors.extend(_dep_errs)
|
|
783
|
+
return out
|
|
784
|
+
# N176:兑现 verifier_identity 的下游消费——无令牌签章(验证方身份纯自报,
|
|
785
|
+
# E041 只拦同源变体,编造他名即可冒充编外复核)不构成 knowledge 层落库准入;
|
|
786
|
+
# dry-run 不拦,附 E052 预告供调用方提前换令牌验证方。fail-closed:字段缺失
|
|
787
|
+
# 或未知取值一律按无凭据处理。
|
|
788
|
+
if getattr(attestation, "verifier_identity", "") != "token":
|
|
789
|
+
if not apply:
|
|
790
|
+
out.warnings.append("E052 预告:签章为 self-reported(无令牌验签)"
|
|
791
|
+
"——apply=true 将拒绝写入,请换令牌验证方复核")
|
|
792
|
+
else:
|
|
793
|
+
out.errors.append("E052 " + E_CODES["E052"]
|
|
794
|
+
+ "(verifier=" + attestation.verifier + ")")
|
|
795
|
+
return out
|
|
796
|
+
if not apply:
|
|
797
|
+
out.ok = True
|
|
798
|
+
out.warnings.append("dry-run(apply=False):未写入;签章 token=" + attestation.token)
|
|
799
|
+
return out
|
|
800
|
+
|
|
801
|
+
from . import crypto
|
|
802
|
+
fm, content = direct_read(_cg, entry) # 写前重查走盘上真值(读缓存口径)
|
|
803
|
+
if fm is None or crypto.is_encrypted(content):
|
|
804
|
+
out.errors.append("E004 " + E_CODES["E004"])
|
|
805
|
+
return out
|
|
806
|
+
|
|
807
|
+
before = {"生效条件": (fm.get("state_attributes") or {}).get("comment", {}).get("生效条件")
|
|
808
|
+
if isinstance((fm.get("state_attributes") or {}).get("comment"), dict) else None}
|
|
809
|
+
new_content = content
|
|
810
|
+
for field_name, value in compiled.lines.items():
|
|
811
|
+
new_content = _upsert_ccg_line(new_content, field_name, value)
|
|
812
|
+
|
|
813
|
+
fm["condition_space"] = compiled.slots
|
|
814
|
+
fm["verification_basis"] = basis or compiled.sources.get("verification_basis") or "other"
|
|
815
|
+
if compiled.lines.get("不适用条件"):
|
|
816
|
+
fm["non_applicable_conditions"] = [
|
|
817
|
+
s.strip() for s in re.split(r"[;;]", compiled.lines["不适用条件"]) if s.strip()]
|
|
818
|
+
st = fm.get("state_attributes")
|
|
819
|
+
if not isinstance(st, dict):
|
|
820
|
+
st = {}
|
|
821
|
+
fm["state_attributes"] = st
|
|
822
|
+
comment = st.get("comment")
|
|
823
|
+
if not isinstance(comment, dict):
|
|
824
|
+
comment = {}
|
|
825
|
+
st["comment"] = comment
|
|
826
|
+
if compiled.lines.get("生效条件"):
|
|
827
|
+
comment["生效条件"] = compiled.lines["生效条件"]
|
|
828
|
+
|
|
829
|
+
# N208(2026-09-28):本面是**既有节点的覆写**(正文六行 + condition_space +
|
|
830
|
+
# state_attributes),原先直调 `_write_node` 零闸:与同层 `add` 待遇相反
|
|
831
|
+
# (层白名单不含该层的身份照样改得动),self/anchor/immutable 节点被无痕
|
|
832
|
+
# 覆写(无快照、无审计)。统一走 protect.guard_overwrite(层闸 + 保护闸,
|
|
833
|
+
# 与 N131 merge 面同序同错型)。
|
|
834
|
+
protect.guard_overwrite(_cg, node_id, layer=fm.get("layer"),
|
|
835
|
+
sensitivity=fm.get("sensitivity"),
|
|
836
|
+
actor=actor or compiled.actor)
|
|
837
|
+
_cg._write_node(node_id, os.path.join(_cg.root, entry["path"]), fm,
|
|
838
|
+
new_content, durable=True)
|
|
839
|
+
_append_jsonl(_log_path(_cg), {
|
|
840
|
+
"action": "ccgc_link", "ts": time.time(), "node": node_id,
|
|
841
|
+
"actor": actor or compiled.actor, "verifier": attestation.verifier,
|
|
842
|
+
"token": attestation.token, "entry_id": out.entry_id,
|
|
843
|
+
"fields": {k: {"after": v, "source": compiled.sources.get(k, "")}
|
|
844
|
+
for k, v in compiled.lines.items()},
|
|
845
|
+
"slots": compiled.slots, "before": before,
|
|
846
|
+
"content_hash_after": nodefile.content_hash(new_content)})
|
|
847
|
+
_cg.rebuild_index()
|
|
848
|
+
out.ok = True
|
|
849
|
+
out.written = len(compiled.lines)
|
|
850
|
+
return out
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
# =============================================================================
|
|
854
|
+
# recalibrate:用后续验证修正生效条件(裁定 A 的闭环)
|
|
855
|
+
# =============================================================================
|
|
856
|
+
|
|
857
|
+
# 生效条件:node_id 空→E002;verifier 为真值且==compiled_by→E041;corrections 为假值(空 dict/None)→E043;corrections 含不在模块级 nodefile.CONDITION_SLOTS 的键→E043;_as_cg(cg) 为 None→E002;节点不在 cg.index 的 nodes 中→E002;fm 为 None 或内容加密→E004;旧槽合并 corrections 后 condition_space_text 为空→E020;apply 为假→ok=True 的 dry-run 返回;否则写入并按 1 计 written。
|
|
858
|
+
def recalibrate(node_id: str, corrections: Dict[str, Any], verifier: str,
|
|
859
|
+
compiled_by: str, *, evidence: str = "", cg: Any = None,
|
|
860
|
+
apply: bool = False) -> LinkResult:
|
|
861
|
+
"""用验证方给出的**四槽修正**重合成生效条件(唯一入口重算,可回滚)。
|
|
862
|
+
|
|
863
|
+
E043:只允许修正 condition_space 四槽——生效条件行本身不是修正入口,
|
|
864
|
+
否则会绕开「四槽 → 合成」的唯一通路,退回到手写条件的老问题。
|
|
865
|
+
修正须由编译方**之外**的一方给出(E041)。
|
|
866
|
+
"""
|
|
867
|
+
out = LinkResult(node_id=str(node_id or "").strip(), dry_run=not apply,
|
|
868
|
+
entry_id=_sha("ccgc_recal|" + str(node_id) + "|" + str(time.time())))
|
|
869
|
+
if not out.node_id:
|
|
870
|
+
out.errors.append("E002 " + E_CODES["E002"])
|
|
871
|
+
return out
|
|
872
|
+
if verifier and _same_subject(verifier, compiled_by):
|
|
873
|
+
out.errors.append("E041 " + E_CODES["E041"] + "(身份归一比较命中)")
|
|
874
|
+
return out
|
|
875
|
+
if not corrections:
|
|
876
|
+
out.errors.append("E043 " + E_CODES["E043"] + "(未给出任何修正)")
|
|
877
|
+
return out
|
|
878
|
+
allowed = {k for k, _l in nodefile.CONDITION_SLOTS}
|
|
879
|
+
bad = [k for k in corrections if k not in allowed]
|
|
880
|
+
if bad:
|
|
881
|
+
out.errors.append("E043 " + E_CODES["E043"] + "(非法键 " + ",".join(bad) + ")")
|
|
882
|
+
return out
|
|
883
|
+
# N208(2026-09-28):四槽值是**单行契约**——换行会在合成出的
|
|
884
|
+
# `# 生效条件:…` 行里拆出一行伪造的独立正文行(`_upsert_ccg_line` 已拒,
|
|
885
|
+
# 这里在更早的入口给出结构化 E022,不留半改状态)。dry-run 同样拒:
|
|
886
|
+
# 「预演能过、apply 才炸」的口径分裂比早拒更坏。
|
|
887
|
+
lb = [k for k, v in corrections.items() if _has_line_break(v)]
|
|
888
|
+
if lb:
|
|
889
|
+
out.errors.append("E022 " + E_CODES["E022"] + "(槽 " + ",".join(lb) + ")")
|
|
890
|
+
return out
|
|
891
|
+
|
|
892
|
+
_cg = _as_cg(cg)
|
|
893
|
+
if _cg is None:
|
|
894
|
+
out.errors.append("E002 " + E_CODES["E002"] + "(未提供 cg)")
|
|
895
|
+
return out
|
|
896
|
+
entry = (_cg.index.get("nodes") or {}).get(out.node_id)
|
|
897
|
+
if not entry:
|
|
898
|
+
out.errors.append("E002 节点不存在:" + out.node_id)
|
|
899
|
+
return out
|
|
900
|
+
|
|
901
|
+
from . import crypto
|
|
902
|
+
fm, content = direct_read(_cg, entry) # 写前重查走盘上真值(读缓存口径)
|
|
903
|
+
if fm is None or crypto.is_encrypted(content):
|
|
904
|
+
out.errors.append("E004 " + E_CODES["E004"])
|
|
905
|
+
return out
|
|
906
|
+
old_cs = dict(fm.get("condition_space") or {})
|
|
907
|
+
new_cs = dict(old_cs)
|
|
908
|
+
new_cs.update(corrections)
|
|
909
|
+
new_text = nodefile.condition_space_text(new_cs)
|
|
910
|
+
if not new_text:
|
|
911
|
+
out.errors.append("E020 " + E_CODES["E020"] +
|
|
912
|
+
"(修正后仍缺 " + ",".join(nodefile.condition_space_missing(new_cs)) + ")")
|
|
913
|
+
return out
|
|
914
|
+
out.warnings.append("修正后生效条件:" + new_text)
|
|
915
|
+
if not apply:
|
|
916
|
+
out.ok = True
|
|
917
|
+
return out
|
|
918
|
+
|
|
919
|
+
new_content = _upsert_ccg_line(content, "生效条件", new_text)
|
|
920
|
+
fm["condition_space"] = new_cs
|
|
921
|
+
# N208(2026-09-28):与 link 同款——本面也是**既有节点的覆写**,原先直调
|
|
922
|
+
# `_write_node` 零闸(层白名单不含该层的身份照样改得动 self/anchor 层与
|
|
923
|
+
# immutable 节点,且无快照无审计)。统一走 protect.guard_overwrite。
|
|
924
|
+
protect.guard_overwrite(_cg, out.node_id, layer=fm.get("layer"),
|
|
925
|
+
sensitivity=fm.get("sensitivity"), actor=verifier)
|
|
926
|
+
_cg._write_node(out.node_id, os.path.join(_cg.root, entry["path"]), fm,
|
|
927
|
+
new_content, durable=True)
|
|
928
|
+
_append_jsonl(_log_path(_cg), {
|
|
929
|
+
"action": "ccgc_recalibrate", "ts": time.time(), "node": out.node_id,
|
|
930
|
+
"verifier": verifier, "compiled_by": compiled_by, "evidence": evidence,
|
|
931
|
+
"entry_id": out.entry_id,
|
|
932
|
+
"slots_before": old_cs, "slots_after": new_cs,
|
|
933
|
+
"condition_before": nodefile.condition_space_text(old_cs),
|
|
934
|
+
"condition_after": new_text})
|
|
935
|
+
_cg.rebuild_index()
|
|
936
|
+
out.ok = True
|
|
937
|
+
out.written = 1
|
|
938
|
+
return out
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
# =============================================================================
|
|
942
|
+
# pending:三段式在「跨调用」形态下的持久化(候选与签章绑定,防篡改)
|
|
943
|
+
# =============================================================================
|
|
944
|
+
#
|
|
945
|
+
# 为什么需要:compile / attest / link 在 MCP 面上是**三次独立调用**(签章必须由
|
|
946
|
+
# 认知图**外**的一方在别处完成),候选对象无法跨调用传递。若让调用方把候选 JSON
|
|
947
|
+
# 原样回传,改写「六行」不会被任何检查发现(签章 token 只覆盖 node/verifier/
|
|
948
|
+
# state/evidence,不含候选内容)。因此候选落盘到 `_ccgc_pending/`(LAYERS 之外、
|
|
949
|
+
# `.json` 后缀 → 索引双隐身的冷区),并对其内容取 hash;link 时校验 hash,
|
|
950
|
+
# 不一致即拒绝写入。
|
|
951
|
+
#
|
|
952
|
+
# 纪律:pending 是**暂存**不是存档——link 成功即删除;留痕在 _ccgc.jsonl。
|
|
953
|
+
|
|
954
|
+
PENDING_DIR = "_ccgc_pending"
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
# 生效条件:node_id 为 None 或空串时按 "" 处理,非 [0-9A-Za-z_.\-] 字符一律替换为 "-",取前 80 个字符;结果为空串时返回 "unnamed"。
|
|
958
|
+
def _safe_name(node_id: str) -> str:
|
|
959
|
+
"""node_id → 文件名安全形态(分支节点形如 mem_x@br1,须剥掉非 [A-Za-z0-9_.-])。"""
|
|
960
|
+
return re.sub(r"[^0-9A-Za-z_.\-]", "-", str(node_id or ""))[:80] or "unnamed"
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
# 生效条件:_as_cg(cg) 的 root 属性为假值→返回 "";否则返回 os.path.join(str(root), PENDING_DIR, _safe_name(node_id) + ".json")。
|
|
964
|
+
def pending_path(cg, node_id: str) -> str:
|
|
965
|
+
_cg = _as_cg(cg)
|
|
966
|
+
root = getattr(_cg, "root", None)
|
|
967
|
+
if not root:
|
|
968
|
+
return ""
|
|
969
|
+
return os.path.join(str(root), PENDING_DIR, _safe_name(node_id) + ".json")
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
# 生效条件:lines、slots 为假值时按 {} 处理,返回 `_sha(json.dumps({"lines":..., "slots":...}, ensure_ascii=False, sort_keys=True))`。
|
|
973
|
+
def _payload_hash(lines, slots) -> str:
|
|
974
|
+
"""候选内容摘要(六行 + 四槽,键序无关)——签章与产物的绑定依据。"""
|
|
975
|
+
return _sha(json.dumps({"lines": lines or {}, "slots": slots or {}},
|
|
976
|
+
ensure_ascii=False, sort_keys=True))
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
# 生效条件:_as_cg(cg) 为 None→返回 {'ok': False, 'error': '未提供 cg'};pending_path 为空→返回 {'ok': False, 'error': '无法定位 pending 目录'};否则写 json(OSError 时返回 ok=False 且带 path 与异常名),成功返回 {'ok': True, 'path': p, 'hash': rec['hash']},其中 attest 按 `attestation is not None` 决定是否落盘。
|
|
980
|
+
def save_pending(cg, compiled: CompileResult,
|
|
981
|
+
attestation: Optional[AttestResult] = None) -> dict:
|
|
982
|
+
"""候选(+ 可选签章)落 pending;返回 {ok, path, hash}。"""
|
|
983
|
+
_cg = _as_cg(cg)
|
|
984
|
+
if _cg is None:
|
|
985
|
+
return {"ok": False, "error": "未提供 cg"}
|
|
986
|
+
p = pending_path(_cg, compiled.node_id)
|
|
987
|
+
if not p:
|
|
988
|
+
return {"ok": False, "error": "无法定位 pending 目录"}
|
|
989
|
+
rec = {"node_id": compiled.node_id, "actor": compiled.actor, "ts": time.time(),
|
|
990
|
+
"hash": _payload_hash(compiled.lines, compiled.slots),
|
|
991
|
+
"compiled": asdict(compiled),
|
|
992
|
+
"attest": asdict(attestation) if attestation is not None else None}
|
|
993
|
+
try:
|
|
994
|
+
os.makedirs(os.path.dirname(p), exist_ok=True)
|
|
995
|
+
with open(p, "w", encoding="utf-8") as f:
|
|
996
|
+
json.dump(rec, f, ensure_ascii=False, indent=1)
|
|
997
|
+
except OSError as exc:
|
|
998
|
+
return {"ok": False, "error": "%s: %s" % (type(exc).__name__, exc), "path": p}
|
|
999
|
+
return {"ok": True, "path": p, "hash": rec["hash"]}
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
# 生效条件:_as_cg(cg) 为 None→error='未提供 cg';pending_path 为空或 os.path.isfile(p) 为假→error=未找到 pending;open/json.load 抛 OSError 或 ValueError→error=pending 不可读;否则按 dataclasses.fields 过滤重建 CompileResult 与(rec['attest'] 为真值时的)AttestResult,hash_ok = `rec.get("hash") == _payload_hash(c.lines, c.slots)`,ok=True。
|
|
1003
|
+
def load_pending(cg, node_id: str) -> dict:
|
|
1004
|
+
"""读回候选与签章,并校验 hash(内容被改即 hash_ok=False,调用方须拒绝写入)。"""
|
|
1005
|
+
_cg = _as_cg(cg)
|
|
1006
|
+
out = {"ok": False, "compiled": None, "attest": None, "hash_ok": False,
|
|
1007
|
+
"path": "", "error": ""}
|
|
1008
|
+
if _cg is None:
|
|
1009
|
+
out["error"] = "未提供 cg"
|
|
1010
|
+
return out
|
|
1011
|
+
p = pending_path(_cg, node_id)
|
|
1012
|
+
out["path"] = p
|
|
1013
|
+
if not p or not os.path.isfile(p):
|
|
1014
|
+
out["error"] = "未找到待写入的编译产物(pending):" + str(node_id)
|
|
1015
|
+
return out
|
|
1016
|
+
try:
|
|
1017
|
+
with open(p, encoding="utf-8") as f:
|
|
1018
|
+
rec = json.load(f)
|
|
1019
|
+
except (OSError, ValueError) as exc:
|
|
1020
|
+
out["error"] = "pending 不可读:%s" % type(exc).__name__
|
|
1021
|
+
return out
|
|
1022
|
+
cfields = {f.name for f in dataclasses.fields(CompileResult)}
|
|
1023
|
+
afields = {f.name for f in dataclasses.fields(AttestResult)}
|
|
1024
|
+
c = CompileResult(**{k: v for k, v in (rec.get("compiled") or {}).items()
|
|
1025
|
+
if k in cfields})
|
|
1026
|
+
a = None
|
|
1027
|
+
if rec.get("attest"):
|
|
1028
|
+
a = AttestResult(**{k: v for k, v in rec["attest"].items() if k in afields})
|
|
1029
|
+
out.update({"compiled": c, "attest": a,
|
|
1030
|
+
"hash_ok": rec.get("hash") == _payload_hash(c.lines, c.slots)})
|
|
1031
|
+
out["ok"] = True
|
|
1032
|
+
return out
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
# 生效条件:pending_path(cg, node_id) 为空→返回 False;否则 os.remove 成功→True,抛 OSError→False。
|
|
1036
|
+
def drop_pending(cg, node_id: str) -> bool:
|
|
1037
|
+
"""删除 pending(link 成功后调用;失败静默——待办件不是事实,无需强保证)。"""
|
|
1038
|
+
p = pending_path(cg, node_id)
|
|
1039
|
+
if not p:
|
|
1040
|
+
return False
|
|
1041
|
+
try:
|
|
1042
|
+
os.remove(p)
|
|
1043
|
+
return True
|
|
1044
|
+
except OSError:
|
|
1045
|
+
return False
|
|
1046
|
+
|
|
1047
|
+
|
|
1048
|
+
# 生效条件:load_pending(cg, node_id) 的 ok 为假→带其 error 返回 LinkResult;hash_ok 为假→以「hash 不符」错误返回;否则转调 link(got["compiled"], got.get("attest"), cg=cg, apply=apply, actor=actor, basis=basis) 并返回其结果,且仅当 res.ok 与 apply 同时为真时调用 drop_pending。
|
|
1049
|
+
def link_pending(cg, node_id: str, *, apply: bool = False, actor: str = "",
|
|
1050
|
+
basis: str = "") -> LinkResult:
|
|
1051
|
+
"""从 pending 读回候选与签章后 link:hash 校验 → 签章准入 → 写入 → 清理 pending。"""
|
|
1052
|
+
out = LinkResult(node_id=str(node_id or "").strip(), dry_run=not apply)
|
|
1053
|
+
got = load_pending(cg, node_id)
|
|
1054
|
+
if not got.get("ok"):
|
|
1055
|
+
out.errors.append(got.get("error") or "pending 不可用")
|
|
1056
|
+
return out
|
|
1057
|
+
if not got.get("hash_ok"):
|
|
1058
|
+
out.errors.append("pending 产物与编译时不一致(hash 不符),拒绝写入——"
|
|
1059
|
+
"候选落盘后不得被改写,请重新编译")
|
|
1060
|
+
return out
|
|
1061
|
+
res = link(got["compiled"], got.get("attest"), cg=cg, apply=apply,
|
|
1062
|
+
actor=actor, basis=basis)
|
|
1063
|
+
if res.ok and apply:
|
|
1064
|
+
drop_pending(cg, node_id)
|
|
1065
|
+
return res
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
__all__ = ["ACCEPT", "REJECT", "DEFER", "BLINDSPOT", "STATES", "CONTRACT_ROLES",
|
|
1069
|
+
"E_CODES", "CompileResult", "AttestResult", "LinkResult", "RuleParser",
|
|
1070
|
+
"compile_dialog", "attest", "link", "recalibrate",
|
|
1071
|
+
"PENDING_DIR", "pending_path", "save_pending", "load_pending",
|
|
1007
1072
|
"drop_pending", "link_pending"]
|