@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
@@ -0,0 +1,385 @@
1
+ // textmon-backtrace.ts
2
+ //
3
+ // THE ONE owning module for VICE's `bt` (backtrace) text-monitor output
4
+ // (PARSE-02, PARSE-03). Nothing else in this tree reads or interprets a
5
+ // `bt` reply -- a handler dials the wire, this module is the only place
6
+ // that turns the framed text into structured data. This module's sibling
7
+ // is `textmon-cpuhistory.ts` (Task 1 of this same plan); both share the
8
+ // address/bytes/disassembly column layout the `chis` format established,
9
+ // and both inherit D-42-3 (a parser returns a discriminated refusal, never
10
+ // throws) from plan 42-01's `textmon-memmap.ts`.
11
+ //
12
+ // WHY THIS FILE EXISTS RATHER THAN LIVING INSIDE THE TOOL HANDLER: the same
13
+ // reasoning `textmon-memmap.ts` and `textmon-cpuhistory.ts` state for
14
+ // themselves applies here unchanged.
15
+ //
16
+ // WHAT NOT TO DO:
17
+ // - Never import anything -- not `node:` anything, not `text-protocol.ts`,
18
+ // not `textmon-fixtures.ts`, not even a type-only import of a sibling
19
+ // module. Purity is asserted mechanically by this file's own test.
20
+ // - Never throw on a malformed or drifted input (D-42-3). Return the
21
+ // discriminated `BacktraceParseResult` instead.
22
+ // - Never re-sort or reverse `frames`. VICE emits the JSR chain in its own
23
+ // order -- the chain's own order IS the evidence; silently re-sorting
24
+ // or reversing it would restate which call is innermost.
25
+ // - Never truncate the frame list to a caller-requested depth. That is
26
+ // the tool's concern (plan 42-07), never this module's -- a parser that
27
+ // truncated would make the frame count a function of a caller's
28
+ // argument rather than of the machine.
29
+ // - Never read an unrecognised origin token as if it were an address. An
30
+ // unknown origin means the format has drifted from what this parser
31
+ // has ever seen; fabricating a caller from it would be worse than
32
+ // refusing.
33
+ // - Never treat the literal `+-` form (a negative SP offset) as a sign to
34
+ // "honour" separately from the digits -- the `+` is a fixed part of the
35
+ // field VICE always prints, and the sign lives in the digits' own
36
+ // prefix (see `parseSpOffset()` below).
37
+ // - Never strip a leading entry-echo prompt. `bt` never emits one
38
+ // (`fixtures/textmon/README.md`'s "Framing" section) -- stripping a
39
+ // first line here would delete the current-PC line.
40
+
41
+ /**
42
+ * The current-PC frame: where execution is actually paused. Carries no
43
+ * caller, callee or SP offset -- those only apply to the call frames below
44
+ * it in the reconstructed chain.
45
+ */
46
+ export interface BacktraceCurrentPc {
47
+ readonly address: number;
48
+ readonly bytes: readonly number[];
49
+ readonly disassembly: string;
50
+ }
51
+
52
+ /** The closed set of non-numeric frame origins: the three interrupt
53
+ * vectors. A numeric origin (a 4-hex-digit return address) is represented
54
+ * as a `number` instead -- see {@link BacktraceFrame.origin}. */
55
+ export type BacktraceOriginName = "reset" | "irq" | "nmi";
56
+
57
+ /** One reconstructed call frame. `origin` is either the caller's own
58
+ * address (a `number`) or one of the three interrupt-vector names -- never
59
+ * a token this parser has not explicitly recognised. `spOffset` is signed;
60
+ * VICE's own literal `+-` form for a negative offset is decoded, never
61
+ * treated as an edge case. */
62
+ export interface BacktraceFrame {
63
+ readonly origin: number | BacktraceOriginName;
64
+ readonly callee: number;
65
+ readonly spOffset: number;
66
+ readonly address: number;
67
+ readonly bytes: readonly number[];
68
+ readonly disassembly: string;
69
+ }
70
+
71
+ /** The full decoded backtrace: the current-PC frame plus every call frame,
72
+ * in VICE's own emitted order -- never re-sorted, never reversed, never
73
+ * truncated. */
74
+ export interface Backtrace {
75
+ readonly currentPc: BacktraceCurrentPc;
76
+ readonly frames: readonly BacktraceFrame[];
77
+ }
78
+
79
+ /** The closed refusal-code union (D-42-3). `malformed-frame-line` covers a
80
+ * frame line whose overall layout does not match at all -- a STRUCTURAL
81
+ * mismatch, distinct from `unrecognised-origin` and `malformed-sp-offset`,
82
+ * each of which covers a line that IS laid out correctly but carries a
83
+ * value this parser has never seen at that position. */
84
+ export type BacktraceRefusalCode =
85
+ | "empty-response"
86
+ | "missing-current-pc-line"
87
+ | "malformed-frame-line"
88
+ | "unrecognised-origin"
89
+ | "malformed-sp-offset"
90
+ | "unrecognised-memspace";
91
+
92
+ /** A parser refusal (D-42-3): returned, never thrown. */
93
+ export interface TextParseRefusal {
94
+ readonly code: BacktraceRefusalCode;
95
+ readonly message: string;
96
+ readonly line: string;
97
+ readonly lineNumber: number;
98
+ }
99
+
100
+ /** D-42-3's discriminated shape: a parser never throws, it returns one of
101
+ * these two arms. */
102
+ export type BacktraceParseResult = { ok: true; value: Backtrace } | { ok: false; refusal: TextParseRefusal };
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Framing constants -- this module's OWN copy, never imported from
106
+ // text-protocol.ts. The transport strips the trailing `(C:$xxxx)` prompt on
107
+ // the live wire path, but a fixture's raw `text` still carries it, so this
108
+ // module strips it defensively too -- never a leading prompt, since `bt`
109
+ // carries no entry-echo (README.md's "Framing" section).
110
+ // ---------------------------------------------------------------------------
111
+
112
+ const TRAILING_PROMPT_RE = /\(C:\$[0-9A-Fa-f]{4}\)\s*$/;
113
+
114
+ /** The one recognised memspace letter: the main CPU. Any other letter means
115
+ * the monitor's default memspace is pointed at a drive (CLAUDE.md's
116
+ * `default_memspace` contamination constraint). */
117
+ const MAIN_CPU_MEMSPACE = "C";
118
+
119
+ /**
120
+ * The shared suffix every line (the current-PC line and every frame line
121
+ * alike) carries: an arbitrary prefix (captured separately and validated by
122
+ * the caller), then a memspace marker (`.`, one letter, `:`, 4 hex digits),
123
+ * three literal spaces, a 12-character-wide raw-bytes column, then the
124
+ * disassembly text running to the end of the line (never padded here --
125
+ * unlike `chis`, nothing follows it on this format, so trailing width is
126
+ * whatever the mnemonic and operand actually need). The non-greedy prefix
127
+ * capture matches the SHORTEST text before the first occurrence of the
128
+ * memspace-marker pattern, which is exactly once per line by construction.
129
+ */
130
+ const SUFFIX_RE = /^(.*?)\.([A-Za-z]):([0-9A-Fa-f]{4}) (.{12})(.*)$/;
131
+
132
+ /**
133
+ * A frame line's prefix, once split from the shared suffix above: an origin
134
+ * token (captured loosely as any 4 characters, validated separately against
135
+ * the closed origin set below), ` -> `, the callee address (exactly 4 hex
136
+ * digits), ` [SP +`, the offset field (captured loosely, validated
137
+ * separately -- VICE's own literal `+-` form for a negative offset means
138
+ * the digits' own prefix carries the sign, never a separate sign to
139
+ * honour), `]`, one trailing space.
140
+ */
141
+ const FRAME_PREFIX_RE = /^(.{4}) -> ([0-9A-Fa-f]{4}) \[SP \+([^\]]*)\] $/;
142
+
143
+ /** The closed origin-name set: VICE's three interrupt vectors, each printed
144
+ * padded to 4 characters (`"RST "`, `"IRQ "`, `"NMI "`) -- the same width
145
+ * as a 4-hex-digit numeric origin. */
146
+ const ORIGIN_NAMES: ReadonlyArray<{ token: string; name: BacktraceOriginName }> = [
147
+ { token: "RST ", name: "reset" },
148
+ { token: "IRQ ", name: "irq" },
149
+ { token: "NMI ", name: "nmi" },
150
+ ];
151
+
152
+ function makeRefusal(code: BacktraceRefusalCode, message: string, line: string, lineNumber: number): TextParseRefusal {
153
+ return { code, message, line, lineNumber };
154
+ }
155
+
156
+ /** Decodes the 12-character-wide raw-bytes column (1 to 3 space-separated
157
+ * hex byte pairs, space-padded on the right to a fixed width) -- the exact
158
+ * same shape `chis` uses. */
159
+ function parseBytesField(field: string): { ok: true; bytes: number[] } | { ok: false } {
160
+ const trimmed = field.trimEnd();
161
+ if (!/^[0-9A-Fa-f]{2}(?: [0-9A-Fa-f]{2}){0,2}$/.test(trimmed)) return { ok: false };
162
+ const bytes = trimmed.split(" ").map((token) => parseInt(token, 16));
163
+ return { ok: true, bytes };
164
+ }
165
+
166
+ /** Decodes the SP-offset field's raw captured text (whatever sat between
167
+ * `+` and `]`) into a signed integer. VICE's own literal `+-` form for a
168
+ * negative offset means the field can read `-241` directly (no leading
169
+ * spaces) or ` 3` (spaces then an unsigned magnitude) -- both are valid
170
+ * shapes of "optional leading spaces, optional minus, digits"; anything
171
+ * else refuses. */
172
+ function parseSpOffset(raw: string): { ok: true; value: number } | { ok: false } {
173
+ if (!/^ *-?\d+$/.test(raw)) return { ok: false };
174
+ return { ok: true, value: parseInt(raw.trim(), 10) };
175
+ }
176
+
177
+ /** Matches the shared address/bytes/disassembly suffix against `line`,
178
+ * returning its four parts, or `undefined` if the line does not carry the
179
+ * memspace-marker-plus-address pattern at all. */
180
+ function matchSuffix(
181
+ line: string,
182
+ ): { prefix: string; memspace: string; addrHex: string; bytesField: string; disasm: string } | undefined {
183
+ const match = SUFFIX_RE.exec(line);
184
+ if (!match) return undefined;
185
+ return { prefix: match[1]!, memspace: match[2]!, addrHex: match[3]!, bytesField: match[4]!, disasm: match[5]! };
186
+ }
187
+
188
+ /**
189
+ * Parses `bt`'s framed text-monitor reply into a structured
190
+ * {@link Backtrace}. Never throws (D-42-3): every failure mode returns
191
+ * `{ ok: false, refusal }` naming exactly what was wrong and where.
192
+ *
193
+ * Bounded by construction: one pass over the payload's lines, each
194
+ * iteration consumes exactly one line, no recursion anywhere in this
195
+ * function.
196
+ */
197
+ export function parseBacktrace(text: string): BacktraceParseResult {
198
+ if (typeof text !== "string" || text.trim() === "") {
199
+ return {
200
+ ok: false,
201
+ refusal: makeRefusal(
202
+ "empty-response",
203
+ "bt: the response was empty or whitespace-only -- never decoded as a zero-frame backtrace, because an " +
204
+ "empty response and a real capture that recorded nothing are two different facts",
205
+ "",
206
+ 0,
207
+ ),
208
+ };
209
+ }
210
+
211
+ const body = text.replace(TRAILING_PROMPT_RE, "");
212
+
213
+ if (body.trim() === "") {
214
+ return {
215
+ ok: false,
216
+ refusal: makeRefusal(
217
+ "empty-response",
218
+ "bt: the response contained only prompt text and no current-PC line -- never decoded as a zero-frame backtrace",
219
+ "",
220
+ 0,
221
+ ),
222
+ };
223
+ }
224
+
225
+ const lines = body.split("\n");
226
+ // A trailing "\n" before the (already-stripped) prompt splits into one
227
+ // trailing empty element -- drop exactly that split artifact, never any
228
+ // other blank line, so a genuinely blank line inside the payload still
229
+ // reaches the line parser below and refuses structurally rather than
230
+ // being silently swallowed here.
231
+ if (lines.length > 0 && lines[lines.length - 1] === "") {
232
+ lines.pop();
233
+ }
234
+
235
+ const pcLine = lines[0] ?? "";
236
+ const pcMatch = matchSuffix(pcLine);
237
+ if (!pcMatch || !/^\s*PC\s*$/.test(pcMatch.prefix)) {
238
+ return {
239
+ ok: false,
240
+ refusal: makeRefusal(
241
+ "missing-current-pc-line",
242
+ `bt: the first line is not a recognised current-PC line: ${JSON.stringify(pcLine)}`,
243
+ pcLine,
244
+ 1,
245
+ ),
246
+ };
247
+ }
248
+ if (pcMatch.memspace !== MAIN_CPU_MEMSPACE) {
249
+ return {
250
+ ok: false,
251
+ refusal: makeRefusal(
252
+ "unrecognised-memspace",
253
+ `bt: the current-PC line carries memspace marker ${JSON.stringify(pcMatch.memspace)}, not the main CPU's ` +
254
+ `${JSON.stringify(MAIN_CPU_MEMSPACE)} -- reset the default memspace with the vice_device_console tool`,
255
+ pcLine,
256
+ 1,
257
+ ),
258
+ };
259
+ }
260
+ const pcBytesResult = parseBytesField(pcMatch.bytesField);
261
+ if (!pcBytesResult.ok) {
262
+ return {
263
+ ok: false,
264
+ refusal: makeRefusal(
265
+ "malformed-frame-line",
266
+ `bt: the current-PC line has a malformed raw-bytes column: ${JSON.stringify(pcMatch.bytesField)}`,
267
+ pcLine,
268
+ 1,
269
+ ),
270
+ };
271
+ }
272
+
273
+ const currentPc: BacktraceCurrentPc = {
274
+ address: parseInt(pcMatch.addrHex, 16),
275
+ bytes: pcBytesResult.bytes,
276
+ disassembly: pcMatch.disasm.trimEnd(),
277
+ };
278
+
279
+ const frames: BacktraceFrame[] = [];
280
+
281
+ for (let i = 1; i < lines.length; i++) {
282
+ const line = lines[i]!;
283
+ const lineNumber = i + 1;
284
+
285
+ const suffixMatch = matchSuffix(line);
286
+ if (!suffixMatch) {
287
+ return {
288
+ ok: false,
289
+ refusal: makeRefusal(
290
+ "malformed-frame-line",
291
+ `bt: line ${lineNumber} does not match the expected frame layout: ${JSON.stringify(line)}`,
292
+ line,
293
+ lineNumber,
294
+ ),
295
+ };
296
+ }
297
+
298
+ const frontMatch = FRAME_PREFIX_RE.exec(suffixMatch.prefix);
299
+ if (!frontMatch) {
300
+ return {
301
+ ok: false,
302
+ refusal: makeRefusal(
303
+ "malformed-frame-line",
304
+ `bt: line ${lineNumber} does not match the expected "origin -> callee [SP +offset]" layout: ${JSON.stringify(line)}`,
305
+ line,
306
+ lineNumber,
307
+ ),
308
+ };
309
+ }
310
+
311
+ const originToken = frontMatch[1]!;
312
+ const calleeHex = frontMatch[2]!;
313
+ const offsetRaw = frontMatch[3]!;
314
+
315
+ let origin: number | BacktraceOriginName;
316
+ if (/^[0-9A-Fa-f]{4}$/.test(originToken)) {
317
+ origin = parseInt(originToken, 16);
318
+ } else {
319
+ const named = ORIGIN_NAMES.find((entry) => entry.token === originToken);
320
+ if (!named) {
321
+ return {
322
+ ok: false,
323
+ refusal: makeRefusal(
324
+ "unrecognised-origin",
325
+ `bt: line ${lineNumber} carries an unrecognised frame origin ${JSON.stringify(originToken)} -- not a ` +
326
+ `4-hex-digit address and not one of the three recognised interrupt-vector names`,
327
+ line,
328
+ lineNumber,
329
+ ),
330
+ };
331
+ }
332
+ origin = named.name;
333
+ }
334
+
335
+ if (suffixMatch.memspace !== MAIN_CPU_MEMSPACE) {
336
+ return {
337
+ ok: false,
338
+ refusal: makeRefusal(
339
+ "unrecognised-memspace",
340
+ `bt: line ${lineNumber} carries memspace marker ${JSON.stringify(suffixMatch.memspace)}, not the main ` +
341
+ `CPU's ${JSON.stringify(MAIN_CPU_MEMSPACE)} -- reset the default memspace with the vice_device_console tool`,
342
+ line,
343
+ lineNumber,
344
+ ),
345
+ };
346
+ }
347
+
348
+ const spOffsetResult = parseSpOffset(offsetRaw);
349
+ if (!spOffsetResult.ok) {
350
+ return {
351
+ ok: false,
352
+ refusal: makeRefusal(
353
+ "malformed-sp-offset",
354
+ `bt: line ${lineNumber} has a malformed SP-offset field ${JSON.stringify(offsetRaw)} -- never defaulted to zero`,
355
+ line,
356
+ lineNumber,
357
+ ),
358
+ };
359
+ }
360
+
361
+ const bytesResult = parseBytesField(suffixMatch.bytesField);
362
+ if (!bytesResult.ok) {
363
+ return {
364
+ ok: false,
365
+ refusal: makeRefusal(
366
+ "malformed-frame-line",
367
+ `bt: line ${lineNumber} has a malformed raw-bytes column: ${JSON.stringify(suffixMatch.bytesField)}`,
368
+ line,
369
+ lineNumber,
370
+ ),
371
+ };
372
+ }
373
+
374
+ frames.push({
375
+ origin,
376
+ callee: parseInt(calleeHex, 16),
377
+ spOffset: spOffsetResult.value,
378
+ address: parseInt(suffixMatch.addrHex, 16),
379
+ bytes: bytesResult.bytes,
380
+ disassembly: suffixMatch.disasm.trimEnd(),
381
+ });
382
+ }
383
+
384
+ return { ok: true, value: { currentPc, frames } };
385
+ }