@furongjun1999/dsh-memory 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +588 -552
- package/codebuddy/CODEBUDDY.md +10 -10
- package/data/policy.json +27 -0
- package/docs/discipline/harnesses.yaml +31 -0
- package/docs/discipline/templates/full.md.tmpl +1 -1
- package/docs/discipline/templates/rules.mdc.tmpl +1 -1
- package/docs/eval/DSH/346/227/245/345/277/227/347/264/242/345/274/225v2_/345/217/202/350/200/203dsh-TUI_v1.0.md +248 -0
- package/docs/eval/DSH/346/227/245/345/277/227/347/264/242/345/274/225/346/225/210/346/236/234/351/252/214/350/257/201_v1.0.md +209 -0
- package/docs/eval/DSH/347/253/257/347/274/272/351/231/267/344/270/223/351/241/271_v1.0.md +254 -0
- package/docs/eval/N225_/347/264/242/345/274/225/346/227/245/345/277/227/351/235/236/345/257/271/350/261/241/350/243/205/350/275/275/351/235/242/347/261/273/345/236/213/351/227/270_v1.0.md +418 -0
- package/docs/eval/P1b2_/350/257/273/351/235/242/344/273/243/351/231/205/344/277/256/345/244/215_v1.0.md +124 -0
- package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/344/270/216/351/224/256/347/261/273/345/236/213/351/227/270_v1.1.md +441 -0
- package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/345/212/240/350/275/275/344/270/216/345/207/255/346/215/256/346/230/216/346/226/207/351/230/237/345/210/227_v1.0.md +398 -0
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/200/346/211/271_/346/216/245/347/272/277/344/270/216/347/255/211/344/273/267/345/217/230/346/215/242_v1.0.md +498 -0
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/211/346/211/271_/351/227/250/347/246/201/350/275/254/346/255/243/344/270/216/350/260/203/345/272/246/346/255/242/350/241/200/344/270/216/351/227/250/346/216/247/346/224/266/345/217/243_v1.0.md +669 -0
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/272/214/346/211/271_/344/276/235/350/265/226/351/200/217/344/274/240/344/270/216/345/257/271/346/213/215/345/217/243/345/276/204/344/270/216/350/264/237/347/274/223/345/255/230_v1.0.md +683 -0
- package/docs/eval//345/217/221/345/270/20306_/344/270/200/351/224/256/351/205/215/347/275/256/344/270/216DSH0172_v1.0.md +171 -0
- package/docs/eval//345/217/221/345/270/20307_/345/244/226/351/203/250/346/212/245/345/221/212/345/233/233/346/211/271/344/277/256/345/244/215/344/270/216/346/217/222/344/273/266/351/235/242/345/212/240/345/233/272_v1.0.md +205 -0
- package/docs/eval//346/200/247/350/203/275/344/270/223/351/241/271_/345/206/267/346/237/245/350/257/242/344/270/216/345/206/205/345/255/230_v1.0.md +218 -0
- package/docs/eval//346/225/205/351/232/234/346/263/250/345/205/245/345/256/236/346/265/213_v1.0.md +10 -0
- package/docs/eval//347/274/226/347/240/201/351/235/242/345/211/215/347/275/256_/345/205/245/345/217/243/350/207/252/344/277/235/350/257/201UTF8/344/270/216/346/226/207/346/234/254open/345/256/210/345/215/253_v1.0.md +646 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v18.md +183 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v19.md +207 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v20.md +283 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v21.md +224 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v22.md +223 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v23.md +293 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v24.md +303 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v25.md +230 -0
- package/docs/hive//346/243/200/347/264/242/350/267/257/345/276/204/344/270/216/350/256/244/347/237/245/347/273/223/346/236/204/345/245/221/347/272/246_v0.1.md +20 -0
- package/docs/hive//350/234/202/345/267/242M6_ingest/345/256/236/346/226/275/350/256/241/345/210/222_v0.1.md +1 -1
- package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +12 -0
- package/docs/mdcg//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +40 -40
- package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +21 -9
- package/docs/mdcg//347/201/265/346/236/242/350/256/260/345/277/206/345/212/250/350/257/215/345/215/217/350/256/256_v1.0-draft.md +7 -2
- package/docs/mdcg//350/256/244/347/237/245/345/233/276_/347/264/242/345/274/225/344/270/216/345/267/245/347/250/213/350/247/204/350/214/203/345/214/226_/350/256/241/345/210/222_v0.1.md +39 -3
- package/docs/plans//345/205/250/344/270/255/346/226/207/347/274/226/347/240/201/344/270/216/350/234/202/345/267/242/344/273/273/345/212/241/346/240/207/350/257/206/345/245/221/347/272/246_v2.0.md +220 -0
- package/docs/plans//347/234/237/346/272/220/347/264/242/345/274/225_/351/200/232/347/224/250/346/234/272/345/210/266_v0.3.md +227 -0
- package/docs/plans//350/234/202/345/267/242/346/250/241/345/236/213/345/257/206/351/222/245/351/205/215/347/275/256/351/235/242_v1.0.md +385 -0
- package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +460 -460
- package/dsh/cordis-patch-profile-web.example.yml +35 -0
- package/lib/bridge.js +24 -2
- package/lib/cli.d.ts +3 -0
- package/lib/cli.js +66 -0
- package/lib/hooks.d.ts +31 -2
- package/lib/hooks.js +219 -16
- package/lib/init.d.ts +88 -0
- package/lib/init.js +302 -0
- package/lib/lib/prompt_safety.d.ts +52 -1
- package/lib/lib/prompt_safety.js +76 -1
- package/md_cg/audit.py +652 -379
- package/md_cg/bench6_arms.py +1 -1
- package/md_cg/bench_p0.py +1 -1
- package/md_cg/branches.py +2 -2
- package/md_cg/ccgc.py +1071 -1006
- package/md_cg/chain.py +1 -1
- package/md_cg/consistency.py +97 -9
- package/md_cg/consolidate.py +34 -9
- package/md_cg/crypto.py +59 -26
- package/md_cg/docindex.py +16 -1
- package/md_cg/evidence.py +585 -582
- package/md_cg/evolution.py +17 -1
- package/md_cg/export.py +1 -1
- package/md_cg/forgetting.py +309 -13
- package/md_cg/fsutil.py +454 -8
- package/md_cg/identity.py +3 -3
- package/md_cg/insight.py +24 -3
- package/md_cg/linkref.py +1 -1
- package/md_cg/logref.py +327 -0
- package/md_cg/mcp_server.py +4152 -3818
- package/md_cg/mdcg.py +4208 -3462
- package/md_cg/mdcos.py +4491 -4137
- package/md_cg/mreview/pipeline.py +15 -1
- package/md_cg/nodefile.py +639 -575
- package/md_cg/protect.py +158 -11
- package/md_cg/protocol.py +41 -2
- package/md_cg/provenance.py +22 -4
- package/md_cg/reach.py +4 -4
- package/md_cg/readcache.py +76 -15
- package/md_cg/reconcile.py +1 -1
- package/md_cg/refindex.py +246 -37
- package/md_cg/refine.py +1 -1
- package/md_cg/review_cli.py +49 -4
- package/md_cg/routing.py +32 -5
- package/md_cg/run_tests.py +17 -0
- package/md_cg/scrub.py +30 -3
- package/md_cg/security.py +47 -1
- package/md_cg/self_state.py +5 -5
- package/md_cg/sources.py +23 -6
- package/md_cg/srcindex.py +352 -0
- package/md_cg/stg.py +47 -3
- package/md_cg/subgraph.py +2 -2
- package/md_cg/sustain.py +1299 -1168
- package/md_cg/tasks.py +469 -470
- package/md_cg/test_b1_auto_id_multiproc.py +277 -0
- package/md_cg/test_b1b2_write_face.py +417 -0
- package/md_cg/test_b2_sensitivity_landing.py +223 -0
- package/md_cg/test_b3_merge_keeps_content.py +576 -0
- package/md_cg/test_b4_shard_dir_selfheal.py +843 -0
- package/md_cg/test_b4_shard_dir_selfheal_guard.py +238 -0
- package/md_cg/test_c3_transient_read_negative.py +793 -0
- package/md_cg/test_c8_search_rrf_gates.py +502 -0
- package/md_cg/test_ccg_form_parity.py +188 -0
- package/md_cg/test_ccgc.py +28 -3
- package/md_cg/test_govern_directread.py +17 -4
- package/md_cg/test_h2_session_view_norm.py +233 -0
- package/md_cg/test_h4_sustain_snapshot.py +1377 -0
- package/md_cg/test_hive_ingest.py +285 -285
- package/md_cg/test_index_crossprocess_reload.py +301 -0
- package/md_cg/test_issue39_utf8_stdio.py +307 -16
- package/md_cg/test_issue43_default_policy.py +865 -0
- package/md_cg/test_legacy_p3_node_id_type.py +375 -0
- package/md_cg/test_linkref.py +10 -3
- package/md_cg/test_lock.py +2 -2
- package/md_cg/test_logref.py +1109 -0
- package/md_cg/test_m3_h9_bucket_health_protect_mark.py +301 -0
- package/md_cg/test_m3_h9_semantic_guard.py +525 -0
- package/md_cg/test_mr_m2.py +15 -3
- package/md_cg/test_n130_verify_falsified_protect.py +1 -1
- package/md_cg/test_n139_dek_provision_failclosed.py +185 -0
- package/md_cg/test_n176_link_trust.py +221 -0
- package/md_cg/test_n178_units_jobid_gate.py +212 -0
- package/md_cg/test_n184_keys_concurrent_provision.py +307 -0
- package/md_cg/test_n195_writepath_reload.py +283 -0
- package/md_cg/test_n196_stale_gate_skip.py +169 -0
- package/md_cg/test_n197_n208_write_face_gates.py +471 -0
- package/md_cg/test_n198_tokens_corrupt_failclosed.py +225 -0
- package/md_cg/test_n199_tokens_concurrent_write.py +378 -0
- package/md_cg/test_n201_proposal_visibility.py +382 -0
- package/md_cg/test_n202_session_notes_visibility.py +461 -0
- package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +542 -0
- package/md_cg/test_n206_stdio_jsonrpc_type.py +403 -0
- package/md_cg/test_n209_verify_write_face_gates.py +461 -0
- package/md_cg/test_n212_n213_n224_generation_gates.py +513 -0
- package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +563 -0
- package/md_cg/test_n225_nonobject_load.py +1110 -0
- package/md_cg/test_n62_tenant_bind_failclosed.py +200 -0
- package/md_cg/test_neg_condition_hits.py +333 -0
- package/md_cg/test_neg_tail_honesty.py +663 -0
- package/md_cg/test_none_id_write_guard.py +166 -0
- package/md_cg/test_opt_batch1_md_cg.py +451 -0
- package/md_cg/test_p1.py +5 -5
- package/md_cg/test_p11_consistency.py +161 -12
- package/md_cg/test_p26_refindex.py +2 -2
- package/md_cg/test_p27_docindex.py +777 -774
- package/md_cg/test_p28_refcheck.py +740 -13
- package/md_cg/test_p29_session_ingest_export.py +2 -2
- package/md_cg/test_p2_mcp.py +12 -1
- package/md_cg/test_p30_maintain.py +33 -2
- package/md_cg/test_p31_insight.py +1 -0
- package/md_cg/test_p9c_dedup_hints.py +276 -0
- package/md_cg/test_policy_required_ccg.py +426 -0
- package/md_cg/test_protocol.py +22 -0
- package/md_cg/test_rank_parity_score_mode.py +516 -0
- package/md_cg/test_read_face_input_gates.py +363 -0
- package/md_cg/test_read_face_semantics.py +489 -0
- package/md_cg/test_recall_face_guards.py +812 -0
- package/md_cg/test_rejected_credential_forms.py +188 -0
- package/md_cg/test_rejected_redact.py +146 -0
- package/md_cg/test_retr_s1b.py +2 -2
- package/md_cg/test_retr_s5.py +20 -7
- package/md_cg/test_review_cli_attribution.py +177 -0
- package/md_cg/test_review_cli_visibility.py +235 -0
- package/md_cg/test_review_conformance.py +21 -3
- package/md_cg/test_security_audit_v21.py +2 -2
- package/md_cg/test_server_version.py +67 -0
- package/md_cg/test_srcindex.py +171 -0
- package/md_cg/test_tenant_env_override_warn.py +63 -50
- package/md_cg/test_token_lowercase_form.py +315 -0
- package/md_cg/test_v21r1_package_version.py +310 -0
- package/md_cg/test_writepipe.py +10 -4
- package/md_cg/tokens.py +225 -95
- package/md_cg/tool_face.py +4 -4
- package/md_cg/trust.py +49 -5
- package/md_cg/units.py +204 -6
- package/md_cg/weights.py +4 -4
- package/md_cg/whitebox_kb/wisdom/knowledge_points.py +1 -1
- package/md_cg/writelimit.py +42 -8
- package/md_cg/writepipe.py +651 -554
- package/package.json +8 -2
- package/skills/plugin.json +1 -1
- package/src/bridge.ts +25 -2
- package/src/cli.ts +65 -0
- package/src/hooks.ts +229 -16
- package/src/init.ts +361 -0
- package/src/lib/prompt_safety.ts +88 -1
- package/zcode/AGENTS.md +10 -10
package/md_cg/units.py
CHANGED
|
@@ -19,7 +19,15 @@ crosscheck 同款 verdicts 通道回填)。理由:使用者的复核是昂
|
|
|
19
19
|
(同 hive/serve_start.serve_alive;四路口径由
|
|
20
20
|
hive/test_serve_entry.py 机械守卫)
|
|
21
21
|
job 目录 = jobs/<job_id>/{spec.json,status.json,result.json,kill}
|
|
22
|
-
job_id = h<java_ms>_<uuid6
|
|
22
|
+
job_id = 本模块自造旧形态 h<java_ms>_<uuid6>(`_job_id`,**不是** `_submit` 同款);
|
|
23
|
+
hive 提交面自 id 契约 v2(B8)起为四槽 h_<身份>_<任务>_<单元>_<编号>,由
|
|
24
|
+
Rust 侧 `hive alloc-id` 分配——两形态都过 job.rs::valid_job_id(存量零迁移)
|
|
25
|
+
id 字符判据 = **区块白名单**:唯一真源 hive/id_charset_blocks.txt 的区间并集
|
|
26
|
+
([`_valid_job_id`];本模块**读这份数据文件**、不 import hive,
|
|
27
|
+
与 job.rs / mcp_server.py 三处共读同一份 ⇒ 不查任何 Unicode 属性库,
|
|
28
|
+
两侧版本差结构性不可能)。本闸**有意保留**两处放宽(既有对外契约):
|
|
29
|
+
不要求 h 前缀、允许 `-`;拒收面(零宽/双向控制/控制字符/路径成分/尾点/
|
|
30
|
+
首尾空白/Windows 保留设备名)逐条显式断,不靠「白名单外顺带拒」。
|
|
23
31
|
result.json = {"ok":true,"content":...} | {"ok":false,"error":...}
|
|
24
32
|
终态 = done | error | timeout | killed
|
|
25
33
|
拉起 serve = <repo>/hive/target/release/hive serve --jobs <jobs>(detached)
|
|
@@ -269,10 +277,189 @@ def plan(jobs: str = "", model: str = "", fresh_s: float = FRESH_S,
|
|
|
269
277
|
|
|
270
278
|
# 生效条件:无入参,恒返回 "h"+int(time.time()*1000)+"_"+uuid.uuid4().hex 前 6 位组成的字符串。
|
|
271
279
|
def _job_id() -> str:
|
|
272
|
-
"""
|
|
280
|
+
"""本地 id 生成(旧形态 `h<毫秒>_<uuid 前 6 位>`):serve 侧按 'h' 前缀识别任务目录。
|
|
281
|
+
|
|
282
|
+
诚实边界(id 契约 v2 · C2 同类措辞订正):**本函数已不是 `_submit` 的同款实现**
|
|
283
|
+
——`hive/hive_mcp/mcp_server.py::_submit` 自 B8 起调 Rust 侧 `hive alloc-id` 分配
|
|
284
|
+
`h_<身份>_<任务>_<单元>_<编号>`,**不再自造 id**;本模块是 md_cg 的复核派发面,
|
|
285
|
+
**不在 B8 点名的改动面**(契约只列 CLI / MCP 提交面 / orch 透传),故保留旧形态。
|
|
286
|
+
旧形态 id 在新契约下**仍然合法**(存量零迁移:`job.rs::valid_job_id` 收),故此处
|
|
287
|
+
不闯闸、不破坏 serve 领取;但它绕过了分配器的独占创建与五单元闭集 ⇒ 与「分配器
|
|
288
|
+
唯一实现在 Rust 侧」(契约 §五 裁决 3)不齐,宜单独裁决(牵动 md_cg 派发/等待链
|
|
289
|
+
语义与其守卫 `md_cg/test_units_poll.py`,不在本批「只论证订正」的范围内)。
|
|
290
|
+
"""
|
|
273
291
|
return "h%d_%s" % (int(time.time() * 1000), uuid.uuid4().hex[:6])
|
|
274
292
|
|
|
275
293
|
|
|
294
|
+
# ------------------------------------------------ id 字符判据(区块白名单 · 唯一真源)
|
|
295
|
+
|
|
296
|
+
#: 区块表数据文件(**唯一真源**,与 `hive/src/job.rs` / `hive/hive_mcp/mcp_server.py`
|
|
297
|
+
#: **共读同一份数据**;本模块**读数据文件**而不是 import hive ⇒ 不破零依赖家法)。
|
|
298
|
+
_CHARSET_BLOCKS_REL = "hive/id_charset_blocks.txt"
|
|
299
|
+
_MAX_CP = 0x10FFFF
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
# 生效条件:text 为表文件全文时返回区间列表 [(起, 止)]——**唯一解析配方**(与 Rust
|
|
303
|
+
# `job.rs::parse_id_charset_blocks`、`mcp_server._parse_charset_blocks` 同一条,不许各写
|
|
304
|
+
# 一套):每行取 `#` 之前部分后 strip;空行跳过;余下 split('-') 两段 int(x, 16)。
|
|
305
|
+
# 任一行不合形态 / 越界 / 逆序 / 未归并到最小 / 空表 → 抛 ValueError(fail-closed:
|
|
306
|
+
# 绝不静默跳过坏行——跳过一行的表是另一张表,三处读者随即不同判)。
|
|
307
|
+
def _parse_charset_blocks(text: str) -> list:
|
|
308
|
+
"""区块表解析(唯一配方)。"""
|
|
309
|
+
out: list = []
|
|
310
|
+
for lineno, line in enumerate(text.split("\n"), 1):
|
|
311
|
+
body = line.split("#", 1)[0].strip()
|
|
312
|
+
if not body:
|
|
313
|
+
continue
|
|
314
|
+
parts = body.split("-")
|
|
315
|
+
if len(parts) != 2:
|
|
316
|
+
raise ValueError("区块表第 %d 行不是 LO-HI 形态:%r" % (lineno, line))
|
|
317
|
+
lo, hi = int(parts[0], 16), int(parts[1], 16)
|
|
318
|
+
if not (0 <= lo <= hi <= _MAX_CP):
|
|
319
|
+
raise ValueError("区块表第 %d 行区间非法:%r" % (lineno, line))
|
|
320
|
+
if out and lo <= out[-1][1] + 1:
|
|
321
|
+
raise ValueError("区块表第 %d 行未归并到最小(相邻/相交/乱序):%r"
|
|
322
|
+
% (lineno, line))
|
|
323
|
+
out.append((lo, hi))
|
|
324
|
+
if not out:
|
|
325
|
+
raise ValueError("区块表解析出空区间集(fail-closed:空表绝不放行)")
|
|
326
|
+
return out
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
# 生效条件:无入参,返回 (区间表, 错误说明)——文件可读且解析通过 → (tuple 区间表, None);
|
|
330
|
+
# 表缺失 / 不可读 / 解析失败 → ((), 原因)。后两者让判据 fail-closed(对一切字符 False)。
|
|
331
|
+
def _load_charset_blocks() -> tuple:
|
|
332
|
+
"""**模块导入时读一次**(不做每调用 I/O);路径基准 = 本文件上溯两级(= 仓根)。"""
|
|
333
|
+
path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
|
334
|
+
"hive", "id_charset_blocks.txt")
|
|
335
|
+
try:
|
|
336
|
+
with open(path, encoding="utf-8") as fh:
|
|
337
|
+
text = fh.read()
|
|
338
|
+
except OSError as exc:
|
|
339
|
+
return (), "%s 不可读:%s" % (_CHARSET_BLOCKS_REL, exc)
|
|
340
|
+
try:
|
|
341
|
+
return tuple(_parse_charset_blocks(text)), None
|
|
342
|
+
except ValueError as exc:
|
|
343
|
+
return (), "%s 解析失败:%s" % (_CHARSET_BLOCKS_REL, exc)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
#: 判据数据(模块导入时求值一次):区间表 + 读取/解析错误(错误非 None ⇒ 判据一律 False)。
|
|
347
|
+
CHARSET_BLOCKS, CHARSET_BLOCKS_ERROR = _load_charset_blocks()
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
# 生效条件:c 为单字符且其码点落在区间表内 → True;表缺失 / 坏行 / 空表 → False
|
|
351
|
+
# (fail-closed:绝不放行)。
|
|
352
|
+
def _charset_member(c: str) -> bool:
|
|
353
|
+
u = ord(c)
|
|
354
|
+
return any(lo <= u <= hi for lo, hi in CHARSET_BLOCKS)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
# Windows 保留设备名(B4 拒收项;与 `job.rs::RESERVED_DEVICE_NAMES`、
|
|
358
|
+
# `mcp_server.RESERVED_DEVICE_NAMES` 逐项同集——md_cg 只复制这 22 个常量,**还是读数据
|
|
359
|
+
# 而不是 import hive**)。Win32 对**单个路径分量**做设备名解析且**忽略扩展名**
|
|
360
|
+
# (`CON.txt` 与 `CON` 同指设备),故比对的是「首个 `.` 之前」部分;比对**ASCII 大小写
|
|
361
|
+
# 不敏感**(不用 str.upper():它的 Unicode 折叠面比 Rust 的 eq_ignore_ascii_case 宽,
|
|
362
|
+
# 会让两侧分叉)。
|
|
363
|
+
RESERVED_DEVICE_NAMES = (
|
|
364
|
+
"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6",
|
|
365
|
+
"COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6",
|
|
366
|
+
"LPT7", "LPT8", "LPT9",
|
|
367
|
+
)
|
|
368
|
+
_ASCII_UPPER = str.maketrans("abcdefghijklmnopqrstuvwxyz",
|
|
369
|
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
# 生效条件:s 的「首个 `.` 之前」部分与任一保留设备名 **ASCII 大小写不敏感**相等 → True
|
|
373
|
+
# (含 `CON.txt` 这类带扩展名形态)——与 rust `job::is_reserved_device_name` 同判。
|
|
374
|
+
def _is_reserved_device_name(s: str) -> bool:
|
|
375
|
+
base = s.split(".")[0].translate(_ASCII_UPPER)
|
|
376
|
+
return base in RESERVED_DEVICE_NAMES
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
# 生效条件:jid 为 str、长度 1..128、strip() 前后无差异(首尾无空白)、首尾字符都不是
|
|
380
|
+
# "."(拒 "."/".."/"..." 与 Win32 会剥尾点的 "abc." 形态)、逐字符过显式拒收集合后落在
|
|
381
|
+
# **区块白名单**([`_charset_member`],唯一真源 `hive/id_charset_blocks.txt`,与 hive 两侧
|
|
382
|
+
# 共读同一份数据)或属结构字符 `_` `-` `.`,且整 id 与其按 `_` 分段的任一段都不是
|
|
383
|
+
# Windows 保留设备名、且按 `_` 分段的**任一段都不等于** `.` 或 `..`(c7 段级)时返回 True,
|
|
384
|
+
# 否则 False。
|
|
385
|
+
def _valid_job_id(jid) -> bool:
|
|
386
|
+
r"""job_id 结构闸(N178,2026-09-28;2026-09-30 字符集判据换面为**区块白名单**)。
|
|
387
|
+
|
|
388
|
+
同族家法:`hive/hive_mcp/mcp_server.py::_valid_job_id`、
|
|
389
|
+
`hive/src/job.rs::valid_job_id`(三处**共读同一份区块表**,不是三套常量)。
|
|
390
|
+
本闸**保留**的既有放宽(都是既有对外契约,**不得**为「同口径」收紧):
|
|
391
|
+
· **不要求 "h" 前缀**——`md_cg/test_units_poll.py` 的 "j_empty"/"j_good"/
|
|
392
|
+
"j_fail" 是本模块对外契约(poll 面向任意宿主派发器写出的 job 目录),前缀
|
|
393
|
+
收紧会误杀;
|
|
394
|
+
· 允许 `-`(hive 侧拒它:不在区块表内、也不是它的结构字符);
|
|
395
|
+
· 长度上限 128、首尾无空白、首尾非点(拒 "."/".."/"..." 与 "x." 尾点形态)
|
|
396
|
+
一并保留。
|
|
397
|
+
非 str 一律拒(**不** str() 归一:与 legacy P3 同纪律,未校验的强制转换会把对象
|
|
398
|
+
形态洗成合法路径成分)。
|
|
399
|
+
字符类判据 = **区块白名单**(与 hive 两侧同源)——白名单外的一切字符一律拒。
|
|
400
|
+
放宽带来的静默风险**逐条显式挡住**(c9:「isalpha」类判据下 Cf/Cc 天然不入,但
|
|
401
|
+
**不能靠这个**):零宽 U+200B..U+200F/U+2060/U+FEFF、双向控制
|
|
402
|
+
U+202A..U+202E/U+2066..U+2069、控制字符(C0/DEL/C1)**逐条显式判**;`/` `\` `:`
|
|
403
|
+
与 NUL 同样显式拒(路径成分:相对父段 "../outside"、分隔符、盘符/ADS、绝对路径
|
|
404
|
+
会被 os.path.join 吸附);Windows 保留设备名(CON/PRN/AUX/NUL/COM1..9/LPT1..9,
|
|
405
|
+
含 `CON.txt` 带扩展名形态与 ASCII 大小写变体)**整 id 与按 `_` 分段的任一段**逐名
|
|
406
|
+
显式拒——旧 ASCII 白名单下 `CON` 这类全字母 id 是**放行**的,故这是本闸换面时
|
|
407
|
+
**收紧**的那一面(与 hive 两侧同集同判,语料 80-84 行逐例钉死)。
|
|
408
|
+
表缺失 / 坏表 ⇒ 判据一律 False(fail-closed,绝不放行)。
|
|
409
|
+
本闸本批**新增收紧**(c7,2026-09-30):按 `_` 分段的**任一段**等于 `.` 或 `..` 即拒
|
|
410
|
+
——整串口径下 `h_.._x` / `h_a_.._b` 是被收的;段级是面向重构的纵深加固(段可能被当
|
|
411
|
+
父目录段用),与 hive 两侧同判。范围只加这一条:空段(`h__x`)等形态不动。
|
|
412
|
+
"""
|
|
413
|
+
if not isinstance(jid, str) or not jid or len(jid) > 128:
|
|
414
|
+
return False
|
|
415
|
+
if jid != jid.strip():
|
|
416
|
+
return False
|
|
417
|
+
if jid[0] == "." or jid[-1] == ".":
|
|
418
|
+
return False
|
|
419
|
+
for c in jid:
|
|
420
|
+
u = ord(c)
|
|
421
|
+
if u <= 0x1F or 0x7F <= u <= 0x9F or c in "/\\:":
|
|
422
|
+
return False # 控制字符(C0/DEL/C1,含 NUL)与路径成分(显式判,c9)
|
|
423
|
+
if "\u200b" <= c <= "\u200f" or c in ("\u2060", "\ufeff"):
|
|
424
|
+
return False # 零宽/不可见注入载体(显式判,c9)
|
|
425
|
+
if "\u202a" <= c <= "\u202e" or "\u2066" <= c <= "\u2069":
|
|
426
|
+
return False # 双向控制(显式判,c9)
|
|
427
|
+
if c in "_.-":
|
|
428
|
+
continue # 结构字符:槽分隔符 / 点(首尾点已在上方拒)/ 既有放宽的 `-`
|
|
429
|
+
if not _charset_member(c):
|
|
430
|
+
return False
|
|
431
|
+
# Windows 保留设备名(显式判,c7/c9):整 id(首个 `.` 之前部分,Win32 忽略扩展名)
|
|
432
|
+
# 与按 `_` 分段的**任一段**(Win32 的设备名解析是按路径分量做的)——与
|
|
433
|
+
# `job.rs::valid_job_id` / `mcp_server._job_id_reject_reason` 同集同判。
|
|
434
|
+
if _is_reserved_device_name(jid):
|
|
435
|
+
return False
|
|
436
|
+
if any(_is_reserved_device_name(seg) for seg in jid.split("_")):
|
|
437
|
+
return False
|
|
438
|
+
# 「. / ..」的**段级**判据(c7,2026-09-30 加固):按 `_` 分段,任一段等于 `.` 或 `..`
|
|
439
|
+
# 即拒——与 `job.rs::valid_job_id` / `mcp_server._job_id_reject_reason` **三处同判**。
|
|
440
|
+
# 段级而非整串(本面原有的「首尾非点」只挡整串与尾点形态):id 的段可能被宿主当
|
|
441
|
+
# **目录名**或**父目录段**用(契约 §四.4「目录可作它的父段」)。现场定性:`h_.._x` /
|
|
442
|
+
# `h_a_.._b` 这类「含 `..` 段但不以点结尾」的 id 拼出的路径停在池内、不构成穿越 ⇒
|
|
443
|
+
# 面向重构的纵深加固。范围**只加这一条**:不拒空段(`h__x`)等未被裁定的形态。
|
|
444
|
+
if any(seg in (".", "..") for seg in jid.split("_")):
|
|
445
|
+
return False
|
|
446
|
+
return True
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
# 生效条件:无入参,返回 job_id 非法的结构化拒绝 dict(ok=False、state="invalid_job_id"、terminal=False、job_dir/content/status 均为 None、error 含非法 id 与本闸口径),字段集与 poll 的返回同构。
|
|
450
|
+
def _bad_job_id(job_id, *, where: str) -> dict:
|
|
451
|
+
"""非法 job_id 的统一拒答(poll/wait/submit 同构,便于调用方机械判别)。"""
|
|
452
|
+
return {"job_id": job_id, "job_dir": None, "state": "invalid_job_id",
|
|
453
|
+
"terminal": False, "ok": False, "content": None, "status": None,
|
|
454
|
+
"error": ("job_id 非法:%r(%s)——job_id 须为**单个路径分量**:区块白名单"
|
|
455
|
+
"(hive/id_charset_blocks.txt)内的字母/数字,或结构字符 `_` `-` `.`;"
|
|
456
|
+
"首尾非点,不含 / \\ : 与控制/零宽/双向控制字符/NUL,"
|
|
457
|
+
"且整 id 与按 `_` 分段的任一段都不得是 Windows 保留设备名"
|
|
458
|
+
"(CON/PRN/AUX/NUL/COM1..9/LPT1..9,含 CON.txt 形态)"
|
|
459
|
+
"(N178:拒 ../victim 型越池读 result.json、"
|
|
460
|
+
"拒任意目录建目录写 spec.json)" % (job_id, where))}
|
|
461
|
+
|
|
462
|
+
|
|
276
463
|
# 生效条件:cg.root 与 cg.cg.root 都取不到真值时不写、直接返回 None;取到 root 时向 root/LOG_NAME 追加一行 rec(副本,setdefault ts)的 JSON,写入抛 OSError 时被吞掉静默返回 None。
|
|
277
464
|
def _log(cg, rec: dict) -> None:
|
|
278
465
|
"""留痕 _units.jsonl(对齐 _crosscheck.jsonl / _backfill.jsonl 纪律)。"""
|
|
@@ -288,7 +475,7 @@ def _log(cg, rec: dict) -> None:
|
|
|
288
475
|
pass
|
|
289
476
|
|
|
290
477
|
|
|
291
|
-
# 生效条件:role 不在 ROLES 时返回 ok=False 的未知角色 error;否则 model_name(model) 为空时返回 ok=False 的未配置复核模型;否则 str(prompt or "").strip() 为空时返回 ok=False 的 prompt
|
|
478
|
+
# 生效条件:role 不在 ROLES 时返回 ok=False 的未知角色 error;否则 model_name(model) 为空时返回 ok=False 的未配置复核模型;否则 str(prompt or "").strip() 为空时返回 ok=False 的 prompt 为空;否则落盘前先过 _valid_job_id 结构闸(未过返回 ok=False 的 job_id 非法,**不建目录不写文件**——N178:job_id 来源一旦不是 _job_id(),join 后就是「任意目录 + 生成名」的建目录/写文件原语);闸过则在 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。
|
|
292
479
|
def submit(*, prompt: str, role: str = REFLECT, model: str = "", system_prompt: str = "",
|
|
293
480
|
context_files=None, timeout_s: int = DEFAULT_TIMEOUT_S,
|
|
294
481
|
max_tokens: int = DEFAULT_MAX_TOKENS, temperature=None, jobs: str = "",
|
|
@@ -302,6 +489,10 @@ def submit(*, prompt: str, role: str = REFLECT, model: str = "", system_prompt:
|
|
|
302
489
|
if not str(prompt or "").strip():
|
|
303
490
|
return {"ok": False, "error": "prompt 为空"}
|
|
304
491
|
jd, job_id = jobs_dir(jobs), _job_id()
|
|
492
|
+
# N178(2026-09-28):**落盘前**过闸——makedirs 之后再过就等于已经建了目录。
|
|
493
|
+
if not _valid_job_id(job_id):
|
|
494
|
+
return {"ok": False, "job_dir": None, "job_id": job_id,
|
|
495
|
+
"error": _bad_job_id(job_id, where="submit 落点")["error"]}
|
|
305
496
|
d = os.path.join(jd, job_id)
|
|
306
497
|
spec = {"model": mdl, "user_prompt": str(prompt), "system_prompt": str(system_prompt or ""),
|
|
307
498
|
"timeout_s": int(timeout_s), "max_tokens": int(max_tokens),
|
|
@@ -330,10 +521,14 @@ def submit(*, prompt: str, role: str = REFLECT, model: str = "", system_prompt:
|
|
|
330
521
|
"unit_role": role, "model": mdl}
|
|
331
522
|
|
|
332
523
|
|
|
333
|
-
# 生效条件:jobs_dir(jobs)/
|
|
524
|
+
# 生效条件:job_id 先过 _valid_job_id 结构闸(未过返回 state="invalid_job_id" 的结构化拒绝,**不触盘**——N178:修前 "../victim" 会被 join 成池外目录并读出其 result.json 全文);闸过时 jobs_dir(jobs)/job_id 不是目录返回 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 为原值。
|
|
334
525
|
def poll(job_id: str, jobs: str = "") -> dict:
|
|
335
526
|
"""读 job 终态视图:**以 result.json 出现为终态主判据**,status.json 仅作辅助。"""
|
|
336
|
-
|
|
527
|
+
# N178(2026-09-28):调用方给的 job_id 是路径成分,**先过闸再 join**。
|
|
528
|
+
# 修前此处无闸,且 `str(job_id or "")` 还会把非 str 洗成合法成分。
|
|
529
|
+
if not _valid_job_id(job_id):
|
|
530
|
+
return _bad_job_id(job_id, where="poll 池路径成分")
|
|
531
|
+
d = os.path.join(jobs_dir(jobs), job_id)
|
|
337
532
|
out = {"job_id": job_id, "job_dir": d, "state": "missing", "terminal": False,
|
|
338
533
|
"ok": False, "content": None, "error": None, "status": None}
|
|
339
534
|
if not os.path.isdir(d):
|
|
@@ -377,10 +572,13 @@ def poll(job_id: str, jobs: str = "") -> dict:
|
|
|
377
572
|
return out
|
|
378
573
|
|
|
379
574
|
|
|
380
|
-
#
|
|
575
|
+
# 生效条件:job_id 未过 _valid_job_id 结构闸时**立即**返回 poll 的同一拒绝结构(N178:否则非法 id 会空转满 timeout_s 才超时返回——拒绝不该以等待为代价);闸过时循环 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 非终态即超时返回)。
|
|
381
576
|
def wait(job_id: str, *, jobs: str = "", timeout_s: float = DEFAULT_TIMEOUT_S,
|
|
382
577
|
poll_s: float = DEFAULT_POLL_S) -> dict:
|
|
383
578
|
"""阻塞等终态;超时如实返回(不假装成功、不强杀 job)。"""
|
|
579
|
+
if not _valid_job_id(job_id):
|
|
580
|
+
# N178(2026-09-28):fail-fast——复用 poll 的拒绝结构,绝不进轮询。
|
|
581
|
+
return poll(job_id, jobs)
|
|
384
582
|
t0 = time.time()
|
|
385
583
|
while True:
|
|
386
584
|
cur = poll(job_id, jobs)
|
package/md_cg/weights.py
CHANGED
|
@@ -405,8 +405,8 @@ def basis_trust(basis):
|
|
|
405
405
|
def coverage_index(cg) -> dict:
|
|
406
406
|
"""入度表:nid → 被多少节点指向(覆盖度,O(N) 免读文件)。"""
|
|
407
407
|
nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
|
|
408
|
-
indeg = {nid: 0 for nid in nodes}
|
|
409
|
-
for nid, e in nodes.items():
|
|
408
|
+
indeg = {nid: 0 for nid in list(nodes)}
|
|
409
|
+
for nid, e in list(nodes.items()):
|
|
410
410
|
for t in _targets(e):
|
|
411
411
|
if t in indeg and t != nid:
|
|
412
412
|
indeg[t] += 1
|
|
@@ -422,7 +422,7 @@ def redundancy_map(cg, layer=None) -> dict:
|
|
|
422
422
|
"""
|
|
423
423
|
nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
|
|
424
424
|
seen, out = {}, {}
|
|
425
|
-
for nid, e in nodes.items():
|
|
425
|
+
for nid, e in list(nodes.items()):
|
|
426
426
|
if layer and e.get("layer") != layer:
|
|
427
427
|
continue
|
|
428
428
|
h = e.get("content_hash") or ""
|
|
@@ -480,7 +480,7 @@ def recalc(cg, layer=None, limit=None, apply=False, min_delta=APPLY_DELTA,
|
|
|
480
480
|
nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
|
|
481
481
|
indeg = coverage_index(cg)
|
|
482
482
|
red = redundancy_map(cg, layer=layer)
|
|
483
|
-
ids = [nid for nid, e in nodes.items()
|
|
483
|
+
ids = [nid for nid, e in list(nodes.items())
|
|
484
484
|
if not layer or e.get("layer") == layer]
|
|
485
485
|
ids.sort()
|
|
486
486
|
if limit:
|
|
@@ -160,7 +160,7 @@ class KnowledgePointSplitter:
|
|
|
160
160
|
existing = set()
|
|
161
161
|
if self.cg is not None:
|
|
162
162
|
pref = f"card:{card_id[:16]}"
|
|
163
|
-
for nid, e in self.cg.index["nodes"].items():
|
|
163
|
+
for nid, e in list(self.cg.index["nodes"].items()):
|
|
164
164
|
if pref not in (e.get("tags") or []):
|
|
165
165
|
continue
|
|
166
166
|
try:
|
package/md_cg/writelimit.py
CHANGED
|
@@ -32,7 +32,7 @@ import os
|
|
|
32
32
|
import re
|
|
33
33
|
import time
|
|
34
34
|
|
|
35
|
-
from . import lifecycle
|
|
35
|
+
from . import lifecycle, protect
|
|
36
36
|
from .fsutil import FileLock, atomic_write
|
|
37
37
|
|
|
38
38
|
STATE_FILE = "_writelimit.json"
|
|
@@ -207,30 +207,64 @@ def record_accepted(cg, node_id, content, now=None) -> None:
|
|
|
207
207
|
_save(cg, st)
|
|
208
208
|
|
|
209
209
|
|
|
210
|
-
# 生效条件:cg.get(target) 为假值返回 {"ok": False, "error": "target_missing"}
|
|
211
|
-
def converge_into(cg, target: str, content: str
|
|
210
|
+
# 生效条件:cg.get(target) 为假值返回 {"ok": False, "error": "target_missing"};随后以节点真层/敏感度过 protect.guard_overwrite(层闸 + 保护闸,override 为真时先快照 + 审计);通过后按 forgetting.aggregate_line(单点)把新正文里目标还没有的行追加为聚合行、fm["merge_count"]=int(fm.get("merge_count") or 0)+1 后 _write_node 落盘(重要性不变),并把 session/actor 归属并列记进 fm(forgetting.note_source);返回 {"ok": True, "merge_count": 新值, "content_sink": 去向, "source": 归属}。
|
|
211
|
+
def converge_into(cg, target: str, content: str, override=False, actor=None,
|
|
212
|
+
session=None) -> dict:
|
|
212
213
|
"""同构聚合落库:正文追加一行【聚合】摘要,merge_count+1。
|
|
213
214
|
|
|
214
215
|
importance **不变**——流水不该越聚越重要(与 forgetting.reinforce
|
|
215
|
-
的 +0.05
|
|
216
|
+
的 +0.05 相反)。
|
|
217
|
+
|
|
218
|
+
B3(2026-09-30):追加内容改由 `forgetting.aggregate_line`(**单点**)
|
|
219
|
+
生成——原口径 `" ".join(content.split())[:80]` 会把落在 80 字外的新值
|
|
220
|
+
整体丢掉(P-3 实测:端口 9090 变体在库中 0 次落盘),与「新正文不许被
|
|
221
|
+
静默丢弃」同病;现改为「目标正文还没有的行」逐行追加、不截断,纯重复
|
|
222
|
+
零追加。**不变**的部分:目标节点不变、merge_count+1、importance 不动、
|
|
223
|
+
层闸/保护闸仍在写盘之前。
|
|
224
|
+
H3:`session` 槽把新写入方归属并列落 fm(`forgetting.note_source`)。
|
|
225
|
+
|
|
226
|
+
N215(2026-09-28,同族未接线写点):本函数此前**裸调 `cg._write_node`**
|
|
227
|
+
追加既有节点正文,唯一入口 `mdcos.remember_gated` 的 CONVERGE 分支(:3336)
|
|
228
|
+
全程无 principal 层闸、无保护闸、无快照无审计。`writelimit.check` 的形参层
|
|
229
|
+
判据(:141)只看调用方声明的 layer、签名映射落的是 `node_id`——攻击链:
|
|
230
|
+
先以 `(node_id=<anchor 节点>, layer='contextual')` 预占位签名(该次 `add`
|
|
231
|
+
被保护闸拒,但 `sig→anchor id` 已落盘),再换正文重发 → CONVERGE →
|
|
232
|
+
**anchor 层不可篡改节点被无痕追加聚合行**(可反复追加、merge_count 递增)。
|
|
233
|
+
落盘前统一过 `protect.guard_overwrite`(单点)。
|
|
216
234
|
"""
|
|
217
235
|
e = cg.get(target)
|
|
218
236
|
if not e:
|
|
219
237
|
return {"ok": False, "error": "target_missing"}
|
|
220
238
|
fm = dict(e.get("frontmatter") or {})
|
|
221
|
-
|
|
222
|
-
|
|
239
|
+
protect.guard_overwrite(cg, target, layer=fm.get("layer"),
|
|
240
|
+
sensitivity=fm.get("sensitivity"),
|
|
241
|
+
override=override, actor=actor)
|
|
242
|
+
# 延迟导入:forgetting 是聚合行/归属的单点实现(writelimit 不反向被它依赖,
|
|
243
|
+
# 无环);放函数内避免模块导入期把 md_cg.forgetting 的依赖面(mdcg/bigrams)
|
|
244
|
+
# 拉进本模块的加载链。
|
|
245
|
+
from . import forgetting
|
|
246
|
+
new_body, line, line_no = forgetting.aggregate_line(e.get("content"), content)
|
|
223
247
|
fm["merge_count"] = int(fm.get("merge_count") or 0) + 1
|
|
224
248
|
fm["last_merge_at"] = time.time()
|
|
249
|
+
source = forgetting.note_source(fm, session=session, actor=actor)
|
|
225
250
|
cg._write_node(target, os.path.join(cg.root, e["path"]), fm,
|
|
226
|
-
(e.get("content") or "")
|
|
251
|
+
new_body if new_body is not None else (e.get("content") or ""),
|
|
227
252
|
durable=True)
|
|
228
253
|
entry = cg.index["nodes"].get(target)
|
|
229
254
|
if entry is not None:
|
|
230
255
|
entry.update({"merge_count": fm["merge_count"],
|
|
231
256
|
"last_merge_at": fm["last_merge_at"]})
|
|
257
|
+
if fm.get("merge_sources"):
|
|
258
|
+
entry["merge_sources"] = fm["merge_sources"]
|
|
232
259
|
cg._dirty[target] = entry
|
|
233
|
-
return {"ok": True, "merge_count": fm["merge_count"]
|
|
260
|
+
return {"ok": True, "merge_count": fm["merge_count"],
|
|
261
|
+
"content_sink": {
|
|
262
|
+
"node_id": target,
|
|
263
|
+
"action": "appended" if line else "already_covered",
|
|
264
|
+
"line": line, "line_no": line_no,
|
|
265
|
+
"chars": len(new_body if new_body is not None
|
|
266
|
+
else (e.get("content") or ""))},
|
|
267
|
+
"source": source}
|
|
234
268
|
|
|
235
269
|
|
|
236
270
|
# ---------- 读侧:已落盘同构组的周期整理(sustain 巡检入口)----------
|