@henols/vice-mcp 0.2.0 → 0.2.2
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 -1
- package/THIRD-PARTY-NOTICES.md +1 -1
- package/anno-acme-ident.ts +97 -0
- package/anno-cli.ts +1465 -0
- package/anno-confidence.ts +233 -0
- package/anno-coverage.ts +2465 -0
- package/anno-d64.ts +310 -0
- package/anno-derive.ts +590 -0
- package/anno-details.ts +169 -0
- package/anno-enum-gen.ts +533 -0
- package/anno-export-asm.ts +1310 -0
- package/anno-index.ts +150 -0
- package/anno-memmap-render.ts +672 -0
- package/anno-regbits-gen.ts +421 -0
- package/anno-regbits.json +1370 -0
- package/anno-register.ts +240 -0
- package/anno-store.ts +3486 -0
- package/anno-symbols.ts +266 -0
- package/anno-tools.ts +2111 -0
- package/anno-types.ts +1636 -0
- package/block-class.ts +201 -0
- package/build.ts +1 -1
- package/capability-registry.ts +3 -1
- package/disasm-decoder.ts +14 -14
- package/disasm-opcodes.ts +4 -4
- package/disasm-renderer.ts +2 -2
- package/hostpath.ts +1 -1
- package/install-resources.ts +1 -1
- package/package.json +23 -3
- package/prg-image.ts +119 -0
- package/repo-root.ts +20 -5
- package/resources/broker-launch.mjs +8 -4
- package/resources/vice-launcher.sh +3 -3
- package/stock-address.ts +5 -5
- package/stock-cia.ts +2 -2
- package/stock-condition.ts +7 -7
- package/stock-connect.ts +1 -1
- package/stock-dispatch.ts +35 -5
- package/stock-execution.ts +5 -3
- package/stock-input.ts +9 -9
- package/stock-machine.ts +17 -6
- package/stock-protocol.ts +16 -11
- package/stock-registers.ts +54 -29
- package/stock-sprites.ts +3 -3
- package/stock-symbols.ts +33 -9
- package/stock-timing.ts +1 -1
- package/stock-vicii.ts +1 -1
- package/version.ts +1 -1
- package/vice-proxy.ts +168 -0
package/anno-symbols.ts
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// anno-symbols.ts -- the ONE authoritative place in this repo for the
|
|
3
|
+
// PRE-SPAWN half of the symbol round trip between an annotation store and
|
|
4
|
+
// stock VICE's symbol table (ANNO-14/ANNO-15, ARCHITECTURE.md Rule A20).
|
|
5
|
+
//
|
|
6
|
+
// WHAT LEFT, WHAT STAYED, AND WHERE THE ROUTE RETURNS (plan 29-10, D-01/D-14,
|
|
7
|
+
// 2026-08-30). Read this paragraph before looking for a function that is not
|
|
8
|
+
// here.
|
|
9
|
+
//
|
|
10
|
+
// WHAT LEFT: the ROUTE. `exportLabels()`, `importLabels()` and
|
|
11
|
+
// `regenerateAndReload()` are gone. All three drove the retired external
|
|
12
|
+
// static-analysis child -- one built and spawned its `--export_lbl` argv,
|
|
13
|
+
// one built its `--import_lbl` argv and held its stdio session open across a
|
|
14
|
+
// save-and-verify, one wrote a name through its curated tool surface before
|
|
15
|
+
// re-exporting. Every module they reached was deleted in the same commit,
|
|
16
|
+
// so the functions could not have been kept in any form that still ran.
|
|
17
|
+
//
|
|
18
|
+
// WHAT STAYED: the KNOWLEDGE. The label-file format is not this module's to
|
|
19
|
+
// re-parse (`stock-symbols.ts` owns it, see below), but the DISCIPLINE
|
|
20
|
+
// around it is this project's own and survives intact:
|
|
21
|
+
// - `validateLabelFileForImport()`, below, is the whole pre-spawn gate the
|
|
22
|
+
// deleted `importLabels()` ran BEFORE any child existed: the byte-size
|
|
23
|
+
// ceiling check (T-11-LBL-SIZE), the single-parser pass, and the
|
|
24
|
+
// per-name ACME identifier validation that REJECTS rather than
|
|
25
|
+
// sanitizes (T-11-NAME-INJECT, closed) while naming the offending name,
|
|
26
|
+
// its 1-based line number and that line's own text. It is unchanged
|
|
27
|
+
// from the code it was lifted out of; it never touched the deleted
|
|
28
|
+
// modules and so had no reason to go with them.
|
|
29
|
+
// - `ImportLabelsResult`'s discriminated union, and the reason it is one:
|
|
30
|
+
// a caller must not be able to read `importedNames` and mistake "the
|
|
31
|
+
// import call returned no error" for "the names are actually on disk".
|
|
32
|
+
// - The three rules of the `.lbl` loop, recorded in WHAT NOT TO DO below:
|
|
33
|
+
// the store is the merge point, `vice_symbols_load` is
|
|
34
|
+
// replace-not-merge so a regeneration is WHOLE, and no second
|
|
35
|
+
// `al C:xxxx .Name` parser is ever added here.
|
|
36
|
+
// - The two measured facts about the retired producer's own behaviour,
|
|
37
|
+
// kept in the past tense because they are why the discipline has the
|
|
38
|
+
// shape it has, not because anything still calls that producer.
|
|
39
|
+
//
|
|
40
|
+
// WHERE THE ROUTE RETURNS: **NO PHASE CURRENTLY OWNS ITS RETURN**, and the
|
|
41
|
+
// earlier version of this line said otherwise. It forecast that the `.lbl`
|
|
42
|
+
// round trip would be rebuilt over the annotation store alongside the ACME
|
|
43
|
+
// export oracle. Half of that happened on 2026-08-31 -- the ACME export
|
|
44
|
+
// route came back as the `anno export-asm` CLI verb -- but the work that
|
|
45
|
+
// rebuilt it covered that route ONLY: no requirement and no success
|
|
46
|
+
// criterion of it mentioned `export-lbl` or `import-lbl`. So the forecast
|
|
47
|
+
// was wrong, and it is CORRECTED here rather than deleted, because deleting
|
|
48
|
+
// a withdrawal notice erases the record that a capability went missing and
|
|
49
|
+
// why. `.planning/PROJECT.md` carries the dated ANNO-14/ANNO-15 notice and
|
|
50
|
+
// says the same thing: this is a temporary loss of a capability that was
|
|
51
|
+
// genuinely Validated, not a completed one being tidied away. The
|
|
52
|
+
// demonstration was made end to end against genuine unpatched stock `x64sc`
|
|
53
|
+
// and that record still stands. A reader checking today whether the symbol
|
|
54
|
+
// round trip works should read this as: it does not, nobody currently owns
|
|
55
|
+
// making it work again, and the specification for whoever eventually does is
|
|
56
|
+
// the measured-facts block below.
|
|
57
|
+
//
|
|
58
|
+
// WHY THIS FILE EXISTS: static-analysis symbols going OUT to VICE and
|
|
59
|
+
// live-discovered symbols coming IN from VICE must flow through explicit
|
|
60
|
+
// adapter code -- neither side may parse the other's internal representation
|
|
61
|
+
// (Rule A20). This module is that adapter's own side of the boundary. It
|
|
62
|
+
// reuses `stock-symbols.ts`'s existing `al C:xxxx .Name` parser
|
|
63
|
+
// (`parseViceLabelFile()`, exported there for exactly this reuse) rather than
|
|
64
|
+
// adding this repo's THIRD copy of that format -- `stock-symbols.ts` and
|
|
65
|
+
// `acme-build/scripts/acme.mjs`'s `curateLabels()` are the two that already
|
|
66
|
+
// exist.
|
|
67
|
+
//
|
|
68
|
+
// MEASURED FACTS, PAST TENSE, kept because they are the reasons for the
|
|
69
|
+
// discipline above rather than instructions to anyone:
|
|
70
|
+
// - `--export_lbl` exported USER labels only. Measured (Phase 9, and
|
|
71
|
+
// re-confirmed by the round-trip test that was deleted with its subject):
|
|
72
|
+
// an annotated project emitted exactly the labels a caller had set, and
|
|
73
|
+
// the auto-generated `a_D011` / `a_D020` / `e_FFD2` externals were NOT
|
|
74
|
+
// exported. A rebuilt exporter that emits an `a_`-prefixed name is
|
|
75
|
+
// emitting something the old one did not.
|
|
76
|
+
// - `--import_lbl` under plain `--headless` DISCARDED. `main.rs:800-806`
|
|
77
|
+
// was `if headless && !mcp_server { return Ok(()) }`: an import ran into
|
|
78
|
+
// memory and then hit that early return without ever calling save, so the
|
|
79
|
+
// import was silently discarded -- measured live: two names imported that
|
|
80
|
+
// way, and a subsequent export read back from disk returned only the
|
|
81
|
+
// pre-existing label. The lesson that outlives the producer is the one
|
|
82
|
+
// `ImportLabelsResult` encodes: never report an import as persisted on
|
|
83
|
+
// the strength of a no-error response.
|
|
84
|
+
//
|
|
85
|
+
// WHAT NOT TO DO, named concretely:
|
|
86
|
+
// - Never add a second `al C:xxxx .Name` regex anywhere in this file.
|
|
87
|
+
// Every read of a label file's TEXT goes through `stock-symbols.ts`'s
|
|
88
|
+
// exported `parseViceLabelFile()` (T-11-LBL-PARSER-DUP).
|
|
89
|
+
// - Never call `vice_symbols_load` (`stock-symbols.ts`'s handlers)
|
|
90
|
+
// incrementally. A rebuilt regeneration path regenerates the WHOLE `.lbl`
|
|
91
|
+
// and returns its path for the CALLER to hand to `vice_symbols_load`
|
|
92
|
+
// exactly once -- `vice_symbols_load` is deliberately replace-not-merge
|
|
93
|
+
// (T-05-02-05), so a full regeneration is what keeps that semantics
|
|
94
|
+
// correct rather than a limitation. A merge mode on `vice_symbols_load`
|
|
95
|
+
// itself was rejected: it would reopen a v0.2.0 decision and make a tool
|
|
96
|
+
// advertised on both backends diverge in semantics.
|
|
97
|
+
// - Never let an illegal label name from a `.lbl` file reach a spawned
|
|
98
|
+
// child (T-11-NAME-INJECT, closed). `validateLabelFileForImport()` below
|
|
99
|
+
// validates every name against `anno-acme-ident.ts`'s
|
|
100
|
+
// `assertLegalAcmeIdentifier()` BEFORE any argv is built -- REJECT, never
|
|
101
|
+
// sanitize. Any rebuilt import route must call it first, for the same
|
|
102
|
+
// reason and in the same position -- the obligation is on the route,
|
|
103
|
+
// whenever one is built, and is not held by a numbered phase.
|
|
104
|
+
import { readFileSync, statSync } from "node:fs";
|
|
105
|
+
|
|
106
|
+
import { parseViceLabelFile, MAX_LABEL_FILE_BYTES } from "./stock-symbols.ts";
|
|
107
|
+
import { assertLegalAcmeIdentifier } from "./anno-acme-ident.ts";
|
|
108
|
+
|
|
109
|
+
/** This module's own error class, minimal shape (message-only, `.name` set to
|
|
110
|
+
* the class name). Never thrown for a ceiling violation on a `.lbl` file's
|
|
111
|
+
* TEXT -- those come from `stock-symbols.ts`'s `StockSymbolsError`, surfaced
|
|
112
|
+
* verbatim, never re-wrapped as this class. Reserved for this module's OWN
|
|
113
|
+
* failure modes: an oversized file caught before `parseViceLabelFile()` is
|
|
114
|
+
* ever called, or (T-11-NAME-INJECT, closed) an illegal label name caught
|
|
115
|
+
* before any child could be spawned -- naming the offending name, its 1-based
|
|
116
|
+
* line number, and that line's own text. */
|
|
117
|
+
export class AnnoSymbolsError extends Error {
|
|
118
|
+
constructor(message: string) {
|
|
119
|
+
super(message);
|
|
120
|
+
this.name = "AnnoSymbolsError";
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface LabelEntry {
|
|
125
|
+
name: string;
|
|
126
|
+
address: number;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface ExportLabelsOptions {
|
|
130
|
+
projectPath: string;
|
|
131
|
+
outPath: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface ExportLabelsResult {
|
|
135
|
+
path: string;
|
|
136
|
+
symbolCount: number;
|
|
137
|
+
symbols: LabelEntry[];
|
|
138
|
+
skippedLines: number;
|
|
139
|
+
duplicateNames: number;
|
|
140
|
+
lineCount: number;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface ImportLabelsOptions {
|
|
144
|
+
projectPath: string;
|
|
145
|
+
lblPath: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface ImportLabelsVerified {
|
|
149
|
+
diskVerified: true;
|
|
150
|
+
importedNames: string[];
|
|
151
|
+
/** The fresh, independent export result used to prove persistence -- a
|
|
152
|
+
* caller can inspect it without re-exporting itself. */
|
|
153
|
+
exported: ExportLabelsResult;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface ImportLabelsUnverified {
|
|
157
|
+
diskVerified: false;
|
|
158
|
+
importedNames: string[];
|
|
159
|
+
/** Names present in the imported `.lbl` file that a fresh export from
|
|
160
|
+
* disk did NOT contain. Always non-empty when `diskVerified` is `false`. */
|
|
161
|
+
missingNames: string[];
|
|
162
|
+
reason: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* A discriminated union, deliberately -- `diskVerified: true` and
|
|
167
|
+
* `diskVerified: false` are structurally distinct shapes, so a caller cannot
|
|
168
|
+
* read `result.importedNames` and mistake "the import call returned no error"
|
|
169
|
+
* for "the names are actually on disk".
|
|
170
|
+
*
|
|
171
|
+
* KEPT ACROSS THE CUT (plan 29-10). The route that produced it is gone; the
|
|
172
|
+
* distinction it encodes is the whole lesson of the `--import_lbl` discard
|
|
173
|
+
* measured in this module's header. Because no phase currently owns rebuilding
|
|
174
|
+
* that route, this type is its only surviving contract; whenever a route IS
|
|
175
|
+
* built it is expected to return this shape rather than
|
|
176
|
+
* invent a weaker one. A rebuilt route that returns a bare name list has
|
|
177
|
+
* silently dropped the property this type exists to make unrepresentable.
|
|
178
|
+
*/
|
|
179
|
+
export type ImportLabelsResult = ImportLabelsVerified | ImportLabelsUnverified;
|
|
180
|
+
|
|
181
|
+
export interface ValidatedLabelFile {
|
|
182
|
+
/** Every label name the file declares, in file order. */
|
|
183
|
+
names: string[];
|
|
184
|
+
/** The file's full text, already read -- returned so a caller never reads
|
|
185
|
+
* it a second time to locate a line. */
|
|
186
|
+
text: string;
|
|
187
|
+
/** `parseViceLabelFile()`'s own counts, passed through unchanged. */
|
|
188
|
+
symbolCount: number;
|
|
189
|
+
skippedLines: number;
|
|
190
|
+
duplicateNames: number;
|
|
191
|
+
lineCount: number;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* THE PRE-SPAWN GATE (T-11-LBL-SIZE + T-11-NAME-INJECT, both closed) -- the
|
|
196
|
+
* half of the deleted `importLabels()` that never touched a child process,
|
|
197
|
+
* lifted out unchanged when the route around it was removed (plan 29-10).
|
|
198
|
+
*
|
|
199
|
+
* In order, and the order matters:
|
|
200
|
+
* 1. A byte-size check against `stock-symbols.ts`'s own
|
|
201
|
+
* `MAX_LABEL_FILE_BYTES`, so an oversized file is refused before it is
|
|
202
|
+
* even read into memory.
|
|
203
|
+
* 2. One full `parseViceLabelFile()` pass, whose `StockSymbolsError`
|
|
204
|
+
* ceiling violations (`MAX_LABEL_FILE_LINES`/`MAX_SYMBOLS`) propagate
|
|
205
|
+
* VERBATIM rather than being re-wrapped.
|
|
206
|
+
* 3. Every discovered name validated against `anno-acme-ident.ts`'s
|
|
207
|
+
* `assertLegalAcmeIdentifier()`. An illegal name throws
|
|
208
|
+
* `AnnoSymbolsError` naming the offending name, its 1-based line
|
|
209
|
+
* number, and that line's own text -- REJECT, never sanitize.
|
|
210
|
+
*
|
|
211
|
+
* The offending line is located by a substring search over the already-read
|
|
212
|
+
* text, never a second `al C:` regex (this module's header forbids a third
|
|
213
|
+
* parser for that format).
|
|
214
|
+
*
|
|
215
|
+
* A rebuilt import route -- whenever one is built, since no phase currently
|
|
216
|
+
* owns writing it -- calls this FIRST, before it builds any argv. That
|
|
217
|
+
* position is the property T-11-NAME-INJECT was closed on: no illegal name can
|
|
218
|
+
* reach a child, because no child exists yet when this runs.
|
|
219
|
+
*/
|
|
220
|
+
export function validateLabelFileForImport({ lblPath }: { lblPath: string }): ValidatedLabelFile {
|
|
221
|
+
let size: number;
|
|
222
|
+
try {
|
|
223
|
+
size = statSync(lblPath).size;
|
|
224
|
+
} catch (err) {
|
|
225
|
+
throw new AnnoSymbolsError(
|
|
226
|
+
`validateLabelFileForImport: could not stat "${lblPath}" (${err instanceof Error ? err.message : String(err)})`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
if (size > MAX_LABEL_FILE_BYTES) {
|
|
230
|
+
throw new AnnoSymbolsError(
|
|
231
|
+
`validateLabelFileForImport: "${lblPath}" is ${size} bytes, which exceeds the ${MAX_LABEL_FILE_BYTES}-byte ceiling`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const text = readFileSync(lblPath, "utf8");
|
|
236
|
+
// stock-symbols.ts's ONE parser, never a second regex. A ceiling violation
|
|
237
|
+
// here (StockSymbolsError) propagates unmodified.
|
|
238
|
+
const parsed = parseViceLabelFile(text);
|
|
239
|
+
const names = Array.from(parsed.table.byName.keys());
|
|
240
|
+
|
|
241
|
+
for (const name of names) {
|
|
242
|
+
try {
|
|
243
|
+
assertLegalAcmeIdentifier(name, "importLabels label name");
|
|
244
|
+
} catch (err) {
|
|
245
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
246
|
+
const lines = text.split(/\r?\n/);
|
|
247
|
+
const lineIndex = lines.findIndex((line) => line.includes(name));
|
|
248
|
+
const lineNumber = lineIndex === -1 ? 0 : lineIndex + 1;
|
|
249
|
+
const lineText = lineIndex === -1 ? "(line not found)" : lines[lineIndex];
|
|
250
|
+
throw new AnnoSymbolsError(
|
|
251
|
+
`validateLabelFileForImport: "${lblPath}" line ${lineNumber} carries an illegal label name "${name}" ` +
|
|
252
|
+
`(${reason}) -- line text: ${JSON.stringify(lineText)}. REJECTED, never sanitized or quoted, before ` +
|
|
253
|
+
"any child is spawned.",
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
names,
|
|
260
|
+
text,
|
|
261
|
+
symbolCount: parsed.symbolCount,
|
|
262
|
+
skippedLines: parsed.skippedLines,
|
|
263
|
+
duplicateNames: parsed.duplicateNames,
|
|
264
|
+
lineCount: parsed.lineCount,
|
|
265
|
+
};
|
|
266
|
+
}
|