@henols/vice-mcp 0.2.2 → 0.2.4

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 (84) hide show
  1. package/README.md +2 -2
  2. package/THIRD-PARTY-NOTICES.md +422 -1
  3. package/anno-bank.ts +171 -0
  4. package/anno-cli.ts +1736 -163
  5. package/anno-confidence.ts +2 -2
  6. package/anno-derive.ts +6 -6
  7. package/anno-details.ts +4 -4
  8. package/anno-enum-gen.ts +416 -30
  9. package/anno-export-asm.ts +1211 -126
  10. package/anno-graphics.ts +338 -0
  11. package/anno-hazard-report.ts +1367 -0
  12. package/anno-import.ts +495 -0
  13. package/anno-index.ts +8 -8
  14. package/anno-join.ts +480 -0
  15. package/anno-memmap-render.ts +22 -21
  16. package/anno-provenance-ledger.ts +472 -0
  17. package/anno-regbits-gen.ts +13 -13
  18. package/anno-register.ts +159 -0
  19. package/anno-store-export.ts +661 -0
  20. package/anno-store.ts +635 -124
  21. package/anno-symbols.ts +7 -7
  22. package/anno-tools.ts +1169 -16
  23. package/anno-types.ts +313 -40
  24. package/backend-detect.mts +124 -312
  25. package/build.ts +3 -1
  26. package/capture-predicate.ts +597 -0
  27. package/channel-lock.ts +349 -0
  28. package/evid-ingest.ts +217 -0
  29. package/evid-reconcile.ts +316 -0
  30. package/host-tool-client.ts +430 -0
  31. package/incident-record.ts +23 -12
  32. package/install-resources.ts +29 -13
  33. package/memmap-lookup.ts +285 -0
  34. package/package.json +27 -8
  35. package/prg-image.ts +1 -2
  36. package/repo-root.ts +87 -3
  37. package/resources/backend-detect.mjs +98 -236
  38. package/resources/broker-control.mjs +220 -54
  39. package/resources/broker-epoch.mjs +7 -8
  40. package/resources/broker-kill.mjs +36 -31
  41. package/resources/broker-launch.mjs +511 -374
  42. package/resources/broker-state.mjs +69 -24
  43. package/resources/container-guard.mjs +1 -1
  44. package/resources/ghidra-project.mjs +790 -0
  45. package/resources/host-tool.mjs +2533 -0
  46. package/resources/vice-broker.mjs +434 -290
  47. package/resources/vice-launcher.sh +127 -9
  48. package/stock-address.ts +1 -1
  49. package/stock-condition.ts +1 -1
  50. package/stock-connect.ts +9 -5
  51. package/stock-derived.ts +29 -37
  52. package/stock-diagnose.ts +200 -36
  53. package/stock-dispatch.ts +179 -77
  54. package/stock-handler.ts +1 -1
  55. package/stock-paths.ts +18 -14
  56. package/stock-petscii.ts +1 -1
  57. package/stock-protocol.ts +1 -1
  58. package/stock-recycle.ts +83 -2
  59. package/stock-reproducible-run.ts +811 -0
  60. package/stock-run-until.ts +100 -1
  61. package/stock-symbols.ts +4 -4
  62. package/stock-timing.ts +1 -1
  63. package/stop-oracle.ts +167 -0
  64. package/text-capability-probe.ts +660 -0
  65. package/text-connect.ts +157 -0
  66. package/text-protocol.ts +810 -0
  67. package/text-tools.ts +778 -0
  68. package/textmon-backtrace.ts +385 -0
  69. package/textmon-cpuhistory.ts +335 -0
  70. package/textmon-memmap.ts +494 -0
  71. package/textmon-profile.ts +458 -0
  72. package/textmon-registers.ts +748 -0
  73. package/tools-manifest.stock.json +864 -3
  74. package/vice-broker-client.ts +253 -108
  75. package/vice-errors.ts +268 -0
  76. package/vice-proxy.ts +339 -2144
  77. package/vsf-slice.ts +640 -0
  78. package/anno-d64.ts +0 -310
  79. package/capability-registry.ts +0 -390
  80. package/refresh-manifest.ts +0 -124
  81. package/tools-manifest.json +0 -1223
  82. package/vice-probe.ts +0 -278
  83. package/vice-sync.ts +0 -336
  84. package/vice.ts +0 -772
@@ -1,54 +1,46 @@
1
1
  // backend-detect.mts
2
2
  //
