# 诗云 / Poetry Cloud — 开发日志 (DEVLOG) Chronological, newest first. Each entry: commits + what changed + how it was verified. The per-area "what works" matrix lives in [HANDOFF.md](devlog/HANDOFF.md); this file is the running diary. Verify gate every entry: `npm run build` (tsc + vite) + `npm test`. **The 3D scene cannot be verified on the headless preview** (swiftshader: the additive galaxy times out / the r3f Canvas subtree stays dormant), so all visual/interaction work is build+test-verified here and eyeballed by the user on a real GPU. Data dirs (`poems/`, `lines/`) are git-ignored — see HANDOFF "data provisioning". --- ## 2026-06-26 — 年度维护轮(10th agent · 6 月常规维护) 首次正式维护轮(此后每年一次);全程在 worktree 分支 `claude/june-maint-2026`,**未合并 main**,待 owner 真机眼测。 门禁每步:tsc · vitest(138 → **217**)· vite build。完整账目见 [docs/MAINTENANCE.md](MAINTENANCE.md)。 - **前序(owner 已做,登记在案):** 检索权重 — `lines/` 名家优先封顶 + 前端整联多句重排(`1347cc8`)。 - **探诗诗体误判(真 bug):** `engineApi.describeAny` 把 `form` 写死 `"ziyou"`,而 `pulledFromIndex`(永久链接 / 拾遗 / 定位虚空)据此把七律误标「自由」,与 `pullByIndex`(已 `inferForm`)前后不一致 → `describeAny` 改用 `inferForm` + 回归测试。**设计阶段子代理初判 FALSE_REPORT,直接核查代码后推翻 —— 子代理结论要复核。** - **触屏整体锁定(改动最大):** 新增 `freeMove`(触屏默认锁定整体、电脑默认自由飞行)。锁定模式绕原点 orbit:单指拖 转 yaw/pitch、双指·滚轮 `orbitZoom` 缩放(解决「只能拖不能放大缩小」),点诗人/诗歌换锁定目标;「更多」加「自由移动」。 - **关随机诗:** `allowRandomPoem`(默认开),关后点虚空不生成随机诗;「更多」加开关。 - **留影重做:** 统一 `⚙ 设置` 子菜单(背景衬底默认关 / 无极调色盘 `` / 顶部文案 / 调整手柄默认关); **可调字体槽** `.cinema-slot`(`useLayoutEffect` 二分字号填满字体槽,右下角手柄 / 双指 / 滚轮调槽大小);取消旧的横/竖排 `cinemaLayout`。手柄曾被 `overflow:hidden` 裁没 → 拆出 `.cinema-clip` 裁剪、手柄移到其外。 - **新手引导:** 精简为四步(B 版,owner 选定)、分平台;localStorage 键 `shiyun_onboarded_v1 → v2`。 验证:tsc · 217 vitest · vite build 全过;3D · 触屏 · 留影排版由 owner 在 5199 + 触屏真机眼测。 --- ## 2026-06-13 — Session: 9th agent · round 5 (自由填诗:字库外字提示) Owner-reported "查不出编号":粘贴的诗含繁体「與」(U+8207),不在简体冻结字库(N=12,877)→ `anyTextIndex` 正确返回 null → 不是 bug,是设计。但**自由填诗(textarea)模式静默无反馈**(固定格模式早有 `.cell.bad` 红格), 用户以为功能坏了。诊断时已用引擎真身证明编号正反算均正确,且线上构建/数据一致(curl 验)。Opus 主循环直接做, 门禁:tsc · **197 tests**(+1)· build。Commit `4c20325`。 - 新增纯函数 `engineApi.outOfCharset(text)`:返回文本中不在字库的唯一字(码点感知、去重、保序);`SearchPanel` 自由分支在算编号前调用它,把字库外的字列出来(「『與』不在字库 —— 诗云用简体,繁体/异体/生僻字没有编号」), 不再静默空白。固定格模式不变(它已有逐格红字)。+1 单测(繁体「與」/拉丁 Z/去重/空串)。 --- ## 2026-06-10 — Session: 9th agent · round 4 (行星指引线重做 — 平面坐标式) Owner verdict on the old guide lines: 太耀眼、太瞩目、有效信息不多(直线光伞)。Redesigned per the orchestrator's spec, Opus 4.8 executed; orchestrator re-ran the gate: tsc · **189 tests**(+17)· build。 Commit `188ba68`。视觉终调(亮度/参考环淡度/动画节奏)留给真机眼测,旋钮已列明。 - **平面坐标式折线**:过诗人取赤道参考面(星团最大横截面,非真实物体),每首诗的指引线 = 平面段 (诗人→H=(ox,0,oz),读方位+水平距离)+ 垂直段(H→P,读相对赤道面的高度);oy≈0 自然退化零长。 - **散射→直射分段生长**:平面段先向外辐射(grow 0→0.6),垂直段再升起(0.6→1),per-vertex `aStage` + 着色器分段进度;grow→hold→fade 生命周期与 显示/覆盖/时长/亮度 全部旋钮沿用。 - **降噪(硬指标)**:整体 alpha 峰值 0.60→**0.28**,平面段强度 0.35 最暗、垂直段 0.70 稍亮(承载新信息); 新增极淡赤道参考环(alpha 0.10,随平面段生长淡入)。 - **旋转保形**:H 与 P 共用 aCenter/aOmega,本地偏移空间绕 Y 旋转后回加中心——赤道面在 Y 轴自转下映射 到自身,L 形随星团自转刚性不剪切(与 PoemOrbits 同方案)。 - **旧模式保留**:`store.guideStyle`("plane"|"line",默认 plane),设置·指引组新增「样式:平面坐标 / 直线(旧版)」分段控件,恢复默认回 plane;旧光束代码路径原样可选。 - 路径数学抽纯模块 `three/planeGuidePath.ts`(零 THREE 依赖)+17 headless 单测(端点/退化/顶点数/进度 单调/参考环);172→**189** 全绿。 --- ## 2026-06-10 — Session: 9th agent · round 3 (五视角评审 Top-8 加固) A 5-lens read-only review (架构/运行时性能/加载网络/产品UX/数据管线运维, 5 Opus reviewers) produced a prioritized list; the owner green-lit the Top 8. Executed as 3 conflict-isolated Opus batches (A 正确性+CI / C 前端 / B 数据运维), orchestrator re-ran the combined gate: tsc · **172 tests** (was 138, +34) · build (three 675.5 KB chunk intact). Commit `ddfdeaa`. 其余评审发现(P2/P3)暂不做,见会话记录。 **批次 A — 正确性防线 + CI** - `shardHash.contract.test.ts` NEW(12):FNV-1a 分桶哈希是管线 3 处 fnv32 + 前端 hashStr 的隐形契约, 漂移=搜索静默全空。7 组黄金值冻结 hashStr + 源码扫描绊线钉死 build-search/lines/fuzzy 的两个常量。 - `charsetHash.ts` NEW + 运行时校验:loadData 由实际 chars 重算 FNV-1a,与文件自带 hash 及冻结常量 `EXPECTED_CHARSET_HASH=a392703b`(实测主目录 charset.json 验证)双向比对;数据↔代码错配(错部署/CDN 串味/`*_v1_backup` 混淆)从「每个编号永久链静默错位」变成响亮报错 + App 顶部可关闭横幅 「数据与本版本不匹配:编号链接可能错位」(仅警告不阻断,截图模式隐藏)。+7 测试。 - `.github/workflows/ci.yml` NEW:push(main/claude/**)+PR → ubuntu·node24·npm 缓存 → ci/build/test; 重数据无依赖(测试全为纯函数/自带 fixture)。九任 agent 的手跑验证门从此机器化。 - 清理 PoemOrbits/engine 三处无 eslint 可依的失效 disable 注释。 **批次 C — FlyControls 零分配 + 拾遗** - FlyControls useFrame 三条热路径(锁定/飞向/WASD)每帧 ~10 个 THREE 对象分配全部上提为模块级临时量 (§6 挂账清掉);锁定与飞向两块因各自提前 return 互斥而安全复用同组临时量,数学与求值顺序逐字不变。 - **拾遗** NEW:虚空诗不可复现、失之永逝,现可一键收存。纯模块 `state/shiyi.ts`(localStorage `shiyun_shiyi_v1`,以全集编号去重、最新置顶、上限 200、容损坏 JSON,+15 测试);PoemPanel 分享/留影旁 「收进拾遗/已在拾遗 ✓」;更多菜单「拾遗 — 我捞起的诗」面板,行点击经 `pulledFromIndex` 重建+飞回 (复刻 permalink `#p=` 还原路径),逐行删除 +「仅存于此浏览器」脚注。独立 zustand 切片,刻意置于 跨域重置纪律之外。本轮仅虚空诗(真实诗经诗人可再寻,扩展应改存 poetId+poemIdx,已注明)。 **批次 B — 数据运维** - `pipeline/pack-data.mjs` NEW + `npm run pack:data`:冷备打包从手工 tar/校验/传 201 资产收敛为一条命令 (流式 SHA-256、缺目录告警不崩、打印 gh release 命令);微型 fixture 全 round-trip 验证;顺手修掉 GNU tar 把 `C:\…` 绝对路径误判为远程 host:path 的真实坑(改相对 -f,bsdtar/GNU 通吃)。 - `VITE_DATA_BASE` 旋钮落地(§6 挂账):load.ts 六个 fetch 助手默认参数统一指 `DATA_BASE`(默认 /data, 不设零变化);vite-env.d.ts/.env.example 补齐;grep 确认 src 无其它 /data 直连。 - 版本化数据缓存铺路:nginx.conf 注释版两条同级 `^~ /data/v2/` location(poems 子块照搬 RAW+Range 规则, immutable 一年),默认仍 /data 86400;DEPLOY §2.1 说明开启时机与三项验证 curl。 --- ## 2026-06-10 — Session: 9th agent · round 2 (留影直达 + 改名) Owner report: 搜索诗人后,目录里无法对某一首诗单独开分享卡,只能绕道编号反查;并要求改名 奇迹时刻→留影. Root cause: Cinema 取诗只认 虚空 `selected` 或 `focus.poemIdx`(后者只有行星点击/诗句命中才设), 目录的"展开"是 PoetPanel 本地状态,Cinema 看不见 —— 面板按钮旧提示「展开一首诗后可框住那首」名不副实。 Opus 4.8 执行,orchestrator 复核门禁:tsc · **138 tests**(+15)· build。Commit `b3d32ba`. - **留影目录直框**(`store.ts` + `Cinema.tsx` + `PoetPanel.tsx`):诗人目录每一行新增「留影」按钮(与 定位/复制编号 并排,传原始索引 `i`,与定位同源,排序不影响),点一下即把那首框成分享卡。 - **显式留影目标**(`store.cinemaPoemIdx` + `openCinemaFor`):优先级高于虚空 `selected` 与搜索 `focus`; 关闭留影 / 换诗人 / 虚空捞诗 / 清空 都复位,绝不残留上一首。`selected`/`selectedPoet` 在 store 里本就 互斥(双向确认),显式分支放最前是防御性设计。 - **解析抽纯函数**(`ui/cinemaResolve.ts` NEW):`resolveCinemaPoem` 把「显式 idx > 虚空 > 搜的这首」三级 优先级独立出来,注入 indexer → 无 Canvas、无数据集可单测。 - **奇迹时刻 → 留影**:用户可见文案全部改名(诗人/虚空面板按钮、退出标题、面板按钮提示改为实情: 「留影当前搜中的那首;目录里每一行也有单独的留影」);代码标识(`cinema*`/`.cinema-btn`)与历史 DEVLOG/HANDOFF 条目不动。README/index.html 无出现(grep 验证)。 - 样式:`.pi-cinema` 行内按钮(金色点缀,随 `.pi-locate` 尺寸),并入 coarse-pointer ≥40px 触控规则。 - 测试 +15(123→**138**):openCinemaFor / 关闭复位 / 换诗人清空 / 互斥 / 解析优先级 / 越界与 idx=0。 --- ## 2026-06-10 — Session: 9th agent (orchestrated — vite 8 升级 · 动态 OG 分享卡 · 数据 v3 调研 NO-GO) Orchestrator-only main loop (fable-5); all execution delegated to Opus 4.8 sub-agents (成本嘱咐). Verify gate independently re-run by the orchestrator on the combined tree: tsc · **123 tests** (was 93, +30 new) · build · deploy:build. Commits `f723db8` (vite 8) + `85cac49` (OG) + this docs commit. **1 — 依赖升级 P2 清账:vite 8 / vitest 4 / plugin-react 6** (`f723db8`) - 清掉全部 5 个安装期 npm-audit 漏洞(1 critical:vitest-UI 任意文件读取 + 4 moderate:esbuild/vite dev-server),全在 vite≤6 dev 链上(round-5 P2 暂缓项)。生产依赖一字未动(react 18 / three 0.169 / fiber 8 / drei 9 / zustand 5 / postprocessing 2.19)。`npm audit` → **0**;依赖树 182→139 包。 - **Rollup→Rolldown 迁移**:vite 8 弃用对象式 `manualChunks`,且会把"唯一引用方"chunk 合并(three+r3f 塌成 ~950 KB 触发 700 KB 警告)。改用 `rolldownOptions.output.codeSplitting.groups` + `priority` (three=2 / r3f=1)→ 恢复 vite-5 同款三段切分(three 675.5 KB / r3f 276.9 KB / index 181.7 KB),无警告。 - **tsconfig `types` 补 `"node"`**:vite 8 的 `vite/client` 不再间接引用 `@types/node`,`*.test.ts` 里 `node:fs`/`node:url` 报 TS2307。 - **测试零改动**:vitest 2→4,6 个既有测试文件只用 `describe/it/expect/beforeAll` —— 93/93 未改一行。 - 保留项全部验证:5199 strictPort、`__OG_ORIGIN__` 构建期替换(built dist 0 残留)、chunk 警告限额注释、 precompress(容忍轻量 data 目录)。冒烟走 5198 端口(不碰用户在看的 5199,未用 preview MCP)。 **2 — 动态 OG 分享卡** (`85cac49`, `permalink.ts` + `deploy/og-inject.mjs` NEW + `feedback-server.mjs` + nginx + DEPLOY §6) - **问题**:分享链接是纯 hash(`#a=`/`#p=`),爬虫看不见 fragment → 每条分享都预览同一张通用 og.jpg。 - **Query 镜像**:`permalink.ts` 把目标镜像进 query(`/?a=…#a=…`),hash 仍是权威还原机制;boot 时无 hash 则回退读 query;清空选择剥离 a/p 但保留无关参数(utm…);旧纯 hash 链接逐字节兼容。纯函数 `buildShareUrl`/`parseTarget` 抽出 + 13 单测。 - **服务端注入(可选,零依赖)**:复用唯一后端 `feedback-server.mjs` —— 设 `SITE_ROOT` 后 `GET /?a=/?p=` 返回按目标改写 og/twitter 标题+描述的 index.html(诗人卡:「李白 — 诗云 · Poetry Cloud / 唐 · 1107 首 · 在三维诗云星图中漫游他的星团」;`?p=` 服务端无法 unrank(引擎在客户端 BigInt)→ 通用卡 + 编号截断)。 index.html + poets.index.json 启动时一次载入,绝无逐请求 I/O;`Cache-Control: public, max-age=3600`。 **不设 `SITE_ROOT` → 路由照旧 404,`/api/feedback` 字节不变;不部署/不改 nginx → 纯静态行为与今天完全一致。** - **安全(round-6 教训延续)**:注入值全 HTML 转义(敌意名 fixture 验证);query 先封顶(诗人 id≤64 hex、 编号≤4000 位)再查表;锚定正则只改写已知 meta 的 content,绝不回显输入;Host-DoS 仍 400 不崩(冒烟复测)。 - **nginx**:`location = /` 仅当 `$arg_a`/`$arg_p` 存在时反代 127.0.0.1:8787(if-safe proxy_pass),否则静态。 DEPLOY 新 §6:systemd `Environment=SITE_ROOT=…`、og-inject.mjs 一并拷贝、nginx 片段、验证 curl。 - 测试:og-inject 纯函数单测 + 1 spawn 冒烟(随机端口 + tmp SITE_ROOT:注入 200 / Host-DoS 400 / 反馈 POST 仍可用)。合计 **123 全绿**。 **3 — 数据 v3(当代诗人扩充)调研 → NO-GO**(只读调研,无代码变更;详见 DATA_AUDIT.md 补记) - 当代/现代语料源已饱和:所有可寻候选都追溯到 v2 已吃下的上游(sheepzh / yuxqiu / poemwiki)。 - 唯一更大候选 HF `Iess/chinese_modern_poetry` 是 sheepzh 二次抓取重塑的 LLM 训练对,**无作者字段** → 挂不上"每诗人一颗星"模型,去重后净增≈0,字库跳过率预计更高(3–6%),版权暴露更差。 - 收益≈0(当代名家 v2 已全在)vs 成本(数小时重建 + main 镜像 + ~201 资产冷备重传)→ **维持 v2**。 --- ## 2026-06-10 — Session: 8th agent · round 6 (adversarial review of round 5 — fixes) A multi-agent adversarial review of round 5's commit (2508b5a) ran in the cloud; it surfaced a critical DoS + a real data regression I'd introduced. All confirmed findings fixed + verified (commit `faf11f9`). - **CRITICAL — feedback-server unauth DoS:** a malformed `Host: a b` header made `new URL` throw in the async handler → unhandled rejection → process exit (one-line crash, reproduced). Guarded the URL parse (400) + wrapped the whole handler in try/catch. Re-smoke-tested: Host-DoS → 400 + server alive. - **SECURITY — feedback-server token:** moved the owner-inbox token from the query string (leaks to nginx access logs) to `Authorization: Bearer`, compared via `crypto.timingSafeEqual` over sha256 (was `!==`); GET path rate-limited too. DEPLOY §5 updated. - **REGRESSION (real, mine) — 17 broken `#a=` permalinks:** round 5 added 20 民国 names to `MODERN_JINXIANDAI`, which flipped **17 EXISTING** poets dangdai→jinxiandai → new `poetId` → broken shared links + moved star clusters (poet-id is the sibling of the charset-permalink contract). Reverted the additions (set FROZEN at v1 membership + documented); full rebuild restored **17/17** to their v1 dangdai ids; charset/lexicon stay byte-identical. Totals unchanged (32,657 / 933,857). - **ROBUSTNESS — cache poisoning:** the failure-as-success cache bug fixed for `poems/` in round 5 still bit `lines/` `linesf/` `search/` `gifts.json` (a transient 5xx/network failure latched empty → search/ gifts silently dead until reload). Now only `r.ok`/genuine-404 is cached; the sidecar idx cache no longer latches null on 5xx. - **UX/robustness:** SearchPanel clears the alias/miss note on select + tab-switch; the 五代十国 info row drops its clickable hover; the no-WebGL gate now sets `window.__SHIYUN_UNSUPPORTED__` so `main.tsx` skips the React mount (was black-screening over the gate message). - Corrected heavy data mirrored to the main worktree + the GitHub backup release refreshed to match. --- ## 2026-06-10 — Session: 8th agent · round 5 (post-launch P0/P1/P2 — alias search, error fallbacks, data v2, 自建反馈后端) Post-launch hardening per the owner's prioritized list. Verify gate green: tsc · **93 tests** (4 new) · vite build; every UI path also exercised live in the preview browser. **P0-1 — 诗人别名搜索 + 落空文案** (`src/data/poetAliases.ts` NEW + test, `SearchPanel`) - ~230-entry 字号/别称→语料本名 alias layer over poet search: 搜「陶渊明」→ 陶潜 (语料本名!), 李太白→李白, 苏东坡→苏轼, with a 「X」即「Y」 note. `NOT_POETS` explains prose-master misses (庄子/诸葛亮/三字经…) and points 探诗 at their 虚空编号; generic miss line otherwise. - **The integrity test caught real corpus traps**: 王右丞/李义山/晦庵/柳三变/元遗山/元美/文衡山/祝枝山/天随子 exist as their own duplicate-attribution rows, **方回 is a real distinct 元代 poet** (alias→贺铸 would have hidden him), and **王羲之 has real 兰亭诗** (removed from NOT_POETS). Runtime rule: a real row always wins. - Fixed the SAME dead-target bug in the pipeline: `GIFT_ALIAS` mapped 渊明-family → "陶渊明" (no such row) → those dedication edges never resolved. Now → 陶潜; gift edges 4,849 → **4,976**. build-search FAMOUS also fixed (陶渊明→陶潜) + modern famous added (食指/余秀华/西川…). **P0-2 — 加载失败兜底 + 浏览器守门** (`load.ts`, `poetPoemsLoader.ts` NEW, `store`, `PoetPanel`, `App`, `index.html`) - **Found + fixed a real production bug while verifying**: `loadPoetPoems` CACHED FAILURES as successes — `.catch(() => ({}))` latched an empty bucket on any network hiccup, so the poet rendered "0 poems" forever (no error, retry impossible). Failures now THROW and are never cached; 404 vs network-failure are distinguished for the sidecar cache. - Central `fetchPoetPoems` (replaces 5 copy-pasted call sites) reports failure → PoetPanel shows 作品载入失败 + 重试 (verified: simulated offline → error row; healed + retry → 李白 1107 首 render). - Boot failure → loading screen shows 星图数据载入失败 + 重新载入 (was an eternal spinner). - `index.html` gains a plain-ES5 capability gate: no BigInt / no WebGL → a human message instead of a black screen. **Data v2 — sheepzh/poetry modern layer, charset-FROZEN** (`pipeline/build-data.mjs`) - 字库 freeze is now the DEFAULT build behavior (production contract): existing `charset.json` re-emitted **byte-identical**, any poem with an out-of-字库 char is skipped → N=12,877 unchanged → **every existing 编号 permalink survives**. `REFLOW_CHARSET=1` for a deliberate breaking rebuild. - Imported [sheepzh/poetry](https://github.com/sheepzh/poetry) (汉语现代诗歌语料库, `data/作者_拼音/诗名.pt`): **+75,980 poems / +2,849 poets** after content-dedup vs yuxqiu (3,016), junk-folder filter (125 non-Han handles), charset gate (1,597 skipped). New totals **32,657 poets / 933,857 poems**. 余秀华 249 · 顾城 489 · 海子 323 · 徐志摩 19→65 · 食指 43. Full chain rebuilt (poems/lines/sidecars/search); git-tracked charset/lexicon byte-identical (verified via git diff), poets.index/gifts/manifest updated. - Modern poem TEXTS remain author-copyrighted (repo: 非商用) — same exposure class as yuxqiu; noted in credits (* footnote) + DATA_CONTRACT. **自建反馈后端** (`deploy/feedback-server.mjs` NEW + DEPLOY §5 rewrite) - Owner's direction: **our own backend, no third-party**. Zero-dep node collector (~100 lines): POST /api/feedback → JSONL append; token-protected GET inbox; coarse rate limit; **no IP stored** (privacy by design). Smoke-tested (POST ok / empty 400 / no-token 403 / token list / health). - DEPLOY §5 rewritten to lead with self-hosted (systemd unit + nginx same-origin location → no CORS) + client wiring `VITE_FEEDBACK_ENDPOINT="/api/feedback"`. Worker/Formspree demoted to no-server fallback. The 🟡 ACTION-FOR-运维 callout updated accordingly. **P1 — LICENSE / OG 卡 / favicon / 五代十国** - `LICENSE` (MIT + data-rights note) — README claimed MIT but the file was missing. - Share preview: full og/twitter meta in `index.html`; `public/og.jpg` (1200×630 星海 wordmark card, canvas-generated) + `public/favicon.png`; `__OG_ORIGIN__` build-time replacement from `VITE_SITE_ORIGIN` (vite.config inline plugin; unset → root-relative). - 五代十国 dynasty chip → non-interactive 「已并入唐」 note (the shell is empty by data design; 李煜 lives under 唐). **P2 — dev vulns / chunk** - `npm audit` (prod deps): **0 vulnerabilities**. The 5 install-time vulns (1 critical) are ALL dev-chain (esbuild dev-server GHSA via vite≤6/vitest); the fix is vite 8 — a breaking major NOT taken pre-launch. Revisit post-launch. Production static output is unaffected. - three.js chunk warning resolved via `chunkSizeWarningLimit: 700` + rationale comment (the app IS the canvas; splitting three out of the critical path buys nothing). --- ## 2026-06-09 — Session: 8th agent (pre-launch review — UI polish, data audit, feedback backend, deploy) Final pre-launch pass. Verify gate green every round: `tsc --noEmit` + `vite build` + **89 tests**; UI changes additionally eyeballed via `preview_eval` DOM measurement (screenshots time out on the WebGL canvas). Commits `0ac0bd7`→`140dc3b` on `main`. **Round 1 — 奇迹时刻 polish + data audit + feedback backend + deploy** (`9e70d8a`) - **画框 removed** (`ui/Cinema.tsx` + CSS): the gold frame + corner brackets collided with the 退出 button — gone. - **Tagline no longer orphans its last char**: `.cinema-tag-text { text-wrap: balance }` + auto-margin full-width centering (was `left:50%`-anchored, capping the box at half-width). 1-line on desktop, balanced 2–3 on mobile. - **Poem card pan + zoom**: the centred card is drag-to-move + pinch/wheel/± zoom (+ reset) so the user composes the shot. Pointer-events opt-in; `setPointerCapture` wrapped in try/catch. - **Data audit** (`docs/DATA_AUDIT.md` NEW, multi-agent web survey + adversarial verify): verdict **SHIP AS-IS**. Werneror+yuxqiu is the *optimal fit* (only broad+Simplified+permissive+parseable corpus); *not* the most comprehensive (ORCHESTRA-simple-1M +28% but Traditional/encumbered); *not* complete (明/清 ceiling — no 全清诗 exists anywhere). Corrected stale `chinese-poetry`-as-live-overlay copy in README + DATA_CONTRACT (the build reads only Werneror + yuxqiu); reframed 857,877 as a raw upstream count. - **Feedback backend** (`state/feedback.ts`, `vite-env.d.ts` NEW, `.env.example` NEW): `submitFeedback` now ALSO POSTs `{source,message,ts}` to `VITE_FEEDBACK_ENDPOINT` when set (fire-and-forget, keepalive); localStorage stays the source of truth; unset ⇒ 100% static. Both paths verified in-browser. - **Build hardening** (`pipeline/build-data.mjs`): a missing `/modern-poetry` now **fails loud** (was a WARN-only `try/catch` that silently dropped the 508 modern poets and desynced the index). Opt out: `ALLOW_NO_MODERN=1`. **Round 2 — data loading fix** (no code change; `f03b437` docs) - A fresh worktree shows a poet's poem COUNT (from git-tracked `poets.index.json`) but can't LOAD poems: the `public/data/{poems,lines,search}` buckets are git-ignored. Provisioned them by junctioning from the main worktree (李白 → 1107 poems load, bucket `206` raw). Rewrote `docs/DEPLOY.md §1` to lead with **provisioning the git-ignored data FIRST** (copy/junction the existing complete copy vs regenerate) + a 运维 Quickstart. **Round 3 — 搜的这首 + emoji + button size** (`140bd → 140dc3b`) - **`搜的这首` hit badge → its own line** (`.pi-hit { display:block }`): a 1-char title (秋/句) is no longer squeezed/truncated by it (verified: badge sits a line below the title). - **Removed all decorative emojis** → clean monochrome text (🔗📷💬🌟🛸🎯⚙⌨ gone; kept clean ✓ ✕ ⟲). - **统一大小**: 分享 (`.copy-btn.share`) and 奇迹时刻 (`.cinema-btn`) now share one size (11px / 3px 10px / radius 6px). - **Cinema zoom** uses a clean `−` (U+2212) / `+` instead of full-width `+/-`. **Round 4 — long-poem clipping + wording + 开源致谢 + deploy callout** (this entry) - **Long poems no longer clipped** (`.cinema-poem`): dropped `overflow:hidden` + `max-width:88vw` + `max-height:58vh` so the poem renders at FULL length (may extend past the viewport) — the user zooms out with `−` + drags to compose, instead of the left columns being occluded. (Verified: `overflow:visible; max-width:none; max-height:none`.) - **Less "technical" wording** (`ui/HUD.tsx`, `PoemPanel`, `SearchPanel`): 生成→捕捉, 随机→虚空 ("点击虚空时捕捉的 诗体", "只捕捉合平仄、押韵的诗", "非格律 · 虚空目录", "虚空 · 半编号"). Zero 生成/随机 left in the rendered UI. - **开源致谢 modal** (`ui/SettingsMenu.tsx` `Credits` + CSS): a third link beside GitHub opens an acknowledgements modal thanking **15 open-source projects** across 渲染/工具链/语料 (three.js, R3F fiber/drei/postprocessing, React, Zustand, Vite, TypeScript, Vitest, opencc-js, pinyin-pro, Werneror/Poetry, yuxqiu/modern-poetry, chinese_word_rhyme, chinese-poetry) with links + licenses, plus the 刘慈欣《诗云》/博尔赫斯《巴别图书馆》 inspiration. - **Deploy doc** (`docs/DEPLOY.md`): added a 🟡 **ACTION FOR 运维** callout — the server-side feedback store is NOT set up (no table/KV); stand one up + set `VITE_FEEDBACK_ENDPOINT` only if cross-device feedback is wanted (§5). --- ## 2026-06-09 — Session: 7th agent · round 3 (UI polish — 竖排分享卡 + 设置→更多 + 个人链接 + 页内反馈) Mostly UI, per the user's screenshots. Verify gate green: tsc + vite build + **89 tests**. - **奇迹时刻 → classical 竖排** (`ui/Cinema.tsx` + CSS): the share card poem was clipping long poems and the copy ran together. The poem now renders **vertical, right-to-left, one column per line** (`writing-mode: vertical-rl; text-orientation: upright`; `.cinema-line { white-space: nowrap }`) so a long poem spreads sideways instead of being truncated at the bottom; the card is centred like a hanging scroll. The exit button moved to the **top-left corner in red** (`截好图 · 退出 ✕`) — out of the shot but easy to find. - **设置 → 更多** (`ui/HUD.tsx`, `ui/SettingsMenu.tsx`): the HUD button + the panel title are renamed 更多 (it now also holds 关于/反馈, so 设置 was too narrow). - **关于 + 个人链接** (`SettingsMenu`): at the bottom of 更多 — 个人主页 `cohenjikan.com` + `GitHub` (Cohenjikan). - **页内反馈** (`state/feedback.ts` NEW, `SettingsMenu` FeedbackBox, `ui/FeedbackViewer.tsx` NEW): a 反馈 box in 更多 stores messages in **localStorage** (capped 5000 汉字, oldest drop first). The OWNER reads them via a hidden gesture — **5 taps on the 诗云 logo within 10 s** → `store.feedbackOpen` → a FeedbackViewer overlay listing each message + timestamp (+ 清空). ⚠ **localStorage is per-device**: the owner only sees feedback typed on the same browser. A cross-visitor inbox needs a serverless form (Formspree / Google Forms / a Cloudflare Worker) — `submitFeedback` is the single seam to repoint; wire it at DEPLOY if wanted. --- ## 2026-06-09 — Session: 7th agent · round 2 (奇迹时刻 share card + 手机面板默认折叠 + 寻路修复/增强) Follow-ups on the same worktree after the user tested on a real phone. Verify gate green: tsc + vite build + **89 tests**. (Commits: `8acb449` mobile-collapse; this round's cinema + gift-path on top.) ### 1 — 手机端面板默认折叠成底部提示条 (`ui/useSheet.ts` NEW) - On a phone a selection (诗人 / 虚空诗) or search result opened a FULL-screen bottom sheet that covered the galaxy. Now on coarse-pointer devices the data panels default to a slim **bottom peek bar** (one-line summary + 「▲ 展开」); tap to open the full sheet, 「▾ 收起」 to re-collapse. `useSheet(resetKey)` re-collapses on each new selection. SearchPanel starts collapsed to its tab row (tapping a tab expands). Desktop unchanged (peek only renders on `COARSE`). `PoetPanel` / `PoemPanel` / `SearchPanel` + the `.sheet-peek` CSS. ### 2 — 奇迹时刻 / cinema share card (`ui/Cinema.tsx` NEW) - A 📷 button (poem + poet panels) enters a **framed share card over the FROZEN scene** to guide a screenshot. `store.cinema` pauses ALL auto-animation — the galaxy spin (`Galaxy` skips `advanceSpin`), the void-pull lifecycle (`PulledStars` early-returns → markers never dissipate, per the user's ask), the highlight flash/fade (`PoemOrbits` freezes `poemClock`), and the gravity co-rotation (`FlyControls`). The overlay is a gold 相框 (border + corner ornaments + vignette) + a concept TAGLINE (5 options, cycled with ‹ ›, emphasising the 诗云 / 巴别图书馆 idea) + the framed poem with its full 全集编号 (「它在诗云里的唯一住址」) + the 诗云 brand. It is `pointer-events:none` except its controls, so you can still drag the camera THROUGH it to compose, then shoot. Manual camera input stays live during the freeze; only auto-motion stops. ### 3 — 寻路 (path-find) bug fix + bidirectional + 100 hops (`data/giftGraph.ts`, `three/GiftTrail.tsx`, `ui/GiftRoam.tsx`) - **The reported bug** (李白↔王安石 連線 looked wrong): two real issues — (a) BFS was **direction-sensitive** (李白→王安石 returned a different intermediary than 王安石→李白, both valid 2-hops → felt random), and (b) the gold 足迹 (manual roaming breadcrumb) and the cyan 路径 line coexisted and contradicted each other on screen. Verified against the data: 王安石 has NO 韩愈 edge (the screenshot 足迹 was actually 王安石→俞律, a real neighbour) — so the path-find itself was already correct; the confusion was the asymmetry + the two lines. - **Fix**: `giftPath` is now **deterministic + symmetric** — it searches from the canonical (smaller-id) endpoint with a weight-sorted expansion order, so A→B and B→A return the SAME chain (oriented to the caller) and a stronger relationship wins ties. Verified on real data: 李白↔王安石 = 李白→黄庭坚→王安石 both ways; 纳兰性德→李白 = 纳兰性德→李之仪→苏轼→李白 (3 hops). The gold 足迹 line is now **suppressed while a 路径 result is shown** (cyan path is the focus) and only ever draws **real edges** (`giftAdjacent` guard → never a fake straight line between unconnected poets). - **Task 3**: path budget **10 → 100 hops** (already UNDIRECTED — any 赠诗 relationship connects regardless of giver/receiver; confirmed). >100 hops → 无法链接 as before. The 足迹/return-line MEMORY stays a separate, unchanged ≤10 cap (`store.hopToPoet`). --- ## 2026-06-09 — Session: 7th agent (性能优化 + 移动端适配 — touch-fly/pinch, auto-quality, dpr cap, responsive bottom-sheets) Cut from `main` @ `59103ed`. Provisioned `poems/`+`lines/`+`search/` via junction to `main/public/data` (now the canonical source per the 6th-agent recovery) + `linesf/` from `nifty-kirch`; `npm install`; TOOK OVER port 5199 (stopped the 6th-agent's stale vite, restarted from this worktree). Worked the whole round behind two multi-agent passes: a 6-dimension **audit** (touch / responsive CSS / render-perf / load-perf / mobile-platform gotchas / deploy) → one prioritized plan, then an **adversarial review** (4 finders → each finding independently verified) that surfaced 20 confirmed issues, all of which are folded in below. The app was desktop-only (mouse-drag look, WASD fly, wheel speed) with zero `@media`/`touch-action` and a manual-only 画质 toggle. This round makes it usable + performant on phones/tablets. **No new data, no backend; engine/data untouched.** Verify gate green: `npm run build` (tsc + vite) + **89 tests** (was 68; +21 new pure touch-gesture tests). *(Real-device touch can't be exercised headlessly — multi-touch logic is covered by the pure `touchGesture.ts` unit tests + the verified state machine; the user eyeballs on a real phone.)* ### 1 — One shared device signal — `src/three/detectQuality.ts` (NEW) - `COARSE` = `matchMedia("(pointer: coarse)")` (touch primary). `WEAK` = `detectWeakGPU()`, evaluated ONCE at module load (before the Canvas mounts, so the galaxy is built at the right size — no high→low rebuild flash). **Heuristic: any coarse PRIMARY pointer ⇒ weak** — robust to the iPadOS-on-Mac-UA case (an iPad sends a *Macintosh* UA + reports >768px, so a UA/screen test misses it; `pointer:coarse` does not) and to large Android tablets; a touchscreen LAPTOP keeps a fine primary pointer so it stays high. Secondary desktop low-end: `deviceMemory≤2`, or `cores≤2 && mem≤4` (the `&&` avoids false-downgrading a privacy-clamped `hardwareConcurrency`). Consumed by store / App / FlyControls / SettingsMenu / Onboarding. ### 2 — Auto quality + dpr cap (the biggest mobile FPS levers) - `store.quality` now defaults `WEAK ? "low" : "high"` (manual 画质 toggle still wins). `low` already halves particle counts + drops bloom. - `App` caps Canvas `dpr={[1, WEAK ? 1.5 : 2]}` — dpr 1→2 quadruples the additive-fragment fill that dominates cost on a phone. Keyed to the **initial** WEAK seed, NOT the live toggle (changing a live Canvas dpr forces a GL context resize/flash); `gpuPick` reads `gl.getPixelRatio()` at pick time so the cap never breaks picking. Bloom stays gated on the reactive `quality` (mount/unmount is safe). - **行星·全部** (the ~857k-point additive layer) is gated OFF on weak devices (`toggleAllPoems` can turn it off but not on; SettingsMenu shows it disabled「弱设备已禁用,避免卡死」) — a phone flipping it on froze/OOM'd. ### 3 — Touch input — `src/three/FlyControls.tsx` + `src/three/touchGesture.ts` (NEW) - `index.html` viewport: dropped `maximum-scale` (WCAG 1.4.4) + added `viewport-fit=cover` (notch insets). `canvas { touch-action: none }` + `body { overscroll-behavior: none }` — THE gate for touch (else the browser eats one-finger drag as scroll, fires pointercancel mid-gesture, and pull-to-refresh reloads). - **Gesture scheme (user-chosen): 1-finger drag = look; 2-finger drag = fly; 2-finger pinch = speed/zoom; tap = pick.** All reuse the exact desktop camera math (`BASE_SPEED·speedMul`, the wheel's 0.1..80 clamp). Forward thrust is an analog **joystick** (centroid displacement off the gesture origin → hold to cruise), clamped to unit magnitude (a diagonal drag isn't faster than a cardinal one). A 2-finger gesture releases any lock (so touch users can leave a locked view). Pinch speed telescopes (per-move ratio product = total spread) → frame-rate independent. - Input is a `pointers` Map state machine (1 = look/orbit, 2-both-touch = fly/pinch). Hover-pick (a synchronous GPU readback) is skipped entirely on `COARSE` (touch has no hover). `pointercancel` handled. Touch-aware tap slop (14px vs 6px). The desktop single-pointer path is unchanged. - `touchGesture.ts` holds the pure, unit-tested math: `centroid`, `pinchDistance`, `thrustFromDrag`, `pinchSpeed`, `classifyGesture`. **Mode-lock** (`classifyGesture`): a 2-finger gesture commits to pan XOR pinch once it moves enough, so a one-handed pinch (centroid drifts ~½ the spread) can't leak thrust. ### 4 — Responsive layout — `src/styles.css` (one `@media (max-width:600px)` block) - Transient overlays (诗/诗人/设置/赠诗漫游) → full-width **bottom sheets** (slide-up; `transform:none`, NOT `!important`, so the keyframe still animates; `!important` only on left/right/top/bottom/width to beat the Settings inline drag style). 搜索 stays at TOP, full-width, positioned below the LIVE wrapped HUD height (`HUD.tsx` publishes `--hud-h` via a `ResizeObserver` incl. the notch — no fragile magic constant). - HUD top bar `flex-wrap` + trims desktop-only chrome (title-en / stat / 隐藏界面); HUD bottom hides the WASD/wheel hint (wrong on touch — Onboarding step 3 now shows a COARSE-specific touch hint instead). - Inputs forced to 16px (iOS won't zoom-on-focus); ≥40px tap targets under `(pointer:coarse)`; `vh`→`dvh` cascade on the 7 panel max-heights (iOS dynamic-toolbar safe); safe-area padding throughout. ### 5 — Review fixes folded in (the 20 confirmed findings) - **DQ-1 (high)** iPad/tablet false-negative → the COARSE-primary rule above. **F1/F2/F4** finger-transition bugs (onCancel view-jump; a 3→2 lift killing the last finger's look; stale 3→2 gesture baseline) → unified `reseedAfterLift` + onUp only finalizes a tap at zero fingers. **TG-2** one-handed-pinch thrust leak → `classifyGesture` mode-lock. **TG-1** `pinchDist` was dead code (2-finger unlocks) → deleted + its misleading tests. **F5** mouse+finger on a 2-in-1 tripping the gesture → arm only when both pointers are touch. **TG-3** diagonal-faster → unit-clamp the thrust. **DQ-4** privacy-clamped-cores false positive, **TG-4** test gaps, **settings-drag** no-op grab cursor on mobile → all fixed. - Consciously DEFERRED (verified low / intended): the joystick "hold = cruise" (documented + onboarding hint + the top search panel flies you back), per-frame `useFrame` allocation hoist (GC; do as a focused follow-up to avoid colliding with this round's touch edits), `prefers-reduced-motion`, `webglcontextlost` (iOS backgrounding — needs a real device), lexicon lazy-load, no dedicated vertical-touch gesture (pitch-up + forward-thrust climbs since thrust is camera-space), and the whole deploy/fuzzy-hosting track. ## 2026-06-09 — Session: 6th agent (徐志摩 data recovery + 寻诗/探诗 rename + 寻诗 prefix/title search + cluster-centering + guide-line coverage) Cut from `main` @ `27d3ec5`. A fresh worktree has no heavy data; provisioned poems/lines via junction to the known-good `epic-sinoussi` worktree + linesf from `inspiring-bhabha`, then TOOK OVER port 5199 (stopped the 5th-agent's stale dev server, restarted from this worktree) per the user. main/other worktrees left untouched. ### 1 — 徐志摩 (and the whole 新诗 set) data LOSS — recovered - **Symptom**: clicking 徐志摩 loaded no poems (panel still said 「19 首真实作品」 from poets.index). - **Scope (it was systematic)**: exactly the **508 modern poets** (475 当代 + 33 近现代 = the entire yuxqiu/modern-poetry import: 徐志摩/海子/北岛/顾城/戴望舒/洛夫/芒克…) were missing their poem TEXT from BOTH `poems/*.json` AND `lines/*.json`, while their `poets.index.json` rows (committed in git) survived. All 29,300 classical poets + every committed asset (charset/gifts/lexicon/manifest/poets.index) were intact. - **Root cause**: `build-data.mjs` reads the modern corpus inside a `try/catch` that only WARNS on failure (`build-data.mjs:163`). A `poems/` rebuild that didn't ingest modern produced poems/ + lines/ without it, while git's `poets.index.json` kept modern from an earlier good build → the two diverged. `inspiring-bhabha` (the live 5199) **junctions main's `poems/`**, so main + bhabha were broken identically. - **Fix (this worktree)**: junctioned `poems/`+`lines/` from `epic-sinoussi` (a COMPLETE copy — all 29,808 poets incl. modern, with sidecars) → `missing = 0 / 29,808`. Verified live on 5199: 徐志摩 Range-fetch → `206`, 19 poems《雪花的快乐》; 诗句「轻轻的我走了」→ 徐志摩《再别康桥》. **The source corpus is intact**, so a full `build-data.mjs` rerun also recovers it. ⚠ **main's `poems/`/`lines/` are STILL broken** (left untouched per the user) — the NEXT worktree cut from main must provision from a good source or regenerate. ### 2 — 诗句 → 寻诗, 造诗 → 探诗 (display rename, logic unchanged) - The two tab names overlapped in meaning. 「诗句」(find a real poem) → **寻诗**; 「造诗」(compute a poem from an index) → **探诗**. Display-only: the internal `Tab` ids stay `"line"`/`"compose"`. (`SearchPanel`, `Onboarding`.) ### 3 — 寻诗 prefix + 诗名 search (incremental) — `pipeline/build-search.mjs` (`npm run build:search`) - The old 诗句 search keyed only WHOLE lines (hash-bucketed) → a mid-line like 「举头望明月」 found nothing until the full line, and there was NO title search. New `search/` index (sharded by `hashStr(key)&0xff`, 256 shards): • **EXACT full TITLE for every poem** → 诗名搜索 for ANYONE, incl. an obscure poet's famous piece (张若虚《春江花月夜》) — found when the whole title is typed. • **len-≤3 PREFIX of a FAMOUS poet's lines + title** → incremental: a single 字, a half line, or a title prefix matches as you type. `举头望` → 李白《静夜思》 (mid-line!); `静` → 静夜思; capped 12 famous-first. - **Size discipline**: prefix-expanding ALL poems was 0.8–2.9 GB. A poemCount bar can't bound it (prolific poets own most poems). Gating PREFIX keys to the 48-name FAMOUS set (≈30 K poems) + exact-title-for-all lands **129 MB / 256 shards (~0.5 MB each)** — local-rich, deploy-curatable (lever = FAMOUS list / PREFIX_MAX). - **Wiring**: `load.ts::searchByHead` (prefix+title) + `searchPoems` (merges searchByHead with the exact-line `searchByLine` + fuzzy, dedups, ranks famous-first, caps ≤2/poet for variety, top 10). 寻诗 tab calls `searchPoems`; 探诗's `findReal` still uses `searchByLine`. 纯随机 半编号 section unchanged. - Limitation: incremental (prefix) only surfaces the 48 famous poets; a non-famous poem appears via exact TITLE (full) or exact LINE (full)/fuzzy. Widen `FAMOUS` in build-search.mjs + rerun to broaden. ### 4 — cluster centering (4a) + guide-line coverage (4b) - **4a 恒星系偏上**: `positions.poemOffset` tied the planet RADIUS to the poem index (`pow((i+0.5)/P,…)`) while the LATITUDE `yd` was also monotonic in the index → small radius at the +y pole, large at the −y pole → a lopsided teardrop hanging BELOW the poet, so the cluster centre read as offset toward the TOP of the frame. Replaced the radial quantile with a HASHED uniform (same density, decorrelated from latitude) → symmetric cloud centred on the poet. Same function backs render/pick/locate/guides → clicks stay aligned. - **4b 指引线漏诗**: `PoemGuides` drew the FIRST `MAX_LINES=4000` poems → for a >4000-poem poet it dropped the outermost planets (the ones most needing a guide). Now SAMPLES uniformly across the whole range (`poemIndexOf`) so guides span the entire cluster; ≤4000-poem poets are unchanged (every poem still gets a line). Verify gate: `npm run typecheck` clean, `npm test` **66/66**, `npm run build` ✓. Data + search HTTP-verified on 5199. **4a/4b are visual — the user eyeballs them on a real GPU (no in-conversation preview, per the user).** ### 5–7 — 产品优化: 行星指引常驻 + 赠诗漫游 (跳跃 / 足迹 / 路径) After GitHub backup + **syncing main's data** (copied the complete `poems/` into main, rebuilt main `lines/`+`search/` → `missing 0/29808`, main no longer broken), built three coupled features. **All build + 66/66; the 3D interactions need a real-GPU pass (no preview).** - **5 — 行星指引线常驻 (HUD 指引)**: new `store.guideHold` + HUD toggle. ON → the selected poet's `PoemGuides` lines hold full brightness instead of the ~10 s auto-fade; only ONE poet's guides show at a time (they follow `selectedPoet`, so picking/hopping to another poet switches them). OFF = the existing one-shot flash. - **6 — 飞跃赠诗线 (hop to the linked poet)** [user chose 新面板+3D点线]: new **`GiftRoam`** panel (docked bottom-left, shown when 赠诗 on) lists the selected poet's 赠答往来 (赠出→/←收到 · 对方 · 对应赠诗) — click a row to fly across to that poet. ALSO **3D**: clicking a 赠诗 arc in the scene hops along it — `FlyControls` CPU-projects the selected poet's ego-net arcs (same bundled Bézier as `GiftLines`) and picks the nearest within 16 px on a void click (cheap, click-only). Hopping = `store.hopToPoet` (select + lock-follow + APPEND to trail). - **7 — 赠诗漫游升级 (breadcrumb + return + path search)**: • **足迹/返回线**: `store.giftTrail` = the poets you hopped through; **`GiftTrail.tsx`** draws PERSISTENT bright-GOLD return lines between consecutive nodes (≤10 edges; trail capped at 11 nodes), with a pulse. Click a 足迹 node (panel) or re-hop to return (the trail trims back). Cleared only on 赠诗 off / 清除 / selecting an UNRELATED poet (`selectPoet` resets the trail to `[that poet]`). • **对应赠诗标注**: for an out-edge, `giftGraph.dedicationPoemIdx` finds the giver's poem whose title contains the recipient's name (best-effort; 字号 aliases like 子由→苏辙 may miss → shows the link without a poem). Clicking it flares that planet (`pulseAt`, no lock change). • **路径查找**: set 起点/终点 (from the selected poet) → `giftGraph.giftPath` BFS shortest path ≤10 hops over the 4 849-edge graph (microseconds; budget raisable) → CYAN path highlight in 3D (`GiftTrail`) + clickable result chips to fly along. Verified on real data: 苏轼→苏辙 1跳, 苏轼→纳兰性德 2跳 (苏轼→李之仪→纳兰性德, 跨宋清), 李白↔徐志摩 无连接 (古典/新诗为不连通分量). New: `data/giftGraph.ts` (adjacency + BFS + dedication finder), `three/GiftTrail.tsx`, `ui/GiftRoam.tsx`; store gains `giftTrail`/`pathStart`/`pathEnd`/`pathResult` + `hopToPoet`/`clearTrail`/`setPath`; HUD 指引 toggle. ### 8 — 设置菜单 + 指引设置 + 漫游易用性 (+ 编号唯一性 discussion) **编号唯一性 (discussion only, no change)**: the user noticed `编号 N` means DIFFERENT poems under different 诗体 (五绝=20字 vs 七律=56字 — each form is a SEPARATE fixed-length catalog whose index starts at 0 → overlap → the same number collides). Verdict: **solvable, no new math needed.** All poems form a countable set, so a single `ℕ↔诗` bijection exists — and the project ALREADY has one: `engine.anyRank/anyUnrank` (the 自由/任意长 全集编号 over 字库∪break) gives every poem (any form/length) a UNIQUE number. The per-form numbers are convenience sub-catalogs. Fundamental tradeoff: either the number is longer (a universal length-aware encoding) OR a short per-form number is only unique WITH its 诗体 tag — length info must live somewhere. Recommended (deferred): treat the 自由 全集编号 as the canonical id, or always show 编号 with its form. Recorded for a future engine decision. Then 7 product items (all build + 66/66; visual/interaction need a real-GPU pass): - **1 指引设置**: `guideHold`→`guideMode`(off/flash/hold) + `guideCoverage`(all/optimized) + `guideSeconds`. 'all' = a line to EVERY poem (一首不漏; uncapped — max poet ~8k = cheap); 'optimized' = the round-9 sampled cap. flash = show `guideSeconds`s then fade; hold = 常驻. 恢复默认 = flash/optimized/10. (`PoemGuides`, `store`.) - **2 诗云设置菜单**: new `ui/SettingsMenu.tsx` (⚙设置 in HUD) collects 指引(全套) / 行星 / 赠诗 / 引力, each with 恢复默认 (+ a 全部恢复默认). The 4 toggles moved OUT of the HUD top bar. 赠诗漫游 stays a separate panel. - **3 路径查找手填**: GiftRoam path endpoints can be TYPED (reuse `searchPoets` autocomplete) or set 选中, not only click-picked. - **4 弱化往来线**: `store.pathDimEgo` + GiftRoam checkbox → `GiftLines` dims the ego arcs (×0.16) so the cyan path /gold trail dominate when finding a route. - **5 滑动条统一**: shared thin gold `::-webkit-scrollbar` + `scrollbar-color` across all overlay panels. - **6 赠诗线好点**: hover-highlight — `FlyControls` hover-projects the ego arcs; nearest within 26px sets `store.giftHoverId` → `GiftLines` lights that arc (×2.8); the click range is the same generous threshold (22px) and clicking the highlighted arc hops. So you SEE what you'll click + hit it easily (was pure luck). - **7 行星更好点 + 诗名 + 提亮(仅选中)**: the selected poet's cluster now HOLDS the highlight for the WHOLE selection (was ~10s) at brighter+larger (`bright 3.4`, `sizeScale 860`, `maxPx 44`) → bigger GPU pick target = easier to click; hovering one of its planets shows the poem 《title》 near the cursor (`store.hoverPoem` + `ui/PoemHoverLabel`, via hover poem-pick gated to the selected poet). (`PoemOrbits`, `FlyControls`.) ### 9 — 唯一全集编号 (SOLVED) + 真实诗定位 + 设置可拖/亮度 + 搜索宽度 - **唯一全集编号 (the headline fix)**: the per-form catalogs collided (`编号 N` was a DIFFERENT poem in each 诗体, because each fixed form is its own `[0, N^L)` catalog → overlapping ranges). Unified on the **universal `anyRank`** over (chars + LINE-BREAK symbols) as THE displayed 编号 for EVERY poem. Because any poem is just a char-and-break sequence and `anyRank` is a bijection over those, a 七绝 and its 自由 twin are the IDENTICAL symbol run → the IDENTICAL number — so the **dedup is automatic** (no detection needed; the user's "自由 复刻 fixed-form → 一首诗两个编号" worry dissolves). Verified on real data: 李白 《赠汪伦》 → a 127-digit number, identical via the 七绝 path and the 自由 path, reverses to the exact poem, and differs from the old 114-digit per-form babelRank. Changes (`engineApi`): `describe` (void-pull), `pullByIndex` (reverse — now form-agnostic, infers 诗体 from structure, always in-range), `pulledFromIndex`, `halfIndexAuto` (now `anyRank(opening)` — a TRUE prefix of the universal number, kept). UI: 探诗 填字/凭编号, PoetPanel 目录, PoemPanel, `permalink` (`#p=`, form dropped) all use the universal index. The per-form babelRank/格律 catalogs survive in the engine for spatial scatter + 格律 mode only. **+2 engineApi tests (dedup + form-agnostic reverse) → 68 total.** The number is long on purpose ("越长越贴合原著"). - **1 真实诗定位**: when a composed / reverse-looked-up poem IS a real corpus poem (`findReal` now returns poetId+poemIdx), its 定位 button flies to that poet's ACTUAL orbiting planet (李白's 赠汪伦) — not a random void-scatter point. (`SearchPanel.goReal`; cyan button.) - **2 设置可拖动**: `SettingsMenu` is now drag-by-header (default below the bar, left of the right-side panels) so it never traps behind the 诗人/诗 panels and you can watch the effect live. - **3 搜索宽度**: 寻诗/探诗 panel `width: min(384px, 100vw-36px)` + overflow-x hidden — fills the space, no sideways scroll. - **4 指引线亮度可调**: `store.guideBrightness` (default 0.7×, lower) + a settings slider (0.2..2×); `PoemGuides` scales its colour by a `uBright` uniform. ### Round 8 — fuzzy LINE index (mid-line 异文) + orbit-lock + sustained highlight + guide lines - **诗句 mid-line variant search (item 1)** — round-7's `findReal` fuzzy only covered COMPOSE; 诗句 search of a variant line (「举头望明月」) still missed. New `pipeline/build-fuzzy.mjs` (`npm run build:fuzzy`) builds a delete-1 / SymSpell skeleton index `linesf/` (4096 shards, disk-staged so it doesn't OOM): a same-length 1-substitution shares the (L-1) skeleton with the differing char dropped. `searchByLine` adds a fuzzy fallback (when exact = 0, len 4..10) via `lineSkeletons` + `loadFzShard`. `lineSkeletons` has 4 unit tests. **Large local index (~4.4 GB, 41 M keys, git-ignored); a DEPLOY needs a curated/server-side fuzzy** (noted). - `fb2ad58` **fix**: the per-skeleton cap ranked by poemCount → 李白《静夜思》(1107首) was EVICTED from the shared skeleton 举头望月 by hyper-prolific minor poets (王世贞 8009首), so 举头望明月 found noise. Now the cap scores the 48 landmark poets (`FAMOUS`) far above poemCount (never evicted) + `searchByLine` ranks landmark poets first. Verified: 举头望明月 → 李白《静夜思》 #1. **Limitation/lever**: only the 48 landmark poets are protected — a famous poem by a non-landmark poet (《春江花月夜》/张若虚, 2首) can still be evicted from a shared skeleton. Widen `FAMOUS` in `build-fuzzy.mjs` (+ re-run `npm run build:fuzzy`) to cover more, or move to a curated名篇 table for deploy. - **Orbit-lock (item 2)** — the lock is now an orbit camera: closer default distance (was too far), DRAG rotates the locked view (yaw/pitch, no release), WHEEL zooms (distance); movement keys still release. (`FlyControls` `lock` ref + handlers.) - **Sustained highlight (item 3)** — the highlight now holds FULL brightness (`HOLD_FLARE`) for the whole ~10 s then weakens (was flash-then-dim); brighter/larger so the cluster stays legible in the spread field. - **行星指引 / guide lines (item 4)** — new `three/PoemGuides.tsx`: selecting a poet emits a line to EVERY poem it wrote (赠诗-style), self-rotating with the cloud, one-shot ~10 s (grow→hold→fade) then auto-deletes. - Verified: build + 66/66. ### Round 7 — bigger irregular self-rotating clusters + 10s highlight + camera lock + fuzzy findReal `874cbba` - **Clusters too small/local/uniform** (user) → `positions.poemSystemRadius` ~6× (35+13√P; 杜甫→~555); `poemOffset` clumpy power-law radius + WIDE jitter (non-uniform) + per-poet ELLIPSOID axes (irregular shapes: sphere/ellipse/oblate). - **Self-rotation**: `poemOmega` + shared `poemClock`; each cloud rotates around its poet. Mirrored in the visual shader, the GPU pick shader (clicks still land), and the time-aware `poemPosition` (locate tracks). - **Highlight (item 1)**: selecting a poet ALWAYS flashes its whole cluster in for ~10 s regardless of the 行星 toggle (flash-in → hold → fade-out); selected poet star also enlarged ×1.8. - **Camera lock-follow (item 3)**: `store.lockPoetId/lockPoemIdx` + FlyControls — selecting a poet/planet centres + follows it (decoration's faster spin streams past = motion); released by any movement key or a look-drag. Wired from 3D click / 诗人 / 诗句 / 目录. - **Search (item 4)**: `findReal` relaxed to a same-length ≤2-char (≥85%) near-match → popular 静夜思 「举头望明月」 (corpus「山月」) now flagged as 异文. Mid-line variant *search* still needs the fuzzy line index. - Verified: build + 62/62. ### Round 6 — clickable planets + 群星 v1 (soft 3D clusters, fade, emphasis) `05ca09f` (clickable) · `9f57d11` (群星 v1) - **Click a planet → open its poem**: `gpuPick` renders a 2nd pick layer (poem ids offset by `POEM_PICK_BASE`) in the same offscreen pass (depth-tested), click-only; `PickResult={kind:poet|poem}`; PoemOrbits registers `pickTargets.poemLayer` + `resolve`. +5 vitest (→62). - **De-blockify v1**: flat disc → soft near-spherical cluster; selecting flashes/fades the cluster in/out. (User then said still too small/blocky-when-all-on → Round 7.) ### Round "planets" — 行星 feature (poems orbit their poet) + 目录/搜索 locate `60a34a7` - `three/positions.ts` (poetPosition moved here + poemPosition/poemOffset). `three/PoemOrbits.tsx` + HUD **行星** toggle (`store.showAllPoems`): OFF = selected poet's poems; ON = all 857,877. 目录定位 (PoetPanel 🛸定位) + 诗句 search fly to the exact planet (`store.pulseAt`). Verified build + 57/57 + DOM e2e. ### Fix — dead 诗句 search / real-poem detection (missing `lines/`) `20a55dd` - `public/data/lines/` was absent → `searchByLine` found nothing → no real hits + `findReal` failed + the void/planet double-location. `pipeline/build-lines.mjs` (`npm run build:lines`) rebuilds it from `poems/` (no corpus; per-line cap keeps the most-prolific author). 256 buckets / 9.18M refs. Verified: 床前明月光 → 李白《静夜思》 → flies to the planet (same spot as 目录). ### Fix — dormant Range egress `3596841` - `manifest.poemSidecar:true` but `poems/*.idx.json` absent → whole-bucket (~0.9MB) fetch per poet. `pipeline/build-sidecars.mjs` (`npm run build:sidecars`) re-emits each bucket + sidecar. Verified live: 206 `bytes 12-9787/890706` (~98.9% saved). ### Round 5 — UX: placeholder, centre cross dissolve, fixed port `17242a3` - 造诗 placeholder simplified; `poetPosition` centreBlur 0.42→0.5 + coreScat 0.15→0.22; Galaxy disk azimuthal+core fill; BULGE 42k→64k. `vite` fixed port 5199 strictPort. Centre confirmed by user on a real GPU.