@henols/vice-mcp 0.2.1 → 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 (108) hide show
  1. package/README.md +4 -3
  2. package/THIRD-PARTY-NOTICES.md +423 -25
  3. package/{r2000-acme-ident.ts → anno-acme-ident.ts} +13 -13
  4. package/anno-bank.ts +171 -0
  5. package/anno-cli.ts +3040 -0
  6. package/{r2000-confidence.ts → anno-confidence.ts} +22 -22
  7. package/anno-coverage.ts +2465 -0
  8. package/anno-derive.ts +590 -0
  9. package/anno-details.ts +169 -0
  10. package/anno-enum-gen.ts +919 -0
  11. package/anno-export-asm.ts +2396 -0
  12. package/anno-graphics.ts +338 -0
  13. package/anno-hazard-report.ts +1367 -0
  14. package/anno-import.ts +495 -0
  15. package/anno-index.ts +150 -0
  16. package/anno-join.ts +480 -0
  17. package/{r2000-memmap-render.ts → anno-memmap-render.ts} +236 -95
  18. package/anno-provenance-ledger.ts +472 -0
  19. package/{r2000-regbits-gen.ts → anno-regbits-gen.ts} +20 -15
  20. package/{r2000-regbits.json → anno-regbits.json} +2 -2
  21. package/anno-register.ts +399 -0
  22. package/anno-store-export.ts +661 -0
  23. package/anno-store.ts +4002 -0
  24. package/anno-symbols.ts +266 -0
  25. package/anno-tools.ts +3264 -0
  26. package/anno-types.ts +1909 -0
  27. package/backend-detect.mts +124 -312
  28. package/block-class.ts +201 -0
  29. package/build.ts +4 -2
  30. package/capture-predicate.ts +597 -0
  31. package/channel-lock.ts +349 -0
  32. package/disasm-decoder.ts +14 -14
  33. package/disasm-opcodes.ts +4 -4
  34. package/disasm-renderer.ts +2 -2
  35. package/evid-ingest.ts +217 -0
  36. package/evid-reconcile.ts +316 -0
  37. package/host-tool-client.ts +430 -0
  38. package/hostpath.ts +1 -1
  39. package/incident-record.ts +23 -12
  40. package/install-resources.ts +30 -14
  41. package/memmap-lookup.ts +285 -0
  42. package/package.json +48 -23
  43. package/prg-image.ts +118 -0
  44. package/repo-root.ts +107 -8
  45. package/resources/backend-detect.mjs +98 -236
  46. package/resources/broker-control.mjs +189 -16
  47. package/resources/broker-epoch.mjs +1 -1
  48. package/resources/broker-kill.mjs +8 -2
  49. package/resources/broker-launch.mjs +373 -214
  50. package/resources/broker-state.mjs +64 -18
  51. package/resources/container-guard.mjs +1 -1
  52. package/resources/ghidra-project.mjs +790 -0
  53. package/resources/host-tool.mjs +2561 -0
  54. package/resources/vice-broker.mjs +330 -184
  55. package/resources/vice-launcher.sh +130 -12
  56. package/stock-address.ts +6 -6
  57. package/stock-cia.ts +2 -2
  58. package/stock-condition.ts +8 -8
  59. package/stock-connect.ts +10 -6
  60. package/stock-derived.ts +29 -37
  61. package/stock-diagnose.ts +200 -36
  62. package/stock-dispatch.ts +200 -68
  63. package/stock-execution.ts +5 -3
  64. package/stock-handler.ts +1 -1
  65. package/stock-input.ts +9 -9
  66. package/stock-machine.ts +17 -6
  67. package/stock-paths.ts +18 -14
  68. package/stock-petscii.ts +1 -1
  69. package/stock-protocol.ts +17 -12
  70. package/stock-recycle.ts +83 -2
  71. package/stock-registers.ts +54 -29
  72. package/stock-reproducible-run.ts +811 -0
  73. package/stock-run-until.ts +100 -1
  74. package/stock-sprites.ts +3 -3
  75. package/stock-symbols.ts +13 -13
  76. package/stock-timing.ts +2 -2
  77. package/stock-vicii.ts +1 -1
  78. package/stop-oracle.ts +167 -0
  79. package/text-capability-probe.ts +660 -0
  80. package/text-connect.ts +157 -0
  81. package/text-protocol.ts +810 -0
  82. package/text-tools.ts +778 -0
  83. package/textmon-backtrace.ts +385 -0
  84. package/textmon-cpuhistory.ts +335 -0
  85. package/textmon-memmap.ts +494 -0
  86. package/textmon-profile.ts +458 -0
  87. package/textmon-registers.ts +748 -0
  88. package/tools-manifest.stock.json +864 -3
  89. package/version.ts +1 -1
  90. package/vice-broker-client.ts +189 -42
  91. package/vice-errors.ts +268 -0
  92. package/vice-proxy.ts +392 -2175
  93. package/vsf-slice.ts +640 -0
  94. package/capability-registry.ts +0 -388
  95. package/r2000-cli.ts +0 -1103
  96. package/r2000-d64.ts +0 -310
  97. package/r2000-enum-gen.ts +0 -574
  98. package/r2000-launch.ts +0 -357
  99. package/r2000-mcp-client.ts +0 -596
  100. package/r2000-project.ts +0 -190
  101. package/r2000-symbols.ts +0 -388
  102. package/r2000-tools.ts +0 -914
  103. package/r2000-verify.ts +0 -184
  104. package/refresh-manifest.ts +0 -124
  105. package/tools-manifest.json +0 -1223
  106. package/vice-probe.ts +0 -278
  107. package/vice-sync.ts +0 -336
  108. package/vice.ts +0 -772
