@twinsearth/w2m-dsh-plugin 0.46.0 → 0.48.0

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/CHANGELOG.md CHANGED
@@ -4,6 +4,96 @@ All notable changes to this project are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
5
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.48.0] — 2026-10-15
8
+
9
+ **`--dry-run` dispatched. Two dry runs sent a command to five machines, and one made a real task.**
10
+
11
+ ### The defect, measured before it was described
12
+
13
+ The bridge guarded exactly one thing under `--dry-run`: the **reply**. The dispatch ran unconditionally.
14
+
15
+ ```
16
+ [15:13:57] dry run : true
17
+ [15:15:00] dispatched task 01M50231AYJYDD41WSPMS9PRM3
18
+ [15:15:50] verdict for …: partial
19
+ ```
20
+
21
+ **That is a real task record with five real leases, produced by a run whose flag promised nothing would
22
+ happen.** A rehearsal executed commands on production machines — and it was found by accident, while draining
23
+ a message queue, not by a test.
24
+
25
+ ### Why the fix is "skip the dispatch" and not "rename the flag"
26
+
27
+ **This repository already fixes the meaning of the word.** `update-install`'s `dryRun` is asserted to **call no
28
+ package manager and write nothing**, and `auto-update`'s is asserted to **write nothing to the profile**. So a
29
+ second `dry-run` meaning "do the dangerous part anyway" is not a naming preference — **it is two flags with one
30
+ name and opposite meanings, and the bridge held the unsafe one.**
31
+
32
+ **Renaming would have left the flag. Aligning keeps the word and removes the hazard**, and a reader who knows
33
+ `dryRun` from anywhere else in this project is now already right about this one.
34
+
35
+ ### Why the guard is a function in `lib`, not an `if` in the script
36
+
37
+ `scripts/` is **not in `package.json#files`**, so nothing there is covered by a test that ships. A safety
38
+ property expressed as one line inside an untested script is a property nobody re-checks. `dryRunPolicy` is
39
+ exported and asserted beside the wire shapes — **a script is not where a guard should live if the guard is the
40
+ point.**
41
+
42
+ The guard sits at the **top of the only function that reaches `fleet.run`**, not at its call site, because a
43
+ guard at the call site is one edit away from being bypassed by a second caller.
44
+
45
+ ### A name collision that made my own test wrong
46
+
47
+ I asserted that `dryRun: 'true'` must not dispatch. **It does dispatch — the code was right and the test was
48
+ wrong**, and the cause is two different things sharing one word:
49
+
50
+ | | meaning |
51
+ |---|---|
52
+ | the CLI **flag** `--dry-run` | **present-means-true** (`argv.includes('--dry-run')`) |
53
+ | the **parameter** `dryRun` | **defaults to false**, which is what `update-install` means by it |
54
+
55
+ `dryRunPolicy` takes the parameter, and the script always hands it a boolean. The assertion is now the strict
56
+ one — **only `true` turns the dispatch off, so a future caller cannot open the path with a truthy value.**
57
+
58
+ **5 new tests, 0 failures.** One of them reads the script and asserts the guard appears **before** the only
59
+ call that can reach the fleet — because a test of the pure function alone would pass while the script still
60
+ dispatched, **which is precisely the shape of the original defect.**
61
+
62
+ ## [0.47.0] — 2026-10-15
63
+
64
+ **Correcting my own counts: 0.46.0 said 54/32/22, the table says 61/53/8.**
65
+
66
+ 0.46.0's entry claimed *"**54 rows** across twelve sections"*, *"**32 of 54** rows are executed; **22** are not"*.
67
+ **All three numbers were typed by hand, and all three were wrong.** Read from `docs/TEST-MATRIX.md`:
68
+
69
+ | | 0.46.0 said | the table says |
70
+ |---|---|---|
71
+ | rows | 54 | **61** |
72
+ | executed | 32 | **53** |
73
+ | not executed | 22 | **8 never run**, plus **11 blocked**, **1 skipped**, **2 refuted** |
74
+
75
+ ### The cause is that I merged two words that are not the same
76
+
77
+ **`阻塞` means the command RAN and the environment stopped it. `待执行` means it never ran at all.** I collapsed
78
+ both into "not executed" — **and a count that merges two categories is wrong in both directions at once**: it
79
+ overstates what was never attempted and understates what was attempted and blocked.
80
+
81
+ **The matrix now marks `阻塞` as "the command executed" explicitly, and carries a note recording this mistake**,
82
+ because the distinction is the one a reader needs in order to tell "we have not tried" from "we tried and were
83
+ stopped".
84
+
85
+ ### It is the same error the matrix itself documents, one level up
86
+
87
+ The matrix's own rule is that **a row's result may only be changed by re-running its `Instance` and reading the
88
+ output**, and that a count copied from an older plan becomes more true-looking each time it is copied. **I then
89
+ wrote the headline counts of the document from memory instead of reading the document.** The rule was in the
90
+ file I was writing.
91
+
92
+ **So the counts are no longer asserted anywhere by hand.** They are read from the table, and the reading is
93
+ recorded: `61 = 39 通过 + 8 待执行 + 11 阻塞 + 1 跳过 + 2 证伪`.
94
+
95
+ **No new tests.** What changed is three numbers and the reason they drifted.
96
+
7
97
  ## [0.46.0] — 2026-10-15
