@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.
Files changed (77) 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 +1674 -99
  5. package/anno-enum-gen.ts +416 -30
  6. package/anno-export-asm.ts +1175 -89
  7. package/anno-graphics.ts +338 -0
  8. package/anno-hazard-report.ts +1367 -0
  9. package/anno-import.ts +495 -0
  10. package/anno-join.ts +480 -0
  11. package/anno-provenance-ledger.ts +472 -0
  12. package/anno-register.ts +159 -0
  13. package/anno-store-export.ts +661 -0
  14. package/anno-store.ts +518 -2
  15. package/anno-tools.ts +1169 -16
  16. package/anno-types.ts +275 -2
  17. package/backend-detect.mts +124 -312
  18. package/build.ts +3 -1
  19. package/capture-predicate.ts +597 -0
  20. package/channel-lock.ts +349 -0
  21. package/evid-ingest.ts +217 -0
  22. package/evid-reconcile.ts +316 -0
  23. package/host-tool-client.ts +430 -0
  24. package/incident-record.ts +23 -12
  25. package/install-resources.ts +29 -13
  26. package/memmap-lookup.ts +285 -0
  27. package/package.json +27 -8
  28. package/prg-image.ts +1 -2
  29. package/repo-root.ts +87 -3
  30. package/resources/backend-detect.mjs +98 -236
  31. package/resources/broker-control.mjs +189 -16
  32. package/resources/broker-epoch.mjs +1 -1
  33. package/resources/broker-kill.mjs +8 -2
  34. package/resources/broker-launch.mjs +365 -210
  35. package/resources/broker-state.mjs +64 -18
  36. package/resources/container-guard.mjs +1 -1
  37. package/resources/ghidra-project.mjs +790 -0
  38. package/resources/host-tool.mjs +2561 -0
  39. package/resources/vice-broker.mjs +330 -184
  40. package/resources/vice-launcher.sh +127 -9
  41. package/stock-address.ts +1 -1
  42. package/stock-condition.ts +1 -1
  43. package/stock-connect.ts +9 -5
  44. package/stock-derived.ts +29 -37
  45. package/stock-diagnose.ts +200 -36
  46. package/stock-dispatch.ts +179 -77
  47. package/stock-handler.ts +1 -1
  48. package/stock-paths.ts +18 -14
  49. package/stock-petscii.ts +1 -1
  50. package/stock-protocol.ts +1 -1
  51. package/stock-recycle.ts +83 -2
  52. package/stock-reproducible-run.ts +811 -0
  53. package/stock-run-until.ts +100 -1
  54. package/stock-symbols.ts +4 -4
  55. package/stock-timing.ts +1 -1
  56. package/stop-oracle.ts +167 -0
  57. package/text-capability-probe.ts +660 -0
  58. package/text-connect.ts +157 -0
  59. package/text-protocol.ts +810 -0
  60. package/text-tools.ts +778 -0
  61. package/textmon-backtrace.ts +385 -0
  62. package/textmon-cpuhistory.ts +335 -0
  63. package/textmon-memmap.ts +494 -0
  64. package/textmon-profile.ts +458 -0
  65. package/textmon-registers.ts +748 -0
  66. package/tools-manifest.stock.json +864 -3
  67. package/vice-broker-client.ts +189 -42
  68. package/vice-errors.ts +268 -0
  69. package/vice-proxy.ts +339 -2144
  70. package/vsf-slice.ts +640 -0
  71. package/anno-d64.ts +0 -310
  72. package/capability-registry.ts +0 -390
  73. package/refresh-manifest.ts +0 -124
  74. package/tools-manifest.json +0 -1223
  75. package/vice-probe.ts +0 -278
  76. package/vice-sync.ts +0 -336
  77. package/vice.ts +0 -772
@@ -49,6 +49,10 @@ import { parseAddress } from "./stock-address.ts";
49
49
  import { stockAnswer, isErrorText, convertWireError, type StockSessionHandler } from "./stock-handler.ts";
50
50
  import { readProgramCounter } from "./stock-timing.ts";
