@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
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
- // r2000-memmap-render.ts -- the ONE authoritative place in this repo that
3
- // renders the human-readable Markdown memory map from the r2000 annotation
4
- // store (D-24) plus a validated run-scoped provenance sidecar (D-27's
5
- // reconciliation, recorded in 11-10-PLAN.md's objective).
2
+ // anno-memmap-render.ts -- the ONE authoritative place in this repo that
3
+ // renders the human-readable Markdown memory map from this project's own
4
+ // annotation store (D-24) plus a validated run-scoped provenance sidecar
5
+ // (D-27's reconciliation, recorded in 11-10-PLAN.md's objective).
6
6
  //
7
7
  // WHY THIS MODULE EXISTS (D-24): the store is canonical; the Markdown memory
8
8
  // map becomes a rendered VIEW. Criterion 1 says findings must be queryable
9
9
  // "instead of re-deriving from Markdown prose" -- that is only true by
10
10
  // construction if the prose is GENERATED from the queryable thing. Nothing
11
11
  // downstream of this module may hand-author an address row: every row in
12
- // the Range/Contents/Confidence/Evidence table comes from
13
- // `r2000_get_blocks`/`r2000_get_symbols`/`r2000_get_comments`, never from a
12
+ // the Range/Contents/Confidence/Evidence table comes from the store's own
13
+ // `listRanges()`/`listLabels()`/`listComments()` readers, never from a
14
14
  // human editing the output file directly.
15
15
  //
16
16
  // THE D-24/D-27 RECONCILIATION THIS FILE IMPLEMENTS: run-scoped facts (the
@@ -66,49 +66,65 @@
66
66
  import { existsSync, readFileSync } from "node:fs";
67
67
  import { createHash } from "node:crypto";
68
68
 
69
- import { runR2000Tool } from "./r2000-tools.ts";
70
- import { CONFIDENCE_GRADES, parseConfidencePrefix } from "./r2000-confidence.ts";
71
- import type { ConfidenceGrade } from "./r2000-confidence.ts";
72
-
73
- function errMsg(err: unknown): string {
74
- return err instanceof Error ? err.message : String(err);
75
- }
69
+ import { CONFIDENCE_GRADES, parseConfidencePrefix } from "./anno-confidence.ts";
70
+ import type { ConfidenceGrade } from "./anno-confidence.ts";
71
+ import { openStore, closeStore, listRanges, listLabels, listComments } from "./anno-store.ts";
72
+ import { COMMENT_TYPES, workspaceRelativePath } from "./anno-types.ts";
73
+ import type { CommentRow, LabelRow, RangeRow } from "./anno-types.ts";
74
+ import { blockClassAt } from "./block-class.ts";
76
75
 
77
76
  // ---------------------------------------------------------------------------
78
- // The r2000 query result shapes this renderer consumes, measured live
79
- // against a real regenerator2000-core-0.9.20 `--mcp-server-stdio` child
80
- // (never transcribed from a document): `r2000_get_blocks` returns
81
- // `{start_address, end_address, type}`; `r2000_get_symbols` returns
82
- // `{address, name, kind, type}`; `r2000_get_comments` returns
83
- // `{address, comment, type}`.
77
+ // WHAT THE VERSION-2 DIGEST HASHED -- the provenance of a lineage this
78
+ // renderer no longer reads. Version 2 hashed three wire result shapes
79
+ // measured LIVE against a real external-analyser-core-0.9.20
80
+ // `--mcp-server-stdio` child, never transcribed from a document:
81
+ // `anno_get_blocks` returned `{start_address, end_address, type}`,
82
+ // `anno_get_symbols` returned `{address, name, kind, type}`, and
83
+ // `anno_get_comments` returned `{address, comment, type}`.
84
+ //
85
+ // THIS PARAGRAPH IS THE RECORD, not a pointer at one. The three `interface`
86
+ // declarations it used to sit above went with the queries, so the spellings
87
+ // are carried here inline rather than left as a comment above a hole. It
88
+ // survives because `RENDERER_VERSION`'s "2" -> "3" bump is a statement about
89
+ // TWO KNOWN input shapes -- version 3 canonicalises this store's own
90
+ // `RangeRow`/`LabelRow`/`CommentRow` -- and that statement is only true
91
+ // while the older one is on the record. Delete this and the bump names one
92
+ // known input shape and one assumed one.
84
93
  // ---------------------------------------------------------------------------
85
94
 
