@henols/vice-mcp 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +2 -2
  2. package/THIRD-PARTY-NOTICES.md +422 -1
  3. package/anno-bank.ts +171 -0
  4. package/anno-cli.ts +1674 -99
  5. package/anno-enum-gen.ts +416 -30
  6. package/anno-export-asm.ts +1175 -89
  7. package/anno-graphics.ts +338 -0
  8. package/anno-hazard-report.ts +1367 -0
  9. package/anno-import.ts +495 -0
  10. package/anno-join.ts +480 -0
  11. package/anno-provenance-ledger.ts +472 -0
  12. package/anno-register.ts +159 -0
  13. package/anno-store-export.ts +661 -0
  14. package/anno-store.ts +518 -2
  15. package/anno-tools.ts +1169 -16
  16. package/anno-types.ts +275 -2
  17. package/backend-detect.mts +124 -312
  18. package/build.ts +3 -1
  19. package/capture-predicate.ts +597 -0
  20. package/channel-lock.ts +349 -0
  21. package/evid-ingest.ts +217 -0
  22. package/evid-reconcile.ts +316 -0
  23. package/host-tool-client.ts +430 -0
  24. package/incident-record.ts +23 -12
  25. package/install-resources.ts +29 -13
  26. package/memmap-lookup.ts +285 -0
  27. package/package.json +27 -8
  28. package/prg-image.ts +1 -2
  29. package/repo-root.ts +87 -3
  30. package/resources/backend-detect.mjs +98 -236
  31. package/resources/broker-control.mjs +189 -16
  32. package/resources/broker-epoch.mjs +1 -1
  33. package/resources/broker-kill.mjs +8 -2
  34. package/resources/broker-launch.mjs +365 -210
  35. package/resources/broker-state.mjs +64 -18
  36. package/resources/container-guard.mjs +1 -1
  37. package/resources/ghidra-project.mjs +790 -0
  38. package/resources/host-tool.mjs +2561 -0
  39. package/resources/vice-broker.mjs +330 -184
  40. package/resources/vice-launcher.sh +127 -9
  41. package/stock-address.ts +1 -1
  42. package/stock-condition.ts +1 -1
  43. package/stock-connect.ts +9 -5
  44. package/stock-derived.ts +29 -37
  45. package/stock-diagnose.ts +200 -36
  46. package/stock-dispatch.ts +179 -77
  47. package/stock-handler.ts +1 -1
  48. package/stock-paths.ts +18 -14
  49. package/stock-petscii.ts +1 -1
  50. package/stock-protocol.ts +1 -1
  51. package/stock-recycle.ts +83 -2
  52. package/stock-reproducible-run.ts +811 -0
  53. package/stock-run-until.ts +100 -1
  54. package/stock-symbols.ts +4 -4
  55. package/stock-timing.ts +1 -1
  56. package/stop-oracle.ts +167 -0
  57. package/text-capability-probe.ts +660 -0
  58. package/text-connect.ts +157 -0
  59. package/text-protocol.ts +810 -0
  60. package/text-tools.ts +778 -0
  61. package/textmon-backtrace.ts +385 -0
  62. package/textmon-cpuhistory.ts +335 -0
  63. package/textmon-memmap.ts +494 -0
  64. package/textmon-profile.ts +458 -0
  65. package/textmon-registers.ts +748 -0
  66. package/tools-manifest.stock.json +864 -3
  67. package/vice-broker-client.ts +189 -42
  68. package/vice-errors.ts +268 -0
  69. package/vice-proxy.ts +339 -2144
  70. package/vsf-slice.ts +640 -0
  71. package/anno-d64.ts +0 -310
  72. package/capability-registry.ts +0 -390
  73. package/refresh-manifest.ts +0 -124
  74. package/tools-manifest.json +0 -1223
  75. package/vice-probe.ts +0 -278
  76. package/vice-sync.ts +0 -336
  77. package/vice.ts +0 -772
