@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
package/anno-join.ts
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// anno-join.ts
|
|
3
|
+
//
|
|
4
|
+
// The mechanical join between stored
|
|
5
|
+
// cross-references and `memmap.json`, with no agent call, no queue walk and
|
|
6
|
+
// no skill invocation anywhere in the loop.
|
|
7
|
+
//
|
|
8
|
+
// THIS MODULE RECEIVES AN ALREADY-OPEN STORE HANDLE, exactly like
|
|
9
|
+
// `anno-import.ts` -- there is no second store session anywhere in this
|
|
10
|
+
// file. It never names `node:sqlite`, never calls `openStore()`/`closeStore()`
|
|
11
|
+
// itself, and never imports `hostpath.ts`/`containerpath.ts`: the image range
|
|
12
|
+
// it needs arrives as plain numbers (`imageOrigin`, `imageByteLength`) that
|
|
13
|
+
// the caller has already derived from a loaded image, mirroring
|
|
14
|
+
// `dxa-blocks.ts`'s own header posture of reading only already-fetched facts
|
|
15
|
+
// a caller passes in.
|
|
16
|
+
//
|
|
17
|
+
// WHAT THIS IS THE ONE AUTHORITATIVE PLACE FOR: reading `listXrefs()`'s
|
|
18
|
+
// distinct target addresses, classifying each one (inside the loaded image,
|
|
19
|
+
// no `memmap.json` entry, or annotated) and writing the resulting comment
|
|
20
|
+
// through `setComment()`. This module's own criterion -- no agent, no queue
|
|
21
|
+
// walk, no skill invocation -- is checked STRUCTURALLY over this module's own
|
|
22
|
+
// source in `anno-join.test.ts`, not merely asserted here in
|
|
23
|
+
// prose.
|
|
24
|
+
//
|
|
25
|
+
// WHAT NOT TO DO:
|
|
26
|
+
// - Never import anything under `src/skills/`. This function's entire
|
|
27
|
+
// point is that annotating an address costs one mechanical call, not an
|
|
28
|
+
// agent turn.
|
|
29
|
+
// - Never spawn a child process from this module.
|
|
30
|
+
// - Never carry a bank value forward past a point where two paths
|
|
31
|
+
// disagree, and never default an unresolved bank state to the power-on
|
|
32
|
+
// value (T-37-26). A reaching-values set of size other than one, or one
|
|
33
|
+
// that decodes to more than one region, DECLINES -- see the bank-state
|
|
34
|
+
// block below.
|
|
35
|
+
//
|
|
36
|
+
// `runMemmapJoin()` gains an
|
|
37
|
+
// OPTIONAL `constWrites` argument. When `undefined` (every pre-37-06 call
|
|
38
|
+
// site, and every existing test in this file), the bank-state machinery
|
|
39
|
+
// below is a complete no-op and every address resolves EXACTLY as it did
|
|
40
|
+
// before this plan -- the candidate-constraint argument must not change any
|
|
41
|
+
// unconstrained selection's answer. Only when a caller explicitly supplies
|
|
42
|
+
// an array (even an empty one) does the reaching-values/decline logic
|
|
43
|
+
// activate for addresses inside `BANK_CONDITIONAL_RANGES`. The
|
|
44
|
+
// reaching-values computation is deliberately conservative -- it is NOT a
|
|
45
|
+
// dataflow analysis. It uses only what the export gives: the recovered
|
|
46
|
+
// constant stores to the processor port (each with its own address), and
|
|
47
|
+
// the cross-reference graph the importer already stored via `putXref()`.
|
|
48
|
+
// The set of recovered values reaching a given address is the set of
|
|
49
|
+
// `constWrites` whose OWN store address can reach that address, walked
|
|
50
|
+
// forward over that same graph. Where the graph does not connect a store to
|
|
51
|
+
// the address at all, that value is simply not in the reaching set --
|
|
52
|
+
// the decline-on-empty-or-disagreement rule (below) is what turns
|
|
53
|
+
// "nothing reaches this point" into a stated absence rather than a silent
|
|
54
|
+
// default.
|
|
55
|
+
|
|
56
|
+
import { listXrefs, setComment, setDataType } from "./anno-store.ts";
|
|
57
|
+
import type { AnnoStoreHandle } from "./anno-store.ts";
|
|
58
|
+
import {
|
|
59
|
+
decodeBankState,
|
|
60
|
+
isBankConditionalAddress,
|
|
61
|
+
regionAdmitsEntry,
|
|
62
|
+
resolveBankedRegion,
|
|
63
|
+
} from "./anno-bank.ts";
|
|
64
|
+
import type { BankedRegion } from "./anno-bank.ts";
|
|
65
|
+
import type { ConstWriteFact } from "./anno-import.ts";
|
|
66
|
+
import type { ContradictedComment, SplitTableReinterpretation, XrefRow } from "./anno-types.ts";
|
|
67
|
+
import { loadMemmap, memmapDigest, PROVENANCE_TOKEN_PREFIX, selectMemmapEntry } from "./memmap-lookup.ts";
|
|
68
|
+
import type { MemmapEntry, MemmapSelection } from "./memmap-lookup.ts";
|
|
69
|
+
// The graphics write-back. deriveGraphicsRanges()
|
|
70
|
+
// is structurally typed against ConstWriteFact -- GraphicsConstWriteFact's own
|
|
71
|
+
// shape is identical ({storeAddress, targetAddress, value}) -- so THIS module's
|
|
72
|
+
// own constWrites argument, already threaded for the bank-state block, is
|
|
73
|
+
// handed straight through with no translation layer.
|
|
74
|
+
import { deriveGraphicsRanges } from "./anno-graphics.ts";
|
|
75
|
+
import type { GraphicsMap } from "./anno-graphics.ts";
|
|
76
|
+
|
|
77
|
+
/** The axis-qualified provenance marker an annotated bank-conditional
|
|
78
|
+
* comment carries, ALWAYS before `PROVENANCE_TOKEN_PREFIX`'s own digest
|
|
79
|
+
* token (which stays last). Names the axis ("processor-port")
|
|
80
|
+
* explicitly, so a later phase adding the VIC banking axis is additive
|
|
81
|
+
* rather than ambiguous about which axis a given token names. */
|
|
82
|
+
const BANK_PROVENANCE_PREFIX = "[processor-port:";
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The reaching-values computation's own result shape: exactly one
|
|
86
|
+
* recovered value reaches the address; several do (an array -- length 0
|
|
87
|
+
* means none reach at all, length 2+ means genuine disagreement-or-agreement
|
|
88
|
+
* to resolve); or the computation cannot decide at all. Nothing in this
|
|
89
|
+
* project's current data model (a plain directed graph of already-resolved
|
|
90
|
+
* `XrefRow`s) can currently produce `"unknown"` -- there is no signal here
|
|
91
|
+
* for a dropped or unresolved reference (a stated limit of this computation) -- but
|
|
92
|
+
* the shape is kept complete for a future importer that DOES emit such a
|
|
93
|
+
* signal, rather than silently folding that case into "empty".
|
|
94
|
+
*/
|
|
95
|
+
type ReachingValues = { kind: "one"; value: number } | { kind: "several"; values: number[] } | { kind: "unknown" };
|
|
96
|
+
|
|
97
|
+
/** Builds a forward adjacency map from `xrefs`, ONCE per join run, reused for
|
|
98
|
+
* every address's own reachability walk below. */
|
|
99
|
+
function buildAdjacency(xrefs: readonly XrefRow[]): Map<number, number[]> {
|
|
100
|
+
const adjacency = new Map<number, number[]>();
|
|
101
|
+
for (const xref of xrefs) {
|
|
102
|
+
const list = adjacency.get(xref.fromAddress);
|
|
103
|
+
if (list) list.push(xref.toAddress);
|
|
104
|
+
else adjacency.set(xref.fromAddress, [xref.toAddress]);
|
|
105
|
+
}
|
|
106
|
+
return adjacency;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Plain forward reachability (DFS, visited-set guarded against cycles):
|
|
110
|
+
* can `from` reach `target` by following zero or more `adjacency` edges? */
|
|
111
|
+
function canReach(from: number, target: number, adjacency: Map<number, number[]>): boolean {
|
|
112
|
+
if (from === target) return true;
|
|
113
|
+
const visited = new Set<number>([from]);
|
|
114
|
+
const stack = [from];
|
|
115
|
+
while (stack.length > 0) {
|
|
116
|
+
const current = stack.pop()!;
|
|
117
|
+
for (const next of adjacency.get(current) ?? []) {
|
|
118
|
+
if (next === target) return true;
|
|
119
|
+
if (!visited.has(next)) {
|
|
120
|
+
visited.add(next);
|
|
121
|
+
stack.push(next);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The set of recovered processor-port values whose OWN store
|
|
129
|
+
* address can reach `targetAddress`, over `adjacency`. Never a dataflow
|
|
130
|
+
* analysis -- purely "does the stored cross-reference graph connect this
|
|
131
|
+
* store to this address". */
|
|
132
|
+
function computeReachingValues(
|
|
133
|
+
targetAddress: number,
|
|
134
|
+
constWrites: readonly ConstWriteFact[],
|
|
135
|
+
adjacency: Map<number, number[]>,
|
|
136
|
+
): ReachingValues {
|
|
137
|
+
const reaching: number[] = [];
|
|
138
|
+
for (const write of constWrites) {
|
|
139
|
+
if (canReach(write.storeAddress, targetAddress, adjacency)) reaching.push(write.value);
|
|
140
|
+
}
|
|
141
|
+
if (reaching.length === 1) return { kind: "one", value: reaching[0]! };
|
|
142
|
+
return { kind: "several", values: reaching };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Raised when `runMemmapJoin()`'s own inputs cannot support a join at all --
|
|
146
|
+
* currently only the zero-byte-image refusal below. Follows `anno-import.ts`'s
|
|
147
|
+
* `AnnoImportError` construction idiom (a bare `Error` subclass, not
|
|
148
|
+
* `AnnoStoreError`, since this module never touches the store's own
|
|
149
|
+
* persistence). */
|
|
150
|
+
export class AnnoJoinError extends Error {
|
|
151
|
+
constructor(message: string) {
|
|
152
|
+
super(message);
|
|
153
|
+
this.name = "AnnoJoinError";
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** What one `runMemmapJoin()` call reports. `addressesConsidered` is always
|
|
158
|
+
* the sum of the next four fields. The three `graphics*` fields are always
|
|
159
|
+
* present and `0` when `constWrites` is omitted or when
|
|
160
|
+
* the selected map derives zero ranges -- never absent, so a caller reads
|
|
161
|
+
* them unconditionally instead of guarding on them, mirroring
|
|
162
|
+
* `SetDataTypeResult`'s own "always present, often empty" convention for
|
|
163
|
+
* `contradictedComments`/`reinterpretedSplitTables`. */
|
|
164
|
+
export interface JoinCounts {
|
|
165
|
+
addressesConsidered: number;
|
|
166
|
+
annotated: number;
|
|
167
|
+
skippedInImage: number;
|
|
168
|
+
skippedNoMapEntry: number;
|
|
169
|
+
declined: number;
|
|
170
|
+
commentsChanged: number;
|
|
171
|
+
graphicsRangesWritten: number;
|
|
172
|
+
graphicsContradictedComments: number;
|
|
173
|
+
graphicsReinterpretedSplitTables: number;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* What the graphics write-back reports, in full --
|
|
178
|
+
* `JoinCounts`'s own `graphics*` fields are the COUNTS of these same
|
|
179
|
+
* `contradictedComments`/`reinterpretedSplitTables` arrays; this record
|
|
180
|
+
* carries the disclosures themselves so neither is dropped (must_haves.truths:
|
|
181
|
+
* "the join's returned counts include the contradicted-comment and
|
|
182
|
+
* fragmented-split-table disclosures the range write reported; neither is
|
|
183
|
+
* dropped"). `mapIndex` records WHICH of `deriveGraphicsRanges()`'s several
|
|
184
|
+
* maps was written -- this project's own rule (several valid combinations are
|
|
185
|
+
* several maps, never one merged map) means writing more than one would
|
|
186
|
+
* write mutually-contradicting ranges into the SAME store, so exactly one is
|
|
187
|
+
* ever written and this field is the record of which. */
|
|
188
|
+
export interface GraphicsWriteBack {
|
|
189
|
+
mapIndex: number;
|
|
190
|
+
rangesWritten: number;
|
|
191
|
+
contradictedComments: readonly ContradictedComment[];
|
|
192
|
+
reinterpretedSplitTables: readonly SplitTableReinterpretation[];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** One address's own outcome. A skip or a decline always carries a non-empty
|
|
196
|
+
* `reason` naming WHY in the join's own vocabulary; an annotation carries the
|
|
197
|
+
* `label` it wrote. */
|
|
198
|
+
export interface JoinDecision {
|
|
199
|
+
address: number;
|
|
200
|
+
outcome: "annotated" | "skipped-in-image" | "skipped-no-entry" | "declined";
|
|
201
|
+
reason?: string;
|
|
202
|
+
label?: string;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export interface RunMemmapJoinArgs {
|
|
206
|
+
imageOrigin: number;
|
|
207
|
+
imageByteLength: number;
|
|
208
|
+
/** The recovered `$01` const-write facts this run
|
|
209
|
+
* has evidence for, typically `parseConstWrites()`'s own output over one
|
|
210
|
+
* imported export. `undefined` (every pre-37-06 call site) means "this run
|
|
211
|
+
* carries no bank-state evidence at all" -- the bank-state machinery is a
|
|
212
|
+
* complete no-op and every address resolves EXACTLY as before this plan.
|
|
213
|
+
* An explicit array (even `[]`) activates it for addresses inside
|
|
214
|
+
* `BANK_CONDITIONAL_RANGES`. */
|
|
215
|
+
constWrites?: readonly ConstWriteFact[];
|
|
216
|
+
/** Which of `deriveGraphicsRanges()`'s
|
|
217
|
+
* several maps to write back, when `constWrites` derives more than one
|
|
218
|
+
* distinct register-value combination. Defaults to `0`. Consulted ONLY
|
|
219
|
+
* when `constWrites` is supplied AT ALL (the SAME gate that activates the
|
|
220
|
+
* bank-state block above) -- omitting `constWrites` entirely skips the
|
|
221
|
+
* graphics write-back completely, exactly like the bank-state block. Out
|
|
222
|
+
* of range for the derived map count refuses BY NAME (`AnnoJoinError`)
|
|
223
|
+
* rather than silently clamping or picking a default. */
|
|
224
|
+
graphicsMapIndex?: number;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Joins every DISTINCT `toAddress` `listXrefs()` holds against `memmap.json`,
|
|
229
|
+
* in ascending address order. An address inside the caller's own loaded
|
|
230
|
+
* image range is a program address, not a hardware/memory-map feature, and
|
|
231
|
+
* is skipped WITHOUT a `memmap.json` lookup -- the membership test runs
|
|
232
|
+
* BEFORE `selectEntry()` is called at all, so the guard is a
|
|
233
|
+
* control-flow fact rather than a result-filtering one: this file's own
|
|
234
|
+
* contract says an
|
|
235
|
+
* in-image address is "never looked up in memmap.json", a claim about what
|
|
236
|
+
* runs, not merely about what the caller sees back. An address with no
|
|
237
|
+
* containing `memmap.json` entry is skipped for that reason instead;
|
|
238
|
+
* everything else is annotated with the selected entry's label via
|
|
239
|
+
* `setComment()`. Running this twice over an unchanged store re-classifies
|
|
240
|
+
* every address identically and reports `commentsChanged: 0` on the second
|
|
241
|
+
* run, because `setComment()` itself reports `changed: false` for a
|
|
242
|
+
* byte-identical repeat.
|
|
243
|
+
*
|
|
244
|
+
* `selectEntry` defaults to the real `selectMemmapEntry` and exists as an
|
|
245
|
+
* injection point for exactly one reason: it is the seam that makes "never
|
|
246
|
+
* looked up" checkable at all. Without it, the only available assertion is
|
|
247
|
+
* on the RESULT, and a result-filtering implementation (compute the
|
|
248
|
+
* selection, then discard it for an in-image address) would pass that
|
|
249
|
+
* assertion while still violating the requirement. A test passes a counting
|
|
250
|
+
* spy in its place and asserts zero calls for an all-in-image store.
|
|
251
|
+
*/
|
|
252
|
+
export function runMemmapJoin(
|
|
253
|
+
handle: AnnoStoreHandle,
|
|
254
|
+
args: RunMemmapJoinArgs,
|
|
255
|
+
entries: readonly MemmapEntry[] = loadMemmap(),
|
|
256
|
+
selectEntry: (address: number, entries: readonly MemmapEntry[]) => MemmapSelection | undefined = selectMemmapEntry,
|
|
257
|
+
): { counts: JoinCounts; decisions: JoinDecision[]; graphics?: GraphicsWriteBack } {
|
|
258
|
+
if (args.imageByteLength === 0) {
|
|
259
|
+
throw new AnnoJoinError(
|
|
260
|
+
"runMemmapJoin refused: the image's own body length is 0 -- an image with no bytes has no range, and treating " +
|
|
261
|
+
"it as a zero-width range at its own origin would silently make one address (the origin) read as in-image.",
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Computed ONCE per join run and reused for every annotated row,
|
|
266
|
+
// never recomputed per row -- two comments written in the same run are
|
|
267
|
+
// therefore GUARANTEED to carry byte-identical tokens, not merely likely
|
|
268
|
+
// to (the file cannot change mid-run, but a per-row recompute would still
|
|
269
|
+
// be wasted work re-reading and re-hashing the same bytes for nothing).
|
|
270
|
+
const digest = memmapDigest();
|
|
271
|
+
|
|
272
|
+
const xrefs = listXrefs(handle);
|
|
273
|
+
const targets = [...new Set(xrefs.map((xref) => xref.toAddress))].sort((a, b) => a - b);
|
|
274
|
+
|
|
275
|
+
// Built ONCE per run, over the SAME xref graph the unconstrained
|
|
276
|
+
// path already reads via `listXrefs()` above -- reused for every address's
|
|
277
|
+
// own reachability walk below. `undefined` `args.constWrites` means the
|
|
278
|
+
// bank-state block is never entered at all, so this adjacency map is built
|
|
279
|
+
// but simply never consulted -- negligible cost, and keeps the "no
|
|
280
|
+
// constWrites -> no-op" contract a single conditional rather than two
|
|
281
|
+
// divergent code paths.
|
|
282
|
+
const bankAdjacency = buildAdjacency(xrefs);
|
|
283
|
+
|
|
284
|
+
// The inclusive image range, computed ONCE from the LoadedImage's own body
|
|
285
|
+
// bytes -- never from `totalBytes` (the file's own byte count,
|
|
286
|
+
// which on the .prg route includes the two-byte load-address header and
|
|
287
|
+
// would shift this whole range by two bytes) and never a caller-supplied
|
|
288
|
+
// number pair that could silently widen or narrow the program's own
|
|
289
|
+
// extent.
|
|
290
|
+
const imageStart = args.imageOrigin;
|
|
291
|
+
const imageEnd = args.imageOrigin + args.imageByteLength - 1;
|
|
292
|
+
|
|
293
|
+
const decisions: JoinDecision[] = [];
|
|
294
|
+
let annotated = 0;
|
|
295
|
+
let skippedInImage = 0;
|
|
296
|
+
let skippedNoMapEntry = 0;
|
|
297
|
+
let declined = 0;
|
|
298
|
+
let commentsChanged = 0;
|
|
299
|
+
|
|
300
|
+
for (const address of targets) {
|
|
301
|
+
// THE GUARD: one early-return, before selectEntry() is ever called. A
|
|
302
|
+
// single textual deletion of this block removes it cleanly -- that
|
|
303
|
+
// deletion is this file's own observed-red control.
|
|
304
|
+
if (address >= imageStart && address <= imageEnd) {
|
|
305
|
+
skippedInImage += 1;
|
|
306
|
+
decisions.push({
|
|
307
|
+
address,
|
|
308
|
+
outcome: "skipped-in-image",
|
|
309
|
+
reason:
|
|
310
|
+
`address $${address.toString(16)} lies inside the loaded image's own range ($${imageStart.toString(16)}-` +
|
|
311
|
+
`$${imageEnd.toString(16)}) and is therefore a program address, never looked up in memmap.json`,
|
|
312
|
+
});
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// THE BANK-STATE BLOCK. Only
|
|
317
|
+
// entered when the caller supplied `constWrites` AT ALL (`undefined`
|
|
318
|
+
// skips this whole block, falling through to the unconstrained path
|
|
319
|
+
// below exactly as pre-37-06) AND the address is inside one of the
|
|
320
|
+
// three bank-conditional ranges -- outside them, bank state is
|
|
321
|
+
// irrelevant and the candidate set stays unconstrained regardless of
|
|
322
|
+
// `constWrites`.
|
|
323
|
+
if (args.constWrites !== undefined && isBankConditionalAddress(address)) {
|
|
324
|
+
const reaching = computeReachingValues(address, args.constWrites, bankAdjacency);
|
|
325
|
+
|
|
326
|
+
if (reaching.kind === "unknown") {
|
|
327
|
+
declined += 1;
|
|
328
|
+
decisions.push({
|
|
329
|
+
address,
|
|
330
|
+
outcome: "declined",
|
|
331
|
+
reason: `bank state at $${address.toString(16)} could not be determined -- the reaching-values computation could not decide`,
|
|
332
|
+
});
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Resolves ONE region (or refuses) for a single reaching value, applies
|
|
337
|
+
// it as a candidate constraint BEFORE selection runs (never a
|
|
338
|
+
// post-filter), and pushes the matching decision. Shared by
|
|
339
|
+
// both the single-value and the several-values-same-region branches
|
|
340
|
+
// below, so the annotate path is written exactly once.
|
|
341
|
+
const annotateUnderRegion = (region: Exclude<BankedRegion, "not_applicable">, bankNote: string): void => {
|
|
342
|
+
const constrained = entries.filter((entry) => regionAdmitsEntry(entry, region));
|
|
343
|
+
const selection = selectEntry(address, constrained);
|
|
344
|
+
if (selection === undefined) {
|
|
345
|
+
skippedNoMapEntry += 1;
|
|
346
|
+
decisions.push({
|
|
347
|
+
address,
|
|
348
|
+
outcome: "skipped-no-entry",
|
|
349
|
+
reason: `no memmap.json entry admits the resolved ${region} region for $${address.toString(16)}`,
|
|
350
|
+
});
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const commentText = `${selection.entry.label} ${BANK_PROVENANCE_PREFIX}${bankNote}] ${PROVENANCE_TOKEN_PREFIX}${digest}`;
|
|
354
|
+
const write = setComment(handle, { address, commentType: "line", text: commentText });
|
|
355
|
+
annotated += 1;
|
|
356
|
+
if (write.changed) commentsChanged += 1;
|
|
357
|
+
decisions.push({
|
|
358
|
+
address,
|
|
359
|
+
outcome: "annotated",
|
|
360
|
+
label: selection.entry.label,
|
|
361
|
+
...(reaching.kind === "several" ? { reason: `reached under differing processor-port values (${bankNote}) that all resolve to the same region (${region})` } : {}),
|
|
362
|
+
});
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
if (reaching.kind === "one") {
|
|
366
|
+
const region = resolveBankedRegion(address, decodeBankState(reaching.value));
|
|
367
|
+
annotateUnderRegion(region as Exclude<BankedRegion, "not_applicable">, `$${reaching.value.toString(16)}`);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// reaching.kind === "several"
|
|
372
|
+
if (reaching.values.length === 0) {
|
|
373
|
+
declined += 1;
|
|
374
|
+
decisions.push({
|
|
375
|
+
address,
|
|
376
|
+
outcome: "declined",
|
|
377
|
+
reason: `no recovered processor-port value reaches $${address.toString(16)} -- declining rather than defaulting to the power-on state`,
|
|
378
|
+
});
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const uniqueValues = [...new Set(reaching.values)].sort((a, b) => a - b);
|
|
383
|
+
const regionsByValue = uniqueValues.map((value) => resolveBankedRegion(address, decodeBankState(value)));
|
|
384
|
+
const uniqueRegions = new Set(regionsByValue);
|
|
385
|
+
if (uniqueRegions.size > 1) {
|
|
386
|
+
declined += 1;
|
|
387
|
+
const named = uniqueValues.map((value, i) => `$${value.toString(16)}(${regionsByValue[i]})`).join(", ");
|
|
388
|
+
decisions.push({
|
|
389
|
+
address,
|
|
390
|
+
outcome: "declined",
|
|
391
|
+
reason: `$${address.toString(16)} is reached under disagreeing processor-port values: ${named}`,
|
|
392
|
+
});
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const region = regionsByValue[0]! as Exclude<BankedRegion, "not_applicable">;
|
|
397
|
+
const valuesNote = uniqueValues.map((value) => `$${value.toString(16)}`).join(",");
|
|
398
|
+
annotateUnderRegion(region, valuesNote);
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const selection = selectEntry(address, entries);
|
|
403
|
+
if (selection === undefined) {
|
|
404
|
+
skippedNoMapEntry += 1;
|
|
405
|
+
decisions.push({
|
|
406
|
+
address,
|
|
407
|
+
outcome: "skipped-no-entry",
|
|
408
|
+
reason: "no memmap.json entry contains this address",
|
|
409
|
+
});
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// The full comment text: the selected entry's label, one space, the
|
|
414
|
+
// provenance prefix, then the full 64-character digest -- always LAST,
|
|
415
|
+
// never truncated. setComment() -> assertCommentText() refuses
|
|
416
|
+
// (never truncates) a text that overflows MAX_COMMENT_BYTES; that
|
|
417
|
+
// refusal is left to propagate here rather than being pre-checked and
|
|
418
|
+
// silently worked around, because a truncated provenance token would be
|
|
419
|
+
// a wrong answer that reports success.
|
|
420
|
+
const commentText = `${selection.entry.label} ${PROVENANCE_TOKEN_PREFIX}${digest}`;
|
|
421
|
+
const write = setComment(handle, { address, commentType: "line", text: commentText });
|
|
422
|
+
annotated += 1;
|
|
423
|
+
if (write.changed) commentsChanged += 1;
|
|
424
|
+
decisions.push({ address, outcome: "annotated", label: selection.entry.label });
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// THE GRAPHICS WRITE-BACK. Runs AFTER the main
|
|
428
|
+
// per-address loop above, as its own step -- graphics ranges are derived
|
|
429
|
+
// from register VALUES, never from the cross-reference targets the loop
|
|
430
|
+
// above walks, so there is no reason to interleave the two. Gated on the
|
|
431
|
+
// SAME `constWrites !== undefined` condition the bank-state block uses:
|
|
432
|
+
// omitting `constWrites` entirely is a complete no-op here too, exactly
|
|
433
|
+
// like the bank-state block above.
|
|
434
|
+
let graphics: GraphicsWriteBack | undefined;
|
|
435
|
+
if (args.constWrites !== undefined) {
|
|
436
|
+
const maps = deriveGraphicsRanges(args.constWrites);
|
|
437
|
+
const mapIndex = args.graphicsMapIndex ?? 0;
|
|
438
|
+
const selectedMap: GraphicsMap | undefined = maps[mapIndex];
|
|
439
|
+
if (selectedMap === undefined) {
|
|
440
|
+
throw new AnnoJoinError(
|
|
441
|
+
`runMemmapJoin refused: graphicsMapIndex ${mapIndex} is out of range -- deriveGraphicsRanges() produced ` +
|
|
442
|
+
`${maps.length} map(s) for this run's own constWrites`,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Write ONLY the selected map's own ranges -- never every map
|
|
447
|
+
// deriveGraphicsRanges() returned. Several distinct register-value
|
|
448
|
+
// combinations describe MUTUALLY CONTRADICTING layouts (the reason
|
|
449
|
+
// several maps exist at all); writing more than one into the
|
|
450
|
+
// same store would write ranges that disagree with each other by
|
|
451
|
+
// construction.
|
|
452
|
+
let rangesWritten = 0;
|
|
453
|
+
const contradictedComments: ContradictedComment[] = [];
|
|
454
|
+
const reinterpretedSplitTables: SplitTableReinterpretation[] = [];
|
|
455
|
+
for (const range of selectedMap.ranges) {
|
|
456
|
+
const write = setDataType(handle, { start: range.start, endInclusive: range.endInclusive, dataType: range.dataType });
|
|
457
|
+
rangesWritten += 1;
|
|
458
|
+
// Disclosures a successful range write can carry -- surfaced, never
|
|
459
|
+
// dropped (must_haves.truths): a comment whose recorded confidence now
|
|
460
|
+
// contradicts the type this write just assigned, or a split table this
|
|
461
|
+
// write fragmented.
|
|
462
|
+
contradictedComments.push(...write.contradictedComments);
|
|
463
|
+
reinterpretedSplitTables.push(...write.reinterpretedSplitTables);
|
|
464
|
+
}
|
|
465
|
+
graphics = { mapIndex, rangesWritten, contradictedComments, reinterpretedSplitTables };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const counts: JoinCounts = {
|
|
469
|
+
addressesConsidered: targets.length,
|
|
470
|
+
annotated,
|
|
471
|
+
skippedInImage,
|
|
472
|
+
skippedNoMapEntry,
|
|
473
|
+
declined,
|
|
474
|
+
commentsChanged,
|
|
475
|
+
graphicsRangesWritten: graphics?.rangesWritten ?? 0,
|
|
476
|
+
graphicsContradictedComments: graphics?.contradictedComments.length ?? 0,
|
|
477
|
+
graphicsReinterpretedSplitTables: graphics?.reinterpretedSplitTables.length ?? 0,
|
|
478
|
+
};
|
|
479
|
+
return graphics === undefined ? { counts, decisions } : { counts, decisions, graphics };
|
|
480
|
+
}
|
package/anno-memmap-render.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// anno-memmap-render.ts -- the ONE authoritative place in this repo that
|
|
3
3
|
// renders the human-readable Markdown memory map from this project's own
|
|
4
|
-
// annotation store
|
|
5
|
-
//
|
|
4
|
+
// annotation store plus a validated run-scoped provenance sidecar, using
|
|
5
|
+
// the reconciliation between those two sources this file implements.
|
|
6
6
|
//
|
|
7
|
-
// WHY THIS MODULE EXISTS
|
|
7
|
+
// WHY THIS MODULE EXISTS: 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
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
// `listRanges()`/`listLabels()`/`listComments()` readers, never from a
|
|
14
14
|
// human editing the output file directly.
|
|
15
15
|
//
|
|
16
|
-
// THE
|
|
16
|
+
// THE RECONCILIATION THIS FILE IMPLEMENTS, BETWEEN THE STORE AND THE
|
|
17
|
+
// RUN-SCOPED SIDECAR: run-scoped facts (the
|
|
17
18
|
// capture's SHA-256, `$01`, `$DD00`, the derived graphics chain, the video
|
|
18
19
|
// standard, the live vector pair, observed raster positions) are facts about
|
|
19
20
|
// a RUN, not about an address -- the store is address-keyed and has no shape
|
|
@@ -24,8 +25,8 @@
|
|
|
24
25
|
// `<placeholder>` for one.
|
|
25
26
|
//
|
|
26
27
|
// WHY THE LAYOUT IS EMBEDDED IN TYPESCRIPT RATHER THAN READ FROM A TEMPLATE
|
|
27
|
-
// FILE AT RUNTIME (the second decision this plan records):
|
|
28
|
-
// established that `.claude/mcp/vice/*.ts` exists as files on disk only
|
|
28
|
+
// FILE AT RUNTIME (the second decision this plan records): an earlier
|
|
29
|
+
// finding established that `.claude/mcp/vice/*.ts` exists as files on disk only
|
|
29
30
|
// under the Claude Code plugin route -- both npm-installer routes launch via
|
|
30
31
|
// `npx`. A renderer that resolved a template path into the skills tree at
|
|
31
32
|
// runtime would silently fail to resolve for an npm-installed user. The
|
|
@@ -41,7 +42,7 @@
|
|
|
41
42
|
// - drift detection (`checkRenderedMemoryMap()`) -- the one place a
|
|
42
43
|
// rendered file on disk is compared against what the store (plus the
|
|
43
44
|
// sidecar) would produce right now;
|
|
44
|
-
// - Markdown-cell escaping (`escapeMarkdownCell()
|
|
45
|
+
// - Markdown-cell escaping (`escapeMarkdownCell()`) --
|
|
45
46
|
// every store-derived text interpolation in the generated document
|
|
46
47
|
// (comment evidence, symbol names) is escaped through this one
|
|
47
48
|
// function, never a second ad hoc `.replace()` at a call site.
|
|
@@ -50,7 +51,7 @@
|
|
|
50
51
|
// - Never hand-edit the rendered output. The banner exists precisely so a
|
|
51
52
|
// human editor is caught by `checkRenderedMemoryMap()` -- see the
|
|
52
53
|
// `render_digest` comment below for exactly what it covers.
|
|
53
|
-
// - Never read the layout from the skills tree at runtime (
|
|
54
|
+
// - Never read the layout from the skills tree at runtime (see the module header for why).
|
|
54
55
|
// This module's own non-vacuity test asserts a zero-count grep for the
|
|
55
56
|
// recon skill's template filename -- if you are tempted to add a
|
|
56
57
|
// `readFileSync()` call reaching into `.claude/skills/`, don't; the
|
|
@@ -101,14 +102,14 @@ function errMsg(err: unknown): string {
|
|
|
101
102
|
* offset at which parsing stopped, as ` (at byte offset N)`, or `""` when the
|
|
102
103
|
* runtime did not name one.
|
|
103
104
|
*
|
|
104
|
-
* WHY THIS IS A DIGIT EXTRACTOR AND NOT A MESSAGE PASS-THROUGH
|
|
105
|
+
* WHY THIS IS A DIGIT EXTRACTOR AND NOT A MESSAGE PASS-THROUGH. V8's
|
|
105
106
|
* JSON `SyntaxError` embeds a SNIPPET OF THE INPUT in its own message --
|
|
106
107
|
* `Unexpected token 'Q', "QQZZORACLE"... is not valid JSON` -- so any code
|
|
107
108
|
* that forwards `err.message` from a JSON parse over caller-supplied bytes is
|
|
108
109
|
* a content-disclosure oracle. The capture group here is `(\d+)` and nothing
|
|
109
110
|
* else, so no byte of the parsed file can reach the returned string however
|
|
110
111
|
* the runtime words its message. Widening this regex to capture anything but
|
|
111
|
-
* digits reopens
|
|
112
|
+
* digits reopens the same content-disclosure risk.
|
|
112
113
|
*
|
|
113
114
|
* Returns `""` rather than guessing when no position is present (`Unexpected
|
|
114
115
|
* end of JSON input` carries none) -- an absent offset is reported by absence,
|
|
@@ -280,9 +281,9 @@ export function parseProvenanceHeader(json: unknown): ProvenanceHeader {
|
|
|
280
281
|
* distinguishable from drift under the same one.
|
|
281
282
|
*
|
|
282
283
|
* Version 2 (260821-a86) escaped Markdown table cells via
|
|
283
|
-
* `escapeMarkdownCell()
|
|
284
|
+
* `escapeMarkdownCell()`, an output-shape change.
|
|
284
285
|
*
|
|
285
|
-
* Version 3
|
|
286
|
+
* Version 3 is an INPUT change: `computeRenderDigest()` canonicalises
|
|
286
287
|
* this store's own `RangeRow`/`LabelRow`/`CommentRow` instead of the three
|
|
287
288
|
* wire shapes recorded above, so the same underlying annotations hash
|
|
288
289
|
* differently either side of it. Leaving the version at "2" across that
|
|
@@ -298,7 +299,7 @@ export const RENDERER_VERSION = "3";
|
|
|
298
299
|
* other leg), because comment `evidence` legitimately contains `|` and
|
|
299
300
|
* embedded newlines (`anno_set_comment`'s own schema documents multi-line
|
|
300
301
|
* support) -- refusing here would refuse valid data, not an attack. Closes
|
|
301
|
-
*
|
|
302
|
+
* this control's own render leg of T-11-NAME-INJECT: an unescaped `|` or newline in
|
|
302
303
|
* store text used to be able to inject an extra table cell or split a row
|
|
303
304
|
* across lines in the generated Markdown. A plain string or an empty string
|
|
304
305
|
* is returned unchanged. */
|
|
@@ -348,7 +349,7 @@ export interface RenderMemoryMapOptions {
|
|
|
348
349
|
* reaching an equally real `readFileSync`, carried nothing -- so a reader
|
|
349
350
|
* comparing the two would reasonably conclude the difference was
|
|
350
351
|
* deliberate. It was not: the CLI read this argument raw, making it an
|
|
351
|
-
* arbitrary-file read oracle
|
|
352
|
+
* arbitrary-file read oracle. An absent comment
|
|
352
353
|
* beside a present one is a claim, and this one was false. */
|
|
353
354
|
provenancePath: string;
|
|
354
355
|
/** The workspace root both confinement checks are taken against. REQUIRED
|
|
@@ -396,7 +397,7 @@ export async function renderMemoryMap(opts: RenderMemoryMapOptions): Promise<Ren
|
|
|
396
397
|
try {
|
|
397
398
|
sidecarJson = JSON.parse(sidecarBytes);
|
|
398
399
|
} catch (err) {
|
|
399
|
-
// NEVER INTERPOLATE THE UNDERLYING PARSE ERROR HERE
|
|
400
|
+
// NEVER INTERPOLATE THE UNDERLYING PARSE ERROR HERE. Node's
|
|
400
401
|
// SyntaxError quotes a snippet of the input it choked on -- e.g.
|
|
401
402
|
// `Unexpected token 'Q', "QQZZORACLE"... is not valid JSON` -- so passing
|
|
402
403
|
// it through turns a read refusal into a CONTENT-DISCLOSURE ORACLE. That
|
|
@@ -410,7 +411,7 @@ export async function renderMemoryMap(opts: RenderMemoryMapOptions): Promise<Ren
|
|
|
410
411
|
// where parsing stopped and not about what the file contains.
|
|
411
412
|
throw new Error(
|
|
412
413
|
`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
|
|
414
|
+
"The underlying parser message is deliberately NOT included -- it quotes the file's own bytes.",
|
|
414
415
|
);
|
|
415
416
|
}
|
|
416
417
|
const provenance = parseProvenanceHeader(sidecarJson);
|
|
@@ -460,7 +461,7 @@ export async function renderMemoryMap(opts: RenderMemoryMapOptions): Promise<Ren
|
|
|
460
461
|
// load-bearing detail rather than a formatting preference: every byte below
|
|
461
462
|
// is re-rendered and compared BYTE FOR BYTE by `checkRenderedMemoryMap()`,
|
|
462
463
|
// so an absolute path here would make the drift verdict a function of where
|
|
463
|
-
// the checkout sits
|
|
464
|
+
// the checkout sits. `workspaceRelativePath()` is the one definition
|
|
464
465
|
// of that spelling; it computes a location and refuses one that escapes the
|
|
465
466
|
// root. It is NOT a confinement check -- this module still performs no
|
|
466
467
|
// confinement of its own, exactly as `RenderMemoryMapOptions` documents.
|
|
@@ -587,13 +588,13 @@ export interface CheckRenderedMemoryMapOptions {
|
|
|
587
588
|
/** See `RenderMemoryMapOptions.provenancePath` -- same argument, one layer
|
|
588
589
|
* up. The CALLER (`anno-cli.ts`'s `cmdRenderMemmap()`) confines it through
|
|
589
590
|
* `storePathWithinWorkspace()`; this module performs no confinement of its
|
|
590
|
-
* own
|
|
591
|
+
* own. */
|
|
591
592
|
provenancePath: string;
|
|
592
593
|
/** The rendered file to compare against, read RAW by `readFileSync` below.
|
|
593
594
|
* The CALLER confines it through `storePathWithinWorkspace()` -- the SAME
|
|
594
595
|
* resolution that produces the write path on the non-`--check` branch, so
|
|
595
596
|
* the drift check and the write are one confined value rather than two
|
|
596
|
-
* rules. This module performs no confinement of its own
|
|
597
|
+
* rules. This module performs no confinement of its own. */
|
|
597
598
|
renderedPath: string;
|
|
598
599
|
/** See `RenderMemoryMapOptions.workspaceRoot`. */
|
|
599
600
|
workspaceRoot: string;
|
|
@@ -626,8 +627,8 @@ export type CheckRenderedMemoryMapResult =
|
|
|
626
627
|
* the workspace root;
|
|
627
628
|
* - a renderer change (output shape, or a `RENDERER_VERSION` bump).
|
|
628
629
|
*
|
|
629
|
-
* AND THE NEGATIVE, which is the defect this list was corrected for
|
|
630
|
-
*
|
|
630
|
+
* AND THE NEGATIVE, which is the defect this list was corrected for: relocating
|
|
631
|
+
* the checkout -- the same tree at a
|
|
631
632
|
* different absolute path -- does NOT drift. The banner records
|
|
632
633
|
* workspace-relative locations, so no compared byte is a function of where the
|
|
633
634
|
* checkout sits. Before that fix this returned `drifted` for a byte-identical
|