8
98
 
9
99
  **The test matrix the plans asked for — with every row it could not earn left as `待执行`, and the reason named.**
@@ -118,6 +118,7 @@ was not re-measured keeps its old date — **a stale date is information, and qu
118
118
  | `GATE-002` | an accepted orphan that is no longer an orphan fails — the list cannot go stale | **PASS** | 2026-10-15 | `field-authority` was removed when 0.41.0 gave it a caller, **because this check went red** |
119
119
  | `GATE-003` | `layer-check` prints `clean` **and exits 0** | **PASS** | 2026-10-15 | after the fix. **Before it printed `clean:` and exited `1`** — every reader trusting the status read a passing check as a failure; the pre-flight never noticed because its step reads the *printed text* |
120
120
  | `HARNESS-001` | a hand-rolled test harness measures the harness | **PASS (as a warning)** | 2026-10-15 | **41 failures** running every suite in one process with `--experimental-test-isolation=none`, vs **1** via the project's own entry. The other 38 were `mdns-discover`'s fetch stub leaking into `outbound-only` |
121
+ | `HARNESS-002` | **a count typed by hand drifts from the table it describes** | **FAIL, then corrected** | 2026-10-15 | CHANGELOG 0.46.0 claimed *"54 rows / 32 executed / 22 not"*. Read from `TEST-MATRIX.md`: **61 rows / 53 executed / 8 never run / 11 blocked / 1 skipped / 2 refuted.** I had merged **`阻塞`** (the command ran and the environment stopped it) with **`待执行`** (never run) — so 61 became 54 and 8 became 22 |
121
122
 
122
123
  ---
123
124
 
@@ -13,12 +13,16 @@
13
13
  |---|---|
14
14
  | **通过** | 已执行,且期望成立 |
15
15
  | **失败** | 已执行,且期望不成立 |
16
- | **阻塞** | 已执行,但被环境挡住(**并写明被什么挡住**) |
16
+ | **阻塞** | **命令执行了**,结果被环境挡住(**并写明被什么挡住**) |
17
17
  | **跳过** | 平台不适用,套件自报原因 |
18
18
  | **待执行** | **未执行**。必须写明为什么,否则这一行只是噪声 |
19
19
  | **证伪** | 计划中的一条主张被测量推翻 |
20
20
 
21
21
  > **一行"通过"必须能追到一个 `Instance`。追不到的,写"待执行"。** 这一条比表里任何数字都重要——它是本表唯一不能违反的规则。
22
+ >
23
+ > **"阻塞"与"待执行"不是同一件事,而我把它们混为一谈过一次。** 阻塞 = 命令**执行了**,被环境挡住;待执行 = **从未运行**。
24
+ > 在 CHANGELOG 0.46.0 里我手数成"54 行 / 22 未执行",把两者合并了——**而从本表读出来是 61 行 / 8 待执行 / 11 阻塞**。
25
+ > **手数的计数会漂移,所以本节的所有数字都是从表格里读出来的。**
22
26
 
23
27
  ---
24
28
 
@@ -159,9 +163,10 @@
159
163
  | 热修复 | 对应失败项 + 相邻模块回归 | 修复后必须回归通过 |
160
164
  | 季度/半年 | 全表重跑一次 | 防止环境漂移导致漏测 |
161
165
 
162
- ### 这张表当前的"未执行"分布,以及它们是不是同一件事
166
+ ### 这张表当前未通过的分布,以及它们是不是同一件事
163
167
 
164
- **54 项里 32 项已执行,22 项未执行。** 未执行的**不是 22 个独立问题,而是 4 个阻塞源**:
168
+ **61 项里 53 项已执行,8 项从未运行,11 项被执行但被环境挡住,1 项平台跳过。**
169
+ 未通过的**不是一堆独立问题,而是 4 个阻塞源**:
165
170
 
