@henols/vice-mcp 0.2.1 → 0.2.2

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 (57) hide show
  1. package/README.md +2 -1
  2. package/THIRD-PARTY-NOTICES.md +1 -24
  3. package/{r2000-acme-ident.ts → anno-acme-ident.ts} +13 -13
  4. package/anno-cli.ts +1465 -0
  5. package/{r2000-confidence.ts → anno-confidence.ts} +22 -22
  6. package/anno-coverage.ts +2465 -0
  7. package/{r2000-d64.ts → anno-d64.ts} +5 -5
  8. package/anno-derive.ts +590 -0
  9. package/anno-details.ts +169 -0
  10. package/anno-enum-gen.ts +533 -0
  11. package/anno-export-asm.ts +1310 -0
  12. package/anno-index.ts +150 -0
  13. package/{r2000-memmap-render.ts → anno-memmap-render.ts} +236 -95
  14. package/{r2000-regbits-gen.ts → anno-regbits-gen.ts} +20 -15
  15. package/{r2000-regbits.json → anno-regbits.json} +2 -2
  16. package/anno-register.ts +240 -0
  17. package/anno-store.ts +3486 -0
  18. package/anno-symbols.ts +266 -0
  19. package/anno-tools.ts +2111 -0
  20. package/anno-types.ts +1636 -0
  21. package/block-class.ts +201 -0
  22. package/build.ts +1 -1
  23. package/capability-registry.ts +3 -1
  24. package/disasm-decoder.ts +14 -14
  25. package/disasm-opcodes.ts +4 -4
  26. package/disasm-renderer.ts +2 -2
  27. package/hostpath.ts +1 -1
  28. package/install-resources.ts +1 -1
  29. package/package.json +23 -17
  30. package/prg-image.ts +119 -0
  31. package/repo-root.ts +20 -5
  32. package/resources/broker-launch.mjs +8 -4
  33. package/resources/vice-launcher.sh +3 -3
  34. package/stock-address.ts +5 -5
  35. package/stock-cia.ts +2 -2
  36. package/stock-condition.ts +7 -7
  37. package/stock-connect.ts +1 -1
  38. package/stock-dispatch.ts +35 -5
  39. package/stock-execution.ts +5 -3
  40. package/stock-input.ts +9 -9
  41. package/stock-machine.ts +17 -6
  42. package/stock-protocol.ts +16 -11
  43. package/stock-registers.ts +54 -29
  44. package/stock-sprites.ts +3 -3
  45. package/stock-symbols.ts +9 -9
  46. package/stock-timing.ts +1 -1
  47. package/stock-vicii.ts +1 -1
  48. package/version.ts +1 -1
  49. package/vice-proxy.ts +68 -46
  50. package/r2000-cli.ts +0 -1103
  51. package/r2000-enum-gen.ts +0 -574
  52. package/r2000-launch.ts +0 -357
  53. package/r2000-mcp-client.ts +0 -596
  54. package/r2000-project.ts +0 -190
  55. package/r2000-symbols.ts +0 -388
  56. package/r2000-tools.ts +0 -914
  57. package/r2000-verify.ts +0 -184
