@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.
- package/README.md +2 -2
- package/THIRD-PARTY-NOTICES.md +422 -1
- package/anno-bank.ts +171 -0
- package/anno-cli.ts +1674 -99
- package/anno-enum-gen.ts +416 -30
- package/anno-export-asm.ts +1175 -89
- package/anno-graphics.ts +338 -0
- package/anno-hazard-report.ts +1367 -0
- package/anno-import.ts +495 -0
- package/anno-join.ts +480 -0
- package/anno-provenance-ledger.ts +472 -0
- package/anno-register.ts +159 -0
- package/anno-store-export.ts +661 -0
- package/anno-store.ts +518 -2
- package/anno-tools.ts +1169 -16
- package/anno-types.ts +275 -2
- 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 +189 -16
- package/resources/broker-epoch.mjs +1 -1
- package/resources/broker-kill.mjs +8 -2
- package/resources/broker-launch.mjs +365 -210
- package/resources/broker-state.mjs +64 -18
- package/resources/container-guard.mjs +1 -1
- package/resources/ghidra-project.mjs +790 -0
- package/resources/host-tool.mjs +2561 -0
- package/resources/vice-broker.mjs +330 -184
- 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 +189 -42
- 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/stock-run-until.ts
CHANGED
|
@@ -49,6 +49,10 @@ import { parseAddress } from "./stock-address.ts";
|
|
|
49
49
|
import { stockAnswer, isErrorText, convertWireError, type StockSessionHandler } from "./stock-handler.ts";
|
|
50
50
|
import { readProgramCounter } from "./stock-timing.ts";
|
|
51
51
|
import { runStateFor } from "./stock-runstate.ts";
|
|
52
|
+
// The reproducible-run protocol (REPRO-02). Imported for exactly one branch
|
|
53
|
+
// below -- this module is the ONLY non-test caller of runReproducible() in the
|
|
54
|
+
// tree, which is the single-seam property REPRO-02 requires.
|
|
55
|
+
import { runReproducible, REPRODUCIBLE_RUN_REQUIRED_SIBLINGS } from "./stock-reproducible-run.ts";
|
|
52
56
|
|
|
53
57
|
/** True iff `value` is a well-formed, generic JSON object -- not null, not
|
|
54
58
|
* an array. Matches this module tree's own isPlainObject() convention
|
|
@@ -74,6 +78,24 @@ export const RUN_UNTIL_DEFAULT_TIMEOUT_MS = 30000;
|
|
|
74
78
|
* value is. */
|
|
75
79
|
export const RUN_UNTIL_MAX_TIMEOUT_MS = 600000;
|
|
76
80
|
|
|
81
|
+
/** EVERY argument name `vice_run_until` accepts, and the ONLY definition of
|
|
82
|
+
* that set. Anything else is refused BY NAME (see handleRunUntil below).
|
|
83
|
+
*
|
|
84
|
+
* Hoisted to module scope and EXPORTED so `D-13`'s no-sub-flags rule is
|
|
85
|
+
* ASSERTED rather than merely stated: `stock-reproducible-run.test.ts` pins
|
|
86
|
+
* this array with a single `assert.deepEqual`, which is what makes a future
|
|
87
|
+
* `skip_reset`, `no_anchor` or `reset_only` a RED TEST rather than a review
|
|
88
|
+
* comment. Shipping a "protocol without the reset" option would ship exactly
|
|
89
|
+
* the second route `REPRO-02` exists to prevent a caller forgetting -- and the
|
|
90
|
+
* reset-removed CONTROL this phase records red is produced by an evidence
|
|
91
|
+
* script calling `runReproducible()`'s pieces directly, never by a published
|
|
92
|
+
* flag.
|
|
93
|
+
*
|
|
94
|
+
* `reproducible` and `frame_anchor` are STOCK-ONLY and both OPTIONAL: the fork
|
|
95
|
+
* manifest's tool list is frozen byte-identical from v0.1.x, and stock may add
|
|
96
|
+
* optional parameters but never removes, retypes, or newly-requires one. */
|
|
97
|
+
export const RUN_UNTIL_KEYS: readonly string[] = ["address", "cycles", "timeout_ms", "reproducible", "frame_anchor"];
|
|
98
|
+
|
|
77
99
|
/** Narrows an emitted `event` item to a CHECKPOINT_INFO event -- checked on
|
|
78
100
|
* the parsed item's own `.type` discriminant, never on response type alone
|
|
79
101
|
* (CHECKPOINT_INFO (0x11) shares a response type with a legitimate command
|
|
@@ -152,7 +174,6 @@ export const handleRunUntil: StockSessionHandler = async (args, session, _deps)
|
|
|
152
174
|
// stock-timing.ts). Accepting them silently means a typo -- `timeoutMs` for
|
|
153
175
|
// `timeout_ms`, `addr` for `address` -- runs with the DEFAULT bound and
|
|
154
176
|
// reports a confident answer, and the caller has no way to tell.
|
|
155
|
-
const RUN_UNTIL_KEYS = ["address", "cycles", "timeout_ms"];
|
|
156
177
|
const unexpectedKeys = Object.keys(args).filter((key) => !RUN_UNTIL_KEYS.includes(key));
|
|
157
178
|
if (unexpectedKeys.length > 0) {
|
|
158
179
|
return isErrorText(
|
|
@@ -223,6 +244,84 @@ export const handleRunUntil: StockSessionHandler = async (args, session, _deps)
|
|
|
223
244
|
}
|
|
224
245
|
}
|
|
225
246
|
|
|
247
|
+
// D-12: `reproducible` is a BOOLEAN. Any other type is refused naming the
|
|
248
|
+
// offending value -- never coerced. `reproducible: "false"` is truthy in
|
|
249
|
+
// JavaScript, so a coercing gate would run the whole protocol (including the
|
|
250
|
+
// hard reset) for a caller who wrote the string "false" meaning to disable
|
|
251
|
+
// it. Refusing is the only reading that cannot be wrong.
|
|
252
|
+
if (args.reproducible !== undefined && typeof args.reproducible !== "boolean") {
|
|
253
|
+
return isErrorText(
|
|
254
|
+
`vice_run_until: reproducible must be a boolean, got ${JSON.stringify(args.reproducible)} -- it is not coerced, because ` +
|
|
255
|
+
`the string "false" is truthy in JavaScript and coercing it would run the whole protocol (hard reset included) for a ` +
|
|
256
|
+
`caller who meant to disable it.`,
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// `frame_anchor` goes through parseAddress with a `what:` label, exactly as
|
|
261
|
+
// `address` does, so a malformed anchor is refused with the same wording a
|
|
262
|
+
// malformed target gets rather than a second, divergent message.
|
|
263
|
+
let frameAnchor: number | undefined;
|
|
264
|
+
if (args.frame_anchor !== undefined) {
|
|
265
|
+
try {
|
|
266
|
+
frameAnchor = parseAddress(args.frame_anchor, { what: "vice_run_until frame_anchor" });
|
|
267
|
+
} catch (err) {
|
|
268
|
+
return isErrorText(`vice_run_until: ${describeError(err)}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// An ACCEPTED-AND-IGNORED argument is the exact defect the by-name gate above
|
|
273
|
+
// exists to prevent, so `frame_anchor` without `reproducible` is refused
|
|
274
|
+
// rather than silently dropped. It is not merely inert: a caller who supplies
|
|
275
|
+
// an anchor has asked for the anchored protocol, and answering from the
|
|
276
|
+
// ordinary path would report a confident `reached: true` for a run that was
|
|
277
|
+
// never anchored to a frame at all.
|
|
278
|
+
if (args.frame_anchor !== undefined && args.reproducible !== true) {
|
|
279
|
+
return isErrorText(
|
|
280
|
+
`vice_run_until: "frame_anchor" has no meaning without "reproducible": true -- the frame anchor is armed by the ` +
|
|
281
|
+
`reproducible protocol and by nothing else, so on the ordinary path this argument would be accepted and IGNORED while ` +
|
|
282
|
+
`the answer still reported a stop. Refused rather than dropped. Pass reproducible: true to run the anchored protocol, ` +
|
|
283
|
+
`or remove "frame_anchor".`,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ---------------------------------------------------------------------
|
|
288
|
+
// THE ONE CALL SITE of the reproducible-run protocol (`REPRO-02`).
|
|
289
|
+
//
|
|
290
|
+
// One procedure, one branch, no sub-flags. `reproducible` is a
|
|
291
|
+
// WHOLE-PROCEDURE switch: absent or false takes the pre-existing path below
|
|
292
|
+
// COMPLETELY unchanged and never reaches runReproducible(). There is no
|
|
293
|
+
// second tool and no partial mode, because a second route is a route a
|
|
294
|
+
// caller can forget it took.
|
|
295
|
+
//
|
|
296
|
+
// Do not restructure the path below to "share" steps with the procedure. The
|
|
297
|
+
// reproducible sequence's ordering IS its content -- arm both while halted,
|
|
298
|
+
// hard reset, exactly one resume -- and a shared helper is how a step gets
|
|
299
|
+
// lifted out of that ordering by a later edit that looks like a cleanup.
|
|
300
|
+
// ---------------------------------------------------------------------
|
|
301
|
+
if (args.reproducible === true) {
|
|
302
|
+
// D-14's refusal, in the `cycles`-refusal register: it refuses whenever the
|
|
303
|
+
// protocol is requested without its required sibling, and it explains WHY
|
|
304
|
+
// refusing beats degrading rather than just reporting that a field is
|
|
305
|
+
// missing.
|
|
306
|
+
//
|
|
307
|
+
// The required name is read from REPRODUCIBLE_RUN_REQUIRED_SIBLINGS, not
|
|
308
|
+
// repeated as a literal here -- a second copy is how the message and the
|
|
309
|
+
// gate drift apart after a rename.
|
|
310
|
+
if (frameAnchor === undefined) {
|
|
311
|
+
return isErrorText(
|
|
312
|
+
`vice_run_until: reproducible: true requires ${REPRODUCIBLE_RUN_REQUIRED_SIBLINGS.join(", ")} -- an address executed ` +
|
|
313
|
+
`once per frame, whose hit count IS the frame term of the stop identity (PC, hit_count, (LIN, CYC)). No default is ` +
|
|
314
|
+
`supplied because the once-per-frame site is RELEASE-SPECIFIC: a cracked release almost always takes over the IRQ, ` +
|
|
315
|
+
`so no KERNAL site -- $EA31 included -- is safe to guess, and an anchor that never executes would bound out as a ` +
|
|
316
|
+
`timeout while looking like a wedge. Refusing beats silently degrading to a two-term stop identity: (LIN, CYC) is a ` +
|
|
317
|
+
`WITHIN-FRAME position, not a monotonic clock, so without the frame term two stops one whole frame apart certify as ` +
|
|
318
|
+
`the same stop. Pass frame_anchor, or drop reproducible to run the ordinary unanchored path.`,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
const { result } = await runReproducible(session, { address, frameAnchor, timeoutMs, timeoutClamped });
|
|
322
|
+
return result;
|
|
323
|
+
}
|
|
324
|
+
|
|
226
325
|
// Arm a temporary, stopping exec checkpoint at `address`. This is this
|
|
227
326
|
// codebase's first caller in this tree to pass the temporary flag as
|
|
228
327
|
// true: VICE itself auto-deletes a temporary checkpoint the instant it
|
package/stock-symbols.ts
CHANGED
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
// Node's `fs` inside the MCP server's OWN process; there is no wire
|
|
20
20
|
// filename argument at all, so the translation does not apply and applying
|
|
21
21
|
// it would read the wrong file (or nothing). hostpath-consumers.test.ts's
|
|
22
|
-
// closed
|
|
23
|
-
// install-resources.ts, stock-paths.ts, vice-proxy.ts
|
|
24
|
-
//
|
|
22
|
+
// closed four-member production consumer set (containerpath.ts,
|
|
23
|
+
// install-resources.ts, stock-paths.ts, vice-proxy.ts) must stay exactly
|
|
24
|
+
// four -- this module joining it would fail that test outright.
|
|
25
25
|
//
|
|
26
26
|
// The confirmed input format is a VICE label file, one `al C:xxxx .Name`
|
|
27
27
|
// line per symbol, verified against ACME's `--vicelabels` output via
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
import { readFileSync, realpathSync, statSync } from "node:fs";
|
|
54
54
|
import { resolve, sep } from "node:path";
|
|
55
55
|
|
|
56
|
-
import { ViceError, type ViceErrorOptions } from "./vice.ts";
|
|
56
|
+
import { ViceError, type ViceErrorOptions } from "./vice-errors.ts";
|
|
57
57
|
import { repoRoot } from "./repo-root.ts";
|
|
58
58
|
import { parseAddress, setSymbolResolver, type SymbolResolver } from "./stock-address.ts";
|
|
59
59
|
import { derivedAnswer, isErrorText } from "./stock-handler.ts";
|
package/stock-timing.ts
CHANGED
|
@@ -45,7 +45,7 @@ import {
|
|
|
45
45
|
type ParsedCpuHistoryEntry,
|
|
46
46
|
type StockProtocolError,
|
|
47
47
|
} from "./stock-protocol.ts";
|
|
48
|
-
import { MachineRestartedError } from "./vice.ts";
|
|
48
|
+
import { MachineRestartedError } from "./vice-errors.ts";
|
|
49
49
|
import { clampCpuHistoryCount, type StockConnectSession } from "./stock-connect.ts";
|
|
50
50
|
import { registerCatalogFor } from "./stock-registers.ts";
|
|
51
51
|
import { stockAnswer, convertWireError, isErrorText, type StockSessionHandler, type StockOkResult } from "./stock-handler.ts";
|
package/stop-oracle.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// stop-oracle.ts -- the ONE authoritative place in this repo holding the
|
|
2
|
+
// STOP-IDENTITY comparison: whether two stops of the same protocol, on the same
|
|
3
|
+
// release, are the SAME stop. The oracle is the three-term
|
|
4
|
+
// `(PC, hit_count, (LIN, CYC))`, expressed here as its four scalar terms.
|
|
5
|
+
//
|
|
6
|
+
// WHAT THIS MODULE CERTIFIES, AND WHY THAT DETERMINES ITS SHAPE: it certifies a
|
|
7
|
+
// STOP. The captured 64K is the DEPENDENT VARIABLE -- the thing whose
|
|
8
|
+
// reproducibility is being claimed on the strength of the stop being identical.
|
|
9
|
+
// So the capture must never become a conjunct of the stop's own identity. If it
|
|
10
|
+
// did, two runs that stopped in genuinely different places could be certified
|
|
11
|
+
// "the same stop" because their memory happened to agree, and a capture would be
|
|
12
|
+
// participating in certifying itself. That circularity, once it has certified a
|
|
13
|
+
// capture, cannot be laundered back out of that capture's record -- which is why
|
|
14
|
+
// it is barred by SHAPE rather than by convention (`CAP-03`, `D-26`):
|
|
15
|
+
//
|
|
16
|
+
// 1. This module takes NO image buffer. No exported function here declares a
|
|
17
|
+
// byte-array parameter of any kind, and `capture-seam.test.ts` asserts that
|
|
18
|
+
// from this module's own exported signatures, over stripped code, with a
|
|
19
|
+
// planted oracle-shaped module as its positive control.
|
|
20
|
+
// 2. This module imports NOTHING from `capture-predicate.ts`, on any route,
|
|
21
|
+
// static or dynamic -- and that module imports nothing from here. Both
|
|
22
|
+
// directions are asserted, because the circularity is symmetric and a
|
|
23
|
+
// one-directional guard leaves half of it open.
|
|
24
|
+
//
|
|
25
|
+
// This module performs NO filesystem and NO network I/O, imports nothing at all,
|
|
26
|
+
// and holds no module-level mutable state. It is four scalars in, one record
|
|
27
|
+
// out.
|
|
28
|
+
//
|
|
29
|
+
// WHY FOUR TERMS AND NOT TWO: `(LIN, CYC)` is a WITHIN-FRAME position, not a
|
|
30
|
+
// monotonic clock -- stock VICE's binary monitor has no monotonic cycle register
|
|
31
|
+
// below 3.10 -- so two stops one whole frame apart can carry identical
|
|
32
|
+
// `(LIN, CYC)`. `PC` and `hit_count` are what distinguish them, and the frame
|
|
33
|
+
// term is what distinguishes two stops at the same PC and hit count within a
|
|
34
|
+
// frame. Each term is load-bearing for a different confusion, and dropping any
|
|
35
|
+
// one of them silently widens what counts as "the same stop".
|
|
36
|
+
//
|
|
37
|
+
// WHAT NOT TO DO:
|
|
38
|
+
// - Never add a parameter carrying capture bytes, a byte array, a memory
|
|
39
|
+
// image, or a value DERIVED from one (a digest, a differing-address count,
|
|
40
|
+
// an equivalence verdict). The derived-scalar route is the realistic one and
|
|
41
|
+
// the structural assertions cannot see it: a caller that reads the capture
|
|
42
|
+
// and passes a digest in here has reintroduced the circularity through a
|
|
43
|
+
// `number`. It is recorded as an accepted limit rather than left implicit.
|
|
44
|
+
// - Never import `capture-predicate.ts` here, and never reach it dynamically.
|
|
45
|
+
// - Never ship a two-term mode flag. `GATE-01`'s pre-mapped
|
|
46
|
+
// `ORACLE_NECESSITY: unproven` narrowing is implementable by READING
|
|
47
|
+
// `differingTerms` and treating the frame entries as recorded-not-fatal;
|
|
48
|
+
// `frameTermAsserted` records which form the comparison used and does not
|
|
49
|
+
// select one. `D-13`'s no-sub-flags rule is about the run protocol, and the
|
|
50
|
+
// same reasoning applies here -- a published second mode is a second route a
|
|
51
|
+
// caller can forget it took.
|
|
52
|
+
// - Never substitute a zero, a null or a sentinel for a term the caller did
|
|
53
|
+
// not supply. A partial stop record is REFUSED, naming the term and the
|
|
54
|
+
// side. Silently passing on a partial record is the one failure this module
|
|
55
|
+
// must not have: it would certify two stops as identical on the strength of
|
|
56
|
+
// the terms that happened to be present.
|
|
57
|
+
// - Never hardcode a wire register id for `PC`, `LIN` or `CYC` anywhere near
|
|
58
|
+
// this module. Register ids are not stable across builds, and
|
|
59
|
+
// `stock-timing.ts`'s `registerCatalogFor()` is the only route from a
|
|
60
|
+
// register NAME to its id. This module takes already-read numbers and never
|
|
61
|
+
// touches a wire.
|
|
62
|
+
|
|
63
|
+
/** The four scalar terms of the three-term stop-identity oracle
|
|
64
|
+
* `(PC, hit_count, (LIN, CYC))`. The frame term is a pair, which is why four
|
|
65
|
+
* scalars express three terms.
|
|
66
|
+
*
|
|
67
|
+
* Ordering is normative: `differingTerms` below is reported in this order, so a
|
|
68
|
+
* reader comparing two transcripts sees the terms in the same sequence every
|
|
69
|
+
* time. */
|
|
70
|
+
export const ORACLE_TERMS = ["pc", "hitCount", "line", "cycle"] as const;
|
|
71
|
+
|
|
72
|
+
/** One member of `ORACLE_TERMS`. */
|
|
73
|
+
export type OracleTerm = (typeof ORACLE_TERMS)[number];
|
|
74
|
+
|
|
75
|
+
/** One stop, as its four already-read scalar terms. No bytes, by design -- see
|
|
76
|
+
* this module's header. */
|
|
77
|
+
export interface StopIdentity {
|
|
78
|
+
/** The program counter at the stop. */
|
|
79
|
+
pc: number;
|
|
80
|
+
/** The checkpoint's hit count at the stop. */
|
|
81
|
+
hitCount: number;
|
|
82
|
+
/** `LIN` -- the raster line. Half of the frame term. */
|
|
83
|
+
line: number;
|
|
84
|
+
/** `CYC` -- the cycle within the raster line. The other half. */
|
|
85
|
+
cycle: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Raised when a stop record cannot be compared. Carries the term and the side
|
|
89
|
+
* so a caller's failure output names both rather than paraphrasing. */
|
|
90
|
+
export class StopOracleError extends Error {
|
|
91
|
+
readonly term: string;
|
|
92
|
+
readonly side: string;
|
|
93
|
+
constructor(message: string, term: string, side: string) {
|
|
94
|
+
super(message);
|
|
95
|
+
this.name = "StopOracleError";
|
|
96
|
+
this.term = term;
|
|
97
|
+
this.side = side;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The verdict. */
|
|
102
|
+
export interface StopIdentityComparison {
|
|
103
|
+
/** True iff every term in `ORACLE_TERMS` agrees. */
|
|
104
|
+
identical: boolean;
|
|
105
|
+
/** Which terms differ, ordered as `ORACLE_TERMS` is. */
|
|
106
|
+
differingTerms: string[];
|
|
107
|
+
/** Whether the frame term participated in the verdict.
|
|
108
|
+
*
|
|
109
|
+
* `true` for every comparison this function performs: all four terms are
|
|
110
|
+
* asserted, always. The field exists so `GATE-01`'s pre-mapped
|
|
111
|
+
* `ORACLE_NECESSITY: unproven` narrowing is implementable without a rewrite --
|
|
112
|
+
* under that narrowing the frame term is RECORDED but not ASSERTED, and a
|
|
113
|
+
* caller expresses it by reading `differingTerms` and treating the
|
|
114
|
+
* `line`/`cycle` entries as recorded rather than fatal. It RECORDS which form
|
|
115
|
+
* the comparison used; it never selects one. */
|
|
116
|
+
frameTermAsserted: boolean;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Every term of one side, validated. Throws naming the term and the side
|
|
120
|
+
* rather than returning a partial record: there is no honest way to compare
|
|
121
|
+
* stops when a term is absent, and no sentinel that would not be a lie. */
|
|
122
|
+
function requireTerms(record: StopIdentity, side: string): void {
|
|
123
|
+
if (record === null || typeof record !== "object") {
|
|
124
|
+
throw new StopOracleError(
|
|
125
|
+
`compareStopIdentity: side ${side} is not a stop record -- expected an object carrying ${ORACLE_TERMS.join(", ")}`,
|
|
126
|
+
"(whole record)",
|
|
127
|
+
side,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
for (const term of ORACLE_TERMS) {
|
|
131
|
+
const value: unknown = record[term];
|
|
132
|
+
if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value)) {
|
|
133
|
+
throw new StopOracleError(
|
|
134
|
+
`compareStopIdentity: term "${term}" is absent or not a finite integer on side ${side} ` +
|
|
135
|
+
`(got ${JSON.stringify(value)}) -- refusing rather than passing on a partial stop record, which ` +
|
|
136
|
+
`would certify two stops as identical on the strength of the terms that happened to be present`,
|
|
137
|
+
term,
|
|
138
|
+
side,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Compare two stops under the three-term oracle.
|
|
145
|
+
*
|
|
146
|
+
* Symmetric in its arguments: the verdict and `differingTerms` depend only on
|
|
147
|
+
* WHICH terms disagree, never on which record was passed first. Two stops with
|
|
148
|
+
* identical `pc` and `hitCount` whose frame position is one frame apart compare
|
|
149
|
+
* NOT identical -- that is the frame term doing its job, and it is the whole
|
|
150
|
+
* reason a two-term oracle is insufficient. */
|
|
151
|
+
export function compareStopIdentity(a: StopIdentity, b: StopIdentity): StopIdentityComparison {
|
|
152
|
+
requireTerms(a, "a");
|
|
153
|
+
requireTerms(b, "b");
|
|
154
|
+
|
|
155
|
+
// Built by walking ORACLE_TERMS, so the reported order IS the declared order
|
|
156
|
+
// and cannot drift from it after an edit.
|
|
157
|
+
const differingTerms: string[] = [];
|
|
158
|
+
for (const term of ORACLE_TERMS) {
|
|
159
|
+
if (a[term] !== b[term]) differingTerms.push(term);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
identical: differingTerms.length === 0,
|
|
164
|
+
differingTerms,
|
|
165
|
+
frameTermAsserted: true,
|
|
166
|
+
};
|
|
167
|
+
}
|