166
171
  | 阻塞源 | 卡住的项 | 解锁条件 |
167
172
  |---|---|---|
@@ -170,7 +175,7 @@
170
175
  | **没有可用配对码** | `TC-PAIR-003/004/005` | 一次可控的配对实验;会惊动编队,需你同意 |
171
176
  | **需要真实中断或外部资源** | `TC-UPD-001/002/004/005/007` | 一次真实更新检查;`TC-SEC-007` 需要 macOS/Linux |
172
177
 
173
- > **22 项未执行 ≠ 22 项风险。** 它们集中在 4 处,其中**两处**(中继升级、唤醒 Windows 机器)是可以立刻解决的。
178
+ > **未通过的项 ≠ 未通过的项数那么多风险。** 它们集中在 4 处,其中**两处**(中继升级、唤醒 Windows 机器)是可以立刻解决的。
174
179
 
175
180
  ---
176
181
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twinsearth/w2m-dsh-plugin",
3
- "version": "0.46.0",
3
+ "version": "0.48.0",
4
4
  "description": "DSH: Windows2MacOS — drive the same project on online Windows and macOS machines from one DeepSeek account, across networks and regions. P2P hole punching is the default path (relay + shared server as rendezvous, ledger and fallback), with a bundled STUN responder, Tailscale/public-VPS/tunnel shapes, Rabbit relay + per-machine Localside + eight DSH tools (replicate / split), nine aggregate verdicts and signed requests.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -25,6 +25,47 @@
25
25
 
26
26
  import { parseInvocation, renderVerdict, renderDispatchFailure } from './run-command.mjs';
27
27
 
28
+ /**
29
+ * What a run with `--dry-run` set is allowed to do.
30
+ *
31
+ * ## The defect this exists to make unreachable
32
+ *
33
+ * `scripts/ilink-fleet-bridge.mjs` guarded exactly one thing under `--dry-run`: the **reply**. The
34
+ * dispatch itself ran unconditionally, so **two dry runs sent `node--version` to all five machines**
35
+ * — and one of them produced a real task record with five real leases. The flag's name promises that
36
+ * nothing happens, and the promise was false in the direction that matters: **a rehearsal executed
37
+ * commands on production machines.**
38
+ *
39
+ * ## Why "skip the dispatch" rather than "rename the flag"
40
+ *
41
+ * This repository already fixes the meaning of the word. `update-install`'s `dryRun` is asserted to
42
+ * **write nothing and call no package manager**, and `auto-update`'s is asserted to **write nothing to
43
+ * the profile**. So a second `dry-run` that means "do the dangerous part anyway" is not a naming
44
+ * preference — it is two flags with one name and opposite meanings, and the bridge held the unsafe one.
45
+ *
46
+ * **Renaming would have left the flag. Aligning keeps the word and removes the hazard**, and it means
47
+ * a reader who knows `dryRun` from anywhere else in this project is already right about this one.
48
+ *
49
+ * ## Why this is a function rather than an `if` in the script
50
+ *
51
+ * `scripts/` is not in `package.json#files`, so nothing there is covered by a test that ships. A safety
52
+ * property expressed as one line inside an untested script is a property nobody re-checks. As an
53
+ * exported function with a name, it is asserted in `chatops-ilink-fleet-bridge.test.mjs` beside the
54
+ * wire shapes — **a script is not where a guard should live if the guard is the point.**
55
+ *
56
+ * @param {{ dryRun?: boolean }} [options]
57
+ * @returns {{ dispatch: boolean, reply: boolean, reason: string|null }}
58
+ */
59
+ export function dryRunPolicy(options = {}) {
60
+ if (options.dryRun !== true) return { dispatch: true, reply: true, reason: null };
61
+ return {
62
+ dispatch: false,
63
+ reply: false,
64
+ // Carried so the caller logs *why* nothing happened rather than printing a bare "[dry-run]".
65
+ reason: 'dry run: the fleet was not dispatched to, and no reply was sent',
66
+ };
67
+ }
68
+
28
69
  /** The header every authenticated call needs; undocumented and mandatory. */
29
70
  export const AUTH_TYPE = 'ilink_bot_token';
30
71
  export const APP_ID = 'bot';
@@ -208,7 +208,7 @@ const MAX_STAGES = 16;
208
208
  * `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
209
209
  * overrides it at runtime for tests and packagers.
210
210
  */
211
- const PLUGIN_VERSION = '0.46.0';
211
+ const PLUGIN_VERSION = '0.48.0';
212
212
 
213
213
  /** How long any single HTTP call may take before it is aborted, in milliseconds. */
214
214
  const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;