用户要求这份记忆跟着仓库走,而不是只留在本机。 .gitignore 里挡路的是 `.workbuddy/`。改成反忽略六行,并且**只放行 .md**: 别用 `!.workbuddy/memory/**`——它会把 memory 下任何文件都放行,本项目真发生过 `web.test.exe` 混进暂存。复核用 `git check-ignore -q --no-index` 逐条打靶: memory/*.png、memory/sub/x.md、memory/MEMORY.md.bak、plans/a.md 全 IGNORED, 17 个 .md 全 included;`git add -An .workbuddy/` 恰好 16 个文件。 其余 .workbuddy 内容(计划、本地设置、临时产物)继续忽略。记忆里没有真凭据 (唯一命中是 2026-09-14.md 里的 `password=rond-admin`,那是项目默认口令)。 顺带做两件整理: 1. 取证了注入上限。以前按「字节数」判断 MEMORY.md 是否超限是错的: WorkBuddy 的 `getWorkingMemoryContent()` 用 `MAX_MEMORY_CHARS = 1e4` 比 JS `content.length`,即**字符数**(中文 1 字算 1);超了 substring 截断、尾部静默丢失。 本 details 与每日日志都不参与注入,只有 MEMORY.md 受约束。 现已从 6604 字符收到上限的 66%,并把这条上限写进索引抬头。 2. 把「备份合并」「导出/恢复三条铁律」两段长文迁进 MEMORY-details.md 的卅一/卅二节, 索引只留关键词指针。行尾统一为 LF(仓库 .gitattributes 是 `* text=auto eol=lf`)。
14 KiB
2026-09-30
生产时间线「部分重复部分没有」的根因与修复
用户报 https://life.shiroko.one/timeline 上仍有重复。抓页面逐条比对:一屏 180 条里有 53 对
到达时间、离开时间、停留时长完全相同的条目——与 09-04 那种「离开时间不同」的已并入行不同,
ZMERGEDTO/ZPARENT 判据一条都收不到(43 对里隐藏侧 0)。
真正成因:站点补录的行没有留档 ⇒ 没有 UUID,导出时被补上 v5("ZVISIT/<src_pk>");这份包
再合并回站点时站点侧没有 UUID 可对齐,被当成「备份新增」又插一条(41/43 的副本 UUID 正好等于
配对行的派生 UUID,实测确认)。
修:rond 里把派生 UUID 抽出来当对账键(DerivedUUID/VisitMatchUUID),差异与合并都用它;
卡片新增第二类判定(成因「你补录的那条被合并回来的一份」),清理时把副本名下的位移/天气改挂
到保留项。提交 8dec973(+ 格式 aaf65f6),已推送;全量验收随后跑。
「本地测试要十几分钟」的根因:我昨天那个收口把页面拖成了 44~70 秒
- 症状:最慢的 4 个用例占全套 55%(309s/274s/226s/115s),全是「渲染若干页面」的布局用例。
- 定位:临时逐页计时探针(
/43.6s、/places70.1s)→ 变异对照(关掉判据 0.28s)→ CPU profile (135 秒只用 3 秒 CPU ⇒ 在等,不是算)。孤立的 EXPLAIN 只有 12~38ms,会骗人。 - 根因:判据打在
entity_raw.raw(TOAST 过的整行 JSON)上,而 /places 是「每个地点 × 5 个子查询」。 - 修:派生列
visits.is_merged+ 刷新(ingest / recountPlaces)+ 部分索引;/places 70084ms → 206ms, 首页 43633ms → 698ms。新增page_render_budget_test.go(7 个页面卡 10 秒的灾难探测器)。 - 教训:别拿单条查询的耗时判断整页性能;「功能对、就是慢」的退化必须有守卫。
生产导出 data/20260930.rondbackup 被真机拒收 —— 逐项排查(未定位到形状违规)
按技能里「备份恢复失败」的排查顺序全跑了一遍,每一层都与手机包/曾被接受的包同形:
- 打包层(头号真凶):本地头 bit3=0、长度非 0、条目名与顺序与 09-20 被接受的那份一致 ✓
- 模型骨架:表 31 / 索引 47 / CREATE TABLE 逐字符 /
Z_PLISTsha256 /Z_MODELCACHE16073B /Z_UUID/Z_VERSION—— 与 09-29v2 手机包完全一致 ✓ - 逐列空值画像:唯一差异
ZLOCATION.ZISOCOUNTRYCODE20 个 NULL。这不是问题——09-20 被接受过的包也是 23 个 NULL, 且 09-29 做过真机对照实验(同一份包唯一差异这列):''被拒、NULL能导入(见 commitd5e96ed)⇒ NULL 是对的, 别再去「补空串美化」(我这次差点又去改它)。 - 关系完整性 0 悬空(含
ZRAWVISIT→ZVISIT、ZVISIT.ZLOCATION/ZACTIVITY_、自引用ZMERGEDTO/ZPARENT)✓ Z_OPT全 ≥1、Z_ENT与参照一致、ZIDENTIFIER_16B 且唯一、值类型分布无新增类别、无非法 UTF-8、 行级时间不变量 0 违例、SQLite 文件头(页大小/WAL 版本/编码)一致 ✓- 计数画像同形(1001 到访 / 64 隐藏 / 436 地点 / 392 有到访 / 交通方式 6 条无重名)✓
- 包里仍有 43 组站内副本重复对(就是 dedup 卡片要清的),但不影响导入 ✓
下一步(等用户):要报错原文 + 两个对照导入把范围切两半——
storage/.verify/acceptance-clean.rondbackup(纯净往返)与 data/20260920-fixed-A-数据修复.rondbackup(曾成功)。
若指向「站点数据」,就用这份失败包做行级二分(按 UUID 拆「手机原有的行」/「站内新建的行」)。
拒收排查续:静态判据全过 ⇒ 转真机二分(工具已提交)
用户确认:报错就是「备份恢复失败,你的当前数据未被修改」(QQ 传文件,同一路径传的另一份能导入)⇒ 导出链路与 App 都没问题,问题在这份数据集的数据里。
静态排查全过(打包位标志 / Core Data 骨架逐字节 / 空值画像 / 关系完整性含自引用 / 类型分布 / Z_OPT·UUID·BLOB / 行级时间不变量 / 位移窗口不变量 / 取值域与数值范围 / SQLite 文件头)。 顺手排掉四个「看着像」的:
ZISOCOUNTRYCODE20 个 NULL:09-20 被接受过的包也是 23 个 NULL,且 09-29 真机对照实验证明「空串被拒、NULL 能进」⇒ 不是它;- 站内新建行的 v5 UUID:手机包自己就有 172 个 v5;
- 43 组精确重复到访:手机包自己就有 39 组 ⇒ 真机容忍;
- 位移窗口越界:这份包 0 条(手机包自己倒有 2 条)。
新增 tools/bisect_export.py:以被拒包为底生成 V1(去掉站内新建的 22 条到访)、
V2(+3 个站内新建地点)、V3(手机里也有的行按手机原值重置),都已过审计(悬空 0 / Z_MAX 够 / 打包正常)。
判读:哪一版开始能进,问题就在上一版删/改掉的那一类里;三版都进不去 ⇒ 问题在「行的集合」(删过/多过的行)。
二分结果:V1 ✓ / V2 ✓ / V3 ✗ ⇒ 罪在「站内新建的那 22 条到访」(地点无辜)
- V1(去掉 22 条)能导入、V2(再多去 3 个地点)也能导入、V3(全保留、但把手机里也有的行按手机原值重置)报错 ⇒ 问题在这 22 条到访本身或其附属记录里,与地点、与「共享行被改过的值」无关。
- 这 22 条的构成:
Z_PK=[46,200]+998..1017。其中 46/200 其实是基线(09-14 包)自己的行 (同一 UUID、同一数据),只是手机在 09-29 前把它们删掉了(09-29v2 里没有这两个 UUID)—— 所以「按 UUID 是否在手机包里」分类会把它们误判成站内新建。 - 下一轮对照(都已过审计、位于
data/bisect/):V4= 保留这 22 条但删掉它们的附属记录(15 位移 / 16 天气 / 2 采样);V5= 删[46,200,998..1007];V6= 删[1008..1017]。 - 判读:V4 ✓ ⇒ 罪在附属记录;V5/V6 哪个 ✓ ⇒ 罪在另一半的那 10~12 条里,继续对半拆。
二分第二轮:V4 ✗ / V5 ✓ / V6 ✗ ⇒ 罪在 [46,200,9981007],再收紧到 [9981007]
- V4(保留 22 条、只删它们的附属记录)报错 ⇒ 附属记录(15 位移 / 16 天气 / 2 采样)无罪;
- V5(删 [46,200,998
1007])能进、V6(删 [10081017])报错 ⇒ 罪在 V5 删掉的那 12 条里; - 46/200 被排除:它们就是基线自己的行(同 UUID 同数据),而纯净往返包里也有这两行且能导入 ✓ ⇒ 罪在 [998~1007] 这 10 条站内新建行本身(其余一切都已被 V5 证明是兼容的)。
- 静态差分:这 10 条与无辜的 [1008~1017] 逐列完全同形(同为 v5 UUID、同标志位、ZRAW 都有值、TZ 都是 NULL) —— 看不出任何结构差异,只能继续对半。
- 第三轮:
V10= 删 [9981002]、1007](都在V11= 删 [1003data/bisect/)。
二分定案:罪魁是 ZVISIT #1000(09-22 06:16→10:48,挂在重复地点 416 上)
- 五个「只留一条」对照:只有 P1000 报错,P998/P999/P1001/P1002 都能导入 ⇒ 单条罪魁 = #1000。
- 顺带排除的:重复地点本身(手机包自己就有 8 组同名同坐标且都带访,坐标连低位都相同); UUID(#1000 的 UUID 就是标准的 v5("ZVISIT/1000"),与兄弟们同形);ZRAW 量级(2305 与手机的 2209 同量级); 重复组的 ZLASTARRIVALDATE_ 规律(手机各组也是各成员各值)。
- #1000 与四个无辜兄弟的唯一差异 = 它的地点是 416,而 416 是「广西壮族自治区邮政公司」的 重复地点行(与 417 同名同坐标全字段相同,只有 ZLASTARRIVALDATE_ 不同),且只有 #1000 用它; V1/V2/V10 能导入,正是因为那时 #1000 已删、416 成了没有到访的空壳。
- 判别包(都在
data/bisect/):W1= 只留 #1000 但把它的地点换成 417;W2= 只留 #1000 但把 416 的「最近到访」改成与 417 一致;W3= 只留 #1000 但把 ZRAW 改成 1421。 W1 能进 ⇒ 罪在「416 这一行 / 这个对」;W1 报错而 W2/W3 能进 ⇒ 罪在 416 的 ZLASTARRIVALDATE_ 或采样数。
定案:真机拒收的根因 = ZVISIT.ZRAW 超过包内采样行数(已修,commit d99c63f)
- 判别包结果:只有 W3 能进(W1 改地点、W2 改「最近到访」都报错)⇒ 罪魁是
ZVISIT #1000的ZRAW = 2305(「这次到访由多少个原始采样点归并而来」的计数)。 - 不变量:
ZRAW ≤ 包内 ZRAWVISIT 行数。接受过的三份包(09-14 基线 / 09-20 A 包 / 验收纯净往返) 最大值都 ≤ 采样行数;被拒的那份只有 #1000 越界(2305 > 2246)。夹到 2246 以内即可导入 (W3 用 1421 验证过;被接受的包里 ZRAW 为 NULL/0 的行本来就有 39~139 条 ⇒ 往下夹安全)。 - 成因链:合并进来的到访带着源包的 ZRAW,而源包的
ZRAWVISIT采样行站点不托管(只从基线带) ⇒ 计数与包内采样行数脱节。导出侧修法:写完行、清完悬空采样点之后,一条 UPDATE 把ZRAW夹到count(ZRAWVISIT)以内。用例TestExportClampsRawSampleCount(变异验证过)。 - 顺手:
readDupBaseline先 os.Stat(sql.Open 默认 READWRITE|CREATE,路径不存在会静默建空库, 报「no such table」而不是「没有基线」)。 - 给用户的即时包:
data/20260930-fixed-zraw.rondbackup(就是那份被拒的包,只把 #1000 的 ZRAW 2305→2246,其余一字未动,审计干净)。 - 更彻底的做法(记着):把数据集的基线换成一份较新的手机包——采样行来自基线,
基线越新,新到访的采样行越齐,
ZRAW也就天然自洽。
基线替换:用户按链路操作,差异页显示「完全一致」(= 预期结果,不是失败)
用户把 App 导出的 20260930_180637.rondbackup 直接导入成新数据集,再用旧的站点导出
20260930.rondbackup 做差异合并 → 差异页说「与当前数据集完全一致:没有新增、没有被删、也没有冲突」。
原因:喂给 App 的那份包就是站点自己的导出(20260930-fixed-zraw.rondbackup = 站点导出只夹了 ZRAW),
所以 App 再导出来内容当然一样 ⇒ 换基线这一步已经达成(新数据集的基线 = 一份新鲜手机包 ✓),
清单里的「把旧站点导出合并进去」在他这条路径上是多余的(那一步是为「上传一份全新手机包、
里面没有站点数据」的路径准备的)。
顺带记:新数据集的收益主要是编号/存储对齐(App 导入时会重排 Z_PK,新数据集的 src_pk 与它的基线同一套); 采样行(ZRAWVISIT)不会因此变多——那些采样点从来不在我们的包里(真实缺失), 以后靠导出侧的 ZRAW 夹取兜住即可。
闭环完成:换基线成功并验证(2026-09-30 18:20)
用户流程:App 导出的 20260930_180637.rondbackup 直接导入成新数据集 → 网页端显示 937 条到访
(= 包内 1001 − 64 条隐藏行 ✓ 说明 App 那份把数据放在 -wal(3.16MB)里也被我们完整读入 ✓)→
以它为基线在网页端导出 20260930_180637-export-20260930.rondbackup → 导入 App 正常 ✓✓。
我这边独立验证(新导出 vs 新基线):跨表计数 全部 ±0(1001 到访 / 436 地点 / 815 位移 / 589 天气 /
2246 采样 / 6 方式 / 32 活动 / 8 标签);表 31/31、索引 47/47、CREATE TABLE 逐字符、
Z_PLIST/Z_MODELCACHE sha256 全部一致;ZRAW 最大 2246 ≤ 采样行 2246 ✓;打包层 bit3=0 ✓。
(App 那份的 zip flags 是 0x0800——只有 UTF-8 位,数据在 -wal 里,与我们 0x0000 的写法不同但都被接受。)
技术结论已写进 rond-backup-parse 技能:ZRAW 不变量、逐行二分手法(tools/bisect_export.py)、
换基线六步 + 自检、以及排查顺序里新增的「先验纯净往返包」与「值域不变量」两步。
项目记忆纳入版本控制 + 注入上限取证(2026-09-30 18:30)
用户要求「记忆跟着项目走」,把 .workbuddy/memory/ 从 gitignore 里放出来。
.gitignore挡路的是.workbuddy/那行。改成「反忽略」六行:.workbuddy/→!.workbuddy/→.workbuddy/*→!.workbuddy/memory/→.workbuddy/memory/*→!.workbuddy/memory/*.md。 ⚠️ 别用!.workbuddy/memory/**——它把 memory 下的任何文件都放行。实测./web.test.exe混进 暂存过,所以收紧成只认.md。复核用git check-ignore -q --no-index <path>逐条打靶:memory/*.png、memory/sub/x.md、memory/MEMORY.md.bak、plans/a.md全 IGNORED, 三个.md全 included,git add -An .workbuddy/恰好 16 个文件。- 记忆里没有真凭据(唯一命中是
2026-09-14.md里的password=rond-admin,那是项目默认口令)。 - 注入上限取证(以前一直靠猜):
app.asar里getWorkingMemoryContent()用var MAX_MEMORY_CHARS = 1e4,比较的是 JScontent.length——字符数(UTF-16 单元),不是字节; 中文 1 字算 1。超了就substring(0,1e4)且尾部静默丢失,只在当轮挂一句「ACTION REQUIRED」。 ⇒ 以前按「15.6KB 字节」判断是否超限是错的(中文文本 1 万字符≈1.5 万字节)。 本 details 与每日日志都不参与注入,只有MEMORY.md一个文件受约束。 - 索引现状:
MEMORY.md6604 字符 / 10526 字节,距上限留 34% 余量;仍有 12 节,34 节正文在 details。