@junghanacs/entwurf 0.21.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +3 -2
- package/BASELINE.md +3 -1
- package/CHANGELOG.md +658 -0
- package/DELIVERY.md +167 -29
- package/README.md +104 -14
- package/VERIFY.md +73 -11
- package/docs/external-mcp-host.md +16 -6
- package/docs/setup-clean-host.md +73 -22
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +54 -31
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-caller-seat.js +174 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-declaration.js +609 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +289 -57
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +40 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +6 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peer-observe.js +67 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +9 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +24 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +8 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +423 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +98 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +1018 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +213 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +171 -263
- package/mcp/entwurf-bridge/dist/scripts/codex-socket-path.js +30 -0
- package/mcp/entwurf-bridge/dist/scripts/peer-facts.js +109 -0
- package/mcp/entwurf-bridge/src/index.ts +65 -31
- package/mcp/entwurf-bridge/tsconfig.build.json +10 -0
- package/package.json +4 -4
- package/pi-extensions/entwurf-control.ts +91 -52
- package/pi-extensions/lib/codex-caller-seat.ts +204 -0
- package/pi-extensions/lib/codex-declaration.js +612 -0
- package/pi-extensions/lib/codex-fresh-preflight.ts +325 -52
- package/pi-extensions/lib/entwurf-control-rpc.ts +46 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +13 -2
- package/pi-extensions/lib/entwurf-facts.ts +14 -1
- package/pi-extensions/lib/entwurf-peer-observe.ts +72 -2
- package/pi-extensions/lib/entwurf-peers-render.ts +9 -1
- package/pi-extensions/lib/entwurf-self-address.ts +33 -0
- package/pi-extensions/lib/entwurf-v2-runner.ts +3 -2
- package/pi-extensions/lib/entwurf-v2-send.ts +16 -11
- package/pi-extensions/lib/fresh-call-composition.ts +493 -0
- package/pi-extensions/lib/fresh-call-dispatch.ts +142 -0
- package/pi-extensions/lib/herdr-fresh-call.ts +1378 -0
- package/pi-extensions/lib/herdr-placement.ts +276 -0
- package/pi-extensions/lib/mux-fresh-call.ts +257 -304
- package/run.sh +522 -10
- package/scripts/check-codex-app-server-launch.ts +445 -0
- package/scripts/check-codex-birth-hook.ts +187 -0
- package/scripts/check-entwurf-control-rpc.ts +138 -0
- package/scripts/check-entwurf-fact-provider.ts +4 -2
- package/scripts/check-entwurf-facts.ts +12 -6
- package/scripts/check-entwurf-peers-surface.ts +3 -1
- package/scripts/check-entwurf-self-address.ts +68 -0
- package/scripts/check-entwurf-v2-production.ts +42 -1
- package/scripts/check-entwurf-v2-send.ts +26 -7
- package/scripts/check-fresh-call-dispatch.ts +301 -0
- package/scripts/check-gate-qualification.ts +19 -5
- package/scripts/check-herdr-activation.ts +1293 -0
- package/scripts/check-herdr-fresh-call.ts +1250 -0
- package/scripts/check-herdr-placement.ts +383 -0
- package/scripts/check-herdr-plugin-build.ts +842 -0
- package/scripts/check-herdr-plugin-profile.ts +371 -0
- package/scripts/check-herdr-plugin.ts +523 -0
- package/scripts/check-herdr-runtime-bootstrap.ts +1264 -0
- package/scripts/check-herdr-sandbox.ts +456 -0
- package/scripts/check-herdr-supply.ts +184 -0
- package/scripts/check-mux-launch-tmux.ts +331 -35
- package/scripts/check-mux-launcher-fence.ts +138 -0
- package/scripts/check-peer-facts.ts +331 -0
- package/scripts/check-release-gate-outcomes.ts +12 -0
- package/scripts/check-typing-call-fence.ts +296 -0
- package/scripts/codex-app-server-launch.sh +275 -0
- package/scripts/codex-birth-doctor.sh +159 -16
- package/scripts/codex-birth-install.sh +178 -72
- package/scripts/codex-birth-uninstall.sh +162 -7
- package/scripts/codex-socket-path.ts +33 -0
- package/scripts/codex-terminal-title-config.py +500 -0
- package/scripts/codex_toml_io.py +121 -0
- package/scripts/fixtures/herdr-supply.json +29 -0
- package/scripts/herdr-activation.mjs +536 -0
- package/scripts/herdr-plugin-activate.mjs +270 -0
- package/scripts/herdr-plugin-deactivate.mjs +193 -0
- package/scripts/herdr-runtime.mjs +1203 -0
- package/scripts/install-herdr-ci.sh +96 -0
- package/scripts/lib/codex-fresh-live-protocol.ts +13 -5
- package/scripts/lib/codex-fresh-source-receipts.ts +29 -2
- package/scripts/lib/tree-digest.ts +97 -0
- package/scripts/meta-bridge-install.sh +19 -2
- package/scripts/meta-bridge-state.py +167 -11
- package/scripts/mutants/codex-app-server-launch.json +157 -0
- package/scripts/mutants/codex-caller-seat.json +334 -0
- package/scripts/mutants/codex-native.json +195 -29
- package/scripts/mutants/control-socket-disconnect.json +57 -0
- package/scripts/mutants/fresh-call-dispatch.json +154 -0
- package/scripts/mutants/herdr-activation.json +305 -0
- package/scripts/mutants/herdr-fresh-call.json +448 -0
- package/scripts/mutants/herdr-placement.json +161 -0
- package/scripts/mutants/herdr-plugin-build.json +160 -0
- package/scripts/mutants/herdr-plugin-profile.json +202 -0
- package/scripts/mutants/herdr-plugin.json +133 -0
- package/scripts/mutants/herdr-runtime-bootstrap.json +411 -0
- package/scripts/mutants/herdr-supply.json +133 -0
- package/scripts/mutants/meta-hook-session-switch.json +2 -2
- package/scripts/mutants/mux-fresh-call.json +184 -25
- package/scripts/mutants/mux-launcher-fence.json +13 -0
- package/scripts/mutants/omp-fresh.json +4 -4
- package/scripts/mutants/peer-facts.json +98 -0
- package/scripts/mutants/self-address.json +33 -0
- package/scripts/mutants/typing-call-fence.json +17 -0
- package/scripts/mutants/v2-surface.json +22 -0
- package/scripts/peer-facts.ts +120 -0
- package/scripts/register-pi-package.py +8 -0
- package/scripts/register-pi-provider.py +88 -12
- package/scripts/smoke-codex-birth.sh +281 -12
- package/scripts/smoke-codex-config-state.sh +192 -3
- package/scripts/smoke-codex-fresh-live.ts +277 -37
- package/scripts/smoke-entwurf-chain-live.ts +50 -0
- package/scripts/smoke-herdr-fresh-call-live.ts +1027 -0
- package/scripts/smoke-herdr-plugin-build-live.ts +543 -0
- package/scripts/smoke-setup-verdict.sh +13 -11
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-peer-facts — deterministic gate for the #116 M2-a observed-peer projection
|
|
3
|
+
* (`entwurf peer-facts` / scripts/peer-facts.ts).
|
|
4
|
+
*
|
|
5
|
+
* Drives the REAL CLI as a subprocess against sandboxed fixtures — every root it can
|
|
6
|
+
* reach is an explicit mkdtemp dir (store, control-socket dir, HOME, XDG_DATA_HOME, pi
|
|
7
|
+
* agent dir), so the operator's live store, sockets and markers are never read. What it
|
|
8
|
+
* proves, and why each claim is its own contract rather than a restatement:
|
|
9
|
+
*
|
|
10
|
+
* - `placement` crosses as the STRUCTURED tagged union, never the human `herdr <pane>`
|
|
11
|
+
* string. A machine consumer joins on `paneId`; a string forces it to re-parse a
|
|
12
|
+
* presentation decision;
|
|
13
|
+
* - the peer keyset is EXACTLY the provider's own facts — no herdr `agent_status` or any
|
|
14
|
+
* other display verdict may enter an entwurf payload, because that is the one door
|
|
15
|
+
* through which "observed activity" could drift into delivery liveness;
|
|
16
|
+
* - no socket coordinate is published: `ENTWURF_DIR` is an INPUT to the probe, never an
|
|
17
|
+
* output — #50 C4 retired the legacy listing together with the `controlDir` it exposed;
|
|
18
|
+
* - `diagnostics` survive the projection in-band (a readable store with hazards is a fact
|
|
19
|
+
* to report, not a reason to emit a clean-looking listing);
|
|
20
|
+
* - the control-socket world actually probed is the one `ENTWURF_DIR` names — proved
|
|
21
|
+
* BEHAVIOURALLY (a record-less socket in the fixture dir must surface as a diagnostic),
|
|
22
|
+
* not by echoing the field back;
|
|
23
|
+
* - no observation budget: a rationed row says `unobserved`, which in a machine payload
|
|
24
|
+
* is indistinguishable from "no herdr on this host";
|
|
25
|
+
* - an unreadable store is exit 3 with NO JSON, a missing store is a readable empty one,
|
|
26
|
+
* bad argv is exit 2;
|
|
27
|
+
* - the surface is reachable: run.sh dispatches it and the bridge build emits its
|
|
28
|
+
* compiled twin (the installed-host fence).
|
|
29
|
+
*
|
|
30
|
+
* The placement join itself, the one-read-per-listing rule and the peers payload keyset are
|
|
31
|
+
* NOT re-proved here — they belong to check-herdr-placement, check-entwurf-fact-provider and
|
|
32
|
+
* check-entwurf-peers-surface. This gate owns only what this verb adds.
|
|
33
|
+
*
|
|
34
|
+
* The gate spawns the CLI rather than importing its pieces because the exit code and stdout
|
|
35
|
+
* bytes ARE the contract a consumer holds. Every [QK:PF-*] token appears exactly once, on
|
|
36
|
+
* the assertion that fails for that claim. No herdr binary, no network, no model turn.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import assert from "node:assert/strict";
|
|
40
|
+
import { spawnSync } from "node:child_process";
|
|
41
|
+
import fs from "node:fs";
|
|
42
|
+
import os from "node:os";
|
|
43
|
+
import path from "node:path";
|
|
44
|
+
import { type MetaIdentity, serializeMetaIdentity } from "../pi-extensions/lib/meta-session.ts";
|
|
45
|
+
import { reclaimOnExit } from "./lib/reclaim-on-exit.ts";
|
|
46
|
+
|
|
47
|
+
let passed = 0;
|
|
48
|
+
function ok(label: string, cond: boolean): void {
|
|
49
|
+
assert.ok(cond, label);
|
|
50
|
+
console.log(` ok ${label}`);
|
|
51
|
+
passed++;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const REPO = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
|
|
55
|
+
|
|
56
|
+
const GID_PI = "20260611T111111-aaaaaa";
|
|
57
|
+
const GID_CLAUDE = "20260611T222222-bbbbbb";
|
|
58
|
+
/** A garden id no record in any fixture claims — its socket must be DEMOTED to a diagnostic. */
|
|
59
|
+
const GID_ORPHAN_SOCKET = "20260611T333333-cccccc";
|
|
60
|
+
|
|
61
|
+
function tmp(prefix: string): string {
|
|
62
|
+
return reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), `entwurf-peer-facts-${prefix}-`)));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function record(gardenId: string, over: Partial<MetaIdentity> = {}): string {
|
|
66
|
+
return serializeMetaIdentity({
|
|
67
|
+
schemaVersion: 3,
|
|
68
|
+
gardenId,
|
|
69
|
+
backend: "pi",
|
|
70
|
+
nativeSessionId: `n-${gardenId}`,
|
|
71
|
+
cwd: "/x",
|
|
72
|
+
model: "provider/model",
|
|
73
|
+
transcriptPath: null,
|
|
74
|
+
createdAt: "2026-06-11T00:00:00.000Z",
|
|
75
|
+
recordUpdatedAt: "2026-06-11T00:00:00.000Z",
|
|
76
|
+
...over,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function store(files: Record<string, string>): string {
|
|
81
|
+
const dir = tmp("store");
|
|
82
|
+
for (const [name, body] of Object.entries(files)) fs.writeFileSync(path.join(dir, name), body);
|
|
83
|
+
return dir;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface CliRun {
|
|
87
|
+
status: number | null;
|
|
88
|
+
stdout: string;
|
|
89
|
+
stderr: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Every ambient root this verb or the provider beneath it could reach is replaced. HOME and
|
|
94
|
+
* XDG_DATA_HOME move together: moving HOME alone still writes/reads real install-state below
|
|
95
|
+
* the inherited XDG root, which is exactly how a verification sweep once polluted a live host.
|
|
96
|
+
*/
|
|
97
|
+
function cli(args: string[], env: Record<string, string> = {}): CliRun {
|
|
98
|
+
const home = tmp("home");
|
|
99
|
+
const res = spawnSync(process.execPath, ["--experimental-strip-types", "scripts/peer-facts.ts", ...args], {
|
|
100
|
+
cwd: REPO,
|
|
101
|
+
encoding: "utf8",
|
|
102
|
+
env: {
|
|
103
|
+
...process.env,
|
|
104
|
+
HOME: home,
|
|
105
|
+
XDG_DATA_HOME: path.join(home, ".local", "share"),
|
|
106
|
+
PI_CODING_AGENT_DIR: path.join(home, ".pi", "agent"),
|
|
107
|
+
ENTWURF_DIR: tmp("control"),
|
|
108
|
+
// A herdr read here would be a child process and a live-host dependency. The
|
|
109
|
+
// provider's own gate owns that axis; this one must stay hermetic.
|
|
110
|
+
HERDR_ENV: "0",
|
|
111
|
+
...env,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
return { status: res.status, stdout: res.stdout ?? "", stderr: res.stderr ?? "" };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
interface Placement {
|
|
118
|
+
kind: string;
|
|
119
|
+
paneId?: string;
|
|
120
|
+
}
|
|
121
|
+
interface Peer {
|
|
122
|
+
gardenId: string;
|
|
123
|
+
backend: string;
|
|
124
|
+
cwd: string;
|
|
125
|
+
placement: Placement;
|
|
126
|
+
transcript: string;
|
|
127
|
+
[k: string]: unknown;
|
|
128
|
+
}
|
|
129
|
+
interface Projection {
|
|
130
|
+
schemaVersion: number;
|
|
131
|
+
storeDir: string;
|
|
132
|
+
peers: Peer[];
|
|
133
|
+
diagnostics: { kind: string; message: string }[];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function parseProjection(run: CliRun, label: string): Projection {
|
|
137
|
+
assert.equal(run.status, 0, `${label}: exit 0 (stderr: ${run.stderr})`);
|
|
138
|
+
return JSON.parse(run.stdout) as Projection;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── the projection carries the provider's facts, addressably ────────────────
|
|
142
|
+
{
|
|
143
|
+
const transcriptDir = tmp("transcripts");
|
|
144
|
+
const transcript = path.join(transcriptDir, "live.jsonl");
|
|
145
|
+
fs.writeFileSync(transcript, "{}\n");
|
|
146
|
+
|
|
147
|
+
const dir = store({
|
|
148
|
+
[`${GID_PI}.meta.json`]: record(GID_PI),
|
|
149
|
+
[`${GID_CLAUDE}.meta.json`]: record(GID_CLAUDE, {
|
|
150
|
+
backend: "claude-code",
|
|
151
|
+
nativeSessionId: "ce224ad7-9e29-41a5-81c3-08e09f017802",
|
|
152
|
+
transcriptPath: transcript,
|
|
153
|
+
}),
|
|
154
|
+
});
|
|
155
|
+
const facts = parseProjection(cli([dir]), "valid store");
|
|
156
|
+
|
|
157
|
+
ok(
|
|
158
|
+
"projection declares schemaVersion 1 and echoes the store it was told to read",
|
|
159
|
+
facts.schemaVersion === 1 && facts.storeDir === dir,
|
|
160
|
+
);
|
|
161
|
+
// Deliberately does NOT inspect `placement`: the shape of that field is the next
|
|
162
|
+
// assertion's own claim, and a generic check here would steal its kill.
|
|
163
|
+
ok(
|
|
164
|
+
"both citizens cross with the identity fields a renderer needs",
|
|
165
|
+
facts.peers.length === 2 &&
|
|
166
|
+
facts.peers.every(
|
|
167
|
+
(p) => typeof p.gardenId === "string" && typeof p.backend === "string" && typeof p.cwd === "string",
|
|
168
|
+
),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// PLACEMENT STAYS STRUCTURED. `unobserved` is the only value reachable without a herdr
|
|
172
|
+
// host, and that is enough: the human rendering of THAT value is the bare string
|
|
173
|
+
// "unobserved", so a projection that stringified it would be caught right here.
|
|
174
|
+
const placements = facts.peers.map((p) => p.placement);
|
|
175
|
+
assert.ok(
|
|
176
|
+
placements.every(
|
|
177
|
+
(pl) => typeof pl === "object" && pl !== null && !Array.isArray(pl) && typeof pl.kind === "string",
|
|
178
|
+
),
|
|
179
|
+
'[QK:PF-PLACEMENT-STRUCTURED] placement crosses as the tagged union object ({kind:…}), never the human "herdr <pane>" / bare-kind string a renderer would have to re-parse',
|
|
180
|
+
);
|
|
181
|
+
console.log(" ok placement crosses as the tagged union object, never the human string");
|
|
182
|
+
passed++;
|
|
183
|
+
ok(
|
|
184
|
+
"no herdr on this host reads as unobserved, not none",
|
|
185
|
+
placements.every((pl) => pl.kind === "unobserved"),
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// THE PROMOTION BARRIER. herdr owns idle/working/blocked/done and it is a screen verdict;
|
|
189
|
+
// the moment one of those words enters an entwurf payload it is one rename away from
|
|
190
|
+
// being read as delivery liveness. So the keyset is pinned EXACTLY, not by denylist.
|
|
191
|
+
const PEER_KEYS = [
|
|
192
|
+
"backend",
|
|
193
|
+
"createdAt",
|
|
194
|
+
"cwd",
|
|
195
|
+
"gardenId",
|
|
196
|
+
"liveness",
|
|
197
|
+
"model",
|
|
198
|
+
"nativeSessionId",
|
|
199
|
+
"placement",
|
|
200
|
+
"receiver",
|
|
201
|
+
"recordUpdatedAt",
|
|
202
|
+
"transcript",
|
|
203
|
+
];
|
|
204
|
+
for (const peer of facts.peers) {
|
|
205
|
+
assert.deepEqual(
|
|
206
|
+
Object.keys(peer).sort(),
|
|
207
|
+
PEER_KEYS,
|
|
208
|
+
"[QK:PF-PEER-KEYSET-EXACT] a peer row carries exactly the provider's own facts — no herdr agent_status / interactive_ready / screen verdict may enter an entwurf payload, because that is the door observed activity would use to become delivery liveness",
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
console.log(" ok a peer row carries exactly the provider's own facts, with no herdr display verdict");
|
|
212
|
+
passed++;
|
|
213
|
+
// FOUR, not five: the control dir is an INPUT to the probe and never an output. #50 C4
|
|
214
|
+
// retired the legacy projection "with the `controlDir` it exposed" — the record is the
|
|
215
|
+
// sole address axis and a socket path is dispatch-internal transport. A new verb may not
|
|
216
|
+
// quietly republish it.
|
|
217
|
+
assert.deepEqual(
|
|
218
|
+
Object.keys(facts as unknown as Record<string, unknown>).sort(),
|
|
219
|
+
["diagnostics", "peers", "schemaVersion", "storeDir"],
|
|
220
|
+
"[QK:PF-NO-SOCKET-COORDINATE] the projection publishes no socket coordinate — #50 C4 retired the legacy listing WITH the controlDir it exposed, because the record is the sole address axis and a socket path is dispatch-internal transport; a new verb that republishes it undoes that retirement while looking like a convenience",
|
|
221
|
+
);
|
|
222
|
+
console.log(" ok the projection publishes no socket coordinate (four documented fields)");
|
|
223
|
+
passed++;
|
|
224
|
+
|
|
225
|
+
// NO OBSERVATION BUDGET. A rationed row says `unobserved`; in a machine payload that is
|
|
226
|
+
// indistinguishable from "nobody could look". The live transcript below is the witness:
|
|
227
|
+
// under a budget it would read `unobserved` instead of `exists`.
|
|
228
|
+
const claude = facts.peers.find((p) => p.gardenId === GID_CLAUDE);
|
|
229
|
+
assert.ok(
|
|
230
|
+
claude !== undefined && claude.transcript === "exists",
|
|
231
|
+
"[QK:PF-NO-OBSERVATION-BUDGET] every row is observed — a presentation budget would emit `unobserved` for rows nobody chose to skip, which a machine consumer cannot tell apart from `nobody could look`",
|
|
232
|
+
);
|
|
233
|
+
console.log(" ok every row is observed — no presentation budget rations a machine projection");
|
|
234
|
+
passed++;
|
|
235
|
+
|
|
236
|
+
const again = parseProjection(cli([dir]), "re-run");
|
|
237
|
+
ok("output is byte-deterministic across runs", JSON.stringify(again.peers) === JSON.stringify(facts.peers));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── diagnostics ride in-band, they are not a reason to look clean ───────────
|
|
241
|
+
{
|
|
242
|
+
const dir = store({
|
|
243
|
+
[`${GID_PI}.meta.json`]: record(GID_PI),
|
|
244
|
+
[`${GID_CLAUDE}.meta.json`]: "{ not json",
|
|
245
|
+
});
|
|
246
|
+
const facts = parseProjection(cli([dir]), "store with a defect");
|
|
247
|
+
ok("a readable store with an uncertifiable entry still reports its healthy citizen", facts.peers.length === 1);
|
|
248
|
+
assert.ok(
|
|
249
|
+
facts.diagnostics.some((d) => d.kind === "meta-record-read-error"),
|
|
250
|
+
"[QK:PF-DIAGNOSTICS-PRESERVED] the provider's diagnostics cross the projection in-band — dropping them turns a store with hazards into a clean-looking listing at exit 0",
|
|
251
|
+
);
|
|
252
|
+
console.log(" ok the provider's diagnostics cross the projection in-band");
|
|
253
|
+
passed++;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ── the socket world probed is the one ENTWURF_DIR names, proved behaviourally ──
|
|
257
|
+
{
|
|
258
|
+
const dir = store({ [`${GID_PI}.meta.json`]: record(GID_PI) });
|
|
259
|
+
const control = tmp("control-seeded");
|
|
260
|
+
// Not a listening socket — a plain file with the socket grammar. The probe fails, the
|
|
261
|
+
// liveness is `dead`, and the point stands: this path was READ. A verb that ignored
|
|
262
|
+
// ENTWURF_DIR would probe the sandbox HOME's default dir and report nothing here.
|
|
263
|
+
fs.writeFileSync(path.join(control, `${GID_ORPHAN_SOCKET}.sock`), "");
|
|
264
|
+
|
|
265
|
+
const facts = parseProjection(cli([dir], { ENTWURF_DIR: control }), "seeded control dir");
|
|
266
|
+
// The proof has to be behavioural precisely BECAUSE the dir is not emitted: there is no
|
|
267
|
+
// field to read back, so the witness is what the probe found in it.
|
|
268
|
+
ok(
|
|
269
|
+
"the probed dir is not echoed into the payload",
|
|
270
|
+
!Object.hasOwn(facts as unknown as Record<string, unknown>, "controlDir"),
|
|
271
|
+
);
|
|
272
|
+
assert.ok(
|
|
273
|
+
facts.diagnostics.some((d) => d.kind === "record-less-socket" && d.message.length > 0),
|
|
274
|
+
"[QK:PF-CONTROL-DIR-PROBED] the socket world actually probed is the one ENTWURF_DIR names — the same override the bridge honours, so this verb and entwurf_peers can never answer from two different socket worlds",
|
|
275
|
+
);
|
|
276
|
+
console.log(" ok the socket world probed is the one ENTWURF_DIR names");
|
|
277
|
+
passed++;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ── the exit contract ───────────────────────────────────────────────────────
|
|
281
|
+
{
|
|
282
|
+
const missing = path.join(tmp("absent"), "no-such-store");
|
|
283
|
+
const run = cli([missing]);
|
|
284
|
+
const facts = parseProjection(run, "missing store");
|
|
285
|
+
ok("a store that does not exist is a readable EMPTY store, exit 0", facts.peers.length === 0);
|
|
286
|
+
|
|
287
|
+
// An unreadable store must never be mistakable for an empty one, so the refusal is both
|
|
288
|
+
// a distinct exit code AND an empty stdout — a consumer that only reads stdout still
|
|
289
|
+
// cannot parse a false success out of it.
|
|
290
|
+
const unreadable = tmp("unreadable");
|
|
291
|
+
fs.writeFileSync(path.join(unreadable, "blocker"), "");
|
|
292
|
+
fs.chmodSync(unreadable, 0o000);
|
|
293
|
+
const denied = cli([unreadable]);
|
|
294
|
+
fs.chmodSync(unreadable, 0o700);
|
|
295
|
+
assert.ok(
|
|
296
|
+
denied.status === 3 && denied.stdout === "",
|
|
297
|
+
`[QK:PF-EXIT-UNREADABLE] an unreadable store is exit 3 with no JSON — answering it as exit 0 would make an unreadable host look like an empty one (got ${denied.status}, stdout ${JSON.stringify(denied.stdout.slice(0, 80))})`,
|
|
298
|
+
);
|
|
299
|
+
console.log(" ok an unreadable store is exit 3 with no JSON");
|
|
300
|
+
passed++;
|
|
301
|
+
|
|
302
|
+
const flag = cli(["--help"]);
|
|
303
|
+
assert.ok(
|
|
304
|
+
flag.status === 2 && flag.stdout === "",
|
|
305
|
+
"[QK:PF-USAGE] a dash argv is a flag this command does not have — reading it as a store directory would answer `--help` with `empty store, exit 0`, a silent wrong fact",
|
|
306
|
+
);
|
|
307
|
+
console.log(" ok a dash argv is a usage error, not a store directory");
|
|
308
|
+
passed++;
|
|
309
|
+
ok("a second positional is a usage error", cli(["/a", "/b"]).status === 2);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ── the surface is reachable from an installed host ─────────────────────────
|
|
313
|
+
{
|
|
314
|
+
const runSh = fs.readFileSync(path.join(REPO, "run.sh"), "utf8");
|
|
315
|
+
ok(
|
|
316
|
+
"run.sh dispatches peer-facts through the single run_ts fence",
|
|
317
|
+
/\n {2}peer-facts\)\n(?:.*\n)*?\s*run_ts scripts\/peer-facts\.ts "\$@"\n/.test(runSh),
|
|
318
|
+
);
|
|
319
|
+
ok("run.sh documents peer-facts in its usage block", runSh.includes("./run.sh peer-facts "));
|
|
320
|
+
const build = fs.readFileSync(path.join(REPO, "mcp/entwurf-bridge/tsconfig.build.json"), "utf8");
|
|
321
|
+
ok(
|
|
322
|
+
"the bridge build emits the compiled twin (installed hosts cannot strip-types)",
|
|
323
|
+
build.includes('"../../scripts/peer-facts.ts"'),
|
|
324
|
+
);
|
|
325
|
+
ok(
|
|
326
|
+
"both pack artifact lists carry the compiled twin",
|
|
327
|
+
(runSh.match(/"mcp\/entwurf-bridge\/dist\/scripts\/peer-facts\.js"/g) ?? []).length === 2,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
console.log(`\ncheck-peer-facts: ${passed} assertions passed`);
|
|
@@ -304,6 +304,18 @@ function runSubcommand(sub: string, env: Record<string, string | undefined>): {
|
|
|
304
304
|
// NOT waived: its direct call stays required for a Cortex-rail cut, and running
|
|
305
305
|
// it without the connection still reports protocol SKIP rather than a pass.
|
|
306
306
|
"smoke-acp-cortex-live": ["VERIFY.md", "The release aggregate does not re-certify Cortex"],
|
|
307
|
+
// #116 C4: first evidence precedes promotion. The herdr rail's LIVE acceptance exists and
|
|
308
|
+
// runs on demand, but a rail whose first real acceptance run is days old has not earned a
|
|
309
|
+
// place in the aggregate that blocks every cut — and herdr is an OPTIONAL rail, so a cut
|
|
310
|
+
// on a host without it would block for a capability the product does not require.
|
|
311
|
+
"smoke-herdr-fresh-call-live": ["VERIFY.md", "The release aggregate does not yet require the herdr rail"],
|
|
312
|
+
// #116 M3-b3, same rail and the same reason: the plugin build smoke drives a REAL
|
|
313
|
+
// `herdr plugin install` over the network. herdr is optional, so an aggregate that required it
|
|
314
|
+
// would block a cut on a host that does not have the rail at all. Its first LIVE acceptance
|
|
315
|
+
// landed 2026-09-16 (5 assertions, 0 skipped) — one run is evidence the axis works, not the run
|
|
316
|
+
// history that earns a place in the gate blocking every cut. On-demand, with the exclusion
|
|
317
|
+
// stated where an operator reads it.
|
|
318
|
+
"smoke-herdr-plugin-build-live": ["VERIFY.md", "The release aggregate does not require the plugin build smoke"],
|
|
307
319
|
};
|
|
308
320
|
|
|
309
321
|
const allLive = globSync("scripts/smoke-*live*.{ts,sh}", { cwd: REPO_DIR })
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-typing-call-fence — the boundary nobody has broken yet, kept by NAME.
|
|
3
|
+
*
|
|
4
|
+
* THE RULE: this product composes ARGUMENTS and LETTERS, never keystrokes. It is written in two
|
|
5
|
+
* places already — `docs/herdr-launch-rail.md` §3 ("키 입력(`agent prompt`): 터미널 입력이다") and
|
|
6
|
+
* `pi-extensions/lib/herdr-fresh-call.ts:28` — and until now nothing but prose held it.
|
|
7
|
+
*
|
|
8
|
+
* WHY A GATE FOR A RULE NOBODY BROKE `[외부 관측 2026-09-17 19:35]`. An outside reader measured the
|
|
9
|
+
* tree and found the law perfectly kept and nothing keeping it: zero typing calls across
|
|
10
|
+
* `pi-extensions/`, and zero checks and zero mutants that would notice one arriving. Gates here grow
|
|
11
|
+
* on top of incidents, and this boundary was named and refused at DESIGN time, so it never produced
|
|
12
|
+
* the failure that would have produced a test. A rule kept since day one is the most defenceless
|
|
13
|
+
* kind: the green that covers it means only "nobody has broken it yet".
|
|
14
|
+
*
|
|
15
|
+
* WHAT A TYPING CALL IS. Anything that reaches a sibling by writing into the place a HUMAN types:
|
|
16
|
+
* tmux `send-keys` / `paste-buffer` / `load-buffer`, and herdr's PTY verbs — `agent.prompt`,
|
|
17
|
+
* `agent.send_keys`, `pane.send_text`, `pane.input.set`. `[측정 2026-09-18, herdr @ 7505c08]` those
|
|
18
|
+
* herdr verbs are not an API delivery with an acknowledgement: `src/app/api/agents.rs:195` hands the
|
|
19
|
+
* text to `encode_api_submission_parts`, `src/app/api_helpers.rs:25-32` wraps it in bracketed paste,
|
|
20
|
+
* and the bytes go to the child pane's PTY followed 300 ms later by an encoded Enter. What is
|
|
21
|
+
* acknowledged is that input was written — herdr's own help says it "does not track turns". That is
|
|
22
|
+
* exactly what Hard Rule 16 keeps out of delivery evidence.
|
|
23
|
+
*
|
|
24
|
+
* SCOPE. The production surfaces `pi-extensions/` and `mcp/`, tracked files only (so the gitignored
|
|
25
|
+
* `mcp/entwurf-bridge/dist/` build output is not scanned twice as its own source). This gate lives
|
|
26
|
+
* in `scripts/`, which is NOT in scope — a gate that scanned itself could only be made green by
|
|
27
|
+
* deleting the names it exists to forbid.
|
|
28
|
+
*
|
|
29
|
+
* COMMENTS ARE EXEMPT, AND THAT IS LOAD-BEARING. The sentence stating the rule contains the very
|
|
30
|
+
* names the rule forbids. A scanner that read prose would be red on the rule itself, and the
|
|
31
|
+
* cheapest way to green would be deleting the sentence — the gate would eat its own law. So every
|
|
32
|
+
* file is reduced to CODE (string literals kept, comments blanked) before the names are looked for.
|
|
33
|
+
*
|
|
34
|
+
* TYPEFENCE-PRODUCTION-CLEAN no production module under pi-extensions/ or mcp/ carries a typing
|
|
35
|
+
* call in code. A module that acquires one is refused by name.
|
|
36
|
+
*
|
|
37
|
+
* The three assertions before it are this gate's own oracle: the detector is proven to SEE each
|
|
38
|
+
* forbidden name in planted code, proven NOT to see it in prose, and the scanned inventory is
|
|
39
|
+
* measured rather than assumed — without those, "zero hits" and "looked at nothing" read the same.
|
|
40
|
+
*
|
|
41
|
+
* WHAT THIS GATE DOES NOT CATCH, NAMED RATHER THAN IMPLIED (sol D3, 2026-09-18). It reads SPELLINGS.
|
|
42
|
+
* A typing call assembled at runtime — `["send", "keys"].join("-")`, a template with the verb in a
|
|
43
|
+
* variable, an RPC method built from parts — passes it, and no wording of this scanner would change
|
|
44
|
+
* that without becoming a dataflow analysis.
|
|
45
|
+
*
|
|
46
|
+
* That limit is accepted rather than closed, and the reason is what this gate is FOR. It is a
|
|
47
|
+
* tripwire for the slip: a module that reaches for the vendor's typing verb because the argv route
|
|
48
|
+
* looked harder that afternoon, written the ordinary way, spelled out. It is not a defence against
|
|
49
|
+
* somebody deliberately hiding one, and a gate that claimed to be would be the more dangerous
|
|
50
|
+
* object — the claim above therefore says CARRIES a typing call spelled out, not "composes" one.
|
|
51
|
+
* The defence against deliberate evasion is review, and the rule it enforces is in
|
|
52
|
+
* `docs/herdr-launch-rail.md` §3 where a human reads it.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
import assert from "node:assert/strict";
|
|
56
|
+
import { execFileSync } from "node:child_process";
|
|
57
|
+
import * as fs from "node:fs";
|
|
58
|
+
import * as path from "node:path";
|
|
59
|
+
import { fileURLToPath } from "node:url";
|
|
60
|
+
|
|
61
|
+
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
62
|
+
|
|
63
|
+
let passed = 0;
|
|
64
|
+
function ok(label: string, cond: boolean, detail = ""): void {
|
|
65
|
+
assert.ok(cond, detail ? `${label}\n${detail}` : label);
|
|
66
|
+
console.log(` ok ${label}`);
|
|
67
|
+
passed++;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Each forbidden name, with the surface it belongs to. A pattern matches CODE only. */
|
|
71
|
+
const TYPING_CALLS: readonly { readonly name: string; readonly pattern: RegExp; readonly surface: string }[] = [
|
|
72
|
+
{ name: "send-keys", pattern: /send-keys/, surface: "tmux / herdr CLI" },
|
|
73
|
+
{ name: "send_keys", pattern: /send_keys/, surface: "herdr agent.send_keys" },
|
|
74
|
+
{ name: "paste-buffer", pattern: /paste-buffer/, surface: "tmux" },
|
|
75
|
+
{ name: "load-buffer", pattern: /load-buffer/, surface: "tmux (feeds paste-buffer)" },
|
|
76
|
+
{ name: "send-text / send_text", pattern: /send[-_]text/, surface: "herdr pane.send_text" },
|
|
77
|
+
{ name: '"agent.prompt"', pattern: /["'`]agent\.prompt["'`]/, surface: "herdr JSON-RPC method" },
|
|
78
|
+
{ name: '"pane.input.set"', pattern: /["'`]pane\.input\.set["'`]/, surface: "herdr JSON-RPC method" },
|
|
79
|
+
{ name: '"agent","prompt"', pattern: /["'`]agent["'`]\s*,\s*["'`]prompt["'`]/, surface: "herdr CLI argv" },
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
type Flavor = "c" | "shell";
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Blank every comment, keep every code byte — including string literals, because a typing call
|
|
86
|
+
* spelled as a string IS the call. Offsets are preserved so a hit still reports its own line.
|
|
87
|
+
*/
|
|
88
|
+
function codeOnly(src: string, flavor: Flavor): string {
|
|
89
|
+
const out = src.split("");
|
|
90
|
+
let state: "code" | "line" | "block" | "sq" | "dq" | "tpl" = "code";
|
|
91
|
+
let i = 0;
|
|
92
|
+
while (i < src.length) {
|
|
93
|
+
const c = src[i];
|
|
94
|
+
const n = src[i + 1];
|
|
95
|
+
if (state === "code") {
|
|
96
|
+
if (flavor === "c" && c === "/" && n === "/") {
|
|
97
|
+
state = "line";
|
|
98
|
+
out[i] = " ";
|
|
99
|
+
out[i + 1] = " ";
|
|
100
|
+
i += 2;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (flavor === "c" && c === "/" && n === "*") {
|
|
104
|
+
state = "block";
|
|
105
|
+
out[i] = " ";
|
|
106
|
+
out[i + 1] = " ";
|
|
107
|
+
i += 2;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
// POSIX: `#` opens a comment only at the start of a word, so `${VAR#prefix}` is code.
|
|
111
|
+
if (flavor === "shell" && c === "#" && (i === 0 || /\s/.test(src[i - 1]))) {
|
|
112
|
+
state = "line";
|
|
113
|
+
out[i] = " ";
|
|
114
|
+
i += 1;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (c === "'") state = "sq";
|
|
118
|
+
else if (c === '"') state = "dq";
|
|
119
|
+
else if (c === "`" && flavor === "c") state = "tpl";
|
|
120
|
+
i += 1;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (state === "line") {
|
|
124
|
+
if (c === "\n") state = "code";
|
|
125
|
+
else out[i] = " ";
|
|
126
|
+
i += 1;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (state === "block") {
|
|
130
|
+
if (c === "*" && n === "/") {
|
|
131
|
+
out[i] = " ";
|
|
132
|
+
out[i + 1] = " ";
|
|
133
|
+
state = "code";
|
|
134
|
+
i += 2;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (c !== "\n") out[i] = " ";
|
|
138
|
+
i += 1;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
// inside a string literal: bytes are kept, escapes consume the next byte
|
|
142
|
+
if (c === "\\") {
|
|
143
|
+
i += 2;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if ((state === "sq" && c === "'") || (state === "dq" && c === '"') || (state === "tpl" && c === "`")) {
|
|
147
|
+
state = "code";
|
|
148
|
+
}
|
|
149
|
+
i += 1;
|
|
150
|
+
}
|
|
151
|
+
return out.join("");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
type Hit = { readonly file: string; readonly line: number; readonly name: string; readonly text: string };
|
|
155
|
+
|
|
156
|
+
function scan(rel: string, src: string, flavor: Flavor): Hit[] {
|
|
157
|
+
const code = codeOnly(src, flavor);
|
|
158
|
+
const rawLines = src.split("\n");
|
|
159
|
+
const hits: Hit[] = [];
|
|
160
|
+
code.split("\n").forEach((codeLine, idx) => {
|
|
161
|
+
for (const call of TYPING_CALLS) {
|
|
162
|
+
if (call.pattern.test(codeLine)) {
|
|
163
|
+
hits.push({ file: rel, line: idx + 1, name: call.name, text: rawLines[idx].trim() });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
return hits;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const flavorOf = (rel: string): Flavor => (rel.endsWith(".sh") ? "shell" : "c");
|
|
171
|
+
|
|
172
|
+
// ── 1. the detector sees a planted typing call in code ──────────────────────
|
|
173
|
+
{
|
|
174
|
+
const plantedTs = [
|
|
175
|
+
'const argv = ["send-keys", "-t", pane, text];',
|
|
176
|
+
'await run(["paste-buffer", "-t", pane]);',
|
|
177
|
+
'await run(["load-buffer", "-b", buf]);',
|
|
178
|
+
'const m = "agent.send_keys";',
|
|
179
|
+
'const t = "pane.send_text";',
|
|
180
|
+
'const p = "agent.prompt";',
|
|
181
|
+
'const i = "pane.input.set";',
|
|
182
|
+
'const cli = ["agent", "prompt", target, text];',
|
|
183
|
+
].join("\n");
|
|
184
|
+
const found = new Set(scan("fixture.ts", plantedTs, "c").map((h) => h.name));
|
|
185
|
+
ok(
|
|
186
|
+
`the detector names all ${TYPING_CALLS.length} typing calls planted in code (a green scan means it looked, not that it is blind)`,
|
|
187
|
+
found.size === TYPING_CALLS.length,
|
|
188
|
+
`missed: ${TYPING_CALLS.map((c) => c.name)
|
|
189
|
+
.filter((n) => !found.has(n))
|
|
190
|
+
.join(", ")}`,
|
|
191
|
+
);
|
|
192
|
+
ok(
|
|
193
|
+
"the shell flavor sees a typing call too — a launcher script is production surface",
|
|
194
|
+
scan("fixture.sh", 'tmux send-keys -t "$PANE" "$TEXT"\n', "shell").length === 1,
|
|
195
|
+
);
|
|
196
|
+
ok(
|
|
197
|
+
"a `${VAR#prefix}` expansion does not blind the shell scan for the rest of its line",
|
|
198
|
+
scan("fixture.sh", 'name="${PANE#%}"; tmux send-keys -t "$name" x\n', "shell").length === 1,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── 2. prose is exempt — the rule's own sentence must survive its own gate ──
|
|
203
|
+
{
|
|
204
|
+
const prose = [
|
|
205
|
+
"// keystrokes (`agent prompt`) are terminal input, which this product does not use to launch.",
|
|
206
|
+
"/* tmux send-keys and paste-buffer are the shape this fence refuses. */",
|
|
207
|
+
'const real = "fine";',
|
|
208
|
+
].join("\n");
|
|
209
|
+
ok(
|
|
210
|
+
"a typing call NAMED in a comment is not a violation — otherwise the cheapest green would be deleting the sentence that states the rule",
|
|
211
|
+
scan("fixture.ts", prose, "c").length === 0,
|
|
212
|
+
);
|
|
213
|
+
ok(
|
|
214
|
+
"the same exemption in shell comments",
|
|
215
|
+
scan("fixture.sh", "# tmux send-keys is refused here\nexec node x.js\n", "shell").length === 0,
|
|
216
|
+
);
|
|
217
|
+
// The live sentence, read from the rail itself rather than quoted here: if it is ever reworded
|
|
218
|
+
// the exemption must be re-proved against what the file ACTUALLY says. Only the sentence is
|
|
219
|
+
// scanned — whether the module as a whole is clean is the LAW's claim below, not this one's.
|
|
220
|
+
const railLine = fs
|
|
221
|
+
.readFileSync(path.join(REPO, "pi-extensions/lib/herdr-fresh-call.ts"), "utf8")
|
|
222
|
+
.split("\n")
|
|
223
|
+
.find((l) => l.includes("keystrokes (`agent prompt`)"));
|
|
224
|
+
ok(
|
|
225
|
+
"the launch rail's own written rule names `agent prompt` and is exempt as the prose it is",
|
|
226
|
+
railLine !== undefined && scan("rail-rule", railLine, "c").length === 0,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── 3. the scanned inventory is MEASURED, never assumed ─────────────────────
|
|
231
|
+
//
|
|
232
|
+
// SCOPE grew on 2026-09-18 (sol D3 / glm #4). `plugins/herdr/` and the four `scripts/herdr-*.mjs`
|
|
233
|
+
// runtime scripts are the code that actually drives the herdr CLI, and they sat OUTSIDE the law
|
|
234
|
+
// that exists because of that CLI. They are clean today — this gate is what keeps them so.
|
|
235
|
+
//
|
|
236
|
+
// AND THE LISTING IS NOT `--cached` ALONE. A brand-new production module is untracked until it is
|
|
237
|
+
// staged, so a fence that read only the index would look at everything except the file most likely
|
|
238
|
+
// to be carrying the mistake. `--others --exclude-standard` adds exactly the untracked files git
|
|
239
|
+
// would offer to add, which leaves the gitignored build output (`mcp/entwurf-bridge/dist/`) out
|
|
240
|
+
// for the reason it always was: it is this source, compiled, and scanning it twice proves nothing.
|
|
241
|
+
const SCOPE = ["pi-extensions", "mcp", "plugins/herdr"] as const;
|
|
242
|
+
const files = [
|
|
243
|
+
...execFileSync(
|
|
244
|
+
"git",
|
|
245
|
+
["ls-files", "--cached", "--others", "--exclude-standard", "--", ...SCOPE.map((s) => `${s}/*`)],
|
|
246
|
+
{
|
|
247
|
+
cwd: REPO,
|
|
248
|
+
encoding: "utf8",
|
|
249
|
+
},
|
|
250
|
+
)
|
|
251
|
+
.split("\n")
|
|
252
|
+
.filter(Boolean),
|
|
253
|
+
// The herdr runtime scripts live in `scripts/`, which is NOT in scope as a whole — this gate
|
|
254
|
+
// is in there, and a gate that scanned itself could only be made green by deleting the names
|
|
255
|
+
// it exists to forbid. So they are named, one glob, by what they are.
|
|
256
|
+
...execFileSync("git", ["ls-files", "--cached", "--others", "--exclude-standard", "--", "scripts/herdr-*.mjs"], {
|
|
257
|
+
cwd: REPO,
|
|
258
|
+
encoding: "utf8",
|
|
259
|
+
})
|
|
260
|
+
.split("\n")
|
|
261
|
+
.filter(Boolean),
|
|
262
|
+
]
|
|
263
|
+
.filter((f) => /\.(ts|js|mjs|cjs|sh)$/.test(f))
|
|
264
|
+
.filter((f) => fs.existsSync(path.join(REPO, f)));
|
|
265
|
+
|
|
266
|
+
{
|
|
267
|
+
const required = [
|
|
268
|
+
"pi-extensions/lib/herdr-fresh-call.ts",
|
|
269
|
+
"pi-extensions/lib/mux-fresh-call.ts",
|
|
270
|
+
"pi-extensions/lib/fresh-call-composition.ts",
|
|
271
|
+
"pi-extensions/entwurf-control.ts",
|
|
272
|
+
"mcp/entwurf-bridge/src/index.ts",
|
|
273
|
+
// The two surfaces that drive the vendor CLI this law is about — named, so widening the
|
|
274
|
+
// scope cannot quietly narrow again.
|
|
275
|
+
"plugins/herdr/lib/build.mjs",
|
|
276
|
+
"scripts/herdr-runtime.mjs",
|
|
277
|
+
];
|
|
278
|
+
const missing = required.filter((r) => !files.includes(r));
|
|
279
|
+
ok(
|
|
280
|
+
`scope is non-vacuous: ${files.length} tracked production sources under ${SCOPE.join(" + ")}, both launch rails and the MCP bridge among them`,
|
|
281
|
+
files.length >= 50 && missing.length === 0,
|
|
282
|
+
missing.length ? `not scanned: ${missing.join(", ")}` : `only ${files.length} files matched`,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ── 4. THE LAW ──────────────────────────────────────────────────────────────
|
|
287
|
+
{
|
|
288
|
+
const hits = files.flatMap((rel) => scan(rel, fs.readFileSync(path.join(REPO, rel), "utf8"), flavorOf(rel)));
|
|
289
|
+
ok(
|
|
290
|
+
`[QK:TYPEFENCE-PRODUCTION-CLEAN] none of the ${files.length} production sources CARRIES a typing call spelled out — this rail builds arguments and letters, and a module that acquires one of these names in code is refused by name`,
|
|
291
|
+
hits.length === 0,
|
|
292
|
+
hits.map((h) => `${h.file}:${h.line} carries ${h.name} — ${h.text}`).join("\n"),
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
console.log(`\ncheck-typing-call-fence: ${passed} assertions passed over ${files.length} production sources`);
|