@twinsearth/w2m-dsh-plugin 0.40.0 → 0.41.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 +51 -0
- package/docs/1.0.0-ACCEPTANCE.md +10 -7
- package/package.json +1 -1
- package/src/plugin/tools.mjs +114 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,57 @@ 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.41.0] — 2026-10-15
|
|
8
|
+
|
|
9
|
+
**`field-authority.mjs` has a caller. The correction from 0.29.0 lands, six versions after the claim.**
|
|
10
|
+
|
|
11
|
+
### What was wired, and the claim it settles
|
|
12
|
+
|
|
13
|
+
0.29.0's entry said *"the connection now exists."* **It existed inside that module and nowhere else** — the
|
|
14
|
+
0.36.0 classification reported the file as an **orphan**, and reading the tree rather than the changelog
|
|
15
|
+
confirmed it: `grep 'field-authority' src/` matched **comments and documentation only.**
|
|
16
|
+
|
|
17
|
+
`w2m_memory` now accepts a received CAT7 block **field by field, at the actor's authority level**, and writes it
|
|
18
|
+
as a row in the same JSONL the conclusions use — one store, one rotation, one answer to *"what does this machine
|
|
19
|
+
remember."* Measured, all six cases:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
no level ok=false MEMORY_BLOCK_NO_AUTHORITY
|
|
23
|
+
level 0 ok=true accepted=[subject,body] refused=[evidence]
|
|
24
|
+
level 70 ok=true accepted=[subject,body,evidence]
|
|
25
|
+
tampered ok=false BLOCK_HASH_MISMATCH
|
|
26
|
+
no ancestor ok=false BLOCK_MISSING_PARENT
|
|
27
|
+
with ancestor ok=true accepted 3/3
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**A refused field is OMITTED, not set to null** — v4.8.0's first falsifying test, now reachable through a tool.
|
|
31
|
+
**An unset level REFUSES rather than accepting the block whole**, because accepting whole is the exact behaviour
|
|
32
|
+
field-level acceptance was built to replace, and it would look like success. **A block naming an ancestor the
|
|
33
|
+
caller cannot produce is refused, never re-rooted**, because re-rooting turns an unverifiable history into no
|
|
34
|
+
history — which reads as a fresh, independent assertion. And **refusals are stored**, not discarded: what a
|
|
35
|
+
machine declined to believe is as much a part of its memory as what it took.
|
|
36
|
+
|
|
37
|
+
### The list policed itself, which is the part worth keeping
|
|
38
|
+
|
|
39
|
+
`field-authority` was removed from `scripts/accepted-orphans.mjs` **not by a decision to tidy up, but because
|
|
40
|
+
the gate went red.** Its stale-entry check fails when an accepted orphan stops being an orphan — so the moment
|
|
41
|
+
`tools.mjs` imported the file, the run demanded the acceptance be withdrawn. **Orphans: 8 → 7. Exports: 72 →
|
|
42
|
+
65.**
|
|
43
|
+
|
|
44
|
+
### My own error, and it is the eighth of this exact shape
|
|
45
|
+
|
|
46
|
+
I inserted the new branch by matching an **anchor string** and never checked **what block that string was
|
|
47
|
+
inside.** It landed inside `if (conclusionText !== null)`, so the feature only ran when the caller *also* passed
|
|
48
|
+
`record`. Six tests failed, and I diagnosed it by **guessing at return shapes twice before printing them** —
|
|
49
|
+
`verifyBlock` and `acceptWithAuthority` were both exactly what I had assumed, and the actual fault was a
|
|
50
|
+
scoping mistake three steps earlier.
|
|
51
|
+
|
|
52
|
+
**Two wrong hypotheses, then one printed value, then ten seconds to the answer.** That is the eighth time in
|
|
53
|
+
this span that printing would have been faster than predicting, and the note is here because the pattern has
|
|
54
|
+
now survived being written down seven times.
|
|
55
|
+
|
|
56
|
+
**6 new tests, 0 failures.**
|
|
57
|
+
|
|
7
58
|
## [0.40.0] — 2026-10-15
|
|
8
59
|
|
|
9
60
|
**The acceptance document, converged — and bound to the pre-flight so it cannot drift.**
|
package/docs/1.0.0-ACCEPTANCE.md
CHANGED
|
@@ -126,7 +126,7 @@ limiter down to `util`, **not** to add the edge.
|
|
|
126
126
|
| 3.7 | Nothing destructive auto-settles, at any level | `mayAutoSettle`, asserted | **yes** |
|
|
127
127
|
| 3.8 | An escalated dispatch reaches a queue an approver can answer | `w2m_approvals`, asserted end to end | **yes** |
|
|
128
128
|
| 3.9 | The queue's front door meters **raising only** | `approval-gate.mjs`, wired in 0.31.0 | **yes** |
|
|
129
|
-
| 3.10 | Field-level memory acceptance is driven by the same model | `field-authority.mjs` | **
|
|
129
|
+
| 3.10 | Field-level memory acceptance is driven by the same model | `field-authority.mjs`, called by `w2m_memory` since 0.41.0 | **yes** — the caller arrived six versions after the mechanism |
|
|
130
130
|
| 3.11 | **The gate is ON by default** | `requireAuthorityLevel`, asserted | **NARROWED — see below** |
|
|
131
131
|
| 3.12 | A level is chosen by name, never typed | `authority-choice.mjs`, asserted | **yes** |
|
|
132
132
|
| 3.13 | A misconfigured level is reported, not silently ignored | `w2m_status.actor_authority` | **yes** |
|
|
@@ -181,7 +181,7 @@ within a week. **A check that cannot fail is a report.**
|
|
|
181
181
|
| 1.2 | every dependency edge is declared, and every declared edge is used | `layer-check` |
|
|
182
182
|
| 1.3 | every suite on disk is named by a script | `test wiring` |
|
|
183
183
|
| 1.4 | every document's claim about the artifact matches the artifact | `claims`, 48 claims |
|
|
184
|
-
| **1.5** | **no orphan module without a written reason** | **`reachable or accepted`** |
|
|
184
|
+
| **1.5** | **no orphan module without a written reason** | **`reachable or accepted`** — **7 orphans, 65 exports, down from 8 and 72** |
|
|
185
185
|
| 2.1–2.4 | ranked layers, an allow-list of edges, no upward edge | `layer-check` |
|
|
186
186
|
| 3.2 | every declared capability is reachable by some band | `auditCapabilities`, asserted |
|
|
187
187
|
| 3.4 | the dispatch path consults the gate | `tools.test.mjs`, end to end |
|
|
@@ -198,9 +198,9 @@ within a week. **A check that cannot fail is a report.**
|
|
|
198
198
|
|
|
199
199
|
| # | Criterion | What is actually measured |
|
|
200
200
|
|---|---|---|
|
|
201
|
-
| 1.6 | **reachability itself** |
|
|
201
|
+
| 1.6 | **reachability itself** | 65 orphan exports, 14 cli. **The classifier cannot tell a defect from a library**, so the *decision* is enforced (§1.5) while the *classification* is not |
|
|
202
202
|
| 3.1 | capabilities declared with a cost | the table is read by `permission.mjs`; a new capability with no caller would still be listed |
|
|
203
|
-
| 3.10 |
|
|
203
|
+
| ~~3.10~~ | ~~field-level memory acceptance driven by the authority model~~ | **PROMOTED TO ENFORCED in 0.41.0.** It was the one `interface-unrouted` orphan whose removal had a known answer, and `w2m_memory` now accepts a received block field by field at the actor's level |
|
|
204
204
|
| 3.11 | the gate is on by default | `requireAuthorityLevel` makes it enforceable **per machine**; the default is still off |
|
|
205
205
|
| 4.1 | four suites run on MacMini | verified once, in 0.28.0, with that machine's exit code. **Nothing re-runs them** |
|
|
206
206
|
|
|
@@ -243,9 +243,12 @@ found something the previous three rounds had missed.**
|
|
|
243
243
|
|
|
244
244
|
1. **Flip §3.11's default.** `requireAuthorityLevel` exists and is tested; whether 1.0.0 ships with it on is a
|
|
245
245
|
decision about who the release is for. **A permission system that is off is one that has never run.**
|
|
246
|
-
2. **Wire the
|
|
247
|
-
name; `log-bounds` is the answer to a measured 40× growth; `storage` holds the undelatable
|
|
248
|
-
|
|
246
|
+
2. **Wire the remaining three `interface-unrouted` orphans**, or delete them. `health` is the sensor the user
|
|
247
|
+
asked for by name; `log-bounds` is the answer to a measured 40× growth; `storage` holds the undelatable
|
|
248
|
+
tiers. **Each is on the list with what would remove it.**
|
|
249
|
+
~~`field-authority`~~ **was the fourth and is DONE — 0.41.0 gave it the caller its entry named.** It is
|
|
250
|
+
worth noting what removed it: **not a decision to tidy up, but the gate's stale-entry check going red** the
|
|
251
|
+
moment something imported the file. The list policed itself.
|
|
249
252
|
3. **Re-run the 69 unverified suites on MacMini**, which needs `node_modules` there — an environment change
|
|
250
253
|
nobody has made.
|
|
251
254
|
4. **Promote every MEASURED row to ENFORCED**, so this document stops being a parallel list that can drift from
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/w2m-dsh-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.41.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",
|
package/src/plugin/tools.mjs
CHANGED
|
@@ -98,6 +98,12 @@ import path from 'node:path';
|
|
|
98
98
|
// skipped rather than fatal, and the rotated generation reachable -- and a second implementation would
|
|
99
99
|
// drift from the writer's shape without anything failing.
|
|
100
100
|
import { appendMemory, readMemorySync } from '../agent/memory.mjs';
|
|
101
|
+
// v0.41.0: v4.8.0's other half. Written in 0.9.9 and 0.29.0, and **nothing called them** until here --
|
|
102
|
+
// `field-authority.mjs` was reported as an orphan in 0.36.0 and stayed on the accepted list until 0.41.0.
|
|
103
|
+
// `makeBlock` is NOT imported: this path receives blocks and only reads them. Building one here would be the
|
|
104
|
+
// plugin asserting a claim on its own behalf, which is the sender's job.
|
|
105
|
+
import { acceptBlock, verifyBlock } from '../agent/blocks.mjs';
|
|
106
|
+
import { acceptWithAuthority } from '../agent/field-authority.mjs';
|
|
101
107
|
|
|
102
108
|
// The base-address rules live in one place (§2), shared with the agent side. Importing the - rather
|
|
103
109
|
// than re-deriving them her - is what stops the plugin and the agent from disagreeing about what
|
|
@@ -202,7 +208,7 @@ const MAX_STAGES = 16;
|
|
|
202
208
|
* `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
|
|
203
209
|
* overrides it at runtime for tests and packagers.
|
|
204
210
|
*/
|
|
205
|
-
const PLUGIN_VERSION = '0.
|
|
211
|
+
const PLUGIN_VERSION = '0.41.0';
|
|
206
212
|
|
|
207
213
|
/** How long any single HTTP call may take before it is aborted, in milliseconds. */
|
|
208
214
|
const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|
|
@@ -5044,6 +5050,29 @@ export async function apply(ctx, config = {}) {
|
|
|
5044
5050
|
'field a machine is not allowed to write itself -- the machine stores the fact, a reader ' +
|
|
5045
5051
|
'asserts the meaning. Written as a new record so nothing already written is rewritten.',
|
|
5046
5052
|
},
|
|
5053
|
+
accept_block: {
|
|
5054
|
+
type: 'object',
|
|
5055
|
+
description:
|
|
5056
|
+
'v0.41.0. Accept a CAT7 cognitive block received from another machine, per field, DRIVEN BY AN ' +
|
|
5057
|
+
'AUTHORITY LEVEL (actor_authority below) rather than accepted whole. A field the level cannot ' +
|
|
5058
|
+
'afford is OMITTED from the stored block rather than set to null, and lineage is checked against ' +
|
|
5059
|
+
'the ancestors you supply: a block whose parent cannot be produced is REFUSED, never silently ' +
|
|
5060
|
+
're-rooted, because re-rooting turns an unverifiable history into no history. Every field decision ' +
|
|
5061
|
+
'is reported, so a refusal is readable rather than mysterious.',
|
|
5062
|
+
},
|
|
5063
|
+
actor_authority: {
|
|
5064
|
+
type: 'number',
|
|
5065
|
+
description:
|
|
5066
|
+
'The authority level, 0..100, for accept_block. Required with it: an unset level means there is ' +
|
|
5067
|
+
'no actor to judge, and accepting a block whole would defeat the point of field-level acceptance.',
|
|
5068
|
+
},
|
|
5069
|
+
lineage: {
|
|
5070
|
+
type: 'array',
|
|
5071
|
+
items: { type: 'string' },
|
|
5072
|
+
description:
|
|
5073
|
+
'The hashes of the ancestors you can actually produce, for accept_block. A block naming an ' +
|
|
5074
|
+
'ancestor outside this list is refused rather than re-rooted.',
|
|
5075
|
+
},
|
|
5047
5076
|
},
|
|
5048
5077
|
output: jsonOutput,
|
|
5049
5078
|
async execute(args, exec) {
|
|
@@ -5119,8 +5148,91 @@ export async function apply(ctx, config = {}) {
|
|
|
5119
5148
|
* text, which is the thing a reader most needs when a conclusion turns out to be wrong.
|
|
5120
5149
|
*/
|
|
5121
5150
|
const conclusionText = typeof args?.record === 'string' && args.record.trim() !== '' ? args.record.trim() : null;
|
|
5151
|
+
/**
|
|
5152
|
+
* v0.41.0: accept a received cognitive block, field by field, driven by the authority model.
|
|
5153
|
+
*
|
|
5154
|
+
* ## Why the block is written as a ROW rather than into a structure of its own
|
|
5155
|
+
*
|
|
5156
|
+
* The memory file is JSONL rows with a \`kind\`. A conclusion is one kind; an accepted block is
|
|
5157
|
+
* another. Giving blocks their own file would mean two stores to read, two to rotate, and two
|
|
5158
|
+
* answers to "what does this machine remember" -- and the rotation in \`appendMemory\` already
|
|
5159
|
+
* handles the size problem a second store would be created to solve.
|
|
5160
|
+
*
|
|
5161
|
+
* ## Why an unset authority level REFUSES rather than accepting everything
|
|
5162
|
+
*
|
|
5163
|
+
* Field-level acceptance exists because a receiver should not adopt everything a sender asserts.
|
|
5164
|
+
* With no level there is no actor to judge, so accepting the block whole would be the exact
|
|
5165
|
+
* behaviour this mode was built to replace -- and it would look like success.
|
|
5166
|
+
*/
|
|
5167
|
+
// Hoisted above both branches that write: block acceptance and the conclusion. One read of the
|
|
5168
|
+
// configured directory, so the two cannot disagree about where memory lives.
|
|
5169
|
+
const stateDirForRecord = cfg.stateDir ?? null;
|
|
5170
|
+
|
|
5171
|
+
if (args?.accept_block !== undefined && args?.accept_block !== null) {
|
|
5172
|
+
if (stateDirForRecord === null) {
|
|
5173
|
+
return JSON.stringify({ ok: false, scope: 'local', reason_code: 'MEMORY_NO_STATE_DIR', detail: 'this plugin has no stateDir configured, so a block has nowhere to be accepted into' });
|
|
5174
|
+
}
|
|
5175
|
+
if (!Number.isInteger(args?.actor_authority)) {
|
|
5176
|
+
return JSON.stringify({
|
|
5177
|
+
ok: false,
|
|
5178
|
+
scope: 'local',
|
|
5179
|
+
reason_code: 'MEMORY_BLOCK_NO_AUTHORITY',
|
|
5180
|
+
detail: 'accept_block needs actor_authority as an integer 0..100; without a level there is no actor to judge the fields, and accepting the block whole is what field-level acceptance exists to prevent',
|
|
5181
|
+
});
|
|
5182
|
+
}
|
|
5183
|
+
const received = args.accept_block;
|
|
5184
|
+
const verified = verifyBlock(received);
|
|
5185
|
+
if (!verified.ok) {
|
|
5186
|
+
// A block that fails its own hash is refused entirely: field-level acceptance is about trust, and
|
|
5187
|
+
// there is nothing to trust about content that does not match what it claims to be.
|
|
5188
|
+
return JSON.stringify({ ok: false, scope: 'local', reason_code: verified.code, detail: verified.detail, verified: false });
|
|
5189
|
+
}
|
|
5190
|
+
const { accepted, refused, decisions } = acceptWithAuthority(
|
|
5191
|
+
received,
|
|
5192
|
+
{ level: args.actor_authority, id: self },
|
|
5193
|
+
{ accept: acceptBlock, ...(Array.isArray(args.lineage) ? { lineage: args.lineage } : {}) },
|
|
5194
|
+
);
|
|
5195
|
+
if (accepted === null) {
|
|
5196
|
+
// Refused as a whole -- a missing ancestor, most likely. Reported with the whole field list, because
|
|
5197
|
+
// "refused" without "why" is the failure this project keeps having to correct.
|
|
5198
|
+
return JSON.stringify({ ok: false, scope: 'local', reason_code: refused[0]?.why?.split(':')[0] ?? 'MEMORY_BLOCK_REFUSED', detail: refused[0]?.why ?? 'the block was refused', refused, decisions });
|
|
5199
|
+
}
|
|
5200
|
+
const row = {
|
|
5201
|
+
at: new Date().toISOString(),
|
|
5202
|
+
kind: 'block',
|
|
5203
|
+
machine_id: selfId,
|
|
5204
|
+
machine_name: self,
|
|
5205
|
+
// The block's own hash identifies what was accepted; the lineage it declared travels with the row, so
|
|
5206
|
+
// a reader can tell an accepted block from a fabricated one without re-deriving anything.
|
|
5207
|
+
block_hash: received.hash ?? null,
|
|
5208
|
+
lineage: Array.isArray(received.lineage) ? [...received.lineage] : [],
|
|
5209
|
+
accepted_by_level: args.actor_authority,
|
|
5210
|
+
accepted,
|
|
5211
|
+
accepted_fields: decisions.filter((d) => d.accepted).map((d) => d.field),
|
|
5212
|
+
// Refusals are stored, not discarded: "what did this machine decline to believe" is as much a part
|
|
5213
|
+
// of its memory as what it took, and the design's whole third test is about dissent surviving.
|
|
5214
|
+
refused_fields: decisions.filter((d) => !d.accepted).map((d) => ({ field: d.field, why: d.reason })),
|
|
5215
|
+
};
|
|
5216
|
+
const written = appendMemory(stateDirForRecord, row);
|
|
5217
|
+
return JSON.stringify({
|
|
5218
|
+
ok: written.ok === true,
|
|
5219
|
+
scope: 'local',
|
|
5220
|
+
recorded: written.ok === true,
|
|
5221
|
+
kind: 'block',
|
|
5222
|
+
block_hash: row.block_hash,
|
|
5223
|
+
accepted_fields: row.accepted_fields,
|
|
5224
|
+
refused_fields: row.refused_fields,
|
|
5225
|
+
accepted,
|
|
5226
|
+
decisions,
|
|
5227
|
+
path: written.path ?? null,
|
|
5228
|
+
reason_code: written.ok === true ? null : 'MEMORY_BLOCK_WRITE_FAILED',
|
|
5229
|
+
detail: written.ok === true
|
|
5230
|
+
? `accepted ${row.accepted_fields.length} field(s) at level ${args.actor_authority}; ${row.refused_fields.length} omitted`
|
|
5231
|
+
: (written.error ?? 'the block could not be written'),
|
|
5232
|
+
});
|
|
5233
|
+
}
|
|
5234
|
+
|
|
5122
5235
|
if (conclusionText !== null) {
|
|
5123
|
-
const stateDirForRecord = cfg.stateDir ?? null;
|
|
5124
5236
|
if (stateDirForRecord === null) {
|
|
5125
5237
|
return JSON.stringify(
|
|
5126
5238
|
{
|