@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,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-resume-launch-identity — deterministic gate for `resume-launch-identity.ts`, the
|
|
3
|
+
* record-authoritative leaf preserved when the visible-first cut deleted `spawn-bg` and every
|
|
4
|
+
* caller it had.
|
|
5
|
+
*
|
|
6
|
+
* WHY A GATE FOR A CONSUMER-ZERO MODULE. Identity authority is a fail-closed risk class: this
|
|
7
|
+
* leaf's whole job is deciding WHICH being a resume turn lands in. A preserved leaf whose only
|
|
8
|
+
* guarantee is its own header comment is exactly the rot preservation was meant to prevent — it
|
|
9
|
+
* would be re-adopted, months later, by a visible-resume composition that trusts prose. The cells
|
|
10
|
+
* below are transplanted from the deleted `check-entwurf-v2-spawn-production` §9 (the plan
|
|
11
|
+
* parameter became a bare garden id; nothing else about the identity contract changed), plus one
|
|
12
|
+
* new cell for the #52 read the leaf's header claims and the deleted gate never isolated.
|
|
13
|
+
*
|
|
14
|
+
* What is certified, all through a temp `ENTWURF_META_SESSIONS_DIR` fixture store:
|
|
15
|
+
*
|
|
16
|
+
* 1. HAPPY — gardenId → record.transcriptPath → header id === record.nativeSessionId →
|
|
17
|
+
* LaunchIdentity carrying the header's own cwd/provider/model (#50 C2, #9).
|
|
18
|
+
* 2. C3 INTEGRITY — a transcript whose header id is NOT this citizen's `nativeSessionId` is
|
|
19
|
+
* refused, never resumed. This is the one that keeps a turn out of another being's session.
|
|
20
|
+
* 3. #52 ADDRESSABLE READ — a garden id that does not hold its `nativeSessionId` ALONE is
|
|
21
|
+
* refused. Two records sharing one native session would each pass their own per-record
|
|
22
|
+
* integrity check and resume one transcript twice under two per-garden-id locks, so the
|
|
23
|
+
* plain targeted read is not enough here and the leaf must not be "simplified" back to it.
|
|
24
|
+
* 4. CAUSE FIDELITY — each impossible resume names ITS OWN cause: absent record, out-of-domain
|
|
25
|
+
* backend, no recorded transcript, recorded-but-missing file, no model, headerless file. The
|
|
26
|
+
* missing-file cell is a regression pin: before the existence check that case fell through
|
|
27
|
+
* `readSessionIdentity`'s ENOENT swallow and lied "no recorded model".
|
|
28
|
+
* 4b. THE ONE EXPECTED REFUSAL — a backend with no same-id resume is not a defect at all: the
|
|
29
|
+
* record is fine and the operator reached past a capability boundary. It alone carries a
|
|
30
|
+
* TYPED reason (`target-not-pi`) so the visible-resume composition can match a FIELD instead
|
|
31
|
+
* of parsing a sentence or re-reading the record. Everything else here stays a bare throw.
|
|
32
|
+
* 4c. ABSOLUTE TRANSCRIPT — a relative recorded path would resolve against the WINDOW's own cwd
|
|
33
|
+
* at launch, not against this process's, so it names a different file than the one checked
|
|
34
|
+
* here. Refused before anything opens.
|
|
35
|
+
* 5. SSOT — the module header names THIS gate, so the preserved leaf cannot advertise a
|
|
36
|
+
* certification that does not exist.
|
|
37
|
+
*
|
|
38
|
+
* No spawn, no socket, no timer: the launch this identity feeds is deliberately not in scope.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import assert from "node:assert/strict";
|
|
42
|
+
import * as fs from "node:fs";
|
|
43
|
+
import * as os from "node:os";
|
|
44
|
+
import * as path from "node:path";
|
|
45
|
+
import { serializeMetaIdentity, upsertMetaSession } from "../pi-extensions/lib/meta-session.ts";
|
|
46
|
+
import {
|
|
47
|
+
ResumeBackendUnsupportedError,
|
|
48
|
+
resolveResumeLaunchIdentity,
|
|
49
|
+
} from "../pi-extensions/lib/resume-launch-identity.ts";
|
|
50
|
+
|
|
51
|
+
let passed = 0;
|
|
52
|
+
function ok(label: string, cond: boolean): void {
|
|
53
|
+
assert.ok(cond, label);
|
|
54
|
+
console.log(` ok ${label}`);
|
|
55
|
+
passed++;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const HERE = path.dirname(new URL(import.meta.url).pathname);
|
|
59
|
+
const REPO = path.join(HERE, "..");
|
|
60
|
+
const LEAF_SRC = path.join(REPO, "pi-extensions/lib/resume-launch-identity.ts");
|
|
61
|
+
|
|
62
|
+
function main(): void {
|
|
63
|
+
const world = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "resume-launch-id-"));
|
|
64
|
+
const storeDir = path.join(world, "meta-sessions");
|
|
65
|
+
fs.mkdirSync(storeDir, { recursive: true });
|
|
66
|
+
const prevStore = process.env.ENTWURF_META_SESSIONS_DIR;
|
|
67
|
+
process.env.ENTWURF_META_SESSIONS_DIR = storeDir;
|
|
68
|
+
try {
|
|
69
|
+
const cwd = path.join(world, "repo");
|
|
70
|
+
fs.mkdirSync(cwd, { recursive: true });
|
|
71
|
+
const sessionLine = (id: string) => `${JSON.stringify({ type: "session", id, cwd })}\n`;
|
|
72
|
+
const modelLine = `${JSON.stringify({ type: "model_change", provider: "openai-codex", modelId: "gpt-5.4" })}\n`;
|
|
73
|
+
const writeTranscript = (name: string, content: string): string => {
|
|
74
|
+
const p = path.join(world, name);
|
|
75
|
+
fs.writeFileSync(p, content);
|
|
76
|
+
return p;
|
|
77
|
+
};
|
|
78
|
+
const mintRecord = (nativeSessionId: string, transcriptPath: string | null, backend = "pi"): string =>
|
|
79
|
+
upsertMetaSession({
|
|
80
|
+
input: { backend: backend as "pi", nativeSessionId, cwd, model: "gpt-5.4", transcriptPath },
|
|
81
|
+
dir: storeDir,
|
|
82
|
+
}).record.gardenId;
|
|
83
|
+
const rejects = (gid: string, needle: string, label: string): void => {
|
|
84
|
+
let msg = "";
|
|
85
|
+
try {
|
|
86
|
+
resolveResumeLaunchIdentity(gid);
|
|
87
|
+
} catch (e) {
|
|
88
|
+
msg = e instanceof Error ? e.message : String(e);
|
|
89
|
+
}
|
|
90
|
+
ok(label, msg.includes(needle));
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// ── 1. HAPPY ──────────────────────────────────────────────────────────────
|
|
94
|
+
const nativeOk = "0199aaaa-1111-4222-8333-444455556666";
|
|
95
|
+
const fileOk = writeTranscript("own.jsonl", sessionLine(nativeOk) + modelLine);
|
|
96
|
+
const gidOk = mintRecord(nativeOk, fileOk);
|
|
97
|
+
const launch = resolveResumeLaunchIdentity(gidOk);
|
|
98
|
+
ok("1: record-backed resume resolves the recorded transcript", launch.sessionFile === fileOk);
|
|
99
|
+
ok("1: resume cwd = transcript header authority (#9 cold-resume)", launch.cwd === cwd);
|
|
100
|
+
ok(
|
|
101
|
+
"1: resume model/provider = first model_change",
|
|
102
|
+
launch.model === "gpt-5.4" && launch.provider === "openai-codex",
|
|
103
|
+
);
|
|
104
|
+
// (No arity/shape assertion here on purpose: "the garden id is the only input" is carried
|
|
105
|
+
// by the type system and by every cell below actually resolving through the record.
|
|
106
|
+
// A `fn.length === 1` check would be a claim whose mutant could only be synthetic.)
|
|
107
|
+
|
|
108
|
+
// ── 2. C3 INTEGRITY — header id must be THIS citizen's ────────────────────
|
|
109
|
+
const nativeMine = "0199bbbb-1111-4222-8333-444455556666";
|
|
110
|
+
const foreignFile = writeTranscript(
|
|
111
|
+
"foreign.jsonl",
|
|
112
|
+
sessionLine("0199cccc-9999-4999-8999-999999999999") + modelLine,
|
|
113
|
+
);
|
|
114
|
+
const gidForeign = mintRecord(nativeMine, foreignFile);
|
|
115
|
+
rejects(
|
|
116
|
+
gidForeign,
|
|
117
|
+
"does not match the record's nativeSessionId",
|
|
118
|
+
"2: header ≠ nativeSessionId → refused, never resumed (C3) [QK:RESUME-ID-HEADER-INTEGRITY]",
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// ── 4. CAUSE FIDELITY ─────────────────────────────────────────────────────
|
|
122
|
+
rejects("20260101T000000-facade", "not a garden citizen", "4: recordless gid → not a citizen");
|
|
123
|
+
|
|
124
|
+
const gidClaude = mintRecord("claude-native-1", null, "claude-code");
|
|
125
|
+
rejects(
|
|
126
|
+
gidClaude,
|
|
127
|
+
"is a claude-code citizen",
|
|
128
|
+
"4: out-of-domain citizen → refused as a capability-domain miss, not as citizen rank",
|
|
129
|
+
);
|
|
130
|
+
// This one refusal is EXPECTED, not a defect: the record is perfectly good and the operator
|
|
131
|
+
// has reached past a capability boundary. So it is the only failure here that carries a
|
|
132
|
+
// typed `reason` a caller can match on a FIELD — the visible-resume composition turns it
|
|
133
|
+
// into `target-not-pi`, and if it degraded to a bare Error that caller would either parse
|
|
134
|
+
// this sentence or read the record a second time to ask which backend it was.
|
|
135
|
+
{
|
|
136
|
+
let caught: Error | null = null;
|
|
137
|
+
try {
|
|
138
|
+
resolveResumeLaunchIdentity(gidClaude);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
caught = err as Error;
|
|
141
|
+
}
|
|
142
|
+
ok(
|
|
143
|
+
"4: the backend miss is a TYPED refusal carrying reason + backend, not a bare Error [QK:RESUME-ID-BACKEND-TYPED-REFUSAL]",
|
|
144
|
+
caught instanceof ResumeBackendUnsupportedError &&
|
|
145
|
+
caught.reason === "target-not-pi" &&
|
|
146
|
+
caught.backend === "claude-code",
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// A RELATIVE recorded path passes existsSync here whenever this process happens to sit in
|
|
151
|
+
// the right directory, but the launch resolves `--session` inside the window's own
|
|
152
|
+
// `-c <record cwd>`. Two directories, two files, one receipt that looks correct — so the
|
|
153
|
+
// leaf refuses before anything opens rather than resuming an unknown transcript.
|
|
154
|
+
{
|
|
155
|
+
const nativeRel = "0199ffff-1111-4222-8333-444455556666";
|
|
156
|
+
writeTranscript("relative.jsonl", sessionLine(nativeRel) + modelLine);
|
|
157
|
+
const gidRel = mintRecord(nativeRel, "relative.jsonl");
|
|
158
|
+
rejects(
|
|
159
|
+
gidRel,
|
|
160
|
+
"RELATIVE transcriptPath",
|
|
161
|
+
"4: a relative recorded transcriptPath → refused before launch, because --session would resolve against the WINDOW's cwd, not this one [QK:RESUME-ID-SESSION-ABSOLUTE]",
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const gidNoFile = mintRecord("0199dddd-1111-4222-8333-444455556666", null);
|
|
166
|
+
rejects(gidNoFile, "no recorded transcriptPath", "4: transcriptPath null → nothing to resume");
|
|
167
|
+
|
|
168
|
+
// Regression pin (F7): before the existsSync guard this fell through readSessionIdentity's
|
|
169
|
+
// ENOENT swallow and reported "no recorded model" — a true refusal naming a false cause,
|
|
170
|
+
// which sends an operator to look for a model setting instead of a deleted transcript.
|
|
171
|
+
const gidGhost = mintRecord("0199abcd-1111-4222-8333-444455556666", path.join(world, "never-written.jsonl"));
|
|
172
|
+
rejects(
|
|
173
|
+
gidGhost,
|
|
174
|
+
"does not exist on disk",
|
|
175
|
+
"4: recorded transcript missing on disk → refused as MISSING, not as no-model [QK:RESUME-ID-MISSING-TRANSCRIPT-CAUSE]",
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const nativeNoModel = "0199eeee-1111-4222-8333-444455556666";
|
|
179
|
+
const fileNoModel = writeTranscript("no-model.jsonl", sessionLine(nativeNoModel));
|
|
180
|
+
const gidNoModel = mintRecord(nativeNoModel, fileNoModel);
|
|
181
|
+
rejects(gidNoModel, "no recorded model", "4: no model_change → refused (model preservation)");
|
|
182
|
+
|
|
183
|
+
// A headerless transcript has an UNDEFINED header id, which can never equal the record's
|
|
184
|
+
// nativeSessionId — so the integrity check catches it and says "(none)" rather than
|
|
185
|
+
// resuming a file whose ownership was never stated.
|
|
186
|
+
const nativeNoHeader = "0199ffff-1111-4222-8333-444455556666";
|
|
187
|
+
const fileNoHeader = writeTranscript("no-header.jsonl", modelLine);
|
|
188
|
+
const gidNoHeader = mintRecord(nativeNoHeader, fileNoHeader);
|
|
189
|
+
rejects(gidNoHeader, '"(none)"', "4: headerless transcript → refused as (none), never resumed");
|
|
190
|
+
|
|
191
|
+
// ── 3. #52 ADDRESSABLE READ ───────────────────────────────────────────────
|
|
192
|
+
// Planted LAST so the duplicate cannot disturb the cells above: a store-wide uniqueness
|
|
193
|
+
// scan sees every record, and `upsertMetaSession` would attach to the existing garden id
|
|
194
|
+
// rather than mint a rival, so the rival record is written directly.
|
|
195
|
+
const rivalGid = "20260301T120009-a1b2c3";
|
|
196
|
+
fs.writeFileSync(
|
|
197
|
+
path.join(storeDir, `${rivalGid}.meta.json`),
|
|
198
|
+
serializeMetaIdentity({
|
|
199
|
+
schemaVersion: 3,
|
|
200
|
+
gardenId: rivalGid,
|
|
201
|
+
backend: "pi",
|
|
202
|
+
nativeSessionId: nativeOk, // the SAME native session as gidOk
|
|
203
|
+
cwd,
|
|
204
|
+
model: "gpt-5.4",
|
|
205
|
+
transcriptPath: fileOk,
|
|
206
|
+
createdAt: "2026-03-01T12:00:00.000Z",
|
|
207
|
+
recordUpdatedAt: "2026-03-01T12:30:00.000Z",
|
|
208
|
+
}),
|
|
209
|
+
);
|
|
210
|
+
// The happy-path id resolved a moment ago; the ONLY thing that changed is a second record
|
|
211
|
+
// claiming its native session. If this leaf used the plain targeted read, that resolve
|
|
212
|
+
// would still succeed and two garden ids would resume one transcript under two locks.
|
|
213
|
+
rejects(
|
|
214
|
+
gidOk,
|
|
215
|
+
"must be unique",
|
|
216
|
+
"3: a garden id that no longer holds its nativeSessionId alone → refused by the ADDRESSABLE read [QK:RESUME-ID-ADDRESSABLE-READ]",
|
|
217
|
+
);
|
|
218
|
+
rejects(
|
|
219
|
+
rivalGid,
|
|
220
|
+
"must be unique",
|
|
221
|
+
"3: …refused from EITHER side of the pair (neither rival is silently preferred)",
|
|
222
|
+
);
|
|
223
|
+
} finally {
|
|
224
|
+
if (prevStore === undefined) delete process.env.ENTWURF_META_SESSIONS_DIR;
|
|
225
|
+
else process.env.ENTWURF_META_SESSIONS_DIR = prevStore;
|
|
226
|
+
fs.rmSync(world, { recursive: true, force: true });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ── 5. SSOT — the leaf's header must name the gate that actually certifies it ──
|
|
230
|
+
{
|
|
231
|
+
const src = fs.readFileSync(LEAF_SRC, "utf8");
|
|
232
|
+
ok(
|
|
233
|
+
"5: the leaf's header names check-resume-launch-identity (no certification it does not have) [QK:RESUME-ID-GATE-SSOT]",
|
|
234
|
+
src.includes("`check-resume-launch-identity`"),
|
|
235
|
+
);
|
|
236
|
+
// The preservation is deliberate and stated: a reader who finds an uncalled export must
|
|
237
|
+
// be able to tell "kept on purpose, for the visible lane" from "forgotten".
|
|
238
|
+
ok("5: the leaf states it is preserved with no consumer, on purpose", /no consumer in the shipped tree/.test(src));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
console.log(`\ncheck-resume-launch-identity: ${passed} checks passed`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
main();
|
|
@@ -291,7 +291,7 @@ async function main(): Promise<void> {
|
|
|
291
291
|
}
|
|
292
292
|
|
|
293
293
|
// ── inspectControlSocketPath (5c-3c R1 — the path-addressed SSOT) ────────────────
|
|
294
|
-
//
|
|
294
|
+
// A resume watcher observes an expected socket path and MUST inspect THAT exact
|
|
295
295
|
// path (no gid re-derivation). Proves the path-taking core classifies every branch
|
|
296
296
|
// identically to the gid wrapper, and that the wrapper just feeds it controlSocketPath(gid).
|
|
297
297
|
{
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
{"type":"fixture-provenance","note":"Structure extracted from a fixture-only real Pi parent retake (2026-08-06) and then scrubbed: a real record-backed pi citizen called the native entwurf_fresh_call once, and the fresh sibling's nonce callback arrived in that parent's own transcript. MEASURED and preserved: pi records NO separate toolCall row — the call surfaces as a toolResult row carrying toolCallId + toolName, and the callback arrives later as a custom_message of customType entwurf-message whose body embeds a <sender_info> JSON envelope. Event order, roles, content types, toolName, the nonce flowing from the launch receipt into the callback body, and the sender_info FIELD NAMES are real. Every id, uuid, toolCallId, timestamp, model, path, cwd and message body is a fixture value. This is a sample of the PARENT-SIDE SHAPE — it is NOT placement evidence and says nothing about whether a window opened."}
|
|
2
|
+
{"type":"message","id":"0199f000-0000-7000-8000-000000000000-1","parentId":"0199f000-0000-7000-8000-000000000000-0","timestamp":"2026-01-01T00:00:00.000Z","message":{"role":"toolResult","toolCallId":"fixture-toolcall-1","toolName":"entwurf_fresh_call","content":[{"type":"text","text":"[entwurf fresh call →]\n backend: pi (/fixture/bin/pi)\n model: fixture-provider/fixture-model (requested on the runtime CLI)\n window: @2 (index 2) in session $0\n pane: %2 pid 4242\n nonce: mux-fresh-call-000000000000000000000000\n\nThis is a LAUNCH receipt: tmux created that window and was asked to start the runtime with the model above. It does NOT mean the sibling is running, that its first turn ran, or that the task was delivered.\nThe sibling's garden id arrives separately — it calls entwurf_v2 back with the nonce above as its first action, and the sender envelope of THAT message is the address. Nothing is polling for it; if it never comes, the window is visible and can be read directly."}],"details":{"isError":false},"isError":false,"timestamp":1767225600000}}
|
|
3
|
+
{"type":"custom_message","customType":"entwurf-message","content":"mux-fresh-call-000000000000000000000000\n\n<sender_info>{\"sessionId\":\"20260101T000001-bbbbbb\",\"agentId\":\"fixture-provider/fixture-model\",\"cwd\":\"/fixture/project\",\"timestamp\":\"2026-01-01T00:00:00.000Z\",\"origin\":\"pi-session\",\"replyable\":true}</sender_info>","display":true,"id":"0199f000-0000-7000-8000-000000000000-2","parentId":"0199f000-0000-7000-8000-000000000000-1","timestamp":"2026-01-01T00:00:00.000Z"}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* inventory-verification-surface — Phase 0 of issue #62 (vitest adoption).
|
|
3
|
+
*
|
|
4
|
+
* The issue's own measurements shifted with the glob used; this script FIXES the
|
|
5
|
+
* denominator. Review (2026-08-07) found the first cut mis-measured the migration
|
|
6
|
+
* itself, so the denominator is now BOTH verification axes together:
|
|
7
|
+
*
|
|
8
|
+
* legacy axis scripts/ (hand-built gates, lib, mutants, fixtures)
|
|
9
|
+
* framework axis test/ + vitest.config.ts (vitest-managed lanes)
|
|
10
|
+
*
|
|
11
|
+
* both from `git ls-files --cached --others --exclude-standard` (tracked plus
|
|
12
|
+
* untracked-non-ignored — the same work surface qualification hashes), so moving a
|
|
13
|
+
* gate from scripts/ into test/ moves lines BETWEEN axes and the combined total
|
|
14
|
+
* stays honest: migration is not subtraction unless the combined number drops.
|
|
15
|
+
*
|
|
16
|
+
* PRIMARY classification is the issue's own six semantic classes, decided by the
|
|
17
|
+
* first matching rule below (re-derivable; narrow explicit overrides carry a
|
|
18
|
+
* reason and are printed). `unclassified` is asserted to be ZERO — a file no rule
|
|
19
|
+
* reaches is a loud error, never a silent bucket:
|
|
20
|
+
*
|
|
21
|
+
* 1 real-live name *-live.* OR gates on LIVE=1 (needs real
|
|
22
|
+
* accounts/models/tmux; smoke-*-install-state is NOT
|
|
23
|
+
* live — it matches rule 2 first by name)
|
|
24
|
+
* 2 package-install name carries pack|install (tarball/install surface)
|
|
25
|
+
* 3 hermetic-integration spawns a subprocess, stands a local server, or is a
|
|
26
|
+
* .sh gate — real processes, no external accounts
|
|
27
|
+
* 4 source-topology reads product source as text WITHOUT importing it
|
|
28
|
+
* 5 behavioral-contract imports and executes product code, with fs/fixtures
|
|
29
|
+
* or auxiliary source reads
|
|
30
|
+
* 6 pure-unit imports and executes product code, no fs, no text
|
|
31
|
+
* reads, no processes
|
|
32
|
+
*
|
|
33
|
+
* The SECONDARY style axis (imports-product / source-text / subprocess / mixed /
|
|
34
|
+
* shell / other) is the issue's measurement table, kept for continuity with the
|
|
35
|
+
* recorded e405d64 baseline snapshot — style describes HOW a gate is written,
|
|
36
|
+
* the six classes describe WHAT it proves.
|
|
37
|
+
*
|
|
38
|
+
* A test/*.test.ts file is classified together with the ./helpers/* bodies it
|
|
39
|
+
* imports: the helper is part of how that lane earns its green (a bridge boot
|
|
40
|
+
* hidden in a helper still makes the lane an integration lane).
|
|
41
|
+
*
|
|
42
|
+
* Plus the mutant inventory per lane, split by whether the mutant's SUBJECT is
|
|
43
|
+
* product code or the verification surface itself (scripts/).
|
|
44
|
+
*
|
|
45
|
+
* Read-only and not a gate: it never writes; its one assertion (unclassified=0)
|
|
46
|
+
* exists so the classification cannot silently rot. Phase 1–4 decisions cite this
|
|
47
|
+
* one reproducible table instead of ad-hoc greps.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
import { execFileSync } from "node:child_process";
|
|
51
|
+
import * as fs from "node:fs";
|
|
52
|
+
import * as path from "node:path";
|
|
53
|
+
import { fileURLToPath } from "node:url";
|
|
54
|
+
|
|
55
|
+
const REPO_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
56
|
+
|
|
57
|
+
function git(args: string[]): string {
|
|
58
|
+
return execFileSync("git", args, { cwd: REPO_DIR, encoding: "utf8" });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function listWorkSurface(pathspec: string[]): string[] {
|
|
62
|
+
return [
|
|
63
|
+
...new Set(
|
|
64
|
+
git(["ls-files", "--cached", "--others", "--exclude-standard", "--", ...pathspec])
|
|
65
|
+
.split("\n")
|
|
66
|
+
.filter((f) => f.length > 0 && fs.existsSync(path.join(REPO_DIR, f))),
|
|
67
|
+
),
|
|
68
|
+
].sort();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const legacyFiles = listWorkSurface(["scripts/"]);
|
|
72
|
+
const frameworkFiles = listWorkSurface(["test/", "vitest.config.ts"]);
|
|
73
|
+
|
|
74
|
+
function countLines(rel: string): number {
|
|
75
|
+
const body = fs.readFileSync(path.join(REPO_DIR, rel), "utf8");
|
|
76
|
+
if (body.length === 0) return 0;
|
|
77
|
+
return body.split("\n").length - (body.endsWith("\n") ? 1 : 0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── rule predicates (printed, so every number is re-derivable) ───────────────
|
|
81
|
+
const H_IMPORTS = /(?:from\s+["']|import\(\s*["'])(?:\.\.\/)+(?:pi-extensions|mcp)\//;
|
|
82
|
+
const H_TEXT = /readFileSync[^\n]*(?:pi-extensions|mcp\/|\.ts["'`]|SOURCE|SRC)|\bread\(["'](?:pi-extensions|mcp)\//;
|
|
83
|
+
const H_PROC = /\b(?:spawn|spawnSync|execFile|execFileSync|execSync|fork)\s*\(|subprocess\.(?:run|Popen|check_)/;
|
|
84
|
+
const H_NET = /\b(?:http|net)\.createServer|\.listen\(/;
|
|
85
|
+
const H_FS = /from\s+["']node:fs["']|require\(["']node:fs["']\)/;
|
|
86
|
+
// H_LIVE asks whether the file's OWN CODE reads the LIVE switch — not whether the
|
|
87
|
+
// three letters appear. The naive `\bLIVE=1\b` form classified
|
|
88
|
+
// scripts/check-release-gate-outcomes.ts as real-live because that gate QUOTES the
|
|
89
|
+
// sentence "a CUT needs LIVE=1, SKIP=0" and greps other files for the literal
|
|
90
|
+
// "process.env.LIVE"; it spawns no live turn of its own. So the predicate runs on the
|
|
91
|
+
// CODE-ONLY projection (comments and inert literals blanked, shell expansions kept)
|
|
92
|
+
// and matches a variable READ or an assignment, never prose.
|
|
93
|
+
const H_LIVE = /process\.env\.LIVE\b|\$\{LIVE[:}\-]|\$LIVE\b|(?:^|\n)\s*(?:export\s+)?LIVE=1\b/;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Blank what cannot gate execution, so H_LIVE reads code and not prose.
|
|
97
|
+
*
|
|
98
|
+
* TS: `//` and block comments, and the contents of every quoted literal.
|
|
99
|
+
* Shell: `#` comments and single-quoted literals only — a double-quoted shell string
|
|
100
|
+
* still expands `${LIVE:-0}`, so blanking it would hide a real read.
|
|
101
|
+
*/
|
|
102
|
+
function codeOnly(body: string, isShell: boolean): string {
|
|
103
|
+
if (isShell) {
|
|
104
|
+
return body.replace(/(^|\s)#[^\n]*/g, "$1").replace(/'[^'\n]*'/g, "''");
|
|
105
|
+
}
|
|
106
|
+
return body
|
|
107
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
108
|
+
.replace(/(^|[^:])\/\/[^\n]*/g, "$1")
|
|
109
|
+
.replace(/`(?:[^`\\]|\\.)*`/g, "``")
|
|
110
|
+
.replace(/"(?:[^"\\\n]|\\.)*"/g, '""')
|
|
111
|
+
.replace(/'(?:[^'\\\n]|\\.)*'/g, "''");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
type SemanticClass =
|
|
115
|
+
| "real-live"
|
|
116
|
+
| "package-install"
|
|
117
|
+
| "hermetic-integration"
|
|
118
|
+
| "source-topology"
|
|
119
|
+
| "behavioral-contract"
|
|
120
|
+
| "pure-unit";
|
|
121
|
+
|
|
122
|
+
/** Narrow explicit overrides for files the mechanical rules cannot reach. Every
|
|
123
|
+
* entry carries its reason and is printed with the table — extending this map is
|
|
124
|
+
* a reviewed decision, not a default. */
|
|
125
|
+
const CLASS_OVERRIDES: Record<string, { cls: SemanticClass; reason: string }> = {
|
|
126
|
+
// These three read repo files through paths assembled at runtime, so the
|
|
127
|
+
// H_TEXT line-level heuristic cannot see them; each is a structure-over-text
|
|
128
|
+
// gate with no product execution.
|
|
129
|
+
"scripts/check-entwurf-bridge-pi-free.ts": {
|
|
130
|
+
cls: "source-topology",
|
|
131
|
+
reason: "walks the bridge's static value-import closure as text to forbid @earendil-works imports",
|
|
132
|
+
},
|
|
133
|
+
"scripts/check-shell-quote.ts": {
|
|
134
|
+
cls: "source-topology",
|
|
135
|
+
reason: "reads the shellQuote() implementation sites as text and cross-checks the POSIX escape contract",
|
|
136
|
+
},
|
|
137
|
+
// Imports only scripts/lib (the shared launcher-fence), spawns nothing, and its
|
|
138
|
+
// smoke-wiring reads assemble paths at runtime — so no mechanical rule reaches it.
|
|
139
|
+
"scripts/check-mux-launcher-fence.ts": {
|
|
140
|
+
cls: "behavioral-contract",
|
|
141
|
+
reason:
|
|
142
|
+
"executes the shared claude-launcher-fence lib against disposable fs fixtures and source-reads both mux LIVE smoke wiring sites",
|
|
143
|
+
},
|
|
144
|
+
"scripts/check-mux-parent-artifact.ts": {
|
|
145
|
+
cls: "source-topology",
|
|
146
|
+
reason: "asserts the scrubbed parent-transcript fixture's recorded shape — an artifact contract, no product run",
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* A single class name is the file's STRONGEST tier; some gates also ship a weaker
|
|
152
|
+
* one. Recording that here keeps "real-live" from being read as "never runs without
|
|
153
|
+
* LIVE=1" — and keeps the reverse, a two-tier gate quietly counted as deterministic,
|
|
154
|
+
* from passing as honest either. Stale entries throw.
|
|
155
|
+
*/
|
|
156
|
+
const TWO_TIER_NOTES: Record<string, string> = {
|
|
157
|
+
"scripts/smoke-meta-async-drift.sh":
|
|
158
|
+
"deterministic default tier (pin/marker/doorbell contract — no model, no network) + LIVE=1 add-on tier that delegates to repro-plugin-idle-wake.sh and spawns one metered `claude -p`. Counted real-live because its strongest tier is a real live turn; `pnpm check` only ever runs the deterministic tier.",
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
type Style = "imports-product" | "source-text" | "subprocess" | "mixed" | "shell" | "other";
|
|
162
|
+
|
|
163
|
+
interface Row {
|
|
164
|
+
file: string;
|
|
165
|
+
axis: "legacy" | "framework";
|
|
166
|
+
lines: number;
|
|
167
|
+
cls: SemanticClass;
|
|
168
|
+
overridden: boolean;
|
|
169
|
+
style: Style;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** A test file is judged together with the ./helpers/ bodies it imports. */
|
|
173
|
+
function effectiveBody(rel: string): string {
|
|
174
|
+
let body = fs.readFileSync(path.join(REPO_DIR, rel), "utf8");
|
|
175
|
+
if (rel.startsWith("test/") && rel.endsWith(".test.ts")) {
|
|
176
|
+
for (const m of body.matchAll(/from\s+["']\.\/(helpers\/[^"']+?)(?:\.ts)?["']/g)) {
|
|
177
|
+
const helper = path.join(REPO_DIR, "test", `${m[1]}.ts`.replace(/\.ts\.ts$/, ".ts"));
|
|
178
|
+
if (fs.existsSync(helper)) body += `\n${fs.readFileSync(helper, "utf8")}`;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return body;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function classify(rel: string, axis: "legacy" | "framework"): Row {
|
|
185
|
+
const lines = countLines(rel);
|
|
186
|
+
const isShell = rel.endsWith(".sh");
|
|
187
|
+
const body = isShell ? fs.readFileSync(path.join(REPO_DIR, rel), "utf8") : effectiveBody(rel);
|
|
188
|
+
const base = path.basename(rel);
|
|
189
|
+
|
|
190
|
+
const style: Style = (() => {
|
|
191
|
+
if (isShell) return "shell";
|
|
192
|
+
const axes = [H_IMPORTS.test(body), H_TEXT.test(body), H_PROC.test(body)];
|
|
193
|
+
const n = axes.filter(Boolean).length;
|
|
194
|
+
if (n >= 2) return "mixed";
|
|
195
|
+
if (axes[0]) return "imports-product";
|
|
196
|
+
if (axes[1]) return "source-text";
|
|
197
|
+
if (axes[2]) return "subprocess";
|
|
198
|
+
return "other";
|
|
199
|
+
})();
|
|
200
|
+
|
|
201
|
+
const override = CLASS_OVERRIDES[rel];
|
|
202
|
+
const cls: SemanticClass | null = override
|
|
203
|
+
? override.cls
|
|
204
|
+
: /-live\.(ts|sh)$/.test(base) || H_LIVE.test(codeOnly(body, isShell))
|
|
205
|
+
? "real-live"
|
|
206
|
+
: /pack|install/.test(base)
|
|
207
|
+
? "package-install"
|
|
208
|
+
: isShell || H_PROC.test(body) || H_NET.test(body)
|
|
209
|
+
? "hermetic-integration"
|
|
210
|
+
: H_TEXT.test(body) && !H_IMPORTS.test(body)
|
|
211
|
+
? "source-topology"
|
|
212
|
+
: H_IMPORTS.test(body) && (H_TEXT.test(body) || H_FS.test(body))
|
|
213
|
+
? "behavioral-contract"
|
|
214
|
+
: H_IMPORTS.test(body)
|
|
215
|
+
? "pure-unit"
|
|
216
|
+
: null;
|
|
217
|
+
if (cls === null) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
`unclassified gate: ${rel} — no rule reaches it; extend the rules or add a reasoned CLASS_OVERRIDES entry`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
return { file: rel, axis, lines, cls, overridden: Boolean(override), style };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ── buckets ──────────────────────────────────────────────────────────────────
|
|
226
|
+
const legacyGates = legacyFiles.filter((f) => /^scripts\/(check-|smoke-)/.test(f));
|
|
227
|
+
const frameworkGates = frameworkFiles.filter((f) => /^test\/.*\.test\.ts$/.test(f));
|
|
228
|
+
const lib = legacyFiles.filter((f) => f.startsWith("scripts/lib/"));
|
|
229
|
+
const mutantManifests = legacyFiles.filter((f) => f.startsWith("scripts/mutants/") && f.endsWith(".json"));
|
|
230
|
+
const fixtures = legacyFiles.filter((f) => f.startsWith("scripts/fixtures/"));
|
|
231
|
+
const legacyOther = legacyFiles.filter(
|
|
232
|
+
(f) => !legacyGates.includes(f) && !lib.includes(f) && !mutantManifests.includes(f) && !fixtures.includes(f),
|
|
233
|
+
);
|
|
234
|
+
const frameworkSupport = frameworkFiles.filter((f) => !frameworkGates.includes(f));
|
|
235
|
+
|
|
236
|
+
const rows: Row[] = [
|
|
237
|
+
...legacyGates.map((f) => classify(f, "legacy")),
|
|
238
|
+
...frameworkGates.map((f) => classify(f, "framework")),
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
function sum(rs: Row[]): number {
|
|
242
|
+
return rs.reduce((a, r) => a + r.lines, 0);
|
|
243
|
+
}
|
|
244
|
+
function total(files: string[]): number {
|
|
245
|
+
return files.reduce((a, f) => a + countLines(f), 0);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const head = git(["rev-parse", "--short", "HEAD"]).trim();
|
|
249
|
+
console.log(`inventory-verification-surface — work surface (tracked + untracked-non-ignored) @ HEAD ${head}`);
|
|
250
|
+
console.log(`
|
|
251
|
+
rule predicates (re-derive any number from these):
|
|
252
|
+
H_IMPORTS ${H_IMPORTS}
|
|
253
|
+
H_TEXT ${H_TEXT}
|
|
254
|
+
H_PROC ${H_PROC}
|
|
255
|
+
H_NET ${H_NET}
|
|
256
|
+
H_FS ${H_FS}
|
|
257
|
+
H_LIVE ${H_LIVE}
|
|
258
|
+
H_LIVE is applied to the CODE-ONLY projection (comments and inert literals blanked;
|
|
259
|
+
shell double-quoted expansions kept), so a gate that merely QUOTES "LIVE=1" is not
|
|
260
|
+
real-live. Two-tier gates are listed under the class breakdown.
|
|
261
|
+
semantic classes, first match wins: override → real-live (name -live | H_LIVE on code) →
|
|
262
|
+
package-install (name pack|install) → hermetic-integration (.sh | H_PROC | H_NET) →
|
|
263
|
+
source-topology (H_TEXT ∧ ¬H_IMPORTS) → behavioral-contract (H_IMPORTS ∧ (H_TEXT ∨ H_FS)) →
|
|
264
|
+
pure-unit (H_IMPORTS) → ERROR (unclassified is asserted zero)
|
|
265
|
+
test/*.test.ts is classified together with the ./helpers/* bodies it imports.
|
|
266
|
+
`);
|
|
267
|
+
|
|
268
|
+
const legacyTotal = total(legacyFiles);
|
|
269
|
+
const frameworkTotal = total(frameworkFiles);
|
|
270
|
+
console.log(`legacy axis (scripts/): ${legacyFiles.length} files, ${legacyTotal} lines`);
|
|
271
|
+
console.log(
|
|
272
|
+
` gates (check-*/smoke-*): ${legacyGates.length} files, ${sum(rows.filter((r) => r.axis === "legacy"))} lines`,
|
|
273
|
+
);
|
|
274
|
+
console.log(` scripts/lib/: ${lib.length} files, ${total(lib)} lines`);
|
|
275
|
+
console.log(` scripts/mutants/: ${mutantManifests.length} manifests`);
|
|
276
|
+
console.log(` scripts/fixtures/: ${fixtures.length} files`);
|
|
277
|
+
console.log(` other: ${legacyOther.length} files, ${total(legacyOther)} lines`);
|
|
278
|
+
console.log(`framework axis (test/ + vitest.config.ts): ${frameworkFiles.length} files, ${frameworkTotal} lines`);
|
|
279
|
+
console.log(
|
|
280
|
+
` vitest lanes (test/**/*.test.ts): ${frameworkGates.length} files, ${sum(rows.filter((r) => r.axis === "framework"))} lines`,
|
|
281
|
+
);
|
|
282
|
+
console.log(` helpers/config: ${frameworkSupport.length} files, ${total(frameworkSupport)} lines`);
|
|
283
|
+
console.log(
|
|
284
|
+
`COMBINED verification total: ${legacyFiles.length + frameworkFiles.length} files, ${legacyTotal + frameworkTotal} lines`,
|
|
285
|
+
);
|
|
286
|
+
console.log(" (migration moves lines between axes; only a drop in THIS number is subtraction)");
|
|
287
|
+
|
|
288
|
+
console.log("\nsemantic class breakdown (all gates, both axes):");
|
|
289
|
+
const classes: SemanticClass[] = [
|
|
290
|
+
"pure-unit",
|
|
291
|
+
"behavioral-contract",
|
|
292
|
+
"source-topology",
|
|
293
|
+
"hermetic-integration",
|
|
294
|
+
"package-install",
|
|
295
|
+
"real-live",
|
|
296
|
+
];
|
|
297
|
+
for (const c of classes) {
|
|
298
|
+
const rs = rows.filter((r) => r.cls === c);
|
|
299
|
+
console.log(` ${c.padEnd(21)} ${String(rs.length).padStart(3)} files ${String(sum(rs)).padStart(7)} lines`);
|
|
300
|
+
}
|
|
301
|
+
console.log(` ${"unclassified".padEnd(21)} 0 files 0 lines (asserted: the classifier throws otherwise)`);
|
|
302
|
+
|
|
303
|
+
console.log("\ntwo-tier gates (one class name, two execution tiers):");
|
|
304
|
+
for (const [rel, note] of Object.entries(TWO_TIER_NOTES)) {
|
|
305
|
+
const row = rows.find((r) => r.file === rel);
|
|
306
|
+
if (!row) throw new Error(`stale TWO_TIER_NOTES entry: ${rel} is not a classified gate — remove or repoint it`);
|
|
307
|
+
console.log(` ${rel} [${row.cls}] — ${note}`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
console.log("\nstyle axis (secondary, e405d64-baseline continuity):");
|
|
311
|
+
const styles: Style[] = ["imports-product", "source-text", "subprocess", "mixed", "shell", "other"];
|
|
312
|
+
for (const s of styles) {
|
|
313
|
+
const rs = rows.filter((r) => r.style === s);
|
|
314
|
+
console.log(` ${s.padEnd(21)} ${String(rs.length).padStart(3)} files ${String(sum(rs)).padStart(7)} lines`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const overridden = rows.filter((r) => r.overridden);
|
|
318
|
+
if (overridden.length > 0) {
|
|
319
|
+
console.log("\nexplicit overrides in effect (each is a reviewed decision):");
|
|
320
|
+
for (const r of overridden) console.log(` ${r.file} → ${r.cls} (${CLASS_OVERRIDES[r.file].reason})`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ── mutant inventory ─────────────────────────────────────────────────────────
|
|
324
|
+
interface MutantEntry {
|
|
325
|
+
subject: string;
|
|
326
|
+
}
|
|
327
|
+
let totalMutants = 0;
|
|
328
|
+
let infraMutants = 0;
|
|
329
|
+
console.log("\nmutant lanes (subject=scripts/* counts as verification-infra):");
|
|
330
|
+
for (const manifest of mutantManifests) {
|
|
331
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(REPO_DIR, manifest), "utf8")) as {
|
|
332
|
+
lane: string;
|
|
333
|
+
mutants: MutantEntry[];
|
|
334
|
+
};
|
|
335
|
+
const infra = parsed.mutants.filter((m) => m.subject.startsWith("scripts/")).length;
|
|
336
|
+
totalMutants += parsed.mutants.length;
|
|
337
|
+
infraMutants += infra;
|
|
338
|
+
console.log(
|
|
339
|
+
` ${parsed.lane.padEnd(24)} ${String(parsed.mutants.length).padStart(3)} mutants (${infra} infra-subject)`,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
console.log(` ${"TOTAL".padEnd(24)} ${String(totalMutants).padStart(3)} mutants (${infraMutants} infra-subject)`);
|
|
343
|
+
|
|
344
|
+
console.log("\nper-gate table (axis, class, style, lines):");
|
|
345
|
+
for (const r of rows) {
|
|
346
|
+
console.log(
|
|
347
|
+
` ${r.axis.padEnd(9)} ${r.cls.padEnd(21)} ${r.style.padEnd(16)} ${String(r.lines).padStart(6)} ${r.file}${r.overridden ? " [override]" : ""}`,
|
|
348
|
+
);
|
|
349
|
+
}
|