@henols/vice-mcp 0.2.2 → 0.2.3
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.
- package/README.md +2 -2
- package/THIRD-PARTY-NOTICES.md +422 -1
- package/anno-bank.ts +171 -0
- package/anno-cli.ts +1674 -99
- package/anno-enum-gen.ts +416 -30
- package/anno-export-asm.ts +1175 -89
- package/anno-graphics.ts +338 -0
- package/anno-hazard-report.ts +1367 -0
- package/anno-import.ts +495 -0
- package/anno-join.ts +480 -0
- package/anno-provenance-ledger.ts +472 -0
- package/anno-register.ts +159 -0
- package/anno-store-export.ts +661 -0
- package/anno-store.ts +518 -2
- package/anno-tools.ts +1169 -16
- package/anno-types.ts +275 -2
- package/backend-detect.mts +124 -312
- package/build.ts +3 -1
- package/capture-predicate.ts +597 -0
- package/channel-lock.ts +349 -0
- package/evid-ingest.ts +217 -0
- package/evid-reconcile.ts +316 -0
- package/host-tool-client.ts +430 -0
- package/incident-record.ts +23 -12
- package/install-resources.ts +29 -13
- package/memmap-lookup.ts +285 -0
- package/package.json +27 -8
- package/prg-image.ts +1 -2
- package/repo-root.ts +87 -3
- package/resources/backend-detect.mjs +98 -236
- package/resources/broker-control.mjs +189 -16
- package/resources/broker-epoch.mjs +1 -1
- package/resources/broker-kill.mjs +8 -2
- package/resources/broker-launch.mjs +365 -210
- package/resources/broker-state.mjs +64 -18
- package/resources/container-guard.mjs +1 -1
- package/resources/ghidra-project.mjs +790 -0
- package/resources/host-tool.mjs +2561 -0
- package/resources/vice-broker.mjs +330 -184
- package/resources/vice-launcher.sh +127 -9
- package/stock-address.ts +1 -1
- package/stock-condition.ts +1 -1
- package/stock-connect.ts +9 -5
- package/stock-derived.ts +29 -37
- package/stock-diagnose.ts +200 -36
- package/stock-dispatch.ts +179 -77
- package/stock-handler.ts +1 -1
- package/stock-paths.ts +18 -14
- package/stock-petscii.ts +1 -1
- package/stock-protocol.ts +1 -1
- package/stock-recycle.ts +83 -2
- package/stock-reproducible-run.ts +811 -0
- package/stock-run-until.ts +100 -1
- package/stock-symbols.ts +4 -4
- package/stock-timing.ts +1 -1
- package/stop-oracle.ts +167 -0
- package/text-capability-probe.ts +660 -0
- package/text-connect.ts +157 -0
- package/text-protocol.ts +810 -0
- package/text-tools.ts +778 -0
- package/textmon-backtrace.ts +385 -0
- package/textmon-cpuhistory.ts +335 -0
- package/textmon-memmap.ts +494 -0
- package/textmon-profile.ts +458 -0
- package/textmon-registers.ts +748 -0
- package/tools-manifest.stock.json +864 -3
- package/vice-broker-client.ts +189 -42
- package/vice-errors.ts +268 -0
- package/vice-proxy.ts +339 -2144
- package/vsf-slice.ts +640 -0
- package/anno-d64.ts +0 -310
- package/capability-registry.ts +0 -390
- package/refresh-manifest.ts +0 -124
- package/tools-manifest.json +0 -1223
- package/vice-probe.ts +0 -278
- package/vice-sync.ts +0 -336
- 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
|
+
}
|