@twinsearth/w2m-dsh-plugin 0.0.0-stage → 0.4.7
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 +1309 -0
- package/LICENSE +21 -0
- package/PROTOCOL-v0.1.2.md +165 -0
- package/PROTOCOL-v0.3.0.md +454 -0
- package/PROTOCOL-v0.3.9.md +207 -0
- package/PROTOCOL-v0.4.0.md +255 -0
- package/PROTOCOL.md +339 -0
- package/README.md +665 -2
- package/bin/w2m-localside.mjs +303 -0
- package/bin/w2m-rabbit.mjs +396 -0
- package/bin/w2m-stun.mjs +231 -0
- package/cordis.patch.yml +94 -0
- package/deploy/caddy/Caddyfile.example +118 -0
- package/deploy/docker/Dockerfile +144 -0
- package/deploy/docker/README.md +338 -0
- package/deploy/docker/docker-compose.yml +179 -0
- package/deploy/nginx/w2m.conf.example +196 -0
- package/deploy/shared-server/README.md +118 -0
- package/deploy/shared-server/install.sh +255 -0
- package/deploy/systemd/w2m-rabbit.service +150 -0
- package/deploy/systemd/w2m-stun.service +69 -0
- package/deploy/windows/README.md +115 -0
- package/deploy/windows/install-service.ps1 +195 -0
- package/deploy/windows/w2m-agent-supervisor.mjs +205 -0
- package/deploy/windows/winsw/w2m-localside.xml +50 -0
- package/docs/DEPLOY.md +902 -0
- package/docs/LINT-AUDIT.md +492 -0
- package/docs/PIPELINE-DESIGN.md +95 -0
- package/docs/TROUBLESHOOTING.md +560 -0
- package/docs/UI-CARDS.md +81 -0
- package/lib/index.js +24 -0
- package/lib/tools.js +11 -0
- package/package.json +133 -4
- package/src/agent/agent.mjs +3317 -0
- package/src/agent/allowed-commands.mjs +116 -0
- package/src/agent/caps.mjs +437 -0
- package/src/agent/exec.mjs +356 -0
- package/src/agent/git.mjs +616 -0
- package/src/agent/identity.mjs +253 -0
- package/src/agent/p2p-node.mjs +1243 -0
- package/src/agent/p2p.mjs +605 -0
- package/src/agent/resolve.mjs +148 -0
- package/src/agent/spool.mjs +252 -0
- package/src/agent/stun.mjs +397 -0
- package/src/agent/url.mjs +127 -0
- package/src/plugin/auto-update.mjs +574 -0
- package/src/plugin/schedule.mjs +452 -0
- package/src/plugin/shared-config.mjs +264 -0
- package/src/plugin/tools.mjs +3891 -0
- package/src/plugin/update-install.mjs +764 -0
- package/src/plugin/update-source.mjs +686 -0
- package/src/relay/metrics.mjs +284 -0
- package/src/relay/peers.mjs +180 -0
- package/src/relay/persistence.mjs +207 -0
- package/src/relay/report.mjs +591 -0
- package/src/relay/server.mjs +1187 -0
- package/src/relay/state.mjs +2004 -0
- package/src/relay/stun-server.mjs +733 -0
- package/src/signing.mjs +359 -0
- package/src/util/cli.mjs +70 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
# Lint & format audit (task-23)
|
|
2
|
+
|
|
3
|
+
> Scope: adds ESLint + Prettier configuration and a CI lint job to a project that
|
|
4
|
+
> had neither. **TypeScript is deliberately deferred** — see §1.1.
|
|
5
|
+
>
|
|
6
|
+
> This document is the audit the change is based on: what the tools report on
|
|
7
|
+
> the tree **as it stands**, and what to do about each finding. Nothing in
|
|
8
|
+
> `src/`, `test/`, `bin/` or `scripts/` was modified to make lint pass.
|
|
9
|
+
>
|
|
10
|
+
> Measured 2026-10-08 against `eslint@10.12.0`, `prettier@3.9.9`,
|
|
11
|
+
> `globals@17.13.0`, on Node 24.21.0 (Windows).
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 0. TL;DR
|
|
16
|
+
|
|
17
|
+
| Question | Answer |
|
|
18
|
+
|---|---|
|
|
19
|
+
| Do the tools run? | Yes: `eslint .` lints 38 files; `prettier --check .` flags 44–45 (the repo is being edited concurrently, so this drifts by a file or two). |
|
|
20
|
+
| How many real defects? | **1** (`no-undef`: a function is called but never imported — §3.1). |
|
|
21
|
+
| Current ESLint state | 31 errors, 43 warnings across 19 files. |
|
|
22
|
+
| Would Prettier rewrite the tree? | **Yes — ~41–45 of 48 files, ~90% of all lines.** Recommendation: **do not run it** (§4). |
|
|
23
|
+
| Is `dependencies` still empty? | Yes, `{}` — unchanged. Only `devDependencies` gained four entries. |
|
|
24
|
+
| Is lint a blocking CI gate? | Yes. Prettier is reported, not blocking (§5). |
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 1. What was added
|
|
29
|
+
|
|
30
|
+
| File | Purpose |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `eslint.config.mjs` | Flat config (ESLint 9/10 style), stock JS parser only |
|
|
33
|
+
| `.prettierrc` | Options derived from measuring the existing style (§4.1), not from defaults |
|
|
34
|
+
| `.prettierignore` | Keeps generated artifacts, deployment templates and licence text out of scope |
|
|
35
|
+
| `.github/workflows/ci.yml` | New `lint` job (additive; `test`, `live-api`, `pack` untouched) |
|
|
36
|
+
| `package.json` | `devDependencies` + four scripts; `dependencies` still `{}` |
|
|
37
|
+
|
|
38
|
+
### 1.1 Why no TypeScript, and what that costs
|
|
39
|
+
|
|
40
|
+
The project is plain ESM JavaScript with no build step. Adding
|
|
41
|
+
typescript-eslint would mean adding TypeScript — which is the thing the user
|
|
42
|
+
asked to defer. The practical consequence is that the **type-aware rules are
|
|
43
|
+
unavailable**, and one of them matters here:
|
|
44
|
+
|
|
45
|
+
- `@typescript-eslint/no-floating-promises` — the canonical "you forgot
|
|
46
|
+
`await`" rule. It requires a TS program, so it cannot be enabled in this
|
|
47
|
+
configuration.
|
|
48
|
+
|
|
49
|
+
What was enabled instead, and how far it actually gets:
|
|
50
|
+
|
|
51
|
+
| Substitute | Coverage | Honest limitation |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| `no-undef` | An identifier that does not exist anywhere. **This is what caught the one real defect.** | Does not see a *wrong* identifier that does exist. |
|
|
54
|
+
| `no-async-promise-executor` | `new Promise(async …)` | n/a |
|
|
55
|
+
| `require-atomic-updates` | Shared state written after an `await` | ~6 of 7 findings in `src/` are false positives (§3.3) |
|
|
56
|
+
| `no-promise-executor-return` | Returning a value from a Promise executor | Fires on the idiomatic `new Promise((r) => setTimeout(r, ms))` |
|
|
57
|
+
|
|
58
|
+
**Conclusion:** a forgotten `await` remains invisible to this configuration.
|
|
59
|
+
Anyone wanting that class of check needs TypeScript, and this audit is the
|
|
60
|
+
evidence for the cost of not having it: one real defect out of the 31 errors,
|
|
61
|
+
and it happened to be a missing import rather than a missing `await`.
|
|
62
|
+
|
|
63
|
+
### 1.2 Rule-set structure
|
|
64
|
+
|
|
65
|
+
- `js.configs.recommended` as the base (ESLint 10's recommended set).
|
|
66
|
+
- A general block for `**/*.mjs` with `globals.node`, then explicit rules
|
|
67
|
+
grouped as *unbound/unused names*, *async mistakes*, *suspicious-but-legal*,
|
|
68
|
+
*shadowing*, *hygiene*, and *output discipline*.
|
|
69
|
+
- `bin/**` turns `no-console` off: the CLI entry points print by design.
|
|
70
|
+
- `test/**` and `scripts/**` demote exactly two rules to warnings (§3.3).
|
|
71
|
+
|
|
72
|
+
Two deliberate rule decisions, both with the reason recorded in the config:
|
|
73
|
+
|
|
74
|
+
| Rule | Setting | Why |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| `no-useless-assignment` | **off** | Ships as an *error* in ESLint 10's recommended set, and fires 6 times on `let x = null; try { x = … } catch { x = null }`. ESLint treats try/catch as "assigned on every path" and cannot see the window before the first assignment. Removing those initialisers would be the wrong fix. |
|
|
77
|
+
| `no-control-regex` | **left as error** | Fires 3 times in `src/agent/exec.mjs` on regexes that match ANSI escape sequences (`\x1b`, `\x07`). Those characters are the point of the pattern, so the correct fix is an inline `eslint-disable-next-line` at those lines — in `src/`, i.e. **not this task's write scope**. Reported in §3.2. |
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 2. Baseline numbers (real output)
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
$ eslint .
|
|
85
|
+
✖ 74 problems (31 errors, 43 warnings)
|
|
86
|
+
|
|
87
|
+
$ eslint . --format json -> 38 files linted, 19 with findings
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
By rule:
|
|
91
|
+
|
|
92
|
+
| Count | Rule | Where it comes from |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| 31 | `no-promise-executor-return` | my config, demoted to warn in `test/`+`scripts/` |
|
|
95
|
+
| 21 | `no-unused-vars` | my config |
|
|
96
|
+
| 18 | `require-atomic-updates` | my config, demoted to warn in `test/`+`scripts/` |
|
|
97
|
+
| 6 | `no-useless-assignment` | ESLint 10 recommended — now **off** |
|
|
98
|
+
| 3 | `no-control-regex` | ESLint 10 recommended |
|
|
99
|
+
| 1 | `no-undef` | my config — **the real defect** |
|
|
100
|
+
|
|
101
|
+
By directory (before demotion): `test/` 38, `src/` 21, `scripts/` 9, `bin/` 0,
|
|
102
|
+
`lib/` 0.
|
|
103
|
+
|
|
104
|
+
After the two demotions, CI's blocking count is **31 errors / 43 warnings**.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 3. Findings, by disposition
|
|
109
|
+
|
|
110
|
+
### 3.1 REAL DEFECT — 1 finding (fix in `src/`, outside this task's scope)
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
src/plugin/tools.mjs:571:9 error 'existsSync' is not defined no-undef
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
// src/plugin/tools.mjs
|
|
118
|
+
import { promises as fs } from 'node:fs'; // line 49 — only `fs`, no `existsSync`
|
|
119
|
+
…
|
|
120
|
+
function resolveProfileDir(config) { // line 559
|
|
121
|
+
…
|
|
122
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
123
|
+
const parent = path.dirname(dir);
|
|
124
|
+
if (parent === dir) break;
|
|
125
|
+
dir = parent;
|
|
126
|
+
if (path.basename(dir) !== 'node_modules') continue;
|
|
127
|
+
const root = path.dirname(dir);
|
|
128
|
+
if (existsSync(path.join(root, 'package.json'))) return root; // line 571 ← ReferenceError
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Why it is a defect, not a style complaint**
|
|
133
|
+
|
|
134
|
+
- `existsSync` is never imported anywhere in the file (all six imports checked).
|
|
135
|
+
- In an ESM module an unbound identifier is a `ReferenceError` at evaluation
|
|
136
|
+
time, not `undefined` — so this throws rather than silently taking the false
|
|
137
|
+
branch.
|
|
138
|
+
- It is reachable from normal plugin startup: `resolveProfileDir` is called at
|
|
139
|
+
`tools.mjs:416` while building the tool configuration, and the loop reaches
|
|
140
|
+
line 571 as soon as it walks up into a directory literally named
|
|
141
|
+
`node_modules`. That is the ordinary layout this project *ships* (`files`
|
|
142
|
+
includes `src/` and `lib/`, so the package always sits under a
|
|
143
|
+
`node_modules/`).
|
|
144
|
+
- The module-load walk probably starts from a path whose parent chain contains
|
|
145
|
+
`node_modules` in the published layout, which is exactly when a user would
|
|
146
|
+
hit it.
|
|
147
|
+
- `test/tools.test.mjs` has **no** `resolveProfileDir` coverage (grepped), which
|
|
148
|
+
is why 10 test files and 291 assertions pass while this line stays broken.
|
|
149
|
+
|
|
150
|
+
**Suggested fix** (one line, in `src/plugin/tools.mjs`, which is why this audit
|
|
151
|
+
stops here):
|
|
152
|
+
|
|
153
|
+
```js
|
|
154
|
+
import { existsSync, promises as fs } from 'node:fs';
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Suggested test**: a case that calls the profile-directory resolver from a
|
|
158
|
+
path under a `node_modules` directory and asserts it returns the package root.
|
|
159
|
+
That is the missing signal, not the import.
|
|
160
|
+
|
|
161
|
+
### 3.2 PURE STYLE — 27 findings, worth unifying, no behaviour change
|
|
162
|
+
|
|
163
|
+
**Unused bindings — 21** (`no-unused-vars`). These are dead declarations: no
|
|
164
|
+
behaviour change either way, and the risk of removing them is that a reader
|
|
165
|
+
loses a hint. Grouped by how they should be handled:
|
|
166
|
+
|
|
167
|
+
*Never-used imports (9)* — safe to delete, and two are worth a second look
|
|
168
|
+
because the *name* suggests missing wiring:
|
|
169
|
+
|
|
170
|
+
| Location | Binding | Note |
|
|
171
|
+
|---|---|---|
|
|
172
|
+
| `src/plugin/auto-update.mjs:44` | `parseSha256Sums` | **looks like missing wiring** — a checksum parser imported and never called. Verify whether signature verification was meant to run (§3.3). |
|
|
173
|
+
| `src/plugin/schedule.mjs:198` | `secondOfDayIn` | helper imported, never called |
|
|
174
|
+
| `test/relay.test.mjs:33` | `Persistence` | test-side import left over |
|
|
175
|
+
| `scripts/pack.mjs:29` | `relative` | — |
|
|
176
|
+
| `test/auto-update.test.mjs:11` | `before` | hook imported but no `before()` in the file |
|
|
177
|
+
| `test/auto-update.test.mjs:14` | `mkdirSync` | — |
|
|
178
|
+
| `test/e2e.test.mjs:29` | `tmpdir` | — |
|
|
179
|
+
| `test/tools.test.mjs:23` | `before` | same as above |
|
|
180
|
+
| `test/update-wiring.test.mjs:12` | `fs` | — |
|
|
181
|
+
|
|
182
|
+
*Assigned but never read (12)* — `prefer-const`-adjacent tidy-ups:
|
|
183
|
+
`src/plugin/auto-update.mjs:216` (`intervalDays`), `src/plugin/tools.mjs:1501`
|
|
184
|
+
(`transportError`), `src/relay/report.mjs:23` (`FAILURE_OUTCOME`),
|
|
185
|
+
`src/agent/agent.mjs:1186` (`stderr`), `scripts/probe-restart.mjs:83` (`base`),
|
|
186
|
+
`test/relay.test.mjs:814/828/865/902/914` (five `a`), `test/schedule.test.mjs:127`
|
|
187
|
+
(`midnight`), `test/agent.test.mjs:1925` (`requests`).
|
|
188
|
+
|
|
189
|
+
`test/relay.test.mjs`'s five bare `a` bindings are the strongest candidates for
|
|
190
|
+
a quick cleanup: an unused single-letter binding is almost always a leftover
|
|
191
|
+
from a refactor.
|
|
192
|
+
|
|
193
|
+
**`no-control-regex` — 3**, in one file:
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
src/agent/exec.mjs:311:18 / 312:18 / 313:20 Unexpected control character(s) in regex: \x1b, \x07
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Correct fix: inline `// eslint-disable-next-line no-control-regex` above each,
|
|
200
|
+
because matching ANSI escape sequences is the intent. **In `src/`, so not
|
|
201
|
+
changed here.**
|
|
202
|
+
|
|
203
|
+
**`require-atomic-updates` in `src/` — 7**, see §3.3.
|
|
204
|
+
|
|
205
|
+
### 3.3 RECOMMEND LEAVING ALONE — 46 findings
|
|
206
|
+
|
|
207
|
+
**`no-promise-executor-return` — 31, all of them.**
|
|
208
|
+
|
|
209
|
+
Every occurrence is the same line shape:
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
await new Promise((r) => setTimeout(r, ms)); // test/agent.test.mjs:171 and 30 more
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The rule's message ("Return values from promise executor functions cannot be
|
|
216
|
+
read") is about a *returned value* from an executor. Here the arrow body is a
|
|
217
|
+
bare call expression, which is the idiomatic sleep and is correct. **Disposition:
|
|
218
|
+
demoted to `warn` in `test/` and `scripts/`** (config, not a code change), so a
|
|
219
|
+
genuine instance still surfaces in output. 0 of 31 are defects.
|
|
220
|
+
|
|
221
|
+
**`require-atomic-updates` in tests — 18, all of them.**
|
|
222
|
+
|
|
223
|
+
All are deliberate state assignment in sequential scenario scripts:
|
|
224
|
+
`globalThis.fetch = stub` in a `finally`, `process.env.W2M_OPERATOR_TOKEN = …`
|
|
225
|
+
around a case, `group.relay = await startRelay(...)`. Each test file is a
|
|
226
|
+
straight-line script with no interleaving. **Disposition: demoted to `warn` in
|
|
227
|
+
`test/`.**
|
|
228
|
+
|
|
229
|
+
**`require-atomic-updates` in `src/` — 7, of which ~6 are false positives.**
|
|
230
|
+
These stay as **errors** (production code deserves the scrutiny), but none of
|
|
231
|
+
them should be "fixed" by rewriting the code:
|
|
232
|
+
|
|
233
|
+
| Location | Shared state written after `await` | Assessment |
|
|
234
|
+
|---|---|---|
|
|
235
|
+
| `agent.mjs:1022` | `toolchainCache = {…}` | **False positive.** Written from `toolchainInfo()`, which has an early `if (toolchainCache) return`. Two concurrent callers (`:1334`, `:1516`) can each recompute and the last write wins — but the value is derived from immutable per-project facts, so the only cost is doing the git lookup twice. There is nothing to "fix" short of a promise-coalescing cache. |
|
|
236
|
+
| `agent.mjs:1120,1121` | `identity.device_token`, `identity.rabbit_url` | **False positive.** Single caller: `bin/w2m-localside.mjs:184`, once, before the loop starts. |
|
|
237
|
+
| `agent.mjs:1591,1602` | `state.connected` | **False positive.** `streamOnce()` is called from the single reconnect loop; every writer is in that one function. |
|
|
238
|
+
| `agent.mjs:1798` | `pumping = false` (in `finally`) | **False positive.** `pump()` has a single entry point guarded by `pumping`. |
|
|
239
|
+
| `test/agent.test.mjs:2507` | `child` | Test-side, already demoted. |
|
|
240
|
+
|
|
241
|
+
**`no-useless-assignment` — 6.** The rule is now off with the reasoning in
|
|
242
|
+
§1.2. These are the defensive `let x = null` initialisers at
|
|
243
|
+
`src/agent/agent.mjs:1016`, `:1055`, `src/agent/git.mjs:365`,
|
|
244
|
+
`src/plugin/tools.mjs:1826`, `src/agent/caps.mjs:280`, and
|
|
245
|
+
`test/agent.test.mjs:1043`. Removing them would shorten the code and make the
|
|
246
|
+
declaration's type less clear for no behavioural gain.
|
|
247
|
+
|
|
248
|
+
**`no-console` — 0 findings, and one clarification the task asked for.**
|
|
249
|
+
|
|
250
|
+
The task expected console usage to be confined to `bin/`. It is — *except* that
|
|
251
|
+
a grep for `console.` finds 13 hits in `test/` and `scripts/`, all of which are
|
|
252
|
+
**console calls inside string literals** passed to `node -e` as the command
|
|
253
|
+
under test:
|
|
254
|
+
|
|
255
|
+
```js
|
|
256
|
+
const SMOKE_ARGV = [NODE, '-e', 'console.log("crossnetwork")']; // test/crossnetwork.test.mjs:54
|
|
257
|
+
command_argv: [NODE, '-e', 'console.log("dedupe")'], // test/e2e.test.mjs:471
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
ESLint correctly does not flag string contents. **So there is no counterexample
|
|
261
|
+
to report: no `console` call exists outside `bin/`.**
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## 4. Prettier: measured, and recommended against
|
|
266
|
+
|
|
267
|
+
### 4.1 The configuration was derived, not guessed
|
|
268
|
+
|
|
269
|
+
Measured over all 37 tracked `.js`/`.mjs` files (25,768 lines):
|
|
270
|
+
|
|
271
|
+
| Property | Measurement | Config chosen |
|
|
272
|
+
|---|---|---|
|
|
273
|
+
| Quotes | 7,279 single vs 285 double | `singleQuote: true` |
|
|
274
|
+
| Indentation | 21,283 space-indented lines, **0** tab-indented | `tabWidth: 2`, `useTabs: false` |
|
|
275
|
+
| Semicolons | present on statement ends throughout | `semi: true` |
|
|
276
|
+
| Line endings | 0 CRLF files | `endOfLine: 'lf'` |
|
|
277
|
+
| Object spacing | 1,156 `{ a: 1 }` vs 98 `{a: 1}` | `bracketSpacing: true` |
|
|
278
|
+
| Arrow params | 501 parenthesised vs **0** bare | `arrowParens: 'always'` |
|
|
279
|
+
| **Line width** | p50 34, p90 83, p95 96, **p99 118**, max 611 | `printWidth: 110` (swept, see below) |
|
|
280
|
+
|
|
281
|
+
`printWidth` was chosen by sweeping, not by eye — files that would change:
|
|
282
|
+
|
|
283
|
+
| printWidth | 80 | 90 | 100 | **110** | 120 | 130 | 140 |
|
|
284
|
+
|---|---|---|---|---|---|---|---|
|
|
285
|
+
| files changed | 45 | 44 | 42 | **41** | 42 | 43 | 43 |
|
|
286
|
+
|
|
287
|
+
110 is the minimum. At that width, `trailingComma: 'all'` (41) beats `es5` (44)
|
|
288
|
+
and `none` (44); `arrowParens: 'avoid'` (43) loses to `always` (41).
|
|
289
|
+
|
|
290
|
+
**So the configuration is optimal for this codebase — and it still rewrites 41
|
|
291
|
+
files.**
|
|
292
|
+
|
|
293
|
+
### 4.2 What it would actually change
|
|
294
|
+
|
|
295
|
+
Prettier was run with `--write` on a **throwaway copy** of the tracked tree (the
|
|
296
|
+
repository was never written to). Result:
|
|
297
|
+
|
|
298
|
+
| Metric | Value |
|
|
299
|
+
|---|---|
|
|
300
|
+
| Files rewritten | **31 of 37 JS files** (44–45 of 48 including docs/CI once `.prettierignore` scopes the rest) |
|
|
301
|
+
| Lines changed | **23,715 of 26,383 — 89.9%** |
|
|
302
|
+
| Category of changed lines | 23,513 "other", 122 whitespace-only, 61 quote style, **19 re-wrap** |
|
|
303
|
+
|
|
304
|
+
The decisive number is the last row: only **19** of ~23,700 changed lines are
|
|
305
|
+
pure re-wrapping. The rest are structural. Concretely:
|
|
306
|
+
|
|
307
|
+
```js
|
|
308
|
+
// before (one line, 96 columns — inside the 110 budget)
|
|
309
|
+
'-c', 'user.name=w2m-test', '-c', 'user.email=w2m@test.invalid', 'commit', '-m', 'base',
|
|
310
|
+
|
|
311
|
+
// after
|
|
312
|
+
'-c',
|
|
313
|
+
'user.name=w2m-test',
|
|
314
|
+
'-c',
|
|
315
|
+
'user.email=w2m@test.invalid',
|
|
316
|
+
'commit',
|
|
317
|
+
'-m',
|
|
318
|
+
'base',
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
and, worse, argument lists that already fit get exploded anyway when an
|
|
322
|
+
*enclosing* call exceeds the budget:
|
|
323
|
+
|
|
324
|
+
```js
|
|
325
|
+
// before: 6 lines
|
|
326
|
+
throw new W2MError('W2M_ABORTED', `…${reason instanceof Error && reason.message ? … }`, {
|
|
327
|
+
hint: '…',
|
|
328
|
+
cause: reason,
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// after: 12+ lines, with the message split across lines and the options object
|
|
332
|
+
// re-indented as an argument block
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### 4.3 Recommendation: do not reformat now
|
|
336
|
+
|
|
337
|
+
The measured facts point one way:
|
|
338
|
+
|
|
339
|
+
1. **The magnitude defeats the purpose.** The task's stated worry was "a
|
|
340
|
+
several-thousand-line diff that buries real changes". The real number is
|
|
341
|
+
~23,700 lines across 31 files — 90% of the codebase. As a single commit it
|
|
342
|
+
would also destroy `git blame` for every line of `src/` at once, which
|
|
343
|
+
matters more than the diff size.
|
|
344
|
+
2. **The house style is a deliberate rule, not sloppiness.** The author packs
|
|
345
|
+
multiple values per line and breaks only near ~110 columns (p99 = 118). That
|
|
346
|
+
is a consistent, readable convention; Prettier simply cannot express it, so
|
|
347
|
+
the "fix" is the formatter imposing a different convention.
|
|
348
|
+
3. **The mitigation the task offered — "narrow the config so it doesn't touch
|
|
349
|
+
existing code" — is not available.** The sweep in §4.1 shows 41 files is the
|
|
350
|
+
floor. No combination of supported options reaches zero.
|
|
351
|
+
4. **The cost of waiting is near zero.** Prettier is configured and runnable
|
|
352
|
+
(`npm run format`), `.prettierignore` already excludes generated files,
|
|
353
|
+
deployment templates and licence text, and CI reports the count on every run.
|
|
354
|
+
Nothing degrades by deferring.
|
|
355
|
+
|
|
356
|
+
**What I would do instead**, in order of preference:
|
|
357
|
+
|
|
358
|
+
- **Keep Prettier as a tool for new files** (`npm run format <path>`), and let
|
|
359
|
+
CI keep reporting the count. If the count starts growing rather than shrinking,
|
|
360
|
+
that is the signal to revisit — it would mean new code is being added
|
|
361
|
+
unformatted.
|
|
362
|
+
- **If a formatting commit is wanted, do it on a dedicated branch with no other
|
|
363
|
+
changes**, add it to `.git-blame-ignore-revs`, and only after the current
|
|
364
|
+
work has landed. Both the `45`-vs-`41` question and the blame problem then
|
|
365
|
+
stop mattering.
|
|
366
|
+
- **Do not** add 41 paths to `.prettierignore` to force a green check: that
|
|
367
|
+
would make the check meaningless while looking green, which is worse than an
|
|
368
|
+
informational count.
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## 5. CI integration
|
|
373
|
+
|
|
374
|
+
### 5.1 What was added
|
|
375
|
+
|
|
376
|
+
A new `lint` job (`ubuntu-latest`, Node 22) inserted **before** `test`. `test`,
|
|
377
|
+
`live-api` and `pack` are untouched, as is `release.yml`.
|
|
378
|
+
|
|
379
|
+
```
|
|
380
|
+
lint:
|
|
381
|
+
- actions/checkout@v4
|
|
382
|
+
- actions/setup-node@v4 (node 22)
|
|
383
|
+
- npm install --no-audit --no-fund
|
|
384
|
+
- npm run lint <- BLOCKING
|
|
385
|
+
- npm run format:check || true <- informational, plus a ::warning:: with the count
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### 5.2 `npm install` vs `npx --yes`: why `npm install`
|
|
389
|
+
|
|
390
|
+
The task asked which is more stable. **`devDependencies` + `npm install`** wins
|
|
391
|
+
for three reasons:
|
|
392
|
+
|
|
393
|
+
1. **The repo commits no lockfile on purpose.** That rules out `npm ci`, but it
|
|
394
|
+
also means `npx --yes eslint@9` and `eslint: ^9` resolve through the same
|
|
395
|
+
"latest matching" logic — the reproducibility argument for `npx` does not
|
|
396
|
+
apply here.
|
|
397
|
+
2. **`npx --yes` is the riskier of the two, not the safer.** It installs
|
|
398
|
+
whatever is latest *at run time* into a cache the author never sees, decoupled
|
|
399
|
+
from what a developer has locally. `npm install` uses the ranges in
|
|
400
|
+
`package.json`, so CI and a developer run the same versions.
|
|
401
|
+
3. **The manifest becomes the source of truth.** `eslint@^10.12.0` in
|
|
402
|
+
`devDependencies` is reviewable; `npx --yes eslint@9` buried in YAML is not.
|
|
403
|
+
(It also means the task's `eslint@9` could not be honoured — 10.12.0 is
|
|
404
|
+
current, `@eslint/js` only exists from 9.9 onward, and ESLint 10's
|
|
405
|
+
`recommended` set includes `no-useless-assignment` and `no-control-regex`,
|
|
406
|
+
both discussed above.)
|
|
407
|
+
|
|
408
|
+
`dependencies` remains `{}`. All four additions are dev-only; the published
|
|
409
|
+
tarball is unaffected.
|
|
410
|
+
|
|
411
|
+
### 5.3 Why lint is blocking but Prettier is not
|
|
412
|
+
|
|
413
|
+
- **Lint gates** because its errors are findings, not preferences: an unbound
|
|
414
|
+
identifier (§3.1), dead code, unused bindings. Cheap (seconds, one OS) and it
|
|
415
|
+
fails for a different reason than a test failure, so it is its own job rather
|
|
416
|
+
than a step inside `test`.
|
|
417
|
+
- **Prettier reports** because the only way to make it pass today is the
|
|
418
|
+
~23,700-line rewrite of §4.3 or an ignore list that would make the check
|
|
419
|
+
vacuous. It prints the count and raises a `::warning::` so drift is visible.
|
|
420
|
+
|
|
421
|
+
Flipping Prettier to blocking later is a two-character change: delete the
|
|
422
|
+
`|| true`.
|
|
423
|
+
|
|
424
|
+
### 5.4 Local verification
|
|
425
|
+
|
|
426
|
+
The CI steps were reproduced on the authoring machine (Windows, Node 24.21.0)
|
|
427
|
+
using the same binaries `node_modules/.bin` would expose:
|
|
428
|
+
|
|
429
|
+
```
|
|
430
|
+
$ eslint . --format json -> 38 files linted, 19 with findings
|
|
431
|
+
exit 1 (CI fails the job here, by design)
|
|
432
|
+
$ prettier --check . -> 44 files, exit 1
|
|
433
|
+
$ prettier --list-different . | wc -l -> 44 (what the CI step echoes)
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
`ci.yml` was validated as YAML and its job/step shape printed:
|
|
437
|
+
|
|
438
|
+
```
|
|
439
|
+
ci.yml jobs : ['lint', 'test', 'live-api', 'pack']
|
|
440
|
+
lint runs-on='ubuntu-latest' steps=5
|
|
441
|
+
test runs-on='${{ matrix.os }}' steps=9
|
|
442
|
+
release.yml jobs: ['release'] (unchanged)
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### 5.5 NOT verified here
|
|
446
|
+
|
|
447
|
+
- **The workflow has not been run on GitHub.** A hosted-runner run is the only
|
|
448
|
+
way to confirm `npm install` resolves on a clean Linux image; everything above
|
|
449
|
+
is a local simulation of the same commands.
|
|
450
|
+
- **Node 20 was not exercised.** The `lint` job pins Node 22 (matching the other
|
|
451
|
+
Linux jobs). ESLint 10 requires Node ≥ 20.19, so the job would also work on
|
|
452
|
+
20, but the repository's `engines` floor is `>=20.0.0` — if a user on 20.0.x
|
|
453
|
+
runs `npm run lint` locally it may refuse. Worth raising the `engines` floor
|
|
454
|
+
to `>=20.19.0` in a later change, which is a `package.json` key this task was
|
|
455
|
+
told not to touch beyond scripts and devDependencies.
|
|
456
|
+
- **macOS/Windows lint was not run in CI**, and does not need to be: ESLint
|
|
457
|
+
results do not vary by platform here, and the goal was the cheapest useful
|
|
458
|
+
gate.
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
## 6. Files changed by this task
|
|
463
|
+
|
|
464
|
+
| File | Change |
|
|
465
|
+
|---|---|
|
|
466
|
+
| `eslint.config.mjs` | new |
|
|
467
|
+
| `.prettierrc` | new |
|
|
468
|
+
| `.prettierignore` | new |
|
|
469
|
+
| `.github/workflows/ci.yml` | +47 lines, one new job |
|
|
470
|
+
| `package.json` | +4 devDependencies, +4 scripts; `dependencies` still `{}` |
|
|
471
|
+
|
|
472
|
+
No file under `src/`, `test/`, `bin/`, `scripts/` or
|
|
473
|
+
`.github/workflows/release.yml` was modified. The one real defect (§3.1), the
|
|
474
|
+
three `no-control-regex` lines (§3.2) and the six `no-useless-assignment`
|
|
475
|
+
initialisers (§3.3) are **reported, not fixed** — they are all in `src/`.
|
|
476
|
+
|
|
477
|
+
### 6.1 Side effect to decide on
|
|
478
|
+
|
|
479
|
+
`package.json`'s `files` array includes `docs/` and `deploy/` (only
|
|
480
|
+
`lib`, `src`, `bin`, `cordis.patch.yml`, `README.md`, `CHANGELOG.md`, `LICENSE`,
|
|
481
|
+
`PROTOCOL.md` and `PROTOCOL-v0.1.2.md` are matched as individual files), so
|
|
482
|
+
**any `.md` added under `docs/` enters the published tarball** — including this
|
|
483
|
+
audit. `scripts/pack.mjs` only checks that declared paths *exist*, not what is
|
|
484
|
+
inside them, so nothing breaks; but a contributor-facing audit is arguably not
|
|
485
|
+
runtime payload.
|
|
486
|
+
|
|
487
|
+
If you would rather it stayed out of the artifact, the smallest change is to
|
|
488
|
+
narrow `files` from `docs` to the specific documents that belong in the package
|
|
489
|
+
(`docs/DEPLOY.md`, `docs/TROUBLESHOOTING.md`). That touches a `package.json` key
|
|
490
|
+
beyond scripts/devDependencies, which this task was told not to modify — hence a
|
|
491
|
+
recommendation rather than an edit. Note it also affects the release hash, so it
|
|
492
|
+
belongs in a deliberate release change rather than this one.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Pipeline mode: the design question that has to be answered first
|
|
2
|
+
|
|
3
|
+
Status: **unresolved, and `pipeline` is deliberately not implemented.** The relay rejects
|
|
4
|
+
`mode: "pipeline"` with `BAD_REQUEST` rather than guessing.
|
|
5
|
+
|
|
6
|
+
This file exists because the question is not obvious until you try to answer it, and writing the
|
|
7
|
+
analysis down is cheaper than re-deriving it — or than shipping a guess that half the system then
|
|
8
|
+
depends on.
|
|
9
|
+
|
|
10
|
+
## What is already settled
|
|
11
|
+
|
|
12
|
+
`broadcast` (v0.3.2) covers "run once, tell everyone":
|
|
13
|
+
|
|
14
|
+
| mode | who runs | who is told |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| `replicate` | every machine | — |
|
|
17
|
+
| `split` | every machine, its own slice by index | — |
|
|
18
|
+
| `broadcast` | exactly one machine (deterministic) | every other machine, as an observer |
|
|
19
|
+
|
|
20
|
+
Observers hold an `observing` lease and are excluded from every aggregation step, so a machine that
|
|
21
|
+
did not run can never be counted as agreeing. That much is implemented and tested.
|
|
22
|
+
|
|
23
|
+
## The question
|
|
24
|
+
|
|
25
|
+
A pipeline is a chain: stage 1 produces something, stage 2 consumes it, stage 3 consumes that.
|
|
26
|
+
On one machine that is unambiguous. On a fleet it is not, because the fleet can run stage 1 on *n*
|
|
27
|
+
machines and produce *n* different outputs. So before any code:
|
|
28
|
+
|
|
29
|
+
> **Which machine's stage-1 output does stage 2 receive?**
|
|
30
|
+
|
|
31
|
+
There is no default answer that is obviously right. The three defensible ones behave very
|
|
32
|
+
differently, and picking wrong is expensive to undo because the aggregate report, the CLI, the tool
|
|
33
|
+
schema and the protocol all encode the choice.
|
|
34
|
+
|
|
35
|
+
## Option A — one machine runs the whole chain
|
|
36
|
+
|
|
37
|
+
The pipeline executes entirely on a single machine, stage after stage. `broadcast` then selects which
|
|
38
|
+
machine, and `pipeline` describes the sequence. Composition: `broadcast + pipeline` = "run this chain
|
|
39
|
+
on machine X and tell everyone".
|
|
40
|
+
|
|
41
|
+
- **Reproducible.** One machine, one chain, one record. Directly restartable: rerun the same chain
|
|
42
|
+
and you get the same shape.
|
|
43
|
+
- **Honest.** No cross-machine data flow to model, so nothing can be silently dropped in transit.
|
|
44
|
+
- **No new protocol surface.** Stages ship as a list inside the existing offer.
|
|
45
|
+
- **Cost:** no parallelism across stages, and the fleet is not used for the chain.
|
|
46
|
+
|
|
47
|
+
## Option B — stages fan out, results converge
|
|
48
|
+
|
|
49
|
+
Stage 1 runs on the target set; the relay collects every machine's output and feeds the whole set to
|
|
50
|
+
stage 2, which also runs on the target set. This is a fan-out/fan-in graph.
|
|
51
|
+
|
|
52
|
+
- **Uses the fleet** for the chain, which is the obvious appeal.
|
|
53
|
+
- **Cost, and it is the serious one:** "feed the whole set to stage 2" is not a well-defined input.
|
|
54
|
+
Concatenated in what order? What does stage 2 do with three different outputs — run three times in
|
|
55
|
+
parallel on each machine, or once with all three as input? Each answer is a different feature, and
|
|
56
|
+
the aggregate report's meaning changes with each. Ordering alone is a correctness problem: two
|
|
57
|
+
machines finishing in a different order would produce different stage-2 inputs, so the same task id
|
|
58
|
+
would not describe the same execution — which breaks the property this project exists to provide.
|
|
59
|
+
|
|
60
|
+
## Option C — each machine runs the whole chain independently
|
|
61
|
+
|
|
62
|
+
Every machine runs stage 1, then its own stage 2, then its own stage 3. No cross-machine flow at all;
|
|
63
|
+
it is `replicate` applied to a sequence.
|
|
64
|
+
|
|
65
|
+
- **Trivial to implement** — stage list inside the existing offer, no relay changes beyond validation.
|
|
66
|
+
- **Unambiguous** and keeps per-machine reproducibility.
|
|
67
|
+
- **Cost:** it is `replicate` with a for-loop. It adds no capability that running three `replicate`
|
|
68
|
+
tasks in sequence does not already give, so it earns little.
|
|
69
|
+
|
|
70
|
+
## Recommendation
|
|
71
|
+
|
|
72
|
+
**Option A**, with Option C as a cheap follow-on if useful.
|
|
73
|
+
|
|
74
|
+
Reasons, in order of weight:
|
|
75
|
+
|
|
76
|
+
1. Option B is the only one that needs cross-machine data flow, and it is the only one whose
|
|
77
|
+
semantics are unclear. Not implementing it is a decision, not an omission.
|
|
78
|
+
2. Option A composes with `broadcast`, so it adds a real capability: "run this chain on that machine
|
|
79
|
+
and inform the fleet."
|
|
80
|
+
3. Option A keeps the property the rest of the system is built on — a task id identifies one
|
|
81
|
+
execution, reproducible from its own record.
|
|
82
|
+
|
|
83
|
+
## What implementing Option A requires
|
|
84
|
+
|
|
85
|
+
- `state.mjs`: accept `mode: 'pipeline'` with `stages: [{command_argv, cwd_rel, ...}]`; keep
|
|
86
|
+
`executor_machine_id` meaningful; reject `index_total > 1` for the same reason `broadcast` does.
|
|
87
|
+
- `agent.mjs`: execute stages in order, stop on the first non-`ok` stage, and report per-stage
|
|
88
|
+
outcomes. This is the substantial part — the envelope currently describes exactly one command.
|
|
89
|
+
- `report.mjs`: a chain failure must be distinguishable from a chain that never started.
|
|
90
|
+
- `tools.mjs`: `w2m_run` gains `stages`.
|
|
91
|
+
- `PROTOCOL`: the new field, and a compatibility note.
|
|
92
|
+
|
|
93
|
+
**Not verified, and it cannot be verified here:** whether Option A is what was actually wanted. That
|
|
94
|
+
is a product question, not a technical one, and guessing it is how a fleet ends up with a chained
|
|
95
|
+
mode nobody asked for.
|