@furongjun1999/dsh-memory 0.4.7 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -37
- package/codebuddy/CODEBUDDY.md +195 -185
- package/docs/Pi/345/217/257/345/255/246/344/271/240/344/274/230/347/202/271_/347/201/265/346/236/242/345/244/247/350/204/221/346/224/271/350/277/233/344/272/244/346/216/245_20260915.md +144 -0
- package/docs/README.md +111 -0
- package/docs/discipline/harnesses.yaml +215 -152
- package/docs/discipline/templates/full.md.tmpl +60 -58
- package/docs/eval//347/254/254/344/270/211/346/226/271/351/252/214/350/257/201/346/212/245/345/221/212_LoCoMo_/347/201/265/346/236/242_.md +127 -0
- package/docs/eval//347/254/254/344/270/211/346/226/271/351/252/214/350/257/201/346/212/245/345/221/212_LoCoMo_/347/201/265/346/236/242_.png +0 -0
- package/docs/eval//347/254/254/344/270/211/346/226/271/351/252/214/350/257/201/346/212/245/345/221/212_/347/201/265/346/236/242_vs_dejavu_/347/273/237/344/270/200/350/257/204/345/210/206_v7.md +202 -0
- package/docs/experiments/m4-role-probe/census.py +86 -0
- package/docs/experiments/m4-role-probe/probe.py +89 -0
- package/docs/experiments/mapped_confidence/bench_mapped_conf.py +216 -0
- package/docs/experiments/mapped_confidence/post_check.py +75 -0
- package/docs/experiments/mapped_confidence/repro_thirdparty_tol.py +83 -0
- package/docs/experiments/mapped_confidence/result_mapped_conf.json +215 -0
- package/docs/{ → hive/}/350/234/202/345/267/242/345/267/245/344/275/234/350/256/260/345/277/206_/351/241/271/347/233/256/350/256/241/345/210/222.md +14 -2
- package/docs/{README → mdcg/README}/350/257/246/347/273/206/347/211/210_v0.4.5.md +9 -9
- package/docs/{ → mdcg/}/344/270/273/344/273/243/347/220/206/345/255/220/344/273/243/347/220/206/350/256/260/345/277/206/346/236/266/346/236/204/350/256/276/350/256/241.md +1 -1
- package/docs/mdcg//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +261 -0
- package/docs/{ → mdcg/}/345/215/225/345/205/203/350/207/252/346/210/221/351/224/232/347/202/271_/347/263/273/347/273/237/346/217/220/347/244/272/350/257/215/346/240/207/345/207/206_v0.3.md +1 -1
- package/docs/mdcg//345/255/220/344/273/243/347/220/206/351/205/215/347/275/256/346/240/207/345/207/206_v0.4.md +148 -0
- package/docs/{ → mdcg/}/346/272/220/347/240/201/347/272/247/346/236/266/346/236/204/345/256/241/350/256/241_GPT/346/211/271/350/257/204/345/257/271/347/205/247_v1.0.md +2 -2
- package/docs/{ → mdcg/}/347/201/265/346/236/242/344/270/211/345/261/202/346/213/206/345/210/206/350/247/204/345/210/222_v0.1.md +181 -181
- package/docs/{ → mdcg/}/347/274/272/345/217/243/345/215/225_P0/346/224/266/345/217/243_v0.1.md +2 -2
- package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276_G4-G8/347/274/272/345/217/243/350/243/201/345/256/232/345/215/225_v0.1.md +1 -1
- package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276_/347/264/242/345/274/225/344/270/216/345/267/245/347/250/213/350/247/204/350/214/203/345/214/226_/350/256/241/345/210/222_v0.1.md +4 -4
- package/docs/{ → mdcg/}/350/256/260/345/277/206/346/223/215/344/275/234/347/263/273/347/273/237_MdCGOS/344/270/216MCP/346/216/245/345/205/245_v0.1.md +2 -2
- package/docs/plans/GridWorld/346/234/200/345/260/217/351/227/255/347/216/257/350/247/204/346/240/274_v0.1.md +176 -0
- package/docs/{ → swarm/}/350/234/202/347/276/244/344/272/222/350/201/224_v0.1.md +3 -3
- package/docs/swarm//350/234/202/347/276/244/345/220/214/351/224/231/346/243/200/346/265/213/345/256/236/351/252/214/345/215/217/350/256/256_v0.1.md +242 -0
- package/docs/theory//345/215/225/347/272/277/347/250/213/344/270/216/346/263/250/346/204/217/345/212/233/351/233/206/344/270/255_/346/227/240/344/272/211/350/256/256/347/220/206/350/256/272/346/226/207/346/241/243_v1.0.md +129 -0
- package/docs/theory//345/271/266/345/217/221/345/277/205/347/204/266/346/200/247/347/220/206/350/256/272_v0.2.md +134 -0
- package/docs/theory//346/246/202/345/277/265/345/210/206/345/261/202/345/257/271/351/275/220/350/241/250_v0.1.md +145 -0
- package/docs/{ → theory/}/347/220/206/350/256/272_/346/234/272/345/210/266_/344/273/243/347/240/201_/345/256/236/351/252/214_/347/274/272/345/217/243/347/237/251/351/230/265_v0.1.md +3 -3
- package/docs/{ → theory/}/347/220/206/350/256/272/344/273/223/346/213/206/345/210/206/344/270/216/347/231/275/347/256/261/347/237/245/350/257/206/345/272/223/345/206/205/350/277/201_v0.1.md +1 -1
- package/docs/theory//350/256/244/347/237/245/344/273/243/347/220/206/344/270/216/346/224/266/346/225/233/347/273/223/346/236/204_/346/235/241/344/273/266/350/256/272/351/207/215/346/236/204_v0.1.md +221 -0
- package/docs/world_model//344/270/226/347/225/214/346/250/241/345/236/213_/347/245/236/347/273/217/347/275/221/347/273/234/345/272/225/345/261/202/346/236/266/346/236/204_v1.0.md +237 -0
- package/docs//345/255/230/347/256/227/344/270/200/344/275/223/346/236/266/346/236/204/345/255/246/344/271/240_/345/244/226/351/203/250/347/220/206/350/256/272/345/257/271/347/205/247/344/270/216/350/267/257/347/272/277/344/272/244/346/216/245_20260916.md +98 -0
- package/docs//345/255/230/347/256/227/344/270/200/344/275/223/347/245/236/347/273/217/347/275/221/347/273/234/346/236/266/346/236/204_/346/200/273/347/272/262/344/270/216/344/272/244/346/216/245_20260916.md +109 -0
- package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +34 -6
- package/docs//347/231/275/347/256/261/345/214/226/347/272/262/351/242/206_/344/270/211/351/241/271/347/233/256/345/255/246/344/271/240/346/200/273/346/236/266/346/236/204/344/270/216Ghidra/344/272/244/346/216/245_20260916.md +104 -0
- package/docs//350/256/260/345/277/206/350/257/204/345/256/241/347/263/273/347/273/237_/347/253/213/351/241/271/350/256/276/350/256/241/344/270/216/346/226/275/345/267/245/344/272/244/346/216/245_20260915.md +183 -0
- package/dsh/README.md +1 -1
- package/lib/hooks.d.ts +5 -0
- package/lib/hooks.js +10 -1
- package/lib/lib/prompt_safety.d.ts +48 -0
- package/lib/lib/prompt_safety.js +62 -0
- package/md_cg/audit.py +76 -2
- package/md_cg/bench_membench.py +10 -5
- package/md_cg/bench_progressive.py +48 -9
- package/md_cg/branches.py +275 -0
- package/md_cg/ccgc.py +884 -0
- package/md_cg/conformance.py +662 -0
- package/md_cg/consistency.py +1 -1
- package/md_cg/corpus.py +1 -1
- package/md_cg/eval_common.py +6 -5
- package/md_cg/forgetting.py +11 -3
- package/md_cg/fsutil.py +63 -1
- package/md_cg/identity.py +2 -2
- package/md_cg/insight.py +2 -2
- package/md_cg/lifecycle.py +262 -0
- package/md_cg/mcp_server.py +547 -140
- package/md_cg/mdcg.py +206 -18
- package/md_cg/mdcos.py +512 -50
- package/md_cg/metacognition.py +2 -2
- package/md_cg/mreview/__init__.py +25 -0
- package/md_cg/mreview/__main__.py +107 -0
- package/md_cg/mreview/bundle.py +170 -0
- package/md_cg/mreview/candidates.py +253 -0
- package/md_cg/mreview/govern.py +674 -0
- package/md_cg/mreview/locate.py +905 -0
- package/md_cg/mreview/pipeline.py +701 -0
- package/md_cg/mreview/rules/duplication.json +21 -0
- package/md_cg/mreview/rules/field_coverage.json +54 -0
- package/md_cg/mreview/rules/source_license.json +21 -0
- package/md_cg/mreview/rules/template_flow.json +21 -0
- package/md_cg/mreview/ruleset.py +238 -0
- package/md_cg/nodefile.py +38 -0
- package/md_cg/predict.py +24 -6
- package/md_cg/protect.py +4 -4
- package/md_cg/refindex.py +41 -2
- package/md_cg/scrub.py +5 -1
- package/md_cg/self_state.py +1 -1
- package/md_cg/semantic/en_normalizer.py +85 -25
- package/md_cg/sustain.py +18 -0
- package/md_cg/tasks.py +447 -0
- package/md_cg/test_action_derive.py +203 -0
- package/md_cg/test_audit_rotate.py +270 -0
- package/md_cg/test_branches.py +249 -0
- package/md_cg/test_ccg_perturb.py +1 -1
- package/md_cg/test_ccgc.py +423 -0
- package/md_cg/test_conformance.py +343 -0
- package/md_cg/test_en_pipeline.py +24 -0
- package/md_cg/test_health_scale.py +173 -0
- package/md_cg/test_identity_attribution.py +6 -0
- package/md_cg/test_index_durability.py +224 -0
- package/md_cg/test_lifecycle.py +309 -0
- package/md_cg/test_mr_m1.py +679 -0
- package/md_cg/test_mr_m2.py +587 -0
- package/md_cg/test_mr_m3.py +703 -0
- package/md_cg/test_mr_m4.py +485 -0
- package/md_cg/test_p1.py +1 -1
- package/md_cg/test_p11_consistency.py +1 -1
- package/md_cg/test_p12_metacognition.py +2 -2
- package/md_cg/test_p16_self_state.py +1 -1
- package/md_cg/test_p26_refindex.py +1 -1
- package/md_cg/test_p27_docindex.py +22 -2
- package/md_cg/test_p28_refcheck.py +1 -1
- package/md_cg/test_p29_session_ingest_export.py +1 -1
- package/md_cg/test_p2_mcp.py +7 -1
- package/md_cg/test_p38_contextualize.py +1 -1
- package/md_cg/test_p39_vision_evidence.py +1 -1
- package/md_cg/test_p40_refine_worklist.py +1 -1
- package/md_cg/test_p41_evolve_patrol.py +1 -1
- package/md_cg/test_p42_provenance.py +1 -1
- package/md_cg/test_p43_pooling.py +41 -13
- package/md_cg/test_read_clip.py +137 -0
- package/md_cg/test_review_conformance.py +310 -0
- package/md_cg/test_tasks.py +409 -0
- package/md_cg/test_tool_face.py +189 -0
- package/md_cg/test_twophase.py +286 -0
- package/md_cg/test_writepipe.py +210 -0
- package/md_cg/theory.py +1 -1
- package/md_cg/tokens.py +95 -5
- package/md_cg/tool_face.py +250 -0
- package/md_cg/twophase.py +221 -0
- package/md_cg/units.py +546 -0
- package/md_cg/vision_evidence.py +1 -1
- package/md_cg/weights.py +1 -1
- package/md_cg/whitebox.py +1 -1
- package/md_cg/whitebox_kb/data/verify_cache.json +28 -0
- package/md_cg/whitebox_kb/data/verify_savings.jsonl +46 -0
- package/md_cg/whitebox_kb/wisdom/audit_log/chain_heat.json +10 -10
- package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-shm +0 -0
- package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-wal +0 -0
- package/md_cg/writelimit.py +19 -3
- package/md_cg/writepipe.py +372 -0
- package/package.json +1 -1
- package/src/hooks.ts +10 -1
- package/src/lib/prompt_safety.ts +62 -0
- package/zcode/AGENTS.md +195 -185
- package/docs//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +0 -221
- /package/docs/{AGI → eval/AGI}/344/270/203/347/273/264/350/257/204/345/210/206/346/212/245/345/221/212_md_cg_v1.0.md" +0 -0
- /package/docs/{AGI → eval/AGI}/344/270/203/347/273/264/350/257/204/345/210/206/346/212/245/345/221/212_md_cg_v2.0.md" +0 -0
- /package/docs/{ → eval/}/345/256/236/351/252/214/346/226/271/346/241/210_/345/255/246/344/271/240/351/227/255/347/216/257AB/344/270/216/346/250/252/350/257/204_v1.0.md" +0 -0
- /package/docs/{ → eval/}/346/250/252/350/257/204_/345/205/255/345/256/266100/351/242/230/344/270/255/350/213/261/345/217/214/346/237/245_v1.0.md" +0 -0
- /package/docs/{guardrail-charter.md → mdcg/guardrail-charter.md} +0 -0
- /package/docs/{lingshu_tutorial.html → mdcg/lingshu_tutorial.html} +0 -0
- /package/docs/{memory-assessment.html → mdcg/memory-assessment.html} +0 -0
- /package/docs/{memory_score.html → mdcg/memory_score.html} +0 -0
- /package/docs/{memory_score.png → mdcg/memory_score.png} +0 -0
- /package/docs/{release_v0.3.0.md → mdcg/release_v0.3.0.md} +0 -0
- /package/docs/{release_v0.4.5.md → mdcg/release_v0.4.5.md} +0 -0
- /package/docs/{tool_table_v0.3.0.md → mdcg/tool_table_v0.3.0.md} +0 -0
- /package/docs/{ → mdcg/}/344/273/244/347/211/214/344/270/216/350/247/222/350/211/262/346/235/203/350/201/214/345/210/206/347/246/273_v0.1.md" +0 -0
- /package/docs/{ → mdcg/}/347/201/265/346/236/24282/345/267/245/345/205/267_/345/212/237/350/203/275/346/225/264/347/220/206/344/270/216/350/277/201/347/247/273/346/230/240/345/260/204_v0.1.md" +0 -0
- /package/docs/{ → mdcg/}/347/201/265/346/236/242MCP/345/267/245/345/205/267/346/200/273/350/241/250_v3.4.md" +0 -0
- /package/docs/{ → mdcg/}/347/201/265/346/236/242_/350/207/252/346/210/221/345/261/202/345/256/232/344/271/211.md" +0 -0
- /package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276_MD/347/233/256/345/275/225/346/226/271/346/241/210_v0.1.md" +0 -0
- /package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276_/346/235/241/344/273/266/347/251/272/351/227/264/345/220/210/346/210/220/344/270/216/347/224/237/346/225/210/346/235/241/344/273/266/345/217/243/345/276/204_v0.1.md" +0 -0
- /package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276/344/275/234/344/270/272/350/256/260/345/277/206/346/223/215/344/275/234/347/263/273/347/273/237_/350/257/204/344/274/260/344/270/216/350/267/257/347/272/277_v0.1.md" +0 -0
- /package/docs/{ → plans/}/345/221/275/345/220/215/346/262/273/347/220/206_/351/241/271/347/233/256/350/256/241/345/210/222.md" +0 -0
- /package/docs/{ → plans/}/347/237/245/350/257/206/345/233/276/350/260/261/351/241/271/347/233/256_/344/272/244/346/216/245/346/226/207/346/241/243_20260914.md" +0 -0
- /package/docs/{ → swarm/}/350/234/202/347/276/244/345/244/232/346/231/272/350/203/275/344/275/223_/344/275/277/347/224/250/346/212/245/345/221/212_20260913.md" +0 -0
- /package/docs/{ → swarm/}/350/234/202/347/276/244/345/244/232/346/231/272/350/203/275/344/275/223_/345/212/237/350/203/275/350/257/264/346/230/216_v0.6.md" +0 -0
- /package/docs/{ → swarm/}/350/234/202/347/276/244/347/233/262/345/214/272/346/240/207/350/256/260_v1.0.md" +0 -0
- /package/docs/{ → theory/}/344/277/241/346/201/257/345/267/256/344/270/272/344/273/200/344/271/210/345/277/205/347/204/266/345/255/230/345/234/250/344/270/224/350/207/252/347/204/266/346/211/251/345/244/247.md" +0 -0
- /package/docs/{ → theory/}/346/231/272/350/203/275/347/232/204/345/205/254/347/220/206/345/214/226/345/237/272/347/237/263.md" +0 -0
- /package/docs/{ → theory/}/346/231/272/350/203/275/347/232/204/350/256/244/347/237/245/350/277/207/347/250/213.md" +0 -0
- /package/docs/{ → theory/}/346/231/272/350/203/275/350/256/2723.4.md" +0 -0
- /package/docs/{ → theory/}/347/231/275/347/256/261/346/231/272/350/203/275/346/230/257/344/273/200/344/271/210/357/274/237.md" +0 -0
- /package/docs/{ → theory/}/347/231/275/347/256/261/346/231/272/350/203/275/347/263/273/345/210/227/302/267/347/254/254/344/272/224/347/257/207/357/274/232/350/256/251AI/347/234/237/346/255/243/350/243/205/344/270/212/350/256/260/345/277/206.md" +0 -0
package/md_cg/ccgc.py
ADDED
|
@@ -0,0 +1,884 @@
|
|
|
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 == 编译执行者`——结构级阻断,不依赖 prompt 自觉。
|
|
17
|
+
|
|
18
|
+
对齐既有实现(零发明):
|
|
19
|
+
· 验证能力外置 / 缺能力恒不通过 → md_cg/audit.py(CONTENT_KINDS + register_verifier)
|
|
20
|
+
· 缺参即 error、success 由 errors 定 → compiler/api.py(CompileResult 范式)
|
|
21
|
+
· 生效条件唯一合成入口 → nodefile.condition_space_text(本模块不另写合成)
|
|
22
|
+
· 六行写入口径 → consolidate._upsert_ccg_line(同款语义,就地实现防 import 环)
|
|
23
|
+
· 留痕可回滚 → 对齐 backfill 的 _backfill.jsonl 纪律
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import dataclasses
|
|
28
|
+
import hashlib
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
import re
|
|
32
|
+
import time
|
|
33
|
+
from dataclasses import asdict, dataclass, field
|
|
34
|
+
from typing import Any, Dict, List, Optional
|
|
35
|
+
|
|
36
|
+
from . import nodefile
|
|
37
|
+
|
|
38
|
+
# ---- 四态:复用 audit / judge_qualification 的裁决语汇(不新造状态机) ----
|
|
39
|
+
ACCEPT, REJECT, DEFER, BLINDSPOT = "ACCEPT", "REJECT", "DEFER", "BLINDSPOT"
|
|
40
|
+
STATES = (ACCEPT, REJECT, DEFER, BLINDSPOT)
|
|
41
|
+
|
|
42
|
+
LOG_NAME = "_ccgc.jsonl"
|
|
43
|
+
|
|
44
|
+
# ---- 裁定 B:六要素的契约角色(术语真源在 nodefile,此处只引用不另定义) ----
|
|
45
|
+
# 单一真源纪律:术语若在 ccgc 再写一份,会与 nodefile 漂移(谁是真源无从判定)。
|
|
46
|
+
CONTRACT_ROLES = nodefile.CCG_CONTRACT_ROLES
|
|
47
|
+
|
|
48
|
+
# ---- 编译错误码(裁定 B:缺参数即编译错误,绝不静默降级) ----
|
|
49
|
+
E_CODES = {
|
|
50
|
+
"E001": "缺少源:dialog 为空(无可编译的对话记录)",
|
|
51
|
+
"E002": "缺少目标:node_id 为空或节点不存在",
|
|
52
|
+
"E003": "缺少主体:actor 为空(验证溯源必需)",
|
|
53
|
+
"E004": "源不可读:密文或权限拒绝(fail-closed)",
|
|
54
|
+
"E010": "引用定位失败:span 不是对话记录的字面子串",
|
|
55
|
+
"E011": "值捏造:写入值不是对话记录的字面子串",
|
|
56
|
+
"E020": "四槽不全:condition_space 缺槽,不构成生效条件",
|
|
57
|
+
"E021": "槽值非法:空值、待填充占位或不可渲染",
|
|
58
|
+
"E030": "验证基底非法:不在 VERIFICATION_BASIS 枚举内",
|
|
59
|
+
"E040": "无验证签章:未提供 attestation(缺能力恒不写入)",
|
|
60
|
+
"E041": "自证拒绝:验证方标识 == 编译执行者(LLM 不得自己验证自己)",
|
|
61
|
+
"E042": "验证未通过:签章判定 REJECT / BLINDSPOT",
|
|
62
|
+
"E043": "修正非法:只允许修正 condition_space 四槽",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# ---- 候选来源标识(写进留痕,可溯源到「谁说的」) ----
|
|
66
|
+
SRC_EXPLICIT = "explicit" # 显式入参(人 / 设计者提供)
|
|
67
|
+
SRC_PARSER = "parser" # 外部注入解析器(LLM 等能力外置通道)
|
|
68
|
+
SRC_RULE = "rule" # 内生规则解析器(诚实下界能力)
|
|
69
|
+
|
|
70
|
+
#: 参与名实门的五要素(生效条件由四槽合成,不来自候选,故不在此列)
|
|
71
|
+
_CAND_MARKS = ("功能名", "子功能", "执行", "验证方式", "不适用条件")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# =============================================================================
|
|
75
|
+
# 结果对象(对齐 compiler/api.py 的 CompileResult 范式)
|
|
76
|
+
# =============================================================================
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class CompileResult:
|
|
80
|
+
"""编译结果。`success = (len(errors) == 0)`——与 compiler 同口径。"""
|
|
81
|
+
success: bool = False
|
|
82
|
+
node_id: str = ""
|
|
83
|
+
actor: str = ""
|
|
84
|
+
lines: Dict[str, str] = field(default_factory=dict) # 六要素行
|
|
85
|
+
slots: Dict[str, Any] = field(default_factory=dict) # condition_space 四槽
|
|
86
|
+
spans: Dict[str, List[str]] = field(default_factory=dict) # 要素 → 引用 span 清单
|
|
87
|
+
sources: Dict[str, str] = field(default_factory=dict) # 要素 → explicit|parser|rule
|
|
88
|
+
ungrounded: List[Dict[str, str]] = field(default_factory=list) # 未过名实门的项
|
|
89
|
+
errors: List[str] = field(default_factory=list)
|
|
90
|
+
warnings: List[str] = field(default_factory=list)
|
|
91
|
+
verdict: Optional[Dict] = None # 验证单元终裁
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def has_errors(self) -> bool:
|
|
95
|
+
return len(self.errors) > 0
|
|
96
|
+
|
|
97
|
+
def err(self, code: str, extra: str = "") -> None:
|
|
98
|
+
msg = E_CODES.get(code, code)
|
|
99
|
+
self.errors.append(code + " " + msg + (("(" + extra + ")") if extra else ""))
|
|
100
|
+
|
|
101
|
+
def summary(self) -> str:
|
|
102
|
+
out = ["编译成功" if self.success else
|
|
103
|
+
"编译失败(%d 个错误)" % len(self.errors)]
|
|
104
|
+
out.append(" 节点: %s" % (self.node_id or "-"))
|
|
105
|
+
out.append(" 要素: %s" % ", ".join(k for k in nodefile.CCG_MARKS if k in self.lines))
|
|
106
|
+
for e in self.errors[:10]:
|
|
107
|
+
out.append(" ERROR " + e)
|
|
108
|
+
for w in self.warnings[:5]:
|
|
109
|
+
out.append(" WARN " + w)
|
|
110
|
+
return "\n".join(out)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass
|
|
114
|
+
class AttestResult:
|
|
115
|
+
"""验证签章结果。`ok=True` 才构成 link 的准入凭证。"""
|
|
116
|
+
ok: bool = False
|
|
117
|
+
node_id: str = ""
|
|
118
|
+
state: str = ""
|
|
119
|
+
verifier: str = "" # 认知图**外**的验证方(子代理 / designer / user)
|
|
120
|
+
compiled_by: str = "" # 编译执行者(供 E041 比对)
|
|
121
|
+
evidence: str = ""
|
|
122
|
+
error: str = ""
|
|
123
|
+
token: str = "" # 签章令牌(留痕可查)
|
|
124
|
+
slot_corrections: Dict[str, Any] = field(default_factory=dict)
|
|
125
|
+
ts: float = 0.0
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass
|
|
129
|
+
class LinkResult:
|
|
130
|
+
ok: bool = False
|
|
131
|
+
node_id: str = ""
|
|
132
|
+
dry_run: bool = True
|
|
133
|
+
written: int = 0
|
|
134
|
+
errors: List[str] = field(default_factory=list)
|
|
135
|
+
warnings: List[str] = field(default_factory=list)
|
|
136
|
+
entry_id: str = ""
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# =============================================================================
|
|
140
|
+
# 工具(就地实现,避免跨模块 import 环——与 mdcg.py 复制 _ccg_line 的既有做法一致)
|
|
141
|
+
# =============================================================================
|
|
142
|
+
|
|
143
|
+
def _sha(s: str) -> str:
|
|
144
|
+
return hashlib.sha1((s or "").encode("utf-8")).hexdigest()[:12]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _log_path(cg) -> str:
|
|
148
|
+
return os.path.join(cg.root, LOG_NAME)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _as_cg(x):
|
|
152
|
+
"""接受 root 路径或已构造的 cg 实例——保持密级隔离与密钥上下文。"""
|
|
153
|
+
if x is None or not isinstance(x, str):
|
|
154
|
+
return x
|
|
155
|
+
from .mdcos import MdCGOS
|
|
156
|
+
return MdCGOS(x)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _has_ccg_line(content: str, field_name: str) -> bool:
|
|
160
|
+
text = content or ""
|
|
161
|
+
return ("# " + field_name + ":") in text or ("# " + field_name + ":") in text
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _upsert_ccg_line(content: str, field_name: str, value: str) -> str:
|
|
165
|
+
"""写入/替换 `# <字段>:<值>`,优先插在「# 功能名」之后。
|
|
166
|
+
|
|
167
|
+
与 consolidate._upsert_ccg_line 同款语义(就近实现,避免 import 环)。
|
|
168
|
+
"""
|
|
169
|
+
lines = (content or "").split("\n")
|
|
170
|
+
for i, ln in enumerate(lines):
|
|
171
|
+
s = ln.strip()
|
|
172
|
+
if not s.startswith("#") or field_name not in s:
|
|
173
|
+
continue
|
|
174
|
+
name = s.lstrip("#").strip().split(":")[0].split(":")[0].strip()
|
|
175
|
+
if name == field_name:
|
|
176
|
+
lines[i] = "# " + field_name + ":" + value
|
|
177
|
+
return "\n".join(lines)
|
|
178
|
+
newline = "# " + field_name + ":" + value
|
|
179
|
+
for i, ln in enumerate(lines):
|
|
180
|
+
if ln.strip().startswith("# 功能名"):
|
|
181
|
+
lines.insert(i + 1, newline)
|
|
182
|
+
return "\n".join(lines)
|
|
183
|
+
return newline + "\n" + (content or "")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _append_jsonl(path: str, rec: dict) -> None:
|
|
187
|
+
try:
|
|
188
|
+
with open(path, "a", encoding="utf-8") as f:
|
|
189
|
+
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
|
|
190
|
+
except OSError:
|
|
191
|
+
pass
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _read_jsonl(path: str) -> List[dict]:
|
|
195
|
+
out: List[dict] = []
|
|
196
|
+
if not path or not os.path.exists(path):
|
|
197
|
+
return out
|
|
198
|
+
try:
|
|
199
|
+
with open(path, encoding="utf-8") as f:
|
|
200
|
+
for ln in f:
|
|
201
|
+
ln = ln.strip()
|
|
202
|
+
if not ln:
|
|
203
|
+
continue
|
|
204
|
+
try:
|
|
205
|
+
out.append(json.loads(ln))
|
|
206
|
+
except ValueError:
|
|
207
|
+
continue
|
|
208
|
+
except OSError:
|
|
209
|
+
return []
|
|
210
|
+
return out
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# =============================================================================
|
|
214
|
+
# 名实门(幻觉的编译期机械检出)
|
|
215
|
+
# =============================================================================
|
|
216
|
+
|
|
217
|
+
#: 值分片分隔符——值可为多句枚举,但**每个分片**都必须是原文子串
|
|
218
|
+
_FRAG_SPLIT = re.compile(r"[;;、,,]+")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _value_grounded(dialog: str, value) -> bool:
|
|
222
|
+
"""值是否 grounded:按分片切分后,每个分片都是对话记录的字面子串。
|
|
223
|
+
|
|
224
|
+
分片口径(而非整串口径)是为容纳「多句枚举」这一合法形态(`# 子功能:A;B`);
|
|
225
|
+
但**任一分片不在原文即判捏造**——改写、拼接、概括一律被挡在门外。
|
|
226
|
+
"""
|
|
227
|
+
s = "" if value is None else str(value).strip()
|
|
228
|
+
if not s:
|
|
229
|
+
return False
|
|
230
|
+
frags = [f.strip() for f in _FRAG_SPLIT.split(s) if f.strip()]
|
|
231
|
+
if not frags:
|
|
232
|
+
return False
|
|
233
|
+
return all(f in dialog for f in frags)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _span_grounded(dialog: str, span) -> bool:
|
|
237
|
+
"""引用 span 是否 grounded:必须是对话记录的**连续**字面子串。"""
|
|
238
|
+
s = "" if span is None else str(span).strip()
|
|
239
|
+
return bool(s) and s in dialog
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# =============================================================================
|
|
243
|
+
# 候选来源:显式入参 / 外部 parser / 内生规则解析器
|
|
244
|
+
# =============================================================================
|
|
245
|
+
|
|
246
|
+
#: 内生规则解析器的线索词(确定性、可审计;不做语义推断)
|
|
247
|
+
_CUES = {
|
|
248
|
+
"observation_position": ("位于", "在本地", "在服务", "在文件", "在系统",
|
|
249
|
+
"端上", "本地", "文件系统", "服务器"),
|
|
250
|
+
"observation_tool": ("使用", "采用", "通过", "调用", "借助", "依托", "方法"),
|
|
251
|
+
"existence_constraint": ("仅", "只", "前提", "除非", "限于", "必须是", "应当"),
|
|
252
|
+
}
|
|
253
|
+
_NEG_CUES = ("不适用", "不处理", "除外", "例外", "排除")
|
|
254
|
+
_EXEC_CUES = ("先", "再", "然后", "依次", "步骤", "读", "调用", "执行")
|
|
255
|
+
_SUB_CUES = ("分", "包含", "由", "组成", "包括")
|
|
256
|
+
#: 验证基底线索词 → VERIFICATION_BASIS 枚举
|
|
257
|
+
_BASIS_CUES = (
|
|
258
|
+
("测试", "test"), ("实测", "test"), ("回放", "test"), ("断言", "test"),
|
|
259
|
+
("编译", "compiler"), ("测量", "measurement"), ("证明", "formal_proof"),
|
|
260
|
+
("统计", "data"), ("教材", "textbook"), ("公开知识", "public_kb"),
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
_SENT_SPLIT = re.compile(r"[。!?!?;;\n]+")
|
|
264
|
+
_TS_PAT = re.compile(r"(20\d{2})[-/年](\d{1,2})[-/月](\d{1,2})")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _sentences(dialog: str) -> List[str]:
|
|
268
|
+
return [s.strip() for s in _SENT_SPLIT.split(dialog or "") if s.strip()]
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _pick_by_cues(sents: List[str], cues, limit: int = 1) -> List[str]:
|
|
272
|
+
"""按线索词命中挑句(命中即整句作为候选——整句天然是原文子串,不会捏造)。"""
|
|
273
|
+
out: List[str] = []
|
|
274
|
+
for s in sents:
|
|
275
|
+
if any(c in s for c in cues):
|
|
276
|
+
out.append(s)
|
|
277
|
+
if len(out) >= limit:
|
|
278
|
+
break
|
|
279
|
+
return out
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class RuleParser:
|
|
283
|
+
"""内生规则解析器:**诚实下界能力**,不是主路径。
|
|
284
|
+
|
|
285
|
+
按线索词从对话记录整句摘取候选,产出天然 grounded(无幻觉可能),但覆盖率低:
|
|
286
|
+
四槽不齐即 E020 编译失败——这正是「缺能力时不假装通过」的既有纪律
|
|
287
|
+
(对齐 audit.py:未注入验证器恒 DEFER,绝不假装通过)。
|
|
288
|
+
真正的抽取能力由外部经 `parser=` 注入(LLM 等),能力外置、认知图只留接口。
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
name = SRC_RULE
|
|
292
|
+
|
|
293
|
+
def candidates(self, dialog: str, ctx: Optional[dict] = None) -> Dict[str, Any]:
|
|
294
|
+
sents = _sentences(dialog)
|
|
295
|
+
out: Dict[str, Any] = {"marks": {}, "slots": {}}
|
|
296
|
+
|
|
297
|
+
if sents:
|
|
298
|
+
head = sents[0]
|
|
299
|
+
name = head if len(head) <= 30 else head[:30]
|
|
300
|
+
out["marks"]["功能名"] = {"value": name, "span": name}
|
|
301
|
+
|
|
302
|
+
pos = _pick_by_cues(sents, _CUES["observation_position"])
|
|
303
|
+
tool = _pick_by_cues(sents, _CUES["observation_tool"])
|
|
304
|
+
cons = _pick_by_cues(sents, _CUES["existence_constraint"])
|
|
305
|
+
if pos:
|
|
306
|
+
out["slots"]["observation_position"] = {"value": pos[0], "span": pos[0]}
|
|
307
|
+
if tool:
|
|
308
|
+
out["slots"]["observation_tool"] = {"value": tool[0], "span": tool[0]}
|
|
309
|
+
if cons:
|
|
310
|
+
out["slots"]["existence_constraint"] = {"value": cons[0], "span": cons[0]}
|
|
311
|
+
|
|
312
|
+
# 时间槽:含 ISO 日期 → 该日全天(UTC);不含 → 全时窗。
|
|
313
|
+
# 二者都是**合法声明**(nodefile 已定义全时窗语义),非占位;
|
|
314
|
+
# basis 经 synthetic 标注,避免被误读为「已测得具体时刻」。
|
|
315
|
+
m = _TS_PAT.search(dialog or "")
|
|
316
|
+
if m:
|
|
317
|
+
try:
|
|
318
|
+
t = time.mktime((int(m.group(1)), int(m.group(2)), int(m.group(3)),
|
|
319
|
+
0, 0, 0, 0, 0, 0)) - time.timezone
|
|
320
|
+
out["slots"]["time_window"] = {"value": [t, t + 86399], "span": m.group(0)}
|
|
321
|
+
except (ValueError, OverflowError, OSError):
|
|
322
|
+
pass
|
|
323
|
+
if "time_window" not in out["slots"]:
|
|
324
|
+
out["slots"]["time_window"] = {
|
|
325
|
+
"value": [nodefile.FULL_TIME_WINDOW_MIN, nodefile.FULL_TIME_WINDOW_MAX],
|
|
326
|
+
"span": "", "synthetic": "full_time_window"}
|
|
327
|
+
|
|
328
|
+
for field_name, cues in (("不适用条件", _NEG_CUES), ("执行", _EXEC_CUES),
|
|
329
|
+
("子功能", _SUB_CUES)):
|
|
330
|
+
hit = _pick_by_cues(sents, cues)
|
|
331
|
+
if hit:
|
|
332
|
+
out["marks"][field_name] = {"value": hit[0], "span": hit[0]}
|
|
333
|
+
for cue, basis in _BASIS_CUES:
|
|
334
|
+
if cue in (dialog or ""):
|
|
335
|
+
out["marks"]["验证方式"] = {"value": cue, "span": cue, "basis": basis}
|
|
336
|
+
break
|
|
337
|
+
return out
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _invoke_parser(parser, dialog: str, ctx: Optional[dict]) -> Dict[str, Any]:
|
|
341
|
+
"""调用外部/内生解析器,统一形态;异常不视作通过(返回空候选)。"""
|
|
342
|
+
fn = getattr(parser, "candidates", parser)
|
|
343
|
+
try:
|
|
344
|
+
out = fn(dialog, ctx or {})
|
|
345
|
+
except Exception: # noqa: BLE001 —— 解析器异常 = 无候选,不冒充成功
|
|
346
|
+
return {"marks": {}, "slots": {}}
|
|
347
|
+
if not isinstance(out, dict):
|
|
348
|
+
return {"marks": {}, "slots": {}}
|
|
349
|
+
out.setdefault("marks", {})
|
|
350
|
+
out.setdefault("slots", {})
|
|
351
|
+
return out
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
# =============================================================================
|
|
355
|
+
# 核心:compile_dialog(对话记录 → 六要素候选;只编译,不入库)
|
|
356
|
+
# =============================================================================
|
|
357
|
+
|
|
358
|
+
def compile_dialog(dialog: str, node_id: str, actor: str, *,
|
|
359
|
+
marks: Optional[Dict[str, Any]] = None,
|
|
360
|
+
slots: Optional[Dict[str, Any]] = None,
|
|
361
|
+
parser: Any = None,
|
|
362
|
+
strict_spans: bool = True,
|
|
363
|
+
cg: Any = None) -> CompileResult:
|
|
364
|
+
"""按五环编译:前端契约检查 → 候选 → 名实门 → 四槽合成 → 生成六行 → 终裁。
|
|
365
|
+
|
|
366
|
+
入参优先级:显式 `marks`/`slots` > 外部 `parser=` > 内生 `RuleParser`。
|
|
367
|
+
显式入参在 `strict_spans=True`(默认)时**同样**过名实门——人要写条件,
|
|
368
|
+
也须拿得出对话原文依据;确需写入对话之外的声明时显式传 `strict_spans=False`,
|
|
369
|
+
该豁免计入 warnings 与留痕(可审计,不静默)。
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
dialog: 对话记录原文(编译源)
|
|
373
|
+
node_id: 目标节点 id
|
|
374
|
+
actor: 编译执行者标识(供 attest 的 E041 比对,必填)
|
|
375
|
+
marks: 显式五要素(功能名/子功能/执行/验证方式/不适用条件)
|
|
376
|
+
slots: 显式 condition_space 四槽
|
|
377
|
+
parser: 外部候选生成器(对象含 candidates(dialog, ctx),或直接可调用)
|
|
378
|
+
strict_spans: 显式入参是否也过名实门(默认 True)
|
|
379
|
+
cg: 可选 cg 实例/root——提供时校验节点存在性
|
|
380
|
+
"""
|
|
381
|
+
res = CompileResult(node_id=str(node_id or "").strip(),
|
|
382
|
+
actor=str(actor or "").strip())
|
|
383
|
+
|
|
384
|
+
# ---- 第 0 环:前端契约检查(缺参即编译错误,裁定 B) ----
|
|
385
|
+
if not (dialog or "").strip():
|
|
386
|
+
res.err("E001")
|
|
387
|
+
if not res.node_id:
|
|
388
|
+
res.err("E002")
|
|
389
|
+
if not res.actor:
|
|
390
|
+
res.err("E003")
|
|
391
|
+
_cg = _as_cg(cg)
|
|
392
|
+
if _cg is not None and res.node_id:
|
|
393
|
+
if not ((_cg.index.get("nodes") or {}).get(res.node_id)):
|
|
394
|
+
res.err("E002", "节点不存在:" + res.node_id)
|
|
395
|
+
if res.errors:
|
|
396
|
+
res.verdict = {"passed": False, "reason": res.errors[0],
|
|
397
|
+
"authority": "VERIFICATION_UNIT"}
|
|
398
|
+
return res
|
|
399
|
+
|
|
400
|
+
src = str(dialog)
|
|
401
|
+
ctx = {"cg": _cg, "actor": res.actor, "node_id": res.node_id}
|
|
402
|
+
|
|
403
|
+
# ---- 第 1 环:收集候选(显式 > 外部 parser > 内生 rule) ----
|
|
404
|
+
cand_marks: Dict[str, Any] = {}
|
|
405
|
+
cand_slots: Dict[str, Any] = {}
|
|
406
|
+
if marks or slots:
|
|
407
|
+
src_kind = SRC_EXPLICIT
|
|
408
|
+
for k, v in (slots or {}).items():
|
|
409
|
+
cand_slots[k] = v if isinstance(v, dict) else {"value": v}
|
|
410
|
+
for k, v in (marks or {}).items():
|
|
411
|
+
cand_marks[k] = v if isinstance(v, dict) else {"value": v}
|
|
412
|
+
else:
|
|
413
|
+
if parser is None:
|
|
414
|
+
src_kind = SRC_RULE
|
|
415
|
+
res.warnings.append(
|
|
416
|
+
"未注入外部解析器(parser=)且无显式入参,退回内生规则解析器(下界能力);"
|
|
417
|
+
"四槽不齐将按 E020 编译失败——缺能力时不假装通过。")
|
|
418
|
+
else:
|
|
419
|
+
src_kind = SRC_PARSER
|
|
420
|
+
res.warnings.append(
|
|
421
|
+
"外部解析器产物已过名实门(E010/E011):任一值非对话记录字面子串即编译失败。")
|
|
422
|
+
got = _invoke_parser(parser if parser is not None else RuleParser(), src, ctx)
|
|
423
|
+
cand_marks = dict(got.get("marks") or {})
|
|
424
|
+
cand_slots = dict(got.get("slots") or {})
|
|
425
|
+
|
|
426
|
+
# 显式入参在 strict_spans=True 时同样过门;豁免须显式声明且留痕
|
|
427
|
+
exempt = (src_kind == SRC_EXPLICIT and not strict_spans)
|
|
428
|
+
if exempt:
|
|
429
|
+
res.warnings.append(
|
|
430
|
+
"strict_spans=False:显式入参豁免名实门,含未经对话原文支撑的声明(已留痕)。")
|
|
431
|
+
|
|
432
|
+
def _field(item):
|
|
433
|
+
"""候选项 → (value, span, basis, synthetic);容忍裸值与 dict 两种形态。"""
|
|
434
|
+
if isinstance(item, dict):
|
|
435
|
+
return (item.get("value"), item.get("span"),
|
|
436
|
+
item.get("basis"), item.get("synthetic"))
|
|
437
|
+
return (item, None, None, None)
|
|
438
|
+
|
|
439
|
+
# ---- 第 2 环:四槽校验 + 生效条件合成(唯一入口 nodefile.condition_space_text) ----
|
|
440
|
+
clean_slots: Dict[str, Any] = {}
|
|
441
|
+
for key, _label in nodefile.CONDITION_SLOTS:
|
|
442
|
+
if key not in cand_slots:
|
|
443
|
+
continue
|
|
444
|
+
value, span, _basis, synthetic = _field(cand_slots[key])
|
|
445
|
+
if key == "time_window":
|
|
446
|
+
# 全时窗哨兵是合法声明;其余须为 [lo, hi] 数值对
|
|
447
|
+
if not (isinstance(value, (list, tuple)) and len(value) == 2):
|
|
448
|
+
res.err("E021", "槽 time_window 非 [lo, hi]:" + repr(value))
|
|
449
|
+
continue
|
|
450
|
+
else:
|
|
451
|
+
if nodefile.is_placeholder_text(value):
|
|
452
|
+
res.err("E021", "槽 " + key + " 空值或待填充占位:" + repr(value))
|
|
453
|
+
continue
|
|
454
|
+
if not exempt and not _value_grounded(src, value):
|
|
455
|
+
res.err("E011", "槽 " + key + " 值非原文子串:" + repr(value))
|
|
456
|
+
res.ungrounded.append({"field": "condition_space." + key,
|
|
457
|
+
"span": str(span), "value": str(value)})
|
|
458
|
+
continue
|
|
459
|
+
if not exempt and span and not _span_grounded(src, span):
|
|
460
|
+
res.err("E010", "槽 " + key + " 引用定位失败:" + repr(span))
|
|
461
|
+
res.ungrounded.append({"field": "condition_space." + key,
|
|
462
|
+
"span": str(span), "value": str(value)})
|
|
463
|
+
continue
|
|
464
|
+
clean_slots[key] = value
|
|
465
|
+
span_list = res.spans.setdefault("condition_space." + key, [])
|
|
466
|
+
if span:
|
|
467
|
+
span_list.append(str(span))
|
|
468
|
+
if synthetic:
|
|
469
|
+
res.sources["condition_space." + key] = synthetic
|
|
470
|
+
|
|
471
|
+
missing = nodefile.condition_space_missing(clean_slots)
|
|
472
|
+
env_text = nodefile.condition_space_text(clean_slots)
|
|
473
|
+
if missing or not env_text:
|
|
474
|
+
res.err("E020", "缺槽 " + ",".join(missing or list(nodefile.CONDITION_SLOTS_REQUIRED)))
|
|
475
|
+
res.slots = clean_slots
|
|
476
|
+
|
|
477
|
+
# ---- 第 3 环:五要素过名实门 ----
|
|
478
|
+
clean_marks: Dict[str, str] = {}
|
|
479
|
+
basis_from_cand = None
|
|
480
|
+
for field_name in _CAND_MARKS:
|
|
481
|
+
if field_name not in cand_marks:
|
|
482
|
+
continue
|
|
483
|
+
value, span, basis, _synthetic = _field(cand_marks[field_name])
|
|
484
|
+
if nodefile.is_placeholder_text(value):
|
|
485
|
+
res.err("E021", "要素 " + field_name + " 空值或待填充占位:" + repr(value))
|
|
486
|
+
continue
|
|
487
|
+
if not exempt:
|
|
488
|
+
if not _value_grounded(src, value):
|
|
489
|
+
res.err("E011", "要素 " + field_name + " 值非原文子串:" + repr(value))
|
|
490
|
+
res.ungrounded.append({"field": field_name, "span": str(span),
|
|
491
|
+
"value": str(value)})
|
|
492
|
+
continue
|
|
493
|
+
if span and not _span_grounded(src, span):
|
|
494
|
+
res.err("E010", "要素 " + field_name + " 引用定位失败:" + repr(span))
|
|
495
|
+
res.ungrounded.append({"field": field_name, "span": str(span),
|
|
496
|
+
"value": str(value)})
|
|
497
|
+
continue
|
|
498
|
+
clean_marks[field_name] = str(value).strip()
|
|
499
|
+
res.sources[field_name] = src_kind
|
|
500
|
+
span_list = res.spans.setdefault(field_name, [])
|
|
501
|
+
if span:
|
|
502
|
+
span_list.append(str(span))
|
|
503
|
+
if field_name == "验证方式" and basis:
|
|
504
|
+
basis_from_cand = basis
|
|
505
|
+
res.sources["_src_kind"] = src_kind
|
|
506
|
+
|
|
507
|
+
# ---- 第 4 环:验证基底(显式 basis > 文本线索词 > other) ----
|
|
508
|
+
basis_val = basis_from_cand
|
|
509
|
+
if basis_val is None and clean_marks.get("验证方式"):
|
|
510
|
+
for cue, enum_val in _BASIS_CUES:
|
|
511
|
+
if cue in clean_marks["验证方式"]:
|
|
512
|
+
basis_val = enum_val
|
|
513
|
+
break
|
|
514
|
+
if basis_val is None:
|
|
515
|
+
basis_val = "other"
|
|
516
|
+
res.warnings.append("未声明验证基底,落默认 other(不得据以宣称已实测)。")
|
|
517
|
+
if basis_val not in nodefile.VERIFICATION_BASIS:
|
|
518
|
+
res.err("E030", repr(basis_val))
|
|
519
|
+
res.sources["verification_basis"] = basis_val
|
|
520
|
+
|
|
521
|
+
# ---- 第 5 环:生成六行(生效条件**只能**由四槽合成,无第二通路) ----
|
|
522
|
+
lines: Dict[str, str] = {}
|
|
523
|
+
if clean_marks.get("功能名"):
|
|
524
|
+
lines["功能名"] = clean_marks["功能名"]
|
|
525
|
+
if env_text:
|
|
526
|
+
lines["生效条件"] = env_text
|
|
527
|
+
for k in ("子功能", "执行", "验证方式", "不适用条件"):
|
|
528
|
+
if clean_marks.get(k):
|
|
529
|
+
lines[k] = clean_marks[k]
|
|
530
|
+
res.lines = lines
|
|
531
|
+
|
|
532
|
+
# ---- 第 6 环:验证单元终裁(编译器自证部分;签章由 attest 独立完成) ----
|
|
533
|
+
solid = not res.errors
|
|
534
|
+
res.verdict = {
|
|
535
|
+
"passed": solid,
|
|
536
|
+
"reason": ("候选可提交外部验证:六要素齐备且全部 grounded" if solid
|
|
537
|
+
else (res.errors[0] if res.errors else "未知")),
|
|
538
|
+
"authority": "VERIFICATION_UNIT",
|
|
539
|
+
"requires_attestation": True, # 裁定 A:编译通过 ≠ 可写入,须外部签章
|
|
540
|
+
}
|
|
541
|
+
if solid:
|
|
542
|
+
absent = [m for m in nodefile.CCG_REQUIRED if m not in lines]
|
|
543
|
+
if absent:
|
|
544
|
+
res.warnings.append("六要素尚缺:" + ",".join(absent) + "(link 前须补)")
|
|
545
|
+
res.success = solid
|
|
546
|
+
return res
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
# =============================================================================
|
|
550
|
+
# attest:认知图**外**的验证方签章(裁定 A 的落点)
|
|
551
|
+
# =============================================================================
|
|
552
|
+
|
|
553
|
+
def attest(node_id: str, verdict: str, verifier: str, compiled_by: str, *,
|
|
554
|
+
slot_corrections: Optional[Dict[str, Any]] = None,
|
|
555
|
+
evidence: str = "", cg: Any = None, ledger: bool = True) -> AttestResult:
|
|
556
|
+
"""验证方签章。**验证方必须是编译方之外的一方**(子代理 / 设计者 / 用户)。
|
|
557
|
+
|
|
558
|
+
E041 机械拒绝 `verifier == compiled_by`——LLM 不得自己验证自己。
|
|
559
|
+
这是结构性保证:不依赖 prompt 自觉,也不依赖验证方「愿意」自证。
|
|
560
|
+
DEFER 不构成签章(未定 = 未通过)。
|
|
561
|
+
"""
|
|
562
|
+
res = AttestResult(node_id=str(node_id or "").strip(),
|
|
563
|
+
verifier=str(verifier or "").strip(),
|
|
564
|
+
compiled_by=str(compiled_by or "").strip(),
|
|
565
|
+
state=str(verdict or "").strip(),
|
|
566
|
+
evidence=str(evidence or ""),
|
|
567
|
+
slot_corrections=dict(slot_corrections or {}),
|
|
568
|
+
ts=time.time())
|
|
569
|
+
if not res.node_id:
|
|
570
|
+
res.error = "E002 " + E_CODES["E002"]
|
|
571
|
+
elif not res.verifier:
|
|
572
|
+
res.error = "E003 缺少验证方标识(verifier)"
|
|
573
|
+
elif res.verifier == res.compiled_by:
|
|
574
|
+
res.error = "E041 " + E_CODES["E041"] + "(verifier=" + res.verifier + ")"
|
|
575
|
+
elif res.state not in STATES:
|
|
576
|
+
res.error = ("非法裁决:" + repr(res.state) + "(可选 " + ",".join(STATES) + ")")
|
|
577
|
+
elif res.state != ACCEPT:
|
|
578
|
+
res.error = "E042 " + E_CODES["E042"] + "(state=" + res.state + ")"
|
|
579
|
+
else:
|
|
580
|
+
res.ok = True
|
|
581
|
+
res.token = _sha("|".join([res.node_id, res.verifier, res.state,
|
|
582
|
+
str(res.ts), res.evidence]))
|
|
583
|
+
if ledger:
|
|
584
|
+
_cg = _as_cg(cg)
|
|
585
|
+
if _cg is not None:
|
|
586
|
+
_append_jsonl(_log_path(_cg), {
|
|
587
|
+
"action": "ccgc_attest", "ts": res.ts, "node": res.node_id,
|
|
588
|
+
"verifier": res.verifier, "compiled_by": res.compiled_by,
|
|
589
|
+
"state": res.state, "ok": res.ok, "token": res.token,
|
|
590
|
+
"evidence": res.evidence,
|
|
591
|
+
"slot_corrections": res.slot_corrections, "error": res.error})
|
|
592
|
+
return res
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
# =============================================================================
|
|
596
|
+
# link:签章通过才写入(缺签章恒不写入)
|
|
597
|
+
# =============================================================================
|
|
598
|
+
|
|
599
|
+
def link(compiled: CompileResult, attestation: Optional[AttestResult], *,
|
|
600
|
+
cg: Any = None, apply: bool = False, actor: str = "",
|
|
601
|
+
basis: str = "") -> LinkResult:
|
|
602
|
+
"""把编译产物写入节点。**准入条件 = 有效签章**(裁定 A)。
|
|
603
|
+
|
|
604
|
+
E040 无签章 / E041 自证 / E042 未通过 —— 任一命中即拒绝写入,
|
|
605
|
+
对齐 audit.py「缺能力返回 DEFER,绝不假装通过」。
|
|
606
|
+
"""
|
|
607
|
+
node_id = getattr(compiled, "node_id", "") or ""
|
|
608
|
+
out = LinkResult(node_id=node_id, dry_run=not apply,
|
|
609
|
+
entry_id=_sha("ccgc|" + node_id + "|" + time.strftime("%Y%m%d%H%M%S")))
|
|
610
|
+
if not compiled.success:
|
|
611
|
+
out.errors.append("编译未通过,拒绝写入:" +
|
|
612
|
+
(compiled.errors[0] if compiled.errors else "未知"))
|
|
613
|
+
return out
|
|
614
|
+
if attestation is None:
|
|
615
|
+
out.errors.append("E040 " + E_CODES["E040"])
|
|
616
|
+
return out
|
|
617
|
+
if attestation.node_id != node_id:
|
|
618
|
+
out.errors.append("签章与产物目标不一致:" + attestation.node_id + " != " + node_id)
|
|
619
|
+
return out
|
|
620
|
+
if attestation.verifier and attestation.verifier == (actor or compiled.actor):
|
|
621
|
+
out.errors.append("E041 " + E_CODES["E041"])
|
|
622
|
+
return out
|
|
623
|
+
if not attestation.ok:
|
|
624
|
+
out.errors.append("E042 " + E_CODES["E042"] +
|
|
625
|
+
(("(" + attestation.error + ")") if attestation.error else ""))
|
|
626
|
+
return out
|
|
627
|
+
|
|
628
|
+
_cg = _as_cg(cg)
|
|
629
|
+
if _cg is None:
|
|
630
|
+
out.errors.append("E002 " + E_CODES["E002"] + "(未提供 cg,无法写入)")
|
|
631
|
+
return out
|
|
632
|
+
entry = (_cg.index.get("nodes") or {}).get(node_id)
|
|
633
|
+
if not entry:
|
|
634
|
+
out.errors.append("E002 节点不存在:" + node_id)
|
|
635
|
+
return out
|
|
636
|
+
if not apply:
|
|
637
|
+
out.ok = True
|
|
638
|
+
out.warnings.append("dry-run(apply=False):未写入;签章 token=" + attestation.token)
|
|
639
|
+
return out
|
|
640
|
+
|
|
641
|
+
from . import crypto
|
|
642
|
+
fm, content = _cg._read(entry)
|
|
643
|
+
if fm is None or crypto.is_encrypted(content):
|
|
644
|
+
out.errors.append("E004 " + E_CODES["E004"])
|
|
645
|
+
return out
|
|
646
|
+
|
|
647
|
+
before = {"生效条件": (fm.get("state_attributes") or {}).get("comment", {}).get("生效条件")
|
|
648
|
+
if isinstance((fm.get("state_attributes") or {}).get("comment"), dict) else None}
|
|
649
|
+
new_content = content
|
|
650
|
+
for field_name, value in compiled.lines.items():
|
|
651
|
+
new_content = _upsert_ccg_line(new_content, field_name, value)
|
|
652
|
+
|
|
653
|
+
fm["condition_space"] = compiled.slots
|
|
654
|
+
fm["verification_basis"] = basis or compiled.sources.get("verification_basis") or "other"
|
|
655
|
+
if compiled.lines.get("不适用条件"):
|
|
656
|
+
fm["non_applicable_conditions"] = [
|
|
657
|
+
s.strip() for s in re.split(r"[;;]", compiled.lines["不适用条件"]) if s.strip()]
|
|
658
|
+
st = fm.get("state_attributes")
|
|
659
|
+
if not isinstance(st, dict):
|
|
660
|
+
st = {}
|
|
661
|
+
fm["state_attributes"] = st
|
|
662
|
+
comment = st.get("comment")
|
|
663
|
+
if not isinstance(comment, dict):
|
|
664
|
+
comment = {}
|
|
665
|
+
st["comment"] = comment
|
|
666
|
+
if compiled.lines.get("生效条件"):
|
|
667
|
+
comment["生效条件"] = compiled.lines["生效条件"]
|
|
668
|
+
|
|
669
|
+
_cg._write_node(node_id, os.path.join(_cg.root, entry["path"]), fm,
|
|
670
|
+
new_content, durable=True)
|
|
671
|
+
_append_jsonl(_log_path(_cg), {
|
|
672
|
+
"action": "ccgc_link", "ts": time.time(), "node": node_id,
|
|
673
|
+
"actor": actor or compiled.actor, "verifier": attestation.verifier,
|
|
674
|
+
"token": attestation.token, "entry_id": out.entry_id,
|
|
675
|
+
"fields": {k: {"after": v, "source": compiled.sources.get(k, "")}
|
|
676
|
+
for k, v in compiled.lines.items()},
|
|
677
|
+
"slots": compiled.slots, "before": before,
|
|
678
|
+
"content_hash_after": nodefile.content_hash(new_content)})
|
|
679
|
+
_cg.rebuild_index()
|
|
680
|
+
out.ok = True
|
|
681
|
+
out.written = len(compiled.lines)
|
|
682
|
+
return out
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
# =============================================================================
|
|
686
|
+
# recalibrate:用后续验证修正生效条件(裁定 A 的闭环)
|
|
687
|
+
# =============================================================================
|
|
688
|
+
|
|
689
|
+
def recalibrate(node_id: str, corrections: Dict[str, Any], verifier: str,
|
|
690
|
+
compiled_by: str, *, evidence: str = "", cg: Any = None,
|
|
691
|
+
apply: bool = False) -> LinkResult:
|
|
692
|
+
"""用验证方给出的**四槽修正**重合成生效条件(唯一入口重算,可回滚)。
|
|
693
|
+
|
|
694
|
+
E043:只允许修正 condition_space 四槽——生效条件行本身不是修正入口,
|
|
695
|
+
否则会绕开「四槽 → 合成」的唯一通路,退回到手写条件的老问题。
|
|
696
|
+
修正须由编译方**之外**的一方给出(E041)。
|
|
697
|
+
"""
|
|
698
|
+
out = LinkResult(node_id=str(node_id or "").strip(), dry_run=not apply,
|
|
699
|
+
entry_id=_sha("ccgc_recal|" + str(node_id) + "|" + str(time.time())))
|
|
700
|
+
if not out.node_id:
|
|
701
|
+
out.errors.append("E002 " + E_CODES["E002"])
|
|
702
|
+
return out
|
|
703
|
+
if verifier and verifier == compiled_by:
|
|
704
|
+
out.errors.append("E041 " + E_CODES["E041"])
|
|
705
|
+
return out
|
|
706
|
+
if not corrections:
|
|
707
|
+
out.errors.append("E043 " + E_CODES["E043"] + "(未给出任何修正)")
|
|
708
|
+
return out
|
|
709
|
+
allowed = {k for k, _l in nodefile.CONDITION_SLOTS}
|
|
710
|
+
bad = [k for k in corrections if k not in allowed]
|
|
711
|
+
if bad:
|
|
712
|
+
out.errors.append("E043 " + E_CODES["E043"] + "(非法键 " + ",".join(bad) + ")")
|
|
713
|
+
return out
|
|
714
|
+
|
|
715
|
+
_cg = _as_cg(cg)
|
|
716
|
+
if _cg is None:
|
|
717
|
+
out.errors.append("E002 " + E_CODES["E002"] + "(未提供 cg)")
|
|
718
|
+
return out
|
|
719
|
+
entry = (_cg.index.get("nodes") or {}).get(out.node_id)
|
|
720
|
+
if not entry:
|
|
721
|
+
out.errors.append("E002 节点不存在:" + out.node_id)
|
|
722
|
+
return out
|
|
723
|
+
|
|
724
|
+
from . import crypto
|
|
725
|
+
fm, content = _cg._read(entry)
|
|
726
|
+
if fm is None or crypto.is_encrypted(content):
|
|
727
|
+
out.errors.append("E004 " + E_CODES["E004"])
|
|
728
|
+
return out
|
|
729
|
+
old_cs = dict(fm.get("condition_space") or {})
|
|
730
|
+
new_cs = dict(old_cs)
|
|
731
|
+
new_cs.update(corrections)
|
|
732
|
+
new_text = nodefile.condition_space_text(new_cs)
|
|
733
|
+
if not new_text:
|
|
734
|
+
out.errors.append("E020 " + E_CODES["E020"] +
|
|
735
|
+
"(修正后仍缺 " + ",".join(nodefile.condition_space_missing(new_cs)) + ")")
|
|
736
|
+
return out
|
|
737
|
+
out.warnings.append("修正后生效条件:" + new_text)
|
|
738
|
+
if not apply:
|
|
739
|
+
out.ok = True
|
|
740
|
+
return out
|
|
741
|
+
|
|
742
|
+
new_content = _upsert_ccg_line(content, "生效条件", new_text)
|
|
743
|
+
fm["condition_space"] = new_cs
|
|
744
|
+
_cg._write_node(out.node_id, os.path.join(_cg.root, entry["path"]), fm,
|
|
745
|
+
new_content, durable=True)
|
|
746
|
+
_append_jsonl(_log_path(_cg), {
|
|
747
|
+
"action": "ccgc_recalibrate", "ts": time.time(), "node": out.node_id,
|
|
748
|
+
"verifier": verifier, "compiled_by": compiled_by, "evidence": evidence,
|
|
749
|
+
"entry_id": out.entry_id,
|
|
750
|
+
"slots_before": old_cs, "slots_after": new_cs,
|
|
751
|
+
"condition_before": nodefile.condition_space_text(old_cs),
|
|
752
|
+
"condition_after": new_text})
|
|
753
|
+
_cg.rebuild_index()
|
|
754
|
+
out.ok = True
|
|
755
|
+
out.written = 1
|
|
756
|
+
return out
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
# =============================================================================
|
|
760
|
+
# pending:三段式在「跨调用」形态下的持久化(候选与签章绑定,防篡改)
|
|
761
|
+
# =============================================================================
|
|
762
|
+
#
|
|
763
|
+
# 为什么需要:compile / attest / link 在 MCP 面上是**三次独立调用**(签章必须由
|
|
764
|
+
# 认知图**外**的一方在别处完成),候选对象无法跨调用传递。若让调用方把候选 JSON
|
|
765
|
+
# 原样回传,改写「六行」不会被任何检查发现(签章 token 只覆盖 node/verifier/
|
|
766
|
+
# state/evidence,不含候选内容)。因此候选落盘到 `_ccgc_pending/`(LAYERS 之外、
|
|
767
|
+
# `.json` 后缀 → 索引双隐身的冷区),并对其内容取 hash;link 时校验 hash,
|
|
768
|
+
# 不一致即拒绝写入。
|
|
769
|
+
#
|
|
770
|
+
# 纪律:pending 是**暂存**不是存档——link 成功即删除;留痕在 _ccgc.jsonl。
|
|
771
|
+
|
|
772
|
+
PENDING_DIR = "_ccgc_pending"
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def _safe_name(node_id: str) -> str:
|
|
776
|
+
"""node_id → 文件名安全形态(分支节点形如 mem_x@br1,须剥掉非 [A-Za-z0-9_.-])。"""
|
|
777
|
+
return re.sub(r"[^0-9A-Za-z_.\-]", "-", str(node_id or ""))[:80] or "unnamed"
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def pending_path(cg, node_id: str) -> str:
|
|
781
|
+
_cg = _as_cg(cg)
|
|
782
|
+
root = getattr(_cg, "root", None)
|
|
783
|
+
if not root:
|
|
784
|
+
return ""
|
|
785
|
+
return os.path.join(str(root), PENDING_DIR, _safe_name(node_id) + ".json")
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
def _payload_hash(lines, slots) -> str:
|
|
789
|
+
"""候选内容摘要(六行 + 四槽,键序无关)——签章与产物的绑定依据。"""
|
|
790
|
+
return _sha(json.dumps({"lines": lines or {}, "slots": slots or {}},
|
|
791
|
+
ensure_ascii=False, sort_keys=True))
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
def save_pending(cg, compiled: CompileResult,
|
|
795
|
+
attestation: Optional[AttestResult] = None) -> dict:
|
|
796
|
+
"""候选(+ 可选签章)落 pending;返回 {ok, path, hash}。"""
|
|
797
|
+
_cg = _as_cg(cg)
|
|
798
|
+
if _cg is None:
|
|
799
|
+
return {"ok": False, "error": "未提供 cg"}
|
|
800
|
+
p = pending_path(_cg, compiled.node_id)
|
|
801
|
+
if not p:
|
|
802
|
+
return {"ok": False, "error": "无法定位 pending 目录"}
|
|
803
|
+
rec = {"node_id": compiled.node_id, "actor": compiled.actor, "ts": time.time(),
|
|
804
|
+
"hash": _payload_hash(compiled.lines, compiled.slots),
|
|
805
|
+
"compiled": asdict(compiled),
|
|
806
|
+
"attest": asdict(attestation) if attestation is not None else None}
|
|
807
|
+
try:
|
|
808
|
+
os.makedirs(os.path.dirname(p), exist_ok=True)
|
|
809
|
+
with open(p, "w", encoding="utf-8") as f:
|
|
810
|
+
json.dump(rec, f, ensure_ascii=False, indent=1)
|
|
811
|
+
except OSError as exc:
|
|
812
|
+
return {"ok": False, "error": "%s: %s" % (type(exc).__name__, exc), "path": p}
|
|
813
|
+
return {"ok": True, "path": p, "hash": rec["hash"]}
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def load_pending(cg, node_id: str) -> dict:
|
|
817
|
+
"""读回候选与签章,并校验 hash(内容被改即 hash_ok=False,调用方须拒绝写入)。"""
|
|
818
|
+
_cg = _as_cg(cg)
|
|
819
|
+
out = {"ok": False, "compiled": None, "attest": None, "hash_ok": False,
|
|
820
|
+
"path": "", "error": ""}
|
|
821
|
+
if _cg is None:
|
|
822
|
+
out["error"] = "未提供 cg"
|
|
823
|
+
return out
|
|
824
|
+
p = pending_path(_cg, node_id)
|
|
825
|
+
out["path"] = p
|
|
826
|
+
if not p or not os.path.isfile(p):
|
|
827
|
+
out["error"] = "未找到待写入的编译产物(pending):" + str(node_id)
|
|
828
|
+
return out
|
|
829
|
+
try:
|
|
830
|
+
with open(p, encoding="utf-8") as f:
|
|
831
|
+
rec = json.load(f)
|
|
832
|
+
except (OSError, ValueError) as exc:
|
|
833
|
+
out["error"] = "pending 不可读:%s" % type(exc).__name__
|
|
834
|
+
return out
|
|
835
|
+
cfields = {f.name for f in dataclasses.fields(CompileResult)}
|
|
836
|
+
afields = {f.name for f in dataclasses.fields(AttestResult)}
|
|
837
|
+
c = CompileResult(**{k: v for k, v in (rec.get("compiled") or {}).items()
|
|
838
|
+
if k in cfields})
|
|
839
|
+
a = None
|
|
840
|
+
if rec.get("attest"):
|
|
841
|
+
a = AttestResult(**{k: v for k, v in rec["attest"].items() if k in afields})
|
|
842
|
+
out.update({"compiled": c, "attest": a,
|
|
843
|
+
"hash_ok": rec.get("hash") == _payload_hash(c.lines, c.slots)})
|
|
844
|
+
out["ok"] = True
|
|
845
|
+
return out
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
def drop_pending(cg, node_id: str) -> bool:
|
|
849
|
+
"""删除 pending(link 成功后调用;失败静默——待办件不是事实,无需强保证)。"""
|
|
850
|
+
p = pending_path(cg, node_id)
|
|
851
|
+
if not p:
|
|
852
|
+
return False
|
|
853
|
+
try:
|
|
854
|
+
os.remove(p)
|
|
855
|
+
return True
|
|
856
|
+
except OSError:
|
|
857
|
+
return False
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
def link_pending(cg, node_id: str, *, apply: bool = False, actor: str = "",
|
|
861
|
+
basis: str = "") -> LinkResult:
|
|
862
|
+
"""从 pending 读回候选与签章后 link:hash 校验 → 签章准入 → 写入 → 清理 pending。"""
|
|
863
|
+
out = LinkResult(node_id=str(node_id or "").strip(), dry_run=not apply)
|
|
864
|
+
got = load_pending(cg, node_id)
|
|
865
|
+
if not got.get("ok"):
|
|
866
|
+
out.errors.append(got.get("error") or "pending 不可用")
|
|
867
|
+
return out
|
|
868
|
+
if not got.get("hash_ok"):
|
|
869
|
+
out.errors.append("pending 产物与编译时不一致(hash 不符),拒绝写入——"
|
|
870
|
+
"候选落盘后不得被改写,请重新编译")
|
|
871
|
+
return out
|
|
872
|
+
res = link(got["compiled"], got.get("attest"), cg=cg, apply=apply,
|
|
873
|
+
actor=actor, basis=basis)
|
|
874
|
+
if res.ok and apply:
|
|
875
|
+
drop_pending(cg, node_id)
|
|
876
|
+
return res
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
__all__ = ["ACCEPT", "REJECT", "DEFER", "BLINDSPOT", "STATES", "CONTRACT_ROLES",
|
|
880
|
+
"E_CODES", "CompileResult", "AttestResult", "LinkResult", "RuleParser",
|
|
881
|
+
"compile_dialog", "attest", "link", "recalibrate",
|
|
882
|
+
"PENDING_DIR", "pending_path", "save_pending", "load_pending",
|
|
883
|
+
"drop_pending", "link_pending"]
|
|
884
|
+
|