@junghanacs/entwurf 0.12.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 +240 -0
- package/BASELINE.md +227 -0
- package/CHANGELOG.md +1210 -0
- package/CONTRIBUTING.md +63 -0
- package/DELIVERY.md +209 -0
- package/LICENSE +21 -0
- package/README.md +504 -0
- package/VERIFY.md +260 -0
- package/demo/README.md +188 -0
- package/demo/demo-baseline.sh +156 -0
- package/demo/demo.sh +183 -0
- package/docs/assets/entwurf-demo.gif +0 -0
- package/docs/assets/entwurf-doomemacs.gif +0 -0
- package/docs/assets/entwurf-entwurf.gif +0 -0
- package/docs/assets/entwurf-hero.jpg +0 -0
- package/docs/setup-clean-host.md +305 -0
- package/mcp/entwurf-bridge/src/index.ts +513 -0
- package/mcp/entwurf-bridge/start.sh +25 -0
- package/mcp/entwurf-bridge/test.sh +54 -0
- package/mcp/tsconfig.json +29 -0
- package/package.json +130 -0
- package/pi/entwurf-capabilities.json +9 -0
- package/pi/entwurf-targets.json +20 -0
- package/pi/meta-bridge/.claude-plugin/marketplace.json +12 -0
- package/pi/meta-bridge/entwurf-meta-receive/.claude-plugin/plugin.json +5 -0
- package/pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json +50 -0
- package/pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh +54 -0
- package/pi/settings.reference.json +43 -0
- package/pi/skill-plugin-example/.claude-plugin/plugin.json +4 -0
- package/pi/skill-plugin-example/skills/hello/SKILL.md +18 -0
- package/pi-extensions/acp-provider.ts +63 -0
- package/pi-extensions/entwurf-control.ts +1692 -0
- package/pi-extensions/lib/acp/acp-client.ts +90 -0
- package/pi-extensions/lib/acp/augment.ts +238 -0
- package/pi-extensions/lib/acp/backend-adapter.ts +319 -0
- package/pi-extensions/lib/acp/backend.ts +873 -0
- package/pi-extensions/lib/acp/config.ts +552 -0
- package/pi-extensions/lib/acp/context.ts +177 -0
- package/pi-extensions/lib/acp/engraving.ts +123 -0
- package/pi-extensions/lib/acp/event-mapper.ts +339 -0
- package/pi-extensions/lib/acp/models.ts +102 -0
- package/pi-extensions/lib/acp/overlay.ts +220 -0
- package/pi-extensions/lib/acp/prompts/engraving.md +1 -0
- package/pi-extensions/lib/acp/session-store.ts +418 -0
- package/pi-extensions/lib/acp/tool-surface.ts +184 -0
- package/pi-extensions/lib/entwurf-control-rpc.ts +208 -0
- package/pi-extensions/lib/entwurf-core.ts +2033 -0
- package/pi-extensions/lib/entwurf-deliverability.ts +123 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +158 -0
- package/pi-extensions/lib/entwurf-facts.ts +251 -0
- package/pi-extensions/lib/entwurf-mailbox-guard.ts +100 -0
- package/pi-extensions/lib/entwurf-peers-render.ts +154 -0
- package/pi-extensions/lib/entwurf-preflight.ts +247 -0
- package/pi-extensions/lib/entwurf-resume-args.ts +88 -0
- package/pi-extensions/lib/entwurf-self-address.ts +117 -0
- package/pi-extensions/lib/entwurf-v2-contract.ts +427 -0
- package/pi-extensions/lib/entwurf-v2-decider.ts +441 -0
- package/pi-extensions/lib/entwurf-v2-lock.ts +412 -0
- package/pi-extensions/lib/entwurf-v2-mailbox.ts +87 -0
- package/pi-extensions/lib/entwurf-v2-production.ts +326 -0
- package/pi-extensions/lib/entwurf-v2-release.ts +149 -0
- package/pi-extensions/lib/entwurf-v2-resume-marker.ts +34 -0
- package/pi-extensions/lib/entwurf-v2-runner.ts +196 -0
- package/pi-extensions/lib/entwurf-v2-send-fallback.ts +166 -0
- package/pi-extensions/lib/entwurf-v2-send.ts +271 -0
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +337 -0
- package/pi-extensions/lib/entwurf-v2-spawn.ts +323 -0
- package/pi-extensions/lib/entwurf-v2-surface.ts +205 -0
- package/pi-extensions/lib/meta-mailbox-body.ts +80 -0
- package/pi-extensions/lib/meta-session.ts +1951 -0
- package/pi-extensions/lib/project-trust-handler.ts +154 -0
- package/pi-extensions/lib/session-id.js +57 -0
- package/pi-extensions/lib/socket-discovery.ts +346 -0
- package/pi-extensions/lib/socket-probe.ts +92 -0
- package/pi-extensions/meta-bridge-hook.ts +250 -0
- package/pi-extensions/model-lock.ts +235 -0
- package/prompts/engraving.md +27 -0
- package/protocol.js +31 -0
- package/run.sh +2832 -0
- package/scripts/check-acp-backend-preflight.ts +130 -0
- package/scripts/check-acp-carrier-augment.ts +297 -0
- package/scripts/check-acp-config.ts +322 -0
- package/scripts/check-acp-event-mapper.ts +253 -0
- package/scripts/check-acp-overlay.ts +154 -0
- package/scripts/check-acp-prompt-builder.ts +191 -0
- package/scripts/check-acp-provider-surface.ts +159 -0
- package/scripts/check-acp-sdk-surface.ts +211 -0
- package/scripts/check-acp-session-reuse.ts +756 -0
- package/scripts/check-acp-session-store.ts +387 -0
- package/scripts/check-acp-tool-surface.ts +159 -0
- package/scripts/check-entwurf-bridge-boot.ts +199 -0
- package/scripts/check-entwurf-capabilities.ts +123 -0
- package/scripts/check-entwurf-control-rpc.ts +187 -0
- package/scripts/check-entwurf-deliverability.ts +158 -0
- package/scripts/check-entwurf-fact-provider.ts +316 -0
- package/scripts/check-entwurf-facts.ts +358 -0
- package/scripts/check-entwurf-mailbox-guard.ts +264 -0
- package/scripts/check-entwurf-peers-surface.ts +235 -0
- package/scripts/check-entwurf-resume-args.ts +149 -0
- package/scripts/check-entwurf-self-address.ts +209 -0
- package/scripts/check-entwurf-session-identity.ts +703 -0
- package/scripts/check-entwurf-v2-contract.ts +536 -0
- package/scripts/check-entwurf-v2-decider.ts +795 -0
- package/scripts/check-entwurf-v2-lock.ts +368 -0
- package/scripts/check-entwurf-v2-mailbox.ts +228 -0
- package/scripts/check-entwurf-v2-matrix.ts +437 -0
- package/scripts/check-entwurf-v2-production.ts +406 -0
- package/scripts/check-entwurf-v2-release.ts +245 -0
- package/scripts/check-entwurf-v2-runner.ts +452 -0
- package/scripts/check-entwurf-v2-send-fallback.ts +371 -0
- package/scripts/check-entwurf-v2-send.ts +454 -0
- package/scripts/check-entwurf-v2-spawn-production.ts +452 -0
- package/scripts/check-entwurf-v2-spawn.ts +398 -0
- package/scripts/check-entwurf-v2-surface.ts +352 -0
- package/scripts/check-keyset-overlap.py +120 -0
- package/scripts/check-mailbox-receipt-state.ts +170 -0
- package/scripts/check-meta-capability-source.ts +112 -0
- package/scripts/check-meta-dual-consumers.ts +154 -0
- package/scripts/check-meta-dual-read.ts +158 -0
- package/scripts/check-meta-listing.ts +138 -0
- package/scripts/check-meta-mailbox-state-write.ts +135 -0
- package/scripts/check-meta-migration.ts +212 -0
- package/scripts/check-meta-receiver-marker.ts +185 -0
- package/scripts/check-meta-record-v2.ts +191 -0
- package/scripts/check-meta-session.ts +673 -0
- package/scripts/check-model-lock.ts +408 -0
- package/scripts/check-package-source-routing.ts +253 -0
- package/scripts/check-pi-preflight.ts +304 -0
- package/scripts/check-project-trust-handler.ts +265 -0
- package/scripts/check-shell-quote.ts +121 -0
- package/scripts/check-socket-discovery.ts +428 -0
- package/scripts/check-socket-probe.ts +106 -0
- package/scripts/fixtures/probe-mcp-server.ts +33 -0
- package/scripts/gnew-rpc-drive.ts +211 -0
- package/scripts/lib/acp-child-cleanup.ts +116 -0
- package/scripts/meta-bridge-doctor.sh +315 -0
- package/scripts/meta-bridge-hook-log.sh +26 -0
- package/scripts/meta-bridge-install.sh +135 -0
- package/scripts/meta-bridge-prune.ts +199 -0
- package/scripts/meta-bridge-state.py +549 -0
- package/scripts/meta-bridge-statusline.sh +192 -0
- package/scripts/meta-bridge-store-doctor.ts +64 -0
- package/scripts/meta-bridge-uninstall.sh +39 -0
- package/scripts/new-session-id.ts +25 -0
- package/scripts/postinstall-chmod.cjs +58 -0
- package/scripts/raw-async-delivery/README.md +258 -0
- package/scripts/raw-async-delivery/cc-enqueue-addressed.sh +35 -0
- package/scripts/raw-async-delivery/cc-mailbox-rewake.sh +38 -0
- package/scripts/raw-async-delivery/cc-watch-filechanged.sh +19 -0
- package/scripts/raw-async-delivery/cc-watch-sessionstart.sh +15 -0
- package/scripts/raw-async-delivery/codex-local-appserver.sh +39 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/.claude-plugin/plugin.json +5 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/hooks/hooks.json +28 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-filechanged.sh +28 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-sessionstart.sh +27 -0
- package/scripts/raw-async-delivery/raw-agy-send.sh +29 -0
- package/scripts/raw-async-delivery/raw-claude-enqueue.sh +30 -0
- package/scripts/raw-async-delivery/raw-codex-ws-turn-start.py +164 -0
- package/scripts/raw-async-delivery/repro-addressed-routing.sh +96 -0
- package/scripts/raw-async-delivery/repro-plugin-idle-wake.sh +104 -0
- package/scripts/resolve-acp-bridge.ts +25 -0
- package/scripts/smoke-acp-bundled-mcp-live.ts +292 -0
- package/scripts/smoke-acp-carrier-augment-live.ts +192 -0
- package/scripts/smoke-acp-mcp-live.ts +129 -0
- package/scripts/smoke-acp-memory-containment-live.ts +389 -0
- package/scripts/smoke-acp-overlay-live.ts +314 -0
- package/scripts/smoke-acp-provider-live.ts +162 -0
- package/scripts/smoke-acp-raw-turn-live.ts +261 -0
- package/scripts/smoke-acp-session-reuse-live.ts +172 -0
- package/scripts/smoke-acp-skill-live.ts +144 -0
- package/scripts/smoke-acp-socket-citizen-live.ts +168 -0
- package/scripts/smoke-claude-native-resume-live.sh +198 -0
- package/scripts/smoke-entwurf-v2-matrix-live.ts +398 -0
- package/scripts/smoke-entwurf-v2-spawn-live.ts +175 -0
- package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +428 -0
- package/scripts/smoke-meta-async-drift.sh +171 -0
- package/scripts/smoke-meta-honesty.sh +147 -0
- package/scripts/smoke-meta-install-state.sh +403 -0
- package/scripts/smoke-meta-keyset-guard.sh +111 -0
- package/scripts/smoke-meta-prune.sh +174 -0
- package/scripts/smoke-resident-garden-guard.sh +433 -0
- package/scripts/smoke-session-id-name.ts +187 -0
- package/scripts/tsconfig.json +34 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-entwurf-v2-contract — deterministic gate for 0.11 Stage 0 step 4-pre:
|
|
3
|
+
* the FROZEN entwurf_v2 contract (동결결정 10 + 버킷 B F1/F4/F6 + Fable R1-R5).
|
|
4
|
+
*
|
|
5
|
+
* This gate IS the executable proof F6 demands ("산문 금지"): the intent×liveness
|
|
6
|
+
* decision table is a constant, and the "table cell ↔ dispatch receipt"
|
|
7
|
+
* round-trip is asserted exhaustively — so the contract is enforced by code, not
|
|
8
|
+
* by prose. Pure; no backend, no socket, no API.
|
|
9
|
+
*
|
|
10
|
+
* Proves:
|
|
11
|
+
* - R1 domain guard: only pi is in the liveness domain; claude-code / codex /
|
|
12
|
+
* antigravity are `unsupported` — NEVER folded into dead/indeterminate.
|
|
13
|
+
* - the 6-cell table is exhaustive and every cell is a SINGLE verdict (Q2):
|
|
14
|
+
* exactly two allow cells (ff+live=send, owned+dormant=resume), four reject.
|
|
15
|
+
* - N1/F3: an indeterminate target never spawns (both intents reject it).
|
|
16
|
+
* - Q2/F1: owned-outcome + live = reject (owned-live-no-autosend), not auto-send.
|
|
17
|
+
* - F-mailbox: an `unsupported` citizen routes through a SEPARATE intent-keyed
|
|
18
|
+
* mailbox mini-table (NOT the 6-cell table): ff+deliverable → meta-mailbox/
|
|
19
|
+
* ack-only, ff+undeliverable → mailbox-undeliverable (fail-closed),
|
|
20
|
+
* owned → backend-liveness-unsupported. Deliverability is a 2nd fact ignored
|
|
21
|
+
* in-domain (pi). The N2 asymmetry (dormant-pi reject vs unsupported mailbox)
|
|
22
|
+
* is pinned side-by-side.
|
|
23
|
+
* - R3: resolveDispatch round-trip — receipt.observedLiveness === input AND
|
|
24
|
+
* action/transport/ownership/reason === the table cell, every (intent × liveness).
|
|
25
|
+
* - R5: taxonomy covers the table reasons + pre-claims bad-target /
|
|
26
|
+
* untrusted-fail-fast / target-locked (so bucket B F2 won't reopen it), and
|
|
27
|
+
* is PRE-DISPATCH only (no send-fail-fallback member).
|
|
28
|
+
* - schema ↔ types drift guard: the TypeBox input/receipt property keysets and
|
|
29
|
+
* the StringEnum members match the hand-written contract constants.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import assert from "node:assert/strict";
|
|
33
|
+
import {
|
|
34
|
+
DISPATCH_LIVENESSES,
|
|
35
|
+
DISPATCH_TABLE,
|
|
36
|
+
type DispatchLiveness,
|
|
37
|
+
dispatchLivenessOf,
|
|
38
|
+
ENTWURF_INTENTS,
|
|
39
|
+
ENTWURF_V2_ACTIONS,
|
|
40
|
+
ENTWURF_V2_MODES,
|
|
41
|
+
ENTWURF_V2_OWNERSHIPS,
|
|
42
|
+
ENTWURF_V2_REJECT_REASONS,
|
|
43
|
+
ENTWURF_V2_TRANSPORTS,
|
|
44
|
+
EntwurfV2InputSchema,
|
|
45
|
+
EntwurfV2ReceiptRejectSchema,
|
|
46
|
+
EntwurfV2ReceiptSchema,
|
|
47
|
+
EntwurfV2ReceiptSuccessSchema,
|
|
48
|
+
FACT_LIVENESSES,
|
|
49
|
+
type FactLiveness,
|
|
50
|
+
factLivenessOf,
|
|
51
|
+
isLivenessSupported,
|
|
52
|
+
isPreProbeReject,
|
|
53
|
+
makeRejectReceipt,
|
|
54
|
+
PRE_PROBE_REJECT_REASONS,
|
|
55
|
+
RESOLVER_REJECT_REASONS,
|
|
56
|
+
rejectObservedLivenessWellFormed,
|
|
57
|
+
resolveDispatch,
|
|
58
|
+
UNSUPPORTED_DISPATCH_TABLE,
|
|
59
|
+
} from "../pi-extensions/lib/entwurf-v2-contract.ts";
|
|
60
|
+
import { SESSION_ID_RE } from "../pi-extensions/lib/session-id.js";
|
|
61
|
+
import type { SocketLiveness } from "../pi-extensions/lib/socket-probe.ts";
|
|
62
|
+
|
|
63
|
+
let passed = 0;
|
|
64
|
+
function ok(label: string, cond: boolean): void {
|
|
65
|
+
assert.ok(cond, label);
|
|
66
|
+
console.log(` ok ${label}`);
|
|
67
|
+
passed++;
|
|
68
|
+
}
|
|
69
|
+
function eq(label: string, actual: unknown, expected: unknown): void {
|
|
70
|
+
assert.deepStrictEqual(actual, expected, label);
|
|
71
|
+
console.log(` ok ${label}`);
|
|
72
|
+
passed++;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Pull the literal members out of a StringEnum / Optional(StringEnum) schema,
|
|
76
|
+
// whichever shape typebox emits — so the schema↔const drift guard is real.
|
|
77
|
+
function enumValues(schema: unknown): string[] {
|
|
78
|
+
const s = schema as { enum?: unknown[]; anyOf?: { const?: unknown }[] };
|
|
79
|
+
if (Array.isArray(s?.enum)) return s.enum.map(String);
|
|
80
|
+
if (Array.isArray(s?.anyOf)) return s.anyOf.map((m) => String(m.const));
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const SOCKET_LIVENESSES = ["alive", "dead", "indeterminate"] as const;
|
|
85
|
+
|
|
86
|
+
// ── R1: backend liveness domain — only pi, others unsupported ──────────────
|
|
87
|
+
eq("domain: pi is supported", isLivenessSupported("pi"), true);
|
|
88
|
+
eq("domain: claude-code unsupported (self-fetch, no socket)", isLivenessSupported("claude-code"), false);
|
|
89
|
+
eq("domain: codex unsupported (no probe surface yet)", isLivenessSupported("codex"), false);
|
|
90
|
+
eq("domain: antigravity unsupported", isLivenessSupported("antigravity"), false);
|
|
91
|
+
|
|
92
|
+
// ── R1: factLivenessOf — out-of-domain → unsupported, never dead/indeterminate ──
|
|
93
|
+
eq("fact: pi+alive → alive", factLivenessOf("pi", "alive"), "alive");
|
|
94
|
+
eq("fact: pi+dead → dead", factLivenessOf("pi", "dead"), "dead");
|
|
95
|
+
eq("fact: pi+indeterminate → indeterminate", factLivenessOf("pi", "indeterminate"), "indeterminate");
|
|
96
|
+
eq("fact: pi+no-probe → indeterminate (no proof, never coerced to dead)", factLivenessOf("pi", null), "indeterminate");
|
|
97
|
+
eq("fact: claude-code → unsupported (NOT folded into dead, R1)", factLivenessOf("claude-code", "alive"), "unsupported");
|
|
98
|
+
eq("fact: codex+no-probe → unsupported (NOT indeterminate)", factLivenessOf("codex", null), "unsupported");
|
|
99
|
+
|
|
100
|
+
// ── dispatchLivenessOf: socket result → routing axis ───────────────────────
|
|
101
|
+
eq("dispatch-liveness: alive → live", dispatchLivenessOf("alive"), "live");
|
|
102
|
+
eq("dispatch-liveness: dead → dormant", dispatchLivenessOf("dead"), "dormant");
|
|
103
|
+
eq("dispatch-liveness: indeterminate → indeterminate", dispatchLivenessOf("indeterminate"), "indeterminate");
|
|
104
|
+
|
|
105
|
+
// ── 6-cell table: exhaustive + single verdict per cell (Q2) ────────────────
|
|
106
|
+
let allowCells = 0;
|
|
107
|
+
let rejectCells = 0;
|
|
108
|
+
for (const intent of ENTWURF_INTENTS) {
|
|
109
|
+
for (const dl of DISPATCH_LIVENESSES) {
|
|
110
|
+
const cell = DISPATCH_TABLE[intent]?.[dl as DispatchLiveness];
|
|
111
|
+
ok(`table[${intent}][${dl}] present`, cell != null);
|
|
112
|
+
if (cell.action === "reject") {
|
|
113
|
+
rejectCells++;
|
|
114
|
+
ok(
|
|
115
|
+
`table[${intent}][${dl}] reject = single verdict (reason, no transport)`,
|
|
116
|
+
"reason" in cell && !("transport" in cell),
|
|
117
|
+
);
|
|
118
|
+
ok(
|
|
119
|
+
`table[${intent}][${dl}] reason in taxonomy`,
|
|
120
|
+
(ENTWURF_V2_REJECT_REASONS as readonly string[]).includes(cell.reason),
|
|
121
|
+
);
|
|
122
|
+
} else {
|
|
123
|
+
allowCells++;
|
|
124
|
+
ok(
|
|
125
|
+
`table[${intent}][${dl}] allow = single verdict (transport+ownership, no reason)`,
|
|
126
|
+
"transport" in cell && "ownership" in cell && !("reason" in cell),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
eq("table has 6 cells (2 intents × 3 liveness)", allowCells + rejectCells, 6);
|
|
132
|
+
eq("v2-initial: exactly 2 allow cells", allowCells, 2);
|
|
133
|
+
eq("v2-initial: exactly 4 reject cells", rejectCells, 4);
|
|
134
|
+
|
|
135
|
+
// the two allow cells are exactly the intended ones
|
|
136
|
+
eq("allow: fire-and-forget+live = send/control-socket/ack-only", DISPATCH_TABLE["fire-and-forget"].live, {
|
|
137
|
+
action: "send",
|
|
138
|
+
transport: "control-socket",
|
|
139
|
+
ownership: "ack-only",
|
|
140
|
+
});
|
|
141
|
+
eq("allow: owned-outcome+dormant = resume/spawn-bg/owned", DISPATCH_TABLE["owned-outcome"].dormant, {
|
|
142
|
+
action: "resume",
|
|
143
|
+
transport: "spawn-bg",
|
|
144
|
+
ownership: "owned",
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ── N1/F3: indeterminate never spawns (both intents reject) ────────────────
|
|
148
|
+
for (const intent of ENTWURF_INTENTS) {
|
|
149
|
+
const cell = DISPATCH_TABLE[intent].indeterminate;
|
|
150
|
+
eq(`N1: ${intent}+indeterminate = reject(indeterminate-no-spawn)`, cell, {
|
|
151
|
+
action: "reject",
|
|
152
|
+
reason: "indeterminate-no-spawn",
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── N2: fire-and-forget+dormant = reject-for-now ───────────────────────────
|
|
157
|
+
eq("N2: fire-and-forget+dormant = reject(dormant-fire-forget-unsupported)", DISPATCH_TABLE["fire-and-forget"].dormant, {
|
|
158
|
+
action: "reject",
|
|
159
|
+
reason: "dormant-fire-forget-unsupported",
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// ── Q2/F1: owned-outcome+live = reject, never auto-send ────────────────────
|
|
163
|
+
eq("Q2: owned-outcome+live = reject(owned-live-no-autosend)", DISPATCH_TABLE["owned-outcome"].live, {
|
|
164
|
+
action: "reject",
|
|
165
|
+
reason: "owned-live-no-autosend",
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ── R3: resolveDispatch round-trip — table cell ↔ receipt, every cell ──────
|
|
169
|
+
// In-domain (pi) liveness is authoritative; the F-mailbox deliverability fact is
|
|
170
|
+
// IGNORED here, so the verdict is identical for deliverable true/false.
|
|
171
|
+
for (const intent of ENTWURF_INTENTS) {
|
|
172
|
+
for (const sl of SOCKET_LIVENESSES as readonly SocketLiveness[]) {
|
|
173
|
+
const cell = DISPATCH_TABLE[intent][dispatchLivenessOf(sl)];
|
|
174
|
+
for (const deliverable of [true, false]) {
|
|
175
|
+
const receipt = resolveDispatch(intent, sl, deliverable);
|
|
176
|
+
ok(
|
|
177
|
+
`round-trip ${intent}/${sl} (deliverable=${deliverable}): observedLiveness === input`,
|
|
178
|
+
receipt.observedLiveness === sl,
|
|
179
|
+
);
|
|
180
|
+
if (cell.action === "reject") {
|
|
181
|
+
ok(
|
|
182
|
+
`round-trip ${intent}/${sl} (deliverable=${deliverable}): receipt reject matches cell`,
|
|
183
|
+
receipt.ok === false && receipt.reason === cell.reason,
|
|
184
|
+
);
|
|
185
|
+
} else {
|
|
186
|
+
ok(
|
|
187
|
+
`round-trip ${intent}/${sl} (deliverable=${deliverable}): receipt allow matches cell`,
|
|
188
|
+
receipt.ok === true &&
|
|
189
|
+
receipt.action === cell.action &&
|
|
190
|
+
receipt.transport === cell.transport &&
|
|
191
|
+
receipt.ownership === cell.ownership,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── F-mailbox: unsupported mailbox mini-table — SEPARATE from the 6-cell table ──
|
|
199
|
+
// The unsupported domain-guard routes through UNSUPPORTED_DISPATCH_TABLE (intent-
|
|
200
|
+
// keyed), NOT the liveness table. Two cells, each a single verdict (Q2).
|
|
201
|
+
eq("mini-table: 2 cells (one per intent)", Object.keys(UNSUPPORTED_DISPATCH_TABLE).length, 2);
|
|
202
|
+
eq(
|
|
203
|
+
"mini-table[fire-and-forget] = send/meta-mailbox/ack-only (deliverable path)",
|
|
204
|
+
UNSUPPORTED_DISPATCH_TABLE["fire-and-forget"],
|
|
205
|
+
{
|
|
206
|
+
action: "send",
|
|
207
|
+
transport: "meta-mailbox",
|
|
208
|
+
ownership: "ack-only",
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
eq("mini-table[owned-outcome] = reject(backend-liveness-unsupported)", UNSUPPORTED_DISPATCH_TABLE["owned-outcome"], {
|
|
212
|
+
action: "reject",
|
|
213
|
+
reason: "backend-liveness-unsupported",
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ── F-mailbox: resolveDispatch over the deliverability axis ────────────────
|
|
217
|
+
// fire-and-forget + deliverable → meta-mailbox send (NOT reject); ack-only +
|
|
218
|
+
// observedLiveness stays `unsupported` (transport says it went to the mailbox).
|
|
219
|
+
eq(
|
|
220
|
+
"F-mailbox: ff+unsupported+deliverable → send/meta-mailbox/ack-only",
|
|
221
|
+
resolveDispatch("fire-and-forget", "unsupported" as FactLiveness, true),
|
|
222
|
+
{
|
|
223
|
+
ok: true,
|
|
224
|
+
action: "send",
|
|
225
|
+
transport: "meta-mailbox",
|
|
226
|
+
ownership: "ack-only",
|
|
227
|
+
observedLiveness: "unsupported",
|
|
228
|
+
},
|
|
229
|
+
);
|
|
230
|
+
// fail-closed: unknown/undeliverable → mailbox-undeliverable (distinct reason,
|
|
231
|
+
// NOT backend-liveness-unsupported — that would be a lie; the backend has no
|
|
232
|
+
// liveness predicate but ff does not need one, the mailbox does).
|
|
233
|
+
eq(
|
|
234
|
+
"F-mailbox: ff+unsupported+UNdeliverable → reject(mailbox-undeliverable)",
|
|
235
|
+
resolveDispatch("fire-and-forget", "unsupported" as FactLiveness, false),
|
|
236
|
+
{
|
|
237
|
+
ok: false,
|
|
238
|
+
reason: "mailbox-undeliverable",
|
|
239
|
+
observedLiveness: "unsupported",
|
|
240
|
+
},
|
|
241
|
+
);
|
|
242
|
+
// owned-outcome+unsupported still rejects regardless of deliverability — a
|
|
243
|
+
// self-fetch backend has no real liveness for the caller to own (point 3).
|
|
244
|
+
for (const deliverable of [true, false]) {
|
|
245
|
+
eq(
|
|
246
|
+
`F-mailbox: owned+unsupported (deliverable=${deliverable}) → reject(backend-liveness-unsupported)`,
|
|
247
|
+
resolveDispatch("owned-outcome", "unsupported" as FactLiveness, deliverable),
|
|
248
|
+
{
|
|
249
|
+
ok: false,
|
|
250
|
+
reason: "backend-liveness-unsupported",
|
|
251
|
+
observedLiveness: "unsupported",
|
|
252
|
+
},
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ── N2 asymmetry — the two tables are NOT contradictory ────────────────────
|
|
257
|
+
// fire-and-forget+dormant-PI = reject (confirmed not-running → enqueue is silent
|
|
258
|
+
// pileup) vs fire-and-forget+unsupported-CITIZEN = mailbox (unknown liveness,
|
|
259
|
+
// best-effort doorbell is the honest most). Asserted side-by-side so the gate
|
|
260
|
+
// pins the intended asymmetry, not an accident.
|
|
261
|
+
ok(
|
|
262
|
+
"N2: ff+dormant-pi rejects but ff+unsupported-deliverable sends to mailbox (intended asymmetry)",
|
|
263
|
+
resolveDispatch("fire-and-forget", "dead" as FactLiveness, true).ok === false &&
|
|
264
|
+
resolveDispatch("fire-and-forget", "unsupported" as FactLiveness, true).ok === true,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
// ── F-mailbox: transport + taxonomy members present ────────────────────────
|
|
268
|
+
ok(
|
|
269
|
+
"transport: meta-mailbox present (F-mailbox liveness-free delivery)",
|
|
270
|
+
(ENTWURF_V2_TRANSPORTS as readonly string[]).includes("meta-mailbox"),
|
|
271
|
+
);
|
|
272
|
+
// guardrail enforced by code (NEXT.md "6칸표에 mailbox 굽지 말 것"): meta-mailbox
|
|
273
|
+
// lives ONLY in the unsupported mini-table, never in an in-domain liveness cell.
|
|
274
|
+
for (const intent of ENTWURF_INTENTS) {
|
|
275
|
+
for (const dl of DISPATCH_LIVENESSES) {
|
|
276
|
+
const cell = DISPATCH_TABLE[intent][dl as DispatchLiveness];
|
|
277
|
+
ok(
|
|
278
|
+
`guard: DISPATCH_TABLE[${intent}][${dl}] is NOT meta-mailbox (mailbox stays out of the 6-cell table)`,
|
|
279
|
+
cell.action === "reject" || cell.transport !== "meta-mailbox",
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
ok(
|
|
284
|
+
"taxonomy: mailbox-undeliverable present (F-mailbox fail-closed; pre-claimed for pi-backend future-risk)",
|
|
285
|
+
(ENTWURF_V2_REJECT_REASONS as readonly string[]).includes("mailbox-undeliverable"),
|
|
286
|
+
);
|
|
287
|
+
ok(
|
|
288
|
+
"taxonomy: mailbox-undeliverable is a resolver reason (in RESOLVER_REJECT_REASONS)",
|
|
289
|
+
(RESOLVER_REJECT_REASONS as readonly string[]).includes("mailbox-undeliverable"),
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
// ── R5: taxonomy coverage + pre-claims + pre-dispatch scope ────────────────
|
|
293
|
+
for (const r of RESOLVER_REJECT_REASONS) {
|
|
294
|
+
ok(`taxonomy: resolver reason '${r}' is in the enum`, (ENTWURF_V2_REJECT_REASONS as readonly string[]).includes(r));
|
|
295
|
+
}
|
|
296
|
+
for (const pre of ["bad-target", "untrusted-fail-fast", "target-locked"]) {
|
|
297
|
+
ok(
|
|
298
|
+
`taxonomy: pre-claim '${pre}' present (R5 — bucket B won't reopen)`,
|
|
299
|
+
(ENTWURF_V2_REJECT_REASONS as readonly string[]).includes(pre),
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
eq("taxonomy: no duplicate reasons", new Set(ENTWURF_V2_REJECT_REASONS).size, ENTWURF_V2_REJECT_REASONS.length);
|
|
303
|
+
ok(
|
|
304
|
+
"taxonomy: pre-dispatch only (no send-fail-fallback member)",
|
|
305
|
+
!ENTWURF_V2_REJECT_REASONS.some((r) => /fallback|transport-fail|send-fail/.test(r)),
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
// ── F3: target-address-conflict — named reason, pre-resolver (not RESOLVER_) ──
|
|
309
|
+
// A quarantined gid (garden-id-socket-conflict / symlinked socket) resolves to
|
|
310
|
+
// two receivers; dispatch refuses. It is the ONLY in-band honest channel for a
|
|
311
|
+
// dispatch-level identity-split (a v2 caller sees only the receipt, not the
|
|
312
|
+
// listing diagnostic). Lives with bad-target/target-locked as a pre-resolver
|
|
313
|
+
// reject — NOT a member of RESOLVER_REJECT_REASONS (the 6-cell + mini-table set).
|
|
314
|
+
ok(
|
|
315
|
+
"F3: taxonomy has target-address-conflict (named, in-band honest channel)",
|
|
316
|
+
(ENTWURF_V2_REJECT_REASONS as readonly string[]).includes("target-address-conflict"),
|
|
317
|
+
);
|
|
318
|
+
ok(
|
|
319
|
+
"F3: target-address-conflict is NOT a resolver reason (pre-resolver, like bad-target)",
|
|
320
|
+
!(RESOLVER_REJECT_REASONS as readonly string[]).includes("target-address-conflict"),
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
// ── ?6: reject observedLiveness = required-nullable, reason-dependent ──────
|
|
324
|
+
// Pre-probe rejects (decided before any liveness probe) carry observedLiveness =
|
|
325
|
+
// null; every other reject + every success carries a non-null FactLiveness. The
|
|
326
|
+
// split is reason-dependent, so the receipt schema's blanket `FactLiveness | null`
|
|
327
|
+
// cannot enforce it — these semantic fixtures (the SSOT predicate the 5b decider
|
|
328
|
+
// mints against) do.
|
|
329
|
+
eq(
|
|
330
|
+
"?6: PRE_PROBE_REJECT_REASONS = bad-target/target-locked/target-address-conflict",
|
|
331
|
+
[...PRE_PROBE_REJECT_REASONS],
|
|
332
|
+
["bad-target", "target-locked", "target-address-conflict"],
|
|
333
|
+
);
|
|
334
|
+
// the partition is exhaustive + disjoint: every reject reason is pre-probe XOR not.
|
|
335
|
+
for (const r of ENTWURF_V2_REJECT_REASONS) {
|
|
336
|
+
const pre = (PRE_PROBE_REJECT_REASONS as readonly string[]).includes(r);
|
|
337
|
+
eq(`?6: isPreProbeReject('${r}') matches the const partition`, isPreProbeReject(r), pre);
|
|
338
|
+
}
|
|
339
|
+
// the RESOLVER reasons are all POST-probe (resolveDispatch always has a real
|
|
340
|
+
// liveness in hand) — none may be pre-probe, so all require non-null.
|
|
341
|
+
for (const r of RESOLVER_REJECT_REASONS) {
|
|
342
|
+
ok(`?6: resolver reason '${r}' is post-probe (non-null observedLiveness required)`, !isPreProbeReject(r));
|
|
343
|
+
}
|
|
344
|
+
// untrusted-fail-fast is post-probe too (1B: it now runs AFTER lock+probe, only on
|
|
345
|
+
// a resume verdict, so observedLiveness is the honest measured dormant).
|
|
346
|
+
ok("?6: untrusted-fail-fast is post-probe (1B — runs after lock+probe)", !isPreProbeReject("untrusted-fail-fast"));
|
|
347
|
+
|
|
348
|
+
// A1: socket-only-no-resume-authority — owned-outcome × dormant against a record-less
|
|
349
|
+
// socket-only endpoint, refused by the `allowResume:false` guard AFTER the probe. Like
|
|
350
|
+
// untrusted-fail-fast it is post-probe (non-null) and NOT a table-resolver cell; it is in
|
|
351
|
+
// the master enum but neither PRE_PROBE nor RESOLVER. It exists so a LIVE/addressable
|
|
352
|
+
// socket-only citizen is never mislabeled the pre-probe `bad-target` (absent) lie.
|
|
353
|
+
ok(
|
|
354
|
+
"taxonomy: socket-only-no-resume-authority is in the enum",
|
|
355
|
+
(ENTWURF_V2_REJECT_REASONS as readonly string[]).includes("socket-only-no-resume-authority"),
|
|
356
|
+
);
|
|
357
|
+
ok(
|
|
358
|
+
"?6: socket-only-no-resume-authority is post-probe (A1 guard — runs after lock+probe)",
|
|
359
|
+
!isPreProbeReject("socket-only-no-resume-authority"),
|
|
360
|
+
);
|
|
361
|
+
ok(
|
|
362
|
+
"taxonomy: socket-only-no-resume-authority is NOT a resolver reason (allowResume:false guard, not a table cell)",
|
|
363
|
+
!(RESOLVER_REJECT_REASONS as readonly string[]).includes("socket-only-no-resume-authority"),
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
// rejectObservedLivenessWellFormed: pre-probe ⇒ null only; post-probe ⇒ value only.
|
|
367
|
+
for (const r of PRE_PROBE_REJECT_REASONS) {
|
|
368
|
+
ok(`?6: well-formed('${r}', null) = true`, rejectObservedLivenessWellFormed(r, null));
|
|
369
|
+
for (const v of FACT_LIVENESSES) {
|
|
370
|
+
ok(
|
|
371
|
+
`?6: well-formed('${r}', '${v}') = false (pre-probe must not stamp a value)`,
|
|
372
|
+
!rejectObservedLivenessWellFormed(r, v),
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
for (const r of [
|
|
377
|
+
...RESOLVER_REJECT_REASONS,
|
|
378
|
+
"untrusted-fail-fast" as const,
|
|
379
|
+
"socket-only-no-resume-authority" as const,
|
|
380
|
+
]) {
|
|
381
|
+
ok(
|
|
382
|
+
`?6: well-formed('${r}', null) = false (post-probe must carry a value)`,
|
|
383
|
+
!rejectObservedLivenessWellFormed(r, null),
|
|
384
|
+
);
|
|
385
|
+
for (const v of FACT_LIVENESSES) {
|
|
386
|
+
ok(`?6: well-formed('${r}', '${v}') = true`, rejectObservedLivenessWellFormed(r, v));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
// the illegal example from ?6: bad-target with a stamped liveness — schema cannot
|
|
390
|
+
// catch it (reason-dependent), the predicate does.
|
|
391
|
+
ok(
|
|
392
|
+
"?6: illegal {ok:false, reason:'bad-target', observedLiveness:'indeterminate'} rejected by predicate",
|
|
393
|
+
!rejectObservedLivenessWellFormed("bad-target", "indeterminate"),
|
|
394
|
+
);
|
|
395
|
+
ok(
|
|
396
|
+
"?6: legal {ok:false, reason:'bad-target', observedLiveness:null} accepted by predicate",
|
|
397
|
+
rejectObservedLivenessWellFormed("bad-target", null),
|
|
398
|
+
);
|
|
399
|
+
// every reject resolveDispatch actually emits is post-probe with a non-null value
|
|
400
|
+
// AND well-formed — it never mints a pre-probe reason (those come from the 5b
|
|
401
|
+
// stages that run before the resolver).
|
|
402
|
+
for (const intent of ENTWURF_INTENTS) {
|
|
403
|
+
for (const lv of FACT_LIVENESSES) {
|
|
404
|
+
for (const deliverable of [true, false]) {
|
|
405
|
+
const receipt = resolveDispatch(intent, lv as FactLiveness, deliverable);
|
|
406
|
+
if (receipt.ok === false) {
|
|
407
|
+
ok(
|
|
408
|
+
`?6: resolveDispatch(${intent}/${lv}/${deliverable}) reject is post-probe + well-formed`,
|
|
409
|
+
!isPreProbeReject(receipt.reason) &&
|
|
410
|
+
receipt.observedLiveness !== null &&
|
|
411
|
+
rejectObservedLivenessWellFormed(receipt.reason, receipt.observedLiveness),
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ── ?6 enforcement: makeRejectReceipt is the ONLY sanctioned reject mint ───
|
|
419
|
+
// A pure predicate cannot force a caller to consult it (Fable 4): 5b could
|
|
420
|
+
// hand-assemble an ill-formed reject the blanket schema accepts. makeRejectReceipt
|
|
421
|
+
// THROWS on a violation, so routing every reject through it makes the bypass
|
|
422
|
+
// surface zero. Prove: pre-probe accepts null + rejects any value; post-probe
|
|
423
|
+
// accepts every value + rejects null; and resolveDispatch's own mints are exactly
|
|
424
|
+
// what the constructor would have produced (the in-resolver path uses it too).
|
|
425
|
+
for (const r of PRE_PROBE_REJECT_REASONS) {
|
|
426
|
+
eq(`makeRejectReceipt('${r}', null) → well-formed reject`, makeRejectReceipt(r, null), {
|
|
427
|
+
ok: false,
|
|
428
|
+
reason: r,
|
|
429
|
+
observedLiveness: null,
|
|
430
|
+
});
|
|
431
|
+
for (const v of FACT_LIVENESSES) {
|
|
432
|
+
assert.throws(
|
|
433
|
+
() => makeRejectReceipt(r, v),
|
|
434
|
+
/ill-formed reject/,
|
|
435
|
+
`makeRejectReceipt('${r}', '${v}') throws (pre-probe must not stamp a value)`,
|
|
436
|
+
);
|
|
437
|
+
console.log(` ok makeRejectReceipt('${r}', '${v}') throws (?6 chokepoint)`);
|
|
438
|
+
passed++;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
for (const r of [...RESOLVER_REJECT_REASONS, "untrusted-fail-fast" as const]) {
|
|
442
|
+
assert.throws(
|
|
443
|
+
() => makeRejectReceipt(r, null),
|
|
444
|
+
/ill-formed reject/,
|
|
445
|
+
`makeRejectReceipt('${r}', null) throws (post-probe must carry a value)`,
|
|
446
|
+
);
|
|
447
|
+
console.log(` ok makeRejectReceipt('${r}', null) throws (?6 chokepoint)`);
|
|
448
|
+
passed++;
|
|
449
|
+
for (const v of FACT_LIVENESSES) {
|
|
450
|
+
eq(`makeRejectReceipt('${r}', '${v}') → well-formed reject`, makeRejectReceipt(r, v), {
|
|
451
|
+
ok: false,
|
|
452
|
+
reason: r,
|
|
453
|
+
observedLiveness: v,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ── schema ↔ types drift guard (structural; no @sinclair/value) ────────────
|
|
459
|
+
function literalConst(schema: unknown): unknown {
|
|
460
|
+
return (schema as { const?: unknown }).const;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// input: keys + R2/F6 garden-id pattern (executable, not prose) + every enum.
|
|
464
|
+
eq("input schema keys", Object.keys(EntwurfV2InputSchema.properties), ["target", "intent", "mode", "wantsReply"]);
|
|
465
|
+
eq(
|
|
466
|
+
"input.target pattern === SESSION_ID_RE (R2/F6 — typo gid fails schema, never spawns)",
|
|
467
|
+
(EntwurfV2InputSchema.properties.target as { pattern?: string }).pattern,
|
|
468
|
+
SESSION_ID_RE.source,
|
|
469
|
+
);
|
|
470
|
+
eq("input.intent enum === ENTWURF_INTENTS", enumValues(EntwurfV2InputSchema.properties.intent), [...ENTWURF_INTENTS]);
|
|
471
|
+
eq(
|
|
472
|
+
"input.mode enum === ENTWURF_V2_MODES (delivery mode, not ownership)",
|
|
473
|
+
enumValues(EntwurfV2InputSchema.properties.mode),
|
|
474
|
+
[...ENTWURF_V2_MODES],
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
// receipt: a DISCRIMINATED union (anyOf of 2 branches) — proves a flat illegal
|
|
478
|
+
// receipt {ok:true, reason:...} cannot validate. success carries the allow
|
|
479
|
+
// facets and NO reason; reject carries reason and NONE of the allow facets.
|
|
480
|
+
const receiptBranches = (EntwurfV2ReceiptSchema as { anyOf?: unknown[] }).anyOf ?? [];
|
|
481
|
+
eq("receipt schema is a 2-branch union (not flat optionals)", receiptBranches.length, 2);
|
|
482
|
+
|
|
483
|
+
const succ = EntwurfV2ReceiptSuccessSchema.properties;
|
|
484
|
+
eq("receipt.success keys", Object.keys(succ), ["ok", "action", "transport", "ownership", "observedLiveness"]);
|
|
485
|
+
eq("receipt.success ok literal === true", literalConst(succ.ok), true);
|
|
486
|
+
ok("receipt.success has NO reason (illegal {ok:true,reason} excluded)", !("reason" in succ));
|
|
487
|
+
eq("receipt.success.action enum === ENTWURF_V2_ACTIONS", enumValues(succ.action), [...ENTWURF_V2_ACTIONS]);
|
|
488
|
+
eq("receipt.success.transport enum === ENTWURF_V2_TRANSPORTS", enumValues(succ.transport), [...ENTWURF_V2_TRANSPORTS]);
|
|
489
|
+
eq("receipt.success.ownership enum === ENTWURF_V2_OWNERSHIPS", enumValues(succ.ownership), [...ENTWURF_V2_OWNERSHIPS]);
|
|
490
|
+
eq("receipt.success.observedLiveness enum === FACT_LIVENESSES (4)", enumValues(succ.observedLiveness), [
|
|
491
|
+
...FACT_LIVENESSES,
|
|
492
|
+
]);
|
|
493
|
+
|
|
494
|
+
const rej = EntwurfV2ReceiptRejectSchema.properties;
|
|
495
|
+
eq("receipt.reject keys", Object.keys(rej), ["ok", "reason", "observedLiveness"]);
|
|
496
|
+
eq("receipt.reject ok literal === false", literalConst(rej.ok), false);
|
|
497
|
+
ok(
|
|
498
|
+
"receipt.reject has NO allow facets (illegal {ok:false,action} excluded)",
|
|
499
|
+
!("action" in rej) && !("transport" in rej) && !("ownership" in rej),
|
|
500
|
+
);
|
|
501
|
+
eq("receipt.reject.reason enum === ENTWURF_V2_REJECT_REASONS", enumValues(rej.reason), [...ENTWURF_V2_REJECT_REASONS]);
|
|
502
|
+
// ?6: reject.observedLiveness is a required-nullable union — Type.Union([StringEnum
|
|
503
|
+
// (FACT_LIVENESSES), Type.Null()]) — NOT Optional and NOT a bare StringEnum. The
|
|
504
|
+
// key is always present (required), the value may be a 4-value liveness OR null.
|
|
505
|
+
const rejLiveness = rej.observedLiveness as { anyOf?: { type?: string }[] };
|
|
506
|
+
eq("?6: receipt.reject.observedLiveness is a 2-branch union (liveness | null)", rejLiveness.anyOf?.length, 2);
|
|
507
|
+
ok(
|
|
508
|
+
"?6: receipt.reject.observedLiveness has a null branch (required-nullable, not optional)",
|
|
509
|
+
(rejLiveness.anyOf ?? []).some((b) => b.type === "null"),
|
|
510
|
+
);
|
|
511
|
+
const rejLivenessEnumBranch = (rejLiveness.anyOf ?? []).find((b) => b.type !== "null");
|
|
512
|
+
eq("?6: receipt.reject.observedLiveness enum branch === FACT_LIVENESSES (4)", enumValues(rejLivenessEnumBranch), [
|
|
513
|
+
...FACT_LIVENESSES,
|
|
514
|
+
]);
|
|
515
|
+
ok("?6: receipt.reject still has observedLiveness key (required, not optional-dropped)", "observedLiveness" in rej);
|
|
516
|
+
|
|
517
|
+
// exactness — additionalProperties:false makes the schema reject extra keys at
|
|
518
|
+
// the JSON-Schema level (not merely by declared-property convention). Without
|
|
519
|
+
// it, {ok:true, ..., reason:"bad-target"} would validate against the success
|
|
520
|
+
// branch (default additionalProperties=true).
|
|
521
|
+
function additionalProps(schema: unknown): unknown {
|
|
522
|
+
return (schema as { additionalProperties?: unknown }).additionalProperties;
|
|
523
|
+
}
|
|
524
|
+
eq("input schema is exact (additionalProperties:false)", additionalProps(EntwurfV2InputSchema), false);
|
|
525
|
+
eq(
|
|
526
|
+
"receipt.success is exact (additionalProperties:false — stray reason rejected)",
|
|
527
|
+
additionalProps(EntwurfV2ReceiptSuccessSchema),
|
|
528
|
+
false,
|
|
529
|
+
);
|
|
530
|
+
eq(
|
|
531
|
+
"receipt.reject is exact (additionalProperties:false — stray allow facet rejected)",
|
|
532
|
+
additionalProps(EntwurfV2ReceiptRejectSchema),
|
|
533
|
+
false,
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
console.log(`\ncheck-entwurf-v2-contract: ${passed} assertions passed`);
|