@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.
Files changed (57) hide show
  1. package/README.md +2 -1
  2. package/THIRD-PARTY-NOTICES.md +1 -24
  3. package/{r2000-acme-ident.ts → anno-acme-ident.ts} +13 -13
  4. package/anno-cli.ts +1465 -0
  5. package/{r2000-confidence.ts → anno-confidence.ts} +22 -22
  6. package/anno-coverage.ts +2465 -0
  7. package/{r2000-d64.ts → anno-d64.ts} +5 -5
  8. package/anno-derive.ts +590 -0
  9. package/anno-details.ts +169 -0
  10. package/anno-enum-gen.ts +533 -0
  11. package/anno-export-asm.ts +1310 -0
  12. package/anno-index.ts +150 -0
  13. package/{r2000-memmap-render.ts → anno-memmap-render.ts} +236 -95
  14. package/{r2000-regbits-gen.ts → anno-regbits-gen.ts} +20 -15
  15. package/{r2000-regbits.json → anno-regbits.json} +2 -2
  16. package/anno-register.ts +240 -0
  17. package/anno-store.ts +3486 -0
  18. package/anno-symbols.ts +266 -0
  19. package/anno-tools.ts +2111 -0
  20. package/anno-types.ts +1636 -0
  21. package/block-class.ts +201 -0
  22. package/build.ts +1 -1
  23. package/capability-registry.ts +3 -1
  24. package/disasm-decoder.ts +14 -14
  25. package/disasm-opcodes.ts +4 -4
  26. package/disasm-renderer.ts +2 -2
  27. package/hostpath.ts +1 -1
  28. package/install-resources.ts +1 -1
  29. package/package.json +23 -17
  30. package/prg-image.ts +119 -0
  31. package/repo-root.ts +20 -5
  32. package/resources/broker-launch.mjs +8 -4
  33. package/resources/vice-launcher.sh +3 -3
  34. package/stock-address.ts +5 -5
  35. package/stock-cia.ts +2 -2
  36. package/stock-condition.ts +7 -7
  37. package/stock-connect.ts +1 -1
  38. package/stock-dispatch.ts +35 -5
  39. package/stock-execution.ts +5 -3
  40. package/stock-input.ts +9 -9
  41. package/stock-machine.ts +17 -6
  42. package/stock-protocol.ts +16 -11
  43. package/stock-registers.ts +54 -29
  44. package/stock-sprites.ts +3 -3
  45. package/stock-symbols.ts +9 -9
  46. package/stock-timing.ts +1 -1
  47. package/stock-vicii.ts +1 -1
  48. package/version.ts +1 -1
  49. package/vice-proxy.ts +68 -46
  50. package/r2000-cli.ts +0 -1103
  51. package/r2000-enum-gen.ts +0 -574
  52. package/r2000-launch.ts +0 -357
  53. package/r2000-mcp-client.ts +0 -596
  54. package/r2000-project.ts +0 -190
  55. package/r2000-symbols.ts +0 -388
  56. package/r2000-tools.ts +0 -914
  57. 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
- }