3
- // The ONE place that decides which VICE build a binary is, and the ONE
4
- // reader of VICE_BACKEND anywhere in this tree (D-01). Everything else that
5
- // needs to know "fork or stock" -- broker-launch.mts's buildViceArgs(),
6
- // vice-broker.mts's startup wiring, and plan 02-08's later connect handshake
7
- // -- calls resolvedBackend() below and threads its answer down, exactly like
8
- // vice.ts's mcpHost() documents for its own container-versus-host question:
9
- // a SECOND independent reader of the same signal is a bug waiting to
10
- // happen the moment one copy is updated and the other is not (mcpHost()'s
11
- // own "three inlined copies" incident is the exact regression this file
12
- // exists to keep from recurring here).
3
+ // FORKRM-01 (plan 52-06): this file used to be the ONE place that decided
4
+ // which VICE build a binary is (the fork's `-mcpserver` flag versus stock's
5
+ // `-binarymonitor`-only surface) and the ONE reader of an environment
6
+ // override naming a backend, anywhere in this tree. There is only one
7
+ // backend now, so there is nothing left to detect between: the `--help`
8
+ // probe (its own spawn, its fork/stock/unknown classifier) and the
9
+ // environment override are both deleted outright, not merely unreachable.
13
10
  //
14
- // WHAT NOT TO DO:
15
- // - Do not call resolvedBackend()/probeBackend() per acquire, per launch,
16
- // or per connect. The broker resolves the backend exactly ONCE, at
17
- // process startup (vice-broker.mts's run()), and passes the resolved
18
- // value down through every real launch call site -- see this module's
19
- // own module-level memo below, which exists as a second line of defence
20
- // against an accidental extra call, not as the PRIMARY mechanism (that
21
- // is the caller only ever invoking this once).
22
- // - Do not call this from inside broker-launch.mts's `inFlight`
23
- // single-owner launch guard. This is a possibly-blocking child-process
24
- // spawn (probeBackend()'s --help probe); anything that can block inside
25
- // that synchronous check-and-set window is the exact failure class the
26
- // 2026-08-01 triple-launch outage came from (D-03/T-02-25).
27
- // - Do not add a trial-launch fallback (launch the binary for real and
28
- // watch what happens) as a second detection mechanism. The `--help`
29
- // probe below runs entirely outside any launch-guarded critical section
30
- // BY CONSTRUCTION -- a trial launch would not. It would also depend on
31
- // RESEARCH.md's assumption A1 (stock's argument parser rejects an
32
- // unknown flag rather than ignoring it), which is UNVERIFIED against a
33
- // real stock binary; see docs/phase2-backend-probe-evidence.md.
11
+ // What SURVIVES, and why:
12
+ // - Binary-path resolution (`resolveBinPath`/`binPathFields`, WR-05) --
13
+ // `vice_ping`'s `resolvedBinaryPath` field and host-tool.mts's sibling
14
+ // binary resolution (`c1541`/`petcat`) both still need "which file did
15
+ // you actually mean", independent of any backend concept.
16
+ // - The on-disk identity/capability cache (BACK-04) -- it still records
17
+ // something worth caching once the backend verdict is gone: the version
18
+ // quad and CPU-history capability a live connect handshake (plan 02-08,
19
+ // stock-connect.ts) attaches to a resolved binary's identity
20
+ // (`resolvedPath`/`mtimeMs`/`sizeBytes`). `readCapabilityRecord()`/
21
+ // `writeCapabilityRecord()` are unchanged in shape; only the cache
22
+ // record's `backend` field is gone, since it never meant anything once
23
+ // there was nothing to distinguish.
24
+ //
25
+ // `ViceBackend` is NARROWED to a single literal (`"stock"`), not deleted:
26
+ // text-capability-probe.ts and stock-connect.ts (both out of this plan's
27
+ // scope -- plan 52-07 owns text-capability-probe.ts's own registry-shaped
28
+ // fork references) still import it as a type-only import, and deleting it
29
+ // outright would break their compile for no benefit this plan is scoped to
30
+ // deliver.
34
31
  //
