@junghanacs/entwurf 0.23.0 → 0.24.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/AGENTS.md +3 -1
- package/BASELINE.md +2 -0
- package/CHANGELOG.md +307 -0
- package/FAQ.md +47 -0
- package/README.md +5 -3
- package/VERIFY.md +43 -2
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +97 -18
- package/docs/setup-clean-host.md +8 -7
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +33 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +19 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +2 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/callback-env.js +68 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +54 -22
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +7 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +13 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +19 -10
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js +35 -42
- package/mcp/entwurf-bridge/dist/scripts/probe-bridge-command.js +1 -0
- package/mcp/entwurf-bridge/src/index.ts +41 -1
- package/mcp/entwurf-bridge/test.sh +2 -2
- package/package.json +15 -13
- package/pi-extensions/entwurf-control.ts +56 -0
- package/pi-extensions/lib/acp/acp-client.ts +3 -3
- package/pi-extensions/lib/acp/backend-adapter.ts +3 -3
- package/pi-extensions/lib/acp/backend.ts +20 -10
- package/pi-extensions/lib/acp/context.ts +30 -10
- package/pi-extensions/lib/acp/event-mapper.ts +5 -5
- package/pi-extensions/lib/acp/session-store.ts +2 -2
- package/pi-extensions/lib/acp/tool-surface.ts +2 -1
- package/pi-extensions/lib/callback-env.ts +78 -0
- package/pi-extensions/lib/compaction-send-guard.ts +4 -1
- package/pi-extensions/lib/entwurf-v2-contract.ts +2 -2
- package/pi-extensions/lib/entwurf-v2-lock.ts +1 -1
- package/pi-extensions/lib/entwurf-v2-runner.ts +1 -1
- package/pi-extensions/lib/fresh-call-composition.ts +56 -22
- package/pi-extensions/lib/herdr-fresh-call.ts +12 -4
- package/pi-extensions/lib/herdr-placement.ts +13 -4
- package/pi-extensions/lib/mux-fresh-call.ts +23 -8
- package/pi-extensions/meta-bridge-omp.ts +35 -48
- package/run.sh +327 -246
- package/scripts/check-acp-backend-preflight.ts +117 -9
- package/scripts/check-acp-carrier-augment.ts +6 -4
- package/scripts/check-acp-event-mapper.ts +17 -6
- package/scripts/check-acp-prompt-builder.ts +17 -9
- package/scripts/check-acp-prompt-lifecycle.ts +23 -6
- package/scripts/check-acp-session-reuse.ts +64 -51
- package/scripts/check-acp-session-store.ts +31 -8
- package/scripts/check-acp-stop-reason.ts +20 -2
- package/scripts/check-acp-stream-hooks.ts +22 -5
- package/scripts/check-acp-usage-accounting.ts +42 -18
- package/scripts/check-entwurf-bridge-boot.ts +16 -1
- package/scripts/check-fresh-call-dispatch.ts +4 -5
- package/scripts/check-gate-qualification.ts +45 -5
- package/scripts/check-herdr-activation.ts +73 -0
- package/scripts/check-herdr-fresh-call.ts +22 -6
- package/scripts/check-herdr-sandbox.ts +14 -2
- package/scripts/check-install-container.sh +1 -1
- package/scripts/check-pi-launch.ts +361 -0
- package/scripts/check-release-gate-outcomes.ts +7 -0
- package/scripts/inventory-verification-surface.ts +86 -10
- package/scripts/lib/mutation-qualify.ts +1 -1
- package/scripts/mutants/acp-backend-preflight.json +20 -0
- package/scripts/mutants/bridge-boot-resume.json +26 -0
- package/scripts/mutants/herdr-activation.json +16 -0
- package/scripts/mutants/herdr-fresh-call.json +11 -0
- package/scripts/mutants/herdr-plugin-build.json +1 -1
- package/scripts/mutants/mux-fresh-call.json +21 -8
- package/scripts/mutants/omp-fresh.json +5 -9
- package/scripts/mutants/pack-install.json +3 -3
- package/scripts/mutants/pi-launch.json +113 -0
- package/scripts/mutants/resume-args.json +6 -6
- package/scripts/mutants/v2-surface.json +2 -2
- package/scripts/mutants/v2-visible-resume.json +17 -17
- package/scripts/probe-bridge-command.ts +1 -0
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-acp-session-reuse-live.ts +23 -5
- package/scripts/smoke-agy-install-state.sh +2 -2
- package/scripts/smoke-copilot-mcp-state.sh +1 -1
- package/scripts/smoke-entwurf-v2-matrix-live.ts +1 -1
- package/scripts/smoke-herdr-fresh-call-live.ts +56 -28
- package/scripts/smoke-herdr-raw-install-live.sh +877 -0
- package/scripts/smoke-omp-fresh-live.ts +4 -2
- package/scripts/smoke-omp-mcp-state.sh +1 -1
- package/scripts/smoke-pi-provider-state.sh +2 -2
- package/scripts/tsconfig.json +5 -0
- package/scripts/check-entwurf-facts.ts +0 -391
- package/scripts/check-entwurf-resume-args.ts +0 -122
- package/scripts/check-entwurf-v2-decider.ts +0 -657
- package/scripts/check-entwurf-v2-lock.ts +0 -371
- package/scripts/check-entwurf-v2-mailbox.ts +0 -261
- package/scripts/check-entwurf-v2-matrix.ts +0 -401
- package/scripts/check-entwurf-v2-native-push.ts +0 -221
- package/scripts/check-entwurf-v2-release.ts +0 -151
- package/scripts/check-entwurf-v2-runner.ts +0 -448
- package/scripts/check-entwurf-v2-send-fallback.ts +0 -350
- package/scripts/check-entwurf-v2-send.ts +0 -458
- package/scripts/check-entwurf-v2-visible-resume.ts +0 -445
- package/scripts/check-socket-discovery.ts +0 -401
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* check-entwurf-v2-release — deterministic gate for the PURE release-policy reducer
|
|
3
|
-
* (0.11 Stage 0 step 5c-1). Proves the Fable-3 "release-after-observation" timing as
|
|
4
|
-
* a pure state machine BEFORE any send IO exists:
|
|
5
|
-
*
|
|
6
|
-
* 1. decideReleasePolicy maps each transport correctly and ENFORCES the lock
|
|
7
|
-
* invariants (meta-mailbox ⇒ null else throw; in-domain ⇒ non-null AND
|
|
8
|
-
* lock.gardenId === plan.targetGardenId, else throw).
|
|
9
|
-
* 2. no-lock policy NEVER releases — on any event.
|
|
10
|
-
* 3. control-socket holds before send-final, releases EXACTLY ONCE on send-final
|
|
11
|
-
* (every terminal outcome), holds after.
|
|
12
|
-
* 9. single-release: after a release, no later event releases again.
|
|
13
|
-
*
|
|
14
|
-
* No IO — the reducer is pure; the gate folds event sequences and checks the
|
|
15
|
-
* shouldRelease transitions.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import assert from "node:assert/strict";
|
|
19
|
-
import type { ExecutionPlan } from "../pi-extensions/lib/entwurf-v2-decider.ts";
|
|
20
|
-
import type { LockClaim } from "../pi-extensions/lib/entwurf-v2-lock.ts";
|
|
21
|
-
import {
|
|
22
|
-
decideReleasePolicy,
|
|
23
|
-
initialReleaseState,
|
|
24
|
-
type ReleaseEvent,
|
|
25
|
-
type ReleasePolicy,
|
|
26
|
-
reduceRelease,
|
|
27
|
-
} from "../pi-extensions/lib/entwurf-v2-release.ts";
|
|
28
|
-
|
|
29
|
-
let passed = 0;
|
|
30
|
-
function ok(label: string, cond: boolean): void {
|
|
31
|
-
assert.ok(cond, label);
|
|
32
|
-
console.log(` ok ${label}`);
|
|
33
|
-
passed++;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const GID = "20260612T100000-aaaaaa";
|
|
37
|
-
|
|
38
|
-
function lockClaim(): LockClaim {
|
|
39
|
-
return {
|
|
40
|
-
gardenId: GID,
|
|
41
|
-
pid: 4242,
|
|
42
|
-
hostname: "test-host",
|
|
43
|
-
createdAt: "2026-06-12T01:00:00.000Z",
|
|
44
|
-
nonce: "deadbeefcafef00d",
|
|
45
|
-
owner: "entwurf_v2",
|
|
46
|
-
lockPath: `/fake/locks/${GID}.lock`,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const MAILBOX_PLAN: ExecutionPlan = {
|
|
51
|
-
transport: "meta-mailbox",
|
|
52
|
-
action: "send",
|
|
53
|
-
targetGardenId: GID,
|
|
54
|
-
mailboxDir: "/fake/mailbox",
|
|
55
|
-
sessionsDir: "/fake/sessions",
|
|
56
|
-
wantsReply: false,
|
|
57
|
-
message: "m",
|
|
58
|
-
};
|
|
59
|
-
const CONTROL_PLAN: ExecutionPlan = {
|
|
60
|
-
transport: "control-socket",
|
|
61
|
-
action: "send",
|
|
62
|
-
targetGardenId: GID,
|
|
63
|
-
socketPath: "/fake/ctl/s.sock",
|
|
64
|
-
mode: "follow_up",
|
|
65
|
-
wantsReply: false,
|
|
66
|
-
message: "m",
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
// Fold a sequence of events; return the shouldRelease flag emitted per event.
|
|
70
|
-
function fold(policy: ReleasePolicy, events: ReleaseEvent[]): boolean[] {
|
|
71
|
-
let state = initialReleaseState();
|
|
72
|
-
const flags: boolean[] = [];
|
|
73
|
-
for (const ev of events) {
|
|
74
|
-
const r = reduceRelease(policy, state, ev);
|
|
75
|
-
state = r.state;
|
|
76
|
-
flags.push(r.shouldRelease);
|
|
77
|
-
}
|
|
78
|
-
return flags;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function throws(fn: () => unknown): boolean {
|
|
82
|
-
try {
|
|
83
|
-
fn();
|
|
84
|
-
return false;
|
|
85
|
-
} catch {
|
|
86
|
-
return true;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// ── 1: decideReleasePolicy mapping + lock-nullness enforcement ───────────────
|
|
91
|
-
ok("policy: meta-mailbox + null → no-lock", decideReleasePolicy(MAILBOX_PLAN, null).kind === "no-lock");
|
|
92
|
-
ok(
|
|
93
|
-
"policy: control-socket + lock → release-after-send-final",
|
|
94
|
-
decideReleasePolicy(CONTROL_PLAN, lockClaim()).kind === "release-after-send-final",
|
|
95
|
-
);
|
|
96
|
-
ok(
|
|
97
|
-
"policy: meta-mailbox + lock → throws (?7 violated)",
|
|
98
|
-
throws(() => decideReleasePolicy(MAILBOX_PLAN, lockClaim())),
|
|
99
|
-
);
|
|
100
|
-
ok(
|
|
101
|
-
"policy: control-socket + null → throws (must hold lock)",
|
|
102
|
-
throws(() => decideReleasePolicy(CONTROL_PLAN, null)),
|
|
103
|
-
);
|
|
104
|
-
// in-domain lock whose gardenId ≠ plan target = mis-paired plan/lock (same grade as
|
|
105
|
-
// a null lock — a later release would free a DIFFERENT gid). Fail loud.
|
|
106
|
-
const WRONG_GID = "20260612T999999-bbbbbb";
|
|
107
|
-
ok(
|
|
108
|
-
"policy: control-socket + mismatched lock gid → throws",
|
|
109
|
-
throws(() => decideReleasePolicy(CONTROL_PLAN, { ...lockClaim(), gardenId: WRONG_GID })),
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
// ── 2: no-lock NEVER releases ────────────────────────────────────────────────
|
|
113
|
-
{
|
|
114
|
-
const policy: ReleasePolicy = { kind: "no-lock" };
|
|
115
|
-
const flags = fold(policy, [{ kind: "mailbox-enqueued" }, { kind: "send-final", outcome: "sent" }]);
|
|
116
|
-
ok(
|
|
117
|
-
"no-lock: never releases on any event",
|
|
118
|
-
flags.every((f) => f === false),
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// ── 3: control-socket holds before send-final, releases once, holds after ────
|
|
123
|
-
{
|
|
124
|
-
const policy: ReleasePolicy = { kind: "release-after-send-final" };
|
|
125
|
-
for (const outcome of ["sent", "fallback-sent", "rejected", "failed"] as const) {
|
|
126
|
-
const flags = fold(policy, [
|
|
127
|
-
{ kind: "send-final", outcome },
|
|
128
|
-
{ kind: "send-final", outcome },
|
|
129
|
-
]);
|
|
130
|
-
ok(`control: send-final(${outcome}) releases exactly once`, flags[0] === true && flags[1] === false);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// ── 9: single-release — after release, no later event releases again ─────────
|
|
135
|
-
// Restated on the control-socket policy: the spawn observation events this used to fold
|
|
136
|
-
// went with their transport, but the at-most-once guarantee is the reducer's own and must
|
|
137
|
-
// still be proven against repeated terminal events.
|
|
138
|
-
{
|
|
139
|
-
const policy: ReleasePolicy = { kind: "release-after-send-final" };
|
|
140
|
-
const flags = fold(policy, [
|
|
141
|
-
{ kind: "send-final", outcome: "sent" },
|
|
142
|
-
{ kind: "send-final", outcome: "fallback-sent" },
|
|
143
|
-
{ kind: "send-final", outcome: "failed" },
|
|
144
|
-
]);
|
|
145
|
-
ok(
|
|
146
|
-
"single-release: exactly one release across many terminal events",
|
|
147
|
-
flags.filter((f) => f).length === 1 && flags[0] === true,
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
console.log(`\n[check-entwurf-v2-release] ${passed} assertions ok`);
|
|
@@ -1,448 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* check-entwurf-v2-runner — deterministic gate for the 5d-1 execute-router
|
|
3
|
-
* (`executeDispatch`). It proves the DECISION→hand routing + the outcome-rich result
|
|
4
|
-
* mapping over injected fake hands, with NO socket or timer:
|
|
5
|
-
*
|
|
6
|
-
* 1. reject decision → kind "rejected", receipt+diagnostic carried, NO hand called.
|
|
7
|
-
* 2. control-socket execute → sendControl(plan, lock) called with the SAME plan + lock;
|
|
8
|
-
* result outcome "sent" → executed{control-socket, outcome:"sent"}.
|
|
9
|
-
* 4. meta-mailbox execute → sendMailbox(plan, null) with the NULL lock (?7); success
|
|
10
|
-
* → executed{meta-mailbox, success:true}.
|
|
11
|
-
* 5. N3 — control rejected with rejectReason → executed{outcome:"rejected", rejectReason}
|
|
12
|
-
* carried verbatim.
|
|
13
|
-
* 6. N1 — control hand throws SendDeliveredReleaseFailedError → execution-failed with
|
|
14
|
-
* finalizedOutcome + releaseFailed:true + retrySafe:false.
|
|
15
|
-
* 7. control hand throws a PLAIN transport error → execution-failed, retrySafe:false,
|
|
16
|
-
* NO finalizedOutcome / releaseFailed (a failed send, lock already released).
|
|
17
|
-
* 8. the mailbox hand throws → execution-failed, retrySafe:false.
|
|
18
|
-
* 9. mailbox hand returns {success:false} → CONTRACT VIOLATION (a mailbox has no
|
|
19
|
-
* in-band reject) → fail loud → execution-failed, NOT a silent success.
|
|
20
|
-
* (exactly ONE hand runs per execute — the other two are never called — is asserted
|
|
21
|
-
* inline in cases 2/3/4.)
|
|
22
|
-
*
|
|
23
|
-
* 5d-2a adds the `runEntwurfV2` COMPOSITION gate (cases 10–14): `decide → execute` joined
|
|
24
|
-
* over a FAKE `decide` (not the real decider — that is check-entwurf-v2-decider's job):
|
|
25
|
-
* 10. decide called EXACTLY once, with the SAME input.
|
|
26
|
-
* 11. reject decision → no executor hand called, the rejected result returned.
|
|
27
|
-
* 12. execute decision → the matching executor hand ran (control/mailbox).
|
|
28
|
-
* 13. decide THROWS → propagates (no decision = no receipt to wrap).
|
|
29
|
-
* 14. executeDispatch result returned VERBATIM (passthrough, no re-wrapping).
|
|
30
|
-
*
|
|
31
|
-
* No real IO — fake hands record (plan, lock) so "the decided plan + lock reach the
|
|
32
|
-
* matching hand, and only that hand" is asserted structurally.
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
|
-
import assert from "node:assert/strict";
|
|
36
|
-
import type {
|
|
37
|
-
DispatchDecision,
|
|
38
|
-
DispatchInput,
|
|
39
|
-
ExecutionPlan,
|
|
40
|
-
RejectReceipt,
|
|
41
|
-
SuccessReceipt,
|
|
42
|
-
} from "../pi-extensions/lib/entwurf-v2-decider.ts";
|
|
43
|
-
import type { LockClaim } from "../pi-extensions/lib/entwurf-v2-lock.ts";
|
|
44
|
-
import type { NativePushPlan, NativePushSendResult } from "../pi-extensions/lib/entwurf-v2-native-push.ts";
|
|
45
|
-
import {
|
|
46
|
-
type DispatchExecutorDeps,
|
|
47
|
-
type EntwurfV2RunDeps,
|
|
48
|
-
executeDispatch,
|
|
49
|
-
runEntwurfV2,
|
|
50
|
-
} from "../pi-extensions/lib/entwurf-v2-runner.ts";
|
|
51
|
-
import type {
|
|
52
|
-
ControlSocketPlan,
|
|
53
|
-
ControlSocketSendResult,
|
|
54
|
-
MetaMailboxPlan,
|
|
55
|
-
RpcSendResult,
|
|
56
|
-
} from "../pi-extensions/lib/entwurf-v2-send.ts";
|
|
57
|
-
import { SendDeliveredReleaseFailedError } from "../pi-extensions/lib/entwurf-v2-send.ts";
|
|
58
|
-
|
|
59
|
-
let passed = 0;
|
|
60
|
-
function ok(label: string, cond: boolean): void {
|
|
61
|
-
assert.ok(cond, label);
|
|
62
|
-
console.log(` ok ${label}`);
|
|
63
|
-
passed++;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const GID = "20260613T100000-aaaaaa";
|
|
67
|
-
|
|
68
|
-
function lockClaim(gardenId = GID): LockClaim {
|
|
69
|
-
return {
|
|
70
|
-
gardenId,
|
|
71
|
-
pid: 4242,
|
|
72
|
-
hostname: "test-host",
|
|
73
|
-
createdAt: "2026-06-13T01:00:00.000Z",
|
|
74
|
-
nonce: "deadbeefcafef00d",
|
|
75
|
-
owner: "entwurf_v2",
|
|
76
|
-
lockPath: `/fake/locks/${gardenId}.lock`,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const CONTROL_PLAN: ControlSocketPlan = {
|
|
81
|
-
transport: "control-socket",
|
|
82
|
-
action: "send",
|
|
83
|
-
targetGardenId: GID,
|
|
84
|
-
socketPath: "/fake/ctl/s.sock",
|
|
85
|
-
mode: "follow_up",
|
|
86
|
-
wantsReply: false,
|
|
87
|
-
message: "m",
|
|
88
|
-
};
|
|
89
|
-
const MAILBOX_PLAN: MetaMailboxPlan = {
|
|
90
|
-
transport: "meta-mailbox",
|
|
91
|
-
action: "send",
|
|
92
|
-
targetGardenId: GID,
|
|
93
|
-
mailboxDir: "/fake/mailbox",
|
|
94
|
-
sessionsDir: "/fake/sessions",
|
|
95
|
-
wantsReply: false,
|
|
96
|
-
message: "m",
|
|
97
|
-
};
|
|
98
|
-
const NATIVE_PUSH_PLAN: NativePushPlan = {
|
|
99
|
-
transport: "native-push",
|
|
100
|
-
action: "send",
|
|
101
|
-
targetGardenId: GID,
|
|
102
|
-
backend: "antigravity",
|
|
103
|
-
nativeSessionId: "conv-xyz",
|
|
104
|
-
route: { backend: "antigravity", lsAddress: "127.0.0.1:5599" },
|
|
105
|
-
wantsReply: false,
|
|
106
|
-
message: "m",
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
const SUCCESS_RECEIPT: SuccessReceipt = {
|
|
110
|
-
ok: true,
|
|
111
|
-
action: "send",
|
|
112
|
-
transport: "control-socket",
|
|
113
|
-
ownership: "ack-only",
|
|
114
|
-
observedLiveness: "alive",
|
|
115
|
-
};
|
|
116
|
-
const REJECT_RECEIPT: RejectReceipt = { ok: false, reason: "bad-target", observedLiveness: null };
|
|
117
|
-
|
|
118
|
-
function executeDecision(plan: ExecutionPlan, lock: LockClaim | null): Extract<DispatchDecision, { kind: "execute" }> {
|
|
119
|
-
return { kind: "execute", receipt: SUCCESS_RECEIPT, plan, lock };
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
interface Trace {
|
|
123
|
-
calls: string[];
|
|
124
|
-
controlArgs?: { plan: ControlSocketPlan; lock: LockClaim | null };
|
|
125
|
-
mailboxArgs?: { plan: MetaMailboxPlan; lock: LockClaim | null };
|
|
126
|
-
nativePushArgs?: { plan: NativePushPlan; lock: LockClaim | null };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
interface FakeSpec {
|
|
130
|
-
control?: { result: ControlSocketSendResult } | { throw: unknown };
|
|
131
|
-
mailbox?: { result: RpcSendResult } | { throw: unknown };
|
|
132
|
-
nativePush?: { result: NativePushSendResult } | { throw: unknown };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function makeDeps(spec: FakeSpec): { deps: DispatchExecutorDeps; trace: Trace } {
|
|
136
|
-
const trace: Trace = { calls: [] };
|
|
137
|
-
const deps: DispatchExecutorDeps = {
|
|
138
|
-
async sendControl(plan, lock) {
|
|
139
|
-
trace.calls.push("sendControl");
|
|
140
|
-
trace.controlArgs = { plan, lock };
|
|
141
|
-
if (!spec.control) throw new Error("test: sendControl called but no spec");
|
|
142
|
-
if ("throw" in spec.control) throw spec.control.throw;
|
|
143
|
-
return spec.control.result;
|
|
144
|
-
},
|
|
145
|
-
async sendMailbox(plan, lock) {
|
|
146
|
-
trace.calls.push("sendMailbox");
|
|
147
|
-
trace.mailboxArgs = { plan, lock };
|
|
148
|
-
if (!spec.mailbox) throw new Error("test: sendMailbox called but no spec");
|
|
149
|
-
if ("throw" in spec.mailbox) throw spec.mailbox.throw;
|
|
150
|
-
return spec.mailbox.result;
|
|
151
|
-
},
|
|
152
|
-
async sendNativePush(plan, lock) {
|
|
153
|
-
trace.calls.push("sendNativePush");
|
|
154
|
-
trace.nativePushArgs = { plan, lock };
|
|
155
|
-
if (!spec.nativePush) throw new Error("test: sendNativePush called but no spec");
|
|
156
|
-
if ("throw" in spec.nativePush) throw spec.nativePush.throw;
|
|
157
|
-
return spec.nativePush.result;
|
|
158
|
-
},
|
|
159
|
-
};
|
|
160
|
-
return { deps, trace };
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
async function main(): Promise<void> {
|
|
164
|
-
// ── 1: reject → no hand called, receipt+diagnostic carried ────────────────
|
|
165
|
-
{
|
|
166
|
-
const { deps, trace } = makeDeps({});
|
|
167
|
-
const conflict = {
|
|
168
|
-
reason: "target-locked" as const,
|
|
169
|
-
lockPath: "/fake/locks/x.lock",
|
|
170
|
-
holder: lockClaim(),
|
|
171
|
-
detail: "held",
|
|
172
|
-
};
|
|
173
|
-
const decision: DispatchDecision = {
|
|
174
|
-
kind: "reject",
|
|
175
|
-
receipt: REJECT_RECEIPT,
|
|
176
|
-
diagnostic: { kind: "target-locked", conflict },
|
|
177
|
-
};
|
|
178
|
-
const res = await executeDispatch(decision, deps);
|
|
179
|
-
ok("1: reject → kind 'rejected'", res.kind === "rejected");
|
|
180
|
-
ok("1: reject → receipt carried", res.kind === "rejected" && res.receipt === REJECT_RECEIPT);
|
|
181
|
-
ok("1: reject → diagnostic carried", res.kind === "rejected" && res.diagnostic?.kind === "target-locked");
|
|
182
|
-
ok("1: reject → NO hand called", trace.calls.length === 0);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// ── 2: control-socket execute → sendControl(plan, lock), sent ─────────────
|
|
186
|
-
{
|
|
187
|
-
const lock = lockClaim();
|
|
188
|
-
const { deps, trace } = makeDeps({ control: { result: { outcome: "sent" } } });
|
|
189
|
-
const res = await executeDispatch(executeDecision(CONTROL_PLAN, lock), deps);
|
|
190
|
-
ok("2: only sendControl ran", trace.calls.length === 1 && trace.calls[0] === "sendControl");
|
|
191
|
-
ok(
|
|
192
|
-
"2: same plan + lock reach the hand",
|
|
193
|
-
trace.controlArgs?.plan === CONTROL_PLAN && trace.controlArgs?.lock === lock,
|
|
194
|
-
);
|
|
195
|
-
ok(
|
|
196
|
-
"2: executed{control-socket, sent}",
|
|
197
|
-
res.kind === "executed" && res.outcome.transport === "control-socket" && res.outcome.outcome === "sent",
|
|
198
|
-
);
|
|
199
|
-
ok("2: success receipt carried", res.kind === "executed" && res.receipt === SUCCESS_RECEIPT);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// ── 4: meta-mailbox execute → sendMailbox(plan, null), success ────────────
|
|
203
|
-
{
|
|
204
|
-
const { deps, trace } = makeDeps({ mailbox: { result: { success: true } } });
|
|
205
|
-
const res = await executeDispatch(executeDecision(MAILBOX_PLAN, null), deps);
|
|
206
|
-
ok("4: only sendMailbox ran", trace.calls.length === 1 && trace.calls[0] === "sendMailbox");
|
|
207
|
-
ok(
|
|
208
|
-
"4: NULL lock passed verbatim (?7)",
|
|
209
|
-
trace.mailboxArgs?.lock === null && trace.mailboxArgs?.plan === MAILBOX_PLAN,
|
|
210
|
-
);
|
|
211
|
-
ok(
|
|
212
|
-
"4: executed{meta-mailbox, success}",
|
|
213
|
-
res.kind === "executed" && res.outcome.transport === "meta-mailbox" && res.outcome.success === true,
|
|
214
|
-
);
|
|
215
|
-
// #98 R: a dep that reports no path leaves the outcome's receipt undefined. The
|
|
216
|
-
// runner must not invent one — a guessed path is worse than no path.
|
|
217
|
-
ok(
|
|
218
|
-
"4: no enqueue receipt from the dep → messagePath stays undefined (never invented)",
|
|
219
|
-
res.kind === "executed" && res.outcome.transport === "meta-mailbox" && res.outcome.messagePath === undefined,
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// ── 4a: #98 R — the mailbox enqueue receipt is carried through verbatim ───
|
|
224
|
-
{
|
|
225
|
-
const { deps } = makeDeps({ mailbox: { result: { success: true, messagePath: "/fake/mailbox/g/2026.msg" } } });
|
|
226
|
-
const res = await executeDispatch(executeDecision(MAILBOX_PLAN, null), deps);
|
|
227
|
-
ok(
|
|
228
|
-
"4a: executed{meta-mailbox} carries the dep's messagePath verbatim",
|
|
229
|
-
res.kind === "executed" &&
|
|
230
|
-
res.outcome.transport === "meta-mailbox" &&
|
|
231
|
-
res.outcome.messagePath === "/fake/mailbox/g/2026.msg",
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// ── 4b: native-push execute → sendNativePush(plan, null), lock-free (봉인 4) ──
|
|
236
|
-
{
|
|
237
|
-
const { deps, trace } = makeDeps({ nativePush: { result: { success: true, retried: false } } });
|
|
238
|
-
const res = await executeDispatch(executeDecision(NATIVE_PUSH_PLAN, null), deps);
|
|
239
|
-
ok("4b: only sendNativePush ran", trace.calls.length === 1 && trace.calls[0] === "sendNativePush");
|
|
240
|
-
ok(
|
|
241
|
-
"4b: NULL lock passed verbatim (lock-free rail)",
|
|
242
|
-
trace.nativePushArgs?.lock === null && trace.nativePushArgs?.plan === NATIVE_PUSH_PLAN,
|
|
243
|
-
);
|
|
244
|
-
ok(
|
|
245
|
-
"4b: executed{native-push, success, retried:false}",
|
|
246
|
-
res.kind === "executed" &&
|
|
247
|
-
res.outcome.transport === "native-push" &&
|
|
248
|
-
res.outcome.success === true &&
|
|
249
|
-
res.outcome.retried === false,
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// ── 4c: native-push retried flag surfaces onto the executed outcome ──────────
|
|
254
|
-
{
|
|
255
|
-
const { deps } = makeDeps({ nativePush: { result: { success: true, retried: true } } });
|
|
256
|
-
const res = await executeDispatch(executeDecision(NATIVE_PUSH_PLAN, null), deps);
|
|
257
|
-
ok(
|
|
258
|
-
"4c: retried:true carried onto executed{native-push}",
|
|
259
|
-
res.kind === "executed" && res.outcome.transport === "native-push" && res.outcome.retried === true,
|
|
260
|
-
);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// ── 4d: native-push hand THROWS → execution-failed, retry-unsafe ─────────────
|
|
264
|
-
{
|
|
265
|
-
const { deps } = makeDeps({ nativePush: { throw: new Error("agy send boom") } });
|
|
266
|
-
const res = await executeDispatch(executeDecision(NATIVE_PUSH_PLAN, null), deps);
|
|
267
|
-
ok(
|
|
268
|
-
"4d: native-push throw → execution-failed (retrySafe:false)",
|
|
269
|
-
res.kind === "execution-failed" && res.transport === "native-push" && res.retrySafe === false,
|
|
270
|
-
);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// ── 5: N3 — control rejected with rejectReason carried ────────────────────
|
|
274
|
-
{
|
|
275
|
-
const { deps } = makeDeps({
|
|
276
|
-
control: { result: { outcome: "rejected", rejectReason: "dormant-fire-forget-unsupported" } },
|
|
277
|
-
});
|
|
278
|
-
const res = await executeDispatch(executeDecision(CONTROL_PLAN, lockClaim()), deps);
|
|
279
|
-
ok(
|
|
280
|
-
"5: N3 rejectReason carried onto executed result",
|
|
281
|
-
res.kind === "executed" &&
|
|
282
|
-
res.outcome.transport === "control-socket" &&
|
|
283
|
-
res.outcome.rejectReason === "dormant-fire-forget-unsupported",
|
|
284
|
-
);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// ── 6: N1 — SendDeliveredReleaseFailedError → execution-failed, no-retry ───
|
|
288
|
-
{
|
|
289
|
-
const releaseErr = new Error("release boom");
|
|
290
|
-
const { deps } = makeDeps({ control: { throw: new SendDeliveredReleaseFailedError("sent", releaseErr) } });
|
|
291
|
-
const res = await executeDispatch(executeDecision(CONTROL_PLAN, lockClaim()), deps);
|
|
292
|
-
ok("6: N1 → execution-failed", res.kind === "execution-failed");
|
|
293
|
-
ok(
|
|
294
|
-
"6: N1 → finalizedOutcome 'sent' + releaseFailed + retrySafe:false",
|
|
295
|
-
res.kind === "execution-failed" &&
|
|
296
|
-
res.finalizedOutcome === "sent" &&
|
|
297
|
-
res.releaseFailed === true &&
|
|
298
|
-
res.retrySafe === false,
|
|
299
|
-
);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// ── 7: control PLAIN throw → execution-failed, no finalizedOutcome ────────
|
|
303
|
-
{
|
|
304
|
-
const { deps } = makeDeps({ control: { throw: new Error("connect indeterminate / dep boom") } });
|
|
305
|
-
const res = await executeDispatch(executeDecision(CONTROL_PLAN, lockClaim()), deps);
|
|
306
|
-
ok(
|
|
307
|
-
"7: plain throw → execution-failed, retrySafe:false",
|
|
308
|
-
res.kind === "execution-failed" && res.retrySafe === false,
|
|
309
|
-
);
|
|
310
|
-
ok(
|
|
311
|
-
"7: plain throw → no finalizedOutcome / releaseFailed",
|
|
312
|
-
res.kind === "execution-failed" && res.finalizedOutcome === undefined && res.releaseFailed === undefined,
|
|
313
|
-
);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// ── 8: the mailbox hand throws → execution-failed ─────────────────────────
|
|
317
|
-
{
|
|
318
|
-
const m = makeDeps({ mailbox: { throw: new Error("enqueue boom") } });
|
|
319
|
-
const rm = await executeDispatch(executeDecision(MAILBOX_PLAN, null), m.deps);
|
|
320
|
-
ok(
|
|
321
|
-
"8: mailbox throw → execution-failed",
|
|
322
|
-
rm.kind === "execution-failed" && rm.transport === "meta-mailbox" && rm.retrySafe === false,
|
|
323
|
-
);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// ── 9: mailbox returns {success:false} → CONTRACT VIOLATION, fail loud ─────
|
|
327
|
-
// 5c-4: a mailbox enqueue is {success:true} OR a throw — there is no in-band reject.
|
|
328
|
-
// A success:false must NOT be silently rendered as a success; the runner fails loud.
|
|
329
|
-
{
|
|
330
|
-
const { deps } = makeDeps({ mailbox: { result: { success: false, error: "should never happen" } } });
|
|
331
|
-
const res = await executeDispatch(executeDecision(MAILBOX_PLAN, null), deps);
|
|
332
|
-
ok(
|
|
333
|
-
"9: mailbox success:false → execution-failed (not a silent success)",
|
|
334
|
-
res.kind === "execution-failed" && res.transport === "meta-mailbox" && res.retrySafe === false,
|
|
335
|
-
);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// ── 10–14: runEntwurfV2 composition (5d-2a) over a FAKE decide ─────────────
|
|
339
|
-
const DISPATCH_INPUT: DispatchInput = {
|
|
340
|
-
target: GID,
|
|
341
|
-
intent: "fire-and-forget",
|
|
342
|
-
mode: "follow_up",
|
|
343
|
-
wantsReply: false,
|
|
344
|
-
message: "m",
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
// A fake decide records how it was called and returns a scripted decision (or throws).
|
|
348
|
-
function makeRunDeps(
|
|
349
|
-
decideSpec: { decision: DispatchDecision } | { throw: unknown },
|
|
350
|
-
execSpec: FakeSpec,
|
|
351
|
-
): { deps: EntwurfV2RunDeps; decideCalls: DispatchInput[]; trace: Trace } {
|
|
352
|
-
const { deps: executor, trace } = makeDeps(execSpec);
|
|
353
|
-
const decideCalls: DispatchInput[] = [];
|
|
354
|
-
const deps: EntwurfV2RunDeps = {
|
|
355
|
-
decide(input) {
|
|
356
|
-
decideCalls.push(input);
|
|
357
|
-
if ("throw" in decideSpec) throw decideSpec.throw;
|
|
358
|
-
return decideSpec.decision;
|
|
359
|
-
},
|
|
360
|
-
executor,
|
|
361
|
-
};
|
|
362
|
-
return { deps, decideCalls, trace };
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// ── 10: decide called exactly once, with the same input ───────────────────
|
|
366
|
-
{
|
|
367
|
-
const decision: DispatchDecision = { kind: "reject", receipt: REJECT_RECEIPT };
|
|
368
|
-
const { deps, decideCalls } = makeRunDeps({ decision }, {});
|
|
369
|
-
await runEntwurfV2(DISPATCH_INPUT, deps);
|
|
370
|
-
ok("10: decide called exactly once", decideCalls.length === 1);
|
|
371
|
-
ok("10: decide got the SAME input", decideCalls[0] === DISPATCH_INPUT);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// ── 11: reject decision → no hand called, rejected result returned ────────
|
|
375
|
-
{
|
|
376
|
-
const decision: DispatchDecision = { kind: "reject", receipt: REJECT_RECEIPT };
|
|
377
|
-
const { deps, trace } = makeRunDeps({ decision }, {});
|
|
378
|
-
const res = await runEntwurfV2(DISPATCH_INPUT, deps);
|
|
379
|
-
ok("11: reject → rejected result", res.kind === "rejected" && res.receipt === REJECT_RECEIPT);
|
|
380
|
-
ok("11: reject → NO executor hand called", trace.calls.length === 0);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// ── 12: execute decision → the matching hand ran (per transport) ──────────
|
|
384
|
-
{
|
|
385
|
-
const lock = lockClaim();
|
|
386
|
-
const ctl = makeRunDeps(
|
|
387
|
-
{ decision: executeDecision(CONTROL_PLAN, lock) },
|
|
388
|
-
{ control: { result: { outcome: "sent" } } },
|
|
389
|
-
);
|
|
390
|
-
const rc = await runEntwurfV2(DISPATCH_INPUT, ctl.deps);
|
|
391
|
-
ok(
|
|
392
|
-
"12: control execute → sendControl ran, executed{control-socket}",
|
|
393
|
-
ctl.trace.calls.length === 1 &&
|
|
394
|
-
ctl.trace.calls[0] === "sendControl" &&
|
|
395
|
-
rc.kind === "executed" &&
|
|
396
|
-
rc.outcome.transport === "control-socket",
|
|
397
|
-
);
|
|
398
|
-
const mbx = makeRunDeps(
|
|
399
|
-
{ decision: executeDecision(MAILBOX_PLAN, null) },
|
|
400
|
-
{ mailbox: { result: { success: true } } },
|
|
401
|
-
);
|
|
402
|
-
const rmb = await runEntwurfV2(DISPATCH_INPUT, mbx.deps);
|
|
403
|
-
ok(
|
|
404
|
-
"12: mailbox execute → sendMailbox ran, executed{meta-mailbox}",
|
|
405
|
-
mbx.trace.calls.length === 1 &&
|
|
406
|
-
mbx.trace.calls[0] === "sendMailbox" &&
|
|
407
|
-
rmb.kind === "executed" &&
|
|
408
|
-
rmb.outcome.transport === "meta-mailbox",
|
|
409
|
-
);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// ── 13: decide THROWS → propagates (no decision = no receipt to wrap) ─────
|
|
413
|
-
{
|
|
414
|
-
const boom = new Error("decider boom");
|
|
415
|
-
const { deps, trace } = makeRunDeps({ throw: boom }, {});
|
|
416
|
-
let caught: unknown;
|
|
417
|
-
try {
|
|
418
|
-
await runEntwurfV2(DISPATCH_INPUT, deps);
|
|
419
|
-
} catch (err) {
|
|
420
|
-
caught = err;
|
|
421
|
-
}
|
|
422
|
-
ok("13: decide throw → propagates the SAME error", caught === boom);
|
|
423
|
-
ok("13: decide throw → NO executor hand called", trace.calls.length === 0);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// ── 14: executeDispatch result returned VERBATIM (passthrough) ────────────
|
|
427
|
-
// A control hand throwing the N1 error must surface AS execution-failed{...} through
|
|
428
|
-
// runEntwurfV2 unchanged — the runner adds no wrapping over executeDispatch's mapping.
|
|
429
|
-
{
|
|
430
|
-
const releaseErr = new Error("release boom");
|
|
431
|
-
const { deps } = makeRunDeps(
|
|
432
|
-
{ decision: executeDecision(CONTROL_PLAN, lockClaim()) },
|
|
433
|
-
{ control: { throw: new SendDeliveredReleaseFailedError("sent", releaseErr) } },
|
|
434
|
-
);
|
|
435
|
-
const res = await runEntwurfV2(DISPATCH_INPUT, deps);
|
|
436
|
-
ok(
|
|
437
|
-
"14: N1 surfaces verbatim → execution-failed{finalizedOutcome,releaseFailed,retrySafe:false}",
|
|
438
|
-
res.kind === "execution-failed" &&
|
|
439
|
-
res.finalizedOutcome === "sent" &&
|
|
440
|
-
res.releaseFailed === true &&
|
|
441
|
-
res.retrySafe === false,
|
|
442
|
-
);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
console.log(`\ncheck-entwurf-v2-runner: ${passed} checks passed`);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
await main();
|