@henols/vice-mcp 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/THIRD-PARTY-NOTICES.md +1 -24
- package/{r2000-acme-ident.ts → anno-acme-ident.ts} +13 -13
- package/anno-cli.ts +1465 -0
- package/{r2000-confidence.ts → anno-confidence.ts} +22 -22
- package/anno-coverage.ts +2465 -0
- package/{r2000-d64.ts → anno-d64.ts} +5 -5
- package/anno-derive.ts +590 -0
- package/anno-details.ts +169 -0
- package/anno-enum-gen.ts +533 -0
- package/anno-export-asm.ts +1310 -0
- package/anno-index.ts +150 -0
- package/{r2000-memmap-render.ts → anno-memmap-render.ts} +236 -95
- package/{r2000-regbits-gen.ts → anno-regbits-gen.ts} +20 -15
- package/{r2000-regbits.json → anno-regbits.json} +2 -2
- package/anno-register.ts +240 -0
- package/anno-store.ts +3486 -0
- package/anno-symbols.ts +266 -0
- package/anno-tools.ts +2111 -0
- package/anno-types.ts +1636 -0
- package/block-class.ts +201 -0
- package/build.ts +1 -1
- package/capability-registry.ts +3 -1
- package/disasm-decoder.ts +14 -14
- package/disasm-opcodes.ts +4 -4
- package/disasm-renderer.ts +2 -2
- package/hostpath.ts +1 -1
- package/install-resources.ts +1 -1
- package/package.json +23 -17
- package/prg-image.ts +119 -0
- package/repo-root.ts +20 -5
- package/resources/broker-launch.mjs +8 -4
- package/resources/vice-launcher.sh +3 -3
- package/stock-address.ts +5 -5
- package/stock-cia.ts +2 -2
- package/stock-condition.ts +7 -7
- package/stock-connect.ts +1 -1
- package/stock-dispatch.ts +35 -5
- package/stock-execution.ts +5 -3
- package/stock-input.ts +9 -9
- package/stock-machine.ts +17 -6
- package/stock-protocol.ts +16 -11
- package/stock-registers.ts +54 -29
- package/stock-sprites.ts +3 -3
- package/stock-symbols.ts +9 -9
- package/stock-timing.ts +1 -1
- package/stock-vicii.ts +1 -1
- package/version.ts +1 -1
- package/vice-proxy.ts +68 -46
- package/r2000-cli.ts +0 -1103
- package/r2000-enum-gen.ts +0 -574
- package/r2000-launch.ts +0 -357
- package/r2000-mcp-client.ts +0 -596
- package/r2000-project.ts +0 -190
- package/r2000-symbols.ts +0 -388
- package/r2000-tools.ts +0 -914
- package/r2000-verify.ts +0 -184
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
// renders the human-readable Markdown memory map from
|
|
4
|
-
// store (D-24) plus a validated run-scoped provenance sidecar
|
|
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
|
-
// `
|
|
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 {
|
|
70
|
-
import {
|
|
71
|
-
import
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
// `
|
|
82
|
-
// `{
|
|
83
|
-
// `{address,
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
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 = "
|
|
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 `
|
|
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
|
|
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
|
|
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 `
|
|
254
|
-
// `
|
|
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
|
|
263
|
-
* under a new renderer version is
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
|
|
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
|
-
* (`
|
|
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 (`
|
|
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
|
|
287
|
-
symbols: readonly
|
|
288
|
-
comments: readonly
|
|
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
|
-
|
|
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
|
|
325
|
-
* sidecar.
|
|
326
|
-
*
|
|
327
|
-
* `
|
|
328
|
-
*
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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 = [...
|
|
361
|
-
const sortedSymbols = [...
|
|
362
|
-
const sortedComments = [...
|
|
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.
|
|
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
|
|
379
|
-
lines.push(` store: ${
|
|
380
|
-
lines.push(` sidecar: ${
|
|
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
|
|
478
|
+
" The digest covers the sorted listRanges/listLabels/listComments results, the raw provenance",
|
|
384
479
|
);
|
|
385
480
|
lines.push(
|
|
386
|
-
"
|
|
481
|
+
" sidecar bytes, and this renderer's version constant. `render-memmap --check` reports drift when, and",
|
|
387
482
|
);
|
|
388
|
-
lines.push(
|
|
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
|
|
407
|
-
const match = findGradeInRange(
|
|
408
|
-
const range = `\`${hex4(
|
|
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} | ${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
496
|
-
*
|
|
497
|
-
*
|
|
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 {
|
|
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({
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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 `
|
|
22
|
-
// artifact `
|
|
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
|
|
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
|
|
38
|
-
// guard in
|
|
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
|
-
|
|
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, "
|
|
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
|
-
// (
|
|
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: "
|
|
403
|
+
generator: "anno-regbits-gen.ts",
|
|
399
404
|
memmapSha256: memmapSha256(),
|
|
400
|
-
warning: "GENERATED FILE -- do not hand-edit. Regenerate via `node
|
|
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
|
|
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(`
|
|
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": "
|
|
3
|
+
"generator": "anno-regbits-gen.ts",
|
|
4
4
|
"memmapSha256": "60a517c1833a44e6dc1a99a949554fa39834b558371d9f1e98531499fe3642fe",
|
|
5
|
-
"warning": "GENERATED FILE -- do not hand-edit. Regenerate via `node
|
|
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",
|