86
- interface R2000Block {
87
- start_address: number;
88
- end_address: number;
89
- type: string;
90
- }
91
-
92
- interface R2000Symbol {
93
- address: number;
94
- name: string;
95
- kind: string;
96
- type: string;
95
+ function errMsg(err: unknown): string {
96
+ return err instanceof Error ? err.message : String(err);
97
97
  }
98
98
 
99
- interface R2000Comment {
100
- address: number;
101
- comment: string;
102
- type: "line" | "side";
99
+ /**
100
+ * The ONE piece of a `JSON.parse` failure that is safe to report: the byte
101
+ * offset at which parsing stopped, as ` (at byte offset N)`, or `""` when the
102
+ * runtime did not name one.
103
+ *
104
+ * WHY THIS IS A DIGIT EXTRACTOR AND NOT A MESSAGE PASS-THROUGH (CR-03). V8's
105
+ * JSON `SyntaxError` embeds a SNIPPET OF THE INPUT in its own message --
106
+ * `Unexpected token 'Q', "QQZZORACLE"... is not valid JSON` -- so any code
107
+ * that forwards `err.message` from a JSON parse over caller-supplied bytes is
108
+ * a content-disclosure oracle. The capture group here is `(\d+)` and nothing
109
+ * else, so no byte of the parsed file can reach the returned string however
110
+ * the runtime words its message. Widening this regex to capture anything but
111
+ * digits reopens CR-03.
112
+ *
113
+ * Returns `""` rather than guessing when no position is present (`Unexpected
114
+ * end of JSON input` carries none) -- an absent offset is reported by absence,
115
+ * never by a fabricated zero.
116
+ */
117
+ function jsonParsePosition(err: unknown): string {
118
+ const match = /\bat position (\d+)\b/.exec(errMsg(err));
119
+ return match ? ` (at byte offset ${match[1]})` : "";
103
120
  }
104
121
 
105
- async function queryR2000Json<T>(name: string, args: Record<string, unknown>): Promise<T> {
106
- const result = await runR2000Tool(name, args);
107
- if (result.isError) {
108
- throw new Error(`${name} failed: ${result.content[0]?.text ?? "(no message)"}`);
109
- }
110
- return JSON.parse(result.content[0]!.text) as T;
111
- }
122
+ /** The store's own spelling for a comment placed on its own line before the
123
+ * instruction, read out of `COMMENT_TYPES` -- the ONE home of that
124
+ * vocabulary -- rather than re-typed as a literal here. The pre-store
125
+ * renderer passed `type: "line"` to its comment query; this is that filter,
126
+ * moved to the read boundary. */
127
+ const [LINE_COMMENT] = COMMENT_TYPES;
112
128
 
113
129
  // ---------------------------------------------------------------------------
114
130
  // The provenance sidecar schema.
@@ -159,14 +175,14 @@ const REQUIRED_STRING_KEYS: readonly (keyof ProvenanceHeader)[] = [
159
175
  * copied into a sidecar by accident. */
160
176
  const PLACEHOLDER_PATTERN = /^<.*>$/;
161
177
 
162
- export class R2000ProvenanceHeaderError extends Error {
178
+ export class AnnoProvenanceHeaderError extends Error {
163
179
  /** Every problem found, one entry per offending key -- a caller filling a
164
180
  * sidecar wants the whole list, not one problem at a time. */
165
181
  problems: readonly string[];
166
182
 
167
183
  constructor(message: string, problems: readonly string[]) {
168
184
  super(message);
169
- this.name = "R2000ProvenanceHeaderError";
185
+ this.name = "AnnoProvenanceHeaderError";
170
186
  this.problems = problems;
171
187
  }
172
188
  }
@@ -175,14 +191,14 @@ export class R2000ProvenanceHeaderError extends Error {
175
191
  * Parses and validates a provenance sidecar. Collects EVERY problem (a
176
192
  * missing key, a non-string value, a template placeholder, a malformed
177
193
  * `captureSha256`, an invalid `videoStandard`, a malformed
178
- * `rasterPositions`) into one list and throws `R2000ProvenanceHeaderError`
194
+ * `rasterPositions`) into one list and throws `AnnoProvenanceHeaderError`
179
195
  * naming all of them at once -- never one at a time.
180
196
  */
181
197
  export function parseProvenanceHeader(json: unknown): ProvenanceHeader {
182
198
  const problems: string[] = [];
183
199
 
184
200
  if (typeof json !== "object" || json === null || Array.isArray(json)) {
185
- throw new R2000ProvenanceHeaderError(
201
+ throw new AnnoProvenanceHeaderError(
186
202
  `provenance sidecar must be a JSON object, got ${Array.isArray(json) ? "an array" : typeof json}`,
187
203
  ["<root>: must be a JSON object"],
188
204
  );
@@ -225,7 +241,7 @@ export function parseProvenanceHeader(json: unknown): ProvenanceHeader {
225
241
  }
226
242
 
227
243
  if (problems.length > 0) {
228
- throw new R2000ProvenanceHeaderError(
244
+ throw new AnnoProvenanceHeaderError(
229
245
  `provenance sidecar has ${problems.length} problem(s):\n` + problems.map((p) => ` - ${p}`).join("\n"),
230
246
  problems,
231
247
  );
@@ -250,8 +266,8 @@ export function parseProvenanceHeader(json: unknown): ProvenanceHeader {
250
266
  // ---------------------------------------------------------------------------
251
267
  // The render digest -- documented exactly, because a digest whose inputs are
252
268
  // unclear is a digest nobody trusts. It covers, in order: a canonical JSON
253
- // serialisation of the SORTED `r2000_get_blocks`/`r2000_get_symbols`/
254
- // `r2000_get_comments` results (so a store-side change, e.g. a comment's
269
+ // serialisation of the SORTED `listRanges()`/`listLabels()`/
270
+ // `listComments()` store rows (so a store-side change, e.g. a comment's
255
271
  // confidence grade, changes the digest even with the rendered file
256
272
  // untouched), the raw provenance sidecar BYTES (not the parsed object, so
257
273
  // even whitespace-only sidecar edits are covered), and this renderer's own
@@ -259,20 +275,28 @@ export function parseProvenanceHeader(json: unknown): ProvenanceHeader {
259
275
  // hand edit).
260
276
  // ---------------------------------------------------------------------------
261
277
 
262
- /** Bumped whenever this renderer's OUTPUT SHAPE changes, so a re-render
263
- * under a new renderer version is distinguishable from drift under the same
264
- * one. Version 2 (this plan, 260821-a86) escapes Markdown table cells via
265
- * `escapeMarkdownCell()` -- WR-04. */
266
- export const RENDERER_VERSION = "2";
278
+ /** Bumped whenever this renderer's OUTPUT SHAPE **or its digest's canonical
279
+ * INPUT** changes, so a re-render under a new renderer version is
280
+ * distinguishable from drift under the same one.
281
+ *
282
+ * Version 2 (260821-a86) escaped Markdown table cells via
283
+ * `escapeMarkdownCell()` -- WR-04, an output-shape change.
284
+ *
285
+ * Version 3 (D-17) is an INPUT change: `computeRenderDigest()` canonicalises
286
+ * this store's own `RangeRow`/`LabelRow`/`CommentRow` instead of the three
287
+ * wire shapes recorded above, so the same underlying annotations hash
288
+ * differently either side of it. Leaving the version at "2" across that
289
+ * boundary would let two incompatible renderings compare as ordinary drift. */
290
+ export const RENDERER_VERSION = "3";
267
291
 
268
292
  /**
269
293
  * Escapes `text` for safe interpolation into a Markdown table cell or list
270
294
  * item: every `|` becomes `\|`, and every `\r\n`/`\n`/bare `\r` collapses to
271
295
  * `<br>` (a single-line-safe line break inside a table cell). This control
272
296
  * ESCAPES and never REJECTS -- unlike the label-name policy
273
- * (`r2000-acme-ident.ts`'s `assertLegalAcmeIdentifier()`, T-11-NAME-INJECT's
297
+ * (`anno-acme-ident.ts`'s `assertLegalAcmeIdentifier()`, T-11-NAME-INJECT's
274
298
  * other leg), because comment `evidence` legitimately contains `|` and
275
- * embedded newlines (`r2000_set_comment`'s own schema documents multi-line
299
+ * embedded newlines (`anno_set_comment`'s own schema documents multi-line
276
300
  * support) -- refusing here would refuse valid data, not an attack. Closes
277
301
  * WR-04 / T-11-NAME-INJECT's render leg: an unescaped `|` or newline in
278
302
  * store text used to be able to inject an extra table cell or split a row
@@ -283,9 +307,9 @@ export function escapeMarkdownCell(text: string): string {
283
307
  }
284
308
 
285
309
  function computeRenderDigest(
286
- blocks: readonly R2000Block[],
287
- symbols: readonly R2000Symbol[],
288
- comments: readonly R2000Comment[],
310
+ blocks: readonly RangeRow[],
311
+ symbols: readonly LabelRow[],
312
+ comments: readonly CommentRow[],
289
313
  sidecarBytes: string,
290
314
  ): string {
291
315
  const canonical = JSON.stringify({ blocks, symbols, comments }) + "" + sidecarBytes + "" + RENDERER_VERSION;
@@ -307,8 +331,31 @@ interface GradedComment {
307
331
  // ---------------------------------------------------------------------------
308
332
 
309
333
  export interface RenderMemoryMapOptions {
310
- projectPath: string;
334
+ /** The annotation store to render. The CALLER confines it through
335
+ * `storePathWithinWorkspace()` and `openStore()` below confines it again
336
+ * against the same `workspaceRoot`, so both answers agree by construction
337
+ * rather than by a second rule (T-29-51). */
338
+ storePath: string;
339
+ /** The provenance sidecar to render from. The CALLER confines it through
340
+ * `storePathWithinWorkspace()` before entering this module -- today that
341
+ * caller is `anno-cli.ts`'s `cmdRenderMemmap()`. THIS MODULE PERFORMS NO
342
+ * CONFINEMENT OF ITS OWN, and must never be handed a path that has not
343
+ * been through that seam.
344
+ *
345
+ * THIS FIELD WAS DOCUMENTED BY SILENCE, and the silence is what the review
346
+ * names as the mechanism. `storePath` one line above carried four lines
347
+ * stating who confines it; this field, an equally caller-supplied path
348
+ * reaching an equally real `readFileSync`, carried nothing -- so a reader
349
+ * comparing the two would reasonably conclude the difference was
350
+ * deliberate. It was not: the CLI read this argument raw, making it an
351
+ * arbitrary-file read oracle (`29-REVIEW.md` CR-03). An absent comment
352
+ * beside a present one is a claim, and this one was false. */
311
353
  provenancePath: string;
354
+ /** The workspace root both confinement checks are taken against. REQUIRED
355
+ * rather than defaulted: `openStore()`'s default behaviour is to CREATE
356
+ * the file, so an unconfined store path is a store file created wherever
357
+ * the caller's argument pointed. */
358
+ workspaceRoot: string;
312
359
  }
313
360
 
314
361
  export interface RenderMemoryMapResult {
@@ -321,11 +368,14 @@ export interface RenderMemoryMapResult {
321
368
  }
322
369
 
323
370
  /**
324
- * Renders the memory map from the r2000 store plus a validated provenance
325
- * sidecar. Queries `r2000_get_blocks`/`r2000_get_symbols`/`r2000_get_comments`
326
- * through `r2000-tools.ts`'s curated, allow-listed `runR2000Tool()` -- never
327
- * `r2000-mcp-client.ts` directly (mirrors every other consumer's discipline
328
- * in this repo).
371
+ * Renders the memory map from an annotation store plus a validated
372
+ * provenance sidecar. Reads the store DIRECTLY -- `listRanges()`,
373
+ * `listLabels()` and `listComments()` on ONE handle, opened once per render
374
+ * and closed in a `finally` -- with no child process anywhere on this path.
375
+ *
376
+ * The store's block-kind spelling is interpreted in exactly one place,
377
+ * `block-class.ts`'s `blockClassAt()`; nothing below compares a
378
+ * `dataType` string itself.
329
379
  *
330
380
  * A malformed confidence prefix inside a store comment (a typo that survived
331
381
  * whatever wrote it) THROWS through `parseConfidencePrefix()` -- this
@@ -333,7 +383,7 @@ export interface RenderMemoryMapResult {
333
383
  * must be fixed in the store, not hidden in the rendered view.
334
384
  */
335
385
  export async function renderMemoryMap(opts: RenderMemoryMapOptions): Promise<RenderMemoryMapResult> {
336
- const { projectPath, provenancePath } = opts;
386
+ const { storePath, provenancePath, workspaceRoot } = opts;
337
387
 
338
388
  let sidecarBytes: string;
339
389
  try {
@@ -346,23 +396,57 @@ export async function renderMemoryMap(opts: RenderMemoryMapOptions): Promise<Ren
346
396
  try {
347
397
  sidecarJson = JSON.parse(sidecarBytes);
348
398
  } catch (err) {
349
- throw new Error(`renderMemoryMap: provenance sidecar at "${provenancePath}" is not valid JSON: ${errMsg(err)}`);
399
+ // NEVER INTERPOLATE THE UNDERLYING PARSE ERROR HERE (CR-03). Node's
400
+ // SyntaxError quotes a snippet of the input it choked on -- e.g.
401
+ // `Unexpected token 'Q', "QQZZORACLE"... is not valid JSON` -- so passing
402
+ // it through turns a read refusal into a CONTENT-DISCLOSURE ORACLE. That
403
+ // matters here specifically because this argument arrives from an
404
+ // agent-composed Bash invocation: the shipped playbooks tell an LLM to
405
+ // compose this path, so the error text is read by whatever composed it.
406
+ //
407
+ // What survives is everything a caller legitimately needs to fix the
408
+ // problem: WHICH file, and THAT it is not JSON. The byte OFFSET is
409
+ // included where Node exposes one, because a position is a fact about
410
+ // where parsing stopped and not about what the file contains.
411
+ throw new Error(
412
+ `renderMemoryMap: provenance sidecar at "${provenancePath}" is not valid JSON${jsonParsePosition(err)}. ` +
413
+ "The underlying parser message is deliberately NOT included -- it quotes the file's own bytes (CR-03).",
414
+ );
350
415
  }
351
416
  const provenance = parseProvenanceHeader(sidecarJson);
352
417
 
353
- const blocks = await queryR2000Json<R2000Block[]>("r2000_get_blocks", { project: projectPath });
354
- const symbols = await queryR2000Json<R2000Symbol[]>("r2000_get_symbols", { project: projectPath });
355
- const comments = await queryR2000Json<R2000Comment[]>("r2000_get_comments", {
356
- project: projectPath,
357
- type: "line",
358
- });
418
+ // ONE handle for the whole render, closed in a `finally`. `mustExist` is
419
+ // what makes "the annotations are gone" and "there are no annotations"
420
+ // refuse differently (T-29-52): without it a mistyped path would CREATE an
421
+ // empty store and render as an empty memory map indistinguishable from a
422
+ // real one.
423
+ const handle = openStore(storePath, { workspaceRoot, mustExist: true });
424
+ let ranges: RangeRow[];
425
+ let labels: LabelRow[];
426
+ let lineComments: CommentRow[];
427
+ try {
428
+ ranges = listRanges(handle);
429
+ labels = listLabels(handle);
430
+ lineComments = listComments(handle).filter((c) => c.commentType === LINE_COMMENT);
431
+ } finally {
432
+ closeStore(handle);
433
+ }
359
434
 
360
- const sortedBlocks = [...blocks].sort((a, b) => a.start_address - b.start_address);
361
- const sortedSymbols = [...symbols].sort((a, b) => a.address - b.address);
362
- const sortedComments = [...comments].sort((a, b) => a.address - b.address);
435
+ const sortedBlocks = [...ranges].sort((a, b) => a.start - b.start);
436
+ const sortedSymbols = [...labels].sort((a, b) => a.address - b.address);
437
+ const sortedComments = [...lineComments].sort((a, b) => a.address - b.address);
438
+
439
+ // The block listing in `block-class.ts`'s own entry shape. The `dataType`
440
+ // column is copied VERBATIM and never compared here -- that module is the
441
+ // one place in this tree allowed to interpret it.
442
+ const blockEntries = sortedBlocks.map((row) => ({
443
+ start_address: row.start,
444
+ end_address: row.endInclusive,
445
+ type: row.dataType as string,
446
+ }));
363
447
 
364
448
  const gradedComments: GradedComment[] = sortedComments.map((c) => {
365
- const parsed = parseConfidencePrefix(c.comment);
449
+ const parsed = parseConfidencePrefix(c.text);
366
450
  return { address: c.address, grade: parsed.grade, evidence: parsed.rest };
367
451
  });
368
452
 
@@ -372,20 +456,46 @@ export async function renderMemoryMap(opts: RenderMemoryMapOptions): Promise<Ren
372
456
 
373
457
  const renderDigest = computeRenderDigest(sortedBlocks, sortedSymbols, sortedComments, sidecarBytes);
374
458
 
459
+ // The two recorded locations are WORKSPACE-RELATIVE, and that is the
460
+ // load-bearing detail rather than a formatting preference: every byte below
461
+ // is re-rendered and compared BYTE FOR BYTE by `checkRenderedMemoryMap()`,
462
+ // so an absolute path here would make the drift verdict a function of where
463
+ // the checkout sits (CR-01). `workspaceRelativePath()` is the one definition
464
+ // of that spelling; it computes a location and refuses one that escapes the
465
+ // root. It is NOT a confinement check -- this module still performs no
466
+ // confinement of its own, exactly as `RenderMemoryMapOptions` documents.
467
+ const storeLocation = workspaceRelativePath(storePath, workspaceRoot);
468
+ const sidecarLocation = workspaceRelativePath(provenancePath, workspaceRoot);
469
+
375
470
  const lines: string[] = [];
376
471
 
377
472
  lines.push("<!--");
378
- lines.push(" GENERATED by `vice-mcp r2000 render-memmap` -- do not hand-edit; re-run the generator.");
379
- lines.push(` store: ${projectPath}`);
380
- lines.push(` sidecar: ${provenancePath}`);
473
+ lines.push(" GENERATED by `vice-mcp anno render-memmap` -- do not hand-edit; re-run the generator.");
474
+ lines.push(` store: ${storeLocation}`);
475
+ lines.push(` sidecar: ${sidecarLocation}`);
381
476
  lines.push(` render_digest: ${renderDigest}`);
382
477
  lines.push(
383
- " The digest covers the sorted r2000_get_blocks/r2000_get_symbols/r2000_get_comments results, the",
478
+ " The digest covers the sorted listRanges/listLabels/listComments results, the raw provenance",
384
479
  );
385
480
  lines.push(
386
- " raw provenance sidecar bytes, and this renderer's version constant -- so either a hand edit or a",
481
+ " sidecar bytes, and this renderer's version constant. `render-memmap --check` reports drift when, and",
387
482
  );
388
- lines.push(" store-side change (e.g. a comment's confidence grade) is detected by `render-memmap --check`.");
483
+ lines.push(
484
+ " only when, one of these changed: this file was hand-edited; a store row changed (a range, a label, a",
485
+ );
486
+ lines.push(
487
+ " comment, or a comment's confidence grade); the provenance sidecar's bytes changed; the store or the",
488
+ );
489
+ lines.push(
490
+ " sidecar moved to a different location RELATIVE TO THE WORKSPACE ROOT; or the renderer changed.",
491
+ );
492
+ lines.push(
493
+ " Relocating the checkout is NOT drift -- the same tree at a different absolute path renders these same",
494
+ );
495
+ lines.push(
496
+ " bytes, because the two locations above are workspace-relative. That matters because this file is meant",
497
+ );
498
+ lines.push(" to be committed and read on a machine that did not produce it.");
389
499
  lines.push("-->");
390
500
  lines.push("");
391
501
  lines.push(`# Memory map — ${provenance.capturePath}`);
@@ -403,12 +513,12 @@ export async function renderMemoryMap(opts: RenderMemoryMapOptions): Promise<Ren
403
513
  lines.push("");
404
514
  lines.push("| Range | Contents | Confidence | Evidence |");
405
515
  lines.push("|---|---|---|---|");
406
- for (const block of sortedBlocks) {
407
- const match = findGradeInRange(block.start_address, block.end_address);
408
- const range = `\`${hex4(block.start_address)}-${hex4(block.end_address)}\``;
516
+ for (const row of sortedBlocks) {
517
+ const match = findGradeInRange(row.start, row.endInclusive);
518
+ const range = `\`${hex4(row.start)}-${hex4(row.endInclusive)}\``;
409
519
  const grade = match?.grade ? match.grade.phrase.toUpperCase() : "";
410
520
  const evidence = match ? escapeMarkdownCell(match.evidence) : "";
411
- lines.push(`| ${range} | ${block.type} | ${grade} | ${evidence} |`);
521
+ lines.push(`| ${range} | ${row.dataType} | ${grade} | ${evidence} |`);
412
522
  }
413
523
  lines.push("");
414
524
  lines.push("Confidence vocabulary — the project's HIGH / MEDIUM / LOW scale, applied to classification:");
@@ -443,10 +553,8 @@ export async function renderMemoryMap(opts: RenderMemoryMapOptions): Promise<Ren
443
553
  lines.push("");
444
554
  lines.push("| Address | Provisional name | Confirmed by | Confidence |");
445
555
  lines.push("|---|---|---|---|");
446
- const codeBlocks = sortedBlocks.filter((b) => b.type === "Code");
447
556
  for (const sym of sortedSymbols) {
448
- const inCode = codeBlocks.some((b) => sym.address >= b.start_address && sym.address <= b.end_address);
449
- if (!inCode) continue;
557
+ if (blockClassAt(blockEntries, sym.address) !== "code") continue;
450
558
  const match = gradedComments.find((c) => c.address === sym.address);
451
559
  const grade = match?.grade ? match.grade.phrase.toUpperCase() : "";
452
560
  const confirmedBy = match ? escapeMarkdownCell(match.evidence) : "";
@@ -474,9 +582,21 @@ export async function renderMemoryMap(opts: RenderMemoryMapOptions): Promise<Ren
474
582
  // ---------------------------------------------------------------------------
475
583
 
476
584
  export interface CheckRenderedMemoryMapOptions {
477
- projectPath: string;
585
+ /** See `RenderMemoryMapOptions.storePath`. */
586
+ storePath: string;
587
+ /** See `RenderMemoryMapOptions.provenancePath` -- same argument, one layer
588
+ * up. The CALLER (`anno-cli.ts`'s `cmdRenderMemmap()`) confines it through
589
+ * `storePathWithinWorkspace()`; this module performs no confinement of its
590
+ * own (CR-03). */
478
591
  provenancePath: string;
592
+ /** The rendered file to compare against, read RAW by `readFileSync` below.
593
+ * The CALLER confines it through `storePathWithinWorkspace()` -- the SAME
594
+ * resolution that produces the write path on the non-`--check` branch, so
595
+ * the drift check and the write are one confined value rather than two
596
+ * rules. This module performs no confinement of its own (CR-02). */
479
597
  renderedPath: string;
598
+ /** See `RenderMemoryMapOptions.workspaceRoot`. */
599
+ workspaceRoot: string;
480
600
  }
481
601
 
482
602
  export type CheckRenderedMemoryMapResult =
@@ -492,21 +612,42 @@ export type CheckRenderedMemoryMapResult =
492
612
  * - `{status:"in-sync"}` when the freshly rendered text is byte-identical
493
613
  * to the file on disk;
494
614
  * - `{status:"drifted", line, expected, actual}` naming the first
495
- * differing line otherwise -- reached by EITHER a hand edit to the file
496
- * OR a store-side change (a label, a comment, a block) since the file
497
- * was last rendered, because both change what a fresh render produces.
615
+ * differing line otherwise.
616
+ *
617
+ * WHAT REACHES `drifted`, enumerated from what the compared bytes are a
618
+ * function of rather than from a remembered summary -- the fresh render is a
619
+ * function of the store rows, the sidecar bytes, `RENDERER_VERSION` and the
620
+ * two WORKSPACE-RELATIVE locations, and nothing else:
621
+ * - a hand edit to the rendered file;
622
+ * - a store-side change (a range, a label, a comment, or a comment's
623
+ * confidence grade);
624
+ * - a change to the provenance sidecar's bytes;
625
+ * - a move of the store or the sidecar to a different location RELATIVE TO
626
+ * the workspace root;
627
+ * - a renderer change (output shape, or a `RENDERER_VERSION` bump).
628
+ *
629
+ * AND THE NEGATIVE, which is the defect this list was corrected for (CR-01,
630
+ * `29-VERIFICATION.md` gap 1): relocating the checkout -- the same tree at a
631
+ * different absolute path -- does NOT drift. The banner records
632
+ * workspace-relative locations, so no compared byte is a function of where the
633
+ * checkout sits. Before that fix this returned `drifted` for a byte-identical
634
+ * store, sidecar and rendered file while `renderMemoryMap()` printed the SAME
635
+ * `render_digest` in both trees, so the gate contradicted its own artifact.
636
+ * That matters here specifically because the rendered file is a committed
637
+ * artifact and this repository runs its phases in worktrees, which makes a
638
+ * differing checkout path the normal case rather than an edge.
498
639
  */
499
640
  export async function checkRenderedMemoryMap(
500
641
  opts: CheckRenderedMemoryMapOptions,
501
642
  ): Promise<CheckRenderedMemoryMapResult> {
502
- const { projectPath, provenancePath, renderedPath } = opts;
643
+ const { storePath, provenancePath, renderedPath, workspaceRoot } = opts;
503
644
 
504
645
  if (!existsSync(renderedPath)) {
505
646
  return { status: "missing", path: renderedPath };
506
647
  }
507
648
 
508
649
  const onDisk = readFileSync(renderedPath, "utf8");
509
- const { markdown } = await renderMemoryMap({ projectPath, provenancePath });
650
+ const { markdown } = await renderMemoryMap({ storePath, provenancePath, workspaceRoot });
510
651
 
511
652
  if (onDisk === markdown) {
512
653
  return { status: "in-sync" };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- // r2000-regbits-gen.ts -- the ONE authoritative place in this repo that turns
2
+ // anno-regbits-gen.ts -- the ONE authoritative place in this repo that turns
3
3
  // c64-memory-mapping's memmap.json into the curated address->bit-name table
4
- // r2000-enum-gen.ts decodes register values against (D-22, R2000-13).
4
+ // anno-enum-gen.ts decodes register values against (D-22, ANNO-13).
5
5
  //
6
6
  // WHY THIS EXISTS (D-22): neither register the phase's own pinned criterion-3
7
7
  // target needs ($D011) nor the registers a real game writes to constantly
@@ -18,8 +18,8 @@
18
18
  // WHAT THIS IS THE ONE AUTHORITATIVE PLACE FOR: reading memmap.json's 29
19
19
  // structured `bits` entries, normalising each `bits[].bit` range string into
20
20
  // `{mask, shift}`, deriving (or overriding) a legal ACME identifier for every
21
- // field, and emitting the committed, banner-marked `r2000-regbits.json`
22
- // artifact `r2000-enum-gen.ts` decodes against. No other module may read
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
23
  // memmap.json for this purpose or hand-maintain a second bit-name table.
24
24
  //
25
25
  // KEY-SHAPE DECISION: table keys are `$XXXX` (uppercase, 4-hex-digit,
@@ -31,11 +31,11 @@
31
31
  // numeric-sort step is needed to keep the emitted JSON diff-stable.
32
32
  //
33
33
  // WHAT NOT TO DO, named concretely:
34
- // - Never hand-edit r2000-regbits.json. It is a generated-but-committed
34
+ // - Never hand-edit anno-regbits.json. It is a generated-but-committed
35
35
  // artifact (ENGINEERING_RULES.md Sec 11), the same shape
36
36
  // `resources-sync.test.ts` already established for compiled `.mjs`
37
- // build output -- re-run `node r2000-regbits-gen.ts` and let the drift
38
- // guard in r2000-regbits.test.ts confirm the result matches.
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
39
  // - Never silently skip or placeholder an unmappable bit description.
40
40
  // `buildRegBits()` THROWS, naming the address, the bit range and the
41
41
  // offending description, when mechanical derivation fails AND no
@@ -53,12 +53,17 @@ import { fileURLToPath, pathToFileURL } from "node:url";
53
53
  const HERE = dirname(fileURLToPath(import.meta.url));
54
54
 
55
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
- const MEMMAP_PATH = join(HERE, "..", "..", "skills", "c64-memory-mapping", "memmap.json");
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");
58
63
 
59
64
  /** Where the generated, committed artifact lives -- always a sibling of this
60
65
  * generator, never a caller-supplied path. */
61
- const OUTPUT_PATH = join(HERE, "r2000-regbits.json");
66
+ const OUTPUT_PATH = join(HERE, "anno-regbits.json");
62
67
 
63
68
  export type FieldKind = "flag" | "numeric" | "enum";
64
69
 
@@ -186,7 +191,7 @@ function spriteBitFields(suffix: string): RegBitsField[] {
186
191
 
187
192
  // ---------------------------------------------------------------------------
188
193
  // OVERRIDES -- every entry carries its own WHY comment immediately above it
189
- // (r2000-regbits.test.ts's own non-vacuity check counts these two things
194
+ // (anno-regbits.test.ts's own non-vacuity check counts these two things
190
195
  // against each other, so removing a comment without removing its entry, or
191
196
  // vice versa, fails a test rather than silently drifting).
192
197
  // ---------------------------------------------------------------------------
@@ -395,9 +400,9 @@ export function buildRegBitsDocument(): RegBitsDocument {
395
400
  const table = buildRegBits();
396
401
  const doc: RegBitsDocument = {
397
402
  _generated: {
398
- generator: "r2000-regbits-gen.ts",
403
+ generator: "anno-regbits-gen.ts",
399
404
  memmapSha256: memmapSha256(),
400
- warning: "GENERATED FILE -- do not hand-edit. Regenerate via `node r2000-regbits-gen.ts` from .claude/mcp/vice.",
405
+ warning: "GENERATED FILE -- do not hand-edit. Regenerate via `node anno-regbits-gen.ts` from src/mcp/vice.",
401
406
  },
402
407
  };
403
408
  for (const key of Object.keys(table).sort()) {
@@ -407,10 +412,10 @@ export function buildRegBitsDocument(): RegBitsDocument {
407
412
  }
408
413
 
409
414
  // Run-as-script: regenerate the committed artifact. Guarded so importing this
410
- // module (e.g. from r2000-regbits.test.ts) never has a write side effect.
415
+ // module (e.g. from anno-regbits.test.ts) never has a write side effect.
411
416
  const isMain = process.argv[1] !== undefined && pathToFileURL(process.argv[1]).href === import.meta.url;
412
417
  if (isMain) {
413
418
  const doc = buildRegBitsDocument();
414
419
  writeFileSync(OUTPUT_PATH, `${JSON.stringify(doc, null, 2)}\n`);
415
- console.log(`r2000-regbits-gen: wrote ${OUTPUT_PATH} (${Object.keys(doc).length - 1} registers)`);
420
+ console.log(`anno-regbits-gen: wrote ${OUTPUT_PATH} (${Object.keys(doc).length - 1} registers)`);
416
421
  }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "_generated": {
3
- "generator": "r2000-regbits-gen.ts",
3
+ "generator": "anno-regbits-gen.ts",
4
4
  "memmapSha256": "60a517c1833a44e6dc1a99a949554fa39834b558371d9f1e98531499fe3642fe",
5
- "warning": "GENERATED FILE -- do not hand-edit. Regenerate via `node r2000-regbits-gen.ts` from .claude/mcp/vice."
5
+ "warning": "GENERATED FILE -- do not hand-edit. Regenerate via `node anno-regbits-gen.ts` from src/mcp/vice."
6
6
  },
7
7
  "$0001": {
8
8
  "label": "MOS 6510 Micro-Processor On-Chip I/O Port",