35
- // ENVIRONMENT CONSTRAINT (2026-08-13, explicit user scope override): no real
36
- // stock or fork VICE binary is reachable from the environment this plan
37
- // executed in, and the user's own ruling for this plan is "we can't do
38
- // tests with deciding what vice is". Every test in backend-detect.test.ts
39
- // therefore drives this module's OVERRIDE path, its on-disk CACHE lifecycle,
40
- // and classifyHelpOutput()'s STRING-PARSING logic against fixture strings
41
- // authored in the test file -- never a real spawned binary. The `--help`
42
- // discriminator itself (does a real stock build's --help output actually
43
- // contain "-binarymonitor" and omit "-mcpserver", the way classifyHelpOutput()
44
- // below assumes) is recorded as an OPEN, not a VERIFIED, question in
45
- // docs/phase2-backend-probe-evidence.md section 2 -- that document's verdict
46
- // is deliberately left standing; nothing in this file's own tests attempts to
47
- // resolve it, and no fixture string anywhere in this tree should ever be
48
- // presented as real captured output from either build. See the follow-up
49
- // todo tracked under .planning/todos/pending/ for what a real-hardware run
50
- // must still confirm.
51
- import { spawnSync } from "node:child_process";
32
+ // WHAT NOT TO DO:
33
+ // - Do not reintroduce an environment-variable backend override, a
34
+ // --help probe, or any other backend-discrimination mechanism. There is
35
+ // one binary shape now; a second detection mechanism would be solving a
36
+ // problem that no longer exists.
37
+ // - Do not call resolvedBackend() from inside broker-launch.mts's
38
+ // `inFlight` single-owner launch guard. This still performs filesystem
39
+ // I/O (a stat call and, on a fresh or changed identity, a cache write);
40
+ // anything that can block inside that synchronous check-and-set window
41
+ // is the exact failure class the 2026-08-01 triple-launch outage came
42
+ // from (D-03/T-02-25) -- unchanged reasoning from before this plan, only
43
+ // the mechanism inside resolvedBackend() changed.
52
44
  import {
53
45
  existsSync,
54
46
  readFileSync,
@@ -60,98 +52,10 @@ import {
60
52
  } from "node:fs";
61
53
  import { join, resolve as resolvePath } from "node:path";
62
54
 
63
- /** Phase 2 (BROK-01, D-12, D-01): the two shapes this whole tree ever
64
- * launches or speaks to. Moved here (plan 02-07) from broker-launch.mts's
65
- * own plan-02-03 definition -- broker-launch.mts now imports/re-exports
66
- * this one, so the type keeps exactly one home. */
67
- export type ViceBackend = "fork" | "stock";
68
-
69
- // ---------------------------------------------------------------------------
70
- // classifyHelpOutput() -- pure string classification, no I/O at all.
71
- // ---------------------------------------------------------------------------
72
-
73
- /** Matches on the literal flag tokens D-02 names as the discriminator: the
74
- * fork's `-mcpserver` flag versus stock's `-binarymonitor`-only surface.
75
- * `"fork"` wins when BOTH tokens appear (the fork's own VICE tree is a 3.10
76
- * checkout and accepts both flags) -- checked FIRST, deliberately, so a
77
- * build that advertises both is classified by the flag that actually makes
78
- * it the fork, not merely "also has stock's flag too". `"unknown"` when
79
- * NEITHER token appears -- a real --help transcript that does not match
80
- * either shape, a probe that spawned nothing at all (empty text), or
81
- * anything else this function was never taught to recognise. Never throws;
82
- * pure function of the text it is given. */
83
- export function classifyHelpOutput(text: string): "fork" | "stock" | "unknown" {
84
- const hasFork = text.includes("-mcpserver");
85
- const hasStock = text.includes("-binarymonitor");
86
- if (hasFork) return "fork";
87
- if (hasStock) return "stock";
88
- return "unknown";
89
- }
90
-
91
- // ---------------------------------------------------------------------------
92
- // probeBackend() -- the --help probe. spawnSync only, argv array, shell:
93
- // false, never a shell string and never string interpolation of binPath
94
- // into a command line (T-02-03's mitigation). Bounded by a 5000ms timeout
95
- // with kill-on-timeout so a hostile or hung binary cannot stall broker
96
- // startup (T-02-25's second mitigation half).
97
- // ---------------------------------------------------------------------------
98
-
99
- const PROBE_TIMEOUT_MS = 5000;
100
-
101
- /** `--help` first, falling back to `-help` then `-?` ONLY when a run exits
102
- * non-zero with EMPTY combined output -- a run that exits non-zero but
103
- * still printed something (some builds write usage to stderr and exit 1) is
104
- * already usable and is not retried further. */
105
- const HELP_FLAG_CANDIDATES: readonly string[] = ["--help", "-help", "-?"];
106
-
107
- export interface SpawnHelpResult {
108
- text: string;
109
- exitedZero: boolean;
110
- }
111
-
112
- export interface ProbeBackendDeps {
113
- /** Runs ONE candidate flag against `binPath` and returns its combined
114
- * stdout+stderr text (VICE writes usage to either, depending on build)
115
- * plus whether the process exited zero. Injected so no test in this tree
116
- * ever spawns a real binary -- this environment has none, and per this
117
- * file's own environment-constraint note above, no test may execute one.
118
- * Defaults to a real, argv-array, shell:false spawnSync call bounded by
119
- * PROBE_TIMEOUT_MS with kill-on-timeout; never throws -- a spawn failure
120
- * (ENOENT, EACCES, a real timeout) collapses to `{ text: "", exitedZero:
121
- * false }`, which classifyHelpOutput() reads as "unknown" like any other
122
- * unrecognised output. */
123
- spawnHelp?: (binPath: string, flag: string) => SpawnHelpResult;
124
- }
125
-
126
- function defaultSpawnHelp(binPath: string, flag: string): SpawnHelpResult {
127
- try {
128
- const result = spawnSync(binPath, [flag], {
129
- encoding: "utf8",
130
- timeout: PROBE_TIMEOUT_MS,
131
- killSignal: "SIGKILL",
132
- });
133
- const text = `${result.stdout ?? ""}${result.stderr ?? ""}`;
134
- return { text, exitedZero: result.status === 0 };
135
- } catch {
136
- return { text: "", exitedZero: false };
137
- }
138
- }
139
-
140
- /** Runs the fallback ladder above against `binPath` and classifies whatever
141
- * text the LAST attempted flag produced. Never throws -- every failure mode
142
- * (spawn error, timeout, empty output, an exit code the caller does not
143
- * recognise) flows through to classifyHelpOutput() as ordinary text, which
144
- * itself never throws either. */
145
- export function probeBackend(binPath: string, deps: ProbeBackendDeps = {}): "fork" | "stock" | "unknown" {
146
- const spawnHelp = deps.spawnHelp ?? defaultSpawnHelp;
147
- let text = "";
148
- for (const flag of HELP_FLAG_CANDIDATES) {
149
- const outcome = spawnHelp(binPath, flag);
150
- text = outcome.text;
151
- if (outcome.exitedZero || text.trim() !== "") break;
152
- }
153
- return classifyHelpOutput(text);
154
- }
55
+ /** The one shape this tree ever launches or speaks to. FORKRM-01 narrowed
56
+ * this from a two-member union ("fork" | "stock") to this single literal --
57
+ * narrowed rather than deleted; see this file's own header for why. */
58
+ export type ViceBackend = "stock";
155
59
 
156
60
  // ---------------------------------------------------------------------------
157
61
  // The on-disk cache -- `join(supervisorDir, "backend.json")`. `supervisorDir`
@@ -177,11 +81,11 @@ export function probeBackend(binPath: string, deps: ProbeBackendDeps = {}): "for
177
81
  //
178
82
  // When `supervisorDir` is omitted entirely, every cache read/write below is a
179
83
  // no-op (a miss on read, silently skipped on write) -- this module still
180
- // answers correctly (probe-and-memoise-in-process only), it just never
181
- // persists an answer across process restarts. This is a graceful
182
- // degradation, not an error: a caller that has not yet resolved a
183
- // supervisor directory (or genuinely has none) gets a working, if
184
- // unpersisted, answer rather than a thrown exception or a guessed path.
84
+ // answers correctly (identity resolution only), it just never persists
85
+ // anything across process restarts. This is a graceful degradation, not an
86
+ // error: a caller that has not yet resolved a supervisor directory (or
87
+ // genuinely has none) gets a working, if unpersisted, answer rather than a
88
+ // thrown exception or a guessed path.
185
89
  // ---------------------------------------------------------------------------
186
90
 
187
91
  export interface BackendCacheRecord {
@@ -189,11 +93,10 @@ export interface BackendCacheRecord {
189
93
  resolvedPath: string;
190
94
  mtimeMs: number;
191
95
  sizeBytes: number;
192
- backend: ViceBackend;
193
96
  probedAt: string;
194
97
  /** BACK-04: filled in later, by a connect handshake (plan 02-08), never by
195
- * this file's own probe -- the `--help` probe cannot observe a version
196
- * quad; only a live VICE_INFO reply over an established connection can. */
98
+ * this file's own resolution -- resolvedBackend() only ever initialises
99
+ * the identity fields below; it has no way to observe a version quad. */
197
100
  versionQuad?: string;
198
101
  cpuHistoryAvailable?: boolean;
199
102
  /** CR-01 (07-REVIEW.md re-review): the CLIENT-side schema version that
@@ -253,8 +156,7 @@ function readCacheRecord(supervisorDir: string): BackendCacheRecord | null {
253
156
  if (
254
157
  typeof parsed.resolvedPath !== "string" ||
255
158
  typeof parsed.mtimeMs !== "number" ||
256
- typeof parsed.sizeBytes !== "number" ||
257
- (parsed.backend !== "fork" && parsed.backend !== "stock")
159
+ typeof parsed.sizeBytes !== "number"
258
160
  ) {
259
161
  return null;
260
162
  }
@@ -263,7 +165,6 @@ function readCacheRecord(supervisorDir: string): BackendCacheRecord | null {
263
165
  resolvedPath: parsed.resolvedPath,
264
166
  mtimeMs: parsed.mtimeMs,
265
167
  sizeBytes: parsed.sizeBytes,
266
- backend: parsed.backend,
267
168
  probedAt: typeof parsed.probedAt === "string" ? parsed.probedAt : "",
268
169
  };
269
170
  if (typeof parsed.versionQuad === "string") record.versionQuad = parsed.versionQuad;
@@ -273,10 +174,10 @@ function readCacheRecord(supervisorDir: string): BackendCacheRecord | null {
273
174
  }
274
175
 
275
176
  /** Tmp-sibling -> chmod 0600 -> content -> rename, the SAME atomic-write
276
- * discipline refresh-manifest.ts's writeManifestAtomic() and vice-broker.mts's
277
- * writeBrokerRecordFile() both already use -- a crash mid-write can only ever
278
- * leave a stray tmp sibling behind, never a truncated or empty file at the
279
- * real cache path that a later read would wrongly accept. */
177
+ * discipline vice-broker.mts's writeBrokerRecordFile() already uses -- a
178
+ * crash mid-write can only ever leave a stray tmp sibling behind, never a
179
+ * truncated or empty file at the real cache path that a later read would
180
+ * wrongly accept. */
280
181
  function writeCacheRecordAtomic(supervisorDir: string, record: BackendCacheRecord): void {
281
182
  mkdirSync(supervisorDir, { recursive: true });
282
183
  const finalPath = cachePathFor(supervisorDir);
@@ -289,13 +190,10 @@ function writeCacheRecordAtomic(supervisorDir: string, record: BackendCacheRecor
289
190
 
290
191
  // ---------------------------------------------------------------------------
291
192
  // Binary identity -- resolve a possibly-bare command name (e.g. "x64sc") to
292
- // an absolute path (for cache KEYING and stat only -- probeBackend() above
293
- // still spawns the ORIGINAL, unresolved binPath/viceBin string, letting the
294
- // OS's own PATH search resolve it exactly like a real invocation would), and
295
- // stat it for mtimeMs/sizeBytes -- the D-03 planner decision's cache key
296
- // half: `{ resolvedPath, mtimeMs, sizeBytes }`, which catches a binary
297
- // replaced in place (an `apt upgrade`, a manual `cp`) without hashing a
298
- // multi-megabyte file on every broker start.
193
+ // an absolute path (for cache KEYING and stat only), and stat it for
194
+ // mtimeMs/sizeBytes -- the cache key: `{ resolvedPath, mtimeMs, sizeBytes }`,
195
+ // which catches a binary replaced in place (an `apt upgrade`, a manual `cp`)
196
+ // without hashing a multi-megabyte file on every broker start.
299
197
  // ---------------------------------------------------------------------------
300
198
 
301
199
  export interface BinaryIdentity {
@@ -326,34 +224,21 @@ function defaultStat(resolvedPath: string): BinaryIdentity | null {
326
224
  }
327
225
  }
328
226
 
329
- function defaultLog(line: string): void {
330
- process.stderr.write(`${line}\n`);
331
- }
332
-
333
227
  // ---------------------------------------------------------------------------
334
228
  // resolvedBackend() -- the public entry point.
335
229
  // ---------------------------------------------------------------------------
336
230
 
337
- export type ResolvedBackendSource = "override" | "cache" | "probe" | "indeterminate";
338
-
339
231
  export interface ResolvedBackendResult {
340
232
  backend: ViceBackend;
341
- source: ResolvedBackendSource;
342
233
  /**
343
234
  * WR-05: the ABSOLUTE path this binary resolved to when it could be resolved,
344
- * falling back to the configured name otherwise. It used to be `viceBin`
345
- * unconditionally -- the raw `VICE_BIN`/`"x64sc"` string -- even though this
346
- * function already computes `resolvedPath` internally for cache keying. Two
347
- * consumers called that a resolved path in their own doc comments
348
- * (StockDispatchDeps.resolvedBinaryPath, and BACK-03's `vice_ping` answer),
349
- * so `vice_ping` on stock reported `"x64sc"` -- a name that, inside a
350
- * container, resolves to nothing at all.
235
+ * falling back to the configured name otherwise. Two consumers read this as
236
+ * a resolved path in their own doc comments (StockDispatchDeps.resolvedBinaryPath,
237
+ * and BACK-03's `vice_ping` answer), so a bare configured name (e.g. `"x64sc"`)
238
+ * would report a name that, inside a container, resolves to nothing at all.
351
239
  *
352
- * Deliberately NOT what probeBackend() spawns: that still receives the
353
- * ORIGINAL, unresolved string, so the OS's own PATH search happens exactly
354
- * as it would for a real invocation. This field is what a HUMAN or an agent
355
- * is shown, and "which file did you actually mean" is the question it has to
356
- * answer.
240
+ * This field is what a HUMAN or an agent is shown, and "which file did you
241
+ * actually mean" is the question it has to answer.
357
242
  */
358
243
  binPath: string;
359
244
  /** WR-05: `true` only when `binPath` above is a real resolved absolute path;
@@ -362,119 +247,71 @@ export interface ResolvedBackendResult {
362
247
  * the two it has, instead of a reader having to guess from whether the string
363
248
  * happens to contain a slash. */
364
249
  binPathResolved: boolean;
365
- note?: string;
366
250
  }
367
251
 
368
252
  export interface ResolvedBackendDeps {
369
253
  env?: NodeJS.ProcessEnv;
370
254
  /** Which binary to detect against -- defaults to VICE_BIN or "x64sc",
371
255
  * matching broker-launch.mts's own spawnAndRecordInstance() default
372
- * exactly (one broker, one binary, one verdict -- D-04). */
256
+ * exactly (one broker, one binary, one verdict). */
373
257
  viceBin?: string;
374
258
  /** See this module's own header comment on the cache section above --
375
259
  * NEVER defaulted here. Omitted entirely disables the on-disk cache
376
- * (probe-and-memoise-in-process only, never persisted). */
260
+ * (identity resolution only, never persisted). */
377
261
  supervisorDir?: string;
378
262
  resolveBinPath?: (bin: string, env: NodeJS.ProcessEnv) => string | null;
379
263
  stat?: (resolvedPath: string) => BinaryIdentity | null;
380
- probe?: (binPath: string) => "fork" | "stock" | "unknown";
381
264
  now?: () => number;
382
- log?: (line: string) => void;
383
265
  }
384
266
 
385
- // Memoised answer for the probe/cache path ONLY -- the override path
386
- // (VICE_BACKEND set) is always answered fresh, on every call, straight from
387
- // the environment, and never touches this memo (an explicit override can
388
- // legitimately differ from call to call within, e.g., a test process driving
389
- // many scenarios; the detected-backend answer for a fixed binary cannot).
390
- // This is what makes resolvedBackend() answer "once per long-running
391
- // process" for the case that actually spawns something, while never
392
- // requiring a caller to somehow signal "this is a fresh scenario" the way
393
- // container-guard.mts's isInsideContainer() asks callers to pass explicit
394
- // deps to bypass ITS OWN memo -- here, the override/no-override distinction
395
- // already IS that signal.
267
+ // Memoised answer -- a long-running process (the real broker) resolves once
268
+ // per process lifetime; a test suite driving many scenarios calls
269
+ // resetResolvedBackendForTests() between them (see that function's own
270
+ // comment).
396
271
  let memoisedResult: ResolvedBackendResult | null = null;
397
272
 
398
- // D-06: gates the "detected backend X for binary Y" stderr note so a
399
- // long-running broker (or a test suite driving resolvedBackend() many times)
400
- // emits it at most once per process -- repo-root.ts's warnedEnvOutsideFrom/
401
- // warnedNoMarkerFound pattern, reused here verbatim.
402
- let warnedBackendUnset = false;
403
-
404
- function emitDetectedNote(result: ResolvedBackendResult, viceBin: string, log: (line: string) => void): void {
405
- if (warnedBackendUnset) return;
406
- warnedBackendUnset = true;
407
- log(
408
- `vice-broker: detected backend "${result.backend}" for ${viceBin} (source: ${result.source}) -- ` +
409
- `set VICE_BACKEND=stock or VICE_BACKEND=fork to override this detection explicitly`,
410
- );
411
- }
412
-
413
- /** Test-only escape hatch: clears the in-process memo and the D-06
414
- * one-time-note gate. Never called by any real production code path --
415
- * vice-broker.mts calls resolvedBackend() exactly once per real process
416
- * lifetime and has no reason to ever reset it; this exists solely so
417
- * backend-detect.test.ts can drive many distinct scenarios (cache hit, cache
418
- * miss, indeterminate, ...) in one shared test process without one
419
- * scenario's memoised answer contaminating the next -- mirroring
420
- * broker-launch.test.ts's own discipline of restoring module-level state
421
- * between test cases, made explicit here rather than left to careful test
422
- * ordering, since this module's memo (unlike buildViceArgs()'s one-time
423
- * note) has no natural "always widens the same way" ordering to exploit. */
273
+ /** Test-only escape hatch: clears the in-process memo. Never called by any
274
+ * real production code path -- vice-broker.mts calls resolvedBackend()
275
+ * exactly once per real process lifetime and has no reason to ever reset it;
276
+ * this exists solely so backend-detect.test.ts can drive many distinct
277
+ * scenarios (fresh identity, matching cached identity, changed identity, ...)
278
+ * in one shared test process without one scenario's memoised answer
279
+ * contaminating the next. */
424
280
  export function resetResolvedBackendForTests(): void {
425
281
  memoisedResult = null;
426
- warnedBackendUnset = false;
427
282
  }
428
283
 
429
- /** Honours VICE_BACKEND FIRST, returning immediately without spawning
430
- * anything when it names `stock` or `fork` (BACK-01: one optional config
431
- * value switches backends, no code edit). Otherwise consults the on-disk
432
- * cache (when `supervisorDir` is given and the binary's current
433
- * `{ resolvedPath, mtimeMs, sizeBytes }` all match the stored record); on a
434
- * miss, probes via probeBackend() and writes the cache. Memoises the
435
- * probe/cache answer in a module-level variable so a long-running process
436
- * resolves once (see the memo's own comment above for what "once" means
437
- * here). Never throws: a probe that classifies "unknown" (including a
438
- * spawn failure or a timeout, both of which probeBackend() already reduces
439
- * to "unknown") returns a defined `{ backend: "fork", source:
440
- * "indeterminate", ... }` outcome instead -- "fork" because that is the
441
- * pre-Phase-2 behaviour every existing install already has, so an
442
- * undetectable binary degrades to what already worked rather than to
443
- * nothing. */
444
- /** WR-05: the ONE place `binPath`/`binPathResolved` are derived, so the four
445
- * return paths below cannot disagree about what "the binary" means. A resolved
446
- * absolute path when there is one; the configured name, flagged as unresolved,
447
- * when there is not. */
284
+ /** WR-05: the ONE place `binPath`/`binPathResolved` are derived, so a
285
+ * resolved absolute path when there is one, or the configured name flagged
286
+ * as unresolved when there is not, is computed identically everywhere this
287
+ * file returns it. */
448
288
  function binPathFields(resolvedPath: string | null, viceBin: string): { binPath: string; binPathResolved: boolean } {
449
289
  return resolvedPath !== null ? { binPath: resolvedPath, binPathResolved: true } : { binPath: viceBin, binPathResolved: false };
450
290
  }
451
291
 
292
+ /** Resolves the emulator binary's identity and (re)initialises the on-disk
293
+ * identity/capability cache record for it when a `supervisorDir` is given.
294
+ * Memoises the answer in a module-level variable so a long-running process
295
+ * resolves once (see the memo's own comment above for what "once" means
296
+ * here). Never throws.
297
+ *
298
+ * FORKRM-01: there is nothing left to detect -- `backend` is always
299
+ * `"stock"`. What this function still does is identity resolution (WR-05's
300
+ * `binPath`/`binPathResolved`) and cache bookkeeping for BACK-04's capability
301
+ * record: when the resolved binary's identity (`resolvedPath`/`mtimeMs`/
302
+ * `sizeBytes`) does not match whatever is already on file -- no record at
303
+ * all, or a record describing a DIFFERENT binary (replaced in place, or a
304
+ * fresh install) -- a fresh identity record is written, with no stale
305
+ * capability fields carried over from a different binary's answer. A
306
+ * matching identity is left untouched, so a capability answer already
307
+ * recorded for THIS binary survives. */
452
308
  export function resolvedBackend(deps: ResolvedBackendDeps = {}): ResolvedBackendResult {
309
+ if (memoisedResult !== null) return memoisedResult;
310
+
453
311
  const env = deps.env ?? process.env;
454
312
  const viceBin = deps.viceBin ?? env.VICE_BIN ?? "x64sc";
455
- // A direct read of the real environment on the right of this ternary
456
- // (rather than the generic `env` local above) is deliberate: this file is
457
- // grep-gated, tree-wide, as the ONE place that ever names this variable
458
- // directly against the real environment -- `deps.env` (the test-injection
459
- // seam) still takes precedence when supplied, exactly like every other
460
- // field on this options object.
461
- const override = deps.env ? deps.env.VICE_BACKEND : process.env.VICE_BACKEND;
462
313
  const resolveBinPath = deps.resolveBinPath ?? defaultResolveBinPath;
463
-
464
- if (override === "stock" || override === "fork") {
465
- // WR-05: an explicit backend override still resolves the PATH, so
466
- // `vice_ping` reports a real file rather than the bare name. This is a
467
- // filesystem lookup only -- existsSync per PATH entry -- and NEVER a spawn,
468
- // so the override path keeps its "answered fresh, straight from the
469
- // environment, spawns nothing" property.
470
- return { backend: override, source: "override", ...binPathFields(resolveBinPath(viceBin, env), viceBin) };
471
- }
472
-
473
- if (memoisedResult !== null) return memoisedResult;
474
-
475
- const log = deps.log ?? defaultLog;
476
314
  const stat = deps.stat ?? defaultStat;
477
- const probe = deps.probe ?? ((bin: string) => probeBackend(bin));
478
315
  const now = deps.now ?? ((): number => Date.now());
479
316
 
480
317
  const resolvedPath = resolveBinPath(viceBin, env);
@@ -482,56 +319,34 @@ export function resolvedBackend(deps: ResolvedBackendDeps = {}): ResolvedBackend
482
319
  const cacheEligible = resolvedPath !== null && identity !== null && typeof deps.supervisorDir === "string";
483
320
 
484
321
  if (cacheEligible) {
485
- const cached = readCacheRecord(deps.supervisorDir as string);
486
- if (
487
- cached &&
488
- cached.resolvedPath === resolvedPath &&
489
- cached.mtimeMs === identity!.mtimeMs &&
490
- cached.sizeBytes === identity!.sizeBytes
491
- ) {
492
- const result: ResolvedBackendResult = { backend: cached.backend, source: "cache", ...binPathFields(resolvedPath, viceBin) };
493
- memoisedResult = result;
494
- emitDetectedNote(result, viceBin, log);
495
- return result;
322
+ const existing = readCacheRecord(deps.supervisorDir as string);
323
+ const identityMatches =
324
+ existing !== null &&
325
+ existing.resolvedPath === resolvedPath &&
326
+ existing.mtimeMs === identity!.mtimeMs &&
327
+ existing.sizeBytes === identity!.sizeBytes;
328
+ if (!identityMatches) {
329
+ writeCacheRecordAtomic(deps.supervisorDir as string, {
330
+ version: 1,
331
+ resolvedPath: resolvedPath as string,
332
+ mtimeMs: identity!.mtimeMs,
333
+ sizeBytes: identity!.sizeBytes,
334
+ probedAt: new Date(now()).toISOString(),
335
+ });
496
336
  }
497
337
  }
498
338
 
499
- const verdict = probe(viceBin);
500
-
501
- if (verdict === "unknown") {
502
- const note =
503
- `vice-broker: could not determine whether ${viceBin} is the stock or fork VICE build -- ` +
504
- `its --help output matched neither the -mcpserver nor the -binarymonitor discriminator. ` +
505
- `Set VICE_BACKEND=stock or VICE_BACKEND=fork explicitly.`;
506
- log(note);
507
- const result: ResolvedBackendResult = { backend: "fork", source: "indeterminate", ...binPathFields(resolvedPath, viceBin), note };
508
- memoisedResult = result;
509
- return result;
510
- }
511
-
512
- if (cacheEligible) {
513
- writeCacheRecordAtomic(deps.supervisorDir as string, {
514
- version: 1,
515
- resolvedPath: resolvedPath as string,
516
- mtimeMs: identity!.mtimeMs,
517
- sizeBytes: identity!.sizeBytes,
518
- backend: verdict,
519
- probedAt: new Date(now()).toISOString(),
520
- });
521
- }
522
-
523
- const result: ResolvedBackendResult = { backend: verdict, source: "probe", ...binPathFields(resolvedPath, viceBin) };
339
+ const result: ResolvedBackendResult = { backend: "stock", ...binPathFields(resolvedPath, viceBin) };
524
340
  memoisedResult = result;
525
- emitDetectedNote(result, viceBin, log);
526
341
  return result;
527
342
  }
528
343
 
529
344
  // ---------------------------------------------------------------------------
530
345
  // BACK-04: the capability record. Same cache file, same identity match --
531
346
  // filled in by plan 02-08's connect handshake, once per binary, never once
532
- // per connect. This file's own probe never populates these fields (a
533
- // `--help` transcript cannot carry a version quad); it only ever reads or
534
- // updates them against a backend verdict this file already wrote.
347
+ // per connect. This file's own resolution never populates these fields; it
348
+ // only ever reads or updates them against an identity record this file
349
+ // already wrote.
535
350
  // ---------------------------------------------------------------------------
536
351
 
537
352
  export interface CapabilityRecordResult {
@@ -601,15 +416,13 @@ export function readCapabilityRecord(binPath: string, deps: CapabilityDeps = {})
601
416
  return result;
602
417
  }
603
418
 
604
- /** Attaches `{ versionQuad, cpuHistoryAvailable }` to the EXISTING backend
605
- * verdict already on record for `binPath`'s resolved identity -- a no-op,
606
- * never a throw, when there is no such matching record yet (no supervisorDir
419
+ /** Attaches `{ versionQuad, cpuHistoryAvailable }` to the EXISTING identity
420
+ * record already on file for `binPath`'s resolved identity -- a no-op, never
421
+ * a throw, when there is no such matching record yet (no supervisorDir
607
422
  * given, the binary cannot be resolved or stat'd, or the cache names a
608
- * different binary or has no verdict at all). This function never invents a
609
- * backend verdict of its own: it can only EXTEND a record resolvedBackend()
610
- * already wrote, since a `--help` probe has no way to observe a version
611
- * quad and this function must not silently fabricate the field it did not
612
- * observe either. */
423
+ * different binary or has no record at all). This function never invents an
424
+ * identity record of its own: it can only EXTEND a record resolvedBackend()
425
+ * already wrote. */
613
426
  export function writeCapabilityRecord(
614
427
  binPath: string,
615
428
  capability: { versionQuad: string; cpuHistoryAvailable: boolean },
@@ -633,7 +446,6 @@ export function writeCapabilityRecord(
633
446
  resolvedPath,
634
447
  mtimeMs: identity.mtimeMs,
635
448
  sizeBytes: identity.sizeBytes,
636
- backend: existing.backend,
637
449
  probedAt: existing.probedAt,
638
450
  versionQuad: capability.versionQuad,
639
451
  cpuHistoryAvailable: capability.cpuHistoryAvailable,
package/build.ts CHANGED
@@ -48,6 +48,8 @@ export const HOST_BOUND_ARTIFACTS: string[] = [
48
48
  "broker-epoch.mjs",
49
49
  "broker-control.mjs",
50
50
  "backend-detect.mjs",
51
+ "host-tool.mjs",
52
+ "ghidra-project.mjs",
51
53
  ];
52
54
 
53
55
  /** The generated-file banner (01.6-RESEARCH.md §F), a function of the
@@ -61,7 +63,7 @@ export function GENERATED_BANNER(relSourcePath: string): string {
61
63
  `// Compiled by \`tsc\` from ${relSourcePath}. Edit the TypeScript source and rebuild;\n` +
62
64
  "// changes made directly to this file are silently overwritten by the next build, and are never\n" +
63
65
  "// deployed to the host on their own -- install-resources.mjs copies THIS file's on-disk contents\n" +
64
- "// verbatim to tools/, so an edit made only here reaches the host but is lost on the very next\n" +
66
+ "// verbatim to .c64-re-tools/bin/, so an edit made only here reaches the host but is lost on the very next\n" +
65
67
  "// rebuild.\n"
66
68
  );
67
69
  }