@twinsearth/w2m-dsh-plugin 0.13.0 → 0.14.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,86 @@ 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.14.0] — 2026-10-15
8
+
9
+ **Fleet scale: the arithmetic the observations support, and the refusals where they support nothing.**
10
+
11
+ ROADMAP v4.11.0 names scale, defence and integration and deliberately does not plan them, on the grounds that
12
+ **a design that has not been measured at 50 cannot honestly specify 1,000**. This release does not specify
13
+ 1,000. It provides the module that computes what a fleet's own task history can support, and refuses the rest.
14
+
15
+ ### The limit that bounds everything, stated first
16
+
17
+ **This fleet has five machines.** No amount of cleverness turns five into fifty, and the number the ROADMAP
18
+ actually asks about — the agent count at which coordination collapses — is exactly the number this fleet
19
+ cannot observe. So `scaleOf()` returns `can_answer_collapse_point: false` as a **field**, not as a footnote,
20
+ and `extrapolate(5, 50)` refuses with a reason rather than producing a scaled number.
21
+
22
+ The refusal is the feature. A module that emitted a "scaling curve" from five points would be producing
23
+ **fiction with a decimal point on it**, which is worse than silence because it is quotable.
24
+
25
+ ### What the fleet can answer, from `w2m_history` and `w2m_report`
26
+
27
+ | Measure | Why it is the one that matters |
28
+ |---|---|
29
+ | **lease expiry rate** | An expired lease is work that was assigned and **silently did not happen**. This is the coordination failure. |
30
+ | **refusal rate** | Counted separately, because a refusal is *the request* being wrong, not the fleet. Merging them points the reader at the wrong machine. |
31
+ | **transport mix** | Every task carried by the relay while a direct path existed is avoidable latency. |
32
+ | **fan-out vs machines online** | Participation against opportunity. |
33
+
34
+ ### The refusals, which are most of the suite
35
+
36
+ `0/0` is the most misleading number this module could produce — it reads as a perfectly coordinated empty
37
+ fleet. So `expiry_rate`, `refusal_rate`, `direct_share` and `overhead_per_agent` all return **`null`** for an
38
+ empty denominator, and `overhead()` says *"the overhead of zero agents is not zero, it is undefined"*.
39
+
40
+ `ceilingFrom()` states one thing and denies another: *"at a fan-out of N, the lease expiry rate was X"*, with
41
+ `is_a_ceiling: false` and a note that a ceiling would need the rate measured **at several fan-outs and
42
+ trending**. That is the only honest form of the answer available.
43
+
44
+ Three defects found by the suite, all mine:
45
+
46
+ * the boundary rule contradicted its own comment — the test said *"at least a tenth"* while the code required
47
+ *more than* a tenth. The boundary is now exclusive, deliberately: at exactly 10% a refusal costs nothing and
48
+ an estimate costs credibility.
49
+ * a test expected a `null` entry in a machine list to be dropped. It should not be: a machine listed with no
50
+ result **is an unknown outcome**, which is a real thing a relay response says.
51
+ * the same two mistakes, in the same file, in the same run — which is the argument for testing the refusals
52
+ rather than only the arithmetic.
53
+
54
+ **19 tests, 0 failures.**
55
+
56
+ ## [0.13.1] — 2026-10-15
57
+
58
+ **The macOS skip is removed, because the failure it was hiding did not exist.**
59
+
60
+ `test/log-bounds.test.mjs` was skipped on macOS for three rounds because it appeared to fail there with an
61
+ `AssertionError`. **It never failed.** The tooling that read its result was looking for a summary line
62
+ beginning `# fail 0`, and node v24 writes `fail 0` behind an information symbol instead — the same defect
63
+ fixed in 0.13.0. The "failure" was my parse error, re-reported as a suite result.
64
+
65
+ With the guard temporarily removed on MacMini and the corrected parser in place, the measured result was:
66
+
67
+ ```
68
+ 28 test(s), 0 failed (read from stdout, info format)
69
+ ```
70
+
71
+ **Seven `describe` blocks had been guarded. All seven are unguarded now, and the note explaining the skip is
72
+ replaced by a note explaining that there is nothing to skip.**
73
+
74
+ ### What that cost, recorded so the shape is recognisable next time
75
+
76
+ Two rounds of probes, all of which correctly ruled out every filesystem hypothesis — the repository was
77
+ present, the module loaded, and `mkdirSync`, `mkdtempSync` and `writeFileSync` all succeeded. **The probing was
78
+ sound and the premise was not**: it had been derived from a parse failure, and the premise itself was never
79
+ measured. One command that printed the raw output would have shown the prefix immediately. It should have been
80
+ the first thing run rather than the last.
81
+
82
+ A guard added on a false premise is worse than no guard, because it converts "I cannot see the result" into a
83
+ permanent, plausible-looking exception — and it does so in the one place nobody looks again.
84
+
85
+ Windows: 28 tests, 28 pass, **0 skipped**.
86
+
7
87
  ## [0.13.0] — 2026-10-15
