@henols/vice-mcp 0.2.2 → 0.2.4
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 -2
- package/THIRD-PARTY-NOTICES.md +422 -1
- package/anno-bank.ts +171 -0
- package/anno-cli.ts +1736 -163
- package/anno-confidence.ts +2 -2
- package/anno-derive.ts +6 -6
- package/anno-details.ts +4 -4
- package/anno-enum-gen.ts +416 -30
- package/anno-export-asm.ts +1211 -126
- package/anno-graphics.ts +338 -0
- package/anno-hazard-report.ts +1367 -0
- package/anno-import.ts +495 -0
- package/anno-index.ts +8 -8
- package/anno-join.ts +480 -0
- package/anno-memmap-render.ts +22 -21
- package/anno-provenance-ledger.ts +472 -0
- package/anno-regbits-gen.ts +13 -13
- package/anno-register.ts +159 -0
- package/anno-store-export.ts +661 -0
- package/anno-store.ts +635 -124
- package/anno-symbols.ts +7 -7
- package/anno-tools.ts +1169 -16
- package/anno-types.ts +313 -40
- package/backend-detect.mts +124 -312
- package/build.ts +3 -1
- package/capture-predicate.ts +597 -0
- package/channel-lock.ts +349 -0
- package/evid-ingest.ts +217 -0
- package/evid-reconcile.ts +316 -0
- package/host-tool-client.ts +430 -0
- package/incident-record.ts +23 -12
- package/install-resources.ts +29 -13
- package/memmap-lookup.ts +285 -0
- package/package.json +27 -8
- package/prg-image.ts +1 -2
- package/repo-root.ts +87 -3
- package/resources/backend-detect.mjs +98 -236
- package/resources/broker-control.mjs +220 -54
- package/resources/broker-epoch.mjs +7 -8
- package/resources/broker-kill.mjs +36 -31
- package/resources/broker-launch.mjs +511 -374
- package/resources/broker-state.mjs +69 -24
- package/resources/container-guard.mjs +1 -1
- package/resources/ghidra-project.mjs +790 -0
- package/resources/host-tool.mjs +2533 -0
- package/resources/vice-broker.mjs +434 -290
- package/resources/vice-launcher.sh +127 -9
- package/stock-address.ts +1 -1
- package/stock-condition.ts +1 -1
- package/stock-connect.ts +9 -5
- package/stock-derived.ts +29 -37
- package/stock-diagnose.ts +200 -36
- package/stock-dispatch.ts +179 -77
- package/stock-handler.ts +1 -1
- package/stock-paths.ts +18 -14
- package/stock-petscii.ts +1 -1
- package/stock-protocol.ts +1 -1
- package/stock-recycle.ts +83 -2
- package/stock-reproducible-run.ts +811 -0
- package/stock-run-until.ts +100 -1
- package/stock-symbols.ts +4 -4
- package/stock-timing.ts +1 -1
- package/stop-oracle.ts +167 -0
- package/text-capability-probe.ts +660 -0
- package/text-connect.ts +157 -0
- package/text-protocol.ts +810 -0
- package/text-tools.ts +778 -0
- package/textmon-backtrace.ts +385 -0
- package/textmon-cpuhistory.ts +335 -0
- package/textmon-memmap.ts +494 -0
- package/textmon-profile.ts +458 -0
- package/textmon-registers.ts +748 -0
- package/tools-manifest.stock.json +864 -3
- package/vice-broker-client.ts +253 -108
- package/vice-errors.ts +268 -0
- package/vice-proxy.ts +339 -2144
- package/vsf-slice.ts +640 -0
- package/anno-d64.ts +0 -310
- package/capability-registry.ts +0 -390
- package/refresh-manifest.ts +0 -124
- package/tools-manifest.json +0 -1223
- package/vice-probe.ts +0 -278
- package/vice-sync.ts +0 -336
- package/vice.ts +0 -772
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
// anno-provenance-ledger.ts -- the ONE place `recovery/PROVENANCE.md`'s
|
|
2
|
+
// generated tier is turned back into typed rows.
|
|
3
|
+
//
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// WHY THIS FILE EXISTS
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Nothing in this repository has ever READ the provenance ledger. It has one
|
|
8
|
+
// writer -- `renderLedger()` in `src/skills/c64-provenance-diff/scripts/
|
|
9
|
+
// diff-images.mjs` -- and zero readers, because the requirement governing this
|
|
10
|
+
// module requires the verdict be READ from the existing ledger, "never
|
|
11
|
+
// re-derived". Recomputing it in-process would force the
|
|
12
|
+
// exporter to import a registry-resolution path it has no other reason to
|
|
13
|
+
// know about, and would make "ledger absent" ambiguous between three
|
|
14
|
+
// different missing things. This module exists so "read" has an implementation
|
|
15
|
+
// distinct from "recompute".
|
|
16
|
+
//
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// WHAT THIS IS THE ONE AUTHORITATIVE PLACE FOR
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Turning `renderLedger()`'s emitted Markdown table -- the generated tier of
|
|
21
|
+
// `recovery/PROVENANCE.md` -- back into typed `ProvenanceLedgerRow` values,
|
|
22
|
+
// plus the one pure address-range join (`provenanceForRange()`) a caller uses
|
|
23
|
+
// to find which rows overlap a block it is about to emit.
|
|
24
|
+
//
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// WHAT NOT TO DO
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// - Never recompute a verdict. This module parses TEXT `renderLedger()`
|
|
29
|
+
// already wrote; it never calls `diffRanges()`, never touches a release
|
|
30
|
+
// registry, and never derives a Verdict or Confidence value from bytes.
|
|
31
|
+
// - Never repair a malformed row. A row that does not split into exactly
|
|
32
|
+
// seven pipe-delimited cells, or whose Start/End cell is not the `$XXXX`
|
|
33
|
+
// shape `renderLedger()`'s own `hex4()` writes, is REFUSED -- never
|
|
34
|
+
// best-effort-parsed, never defaulted, never silently skipped. No
|
|
35
|
+
// trimming a cell into shape, no defaulting a missing column, no
|
|
36
|
+
// coercing a bad address to zero, no dropping a bad row and continuing
|
|
37
|
+
// with the rest: a partially-parsed ledger that reports success is worse
|
|
38
|
+
// than a refusal, because every downstream verdict then looks
|
|
39
|
+
// authoritative.
|
|
40
|
+
// - Never interpolate the ledger's own row text into an error message. A
|
|
41
|
+
// path, a line number and an expected shape are facts ABOUT a file; its
|
|
42
|
+
// contents are not, and quoting them turns a refusal into a
|
|
43
|
+
// content-disclosure oracle -- the same rule `anno-export-asm.ts`'s own
|
|
44
|
+
// header states and this module's refusals below all follow. THE
|
|
45
|
+
// PERMITTED VOCABULARY, enumerated rather than left as a general
|
|
46
|
+
// instruction so a later contributor adding a refusal has it in front of
|
|
47
|
+
// them, is exactly these five fact kinds and nothing else:
|
|
48
|
+
// 1. the ledger's PATH.
|
|
49
|
+
// 2. a 1-based LINE NUMBER, when the refusal is about one row.
|
|
50
|
+
// 3. a column NAME (`"Start"`, `"End"`, or one of
|
|
51
|
+
// `PROVENANCE_LEDGER_HEADER_CELLS`).
|
|
52
|
+
// 4. a cell COUNT (how many cells a row split into, vs. how many were
|
|
53
|
+
// expected).
|
|
54
|
+
// 5. a parsed ADDRESS (a `number` this module itself derived by calling
|
|
55
|
+
// `parseHexAddress()`, formatted back through this module's own
|
|
56
|
+
// `hex4()` -- never a cell's raw text passed through unexamined).
|
|
57
|
+
// Cell TEXT -- Kind, Verdict, Confidence, Agreeing releases, Evidence /
|
|
58
|
+
// Reason, or any cell that failed to parse -- is not on this list and
|
|
59
|
+
// must never appear in a thrown message.
|
|
60
|
+
// - Never re-derive `renderLedger()`'s CONTENT-shaped preconditions here.
|
|
61
|
+
// `renderLedger()` refuses to EMIT under three conditions: an UNKNOWN row
|
|
62
|
+
// with an empty reason, an ORIGINAL row with `agreeing_releases` below 2,
|
|
63
|
+
// and a generated tier that does not tile `$0000-$FFFF` exactly. The
|
|
64
|
+
// first two are claims about what a Verdict cell's CONTENT means --
|
|
65
|
+
// asserting them here would make this reader adjudicate a verdict's
|
|
66
|
+
// substance, exactly the "tool is the decider" shape this phase exists
|
|
67
|
+
// to make structurally unreachable. Coverage, disjointness and ascending
|
|
68
|
+
// order (the third condition, and the ones this module DOES assert
|
|
69
|
+
// below) are claims about ADDRESSES only, which is the one thing a join
|
|
70
|
+
// over ranges is allowed to reason about. A file violating an
|
|
71
|
+
// ADDRESS-shaped invariant was never producible by the one writer this
|
|
72
|
+
// format has, so the violation is evidence of a hand edit or corruption,
|
|
73
|
+
// not of a reader bug -- accepting it would mean joining a store range
|
|
74
|
+
// against a table that does not describe the whole address space.
|
|
75
|
+
//
|
|
76
|
+
// This module imports NOTHING from `src/skills/` (the shipped `@henols/
|
|
77
|
+
// vice-mcp` tarball does not contain that tree, and `anno-join.ts:26` already
|
|
78
|
+
// states the rule for this module family) and nothing from
|
|
79
|
+
// `hostpath.ts`/`containerpath.ts` -- every path this module reads is handed
|
|
80
|
+
// to it already resolved, exactly as `anno-export-asm.ts`'s own `imagePath`
|
|
81
|
+
// is documented: the CALLER owns confinement, this module only reads.
|
|
82
|
+
import { readFileSync } from "node:fs";
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The seven column names `renderLedger()` writes, in this exact order and
|
|
86
|
+
* spelling (`diff-images.mjs`'s own header row). ONE declaration, one
|
|
87
|
+
* spelling -- `readProvenanceLedger()` below locates the header row by
|
|
88
|
+
* matching every cell against this array, so a header row that drifted from
|
|
89
|
+
* this exact wording is treated as "no header found" rather than guessed at.
|
|
90
|
+
*/
|
|
91
|
+
export const PROVENANCE_LEDGER_HEADER_CELLS = Object.freeze([
|
|
92
|
+
"Start",
|
|
93
|
+
"End",
|
|
94
|
+
"Kind",
|
|
95
|
+
"Verdict",
|
|
96
|
+
"Confidence",
|
|
97
|
+
"Agreeing releases",
|
|
98
|
+
"Evidence / Reason",
|
|
99
|
+
] as const);
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* One row of the ledger's generated tier, copied verbatim off the table --
|
|
103
|
+
* every string field is the cell's own text, UN-ESCAPED (`\|` back to `|`)
|
|
104
|
+
* but never trimmed of meaning, normalised, upper-cased or mapped. An
|
|
105
|
+
* unrecognised Verdict or Confidence string is carried through exactly as
|
|
106
|
+
* written; this module does not know the vocabulary and does not check it.
|
|
107
|
+
*/
|
|
108
|
+
export interface ProvenanceLedgerRow {
|
|
109
|
+
/** First address the row covers, parsed from the `$XXXX` form `hex4()`
|
|
110
|
+
* writes. */
|
|
111
|
+
start: number;
|
|
112
|
+
/** Last address the row covers, INCLUSIVE -- `renderLedger()` writes
|
|
113
|
+
* `hex4(r.end)` and advances its own coverage cursor with `r.end + 1`, so
|
|
114
|
+
* the column this module reads is already an inclusive end. */
|
|
115
|
+
endInclusive: number;
|
|
116
|
+
/** The Kind cell, copied verbatim -- `game`/`loader`/`cracktro`/`io`/
|
|
117
|
+
* `unused`/`unresolved` in a well-formed ledger, but this module does not
|
|
118
|
+
* check membership; it copies whatever text is there. */
|
|
119
|
+
kind: string;
|
|
120
|
+
/** The Verdict cell, copied verbatim. Never normalised: an unrecognised
|
|
121
|
+
* string is carried through as text, exactly as the plan's own prohibition
|
|
122
|
+
* requires. */
|
|
123
|
+
verdict: string;
|
|
124
|
+
/** The Confidence cell, copied verbatim, on the same terms as `verdict`. */
|
|
125
|
+
confidence: string;
|
|
126
|
+
/**
|
|
127
|
+
* The Agreeing releases cell, carried as a STRING, never parsed to a
|
|
128
|
+
* number. `renderLedger()` interpolates `${r.agreeing_releases}` with no
|
|
129
|
+
* guard, so a `CRACKER-PATCH` or `UNKNOWN` row can legitimately carry the
|
|
130
|
+
* literal text `undefined` in this column -- inventing a number there
|
|
131
|
+
* would be this module deciding something the ledger itself never
|
|
132
|
+
* committed to.
|
|
133
|
+
*/
|
|
134
|
+
agreeingReleases: string;
|
|
135
|
+
/** The Evidence / Reason cell, un-escaped (`\|` back to `|`) but
|
|
136
|
+
* otherwise verbatim. */
|
|
137
|
+
evidence: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** One parsed ledger: the path it was read from, and every row the generated
|
|
141
|
+
* tier carried, in file order. */
|
|
142
|
+
export interface ProvenanceLedger {
|
|
143
|
+
path: string;
|
|
144
|
+
rows: readonly ProvenanceLedgerRow[];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface ProvenanceLedgerErrorOptions {
|
|
148
|
+
path: string;
|
|
149
|
+
/** 1-based line number of the offending row, when the refusal is about one
|
|
150
|
+
* specific row rather than the file as a whole. */
|
|
151
|
+
lineNumber?: number;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Thrown by `readProvenanceLedger()`. Follows `ViceError`'s shape convention
|
|
156
|
+
* (`vice.ts`) -- plain public fields, no sanitising -- rather than a
|
|
157
|
+
* message-only `Error`, so a caller that wants the path or the line number
|
|
158
|
+
* programmatically (a CLI wrapper printing its own summary, say) does not
|
|
159
|
+
* have to parse them back out of prose.
|
|
160
|
+
*/
|
|
161
|
+
export class ProvenanceLedgerError extends Error {
|
|
162
|
+
path: string;
|
|
163
|
+
lineNumber?: number;
|
|
164
|
+
|
|
165
|
+
constructor(message: string, { path, lineNumber }: ProvenanceLedgerErrorOptions) {
|
|
166
|
+
super(message);
|
|
167
|
+
this.name = "ProvenanceLedgerError";
|
|
168
|
+
this.path = path;
|
|
169
|
+
this.lineNumber = lineNumber;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The remedy every refusal below names -- one sentence, one place, so the
|
|
174
|
+
* two named routes stay in sync with each other rather than being retyped at
|
|
175
|
+
* every throw site. */
|
|
176
|
+
const LEDGER_REMEDY =
|
|
177
|
+
'regenerate it with c64-provenance-diff\'s "ledger" verb, or omit --ledger to export without provenance annotation';
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Splits one line into its pipe-delimited cells, or returns `undefined` when
|
|
181
|
+
* the line is not shaped like a table row at all (does not start with `|`
|
|
182
|
+
* once trimmed -- true of the blank line and the `## Prose tier` heading that
|
|
183
|
+
* follow the generated tier, and of every prose line before the table).
|
|
184
|
+
*
|
|
185
|
+
* `\|` is protected before splitting and restored afterward in every cell,
|
|
186
|
+
* matching the one escape `renderLedger()`'s writer applies (only ever to the
|
|
187
|
+
* Evidence/Reason cell in practice, but restored uniformly here since a
|
|
188
|
+
* split-time placeholder is an artefact of splitting, not a second escaping
|
|
189
|
+
* rule).
|
|
190
|
+
*
|
|
191
|
+
* `PLACEHOLDER` is a one-character NUL string, built at RUNTIME with
|
|
192
|
+
* String.fromCharCode(0) rather than written as a literal byte -- or any
|
|
193
|
+
* backslash escape sequence naming that byte -- anywhere in this file's
|
|
194
|
+
* own source text. A raw NUL byte on disk makes `git diff` render this
|
|
195
|
+
* module as `Bin 0 -> N bytes` with no line-level diff ever again, and
|
|
196
|
+
* hides the file from a plain (non-`-a`) `grep` census -- the exact
|
|
197
|
+
* hazard `anno-memmap-render.ts` already carries and that this module
|
|
198
|
+
* must not add a second instance of. `String.fromCharCode(0)` produces
|
|
199
|
+
* the IDENTICAL runtime character while keeping every byte of the
|
|
200
|
+
* source file printable ASCII, with no escape-sequence spelling for a
|
|
201
|
+
* reviewing tool (or a lossy copy-paste) to silently collapse into the
|
|
202
|
+
* raw byte it names. A plain space is NOT a safe substitute for the
|
|
203
|
+
* sentinel value: ordinary ledger prose ("Agreeing releases", "Evidence
|
|
204
|
+
* / Reason") already contains spaces, so restoring a space-shaped
|
|
205
|
+
* placeholder back to `|` would corrupt every cell that happens to
|
|
206
|
+
* contain one -- reproduced against this file's own header-row match
|
|
207
|
+
* failing outright. NUL is chosen because ordinary ledger text -- a
|
|
208
|
+
* Markdown table written by renderLedger(), plausibly hand-edited --
|
|
209
|
+
* cannot contain it.
|
|
210
|
+
*
|
|
211
|
+
* The leading and trailing empty cells a `| a | b |`-shaped line produces are
|
|
212
|
+
* dropped ONLY when they are genuinely empty -- a row missing its closing `|`
|
|
213
|
+
* is left with a non-empty last cell, so the seven-cell count check at the
|
|
214
|
+
* call site catches the malformed shape rather than this function silently
|
|
215
|
+
* repairing it.
|
|
216
|
+
*
|
|
217
|
+
* A raw NUL byte already present in the line -- e.g. in a hand-edited or
|
|
218
|
+
* corrupted ledger's Evidence/Reason cell -- is REFUSED rather than restored:
|
|
219
|
+
* without this check, an existing NUL would collide with the placeholder
|
|
220
|
+
* this function inserts for its own `\|` escaping, and the unconditional
|
|
221
|
+
* `.join("|")` restoration below would silently turn that pre-existing NUL
|
|
222
|
+
* into an extra, unescaped `|` that never delimited anything in the source
|
|
223
|
+
* file. That is exactly the "repair a malformed row" outcome this module's
|
|
224
|
+
* header forbids, so it is named and refused like every other malformed
|
|
225
|
+
* shape this function's caller checks for -- never coerced, never dropped.
|
|
226
|
+
* `ledgerPath` and `lineNumber` are threaded through only to build that
|
|
227
|
+
* refusal's message; they change no other behaviour of this function.
|
|
228
|
+
*/
|
|
229
|
+
function splitTableRow(line: string, ledgerPath: string, lineNumber: number): string[] | undefined {
|
|
230
|
+
const trimmed = line.trim();
|
|
231
|
+
if (!trimmed.startsWith("|")) return undefined;
|
|
232
|
+
|
|
233
|
+
const PLACEHOLDER = String.fromCharCode(0);
|
|
234
|
+
if (trimmed.includes(PLACEHOLDER)) {
|
|
235
|
+
throw new ProvenanceLedgerError(
|
|
236
|
+
`anno-provenance-ledger: row ${lineNumber} of "${ledgerPath}" contains a raw NUL byte -- refusing to parse a row that ` +
|
|
237
|
+
`could collide with this module's own internal escape-placeholder sentinel rather than silently turning it into an ` +
|
|
238
|
+
`unescaped "|". ${LEDGER_REMEDY}.`,
|
|
239
|
+
{ path: ledgerPath, lineNumber },
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
const protectedLine = trimmed.replace(/\\\|/g, PLACEHOLDER);
|
|
243
|
+
let cells = protectedLine.split("|");
|
|
244
|
+
if (cells[0] === "") cells = cells.slice(1);
|
|
245
|
+
if (cells.length > 0 && cells[cells.length - 1] === "") cells = cells.slice(0, -1);
|
|
246
|
+
|
|
247
|
+
return cells.map((cell) => cell.trim().split(PLACEHOLDER).join("|"));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Parses a `$XXXX` cell (exactly `hex4()`'s own output shape) into a
|
|
251
|
+
* number, or `undefined` when the cell does not match -- the caller turns
|
|
252
|
+
* `undefined` into a named refusal rather than this function guessing. */
|
|
253
|
+
function parseHexAddress(cell: string): number | undefined {
|
|
254
|
+
const match = /^\$([0-9a-fA-F]{4})$/.exec(cell);
|
|
255
|
+
if (!match) return undefined;
|
|
256
|
+
return Number.parseInt(match[1]!, 16);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Formats a PARSED address (never a cell's raw text) as `$XXXX`, for use in
|
|
260
|
+
* refusal messages -- the one fact kind on the permitted-vocabulary list that
|
|
261
|
+
* is a number this module derived itself, not text copied off the file. */
|
|
262
|
+
function hex4(value: number): string {
|
|
263
|
+
return `$${(value & 0xffff).toString(16).padStart(4, "0")}`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Throws the shared bad-address refusal for row `lineNumber`'s `column`
|
|
268
|
+
* cell (`"Start"` or `"End"`) -- ONE template for both columns, since the
|
|
269
|
+
* only fact that differs between them is the column NAME, which is on the
|
|
270
|
+
* permitted vocabulary list. The cell's own text is never read here or by
|
|
271
|
+
* either call site: the caller passes only the column name, never the
|
|
272
|
+
* string that failed to parse.
|
|
273
|
+
*/
|
|
274
|
+
function refuseBadAddress(ledgerPath: string, lineNumber: number, column: "Start" | "End"): never {
|
|
275
|
+
throw new ProvenanceLedgerError(
|
|
276
|
+
`anno-provenance-ledger: row ${lineNumber} of "${ledgerPath}" has a ${column} cell that is not the "$XXXX" shape ` +
|
|
277
|
+
`renderLedger()'s own hex4() writes -- refusing to parse an address this module cannot be sure of, and never guessing ` +
|
|
278
|
+
`one from the cell's own text. ${LEDGER_REMEDY}.`,
|
|
279
|
+
{ path: ledgerPath, lineNumber },
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Reads and parses `ledgerPath`'s generated tier.
|
|
285
|
+
*
|
|
286
|
+
* Locates the header row by matching every cell against
|
|
287
|
+
* `PROVENANCE_LEDGER_HEADER_CELLS`, skips the `|---|` separator line
|
|
288
|
+
* immediately below it without re-validating its own shape, then parses
|
|
289
|
+
* every following pipe-delimited line into a `ProvenanceLedgerRow` until the
|
|
290
|
+
* first line that is not a table row at all, then asserts the accepted rows
|
|
291
|
+
* as a WHOLE are strictly ascending, disjoint, and tile exactly
|
|
292
|
+
* `$0000-$FFFF`.
|
|
293
|
+
*
|
|
294
|
+
* Refuses (`ProvenanceLedgerError`) for eight distinct reasons, and REJECTS
|
|
295
|
+
* rather than repairs at every one of them -- no trimming, no defaulting, no
|
|
296
|
+
* coercing a bad address to zero, no dropping a bad row and continuing with
|
|
297
|
+
* the rest. Listed here in RULE order rather than strict execution order
|
|
298
|
+
* (the zero-data-rows check can only run once the per-row loop below has
|
|
299
|
+
* finished, so it fires textually after checks 4-6 even though it is
|
|
300
|
+
* conceptually "does the table hold any rows at all"):
|
|
301
|
+
* 1. the file cannot be read.
|
|
302
|
+
* 2. no header row matching all seven cells is found.
|
|
303
|
+
* 3. the header and separator are present but zero data rows follow.
|
|
304
|
+
* 4. a candidate row does not split into exactly seven cells.
|
|
305
|
+
* 5. a candidate row's Start or End cell is not the `$XXXX` shape
|
|
306
|
+
* `hex4()` writes.
|
|
307
|
+
* 6. a candidate row's End is below its Start.
|
|
308
|
+
* 7. two rows overlap, or are not strictly ascending by Start.
|
|
309
|
+
* 8. the accepted rows do not begin at `$0000`, leave a gap, or stop
|
|
310
|
+
* below `$FFFF`.
|
|
311
|
+
*
|
|
312
|
+
* Every refusal names the path and, for a single-row problem, the 1-based
|
|
313
|
+
* line number -- never the row's own cell text. See the module header's
|
|
314
|
+
* "WHAT NOT TO DO" section for the full permitted-fact vocabulary, and for
|
|
315
|
+
* why checks 7 and 8 are the ONLY two that mirror `renderLedger()`'s own
|
|
316
|
+
* emit-time preconditions: they are the ADDRESS-shaped ones, and the two
|
|
317
|
+
* CONTENT-shaped ones (UNKNOWN-with-empty-reason, ORIGINAL-agreeing-below-2)
|
|
318
|
+
* are deliberately never re-asserted here.
|
|
319
|
+
*/
|
|
320
|
+
export function readProvenanceLedger(ledgerPath: string): ProvenanceLedger {
|
|
321
|
+
let raw: string;
|
|
322
|
+
try {
|
|
323
|
+
raw = readFileSync(ledgerPath, "utf8");
|
|
324
|
+
} catch (err) {
|
|
325
|
+
throw new ProvenanceLedgerError(
|
|
326
|
+
`anno-provenance-ledger: could not read the ledger at "${ledgerPath}" (${err instanceof Error ? err.message : String(err)}) -- ` +
|
|
327
|
+
`refusing to annotate without it. ${LEDGER_REMEDY}.`,
|
|
328
|
+
{ path: ledgerPath },
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const lines = raw.split(/\r?\n/);
|
|
333
|
+
|
|
334
|
+
let headerLineIndex = -1;
|
|
335
|
+
for (let i = 0; i < lines.length; i++) {
|
|
336
|
+
const cells = splitTableRow(lines[i]!, ledgerPath, i + 1);
|
|
337
|
+
if (
|
|
338
|
+
cells !== undefined &&
|
|
339
|
+
cells.length === PROVENANCE_LEDGER_HEADER_CELLS.length &&
|
|
340
|
+
cells.every((cell, idx) => cell === PROVENANCE_LEDGER_HEADER_CELLS[idx])
|
|
341
|
+
) {
|
|
342
|
+
headerLineIndex = i;
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (headerLineIndex === -1) {
|
|
347
|
+
throw new ProvenanceLedgerError(
|
|
348
|
+
`anno-provenance-ledger: no header row matching the seven expected columns (${PROVENANCE_LEDGER_HEADER_CELLS.join(", ")}) ` +
|
|
349
|
+
`was found in "${ledgerPath}" -- refusing to guess which row starts the table. ${LEDGER_REMEDY}.`,
|
|
350
|
+
{ path: ledgerPath },
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const rows: ProvenanceLedgerRow[] = [];
|
|
355
|
+
/** 1-based source line number for `rows[k]`, kept parallel to `rows` so the
|
|
356
|
+
* ordering/overlap check below can name both offending rows without
|
|
357
|
+
* re-scanning `lines` -- an internal bookkeeping array, never exposed on
|
|
358
|
+
* `ProvenanceLedgerRow` itself. */
|
|
359
|
+
const rowLineNumbers: number[] = [];
|
|
360
|
+
// headerLineIndex + 1 is the `|---|` separator -- skipped, not
|
|
361
|
+
// re-validated. Parsing starts one line further on.
|
|
362
|
+
for (let i = headerLineIndex + 2; i < lines.length; i++) {
|
|
363
|
+
const line = lines[i]!;
|
|
364
|
+
const cells = splitTableRow(line, ledgerPath, i + 1);
|
|
365
|
+
if (cells === undefined) break; // first line that is not a table row at all ends the generated tier
|
|
366
|
+
|
|
367
|
+
if (cells.length !== PROVENANCE_LEDGER_HEADER_CELLS.length) {
|
|
368
|
+
throw new ProvenanceLedgerError(
|
|
369
|
+
`anno-provenance-ledger: row ${i + 1} of "${ledgerPath}" splits into ${cells.length} pipe-delimited cells, not the ` +
|
|
370
|
+
`${PROVENANCE_LEDGER_HEADER_CELLS.length} renderLedger() always writes -- refusing to parse a row whose own shape ` +
|
|
371
|
+
`is wrong. ${LEDGER_REMEDY}.`,
|
|
372
|
+
{ path: ledgerPath, lineNumber: i + 1 },
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const [startCell, endCell, kind, verdict, confidence, agreeingReleases, evidence] = cells as [
|
|
377
|
+
string,
|
|
378
|
+
string,
|
|
379
|
+
string,
|
|
380
|
+
string,
|
|
381
|
+
string,
|
|
382
|
+
string,
|
|
383
|
+
string,
|
|
384
|
+
];
|
|
385
|
+
const start = parseHexAddress(startCell);
|
|
386
|
+
if (start === undefined) refuseBadAddress(ledgerPath, i + 1, "Start");
|
|
387
|
+
const end = parseHexAddress(endCell);
|
|
388
|
+
if (end === undefined) refuseBadAddress(ledgerPath, i + 1, "End");
|
|
389
|
+
|
|
390
|
+
if (end < start) {
|
|
391
|
+
throw new ProvenanceLedgerError(
|
|
392
|
+
`anno-provenance-ledger: row ${i + 1} of "${ledgerPath}" has End ${hex4(end)} below its Start ${hex4(start)} -- ` +
|
|
393
|
+
`refusing to accept a row whose own span is inverted. ${LEDGER_REMEDY}.`,
|
|
394
|
+
{ path: ledgerPath, lineNumber: i + 1 },
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
rows.push({ start, endInclusive: end, kind, verdict, confidence, agreeingReleases, evidence });
|
|
399
|
+
rowLineNumbers.push(i + 1);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (rows.length === 0) {
|
|
403
|
+
throw new ProvenanceLedgerError(
|
|
404
|
+
`anno-provenance-ledger: the header and separator in "${ledgerPath}" are present but zero data rows follow -- ` +
|
|
405
|
+
`refusing to treat a table that parsed to no rows the same as no ledger having been asked for at all. ${LEDGER_REMEDY}.`,
|
|
406
|
+
{ path: ledgerPath },
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// CHECK 7 -- ADDRESS-SHAPED, mirrors `renderLedger()`'s own "gap or
|
|
411
|
+
// overlap" precondition. Two rows overlap, or the rows are not strictly
|
|
412
|
+
// ascending by Start, iff a later row's Start does not come strictly after
|
|
413
|
+
// the earlier row's own Start AND End. `rows` is in FILE order (not
|
|
414
|
+
// re-sorted), so this also catches a ledger whose rows were reordered by a
|
|
415
|
+
// hand edit, not only a genuine address collision.
|
|
416
|
+
for (let idx = 1; idx < rows.length; idx++) {
|
|
417
|
+
const prev = rows[idx - 1]!;
|
|
418
|
+
const cur = rows[idx]!;
|
|
419
|
+
if (cur.start <= prev.start || cur.start <= prev.endInclusive) {
|
|
420
|
+
throw new ProvenanceLedgerError(
|
|
421
|
+
`anno-provenance-ledger: rows ${rowLineNumbers[idx - 1]} and ${rowLineNumbers[idx]} of "${ledgerPath}" are not ` +
|
|
422
|
+
`strictly ascending and disjoint by Start -- row ${rowLineNumbers[idx - 1]} spans ${hex4(prev.start)}..${hex4(prev.endInclusive)} ` +
|
|
423
|
+
`and row ${rowLineNumbers[idx]} spans ${hex4(cur.start)}..${hex4(cur.endInclusive)} -- refusing to join a store range against rows ` +
|
|
424
|
+
`this module cannot order unambiguously. ${LEDGER_REMEDY}.`,
|
|
425
|
+
{ path: ledgerPath, lineNumber: rowLineNumbers[idx] },
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// CHECK 8 -- ADDRESS-SHAPED, mirrors `renderLedger()`'s own two coverage
|
|
431
|
+
// preconditions (starts at $0000 with no gap; reaches $FFFF). Run only
|
|
432
|
+
// after check 7 has already established the rows are disjoint and
|
|
433
|
+
// ascending, so `expected` is a running "next address a row must start at"
|
|
434
|
+
// cursor rather than a re-derivation of the ordering check above.
|
|
435
|
+
let expected = 0;
|
|
436
|
+
for (const row of rows) {
|
|
437
|
+
if (row.start !== expected) {
|
|
438
|
+
throw new ProvenanceLedgerError(
|
|
439
|
+
`anno-provenance-ledger: "${ledgerPath}"'s accepted rows do not cover $0000..$FFFF -- address ${hex4(expected)} is not ` +
|
|
440
|
+
`covered by any row -- refusing to join a store range against a table that does not describe the whole address space. ` +
|
|
441
|
+
`${LEDGER_REMEDY}.`,
|
|
442
|
+
{ path: ledgerPath },
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
expected = row.endInclusive + 1;
|
|
446
|
+
}
|
|
447
|
+
if (expected !== 0x10000) {
|
|
448
|
+
throw new ProvenanceLedgerError(
|
|
449
|
+
`anno-provenance-ledger: "${ledgerPath}"'s accepted rows stop at ${hex4(expected - 1)}, not $FFFF -- refusing to join a ` +
|
|
450
|
+
`store range against a table that does not describe the whole address space. ${LEDGER_REMEDY}.`,
|
|
451
|
+
{ path: ledgerPath },
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return { path: ledgerPath, rows };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Every row of `ledger` overlapping `[start, endInclusive]`, both ends
|
|
460
|
+
* inclusive, in ascending `start` order.
|
|
461
|
+
*
|
|
462
|
+
* Two ranges overlap iff each starts at or before the other ends -- the same
|
|
463
|
+
* `>=` reasoning `addScope()` in `anno-store.ts` already documents for
|
|
464
|
+
* scopes, so a row ending exactly at `start - 1` does NOT overlap. Returns
|
|
465
|
+
* EVERY match and never picks one: multiplicity is the caller's to record,
|
|
466
|
+
* never this function's to resolve. Takes no threshold, no filter predicate
|
|
467
|
+
* and no default -- the join is address-only, and no Verdict, Confidence or
|
|
468
|
+
* Kind value is ever read here.
|
|
469
|
+
*/
|
|
470
|
+
export function provenanceForRange(ledger: ProvenanceLedger, start: number, endInclusive: number): readonly ProvenanceLedgerRow[] {
|
|
471
|
+
return ledger.rows.filter((row) => row.start <= endInclusive && row.endInclusive >= start).sort((a, b) => a.start - b.start);
|
|
472
|
+
}
|
package/anno-regbits-gen.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
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
|
-
// anno-enum-gen.ts decodes register values against
|
|
4
|
+
// anno-enum-gen.ts decodes register values against.
|
|
5
5
|
//
|
|
6
|
-
// WHY THIS EXISTS
|
|
6
|
+
// WHY THIS EXISTS: neither register the phase's own pinned criterion-3
|
|
7
7
|
// target needs ($D011) nor the registers a real game writes to constantly
|
|
8
8
|
// ($D015/$D017/$D01A-$D01D) can be named from memmap.json's own `bits` prose
|
|
9
9
|
// alone -- some of that prose is OCR-damaged ("O = Blank" uses a letter O for
|
|
@@ -54,11 +54,11 @@ 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
56
|
* its only consumer for this purpose (per this plan's key_links entry).
|
|
57
|
-
* 2026-08-22
|
|
57
|
+
* 2026-08-22: the skills tree moved from `.claude/skills/`
|
|
58
58
|
* (two levels up from `.claude/mcp/vice`) to `src/skills/` (three levels up,
|
|
59
59
|
* since `src/` sits directly under the repo root rather than under `.claude/`).
|
|
60
|
-
* This literal was not in
|
|
61
|
-
* test
|
|
60
|
+
* This literal was not in the original enumerated consumer list, and a
|
|
61
|
+
* test failure caught the gap live. */
|
|
62
62
|
const MEMMAP_PATH = join(HERE, "..", "..", "..", "src", "skills", "c64-memory-mapping", "memmap.json");
|
|
63
63
|
|
|
64
64
|
/** Where the generated, committed artifact lives -- always a sibling of this
|
|
@@ -163,14 +163,14 @@ export interface RegbitsRegisterOverride {
|
|
|
163
163
|
* entries by their `bit` string. */
|
|
164
164
|
fields?: readonly RegbitsFieldOverride[];
|
|
165
165
|
/** A COMPLETE field list for an address memmap.json's `io` parser produced
|
|
166
|
-
* no `bits` entry for at all
|
|
166
|
+
* no `bits` entry for at all -- used only when no
|
|
167
167
|
* memmap entry exists for this address, never to replace one that does. */
|
|
168
168
|
synthetic?: readonly RegBitsField[];
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
/** Builds the 8 independent "bit N = sprite N" flag fields the five VIC
|
|
172
172
|
* sprite-plane registers ($D015/$D017/$D01B/$D01C/$D01D) all share the same
|
|
173
|
-
* shape for
|
|
173
|
+
* shape for -- each bit is silent (empty token) when
|
|
174
174
|
* clear and names the specific sprite when set, so a typical enum (most
|
|
175
175
|
* sprites off, one or two on) renders as a short, readable name instead of
|
|
176
176
|
* naming all eight sprites' negative state every time.
|
|
@@ -236,15 +236,15 @@ export const OVERRIDES: readonly RegbitsRegisterOverride[] = [
|
|
|
236
236
|
label: "CIA Interrupt Control Register (Read NMIs/Write Mask)",
|
|
237
237
|
},
|
|
238
238
|
{
|
|
239
|
-
// WHY: memmap.json's `io` parser produced no `bits` entry at all for this address
|
|
240
|
-
//
|
|
239
|
+
// WHY: memmap.json's `io` parser produced no `bits` entry at all for this address,
|
|
240
|
+
// even though a real game writes to it constantly -- sprite enable, one flag bit
|
|
241
241
|
// per sprite, is trivially regular and does not need memmap's own prose to describe correctly.
|
|
242
242
|
address: 53269, // $D015 -- Sprite Enable
|
|
243
243
|
label: "Sprite Enable",
|
|
244
244
|
synthetic: spriteBitFields("EN"),
|
|
245
245
|
},
|
|
246
246
|
{
|
|
247
|
-
// WHY: same
|
|
247
|
+
// WHY: same missing-`bits`-entry gap as $D015 -- Sprite Y-Expand, one flag bit per sprite.
|
|
248
248
|
address: 53271, // $D017 -- Sprite Y-Expand
|
|
249
249
|
label: "Sprite Y-Expand",
|
|
250
250
|
synthetic: spriteBitFields("YEXP"),
|
|
@@ -264,20 +264,20 @@ export const OVERRIDES: readonly RegbitsRegisterOverride[] = [
|
|
|
264
264
|
],
|
|
265
265
|
},
|
|
266
266
|
{
|
|
267
|
-
// WHY: same
|
|
267
|
+
// WHY: same missing-`bits`-entry gap as $D015 -- Sprite Priority (behind/in-front of background), one flag bit
|
|
268
268
|
// per sprite.
|
|
269
269
|
address: 53275, // $D01B -- Sprite Data Priority
|
|
270
270
|
label: "Sprite Data Priority",
|
|
271
271
|
synthetic: spriteBitFields("BG"),
|
|
272
272
|
},
|
|
273
273
|
{
|
|
274
|
-
// WHY: same
|
|
274
|
+
// WHY: same missing-`bits`-entry gap as $D015 -- Sprite Multicolor, one flag bit per sprite.
|
|
275
275
|
address: 53276, // $D01C -- Sprite Multicolor
|
|
276
276
|
label: "Sprite Multicolor",
|
|
277
277
|
synthetic: spriteBitFields("MC"),
|
|
278
278
|
},
|
|
279
279
|
{
|
|
280
|
-
// WHY: same
|
|
280
|
+
// WHY: same missing-`bits`-entry gap as $D015 -- Sprite X-Expand, one flag bit per sprite.
|
|
281
281
|
address: 53277, // $D01D -- Sprite X-Expand
|
|
282
282
|
label: "Sprite X-Expand",
|
|
283
283
|
synthetic: spriteBitFields("XEXP"),
|