@junghanacs/entwurf 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +52 -17
- package/BASELINE.md +60 -206
- package/CHANGELOG.md +64 -0
- package/CONTRIBUTING.md +14 -10
- package/DELIVERY.md +118 -261
- package/README.md +91 -431
- package/VERIFY.md +74 -95
- package/demo/README.md +1 -1
- package/demo/demo-baseline.sh +1 -3
- package/demo/demo.sh +2 -5
- package/docs/acp-backend-rail.md +236 -0
- package/docs/external-mcp-host.md +131 -0
- package/docs/fresh-cut-policy.md +99 -0
- package/docs/setup-clean-host.md +124 -328
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +160 -67
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +12 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +11 -10
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/engraving.js +42 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +4 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +11 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +7 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-core.js +13 -14
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-resume-args.js +45 -40
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +117 -95
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +23 -57
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +16 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +5 -53
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-release.js +21 -36
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -15
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send-fallback.js +12 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +2 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +30 -67
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +256 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +91 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +258 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-launch.js +202 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-placement.js +289 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +170 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +136 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/session-id.js +8 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/socket-discovery.js +3 -3
- package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +51 -0
- package/mcp/entwurf-bridge/dist/scripts/new-session-id.js +9 -4
- package/mcp/entwurf-bridge/src/index.ts +176 -67
- package/mcp/entwurf-bridge/start.sh +2 -2
- package/mcp/entwurf-bridge/test.sh +23 -9
- package/mcp/entwurf-bridge/tsconfig.build.json +11 -2
- package/package.json +25 -11
- package/pi-extensions/entwurf-control.ts +220 -71
- package/pi-extensions/lib/acp/acp-client.ts +22 -0
- package/pi-extensions/lib/acp/augment.ts +42 -3
- package/pi-extensions/lib/acp/backend-adapter.ts +12 -11
- package/pi-extensions/lib/acp/backend.ts +366 -38
- package/pi-extensions/lib/acp/config.ts +1 -1
- package/pi-extensions/lib/acp/engraving.ts +43 -1
- package/pi-extensions/lib/acp/event-mapper.ts +16 -13
- package/pi-extensions/lib/acp/models.ts +4 -4
- package/pi-extensions/lib/acp/overlay.ts +11 -9
- package/pi-extensions/lib/entwurf-control-rpc.ts +7 -5
- package/pi-extensions/lib/entwurf-core.ts +15 -15
- package/pi-extensions/lib/entwurf-resume-args.ts +41 -52
- package/pi-extensions/lib/entwurf-v2-contract-schema.ts +1 -1
- package/pi-extensions/lib/entwurf-v2-contract.ts +120 -99
- package/pi-extensions/lib/entwurf-v2-decider.ts +30 -91
- package/pi-extensions/lib/entwurf-v2-lock.ts +16 -7
- package/pi-extensions/lib/entwurf-v2-production.ts +4 -78
- package/pi-extensions/lib/entwurf-v2-release.ts +25 -49
- package/pi-extensions/lib/entwurf-v2-runner.ts +6 -21
- package/pi-extensions/lib/entwurf-v2-send-fallback.ts +12 -11
- package/pi-extensions/lib/entwurf-v2-send.ts +2 -7
- package/pi-extensions/lib/entwurf-v2-surface.ts +36 -76
- package/pi-extensions/lib/entwurf-v2-visible-resume.ts +370 -0
- package/pi-extensions/lib/meta-session.ts +93 -5
- package/pi-extensions/lib/mux-fresh-call.ts +328 -0
- package/pi-extensions/lib/mux-launch.ts +267 -0
- package/pi-extensions/lib/mux-placement.ts +387 -0
- package/pi-extensions/lib/mux-resume-call.ts +221 -0
- package/pi-extensions/lib/resume-launch-identity.ts +162 -0
- package/pi-extensions/lib/session-id.js +8 -5
- package/pi-extensions/lib/socket-discovery.ts +3 -3
- package/prompts/engraving.md +13 -5
- package/run.sh +688 -282
- package/scripts/agy-bridge-config.py +5 -1
- package/scripts/check-acp-backend-preflight.ts +1 -1
- package/scripts/check-acp-carrier-augment.ts +246 -10
- package/scripts/check-acp-cortex.ts +5 -5
- package/scripts/check-acp-overlay.ts +13 -3
- package/scripts/check-acp-prompt-lifecycle.ts +565 -0
- package/scripts/check-acp-stop-reason.ts +342 -0
- package/scripts/check-acp-stream-hooks.ts +504 -0
- package/scripts/check-elapsed.sh +25 -0
- package/scripts/check-entwurf-bridge-boot.ts +51 -4
- package/scripts/check-entwurf-bridge-pi-free.ts +6 -5
- package/scripts/check-entwurf-control-rpc.ts +4 -3
- package/scripts/check-entwurf-resume-args.ts +72 -70
- package/scripts/check-entwurf-session-identity.ts +14 -10
- package/scripts/check-entwurf-v2-contract.ts +34 -59
- package/scripts/check-entwurf-v2-decider.ts +17 -177
- package/scripts/check-entwurf-v2-lock.ts +5 -2
- package/scripts/check-entwurf-v2-matrix.ts +3 -53
- package/scripts/check-entwurf-v2-production.ts +2 -91
- package/scripts/check-entwurf-v2-release.ts +10 -105
- package/scripts/check-entwurf-v2-runner.ts +4 -85
- package/scripts/check-entwurf-v2-send-fallback.ts +5 -6
- package/scripts/check-entwurf-v2-send.ts +0 -28
- package/scripts/check-entwurf-v2-surface.ts +198 -128
- package/scripts/check-entwurf-v2-visible-resume.ts +445 -0
- package/scripts/check-fresh-cut-gate.sh +1 -1
- package/scripts/check-gate-qualification.ts +101 -7
- package/scripts/check-install-container.sh +10 -2
- package/scripts/check-install-surface.ts +1 -1
- package/scripts/check-keyset-overlap.py +1 -1
- package/scripts/check-meta-facts.ts +249 -0
- package/scripts/check-meta-identity-consumers.ts +1 -1
- package/scripts/check-meta-session.ts +169 -0
- package/scripts/check-mux-launch-tmux.ts +316 -0
- package/scripts/check-mux-launch.ts +288 -0
- package/scripts/check-mux-launcher-fence.ts +264 -0
- package/scripts/check-mux-parent-artifact.ts +195 -0
- package/scripts/check-mux-placement-tmux.ts +322 -0
- package/scripts/check-mux-placement.ts +323 -0
- package/scripts/check-mux-resume-call.ts +282 -0
- package/scripts/check-probe-cli-shim.ts +25 -22
- package/scripts/check-probe-ordering.ts +121 -81
- package/scripts/check-release-gate-outcomes.ts +452 -0
- package/scripts/check-resume-launch-identity.ts +244 -0
- package/scripts/check-socket-discovery.ts +1 -1
- package/scripts/fixtures/mux-parent-transcript.scrubbed.jsonl +3 -0
- package/scripts/inventory-verification-surface.ts +349 -0
- package/scripts/lib/claude-launcher-fence.ts +322 -0
- package/scripts/lib/live-skip.ts +33 -0
- package/scripts/lib/mutation-qualify.ts +109 -3
- package/scripts/lib/probe-acp-turn.ts +33 -12
- package/scripts/lib/step-outcome.sh +88 -0
- package/scripts/meta-bridge-doctor.sh +6 -8
- package/scripts/meta-facts.ts +60 -0
- package/scripts/mutants/acp-augment.json +106 -0
- package/scripts/mutants/acp-cortex.json +2 -2
- package/scripts/mutants/acp-overlay.json +17 -0
- package/scripts/mutants/acp-prompt-lifecycle.json +100 -0
- package/scripts/mutants/acp-stop-reason.json +80 -0
- package/scripts/mutants/acp-stream-hooks.json +158 -0
- package/scripts/mutants/bridge-boot-resume.json +45 -0
- package/scripts/mutants/meta-facts.json +50 -0
- package/scripts/mutants/meta-identity.json +36 -0
- package/scripts/mutants/mux-boundary.json +196 -0
- package/scripts/mutants/mux-fresh-call.json +185 -0
- package/scripts/mutants/mux-launcher-fence.json +123 -0
- package/scripts/mutants/mux-parent-artifact.json +39 -0
- package/scripts/mutants/mux-resume-call.json +148 -0
- package/scripts/mutants/probe-ordering.json +6 -1021
- package/scripts/mutants/release-gate.json +140 -0
- package/scripts/mutants/resume-args.json +76 -0
- package/scripts/mutants/resume-launch-identity.json +96 -0
- package/scripts/mutants/v2-surface.json +74 -23
- package/scripts/mutants/v2-visible-resume.json +215 -0
- package/scripts/new-session-id.ts +9 -4
- package/scripts/smoke-acp-bundled-mcp-live.ts +4 -3
- package/scripts/smoke-acp-carrier-augment-live.ts +2 -2
- package/scripts/smoke-acp-cortex-live.ts +8 -14
- package/scripts/smoke-acp-long-turn-live.ts +185 -0
- package/scripts/smoke-acp-mcp-live.ts +2 -2
- package/scripts/smoke-acp-memory-containment-live.ts +2 -2
- package/scripts/smoke-acp-ordering-probe-live.ts +2 -2
- package/scripts/smoke-acp-overlay-live.ts +2 -2
- package/scripts/smoke-acp-provider-live.ts +2 -2
- package/scripts/smoke-acp-raw-turn-live.ts +3 -3
- package/scripts/smoke-acp-session-reuse-live.ts +2 -2
- package/scripts/smoke-acp-skill-live.ts +2 -2
- package/scripts/smoke-acp-socket-citizen-live.ts +4 -3
- package/scripts/smoke-acp-v2-send-live.ts +4 -3
- package/scripts/smoke-agy-native-push-live.ts +10 -20
- package/scripts/smoke-claude-native-resume-live.sh +13 -3
- package/scripts/smoke-entwurf-chain-live.ts +352 -0
- package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -3
- package/scripts/smoke-meta-honesty.sh +1 -1
- package/scripts/smoke-meta-install-state.sh +4 -0
- package/scripts/smoke-mux-fresh-call-live.ts +365 -0
- package/scripts/smoke-mux-lifecycle-live.ts +1136 -0
- package/scripts/smoke-pi-attach.ts +1 -1
- package/scripts/smoke-user-scope-citizen.sh +1 -1
- package/scripts/tsconfig.json +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-preflight.js +0 -160
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +0 -273
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn.js +0 -216
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +0 -373
- package/pi-extensions/lib/entwurf-v2-spawn.ts +0 -323
- package/scripts/check-acp-sdk-surface.ts +0 -236
- package/scripts/check-entwurf-v2-spawn-production.ts +0 -551
- package/scripts/check-entwurf-v2-spawn.ts +0 -399
- package/scripts/smoke-entwurf-v2-spawn-live.ts +0 -188
- package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +0 -469
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-meta-facts — deterministic gate for the #65 owner-normalized store
|
|
3
|
+
* projection (`entwurf meta-facts` / scripts/meta-facts.ts).
|
|
4
|
+
*
|
|
5
|
+
* Drives the REAL CLI as a subprocess against sandboxed fixture stores (never the
|
|
6
|
+
* live store; every path is an explicit mkdtemp dir). What it proves:
|
|
7
|
+
*
|
|
8
|
+
* - the join is the FULL verbatim strict 9-field v3 record — gardenId,
|
|
9
|
+
* nativeSessionId AND transcriptPath cross together, sorted by gardenId;
|
|
10
|
+
* - drift / symlink / invalid-UTF-8 entries become in-band defects with exit 0,
|
|
11
|
+
* and a symlinked record's target bytes are never followed into the join;
|
|
12
|
+
* - the projection carries the owner's listing semantics, not a copy: a
|
|
13
|
+
* schema-invalid rival is dropped by parse BEFORE the uniqueness pass and can
|
|
14
|
+
* never quarantine a healthy citizen; a true duplicate pair loses BOTH sides;
|
|
15
|
+
* - output is byte-deterministic across runs;
|
|
16
|
+
* - a missing store is a readable EMPTY store (exit 0), an unreadable store is
|
|
17
|
+
* exit 3 with NO JSON (never mistakable for empty), bad argv is exit 2;
|
|
18
|
+
* - the surface is reachable: run.sh dispatches it and the bridge build emits
|
|
19
|
+
* its compiled twin (the installed-host fence).
|
|
20
|
+
*
|
|
21
|
+
* The gate spawns the CLI rather than importing its pieces because the exit code
|
|
22
|
+
* and stdout bytes ARE the contract a consumer holds. Every [QK:MF-*] token
|
|
23
|
+
* appears exactly once, on the assertion that fails for that claim.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import assert from "node:assert/strict";
|
|
27
|
+
import { spawnSync } from "node:child_process";
|
|
28
|
+
import fs from "node:fs";
|
|
29
|
+
import os from "node:os";
|
|
30
|
+
import path from "node:path";
|
|
31
|
+
import {
|
|
32
|
+
type MetaCitizenBackend,
|
|
33
|
+
type MetaIdentity,
|
|
34
|
+
serializeMetaIdentity,
|
|
35
|
+
} from "../pi-extensions/lib/meta-session.ts";
|
|
36
|
+
|
|
37
|
+
let passed = 0;
|
|
38
|
+
function ok(label: string, cond: boolean): void {
|
|
39
|
+
assert.ok(cond, label);
|
|
40
|
+
console.log(` ok ${label}`);
|
|
41
|
+
passed++;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const GID_A = "20260611T111111-aaaaaa";
|
|
45
|
+
const GID_B = "20260611T222222-bbbbbb";
|
|
46
|
+
const GID_C = "20260611T333333-cccccc";
|
|
47
|
+
|
|
48
|
+
function record(gardenId: string, over: Partial<MetaIdentity> = {}): string {
|
|
49
|
+
const backend: MetaCitizenBackend = "pi";
|
|
50
|
+
return serializeMetaIdentity({
|
|
51
|
+
schemaVersion: 3,
|
|
52
|
+
gardenId,
|
|
53
|
+
backend,
|
|
54
|
+
nativeSessionId: `n-${gardenId}`,
|
|
55
|
+
cwd: "/x",
|
|
56
|
+
model: "provider/model",
|
|
57
|
+
transcriptPath: `/t/${gardenId}.jsonl`,
|
|
58
|
+
createdAt: "2026-06-11T00:00:00.000Z",
|
|
59
|
+
recordUpdatedAt: "2026-06-11T00:00:00.000Z",
|
|
60
|
+
...over,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function store(files: Record<string, string | Buffer>): string {
|
|
65
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-meta-facts-"));
|
|
66
|
+
for (const [name, body] of Object.entries(files)) fs.writeFileSync(path.join(dir, name), body);
|
|
67
|
+
return dir;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface CliRun {
|
|
71
|
+
status: number | null;
|
|
72
|
+
stdout: string;
|
|
73
|
+
stderr: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function cli(args: string[], env: Record<string, string> = {}): CliRun {
|
|
77
|
+
const res = spawnSync(process.execPath, ["--experimental-strip-types", "scripts/meta-facts.ts", ...args], {
|
|
78
|
+
encoding: "utf8",
|
|
79
|
+
env: { ...process.env, ...env },
|
|
80
|
+
});
|
|
81
|
+
return { status: res.status, stdout: res.stdout ?? "", stderr: res.stderr ?? "" };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface Projection {
|
|
85
|
+
schemaVersion: number;
|
|
86
|
+
storeDir: string;
|
|
87
|
+
citizens: MetaIdentity[];
|
|
88
|
+
defects: { filename: string; message: string }[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function parseProjection(run: CliRun, label: string): Projection {
|
|
92
|
+
assert.equal(run.status, 0, `${label}: exit 0 (stderr: ${run.stderr})`);
|
|
93
|
+
return JSON.parse(run.stdout) as Projection;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── full verbatim join, sorted, deterministic ───────────────────────────────
|
|
97
|
+
{
|
|
98
|
+
const dir = store({
|
|
99
|
+
[`${GID_B}.meta.json`]: record(GID_B),
|
|
100
|
+
[`${GID_A}.meta.json`]: record(GID_A, { model: null, transcriptPath: null }),
|
|
101
|
+
});
|
|
102
|
+
const run = cli([dir]);
|
|
103
|
+
const facts = parseProjection(run, "valid store");
|
|
104
|
+
ok(
|
|
105
|
+
"projection declares schemaVersion 1 and the resolved storeDir",
|
|
106
|
+
facts.schemaVersion === 1 && facts.storeDir === path.resolve(dir),
|
|
107
|
+
);
|
|
108
|
+
assert.deepEqual(
|
|
109
|
+
facts.citizens,
|
|
110
|
+
[
|
|
111
|
+
{
|
|
112
|
+
schemaVersion: 3,
|
|
113
|
+
gardenId: GID_A,
|
|
114
|
+
backend: "pi",
|
|
115
|
+
nativeSessionId: `n-${GID_A}`,
|
|
116
|
+
cwd: "/x",
|
|
117
|
+
model: null,
|
|
118
|
+
transcriptPath: null,
|
|
119
|
+
createdAt: "2026-06-11T00:00:00.000Z",
|
|
120
|
+
recordUpdatedAt: "2026-06-11T00:00:00.000Z",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
schemaVersion: 3,
|
|
124
|
+
gardenId: GID_B,
|
|
125
|
+
backend: "pi",
|
|
126
|
+
nativeSessionId: `n-${GID_B}`,
|
|
127
|
+
cwd: "/x",
|
|
128
|
+
model: "provider/model",
|
|
129
|
+
transcriptPath: `/t/${GID_B}.jsonl`,
|
|
130
|
+
createdAt: "2026-06-11T00:00:00.000Z",
|
|
131
|
+
recordUpdatedAt: "2026-06-11T00:00:00.000Z",
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
"[QK:MF-FULL-V3-JOIN] citizens are the full verbatim 9-field v3 records, sorted by gardenId",
|
|
135
|
+
);
|
|
136
|
+
console.log(" ok citizens are the full verbatim 9-field v3 records, sorted by gardenId");
|
|
137
|
+
passed++;
|
|
138
|
+
ok("valid store has no defects", facts.defects.length === 0);
|
|
139
|
+
const again = cli([dir]);
|
|
140
|
+
ok("two runs over one store are byte-identical", again.status === 0 && again.stdout === run.stdout);
|
|
141
|
+
// default-dir resolution rides the same env override every runtime surface uses
|
|
142
|
+
const viaEnv = cli([], { ENTWURF_META_SESSIONS_DIR: dir });
|
|
143
|
+
ok(
|
|
144
|
+
"no argv resolves the store through ENTWURF_META_SESSIONS_DIR",
|
|
145
|
+
viaEnv.status === 0 && viaEnv.stdout === run.stdout,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── drift / symlink / invalid UTF-8 are in-band defects, exit 0 ─────────────
|
|
150
|
+
{
|
|
151
|
+
const outside = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-meta-facts-outside-"));
|
|
152
|
+
fs.writeFileSync(path.join(outside, "foreign.meta.json"), record(GID_C));
|
|
153
|
+
const dir = store({
|
|
154
|
+
[`${GID_A}.meta.json`]: record(GID_A),
|
|
155
|
+
[`${GID_B}.meta.json`]: record(GID_C), // body gid ≠ filename → drift
|
|
156
|
+
"20260611T444444-dddddd.meta.json": Buffer.from([0xff, 0xfe, 0x00, 0x7b]), // invalid UTF-8
|
|
157
|
+
});
|
|
158
|
+
fs.symlinkSync(path.join(outside, "foreign.meta.json"), path.join(dir, "20260611T555555-eeeeee.meta.json"));
|
|
159
|
+
const facts = parseProjection(cli([dir]), "defective store");
|
|
160
|
+
ok(
|
|
161
|
+
"[QK:MF-DEFECTS-IN-BAND] drift + symlink + invalid UTF-8 are in-band defects and the store still answers exit 0",
|
|
162
|
+
facts.defects.length === 3 && facts.citizens.length === 1 && facts.citizens[0]?.gardenId === GID_A,
|
|
163
|
+
);
|
|
164
|
+
ok(
|
|
165
|
+
"a symlinked record's target bytes are never followed into the join",
|
|
166
|
+
!facts.citizens.some((c) => c.gardenId === GID_C) &&
|
|
167
|
+
facts.defects.some((d) => d.message.includes("not a regular file")),
|
|
168
|
+
);
|
|
169
|
+
ok(
|
|
170
|
+
"drift and invalid-UTF-8 defects carry their own causes",
|
|
171
|
+
facts.defects.some((d) => d.message.includes("drift")) &&
|
|
172
|
+
facts.defects.some((d) => d.message.includes("not valid JSON")),
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── parse-before-uniqueness: a schema-invalid rival never quarantines ───────
|
|
177
|
+
{
|
|
178
|
+
const rival = record(GID_B, { nativeSessionId: `n-${GID_A}` }).replace('"schemaVersion": 3', '"schemaVersion": 2');
|
|
179
|
+
const dir = store({
|
|
180
|
+
[`${GID_A}.meta.json`]: record(GID_A),
|
|
181
|
+
[`${GID_B}.meta.json`]: rival,
|
|
182
|
+
});
|
|
183
|
+
const facts = parseProjection(cli([dir]), "invalid rival");
|
|
184
|
+
ok(
|
|
185
|
+
"a schema-invalid rival is a defect and the healthy citizen stays listed",
|
|
186
|
+
facts.citizens.length === 1 &&
|
|
187
|
+
facts.citizens[0]?.gardenId === GID_A &&
|
|
188
|
+
facts.defects.length === 1 &&
|
|
189
|
+
facts.defects[0]?.filename === `${GID_B}.meta.json`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── no-winner duplicates: both rivals leave the join ────────────────────────
|
|
194
|
+
{
|
|
195
|
+
const dir = store({
|
|
196
|
+
[`${GID_A}.meta.json`]: record(GID_A, { nativeSessionId: "n-shared" }),
|
|
197
|
+
[`${GID_B}.meta.json`]: record(GID_B, { nativeSessionId: "n-shared" }),
|
|
198
|
+
[`${GID_C}.meta.json`]: record(GID_C),
|
|
199
|
+
});
|
|
200
|
+
const facts = parseProjection(cli([dir]), "duplicate pair");
|
|
201
|
+
ok(
|
|
202
|
+
"a duplicate nativeSessionId pair loses BOTH sides; the unrelated citizen keeps listing",
|
|
203
|
+
facts.citizens.length === 1 && facts.citizens[0]?.gardenId === GID_C && facts.defects.length === 2,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ── missing store = readable EMPTY store ────────────────────────────────────
|
|
208
|
+
{
|
|
209
|
+
const gone = path.join(fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-meta-facts-")), "never-created");
|
|
210
|
+
const facts = parseProjection(cli([gone]), "missing store");
|
|
211
|
+
ok(
|
|
212
|
+
"a store that does not exist is a readable empty store (ENOENT only)",
|
|
213
|
+
facts.citizens.length === 0 && facts.defects.length === 0,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── unreadable store = exit 3, NO JSON ──────────────────────────────────────
|
|
218
|
+
{
|
|
219
|
+
const base = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-meta-facts-"));
|
|
220
|
+
const file = path.join(base, "not-a-dir");
|
|
221
|
+
fs.writeFileSync(file, "plain file");
|
|
222
|
+
const run = cli([path.join(file, "store")]); // ENOTDIR: the path runs THROUGH a regular file
|
|
223
|
+
ok(
|
|
224
|
+
"[QK:MF-EXIT-UNREADABLE] an unreadable store is exit 3 with no JSON — never mistakable for an empty one",
|
|
225
|
+
run.status === 3 && run.stdout === "" && run.stderr.includes("FAIL"),
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ── usage ───────────────────────────────────────────────────────────────────
|
|
230
|
+
{
|
|
231
|
+
ok("[QK:MF-USAGE] two positional args are a usage error (exit 2)", cli(["/a", "/b"]).status === 2);
|
|
232
|
+
ok("a dash argv is a usage error, not a store directory", cli(["--help"]).status === 2);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── reachability: dispatch + compiled twin ──────────────────────────────────
|
|
236
|
+
{
|
|
237
|
+
const runSh = fs.readFileSync("run.sh", "utf8");
|
|
238
|
+
ok(
|
|
239
|
+
"run.sh dispatches the meta-facts operator verb through run_ts",
|
|
240
|
+
/^ {2}meta-facts\)$/m.test(runSh) && runSh.includes("run_ts scripts/meta-facts.ts"),
|
|
241
|
+
);
|
|
242
|
+
const build = fs.readFileSync("mcp/entwurf-bridge/tsconfig.build.json", "utf8");
|
|
243
|
+
ok(
|
|
244
|
+
"the bridge build emits the compiled twin an installed host needs",
|
|
245
|
+
build.includes('"../../scripts/meta-facts.ts"'),
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
console.log(`check-meta-facts: ${passed} checks passed`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* check-meta-identity-consumers — deterministic gate for the V3-only identity
|
|
3
3
|
* consumer seam. A real temp dir; no backend, no network, no hook. Safe in the
|
|
4
|
-
* `pnpm check` static floor.
|
|
4
|
+
* `pnpm run check:full` static floor.
|
|
5
5
|
*
|
|
6
6
|
* Proves the consumers every live path stands on (MCP sender-marker check, prune,
|
|
7
7
|
* store-doctor, the identity writers):
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import assert from "node:assert/strict";
|
|
16
16
|
import fs from "node:fs";
|
|
17
|
+
import { syncBuiltinESMExports } from "node:module";
|
|
17
18
|
import os from "node:os";
|
|
18
19
|
import path from "node:path";
|
|
19
20
|
import {
|
|
@@ -27,6 +28,7 @@ import {
|
|
|
27
28
|
metaRecordFilename,
|
|
28
29
|
mintMetaIdentity,
|
|
29
30
|
parseMetaIdentity,
|
|
31
|
+
publishExclusiveIdentity,
|
|
30
32
|
readMailboxReceiptState,
|
|
31
33
|
readMetaInbox,
|
|
32
34
|
serializeMetaIdentity,
|
|
@@ -166,6 +168,173 @@ check("upsertMetaSession: creates the store dir if absent; leaves no .tmp residu
|
|
|
166
168
|
}
|
|
167
169
|
});
|
|
168
170
|
|
|
171
|
+
// ---------------------------------------------------------------- exclusive CREATE publish (#66 fail-closed)
|
|
172
|
+
check("publishExclusiveIdentity: absent final publishes, round-trips, no tmp residue", () => {
|
|
173
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "meta-session-excl-"));
|
|
174
|
+
try {
|
|
175
|
+
const record = mintMetaIdentity(claudeInput({ nativeSessionId: "excl-fresh" }), T0);
|
|
176
|
+
const file = path.join(dir, metaRecordFilename(record));
|
|
177
|
+
publishExclusiveIdentity(file, record);
|
|
178
|
+
assert.deepEqual(parseMetaIdentity(fs.readFileSync(file, "utf8")), record, "published bytes round-trip");
|
|
179
|
+
assert.deepEqual(
|
|
180
|
+
fs.readdirSync(dir).filter((f) => f.includes(".tmp-")),
|
|
181
|
+
[],
|
|
182
|
+
"normal exclusive publish leaves no tmp residue",
|
|
183
|
+
);
|
|
184
|
+
} finally {
|
|
185
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
check("publishExclusiveIdentity: pre-planted final REFUSES, victim byte-identical, tmp gone", () => {
|
|
190
|
+
// Deterministic collision fixture: the minted suffix is random, so the public
|
|
191
|
+
// upsert seam cannot be forced onto an occupied final path without a
|
|
192
|
+
// retry/sample trick — the primitive is the production subject here, and the
|
|
193
|
+
// wiring claim below pins upsert's create branch to it.
|
|
194
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "meta-session-excl-"));
|
|
195
|
+
try {
|
|
196
|
+
const victim = mintMetaIdentity(claudeInput({ nativeSessionId: "excl-victim" }), T0);
|
|
197
|
+
const rival = {
|
|
198
|
+
...mintMetaIdentity(claudeInput({ nativeSessionId: "excl-rival" }), T1),
|
|
199
|
+
gardenId: victim.gardenId,
|
|
200
|
+
};
|
|
201
|
+
const file = path.join(dir, metaRecordFilename(victim));
|
|
202
|
+
fs.writeFileSync(file, serializeMetaIdentity(victim));
|
|
203
|
+
const victimBytes = fs.readFileSync(file);
|
|
204
|
+
assert.throws(
|
|
205
|
+
() => publishExclusiveIdentity(file, rival),
|
|
206
|
+
(err: unknown) => {
|
|
207
|
+
assert.ok(err instanceof MetaRecordError, "collision throws MetaRecordError");
|
|
208
|
+
assert.match(err.message, /CREATE collision/, "named collision cause");
|
|
209
|
+
assert.ok(err.message.includes(victim.gardenId), "error names the colliding garden id");
|
|
210
|
+
assert.ok(err.message.includes(file), "error names the occupied record path");
|
|
211
|
+
return true;
|
|
212
|
+
},
|
|
213
|
+
"occupied final path must refuse loud, never replace [QK:META-CREATE-EXCLUSIVE]",
|
|
214
|
+
);
|
|
215
|
+
assert.ok(fs.readFileSync(file).equals(victimBytes), "victim bytes byte-identical");
|
|
216
|
+
assert.deepEqual(
|
|
217
|
+
fs.readdirSync(dir).filter((f) => f.includes(".tmp-")),
|
|
218
|
+
[],
|
|
219
|
+
"collision refusal leaves no tmp residue",
|
|
220
|
+
);
|
|
221
|
+
} finally {
|
|
222
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Measured official patch seam for the two error-contract cells below: patch the
|
|
228
|
+
* default CJS fs object, then `syncBuiltinESMExports()` so the production
|
|
229
|
+
* `import * as fs` namespace binding sees it (measured on this host: the
|
|
230
|
+
* namespace does NOT see the patch before sync, does after, and restores after
|
|
231
|
+
* the finally's sync). A non-EEXIST link errno and a post-publish cleanup
|
|
232
|
+
* failure cannot be forced on the real filesystem synchronously.
|
|
233
|
+
*/
|
|
234
|
+
function withPatchedFs(method: "linkSync" | "unlinkSync", impl: () => never, fn: () => void): void {
|
|
235
|
+
const real = fs[method];
|
|
236
|
+
(fs as unknown as Record<string, unknown>)[method] = impl;
|
|
237
|
+
syncBuiltinESMExports();
|
|
238
|
+
try {
|
|
239
|
+
fn();
|
|
240
|
+
} finally {
|
|
241
|
+
(fs as unknown as Record<string, unknown>)[method] = real;
|
|
242
|
+
syncBuiltinESMExports();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
check("publishExclusiveIdentity: non-EEXIST link failure rethrows the ORIGINAL error, tmp cleaned", () => {
|
|
247
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "meta-session-excl-"));
|
|
248
|
+
try {
|
|
249
|
+
const record = mintMetaIdentity(claudeInput({ nativeSessionId: "excl-eacces" }), T0);
|
|
250
|
+
const file = path.join(dir, metaRecordFilename(record));
|
|
251
|
+
const primary = Object.assign(new Error("EACCES: permission denied, link"), { code: "EACCES" });
|
|
252
|
+
withPatchedFs(
|
|
253
|
+
"linkSync",
|
|
254
|
+
() => {
|
|
255
|
+
throw primary;
|
|
256
|
+
},
|
|
257
|
+
() => {
|
|
258
|
+
assert.throws(
|
|
259
|
+
() => publishExclusiveIdentity(file, record),
|
|
260
|
+
(err: unknown) => {
|
|
261
|
+
assert.equal(err, primary, "the identical original error object is rethrown (errno/cause intact)");
|
|
262
|
+
return true;
|
|
263
|
+
},
|
|
264
|
+
"non-EEXIST link failure must surface the original error, not a translation",
|
|
265
|
+
);
|
|
266
|
+
},
|
|
267
|
+
);
|
|
268
|
+
assert.ok(!fs.existsSync(file), "no final record was published");
|
|
269
|
+
assert.deepEqual(
|
|
270
|
+
fs.readdirSync(dir).filter((f) => f.includes(".tmp-")),
|
|
271
|
+
[],
|
|
272
|
+
"tmp cleaned even when the link failed for a non-EEXIST reason",
|
|
273
|
+
);
|
|
274
|
+
} finally {
|
|
275
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
check("publishExclusiveIdentity: post-link cleanup failure is LOUD, final stays published, no rollback", () => {
|
|
280
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "meta-session-excl-"));
|
|
281
|
+
try {
|
|
282
|
+
const record = mintMetaIdentity(claudeInput({ nativeSessionId: "excl-cleanup" }), T0);
|
|
283
|
+
const file = path.join(dir, metaRecordFilename(record));
|
|
284
|
+
withPatchedFs(
|
|
285
|
+
"unlinkSync",
|
|
286
|
+
() => {
|
|
287
|
+
throw Object.assign(new Error("EPERM: operation not permitted, unlink"), { code: "EPERM" });
|
|
288
|
+
},
|
|
289
|
+
() => {
|
|
290
|
+
assert.throws(
|
|
291
|
+
() => publishExclusiveIdentity(file, record),
|
|
292
|
+
(err: unknown) => {
|
|
293
|
+
assert.ok(err instanceof MetaRecordError, "cleanup failure throws MetaRecordError");
|
|
294
|
+
assert.match(err.message, /already published/, "states the record IS published");
|
|
295
|
+
assert.match(err.message, /temp cleanup failed/, "names the cleanup failure");
|
|
296
|
+
assert.ok(err.message.includes(".tmp-"), "names the leftover tmp path");
|
|
297
|
+
assert.match(
|
|
298
|
+
err.message,
|
|
299
|
+
/no successful birth\/registration receipt/,
|
|
300
|
+
"states the transport-neutral abort consequence",
|
|
301
|
+
);
|
|
302
|
+
assert.match(err.message, /will ATTACH/, "states the retry semantics");
|
|
303
|
+
return true;
|
|
304
|
+
},
|
|
305
|
+
"a real cleanup failure after publish must be loud, never a false success [QK:META-CREATE-PUBLISHED-CLEANUP]",
|
|
306
|
+
);
|
|
307
|
+
},
|
|
308
|
+
);
|
|
309
|
+
assert.deepEqual(
|
|
310
|
+
parseMetaIdentity(fs.readFileSync(file, "utf8")),
|
|
311
|
+
record,
|
|
312
|
+
"the published final record remains and round-trips (no rollback)",
|
|
313
|
+
);
|
|
314
|
+
assert.equal(
|
|
315
|
+
fs.readdirSync(dir).filter((f) => f.includes(".tmp-")).length,
|
|
316
|
+
1,
|
|
317
|
+
"the tmp the primitive could not clean is still there (honest state)",
|
|
318
|
+
);
|
|
319
|
+
} finally {
|
|
320
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
check("upsertMetaSession: CREATE dispatches to the exclusive publish, ATTACH to the replace write", () => {
|
|
325
|
+
// Structural action-dispatch claim, deliberately minimal: runtime forcing of a
|
|
326
|
+
// create collision through upsertMetaSession is genuinely unavailable without
|
|
327
|
+
// a seam (the gid suffix is drawn from randomBytes inside mint), so the
|
|
328
|
+
// runtime oracles above prove the primitive and this claim proves the wiring.
|
|
329
|
+
const source = fs.readFileSync(new URL("../pi-extensions/lib/meta-session.ts", import.meta.url), "utf8");
|
|
330
|
+
assert.ok(
|
|
331
|
+
source.includes(
|
|
332
|
+
'if (decision.action === "create") publishExclusiveIdentity(file, decision.record);\n\telse atomicWriteIdentity(file, decision.record);',
|
|
333
|
+
),
|
|
334
|
+
"upsertMetaSession must split the write by action: create→exclusive, attach→replace [QK:META-CREATE-WIRING]",
|
|
335
|
+
);
|
|
336
|
+
});
|
|
337
|
+
|
|
169
338
|
expectThrows("upsertMetaSession: duplicate nativeSessionId in the store throws (fail-fast)", () => {
|
|
170
339
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "meta-session-store-"));
|
|
171
340
|
try {
|