@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.
Files changed (49) hide show
  1. package/README.md +2 -1
  2. package/THIRD-PARTY-NOTICES.md +1 -1
  3. package/anno-acme-ident.ts +97 -0
  4. package/anno-cli.ts +1465 -0
  5. package/anno-confidence.ts +233 -0
  6. package/anno-coverage.ts +2465 -0
  7. package/anno-d64.ts +310 -0
  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/anno-memmap-render.ts +672 -0
  14. package/anno-regbits-gen.ts +421 -0
  15. package/anno-regbits.json +1370 -0
  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 -3
  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 +33 -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 +168 -0
@@ -0,0 +1,421 @@
1
+ #!/usr/bin/env node
2
+ // anno-regbits-gen.ts -- the ONE authoritative place in this repo that turns
3
+ // c64-memory-mapping's memmap.json into the curated address->bit-name table
4
+ // anno-enum-gen.ts decodes register values against (D-22, ANNO-13).
5
+ //
6
+ // WHY THIS EXISTS (D-22): neither register the phase's own pinned criterion-3
7
+ // target needs ($D011) nor the registers a real game writes to constantly
8
+ // ($D015/$D017/$D01A-$D01D) can be named from memmap.json's own `bits` prose
9
+ // alone -- some of that prose is OCR-damaged ("O = Blank" uses a letter O for
10
+ // the digit 0; "Read NMls" uses a lowercase L for an uppercase I), and five
11
+ // addresses have NO `bits` entry in memmap.json at all (its `io` parser never
12
+ // produced one for them; widening memmap.json itself is separate work
13
+ // belonging to `c64-memory-mapping`, not this phase). `OVERRIDES` below is
14
+ // the curated fix for both problems, carrying a WHY comment on every entry
15
+ // so a future reader never has to guess why a bit was hand-named instead of
16
+ // mechanically derived.
17
+ //
18
+ // WHAT THIS IS THE ONE AUTHORITATIVE PLACE FOR: reading memmap.json's 29
19
+ // structured `bits` entries, normalising each `bits[].bit` range string into
20
+ // `{mask, shift}`, deriving (or overriding) a legal ACME identifier for every
21
+ // field, and emitting the committed, banner-marked `anno-regbits.json`
22
+ // artifact `anno-enum-gen.ts` decodes against. No other module may read
23
+ // memmap.json for this purpose or hand-maintain a second bit-name table.
24
+ //
25
+ // KEY-SHAPE DECISION: table keys are `$XXXX` (uppercase, 4-hex-digit,
26
+ // dollar-prefixed) strings, not decimal numbers -- this matches how every
27
+ // register in this project's own assembly/documentation conventions is
28
+ // named (`$D011`, not `53265`), and because every key is the same fixed
29
+ // width, `Object.keys(table).sort()` on the STRING keys already produces the
30
+ // same order as sorting the underlying addresses numerically, so no separate
31
+ // numeric-sort step is needed to keep the emitted JSON diff-stable.
32
+ //
33
+ // WHAT NOT TO DO, named concretely:
34
+ // - Never hand-edit anno-regbits.json. It is a generated-but-committed
35
+ // artifact (ENGINEERING_RULES.md Sec 11), the same shape
36
+ // `resources-sync.test.ts` already established for compiled `.mjs`
37
+ // build output -- re-run `node anno-regbits-gen.ts` and let the drift
38
+ // guard in anno-regbits.test.ts confirm the result matches.
39
+ // - Never silently skip or placeholder an unmappable bit description.
40
+ // `buildRegBits()` THROWS, naming the address, the bit range and the
41
+ // offending description, when mechanical derivation fails AND no
42
+ // OVERRIDES entry covers it -- the difference between a curated table
43
+ // and an implicit one is exactly this refusal.
44
+ // - Never emit a timestamp into the banner. The drift guard's whole
45
+ // comparison (`buildRegBits()` re-run in memory vs. the committed file)
46
+ // is only TOTAL because nothing in the emitted document changes between
47
+ // two runs against the same memmap.json bytes.
48
+ import { readFileSync, writeFileSync } from "node:fs";
49
+ import { createHash } from "node:crypto";
50
+ import { dirname, join } from "node:path";
51
+ import { fileURLToPath, pathToFileURL } from "node:url";
52
+
53
+ const HERE = dirname(fileURLToPath(import.meta.url));
54
+
55
+ /** The sole read of c64-memory-mapping's own memmap.json -- this generator is
56
+ * its only consumer for this purpose (per this plan's key_links entry).
57
+ * 2026-08-22 (plan 16-01): the skills tree moved from `.claude/skills/`
58
+ * (two levels up from `.claude/mcp/vice`) to `src/skills/` (three levels up,
59
+ * since `src/` sits directly under the repo root rather than under `.claude/`).
60
+ * This literal was not in plan 16-01's own enumerated consumer list and its
61
+ * test failures caught the gap live -- see 16-01-SUMMARY.md deviations. */
62
+ const MEMMAP_PATH = join(HERE, "..", "..", "..", "src", "skills", "c64-memory-mapping", "memmap.json");
63
+
64
+ /** Where the generated, committed artifact lives -- always a sibling of this
65
+ * generator, never a caller-supplied path. */
66
+ const OUTPUT_PATH = join(HERE, "anno-regbits.json");
67
+
68
+ export type FieldKind = "flag" | "numeric" | "enum";
69
+
70
+ export interface RegBitsField {
71
+ mask: number;
72
+ shift: number;
73
+ name: string;
74
+ kind: FieldKind;
75
+ /** Present only for "flag"/"enum" fields whose decoded value maps to a
76
+ * specific token string. A field silent-by-design in one state (e.g. a
77
+ * "the bit only matters when set" flag like ECM/RST8 below) still carries
78
+ * an EXPLICIT entry for that state -- an empty string, never an absent
79
+ * key -- so decoding never has to guess whether an omission was
80
+ * deliberate. */
81
+ tokens?: Record<number, string>;
82
+ }
83
+
84
+ export interface RegBitsEntry {
85
+ label: string;
86
+ fields: RegBitsField[];
87
+ }
88
+
89
+ export type RegBitsTable = Record<string, RegBitsEntry>;
90
+
91
+ const ACME_IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
92
+
93
+ interface MemmapBit {
94
+ bit: string;
95
+ desc: string;
96
+ }
97
+
98
+ interface MemmapEntry {
99
+ start: number;
100
+ end: number;
101
+ label?: string | null;
102
+ desc?: string;
103
+ bits?: MemmapBit[];
104
+ }
105
+
106
+ interface MemmapFile {
107
+ sources: unknown[];
108
+ entries: MemmapEntry[];
109
+ }
110
+
111
+ /**
112
+ * Parses a memmap `bits[].bit` string into `{mask, shift}`. Handles a single
113
+ * index ("7"), a descending range ("2-0") and an ascending range ("6-7") --
114
+ * both range forms appear in memmap.json's real data, and the result is
115
+ * identical either way since a bit RANGE has no inherent direction; only the
116
+ * high/low bounds matter for the mask this produces.
117
+ */
118
+ export function parseBitRange(bitStr: string): { mask: number; shift: number } {
119
+ const parts = bitStr.split("-").map((s) => Number.parseInt(s.trim(), 10));
120
+ if (parts.length === 1) {
121
+ const bit = parts[0]!;
122
+ return { mask: 1 << bit, shift: bit };
123
+ }
124
+ const [a, b] = parts as [number, number];
125
+ const hi = Math.max(a, b);
126
+ const lo = Math.min(a, b);
127
+ const width = hi - lo + 1;
128
+ return { mask: ((1 << width) - 1) << lo, shift: lo };
129
+ }
130
+
131
+ /**
132
+ * Mechanically derives an ACME-legal identifier from a memmap bit
133
+ * description: drops parenthetical asides, uppercases, replaces every
134
+ * non-alphanumeric run with a single underscore, and trims leading/trailing
135
+ * underscores. Returns `null` (never throws) when the result is empty or
136
+ * still not a legal identifier -- the caller decides whether to consult
137
+ * `OVERRIDES` or throw; this function's job is only the mechanical half of
138
+ * that decision.
139
+ */
140
+ export function deriveIdentifier(desc: string): string | null {
141
+ let s = desc.replace(/\([^)]*\)/g, " ");
142
+ s = s.toUpperCase();
143
+ s = s.replace(/[^A-Z0-9]+/g, "_");
144
+ s = s.replace(/^_+|_+$/g, "");
145
+ if (s === "") return null;
146
+ if (!ACME_IDENT_RE.test(s)) return null;
147
+ return s;
148
+ }
149
+
150
+ export interface RegbitsFieldOverride {
151
+ /** Must match a memmap `bits[].bit` string EXACTLY (e.g. "2-0", "4"). */
152
+ bit: string;
153
+ name: string;
154
+ kind: FieldKind;
155
+ tokens?: Record<number, string>;
156
+ }
157
+
158
+ export interface RegbitsRegisterOverride {
159
+ address: number;
160
+ /** Overrides memmap's own `label` for this address (OCR-damage fix). */
161
+ label?: string;
162
+ /** Per-bit overrides, matched against this address's own memmap `bits`
163
+ * entries by their `bit` string. */
164
+ fields?: readonly RegbitsFieldOverride[];
165
+ /** A COMPLETE field list for an address memmap.json's `io` parser produced
166
+ * no `bits` entry for at all (D-22's known gap) -- used only when no
167
+ * memmap entry exists for this address, never to replace one that does. */
168
+ synthetic?: readonly RegBitsField[];
169
+ }
170
+
171
+ /** Builds the 8 independent "bit N = sprite N" flag fields the five VIC
172
+ * sprite-plane registers ($D015/$D017/$D01B/$D01C/$D01D) all share the same
173
+ * shape for (D-22's named gap) -- each bit is silent (empty token) when
174
+ * clear and names the specific sprite when set, so a typical enum (most
175
+ * sprites off, one or two on) renders as a short, readable name instead of
176
+ * naming all eight sprites' negative state every time.
177
+ */
178
+ function spriteBitFields(suffix: string): RegBitsField[] {
179
+ const fields: RegBitsField[] = [];
180
+ for (let n = 0; n < 8; n++) {
181
+ fields.push({
182
+ mask: 1 << n,
183
+ shift: n,
184
+ name: `SPR${n}${suffix}`,
185
+ kind: "flag",
186
+ tokens: { 0: "", 1: `SPR${n}${suffix}` },
187
+ });
188
+ }
189
+ return fields;
190
+ }
191
+
192
+ // ---------------------------------------------------------------------------
193
+ // OVERRIDES -- every entry carries its own WHY comment immediately above it
194
+ // (anno-regbits.test.ts's own non-vacuity check counts these two things
195
+ // against each other, so removing a comment without removing its entry, or
196
+ // vice versa, fails a test rather than silently drifting).
197
+ // ---------------------------------------------------------------------------
198
+
199
+ export const OVERRIDES: readonly RegbitsRegisterOverride[] = [
200
+ {
201
+ address: 53265, // $D011 -- VIC Control Register 1, this phase's pinned criterion-3 target
202
+ fields: [
203
+ // WHY: memmap's own prose ("Smooth Scroll to Y Dot-Position (0-7)") reads as a sentence, not
204
+ // a compact field name; mechanical derivation would bake the whole sentence into the
205
+ // identifier instead of the short numeric field the pinned criterion-3 target needs
206
+ // (YSCROLL3, not SMOOTH_SCROLL_TO_Y_DOT_POSITION3).
207
+ { bit: "2-0", name: "YSCROLL", kind: "numeric" },
208
+ // WHY: OCR damage -- "O = Blank" uses a letter O for the digit 0, and the raw prose reads as
209
+ // a NEGATIVE ("0 = Blank" the screen, i.e. bit=0 blanks it) rather than the positive
210
+ // "is the screen on" framing this table wants. This override fixes both problems in one
211
+ // place: the OCR-damaged letter, and the polarity, landing on the pinned SCREENON name.
212
+ { bit: "4", name: "SCREENON", kind: "flag", tokens: { 0: "SCREENOFF", 1: "SCREENON" } },
213
+ // WHY: memmap's prose ("Select 24/25 Row Text Display: 1 = 25 Rows") mixes the field name
214
+ // with its own value table; this override extracts the clean ROW24/ROW25 pair the pinned
215
+ // criterion-3 target needs instead of a mechanical transcription of the whole sentence.
216
+ { bit: "3", name: "ROWS", kind: "flag", tokens: { 0: "ROW24", 1: "ROW25" } },
217
+ // WHY: same shape as ROWS above -- "Bit Map Mode. 1 = Enable" is this register's other
218
+ // genuinely-binary choice (text vs. bitmap), and both states are equally meaningful, so both
219
+ // get a name rather than one state being silent.
220
+ { bit: "5", name: "MODE", kind: "flag", tokens: { 0: "TEXT", 1: "BITMAP" } },
221
+ // WHY: Extended Color Mode is off in the overwhelming majority of real programs' writes to
222
+ // this register; naming only the "on" state (silent when clear, via an explicit empty-string
223
+ // token for 0 -- never an absent key) keeps a typical generated variant name short rather
224
+ // than always appending "_ECMOFF" to every single value.
225
+ { bit: "6", name: "ECM", kind: "flag", tokens: { 0: "", 1: "ECM" } },
226
+ // WHY: same reasoning as ECM -- bit 8 of the raster compare value is only interesting when
227
+ // set (a compare line past 255), so the clear state is silent by design, not a dropped token.
228
+ { bit: "7", name: "RST8", kind: "flag", tokens: { 0: "", 1: "RST8" } },
229
+ ],
230
+ },
231
+ {
232
+ // WHY: OCR damage -- memmap's own label reads "Read NMls" (a lowercase L in place of an
233
+ // uppercase I). This is the register's LABEL, not a field name, so it is fixed here rather
234
+ // than via a per-bit override.
235
+ address: 56589, // $DD0D -- CIA2 Interrupt Control Register
236
+ label: "CIA Interrupt Control Register (Read NMIs/Write Mask)",
237
+ },
238
+ {
239
+ // WHY: memmap.json's `io` parser produced no `bits` entry at all for this address (D-22's
240
+ // named gap) even though a real game writes to it constantly -- sprite enable, one flag bit
241
+ // per sprite, is trivially regular and does not need memmap's own prose to describe correctly.
242
+ address: 53269, // $D015 -- Sprite Enable
243
+ label: "Sprite Enable",
244
+ synthetic: spriteBitFields("EN"),
245
+ },
246
+ {
247
+ // WHY: same D-22 gap as $D015 -- Sprite Y-Expand, one flag bit per sprite.
248
+ address: 53271, // $D017 -- Sprite Y-Expand
249
+ label: "Sprite Y-Expand",
250
+ synthetic: spriteBitFields("YEXP"),
251
+ },
252
+ {
253
+ // WHY: memmap.json's `io` parser produced no `bits` entry for the VIC IRQ mask register at
254
+ // all -- distinct from the VIC Interrupt FLAG register at $D019, which memmap.json does cover.
255
+ // Named per-bit since each bit enables a DIFFERENT interrupt source, not a repeated pattern.
256
+ address: 53274, // $D01A -- VIC Interrupt Enable (mask) Register
257
+ label: "VIC Interrupt Enable Register",
258
+ synthetic: [
259
+ { mask: 0x01, shift: 0, name: "RSTIRQEN", kind: "numeric" },
260
+ { mask: 0x02, shift: 1, name: "SPRBGIRQEN", kind: "numeric" },
261
+ { mask: 0x04, shift: 2, name: "SPRSPRIRQEN", kind: "numeric" },
262
+ { mask: 0x08, shift: 3, name: "LPIRQEN", kind: "numeric" },
263
+ { mask: 0xf0, shift: 4, name: "IRQENUNUSED", kind: "numeric" },
264
+ ],
265
+ },
266
+ {
267
+ // WHY: same D-22 gap as $D015 -- Sprite Priority (behind/in-front of background), one flag bit
268
+ // per sprite.
269
+ address: 53275, // $D01B -- Sprite Data Priority
270
+ label: "Sprite Data Priority",
271
+ synthetic: spriteBitFields("BG"),
272
+ },
273
+ {
274
+ // WHY: same D-22 gap as $D015 -- Sprite Multicolor, one flag bit per sprite.
275
+ address: 53276, // $D01C -- Sprite Multicolor
276
+ label: "Sprite Multicolor",
277
+ synthetic: spriteBitFields("MC"),
278
+ },
279
+ {
280
+ // WHY: same D-22 gap as $D015 -- Sprite X-Expand, one flag bit per sprite.
281
+ address: 53277, // $D01D -- Sprite X-Expand
282
+ label: "Sprite X-Expand",
283
+ synthetic: spriteBitFields("XEXP"),
284
+ },
285
+ ];
286
+
287
+ function findOverride(address: number): RegbitsRegisterOverride | undefined {
288
+ return OVERRIDES.find((o) => o.address === address);
289
+ }
290
+
291
+ /**
292
+ * Builds the field list for one memmap entry, consulting `override` for
293
+ * per-bit name/kind/tokens, falling back to mechanical derivation, and
294
+ * THROWING when neither succeeds (see this module's header). Also
295
+ * deduplicates overlapping bit ranges within a single entry -- two memmap
296
+ * "io" registers (`$DC00`/`$DC01`, the joystick/keyboard data ports) list
297
+ * multiple ALTERNATE readings of the very same bits (e.g. "7-0: keyboard
298
+ * column" and, separately, "4: joystick fire" -- the same physical bits
299
+ * read two different ways depending on what is plugged in), which would
300
+ * otherwise double-count those bits into two overlapping fields. First
301
+ * claim wins (array order); a later entry whose mask intersects an
302
+ * already-claimed one is skipped, never merged or thrown on.
303
+ */
304
+ function buildFieldsForEntry(entry: MemmapEntry, override: RegbitsRegisterOverride | undefined): RegBitsField[] {
305
+ const claimed: RegBitsField[] = [];
306
+ for (const b of entry.bits ?? []) {
307
+ const { mask, shift } = parseBitRange(b.bit);
308
+ if (claimed.some((f) => (f.mask & mask) !== 0)) continue;
309
+
310
+ const fo = override?.fields?.find((f) => f.bit === b.bit);
311
+ if (fo) {
312
+ const field: RegBitsField = { mask, shift, name: fo.name, kind: fo.kind };
313
+ if (fo.tokens) field.tokens = fo.tokens;
314
+ claimed.push(field);
315
+ continue;
316
+ }
317
+
318
+ const name = deriveIdentifier(b.desc);
319
+ if (name === null) {
320
+ throw new Error(
321
+ `buildRegBits: address $${entry.start.toString(16).toUpperCase().padStart(4, "0")} bit "${b.bit}" ` +
322
+ `desc "${b.desc}" did not mechanically derive a legal ACME identifier, and no OVERRIDES entry ` +
323
+ `covers it -- add a fieldOverride for { address: ${entry.start}, bit: "${b.bit}" }.`,
324
+ );
325
+ }
326
+ claimed.push({ mask, shift, name, kind: "numeric" });
327
+ }
328
+ claimed.sort((a, b) => a.shift - b.shift);
329
+ return claimed;
330
+ }
331
+
332
+ function formatAddressKey(address: number): string {
333
+ return `$${address.toString(16).toUpperCase().padStart(4, "0")}`;
334
+ }
335
+
336
+ /**
337
+ * Reads memmap.json, takes every entry with a `bits` array (29 as of this
338
+ * writing), normalises each field, and returns the curated table keyed by
339
+ * `$XXXX` address string, sorted so the result -- and the JSON this module
340
+ * emits from it -- is diff-stable. Synthetic (memmap-absent) registers named
341
+ * in `OVERRIDES` are added afterward, only when memmap produced no entry for
342
+ * that address.
343
+ */
344
+ export function buildRegBits(): RegBitsTable {
345
+ const memmap = JSON.parse(readFileSync(MEMMAP_PATH, "utf8")) as MemmapFile;
346
+ const table: RegBitsTable = {};
347
+
348
+ for (const entry of memmap.entries) {
349
+ if (!Array.isArray(entry.bits) || entry.bits.length === 0) continue;
350
+ const address = entry.start;
351
+ const override = findOverride(address);
352
+ const key = formatAddressKey(address);
353
+ const label = override?.label ?? entry.label ?? entry.desc ?? key;
354
+ const fields = buildFieldsForEntry(entry, override);
355
+ table[key] = { label, fields };
356
+ }
357
+
358
+ for (const override of OVERRIDES) {
359
+ if (!override.synthetic) continue;
360
+ const key = formatAddressKey(override.address);
361
+ if (table[key]) continue; // memmap already produced this address -- synthetic is a fallback only
362
+ table[key] = {
363
+ label: override.label ?? key,
364
+ fields: [...override.synthetic].sort((a, b) => a.shift - b.shift),
365
+ };
366
+ }
367
+
368
+ const sorted: RegBitsTable = {};
369
+ for (const key of Object.keys(table).sort()) {
370
+ sorted[key] = table[key]!;
371
+ }
372
+ return sorted;
373
+ }
374
+
375
+ /** SHA-256 hex digest of memmap.json's current bytes -- the banner's own
376
+ * drift-pin (T-11-GEN-DRIFT). */
377
+ export function memmapSha256(): string {
378
+ return createHash("sha256").update(readFileSync(MEMMAP_PATH)).digest("hex");
379
+ }
380
+
381
+ export interface RegBitsBanner {
382
+ generator: string;
383
+ memmapSha256: string;
384
+ warning: string;
385
+ }
386
+
387
+ export interface RegBitsDocument {
388
+ _generated: RegBitsBanner;
389
+ [key: string]: unknown;
390
+ }
391
+
392
+ /**
393
+ * Wraps `buildRegBits()`'s table with the first-key banner (generator
394
+ * filename, memmap.json's own digest, a do-not-hand-edit warning) --
395
+ * deliberately no timestamp, so two runs against the same memmap.json bytes
396
+ * produce byte-identical output and the drift guard's comparison stays
397
+ * total.
398
+ */
399
+ export function buildRegBitsDocument(): RegBitsDocument {
400
+ const table = buildRegBits();
401
+ const doc: RegBitsDocument = {
402
+ _generated: {
403
+ generator: "anno-regbits-gen.ts",
404
+ memmapSha256: memmapSha256(),
405
+ warning: "GENERATED FILE -- do not hand-edit. Regenerate via `node anno-regbits-gen.ts` from src/mcp/vice.",
406
+ },
407
+ };
408
+ for (const key of Object.keys(table).sort()) {
409
+ doc[key] = table[key];
410
+ }
411
+ return doc;
412
+ }
413
+
414
+ // Run-as-script: regenerate the committed artifact. Guarded so importing this
415
+ // module (e.g. from anno-regbits.test.ts) never has a write side effect.
416
+ const isMain = process.argv[1] !== undefined && pathToFileURL(process.argv[1]).href === import.meta.url;
417
+ if (isMain) {
418
+ const doc = buildRegBitsDocument();
419
+ writeFileSync(OUTPUT_PATH, `${JSON.stringify(doc, null, 2)}\n`);
420
+ console.log(`anno-regbits-gen: wrote ${OUTPUT_PATH} (${Object.keys(doc).length - 1} registers)`);
421
+ }