@botiverse/k-carrier 0.1.8 → 0.2.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.
Files changed (62) hide show
  1. package/NOTICE +5 -2
  2. package/README.md +72 -26
  3. package/core/src/artifact/download.ts +23 -1
  4. package/core/src/artifact/gzip.ts +26 -0
  5. package/core/src/artifact/source.ts +2 -0
  6. package/core/src/{createUpgrader.ts → createRunner.ts} +13 -15
  7. package/core/src/index.ts +11 -3
  8. package/core/src/launcher/launch.ts +15 -0
  9. package/core/src/launcher/supervise.ts +170 -0
  10. package/core/src/lifecycle/commandHost.ts +111 -0
  11. package/core/src/lifecycle/hostAdapter.ts +28 -16
  12. package/core/src/operation.ts +47 -25
  13. package/core/src/operationLifecycle.ts +2 -7
  14. package/core/src/platform/ops.ts +7 -0
  15. package/core/src/platform/posix.ts +26 -6
  16. package/core/src/platform/windows.ts +8 -2
  17. package/core/src/protocol/runner.ts +81 -0
  18. package/core/src/provenance/journal.ts +1 -1
  19. package/core/src/runner/cli.ts +27 -0
  20. package/core/src/runner/execute.ts +68 -0
  21. package/core/src/txn/engine.ts +41 -85
  22. package/core/src/txn/fileEffects.ts +15 -1
  23. package/core/src/txn/hostCallBudget.ts +4 -1
  24. package/core/src/txn/hostCallUncertain.ts +2 -0
  25. package/core/src/txn/lock.ts +81 -37
  26. package/core/src/txn/state.ts +1 -1
  27. package/core/src/upgrade/drive.ts +31 -2
  28. package/core/src/upgrade/outcome.ts +1 -1
  29. package/core/src/upgrade/recover.ts +22 -1
  30. package/core/src/upgrade/retire.ts +1 -1
  31. package/core/src/upgrader.ts +4 -9
  32. package/docs/design.md +173 -0
  33. package/docs/guide.md +196 -0
  34. package/docs/harness-design.md +75 -170
  35. package/docs/integration.md +221 -364
  36. package/docs/prior-art/design-influences.md +26 -0
  37. package/docs/prior-art/external-runner-research.md +49 -0
  38. package/docs/reference.md +209 -0
  39. package/docs/test-plan.md +89 -92
  40. package/harness/src/adapter/releaseKnob.ts +1 -1
  41. package/harness/src/adapter/serviceChecks.ts +5 -5
  42. package/harness/src/artifact/m1.ts +8 -8
  43. package/harness/src/artifact/m1Resume.ts +2 -2
  44. package/harness/src/artifact/m3.ts +25 -104
  45. package/harness/src/artifact/m3Hosts.ts +9 -61
  46. package/harness/src/artifact/m4.ts +3 -3
  47. package/harness/src/artifact/m5.ts +5 -5
  48. package/harness/src/artifact/m6.ts +6 -6
  49. package/harness/src/artifact/m6Status.ts +1 -1
  50. package/harness/src/examples/checks.ts +10 -13
  51. package/harness/src/fixtures/cliToolSource.ts +166 -0
  52. package/harness/src/fixtures/externalCrashAdapter.ts +19 -0
  53. package/harness/src/fixtures/managedHost.ts +100 -0
  54. package/harness/src/fixtures/serviceSource.ts +181 -0
  55. package/harness/src/fixtures/supervisedAdapter.ts +57 -0
  56. package/harness/src/scenario/processScan.ts +3 -1
  57. package/harness/src/scenario/sandbox.ts +2 -2
  58. package/harness/src/teeth/artifact.ts +3 -3
  59. package/harness/src/teeth/examples.ts +1 -1
  60. package/package.json +5 -3
  61. package/docs/design-v1.md +0 -246
  62. package/docs/prior-art.md +0 -150
@@ -1,7 +1,7 @@
1
1
  import assert from "node:assert/strict";
2
2
  import * as path from "node:path";
3
3
  import { type ToothContext } from "../teeth/registry.ts";