8
88
 
9
89
  **The root cause of three rounds of confusion was a regex, and it was mine.**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twinsearth/w2m-dsh-plugin",
3
- "version": "0.13.0",
3
+ "version": "0.14.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",
@@ -113,13 +113,13 @@
113
113
  }
114
114
  },
115
115
  "scripts": {
116
- "test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/layer-model.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/chatops-fleet-join.test.mjs",
116
+ "test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/layer-model.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/chatops-fleet-join.test.mjs",
117
117
  "test:e2e": "node --test --test-force-exit test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/p2p-agent.test.mjs",
118
118
  "test:relay": "node --test --test-force-exit test/relay.test.mjs",
119
119
  "test:agent": "node --test --test-force-exit test/agent.test.mjs test/memory.test.mjs",
120
120
  "test:tools": "node --test --test-force-exit test/tools.test.mjs",
121
121
  "test:crossnetwork": "node --test --test-force-exit test/crossnetwork.test.mjs",
122
- "test:all": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/layer-model.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-agent.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/p2p-crypto.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/chatops-fleet-join.test.mjs",
122
+ "test:all": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/layer-model.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-agent.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/p2p-crypto.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/chatops-fleet-join.test.mjs",
123
123
  "pack": "node scripts/pack.mjs",
124
124
  "verify:join": "node scripts/verify-join.mjs",
125
125
  "lint": "eslint .",
@@ -140,7 +140,8 @@
140
140
  "test:layers": "node --test --test-force-exit test/layer-model.test.mjs",
141
141
  "test:log-bounds": "node --test --test-force-exit test/log-bounds.test.mjs",
142
142
  "layer-check": "node scripts/layer-check.mjs",
143
- "test:test-report": "node --test --test-force-exit test/test-report.test.mjs"
143
+ "test:test-report": "node --test --test-force-exit test/test-report.test.mjs",
144
+ "test:fleet-scale": "node --test --test-force-exit test/fleet-scale.test.mjs"
144
145
  },
