@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
package/md_cg/twophase.py
CHANGED
|
@@ -56,10 +56,12 @@ R_MISMATCH = "content_mismatch" # 落了别的正文 → interrupted(
|
|
|
56
56
|
R_UNREADABLE = "node_unreadable" # 读节点失败 → interrupted(不猜测)
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
# 生效条件:给定 cg 且其 root 属性可参与 os.path.join 时,恒返回 os.path.join(cg.root, LOG_FILE),无分支与早退。
|
|
59
60
|
def log_path(cg) -> str:
|
|
60
61
|
return os.path.join(cg.root, LOG_FILE)
|
|
61
62
|
|
|
62
63
|
|
|
64
|
+
# 生效条件:text 为假值(None/"")时按空串处理,否则用 text 本身,返回去尾换行后的字符串(仅剥 "\n")。
|
|
63
65
|
def _norm(text: str) -> str:
|
|
64
66
|
"""正文口径归一(**对账正确性的前提**):`nodefile.dumps` 写盘时会给正文补尾
|
|
65
67
|
换行,`loads` 读回即带它。若 intent 按入参原文、对账按读回内容各算一次指纹,
|
|
@@ -69,10 +71,12 @@ def _norm(text: str) -> str:
|
|
|
69
71
|
return (text or "").rstrip("\n")
|
|
70
72
|
|
|
71
73
|
|
|
74
|
+
# 生效条件:给定 cg 与 rec 时,把 rec 以 JSONL 追加写入 log_path(cg),无返回值、无分支。
|
|
72
75
|
def _append(cg, rec: dict) -> None:
|
|
73
76
|
append_jsonl(log_path(cg), rec)
|
|
74
77
|
|
|
75
78
|
|
|
79
|
+
# 生效条件:pr 取自 cg.principal 或(假值时)回落 cg.session——pr 为真字符串时 meta["session"]=pr;否则 pr 非 None 时把 pr.session 记入 session、把 pr.writer 或(假值时)pr.unit 记入 writer;actor 为真时 meta["actor"]=actor,最后返回 meta。
|
|
76
80
|
def _ident(cg, actor=None) -> dict:
|
|
77
81
|
"""归因字段(尽力而为):principal 缺省时留空,不编造。"""
|
|
78
82
|
pr = getattr(cg, "principal", None) or getattr(cg, "session", None)
|
|
@@ -91,6 +95,7 @@ def _ident(cg, actor=None) -> dict:
|
|
|
91
95
|
return meta
|
|
92
96
|
|
|
93
97
|
|
|
98
|
+
# 生效条件:给定 cg、node_id、content 时生成 iid 与 content_hash(对 _norm(content) 计算),先写 phase="intent" 记录再返回 tok;layer 为真才写入 rec["layer"],actor 与 **meta 分别经 _ident、rec.update 合入。
|
|
94
99
|
def begin(cg, node_id: str, content: str, layer: str = None,
|
|
95
100
|
actor: str = None, **meta) -> dict:
|
|
96
101
|
"""记录写入**意图**(先行持久化)→ 返回令牌 tok(供 `commit` 配对)。
|
|
@@ -112,6 +117,7 @@ def begin(cg, node_id: str, content: str, layer: str = None,
|
|
|
112
117
|
return tok
|
|
113
118
|
|
|
114
119
|
|
|
120
|
+
# 生效条件:nid 取 node_id,node_id 为假值(None/"")时回落 (tok or {}).get("node_id"),iid 取 (tok or {}).get("iid")(tok 为假值时两项均为 None);status 用实参或默认 STATUS_COMMITTED;tok 为真且 tok.get("content_hash") 为真才写入 content_hash;reason 为真才写入 rec["reason"];追加记录后返回 rec。
|
|
115
121
|
def commit(cg, tok: dict, status: str = STATUS_COMMITTED, node_id: str = None,
|
|
116
122
|
reason: str = None, **meta) -> dict:
|
|
117
123
|
"""记录写入**结果**(与 intent 按 iid 配对)。返回结果记录。"""
|
|
@@ -131,6 +137,7 @@ def commit(cg, tok: dict, status: str = STATUS_COMMITTED, node_id: str = None,
|
|
|
131
137
|
# ----------------------------------------------------------------------
|
|
132
138
|
# 读侧:盘点 / 对账
|
|
133
139
|
|
|
140
|
+
# 生效条件:log_path(cg) 不是文件时直接返回空列表;否则逐行读取,空行跳过,json.loads 抛异常的行跳过,返回成功解析的记录列表。
|
|
134
141
|
def records(cg) -> list:
|
|
135
142
|
"""读全部账本记录(append-only,坏行跳过不炸)。"""
|
|
136
143
|
out = []
|
|
@@ -149,6 +156,7 @@ def records(cg) -> list:
|
|
|
149
156
|
return out
|
|
150
157
|
|
|
151
158
|
|
|
159
|
+
# 生效条件:遍历 records(cg),记录 r.get("iid") 为假值(缺键或空串)时跳过;否则按 iid 建槽,仅当槽内 intent/outcome 仍为 None 时分别填入首个 phase=="intent" / phase=="outcome" 的记录,返回 pairs。
|
|
152
160
|
def _pair(cg) -> dict:
|
|
153
161
|
"""把账本配成 {iid: {"intent":…, "outcome":…}}(同时计入无 iid 的脏行)。"""
|
|
154
162
|
pairs = {}
|
|
@@ -164,6 +172,7 @@ def _pair(cg) -> dict:
|
|
|
164
172
|
return pairs
|
|
165
173
|
|
|
166
174
|
|
|
175
|
+
# 生效条件:遍历 _pair(cg) 取有 intent 且 outcome 为 None 的槽(展开 intent 字段)组成 out,按 r.get("t") or 0 升序排序后返回 out[:max(0, int(limit))]——limit 为 0 或负数时切片为空列表,默认值 200 仅在未传参时生效。
|
|
167
176
|
def pending(cg, limit: int = 200) -> list:
|
|
168
177
|
"""未结清的写入意图(有 intent 无 outcome)——只读,不改盘。
|
|
169
178
|
|
|
@@ -177,6 +186,7 @@ def pending(cg, limit: int = 200) -> list:
|
|
|
177
186
|
return out[:max(0, int(limit))]
|
|
178
187
|
|
|
179
188
|
|
|
189
|
+
# 生效条件:intent 的 id 为假值或 cg.get(nid) 抛异常时返回 (STATUS_INTERRUPTED, R_UNREADABLE);取回 node 为假值时返回 (STATUS_INTERRUPTED, R_NODE_MISSING);intent 的 content_hash 为真且与 _norm(node.get("content") or "") 的指纹不等时返回 (STATUS_INTERRUPTED, R_MISMATCH);否则返回 (STATUS_COMMITTED, R_NODE_OK)。
|
|
180
190
|
def _judge(cg, intent: dict):
|
|
181
191
|
"""判定一笔未结清意图的真实结局 → `(status, code)`。**不猜测**:读不到就如实说。"""
|
|
182
192
|
nid = intent.get("id")
|
|
@@ -195,6 +205,7 @@ def _judge(cg, intent: dict):
|
|
|
195
205
|
return STATUS_COMMITTED, R_NODE_OK
|
|
196
206
|
|
|
197
207
|
|
|
208
|
+
# 生效条件:以 limit 调 pending(cg, limit=limit) 得到未结清意图并统计 unpaired/committed/interrupted;apply 为真时对每笔 intent 调 commit 补写 outcome(reconciled=True、actor="twophase:reconcile"),apply 为假值时只报告不写账本,rep["applied"]=bool(apply);limit 为 0 或负数时 pending 返回空、details 为空列表。
|
|
198
209
|
def reconcile(cg, apply: bool = True, limit: int = 2000) -> dict:
|
|
199
210
|
"""启动/巡检对账:把半途写入**补账或如实标记**。
|
|
200
211
|
|
|
@@ -218,4 +229,4 @@ def reconcile(cg, apply: bool = True, limit: int = 2000) -> dict:
|
|
|
218
229
|
"content_hash": intent.get("content_hash")},
|
|
219
230
|
status=status, reason=code, reconciled=True,
|
|
220
231
|
actor="twophase:reconcile")
|
|
221
|
-
return rep
|
|
232
|
+
return rep
|
package/md_cg/units.py
CHANGED
|
@@ -14,7 +14,10 @@ crosscheck 同款 verdicts 通道回填)。理由:使用者的复核是昂
|
|
|
14
14
|
蜂巢契约(真源 hive/README.md + hive/hive_mcp/mcp_server.py;**复制契约不 import**,
|
|
15
15
|
保持 md_cg 对 hive 零依赖,与 ccgc「同款语义就地实现防 import 环」惯例一致):
|
|
16
16
|
jobs 目录 = $HIVE_JOBS_DIR | <repo>/hive/jobs
|
|
17
|
-
serve 判活 = jobs/_serve.json 的 ts(毫秒)距今 <
|
|
17
|
+
serve 判活 = **三层**:jobs/_serve.json 的 ts(毫秒)距今 < FRESH_S(15s)
|
|
18
|
+
∧ pid 存活 ∧ 该 pid 是本程序
|
|
19
|
+
(同 hive/serve_start.serve_alive;四路口径由
|
|
20
|
+
hive/test_serve_entry.py 机械守卫)
|
|
18
21
|
job 目录 = jobs/<job_id>/{spec.json,status.json,result.json,kill}
|
|
19
22
|
job_id = h<java_ms>_<uuid6> (同 _submit)
|
|
20
23
|
result.json = {"ok":true,"content":...} | {"ok":false,"error":...}
|
|
@@ -45,8 +48,16 @@ SERVE_FILE, SPEC_FILE = "_serve.json", "spec.json"
|
|
|
45
48
|
STATUS_FILE, RESULT_FILE, KILL_FILE = "status.json", "result.json", "kill"
|
|
46
49
|
LOG_NAME = "_units.jsonl"
|
|
47
50
|
TERMINAL_STATES = ("done", "error", "timeout", "killed")
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
#: serve 心跳新鲜窗口(秒)。**须与 hive/serve_start.FRESH_S 同值**——本模块
|
|
52
|
+
#: 只持有「判活的第 4 处实现」(复制契约不 import hive),阈值/判据结构若与
|
|
53
|
+
#: 权威漂移,会在通道选择面复现 v13 的「假存活」病类(v14 缺陷 E:
|
|
54
|
+
#: 旧值 5.0 vs 权威 15,serve 崩溃后 ≤5s 窗口内判「存活」→ 选 hive 通道 →
|
|
55
|
+
#: 提交的 job 永远无人处理)。守卫:hive/test_serve_entry.py。
|
|
56
|
+
FRESH_S, DEFAULT_TIMEOUT_S, DEFAULT_POLL_S = 15.0, 120, 1.0
|
|
57
|
+
#: 复核委派的 completion 预算。**必须对齐统一默认(文档:最大输出 200000)**——
|
|
58
|
+
#: 该模型 reasoning 与正文**共享 completion 预算**,小预算会把正文吃光并静默返回空正文:
|
|
59
|
+
#: 实测 2048 → reasoning 2048 / content 空;16384 → 时好时坏;200000 → 正常出裁决。
|
|
60
|
+
DEFAULT_MAX_TOKENS = 200000
|
|
50
61
|
|
|
51
62
|
ENV_JOBS_DIR, ENV_EXE = "HIVE_JOBS_DIR", "HIVE_EXE"
|
|
52
63
|
ENV_MODEL, ENV_API_KEY = "MDCG_UNIT_MODEL", "HIVE_API_KEY"
|
|
@@ -61,16 +72,19 @@ _VERDICT_MAP = {
|
|
|
61
72
|
}
|
|
62
73
|
|
|
63
74
|
|
|
75
|
+
# 生效条件:无入参,恒返回模块 `__file__` 绝对路径上溯两级的目录(md_cg 的上一级),与 cwd 无关。
|
|
64
76
|
def repo_root() -> str:
|
|
65
77
|
"""仓库根(md_cg 的上一级);不用 cwd——cwd 由宿主决定,不可作判据。"""
|
|
66
78
|
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
67
79
|
|
|
68
80
|
|
|
81
|
+
# 生效条件:explicit 为真值时 d=explicit;explicit 为空串等假值而 ENV_JOBS_DIR 有真值时 d=该环境变量;两者皆假值时 d=repo_root()/hive/jobs;最终返回 os.path.abspath(d)。
|
|
69
82
|
def jobs_dir(explicit: str = "") -> str:
|
|
70
83
|
d = explicit or os.environ.get(ENV_JOBS_DIR) or os.path.join(repo_root(), "hive", "jobs")
|
|
71
84
|
return os.path.abspath(d)
|
|
72
85
|
|
|
73
86
|
|
|
87
|
+
# 生效条件:ENV_EXE 有真值时原样返回该值;ENV_EXE 缺失或为空串时返回 repo_root()/hive/target/release/ 下按 os.name=="nt" 取 hive.exe、否则取 hive 的拼接路径。
|
|
74
88
|
def exe_path() -> str:
|
|
75
89
|
exe = os.environ.get(ENV_EXE)
|
|
76
90
|
if exe:
|
|
@@ -79,13 +93,81 @@ def exe_path() -> str:
|
|
|
79
93
|
"hive.exe" if os.name == "nt" else "hive")
|
|
80
94
|
|
|
81
95
|
|
|
96
|
+
# 生效条件:explicit 为真值时取 explicit,否则取 ENV_MODEL,两者皆假值时按空串,返回该结果 strip() 后的字符串(可为空串)。
|
|
82
97
|
def model_name(explicit: str = "") -> str:
|
|
83
98
|
"""复核模型(LLM 委托型执行器的 spec.model 必填)。"""
|
|
84
99
|
return (explicit or os.environ.get(ENV_MODEL) or "").strip()
|
|
85
100
|
|
|
86
101
|
|
|
102
|
+
# 生效条件:pid 为 int 且大于 0 时(否则 False),os.name 为 "nt" 时返回 _tasklist_row(pid) 是否非 None,非 "nt" 时 os.kill(pid, 0) 不抛 OSError 返回 True、抛 OSError 返回 False。
|
|
103
|
+
def pid_alive(pid) -> bool:
|
|
104
|
+
"""该 pid **号**是否存在(Windows tasklist 精确列比对 / unix `kill -0`)。
|
|
105
|
+
|
|
106
|
+
只回答「这个号有没有进程」——**不足以判定「serve 还在跑」**(见
|
|
107
|
+
`pid_is_self_program`)。实现与 hive/serve_start.pid_alive 同口径。
|
|
108
|
+
"""
|
|
109
|
+
if not isinstance(pid, int) or pid <= 0:
|
|
110
|
+
return False
|
|
111
|
+
if os.name == "nt":
|
|
112
|
+
return _tasklist_row(pid) is not None
|
|
113
|
+
try:
|
|
114
|
+
os.kill(pid, 0)
|
|
115
|
+
return True
|
|
116
|
+
except OSError:
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# 生效条件:对传入 pid 执行 tasklist /FO CSV 后,在其 stdout 中遇到的第一个按 '","' 切分、列数≥2 且第 2 列 strip 再 strip('"') 后等于 str(pid) 的行即返回 [映像名, pid 字符串],无此行或 subprocess.run 抛 OSError 时返回 None。
|
|
121
|
+
def _tasklist_row(pid):
|
|
122
|
+
"""Windows:查该 pid 的 tasklist 行 → [映像名, pid 字符串];查不到返回 None。
|
|
123
|
+
|
|
124
|
+
按列精确比对,**不用子串包含**——子串会让 pid=441 被 4410 命中(假存活)。
|
|
125
|
+
"""
|
|
126
|
+
try:
|
|
127
|
+
# 显式 utf-8 + replace:只消费 ASCII 的 pid 列,但**不依赖 locale**——
|
|
128
|
+
# locale 口径与「后代写 UTF-8」不一致时读线程会崩(见 test_subproc_encoding.py)。
|
|
129
|
+
r = subprocess.run(["tasklist", "/FI", f"PID eq {pid}", "/NH", "/FO", "CSV"],
|
|
130
|
+
capture_output=True, text=True,
|
|
131
|
+
encoding="utf-8", errors="replace")
|
|
132
|
+
except OSError:
|
|
133
|
+
return None
|
|
134
|
+
for line in (r.stdout or "").splitlines():
|
|
135
|
+
cols = line.split('","')
|
|
136
|
+
if len(cols) >= 2 and cols[1].strip().strip('"') == str(pid):
|
|
137
|
+
return [cols[0].strip().strip('"'), cols[1].strip().strip('"')]
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# 生效条件:exe_path() 的 basename 小写非空且 pid 为 int 大于 0 时(否则 False),"nt" 下要求 _tasklist_row(pid) 非空且映像名小写等于该 basename,非 "nt" 下要求 /proc/<pid>/cmdline 首个 b"\x00" 前 token 的 basename 小写等于它(读取抛 OSError 则 False)。
|
|
142
|
+
def pid_is_self_program(pid) -> bool:
|
|
143
|
+
"""该 pid 是否**就是本程序**(同映像名)——pid 号会被无关进程复用。
|
|
144
|
+
|
|
145
|
+
与 hive/serve_start.pid_is_self_program 同口径(复制契约)。零依赖边界:
|
|
146
|
+
拿不到映像名返回 False(宁可放行启动,也不误报「已有 serve 在跑」)。
|
|
147
|
+
"""
|
|
148
|
+
want = os.path.basename(exe_path()).lower()
|
|
149
|
+
if not want or not isinstance(pid, int) or pid <= 0:
|
|
150
|
+
return False
|
|
151
|
+
if os.name == "nt":
|
|
152
|
+
row = _tasklist_row(pid)
|
|
153
|
+
return bool(row) and row[0].lower() == want
|
|
154
|
+
try:
|
|
155
|
+
with open(f"/proc/{pid}/cmdline", "rb") as f:
|
|
156
|
+
first = f.read().split(b"\x00")[0]
|
|
157
|
+
except OSError:
|
|
158
|
+
return False
|
|
159
|
+
return os.path.basename(first.decode("utf-8", "replace")).lower() == want
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# 生效条件:以 jobs_dir(jobs) 下的 SERVE_FILE 为心跳路径——该路径不被 isfile 命中时返回 exists/alive 均 False 的未启动 reason;命中但 open/json.load 抛 OSError 或 ValueError 时返回不可读 reason;解析成功后按**三层判据**(age < fresh_s ∧ pid_alive(pid) ∧ pid_is_self_program(pid))定 alive,回填 age_s/pid/raw 与逐层明细及失败原因。
|
|
87
163
|
def serve_state(jobs: str = "", fresh_s: float = FRESH_S) -> dict:
|
|
88
|
-
"""serve
|
|
164
|
+
"""serve 判活:**三层**(心跳新鲜 ∧ pid 存活 ∧ 该 pid 是本程序)。
|
|
165
|
+
|
|
166
|
+
与 `hive/serve_start.serve_alive` 同口径(同值 FRESH_S=15s、同三层判据)。
|
|
167
|
+
v14 缺陷 E:旧实现只判 ts 新鲜度且阈值 5s——双漂移,serve 崩溃后
|
|
168
|
+
≤5s 内判「存活」会让 `probe()` 选 hive 通道(提交的 job 永远无人处理),
|
|
169
|
+
这正是 v13「假存活」病类从守卫面搬到了**通道选择面**。
|
|
170
|
+
"""
|
|
89
171
|
jd = jobs_dir(jobs)
|
|
90
172
|
p = os.path.join(jd, SERVE_FILE)
|
|
91
173
|
out = {"jobs_dir": jd, "heartbeat": p, "exists": os.path.isfile(p),
|
|
@@ -100,13 +182,28 @@ def serve_state(jobs: str = "", fresh_s: float = FRESH_S) -> dict:
|
|
|
100
182
|
out["reason"] = "心跳不可读:%s: %s" % (type(exc).__name__, exc)
|
|
101
183
|
return out
|
|
102
184
|
age = time.time() - ((hb.get("ts") or 0) / 1000.0)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
185
|
+
pid = hb.get("pid")
|
|
186
|
+
fresh_ok = age < float(fresh_s)
|
|
187
|
+
pid_ok = pid_alive(pid)
|
|
188
|
+
ident_ok = pid_is_self_program(pid)
|
|
189
|
+
out.update({"age_s": round(age, 3), "pid": pid, "raw": hb,
|
|
190
|
+
"fresh": bool(fresh_ok), "pid_alive": bool(pid_ok),
|
|
191
|
+
"pid_is_self_program": bool(ident_ok)})
|
|
192
|
+
out["alive"] = bool(fresh_ok and pid_ok and ident_ok)
|
|
193
|
+
if out["alive"]:
|
|
194
|
+
out["reason"] = ""
|
|
195
|
+
elif not fresh_ok:
|
|
196
|
+
out["reason"] = "心跳过期 %.1fs(阈值 %ss)——serve 可能已退出" % (age, fresh_s)
|
|
197
|
+
elif not pid_ok:
|
|
198
|
+
out["reason"] = "心跳新鲜但 pid=%s 已不存在——serve 已退出" % (pid,)
|
|
199
|
+
else:
|
|
200
|
+
out["reason"] = ("心跳新鲜且 pid=%s 存活,但该 pid 不属于本程序"
|
|
201
|
+
"(非 %s)——pid 号被无关进程复用"
|
|
202
|
+
% (pid, os.path.basename(exe_path())))
|
|
107
203
|
return out
|
|
108
204
|
|
|
109
205
|
|
|
206
|
+
# 生效条件:jobs 仅经 jobs_dir(jobs) 用于填充指引里的 jobs 路径;reason 为假值时文案首行取“serve 未存活”,否则取 reason 原文;返回模板固定嵌入 exe_path() 的目录与路径、ENV_API_KEY、ENV_MODEL、jd、jd 下的 SERVE_FILE 与 int(FRESH_S)。
|
|
110
207
|
def setup_hint(jobs: str = "", reason: str = "") -> str:
|
|
111
208
|
"""不可用时给使用者看的**配置指引**(含降级指引)——提示即责任,须可照着做。"""
|
|
112
209
|
jd = jobs_dir(jobs)
|
|
@@ -123,6 +220,7 @@ def setup_hint(jobs: str = "", reason: str = "") -> str:
|
|
|
123
220
|
ENV_MODEL, exe_path(), jd, os.path.join(jd, SERVE_FILE), int(FRESH_S)))
|
|
124
221
|
|
|
125
222
|
|
|
223
|
+
# 生效条件:先取 st=serve_state(jobs, fresh_s) 与 mdl=model_name(model);st["alive"] 为真且 mdl 非空时返回 state=HIVE;否则 allow_degrade 为真时返回 state=SUBAGENT 且 channel 取 channel 或 "harness-subagent";否则返回 state=CONFIGURE、transport=None 并附 setup_hint(jobs, why)。
|
|
126
224
|
def probe(jobs: str = "", model: str = "", fresh_s: float = FRESH_S,
|
|
127
225
|
allow_degrade: bool = False, channel: str = "") -> dict:
|
|
128
226
|
"""三级能力探测:hive(可派发)→ configure(提示配置)→ subagent(显式降级)。
|
|
@@ -147,6 +245,7 @@ def probe(jobs: str = "", model: str = "", fresh_s: float = FRESH_S,
|
|
|
147
245
|
"hint": setup_hint(jobs, why)}
|
|
148
246
|
|
|
149
247
|
|
|
248
|
+
# 生效条件:state/transport/model/jobs_dir/serve/channel/hint 全部直接取自 probe(jobs, model, fresh_s, allow_degrade, channel) 的对应键,本函数不另做判活或模型判断,仅在 chain 文案中固定引用 int(FRESH_S) 与 ENV_MODEL。
|
|
150
249
|
def plan(jobs: str = "", model: str = "", fresh_s: float = FRESH_S,
|
|
151
250
|
allow_degrade: bool = False, channel: str = "") -> dict:
|
|
152
251
|
"""优先级链自描述(供 cg op=ccg action=units 与审计查看)。"""
|
|
@@ -155,7 +254,8 @@ def plan(jobs: str = "", model: str = "", fresh_s: float = FRESH_S,
|
|
|
155
254
|
"jobs_dir": p["jobs_dir"], "serve": p["serve"], "channel": p["channel"],
|
|
156
255
|
"chain": [
|
|
157
256
|
{"order": 1, "transport": HIVE, "action": "派发 reflect/verify 单元并等待 result.json",
|
|
158
|
-
"when": "jobs/_serve.json 心跳 < %ss
|
|
257
|
+
"when": ("jobs/_serve.json 心跳 < %ss 且(%s 已配置 **或调用方显式传 model**)"
|
|
258
|
+
% (int(FRESH_S), ENV_MODEL))},
|
|
159
259
|
{"order": 2, "transport": CONFIGURE, "action": "返回配置指引(不自动拉起、不假装通过)",
|
|
160
260
|
"when": "蜂巢不可用"},
|
|
161
261
|
{"order": 3, "transport": SUBAGENT, "action": "返回复核请求包,由 harness 端子代理执行并回填",
|
|
@@ -166,11 +266,13 @@ def plan(jobs: str = "", model: str = "", fresh_s: float = FRESH_S,
|
|
|
166
266
|
|
|
167
267
|
# ---------------------------------------------------------------- 派发与收取
|
|
168
268
|
|
|
269
|
+
# 生效条件:无入参,恒返回 "h"+int(time.time()*1000)+"_"+uuid.uuid4().hex 前 6 位组成的字符串。
|
|
169
270
|
def _job_id() -> str:
|
|
170
271
|
"""同 _submit:serve 侧按 'h' 前缀识别任务目录。"""
|
|
171
272
|
return "h%d_%s" % (int(time.time() * 1000), uuid.uuid4().hex[:6])
|
|
172
273
|
|
|
173
274
|
|
|
275
|
+
# 生效条件:cg.root 与 cg.cg.root 都取不到真值时不写、直接返回 None;取到 root 时向 root/LOG_NAME 追加一行 rec(副本,setdefault ts)的 JSON,写入抛 OSError 时被吞掉静默返回 None。
|
|
174
276
|
def _log(cg, rec: dict) -> None:
|
|
175
277
|
"""留痕 _units.jsonl(对齐 _crosscheck.jsonl / _backfill.jsonl 纪律)。"""
|
|
176
278
|
root = getattr(cg, "root", None) or getattr(getattr(cg, "cg", None), "root", None)
|
|
@@ -185,6 +287,7 @@ def _log(cg, rec: dict) -> None:
|
|
|
185
287
|
pass
|
|
186
288
|
|
|
187
289
|
|
|
290
|
+
# 生效条件:role 不在 ROLES 时返回 ok=False 的未知角色 error;否则 model_name(model) 为空时返回 ok=False 的未配置复核模型;否则 str(prompt or "").strip() 为空时返回 ok=False 的 prompt 为空;否则在 jobs_dir(jobs)/_job_id() 下写 spec.json 与 status.json(context_files 为真、temperature 非 None、extra 为真时才并入 spec),OSError 时返回 ok=False 的写入失败,全部成功返回 ok=True 与 job_id/job_dir/spec/model。
|
|
188
291
|
def submit(*, prompt: str, role: str = REFLECT, model: str = "", system_prompt: str = "",
|
|
189
292
|
context_files=None, timeout_s: int = DEFAULT_TIMEOUT_S,
|
|
190
293
|
max_tokens: int = DEFAULT_MAX_TOKENS, temperature=None, jobs: str = "",
|
|
@@ -226,6 +329,7 @@ def submit(*, prompt: str, role: str = REFLECT, model: str = "", system_prompt:
|
|
|
226
329
|
"unit_role": role, "model": mdl}
|
|
227
330
|
|
|
228
331
|
|
|
332
|
+
# 生效条件:jobs_dir(jobs)/str(job_id or "") 不是目录时返回 state=missing、terminal=False 的目录不存在 error;是目录时读 STATUS_FILE(读失败则 status 置 None)并用其 state 覆盖 state/terminal(state 属 TERMINAL_STATES 才 terminal=True);RESULT_FILE 被 isfile 命中则 terminal=True、state=st or "done",解析抛 OSError/ValueError 时提前返回该 error;解析为 dict 时取 ok/content/error/usage/model,且 ok 为真而 content 去空白为空时把 ok 改 False 并写空正文 error,解析为非 dict 时 ok=True 且 content 为原值。
|
|
229
333
|
def poll(job_id: str, jobs: str = "") -> dict:
|
|
230
334
|
"""读 job 终态视图:**以 result.json 出现为终态主判据**,status.json 仅作辅助。"""
|
|
231
335
|
d = os.path.join(jobs_dir(jobs), str(job_id or ""))
|
|
@@ -260,11 +364,19 @@ def poll(job_id: str, jobs: str = "") -> dict:
|
|
|
260
364
|
out["error"] = res.get("error") or out["error"]
|
|
261
365
|
out["usage"] = res.get("usage")
|
|
262
366
|
out["model"] = res.get("model")
|
|
367
|
+
# 空正文**不得报成功**:reasoning 与正文共享 completion 预算,预算不足时
|
|
368
|
+
# job 仍自称 ok 但 content 为空 → 若不拦,会被误读成「复核单元无答复」
|
|
369
|
+
# (假死锁)。run() 早已有同等校验,此处补齐,消除两处口径不一致。
|
|
370
|
+
if out["ok"] and not str(out["content"] or "").strip():
|
|
371
|
+
out["ok"] = False
|
|
372
|
+
out["error"] = ("空正文:job 自称 ok 但 content 为空(多为 reasoning 吃尽"
|
|
373
|
+
"完成预算;提高 max_tokens,勿把预算不足当通道不可用)")
|
|
263
374
|
else:
|
|
264
375
|
out["ok"], out["content"] = True, res
|
|
265
376
|
return out
|
|
266
377
|
|
|
267
378
|
|
|
379
|
+
# 生效条件:循环 poll(job_id, jobs),结果 terminal 为真即补 waited_s 后返回;否则 time.time()-t0 >= float(timeout_s) 时返回 terminal=False、timeout=True 与超时 error;两者皆不满足则 sleep(float(poll_s)) 后重试(timeout_s=0 时首次 poll 非终态即超时返回)。
|
|
268
380
|
def wait(job_id: str, *, jobs: str = "", timeout_s: float = DEFAULT_TIMEOUT_S,
|
|
269
381
|
poll_s: float = DEFAULT_POLL_S) -> dict:
|
|
270
382
|
"""阻塞等终态;超时如实返回(不假装成功、不强杀 job)。"""
|
|
@@ -282,6 +394,7 @@ def wait(job_id: str, *, jobs: str = "", timeout_s: float = DEFAULT_TIMEOUT_S,
|
|
|
282
394
|
time.sleep(float(poll_s))
|
|
283
395
|
|
|
284
396
|
|
|
397
|
+
# 生效条件:submit(prompt=prompt, role=role, cg=cg, actor=actor, **kw) 的 ok 为假时返回 stage="submit" 的失败 dict(含 **sub);ok 为真时 wait(sub["job_id"], jobs=kw.get("jobs",""), timeout_s=wait_s),补 stage="wait"/job_id/unit_role/model,并把 ok 改为 res 的 ok 与 content 同时为真。
|
|
285
398
|
def run(*, prompt: str, role: str = REFLECT, cg=None, actor: str = "",
|
|
286
399
|
wait_s: float = DEFAULT_TIMEOUT_S, **kw) -> dict:
|
|
287
400
|
"""submit + wait 组合(阻塞式复核,MCP action=review 主路径)。"""
|
|
@@ -300,6 +413,7 @@ def run(*, prompt: str, role: str = REFLECT, cg=None, actor: str = "",
|
|
|
300
413
|
_FENCE_RE = re.compile(r"```(?:json)?\s*(.+?)\s*```", re.S)
|
|
301
414
|
|
|
302
415
|
|
|
416
|
+
# 生效条件:raw 是 dict/list 时原样返回;否则取 str(raw or "").strip(),为空串(含 0/False/None 等假值)返回 None;否则依次尝试 _FENCE_RE 第 1 组、整段文本、首个 "{" 到末个 "}" 及首个 "[" 到末个 "]" 的切片,返回首个能 json.loads 成 dict 或 list 的候选;全部失败返回 None。
|
|
303
417
|
def _json_of(raw):
|
|
304
418
|
"""从单元输出抽第一个 JSON 对象/数组(容忍 ```json 围栏与前后噪声)。"""
|
|
305
419
|
if isinstance(raw, (dict, list)):
|
|
@@ -326,6 +440,7 @@ def _json_of(raw):
|
|
|
326
440
|
return None
|
|
327
441
|
|
|
328
442
|
|
|
443
|
+
# 生效条件:data=_json_of(raw) 为 list 时取 data[0](仅当它是 dict,否则 None);非 dict 时返回 verdict=DEFER、parsed=False;dict 时 word 取 verdict/decision/state/result 中首个真值后 strip().lower(),_VERDICT_MAP.get(word) 为 None 时返回 DEFER、parsed=True 并带 corr 与 reason,命中时返回该裁决及 reason、slot_corrections(取自 slot_corrections/corrections,非 dict 则置 {})、checks、raw 前 500 字符。
|
|
329
444
|
def verdict_of(raw) -> dict:
|
|
330
445
|
"""单元输出 → 裁决四态;解析不出 → DEFER(不猜测、不当通过)。"""
|
|
331
446
|
data = _json_of(raw)
|
|
@@ -347,6 +462,7 @@ def verdict_of(raw) -> dict:
|
|
|
347
462
|
"checks": data.get("checks"), "raw": str(raw)[:500], "parsed": True}
|
|
348
463
|
|
|
349
464
|
|
|
465
|
+
# 生效条件:state==HIVE 时返回 "hive:<role>:<job_id 或 'unknown'>";state==SUBAGENT 时返回 "subagent:<role>:<channel 或 'harness'>";其余 state 返回空串。
|
|
350
466
|
def transport_name(state: str, role: str, job_id: str = "", channel: str = "") -> str:
|
|
351
467
|
"""裁决来源标识:**结构上不可能等于编译者**,保证 A 裁定「不得自证」不被误伤。"""
|
|
352
468
|
if state == HIVE:
|
|
@@ -356,6 +472,7 @@ def transport_name(state: str, role: str, job_id: str = "", channel: str = "") -
|
|
|
356
472
|
return ""
|
|
357
473
|
|
|
358
474
|
|
|
475
|
+
# 生效条件:unit 为假值时按 {} 处理;verdict=unit.get("verdict") or DEFER,verifier=transport_name(state, role, job_id, channel),evidence 取 unit 的 reason,为空则退回 str(unit["raw"])[:300],job_id 为真时再追加“(job=<job_id>)”;仅当 unit 的 slot_corrections 为真才带该键,仅当 compiled_by 与 verifier 皆非空且相等才置 self_verify=True。
|
|
359
476
|
def to_attest_args(unit: dict, *, state: str = "", role: str = REFLECT, job_id: str = "",
|
|
360
477
|
channel: str = "", compiled_by: str = "") -> dict:
|
|
361
478
|
"""单元裁决 → ccgc.attest(...) 入参(纯函数,离线可测)。
|
|
@@ -416,6 +533,7 @@ _VERIFY_TPL = """你是独立**验证单元**(verify)。对下面这份 CCG
|
|
|
416
533
|
{{"verdict":"accept|drop|defer","reason":"一句话依据","slot_corrections":{{}}}}"""
|
|
417
534
|
|
|
418
535
|
|
|
536
|
+
# 生效条件:digest is None 时返回 "{}";digest 是 str 时原样返回;否则先试 to_dict/as_dict 可调用方法并 json.dumps 其返回值(抛异常即跳出改走后续分支);是 dict 时 json.dumps(digest);其余取 vars(digest) 中不以 "_" 开头的属性 json.dumps(default=str)。
|
|
419
537
|
def _digest_of(digest) -> str:
|
|
420
538
|
if digest is None:
|
|
421
539
|
return "{}"
|
|
@@ -434,6 +552,7 @@ def _digest_of(digest) -> str:
|
|
|
434
552
|
ensure_ascii=False, indent=1, default=str)
|
|
435
553
|
|
|
436
554
|
|
|
555
|
+
# 生效条件:role==VERIFY 选 _VERIFY_TPL,否则选 _REFLECT_TPL;reflect_rows 是 str 时直接作为 rows,否则 json.dumps(reflect_rows or [], ensure_ascii=False, indent=1);最终返回 tpl.format(digest=_digest_of(digest), dialog=str(dialog or "(未提供)")[:6000], reflect_rows=rows[:3000])。
|
|
437
556
|
def prompt_for(role: str, digest=None, *, dialog: str = "", reflect_rows="") -> str:
|
|
438
557
|
"""按角色生成复核请求包正文(reflect/verify 共用一处模板真源)。"""
|
|
439
558
|
tpl = _VERIFY_TPL if role == VERIFY else _REFLECT_TPL
|
|
@@ -443,6 +562,7 @@ def prompt_for(role: str, digest=None, *, dialog: str = "", reflect_rows="") ->
|
|
|
443
562
|
reflect_rows=rows[:3000])
|
|
444
563
|
|
|
445
564
|
|
|
565
|
+
# 生效条件:text 取 prompt 真值或 prompt_for(role, digest, dialog=dialog, reflect_rows=reflect_rows);pr=probe(jobs, model, allow_degrade=allow_degrade, channel=channel) 为 SUBAGENT 时原样返回未派发的 SUBAGENT 包、为 CONFIGURE 时返回未派发的 CONFIGURE 包;为 HIVE 时若 autostart 为真且 serve_state(jobs) 非 alive 先 autostart_serve(jobs),再 submit(...),submit 失败回 CONFIGURE 且把 error 拼进 hint,成功则给出 job_id,blocking=False 直接返回;blocking=True 时 wait(sub["job_id"], jobs=jobs, timeout_s=wait_s),res.ok 为假则 unit 记 DEFER 并附未产出有效结果的 hint,为真则 unit=verdict_of(res.get("content")),随后填 transport/attest/result_ok/waited_s 并 _log。
|
|
446
566
|
def review(*, prompt: str = "", role: str = REFLECT, digest=None, dialog: str = "",
|
|
447
567
|
reflect_rows="", node_id: str = "", jobs: str = "", model: str = "",
|
|
448
568
|
timeout_s: int = DEFAULT_TIMEOUT_S, allow_degrade: bool = False,
|
|
@@ -501,6 +621,7 @@ def review(*, prompt: str = "", role: str = REFLECT, digest=None, dialog: str =
|
|
|
501
621
|
|
|
502
622
|
# ---------------------------------------------------------------- serve 拉起(显式)
|
|
503
623
|
|
|
624
|
+
# 生效条件:serve_state(jobs_dir(jobs)) 已 alive 时返回 started=False 的“serve 存活”;否则 exe_path() 未被 isfile 命中时返回 started=False 并提示先 cargo build --release;否则以 detached/新会话 Popen 拉起 exe serve --jobs jd 并把 stdout/stderr 写入 jd/_serve.log,Popen 抛 OSError 时返回 started=False 的拉起失败;拉起后在 float(wait_s) 内轮询到 alive 返回 started=True 的“serve 已拉起”,轮询超时仍返回 started=True 但标注心跳未就绪。
|
|
504
625
|
def autostart_serve(jobs: str = "", wait_s: float = 5.0) -> dict:
|
|
505
626
|
"""**显式**拉起 serve(默认不启用;使用者裁定:不可用即提示配置)。
|
|
506
627
|
|
|
@@ -532,6 +653,7 @@ def autostart_serve(jobs: str = "", wait_s: float = 5.0) -> dict:
|
|
|
532
653
|
return {"started": True, "note": "serve 已拉起(心跳未就绪,稍后自愈)"}
|
|
533
654
|
|
|
534
655
|
|
|
656
|
+
# 生效条件:返回 dict 的 ok=p["state"]==HIVE,其中 p=plan(jobs)(model/fresh_s/allow_degrade/channel 全走默认),state/serve_alive/serve_age_s/chain/hint/model 取自该 p;jobs_dir 取 jobs_dir(jobs),exe_found 与 exe_path 取 exe_path() 是否被 isfile 命中,model_set=bool(model_name())(无参,读 ENV_MODEL),api_key_set=bool(os.environ.get(ENV_API_KEY)),env 记录 ENV_JOBS_DIR 与 ENV_MODEL 的原值。
|
|
535
657
|
def doctor(jobs: str = "") -> dict:
|
|
536
658
|
"""能力体检(形态对齐 hive_doctor):判活 + exe + 模型 + 优先级链。"""
|
|
537
659
|
jd = jobs_dir(jobs)
|
|
@@ -543,4 +665,4 @@ def doctor(jobs: str = "") -> dict:
|
|
|
543
665
|
"api_key_set": bool(os.environ.get(ENV_API_KEY)),
|
|
544
666
|
"env": {"HIVE_JOBS_DIR": os.environ.get(ENV_JOBS_DIR, ""),
|
|
545
667
|
"MDCG_UNIT_MODEL": os.environ.get(ENV_MODEL, "")},
|
|
546
|
-
"chain": p["chain"], "hint": p["hint"]}
|
|
668
|
+
"chain": p["chain"], "hint": p["hint"]}
|
package/md_cg/vision_evidence.py
CHANGED
|
@@ -116,15 +116,18 @@ _RE_DIR_N = re.compile(r"_(\d+)$")
|
|
|
116
116
|
|
|
117
117
|
# ---- 通用工具 -------------------------------------------------------------
|
|
118
118
|
|
|
119
|
+
# 生效条件:x 为 str 时返回 MdCGOS(x) 新实例,否则原样返回 x。
|
|
119
120
|
def _as_cg(x):
|
|
120
121
|
"""接受 root 路径或已构造 cg 实例——保持密级隔离与密钥上下文。"""
|
|
121
122
|
return MdCGOS(x) if isinstance(x, str) else x
|
|
122
123
|
|
|
123
124
|
|
|
125
|
+
# 生效条件:无必需形参,调用即返回 time.strftime("%Y%m%d-%H%M%S") 的当前批次串。
|
|
124
126
|
def _now_batch() -> str:
|
|
125
127
|
return time.strftime("%Y%m%d-%H%M%S")
|
|
126
128
|
|
|
127
129
|
|
|
130
|
+
# 生效条件:base 为字符串批号,先读 _log_path(cg) 的 jsonl 收集 batch 字段中以 base 开头的已有值,base 未被占用则原样返回 base,已占用则返回首个未占用的 f"{base}.{i}"(i 从 2 递增)。
|
|
128
131
|
def _unique_batch(cg, base: str) -> str:
|
|
129
132
|
"""同秒重复调用时批号去重(后缀 .2/.3…),保证按批次回滚不打偏。"""
|
|
130
133
|
seen = set()
|
|
@@ -140,21 +143,25 @@ def _unique_batch(cg, base: str) -> str:
|
|
|
140
143
|
return f"{base}.{i}"
|
|
141
144
|
|
|
142
145
|
|
|
146
|
+
# 生效条件:cg 具 root 属性时取 cg.root、否则取 str(cg) 作为 root,返回 os.path.join(root, EVIDENCE_LOG)。
|
|
143
147
|
def _log_path(cg) -> str:
|
|
144
148
|
root = cg.root if hasattr(cg, "root") else str(cg)
|
|
145
149
|
return os.path.join(root, EVIDENCE_LOG)
|
|
146
150
|
|
|
147
151
|
|
|
152
|
+
# 生效条件:batch 与 nid 恒以 "%s|%s" 拼接成条目号,不做空值或类型校验。
|
|
148
153
|
def _entry_id(batch: str, nid: str) -> str:
|
|
149
154
|
return "%s|%s" % (batch, nid)
|
|
150
155
|
|
|
151
156
|
|
|
157
|
+
# 生效条件:v 为 list/tuple 时返回各元素 str(x).strip() 后非空项以「;」连接;否则 v 为 None 返回空串,其余值返回 str(v).strip()。
|
|
152
158
|
def _as_text(v) -> str:
|
|
153
159
|
if isinstance(v, (list, tuple)):
|
|
154
160
|
return ";".join(str(x).strip() for x in v if str(x).strip())
|
|
155
161
|
return "" if v is None else str(v).strip()
|
|
156
162
|
|
|
157
163
|
|
|
164
|
+
# 生效条件:path 经 abspath→dirname→basename 取名后匹配 _RE_DIR_N,命中则返回 int(m.group(1)),未命中返回 None。
|
|
158
165
|
def _gallery_no(path: str):
|
|
159
166
|
"""图集编号:目录名尾部 `_<N>`;缺省 None(脱敏引用用)。"""
|
|
160
167
|
name = os.path.basename(os.path.dirname(os.path.abspath(path)))
|
|
@@ -162,10 +169,12 @@ def _gallery_no(path: str):
|
|
|
162
169
|
return int(m.group(1)) if m else None
|
|
163
170
|
|
|
164
171
|
|
|
172
|
+
# 生效条件:gal 非 None 时返回 "图集_%s" % gal,gal 为 None 时返回 "图集_?"。
|
|
165
173
|
def _gallery_ref(gal) -> str:
|
|
166
174
|
return "图集_%s" % (gal if gal is not None else "?")
|
|
167
175
|
|
|
168
176
|
|
|
177
|
+
# 生效条件:无必需形参,按 os.environ.get(VISION_ROOT_ENV) or os.environ.get(LEGACY_VISION_ROOT_ENV) or DEFAULT_VISION_ROOT 取值——某环境变量为空串时视为假值继续回落下一项。
|
|
169
178
|
def vision_root() -> str:
|
|
170
179
|
"""视觉证据归档根:env 覆盖 > 遗留 env > 本仓 data/vision 的父目录。"""
|
|
171
180
|
return (os.environ.get(VISION_ROOT_ENV)
|
|
@@ -179,6 +188,7 @@ aeis_root = vision_root
|
|
|
179
188
|
|
|
180
189
|
# ---- 证据源(只读归档) ---------------------------------------------------
|
|
181
190
|
|
|
191
|
+
# 生效条件:root 下 data/vision/*/*.json 逐文件读;文件 OSError/ValueError、JSON 顶层非 dict、parts 非非空 list、或过滤后(type 与 cond_hash 皆真值的 dict)无记录时跳过该文件,否则收入含 path/gallery/image_id/algo/identity_cond_hash/by_type/by_cond 的 src 并最终返回 out 列表。
|
|
182
192
|
def load_sources(root: str) -> list:
|
|
183
193
|
"""扫描 `AEIS/data/vision/*/*.json`,取逐部件结构化结果(主证据源)。"""
|
|
184
194
|
base = os.path.join(root, "data", "vision")
|
|
@@ -215,6 +225,7 @@ def load_sources(root: str) -> list:
|
|
|
215
225
|
|
|
216
226
|
# ---- 节点正文解析 ---------------------------------------------------------
|
|
217
227
|
|
|
228
|
+
# 生效条件:content 为 None 或空串时按 "" 处理;逐行 strip 后跳过空行与以 # 开头的行,返回首个含 ROOT_MARK 或匹配 _RE_PART 的行,全部无命中返回 ""。
|
|
218
229
|
def _find_body_line(content: str) -> str:
|
|
219
230
|
for ln in (content or "").split("\n"):
|
|
220
231
|
s = ln.strip()
|
|
@@ -225,6 +236,7 @@ def _find_body_line(content: str) -> str:
|
|
|
225
236
|
return ""
|
|
226
237
|
|
|
227
238
|
|
|
239
|
+
# 生效条件:rest 为 None/假值时按 "" 处理,分别用 _RE_COND/_RE_VERDICT/_RE_REASON/_RE_FG 捕获;fg 命中则转 float、ValueError 时置 None;reason 命中并 strip 后为空则置 None;返回含 cond_hash/verdict/reason/fg_ratio 四键的 dict(未命中键值为 None)。
|
|
228
240
|
def _fields(rest: str) -> dict:
|
|
229
241
|
m = _RE_COND.search(rest or "")
|
|
230
242
|
v = _RE_VERDICT.search(rest or "")
|
|
@@ -242,6 +254,7 @@ def _fields(rest: str) -> dict:
|
|
|
242
254
|
"fg_ratio": fg}
|
|
243
255
|
|
|
244
256
|
|
|
257
|
+
# 生效条件:content 无正文行(空串、全为注释/空行、或无 ROOT_MARK 且不匹配 _RE_PART)返回 None;首行含 ROOT_MARK 返回 kind="root" 记录(n_parts 由 _RE_NPARTS 转 int、未命中为 None,cond_hash 取自 _fields);否则须匹配 _RE_PART,不匹配返回 None,匹配后 bbox 按逗号切分对非空项做 int(float(x))(ValueError 则 bbox=None)并返回 kind="part" 记录。
|
|
245
258
|
def parse_node(content: str):
|
|
246
259
|
"""视觉节点正文 → 结构化记录;非视觉节点 → None。"""
|
|
247
260
|
line = _find_body_line(content)
|
|
@@ -268,6 +281,7 @@ def parse_node(content: str):
|
|
|
268
281
|
|
|
269
282
|
# ---- 节点集合 -------------------------------------------------------------
|
|
270
283
|
|
|
284
|
+
# 生效条件:layer 透传给 cg._candidates,nid 取 e["id"] 或 path 去 .md 后须以 prefixes 中任一开头且 cg._read 返回非 None 的 fm 才被计数;加密内容记录 locked=True/parsed=None 且不触发 limit 检查,非加密内容 parse_node 后若 limit 非 None 且节点数已达 limit 即 break(因此最多多计该条)。
|
|
271
285
|
def _vision_nodes(cg, layer=None, prefixes=VISION_PREFIXES, limit=None) -> list:
|
|
272
286
|
"""收集视觉节点(只读 index)。
|
|
273
287
|
|
|
@@ -295,6 +309,7 @@ def _vision_nodes(cg, layer=None, prefixes=VISION_PREFIXES, limit=None) -> list:
|
|
|
295
309
|
return nodes
|
|
296
310
|
|
|
297
311
|
|
|
312
|
+
# 生效条件:nodes 中 parsed 为 dict、kind=="root" 且 cond_hash 为真值的节点,以其 tags[-1](无 tags 时为空串)为标签 setdefault 记录首个 cond_hash,返回标签→cond_hash 的 out。
|
|
298
313
|
def _family_root_cond(nodes) -> dict:
|
|
299
314
|
"""家族标签(image_id) → 根节点 cond_hash。"""
|
|
300
315
|
out = {}
|
|
@@ -306,6 +321,7 @@ def _family_root_cond(nodes) -> dict:
|
|
|
306
321
|
return out
|
|
307
322
|
|
|
308
323
|
|
|
324
|
+
# 生效条件:n["id"] 以 "vpipe_" 开头时先以 roots.get(tags[-1] 或 "") 取 cond——cond 为假返回 (None,"no_family_root"),cond 为真则在 sources 中匹配 identity_cond_hash 成功返回 (s,"identity_cond_hash")、未命中再按该标签匹配 image_id 成功返回 (s,"image_id")、仍失败返回 (None,"no_source_archive");非 vpipe_ 时取 tags 中首个匹配 _RE_IMG_TAG 的 img<N>(未取到则不匹配)按 image_id 命中返回 (s,"image_tag"),否则返回 (None,"no_source_archive")。
|
|
309
325
|
def _pick_source(n, sources, roots) -> tuple:
|
|
310
326
|
"""→ (source, joined_by);无法定位图集 → (None, 原因)。"""
|
|
311
327
|
nid = n["id"]
|
|
@@ -339,6 +355,7 @@ def _pick_source(n, sources, roots) -> tuple:
|
|
|
339
355
|
|
|
340
356
|
# ---- 三档映射 -------------------------------------------------------------
|
|
341
357
|
|
|
358
|
+
# 生效条件:parsed.cond_hash 为真值时按 str(cond_hash) 从 src["by_cond"] 取候选——其中 type 与 parsed["type"] 相同者直接返回 (r,"cond_hash"),否则候选仅 1 条返回 (cands[0],"cond_hash")、多于 1 条返回 (None,None);候选为空或 cond_hash 为假时若 parsed.type 为真则按 src["by_type"].get(type) 命中返回 (r,"type"),否则返回 (None,None)。
|
|
342
359
|
def _match_record(src, parsed, joined_by):
|
|
343
360
|
"""在归档里定位对应部件记录(imgpart 优先 cond_hash 精确,vpipe 按 type)。"""
|
|
344
361
|
if parsed.get("cond_hash"):
|
|
@@ -355,6 +372,7 @@ def _match_record(src, parsed, joined_by):
|
|
|
355
372
|
return None, None
|
|
356
373
|
|
|
357
374
|
|
|
375
|
+
# 生效条件:n["locked"] 为真→BLINDSPOT(reason="locked");否则 parsed 缺失→"unparsed"、kind 非 "part"→"root_no_verdict";否则 _pick_source(n,sources,roots) 无源→以 why 为 reason;否则 _match_record 无记录→"no_matching_part";否则 parsed 与 rec 的 verdict 均为真且不等→"verdict_mismatch";否则 TIER2_FIELDS 中任一字段为 None→"missing_field:…";全部通过才返回 STATUS_WHITEBOX 与 ev(未用到的形参 aeis_root_used 不参与判定)。
|
|
358
376
|
def build_evidence(n, sources, roots, aeis_root_used):
|
|
359
377
|
"""单节点 → (status, evidence, meta);严格三档,缺源即 BLINDSPOT。"""
|
|
360
378
|
parsed = n.get("parsed")
|
|
@@ -397,6 +415,7 @@ def build_evidence(n, sources, roots, aeis_root_used):
|
|
|
397
415
|
|
|
398
416
|
# ---- 预演 / 执行 / 回滚 / 留痕 --------------------------------------------
|
|
399
417
|
|
|
418
|
+
# 生效条件:x 经 _as_cg 转换;prefixes 为假值回落 VISION_PREFIXES、aeis_root_ 为假值回落 aeis_root();ids 为真值时才按 set(ids) 过滤 nodes;对 nodes 调 build_evidence,locked 节点只累加 skipped_locked,白箱项入 items、其余入 blindspot_items,全程不写盘并返回含 aeis_root/sources/nodes_scanned/targeted/blindspot/by_reason 的报表。
|
|
400
419
|
def plan(x, layer=None, prefixes=None, limit=None,
|
|
401
420
|
aeis_root_=None, ids=None) -> dict:
|
|
402
421
|
"""预演:产出证据回填清单,不写盘。"""
|
|
@@ -445,6 +464,7 @@ def plan(x, layer=None, prefixes=None, limit=None,
|
|
|
445
464
|
}
|
|
446
465
|
|
|
447
466
|
|
|
467
|
+
# 生效条件:x 经 _as_cg,batch 为假值回落 BATCH_DEFAULT 并交给 _unique_batch(cg, …) 去重;ids 为真值才按 id 过滤、entry_ids 为真值才按 _entry_id(batch,id) 过滤;循环中节点不在 cg.index["nodes"] 记 skipped_drift,fm 为 None 或内容加密记 skipped_locked,fm 已有同 status(白箱还要求 evidence 相同)记 skipped_already,否则改写 fm 并落盘、追加 jsonl、收集 entry_id;written 非 0 时 cg.rebuild_index() 并尝试 evolution.record(异常被吞)后返回 rep。
|
|
448
468
|
def apply(x, ids=None, entry_ids=None, layer=None, prefixes=None,
|
|
449
469
|
limit=None, batch=None, aeis_root_=None, actor=None) -> dict:
|
|
450
470
|
"""执行回填:逐节点改写 frontmatter 证据面,写 `_vision_evidence.jsonl`。"""
|
|
@@ -528,6 +548,7 @@ def apply(x, ids=None, entry_ids=None, layer=None, prefixes=None,
|
|
|
528
548
|
return rep
|
|
529
549
|
|
|
530
550
|
|
|
551
|
+
# 生效条件:x 经 _as_cg 后读 _log_path(cg) 日志,只处理 action=="vision_evidence" 记录;batch 为真值时仅取 batch 相同记录、entry_ids 为真值时仅取 entry_id 在集合内记录、该 entry_id 已出现在 rollback 日志则记 skipped_done;节点缺失或 _read 返回 fm 为 None 记 missing,EVIDENCE_KEYS 一个都不在 fm 中记 skipped_done,否则删除命中键、写盘并追加 rollback 留痕,reverted 非 0 时重建索引后返回 rep。
|
|
531
552
|
def rollback(x, batch=None, entry_ids=None, actor=None) -> dict:
|
|
532
553
|
"""按留痕反向应用:删除本批次写入的证据键(幂等,防覆盖)。"""
|
|
533
554
|
cg = _as_cg(x)
|
|
@@ -580,6 +601,7 @@ def rollback(x, batch=None, entry_ids=None, actor=None) -> dict:
|
|
|
580
601
|
return rep
|
|
581
602
|
|
|
582
603
|
|
|
604
|
+
# 生效条件:x 经 _as_cg 后逐条读 _log_path(cg),batch 为真值时才按 rec.get("batch")==batch 过滤;limit 非 None 且 >=0 时执行 recs = recs[-limit:](limit=0 因 -0 切片退化为全量),limit 为 None 或负数时不截断,返回 {root,total,returned,records}。
|
|
583
605
|
def history(x, limit=100, batch=None) -> dict:
|
|
584
606
|
cg = _as_cg(x)
|
|
585
607
|
recs = []
|
|
@@ -596,6 +618,7 @@ def history(x, limit=100, batch=None) -> dict:
|
|
|
596
618
|
|
|
597
619
|
# ---- CLI(真实库预演/执行用;MCP 侧走 maintain action) -------------------
|
|
598
620
|
|
|
621
|
+
# 生效条件:argv 为 None 时 argparse 取 sys.argv;--prefixes 默认由 ",".join(VISION_PREFIXES) 提供并切出非空前缀;a.rollback 为真调 rollback(entry_ids 切分后为空则传 None)、否则 a.apply 为真调 apply、否则调 plan;--json 为真打印整份 JSON,否则按固定关键字打印并恒返回 0。
|
|
599
622
|
def _main(argv=None) -> int:
|
|
600
623
|
import argparse
|
|
601
624
|
ap = argparse.ArgumentParser(description="G5 视觉证据回填(默认只预演)")
|
|
@@ -641,4 +664,4 @@ def _main(argv=None) -> int:
|
|
|
641
664
|
|
|
642
665
|
|
|
643
666
|
if __name__ == "__main__": # pragma: no cover
|
|
644
|
-
raise SystemExit(_main())
|
|
667
|
+
raise SystemExit(_main())
|