@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,335 @@
1
+ // textmon-cpuhistory.ts
2
+ //
3
+ // THE ONE owning module for VICE's `chis` (CPU history) text-monitor output
4
+ // (PARSE-02, PARSE-03). Nothing else in this tree reads or interprets a
5
+ // `chis` reply -- a handler dials the wire, this module is the only place
6
+ // that turns the framed text into structured data.
7
+ //
8
+ // WHY THIS FILE EXISTS RATHER THAN LIVING INSIDE THE TOOL HANDLER: same
9
+ // reasoning `textmon-memmap.ts` states for itself (D-42-3, inherited from
10
+ // plan 42-01, not re-decided here) -- keeping the parser import-free of
11
+ // transport code means any future non-tool consumer can depend on this one
12
+ // file without dragging in a socket, and an owning module that is pure by
13
+ // construction cannot be the thing that silently launders a wire error into
14
+ // a parse result, because it never touches the wire at all.
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
+ // (reads this module's source, greps for a top-level `import`) -- it is
21
+ // not a style preference to be relaxed later.
22
+ // - Never throw on a malformed or drifted input (D-42-3). Return the
23
+ // discriminated `CpuHistoryParseResult` instead. `withTextTool()`'s own
24
+ // `try/catch` converts anything THROWN into a transport-error message --
25
+ // a thrown refusal would be reported to the caller as a WIRE failure,
26
+ // laundering a format-drift refusal into exactly the "absorbed into a
27
+ // plausible-looking wrong answer" failure mode PARSE-03 exists to
28
+ // prevent.
29
+ // - Never default an unrecognised flag character to "clear" or an
30
+ // unrecognised memspace marker to the main CPU. Either silent default is
31
+ // an inverted statement about the machine's state -- refuse by name
32
+ // instead (see `parseFlags()` and the memspace check in
33
+ // `parseCpuHistoryLine()` below).
34
+ // - Never enforce monotonicity across entries' `cycles` values. There is
35
+ // no monotonic cycle register on this backend (CLAUDE.md's own
36
+ // constraint) -- asserting an ordering the emulator does not promise
37
+ // would turn a legitimate reading into a refusal.
38
+ // - Never assume the cycle-count column is narrower than 12 digits. VICE
39
+ // widened this column; a parser that hardcodes an 8-digit width would
40
+ // refuse or truncate a value the real emulator can legitimately emit.
41
+ // - Never strip a leading entry-echo prompt. Unlike `memmapshow`, `chis`
42
+ // never emits one (`fixtures/textmon/README.md`'s "Framing" section) --
43
+ // stripping a first line here would delete a real history entry.
44
+
45
+ /** The eight processor-status-flag positions, in VICE's own fixed print
46
+ * order. Index 2 ("unused") is the 6502's reserved status bit -- VICE
47
+ * always prints it with the set-glyph `-` when set, never a letter, but per
48
+ * this module's closed decoding rule it is still just one more position
49
+ * that is either its set-glyph or a clear-glyph `.`; it is represented as
50
+ * its own member here rather than folded into a neighbour. */
51
+ export interface CpuHistoryFlags {
52
+ readonly n: boolean;
53
+ readonly v: boolean;
54
+ readonly unused: boolean;
55
+ readonly b: boolean;
56
+ readonly d: boolean;
57
+ readonly i: boolean;
58
+ readonly z: boolean;
59
+ readonly c: boolean;
60
+ }
61
+
62
+ /** One decoded `chis` entry -- one executed instruction. */
63
+ export interface CpuHistoryEntry {
64
+ readonly address: number;
65
+ readonly bytes: readonly number[];
66
+ readonly disassembly: string;
67
+ readonly registers: {
68
+ readonly a: number;
69
+ readonly x: number;
70
+ readonly y: number;
71
+ readonly sp: number;
72
+ };
73
+ readonly flags: CpuHistoryFlags;
74
+ readonly cycles: number;
75
+ }
76
+
77
+ /** The full decoded CPU history: entries in VICE's own emitted order,
78
+ * oldest first, exactly as `chis` printed them -- never re-sorted. */
79
+ export interface CpuHistory {
80
+ readonly entries: readonly CpuHistoryEntry[];
81
+ }
82
+
83
+ /** The closed refusal-code union (D-42-3). `malformed-line` covers a data
84
+ * line whose overall layout does not match at all (including a missing
85
+ * cycle-count column) -- a STRUCTURAL mismatch, distinct from the three
86
+ * content-level refusals below, each of which covers a line that IS laid
87
+ * out correctly but carries a value this parser has never seen at that
88
+ * position. */
89
+ export type CpuHistoryRefusalCode =
90
+ | "empty-response"
91
+ | "malformed-line"
92
+ | "unrecognised-flag"
93
+ | "unrecognised-memspace"
94
+ | "unrecognised-register-label";
95
+
96
+ /** A parser refusal (D-42-3): returned, never thrown. */
97
+ export interface TextParseRefusal {
98
+ readonly code: CpuHistoryRefusalCode;
99
+ readonly message: string;
100
+ readonly line: string;
101
+ readonly lineNumber: number;
102
+ }
103
+
104
+ /** D-42-3's discriminated shape: a parser never throws, it returns one of
105
+ * these two arms. */
106
+ export type CpuHistoryParseResult = { ok: true; value: CpuHistory } | { ok: false; refusal: TextParseRefusal };
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // Framing constants -- this module's OWN copy, never imported from
110
+ // text-protocol.ts (purity rule above). The transport strips the trailing
111
+ // `(C:$xxxx)` prompt before this module ever sees the string in the live
112
+ // wire path, but a fixture's raw `text` still carries it, so this module
113
+ // strips it defensively too -- never a leading prompt, since `chis` carries
114
+ // no entry-echo (README.md's "Framing" section).
115
+ // ---------------------------------------------------------------------------
116
+
117
+ const TRAILING_PROMPT_RE = /\(C:\$[0-9A-Fa-f]{4}\)\s*$/;
118
+
119
+ /** The 4-character-wide register-value hex pattern shared by every field. */
120
+ const HEX2 = "[0-9A-Fa-f]{2}";
121
+
122
+ /** One data line's overall layout, taken from the committed captures. A
123
+ * memspace marker (`.`, one letter, `:`, 4 hex digits), two literal spaces,
124
+ * a 12-character-wide raw-bytes column, a 15-character-wide disassembly
125
+ * column, then the four register fields in loose `LABEL:hex` form (the
126
+ * label text itself is validated separately, closing the "register labels
127
+ * in this exact order" set without conflating a wrong label with a
128
+ * structural malformation), an 8-character flag string, one or more spaces,
129
+ * then a decimal cycle count of up to 12 digits. A line that does not match
130
+ * this AT ALL -- including one missing the trailing cycle-count column
131
+ * entirely -- is a STRUCTURAL malformation (`malformed-line`). */
132
+ const DATA_LINE_RE = new RegExp(
133
+ `^\\.([0-9A-Za-z]):([0-9A-Fa-f]{4}) (.{12})(.{15})` +
134
+ `([A-Za-z]+):(${HEX2}) ([A-Za-z]+):(${HEX2}) ([A-Za-z]+):(${HEX2}) ([A-Za-z]+):(${HEX2}) ` +
135
+ `(.{8}) +(\\d{1,12})$`,
136
+ );
137
+
138
+ /** The eight flag positions' set-glyph sequence, VICE's own fixed order.
139
+ * Index 2 ("unused") is the reserved bit, whose set-glyph happens to be a
140
+ * literal hyphen -- not a special case, just this position's own glyph. */
141
+ const FLAG_SET_GLYPHS = "NV-BDIZC";
142
+ const FLAG_NAMES = ["n", "v", "unused", "b", "d", "i", "z", "c"] as const;
143
+
144
+ /** The one recognised memspace letter: the main CPU. Any other letter means
145
+ * the monitor's default memspace is pointed at a drive (CLAUDE.md's
146
+ * `default_memspace` contamination constraint) -- every field on the line
147
+ * would then describe a different CPU than the caller believes. */
148
+ const MAIN_CPU_MEMSPACE = "C";
149
+
150
+ function makeRefusal(code: CpuHistoryRefusalCode, message: string, line: string, lineNumber: number): TextParseRefusal {
151
+ return { code, message, line, lineNumber };
152
+ }
153
+
154
+ /** Decodes one 12-character-wide raw-bytes column (1 to 3 space-separated
155
+ * hex byte pairs, space-padded on the right to a fixed width). The width
156
+ * itself is already guaranteed by `DATA_LINE_RE`'s own fixed-length capture
157
+ * group; only content is checked here. */
158
+ function parseBytesField(field: string): { ok: true; bytes: number[] } | { ok: false } {
159
+ const trimmed = field.trimEnd();
160
+ if (!/^[0-9A-Fa-f]{2}(?: [0-9A-Fa-f]{2}){0,2}$/.test(trimmed)) return { ok: false };
161
+ const bytes = trimmed.split(" ").map((token) => parseInt(token, 16));
162
+ return { ok: true, bytes };
163
+ }
164
+
165
+ /** Decodes the 8-character flag string. Character at index i must be either
166
+ * a period (that flag clear) or `FLAG_SET_GLYPHS[i]` (that flag set) --
167
+ * anything else refuses: a character silently treated as "clear" is an
168
+ * inverted statement about the machine's flags. */
169
+ function parseFlags(flagStr: string): { ok: true; flags: CpuHistoryFlags } | { ok: false; badChar: string; position: number } {
170
+ const flags: Record<string, boolean> = {};
171
+ for (let i = 0; i < 8; i++) {
172
+ const ch = flagStr[i]!;
173
+ if (ch === ".") {
174
+ flags[FLAG_NAMES[i]!] = false;
175
+ } else if (ch === FLAG_SET_GLYPHS[i]) {
176
+ flags[FLAG_NAMES[i]!] = true;
177
+ } else {
178
+ return { ok: false, badChar: ch, position: i };
179
+ }
180
+ }
181
+ return { ok: true, flags: flags as unknown as CpuHistoryFlags };
182
+ }
183
+
184
+ /**
185
+ * Parses `chis`'s framed text-monitor reply into a structured
186
+ * {@link CpuHistory}. Never throws (D-42-3): every failure mode returns
187
+ * `{ ok: false, refusal }` naming exactly what was wrong and where.
188
+ *
189
+ * Bounded by construction: one pass over the payload's lines, each
190
+ * iteration consumes exactly one line, no recursion anywhere in this
191
+ * function.
192
+ */
193
+ export function parseCpuHistory(text: string): CpuHistoryParseResult {
194
+ if (typeof text !== "string" || text.trim() === "") {
195
+ return {
196
+ ok: false,
197
+ refusal: makeRefusal(
198
+ "empty-response",
199
+ "chis: the response was empty or whitespace-only -- never decoded as a zero-entry CPU history, because an " +
200
+ "empty response and a real capture that recorded nothing are two different facts",
201
+ "",
202
+ 0,
203
+ ),
204
+ };
205
+ }
206
+
207
+ const body = text.replace(TRAILING_PROMPT_RE, "");
208
+
209
+ if (body.trim() === "") {
210
+ return {
211
+ ok: false,
212
+ refusal: makeRefusal(
213
+ "empty-response",
214
+ "chis: the response contained only prompt text and no entries -- never decoded as a zero-entry CPU history",
215
+ "",
216
+ 0,
217
+ ),
218
+ };
219
+ }
220
+
221
+ const lines = body.split("\n");
222
+ // A trailing "\n" before the (already-stripped) prompt splits into one
223
+ // trailing empty element -- drop exactly that split artifact, never any
224
+ // other blank line, so a genuinely blank line inside the payload still
225
+ // reaches the data-line parser below and refuses structurally rather
226
+ // than being silently swallowed here.
227
+ if (lines.length > 0 && lines[lines.length - 1] === "") {
228
+ lines.pop();
229
+ }
230
+
231
+ const entries: CpuHistoryEntry[] = [];
232
+
233
+ for (let i = 0; i < lines.length; i++) {
234
+ const line = lines[i]!;
235
+ const lineNumber = i + 1;
236
+
237
+ const match = DATA_LINE_RE.exec(line);
238
+ if (!match) {
239
+ return {
240
+ ok: false,
241
+ refusal: makeRefusal(
242
+ "malformed-line",
243
+ `chis: line ${lineNumber} does not match the expected entry layout: ${JSON.stringify(line)}`,
244
+ line,
245
+ lineNumber,
246
+ ),
247
+ };
248
+ }
249
+
250
+ const memspace = match[1]!;
251
+ const addrHex = match[2]!;
252
+ const bytesField = match[3]!;
253
+ const disasm = match[4]!;
254
+ const labelA = match[5]!;
255
+ const hexA = match[6]!;
256
+ const labelX = match[7]!;
257
+ const hexX = match[8]!;
258
+ const labelY = match[9]!;
259
+ const hexY = match[10]!;
260
+ const labelSp = match[11]!;
261
+ const hexSp = match[12]!;
262
+ const flagStr = match[13]!;
263
+ const cyclesStr = match[14]!;
264
+
265
+ if (memspace !== MAIN_CPU_MEMSPACE) {
266
+ return {
267
+ ok: false,
268
+ refusal: makeRefusal(
269
+ "unrecognised-memspace",
270
+ `chis: line ${lineNumber} carries memspace marker ${JSON.stringify(memspace)}, not the main CPU's ` +
271
+ `${JSON.stringify(MAIN_CPU_MEMSPACE)} -- the monitor's default memspace is likely pointed at a drive, ` +
272
+ `which makes every field on this line describe the wrong CPU; reset it with the vice_device_console tool`,
273
+ line,
274
+ lineNumber,
275
+ ),
276
+ };
277
+ }
278
+
279
+ if (labelA !== "A" || labelX !== "X" || labelY !== "Y" || labelSp !== "SP") {
280
+ return {
281
+ ok: false,
282
+ refusal: makeRefusal(
283
+ "unrecognised-register-label",
284
+ `chis: line ${lineNumber} does not carry the register labels in the expected A, X, Y, SP order: found ` +
285
+ `${JSON.stringify([labelA, labelX, labelY, labelSp])}`,
286
+ line,
287
+ lineNumber,
288
+ ),
289
+ };
290
+ }
291
+
292
+ const bytesResult = parseBytesField(bytesField);
293
+ if (!bytesResult.ok) {
294
+ return {
295
+ ok: false,
296
+ refusal: makeRefusal(
297
+ "malformed-line",
298
+ `chis: line ${lineNumber} has a malformed raw-bytes column: ${JSON.stringify(bytesField)}`,
299
+ line,
300
+ lineNumber,
301
+ ),
302
+ };
303
+ }
304
+
305
+ const flagsResult = parseFlags(flagStr);
306
+ if (!flagsResult.ok) {
307
+ return {
308
+ ok: false,
309
+ refusal: makeRefusal(
310
+ "unrecognised-flag",
311
+ `chis: line ${lineNumber} has an unrecognised processor-flag character ${JSON.stringify(flagsResult.badChar)} ` +
312
+ `at position ${flagsResult.position}: ${JSON.stringify(flagStr)}`,
313
+ line,
314
+ lineNumber,
315
+ ),
316
+ };
317
+ }
318
+
319
+ entries.push({
320
+ address: parseInt(addrHex, 16),
321
+ bytes: bytesResult.bytes,
322
+ disassembly: disasm.trimEnd(),
323
+ registers: {
324
+ a: parseInt(hexA, 16),
325
+ x: parseInt(hexX, 16),
326
+ y: parseInt(hexY, 16),
327
+ sp: parseInt(hexSp, 16),
328
+ },
329
+ flags: flagsResult.flags,
330
+ cycles: parseInt(cyclesStr, 10),
331
+ });
332
+ }
333
+
334
+ return { ok: true, value: { entries } };
335
+ }