@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,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-meta-capability-source — deterministic gate for 0.11 Stage 0 step 3D-3:
|
|
3
|
+
* the capability-source cut-over. mint/parse now read backend honesty metadata
|
|
4
|
+
* (wakeMode/deliveryLevel) from the capability registry (3C) via metaCapabilityFor,
|
|
5
|
+
* NOT from META_BACKEND_DESCRIPTORS. No backend, no network, no hook, no fs writes
|
|
6
|
+
* (the registry is the packaged file). Safe in the `pnpm check` static floor.
|
|
7
|
+
*
|
|
8
|
+
* Proves the cut-over without changing observable behaviour:
|
|
9
|
+
* - the lookup seam is registry-DRIVEN — fed a doctored registry, metaCapabilityFor
|
|
10
|
+
* follows it (so the value is read from the registry, not hardcoded off the const);
|
|
11
|
+
* the default still reads the shipped file.
|
|
12
|
+
* - mintMetaRecord sources delivery.wakeMode/deliveryLevel through that seam.
|
|
13
|
+
* - parseMetaRecord's drift guard is now registry-sourced: a stored wakeMode that
|
|
14
|
+
* contradicts the registry canonical is rejected; a consistent one round-trips.
|
|
15
|
+
* - behaviour preserved: registry ≡ META_BACKEND_DESCRIPTORS for the 3 existing
|
|
16
|
+
* backends (the const survives only as the drift-guard reference, 3C → 3D-3).
|
|
17
|
+
*
|
|
18
|
+
* Scope is 3D-3: the SOURCE moves. NOTE (post-3D-4): the consumers proven here are
|
|
19
|
+
* mintMetaRecord / parseMetaRecord — the V1 (legacy / dual-read) path, which is the
|
|
20
|
+
* only place wakeMode/deliveryLevel still flow from. The live v2 mint
|
|
21
|
+
* (mintMetaIdentity) carries NO delivery, so it never sources capability — the v2
|
|
22
|
+
* record has no wakeMode at all. check-entwurf-capabilities still owns the
|
|
23
|
+
* registry ≡ const drift guard.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import assert from "node:assert/strict";
|
|
27
|
+
import {
|
|
28
|
+
loadMetaCapabilityRegistry,
|
|
29
|
+
META_BACKEND_DESCRIPTORS,
|
|
30
|
+
META_BACKENDS,
|
|
31
|
+
type MetaCapabilityRegistry,
|
|
32
|
+
MetaRecordError,
|
|
33
|
+
metaCapabilityFor,
|
|
34
|
+
mintMetaRecord,
|
|
35
|
+
parseMetaRecord,
|
|
36
|
+
serializeMetaRecord,
|
|
37
|
+
} from "../pi-extensions/lib/meta-session.ts";
|
|
38
|
+
|
|
39
|
+
let passed = 0;
|
|
40
|
+
function ok(label: string, cond: boolean): void {
|
|
41
|
+
assert.ok(cond, label);
|
|
42
|
+
console.log(` ok ${label}`);
|
|
43
|
+
passed++;
|
|
44
|
+
}
|
|
45
|
+
function throws(label: string, fn: () => unknown): void {
|
|
46
|
+
assert.throws(fn, MetaRecordError, label);
|
|
47
|
+
console.log(` ok ${label}`);
|
|
48
|
+
passed++;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const MINT = { nativeSessionId: "n-capsrc", transcriptPath: "/tmp/t.jsonl", cwd: "/tmp" };
|
|
52
|
+
|
|
53
|
+
// --- the seam is registry-driven, not const-hardcoded ----------------------
|
|
54
|
+
// Doctor the shipped registry (flip claude-code's wakeMode) and feed it in: if the
|
|
55
|
+
// lookup follows the doctored value, it is reading from the registry, not the const.
|
|
56
|
+
const shipped = loadMetaCapabilityRegistry();
|
|
57
|
+
const flip = (w: "self-fetch" | "direct-inject") => (w === "self-fetch" ? "direct-inject" : "self-fetch");
|
|
58
|
+
const doctored: MetaCapabilityRegistry = {
|
|
59
|
+
schemaVersion: shipped.schemaVersion,
|
|
60
|
+
backends: {
|
|
61
|
+
...shipped.backends,
|
|
62
|
+
"claude-code": { ...shipped.backends["claude-code"], wakeMode: flip(shipped.backends["claude-code"].wakeMode) },
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
const doctoredWake = doctored.backends["claude-code"].wakeMode;
|
|
66
|
+
ok(
|
|
67
|
+
"seam is registry-driven: metaCapabilityFor follows an injected (doctored) registry",
|
|
68
|
+
metaCapabilityFor("claude-code", doctored).wakeMode === doctoredWake &&
|
|
69
|
+
doctoredWake !== shipped.backends["claude-code"].wakeMode,
|
|
70
|
+
);
|
|
71
|
+
ok(
|
|
72
|
+
"seam default reads the shipped registry, not the doctored copy",
|
|
73
|
+
metaCapabilityFor("claude-code").wakeMode === shipped.backends["claude-code"].wakeMode,
|
|
74
|
+
);
|
|
75
|
+
const load1 = loadMetaCapabilityRegistry();
|
|
76
|
+
const load2 = loadMetaCapabilityRegistry();
|
|
77
|
+
ok("loadMetaCapabilityRegistry memoizes (same object on repeat load)", load1 === load2);
|
|
78
|
+
|
|
79
|
+
// --- mint sources delivery metadata through the seam -----------------------
|
|
80
|
+
for (const backend of META_BACKENDS) {
|
|
81
|
+
const cap = metaCapabilityFor(backend);
|
|
82
|
+
const r = mintMetaRecord({ backend, ...MINT });
|
|
83
|
+
ok(`mint(${backend}): delivery.wakeMode sourced from the registry`, r.delivery.wakeMode === cap.wakeMode);
|
|
84
|
+
ok(
|
|
85
|
+
`mint(${backend}): delivery.deliveryLevel sourced from the registry`,
|
|
86
|
+
r.delivery.deliveryLevel === cap.deliveryLevel,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// --- cut-over preserves behaviour: registry ≡ const for the 3 backends ------
|
|
91
|
+
for (const backend of META_BACKENDS) {
|
|
92
|
+
const cap = metaCapabilityFor(backend);
|
|
93
|
+
const d = META_BACKEND_DESCRIPTORS[backend];
|
|
94
|
+
ok(
|
|
95
|
+
`cut-over preserves behaviour: registry ≡ const for ${backend} (wakeMode/deliveryLevel)`,
|
|
96
|
+
cap.wakeMode === d.wakeMode && cap.deliveryLevel === d.deliveryLevel,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// --- parse drift guard is now registry-sourced -----------------------------
|
|
101
|
+
const good = mintMetaRecord({ backend: "claude-code", ...MINT });
|
|
102
|
+
const tampered = JSON.parse(serializeMetaRecord(good)) as { delivery: { wakeMode: string } };
|
|
103
|
+
tampered.delivery.wakeMode = flip(metaCapabilityFor("claude-code").wakeMode); // contradicts the registry canonical
|
|
104
|
+
throws("parse: a wakeMode contradicting the registry canonical throws (drift guard registry-sourced)", () =>
|
|
105
|
+
parseMetaRecord(JSON.stringify(tampered)),
|
|
106
|
+
);
|
|
107
|
+
ok(
|
|
108
|
+
"parse: a registry-consistent record round-trips",
|
|
109
|
+
parseMetaRecord(serializeMetaRecord(good)).delivery.wakeMode === metaCapabilityFor("claude-code").wakeMode,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
console.log(`[check-meta-capability-source] ${passed} assertions ok`);
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-meta-dual-consumers — deterministic gate for 0.11 Stage 0 step 3D-4 commit1
|
|
3
|
+
* (green checkpoint). The delivery-agnostic dual-read seam: readMetaIdentityByGardenId
|
|
4
|
+
* + scanIdentityByNativeId read v1 AND v2 records and return normalized identity, so
|
|
5
|
+
* the consumers that move onto them (MCP sender-marker check, prune, store-doctor, and
|
|
6
|
+
* the v2 upsert's existence scan in commit2) survive the v2 cut. Pure + a real temp
|
|
7
|
+
* dir; no backend, no network, no hook. Safe in the `pnpm check` static floor.
|
|
8
|
+
*
|
|
9
|
+
* Proves the dual-read identity seam (the live path + scanIdentityByNativeId now read
|
|
10
|
+
* v1 AND v2; the v1-only raw readers remain for v1-fixture gates):
|
|
11
|
+
* - readMetaIdentityByGardenId normalizes a v1 file (lastSeen→recordUpdatedAt,
|
|
12
|
+
* transcriptPath carried, model/parentGardenId null, isEntwurf false) and reads a v2
|
|
13
|
+
* file as-is; body/filename gardenId drift fails-fast.
|
|
14
|
+
* - scanIdentityByNativeId matches by nativeSessionId across BOTH schemas, returns null
|
|
15
|
+
* on no match, skips non-.meta.json, surfaces malformed via onSkip, and — THE G1
|
|
16
|
+
* invariant — flags a nativeSessionId duplicated across a v1 AND a v2 file as
|
|
17
|
+
* authority ambiguity (throw), so the v2 upsert never mints a duplicate id for an
|
|
18
|
+
* existing citizen.
|
|
19
|
+
*
|
|
20
|
+
* Scope is commit1 ONLY: dual-read capability, additive. The cut (enqueue/read
|
|
21
|
+
* record-stamp stop + v2 upsert/migration) is commit2 = 끊을 지점 ②.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import assert from "node:assert/strict";
|
|
25
|
+
import fs from "node:fs";
|
|
26
|
+
import os from "node:os";
|
|
27
|
+
import path from "node:path";
|
|
28
|
+
import {
|
|
29
|
+
type MetaIdentity,
|
|
30
|
+
MetaRecordError,
|
|
31
|
+
mintMetaRecord,
|
|
32
|
+
readMetaIdentityByGardenId,
|
|
33
|
+
scanIdentityByNativeId,
|
|
34
|
+
serializeMetaIdentity,
|
|
35
|
+
serializeMetaRecord,
|
|
36
|
+
} from "../pi-extensions/lib/meta-session.ts";
|
|
37
|
+
|
|
38
|
+
let passed = 0;
|
|
39
|
+
function ok(label: string, cond: boolean): void {
|
|
40
|
+
assert.ok(cond, label);
|
|
41
|
+
console.log(` ok ${label}`);
|
|
42
|
+
passed++;
|
|
43
|
+
}
|
|
44
|
+
function throws(label: string, fn: () => unknown): void {
|
|
45
|
+
assert.throws(fn, MetaRecordError, label);
|
|
46
|
+
console.log(` ok ${label}`);
|
|
47
|
+
passed++;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const T0 = new Date("2026-03-01T12:00:00.000Z");
|
|
51
|
+
|
|
52
|
+
// A v1 record (mint → serialize) and a v2 identity (hand-built → serialize). The v2
|
|
53
|
+
// id is a pi citizen with a null transcriptPath (nullable-at-birth), exercising the
|
|
54
|
+
// shape v1 never had.
|
|
55
|
+
const v1 = mintMetaRecord(
|
|
56
|
+
{ backend: "claude-code", nativeSessionId: "n-v1", transcriptPath: "/tmp/t.jsonl", cwd: "/tmp/v1" },
|
|
57
|
+
T0,
|
|
58
|
+
);
|
|
59
|
+
const v1Json = serializeMetaRecord(v1);
|
|
60
|
+
const V2_GID = "20260301T120000-aaaaaa";
|
|
61
|
+
const v2: MetaIdentity = {
|
|
62
|
+
schemaVersion: 2,
|
|
63
|
+
gardenId: V2_GID,
|
|
64
|
+
backend: "pi",
|
|
65
|
+
nativeSessionId: "n-v2",
|
|
66
|
+
cwd: "/tmp/v2",
|
|
67
|
+
model: null,
|
|
68
|
+
transcriptPath: null,
|
|
69
|
+
parentGardenId: null,
|
|
70
|
+
isEntwurf: false,
|
|
71
|
+
createdAt: T0.toISOString(),
|
|
72
|
+
recordUpdatedAt: T0.toISOString(),
|
|
73
|
+
};
|
|
74
|
+
const v2Json = serializeMetaIdentity(v2);
|
|
75
|
+
|
|
76
|
+
// --- scanIdentityByNativeId: dual-schema in-memory scan --------------------
|
|
77
|
+
const reader = (f: string): string => {
|
|
78
|
+
if (f === `${v1.gardenId}.meta.json`) return v1Json;
|
|
79
|
+
if (f === `${V2_GID}.meta.json`) return v2Json;
|
|
80
|
+
throw new Error(`no fixture for ${f}`);
|
|
81
|
+
};
|
|
82
|
+
const entries = [`${v1.gardenId}.meta.json`, `${V2_GID}.meta.json`, "README.md"];
|
|
83
|
+
|
|
84
|
+
const foundV1 = scanIdentityByNativeId(entries, "n-v1", reader);
|
|
85
|
+
ok(
|
|
86
|
+
"scanIdentityByNativeId: matches a v1 record (normalized identity)",
|
|
87
|
+
foundV1?.gardenId === v1.gardenId && foundV1?.backend === "claude-code",
|
|
88
|
+
);
|
|
89
|
+
const foundV2 = scanIdentityByNativeId(entries, "n-v2", reader);
|
|
90
|
+
ok(
|
|
91
|
+
"scanIdentityByNativeId: matches a v2 record (pi, null transcriptPath)",
|
|
92
|
+
foundV2?.gardenId === V2_GID && foundV2?.transcriptPath === null,
|
|
93
|
+
);
|
|
94
|
+
ok(
|
|
95
|
+
"scanIdentityByNativeId: returns null when no body matches",
|
|
96
|
+
scanIdentityByNativeId(entries, "n-absent", reader) === null,
|
|
97
|
+
);
|
|
98
|
+
ok(
|
|
99
|
+
"scanIdentityByNativeId: ignores non-.meta.json entries (README.md not read)",
|
|
100
|
+
scanIdentityByNativeId(["README.md"], "n-v1", reader) === null,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// THE G1 invariant: the same nativeSessionId in a v1 AND a v2 file is an authority
|
|
104
|
+
// ambiguity across schemas — fail-fast, so the v2 upsert never duplicate-mints.
|
|
105
|
+
const dupReader = (f: string): string =>
|
|
106
|
+
f === "a.meta.json" ? v1Json : f === "b.meta.json" ? v2Json.replace(`"n-v2"`, `"n-v1"`) : "";
|
|
107
|
+
throws("scanIdentityByNativeId: nativeSessionId duplicated across v1 AND v2 → ambiguity throw (G1)", () =>
|
|
108
|
+
scanIdentityByNativeId(["a.meta.json", "b.meta.json"], "n-v1", dupReader),
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
// malformed entry surfaces via onSkip, scan continues to find the good one
|
|
112
|
+
const skipped: string[] = [];
|
|
113
|
+
const mixedReader = (f: string): string => (f === "bad.meta.json" ? "{not json" : v1Json);
|
|
114
|
+
const survived = scanIdentityByNativeId(["bad.meta.json", `${v1.gardenId}.meta.json`], "n-v1", mixedReader, (f) =>
|
|
115
|
+
skipped.push(f),
|
|
116
|
+
);
|
|
117
|
+
ok(
|
|
118
|
+
"scanIdentityByNativeId: malformed entry skipped via onSkip, scan continues",
|
|
119
|
+
survived?.gardenId === v1.gardenId && skipped.length === 1,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// --- readMetaIdentityByGardenId: dual-schema read-by-gardenId on disk ------
|
|
123
|
+
const tmpRoot = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "psa-dualcons-")));
|
|
124
|
+
try {
|
|
125
|
+
fs.writeFileSync(path.join(tmpRoot, `${v1.gardenId}.meta.json`), v1Json);
|
|
126
|
+
fs.writeFileSync(path.join(tmpRoot, `${V2_GID}.meta.json`), v2Json);
|
|
127
|
+
|
|
128
|
+
const idV1 = readMetaIdentityByGardenId(v1.gardenId, tmpRoot);
|
|
129
|
+
ok(
|
|
130
|
+
"readMetaIdentityByGardenId: v1 file normalizes (lastSeen→recordUpdatedAt, transcript carried, model/parent null, not entwurf)",
|
|
131
|
+
idV1.schemaVersion === 2 &&
|
|
132
|
+
idV1.recordUpdatedAt === v1.lastSeen &&
|
|
133
|
+
idV1.transcriptPath === "/tmp/t.jsonl" &&
|
|
134
|
+
idV1.model === null &&
|
|
135
|
+
idV1.parentGardenId === null &&
|
|
136
|
+
idV1.isEntwurf === false,
|
|
137
|
+
);
|
|
138
|
+
const idV2 = readMetaIdentityByGardenId(V2_GID, tmpRoot);
|
|
139
|
+
ok(
|
|
140
|
+
"readMetaIdentityByGardenId: v2 file reads as-is (pi, null transcriptPath)",
|
|
141
|
+
idV2.backend === "pi" && idV2.transcriptPath === null,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// body/filename gardenId drift is corruption — fail-fast (body is SSOT).
|
|
145
|
+
const OTHER = "20260301T120000-bbbbbb";
|
|
146
|
+
fs.writeFileSync(path.join(tmpRoot, `${OTHER}.meta.json`), v2Json); // body says V2_GID, filename says OTHER
|
|
147
|
+
throws("readMetaIdentityByGardenId: body/filename gardenId drift is corruption", () =>
|
|
148
|
+
readMetaIdentityByGardenId(OTHER, tmpRoot),
|
|
149
|
+
);
|
|
150
|
+
} finally {
|
|
151
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
console.log(`[check-meta-dual-consumers] ${passed} assertions ok`);
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-meta-dual-read — deterministic gate for 0.11 Stage 0 step 3D-1: the v2
|
|
3
|
+
* write shape + the dual-read dispatcher. Pure functions; no fs, no backend, no
|
|
4
|
+
* API. Safe in the `pnpm check` static floor.
|
|
5
|
+
*
|
|
6
|
+
* Proves:
|
|
7
|
+
* - serializeMetaIdentity is canonical (stable order, 2-space, trailing \n) and
|
|
8
|
+
* round-trips through parseMetaRecordV2,
|
|
9
|
+
* - parseMetaRecordAny peeks schemaVersion and routes to the right strict
|
|
10
|
+
* parser (v1 keeps delivery; v2 is identity), and refuses unknown versions,
|
|
11
|
+
* - parseMetaIdentity reads EITHER version straight to a normalized identity,
|
|
12
|
+
* - write → read identity is stable: parseMetaIdentity(serializeMetaIdentity).
|
|
13
|
+
*
|
|
14
|
+
* Scope is 3D-1 ONLY: pure serializer + dispatcher. NO fs upsert wiring, NO
|
|
15
|
+
* readMetaInbox/enqueueMetaMessage change, NO record.delivery removal — those are
|
|
16
|
+
* 3D-2/3/4. The v1 fixtures here are sanitized (never a real disk record).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import assert from "node:assert/strict";
|
|
20
|
+
import {
|
|
21
|
+
type MetaIdentity,
|
|
22
|
+
MetaRecordError,
|
|
23
|
+
parseMetaIdentity,
|
|
24
|
+
parseMetaRecordAny,
|
|
25
|
+
serializeMetaIdentity,
|
|
26
|
+
} from "../pi-extensions/lib/meta-session.ts";
|
|
27
|
+
|
|
28
|
+
let passed = 0;
|
|
29
|
+
function ok(label: string, cond: boolean): void {
|
|
30
|
+
assert.ok(cond, label);
|
|
31
|
+
console.log(` ok ${label}`);
|
|
32
|
+
passed++;
|
|
33
|
+
}
|
|
34
|
+
function golden(label: string, actual: unknown, expected: unknown): void {
|
|
35
|
+
assert.deepStrictEqual(actual, expected, label);
|
|
36
|
+
console.log(` ok ${label}`);
|
|
37
|
+
passed++;
|
|
38
|
+
}
|
|
39
|
+
function throws(label: string, fn: () => unknown): void {
|
|
40
|
+
assert.throws(fn, MetaRecordError, label);
|
|
41
|
+
console.log(` ok ${label}`);
|
|
42
|
+
passed++;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Sanitized fixtures (synthetic paths; gardenId obeys the SSOT grammar).
|
|
46
|
+
const V1_FIXTURE_JSON = JSON.stringify({
|
|
47
|
+
schemaVersion: 1,
|
|
48
|
+
gardenId: "20260101T000000-0a1b2c",
|
|
49
|
+
backend: "claude-code",
|
|
50
|
+
nativeSessionId: "n-fixture-0001",
|
|
51
|
+
transcriptPath: "/synthetic/project/.transcript.jsonl",
|
|
52
|
+
cwd: "/synthetic/project",
|
|
53
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
54
|
+
lastSeen: "2026-01-02T03:04:05.000Z",
|
|
55
|
+
delivery: {
|
|
56
|
+
wakeMode: "self-fetch",
|
|
57
|
+
deliveryLevel: "D6",
|
|
58
|
+
lastEnqueuedAt: null,
|
|
59
|
+
lastDeliveredAt: null,
|
|
60
|
+
lastReadAt: "2026-01-02T03:04:05.000Z",
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// The v2 identity the v1 fixture normalizes to (= the 3A golden).
|
|
65
|
+
const GOLDEN_V2_IDENTITY: MetaIdentity = {
|
|
66
|
+
schemaVersion: 2,
|
|
67
|
+
gardenId: "20260101T000000-0a1b2c",
|
|
68
|
+
backend: "claude-code",
|
|
69
|
+
nativeSessionId: "n-fixture-0001",
|
|
70
|
+
cwd: "/synthetic/project",
|
|
71
|
+
model: null,
|
|
72
|
+
transcriptPath: "/synthetic/project/.transcript.jsonl",
|
|
73
|
+
parentGardenId: null,
|
|
74
|
+
isEntwurf: false,
|
|
75
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
76
|
+
recordUpdatedAt: "2026-01-02T03:04:05.000Z",
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const V2_PI_IDENTITY: MetaIdentity = {
|
|
80
|
+
schemaVersion: 2,
|
|
81
|
+
gardenId: "20260301T120000-abc123",
|
|
82
|
+
backend: "pi",
|
|
83
|
+
nativeSessionId: "pi-sess-xyz",
|
|
84
|
+
cwd: "/synthetic/pi-proj",
|
|
85
|
+
model: null,
|
|
86
|
+
transcriptPath: null,
|
|
87
|
+
parentGardenId: "20260301T115900-aaaaaa",
|
|
88
|
+
isEntwurf: true,
|
|
89
|
+
createdAt: "2026-03-01T12:00:00.000Z",
|
|
90
|
+
recordUpdatedAt: "2026-03-01T12:30:00.000Z",
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// --- serializeMetaIdentity (the v2 write shape) -----------------------------
|
|
94
|
+
ok(
|
|
95
|
+
"serializeMetaIdentity: stable key order, 2-space indent, trailing newline, schemaVersion 2 first",
|
|
96
|
+
serializeMetaIdentity(GOLDEN_V2_IDENTITY).startsWith(
|
|
97
|
+
'{\n "schemaVersion": 2,\n "gardenId": "20260101T000000-0a1b2c",',
|
|
98
|
+
) && serializeMetaIdentity(GOLDEN_V2_IDENTITY).endsWith("\n}\n"),
|
|
99
|
+
);
|
|
100
|
+
ok(
|
|
101
|
+
"serializeMetaIdentity is deterministic (same identity → byte-identical)",
|
|
102
|
+
serializeMetaIdentity(GOLDEN_V2_IDENTITY) === serializeMetaIdentity({ ...GOLDEN_V2_IDENTITY }),
|
|
103
|
+
);
|
|
104
|
+
ok(
|
|
105
|
+
"serializeMetaIdentity output carries no v1 delivery/lastSeen keys",
|
|
106
|
+
!serializeMetaIdentity(GOLDEN_V2_IDENTITY).includes("delivery") &&
|
|
107
|
+
!serializeMetaIdentity(GOLDEN_V2_IDENTITY).includes("lastSeen"),
|
|
108
|
+
);
|
|
109
|
+
// write → read identity stability (serialize then dual-read back to identity).
|
|
110
|
+
golden(
|
|
111
|
+
"serialize → parseMetaIdentity round-trips (v2 identity stable)",
|
|
112
|
+
parseMetaIdentity(serializeMetaIdentity(GOLDEN_V2_IDENTITY)),
|
|
113
|
+
GOLDEN_V2_IDENTITY,
|
|
114
|
+
);
|
|
115
|
+
golden(
|
|
116
|
+
"serialize → parseMetaIdentity round-trips (pi identity stable)",
|
|
117
|
+
parseMetaIdentity(serializeMetaIdentity(V2_PI_IDENTITY)),
|
|
118
|
+
V2_PI_IDENTITY,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// --- parseMetaRecordAny (the dual-read dispatcher) --------------------------
|
|
122
|
+
const anyV1 = parseMetaRecordAny(V1_FIXTURE_JSON);
|
|
123
|
+
ok(
|
|
124
|
+
"parseMetaRecordAny(v1) returns the v1 record in its OWN shape (delivery retained)",
|
|
125
|
+
anyV1.schemaVersion === 1 && "delivery" in anyV1 && anyV1.delivery.lastReadAt === "2026-01-02T03:04:05.000Z",
|
|
126
|
+
);
|
|
127
|
+
const anyV2 = parseMetaRecordAny(serializeMetaIdentity(V2_PI_IDENTITY));
|
|
128
|
+
ok(
|
|
129
|
+
"parseMetaRecordAny(v2) returns the v2 identity in its OWN shape",
|
|
130
|
+
anyV2.schemaVersion === 2 && !("delivery" in anyV2),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// --- parseMetaIdentity (dual-read straight to identity) --------------------
|
|
134
|
+
golden(
|
|
135
|
+
"parseMetaIdentity(v1 raw) → normalized v2 identity golden",
|
|
136
|
+
parseMetaIdentity(V1_FIXTURE_JSON),
|
|
137
|
+
GOLDEN_V2_IDENTITY,
|
|
138
|
+
);
|
|
139
|
+
golden(
|
|
140
|
+
"parseMetaIdentity(v2 raw) → identity (idempotent)",
|
|
141
|
+
parseMetaIdentity(serializeMetaIdentity(V2_PI_IDENTITY)),
|
|
142
|
+
V2_PI_IDENTITY,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// --- crash, don't warn -----------------------------------------------------
|
|
146
|
+
throws("parseMetaRecordAny: invalid JSON throws", () => parseMetaRecordAny("{nope"));
|
|
147
|
+
throws("parseMetaRecordAny: array (non-object) throws", () => parseMetaRecordAny("[]"));
|
|
148
|
+
throws("parseMetaRecordAny: unknown schemaVersion (3) throws", () =>
|
|
149
|
+
parseMetaRecordAny(JSON.stringify({ ...JSON.parse(V1_FIXTURE_JSON), schemaVersion: 3 })),
|
|
150
|
+
);
|
|
151
|
+
throws("parseMetaRecordAny: missing schemaVersion throws", () => parseMetaRecordAny(JSON.stringify({ gardenId: "x" })));
|
|
152
|
+
// the dispatcher delegates to the STRICT per-version parser — a corrupt v2 body
|
|
153
|
+
// (stale v1 delivery under schemaVersion 2) must still be rejected through Any.
|
|
154
|
+
throws("parseMetaRecordAny(v2 with stale delivery) is rejected by the strict v2 parser", () =>
|
|
155
|
+
parseMetaRecordAny(JSON.stringify({ ...V2_PI_IDENTITY, delivery: {} })),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
console.log(`[check-meta-dual-read] ${passed} assertions ok`);
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-meta-listing — deterministic gate for `listAllMetaIdentities` (0.11
|
|
3
|
+
* Stage 0 step 4, slice 4a). The meta-store axis the fact-provider consumes.
|
|
4
|
+
*
|
|
5
|
+
* Proves the explicit-partial contract (Fable Q2 / GPT힣):
|
|
6
|
+
* - valid records → identities, sorted by gardenId,
|
|
7
|
+
* - a parse failure does NOT blind the listing — valid records still surface,
|
|
8
|
+
* and the corrupt one becomes an explicit error (the 0.10 "corrupt blocks
|
|
9
|
+
* registration forever" lesson),
|
|
10
|
+
* - body/filename drift (body gardenId ≠ filename) → error, excluded from
|
|
11
|
+
* identities (same authority check as readMetaIdentityByGardenId),
|
|
12
|
+
* - a throwing reader (file vanished) → error, not a crash,
|
|
13
|
+
* - non-`.meta.json` entries ignored,
|
|
14
|
+
* - verbatim-or-nothing: an error carries ONLY {filename, message} — never a
|
|
15
|
+
* half-parsed identity field (a salvaged gid string as a fact = synthetic),
|
|
16
|
+
* - mode "strict" throws on any error; "collect" (default) returns partial,
|
|
17
|
+
* - errors sorted by filename.
|
|
18
|
+
*
|
|
19
|
+
* Pure; entries/readRecord injected, no IO.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import assert from "node:assert/strict";
|
|
23
|
+
import {
|
|
24
|
+
listAllMetaIdentities,
|
|
25
|
+
type MetaBackendV2,
|
|
26
|
+
type MetaIdentity,
|
|
27
|
+
serializeMetaIdentity,
|
|
28
|
+
} from "../pi-extensions/lib/meta-session.ts";
|
|
29
|
+
|
|
30
|
+
let passed = 0;
|
|
31
|
+
function ok(label: string, cond: boolean): void {
|
|
32
|
+
assert.ok(cond, label);
|
|
33
|
+
console.log(` ok ${label}`);
|
|
34
|
+
passed++;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const GID_A = "20260611T111111-aaaaaa";
|
|
38
|
+
const GID_B = "20260611T222222-bbbbbb";
|
|
39
|
+
|
|
40
|
+
function rec(gardenId: string, over: Partial<MetaIdentity> = {}): string {
|
|
41
|
+
const backend: MetaBackendV2 = "pi";
|
|
42
|
+
return serializeMetaIdentity({
|
|
43
|
+
schemaVersion: 2,
|
|
44
|
+
gardenId,
|
|
45
|
+
backend,
|
|
46
|
+
nativeSessionId: `n-${gardenId}`,
|
|
47
|
+
cwd: "/x",
|
|
48
|
+
model: null,
|
|
49
|
+
transcriptPath: null,
|
|
50
|
+
parentGardenId: null,
|
|
51
|
+
isEntwurf: false,
|
|
52
|
+
createdAt: "2026-06-11T00:00:00.000Z",
|
|
53
|
+
recordUpdatedAt: "2026-06-11T00:00:00.000Z",
|
|
54
|
+
...over,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function reader(map: Record<string, string>): (filename: string) => string {
|
|
59
|
+
return (filename: string) => {
|
|
60
|
+
const v = map[filename];
|
|
61
|
+
if (v === undefined) throw new Error(`ENOENT: ${filename}`);
|
|
62
|
+
return v;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── valid records → identities, sorted ──────────────────────────────────────
|
|
67
|
+
{
|
|
68
|
+
const map = { [`${GID_B}.meta.json`]: rec(GID_B), [`${GID_A}.meta.json`]: rec(GID_A) };
|
|
69
|
+
const { identities, errors } = listAllMetaIdentities(Object.keys(map), reader(map));
|
|
70
|
+
ok("2 valid → 2 identities, 0 errors", identities.length === 2 && errors.length === 0);
|
|
71
|
+
ok("identities sorted by gardenId", identities[0]?.gardenId === GID_A);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── corrupt record does NOT blind the listing ───────────────────────────────
|
|
75
|
+
{
|
|
76
|
+
const map = { [`${GID_A}.meta.json`]: rec(GID_A), [`${GID_B}.meta.json`]: "{ broken json" };
|
|
77
|
+
const { identities, errors } = listAllMetaIdentities(Object.keys(map), reader(map));
|
|
78
|
+
ok("corrupt 1 → valid still listed (not blinded)", identities.length === 1 && identities[0]?.gardenId === GID_A);
|
|
79
|
+
ok("corrupt → exactly 1 error", errors.length === 1);
|
|
80
|
+
ok("error names the corrupt file", errors[0]?.filename === `${GID_B}.meta.json`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── body/filename drift → error, excluded ───────────────────────────────────
|
|
84
|
+
{
|
|
85
|
+
const map = { [`${GID_A}.meta.json`]: rec(GID_B) }; // filename A, body claims B
|
|
86
|
+
const { identities, errors } = listAllMetaIdentities(Object.keys(map), reader(map));
|
|
87
|
+
ok("drift → excluded from identities", identities.length === 0);
|
|
88
|
+
ok("drift → 1 error mentioning drift", errors.length === 1 && /drift/.test(errors[0]?.message ?? ""));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── throwing reader (file vanished) → error, not crash ──────────────────────
|
|
92
|
+
{
|
|
93
|
+
const read = (f: string): string => {
|
|
94
|
+
if (f === `${GID_B}.meta.json`) throw new Error("ENOENT mid-scan");
|
|
95
|
+
return rec(GID_A);
|
|
96
|
+
};
|
|
97
|
+
const { identities, errors } = listAllMetaIdentities([`${GID_A}.meta.json`, `${GID_B}.meta.json`], read);
|
|
98
|
+
ok("reader throw → error, valid still listed", identities.length === 1 && errors.length === 1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── non-.meta.json ignored ──────────────────────────────────────────────────
|
|
102
|
+
{
|
|
103
|
+
const map = { [`${GID_A}.meta.json`]: rec(GID_A), "README.txt": "x", "inbox.signal": "y" };
|
|
104
|
+
const { identities, errors } = listAllMetaIdentities(Object.keys(map), reader(map));
|
|
105
|
+
ok("non-.meta.json entries ignored", identities.length === 1 && errors.length === 0);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── verbatim-or-nothing: error keyset = {filename, message} ─────────────────
|
|
109
|
+
{
|
|
110
|
+
const map = { [`${GID_A}.meta.json`]: "{ broken" };
|
|
111
|
+
const { errors } = listAllMetaIdentities(Object.keys(map), reader(map));
|
|
112
|
+
const keys = Object.keys(errors[0] ?? {}).sort();
|
|
113
|
+
assert.deepStrictEqual(keys, ["filename", "message"], `error keyset drift: ${keys.join(",")}`);
|
|
114
|
+
ok("error verbatim-or-nothing (filename + message only, no identity field)", true);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── strict vs collect ───────────────────────────────────────────────────────
|
|
118
|
+
{
|
|
119
|
+
const map = { [`${GID_A}.meta.json`]: rec(GID_A), [`${GID_B}.meta.json`]: "{ broken" };
|
|
120
|
+
let threw = false;
|
|
121
|
+
try {
|
|
122
|
+
listAllMetaIdentities(Object.keys(map), reader(map), { mode: "strict" });
|
|
123
|
+
} catch {
|
|
124
|
+
threw = true;
|
|
125
|
+
}
|
|
126
|
+
ok("mode strict → throw if any record unreadable", threw);
|
|
127
|
+
const { identities } = listAllMetaIdentities(Object.keys(map), reader(map), { mode: "collect" });
|
|
128
|
+
ok("mode collect (default) → partial, no throw", identities.length === 1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── errors sorted by filename ───────────────────────────────────────────────
|
|
132
|
+
{
|
|
133
|
+
const map = { [`${GID_B}.meta.json`]: "{ x", [`${GID_A}.meta.json`]: "{ y" };
|
|
134
|
+
const { errors } = listAllMetaIdentities(Object.keys(map), reader(map));
|
|
135
|
+
ok("errors sorted by filename", errors[0]?.filename === `${GID_A}.meta.json`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log(`\n[check-meta-listing] ${passed} assertions ok`);
|