@furongjun1999/dsh-memory 0.6.0 → 0.7.0
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 +618 -572
- package/codebuddy/CODEBUDDY.md +10 -10
- package/data/policy.json +27 -0
- package/docs/README.md +1 -0
- package/docs/discipline/harnesses.yaml +31 -0
- package/docs/discipline/templates/full.md.tmpl +1 -1
- package/docs/discipline/templates/rules.mdc.tmpl +1 -1
- package/docs/eval/N225_/347/264/242/345/274/225/346/227/245/345/277/227/351/235/236/345/257/271/350/261/241/350/243/205/350/275/275/351/235/242/347/261/273/345/236/213/351/227/270_v1.0.md +418 -0
- package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/344/270/216/351/224/256/347/261/273/345/236/213/351/227/270_v1.1.md +441 -0
- package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/345/212/240/350/275/275/344/270/216/345/207/255/346/215/256/346/230/216/346/226/207/351/230/237/345/210/227_v1.0.md +398 -0
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/200/346/211/271_/346/216/245/347/272/277/344/270/216/347/255/211/344/273/267/345/217/230/346/215/242_v1.0.md +498 -0
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/211/346/211/271_/351/227/250/347/246/201/350/275/254/346/255/243/344/270/216/350/260/203/345/272/246/346/255/242/350/241/200/344/270/216/351/227/250/346/216/247/346/224/266/345/217/243_v1.0.md +669 -0
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/272/214/346/211/271_/344/276/235/350/265/226/351/200/217/344/274/240/344/270/216/345/257/271/346/213/215/345/217/243/345/276/204/344/270/216/350/264/237/347/274/223/345/255/230_v1.0.md +683 -0
- package/docs/eval//345/207/272/350/264/247/351/235/242/345/206/222/347/203/237_/350/277/233/350/264/247/351/227/250/347/246/201_v1.0.md +460 -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//345/217/221/345/270/20308_/350/207/252/350/277/255/344/273/243/344/270/216/347/235/241/347/234/240_/345/233/276/346/243/200/347/264/242/350/267/257/344/270/216/346/235/203/351/207/215_v1.0.md +97 -0
- package/docs/eval//345/275/222/344/270/200/345/261/202/347/274/272/347/234/201/347/277/273/345/205/263_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +458 -0
- package/docs/eval//346/200/247/350/203/275/344/270/223/351/241/271_/345/206/267/346/237/245/350/257/242/344/270/216/345/206/205/345/255/230_v1.0.md +218 -0
- package/docs/eval//346/225/205/351/232/234/346/263/250/345/205/245/345/256/236/346/265/213_v1.0.md +10 -0
- package/docs/eval//347/274/226/347/240/201/351/235/242/345/211/215/347/275/256_/345/205/245/345/217/243/350/207/252/344/277/235/350/257/201UTF8/344/270/216/346/226/207/346/234/254open/345/256/210/345/215/253_v1.0.md +646 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v20.md +283 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v21.md +224 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v22.md +223 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v23.md +293 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v24.md +303 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v25.md +230 -0
- package/docs/hive//346/243/200/347/264/242/347/256/227/346/263/225/345/217/243/345/276/204/345/257/271/347/205/247_v0.1.md +136 -11
- 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 +52 -2
- 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 +100 -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 +57 -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//347/235/241/347/234/240/345/221/250/346/234/237_/350/277/220/347/273/264/345/211/215/346/217/220/344/270/216/347/273/264/346/212/244/346/214/207/345/215/227_v1.0.md +183 -0
- 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//347/235/241/347/234/240/344/270/216/350/207/252/350/277/255/344/273/243_/345/212/237/350/203/275/344/274/230/345/214/226/350/256/276/350/256/241_v0.4.md +547 -0
- package/docs/plans//350/234/202/345/267/242/346/250/241/345/236/213/345/257/206/351/222/245/351/205/215/347/275/256/351/235/242_v1.0.md +385 -0
- package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +460 -460
- package/lib/bridge.js +24 -2
- package/lib/hooks.d.ts +31 -2
- package/lib/hooks.js +219 -16
- package/lib/init.js +16 -1
- package/lib/lib/prompt_safety.d.ts +52 -1
- package/lib/lib/prompt_safety.js +76 -1
- package/md_cg/audit.py +652 -379
- package/md_cg/bench6_arms.py +1 -1
- package/md_cg/bench_e2e_locomo_qa.py +11 -4
- 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 +48 -1
- package/md_cg/consistency.py +97 -9
- package/md_cg/consolidate.py +34 -9
- package/md_cg/crypto.py +59 -26
- package/md_cg/docindex.py +16 -1
- package/md_cg/evolution.py +17 -1
- package/md_cg/export.py +1 -1
- package/md_cg/forgetting.py +309 -13
- package/md_cg/freshness.py +511 -0
- package/md_cg/fsutil.py +454 -8
- package/md_cg/generation.py +409 -0
- package/md_cg/hotcache.py +4 -1
- 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 +4261 -3818
- package/md_cg/mdcg.py +4695 -3547
- package/md_cg/mdcos.py +4672 -4159
- package/md_cg/mreview/pipeline.py +15 -1
- package/md_cg/nodefile.py +713 -576
- package/md_cg/protect.py +158 -11
- package/md_cg/protocol.py +41 -2
- package/md_cg/provenance.py +22 -4
- package/md_cg/reach.py +4 -4
- package/md_cg/readcache.py +76 -15
- package/md_cg/reconcile.py +1 -1
- package/md_cg/refindex.py +246 -37
- package/md_cg/refine.py +1 -1
- package/md_cg/routing.py +32 -5
- package/md_cg/run_tests.py +17 -0
- package/md_cg/scrub.py +30 -3
- package/md_cg/security.py +47 -1
- package/md_cg/self_state.py +5 -5
- package/md_cg/semantic/canonical.py +22 -0
- package/md_cg/semantic/unify.py +73 -22
- package/md_cg/semantic/unify_fixture.json +25 -0
- package/md_cg/sleep.py +1297 -0
- 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 +1436 -1168
- package/md_cg/tasks.py +469 -470
- package/md_cg/test_auto_defaults.py +424 -0
- 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_boundary_hit.py +410 -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_en_pipeline.py +22 -13
- package/md_cg/test_generation_guard.py +352 -0
- 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 +1387 -0
- package/md_cg/test_hive_ingest.py +285 -285
- package/md_cg/test_issue39_utf8_stdio.py +307 -16
- package/md_cg/test_issue43_default_policy.py +865 -0
- package/md_cg/test_legacy_p3_node_id_type.py +375 -0
- package/md_cg/test_linkref.py +10 -3
- package/md_cg/test_lock.py +2 -2
- package/md_cg/test_logref.py +1109 -0
- package/md_cg/test_m3_h9_bucket_health_protect_mark.py +301 -0
- package/md_cg/test_m3_h9_semantic_guard.py +525 -0
- package/md_cg/test_mr_m2.py +15 -3
- package/md_cg/test_n130_verify_falsified_protect.py +1 -1
- package/md_cg/test_n139_dek_provision_failclosed.py +185 -0
- package/md_cg/test_n176_link_trust.py +221 -0
- package/md_cg/test_n178_units_jobid_gate.py +212 -0
- package/md_cg/test_n184_keys_concurrent_provision.py +307 -0
- package/md_cg/test_n195_writepath_reload.py +283 -0
- package/md_cg/test_n196_stale_gate_skip.py +169 -0
- package/md_cg/test_n197_n208_write_face_gates.py +471 -0
- package/md_cg/test_n198_tokens_corrupt_failclosed.py +225 -0
- package/md_cg/test_n199_tokens_concurrent_write.py +378 -0
- package/md_cg/test_n201_proposal_visibility.py +382 -0
- package/md_cg/test_n202_session_notes_visibility.py +461 -0
- package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +542 -0
- package/md_cg/test_n206_stdio_jsonrpc_type.py +403 -0
- package/md_cg/test_n209_verify_write_face_gates.py +461 -0
- package/md_cg/test_n212_n213_n224_generation_gates.py +519 -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_n230_dirty_replay.py +375 -0
- package/md_cg/test_n62_tenant_bind_failclosed.py +200 -0
- package/md_cg/test_neg_condition_hits.py +333 -0
- package/md_cg/test_neg_tail_honesty.py +663 -0
- package/md_cg/test_none_id_write_guard.py +4 -3
- package/md_cg/test_opt_batch1_md_cg.py +451 -0
- package/md_cg/test_p1.py +5 -5
- package/md_cg/test_p11_consistency.py +161 -12
- package/md_cg/test_p26_refindex.py +2 -2
- package/md_cg/test_p27_docindex.py +777 -774
- package/md_cg/test_p28_refcheck.py +740 -13
- package/md_cg/test_p29_session_ingest_export.py +2 -2
- package/md_cg/test_p2_mcp.py +12 -1
- package/md_cg/test_p2_six_elements.py +463 -0
- package/md_cg/test_p30_maintain.py +33 -2
- package/md_cg/test_p31_insight.py +1 -0
- package/md_cg/test_p3_legacy_closure.py +433 -0
- package/md_cg/test_p4_freshness.py +680 -0
- package/md_cg/test_p8_subgraph_chain.py +14 -1
- 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 +522 -0
- package/md_cg/test_read_face_input_gates.py +363 -0
- package/md_cg/test_read_face_semantics.py +489 -0
- package/md_cg/test_recall_face_guards.py +812 -0
- package/md_cg/test_rejected_credential_forms.py +188 -0
- package/md_cg/test_rejected_redact.py +146 -0
- package/md_cg/test_retr_s1b.py +2 -2
- package/md_cg/test_retr_s5.py +20 -7
- package/md_cg/test_review_conformance.py +21 -3
- package/md_cg/test_security_audit_v21.py +2 -2
- package/md_cg/test_semantic_canonical.py +5 -3
- package/md_cg/test_server_version.py +67 -0
- package/md_cg/test_sleep.py +611 -0
- package/md_cg/test_sleep_p1.py +784 -0
- package/md_cg/test_srcindex.py +171 -0
- package/md_cg/test_tenant_env_override_warn.py +63 -50
- package/md_cg/test_time_core_lint.py +968 -0
- package/md_cg/test_token_lowercase_form.py +315 -0
- package/md_cg/test_unify_default_off.py +701 -0
- package/md_cg/test_unify_scope.py +182 -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/aeis_core/time_core.py +8 -0
- 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 +4 -2
- package/skills/plugin.json +1 -1
- package/src/bridge.ts +25 -2
- package/src/hooks.ts +229 -16
- package/src/init.ts +15 -1
- package/src/lib/prompt_safety.ts +88 -1
- package/utf8_boot.py +237 -0
- package/zcode/AGENTS.md +10 -10
package/md_cg/logref.py
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""md_cg · 日志真源桥(P1):认知图 ref 节点 ↔ DSH 日志真源 / 确定性转写。
|
|
3
|
+
|
|
4
|
+
真源身份与区间表的**唯一实现**在 `srcindex`(`session_src_id` / `log_units`)——
|
|
5
|
+
本模块只做桥与读路径:把 `refindex` 的 ref 协议接到「日志真源 + 转写区间」上。
|
|
6
|
+
md_cg 侧**零路径常量、零 scripts 依赖**:转写根由调用方(scripts 侧)注入,
|
|
7
|
+
单点仍在 `scripts/dsh_log_index.py:138/:514-516`;服务态因此不可能「自己决定
|
|
8
|
+
把转写写到哪」(P2 的再生**不在服务态做**,见 `read_index_node`)。
|
|
9
|
+
|
|
10
|
+
两层回读(方案 v0.3 §2、§9 裁定①):
|
|
11
|
+
区间层 `refindex.read_ref(doc_ref)` → 转写文件行区间
|
|
12
|
+
真源身份层 `srcindex.read_unit(src_unit(ref))` → zstd 日志本体(whole 粒度)
|
|
13
|
+
|
|
14
|
+
生效条件:ref 为 `refindex` 写出的 doc_ref(含 path/root/hash),且 root(转写根)
|
|
15
|
+
由调用方注入;`probe_src` 另需 `doc_ref.src` 且在 `MDCG_INGEST_ROOT` 白名单内。
|
|
16
|
+
不适用条件:无 doc_ref 的节点(非索引节点);`src.path` 为空(`--src-path hash`
|
|
17
|
+
降级)时无法探测真源——`probe_src` 如实返回 unresolved,不猜。
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
|
|
23
|
+
from . import refindex, srcindex
|
|
24
|
+
|
|
25
|
+
#: 落库判据的两个状态名(`node_state` 的 state;其余状态见 docstring)
|
|
26
|
+
WRITE_STATES = ("new", "overwrite")
|
|
27
|
+
KIND = "doc_ref"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# 生效条件:path/workspace/session_id 为任意值(缺省按空串处理);返回
|
|
31
|
+
# "<workspace>/<session_id>/<session_id>.md"("/" 分隔,与 doc_ref.path 同形)。
|
|
32
|
+
def default_rel_of(workspace: str, session_id: str) -> str:
|
|
33
|
+
ws = (workspace or "").strip("/\\")
|
|
34
|
+
sid = (session_id or "").strip("/\\")
|
|
35
|
+
return "/".join(p for p in (ws, sid, sid + ".md") if p)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# 生效条件:无入参副作用;返回 doc_ref → srcindex whole 单位的 dict(真源身份层回读入参)。
|
|
39
|
+
def src_unit(ref: dict) -> dict:
|
|
40
|
+
"""doc_ref → `srcindex` 的 whole 单位(真源身份层的回读入参,纯函数)。"""
|
|
41
|
+
src = (ref or {}).get("src")
|
|
42
|
+
src = src if isinstance(src, dict) else {}
|
|
43
|
+
return {"src": src, "span": {"unit": "whole"},
|
|
44
|
+
"span_hash": src.get("file_hash")}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# 生效条件:ref 为 dict(其余按 {} 处理);verify 取 "hash"(缺省,实核 sha256)或其它值
|
|
48
|
+
# (只做 size/mtime 快路径)。返回 status ∈ ok/stale/unverified/dangling/unresolved/denied
|
|
49
|
+
# 与 ok/hash_verified 布尔;无 src 子键或 src.path 为空时返回 unresolved(不抛 KeyError)。
|
|
50
|
+
def probe_src(ref: dict, *, verify: str = "hash") -> dict:
|
|
51
|
+
"""探测真源(zstd 日志)状态——**与 ref 回读挂同一条根白名单闸**。
|
|
52
|
+
|
|
53
|
+
为什么必须挂闸:frontmatter 是写入侧可塞的内容(`mdcg.py:1709 fm.update(extra)`),
|
|
54
|
+
于是 `doc_ref.src.path` 与 `doc_ref.path` 一样是模型可控输入;本函数直接 stat
|
|
55
|
+
并按需整读该文件。故走 `security.check_path_root(..., "MDCG_INGEST_ROOT", "src")`
|
|
56
|
+
——与 `mcp_server.py:2939-2942` 给 ref 回读挂的是同一条闸(未配置=放开、
|
|
57
|
+
配置=realpath 落根内否则拒,fail-closed)。
|
|
58
|
+
|
|
59
|
+
`verify="hash"`(缺省)时**必须**核 sha256 才算 ok:只对 (size, mtime) 相同只能
|
|
60
|
+
给 `unverified`(fail-closed,巡检视为 not-ok)——否则「同长度同时间戳的替换」
|
|
61
|
+
会静默通过。`hash_verified` 布尔用来区分「核过的 ok」与「没核的 ok」。
|
|
62
|
+
"""
|
|
63
|
+
ref = ref or {}
|
|
64
|
+
src = ref.get("src")
|
|
65
|
+
src = src if isinstance(src, dict) else None
|
|
66
|
+
path = (src or {}).get("path") or ""
|
|
67
|
+
base = {"src": src, "path": path, "verify": verify,
|
|
68
|
+
"ok": False, "hash_verified": False}
|
|
69
|
+
if not src:
|
|
70
|
+
return {**base, "status": "unresolved",
|
|
71
|
+
"error": "该 ref 无 src 子键(不是日志真源索引节点),无法探测真源"}
|
|
72
|
+
if not path:
|
|
73
|
+
return {**base, "status": "unresolved",
|
|
74
|
+
"error": "src.path 为空(--src-path hash 降级:真源路径未落库)"
|
|
75
|
+
",无法探测——不猜"}
|
|
76
|
+
if not os.path.isabs(path):
|
|
77
|
+
return {**base, "status": "unresolved",
|
|
78
|
+
"error": "src.path 是相对路径(--src-path rel 降级:dir 未落库),"
|
|
79
|
+
"无绝对根可解析——不猜"}
|
|
80
|
+
from .security import check_path_root
|
|
81
|
+
try:
|
|
82
|
+
check_path_root(path, "MDCG_INGEST_ROOT", "src")
|
|
83
|
+
except PermissionError as exc: # 拒读即 not-ok,绝不降级放行
|
|
84
|
+
return {**base, "status": "denied", "error": str(exc)[:300]}
|
|
85
|
+
if not os.path.isfile(path):
|
|
86
|
+
return {**base, "status": "dangling", "abspath": path,
|
|
87
|
+
"error": "真源不存在(索引已悬空):%s" % path}
|
|
88
|
+
expect = src.get("file_hash")
|
|
89
|
+
if verify == "hash":
|
|
90
|
+
if not expect:
|
|
91
|
+
return {**base, "status": "unverified", "abspath": path,
|
|
92
|
+
"error": "src.file_hash 缺失,无法核 hash(fail-closed:不计 ok)"}
|
|
93
|
+
# 整文件回读走 **P0 的唯一实现**(`srcindex.read_unit(whole)`)——不在这里
|
|
94
|
+
# 再写一份 sha256:与区域哈希同一条教训,两处算法会让「核过了」永远为真。
|
|
95
|
+
u = srcindex.read_unit(src_unit(ref))
|
|
96
|
+
if not u.get("ok"):
|
|
97
|
+
return {**base, "abspath": path, "status": "dangling",
|
|
98
|
+
"error": str(u.get("error"))[:200]}
|
|
99
|
+
match = bool(u.get("hash_match"))
|
|
100
|
+
return {**base, "abspath": path, "bytes": u.get("bytes"),
|
|
101
|
+
"hash": u.get("hash"), "hash_expected": expect,
|
|
102
|
+
"hash_match": match, "hash_verified": match,
|
|
103
|
+
"ok": match, "status": "ok" if match else "stale"}
|
|
104
|
+
try:
|
|
105
|
+
st = os.stat(path)
|
|
106
|
+
except OSError as exc:
|
|
107
|
+
return {**base, "status": "dangling", "abspath": path, "error": str(exc)[:200]}
|
|
108
|
+
same = (src.get("size") == st.st_size
|
|
109
|
+
and abs(float(src.get("mtime") or 0.0) - st.st_mtime) < 1e-6)
|
|
110
|
+
if same:
|
|
111
|
+
return {**base, "abspath": path, "status": "unverified",
|
|
112
|
+
"error": "仅 (size, mtime) 相同、未核 hash ⇒ 不计 ok(fail-closed)"}
|
|
113
|
+
return {**base, "abspath": path, "status": "stale",
|
|
114
|
+
"size": st.st_size, "mtime": st.st_mtime}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# 生效条件:it 为 docindex 口径条目、kind 默认 "doc_ref";返回 {id, state, ...},
|
|
118
|
+
# state ∈ new/unchanged/overwrite/blocked(blocked 带 error 原因)。
|
|
119
|
+
def node_state(cg, it: dict, *, kind: str = KIND) -> dict:
|
|
120
|
+
"""单条条目的落库判据:**读节点自己的 doc_ref**,不读水位台账。
|
|
121
|
+
|
|
122
|
+
判据 = `hash` 与 `lineno/end` **全等**才算已索引。为什么不看 id:
|
|
123
|
+
`docindex.node_id` 是 `path#heading_path` 寻址(docindex.py:294-299),
|
|
124
|
+
同标题换正文时 **id 不变而 hash 变**——「id 撞即跳」会把漂移判成已索引、
|
|
125
|
+
静默放过(N203 的静默正是这么来的)。
|
|
126
|
+
为什么不看 `Ledger` 水位:水位在活库根、且记的 rel 与改写后的
|
|
127
|
+
`doc_ref.path` 不同源(refindex.py:302-317),开它就等于引一条会误判的
|
|
128
|
+
快路径;真源增量改由节点自描述的 `doc_ref.src.file_hash` 承担。
|
|
129
|
+
"""
|
|
130
|
+
nid = refindex.node_id_of(it, kind)
|
|
131
|
+
nodes = (getattr(cg, "index", {}) or {}).get("nodes") or {}
|
|
132
|
+
if nid not in nodes:
|
|
133
|
+
return {"id": nid, "state": "new"}
|
|
134
|
+
try:
|
|
135
|
+
node = cg.get(nid)
|
|
136
|
+
except Exception as exc: # 读隔离/保护抛错 → 拦下,不越权覆写
|
|
137
|
+
return {"id": nid, "state": "blocked", "error": ("cg.get 抛错:%s" % exc)[:200]}
|
|
138
|
+
if not node:
|
|
139
|
+
return {"id": nid, "state": "blocked",
|
|
140
|
+
"error": "索引有条目但 cg.get 读不回(密级/保护)——fail-closed,不覆写"}
|
|
141
|
+
_k, ref = refindex.ref_of(node)
|
|
142
|
+
if not isinstance(ref, dict) or not ref:
|
|
143
|
+
# refindex.ref_of 对无 doc_ref 的节点返回 ('', None):覆写并**计数**(不静默)
|
|
144
|
+
return {"id": nid, "state": "overwrite", "reason": "no_ref"}
|
|
145
|
+
same = (ref.get("hash") == it.get("hash")
|
|
146
|
+
and _int(ref.get("lineno")) == _int(it.get("lineno"))
|
|
147
|
+
and _int(ref.get("end")) == _int(it.get("end")))
|
|
148
|
+
if same:
|
|
149
|
+
return {"id": nid, "state": "unchanged"}
|
|
150
|
+
return {"id": nid, "state": "overwrite", "reason": "hash_or_span",
|
|
151
|
+
"was": {"hash": ref.get("hash"), "lineno": _int(ref.get("lineno")),
|
|
152
|
+
"end": _int(ref.get("end"))},
|
|
153
|
+
"now": {"hash": it.get("hash"), "lineno": _int(it.get("lineno")),
|
|
154
|
+
"end": _int(it.get("end"))}}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# 生效条件:v 可转 int 时返回该 int,否则返回 -1(行位缺失/非法一律判不等,不抛)。
|
|
158
|
+
def _int(v) -> int:
|
|
159
|
+
try:
|
|
160
|
+
return int(v)
|
|
161
|
+
except (TypeError, ValueError):
|
|
162
|
+
return -1
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# 生效条件:items 为 docindex 口径条目序列、transcript_root 为转写根、src 为日志真源身份;
|
|
166
|
+
# 返回 {new/overwritten/unchanged/blocked 计数, ids, rows, sens, encrypted};
|
|
167
|
+
# dry_run 为真时只判定不写库。
|
|
168
|
+
def write_session(cg, items, *, transcript_root: str, src: dict, session: str,
|
|
169
|
+
workspace: str, session_uuid: str, kind: str = KIND,
|
|
170
|
+
rel_of=None, dry_run: bool = False) -> dict:
|
|
171
|
+
"""把一个会话的转写区间条目落库(真源身份挂在 `doc_ref.src`)。
|
|
172
|
+
|
|
173
|
+
· `items` 的 `path` 一律改写成 `rel_of(workspace, session_uuid)`——即转写文件
|
|
174
|
+
**相对转写根**的真实位置(`<ws>/<sid>/<sid>.md`),`doc_ref.root` = 转写根:
|
|
175
|
+
这样 `read_ref(doc_ref)` 的 `join(root, path)` 才落在转写上。裸文件名口径
|
|
176
|
+
在「同 sid 跨工作区」时会撞 id 且回读指错文件,故缺省即改写。
|
|
177
|
+
· 逐条先过 `node_state`:`blocked` 的**不写**(fail-closed,原因透出),
|
|
178
|
+
其余(new / overwrite)交给 `refindex.add_items`(落盘细节与生产同源)。
|
|
179
|
+
· 密级**跟源**:`docindex.sensitivity_for(src["path"], None)` —— 真源路径判,
|
|
180
|
+
只可能更严;落在加密档的条数单独计数(`encrypted`)由调用方告警:v1.0 曾
|
|
181
|
+
误标 private 触发加密致检索零命中,静默复现是不可接受的。
|
|
182
|
+
不改调用方传入的 `items`(内部逐条复制)。
|
|
183
|
+
"""
|
|
184
|
+
if kind != KIND:
|
|
185
|
+
raise ValueError("logref 只服务日志转写(kind=%r)" % KIND)
|
|
186
|
+
rel_of = rel_of or default_rel_of
|
|
187
|
+
rel = rel_of(workspace, session_uuid)
|
|
188
|
+
plan, keep = [], []
|
|
189
|
+
for it in items or []:
|
|
190
|
+
it2 = dict(it)
|
|
191
|
+
it2["path"] = rel
|
|
192
|
+
st = node_state(cg, it2, kind=kind)
|
|
193
|
+
plan.append(st)
|
|
194
|
+
if st["state"] in WRITE_STATES:
|
|
195
|
+
keep.append(it2)
|
|
196
|
+
sens, _why = _docindex().sensitivity_for((src or {}).get("path") or "", None)
|
|
197
|
+
enc = _encrypted_levels()
|
|
198
|
+
rows = [{"id": st["id"], "state": st["state"], "reason": st.get("reason"),
|
|
199
|
+
"heading_path": (it.get("heading_path") or []),
|
|
200
|
+
"lineno": _int(it.get("lineno")), "end": _int(it.get("end")),
|
|
201
|
+
"hash": it.get("hash")}
|
|
202
|
+
for it, st in zip(items or [], plan)]
|
|
203
|
+
out = {
|
|
204
|
+
"transcript_root": transcript_root, "rel": rel,
|
|
205
|
+
"items": len(items or []),
|
|
206
|
+
"new": [st["id"] for st in plan if st["state"] == "new"],
|
|
207
|
+
"overwritten": [st["id"] for st in plan if st["state"] == "overwrite"],
|
|
208
|
+
"overwritten_no_ref": len([st for st in plan
|
|
209
|
+
if st.get("reason") == "no_ref"]),
|
|
210
|
+
"unchanged": [st["id"] for st in plan if st["state"] == "unchanged"],
|
|
211
|
+
"blocked": [{"id": st["id"], "error": st.get("error")}
|
|
212
|
+
for st in plan if st["state"] == "blocked"],
|
|
213
|
+
"rows": rows, "sensitivity": sens, "sens": {}, "src_sensitivity": sens,
|
|
214
|
+
"encrypted": 0, "dry_run": bool(dry_run),
|
|
215
|
+
}
|
|
216
|
+
if dry_run or not keep:
|
|
217
|
+
return out
|
|
218
|
+
|
|
219
|
+
def _extra(it):
|
|
220
|
+
return {"tags": ["dsh-log", "logsrc", "regenerable"]
|
|
221
|
+
+ ([f"ws:{workspace}"] if workspace else []),
|
|
222
|
+
"attrs": {"session": session, "workspace": workspace,
|
|
223
|
+
"dsh_session_uuid": session_uuid}}
|
|
224
|
+
|
|
225
|
+
ids, sens_counts = refindex.add_items(
|
|
226
|
+
cg, keep, kind=kind, root=transcript_root, sensitivity=sens,
|
|
227
|
+
src_of=lambda it: src, extra_of=_extra)
|
|
228
|
+
out["sens"] = sens_counts
|
|
229
|
+
out["written"] = ids
|
|
230
|
+
out["encrypted"] = sum(c for s, c in sens_counts.items() if s in enc)
|
|
231
|
+
return out
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# 生效条件:无入参副作用;返回 docindex 模块(局部 import,避免模块级循环)。
|
|
235
|
+
def _docindex():
|
|
236
|
+
from . import docindex
|
|
237
|
+
return docindex
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# 生效条件:无入参副作用;返回 at-rest 加密档位元组(crypto.ENCRYPTED_LEVELS)。
|
|
241
|
+
def _encrypted_levels() -> tuple:
|
|
242
|
+
from . import crypto
|
|
243
|
+
return tuple(crypto.ENCRYPTED_LEVELS)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# 生效条件:node 为 cg.get 取回的节点 dict(其余按无 ref 处理);with_src 缺省 False。
|
|
247
|
+
# 返回 {ok, status, text/hash_match, src_verified, ...};真源块仅在 with_src 为真时给出。
|
|
248
|
+
def read_index_node(node, *, with_src: bool = False, verify: str = "hash") -> dict:
|
|
249
|
+
"""节点 → 两层回读结果(区间层恒做,真源身份层按 `with_src` 显式开)。
|
|
250
|
+
|
|
251
|
+
`with_src` 缺省 **False**:默认不 stat/哈希真源——每次检索命中都整读一遍大
|
|
252
|
+
zstd 是不可能的代价。返回里恒带 `src_verified: false` 明示「本次未看真源」,
|
|
253
|
+
免得调用方把「没核」读成「核过且 ok」(与 `probe_src` 的 unverified 同源纪律)。
|
|
254
|
+
|
|
255
|
+
状态机(转写层):`ok` / `stale`(转写被重生成或源续写致内容变)⇒ **原样返回
|
|
256
|
+
stale + 建议重跑落库,不自动覆写**;`dangling`(转写被系统清理)⇒ 返回
|
|
257
|
+
`rematerialize` 指路离线工具——**服务态不做再生**(mcp_server 进程零写临时目录)。
|
|
258
|
+
"""
|
|
259
|
+
kind, ref = refindex.ref_of(node)
|
|
260
|
+
if not ref:
|
|
261
|
+
return {"ok": False, "error": "该节点没有 code_ref/doc_ref(不是索引节点)"}
|
|
262
|
+
out = refindex.read_ref(ref, ref_kind=kind)
|
|
263
|
+
# read_ref 的非 ok 只有 dangling/unresolved/error,且**仅 dangling** 置 stale=True
|
|
264
|
+
# (refindex.py:474-478)——故用 stale 位区分 dangling 与 unresolved,不猜。
|
|
265
|
+
if out.get("ok"):
|
|
266
|
+
out["status"] = "stale" if out.get("stale") else "ok"
|
|
267
|
+
else:
|
|
268
|
+
out["status"] = "dangling" if out.get("stale") else "unresolved"
|
|
269
|
+
out["node_ref_kind"] = kind
|
|
270
|
+
out["span_status"] = out["status"]
|
|
271
|
+
out["src_verified"] = False
|
|
272
|
+
if out["status"] == "stale":
|
|
273
|
+
out["suggested_action"] = ("重跑落库(区间哈希已变:转写被重生成或日志续写);"
|
|
274
|
+
"本函数不自动覆写")
|
|
275
|
+
elif out["status"] == "dangling":
|
|
276
|
+
out["rematerialize"] = "scripts/_mdcg_reindex_dshlogs.py --repair 1"
|
|
277
|
+
out["note"] = ("服务态不写临时目录(不自动再生转写);回读落到转写文件行区间,"
|
|
278
|
+
"转写缺失时用上述离线工具再生")
|
|
279
|
+
if with_src:
|
|
280
|
+
out["src"] = probe_src(ref, verify=verify)
|
|
281
|
+
out["src_verified"] = bool((out["src"] or {}).get("hash_verified"))
|
|
282
|
+
else:
|
|
283
|
+
out["src_note"] = "本次未看真源(with_src=False):默认不 stat/哈希 zstd 日志"
|
|
284
|
+
return out
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# 生效条件:cg 的节点带 doc_ref 且 ref['src']['file_hash'] 存在时按会话 src 水位判
|
|
288
|
+
# 「是否需要重切」;返回 {sessions: {sid: {file_hash, changed, n}}}(纯只读)。
|
|
289
|
+
def session_src_state(cg, *, tag: str = "logsrc") -> dict:
|
|
290
|
+
"""按**会话级 src 水位**报每条日志会话的真源哈希现状(P1 的增量口径)。
|
|
291
|
+
|
|
292
|
+
为什么不用 `refindex` 的 `ledger` 增量:`Ledger` 只在活库根开 `_refindex.json`,
|
|
293
|
+
且记的 rel 与改写后的 `doc_ref.path` 不同源(见 `node_state` docstring)。
|
|
294
|
+
日志一路的增量判据改由**节点自描述的 `doc_ref.src.file_hash`** 承担——
|
|
295
|
+
「某会话每个节点的 src 哈希都与当前真源相同」即无需 re-parse。
|
|
296
|
+
"""
|
|
297
|
+
nodes = (getattr(cg, "index", {}) or {}).get("nodes") or {}
|
|
298
|
+
out = {}
|
|
299
|
+
for nid, e in list(nodes.items()):
|
|
300
|
+
if tag not in ((e or {}).get("tags") or []):
|
|
301
|
+
continue
|
|
302
|
+
try:
|
|
303
|
+
node = cg.get(nid)
|
|
304
|
+
except Exception:
|
|
305
|
+
continue
|
|
306
|
+
if not node:
|
|
307
|
+
continue
|
|
308
|
+
_k, ref = refindex.ref_of(node)
|
|
309
|
+
src = (ref or {}).get("src") if isinstance(ref, dict) else None
|
|
310
|
+
if not isinstance(src, dict):
|
|
311
|
+
continue
|
|
312
|
+
ws = src.get("workspace") or ""
|
|
313
|
+
sid = src.get("session_uuid") or ""
|
|
314
|
+
key = f"{ws}/{sid}"
|
|
315
|
+
row = out.setdefault(key, {"file_hash": src.get("file_hash"), "n": 0,
|
|
316
|
+
"path": src.get("path")})
|
|
317
|
+
row["n"] += 1
|
|
318
|
+
if row["file_hash"] != src.get("file_hash"):
|
|
319
|
+
row["file_hash"] = None # 同会话内哈希不一致:不判「未变」
|
|
320
|
+
for row in out.values():
|
|
321
|
+
row["changed"] = None
|
|
322
|
+
if row.get("path"):
|
|
323
|
+
try:
|
|
324
|
+
row["changed"] = (row["file_hash"] != srcindex.file_hash(row["path"]))
|
|
325
|
+
except OSError:
|
|
326
|
+
row["changed"] = True
|
|
327
|
+
return {"sessions": out}
|