@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.
Files changed (65) hide show
  1. package/README.md +7 -6
  2. 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
  3. 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
  4. 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
  5. 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
  6. 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
  7. 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
  8. 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
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. package/md_cg/admission.py +718 -0
  21. package/md_cg/autonomy_modes.py +642 -0
  22. package/md_cg/consistency.py +133 -8
  23. package/md_cg/forgetting.py +38 -6
  24. package/md_cg/freshness.py +17 -1
  25. package/md_cg/lifecycle.py +30 -2
  26. package/md_cg/mcp_server.py +63 -7
  27. package/md_cg/mdcg.py +85 -17
  28. package/md_cg/mdcos.py +666 -16
  29. package/md_cg/protect.py +79 -4
  30. package/md_cg/provenance.py +1 -1
  31. package/md_cg/review_cli.py +31 -2
  32. package/md_cg/rollback.py +411 -0
  33. package/md_cg/rollback_cli.py +104 -0
  34. package/md_cg/stg.py +400 -50
  35. package/md_cg/stgidx.py +281 -0
  36. package/md_cg/test_autonomy_admission.py +1098 -0
  37. package/md_cg/test_autonomy_modes.py +1972 -0
  38. package/md_cg/test_b1_auto_id_multiproc.py +7 -0
  39. package/md_cg/test_b1b2_write_face.py +7 -0
  40. package/md_cg/test_b3_merge_keeps_content.py +7 -0
  41. package/md_cg/test_cons200_scan_selection.py +791 -0
  42. package/md_cg/test_i50a_half_dup_defer.py +408 -0
  43. package/md_cg/test_i50b_defer_to_review_queue.py +545 -0
  44. package/md_cg/test_i50c_meta_passthrough.py +608 -0
  45. package/md_cg/test_i50d_importance_source.py +531 -0
  46. package/md_cg/test_i50e_readside_protection.py +656 -0
  47. package/md_cg/test_issue39_utf8_stdio.py +9 -1
  48. package/md_cg/test_issue52_scan_condition_first.py +677 -0
  49. package/md_cg/test_linkref.py +7 -0
  50. package/md_cg/test_mode_parity.py +1393 -0
  51. package/md_cg/test_mutation_rollback.py +805 -0
  52. package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +1 -1
  53. package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +7 -0
  54. package/md_cg/test_p2_mcp.py +8 -0
  55. package/md_cg/test_p9_forget_protect.py +7 -0
  56. package/md_cg/test_p9c_dedup_hints.py +7 -0
  57. package/md_cg/test_policy_required_ccg.py +5 -1
  58. package/md_cg/test_protocol.py +7 -0
  59. package/md_cg/test_stgidx_index_parity.py +1020 -0
  60. package/md_cg/test_writelimit.py +36 -9
  61. package/md_cg/test_writepipe.py +5 -2
  62. package/md_cg/weights.py +15 -1
  63. package/md_cg/writepipe.py +101 -2
  64. package/package.json +1 -1
  65. package/skills/plugin.json +1 -1