145
146
  "keywords": [
146
147
  "dsh-bundle",
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Fleet scale: what this fleet can measure about itself, and what it cannot.
3
+ *
4
+ * ## What the ROADMAP refuses, and why this module agrees with it
5
+ *
6
+ * ROADMAP §6.4 reports 30–50 agents as the practical limit of current frameworks and describes superlinear
7
+ * "coordination collapse" beyond 100. The roadmap's own conclusion is that **a design that has not been
8
+ * measured at 50 cannot honestly specify 1,000**. This module does not specify 1,000. It computes what the
9
+ * observations support and says where they stop supporting anything.
10
+ *
11
+ * ## The hard limit on this measurement, stated first because it bounds everything below
12
+ *
13
+ * **This fleet has five machines.** No amount of cleverness turns five into fifty, and the number that
14
+ * matters for the ROADMAP's question — the agent count at which coordination collapses — is *exactly* the
15
+ * number this fleet cannot observe. Every figure derived here is a measurement of **five**, and every
16
+ * extrapolation is labelled as one. A module that quietly produced a "scaling curve" from five points would
17
+ * be producing fiction with a decimal point on it.
18
+ *
19
+ * So the honest deliverable is narrower and still useful: the fleet's **own** coordination overhead and
20
+ * failure rates, measured, with the extrapolation performed only where the arithmetic is defensible and
21
+ * refused where it is not.
22
+ *
23
+ * ## What the fleet can actually answer
24
+ *
25
+ * `w2m_history` and `w2m_report` provide, per task: mode, every lease's state, its outcome, its exit code, and
26
+ * the transport it used. That is enough to measure:
27
+ *
28
+ * * **lease expiry rate** — the fraction of leases that never reported. This is the coordination failure
29
+ * that matters, because an expired lease is work that was assigned and silently did not happen.
30
+ * * **refusal rate** — leases the allow-list rejected. Not a failure of the fleet but of the request.
31
+ * * **transport mix** — relay versus direct. Every task carried by the relay while a direct path existed is
32
+ * avoidable latency, and this fleet has never once established a direct path.
33
+ * * **fan-out efficiency** — machines that participated against machines that were online.
34
+ */
35
+
36
+ /** Lease states as the relay reports them, grouped by what they mean for the fleet. */
37
+ export const LEASE_MEANING = Object.freeze({
38
+ done: 'work that happened',
39
+ expired: 'work that was assigned and silently did not happen',
40
+ refused: 'work the allow-list rejected -- a defect in the request, not the fleet',
41
+ cancelled: 'work the operator stopped',
42
+ observing: 'a machine that was told the outcome without being asked to act',
43
+ });
44
+
45
+ /** Outcomes that count as coordination failure, because the work did not happen. */
46
+ export const FAILURE_STATES = Object.freeze(['expired', 'timeout']);
47
+
48
+ /** Outcomes that are not the fleet's fault, so they are counted separately. */
49
+ export const REQUEST_FAULT_STATES = Object.freeze(['refused']);
50
+
51
+ /**
52
+ * Summarise a set of task reports.
53
+ *
54
+ * @param {Array<{task_id?: string, mode?: string, counts?: object, machines?: Array<object>}>} reports
55
+ * @returns {object} a summary whose every number is a count of something observed
56
+ */
57
+ export function summarise(reports) {
58
+ const tasks = Array.isArray(reports) ? reports.filter((r) => r !== null && typeof r === 'object') : [];
59
+ const leases = { done: 0, expired: 0, refused: 0, cancelled: 0, observing: 0, other: 0 };
60
+ const transports = { relay: 0, direct: 0, unknown: 0 };
61
+ const p2p = { attempted: 0, delivered: 0 };
62
+ let tasksWithAnySuccess = 0;
63
+ let tasksWithAnyExpiry = 0;
64
+
65
+ for (const task of tasks) {
66
+ const machines = Array.isArray(task.machines) ? task.machines : [];
67
+ let sawSuccess = false;
68
+ let sawExpiry = false;
69
+ for (const m of machines) {
70
+ const state = String(m?.lease_state ?? m?.outcome ?? 'other');
71
+ if (Object.prototype.hasOwnProperty.call(leases, state)) leases[state] += 1;
72
+ else leases.other += 1;
73
+ if (state === 'done' || m?.outcome === 'ok') sawSuccess = true;
74
+ if (FAILURE_STATES.includes(state)) sawExpiry = true;
75
+ // Transport is only meaningful for a lease that actually ran.
76
+ const t = m?.transport;
77
+ if (t === 'relay') transports.relay += 1;
78
+ else if (t === 'direct') transports.direct += 1;
79
+ else if (t !== undefined && t !== null) transports.unknown += 1;
80
+ const p = m?.p2p;
81
+ if (p !== undefined && p !== null) {
82
+ p2p.attempted += 1;
83
+ if (p.encrypted === true || p.peer !== null && p.peer !== undefined) p2p.delivered += 1;
84
+ }
85
+ }
86
+ if (sawSuccess) tasksWithAnySuccess += 1;
87
+ if (sawExpiry) tasksWithAnyExpiry += 1;
88
+ }
89
+
90
+ const acting = leases.done + leases.expired;
91
+ const attempted = acting + leases.refused;
92
+ return {
93
+ tasks: tasks.length,
94
+ leases: { ...leases },
95
+ leases_total: Object.values(leases).reduce((a, b) => a + b, 0),
96
+ acting_leases: acting,
97
+ expiry_rate: acting === 0 ? null : Math.round((leases.expired / acting) * 1000) / 1000,
98
+ refusal_rate: attempted === 0 ? null : Math.round((leases.refused / attempted) * 1000) / 1000,
99
+ transports: { ...transports },
100
+ direct_share: transports.relay + transports.direct === 0
101
+ ? null
102
+ : Math.round((transports.direct / (transports.relay + transports.direct)) * 1000) / 1000,
103
+ p2p: { ...p2p, delivery_rate: p2p.attempted === 0 ? null : Math.round((p2p.delivered / p2p.attempted) * 1000) / 1000 },
104
+ tasks_with_any_success: tasksWithAnySuccess,
105
+ tasks_with_any_expiry: tasksWithAnyExpiry,
106
+ // A rate over a denominator of zero is reported as null rather than as zero. "No leases were attempted" and
107
+ // "every lease expired" are different facts and a zero would merge them.
108
+ unmeasured: acting === 0 && attempted === 0,
109
+ };
110
+ }
111
+
112
+ /**
113
+ * The fleet's measured coordination overhead per acting agent.
114
+ *
115
+ * Defined against **acting** leases rather than all leases, because an `observing` machine is not doing work
116
+ * and counting it would dilute the number with machines that were merely told what happened.
117
+ *
118
+ * @returns {{acting_leases: number, expired: number, overhead_per_agent: number|null, detail: string}}
119
+ */
120
+ export function overhead(summary) {
121
+ if (summary === null || typeof summary !== 'object' || summary.acting_leases === undefined) {
122
+ return { acting_leases: 0, expired: 0, overhead_per_agent: null, detail: 'no summary supplied' };
123
+ }
124
+ const acting = summary.acting_leases;
125
+ if (acting === 0) {
126
+ return { acting_leases: 0, expired: summary.leases?.expired ?? 0, overhead_per_agent: null, detail: 'no acting leases: the overhead of zero agents is not zero, it is undefined' };
127
+ }
128
+ return {
129
+ acting_leases: acting,
130
+ expired: summary.leases?.expired ?? 0,
131
+ overhead_per_agent: Math.round((summary.leases?.expired ?? 0) / acting * 1000) / 1000,
132
+ detail: `${summary.leases?.expired ?? 0} of ${acting} acting lease(s) never reported`,
133
+ };
134
+ }
135
+
136
+ /** The fleet size actually observed, and the honest statement of what that bounds. */
137
+ export function scaleOf(machineIds) {
138
+ const distinct = [...new Set((Array.isArray(machineIds) ? machineIds : []).filter((x) => typeof x === 'string' && x !== ''))];
139
+ return {
140
+ observed_machines: distinct.length,
141
+ // The ROADMAP's question is about the collapse point, which is beyond anything this can see.
142
+ can_answer_collapse_point: false,
143
+ detail: `this observation covers ${distinct.length} machine(s); the ROADMAP asks about the collapse point near 50, which is outside what ${distinct.length} can show`,
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Extrapolate, or refuse to.
149
+ *
150
+ * The rule this encodes: an extrapolation is only offered when the observation covers **more than a tenth**
151
+ * of the target, and even then it is labelled. The boundary is exclusive deliberately — a sample that is
152
+ * *exactly* 10% is the borderline case, and the borderline case is the one where a refusal costs nothing and
153
+ * an estimate costs credibility. Below the boundary the function returns `null` for the estimate with a
154
+ * reason, because the alternative — a number produced by multiplying a small sample — is exactly the fiction
155
+ * the ROADMAP refuses to write.
156
+ *
157
+ * @param {number} observed agents actually measured
158
+ * @param {number} target agents the estimate would be about
159
+ */
160
+ export function extrapolate(observed, target) {
161
+ if (!Number.isFinite(observed) || !Number.isFinite(target) || observed <= 0 || target <= 0) {
162
+ return { ok: false, estimate: null, detail: 'observed and target must both be positive numbers' };
163
+ }
164
+ const coverage = observed / target;
165
+ if (coverage <= 0.1) {
166
+ return {
167
+ ok: false,
168
+ estimate: null,
169
+ coverage: Math.round(coverage * 1000) / 1000,
170
+ detail: `refusing to extrapolate from ${observed} to ${target}: that is ${(coverage * 100).toFixed(2)}% coverage, and a number produced by scaling a sample that small is fiction with a decimal point on it`,
171
+ };
172
+ }
173
+ return {
174
+ ok: true,
175
+ estimate: target,
176
+ coverage: Math.round(coverage * 1000) / 1000,
177
+ detail: `extrapolating from ${observed} to ${target} at ${(coverage * 100).toFixed(0)}% coverage -- reported as an extrapolation, never as a measurement`,
178
+ };
179
+ }
180
+
181
+ /**
182
+ * The one claim this fleet can make about a ceiling, stated as a falsifiable inequality.
183
+ *
184
+ * Not a predicted collapse point. It is: **at the largest fan-out this fleet has actually exercised, the
185
+ * expiry rate was X**. That is measurable, it is falsifiable by re-running, and it is the only form of the
186
+ * answer the data supports.
187
+ */
188
+ export function ceilingFrom(summary, observedFanOut) {
189
+ if (summary === null || typeof summary !== 'object' || !Number.isFinite(observedFanOut) || observedFanOut <= 0) {
190
+ return { claim: null, detail: 'a ceiling claim needs a summary and the fan-out it was measured at' };
191
+ }
192
+ const rate = summary.expiry_rate;
193
+ if (rate === null) {
194
+ return { claim: null, detail: 'no acting leases, so no expiry rate to state' };
195
+ }
196
+ return {
197
+ claim: `at a fan-out of ${observedFanOut}, the lease expiry rate was ${(rate * 100).toFixed(1)}%`,
198
+ fan_out: observedFanOut,
199
+ expiry_rate: rate,
200
+ // Explicitly not a ceiling. Naming what it is not is the point of the function.
201
+ is_a_ceiling: false,
202
+ detail: 'this is an observation at one fan-out, not a collapse point; a ceiling would need the rate measured at several fan-outs and trending',
203
+ };
204
+ }
@@ -185,7 +185,7 @@ const MAX_STAGES = 16;
185
185
  * `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
186
186
  * overrides it at runtime for tests and packagers.
187
187
  */
188
- const PLUGIN_VERSION = '0.13.0';
188
+ const PLUGIN_VERSION = '0.14.0';
189
189
 
190
190
  /** How long any single HTTP call may take before it is aborted, in milliseconds. */
191
191
  const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;