@henols/vice-mcp 0.2.1 → 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 -24
- package/{r2000-acme-ident.ts → anno-acme-ident.ts} +13 -13
- package/anno-cli.ts +1465 -0
- package/{r2000-confidence.ts → anno-confidence.ts} +22 -22
- package/anno-coverage.ts +2465 -0
- package/{r2000-d64.ts → anno-d64.ts} +5 -5
- 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/{r2000-memmap-render.ts → anno-memmap-render.ts} +236 -95
- package/{r2000-regbits-gen.ts → anno-regbits-gen.ts} +20 -15
- package/{r2000-regbits.json → anno-regbits.json} +2 -2
- 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 -17
- 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 +9 -9
- package/stock-timing.ts +1 -1
- package/stock-vicii.ts +1 -1
- package/version.ts +1 -1
- package/vice-proxy.ts +68 -46
- package/r2000-cli.ts +0 -1103
- package/r2000-enum-gen.ts +0 -574
- package/r2000-launch.ts +0 -357
- package/r2000-mcp-client.ts +0 -596
- package/r2000-project.ts +0 -190
- package/r2000-symbols.ts +0 -388
- package/r2000-tools.ts +0 -914
- package/r2000-verify.ts +0 -184
package/r2000-project.ts
DELETED
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// r2000-project.ts -- the ONE authoritative place in this repo that builds a
|
|
3
|
-
// `.regen2000proj` file. This module performs no filesystem or network I/O:
|
|
4
|
-
// callers read the source bytes and write the returned JSON text themselves;
|
|
5
|
-
// this module only transforms bytes into the wire shape regenerator2000
|
|
6
|
-
// loads. Nothing else in this repo may hand-build a project file -- if a
|
|
7
|
-
// future caller needs a `.regen2000proj`, it imports `synthesizeProject`
|
|
8
|
-
// from here rather than re-deriving the shape.
|
|
9
|
-
//
|
|
10
|
-
// WHY THIS EXISTS (D-01): Phase 9 already proved a working bootstrap route --
|
|
11
|
-
// drive the TUI's own Save-As flow over a synthetic pty and a scripted
|
|
12
|
-
// keystroke sequence. That route works, but it costs `tmux` as a declared
|
|
13
|
-
// prerequisite this project does not otherwise need, AND it still requires a
|
|
14
|
-
// post-save JSON edit to force `settings.use_illegal_opcodes` (the TUI's own
|
|
15
|
-
// Save-As does not expose that setting). Synthesising the project file
|
|
16
|
-
// directly in Node removes all four costs at once: no pty, no modal TUI, no
|
|
17
|
-
// keystroke encoding, no terminal-size assumption, and no post-edit -- the
|
|
18
|
-
// forced setting is written correctly the first time, by construction.
|
|
19
|
-
//
|
|
20
|
-
// WHAT NOT TO DO (two concrete past mistakes, both from Phase 9's own
|
|
21
|
-
// evidence and CONTEXT.md's D-01/D-04):
|
|
22
|
-
// 1. Do NOT write `raw_data_base64` as a plain (uncompressed) base64
|
|
23
|
-
// payload. regenerator2000 loads the project file expecting the value
|
|
24
|
-
// to be gzip-then-base64 encoded; an uncompressed payload was tried
|
|
25
|
-
// during Phase 9's probe and failed to load with
|
|
26
|
-
// `Error loading file: invalid gzip header`. Always run the bytes
|
|
27
|
-
// through `node:zlib`'s `gzipSync` first.
|
|
28
|
-
// 2. Do NOT add a version pin or a `--version` allow-list anywhere near
|
|
29
|
-
// this module. D-04 explicitly rejects that shape: it blocks a user on
|
|
30
|
-
// a newer, perfectly working regenerator2000 build, and it still
|
|
31
|
-
// cannot detect a schema break that lands *within* a permitted
|
|
32
|
-
// version range. The compatibility strategy here is minimality --
|
|
33
|
-
// write only the fields `ProjectState` requires (no `#[serde(default)]`
|
|
34
|
-
// on the Rust side) plus the two deliberately-forced settings, and let
|
|
35
|
-
// every other field's own default carry the rest. The self-check that
|
|
36
|
-
// this is compatible is running a real regenerator2000 against the
|
|
37
|
-
// synthesised file once (see `r2000-project.test.ts`'s gated
|
|
38
|
-
// integration half), never a version table.
|
|
39
|
-
//
|
|
40
|
-
// Ground truth for the shape below was independently re-verified against the
|
|
41
|
-
// installed regenerator2000-core-0.9.20 crate source (not merely paraphrased
|
|
42
|
-
// from CONTEXT.md) -- see
|
|
43
|
-
// `.planning/phases/10-adoption-boundaries-automated-bootstrap-and-the-removal/evidence/10-environment-recheck.txt`
|
|
44
|
-
// for the file:line citations. Summary: `ProjectState`'s only three fields
|
|
45
|
-
// without a `#[serde(default...)]` are `origin`, `raw_data` (serde-renamed
|
|
46
|
-
// to `raw_data_base64`), and `blocks`; `origin` is `Addr`, a
|
|
47
|
-
// `#[serde(transparent)]` newtype over `u16`, so it serialises as a plain
|
|
48
|
-
// JSON number; and `System`, `DocumentSettings.system`'s type, is likewise
|
|
49
|
-
// `#[serde(transparent)]` over `String`, so `settings.system` is a plain
|
|
50
|
-
// JSON string, with the C64 constant equal to the exact literal
|
|
51
|
-
// `Commodore 64`.
|
|
52
|
-
//
|
|
53
|
-
// `.vsf` is deliberately NOT an input to this module (D-03). Phase 9 found
|
|
54
|
-
// `.vsf`'s machine-type field only reads correctly by coincidence --
|
|
55
|
-
// `"C64SC"` matches none of regenerator2000's literal `System` arms and
|
|
56
|
-
// falls through to that tool's own C64 default.
|
|
57
|
-
//
|
|
58
|
-
// FLOW-02 (D-11.1-01): this comment used to end by naming a specific
|
|
59
|
-
// numbered phase as the eventual owner of closing that gap. That phase
|
|
60
|
-
// shipped and never touched `.vsf` bootstrap, so the pointer was false the
|
|
61
|
-
// moment that phase closed -- a phase number is a planning artifact, not a
|
|
62
|
-
// durable remediation path. The idea is recorded as backlog, not assigned
|
|
63
|
-
// to any phase: see
|
|
64
|
-
// `.planning/todos/pending/2026-08-20-vsf-as-a-bootstrap-input.md`. (This is
|
|
65
|
-
// a comment, not a user-facing string literal, so it is fixed here by hand
|
|
66
|
-
// rather than by `docs-dangling-refs.test.ts`'s guard -- see that guard's
|
|
67
|
-
// header for why it is deliberately scoped to string literals only.)
|
|
68
|
-
|
|
69
|
-
import { gzipSync, gunzipSync } from "node:zlib";
|
|
70
|
-
|
|
71
|
-
/** The exact literal regenerator2000's `System::C64` constant serialises as
|
|
72
|
-
* (`types.rs`, `#[serde(transparent)] pub struct System(String)`). This is
|
|
73
|
-
* the only system string this module ever writes unless a caller passes an
|
|
74
|
-
* explicit override. */
|
|
75
|
-
export const R2000_SYSTEM_C64 = "Commodore 64";
|
|
76
|
-
|
|
77
|
-
export interface SynthesizeOptions {
|
|
78
|
-
/** Load address (`ProjectState.origin`). `Addr` is `#[serde(transparent)]`
|
|
79
|
-
* over `u16`, so this is written as a plain JSON number -- never a hex
|
|
80
|
-
* string, never an object. */
|
|
81
|
-
origin: number;
|
|
82
|
-
/** `DocumentSettings.system`, a plain JSON string. Defaults to
|
|
83
|
-
* `R2000_SYSTEM_C64`. This module never infers, detects or defaults the
|
|
84
|
-
* machine type from the payload bytes -- an explicit caller-supplied (or
|
|
85
|
-
* defaulted-to-C64) value is the whole point (D-05): it is what makes
|
|
86
|
-
* Phase 9's `.vsf` machine-type coincidence unreachable rather than
|
|
87
|
-
* merely mitigated, because there is no inference path here to get
|
|
88
|
-
* wrong. */
|
|
89
|
-
system?: string;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Builds the JSON text of a `.regen2000proj` file from raw program bytes.
|
|
94
|
-
*
|
|
95
|
-
* The object written is EXACTLY four top-level keys: `origin`,
|
|
96
|
-
* `raw_data_base64`, `blocks`, and `settings` (itself exactly two keys:
|
|
97
|
-
* `use_illegal_opcodes` and `system`). Every other `ProjectState` member
|
|
98
|
-
* (`version`, `labels`, `user_side_comments`, `cursor_address`, and so on)
|
|
99
|
-
* carries its own `#[serde(default...)]` on the Rust side, so omitting them
|
|
100
|
-
* is the forward-compatibility strategy (D-04), not an oversight -- a field
|
|
101
|
-
* this module does not write is a field a future regenerator2000 release
|
|
102
|
-
* can freely add, rename the default of, or restructure without ever
|
|
103
|
-
* breaking this synthesiser. `version` is deliberately not written; it
|
|
104
|
-
* defaults to the crate's current `PROJECT_FORMAT_VERSION` on load.
|
|
105
|
-
*
|
|
106
|
-
* `settings.use_illegal_opcodes` and `settings.system` are deliberately NOT
|
|
107
|
-
* configurable to be turned off or omitted -- there is no flag, no option,
|
|
108
|
-
* no code path that skips writing either:
|
|
109
|
-
* - `use_illegal_opcodes` defaults to `false` on the Rust side
|
|
110
|
-
* (`settings.rs`). Illegal-opcode-*correct* decoding is the entire
|
|
111
|
-
* reason this project's now-removed `toacme` had caveats in the first
|
|
112
|
-
* place; making this setting optional here would silently reintroduce
|
|
113
|
-
* that exact defect as a configuration choice rather than closing it
|
|
114
|
-
* (D-05).
|
|
115
|
-
* - An explicit `system` on every synthesised project (rather than
|
|
116
|
-
* omitting the key and letting the Rust-side default apply) is what
|
|
117
|
-
* makes Phase 9's `.vsf` machine-type limit unreachable through this
|
|
118
|
-
* route rather than merely mitigated -- there is no coincidental
|
|
119
|
-
* fallback to fall into, because a value is always supplied.
|
|
120
|
-
*/
|
|
121
|
-
export function synthesizeProject(bytes: Uint8Array, opts: SynthesizeOptions): string {
|
|
122
|
-
const { origin, system = R2000_SYSTEM_C64 } = opts;
|
|
123
|
-
|
|
124
|
-
if (!Number.isInteger(origin) || origin < 0 || origin > 0xffff) {
|
|
125
|
-
throw new Error(
|
|
126
|
-
`synthesizeProject: origin ${origin} is out of range -- expected an integer 0..0xffff (0..65535)`,
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
if (bytes.length === 0) {
|
|
130
|
-
throw new Error("synthesizeProject: payload is empty -- a .regen2000proj must carry at least one byte");
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const raw_data_base64 = gzipSync(bytes).toString("base64");
|
|
134
|
-
|
|
135
|
-
const project = {
|
|
136
|
-
origin,
|
|
137
|
-
raw_data_base64,
|
|
138
|
-
blocks: [] as unknown[],
|
|
139
|
-
settings: {
|
|
140
|
-
// Forced true, never configurable -- see the function doc comment
|
|
141
|
-
// above and D-05. Do not add a parameter that overrides this.
|
|
142
|
-
use_illegal_opcodes: true,
|
|
143
|
-
// Always written explicitly, never omitted -- see the function doc
|
|
144
|
-
// comment above and D-05/Phase 9's .vsf finding.
|
|
145
|
-
system,
|
|
146
|
-
},
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
return JSON.stringify(project);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Parses a `.prg` file: a little-endian 2-byte load address followed by the
|
|
154
|
-
* payload bytes. This is the C64 program-file convention every C64 loader
|
|
155
|
-
* (and this project's own `acme-build` output) already follows.
|
|
156
|
-
*/
|
|
157
|
-
export function parsePrg(bytes: Uint8Array): { origin: number; body: Uint8Array } {
|
|
158
|
-
if (bytes.length < 3) {
|
|
159
|
-
throw new Error(
|
|
160
|
-
`parsePrg: input is ${bytes.length} byte(s) -- a .prg needs at least 3 bytes (2-byte load address plus at least 1 payload byte)`,
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
const origin = bytes[0]! | (bytes[1]! << 8);
|
|
164
|
-
const body = bytes.subarray(2);
|
|
165
|
-
return { origin, body };
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Returns the load address (`0`) for a flat 64K RAM capture, and throws for
|
|
170
|
-
* anything else. Flat 64K is in scope because `R2000-06` names it directly
|
|
171
|
-
* and it is exactly the shape `c64-ram-capture` already produces (D-03) --
|
|
172
|
-
* this function does not attempt to support any other flat-image size.
|
|
173
|
-
*/
|
|
174
|
-
export function flatImageOrigin(bytes: Uint8Array): number {
|
|
175
|
-
if (bytes.length !== 65536) {
|
|
176
|
-
throw new Error(
|
|
177
|
-
`flatImageOrigin: input is ${bytes.length} byte(s) -- a flat 64K capture must be exactly 65536 bytes`,
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
return 0;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* The inverse of the `raw_data_base64` encoding step: base64-decode then
|
|
185
|
-
* gunzip. Exported so tests can prove the payload round-trips exactly,
|
|
186
|
-
* rather than asserting against an opaque blob.
|
|
187
|
-
*/
|
|
188
|
-
export function decodeRawData(base64: string): Uint8Array {
|
|
189
|
-
return gunzipSync(Buffer.from(base64, "base64"));
|
|
190
|
-
}
|
package/r2000-symbols.ts
DELETED
|
@@ -1,388 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// r2000-symbols.ts -- the ONE authoritative place in this repo for the
|
|
3
|
-
// symbol round trip between regenerator2000's annotation store and stock
|
|
4
|
-
// VICE's symbol table (R2000-14/R2000-15, ARCHITECTURE.md Rule A20).
|
|
5
|
-
//
|
|
6
|
-
// WHY THIS FILE EXISTS: static-analysis symbols going OUT to VICE
|
|
7
|
-
// (`exportLabels()`) and live-discovered symbols coming IN from VICE
|
|
8
|
-
// (`importLabels()`) must flow through explicit adapter code -- neither side
|
|
9
|
-
// may parse the other's internal representation (Rule A20). This module is
|
|
10
|
-
// that adapter. It reuses `stock-symbols.ts`'s existing `al C:xxxx .Name`
|
|
11
|
-
// parser (`parseViceLabelFile()`, exported there for exactly this reuse)
|
|
12
|
-
// rather than adding this repo's THIRD copy of that format --
|
|
13
|
-
// `stock-symbols.ts` and `acme-build/scripts/acme.mjs`'s `curateLabels()`
|
|
14
|
-
// are the two that already exist.
|
|
15
|
-
//
|
|
16
|
-
// MEASURED FACTS this module's behaviour depends on:
|
|
17
|
-
// - `--export_lbl` exports USER labels only. Measured (Phase 9, and
|
|
18
|
-
// re-confirmed by this plan's own round-trip test): an annotated
|
|
19
|
-
// project emits exactly the labels a caller set via
|
|
20
|
-
// `r2000_set_label_name`/`--import_lbl` -- the auto-generated `a_D011` /
|
|
21
|
-
// `a_D020` / `e_FFD2` externals are NOT exported. A test asserting an
|
|
22
|
-
// `a_`-prefixed name appears in an `exportLabels()` result is testing
|
|
23
|
-
// the wrong thing.
|
|
24
|
-
// - `--import_lbl` under plain `--headless` DISCARDS. `main.rs:800-806` is
|
|
25
|
-
// `if headless && !mcp_server { return Ok(()) }`: an argv of
|
|
26
|
-
// `--import_lbl <path> --headless <proj>` imports the labels into
|
|
27
|
-
// memory and then hits that early return WITHOUT ever calling save, so
|
|
28
|
-
// the import is silently discarded -- measured live: two names imported
|
|
29
|
-
// that way, and a subsequent `--export_lbl` read back from disk
|
|
30
|
-
// returned only the pre-existing label. `r2000-launch.ts`'s
|
|
31
|
-
// `buildImportLblArgs()` makes this combination unbuildable by always
|
|
32
|
-
// pairing `--mcp-server-stdio` (which sets both `headless` AND
|
|
33
|
-
// `mcp_server`, `main.rs:709-711`, skipping the early return) --
|
|
34
|
-
// `r2000-symbol-roundtrip.test.ts` pins the trap itself with a
|
|
35
|
-
// hand-built argv, so that builder's pairing stays provably load-bearing
|
|
36
|
-
// rather than merely assumed.
|
|
37
|
-
//
|
|
38
|
-
// WHAT NOT TO DO, named concretely:
|
|
39
|
-
// - Never add a second `al C:xxxx .Name` regex anywhere in this file.
|
|
40
|
-
// Every read of a label file's TEXT goes through
|
|
41
|
-
// `stock-symbols.ts`'s exported `parseViceLabelFile()`.
|
|
42
|
-
// - Never call `vice_symbols_load` (`stock-symbols.ts`'s handlers)
|
|
43
|
-
// incrementally. `regenerateAndReload()` below regenerates the WHOLE
|
|
44
|
-
// `.lbl` and returns its path for the CALLER to hand to
|
|
45
|
-
// `vice_symbols_load` exactly once -- `vice_symbols_load` is
|
|
46
|
-
// deliberately replace-not-merge (T-05-02-05), so a full regeneration is
|
|
47
|
-
// what keeps that semantics correct rather than a limitation. A merge
|
|
48
|
-
// mode on `vice_symbols_load` itself was rejected: it would reopen a
|
|
49
|
-
// v0.2.0 decision and make a tool advertised on both backends diverge in
|
|
50
|
-
// semantics.
|
|
51
|
-
// - Never build `--import_lbl`'s argv by hand in this file. It comes ONLY
|
|
52
|
-
// from `r2000-launch.ts`'s `buildImportLblArgs()` -- there is no literal
|
|
53
|
-
// `"--import_lbl"` string anywhere below.
|
|
54
|
-
// - Never report an import as persisted on the strength of a
|
|
55
|
-
// no-error response alone. `importLabels()` below proves persistence
|
|
56
|
-
// TWICE: once via `saveAndVerify()`'s content-hash check (inside the
|
|
57
|
-
// import session), and independently via a fresh `exportLabels()` from
|
|
58
|
-
// disk, in a BRAND NEW process, after the import session has fully
|
|
59
|
-
// closed. `ImportLabelsResult` is a discriminated union
|
|
60
|
-
// (`diskVerified: true | false`) specifically so a caller cannot
|
|
61
|
-
// mistake "the import call returned no error" for "the names are
|
|
62
|
-
// actually on disk".
|
|
63
|
-
// - Never let an illegal label name from a `.lbl` file reach a spawned
|
|
64
|
-
// child (T-11-NAME-INJECT, closed). `importLabels()` validates every
|
|
65
|
-
// name against `r2000-acme-ident.ts`'s `assertLegalAcmeIdentifier()`
|
|
66
|
-
// BEFORE `buildImportLblArgs()` is ever called -- REJECT, never
|
|
67
|
-
// sanitize, matching `r2000-tools.ts`'s `r2000_set_label_name` posture.
|
|
68
|
-
import { existsSync, mkdtempSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
69
|
-
import { tmpdir } from "node:os";
|
|
70
|
-
import { join } from "node:path";
|
|
71
|
-
|
|
72
|
-
import { buildExportLblArgs, buildImportLblArgs, runR2000 } from "./r2000-launch.ts";
|
|
73
|
-
import { withR2000Session, saveAndVerify } from "./r2000-mcp-client.ts";
|
|
74
|
-
import { runR2000Tool } from "./r2000-tools.ts";
|
|
75
|
-
import { parseViceLabelFile, MAX_LABEL_FILE_BYTES } from "./stock-symbols.ts";
|
|
76
|
-
import { assertLegalAcmeIdentifier } from "./r2000-acme-ident.ts";
|
|
77
|
-
|
|
78
|
-
/** This module's own error class, following `r2000-tools.ts`'s
|
|
79
|
-
* `R2000StorePathError` / `r2000-launch.ts`'s `R2000ViceFlagError` minimal
|
|
80
|
-
* shape (message-only, `.name` set to the class name). Never thrown for a
|
|
81
|
-
* ceiling violation on a `.lbl` file's TEXT -- those come from
|
|
82
|
-
* `stock-symbols.ts`'s `StockSymbolsError`, surfaced verbatim, never
|
|
83
|
-
* re-wrapped as this class. Reserved for this module's OWN failure modes:
|
|
84
|
-
* a nonzero regenerator2000 exit, a missing output file despite a zero
|
|
85
|
-
* exit, an oversized file caught before `parseViceLabelFile()` is ever
|
|
86
|
-
* called, or (T-11-NAME-INJECT, closed) an illegal label name caught in
|
|
87
|
-
* `importLabels()` before any child is spawned -- naming the offending
|
|
88
|
-
* name, its 1-based line number, and that line's own text. */
|
|
89
|
-
export class R2000SymbolsError extends Error {
|
|
90
|
-
constructor(message: string) {
|
|
91
|
-
super(message);
|
|
92
|
-
this.name = "R2000SymbolsError";
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export interface LabelEntry {
|
|
97
|
-
name: string;
|
|
98
|
-
address: number;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export interface ExportLabelsOptions {
|
|
102
|
-
projectPath: string;
|
|
103
|
-
outPath: string;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export interface ExportLabelsResult {
|
|
107
|
-
path: string;
|
|
108
|
-
symbolCount: number;
|
|
109
|
-
symbols: LabelEntry[];
|
|
110
|
-
skippedLines: number;
|
|
111
|
-
duplicateNames: number;
|
|
112
|
-
lineCount: number;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* The export leg (R2000-14). Runs `buildExportLblArgs()` through
|
|
117
|
-
* `runR2000()`, then READS THE PRODUCED FILE BACK through
|
|
118
|
-
* `stock-symbols.ts`'s existing parser to validate it and return the parsed
|
|
119
|
-
* symbol list plus a count.
|
|
120
|
-
*
|
|
121
|
-
* The read-back is not optional, for two independent reasons: a
|
|
122
|
-
* regenerator2000 exit code has lied before (`r2000-verify.ts`'s founding
|
|
123
|
-
* incident, D-10 -- a zero exit alongside content that should have failed),
|
|
124
|
-
* and the parse is the only thing that proves the produced file is in the
|
|
125
|
-
* format `vice_symbols_load` actually accepts. Ceiling violations from
|
|
126
|
-
* `parseViceLabelFile()` (`StockSymbolsError`) are surfaced VERBATIM, never
|
|
127
|
-
* re-wrapped -- only this function's OWN failure modes (nonzero exit,
|
|
128
|
-
* missing file, oversized file) throw `R2000SymbolsError`.
|
|
129
|
-
*/
|
|
130
|
-
export async function exportLabels({ projectPath, outPath }: ExportLabelsOptions): Promise<ExportLabelsResult> {
|
|
131
|
-
const argv = buildExportLblArgs({ projectPath, outPath });
|
|
132
|
-
const result = runR2000(argv);
|
|
133
|
-
if (result.status !== 0) {
|
|
134
|
-
throw new R2000SymbolsError(
|
|
135
|
-
`exportLabels: regenerator2000 exited ${result.status} for "${projectPath}" -- stderr: ${result.stderr || "(empty)"}`,
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
if (!existsSync(outPath)) {
|
|
139
|
-
throw new R2000SymbolsError(
|
|
140
|
-
`exportLabels: regenerator2000 exited 0 but did not produce "${outPath}" -- a lying zero exit code has ` +
|
|
141
|
-
"happened before (r2000-verify.ts's founding incident, D-10); refusing to trust the exit code alone.",
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
let size: number;
|
|
146
|
-
try {
|
|
147
|
-
size = statSync(outPath).size;
|
|
148
|
-
} catch (err) {
|
|
149
|
-
throw new R2000SymbolsError(
|
|
150
|
-
`exportLabels: could not stat "${outPath}" (${err instanceof Error ? err.message : String(err)})`,
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
if (size > MAX_LABEL_FILE_BYTES) {
|
|
154
|
-
throw new R2000SymbolsError(
|
|
155
|
-
`exportLabels: "${outPath}" is ${size} bytes, which exceeds the ${MAX_LABEL_FILE_BYTES}-byte ceiling`,
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const text = readFileSync(outPath, "utf8");
|
|
160
|
-
// Reused verbatim -- stock-symbols.ts's ONE parser, never a second regex.
|
|
161
|
-
// A ceiling violation here (StockSymbolsError) propagates unmodified.
|
|
162
|
-
const parsed = parseViceLabelFile(text);
|
|
163
|
-
|
|
164
|
-
const symbols: LabelEntry[] = Array.from(parsed.table.byName.entries()).map(([name, address]) => ({ name, address }));
|
|
165
|
-
|
|
166
|
-
return {
|
|
167
|
-
path: outPath,
|
|
168
|
-
symbolCount: parsed.symbolCount,
|
|
169
|
-
symbols,
|
|
170
|
-
skippedLines: parsed.skippedLines,
|
|
171
|
-
duplicateNames: parsed.duplicateNames,
|
|
172
|
-
lineCount: parsed.lineCount,
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export interface ImportLabelsOptions {
|
|
177
|
-
projectPath: string;
|
|
178
|
-
lblPath: string;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export interface ImportLabelsVerified {
|
|
182
|
-
diskVerified: true;
|
|
183
|
-
importedNames: string[];
|
|
184
|
-
/** The fresh, independent `exportLabels()` result used to prove
|
|
185
|
-
* persistence -- a caller can inspect it without re-exporting itself. */
|
|
186
|
-
exported: ExportLabelsResult;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export interface ImportLabelsUnverified {
|
|
190
|
-
diskVerified: false;
|
|
191
|
-
importedNames: string[];
|
|
192
|
-
/** Names present in the imported `.lbl` file that a fresh export from
|
|
193
|
-
* disk did NOT contain. Always non-empty when `diskVerified` is `false`. */
|
|
194
|
-
missingNames: string[];
|
|
195
|
-
reason: string;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* A discriminated union, deliberately -- `diskVerified: true` and
|
|
200
|
-
* `diskVerified: false` are structurally distinct shapes, so a caller
|
|
201
|
-
* cannot read `result.importedNames` and mistake "the import call returned
|
|
202
|
-
* no error" for "the names are actually on disk" (the plan's own required
|
|
203
|
-
* distinction). Only the `true` variant carries `exported`; only the
|
|
204
|
-
* `false` variant carries `missingNames`/`reason`.
|
|
205
|
-
*/
|
|
206
|
-
export type ImportLabelsResult = ImportLabelsVerified | ImportLabelsUnverified;
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* The import leg (R2000-15, the D-28 path). Runs `buildImportLblArgs()` --
|
|
210
|
-
* which ALWAYS carries `--mcp-server-stdio` -- and, over that SAME stdio
|
|
211
|
-
* session, calls `r2000_save_project` through `r2000-mcp-client.ts`'s
|
|
212
|
-
* `saveAndVerify()`. `--import_lbl` mutates only IN-MEMORY state
|
|
213
|
-
* (`main.rs:800-806` is why the save must be explicit); `saveAndVerify()`
|
|
214
|
-
* proves the save changed the project file's own content hash on disk.
|
|
215
|
-
*
|
|
216
|
-
* That alone is `r2000-mcp-client.ts`'s own T-11-FALSESUCCESS proof, not
|
|
217
|
-
* THIS module's. `importLabels()` proves persistence a SECOND, independent
|
|
218
|
-
* way: after the import session has fully closed, a BRAND NEW process
|
|
219
|
-
* (`exportLabels()`, a fresh `runR2000()` child) re-reads the project from
|
|
220
|
-
* disk and re-exports its labels. Only when every name in the caller's
|
|
221
|
-
* `.lbl` file appears in that fresh export does this function report
|
|
222
|
-
* `diskVerified: true`.
|
|
223
|
-
*
|
|
224
|
-
* The caller-supplied `.lbl` file itself is ceiling-checked BEFORE any
|
|
225
|
-
* child process is spawned (T-11-LBL-SIZE): a byte-size check against
|
|
226
|
-
* `stock-symbols.ts`'s own `MAX_LABEL_FILE_BYTES`, then a full
|
|
227
|
-
* `parseViceLabelFile()` pass, whose `StockSymbolsError` ceiling violations
|
|
228
|
-
* (`MAX_LABEL_FILE_LINES`/`MAX_SYMBOLS`) propagate verbatim -- an oversized
|
|
229
|
-
* or over-populated `.lbl` never reaches regenerator2000 at all.
|
|
230
|
-
*
|
|
231
|
-
* Every discovered name is then validated against `r2000-acme-ident.ts`'s
|
|
232
|
-
* `assertLegalAcmeIdentifier()`, also BEFORE any spawn (T-11-NAME-INJECT,
|
|
233
|
-
* closed): an illegal name throws `R2000SymbolsError` naming the offending
|
|
234
|
-
* name, its 1-based line number, and that line's own text -- REJECT, never
|
|
235
|
-
* sanitize, the same posture `r2000-tools.ts`'s `r2000_set_label_name`
|
|
236
|
-
* takes on the tool-surface entry route.
|
|
237
|
-
*/
|
|
238
|
-
export async function importLabels({ projectPath, lblPath }: ImportLabelsOptions): Promise<ImportLabelsResult> {
|
|
239
|
-
let size: number;
|
|
240
|
-
try {
|
|
241
|
-
size = statSync(lblPath).size;
|
|
242
|
-
} catch (err) {
|
|
243
|
-
throw new R2000SymbolsError(
|
|
244
|
-
`importLabels: could not stat "${lblPath}" (${err instanceof Error ? err.message : String(err)})`,
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
if (size > MAX_LABEL_FILE_BYTES) {
|
|
248
|
-
throw new R2000SymbolsError(
|
|
249
|
-
`importLabels: "${lblPath}" is ${size} bytes, which exceeds the ${MAX_LABEL_FILE_BYTES}-byte ceiling`,
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const inputText = readFileSync(lblPath, "utf8");
|
|
254
|
-
// Reused verbatim -- same parser as exportLabels(). A ceiling violation
|
|
255
|
-
// here (StockSymbolsError) propagates unmodified, before any spawn.
|
|
256
|
-
const inputParsed = parseViceLabelFile(inputText);
|
|
257
|
-
const importedNames = Array.from(inputParsed.table.byName.keys());
|
|
258
|
-
|
|
259
|
-
// T-11-NAME-INJECT (route B, closed): every discovered label name is
|
|
260
|
-
// validated against the one ACME identifier seam (r2000-acme-ident.ts)
|
|
261
|
-
// BEFORE any child is spawned -- REJECT, never sanitize, matching
|
|
262
|
-
// r2000-tools.ts's r2000_set_label_name posture. The offending line is
|
|
263
|
-
// located by a substring search over the already-read inputText, never a
|
|
264
|
-
// second `al C:` regex (this module's own header forbids a third parser
|
|
265
|
-
// for that format, T-11-LBL-PARSER-DUP).
|
|
266
|
-
for (const name of importedNames) {
|
|
267
|
-
try {
|
|
268
|
-
assertLegalAcmeIdentifier(name, "importLabels label name");
|
|
269
|
-
} catch (err) {
|
|
270
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
271
|
-
const inputLines = inputText.split(/\r?\n/);
|
|
272
|
-
const lineIndex = inputLines.findIndex((line) => line.includes(name));
|
|
273
|
-
const lineNumber = lineIndex === -1 ? 0 : lineIndex + 1;
|
|
274
|
-
const lineText = lineIndex === -1 ? "(line not found)" : inputLines[lineIndex];
|
|
275
|
-
throw new R2000SymbolsError(
|
|
276
|
-
`importLabels: "${lblPath}" line ${lineNumber} carries an illegal label name "${name}" (${reason}) -- ` +
|
|
277
|
-
`line text: ${JSON.stringify(lineText)}. REJECTED, never sanitized or quoted, before any child is spawned.`,
|
|
278
|
-
);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// buildImportLblArgs() is the ONLY producer of this argv -- there is no
|
|
283
|
-
// literal "--import_lbl" string anywhere in this file.
|
|
284
|
-
const argv = buildImportLblArgs({ projectPath, lblPath });
|
|
285
|
-
await withR2000Session(projectPath, (call) => saveAndVerify(projectPath, call), { argv });
|
|
286
|
-
|
|
287
|
-
// Independent proof #2: a brand-new process, after the import session has
|
|
288
|
-
// fully closed, re-reads the project from disk and re-exports its labels.
|
|
289
|
-
const verifyDir = mkdtempSync(join(tmpdir(), "r2000-symbols-verify-"));
|
|
290
|
-
try {
|
|
291
|
-
const reExportPath = join(verifyDir, "reexport.lbl");
|
|
292
|
-
const exported = await exportLabels({ projectPath, outPath: reExportPath });
|
|
293
|
-
const exportedNames = new Set(exported.symbols.map((s) => s.name));
|
|
294
|
-
const missingNames = importedNames.filter((n) => !exportedNames.has(n));
|
|
295
|
-
|
|
296
|
-
if (missingNames.length > 0) {
|
|
297
|
-
return {
|
|
298
|
-
diskVerified: false,
|
|
299
|
-
importedNames,
|
|
300
|
-
missingNames,
|
|
301
|
-
reason:
|
|
302
|
-
`importLabels: saveAndVerify() reported a changed content hash for "${projectPath}", but a fresh ` +
|
|
303
|
-
`exportLabels() from disk is missing ${missingNames.length} of ${importedNames.length} imported ` +
|
|
304
|
-
`name(s): ${missingNames.join(", ")}`,
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
return { diskVerified: true, importedNames, exported };
|
|
309
|
-
} finally {
|
|
310
|
-
rmSync(verifyDir, { recursive: true, force: true });
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
export interface RegenerateAndReloadOptions {
|
|
315
|
-
projectPath: string;
|
|
316
|
-
outPath: string;
|
|
317
|
-
address: number;
|
|
318
|
-
name: string;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
export interface RegenerateAndReloadResult {
|
|
322
|
-
path: string;
|
|
323
|
-
symbolCount: number;
|
|
324
|
-
symbols: LabelEntry[];
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* D-29: the store is the merge point for a live-discovered name. Writes
|
|
329
|
-
* `name` at `address` into the store FIRST (via the curated
|
|
330
|
-
* `r2000_set_label_name`, `r2000-tools.ts`'s `runR2000Tool()` -- which
|
|
331
|
-
* auto-saves internally before its session exits), THEN regenerates the
|
|
332
|
-
* WHOLE `.lbl` with `exportLabels()`, and returns the path for the CALLER
|
|
333
|
-
* to hand to `vice_symbols_load` EXACTLY ONCE.
|
|
334
|
-
*
|
|
335
|
-
* This function deliberately does NOT call `vice_symbols_load` itself --
|
|
336
|
-
* see this module's header for why an incremental/repeated load would
|
|
337
|
-
* violate `vice_symbols_load`'s replace-not-merge semantics (T-05-02-05).
|
|
338
|
-
*
|
|
339
|
-
* Note (T-11-NAME-INJECT, closed): an illegal `name` now REJECTS this
|
|
340
|
-
* function's returned promise -- `r2000_set_label_name`'s own pre-spawn
|
|
341
|
-
* `assertLegalLabelArg()` gate (`r2000-tools.ts`) fires inside
|
|
342
|
-
* `runR2000Tool()`'s `assertCuratedTool()` call, before `runR2000Tool()`'s
|
|
343
|
-
* own `try` block, rather than surfacing as `setResult.isError` the way a
|
|
344
|
-
* regenerator2000-side failure would. This function had no production
|
|
345
|
-
* caller when that note was written, so no existing behaviour depended on
|
|
346
|
-
* the old `isError`-shaped outcome -- superseded in place by the formal
|
|
347
|
-
* status below rather than left as an aside.
|
|
348
|
-
*
|
|
349
|
-
* LIBRARY-ONLY (Phase 11 IN-02, D-11.1-06): as of this phase,
|
|
350
|
-
* `regenerateAndReload()` is a library-only export -- available for
|
|
351
|
-
* programmatic use, with NO PRODUCTION CALLER anywhere in this repo
|
|
352
|
-
* (`.claude/mcp/vice/`, `.claude/skills/`, `scripts/`). A future phase must
|
|
353
|
-
* not assume it is wired into any real workflow just because it is the
|
|
354
|
-
* named D-29 live-discovery merge point.
|
|
355
|
-
*
|
|
356
|
-
* The proven live path for the symbol round trip is NOT this function --
|
|
357
|
-
* it is `r2000 export-lbl` -> `vice_symbols_load` -> live discovery ->
|
|
358
|
-
* `r2000_set_label_name` -> `r2000 import-lbl`, documented as one closed
|
|
359
|
-
* loop in `c64-program-recon/SKILL.md`. `R2000-15` is satisfied through
|
|
360
|
-
* that sequence, not through this convenience wrapper. Giving this
|
|
361
|
-
* function a caller would mean inventing a new CLI verb or skill entry
|
|
362
|
-
* point it was never actually proven through -- out of this phase's scope
|
|
363
|
-
* fence (11.1-CONTEXT.md).
|
|
364
|
-
*
|
|
365
|
-
* ADOPTION CONDITION: if `regenerateAndReload()` ever acquires a real
|
|
366
|
-
* production caller, the `LIBRARY-ONLY` marker above must be deleted in
|
|
367
|
-
* the SAME commit that adds the caller. `r2000-symbol-roundtrip.test.ts`
|
|
368
|
-
* enforces this as a biconditional, in both directions: zero callers
|
|
369
|
-
* requires the marker present; one or more callers requires the marker
|
|
370
|
-
* absent. The two can never legally disagree.
|
|
371
|
-
*/
|
|
372
|
-
export async function regenerateAndReload({
|
|
373
|
-
projectPath,
|
|
374
|
-
outPath,
|
|
375
|
-
address,
|
|
376
|
-
name,
|
|
377
|
-
}: RegenerateAndReloadOptions): Promise<RegenerateAndReloadResult> {
|
|
378
|
-
const setResult = await runR2000Tool("r2000_set_label_name", { project: projectPath, address, name });
|
|
379
|
-
if (setResult.isError) {
|
|
380
|
-
throw new R2000SymbolsError(
|
|
381
|
-
`regenerateAndReload: r2000_set_label_name failed for "${name}" at ${address}: ` +
|
|
382
|
-
`${setResult.content.map((c) => c.text).join(" ")}`,
|
|
383
|
-
);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const exported = await exportLabels({ projectPath, outPath });
|
|
387
|
-
return { path: exported.path, symbolCount: exported.symbolCount, symbols: exported.symbols };
|
|
388
|
-
}
|