@henols/vice-mcp 0.2.3 → 0.2.5
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/anno-cli.ts +156 -158
- package/anno-confidence.ts +2 -2
- package/anno-derive.ts +6 -6
- package/anno-details.ts +4 -4
- package/anno-export-asm.ts +100 -101
- package/anno-graphics.ts +16 -16
- package/anno-hazard-report.ts +2 -2
- package/anno-import.ts +15 -15
- package/anno-index.ts +8 -8
- package/anno-join.ts +35 -35
- package/anno-memmap-render.ts +22 -21
- package/anno-provenance-ledger.ts +4 -4
- package/anno-regbits-gen.ts +13 -13
- package/anno-store-export.ts +11 -11
- package/anno-store.ts +145 -165
- package/anno-symbols.ts +7 -7
- package/anno-types.ts +55 -55
- package/capture-predicate.ts +2 -6
- package/evid-ingest.ts +1 -3
- package/memmap-lookup.ts +1 -1
- package/package.json +1 -1
- package/resources/broker-control.mjs +85 -92
- package/resources/broker-epoch.mjs +6 -7
- package/resources/broker-kill.mjs +29 -30
- package/resources/broker-launch.mjs +352 -370
- package/resources/broker-state.mjs +9 -10
- package/resources/host-tool.mjs +636 -664
- package/resources/vice-broker.mjs +189 -191
- package/stock-derived.ts +1 -0
- package/stock-dispatch.ts +5 -1
- package/text-protocol.ts +295 -25
- package/text-tools.ts +106 -1
- package/tools-manifest.stock.json +63 -0
- package/vice-broker-client.ts +98 -100
package/stock-derived.ts
CHANGED
|
@@ -106,6 +106,7 @@ export const STOCK_DERIVED_TOOLS: ReadonlySet<string> = new Set([
|
|
|
106
106
|
"vice_profile_flat", // Plan 42-07, PARSE-02 -- text-channel flat-profile tool, needsSession:false (text-tools.ts)
|
|
107
107
|
"vice_backtrace", // Plan 42-07, PARSE-02/D-42-4 -- text-channel backtrace tool, shares the fork's own name, needsSession:false (text-tools.ts)
|
|
108
108
|
"vice_io_registers", // Plan 42-07, PARSE-02 -- text-channel register-decode tool, needsSession:false (text-tools.ts)
|
|
109
|
+
"vice_program_load", // Plan 50-04, route-d -- text-channel program-load tool for the committed hazard-subject fixture, needsSession:false (text-tools.ts)
|
|
109
110
|
]);
|
|
110
111
|
|
|
111
112
|
/**
|
package/stock-dispatch.ts
CHANGED
|
@@ -75,7 +75,7 @@ import { handleCyclesStopwatch, forgetTimingForOtherTargets } from "./stock-timi
|
|
|
75
75
|
import { handleRunUntil } from "./stock-run-until.ts";
|
|
76
76
|
import { handleDiagnoseStock } from "./stock-diagnose.ts";
|
|
77
77
|
import { handleRecycleStock } from "./stock-recycle.ts";
|
|
78
|
-
import { handleDeviceConsole, handleWarpSet, handleMemmapShow, handleMemmapZap, handleCpuHistory, handleProfileFlat, handleBacktrace, handleIoRegisters } from "./text-tools.ts";
|
|
78
|
+
import { handleDeviceConsole, handleWarpSet, handleMemmapShow, handleMemmapZap, handleCpuHistory, handleProfileFlat, handleBacktrace, handleIoRegisters, handleProgramLoad } from "./text-tools.ts";
|
|
79
79
|
|
|
80
80
|
// Re-exported so Phase 2's existing import surface (and its 921-line test
|
|
81
81
|
// file) keeps working unchanged -- these four names used to be DEFINED
|
|
@@ -832,6 +832,10 @@ const STOCK_DISPATCH_TABLE: Record<string, StockHandler> = {
|
|
|
832
832
|
vice_profile_flat: withDerivedTool("vice_profile_flat", { needsSession: false }, handleProfileFlat),
|
|
833
833
|
vice_backtrace: withDerivedTool("vice_backtrace", { needsSession: false }, handleBacktrace),
|
|
834
834
|
vice_io_registers: withDerivedTool("vice_io_registers", { needsSession: false }, handleIoRegisters),
|
|
835
|
+
// Plan 50-04 (route-d): reaches text-protocol.ts's widened `load` verb --
|
|
836
|
+
// same needsSession:false reasoning as its five siblings above (each
|
|
837
|
+
// resolves its own lease and takes only the text channel's own lock).
|
|
838
|
+
vice_program_load: withDerivedTool("vice_program_load", { needsSession: false }, handleProgramLoad),
|
|
835
839
|
};
|
|
836
840
|
|
|
837
841
|
/** Looks up the table entry for `name` -- `undefined` on a miss, never a
|
package/text-protocol.ts
CHANGED
|
@@ -51,24 +51,33 @@
|
|
|
51
51
|
// outbound command must come from TEXT_COMMAND_ALLOWLIST below;
|
|
52
52
|
// command() refuses anything else BY NAME, before a single byte reaches
|
|
53
53
|
// the socket (D-01). The ONE stated, bounded exception (D-42-1, plan
|
|
54
|
-
// 42-04): TEXT_COMMAND_PARAM_SPECS lets exactly
|
|
55
|
-
// verbs also carry a caller-chosen value, but
|
|
56
|
-
// typed, bounded number -- never a string, never
|
|
57
|
-
// passthrough, never a `params` field -- validated and
|
|
58
|
-
// buildTextCommand(), the ONE place such a string is built,
|
|
59
|
-
// accepted as dialable only when isDialableTextCommandForVerb()'s
|
|
60
|
-
// re-render round trip reproduces it byte-for-byte. This does not
|
|
54
|
+
// 42-04, widened plan 50-04): TEXT_COMMAND_PARAM_SPECS lets exactly
|
|
55
|
+
// four of the allowlisted verbs also carry a caller-chosen value, but
|
|
56
|
+
// that value is always a typed, bounded number -- never a string, never
|
|
57
|
+
// a rest-of-line passthrough, never a `params` field -- validated and
|
|
58
|
+
// rendered by buildTextCommand(), the ONE place such a string is built,
|
|
59
|
+
// and accepted as dialable only when isDialableTextCommandForVerb()'s
|
|
60
|
+
// own re-render round trip reproduces it byte-for-byte. This does not
|
|
61
61
|
// widen what "free-text command" means above; it narrows one bounded
|
|
62
|
-
// numeric slot per verb.
|
|
62
|
+
// numeric slot per verb. Plan 50-04's `load` entry is the sole
|
|
63
|
+
// exception to "never a string": the FILENAME half of that one entry is
|
|
64
|
+
// not a caller-supplied string at all -- it is baked into the verb's
|
|
65
|
+
// own frozen identity, a reviewed literal chosen by this file, never a
|
|
66
|
+
// value a caller passes in. Only the device NUMBER is the caller's
|
|
67
|
+
// bounded value, exactly like every other entry in this table. See
|
|
68
|
+
// TEXT_COMMAND_PARAM_SPECS's own `load` entry below for the full
|
|
69
|
+
// rationale.
|
|
63
70
|
// - Never frame a response by a timeout, a byte count, or any fallback
|
|
64
71
|
// that hands back a plausible-looking partial payload. A response that
|
|
65
72
|
// cannot be honestly framed refuses by name (TextFramingError), naming
|
|
66
73
|
// what was actually observed -- never a guess.
|
|
67
74
|
import { EventEmitter } from "node:events";
|
|
68
75
|
import net from "node:net";
|
|
76
|
+
import { join } from "node:path";
|
|
69
77
|
|
|
70
78
|
import { ViceError } from "./vice-errors.ts";
|
|
71
79
|
import { acquireChannelLock, currentChannelLockHolder } from "./channel-lock.ts";
|
|
80
|
+
import { repoRoot } from "./repo-root.ts";
|
|
72
81
|
|
|
73
82
|
// ---------------------------------------------------------------------------
|
|
74
83
|
// The prompt terminator and the closed command allowlist.
|
|
@@ -117,10 +126,38 @@ export const PROMPT_RE = /\(C:\$[0-9A-Fa-f]{4}\)\s*$/;
|
|
|
117
126
|
* subtracting a prior baseline after the fact. The sibling verb `memmapsave`
|
|
118
127
|
* is deliberately NOT added here -- it writes a host file, and this
|
|
119
128
|
* allowlist's own membership test (`text-protocol.test.ts`) refuses any
|
|
120
|
-
* entry whose name matches the pattern that spells the
|
|
121
|
-
*
|
|
129
|
+
* entry whose name matches the pattern that spells the word `save`, the
|
|
130
|
+
* same file-touching-WRITE-verb rule `device c:`, `warp on/off`,
|
|
122
131
|
* `memmapshow`, `prof flat`, `chis`, `bt`, `io` and `prof on/off` already
|
|
123
132
|
* satisfy.
|
|
133
|
+
*
|
|
134
|
+
* `load` (plan 50-04, 2026-09-15, a conscious, measured widening, and the
|
|
135
|
+
* FIRST one that reverses part of the load/save refusal rather than adding
|
|
136
|
+
* a fresh always-safe verb): Phase 50 needed a way to get a committed
|
|
137
|
+
* `.prg` into a running stock VICE for a live capture, and the project's
|
|
138
|
+
* only other route -- `vice_autostart` against a bare `.prg` -- was
|
|
139
|
+
* MEASURED to fail with monitor error `0x8f` (see
|
|
140
|
+
* `fixtures/hazard-subject/FIXTURE-DESIGN.md`), with no committed `.d64`
|
|
141
|
+
* writer anywhere in this tree to fall back to. The developer was shown
|
|
142
|
+
* three routes that added no capability, a hand-authored disk image, or a
|
|
143
|
+
* new file-WRITING host-tool capability -- and chose a FOURTH: the text
|
|
144
|
+
* monitor's own `load` command (`load "<filename>" <device> [<address>]`,
|
|
145
|
+
* VICE Manual ch. 12, `mon_parse.y`'s `disk_rules` grammar), because it
|
|
146
|
+
* READS a host file and writes nothing back to the host. That is the
|
|
147
|
+
* load-bearing distinction this widening rests on: this allowlist's own
|
|
148
|
+
* rule above was never "no `load` or `save`" in principle, it was "no verb
|
|
149
|
+
* that touches a host file" -- and `memmapsave`'s rejection above is a
|
|
150
|
+
* WRITE. `load` is a READ, and the developer judged the original rule to
|
|
151
|
+
* have over-reached for the read direction (recorded in
|
|
152
|
+
* `.planning/phases/50-equivalence-and-modifiability/evidence/LOAD-ROUTE.md`).
|
|
153
|
+
* This is NOT a general "load anything" capability: the entry added to
|
|
154
|
+
* TEXT_COMMAND_PARAM_SPECS below bakes in the ONE committed fixture path
|
|
155
|
+
* this phase's tracer plan targets as part of the verb's own frozen
|
|
156
|
+
* identity, never a caller-supplied filename -- and, since plan 50-05, one
|
|
157
|
+
* such frozen entry per member of the closed HAZARD_SUBJECT_PRG_RELPATHS
|
|
158
|
+
* table, still never a caller-supplied filename. See those entries' own
|
|
159
|
+
* comment for why a caller still cannot choose what gets loaded. `save`
|
|
160
|
+
* remains refused exactly as before; only the read direction moved.
|
|
124
161
|
*/
|
|
125
162
|
export const TEXT_COMMAND_ALLOWLIST = Object.freeze([
|
|
126
163
|
"device c:",
|
|
@@ -139,17 +176,24 @@ export const TEXT_COMMAND_ALLOWLIST = Object.freeze([
|
|
|
139
176
|
export type TextCommand = (typeof TEXT_COMMAND_ALLOWLIST)[number];
|
|
140
177
|
|
|
141
178
|
// ---------------------------------------------------------------------------
|
|
142
|
-
// Parameterized commands (D-42-1, plan 42-04). Three of
|
|
143
|
-
// allowlisted verbs -- "chis", "prof flat", "io" -- were captured on the
|
|
179
|
+
// Parameterized commands (D-42-1, plan 42-04, widened plan 50-04). Three of
|
|
180
|
+
// the allowlisted verbs -- "chis", "prof flat", "io" -- were captured on the
|
|
144
181
|
// real wire carrying a caller-chosen value ("chis 4", "prof flat 5",
|
|
145
182
|
// "io $d020" -- see fixtures/textmon/{cpu-history,flat-profile,
|
|
146
183
|
// register-decode}-stock.json's own "command" field), so a bare literal
|
|
147
184
|
// alone cannot reach them meaningfully. TEXT_COMMAND_ALLOWLIST above is NOT
|
|
148
|
-
// widened for this:
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
185
|
+
// widened for this: its own membership assertion is unaffected by any entry
|
|
186
|
+
// here. TEXT_COMMAND_PARAM_SPECS is a SIBLING table describing, for the
|
|
187
|
+
// subset of verbs that take one, the bounded typed value each accepts and
|
|
188
|
+
// the ONE renderer that turns a validated value into the exact command
|
|
189
|
+
// string VICE accepts. Plan 50-04 adds a "load" entry, and plan 50-05 turns
|
|
190
|
+
// that single entry into one DERIVED entry per committed subject (see
|
|
191
|
+
// HAZARD_SUBJECT_PRG_RELPATHS) -- see those entries' own comment below and
|
|
192
|
+
// TEXT_COMMAND_ALLOWLIST's doc comment above for the full
|
|
193
|
+
// rationale; unlike the first three, this one is not sourced from a
|
|
194
|
+
// committed live-captured fixture (no live capture was run to add it -- the
|
|
195
|
+
// syntax is sourced directly from VICE's own upstream grammar and manual,
|
|
196
|
+
// cited on the entry itself).
|
|
153
197
|
// ---------------------------------------------------------------------------
|
|
154
198
|
|
|
155
199
|
/** The parameter kind a spec entry declares. "count" bounds a decimal
|
|
@@ -175,16 +219,241 @@ function renderAddressParam(verb: string, value: number): string {
|
|
|
175
219
|
return `${verb} $${value.toString(16).padStart(4, "0")}`;
|
|
176
220
|
}
|
|
177
221
|
|
|
222
|
+
/**
|
|
223
|
+
* The CLOSED set of committed fixtures the `load` widening below may load,
|
|
224
|
+
* as an id -> BASENAME table (plan 50-04 committed the first member; plan
|
|
225
|
+
* 50-05 turned the single constant into this table).
|
|
226
|
+
*
|
|
227
|
+
* WHY A TABLE AND NOT ONE CONSTANT PER SUBJECT. Plan 50-04's own note here
|
|
228
|
+
* said a later plan adding a second committed subject would add "its OWN new
|
|
229
|
+
* constant and its OWN new TEXT_COMMAND_PARAM_SPECS entry". Plan 50-05 is
|
|
230
|
+
* that later plan, and plan 50-06 needs two more. Three hand-copied
|
|
231
|
+
* constants, three hand-copied spec entries and three hand-copied verb
|
|
232
|
+
* strings in text-tools.ts is three chances to mis-copy a path and load the
|
|
233
|
+
* WRONG subject into a capture that then silently becomes evidence for the
|
|
234
|
+
* wrong binary. The table removes that class of mistake: adding a subject is
|
|
235
|
+
* ONE reviewed row here, and every spec entry, every verb string and the
|
|
236
|
+
* tool's own accepted id set are all derived from it.
|
|
237
|
+
*
|
|
238
|
+
* WHAT DID NOT CHANGE, AND MUST NOT. Every path here is still a reviewed
|
|
239
|
+
* literal chosen by THIS file. A caller never supplies a path, a basename or
|
|
240
|
+
* any fragment of one. The tool's `subject` argument (text-tools.ts) is an
|
|
241
|
+
* enumerated ID that is looked up in this frozen table by exact membership
|
|
242
|
+
* and is NEVER concatenated into a command string -- an id this table does
|
|
243
|
+
* not carry is refused BY NAME, so the set of loadable files stays exactly
|
|
244
|
+
* as closed as it was when it held one entry. TextCommandParamKind is
|
|
245
|
+
* likewise untouched: the only caller-supplied VALUE is still the bounded
|
|
246
|
+
* device NUMBER, and no parameter kind in this module accepts a string
|
|
247
|
+
* domain (text-protocol.test.ts pins both facts, at runtime and at source
|
|
248
|
+
* level).
|
|
249
|
+
*
|
|
250
|
+
* WHAT NOT TO DO: do not add a function that builds a path from caller
|
|
251
|
+
* input, and do not widen a row into anything a caller can steer. Every row
|
|
252
|
+
* below is a REVIEWED LITERAL spelled out in this file, whole.
|
|
253
|
+
*
|
|
254
|
+
* WHY THE ROWS CARRY A WHOLE REPO-RELATIVE PATH AND NOT A BARE BASENAME
|
|
255
|
+
* (plan 50-06). Plan 50-05 wrote each row as a basename and joined a single
|
|
256
|
+
* fixed `src/mcp/vice/fixtures/hazard-subject` directory onto it, and said
|
|
257
|
+
* in this very comment that a build artifact outside that directory "gets a
|
|
258
|
+
* reviewed row of its own, spelled out here the same way". Plan 50-06 is the
|
|
259
|
+
* plan with that artifact: its rebuild `.prg` is produced from the committed
|
|
260
|
+
* annotation store and lands under the PHASE EVIDENCE directory, not the
|
|
261
|
+
* fixture directory, because it is an output of this phase rather than a
|
|
262
|
+
* committed fixture. A basename-plus-fixed-directory row cannot spell that,
|
|
263
|
+
* so the row now carries the whole repo-relative path as an array of
|
|
264
|
+
* reviewed segments. Nothing about the CLOSURE changed: the path is still
|
|
265
|
+
* chosen entirely by this file, a caller still supplies no path, no
|
|
266
|
+
* basename, no directory and no fragment of one, and the only thing a
|
|
267
|
+
* caller ever names is an id this table's own keys define.
|
|
268
|
+
*
|
|
269
|
+
* `misaligned` is deliberately ABSENT: `hazard-subject-misaligned.prg` is a
|
|
270
|
+
* committed fixture, but no plan loads it into a running emulator -- it is
|
|
271
|
+
* consumed offline by the hazard-report gate. The set is what is actually
|
|
272
|
+
* dialed, not every fixture that happens to exist, and a committed test
|
|
273
|
+
* asserts it stays undialable so the boundary is this table rather than a
|
|
274
|
+
* directory.
|
|
275
|
+
*/
|
|
276
|
+
export const HAZARD_SUBJECT_PRG_RELPATHS = Object.freeze({
|
|
277
|
+
/** Plan 50-04's tracer-slice subject -- the original. */
|
|
278
|
+
original: Object.freeze(["src", "mcp", "vice", "fixtures", "hazard-subject", "hazard-subject.prg"]),
|
|
279
|
+
/** Plan 50-02's regressed twin: three planted single-bit regressions at
|
|
280
|
+
* the immediates feeding $D020, $D015 and $D018. Plan 50-05's red
|
|
281
|
+
* control. */
|
|
282
|
+
regressed: Object.freeze(["src", "mcp", "vice", "fixtures", "hazard-subject", "hazard-subject-regressed.prg"]),
|
|
283
|
+
/** Plan 50-02's modified subject: one behaviour removed and one added.
|
|
284
|
+
* Plan 50-06's modifiability observation. */
|
|
285
|
+
modified: Object.freeze(["src", "mcp", "vice", "fixtures", "hazard-subject", "hazard-subject-modified.prg"]),
|
|
286
|
+
/** Plan 50-06's REBUILD: the committed subject re-produced from its own
|
|
287
|
+
* committed annotation store through importStoreDocument() ->
|
|
288
|
+
* exportAsmTree() -> verifyAcmeAssemblesTree(), recorded in
|
|
289
|
+
* `.planning/phases/50-equivalence-and-modifiability/evidence/REBUILD.md`.
|
|
290
|
+
* The only row that is not a committed fixture, and the reason the rows
|
|
291
|
+
* carry a whole repo-relative path -- see this table's own comment. */
|
|
292
|
+
rebuild: Object.freeze([
|
|
293
|
+
".planning",
|
|
294
|
+
"phases",
|
|
295
|
+
"50-equivalence-and-modifiability",
|
|
296
|
+
"evidence",
|
|
297
|
+
"hazard-subject-rebuild.prg",
|
|
298
|
+
]),
|
|
299
|
+
/** Plan 50-08's exported-edit subject: the same one-behaviour-removed,
|
|
300
|
+
* one-behaviour-added pair `modified` carries, made this time in a file
|
|
301
|
+
* `exportAsmTree()` itself emitted (`scope_087a.a`) rather than in the
|
|
302
|
+
* hand-written `modified` fixture family, reassembled through the same
|
|
303
|
+
* single oracle against a pre-registered byte manifest committed at
|
|
304
|
+
* `fixtures/hazard-subject/exported-edit.manifest.json`. See
|
|
305
|
+
* `docs/phase50-exported-edit-findings.md` and
|
|
306
|
+
* `docs/phase50-exported-modifiability-transcript.md`. */
|
|
307
|
+
"exported-edit": Object.freeze([
|
|
308
|
+
"src",
|
|
309
|
+
"mcp",
|
|
310
|
+
"vice",
|
|
311
|
+
"fixtures",
|
|
312
|
+
"hazard-subject",
|
|
313
|
+
"hazard-subject-exported-edit.prg",
|
|
314
|
+
]),
|
|
315
|
+
} as const);
|
|
316
|
+
|
|
317
|
+
/** The id half of HAZARD_SUBJECT_PRG_RELPATHS -- the only thing a caller
|
|
318
|
+
* ever names, and never a path. */
|
|
319
|
+
export type HazardSubjectId = keyof typeof HAZARD_SUBJECT_PRG_RELPATHS;
|
|
320
|
+
|
|
321
|
+
/** The frozen id list, in table order. Exported so text-tools.ts can state
|
|
322
|
+
* the accepted set in its refusal message without re-typing it. */
|
|
323
|
+
export const HAZARD_SUBJECT_IDS: readonly HazardSubjectId[] = Object.freeze(
|
|
324
|
+
Object.keys(HAZARD_SUBJECT_PRG_RELPATHS) as HazardSubjectId[],
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
/** Each row's own last segment, DERIVED from the table above rather than
|
|
328
|
+
* spelled a second time. Preserved by name because committed tests already
|
|
329
|
+
* bind it, and because "which file does this id name" is a question worth
|
|
330
|
+
* answering without re-walking the path. */
|
|
331
|
+
export const HAZARD_SUBJECT_PRG_BASENAMES: Readonly<Record<HazardSubjectId, string>> = Object.freeze(
|
|
332
|
+
Object.fromEntries(
|
|
333
|
+
HAZARD_SUBJECT_IDS.map((id) => {
|
|
334
|
+
const segments = HAZARD_SUBJECT_PRG_RELPATHS[id];
|
|
335
|
+
return [id, segments[segments.length - 1]] as const;
|
|
336
|
+
}),
|
|
337
|
+
) as Record<HazardSubjectId, string>,
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
/** True only for an id this table actually carries. The ONE membership test
|
|
341
|
+
* -- `Object.keys`-derived rather than a prototype lookup, so an inherited
|
|
342
|
+
* name ("constructor", "__proto__", "toString") can never test true. */
|
|
343
|
+
export function isHazardSubjectId(value: unknown): value is HazardSubjectId {
|
|
344
|
+
return typeof value === "string" && (HAZARD_SUBJECT_IDS as readonly string[]).includes(value);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Absolute host path to one member of the closed table above. Three members
|
|
349
|
+
* are committed fixtures; `rebuild` is plan 50-06's own build artifact under
|
|
350
|
+
* the phase evidence directory, which is why the table's rows carry a whole
|
|
351
|
+
* repo-relative path rather than a basename joined onto one fixed directory.
|
|
352
|
+
*
|
|
353
|
+
* Resolved through repoRoot() rather than hard-coded as a relative string:
|
|
354
|
+
* `broker-launch.mts` spawns `x64sc` with no explicit `cwd` (checked
|
|
355
|
+
* directly in this session -- no `cwd` option anywhere in that file), so a
|
|
356
|
+
* repo-relative string would resolve against whatever directory the broker
|
|
357
|
+
* process itself happened to be started from, not necessarily this
|
|
358
|
+
* repository. An absolute path removes that ambiguity. Residual, stated
|
|
359
|
+
* risk (not solved here): this is the CONTAINER-side path as seen by this
|
|
360
|
+
* Node process; on a genuinely containerized deployment (this project has
|
|
361
|
+
* none today -- host-developed, no devcontainer) the host process actually
|
|
362
|
+
* running `x64sc` would need this translated through `hostpath.ts` first.
|
|
363
|
+
* That translation is deliberately NOT added here, matching this project's
|
|
364
|
+
* existing "solve the general host/container case only where it is
|
|
365
|
+
* actually exercised" discipline -- a later plan that runs this widening
|
|
366
|
+
* through a real container split adds it then.
|
|
367
|
+
*/
|
|
368
|
+
export function hazardSubjectPrgPath(id: HazardSubjectId): string {
|
|
369
|
+
return join(repoRoot(), ...HAZARD_SUBJECT_PRG_RELPATHS[id]);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** THE ONE place a `load "<path>"` verb string is spelled, for any subject.
|
|
373
|
+
* Both the TEXT_COMMAND_PARAM_SPECS keys below and text-tools.ts's own
|
|
374
|
+
* lookup go through this function, so the table key and the dialed verb can
|
|
375
|
+
* never drift apart into two literals that differ by a character. */
|
|
376
|
+
export function hazardSubjectLoadVerb(id: HazardSubjectId): string {
|
|
377
|
+
return `load "${hazardSubjectPrgPath(id)}"`;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** Plan 50-04's original constant, preserved by name and by value: it is
|
|
381
|
+
* exactly the `original` member of the table above. Kept because several
|
|
382
|
+
* committed tests and text-tools.ts already bind this name, and because the
|
|
383
|
+
* default subject is still the original. */
|
|
384
|
+
export const HAZARD_SUBJECT_PRG_PATH: string = hazardSubjectPrgPath("original");
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* One frozen `load "<path>"` spec per member of HAZARD_SUBJECT_PRG_RELPATHS,
|
|
388
|
+
* derived from that closed table rather than hand-copied per subject (plan
|
|
389
|
+
* 50-05). Every entry is identical except for the reviewed path baked into
|
|
390
|
+
* its own key: same "count" kind, same 0-11 device bound, same renderer. The
|
|
391
|
+
* DERIVATION is the point -- a hand-copied entry per subject is how a path
|
|
392
|
+
* and its renderer drift apart, and a renderer that disagrees with its own
|
|
393
|
+
* key fails isDialableTextCommandForVerb()'s round trip and refuses the
|
|
394
|
+
* command outright, which is a confusing way to discover a typo.
|
|
395
|
+
*/
|
|
396
|
+
const HAZARD_SUBJECT_LOAD_SPECS: Readonly<Record<string, TextCommandParamSpec>> = Object.freeze(
|
|
397
|
+
Object.fromEntries(
|
|
398
|
+
HAZARD_SUBJECT_IDS.map((id) => {
|
|
399
|
+
const verb = hazardSubjectLoadVerb(id);
|
|
400
|
+
return [
|
|
401
|
+
verb,
|
|
402
|
+
Object.freeze({
|
|
403
|
+
kind: "count",
|
|
404
|
+
min: 0,
|
|
405
|
+
max: 11,
|
|
406
|
+
render: (value: number) => renderCountParam(verb, value),
|
|
407
|
+
} satisfies TextCommandParamSpec),
|
|
408
|
+
] as const;
|
|
409
|
+
}),
|
|
410
|
+
),
|
|
411
|
+
);
|
|
412
|
+
|
|
178
413
|
/**
|
|
179
414
|
* Frozen, per-verb parameter specs (D-42-1). Keyed by the verb exactly as
|
|
180
|
-
* it appears in TEXT_COMMAND_ALLOWLIST above
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
415
|
+
* it appears in TEXT_COMMAND_ALLOWLIST above -- with ONE exception, the
|
|
416
|
+
* `load` family (plan 50-04, one entry per committed subject since plan
|
|
417
|
+
* 50-05), whose keys are full frozen literals that already embed their own
|
|
418
|
+
* filename argument (see below); none of them ever appears in
|
|
419
|
+
* TEXT_COMMAND_ALLOWLIST as a bare entry, because `load "<file>"` with no
|
|
420
|
+
* device number is not valid VICE syntax on its own (`mon_parse.y`'s
|
|
421
|
+
* `disk_rules: CMD_LOAD filename device_num opt_address` requires the
|
|
422
|
+
* device number) -- unlike "chis"/"prof flat"/"io", which ARE independently
|
|
423
|
+
* valid bare and so are also listed in TEXT_COMMAND_ALLOWLIST.
|
|
424
|
+
*
|
|
425
|
+
* A verb with no entry here takes no parameter -- it keeps dialing its bare
|
|
426
|
+
* frozen literal, unchanged (the three no-parameter verbs -- "device c:",
|
|
427
|
+
* "warp on", "warp off" -- are deliberately absent). The count bound is 1
|
|
428
|
+
* through 65535 for "chis"/"prof flat": one because a zero-row request is
|
|
429
|
+
* not a request, and 65535 because that is the same 16-bit domain the
|
|
430
|
+
* CPU-history count lives in on this machine (CPUHISTORY_GET's own count
|
|
431
|
+
* field, monitor_binary.c:1492). The address bound for "io" is 0 through
|
|
432
|
+
* 65535, the full 16-bit machine address space.
|
|
433
|
+
*
|
|
434
|
+
* `load "<one committed subject path>"` (plan 50-04, 2026-09-15; one entry
|
|
435
|
+
* per subject since plan 50-05): the single bounded value is still the
|
|
436
|
+
* DEVICE NUMBER, per VICE's own documented syntax
|
|
437
|
+
* (`load "<filename>" <device> [<address>]`, VICE Manual ch. 12 -- "If
|
|
438
|
+
* device is 0, the file is read from the file system"). The address
|
|
439
|
+
* argument is deliberately never offered here: omitting it makes VICE use
|
|
440
|
+
* the load address embedded in the `.prg` file's own two-byte header, which
|
|
441
|
+
* is exactly what a committed machine-code fixture needs and removes a
|
|
442
|
+
* second numeric slot this project would otherwise have to bound and
|
|
443
|
+
* justify for no present use. This reuses the SAME "count" kind and the
|
|
444
|
+
* SAME `${verb} ${value}` rendering `renderCountParam()` already produces
|
|
445
|
+
* for "chis"/"prof flat" -- no new TextCommandParamKind, no new render
|
|
446
|
+
* shape; only the verb string itself is new, and it is a reviewed literal,
|
|
447
|
+
* never a caller-supplied string, and the SUBJECT is chosen by an
|
|
448
|
+
* enumerated id looked up in that same frozen table, never by a path a
|
|
449
|
+
* caller passes in (see TEXT_COMMAND_ALLOWLIST's own `load` paragraph above
|
|
450
|
+
* and HAZARD_SUBJECT_PRG_RELPATHS). Bound 0 through 11: 0 is the one value this phase
|
|
451
|
+
* exercises (host filesystem, per the manual quote above); 1 through 11
|
|
452
|
+
* spans this project's own documented device-number range elsewhere
|
|
453
|
+
* (CLAUDE.md's wire memspace note: units 8-11 are the four IEC disk
|
|
454
|
+
* drives this codebase ever names) -- a deliberately narrow bound, not the
|
|
455
|
+
* full addressable device range VICE itself accepts, because nothing in
|
|
456
|
+
* this project has a reason to dial anything wider yet.
|
|
188
457
|
*/
|
|
189
458
|
export const TEXT_COMMAND_PARAM_SPECS: Readonly<Record<string, TextCommandParamSpec>> = Object.freeze({
|
|
190
459
|
chis: Object.freeze({
|
|
@@ -205,6 +474,7 @@ export const TEXT_COMMAND_PARAM_SPECS: Readonly<Record<string, TextCommandParamS
|
|
|
205
474
|
max: 65535,
|
|
206
475
|
render: (value: number) => renderAddressParam("io", value),
|
|
207
476
|
}),
|
|
477
|
+
...HAZARD_SUBJECT_LOAD_SPECS,
|
|
208
478
|
} satisfies Record<string, TextCommandParamSpec>);
|
|
209
479
|
|
|
210
480
|
function isSafeIntegerNumber(value: unknown): value is number {
|
package/text-tools.ts
CHANGED
|
@@ -69,7 +69,15 @@
|
|
|
69
69
|
// withChannelLockHeld()'s own discipline in stock-dispatch.ts.
|
|
70
70
|
// - Never embed a phase number in any string or template literal here.
|
|
71
71
|
import { textConnect, textDisconnect } from "./text-connect.ts";
|
|
72
|
-
import {
|
|
72
|
+
import {
|
|
73
|
+
withTextChannelLock,
|
|
74
|
+
buildTextCommand,
|
|
75
|
+
hazardSubjectLoadVerb,
|
|
76
|
+
isHazardSubjectId,
|
|
77
|
+
HAZARD_SUBJECT_IDS,
|
|
78
|
+
type HazardSubjectId,
|
|
79
|
+
type TextMonitorClient,
|
|
80
|
+
} from "./text-protocol.ts";
|
|
73
81
|
import { MonitorOwnershipError } from "./vice-broker-client.ts";
|
|
74
82
|
import { ChannelLockTimeoutError } from "./channel-lock.ts";
|
|
75
83
|
import { isErrorText, derivedAnswer, convertHandshakeError, convertWireError, type StockToolResult } from "./stock-handler.ts";
|
|
@@ -776,3 +784,100 @@ export async function handleIoRegisters(args: Record<string, unknown>, deps: Sto
|
|
|
776
784
|
});
|
|
777
785
|
});
|
|
778
786
|
}
|
|
787
|
+
|
|
788
|
+
// ---------------------------------------------------------------------------
|
|
789
|
+
// Plan 50-04 (route-d): the tool that reaches text-protocol.ts's widened
|
|
790
|
+
// `load` verb. See TEXT_COMMAND_ALLOWLIST's own `load` paragraph and
|
|
791
|
+
// TEXT_COMMAND_PARAM_SPECS's own `load` entry (both text-protocol.ts) for
|
|
792
|
+
// the full rationale this handler leans on without repeating it.
|
|
793
|
+
// ---------------------------------------------------------------------------
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* The subject loaded when the caller names none. Plan 50-04's behaviour,
|
|
797
|
+
* unchanged: an omitted `subject` dials exactly the path that plan's single
|
|
798
|
+
* frozen entry dialed.
|
|
799
|
+
*/
|
|
800
|
+
const DEFAULT_SUBJECT: HazardSubjectId = "original";
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Resolve the caller's `subject` argument to one of text-protocol.ts's own
|
|
804
|
+
* frozen ids, or to `null` for anything else.
|
|
805
|
+
*
|
|
806
|
+
* WHY THIS IS NOT A FILENAME PARAMETER, AND MUST NEVER BECOME ONE. The value
|
|
807
|
+
* a caller supplies here is an ID, checked for exact membership in
|
|
808
|
+
* HAZARD_SUBJECT_IDS and then used only as a LOOKUP KEY -- it is never
|
|
809
|
+
* concatenated into a command string, never joined onto a path, and never
|
|
810
|
+
* reaches the socket in any form. The dialed verb is built by
|
|
811
|
+
* hazardSubjectLoadVerb() from the reviewed literal text-protocol.ts's own
|
|
812
|
+
* closed table carries. So the set of host files this tool can ever load is
|
|
813
|
+
* exactly that table, whatever a caller sends. An unrecognised id is refused
|
|
814
|
+
* BY NAME before any text-monitor byte is written, the same way
|
|
815
|
+
* buildTextCommand() refuses an out-of-bounds device.
|
|
816
|
+
*/
|
|
817
|
+
function resolveSubjectId(raw: unknown): HazardSubjectId | null {
|
|
818
|
+
if (raw === undefined) return DEFAULT_SUBJECT;
|
|
819
|
+
return isHazardSubjectId(raw) ? raw : null;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* `vice_program_load` -- the shipped tool that reaches plan 50-04's widened
|
|
824
|
+
* `load` verb (route-d, `.planning/phases/50-equivalence-and-modifiability/evidence/LOAD-ROUTE.md`).
|
|
825
|
+
* Dials VICE's text-monitor `load "<file>" <device>` command for ONE member
|
|
826
|
+
* of text-protocol.ts's closed HAZARD_SUBJECT_PRG_RELPATHS table, each
|
|
827
|
+
* baked into its own frozen allowlist identity -- this handler takes NO
|
|
828
|
+
* filename argument at all, so there is nothing here for a caller to
|
|
829
|
+
* inject; the loaded path can never be anything other than a reviewed
|
|
830
|
+
* literal that table already carries.
|
|
831
|
+
*
|
|
832
|
+
* Takes two OPTIONAL parameters. `subject` is an enumerated id from that
|
|
833
|
+
* table ("original", "regressed", "modified", "rebuild"). It defaults to
|
|
834
|
+
* "original", which is exactly plan 50-04's behaviour, and an id the table
|
|
835
|
+
* does not carry is refused by name (see resolveSubjectId() above for why an
|
|
836
|
+
* id is not a filename). "rebuild" (plan 50-06) is the one id whose row
|
|
837
|
+
* resolves outside the fixture directory -- a build artifact under the phase
|
|
838
|
+
* evidence directory -- which is why the table's rows carry a whole
|
|
839
|
+
* repo-relative path. `device`: an omitted device defaults
|
|
840
|
+
* to 0 ("the file is read from the file system", VICE Manual ch. 12).
|
|
841
|
+
* `buildTextCommand()` alone validates and bounds the device (0 through 11,
|
|
842
|
+
* TEXT_COMMAND_PARAM_SPECS's own entry for this verb) -- this handler
|
|
843
|
+
* duplicates no bound, mirroring `handleCpuHistory()`'s own discipline of
|
|
844
|
+
* never re-stating a spec's own bound in a second place.
|
|
845
|
+
*
|
|
846
|
+
* No address argument is offered, and none ever will be through this tool:
|
|
847
|
+
* omitting it makes VICE use the load address embedded in the `.prg` file's
|
|
848
|
+
* own two-byte header, which is exactly what a committed machine-code
|
|
849
|
+
* fixture needs -- text-protocol.ts's own `load` entry documents this same
|
|
850
|
+
* choice and why a second numeric slot is not worth bounding for no present
|
|
851
|
+
* use.
|
|
852
|
+
*/
|
|
853
|
+
export async function handleProgramLoad(args: Record<string, unknown>, deps: StockDispatchDeps): Promise<StockToolResult> {
|
|
854
|
+
const { device, subject } = args;
|
|
855
|
+
const subjectId = resolveSubjectId(subject);
|
|
856
|
+
if (subjectId === null) {
|
|
857
|
+
return isErrorText(
|
|
858
|
+
`vice_program_load: "subject" must be one of ${HAZARD_SUBJECT_IDS.map((id) => JSON.stringify(id)).join(", ")} ` +
|
|
859
|
+
`(got ${JSON.stringify(subject)}) -- refusing before any text-monitor byte is written; this tool never accepts a filename`,
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
const resolvedDevice = device === undefined ? 0 : device;
|
|
863
|
+
const built = buildTextCommand(hazardSubjectLoadVerb(subjectId), resolvedDevice);
|
|
864
|
+
if (!built.ok) {
|
|
865
|
+
return isErrorText(`vice_program_load: ${built.message} -- refusing before any text-monitor byte is written`);
|
|
866
|
+
}
|
|
867
|
+
const command = built.command;
|
|
868
|
+
|
|
869
|
+
return withTextTool("vice_program_load", deps, async (client) => {
|
|
870
|
+
const response = await client.command(command);
|
|
871
|
+
return derivedAnswer({
|
|
872
|
+
command,
|
|
873
|
+
device: resolvedDevice,
|
|
874
|
+
subject: subjectId,
|
|
875
|
+
response,
|
|
876
|
+
note:
|
|
877
|
+
`loads the reviewed Phase 50 hazard subject "${subjectId}", baked into this verb's own frozen ` +
|
|
878
|
+
"identity (plan 50-04 route-d; one frozen verb per subject since plan 50-05) -- no filename is ever " +
|
|
879
|
+
"caller-supplied, only an enumerated subject id; the load address comes from the .prg file's own " +
|
|
880
|
+
"two-byte header, since no address argument is offered",
|
|
881
|
+
});
|
|
882
|
+
});
|
|
883
|
+
}
|
|
@@ -4505,6 +4505,69 @@
|
|
|
4505
4505
|
"runState"
|
|
4506
4506
|
]
|
|
4507
4507
|
}
|
|
4508
|
+
},
|
|
4509
|
+
{
|
|
4510
|
+
"name": "vice_program_load",
|
|
4511
|
+
"description": "Stock-only, no fork counterpart -- reaches VICE's text monitor over the -remotemonitor channel and halts the machine for the duration of the command, exactly as a binary-monitor command does. Issues \"load \\\"<path>\\\" <device>\" for ONE member of a closed, reviewed Phase 50 hazard-subject table -- four committed fixtures under fixtures/hazard-subject/ plus the rebuild produced under the phase evidence directory. Every loadable path is baked into this tool's own frozen dialed verbs and is never a caller-supplied argument, so there is nothing here for a caller to inject. Takes an OPTIONAL \"subject\", an enumerated subject id, defaulting to \"original\". Takes an OPTIONAL \"device\" number, 0 through 11; an omitted device defaults to 0 (\"the file is read from the file system\", VICE Manual ch. 12). No address argument is offered -- omitting it makes VICE use the load address embedded in the .prg file's own two-byte header, which is exactly what these committed machine-code fixtures need.",
|
|
4512
|
+
"inputSchema": {
|
|
4513
|
+
"type": "object",
|
|
4514
|
+
"properties": {
|
|
4515
|
+
"device": {
|
|
4516
|
+
"type": "integer",
|
|
4517
|
+
"minimum": 0,
|
|
4518
|
+
"maximum": 11,
|
|
4519
|
+
"description": "OPTIONAL. IEC device number VICE loads from. Defaults to 0 (the host filesystem) when omitted. 1 through 11 span this project's own documented device-number range elsewhere; this tool never accepts a filename."
|
|
4520
|
+
},
|
|
4521
|
+
"subject": {
|
|
4522
|
+
"type": "string",
|
|
4523
|
+
"enum": [
|
|
4524
|
+
"original",
|
|
4525
|
+
"regressed",
|
|
4526
|
+
"modified",
|
|
4527
|
+
"rebuild",
|
|
4528
|
+
"exported-edit"
|
|
4529
|
+
],
|
|
4530
|
+
"description": "OPTIONAL. Which reviewed Phase 50 hazard subject to load, named by id, never by path. \"original\" is the tracer-slice subject and is the default. \"regressed\" is its three-single-bit-regression twin. \"modified\" is the one-behaviour-removed, one-behaviour-added variant made by hand in the fixture's own source. \"rebuild\" is the original re-produced from its own committed annotation store, and is the one id that resolves outside the fixture directory. \"exported-edit\" is the same one-behaviour-removed, one-behaviour-added pair made instead in a file exportAsmTree() itself emitted, reassembled against a pre-registered byte manifest. This tool never accepts a filename."
|
|
4531
|
+
}
|
|
4532
|
+
},
|
|
4533
|
+
"additionalProperties": false
|
|
4534
|
+
},
|
|
4535
|
+
"outputSchema": {
|
|
4536
|
+
"type": "object",
|
|
4537
|
+
"properties": {
|
|
4538
|
+
"command": {
|
|
4539
|
+
"type": "string"
|
|
4540
|
+
},
|
|
4541
|
+
"device": {
|
|
4542
|
+
"type": "integer"
|
|
4543
|
+
},
|
|
4544
|
+
"response": {
|
|
4545
|
+
"type": "string"
|
|
4546
|
+
},
|
|
4547
|
+
"note": {
|
|
4548
|
+
"type": "string"
|
|
4549
|
+
},
|
|
4550
|
+
"runState": {
|
|
4551
|
+
"type": "string",
|
|
4552
|
+
"enum": [
|
|
4553
|
+
"running",
|
|
4554
|
+
"stopped",
|
|
4555
|
+
"unknown"
|
|
4556
|
+
]
|
|
4557
|
+
},
|
|
4558
|
+
"subject": {
|
|
4559
|
+
"type": "string"
|
|
4560
|
+
}
|
|
4561
|
+
},
|
|
4562
|
+
"required": [
|
|
4563
|
+
"command",
|
|
4564
|
+
"device",
|
|
4565
|
+
"subject",
|
|
4566
|
+
"response",
|
|
4567
|
+
"note",
|
|
4568
|
+
"runState"
|
|
4569
|
+
]
|
|
4570
|
+
}
|
|
4508
4571
|
}
|
|
4509
4572
|
]
|
|
4510
4573
|
}
|