@@ -0,0 +1,281 @@
1
+ # -*- coding: utf-8 -*-
2
+ """stg 第 3 层:结构索引(装载期内存倒排 + 写路径增量维护)。
3
+
4
+ 设计稿:`docs/plans/stg条件化与结构索引_设计_v0.1.md`(2026-10-03 使用者签收)。
5
+ 本模块只放**结构与取数**:三张表的构建 / 增删 / 取子集 / 一致性自检。
6
+
7
+ 「何时构建、何时回退、回退怎么上报」的编排在读侧(`stg.py`);
8
+ 「何时调增删钩」的编排在写侧(`mdcg._set_index_entry` / `mdcg._remove_index_entry`)。
9
+ 两处都只经本模块的单点,不另写第二份分类口径。
10
+
11
+ 三张表(**不变量:恒为快照 keys 的划分**,见 `verify`):
12
+
13
+ · `by_session: {sid | None | _UNHASHABLE: [id, ...]}`——`None` = 无会话归属
14
+ (`unassigned` 兜底桶,语义与基线 ``fm.get("session") != sid`` 的等值比较一致);
15
+ · `by_layer: {layer | _UNHASHABLE: [id, ...]}`;
16
+ · `by_time: [(t, id), ...]` 升序——`t` 取**观察轴区间起点**,经既有单点
17
+ `trust.time_window_of`(= `stg._interval` 同一实现,不引入第二套时间解析);
18
+ 无有效时间戳 → `untimed`;非有限值(NaN/±inf)→ `nonfinite`(见下)。
19
+ 另加 `pos: {id: 物理位序}`——子集按**索引物理序**回排:基线 `_scan` 的候选序
20
+ 就是快照物理序,等价性判据(flag 开/关逐位一致)要求取数面能复现它。
21
+
22
+ **为什么 nonfinite 单列**(不并入 untimed):基线里 NaN 时间戳节点并非「无时间」——
23
+ `_interval` 对 `(nan, nan)` 判非 None,`time_relation` 落到最后一档 `overlaps`,于是
24
+ NaN 节点**会**命中时间窗查询;`±inf` 则分别落 `after`/`before`(永不命中)。故 NaN 必须
25
+ 进时间窗查询的候选面(`subset` 对 time 维一并带上 nonfinite——保守超集,逐条精确判定
26
+ 仍在 `_scan` 侧),untimed 才与基线「跳过」语义对齐。
27
+
28
+ 不适用条件:非 stg 读侧(`cg(op=read)` 的候选收敛走 `apply_retrieval_gates`,与
29
+ 本索引不同维、不共用);跨进程共享(本索引是**进程内**派生结构,各进程自建)。
30
+ """
31
+ from __future__ import annotations
32
+
33
+ import bisect
34
+ import math
35
+
36
+ from . import trust
37
+
38
+ #: 不可哈希的 session/layer 值(脏盘面才可能出现的形态)统一归此哨兵键。
39
+ #: 任何**具体会话/层**查询都查不到它——与基线 `!=` 等值比较同语义(脏值恒不等)。
40
+ _UNHASHABLE = object()
41
+
42
+ #: 维缺省哨兵(`None` 是**合法取值**:session=None 即 unassigned 桶,不可与「未给该维」混同)。
43
+ MISSING = object()
44
+
45
+
46
+ # 生效条件:value 可 hash 时原样返回,hash 抛 TypeError(list/dict 等)时返回模块级哨兵 _UNHASHABLE。
47
+ def _hashable(value):
48
+ try:
49
+ hash(value)
50
+ except TypeError:
51
+ return _UNHASHABLE
52
+ return value
53
+
54
+
55
+ # 生效条件:entry 为 dict 时以 trust.time_window_of(entry, "observed") 取观察轴两端点,两端均可判定则返回 float(起点),任一端不可判定(None)或 entry 非 dict 时返回 None;不做第二套解析、不猜测。
56
+ def time_of(entry):
57
+ """条目 → 观察轴区间**起点**(epoch 秒);不可判定 → None。
58
+
59
+ 与 `stg._interval` **同一单点**:`_scan` 把快照条目拼成 fm 后取到的区间,
60
+ 与直接对条目取逐位一致——`_scan` 把 `time_window` 平铺进 `condition_space`
61
+ 而 `time_window_of` 两种载体都读(见其「兼容两种载体形态」注释),
62
+ `temporal` 优先级与毫秒归一也同一份 `trust.parse_time`。
63
+ """
64
+ if not isinstance(entry, dict):
65
+ return None
66
+ s, e = trust.time_window_of(entry, "observed")
67
+ if s is None or e is None:
68
+ return None
69
+ return float(s)
70
+
71
+
72
+ # 生效条件:index_obj 为含 dict 型 "nodes" 的快照(或顶层就是 dict)时返回按其**物理序**一次性构建的 StgIndex(O(N),逐条经 add 分类);"nodes" 缺失或非 dict 时返回 None(调用方据此回退并上报 reason,不静默)。
73
+ def build(index_obj):
74
+ """快照 → 三张表(O(N));快照形态不可用返回 None。"""
75
+ nodes = index_obj.get("nodes") if isinstance(index_obj, dict) else None
76
+ if not isinstance(nodes, dict):
77
+ return None
78
+ ix = StgIndex(index_obj)
79
+ for nid in nodes: # 快照物理序(dict 插入序)→ pos 即此序
80
+ ix.add(nid, nodes[nid])
81
+ return ix
82
+
83
+
84
+ # 生效条件:ix 为 StgIndex、nodes 为快照 nodes 映射时,逐项核对「三表恒为快照 keys 的划分」——pos 与快照键集双向相等、pos 序 == 快照物理序、by_session/by_layer 各桶两两不交且并集 == 键集、by_time ∪ nonfinite ∪ untimed 同上、by_time 升序且分类与条目实际 session/layer/时间逐条一致;返回不一致说明列表(空 = 不变量成立)。O(N),守卫/审计用,不在取数热路径。
85
+ def verify(ix, nodes):
86
+ """划分不变量自检:返回不一致说明列表(空 = 完好)。"""
87
+ bad = []
88
+ if ix.broken:
89
+ bad.append("broken:增量维护期已记不一致标记(写钩异常)")
90
+ ids = set(nodes)
91
+ if set(ix.pos) != ids:
92
+ bad.append("pos 键集与快照不等:多 %d / 缺 %d"
93
+ % (len(set(ix.pos) - ids), len(ids - set(ix.pos))))
94
+ if sorted(ix.pos, key=ix.pos.get) != list(nodes):
95
+ bad.append("pos 序与快照物理序不一致(取数面无法复现基线候选序)")
96
+ if len(ix.pos) != len(nodes):
97
+ bad.append("len(pos)=%d ≠ len(nodes)=%d" % (len(ix.pos), len(nodes)))
98
+ for name, table in (("by_session", ix.by_session), ("by_layer", ix.by_layer)):
99
+ seen = []
100
+ for bucket, lst in table.items():
101
+ seen.extend(lst)
102
+ if len(set(lst)) != len(lst):
103
+ bad.append("%s[%r] 桶内重复 id" % (name, bucket))
104
+ if len(seen) != len(set(seen)):
105
+ bad.append("%s 有 id 落在多个桶(非划分)" % name)
106
+ if set(seen) != ids:
107
+ bad.append("%s 并集与快照键集不等(多 %d / 缺 %d)"
108
+ % (name, len(set(seen) - ids), len(ids - set(seen))))
109
+ timed = [nid for _t, nid in ix.by_time]
110
+ sess_sets = {k: set(v) for k, v in ix.by_session.items()}
111
+ layer_sets = {k: set(v) for k, v in ix.by_layer.items()}
112
+ union = set(timed) | set(ix.nonfinite) | set(ix.untimed)
113
+ if len(timed) != len(set(timed)) or len(set(ix.nonfinite)) != len(ix.nonfinite) \
114
+ or len(set(ix.untimed)) != len(ix.untimed):
115
+ bad.append("by_time/nonfinite/untimed 有重复 id")
116
+ if union != ids:
117
+ bad.append("by_time ∪ nonfinite ∪ untimed 与快照键集不等(多 %d / 缺 %d)"
118
+ % (len(union - ids), len(ids - union)))
119
+ if [t for t, _ in ix.by_time] != sorted(t for t, _ in ix.by_time):
120
+ bad.append("by_time 非升序")
121
+ for nid in ids:
122
+ e = nodes.get(nid)
123
+ sid, layer, bucket, _t0 = ix.cls.get(nid, (None, None, None, None))
124
+ if sid != _hashable((e or {}).get("session")):
125
+ bad.append("%s 的 by_session 分类与条目不符" % nid)
126
+ if layer != _hashable((e or {}).get("layer")):
127
+ bad.append("%s 的 by_layer 分类与条目不符" % nid)
128
+ t = time_of(e)
129
+ want = 0 if (t is not None and math.isfinite(t)) else (
130
+ 1 if t is not None else 2)
131
+ if bucket != want:
132
+ bad.append("%s 的 by_time 分类与条目不符(t=%r)" % (nid, t))
133
+ # 「划分」不只要求并集相等,还要求 id 落在**其分类键对应的桶**里——
134
+ # 否则分桶键取错字段(如 by_layer 按 session 建)时并集仍相等、静默漏判。
135
+ if nid not in sess_sets.get(sid, ()):
136
+ bad.append("%s 不在其分类对应的 by_session 桶里" % nid)
137
+ if nid not in layer_sets.get(layer, ()):
138
+ bad.append("%s 不在其分类对应的 by_layer 桶里" % nid)
139
+ if bucket == 0 and nid not in timed:
140
+ bad.append("%s 应在 by_time 而不在" % nid)
141
+ if bucket == 1 and nid not in ix.nonfinite:
142
+ bad.append("%s 应在 nonfinite 而不在" % nid)
143
+ if bucket == 2 and nid not in ix.untimed:
144
+ bad.append("%s 应在 untimed 而不在" % nid)
145
+ return bad
146
+
147
+
148
+ class StgIndex:
149
+ """快照的结构倒排(三张表 + 物理位序 + 每节点分类记录)。
150
+
151
+ 维护纪律:增删只经 `add` / `remove`(写侧单点),取数只经 `subset`(读侧单点)。
152
+ `cls[nid] = (sid, layer, bucket, t)` 是**增量摘除**的依据(不必回扫桶猜分类);
153
+ 任一步异常 ⇒ `broken = True`(读侧据此回退全量并重建,绝不带着半张表取数)。
154
+ """
155
+
156
+ def __init__(self, index_obj):
157
+ self.index_obj = index_obj # 强引用:读侧按 `is` 判代际(防 id 复用)
158
+ self.by_session = {}
159
+ self.by_layer = {}
160
+ self.by_time = []
161
+ self.untimed = []
162
+ self.nonfinite = []
163
+ self.pos = {}
164
+ self.cls = {}
165
+ self.broken = False
166
+ self._next_pos = 0
167
+
168
+ # 生效条件:nid 已在 pos 中时先摘旧分类(**位序不变**——与 dict 原地覆写保序同语义),否则分配下一个位序;随后按 entry 的 session/layer/时间重新分类一次(同键即幂等);任一步异常置 broken=True(读侧回退重建)。
169
+ def add(self, nid, entry):
170
+ """增量增/改:与 `dict.__setitem__` 同序语义(已有键保位序、新键落尾)。"""
171
+ try:
172
+ if nid in self.pos:
173
+ self._unclassify(nid) # 位序保留(dict 覆写不移动)
174
+ else:
175
+ self.pos[nid] = self._next_pos
176
+ self._next_pos += 1
177
+ self._classify(nid, entry)
178
+ except Exception: # noqa: BLE001 一致性存疑即标记,读侧回退
179
+ self.broken = True
180
+
181
+ # 生效条件:nid 不在 pos 中时无操作;在则摘分类并 pop 位序(位序**不回收**——后续新键恒得更大位序,与 dict 删后新增落尾一致);任一步异常置 broken=True。
182
+ def remove(self, nid):
183
+ """增量删(不回收位序:新键恒落尾,与 dict 删除/追加语义同构)。"""
184
+ try:
185
+ if nid not in self.pos:
186
+ return
187
+ self._unclassify(nid)
188
+ self.pos.pop(nid, None)
189
+ except Exception: # noqa: BLE001
190
+ self.broken = True
191
+
192
+ def _classify(self, nid, entry):
193
+ e = entry if isinstance(entry, dict) else {}
194
+ sid, layer = _hashable(e.get("session")), _hashable(e.get("layer"))
195
+ t = time_of(e)
196
+ self.by_session.setdefault(sid, []).append(nid)
197
+ self.by_layer.setdefault(layer, []).append(nid)
198
+ if t is None:
199
+ bucket = 2
200
+ self.untimed.append(nid)
201
+ elif math.isfinite(t):
202
+ bucket = 0
203
+ bisect.insort(self.by_time, (t, nid))
204
+ else:
205
+ bucket = 1 # NaN/±inf:见模块 docstring「为什么单列」
206
+ self.nonfinite.append(nid)
207
+ self.cls[nid] = (sid, layer, bucket, t)
208
+
209
+ def _unclassify(self, nid):
210
+ sid, layer, bucket, t = self.cls.pop(nid)
211
+ lst = self.by_session[sid]
212
+ lst.remove(nid)
213
+ if not lst: # 空桶不保留(表规模读数如实)
214
+ self.by_session.pop(sid, None)
215
+ lst = self.by_layer[layer]
216
+ lst.remove(nid)
217
+ if not lst:
218
+ self.by_layer.pop(layer, None)
219
+ if bucket == 0:
220
+ # 二分定位而非全表连比:(t, nid) 在升序表内唯一,命中即该位;
221
+ # 不在该位 ⇒ 状态已不可信,抛错由上层置 broken(绝不摘错条目)
222
+ i = bisect.bisect_left(self.by_time, (t, nid))
223
+ if i >= len(self.by_time) or self.by_time[i] != (t, nid):
224
+ raise KeyError(nid)
225
+ del self.by_time[i]
226
+ elif bucket == 1:
227
+ self.nonfinite.remove(nid)
228
+ else:
229
+ self.untimed.remove(nid)
230
+
231
+ # 生效条件:lo <= hi 时返回 t ∈ [lo, hi] 的 id 列表(**时间序**,调用方按 pos 回排);lo > hi 返回空列表(区间为空——调用方在 inverted 查询上先回退,见 stg 侧)。
232
+ def range_ids(self, lo, hi):
233
+ """`by_time` 区间截取(二分);返回时间序 id 列表。"""
234
+ if lo > hi:
235
+ return []
236
+ a = bisect.bisect_left(self.by_time, (lo,))
237
+ b = bisect.bisect_left(self.by_time, (hi,))
238
+ # 同 t 并列:把 t == hi 的尾巴一并纳入(不假设 id 字符上界,避免脆弱判据)
239
+ while b < len(self.by_time) and self.by_time[b][0] == hi:
240
+ b += 1
241
+ return [nid for _t, nid in self.by_time[a:b]]
242
+
243
+ # 生效条件:session/layer 为 MISSING 表示该维不参与(**注意 None 是合法维值**,不表示缺省),time_range 为 None 表示时间维不参与、否则为 (lo, hi);无任何参与维时返回 (None, ());否则返回 (候选 id 列表(**索引物理序**), 实际服务的维名元组)——任一维在表中无键即得空列表(与基线等值比较同语义,非回退)。
244
+ def subset(self, session=MISSING, layer=MISSING, time_range=None):
245
+ """按维直取子集;返回 (ids | None, dims)。"""
246
+ dims, srcs = [], []
247
+ if session is not MISSING:
248
+ dims.append("session")
249
+ srcs.append(self.by_session.get(session, []))
250
+ if layer is not MISSING:
251
+ dims.append("layer")
252
+ srcs.append(self.by_layer.get(layer, []))
253
+ if time_range is not None:
254
+ dims.append("time")
255
+ lo, hi = time_range
256
+ # nonfinite 是保守超集(NaN 在基线上会命中时间窗,见模块 docstring)
257
+ srcs.append(self.range_ids(lo, hi) + list(self.nonfinite))
258
+ if not dims:
259
+ return None, ()
260
+ if len(srcs) > 1:
261
+ # 交集:三张表各自都是**物理序子序列**,除 time 外直接可用;
262
+ # time 先回排成物理序,再以最短表驱动(结果仍是物理序子序列)。
263
+ for i, d in enumerate(dims):
264
+ if d == "time":
265
+ srcs[i] = sorted(srcs[i], key=self.pos.__getitem__)
266
+ k = min(range(len(srcs)), key=lambda i: len(srcs[i]))
267
+ driver = srcs[k]
268
+ others = [set(srcs[i]) for i in range(len(srcs)) if i != k]
269
+ return ([nid for nid in driver if all(nid in o for o in others)],
270
+ tuple(dims))
271
+ ids = srcs[0]
272
+ if dims[0] == "time":
273
+ ids = sorted(ids, key=self.pos.__getitem__)
274
+ return list(ids), tuple(dims)
275
+
276
+ # 生效条件:恒返回表规模读数 dict——nodes=pos 条数、sessions/layers=两张表的**非空桶**数、timed=by_time 条数、untimed/nonfinite=两表条数;只读、无副作用。
277
+ def size(self):
278
+ """表规模读数(meta 用)。"""
279
+ return {"nodes": len(self.pos), "sessions": len(self.by_session),
280
+ "layers": len(self.by_layer), "timed": len(self.by_time),
281
+ "untimed": len(self.untimed), "nonfinite": len(self.nonfinite)}