51
51
  import { runStateFor } from "./stock-runstate.ts";
52
+ // The reproducible-run protocol (REPRO-02). Imported for exactly one branch
53
+ // below -- this module is the ONLY non-test caller of runReproducible() in the
54
+ // tree, which is the single-seam property REPRO-02 requires.
55
+ import { runReproducible, REPRODUCIBLE_RUN_REQUIRED_SIBLINGS } from "./stock-reproducible-run.ts";
52
56
 
53
57
  /** True iff `value` is a well-formed, generic JSON object -- not null, not
54
58
  * an array. Matches this module tree's own isPlainObject() convention
@@ -74,6 +78,24 @@ export const RUN_UNTIL_DEFAULT_TIMEOUT_MS = 30000;
74
78
  * value is. */
75
79
  export const RUN_UNTIL_MAX_TIMEOUT_MS = 600000;
76
80
 
81
+ /** EVERY argument name `vice_run_until` accepts, and the ONLY definition of
82
+ * that set. Anything else is refused BY NAME (see handleRunUntil below).
83
+ *
84
+ * Hoisted to module scope and EXPORTED so `D-13`'s no-sub-flags rule is
85
+ * ASSERTED rather than merely stated: `stock-reproducible-run.test.ts` pins
86
+ * this array with a single `assert.deepEqual`, which is what makes a future
87
+ * `skip_reset`, `no_anchor` or `reset_only` a RED TEST rather than a review
88
+ * comment. Shipping a "protocol without the reset" option would ship exactly
89
+ * the second route `REPRO-02` exists to prevent a caller forgetting -- and the
90
+ * reset-removed CONTROL this phase records red is produced by an evidence
91
+ * script calling `runReproducible()`'s pieces directly, never by a published
92
+ * flag.
93
+ *
94
+ * `reproducible` and `frame_anchor` are STOCK-ONLY and both OPTIONAL: the fork
95
+ * manifest's tool list is frozen byte-identical from v0.1.x, and stock may add
96
+ * optional parameters but never removes, retypes, or newly-requires one. */
97
+ export const RUN_UNTIL_KEYS: readonly string[] = ["address", "cycles", "timeout_ms", "reproducible", "frame_anchor"];
98
+
77
99
  /** Narrows an emitted `event` item to a CHECKPOINT_INFO event -- checked on
78
100
  * the parsed item's own `.type` discriminant, never on response type alone
79
101
  * (CHECKPOINT_INFO (0x11) shares a response type with a legitimate command
@@ -152,7 +174,6 @@ export const handleRunUntil: StockSessionHandler = async (args, session, _deps)
152
174
  // stock-timing.ts). Accepting them silently means a typo -- `timeoutMs` for
153
175
  // `timeout_ms`, `addr` for `address` -- runs with the DEFAULT bound and
154
176
  // reports a confident answer, and the caller has no way to tell.
155
- const RUN_UNTIL_KEYS = ["address", "cycles", "timeout_ms"];
156
177
  const unexpectedKeys = Object.keys(args).filter((key) => !RUN_UNTIL_KEYS.includes(key));
157
178
  if (unexpectedKeys.length > 0) {
158
179
  return isErrorText(
@@ -223,6 +244,84 @@ export const handleRunUntil: StockSessionHandler = async (args, session, _deps)
223
244
  }
224
245
  }
225
246
 
247
+ // D-12: `reproducible` is a BOOLEAN. Any other type is refused naming the
248
+ // offending value -- never coerced. `reproducible: "false"` is truthy in
249
+ // JavaScript, so a coercing gate would run the whole protocol (including the
250
+ // hard reset) for a caller who wrote the string "false" meaning to disable
251
+ // it. Refusing is the only reading that cannot be wrong.
252
+ if (args.reproducible !== undefined && typeof args.reproducible !== "boolean") {
253
+ return isErrorText(
254
+ `vice_run_until: reproducible must be a boolean, got ${JSON.stringify(args.reproducible)} -- it is not coerced, because ` +
255
+ `the string "false" is truthy in JavaScript and coercing it would run the whole protocol (hard reset included) for a ` +
256
+ `caller who meant to disable it.`,
257
+ );
258
+ }
259
+
260
+ // `frame_anchor` goes through parseAddress with a `what:` label, exactly as
261
+ // `address` does, so a malformed anchor is refused with the same wording a
262
+ // malformed target gets rather than a second, divergent message.
263
+ let frameAnchor: number | undefined;
264
+ if (args.frame_anchor !== undefined) {
265
+ try {
266
+ frameAnchor = parseAddress(args.frame_anchor, { what: "vice_run_until frame_anchor" });
267
+ } catch (err) {
268
+ return isErrorText(`vice_run_until: ${describeError(err)}`);
269
+ }
270
+ }
271
+
272
+ // An ACCEPTED-AND-IGNORED argument is the exact defect the by-name gate above
273
+ // exists to prevent, so `frame_anchor` without `reproducible` is refused
274
+ // rather than silently dropped. It is not merely inert: a caller who supplies
275
+ // an anchor has asked for the anchored protocol, and answering from the
276
+ // ordinary path would report a confident `reached: true` for a run that was
277
+ // never anchored to a frame at all.
278
+ if (args.frame_anchor !== undefined && args.reproducible !== true) {
279
+ return isErrorText(
280
+ `vice_run_until: "frame_anchor" has no meaning without "reproducible": true -- the frame anchor is armed by the ` +
281
+ `reproducible protocol and by nothing else, so on the ordinary path this argument would be accepted and IGNORED while ` +
282
+ `the answer still reported a stop. Refused rather than dropped. Pass reproducible: true to run the anchored protocol, ` +
283
+ `or remove "frame_anchor".`,
284
+ );
285
+ }
286
+
287
+ // ---------------------------------------------------------------------
288
+ // THE ONE CALL SITE of the reproducible-run protocol (`REPRO-02`).
289
+ //
290
+ // One procedure, one branch, no sub-flags. `reproducible` is a
291
+ // WHOLE-PROCEDURE switch: absent or false takes the pre-existing path below
292
+ // COMPLETELY unchanged and never reaches runReproducible(). There is no
293
+ // second tool and no partial mode, because a second route is a route a
294
+ // caller can forget it took.
295
+ //
296
+ // Do not restructure the path below to "share" steps with the procedure. The
297
+ // reproducible sequence's ordering IS its content -- arm both while halted,
298
+ // hard reset, exactly one resume -- and a shared helper is how a step gets
299
+ // lifted out of that ordering by a later edit that looks like a cleanup.
300
+ // ---------------------------------------------------------------------
301
+ if (args.reproducible === true) {
302
+ // D-14's refusal, in the `cycles`-refusal register: it refuses whenever the
303
+ // protocol is requested without its required sibling, and it explains WHY
304
+ // refusing beats degrading rather than just reporting that a field is
305
+ // missing.
306
+ //
307
+ // The required name is read from REPRODUCIBLE_RUN_REQUIRED_SIBLINGS, not
308
+ // repeated as a literal here -- a second copy is how the message and the
309
+ // gate drift apart after a rename.
310
+ if (frameAnchor === undefined) {
311
+ return isErrorText(
312
+ `vice_run_until: reproducible: true requires ${REPRODUCIBLE_RUN_REQUIRED_SIBLINGS.join(", ")} -- an address executed ` +
313
+ `once per frame, whose hit count IS the frame term of the stop identity (PC, hit_count, (LIN, CYC)). No default is ` +
314
+ `supplied because the once-per-frame site is RELEASE-SPECIFIC: a cracked release almost always takes over the IRQ, ` +
315
+ `so no KERNAL site -- $EA31 included -- is safe to guess, and an anchor that never executes would bound out as a ` +
316
+ `timeout while looking like a wedge. Refusing beats silently degrading to a two-term stop identity: (LIN, CYC) is a ` +
317
+ `WITHIN-FRAME position, not a monotonic clock, so without the frame term two stops one whole frame apart certify as ` +
318
+ `the same stop. Pass frame_anchor, or drop reproducible to run the ordinary unanchored path.`,
319
+ );
320
+ }
321
+ const { result } = await runReproducible(session, { address, frameAnchor, timeoutMs, timeoutClamped });
322
+ return result;
323
+ }
324
+
226
325
  // Arm a temporary, stopping exec checkpoint at `address`. This is this
227
326
  // codebase's first caller in this tree to pass the temporary flag as
228
327
  // true: VICE itself auto-deletes a temporary checkpoint the instant it
package/stock-symbols.ts CHANGED
@@ -19,9 +19,9 @@
19
19
  // Node's `fs` inside the MCP server's OWN process; there is no wire
20
20
  // filename argument at all, so the translation does not apply and applying
21
21
  // it would read the wrong file (or nothing). hostpath-consumers.test.ts's
22
- // closed five-member production consumer set (containerpath.ts,
23
- // install-resources.ts, stock-paths.ts, vice-proxy.ts, vice-sync.ts) must
24
- // stay exactly five -- this module joining it would fail that test outright.
22
+ // closed four-member production consumer set (containerpath.ts,
23
+ // install-resources.ts, stock-paths.ts, vice-proxy.ts) must stay exactly
24
+ // four -- this module joining it would fail that test outright.
25
25
  //
26
26
  // The confirmed input format is a VICE label file, one `al C:xxxx .Name`
27
27
  // line per symbol, verified against ACME's `--vicelabels` output via
@@ -53,7 +53,7 @@
53
53
  import { readFileSync, realpathSync, statSync } from "node:fs";
54
54
  import { resolve, sep } from "node:path";
55
55
 
56
- import { ViceError, type ViceErrorOptions } from "./vice.ts";
56
+ import { ViceError, type ViceErrorOptions } from "./vice-errors.ts";
57
57
  import { repoRoot } from "./repo-root.ts";
58
58
  import { parseAddress, setSymbolResolver, type SymbolResolver } from "./stock-address.ts";
59
59
  import { derivedAnswer, isErrorText } from "./stock-handler.ts";
package/stock-timing.ts CHANGED
@@ -45,7 +45,7 @@ import {
45
45
  type ParsedCpuHistoryEntry,
46
46
  type StockProtocolError,
47
47
  } from "./stock-protocol.ts";
48
- import { MachineRestartedError } from "./vice.ts";
48
+ import { MachineRestartedError } from "./vice-errors.ts";
49
49
  import { clampCpuHistoryCount, type StockConnectSession } from "./stock-connect.ts";
50
50
  import { registerCatalogFor } from "./stock-registers.ts";
51
51
  import { stockAnswer, convertWireError, isErrorText, type StockSessionHandler, type StockOkResult } from "./stock-handler.ts";
package/stop-oracle.ts ADDED
@@ -0,0 +1,167 @@
1
+ // stop-oracle.ts -- the ONE authoritative place in this repo holding the
2
+ // STOP-IDENTITY comparison: whether two stops of the same protocol, on the same
3
+ // release, are the SAME stop. The oracle is the three-term
4
+ // `(PC, hit_count, (LIN, CYC))`, expressed here as its four scalar terms.
5
+ //
6
+ // WHAT THIS MODULE CERTIFIES, AND WHY THAT DETERMINES ITS SHAPE: it certifies a
7
+ // STOP. The captured 64K is the DEPENDENT VARIABLE -- the thing whose
8
+ // reproducibility is being claimed on the strength of the stop being identical.
9
+ // So the capture must never become a conjunct of the stop's own identity. If it
10
+ // did, two runs that stopped in genuinely different places could be certified
11
+ // "the same stop" because their memory happened to agree, and a capture would be
12
+ // participating in certifying itself. That circularity, once it has certified a
13
+ // capture, cannot be laundered back out of that capture's record -- which is why
14
+ // it is barred by SHAPE rather than by convention (`CAP-03`, `D-26`):
15
+ //
16
+ // 1. This module takes NO image buffer. No exported function here declares a
17
+ // byte-array parameter of any kind, and `capture-seam.test.ts` asserts that
18
+ // from this module's own exported signatures, over stripped code, with a
19
+ // planted oracle-shaped module as its positive control.
20
+ // 2. This module imports NOTHING from `capture-predicate.ts`, on any route,
21
+ // static or dynamic -- and that module imports nothing from here. Both
22
+ // directions are asserted, because the circularity is symmetric and a
23
+ // one-directional guard leaves half of it open.
24
+ //
25
+ // This module performs NO filesystem and NO network I/O, imports nothing at all,
26
+ // and holds no module-level mutable state. It is four scalars in, one record
27
+ // out.
28
+ //
29
+ // WHY FOUR TERMS AND NOT TWO: `(LIN, CYC)` is a WITHIN-FRAME position, not a
30
+ // monotonic clock -- stock VICE's binary monitor has no monotonic cycle register
31
+ // below 3.10 -- so two stops one whole frame apart can carry identical
32
+ // `(LIN, CYC)`. `PC` and `hit_count` are what distinguish them, and the frame
33
+ // term is what distinguishes two stops at the same PC and hit count within a
34
+ // frame. Each term is load-bearing for a different confusion, and dropping any
35
+ // one of them silently widens what counts as "the same stop".
36
+ //
37
+ // WHAT NOT TO DO:
38
+ // - Never add a parameter carrying capture bytes, a byte array, a memory
39
+ // image, or a value DERIVED from one (a digest, a differing-address count,
40
+ // an equivalence verdict). The derived-scalar route is the realistic one and
41
+ // the structural assertions cannot see it: a caller that reads the capture
42
+ // and passes a digest in here has reintroduced the circularity through a
43
+ // `number`. It is recorded as an accepted limit rather than left implicit.
44
+ // - Never import `capture-predicate.ts` here, and never reach it dynamically.
45
+ // - Never ship a two-term mode flag. `GATE-01`'s pre-mapped
46
+ // `ORACLE_NECESSITY: unproven` narrowing is implementable by READING
47
+ // `differingTerms` and treating the frame entries as recorded-not-fatal;
48
+ // `frameTermAsserted` records which form the comparison used and does not
49
+ // select one. `D-13`'s no-sub-flags rule is about the run protocol, and the
50
+ // same reasoning applies here -- a published second mode is a second route a
51
+ // caller can forget it took.
52
+ // - Never substitute a zero, a null or a sentinel for a term the caller did
53
+ // not supply. A partial stop record is REFUSED, naming the term and the
54
+ // side. Silently passing on a partial record is the one failure this module
55
+ // must not have: it would certify two stops as identical on the strength of
56
+ // the terms that happened to be present.
57
+ // - Never hardcode a wire register id for `PC`, `LIN` or `CYC` anywhere near
58
+ // this module. Register ids are not stable across builds, and
59
+ // `stock-timing.ts`'s `registerCatalogFor()` is the only route from a
60
+ // register NAME to its id. This module takes already-read numbers and never
61
+ // touches a wire.
62
+
63
+ /** The four scalar terms of the three-term stop-identity oracle
64
+ * `(PC, hit_count, (LIN, CYC))`. The frame term is a pair, which is why four
65
+ * scalars express three terms.
66
+ *
67
+ * Ordering is normative: `differingTerms` below is reported in this order, so a
68
+ * reader comparing two transcripts sees the terms in the same sequence every
69
+ * time. */
70
+ export const ORACLE_TERMS = ["pc", "hitCount", "line", "cycle"] as const;
71
+
72
+ /** One member of `ORACLE_TERMS`. */
73
+ export type OracleTerm = (typeof ORACLE_TERMS)[number];
74
+
75
+ /** One stop, as its four already-read scalar terms. No bytes, by design -- see
76
+ * this module's header. */
77
+ export interface StopIdentity {
78
+ /** The program counter at the stop. */
79
+ pc: number;
80
+ /** The checkpoint's hit count at the stop. */
81
+ hitCount: number;
82
+ /** `LIN` -- the raster line. Half of the frame term. */
83
+ line: number;
84
+ /** `CYC` -- the cycle within the raster line. The other half. */
85
+ cycle: number;
86
+ }
87
+
88
+ /** Raised when a stop record cannot be compared. Carries the term and the side
89
+ * so a caller's failure output names both rather than paraphrasing. */
90
+ export class StopOracleError extends Error {
91
+ readonly term: string;
92
+ readonly side: string;
93
+ constructor(message: string, term: string, side: string) {
94
+ super(message);
95
+ this.name = "StopOracleError";
96
+ this.term = term;
97
+ this.side = side;
98
+ }
99
+ }
100
+
101
+ /** The verdict. */
102
+ export interface StopIdentityComparison {
103
+ /** True iff every term in `ORACLE_TERMS` agrees. */
104
+ identical: boolean;
105
+ /** Which terms differ, ordered as `ORACLE_TERMS` is. */
106
+ differingTerms: string[];
107
+ /** Whether the frame term participated in the verdict.
108
+ *
109
+ * `true` for every comparison this function performs: all four terms are
110
+ * asserted, always. The field exists so `GATE-01`'s pre-mapped
111
+ * `ORACLE_NECESSITY: unproven` narrowing is implementable without a rewrite --
112
+ * under that narrowing the frame term is RECORDED but not ASSERTED, and a
113
+ * caller expresses it by reading `differingTerms` and treating the
114
+ * `line`/`cycle` entries as recorded rather than fatal. It RECORDS which form
115
+ * the comparison used; it never selects one. */
116
+ frameTermAsserted: boolean;
117
+ }
118
+
119
+ /** Every term of one side, validated. Throws naming the term and the side
120
+ * rather than returning a partial record: there is no honest way to compare
121
+ * stops when a term is absent, and no sentinel that would not be a lie. */
122
+ function requireTerms(record: StopIdentity, side: string): void {
123
+ if (record === null || typeof record !== "object") {
124
+ throw new StopOracleError(
125
+ `compareStopIdentity: side ${side} is not a stop record -- expected an object carrying ${ORACLE_TERMS.join(", ")}`,
126
+ "(whole record)",
127
+ side,
128
+ );
129
+ }
130
+ for (const term of ORACLE_TERMS) {
131
+ const value: unknown = record[term];
132
+ if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value)) {
133
+ throw new StopOracleError(
134
+ `compareStopIdentity: term "${term}" is absent or not a finite integer on side ${side} ` +
135
+ `(got ${JSON.stringify(value)}) -- refusing rather than passing on a partial stop record, which ` +
136
+ `would certify two stops as identical on the strength of the terms that happened to be present`,
137
+ term,
138
+ side,
139
+ );
140
+ }
141
+ }
142
+ }
143
+
144
+ /** Compare two stops under the three-term oracle.
145
+ *
146
+ * Symmetric in its arguments: the verdict and `differingTerms` depend only on
147
+ * WHICH terms disagree, never on which record was passed first. Two stops with
148
+ * identical `pc` and `hitCount` whose frame position is one frame apart compare
149
+ * NOT identical -- that is the frame term doing its job, and it is the whole
150
+ * reason a two-term oracle is insufficient. */
151
+ export function compareStopIdentity(a: StopIdentity, b: StopIdentity): StopIdentityComparison {
152
+ requireTerms(a, "a");
153
+ requireTerms(b, "b");
154
+
155
+ // Built by walking ORACLE_TERMS, so the reported order IS the declared order
156
+ // and cannot drift from it after an edit.
157
+ const differingTerms: string[] = [];
158
+ for (const term of ORACLE_TERMS) {
159
+ if (a[term] !== b[term]) differingTerms.push(term);
160
+ }
161
+
162
+ return {
163
+ identical: differingTerms.length === 0,
164
+ differingTerms,
165
+ frameTermAsserted: true,
166
+ };
167
+ }