@furongjun1999/dsh-memory 0.7.0 → 0.7.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 +7 -6
- package/docs/eval/cons200_/345/206/262/347/252/201/346/243/200/346/265/213/351/200/211/351/235/242_/345/256/236/346/226/275/350/256/260/345/275/225_v1.0.md +223 -0
- package/docs/eval/cons200_/345/206/262/347/252/201/346/243/200/346/265/213/351/200/211/351/235/242_/345/256/236/346/226/275/350/256/260/345/275/225_v1.1.md +340 -0
- package/docs/eval/issue50_/345/205/203/346/225/260/346/215/256/351/200/217/344/274/240/344/270/216/345/205/234/345/272/225_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +437 -0
- package/docs/eval/issue50_/345/215/212/351/207/215/345/244/215/345/276/205/345/256/232/345/244/215/346/240/270_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +488 -0
- package/docs/eval/issue50_/345/276/205/345/256/232/345/244/215/346/240/270/345/205/245/351/230/237_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +428 -0
- package/docs/eval/issue50_/350/257/273/351/235/242/344/277/235/346/212/244/345/217/252/350/256/244/346/230/276/345/274/217/346/235/245/346/272/220_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +271 -0
- package/docs/eval/issue50_/351/207/215/350/246/201/345/272/246/345/220/214/346/272/220/344/270/216/344/277/235/346/212/244/350/257/255/344/271/211_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +306 -0
- package/docs/eval/issue51_/344/270/200/351/224/256/345/256/211/350/243/205/345/244/261/350/264/245_/345/275/222/345/261/236/345/210/244/345/256/232_v1.0.md +58 -0
- package/docs/eval/issue52_/346/235/241/344/273/266/345/205/210/350/241/214/344/270/216/346/210/252/346/226/255/345/217/257/350/247/202/346/265/213_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +360 -0
- package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/241/346/241/243/344/275/215/345/215/225/344/270/200/345/205/245/345/217/243_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +651 -0
- package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/242/345/217/230/346/233/264/345/215/225/344/270/216/345/233/236/346/273/232/345/216/237/350/257/255_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +439 -0
- package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/243/345/207/206/345/205/245/350/257/273/346/225/260_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +1337 -0
- package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/244/346/224/266/345/256/230/344/270/216/345/205/250/351/223/276/351/252/214/346/224/266_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +1817 -0
- package/docs/eval//345/217/221/345/270/20309_/346/243/200/347/264/242/351/235/242/344/270/211/346/211/271/346/224/266/345/217/243_v1.0.md +66 -0
- package/docs/eval//347/254/2543/345/261/202stg/347/273/223/346/236/204/347/264/242/345/274/225_/345/256/236/346/226/275/350/256/260/345/275/225_v1.0.md +250 -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 +25 -25
- package/docs/plans/stg/346/235/241/344/273/266/345/214/226/344/270/216/347/273/223/346/236/204/347/264/242/345/274/225_/350/256/276/350/256/241_v0.1.md +124 -0
- package/docs/plans//350/256/260/345/277/206/350/207/252/345/244/204/347/220/206/344/270/211/346/241/243/350/207/252/346/262/273_/350/256/276/350/256/241_v0.2.md +207 -0
- package/md_cg/admission.py +718 -0
- package/md_cg/autonomy_modes.py +642 -0
- package/md_cg/consistency.py +133 -8
- package/md_cg/forgetting.py +38 -6
- package/md_cg/freshness.py +17 -1
- package/md_cg/lifecycle.py +30 -2
- package/md_cg/mcp_server.py +63 -7
- package/md_cg/mdcg.py +85 -17
- package/md_cg/mdcos.py +666 -16
- package/md_cg/protect.py +79 -4
- package/md_cg/provenance.py +1 -1
- package/md_cg/review_cli.py +31 -2
- package/md_cg/rollback.py +411 -0
- package/md_cg/rollback_cli.py +104 -0
- package/md_cg/stg.py +400 -50
- package/md_cg/stgidx.py +281 -0
- package/md_cg/test_autonomy_admission.py +1098 -0
- package/md_cg/test_autonomy_modes.py +1972 -0
- package/md_cg/test_b1_auto_id_multiproc.py +7 -0
- package/md_cg/test_b1b2_write_face.py +7 -0
- package/md_cg/test_b3_merge_keeps_content.py +7 -0
- package/md_cg/test_cons200_scan_selection.py +791 -0
- package/md_cg/test_i50a_half_dup_defer.py +408 -0
- package/md_cg/test_i50b_defer_to_review_queue.py +545 -0
- package/md_cg/test_i50c_meta_passthrough.py +608 -0
- package/md_cg/test_i50d_importance_source.py +531 -0
- package/md_cg/test_i50e_readside_protection.py +656 -0
- package/md_cg/test_issue39_utf8_stdio.py +9 -1
- package/md_cg/test_issue52_scan_condition_first.py +677 -0
- package/md_cg/test_linkref.py +7 -0
- package/md_cg/test_mode_parity.py +1393 -0
- package/md_cg/test_mutation_rollback.py +805 -0
- package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +1 -1
- package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +7 -0
- package/md_cg/test_p2_mcp.py +8 -0
- package/md_cg/test_p9_forget_protect.py +7 -0
- package/md_cg/test_p9c_dedup_hints.py +7 -0
- package/md_cg/test_policy_required_ccg.py +5 -1
- package/md_cg/test_protocol.py +7 -0
- package/md_cg/test_stgidx_index_parity.py +1020 -0
- package/md_cg/test_writelimit.py +36 -9
- package/md_cg/test_writepipe.py +5 -2
- package/md_cg/weights.py +15 -1
- package/md_cg/writepipe.py +101 -2
- package/package.json +1 -1
- package/skills/plugin.json +1 -1
package/md_cg/stg.py
CHANGED
|
@@ -11,9 +11,24 @@
|
|
|
11
11
|
timeline(...) 按时间排序
|
|
12
12
|
anchors(...) 落在给定时间窗 / 空间范围内的节点
|
|
13
13
|
consistency() 时空字段自洽性检查
|
|
14
|
+
|
|
15
|
+
**分层(issue #52 线)**:
|
|
16
|
+
· 第 1/2 层(已收口):条件先于限额 + 截断可观测——`_scan` 只做「遍历 +
|
|
17
|
+
layer/可见性过滤」,条件过滤与 `_cap_hits` 截断归各接口。
|
|
18
|
+
· 第 3 层(本模块本轮):结构索引直取(`flag MDCG_STG_INDEX`,**默认关**)——
|
|
19
|
+
条件维(session/layer/time_window)经 `md_cg/stgidx` 的三张内存倒排表直取
|
|
20
|
+
子集,**只改取数面、不改语义面**;表缺失/代际不符一律回退第 1 层全量遍历
|
|
21
|
+
并在返回体 `index` 读数里带 reason(禁止静默)。设计稿与签收记录:
|
|
22
|
+
`docs/plans/stg条件化与结构索引_设计_v0.1.md`。
|
|
23
|
+
· 条件资格首验(`flag MDCG_STG_QUALIFY`,**默认关**):对返回条目复用 read 面
|
|
24
|
+
单点 `MdCG.judge_qualification`,结果随返回体带出(`qualification` 字段)——
|
|
25
|
+
**只上报不过滤**(契约 §3.4;硬过滤另立裁定)。
|
|
14
26
|
"""
|
|
15
27
|
from __future__ import annotations
|
|
16
28
|
|
|
29
|
+
import os
|
|
30
|
+
|
|
31
|
+
from . import stgidx
|
|
17
32
|
from . import trust
|
|
18
33
|
|
|
19
34
|
TIME_RELATIONS = ("before", "after", "equals", "contains", "during", "overlaps")
|
|
@@ -115,45 +130,238 @@ def _node(cg, node_id):
|
|
|
115
130
|
"content": n.get("content") or ""}
|
|
116
131
|
|
|
117
132
|
|
|
118
|
-
# 生效条件:
|
|
119
|
-
def
|
|
133
|
+
# 生效条件:nid/e 为一条快照条目、layer 为层过滤值时——layer 为真值且 e.get("layer") != layer 即返回 None;cg 带可调用的 _readable(MdCGSecure)且判不可见即返回 None;e 含 "temporal" 或 "spatial" 键时以快照字段构造 frontmatter,否则调用 cg._read(e) 且在 fm 为 None 时返回 None;返回 {"id","frontmatter","layer","path"}。
|
|
134
|
+
def _scan_one(cg, nid, e, layer=None):
|
|
135
|
+
"""单条目 → 候选条目(`_scan` 的逐条实现**单点**:全量遍历与索引子集共用)。
|
|
136
|
+
|
|
137
|
+
第 3 层只换「候选面怎么来」(全量快照 vs 索引子集),逐条的构造与过滤
|
|
138
|
+
一字不改地留在这一处——等价性(flag 开/关逐位一致)由构造保证,而不是
|
|
139
|
+
靠两条路径各自对齐。
|
|
140
|
+
"""
|
|
141
|
+
if layer and e.get("layer") != layer:
|
|
142
|
+
return None
|
|
143
|
+
_sec = getattr(cg, "_readable", None)
|
|
144
|
+
if _sec is not None and not _sec(e):
|
|
145
|
+
return None
|
|
146
|
+
if "temporal" in e or "spatial" in e:
|
|
147
|
+
# 效力轴四键必须一并从快照带出:否则 `time_axis="effective"` 在快照
|
|
148
|
+
# 路径上永远「不可判定」(静默全空,比报错更难查)。旧索引快照无这些
|
|
149
|
+
# 键时 `.get` 得 None → 不可判定,是本轴**如实降级**而非误判。
|
|
150
|
+
fm = {"temporal": e.get("temporal"), "spatial": e.get("spatial"),
|
|
151
|
+
# 会话归属必须一并从快照带出:timeline 的会话过滤与归属回带都
|
|
152
|
+
# 走这条快照路径,缺键 → 本会话视图静默全空(比报错更难查)。
|
|
153
|
+
"session": e.get("session"),
|
|
154
|
+
trust.EFFECTIVE_FROM_FIELD: e.get(trust.EFFECTIVE_FROM_FIELD),
|
|
155
|
+
trust.EFFECTIVE_UNTIL_FIELD: e.get(trust.EFFECTIVE_UNTIL_FIELD),
|
|
156
|
+
trust.FROM_FIELD: e.get(trust.FROM_FIELD),
|
|
157
|
+
trust.UNTIL_FIELD: e.get(trust.UNTIL_FIELD),
|
|
158
|
+
"condition_space": {"time_window": e.get("time_window")}}
|
|
159
|
+
else:
|
|
160
|
+
fm, _content = cg._read(e)
|
|
161
|
+
if fm is None:
|
|
162
|
+
return None
|
|
163
|
+
return {"id": nid, "frontmatter": fm, "layer": e.get("layer"),
|
|
164
|
+
"path": e.get("path")}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# 生效条件:nodes 为 None 时**逐条**遍历 cg.index["nodes"] 全部条目(不按索引序切片、不读正文);nodes 为 (nid, entry) 对的序列时只遍历该序列(第 3 层索引子集,序由调用方保证=索引物理序);两种形态都逐条经 _scan_one(layer 过滤 + 可见性 + 条目化同一单点);返回 out 列表(全部 layer/可见性命中,**不做截断**——截断由各接口在条件过滤之后经 _cap_hits 执行,issue #52);
|
|
168
|
+
def _scan(cg, layer=None, nodes=None):
|
|
120
169
|
"""遍历节点:时空字段直接读索引快照(不读文件,O(1)/节点)。
|
|
121
170
|
|
|
122
171
|
索引为旧快照(无 temporal/spatial 键)时回退读文件,保证兼容;
|
|
123
172
|
正文一律不在此加载——预览按需读,避免全库 IO。
|
|
124
173
|
授权单点(issue #35 会话隔离定稿):cg 带 `_readable`(MdCGSecure)
|
|
125
174
|
时逐条过读可见性——密级 × 会话绑定档在此与 _candidates 同口径,
|
|
126
|
-
stg 各 op(timeline/relation/anchors
|
|
175
|
+
stg 各 op(timeline/relation/anchors)不得成为绕过路径。可见性判定
|
|
176
|
+
**先于一切**(含截断):不可见节点既不进候选、也不占 kept 名额。
|
|
177
|
+
|
|
178
|
+
issue #52(条件先行于限额):旧实现在此处 `list(index["nodes"].items())[:max_scan]`
|
|
179
|
+
——按 id 字典序在**条件过滤之前**砍尾巴,库超 max_scan 后(a)本会话记忆等条件
|
|
180
|
+
命中若落在切片外即被永久排除(新写入节点恰在索引尾部)、(b)选面是无意义的 id
|
|
181
|
+
序、(c)截断完全静默、「读不全」与「读不到」不可区分、(d)被排除集合随索引
|
|
182
|
+
重排漂移。现改为:本函数只做「遍历 + layer/可见性过滤」,条件过滤与截断都归
|
|
183
|
+
各接口(条件先行,截断经 `_cap_hits` 只作用于条件命中集)。
|
|
184
|
+
全量快照遍历是 O(N) 内存操作(历代实测 1.7 万节点 ≈0.04s),是本修正的既定代价。
|
|
185
|
+
迭代仍取 `list(...)` 快照(H-4(a) 并发纪律:裸迭代在并写索引下会
|
|
186
|
+
RuntimeError)——去掉的只是切片,不是快照。
|
|
187
|
+
|
|
188
|
+
第 3 层(本批):`nodes` 非 None 时只遍历**索引直取的子集**(条件维由
|
|
189
|
+
`stgidx` 三张表先收窄),逐条仍走 `_scan_one` 同一单点——取数面收窄、
|
|
190
|
+
语义面(layer/可见性/条目化)逐位不变。
|
|
127
191
|
"""
|
|
128
192
|
out = []
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
193
|
+
if nodes is not None:
|
|
194
|
+
for nid, e in list(nodes):
|
|
195
|
+
n = _scan_one(cg, nid, e, layer)
|
|
196
|
+
if n is not None:
|
|
197
|
+
out.append(n)
|
|
198
|
+
return out
|
|
199
|
+
for nid, e in list(cg.index["nodes"].items()):
|
|
200
|
+
n = _scan_one(cg, nid, e, layer)
|
|
201
|
+
if n is not None:
|
|
202
|
+
out.append(n)
|
|
203
|
+
return out
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# 生效条件:hits 为条件命中序列、max_scan 为 None 或可 int 化的单次扫描限额(None 视同不截断)、recency_key 为把单条命中映射到「越大越近期」可比较键的可调用对象时——len(hits) <= cap(cap = max_scan 为 None 时取 len(hits),否则 int(max_scan),负数归 0)返回 (hits 原序, False);超限返回 (sorted(hits, key=recency_key, reverse=True)[:cap], True)。recency_key 由各接口按自己的时间轴构造且须含 id 稳定终键(同一批键下结果与索引物理序无关)。
|
|
207
|
+
def _cap_hits(hits, max_scan, recency_key):
|
|
208
|
+
"""条件命中集截断(issue #52 第 1/2 层):只作用于**条件命中集**。
|
|
209
|
+
|
|
210
|
+
调用点恒在条件过滤之后(条件先行);不过限时不改序、不标记(逐位兼容)。
|
|
211
|
+
兜底选序=时间倒序优先保留近期——**不是**主修法:主修法是明确检索条件
|
|
212
|
+
与建立条件索引(docs/plans/stg条件化与结构索引_设计_v0.1.md);选序只保证
|
|
213
|
+
截断真的发生时,先抛掉的是最久远/最不可判定者,而不是索引尾部。
|
|
214
|
+
"""
|
|
215
|
+
n = len(hits)
|
|
216
|
+
cap = n if max_scan is None else int(max_scan)
|
|
217
|
+
if cap < 0:
|
|
218
|
+
cap = 0
|
|
219
|
+
if n <= cap:
|
|
220
|
+
return hits, False
|
|
221
|
+
return sorted(hits, key=recency_key, reverse=True)[:cap], True
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# 生效条件:h 为 timeline 命中元组 (start, end, id, layer, session);返回 (start, end, id)——「越大越近期」,id 终键保证与索引物理序无关。
|
|
225
|
+
def _tl_recent(h):
|
|
226
|
+
return (h[0], h[1], h[2])
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# 生效条件:h 为 anchors 命中 dict(含 "time" 区间或 None)时返回 (时间可判定否, start, end, id)——时间不可判定者键最小(倒序保留时最先被截),id 终键保证与索引物理序无关。
|
|
230
|
+
def _an_recent(h):
|
|
231
|
+
iv = h.get("time")
|
|
232
|
+
if iv is None:
|
|
233
|
+
return (False, 0.0, 0.0, str(h.get("id")))
|
|
234
|
+
return (True, iv[0], iv[1], str(h.get("id")))
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# 生效条件:n 为 _scan 条目、time_axis 为时间轴名时返回 (时间可判定否, start, end, id)(口径同 _an_recent,区间按 time_axis 轴经 _interval 取;非法轴由 _interval 抛 ValueError,不吞错)。
|
|
238
|
+
def _co_recent(n, time_axis):
|
|
239
|
+
iv = _interval(n["frontmatter"], time_axis)
|
|
240
|
+
if iv is None:
|
|
241
|
+
return (False, 0.0, 0.0, str(n["id"]))
|
|
242
|
+
return (True, iv[0], iv[1], str(n["id"]))
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# 截断可操作提示(issue #52 第 2 层:禁止静默)——文案必须含「细化生效条件/不适用条件」与「建立条件索引」语义;数值放大(调 max_scan)不是修法,故明文劝阻。
|
|
246
|
+
_CAP_HINT = ("条件命中 %s 条超过单次扫描限额 max_scan=%s,已按时间倒序保留近期 %s 条"
|
|
247
|
+
"(截断不静默):请细化生效条件/不适用条件(如 session/layer/time_window)"
|
|
248
|
+
"收窄命中集,或按设计稿建立条件索引(docs/plans/stg条件化与结构索引_设计_v0.1.md)"
|
|
249
|
+
"——不要调大 max_scan 数值。")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# 生效条件:out 为接口返回体(dict)、scanned 为候选遍历数(layer/可见性过滤后、截断前)、hits 为条件命中总数(截断前)、kept 为截断后保留数、truncated 为其布尔标记、max_scan 为本次限额——恒把 scanned/kept/truncated 三键并入 out;truncated 为真时再并入 hint(_CAP_HINT 插值 hits/max_scan/kept);index_meta 非 None 时把其并入 out["index"](第 3 层读数;**flag 关时为 None、不落键**——关臂返回体与第 1/2 层逐位一致);返回 out。
|
|
253
|
+
def _with_scan_reads(out, *, scanned, hits, kept, truncated, max_scan,
|
|
254
|
+
index_meta=None):
|
|
255
|
+
"""读数与截断标记的统一出口(timeline/anchors/consistency 同一口径)。"""
|
|
256
|
+
out.update({"scanned": scanned, "kept": kept, "truncated": truncated})
|
|
257
|
+
if truncated:
|
|
258
|
+
out["hint"] = _CAP_HINT % (hits, max_scan, kept)
|
|
259
|
+
if index_meta is not None:
|
|
260
|
+
out["index"] = index_meta
|
|
154
261
|
return out
|
|
155
262
|
|
|
156
263
|
|
|
264
|
+
# ---------------------------------------------------------------------------
|
|
265
|
+
# 第 3 层:结构索引(flag MDCG_STG_INDEX,默认关)——只改取数面
|
|
266
|
+
# ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
#: 条件索引开关(契约 §4:先 flag 化、逐项验证、再讨论默认开启)。
|
|
269
|
+
_INDEX_ENV = "MDCG_STG_INDEX"
|
|
270
|
+
#: 条件资格首验开关(只上报不过滤)。
|
|
271
|
+
_QUALIFY_ENV = "MDCG_STG_QUALIFY"
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# 生效条件:环境变量 name 取值属 ("1","true","True") 时返回 True,其余(含未设/其它值)返回 False——默认关的开关一律走本判据(与 MDCG_LEGACY_ENV_AUTH 同形)。
|
|
275
|
+
def _flag_on(name):
|
|
276
|
+
return os.environ.get(name) in ("1", "true", "True")
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# 生效条件:cg._stg_index 为已构建的 StgIndex 且其 index_obj is cg.index、not broken、len(pos) == len(cg.index["nodes"]) 时返回 (bundle, None);否则返回 (None, reason)——reason ∈ tables_missing(快照不可用/构建失败)/generation_mismatch(表绑的是**另一份**快照:陈旧表当场丢弃,下次访问按新快照重建)/table_snapshot_mismatch(表与快照键数不等:同上丢弃);未构建时按 cg.index 惰性构建一次并挂回 cg._stg_index(首次 stg 需要时构建,写路径零成本)。
|
|
280
|
+
def _index_bundle(cg):
|
|
281
|
+
"""取(或首次构建)结构索引;任何不可用情形都带 reason 返回,**恒不静默**。"""
|
|
282
|
+
ix = getattr(cg, "_stg_index", None)
|
|
283
|
+
nodes = (getattr(cg, "index", None) or {}).get("nodes")
|
|
284
|
+
if not isinstance(nodes, dict):
|
|
285
|
+
return None, "tables_missing"
|
|
286
|
+
if ix is not None:
|
|
287
|
+
if getattr(ix, "index_obj", None) is not cg.index:
|
|
288
|
+
# 代际不符(整体换过快照却没走失效点):本次**回退**第 1 层并在
|
|
289
|
+
# meta 上报;陈旧表当场丢弃 ⇒ 下次访问按新快照重建(自愈)。
|
|
290
|
+
cg._stg_index = None
|
|
291
|
+
return None, "generation_mismatch"
|
|
292
|
+
if ix.broken or len(ix.pos) != len(nodes):
|
|
293
|
+
cg._stg_index = None
|
|
294
|
+
return None, "table_snapshot_mismatch"
|
|
295
|
+
return ix, None
|
|
296
|
+
try:
|
|
297
|
+
ix = stgidx.build(cg.index)
|
|
298
|
+
except Exception: # noqa: BLE001 构建失败即回退
|
|
299
|
+
return None, "build_failed"
|
|
300
|
+
if ix is None:
|
|
301
|
+
return None, "tables_missing"
|
|
302
|
+
cg._stg_index = ix
|
|
303
|
+
return ix, None
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# 生效条件:ix 为已校验的 StgIndex、served 为其实际服务的维名元组、full 为快照节点总数——恒返回 meta dict(path=index、index_hit=len(served)、index_miss=0、fallback=None、size=表规模、full_nodes=full)。
|
|
307
|
+
def _index_meta_hit(ix, served, full):
|
|
308
|
+
return {"enabled": True, "path": "index", "index_hit": len(served),
|
|
309
|
+
"index_miss": 0, "fallback": None, "full_nodes": full,
|
|
310
|
+
"size": ix.size()}
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# 生效条件:reason 为非空回退原因字符串、want 为本次查询**想要**索引服务的维数、full 为快照节点总数(未知时 None)、ix 为可用/不可用的 StgIndex(未知时 None)——恒返回 meta dict(path=full、index_hit=0、index_miss=want、fallback=reason、size=表规模或 None、full_nodes=full)。
|
|
314
|
+
def _index_meta_full(reason, want, full, ix=None):
|
|
315
|
+
return {"enabled": True, "path": "full", "index_hit": 0,
|
|
316
|
+
"index_miss": want, "fallback": reason, "full_nodes": full,
|
|
317
|
+
"size": ix.size() if ix is not None else None}
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# 生效条件:session/layer 为维值或 stgidx.MISSING(**MISSING 表示该维不参与;None 是合法维值**=unassigned 桶)、time_range 为 (lo,hi) 或 None、blocked 为「有条件下但该维不可索引」时的回退原因(无则 None);flag 关闭时返回 (None, None)(关臂不落 index 键、不碰索引);否则按 表缺失/代际不符/表-快照不符/迭代期旧快照条目/无条件维 逐一回退并回报 reason,可服务时返回 (pairs(索引物理序的 (nid, entry) 序列), meta(path=index))。
|
|
321
|
+
def _index_pairs(cg, *, session=stgidx.MISSING, layer=stgidx.MISSING,
|
|
322
|
+
time_range=None, blocked=None):
|
|
323
|
+
"""条件维 → 索引子集;返回 (pairs | None, meta | None)。
|
|
324
|
+
|
|
325
|
+
**回退即回报**(契约:表缺失/代际不符 ⇒ 回退第 1 层全量遍历,安全降级且
|
|
326
|
+
可观测,禁止静默):本函数返回 None 的每一条路径都带非空 reason。
|
|
327
|
+
"""
|
|
328
|
+
if not _flag_on(_INDEX_ENV):
|
|
329
|
+
return None, None
|
|
330
|
+
want = ((session is not stgidx.MISSING) + (layer is not stgidx.MISSING)
|
|
331
|
+
+ (time_range is not None) + (1 if blocked else 0))
|
|
332
|
+
nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
|
|
333
|
+
full = len(nodes)
|
|
334
|
+
ix, reason = _index_bundle(cg)
|
|
335
|
+
if ix is None:
|
|
336
|
+
return None, _index_meta_full(reason, want, full)
|
|
337
|
+
ids, served = ix.subset(session=session, layer=layer,
|
|
338
|
+
time_range=time_range)
|
|
339
|
+
if not served:
|
|
340
|
+
return None, _index_meta_full(blocked or "no_condition_dimension",
|
|
341
|
+
want, full, ix)
|
|
342
|
+
if len(ids) >= full:
|
|
343
|
+
# 子集 == 全量:索引**未收窄**任何面(如层条件恰好覆盖全库)——走索引臂
|
|
344
|
+
# 只会多付一次候选拷贝与逐条取件(5320 节点实测约 +20%),不如直接走
|
|
345
|
+
# 第 1 层全量(设计稿 §3.1「触碰数降到子集」的诚实版:没收窄就别绕)。
|
|
346
|
+
return None, _index_meta_full("no_convergence", want, full, ix)
|
|
347
|
+
pairs = []
|
|
348
|
+
for nid in ids:
|
|
349
|
+
e = nodes.get(nid)
|
|
350
|
+
if e is None:
|
|
351
|
+
# 表里有 id、快照里没有(长度校验放行不了的残余不一致):
|
|
352
|
+
# 宁慢不丢召回——整查询回退,并丢表待重建。
|
|
353
|
+
cg._stg_index = None
|
|
354
|
+
return None, _index_meta_full("table_snapshot_mismatch", want,
|
|
355
|
+
full)
|
|
356
|
+
if "temporal" not in e and "spatial" not in e:
|
|
357
|
+
# 迭代期旧快照条目:`_scan` 会**读文件**取 fm,该节点的 session/
|
|
358
|
+
# layer/时间口径来自盘面而非条目——条目键的分类不保证与之一致
|
|
359
|
+
# (会话视图下会漏召回)。故整查询回退(宁慢不丢召回,可观测)。
|
|
360
|
+
return None, _index_meta_full("entry_file_read", want, full, ix)
|
|
361
|
+
pairs.append((nid, e))
|
|
362
|
+
return pairs, _index_meta_hit(ix, served, full)
|
|
363
|
+
|
|
364
|
+
|
|
157
365
|
# 生效条件:cg.index["nodes"].get(node_id) 缺失或为假值时返回 "";否则 cg._readable 可调用且对其返回假值或抛异常时返回 PLACEHOLDER_DENIED;cg._read(e) 的 frontmatter 为 None 时返回 "";content 非密文时返回 content[:n](n 默认 200);content 为密文时,cg._open_content 可调用且取到非 None 且非密文的 opened 才返回 opened[:n],opened 为 None、抛异常或仍为密文时返回 PLACEHOLDER_LOCKED。
|
|
158
366
|
def _preview(cg, node_id, n=200):
|
|
159
367
|
"""按需读单个节点正文做预览(只发生在最终返回的条目上)。
|
|
@@ -193,6 +401,69 @@ def _preview(cg, node_id, n=200):
|
|
|
193
401
|
return opened[:n]
|
|
194
402
|
|
|
195
403
|
|
|
404
|
+
# 生效条件:cg 有 index["nodes"][node_id] 且(cg._readable 可调用时)该条目过可见性判定、cg._read 取回 fm 非 None 时返回 {"frontmatter": fm, "content": content}——content 为密文且 cg._open_content 可解出非密文时才用解出的明文,仍为密文/解不开亦返回 None;条目不存在、被读隔离拦下、fm 取不回一律返回 None(**拿不到可判定的正文就不做判定**,不猜测)。
|
|
405
|
+
def _qual_node_dict(cg, node_id):
|
|
406
|
+
"""资格判定的 node_dict——取件与脱敏口径与 `_preview` **同一份**(判定不越权)。
|
|
407
|
+
|
|
408
|
+
密文不可解 ⇒ 返回 None ⇒ 该条不附 qualification:**不伪造状态**(若照密文
|
|
409
|
+
判 CCG 完整性,会把「无密钥」误报成「要素不全」,那是拿不到证据时的假话)。
|
|
410
|
+
"""
|
|
411
|
+
from . import crypto
|
|
412
|
+
e = cg.index["nodes"].get(node_id)
|
|
413
|
+
if not e:
|
|
414
|
+
return None
|
|
415
|
+
guard = getattr(cg, "_readable", None)
|
|
416
|
+
if callable(guard):
|
|
417
|
+
try:
|
|
418
|
+
if not guard(e):
|
|
419
|
+
return None
|
|
420
|
+
except Exception: # noqa: BLE001
|
|
421
|
+
return None
|
|
422
|
+
fm, content = cg._read(e)
|
|
423
|
+
if fm is None:
|
|
424
|
+
return None
|
|
425
|
+
content = content or ""
|
|
426
|
+
if crypto.is_encrypted(content):
|
|
427
|
+
opener = getattr(cg, "_open_content", None)
|
|
428
|
+
opened = None
|
|
429
|
+
if callable(opener):
|
|
430
|
+
try:
|
|
431
|
+
opened = opener(node_id, fm, content)
|
|
432
|
+
except Exception: # noqa: BLE001
|
|
433
|
+
opened = None
|
|
434
|
+
if opened is None or crypto.is_encrypted(opened):
|
|
435
|
+
return None
|
|
436
|
+
content = opened
|
|
437
|
+
return {"frontmatter": fm, "content": content}
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
# 生效条件:items 为接口最终返回条目序列、context 为情境 dict、query 为情境问句(stg 无自由问句,恒传入 "")时——_QUALIFY_ENV 未启用或 items 为空即原样返回 items;启用时逐条取 _qual_node_dict(None 即该条**不附** qualification,不伪造状态),取到则调 read 面单点 MdCG.judge_qualification(node_dict, query, context) 并把其返回原样挂到 entry["qualification"](判定抛异常该条亦不附);返回 items。**只上报不过滤**:不动成员、不动次序、不动截断面。
|
|
441
|
+
def _attach_qualification(cg, items, context, query=""):
|
|
442
|
+
"""条件资格首验(设计稿 §3.4,`MDCG_STG_QUALIFY=1` 显式开)。
|
|
443
|
+
|
|
444
|
+
语义与 read 面**同口径**:直接复用 `MdCG.judge_qualification` 单点,本模块
|
|
445
|
+
不另写一份资格判据(契约 §五:不引入第二套条件解析)。stg 无自由问句,
|
|
446
|
+
情境取自**本次 stg 调用自身的条件面**(op/session/layer/time_window/bbox/
|
|
447
|
+
time_axis),故 query 恒为 ""。
|
|
448
|
+
|
|
449
|
+
契约红线:**只上报不过滤**——硬过滤(与 read 面同权)另立裁定;本函数的
|
|
450
|
+
调用点在各接口返回体成型**之后**,任何筛选/截断都不经过它。
|
|
451
|
+
"""
|
|
452
|
+
if not items or not _flag_on(_QUALIFY_ENV):
|
|
453
|
+
return items
|
|
454
|
+
from .mdcg import MdCG
|
|
455
|
+
for it in items:
|
|
456
|
+
nid = it.get("id")
|
|
457
|
+
nd = _qual_node_dict(cg, nid) if nid else None
|
|
458
|
+
if nd is None:
|
|
459
|
+
continue
|
|
460
|
+
try:
|
|
461
|
+
it["qualification"] = MdCG.judge_qualification(nd, query, context)
|
|
462
|
+
except Exception: # noqa: BLE001 判定异常不伪状态
|
|
463
|
+
continue
|
|
464
|
+
return items
|
|
465
|
+
|
|
466
|
+
|
|
196
467
|
# 生效条件:cg 上 _node(cg, a_id) 与 _node(cg, b_id) 均返回真值时返回含 a_id/b_id、时间关系、空间关系和 time_known/space_known 的 dict(两侧时间区间均按 time_axis 轴取,见 _interval;time_axis 非法经 trust.time_axis_of 抛 ValueError);任一 _node 结果为假值时返回 {"error":"node_not_found","missing":[...]};
|
|
197
468
|
def relation(cg, a_id, b_id, time_axis="observed"):
|
|
198
469
|
"""两节点间的时空关系(a 相对 b)。`time_axis` 决定时间区间取哪条轴(见 `_interval`)。"""
|
|
@@ -252,7 +523,7 @@ def _view_session(session):
|
|
|
252
523
|
return _normalize_session(s)
|
|
253
524
|
|
|
254
525
|
|
|
255
|
-
# 生效条件:以 _scan(cg,layer=layer
|
|
526
|
+
# 生效条件:以 _scan(cg,layer=layer) 为候选(全部 layer/可见性命中),session 经 _view_session 归一后(None/空/"*"=跨会话不过滤,其它值=归一后的具体会话)非跨会话时仅保留 frontmatter.session 精确相等的节点,_interval(n["frontmatter"], time_axis) 为 None 的节点被跳过——**条件命中集到此确定**;随后才截断:命中集超过 max_scan(默认 5000 不变)时按 (start,end,id) 时间倒序保留近期 max_scan 条并标记 truncated;保留集按 (start,end,id) 以 reverse=bool(desc) 排序,返回 count=条件命中总数(截断前,**不再受索引切片影响**)、limit=传入 limit、session=生效的会话过滤值(跨会话时为 None)、scanned=候选遍历数、kept=截断后保留数、truncated=截断标记(截断时另有 hint)、items 为排序后前 limit 项(limit=0 时为空列表)且每项附 session 归属与 _preview(cg,id)(time_axis 缺省 observed,与旧行为逐位一致;非法轴抛 ValueError)。
|
|
256
527
|
def timeline(cg, layer=None, limit=50, desc=True, max_scan=5000,
|
|
257
528
|
time_axis="observed", session=None):
|
|
258
529
|
"""按时间排序的节点列表。`time_axis` 决定排序依据的时间区间(见 `_interval`)。
|
|
@@ -267,11 +538,24 @@ def timeline(cg, layer=None, limit=50, desc=True, max_scan=5000,
|
|
|
267
538
|
「写进去查不出」;返回体 `session` 回带的是**归一后**的生效值。
|
|
268
539
|
`items` 一并回带 `session`:跨会话视图下「这条是哪个会话做的」必须可辨,
|
|
269
540
|
否则「能读到所有会话做了什么」只剩内容、丢了归属。
|
|
541
|
+
|
|
542
|
+
issue #52(条件先行 + 截断可观测):`max_scan`(默认 5000,数值不变)
|
|
543
|
+
**只在条件命中集上生效**——旧实现把它当索引序切片(在 session/时间条件
|
|
544
|
+
过滤**之前**砍尾巴),本会话记忆因落在切片外而整片消失且没有任何标记;
|
|
545
|
+
现在 `count` 恒为条件命中总数(不受切片影响),命中集超限才按时间倒序
|
|
546
|
+
保留近期,并在返回体上报 `truncated`/`scanned`/`kept`/`hint`。
|
|
270
547
|
"""
|
|
271
548
|
sid = _view_session(session)
|
|
272
549
|
cross = sid in ("", "*") # 跨会话:显式 "*" 与缺省同义
|
|
550
|
+
# 第 3 层:会话/层两维由结构索引直取(跨会话视图下会话维不参与——它本就
|
|
551
|
+
# 「不过滤」;具体会话值走 by_session[归一后 sid],与基线等值比较同一把尺)。
|
|
552
|
+
pairs, imeta = _index_pairs(
|
|
553
|
+
cg, session=stgidx.MISSING if cross else sid,
|
|
554
|
+
layer=layer if layer else stgidx.MISSING)
|
|
555
|
+
scanned = 0
|
|
273
556
|
items = []
|
|
274
|
-
for n in _scan(cg, layer=layer,
|
|
557
|
+
for n in _scan(cg, layer=layer, nodes=pairs):
|
|
558
|
+
scanned += 1
|
|
275
559
|
fm = n["frontmatter"] or {}
|
|
276
560
|
if not cross and fm.get("session") != sid:
|
|
277
561
|
continue
|
|
@@ -279,18 +563,34 @@ def timeline(cg, layer=None, limit=50, desc=True, max_scan=5000,
|
|
|
279
563
|
if iv is None:
|
|
280
564
|
continue
|
|
281
565
|
items.append((iv[0], iv[1], n["id"], n["layer"], fm.get("session")))
|
|
566
|
+
hits = len(items) # 条件命中总数(截断前,count 的口径)
|
|
567
|
+
items, truncated = _cap_hits(items, max_scan, _tl_recent)
|
|
568
|
+
kept = len(items)
|
|
282
569
|
items.sort(key=lambda x: (x[0], x[1], x[2]), reverse=bool(desc))
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
570
|
+
out_items = _attach_qualification(
|
|
571
|
+
cg, [{"id": i, "layer": l, "start": s, "end": e,
|
|
572
|
+
"session": sn, "preview": _preview(cg, i)}
|
|
573
|
+
for s, e, i, l, sn in items[:limit]],
|
|
574
|
+
{"stg": "timeline", "session": None if cross else sid,
|
|
575
|
+
"layer": layer, "time_axis": time_axis})
|
|
576
|
+
return _with_scan_reads(
|
|
577
|
+
{"count": hits, "limit": limit,
|
|
578
|
+
"session": None if cross else sid, "items": out_items},
|
|
579
|
+
scanned=scanned, hits=hits, kept=kept, truncated=truncated,
|
|
580
|
+
max_scan=max_scan, index_meta=imeta)
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
# 生效条件:time_window 为长度 2 的 list/tuple 时 q_t=(float(time_window[0]),float(time_window[1]))(元素不可转 float 会直接抛异常,源码未捕获),bbox 为长度 4 的 list/tuple 时同理构造 q_b;q_t 与 q_b 均为 None 时返回 {"error":"need_time_window_or_bbox"};否则以 _scan(cg,layer=layer) 为候选逐节点取 _interval(fm, time_axis)(time_axis 缺省 observed 与旧行为逐位一致,非法轴抛 ValueError)与 _bbox,要求时间关系在 during/contains/overlaps/equals、空间关系在 inside/contains/overlaps/equals(提供查询侧才检查)——**条件命中集到此确定**;随后才截断:命中集超过 max_scan(默认 5000 不变)时按 (时间可判定否,start,end,id) 倒序保留近期 max_scan 条并标记 truncated(无时间区间者最先被截);返回 count=条件命中总数(截断前)、query、scanned=候选遍历数、kept=截断后保留数、truncated=截断标记(截断时另有 hint)、items=hits[:limit](limit=None 取全部,0/False 取空)且每条附 preview;
|
|
291
584
|
def anchors(cg, time_window=None, bbox=None, layer=None, limit=50, max_scan=5000,
|
|
292
585
|
time_axis="observed"):
|
|
293
|
-
"""落在给定时间窗 / 空间范围内的节点。`time_axis` 决定候选时间区间(见 `_interval`)。
|
|
586
|
+
"""落在给定时间窗 / 空间范围内的节点。`time_axis` 决定候选时间区间(见 `_interval`)。
|
|
587
|
+
|
|
588
|
+
issue #52(条件先行 + 截断可观测):时间窗/空间范围的条件命中集先于
|
|
589
|
+
`max_scan`(默认 5000,数值不变)确定——`count` 恒为条件命中总数,
|
|
590
|
+
命中集超限才按时间倒序保留近期(无时间区间者先被截),并上报
|
|
591
|
+
`truncated`/`scanned`/`kept`/`hint`;旧实现按索引序切片在条件**之前**,
|
|
592
|
+
命中项落在切片外即静默消失。
|
|
593
|
+
"""
|
|
294
594
|
q_t = None
|
|
295
595
|
if isinstance(time_window, (list, tuple)) and len(time_window) == 2:
|
|
296
596
|
q_t = (float(time_window[0]), float(time_window[1]))
|
|
@@ -300,8 +600,32 @@ def anchors(cg, time_window=None, bbox=None, layer=None, limit=50, max_scan=5000
|
|
|
300
600
|
if q_t is None and q_b is None:
|
|
301
601
|
return {"error": "need_time_window_or_bbox"}
|
|
302
602
|
|
|
603
|
+
# 第 3 层:时间维经 by_time 区间直取(层维同 timeline)。三条回退条件都在
|
|
604
|
+
# 索引面**如实上报**(禁止静默):
|
|
605
|
+
# · 非观察轴(effective 等):表按观察轴建,换轴即无法预筛 ⇒ 不索引该维;
|
|
606
|
+
# · 倒置查询窗(qs > qe):`equals` 命中可要求 a1 = qs > qe,「t ≤ qe」的
|
|
607
|
+
# 预筛会漏 ⇒ 不索引该维(宁慢不丢召回);
|
|
608
|
+
# · 非法轴:不索引 ⇒ 回退后由 `_interval` 在**与基线同一处**抛 ValueError。
|
|
609
|
+
time_range, blocked = None, None
|
|
610
|
+
if q_t is not None:
|
|
611
|
+
try:
|
|
612
|
+
_observed = trust.time_axis_of(time_axis) == "observed"
|
|
613
|
+
except ValueError:
|
|
614
|
+
_observed = False
|
|
615
|
+
if not _observed:
|
|
616
|
+
blocked = "time_axis_not_indexed"
|
|
617
|
+
elif q_t[0] <= q_t[1]:
|
|
618
|
+
time_range = (q_t[0], q_t[1])
|
|
619
|
+
else:
|
|
620
|
+
blocked = "inverted_query_window"
|
|
621
|
+
pairs, imeta = _index_pairs(
|
|
622
|
+
cg, layer=layer if layer else stgidx.MISSING,
|
|
623
|
+
time_range=time_range, blocked=blocked)
|
|
624
|
+
|
|
625
|
+
scanned = 0
|
|
303
626
|
hits = []
|
|
304
|
-
for n in _scan(cg, layer=layer,
|
|
627
|
+
for n in _scan(cg, layer=layer, nodes=pairs):
|
|
628
|
+
scanned += 1
|
|
305
629
|
fm = n["frontmatter"]
|
|
306
630
|
iv, bb = _interval(fm, time_axis), _bbox(fm)
|
|
307
631
|
t_rel = time_relation(iv, q_t) if (q_t and iv) else None
|
|
@@ -312,13 +636,24 @@ def anchors(cg, time_window=None, bbox=None, layer=None, limit=50, max_scan=5000
|
|
|
312
636
|
continue
|
|
313
637
|
hits.append({"id": n["id"], "layer": n["layer"], "time": iv, "bbox": bb,
|
|
314
638
|
"time_relation": t_rel, "space_relation": s_rel})
|
|
639
|
+
total = len(hits) # 条件命中总数(截断前,count 的口径)
|
|
640
|
+
hits, truncated = _cap_hits(hits, max_scan, _an_recent)
|
|
641
|
+
kept = len(hits)
|
|
315
642
|
for h in hits[:limit]:
|
|
316
643
|
h["preview"] = _preview(cg, h["id"])
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
644
|
+
_attach_qualification(
|
|
645
|
+
cg, hits[:limit],
|
|
646
|
+
{"stg": "anchors", "time_window": list(q_t) if q_t else None,
|
|
647
|
+
"bbox": list(q_b) if q_b else None, "layer": layer,
|
|
648
|
+
"time_axis": time_axis})
|
|
649
|
+
return _with_scan_reads(
|
|
650
|
+
{"count": total, "query": {"time_window": q_t, "bbox": q_b},
|
|
651
|
+
"items": hits[:limit]},
|
|
652
|
+
scanned=scanned, hits=total, kept=kept, truncated=truncated,
|
|
653
|
+
max_scan=max_scan, index_meta=imeta)
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
# 生效条件:以 cand=list(_scan(cg,layer=layer)) 为候选(scanned=len(cand) 为遍历读数,layer/可见性即其条件面),**先**按 (时间可判定否,start,end,id) 时间倒序截断到 max_scan(默认 5000 不变,截断时 kept=保留数、truncated=True 并附 hint)——随后逐条检查:bb 非 None 且不满足 bb[0]<=bb[2] and bb[1]<=bb[3] 记 invalid_bbox、iv(由 _interval(fm, time_axis) 取,time_axis 缺省 observed 与旧行为逐位一致、非法轴抛 ValueError)非 None 且 iv[0]>iv[1] 记 inverted_time_window、temporal 与 time_window 均经 trust.epoch_seconds 归一后可比且不满足 tw[0]<=t<=tw[1] 记 temporal_outside_window(该检查恒按观察轴内部口径、不随 time_axis 漂移;任一端不可转数值则忽略);返回 issues 总数与 issues[:limit](limit 默认 50)、scanned=遍历读数、kept=实际检查节点数、truncated=截断标记(截断时另有 hint)。
|
|
322
657
|
def consistency(cg, layer=None, limit=50, max_scan=5000, time_axis="observed"):
|
|
323
658
|
"""时空字段自洽性检查:非法 bbox / 时间倒置 / 窗口与时刻冲突。
|
|
324
659
|
|
|
@@ -329,11 +664,21 @@ def consistency(cg, layer=None, limit=50, max_scan=5000, time_axis="observed"):
|
|
|
329
664
|
两端比较前统一经 `trust.epoch_seconds` 归一:旧实现裸 `float` 比较,历史毫秒
|
|
330
665
|
节点的 `1.7e12` 与秒级 `temporal` 永不落入区间 → 该检查在真实库中**静默失效**
|
|
331
666
|
(issue #23 同根因)。返回体的 `temporal` / `time_window` 也随之为归一后的秒值。
|
|
667
|
+
|
|
668
|
+
issue #52(条件先行 + 截断可观测):候选/条件命中集(layer × 可见性)
|
|
669
|
+
**先于** `max_scan`(默认 5000,数值不变)确定;超限时按时间倒序保留近期
|
|
670
|
+
再检查——`kept` 为实际检查数、`truncated` 显式上报(旧实现按索引序切片
|
|
671
|
+
且无任何标记,「读不全」与「读不到」不可区分)。
|
|
332
672
|
"""
|
|
673
|
+
# 第 3 层:候选/条件面就是「层 × 可见性」,层维由 by_layer 直取(层缺省时
|
|
674
|
+
# 无维可索引 ⇒ 回退全量并上报 no_condition_dimension)。
|
|
675
|
+
pairs, imeta = _index_pairs(cg, layer=layer if layer else stgidx.MISSING)
|
|
676
|
+
cand = list(_scan(cg, layer=layer, nodes=pairs))
|
|
677
|
+
scanned = len(cand)
|
|
678
|
+
cand, truncated = _cap_hits(cand, max_scan, lambda n: _co_recent(n, time_axis))
|
|
679
|
+
kept = len(cand)
|
|
333
680
|
issues = []
|
|
334
|
-
|
|
335
|
-
for n in _scan(cg, layer=layer, max_scan=max_scan):
|
|
336
|
-
scanned += 1
|
|
681
|
+
for n in cand:
|
|
337
682
|
fm = n["frontmatter"]
|
|
338
683
|
bb, iv = _bbox(fm), _interval(fm, time_axis)
|
|
339
684
|
if bb and not (bb[0] <= bb[2] and bb[1] <= bb[3]):
|
|
@@ -348,5 +693,10 @@ def consistency(cg, layer=None, limit=50, max_scan=5000, time_axis="observed"):
|
|
|
348
693
|
if lo is not None and hi is not None and not (lo <= t <= hi):
|
|
349
694
|
issues.append({"id": n["id"], "issue": "temporal_outside_window",
|
|
350
695
|
"temporal": t, "time_window": [lo, hi]})
|
|
351
|
-
|
|
352
|
-
|
|
696
|
+
_attach_qualification(
|
|
697
|
+
cg, issues[:limit],
|
|
698
|
+
{"stg": "consistency", "layer": layer, "time_axis": time_axis})
|
|
699
|
+
return _with_scan_reads(
|
|
700
|
+
{"issues": len(issues), "limit": limit, "items": issues[:limit]},
|
|
701
|
+
scanned=scanned, hits=scanned, kept=kept, truncated=truncated,
|
|
702
|
+
max_scan=max_scan, index_meta=imeta)
|