perceptkit 0.2.2__py3-none-any.whl
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.
- perceptkit/__init__.py +85 -0
- perceptkit/algorithms/__init__.py +40 -0
- perceptkit/algorithms/attribution.py +147 -0
- perceptkit/algorithms/glance.py +236 -0
- perceptkit/algorithms/history.py +663 -0
- perceptkit/algorithms/identity.py +43 -0
- perceptkit/algorithms/observation.py +44 -0
- perceptkit/algorithms/streaks.py +111 -0
- perceptkit/algorithms/trend_models.py +184 -0
- perceptkit/algorithms/wake.py +149 -0
- perceptkit/catalog.py +252 -0
- perceptkit/conformance/__init__.py +28 -0
- perceptkit/conformance/memory.py +364 -0
- perceptkit/conformance/report.py +170 -0
- perceptkit/conformance/suite.py +419 -0
- perceptkit/conformance/wake.py +151 -0
- perceptkit/contracts/__init__.py +97 -0
- perceptkit/contracts/_time.py +89 -0
- perceptkit/contracts/availability.py +77 -0
- perceptkit/contracts/context.py +50 -0
- perceptkit/contracts/delivery.py +167 -0
- perceptkit/contracts/errors.py +22 -0
- perceptkit/contracts/event.py +137 -0
- perceptkit/contracts/observation.py +172 -0
- perceptkit/contracts/receipt.py +129 -0
- perceptkit/contracts/records.py +367 -0
- perceptkit/contracts/report.py +127 -0
- perceptkit/contracts/versioning.py +63 -0
- perceptkit/fields.py +184 -0
- perceptkit/kit.py +223 -0
- perceptkit/manifest/__init__.py +57 -0
- perceptkit/manifest/checks.py +323 -0
- perceptkit/manifest/mapping.py +96 -0
- perceptkit/manifest/minimal.py +1282 -0
- perceptkit/manifest/types.py +211 -0
- perceptkit/manifest/units.py +84 -0
- perceptkit/ports/__init__.py +19 -0
- perceptkit/ports/storage.py +288 -0
- perceptkit/ports/wake.py +43 -0
- perceptkit/processing/__init__.py +49 -0
- perceptkit/processing/aggregate.py +80 -0
- perceptkit/processing/dispatch.py +356 -0
- perceptkit/processing/normalize.py +458 -0
- perceptkit/processing/pipeline.py +406 -0
- perceptkit/processing/recompute.py +170 -0
- perceptkit/processing/recurrence.py +166 -0
- perceptkit/processing/scheduled.py +233 -0
- perceptkit/prompts.py +75 -0
- perceptkit/queries/__init__.py +32 -0
- perceptkit/queries/api.py +457 -0
- perceptkit/retention.py +84 -0
- perceptkit/rules/__init__.py +19 -0
- perceptkit/rules/engine.py +112 -0
- perceptkit/rules/evaluators.py +228 -0
- perceptkit/rules/types.py +236 -0
- perceptkit-0.2.2.dist-info/METADATA +439 -0
- perceptkit-0.2.2.dist-info/RECORD +59 -0
- perceptkit-0.2.2.dist-info/WHEEL +4 -0
- perceptkit-0.2.2.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""Manifest 的自动检查 —— 让"忘了声明"变成一条红色的测试。
|
|
2
|
+
|
|
3
|
+
以前一个字段的属性散在七个地方,**漏掉其中一处不会有任何东西变红**:
|
|
4
|
+
新加的健康字段忘了声明 retention,历史就永远不会被清理;忘了声明 comparator,
|
|
5
|
+
它的变化就永远触发不了事件;resolver 名字拼错,就是一个永远不会被发现的空指针
|
|
6
|
+
——现状里就有 10 个这样的名字,声明了但没有任何实现。
|
|
7
|
+
|
|
8
|
+
这里的四条检查对应产品规范 §8 的四条要求。它们不判风格、不判命名,
|
|
9
|
+
只判**结构上有没有缺口**,误伤为零。
|
|
10
|
+
|
|
11
|
+
用法(在宿主的测试里)::
|
|
12
|
+
|
|
13
|
+
problems = validate_manifest(MY_SIGNALS, available_normalizers=MY_NORMALIZERS)
|
|
14
|
+
assert not problems, "\\n".join(problems)
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Iterable, Mapping
|
|
19
|
+
|
|
20
|
+
from .types import (
|
|
21
|
+
AGGREGATION_STRATEGIES,
|
|
22
|
+
TREND_MODELS,
|
|
23
|
+
ATTRIBUTION_STRATEGIES,
|
|
24
|
+
COMPARISON_STRATEGIES,
|
|
25
|
+
IDENTITY_STRATEGIES,
|
|
26
|
+
PERMANENT,
|
|
27
|
+
PRIVACY_CLASSES,
|
|
28
|
+
QUERY_VISIBILITY,
|
|
29
|
+
SOURCE_PROFILES,
|
|
30
|
+
STORAGE_MODES,
|
|
31
|
+
VALUE_TYPES,
|
|
32
|
+
SignalDefinition,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
#: 需要单位的类型。布尔、枚举、字符串、对象没有量纲。
|
|
36
|
+
_NUMERIC_TYPES = frozenset({"integer", "number"})
|
|
37
|
+
|
|
38
|
+
#: 会产生日聚合的存储形态。
|
|
39
|
+
_AGGREGATING_MODES = frozenset({"current_timeline_aggregate", "current_short_timeline"})
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def check_types_and_units(signals: Mapping[str, SignalDefinition]) -> list[str]:
|
|
43
|
+
"""① 每个字段都要有合法类型;数值型必须有单位。
|
|
44
|
+
|
|
45
|
+
没有单位的数字在跨宿主传递时**必然**被解释错 —— 体重是公斤还是磅、
|
|
46
|
+
时长是秒还是分钟,接收方只能猜,而猜错不会报错。
|
|
47
|
+
"""
|
|
48
|
+
problems: list[str] = []
|
|
49
|
+
for key, sig in signals.items():
|
|
50
|
+
if not sig.fields:
|
|
51
|
+
problems.append(f"{key}: 一个字段都没声明")
|
|
52
|
+
for f in sig.fields:
|
|
53
|
+
if f.value_type not in VALUE_TYPES:
|
|
54
|
+
problems.append(
|
|
55
|
+
f"{key}.{f.key}: value_type={f.value_type!r} 不在 {sorted(VALUE_TYPES)}"
|
|
56
|
+
)
|
|
57
|
+
if f.value_type in _NUMERIC_TYPES and not f.unit:
|
|
58
|
+
problems.append(
|
|
59
|
+
f"{key}.{f.key}: 数值字段必须声明 unit"
|
|
60
|
+
f"(没有单位的数字跨宿主必然被解释错)"
|
|
61
|
+
)
|
|
62
|
+
if f.value_type == "enum" and not f.enum:
|
|
63
|
+
problems.append(f"{key}.{f.key}: enum 类型必须列出合法取值")
|
|
64
|
+
if f.privacy_class not in PRIVACY_CLASSES:
|
|
65
|
+
problems.append(
|
|
66
|
+
f"{key}.{f.key}: privacy_class={f.privacy_class!r} "
|
|
67
|
+
f"不在 {sorted(PRIVACY_CLASSES)}"
|
|
68
|
+
)
|
|
69
|
+
if f.query_visibility not in QUERY_VISIBILITY:
|
|
70
|
+
problems.append(
|
|
71
|
+
f"{key}.{f.key}: query_visibility={f.query_visibility!r} "
|
|
72
|
+
f"不在 {sorted(QUERY_VISIBILITY)}"
|
|
73
|
+
)
|
|
74
|
+
return problems
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def check_history_has_retention(signals: Mapping[str, SignalDefinition]) -> list[str]:
|
|
78
|
+
"""② 会存历史的信号必须声明保留期,会聚合的字段必须声明聚合方式。
|
|
79
|
+
|
|
80
|
+
忘了声明 retention,历史就无限增长且永远不会被清理 —— 而且不会有任何
|
|
81
|
+
症状,直到某天库满了。
|
|
82
|
+
"""
|
|
83
|
+
problems: list[str] = []
|
|
84
|
+
for key, sig in signals.items():
|
|
85
|
+
if sig.storage_mode in _AGGREGATING_MODES and not sig.stores_history:
|
|
86
|
+
problems.append(
|
|
87
|
+
f"{key}: storage_mode={sig.storage_mode} 会产生历史,"
|
|
88
|
+
f"但 history_retention_days=0(等于声明不存历史,自相矛盾)"
|
|
89
|
+
)
|
|
90
|
+
if sig.storage_mode == "current_only" and sig.stores_history:
|
|
91
|
+
problems.append(
|
|
92
|
+
f"{key}: storage_mode=current_only 却声明了保留期 "
|
|
93
|
+
f"{sig.history_retention_days},两者只能留一个"
|
|
94
|
+
)
|
|
95
|
+
if sig.stores_history:
|
|
96
|
+
aggregating = [f for f in sig.fields if f.aggregation_strategy != "none"]
|
|
97
|
+
if not aggregating:
|
|
98
|
+
problems.append(
|
|
99
|
+
f"{key}: 声明了要存历史,但没有任何字段声明 aggregation_strategy"
|
|
100
|
+
f"(那存下来的明细没有任何东西会去读它)"
|
|
101
|
+
)
|
|
102
|
+
if sig.history_retention_days < -1:
|
|
103
|
+
problems.append(
|
|
104
|
+
f"{key}: history_retention_days={sig.history_retention_days} 非法"
|
|
105
|
+
f"(-1 表示永久,0 表示不存,正数表示天数)"
|
|
106
|
+
)
|
|
107
|
+
return problems
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def check_named_implementations_exist(
|
|
111
|
+
signals: Mapping[str, SignalDefinition],
|
|
112
|
+
*,
|
|
113
|
+
available_normalizers: Iterable[str] = (),
|
|
114
|
+
) -> list[str]:
|
|
115
|
+
"""③ manifest 里提到的每个名字都要能解析到实现。
|
|
116
|
+
|
|
117
|
+
这一条抓的是**空指针式的声明**:写了 ``normalizer="health_vitals"``
|
|
118
|
+
但没有任何地方实现它。这类错误不会抛异常,只会让那个字段静默地不被标准化。
|
|
119
|
+
|
|
120
|
+
``available_normalizers`` 由调用方传入 —— 有些 normalizer 天然属于宿主
|
|
121
|
+
(比如把坐标粗化成城市要查地理编码,那是 I/O,kit 不做)。
|
|
122
|
+
"""
|
|
123
|
+
known = frozenset(available_normalizers)
|
|
124
|
+
problems: list[str] = []
|
|
125
|
+
for key, sig in signals.items():
|
|
126
|
+
if sig.storage_mode not in STORAGE_MODES:
|
|
127
|
+
problems.append(
|
|
128
|
+
f"{key}: storage_mode={sig.storage_mode!r} 不在 {sorted(STORAGE_MODES)}"
|
|
129
|
+
)
|
|
130
|
+
if sig.identity_strategy not in IDENTITY_STRATEGIES:
|
|
131
|
+
problems.append(
|
|
132
|
+
f"{key}: identity_strategy={sig.identity_strategy!r} "
|
|
133
|
+
f"不在 {sorted(IDENTITY_STRATEGIES)}"
|
|
134
|
+
)
|
|
135
|
+
if sig.attribution_strategy not in ATTRIBUTION_STRATEGIES:
|
|
136
|
+
problems.append(
|
|
137
|
+
f"{key}: attribution_strategy={sig.attribution_strategy!r} "
|
|
138
|
+
f"不在 {sorted(ATTRIBUTION_STRATEGIES)}"
|
|
139
|
+
)
|
|
140
|
+
agg_days = sig.aggregate_retention_days
|
|
141
|
+
if agg_days is not None:
|
|
142
|
+
if agg_days == 0 and sig.stores_history:
|
|
143
|
+
problems.append(
|
|
144
|
+
f"{key}: 存明细却声明聚合保留 0 天。"
|
|
145
|
+
"明细进了聚合表却当天就被删,那张表永远是空的"
|
|
146
|
+
)
|
|
147
|
+
elif (agg_days != PERMANENT
|
|
148
|
+
and sig.history_retention_days == PERMANENT):
|
|
149
|
+
problems.append(
|
|
150
|
+
f"{key}: 明细永久保存但聚合只留 {agg_days} 天。"
|
|
151
|
+
"反了 —— 聚合是压缩过的、体量小的那一半,"
|
|
152
|
+
"留明细不留聚合等于既花了存储又丢了长期趋势"
|
|
153
|
+
)
|
|
154
|
+
elif (agg_days != PERMANENT and sig.history_retention_days != PERMANENT
|
|
155
|
+
and agg_days < sig.history_retention_days):
|
|
156
|
+
problems.append(
|
|
157
|
+
f"{key}: 聚合留 {agg_days} 天比明细的 "
|
|
158
|
+
f"{sig.history_retention_days} 天还短。"
|
|
159
|
+
"日统计会先于它依据的明细消失,历史上会出现一段"
|
|
160
|
+
"有明细却查不到统计的窗口"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
if sig.source_profile is not None and sig.source_profile not in SOURCE_PROFILES:
|
|
164
|
+
problems.append(
|
|
165
|
+
f"{key}: source_profile={sig.source_profile!r} "
|
|
166
|
+
f"不在 {sorted(SOURCE_PROFILES)}"
|
|
167
|
+
)
|
|
168
|
+
for f in sig.fields:
|
|
169
|
+
if f.aggregation_strategy not in AGGREGATION_STRATEGIES:
|
|
170
|
+
problems.append(
|
|
171
|
+
f"{key}.{f.key}: aggregation_strategy={f.aggregation_strategy!r} "
|
|
172
|
+
f"不在 {sorted(AGGREGATION_STRATEGIES)}"
|
|
173
|
+
)
|
|
174
|
+
if f.comparison_strategy not in COMPARISON_STRATEGIES:
|
|
175
|
+
problems.append(
|
|
176
|
+
f"{key}.{f.key}: comparison_strategy={f.comparison_strategy!r} "
|
|
177
|
+
f"不在 {sorted(COMPARISON_STRATEGIES)}"
|
|
178
|
+
)
|
|
179
|
+
if f.trend_model not in TREND_MODELS:
|
|
180
|
+
problems.append(
|
|
181
|
+
f"{key}.{f.key}: trend_model={f.trend_model!r} "
|
|
182
|
+
f"不在 {sorted(TREND_MODELS)}"
|
|
183
|
+
)
|
|
184
|
+
if (f.value_type in ("integer", "number")
|
|
185
|
+
and f.aggregation_strategy != "none"
|
|
186
|
+
and f.trend_model == "none"):
|
|
187
|
+
problems.append(
|
|
188
|
+
f"{key}.{f.key}: 数值字段会进日聚合却没声明 trend_model"
|
|
189
|
+
f"(趋势查询只能瞎猜用哪种算法,而三种结论完全不同)"
|
|
190
|
+
)
|
|
191
|
+
if f.accepted_units:
|
|
192
|
+
from .units import can_convert
|
|
193
|
+
if not f.unit:
|
|
194
|
+
problems.append(
|
|
195
|
+
f"{key}.{f.key}: 声明了 accepted_units 却没有标准单位"
|
|
196
|
+
)
|
|
197
|
+
else:
|
|
198
|
+
for u in f.accepted_units:
|
|
199
|
+
if not can_convert(u, f.unit):
|
|
200
|
+
problems.append(
|
|
201
|
+
f"{key}.{f.key}: 声明接受 {u!r} 但没有 "
|
|
202
|
+
f"{u!r} -> {f.unit!r} 的换算实现"
|
|
203
|
+
)
|
|
204
|
+
if f.max_relative_jump is not None and f.max_relative_jump <= 0:
|
|
205
|
+
problems.append(
|
|
206
|
+
f"{key}.{f.key}: max_relative_jump 必须为正数"
|
|
207
|
+
)
|
|
208
|
+
if f.normalizer is not None and f.normalizer not in known:
|
|
209
|
+
problems.append(
|
|
210
|
+
f"{key}.{f.key}: normalizer={f.normalizer!r} 没有对应实现"
|
|
211
|
+
f"(声明了名字却没人实现 = 这个字段静默地不被标准化)"
|
|
212
|
+
)
|
|
213
|
+
return problems
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def check_wake_eligible_fields_have_comparators(
|
|
217
|
+
signals: Mapping[str, SignalDefinition],
|
|
218
|
+
) -> list[str]:
|
|
219
|
+
"""④ 能触发唤醒的字段必须说清楚"怎么算变了"。
|
|
220
|
+
|
|
221
|
+
没有 comparator 的 wake_eligible 字段永远不会触发 —— 它看起来配好了,
|
|
222
|
+
实际是死的。这是最典型的"上线了但功能没生效"。
|
|
223
|
+
"""
|
|
224
|
+
problems: list[str] = []
|
|
225
|
+
for key, sig in signals.items():
|
|
226
|
+
for f in sig.fields:
|
|
227
|
+
if f.wake_eligible and f.comparison_strategy == "none":
|
|
228
|
+
problems.append(
|
|
229
|
+
f"{key}.{f.key}: wake_eligible=True 但 comparison_strategy=none"
|
|
230
|
+
f"(永远不会触发,看起来配好了实际是死的)"
|
|
231
|
+
)
|
|
232
|
+
if f.wake_eligible and f.query_visibility == "never":
|
|
233
|
+
problems.append(
|
|
234
|
+
f"{key}.{f.key}: 既然 agent 永远看不到它,"
|
|
235
|
+
f"用它去唤醒 agent 说不通"
|
|
236
|
+
)
|
|
237
|
+
return problems
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def check_projections_do_not_drift(
|
|
242
|
+
signals: Mapping[str, SignalDefinition],
|
|
243
|
+
) -> list[str]:
|
|
244
|
+
"""第五条检查:current / history / query 三种投影必须对得上。
|
|
245
|
+
|
|
246
|
+
产品规范 §8 列了五条自动检查,这是最后一条 —— 也是最容易被漏掉的,
|
|
247
|
+
因为「漂移」不长得像 bug,它长得像一个字段配置得有点奇怪。
|
|
248
|
+
|
|
249
|
+
三种漂移,两种是白干、一种是泄漏:
|
|
250
|
+
|
|
251
|
+
算了没人能读 字段进日聚合,却声明了永不对 agent 开放 ——
|
|
252
|
+
每天算一遍、存一份,谁也读不到
|
|
253
|
+
永不落库却参与判断 声明了 never 的字段在写入边界就被丢掉,
|
|
254
|
+
拿它做比较或唤醒,判断依据根本不存在
|
|
255
|
+
🔴 值随事件出去 声明了 never,却参与唤醒 —— 它的前后值会被写进
|
|
256
|
+
事件信封的 previous/current,而信封会存下来、投出去、
|
|
257
|
+
进模型上下文。写入边界拦住的东西,从这条路漏出去了
|
|
258
|
+
"""
|
|
259
|
+
problems: list[str] = []
|
|
260
|
+
for key in sorted(signals):
|
|
261
|
+
sig = signals[key]
|
|
262
|
+
for f in sig.fields:
|
|
263
|
+
never = f.query_visibility == "never"
|
|
264
|
+
if never and f.aggregation_strategy != "none":
|
|
265
|
+
problems.append(
|
|
266
|
+
f"{key}.{f.key}: 声明了永不对 agent 开放,却要进日聚合"
|
|
267
|
+
f"({f.aggregation_strategy})—— 每天算一遍存一份,谁也读不到"
|
|
268
|
+
)
|
|
269
|
+
if never and f.wake_eligible:
|
|
270
|
+
problems.append(
|
|
271
|
+
f"{key}.{f.key}: 声明了永不对 agent 开放,却是 wake_eligible。"
|
|
272
|
+
"它的前后值会被写进事件信封的 previous/current,"
|
|
273
|
+
"而信封会存下来、投出去、进模型上下文 —— "
|
|
274
|
+
"写入边界拦住的东西,从这条路漏出去了"
|
|
275
|
+
)
|
|
276
|
+
if never and f.comparison_strategy != "none":
|
|
277
|
+
problems.append(
|
|
278
|
+
f"{key}.{f.key}: 声明了永不对 agent 开放,却参与比较"
|
|
279
|
+
f"({f.comparison_strategy})—— 这个字段在写入边界就被丢掉了,"
|
|
280
|
+
"判断依据根本不存在"
|
|
281
|
+
)
|
|
282
|
+
if f.aggregation_strategy != "none" and not sig.stores_history:
|
|
283
|
+
problems.append(
|
|
284
|
+
f"{key}.{f.key}: 声明了聚合策略,但 {key} 不存历史 —— "
|
|
285
|
+
"聚合结果没有地方落"
|
|
286
|
+
)
|
|
287
|
+
return problems
|
|
288
|
+
|
|
289
|
+
def validate_manifest(
|
|
290
|
+
signals: Mapping[str, SignalDefinition],
|
|
291
|
+
*,
|
|
292
|
+
available_normalizers: Iterable[str] = (),
|
|
293
|
+
) -> list[str]:
|
|
294
|
+
"""跑全部五条检查(产品规范 §8 列的那五条),返回问题清单(空 = 通过)。
|
|
295
|
+
|
|
296
|
+
返回列表而不是抛异常:一次看到全部缺口,比逐个修再重跑快得多。
|
|
297
|
+
"""
|
|
298
|
+
problems: list[str] = []
|
|
299
|
+
for key, sig in signals.items():
|
|
300
|
+
if key != sig.key:
|
|
301
|
+
problems.append(f"{key}: 字典的键和 SignalDefinition.key={sig.key!r} 对不上")
|
|
302
|
+
seen: set[str] = set()
|
|
303
|
+
for f in sig.fields:
|
|
304
|
+
if f.key in seen:
|
|
305
|
+
problems.append(f"{key}.{f.key}: 字段重复声明")
|
|
306
|
+
seen.add(f.key)
|
|
307
|
+
problems += check_types_and_units(signals)
|
|
308
|
+
problems += check_history_has_retention(signals)
|
|
309
|
+
problems += check_named_implementations_exist(
|
|
310
|
+
signals, available_normalizers=available_normalizers
|
|
311
|
+
)
|
|
312
|
+
problems += check_wake_eligible_fields_have_comparators(signals)
|
|
313
|
+
problems += check_projections_do_not_drift(signals)
|
|
314
|
+
return problems
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
__all__ = [
|
|
318
|
+
"check_types_and_units",
|
|
319
|
+
"check_history_has_retention",
|
|
320
|
+
"check_named_implementations_exist",
|
|
321
|
+
"check_wake_eligible_fields_have_comparators",
|
|
322
|
+
"validate_manifest",
|
|
323
|
+
]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Reference storage mapping —— 产品规范 §15 点名要的那份表。
|
|
2
|
+
|
|
3
|
+
它回答一句话:**每个信号的数据落到哪几个逻辑存储对象里、留多久。**
|
|
4
|
+
|
|
5
|
+
刻意**从 manifest 生成**,不手写。手写的表和代码之间没有任何东西拦着它们漂开,
|
|
6
|
+
而这份表恰恰是给别人照着建库用的 —— 漂了就是让人照着一份过期的图施工。
|
|
7
|
+
|
|
8
|
+
from perceptkit.manifest import render_reference_mapping
|
|
9
|
+
print(render_reference_mapping())
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Mapping
|
|
14
|
+
|
|
15
|
+
from .types import PERMANENT, SignalDefinition
|
|
16
|
+
|
|
17
|
+
#: storage_mode → 这个信号实际会写到哪几个逻辑对象。
|
|
18
|
+
#: 这是 manifest 里那个枚举的"落地含义",写在一处免得每个宿主自己推。
|
|
19
|
+
MODE_OBJECTS: dict[str, tuple[str, ...]] = {
|
|
20
|
+
"current_only": ("CurrentProjection",),
|
|
21
|
+
"current_short_timeline": ("CurrentProjection", "StoredObservation"),
|
|
22
|
+
"current_timeline_aggregate": (
|
|
23
|
+
"CurrentProjection", "StoredObservation", "DailyAggregate",
|
|
24
|
+
),
|
|
25
|
+
"source_mirror": ("CalendarEventMirror / ReminderItemMirror", "SourceSyncState"),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _days(n: int | None) -> str:
|
|
30
|
+
if n is None:
|
|
31
|
+
return "同明细"
|
|
32
|
+
if n == PERMANENT:
|
|
33
|
+
return "永久"
|
|
34
|
+
if n == 0:
|
|
35
|
+
return "不存"
|
|
36
|
+
return f"{n} 天"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def reference_mapping(
|
|
40
|
+
signals: Mapping[str, SignalDefinition],
|
|
41
|
+
) -> list[dict[str, object]]:
|
|
42
|
+
"""每个信号一行,说明它落到哪些对象、各留多久。"""
|
|
43
|
+
rows: list[dict[str, object]] = []
|
|
44
|
+
for key in sorted(signals):
|
|
45
|
+
sig = signals[key]
|
|
46
|
+
rows.append({
|
|
47
|
+
"signal": key,
|
|
48
|
+
"storage_mode": sig.storage_mode,
|
|
49
|
+
"objects": MODE_OBJECTS.get(sig.storage_mode, ()),
|
|
50
|
+
"current_ttl_sec": sig.current_ttl_sec,
|
|
51
|
+
"detail_retention": _days(sig.history_retention_days),
|
|
52
|
+
"aggregate_retention": (
|
|
53
|
+
_days(sig.aggregate_retention_days)
|
|
54
|
+
if sig.stores_history else "不适用"
|
|
55
|
+
),
|
|
56
|
+
"identity_strategy": sig.identity_strategy,
|
|
57
|
+
"attribution_strategy": sig.attribution_strategy,
|
|
58
|
+
"note": sig.note,
|
|
59
|
+
})
|
|
60
|
+
return rows
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def render_reference_mapping(signals: Mapping[str, SignalDefinition]) -> str:
|
|
64
|
+
"""渲染成 Markdown 表格。"""
|
|
65
|
+
rows = reference_mapping(signals)
|
|
66
|
+
out = [
|
|
67
|
+
"# Reference storage mapping",
|
|
68
|
+
"",
|
|
69
|
+
"> 由 `perceptkit.manifest.render_reference_mapping()` 从 manifest 生成。",
|
|
70
|
+
"> **不要手改** —— 改 manifest,然后重新生成。",
|
|
71
|
+
"",
|
|
72
|
+
"`Current TTL` 到期之后这个值不再冒充「现在」,但仍可作为 last known 返回。",
|
|
73
|
+
"明细和聚合是**两个保留期**:典型形态是明细短、聚合永久 ——"
|
|
74
|
+
"明细体量大而问题的价值随时间递减,聚合正好相反。",
|
|
75
|
+
"",
|
|
76
|
+
"| 信号 | 落到哪些对象 | Current TTL | 明细 | 聚合 | 身份 | 日期归属 |",
|
|
77
|
+
"|---|---|---:|---:|---:|---|---|",
|
|
78
|
+
]
|
|
79
|
+
for r in rows:
|
|
80
|
+
ttl = f"{int(r['current_ttl_sec'])}s" if r["current_ttl_sec"] else "—"
|
|
81
|
+
objects = " + ".join(r["objects"]) or "—" # type: ignore[arg-type]
|
|
82
|
+
out.append(
|
|
83
|
+
f"| `{r['signal']}` | {objects} | {ttl} | {r['detail_retention']} | "
|
|
84
|
+
f"{r['aggregate_retention']} | {r['identity_strategy']} | "
|
|
85
|
+
f"{r['attribution_strategy']} |"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
noted = [r for r in rows if r["note"]]
|
|
89
|
+
if noted:
|
|
90
|
+
out += ["", "## 和产品规范有出入的地方", ""]
|
|
91
|
+
for r in noted:
|
|
92
|
+
out += [f"### `{r['signal']}`", "", str(r["note"]), ""]
|
|
93
|
+
return "\n".join(out) + "\n"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
__all__ = ["MODE_OBJECTS", "reference_mapping", "render_reference_mapping"]
|