dsh-disk-cleaner 1.6.0 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  <!-- badges -->
6
6
  | | |
7
7
  | --- | --- |
8
- | **version** | `1.6.0` |
8
+ | **version** | `1.6.2` |
9
9
  | **DSH** | `0.2.1-rc.5`(开发与实测版本;用到 `plugins.bundle.config`、`settings.section`) |
10
10
  | **license** | [MIT](LICENSE) |
11
11
  | **Node** | `>= 22`(仅运行冒烟测试需要) |
@@ -76,7 +76,7 @@ Every path is built from this table — a caller selects an **id**, never a path
76
76
  | `rewind-snapshots` | `session-*` folders under `%USERPROFILE%\.dsh\rewind-snapshots` | aged, 30 days | caution | a recent session stays restorable |
77
77
  | `rotated-logs` | `*.old.log` and `*.log.N` files in the `logs` folders of the agent apps | aged, 3 days | safe | nothing reads a rotated file back |
78
78
  | `qoder-old-versions` | version folders under `%LOCALAPPDATA%\Programs\Qoder\.qoder-versions` other than the one the launcher names | purge | caution | the launcher's `state.ini` names the build it runs; an unreadable one means nothing is removed |
79
- | `orphan-app-data` | `%LOCALAPPDATA%\Tencent\QQGuild` - left by an app with no shortcut, no uninstall entry and nothing written for over two years | purge | caution | all four signals must agree before the folder is offered |
79
+ | `orphan-app-data` | `%LOCALAPPDATA%\Tencent\QQGuild` - the QQGuild data folder | purge | caution | the whole folder is offered for deletion once no file in it has been written for 30 days |
80
80
  | `app-data-caches` | Chromium caches under `%LOCALAPPDATA%` for Electron, DingTalk, JianyingPro and the WeChat devtools | aged, 7 days | caution | an app that still runs rewrites its cache constantly, so only a week-old tree qualifies |
81
81
  | `office-update-cache` | `%LOCALAPPDATA%\Microsoft\Office\SolutionPackages` - update packages Office already applied | purge | caution | Office stages them before applying; nothing reads them back afterwards |
82
82
 
@@ -159,23 +159,31 @@ These are enforced in `lib/catalog.js` and must not be relaxed:
159
159
  - `editor-builds` drops a build only when `bin\code.cmd` exists and names a different build that is also on disk; `old_x` goes only while `x` is still beside it.
160
160
  - `database-backups` matches `.sqlite*.bak` by name only; the live `zotero.sqlite` is never a candidate.
161
161
  - `qoder-old-versions` reads the launcher's `state.ini` for the build it runs; a missing or unparsable file, or a named build that is not on disk, removes nothing.
162
- - `orphan-app-data` needs four independent signals to agree: no Start-menu shortcut, no uninstall entry, no folder under either `Program Files`, and no file written for two years. Any one missing and the folder is not offered.
162
+ - `orphan-app-data` is offered when the QQGuild folder exists and no file in it has been written in the last 30 days; the whole folder is then a full-delete target. It does not check shortcuts, uninstall entries, or Program Files.
163
163
  - The directory Windows refuses to let an unelevated process write (`SoftwareDistribution\Download`) is labelled `elevation` and excluded from "reclaimable now". `C:\Windows\Temp` is **not** one of them on a default ACL: it grants `BUILTIN\Users` create and delete, so only EBUSY on a live file stops a delete there, which the age limit covers.
164
164
 
165
165
  ### Install
166
166
 
