Repository navigation
Releases
v0.12.0
0.12.0 — every record now says whether it still means anything 🔍
Phase E added no capability. It made the records the plugin already keeps reviewable: a lifetime total that
names the day the books opened, an ignore line that is named when it excludes nothing, and a mute list that can
be shrunk by one entry after a plan. Nothing expires on its own; a human retires what no longer describes anything.
✨ Features
- A lifetime total says when the books opened.
scanCountsSinceis the selected bucket's own start, and for
an aggregate the oldest contributing bucket's — never the document's, which would date a young workspace before
it existed. The caption gains· since 2026-09-14; a document with no origin renders the 0.11.0 caption. - An ignore line that excludes nothing is named. The walk records, per source line, whether that pattern
excluded anything, and the report prints.scanignore: 3 patterns, 1 matched nothing.Two lines with the same
effect are counted separately, so the duplicate you may now delete is visible. - The mute list can be reviewed and shrunk by one entry.
/guidance-suppress --reviewlists every entry
with its date and reason against the last scan, and never writes;--remove <siteId> [--dry-run]plans one
removal, and the same line without the flag applies it.
🧪 Behaviour changes
- Absence is stated, never assumed.
unusedisnull, never0and never an empty list, when the scan was
truncated, hit a cap or read fewer files than its baseline: an incomplete scan cannot prove a suppression dead. - An old counter stays undated rather than borrowing a date.
normalizeStateno longer stampsnowonto a
loaded document withoutsince; legacy root counters have no origin, so_unattributedand its aggregates are silent. - A removal is not a fix. It un-mutes one site and moves no counter: the counters document is byte-identical
through a removal,lastScan.suppressedkeeps the count it was given, and the next scan counts the finding
again. The words stay apart: muted, unused, resolved — the last one does not exist in the product. - A copy that never answers says so. The live panel pass found that
Копировать JSONcould look dead
forever, because a clipboard write parked by an unfocused window never settles: the label now races a bounded
wait and says "The clipboard did not answer". That is not "refused", and a late success does not flip it back. - The transcript row each panel action leaves is the accepted price.
executerenders every command line the
panel runs, so a mount and a scan cost one visible JSON block each; a cache or a second data path would be worse.
🛠 Hardening
- The removal writer is narrower than the append writer. One entry, one invocation, one confirmed rewrite:
temp-then-rename, every other byte and the entry order preserved, a malformed line preserved exactly, the landed
bytes re-parsed before the rename, a failed write leaving the file byte-identical, and an unknown id is a no-op. - The ignore patterns never reach
stats.json. Rows stay in memory on the scan value and only
{ patterns, idle }is persisted — an ignore line is a path fragment and that file is profile-wide.
🧩 Compatibility
versionstays1. Every field is additive and absent on an older document, which renders the 0.11.0 panel.
The four tools are unchanged, there is no new command — review and removal are flags on it — and no new
dependency and no new chart.
📝 Skills and docs
- ADR-0004 amends ADR-0003's "never a delete" to append and one confirmed
removal, and states the residual it cannot design away: a removal cannot undo what a previous scan reported.
📦 Package
- 258 files, 989 kB packed, 2.9 MB unpacked — no new module, zero runtime
dependencies, Apache-2.0. - 506 tests in 18 files: 502 passed, four skipped (this machine's symlink limit). The final tree was then
run 40 times back to back to answer one question the earlier phases had only recorded: a flake that
showed up asEBUSYintest/stats.test.mjs's own teardown, and — once the retry was in — asENOENT
reading the counters file, one run in twenty each. The retry alone still gave 19 green of 20; the cause was
underneath it, a fixed 20 ms sleep racing the plugin's 2-second debounced save, sosettle()now waits for
the write that is actually in flight and the retry only covers the platform's lock. 20 green, 0 red, and
the harness sweeps the temp homes it leaves behind.
🇷🇺 Коротко по-русски
Развернуть
- Пожизненный итог говорит, когда открылись книги:
scanCountsSince— это дата, начало выбранной папки, а для
сводки — начало самой старой из входящих в неё; дата документа не подходит, она была бы раньше самой папки.
Подпись в панели получает· since 2026-09-14, а документ без начала рендерится ровно как в 0.11.0. - Строка
.scanignore, которая ничего не исключает, теперь названа: отчёт печатает
.scanignore: 3 patterns, 1 matched nothing.— это единственное место, где строку файла можно назвать
простаивающей, и две строки с одинаковым эффектом считаются отдельно, чтобы дубликат был виден. - Список подавлений можно просмотреть и уменьшить на одну запись:
/guidance-suppress --reviewсверяет файл
с последним сканированием (дата, причина, сколько совпало, сколько нет) и ничего не пишет;
/guidance-suppress --remove <siteId> [--dry-run]сначала показывает план, а без флага удаляет ровно одну
запись. - Отсутствие доказательства не превращается в ноль:
unused— этоnull, а не0и не пустой список, когда
сканирование было усечено, упёрлось в кап или прочло меньше файлов, чем база; битая строка — это проблема
разбора, а не «неиспользуемая». - Старый счётчик остаётся без даты, а не занимает чужую:
normalizeStateбольше не ставитnowдокументу без
since, дату получает только действительно новый документ или папка, поэтому_unattributedи сводки с ним
даты не печатают. - Удаление — не исправление: оно снимает заглушение с одного места и не двигает ни один счётчик, документ
счётчиков остаётся байт в байт, а следующее сканирование снова считает находку находкой. - Копирование, которое не отвечает, так и говорит: метка гоняется с ограниченным ожиданием и пишет «буфер
обмена не ответил», это не «отказал», и поздний успех её не переворачивает. Строка команды, которую каждая
кнопка панели оставляет в транскрипте, — принятая цена, а не недосмотр. Пакет: 258 файлов,
989 kB в упаковке, 2.9 MB распакованного, 506 тестов в восемнадцати файлах (502 прошли,
четыре пропущены — все из-за symlink в этой машине). Финальное дерево прогнано 40 раз подряд: сначала
флейк проявлялся какEBUSYпри уборке временного дома, а после ретрая — какENOENTпри чтении файла
счётчиков, по одному разу на двадцать прогонов; ретрай дал 19 из 20, а причина была глубже — фиксированные
20 мс гонялись с двухсекундным дебаунсом записи, поэтомуsettle()теперь ждёт реально идущую запись.
Итог: 20 зелёных, 0 красных, и обвязка подметает временные дома прошлых прогонов. Зависимостей ноль.
🇨🇳 中文摘要
展开
- 终身合计现在说明账本从哪天开始:
scanCountsSince是一个日期,取所选工作区自己的起点,汇总时取其中最早的一个;
不用文档的开始日期,那会把年轻的工作区标到它存在之前。面板的终身说明加上· since 2026-09-14,而没有起点
的文档渲染得和 0.11.0 一模一样。 - 什么都不排除的
.scanignore行现在会被点名:报告打印.scanignore: 3 patterns, 1 matched nothing.—— 这是
产品里唯一允许把某行叫作「闲置」的句子;效果相同的两行分别计数,因此可以删掉的重复行看得见。 - 屏蔽列表可以查看,也可以减掉一条:
/guidance-suppress --review把文件与最近一次扫描对照(日期、原因、匹配
几条、几条未匹配)且不写任何东西;/guidance-suppress --remove <siteId> [--dry-run]先给出计划,去掉
--dry-run后只删除一条记录。 - 没有证据就不写成零:扫描被截断、触到上限或读到的文件比基线少时,
unused是null,不是0,也不是空
数组;格式坏掉的行是解析问题,永远不会被算作「未匹配」。 - 旧计数器宁可没有日期,也不借用别人的:
normalizeState不再给没有since的文档盖上now,只有真正新建的
文档或工作区才有日期,因此_unattributed以及包含它的汇总都不打印日期。 - 删除不是修复:它解除一处屏蔽,不动任何计数器 —— 计数器文档逐字节不变,下一次扫描仍把该发现算作发现。
- 不会应答的复制会说出来:标签与一段有界等待赛跑,并显示「剪贴板没有响应」;这不是「拒绝」,迟到的成功也不会
把标签翻回去。面板每个动作在对话里留下一行命令,这是被接受的代价,不是疏忽。打包:258 个文件、
989 kB、解包 2.9 MB,506 个测试 / 18 个文件(502 通过,4 跳过,都是这台机器无法创建
符号链接所致)。最终代码树连跑 40 次:抖动先以清理临时 home 时的EBUSY出现,加上重试后又以读取计数器
文件时的ENOENT出现,各约每 20 次 1 次;只加重试仍是 19/20,根因更深 —— 固定的 20 毫秒在和插件两秒的
防抖写入赛跑,所以settle()现在等待真正进行中的写入。结果:20 次通过、0 次失败,测试夹具也会清扫
上一次运行留下的临时目录。运行时依赖仍为零。
Install: dsh plugin --profile desktop add dsh-modern-web-guidance
Full diff: v0.11.0...v0.12.0
v0.11.0
0.11.0 — the panel can act, and every action asks first ✍️
0.10.0 made the counters honest about whose numbers they are. 0.11.0 gives the panel hands: it writes for
the first time, to exactly two files, one entry at a time — behind a visible plan, a second, distinct
confirmation, and a refusal that is reported as a refusal. Same four tools, no new dependency.
✨ Features
- Mute one finding, never a rule or a line.
/guidance-suppress <siteId> [--dry-run]appends one entry
to<workspace>/.suppress, keyed by the 8-character site hash phase B compares, capped at 200 entries, a
200-character reason and 256 KiB. A row offers Mute, the plan shows under it, only a second click appends.
The muted finding keeps its place, order and count:85 findings, 3 suppressedin the scan,Suppressed: 3 in the last scan (ls-13 2, ls-02 1) - muted, still findingsin the report,mutedin the panel. - Offer a
.scanignoreline, and say what it does./guidance-scanignore <pattern> [--dry-run]appends
exactly one line, never rewrites, and judges identity by effect (report/and/report/are one pattern).
It makes the next scan read less and mutes nothing; the plan names the file's own line, the count it would
reach and how wide the pattern is, and the panel keeps "reads less" apart from "muted" in all three languages. - Reset one slice instead of everything.
--scan-only,--workspace <name|id|path>and--days Nnarrow
--reset --yes, which keeps the meaning it always had; each plan names the target, each result names what
stayed. - Copy as JSON. The panel copies the envelope it already holds: no host call, no file, no new command —
and the button is absent when there is no report or no clipboard.
🧪 Behaviour changes
- The panel is no longer read-only, and it is not a writer either: every action is a command the host
runs — four human commands now, not two — so a scan or a render still writes nothing, a single click writes
nothing, and a refusal can never look like a success. - Muted is not fixed. A suppressed site keeps its place in the scan and is never reported as "resolved" by
the phase B comparison: the number sits beside the findings, it is never subtracted from them. - No reset, at any scope, opens the suppression file: its messages end "The suppression file was not
touched: muting is not a counter", and a--daysreset adds that lifetime rule and guide totals carry no day.
🛠 Hardening
- The reader stays forgiving; the writer does not. Append-only, one fixed path, realpath-contained,
temp-then-rename, and the result re-parsed through the scanner's own reader before the rename: a symlink, a
directory or a malformed file is a refusal, and a failed write leaves the original byte for byte..scanignore
has no cap where.suppressdoes — deliberate, and the plan states the count without claiming headroom. - One identity, three readers.
lib/site.jsis the single site derivation the scanner, the counters and
the file all use, so a mute cannot name a different site than the scan that found it.
🧩 Compatibility
versionstays1.suppressedandsuppressedRulesare additive and clamped tofindings; a
document written before this release loads and renders byte-identically — zero prints nothing at all.- The four tools are untouched. The chat row gains a
(suppressed)suffix and nothing else.
📝 Skills and docs
- ADR-0003 licenses exactly two writes, and the
suppression contract fixes the file, its caps and the muted-not-fixed
rule.
📦 Package
- 258 files, 971 kB packed, 2.9 MB unpacked — two new modules (
lib/site.js,
lib/suppress.js), zero runtime dependencies, Apache-2.0. - 463 tests in 18 files: 459 passed and four skipped, every skip this machine's symlink restriction.
🇷🇺 Коротко по-русски
Развернуть
- Заглушить одно место — не правило и не строку:
/guidance-suppress <siteId> [--dry-run] [--reason <text>]
дописывает одну запись в<workspace>/.suppress, ключ — тот же 8-символьный хеш места, что и в фазе B;
капы 200 записей, 200 символов причины, 256 KiB. В панели у строки есть кнопка Заглушить, план
показывается под ней, и дописывает только второй, отдельный клик. - Заглушённая находка остаётся находкой: она сохраняет место, порядок и счёт, получает
suppressed: true,
и её по-прежнему считают все поверхности — скан печатает85 findings, 3 suppressed, отчёт
Suppressed: 3 in the last scan (ls-13 2, ls-02 1) - muted, still findings, а панель помечает строку
muted. Заглушено — не исправлено, и сравнение фазы B никогда не назовёт такое место «исчезнувшим». - Игнорирование — не заглушение:
/guidance-scanignore <pattern> [--dry-run]дописывает ровно одну строку,
никогда не перезаписывает файл, а совпадение считает по эффекту (report/и/report/— один шаблон).
Следующее сканирование читает меньше; панель так и говорит, ни в одном из трёх языков не смешивая слова. - Сброс теперь можно сузить:
--scan-only,--workspace <name|id|path>,--days N; голый
--reset --yesстирает ровно то же, что и раньше. План называет цель, результат — что осталось. - Ни один сброс, ни в каком объёме, не открывает файл подавлений: его сообщения кончаются словами «файл
подавлений не тронут: заглушение — не счётчик». Сброс по--daysдобавляет, что пожизненные итоги правил
и гайдов не имеют дня и потому остаются. - «Копировать JSON» копирует конверт, который у панели уже есть: без вызова хоста, без файла и без новой
команды; кнопки нет, если нет отчёта или буфера обмена. Пакет: 258 файлов, 971 kB в упаковке,
2.9 MB распакованного, 463 теста в восемнадцати файлах (459 прошли, четыре пропущены —
все из-за ограничения на symlink в этой машине), зависимостей по-прежнему ноль.
🇨🇳 中文摘要
展开
- 屏蔽一条发现 —— 不是一条规则,也不是一行:
/guidance-suppress <siteId> [--dry-run] [--reason <text>]
往<workspace>/.suppress追加一条记录,键是 B 阶段用的同一个 8 字符位置哈希;上限 200 条、原因 200
字符、文件 256 KiB。面板在对应行提供屏蔽按钮,计划显示在该行下方,只有第二次、单独的一次点击才写入。 - 被屏蔽的发现仍然是发现:它保留位置、顺序与计数,加上
suppressed: true,所有界面照旧统计它 —— 扫描打印
85 findings, 3 suppressed,报告打印Suppressed: 3 in the last scan (ls-13 2, ls-02 1) - muted, still findings,面板给该行打上muted标签。屏蔽不等于修好,B 阶段的比较永不会把它说成「已消失」。 - 忽略不是屏蔽:
/guidance-scanignore <pattern> [--dry-run]只追加一行,从不重写文件,判定按效果
(report/与/report/是同一条模式)。下一次扫描读得更少;面板在三种语言里都把这两个承诺分开说。 - 重置可以只清一片:
--scan-only、--workspace <name|id|path>、--days N;裸的--reset --yes仍然
清掉它一直清掉的那些。计划说明目标,结果说明留下了什么。 - 任何范围的重置都不会打开屏蔽文件:消息结尾是「屏蔽文件未被触碰:屏蔽不是计数器」。按
--days重置还会
说明规则与指南的终身合计没有日期,因此保留。 - 「复制为 JSON」复制的是面板本来就持有的信封:不调用宿主、不读写文件、不新增命令;没有报告或没有剪贴板时
按钮不出现。打包:258 个文件、971 kB、解包 2.9 MB,463 个测试 / 18 个文件
(459 通过,4 跳过,都是这台机器无法创建符号链接所致),运行时依赖仍为零。
Install: dsh plugin --profile desktop add dsh-modern-web-guidance
Full diff: v0.10.0...v0.11.0
v0.10.0
0.10.0 — whose numbers are these, and did the fix land? 🧭
0.9.0 taught a counter to explain itself. 0.10.0 makes it say whose numbers they are: scan counters
are partitioned per workspace, the last scan names its branch, and history answers "when did a rule fire"
and "is this site still there". Same four tools, same 33 rules, same version: 1, no new dependency.
✨ Features
- The panel says which project it is showing. A
Workspace: {name}chip and a switcher sit in the
header;/guidance-stats --workspace <name|id|path>and--alldo the same from the command line.
Scan counters are kept apart per workspace inside the onestats.json; guidance counters stay
profile-wide, because a hit rate measures the corpus, not a directory. - The last scan names its branch. A completed scan reads exactly
<workspace>/.git/HEAD— one hop
through a worktree'sgitdir:pointer, a 1 KiB cap on each file, nogitprocess, no enumeration —
and prints; branch mainor; detached at a1b2c3d. A failed read prints nothing at all: no
"unknown" token, no guess (ADR-0002). - Twenty scans of history, and one honest comparison.
scanByDayand per-rule first/last-seen
dates answer "when", a 20-deepscanHistoryanswers "what the recent scans found", and a comparison
line answers "did the fix land":Since the scan of 2026-11-09 08:12: 3 no longer present, 82 still open, 1 new. The report adds a five-rowRecent scansblock, the panel the same line and a last-seen date.
🧪 Behaviour changes
/guidance-statsdefaults to the session's workspace, not to every workspace: the unqualified
number that mixed projects is the defect this release removes. With no session workspace it aggregates
and says why;--allasks for the aggregate explicitly.- A site is compared by its hash, not by its line. The key is FNV-1a over path, rule id and snippet,
so a comment inserted above it changes nothing, and only eight hex characters are stored — a hash of a
path, never a path, and never the snippet; an edited snippet is one site gone and one new, never a fix. - "Not comparable" is a first-class answer, with seven guard tokens:
no-baseline,fewer-files,
truncated,no-files,sites-unknown,branch-changedandrule-table-changed. They print raw,
like a skip reason, and the comparison then carries no numbers — never three zeros.
🛠 Hardening
- Every addition is bounded: history depth 20, ten rules per entry, four skip reasons, 200 site keys,
40 diff rows, five rows on screen, one 1 KiB cap per file read, onegitdir:hop and nothing more. - One new read surface, and it only makes a label:
.git/HEADis read on the completed-scan path
only, never from an argument, and the scan tool's result and text are unchanged — the model sees no label.
🧩 Compatibility
versionstays1. The partition, the branch member and the history are additive, and a bump
would be silent data loss: the normaliser reads any other version as an empty document. A 0.9.0
document loads intact, its scan counters becoming the_unattributedbucket, named rather than hidden.- Version skew costs a line, never a row. A host without the new fields renders the 0.9.0 panel and
report exactly; an absent history, comparison or label renders shorter rather than as an invented zero.
enginesunchanged: DSH>=0.2.0-rc.1 <0.3.0-0, Node>=22.19.0.
📝 Skills and docs
- The decisions are written down: ADR-0002, the attribution
contract and the history contract.
📦 Package
- 256 files, 943 kB packed, 2.8 MB unpacked, zero runtime dependencies, Apache-2.0.
- 373 tests in 17 files — 372 passed plus one symlink-walk test that skips itself where the host
forbids creating symlinks.
🇷🇺 Коротко по-русски
Развернуть
- Панель говорит, чьи это числа: чип
Рабочая папка: {name}и переключатель в шапке, флаги
--workspace <name|id|path>и--allу команды. Счётчики сканера у каждой рабочей папки свои,
счётчики гайдов общие на профиль: доля попаданий измеряет корпус, а не каталог. - Последний скан называет ветку: читается ровно
<workspace>/.git/HEAD— один переход поgitdir:,
кап 1 KiB на файл, безgitи без перечисления, — и печатается; branch mainили
; detached at a1b2c3d. Не прочиталось — не печатается ничего, без «unknown». - История на 20 сканов и честное сравнение:
scanByDayи даты первого/последнего появления правила
отвечают «когда», а строка сравнения — «исправили ли»:Since the scan of …: 3 no longer present, 82 still open, 1 new. В отчёте появляется блокRecent scansиз пяти строк, в панели — та же
строка, пять строк истории и дата последнего появления у каждого правила. /guidance-statsпо умолчанию показывает рабочую папку сессии, а не все сразу: без сессионной
папки отчёт агрегирует и объясняет почему,--allпросит агрегат явно.- Сайт сравнивается по хешу, а не по строке: ключ — FNV-1a от пути, id правила и сниппета, поэтому
вставленный комментарий больше не переименовывает всё ниже; хранятся восемь hex-символов — хеш
пути, а не путь, и не сниппет. - «Сравнение невозможно» — полноценный ответ: семь токенов (
no-baseline,fewer-files,truncated,
no-files,sites-unknown,branch-changed,rule-table-changed) печатаются сырыми, и тогда в
сравнении нет ни одного числа — никаких трёх нулей. versionостаётся1: всё аддитивно, документ 0.9.0 читается целиком, его счётчики сканера
становятся бакетом_unattributed. Пакет: 256 файлов, 943 kB в упаковке, 2.8 MB
распакованного, 373 теста в семнадцати файлах (372 прошли, один пропускается), зависимостей
по-прежнему ноль.
🇨🇳 中文摘要
展开
- 面板说清数字属于谁:标题栏里有
工作区:{name}chip 与切换器,命令行有--workspace <name|id|path>
与--all。扫描计数器按工作区分开,指南计数器仍是整个 profile 的:命中率衡量的是语料,不是目录。 - 最近一次扫描带分支标签:只读
<workspace>/.git/HEAD—— 透过 worktree 的gitdir:只跟一跳、每个
文件 1 KiB 上限、不运行git、不枚举目录 —— 打印; branch main或; detached at a1b2c3d;
读不到就什么都不打印,没有 “unknown”。 - 20 条扫描历史加上诚实的比较:
scanByDay与规则首次/末次出现日期回答「何时」,比较行打印
Since the scan of …: 3 no longer present, 82 still open, 1 new;文本报告多出五行Recent scans,
面板多出同一行比较、五行历史与每条规则的最后出现日期。 /guidance-stats默认显示当前会话的工作区,而不是所有工作区混在一起:没有会话工作区时报告会聚合
并说明原因,--all显式要求聚合。- 位置按哈希比较而不是按行:键是路径、规则 id 与代码片段的 FNV-1a,插入一行注释不会再把它下面每个
位置改名;只存八个十六进制字符 —— 是路径的哈希,不是路径,也不是片段。 - 「无法比较」是一等答案:七个守卫 token(
no-baseline、fewer-files、truncated、no-files、
sites-unknown、branch-changed、rule-table-changed)原样打印,此时比较里没有任何数字 ——
绝不给三个零。 version仍是1:所有字段都是追加的,0.9.0 的文档完整读入,其扫描计数器成为_unattributed桶。
打包:256 个文件、943 kB、解包 2.8 MB,373 个测试 / 17 个文件(372 通过,
1 跳过),运行时依赖仍为零。
Install: dsh plugin --profile desktop add dsh-modern-web-guidance
Full diff: v0.9.0...v0.10.0
v0.9.0
0.9.0 — a counter that explains itself 🔎
A ranked rule used to read ls-17 121: an id only the source explains, a number with no unit, on a
screen whose period switch does not apply to it. 0.9.0 gives every ranked counter the words it was
missing — its confidence, a count that says 121 findings, and the rule's own sentence — and adds one
caption saying the ranked totals are lifetime. Same four tools, same 33 rules, same scanner behaviour,
no new dependency.
✨ Features
- A rule row explains itself.
ls-17 121becomesls-17 · medium · 121 findingswith the rule's
own sentence — "Build the DOM with createElement/append instead ofdocument.write(), which discards
the parsed document." — clamped to two lines underneath, the whole message in the row'stitle, and
no second line at all when the host sends none. The confidence token stays raw
(high/medium/low), like a rule id. - A guide chip names its guide.
html (121)keeps its visible id and count and carries the
catalog'sdescriptionas a hover hint, so the ranked hints can be read without a retrieval call. - The chat row explains a finding without adding a line. The scan table reads the per-finding
messageandsnippetit already received astitles on the rule and place cells — the two facts
it used to throw away.
🧪 Behaviour changes
- One caption under "Rules that matched" while the scope is lifetime: "Lifetime totals, not the
{days}-day window", naming the window actually selected, so the period switch and the ranked counts
no longer look like they answer the same question. It is the release's only new panel string, and it
lands in the English, Russian and Chinese dictionaries in one commit — the parity test fails
otherwise. /guidance-stats --jsoncan carry the meaning.renderJson(snapshot, { rules, guides })adds
message,confidenceandguideIdto everytopRulesrow,descriptionto every
topScanGuidesrow, and the top-levelscanCountsScope: 'lifetime'. The command passes both
tables;renderJson(snapshot)with no options still returns the earlier document byte for byte, so
an older consumer cannot tell the enrichment exists.- Nothing in the scanner moved. Same 33 rules, same caps, same refusals, same text, same finding
shapes: this release changes what the counters say, not what they count.
🧩 Compatibility
- Version skew costs a caption or a hint, never a row. A host without the enriched fields leaves
this panel rendering the terse ids and counts it always did, with no caption. A rule id the current
table no longer knows keeps its row with empty text, an empty confidence and a null guide — the
count is still true when its explanation is gone. enginesis unchanged: DSH>=0.2.0-rc.1 <0.3.0-0, Node>=22.19.0.
📝 Skills and docs
- The contract is written down where the next reader looks:
docs/notes/legacy-scan-contract.md, the
panel'sdocs/notes/client-panel-spike.md, and theAGENTS.mdbullet a later panel change has to
keep true.
📦 Package
- 256 files, 913 kB packed, 2.7 MB unpacked, zero runtime dependencies, Apache-2.0. The file list
does not move: the growth is source insidelib/report.js,index.jsandclient.js. - 257 tests in 15 files — 256 passed plus one symlink-walk test that skips itself where the host
forbids creating symlinks.
🇷🇺 Коротко по-русски
Развернуть
- Панель объясняет счётчики: строка
ls-17 121читается какls-17 · medium · 121 findings, под ней
собственное предложение правила (две строки, полный текст вtitle; пустое сообщение не оставляет
второй строки). Токен уверенности (high/medium/low) остаётся сырым, как id правила. - Чип гайда
html (121)сохраняет видимые id и число, а описание из каталога уходит в подсказку. - Строка находки в диалоге берёт уже полученные
messageиsnippetкакtitleу ячейки правила и
ячейки места — без новой строки на каждую находку. - Одна подпись под «Сработавшие правила», пока счёт идёт за всё время: «Итоги за всё время, а не за
{days} дн.». Она называет выбранное окно, поэтому переключатель периода и ранжированные числа больше
не выглядят ответом на один вопрос. Это единственная новая строка панели, и она есть в словарях EN,
RU и ZH сразу. - JSON прирастает, форма не меняется:
renderJson(snapshot, { rules, guides })добавляетmessage,
confidence,guideIdк строкам правил,description— к строкам гайдов, и
scanCountsScope: 'lifetime'; вызов без опций возвращает прежний документ байт в байт. - Расхождение версий стоит подписи или подсказки, но не строки: правило, которого таблица уже не
знает, остаётся со счётчиком и пустым текстом. - Пакет: 256 файлов, 913 kB в упаковке, 2.7 MB распакованного, 257 тестов в 15 файлах (256
прошли, один пропускается там, где нельзя создать символическую ссылку), зависимостей по-прежнему
ноль; поведение сканера не изменилось.
🇨🇳 中文摘要
展开
- 面板让计数器自己解释:
ls-17 121变成ls-17 · medium · 121 findings,下面附上规则自己那句话(最多
两行,完整内容放在title;宿主没有句子就只剩一行)。置信度保持原样,和规则 id 一样不翻译。 - 指南 chip
html (121)仍显示 id 与计数,目录里的描述放进title作为提示。 - 对话里的发现表格把已经收到的
message与snippet用作规则单元格和位置单元格的title,不再为
每条发现多加一行。 - 「命中的规则」下方多一行说明:「累计总数,不是最近 {days} 天的窗口」。它点明当前选中的窗口,上方的时间
切换与排名数字不再看起来在回答同一个问题。这是本版唯一新增的面板字符串,EN、RU、ZH 三份词典同时
加入。 - JSON 只增加字段:
renderJson(snapshot, { rules, guides })为规则行补上message、confidence、
guideId,为指南行补上description,并给出scanCountsScope: 'lifetime';不带参数调用仍然逐字节
返回旧文档。 - 版本错配只丢一行说明或一个提示,不会丢一行数据:规则表已经不认识的 id 仍然保留计数,只是文字为空。
- 打包:256 个文件、913 kB、解包 2.7 MB,257 个测试 / 15 个文件(256 通过,1 个在无法创建
符号链接的环境里跳过),运行时依赖仍为零;扫描器行为没有变化。
Install: dsh plugin --profile desktop add dsh-modern-web-guidance
Full diff: v0.8.2...v0.9.0
v0.8.2
0.8.2 — the mark nobody can see 🫥
0.8.1 shipped a manifest the host refuses to load. Nothing about the code changed between the two: a
UTF-8 byte-order mark was written into package.json, the file looks identical in every editor and in
git diff, and the install fails with SyntaxError: Unexpected token. 0.8.2 removes the mark and adds
the check that would have caught it.
🛠 Fix
package.jsonno longer starts with a BOM. The host validates the manifest withJSON.parse,
which throws on a leadingU+FEFF;require()strips the mark andnpm packdoes not mind, so the
suite and the CI gate were both green while every install of0.8.1failed. A DSH install of0.8.1
reportsinstallation rejected: Cannot validate installed package … SyntaxError: Unexpected token '', and a published package cannot be corrected in place, so the fix is a new version.- A guard now fails on a BOM instead of a user.
test/plugin.test.mjsreads the first three bytes
of every file the host parses —package.json,index.js,client.js, the three locale
dictionaries and the two corpus JSON files — and asserts they are notEF BB BF. Verified by putting
the mark back: the test goes red withpackage.json starts with a UTF-8 BOM; the host parses it with JSON.parse, which fails on one, then green once it is stripped.
🧪 Behaviour changes
- None. Same four tools, same 33 rules, same caps, same JSON shapes, same panel: 0.8.1's
ls-12
narrowing is unchanged, and the only difference a scan can show is that 0.8.2 exists.0.8.1is
superseded by this version; deprecating it on npm with a pointer here takes one authenticated
npm deprecate, which trusted publishing cannot do — it can only publish — so that command is the
owner's to run.
📦 Package
- 256 files, 908 kB packed, 2.7 MB unpacked, zero runtime dependencies, Apache-2.0, and
245 tests in 15 files (244 passed plus one symlink-walk test that skips itself where the host
forbids creating symlinks).
🇷🇺 Коротко по-русски
Развернуть
- 0.8.1 уехала с BOM в
package.json— невидимой меткой UTF-8. Код тот же, файл выглядит так же, а
установка падает: хост проверяет манифест черезJSON.parse, который на этой метке бросает
SyntaxError: Unexpected token.require()метку срезает,npm packна неё не жалуется — поэтому
тесты и CI были зелёными, а ставилось ничего. - В 0.8.2 метка убрана; опубликованный пакет на месте не правится, поэтому это новая версия, а 0.8.1
этой версией перекрыта. Пометить её как deprecated на npm со ссылкой сюда — одна команда
npm deprecateс вашим логином: у trusted publishing есть право только на публикацию. - Добавлен тест, который читает первые три байта у всего, что хост парсит (
package.json,index.js,
client.js, три словаря локалей и два JSON корпуса) и требует, чтобы это не былиEF BB BF.
Проверено возвратом метки: тест краснеет с текстом про BOM, после снятия — зелёный. - Поведение не меняется: те же четыре инструмента, те же 33 правила, те же капы и формы JSON.
- Пакет: 256 файлов, 908 kB в упаковке, 2.7 MB распакованного, 245 тестов в 15 файлах,
зависимостей по-прежнему ноль.
🇨🇳 中文摘要
展开
- 0.8.1 的
package.json带上了 UTF-8 BOM。代码没变、文件看起来也没变,但安装会失败:宿主用
JSON.parse校验清单,遇到这个标记直接抛SyntaxError: Unexpected token;而require()会去掉它、
npm pack也不报错,所以测试与 CI 全是绿的,装上却用不了。 - 0.8.2 去掉该标记;已发布的包无法就地修正,因此发新版本,由 0.8.2 取代 0.8.1。要在 npm 上把它标为
deprecated 并指向这里,需要一次带登录的npm deprecate—— trusted publishing 只有发布权限。 - 新增测试读取宿主会解析的每个文件的前三个字节(
package.json、index.js、client.js、三个语言词典
与两个语料 JSON),要求不是EF BB BF。把标记放回去验证过:测试变红并给出 BOM 说明,去掉后恢复绿色。 - 行为没有变化:仍是四个工具、33 条规则、同样的上限与 JSON 结构。
- 打包:256 个文件、908 kB、解包 2.7 MB,245 个测试 / 15 个文件,运行时依赖仍为零。
Install: dsh plugin --profile desktop add dsh-modern-web-guidance
Full diff: v0.8.1...v0.8.2
v0.8.1
0.8.1 — the scanner stops giving advice that breaks a clamp 🧷
A finding is only worth its fix if the fix is safe. ls-12, the 2009-prefixed-flexbox marker, also fired on
the standard clamp block, where "use unprefixed display: flex" deletes the truncation the block exists for.
It now flags only the spellings a clamp cannot serve — same 33 rules, same caps, nothing to migrate.
🛠 Hardening
-
The rule no longer fires on the line-clamp idiom. This block is not the 2009 flexbox:
display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
display: -webkit-boxis the block container-webkit-line-clampneeds, and
-webkit-box-orient: verticalis how the clamp stacks its lines, so adisplay: flexrewrite would break
both.ls-12reports neither line now. On the live bench project the wrong finding at
src/components/ui/PostCard.astro:110-112is gone: that scan went from 10 findings to 9, withls-12
from 1 to 0. -
What it still flags cannot be a clamp.
display: -webkit-flex,display: -webkit-inline-flex,
-webkit-box-pack,-webkit-box-align,-webkit-box-flex,-webkit-box-ordinal-group,
-webkit-box-direction, and-webkit-box-orient: horizontal— each of those is prefixed flexbox with no
clamp reading, so the advice to use unprefixeddisplay: flexandgapstays correct there. Same rule id,
same guide (css-layout), samehighconfidence, same message text. -
The cost, stated rather than buried. Precision was bought with recall: a page that genuinely uses a bare
display: -webkit-boxas flexbox is now unreported. A rule is one line regex and cannot see the
-webkit-line-clampline above the marker, so it cannot tell the idiom from the old spelling, and we chose
silence on the two spellings the idiom owns. The boundary and its reasoning are recorded in
docs/notes/legacy-scan-parity.md.
🧪 Behaviour changes
- This repository still reports 85 findings, and that is a coincidence, not a no-op. The composition
moved:test/fixtures/scan/dirty/styles.csswas retargeted fromdisplay: -webkit-boxto
display: -webkit-flexso the fixture keeps carrying a truels-12marker (the fixture set still returns
3 files and 8 findings, one of themls-12), a bare-webkit-boxinside a test comment stops matching, and
eight prefix spellings added to a test collapse — under 0.8.0's run rule — into a single finding.ls-12
still accounts for 3 findings here, at different places. Re-run the scan and expect the same total with
a different report. - Nothing else in a report moves. Same 33 rules, same 18 code extensions, same
SCAN_LIMITS, same
ordering, samelinesandoccurrencesfields and the same JSON shape: a diff of two scans differs only in
ls-12entries.
📝 Skills and docs
.scanignorenow says it may park a single file (issue #10): a pattern can name one file rather than a
directory, and that file is skipped while its siblings are read — measured on a workspace of three pages
with one named, 2 files and 2 findings with onescanignoreskip. Naming the parked path explicitly still
returns 0 files and the same skip: the ignore wins over an explicit path, deliberately, because the file may
only ever make a scan read less. The contract and the glossary both say so now.- The rule's boundary is written down, so whoever widens
ls-12next meets the reason it was narrowed
before changing it:docs/notes/legacy-scan-parity.md.
📦 Package
- 256 files, 907 kB packed, 2.7 MB unpacked (measured by
npm pack --dry-run) — no file
added or removed, still zero runtime dependencies, Apache-2.0, and 244 tests in 15 files (243 passed
plus one symlink-walk test that skips itself where the host forbids creating symlinks).
🇷🇺 Коротко по-русски
Развернуть
ls-12больше не срабатывает на идиому line-clamp:display: -webkit-box+-webkit-line-clamp+
-webkit-box-orient: vertical— это не флексбокс 2009 года, и совет «перейди наdisplay: flex» ломает
обрезку, ради которой блок и написан.- Остаётся то, что clamp-у служить не может:
display: -webkit-flex,display: -webkit-inline-flex,
-webkit-box-pack|align|flex|ordinal-group|directionи-webkit-box-orient: horizontal; id правила,
гайд, уверенность и текст сообщения не менялись. - Цена названа прямо: страница, где голый
display: -webkit-boxдействительно используется как флексбокс,
теперь не репортится — это точность за счёт полноты, и граница записана в parity-ноте. - Bench-проект
src: 10 находок → 9,ls-121 → 0, ложная находкаPostCard.astro:110-112ушла;
остальные правила не сдвинулись. - В этом репозитории по-прежнему 85 находок, и это совпадение, а не «ничего не изменилось»: фикстура
dirtyперенацелена наdisplay: -webkit-flex, голый-webkit-boxв комментарии теста больше не
матчится, восемь новых написаний в тесте схлопнулись в одну находку;ls-12здесь по-прежнему 3, но в
других местах. .scanignoreв контракте и глоссарии теперь говорит, что паттерн может называть один файл (issue #10):
соседние страницы читаются, а явно названный припаркованный путь даёт 0 файлов — игнор сильнее явного
пути, и это осознанно.- Пакет: 256 файлов, 907 kB в упаковке, 2.7 MB распакованного, 244 теста в 15 файлах,
зависимостей по-прежнему ноль.
🇨🇳 中文摘要
展开
ls-12不再命中 line-clamp 写法:display: -webkit-box+-webkit-line-clamp+
-webkit-box-orient: vertical不是 2009 年的弹性盒,建议改成display: flex会破坏这段代码的本意——
截断。- 仍然命中的是无法用于 clamp 的前缀写法:
display: -webkit-flex、display: -webkit-inline-flex、
-webkit-box-pack|align|flex|ordinal-group|direction与-webkit-box-orient: horizontal;规则 id、
指南、置信度与文案均未改动。 - 代价已写明:确实把裸
display: -webkit-box当作弹性盒的页面现在不再报告——用召回率换精确度,边界记录在
parity 笔记中。 - bench 项目
src:10 条发现 → 9 条,ls-121 → 0,PostCard.astro:110-112的错误发现消失,
其它规则没有变化。 - 本仓库仍然是 85 条发现,但这是巧合,不是「什么都没变」:
dirty夹具改为display: -webkit-flex,
测试注释里的裸-webkit-box不再命中,新测试里的八种写法合并为一条发现;ls-12仍是 3 条,只是位置
不同。 .scanignore在契约与术语表中写明可以只停一个文件(issue #10):同目录其它页面照常读取,而显式点名
被停的文件返回 0 个文件——忽略优先于显式路径,这是有意为之。- 打包:256 个文件、907 kB、解包 2.7 MB,244 个测试 / 15 个文件,运行时依赖仍为零。
Install: dsh plugin --profile desktop add dsh-modern-web-guidance
Full diff: v0.8.0...v0.8.1
v0.8.0
0.8.0 — one ladder, one finding 🪜
A block of twenty legacy lines used to be twenty findings, because the scanner counted hits per line. It
now counts code sites: a run of one rule's hits on neighbouring lines is one finding, and every surface
prints the range it stands for. Same rules, same caps, same zero dependencies — and smaller numbers, which
are not the same thing as a cleaner project.
✨ Features
-
A finding is one place in the code. Hits of one rule in one file collapse into a single finding while
the next hit's line is at mostRUN_LINE_GAP = 2lines after the previous one — the next line, or one
blank line later. A wider gap, a different file or a different rule starts a new finding. -
Every finding carries
linesandoccurrences, always. A single hit islines: [154]with
occurrences: 1, so a consumer never needs a branch for the new fields, andoccurrencesis also
lines.length. -
The scan text prints a range instead of a line. A single hit still renders
path:line:columnbyte for
byte; a run renderspath:first-last, with(N hits)only when the run skips a line:- src/components/Header.astro:642:5 ls-09 (medium) - src/components/ui/PostCard.astro:110-112 (2 hits) ls-12 (high) - src/styles/report-template.css:306-313 ls-05 (medium)110-112alone would claim three hits where there are two, hence the count; a dense run needs none. -
The panel's chat row shows the same range through one helper:
path:110-112for a run and
path:2-4 (2)when it skips a line, falling back topath:linewhen an older host sends onlyline.
🧪 Behaviour changes
- Counts fall, and that is collapse, not repair. The bench project's
src(58 files) goes from 22
findings to 10, withls-05from 13 to 2: the eight-hit and five-hit ladders in
report-template.cssare now two findings. This repository goes from 97 to 85. 22 line references
existed before and 22 exist after — nothing was lost, it was grouped. The 97 was never a quality metric
(most of it is this repository's own rule table storing markers as data), and 85 is not "twelve problems
fixed": a total that drops after upgrading says the counting changed, not the code. maxFindingscounts findings, not hits. A 40-line ladder now costs one of the 200 slots instead of
forty, so the same caps admit far more code.SCAN_LIMITSwas not touched: nothing was widened, the unit
changed.- Ordering follows the site: path, then first line, then column, then rule id — a collapsed run sorts
by where it starts, like any other finding. guidescounts findings too, so the per-guide table stays consistent with the list above it.
🧩 Compatibility
- The two new fields are additive:
line,column,snippetandmessagekeep their meaning and now
describe the first hit of the site, so code that printedpath:line:columnkeeps printing something
true. Nothing to change to keep working; uselineswhen you want the range. - Rule messages were not rewritten to mention ranges — the range belongs to the finding, so a rule's text
stays the one sentence it always was. - The panel reads
linesandoccurrencesdefensively and falls back toline, so a client talking to an
older host renders exactly what it used to.
📦 Package
- 256 files, 906 kB packed, 2.7 MB unpacked (measured by
npm pack --dry-run) — no file
added or removed, still zero runtime dependencies, Apache-2.0, and 242 tests in 15 files (241
passed plus one symlink-walk test that skips itself where the host forbids creating symlinks).
🇷🇺 Коротко по-русски
Развернуть
- Находка теперь — одно место в коде, а не одна строка: серия попаданий одного правила на соседних
строках схлопывается в одну находку, и все поверхности печатают диапазон. - У каждой находки всегда есть
linesиoccurrences: у одиночного попаданияlines: [154],
occurrences: 1, так что потребителю не нужна ветка для нового поля. - Текст печатает
path:642:5для одиночного хита иpath:110-112для серии;(2 hits)появляется только
там, где серия пропускает строку, иначе110-112соврало бы про три хита вместо двух. - Числа падают: bench-проект
src— 22 находки → 10,ls-05— 13 → 2; этот репозиторий —
97 → 85. Это схлопывание, а не починка: 22 ссылки на строки были и остались 22, а меньшее число
после обновления говорит о смене подсчёта, а не об очистке кода. maxFindingsсчитает находки, а не хиты: лестница из 40 строк занимает один слот из 200.SCAN_LIMITS
не меняли — изменилась единица измерения.- Порядок — путь, первая строка, колонка, id правила; строка в чате показывает диапазон через тот же
хелпер и терпит старый хост, у которого есть толькоline.
🇨🇳 中文摘要
展开
- 一条发现现在是一处代码位置,不是一行:同一规则在相邻行上的连续命中会合并成一条发现,各处文本都改为
打印范围。 - 每条发现都带
lines与occurrences:单次命中即lines: [154]、occurrences: 1,调用方不必为
新字段写分支。 - 文本对单次命中仍打印
path:642:5,对连续命中打印path:110-112;只有跨过一行时才追加(2 hits),
否则110-112会把两次命中说成三次。 - 数字会下降:bench 项目的
src从 22 条变成 10 条,ls-05从 13 变成 2;本仓库从 97 变成
85。这是合并,不是修复:行号引用更新前是 22 处,更新后仍是 22 处;升级后数字变小说明计数方式
变了,不代表代码变干净。 maxFindings数的是发现而不是命中:40 行的连片命中只占 200 个名额中的一个;SCAN_LIMITS未作改动,
改的是计数单位。- 排序为路径、首行、列、规则 id;聊天里的那一行通过同一个辅助函数显示范围,并在老宿主只给
line时保持
原有渲染。
Install: dsh plugin --profile desktop add dsh-modern-web-guidance
Full diff: v0.7.3...v0.8.0
v0.7.3
0.7.3 — the panel stops quoting a scan without its caveats 🧾
0.7.2 taught the scan's own text to say what it skipped. The panel draws its summary from the same JSON,
and it was still printing a count with no word about the paths the walk never read: a live run showed
Просканировано 70 файлов, 97 замечаний and nothing else. Both caveats are now on screen, on all three
surfaces the panel owns. Same tool, same arguments, same caps, no new dependency.
✨ Features
- The button's answer says what it did not read. Under
Scanned 70 files, 97 findingsthe panel now
printsSkipped 280 (278 extension, 2 dot-directory), naming the top three reasons and adding+N more
inside the parentheses when a scan skipped for more — and nothing at all when nothing was skipped. - The last-scan line carries the same two facts, shorter.
Last scan: 2026-11-10 · 70 files · 97 findings · skipped 280 · truncated— the total and the flag only, never the breakdown. - The chat tool row adds the skip line under its table, for a JSON and a summary answer: the
summary format printsSkipped 280 path(s): 278 extension, 2 dot-directoryas prose and the row parses
that line back, so one reader serves both formats and no second implementation appears. - Truncation reuses the 0.7.0 string —
Truncated: the scan hit a cap, so this list is incomplete.—
because a second wording for the same fact is a second thing to keep in sync. There is no fifth key.
The counts differ between workspaces, and that is the point of them: a live run of this repository from
the GUI reported 280 skipped paths, a clean checkout reports 279, and neither is wrong — a scan counts
what is in the directory when it runs, so a .tmp scratch file or a harness service file is one more
skipped path like any other.
🧪 Behaviour changes
lastScangrows from four keys to six —{ at, files, findings, rules, skipped, truncated }.
skippedholds the top four reasons (count descending, then name),{}when nothing was skipped;
truncatedis true when the run stopped at any cap.versionstays1: the fields are additive.- The printed sum is the sum of the four reasons the state kept. A scan that skipped for five or more
reasons prints fewer than its ownskippedTotal, because the state document is a summary: keeping all
eight reason rows would make every state file carry a scan's whole skip table. That is the contract, not
a rounding bug — the fresh scan result, the button, the chat row and the tool's own text still carry every
reason and its own total. The long-form contract is in
docs/notes/legacy-scan-contract.md. - A stale
stats.jsonupgrades silently. A 0.7.2 file has neither field, so the first report renders
the shorter line — noskipped 0, no inventedtruncated— and the next completed scan rewrites the
document with six keys. No migration step, noversionbump, nothing to run by hand. - A refusal still leaves
lastScanalone, on the tool and on the command alike: an error is counted
as an error call, and the stored summary keeps describing the last scan that actually finished.
🧩 Compatibility
- Reason tokens are never translated, on any surface:
dot-directorystaysdot-directoryin EN, RU
and ZH, exactly as a rule id staysls-13. They are values a reader matches against a scan. - Still one hand-written client file, still no host method and no new dependency; the data was already in
the answer the panel received. Nothing to enable or configure. - No path and no snippet appears anywhere new: the three surfaces show counts and reason names only, and
the chat table remains the one place where files and lines appear.
📦 Package
- 256 files, 905 kB packed, 2.6 MB unpacked (measured by
npm pack --dry-run) — no file added or
removed, still zero runtime dependencies, Apache-2.0, and 234 tests in 15 files (233 passed plus
one symlink-walk test that skips itself where the host forbids creating symlinks).
🇷🇺 Коротко по-русски
Развернуть
- Панель больше не показывает числа скана без оговорок: под строкой итога появилось
Пропущено 280 (278 extension, 2 dot-directory)— до трёх причин, дальше+N ещё, а если пропусков не
было, строки нет вовсе. - Строка последнего скана тоже честная:
Последнее сканирование: 2026-11-10 · 70 файлов · 97 замечаний · пропущено 280 · обрезано— только сумма и флаг, без разбивки. - Строка в чате под таблицей показывает те же пропуски и для JSON, и для summary: текст
Skipped 280 path(s): …разбирается обратно тем же читателем, что и таблица. - В
stats.jsonуlastScanтеперь шесть ключей (at, files, findings, rules, skipped, truncated),
версия состояния остаётся1:skipped— верхние четыре причины,truncated— правда, если скан
упёрся в любой кап. - Поэтому печатаемая сумма — это сумма сохранённых четырёх причин: при пяти и более причинах она меньше
skippedTotalсамого скана. Это контракт, а не баг: свежий результат скана по-прежнему знает все причины. - Старый
stats.jsonобновляется молча: без полей он читается как{}/false, строка просто короче, а
следующий завершённый скан перезаписывает файл шестью ключами. Токены причин не переводятся нигде — какls-13.
🇨🇳 中文摘要
展开
- 面板不再只报数字不报前提:结果下方新增
已跳过 280 个(278 extension, 2 dot-directory)—— 最多列三个
原因,其余用另有 N 项,没有跳过时整行不出现。 - 最近一次扫描那一行同样诚实:
最近一次扫描:2026-11-10 · 70 个文件 · 97 条发现 · 跳过 280 · 已截断——
只给总数和截断标记,不给分解。 - 聊天里的工具卡片在表格下方补上同一行,JSON 与 summary 两种答案都有:summary 文本中的
Skipped 280 path(s): …会被同一个读取器反解回来。 stats.json里的lastScan从四个键变成六个(at, files, findings, rules, skipped, truncated),状态
版本仍是1:skipped只保留前四个原因,truncated表示扫描是否触到任一上限。- 因此打印出来的总数是「保存下来的四个原因」之和:原因达到五个以上时,它会小于扫描自身的
skippedTotal。
这是契约而不是缺陷 —— 最新一次扫描的结果仍然知道全部原因。 - 旧的
stats.json会静默升级:缺少字段时按{}/false处理,那一行只是更短;下一次完成的扫描会用六个键
重写文件。原因名在任何地方都不翻译,就像规则 id。
Install: dsh plugin --profile desktop add dsh-modern-web-guidance
Full diff: v0.7.2...v0.7.3
v0.7.2
0.7.2 — the scanner learns which directories are none of its business 🚧
Dot-directories are no longer read, and a workspace-root .scanignore says which directories and files to
leave out. The scan text now itemises what it skipped and why — Skipped 279 path(s): 277 extension, 2 dot-directory — and a malformed or unreadable .scanignore is reported instead of failing the scan. The
tool, its arguments, its caps and its refusal codes are unchanged.
🧪 Behaviour changes
- Dot-directories are never walked. A directory whose name starts with a dot is skipped with reason
dot-directory— the usual noise names (.git,.turbo,.next,.cache) were already in the
scanner's own skip list, so this now covers.githuband.vscodetoo. The check runs whether the walk
meets the directory or a path names it explicitly, and it costs one skip entry per directory, not one
per file inside, so build output and scraped dumps stop inflating a report. .scanignoreat the workspace root. One pattern per line,#for comments, one-segment patterns
matching a name at any depth (report/), patterns containing/anchored at the root (src/app.js),
*inside one segment, a trailing/for directories only. No negation, no**, no?. A line
outside that subset is reported, not fatal:Ignored 1 line in .scanignore (line 7: bad pattern), and an
unreadable file producesCould not read .scanignore; the scan ran without ignore patterns.- Skips are itemised per reason. Right after
Scanned N file(s) …the summary now says
Skipped 279 path(s): 277 extension, 2 dot-directory— counted over every skip, not the 100 that
theskippedsample can carry, and ordered by count so the same scan always renders the same line. - This repository, measured:
.returns 70 files and 97 findings,truncated: false, skips
{extension: 277, dot-directory: 2};libalone returns 6 files and 18 findings. The 97 is still
mostly the rule table — 61 of them live inlib/scan-rules.jsandtest/scan-rules.test.mjs, where the
markers are data — so it measures what the repo contains, not how old its code is.
🛠 Hardening
- A scan can still be truncated, and that is the honest answer. A real Astro project with 58 source
files scanssrcas 58 files, 22 findings before and after this release; scanned as.it hits the
200-file cap with 41 findings and staystruncated: true, because outside its dot-directories it
holds 241 candidate files (tmp-deploy82,report66,src58, …). Six lines of.scanignore
naming those artifact directories give 77 files, 22 findings, all of them insrcand no truncation. - The fix for truncation is
.scanignore, not a bigger cap. The cap stays at 200 files precisely so a
scan of a wrongly-shaped workspace cannot turn into a tarball of every asset the repo ships.
🧩 Compatibility
- Same tool, same arguments, same caps, same refusal codes; the only text that changed is the new
Skipped … path(s)line and the two.scanignorediagnostics.scanned.skippedByReasonand
scanned.ignoreProblemsare additive fields inside the same result; state files and counters are
untouched, and a.scanignorediagnostic is never counted as a file skip. .scanignoreis not a security boundary. It is a convenience for a project that knows what it means
by "here", and the scanner is already confined to the workspace byrealpathcontainment — an ignore
file can only make it read less than it was allowed to.
📦 Package
- 256 files, 901 kB packed, 2.6 MB unpacked (measured by
npm pack --dry-run) — no file added or
removed, still zero runtime dependencies, Apache-2.0, sigstore provenance from the release workflow.
🇷🇺 Коротко по-русски
Развернуть
- Сканер больше не заходит в каталоги, чьё имя начинается с точки: такой каталог получает причину
dot-directory— одной записью на каталог, а не на каждый файл внутри, и это работает и когда путь
назван явно. - В корне рабочей папки читается
.scanignore: одна шаблонная строка на строку,#— комментарий,
шаблон из одного сегмента совпадает с именем на любой глубине (report/), шаблон со/привязан к
корню,*не переходит через/, слэш на конце — только каталоги. - Ошибка в
.scanignoreне роняет скан: текст говоритIgnored 1 line in .scanignore (line 7: bad pattern),
а нечитаемый файл —Could not read .scanignore; the scan ran without ignore patterns. - Пропуски теперь перечислены по причинам:
Skipped 279 path(s): 277 extension, 2 dot-directory, и
считаются по всем пропускам, а не по 100, которые помещаются в выборку. - Этот репозиторий:
.даёт 70 файлов и 97 находок,{extension: 277, dot-directory: 2},lib— 6 и 18. - Скан может остаться обрезанным: у реального Astro-проекта 241 подходящий файл при капе в 200, и лечится это
.scanignore(77 файлов, 22 находки), а не увеличением капа; сам.scanignore— не граница безопасности.
🇨🇳 中文摘要
展开
- 扫描器不再进入以点开头的目录:这类目录以
dot-directory为原因记为一条跳过(一个目录一条,而不是
里面的每个文件一条),即使路径被显式点名也一样。 - 工作区根目录的
.scanignore会被读取:一行一个模式,#是注释,单段模式在任意深度匹配同名项(report/),
含/的模式锚定在根目录,*不跨/,结尾/只匹配目录。 .scanignore里出错不会中断扫描:文本会说Ignored 1 line in .scanignore (line 7: bad pattern);
文件读不到则说Could not read .scanignore; the scan ran without ignore patterns.- 跳过现在按原因列出:
Skipped 279 path(s): 277 extension, 2 dot-directory,统计的是全部跳过,
而不是只能放进样本的 100 条。 - 本仓库实测:
.得到 70 个文件、97 条发现,{extension: 277, dot-directory: 2};lib得到 6 个文件、18 条发现。 - 扫描仍可能被截断:真实的 Astro 项目在点目录外有 241 个候选文件,超过 200 的上限;正确的做法是写
.scanignore
(77 个文件、22 条发现),而不是调大上限。它也不是安全边界 —— 扫描器本来就被限制在工作区内。
Install: dsh plugin --profile desktop add dsh-modern-web-guidance
Full diff: v0.7.1...v0.7.2
v0.7.1
0.7.1 — the scanner stops reading documentation 📖
A .md or .mdx file is no longer scanned: it is reported as skipped, with reason extension, and the
count in skippedTotal includes it. Code is unaffected — the allowlist is 18 source extensions, the caps,
the refusals and the rule table are exactly as they were. The change exists because prose quotes
anti-patterns: a README that says "use dialog.showModal() instead of $.fn.modal" is documentation, not
legacy code, and the panel's scan button was reporting 73 findings on a project whose only defect was its
documentation.
🧪 Behaviour changes
- Prose is skipped, not read.
.mdand.mdxleft the allowlist, so a Markdown file met while walking
is skipped with reasonextensionand counted inskippedTotal; a.mdpath named explicitly gets
the same skip instead of an error. A project whose only defect is its documentation now scans clean. - A scan of this repository, before and after: 200 files with 73 findings, 71 of them in Markdown
andtruncated: true→ 68 files with 82 findings, 0 in Markdown,truncated: false. - That rise is not a regression, and the note will not call it one. 61 of the 82 are the rule table
itself —lib/scan-rules.js(18) andtest/scan-rules.test.mjs(43) store every marker as data — 9 are
deliberate fixtures, and the rest sit in tests and one audit repro. The old 200-file cap was consumed by
the bundled corpus, solib/andtest/were never reached; the scan now covers the code. - Counters follow: a Markdown-only project records a
cleanscan instead of afindingsone, with
findings: 0. No state-file field changed.
🧩 Compatibility
- Same tool, same arguments, same caps (16 paths, 200 files, 512 kB per file, 8 MB per call, 200 findings),
same refusal codes, same text. Only the allowlist shrank, so a path that used to produce findings can now
come back empty — that is the intended answer, not an error. - Engines unchanged: DSH
>=0.2.0-rc.1 <0.3.0-0, Node>=22.19.0. The panel's scan button needs no change,
and/guidance-scanstill refuses to run without an explicit path.
📦 Package
- 256 files, 895 kB packed, 2.6 MB unpacked (measured by
npm pack --dry-run) — no file added or
removed, still zero runtime dependencies, Apache-2.0, sigstore provenance from the release workflow.
🇷🇺 Коротко по-русски
Развернуть
.mdи.mdxбольше не сканируются: файл попадает в пропущенные с причинойextensionи считается в
skippedTotal, а явно указанный.mdдаёт тот же пропуск, а не ошибку.- Причина: проза цитирует антипаттерны. README, который говорит «используйте
dialog.showModal()вместо
$.fn.modal», — документация, а не legacy-код; кнопка панели ставила 73 замечания проекту, у которого
дефект был только в документации. - Скан этого репозитория: было 200 файлов и 73 находки (71 в markdown,
truncated: true), стало 68 файлов
и 82 находки, в markdown 0,truncated: false. - Рост числа — не регресс: 61 из 82 — сама таблица правил (
lib/scan-rules.jsи её тест), 9 — фикстуры.
Прежний лимит в 200 файлов съедал корпус, и доlib/иtest/обход не доходил. - Инструмент, аргументы, лимиты, отказы и текст сканера не изменились — сузился только список расширений.
Счётчики: скан проекта, где есть только markdown, теперь записывается какcleanсfindings: 0. - Зависимостей по-прежнему ноль, файлов в пакете 256, движки те же.
🇨🇳 中文摘要
展开
.md与.mdx不再被扫描:遍历时遇到就以原因extension记入跳过并计入skippedTotal;显式指定的.md
路径也是同样的跳过,而不是报错。- 原因是散文会引用反模式。README 里写“用
dialog.showModal()代替$.fn.modal”属于文档,不是过时代码;
此前面板的扫描按钮会给一个只有文档有问题的项目报出 73 条发现。 - 本仓库实测:之前 200 个文件、73 条发现(71 条在 markdown 里,
truncated: true);现在 68 个文件、82 条发现,
markdown 中 0 条,truncated: false。 - 数字上升不是退化:82 条里有 61 条来自规则表本身(
lib/scan-rules.js及其测试),9 条是刻意的测试夹具。
旧的 200 文件上限被语料吃掉了,遍历根本到不了lib/和test/。 - 工具、参数、上限、拒绝规则和输出文本都没有变,缩小的只是扩展名白名单。计数上,只有文档的项目现在记为
clean、findings: 0。 - 依旧零运行时依赖,包内 256 个文件,引擎版本不变。
Install: dsh plugin --profile desktop add dsh-modern-web-guidance
Full diff: v0.7.0...v0.7.1