package/r2000-launch.ts DELETED
@@ -1,357 +0,0 @@
1
- #!/usr/bin/env node
2
- // This module's original header (through 11.1-03) claimed to be the only
3
- // place in this repo that shells out to regenerator2000. That claim was
4
- // WRONG and is corrected below (INT-02/D-11.1-05) -- noted here, not
5
- // deleted outright, because this line is the sentence the correction below
6
- // is against.
7
- //
8
- // Why this seam exists at all (D-06): this directory is the only place a
9
- // guard test actually runs in CI -- `hostpath-consumers.test.ts`'s
10
- // closed-consumer-set machinery lives here, and CI's `npm test` never
11
- // reaches skill-side `*.test.mjs` scripts. A guard test living beside a
12
- // skill script would be green-by-absence. Phase 11's `r2000_*` MCP surface
13
- // is also planned to land in this same directory, so this module is the
14
- // natural first tenant rather than a throwaway.
15
- //
16
- // CORRECTED CLAIM (INT-02/D-11.1-05): this is NOT the only file that spawns
17
- // regenerator2000 -- `r2000-mcp-client.ts` is a second, necessary spawn
18
- // site, and its own header already said so correctly ("both spawn call
19
- // sites in this repo") while this file's original claim above said the
20
- // opposite. What this file IS actually the one authoritative place for:
21
- // - the `--vice` guard (`FORBIDDEN_R2000_FLAGS`, `assertNoViceFlag()`,
22
- // `viceFlagRefusalMessage()`) that every spawn site in this repo must
23
- // call before spawning;
24
- // - every FIXED argv builder (`buildExportAsmArgs()`, `buildVerifyArgs()`,
25
- // `buildMcpServerStdioArgs()`, `buildExportLblArgs()`,
26
- // `buildImportLblArgs()`) -- a future verb gets a new fixed builder
27
- // here, never a bespoke ad hoc argv assembled at a call site;
28
- // - the ONE **synchronous**, blocking `spawnSync` of regenerator2000
29
- // (`runR2000()`), used by every CLI verb (`r2000-cli.ts`) and by
30
- // `r2000-verify.ts`/`r2000-symbols.ts`.
31
- // `r2000-mcp-client.ts`'s `withR2000Session()` is the ONE **asynchronous**
32
- // spawn site (`spawn()`, not `spawnSync()`) -- it exists because a
33
- // long-lived MCP-over-stdio child session cannot be a blocking call, which
34
- // `runR2000()` deliberately is. Both sites are safe for the same reason:
35
- // EVERY spawn call site in this repo calls `assertNoViceFlag(argv)` before
36
- // spawning, and that is no longer a prose promise -- `r2000-spawn-seam.test.ts`
37
- // derives the full production-module set, finds every regenerator2000
38
- // spawn call site in it, and FAILS if any of them spawns without guarding
39
- // first, or if a third, unguarded site ever appears. `R2000-01`'s
40
- // guarantee is therefore checked, not merely stated.
41
- //
42
- // What NOT to do, named concretely (D-07/D-08):
43
- // - Never add a caller-supplied argv pass-through parameter (no rest
44
- // parameter, no field for extra command-line arguments, no field that
45
- // forwards arbitrary caller-supplied tokens on any builder's options
46
- // object). Every verb's argv shape is fixed by this file, not by its
47
- // caller.
48
- // - Never strip `--vice` silently if a caller somehow supplies it. Stock
49
- // VICE's binary monitor services *exactly one client*
50
- // (CLAUDE.md's "Concurrency" constraint) -- a regenerator2000 launched
51
- // with `--vice <host:port>` becomes a second, unserviced client against
52
- // an instance the broker already owns, indistinguishable from a wedge
53
- // with no diagnostic. Silently removing the flag would hide exactly the
54
- // bug class this guard exists to catch, so the guard throws a named
55
- // error instead.
56
- // - Never import `hostpath.ts` or `containerpath.ts` here. regenerator2000
57
- // runs container-side (D-R4, same side as the MCP proxy), so no path
58
- // translation ever applies to any argument passed to it -- translating
59
- // one would be the mirror image of the DERIV-07 screenshot-path trap,
60
- // where a client-side-derived path was wrongly translated a second
61
- // time. This absence is asserted structurally by
62
- // `hostpath-consumers.test.ts` (D-08), not merely stated here.
63
- import { spawnSync } from "node:child_process";
64
-
65
- /** Overridable binary name, mirroring `disasm-roundtrip.test.ts`'s `ACME_BIN`
66
- * convention -- lets tests point at a name guaranteed not to exist on PATH
67
- * without needing regenerator2000 installed at all. */
68
- export const R2000_BIN: string = process.env.R2000_BIN ?? "regenerator2000";
69
-
70
- /** The permanent deny list for regenerator2000 argv, declared once as a
71
- * named constant -- the same shape `vice.ts` uses for `DENY_LIST`. Today
72
- * this holds exactly one entry; if a second hazardous flag is ever
73
- * discovered, it joins this array rather than spawning a parallel check.
74
- * `assertNoViceFlag()` below actually iterates this array (WR-01) -- it is
75
- * the single source of truth for the scan, not merely documentation of
76
- * intent, so adding an entry here is sufficient to enforce it. */
77
- export const FORBIDDEN_R2000_FLAGS: readonly string[] = ["--vice"];
78
-
79
- export interface R2000ViceFlagErrorOptions {
80
- argv: readonly string[];
81
- }
82
-
83
- /** Thrown by `assertNoViceFlag()`/`runR2000()` when `--vice` (or its
84
- * `--vice=<value>` single-token form) is found anywhere in a built argv. */
85
- export class R2000ViceFlagError extends Error {
86
- argv: readonly string[];
87
-
88
- constructor(message: string, { argv }: R2000ViceFlagErrorOptions) {
89
- super(message);
90
- this.name = "R2000ViceFlagError";
91
- this.argv = argv;
92
- }
93
- }
94
-
95
- /**
96
- * Renders the pinned refusal text for a `--vice`-bearing argv, mirroring
97
- * `vice.ts`'s `denyListRefusalMessage()`. States plainly that the flag is
98
- * permanently forbidden, why (stock VICE's binary monitor serves exactly
99
- * one client and the broker owns that socket), what a second client would
100
- * look like (indistinguishable from a wedge), and that the flag was
101
- * refused rather than silently removed.
102
- */
103
- export function viceFlagRefusalMessage(argv: readonly string[]): string {
104
- return (
105
- `--vice is permanently forbidden on any regenerator2000 launch -- stock VICE's binary monitor ` +
106
- `serves exactly one client and the broker already owns that socket. A second client (this ` +
107
- `regenerator2000 process connecting via --vice) would be indistinguishable from a wedge, with no ` +
108
- `diagnostic. The flag was refused, not removed -- caller-supplied argv: [${argv.join(", ")}]`
109
- );
110
- }
111
-
112
- /**
113
- * Scans a finished argv array against every entry of `FORBIDDEN_R2000_FLAGS`
114
- * and throws `R2000ViceFlagError` if any is found (WR-01: the scan reads
115
- * the array itself, so a future addition to the deny list is enforced by
116
- * construction rather than requiring a parallel hand-edit here). Exact-token
117
- * comparison only -- `arg === flag` or the single-token `flag=<value>` form
118
- * via `startsWith`. Deliberately does NOT join argv into a string and
119
- * substring-match: a filename containing the characters `--vice` (e.g.
120
- * `/tmp/my--vice-notes.a`) must never false-positive.
121
- */
122
- export function assertNoViceFlag(argv: readonly string[]): void {
123
- for (const arg of argv) {
124
- for (const flag of FORBIDDEN_R2000_FLAGS) {
125
- if (arg === flag || arg.startsWith(`${flag}=`)) {
126
- throw new R2000ViceFlagError(viceFlagRefusalMessage(argv), { argv });
127
- }
128
- }
129
- }
130
- }
131
-
132
- export interface BuildExportAsmArgsOptions {
133
- projectPath: string;
134
- outPath: string;
135
- }
136
-
137
- /** Fixed argv builder for the "export ACME assembly source" verb. No rest
138
- * parameter, no extra-args field -- the shape is exactly these four tokens
139
- * plus the two caller-supplied paths. */
140
- export function buildExportAsmArgs({ projectPath, outPath }: BuildExportAsmArgsOptions): string[] {
141
- return ["--headless", "--export_asm", outPath, "--assembler", "acme", projectPath];
142
- }
143
-
144
- export interface BuildVerifyArgsOptions {
145
- projectPath: string;
146
- }
147
-
148
- /** Fixed argv builder for the "verify export roundtrip" verb. */
149
- export function buildVerifyArgs({ projectPath }: BuildVerifyArgsOptions): string[] {
150
- return ["--verify", "--assembler", "acme", projectPath];
151
- }
152
-
153
- export interface BuildMcpServerStdioArgsOptions {
154
- projectPath: string;
155
- }
156
-
157
- /** Fixed argv builder for the "run as an MCP server over stdio" verb
158
- * (D-16/D-17). `--mcp-server-stdio` takes no value of its own -- the
159
- * project path is the positional `[FILE]` argument documented by
160
- * `regenerator2000 --help`, confirmed at execution time on this host
161
- * (0.9.20): `Usage: regenerator2000 [OPTIONS] [FILE]`. */
162
- export function buildMcpServerStdioArgs({ projectPath }: BuildMcpServerStdioArgsOptions): string[] {
163
- return ["--mcp-server-stdio", projectPath];
164
- }
165
-
166
- export interface BuildExportLblArgsOptions {
167
- projectPath: string;
168
- outPath: string;
169
- }
170
-
171
- /** Fixed argv builder for the "export VICE labels" verb (R2000-14, the
172
- * live-discovered-symbols-flow-back leg). `--export_lbl <PATH>` takes one
173
- * value, confirmed at execution time from `--help`; `--headless` is
174
- * required because this verb produces no TUI output. */
175
- export function buildExportLblArgs({ projectPath, outPath }: BuildExportLblArgsOptions): string[] {
176
- return ["--headless", "--export_lbl", outPath, projectPath];
177
- }
178
-
179
- export interface BuildImportLblArgsOptions {
180
- projectPath: string;
181
- lblPath: string;
182
- }
183
-
184
- /**
185
- * Fixed argv builder for the "import VICE labels" verb (R2000-15, the
186
- * annotate-then-export-to-VICE leg). `--import_lbl <PATH>` takes one value,
187
- * confirmed at execution time from `--help`.
188
- *
189
- * `--mcp-server-stdio` is NOT optional here -- this is the D-28 trap this
190
- * builder exists to make unreachable by construction. `main.rs:800-806` is
191
- * `if headless && !mcp_server { return Ok(()) }`: an argv of
192
- * `--import_lbl <path> --headless <proj>` imports the labels into memory
193
- * and then hits that early return WITHOUT ever calling save, so the import
194
- * is silently discarded -- measured live on this host: two names imported
195
- * that way, and a subsequent `--export_lbl` read back from disk returned
196
- * only the pre-existing label. `main.rs:709-711` makes `--mcp-server-stdio`
197
- * set both `headless` and `mcp_server`, which is what skips that early
198
- * return and leaves the process alive long enough for a caller to issue an
199
- * explicit `r2000_save_project` over the resulting stdio session.
200
- *
201
- * Deliberately does NOT also add `--headless` to this argv: `--mcp-server-
202
- * stdio` already implies it (`main.rs:709-711`), and a caller reading this
203
- * argv should see the minimum token set that makes persistence possible,
204
- * not a redundant flag alongside it. `--import_lbl` only mutates in-memory
205
- * state -- the caller is still responsible for issuing `r2000_save_project`
206
- * over the resulting session before closing it; this builder only gets the
207
- * import to a point where saving is possible.
208
- */
209
- export function buildImportLblArgs({ projectPath, lblPath }: BuildImportLblArgsOptions): string[] {
210
- return ["--import_lbl", lblPath, "--mcp-server-stdio", projectPath];
211
- }
212
-
213
- export interface RunR2000Options {
214
- cwd?: string;
215
- timeoutMs?: number;
216
- }
217
-
218
- export interface RunR2000Result {
219
- status: number | null;
220
- stdout: string;
221
- stderr: string;
222
- }
223
-
224
- /** Fallback used when `R2000_TIMEOUT_MS`'s env override is absent or
225
- * invalid (WR-10, 10-REVIEW.md:481-511). Exported as its own name, distinct
226
- * from `R2000_TIMEOUT_MS`, so `parseR2000TimeoutMs()` and its tests can
227
- * refer to "the default" without re-deriving the literal `120_000`. */
228
- export const R2000_DEFAULT_TIMEOUT_MS = 120_000;
229
-
230
- /** One-time stderr warning for a malformed `R2000_TIMEOUT_MS` override,
231
- * mirroring `repo-root.ts`'s "warn once on stderr rather than throw on a
232
- * bad env var" convention -- a bad timeout value is an operator mistake,
233
- * not a reason to crash the process before it has done anything. */
234
- let warnedBadTimeoutEnv = false;
235
-
236
- /**
237
- * Parses a `R2000_TIMEOUT_MS`-shaped raw string into a valid, positive
238
- * timeout in milliseconds, falling back to `fallbackMs` (with a one-time
239
- * stderr warning naming the rejected value) for anything non-numeric,
240
- * `NaN`, zero or negative. Never returns `NaN` -- a bad value must fall
241
- * back to the default, never propagate into `spawnSync`'s `timeout` option
242
- * unchecked.
243
- *
244
- * Exported and free of any module-load-time side effect so a test can
245
- * exercise every input shape directly, by calling this function, rather
246
- * than mutating `process.env.R2000_TIMEOUT_MS` after `R2000_TIMEOUT_MS`
247
- * has already been evaluated -- exactly the IN-04 mistake (`R2000_BIN` is
248
- * resolved once at module load; env mutation afterward is a no-op against
249
- * it) applied to a second env-derived constant.
250
- */
251
- export function parseR2000TimeoutMs(
252
- raw: string | undefined,
253
- fallbackMs: number = R2000_DEFAULT_TIMEOUT_MS
254
- ): number {
255
- if (raw === undefined) return fallbackMs;
256
- const n = Number(raw);
257
- if (!Number.isFinite(n) || n <= 0) {
258
- if (!warnedBadTimeoutEnv) {
259
- warnedBadTimeoutEnv = true;
260
- console.error(
261
- `warn: R2000_TIMEOUT_MS="${raw}" is not a positive, finite number -- falling back to the default ` +
262
- `(${fallbackMs}ms). Set R2000_TIMEOUT_MS to a positive number of milliseconds to override it.`
263
- );
264
- }
265
- return fallbackMs;
266
- }
267
- return n;
268
- }
269
-
270
- /** The default `runR2000()` timeout (WR-10): 120s unless overridden by
271
- * `R2000_TIMEOUT_MS`, validated by `parseR2000TimeoutMs()` above so a bad
272
- * override cannot turn into `NaN` inside `spawnSync`'s `timeout` option
273
- * (an unbounded, non-numeric timeout is the same hazard as no timeout at
274
- * all). A per-call `opts.timeoutMs` always overrides this module-level
275
- * default (D-11.1-04) -- this constant only supplies what every one of the
276
- * seven CLI verbs gets when it does not ask for anything different. */
277
- export const R2000_TIMEOUT_MS: number = parseR2000TimeoutMs(process.env.R2000_TIMEOUT_MS);
278
-
279
- /** `spawnSync`'s Node default `maxBuffer` is 1 MiB. That is too small for
280
- * this module's own reason to exist: `--verify`/`--export_asm` transcripts
281
- * ARE the payload `acmeVerdict()` and the label parsers exist to parse, so
282
- * a verbose-but-successful child run must never turn into a truncated
283
- * buffer and an opaque `ERR_CHILD_PROCESS_STDIO_MAXBUFFER`/`ENOBUFS` stack
284
- * in place of the parsed verdict this seam was built to produce (WR-10).
285
- * 32 MiB is comfortably above any transcript measured live against
286
- * regenerator2000 0.9.20 on this host. */
287
- export const R2000_MAX_BUFFER = 32 * 1024 * 1024;
288
-
289
- /**
290
- * Spawns regenerator2000 with the given argv. Calls `assertNoViceFlag(argv)`
291
- * as the FIRST statement of this function body, deliberately, so the guard
292
- * is enforced even if a future edit reorders the rest of the function --
293
- * exactly the discipline `vice.ts`'s `call()` uses for `DENY_LIST`.
294
- *
295
- * Always spawns with an argv ARRAY and never enables a shell interpreter for
296
- * the child process, so a caller-controlled filename can never be
297
- * interpreted by a shell.
298
- *
299
- * BOUNDED (WR-10, 10-REVIEW.md:481-511): this is a **blocking** `spawnSync`,
300
- * so an unbounded child owns the whole Node event loop -- no JSON-RPC, no
301
- * diagnostics, nothing, for as long as the child runs. `timeout` defaults to
302
- * `R2000_TIMEOUT_MS` (120s, env-overridable) and `maxBuffer` is fixed at
303
- * `R2000_MAX_BUFFER` (32 MiB); `opts.timeoutMs` still overrides the default
304
- * per call. Both bounds, when hit, are translated into a named, actionable
305
- * `Error` naming the limit and the argv -- never a raw re-thrown `spawnSync`
306
- * error object, which is what WR-10 found: a *successful* verify with a
307
- * verbose transcript could previously surface as an opaque `ENOBUFS` stack
308
- * instead of the parsed verdict this module exists to produce.
309
- */
310
- export function runR2000(argv: readonly string[], opts: RunR2000Options = {}): RunR2000Result {
311
- assertNoViceFlag(argv);
312
- // Caller override preserved (D-11.1-04): opts.timeoutMs still wins over
313
- // the module-level default. `timeoutMs` below mirrors the exact
314
- // expression passed to spawnSync() so the two can never drift and an
315
- // error message below can name the real bound that was actually applied.
316
- const timeoutMs = opts.timeoutMs ?? R2000_TIMEOUT_MS;
317
- const r = spawnSync(R2000_BIN, [...argv], {
318
- encoding: "utf8",
319
- cwd: opts.cwd,
320
- timeout: opts.timeoutMs ?? R2000_TIMEOUT_MS,
321
- maxBuffer: R2000_MAX_BUFFER,
322
- });
323
- if (r.error) {
324
- const err = r.error as NodeJS.ErrnoException;
325
- if (err.code === "ENOENT") {
326
- throw new Error(
327
- `regenerator2000 was not found on PATH -- install it with \`cargo install regenerator2000\` and ` +
328
- `ensure \`regenerator2000\` is on $PATH (or set R2000_BIN to its full path).`
329
- );
330
- }
331
- // Timeout (WR-10): Node's shape has varied across versions -- observed
332
- // on this host (Node 22) as `error.code === "ETIMEDOUT"` PLUS
333
- // `r.signal === "SIGTERM"` together, so both are checked rather than
334
- // relying on either alone.
335
- if (err.code === "ETIMEDOUT" || r.signal === "SIGTERM") {
336
- throw new Error(
337
- `regenerator2000 timed out after ${timeoutMs}ms and was killed (argv: [${argv.join(", ")}]) -- ` +
338
- `either the child is wedged, or a real run legitimately needs longer than ${timeoutMs}ms. Raise ` +
339
- `R2000_TIMEOUT_MS (or pass { timeoutMs } to runR2000()) if the latter.`
340
- );
341
- }
342
- // Max-buffer overrun (WR-10): observed on this host as
343
- // `error.code === "ENOBUFS"`; matched more broadly against the message
344
- // too, since Node's own docs describe this failure mode inconsistently
345
- // across versions as `ENOBUFS` or `ERR_CHILD_PROCESS_STDIO_MAXBUFFER`.
346
- if (err.code === "ENOBUFS" || /MAXBUFFER|maxBuffer/i.test(err.message ?? "")) {
347
- const limitMiB = (R2000_MAX_BUFFER / (1024 * 1024)).toFixed(0);
348
- throw new Error(
349
- `regenerator2000's combined stdout+stderr exceeded the ${limitMiB} MiB limit (argv: ` +
350
- `[${argv.join(", ")}]) -- its output was truncated, so any verdict parsed from it would be ` +
351
- `unreliable. This is a hard failure, never a silently-shortened transcript feeding a pass.`
352
- );
353
- }
354
- throw r.error;
355
- }
356
- return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
357
- }