@@ -0,0 +1,285 @@
1
+ #!/usr/bin/env node
2
+ // memmap-lookup.ts
3
+ //
4
+ // Phase 37, plan 37-01 (IMP-01/AUTO-01's shared foundation): the ONE loader
5
+ // for `c64-memory-mapping`'s own `memmap.json`, its content digest, and
6
+ // narrowest-containing-range selection over its 959 entries.
7
+ //
8
+ // THIS MODULE MUST NEVER IMPORT `hostpath.ts` OR `containerpath.ts` -- every
9
+ // path here is repo-relative and derived from this module's own location, the
10
+ // same posture `anno-regbits-gen.ts` and `dxa-blocks.ts` take for themselves.
11
+ // It also NEVER NAMES `node:sqlite` and NEVER OPENS THE ANNOTATION STORE:
12
+ // `anno-store.ts` is the one module `anno-seam.test.ts` allows to name that
13
+ // dependency, and this module answers a pure question about a static JSON
14
+ // file that has nothing to do with the store's own persistence.
15
+ //
16
+ // D-37-03: `anno-regbits-gen.ts:62` already computes the SAME `HERE`-relative
17
+ // five-hop path to `memmap.json` and `anno-regbits-gen.ts:377` already
18
+ // computes its sha256 digest, for a different purpose (bit-name generation).
19
+ // This module COPIES that formula rather than importing the generator --
20
+ // `anno-regbits-gen.ts`'s own header declares itself "the sole read of
21
+ // c64-memory-mapping's own memmap.json ... for this purpose", and widening
22
+ // that consumer set is a separate decision this plan does not make.
23
+ //
24
+ // WHAT THIS IS THE ONE AUTHORITATIVE PLACE FOR: loading and caching
25
+ // `memmap.json`'s 959 entries, computing the file's own sha256 provenance
26
+ // digest, and the NARROWEST-CONTAINING-RANGE selection rule an address
27
+ // resolves through. `anno-join.ts` calls this module; nothing else needs to.
28
+ //
29
+ // WHAT NOT TO DO:
30
+ // - Never re-derive `MEMMAP_PATH` from a different relative offset. This
31
+ // formula has already broken once when the skills tree moved (plan
32
+ // 16-01's own deviation note) -- one HERE-relative constant, copied
33
+ // verbatim in shape from `anno-regbits-gen.ts:62`.
34
+ // - Never mutate the cached entries array. `loadMemmap()` returns the same
35
+ // frozen array on every call after the first; a caller that needs a
36
+ // filtered view copies it.
37
+ // - Never derive "is this range bank-conditional" from `memmap.json`'s own
38
+ // fields. `BANK_CONDITIONAL_RANGES` below is HAND-MAINTAINED on purpose
39
+ // (37-RESEARCH.md Pitfall 2) -- the schema carries no structured
40
+ // bank-condition field, only free prose inside `desc`.
41
+
42
+ import { createHash } from "node:crypto";
43
+ import { readFileSync } from "node:fs";
44
+ import { dirname, join } from "node:path";
45
+ import { fileURLToPath } from "node:url";
46
+
47
+ const HERE = dirname(fileURLToPath(import.meta.url));
48
+
49
+ /** The `HERE`-relative path to `c64-memory-mapping`'s `memmap.json`, using the
50
+ * SAME five-hop formula `anno-regbits-gen.ts:62` uses. Copied, not imported
51
+ * (D-37-03). */
52
+ export const MEMMAP_PATH = join(HERE, "..", "..", "..", "src", "skills", "c64-memory-mapping", "memmap.json");
53
+
54
+ /** One entry as `memmap.json` holds it. MEASURED at plan time: 959 entries,
55
+ * `start`/`end` are INCLUSIVE integers with no length field, and `sym` is
56
+ * present on 219 of them. */
57
+ export interface MemmapEntry {
58
+ start: number;
59
+ end: number;
60
+ label: string;
61
+ section: string;
62
+ desc: string;
63
+ src: string;
64
+ sym?: string;
65
+ }
66
+
67
+ interface MemmapDocument {
68
+ sources: unknown[];
69
+ entries: MemmapEntry[];
70
+ }
71
+
72
+ /** Module-level cache, exactly as `anno-enum-gen.ts`'s `loadRegBits()` caches
73
+ * its own generated table -- `memmap.json` is read and parsed once per
74
+ * process, never per call. */
75
+ let cachedEntries: readonly MemmapEntry[] | undefined;
76
+
77
+ /** Reads and parses `memmap.json`, caching the parsed array. Refuses BY NAME
78
+ * if the top-level shape is not `{sources, entries}` or if `entries` is
79
+ * empty -- a malformed or truncated memmap.json must not read as "a memmap
80
+ * with nothing in it". */
81
+ export function loadMemmap(): readonly MemmapEntry[] {
82
+ if (cachedEntries !== undefined) return cachedEntries;
83
+
84
+ let parsed: unknown;
85
+ try {
86
+ parsed = JSON.parse(readFileSync(MEMMAP_PATH, "utf8"));
87
+ } catch (err) {
88
+ const reason = err instanceof Error ? err.message : String(err);
89
+ throw new Error(`memmap-lookup: ${MEMMAP_PATH} could not be read/parsed as JSON (${reason}).`);
90
+ }
91
+
92
+ const doc = parsed as Partial<MemmapDocument> | null;
93
+ if (doc === null || typeof doc !== "object" || !Array.isArray(doc.sources) || !Array.isArray(doc.entries)) {
94
+ throw new Error(
95
+ `memmap-lookup: ${MEMMAP_PATH} is not a { sources, entries } document -- refusing to guess a shape for a ` +
96
+ "malformed memmap.json.",
97
+ );
98
+ }
99
+ if (doc.entries.length === 0) {
100
+ throw new Error(
101
+ `memmap-lookup: ${MEMMAP_PATH}'s "entries" array is empty -- "a memmap with nothing in it" and "a memmap ` +
102
+ 'that failed to load" must not read the same, so this refuses rather than returning an empty selector.',
103
+ );
104
+ }
105
+
106
+ cachedEntries = Object.freeze(doc.entries.slice());
107
+ return cachedEntries;
108
+ }
109
+
110
+ /** The sha256 hex digest of `memmap.json`'s RAW BYTES -- the provenance value
111
+ * `AUTO-08` (a later plan) appends to derived comment text. MEASURED at plan
112
+ * time: this equals the same value `anno-regbits.json`'s committed banner
113
+ * carries (`_generated.memmapSha256`), and the test that pins this asserts
114
+ * that RELATION rather than a hard-coded literal, because the map is a living
115
+ * file. */
116
+ export function memmapDigest(): string {
117
+ return createHash("sha256").update(readFileSync(MEMMAP_PATH)).digest("hex");
118
+ }
119
+
120
+ /**
121
+ * D-37-13: the fixed, machine-parseable prefix `anno-join.ts` appends
122
+ * `memmapDigest()`'s full 64-character lowercase hex digest to, on every
123
+ * derived comment (`AUTO-08`). Chosen to be unlikely to collide with
124
+ * ordinary comment prose and to sit LAST in the comment text (no closing
125
+ * delimiter follows it), so a regex can find it without knowing the
126
+ * preceding label's own shape: `new RegExp(PROVENANCE_TOKEN_PREFIX_ESCAPED +
127
+ * "[0-9a-f]{64}$")`. Exported from here, not `anno-join.ts`, because the
128
+ * digest and the prefix that names it belong to the same module as
129
+ * `memmapDigest()` itself.
130
+ */
131
+ export const PROVENANCE_TOKEN_PREFIX = "[memmap-sha256:";
132
+
133
+ /** WHICH of the three steps decided a selection, or `unique` when only one
134
+ * entry contained the address at all. `AUTO-02` names two steps
135
+ * (narrowest-range-wins, then the `sym` tie-break); D-37-10 adds a third,
136
+ * because the phase's own headline `$D020` example is NOT resolved by the
137
+ * first two -- MEASURED at plan time, its two 1-byte contenders both lack a
138
+ * `sym`. Reported rather than left for a caller to infer, so a test (and
139
+ * plan 37-04's controls) can assert WHICH rule decided, not merely what it
140
+ * decided. */
141
+ export type MemmapTieBreak = "unique" | "width" | "symbol" | "order";
142
+
143
+ /** What `selectMemmapEntry()` returns for a unique or a resolved-tie hit.
144
+ * `contenderCount` is the number of containing entries the scan considered,
145
+ * so a caller can tell a unique hit from a resolved tie without re-running
146
+ * the scan itself. */
147
+ export interface MemmapSelection {
148
+ entry: MemmapEntry;
149
+ width: number;
150
+ contenderCount: number;
151
+ tieBrokenBy: MemmapTieBreak;
152
+ }
153
+
154
+ /** Inclusive width of one entry: `end - start`. Named once so every step
155
+ * below computes it identically. */
156
+ function inclusiveWidth(entry: MemmapEntry): number {
157
+ return entry.end - entry.start;
158
+ }
159
+
160
+ /**
161
+ * STEP ONE, WIDTH -- the whole of narrowest-range-wins. Prefers the smallest
162
+ * `end - start`; returns every entry tied at that minimum, since a single
163
+ * winner here is `unique`ly correct only when nothing else shares its width.
164
+ * Kept as its own named function (not folded into a single comparator) so
165
+ * plan 37-04's control -- "switch selection to first-match" -- is a single,
166
+ * small, textual replacement of exactly this step, per this plan's own
167
+ * `<read_first>` instruction.
168
+ */
169
+ function narrowestWidthSurvivors(containing: readonly MemmapEntry[]): MemmapEntry[] {
170
+ let minWidth = Infinity;
171
+ for (const entry of containing) {
172
+ const width = inclusiveWidth(entry);
173
+ if (width < minWidth) minWidth = width;
174
+ }
175
+ return containing.filter((entry) => inclusiveWidth(entry) === minWidth);
176
+ }
177
+
178
+ /**
179
+ * STEP TWO, SYMBOL -- `AUTO-02`'s own tie-break. Among step one's survivors,
180
+ * prefers an entry carrying a non-empty `sym` over one that does not.
181
+ * D-37-11's `$0000` fixture (three equal-width contenders, exactly one
182
+ * carrying `sym: "D6510"`) is this step's own fixture. Returns every
183
+ * `sym`-carrying survivor when at least one exists, else returns every
184
+ * survivor unchanged (this step decided nothing -- step three must run).
185
+ */
186
+ function symbolSurvivors(survivors: readonly MemmapEntry[]): MemmapEntry[] {
187
+ const withSym = survivors.filter((entry) => typeof entry.sym === "string" && entry.sym.length > 0);
188
+ return withSym.length > 0 ? withSym : survivors.slice();
189
+ }
190
+
191
+ /**
192
+ * STEP THREE, ORDER -- D-37-10. Among step two's survivors, the entry
193
+ * appearing FIRST in `memmap.json`'s own `entries` array wins. Stated
194
+ * explicitly, rather than left to whatever order a scan happened to
195
+ * produce, because the phase's own headline `$D020` example needs it: two
196
+ * 1-byte contenders there are tied on width AND neither carries a `sym`, so
197
+ * without this named third rule the selection would be an unstated,
198
+ * scan-order accident that a later refactor could change silently. No
199
+ * `sort()` is used here -- a single linear scan over `entries` (the same
200
+ * order the caller supplied) finds the first survivor, which is cheaper and
201
+ * keeps this step's own mutation (plan 37-05's "reverse the tie-break")
202
+ * a one-line replacement rather than a sort-comparator edit.
203
+ */
204
+ function orderWinner(survivors: readonly MemmapEntry[], entries: readonly MemmapEntry[]): MemmapEntry {
205
+ for (const entry of entries) {
206
+ if (survivors.includes(entry)) return entry;
207
+ }
208
+ // Unreachable: `survivors` is always drawn from `entries` by reference, so
209
+ // the scan above always finds one before falling through.
210
+ return survivors[0]!;
211
+ }
212
+
213
+ /**
214
+ * The THREE-DEEP selection order (D-37-10): narrowest-containing-range,
215
+ * then the `sym` tie-break (`AUTO-02`), then -- because those two do not
216
+ * resolve every real tie in the committed `memmap.json` -- the stated
217
+ * residual rule of "first in `entries` order". Collects every entry whose
218
+ * inclusive `[start, end]` contains `address`; returns `undefined` if none
219
+ * does. `tieBrokenBy` names WHICH step decided, so a caller (and plan
220
+ * 37-04's controls) can tell width-decided from symbol-decided from
221
+ * order-decided, not merely infer it from the winning entry's own shape.
222
+ */
223
+ export function selectMemmapEntry(address: number, entries: readonly MemmapEntry[] = loadMemmap()): MemmapSelection | undefined {
224
+ const containing: MemmapEntry[] = [];
225
+ for (const entry of entries) {
226
+ if (address < entry.start || address > entry.end) continue;
227
+ containing.push(entry);
228
+ }
229
+ const contenderCount = containing.length;
230
+ if (contenderCount === 0) return undefined;
231
+ if (contenderCount === 1) {
232
+ const entry = containing[0]!;
233
+ return { entry, width: inclusiveWidth(entry), contenderCount, tieBrokenBy: "unique" };
234
+ }
235
+
236
+ const widthSurvivors = narrowestWidthSurvivors(containing);
237
+ if (widthSurvivors.length === 1) {
238
+ const entry = widthSurvivors[0]!;
239
+ return { entry, width: inclusiveWidth(entry), contenderCount, tieBrokenBy: "width" };
240
+ }
241
+
242
+ const symSurvivors = symbolSurvivors(widthSurvivors);
243
+ if (symSurvivors.length === 1) {
244
+ const entry = symSurvivors[0]!;
245
+ return { entry, width: inclusiveWidth(entry), contenderCount, tieBrokenBy: "symbol" };
246
+ }
247
+
248
+ const entry = orderWinner(symSurvivors, entries);
249
+ return { entry, width: inclusiveWidth(entry), contenderCount, tieBrokenBy: "order" };
250
+ }
251
+
252
+ /** One hand-maintained bank-conditional range: its meaning depends on the
253
+ * processor port's bits #0-#2 at `$0001`. */
254
+ export interface BankConditionalRange {
255
+ start: number;
256
+ end: number;
257
+ why: string;
258
+ }
259
+
260
+ /**
261
+ * The three inclusive ranges whose interpretation depends on the processor
262
+ * port, HAND-MAINTAINED rather than derived from `memmap.json` (37-RESEARCH.md
263
+ * Pitfall 2): only 6 of 959 entries even mention the condition in free-text
264
+ * `desc`, and there is no structured bank-condition field to scan instead.
265
+ * Nothing in THIS module branches on this list -- a later plan's bank-decode
266
+ * logic does. Landed here so the list has one home rather than being
267
+ * reinvented per caller.
268
+ */
269
+ export const BANK_CONDITIONAL_RANGES: readonly BankConditionalRange[] = Object.freeze([
270
+ {
271
+ start: 0xa000,
272
+ end: 0xbfff,
273
+ why: "BASIC ROM when banked in, otherwise RAM (bits #0-#2 of $0001 select between them).",
274
+ },
275
+ {
276
+ start: 0xd000,
277
+ end: 0xdfff,
278
+ why: "I/O area, Character ROM, or RAM depending on $0001 bits #0-#2 -- the canonical three-way case.",
279
+ },
280
+ {
281
+ start: 0xe000,
282
+ end: 0xffff,
283
+ why: "KERNAL ROM when banked in, otherwise RAM (bits #0-#2 of $0001 select between them).",
284
+ },
285
+ ]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henols/vice-mcp",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "VICE emulator MCP server for C64 reverse-engineering: a stdio MCP server that proxies vice tools to a host VICE MCP server.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,23 +9,24 @@
9
9
  "main": "vice-proxy.ts",
10
10
  "files": [
11
11
  "vice-proxy.ts",
12
- "vice.ts",
13
- "vice-sync.ts",
14
- "vice-probe.ts",
12
+ "vice-errors.ts",
15
13
  "vice-broker-client.ts",
14
+ "host-tool-client.ts",
16
15
  "version.ts",
17
16
  "stock-protocol.ts",
18
17
  "stock-connect.ts",
18
+ "text-protocol.ts",
19
+ "text-connect.ts",
20
+ "text-tools.ts",
21
+ "channel-lock.ts",
19
22
  "containerpath.ts",
20
23
  "hostpath.ts",
21
24
  "repo-root.ts",
22
25
  "install-resources.ts",
23
26
  "incident-record.ts",
24
- "refresh-manifest.ts",
25
27
  "build.ts",
26
28
  "container-guard.mts",
27
29
  "backend-detect.mts",
28
- "capability-registry.ts",
29
30
  "stock-dispatch.ts",
30
31
  "stock-derived.ts",
31
32
  "stock-handler.ts",
@@ -44,6 +45,12 @@
44
45
  "disasm-opcodes.ts",
45
46
  "disasm-decoder.ts",
46
47
  "disasm-renderer.ts",
48
+ "textmon-memmap.ts",
49
+ "textmon-cpuhistory.ts",
50
+ "textmon-backtrace.ts",
51
+ "textmon-profile.ts",
52
+ "textmon-registers.ts",
53
+ "text-capability-probe.ts",
47
54
  "stock-memory-search.ts",
48
55
  "stock-symbols.ts",
49
56
  "stock-vicii.ts",
@@ -51,30 +58,48 @@
51
58
  "stock-sprites.ts",
52
59
  "stock-timing.ts",
53
60
  "stock-run-until.ts",
61
+ "stock-reproducible-run.ts",
54
62
  "stock-diagnose.ts",
55
63
  "stock-recycle.ts",
56
- "r2000-launch.ts",
57
- "r2000-project.ts",
58
- "r2000-d64.ts",
59
- "r2000-cli.ts",
60
- "r2000-verify.ts",
61
- "r2000-mcp-client.ts",
62
- "r2000-tools.ts",
63
- "r2000-symbols.ts",
64
- "r2000-regbits-gen.ts",
65
- "r2000-regbits.json",
66
- "r2000-enum-gen.ts",
67
- "r2000-acme-ident.ts",
68
- "r2000-confidence.ts",
69
- "r2000-memmap-render.ts",
64
+ "anno-cli.ts",
65
+ "anno-symbols.ts",
66
+ "anno-regbits-gen.ts",
67
+ "anno-regbits.json",
68
+ "anno-enum-gen.ts",
69
+ "anno-acme-ident.ts",
70
+ "anno-confidence.ts",
71
+ "anno-memmap-render.ts",
72
+ "anno-export-asm.ts",
73
+ "anno-provenance-ledger.ts",
74
+ "anno-coverage.ts",
75
+ "block-class.ts",
76
+ "anno-hazard-report.ts",
77
+ "evid-reconcile.ts",
78
+ "evid-ingest.ts",
79
+ "prg-image.ts",
80
+ "vsf-slice.ts",
81
+ "capture-predicate.ts",
82
+ "stop-oracle.ts",
83
+ "anno-types.ts",
84
+ "anno-index.ts",
85
+ "anno-store.ts",
86
+ "anno-store-export.ts",
87
+ "anno-tools.ts",
88
+ "anno-derive.ts",
89
+ "anno-details.ts",
90
+ "anno-register.ts",
91
+ "anno-import.ts",
92
+ "anno-join.ts",
93
+ "anno-bank.ts",
94
+ "anno-graphics.ts",
95
+ "memmap-lookup.ts",
70
96
  "resources",
71
- "tools-manifest.json",
72
97
  "tools-manifest.stock.json",
73
98
  "README.md",
74
99
  "THIRD-PARTY-NOTICES.md"
75
100
  ],
76
101
  "engines": {
77
- "node": ">=22.18.0"
102
+ "node": ">=24.0.0"
78
103
  },
79
104
  "publishConfig": {
80
105
  "access": "public"
@@ -87,7 +112,7 @@
87
112
  "repository": {
88
113
  "type": "git",
89
114
  "url": "git+https://github.com/henols/c64-re-tools.git",
90
- "directory": ".claude/mcp/vice"
115
+ "directory": "src/mcp/vice"
91
116
  },
92
117
  "homepage": "https://github.com/henols/c64-re-tools#readme",
93
118
  "bugs": {
package/prg-image.ts ADDED
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+ // prg-image.ts -- the ONE authoritative place in this repo holding pure C64
3
+ // image byte-layout knowledge: how a `.prg` splits into a load address plus a
4
+ // body, what load address a flat 64K RAM capture has, and how to invert a
5
+ // gzip-then-base64 payload back into bytes. These are facts about C64 file
6
+ // formats and about one payload encoding -- they depend on no external
7
+ // analyser, no emulator, and no annotation store, so they belong in a module
8
+ // none of those can take with them when it goes away.
9
+ //
10
+ // This module performs NO filesystem and NO network I/O: every function takes
11
+ // bytes (or a base64 string) and returns values. Callers obtain and persist
12
+ // the bytes themselves. That is the same claim `anno-project.ts` makes about
13
+ // itself, and it must remain true of both files now that the split has
14
+ // happened -- a structural test in `prg-image.test.ts` asserts it from this
15
+ // module's own source rather than trusting this paragraph.
16
+ //
17
+ // WHY THIS FILE EXISTS SEPARATELY: these three functions used to live in
18
+ // `anno-project.ts`, the module that builds a `.regen2000proj` file for the
19
+ // the external analyser analyser. They were never about that analyser. One of them
20
+ // is imported statically by the byte-coverage census (`anno-coverage.ts`), a
21
+ // capability that must keep working independently of whether this repo still
22
+ // drives that analyser at all -- so a census whose only route to a payload
23
+ // decoder ran through analyser glue was one deletion away from breaking with
24
+ // no announcement (SEAM-02). Extracting them under a name that carries no
25
+ // analyser prefix removes that coupling outright instead of recording it as a
26
+ // hazard to remember later. There is deliberately NO re-export left behind in
27
+ // `anno-project.ts`: a compatibility shim would leave the coupling fully
28
+ // intact while looking finished.
29
+ //
30
+ // THIS MODULE MUST BE LISTED IN `package.json`'s `files[]`. It is reachable
31
+ // from the published entry point's import closure, and the STATIC route is
32
+ // named here first because it is the stronger reachability claim: `anno-
33
+ // tools.ts` -- the curated `anno_*` MCP tool surface -- imports `parsePrg` and
34
+ // `flatImageOrigin` from here with a plain top-level import, and `vice-proxy.ts`
35
+ // imports `anno-tools.ts` statically. `anno-coverage.ts` (the byte-coverage
36
+ // census) imports `decodeRawData`, `parsePrg` and `flatImageOrigin` for the
37
+ // same three facts on the CLI's route. `scripts/check-npm-packages.mjs` walks
38
+ // that closure over `files[]` and fails the pack the moment a reachable module
39
+ // sits outside the listed set.
40
+ //
41
+ // CORRECTED 2026-08-30 (WR-07, plan 29-16). This paragraph previously named
42
+ // `anno-cli.ts` as importing `parsePrg` and `flatImageOrigin` and rested the
43
+ // whole reachability claim on the DYNAMIC import that reaches that file. That
44
+ // was doubly wrong: `anno-cli.ts` imported neither symbol (it imported
45
+ // `decodeRawData` only), and it now imports nothing from here at all -- its
46
+ // image decode delegates to `anno-coverage.ts`'s `loadProjectImage()`. A
47
+ // stated reason for shipping a file has to be true or it is worse than absent.
48
+ //
49
+ // WHAT NOT TO DO:
50
+ // - Never give any function here a filesystem PATH parameter. They take byte
51
+ // arrays and a base64 string, which is precisely what keeps path traversal
52
+ // out of this module's threat surface entirely. Path resolution belongs to
53
+ // the CLI. For the same reason this module imports nothing from either of
54
+ // this repo's two host/container path-translation seams; that absence is
55
+ // asserted structurally by `hostpath-consumers.test.ts`, not merely stated
56
+ // here.
57
+ // - Never relax, reword or reorder either input refusal below. The concrete
58
+ // incident: a 4096-byte flat `.raw` capture fell through to the `.prg`
59
+ // parser, whose first two bytes become the load address, so a truncated
60
+ // capture silently "bootstrapped" with an origin read backwards out of its
61
+ // own payload bytes and exited zero -- every downstream address wrong, no
62
+ // diagnostic. The refusal message texts are a user-visible contract: both
63
+ // `anno-tools.ts`'s `loadImage()` and `anno-coverage.ts`'s
64
+ // `loadProjectImage()` prefix them with the caller's own image path, and
65
+ // tests on both routes match on their wording, so a reworded message
66
+ // breaks a test for a reason that looks unrelated. (Attribution corrected
67
+ // 2026-08-30, WR-07: this line named `anno-cli.ts`, which prefixed them
68
+ // through a `bootstrap` verb deleted by D-14 on 2026-08-29.)
69
+ // - Never add the dispatch ORDER discipline here. Which check runs first for
70
+ // a given input extension belongs to the two loaders that own it --
71
+ // `anno-tools.ts`'s `loadImage()` for the MCP tool surface and
72
+ // `anno-coverage.ts`'s `loadProjectImage()` for the coverage verb. Both
73
+ // dispatch `.raw`/`.bin` by extension BEFORE any length check, so that
74
+ // `flatImageOrigin`'s named refusal is always reachable for those two
75
+ // extensions. Neither function below may start inferring what kind of
76
+ // image it was handed.
77
+
78
+ import { gunzipSync } from "node:zlib";
79
+
80
+ /**
81
+ * Parses a `.prg` file: a little-endian 2-byte load address followed by the
82
+ * payload bytes. This is the C64 program-file convention every C64 loader
83
+ * (and this project's own `acme-build` output) already follows.
84
+ */
85
+ export function parsePrg(bytes: Uint8Array): { origin: number; body: Uint8Array } {
86
+ if (bytes.length < 3) {
87
+ throw new Error(
88
+ `parsePrg: input is ${bytes.length} byte(s) -- a .prg needs at least 3 bytes (2-byte load address plus at least 1 payload byte)`,
89
+ );
90
+ }
91
+ const origin = bytes[0]! | (bytes[1]! << 8);
92
+ const body = bytes.subarray(2);
93
+ return { origin, body };
94
+ }
95
+
96
+ /**
97
+ * Returns the load address (`0`) for a flat 64K RAM capture, and throws for
98
+ * anything else. Flat 64K is in scope because `ANNO-06` names it directly
99
+ * and it is exactly the shape `c64-ram-capture` already produces (D-03) --
100
+ * this function does not attempt to support any other flat-image size.
101
+ */
102
+ export function flatImageOrigin(bytes: Uint8Array): number {
103
+ if (bytes.length !== 65536) {
104
+ throw new Error(
105
+ `flatImageOrigin: input is ${bytes.length} byte(s) -- a flat 64K capture must be exactly 65536 bytes`,
106
+ );
107
+ }
108
+ return 0;
109
+ }
110
+
111
+ /**
112
+ * The inverse of a gzip-then-base64 payload encoding: base64-decode, then
113
+ * gunzip. Exported so tests can prove the payload round-trips exactly,
114
+ * rather than asserting against an opaque blob.
115
+ */
116
+ export function decodeRawData(base64: string): Uint8Array {
117
+ return gunzipSync(Buffer.from(base64, "base64"));
118
+ }