diff --git a/AGENTS.md b/AGENTS.md index 8d1b78d..67e42c0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,7 @@ 以下几条每次提交都要做到,没有“这次改动小”的例外;其余各节是怎么做好的指引。 1. **结构和文档同步。** 按下表更新文档,与代码放在同一次提交里。新增、删除、改名、拆分源码文件一律算改了模块职责:职责清单同时是文件目录,每个源码文件都要有一行。 -2. **跑结构检查。** `node tools/check_structure.mjs`,失败不提交。它检查每个源码文件都登记在架构文档里、每个前端模块都能从入口引用到、每个前端文件都能按浏览器的方式解析(`node --check` 对 .js 文件会漏掉浏览器报错的写法),并执行规模阈值(第 2 节):新文件不能一开始就超线,已超线的文件比上次提交只能变短或持平。 +2. **跑结构检查。** `node tools/check_structure.mjs`,失败不提交。它检查每个源码文件都登记在架构文档里、每个前端模块都能从入口引用到、每个前端文件都能按浏览器的方式解析(`node --check` 对 .js 文件会漏掉浏览器报错的写法)、CSS 里没有按 `style` 属性匹配的 `:has()`(会让拖动画布掉帧,见 [lessons.md](docs/architecture/lessons.md)),并执行规模阈值(第 2 节):新文件不能一开始就超线,已超线的文件比上次提交只能变短或持平。 3. **不留旧实现。** 被取代的模块、样式、文案 key、文档说明同一次提交删掉(第 6 节)。 4. **按风险验证**(第 7 节),只提交本任务的文件,没有明确同意不推送。收尾时工作区干净,或在交付说明里列出每个有意留下的未提交文件。 5. **测试产物不进仓库。** 测试代码放 `tests/`;截图、录屏、日志、原始测试数据、审计附件放在 `.agents/`(已忽略,只留本机),不用 `git add -f` 绕过忽略规则。提交的文件里不写本机路径、用户名或 `file:///` 链接,文档之间用相对链接。 diff --git a/docs/architecture/lessons.md b/docs/architecture/lessons.md index f732407..d06d1c9 100644 --- a/docs/architecture/lessons.md +++ b/docs/architecture/lessons.md @@ -103,3 +103,23 @@ body:has(#anomalous-toolbox-modal[style*="display: flex"]) #anomalous-sidebar-to **以后怎么做** - 两个仓库的规范都写了安全底线(本仓库 AGENTS.md 第 5 节“安全底线”,Anomalous_TTS 的 ARCHITECTURE.md“不能破的规矩”)。新接口按它写,审查时逐条对照。 - 本仓库的现状(2026-10-09 统计):55 个会写文件的接口,只有 MCP、模型导入、整理三处检查了本机请求;删除模型、改模型信息、开始下载、导入备份等在 `--listen` 时局域网里的其他设备也能调用。没有执行下载内容、pickle、eval 的代码。2026-10-12 的 v1.58.1 一次补齐:`api/local_guard.py` 一个中间件把关 `/anomalous/` 下所有会改东西的请求,只接受本机(见 backend.md“Requests from other devices”)。 + +--- + +## 2026-10-13 · v1.58.0–v1.58.2:同一种 `:has()` 又掉帧(第二次) + +**现象**:#22(Lindemellis):216 个节点的工作流,开着 Prompt Assistant 时一次画布重绘 465–770 ms,拖动只有 2–3 帧。只删掉一条规则就回到 53 ms。 + +**根因**:图标栏(46facb9,2026-09-29,随 v1.58.0 发布)在 `web/styles/15-shell-rail.css` 里又写了 + +```css +#anomalous-container.anomalous-has-rail:has(> .anomalous-nb-modal[style*="display: flex"]) #anomalous-sidebar-wrapper +``` + +和上面 v1.57.2 那条是同一种写法:`:has()` 里按 `style` 属性匹配。v1.57.2 修好三天后就写回来了,因为经验只写在这份文档里,没有东西拦着;当时的性能实测也只跑了 main 和音频分支,没跑图标栏所在的分支。 + +**修复**:工作区开没开由 JS 在容器上加减 `anomalous-workspace-open`,CSS 只看这个 class(#22 的修复)。 + +**以后怎么做** +- `node tools/check_structure.mjs` 现在会拒绝 `:has()` 里出现 `[style…]`,报出文件和行号。 +- 同一个坑第二次出现,就把它做成检查,不再只补一段经验。 diff --git a/docs/guides/testing.md b/docs/guides/testing.md index b578990..574dec6 100644 --- a/docs/guides/testing.md +++ b/docs/guides/testing.md @@ -31,7 +31,7 @@ Python 用哪个: ## 3. 只跑一部分 ```bash -# 结构检查:每个源码文件都登记在架构文档里、前端模块都能被引用到、文件长度不超线 +# 结构检查:每个源码文件都登记在架构文档里、前端模块都能被引用到、文件长度不超线、CSS 没有按 style 属性匹配的 :has() node tools/check_structure.mjs # 一个前端测试 diff --git a/tools/check_structure.mjs b/tools/check_structure.mjs index 6e823e6..d83fb2b 100644 --- a/tools/check_structure.mjs +++ b/tools/check_structure.mjs @@ -2,7 +2,8 @@ // Fails when a source file is missing from the architecture map, a web module is // no longer imported from an entry, a new file starts over the size threshold, a // file already over it grew since the last commit (AGENTS.md section 2), or a web file -// does not parse as the ES module the browser loads. +// does not parse as the ES module the browser loads, or a CSS :has() matches the style +// attribute. import fs from 'node:fs'; import path from 'node:path'; import { execFileSync } from 'node:child_process'; @@ -81,6 +82,26 @@ for (const line of syntax.split(/\r?\n/).filter(Boolean)) { failures.push(`${path.relative(ROOT, file).split(path.sep).join('/')}: does not parse (${message})`); } +// 5. No `:has()` that matches the style attribute. Chrome then treats every inline-style +// write on the page as a possible change and recalculates styles for the whole page; with +// extensions that move overlays every frame, canvas dragging fell to a few fps (v1.57.2, +// issue #22). Mirror the state as a class on an ancestor instead. +for (const file of [...list('web', '.css'), ...list('web/styles', '.css')]) { + const css = read(file).replace(/\/\*[\s\S]*?\*\//g, (comment) => comment.replace(/[^\n]/g, ' ')); + for (let start = css.indexOf(':has('); start !== -1; start = css.indexOf(':has(', start + 1)) { + let depth = 0; + let end = start + 4; + for (; end < css.length; end++) { + if (css[end] === '(') depth++; + else if (css[end] === ')' && --depth === 0) break; + } + if (/\[\s*style\b/i.test(css.slice(start, end))) { + const line = css.slice(0, start).split('\n').length; + failures.push(`${file}:${line}: :has() matches the style attribute, which makes every inline-style change restyle the whole page; toggle a class instead`); + } + } +} + if (failures.length) { console.error(`\nStructure check failed:\n ${failures.join('\n ')}`); process.exit(1);