diff --git a/.workbuddy/memory/MEMORY.md b/.workbuddy/memory/MEMORY.md index ea1ad5f..87f2b30 100644 --- a/.workbuddy/memory/MEMORY.md +++ b/.workbuddy/memory/MEMORY.md @@ -2,7 +2,7 @@ > 全文:`.workbuddy/memory/MEMORY-details.md`(47 节);验证机制:技能 `rond-fix-verify`;备份包解析/导出:技能 `rond-backup-parse`。本文件只做关键词索引——**新规矩写 details,别堆这里**。 > -> ⚠️ 本文件**按字符数**注入,硬上限 `MAX_MEMORY_CHARS = 1e4`(WorkBuddy `app.asar` 里 `getWorkingMemoryContent`;中文 1 字算 1,不是按字节)。超了会被 `substring(0,1e4)` 截断并写明「先整理再干活」——即整段尾部静默丢失。日常日志与本 details **不参与注入**,可以随便写。现值见 `wc -m`,留 20% 余量。 +> ⚠️ 本文件**按字符数**注入,硬上限 1e4(`MAX_MEMORY_CHARS`;中文 1 字算 1,不是按字节)。超了会被 `substring` 截断=**尾部静默丢失**。日常日志与 details **不参与注入**,可随便写。现值见 `wc -m`,留 20% 余量。 ## 落库 / 精度(details 一、四、五、十六、十九、廿八A) - 建地点+到访**同一事务**;新行号只走 `nextSrcPK`+`clearStaleRaw`,**禁 `MAX(src_pk)+1`**。`seq_test.go` @@ -21,7 +21,7 @@ - rows 收尾必须 `drainRows`(先 `Err()` 再 `Close()`)。`parse_rows_test.go` - **扫进裸 `bool` 的列必须先 `COALESCE`**:`weather.is_daylight` 漏写 ⇒ NULL ⇒ 整包拒收(details 卅C)。 - 核对导出包**别按留档 UUID 找行**:站点新建行会被补派生 UUID(`coreDataUUID`)。 -- **换基线**(底座换代,`/admin/baseline`):站点数据一行不动,只换底稿。判据=到访 UUID 重合率(`matched==0` 硬拒)+ 干跑导出;记档 `datasets.baseline_*`,`ensureBaseline` **必须先看记档**;留档名**必须防重名**(秒级时间戳会让回滚覆盖掉刚生成的留档)。`baseline_swap_test.go`(details 卅三) +- **换基线**(`/admin/baseline`):站点数据一行不动,只换底稿。判据=到访 UUID 重合率(`matched==0` 硬拒)+ 干跑导出;记档 `datasets.baseline_*`,`ensureBaseline` **必须先看记档**;留档名**必须防重名**。`baseline_swap_test.go`(卅三) ## 迷雾 / 遮罩(details 三、九、十、廿二) - `.fwss`=zip、条目 zlib;`Model/*/`=头 32768B+每块 515B;`Model/~/`、`Model/#/` 原样写回;清空/替换都要连 `fog_raw_files`。 @@ -39,8 +39,8 @@ (以访客 ctx 渲染 `/`、`/stats`、`/report` 逐个断言禁用词,并确认站长那份仍说「我的」)。 - 改 Go 必须重启;模板/CSS/JS 只在 `debug=true` 热加载(`AssetV` 启动哈希)。⚠️ **新模板+旧二进制**=「渲染到一半仍 200」;启动自检 `smokeRender` 会拒启(四十一)。 - 字阶/圆角/阴影**只走令牌**,裸 `font-size: 13px` 有静态守卫 `TestNoBareFontSizePx`;卡片三档 `.card--hero/--quiet`。⚠️ 改**根字号**要查 Leaflet(`.75rem` 会跟着漂),(四十二)。 -- 类型色走 `csscolor/csssoft`(**必须返回 `template.CSS`**,否则 `var()` 被换成 `ZgotmplZ` 且不报错);这套色连 3:1 都不到,**只能当圆点标记、别当文字色**。窄屏导航收进 `.nav-menu`,顶栏 `--z-nav` 必须高于地图浮层(四十三)。 -- 日期控件一律走 `attachAll`(原生 `type=date` 的年段会吞 6 位数字 ⇒ 敲 20260606 变 `202606/06` 且值为空);模板里出现 `input[type=date]` 选择器有守卫。`.grid` 必须 `align-items:start`(四十四)。前端脚本单测(`static/js/*.test.js`)已挂进 verify.sh「第 1.5 段」,**node 缺失直接失败不跳过**;可测的纯函数要抽成独立文件(`rond-map.js` 是大 IIFE,函数取不出来)。(四十六) +- 类型色走 `csscolor/csssoft`(**必须返回 `template.CSS`**,否则 `var()` 被换成 `ZgotmplZ` 且不报错);这套色连 3:1 都不到,**只能当圆点标记**。窄屏导航收进 `.nav-menu`,顶栏 `--z-nav` 必须高于地图浮层(四十三)。 +- 日期控件一律走 `attachAll`(原生 `type=date` 年段会吞 6 位数字 ⇒ 敲 20260606 变 `202606/06` 且值为空),模板里有守卫。`.grid` 必须 `align-items:start`(四十四)。前端单测(`static/js/*.test.js`)已挂进 verify.sh,**node 缺失直接失败不跳过**;可测的纯函数要抽成独立文件(`rond-map.js` 是大 IIFE,取不出函数)。(四十六) - 拼查询串走返回 `template.URL` 的方法(`PlaceLink`/`EditPlaceLink`/`MapLink`/`EditLink`),勿手工转义。 - 窄屏覆盖写 `app.css` **末尾那块**;宽表堆叠类名**成对** `class="tb tb-stack"`;触控 44px。`tb_stack_test.go` - 加表格列:`PlaceCols`/`scanPlace` **成对**、新列追加最后;CJK 挤换行只抬该表 `min-width`。 @@ -60,35 +60,33 @@ - 站内新建行 `entity_raw.raw IS NULL`;`ZCATEGORY_` 真机只有 `MKPOICategoryXxx` 枚举。 - 类别候选:编辑/照片页=库里在用 `GROUP BY`;补全页=在用 ∪ 静态表;下拉留「其它(自己写)」,默认「不动」。 -## 归属口径 / 防复发(details 卅五) +## 归属口径 / 跨备份 / 附属数据(details 十九~廿一、卅四、卅五) - ⚠️ **铁律:这份记录能不能从备份重新导入?** 能 ⇒ 可挂 `dataset_id … ON DELETE CASCADE` (places/visits/movements/weather/activities/tags/entity_raw);**不能 ⇒ 只留 dataset_id 当出处、不许挂外键** (place_images/place_notes/place_votes/topics)。挂在备份里没有的表上 = 删数据集就永久丢失。 - 两道门禁都钉着它:运行时查 `pg_constraint`(表必须在白名单里,反向也查)+ **静态扫 `schema.sql`** - (新建的库走 CREATE TABLE,只看运行时会有盲点)。`dataset_ownership_test.go` + (新建的库走 CREATE TABLE,只看运行时会有盲点)。`dataset_ownership_test.go`(卅五) - **体检常驻后台首页**:库里 N 张 / 本地盘上 M 个 / 落单 K 个(`imageHealth`)—— 事故的本质是「行与对象悄悄对不上、没人发现」,让它当场可见而不是等照片消失才追。 - -## 跨备份 / 附属数据(details 十九~廿一、卅四) - 预演按 `VisitUUID`/`ActivityUUID` 对齐、只读;`raw #>> '{ZIDENTIFIER_,$blob}'` 是 base64 须转 hex 比;地点只认坐标(`PlaceCoordKey` 6 位)。 - 附属数据(备注/投票/图片)主键=`place_key` 坐标键(非 dataset+src_pk);Go 与 SQL 两套须逐位一致。 -- **站长结论(`place_votes`)与访客票(`place_visitor_votes`)是两码事**:各存各表、各看各榜; - 统计页拆成「我的结论」与「访客点赞」两块(`votes.TopVisitorVoted`,`HAVING` 只算有票的那个方向)。 - ⚠️ 票表也按坐标键跨数据集读 ⇒ 用例必须按 `place_key` 清历史票,否则票数越跑越多。(details 卅六) -- ⚠️ **附属数据不能挂数据集外键**:`place_images` 曾按 `dataset_id` CASCADE ⇒ 删旧数据集=删图片行、**文件却留在盘上**(读取按坐标键、行按数据集级联,两套口径打架)。已摘掉外键;恢复走后台「图片挂载恢复」,认地点靠旧数据集留下的 `baselines/.sqlite`(**别清 baselines 目录**);⚠️ 来源必须**全部读进来合并**(places → entity_raw 留档 → 该 sqlite 及**每份 `.bak`**)——换基线会覆盖底座文件,只看当前那份会整片认不出(实测 40 张只挂回 7 张)。`image_recovery_test.go`(details 卅四) +- **站长结论(`place_votes`)与访客票(`place_visitor_votes`)是两码事**:各存各表、各看各榜(`votes.TopVisitorVoted`,`HAVING` 只算有票的方向)。 + ⚠️ 票表也按坐标键跨数据集读 ⇒ 用例必须按 `place_key` 清历史票,否则票数越跑越多。(卅六) +- ⚠️ **附属数据不能挂数据集外键**(`place_images` 曾按 `dataset_id` CASCADE ⇒ 删数据集=删行、**文件还在盘上**); + 恢复走后台「图片挂载恢复」,认地点靠 `baselines/.sqlite`(**别清该目录**); + ⚠️ 来源必须**全部读进来合并**(places → entity_raw 留档 → 该 sqlite 及**每份 `.bak`**),只看当前那份会整片认不出。`image_recovery_test.go` - ⚠️ 恢复**同时扫对象存储**(R2/S3):`media.ListObjects`(ListObjectsV2 + SigV4,查询串先编码再签), - 需 `s3:ListBucket` 权限;列不动时本地照救 + warning。**专题同理**:`topics` 已改成归人不归数据集 - (`user_id` 过滤,内容仍照生效数据集算)。用户级数据靠 `Dataset.UserID` 定位。(details 卅四) + 需 `s3:ListBucket`;列不动时本地照救 + warning。**专题同理**:`topics` 已改成归人不归数据集(`user_id` 过滤,内容仍照生效数据集算)。(卅四) - ⚠️ `schema.sql` 迁移块必须收敛且排在依赖表之后(插在 `place_images` 前:开发库无症、**新库起不来**)。 - 合并 `backup_merge.go`:整个 `withTx`、不删除;新行 `*_visit_src` 写**库里的 src_pk**;必须 `writeRawRow`;删地点前先查有无到访(CASCADE)。 ## 后端 / 性能(details 十、廿八C) -- `findSourceArchive` 先按 `size_bytes` 筛再算 sha256(`pickArchiveBySha`)。池 8/8 + `db.WarmUp(d,6)`;页面内查询 `runJobs`(≤6);别用 `fog.Index.All()`;改遮罩须 `masks.invalidate()`+`fogTiles.clear()`。 +- `findSourceArchive` 先按 `size_bytes` 筛再算 sha256。池 8/8 + `db.WarmUp(d,6)`;页面内查询 `runJobs`(≤6);别用 `fog.Index.All()`;改遮罩须 `masks.invalidate()`+`fogTiles.clear()`。 - `placeCols` 子查询必须带 `vN.dataset_id=p.dataset_id`。`stats/placecols_test.go`(按子查询分段判) - multipart handler 直接 `ParseMultipartForm`(`r.ParseForm()` 毒化字段);**之前必须 `MaxBytesReader`**。五个上传口:备份/GPX/迷雾/照片/地点图片。 - 包了 `ResponseWriter` 必须实现 `Unwrap()`(`gzipWriter`/`headerSent`);`recover` 写错误页前须看 `headerSent`。`server_error_test.go` - 「先落盘再解析」的上传 handler,落盘后每条失败路径都要删半成品。`upload_cleanup_test.go` -- 批量勾选走 `*Bulk` + `p.src_pk = ANY($3::bigint[])`(pgx 直接吃 `[]int64`);**全局内存缓存必须带上限**(`fillupCache` FIFO 淘汰最旧 1/4);**同一查询别在一个 handler 里查两遍**。`admin_notes_bulk_test.go`+`fillup_cache_test.go`+`stats_weekday_once_test.go` +- 批量勾选走 `*Bulk` + `p.src_pk = ANY($3::bigint[])`;**全局内存缓存必须带上限**(`fillupCache` FIFO 淘汰最旧 1/4);**同一查询别在一个 handler 里查两遍**。`admin_notes_bulk_test.go`+`fillup_cache_test.go`+`stats_weekday_once_test.go` - 加 `setting` 字段改三处:`Default()`/`Load()`/`settingsSubmit()`。 ## 安全 / 跳转(details 廿八B) @@ -110,4 +108,4 @@ - 地点配对只有一份实现 `rond.MatchPlaces`(差异与合并共用);同名兜底必须有距离上限(`nameMatchMaxKm=1.0`)。 - 差异页默认 radio 是「**用实机**」;写合并测试要照这个提交(抓 `value="backup"`),否则测的不是真实点击行为。 - 自检不变量:合并后没有任何地点被搬动 >1km、也没有任何到访离它的实机地点 >1km。 -- **三条铁律**:① app 只靠 `Z_PK` 解引用地点/活动 ⇒ 导出的编号必须与**数据集基线**同一套 ⇒ **基线必须是手机包**;② rond 导入是**整库替换** ⇒ 导入前先清空 app 数据;③ 站点数据是超集 ⇒ 用「手机导出作新数据集 + 旧站点导出当备份合并进去」对齐;`ZRAW` 这类从源包抄来的计数会越界,靠导出侧夹取兜住。 +- **三条铁律**:① app 只靠 `Z_PK` 解引用 ⇒ 导出编号必须与**数据集基线**同一套 ⇒ **基线必须是手机包**;② rond 导入是**整库替换** ⇒ 导入前先清空 app 数据;③ 站点数据是超集 ⇒ 用「手机导出作新数据集 + 旧站点导出当备份合并进去」;`ZRAW` 这类从源包抄来的计数会越界,靠导出侧夹取兜住。