@@ -0,0 +1,316 @@
1
+ #!/usr/bin/env node
2
+ // evid-reconcile.ts
3
+ //
4
+ // Plan 43-04 (EVID-03, EVID-04): the ONE place that joins the byte-derived
5
+ // block table (`block-class.ts`'s own vocabulary, read through `blockClassAt`)
6
+ // against the runtime evidence table (`anno_evid_exec`, plan 43-02's
7
+ // `EvidExecRow`) and reports where the two independent classifiers disagree.
8
+ // Shaped exactly like `dxa-proof01-compare.ts` (Phase 38, plan 38-01): two
9
+ // already-fetched classifications in, buckets with an explicit denominator
10
+ // and named third/fourth buckets out, nothing fetched and nothing mutated.
11
+ //
12
+ // WHY THIS FILE EXISTS: an address the byte-derived block table calls `data`
13
+ // AND the emulator was observed executing is the single highest-value output
14
+ // this whole evidence layer can produce -- proof that a byte-derived guess
15
+ // was wrong, from a source (real execution) that never saw the guess. That
16
+ // value only exists because the two classifiers are independent; the moment
17
+ // this module compares a block-type string itself, or folds absence into a
18
+ // classification, both requirements this plan settles are void even though
19
+ // every existing test keeps passing.
20
+ //
21
+ // THE FOUR BUCKETS, AND WHY THE THIRD AND FOURTH ARE NOT OPTIONAL (EVID-03):
22
+ // 1. disagreement -- block class is `data`, evidence holds an observed
23
+ // execute. Reported FIRST, as rows. This is the whole point of the query.
24
+ // 2. agreement -- block class is `code`, evidence holds an observed
25
+ // execute. Reported as a COUNT ONLY. A wall of agreeing rows here would
26
+ // bury bucket 1.
27
+ // 3. block-covered, never observed -- the block table classifies the
28
+ // address and NO run ever observed it executing. Neither agreement nor
29
+ // disagreement: absence proves nothing (EVID-04), and folding this
30
+ // population into either bucket would be exactly the soundness
31
+ // violation EVID-04 forbids.
32
+ // 4. observed outside any block / observed at an explicitly `undefined`
33
+ // block -- evidence about an address the block table does not classify
34
+ // as code or data at all. Neither agreement nor disagreement either;
35
+ // named and counted so the denominator can never quietly drop real
36
+ // evidence and lie about what it accounts for.
37
+ //
38
+ // UNION-ACROSS-RUNS PROHIBITION, STATED HERE ONCE FOR THE WHOLE MODULE
39
+ // (EVID-04): however many runs contribute observations to `observations`,
40
+ // their union is NEVER exhaustive and NEVER complete coverage of the image.
41
+ // No field this module returns may be read as "the rest is data" -- the four
42
+ // buckets are the structural reason: summing them tells a reader what the
43
+ // block table covers, never what the program actually is. An address absent
44
+ // from every run's observations is `blockCoveredNeverObservedCount` (if the
45
+ // block table covers it) or simply outside `denominator` altogether (if it
46
+ // does not) -- in neither case is it, or does it become, `data`.
47
+ //
48
+ // ---------------------------------------------------------------------------
49
+ // WHAT NOT TO DO -- each of these is a specific, named trap
50
+ // ---------------------------------------------------------------------------
51
+ // 1. NEVER fetch either side here. `blocks` and `observations` arrive as
52
+ // plain data the caller already fetched (`listRanges()` and
53
+ // `listExecObservations()` respectively); this module never opens a
54
+ // store, reaches a transport, or names a filesystem/child-process
55
+ // specifier. A structural source assertion in this module's own test
56
+ // file bars exactly that.
57
+ // 2. NEVER compare a store block-type string here. That is `block-class.ts`'s
58
+ // job and its own trap 2 ("never compare a store vocabulary string
59
+ // outside this module") -- this module borrows `blockClassAt`'s answer
60
+ // through the injectable `classifier` parameter and never reads
61
+ // `BlockEntry.type` itself.
62
+ // 3. NEVER return a percentage, rate, ratio or score. Every count this
63
+ // module returns carries `denominator` as the fraction it is relative
64
+ // to; a consumer that wants a rate forms it from the two. There is no
65
+ // rounding site in this module for exactly that reason.
66
+ // 4. NEVER let "no row" and "observed not executing" collide. A row in
67
+ // `observations` exists only for an observed execute (plan 43-05's own
68
+ // discipline) -- there is no "observed but not executing" state this
69
+ // module's input can even represent, so this module never invents one.
70
+ // 5. NEVER derive `data` from absence. `blockCoveredNeverObservedCount` is
71
+ // the ONLY thing this module may say about the never-observed
72
+ // population: a count against a named denominator, never a class. See
73
+ // that field's own doc comment below.
74
+ import { blockClassAt, type BlockClass, type BlockEntry, type BlockClassifier } from "./block-class.ts";
75
+ import type { EvidExecRow, EvidSourceBank, RuntimeExecClass } from "./anno-types.ts";
76
+
77
+ /**
78
+ * Plain data the caller already fetched -- this module never fetches either
79
+ * side itself (trap 1 above). `blocks` is `listRanges()`'s own byte-derived
80
+ * listing; `observations` is `listExecObservations()`'s own runtime-evidence
81
+ * rows. `classifier` defaults to the one production interpreter of a store
82
+ * block-type string, `blockClassAt` -- the exact
83
+ * `typeof opts.x === "function" ? opts.x : blockClassAt` guard
84
+ * `anno-coverage.ts` already uses for the identical substitutability
85
+ * discipline, so a test can drive a substituted vocabulary through the real
86
+ * code path rather than a copy of it.
87
+ */
88
+ export interface EvidReconcileInput {
89
+ blocks: readonly BlockEntry[];
90
+ observations: readonly EvidExecRow[];
91
+ classifier?: BlockClassifier;
92
+ }
93
+
94
+ /**
95
+ * One disagreement row: the byte-derived tier said `data`, the runtime
96
+ * evidence layer holds at least one observed execute at that address.
97
+ * `byteDerived` and `runtime` are fixed to `"data"`/`"code"` for every member
98
+ * of this bucket by construction (a row only ever lands here on that exact
99
+ * pairing) -- carried as fields anyway so a renderer never has to re-derive
100
+ * what bucket it is looking at from context.
101
+ */
102
+ export interface EvidDisagreement {
103
+ address: number;
104
+ /** Always `"data"` for a member of this bucket. */
105
+ byteDerived: BlockClass;
106
+ /** Always `"code"` for a member of this bucket. */
107
+ runtime: RuntimeExecClass;
108
+ /** Sorted ascending and de-duplicated, so a reader can see at a glance
109
+ * whether the execute bit came from RAM, ROM or IO space. */
110
+ sourceBanks: readonly EvidSourceBank[];
111
+ }
112
+
113
+ /**
114
+ * The join's result. `disagreements` is declared and returned FIRST --
115
+ * EVID-03's own requirement that disagreement is reported before agreement,
116
+ * never buried under it. `agreementCount` is a NUMBER; there is no
117
+ * agreement-row array anywhere in this shape, on purpose (a wall of agreeing
118
+ * rows would bury the one output this whole query exists to surface).
119
+ *
120
+ * Every count field here is a fraction of `denominator`, and this type has
121
+ * no field that is, or that could be mistaken for, a percentage, rate, ratio
122
+ * or score (trap 3 above).
123
+ */
124
+ export interface EvidReconciliation {
125
+ /** The disagreements. FIRST key, both declared and returned -- EVID-03. */
126
+ disagreements: EvidDisagreement[];
127
+ /** `disagreements.length`, exposed as its own field so a caller never has
128
+ * to re-derive a count from an array it may not want to hold onto. */
129
+ disagreementCount: number;
130
+ /** Count only -- see this interface's own doc comment for why there is no
131
+ * corresponding row array. */
132
+ agreementCount: number;
133
+ /**
134
+ * The block table classifies this address (either `code` or `data` or
135
+ * `undefined`) and NO run's observations ever recorded it executing. This
136
+ * count is the ONLY thing this module may say about that population -- a
137
+ * count against `denominator`, never a class -- because an address never
138
+ * observed executing proves nothing about whether it is code or data
139
+ * (EVID-04, and the same discipline `textmon-memmap.ts` states for itself:
140
+ * "never classify an address as data on the strength of never having been
141
+ * observed"). Neither agreement nor disagreement folds this population in.
142
+ */
143
+ blockCoveredNeverObservedCount: number;
144
+ /**
145
+ * An observed execute at an address NO block covers at all
146
+ * (`classifier` returned `null`). This exists so the denominator cannot
147
+ * lie: an observation the block table does not cover is real evidence
148
+ * about an unclassified address, and silently dropping it would leave a
149
+ * reader believing every observation this module saw was accounted for by
150
+ * `denominator` when it was not.
151
+ */
152
+ observedOutsideAnyBlockCount: number;
153
+ /**
154
+ * An observed execute at an address the block table explicitly marks
155
+ * `undefined`. An explicitly-undefined block claimed nothing about that
156
+ * address, so an observation there is neither agreement nor disagreement
157
+ * -- it joins `blockCoveredNeverObservedCount`'s sibling count under its
158
+ * own name rather than being silently absorbed into either bucket.
159
+ */
160
+ observedAtUndefinedBlockCount: number;
161
+ /**
162
+ * The count of distinct addresses the block table covers -- what every
163
+ * other count on this object is a fraction of. Computed here from the
164
+ * blocks' own inclusive ranges, never copied from an input or a literal.
165
+ * No rate is ever formed from it in this module (trap 3 above), so a zero
166
+ * denominator (an empty `blocks` array) is a legal input, never a refusal.
167
+ */
168
+ denominator: number;
169
+ /** Always `"code"` -- the runtime evidence layer's only positive class. */
170
+ positiveClass: "code";
171
+ /** Always `"runtime-observed"` -- this reconciler only ever joins against
172
+ * the runtime-observed tier. */
173
+ tier: "runtime-observed";
174
+ }
175
+
176
+ /** De-duplicates and sorts a source-bank list ascending. Never insertion
177
+ * order, which would depend on the order observation rows happened to
178
+ * arrive in and could differ across two logically-identical inputs built by
179
+ * different code paths. */
180
+ function sortSourceBanksAscending(banks: readonly EvidSourceBank[]): EvidSourceBank[] {
181
+ return [...new Set(banks)].sort();
182
+ }
183
+
184
+ /** Sorts disagreement rows by address ascending. Same determinism
185
+ * discipline as `sortSourceBanksAscending`, applied to the one row array
186
+ * this module returns. */
187
+ function sortDisagreementsAscending(disagreements: readonly EvidDisagreement[]): EvidDisagreement[] {
188
+ return [...disagreements].sort((a, b) => a.address - b.address);
189
+ }
190
+
191
+ /**
192
+ * The join. For every address the block table covers (derived from the
193
+ * blocks' own inclusive ranges, both ends, never from a hardcoded 65536):
194
+ * `disagreements` when the classifier says `data` and an observation exists;
195
+ * `agreementCount` when it says `code` and an observation exists;
196
+ * `blockCoveredNeverObservedCount` when NO observation exists, regardless of
197
+ * class. A covered address whose class is `undefined` (or, defensively, an
198
+ * address a substituted classifier fails to classify at all) that DOES carry
199
+ * an observation falls through this first walk untouched, and is picked up
200
+ * by the second walk below as `observedAtUndefinedBlockCount` -- never here,
201
+ * so it can never be miscounted as "never observed".
202
+ *
203
+ * A second walk, over the observation side alone, then counts every observed
204
+ * address the first walk did not already account for: `classifier` returning
205
+ * `null` (no block covers it at all) becomes `observedOutsideAnyBlockCount`;
206
+ * `classifier` returning `"undefined"` becomes `observedAtUndefinedBlockCount`.
207
+ * An observed address the classifier calls `code` or `data` is skipped here
208
+ * -- it was already counted in the first walk, and counting it twice would
209
+ * break the identity that every block-covered address falls into EXACTLY one
210
+ * of `disagreementCount` / `agreementCount` / `blockCoveredNeverObservedCount`
211
+ * / `observedAtUndefinedBlockCount`.
212
+ *
213
+ * Never mutates `input.blocks`, `input.observations`, or any of their
214
+ * elements -- both are read only, and no `BlockEntry` is ever constructed
215
+ * from an `EvidExecRow` or vice versa.
216
+ */
217
+ export function reconcileObservedExecution(input: EvidReconcileInput): EvidReconciliation {
218
+ const classifier: BlockClassifier = typeof input.classifier === "function" ? input.classifier : blockClassAt;
219
+ const blocks: readonly BlockEntry[] = Array.isArray(input.blocks) ? input.blocks : [];
220
+ const observations: readonly EvidExecRow[] = Array.isArray(input.observations) ? input.observations : [];
221
+
222
+ // One pass over the observations: address -> every source bank an
223
+ // observation named at that address, in whatever order the rows arrived.
224
+ // De-duplicated and sorted only where a caller actually receives the list
225
+ // (`EvidDisagreement.sourceBanks`), never here.
226
+ const observationBanks = new Map<number, EvidSourceBank[]>();
227
+ for (const row of observations) {
228
+ if (!row) continue;
229
+ const existing = observationBanks.get(row.address);
230
+ if (existing) {
231
+ existing.push(row.sourceBank);
232
+ } else {
233
+ observationBanks.set(row.address, [row.sourceBank]);
234
+ }
235
+ }
236
+
237
+ // The addresses the block table covers -- a `null`/`undefined` hole in
238
+ // `blocks` is skipped, exactly as `blockClassAt` itself skips one, so this
239
+ // module's own defences match the classifier's documented ones (trap 2's
240
+ // boundary is the vocabulary comparison, not this geometric coverage
241
+ // check, which every well-behaved `BlockClassifier` -- production or
242
+ // substituted -- agrees on: both ends inclusive, first-match-wins never
243
+ // changes which addresses are covered, only which class they map to).
244
+ const coveredAddresses = new Set<number>();
245
+ for (const block of blocks) {
246
+ if (!block) continue;
247
+ const start = block.start_address;
248
+ const end = block.end_address;
249
+ if (!Number.isInteger(start) || !Number.isInteger(end)) continue;
250
+ for (let address = start; address <= end; address++) {
251
+ coveredAddresses.add(address);
252
+ }
253
+ }
254
+
255
+ const disagreements: EvidDisagreement[] = [];
256
+ let disagreementCount = 0;
257
+ let agreementCount = 0;
258
+ let blockCoveredNeverObservedCount = 0;
259
+
260
+ for (const address of coveredAddresses) {
261
+ const byteDerived = classifier(blocks, address);
262
+ const banks = observationBanks.get(address);
263
+ const hasObservation = banks !== undefined && banks.length > 0;
264
+
265
+ if (byteDerived === "data") {
266
+ if (hasObservation) {
267
+ disagreements.push({
268
+ address,
269
+ byteDerived: "data",
270
+ runtime: "code",
271
+ sourceBanks: sortSourceBanksAscending(banks!),
272
+ });
273
+ disagreementCount++;
274
+ } else {
275
+ blockCoveredNeverObservedCount++;
276
+ }
277
+ } else if (byteDerived === "code") {
278
+ if (hasObservation) {
279
+ agreementCount++;
280
+ } else {
281
+ blockCoveredNeverObservedCount++;
282
+ }
283
+ } else if (!hasObservation) {
284
+ // byteDerived is "undefined", or (defensively) a substituted classifier
285
+ // failed to classify a geometrically-covered address at all. Neither
286
+ // case was ever claimed as code or data, so an UNOBSERVED address here
287
+ // is exactly the never-observed population -- and an OBSERVED one is
288
+ // deliberately left uncounted here; the walk below names it instead.
289
+ blockCoveredNeverObservedCount++;
290
+ }
291
+ }
292
+
293
+ let observedOutsideAnyBlockCount = 0;
294
+ let observedAtUndefinedBlockCount = 0;
295
+ for (const address of observationBanks.keys()) {
296
+ const byteDerived = classifier(blocks, address);
297
+ if (byteDerived === null) {
298
+ observedOutsideAnyBlockCount++;
299
+ } else if (byteDerived === "undefined") {
300
+ observedAtUndefinedBlockCount++;
301
+ }
302
+ // "code" and "data" were already counted in the walk above.
303
+ }
304
+
305
+ return {
306
+ disagreements: sortDisagreementsAscending(disagreements),
307
+ disagreementCount,
308
+ agreementCount,
309
+ blockCoveredNeverObservedCount,
310
+ observedOutsideAnyBlockCount,
311
+ observedAtUndefinedBlockCount,
312
+ denominator: coveredAddresses.size,
313
+ positiveClass: "code",
314
+ tier: "runtime-observed",
315
+ };
316
+ }