4
- import { PLAIN_DAEMON_SOURCE } from "../../../examples/service-daemon/source.ts";
4
+ import { PLAIN_DAEMON_SOURCE } from "../fixtures/serviceSource.ts";
5
5
  import { serveRelease } from "./m1.ts";
6
6
  import { runCommand } from "../artifact-factory/run.ts";
7
7
  import { processAlive } from "../fake-host/daemon.ts";
@@ -10,7 +10,6 @@ import {
10
10
  serviceEnv,
11
11
  readIncarnation,
12
12
  killIncarnation,
13
- respawnUntilUp,
14
13
  seedService,
15
14
  waitDead,
16
15
  HOST_SHAPES,
@@ -19,7 +18,7 @@ import {
19
18
  import { readState } from "./m1.ts";
20
19
 
21
20
  // ---------------------------------------------------------------------------
22
- // m3.service-upgrade (both host shapes)
21
+ // m3.service-upgrade (external driver)
23
22
  // ---------------------------------------------------------------------------
24
23
 
25
24
  export async function checkM3ServiceUpgrade(
@@ -52,7 +51,6 @@ async function runUpgradeShape(
52
51
  });
53
52
  const { seedChild, seedPid } = await seedService(shapeCtx, shape, binPath, seed);
54
53
  const env = serviceEnv(shapeCtx, shape, target.url);
55
- let respawned: Awaited<ReturnType<typeof respawnUntilUp>> | null = null;
56
54
  try {
57
55
  const before = await readIncarnation(shapeCtx);
58
56
  assert.ok(before, "a running incarnation must exist before the upgrade");
@@ -66,12 +64,6 @@ async function runUpgradeShape(
66
64
  0,
67
65
  `self upgrade must exit 0 (${up.stderr.trim()}; serveBadVersion mutation => RED)`,
68
66
  );
69
- if (shape === "respawn") {
70
- // Tracked, not discarded: if an assertion below fails, an untracked
71
- // live service keeps the test runner alive and a red degrades into a
72
- // hang -- the failure mode is then indistinguishable from a wedge.
73
- respawned = await respawnUntilUp(shapeCtx, env);
74
- }
75
67
 
76
68
  // signal-sent ≠ dead: the OLD pid must be VERIFIED gone.
77
69
  assert.ok(!processAlive(oldPid), `[${shape}] the old incarnation must be verified dead`);
@@ -91,9 +83,6 @@ async function runUpgradeShape(
91
83
  assert.equal(state.stableVersion, "2.0.0", `[${shape}] stable slot must hold the new version`);
92
84
  assert.equal(state.phase, "promoted", `[${shape}] journal must end at promoted`);
93
85
  } finally {
94
- if (respawned && respawned.child.pid && processAlive(respawned.child.pid)) {
95
- respawned.child.kill("SIGKILL");
96
- }
97
86
  await killIncarnation(shapeCtx);
98
87
  if (processAlive(seedPid)) seedChild.kill("SIGKILL");
99
88
  await seed.stop();
@@ -102,7 +91,7 @@ async function runUpgradeShape(
102
91
  }
103
92
 
104
93
  // ---------------------------------------------------------------------------
105
- // m3.service-rollback (both host shapes)
94
+ // m3.service-rollback (external driver)
106
95
  // ---------------------------------------------------------------------------
107
96
 
108
97
  export async function checkM3ServiceRollback(
@@ -135,19 +124,12 @@ async function runRollbackShape(
135
124
  });
136
125
  const { seedChild, seedPid } = await seedService(shapeCtx, shape, binPath, seed);
137
126
  const env = serviceEnv(shapeCtx, shape, target.url);
138
- let respawned: Awaited<ReturnType<typeof respawnUntilUp>> | null = null;
139
127
  try {
140
128
  const before = await readIncarnation(shapeCtx);
141
129
  assert.ok(before, "a running incarnation must exist before the upgrade");
142
130
  const oldPid = before!.pid;
143
131
 
144
132
  const up = await runCommand(binPath, ["self", "upgrade"], { env, timeoutMs: 30000 });
145
- if (shape === "respawn") {
146
- // Tracked, not discarded: if an assertion below fails, an untracked
147
- // live service keeps the test runner alive and a red degrades into a
148
- // hang -- the failure mode is then indistinguishable from a wedge.
149
- respawned = await respawnUntilUp(shapeCtx, env);
150
- }
151
133
 
152
134
  const state = await readState(env);
153
135
  if (serveGoodVersion) {
@@ -168,8 +150,6 @@ async function runRollbackShape(
168
150
  `[${shape}] upgrade to a bad version must exit non-zero (${up.stderr.trim()})`,
169
151
  );
170
152
  }
171
- // (the respawn-shape driver exits 0 to request the successor; the
172
- // outcome is decided by the successor's recovery below)
173
153
  assert.equal(state.phase, "rolled-back", `[${shape}] journal must end at rolled-back`);
174
154
  assert.equal(state.stableVersion, "1.0.0", `[${shape}] stable slot must hold the old version`);
175
155
  assert.equal(state.experimentVersion, null, `[${shape}] experiment slot must be cleared`);
@@ -187,9 +167,6 @@ async function runRollbackShape(
187
167
  );
188
168
  assert.ok(!processAlive(oldPid), `[${shape}] the pre-upgrade incarnation must stay dead`);
189
169
  } finally {
190
- if (respawned && respawned.child.pid && processAlive(respawned.child.pid)) {
191
- respawned.child.kill("SIGKILL");
192
- }
193
170
  await killIncarnation(shapeCtx);
194
171
  if (processAlive(seedPid)) seedChild.kill("SIGKILL");
195
172
  await seed.stop();
@@ -198,98 +175,42 @@ async function runRollbackShape(
198
175
  }
199
176
 
200
177
  // ---------------------------------------------------------------------------
201
- // m3.stuck-driver-evidence-recovery (respawn shape)
178
+ // m3.stuck-driver-rollback-recovery
202
179
  // ---------------------------------------------------------------------------
203
180
 
204
- /**
205
- * A wedged driver (host.stop() never returns) must not wedge the updater:
206
- * the host-call budget times the driver out, the upgrade lock is released,
207
- * the owner respawns the successor, and the successor's recovery decides by
208
- * EVIDENCE (a live v2 with a fresh startId) — never by a "this restart was
209
- * planned" flag. `skipRecovery` simulates the must-red mutation (the
210
- * successor never recovers, so the transaction stays pending).
211
- */
181
+ /** A fresh external driver must undo interrupted work, never adopt a candidate. */
212
182
  export async function checkM3StuckDriverEvidence(
213
183
  ctx: ToothContext,
214
184
  opts: { skipRecovery?: boolean } = {},
215
185
  ): Promise<void> {
216
- const shape: HostShape = "respawn";
186
+ const shape: HostShape = "spawn";
217
187
  const shapeCtx: ToothContext = { profile: ctx.profile, sandboxDir: path.join(ctx.sandboxDir, shape) };
218
188
  const binPath = await buildServiceDaemon(shapeCtx);
219
- const seed = await serveRelease(shapeCtx, {
220
- version: "1.0.0",
221
- behavior: "ok",
222
- name: "seed",
223
- source: PLAIN_DAEMON_SOURCE,
224
- });
225
- const target = await serveRelease(shapeCtx, {
226
- version: "2.0.0",
227
- behavior: "ok",
228
- name: "target",
229
- source: PLAIN_DAEMON_SOURCE,
230
- });
231
- const { seedChild, seedPid } = await seedService(shapeCtx, shape, binPath, seed);
232
- const env = serviceEnv(shapeCtx, shape, target.url);
233
- let oldPid: number | null = null;
189
+ const seed = await serveRelease(shapeCtx, { version: "1.0.0", behavior: "ok", name: "seed", source: PLAIN_DAEMON_SOURCE });
190
+ const target = await serveRelease(shapeCtx, { version: "2.0.0", behavior: "ok", name: "target", source: PLAIN_DAEMON_SOURCE });
191
+ let seedPid: number | undefined;
234
192
  try {
235
- const before = await readIncarnation(shapeCtx);
236
- assert.ok(before, "a running incarnation must exist before the upgrade");
237
- oldPid = before!.pid;
238
-
239
- // The driver is wedged at stop(): it must time out (host-call budget),
240
- // release the lock, and exit — never wedge the tooth.
241
- const stuckEnv = { ...env, K_STUCK_DRIVER: "1" };
193
+ const seeded = await seedService(shapeCtx, shape, binPath, seed);
194
+ seedPid = seeded.seedPid;
195
+ const env = serviceEnv(shapeCtx, shape, target.url);
242
196
  const up = await runCommand(binPath, ["self", "upgrade"], {
243
- env: stuckEnv,
244
- timeoutMs: 30000,
197
+ env: { ...env, K_STUCK_DRIVER: "1" }, timeoutMs: 30000,
245
198
  });
246
- assert.notEqual(up.code, 0, "the wedged driver must fail (budget timeout), not hang");
247
-
248
- // The owner respawns the successor from the new bytes; its recovery
249
- // acquires the (released) lock and decides by evidence.
250
- const respawnEnv = { ...env, ...(opts.skipRecovery === true ? { K_SKIP_RECOVERY: "1" } : {}) };
251
- const { info: successor, child } = await respawnUntilUp(shapeCtx, respawnEnv);
252
- try {
253
- if (opts.skipRecovery) {
254
- // mutation: the successor never recovers — the transaction stays
255
- // pending at handing-over, so the promoted assertion goes RED
256
- const st = await readState(env);
257
- assert.equal(st.phase, "promoted", "the successor's recovery must finish the transaction (skipRecovery => RED)");
258
- return;
259
- }
260
-
261
- // ② the successor is up (respawned from the experiment bytes)
262
- assert.equal(successor.version, "2.0.0", "the successor must run the new version");
263
- assert.ok(processAlive(successor.pid), "the successor must be alive");
264
- assert.notEqual(successor.startId, before!.startId, "the successor is a fresh incarnation");
265
-
266
- // ③ the recovery's judgment matched the evidence (v2 + fresh startId):
267
- // the transaction reached promoted, so the lock WAS released (the
268
- // recovery acquired it) and the evidence said handover succeeded.
269
- const state = await readState(env);
270
- assert.equal(state.phase, "promoted", "recovery must finish at promoted (evidence, not a flag)");
271
- assert.equal(state.stableVersion, "2.0.0", "stable must hold the new version");
272
- } finally {
273
- if (processAlive(successor.pid)) {
274
- try {
275
- process.kill(successor.pid, "SIGKILL");
276
- } catch {
277
- // already gone
278
- }
279
- await waitDead(successor.pid, 5000);
280
- }
281
- void child;
199
+ assert.notEqual(up.code, 0, "the wedged driver must fail");
200
+ if (!opts.skipRecovery) {
201
+ const recovery = await runCommand(binPath, ["self", "recover"], { env, timeoutMs: 30000 });
202
+ assert.equal(recovery.code, 0, `external recovery must complete: ${recovery.stderr}`);
282
203
  }
204
+ const state = await readState(env);
205
+ assert.equal(state.phase, "rolled-back", "external recovery must finish the transaction by rollback");
206
+ assert.equal(state.stableVersion, "1.0.0");
207
+ assert.equal(state.experimentVersion, null);
208
+ const running = await readIncarnation(shapeCtx);
209
+ assert.ok(running && processAlive(running.pid), "restored stable must be alive");
210
+ assert.equal(running.version, "1.0.0");
283
211
  } finally {
284
212
  await killIncarnation(shapeCtx);
285
- if (oldPid !== null && processAlive(oldPid)) {
286
- try {
287
- process.kill(oldPid, "SIGKILL"); // the wedged stop never killed it
288
- } catch {
289
- // already gone
290
- }
291
- }
292
- if (processAlive(seedPid)) seedChild.kill("SIGKILL");
213
+ if (seedPid && processAlive(seedPid)) { process.kill(seedPid, "SIGKILL"); await waitDead(seedPid, 5000); }
293
214
  await seed.stop();
294
215
  await target.stop();
295
216
  }
@@ -9,25 +9,12 @@ import { FakeServer } from "../fake-server/server.ts";
9
9
  import { ArtifactFactory } from "../artifact-factory/factory.ts";
10
10
  import { processAlive } from "../fake-host/daemon.ts";
11
11
  import { commandForArtifact, runCommand } from "../artifact-factory/run.ts";
12
- import { PLAIN_DAEMON_SOURCE } from "../../../examples/service-daemon/source.ts";
13
- import { coreUpgraderUrl, readState } from "./m1.ts";
12
+ import { PLAIN_DAEMON_SOURCE } from "../fixtures/serviceSource.ts";
13
+ import { runnerFactoryUrl, readState } from "./m1.ts";
14
14
 
15
- /**
16
- * M3 host orchestration helpers — shared by both service-profile teeth
17
- * (m3.service-upgrade / m3.service-rollback), each of which runs on BOTH
18
- * host shapes:
19
- * - spawn: self-starting — the driver spawns the successor and finishes
20
- * the transaction itself.
21
- * - respawn: computer's shape — the driver DIES mid-handover (it cannot
22
- * start itself; the owner respawns it from the new bytes), and
23
- * the SUCCESSOR finishes the transaction via recovery, proving
24
- * the handover by a fresh startId (a flag could be set by the
25
- * crash path; a different incarnation is something that
26
- * happened).
27
- */
28
-
29
- export type HostShape = "spawn" | "respawn";
30
- export const HOST_SHAPES: HostShape[] = ["spawn", "respawn"];
15
+ /** External service driver; the resident never owns recovery. */
16
+ export type HostShape = "spawn";
17
+ export const HOST_SHAPES: HostShape[] = ["spawn"];
31
18
 
32
19
  const INCARNATION_FILE = "incarnation.json";
33
20
 
@@ -62,20 +49,19 @@ function stateDir(ctx: ToothContext): string {
62
49
 
63
50
  export function serviceEnv(
64
51
  ctx: ToothContext,
65
- shape: HostShape,
52
+ _shape: HostShape,
66
53
  baseUrl: string,
67
54
  ): Record<string, string> {
68
55
  return {
69
56
  K_RELEASE_BASE: baseUrl,
70
57
  K_STATE_DIR: stateDir(ctx),
71
- K_HOST_SHAPE: shape,
72
- K_CORE_UPGRADER: coreUpgraderUrl(),
58
+ K_CORE_UPGRADER: runnerFactoryUrl(),
73
59
  // Every process this tooth spawns — and, via inheritance, every process
74
60
  // the driver spawns — carries the sandbox marker, so the sandbox
75
61
  // teardown's verifyProcessTreeDead is the backstop for any leak: a "red"
76
62
  // can never degrade into a hang.
77
63
  // Resolved back to the SANDBOX id: ctx here is a per-shape NESTED context,
78
- // and basename() of that is "spawn"/"respawn" -- a marker no teardown scan
64
+ // and basename() of that is "spawn" -- a marker no teardown scan
79
65
  // matches, which turns the backstop into a scan that always finds zero.
80
66
  K_SANDBOX_MARKER: sandboxMarkerFor(ctx.sandboxDir),
81
67
  };
@@ -178,42 +164,7 @@ export async function killIncarnation(ctx: ToothContext): Promise<void> {
178
164
  }
179
165
  }
180
166
 
181
- /**
182
- * The OWNER (respawn shape): the driver exits mid-handover; the owner
183
- * respawns the service from the new bytes (experiment first), falling back
184
- * to stable when the new version cannot start, and respawning again when a
185
- * recovery asks for a successor by exiting. A bounded loop: the service
186
- * eventually stays up or the owner gives up.
187
- */
188
- export async function respawnUntilUp(
189
- ctx: ToothContext,
190
- env: Record<string, string>,
191
- ): Promise<{ info: Incarnation; child: ChildProcess }> {
192
- const dir = stateDir(ctx);
193
- for (let attempt = 0; attempt < 6; attempt++) {
194
- const slot: "experiment" | "stable" = attempt === 0 ? "experiment" : "stable";
195
- const artifact = path.join(dir, "slots", slot, "artifact.bin");
196
- try {
197
- await fs.access(artifact);
198
- } catch {
199
- continue;
200
- }
201
- const child = spawn(process.execPath, [artifact], {
202
- env: { ...process.env, ...env },
203
- stdio: ["pipe", "pipe", "ignore"],
204
- });
205
- const info = await tryReady(child, 2000);
206
- if (info !== null) return { info, child };
207
- if (child.pid && processAlive(child.pid)) child.kill("SIGKILL");
208
- }
209
- throw new Error("owner: the service never came up");
210
- }
211
-
212
- /**
213
- * Seed the service world: spawn the v1 service (registers), then a real
214
- * driver upgrade to 1.0.0 lands stable v1 with a running incarnation
215
- * (driver-completed on spawn, successor-completed on respawn).
216
- */
167
+ /** Seed stable v1 with an external driver and a live service. */
217
168
  export async function seedService(
218
169
  ctx: ToothContext,
219
170
  shape: HostShape,
@@ -228,9 +179,6 @@ export async function seedService(
228
179
 
229
180
  const up = await runCommand(binPath, ["self", "upgrade"], { env, timeoutMs: 30000 });
230
181
  assert.equal(up.code, 0, `seed upgrade must exit 0 (${up.stderr.trim()})`);
231
- if (shape === "respawn") {
232
- await respawnUntilUp(ctx, env); // the driver died; the owner brings the successor up
233
- }
234
182
  const seeded = await readState(env);
235
183
  assert.equal(seeded.stableVersion, "1.0.0", "seed upgrade must land stable 1.0.0");
236
184
  const running = await readIncarnation(ctx);
@@ -16,8 +16,8 @@ import { promises as fs } from "node:fs";
16
16
  import { type ToothContext } from "../teeth/registry.ts";
17
17
  import { FakeServer } from "../fake-server/server.ts";
18
18
  import { runCommand } from "../artifact-factory/run.ts";
19
- import { buildSwapTool, coreUpgraderUrl, readState, serveRelease } from "./m1.ts";
20
- import { CLI_TOOL_SOURCE } from "../../../examples/swap-tool/source.ts";
19
+ import { buildSwapTool, runnerFactoryUrl, readState, serveRelease } from "./m1.ts";
20
+ import { CLI_TOOL_SOURCE } from "../fixtures/cliToolSource.ts";
21
21
  import { ArtifactFactory } from "../artifact-factory/factory.ts";
22
22
  import { currentPlatformKey } from "../../../core/src/artifact/staticManifestSource.ts";
23
23
 
@@ -25,7 +25,7 @@ function swapEnv(ctx: ToothContext, baseUrl: string, servers: FakeServer[], extr
25
25
  return {
26
26
  K_RELEASE_BASE: baseUrl,
27
27
  K_STATE_DIR: path.join(ctx.sandboxDir, "state"),
28
- K_CORE_UPGRADER: coreUpgraderUrl(),
28
+ K_CORE_UPGRADER: runnerFactoryUrl(),
29
29
  ...extra,
30
30
  };
31
31
  }
@@ -3,14 +3,14 @@
3
3
  * (test-plan M5 rows: 点名面读回一致;面在 allowlist 才可作证;未注册面被引用
4
4
  * ⇒ 拒; fail-closed 退役序; 禁投影).
5
5
  *
6
- * These drive createUpgrader in-process (the library plane — the converge
6
+ * These drive createRunner in-process (the library plane — the converge
7
7
  * machinery is exactly the kind of internal tooth the black-box plane
8
8
  * cannot reach).
9
9
  */
10
10
  import assert from "node:assert/strict";
11
11
  import * as path from "node:path";
12
12
  import { type ToothContext } from "../teeth/registry.ts";
13
- import { createUpgrader, type CreateUpgraderOptions } from "../../../core/src/createUpgrader.ts";
13
+ import { createRunner, type RunnerOptions } from "../../../core/src/createRunner.ts";
14
14
  import { staticManifestSource } from "../../../core/src/artifact/staticManifestSource.ts";
15
15
  import {
16
16
  buildSurfaceAllowlist,
@@ -66,8 +66,8 @@ function makeUpgrader(
66
66
  ctx: ToothContext,
67
67
  server: FakeServer,
68
68
  surfaces: ReadbackSurface[],
69
- ): ReturnType<typeof createUpgrader> {
70
- const opts: CreateUpgraderOptions = {
69
+ ): ReturnType<typeof createRunner> {
70
+ const opts: RunnerOptions = {
71
71
  host: simpleHost(),
72
72
  source: staticManifestSource({ baseUrl: server.url }),
73
73
  policy: "auto",
@@ -75,7 +75,7 @@ function makeUpgrader(
75
75
  stateDir: stateDir(ctx),
76
76
  lifecycleSurfaces: surfaces,
77
77
  };
78
- return createUpgrader(opts);
78
+ return createRunner(opts);
79
79
  }
80
80
 
81
81
  // ---------------------------------------------------------------------------
@@ -5,7 +5,7 @@
5
5
  * genesis; every reconcile that reaches the transaction records WHO drove
6
6
  * it, write-ahead).
7
7
  *
8
- * These drive createUpgrader in-process (the library plane — the journal
8
+ * These drive createRunner in-process (the library plane — the journal
9
9
  * is exactly the kind of internal tooth the black-box plane cannot reach).
10
10
  * Mutation journals/summaries live in m6Mutations.ts (line budget).
11
11
  */
@@ -13,7 +13,7 @@ import assert from "node:assert/strict";
13
13
  import * as path from "node:path";
14
14
  import { promises as fs } from "node:fs";
15
15
  import { type ToothContext } from "../teeth/registry.ts";
16
- import { createUpgrader, type CreateUpgraderOptions } from "../../../core/src/createUpgrader.ts";
16
+ import { createRunner, type RunnerOptions } from "../../../core/src/createRunner.ts";
17
17
  import type { ReleaseSource } from "../../../core/src/artifact/source.ts";
18
18
  import { staticManifestSource } from "../../../core/src/artifact/staticManifestSource.ts";
19
19
  import {
@@ -75,16 +75,16 @@ export async function makeUpgrader(
75
75
  server: FakeServer,
76
76
  journal: ProvenanceJournal | null,
77
77
  opts: MakeUpgraderOpts = {},
78
- ): Promise<ReturnType<typeof createUpgrader>> {
79
- const base: Omit<CreateUpgraderOptions, "provenance"> = {
78
+ ): Promise<ReturnType<typeof createRunner>> {
79
+ const base: Omit<RunnerOptions, "provenance"> = {
80
80
  host: opts.host ?? hostReporting("2.0.0"),
81
81
  source: opts.source ?? staticManifestSource({ baseUrl: server.url }),
82
82
  policy: opts.policy ?? "auto",
83
- notificationSink: (opts.notificationSink ?? (async () => {})) as CreateUpgraderOptions["notificationSink"],
83
+ notificationSink: (opts.notificationSink ?? (async () => {})) as RunnerOptions["notificationSink"],
84
84
  stateDir: stateDir(ctx),
85
85
  ...(opts.installOwnership === undefined ? {} : { installOwnership: opts.installOwnership }),
86
86
  };
87
- return createUpgrader(journal === null ? base : { ...base, provenance: journal });
87
+ return createRunner(journal === null ? base : { ...base, provenance: journal });
88
88
  }
89
89
 
90
90
  // ---------------------------------------------------------------------------
@@ -5,7 +5,7 @@
5
5
  * an unreadable report is its own state, never "never observed"; an
6
6
  * observed pass survives a restart).
7
7
  *
8
- * These drive createUpgrader in-process: status() is the L5 read-back
8
+ * These drive createRunner in-process: status() is the L5 read-back
9
9
  * surface, and the teeth pin that it is a READ-BACK, not an invention.
10
10
  */
11
11
  import assert from "node:assert/strict";
@@ -2,9 +2,7 @@
2
2
  * Examples acceptance checks — the run bodies of the demo teeth
3
3
  * (registered in teeth/examples.ts). Each throws on violation.
4
4
  *
5
- * Each demo is the credential for its profile's support claim
6
- * (examples/README.md: "if a profile has no green example, the claim does
7
- * not exist"):
5
+ * These fixtures exercise internal mechanisms, not application integration:
8
6
  * - swap-tool: black-box upgrade loop (L0/L0.5/L1') — version command,
9
7
  * self-upgrade swaps bytes, next run reports the served version;
10
8
  * - service-daemon: process-reality L2/L3 — real spawn, startId-bound
@@ -31,12 +29,12 @@ import {
31
29
  checkProbeBindsCurrentIncarnation,
32
30
  } from "../fake-host/checks.ts";
33
31
  import type { HostDriver } from "../fake-host/inproc.ts";
34
- import { CLI_TOOL_SOURCE } from "../../../examples/swap-tool/source.ts";
35
- import { PLAIN_DAEMON_SOURCE } from "../../../examples/service-daemon/source.ts";
32
+ import { CLI_TOOL_SOURCE } from "../fixtures/cliToolSource.ts";
33
+ import { PLAIN_DAEMON_SOURCE } from "../fixtures/serviceSource.ts";
36
34
 
37
35
  const RELEASE_BASE_ENV = "K_RELEASE_BASE";
38
36
 
39
- /** The swap-tool's explicit target declaration (mirrors k.target.ts). */
37
+ /** The swap-tool fixture's explicit black-box target declaration. */
40
38
  export const CLI_TOOL_TARGET_TS = `export default { version: ["--version"], selfUpgrade: ["self", "upgrade"] };
41
39
  `;
42
40
 
@@ -71,9 +69,9 @@ async function fileSha256(p: string): Promise<string> {
71
69
  return sha256Hex(new Uint8Array(await fs.readFile(p)));
72
70
  }
73
71
 
74
- /** The swap-tool demo's @botiverse/k-carrier wiring (createUpgrader module URL). */
75
- function coreUpgraderUrl(): string {
76
- return pathToFileURL(path.join(import.meta.dirname, "../../../core/src/createUpgrader.ts")).href;
72
+ /** The swap-tool demo's @botiverse/k-carrier wiring (createRunner module URL). */
73
+ function runnerFactoryUrl(): string {
74
+ return pathToFileURL(path.join(import.meta.dirname, "../../../core/src/createRunner.ts")).href;
77
75
  }
78
76
 
79
77
  // ---------------------------------------------------------------------------
@@ -111,7 +109,7 @@ export async function checkCliToolBlackbox(
111
109
  const up = await runCommand(binPath, ["self", "upgrade"], {
112
110
  env: {
113
111
  [RELEASE_BASE_ENV]: server.url,
114
- K_CORE_UPGRADER: coreUpgraderUrl(),
112
+ K_CORE_UPGRADER: runnerFactoryUrl(),
115
113
  },
116
114
  });
117
115
  assert.equal(up.code, 0, `self upgrade must exit 0 (${up.stderr.trim()})`);
@@ -237,9 +235,8 @@ export async function checkPlainDaemonContract(
237
235
  const up = await runCommand(binPath, ["self", "upgrade"], {
238
236
  env: {
239
237
  [RELEASE_BASE_ENV]: server.url,
240
- K_CORE_UPGRADER: coreUpgraderUrl(),
238
+ K_CORE_UPGRADER: runnerFactoryUrl(),
241
239
  K_STATE_DIR: stateDir,
242
- K_HOST_SHAPE: "spawn",
243
240
  },
244
241
  timeoutMs: 30000,
245
242
  });
@@ -275,7 +272,7 @@ export async function checkManagedHostAdapter(
275
272
  const makeHost = async (): Promise<HostDriver> =>
276
273
  opts.hostOverride
277
274
  ? opts.hostOverride()
278
- : (await import("../../../examples/hosted-service/host.ts")).createManagedHost(
275
+ : (await import("../fixtures/managedHost.ts")).createManagedHost(
279
276
  path.join(ctx.sandboxDir, "host"),
280
277
  );
281
278
  // the same contract subset k-harness --adapter runs (§1.7)