167
- Published on npm as [`dsh-disk-cleaner`](https://www.npmjs.com/package/dsh-disk-cleaner):
167
+ A catalog entry has been submitted to the curated [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) list, which powers **dshmarket → Discover** ([PR #12970](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/12970), pending review) — once it merges, search **dsh-disk-cleaner** there and install it in one click. Until then, use a direct install below.
168
+
169
+ Published on npm as [`dsh-disk-cleaner`](https://www.npmjs.com/package/dsh-disk-cleaner) — latest `1.6.2`:
168
170
 
169
171
  ```bash
170
172
  npm i dsh-disk-cleaner
171
173
  ```
172
174
 
175
+ A prebuilt tarball is attached to the [v1.6.2 Release](https://github.com/stallenlu/dsh-disk-cleaner/releases/tag/v1.6.2); installing from it skips DSH's build-approval step:
176
+
177
+ ```bash
178
+ dsh plugin --profile <profile> add https://github.com/stallenlu/dsh-disk-cleaner/releases/download/v1.6.2/dsh-disk-cleaner-1.6.2.tgz
179
+ ```
180
+
173
181
  Then enable it in the profile — in `$DSH_PROFILE_DIR/package.json`:
174
182
 
175
183
  ```json
176
184
  {
177
185
  "dependencies": {
178
- "dsh-disk-cleaner": "^1.0.0"
186
+ "dsh-disk-cleaner": "^1.6.2"
179
187
  },
180
188
  "dsh": {
181
189
  "profile": {
@@ -196,7 +204,7 @@ The bundle's own `cordis.patch.yml` inserts the plugin row; no profile patch edi
196
204
  ### Development
197
205
 
198
206
  ```bash
199
- node smoke.mjs # 82 assertions, zero dependencies
207
+ node smoke.mjs # 85 assertions, zero dependencies
200
208
  node --check lib/client.js # and lib/index.js, lib/catalog.js, lib/audit.js, lib/plan.js
201
209
  ```
202
210
 
@@ -225,6 +233,8 @@ What the smoke test deliberately does **not** cover: anything that walks a real
225
233
  | A dry run reports what a real run frees | The 30-day rewind window reports 263 KB not 283 MB, and scan matches the dry run |
226
234
  | Grouping and folding | The real `groupRows` runs out of the bundle against a mixed list: fixed order, per-section totals, an unknown group lands in "other", an empty section is the folded one. A click on a section head folds that section and only that one, and a folded section's rows drop out of bulk select |
227
235
  | Visual layout | Checked by hand in the running Web UI. No browser automation here, so this one is a human check, not a test. |
236
+ | Agent tool returns a string matching its output schema | `node smoke.mjs` regression assertion (the 1.6.1 fix) |
237
+ | A real agent-tool delete invalidates the assessment cache | `node smoke.mjs` regression assertion (the 1.6.1 fix) |
228
238
 
229
239
  ### Implementation notes
230
240
 
@@ -248,20 +258,26 @@ What the smoke test deliberately does **not** cover: anything that walks a real
248
258
  ```
249
259
  ├─ package.json dsh.bundle.patch + dsh.client { platform: "web" }
250
260
  ├─ cordis.patch.yml - insert: id: dsh-disk-cleaner
251
- ├─ lib/index.js (411) Host: HTTP route + disk_cleaner tool, same run() for both; reveal
261
+ ├─ lib/index.js (431) Host: HTTP route + disk_cleaner tool, same run() for both; reveal
252
262
  ├─ lib/catalog.js (1192) The 32 targets, measurePath / purgePath / resolveTargets / drives
253
263
  ├─ lib/audit.js (365) Read-only assessment, 10-minute cache, areaPaths, exposes no deletion
254
264
  ├─ lib/plan.js (150) Pure: measurements → ordered steps
255
- ├─ lib/client.js (1492) Client half: slots, panel, preferences, zh/en dictionaries, CSS
265
+ ├─ lib/client.js (1493) Client half: slots, panel, preferences, zh/en dictionaries, CSS
256
266
  ├─ locale/{en,zh}.json Display metadata (meta.title / meta.description)
257
267
  ├─ icon.svg Plugin icon
258
268
  ├─ README.md This file
259
269
  ├─ LICENSE MIT
260
- └─ smoke.mjs 82 assertions, zero dependencies
270
+ └─ smoke.mjs 85 assertions, zero dependencies
261
271
  ```
262
272
 
263
273
  Client-half slots: `sidebar.panellist` (order 50, disposed in settings-only mode), `main` (keyed by the package name), `settings.section` (order 30, disposed in sidebar-only mode), `plugins.bundle.config` (keyed by the package name).
264
274
 
275
+ ### What's new
276
+
277
+ - **1.6.2** — Documentation only: the README, the in-source comment, the panel's zh/en explanations and the tool's note legend now describe `orphan-app-data` as it actually behaves (the QQGuild folder is offered for full deletion once no file in it has been written for 30 days), instead of claiming four independent signals and a two-year idle period. No behaviour change; the refreshed README ships in the npm package and the [v1.6.2 Release](https://github.com/stallenlu/dsh-disk-cleaner/releases/tag/v1.6.2) tarball.
278
+ - **1.6.1** — Fixed the `disk_cleaner` agent tool: `output.schema` now declares `type: 'string'`, matching the text `execute()` returns (previously every call was rejected by the host's output validator with `tool "disk_cleaner" returned invalid output: "value" must be an object`). A real delete through the agent tool now invalidates the assessment cache, the same way the panel's HTTP route does. Two regression assertions added; a prebuilt tarball ships on the [v1.6.1 Release](https://github.com/stallenlu/dsh-disk-cleaner/releases/tag/v1.6.1).
279
+ - **1.6.0** — The 32 targets are grouped under seven foldable headings.
280
+
265
281
  ---
266
282
 
267
283
  <a id="中文"></a>
@@ -329,7 +345,7 @@ Client-half slots: `sidebar.panellist` (order 50, disposed in settings-only mode
329
345
  | `rewind-snapshots` | `%USERPROFILE%\.dsh\rewind-snapshots` 下的 `session-*` 目录 | 保留期 30 天 | 注意 | 最近的会话仍然可以回滚 |
330
346
  | `rotated-logs` | 各 agent 应用 `logs` 目录下的 `*.old.log` 与 `*.log.N` 文件 | 保留期 3 天 | 安全 | 轮转掉的日志不会被读回 |
331
347
  | `qoder-old-versions` | `%LOCALAPPDATA%\Programs\Qoder\.qoder-versions` 下不是启动器所指的那个版本目录 | 全清 | 注意 | 启动器 `state.ini` 写明它跑哪个版本;读不到就一个都不删 |
332
- | `orphan-app-data` | `%LOCALAPPDATA%\Tencent\QQGuild` —— 没有快捷方式、没有卸载项、两年多没写过文件的程序留下的数据 | 全清 | 注意 | 四条证据必须同时成立才会被列为可删 |
348
+ | `orphan-app-data` | `%LOCALAPPDATA%\Tencent\QQGuild` —— QQ 频道数据目录 | 全清 | 注意 | 目录内 30 天没有任何文件写入时,整个目录会被列为全清目标 |
333
349
  | `app-data-caches` | `%LOCALAPPDATA%` 下 Electron、钉钉、剪映、微信开发者工具的 Chromium 缓存 | 保留期 7 天 | 注意 | 还在跑的程序会不停改写缓存,所以只有一周没动过的才算 |
334
350
  | `office-update-cache` | `%LOCALAPPDATA%\Microsoft\Office\SolutionPackages` —— Office 已应用的更新暂存包 | 全清 | 注意 | Office 先暂存再应用,应用后没有东西会读回 |
335
351
 
@@ -411,23 +427,31 @@ Client-half slots: `sidebar.panellist` (order 50, disposed in settings-only mode
411
427
  - `editor-builds` 只有在 `bin\code.cmd` 存在且指向另一份确实在磁盘上的构建时才删;`old_x` 也只有在 `x` 仍在旁边时才删。
412
428
  - `database-backups` 只按名字匹配 `.sqlite*.bak`,正在用的 `zotero.sqlite` 永不成为候选。
413
429
  - `qoder-old-versions` 从启动器的 `state.ini` 读出它实际运行哪个版本;文件缺失、解析不出、或它指的版本不在磁盘上,一律一个都不删。
414
- - `orphan-app-data` 要求四条独立证据同时成立:开始菜单没有快捷方式、注册表没有卸载项、两个 `Program Files` 下都没有目录、两年内没有任何文件被写过。缺一条就不会列为可删。
430
+ - `orphan-app-data` 的判据只有一条:QQGuild 目录存在,且目录内 30 天内没有任何文件被写入过,此时整个目录被列为全清目标。它不检查快捷方式、卸载项或 Program Files。
415
431
  - 那个拒绝普通权限写入的 Windows 目录标为 `elevation`,并排除在「现在能回收」之外。
416
432
 
417
433
  ### 安装
418
434
 
419
- 已发布到 npm:[`dsh-disk-cleaner`](https://www.npmjs.com/package/dsh-disk-cleaner)。
435
+ 已向精选目录 [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) 提交收录,它是 **dshmarket → 发现** 的数据源([PR #12970](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/12970),审核中)——合并后即可在 dshmarket 里搜索 **dsh-disk-cleaner** 一键安装。在那之前,用下面任一种直接安装方式。
436
+
437
+ 已发布到 npm:[`dsh-disk-cleaner`](https://www.npmjs.com/package/dsh-disk-cleaner),最新版 `1.6.2`。
420
438
 
421
439
  ```bash
422
440
  npm i dsh-disk-cleaner
423
441
  ```
424
442
 
443
+ [v1.6.2 Release](https://github.com/stallenlu/dsh-disk-cleaner/releases/tag/v1.6.2) 还附带一个预构建 tarball,从它安装可跳过 DSH 的构建授权步骤:
444
+
445
+ ```bash
446
+ dsh plugin --profile <profile> add https://github.com/stallenlu/dsh-disk-cleaner/releases/download/v1.6.2/dsh-disk-cleaner-1.6.2.tgz
447
+ ```
448
+
425
449
  然后在 profile 里启用 —— 在 `$DSH_PROFILE_DIR/package.json`:
426
450
 
427
451
  ```json
428
452
  {
429
453
  "dependencies": {
430
- "dsh-disk-cleaner": "^1.0.0"
454
+ "dsh-disk-cleaner": "^1.6.2"
431
455
  },
432
456
  "dsh": {
433
457
  "profile": {
@@ -445,14 +469,10 @@ git clone https://github.com/stallenlu/dsh-disk-cleaner
445
469
 
446
470
  bundle 自带的 `cordis.patch.yml` 会插入插件行,无需改 profile 的 patch。`link:` 依赖是符号链接,所以改 **client** 刷新页面即生效,改 **host**(`lib/index.js`)需要重新启用该 bundle 或重启 DSH。从 npm 装则是相反的一面:要升级就改版本号。
447
471
 
448
- ```bash
449
- git clone https://github.com/stallenlu/dsh-disk-cleaner
450
- ```
451
-
452
472
  ### 开发
453
473
 
454
474
  ```bash
455
- node smoke.mjs # 82 条断言,零依赖
475
+ node smoke.mjs # 85 条断言,零依赖
456
476
  node --check lib/client.js # 以及 lib/index.js、lib/catalog.js、lib/audit.js、lib/plan.js
457
477
  ```
458
478
 
@@ -481,6 +501,8 @@ node --check lib/client.js # 以及 lib/index.js、lib/catalog.js、li
481
501
  | 预演报告的字节数等于真删能释放的字节数 | 30 天的回滚窗口报 263 KB 而不是 283 MB,且扫描结果与预演一致 |
482
502
  | 分类与折叠 | 从 bundle 里取出真正的 `groupRows` 跑一组混合数据:顺序固定、按类合计、未知类别落到「其他」、空的类别是收起来的那个。点某一类的标题只折叠这一类,折叠里的行不参与批量选择 |
483
503
  | 视觉排版 | 在运行中的 Web UI 里手工确认过。本环境没有浏览器自动化,所以这条是人眼检查,不是测试。 |
504
+ | agent 工具返回字符串且与 output.schema 一致 | `node smoke.mjs` 回归断言(1.6.1 修复) |
505
+ | agent 工具真删后评估缓存失效 | `node smoke.mjs` 回归断言(1.6.1 修复) |
484
506
 
485
507
  ### 实现要点
486
508
 
@@ -504,20 +526,26 @@ node --check lib/client.js # 以及 lib/index.js、lib/catalog.js、li
504
526
  ```
505
527
  ├─ package.json dsh.bundle.patch + dsh.client { platform: "web" }
506
528
  ├─ cordis.patch.yml - insert: id: dsh-disk-cleaner
507
- ├─ lib/index.js (411) Host:HTTP 路由 + disk_cleaner 工具,共用同一个 run(),含 reveal
529
+ ├─ lib/index.js (431) Host:HTTP 路由 + disk_cleaner 工具,共用同一个 run(),含 reveal
508
530
  ├─ lib/catalog.js (1192) 32 个目标,measurePath / purgePath / resolveTargets / drives
509
531
  ├─ lib/audit.js (365) 只读评估,10 分钟缓存,带 areaPaths,不暴露任何删除
510
532
  ├─ lib/plan.js (150) 纯函数:测量结果 → 有序步骤
511
- ├─ lib/client.js (1492) Client 半面:slot、面板、偏好、zh/en 字典、CSS
533
+ ├─ lib/client.js (1493) Client 半面:slot、面板、偏好、zh/en 字典、CSS
512
534
  ├─ locale/{en,zh}.json 展示元数据(meta.title / meta.description)
513
535
  ├─ icon.svg 插件图标
514
536
  ├─ README.md 本文件
515
537
  ├─ LICENSE MIT
516
- └─ smoke.mjs 82 条断言,零依赖
538
+ └─ smoke.mjs 85 条断言,零依赖
517
539
  ```
518
540
 
519
541
  Client 半面注册的 slot:`sidebar.panellist`(order 50,仅设置页模式下拆除)、`main`(以包名为 key)、`settings.section`(order 30,仅侧边栏模式下拆除)、`plugins.bundle.config`(以包名为 key)。
520
542
 
543
+ ### 更新记录
544
+
545
+ - **1.6.2** — 仅文档修正:README、源码注释、面板中英文说明和工具里的注意码释义,现在都按真实行为描述 `orphan-app-data`(QQGuild 目录内 30 天没有文件写入时会被整目录列为可全清),不再声称「四条独立证据 / 两年未写」。行为不变;更新后的 README 也随 npm 包与 [v1.6.2 Release](https://github.com/stallenlu/dsh-disk-cleaner/releases/tag/v1.6.2) 的 tarball 一起发布。
546
+ - **1.6.1** — 修复了 `disk_cleaner` agent 工具:`output.schema` 现在声明 `type: 'string'`,与 `execute()` 返回的文本一致(此前每次调用都被宿主的输出校验器拒绝,报 `tool "disk_cleaner" returned invalid output: "value" must be an object`)。经由 agent 工具真删后也会让评估缓存失效,与面板的 HTTP 路径一致。新增两条回归断言;[v1.6.1 Release](https://github.com/stallenlu/dsh-disk-cleaner/releases/tag/v1.6.1) 附带预构建 tarball。
547
+ - **1.6.0** — 32 个目标按七个可折叠分组归类。
548
+
521
549
  ## License
522
550
 
523
551
  [MIT](LICENSE)
package/lib/catalog.js CHANGED
@@ -872,9 +872,9 @@ const CATALOG = [
872
872
  roots: ['LOCALAPPDATA'],
873
873
  paths: () => {
874
874
  const guild = path.join(env('LOCALAPPDATA') ?? '', 'Tencent', 'QQGuild');
875
- // Measured: no Start-menu shortcut, no uninstall entry, no Program Files folder and no
876
- // file touched for over two years. The folder also carries its own `Uninstall QQGuild.exe`,
877
- // which only a real install does. Every one of those signals has to agree before it goes.
875
+ // The QQGuild data folder is offered for full deletion when it exists and no file
876
+ // inside it has been written in the last 30 days. Nothing else is checked - no
877
+ // shortcut, uninstall entry or Program Files probe. Keep the docs in step with this.
878
878
  const recent = newestMtime(guild) > Date.now() - 30 * DAY;
879
879
  return isDir(guild) && !recent ? [guild] : [];
880
880
  },
package/lib/client.js CHANGED
@@ -41,7 +41,7 @@ window.__ModuleLoader__.load({
41
41
  'why_pending-update': '只清 {n} 天前的下载,避免删掉还没装的更新',
42
42
  why_rollback: '每个插件/程序只留版本号最高的一份,其余是回滚副本',
43
43
  why_superseded: '版本号不高于已装版本的更新包,装着的那份已经生效',
44
- why_orphan: '写这个目录的程序已经不在这台机器上了:没有快捷方式、没有卸载项,两年多没写过文件',
44
+ why_orphan: 'QQ 频道数据目录:目录内 30 天没有任何文件写入时会被整目录清除',
45
45
  why_history: '每个会话的整文件备份,删了不影响会话本身',
46
46
  why_duplicate: '内容完全相同的重复副本,保留最新那份',
47
47
  'why_database-backup': '升级前的数据库副本,正在用的数据库还在旁边',
@@ -213,7 +213,7 @@ window.__ModuleLoader__.load({
213
213
  'why_pending-update': 'only downloads older than {n} day(s), so a pending update survives',
214
214
  why_rollback: 'only the highest version of each family stays; the rest are rollback copies',
215
215
  why_superseded: 'the update package version is not above what is installed, so it is already applied',
216
- why_orphan: 'the app that wrote this folder is gone: no shortcut, no uninstall entry, nothing written for over two years',
216
+ why_orphan: 'the QQGuild data folder, deleted in full when no file in it has been written for 30 days',
217
217
  why_history: 'a whole-file backup per session; removing it does not touch the session',
218
218
  why_duplicate: 'an identical copy; the newest one is kept',
219
219
  'why_database-backup': 'a copy made before an upgrade; the live database is beside it',
package/lib/index.js CHANGED
@@ -210,7 +210,7 @@ const NOTE_MEANING = {
210
210
  history: 'a backup snapshot of file edits that nothing reads back; the live state sits beside it',
211
211
  duplicate: 'a byte-identical copy of a directory that is still present',
212
212
  'database-backup': 'a copy of a database file taken before an upgrade; the live database is beside it',
213
- orphan: 'the app that wrote this folder is no longer installed: no shortcut, no uninstall entry, and nothing written for over two years',
213
+ orphan: 'the QQGuild data folder is deleted in full when no file in it has been written for 30 days',
214
214
  };
215
215
 
216
216
  const formatBytes = (bytes) => {
@@ -349,7 +349,7 @@ export function apply(ctx) {
349
349
  additionalProperties: false,
350
350
  },
351
351
  output: {
352
- schema: { type: 'object', additionalProperties: true },
352
+ schema: { type: 'string' },
353
353
  render: (args, value) => [{ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value, null, 2) }],
354
354
  },
355
355
  timeoutMs: 300_000,
@@ -413,6 +413,7 @@ export function apply(ctx) {
413
413
  if (ids === undefined || ids.length === 0) throw new Error('clean needs at least one target id');
414
414
  const dryRun = args.confirm !== true;
415
415
  const results = await clean(ids, { dryRun, drive: readDrive(args.drive) });
416
+ if (!dryRun) invalidateAudit();
416
417
  const freed = results.reduce((sum, item) => sum + item.bytes, 0);
417
418
  const files = results.reduce((sum, item) => sum + item.removed, 0);
418
419
  const failed = results.reduce((sum, item) => sum + item.failed, 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-disk-cleaner",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "type": "module",
5
5
  "description": "Disk Cleaner for DSH: scan known-safe cache locations, see what they occupy, and reclaim the space from a panel or from the agent.",
6
6
  "keywords": [
package/smoke.mjs CHANGED
@@ -260,6 +260,11 @@ check(
260
260
  check('tool needs no id for overview', JSON.stringify(definition.parameters.required) === JSON.stringify(['action']));
261
261
  check('tool description warns audit is read-only', /read-only: it never removes anything/.test(definition.description));
262
262
  check('every effect is labelled', applied.every((label) => typeof label === 'string' && label.startsWith('dsh-disk-cleaner:')));
263
+ check('tool output schema matches its string reply', definition.output.schema.type === 'string' && typeof (await definition.execute({ action: 'overview' })) === 'string');
264
+ check('the read-only agent clean keeps the audit cache in sync', /if \(!dryRun\) invalidateAudit\(\);[\s\S]*const freed = results\.reduce/.test(index));
265
+ check('orphan target descriptions state the real 30-day rule, not four signals / two years',
266
+ /30 days/.test(catalog) && /30 days/.test(index) && /QQGuild data folder/.test(source) &&
267
+ !/four independent signals|over two years|no Start-menu shortcut|two years/.test(catalog + index + source));
263
268
 
264
269
  const ids = [...catalog.matchAll(/^\s{4}id: '([a-z-]+)',$/gm)].map((m) => m[1]);
265
270
  check('catalog has the documented 32 targets', ids.length === 32, `got ${ids.length}`);