@furongjun1999/dsh-memory 0.4.8 → 0.4.10
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 +54 -26
- package/codebuddy/CODEBUDDY.md +196 -195
- package/codebuddy/README.md +13 -1
- package/codebuddy/mcp.json +9 -0
- package/docs/GBrain/345/217/257/345/200/237/351/211/264/347/202/271_/347/201/265/346/236/242/350/220/275/347/202/271/344/272/244/346/216/245_20260919.md +169 -0
- package/docs/README.md +1 -1
- package/docs/discipline/harnesses.yaml +18 -7
- package/docs/discipline/templates/full.md.tmpl +4 -3
- package/docs/experiments/linkref_backfill/candidates_20260917.json +726 -0
- package/docs/experiments/linkref_backfill/candidates_internal_20260917.json +602 -0
- package/docs/experiments/linkref_backfill/candidates_internal_v2.json +603 -0
- package/docs/experiments/linkref_backfill/candidates_secret_20260917.json +884 -0
- package/docs/experiments/linkref_backfill/candidates_secret_v2.json +789 -0
- package/docs/hive//345/244/232/347/253/257harness/351/200/232/344/277/241/345/245/221/347/272/246_v0.1.md +42 -0
- package/docs/hive//346/243/200/347/264/242/346/224/266/346/225/233/345/256/236/346/265/213/344/270/216S1b/350/256/276/350/256/241_v0.1.md +43 -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 +102 -0
- package/docs/hive//347/234/237/345/256/236/345/272/223/347/253/257/345/210/260/347/253/257/345/256/236/346/265/213_S1b/344/270/216/345/217/254/345/233/236/346/235/203/350/241/241_v0.1.md +57 -0
- package/docs/hive//347/234/237/345/256/236/345/272/223/347/253/257/345/210/260/347/253/257/345/256/236/346/265/213_S7/345/200/222/346/216/222/345/200/231/351/200/211/345/261/202_v0.1.md +126 -0
- package/docs/hive//350/234/202/345/267/242/345/217/214/345/256/236/344/276/213/344/272/222/351/252/214_/350/256/276/350/256/241/345/256/232/347/250/277.md +503 -0
- package/docs/mdcg/D_meta_/345/267/245/347/250/213/345/214/226/346/226/271/346/241/210_v0.2.md +216 -0
- package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.5.md +631 -625
- package/docs/mdcg//344/273/243/347/240/201/350/257/204/345/256/241/344/270/216/346/235/241/344/273/266/345/214/226/346/263/250/351/207/212_/345/245/221/347/272/246_v0.1.md +82 -0
- package/docs/mdcg//345/205/250/345/272/223/344/273/243/347/240/201/350/257/204/345/256/241/344/270/216/346/235/241/344/273/266/345/214/226/346/263/250/351/207/212_/350/256/241/345/210/222_v0.1.md +600 -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 +47 -45
- 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 → 206_v0.5.md} +92 -4
- 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 +172 -0
- package/docs/mdcg//347/216/257/344/272/214_/347/231/275/347/256/261/345/241/253/345/205/205/346/265/201/346/260/264/347/272/277_v0.1.md +31 -0
- package/docs/mdcg//350/267/250/347/253/257/351/252/214/350/257/201/344/270/216/345/220/214/346/255/245/345/215/217/350/256/256_v0.1.md +93 -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 +2 -2
- 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//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 +2 -2
- 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 +434 -433
- package/docs//347/201/265/346/236/242/350/207/252/346/210/221/346/224/271/350/277/233/345/267/245/344/275/234/350/256/241/345/210/222_/345/244/226/351/203/250/347/240/224/347/251/266/347/263/273/345/210/227/345/220/270/346/224/266_v1_20260919.md +286 -0
- package/dsh/README.md +33 -0
- package/dsh/cordis.yml.example +13 -7
- package/dsh/hive-mcp-probe.mjs +94 -0
- package/dsh/hive-mcp.example.yml +62 -0
- package/dsh/update-lingshu.bat +11 -0
- package/dsh/update-lingshu.ps1 +337 -0
- package/lib/hooks.d.ts +3 -0
- package/lib/hooks.js +17 -23
- package/lib/index.d.ts +4 -2
- package/lib/index.js +29 -6
- package/lib/lib/datapath.d.ts +76 -1
- package/lib/lib/datapath.js +199 -13
- package/lib/lib/mdcg_client.d.ts +40 -3
- package/lib/lib/mdcg_client.js +46 -22
- package/lib/lib/mutual.js +4 -4
- package/lib/lib/token_store.js +4 -5
- package/md_cg/audit.py +17 -2
- package/md_cg/autonomy.py +86 -15
- package/md_cg/backfill.py +36 -1
- package/md_cg/backfill_bigdomain.py +34 -0
- package/md_cg/bench6_arms.py +28 -1
- package/md_cg/bench6_common.py +10 -1
- package/md_cg/bench6_competitors.py +6 -1
- package/md_cg/bench_axis_domain.py +9 -1
- package/md_cg/bench_blind_comp.py +7 -1
- package/md_cg/bench_en_atoms_public.py +9 -0
- package/md_cg/bench_governance.py +348 -0
- package/md_cg/bench_lme_zh.py +16 -1
- package/md_cg/bench_locomo.py +2 -1
- package/md_cg/bench_locomo_zh.py +16 -1
- package/md_cg/bench_locomo_zh_public.py +4 -1
- package/md_cg/bench_longmem.py +2 -1
- package/md_cg/bench_membench.py +27 -1
- package/md_cg/bench_p0.py +4 -1
- package/md_cg/bench_progressive.py +13 -1
- package/md_cg/bench_role_views.py +238 -0
- package/md_cg/bench_task_ab.py +8 -1
- package/md_cg/bench_task_ab_llm.py +13 -1
- package/md_cg/bench_unified_en.py +6 -1
- package/md_cg/bench_zh_mad.py +20 -1
- package/md_cg/blindspot_tickets.py +123 -0
- package/md_cg/branches.py +12 -1
- package/md_cg/build_postings.py +73 -0
- package/md_cg/ccgc.py +67 -2
- package/md_cg/census.py +5 -1
- package/md_cg/chain.py +24 -3
- package/md_cg/codeindex.py +134 -17
- package/md_cg/coldverify.py +265 -0
- package/md_cg/comment_gate.py +338 -0
- package/md_cg/cond_compose.py +190 -0
- package/md_cg/cond_facts.py +155 -0
- package/md_cg/cond_template.json +107 -0
- package/md_cg/condition_anchor.py +143 -0
- package/md_cg/conformance.py +69 -4
- package/md_cg/consistency.py +24 -1
- package/md_cg/consolidate.py +53 -2
- package/md_cg/corpus.py +4 -0
- package/md_cg/crosscheck.py +42 -2
- package/md_cg/crypto.py +35 -1
- package/md_cg/d_meta.py +310 -0
- package/md_cg/datapath.py +201 -26
- package/md_cg/docindex.py +122 -1
- package/md_cg/eval_common.py +29 -1
- package/md_cg/evidence.py +27 -1
- package/md_cg/evolution.py +21 -1
- package/md_cg/export.py +11 -1
- package/md_cg/forgetting.py +23 -1
- package/md_cg/fsutil.py +18 -1
- package/md_cg/hotcache.py +214 -0
- package/md_cg/hyperedge.py +251 -0
- package/md_cg/identity.py +18 -1
- package/md_cg/insight.py +17 -1
- package/md_cg/lexicon/build_cedict_en_zh.py +9 -0
- package/md_cg/lexicon/build_standard_en.py +171 -168
- package/md_cg/lexicon/expand_en_zh.py +6 -0
- package/md_cg/lifecycle.py +12 -1
- package/md_cg/linkref.py +281 -0
- package/md_cg/links.py +29 -1
- package/md_cg/mcp_server.py +362 -43
- package/md_cg/md_whitebox.py +53 -1
- package/md_cg/mdcg.py +1003 -27
- package/md_cg/mdcos.py +558 -36
- package/md_cg/metacognition.py +37 -2
- package/md_cg/migrate.py +4 -0
- package/md_cg/migrate_aeis.py +221 -213
- package/md_cg/migrate_roleplay.py +8 -0
- package/md_cg/migrate_wisdom_graph.py +14 -1
- package/md_cg/mreview/__main__.py +3 -0
- package/md_cg/mreview/bundle.py +8 -0
- package/md_cg/mreview/candidates.py +9 -0
- package/md_cg/mreview/govern.py +21 -1
- package/md_cg/mreview/locate.py +34 -0
- package/md_cg/mreview/pipeline.py +29 -1
- package/md_cg/mreview/ruleset.py +16 -1
- package/md_cg/nodefile.py +233 -3
- package/md_cg/pooling.py +23 -1
- package/md_cg/postings.py +298 -0
- package/md_cg/predict.py +89 -9
- package/md_cg/progressive.py +3 -0
- package/md_cg/protect.py +14 -1
- package/md_cg/protocol.py +372 -0
- package/md_cg/provenance.py +262 -0
- package/md_cg/reach.py +453 -0
- package/md_cg/refindex.py +47 -2
- package/md_cg/refine.py +20 -1
- package/md_cg/roleviews.py +89 -0
- package/md_cg/routing.py +76 -0
- package/md_cg/scrub.py +63 -2
- package/md_cg/security.py +26 -1
- package/md_cg/self_state.py +64 -1
- package/md_cg/selfreport.py +151 -0
- package/md_cg/semantic/canonical.py +5 -0
- package/md_cg/semantic/en_normalizer.py +364 -355
- package/md_cg/semantic/zh_en_atoms.py +139 -136
- package/md_cg/signer.py +41 -1
- package/md_cg/sources.py +583 -547
- package/md_cg/statushdr.py +179 -0
- package/md_cg/stg.py +59 -18
- package/md_cg/subgraph.py +23 -0
- package/md_cg/sustain.py +56 -1
- package/md_cg/tasks.py +26 -2
- package/md_cg/test_autonomy.py +26 -0
- package/md_cg/test_bench_governance.py +102 -0
- package/md_cg/test_blindspot_tickets.py +166 -0
- package/md_cg/test_ccgc.py +10 -0
- package/md_cg/test_codeindex.py +338 -0
- package/md_cg/test_comment_gate.py +187 -0
- package/md_cg/test_cond_compose_anchors.py +76 -0
- package/md_cg/test_condition_anchor.py +82 -0
- package/md_cg/test_d_meta.py +412 -0
- package/md_cg/test_datapath_root.py +188 -0
- package/md_cg/test_gain_gate.py +47 -1
- package/md_cg/test_hot_cold.py +187 -0
- package/md_cg/test_hyperedge.py +245 -0
- package/md_cg/test_linkref.py +306 -0
- package/md_cg/test_md_access_parity.py +15 -3
- package/md_cg/test_mr_m1.py +108 -18
- package/md_cg/test_mr_m3.py +8 -1
- package/md_cg/test_p26_refindex.py +49 -20
- package/md_cg/test_p27_docindex.py +236 -2
- package/md_cg/test_p2_mcp.py +1 -1
- package/md_cg/test_p31_insight.py +24 -0
- package/md_cg/test_p44_md_whitebox.py +14 -1
- package/md_cg/test_protocol.py +243 -0
- package/md_cg/test_reach.py +378 -0
- package/md_cg/test_reach_keys.py +201 -0
- package/md_cg/test_reach_meta_exits.py +145 -0
- package/md_cg/test_read_clip.py +8 -4
- package/md_cg/test_retr_s1.py +340 -0
- package/md_cg/test_retr_s1b.py +209 -0
- package/md_cg/test_retr_s3.py +194 -0
- package/md_cg/test_retr_s4.py +163 -0
- package/md_cg/test_retr_s5.py +200 -0
- package/md_cg/test_retr_s6.py +157 -0
- package/md_cg/test_retr_s7.py +385 -0
- package/md_cg/test_retr_s8_time.py +316 -0
- package/md_cg/test_retr_s9_edges.py +286 -0
- package/md_cg/test_retr_s9_entity_ctx.py +175 -0
- package/md_cg/test_review_conformance.py +59 -2
- package/md_cg/test_role_views.py +354 -0
- package/md_cg/test_subproc_encoding.py +188 -0
- package/md_cg/test_trust.py +361 -0
- package/md_cg/test_units_poll.py +71 -0
- package/md_cg/test_v14_fixes.py +397 -0
- package/md_cg/test_validity_filter.py +280 -0
- package/md_cg/test_wisdom_md_store.py +7 -3
- package/md_cg/test_writepipe.py +5 -1
- package/md_cg/theory.py +16 -1
- package/md_cg/tokens.py +40 -8
- package/md_cg/tool_face.py +13 -2
- package/md_cg/trust.py +943 -0
- package/md_cg/twophase.py +12 -1
- package/md_cg/units.py +132 -10
- package/md_cg/vision_evidence.py +24 -1
- package/md_cg/weights.py +24 -1
- package/md_cg/whitebox.py +32 -1
- package/md_cg/whitebox_kb/data/verify_cache.json +21210 -365
- package/md_cg/whitebox_kb/data/verify_savings.jsonl +5078 -0
- package/md_cg/whitebox_kb/wisdom/audit_log/chain_heat.json +10 -10
- package/md_cg/whitebox_kb/wisdom/code_compose.py +113 -6
- package/md_cg/whitebox_kb/wisdom/code_solidified.json +1 -1
- package/md_cg/whitebox_kb/wisdom/verifier.py +340 -55
- package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db +0 -0
- package/md_cg/writelimit.py +18 -4
- package/md_cg/writepipe.py +178 -7
- package/package.json +2 -2
- package/skills/skills/designer-perspective/scripts/__pycache__/designer.cpython-310.pyc +0 -0
- package/skills/skills/designer-perspective/scripts/designer.py +17 -1
- package/skills/skills/designer-perspective/tests/selftest.py +3 -1
- package/src/hooks.ts +17 -21
- package/src/index.ts +33 -6
- package/src/lib/datapath.ts +211 -13
- package/src/lib/mdcg_client.ts +64 -25
- package/src/lib/mutual.ts +411 -411
- package/src/lib/token_store.ts +4 -5
- package/zcode/AGENTS.md +196 -195
- package/zcode/README.md +4 -0
- 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
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""md_cg · 状态摘要协议(MCP 输出面一等公民)
|
|
3
|
+
|
|
4
|
+
【为什么】验证态若只在 frontmatter / 索引里躺着,读面就得「先知道去哪查」——
|
|
5
|
+
与纪律只存在于认知图时执行率近零同构。故把状态**推进每一次返回体**:
|
|
6
|
+
人类与 agent 读面一致,默认开启。
|
|
7
|
+
|
|
8
|
+
四符号(`HEAD_MARKS`):
|
|
9
|
+
✓ 已验证 证据在位、依赖未动、时效内
|
|
10
|
+
△ 已修改 复核中(上游动过,复核进行态)
|
|
11
|
+
! 异常 已过期(时效失效)或依赖目标悬空——**地基动了**
|
|
12
|
+
? 存疑 依赖变动后被自动标记,待复核
|
|
13
|
+
(未验证补 `·`:不是四态之一,但读面需要一个「尚未验证」的可视锚点。)
|
|
14
|
+
|
|
15
|
+
【怎么做】在 `_cg_call` **单一出口**统一挂 `status_head` 并升级既有 `hint`——
|
|
16
|
+
29+ 个 op 分支一处不改,爆炸半径最小。注入仅对返回体中**实际出现的节点 id**
|
|
17
|
+
计算(O(k),受 `k` 与 MAX_IDS 约束),不做全库扫描。
|
|
18
|
+
|
|
19
|
+
【关闭】`MDCG_STATUS_HEAD=0` 回退旧 hint 文本(灰度/排障用)。
|
|
20
|
+
|
|
21
|
+
零第三方依赖(D-005)。
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import os
|
|
26
|
+
|
|
27
|
+
from . import trust
|
|
28
|
+
|
|
29
|
+
#: 四符号(读面契约,勿随意改字形——agent 侧提示词按此对齐)
|
|
30
|
+
MARK_VERIFIED = "✓"
|
|
31
|
+
MARK_MODIFIED = "△"
|
|
32
|
+
MARK_ABNORMAL = "!"
|
|
33
|
+
MARK_DOUBTED = "?"
|
|
34
|
+
MARK_UNVERIFIED = "·"
|
|
35
|
+
HEAD_MARKS = (MARK_VERIFIED, MARK_MODIFIED, MARK_ABNORMAL, MARK_DOUBTED)
|
|
36
|
+
|
|
37
|
+
ENV = "MDCG_STATUS_HEAD"
|
|
38
|
+
#: 注入上限:单次返回体最多为这么多节点计算状态头(防大结果集拖慢出口)
|
|
39
|
+
MAX_IDS = 50
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def enabled() -> bool:
|
|
43
|
+
"""状态头开关(默认开启;`MDCG_STATUS_HEAD=0` 回退旧 hint 文本)。"""
|
|
44
|
+
v = (os.environ.get(ENV) or "").strip().lower()
|
|
45
|
+
return v not in ("0", "false", "no", "off")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _mark(state: str, valid_kind: str, dangling: bool) -> str:
|
|
49
|
+
if dangling or valid_kind == "expired" or state == "expired":
|
|
50
|
+
return MARK_ABNORMAL
|
|
51
|
+
if state == "doubted":
|
|
52
|
+
return MARK_DOUBTED
|
|
53
|
+
if state == "rechecking":
|
|
54
|
+
return MARK_MODIFIED
|
|
55
|
+
if state == "verified":
|
|
56
|
+
return MARK_VERIFIED
|
|
57
|
+
return MARK_UNVERIFIED
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _label(mark: str) -> str:
|
|
61
|
+
return {MARK_VERIFIED: "已验证", MARK_MODIFIED: "已修改",
|
|
62
|
+
MARK_ABNORMAL: "异常", MARK_DOUBTED: "存疑",
|
|
63
|
+
MARK_UNVERIFIED: "未验证"}.get(mark, "未验证")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def render(cg, node_id: str, *, node: dict = None, index_entry: dict = None,
|
|
67
|
+
now: float = None) -> str:
|
|
68
|
+
"""单条状态头字符串(附方式 / 时间 / 影响范围 / 待确认条件)。只读不抛。"""
|
|
69
|
+
try:
|
|
70
|
+
e = index_entry
|
|
71
|
+
if e is None:
|
|
72
|
+
e = ((getattr(cg, "index", None) or {}).get("nodes") or {}).get(node_id)
|
|
73
|
+
fm = (node or {}).get("frontmatter") if node else None
|
|
74
|
+
if fm is None:
|
|
75
|
+
fm = e or {}
|
|
76
|
+
state = trust.state_of(fm)
|
|
77
|
+
kind, _s, _e = trust.validity(fm, now=now)
|
|
78
|
+
deps = trust.as_deps(fm.get(trust.DEPS_FIELD))
|
|
79
|
+
known = set((getattr(cg, "index", None) or {}).get("nodes") or {})
|
|
80
|
+
dangling = [p for p in deps if p not in known]
|
|
81
|
+
mark = _mark(state, kind, bool(dangling))
|
|
82
|
+
bits = []
|
|
83
|
+
if dangling:
|
|
84
|
+
bits.append(f"依赖目标缺失:{','.join(dangling[:3])}")
|
|
85
|
+
if kind == "expired":
|
|
86
|
+
bits.append("时效已过")
|
|
87
|
+
elif kind == "not_yet":
|
|
88
|
+
bits.append("尚未生效")
|
|
89
|
+
method = (fm.get("verification_method") or fm.get("verification_basis")
|
|
90
|
+
or "")
|
|
91
|
+
if method and mark == MARK_VERIFIED:
|
|
92
|
+
bits.append(f"方式={str(method)[:40]}")
|
|
93
|
+
if mark == MARK_DOUBTED:
|
|
94
|
+
bits.append("上游变动,待复核")
|
|
95
|
+
if mark in (MARK_VERIFIED, MARK_MODIFIED) and fm.get("verified_at"):
|
|
96
|
+
try:
|
|
97
|
+
import time as _t
|
|
98
|
+
bits.append("验证于 " + _t.strftime(
|
|
99
|
+
"%Y-%m-%d", _t.localtime(float(fm["verified_at"]))))
|
|
100
|
+
except Exception: # noqa: BLE001
|
|
101
|
+
pass
|
|
102
|
+
return f"{mark} {_label(mark)}" + (f"({';'.join(bits)})" if bits else "")
|
|
103
|
+
except Exception as exc: # noqa: BLE001
|
|
104
|
+
return f"{MARK_UNVERIFIED} 未验证(状态头降级:{type(exc).__name__})"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
#: 返回体中可能承载节点 id 的键(显式枚举,避免把任意字符串误当 id)
|
|
108
|
+
_ID_KEYS = ("node_id", "id", "nid")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _collect_ids(obj, acc: list, depth: int = 0) -> None:
|
|
112
|
+
"""递归收集候选节点 id(深度/数量双上限;找不到就不找,绝不猜)。"""
|
|
113
|
+
if len(acc) >= MAX_IDS or depth > 4:
|
|
114
|
+
return
|
|
115
|
+
if isinstance(obj, dict):
|
|
116
|
+
for k, v in obj.items():
|
|
117
|
+
if k in _ID_KEYS and isinstance(v, str) and v:
|
|
118
|
+
acc.append(v)
|
|
119
|
+
elif isinstance(v, (dict, list)):
|
|
120
|
+
_collect_ids(v, acc, depth + 1)
|
|
121
|
+
if len(acc) >= MAX_IDS:
|
|
122
|
+
return
|
|
123
|
+
elif isinstance(obj, list):
|
|
124
|
+
for it in obj:
|
|
125
|
+
_collect_ids(it, acc, depth + 1)
|
|
126
|
+
if len(acc) >= MAX_IDS:
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def attach_heads(cg, out: dict, *, now: float = None) -> dict:
|
|
131
|
+
"""扫描返回体中的节点 id,批量挂 `status_head`(O(k),幂等)。"""
|
|
132
|
+
if not isinstance(out, dict):
|
|
133
|
+
return out
|
|
134
|
+
ids, seen = [], set()
|
|
135
|
+
_collect_ids(out, ids)
|
|
136
|
+
known = (getattr(cg, "index", None) or {}).get("nodes") or {}
|
|
137
|
+
heads = {}
|
|
138
|
+
for nid in ids:
|
|
139
|
+
if nid in seen or nid not in known:
|
|
140
|
+
continue
|
|
141
|
+
seen.add(nid)
|
|
142
|
+
heads[nid] = render(cg, nid, index_entry=known.get(nid), now=now)
|
|
143
|
+
if not heads:
|
|
144
|
+
return out
|
|
145
|
+
out["status_heads"] = heads
|
|
146
|
+
out["status_head"] = (next(iter(heads.values())) if len(heads) == 1
|
|
147
|
+
else f"{len(heads)} 节点(见 status_heads)")
|
|
148
|
+
return out
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def annotate(cg, out: dict, *, now: float = None) -> dict:
|
|
152
|
+
"""**单出口**注入口:挂状态头 + 把既有 `hint` 升级为状态头格式。
|
|
153
|
+
|
|
154
|
+
幂等:已注入过(`status_head` 在返回体中)则不再重复改写 hint。
|
|
155
|
+
"""
|
|
156
|
+
if not enabled() or not isinstance(out, dict):
|
|
157
|
+
return out
|
|
158
|
+
if "status_head" in out:
|
|
159
|
+
return out
|
|
160
|
+
before_hint = out.get("hint")
|
|
161
|
+
attach_heads(cg, out, now=now)
|
|
162
|
+
head = out.get("status_head")
|
|
163
|
+
if head and before_hint:
|
|
164
|
+
out["hint"] = f"{head} · {before_hint}"
|
|
165
|
+
elif head:
|
|
166
|
+
out["hint"] = head
|
|
167
|
+
return out
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def catalog() -> dict:
|
|
171
|
+
"""自描述(供 MCP catalog / 人工核对)。"""
|
|
172
|
+
return {
|
|
173
|
+
"layer": "状态摘要协议",
|
|
174
|
+
"marks": {"verified": MARK_VERIFIED, "modified": MARK_MODIFIED,
|
|
175
|
+
"abnormal": MARK_ABNORMAL, "doubted": MARK_DOUBTED,
|
|
176
|
+
"unverified": MARK_UNVERIFIED},
|
|
177
|
+
"env": ENV, "default_on": True, "max_ids": MAX_IDS,
|
|
178
|
+
"note": "统一在 _cg_call 单出口注入;hint 同步升级(Q3 默认开启)",
|
|
179
|
+
}
|
package/md_cg/stg.py
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
"""
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
+
from . import trust
|
|
18
|
+
|
|
17
19
|
TIME_RELATIONS = ("before", "after", "equals", "contains", "during", "overlaps")
|
|
18
20
|
SPACE_RELATIONS = ("left_of", "right_of", "above", "below",
|
|
19
21
|
"contains", "inside", "overlaps")
|
|
@@ -23,12 +25,26 @@ PLACEHOLDER_LOCKED = "[密文·预览已脱敏]"
|
|
|
23
25
|
PLACEHOLDER_DENIED = "[无权限·预览已脱敏]"
|
|
24
26
|
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
# 生效条件:time_axis 经 trust.time_axis_of 归一(None → observed,非法轴如 believed 抛 ValueError);observed 轴下 fm 的 temporal 非 None 且 float(t) 可成功时返回 (float(t), float(t)),temporal 缺失/为 None/转换抛 TypeError 或 ValueError 时回退到 fm["condition_space"](假值按 {} 处理)的 time_window,仅当其为长度 2 的 list/tuple 且两元素可 float 时返回 (float(tw[0]), float(tw[1])),否则返回 None;effective 轴下取 trust.time_window_of(fm, "effective") 的两端点,两端均可解析才返回 (float(s), float(e)),任一端缺失或不可解析返回 None。
|
|
29
|
+
def _interval(fm, time_axis="observed"):
|
|
30
|
+
"""节点时间区间;轴语义与 `trust.time_window_of` **同源**(唯一口径,不新写解析)。
|
|
31
|
+
|
|
32
|
+
· `observed`(默认,与旧行为**逐位一致**):优先 `temporal`(事件时刻),
|
|
33
|
+
回退 `condition_space.time_window`(观测窗)。
|
|
34
|
+
注意 add() 在调用方未给 time_window 时会以「写入时刻」自动填充;
|
|
35
|
+
若把它当事件时间,两条不同时刻的节点会得到假的重叠关系,故 temporal 优先。
|
|
36
|
+
· `effective`(效力轴):`effective_from` / `effective_until` 及其别名
|
|
37
|
+
(`trust.FROM_ALIASES` / `UNTIL_ALIASES`,规范键优先、别名回落)。
|
|
38
|
+
区间语义要求**两端齐备**——单侧缺失/不可解析 → `None`(不可判定,
|
|
39
|
+
不猜测边界:给半开区间补 `±inf` 会让 `time_relation` 报出假的 contains/during)。
|
|
28
40
|
|
|
29
|
-
|
|
30
|
-
若把它当事件时间,两条不同时刻的节点会得到假的重叠关系,故 temporal 优先。
|
|
41
|
+
`believed_at` **永不参与任何轴**(同 `trust.BELIEVED_FIELD` 的隔离纪律)。
|
|
31
42
|
"""
|
|
43
|
+
if trust.time_axis_of(time_axis) == "effective":
|
|
44
|
+
s, e = trust.time_window_of(fm, "effective")
|
|
45
|
+
if s is None or e is None:
|
|
46
|
+
return None
|
|
47
|
+
return (float(s), float(e))
|
|
32
48
|
t = fm.get("temporal")
|
|
33
49
|
if t is not None:
|
|
34
50
|
try:
|
|
@@ -45,6 +61,7 @@ def _interval(fm):
|
|
|
45
61
|
return None
|
|
46
62
|
|
|
47
63
|
|
|
64
|
+
# 生效条件:fm["spatial"](假值按 {} 处理)为 dict 且其 bbox 是长度 4 的 list/tuple 且四元素可 float 时返回浮点四元组,spatial 非 dict、bbox 非长度 4 序列或元素转换抛 TypeError/ValueError 时返回 None。
|
|
48
65
|
def _bbox(fm):
|
|
49
66
|
sp = fm.get("spatial") or {}
|
|
50
67
|
bb = sp.get("bbox") if isinstance(sp, dict) else None
|
|
@@ -56,6 +73,7 @@ def _bbox(fm):
|
|
|
56
73
|
return None
|
|
57
74
|
|
|
58
75
|
|
|
76
|
+
# 生效条件:a、b 均非 None 且各可解包为两个元素时,按 a 相对 b 依次返回 equals(两端全等)、before(a2<b1)、after(a1>b2)、contains(a1<=b1 且 a2>=b2)、during(a1>=b1 且 a2<=b2)或 overlaps(其余);a 或 b 为 None 时返回 None;
|
|
59
77
|
def time_relation(a, b):
|
|
60
78
|
"""Allen 区间代数的 6 个基本态。"""
|
|
61
79
|
if a is None or b is None:
|
|
@@ -74,6 +92,7 @@ def time_relation(a, b):
|
|
|
74
92
|
return "overlaps"
|
|
75
93
|
|
|
76
94
|
|
|
95
|
+
# 生效条件:a 或 b 为 None 时返回 None,否则按 a=(ax1,ay1,ax2,ay2)、b=(bx1,by1,bx2,by2) 依序判定 ax2<=bx1→"left_of"、ax1>=bx2→"right_of"、ay2<=by1→"above"、ay1>=by2→"below"、四边全含→"contains"、四边全被含→"inside",全部不满足时返回 "overlaps"。
|
|
77
96
|
def space_relation(a, b):
|
|
78
97
|
"""RCC-8 简化的 7 个空间态(图像坐标:y 向下为正)。"""
|
|
79
98
|
if a is None or b is None:
|
|
@@ -97,6 +116,7 @@ def space_relation(a, b):
|
|
|
97
116
|
|
|
98
117
|
# ---------- 查询实现 ----------
|
|
99
118
|
|
|
119
|
+
# 生效条件:cg.get(node_id) 为真值(非 None、非空映射)时返回 {"id": node_id, "frontmatter": n.get("frontmatter") or {}(假值回落 {}), "content": n.get("content") or ""(假值回落 "")},cg.get(node_id) 为假值时返回 None。
|
|
100
120
|
def _node(cg, node_id):
|
|
101
121
|
n = cg.get(node_id)
|
|
102
122
|
if not n:
|
|
@@ -105,6 +125,7 @@ def _node(cg, node_id):
|
|
|
105
125
|
"content": n.get("content") or ""}
|
|
106
126
|
|
|
107
127
|
|
|
128
|
+
# 生效条件:cg.index["nodes"] 存在时按 list(...items())[:max_scan] 遍历,layer 为真值时仅保留 e.get("layer")==layer 的条目(layer 为假值不筛层),e 含 "temporal" 或 "spatial" 键时直接以快照字段构造 frontmatter、否则调用 cg._read(e) 且在 fm 为 None 时跳过;返回 out 列表(max_scan=None 切片取全部,0 时为空);
|
|
108
129
|
def _scan(cg, layer=None, max_scan=5000):
|
|
109
130
|
"""遍历节点:时空字段直接读索引快照(不读文件,O(1)/节点)。
|
|
110
131
|
|
|
@@ -116,7 +137,14 @@ def _scan(cg, layer=None, max_scan=5000):
|
|
|
116
137
|
if layer and e.get("layer") != layer:
|
|
117
138
|
continue
|
|
118
139
|
if "temporal" in e or "spatial" in e:
|
|
140
|
+
# 效力轴四键必须一并从快照带出:否则 `time_axis="effective"` 在快照
|
|
141
|
+
# 路径上永远「不可判定」(静默全空,比报错更难查)。旧索引快照无这些
|
|
142
|
+
# 键时 `.get` 得 None → 不可判定,是本轴**如实降级**而非误判。
|
|
119
143
|
fm = {"temporal": e.get("temporal"), "spatial": e.get("spatial"),
|
|
144
|
+
trust.EFFECTIVE_FROM_FIELD: e.get(trust.EFFECTIVE_FROM_FIELD),
|
|
145
|
+
trust.EFFECTIVE_UNTIL_FIELD: e.get(trust.EFFECTIVE_UNTIL_FIELD),
|
|
146
|
+
trust.FROM_FIELD: e.get(trust.FROM_FIELD),
|
|
147
|
+
trust.UNTIL_FIELD: e.get(trust.UNTIL_FIELD),
|
|
120
148
|
"condition_space": {"time_window": e.get("time_window")}}
|
|
121
149
|
else:
|
|
122
150
|
fm, _content = cg._read(e)
|
|
@@ -127,6 +155,7 @@ def _scan(cg, layer=None, max_scan=5000):
|
|
|
127
155
|
return out
|
|
128
156
|
|
|
129
157
|
|
|
158
|
+
# 生效条件:cg.index["nodes"].get(node_id) 缺失或为假值时返回 "";否则 cg._readable 可调用且对其返回假值或抛异常时返回 PLACEHOLDER_DENIED;cg._read(e) 的 frontmatter 为 None 时返回 "";content 非密文时返回 content[:n](n 默认 200);content 为密文时,cg._open_content 可调用且取到非 None 且非密文的 opened 才返回 opened[:n],opened 为 None、抛异常或仍为密文时返回 PLACEHOLDER_LOCKED。
|
|
130
159
|
def _preview(cg, node_id, n=200):
|
|
131
160
|
"""按需读单个节点正文做预览(只发生在最终返回的条目上)。
|
|
132
161
|
|
|
@@ -165,13 +194,15 @@ def _preview(cg, node_id, n=200):
|
|
|
165
194
|
return opened[:n]
|
|
166
195
|
|
|
167
196
|
|
|
168
|
-
|
|
169
|
-
|
|
197
|
+
# 生效条件:cg 上 _node(cg, a_id) 与 _node(cg, b_id) 均返回真值时返回含 a_id/b_id、时间关系、空间关系和 time_known/space_known 的 dict(两侧时间区间均按 time_axis 轴取,见 _interval;time_axis 非法经 trust.time_axis_of 抛 ValueError);任一 _node 结果为假值时返回 {"error":"node_not_found","missing":[...]};
|
|
198
|
+
def relation(cg, a_id, b_id, time_axis="observed"):
|
|
199
|
+
"""两节点间的时空关系(a 相对 b)。`time_axis` 决定时间区间取哪条轴(见 `_interval`)。"""
|
|
170
200
|
na, nb = _node(cg, a_id), _node(cg, b_id)
|
|
171
201
|
if not na or not nb:
|
|
172
202
|
return {"error": "node_not_found",
|
|
173
203
|
"missing": [x for x, n in ((a_id, na), (b_id, nb)) if not n]}
|
|
174
|
-
ia, ib = _interval(na["frontmatter"]
|
|
204
|
+
ia, ib = (_interval(na["frontmatter"], time_axis),
|
|
205
|
+
_interval(nb["frontmatter"], time_axis))
|
|
175
206
|
ba, bb = _bbox(na["frontmatter"]), _bbox(nb["frontmatter"])
|
|
176
207
|
return {"a": a_id, "b": b_id,
|
|
177
208
|
"time": {"relation": time_relation(ia, ib), "a": ia, "b": ib},
|
|
@@ -180,23 +211,27 @@ def relation(cg, a_id, b_id):
|
|
|
180
211
|
"space_known": ba is not None and bb is not None}}
|
|
181
212
|
|
|
182
213
|
|
|
183
|
-
|
|
184
|
-
|
|
214
|
+
# 生效条件:以 _scan(cg,layer=layer,max_scan=max_scan) 为范围,_interval(n["frontmatter"], time_axis) 为 None 的节点被跳过,其余按 (start,end) 以 reverse=bool(desc) 排序,返回 count=全部命中数、limit=传入 limit、items 为排序后前 limit 项(limit=0 时为空列表)且每项附 _preview(cg,id)(time_axis 缺省 observed,与旧行为逐位一致;非法轴抛 ValueError)。
|
|
215
|
+
def timeline(cg, layer=None, limit=50, desc=True, max_scan=5000,
|
|
216
|
+
time_axis="observed"):
|
|
217
|
+
"""按时间排序的节点列表。`time_axis` 决定排序依据的时间区间(见 `_interval`)。"""
|
|
185
218
|
items = []
|
|
186
219
|
for n in _scan(cg, layer=layer, max_scan=max_scan):
|
|
187
|
-
iv = _interval(n["frontmatter"])
|
|
220
|
+
iv = _interval(n["frontmatter"], time_axis)
|
|
188
221
|
if iv is None:
|
|
189
222
|
continue
|
|
190
223
|
items.append((iv[0], iv[1], n["id"], n["layer"]))
|
|
191
|
-
items.sort(key=lambda x: (x[0], x[1]), reverse=bool(desc))
|
|
224
|
+
items.sort(key=lambda x: (x[0], x[1], x[2]), reverse=bool(desc))
|
|
192
225
|
return {"count": len(items), "limit": limit,
|
|
193
226
|
"items": [{"id": i, "layer": l, "start": s, "end": e,
|
|
194
227
|
"preview": _preview(cg, i)}
|
|
195
228
|
for s, e, i, l in items[:limit]]}
|
|
196
229
|
|
|
197
230
|
|
|
198
|
-
|
|
199
|
-
|
|
231
|
+
# 生效条件:time_window 为长度 2 的 list/tuple 时 q_t=(float(time_window[0]),float(time_window[1]))(元素不可转 float 会直接抛异常,源码未捕获),bbox 为长度 4 的 list/tuple 时同理构造 q_b;q_t 与 q_b 均为 None 时返回 {"error":"need_time_window_or_bbox"};否则扫描节点、每节点时间区间按 _interval(fm, time_axis) 取(time_axis 缺省 observed 与旧行为逐位一致,非法轴抛 ValueError),并要求时间关系在 during/contains/overlaps/equals、空间关系在 inside/contains/overlaps/equals(提供查询侧才检查),返回 hits[:limit](limit=None 取全部,0/False 取空);
|
|
232
|
+
def anchors(cg, time_window=None, bbox=None, layer=None, limit=50, max_scan=5000,
|
|
233
|
+
time_axis="observed"):
|
|
234
|
+
"""落在给定时间窗 / 空间范围内的节点。`time_axis` 决定候选时间区间(见 `_interval`)。"""
|
|
200
235
|
q_t = None
|
|
201
236
|
if isinstance(time_window, (list, tuple)) and len(time_window) == 2:
|
|
202
237
|
q_t = (float(time_window[0]), float(time_window[1]))
|
|
@@ -209,7 +244,7 @@ def anchors(cg, time_window=None, bbox=None, layer=None, limit=50, max_scan=5000
|
|
|
209
244
|
hits = []
|
|
210
245
|
for n in _scan(cg, layer=layer, max_scan=max_scan):
|
|
211
246
|
fm = n["frontmatter"]
|
|
212
|
-
iv, bb = _interval(fm), _bbox(fm)
|
|
247
|
+
iv, bb = _interval(fm, time_axis), _bbox(fm)
|
|
213
248
|
t_rel = time_relation(iv, q_t) if (q_t and iv) else None
|
|
214
249
|
s_rel = space_relation(bb, q_b) if (q_b and bb) else None
|
|
215
250
|
if q_t and t_rel not in ("during", "contains", "overlaps", "equals"):
|
|
@@ -224,14 +259,20 @@ def anchors(cg, time_window=None, bbox=None, layer=None, limit=50, max_scan=5000
|
|
|
224
259
|
"items": hits[:limit]}
|
|
225
260
|
|
|
226
261
|
|
|
227
|
-
|
|
228
|
-
|
|
262
|
+
# 生效条件:遍历 _scan(cg,layer=layer,max_scan=max_scan) 每条 frontmatter,bb 非 None 且不满足 bb[0]<=bb[2] and bb[1]<=bb[3] 记 invalid_bbox、iv(由 _interval(fm, time_axis) 取,time_axis 缺省 observed 与旧行为逐位一致、非法轴抛 ValueError)非 None 且 iv[0]>iv[1] 记 inverted_time_window、temporal 非 None 且 time_window 为长度 2 的 list/tuple 且 float 比较成功却不满足 tw[0]<=t<=tw[1] 记 temporal_outside_window(该检查恒按观察轴内部口径、不随 time_axis 漂移;转换抛 TypeError/ValueError 则忽略),返回 scanned 计数、issues 总数与 issues[:limit](limit 默认 50)。
|
|
263
|
+
def consistency(cg, layer=None, limit=50, max_scan=5000, time_axis="observed"):
|
|
264
|
+
"""时空字段自洽性检查:非法 bbox / 时间倒置 / 窗口与时刻冲突。
|
|
265
|
+
|
|
266
|
+
`time_axis` 只决定「时间倒置」按哪条轴判;`temporal_outside_window`
|
|
267
|
+
恒按**观察轴内部**口径(temporal 与 time_window 的关系)——那是该 issue 的
|
|
268
|
+
定义本身,换轴会让它变成另一件事(不随参数漂移)。
|
|
269
|
+
"""
|
|
229
270
|
issues = []
|
|
230
271
|
scanned = 0
|
|
231
272
|
for n in _scan(cg, layer=layer, max_scan=max_scan):
|
|
232
273
|
scanned += 1
|
|
233
274
|
fm = n["frontmatter"]
|
|
234
|
-
bb, iv = _bbox(fm), _interval(fm)
|
|
275
|
+
bb, iv = _bbox(fm), _interval(fm, time_axis)
|
|
235
276
|
if bb and not (bb[0] <= bb[2] and bb[1] <= bb[3]):
|
|
236
277
|
issues.append({"id": n["id"], "issue": "invalid_bbox", "bbox": bb})
|
|
237
278
|
if iv and iv[0] > iv[1]:
|
|
@@ -247,4 +288,4 @@ def consistency(cg, layer=None, limit=50, max_scan=5000):
|
|
|
247
288
|
except (TypeError, ValueError):
|
|
248
289
|
pass
|
|
249
290
|
return {"scanned": scanned, "issues": len(issues), "limit": limit,
|
|
250
|
-
"items": issues[:limit]}
|
|
291
|
+
"items": issues[:limit]}
|
package/md_cg/subgraph.py
CHANGED
|
@@ -40,6 +40,7 @@ SEP_MAX_NODES = 400 # 单次扫描节点上限(防 O(N²) 读盘爆
|
|
|
40
40
|
SEP_MAX_PAIRS = 50 # 单次返回/落库的候选对上限
|
|
41
41
|
|
|
42
42
|
|
|
43
|
+
# 生效条件:fm 为假值或 fm.get("subgraph") 取值为假(None/[]/{}/空串)时直接返回 {'nodes': [], 'edges': []};subgraph 为 list/tuple 时其元素作节点、边记空列表,为 dict 时节点取 sg.get("nodes")(缺键或假值回落 [])、边取 sg.get("edges")(同样回落),为其他类型仍返回空;节点项为 dict 时取 n.get("id")、否则取元素本身,nid 为 None 跳过,仅当 str(nid).strip() 非空且未出现过才追加;边只保留 isinstance(e, dict) 的项;
|
|
43
44
|
def declared(fm):
|
|
44
45
|
"""规范化 `frontmatter.subgraph` → `{"nodes": [id...], "edges": [edge...]}`。
|
|
45
46
|
|
|
@@ -68,6 +69,7 @@ def declared(fm):
|
|
|
68
69
|
return {"nodes": nodes, "edges": [e for e in raw_edges if isinstance(e, dict)]}
|
|
69
70
|
|
|
70
71
|
|
|
72
|
+
# 生效条件:当 getattr(cg,"index",None) 的 nodes 字典中存在 nid(entry 非 None)且 "subgraph" in entry 时,返回 {'id': nid, 'subgraph': entry.get("subgraph"), 'edges': entry.get("edges") or []}(subgraph 值为 None 也照样返回);否则改走 cg.get(nid),返回 (node or {}).get("frontmatter") or {},节点不存在或 frontmatter 为假值时得空 dict;
|
|
71
73
|
def _fm(cg, nid):
|
|
72
74
|
"""取节点 frontmatter:优先索引快照(免 IO),快照缺字段时回退读文件。"""
|
|
73
75
|
entry = ((getattr(cg, "index", None) or {}).get("nodes") or {}).get(nid)
|
|
@@ -89,6 +91,7 @@ def _fm(cg, nid):
|
|
|
89
91
|
SRC_REL_MAP = {"hierarchical": "contains"}
|
|
90
92
|
|
|
91
93
|
|
|
94
|
+
# 生效条件:rel 先经 str(rel or "").strip().lower() 归一(None/空串得 ""),返回 {'target': str(tgt), 'relation_type': SRC_REL_MAP.get(rel, rel), 'confidence': confidence, 'verified': verified}——归一后的 rel 命中模块常量 SRC_REL_MAP 时用映射值、未命中(含 rel 为空串)时原样保留,confidence/verified 直接透传默认 1.0/0 的实参不做校验;
|
|
92
95
|
def normalize_edge(tgt, rel, confidence=1.0, verified=0):
|
|
93
96
|
"""源库边 → md_cg 本地边(迁移器专用)。做两件事:
|
|
94
97
|
|
|
@@ -102,6 +105,7 @@ def normalize_edge(tgt, rel, confidence=1.0, verified=0):
|
|
|
102
105
|
"confidence": confidence, "verified": verified}
|
|
103
106
|
|
|
104
107
|
|
|
108
|
+
# 生效条件:cg 的 `_subgraph_children` 不为 None 时原样返回该缓存字典,否则遍历 cg.index["nodes"] 的键,用 declared(fm)["nodes"] 收子节点、并按边 rel 为 part_of/hierarchical(target 记为父、当前 pid 记为子)或 parent_of/contains(当前 pid 记为父、target 记为子,target 为 None 则该边跳过)补全 parent_id→[child_id] 索引,回写 cg._subgraph_children 后返回 idx;
|
|
105
109
|
def children_index(cg):
|
|
106
110
|
"""全局正查:parent_id → [child_id...](声明式 ∪ 边式,一次 O(N) 后缓存)。"""
|
|
107
111
|
idx = getattr(cg, "_subgraph_children", None)
|
|
@@ -140,11 +144,13 @@ def children_index(cg):
|
|
|
140
144
|
return idx
|
|
141
145
|
|
|
142
146
|
|
|
147
|
+
# 生效条件:返回 list(children_index(cg).get(nid) or []),即 nid 在索引中无键或对应值为空(None/空列表)时得空列表,否则返回其子 id 的浅拷贝列表;
|
|
143
148
|
def children(cg, nid):
|
|
144
149
|
"""直接子节点 id 列表:`subgraph.nodes` 声明 ∪ 边式父子关系。"""
|
|
145
150
|
return list(children_index(cg).get(nid) or [])
|
|
146
151
|
|
|
147
152
|
|
|
153
|
+
# 生效条件:cg 的 `_subgraph_parents` 不为 None 时原样返回该缓存字典,否则遍历 cg.index["nodes"] 的键,用 declared(fm)["nodes"] 把 pid 记为每个子节点的父、并按边 rel 为 part_of/hierarchical(target 记为 pid 的父)或 parent_of/contains(pid 记为 target 的父,target 为 None 则该边跳过)补全 child_id→[parent_id] 索引,回写 cg._subgraph_parents 后返回 idx;
|
|
148
154
|
def parents_index(cg):
|
|
149
155
|
"""全局反查:child_id → [parent_id...](供 children/validate 复用,一次 O(N))。"""
|
|
150
156
|
idx = getattr(cg, "_subgraph_parents", None)
|
|
@@ -183,6 +189,7 @@ def parents_index(cg):
|
|
|
183
189
|
return idx
|
|
184
190
|
|
|
185
191
|
|
|
192
|
+
# 生效条件:无条件执行——把 cg 的 _subgraph_parents 与 _subgraph_children 依次 setattr 为 None(setattr 抛异常被 except 吞掉后继续下一个),无返回值,与 cg 是否已建有缓存无关;
|
|
186
193
|
def invalidate_cache(cg):
|
|
187
194
|
"""写入/删除节点后调用,丢弃父子正查/反查缓存。"""
|
|
188
195
|
for attr in ("_subgraph_parents", "_subgraph_children"):
|
|
@@ -192,12 +199,14 @@ def invalidate_cache(cg):
|
|
|
192
199
|
pass
|
|
193
200
|
|
|
194
201
|
|
|
202
|
+
# 生效条件:取 parents_index(cg).get(nid) or [],列表非空时返回其首元素,键缺失或值为空/假(None、[])时返回 None;多父时只返回第一个,不在此处报歧义;
|
|
195
203
|
def parent_of(cg, nid):
|
|
196
204
|
"""唯一父(多父时返回第一个并置 `ambiguous` 标记由 validate 报出)。"""
|
|
197
205
|
ps = parents_index(cg).get(nid) or []
|
|
198
206
|
return ps[0] if ps else None
|
|
199
207
|
|
|
200
208
|
|
|
209
|
+
# 生效条件:max_depth 非 None 时先 max(0, min(int(max_depth), MAX_DEPTH_HARD)) 收敛;以 nid 为根迭代 DFS,已入 seen 的节点跳过,len(seen) >= max_nodes(max_nodes 传 0 时首次循环即成立)置 truncated=True 并 break,depth >= max_depth 且 children 非空时置 truncated=True 并 continue,否则对 children(cg,cur) 逆序、对未出现过的 (ch,cur) 追加 part_of 边并压栈;返回 {'root','nodes','paths','edges','n_nodes','n_edges','truncated'};
|
|
201
210
|
def expand(cg, nid, max_depth=None, max_nodes=MAX_NODES_DEFAULT):
|
|
202
211
|
"""递归展开子树(迭代 DFS,防递归深度爆栈)。
|
|
203
212
|
|
|
@@ -237,6 +246,7 @@ def expand(cg, nid, max_depth=None, max_nodes=MAX_NODES_DEFAULT):
|
|
|
237
246
|
"truncated": truncated}
|
|
238
247
|
|
|
239
248
|
|
|
249
|
+
# 生效条件:先调用 expand(cg, nid, max_depth=max_depth, max_nodes=max_nodes),对其中每条边复制一份并追加 source/target 互换、relation_type="parent_of"、confidence/verified 沿用原边 .get(…,1.0/0) 的反向边;返回 root=nid、nodes=ex["nodes"]、paths=ex["paths"]、n_nodes=ex["n_nodes"]、n_edges=双边后条数、truncated=ex["truncated"];
|
|
240
250
|
def flatten(cg, nid, max_depth=None, max_nodes=MAX_NODES_DEFAULT):
|
|
241
251
|
"""把嵌套子图摊平成「节点 + 边」,父子生成**对称双边**。
|
|
242
252
|
|
|
@@ -257,6 +267,7 @@ def flatten(cg, nid, max_depth=None, max_nodes=MAX_NODES_DEFAULT):
|
|
|
257
267
|
"truncated": ex["truncated"]}
|
|
258
268
|
|
|
259
269
|
|
|
270
|
+
# 生效条件:known 取 cg.index 的 nodes 键集合,仅当 parents_index(cg).get(nid) or [] 为空(键缺失或空列表)的 nid 才视为树根,返回其排序后的 id 列表;
|
|
260
271
|
def roots(cg):
|
|
261
272
|
"""无父节点(树根)的 id 列表。"""
|
|
262
273
|
known = set(((getattr(cg, "index", None) or {}).get("nodes") or {}).keys())
|
|
@@ -264,6 +275,7 @@ def roots(cg):
|
|
|
264
275
|
return sorted(nid for nid in known if not (pmap.get(nid) or []))
|
|
265
276
|
|
|
266
277
|
|
|
278
|
+
# 生效条件:known 取 cg.index 的 nodes 键列表,max_scan 为真值时截断为 known[:int(max_scan)];逐 pid 遍历 declared(_fm(cg,pid))["nodes"]:ch==pid 记 self_loop 并 continue(不再进入悬空/多父判断),ch 不在 known_set 记 dangling_child,随后 parent 中已有 ch 且 prev != pid 记 multi_parent、否则写 parent[ch]=pid;再沿 parent 指针上溯,遇本路径已访问节点记 cycle;_issue 仅在 len(issues) < limit*4 时追加(limit 为 0 时 0<0 不成立,不追加任何 issue);返回 scanned、issues=len(issues)、items=issues[:limit]、truncated=len(issues)>limit;
|
|
267
279
|
def validate(cg, limit=50, max_scan=None):
|
|
268
280
|
"""树一致性校验:自环 / 悬空子节点 / 多父 / 环。
|
|
269
281
|
|
|
@@ -276,6 +288,7 @@ def validate(cg, limit=50, max_scan=None):
|
|
|
276
288
|
known_set = set(known)
|
|
277
289
|
parent, issues = {}, []
|
|
278
290
|
|
|
291
|
+
# 生效条件:仅当 len(issues) < limit*4 时把 kw 追加进 issues(限流防报告膨胀);无返回值,只改外层 issues;
|
|
279
292
|
def _issue(**kw):
|
|
280
293
|
if len(issues) < limit * 4:
|
|
281
294
|
issues.append(kw)
|
|
@@ -320,6 +333,7 @@ def validate(cg, limit=50, max_scan=None):
|
|
|
320
333
|
# · 边携带 reason + 判定分量,供后续重构/审计追溯;
|
|
321
334
|
# · 条件**相同**的相似对**不是**分离候选(那是重复,该走 MERGE)。
|
|
322
335
|
|
|
336
|
+
# 生效条件:cg.get(nid) 为假值(None/{})时返回 None;否则取 fm=node.get("frontmatter") or {}、content=node.get("content") or "",返回 {'grams': bigrams(forgetting.payload(content)), 'pos','neg': consistency.condition_terms(fm, content) 的结果, 'layer': fm.get("layer")}——grams 为空也照常返回该字典,不返回 None;
|
|
323
337
|
def _node_terms_and_grams(cg, nid):
|
|
324
338
|
"""读取节点并返回 (payload 二元组, 正条件词面, 负条件词面);不可读返回 None。"""
|
|
325
339
|
from . import consistency, forgetting
|
|
@@ -334,6 +348,7 @@ def _node_terms_and_grams(cg, nid):
|
|
|
334
348
|
return {"grams": grams, "pos": pos, "neg": neg, "layer": fm.get("layer")}
|
|
335
349
|
|
|
336
350
|
|
|
351
|
+
# 生效条件:pool 初取 cg.index 的 nodes 键,layer 为假值(None/"")时不按层过滤、否则只留 e.get("layer")==layer 的 nid,ids 为真值时只留 str(x) 属于该集合的 nid;pool 排序后 truncated=len(pool)>int(max_nodes)、并截为前 int(max_nodes) 个;只有 _node_terms_and_grams 返回非 None 且 grams 非空的节点进 cache;对 cache 键两两比较——无公共 bigram 跳过、jaccard 小于 float(min_jaccard) 跳过、condition_distinct 的 distinct 为假或 overlap > float(max_cond_overlap) 跳过,其余成候选并按 (-jaccard,a,b) 排序;返回 scanned、compared、candidates=out[:int(limit)]、total_candidates、truncated、note;
|
|
337
352
|
def separation_pairs(cg, layer=None, ids=None, max_nodes=SEP_MAX_NODES,
|
|
338
353
|
min_jaccard=SEP_MIN_JACCARD,
|
|
339
354
|
max_cond_overlap=SEP_MAX_COND_OVERLAP,
|
|
@@ -393,6 +408,7 @@ def separation_pairs(cg, layer=None, ids=None, max_nodes=SEP_MAX_NODES,
|
|
|
393
408
|
"note": "只读候选;apply=True 才写分离边"}
|
|
394
409
|
|
|
395
410
|
|
|
411
|
+
# 生效条件:batch 为假值(None 或空串)时回落到 time.strftime("%Y%m%d-%H%M%S");按 (a,b)、(b,a) 两向处理:cg.get(src) 为假值跳过,dst 已出现在 consistency.separation_targets(fm) 中跳过,否则往 fm["edges"] 追加 distinct_from 边、向 fm["pattern_separated_from"] 追加 dst,按 os.path.join(cg.root, e.get("path") or f"{src}.md") 调 cg._write_node 写盘并追加 MAINTAIN_LOG 行,src 记入 written;返回 {'a','b','written','batch'};
|
|
396
412
|
def mark_separated(cg, a, b, reason="", actor="maintain", batch=None):
|
|
397
413
|
"""写入 (a↔b) 对称 `distinct_from` 边(幂等:已存在则不重复写)。"""
|
|
398
414
|
from . import consistency
|
|
@@ -427,6 +443,7 @@ def mark_separated(cg, a, b, reason="", actor="maintain", batch=None):
|
|
|
427
443
|
return {"a": a, "b": b, "written": written, "batch": batch}
|
|
428
444
|
|
|
429
445
|
|
|
446
|
+
# 生效条件:pairs 为 None 时调 separation_pairs 取 candidates 与 meta(scanned/compared/truncated/total_candidates),否则 cands=list(pairs) 且 meta 中 scanned/compared 记 0、truncated 记 False、total_candidates=len(cands);apply 为真值时先生成统一 batch=time.strftime(...),逐个候选调 mark_separated(..., batch=batch),written 非空且 cg 有 rebuild_index 时调 cg.rebuild_index()、否则调 invalidate_cache(cg),apply 为假值时不写盘;返回 ok、action='separate'、dry_run=not apply、layer、candidates、written、written_count、elapsed_ms、log 与 meta、note;
|
|
430
447
|
def separate_run(cg, layer=None, pairs=None, apply=False, ids=None,
|
|
431
448
|
min_jaccard=SEP_MIN_JACCARD, limit=SEP_MAX_PAIRS,
|
|
432
449
|
actor="maintain"):
|
|
@@ -487,6 +504,7 @@ RECON_COMMON_SHARE = 0.5 # 共同条件判定:≥ 半数锚点共享
|
|
|
487
504
|
RECON_SCENE_PREFIX = "scene_"
|
|
488
505
|
|
|
489
506
|
|
|
507
|
+
# 生效条件:clues 为 None 时返回 [];clues 为 str 时包装成单元素列表;逐项取 str(c or "").strip().lower(),空串跳过,整串去重入 out,再按 re.split(r"[\s,,、;;/|]+") 切词、去重后追加;返回 out;
|
|
490
508
|
def _clue_terms(clues):
|
|
491
509
|
"""线索归一化:字符串 / 列表 → 去重词面(保留整串 + 切分后的词)。"""
|
|
492
510
|
if clues is None:
|
|
@@ -507,6 +525,7 @@ def _clue_terms(clues):
|
|
|
507
525
|
return out
|
|
508
526
|
|
|
509
527
|
|
|
528
|
+
# 生效条件:term_sets 为假值(None/[])时返回 [];否则对每个集合的 set(s or ()) 统计词面出现次数,need = max(2, int(len(term_sets)*RECON_COMMON_SHARE) 向上取整),返回出现次数 >= need 的词面排序列表(阈值取自模块常量 RECON_COMMON_SHARE);
|
|
510
529
|
def _shared_terms(term_sets):
|
|
511
530
|
"""出现在 ≥ RECON_COMMON_SHARE 比例集合中的词面(且至少 2 个集合共享)。"""
|
|
512
531
|
if not term_sets:
|
|
@@ -519,6 +538,7 @@ def _shared_terms(term_sets):
|
|
|
519
538
|
return sorted(t for t, c in cnt.items() if c >= need)
|
|
520
539
|
|
|
521
540
|
|
|
541
|
+
# 生效条件:按 term_sets 原顺序遍历每个集合的 sorted(s or ()),首次出现的词面追加到 seen,返回 seen[:limit](limit 默认 99,传 0 时切片为空列表,无 or 回落);
|
|
522
542
|
def _uniq_terms(term_sets, limit=99):
|
|
523
543
|
seen = []
|
|
524
544
|
for s in term_sets:
|
|
@@ -528,6 +548,7 @@ def _uniq_terms(term_sets, limit=99):
|
|
|
528
548
|
return seen[:limit]
|
|
529
549
|
|
|
530
550
|
|
|
551
|
+
# 生效条件:bigrams(" ".join(clue_terms)) 为空时返回 [];否则遍历 pool,cg.get(nid) 为假值、或 forgetting.payload 后 bigrams 为空、或与 clue_grams 无交集时跳过,否则 jac=inter/len(并集 or 1)、exact 为 clue_terms 中非空且作为子串出现在 body 的个数,score=jac+0.05*exact,仅 score >= RECON_MIN_SCORE 才收录,最终按 (-score, nid) 排序返回;
|
|
531
552
|
def _anchor_scores(cg, pool, clue_terms):
|
|
532
553
|
"""按词面重合给候选锚点打分(返回 [(nid, score, shared, exact)],降序)。"""
|
|
533
554
|
from . import forgetting
|
|
@@ -556,6 +577,7 @@ def _anchor_scores(cg, pool, clue_terms):
|
|
|
556
577
|
return scored
|
|
557
578
|
|
|
558
579
|
|
|
580
|
+
# 生效条件:对 anchor_ids 中每个 nid,cg.get(nid) 为假值跳过,否则取 fm 与 consistency.condition_terms 得 pos/neg,per[nid] 记为 {"effective": sorted(pos)[:8], "non_applicable": sorted(neg)[:8]};返回 common=_shared_terms(pos_sets)、individual=_uniq_terms(pos_sets) 中不在 common 的项、non_applicable=_uniq_terms(neg_sets)、declared=list(declared_terms or [])、per_anchor=per;
|
|
559
581
|
def _condition_space(cg, anchor_ids, declared_terms=None):
|
|
560
582
|
"""合成锚点群的条件空间:共同条件 / 个别条件 / 不适用条件 / 逐锚点明细。"""
|
|
561
583
|
from . import consistency
|
|
@@ -576,6 +598,7 @@ def _condition_space(cg, anchor_ids, declared_terms=None):
|
|
|
576
598
|
"declared": list(declared_terms or []), "per_anchor": per}
|
|
577
599
|
|
|
578
600
|
|
|
601
|
+
# 生效条件:以 cg.index["nodes"] 为节点池(layer 为真时只保留 layer 等于该值的节点,池按 max_nodes 截断),由 clues 词项与 ids 收集锚点并截取 limit 个,锚点为空则返回 status="blindspot" 且 condition_space=None,否则返回 status="reconstructed"(neighbors 为真时按链邻接补边并在 max_nodes 内补节点,apply 为真且算出的 scene_id 尚不在 nodes 中时再写入该情境节点并记日志);
|
|
579
602
|
def reconstruct_scene(cg, clues=None, ids=None, conditions=None, layer=None,
|
|
580
603
|
max_nodes=RECON_MAX_NODES, limit=RECON_MAX_ANCHORS,
|
|
581
604
|
neighbors=True, apply=False, actor="insight"):
|