@henols/vice-mcp 0.1.12 → 0.2.1
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/THIRD-PARTY-NOTICES.md +23 -0
- package/backend-detect.mts +58 -8
- package/capability-registry.ts +388 -0
- package/package.json +26 -1
- package/r2000-acme-ident.ts +97 -0
- package/r2000-cli.ts +1103 -0
- package/r2000-confidence.ts +233 -0
- package/r2000-d64.ts +310 -0
- package/r2000-enum-gen.ts +574 -0
- package/r2000-launch.ts +357 -0
- package/r2000-mcp-client.ts +596 -0
- package/r2000-memmap-render.ts +531 -0
- package/r2000-project.ts +190 -0
- package/r2000-regbits-gen.ts +416 -0
- package/r2000-regbits.json +1370 -0
- package/r2000-symbols.ts +388 -0
- package/r2000-tools.ts +914 -0
- package/r2000-verify.ts +184 -0
- package/resources/backend-detect.mjs +30 -2
- package/resources/broker-launch.mjs +166 -34
- package/resources/vice-broker.mjs +25 -4
- package/stock-cia.ts +598 -0
- package/stock-connect.ts +137 -20
- package/stock-derived.ts +67 -14
- package/stock-diagnose.ts +994 -0
- package/stock-dispatch.ts +105 -4
- package/stock-handler.ts +29 -0
- package/stock-memory-search.ts +441 -0
- package/stock-memory.ts +92 -13
- package/stock-protocol.ts +328 -0
- package/stock-recycle.ts +500 -0
- package/stock-run-until.ts +400 -0
- package/stock-runstate.ts +46 -2
- package/stock-sprites.ts +712 -0
- package/stock-symbols.ts +455 -0
- package/stock-timing.ts +562 -0
- package/stock-vicii.ts +318 -0
- package/tools-manifest.stock.json +3031 -223
- package/version.ts +279 -0
- package/vice-proxy.ts +195 -6
|
@@ -0,0 +1,531 @@
|
|
|
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).
|
|
6
|
+
//
|
|
7
|
+
// WHY THIS MODULE EXISTS (D-24): the store is canonical; the Markdown memory
|
|
8
|
+
// map becomes a rendered VIEW. Criterion 1 says findings must be queryable
|
|
9
|
+
// "instead of re-deriving from Markdown prose" -- that is only true by
|
|
10
|
+
// construction if the prose is GENERATED from the queryable thing. Nothing
|
|
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
|
|
14
|
+
// human editing the output file directly.
|
|
15
|
+
//
|
|
16
|
+
// THE D-24/D-27 RECONCILIATION THIS FILE IMPLEMENTS: run-scoped facts (the
|
|
17
|
+
// capture's SHA-256, `$01`, `$DD00`, the derived graphics chain, the video
|
|
18
|
+
// standard, the live vector pair, observed raster positions) are facts about
|
|
19
|
+
// a RUN, not about an address -- the store is address-keyed and has no shape
|
|
20
|
+
// for them. They arrive here as an INPUT to the renderer (a JSON sidecar,
|
|
21
|
+
// `parseProvenanceHeader()`'s own schema), never as a hand-edited region of
|
|
22
|
+
// this module's OUTPUT. A missing or malformed required sidecar key is a
|
|
23
|
+
// named error listing every problem at once; this module never substitutes a
|
|
24
|
+
// `<placeholder>` for one.
|
|
25
|
+
//
|
|
26
|
+
// WHY THE LAYOUT IS EMBEDDED IN TYPESCRIPT RATHER THAN READ FROM A TEMPLATE
|
|
27
|
+
// FILE AT RUNTIME (the second decision this plan records): Phase 10's D-06
|
|
28
|
+
// established that `.claude/mcp/vice/*.ts` exists as files on disk only
|
|
29
|
+
// under the Claude Code plugin route -- both npm-installer routes launch via
|
|
30
|
+
// `npx`. A renderer that resolved a template path into the skills tree at
|
|
31
|
+
// runtime would silently fail to resolve for an npm-installed user. The
|
|
32
|
+
// recon skill's own template becomes prose pointing at this generator
|
|
33
|
+
// instead (a later plan's job); this module hardcodes the target shape.
|
|
34
|
+
//
|
|
35
|
+
// WHAT THIS IS THE ONE AUTHORITATIVE PLACE FOR:
|
|
36
|
+
// - the provenance sidecar schema (`ProvenanceHeader`,
|
|
37
|
+
// `parseProvenanceHeader()`) -- nowhere else in this repo may hand-parse
|
|
38
|
+
// or hand-validate that JSON shape;
|
|
39
|
+
// - rendering the memory map (`renderMemoryMap()`) -- nowhere else may
|
|
40
|
+
// assemble the Range/Contents/Confidence/Evidence table or the banner;
|
|
41
|
+
// - drift detection (`checkRenderedMemoryMap()`) -- the one place a
|
|
42
|
+
// rendered file on disk is compared against what the store (plus the
|
|
43
|
+
// sidecar) would produce right now;
|
|
44
|
+
// - Markdown-cell escaping (`escapeMarkdownCell()`, WR-04, closed) --
|
|
45
|
+
// every store-derived text interpolation in the generated document
|
|
46
|
+
// (comment evidence, symbol names) is escaped through this one
|
|
47
|
+
// function, never a second ad hoc `.replace()` at a call site.
|
|
48
|
+
//
|
|
49
|
+
// WHAT NOT TO DO, named concretely:
|
|
50
|
+
// - Never hand-edit the rendered output. The banner exists precisely so a
|
|
51
|
+
// human editor is caught by `checkRenderedMemoryMap()` -- see the
|
|
52
|
+
// `render_digest` comment below for exactly what it covers.
|
|
53
|
+
// - Never read the layout from the skills tree at runtime (Phase 10 D-06).
|
|
54
|
+
// This module's own non-vacuity test asserts a zero-count grep for the
|
|
55
|
+
// recon skill's template filename -- if you are tempted to add a
|
|
56
|
+
// `readFileSync()` call reaching into `.claude/skills/`, don't; the
|
|
57
|
+
// layout lives here, in TypeScript, by design.
|
|
58
|
+
// - Never substitute a placeholder for a missing or malformed sidecar key.
|
|
59
|
+
// `parseProvenanceHeader()` throws, naming every problem at once, rather
|
|
60
|
+
// than rendering a document that LOOKS complete but silently carries a
|
|
61
|
+
// `<hash>`-shaped lie.
|
|
62
|
+
// - Never write an address row from anywhere but the store. If a future
|
|
63
|
+
// caller wants to add a derived-but-not-address-keyed fact (a new
|
|
64
|
+
// run-scoped field), it joins `ProvenanceHeader`'s schema, not a second
|
|
65
|
+
// ad hoc parameter to `renderMemoryMap()`.
|
|
66
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
67
|
+
import { createHash } from "node:crypto";
|
|
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
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
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}`.
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
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;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface R2000Comment {
|
|
100
|
+
address: number;
|
|
101
|
+
comment: string;
|
|
102
|
+
type: "line" | "side";
|
|
103
|
+
}
|
|
104
|
+
|
|
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
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// The provenance sidecar schema.
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
export interface ProvenanceHeader {
|
|
118
|
+
capturePath: string;
|
|
119
|
+
/** 64 lowercase or uppercase hex characters -- the capture's SHA-256. */
|
|
120
|
+
captureSha256: string;
|
|
121
|
+
/** The `$01` port value, e.g. `"$35"`. */
|
|
122
|
+
port01: string;
|
|
123
|
+
/** The `$DD00` value, e.g. `"$06"`. */
|
|
124
|
+
dd00: string;
|
|
125
|
+
/** The VIC bank derived from `$DD00` bits 0-1 (inverted), e.g. `"0 ($0000-$3FFF)"`. */
|
|
126
|
+
vicBank: string;
|
|
127
|
+
/** Screen RAM derived from `$D018` bits 4-7, e.g. `"$0400"`. */
|
|
128
|
+
screenRam: string;
|
|
129
|
+
/** Charset/bitmap derived from `$D018` bits 1-3, e.g. `"$1000 (ROM shadow)"`. */
|
|
130
|
+
charsetOrBitmap: string;
|
|
131
|
+
/** The graphics mode derived from `$D011` bits 5-6 and `$D016` bit 4, e.g. `"text, multicolor off"`. */
|
|
132
|
+
mode: string;
|
|
133
|
+
videoStandard: "PAL" | "NTSC";
|
|
134
|
+
/** The live vector pair in effect, e.g. `"$0314/$0315"` or `"$FFFE/$FFFF"`. */
|
|
135
|
+
liveVectorPair: string;
|
|
136
|
+
/** The address (or label) the live vector pair points at. */
|
|
137
|
+
vectorHandler: string;
|
|
138
|
+
/** One entry per observed `$D012` write on the way out of a handler. Optional. */
|
|
139
|
+
rasterPositions?: string[];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const REQUIRED_STRING_KEYS: readonly (keyof ProvenanceHeader)[] = [
|
|
143
|
+
"capturePath",
|
|
144
|
+
"captureSha256",
|
|
145
|
+
"port01",
|
|
146
|
+
"dd00",
|
|
147
|
+
"vicBank",
|
|
148
|
+
"screenRam",
|
|
149
|
+
"charsetOrBitmap",
|
|
150
|
+
"mode",
|
|
151
|
+
"videoStandard",
|
|
152
|
+
"liveVectorPair",
|
|
153
|
+
"vectorHandler",
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
/** A template placeholder is anything shaped like `<...>` -- the recon
|
|
157
|
+
* template's own placeholders (`<hash>`, `<PAL/NTSC>`, `<n>`, `<value>`,
|
|
158
|
+
* `<handler>`, ...) are exactly this shape, and the most likely thing to be
|
|
159
|
+
* copied into a sidecar by accident. */
|
|
160
|
+
const PLACEHOLDER_PATTERN = /^<.*>$/;
|
|
161
|
+
|
|
162
|
+
export class R2000ProvenanceHeaderError extends Error {
|
|
163
|
+
/** Every problem found, one entry per offending key -- a caller filling a
|
|
164
|
+
* sidecar wants the whole list, not one problem at a time. */
|
|
165
|
+
problems: readonly string[];
|
|
166
|
+
|
|
167
|
+
constructor(message: string, problems: readonly string[]) {
|
|
168
|
+
super(message);
|
|
169
|
+
this.name = "R2000ProvenanceHeaderError";
|
|
170
|
+
this.problems = problems;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Parses and validates a provenance sidecar. Collects EVERY problem (a
|
|
176
|
+
* missing key, a non-string value, a template placeholder, a malformed
|
|
177
|
+
* `captureSha256`, an invalid `videoStandard`, a malformed
|
|
178
|
+
* `rasterPositions`) into one list and throws `R2000ProvenanceHeaderError`
|
|
179
|
+
* naming all of them at once -- never one at a time.
|
|
180
|
+
*/
|
|
181
|
+
export function parseProvenanceHeader(json: unknown): ProvenanceHeader {
|
|
182
|
+
const problems: string[] = [];
|
|
183
|
+
|
|
184
|
+
if (typeof json !== "object" || json === null || Array.isArray(json)) {
|
|
185
|
+
throw new R2000ProvenanceHeaderError(
|
|
186
|
+
`provenance sidecar must be a JSON object, got ${Array.isArray(json) ? "an array" : typeof json}`,
|
|
187
|
+
["<root>: must be a JSON object"],
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
const obj = json as Record<string, unknown>;
|
|
191
|
+
|
|
192
|
+
for (const key of REQUIRED_STRING_KEYS) {
|
|
193
|
+
const value = obj[key];
|
|
194
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
195
|
+
problems.push(`${key}: missing or not a non-empty string`);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (PLACEHOLDER_PATTERN.test(value.trim())) {
|
|
199
|
+
problems.push(`${key}: still carries a template placeholder (${value}) -- fill in the real value`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const sha = obj.captureSha256;
|
|
204
|
+
if (typeof sha === "string" && sha.trim() !== "" && !PLACEHOLDER_PATTERN.test(sha.trim())) {
|
|
205
|
+
if (!/^[0-9a-fA-F]{64}$/.test(sha.trim())) {
|
|
206
|
+
problems.push(`captureSha256: must be exactly 64 hex characters, got "${sha}" (length ${sha.trim().length})`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const vs = obj.videoStandard;
|
|
211
|
+
if (typeof vs === "string" && vs.trim() !== "" && !PLACEHOLDER_PATTERN.test(vs.trim())) {
|
|
212
|
+
if (vs !== "PAL" && vs !== "NTSC") {
|
|
213
|
+
problems.push(`videoStandard: must be exactly "PAL" or "NTSC", got "${vs}"`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let rasterPositions: string[] | undefined;
|
|
218
|
+
if (obj.rasterPositions !== undefined) {
|
|
219
|
+
const rp = obj.rasterPositions;
|
|
220
|
+
if (!Array.isArray(rp) || rp.some((v) => typeof v !== "string")) {
|
|
221
|
+
problems.push("rasterPositions: when present must be an array of strings");
|
|
222
|
+
} else {
|
|
223
|
+
rasterPositions = rp as string[];
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (problems.length > 0) {
|
|
228
|
+
throw new R2000ProvenanceHeaderError(
|
|
229
|
+
`provenance sidecar has ${problems.length} problem(s):\n` + problems.map((p) => ` - ${p}`).join("\n"),
|
|
230
|
+
problems,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
capturePath: obj.capturePath as string,
|
|
236
|
+
captureSha256: (obj.captureSha256 as string).trim(),
|
|
237
|
+
port01: obj.port01 as string,
|
|
238
|
+
dd00: obj.dd00 as string,
|
|
239
|
+
vicBank: obj.vicBank as string,
|
|
240
|
+
screenRam: obj.screenRam as string,
|
|
241
|
+
charsetOrBitmap: obj.charsetOrBitmap as string,
|
|
242
|
+
mode: obj.mode as string,
|
|
243
|
+
videoStandard: obj.videoStandard as "PAL" | "NTSC",
|
|
244
|
+
liveVectorPair: obj.liveVectorPair as string,
|
|
245
|
+
vectorHandler: obj.vectorHandler as string,
|
|
246
|
+
rasterPositions,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
// The render digest -- documented exactly, because a digest whose inputs are
|
|
252
|
+
// 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
|
|
255
|
+
// confidence grade, changes the digest even with the rendered file
|
|
256
|
+
// untouched), the raw provenance sidecar BYTES (not the parsed object, so
|
|
257
|
+
// even whitespace-only sidecar edits are covered), and this renderer's own
|
|
258
|
+
// version constant (so a future format change is distinguishable from a
|
|
259
|
+
// hand edit).
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
|
|
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";
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Escapes `text` for safe interpolation into a Markdown table cell or list
|
|
270
|
+
* item: every `|` becomes `\|`, and every `\r\n`/`\n`/bare `\r` collapses to
|
|
271
|
+
* `<br>` (a single-line-safe line break inside a table cell). This control
|
|
272
|
+
* ESCAPES and never REJECTS -- unlike the label-name policy
|
|
273
|
+
* (`r2000-acme-ident.ts`'s `assertLegalAcmeIdentifier()`, T-11-NAME-INJECT's
|
|
274
|
+
* other leg), because comment `evidence` legitimately contains `|` and
|
|
275
|
+
* embedded newlines (`r2000_set_comment`'s own schema documents multi-line
|
|
276
|
+
* support) -- refusing here would refuse valid data, not an attack. Closes
|
|
277
|
+
* WR-04 / T-11-NAME-INJECT's render leg: an unescaped `|` or newline in
|
|
278
|
+
* store text used to be able to inject an extra table cell or split a row
|
|
279
|
+
* across lines in the generated Markdown. A plain string or an empty string
|
|
280
|
+
* is returned unchanged. */
|
|
281
|
+
export function escapeMarkdownCell(text: string): string {
|
|
282
|
+
return text.replace(/\|/g, "\\|").replace(/\r\n|\r|\n/g, "<br>");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function computeRenderDigest(
|
|
286
|
+
blocks: readonly R2000Block[],
|
|
287
|
+
symbols: readonly R2000Symbol[],
|
|
288
|
+
comments: readonly R2000Comment[],
|
|
289
|
+
sidecarBytes: string,
|
|
290
|
+
): string {
|
|
291
|
+
const canonical = JSON.stringify({ blocks, symbols, comments }) + "" + sidecarBytes + "" + RENDERER_VERSION;
|
|
292
|
+
return createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function hex4(addr: number): string {
|
|
296
|
+
return `$${addr.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
interface GradedComment {
|
|
300
|
+
address: number;
|
|
301
|
+
grade: ConfidenceGrade | null;
|
|
302
|
+
evidence: string;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
// renderMemoryMap()
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
export interface RenderMemoryMapOptions {
|
|
310
|
+
projectPath: string;
|
|
311
|
+
provenancePath: string;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export interface RenderMemoryMapResult {
|
|
315
|
+
markdown: string;
|
|
316
|
+
renderDigest: string;
|
|
317
|
+
/** Number of Range/Contents/Confidence/Evidence rows emitted. */
|
|
318
|
+
rowCount: number;
|
|
319
|
+
/** Number of comments carrying the `[unknown]` grade -- the Open questions count. */
|
|
320
|
+
unknownCount: number;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
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).
|
|
329
|
+
*
|
|
330
|
+
* A malformed confidence prefix inside a store comment (a typo that survived
|
|
331
|
+
* whatever wrote it) THROWS through `parseConfidencePrefix()` -- this
|
|
332
|
+
* renderer never silently drops or blanks a grade it cannot parse; the typo
|
|
333
|
+
* must be fixed in the store, not hidden in the rendered view.
|
|
334
|
+
*/
|
|
335
|
+
export async function renderMemoryMap(opts: RenderMemoryMapOptions): Promise<RenderMemoryMapResult> {
|
|
336
|
+
const { projectPath, provenancePath } = opts;
|
|
337
|
+
|
|
338
|
+
let sidecarBytes: string;
|
|
339
|
+
try {
|
|
340
|
+
sidecarBytes = readFileSync(provenancePath, "utf8");
|
|
341
|
+
} catch (err) {
|
|
342
|
+
throw new Error(`renderMemoryMap: could not read provenance sidecar at "${provenancePath}": ${errMsg(err)}`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
let sidecarJson: unknown;
|
|
346
|
+
try {
|
|
347
|
+
sidecarJson = JSON.parse(sidecarBytes);
|
|
348
|
+
} catch (err) {
|
|
349
|
+
throw new Error(`renderMemoryMap: provenance sidecar at "${provenancePath}" is not valid JSON: ${errMsg(err)}`);
|
|
350
|
+
}
|
|
351
|
+
const provenance = parseProvenanceHeader(sidecarJson);
|
|
352
|
+
|
|
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
|
+
});
|
|
359
|
+
|
|
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);
|
|
363
|
+
|
|
364
|
+
const gradedComments: GradedComment[] = sortedComments.map((c) => {
|
|
365
|
+
const parsed = parseConfidencePrefix(c.comment);
|
|
366
|
+
return { address: c.address, grade: parsed.grade, evidence: parsed.rest };
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
function findGradeInRange(startAddr: number, endAddr: number): GradedComment | undefined {
|
|
370
|
+
return gradedComments.find((c) => c.address >= startAddr && c.address <= endAddr);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const renderDigest = computeRenderDigest(sortedBlocks, sortedSymbols, sortedComments, sidecarBytes);
|
|
374
|
+
|
|
375
|
+
const lines: string[] = [];
|
|
376
|
+
|
|
377
|
+
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}`);
|
|
381
|
+
lines.push(` render_digest: ${renderDigest}`);
|
|
382
|
+
lines.push(
|
|
383
|
+
" The digest covers the sorted r2000_get_blocks/r2000_get_symbols/r2000_get_comments results, the",
|
|
384
|
+
);
|
|
385
|
+
lines.push(
|
|
386
|
+
" raw provenance sidecar bytes, and this renderer's version constant -- so either a hand edit or a",
|
|
387
|
+
);
|
|
388
|
+
lines.push(" store-side change (e.g. a comment's confidence grade) is detected by `render-memmap --check`.");
|
|
389
|
+
lines.push("-->");
|
|
390
|
+
lines.push("");
|
|
391
|
+
lines.push(`# Memory map — ${provenance.capturePath}`);
|
|
392
|
+
lines.push("");
|
|
393
|
+
lines.push(`Capture: \`${provenance.capturePath}\` · SHA-256 \`${provenance.captureSha256}\``);
|
|
394
|
+
lines.push(
|
|
395
|
+
`\`$01\` = \`${provenance.port01}\` · VIC bank \`${provenance.vicBank}\` (\`$DD00\` = \`${provenance.dd00}\`) · video standard \`${provenance.videoStandard}\``,
|
|
396
|
+
);
|
|
397
|
+
lines.push(`Live vector pair: \`${provenance.liveVectorPair}\` → \`${provenance.vectorHandler}\``);
|
|
398
|
+
lines.push("");
|
|
399
|
+
lines.push(
|
|
400
|
+
"Every row carries a confidence. Do not promote a row by editing its grade -- re-verify and restate",
|
|
401
|
+
);
|
|
402
|
+
lines.push("the evidence, so the record of when something stopped being a guess survives.");
|
|
403
|
+
lines.push("");
|
|
404
|
+
lines.push("| Range | Contents | Confidence | Evidence |");
|
|
405
|
+
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)}\``;
|
|
409
|
+
const grade = match?.grade ? match.grade.phrase.toUpperCase() : "";
|
|
410
|
+
const evidence = match ? escapeMarkdownCell(match.evidence) : "";
|
|
411
|
+
lines.push(`| ${range} | ${block.type} | ${grade} | ${evidence} |`);
|
|
412
|
+
}
|
|
413
|
+
lines.push("");
|
|
414
|
+
lines.push("Confidence vocabulary — the project's HIGH / MEDIUM / LOW scale, applied to classification:");
|
|
415
|
+
lines.push("");
|
|
416
|
+
lines.push("| Grade | Means |");
|
|
417
|
+
lines.push("|---|---|");
|
|
418
|
+
for (const grade of CONFIDENCE_GRADES) {
|
|
419
|
+
lines.push(`| **${grade.phrase}** | ${grade.meaning} |`);
|
|
420
|
+
}
|
|
421
|
+
lines.push("");
|
|
422
|
+
lines.push("## Graphics chain");
|
|
423
|
+
lines.push("");
|
|
424
|
+
lines.push("| What | Address | Derived from |");
|
|
425
|
+
lines.push("|---|---|---|");
|
|
426
|
+
lines.push(`| VIC bank | ${provenance.vicBank} | \`$DD00\` bits 0-1, inverted |`);
|
|
427
|
+
lines.push(`| Screen RAM (VM) | ${provenance.screenRam} | \`$D018\` bits 4-7 |`);
|
|
428
|
+
lines.push(`| Charset / bitmap (CB) | ${provenance.charsetOrBitmap} | \`$D018\` bits 1-3 |`);
|
|
429
|
+
lines.push(`| Mode | ${provenance.mode} | \`$D011\` bits 5-6, \`$D016\` bit 4 |`);
|
|
430
|
+
lines.push("");
|
|
431
|
+
lines.push("## Interrupts");
|
|
432
|
+
lines.push("");
|
|
433
|
+
lines.push("| | Address | Notes |");
|
|
434
|
+
lines.push("|---|---|---|");
|
|
435
|
+
lines.push(`| Live IRQ handler | ${provenance.vectorHandler} | via ${provenance.liveVectorPair} |`);
|
|
436
|
+
if (provenance.rasterPositions && provenance.rasterPositions.length > 0) {
|
|
437
|
+
lines.push(
|
|
438
|
+
`| Raster positions | ${provenance.rasterPositions.join(", ")} | one per \`$D012\` write on the way out of a handler |`,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
lines.push("");
|
|
442
|
+
lines.push("## Routines");
|
|
443
|
+
lines.push("");
|
|
444
|
+
lines.push("| Address | Provisional name | Confirmed by | Confidence |");
|
|
445
|
+
lines.push("|---|---|---|---|");
|
|
446
|
+
const codeBlocks = sortedBlocks.filter((b) => b.type === "Code");
|
|
447
|
+
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;
|
|
450
|
+
const match = gradedComments.find((c) => c.address === sym.address);
|
|
451
|
+
const grade = match?.grade ? match.grade.phrase.toUpperCase() : "";
|
|
452
|
+
const confirmedBy = match ? escapeMarkdownCell(match.evidence) : "";
|
|
453
|
+
lines.push(`| ${hex4(sym.address)} | ${escapeMarkdownCell(sym.name)} | ${confirmedBy} | ${grade} |`);
|
|
454
|
+
}
|
|
455
|
+
lines.push("");
|
|
456
|
+
lines.push("## Open questions");
|
|
457
|
+
lines.push("");
|
|
458
|
+
const unknowns = gradedComments.filter((c) => c.grade?.token === "unknown");
|
|
459
|
+
if (unknowns.length === 0) {
|
|
460
|
+
lines.push("- (none)");
|
|
461
|
+
} else {
|
|
462
|
+
for (const u of unknowns) {
|
|
463
|
+
lines.push(`- ${hex4(u.address)}: ${escapeMarkdownCell(u.evidence)}`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
lines.push("");
|
|
467
|
+
|
|
468
|
+
const markdown = lines.join("\n");
|
|
469
|
+
return { markdown, renderDigest, rowCount: sortedBlocks.length, unknownCount: unknowns.length };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ---------------------------------------------------------------------------
|
|
473
|
+
// checkRenderedMemoryMap()
|
|
474
|
+
// ---------------------------------------------------------------------------
|
|
475
|
+
|
|
476
|
+
export interface CheckRenderedMemoryMapOptions {
|
|
477
|
+
projectPath: string;
|
|
478
|
+
provenancePath: string;
|
|
479
|
+
renderedPath: string;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export type CheckRenderedMemoryMapResult =
|
|
483
|
+
| { status: "in-sync" }
|
|
484
|
+
| { status: "drifted"; line: number; expected: string; actual: string }
|
|
485
|
+
| { status: "missing"; path: string };
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Re-renders the memory map from the CURRENT store and sidecar state and
|
|
489
|
+
* compares it against the file on disk at `renderedPath`, line by line.
|
|
490
|
+
* Never auto-fixes. Returns:
|
|
491
|
+
* - `{status:"missing"}` when `renderedPath` does not exist;
|
|
492
|
+
* - `{status:"in-sync"}` when the freshly rendered text is byte-identical
|
|
493
|
+
* to the file on disk;
|
|
494
|
+
* - `{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.
|
|
498
|
+
*/
|
|
499
|
+
export async function checkRenderedMemoryMap(
|
|
500
|
+
opts: CheckRenderedMemoryMapOptions,
|
|
501
|
+
): Promise<CheckRenderedMemoryMapResult> {
|
|
502
|
+
const { projectPath, provenancePath, renderedPath } = opts;
|
|
503
|
+
|
|
504
|
+
if (!existsSync(renderedPath)) {
|
|
505
|
+
return { status: "missing", path: renderedPath };
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const onDisk = readFileSync(renderedPath, "utf8");
|
|
509
|
+
const { markdown } = await renderMemoryMap({ projectPath, provenancePath });
|
|
510
|
+
|
|
511
|
+
if (onDisk === markdown) {
|
|
512
|
+
return { status: "in-sync" };
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const diskLines = onDisk.split("\n");
|
|
516
|
+
const freshLines = markdown.split("\n");
|
|
517
|
+
const max = Math.max(diskLines.length, freshLines.length);
|
|
518
|
+
for (let i = 0; i < max; i++) {
|
|
519
|
+
if (diskLines[i] !== freshLines[i]) {
|
|
520
|
+
return {
|
|
521
|
+
status: "drifted",
|
|
522
|
+
line: i + 1,
|
|
523
|
+
expected: freshLines[i] ?? "(end of file)",
|
|
524
|
+
actual: diskLines[i] ?? "(end of file)",
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
// Unreachable in practice (the strings already compared unequal above),
|
|
529
|
+
// kept only as a defensive fallback.
|
|
530
|
+
return { status: "drifted", line: max + 1, expected: "(no further lines)", actual: "(no further lines)" };
|
|
531
|
+
}
|